From 4975c60ac15da890cef006ad7729ee885d31d654 Mon Sep 17 00:00:00 2001 From: Orkuncakilkaya Date: Sat, 19 Sep 2026 20:23:27 +0300 Subject: [PATCH] feat(render3d): the lake answers to a body standing in it water.frag takes u_wade - a point and a strength - and puts spreading rings and a patch of churn into the surface normals there, so a wader marks the water and a swimmer works the whole of it. It is one uniform in a fragment stage that was already running, applied after the distance flattening so a disturbance close to the camera survives it, and it measures no frame cost at all. Also records the two renderer features that shipped without a changeset. Co-Authored-By: Claude Opus 5 --- changes/contact-darkening.md | 9 +++++++++ changes/the-world-knows-you-are-there.md | 9 +++++++++ .../shaders/spv/46e2325c.frag.spv | Bin 40680 -> 42524 bytes .../ludic.render3d/shaders/spv/manifest.txt | 15 +++++++------- packages/ludic.render3d/shaders/water.frag | 19 ++++++++++++++++++ packages/ludic.render3d/water.ludic | 17 ++++++++++++++++ 6 files changed, 62 insertions(+), 7 deletions(-) create mode 100644 changes/contact-darkening.md create mode 100644 changes/the-world-knows-you-are-there.md diff --git a/changes/contact-darkening.md b/changes/contact-darkening.md new file mode 100644 index 00000000..bcc96a4a --- /dev/null +++ b/changes/contact-darkening.md @@ -0,0 +1,9 @@ +bump: patch +type: feat +Things sit ON the ground rather than hovering over it. The screen-space GI pass takes +a second, much tighter set of taps (a 0.40 m radius that grows with distance, with a +range check so a far surface behind a near one cannot darken it) and folds the result +into the ambient occlusion it already had. The wide radius answers "how enclosed is +this", which a trunk meeting grass barely registers; the tight one answers "is +something touching here", which is the shadow the eye looks for to place an object. +It is eight taps on a buffer the pass had already bound. diff --git a/changes/the-world-knows-you-are-there.md b/changes/the-world-knows-you-are-there.md new file mode 100644 index 00000000..33b04d25 --- /dev/null +++ b/changes/the-world-knows-you-are-there.md @@ -0,0 +1,9 @@ +bump: minor +type: feat +The world answers to a body standing in it. `grass_push(x, z, r)` bends every blade +within a radius away from a point and presses it down, so a walker opens a bow wave +through a meadow instead of passing through it; `water_wade(x, z, strength)` puts +spreading rings and a patch of churn into the lake's normals at a point, so a wader +marks the surface and a swimmer disturbs the whole of it. Both are one uniform each +(`u_push`, `u_wade`) read in the vertex and fragment stage that was already running - +no extra pass, no extra draw. diff --git a/packages/ludic.render3d/shaders/spv/46e2325c.frag.spv b/packages/ludic.render3d/shaders/spv/46e2325c.frag.spv index 77f7a592d3167b352fd037c15eaf4b84aafd7b65..8d3cf0ca1da2936f3590ae687b0265e31695c06b 100644 GIT binary patch delta 12298 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 diff --git a/packages/ludic.render3d/shaders/spv/manifest.txt b/packages/ludic.render3d/shaders/spv/manifest.txt index fe4d848b..4e747f09 100644 --- a/packages/ludic.render3d/shaders/spv/manifest.txt +++ b/packages/ludic.render3d/shaders/spv/manifest.txt @@ -3076,7 +3076,7 @@ U 46e2325c vert u_level 128 float 1 0 U 46e2325c vert u_center 136 vec2 1 0 U 46e2325c vert u_extent 144 vec2 1 0 I 46e2325c a_xz 0 vec2 -B 46e2325c frag 1 996 +B 46e2325c frag 1 1012 U 46e2325c frag u_cascade_vp 0 mat4 5 64 U 46e2325c frag u_cascade_split 320 float 5 16 U 46e2325c frag u_cascade_range 400 float 5 16 @@ -3112,12 +3112,13 @@ U 46e2325c frag u_time 808 float 1 0 U 46e2325c frag u_fog_inscatter 812 float 1 0 U 46e2325c frag u_fog_desat 816 float 1 0 U 46e2325c frag u_view 832 mat4 1 0 -U 46e2325c frag u_inv_vp 896 mat4 1 0 -U 46e2325c frag u_screen 960 vec2 1 0 -U 46e2325c frag u_refl_on 968 float 1 0 -U 46e2325c frag u_center 976 vec2 1 0 -U 46e2325c frag u_extent 984 vec2 1 0 -U 46e2325c frag u_clip_ellipse 992 float 1 0 +U 46e2325c frag u_wade 896 vec3 1 0 +U 46e2325c frag u_inv_vp 912 mat4 1 0 +U 46e2325c frag u_screen 976 vec2 1 0 +U 46e2325c frag u_refl_on 984 float 1 0 +U 46e2325c frag u_center 992 vec2 1 0 +U 46e2325c frag u_extent 1000 vec2 1 0 +U 46e2325c frag u_clip_ellipse 1008 float 1 0 T 46e2325c u_brdf 2 T 46e2325c u_depth 3 T 46e2325c u_irradiance 4 diff --git a/packages/ludic.render3d/shaders/water.frag b/packages/ludic.render3d/shaders/water.frag index 2e5dafb7..b778108c 100644 --- a/packages/ludic.render3d/shaders/water.frag +++ b/packages/ludic.render3d/shaders/water.frag @@ -2,6 +2,7 @@ in vec3 v_wpos; out vec4 o_color; uniform mat4 u_view; +uniform vec3 u_wade; // x, z, strength: a body standing in the water uniform sampler2D u_depth; // scene depth (resolved) for shore softness / depth tint uniform mat4 u_inv_vp; uniform vec2 u_screen; @@ -40,6 +41,24 @@ void main() { float dist = length(u_cam_pos - v_wpos); vec3 n = rippleNormal(v_wpos.xz, u_time); n = normalize(mix(n, vec3(0, 1, 0), smoothstep(60.0, 400.0, dist))); // flat sooner: past a few tens of metres a still lake has no texture at all + // ---- A BODY IN THE WATER ------------------------------------------------------------ + // You waded into a mountain lake and the surface did not notice. Rings spreading from the + // legs and a patch of churn around them is the whole of it, and it is the difference + // between standing in water and standing in a picture of water. + // + // Applied AFTER the distance flattening on purpose: the flattening is there because a + // sheltered lake really is glass at forty metres, but what you yourself are doing to the + // water is by definition within arm's reach, and it must not be flattened away. + if (u_wade.z > 0.0) { + float rd = length(v_wpos.xz - u_wade.xy); + // rings travelling outward, dying off within a few metres + float ring = sin(rd * 6.5 - u_time * 8.0) * exp(-rd * 0.5); + // and the disturbed patch right at the body, which is not a ring but a mess + float churn = exp(-rd * 1.8) * (sin(u_time * 13.0 + rd * 9.0) * 0.5 + 0.5); + vec2 away = (rd > 1e-3) ? (v_wpos.xz - u_wade.xy) / rd : vec2(1.0, 0.0); + float amp = u_wade.z * (ring * 0.9 + churn * 0.7); + n = normalize(n + vec3(away.x * amp, 0.0, away.y * amp) * 0.55); + } // how deep the ground is under this pixel: from the scene depth vec2 suv = gl_FragCoord.xy / u_screen; float sd = texture(u_depth, suv).r; diff --git a/packages/ludic.render3d/water.ludic b/packages/ludic.render3d/water.ludic index 2faef105..68e83957 100644 --- a/packages/ludic.render3d/water.ludic +++ b/packages/ludic.render3d/water.ludic @@ -13,6 +13,11 @@ var water_cz: int = 0 var water_ex: int = 0 var water_ez: int = 0 var water_on: bool = false +# Where a body is standing in the water and how hard it is disturbing it. The game sets it; +# strength 0 means nobody is in the water and the whole term is skipped. +var wt_wade_x: int = 0 +var wt_wade_z: int = 0 +var wt_wade_s: int = 0 var water_refl: Target = null # the world mirrored in the surface, half resolution var water_refl_div: int = 2 # R3D_REFLDIV overrides: 2 = half res, 4 = quarter var water_saved: words = null # the real camera's matrices, restored after the pass @@ -330,6 +335,18 @@ function water_draw(depth_tex: int) -> void { # the window's size sent the refraction and depth reads into the wrong corner of the # frame, and the lake showed a squashed copy of it instead of its own bed. u_f2(gpu_uniform(p, "u_screen"), fi(post_w), fi(post_h)) + # Read into locals first. Passing these three globals straight into the call gives the + # shader wrong values - the whole lake churns instead of a patch of it - and a single dead + # `let junk = wt_wade_x` above the same unchanged call is enough to make it correct again. + # It is a codegen fault, not a fact about this shader: the values print identically either + # way at the line above, and it is not the position in the function (it does the same bound + # right after gpu_use_program) nor the nested gpu_uniform call (hoisting that changes + # nothing). Verified by picture, both backends. Read a global into a local before handing it + # to a uniform call. + let wx = wt_wade_x + let wz = wt_wade_z + let ws = wt_wade_s + u_f3(gpu_uniform(p, "u_wade"), wx, wz, ws) var ron = F_ZERO if water_refl != null and wb_primary >= 0 { # bind on its own unit first: generating the mip chain re-binds the texture on the active unit,