ludic/packages/ludic.render3d/water.ludic
Orkuncakilkaya 6dfdee75e2 fix(render3d): a lake can be the mirrored water, with its own wet shore
A reflecting body is clipped to its ellipse like every other unless it is an
unbounded sea, so a map whose reflection belongs to its lake (Maroon Lake, once
its sea sits below it) does not draw that lake's level over every hollow in the
survey. The terrain's wet shore and its forest and scree gates read the carved
lake's line inside its outline (u_lake), the rule grass already used. SPIR-V
regenerated.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 15:15:28 +03:00

374 lines
18 KiB
Text

# ============================================================================
# water.ludic — still water for the valley floor: a level plane over a region,
# drawn after the opaque scene, showing only where the ground lies below it.
# Sky reflection with fresnel, sun glitter, scrolling ripple normals, a depth
# tinted body read from the scene depth, and soft shores.
# ============================================================================
var water_mesh: Mesh = null
var water_prog: int = 0
var water_level: int = 0
var water_cx: int = 0
var water_cz: int = 0
var water_ex: int = 0
var water_ez: int = 0
var water_on: bool = false
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
var water_dumped: bool = false
# Several still-water planes, each at its own level over its own bounds: the sea round an
# island and a lake a hundred metres above it cannot be one surface. Each draws the same way;
# only one - the first added with `reflect` - gets the planar reflection pass, because every
# mirrored plane is another full scene pass. water_level / water_cx ... mirror that one, so
# code written against a single plane still reads the surface that reflects.
const WATER_MAX: int = 8
var wb_n: int = 0
var wb_level: words = null
var wb_cx: words = null
var wb_cz: words = null
var wb_ex: words = null
var wb_ez: words = null
var wb_reflect: words = null
var wb_primary: int = -1 # the body the reflection pass mirrors, or -1
# Render the scene through a camera mirrored in the water plane into water_refl,
# clipping everything below the surface; terrain, scattered layers and sky.
function water_reflection_pass() -> void {
if wb_primary < 0 { return } # no body reflects: nothing to mirror
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) }
# 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
m4_copy(water_saved, cam_view)
m4_copy(mem_off(water_saved, 64), cam_vp)
m4_copy(mem_off(water_saved, 128), cam_inv_vp)
let sx = cam_pos[0]; let sy = cam_pos[1]; let sz = cam_pos[2]
# the mirrored camera: view' = view * R, R reflecting y about the surface (y' = 2L - y).
# R has determinant -1, so the winding flips (front faces culled below) and the image
# lands exactly where the main camera's pixels expect the reflection.
let eye = v3_new(sx, f_sub(f_mul(F_TWO, water_level), sy), sz)
let refl = m4_new()
refl[5] = f_neg1()
refl[13] = f_mul(F_TWO, water_level)
let mv = words(16)
m4_mul(mv, water_saved, refl)
m4_copy(cam_view, mv)
free(mv); free(refl)
let fwd = words(3); let up = words(3); let at = words(3)
m4_mul(cam_vp, cam_proj, cam_view)
m4_inverse(cam_inv_vp, cam_vp)
v3_copy(cam_pos, eye)
r3d_clip_y = f_sub(water_level, fl(0.05))
target_bind(water_refl)
gpu_depth_test(true)
gpu_depth_func(GL_LESS)
gpu_depth_write(true)
gpu_cull(true)
gpu_cull_face(GL_FRONT) # the mirror flips the winding
gpu_clear_color(0.0, 0.0, 0.0, 1.0)
gpu_clear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
sc_freeze = true
let sb = sc_skip_blade
sc_skip_blade = true # blades are invisible at this scale in a reflection
ter_reflect = true
prof_cpu_mark("reflection setup")
terrain_draw()
prof_cpu_mark("reflection terrain")
scene_draw()
prof_cpu_mark("reflection scene")
r3d_draw_sky()
prof_cpu_mark("reflection sky")
ter_reflect = false
sc_skip_blade = sb
sc_freeze = false
gpu_cull_face(GL_BACK)
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)
m4_copy(cam_vp, mem_off(water_saved, 64))
m4_copy(cam_inv_vp, mem_off(water_saved, 128))
v3_set(cam_pos, sx, sy, sz)
free(eye); free(fwd); free(up); free(at)
gpu_fb_bind(0)
}
# Is any of the mirrored water in view? The reflection pass is a second copy of the terrain, the
# vegetation and the actors - 170-260 draws - and it ran on every frame of a map with a lake, looking
# straight down at a meadow included, where no pixel samples it. The mirrored body's bounds are cut
# into rectangles (water_cells_build) that count where the ground lies below the water, and the pass
# runs when one of those rectangles meets the view frustum and its water is not all dry where it shows,
# outside the frame or behind the ground (wb_cell_occluded). Past 16 rectangles in view it runs without
# asking, so it errs on the side of running. R3D_REFL_ALWAYS=1 runs it on every frame, for comparing.
# Mac, the five shot viewpoints: looking down at the meadow (c) 225 reflection draws -> none; every view
# with the lake in it unchanged, byte-identical frames.
var wb_cells: words = null # x0, z0, x1, z1 of each rectangle with water showing
var wb_ncells: int = -1 # -1: not built for the current mirrored body
var wb_blocks: words = null # x0, z0, x1, z1, first cell, cells past the last - of each block
var wb_nblocks: int = 0
var wb_refl_always: int = -1
var wb_dbg: int = -1
var wb_build_us: long = 0 # how long the last water_cells_build took (R3D_REFL_DBG prints it)
function water_reflect_visible() -> bool {
if wb_primary < 0 { return false }
if wb_refl_always < 0 { wb_refl_always = 0; if Os.has_env("R3D_REFL_ALWAYS") { wb_refl_always = 1 } }
if wb_refl_always == 1 or ter_heights == null { return true }
if wb_ncells < 0 { let t0 = gl_now_us(); water_cells_build(); wb_build_us = gl_now_us() - t0 }
if wb_dbg < 0 { wb_dbg = 0; if Os.has_env("R3D_REFL_DBG") { wb_dbg = 1 } }
if wb_dbg == 1 {
# R3D_REFL_DBG: once, what the test is made of and how much of it is in view
wb_dbg = 2
var seen = 0
var hidden = 0
for i in 0 .. wb_ncells { if wb_cell_visible(i) { seen += 1; if wb_cell_occluded(i) { hidden += 1 } } }
print(`water reflection test: {wb_ncells} wet rectangles in {wb_nblocks} blocks (built in {wb_build_us} us), {seen} in view, {hidden} of them behind the ground`)
}
# A rectangle in the frustum can still be behind the ground or dry where it shows: standing on a shore
# above the lake and looking down at your feet puts water inside the view and none of it on screen.
# The first 16 in view are checked against the height field; past that the pass simply runs.
var checked = 0
for b in 0 .. wb_nblocks {
let o = b * 6
if not wb_rect_visible(wb_blocks[o], wb_blocks[o + 1], wb_blocks[o + 2], wb_blocks[o + 3]) { continue }
for i in wb_blocks[o + 4] .. wb_blocks[o + 5] {
if not wb_cell_visible(i) { continue }
if checked >= 16 { return true }
checked += 1
if not wb_cell_occluded(i) { return true }
}
}
return false
}
# A flat rectangle at the water's level against the view's side planes: hidden when all four corners
# lie outside one plane. (A bounding sphere was the first version: a 156 m cell's sphere reached into
# the view from a lake the camera had its back to, and the pass never skipped.)
function wb_rect_visible(x0: int, z0: int, x1: int, z1: int) -> bool {
if cam_planes == null { return true }
for p in 0 .. 4 {
let q = p * 4
let a = cam_planes[q]; let c = cam_planes[q + 2]
let by = f_add(f_mul(cam_planes[q + 1], water_level), cam_planes[q + 3])
let ax0 = f_mul(a, x0); let ax1 = f_mul(a, x1); let cz0 = f_mul(c, z0); let cz1 = f_mul(c, z1)
if f_ls(f_add(f_add(ax0, cz0), by), F_ZERO) and f_ls(f_add(f_add(ax1, cz0), by), F_ZERO) and f_ls(f_add(f_add(ax0, cz1), by), F_ZERO) and f_ls(f_add(f_add(ax1, cz1), by), F_ZERO) { return false }
}
return true
}
function wb_cell_visible(i: int) -> bool {
let o = i * 4
return wb_rect_visible(wb_cells[o], wb_cells[o + 1], wb_cells[o + 2], wb_cells[o + 3])
}
# Is the water at (x, z) out of sight - dry ground there, outside the frame, or behind the ground? The
# sight line is sampled at seven points short of both ends, with half a metre (and a little more with
# distance) of margin, so a far ridge the drawn terrain rounds off never hides real water. A camera at
# or under the surface hides nothing behind the ground.
function wb_point_hidden(x: int, z: int) -> bool {
if f_gt(terrain_height(x, z), f_add(water_level, fl(0.05))) { return true }
if not cam_sphere_visible(x, water_level, z, F_HALF) { return true }
let ey = cam_pos[1]
if not f_gt(ey, water_level) { return false }
let dx = f_sub(x, cam_pos[0]); let dz = f_sub(z, cam_pos[2])
let margin = f_add(F_HALF, f_mul(f_sqrt(f_add(f_mul(dx, dx), f_mul(dz, dz))), fl(0.004)))
for k in 1 .. 8 {
let t = fr(k, 8)
let sy = f_add(ey, f_mul(f_sub(water_level, ey), t))
if f_gt(terrain_height(f_add(cam_pos[0], f_mul(dx, t)), f_add(cam_pos[2], f_mul(dz, t))), f_add(sy, margin)) { return true }
}
return false
}
# every point the wet test sampled (the centre and four near the corners) is out of sight
function wb_cell_occluded(i: int) -> bool {
let o = i * 4
let x0 = wb_cells[o]; let z0 = wb_cells[o + 1]; let x1 = wb_cells[o + 2]; let z1 = wb_cells[o + 3]
let cx = f_mul(f_add(x0, x1), F_HALF); let cz = f_mul(f_add(z0, z1), F_HALF)
let ox = f_mul(f_sub(x1, x0), fl(0.45)); let oz = f_mul(f_sub(z1, z0), fl(0.45))
if not wb_point_hidden(cx, cz) { return false }
if not wb_point_hidden(f_sub(cx, ox), f_sub(cz, oz)) { return false }
if not wb_point_hidden(f_add(cx, ox), f_sub(cz, oz)) { return false }
if not wb_point_hidden(f_sub(cx, ox), f_add(cz, oz)) { return false }
return wb_point_hidden(f_add(cx, ox), f_add(cz, oz))
}
function wb_cell_add(x0: int, z0: int, x1: int, z1: int) -> void {
let o = wb_ncells * 4
wb_cells[o] = x0; wb_cells[o + 1] = z0; wb_cells[o + 2] = x1; wb_cells[o + 3] = z1
wb_ncells += 1
}
function wb_block_add(x0: int, z0: int, x1: int, z1: int, first: int) -> void {
if wb_ncells <= first { return }
let o = wb_nblocks * 6
wb_blocks[o] = x0; wb_blocks[o + 1] = z0; wb_blocks[o + 2] = x1; wb_blocks[o + 3] = z1
wb_blocks[o + 4] = first; wb_blocks[o + 5] = wb_ncells
wb_nblocks += 1
}
function wb_wet(x: int, z: int, off: int) -> bool {
if f_ls(terrain_height(x, z), water_level) { return true }
if off == 0 { return false }
if f_ls(terrain_height(f_sub(x, off), f_sub(z, off)), water_level) { return true }
if f_ls(terrain_height(f_add(x, off), f_sub(z, off)), water_level) { return true }
if f_ls(terrain_height(f_sub(x, off), f_add(z, off)), water_level) { return true }
return f_ls(terrain_height(f_add(x, off), f_add(z, off)), water_level)
}
# Over the height map, 32 m rectangles (at most 512 a side) tested at five points and kept in blocks of
# 8 x 8, so a block out of view skips its rectangles in one test - looking away from the water was a
# walk of every rectangle, every frame. The body beyond the map (a sea runs far past it) is 64 x 64
# coarse rectangles tested at their centre, in one last block, which only ever matter kilometres away.
const WB_BLOCK: int = 8
function water_cells_build() -> void {
let bx0 = f_sub(water_cx, water_ex); let bx1 = f_add(water_cx, water_ex)
let bz0 = f_sub(water_cz, water_ez); let bz1 = f_add(water_cz, water_ez)
let th = fi(TERRAIN_HALF)
let tx0 = f_max(bx0, f_sub(ter_ox, th)); let tx1 = f_min(bx1, f_add(ter_ox, th))
let tz0 = f_max(bz0, f_sub(ter_oz, th)); let tz1 = f_min(bz1, f_add(ter_oz, th))
let inside = f_gt(tx1, tx0) and f_gt(tz1, tz0)
var cell = fi(32)
var nx = 0
var nz = 0
if inside {
let span = f_max(f_sub(tx1, tx0), f_sub(tz1, tz0))
if f_gt(f_div(span, cell), fi(512)) { cell = f_div(span, fi(512)) }
nx = f_to_int(f_div(f_sub(tx1, tx0), cell)) + 1
nz = f_to_int(f_div(f_sub(tz1, tz0), cell)) + 1
}
if wb_cells != null { free(wb_cells) }
if wb_blocks != null { free(wb_blocks) }
wb_cells = words((nx * nz + 64 * 64) * 4)
let nbx = (nx + WB_BLOCK - 1) / WB_BLOCK
let nbz = (nz + WB_BLOCK - 1) / WB_BLOCK
wb_blocks = words((nbx * nbz + 1) * 6)
wb_ncells = 0
wb_nblocks = 0
let off = f_mul(cell, fl(0.45))
let half = f_mul(cell, F_HALF)
for bz in 0 .. nbz {
for bx in 0 .. nbx {
let first = wb_ncells
var iz = bz * WB_BLOCK
while iz < (bz + 1) * WB_BLOCK and iz < nz {
let z0 = f_add(tz0, f_mul(fi(iz), cell))
var ix = bx * WB_BLOCK
while ix < (bx + 1) * WB_BLOCK and ix < nx {
let x0 = f_add(tx0, f_mul(fi(ix), cell))
if wb_wet(f_add(x0, half), f_add(z0, half), off) { wb_cell_add(x0, z0, f_add(x0, cell), f_add(z0, cell)) }
ix += 1
}
iz += 1
}
let x0 = f_add(tx0, f_mul(fi(bx * WB_BLOCK), cell)); let z0 = f_add(tz0, f_mul(fi(bz * WB_BLOCK), cell))
wb_block_add(x0, z0, f_add(x0, f_mul(fi(WB_BLOCK), cell)), f_add(z0, f_mul(fi(WB_BLOCK), cell)), first)
}
}
let first = wb_ncells
let cw = f_div(f_sub(bx1, bx0), fi(64))
let ch = f_div(f_sub(bz1, bz0), fi(64))
for iz in 0 .. 64 {
let z0 = f_add(bz0, f_mul(fi(iz), ch)); let z1 = f_add(z0, ch)
for ix in 0 .. 64 {
let x0 = f_add(bx0, f_mul(fi(ix), cw)); let x1 = f_add(x0, cw)
# inside the height map's rectangle: the fine cells above cover it
if inside and not f_ls(x0, tx0) and not f_gt(x1, tx1) and not f_ls(z0, tz0) and not f_gt(z1, tz1) { continue }
if wb_wet(f_add(x0, f_mul(cw, F_HALF)), f_add(z0, f_mul(ch, F_HALF)), 0) { wb_cell_add(x0, z0, x1, z1) }
}
}
wb_block_add(bx0, bz0, bx1, bz1, first)
}
# the program and the plane are the process's, built once; the bodies are the map's
function water_setup() -> void {
if water_prog != 0 { return }
water_mesh = mesh_grid(2, F_HALF)
water_prog = r3d_program("water.vert", "water.frag", "")
wb_level = words(WATER_MAX); wb_cx = words(WATER_MAX); wb_cz = words(WATER_MAX)
wb_ex = words(WATER_MAX); wb_ez = words(WATER_MAX); wb_reflect = words(WATER_MAX)
}
function water_bodies_clear() -> void {
wb_n = 0
wb_primary = -1
wb_ncells = -1
water_on = false
}
# Add a plane at `level` over (cx, cz) +- (ex, ez); true `reflect` makes it the mirrored one
# if none is yet. Returns its index, or -1 once WATER_MAX are in use.
function water_body_add(level: int, cx: int, cz: int, ex: int, ez: int, reflect: bool) -> int {
water_setup()
if wb_n >= WATER_MAX { return -1 }
let i = wb_n
wb_level[i] = level; wb_cx[i] = cx; wb_cz[i] = cz; wb_ex[i] = ex; wb_ez[i] = ez
wb_reflect[i] = 0
if reflect { wb_reflect[i] = 1 }
wb_n += 1
water_on = true
if reflect and wb_primary < 0 {
wb_primary = i
wb_ncells = -1
water_level = level; water_cx = cx; water_cz = cz; water_ex = ex; water_ez = ez
}
return i
}
# one reflecting plane: what this function always meant, without a new mesh and program
# every time it is called
function water_init(level: int, cx: int, cz: int, ex: int, ez: int) -> void {
water_bodies_clear()
water_body_add(level, cx, cz, ex, ez, true)
}
# call after the opaque pass, before the sky: blends over the resolved depth
function water_draw(depth_tex: int) -> void {
if not water_on or wb_n == 0 { return }
let p = water_prog
gpu_use_program(p)
u_mat4(gpu_uniform(p, "u_view"), cam_view)
u_mat4(gpu_uniform(p, "u_proj"), cam_proj)
u_mat4(gpu_uniform(p, "u_inv_vp"), cam_inv_vp)
# 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(gpu_uniform(p, "u_screen"), fi(post_w), fi(post_h))
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,
# and it must not displace the depth texture the shader reads for the shore
r3d_bind_2d(p, "u_refl", 1, water_refl.color); ron = F_ONE
gpu_tex_param(GPU_TEX2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR)
gpu_tex_mips(GPU_TEX2D)
}
r3d_bind_2d(p, "u_depth", 0, depth_tex)
r3d_bind_2d(p, "u_scene", 2, post_scene.color)
sky_bind_lighting(p)
shadow_bind(p)
fog_bind(p)
# Opaque. The surface composites the refracted bed itself, so there is nothing for
# hardware blending to do — and an alpha was what left see-through gaps in the foam
# and a clear band at the shore wide enough to give the plane away.
gpu_blend(false)
gpu_blend_func(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
# the surface writes depth: the ambient-occlusion and temporal passes read the frame's depth,
# and the bed 9 m below the shore would otherwise darken a band along the water line
gpu_depth_write(true)
gpu_cull(false)
# every body is the same plane at its own level and bounds; only the mirrored one samples
# the reflection - another body reading it would show the wrong world upside down
for i in 0 .. wb_n {
u_f(gpu_uniform(p, "u_level"), wb_level[i])
u_f2(gpu_uniform(p, "u_center"), wb_cx[i], wb_cz[i])
u_f2(gpu_uniform(p, "u_extent"), wb_ex[i], wb_ez[i])
var r = F_ZERO
if i == wb_primary { r = ron }
u_f(gpu_uniform(p, "u_refl_on"), r)
# Every body is clipped to the ellipse inside its bounds but an unbounded mirrored sea,
# whose rectangle is the point. A mirrored LAKE is clipped like any other: a map whose
# reflection belongs to its lake (the Bells in Maroon Lake) would otherwise draw that
# lake's level over every hollow in the survey.
var clip = F_ONE
if i == wb_primary and f_gt(f_max(wb_ex[i], wb_ez[i]), fi(10000)) { clip = F_ZERO }
u_f(gpu_uniform(p, "u_clip_ellipse"), clip)
mesh_draw(water_mesh)
}
gpu_blend(false)
}