feat: actors stage 2
This commit is contained in:
parent
4869db01e5
commit
581e5892b0
19 changed files with 752 additions and 594 deletions
98
app/level.ts
98
app/level.ts
|
|
@ -55,6 +55,14 @@ type ChunkMaterials = {
|
|||
flower: Material
|
||||
}
|
||||
|
||||
/** A chunk-material key (also the tag props reference, e.g. a tree's `trunk`). */
|
||||
type MatKey = keyof ChunkMaterials
|
||||
|
||||
/** The fixed order draw groups are emitted in (grass first, flowers -- double-sided
|
||||
* -- last), so the per-chunk draw sequence is deterministic and matches the pre-
|
||||
* registry order. Every material key must appear here. */
|
||||
const MAT_ORDER: MatKey[] = ["grass", "rock", "bark", "birch", "leaf", "needle", "flower"]
|
||||
|
||||
/** The playground: a flat-floored room dropped into the center of a big open
|
||||
* landscape. The room (floor/walls/crate) is small and always drawn; the
|
||||
* outdoor world is split into `chunks` that are frustum-culled per frame. */
|
||||
|
|
@ -249,74 +257,43 @@ function buildChunks(m: ChunkMaterials, trees: Tree[], boulders: Boulder[], bush
|
|||
for (let cj = 0; cj < CHUNK_GRID; cj++) {
|
||||
const z0 = -TERRAIN.outer + cj * cell
|
||||
const z1 = z0 + cell
|
||||
const grass = mesh()
|
||||
const bark = mesh()
|
||||
const birchBark = mesh()
|
||||
const leaf = mesh()
|
||||
const needle = mesh()
|
||||
const rock = mesh()
|
||||
const flowerMesh = mesh()
|
||||
const barkFar = mesh()
|
||||
const birchBarkFar = mesh()
|
||||
const leafFar = mesh()
|
||||
const needleFar = mesh()
|
||||
const rockFar = mesh()
|
||||
// Accumulate geometry into one mesh per material key, for the near (full) and
|
||||
// far (impostor) LOD sets. Props declare which material(s) they write, so the
|
||||
// baker never names a texture -- adding a species/material touches no code here.
|
||||
const near = new Map<string, Mesh>()
|
||||
const far = new Map<string, Mesh>()
|
||||
const grass = matMesh(near, "grass")
|
||||
far.set("grass", grass) // the ground is drawn in both LOD sets
|
||||
Terrain.patch(TERRAIN, grass, x0, z0, x1, z1, TERRAIN_SUBDIV, TERRAIN_SUBDIV, GROUND_UV)
|
||||
for (const tree of trees) {
|
||||
if (inCell(tree.position, x0, z0, x1, z1)) {
|
||||
// Birch trunks go to their own white-bark mesh; oak + birch share the oak
|
||||
// leaf foliage, spruce keeps its needles.
|
||||
const trunk = tree.kind === "birch" ? birchBark : bark
|
||||
const trunkFar = tree.kind === "birch" ? birchBarkFar : barkFar
|
||||
const foliage = tree.kind === "spruce" ? needle : leaf
|
||||
const foliageFar = tree.kind === "spruce" ? needleFar : leafFar
|
||||
Tree.build(tree, trunk, foliage)
|
||||
Tree.build(tree, trunkFar, foliageFar, "impostor")
|
||||
const s = Tree.species(tree.kind)
|
||||
Tree.build(tree, matMesh(near, s.trunk), matMesh(near, s.foliage))
|
||||
Tree.build(tree, matMesh(far, s.trunk), matMesh(far, s.foliage), "impostor")
|
||||
}
|
||||
}
|
||||
for (const boulder of boulders) {
|
||||
if (inCell(boulder.position, x0, z0, x1, z1)) {
|
||||
Boulder.build(boulder, rock)
|
||||
Boulder.build(boulder, rockFar, "impostor")
|
||||
Boulder.build(boulder, matMesh(near, "rock"))
|
||||
Boulder.build(boulder, matMesh(far, "rock"), "impostor")
|
||||
}
|
||||
}
|
||||
// Bushes share the near leaf mesh; they just drop out past lodDistance.
|
||||
// Bushes fold into the near leaf mesh; they just drop out past lodDistance.
|
||||
for (const bush of bushes) {
|
||||
if (inCell(bush.position, x0, z0, x1, z1)) {
|
||||
Bush.build(bush, leaf)
|
||||
Bush.build(bush, matMesh(near, "leaf"))
|
||||
}
|
||||
}
|
||||
for (const flower of flowers) {
|
||||
if (inCell(flower.position, x0, z0, x1, z1)) {
|
||||
Flower.build(flower, flowerMesh)
|
||||
Flower.build(flower, matMesh(near, "flower"))
|
||||
}
|
||||
}
|
||||
const b = bounds([grass, bark, birchBark, leaf, needle, rock, flowerMesh])
|
||||
const b = bounds([...near.values()])
|
||||
if (b === null) {
|
||||
continue
|
||||
}
|
||||
// Same draws as before, just described as data. Order is preserved (it
|
||||
// matches the old fixed sequence): grass, then the solid props, then the
|
||||
// double-sided flowers. Empty meshes are pruned so a chunk only carries the
|
||||
// groups it actually has.
|
||||
const near = drawGroups([
|
||||
[grass, m.grass],
|
||||
[rock, m.rock],
|
||||
[bark, m.bark],
|
||||
[birchBark, m.birch],
|
||||
[leaf, m.leaf],
|
||||
[needle, m.needle],
|
||||
[flowerMesh, m.flower],
|
||||
])
|
||||
const far = drawGroups([
|
||||
[grass, m.grass],
|
||||
[rockFar, m.rock],
|
||||
[barkFar, m.bark],
|
||||
[birchBarkFar, m.birch],
|
||||
[leafFar, m.leaf],
|
||||
[needleFar, m.needle],
|
||||
])
|
||||
chunks.push({ ...b, near, far })
|
||||
chunks.push({ ...b, near: toGroups(near, m), far: toGroups(far, m) })
|
||||
}
|
||||
}
|
||||
return chunks
|
||||
|
|
@ -326,13 +303,26 @@ function inCell(p: { x: number; z: number }, x0: number, z0: number, x1: number,
|
|||
return p.x >= x0 && p.x < x1 && p.z >= z0 && p.z < z1
|
||||
}
|
||||
|
||||
/** Pair meshes with their materials into a draw-group list, dropping any mesh
|
||||
* that ended up empty (a cell rarely holds every prop kind). */
|
||||
function drawGroups(pairs: [Mesh, Material][]): DrawGroup[] {
|
||||
/** Lazily get (creating on first use) the accumulation mesh for a material key in a
|
||||
* chunk's near/far map. Props write into these by key, so the baker stays generic. */
|
||||
function matMesh(map: Map<string, Mesh>, key: string): Mesh {
|
||||
let m = map.get(key)
|
||||
if (m === undefined) {
|
||||
m = mesh()
|
||||
map.set(key, m)
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
/** Turn a chunk's per-material meshes into a draw-group list, in a fixed material
|
||||
* order (so the draw sequence is deterministic across bakes) and dropping any that
|
||||
* ended up empty (a cell rarely holds every prop kind). */
|
||||
function toGroups(map: Map<string, Mesh>, materials: ChunkMaterials): DrawGroup[] {
|
||||
const out: DrawGroup[] = []
|
||||
for (const [m, material] of pairs) {
|
||||
if (m.indices.length > 0) {
|
||||
out.push({ mesh: m, material })
|
||||
for (const key of MAT_ORDER) {
|
||||
const m = map.get(key)
|
||||
if (m !== undefined && m.indices.length > 0) {
|
||||
out.push({ mesh: m, material: materials[key] })
|
||||
}
|
||||
}
|
||||
return out
|
||||
|
|
|
|||
21
app/main.ts
21
app/main.ts
|
|
@ -1,7 +1,7 @@
|
|||
import { RenderConfig } from "../engine/render/RenderConfig"
|
||||
import { Camera } from "../engine/scene/Camera"
|
||||
import type { Mesh } from "../engine/scene/Mesh"
|
||||
import { Mob } from "../engine/scene/Mob"
|
||||
import { Mob, MOB_KINDS, type MobKind } from "../engine/scene/Mob"
|
||||
import type { Vec3 } from "../engine/math/Vec3"
|
||||
import { loadTextures } from "./assets"
|
||||
import { buildLevel, type Level } from "./level"
|
||||
|
|
@ -48,21 +48,22 @@ function benchStats(a: number[]): { median: number; p95: number; max: number; me
|
|||
async function main(): Promise<void> {
|
||||
const textures = await loadTextures()
|
||||
const level = buildLevel(textures)
|
||||
// Two canonical mob meshes, built once and shared by every instance (the sim
|
||||
// supplies each mob's per-frame transform).
|
||||
const frogMesh: Mesh = { verts: [], indices: [] }
|
||||
const beeMesh: Mesh = { verts: [], indices: [] }
|
||||
const robinMesh: Mesh = { verts: [], indices: [] }
|
||||
Mob.build("frog", frogMesh)
|
||||
Mob.build("bee", beeMesh)
|
||||
Mob.build("robin", robinMesh)
|
||||
// Build each kind's canonical mesh once, shared by every instance (the sim
|
||||
// supplies each mob's per-frame transform). Registry-driven -- a new kind needs
|
||||
// no change here.
|
||||
const mobMesh = {} as Record<MobKind, Mesh>
|
||||
for (const kind of MOB_KINDS) {
|
||||
const m: Mesh = { verts: [], indices: [] }
|
||||
Mob.build(kind, m)
|
||||
mobMesh[kind] = m
|
||||
}
|
||||
const scene: Scene = {
|
||||
chunks: level.chunks,
|
||||
floor: level.floor,
|
||||
walls: level.walls,
|
||||
crate: level.crate,
|
||||
npc: { position: level.npcPosition, size: { x: 1.1, y: 1.5 } },
|
||||
mobMesh: { frog: frogMesh, bee: beeMesh, robin: robinMesh },
|
||||
mobMesh,
|
||||
mobCount: level.mobs.length,
|
||||
sky: level.sky,
|
||||
textures,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import type { Framebuffer } from "../engine/render/Framebuffer"
|
||||
import type { RenderConfig } from "../engine/render/RenderConfig"
|
||||
import { renderBand, MOB_FLOATS, MOB_KINDS, type MobDraw, type Scene } from "./renderScene"
|
||||
import { MOB_KINDS } from "../engine/scene/Mob"
|
||||
import { renderBand, MOB_FLOATS, type MobDraw, type Scene } from "./renderScene"
|
||||
|
||||
/** One-time setup: shared framebuffer + control/param buffers, the (cloned)
|
||||
* scene, this worker's row band, and its index into the per-worker times array. */
|
||||
|
|
|
|||
|
|
@ -36,11 +36,7 @@ export type Scene = {
|
|||
* `renderBand` (single-thread) or packed into the shared `mobState` buffer and
|
||||
* rebuilt in each worker. `MOB_FLOATS` is that packed layout's stride. */
|
||||
export type MobDraw = { kind: MobKind; x: number; y: number; z: number; heading: number; scale: number }
|
||||
export const MOB_FLOATS = 6 // kind index, x, y, z, heading, scale
|
||||
/** Canonical kind order -- the index packed into the shared `mobState` buffer
|
||||
* (main packs `indexOf`, each worker reads it back). Keep frog/bee first so the
|
||||
* existing indices don't shift. */
|
||||
export const MOB_KINDS: MobKind[] = ["frog", "bee", "robin"]
|
||||
export const MOB_FLOATS = 6 // kind index (into MOB_KINDS), x, y, z, heading, scale
|
||||
|
||||
/** Chunk indices whose bounding box is inside the view frustum. Computed once on
|
||||
* the main thread and shared with every worker (so they don't each re-cull). */
|
||||
|
|
|
|||
|
|
@ -2,7 +2,8 @@ import { Framebuffer } from "../engine/render/Framebuffer"
|
|||
import type { RenderConfig } from "../engine/render/RenderConfig"
|
||||
import type { Mat4 } from "../engine/math/Mat4"
|
||||
import type { Camera } from "../engine/scene/Camera"
|
||||
import { renderBand, MOB_FLOATS, MOB_KINDS, type MobDraw, type Scene } from "./renderScene"
|
||||
import { MOB_KINDS } from "../engine/scene/Mob"
|
||||
import { renderBand, MOB_FLOATS, type MobDraw, type Scene } from "./renderScene"
|
||||
|
||||
/** Sky is drawn at 1/SKY_STEP resolution; band splits align to it. */
|
||||
const SKY_STEP = 2
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue