diff --git a/changes/render3d-reload.md b/changes/render3d-reload.md new file mode 100644 index 00000000..faf58ed2 --- /dev/null +++ b/changes/render3d-reload.md @@ -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. diff --git a/examples/rendering/reload.ludic b/examples/rendering/reload.ludic new file mode 100644 index 00000000..16130127 --- /dev/null +++ b/examples/rendering/reload.ludic @@ -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() + } +} diff --git a/packages/ludic.render3d/actor.ludic b/packages/ludic.render3d/actor.ludic index 55300c94..fe966945 100644 --- a/packages/ludic.render3d/actor.ludic +++ b/packages/ludic.render3d/actor.ludic @@ -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() +} diff --git a/packages/ludic.render3d/collide.ludic b/packages/ludic.render3d/collide.ludic index bb88ca17..d12da01b 100644 --- a/packages/ludic.render3d/collide.ludic +++ b/packages/ludic.render3d/collide.ludic @@ -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 } +} diff --git a/packages/ludic.render3d/grass.ludic b/packages/ludic.render3d/grass.ludic index 0101e4ee..81a8b6e1 100644 --- a/packages/ludic.render3d/grass.ludic +++ b/packages/ludic.render3d/grass.ludic @@ -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) diff --git a/packages/ludic.render3d/mesh.ludic b/packages/ludic.render3d/mesh.ludic index 699a4b94..1160c234 100644 --- a/packages/ludic.render3d/mesh.ludic +++ b/packages/ludic.render3d/mesh.ludic @@ -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 } +} diff --git a/packages/ludic.render3d/scatter.ludic b/packages/ludic.render3d/scatter.ludic index b22428a6..59fae935 100644 --- a/packages/ludic.render3d/scatter.ludic +++ b/packages/ludic.render3d/scatter.ludic @@ -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 +} diff --git a/packages/ludic.render3d/shaders/grass.vert b/packages/ludic.render3d/shaders/grass.vert index a9c37917..dbf8d966 100644 --- a/packages/ludic.render3d/shaders/grass.vert +++ b/packages/ludic.render3d/shaders/grass.vert @@ -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)); diff --git a/packages/ludic.render3d/shaders/heightgen.frag b/packages/ludic.render3d/shaders/heightgen.frag index 591abaca..4da0e16e 100644 --- a/packages/ludic.render3d/shaders/heightgen.frag +++ b/packages/ludic.render3d/shaders/heightgen.frag @@ -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) diff --git a/packages/ludic.render3d/shaders/terrain.frag b/packages/ludic.render3d/shaders/terrain.frag index fe2447aa..d98afcca 100644 --- a/packages/ludic.render3d/shaders/terrain.frag +++ b/packages/ludic.render3d/shaders/terrain.frag @@ -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); diff --git a/packages/ludic.render3d/stream.ludic b/packages/ludic.render3d/stream.ludic index 6ffcd7f4..15db50a5 100644 --- a/packages/ludic.render3d/stream.ludic +++ b/packages/ludic.render3d/stream.ludic @@ -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 +} diff --git a/packages/ludic.render3d/terrain.ludic b/packages/ludic.render3d/terrain.ludic index c81d1241..be1f5270 100644 --- a/packages/ludic.render3d/terrain.ludic +++ b/packages/ludic.render3d/terrain.ludic @@ -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") +} diff --git a/packages/ludic.render3d/water.ludic b/packages/ludic.render3d/water.ludic index 45f51a95..8f18da18 100644 --- a/packages/ludic.render3d/water.ludic +++ b/packages/ludic.render3d/water.ludic @@ -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) }