Yard
A whole yard saved and restored: a truck you drive with the arrows, a figure pacing across it, a second figure lying where it fell, and a banner strung up over it. S writes the lot to a file, and L reads it back, so a restore comes back mid-drive and mid-stride. The terrain floor and the cloth are named rather than held, and the figure's skin stays the sketch's own asset. R also puts back the exact arrangement the yard settled into, before anything is saved. Shows snapshot(), assetName, restore(_:resolving:), world.vehicles, world.characters, world.ragdolls, and world.softBodies.
- Run
swift run --package-path Examples Example-3D-Physics-Yard- Imports
OllinPhysics- Beside it
- figure.gltf
- Source
- Examples/3D/Physics/Yard/Sketch.swift
import Foundation
import Ollin
import OllinPhysics
/// A yard with a truck in it, a figure pacing across it, and a heap of crates
/// to knock about. Then the whole yard is put in a file, and comes back.
///
/// A saved world holds more than its loose crates. The truck comes back with
/// its wheels still turning at the speed they were turning, in the gear the
/// box had picked, so a restore mid-drive carries on rather than pulling away
/// from rest. The walker comes back mid-stride. The figure comes back where it
/// fell, with the capsules that were fitted to its mesh.
///
/// - **Arrow keys** drive the truck. Run it into the crates, or into the
/// figure lying by the wall.
/// - **S** writes the yard to a file, **L** reads it back, **R** puts back the
/// moment the yard was last saved. The file is loaded at startup whenever
/// one is found, so quitting and running again finds the yard as you left it.
/// - **N** clears it out and lays a fresh one.
///
/// **R** works before anything is saved, too: the yard keeps a snapshot of the
/// moment it first settled, so a wrecked yard comes back exactly, down to
/// which crate leans on which. Settling again would not do that. The solver
/// runs in floating point, and a toppling stack turns a last-bit difference
/// into a different arrangement; a snapshot has nothing left to compute.
///
/// The one thing the file does not hold is the figure's skin. That is the
/// sketch's own asset, loaded from the sketch's own bundle, and the snapshot
/// holds only what the solver was built from: a capsule per joint, the tree
/// they hang in, and how far each may bend. So `figure.apply(walker)` still
/// draws the mesh over the restored bodies, exactly as it did before.
///
/// Two things in the yard say the same thing out loud. The ground the yard is
/// cut into is a heightfield, and the banner strung across it is a cloth, and
/// both are made of more numbers than everything else here put together. So
/// each is given an `assetName`, the file writes the name down instead of the
/// geometry, and the sketch says what the names mean on the way back in. That
/// is the trade: a snapshot that names nothing is self-contained and can be
/// committed beside a sketch, and one that names things is a fraction of the
/// size and needs the sketch to hand its assets over.
@main
final class Yard: Sketch {
let world = World3D()
/// The figure's skin: the sketch's asset, not the snapshot's. One copy is
/// posed from the fallen figure every frame; the other stays as it was
/// loaded, so the pacer has a body to draw.
var skin: Scene!
var standing: Scene!
/// The two heavy pieces of geometry, which the file names rather than
/// holds. They are built here, once, and handed back when it asks.
let ground = Heightfield.diamondSquare(size: 65, roughness: 0.5, seed: 6)
let bannerMesh = Mesh.plane(width: 5, depth: 2.4, segments: 18)
var groundMesh: Mesh!
var banner: SoftBody3D?
/// The animation the file ships with, so the pacer is not a mannequin.
var idle: SceneAnimation?
var truck: Vehicle3D?
var pacer: Character3D?
var fallen: Ragdoll3D?
/// The yard as it stood when it was last saved.
var kept: PhysicsSnapshot?
/// Where **S** writes and **L** reads.
let file = FileManager.default.temporaryDirectory
.appendingPathComponent("ollin-yard.physics")
var message = ""
var messageUntil = 0.0
/// Which way the pacer is walking, so it turns at the end of its beat.
var pacing = 1.0
let crateColors: [Color] = [Color(hex: 0xB98B54), Color(hex: 0xA97B49),
Color(hex: 0xC59A63), Color(hex: 0x94693D)]
let paint = Color(hex: 0x3F6E8C)
let rubber = Color(hex: 0x22242A)
let cloth = Color(hex: 0xD8CBB4)
override func setup() {
skin = try? Scene(resource: "figure", withExtension: "gltf", in: Bundle.module)
standing = skin
idle = skin.animations.first
groundMesh = ground.mesh(width: 40, depth: 34, height: 1.6)
world.ground = nil
world.restitution = 0.1
do {
try world.load(contentsOf: file, resolving: asset)
adopt()
kept = try? PhysicsSnapshot(contentsOf: file)
say("loaded the yard from the last run")
} catch {
build()
}
}
// MARK: Laying out a yard
/// What each name the snapshot wrote down actually is. The sketch stays
/// the source of truth about where its own geometry lives.
func asset(_ name: String) -> PhysicsAsset? {
switch name {
case "yard": return .heightfield(ground)
case "banner": return .mesh(bannerMesh)
default: return nil
}
}
func build() {
world.removeAll()
// The yard's own ground: a heightfield, which is thousands of numbers,
// so it is named rather than written into the file.
let floor = world.addBody(.heightfield(ground, width: 40, depth: 34,
height: 1.6),
at: Vector3(0, -0.8, 0), kind: .static,
friction: 0.9)
floor.assetName = "yard"
// The walls, so nothing that gets hit leaves.
world.addBody(.box(width: 22, height: 1.4, depth: 0.5),
at: Vector3(0, 0.7, -6), kind: .static, friction: 0.8)
world.addBody(.box(width: 0.5, height: 1.4, depth: 12),
at: Vector3(-9, 0.7, 0), kind: .static, friction: 0.8)
// A stack to drive into.
for i in 0 ..< 12 {
let row = i / 3, column = i % 3
world.addBody(.box(width: 0.8, height: 0.8, depth: 0.8),
at: Vector3(Double(column) * 0.85 - 3.15,
0.41 + Double(row) * 0.81, -4.3),
density: 0.6, friction: 0.7)
}
let wheels = [Vector3(0.85, -0.28, 1.2), Vector3(-0.85, -0.28, 1.2),
Vector3(0.85, -0.28, -1.2), Vector3(-0.85, -0.28, -1.2)]
.enumerated().map { index, mount -> Wheel3D in
let wheel = Wheel3D.wheel(at: mount, radius: 0.38, width: 0.28,
steers: index < 2, driven: index >= 2,
handBrake: index >= 2)
// A shorter spring than the default, so the body sits down on
// its wheels rather than up on stilts.
wheel.suspensionLength = 0.26
wheel.suspensionTravel = 0.2
return wheel
}
truck = try? world.addVehicle(.box(width: 1.7, height: 0.7, depth: 3.4),
at: Vector3(2.4, 1.3, 2.4), wheels: wheels,
mass: 1400, engineTorque: 520, topSpeed: 16,
rotated: .pi, axis: .unitY)
// Parked while the yard settles, or it rolls away down the slope
// before anyone has touched it.
truck?.handBrake = 1
pacer = world.addCharacter(radius: 0.3, height: 1.75,
at: Vector3(-5.5, 0.1, 1.6))
// A second figure, dropped from a height so it lands in a heap: the
// arrangement a snapshot exists to keep.
fallen = try? world.addRagdoll(from: skin, at: Vector3(-0.6, 2.4, -1.4),
mass: 68, friction: 0.7)
// A banner strung between two posts. A soft body *is* its mesh, so a
// name is what makes it saveable at all.
banner = try? world.addSoftBody(from: bannerMesh, at: Vector3(2.9, 2.7, -3.9),
mass: 1.2, stiffness: 0.7, damping: 0.2,
pinned: { $0.z < -1.1 && abs($0.x) > 2.2 })
banner?.assetName = "banner"
for _ in 0 ..< 300 { world.advance(by: 1.0 / 60) }
kept = world.snapshot()
say("a fresh yard")
}
/// After a restore the objects are new ones, so the sketch takes them from
/// the world again rather than holding on to what it had.
func adopt() {
truck = world.vehicles.first
pacer = world.characters.first
fallen = world.ragdolls.first
banner = world.softBodies.first
}
// MARK: Keeping it
override func keyPressed() {
switch key?.lowercased() ?? "" {
case "s":
do {
try world.save(to: file)
kept = world.snapshot()
say("saved to \(file.path)")
} catch {
say("could not write \(file.path)")
}
case "l":
do {
try world.load(contentsOf: file, resolving: asset)
adopt()
kept = try? PhysicsSnapshot(contentsOf: file)
say("loaded the yard from the file")
} catch {
say("no file yet: press S first")
}
case "r":
guard let kept else { return }
try? world.restore(kept, resolving: asset)
adopt()
say("back to the yard as it was saved")
case "n":
build()
default:
break
}
}
func say(_ text: String) {
message = text
messageUntil = time + 5
}
// MARK: Drawing
override func draw() {
background(Color(hex: 0x151A21))
environment(.sky(turbidity: 3.6, sunElevation: 0.36))
lightingPreset(.goldenHour)
castShadows()
perspective(eye: Vector3(7.4, 6.2, 12.2), target: Vector3(-0.7, 0.9, -1.4))
drive()
walk()
if let idle {
standing.apply(idle, at: time.truncatingRemainder(dividingBy: idle.duration))
}
world.advance(by: deltaTime)
if let fallen { skin.apply(fallen) }
drawGround()
drawBanner()
drawCrates()
drawTruck()
drawPacer()
drawScene(skin)
drawCaption(time < messageUntil
? message
: "arrows drive S save L load R saved yard"
+ " N new yard")
}
/// The truck is driven the way any vehicle is: three numbers set each
/// frame. They are saved too, so a restore comes back under power.
func drive() {
guard let truck else { return }
truck.throttle = (isKeyDown(.upArrow) ? 1 : 0) - (isKeyDown(.downArrow) ? 1 : 0)
truck.steering = (isKeyDown(.rightArrow) ? 1 : 0) - (isKeyDown(.leftArrow) ? 1 : 0)
truck.brake = isKeyDown(" ") ? 1 : 0
// The yard is not flat, so a truck nobody is driving is a truck with
// its parking brake on.
truck.handBrake = truck.throttle == 0 ? 1 : 0
}
/// The pacer walks its own beat, so a restore lands mid-stride rather than
/// at either end of it.
func walk() {
guard let pacer else { return }
if pacer.position.x > -2 { pacing = -1 }
if pacer.position.x < -7 { pacing = 1 }
pacer.walk(at: Vector3(1.6 * pacing, 0, 0))
pacer.facing = pacing > 0 ? .pi / 2 : -.pi / 2
}
/// The yard's ground is the same heightfield the collider was cut from, so
/// the mesh drawn and the surface walked on are one surface.
func drawGround() {
fill(Color(hex: 0x8A7F63))
material(.dielectric(roughness: 1))
withState {
translate(0, -0.8, 0)
drawMesh(groundMesh)
}
}
func drawBanner() {
guard let banner else { return }
fill(Color(hex: 0xC2544A))
material(.dielectric(roughness: 0.85))
drawSoftBody(banner)
}
/// Everything loose in the yard, walls included, drawn out of the collider
/// each body came back with.
func drawCrates() {
for (index, body) in world.bodies.enumerated() {
guard case .box(let width, let height, let depth) = body.collider else {
continue
}
let isWall = body.kind == .static
let isTruck = body === truck?.body
if isTruck { continue }
fill(isWall ? Color(hex: 0x6B6552) : crateColors[index % crateColors.count])
material(.dielectric(roughness: isWall ? 1 : 0.85))
withBody(body) { drawBox(width: width, height: height, depth: depth) }
}
}
func drawTruck() {
guard let truck else { return }
fill(paint)
material(.glossy)
withBody(truck.body) { drawBox(width: 1.7, height: 0.7, depth: 3.4) }
fill(rubber)
material(.dielectric(roughness: 0.9))
for wheel in truck.wheels {
withWheel(wheel) {
drawCylinder(radius: wheel.radius, height: wheel.width)
}
}
}
/// A character is a swept capsule with no mesh of its own, so the pacer
/// is drawn as the same figure as the fallen one, standing in the pose it was
/// loaded in. `withCharacter` puts it on its feet facing the right way.
func drawPacer() {
guard let pacer else { return }
fill(cloth)
material(.dielectric(roughness: 0.8))
withCharacter(pacer) { drawScene(standing) }
}
}