Skip to content

Physics Heightfield

App: apps/physics_heightfield/

Demonstrates the heightfield terrain collider (Physics.addStaticHeightfield): terrain defined by a square grid of heights rather than by triangles. The same height grid feeds both the render mesh and the physics collider, so the visible surface is the collision surface. A dynamic sphere dropped off-centre rolls down into the bowl. The app is non-interactive — it takes a screenshot at 2.8 seconds and exits.

Terminal window
% ./plume3d physics_heightfield
  • Builds a 16 x 16 row-major height grid (heights[gz * n + gx]) shaped as a bowl: height is (dx*dx + dz*dz) * 0.04 measured from the grid centre, so it is low in the middle and rises to the rim.
  • Registers the collider with Physics.addStaticHeightfield(scene, terrain, heights, 16, 0.8)sampleCount 16, cellSize 0.8, giving a (16-1) * 0.8 = 12 m square of terrain.
  • The grid’s (0,0) sample sits at the node origin and extends +X/+Z, so the terrain node is placed at (-6, 0, -6) to centre the bowl on the world origin.
  • Builds the render mesh from that identical grid with Graphics.newMesh(verts, "triangles"), two triangles per cell, wound so face normals point up.
  • Drops a ball at (2.4, 6, 0) via Physics.addDynamicSphere (radius 0.35, mass 0.1), tuned with Physics.setRestitution(0.3), Physics.setFriction(0.3) and Physics.setDamping(0.05, 0.2) so it settles rather than bouncing out.
  • Sets gravity with Physics.setGravity(scene, 0, -9.81, 0).
  • Draws with an explicit camera: Graphics.setViewMatrix / setProjectionMatrix from camera.getViewMatrix() and camera.getProjectionMatrix(aspect), aspect from Window.getWidth() / getHeight().
  • Calls Engine.screenshot("physics_heightfield.png") then Engine.exit(0) once t > 2.8 — enough time for the ball to roll in and settle.

The grid is authored once and used twice — collider and mesh cannot drift apart:

// sampleCount must be an even integer >= 4; heights must hold exactly n*n values.
var n = 16
var cell = 0.8
var center = (n - 1) / 2
var heights = []
for (gz in 0...n) {
for (gx in 0...n) {
var dx = gx - center
var dz = gz - center
heights.add((dx * dx + dz * dz) * 0.04) // bowl: low centre, high rim
}
}
// The grid corner sits at the node origin and extends +X/+Z, so offset the node
// by half the extent to put the bowl centre on the world origin.
var half = (n - 1) * cell / 2
var terrain = _scene.addNode("terrain")
terrain.setPosition(-half, 0, -half)
Physics.addStaticHeightfield(_scene, terrain, heights, n, cell)
// The same grid drives the render mesh, drawn at the same origin.
_terrainMesh = buildHeightfieldMesh_(heights, n, cell, [0.34, 0.5, 0.36])

sampleCount must be an even integer >= 4 and heights must hold exactly sampleCount * sampleCount values; cellSize must be > 0. Malformed input aborts the fiber with a diagnostic rather than failing silently. Heightfield shapes are static only — there is no dynamic equivalent, because Jolt heightfield shapes cannot move.

addStaticHeightfield is the efficient path for large terrain; use addStaticMesh instead when the surface is not a height grid (overhangs, caves, arbitrary geometry).

  • PhysicsaddStaticHeightfield, addDynamicSphere, setGravity, setRestitution, setFriction, setDamping.
  • SceneaddNode, addCamera, draw.
  • NodesetPosition, getPosition, lookAt.
  • CamerasetTag, setFovYRadians, setNearPlane, setFarPlane, setNode, getViewMatrix, getProjectionMatrix.
  • GraphicsloadShader, useShader, newMesh, drawMesh, setViewMatrix, setProjectionMatrix, setViewProjectionEnabled.
  • Enginescreenshot, exit.
  • WindowgetWidth, getHeight.
  • Mathsqrt, cos, sin.