[Add] Procedural cloud rendering.

Adds a skydome with two scrolling layers of procedurally generated
clouds to create an interestingish environment.
This commit is contained in:
Ritchie Cunningham 2025-09-18 20:18:02 +01:00
parent 8c701d39b7
commit a1c214caec
8 changed files with 294 additions and 29 deletions

29
assets/shaders/cloud.frag Normal file
View File

@ -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);
}

21
assets/shaders/cloud.vert Normal file
View File

@ -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);
}

17
assets/shaders/sky.frag Normal file
View File

@ -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);
}

17
assets/shaders/sky.vert Normal file
View File

@ -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;
}

View File

@ -1,6 +1,9 @@
#include <GL/glew.h>
#include <climits>
#include <cstdio>
#include <vector>
#include <cmath>
#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,26 +138,57 @@ 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<float> sky_vertices;
std::vector<unsigned int> 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);
}
}
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);
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);
glEnableVertexAttribArray(0);
glBindBuffer(GL_ARRAY_BUFFER, 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<float> 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<RemotePlayer>& 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. */

View File

@ -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;
};

View File

@ -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:

View File

@ -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);
}