fix(render3d): a lake can be the mirrored water, with its own wet shore

A reflecting body is clipped to its ellipse like every other unless it is an
unbounded sea, so a map whose reflection belongs to its lake (Maroon Lake, once
its sea sits below it) does not draw that lake's level over every hollow in the
survey. The terrain's wet shore and its forest and scree gates read the carved
lake's line inside its outline (u_lake), the rule grass already used. SPIR-V
regenerated.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-16 15:15:28 +03:00
parent 132deac5bc
commit 6dfdee75e2
11 changed files with 62 additions and 35 deletions

8
changes/lake-water.md Normal file
View file

@ -0,0 +1,8 @@
bump: patch
type: fix
**A lake can be the water that mirrors the world** — a reflecting body is clipped to its
ellipse like any other unless it is an unbounded sea, so a map whose reflection belongs to
its lake no longer floods every hollow in the survey with that lake's level; and the
terrain's wet shore, forest and scree gates read the carved lake's line inside its outline
(`u_lake`), the rule the grass already used, so a lake above the sea keeps its wet bank and
no forest is painted down its bed.

View file

@ -2297,7 +2297,7 @@ U 2e2908c8 vert u_node 176 vec3 1 0
U 2e2908c8 vert u_morph 192 vec2 1 0 U 2e2908c8 vert u_morph 192 vec2 1 0
U 2e2908c8 vert u_grid 200 float 1 0 U 2e2908c8 vert u_grid 200 float 1 0
I 2e2908c8 a_xz 0 vec2 I 2e2908c8 a_xz 0 vec2
B 2e2908c8 frag 1 872 B 2e2908c8 frag 1 904
U 2e2908c8 frag u_cascade_vp 0 mat4 5 64 U 2e2908c8 frag u_cascade_vp 0 mat4 5 64
U 2e2908c8 frag u_cascade_split 320 float 5 16 U 2e2908c8 frag u_cascade_split 320 float 5 16
U 2e2908c8 frag u_cascade_range 400 float 5 16 U 2e2908c8 frag u_cascade_range 400 float 5 16
@ -2333,10 +2333,11 @@ U 2e2908c8 frag u_carpet_on 776 float 1 0
U 2e2908c8 frag u_snow_line 780 float 1 0 U 2e2908c8 frag u_snow_line 780 float 1 0
U 2e2908c8 frag u_lake_level 784 float 1 0 U 2e2908c8 frag u_lake_level 784 float 1 0
U 2e2908c8 frag u_sea_level 788 float 1 0 U 2e2908c8 frag u_sea_level 788 float 1 0
U 2e2908c8 frag u_origin 792 vec2 1 0 U 2e2908c8 frag u_lake 800 vec4 1 0
U 2e2908c8 frag u_view 800 mat4 1 0 U 2e2908c8 frag u_origin 816 vec2 1 0
U 2e2908c8 frag u_far_split 864 float 1 0 U 2e2908c8 frag u_view 832 mat4 1 0
U 2e2908c8 frag u_far_band 868 float 1 0 U 2e2908c8 frag u_far_split 896 float 1 0
U 2e2908c8 frag u_far_band 900 float 1 0
T 2e2908c8 u_brdf 2 T 2e2908c8 u_brdf 2
T 2e2908c8 u_carpet 3 T 2e2908c8 u_carpet 3
T 2e2908c8 u_grass_a 4 T 2e2908c8 u_grass_a 4
@ -2365,7 +2366,7 @@ U 1f95f31b vert u_node 176 vec3 1 0
U 1f95f31b vert u_morph 192 vec2 1 0 U 1f95f31b vert u_morph 192 vec2 1 0
U 1f95f31b vert u_grid 200 float 1 0 U 1f95f31b vert u_grid 200 float 1 0
I 1f95f31b a_xz 0 vec2 I 1f95f31b a_xz 0 vec2
B 1f95f31b frag 1 872 B 1f95f31b frag 1 904
U 1f95f31b frag u_cascade_vp 0 mat4 5 64 U 1f95f31b frag u_cascade_vp 0 mat4 5 64
U 1f95f31b frag u_cascade_split 320 float 5 16 U 1f95f31b frag u_cascade_split 320 float 5 16
U 1f95f31b frag u_cascade_range 400 float 5 16 U 1f95f31b frag u_cascade_range 400 float 5 16
@ -2401,10 +2402,11 @@ U 1f95f31b frag u_carpet_on 776 float 1 0
U 1f95f31b frag u_snow_line 780 float 1 0 U 1f95f31b frag u_snow_line 780 float 1 0
U 1f95f31b frag u_lake_level 784 float 1 0 U 1f95f31b frag u_lake_level 784 float 1 0
U 1f95f31b frag u_sea_level 788 float 1 0 U 1f95f31b frag u_sea_level 788 float 1 0
U 1f95f31b frag u_origin 792 vec2 1 0 U 1f95f31b frag u_lake 800 vec4 1 0
U 1f95f31b frag u_view 800 mat4 1 0 U 1f95f31b frag u_origin 816 vec2 1 0
U 1f95f31b frag u_far_split 864 float 1 0 U 1f95f31b frag u_view 832 mat4 1 0
U 1f95f31b frag u_far_band 868 float 1 0 U 1f95f31b frag u_far_split 896 float 1 0
U 1f95f31b frag u_far_band 900 float 1 0
T 1f95f31b u_brdf 2 T 1f95f31b u_brdf 2
T 1f95f31b u_carpet 3 T 1f95f31b u_carpet 3
T 1f95f31b u_grass_a 4 T 1f95f31b u_grass_a 4
@ -2433,7 +2435,7 @@ U f4382918 vert u_node 176 vec3 1 0
U f4382918 vert u_morph 192 vec2 1 0 U f4382918 vert u_morph 192 vec2 1 0
U f4382918 vert u_grid 200 float 1 0 U f4382918 vert u_grid 200 float 1 0
I f4382918 a_xz 0 vec2 I f4382918 a_xz 0 vec2
B f4382918 frag 1 872 B f4382918 frag 1 904
U f4382918 frag u_cascade_vp 0 mat4 5 64 U f4382918 frag u_cascade_vp 0 mat4 5 64
U f4382918 frag u_cascade_split 320 float 5 16 U f4382918 frag u_cascade_split 320 float 5 16
U f4382918 frag u_cascade_range 400 float 5 16 U f4382918 frag u_cascade_range 400 float 5 16
@ -2469,10 +2471,11 @@ U f4382918 frag u_carpet_on 776 float 1 0
U f4382918 frag u_snow_line 780 float 1 0 U f4382918 frag u_snow_line 780 float 1 0
U f4382918 frag u_lake_level 784 float 1 0 U f4382918 frag u_lake_level 784 float 1 0
U f4382918 frag u_sea_level 788 float 1 0 U f4382918 frag u_sea_level 788 float 1 0
U f4382918 frag u_origin 792 vec2 1 0 U f4382918 frag u_lake 800 vec4 1 0
U f4382918 frag u_view 800 mat4 1 0 U f4382918 frag u_origin 816 vec2 1 0
U f4382918 frag u_far_split 864 float 1 0 U f4382918 frag u_view 832 mat4 1 0
U f4382918 frag u_far_band 868 float 1 0 U f4382918 frag u_far_split 896 float 1 0
U f4382918 frag u_far_band 900 float 1 0
T f4382918 u_brdf 2 T f4382918 u_brdf 2
T f4382918 u_carpet 3 T f4382918 u_carpet 3
T f4382918 u_grass_a 4 T f4382918 u_grass_a 4
@ -2501,7 +2504,7 @@ U f2952361 vert u_node 176 vec3 1 0
U f2952361 vert u_morph 192 vec2 1 0 U f2952361 vert u_morph 192 vec2 1 0
U f2952361 vert u_grid 200 float 1 0 U f2952361 vert u_grid 200 float 1 0
I f2952361 a_xz 0 vec2 I f2952361 a_xz 0 vec2
B f2952361 frag 1 872 B f2952361 frag 1 904
U f2952361 frag u_cascade_vp 0 mat4 5 64 U f2952361 frag u_cascade_vp 0 mat4 5 64
U f2952361 frag u_cascade_split 320 float 5 16 U f2952361 frag u_cascade_split 320 float 5 16
U f2952361 frag u_cascade_range 400 float 5 16 U f2952361 frag u_cascade_range 400 float 5 16
@ -2537,10 +2540,11 @@ U f2952361 frag u_carpet_on 776 float 1 0
U f2952361 frag u_snow_line 780 float 1 0 U f2952361 frag u_snow_line 780 float 1 0
U f2952361 frag u_lake_level 784 float 1 0 U f2952361 frag u_lake_level 784 float 1 0
U f2952361 frag u_sea_level 788 float 1 0 U f2952361 frag u_sea_level 788 float 1 0
U f2952361 frag u_origin 792 vec2 1 0 U f2952361 frag u_lake 800 vec4 1 0
U f2952361 frag u_view 800 mat4 1 0 U f2952361 frag u_origin 816 vec2 1 0
U f2952361 frag u_far_split 864 float 1 0 U f2952361 frag u_view 832 mat4 1 0
U f2952361 frag u_far_band 868 float 1 0 U f2952361 frag u_far_split 896 float 1 0
U f2952361 frag u_far_band 900 float 1 0
T f2952361 u_brdf 2 T f2952361 u_brdf 2
T f2952361 u_carpet 3 T f2952361 u_carpet 3
T f2952361 u_grass_a 4 T f2952361 u_grass_a 4
@ -2569,7 +2573,7 @@ U a0f2dbac vert u_node 176 vec3 1 0
U a0f2dbac vert u_morph 192 vec2 1 0 U a0f2dbac vert u_morph 192 vec2 1 0
U a0f2dbac vert u_grid 200 float 1 0 U a0f2dbac vert u_grid 200 float 1 0
I a0f2dbac a_xz 0 vec2 I a0f2dbac a_xz 0 vec2
B a0f2dbac frag 1 872 B a0f2dbac frag 1 904
U a0f2dbac frag u_cascade_vp 0 mat4 5 64 U a0f2dbac frag u_cascade_vp 0 mat4 5 64
U a0f2dbac frag u_cascade_split 320 float 5 16 U a0f2dbac frag u_cascade_split 320 float 5 16
U a0f2dbac frag u_cascade_range 400 float 5 16 U a0f2dbac frag u_cascade_range 400 float 5 16
@ -2605,10 +2609,11 @@ U a0f2dbac frag u_carpet_on 776 float 1 0
U a0f2dbac frag u_snow_line 780 float 1 0 U a0f2dbac frag u_snow_line 780 float 1 0
U a0f2dbac frag u_lake_level 784 float 1 0 U a0f2dbac frag u_lake_level 784 float 1 0
U a0f2dbac frag u_sea_level 788 float 1 0 U a0f2dbac frag u_sea_level 788 float 1 0
U a0f2dbac frag u_origin 792 vec2 1 0 U a0f2dbac frag u_lake 800 vec4 1 0
U a0f2dbac frag u_view 800 mat4 1 0 U a0f2dbac frag u_origin 816 vec2 1 0
U a0f2dbac frag u_far_split 864 float 1 0 U a0f2dbac frag u_view 832 mat4 1 0
U a0f2dbac frag u_far_band 868 float 1 0 U a0f2dbac frag u_far_split 896 float 1 0
U a0f2dbac frag u_far_band 900 float 1 0
T a0f2dbac u_brdf 2 T a0f2dbac u_brdf 2
T a0f2dbac u_carpet 3 T a0f2dbac u_carpet 3
T a0f2dbac u_grass_a 4 T a0f2dbac u_grass_a 4
@ -2637,7 +2642,7 @@ U 5e4589a1 vert u_node 176 vec3 1 0
U 5e4589a1 vert u_morph 192 vec2 1 0 U 5e4589a1 vert u_morph 192 vec2 1 0
U 5e4589a1 vert u_grid 200 float 1 0 U 5e4589a1 vert u_grid 200 float 1 0
I 5e4589a1 a_xz 0 vec2 I 5e4589a1 a_xz 0 vec2
B 5e4589a1 frag 1 872 B 5e4589a1 frag 1 904
U 5e4589a1 frag u_cascade_vp 0 mat4 5 64 U 5e4589a1 frag u_cascade_vp 0 mat4 5 64
U 5e4589a1 frag u_cascade_split 320 float 5 16 U 5e4589a1 frag u_cascade_split 320 float 5 16
U 5e4589a1 frag u_cascade_range 400 float 5 16 U 5e4589a1 frag u_cascade_range 400 float 5 16
@ -2673,10 +2678,11 @@ U 5e4589a1 frag u_carpet_on 776 float 1 0
U 5e4589a1 frag u_snow_line 780 float 1 0 U 5e4589a1 frag u_snow_line 780 float 1 0
U 5e4589a1 frag u_lake_level 784 float 1 0 U 5e4589a1 frag u_lake_level 784 float 1 0
U 5e4589a1 frag u_sea_level 788 float 1 0 U 5e4589a1 frag u_sea_level 788 float 1 0
U 5e4589a1 frag u_origin 792 vec2 1 0 U 5e4589a1 frag u_lake 800 vec4 1 0
U 5e4589a1 frag u_view 800 mat4 1 0 U 5e4589a1 frag u_origin 816 vec2 1 0
U 5e4589a1 frag u_far_split 864 float 1 0 U 5e4589a1 frag u_view 832 mat4 1 0
U 5e4589a1 frag u_far_band 868 float 1 0 U 5e4589a1 frag u_far_split 896 float 1 0
U 5e4589a1 frag u_far_band 900 float 1 0
T 5e4589a1 u_brdf 2 T 5e4589a1 u_brdf 2
T 5e4589a1 u_carpet 3 T 5e4589a1 u_carpet 3
T 5e4589a1 u_grass_a 4 T 5e4589a1 u_grass_a 4

