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>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-29 17:23:53 +03:00
parent 883ea8d48a
commit a12f1b1201
8 changed files with 397 additions and 149 deletions

View file

@ -0,0 +1,199 @@
# terrain_ltt2_write.ludic — the terrain as THE quantized tiles every reader answers from (plan 26 of
# maroon-lake): heights as u16 over each tile's own minimum and step, normals octahedral 8 + 8, the
# photograph RGB8, and the coarse level. Written by the build-time bake and, in a dev build with no bake,
# by the cut; either way the whole copy is put back to the quantized values, so the build's own
# queries, the physics, the placements' bake and every machine read the same numbers.
const LT_MAGIC: int = 844383308 # "LTT2"
const LT_VERSION: int = 1
const LT_HEAD: int = 128 # 32 words
# the payload at the file's current place (its start is `base`); its length. Header written last.
@alloc_ok("a map being made or baked: the tiles written once")
function ltt2_write(render3d_st: mut Render3dState, f: pointer, base: int) -> int {
let n = TERRAIN_RES / TT_TEX
let side = render3d_st.ter_ortho_w / n
let head = words(32)
for k in 0 .. 32 { head[k] = 0 }
file_write(f, data_of(head), LT_HEAD)
var at = LT_HEAD
head[8] = at; at += ltt2_write_heights(render3d_st, f, n)
let nrm = ltt2_normals_back(render3d_st)
head[10] = at; at += ltt2_write_normals(f, nrm, n)
head[11] = at; at += ltt2_write_ortho(render3d_st, f, n, side)
head[12] = at; at += ltt2_write_coarse_h(render3d_st, f)
head[13] = at; at += ltt2_write_coarse_n(f, nrm)
free(nrm)
head[14] = at; at += ltt2_write_coarse_o(render3d_st, f)
head[0] = LT_MAGIC; head[1] = LT_VERSION; head[2] = TT_TEX; head[3] = n; head[4] = side
head[5] = TERRAIN_RES; head[6] = render3d_st.ter_ortho_w; head[7] = TT_COARSE; head[15] = at
file_seek(f, base, 0)
file_write(f, data_of(head), LT_HEAD)
file_seek(f, base + at, 0)
free(head)
return at
}
# each tile's (min, step) table, then its heights as u16; the whole copy put back to what they decode to
@alloc_ok("a map being made or baked: one tile's scratch and the table, freed after")
function ltt2_write_heights(render3d_st: mut Render3dState, f: pointer, n: int) -> int {
let tab = floats(n * n * 2)
let h = render3d_st.ter_heights
for j in 0 .. n {
for i in 0 .. n {
var lo = 1000000.0; var hi = -1000000.0
for r in 0 .. TT_TEX { for k in 0 .. TT_TEX { let v = h[(j * TT_TEX + r) * TERRAIN_RES + i * TT_TEX + k]; lo = Math.min(lo, v); hi = Math.max(hi, v) } }
tab[(j * n + i) * 2] = lo; tab[(j * n + i) * 2 + 1] = (hi - lo) / 65535.0
}
}
file_write(f, data_of(tab), n * n * 8)
let b = buffer(TT_TEX * TT_TEX * 2)
for j in 0 .. n {
for i in 0 .. n {
let lo = tab[(j * n + i) * 2]; let step = tab[(j * n + i) * 2 + 1]
for r in 0 .. TT_TEX {
for k in 0 .. TT_TEX {
let at = (j * TT_TEX + r) * TERRAIN_RES + i * TT_TEX + k
var q = 0
if step > 0.0 { q = min(max(int(Math.floor((h[at] - lo) / step + 0.5)), 0), 65535) }
h[at] = lo + float(q) * step
b[(r * TT_TEX + k) * 2] = q & 255; b[(r * TT_TEX + k) * 2 + 1] = q >> 8
}
}
file_write(f, data_of(b), TT_TEX * TT_TEX * 2)
}
}
free(b)
free(tab)
return n * n * 8 + n * n * TT_TEX * TT_TEX * 2
}
# the baked normals read back once (RG16F: two halves a word)
@alloc_ok("a map being made or baked: the normals read back once, freed by the caller")
function ltt2_normals_back(render3d_st: mut Render3dState) -> words {
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))
return all
}
@alloc_ok("a map being made or baked: one tile's scratch, freed after")
function ltt2_write_normals(f: pointer, nrm: words, n: int) -> int {
let b = buffer(TT_TEX * TT_TEX * 2)
for j in 0 .. n {
for i in 0 .. n {
for r in 0 .. TT_TEX {
for k in 0 .. TT_TEX {
let w = nrm[(j * TT_TEX + r) * TERRAIN_RES + i * TT_TEX + k]
let e = lt_oct_enc(lt_unhalf(w & 0xFFFF), lt_unhalf((w >> 16) & 0xFFFF))
b[(r * TT_TEX + k) * 2] = e & 255; b[(r * TT_TEX + k) * 2 + 1] = e >> 8
}
}
file_write(f, data_of(b), TT_TEX * TT_TEX * 2)
}
}
free(b)
return n * n * TT_TEX * TT_TEX * 2
}
@alloc_ok("a map being made or baked: one tile's scratch, freed after")
function ltt2_write_ortho(render3d_st: Render3dState, f: pointer, n: int, side: int) -> int {
let b = buffer(side * side * 3)
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 d = (r * side + k) * 3
b[d] = px[o]; b[d + 1] = px[o + 1]; b[d + 2] = px[o + 2]
}
}
file_write(f, data_of(b), side * side * 3)
}
}
free(b)
return n * n * side * side * 3
}
# the coarse heights: the mean of each 2 x 2 of the quantized ones (kept in tt_coarse too)
function ltt2_write_coarse_h(render3d_st: mut Render3dState, f: pointer) -> int {
tt_make_coarse(render3d_st)
file_write(f, data_of(render3d_st.tt_coarse), TT_COARSE * TT_COARSE * 4)
return TT_COARSE * TT_COARSE * 4
}
# the coarse normals: each 2 x 2's mean direction, octahedral
@alloc_ok("a map being made or baked: one row's scratch, freed after")
function ltt2_write_coarse_n(f: pointer, nrm: words) -> int {
let k = TERRAIN_RES / TT_COARSE
let b = buffer(TT_COARSE * 2)
for j in 0 .. TT_COARSE {
for i in 0 .. TT_COARSE {
var sx = 0.0; var sz = 0.0
for bb in 0 .. k { for a in 0 .. k { let w = nrm[(j * k + bb) * TERRAIN_RES + i * k + a]; sx = sx + lt_unhalf(w & 0xFFFF); sz = sz + lt_unhalf((w >> 16) & 0xFFFF) } }
let e = lt_oct_enc(sx / float(k * k), sz / float(k * k))
b[i * 2] = e & 255; b[i * 2 + 1] = e >> 8
}
file_write(f, data_of(b), TT_COARSE * 2)
}
free(b)
return TT_COARSE * TT_COARSE * 2
}
# the coarse photograph: each block's mean colour, RGB8
@alloc_ok("a map being made or baked: one row's scratch, freed after")
function ltt2_write_coarse_o(render3d_st: Render3dState, f: pointer) -> int {
let w = render3d_st.ter_ortho_w
let k = max(w / TT_COARSE, 1)
let c = render3d_st.ter_ortho_c
let px = render3d_st.ter_ortho_px
let b = buffer(TT_COARSE * 3)
for j in 0 .. TT_COARSE {
for i in 0 .. TT_COARSE {
for ch in 0 .. 3 {
var s = 0
for bb in 0 .. k { for a in 0 .. k { s += px[((min(j * k + bb, w - 1)) * w + min(i * k + a, w - 1)) * c + ch] } }
b[i * 3 + ch] = (s + k * k / 2) / (k * k)
}
}
file_write(f, data_of(b), TT_COARSE * 3)
}
free(b)
return TT_COARSE * TT_COARSE * 3
}
# a normal's x and z (y up, rebuilt) as octahedral 8 + 8 bits, and back
function lt_oct_enc(x: float, z: float) -> int {
let y = Math.sqrt(Math.max(1.0 - x * x - z * z, 0.0))
let s = Math.abs(x) + y + Math.abs(z)
var px = 0.0; var pz = 0.0
if s > 0.0 { px = x / s; pz = z / s }
let qx = min(max(int(Math.floor((px * 0.5 + 0.5) * 255.0 + 0.5)), 0), 255)
let qz = min(max(int(Math.floor((pz * 0.5 + 0.5) * 255.0 + 0.5)), 0), 255)
return qx | (qz << 8)
}
# the decoded normal's x (want_z false) or z
function lt_oct_dec(e: int, want_z: bool) -> float {
let px = float(e & 255) / 255.0 * 2.0 - 1.0
let pz = float((e >> 8) & 255) / 255.0 * 2.0 - 1.0
let y = Math.max(1.0 - Math.abs(px) - Math.abs(pz), 0.0)
let l = Math.sqrt(px * px + y * y + pz * pz)
if l <= 0.0 { return 0.0 }
if want_z { return pz / l }
return px / l
}
# an IEEE half's bits as a float
function lt_unhalf(h: int) -> float {
let s = (h >> 15) & 1
let e = (h >> 10) & 31
let m = h & 1023
var v = 0.0
if e == 0 { v = float(m) / 16777216.0 } else { v = float_from_bits(((e - 15 + 127) << 23) | (m << 13)) }
if s == 1 { return -v }
return v
}