From a1c214caec3632fe4aad57fc86b2085a87e5a4d9 Mon Sep 17 00:00:00 2001 From: Ritchie Cunningham Date: Thu, 18 Sep 2025 20:18:02 +0100 Subject: [PATCH] [Add] Procedural cloud rendering. Adds a skydome with two scrolling layers of procedurally generated clouds to create an interestingish environment. --- assets/shaders/cloud.frag | 29 +++++ assets/shaders/cloud.vert | 21 +++ assets/shaders/sky.frag | 17 +++ assets/shaders/sky.vert | 17 +++ src/graphics/renderer.cpp | 217 +++++++++++++++++++++++++++---- src/graphics/renderer.h | 17 ++- src/graphics/shader.h | 1 + src/graphics/shader_uniforms.cpp | 4 + 8 files changed, 294 insertions(+), 29 deletions(-) create mode 100644 assets/shaders/cloud.frag create mode 100644 assets/shaders/cloud.vert create mode 100644 assets/shaders/sky.frag create mode 100644 assets/shaders/sky.vert diff --git a/assets/shaders/cloud.frag b/assets/shaders/cloud.frag new file mode 100644 index 0000000..3a0b416 --- /dev/null +++ b/assets/shaders/cloud.frag @@ -0,0 +1,29 @@ +#version 330 core +out vec4 FragColor; + +in vec3 TexCoords; + +uniform sampler2D u_CloudTexture; +uniform vec3 u_LightDir; + +const float PI = 3.14159265359; + +void main() { + /* Convert 3D texture coords to 2D using spherical mapping. */ + float u = atan(TexCoords.z, TexCoords.x) / (2.0 * PI) + 0.5; + float v = asin(TexCoords.y) / PI + 0.5; + + /* Sample noise value from cloud texture. */ + float noise = texture(u_CloudTexture, vec2(u, v)).r; + + /* Create soft-edged clouds from the noise. */ + float cloudAlpha = smoothstep(0.5, 0.8, noise); + + /* Some simple lighting. */ + vec3 normal = normalize(TexCoords); + float diffuse = max(dot(normal, -u_LightDir), 0.0); + vec3 cloudColor = vec3(1.0) * (diffuse * 0.5 + 0.5); /* Add some ambient light. */ + + /* Cloud colour is white. it's transparency is determined by alpha. */ + FragColor = vec4(cloudColor, cloudAlpha); +} diff --git a/assets/shaders/cloud.vert b/assets/shaders/cloud.vert new file mode 100644 index 0000000..b2195d8 --- /dev/null +++ b/assets/shaders/cloud.vert @@ -0,0 +1,21 @@ +#version 330 core +layout (location = 0) in vec3 aPos; + +out vec3 TexCoords; + +uniform float u_Time; +uniform float u_ScrollSpeed; +uniform mat4 model; +uniform mat4 view; +uniform mat4 projection; + +void main() { + TexCoords = aPos; + + /* Rotate texture coords around Y axis for scrolling. */ + float angle = u_Time * u_ScrollSpeed; + mat3 rot = mat3(cos(angle), 0, sin(angle), 0, 1, 0, -sin(angle), 0, cos(angle)); + TexCoords = rot * TexCoords; + + gl_Position = projection * view * model * vec4(aPos, 1.0); +} diff --git a/assets/shaders/sky.frag b/assets/shaders/sky.frag new file mode 100644 index 0000000..9b31733 --- /dev/null +++ b/assets/shaders/sky.frag @@ -0,0 +1,17 @@ +#version 330 core +out vec4 FragColor; + +in vec3 WorldPos; + +void main() { + /* Define colours for the top and bottom of the sky. */ + vec3 zenithColor = vec3(0.3, 0.5, 0.8); + vec3 horizonColor = vec3(0.7, 0.85, 1.0); + + float blendFactor = (normalize(WorldPos).y + 1.0) / 2.0; + + /* Blend the two colours. */ + vec3 finalColor = mix(horizonColor, zenithColor, blendFactor); + + FragColor = vec4(finalColor, 1.0); +} diff --git a/assets/shaders/sky.vert b/assets/shaders/sky.vert new file mode 100644 index 0000000..f1f0c05 --- /dev/null +++ b/assets/shaders/sky.vert @@ -0,0 +1,17 @@ +#version 330 core +layout (location = 0) in vec3 aPos; + +out vec3 WorldPos; + +uniform mat4 view; +uniform mat4 projection; + +void main() { + WorldPos = aPos; + + /* Force depth value to 1.0 after perspective division + * to ensure the skybox is always rendered behind everythig else. + */ + vec4 pos = projection * view * vec4(WorldPos, 1.0); + gl_Position = pos.xyww; +} diff --git a/src/graphics/renderer.cpp b/src/graphics/renderer.cpp index 2c5d049..08d861e 100644 --- a/src/graphics/renderer.cpp +++ b/src/graphics/renderer.cpp @@ -1,6 +1,9 @@ #include +#include #include +#include #include +#include "bettola/noise/fast_noise_lite.h" #include "game/player.h" #include "game/world.h" #include "graphics/camera.h" @@ -22,7 +25,9 @@ } \ } while(0) -Renderer::Renderer(void) : _vao(0), _vbo(0), _ground_vao(0), _ground_vbo(0) {} + +Renderer::Renderer(void) : _vao(0), _vbo(0), _sky_vao(0), _sky_vbo(0), _sky_ebo(0), + _cloud_vao(0), _cloud_vbo(0), _cloud_texture(0), _sky_indices_count(0) {} Renderer::~Renderer(void) { if(_vao != 0) { @@ -31,11 +36,20 @@ Renderer::~Renderer(void) { if(_vbo != 0) { glDeleteBuffers(1, &_vbo); } - if(_ground_vao != 0) { - glDeleteVertexArrays(1, &_ground_vao); + if(_sky_vao != 0) { + glDeleteVertexArrays(1, &_sky_vao); } - if(_ground_vbo != 0) { - glDeleteBuffers(1, &_ground_vbo); + if(_sky_vbo != 0) { + glDeleteBuffers(1, &_sky_vbo); + } + if(_sky_ebo != 0) { + glDeleteBuffers(1, &_sky_ebo); + } + if(_cloud_texture != 0) { + glDeleteTextures(1, &_cloud_texture); + } + if(_cloud_vao != 0) { + glDeleteVertexArrays(1, &_cloud_vao); } } @@ -51,6 +65,23 @@ bool Renderer::init(int screen_width, int screen_height) { return false; } + if(!_sky_shader.load_from_files("assets/shaders/sky.vert", + "assets/shaders/sky.frag")) { + fprintf(stderr, "Failed to load sky shaders\n"); + return false; + } + + if(!_cloud_shader.load_from_files("assets/shaders/cloud.vert", + "assets/shaders/cloud.frag")) { + fprintf(stderr, "Failed to load cloud shaders\n"); + return false; + } + + if(!_init_textures()) { + fprintf(stderr, "Failed to init textures\n"); + return false; + } + // Definitive, correct 3D cube vertices float vertices[] = { /* Positions. */ /* Normals. */ @@ -107,27 +138,58 @@ bool Renderer::init(int screen_width, int screen_height) { glEnableVertexAttribArray(0); /* Normal attribute. */ glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6*sizeof(float), (void*)(3*sizeof(float))); + glEnableVertexAttribArray(1); glBindBuffer(GL_ARRAY_BUFFER, 0); glBindVertexArray(0); - /* Setup ground plane. */ - float ground_vertices[] = { - /* Positions. */ - 500.0f, -0.5f, 500.0f, - -500.0f, -0.5f, 500.0f, - -500.0f, -0.5f, -500.0f, + /* Sky Sphere Generation. */ + std::vector sky_vertices; + std::vector sky_indices; + const int segments = 32; + const int rings = 32; - 500.0f, -0.5f, 500.0f, - -500.0f, -0.5f, -500.0f, - 500.0f, -0.5f, -500.0f, - }; + for(int i = 0; i <= rings; i++) { + float phi = i * M_PI / rings; + for(int j = 0; j <= segments; j++) { + float theta = j * 2.0f * M_PI / segments; + float x = cos(theta) * sin(phi); + float y = cos(phi); + float z = sin(theta) * sin(phi); + sky_vertices.push_back(x); + sky_vertices.push_back(y); + sky_vertices.push_back(z); + } + } - glGenVertexArrays(1, &_ground_vao); - glGenBuffers(1, &_ground_vbo); - glBindVertexArray(_ground_vao); - glBindBuffer(GL_ARRAY_BUFFER, _ground_vbo); - glBufferData(GL_ARRAY_BUFFER, sizeof(ground_vertices), ground_vertices, GL_STATIC_DRAW); - glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3*sizeof(float), (void*)0); + for(int i = 0; i < rings; i++) { + for(int j = 0; j < segments; j++) { + int first = (i * (segments + 1)) + j; + int second = first + segments + 1; + sky_indices.push_back(first); + sky_indices.push_back(second); + sky_indices.push_back(first+1); + + sky_indices.push_back(second); + sky_indices.push_back(second+1); + sky_indices.push_back(first+1); + } + } + _sky_indices_count = sky_indices.size(); + + glGenVertexArrays(1, &_sky_vao); + glGenBuffers(1, &_sky_vbo); + glGenBuffers(1, &_sky_ebo); + + glBindVertexArray(_sky_vao); + glBindBuffer(GL_ARRAY_BUFFER, _sky_vbo); + glBufferData(GL_ARRAY_BUFFER, sky_vertices.size()*sizeof(float), + sky_vertices.data(), GL_STATIC_DRAW); + + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, _sky_ebo); + glBufferData(GL_ELEMENT_ARRAY_BUFFER, sky_indices.size()*sizeof(unsigned int), + sky_indices.data(), GL_STATIC_DRAW); + + glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0); glEnableVertexAttribArray(0); glBindBuffer(GL_ARRAY_BUFFER, 0); glBindVertexArray(0); @@ -140,6 +202,107 @@ bool Renderer::init(int screen_width, int screen_height) { return true; } +bool Renderer::_init_textures(void) { + _cloud_texture = _generate_cloud_texture(); + if(_cloud_texture == 0) { + return false; + } + return true; +} + +unsigned int Renderer::_generate_cloud_texture(void) { + const int width = 512; + const int height = 512; + std::vector buffer(width*height); + + FastNoiseLite noise; + noise.SetNoiseType(FastNoiseLite::NoiseType_Perlin); + + noise.SetFractalType(FastNoiseLite::FractalType_FBm); + noise.SetFractalOctaves(4); + noise.SetFractalLacunarity(2.0f); + noise.SetFractalGain(0.5f); + noise.SetFrequency(0.05f); + + for(int y = 0; y < height; y++) { + for(int x = 0; x < width; x++) { + float noise_val = noise.GetNoise((float)x, (float)y); + buffer[y*width+x] = (noise_val + 1.0f) / 2.0f; + } + } + unsigned int texture_id; + glGenTextures(1, &texture_id); + glBindTexture(GL_TEXTURE_2D, texture_id); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RED, width, height, 0, GL_RED, GL_FLOAT, buffer.data()); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + + return texture_id; +} + +void Renderer::_render_sky(const Camera& camera) { + glDepthFunc(GL_LEQUAL); + _sky_shader.use(); + + /* Remove translation from the view matrix so the skybox follows the camera. */ + BettolaMath::Mat4 view = camera.get_view_matrix(); + view.elements[12] = 0; + view.elements[13] = 0; + view.elements[14] = 0; + + _sky_shader.set_mat4("view", view); + _sky_shader.set_mat4("projection", _projection); + + glBindVertexArray(_sky_vao); + glDrawElements(GL_TRIANGLES, _sky_indices_count, GL_UNSIGNED_INT, 0); + glBindVertexArray(0); + glDepthFunc(GL_LESS); /* Put depth function back to default. */ +} + +void Renderer::_render_clouds(const Camera& camera) { + glEnable(GL_BLEND); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + glDisable(GL_DEPTH_TEST); + + _cloud_shader.use(); + + /* Remove translation from the view matrix so clouds feel infintely far away. */ + BettolaMath::Mat4 cloud_view = camera.get_view_matrix(); + cloud_view.elements[12] = 0; + cloud_view.elements[13] = 0; + cloud_view.elements[14] = 0; + + _cloud_shader.set_mat4("projection", _projection); + _cloud_shader.set_mat4("view", cloud_view); + _cloud_shader.set_vec3("u_LightDir", {-0.5f, -1.0f, -0.5f}); + + glBindVertexArray(_sky_vao); + glActiveTexture(GL_TEXTURE0); + glBindTexture(GL_TEXTURE_2D, _cloud_texture); + /* Tell the shader to use texture unit 0. */ + glUniform1i(glGetUniformLocation(_cloud_shader.get_id(), "u_CloudTexture"),0); + + /* Draw first cloud layer. */ + BettolaMath::Mat4 model1 = BettolaMath::Mat4::scale(1.0f, 2.0f, 1.0f); + _cloud_shader.set_mat4("model", model1); + _cloud_shader.set_float("u_Time", (float)SDL_GetTicks() / 1000.0f); + _cloud_shader.set_float("u_ScrollSpeed", 0.02f); + glDrawElements(GL_TRIANGLES, _sky_indices_count, GL_UNSIGNED_INT, 0); + + /* Draw second cloud layer. */ + BettolaMath::Mat4 model2 = BettolaMath::Mat4::scale(1.2f, 1.2f, 1.2f); + model2 = model2.multiply(BettolaMath::Mat4::rotation(30.0f, {0.0f, 1.0f, 0.0f})); + _cloud_shader.set_mat4("model", model2); + _cloud_shader.set_float("u_ScrollSpeed", 0.03f); + glDrawElements(GL_TRIANGLES, _sky_indices_count, GL_UNSIGNED_INT, 0); + + glBindVertexArray(0); + glEnable(GL_DEPTH_TEST); + glDisable(GL_BLEND); +} + void Renderer::render(const Camera& camera, const Player& player, const std::vector& remote_players, const World& world) { @@ -147,14 +310,16 @@ void Renderer::render(const Camera& camera, const Player& player, glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); /* Need to clear depth buffer too. */ GL_CHECK_ERROR(); + _projection = BettolaMath::Mat4::perspective((45.0f*M_PI)/180.0f, 800.0f/600.0f, 0.1f, 2000.0f); + + _render_sky(camera); + _render_clouds(camera); + _shader.use(); GL_CHECK_ERROR(); - 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); - - _shader.set_mat4("view", view); - _shader.set_mat4("projection", projection); + _shader.set_mat4("view", camera.get_view_matrix()); + _shader.set_mat4(("projection"), _projection); /* Render world. */ /* Set lighting uniforms for terrain. */ diff --git a/src/graphics/renderer.h b/src/graphics/renderer.h index f35e9c0..9be6e9a 100644 --- a/src/graphics/renderer.h +++ b/src/graphics/renderer.h @@ -19,13 +19,24 @@ public: const World& world); private: - bool _init_shaders(); + bool _init_shaders(void); + void _render_sky(const Camera& camera); + void _render_clouds(const Camera& camera); + bool _init_textures(void); + unsigned int _generate_cloud_texture(void); Shader _shader; + Shader _sky_shader; unsigned int _vao; unsigned int _vbo; - unsigned int _ground_vao; - unsigned int _ground_vbo; + unsigned int _sky_vao; + unsigned int _sky_vbo; + unsigned int _sky_ebo; + unsigned int _sky_indices_count; + Shader _cloud_shader; + unsigned int _cloud_vao; + unsigned int _cloud_vbo; + unsigned int _cloud_texture; BettolaMath::Mat4 _projection; }; diff --git a/src/graphics/shader.h b/src/graphics/shader.h index 90af8f7..488a3f9 100644 --- a/src/graphics/shader.h +++ b/src/graphics/shader.h @@ -14,6 +14,7 @@ public: void set_mat4(const std::string& name, const BettolaMath::Mat4& matrix) const; void set_bool(const std::string& name, bool value) const; + void set_float(const std::string& name, float value) const; void set_vec3(const std::string& name, const BettolaMath::Vec3& value) const; unsigned int get_id(void) const { return _program_id; } private: diff --git a/src/graphics/shader_uniforms.cpp b/src/graphics/shader_uniforms.cpp index 7a5a3f1..c89014f 100644 --- a/src/graphics/shader_uniforms.cpp +++ b/src/graphics/shader_uniforms.cpp @@ -14,3 +14,7 @@ void Shader::set_vec3(const std::string& name, const BettolaMath::Vec3& value) c void Shader::set_bool(const std::string& name, bool value) const { glUniform1i(glGetUniformLocation(_program_id, name.c_str()), (int)value); } + +void Shader::set_float(const std::string& name, float value) const { + glUniform1f(glGetUniformLocation(_program_id, name.c_str()), value); +}