render3d: the height field and photograph on the CPU as tiles read from a file (off until the game asks)
terrain_tiles_to(dir, key) before a map is made: once made, its heights, photograph and baked normals are written tile by tile (64 m, TT_TEX) to <dir>/<key>.tiles - fresh every time, header last, so no other generator's or a torn file is ever read - and the whole copies go. Every read goes through the tile: resident, else read from the file there and then into a 2048-tile clock, so terrain_height, terrain_height_smooth, terrain_ortho* and terrain_chunk_heights answer exactly what the whole copy did (R3D_TT_CHECK: worst 0.0 m over 4000 points) whatever is resident - two machines and the boot's placements agree to the bit. terrain_chunk_heights takes render3d_st mut for it. terrain_height_near(read-only): the tile if it is in, else a coarse 2048^2 level (worst 0.91 m), never the file - for line checks that must not take render3d_st mut. terrain_texel() is the texel size, terrain_tiles_prefetch(x, z, r, budget) reads ahead, and a frame that reads more than 8 tiles says so once. R3D_TERRAIN_TILES=<dir> turns it on for a run. At the overlook with MallocLargeCache=0: 1731 MB off, 1658 MB on; 192 MB written in ~200 ms; the frame unchanged (0 pixels over 8). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
c5e58d14ea
commit
5b3cfac48e
8 changed files with 385 additions and 15 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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=<m>: the fog wall at that distance, for a shot (a game sets it with r3d_fog_wall)
|
||||
# R3D_TERRAIN_TILES=<dir>: 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=<n>: from frame n on, rebuild every screen-sized buffer every few
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
181
packages/ludic.render3d/terrain_tiles.ludic
Normal file
181
packages/ludic.render3d/terrain_tiles.ludic
Normal file
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
160
packages/ludic.render3d/terrain_tiles_cut.ludic
Normal file
160
packages/ludic.render3d/terrain_tiles_cut.ludic
Normal file
|
|
@ -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`)
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue