Phaser AE

Deformable Terrain

Pro packv3.0.0-43DDeform

Ground that remembers what happened on it. A board cuts a trench through snow, a kart leaves ruts in sand, a shell opens a crater, and each is a real dent in a real surface, so a rider can ride back into its own line and feel it.

This is a Pro pack. Upgrade to Phaser Pro to use it in your games.See Phaser Pro →

Released October 14, 2026 · updated October 19, 2026

Deformable Terrain: 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

deformTerrain3d

function deformTerrain3d(opts: Partial<Deform3dSurface> = {}): Pack<{ deform: Deform3dApi }>

The deform-terrain-3d pack: a carve window that follows the action, a high-poly patch that renders the near trench as real geometry, and the far trail as a tint and rim normals on the base surface itself. Requires a 3D world and render-3d.

Functions

deformPatchVerts

function deformPatchVerts(cells: number): Float32Array

The high-poly snow-layer PATCH: a unit grid in XZ ([-0.5, 0.5], y = 0), cells² quads as a plain pos+normal+uv soup (the VS displaces it by the baked base height + thickness − carve, so the trench is REAL geometry). uv is 0..1 across the patch. Pure — world3d uploads it once.

Classes

DeformMap

class DeformMap

The square window of carve depths that follows the action. Made by game.deform.surface(). The game stamps dents; depthAt reads them back.

  • readonly res: number — Texels per side of the window.
  • readonly window: number — World units the window covers.
  • readonly maxDepth: number — Maximum carve depth, in world units — stamps clamp here.
  • readonly sink: number — Geometric sink cap on the base terrain, in world units.
  • readonly thickness: number — Depth of the deformable snow/sand layer above the base (see DeformOptions.thickness). heightAt lifts the surface by this.
  • readonly patchWin: number — High-poly patch size in world units (0 = no layer) — the feather radius the RENDERED surface uses; snowSurfaceAt matches it.
  • readonly patchCells: number — Patch grid cells per side (the follow-grid resolution).
  • heal: number — Refill rate, depth-units each second. 0 = trails persist.
  • focusX: number — The point the window and the patch follow, set by track from the pack's carvers: the lead carver's position, or the middle of the whole group when they all fit in the window (the deform WINDOW rides ~30% behind this; the patch sits under the action).
  • focusZ: number — The focus's z position — see focusX.
  • patchCx: number — The patch centre SNAPPED to the cell grid. The follow-grid must land on a fixed world lattice every frame — if it slid continuously with the rider, each vertex would sample a moving world point and the triangulation would SWIM (Rich: the trenches "ripple like water"). Snapping to whole cells makes the mesh world-stable (it re-indexes as it shifts, but the surface is identical).
  • patchCz: number — The patch centre z, snapped to the cell grid — see patchCx.
  • readonly data: Float32Array — res² carve depths (≥ 0), row-major, z-major rows, texel centres.
  • cx: number — Window centre in world units — moves via follow(), quantised to texels.
  • cz: number — Window centre z, in world units — see cx.
  • dirtyX0: number — The first dirty texel column (x), inclusive — the dirty rect [x0, z0) .. [x1, z1) for partial uploads.
  • dirtyZ0: number — The first dirty texel row (z), inclusive.
  • dirtyX1: number — One past the last dirty texel column (x).
  • dirtyZ1: number — One past the last dirty texel row (z).
  • fullDirty: boolean — The whole window changed (shift/heal) — upload everything.
  • constructor(opts: DeformOptions = {})
  • get texel(): number — World units per texel.
  • stamp(x: number, z: number, r: number, d: number): void — Stamp one bowl-profiled dent: depth d at the centre easing to 0 at radius r. MAX-blends with what's there (carving is compaction, not excavation — the same pass twice is one trench, not two deep).
  • line(x0: number, z0: number, x1: number, z1: number, r: number, d: number): void — Stamp a capsule: discs along the segment every half-texel — the board's frame-to-frame trail with no dashed gaps at speed.
  • depthAt(x: number, z: number): number — Carve depth at a world position — the SAME manual bilinear over texel centres the shader runs, including its window-edge fade, so a board at heightAt() sits exactly on the rendered snow. 0 outside the window.
  • follow(x: number, z: number, minShift: number = this.res / 8): boolean — Re-centre the window on (x, z) when it has drifted more than an eighth of the window (or minShift texels, when given) — COPY-ON-SHIFT: content moves by whole texels so every carve keeps its world position; texels scrolling in are fresh snow. Returns true when a shift happened (the texture needs a full upload).
  • holds(x: number, z: number): boolean — Is a world point inside the part of the window that is drawn in full — clear of the fade at its edge? A stamp outside the window is dropped, and one in the fade is cut but only faintly shown, so this is whether a carver at (x, z) leaves a trail.
  • track(points: ReadonlyArray<{ x: number; z: number }>): boolean — Keep the window and the patch on a GROUP of positions — every carver — rather than on one. points[0] is the lead. There is one window of texels, so it can hold the group only while the group fits in it: when the points' spread is within GROUP_FIT of the window's width the window is centred on the middle of the group and every point stays inside it (holds says so); when it is not, the window stays with the lead, and the others are held only if they happen to be inside it. The window rides ~30% behind the focus, as before, and only shifts once it has drifted an eighth of its width; a point still outside it after that (a carver that has only just appeared, or jumped) re-centres it at once. The patch (real geometry, a disc of patchWin / 2) takes the middle of the group when the group fits in it, else the lead. Non-finite points are ignored. Returns true when the whole group fits in the window, so every point is held.
  • healStep(dt: number): void — Uniform refill (sand): every carve shallows by heal·dt. Applied in 0.1 s quanta — every heal tick re-uploads the whole map, and 10 Hz is invisible on a slow drift-shut.
  • clearDirty(): void — Clear the dirty rect and the full-dirty flag after an upload.

Constants

DEFORM_UNDER

const DEFORM_UNDER: Record<DeformSurfaceName, string>

The exposed under-layer, per surface — v2's table, and worth keeping its note: compressed snow is BLUE-white, not grey. One grey default for every surface turns a bright snowfield into a smudge, which is what a single #8d9aa8 did here until it was measured against v2.

PATCH_WGSL

const PATCH_WGSL: "\nstruct Cam {\n viewProj : mat4x4<f32>,\n eye : vec4<f32>,\n sunDir : vec4<f32>, // xyz = direction the light travels, w = intensity\n sunCol : vec4<f32>,\n skyCol : vec4<f32>, // w = ambient strength\n groundCol: vec4<f32>,\n fogCol : vec4<f32>,\n params : vec4<f32>, // shininess, spe... …

WGSL source of the snow-layer patch shader: a dense follow-grid that replaces the base surface around the action and digs the carve into the same heightfield, triangle-exact.

sparkleOf

const sparkleOf = (cfg: { sparkle?: number; surface?: DeformSurfaceName }): number=> …

The glint strength for a surface: 0.6 on snow, nothing on anything else.

It is a function rather than a number read at two call sites because TWO passes draw glints — the base terrain across the whole carve window, and the high-poly patch under the rider — and they have to agree exactly or the patch twinkles against a still field, which reads as the disc sliding over the snow. The base's copy was simply missing for the whole of the v2 port; nothing declared the two had to match.

Types

Carver

interface Carver

A live carver.

  • kill(): void — Stop it cutting and forget it.
  • radius: number — Cut radius, live.
  • depth: number — Cut depth, live.
  • readonly alive: boolean — True until killed.
  • readonly covered: boolean — True while the carve window holds this carver, so its trail is cut and drawn. There is one window, and it follows the whole group of carvers while they fit in it (80% of window); a carver left outside it leaves no trail, covered is false for it, and the pack warns once. Updated each step; true before the first.

CarverOptions

interface CarverOptions

A carver: something that cuts as it moves. Follow an object and it stamps its own trail.

  • radius?: number — Cut radius, world units. Default 0.6.
  • depth?: number — Cut depth, world units — clamped to the map's maximum. Default the map's maximum.
  • when?: () => boolean — Only cut while this returns true — a board carves, a board in the air does not.

Deform3dApi

interface Deform3dApi

game.deform.

  • surface(opts: Deform3dSurface): DeformMap — Make a deformable surface. One at a time; a second replaces the first.
  • carver(target: { x: number; y: number; z: number }, opts?: CarverOptions): Carver — Something that cuts as it moves — follow an object and it stamps its own trail. Any number may be live: the carve window follows all of them while they fit in it, and the carver's covered says whether it does.
  • carve(x: number, z: number, radius: number, depth: number): void — Stamp one dent by hand.
  • depthAt(x: number, z: number): number — The carve depth at a world point — what a rider should subtract from the ground.
  • surfaceAt(x: number, z: number): number — The surface height with the carve taken out — hand this to a rider as its groundAt.
  • readonly map: DeformMap | null — The live map, or null before one is made.
  • patch: boolean — Draw the high-poly patch. Default true.

Deform3dSurface

interface Deform3dSurface extends DeformOptions

What the pack needs to know about the surface being carved.

  • groundAt: (x: number, z: number) => number — The undisturbed surface height at a world point — the terrain's heightAt.
  • size?: number — The terrain's extent, world units. Default 400.
  • baseRes?: number — The resolution the base height is baked at. Default 256.
  • frame?: number — The atlas frame the surface is textured with, so the patch matches it exactly.
  • berm?: number — Berm lift: how far displaced material piles into lips along the cut's skirt. Default 0.09.
  • surface?: DeformSurfaceName — Which surface is being carved. It picks the per-surface defaults for under and sparkle — v2 takes this from the terrain's own surface name, and without it a snowfield carves grey.

DeformOptions

interface DeformOptions

The options that configure a deformable surface. Passed to game.deform.surface().

  • res?: number — Texels per side of the window (default 1024 — 4 MB of r32float).
  • window?: number — World units the window covers (default 480). Texel = window/res — 0.47 u at the defaults. SIZE THIS FOR SPEED: the window rides ~30% behind the carver, so trail persistence ≈ 0.8 × window — a 28 u/s snowboarder keeps ~14 s of line at the default. Texels should stay ≲ the carver radius or stamps fall between them.
  • depth?: number — Maximum carve depth in world units (default 0.6). Stamps clamp here — re-riding a trail compacts it, never digs to bedrock.
  • heal?: number — Refill rate, depth-units/second (default 0 — snow: trails persist). Sand wants ~0.1–0.3 so tracks slowly drift shut.
  • under?: string — Exposed under-layer colour (default per surface: compressed blue-white for snow, dark wet sand, dirt for grass).
  • rim?: number — Trench-edge normal strength (default 1 = physical gradient).
  • sparkle?: number — Snow glint strength (default 0.6 on snow surfaces, else 0): pin-point sub-texel sparks on the sun half-vector, over the whole carve window and dimmed inside a trench, where the crystals have been crushed flat. BOTH passes draw them — the base terrain and the high-poly patch — from the same hash, so the disc of real geometry around the rider twinkles in step with the field it sits in. Take the default through sparkleOf rather than repeating it.
  • sink?: number — GEOMETRIC sink cap on the BASE terrain, world units (default 0.18). The base mesh's verts are metres apart, so it can't render a board-width trench — the far/persistent line is fragment work on it (tint + rim normals) with at most this much real dip. The NEAR trench is real geometry on the high-poly snow layer (see layer).
  • layer?: boolean — SNOW LAYER (E5, the real deformable geometry): a dense follow-grid mesh — the WebGPU stand-in for tessellation (Batman: Arkham Origins / UE RVT do this with hardware tessellation) — that REPLACES the base terrain around the rider: the base discards a disc under it, the patch renders the SAME surface (triangle-exact) minus the carve. One surface, never two → no z-fighting, and the trench is real geometry you can fly a probe line into. false disables it (base-only, the flat fragment-trail look). Default on.
  • thickness?: number — Snow/sand depth available to carve into (default 0.6): conceptually the terrain surface IS the snow top and the ground sits thickness below it — carves clamp here so a trench never reaches bedrock.
  • patch?: number — Layer patch size in world units (default 240 — the real-geometry radius around the rider; beyond it the base-terrain fragment trail carries the line).
  • cells?: number — Grid cells per side of the patch (default 288 → ~0.83 u at the default patch; keep the cell ≲ the carver radius).

DeformSurfaceName

type DeformSurfaceName = 'snow' | 'sand' | 'grass' | 'rock'

The surface being carved, which picks the per-surface defaults below. v2 derives it from the terrain's own surface name; here it is passed, because the deform pack does not otherwise know what it is sitting on.