feat(render3d): replace the world at run time, water bodies, terrain_sea

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>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-13 00:03:40 +03:00
parent 70bda0c4df
commit a4a3d75cd9
13 changed files with 308 additions and 19 deletions

View file

@ -0,0 +1,10 @@
bump: minor
type: feat
**ludic.render3d: replace the world at run time** — a game can swap its terrain, scatter, colliders, actors and water for another map's without restarting.
- `terrain_reload(dem, emin, emax, base, ox, oz, ortho, half)` releases the current map's height field, survey, photograph and patch bounds and generates another at any `TERRAIN_HALF`; `terrain_unload()` is the release on its own.
- `scatter_clear_all()` (with `stream_clear_all()`), `actor_clear_all()` and `col_reset()` empty the scene for the next map; `mesh_free()` releases a mesh's GL objects.
- **Water bodies** — `water_body_add(level, cx, cz, ex, ez, reflect)` and `water_bodies_clear()` draw several still-water planes at their own levels; the first that reflects gets the planar reflection. `water_init` still means one reflecting plane.
- **Fix:** the terrain's patch quadtree placed patches at a fixed 32 m leaf, so any `TERRAIN_HALF` other than 4096 put patch bounds in the wrong place; leaves now scale with the map.
- **Fix:** `water_init` built a new mesh and compiled a new program on every call.
- `terrain_sea(level)` separates the sea's level (the coast, the strand, the shoreline, forest and scree shading) from the carved lake's, so a lake can sit above the sea. Unset, the sea is the lake's level as before.

View file

@ -0,0 +1,82 @@
# reload.ludic — the world replaced at run time.
#
# Generate a 2 km map, scatter onto it, give it colliders and water; release all of it and
# generate a 3 km map in its place with two water planes; then go back. Prints RELOAD OK when
# nothing of the previous map survived and the new one is whole.
#
# bin/ludic build examples/rendering/reload.ludic --headless && ./build/reload_headless
program Reload {
import "ludic.render3d/r3d.ludic"
property Marker { on: int = 1 }
model Anchor { Marker }
# A program only links the GL runtime render3d stands on when it names Gl.* itself
# (the parser's g_uses_gl), so even a check that draws nothing presents its frame.
# render3d calls back into the program for what stands on the terrain
function scene_draw() -> void { scatter_draw() }
function scene_draw_casters() -> void { scatter_draw_casters(shadow_cascade_vp(sh_cascade)) }
# no streamed cover here, but stream.ludic names the generator, so every program supplies one
function stream_fill(s: Stream, cx: int, cz: int, band: int) -> void { }
handler Present phase Render {
Gl.swap()
}
function check(what: string, cond: bool) -> bool {
if not cond { print(`reload: FAILED - {what}`) }
return cond
}
function populate() -> void {
let l = layer_cards(model_cross_card(), 1000, F_ZERO, fi(400))
for i in 0 .. 400 {
let x = fi(i * 2 - 400)
layer_add(l, x, terrain_height(x, fi(30)), fi(30), F_ONE, F_ZERO, F_ZERO, F_ZERO)
col_add(x, fi(30), F_ONE)
}
col_build()
}
handler Boot phase Start {
spawn Anchor {}
TERRAIN_HALF = 1000
ter_smooth = true
if not r3d_init(640, 360, "Reload") { quit() }
cam_set(F_ZERO, fi(80), fi(300), fi(180), f_neg(fi(10)))
water_init(fl(4.0), F_ZERO, F_ZERO, fi(200), fi(200))
populate()
var ok = true
ok = check("the first map has layers and colliders", len(sc_layers) == 1 and col_n == 400) and ok
# --- swap to a 3 km map with a sea and a lake above it
scatter_clear_all()
actor_clear_all()
col_reset()
water_bodies_clear()
terrain_lake(F_ZERO, F_ZERO, F_ZERO, F_ZERO, F_ZERO)
terrain_reload("", F_ZERO, F_ZERO, F_ZERO, F_ZERO, F_ZERO, "", 1500)
ok = check("everything of the first map released", len(sc_layers) == 0 and col_n == 0 and wb_n == 0 and not water_on) and ok
ok = check("the new half", TERRAIN_HALF == 1500 and ter_heights != null and ter_height_tex != 0) and ok
ok = check("patch bounds rebuilt", cd_min != null and len(cd_min) == CD_LEVELS) and ok
ok = check("patches scale with the map", cd_size(0) == f_mul(fi(32), fr(3000, 8192))) and ok
terrain_height(F_ZERO, F_ZERO)
ok = check("the cached height scale is the new map's", ter_h_scale == fr(TERRAIN_RES, 3000)) and ok
let sea = water_body_add(F_ZERO, F_ZERO, F_ZERO, fi(20000), fi(20000), true)
let lake = water_body_add(fl(12.0), fi(600), fi(-400), fi(150), fi(100), false)
ok = check("two water bodies, the sea mirrored", sea == 0 and lake == 1 and wb_n == 2 and wb_primary == 0 and water_level == F_ZERO and water_on) and ok
populate()
ok = check("the new map takes layers and colliders", len(sc_layers) == 1 and col_n == 400 and col_side == 3000 / COL_CELL) and ok
# a sea below the carved lake: set once, and not undone by a reload
terrain_sea(fl(-2.0))
terrain_lake(fl(12.0), fi(600), fi(-400), fi(150), fi(100))
ok = check("the sea is its own level", ter_sea_set and ter_sea_gen() == fl(-2.0) and ter_lake_level == fl(12.0)) and ok
# --- and back: the swap is repeatable
scatter_clear_all()
col_reset()
water_init(fl(4.0), F_ZERO, F_ZERO, fi(200), fi(200))
terrain_reload("", F_ZERO, F_ZERO, F_ZERO, F_ZERO, F_ZERO, "", 1000)
populate()
ok = check("back to 2 km", TERRAIN_HALF == 1000 and len(cd_min) == CD_LEVELS and wb_n == 1 and col_side == 2000 / COL_CELL) and ok
if ok { print("RELOAD OK") } else { print("RELOAD FAILED") }
quit()
}
}

