From 95134c91d8789f4334d43ea945a1ef734a7ad54c Mon Sep 17 00:00:00 2001 From: Ritchie Cunningham Date: Thu, 18 Sep 2025 21:54:31 +0100 Subject: [PATCH] [Add] Atmospheric scattering and improved clouds. --- assets/shaders/cloud.frag | 84 +++++++++++++++++++++++++++++++-------- assets/shaders/sky.frag | 79 ++++++++++++++++++++++++++++++++---- assets/shaders/sky.vert | 12 ++---- src/graphics/renderer.cpp | 2 + 4 files changed, 145 insertions(+), 32 deletions(-) diff --git a/assets/shaders/cloud.frag b/assets/shaders/cloud.frag index 9efd7ad..2e6a7db 100644 --- a/assets/shaders/cloud.frag +++ b/assets/shaders/cloud.frag @@ -4,30 +4,82 @@ out vec4 FragColor; in vec3 TexCoords; uniform sampler2D u_CloudTexture; -uniform vec3 u_LightDir; +uniform vec3 u_SunPos; const float PI = 3.14159265359; +/* Atmosphere parameters. */ +const float ATMOSPHERE_RADIUS = 6520275.0; +const float PLANET_RADIUS = 6371000.0; +const float RAYLEIGH_SCALE_HEIGHT = 8000.0; +const float MIE_SCALE_HEIGHT = 1200.0; + +/* Scattering coefficients. */ +const vec3 RAYLEIGH_SCATTERING_COEFF = vec3(0.0000058, 0.0000135, 0.0000331); +const vec3 MIE_SCATTERING_COEFF = vec3(0.00002); + +/* Mie scattering parameters. */ +const float MIE_G = 0.76; + +/* Number of samples for ray marching. */ +const int SAMPLES = 16; +const int LIGHT_SAMPLES = 8; + +float ray_sphere_intersect(vec3 origin, vec3 dir, float radius) { + float a = dot(dir, dir); + float b = 2.0 * dot(origin, dir); + float c = dot(origin, origin) - radius * radius; + float d = b * b - 4.0 * a * c; + if (d < 0.0) { + return -1.0; + } + return (-b + sqrt(d)) / (2.0 * a); +} + +vec3 get_sky_color(vec3 dir) { + vec3 eye = vec3(0.0, PLANET_RADIUS + 1.0, 0.0); + float dist = ray_sphere_intersect(eye, dir, ATMOSPHERE_RADIUS); + vec3 totalRayleigh = vec3(0.0); + vec3 totalMie = vec3(0.0); + float stepSize = dist / float(SAMPLES); + for (int i = 0; i < SAMPLES; i++) { + vec3 p = eye + dir * (float(i) + 0.5) * stepSize; + float h = length(p) - PLANET_RADIUS; + float rayleighDensity = exp(-h / RAYLEIGH_SCALE_HEIGHT); + float mieDensity = exp(-h / MIE_SCALE_HEIGHT); + float lightDist = ray_sphere_intersect(p, normalize(u_SunPos), ATMOSPHERE_RADIUS); + float lightStepSize = lightDist / float(LIGHT_SAMPLES); + float opticalDepthLight = 0.0; + for (int j = 0; j < LIGHT_SAMPLES; j++) { + vec3 lp = p + normalize(u_SunPos) * (float(j) + 0.5) * lightStepSize; + float lh = length(lp) - PLANET_RADIUS; + opticalDepthLight += exp(-lh / RAYLEIGH_SCALE_HEIGHT) * lightStepSize; + } + vec3 transmittance = exp(-(opticalDepthLight * RAYLEIGH_SCATTERING_COEFF + + opticalDepthLight * MIE_SCATTERING_COEFF)); + totalRayleigh += rayleighDensity * stepSize * transmittance; + totalMie += mieDensity * stepSize * transmittance; + } + + float mu = dot(dir, normalize(u_SunPos)); + float rayleighPhase = 3.0 / (16.0 * PI) * (1.0 + mu * mu); + float miePhase = 3.0 / (8.0 * PI) * ((1.0 - MIE_G * MIE_G) + * (1.0 + mu * mu)) / ((2.0 + MIE_G * MIE_G) * pow(1.0 + MIE_G + * MIE_G - 2.0 * MIE_G * mu, 1.5)); + return 20.0 * (totalRayleigh * RAYLEIGH_SCATTERING_COEFF * rayleighPhase + + totalMie * MIE_SCATTERING_COEFF * miePhase); +} + 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; + float cloudAlpha = texture(u_CloudTexture, vec2(u,v)).r; - /* Sample noise value from cloud texture. */ - float noise = texture(u_CloudTexture, vec2(u, v)).r; + vec3 skyColor = get_sky_color(normalize(TexCoords)); + vec3 cloudColor = mix(skyColor, vec3(1.0), smoothstep(0.5, 0.8, cloudAlpha)); - /* 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. */ - - /* Fade out clouds at the poles to hide pinching. */ float poleFade = 1.0 - abs(TexCoords.y); - cloudAlpha *= smoothstep(0.0, 0.2, poleFade); + float finalAlpha = smoothstep(0.0, 0.2, poleFade) * smoothstep(0.5, 0.8, cloudAlpha); - /* Cloud colour is white. it's transparency is determined by alpha. */ - FragColor = vec4(cloudColor, cloudAlpha); + FragColor = vec4(pow(cloudColor, vec3(1.0/2.2)), finalAlpha); } diff --git a/assets/shaders/sky.frag b/assets/shaders/sky.frag index 9b31733..2d01768 100644 --- a/assets/shaders/sky.frag +++ b/assets/shaders/sky.frag @@ -1,17 +1,80 @@ #version 330 core out vec4 FragColor; -in vec3 WorldPos; +in vec3 v_WorldPos; + +uniform vec3 u_SunPos; + +const float PI = 3.14159265359; + +/* Atmosphere parameters. */ +const float ATMOSPHERE_RADIUS = 6520275.0; +const float PLANET_RADIUS = 6371000.0; +const float RAYLEIGH_SCALE_HEIGHT = 8000.0; +const float MIE_SCALE_HEIGHT = 1200.0; + +/* Scattering coefficients. */ +const vec3 RAYLEIGH_SCATTERING_COEFF = vec3(0.0000058, 0.0000135, 0.0000331); +const vec3 MIE_SCATTERING_COEFF = vec3(0.00002); + +/* Mie scattering parameters. */ +const float MIE_G = 0.76; + +/* Number of samples for ray marching. */ +const int SAMPLES = 16; +const int LIGHT_SAMPLES = 8; + +float ray_sphere_intersect(vec3 origin, vec3 dir, float radius) { + float a = dot(dir, dir); + float b = 2.0 * dot(origin, dir); + float c = dot(origin, origin) - radius * radius; + float d = b * b - 4.0 * a * c; + if (d < 0.0) { + return -1.0; + } + return (-b + sqrt(d)) / (2.0 * a); +} 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); + vec3 dir = normalize(v_WorldPos); + vec3 eye = vec3(0.0, PLANET_RADIUS + 1.0, 0.0); - float blendFactor = (normalize(WorldPos).y + 1.0) / 2.0; + float dist = ray_sphere_intersect(eye, dir, ATMOSPHERE_RADIUS); - /* Blend the two colours. */ - vec3 finalColor = mix(horizonColor, zenithColor, blendFactor); + vec3 totalRayleigh = vec3(0.0); + vec3 totalMie = vec3(0.0); - FragColor = vec4(finalColor, 1.0); + float stepSize = dist / float(SAMPLES); + for(int i = 0; i < SAMPLES; i++) { + vec3 p = eye + dir * (float(i) + 0.5) * stepSize; + float h = length(p) - PLANET_RADIUS; + + float rayleighDensity = exp(-h / RAYLEIGH_SCALE_HEIGHT); + float mieDensity = exp(-h / MIE_SCALE_HEIGHT); + + float lightDist = ray_sphere_intersect(p, normalize(u_SunPos), ATMOSPHERE_RADIUS); + float lightStepSize = lightDist / float(LIGHT_SAMPLES); + float opticalDepthLight = 0.0; + for(int j = 0; j < LIGHT_SAMPLES; j++) { + vec3 lp = p + normalize(u_SunPos) * (float(j) + 0.5) * lightStepSize; + float lh = length(lp) - PLANET_RADIUS; + opticalDepthLight += exp(-lh / RAYLEIGH_SCALE_HEIGHT) * lightStepSize; + } + + vec3 transmittance = exp(-(opticalDepthLight * RAYLEIGH_SCATTERING_COEFF + + opticalDepthLight * MIE_SCATTERING_COEFF)); + totalRayleigh += rayleighDensity * stepSize * transmittance; + totalMie += mieDensity * stepSize * transmittance; + } + + float mu = dot(dir, normalize(u_SunPos)); + float rayleighPhase = 3.0 / (16.0 * PI) * (1.0 + mu * mu); + float miePhase = 3.0 / (8.0 * PI) * ((1.0 - MIE_G * MIE_G) + * (1.0 + mu * mu)) / ((2.0 + MIE_G * MIE_G) + * pow(1.0 + MIE_G * MIE_G - 2.0 * MIE_G * mu, 1.5)); + + vec3 finalColor = 20.0 * (totalRayleigh * RAYLEIGH_SCATTERING_COEFF + * rayleighPhase + totalMie * MIE_SCATTERING_COEFF * miePhase); + + FragColor = vec4(pow(finalColor, vec3(1.0/2.2)), 1.0); } diff --git a/assets/shaders/sky.vert b/assets/shaders/sky.vert index f1f0c05..d4645a9 100644 --- a/assets/shaders/sky.vert +++ b/assets/shaders/sky.vert @@ -1,17 +1,13 @@ #version 330 core layout (location = 0) in vec3 aPos; -out vec3 WorldPos; - uniform mat4 view; uniform mat4 projection; -void main() { - WorldPos = aPos; +out vec3 v_WorldPos; - /* 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); +void main() { + v_WorldPos = aPos; + vec4 pos = projection * view * vec4(aPos, 1.0); gl_Position = pos.xyww; } diff --git a/src/graphics/renderer.cpp b/src/graphics/renderer.cpp index 0091a1f..c6fa5dc 100644 --- a/src/graphics/renderer.cpp +++ b/src/graphics/renderer.cpp @@ -265,6 +265,7 @@ void Renderer::_render_sky(const Camera& camera) { _sky_shader.set_mat4("view", view); _sky_shader.set_mat4("projection", _projection); + _sky_shader.set_vec3("u_SunPos", {0.0f, 1.0f, 0.0f}); glBindVertexArray(_sky_vao); glDrawElements(GL_TRIANGLES, _sky_indices_count, GL_UNSIGNED_INT, 0); @@ -288,6 +289,7 @@ void Renderer::_render_clouds(const Camera& camera) { _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}); + _cloud_shader.set_vec3("u_SunPos", {0.0f, 1.0f, 0.0f}); glBindVertexArray(_sky_vao); glActiveTexture(GL_TEXTURE0);