View file

@ -19,6 +19,15 @@ uniform float u_carpet_on;
uniform float u_snow_line; uniform float u_snow_line;
uniform float u_lake_level; // the ground just above the water is wet and dark uniform float u_lake_level; // the ground just above the water is wet and dark
uniform float u_sea_level; // the sea's line: shore, forest and scree gates (terrain_sea) uniform float u_sea_level; // the sea's line: shore, forest and scree gates (terrain_sea)
uniform vec4 u_lake; // the carved lake's outline: centre x/z, half extents (zero = none)
// The water line that applies here: the sea everywhere, the lake's own inside its outline -
// the rule the grass and the game's water test use. One line for both put a lake above the
// sea with no wet shore and forest painted down its bed.
float waterLine(vec2 xz) {
float wl = u_sea_level;
if (u_lake.z > 0.0) { vec2 q = (xz - u_lake.xy) / u_lake.zw; if (dot(q, q) < 1.0) wl = max(wl, u_lake_level); }
return wl;
}
uniform vec2 u_origin; // world offset of the terrain grid uniform vec2 u_origin; // world offset of the terrain grid
// The ground's own sun shadow comes from the baked height-field map (tershadow.frag), // The ground's own sun shadow comes from the baked height-field map (tershadow.frag),
@ -251,14 +260,14 @@ vec3 groundShade(vec3 p, vec3 N, float slope, float dist, float viewDepth, bool
vec3 ocf = textureLod(u_ortho, v_huv, 2.0).rgb; vec3 ocf = textureLod(u_ortho, v_huv, 2.0).rgb;
float gx = ocf.g - max(ocf.r, ocf.b); float gx = ocf.g - max(ocf.r, ocf.b);
// (the photograph is sampled linear: sRGB 72 is 0.06, 92 is 0.11) // (the photograph is sampled linear: sRGB 72 is 0.06, 92 is 0.11)
forestW = smoothstep(0.12, 0.06, max(ocf.r, max(ocf.g, ocf.b))) * smoothstep(0.004, 0.012, gx) * smoothstep(u_sea_level + 0.8, u_sea_level + 1.8, p.y); forestW = smoothstep(0.12, 0.06, max(ocf.r, max(ocf.g, ocf.b))) * smoothstep(0.004, 0.012, gx) * smoothstep(waterLine(p.xz) + 0.8, waterLine(p.xz) + 1.8, p.y);
// Classify from a ~60 m blur, never from the pixels: the survey's 10 m pixels carry // Classify from a ~60 m blur, never from the pixels: the survey's 10 m pixels carry
// a foot trail as a broken line of bare ground, and thresholding them painted it // a foot trail as a broken line of bare ground, and thresholding them painted it
// across the meadow as tan dashes (and, on the CPU, lined boulders up along it). // across the meadow as tan dashes (and, on the CPU, lined boulders up along it).
vec3 ocl = textureLod(u_ortho, v_huv, 2.5).rgb; vec3 ocl = textureLod(u_ortho, v_huv, 2.5).rgb;
float mxc = max(ocl.r, max(ocl.g, ocl.b)), mnc = min(ocl.r, min(ocl.g, ocl.b)); float mxc = max(ocl.r, max(ocl.g, ocl.b)), mnc = min(ocl.r, min(ocl.g, ocl.b));
float greenEx = ocl.g - max(ocl.r, ocl.b); float greenEx = ocl.g - max(ocl.r, ocl.b);
screeC = smoothstep(0.008, -0.002, greenEx) * smoothstep(0.06, 0.12, mxc) * (1.0 - smoothstep(0.55, 0.75, mxc)) * smoothstep(u_sea_level + 0.2, u_sea_level + 1.2, p.y); screeC = smoothstep(0.008, -0.002, greenEx) * smoothstep(0.06, 0.12, mxc) * (1.0 - smoothstep(0.55, 0.75, mxc)) * smoothstep(waterLine(p.xz) + 0.2, waterLine(p.xz) + 1.2, p.y);
snowC = smoothstep(0.08, 0.04, mxc - mnc) * smoothstep(0.55, 0.8, mxc); snowC = smoothstep(0.08, 0.04, mxc - mnc) * smoothstep(0.55, 0.8, mxc);
} }
// snow lingering in the high gullies is drawn further down, but whether it can be // snow lingering in the high gullies is drawn further down, but whether it can be
@ -377,7 +386,7 @@ vec3 groundShade(vec3 p, vec3 N, float slope, float dist, float viewDepth, bool
// the bank here: identical noise fields, identical time, identical phase, keyed off // the bank here: identical noise fields, identical time, identical phase, keyed off
// height above the lake instead of depth below it. Across the seam the two agree, so // height above the lake instead of depth below it. Across the seam the two agree, so
// there is nothing there to read as an edge. // there is nothing there to read as an edge.
float above = p.y - u_sea_level; // >0 on land, metres float above = p.y - waterLine(p.xz); // >0 on land, metres
// wet ground: darker and glossier near the water, fading out over ~1.2 m // wet ground: darker and glossier near the water, fading out over ~1.2 m
float wet = smoothstep(1.2, 0.0, above); float wet = smoothstep(1.2, 0.0, above);
alb *= mix(1.0, 0.5, wet * 0.85); alb *= mix(1.0, 0.5, wet * 0.85);

View file

@ -480,6 +480,7 @@ function terrain_bind_prog(p: int) -> void {
var sea = lake var sea = lake
if ter_sea_set { sea = ter_sea_level } if ter_sea_set { sea = ter_sea_level }
u_f(gpu_uniform(p, "u_sea_level"), sea) u_f(gpu_uniform(p, "u_sea_level"), sea)
u_f4(gpu_uniform(p, "u_lake"), ter_lake_cx, ter_lake_cz, ter_lake_ex, ter_lake_ez)
u_mat4(gpu_uniform(p, "u_view"), cam_view) u_mat4(gpu_uniform(p, "u_view"), cam_view)
u_mat4(gpu_uniform(p, "u_proj"), cam_proj) u_mat4(gpu_uniform(p, "u_proj"), cam_proj)
u_f2(gpu_uniform(p, "u_origin"), ter_ox, ter_oz) u_f2(gpu_uniform(p, "u_origin"), ter_ox, ter_oz)

View file

@ -361,9 +361,12 @@ function water_draw(depth_tex: int) -> void {
var r = F_ZERO var r = F_ZERO
if i == wb_primary { r = ron } if i == wb_primary { r = ron }
u_f(gpu_uniform(p, "u_refl_on"), r) u_f(gpu_uniform(p, "u_refl_on"), r)
# every body but the mirrored sea is clipped to the ellipse inside its bounds # Every body is clipped to the ellipse inside its bounds but an unbounded mirrored sea,
# whose rectangle is the point. A mirrored LAKE is clipped like any other: a map whose
# reflection belongs to its lake (the Bells in Maroon Lake) would otherwise draw that
# lake's level over every hollow in the survey.
var clip = F_ONE var clip = F_ONE
if i == wb_primary { clip = F_ZERO } if i == wb_primary and f_gt(f_max(wb_ex[i], wb_ez[i]), fi(10000)) { clip = F_ZERO }
u_f(gpu_uniform(p, "u_clip_ellipse"), clip) u_f(gpu_uniform(p, "u_clip_ellipse"), clip)
mesh_draw(water_mesh) mesh_draw(water_mesh)
} }