View file

@ -307,3 +307,10 @@ function actor_draw_casters(light_vp: words) -> void {
actor_draw_one(a, ap, true)
}
}
# every actor off the stage at once, for a world being replaced. Actors own no GL objects;
# their models belong to whoever loaded them.
function actor_clear_all() -> void {
ac_actors = new []Actor
outline_clear()
}

View file

@ -108,3 +108,12 @@ function col_clear(x0: int, z0: int, x1: int, z1: int, r: int) -> bool {
}
return true
}
# No colliders, ready for another map's. The cell index is sized from TERRAIN_HALF on its
# first build and kept, so it is released too: a larger map would overrun the old one.
function col_reset() -> void {
col_n = 0
col_built = false
if col_start != null { free(col_start); col_start = null }
if col_sorted != null { free(col_sorted); col_sorted = null }
}

View file

@ -146,6 +146,10 @@ function grass_draw() -> void {
var lake = fl(-100000.0)
if ter_lake_ex != 0 { lake = ter_lake_level }
u_f(gl_uniform(p, "u_lake_level"), lake)
var sea = lake
if ter_sea_set { sea = ter_sea_level }
u_f(gl_uniform(p, "u_sea_level"), sea)
u_f4(gl_uniform(p, "u_lake"), ter_lake_cx, ter_lake_cz, ter_lake_ex, ter_lake_ez)
u_f(gl_uniform(p, "u_snow_line"), ter_snow_line)
sky_bind_lighting(p)
shadow_bind(p)

View file

@ -148,3 +148,12 @@ function mesh_instance_buffer(m: Mesh, first_attr: int, floats_per: int, data: p
gl_bind_vertex_array(0)
return b
}
# release a mesh's GL objects (a mesh this package built for something being thrown away)
function mesh_free(m: Mesh) -> void {
if m == null { return }
let ids = gl_scratch()
if m.vbo != 0 { ids[0] = m.vbo; gl_delete_buffers(1, ids); m.vbo = 0 }
if m.ebo != 0 { ids[0] = m.ebo; gl_delete_buffers(1, ids); m.ebo = 0 }
if m.vao != 0 { ids[0] = m.vao; gl_delete_vertex_arrays(1, ids); m.vao = 0 }
}

View file

@ -1048,3 +1048,40 @@ function carpet_bake(layers: []Layer, count: int, tile: int, res: int) -> int {
gl_check("carpet bake")
return tex
}
# ---- another map at run time ------------------------------------------------------------
# Every scattered layer released - its instance arrays and GL buffers, a card layer's own
# crossed-card mesh and baked atlas - and every stream feeding them. The models a layer drew
# belong to whoever loaded them and are kept, and so are the programs. A game rebuilding its
# world calls this, then places the new map's layers exactly as it did at boot.
function scatter_clear_all() -> void {
stream_clear_all()
if sc_layers == null { return }
let ids = gl_scratch()
for i in 0 .. len(sc_layers) {
let l = sc_layers[i]
if l.buf != 0 { ids[0] = l.buf; gl_delete_buffers(1, ids) }
if l.imp_buf != 0 { ids[0] = l.imp_buf; gl_delete_buffers(1, ids) }
if l.sh_buf != 0 { ids[0] = l.sh_buf; gl_delete_buffers(1, ids) }
if l.lod_buf != null { for k in 0 .. l.n_lods { ids[0] = l.lod_buf[k]; gl_delete_buffers(1, ids) } }
if l.inst != null { free(l.inst) }
if l.scratch != null { free(l.scratch) }
if l.tint != null { free(l.tint) }
if l.last_cam != null { free(l.last_cam) }
if l.gstart != null { free(l.gstart) }
if l.gsorted != null { free(l.gsorted) }
if l.gymin != null { free(l.gymin) }
if l.gymax != null { free(l.gymax) }
if l.vis != null { free(l.vis) }
if l.lod_dist != null { free(l.lod_dist); free(l.lod_card); free(l.lod_buf); free(l.n_lod) }
if l.lvl != null { free(l.lvl) }
# layer_cards built this layer's crossed card and its atlas itself
if l.card and l.n_lods == 0 and l.model != null { for k in 0 .. len(l.model.prims) { mesh_free(l.model.prims[k].mesh) } }
if l.card and l.atlas != null {
ids[0] = l.atlas.albedo; gl_delete_textures(1, ids)
ids[0] = l.atlas.normal; gl_delete_textures(1, ids)
}
}
sc_layers = new []Layer
sc_view_gen += 1
}

View file

@ -20,6 +20,8 @@ uniform float u_ts_half;
uniform sampler2D u_ortho;
uniform float u_ortho_on;
uniform float u_lake_level;
uniform float u_sea_level; // the sea (terrain_sea); the lake's level when there is no separate sea
uniform vec4 u_lake; // the carved lake: centre x/z, half extents (z = 0: none)
uniform float u_snow_line;
uniform float u_wind;
uniform vec2 u_tile; // world xz of this tile's corner
@ -92,7 +94,11 @@ void main() {
if (croot.w < -1.0 || abs(croot.x) > croot.w * 1.25 + 1.5 || abs(croot.y) > croot.w * 1.4 + 1.5) { cull(); return; }
vec3 gn = normalize(ht.gba);
float h3 = bladeHash(ci, j, 2), h4 = bladeHash(ci, j, 3);
float ok = (1.0 - smoothstep(0.30, 0.55, 1.0 - gn.y)) * smoothstep(0.0, 0.6, ht.r - u_lake_level - 0.15) * smoothstep(u_snow_line - 80.0, u_snow_line - 200.0, ht.r);
// No blades under water: the sea's line everywhere, and the lake's inside its outline. One
// line for both left a lake above the sea with grass on its bed or a valley with none.
float wl = u_sea_level;
if (u_lake.z > 0.0) { vec2 q = (xz - u_lake.xy) / u_lake.zw; if (dot(q, q) < 1.0) wl = max(wl, u_lake_level); }
float ok = (1.0 - smoothstep(0.30, 0.55, 1.0 - gn.y)) * smoothstep(0.0, 0.6, ht.r - wl - 0.15) * smoothstep(u_snow_line - 80.0, u_snow_line - 200.0, ht.r);
if (u_ortho_on > 0.5) {
vec3 oc = textureLod(u_ortho, huv, 1.5).rgb;
ok *= 0.25 + 0.75 * smoothstep(0.0, 0.02, oc.g - max(oc.r, oc.b));

View file

@ -10,6 +10,7 @@ uniform float u_dem_base; // the elevation that becomes y = 0
uniform vec2 u_origin; // world x/z of the map's centre
uniform vec4 u_lake; // a lake: centre x/z, half extents (zero = none)
uniform float u_lake_level; // its surface height; the model records the surface, the bed is carved below it
uniform float u_sea_level; // the sea the coast falls into (terrain_sea); the lake's level unless set
uniform vec4 u_isle; // an island: centre x/z, radius (or rim margin), falloff width
uniform float u_isle_mode; // 0 none, 1 radial (terrain_island), 2 inward from the survey's edge (terrain_coast)
// The DEM is Copernicus GLO-30 — 30 m data resampled onto this 4 m grid — so the stored
@ -84,15 +85,15 @@ void main() {
t = smoothstep(u_isle.z, u_isle.z + u_isle.w, d); // 0 inland, 1 past the fall
shelf = 26.0 + 34.0 * smoothstep(0.0, 1.0, (d - u_isle.z - u_isle.w) / max(u_isle.w, 1.0));
}
float above = h - u_lake_level;
h = u_lake_level + above * (1.0 - t) - t * shelf;
float above = h - u_sea_level;
h = u_sea_level + above * (1.0 - t) - t * shelf;
// A strand. Scaling alone hands a 500 m mountainside a 40-degree plunge into the sea,
// which is a cliff coast and nothing else. Real shores are cut flat by the water they
// meet, so the last few metres of height either side of the line are compressed —
// which stretches them out horizontally into beach and shallows. Only inside the
// coastal band: an inland lake has its own bed and wants none of this.
float near = 1.0 - smoothstep(0.0, 48.0, abs(h - u_lake_level));
h = mix(h, u_lake_level + (h - u_lake_level) * 0.30, near * smoothstep(0.02, 0.30, t));
float near = 1.0 - smoothstep(0.0, 48.0, abs(h - u_sea_level));
h = mix(h, u_sea_level + (h - u_sea_level) * 0.30, near * smoothstep(0.02, 0.30, t));
}
o = vec4(h, 0.0, 0.0, 1.0);
#elif defined(SMOOTH)

View file

@ -18,6 +18,7 @@ uniform sampler2D u_snow_d; uniform sampler2D u_carpet; // the clump cards ba
uniform float u_carpet_on;
uniform float u_snow_line;
uniform float u_lake_level; // the ground just above the water is wet and dark
uniform float u_sea_level; // the sea's line: shore, forest and scree gates (terrain_sea)
uniform vec2 u_origin; // world offset of the terrain grid
// The ground's own sun shadow comes from the baked height-field map (tershadow.frag),
@ -236,14 +237,14 @@ vec3 groundShade(vec3 p, vec3 N, float slope, float dist, float viewDepth, bool
vec3 ocf = textureLod(u_ortho, v_huv, 2.0).rgb;
float gx = ocf.g - max(ocf.r, ocf.b);
// (the photograph is sampled linear: sRGB 72 is 0.06, 92 is 0.11)
forestW = smoothstep(0.12, 0.06, max(ocf.r, max(ocf.g, ocf.b))) * smoothstep(0.004, 0.012, gx) * smoothstep(u_lake_level + 0.8, u_lake_level + 1.8, p.y);
forestW = smoothstep(0.12, 0.06, max(ocf.r, max(ocf.g, ocf.b))) * smoothstep(0.004, 0.012, gx) * smoothstep(u_sea_level + 0.8, u_sea_level + 1.8, p.y);
// Classify from a ~60 m blur, never from the pixels: the survey's 10 m pixels carry
// a foot trail as a broken line of bare ground, and thresholding them painted it
// across the meadow as tan dashes (and, on the CPU, lined boulders up along it).
vec3 ocl = textureLod(u_ortho, v_huv, 2.5).rgb;
float mxc = max(ocl.r, max(ocl.g, ocl.b)), mnc = min(ocl.r, min(ocl.g, ocl.b));
float greenEx = ocl.g - max(ocl.r, ocl.b);
screeC = smoothstep(0.008, -0.002, greenEx) * smoothstep(0.06, 0.12, mxc) * (1.0 - smoothstep(0.55, 0.75, mxc)) * smoothstep(u_lake_level + 0.2, u_lake_level + 1.2, p.y);
screeC = smoothstep(0.008, -0.002, greenEx) * smoothstep(0.06, 0.12, mxc) * (1.0 - smoothstep(0.55, 0.75, mxc)) * smoothstep(u_sea_level + 0.2, u_sea_level + 1.2, p.y);
snowC = smoothstep(0.08, 0.04, mxc - mnc) * smoothstep(0.55, 0.8, mxc);
}
// snow lingering in the high gullies is drawn further down, but whether it can be
@ -362,7 +363,7 @@ vec3 groundShade(vec3 p, vec3 N, float slope, float dist, float viewDepth, bool
// the bank here: identical noise fields, identical time, identical phase, keyed off
// height above the lake instead of depth below it. Across the seam the two agree, so
// there is nothing there to read as an edge.
float above = p.y - u_lake_level; // >0 on land, metres
float above = p.y - u_sea_level; // >0 on land, metres
// wet ground: darker and glossier near the water, fading out over ~1.2 m
float wet = smoothstep(1.2, 0.0, above);
alb *= mix(1.0, 0.5, wet * 0.85);

View file

@ -313,3 +313,16 @@ function stream_update_all() -> void {
stream_deadline = gl_now_us() + STREAM_BUDGET_US
for i in 0 .. len(stream_all) { stream_update(stream_all[i], cam_pos[0], cam_pos[2]) }
}
# every stream and its cached chunks, for a world being replaced (scatter_clear_all)
function stream_clear_all() -> void {
if stream_all == null { return }
for i in 0 .. len(stream_all) {
let s = stream_all[i]
if s.chunks != null { for c in 0 .. len(s.chunks) { if s.chunks[c].data != null { free(s.chunks[c].data) } } }
if s.keys != null { free(s.keys) }
if s.htab != null { free(s.htab) }
if s.bands != null { free(s.bands) }
}
stream_all = null
}

View file

@ -68,6 +68,15 @@ var ter_lake_ez: int = 0
function terrain_lake(level: int, cx: int, cz: int, ex: int, ez: int) -> void {
ter_lake_level = level; ter_lake_cx = cx; ter_lake_cz = cz; ter_lake_ex = ex; ter_lake_ez = ez
}
# The SEA's level, for the coast, the strand and the shoreline shading, when it is not the
# carved lake's. They were one number, which holds only while the lake is at sea level: a
# lake eighty metres up the valley drowned the whole coast toward its own surface. Unset,
# the sea is the lake exactly as before, so a map with one water line needs no call.
var ter_sea_level: int = 0
var ter_sea_set: bool = false
function terrain_sea(level: int) -> void { ter_sea_level = level; ter_sea_set = true }
# the level the generator scales the coast toward (it always read the lake's, set or not)
function ter_sea_gen() -> int { if ter_sea_set { return ter_sea_level }; return ter_lake_level }
# An island: land out to `r` from (cx, cz), then the terrain scaled down into the water
# over `fall` metres and on down to a shelf. Scaling rather than blending to a fixed bed is
# what makes the coastline come out of the terrain that is already there — low ground turns
@ -229,6 +238,7 @@ function terrain_generate() -> void {
u_f2(gl_uniform(p, "u_origin"), ter_ox, ter_oz)
u_f4(gl_uniform(p, "u_lake"), ter_lake_cx, ter_lake_cz, ter_lake_ex, ter_lake_ez)
u_f(gl_uniform(p, "u_lake_level"), ter_lake_level)
u_f(gl_uniform(p, "u_sea_level"), ter_sea_gen())
u_f4(gl_uniform(p, "u_isle"), ter_isle_cx, ter_isle_cz, ter_isle_r, ter_isle_fall)
u_f(gl_uniform(p, "u_isle_mode"), fi(ter_isle_mode))
u_f(gl_uniform(p, "u_dem_blur"), ter_dem_blur)
@ -453,6 +463,10 @@ function terrain_bind_prog(p: int) -> void {
var lake = fl(-100000.0)
if ter_lake_ex != 0 { lake = ter_lake_level }
u_f(gl_uniform(p, "u_lake_level"), lake)
# the shoreline, forest and scree gates read the sea; unset it is what they always read
var sea = lake
if ter_sea_set { sea = ter_sea_level }
u_f(gl_uniform(p, "u_sea_level"), sea)
u_mat4(gl_uniform(p, "u_view"), cam_view)
u_mat4(gl_uniform(p, "u_proj"), cam_proj)
u_f2(gl_uniform(p, "u_origin"), ter_ox, ter_oz)
@ -660,6 +674,12 @@ function cdlod_bounds() -> void {
}
}
# A patch's side in metres at a level. The quadtree is CD_LEAVES leaves a side over the whole
# map, so a leaf is (2 * TERRAIN_HALF) / CD_LEAVES - 32 m only on the 8192 m map the numbers
# were chosen for. Placing patches at a fixed 32 m put every bound in the wrong place on any
# other TERRAIN_HALF.
function cd_size(level: int) -> int { return f_mul(fi(32 << level), fr(TERRAIN_HALF * 2, 8192)) }
# does the patch's box come within r of the camera?
function cd_within(x0: int, z0: int, size: int, ymin: int, ymax: int, r: int) -> bool {
let dx = f_max(f_max(f_sub(x0, cam_pos[0]), f_sub(cam_pos[0], f_add(x0, size))), F_ZERO)
@ -679,7 +699,7 @@ function cd_inside(x0: int, z0: int, size: int, ymin: int, ymax: int, r: int) ->
}
function cdlod_draw(level: int, ix: int, iz: int) -> void {
let size = fi(32 << level)
let size = cd_size(level)
let x0 = f_add(f_sub(ter_ox, fi(TERRAIN_HALF)), f_mul(fi(ix), size))
let z0 = f_add(f_sub(ter_oz, fi(TERRAIN_HALF)), f_mul(fi(iz), size))
# Which tier can run inside this patch. A patch that never comes within the split takes
@ -723,7 +743,7 @@ function cdlod_draw(level: int, ix: int, iz: int) -> void {
# its children, in which case each child either selects itself or is drawn at this level.
function cdlod_select(level: int, ix: int, iz: int) -> bool {
let n = CD_LEAVES >> level
let size = fi(32 << level)
let size = cd_size(level)
let x0 = f_add(f_sub(ter_ox, fi(TERRAIN_HALF)), f_mul(fi(ix), size))
let z0 = f_add(f_sub(ter_oz, fi(TERRAIN_HALF)), f_mul(fi(iz), size))
let ymin = cd_min[level][iz * n + ix]
@ -741,3 +761,42 @@ function cdlod_select(level: int, ix: int, iz: int) -> bool {
}
return true
}
# ---- another map at run time ------------------------------------------------------------
# What belongs to ONE map - the generated height field and its CPU copy, the survey, the
# photograph, the patch bounds and the scales cached from TERRAIN_HALF - is released here,
# so a different survey can be generated in its place without restarting the process. What
# belongs to the process stays: the patch mesh, the material textures, the programs, the
# frame-sized targets and the shadow texture, whose size does not depend on the map.
function terrain_unload() -> void {
let ids = gl_scratch()
if ter_height_tex != 0 { ids[0] = ter_height_tex; gl_delete_textures(1, ids); ter_height_tex = 0 }
if ter_dem_tex != 0 { ids[0] = ter_dem_tex; gl_delete_textures(1, ids); ter_dem_tex = 0 }
if ter_ortho_tex != 0 { ids[0] = ter_ortho_tex; gl_delete_textures(1, ids); ter_ortho_tex = 0 }
if ter_heights != null { free(ter_heights); ter_heights = null }
if ter_ortho_px != null { free(ter_ortho_px); ter_ortho_px = null }
ter_ortho_w = 0
if cd_min != null {
for i in 0 .. len(cd_min) { free(cd_min[i]); free(cd_max[i]) }
cd_min = null; cd_max = null
}
# derived from TERRAIN_HALF and cached on first use: stale ones would keep the old size
ter_h_scale = 0; ter_o_scale = 0
ter_carpet = 0
ter_shadow_gen = -1; ter_shadow_yaw = 0x7fffffff
}
# Generate another map in place. Call terrain_lake / terrain_coast / terrain_island for it
# first - the generator reads them - then this. `half` is the new TERRAIN_HALF in metres.
# An empty `dem` generates the analytic ground (with ter_smooth) and an empty `ortho` drapes
# no photograph. The shadow is rebaked and the patch bounds rebuilt before it returns.
function terrain_reload(dem: string, emin: int, emax: int, base: int, ox: int, oz: int, ortho: string, half: int) -> void {
terrain_unload()
TERRAIN_HALF = half
ter_ox = ox; ter_oz = oz
if len(dem) > 0 { terrain_use_dem(dem, emin, emax, base, ox, oz) }
if len(ortho) > 0 { terrain_use_ortho(ortho) }
terrain_generate()
terrain_bake_shadow()
cdlod_bounds()
gl_check("terrain reload")
}

View file

@ -17,10 +17,25 @@ var water_refl: Target = null # the world mirrored in the surface, ha
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) }
@ -81,31 +96,58 @@ function water_reflection_pass() -> void {
gl_bind_framebuffer(GL_FRAMEBUFFER, 0)
}
function water_init(level: int, cx: int, cz: int, ex: int, ez: int) -> void {
# 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", "")
water_level = level; water_cx = cx; water_cz = cz; water_ex = ex; water_ez = ez
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 { return }
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)
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)
# 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 {
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
@ -114,7 +156,6 @@ function water_draw(depth_tex: int) -> void {
}
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)
@ -127,6 +168,16 @@ function water_draw(depth_tex: int) -> void {
# 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)
# 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)
}