From 9ba093bf07216d2dc0d585102b5c8147102bc20d Mon Sep 17 00:00:00 2001 From: Orkuncakilkaya Date: Thu, 10 Sep 2026 16:15:34 +0300 Subject: [PATCH] fix(render3d): water read the window's size, not the frame it drew into The water shader's `u_screen`, and the reflection target's size, were both taken from gl_w/gl_h - the drawable. But gl_FragCoord in that shader runs over the scene target, which is post_w x post_h. They match only at a render scale of 1; at anything less the refraction and depth reads landed in the wrong corner of the frame and the lake showed a squashed copy of it instead of its own bed. Both now come from the scene target. The reflection is sized from it too, which also stops it paying for pixels the water never samples. R3D_DUMP_REFL reads the target's own w/h rather than recomputing them. Co-Authored-By: Claude Opus 5 --- packages/ludic.render3d/water.ludic | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/packages/ludic.render3d/water.ludic b/packages/ludic.render3d/water.ludic index b195455d..45f51a95 100644 --- a/packages/ludic.render3d/water.ludic +++ b/packages/ludic.render3d/water.ludic @@ -24,7 +24,10 @@ function water_reflection_pass() -> void { if water_refl == null { if Os.has_env("R3D_REFLDIV") { water_refl_div = Text.to_int(Os.env("R3D_REFLDIV")) } if water_refl != null { target_free(water_refl) } - water_refl = target_new(gl_w / water_refl_div, gl_h / water_refl_div, GL_RGBA16F, GL_RGBA, GL_HALF_FLOAT, true, GL_LINEAR) + # sized from the scene target, not the window: with a render scale below 1 the frame + # this reflection is composited into is smaller than the drawable, and a reflection + # rendered at the window's size would be paying for pixels the water never samples + water_refl = target_new(post_w / water_refl_div, post_h / water_refl_div, GL_RGBA16F, GL_RGBA, GL_HALF_FLOAT, true, GL_LINEAR) water_saved = words(16 * 4 + 3) } # save the camera @@ -67,7 +70,7 @@ function water_reflection_pass() -> void { sc_skip_blade = sb sc_freeze = false gl_cull_face(GL_BACK) - if Os.has_env("R3D_DUMP_REFL") and not water_dumped { water_dumped = true; tex_dump(water_refl.color, gl_w / water_refl_div, gl_h / water_refl_div, "build/dbg_refl.ppm") } + if Os.has_env("R3D_DUMP_REFL") and not water_dumped { water_dumped = true; tex_dump(water_refl.color, water_refl.w, water_refl.h, "build/dbg_refl.ppm") } # restore r3d_clip_y = 0xCF000000 m4_copy(cam_view, water_saved) @@ -96,7 +99,11 @@ function water_draw(depth_tex: int) -> void { u_f(gl_uniform(p, "u_level"), water_level) u_f2(gl_uniform(p, "u_center"), water_cx, water_cz) u_f2(gl_uniform(p, "u_extent"), water_ex, water_ez) - u_f2(gl_uniform(p, "u_screen"), fi(gl_w), fi(gl_h)) + # gl_FragCoord here runs over the scene target, which is post_w x post_h — not the + # window. They are the same size only at a render scale of 1; at anything less, taking + # 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(gl_uniform(p, "u_screen"), fi(post_w), fi(post_h)) var ron = F_ZERO if water_refl != null { # bind on its own unit first: generating the mip chain re-binds the texture on the active unit,