Merge commit 'c672be1' into lang/foundations
This commit is contained in:
commit
d63de113fb
5 changed files with 282 additions and 0 deletions
|
|
@ -844,6 +844,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)
|
||||
|
|
|
|||
121
packages/ludic.render3d/ground_density.ludic
Normal file
121
packages/ludic.render3d/ground_density.ludic
Normal file
|
|
@ -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)`) }
|
||||
82
packages/ludic.render3d/ground_density_write.ludic
Normal file
82
packages/ludic.render3d/ground_density_write.ludic
Normal file
|
|
@ -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`) }
|
||||
64
packages/ludic.render3d/ground_fill.ludic
Normal file
64
packages/ludic.render3d/ground_fill.ludic
Normal file
|
|
@ -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)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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_kind.ludic"
|
||||
import "grass_bind.ludic"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue