[Add] Terrain lighting.

* Chunks are now rendered as solid, lit meshes instead of wireframe.
* The GLSL shaders have been updated to a directional lighting model.
* Chunkmes now calculates normal vectors for each vertex to enable the
  lighting.
* Shader class now has a 'set_vec3' method for sending lighting date to
  the GPU.

Bug Fix:
* Resolves visual artifacts and "seams" at chunk boundaries.

Next up:
* To calculate the correct angle for a vertex at the edge of chunk A, we
  need to know the height of the terrain in the neighboiring Chunk B.
Since it doesn't have that information, the GPU's making an appoximate
guess. We need to generate and send a small "border" of height data
along with each chunk, I'll do this by increasing the size of the
heightmap array in the network message so it can hold the 32x32 chunk
plus 1 vertex border all around. MAking it 34x34 for each chunk.
This commit is contained in:
Ritchie Cunningham 2025-09-16 23:58:03 +01:00
parent 744c41b8ce
commit fa7159587d
11 changed files with 90 additions and 41 deletions

View File

@ -1,9 +1,23 @@
#version 330 core
out vec4 FragColor;
uniform vec3 overrideColor;
in vec3 FragPos;
in vec3 Normal;
uniform vec3 objectColor;
uniform vec3 lightColor;
uniform vec3 lightDir; /* Normalised direction vector for the light source. */
void main() {
FragColor = vec4(overrideColor, 1.0f);
/* Ambient lighting (cheap, constant base light) */
float ambientStrength = 0.3;
vec3 ambient = ambientStrength * lightColor;
/* Diffuse lighting (depends on angle of the surface) */
vec3 norm = normalize(Normal);
float diff = max(dot(norm, normalize(-lightDir)), 0.0);
vec3 diffuse = diff * lightColor;
vec3 result = (ambient + diffuse) * objectColor;
FragColor = vec4(result, 1.0);
}

View File

@ -1,11 +1,18 @@
#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aNormal;
out vec3 FragPos;
out vec3 Normal;
uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;
void main() {
gl_Position = projection * view * model * vec4(aPos, 1.0);
/* Pass position and normal vectors to the fragment shader in world space. */
FragPos = vec3(model * vec4(aPos, 1.0));
Normal = mat3(transpose(inverse(model))) * aNormal;
gl_Position = projection * view * vec4(FragPos, 1.0);
}

View File

@ -146,6 +146,7 @@ void GameClient::_process_game_state(const BettolaLib::Network::GameStateMessage
BettolaMath::Vec3 cam_front = { input_msg.cam_front_x, input_msg.cam_front_y,
input_msg.cam_front_z };
_player.set_velocity_direction(input_state, cam_front);
_player.update(input_msg.dt);
}
} else {
/* Remote player. Find if we already know about them.. */

View File

@ -1,5 +1,8 @@
#include <GL/glew.h>
#include <cmath>
#include <cstdio>
#include "bettola/game/chunk.h"
#include "bettola/math/vec3.h"
#include "chunk_mesh.h"
ChunkMesh::ChunkMesh(int chunk_x, int chunk_z, const BettolaLib::Game::Chunk& chunk) {
@ -10,6 +13,12 @@ ChunkMesh::ChunkMesh(int chunk_x, int chunk_z, const BettolaLib::Game::Chunk& ch
_vertices.push_back((float)(chunk_x * (BettolaLib::Game::CHUNK_WIDTH-1) + x));
_vertices.push_back(chunk.heightmap[z * BettolaLib::Game::CHUNK_WIDTH+x] * 5.0f); /* 5x scale height */
_vertices.push_back((float)(chunk_z * (BettolaLib::Game::CHUNK_HEIGHT-1) + z));
/* Vertex normal. */
BettolaMath::Vec3 normal = _calculate_normal(x, z, chunk);
_vertices.push_back(normal.x);
_vertices.push_back(normal.y);
_vertices.push_back(normal.z);
}
}
@ -36,6 +45,28 @@ ChunkMesh::ChunkMesh(int chunk_x, int chunk_z, const BettolaLib::Game::Chunk& ch
_setup_mesh();
}
BettolaMath::Vec3 ChunkMesh::_calculate_normal(int x, int z, const BettolaLib::Game::Chunk& chunk) {
/* Get heights of adjacent certices. */
float height_l = chunk.heightmap[z*BettolaLib::Game::CHUNK_WIDTH+(x>0?x-1 : x)] * 5.0f;
float height_r = chunk.heightmap[z*BettolaLib::Game::CHUNK_WIDTH+
(x<BettolaLib::Game::CHUNK_WIDTH-1?x+1 : x)]*5.0f;
float height_d = chunk.heightmap[(z>0?z-1 : z)*BettolaLib::Game::CHUNK_WIDTH+x]*5.0f;
float height_u = chunk.heightmap[(z<BettolaLib::Game::CHUNK_HEIGHT-1?z+1 : z)
* BettolaLib::Game::CHUNK_WIDTH+x]*5.0f;
BettolaMath::Vec3 normal = { height_l - height_r, 2.0f, height_d - height_u };
/* Normalise the normal. */
float mag = sqrt(normal.x*normal.x + normal.y*normal.y + normal.z*normal.z);
if(mag > 0.0f) {
normal.x /= mag;
normal.y /= mag;
normal.z /= mag;
}
return normal;
}
ChunkMesh::~ChunkMesh(void) {
glDeleteVertexArrays(1, &_vao);
glDeleteBuffers(1, &_vbo);
@ -56,9 +87,13 @@ void ChunkMesh::_setup_mesh(void) {
glBufferData(GL_ELEMENT_ARRAY_BUFFER, _indices.size()*sizeof(unsigned int), _indices.data(), GL_STATIC_DRAW);
/* Position attrib. */
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3*sizeof(float), (void*)0);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6*sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
/* Normal attrib. */
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6*sizeof(float), (void*)(3*sizeof(float)));
glEnableVertexAttribArray(1);
glBindVertexArray(0);
}

