diff --git a/packages/ludic.render3d/env.ludic b/packages/ludic.render3d/env.ludic index 100b0b6f..4479bcf9 100644 --- a/packages/ludic.render3d/env.ludic +++ b/packages/ludic.render3d/env.ludic @@ -829,6 +829,25 @@ export state Render3dState { ter_ortho_tex: int = 0 # a photograph of the same window, draped with distance ter_carpet: int = 0 # the distant-grass carpet (carpet_bake), 0 = none ter_shadow_tex: int = 0 # height-field sun shadow: R32F, the lowest lit height + tt_dir: string = "" # terrain_tiles_to: where a map's tiles are written ("" keeps the whole copies) + tt_key: string = "" + tt_file: pointer = null # the tiles' file, open for reading (terrain_tiles.ludic) + tt_n: int = 0 # tiles a side + tt_oside: int = 0 # photograph texels a tile a side + tt_hoff: int = 0 # where the heights and the photograph start in the file + tt_ooff: int = 0 + tt_noff: int = 0 + tt_h: floats = null # TT_SLOTS tiles of heights + tt_o: words = null # ... and of photograph, 0xRRGGBB + tt_nm: words = null # ... and of normals, RG16F in a word + tt_coarse: floats = null # the heights at TT_COARSE a side, for terrain_height_near + tt_slot_of: words = null # per tile: its slot, or -1 + tt_tile_in: words = null # per slot: its tile, or -1 + tt_ref: words = null # per slot: asked for since the clock's hand last passed + tt_hand: int = 0 + tt_reads: int = 0 # tiles read from the file, over the run and this frame + tt_frame_reads: int = 0 + tt_warned: bool = false ter_shadow_prog_aux: int = 0 # the bake of the aux target (TS_AUX) ter_shadow_aux: int = 0 # beside it, RG16F: the occluder distance and the cloud mask ter_shadow_yaw: float = 1000000000.0 # the sky yaw it was baked for diff --git a/packages/ludic.render3d/r3d.ludic b/packages/ludic.render3d/r3d.ludic index 90c22f66..69eb70e3 100644 --- a/packages/ludic.render3d/r3d.ludic +++ b/packages/ludic.render3d/r3d.ludic @@ -25,6 +25,8 @@ import "sky.ludic" import "daylight.ludic" import "terrain.ludic" import "terrain_chunks.ludic" +import "terrain_tiles.ludic" +import "terrain_tiles_cut.ludic" import "overlay.ludic" import "shadow.ludic" import "post.ludic" diff --git a/packages/ludic.render3d/render.ludic b/packages/ludic.render3d/render.ludic index 81c63e92..acf1e2d7 100644 --- a/packages/ludic.render3d/render.ludic +++ b/packages/ludic.render3d/render.ludic @@ -77,6 +77,8 @@ function r3d_env_flags(render3d_st: mut Render3dState) -> void { render3d_st.r3d_no_trees = r3d_env_has(render3d_st, "R3D_NOTREES") render3d_st.r3d_no_refl = r3d_env_has(render3d_st, "R3D_NOREFL") # R3D_FOG_WALL=: the fog wall at that distance, for a shot (a game sets it with r3d_fog_wall) + # R3D_TERRAIN_TILES=: the height field and photograph as tiles in that directory (terrain_tiles_to) + if r3d_env_has(render3d_st, "R3D_TERRAIN_TILES") and render3d_st.tt_dir == "" { terrain_tiles_to(render3d_st, r3d_env(render3d_st, "R3D_TERRAIN_TILES"), "terrain") } if r3d_env_has(render3d_st, "R3D_FOG_WALL") { render3d_st.r3d_fog_wall = float(Text.to_int(r3d_env(render3d_st, "R3D_FOG_WALL"))) } if r3d_env_has(render3d_st, "R3D_DEBUG") { render3d_st.r3d_debug = true } if r3d_env_has(render3d_st, "R3D_DBGSHADOW") { render3d_st.r3d_debug_shadow = true } @@ -224,6 +226,7 @@ function r3d_frame__t(render3d_st: mut Render3dState, time: float) -> void { # a stale attachment or a texture freed twice shows up. render3d_st.r3d_test_frame += 1 vkmem_tick(render3d_st) + tt_frame(render3d_st) fog_at_tick(render3d_st) if render3d_st.r3d_test_resize == 0 and r3d_env_has(render3d_st, "R3D_RESIZE_AT") { render3d_st.r3d_test_resize = Text.to_int(r3d_env(render3d_st, "R3D_RESIZE_AT")) } # R3D_RESIZE_AT=: from frame n on, rebuild every screen-sized buffer every few diff --git a/packages/ludic.render3d/terrain.ludic b/packages/ludic.render3d/terrain.ludic index fa6ed1c8..9217f4d6 100644 --- a/packages/ludic.render3d/terrain.ludic +++ b/packages/ludic.render3d/terrain.ludic @@ -67,7 +67,7 @@ function terrain_coast(render3d_st: mut Render3dState, cx: float, cz: float, mar # the photograph's colour at world (x, z): packed 0xRRGGBB (0 outside the map) function terrain_ortho(render3d_st: mut Render3dState, x: float, z: float) -> int { - if render3d_st.ter_ortho_px == null { return 0 } + if render3d_st.ter_ortho_px == null and render3d_st.tt_file == null { return 0 } if render3d_st.ter_o_scale == 0.0 { render3d_st.ter_o_scale = float(render3d_st.ter_ortho_w) / float(render3d_st.TERRAIN_HALF * 2) } let scale = render3d_st.ter_o_scale var ix = int(Math.floor((x - render3d_st.ter_ox + float(render3d_st.TERRAIN_HALF)) * scale)) @@ -75,6 +75,7 @@ function terrain_ortho(render3d_st: mut Render3dState, x: float, z: float) -> in if ix < 0 { ix = 0 }; if iz < 0 { iz = 0 } if ix > render3d_st.ter_ortho_w - 1 { ix = render3d_st.ter_ortho_w - 1 }; if iz > render3d_st.ter_ortho_w - 1 { iz = render3d_st.ter_ortho_w - 1 } let o = (iz * render3d_st.ter_ortho_w + ix) * render3d_st.ter_ortho_c + if render3d_st.ter_ortho_px == null { return ter_o(render3d_st, ix, iz) } return (render3d_st.ter_ortho_px[o] << 16) | (render3d_st.ter_ortho_px[o + 1] << 8) | render3d_st.ter_ortho_px[o + 2] } # The three classifiers below all read the same pixel. A caller that wants more than @@ -162,6 +163,8 @@ function terrain_use_ortho__t(render3d_st: mut Render3dState, path: string) -> v gpu_tex_bind(render3d_st, GPU_TEX2D, render3d_st.ter_ortho_tex) gpu_tex_param(render3d_st, GPU_TEX2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE) gpu_tex_param(render3d_st, GPU_TEX2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE) + # a photograph that comes after the height field: now both are here, they can go to tiles + if render3d_st.cd_range != null { terrain_tiles_cut(render3d_st) } } # the GPU memory it makes is counted as VKM_TERRAIN (R3D_VKMEM) function terrain_use_dem(render3d_st: mut Render3dState, path: string, emin: float, emax: float, base: float, ox: float, oz: float) -> void { @@ -300,7 +303,7 @@ function terrain_bake_pass(render3d_st: mut Render3dState, p: int, target: int) # of thousands of candidates per chunk. Hoisted, they cost nothing. function terrain_height(render3d_st: mut Render3dState, x: float, z: float) -> float { - if render3d_st.ter_heights == null { return 0.0 } # a plate (r3d_plate_mode): flat at y = 0 + if not ter_present(render3d_st) { return 0.0 } # a plate (r3d_plate_mode): flat at y = 0 if render3d_st.ter_h_scale == 0.0 { render3d_st.ter_h_scale = float(TERRAIN_RES) / float(render3d_st.TERRAIN_HALF * 2) } let scale = render3d_st.ter_h_scale let fx = (x - render3d_st.ter_ox + float(render3d_st.TERRAIN_HALF)) * scale @@ -310,10 +313,10 @@ function terrain_height(render3d_st: mut Render3dState, x: float, z: float) -> f if ix > TERRAIN_RES - 2 { ix = TERRAIN_RES - 2 }; if iz > TERRAIN_RES - 2 { iz = TERRAIN_RES - 2 } let tx = Math.clamp(fx - float(ix), 0.0, 1.0) let tz = Math.clamp(fz - float(iz), 0.0, 1.0) - let h00 = render3d_st.ter_heights[iz * TERRAIN_RES + ix] - let h10 = render3d_st.ter_heights[iz * TERRAIN_RES + ix + 1] - let h01 = render3d_st.ter_heights[(iz + 1) * TERRAIN_RES + ix] - let h11 = render3d_st.ter_heights[(iz + 1) * TERRAIN_RES + ix + 1] + let h00 = ter_h(render3d_st, ix, iz) + let h10 = ter_h(render3d_st, ix + 1, iz) + let h01 = ter_h(render3d_st, ix, iz + 1) + let h11 = ter_h(render3d_st, ix + 1, iz + 1) return Math.lerp(Math.lerp(h00, h10, tx), Math.lerp(h01, h11, tx), tz) } # the same for Q16.16 callers @@ -325,7 +328,7 @@ function terrain_height_fx(render3d_st: mut Render3dState, x: fixed, z: fixed) - # and one buried to the knee in it. Sixteen taps; for things that move, not for the # thousands of placement queries a chunk makes. function terrain_height_smooth(render3d_st: mut Render3dState, x: float, z: float) -> float { - if render3d_st.ter_heights == null { return 0.0 } + if not ter_present(render3d_st) { return 0.0 } if render3d_st.ter_h_scale == 0.0 { render3d_st.ter_h_scale = float(TERRAIN_RES) / float(render3d_st.TERRAIN_HALF * 2) } let scale = render3d_st.ter_h_scale let fx = (x - render3d_st.ter_ox + float(render3d_st.TERRAIN_HALF)) * scale - 0.5 @@ -353,7 +356,7 @@ function terrain_height_smooth(render3d_st: mut Render3dState, x: float, z: floa for i in 0 .. 4 { var rx = ix - 1 + i if rx < 0 { rx = 0 }; if rx > TERRAIN_RES - 1 { rx = TERRAIN_RES - 1 } - row = row + render3d_st.ter_bw[i] * render3d_st.ter_heights[rz * TERRAIN_RES + rx] + row = row + render3d_st.ter_bw[i] * ter_h(render3d_st, rx, rz) } h = h + render3d_st.ter_bw[4 + j] * row } @@ -680,6 +683,7 @@ function cdlod_init(render3d_st: mut Render3dState) -> void { if r3d_env_has(render3d_st, "R3D_CD_R0") { r = float(Text.to_int(r3d_env(render3d_st, "R3D_CD_R0"))) } for l in 0 .. CD_LEVELS { render3d_st.cd_range[l] = r; r = r * 2.0 } cdlod_bounds(render3d_st) + terrain_tiles_cut(render3d_st) } # min/max height per patch at every level, from the CPU copy of the height field @@ -696,7 +700,7 @@ function cdlod_bounds(render3d_st: mut Render3dState) -> void { let ty = min(j * t + y, TERRAIN_RES - 1) for x in 0 .. t + 1 { let tx = min(i * t + x, TERRAIN_RES - 1) - let h = render3d_st.ter_heights[ty * TERRAIN_RES + tx] + let h = ter_h(render3d_st, tx, ty) mn = Math.min(mn, h); mx = Math.max(mx, h) } } @@ -819,6 +823,7 @@ function terrain_unload(render3d_st: mut Render3dState) -> void { if render3d_st.ter_dem_tex != 0 { gpu_tex_free(render3d_st, render3d_st.ter_dem_tex); render3d_st.ter_dem_tex = 0 } if render3d_st.ter_ortho_tex != 0 { gpu_tex_free(render3d_st, render3d_st.ter_ortho_tex); render3d_st.ter_ortho_tex = 0 } if render3d_st.ter_heights != null { free(render3d_st.ter_heights); render3d_st.ter_heights = null } + tt_close(render3d_st) if render3d_st.ter_ortho_px != null { free(render3d_st.ter_ortho_px); render3d_st.ter_ortho_px = null } render3d_st.ter_ortho_w = 0 if render3d_st.cd_min != null { @@ -843,6 +848,7 @@ function terrain_reload(render3d_st: mut Render3dState, dem: string, emin: float terrain_generate(render3d_st) terrain_bake_shadow(render3d_st) cdlod_bounds(render3d_st) + terrain_tiles_cut(render3d_st) gpu_check(render3d_st, "terrain reload") } diff --git a/packages/ludic.render3d/terrain_chunks.ludic b/packages/ludic.render3d/terrain_chunks.ludic index c5cf19f4..5a0b009c 100644 --- a/packages/ludic.render3d/terrain_chunks.ludic +++ b/packages/ludic.render3d/terrain_chunks.ludic @@ -4,12 +4,11 @@ # the ground's height at texel (tx, tz) as the renderer draws it - the cubic B-spline, which at a # texel's centre is the separable (1 4 1) / 6 filter of its neighbours, clamped at the map's edge: # the same numbers ludic.physics' jph_shape_heightfield_bspline makes of the whole map -function ter_spline_at(render3d_st: Render3dState, tx: int, tz: int) -> float { +function ter_spline_at(render3d_st: mut Render3dState, tx: int, tz: int) -> float { var sum = 0.0 for d in -1 .. 2 { let z = min(max(tz + d, 0), TERRAIN_RES - 1) - let r = z * TERRAIN_RES - let row = (render3d_st.ter_heights[r + max(tx - 1, 0)] + 4.0 * render3d_st.ter_heights[r + tx] + render3d_st.ter_heights[r + min(tx + 1, TERRAIN_RES - 1)]) / 6.0 + let row = (ter_h(render3d_st, max(tx - 1, 0), z) + 4.0 * ter_h(render3d_st, tx, z) + ter_h(render3d_st, min(tx + 1, TERRAIN_RES - 1), z)) / 6.0 if d == 0 { sum = sum + 4.0 * row } else { sum = sum + row } } return sum / 6.0 @@ -23,8 +22,8 @@ function ter_spline_at(render3d_st: Render3dState, tx: int, tz: int) -> float { # the caller's, reused), nothing allocated. The read-back lives from terrain_generate to # terrain_unload - the whole of a map - so a chunk can be read any time between; false when there # is none (a plate, or before the map is made). -function terrain_chunk_heights(render3d_st: Render3dState, i: int, j: int, size_m: float, n: int, out: floats) -> bool { - if render3d_st.ter_heights == null or n < 2 or len(out) < n * n { return false } +function terrain_chunk_heights(render3d_st: mut Render3dState, i: int, j: int, size_m: float, n: int, out: floats) -> bool { + if not ter_present(render3d_st) or n < 2 or len(out) < n * n { return false } let texel = 2.0 * float(render3d_st.TERRAIN_HALF) / float(TERRAIN_RES) let step = size_m / float(n - 1) let x0 = float(i) * size_m diff --git a/packages/ludic.render3d/terrain_tiles.ludic b/packages/ludic.render3d/terrain_tiles.ludic new file mode 100644 index 00000000..dc65f521 --- /dev/null +++ b/packages/ludic.render3d/terrain_tiles.ludic @@ -0,0 +1,181 @@ +# terrain_tiles.ludic — the height field and the photograph on the CPU as tiles read from a file, so +# only the tiles something asks about are in memory (plan 26 of maroon-lake). The game turns it on with +# terrain_tiles_to(dir, key) before the map is made; without that the whole copies stay, as before. +# Every answer is the texel the whole copy held: a tile not in is read from the file there and then, +# so what a query returns never depends on what happens to be resident - two machines, and the +# placements at boot, get the same numbers to the bit. + +const TT_TEX: int = 32 # height texels a tile a side: 64 m at 2 m (a build-time constant) +const TT_SHIFT: int = 5 +const TT_MASK: int = 31 +const TT_SLOTS: int = 2048 # tiles held at once: 8 MB each of heights, photograph and normals +const TT_WARN_READS: int = 8 # a frame that reads more from the file than this is said, once + +# where a map's tiles are written, and under what name: set by the game before the map is made +export function terrain_tiles_to(render3d_st: mut Render3dState, dir: string, key: string) -> void { + render3d_st.tt_dir = intern(dir) + render3d_st.tt_key = intern(key) +} + +# the height texel (tx, tz): the whole copy's, or its tile's +function ter_h(render3d_st: mut Render3dState, tx: int, tz: int) -> float { + if render3d_st.ter_heights != null { return render3d_st.ter_heights[tz * TERRAIN_RES + tx] } + let s = tt_slot(render3d_st, (tz >> TT_SHIFT) * render3d_st.tt_n + (tx >> TT_SHIFT)) + if s < 0 { return 0.0 } + return render3d_st.tt_h[s * TT_TEX * TT_TEX + (tz & TT_MASK) * TT_TEX + (tx & TT_MASK)] +} + +# the photograph's texel (px, pz) as 0xRRGGBB +function ter_o(render3d_st: mut Render3dState, px: int, pz: int) -> int { + let side = render3d_st.tt_oside + let s = tt_slot(render3d_st, (pz / side) * render3d_st.tt_n + (px / side)) + if s < 0 { return 0 } + return render3d_st.tt_o[s * side * side + (pz % side) * side + (px % side)] +} + +# the baked normal at height texel (tx, tz): x and z as two halves in a word (RG16F), for the GPU's tiles +function ter_n(render3d_st: mut Render3dState, tx: int, tz: int) -> int { + let s = tt_slot(render3d_st, (tz >> TT_SHIFT) * render3d_st.tt_n + (tx >> TT_SHIFT)) + if s < 0 { return 0 } + return render3d_st.tt_nm[s * TT_TEX * TT_TEX + (tz & TT_MASK) * TT_TEX + (tx & TT_MASK)] +} + +# is there a height field to ask at all (a plate has none) +function ter_present(render3d_st: Render3dState) -> bool { return render3d_st.ter_heights != null or render3d_st.tt_file != null } + +# the slot tile t is in, read from the file first when it is not +function tt_slot(render3d_st: mut Render3dState, t: int) -> int { + if render3d_st.tt_file == null or t < 0 or t >= len(render3d_st.tt_slot_of) { return -1 } + let s = render3d_st.tt_slot_of[t] + if s >= 0 { + render3d_st.tt_ref[s] = 1 + return s + } + return tt_read(render3d_st, t) +} + +# a slot let go (the clock: one asked for since the hand last passed keeps its place), and tile t read into it +function tt_read(render3d_st: mut Render3dState, t: int) -> int { + var s = render3d_st.tt_hand + while render3d_st.tt_ref[s] != 0 { + render3d_st.tt_ref[s] = 0 + s = (s + 1) % TT_SLOTS + } + render3d_st.tt_hand = (s + 1) % TT_SLOTS + let old = render3d_st.tt_tile_in[s] + if old >= 0 { render3d_st.tt_slot_of[old] = -1 } + let hb = TT_TEX * TT_TEX * 4 + file_seek(render3d_st.tt_file, render3d_st.tt_hoff + t * hb, 0) + file_read(render3d_st.tt_file, mem_off(data_of(render3d_st.tt_h), s * hb), hb) + let ob = render3d_st.tt_oside * render3d_st.tt_oside * 4 + if ob > 0 { + file_seek(render3d_st.tt_file, render3d_st.tt_ooff + t * ob, 0) + file_read(render3d_st.tt_file, mem_off(data_of(render3d_st.tt_o), s * ob), ob) + } + file_seek(render3d_st.tt_file, render3d_st.tt_noff + t * hb, 0) + file_read(render3d_st.tt_file, mem_off(data_of(render3d_st.tt_nm), s * hb), hb) + render3d_st.tt_tile_in[s] = t + render3d_st.tt_slot_of[t] = s + render3d_st.tt_ref[s] = 1 + render3d_st.tt_reads += 1 + render3d_st.tt_frame_reads += 1 + return s +} + +# at a frame's start: say a frame that read too much from the file (once), and count the next +function tt_frame(render3d_st: mut Render3dState) -> void { + if render3d_st.tt_frame_reads > TT_WARN_READS and not render3d_st.tt_warned { + render3d_st.tt_warned = true + tt_say_reads(render3d_st.tt_frame_reads) + } + render3d_st.tt_frame_reads = 0 +} +function tt_say_reads(n: int) -> void { print(`r3d: terrain: a frame read {n} tiles from the file (said once; terrain_tiles_prefetch keeps them in ahead)`) } + +# the tiles within r of (x, z) read in ahead of being asked, at most `budget` a call +export function terrain_tiles_prefetch(render3d_st: mut Render3dState, x: float, z: float, r: float, budget: int) -> void { + if render3d_st.tt_file == null { return } + let tile_m = float(TT_TEX) * 2.0 * float(render3d_st.TERRAIN_HALF) / float(TERRAIN_RES) + let fx = (x - render3d_st.ter_ox + float(render3d_st.TERRAIN_HALF)) / tile_m + let fz = (z - render3d_st.ter_oz + float(render3d_st.TERRAIN_HALF)) / tile_m + let rt = r / tile_m + 1.0 + let n = render3d_st.tt_n + var left = budget + for j in max(int(fz - rt), 0) .. min(int(fz + rt) + 1, n) { + for i in max(int(fx - rt), 0) .. min(int(fx + rt) + 1, n) { + let t = j * n + i + if left > 0 and render3d_st.tt_slot_of[t] < 0 { + tt_read(render3d_st, t) + render3d_st.tt_frame_reads -= 1 + left -= 1 + } + } + } +} + +# metres a height texel spans (the chunks' sample spacing), whether or not the whole copy is kept +export function terrain_texel(render3d_st: Render3dState) -> float { return 2.0 * float(render3d_st.TERRAIN_HALF) / float(TERRAIN_RES) } + +# ---- a height without the cache: read-only ---------------------------------------------------- +# For what asks often and only locally - a line of sight, the aim, a photograph's check - and must +# not take render3d_st mut down its whole path: the tile if it is in, else the coarse whole-map level +# (TT_COARSE a side, the mean of 2 x 2 texels). It never reads the file, so over ground nobody is near +# it can be a few decimetres off the exact answer; anything two machines must agree on asks +# terrain_height. +const TT_COARSE: int = 2048 +const TT_ABSENT: float = -1000000.0 # tt_peek: that texel is not in memory + +export function terrain_height_near(render3d_st: Render3dState, x: float, z: float) -> float { + if render3d_st.ter_heights == null and render3d_st.tt_file == null { return 0.0 } + let scale = float(TERRAIN_RES) / float(render3d_st.TERRAIN_HALF * 2) + let fx = (x - render3d_st.ter_ox + float(render3d_st.TERRAIN_HALF)) * scale + let fz = (z - render3d_st.ter_oz + float(render3d_st.TERRAIN_HALF)) * scale + let ix = min(max(int(Math.floor(fx)), 0), TERRAIN_RES - 2) + let iz = min(max(int(Math.floor(fz)), 0), TERRAIN_RES - 2) + let tx = Math.clamp(fx - float(ix), 0.0, 1.0) + let tz = Math.clamp(fz - float(iz), 0.0, 1.0) + let h00 = tt_peek(render3d_st, ix, iz) + let h10 = tt_peek(render3d_st, ix + 1, iz) + let h01 = tt_peek(render3d_st, ix, iz + 1) + let h11 = tt_peek(render3d_st, ix + 1, iz + 1) + if h00 == TT_ABSENT or h10 == TT_ABSENT or h01 == TT_ABSENT or h11 == TT_ABSENT { return tt_coarse_at(render3d_st, fx, fz) } + return Math.lerp(Math.lerp(h00, h10, tx), Math.lerp(h01, h11, tx), tz) +} + +# a height texel if it is in memory (the whole copy, or a resident tile), else TT_ABSENT +function tt_peek(render3d_st: Render3dState, tx: int, tz: int) -> float { + if render3d_st.ter_heights != null { return render3d_st.ter_heights[tz * TERRAIN_RES + tx] } + let s = render3d_st.tt_slot_of[(tz >> TT_SHIFT) * render3d_st.tt_n + (tx >> TT_SHIFT)] + if s < 0 { return TT_ABSENT } + return render3d_st.tt_h[s * TT_TEX * TT_TEX + (tz & TT_MASK) * TT_TEX + (tx & TT_MASK)] +} + +# the coarse level at fine texel position (fx, fz), bilinear: coarse texel c's centre is fine 2c + 0.5 +function tt_coarse_at(render3d_st: Render3dState, fx: float, fz: float) -> float { + let c = render3d_st.tt_coarse + if c == null { return 0.0 } + let cx = (fx - 0.5) * 0.5 + let cz = (fz - 0.5) * 0.5 + let ix = min(max(int(Math.floor(cx)), 0), TT_COARSE - 2) + let iz = min(max(int(Math.floor(cz)), 0), TT_COARSE - 2) + let tx = Math.clamp(cx - float(ix), 0.0, 1.0) + let tz = Math.clamp(cz - float(iz), 0.0, 1.0) + let a = Math.lerp(c[iz * TT_COARSE + ix], c[iz * TT_COARSE + ix + 1], tx) + let b = Math.lerp(c[(iz + 1) * TT_COARSE + ix], c[(iz + 1) * TT_COARSE + ix + 1], tx) + return Math.lerp(a, b, tz) +} + +# the coarse level from the whole copy, before it goes +@alloc_ok("a map being made: the coarse level, once") +function tt_make_coarse(render3d_st: mut Render3dState) -> void { + if render3d_st.tt_coarse == null { render3d_st.tt_coarse = floats(TT_COARSE * TT_COARSE) } + let h = render3d_st.ter_heights + let k = TERRAIN_RES / TT_COARSE + for j in 0 .. TT_COARSE { + for i in 0 .. TT_COARSE { + var sum = 0.0 + for b in 0 .. k { for a in 0 .. k { sum = sum + h[(j * k + b) * TERRAIN_RES + i * k + a] } } + render3d_st.tt_coarse[j * TT_COARSE + i] = sum / float(k * k) + } + } +} diff --git a/packages/ludic.render3d/terrain_tiles_cut.ludic b/packages/ludic.render3d/terrain_tiles_cut.ludic new file mode 100644 index 00000000..8b4e9f08 --- /dev/null +++ b/packages/ludic.render3d/terrain_tiles_cut.ludic @@ -0,0 +1,160 @@ +# terrain_tiles_cut.ludic — the whole height field and photograph written out tile by tile, once a map is +# made, and let go. The file is written fresh every time a map is made and never read by another +# start, so no older generator's tiles are ever read; its header goes in last, so a start that died +# writing it leaves a file whose magic is not there rather than one that looks whole. + +const TT_MAGIC: int = 1415074892 # "LTTT" +const TT_VERSION: int = 1 +const TT_HEAD: int = 64 # bytes: 16 words + +@alloc_ok("a map being made: its tiles' file and their pools, once, not a frame") +function terrain_tiles_cut(render3d_st: mut Render3dState) -> void { + if render3d_st.tt_dir == "" or render3d_st.ter_heights == null or render3d_st.ter_ortho_px == null { return } + let t0 = gl_now_us() + tt_close(render3d_st) + let n = TERRAIN_RES / TT_TEX + let side = render3d_st.ter_ortho_w / n + let path = `{render3d_st.tt_dir}/{render3d_st.tt_key}.tiles` + Fs.mkdir(render3d_st.tt_dir) + let f = file_open(path, "wb") + if f == null { tt_say_no_file(path); return } + let head = words(16) + for k in 0 .. 16 { head[k] = 0 } + file_write(f, data_of(head), TT_HEAD) + var sum = tt_write_heights(render3d_st, f, n) + sum = tt_write_ortho(render3d_st, f, n, side, sum) + sum = tt_write_normals(render3d_st, f, n, sum) + head[0] = TT_MAGIC; head[1] = TT_VERSION; head[2] = TT_TEX; head[3] = n; head[4] = side; head[5] = TERRAIN_RES + head[6] = render3d_st.ter_ortho_w; head[7] = sum + file_seek(f, 0, 0) + file_write(f, data_of(head), TT_HEAD) + file_close(f) + free(head) + tt_open(render3d_st, path, n, side) + tt_make_coarse(render3d_st) + if r3d_env_has(render3d_st, "R3D_TT_CHECK") { tt_check(render3d_st) } + free(render3d_st.ter_heights); render3d_st.ter_heights = null + free(render3d_st.ter_ortho_px); render3d_st.ter_ortho_px = null + tt_say_cut(path, TT_HEAD + n * n * (TT_TEX * TT_TEX * 2 + side * side) * 4, (gl_now_us() - t0) / 1000) +} + +# the heights, tile after tile, each a TT_TEX-row block straight out of the whole copy; their checksum +function tt_write_heights(render3d_st: Render3dState, f: pointer, n: int) -> int { + var sum = 0 + let base = data_of(render3d_st.ter_heights) + for j in 0 .. n { + for i in 0 .. n { + for r in 0 .. TT_TEX { + let at = (j * TT_TEX + r) * TERRAIN_RES + i * TT_TEX + file_write(f, mem_off(base, at * 4), TT_TEX * 4) + for k in 0 .. TT_TEX { sum = tt_mix(sum, float_bits(render3d_st.ter_heights[at + k])) } + } + } + } + return sum +} + +# the photograph, tile after tile, each texel packed 0xRRGGBB into a word +@alloc_ok("a map being made: one tile's scratch, freed after") +function tt_write_ortho(render3d_st: Render3dState, f: pointer, n: int, side: int, sum0: int) -> int { + var sum = sum0 + let buf = words(side * side) + let w = render3d_st.ter_ortho_w + let c = render3d_st.ter_ortho_c + let px = render3d_st.ter_ortho_px + for j in 0 .. n { + for i in 0 .. n { + for r in 0 .. side { + for k in 0 .. side { + let o = ((j * side + r) * w + i * side + k) * c + let v = (px[o] << 16) | (px[o + 1] << 8) | px[o + 2] + buf[r * side + k] = v + sum = tt_mix(sum, v) + } + } + file_write(f, data_of(buf), side * side * 4) + } + } + free(buf) + return sum +} + +# the baked normals (RG16F: x and z, two halves a word), read back once and written tile after tile +@alloc_ok("a map being made: the normals read back once, freed after") +function tt_write_normals(render3d_st: mut Render3dState, f: pointer, n: int, sum0: int) -> int { + var sum = sum0 + let all = words(TERRAIN_RES * TERRAIN_RES) + gpu_tex_bind(render3d_st, GPU_TEX2D, render3d_st.ter_normal_tex) + gpu_pixel_store(render3d_st, GL_PACK_ALIGNMENT, 4) + gpu_tex_read(render3d_st, GPU_TEX2D, GL_RG, GL_HALF_FLOAT, data_of(all)) + let base = data_of(all) + for j in 0 .. n { + for i in 0 .. n { + for r in 0 .. TT_TEX { + let at = (j * TT_TEX + r) * TERRAIN_RES + i * TT_TEX + file_write(f, mem_off(base, at * 4), TT_TEX * 4) + for k in 0 .. TT_TEX { sum = tt_mix(sum, all[at + k]) } + } + } + } + free(all) + return sum +} + +function tt_mix(h: int, v: int) -> int { return ((h ^ (v & 0xFFFFFFFF)) * 16777619) & 0xFFFFFFFF } + +# the file opened for the tiles' reads, and the pools, the index and the clock made for it +@alloc_ok("a map being made: its tiles' file and their pools, once, not a frame") +function tt_open(render3d_st: mut Render3dState, path: string, n: int, side: int) -> void { + render3d_st.tt_file = file_open(path, "rb") + render3d_st.tt_n = n; render3d_st.tt_oside = side + render3d_st.tt_hoff = TT_HEAD + render3d_st.tt_ooff = TT_HEAD + n * n * TT_TEX * TT_TEX * 4 + render3d_st.tt_noff = render3d_st.tt_ooff + n * n * side * side * 4 + if render3d_st.tt_nm == null { render3d_st.tt_nm = words(TT_SLOTS * TT_TEX * TT_TEX) } + if render3d_st.tt_h == null { render3d_st.tt_h = floats(TT_SLOTS * TT_TEX * TT_TEX) } + if render3d_st.tt_o == null or len(render3d_st.tt_o) != TT_SLOTS * side * side { + if render3d_st.tt_o != null { free(render3d_st.tt_o) } + render3d_st.tt_o = words(TT_SLOTS * side * side) + } + if render3d_st.tt_slot_of != null { free(render3d_st.tt_slot_of) } + render3d_st.tt_slot_of = words(n * n) + for t in 0 .. n * n { render3d_st.tt_slot_of[t] = -1 } + if render3d_st.tt_tile_in == null { render3d_st.tt_tile_in = words(TT_SLOTS); render3d_st.tt_ref = words(TT_SLOTS) } + for s in 0 .. TT_SLOTS { render3d_st.tt_tile_in[s] = -1; render3d_st.tt_ref[s] = 0 } + render3d_st.tt_hand = 0 +} + +# the last map's file let go (a world swap, or the map made again) +function tt_close(render3d_st: mut Render3dState) -> void { + if render3d_st.tt_file != null { file_close(render3d_st.tt_file); render3d_st.tt_file = null } +} + +function tt_say_no_file(path: string) -> void { print(`r3d: terrain: cannot write {path}; the whole height field stays in memory`) } +function tt_say_cut(path: string, bytes: int, ms: int) -> void { print(`r3d: terrain: tiles written to {path} ({bytes / 1048576} MB, {ms} ms); the whole copies let go`) } + +# R3D_TT_CHECK: the tiles' answer against the whole copy's at 4000 points (must be exact), and the +# coarse level's (what terrain_height_near falls back to) - while both are still here +@alloc_ok("a developer check, once, under R3D_TT_CHECK") +function tt_check(render3d_st: mut Render3dState) -> void { + var worst = 0.0 + var worst_c = 0.0 + var seed = 12345 + let half = float(render3d_st.TERRAIN_HALF) * 0.98 + for k in 0 .. 4000 { + seed = (seed * 1103515245 + 12345) & 0x7FFFFFFF + let x = render3d_st.ter_ox + (float(seed % 100000) / 50000.0 - 1.0) * half + seed = (seed * 1103515245 + 12345) & 0x7FFFFFFF + let z = render3d_st.ter_oz + (float(seed % 100000) / 50000.0 - 1.0) * half + let whole = terrain_height(render3d_st, x, z) + let keep = render3d_st.ter_heights + render3d_st.ter_heights = null + let tiled = terrain_height(render3d_st, x, z) + let scale = float(TERRAIN_RES) / float(render3d_st.TERRAIN_HALF * 2) + let coarse = tt_coarse_at(render3d_st, (x - render3d_st.ter_ox + float(render3d_st.TERRAIN_HALF)) * scale, (z - render3d_st.ter_oz + float(render3d_st.TERRAIN_HALF)) * scale) + render3d_st.ter_heights = keep + worst = Math.max(worst, Math.abs(tiled - whole)) + worst_c = Math.max(worst_c, Math.abs(coarse - whole)) + } + print(`r3d: terrain: tiles vs the whole copy at 4000 points: worst {worst} m; the coarse level: worst {worst_c} m`) +} diff --git a/packages/ludic.render3d/water.ludic b/packages/ludic.render3d/water.ludic index 4892cac7..5b26a5bf 100644 --- a/packages/ludic.render3d/water.ludic +++ b/packages/ludic.render3d/water.ludic @@ -99,7 +99,7 @@ function water_reflection_pass(render3d_st: mut Render3dState) -> void { function water_reflect_visible(render3d_st: mut Render3dState) -> bool { if render3d_st.wb_primary < 0 { return false } if render3d_st.wb_refl_always < 0 { render3d_st.wb_refl_always = 0; if r3d_env_has(render3d_st, "R3D_REFL_ALWAYS") { render3d_st.wb_refl_always = 1 } } - if render3d_st.wb_refl_always == 1 or render3d_st.ter_heights == null { return true } + if render3d_st.wb_refl_always == 1 or not ter_present(render3d_st) { return true } if render3d_st.wb_ncells < 0 { let t0 = gl_now_us(); water_cells_build(render3d_st); render3d_st.wb_build_us = gl_now_us() - t0 } if render3d_st.wb_dbg < 0 { render3d_st.wb_dbg = 0; if r3d_env_has(render3d_st, "R3D_REFL_DBG") { render3d_st.wb_dbg = 1 } } if render3d_st.wb_dbg == 1 {