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 <noreply@anthropic.com>
This commit is contained in:
parent
5ca14eeaec
commit
4975c60ac1
6 changed files with 62 additions and 7 deletions
9
changes/contact-darkening.md
Normal file
9
changes/contact-darkening.md
Normal file
|
|
@ -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.
|
||||||
9
changes/the-world-knows-you-are-there.md
Normal file
9
changes/the-world-knows-you-are-there.md
Normal file
|
|
@ -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.
|
||||||
Binary file not shown.
|
|
@ -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_center 136 vec2 1 0
|
||||||
U 46e2325c vert u_extent 144 vec2 1 0
|
U 46e2325c vert u_extent 144 vec2 1 0
|
||||||
I 46e2325c a_xz 0 vec2
|
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_vp 0 mat4 5 64
|
||||||
U 46e2325c frag u_cascade_split 320 float 5 16
|
U 46e2325c frag u_cascade_split 320 float 5 16
|
||||||
U 46e2325c frag u_cascade_range 400 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_inscatter 812 float 1 0
|
||||||
U 46e2325c frag u_fog_desat 816 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_view 832 mat4 1 0
|
||||||
U 46e2325c frag u_inv_vp 896 mat4 1 0
|
U 46e2325c frag u_wade 896 vec3 1 0
|
||||||
U 46e2325c frag u_screen 960 vec2 1 0
|
U 46e2325c frag u_inv_vp 912 mat4 1 0
|
||||||
U 46e2325c frag u_refl_on 968 float 1 0
|
U 46e2325c frag u_screen 976 vec2 1 0
|
||||||
U 46e2325c frag u_center 976 vec2 1 0
|
U 46e2325c frag u_refl_on 984 float 1 0
|
||||||
U 46e2325c frag u_extent 984 vec2 1 0
|
U 46e2325c frag u_center 992 vec2 1 0
|
||||||
U 46e2325c frag u_clip_ellipse 992 float 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_brdf 2
|
||||||
T 46e2325c u_depth 3
|
T 46e2325c u_depth 3
|
||||||
T 46e2325c u_irradiance 4
|
T 46e2325c u_irradiance 4
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
in vec3 v_wpos;
|
in vec3 v_wpos;
|
||||||
out vec4 o_color;
|
out vec4 o_color;
|
||||||
uniform mat4 u_view;
|
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 sampler2D u_depth; // scene depth (resolved) for shore softness / depth tint
|
||||||
uniform mat4 u_inv_vp;
|
uniform mat4 u_inv_vp;
|
||||||
uniform vec2 u_screen;
|
uniform vec2 u_screen;
|
||||||
|
|
@ -40,6 +41,24 @@ void main() {
|
||||||
float dist = length(u_cam_pos - v_wpos);
|
float dist = length(u_cam_pos - v_wpos);
|
||||||
vec3 n = rippleNormal(v_wpos.xz, u_time);
|
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
|
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
|
// how deep the ground is under this pixel: from the scene depth
|
||||||
vec2 suv = gl_FragCoord.xy / u_screen;
|
vec2 suv = gl_FragCoord.xy / u_screen;
|
||||||
float sd = texture(u_depth, suv).r;
|
float sd = texture(u_depth, suv).r;
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,11 @@ var water_cz: int = 0
|
||||||
var water_ex: int = 0
|
var water_ex: int = 0
|
||||||
var water_ez: int = 0
|
var water_ez: int = 0
|
||||||
var water_on: bool = false
|
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: 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_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
|
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
|
# 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.
|
# 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))
|
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
|
var ron = F_ZERO
|
||||||
if water_refl != null and wb_primary >= 0 {
|
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,
|
# bind on its own unit first: generating the mip chain re-binds the texture on the active unit,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue