diff --git a/packages/ludic.render3d/env.ludic b/packages/ludic.render3d/env.ludic index 991c1a53..2eeee9df 100644 --- a/packages/ludic.render3d/env.ludic +++ b/packages/ludic.render3d/env.ludic @@ -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 diff --git a/packages/ludic.render3d/ground_cand.ludic b/packages/ludic.render3d/ground_cand.ludic new file mode 100644 index 00000000..c6aa4bf1 --- /dev/null +++ b/packages/ludic.render3d/ground_cand.ludic @@ -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 +} diff --git a/packages/ludic.render3d/ground_clear.ludic b/packages/ludic.render3d/ground_clear.ludic new file mode 100644 index 00000000..9ffc98a5 --- /dev/null +++ b/packages/ludic.render3d/ground_clear.ludic @@ -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 +} diff --git a/packages/ludic.render3d/ground_fill.ludic b/packages/ludic.render3d/ground_fill.ludic index feab1b2f..24821bc3 100644 --- a/packages/ludic.render3d/ground_fill.ludic +++ b/packages/ludic.render3d/ground_fill.ludic @@ -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) } } } diff --git a/packages/ludic.render3d/ground_solid.ludic b/packages/ludic.render3d/ground_solid.ludic new file mode 100644 index 00000000..e52021f6 --- /dev/null +++ b/packages/ludic.render3d/ground_solid.ludic @@ -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) +} diff --git a/packages/ludic.render3d/ground_solid_list.ludic b/packages/ludic.render3d/ground_solid_list.ludic new file mode 100644 index 00000000..a04319c0 --- /dev/null +++ b/packages/ludic.render3d/ground_solid_list.ludic @@ -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 +} diff --git a/packages/ludic.render3d/r3d.ludic b/packages/ludic.render3d/r3d.ludic index c3936e6b..fff3f334 100644 --- a/packages/ludic.render3d/r3d.ludic +++ b/packages/ludic.render3d/r3d.ludic @@ -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"