3 Commits

Author SHA1 Message Date
ab40c2eb34 feat(underwater): add volume scattering and shadow mapping for light shafts 2026-06-19 20:52:56 +08:00
3b5a3b994f fix(water shader): update depth fade and remove underwater check
- Increase DEPTH_FADE_DISTANCE from 8 to 10
- Remove conditional so depth fade always applies
- Darken deepColor from (0, 0.08, 0.15) to (0, 0.015, 0.045)
2026-06-19 19:48:06 +08:00
e1eaf62f09 feat(water): add noise-based caustics and configurable fog density 2026-06-19 17:51:05 +08:00
7 changed files with 159 additions and 26 deletions

View File

@@ -1,35 +1,137 @@
#version 460
in vec2 TexCoord;
in vec3 rayDir;
out vec4 FragColor;
uniform sampler2D u_sceneTexture;
layout (binding = 0) uniform sampler2D u_sceneTexture;
layout (binding = 1) uniform sampler2D u_depthTexture;
layout (binding = 2) uniform sampler2D u_shadowMap;
uniform mat4 u_lightSpaceMatrix;
uniform float u_time;
uniform bool u_underwater;
uniform vec3 u_waterColor;
uniform float u_fogDensity;
uniform mat4 InverseViewProjection;
uniform vec3 cameraPos;
uniform vec3 sunDir;
uniform vec3 sunColor;
uniform float waterDensity;
float hash(vec2 p) {
return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453);
}
float noise(vec2 p) {
vec2 i = floor(p);
vec2 f = fract(p);
f = f * f * (3.0 - 2.0 * f);
return mix(
mix(hash(i), hash(i + vec2(1.0, 0.0)), f.x),
mix(hash(i + vec2(0.0, 1.0)), hash(i + vec2(1.0, 1.0)), f.x),
f.y
);
}
float fbm(vec2 p) {
float value = 0.0;
float amp = 0.5;
float freq = 1.0;
for (int i = 0; i < 4; ++i) {
value += amp * noise(p * freq);
freq *= 2.0;
amp *= 0.5;
}
return value;
}
float getCausticValue(float x, float y, float z) {
float w = 8.0;
float strength = 4.0;
vec2 coord = vec2(x, y) * w + z * 0.5;
float n = fbm(coord);
float caustic = pow(n, 3.0) * strength;
return caustic;
}
float getShadow(vec3 worldPos) {
vec4 posLightSpace = u_lightSpaceMatrix * vec4(worldPos, 1.0);
vec3 projCoords = posLightSpace.xyz / posLightSpace.w;
projCoords = projCoords * 0.5 + 0.5;
if (projCoords.z > 1.0) return 1.0;
float closestDepth = texture(u_shadowMap, projCoords.xy).r;
float currentDepth = projCoords.z;
return currentDepth - 0.005 > closestDepth ? 0.0 : 1.0;
}
void main() {
vec4 original = texture(u_sceneTexture, TexCoord);
if (!u_underwater) {
FragColor = original;
return;
}
float rawDepth = texture(u_depthTexture, TexCoord).r;
vec3 ndc = vec3(TexCoord * 2.0 - 1.0, rawDepth * 2.0 - 1.0);
vec4 worldPosH = InverseViewProjection * vec4(ndc, 1.0);
vec3 worldPos = worldPosH.xyz / worldPosH.w;
float sceneDistance = distance(cameraPos, worldPos);
vec2 distoredUV = TexCoord;
float strength = 0.003;
distoredUV.x += sin(TexCoord.y * 15.0 + u_time * 5.0) * strength;
distoredUV.y += cos(TexCoord.x * 15.0 + u_time * 4.3) * strength;
distoredUV = clamp(distoredUV, 0.001, 0.999);
vec4 distorted = texture(u_sceneTexture, distoredUV);
vec4 distorted = texture(u_sceneTexture, distoredUV);
float rawCaustic = getCausticValue(TexCoord.x, TexCoord.y, u_time * 0.3);
float caustic = 0.5 + rawCaustic * 0.5;
vec3 causticLight = vec3(caustic * 0.9, caustic, caustic * 0.7);
float caustic = 0.9 + 0.1 * sin(TexCoord.x * 20.0 + u_time) * cos(TexCoord.y * 20.0 + u_time * 1.2);
vec3 causticLight = vec3(caustic, caustic * 0.95, caustic * 0.9);
//vec3 causticLight = vec3(1.0);
float fogFactor = clamp(1.0 - (TexCoord.y * u_fogDensity * 10.0), 0.0, 1.0);
vec3 mixed = mix(u_waterColor, distorted.rgb * causticLight, fogFactor);
// Volume Scattering
vec3 rayOrigin = cameraPos;
vec3 rayDirection = normalize(rayDir);
FragColor = vec4(mixed, 1.0);
float maxDist = 10.0; // Maximum visible distance
float stepSize = 0.4;
float phasePower = 8.0; // Beam Sharpness
vec3 scattering = vec3(0.0);
float transmittance = 1.0;
float cosTheta = dot(rayDirection, sunDir);
float phase = pow(max(cosTheta, 0.0), phasePower);
for (float t = 0.0; t < maxDist; t += stepSize) {
if (t > sceneDistance) break;
vec3 pos = rayOrigin + rayDirection * t;
float shadow = getShadow(pos);
float density = waterDensity;
vec3 absorption = vec3(0.3, 0.08, 0.02); // Absorption coefficient per meter
vec3 sunLightAtPos = sunColor * exp(-absorption * t);
scattering += density * phase * sunLightAtPos * transmittance * stepSize * shadow;
float extinction = waterDensity * 1.5; // Extinction coefficient, tunable
transmittance *= exp(-extinction * stepSize);
if (transmittance < 0.01) break;
}
FragColor.rgb = mixed + scattering;
FragColor.a = 1.0;
}

