From a12f1b1201ae38b7446d37a283c64714ab21f89a Mon Sep 17 00:00:00 2001 From: Orkuncakilkaya Date: Tue, 29 Sep 2026 17:23:53 +0300 Subject: [PATCH] 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 /.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 --- packages/ludic.render3d/env.ludic | 5 + packages/ludic.render3d/r3d.ludic | 3 +- packages/ludic.render3d/terrain.ludic | 5 +- .../ludic.render3d/terrain_ltt2_read.ludic | 181 ++++++++++++++++ .../ludic.render3d/terrain_ltt2_write.ludic | 199 ++++++++++++++++++ .../ludic.render3d/terrain_pages_coarse.ludic | 30 --- packages/ludic.render3d/terrain_tiles.ludic | 11 +- .../ludic.render3d/terrain_tiles_cut.ludic | 112 +--------- 8 files changed, 397 insertions(+), 149 deletions(-) create mode 100644 packages/ludic.render3d/terrain_ltt2_read.ludic create mode 100644 packages/ludic.render3d/terrain_ltt2_write.ludic delete mode 100644 packages/ludic.render3d/terrain_pages_coarse.ludic diff --git a/packages/ludic.render3d/env.ludic b/packages/ludic.render3d/env.ludic index 7d4ed7c8..4d4dde90 100644 --- a/packages/ludic.render3d/env.ludic +++ b/packages/ludic.render3d/env.ludic @@ -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 diff --git a/packages/ludic.render3d/r3d.ludic b/packages/ludic.render3d/r3d.ludic index 0c97e623..19ddcab7 100644 --- a/packages/ludic.render3d/r3d.ludic +++ b/packages/ludic.render3d/r3d.ludic @@ -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" diff --git a/packages/ludic.render3d/terrain.ludic b/packages/ludic.render3d/terrain.ludic index dd509379..264ea1ad 100644 --- a/packages/ludic.render3d/terrain.ludic +++ b/packages/ludic.render3d/terrain.ludic @@ -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) } diff --git a/packages/ludic.render3d/terrain_ltt2_read.ludic b/packages/ludic.render3d/terrain_ltt2_read.ludic new file mode 100644 index 00000000..131074ac --- /dev/null +++ b/packages/ludic.render3d/terrain_ltt2_read.ludic @@ -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 +} diff --git a/packages/ludic.render3d/terrain_ltt2_write.ludic b/packages/ludic.render3d/terrain_ltt2_write.ludic new file mode 100644 index 00000000..da418855 --- /dev/null +++ b/packages/ludic.render3d/terrain_ltt2_write.ludic @@ -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 +} diff --git a/packages/ludic.render3d/terrain_pages_coarse.ludic b/packages/ludic.render3d/terrain_pages_coarse.ludic deleted file mode 100644 index 4f084272..00000000 --- a/packages/ludic.render3d/terrain_pages_coarse.ludic +++ /dev/null @@ -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) -} diff --git a/packages/ludic.render3d/terrain_tiles.ludic b/packages/ludic.render3d/terrain_tiles.ludic index dc65f521..04804ff0 100644 --- a/packages/ludic.render3d/terrain_tiles.ludic +++ b/packages/ludic.render3d/terrain_tiles.ludic @@ -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 diff --git a/packages/ludic.render3d/terrain_tiles_cut.ludic b/packages/ludic.render3d/terrain_tiles_cut.ludic index 89c6f8df..36899fbe 100644 --- a/packages/ludic.render3d/terrain_tiles_cut.ludic +++ b/packages/ludic.render3d/terrain_tiles_cut.ludic @@ -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)