Phaser AE

3D World

Free packv3.0.0-43DWorld

The world every game seen in perspective is built in. A mesh is an ordinary game object with a pose, so everything that works on sprites works on it, and the camera, materials and picking are ready to use.

This is a Free pack. Every phaser.io account can use it in their games.

Released October 14, 2026 · updated October 19, 2026

3D World: a screenshot of its demo
A note about these graphics

Expansion packs don't come with graphics. This screenshot is one way of drawing what the pack does, made for its demo: none of the sprites, tiles or backgrounds in it are in the pack.

That's by design. A pack is a game's logic, mechanics and effects, and it works with whatever art you give it. Some packs draw effects, backdrops or 3D scenes of their own, but none carries a game's sprites.

So the look is yours. Make the art on phaser.io, bring your own, or let your agent draw it in code: the same pack can look like anything.

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 is forward(yaw), exactly.
  • pitch: number — Look pitch, radians; positive looks DOWN (as lookAt sets 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, so x y z is 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 (after distance is applied).

Mesh3dSpec

interface Mesh3dSpec

What world3d.spawn takes on top of the usual spawn fields.

  • mesh: number | string — An id from mesh(), or a primitive name built on the spot.
  • meshOptions?: Record<string, number | boolean> — Options when mesh is 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 at emissive: 4 share a single material and a single draw.
  • metallic?: number — How metal it is, 0..1. Default 0. Interned with emissive and rough.
  • rough?: number — How rough, 0..1 — 0 is a mirror, 1 is chalk. Default 0.75. Interned with emissive and metallic.
  • unlit?: boolean — Draw it at full colour with no shading at all. Default false. Interned with the rest.
  • material?: number — A material id from material(), 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: true faces it fully (a particle, an icon), 'y' turns about y only so it stays upright (a tree, a bush). Pair it with the quad mesh. Default none.
  • pickable?: boolean — Can render3d.pick return 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 from world3d.mesh / defineMesh.
  • meshOptions?: Record<string, number | boolean> — Options when mesh is 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 from world3d.material(); wins over the four above.
  • billboard?: boolean | 'y' — Face the camera: true fully, 'y' turning about y only. Pair it with the quad mesh.
  • pickable?: boolean — Can render3d.pick return 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. pick tests 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 as spawn({ 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 procedural pattern (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's Material3dDef but 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 writing rough or metallic here 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 own lookAt does. Sets yaw, and pitch too unless flat, which keeps a character upright while it turns. This is a one-shot — to keep something facing the pointer every frame, use the aim-3d pack, 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 height h, in full world coordinates, or null when the ray never meets it — the camera looking along the plane, or the plane behind the eye. toGround is 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.