Skip to content

Physics Mesh Terrain

App: apps/physics_mesh_terrain/

Demonstrates the static triangle-mesh collider (Physics.addStaticMesh) on arbitrary concave terrain. A parabolic, U-shaped trough is generated in Wren as a vertex + index grid, and that same geometry is used both as the render mesh and as the physics collider — so what you see is exactly what the ball collides with. A dynamic sphere is dropped onto the slope and rolls down into the bottom of the trough. There is no input and no blend file: the geometry is built procedurally, and the app takes a screenshot at ~2.4s and exits.

Terminal window
% ./plume3d physics_mesh_terrain
  • Builds the terrain in a buildGrid_ helper: a 17 × 9 grid of points over x ∈ [-6, 6], z ∈ [-4, 4], with height from y = 0.14 * x² — a trough that is low at x = 0 and rises on both sides. z does not affect height, so the shape is a constant cross-section channel.
  • Flattens the grid points into a [x, y, z, x, y, z, ...] list and hands it, with the triangle index list, to Physics.addStaticMesh(_scene, terrain, flat, idx) on a terrain node at the origin.
  • Winding matters. Jolt mesh shapes are one-sided, so buildGrid_ emits each cell’s two triangles as (a, d, b) and (a, c, d) — wound so their normals face up, the side the ball collides from. buildRenderMesh_ computes its face normals with the same cross-product convention, so the lit surface and the collider agree.
  • Feeds the same points and indices to buildRenderMesh_, which expands them into non-indexed, flat-shaded triangles (12-float vertices — [x, y, z, nx, ny, nz, r, g, b, a, u, v]) via Graphics.newMesh(verts, "triangles").
  • Drops the ball: a ball node at (4.2, 6, 0) with Physics.addDynamicSphere(_scene, _ball, 0.35, 0.1), tuned with Physics.setRestitution(0.35), Physics.setFriction(0.3) and Physics.setDamping(0.05, 0.2). Gravity is set explicitly with Physics.setGravity(_scene, 0, -9.81, 0).
  • Draws the ball with a procedural UV sphere (createSphereMesh, 12 stacks × 18 slices) positioned each frame by a translation matrix built from _ball.getPosition() — the terrain mesh is drawn with an identity matrix, since its vertices are already in world space.
  • Sets up the camera by hand: _scene.addCamera(), tagged main, setFovYRadians(0.72), near 0.1 / far 200, attached to a cam node at (6.5, 6.0, 12) aimed with camNode.lookAt(0, 1.6, 0).
  • Uses shaders/default (Graphics.loadShader + useShader) and drives view/projection manually per frame — setViewProjectionEnabled(true), setViewMatrix, setProjectionMatrix(aspect) — with aspect from Window.getWidth() / Window.getHeight().
  • After 2.4 seconds — by which point the ball has landed and rolled toward the trough bottom — calls Engine.screenshot("physics_mesh_terrain.png") and then Engine.exit(0).
// Build a point grid + triangle indices, wound for upward normals.
buildGrid_(nx, nz, minX, maxX, minZ, maxZ, heightFn) {
var pts = []
for (iz in 0...nz) {
for (ix in 0...nx) {
var x = minX + (maxX - minX) * ix / (nx - 1)
var z = minZ + (maxZ - minZ) * iz / (nz - 1)
pts.add([x, heightFn.call(x, z), z])
}
}
var idx = []
for (iz in 0...(nz - 1)) {
for (ix in 0...(nx - 1)) {
var a = iz * nx + ix
var b = iz * nx + ix + 1
var c = (iz + 1) * nx + ix
var d = (iz + 1) * nx + ix + 1
idx.add(a); idx.add(d); idx.add(b) // Jolt mesh shapes are one-sided —
idx.add(a); idx.add(c); idx.add(d) // these wind normals upward.
}
}
return [pts, idx]
}
// y = 0.14*x^2 — a trough, low at x = 0.
var grid = buildGrid_(17, 9, -6, 6, -4, 4, Fn.new { |x, z| 0.14 * x * x })
var pts = grid[0]
var idx = grid[1]
// Flatten to [x,y,z, x,y,z, ...] and use it as the collider.
var flat = []
for (p in pts) {
flat.add(p[0])
flat.add(p[1])
flat.add(p[2])
}
var terrain = _scene.addNode("terrain")
terrain.setPosition(0, 0, 0)
Physics.addStaticMesh(_scene, terrain, flat, idx)
// The render mesh is built from the same pts/idx, so visuals match collision.
_terrainMesh = buildRenderMesh_(pts, idx, [0.30, 0.52, 0.34])
  • PhysicsaddStaticMesh, addDynamicSphere, setGravity, setRestitution, setFriction, setDamping.
  • Scenenew, addNode, addCamera, draw.
  • NodesetPosition, getPosition, lookAt.
  • CamerasetTag, setNode, setFovYRadians, setNearPlane, setFarPlane, getViewMatrix, getProjectionMatrix.
  • GraphicsloadShader, useShader, newMesh, drawMesh, setViewProjectionEnabled, setViewMatrix, setProjectionMatrix.
  • WindowgetWidth, getHeight.
  • Mathsqrt, sin, cos.
  • Enginescreenshot, exit.
  • Loggerinfo.