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.
Run from root
Section titled “Run from root”% ./plume3d physics_heightfieldWhat it does
Section titled “What it does”- Builds a 16 x 16 row-major height grid (
heights[gz * n + gx]) shaped as a bowl: height is(dx*dx + dz*dz) * 0.04measured 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)viaPhysics.addDynamicSphere(radius 0.35, mass 0.1), tuned withPhysics.setRestitution(0.3),Physics.setFriction(0.3)andPhysics.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/setProjectionMatrixfromcamera.getViewMatrix()andcamera.getProjectionMatrix(aspect), aspect fromWindow.getWidth()/getHeight(). - Calls
Engine.screenshot("physics_heightfield.png")thenEngine.exit(0)oncet > 2.8— enough time for the ball to roll in and settle.
Key pattern
Section titled “Key pattern”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 = 16var cell = 0.8var center = (n - 1) / 2var 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 / 2var 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).
- Physics —
addStaticHeightfield,addDynamicSphere,setGravity,setRestitution,setFriction,setDamping. - Scene —
addNode,addCamera,draw. - Node —
setPosition,getPosition,lookAt. - Camera —
setTag,setFovYRadians,setNearPlane,setFarPlane,setNode,getViewMatrix,getProjectionMatrix. - Graphics —
loadShader,useShader,newMesh,drawMesh,setViewMatrix,setProjectionMatrix,setViewProjectionEnabled. - Engine —
screenshot,exit. - Window —
getWidth,getHeight. - Math —
sqrt,cos,sin.