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.
Run from root
Section titled “Run from root”% ./plume3d physics_golfIt writes physics_golf.png and exits with code 0 after ~2.2 s of simulated time. There is nothing to press.
What it does
Section titled “What it does”- Builds the scene procedurally in Wren — no
.blendand noResource.loadBlend. The green and the balls are meshes assembled vertex-by-vertex and handed toGraphics.newMesh(verts, "triangles"). - The green is two separate things: a static collider via
Physics.addStaticBox(_scene, floor, 20, 0.5, 20)aty = -0.5, and a rendered 26 × 20 quad that is only visual. Scene gravity is set explicitly withPhysics.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 fromy = 4.2atx = -3.2,0.0and3.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.addCameraat(0, 4.2, 11)withlookAt(0, 1.2, 0), and a point light (type = 0) withenergy = 9. Engine.screenshot("physics_golf.png")thenEngine.exit(0)fire once att > 2.2, one bounce in.- Rendering is manual: the ball nodes are read back with
node.getPosition()each frame and drawn withGraphics.drawMesh(mesh, null, model)using a hand-built translation matrix, followed by_scene.draw(). - Shadows are off despite appearances: the light sets
castsShadows = true, butdraw()callsGraphics.setShadowMappingEnabled(false), so nothing casts a shadow. The flag is set on the light and then switched off at the pipeline level.
Key pattern
Section titled “Key pattern”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 — redaddBall( 0.0, 0.5, [0.92, 0.82, 0.24]) // lively — yellowaddBall( 3.2, 0.9, [0.32, 0.78, 0.38]) // bouncy — greenScreenshot-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) }}- Physics —
addDynamicSphere,addStaticBox,setRestitution,setFriction,setDamping,setGravity. - Engine —
screenshot,exit. - Graphics —
newMesh,drawMesh,loadShader,useShader,setViewMatrix,setProjectionMatrix,setLights,setShadowMappingEnabled. - Scene —
addNode,addCamera,addLight,draw. - Node —
setPosition,getPosition,lookAt. - Camera —
setFovYRadians,setNearPlane,setFarPlane,setNode. - Light —
type,energy,radius,castsShadows. - Math —
cos,sin(sphere mesh generation). - Window —
getWidth,getHeight(aspect ratio).