Skip to content

Physics Golf

App: apps/physics_golf/

A side-by-side comparison of restitution (bounciness). Three dynamic spheres are dropped onto the green from the same height, each with a different restitution: 0.0 (dead, red), 0.5 (lively, yellow) and 0.9 (bouncy, green). Despite the name there is no golf here — no club, hole or putting; “the green” is just the floor the balls land on.

The app is not interactive. It reads no input, runs for about 2.2 seconds, takes a screenshot timed to catch the balls at their widest spread of bounce heights, then exits on its own. That makes it double as a headless smoke test of the material setters.

Terminal window
% ./plume3d physics_golf

It writes physics_golf.png and exits with code 0 after ~2.2 s of simulated time. There is nothing to press.

  • Builds the scene procedurally in Wren — no .blend and no Resource.loadBlend. The green and the balls are meshes assembled vertex-by-vertex and handed to Graphics.newMesh(verts, "triangles").
  • The green is two separate things: a static collider via Physics.addStaticBox(_scene, floor, 20, 0.5, 20) at y = -0.5, and a rendered 26 × 20 quad that is only visual. Scene gravity is set explicitly with Physics.setGravity(_scene, 0, -9.81, 0).
  • Each ball is a Physics.addDynamicSphere(_scene, node, 0.4, 0.2) — radius 0.4, mass 0.2 — dropped from y = 4.2 at x = -3.2, 0.0 and 3.2.
  • The only property that varies between the three balls is Physics.setRestitution. Friction (Physics.setFriction(node, 0.4)) and damping (Physics.setDamping(node, 0.05, 0.3)) are identical on all three, so any difference you see is restitution alone.
  • Camera and light are created in code: Scene.addCamera at (0, 4.2, 11) with lookAt(0, 1.2, 0), and a point light (type = 0) with energy = 9.
  • Engine.screenshot("physics_golf.png") then Engine.exit(0) fire once at t > 2.2, one bounce in.
  • Rendering is manual: the ball nodes are read back with node.getPosition() each frame and drawn with Graphics.drawMesh(mesh, null, model) using a hand-built translation matrix, followed by _scene.draw().
  • Shadows are off despite appearances: the light sets castsShadows = true, but draw() calls Graphics.setShadowMappingEnabled(false), so nothing casts a shadow. The flag is set on the light and then switched off at the pipeline level.

Three balls, one varying property — restitution is isolated by holding everything else equal:

addBall(x, restitution, color) {
var r = 0.4
var node = _scene.addNode("ball_%(_balls.count)")
node.setPosition(x, 4.2, 0)
Physics.addDynamicSphere(_scene, node, r, 0.2)
Physics.setRestitution(node, restitution) // the only thing that differs
Physics.setFriction(node, 0.4) // same on all three
Physics.setDamping(node, 0.05, 0.3) // same on all three
_balls.add({ "node": node, "mesh": createSphereMesh(r, 12, 18, color) })
}
// init():
addBall(-3.2, 0.0, [0.85, 0.22, 0.22]) // dead — red
addBall( 0.0, 0.5, [0.92, 0.82, 0.24]) // lively — yellow
addBall( 3.2, 0.9, [0.32, 0.78, 0.38]) // bouncy — green

Screenshot-and-exit, which is what makes it usable as a smoke test:

update(dt) {
_t = _t + dt
if (!_screenshotTaken && _t > 2.2) {
Engine.screenshot("physics_golf.png")
_screenshotTaken = true
Engine.exit(0)
}
}
  • PhysicsaddDynamicSphere, addStaticBox, setRestitution, setFriction, setDamping, setGravity.
  • Enginescreenshot, exit.
  • GraphicsnewMesh, drawMesh, loadShader, useShader, setViewMatrix, setProjectionMatrix, setLights, setShadowMappingEnabled.
  • SceneaddNode, addCamera, addLight, draw.
  • NodesetPosition, getPosition, lookAt.
  • CamerasetFovYRadians, setNearPlane, setFarPlane, setNode.
  • Lighttype, energy, radius, castsShadows.
  • Mathcos, sin (sphere mesh generation).
  • WindowgetWidth, getHeight (aspect ratio).