ludic/packages/ludic.render3d/water.ludic
Orkuncakilkaya f25289db20
Some checks failed
ci / build-and-test (push) Waiting to run
commit-lint / conventional-commits (push) Waiting to run
bootstrap / cfree-fixpoint (push) Has been cancelled
docs / build-and-deploy (push) Successful in 34s
feat(gl): OpenGL 4.1 and the ludic.render3d renderer
`Gl.*` binds the whole OpenGL 4.1 core API — every entry point of the
platform gl3.h with every GL_* constant, generated by `ludic-dev glgen`
with per-call ABI thunks. Windowed builds get an NSOpenGLContext on the
existing window at Retina resolution; headless builds render into an
offscreen CGL context, so a program that uses Gl.* renders and
screenshots identically under the test harness. It links gl.ll, the
thunks and OpenGL.framework only when used; every other build stays
byte-identical.

packages/ludic.render3d is a physically based renderer written on that
surface: HDRI image-based lighting, GPU-generated terrain with scanned
PBR materials, CDLOD, cascaded shadows, glTF with skinning, instanced
vegetation with impostors, procedural grass, water, SSAO, and an HDR
pipeline with bloom, auto-exposure and ACES.

It also carries this session's work on it: the terrain at half its cost
(10.3 -> 5.4 ms of frame), the streaming hitch that got worse the longer
you played, a resize that emptied the world, and the packaging that lets
a game use the renderer from its own repository — `ludic assets`, the
material manifest shipping with the package, and shader lookup falling
back to the install root. See changes/ for each, with its numbers.

The camping game that drove all of it has moved out to its own
repository, Maroon Lake; examples/rendering/smooth.ludic stays as the
renderer's example here.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-10 03:31:12 +03:00

125 lines
5.3 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
# 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 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)
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, gl_w / water_refl_div, gl_h / water_refl_div, "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)
}
function water_init(level: int, cx: int, cz: int, ex: int, ez: int) -> void {
water_mesh = mesh_grid(2, F_HALF)
water_prog = r3d_program("water.vert", "water.frag", "")
water_level = level; water_cx = cx; water_cz = cz; water_ex = ex; water_ez = ez
water_on = 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 { 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)
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))
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,
# 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)
u_f(gl_uniform(p, "u_refl_on"), ron)
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)
mesh_draw(water_mesh)
gl_disable(GL_BLEND)
}