Raft
A raft made of cloth floating on a swell with cargo on it. Drag the deck under a sounding line that stops at it, or through a harbour gate that reports the raft passing. This is a soft body as a member of the world. Shows SoftBody3D.density, world.contacts naming a cloth, raft.touching, and raycast seeing a cloth.
- Run
swift run --package-path Examples Example-3D-Physics-Raft- Imports
OllinPhysics- Source
- Examples/3D/Physics/Raft/Sketch.swift
import Ollin
import OllinPhysics
/// A raft made of cloth, floating on a swell with cargo on it. **Drag** the deck to
/// sail her: under the sounding line, or through the harbor gate. **Space**
/// drops another crate.
///
/// The showcase for a soft body being part of the world rather than a thing
/// draped over it, in three ways. It **floats**: `world.water` pushes each of
/// its particles up on its own, so `density` decides how high she floats even
/// though a sheet encloses no volume to work a waterline out of. It **turns up
/// in `world.contacts`**: a crate landing on the deck reports where and how
/// hard, the gate is a sensor that sees her sail in, and `raft.touching` counts
/// what is aboard. And it is **something a query can find**: the sounding line
/// stops at the deck when she is under it, rather than passing through her.
@main
final class Raft: Sketch {
let world = World3D()
var raft: SoftBody3D?
var gate: Body3D?
var grip: SoftGrip?
var crateGrip: Joint3D?
/// How heavy the deck is for its size, against the water's own weight.
/// Lower floats higher and drier.
@Param(0.1 ... 0.9, icon: "square.stack.3d.up") var deckDensity = 0.25
/// How high the swell runs, in world units.
@Param(0 ... 0.4, icon: "water.waves") var swell = 0.1
/// One box of cargo, kept so the same numbers build it and draw it.
struct Crate {
var body: Body3D
var size: Double
var color: Color
}
var crates: [Crate] = []
/// A ring left where something hit the water or the deck, sized by how hard
/// it landed.
struct Splash {
var at: Vector3
var strength: Double
var age = 0.0
}
var splashes: [Splash] = []
let deep = -5.0
let deckSize = 4.0
let soundingPost = Vector3(-4.4, 0, -0.6)
let gateX = 4.6
let palette = [Color(hex: 0xD9A441), Color(hex: 0xC1553F), Color(hex: 0x8FA860)]
override func setup() {
world.ground = deep
world.water = Water(level: 0, linearDrag: 0.4,
waves: Water.Waves(amplitude: swell, wavelength: 9,
speed: 1.4))
buildRaft()
buildHarbour()
for index in 0 ..< 2 { dropCrate(at: Double(index) * 0.9 - 0.45) }
}
/// The deck: an ordinary plane mesh, stiff enough to carry a crate and
/// still loose enough to bend over a wave.
func buildRaft() {
raft = try? world.addSoftBody(from: .plane(width: deckSize, depth: deckSize,
segments: 16),
at: Vector3(0, 0.4, 0),
mass: 300, stiffness: 0.998, bend: 1,
damping: 0.1, friction: 0.95, iterations: 14,
vertexRadius: 0.08)
// A sheet holds no volume, so unlike a crate it cannot work out its own
// weight for its size: this is the number that decides she floats.
raft?.density = deckDensity
}
/// The two things watching the water: a sounding line that measures what is
/// under it, and a gate that reports what is inside it.
func buildHarbour() {
world.addBody(.box(width: 0.24, height: 3.4, depth: 0.24),
at: soundingPost + Vector3(0, 1.7, 0), kind: .static)
// The arm the line hangs off, so the sounding drops clear of the post.
world.addBody(.box(width: 1.1, height: 0.16, depth: 0.16),
at: soundingPost + Vector3(0.35, 3.3, 0), kind: .static)
// The gate is a detector, not a wall: the raft sails through it and it
// reports the passage.
gate = world.addBody(.box(width: 0.5, height: 2.4, depth: 6.6),
at: Vector3(gateX, 0, 0), kind: .static,
isSensor: true)
for side in [-1.0, 1.0] {
world.addBody(.cylinder(height: 3.0, radius: 0.16),
at: Vector3(gateX, 1.0, side * 3.4), kind: .static)
}
}
/// A crate from above. It lands, the deck dips under it, and the landing
/// comes back as a contact.
func dropCrate(at x: Double) {
let size = random(0.44, 0.58)
let body = world.addBody(.box(width: size, height: size, depth: size),
at: Vector3(x, 1.7, random(-0.5, 0.5)),
rotated: random(-0.4, 0.4),
axis: Vector3(0.3, 1, 0.2),
density: 0.3, friction: 0.9)
crates.append(Crate(body: body, size: size,
color: palette[crates.count % palette.count]))
}
override func mousePressed() {
// The deck and the crates are taken hold of differently: a crate hangs
// on a joint, a cloth has one of its particles pinned to the cursor.
crateGrip = grabBody(at: mouse, in: world)
if crateGrip == nil {
grip = grabSoftBody(at: mouse, in: world)
}
}
override func mouseReleased() {
crateGrip?.remove()
crateGrip = nil
if let grip { releaseSoftGrip(grip) }
grip = nil
}
override func keyPressed() {
if key == " " { dropCrate(at: random(-0.7, 0.7)) }
}
override func draw() {
background(Color(hex: 0x0A1521))
environment(.sky(turbidity: 3.4, sunElevation: 0.8))
lightingPreset(.goldenHour)
castShadows()
cameraShowcase(.autoOrbit(period: 44), from: Camera3D
.perspective(eye: Vector3(1.2, 5.4, 9.2), target: Vector3(0, 0.2, 0)))
// Both parameters reach the water between steps, so the raft answers them
// while she is floating.
world.water?.waves?.amplitude = swell
raft?.density = deckDensity
if let grip { dragSoftGrip(grip, to: mouse) }
if let crateGrip { dragGrab(crateGrip, to: mouse) }
world.advance(by: deltaTime)
markLandings()
drawSea()
drawHarbour()
drawRaft()
drawCargo()
drawSplashes()
report()
}
/// Every landing in the scene, the deck's included. A soft body reaches the
/// same list as everything else, so this one loop rings a crate hitting the
/// water and a crate hitting the cloth alike.
func markLandings() {
for contact in world.contacts where contact.phase == .began {
guard contact.speed > 0.8 else { continue }
splashes.append(Splash(at: contact.point, strength: contact.speed))
}
for index in splashes.indices { splashes[index].age += deltaTime }
splashes.removeAll { $0.age > 1.1 }
}
/// The sea, drawn from the very surface the raft is floating on.
func drawSea() {
guard let surface = world.waterMesh(extent: 34, resolution: 108) else { return }
fill(Color(hex: 0x2A7188))
material(.dielectric(roughness: 0.3))
drawMesh(surface)
fill(Color(hex: 0x0D1A22))
material(.dielectric(roughness: 0.95))
withState {
translate(0, deep, 0)
drawBox(width: 70, height: 0.2, depth: 70)
}
}
/// The sounding line and the gate. The line is a ray straight down: what it
/// stops at is what is there, and the deck is now one of the answers.
func drawHarbour() {
material(.dielectric(roughness: 0.7))
fill(Color(hex: 0x6B5138))
withState {
translate(soundingPost + Vector3(0, 1.7, 0))
drawBox(width: 0.24, height: 3.4, depth: 0.24)
}
withState {
translate(soundingPost + Vector3(0.35, 3.3, 0))
drawBox(width: 1.1, height: 0.16, depth: 0.16)
}
let from = soundingPost + Vector3(0.65, 3.1, 0)
let sounded = world.raycast(from: from, to: from + Vector3(0, -9, 0))
let onDeck = sounded?.body is SoftBody3D
let landed = sounded?.point ?? from + Vector3(0, -9, 0)
// The line is the ray, drawn: it reaches exactly as far as the query
// says, so it shortens onto the deck when she is under it.
let drop = max(0.01, from.y - landed.y)
fill(onDeck ? Color(hex: 0xF5D06A) : Color(hex: 0x8FB4C4))
material(.dielectric(roughness: 0.4))
withState {
translate(from.x, from.y - drop / 2, from.z)
drawCylinder(radius: 0.035, height: drop)
}
withState {
translate(landed)
drawSphere(radius: onDeck ? 0.13 : 0.08)
}
// The gate lights while the raft is inside it, which is a sensor
// answering for a body that has no pose to ask about.
let passing = gate.map { !$0.touching.isEmpty } ?? false
fill(passing ? Color(hex: 0xF0E2A8) : Color(hex: 0x54606B))
material(.dielectric(roughness: passing ? 0.3 : 0.8))
for side in [-1.0, 1.0] {
withState {
translate(gateX, 1.0, side * 3.4)
drawCylinder(radius: 0.16, height: 3.0)
}
}
}
/// One call: the simulation hands back the shape the deck arrived at.
func drawRaft() {
guard let raft else { return }
fill(Color(hex: 0xC7A87C))
material(.dielectric(roughness: 0.75))
drawSoftBody(raft)
}
func drawCargo() {
material(.dielectric(roughness: 0.65))
for crate in crates {
fill(crate.color)
withBody(crate.body) {
drawBox(width: crate.size, height: crate.size, depth: crate.size)
}
}
}
/// A ring at each landing, spreading and fading with age.
func drawSplashes() {
noFill()
strokeWeight(2)
for splash in splashes {
let life = splash.age / 1.1
let radius = 6 + 34 * life * min(1.4, splash.strength / 3)
stroke(Color(hex: 0xEAF4F7).withAlpha(0.7 * (1 - life)))
withBillboard(at: splash.at) { drawCircle(0, 0, radius) }
}
noStroke()
}
/// What is aboard, read off the deck itself: a soft body keeps a touch list
/// like any other body, and holds it even once it has settled.
func report() {
let aboard = raft.map { deck in
crates.filter { $0.body.isTouching(deck) }.count
} ?? 0
drawCaption("aboard: \(aboard) drag the deck space drops a crate")
}
}