Physics · interface

Heightfield

A heightfield as a collision shape: the samples, and every triangle generated from them.

Explained in Your first game, Rigid bodies.

interface Heightfield
import type { Heightfield } from '@driftengine/physics';

In depth

The whole of this is a memory argument. Terrain already collides, as a static triangle mesh — meshShape takes exactly the arrays a heightfield renderer hands back, so the geometry drawn is the geometry collided with, which is the property that matters most and the reason that path is still the right one for a level. What it costs is the triangles: measured on a 129-square field, the positions and indices alone are more than five times the bytes of the heights they were built from, before the tree over them is counted at all.

A heightfield needs none of it. The samples are the geometry, the cell under a body is an index rather than a tree query, and every triangle, plane and edge classification is arithmetic on four numbers. So this carries the heights and nothing else.

What it is not is a second narrow phase. meshContact.ts owns the contact rules — many manifolds rather than one, one-sided triangles, the interior-edge filter that stops a box stumbling on a flat seam — and this changes none of them. It is a source of triangles, and the only thing it replaces is where they come from: an index range instead of a tree, and four samples instead of an index buffer. That split is the 2026-08-13 rule applied to the one file in this package it would be most expensive to have two of.

The triangulation is stated here and mirrored in @driftengine/terrain, which draws the picture this collides with. It has to be: @driftengine/physics imports no other engine package, so the rule cannot be shared as code. What is shared instead is an assertion — that package's terrainCollision.test.ts compares the two — which is the same arrangement glDepthFunc has with DEPTH_COMPARE for the same reason. Track L already paid once for two copies of this rule disagreeing, when a mesh was split one way and its query read the other, so the mirror is guarded rather than trusted.

Properties

NameTypeDescription
widthreadonlynumberSamples across x. At least two, since one sample is not a cell.
depthreadonlynumberSamples across z.
spacingMreadonlynumberMetres between samples, the same both ways.
heightsreadonlyFloat32Arraywidth * depth heights, row-major with x running fastest. Kept, not copied.
originreadonlyoptionalArrayLike<number>World position of sample (0, 0), or nothing for a field at the origin. Its own y is added to every height.
More

Three numbers in an array rather than three fields, and that is the boundary doing its work. @driftengine/physics imports no other engine package, so it cannot know what a Terrain is — but a Terrain is one of these, structurally: same names, same meanings, an origin that is a Float32Array. So heightfieldShape(terrain) type-checks with no import in either direction, and the seam is a shape rather than a dependency.