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 Heightfieldimport 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
| Name | Type | Description |
|---|---|---|
widthreadonly | number | Samples across x. At least two, since one sample is not a cell. |
depthreadonly | number | Samples across z. |
spacingMreadonly | number | Metres between samples, the same both ways. |
heightsreadonly | Float32Array | width * depth heights, row-major with x running fastest. Kept, not copied. |
originreadonlyoptional | ArrayLike<number> | World position of sample (0, 0), or nothing for a field at the origin. Its own y is added to
every height.MoreThree numbers in an array rather than three fields, and that is the boundary doing its work.
|