View File

@ -1,8 +1,9 @@
#pragma once
#include <vector>
#include "bettola/math/vec3.h"
#include "bettola//game/chunk.h"
#include "bettola/game/chunk.h"
class ChunkMesh {
public:
@ -12,6 +13,7 @@ public:
void draw(void);
private:
BettolaMath::Vec3 _calculate_normal(int x, int z, const BettolaLib::Game::Chunk& chunk);
void _setup_mesh(void);
unsigned int _vao, _vbo, _ebo;

View File

@ -146,47 +146,30 @@ void Renderer::render(const Camera& camera, const Player& player,
_shader.use();
GL_CHECK_ERROR();
BettolaMath::Mat4 view_matrix = camera.get_view_matrix();
BettolaMath::Mat4 view = camera.get_view_matrix();
BettolaMath::Mat4 projection = BettolaMath::Mat4::perspective((45.0f * M_PI) / 180.0f, 800.0f/600.0f, 0.1f, 100.0f);
GLint view_loc = glGetUniformLocation(_shader.get_id(), "view");
GLint proj_loc = glGetUniformLocation(_shader.get_id(), "projection");
GLint model_loc = glGetUniformLocation(_shader.get_id(), "model");
GLint color_loc = glGetUniformLocation(_shader.get_id(), "overrideColor");
glUniformMatrix4fv(view_loc, 1, GL_FALSE, view_matrix.get_ptr());
GL_CHECK_ERROR();
glUniformMatrix4fv(proj_loc, 1, GL_FALSE, projection.get_ptr());
GL_CHECK_ERROR();
/* Draw the ground. */
BettolaMath::Mat4 ground_model = BettolaMath::Mat4::translation(0.0f, 0.0f, 0.0f);
glUniformMatrix4fv(model_loc, 1, GL_FALSE, ground_model.get_ptr());
glUniform3f(color_loc, 0.2f, 0.4f, 0.2f);
glBindVertexArray(_ground_vao);
glDrawArrays(GL_TRIANGLES, 0, 6);
glBindVertexArray(0); /* WHOOAAA! Got to make sure we unbind the VAO! */
/* Set players colour back. */
glUniform3f(color_loc, 0.2f, 0.5f, 0.8f);
_shader.set_mat4("view", view);
_shader.set_mat4("projection", projection);
/* Render world. */
glPolygonMode(GL_FRONT_AND_BACK, GL_LINE); /* Wireframe to see geometry. */
/* Set lighting uniforms for terrain. */
_shader.set_vec3("objectColor", { 0.4f, 0.6f, 0.2f}); /* Green. */
_shader.set_vec3("lightColor", { 1.0f, 1.0f, 1.0f});
_shader.set_vec3("lightDir", {-0.5f, -1.0f, -0.5f}); /* From above and to the side. */
for(auto const& [pos, mesh] : world.get_chunk_meshes()) {
BettolaMath::Mat4 model;
_shader.set_mat4("model", model);
mesh->draw();
}
glPolygonMode(GL_FRONT_AND_BACK, GL_FILL); /* Reset to default. */
/* Draw the local player's cube. */
_shader.set_vec3("objectColor", {0.2f, 0.5f, 0.8f}); /* Player colour. */
const auto& player_pos = player.get_position();
BettolaMath::Mat4 trans_matrix = BettolaMath::Mat4::translation(player_pos.x,
player_pos.y, player_pos.z);
BettolaMath::Mat4 rot_matrix = BettolaMath::Mat4::rotation(-camera.get_yaw()-90.0f,
{0.0f,1.0f,0.0f});
BettolaMath::Mat4 model = trans_matrix.multiply(rot_matrix);
glUniformMatrix4fv(model_loc, 1, GL_FALSE, model.get_ptr());
_shader.set_mat4("model", trans_matrix);
GL_CHECK_ERROR();
glBindVertexArray(_vao);
@ -202,7 +185,7 @@ void Renderer::render(const Camera& camera, const Player& player,
BettolaMath::Mat4 remote_rot = BettolaMath::Mat4::rotation(-remote_player.get_yaw()-90.0f,
{0.0f,1.0f,0.0f});
BettolaMath::Mat4 remote_model = remote_trans.multiply(remote_rot);
glUniformMatrix4fv(model_loc, 1, GL_FALSE, remote_model.get_ptr());
_shader.set_mat4("model", remote_model);
glDrawArrays(GL_TRIANGLES, 0, 36);
glBindVertexArray(0); /* Unbind it! */
}

View File

@ -1,6 +1,7 @@
#pragma once
#include <string>
#include "bettola/math/vec3.h"
#include "bettola/math/mat4.h"
class Shader {
@ -12,6 +13,7 @@ public:
void use(void);
void set_mat4(const std::string& name, const BettolaMath::Mat4& matrix) const;
void set_vec3(const std::string& name, const BettolaMath::Vec3& value) const;
unsigned int get_id(void) const { return _program_id; }
private:
bool compile_shader(unsigned int& shader_id, const char* shader_source, int shader_type);

View File

@ -1,7 +1,12 @@
#include <GL/glew.h>
#include "bettola/math/vec3.h"
#include "bettola/math/mat4.h"
#include "shader.h"
void Shader::set_mat4(const std::string& name, const BettolaMath::Mat4& matrix) const {
glUniformMatrix4fv(glGetUniformLocation(_program_id, name.c_str()), 1, GL_FALSE, matrix.get_ptr());
}
void Shader::set_vec3(const std::string& name, const BettolaMath::Vec3& value) const {
glUniform3f(glGetUniformLocation(_program_id, name.c_str()), value.x, value.y, value.z);
}

View File

@ -24,8 +24,8 @@ void World::_generate_chunk(BettolaLib::Game::Chunk& chunk, int chunk_x, int chu
for(int z = 0; z < BettolaLib::Game::CHUNK_HEIGHT; ++z) {
for(int x = 0; x < BettolaLib::Game::CHUNK_WIDTH; ++x) {
/* Calculate world coordinates. */
float world_x = (float)(chunk_x * BettolaLib::Game::CHUNK_WIDTH + x);
float world_z = (float)(chunk_z * BettolaLib::Game::CHUNK_HEIGHT + z);
float world_x = (float)(chunk_x * (BettolaLib::Game::CHUNK_WIDTH - 1) + x);
float world_z = (float)(chunk_z * (BettolaLib::Game::CHUNK_HEIGHT - 1) + z);
/* generate noise value. */
chunk.heightmap[z * BettolaLib::Game::CHUNK_WIDTH + x] = m_noise.GetNoise(world_x, world_z);