From 3e76785b4061cb309bc3c9d56f364623e76ff60a Mon Sep 17 00:00:00 2001 From: Orkuncakilkaya Date: Sun, 13 Sep 2026 00:29:37 +0300 Subject: [PATCH] fix(render3d): lakes clipped to their ellipse; r3d_fog_base A water body other than the reflecting one drew its whole bounding rectangle, so the corners outside the lake's carved ellipse showed water over dry ground; those bodies now discard outside the ellipse. r3d_fog_base (default 0) is the height the height fog is measured from, so maps sharing one datum at different heights get the same air. Co-Authored-By: Claude Opus 5 --- changes/render3d-fog-base.md | 3 +++ changes/render3d-lake-ellipse.md | 3 +++ packages/ludic.render3d/render.ludic | 2 ++ packages/ludic.render3d/shaders/lighting.glsl | 5 ++++- packages/ludic.render3d/shaders/water.frag | 10 ++++++++++ packages/ludic.render3d/water.ludic | 4 ++++ 6 files changed, 26 insertions(+), 1 deletion(-) create mode 100644 changes/render3d-fog-base.md create mode 100644 changes/render3d-lake-ellipse.md diff --git a/changes/render3d-fog-base.md b/changes/render3d-fog-base.md new file mode 100644 index 00000000..718c066c --- /dev/null +++ b/changes/render3d-fog-base.md @@ -0,0 +1,3 @@ +bump: patch +type: feat +**ludic.render3d: `r3d_fog_base`** — the height the height fog's density is measured from (default 0, the world's y = 0). Two maps sharing one height datum, one far lower than the other, no longer give the lower one many times thicker air. diff --git a/changes/render3d-lake-ellipse.md b/changes/render3d-lake-ellipse.md new file mode 100644 index 00000000..a6fadeb6 --- /dev/null +++ b/changes/render3d-lake-ellipse.md @@ -0,0 +1,3 @@ +bump: patch +type: fix +**ludic.render3d: lakes are ellipses** — a water body other than the reflecting one was drawn as the whole rectangle of its bounds, so the corners between that rectangle and the lake's carved ellipse showed sheets of water over dry ground. Those bodies are now clipped to the ellipse; the reflecting body (the sea) still fills its rectangle. diff --git a/packages/ludic.render3d/render.ludic b/packages/ludic.render3d/render.ludic index b611d326..25f1db0f 100644 --- a/packages/ludic.render3d/render.ludic +++ b/packages/ludic.render3d/render.ludic @@ -8,6 +8,7 @@ var r3d_sky_prog: int = 0 var r3d_fog_density: int = 0 var r3d_fog_falloff: int = 0 +var r3d_fog_base: int = 0 # the height fog is measured from (float bits); 0 = y = 0 var r3d_time: int = 0 var r3d_ready: bool = false # set before r3d_init to build the landscape from a real height map @@ -30,6 +31,7 @@ function fog_bind(prog: int) -> void { u_f(gl_uniform(prog, "u_spec_scale"), F_ONE) u_f(gl_uniform(prog, "u_fog_density"), r3d_fog_density) u_f(gl_uniform(prog, "u_fog_height_falloff"), r3d_fog_falloff) + u_f(gl_uniform(prog, "u_fog_base"), r3d_fog_base) var cs = r3d_cloud_shadow if Os.has_env("R3D_NOCLOUD") { cs = F_ZERO } u_f(gl_uniform(prog, "u_cloud_shadow"), cs) diff --git a/packages/ludic.render3d/shaders/lighting.glsl b/packages/ludic.render3d/shaders/lighting.glsl index 6595b6ce..00c70b51 100644 --- a/packages/ludic.render3d/shaders/lighting.glsl +++ b/packages/ludic.render3d/shaders/lighting.glsl @@ -15,6 +15,7 @@ uniform vec3 u_cam_pos; uniform float u_prefilter_levels; uniform float u_fog_density; uniform float u_fog_height_falloff; +uniform float u_fog_base; // the height the fog's density is measured from (0 = the world's y = 0) uniform float u_clip_y; // planar-reflection pass: discard everything below this height uniform float u_spec_scale; // 1 for surfaces; foliage crowns get a fraction: needles are // tiny rough cylinders, not sheets, and a crown of card quads @@ -263,7 +264,9 @@ vec3 applyFog(vec3 col, vec3 wpos, float dist) { float hf = u_fog_height_falloff; float t = dir.y * hf; float integ = (abs(t) > 1e-4) ? (1.0 - exp(-dist * t)) / t : dist * (1.0 - 0.5 * dist * t); - float fogAmt = u_fog_density * exp(-u_cam_pos.y * hf) * integ; + // measured from u_fog_base: two maps on one height datum, one a thousand metres lower, + // would otherwise have that one's air exp(falloff * 1000) times thicker + float fogAmt = u_fog_density * exp(-(u_cam_pos.y - u_fog_base) * hf) * integ; float f = 1.0 - exp(-fogAmt); vec3 fogCol = skyPrefiltered(vec3(dir.x, max(dir.y, 0.02), dir.z), 0.6); float sunAmt = pow(max(dot(dir, u_sun_dir), 0.0), 8.0); diff --git a/packages/ludic.render3d/shaders/water.frag b/packages/ludic.render3d/shaders/water.frag index 80d7258d..e0763ec4 100644 --- a/packages/ludic.render3d/shaders/water.frag +++ b/packages/ludic.render3d/shaders/water.frag @@ -7,6 +7,9 @@ uniform mat4 u_inv_vp; uniform vec2 u_screen; uniform sampler2D u_refl; // the world mirrored in the surface (rendered by the reflection pass) uniform float u_refl_on; +uniform vec2 u_center; // this body's centre and half extents (world xz) +uniform vec2 u_extent; +uniform float u_clip_ellipse; // 1: the body is the ellipse inside its rectangle (a lake), 0: the whole plane (the sea) uniform sampler2D u_scene; // the scene as drawn before the water: the bed, to refract // wind-streaked capillary ripples (stretched along the wind) over slower swells float waterH(vec2 p, float t) { @@ -21,6 +24,13 @@ vec3 rippleNormal(vec2 p, float t) { return normalize(vec3(-(hx - h) * 0.26 / e, 1.0, -(hz - h) * 0.26 / e)); } void main() { + // A lake is an ellipse - the carved bed, the grass and a game's own water test all use + // one - but its plane is the rectangle around it, and the corners between the two hold + // ground below the lake's level. Without this they show as sheets of water on dry land. + if (u_clip_ellipse > 0.5) { + vec2 q = (v_wpos.xz - u_center) / max(u_extent, vec2(0.001)); + if (dot(q, q) > 1.0) discard; + } vec3 v = normalize(u_cam_pos - v_wpos); float dist = length(u_cam_pos - v_wpos); vec3 n = rippleNormal(v_wpos.xz, u_time); diff --git a/packages/ludic.render3d/water.ludic b/packages/ludic.render3d/water.ludic index 8f18da18..05e8ec78 100644 --- a/packages/ludic.render3d/water.ludic +++ b/packages/ludic.render3d/water.ludic @@ -177,6 +177,10 @@ function water_draw(depth_tex: int) -> void { var r = F_ZERO if i == wb_primary { r = ron } u_f(gl_uniform(p, "u_refl_on"), r) + # every body but the mirrored sea is clipped to the ellipse inside its bounds + var clip = F_ONE + if i == wb_primary { clip = F_ZERO } + u_f(gl_uniform(p, "u_clip_ellipse"), clip) mesh_draw(water_mesh) } gl_disable(GL_BLEND)