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 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-13 00:29:37 +03:00
parent 7a0c03dd21
commit 3e76785b40
6 changed files with 26 additions and 1 deletions

View file

@ -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.

View file

@ -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.

View file

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

View file

@ -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);

View file

@ -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);

View file

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