diff --git a/packages/ludic.render3d/env.ludic b/packages/ludic.render3d/env.ludic index a6188f2f..acddabc1 100644 --- a/packages/ludic.render3d/env.ludic +++ b/packages/ludic.render3d/env.ludic @@ -837,6 +837,18 @@ export state Render3dState { tb_version: int = 0 tb_shadow_ok: bool = false # the shadow came from its bake this map gltf_pure: int = 0 # a 1x1 white a factor multiplies exactly (gltf_factors) + gd_file: pointer = null # the map's ground densities (ground_density.ludic) + gd_base: int = 0 + gd_n: int = 0 + gd_layers: int = 0 + gd_ch: words = null + gd_off: words = null + gd_pool: []byte = null + gd_key: words = null + gd_ref: words = null + gd_slot_of: words = null + gd_hand: int = 0 + gd_trample_cb: fn(float, float) -> float = null 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) diff --git a/packages/ludic.render3d/ground_density.ludic b/packages/ludic.render3d/ground_density.ludic new file mode 100644 index 00000000..0672a2f9 --- /dev/null +++ b/packages/ludic.render3d/ground_density.ludic @@ -0,0 +1,121 @@ +# ground_density.ludic — a map's painted ground densities read a 64 m tile at a time (a seek into the bake's +# file, or a dev build's own copy of it) into a small clock cache: nothing of a layer is held whole. A texel +# is 2 m (GD_SIDE texels a 64 m tile side); a layer has R (the chance a cell keeps one) and maybe G (scale). + +const GD_SIDE: int = 32 +const GD_SLOTS: int = 256 # tiles held at once, 2 KB each at most + +# the densities of the bake at `path` (key, version), or of a dev build's copy written from the PNGs to +# `dev_path` when the bake is missing or stale; false when neither is there +@alloc_ok("a map being loaded: the densities' index and cache, once") +export function ground_density_open(render3d_st: mut Render3dState, path: string, key: string, version: int, pngs: []string, dev_path: string) -> bool { + ground_density_close(render3d_st) + var base = ltt2_bake_base(path, key, version) + var file = path + if base < 0 and len(pngs) > 0 and len(dev_path) > 0 { + gd_say_dev(path) + Fs.mkdir(Path.dir(dev_path)) + let f = file_open(dev_path, "wb") + if f == null { return false } + let n = gd_write_payload(render3d_st, f, 0, pngs, TERRAIN_RES / TT_TEX, GD_SIDE) + file_close(f) + if n < 0 { return false } + base = 0 + file = dev_path + } + if base < 0 { return false } + return gd_open_at(render3d_st, file, base) +} + +@alloc_ok("a map being loaded: the densities' index and cache, once") +function gd_open_at(render3d_st: mut Render3dState, path: string, base: int) -> bool { + let f = file_open(path, "rb") + if f == null { return false } + let head = words(GD_HEADW) + file_seek(f, base, 0) + let got = file_read(f, data_of(head), GD_HEADW * 4) + if got != GD_HEADW * 4 or head[0] != GD_MAGIC or head[1] != GD_VERSION or head[3] != GD_SIDE { + free(head) + file_close(f) + return false + } + let n = head[2] + let layers = head[4] + render3d_st.gd_file = f; render3d_st.gd_base = base; render3d_st.gd_n = n; render3d_st.gd_layers = layers + if render3d_st.gd_ch == null { render3d_st.gd_ch = words(GD_LAYERS); render3d_st.gd_off = words(GD_LAYERS) } + for l in 0 .. GD_LAYERS { render3d_st.gd_ch[l] = 0; render3d_st.gd_off[l] = 0 } + for l in 0 .. layers { render3d_st.gd_ch[l] = head[8 + l * 2]; render3d_st.gd_off[l] = head[9 + l * 2] } + free(head) + gd_cache_reset(render3d_st, layers * n * n) + return true +} + +# the cache made (once) and emptied, and the index of where each layer's tile is: -1 where it is not in +@alloc_ok("a map being loaded: the densities' cache and index, once") +function gd_cache_reset(render3d_st: mut Render3dState, tiles: int) -> void { + if render3d_st.gd_pool == null { + render3d_st.gd_pool = buffer(GD_SLOTS * GD_SIDE * GD_SIDE * 2) + render3d_st.gd_key = words(GD_SLOTS); render3d_st.gd_ref = words(GD_SLOTS) + } + if render3d_st.gd_slot_of == null or len(render3d_st.gd_slot_of) < tiles { + if render3d_st.gd_slot_of != null { free(render3d_st.gd_slot_of) } + render3d_st.gd_slot_of = words(max(tiles, 1)) + } + for t in 0 .. len(render3d_st.gd_slot_of) { render3d_st.gd_slot_of[t] = -1 } + for s in 0 .. GD_SLOTS { render3d_st.gd_key[s] = -1; render3d_st.gd_ref[s] = 0 } + render3d_st.gd_hand = 0 +} + +export function ground_density_close(render3d_st: mut Render3dState) -> void { + if render3d_st.gd_file != null { file_close(render3d_st.gd_file); render3d_st.gd_file = null } +} + +# the slot holding layer l's tile t, read in when it is not (a clock, as the terrain's tiles) +function gd_slot(render3d_st: mut Render3dState, l: int, t: int) -> int { + let key = l * render3d_st.gd_n * render3d_st.gd_n + t + let have = render3d_st.gd_slot_of[key] + if have >= 0 { + render3d_st.gd_ref[have] = 1 + return have + } + var s = render3d_st.gd_hand + while render3d_st.gd_ref[s] != 0 { + render3d_st.gd_ref[s] = 0 + s = (s + 1) % GD_SLOTS + } + render3d_st.gd_hand = (s + 1) % GD_SLOTS + if render3d_st.gd_key[s] >= 0 { render3d_st.gd_slot_of[render3d_st.gd_key[s]] = -1 } + let tb = GD_SIDE * GD_SIDE * render3d_st.gd_ch[l] + file_seek(render3d_st.gd_file, render3d_st.gd_base + render3d_st.gd_off[l] + t * tb, 0) + file_read(render3d_st.gd_file, mem_off(data_of(render3d_st.gd_pool), s * GD_SIDE * GD_SIDE * 2), tb) + render3d_st.gd_key[s] = key + render3d_st.gd_slot_of[key] = s + render3d_st.gd_ref[s] = 1 + return s +} + +# layer l's texel (tx, tz) (clamped to the map), channel c (0 density, 1 scale), 0..255 +function gd_texel(render3d_st: mut Render3dState, l: int, tx: int, tz: int, c: int) -> int { + let w = render3d_st.gd_n * GD_SIDE + let x = min(max(tx, 0), w - 1) + let z = min(max(tz, 0), w - 1) + let ch = render3d_st.gd_ch[l] + if c >= ch { return 128 } + let s = gd_slot(render3d_st, l, (z / GD_SIDE) * render3d_st.gd_n + x / GD_SIDE) + return render3d_st.gd_pool[s * GD_SIDE * GD_SIDE * 2 + ((z % GD_SIDE) * GD_SIDE + x % GD_SIDE) * ch + c] +} + +# layer l's channel c at world (x, z), bilinear over texel centres, 0..1 +export function ground_density_at(render3d_st: mut Render3dState, l: int, x: float, z: float, c: int) -> float { + if render3d_st.gd_file == null or l < 0 or l >= render3d_st.gd_layers or render3d_st.gd_ch[l] == 0 { return 0.0 } + let texel = 2.0 * float(render3d_st.TERRAIN_HALF) / float(render3d_st.gd_n * GD_SIDE) + let fx = (x - render3d_st.ter_ox + float(render3d_st.TERRAIN_HALF)) / texel - 0.5 + let fz = (z - render3d_st.ter_oz + float(render3d_st.TERRAIN_HALF)) / texel - 0.5 + let ix = int(Math.floor(fx)); let iz = int(Math.floor(fz)) + let ax = fx - float(ix); let az = fz - float(iz) + let a = Math.lerp(float(gd_texel(render3d_st, l, ix, iz, c)), float(gd_texel(render3d_st, l, ix + 1, iz, c)), ax) + let b = Math.lerp(float(gd_texel(render3d_st, l, ix, iz + 1, c)), float(gd_texel(render3d_st, l, ix + 1, iz + 1, c)), ax) + return Math.lerp(a, b, az) / 255.0 +} + +function gd_say_dev(path: string) -> void { print(`bake: {path} missing or stale - the ground densities cut from their PNGs at run time (ludic bake)`) } diff --git a/packages/ludic.render3d/ground_density_write.ludic b/packages/ludic.render3d/ground_density_write.ludic new file mode 100644 index 00000000..b6a490e8 --- /dev/null +++ b/packages/ludic.render3d/ground_density_write.ludic @@ -0,0 +1,82 @@ +# ground_density_write.ludic — a map's painted ground densities (one PNG per ground layer: R the chance a cell +# keeps one, G an optional scale) cut into the terrain's 64 m tiles and written as one file (LGD1), the +# build-time bake's payload or a dev build's own copy. One layer is decoded, cut and let go at a time. + +const GD_MAGIC: int = 826557260 # "LGD1" read as a little-endian u32: 0x3144474C +const GD_VERSION: int = 1 +const GD_HEADW: int = 136 # words: magic, version, n, side, layers, 3 spare, then (channels, offset) x 64 +const GD_LAYERS: int = 64 + +# the layers' PNGs (paths, in the rows' order) written to f from `base`; the payload's length, or -1. +# n tiles a side of `side` texels each (the PNGs must be n * side square) +@alloc_ok("a bake, or a dev build's map load: the densities cut once") +function gd_write_payload(render3d_st: mut Render3dState, f: pointer, base: int, pngs: []string, n: int, side: int) -> int { + let layers = min(len(pngs), GD_LAYERS) + let head = words(GD_HEADW) + for k in 0 .. GD_HEADW { head[k] = 0 } + let hbytes = GD_HEADW * 4 + file_write(f, data_of(head), hbytes) + var at = hbytes + let tile = buffer(side * side * 2) + for l in 0 .. layers { + let px = png_decode_bytes(render3d_st, pngs[l], null) + if px == null or render3d_st.tex_w != n * side or render3d_st.tex_h != n * side { gd_say_bad(pngs[l], n * side); return -1 } + let c = render3d_st.tex_channels + var ch = 1 + if c >= 2 { ch = 2 } + head[8 + l * 2] = ch; head[9 + l * 2] = at + at += gd_write_layer(f, px, c, ch, n, side, tile) + free(px) + } + free(tile) + head[0] = GD_MAGIC; head[1] = GD_VERSION; head[2] = n; head[3] = side; head[4] = layers + file_seek(f, base, 0) + file_write(f, data_of(head), hbytes) + file_seek(f, base + at, 0) + free(head) + return at +} + +# one layer's tiles, tile after tile; its bytes +function gd_write_layer(f: pointer, px: []byte, c: int, ch: int, n: int, side: int, tile: []byte) -> int { + let w = n * side + 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 + tile[(r * side + k) * ch] = px[o] + if ch == 2 { tile[(r * side + k) * 2 + 1] = px[o + 1] } + } + } + file_write(f, data_of(tile), side * side * ch) + } + } + return n * n * side * side * ch +} + +# The bake: the densities as a bake's file (ludic.base's LBAK header, as terrain_tiles_bake writes it) +@alloc_ok("a bake: once, at build time") +export function ground_density_bake(render3d_st: mut Render3dState, path: string, key: string, version: int, inputs_hash: long, pngs: []string) -> bool { + Fs.mkdir(Path.dir(path)) + let f = file_open(path, "wb") + if f == null { return false } + let base = 32 + (len(key) + 1 + 7) / 8 * 8 + let head = buffer(base) + for i in 0 .. base { head[i] = 0 } + file_write(f, data_of(head), base) + let n = gd_write_payload(render3d_st, f, base, pngs, TERRAIN_RES / TT_TEX, GD_SIDE) + 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 = max(n, 0) + 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 n >= 0 +} + +function gd_say_bad(path: string, want: int) -> void { print(`r3d: ground: {path} is missing or not {want} x {want}; the densities are not written`) } diff --git a/packages/ludic.render3d/ground_fill.ludic b/packages/ludic.render3d/ground_fill.ludic new file mode 100644 index 00000000..3bc9614a --- /dev/null +++ b/packages/ludic.render3d/ground_fill.ludic @@ -0,0 +1,64 @@ +# ground_fill.ludic — a ground layer's instances in one chunk of its stream, placed from its painted density +# and nothing else: a candidate in each cell of the band's step, moved by the jitter, kept when a hash of +# (layer, chunk, band, cell) falls under the density there (times the game's trample, where it asks). The +# same numbers on every machine and in the editor's preview; the camera decides only which chunks. + +# a ground layer's numbers, as the game's row (GroundLayers) gives them; `layer` is its index in the densities +export property GroundFill { + layer: int = 0, + step0: float = 1.4, + step1: float = 1.4, + step2: float = 1.4, + step3: float = 1.4, + jitter: float = 1.0, + scale_lo: float = 1.0, + scale_hi: float = 1.0, + wind_lo: float = 0.6, + wind_hi: float = 1.0, + sink: float = 0.03, + trample: bool = true +} + +# how much of a candidate at (x, z) the game lets stand (the camp, the town): 0..1, asked only when the row says +export function r3d_on_ground_trample(render3d_st: mut Render3dState, f: fn(float, float) -> float) -> void { render3d_st.gd_trample_cb = f } + +function gf_step(g: GroundFill, band: int) -> float { + if band == 0 { return g.step0 } + if band == 1 { return g.step1 } + if band == 2 { return g.step2 } + return g.step3 +} + +# a hash of the candidate and one of its draws, 0..1 (a 32-bit mix: the same on every machine) +function gf_hash(a: int, b: int, c: int, d: int, e: int, k: int) -> float { + var h = (a * 73856093) ^ (b * 19349663) ^ (c * 83492791) ^ (d * 2654435761) ^ (e * 1597334677) ^ (k * 3812015801) + h = h & 0xFFFFFFFF + h = ((h ^ (h >> 16)) * 0x7feb352d) & 0xFFFFFFFF + h = ((h ^ (h >> 15)) * 0x846ca68b) & 0xFFFFFFFF + h = h ^ (h >> 16) + return float(h & 0xFFFFFF) / 16777216.0 +} + +# chunk (cx, cz) of stream s at `band`, from layer g (call it inside the game's r3d_stream_fill) +export function ground_fill(render3d_st: mut Render3dState, s: Stream, cx: int, cz: int, band: int, g: GroundFill) -> void { + let step = gf_step(g, band) + if step <= 0.0 or render3d_st.gd_file == null { return } + let x0 = s.ox + float(cx) * s.size + let z0 = s.oz + float(cz) * s.size + let cells = int(Math.ceil(s.size / step)) + for j in 0 .. cells { + for i in 0 .. cells { + let px = x0 + (float(i) + 0.5 + (gf_hash(g.layer, cx, cz, band, i * 65536 + j, 0) - 0.5) * g.jitter) * step + let pz = z0 + (float(j) + 0.5 + (gf_hash(g.layer, cx, cz, band, i * 65536 + j, 1) - 0.5) * g.jitter) * step + if px < x0 or px >= x0 + s.size or pz < z0 or pz >= z0 + s.size { continue } + var keep = ground_density_at(render3d_st, g.layer, px, pz, 0) + if keep <= 0.0 { continue } + if g.trample and render3d_st.gd_trample_cb != null { keep = keep * render3d_st.gd_trample_cb(px, pz) } + let e = i * 65536 + j + if gf_hash(g.layer, cx, cz, band, e, 2) >= keep { continue } + let sc = Math.lerp(g.scale_lo, g.scale_hi, ground_density_at(render3d_st, g.layer, px, pz, 1)) + let h = terrain_height(render3d_st, px, pz) - g.sink + stream_emit(render3d_st, s, px, h, pz, sc, gf_hash(g.layer, cx, cz, band, e, 3) * 2.0 * PI, gf_hash(g.layer, cx, cz, band, e, 4), Math.lerp(g.wind_lo, g.wind_hi, gf_hash(g.layer, cx, cz, band, e, 5))) + } + } +} diff --git a/packages/ludic.render3d/r3d.ludic b/packages/ludic.render3d/r3d.ludic index cf69746e..ea502e62 100644 --- a/packages/ludic.render3d/r3d.ludic +++ b/packages/ludic.render3d/r3d.ludic @@ -52,6 +52,9 @@ import "actor.ludic" import "stream.ludic" import "place_rec.ludic" import "stream_baked.ludic" +import "ground_density_write.ludic" +import "ground_density.ludic" +import "ground_fill.ludic" import "fog_streams.ludic" import "grass.ludic" import "grass_gpu.ludic"