[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 #version 330 core
out vec4 FragColor; 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() { 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 #version 330 core
layout (location = 0) in vec3 aPos; layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aNormal;
out vec3 FragPos;
out vec3 Normal;
uniform mat4 model; uniform mat4 model;
uniform mat4 view; uniform mat4 view;
uniform mat4 projection; uniform mat4 projection;
void main() { 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

@ -25,8 +25,8 @@ void World::add_chunk(int chunk_x, int chunk_z, const float* heightmap) {
} }
float World::get_height(float world_x, float world_z) const { float World::get_height(float world_x, float world_z) const {
float chunk_width = (float)(BettolaLib::Game::CHUNK_WIDTH -1); float chunk_width = (float)(BettolaLib::Game::CHUNK_WIDTH - 1);
float chunk_height = (float)(BettolaLib::Game::CHUNK_HEIGHT-1); float chunk_height = (float)(BettolaLib::Game::CHUNK_HEIGHT - 1);
int chunk_x = floor(world_x / chunk_width); int chunk_x = floor(world_x / chunk_width);
int chunk_z = floor(world_z / chunk_height); int chunk_z = floor(world_z / chunk_height);

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, BettolaMath::Vec3 cam_front = { input_msg.cam_front_x, input_msg.cam_front_y,
input_msg.cam_front_z }; input_msg.cam_front_z };
_player.set_velocity_direction(input_state, cam_front); _player.set_velocity_direction(input_state, cam_front);
_player.update(input_msg.dt);
} }
} else { } else {
/* Remote player. Find if we already know about them.. */ /* Remote player. Find if we already know about them.. */

View File

@ -1,5 +1,8 @@
#include <GL/glew.h> #include <GL/glew.h>
#include <cmath>
#include <cstdio>
#include "bettola/game/chunk.h" #include "bettola/game/chunk.h"
#include "bettola/math/vec3.h"
#include "chunk_mesh.h" #include "chunk_mesh.h"
ChunkMesh::ChunkMesh(int chunk_x, int chunk_z, const BettolaLib::Game::Chunk& chunk) { ChunkMesh::ChunkMesh(int chunk_x, int chunk_z, const BettolaLib::Game::Chunk& chunk) {
@ -7,9 +10,15 @@ ChunkMesh::ChunkMesh(int chunk_x, int chunk_z, const BettolaLib::Game::Chunk& ch
for(int z = 0; z < BettolaLib::Game::CHUNK_HEIGHT; ++z) { for(int z = 0; z < BettolaLib::Game::CHUNK_HEIGHT; ++z) {
for(int x = 0; x < BettolaLib::Game::CHUNK_WIDTH; ++x) { for(int x = 0; x < BettolaLib::Game::CHUNK_WIDTH; ++x) {
/* Vertex position. */ /* Vertex position. */
_vertices.push_back((float)(chunk_x * (BettolaLib::Game::CHUNK_WIDTH-1) + x)); _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(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)); _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(); _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) { ChunkMesh::~ChunkMesh(void) {
glDeleteVertexArrays(1, &_vao); glDeleteVertexArrays(1, &_vao);
glDeleteBuffers(1, &_vbo); 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); glBufferData(GL_ELEMENT_ARRAY_BUFFER, _indices.size()*sizeof(unsigned int), _indices.data(), GL_STATIC_DRAW);
/* Position attrib. */ /* 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); glEnableVertexAttribArray(0);
/* Normal attrib. */
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6*sizeof(float), (void*)(3*sizeof(float)));
glEnableVertexAttribArray(1);
glBindVertexArray(0); glBindVertexArray(0);
} }

View File

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

View File

@ -146,47 +146,30 @@ void Renderer::render(const Camera& camera, const Player& player,
_shader.use(); _shader.use();
GL_CHECK_ERROR(); 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); 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"); _shader.set_mat4("view", view);
GLint proj_loc = glGetUniformLocation(_shader.get_id(), "projection"); _shader.set_mat4("projection", 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);
/* Render world. */ /* 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()) { for(auto const& [pos, mesh] : world.get_chunk_meshes()) {
BettolaMath::Mat4 model; BettolaMath::Mat4 model;
_shader.set_mat4("model", model); _shader.set_mat4("model", model);
mesh->draw(); mesh->draw();
} }
glPolygonMode(GL_FRONT_AND_BACK, GL_FILL); /* Reset to default. */
/* Draw the local player's cube. */ /* 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(); const auto& player_pos = player.get_position();
BettolaMath::Mat4 trans_matrix = BettolaMath::Mat4::translation(player_pos.x, BettolaMath::Mat4 trans_matrix = BettolaMath::Mat4::translation(player_pos.x,
player_pos.y, player_pos.z); player_pos.y, player_pos.z);
BettolaMath::Mat4 rot_matrix = BettolaMath::Mat4::rotation(-camera.get_yaw()-90.0f, _shader.set_mat4("model", trans_matrix);
{0.0f,1.0f,0.0f});
BettolaMath::Mat4 model = trans_matrix.multiply(rot_matrix);
glUniformMatrix4fv(model_loc, 1, GL_FALSE, model.get_ptr());
GL_CHECK_ERROR(); GL_CHECK_ERROR();
glBindVertexArray(_vao); 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, BettolaMath::Mat4 remote_rot = BettolaMath::Mat4::rotation(-remote_player.get_yaw()-90.0f,
{0.0f,1.0f,0.0f}); {0.0f,1.0f,0.0f});
BettolaMath::Mat4 remote_model = remote_trans.multiply(remote_rot); 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); glDrawArrays(GL_TRIANGLES, 0, 36);
glBindVertexArray(0); /* Unbind it! */ glBindVertexArray(0); /* Unbind it! */
} }

View File

@ -1,6 +1,7 @@
#pragma once #pragma once
#include <string> #include <string>
#include "bettola/math/vec3.h"
#include "bettola/math/mat4.h" #include "bettola/math/mat4.h"
class Shader { class Shader {
@ -12,6 +13,7 @@ public:
void use(void); void use(void);
void set_mat4(const std::string& name, const BettolaMath::Mat4& matrix) const; 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; } unsigned int get_id(void) const { return _program_id; }
private: private:
bool compile_shader(unsigned int& shader_id, const char* shader_source, int shader_type); 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 <GL/glew.h>
#include "bettola/math/vec3.h"
#include "bettola/math/mat4.h" #include "bettola/math/mat4.h"
#include "shader.h" #include "shader.h"
void Shader::set_mat4(const std::string& name, const BettolaMath::Mat4& matrix) const { 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()); 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 z = 0; z < BettolaLib::Game::CHUNK_HEIGHT; ++z) {
for(int x = 0; x < BettolaLib::Game::CHUNK_WIDTH; ++x) { for(int x = 0; x < BettolaLib::Game::CHUNK_WIDTH; ++x) {
/* Calculate world coordinates. */ /* Calculate world coordinates. */
float world_x = (float)(chunk_x * BettolaLib::Game::CHUNK_WIDTH + x); float world_x = (float)(chunk_x * (BettolaLib::Game::CHUNK_WIDTH - 1) + x);
float world_z = (float)(chunk_z * BettolaLib::Game::CHUNK_HEIGHT + z); float world_z = (float)(chunk_z * (BettolaLib::Game::CHUNK_HEIGHT - 1) + z);
/* generate noise value. */ /* generate noise value. */
chunk.heightmap[z * BettolaLib::Game::CHUNK_WIDTH + x] = m_noise.GetNoise(world_x, world_z); chunk.heightmap[z * BettolaLib::Game::CHUNK_WIDTH + x] = m_noise.GetNoise(world_x, world_z);

View File

@ -8,7 +8,7 @@ float World::get_height(float world_x, float world_z) {
* Mesh generation uses (WIDTH-1), so account for it * Mesh generation uses (WIDTH-1), so account for it
* when calculating the chunk and local coords. * when calculating the chunk and local coords.
*/ */
float chunk_width = (float)(BettolaLib::Game::CHUNK_WIDTH-1); float chunk_width = (float)(BettolaLib::Game::CHUNK_WIDTH -1);
float chunk_height = (float)(BettolaLib::Game::CHUNK_HEIGHT-1); float chunk_height = (float)(BettolaLib::Game::CHUNK_HEIGHT-1);
int chunk_x = floor(world_x / chunk_width); int chunk_x = floor(world_x / chunk_width);