ludic/packages/ludic.render3d/terrain_tiles_cut.ludic
Orkuncakilkaya a12f1b1201 render3d: the terrain as quantized tiles (LTT2) that every reader answers from, baked or cut
Heights as u16 over each 64 m tile's own minimum and step (a tile spanning 200 m steps 3 mm), normals
octahedral 8 + 8, the photograph RGB8, and the coarse level (2048: heights f32, normals, photograph)
- 30 + 32 + 48 + 16 + 8 + 12 MB, about 146 MB a map where the float tiles were 192 plus nothing coarse.
The writer puts the whole copy back to the quantized values as it goes, so the build's own queries,
the physics, the placements' bake and every machine read the same numbers; a tile read decodes them
into the pools the queries and the page pool already use.

terrain_tiles_bake(path, key, version, inputs_hash) writes a bake's file (ludic.base's LBAK header,
the tiles as its payload) from a made map; terrain_from_baked(path, key, version, half, ox, oz) opens
one at boot in place of terrain_use_dem / terrain_use_ortho, and terrain_init then generates nothing
(and bakes the sun's shadow from the coarse level unless terrain_shadow_from_bytes gave it). Without a
bake the cut writes the same format to <dir>/<key>.tiles and reads it back. The GPU's coarse level is
made from the file's coarse sections (the blit from the whole maps is gone).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 17:23:53 +03:00

73 lines
4.2 KiB
Text

# 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_baked or 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 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 }
# the check compares the tiles with the whole copy, so it runs on the quantized copy the write leaves
let bytes = ltt2_write(render3d_st, f, 0)
file_close(f)
if not tt_open_ltt2(render3d_st, path, 0) { tt_say_no_file(path); return }
if r3d_env_has(render3d_st, "R3D_TT_CHECK") { tt_check(render3d_st) }
ltt2_gpu_coarse(render3d_st)
tt_say_cut(path, bytes, (gl_now_us() - t0) / 1000)
}
# 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 kept until terrain_tiles_build_done`) }
# 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`)
}
# The world's build places trees, rocks and cover over the whole map, asking exact heights
# everywhere before any ring exists: through the tiles that read the file some 25 000 times. So the
# whole copies stay through the build, and the game lets them go here when it is done (the end of
# world_things, and after a swap's terrain_reload). Queries answer the same before and after.
export function terrain_tiles_build_done(render3d_st: mut Render3dState) -> void {
if render3d_st.tt_file == null { return }
if render3d_st.ter_heights != null { free(render3d_st.ter_heights); render3d_st.ter_heights = null }
if render3d_st.ter_ortho_px != null { free(render3d_st.ter_ortho_px); render3d_st.ter_ortho_px = null }
render3d_st.tt_frame_reads = 0
tt_say_done()
}
function tt_say_done() -> void { print("r3d: terrain: the world is built; the whole height and photograph copies let go, tiles from here") }