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

@ -840,6 +840,11 @@ export state Render3dState {
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_tab: floats = null # per tile: its heights' minimum and step (terrain_ltt2_*.ludic)
tt_scratch: []byte = null # one tile's bytes as read
tt_cnoff: int = 0 # the coarse normals and photograph in the file
tt_cooff: int = 0
tt_baked: bool = false # the terrain came from a bake: nothing is generated or cut
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

View file

@ -29,8 +29,9 @@ import "terrain.ludic"
import "terrain_chunks.ludic"
import "terrain_tiles.ludic"
import "terrain_tiles_cut.ludic"
import "terrain_ltt2_write.ludic"
import "terrain_ltt2_read.ludic"
import "terrain_pages.ludic"
import "terrain_pages_coarse.ludic"
import "terrain_pages_frame.ludic"
import "terrain_pages_fill.ludic"
import "overlay.ludic"

View file

@ -411,8 +411,9 @@ function terrain_init__t(render3d_st: mut Render3dState) -> void {
# shadow, 2 the ground's materials, 3 the patch tree and the terrain programs.
const TERRAIN_INIT_STEPS: int = 4
function terrain_init_step(render3d_st: mut Render3dState, i: int) -> void {
if i == 0 { terrain_generate(render3d_st); return }
if i == 1 { terrain_bake_shadow(render3d_st); return }
if i == 0 { if not render3d_st.tt_baked { terrain_generate(render3d_st) }; return }
# a baked terrain whose shadow came from its own bake (terrain_shadow_from_bytes) keeps it
if i == 1 { if not (render3d_st.tt_baked and render3d_st.ter_shadow_tex != 0) { terrain_bake_shadow(render3d_st) }; return }
if i == 2 { terrain_load_textures(render3d_st); return }
terrain_init_finish(render3d_st)
}

View file

@ -0,0 +1,181 @@
# terrain_ltt2_read.ludic — the quantized tiles opened for reading (a bake's payload, or the dev cut's
# own file), a tile decoded into the pools as it is read, the coarse level made on the GPU from the
# file, and a boot that takes the terrain from a bake instead of making it.
# the tiles' payload at `base` in the file at `path` opened; false when it is not one this build reads
@alloc_ok("a map being loaded: its tiles' index and pools, once")
function tt_open_ltt2(render3d_st: mut Render3dState, path: string, base: int) -> bool {
tt_close(render3d_st)
let f = file_open(path, "rb")
if f == null { return false }
let head = words(32)
file_seek(f, base, 0)
let got = file_read(f, data_of(head), LT_HEAD)
if got != LT_HEAD or head[0] != LT_MAGIC or head[1] != LT_VERSION or head[2] != TT_TEX or head[5] != TERRAIN_RES or head[7] != TT_COARSE {
free(head)
file_close(f)
return false
}
let n = head[3]
let side = head[4]
render3d_st.tt_file = f
render3d_st.tt_n = n; render3d_st.tt_oside = side; render3d_st.ter_ortho_w = head[6]
render3d_st.tt_hoff = base + head[8] + n * n * 8
render3d_st.tt_noff = base + head[10]; render3d_st.tt_ooff = base + head[11]
render3d_st.tt_cnoff = base + head[13]; render3d_st.tt_cooff = base + head[14]
if render3d_st.tt_tab != null { free(render3d_st.tt_tab) }
render3d_st.tt_tab = floats(n * n * 2)
file_seek(f, base + head[8], 0)
file_read(f, data_of(render3d_st.tt_tab), n * n * 8)
if render3d_st.tt_coarse == null { render3d_st.tt_coarse = floats(TT_COARSE * TT_COARSE) }
file_seek(f, base + head[12], 0)
file_read(f, data_of(render3d_st.tt_coarse), TT_COARSE * TT_COARSE * 4)
free(head)
tt_pools(render3d_st, n, side)
return true
}
# the pools, the index, the clock and the read scratch, for n tiles a side
@alloc_ok("a map being loaded: its tiles' pools, once")
function tt_pools(render3d_st: mut Render3dState, n: int, side: int) -> void {
if render3d_st.tt_h == null { render3d_st.tt_h = floats(TT_SLOTS * TT_TEX * TT_TEX) }
if render3d_st.tt_nm == null { render3d_st.tt_nm = words(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)
}
let sc = max(TT_TEX * TT_TEX * 2, side * side * 3)
if render3d_st.tt_scratch == null or len(render3d_st.tt_scratch) < sc {
if render3d_st.tt_scratch != null { free(render3d_st.tt_scratch) }
render3d_st.tt_scratch = buffer(sc)
}
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
}
# tile t's heights, normals and photograph decoded into slot s: exactly the values ltt2_write put back
function tt_decode(render3d_st: mut Render3dState, t: int, s: int) -> void {
let f = render3d_st.tt_file
let sc = render3d_st.tt_scratch
let n2 = TT_TEX * TT_TEX
file_seek(f, render3d_st.tt_hoff + t * n2 * 2, 0)
file_read(f, data_of(sc), n2 * 2)
let lo = render3d_st.tt_tab[t * 2]
let step = render3d_st.tt_tab[t * 2 + 1]
for i in 0 .. n2 { render3d_st.tt_h[s * n2 + i] = lo + float(sc[i * 2] | (sc[i * 2 + 1] << 8)) * step }
file_seek(f, render3d_st.tt_noff + t * n2 * 2, 0)
file_read(f, data_of(sc), n2 * 2)
for i in 0 .. n2 {
let e = sc[i * 2] | (sc[i * 2 + 1] << 8)
render3d_st.tt_nm[s * n2 + i] = gvk_half(float_bits(lt_oct_dec(e, false))) | (gvk_half(float_bits(lt_oct_dec(e, true))) << 16)
}
let side = render3d_st.tt_oside
let o2 = side * side
file_seek(f, render3d_st.tt_ooff + t * o2 * 3, 0)
file_read(f, data_of(sc), o2 * 3)
for i in 0 .. o2 { render3d_st.tt_o[s * o2 + i] = (sc[i * 3] << 16) | (sc[i * 3 + 1] << 8) | sc[i * 3 + 2] }
}
# the GPU's coarse level from the file (the whole textures, if any, let go) and the page pool made
@alloc_ok("a map being loaded: the coarse level, once")
function ltt2_gpu_coarse(render3d_st: mut Render3dState) -> void {
let was = render3d_st.gvk_tag
render3d_st.gvk_tag = VKM_TERRAIN
let c = TT_COARSE
let f = render3d_st.tt_file
let h = tex_target(render3d_st, c, c, GL_R32F, GL_RED, GL_FLOAT, GL_LINEAR)
gpu_tex_fill(render3d_st, h, GL_R32F, c, c, GL_RED, GL_FLOAT, data_of(render3d_st.tt_coarse))
let raw = buffer(c * c * 3)
file_seek(f, render3d_st.tt_cnoff, 0)
file_read(f, data_of(raw), c * c * 2)
let xz = floats(c * c * 2)
for i in 0 .. c * c {
let e = raw[i * 2] | (raw[i * 2 + 1] << 8)
xz[i * 2] = lt_oct_dec(e, false); xz[i * 2 + 1] = lt_oct_dec(e, true)
}
let nm = tex_target(render3d_st, c, c, GL_RG16F, GL_RG, GL_FLOAT, GL_LINEAR)
gpu_tex_fill(render3d_st, nm, GL_RG16F, c, c, GL_RG, GL_FLOAT, data_of(xz))
free(xz)
file_seek(f, render3d_st.tt_cooff, 0)
file_read(f, data_of(raw), c * c * 3)
let o = tex_target(render3d_st, c, c, GL_SRGB8_ALPHA8, GL_RGBA, GL_UNSIGNED_BYTE, GL_LINEAR)
gpu_tex_fill(render3d_st, o, GL_SRGB8_ALPHA8, c, c, GL_RGB, GL_UNSIGNED_BYTE, data_of(raw))
free(raw)
gpu_tex_bind(render3d_st, GPU_TEX2D, o)
gpu_tex_param(render3d_st, GPU_TEX2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR)
gpu_tex_mips(render3d_st, GPU_TEX2D)
gpu_tex_paramf(render3d_st, GPU_TEX2D, GL_TEXTURE_MAX_ANISOTROPY_EXT, render3d_st.tex_anisotropy)
if render3d_st.ter_height_tex != 0 { gpu_tex_free(render3d_st, render3d_st.ter_height_tex) }
if render3d_st.ter_normal_tex != 0 { gpu_tex_free(render3d_st, render3d_st.ter_normal_tex) }
if render3d_st.ter_ortho_tex != 0 { gpu_tex_free(render3d_st, render3d_st.ter_ortho_tex) }
render3d_st.ter_height_tex = h; render3d_st.ter_normal_tex = nm; render3d_st.ter_ortho_tex = o
render3d_st.gvk_tag = was
tp_pool(render3d_st)
}
# ---- the bake -------------------------------------------------------------------------------
# The map as made (heights generated, photograph loaded, normals baked) written as a bake's file:
# ludic.base's LBAK header (render3d cannot import it; the same layout) and the tiles as its payload.
# The whole copy is left holding the quantized values, as everything will read them.
@alloc_ok("a bake: once, at build time")
export function terrain_tiles_bake(render3d_st: mut Render3dState, path: string, key: string, version: int, inputs_hash: long) -> bool {
if render3d_st.ter_heights == null or render3d_st.ter_ortho_px == null or render3d_st.ter_normal_tex == 0 { return false }
let f = file_open(path, "wb")
if f == null { return false }
let base = 32 + (len(key) + 7) / 8 * 8
let head = buffer(base)
for i in 0 .. base { head[i] = 0 }
file_write(f, data_of(head), base)
let n = ltt2_write(render3d_st, f, base)
let p = data_of(head)
Vk.put_i32(p, 0, BAKE_MAGIC); Vk.put_i32(p, 4, 1); Vk.put_i32(p, 8, version); Vk.put_i32(p, 12, base)
Vk.put_i64(p, 16, inputs_hash)
let nl: long = n
Vk.put_i64(p, 24, nl)
for i in 0 .. len(key) { head[32 + i] = key[i] }
file_seek(f, 0, 0)
file_write(f, data_of(head), base)
file_close(f)
free(head)
return true
}
# A boot that takes the terrain from a bake: the tiles opened, the coarse level made, nothing
# generated. Call it where the map was set up (in place of terrain_use_dem / terrain_use_ortho),
# before terrain_init; false (and nothing changed) when the bake is missing or not this version, and
# the caller makes the map as before.
@alloc_ok("a map being loaded from its bake, once")
export function terrain_from_baked(render3d_st: mut Render3dState, path: string, key: string, version: int, half: int, ox: float, oz: float) -> bool {
let base = ltt2_bake_base(path, key, version)
if base < 0 { return false }
render3d_st.TERRAIN_HALF = half
render3d_st.ter_ox = ox; render3d_st.ter_oz = oz
render3d_st.ter_h_scale = 0.0; render3d_st.ter_o_scale = 0.0
if not tt_open_ltt2(render3d_st, path, base) { return false }
render3d_st.tt_baked = true
tp_dummies(render3d_st)
ltt2_gpu_coarse(render3d_st)
return true
}
# where the payload starts in a bake's file when its key and version are these, else -1
@alloc_ok("a bake's header read once at load")
function ltt2_bake_base(path: string, key: string, version: int) -> int {
let f = file_open(path, "rb")
if f == null { return -1 }
let h = buffer(96)
let got = file_read(f, data_of(h), 96)
file_close(f)
let p = data_of(h)
var base = -1
if got >= 32 and Vk.get_i32(p, 0) == BAKE_MAGIC and Vk.get_i32(p, 4) == 1 and Vk.get_i32(p, 8) == version {
base = Vk.get_i32(p, 12)
for i in 0 .. len(key) { if 32 + i >= got or h[32 + i] != key[i] { base = -1 } }
}
free(h)
return base
}

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
}

View file

@ -1,30 +0,0 @@
# terrain_pages_coarse.ludic — the whole-map level the page pool falls back to (terrain_pages.ludic),
# made on the GPU from the whole textures at the cut, before they go.
# at the cut (terrain_tiles_cut): the coarse level made from the whole textures, which then go - the
# patch bounds and the sun's bake have already read them - and the pool made
function tp_cut(render3d_st: mut Render3dState) -> void {
let was = render3d_st.gvk_tag
render3d_st.gvk_tag = VKM_TERRAIN
tp_cut__t(render3d_st)
render3d_st.gvk_tag = was
}
function tp_cut__t(render3d_st: mut Render3dState) -> void {
if render3d_st.tt_file == null or render3d_st.tt_coarse == null or render3d_st.ter_ortho_tex == 0 { return }
let c = TT_COARSE
let h = tex_target(render3d_st, c, c, GL_R32F, GL_RED, GL_FLOAT, GL_LINEAR)
gpu_tex_fill(render3d_st, h, GL_R32F, c, c, GL_RED, GL_FLOAT, data_of(render3d_st.tt_coarse))
let nm = tex_target(render3d_st, c, c, GL_RG16F, GL_RG, GL_FLOAT, GL_LINEAR)
gpu_tex_blit(render3d_st, render3d_st.ter_normal_tex, TERRAIN_RES, TERRAIN_RES, nm, c, c)
let o = tex_target(render3d_st, c, c, GL_SRGB8_ALPHA8, GL_RGBA, GL_UNSIGNED_BYTE, GL_LINEAR)
gpu_tex_blit(render3d_st, render3d_st.ter_ortho_tex, render3d_st.ter_ortho_w, render3d_st.ter_ortho_w, o, c, c)
gpu_tex_bind(render3d_st, GPU_TEX2D, o)
gpu_tex_param(render3d_st, GPU_TEX2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR)
gpu_tex_mips(render3d_st, GPU_TEX2D)
gpu_tex_paramf(render3d_st, GPU_TEX2D, GL_TEXTURE_MAX_ANISOTROPY_EXT, render3d_st.tex_anisotropy)
gpu_tex_free(render3d_st, render3d_st.ter_height_tex)
gpu_tex_free(render3d_st, render3d_st.ter_normal_tex)
gpu_tex_free(render3d_st, render3d_st.ter_ortho_tex)
render3d_st.ter_height_tex = h; render3d_st.ter_normal_tex = nm; render3d_st.ter_ortho_tex = o
tp_pool(render3d_st)
}

View file

@ -64,16 +64,7 @@ function tt_read(render3d_st: mut Render3dState, t: int) -> int {
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)
tt_decode(render3d_st, t, s)
render3d_st.tt_tile_in[s] = t
render3d_st.tt_slot_of[t] = s
render3d_st.tt_ref[s] = 1

View file

@ -9,120 +9,20 @@ 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 }
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 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)
# 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)
free(head)
tt_open(render3d_st, path, n, side)
tt_make_coarse(render3d_st)
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) }
tp_cut(render3d_st)
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
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)