The pack
world3d
function world3d(opts: World3dOptions = {}): Pack<{ world3d: World3dApi }, {}, {}, MeshSpawn>
The 3D world pack: y-up, right-handed, perspective. Registers the world with the kernel, owns the mesh
registry and the camera, and gathers meshes3d tables sorted near→far. Pair it with render-3d (which
draws the tables) and geometry-3d (which builds the meshes).
Helpers
registerMesh3d
function registerMesh3d(k: { registry: { add(kind: string, def: { name: string; id: number; verts: Float32Array; revision: number }): void; list(kind: string): ReadonlyArray<unknown> } }, name: string, verts: Float32Array): number
Register a mesh for the renderer and return the id instances carry. The id comes from the registry's
own length, so every pack that adds geometry — this one, model-files-3d, a voxel or terrain pack —
allocates from ONE source of truth and two of them can never hand out the same id.
A name registered again keeps its id and takes the new vertices (the renderer uploads them again), so a
mesh made in a scene's enter can be made every time the scene runs, and the objects wearing it change
with it. The same vertices again change nothing.
replaceMesh3d
function replaceMesh3d(k: { registry: { list(kind: string): ReadonlyArray<unknown> } }, id: number, verts: Float32Array): void
Replace geometry, including its topology. Renderers resize the GPU allocation on revision changes.
updateMesh3d
function updateMesh3d(k: { registry: { list(kind: string): ReadonlyArray<unknown> } }, id: number, verts: Float32Array): void
Replace a registered mesh's vertices in place and bump its revision, so the renderer re-uploads it.
This is what a CPU-skinned model needs: the same mesh id, posed differently every frame. The vertex count must not change — a skinned pose moves vertices, it does not add or remove them.
Types
Camera3dApi
interface Camera3dApi
The 3D camera: x y z is the eye when distance is 0, and the point of interest when it is not.
x: number— Eye (or focus, when orbiting) x.y: number— Eye (or focus) y — up.z: number— Eye (or focus) z.yaw: number— Look yaw, radians; 0 looks along +z, and increasing it turns RIGHT. It isforward(yaw), exactly.pitch: number— Look pitch, radians; positive looks DOWN (aslookAtsets it, and as the rigs use it).fov: number— Vertical field of view, radians. Default π/3.distance: number— 0 for a first-person eye; above 0 pulls the eye back that far, sox y zis what it orbits.roll: number— Bank about the line of sight, radians; positive rolls the camera RIGHT (clockwise from behind), so the horizon on screen tips up at its right end. Default 0 — and at 0 the view is exactly the unrolled one. A flyer that banks into its turns sets it every step; it is not interpolated between steps, so change it smoothly.lookAt(x: number, y: number, z: number): void— Aim at a world point from where the camera is (sets yaw and pitch).snap(x: number, y: number, z: number): void— Move without interpolating — a cut.readonly eye: { x: number; y: number; z: number }— Where the eye actually is this frame (afterdistanceis applied).
Mesh3dSpec
interface Mesh3dSpec
What world3d.spawn takes on top of the usual spawn fields.
mesh: number | string— An id frommesh(), or a primitive name built on the spot.meshOptions?: Record<string, number | boolean>— Options whenmeshis a name (segments, radii…).id?: string— Object id; default the mesh name.x?: number— World position. Default 0.y?: number— Height; y is UP. Default 0.z?: number— Depth. Default 0.size?: number— Uniform size in world units — shorthand for sx/sy/sz. Default 1.sx?: number— Scale x.sy?: number— Scale y.sz?: number— Scale z.yaw?: number— Yaw about y, radians. Default 0.pitch?: number— Pitch about x, radians. Default 0.roll?: number— Roll about z, radians. Default 0.color?: string— Colour, css; multiplied over the texture when there is one. Default white.emissive?: number— How much light this mesh GIVES OFF, on top of what falls on it. 0 is an ordinary surface; 1 makes it read as lit from within; 3–4 is a lamp, a firefly, a hot coal. A campfire cone, a glowing marker or a bright pickup is this, not a light — an emissive surface is free and lights nothing else, while a point light costs and lights everything. The renderer keys materials by their numbers and hands the same one to every mesh that asks for the same set, so sixty fireflies atemissive: 4share a single material and a single draw.metallic?: number— How metal it is, 0..1. Default 0. Interned withemissiveandrough.rough?: number— How rough, 0..1 — 0 is a mirror, 1 is chalk. Default 0.75. Interned withemissiveandmetallic.unlit?: boolean— Draw it at full colour with no shading at all. Default false. Interned with the rest.material?: number— A material id frommaterial(), when the four shorthands above are not enough. Wins over them.texture?: string— An atlas strip key to wear as a texture — the mesh's own uv (0..1 across the shape) maps into the frame. Untextured by default.textureFrame?: number— Which frame of that strip. Default 0.billboard?: boolean | 'y'— Turn the mesh to face the camera:truefaces it fully (a particle, an icon),'y'turns about y only so it stays upright (a tree, a bush). Pair it with thequadmesh. Default none.pickable?: boolean— Canrender3d.pickreturn it. Default false: a world where every prop is clickable is one where the player selects the floor.alpha?: number— Opacity; below 1 the mesh draws in the transparent pass. Default 1.layer?: LayerName— Layer name. Default the first.tags?: string[]— Tags.
MeshSpawn
interface MeshSpawn
What game.spawn takes for a mesh, beside the usual fields (x y z, yaw, scale, color, tags, data):
game.spawn({ id: 'crate', mesh: 'box', x: 0, y: 0.5, z: 4, size: [2, 1, 1], color: '#c8873a', tags: ['crate'] }).
It is world3d.spawn through the game's own spawn, so packs' spawn data (collide, a body) comes with it.
mesh?: number | string— A primitive by name ('box','sphere','cylinder','cone','capsule','torus','plane'…) or an id fromworld3d.mesh/defineMesh.meshOptions?: Record<string, number | boolean>— Options whenmeshis a name (segments, radii…).size?: number | [number, number, number]— Its size in world units: one number for all three sides, or[w, h, d]. The primitives are one unit across, so this is its scale; a mesh of your own is scaled from the size it was made. Default 1.emissive?: number— How much light it gives off: 0 ordinary, 1 lit from within, 3–4 a lamp. Default 0.metallic?: number— How metal it is, 0..1. Default 0.rough?: number— How rough, 0..1: 0 is a mirror, 1 chalk. Default 0.75.unlit?: boolean— Full colour with no shading. Default false.material?: number— A material id fromworld3d.material(); wins over the four above.billboard?: boolean | 'y'— Face the camera:truefully,'y'turning about y only. Pair it with thequadmesh.pickable?: boolean— Canrender3d.pickreturn it. Default false.
World3dApi
interface World3dApi
game.world3d.
readonly kind: '3d'— Which world this is.readonly layers: ReadonlyArray<LayerName>— Layer names, back to front.readonly camera: Camera3dApi— The camera.mesh(name: string, opts?: Record<string, number | boolean>): number— Build (or reuse) a primitive by name and options; returns the mesh id instances carry.defineMesh(name: string, verts: Float32Array): number— Register your own vertices (pos xyz, normal xyz, uv — 8 floats a vertex) under a name; returns the id. Defining a name again keeps its id and replaces the vertices, so the objects wearing it change with it; the same vertices again change nothing. Any size is fine. Author the mesh in world units — a track 400 across, a wall 30 high — and spawn it at scale 1: the frustum cull and the shadow cascades bound each mesh by how far its vertices reach from its origin, so a big mesh whose origin is off screen still draws while any of it is in view. The bound is a sphere about the origin, so a mesh centred on it is culled most tightly.picktests the box its vertices fill, wherever that box sits.meshVerts(id: number): Float32Array | undefined— A registered mesh's vertices, stride 8, or undefined when that id is not one of this world's. The array is the LIVE one the renderer uploads from, not a copy — read it, do not write it. It is here because geometry has readers outside the renderer: a convex hull collider is built from the shape a mesh actually is, and without this the only way to get it is to keep a second copy of every mesh beside the world, which is how two copies drift apart. It answers for EVERY registered mesh, including the ones a loaded model brought with it — those are registered with the kernel rather than built here, and a model's hull is the main reason to ask.material(def: { name: string } & Partial<Omit<Material3dDef, 'name' | 'id'>>): number— Define a SURFACE — how a mesh answers light — and get the id to pass asspawn({ material }). Most games never need this:spawn({ emissive, metallic, rough, unlit })interns one for you. Reach for it when a material has a texture map, a proceduralpattern(stripes, checks, dice pips…), an sRGB colour map or isotropic filtering on it, or when you want one you can name and reuse. Every field of render-3d'sMaterial3dDefbut its id.materialAt(id: number): { name: string; id: number; metallic: number; rough: number; emissive: number; unlit: boolean } | undefined— A registered material's LIVE definition, or undefined. Its fields are the ones the renderer uploads every frame, so writingroughormetallichere retunes every object wearing that material at once — which is what a material IS: a surface many things share, not a copy each of them took.spawn(spec: Mesh3dSpec): Handle— Spawn a mesh into the world.lookAt(h: Handle, x: number, y: number, z: number, flat?: boolean): void— Turn any object to face a world point, the way the camera's ownlookAtdoes. Sets yaw, and pitch too unlessflat, which keeps a character upright while it turns. This is a one-shot — to keep something facing the pointer every frame, use theaim-3dpack, which also handles turn rate and the cases where the pointer resolves to nothing.toScreen(x: number, y: number, z: number): { x: number; y: number }— World point → logical screen pixels; far off-screen when behind the camera.toGround(sx: number, sy: number): { x: number; y: number }— Screen point → where it meets the y = 0 ground plane, as{ x, y }= world x/z.groundAt(sx: number, sy: number, h: number): { x: number; y: number; z: number } | null— Screen point → where it meets the HORIZONTAL plane at heighth, in full world coordinates, or null when the ray never meets it — the camera looking along the plane, or the plane behind the eye.toGroundis this at h = 0 with the miss folded away; anything aiming at an object that is not on the floor wants this, at that object's own height.screenRay(sx: number, sy: number): { origin: { x: number; y: number; z: number }; dir: { x: number; y: number; z: number } }— Screen point → a world ray, for picking anything that is not the ground.readonly view: View3d— This frame's matrices.readonly culled: number— Objects the frustum dropped last frame.cull: boolean— Enable CPU frustum culling of roster meshes.pick(sx: number, sy: number, filter?: (h: Handle) => boolean): { handle: Handle; distance: number; x: number; y: number; z: number } | null— The nearest object under a screen point, or null. Tests the ray against each object's oriented box: the box its mesh's vertices fill, at the object's scale and turned by its pose, so it is exact for a box and generous for a sphere, and a mesh of any size picks where it is drawn. A billboard tests a unit box at its scale.pickAll(sx: number, sy: number, filter?: (h: Handle) => boolean): ReadonlyArray<{ handle: Handle; distance: number }>— Everything a ray passes through, nearest first.
World3dOptions
interface World3dOptions
Manifest options.
layers?: LayerName[]— Draw layers, back to front. Default ['world'].near?: number— Near clip. Default 0.1.far?: number— Far clip. Default 500.cull?: boolean— Skip objects the camera cannot see. Default true.