View File

@@ -3,9 +3,18 @@
layout (location = 0) in vec2 pos;
layout (location = 1) in vec2 texCoord;
out vec2 TexCoord;
uniform mat4 InverseViewProjection;
uniform vec3 cameraPos;
out vec2 TexCoord;
out vec3 rayDir;
void main() {
gl_Position = vec4(pos.x, pos.y, 0.0, 1.0);
TexCoord = texCoord;
vec4 clipPos = vec4(pos, 1.0, 1.0);
vec4 worldPosH = InverseViewProjection * clipPos;
vec3 worldPos = worldPosH.xyz / worldPosH.w;
vec3 RayDir = worldPos - cameraPos;
rayDir = normalize(RayDir);
gl_Position = clipPos;
}

View File

@@ -237,9 +237,9 @@ void main() {
vec3 sceneViewPos = reconstructViewPos(screenUV, sceneDepthRaw);
float waterDepth = vert_pos.z - sceneViewPos.z;
waterDepth = max(waterDepth, 0.0);
// fully deep water
const float DEPTH_FADE_DISTANCE = 8.0;
float depthFactor = underwater ? 0.0 : clamp(waterDepth / DEPTH_FADE_DISTANCE, 0.0, 1.0);
const float DEPTH_FADE_DISTANCE = 10;
float depthFactor = clamp(waterDepth / DEPTH_FADE_DISTANCE, 0.0, 1.0);
if (underwater) {
reflectColor = vec3(0.0);
@@ -287,7 +287,7 @@ void main() {
if (enableDepthFade) {
vec3 shallowColor = vec3(0.4, 0.75, 0.7);
vec3 deepColor = vec3(0.0, 0.08, 0.15);
vec3 deepColor = vec3(0.0, 0.015, 0.045);
vec3 waterTint = mix(shallowColor, deepColor, depthFactor);

View File

@@ -212,7 +212,7 @@ constexpr float CROSS_NORMALS[2][6][3] = {
{0.0f, 1.0f, 0.0f}}};
#pragma endregion
// [-1, 1]
constexpr float QUAD_VERTICES[] = {
// postion // texcoorlds
-1.0f, 1.0f, 0.0f, 1.0f, -1.0f, -1.0f, 0.0f, 0.0f, 1.0f, -1.0f, 1.0f, 0.0f,

View File

@@ -45,10 +45,12 @@ public:
float& cloud_threshold_low();
float& cloud_threshold_high();
float& refract_strength();
float& underwater_fog_density();
float& water_density();
private:
struct ParallelLight {
glm::vec3 sundir;
glm::vec3 sundir; // direction from sun to vertex
glm::vec3 lightdir;
float sun_height = 0.0f;
float day_light = 0.0f;
@@ -56,6 +58,7 @@ private:
glm::vec3 sun_color;
glm::vec3 directional_light_color;
glm::vec3 finnal_ambient_color;
glm::mat4 light_space_matrix;
};
struct SkyUniform {
@@ -115,7 +118,7 @@ private:
GLuint m_fbo = 0;
GLuint m_screen_texture = 0;
GLuint m_depth_texture = 0;
GLuint m_screen_depth_texture = 0;
GLuint m_oit_fbo = 0;
GLuint m_accum_texture = 0;
@@ -152,6 +155,10 @@ private:
float m_refract_strength = 0.03f;
float m_underwater_fog_density = 0.08f;
float m_water_density = 0.12f;
ParallelLight m_parallel_light;
SkyUniform m_sky_uniform;
/*

View File

@@ -662,6 +662,11 @@ void DevPanel::show_shader_tab_item() {
ImGui::Checkbox("Water Perturb", &m_app.renderer().water_perturb());
ImGui::SameLine();
ImGui::Checkbox("WaterDepthFade", &m_app.renderer().water_depth_fade());
ImGui::SliderFloat("Underwater Fog Density",
&m_app.renderer().underwater_fog_density(), 0.0f,
1.0f);
ImGui::SliderFloat("Water Density", &m_app.renderer().water_density(),
0.0f, 1.0f);
ImGui::EndTabItem();
}
}

View File

@@ -37,7 +37,7 @@ Renderer::~Renderer() {
glDeleteVertexArrays(NUM_VAO, m_vao.data());
glDeleteFramebuffers(1, &m_fbo);
glDeleteTextures(1, &m_screen_texture);
glDeleteTextures(1, &m_depth_texture);
glDeleteTextures(1, &m_screen_depth_texture);
glDeleteFramebuffers(1, &m_oit_fbo);
glDeleteTextures(1, &m_accum_texture);
@@ -443,11 +443,19 @@ void Renderer::render_underwater() {
shader.set_loc("u_time", static_cast<float>(glfwGetTime()));
shader.set_loc("u_underwater", m_camera.is_under_water());
shader.set_loc("u_waterColor", glm::vec3(0.1f, 0.25f, 0.35f));
shader.set_loc("u_fogDensity", 0.08f);
shader.set_loc("u_fogDensity", m_underwater_fog_density);
shader.set_loc("cameraPos", m_camera.get_camera_pos());
shader.set_loc("sunDir", -m_parallel_light.sundir);
shader.set_loc("waterDensity", m_water_density);
shader.set_loc("InverseViewProjection", glm::inverse(m_p_mat * m_v_mat));
shader.set_loc("sunColor", m_parallel_light.sun_color);
shader.set_loc("u_lightSpaceMatrix", m_parallel_light.light_space_matrix);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, m_screen_texture);
glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_2D, m_screen_depth_texture);
glActiveTexture(GL_TEXTURE2);
glBindTexture(GL_TEXTURE_2D, m_depth_map_texture);
glDrawArrays(GL_TRIANGLES, 0, 6);
glBindVertexArray(0);
}
@@ -483,7 +491,7 @@ void Renderer::updata_framebuffer(int width, int height) {
glBindFramebuffer(GL_FRAMEBUFFER, m_fbo);
glDeleteTextures(1, &m_screen_texture);
glDeleteTextures(1, &m_depth_texture);
glDeleteTextures(1, &m_screen_depth_texture);
glGenTextures(1, &m_screen_texture);
glBindTexture(GL_TEXTURE_2D, m_screen_texture);
@@ -495,14 +503,14 @@ void Renderer::updata_framebuffer(int width, int height) {
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D,
m_screen_texture, 0);
glGenTextures(1, &m_depth_texture);
glBindTexture(GL_TEXTURE_2D, m_depth_texture);
glGenTextures(1, &m_screen_depth_texture);
glBindTexture(GL_TEXTURE_2D, m_screen_depth_texture);
glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT32F, width, height, 0,
GL_DEPTH_COMPONENT, GL_FLOAT, NULL);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D,
m_depth_texture, 0);
m_screen_depth_texture, 0);
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {
Logger::error("FBO incomplete after resize!");
@@ -587,7 +595,7 @@ void Renderer::updata_framebuffer(int width, int height) {
#pragma region render_world
void Renderer::render_world() {
// shader map
glm::mat4 light_space_matrix;
glm::mat4& light_space_matrix = m_parallel_light.light_space_matrix;
auto& m_render_snapshots = m_world.render_snapshots();
auto& camera_pos = m_camera.get_camera_pos();
float texels_per_unit = 0.0f;
@@ -868,7 +876,7 @@ void Renderer::render_world() {
glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_2D, m_screen_texture);
glActiveTexture(GL_TEXTURE2);
glBindTexture(GL_TEXTURE_2D, m_depth_texture);
glBindTexture(GL_TEXTURE_2D, m_screen_depth_texture);
glActiveTexture(GL_TEXTURE0);
for (const auto& snapshot : m_render_snapshots) {
@@ -980,4 +988,6 @@ float& Renderer::cloud_speed() { return m_cloud_speed; }
float& Renderer::cloud_threshold_low() { return m_cloud_threshold_low; }
float& Renderer::cloud_threshold_high() { return m_cloud_threshold_high; }
float& Renderer::refract_strength() { return m_refract_strength; }
float& Renderer::underwater_fog_density() { return m_underwater_fog_density; }
float& Renderer::water_density() { return m_water_density; }
} // namespace Cubed