Merge r3d/painted-zones: ground_fill's candidate as a pure function, solid layers with stable ids, clearings as data, and a conformance golden the studio shares
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
commit
176dc79b49
16 changed files with 804 additions and 19 deletions
13
changes/ground-painted-zones.md
Normal file
13
changes/ground-painted-zones.md
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
bump: minor
|
||||
type: feat
|
||||
**`ludic.render3d`: painted ground layers grow solid things, with ids, and the trample is data.**
|
||||
`ground_fill`'s candidate is its own function, `ground_candidate` (pure `gf_*` steps with the density read
|
||||
between them, into a caller-held `GroundCand`), and `ground_fill` draws exactly its answers - the same
|
||||
operations in the same order as before, so every cover layer grows bit for bit what it did. A layer with
|
||||
`solid: true` is filled at `step0` with band 0's hashes whatever the camera, a far band drawing a stable
|
||||
subset; each thing has an int id from (layer, chunk, cell) (`ground_solid_id`), and `ground_solid_list` /
|
||||
`ground_solid_at` answer a chunk's things or one by id for physics, the nav bake and saves.
|
||||
`r3d_ground_clearing(x, z, r_in, r_out, floor)` hands the trample over as discs the editor can see, beside
|
||||
the `r3d_on_ground_trample` callback, which still works. `tests/ground_fill_test.ludic` holds all of it to
|
||||
`tests/ground_fill_golden.json` (written by `tests/gen/ground_fill_golden.ludic`), the file the studio's
|
||||
TypeScript generator is tested against.
|
||||
35
packages/ludic.render3d/README.md
Normal file
35
packages/ludic.render3d/README.md
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
# ludic.render3d
|
||||
|
||||
A physically based 3D renderer over Vulkan. `FEATURES.md` lists what is implemented and where.
|
||||
|
||||
## Painted ground layers
|
||||
|
||||
A ground layer is a density painted over the map (R the chance a cell keeps one, G its scale, 2 m texels
|
||||
baked into an LGD2 file, `ground_density.ludic`) and a `GroundFill` row of numbers. Everything it grows is
|
||||
a pure function of those two - the same on every machine and in the editor's preview.
|
||||
|
||||
- **The candidate** (`ground_cand.ludic`): for (layer, chunk, band, cell) `ground_candidate` answers
|
||||
whether one stands there and its x, z, scale, yaw, seed and wind, into a `GroundCand` the caller holds.
|
||||
Its steps are pure: `gf_along` (the jitter, draws 0 and 1), `gf_keeps` (draw 2 under the chance),
|
||||
`gf_scale` (G and draw 6), `gf_yaw` (3), `gf_seed` (4), `gf_wind` (5). `ground_fill` draws exactly
|
||||
these. Numbers are IEEE single precision, `Math.lerp` is `a + (b - a) * t`, `gf_hash` is 32-bit
|
||||
integer arithmetic with a logical `>>`, and the model shader turns local (x, z) to
|
||||
`(c x + s z, -s x + c z)` - three.js's rotation about +Y.
|
||||
- **Cover and solid.** A cover layer (grass, flowers, pebbles) is filled at its band's step, so a far
|
||||
band is a different, sparser sample. A layer with `solid: true` (rocks, trees) is filled at `step0`
|
||||
with band 0's hashes whatever the band, and a far band draws the subset whose draw 7 falls under
|
||||
`(step0 / step_band)^2`; its scale ignores `band_grow`. Nothing solid moves with the camera.
|
||||
- **Solid ids** (`ground_solid.ludic`): `ground_solid_id(layer, cx, cz, i, j, cells)` =
|
||||
`((layer * 256 + cz + 128) * 256 + cx + 128) * 512 + i * cells + j`, a positive 31-bit int, stable
|
||||
while the painting under it is. It holds chunks -128 .. 127 about the stream's corner and at most 22
|
||||
cells a chunk side (a step of at least 32 / 22 m on 32 m chunks); a cell past that has no id and is
|
||||
not grown. `ground_solid_list` fills a caller's `GroundSolids` (made once, `ground_solids_new`) with a
|
||||
chunk's things for physics, the nav bake or a save; `ground_solid_at` answers one by id.
|
||||
- **The trample as data** (`ground_clear.ludic`): `r3d_ground_clearing(x, z, r_in, r_out, floor)` hands
|
||||
over a clearing - `floor` inside `r_in`, back to full at `r_out` on a smoothstep, the shape of the
|
||||
camp's ring and the town's yard. A layer with `trample` multiplies its chance by the game's callback
|
||||
(`r3d_on_ground_trample`, kept for now) and then by every clearing. A game should use one or the other.
|
||||
- **Conformance**: `tests/ground_fill_test.ludic` holds the package to `tests/ground_fill_golden.json`
|
||||
(gf_hash, lerp, yaw, one cover chunk and one solid chunk over a hand-written density), which the
|
||||
studio's own test reads. After a deliberate change, rebuild it from the repository root:
|
||||
`bin/ludic run packages/ludic.render3d/tests/gen/ground_fill_golden.ludic --headless`, and commit it.
|
||||
|
|
@ -860,6 +860,9 @@ export state Render3dState {
|
|||
gd_zbuf: []byte = null # one tile's deflated bytes as read
|
||||
gd_hand: int = 0
|
||||
gd_trample_cb: fn(float, float) -> float = null
|
||||
gd_clear: floats = floats(GD_CLEAR_MAX * 5) # the clearings as data: (x, z, r_in, r_out, floor) each (ground_clear.ludic)
|
||||
gd_clear_n: int = 0
|
||||
gd_cand: GroundCand = new GroundCand # ground_fill's candidate, filled and reused
|
||||
gb_layer: int = -1 # the densities' layer the blades grow by; -1: the rules (grass_density.ludic)
|
||||
gb_buf: int = 0 # its window round the camera, on the GPU
|
||||
gb_win: words = null # ... and the window's bytes, four texels a word, made once
|
||||
|
|
|
|||
68
packages/ludic.render3d/ground_cand.ludic
Normal file
68
packages/ludic.render3d/ground_cand.ludic
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
# ground_cand.ludic — one candidate of a ground layer: (layer, chunk, band, cell) to where it stands and what it
|
||||
# draws with. The gf_* steps are pure (numbers in, a number out); ground_candidate reads the densities between
|
||||
# them, and nothing but the densities' tile cache changes. The studio's groundFill.ts is these steps.
|
||||
|
||||
# a candidate, filled by the caller's own record (made once, reused)
|
||||
export property GroundCand {
|
||||
x: float = 0.0,
|
||||
z: float = 0.0,
|
||||
scale: float = 1.0,
|
||||
yaw: float = 0.0,
|
||||
seed: float = 0.0,
|
||||
wind: float = 0.0,
|
||||
cell: int = 0, # i * 65536 + j: the hashes' fifth input
|
||||
id: int = -1 # a solid layer's stable id (ground_solid.ludic); -1 for cover
|
||||
}
|
||||
|
||||
export function gf_cell(i: int, j: int) -> int { return i * 65536 + j }
|
||||
|
||||
# a cell's corner o, moved by the jitter's draw h (k 0 for x, 1 for z)
|
||||
export function gf_along(o: float, i: int, h: float, jitter: float, step: float) -> float { return o + (float(i) + 0.5 + (h - 0.5) * jitter) * step }
|
||||
|
||||
# kept when draw 2 falls under the chance (the density, times the trample)
|
||||
export function gf_keeps(g: GroundFill, cx: int, cz: int, band: int, e: int, keep: float) -> bool { return not (gf_hash(g.layer, cx, cz, band, e, 2) >= keep) }
|
||||
|
||||
# its size from the density's G at its place, and its own draw 6
|
||||
export function gf_scale(g: GroundFill, cx: int, cz: int, band: int, e: int, gs: float) -> float {
|
||||
var sc = Math.lerp(g.scale_lo, g.scale_hi, gs)
|
||||
sc = sc * (1.0 + (gf_hash(g.layer, cx, cz, band, e, 6) * 2.0 - 1.0) * g.scale_var) * (1.0 + g.band_grow * float(band))
|
||||
return sc
|
||||
}
|
||||
|
||||
export function gf_yaw(g: GroundFill, cx: int, cz: int, band: int, e: int) -> float { return gf_hash(g.layer, cx, cz, band, e, 3) * 2.0 * PI }
|
||||
|
||||
export function gf_seed(g: GroundFill, cx: int, cz: int, band: int, e: int) -> float { return gf_hash(g.layer, cx, cz, band, e, 4) }
|
||||
|
||||
export function gf_wind(g: GroundFill, cx: int, cz: int, band: int, e: int) -> float { return Math.lerp(g.wind_lo, g.wind_hi, gf_hash(g.layer, cx, cz, band, e, 5)) }
|
||||
|
||||
# the chance at (x, z) after the trample: the game's callback, then the clearings it handed over as data
|
||||
function gf_trampled(render3d_st: mut Render3dState, keep: float, x: float, z: float) -> float {
|
||||
var k = keep
|
||||
if render3d_st.gd_trample_cb != null { k = k * render3d_st.gd_trample_cb(x, z) }
|
||||
if render3d_st.gd_clear_n > 0 { k = k * ground_clear_at(render3d_st, x, z) }
|
||||
return k
|
||||
}
|
||||
|
||||
# cell (i, j) of chunk (cx, cz) at `band` - the chunk's corner (x0, z0), its side `size` - into `out`: true when
|
||||
# one stands there
|
||||
export function ground_candidate(render3d_st: mut Render3dState, g: GroundFill, cx: int, cz: int, band: int, i: int, j: int, x0: float, z0: float, size: float, out: GroundCand) -> bool {
|
||||
let step = gf_step(g, band)
|
||||
if step <= 0.0 { return false }
|
||||
let e = gf_cell(i, j)
|
||||
let px = gf_along(x0, i, gf_hash(g.layer, cx, cz, band, e, 0), g.jitter, step)
|
||||
let pz = gf_along(z0, j, gf_hash(g.layer, cx, cz, band, e, 1), g.jitter, step)
|
||||
if px < x0 or px >= x0 + size or pz < z0 or pz >= z0 + size { return false }
|
||||
var keep = ground_density_at(render3d_st, g.layer, px, pz, 0)
|
||||
if keep <= 0.0 { return false }
|
||||
if g.trample { keep = gf_trampled(render3d_st, keep, px, pz) }
|
||||
if not gf_keeps(g, cx, cz, band, e, keep) { return false }
|
||||
out.x = px
|
||||
out.z = pz
|
||||
out.cell = e
|
||||
out.id = -1
|
||||
out.scale = gf_scale(g, cx, cz, band, e, ground_density_at(render3d_st, g.layer, px, pz, 1))
|
||||
out.yaw = gf_yaw(g, cx, cz, band, e)
|
||||
out.seed = gf_seed(g, cx, cz, band, e)
|
||||
out.wind = gf_wind(g, cx, cz, band, e)
|
||||
return true
|
||||
}
|
||||
44
packages/ludic.render3d/ground_clear.ludic
Normal file
44
packages/ludic.render3d/ground_clear.ludic
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
# ground_clear.ludic — the trample as data: clearings the game hands over (a camp's ring, a town's yard), each a
|
||||
# disc trodden to `floor` inside r_in and back to full at r_out on a smoothstep. A layer with `trample` multiplies
|
||||
# its chance by every clearing's; the editor draws the same discs, where it cannot see a callback.
|
||||
|
||||
const GD_CLEAR_MAX: int = 32
|
||||
|
||||
# no clearings (a map being opened, before the game hands its own)
|
||||
export function r3d_ground_clearings_reset(render3d_st: mut Render3dState) -> void { render3d_st.gd_clear_n = 0 }
|
||||
|
||||
# a clearing at (x, z): false when GD_CLEAR_MAX are already held, or the disc is not r_in < r_out
|
||||
export function r3d_ground_clearing(render3d_st: mut Render3dState, x: float, z: float, r_in: float, r_out: float, floor: float) -> bool {
|
||||
if render3d_st.gd_clear_n >= GD_CLEAR_MAX or not (r_in < r_out) { return false }
|
||||
let o = render3d_st.gd_clear_n * 5
|
||||
render3d_st.gd_clear[o] = x
|
||||
render3d_st.gd_clear[o + 1] = z
|
||||
render3d_st.gd_clear[o + 2] = r_in
|
||||
render3d_st.gd_clear[o + 3] = r_out
|
||||
render3d_st.gd_clear[o + 4] = Math.clamp(floor, 0.0, 1.0)
|
||||
render3d_st.gd_clear_n += 1
|
||||
return true
|
||||
}
|
||||
|
||||
# one clearing's share at (x, z), 0..1: `floor` inside r_in, 1 past r_out
|
||||
export function gf_clear_one(cx: float, cz: float, r_in: float, r_out: float, floor: float, x: float, z: float) -> float {
|
||||
let dx = x - cx
|
||||
let dz = z - cz
|
||||
let d2 = dx * dx + dz * dz
|
||||
if d2 > r_out * r_out { return 1.0 }
|
||||
let d = Math.sqrt(d2)
|
||||
var t = Math.clamp((d - r_in) / (r_out - r_in), 0.0, 1.0)
|
||||
t = t * t * (3.0 - 2.0 * t)
|
||||
return floor + (1.0 - floor) * t
|
||||
}
|
||||
|
||||
# every clearing's share at (x, z), multiplied in the order they were handed over
|
||||
export function ground_clear_at(render3d_st: Render3dState, x: float, z: float) -> float {
|
||||
var k = 1.0
|
||||
for c in 0 .. render3d_st.gd_clear_n {
|
||||
let o = c * 5
|
||||
let cl = render3d_st.gd_clear
|
||||
k = k * gf_clear_one(cl[o], cl[o + 1], cl[o + 2], cl[o + 3], cl[o + 4], x, z)
|
||||
}
|
||||
return k
|
||||
}
|
||||
|
|
@ -1,7 +1,6 @@
|
|||
# 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.
|
||||
# ground_fill.ludic — a ground layer's instances in one chunk of its stream, from its painted density and
|
||||
# nothing else: each cell's candidate (ground_cand.ludic), the same on every machine and in the editor. The
|
||||
# camera picks only the chunks and their band; for a solid layer (ground_solid.ludic) not where things stand.
|
||||
|
||||
# a ground layer's numbers, as the game's row (GroundLayers) gives them; `layer` is its index in the densities
|
||||
export property GroundFill {
|
||||
|
|
@ -18,13 +17,14 @@ export property GroundFill {
|
|||
sink: float = 0.03,
|
||||
trample: bool = true,
|
||||
scale_var: float = 0.0, # each instance's own size: x (1 +- var), from its hash (a flower is not its neighbour)
|
||||
band_grow: float = 0.0 # a far band's instances larger by this a band, to keep the cover as the step widens
|
||||
band_grow: float = 0.0, # a far band's instances larger by this a band, to keep the cover as the step widens
|
||||
solid: bool = false # things to touch: one step (step0) at every band, a stable id each, distance only thins
|
||||
}
|
||||
|
||||
# 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 {
|
||||
export 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 }
|
||||
|
|
@ -32,7 +32,7 @@ function gf_step(g: GroundFill, band: int) -> float {
|
|||
}
|
||||
|
||||
# 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 {
|
||||
export 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
|
||||
|
|
@ -43,25 +43,21 @@ function gf_hash(a: int, b: int, c: int, d: int, e: int, k: int) -> float {
|
|||
|
||||
# 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 {
|
||||
if g.solid {
|
||||
ground_fill_solid(render3d_st, s, cx, cz, band, g)
|
||||
return
|
||||
}
|
||||
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))
|
||||
let c = render3d_st.gd_cand
|
||||
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 }
|
||||
var sc = Math.lerp(g.scale_lo, g.scale_hi, ground_density_at(render3d_st, g.layer, px, pz, 1))
|
||||
sc = sc * (1.0 + (gf_hash(g.layer, cx, cz, band, e, 6) * 2.0 - 1.0) * g.scale_var) * (1.0 + g.band_grow * float(band))
|
||||
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)))
|
||||
if not ground_candidate(render3d_st, g, cx, cz, band, i, j, x0, z0, s.size, c) { continue }
|
||||
let h = terrain_height(render3d_st, c.x, c.z) - g.sink
|
||||
stream_emit(render3d_st, s, c.x, h, c.z, c.scale, c.yaw, c.seed, c.wind)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
65
packages/ludic.render3d/ground_solid.ludic
Normal file
65
packages/ludic.render3d/ground_solid.ludic
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
# ground_solid.ludic — a solid layer (rocks, trees): its candidates at step0 and band 0 whatever the camera, so
|
||||
# a thing stands in one place for physics, the nav bake, a save and the draw alike. Its id is (layer, chunk,
|
||||
# cell), stable while the painting under it is; a far band draws a subset of the same things, never others.
|
||||
|
||||
const GS_SPAN: int = 256 # chunks a side an id holds: -128 .. 127 about the stream's corner
|
||||
const GS_CELLS: int = 512 # cells a chunk an id holds: a solid layer's step is at least the chunk's side / 22
|
||||
|
||||
# the id of cell (i, j) of chunk (cx, cz) in `layer`, where a chunk is cells x cells; -1 past what an id holds
|
||||
export function ground_solid_id(layer: int, cx: int, cz: int, i: int, j: int, cells: int) -> int {
|
||||
if layer < 0 or layer >= GD_LAYERS or cells * cells > GS_CELLS { return -1 }
|
||||
if cx < -128 or cx > 127 or cz < -128 or cz > 127 { return -1 }
|
||||
return ((layer * GS_SPAN + (cz + 128)) * GS_SPAN + (cx + 128)) * GS_CELLS + i * cells + j
|
||||
}
|
||||
|
||||
export function ground_solid_layer(id: int) -> int { return id / (GS_CELLS * GS_SPAN * GS_SPAN) }
|
||||
export function ground_solid_cx(id: int) -> int { return (id / GS_CELLS) % GS_SPAN - 128 }
|
||||
export function ground_solid_cz(id: int) -> int { return (id / (GS_CELLS * GS_SPAN)) % GS_SPAN - 128 }
|
||||
|
||||
# a solid layer's cells a chunk side
|
||||
export function gs_cells(g: GroundFill, size: float) -> int { return int(Math.ceil(size / g.step0)) }
|
||||
|
||||
# the share of a solid layer drawn at `band`: as much fewer as a cover layer's wider step would make it
|
||||
export function gs_thin(g: GroundFill, band: int) -> float {
|
||||
if band <= 0 { return 1.0 }
|
||||
let r = g.step0 / gf_step(g, band)
|
||||
return Math.min(r * r, 1.0)
|
||||
}
|
||||
|
||||
# cell (i, j) of chunk (cx, cz) of solid layer g into `out`, its id with it: true when one stands there
|
||||
export function ground_solid_candidate(render3d_st: mut Render3dState, g: GroundFill, ox: float, oz: float, size: float, cx: int, cz: int, i: int, j: int, out: GroundCand) -> bool {
|
||||
if g.step0 <= 0.0 { return false }
|
||||
let id = ground_solid_id(g.layer, cx, cz, i, j, gs_cells(g, size))
|
||||
if id < 0 { return false }
|
||||
let x0 = ox + float(cx) * size
|
||||
let z0 = oz + float(cz) * size
|
||||
if not ground_candidate(render3d_st, g, cx, cz, 0, i, j, x0, z0, size, out) { return false }
|
||||
out.id = id
|
||||
return true
|
||||
}
|
||||
|
||||
# the draw: every candidate at band 0, a stable subset of them farther out (draw 7 under gs_thin)
|
||||
function ground_fill_solid(render3d_st: mut Render3dState, s: Stream, cx: int, cz: int, band: int, g: GroundFill) -> void {
|
||||
if g.step0 <= 0.0 or render3d_st.gd_file == null { return }
|
||||
let cells = gs_cells(g, s.size)
|
||||
let thin = gs_thin(g, band)
|
||||
let c = render3d_st.gd_cand
|
||||
for j in 0 .. cells {
|
||||
for i in 0 .. cells {
|
||||
if not ground_solid_candidate(render3d_st, g, s.ox, s.oz, s.size, cx, cz, i, j, c) { continue }
|
||||
if band > 0 and not (gf_hash(g.layer, cx, cz, 0, c.cell, 7) < thin) { continue }
|
||||
let h = terrain_height(render3d_st, c.x, c.z) - g.sink
|
||||
stream_emit(render3d_st, s, c.x, h, c.z, c.scale, c.yaw, c.seed, c.wind)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# the candidate `id` names in solid layer g (the stream's corner and chunk side): false when the painting
|
||||
# under it keeps nothing there now, or the id is not this layer's
|
||||
export function ground_solid_at(render3d_st: mut Render3dState, g: GroundFill, ox: float, oz: float, size: float, id: int, out: GroundCand) -> bool {
|
||||
if id < 0 or g.step0 <= 0.0 or ground_solid_layer(id) != g.layer { return false }
|
||||
let cells = gs_cells(g, size)
|
||||
let cell = id % GS_CELLS
|
||||
if cells <= 0 or cell >= cells * cells { return false }
|
||||
return ground_solid_candidate(render3d_st, g, ox, oz, size, ground_solid_cx(id), ground_solid_cz(id), cell / cells, cell % cells, out)
|
||||
}
|
||||
60
packages/ludic.render3d/ground_solid_list.ludic
Normal file
60
packages/ludic.render3d/ground_solid_list.ludic
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
# ground_solid_list.ludic — a solid layer's things in one chunk, into a list the caller made once: what physics,
|
||||
# the nav bake and a save ask, the same candidates the draw grows at band 0 (ground_solid.ludic).
|
||||
|
||||
export property GroundSolids {
|
||||
cap: int = 0,
|
||||
n: int = 0, # how many were written, never over cap
|
||||
found: int = 0, # how many stand in the chunk: over cap, the list was too short for it
|
||||
ids: words = null,
|
||||
xs: floats = null,
|
||||
zs: floats = null,
|
||||
scales: floats = null,
|
||||
yaws: floats = null,
|
||||
seeds: floats = null,
|
||||
winds: floats = null,
|
||||
cand: GroundCand = null
|
||||
}
|
||||
|
||||
@alloc_ok("a caller's list of solid things, made once and reused")
|
||||
export function ground_solids_new(cap: int) -> GroundSolids {
|
||||
let l = new GroundSolids
|
||||
l.cap = max(cap, 1)
|
||||
l.ids = words(l.cap)
|
||||
l.xs = floats(l.cap)
|
||||
l.zs = floats(l.cap)
|
||||
l.scales = floats(l.cap)
|
||||
l.yaws = floats(l.cap)
|
||||
l.seeds = floats(l.cap)
|
||||
l.winds = floats(l.cap)
|
||||
l.cand = new GroundCand
|
||||
return l
|
||||
}
|
||||
|
||||
function gs_put(l: GroundSolids, c: GroundCand) -> void {
|
||||
l.found += 1
|
||||
if l.n >= l.cap { return }
|
||||
let k = l.n
|
||||
l.ids[k] = c.id
|
||||
l.xs[k] = c.x
|
||||
l.zs[k] = c.z
|
||||
l.scales[k] = c.scale
|
||||
l.yaws[k] = c.yaw
|
||||
l.seeds[k] = c.seed
|
||||
l.winds[k] = c.wind
|
||||
l.n += 1
|
||||
}
|
||||
|
||||
# chunk (cx, cz) of solid layer g - the stream's corner (ox, oz) and chunk side `size` - into `out`, emptied
|
||||
# first, in cell order (j, then i); answers how many stand there
|
||||
export function ground_solid_list(render3d_st: mut Render3dState, g: GroundFill, ox: float, oz: float, size: float, cx: int, cz: int, out: GroundSolids) -> int {
|
||||
out.n = 0
|
||||
out.found = 0
|
||||
if g.step0 <= 0.0 or render3d_st.gd_file == null { return 0 }
|
||||
let cells = gs_cells(g, size)
|
||||
for j in 0 .. cells {
|
||||
for i in 0 .. cells {
|
||||
if ground_solid_candidate(render3d_st, g, ox, oz, size, cx, cz, i, j, out.cand) { gs_put(out, out.cand) }
|
||||
}
|
||||
}
|
||||
return out.found
|
||||
}
|
||||
|
|
@ -60,6 +60,10 @@ import "stream_baked.ludic"
|
|||
import "ground_density_write.ludic"
|
||||
import "ground_density.ludic"
|
||||
import "ground_fill.ludic"
|
||||
import "ground_cand.ludic"
|
||||
import "ground_clear.ludic"
|
||||
import "ground_solid.ludic"
|
||||
import "ground_solid_list.ludic"
|
||||
import "fog_streams.ludic"
|
||||
import "grass_kind.ludic"
|
||||
import "grass_bind.ludic"
|
||||
|
|
|
|||
96
packages/ludic.render3d/tests/fakes/ground_golden.ludic
Normal file
96
packages/ludic.render3d/tests/fakes/ground_golden.ludic
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
# fakes/ground_golden.ludic - ground_fill's conformance fixture: one density layer written by hand (R and G, a
|
||||
# block of raw tiles round the chunk, every other tile empty), a cover layer with a clearing and a solid layer
|
||||
# grown over it, and chunk (3, -2) of a 32 m grid from world zero - across four tiles, at negative z.
|
||||
const GG_CX: int = 3
|
||||
const GG_CZ: int = -2
|
||||
const GG_BAND: int = 1 # the cover layer's band: a step that does not divide the chunk, and band_grow
|
||||
const GG_T0: int = 64 # the raw tiles: x 64..67, z 61..64 (the chunk reads x 65..66, z 62..63)
|
||||
const GG_Z0: int = 61
|
||||
const GG_TN: int = 4
|
||||
|
||||
# the density's texels as written, 0..255: R ramps through empty and full, G is a weave
|
||||
function gg_r(tx: int, tz: int) -> int { return min(max((tx * 53 + tz * 97) % 320 - 32, 0), 255) }
|
||||
function gg_g(tx: int, tz: int) -> int { return (tx * 29 + tz * 13) & 255 }
|
||||
|
||||
function gg_put(b: []byte, at: int, v: int) -> void {
|
||||
for k in 0 .. 4 { b[at + k] = (v >> (k * 8)) & 255 }
|
||||
}
|
||||
|
||||
# an LGD2 payload: one layer of two channels, the raw tiles after the index
|
||||
function gg_payload() -> []byte {
|
||||
let n = TERRAIN_RES / TT_TEX
|
||||
let tb = GD_SIDE * GD_SIDE * 2
|
||||
let at0 = GD_HEADW * 4 + n * n * 8
|
||||
let size = at0 + GG_TN * GG_TN * tb
|
||||
let b = buffer(size)
|
||||
for k in 0 .. size { b[k] = 0 }
|
||||
gg_put(b, 0, GD_MAGIC)
|
||||
gg_put(b, 4, GD_VERSION)
|
||||
gg_put(b, 8, n)
|
||||
gg_put(b, 12, GD_SIDE)
|
||||
gg_put(b, 16, 1)
|
||||
gg_put(b, 32, 2)
|
||||
gg_put(b, 36, GD_HEADW * 4)
|
||||
for q in 0 .. GG_TN * GG_TN {
|
||||
let tx = GG_T0 + q % GG_TN
|
||||
let tz = GG_Z0 + q / GG_TN
|
||||
let at = at0 + q * tb
|
||||
gg_put(b, GD_HEADW * 4 + (tz * n + tx) * 8, at)
|
||||
gg_put(b, GD_HEADW * 4 + (tz * n + tx) * 8 + 4, tb)
|
||||
for p in 0 .. GD_SIDE * GD_SIDE {
|
||||
let x = tx * GD_SIDE + p % GD_SIDE
|
||||
let z = tz * GD_SIDE + p / GD_SIDE
|
||||
b[at + p * 2] = gg_r(x, z)
|
||||
b[at + p * 2 + 1] = gg_g(x, z)
|
||||
}
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
# an 8 km map at world zero with no terrain (heights 0), the densities above, and the one clearing
|
||||
function gg_open(render3d_st: mut Render3dState, tag: string) -> bool {
|
||||
render3d_st.TERRAIN_HALF = 4096
|
||||
render3d_st.ter_ox = 0.0
|
||||
render3d_st.ter_oz = 0.0
|
||||
render3d_st.gd_trample_cb = null
|
||||
r3d_ground_clearings_reset(render3d_st)
|
||||
r3d_ground_clearing(render3d_st, 112.0, -48.0, 3.0, 7.5, 0.25)
|
||||
let path = Os.temp_dir() + "/r3d_ground_golden_" + tag + ".lgd2"
|
||||
let b = gg_payload()
|
||||
return Fs.write_bytes(path, b, len(b)) and gd_open_at(render3d_st, path, 0)
|
||||
}
|
||||
|
||||
function gg_cover() -> GroundFill {
|
||||
let g = new GroundFill
|
||||
g.step0 = 2.0
|
||||
g.step1 = 2.5
|
||||
g.scale_lo = 0.6
|
||||
g.scale_hi = 1.4
|
||||
g.wind_lo = 0.5
|
||||
g.scale_var = 0.2
|
||||
g.band_grow = 0.15
|
||||
return g
|
||||
}
|
||||
|
||||
function gg_solid() -> GroundFill {
|
||||
let g = gg_cover()
|
||||
g.step0 = 4.0
|
||||
g.step1 = 5.0
|
||||
g.step2 = 8.0
|
||||
g.step3 = 12.0
|
||||
g.jitter = 0.8
|
||||
g.scale_lo = 0.8
|
||||
g.scale_hi = 1.6
|
||||
g.band_grow = 0.3
|
||||
g.trample = false
|
||||
g.solid = true
|
||||
return g
|
||||
}
|
||||
|
||||
function gg_stream(render3d_st: mut Render3dState) -> Stream {
|
||||
if render3d_st.stream_scratch == null { render3d_st.stream_scratch = floats(4096 * INST_FLOATS) }
|
||||
let s = new Stream
|
||||
s.size = 32.0
|
||||
s.cur = new Chunk
|
||||
return s
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
# fakes/ground_golden_check.ludic - what ground_fill_test compares drawn instances by
|
||||
|
||||
# how far drawn instance k is from candidate c: 0 when every number is the same bits
|
||||
function gft_off(render3d_st: Render3dState, k: int, c: GroundCand) -> int {
|
||||
let o = k * INST_FLOATS
|
||||
let st = render3d_st.stream_scratch
|
||||
if st[o] != c.x or st[o + 2] != c.z or st[o + 3] != c.scale or st[o + 6] != c.seed or st[o + 7] != c.wind { return 1 }
|
||||
if st[o + 4] != Math.sin(c.yaw) or st[o + 5] != Math.cos(c.yaw) { return 1 }
|
||||
return 0
|
||||
}
|
||||
|
||||
# drawn instances not at a listed thing's place
|
||||
function gft_subset(render3d_st: Render3dState, n: int, l: GroundSolids) -> int {
|
||||
var bad = 0
|
||||
for k in 0 .. n {
|
||||
var hit = false
|
||||
for q in 0 .. l.n { hit = hit or (render3d_st.stream_scratch[k * INST_FLOATS] == l.xs[q] and render3d_st.stream_scratch[k * INST_FLOATS + 2] == l.zs[q]) }
|
||||
if not hit { bad += 1 }
|
||||
}
|
||||
return bad
|
||||
}
|
||||
66
packages/ludic.render3d/tests/fakes/ground_golden_json.ludic
Normal file
66
packages/ludic.render3d/tests/fakes/ground_golden_json.ludic
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
# fakes/ground_golden_json.ludic - the golden's first half: every float is its IEEE single bits as an int, so
|
||||
# the file round-trips exactly; Math.lerp's order, the yaw the model shader turns by, gf_hash and the density.
|
||||
|
||||
function gg_b(x: float) -> string { return `{float_bits(x)}` }
|
||||
|
||||
# Math.lerp is a + (b - a) * t; each case here is one a * (1 - t) + b * t would round differently
|
||||
function gg_lerps() -> string {
|
||||
let a = [1.0, 2.0, 6.04, -2.19]
|
||||
let b = [1.7, 7.0, 1.72, 3.62]
|
||||
let t = [0.9, 0.71, 0.639, 0.868]
|
||||
var s = " \"lerp_order\": \"a + (b - a) * t\",\n \"lerp\": [\n"
|
||||
for k in 0 .. 4 {
|
||||
let other = a[k] * (1.0 - t[k]) + b[k] * t[k]
|
||||
s = s + ` [{gg_b(a[k])}, {gg_b(b[k])}, {gg_b(t[k])}, {gg_b(Math.lerp(a[k], b[k], t[k]))}, {gg_b(other)}]`
|
||||
if k < 3 { s = s + "," }
|
||||
s = s + "\n"
|
||||
}
|
||||
return s + " ],\n"
|
||||
}
|
||||
|
||||
# model.vert: p' = (c x + s z, y, -s x + c z) with (s, c) = (sin yaw, cos yaw) as stream_emit stores them
|
||||
function gg_yaw() -> string {
|
||||
let y = 0.5
|
||||
let sn = Math.sin(y)
|
||||
let cs = Math.cos(y)
|
||||
var s = ` "yaw": {{"yaw": {gg_b(y)}, "sin": {gg_b(sn)}, "cos": {gg_b(cs)}, "rule": "p' = (c*x + s*z, y, -s*x + c*z)",\n`
|
||||
s = s + ` "x_axis": {gg_turn(sn, cs, 1.0, 0.0)}, "z_axis": {gg_turn(sn, cs, 0.0, 1.0)}}},\n`
|
||||
return s
|
||||
}
|
||||
|
||||
# where local (x, 0, z) lands, as the shader turns it
|
||||
function gg_turn(sn: float, cs: float, x: float, z: float) -> string { return `[{gg_b(cs * x + sn * z)}, {gg_b(0.0)}, {gg_b(-sn * x + cs * z)}]` }
|
||||
|
||||
function gg_hashes() -> string {
|
||||
let v = [0, 0, 0, 0, 0, 0, 1, 2, 3, 4, 5, 6, 0, 3, -2, 1, 7 * 65536 + 9, 2, 13, -128, 127, 3, 32767, 5,
|
||||
63, -1, -1, 0, 21 * 65536 + 21, 7, -5, 100000, -100000, 2, 123456789, 1, 2147483647, -2147483647, 65536, 3, -1, 4]
|
||||
var s = " \"hash\": [\n"
|
||||
let n = len(v) / 6
|
||||
for r in 0 .. n {
|
||||
let o = r * 6
|
||||
s = s + ` [{v[o]}, {v[o + 1]}, {v[o + 2]}, {v[o + 3]}, {v[o + 4]}, {v[o + 5]}, {gg_b(gf_hash(v[o], v[o + 1], v[o + 2], v[o + 3], v[o + 4], v[o + 5]))}]`
|
||||
if r < n - 1 { s = s + "," }
|
||||
s = s + "\n"
|
||||
}
|
||||
return s + " ],\n"
|
||||
}
|
||||
|
||||
# the texels the chunk's bilinear reads (and a border), R and G row by row from (tx0, tz0); none outside is read
|
||||
function gg_density() -> string {
|
||||
let tx0 = 2094
|
||||
let tz0 = 2014
|
||||
let w = 20
|
||||
var r = ""
|
||||
var g = ""
|
||||
for k in 0 .. w * w {
|
||||
if k > 0 {
|
||||
r = r + ","
|
||||
g = g + ","
|
||||
}
|
||||
r = r + `{gg_r(tx0 + k % w, tz0 + k / w)}`
|
||||
g = g + `{gg_g(tx0 + k % w, tz0 + k / w)}`
|
||||
}
|
||||
var s = ` "density": {{"layer": 0, "half": 4096, "ter_ox": 0, "ter_oz": 0, "texel_m": 2, "rule": "fx = (x - ter_ox + half) / texel - 0.5, bilinear, / 255",\n`
|
||||
s = s + ` "tx0": {tx0}, "tz0": {tz0}, "w": {w},\n "r": [{r}],\n "g": [{g}]}},\n`
|
||||
return s
|
||||
}
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
# fakes/ground_golden_things.ludic - the golden's second half: the clearing, the layers' numbers and what each
|
||||
# grows in the chunk (a cover layer at GG_BAND, a solid layer at band 0 with what band 2 still draws)
|
||||
|
||||
function gg_fill(g: GroundFill) -> string {
|
||||
var s = `{{"layer": {g.layer}, "steps": [{gg_b(g.step0)}, {gg_b(g.step1)}, {gg_b(g.step2)}, {gg_b(g.step3)}], "jitter": {gg_b(g.jitter)}, `
|
||||
s = s + `"scale_lo": {gg_b(g.scale_lo)}, "scale_hi": {gg_b(g.scale_hi)}, "wind_lo": {gg_b(g.wind_lo)}, "wind_hi": {gg_b(g.wind_hi)}, `
|
||||
s = s + `"scale_var": {gg_b(g.scale_var)}, "band_grow": {gg_b(g.band_grow)}, "sink": {gg_b(g.sink)}, "trample": {g.trample}, "solid": {g.solid}}}`
|
||||
return s
|
||||
}
|
||||
|
||||
function gg_clearings(render3d_st: Render3dState) -> string {
|
||||
var s = " \"clearings_rule\": \"floor + (1 - floor) * smoothstep((d - r_in) / (r_out - r_in)), 1 past r_out\",\n \"clearings\": ["
|
||||
for c in 0 .. render3d_st.gd_clear_n {
|
||||
let o = c * 5
|
||||
let cl = render3d_st.gd_clear
|
||||
if c > 0 { s = s + ", " }
|
||||
s = s + `[{gg_b(cl[o])}, {gg_b(cl[o + 1])}, {gg_b(cl[o + 2])}, {gg_b(cl[o + 3])}, {gg_b(cl[o + 4])}]`
|
||||
}
|
||||
return s + "],\n"
|
||||
}
|
||||
|
||||
function gg_thing(c: GroundCand) -> string { return `{gg_b(c.x)}, {gg_b(c.z)}, {gg_b(c.scale)}, {gg_b(c.yaw)}, {gg_b(c.seed)}, {gg_b(c.wind)}` }
|
||||
|
||||
# the cover layer's things in cell order: [cell, x, z, scale, yaw, seed, wind]
|
||||
function gg_cover_json(render3d_st: mut Render3dState) -> string {
|
||||
let g = gg_cover()
|
||||
let size = 32.0
|
||||
let cells = int(Math.ceil(size / gf_step(g, GG_BAND)))
|
||||
let c = new GroundCand
|
||||
var s = ` "cover": {{"fill": {gg_fill(g)}, "ox": 0, "oz": 0, "size": {gg_b(size)}, "cx": {GG_CX}, "cz": {GG_CZ}, "band": {GG_BAND}, "things": [\n`
|
||||
var first = true
|
||||
for j in 0 .. cells {
|
||||
for i in 0 .. cells {
|
||||
if not ground_candidate(render3d_st, g, GG_CX, GG_CZ, GG_BAND, i, j, float(GG_CX) * size, float(GG_CZ) * size, size, c) { continue }
|
||||
if not first { s = s + ",\n" }
|
||||
first = false
|
||||
s = s + ` [{c.cell}, {gg_thing(c)}]`
|
||||
}
|
||||
}
|
||||
return s + "\n ]}},\n"
|
||||
}
|
||||
|
||||
# the solid layer's things: [id, cell, x, z, scale, yaw, seed, wind, drawn at band 2]
|
||||
function gg_solid_json(render3d_st: mut Render3dState) -> string {
|
||||
let g = gg_solid()
|
||||
let l = ground_solids_new(256)
|
||||
ground_solid_list(render3d_st, g, 0.0, 0.0, 32.0, GG_CX, GG_CZ, l)
|
||||
let c = new GroundCand
|
||||
var s = ` "solid": {{"fill": {gg_fill(g)}, "ox": 0, "oz": 0, "size": {gg_b(32.0)}, "cx": {GG_CX}, "cz": {GG_CZ}, "far_band": 2, "things": [\n`
|
||||
for k in 0 .. l.n {
|
||||
ground_solid_at(render3d_st, g, 0.0, 0.0, 32.0, l.ids[k], c)
|
||||
let far = gf_hash(g.layer, GG_CX, GG_CZ, 0, c.cell, 7) < gs_thin(g, 2)
|
||||
if k > 0 { s = s + ",\n" }
|
||||
s = s + ` [{c.id}, {c.cell}, {gg_thing(c)}, {far}]`
|
||||
}
|
||||
return s + "\n ]}}\n"
|
||||
}
|
||||
|
||||
# the whole golden, as ground_fill_golden.json holds it
|
||||
function gg_json(render3d_st: mut Render3dState) -> string {
|
||||
var s = "{\n \"about\": \"ludic.render3d ground_fill conformance (tests/ground_fill_test.ludic; regenerate with tests/gen/ground_fill_golden.ludic)\",\n"
|
||||
s = s + " \"numbers\": \"every float is its IEEE-754 single-precision bits as a signed 32-bit int\",\n"
|
||||
s = s + gg_lerps() + gg_yaw() + gg_hashes() + gg_density() + gg_clearings(render3d_st)
|
||||
return s + gg_cover_json(render3d_st) + gg_solid_json(render3d_st) + "}\n"
|
||||
}
|
||||
21
packages/ludic.render3d/tests/gen/ground_fill_golden.ludic
Normal file
21
packages/ludic.render3d/tests/gen/ground_fill_golden.ludic
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
# gen/ground_fill_golden.ludic - writes tests/ground_fill_golden.json from what ground_fill grows today. Run it
|
||||
# from the repository root after a deliberate change to the fill, and commit the file with the change.
|
||||
import "ludic.render3d/r3d.ludic"
|
||||
program GroundFillGolden {
|
||||
numbers float
|
||||
import "../fakes/ground_golden.ludic"
|
||||
import "../fakes/ground_golden_json.ludic"
|
||||
import "../fakes/ground_golden_things.ludic"
|
||||
# never called: naming Input links the engine's runtime, which render3d's textures stand on
|
||||
function links_runtime() -> bool { return Input.key_pressed(0) }
|
||||
|
||||
entry (render3d_st: mut Render3dState) {
|
||||
if not gg_open(render3d_st, "write") {
|
||||
print("ground_fill_golden: the density could not be written")
|
||||
return
|
||||
}
|
||||
let path = "packages/ludic.render3d/tests/ground_fill_golden.json"
|
||||
let text = gg_json(render3d_st)
|
||||
if Fs.write_text(path, text) { print(`ground_fill_golden: wrote {path}`) } else { print(`ground_fill_golden: could not write {path}`) }
|
||||
}
|
||||
}
|
||||
135
packages/ludic.render3d/tests/ground_fill_golden.json
Normal file
135
packages/ludic.render3d/tests/ground_fill_golden.json
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
{
|
||||
"about": "ludic.render3d ground_fill conformance (tests/ground_fill_test.ludic; regenerate with tests/gen/ground_fill_golden.ludic)",
|
||||
"numbers": "every float is its IEEE-754 single-precision bits as a signed 32-bit int",
|
||||
"lerp_order": "a + (b - a) * t",
|
||||
"lerp": [
|
||||
[1065353216, 1071225242, 1063675494, 1070638040, 1070638039],
|
||||
[1073741824, 1088421888, 1060487823, 1085381018, 1085381017],
|
||||
[1086408622, 1071393014, 1059296641, 1079108521, 1079108520],
|
||||
[-1072944906, 1080536596, 1063138623, 1077319900, 1077319899]
|
||||
],
|
||||
"yaw": {"yaw": 1056964608, "sin": 1056274244, "cos": 1063299392, "rule": "p' = (c*x + s*z, y, -s*x + c*z)",
|
||||
"x_axis": [1063299392, 0, -1091209404], "z_axis": [1056274244, 0, 1063299392]},
|
||||
"hash": [
|
||||
[0, 0, 0, 0, 0, 0, 0],
|
||||
[1, 2, 3, 4, 5, 6, 1059717290],
|
||||
[0, 3, -2, 1, 458761, 2, 1057636647],
|
||||
[13, -128, 127, 3, 32767, 5, 1049010834],
|
||||
[63, -1, -1, 0, 1376277, 7, 1057639637],
|
||||
[-5, 100000, -100000, 2, 123456789, 1, 1063465007],
|
||||
[2147483647, -2147483647, 65536, 3, -1, 4, 1040271092]
|
||||
],
|
||||
"density": {"layer": 0, "half": 4096, "ter_ox": 0, "ter_oz": 0, "texel_m": 2, "rule": "fx = (x - ter_ox + half) / texel - 0.5, bilinear, / 255",
|
||||
"tx0": 2094, "tz0": 2014, "w": 20,
|
||||
"r": [68,121,174,227,255,13,66,119,172,225,255,11,64,117,170,223,255,9,62,115,165,218,255,4,57,110,163,216,255,2,55,108,161,214,255,0,53,106,159,212,255,0,48,101,154,207,255,0,46,99,152,205,255,0,44,97,150,203,255,0,39,92,145,198,251,0,37,90,143,196,249,0,35,88,141,194,247,0,33,86,136,189,242,0,28,81,134,187,240,0,26,79,132,185,238,0,24,77,130,183,233,255,19,72,125,178,231,255,17,70,123,176,229,255,15,68,121,174,227,255,10,63,116,169,222,255,8,61,114,167,220,255,6,59,112,165,218,255,4,57,107,160,213,255,0,52,105,158,211,255,0,50,103,156,209,255,0,48,101,154,204,255,0,43,96,149,202,255,0,41,94,147,200,253,0,39,92,145,198,251,0,34,87,140,193,246,0,32,85,138,191,244,0,30,83,136,189,242,0,28,78,131,184,237,0,23,76,129,182,235,0,21,74,127,180,233,255,19,72,125,175,228,255,14,67,120,173,226,255,12,65,118,171,224,255,10,63,116,169,222,255,5,58,111,164,217,255,3,56,109,162,215,255,1,54,107,160,213,255,0,49,102,155,208,255,0,47,100,153,206,255,0,45,98,151,204,255,0,43,96,146,199,252,0,38,91,144,197,250,0,36,89,142,195,248,0,34,87,140,193,243,0,29,82,135,188,241,0,27,80,133,186,239,0,25,78,131,184,237,0,20,73,126,179,232,255,18,71,124,177,230,255,16,69,122,175,228,255,14,67,117,170,223,255,9,62,115,168,221,255,7,60,113,166,219,255,5,58,111,164,214,255,0,53,106,159,212,255,0,51,104,157,210,255,0,49,102,155,208,255,0,44,97,150,203,255,0,42,95,148,201,254,0,40,93,146,199,252,0,38],
|
||||
"g": [124,153,182,211,240,13,42,71,100,129,158,187,216,245,18,47,76,105,134,163,137,166,195,224,253,26,55,84,113,142,171,200,229,2,31,60,89,118,147,176,150,179,208,237,10,39,68,97,126,155,184,213,242,15,44,73,102,131,160,189,163,192,221,250,23,52,81,110,139,168,197,226,255,28,57,86,115,144,173,202,176,205,234,7,36,65,94,123,152,181,210,239,12,41,70,99,128,157,186,215,189,218,247,20,49,78,107,136,165,194,223,252,25,54,83,112,141,170,199,228,202,231,4,33,62,91,120,149,178,207,236,9,38,67,96,125,154,183,212,241,215,244,17,46,75,104,133,162,191,220,249,22,51,80,109,138,167,196,225,254,228,1,30,59,88,117,146,175,204,233,6,35,64,93,122,151,180,209,238,11,241,14,43,72,101,130,159,188,217,246,19,48,77,106,135,164,193,222,251,24,254,27,56,85,114,143,172,201,230,3,32,61,90,119,148,177,206,235,8,37,11,40,69,98,127,156,185,214,243,16,45,74,103,132,161,190,219,248,21,50,24,53,82,111,140,169,198,227,0,29,58,87,116,145,174,203,232,5,34,63,37,66,95,124,153,182,211,240,13,42,71,100,129,158,187,216,245,18,47,76,50,79,108,137,166,195,224,253,26,55,84,113,142,171,200,229,2,31,60,89,63,92,121,150,179,208,237,10,39,68,97,126,155,184,213,242,15,44,73,102,76,105,134,163,192,221,250,23,52,81,110,139,168,197,226,255,28,57,86,115,89,118,147,176,205,234,7,36,65,94,123,152,181,210,239,12,41,70,99,128,102,131,160,189,218,247,20,49,78,107,136,165,194,223,252,25,54,83,112,141,115,144,173,202,231,4,33,62,91,120,149,178,207,236,9,38,67,96,125,154]},
|
||||
"clearings_rule": "floor + (1 - floor) * smoothstep((d - r_in) / (r_out - r_in)), 1 past r_out",
|
||||
"clearings": [[1121976320, -1035993088, 1077936128, 1089470464, 1048576000]],
|
||||
"cover": {"fill": {"layer": 0, "steps": [1073741824, 1075838976, 1068708659, 1068708659], "jitter": 1065353216, "scale_lo": 1058642330, "scale_hi": 1068708659, "wind_lo": 1056964608, "wind_hi": 1065353216, "scale_var": 1045220557, "band_grow": 1041865114, "sink": 1022739087, "trample": 1, "solid": 0}, "ox": 0, "oz": 0, "size": 1107296256, "cx": 3, "cz": -2, "band": 1, "things": [
|
||||
[131072, 1120722131, -1031988814, 1063839030, 1075514781, 1061975106, 1058404922],
|
||||
[327680, 1121801699, -1032364098, 1068919519, 1063792719, 1049525222, 1060683454],
|
||||
[458752, 1122465832, -1031889000, 1069448012, 1086016211, 1049231304, 1060531988],
|
||||
[524288, 1122652719, -1032091122, 1069044046, 1084335045, 1042065604, 1057936960],
|
||||
[786432, 1124005574, -1031944401, 1066394760, 1066870402, 1064697147, 1060653764],
|
||||
[1, 1120018591, -1032685244, 1067346335, 1085558634, 1054041374, 1062133124],
|
||||
[65537, 1120371200, -1032954977, 1065975606, 1083518089, 1065129413, 1057612078],
|
||||
[262145, 1121356718, -1032479330, 1066928675, 1024682493, 1064430405, 1058254252],
|
||||
[327681, 1121606079, -1033046405, 1068851011, 1077737535, 1064480426, 1057363182],
|
||||
[458753, 1122328230, -1032827722, 1070792961, 1076663355, 1056375478, 1064587742],
|
||||
[65538, 1120463992, -1033613436, 1060457593, 1074573362, 1057445941, 1062637456],
|
||||
[131074, 1120572733, -1033661622, 1062624664, 1082113703, 1062061190, 1057664958],
|
||||
[262146, 1121314977, -1033482165, 1067453131, 1084681630, 1049680874, 1064772764],
|
||||
[327682, 1121561232, -1033237059, 1067452030, 1077105671, 1053690052, 1057286626],
|
||||
[458754, 1122374807, -1033751933, 1068681354, 1072698788, 1052240590, 1060365712],
|
||||
[655362, 1123332418, -1033652843, 1066408536, 1054991633, 1062544760, 1059281512],
|
||||
[720898, 1123715890, -1033428095, 1067696300, 1076179498, 1043138132, 1064618752],
|
||||
[3, 1120166220, -1033968424, 1062267741, 1085755275, 1058969154, 1060467825],
|
||||
[131075, 1120811203, -1033876423, 1062497849, 1079576187, 1052427156, 1058219821],
|
||||
[196611, 1120960981, -1033939129, 1067260015, 1061916453, 1050262064, 1062985215],
|
||||
[327683, 1121744884, -1034111018, 1068186170, 1086195927, 1063684314, 1064611040],
|
||||
[393219, 1121944133, -1034121688, 1067081453, 1084125171, 1046733352, 1061951717],
|
||||
[458755, 1122336852, -1033860481, 1066352794, 1055127513, 1061107994, 1063479044],
|
||||
[655363, 1123201280, -1034160112, 1065573331, 1086839112, 1061752269, 1064321610],
|
||||
[786435, 1123827217, -1034205505, 1068871523, 1079170585, 1056558946, 1061642159],
|
||||
[4, 1120099678, -1034750794, 1061339931, 1080569423, 1057505192, 1057146090],
|
||||
[131076, 1120789658, -1034810383, 1063347522, 1086251169, 1034781696, 1064206448],
|
||||
[262148, 1121453169, -1034634739, 1068136211, 1076486477, 1055007100, 1060210905],
|
||||
[589828, 1123009927, -1034824436, 1062703839, 1085088285, 1064575722, 1057569190],
|
||||
[720900, 1123581713, -1034680000, 1066931396, 1074502627, 1050478582, 1058532338],
|
||||
[131077, 1120685847, -1035628450, 1067060154, 1066287690, 1060023183, 1058700876],
|
||||
[262149, 1121200649, -1035086950, 1067262377, 1074667727, 1061676180, 1060092720],
|
||||
[524293, 1122785508, -1035296987, 1066758411, 1068971106, 1063885378, 1061440870],
|
||||
[589829, 1122835146, -1035370184, 1064284565, 1085271522, 1058223749, 1062305456],
|
||||
[6, 1120071083, -1035796082, 1064759508, 1051495341, 1063759425, 1063330882],
|
||||
[65542, 1120429090, -1035765120, 1062635826, 1072326525, 1052336760, 1061079065],
|
||||
[196614, 1121004046, -1036017291, 1068151216, 1083200182, 1046648512, 1065104064],
|
||||
[327686, 1121734054, -1035736794, 1068830219, 1083091576, 1060148856, 1058380053],
|
||||
[589830, 1123049340, -1035750522, 1068315059, 1078742942, 1053434130, 1057814420],
|
||||
[655366, 1123252776, -1036107092, 1069084182, 1049407803, 1059915340, 1059287390],
|
||||
[720902, 1123688935, -1036027332, 1067353178, 1075493805, 1052157150, 1057779508],
|
||||
[7, 1119972395, -1036776087, 1065928939, 1084187056, 1058669209, 1058172888],
|
||||
[131079, 1120695165, -1036807948, 1065803870, 1085651203, 1064428109, 1063126413],
|
||||
[262151, 1121305432, -1036505111, 1069874039, 1054543943, 1063591813, 1063478878],
|
||||
[589831, 1122949063, -1036739210, 1066469305, 1077561851, 1046913520, 1057635168],
|
||||
[655367, 1123267161, -1036567612, 1065991224, 1075220436, 1058549287, 1059914820],
|
||||
[720903, 1123799626, -1036435331, 1069770113, 1072588927, 1053806348, 1057430074],
|
||||
[8, 1120102246, -1037422342, 1064335519, 1066268080, 1031625520, 1060565898],
|
||||
[196616, 1121145408, -1037253740, 1066916402, 1060346876, 1057616950, 1061816063],
|
||||
[262152, 1121486820, -1037400879, 1063269927, 1083056872, 1040859600, 1063478826],
|
||||
[655368, 1123239728, -1037186555, 1066870852, 1084093218, 1027108672, 1063293641],
|
||||
[720904, 1123691123, -1037347934, 1069066122, 1085326914, 1017624512, 1059328364],
|
||||
[9, 1120054424, -1038292433, 1067066116, 1070302768, 1052711020, 1062115508],
|
||||
[131081, 1120732507, -1037834558, 1067627931, 1013278155, 1061370947, 1059034438],
|
||||
[262153, 1121369458, -1038212635, 1066866956, 1014830353, 1060618345, 1063656644],
|
||||
[327689, 1121588751, -1038330345, 1063079095, 1078405272, 1064082613, 1063230570],
|
||||
[720905, 1123722622, -1037841175, 1068806914, 1084268125, 1060276795, 1063221992],
|
||||
[65546, 1120304158, -1038861964, 1066184590, 1076017353, 1040823344, 1060286255],
|
||||
[196618, 1120885739, -1038486810, 1070801036, 1086041023, 1057627422, 1062033764],
|
||||
[393226, 1121977389, -1038821564, 1066120053, 1061555987, 1049470312, 1061120434],
|
||||
[524298, 1122776602, -1038830134, 1066173247, 1080610903, 1056751238, 1063824694],
|
||||
[589834, 1123096416, -1038584012, 1070866520, 1085814179, 1062620434, 1058791728],
|
||||
[65547, 1120429794, -1039083608, 1068590716, 1072090856, 1061374698, 1059210362],
|
||||
[131083, 1120553328, -1039112057, 1066175623, 1072596269, 1054427332, 1062420510],
|
||||
[458763, 1122234379, -1039228072, 1066196971, 1078080975, 1058563850, 1065351816],
|
||||
[655371, 1123225640, -1039075734, 1071814252, 1065450289, 1041046272, 1058612354],
|
||||
[786443, 1124045186, -1039406495, 1065956757, 1086915761, 1047327912, 1059610038],
|
||||
[12, 1120172331, -1040047278, 1069387899, 1079288505, 1024762976, 1060390814],
|
||||
[65548, 1120247151, -1039775524, 1068907881, 1084308911, 1065073267, 1060439949],
|
||||
[131084, 1120615769, -1039878030, 1068947240, 1083502790, 1056931844, 1057488786],
|
||||
[196620, 1121118525, -1040010303, 1061322280, 1077929596, 1052826214, 1064042924],
|
||||
[589836, 1123092738, -1039727871, 1068558543, 1084332302, 1049315840, 1057920874],
|
||||
[655372, 1123240528, -1039705474, 1068208532, 1086445325, 1062070265, 1061650604]
|
||||
]}},
|
||||
"solid": {"fill": {"layer": 0, "steps": [1082130432, 1084227584, 1090519040, 1094713344], "jitter": 1061997773, "scale_lo": 1061997773, "scale_hi": 1070386381, "wind_lo": 1056964608, "wind_hi": 1065353216, "scale_var": 1045220557, "band_grow": 1050253722, "sink": 1022739087, "trample": 0, "solid": 1}, "ox": 0, "oz": 0, "size": 1107296256, "cx": 3, "cz": -2, "far_band": 2, "things": [
|
||||
[16582144, 0, 1120293028, -1032252411, 1068709886, 1085262221, 1065248692, 1058250104, 0],
|
||||
[16582176, 262144, 1122044225, -1032619009, 1070248319, 1073719313, 1064173498, 1063466348, 1],
|
||||
[16582200, 458752, 1123660781, -1032485573, 1066304277, 1069870346, 1064911989, 1057976674, 0],
|
||||
[16582161, 131073, 1121004655, -1033560481, 1065826519, 1069762310, 1048734550, 1062419942, 0],
|
||||
[16582185, 327681, 1122891219, -1033334636, 1064534541, 1049917068, 1060399914, 1057515134, 0],
|
||||
[16582193, 393217, 1123227094, -1033072107, 1063157924, 1084632888, 1049823870, 1061233682, 1],
|
||||
[16582146, 2, 1120098383, -1034220530, 1064891188, 1071495521, 1049736648, 1058135006, 0],
|
||||
[16582162, 131074, 1121330806, -1034400123, 1066240901, 1081202597, 1064297738, 1061589532, 1],
|
||||
[16582170, 196610, 1121648030, -1034385209, 1067471220, 1067006313, 1063288699, 1064312224, 0],
|
||||
[16582186, 327682, 1122859183, -1034545454, 1063397635, 1080375926, 1052510306, 1057817892, 0],
|
||||
[16582194, 393218, 1123258040, -1034666854, 1065666160, 1076070243, 1059294668, 1063564212, 0],
|
||||
[16582202, 458754, 1123898810, -1034464750, 1070302032, 1083310160, 1046855468, 1064929602, 0],
|
||||
[16582179, 262147, 1122302705, -1035424756, 1065501747, 1082580068, 1048318644, 1063732166, 1],
|
||||
[16582187, 327683, 1122641852, -1035108121, 1066046128, 1085315314, 1033693656, 1063163313, 0],
|
||||
[16582195, 393219, 1123474480, -1035172904, 1068708433, 1085923941, 1013630976, 1058432680, 0],
|
||||
[16582203, 458755, 1123901206, -1035453673, 1069480276, 1068449825, 1065184737, 1061276845, 0],
|
||||
[16582148, 4, 1120270582, -1036144562, 1066589869, 1063486081, 1058251797, 1060618520, 1],
|
||||
[16582172, 196612, 1121632793, -1036239652, 1068097589, 1069693465, 1064717160, 1057397812, 1],
|
||||
[16582188, 327684, 1122789459, -1036866786, 1064673176, 1086879325, 1065325129, 1064975418, 0],
|
||||
[16582196, 393220, 1123285593, -1036479532, 1067976026, 1082656672, 1014975296, 1059244680, 0],
|
||||
[16582157, 65541, 1120532302, -1037364348, 1068574045, 1083796886, 1062530079, 1057480249, 0],
|
||||
[16582165, 131077, 1121217218, -1037958810, 1071536019, 1083057254, 1029748784, 1059703596, 0],
|
||||
[16582173, 196613, 1121828561, -1037307962, 1064590064, 1079332061, 1056723512, 1058688736, 1],
|
||||
[16582166, 131078, 1121177097, -1038543456, 1070246213, 1052418781, 1058206170, 1064924534, 0],
|
||||
[16582182, 262150, 1122224782, -1038960375, 1066984436, 1078036756, 1052132012, 1059085758, 0],
|
||||
[16582206, 458758, 1123771428, -1038852112, 1063183247, 1081742483, 1044344244, 1058875344, 1],
|
||||
[16582167, 131079, 1121082601, -1039914496, 1064736625, 1086794242, 1054124508, 1059956726, 0],
|
||||
[16582175, 196615, 1121761085, -1039599090, 1067374368, 1067261885, 1058627350, 1062771573, 0],
|
||||
[16582191, 327687, 1122751322, -1039341503, 1069678485, 1068499230, 1053671156, 1059624745, 0],
|
||||
[16582199, 393223, 1123213029, -1039687681, 1071554254, 1053024656, 1029041920, 1063208826, 1]
|
||||
]}}
|
||||
}
|
||||
93
packages/ludic.render3d/tests/ground_fill_test.ludic
Normal file
93
packages/ludic.render3d/tests/ground_fill_test.ludic
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
# ground_fill_test.ludic - ground_fill against its golden (ground_fill_golden.json, which the studio's own test
|
||||
# reads too): the hash, Math.lerp's order, the yaw, one chunk of a cover layer and of a solid layer over a
|
||||
# hand-written density - and ground_fill drawing exactly the candidates, a solid layer's far band a subset
|
||||
import "ludic.render3d/r3d.ludic"
|
||||
program GroundFillTest {
|
||||
numbers float
|
||||
import "fakes/ground_golden.ludic"
|
||||
import "fakes/ground_golden_json.ludic"
|
||||
import "fakes/ground_golden_things.ludic"
|
||||
import "fakes/ground_golden_check.ludic"
|
||||
# never called: naming Input links the engine's runtime, which render3d's textures stand on
|
||||
function links_runtime() -> bool { return Input.key_pressed(0) }
|
||||
|
||||
test "the golden is what the generator writes today, number for number" (render3d_st: mut Render3dState) {
|
||||
expect(gg_open(render3d_st, "golden"))
|
||||
let want = Fs.read_text("packages/ludic.render3d/tests/ground_fill_golden.json")
|
||||
expect(want != null)
|
||||
if want == null { return }
|
||||
let got = Text.split(gg_json(render3d_st), "\n")
|
||||
let had = Text.split(want, "\n")
|
||||
expect_eq(len(got), len(had))
|
||||
for k in 0 .. min(len(got), len(had)) {
|
||||
if got[k] != had[k] { print(`line {k + 1}: got {got[k]}`) }
|
||||
expect(got[k] == had[k])
|
||||
}
|
||||
}
|
||||
|
||||
test "Math.lerp is a + (b - a) * t" {
|
||||
let a = [1.0, 2.0, 6.04, -2.19]
|
||||
let b = [1.7, 7.0, 1.72, 3.62]
|
||||
let t = [0.9, 0.71, 0.639, 0.868]
|
||||
for k in 0 .. 4 { expect_eq(float_bits(Math.lerp(a[k], b[k], t[k])), float_bits(a[k] + (b[k] - a[k]) * t[k])) }
|
||||
}
|
||||
|
||||
test "ground_fill draws exactly the cover layer's candidates, in cell order" (render3d_st: mut Render3dState) {
|
||||
expect(gg_open(render3d_st, "cover"))
|
||||
let g = gg_cover()
|
||||
let s = gg_stream(render3d_st)
|
||||
ground_fill(render3d_st, s, GG_CX, GG_CZ, GG_BAND, g)
|
||||
let cells = int(Math.ceil(32.0 / gf_step(g, GG_BAND)))
|
||||
let c = new GroundCand
|
||||
var n = 0
|
||||
var off = 0
|
||||
for j in 0 .. cells {
|
||||
for i in 0 .. cells {
|
||||
if not ground_candidate(render3d_st, g, GG_CX, GG_CZ, GG_BAND, i, j, 96.0, -64.0, 32.0, c) { continue }
|
||||
off += gft_off(render3d_st, n, c)
|
||||
n += 1
|
||||
}
|
||||
}
|
||||
expect(n > 20)
|
||||
expect_eq(s.cur.count, n)
|
||||
expect_eq(off, 0)
|
||||
}
|
||||
|
||||
test "a solid layer stands still: every band draws a subset of band 0, listed and answered by id" (render3d_st: mut Render3dState) {
|
||||
expect(gg_open(render3d_st, "solid"))
|
||||
let g = gg_solid()
|
||||
let l = ground_solids_new(256)
|
||||
let n = ground_solid_list(render3d_st, g, 0.0, 0.0, 32.0, GG_CX, GG_CZ, l)
|
||||
expect(n > 8)
|
||||
expect_eq(l.n, n)
|
||||
let c = new GroundCand
|
||||
var off = 0
|
||||
var far = 0
|
||||
for k in 0 .. l.n {
|
||||
expect(ground_solid_at(render3d_st, g, 0.0, 0.0, 32.0, l.ids[k], c))
|
||||
if c.x != l.xs[k] or c.z != l.zs[k] or c.scale != l.scales[k] or c.yaw != l.yaws[k] { off += 1 }
|
||||
if gf_hash(g.layer, GG_CX, GG_CZ, 0, c.cell, 7) < gs_thin(g, 2) { far += 1 }
|
||||
}
|
||||
expect_eq(off, 0)
|
||||
for band in 0 .. 4 {
|
||||
let s = gg_stream(render3d_st)
|
||||
ground_fill(render3d_st, s, GG_CX, GG_CZ, band, g)
|
||||
expect_eq(gft_subset(render3d_st, s.cur.count, l), 0)
|
||||
if band == 0 { expect_eq(s.cur.count, n) }
|
||||
if band == 2 { expect_eq(s.cur.count, far) }
|
||||
}
|
||||
expect(far < n)
|
||||
expect(not ground_solid_at(render3d_st, g, 0.0, 0.0, 32.0, ground_solid_id(g.layer + 1, GG_CX, GG_CZ, 0, 0, 8), c))
|
||||
}
|
||||
|
||||
test "a solid id holds its layer and chunk, and refuses what it cannot hold" {
|
||||
let id = ground_solid_id(63, -128, 127, 21, 21, 22)
|
||||
expect(id > 0)
|
||||
expect_eq(ground_solid_layer(id), 63)
|
||||
expect_eq(ground_solid_cx(id), -128)
|
||||
expect_eq(ground_solid_cz(id), 127)
|
||||
expect_eq(ground_solid_id(0, 128, 0, 0, 0, 8), -1)
|
||||
expect_eq(ground_solid_id(0, 0, 0, 0, 0, 23), -1)
|
||||
expect_eq(ground_solid_id(64, 0, 0, 0, 0, 8), -1)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue