terrain_reload/terrain_unload release one map's height field, survey, photograph and patch bounds and generate another at any TERRAIN_HALF; scatter_clear_all (with streams), actor_clear_all, col_reset and mesh_free empty the scene; water_body_add/water_bodies_clear draw several still-water planes with one reflecting; terrain_sea separates the coast's sea level from the carved lake's, and grass keeps off both. Fixes CDLOD patch bounds for TERRAIN_HALF != 4096 and water_init leaking a mesh and program per call. examples/rendering/reload.ludic. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
183 lines
8.1 KiB
Text
183 lines
8.1 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)
|
|
gl_enable(GL_DEPTH_TEST)
|
|
gl_depth_func(GL_LESS)
|
|
gl_depth_mask(1)
|
|
gl_enable(GL_CULL_FACE)
|
|
gl_cull_face(GL_FRONT) # the mirror flips the winding
|
|
gl_clear_color(0.0, 0.0, 0.0, 1.0)
|
|
gl_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
|
|
terrain_draw()
|
|
scene_draw()
|
|
r3d_draw_sky()
|
|
ter_reflect = false
|
|
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, 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)
|
|
gl_bind_framebuffer(GL_FRAMEBUFFER, 0)
|
|
}
|
|
|
|
# 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
|
|
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
|
|
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
|
|
gl_use_program(p)
|
|
u_mat4(gl_uniform(p, "u_view"), cam_view)
|
|
u_mat4(gl_uniform(p, "u_proj"), cam_proj)
|
|
u_mat4(gl_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(gl_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
|
|
gl_tex_parameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR)
|
|
gl_generate_mipmap(GL_TEXTURE_2D)
|
|
}
|
|
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.
|
|
gl_disable(GL_BLEND)
|
|
gl_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
|
|
gl_depth_mask(1)
|
|
gl_disable(GL_CULL_FACE)
|
|
# 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(gl_uniform(p, "u_level"), wb_level[i])
|
|
u_f2(gl_uniform(p, "u_center"), wb_cx[i], wb_cz[i])
|
|
u_f2(gl_uniform(p, "u_extent"), wb_ex[i], wb_ez[i])
|
|
var r = F_ZERO
|
|
if i == wb_primary { r = ron }
|
|
u_f(gl_uniform(p, "u_refl_on"), r)
|
|
mesh_draw(water_mesh)
|
|
}
|
|
gl_disable(GL_BLEND)
|
|
}
|