feat: water effects (#19)

* feat(shaders): add lighting to block accumulation OIT shaders

* feat(renderer): add water rendering with OIT and new shaders

* feat(water): implement screen-space reflection and refraction for water with depth fade and perturbation

* refactor(shader): replace glUniform calls with generic set_loc and remove cached locations

* feat(water): add noise-based caustics and configurable fog density

* 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)

* feat(underwater): add volume scattering and shadow mapping for light shafts
This commit is contained in:
zhenyan121
2026-06-20 09:04:09 +08:00
committed by GitHub
parent ca3fc5e3bf
commit a8d2ddbacd
16 changed files with 813 additions and 126 deletions

View File

@@ -4,10 +4,21 @@ layout (location = 0) out vec4 accum;
layout (location = 1) out float reveal;
in vec2 tc;
in vec3 normal;
in vec3 vert_pos;
in float roughness;
flat in int tex_layer;
in float v_depth;
layout (binding = 0) uniform sampler2DArray samp;
uniform float ambientStrength;
uniform vec3 sunlightColor;
uniform vec3 ambientColor;
uniform vec3 sunlightDir;
uniform vec3 cameraPos;
uniform bool shader_on;
uniform float specularStrength;
float weight(float z, float a) {
float intermediate = 0.03 / (1e-5 + pow(z / 200.0, 4.0));
@@ -15,7 +26,71 @@ float weight(float z, float a) {
}
void main() {
vec4 color = texture(samp, vec3(tc, tex_layer));
vec4 objectColor = texture(samp, vec3(tc, tex_layer));
if (!shader_on) {
vec4 color = objectColor;
float alpha = color.a;
if (alpha < 1e-4) discard;
float w = weight(v_depth, alpha);
accum = vec4(color.rgb * alpha * w, alpha * w);
reveal = alpha;
return;
}
vec3 lightDir = normalize(-sunlightDir);
vec3 norm = normalize(normal);
vec3 V =
normalize(cameraPos - vert_pos);
vec3 H =
normalize(lightDir + V);
vec3 ambient = ambientStrength * ambientColor;
float diff = max(dot(norm, lightDir), 0.0);
vec3 diffuse = diff * sunlightColor;
float r =
clamp(roughness, 0.0, 1.0);
float shininess =
mix(
512.0,
4.0,
r
);
float ks =
mix(
0.8,
0.02,
r
);
float spec = 0.0;
if(diff > 0.0)
{
spec =
ks *
pow(
max(dot(norm,H),0.0),
shininess
);
}
vec3 specular = spec * sunlightColor * specularStrength;
vec4 color = vec4((ambient + diffuse) * objectColor.rgb + specular, objectColor.a);
float alpha = color.a;
if (alpha < 1e-4) discard;

View File

@@ -3,19 +3,28 @@
layout (location = 0) in vec3 pos;
layout (location = 1) in vec2 texCoord;
layout (location = 2) in float layer;
layout (location = 3) in vec3 aNormal;
layout (location = 4) in float Roughness;
out vec2 tc;
out vec3 normal;
out vec3 vert_pos;
flat out int tex_layer;
out float roughness;
out float v_depth;
uniform mat4 mv_matrix;
uniform mat4 proj_matrix;
uniform mat4 norm_matrix;
void main(void) {
vec4 view_pos = mv_matrix * vec4(pos, 1.0);
gl_Position = proj_matrix * view_pos;
vert_pos = pos;
roughness = Roughness;
tc = texCoord;
tex_layer = int(layer);
v_depth = -view_pos.z;
normal = mat3(norm_matrix) * aNormal;
gl_Position = proj_matrix * view_pos;
}

View File

@@ -46,7 +46,7 @@ float fbm(vec2 p) {
return v;
}
void main(void) {
vec3 computeSkyColor(vec3 dir) {
vec3 sund = normalize(sunDir);
float t =
@@ -88,6 +88,13 @@ void main(void) {
sky += glow * sunColor;
return sky;
}
void main(void) {
vec3 sky = computeSkyColor(dir);
frag_color = vec4(sky, 1.0);
//frag_color = vec4(vec3(sunAmount), 1.0);
//frag_color = vec4(t,0,0,1);

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

@@ -0,0 +1,316 @@
#version 460
layout (location = 0) out vec4 accum;
layout (location = 1) out float reveal;
in vec2 tc;
in vec3 normal;
in vec3 vert_pos;
in vec3 world_pos;
in float roughness;
flat in int tex_layer;
in float v_depth;
layout (binding = 0) uniform sampler2DArray samp;
uniform sampler2D sceneColorTex; // binding 1
uniform sampler2D sceneDepthTex; // binding 2
uniform mat4 proj_matrix;
uniform mat4 inv_proj_matrix;
uniform mat4 inv_view_matrix;
uniform float ambientStrength;
uniform vec3 sunlightColor;
uniform vec3 ambientColor;
uniform vec3 sunlightDir;
uniform vec3 cameraPos;
uniform bool shader_on;
uniform float specularStrength;
uniform vec3 skyTop;
uniform vec3 skyBottom;
uniform vec3 sunColor;
uniform float horizonSharpness;
uniform float cloudWhiteMix;
uniform float cloudThresholdLow;
uniform float cloudThresholdHigh;
uniform float time;
uniform vec3 sunDir; // world!
uniform bool underwater;
uniform float refractStrength;
uniform bool enablePerturb;
uniform bool enableDepthFade;
float weight(float z, float a) {
float intermediate = 0.03 / (1e-5 + pow(z / 200.0, 4.0));
return a * clamp(intermediate, 1e-2, 3e2);
}
float hash(vec2 p) {
return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453123);
}
float noise(vec2 p) {
vec2 i = floor(p);
vec2 f = fract(p);
f = f * f * (3.0 - 2.0 * f);
float a = hash(i);
float b = hash(i + vec2(1.0, 0.0));
float c = hash(i + vec2(0.0, 1.0));
float d = hash(i + vec2(1.0, 1.0));
return mix(mix(a, b, f.x), mix(c, d, f.x), f.y);
}
float fbm(vec2 p) {
float v = 0.0;
float amp = 0.5;
for (int i = 0; i < 5; i++) {
v += amp * noise(p);
p *= 2.0;
amp *= 0.5;
}
return v;
}
vec3 computeSkyColor(vec3 dir) {
vec3 sund = normalize(sunDir);
float t =
clamp(
dir.y * 0.5 + 0.5,
0.0,
1.0
);
vec3 sky =
mix(
skyBottom,
skyTop,
pow(t, horizonSharpness)
);
// cloud
if (dir.y > 0.0) {
vec2 cloud_uv = dir.xz / (dir.y + 0.15) * 0.5 + vec2(time * 0.005, time * 0.002);
float cloud_density = fbm(cloud_uv * 2.0);
float safeLow = cloudThresholdLow;
float safeHigh = max(cloudThresholdHigh, cloudThresholdLow + 0.001);
cloud_density = smoothstep(safeLow,safeHigh, cloud_density);
float fade = smoothstep(0.0, 0.3, dir.y) * (1.0 - smoothstep(0.85, 1.0, dir.y));
cloud_density *= fade;
vec3 cloud_color = mix(skyBottom, vec3(1.0), cloudWhiteMix);
sky = mix(sky, cloud_color, cloud_density * 0.6);
}
float sunAmount = max(dot(dir, sund), 0.0);
//float glow = pow(sunAmount, 8.0) * 0.15;
float glow = pow(sunAmount, 8.0) * 0.15 + pow(sunAmount, 32.0) * 0.3;
sky += glow * sunColor;
return sky;
}
// Reconstruct eye-space coordinates from screen UV and depth buffer value
vec3 reconstructViewPos(vec2 uv, float depth) {
vec4 clip = vec4(uv * 2.0 - 1.0, depth * 2.0 - 1.0, 1.0);
vec4 view = inv_proj_matrix * clip;
return view.xyz / view.w;
}
// Screen-space ray marching, origin/dir are in eye space
bool traceSSR(vec3 origin, vec3 dir, out vec2 hitUV) {
const int COARSE_STEPS = 32;
const float COARSE_STEP = 0.6;
const int REFINE_STEPS = 8;
const float THICKNESS = 0.3;
vec3 pos = origin;
vec3 prevPos = origin;
bool foundCoarse = false;
for (int i = 0; i < COARSE_STEPS; ++i) {
prevPos = pos;
pos += dir * COARSE_STEP;
vec4 clip = proj_matrix * vec4(pos, 1.0);
if (clip.w <= 0.0) return false;
vec3 ndc = clip.xyz / clip.w;
if (abs(ndc.x) > 1.0 || abs(ndc.y) > 1.0) return false;
vec2 uv = ndc.xy * 0.5 + 0.5;
float sceneDepth = texture(sceneDepthTex, uv).r;
vec3 scenePos = reconstructViewPos(uv, sceneDepth);
if (pos.z - scenePos.z < 0.0) {
foundCoarse = true;
break;
}
}
if (!foundCoarse) return false;
vec3 lo = prevPos;
vec3 hi = pos;
for (int i = 0; i < REFINE_STEPS; ++i) {
vec3 mid = (lo + hi) * 0.5;
vec4 clip = proj_matrix * vec4(mid, 1.0);
vec3 ndc = clip.xyz / clip.w;
vec2 uv = ndc.xy * 0.5 + 0.5;
float sceneDepth = texture(sceneDepthTex, uv).r;
vec3 scenePos = reconstructViewPos(uv, sceneDepth);
if (mid.z - scenePos.z < 0.0) {
hi = mid;
} else {
lo = mid;
}
}
vec4 finalClip = proj_matrix * vec4(hi, 1.0);
vec3 finalNdc = finalClip.xyz / finalClip.w;
vec2 finalUV = finalNdc.xy * 0.5 + 0.5;
float finalDepth = texture(sceneDepthTex, finalUV).r;
vec3 finalScenePos = reconstructViewPos(finalUV, finalDepth);
if (abs(hi.z - finalScenePos.z) > THICKNESS) return false;
hitUV = finalUV;
return true;
}
void main() {
vec4 objectColor = texture(samp, vec3(tc, tex_layer));
if (!shader_on) {
vec4 color = objectColor;
float alpha = color.a;
if (alpha < 1e-4) discard;
float w = weight(v_depth, alpha);
accum = vec4(color.rgb * alpha * w, alpha * w);
reveal = alpha;
return;
}
// Normal perturbation
vec3 N;
if (enablePerturb) {
const float wave_speed_scale = 40.0;
vec2 waveUV1 = world_pos.xz * 8.0 + vec2(time * 0.05 * wave_speed_scale, time * 0.03 * wave_speed_scale);
vec2 waveUV2 = world_pos.xz * 13.0 + vec2(-time * 0.04 * wave_speed_scale, time * 0.06 * wave_speed_scale);
float wave1 = noise(waveUV1);
float wave2 = noise(waveUV2);
vec2 normalPerturb = vec2(wave1 - 0.5, wave2 - 0.5) * 0.1;
N = normalize(normal + vec3(normalPerturb.x, 0.0, normalPerturb.y));
} else {
N = normalize(normal);
}
vec3 V = normalize(-vert_pos);
vec3 reflectColor;
vec3 refractColor;
float fresnel;
vec2 screenUV = gl_FragCoord.xy / vec2(textureSize(sceneDepthTex, 0));
// water depth
float sceneDepthRaw = texture(sceneDepthTex, screenUV).r;
vec3 sceneViewPos = reconstructViewPos(screenUV, sceneDepthRaw);
float waterDepth = vert_pos.z - sceneViewPos.z;
waterDepth = max(waterDepth, 0.0);
const float DEPTH_FADE_DISTANCE = 10;
float depthFactor = clamp(waterDepth / DEPTH_FADE_DISTANCE, 0.0, 1.0);
if (underwater) {
reflectColor = vec3(0.0);
refractColor = objectColor.rgb;
fresnel = 0.0;
} else {
vec3 R = reflect(-V, N);
vec3 origin = vert_pos + N * 0.05;
vec2 hitUV;
if (traceSSR(origin, R, hitUV)) {
reflectColor = texture(sceneColorTex, hitUV).rgb;
} else {
vec3 worldR = mat3(inv_view_matrix) * R;
reflectColor = computeSkyColor(worldR);
}
float effectiveRefractStrength = refractStrength * (1.0 - depthFactor * 0.5);
vec2 refractOffset = N.xz * effectiveRefractStrength;
vec2 refractUV = clamp(screenUV + refractOffset, vec2(0.001), vec2(0.999));
refractColor = texture(sceneColorTex, refractUV).rgb;
fresnel = pow(1.0 - max(dot(N, V), 0.0), 5.0);
fresnel = mix(0.02, 1.0, fresnel);
}
vec3 lightDir = normalize(-sunlightDir);
vec3 H = normalize(lightDir + V);
vec3 ambient = ambientStrength * ambientColor;
float diff = max(dot(N, lightDir), 0.0);
vec3 diffuse = diff * sunlightColor;
float r = clamp(roughness, 0.0, 1.0);
float shininess = mix(512.0, 4.0, r);
float ks = mix(0.8, 0.02, r);
float spec = 0.0;
if (diff > 0.0) {
spec = ks * pow(max(dot(N, H), 0.0), shininess);
}
vec3 specular = spec * sunlightColor * specularStrength;
vec3 tintedRefract;
if (enableDepthFade) {
vec3 shallowColor = vec3(0.4, 0.75, 0.7);
vec3 deepColor = vec3(0.0, 0.015, 0.045);
vec3 waterTint = mix(shallowColor, deepColor, depthFactor);
tintedRefract = mix(refractColor, refractColor * waterTint, depthFactor);
tintedRefract = mix(tintedRefract, objectColor.rgb, 0.4); // Forcefully blend 40% texture color, independent of depth
} else {
tintedRefract = mix(refractColor, objectColor.rgb, 0.4);
}
//vec3 baseWaterColor = (ambient + diffuse) * objectColor.rgb + specular;
vec3 baseWaterColor = (ambient + diffuse) * tintedRefract + specular;
vec3 finalColor = mix(baseWaterColor, reflectColor, fresnel);
float alpha = objectColor.a;
if (alpha < 1e-4) discard;
float w = weight(v_depth, alpha);
accum = vec4(finalColor * alpha * w, alpha * w);
reveal = alpha;
return;
}

View File

@@ -0,0 +1,32 @@
#version 460
layout (location = 0) in vec3 pos;
layout (location = 1) in vec2 texCoord;
layout (location = 2) in float layer;
layout (location = 3) in vec3 aNormal;
layout (location = 4) in float Roughness;
out vec2 tc;
out vec3 normal;
out vec3 vert_pos;
out vec3 world_pos;
flat out int tex_layer;
out float roughness;
out float v_depth;
uniform mat4 mv_matrix;
uniform mat4 proj_matrix;
uniform mat4 norm_matrix;
void main(void) {
vec4 view_pos = mv_matrix * vec4(pos, 1.0);
world_pos = pos;
vert_pos = view_pos.xyz;
normal = mat3(norm_matrix) * aNormal;
roughness = Roughness;
tc = texCoord;
tex_layer = int(layer);
v_depth = -view_pos.z;
gl_Position = proj_matrix * view_pos;
}