Burst
Solids thrown up through the view, each breaking at the top of its arc into pieces that tumble back down. Nobody hits them: every one comes apart on its own. What falls is exactly what went up, with no gap and no overlap. Click a solid to break it early, and space clears the view. Shows Mesh.fractured cutting a solid into convex cells around the point where it gave way. Each cell becomes a body at its own centroid.
- Run
swift run --package-path Examples Example-3D-Physics-Burst- Imports
OllinPhysics- Source
- Examples/3D/Physics/Burst/Sketch.swift
import Ollin
import OllinPhysics
/// Solids thrown up through the view, each breaking at the top of its arc into
/// pieces that tumble back down. Nobody hits them: every one comes apart on its
/// own, and what falls is exactly what went up, in `Mesh.fractured` pieces that
/// fit back together with no gap and no overlap.
///
/// The break is one call. `fractured(into:around:seed:)` cuts the solid into
/// convex cells around the point where it gave way, so the chips are small
/// there and the wedges long away from it. A convex cell is also what a rigid
/// body wants, so each piece goes straight into the world: the body sits at the
/// piece's `centroid`, the solver gets the piece's points as a hull, and the
/// piece keeps the parent's motion plus a push out of the break.
///
/// Every solid here is drawn from the same mesh the solver was handed, whole or
/// broken, so the picture and the physics cannot drift apart.
///
/// **Click** a solid to break it early. **Space** clears the view.
@main
final class Burst3D: Sketch {
let world = World3D()
/// What a body draws as, hung off `Body3D.userData`: its mesh in its own
/// coordinates, its color, and (for a whole solid) the upward speed at
/// which it lets go. A piece carries no such speed and never breaks again.
final class Look {
let mesh: Mesh
let color: Color
let breaksAt: Double?
init(mesh: Mesh, color: Color, breaksAt: Double? = nil) {
self.mesh = mesh
self.color = color
self.breaksAt = breaksAt
}
}
let palette: [Color] = [
Color(hex: 0xE8632F), Color(hex: 0xF2A93B), Color(hex: 0x72BFB2),
Color(hex: 0x8D92E0), Color(hex: 0xDE87B2), Color(hex: 0x9CCB6B),
]
override func setup() {
world.gravity = Vector3(0, -16, 0)
world.restitution = 0.1
}
/// Throw one solid up from under the bottom of the view.
func throwOne() {
guard world.bodies.count < 200 else { return }
let size = random(0.55, 0.95)
// Five convex solids. The collider is the hull of the very mesh that
// gets drawn, so there is one shape here, not a drawn one and a
// simulated one.
let mesh: Mesh
switch Int(random(5)) {
case 0: mesh = .box(size: size * 1.6)
case 1: mesh = .icosphere(radius: size, subdivisions: 1)
case 2: mesh = .icosahedron(radius: size)
case 3: mesh = .dodecahedron(radius: size)
default: mesh = .cone(radius: size, height: size * 2, segments: 18)
}
let solid = world.addBody(.hull(mesh.positions),
at: Vector3(random(-3.4, 3.4), -6, random(-1.2, 1.2)),
rotated: random(.tau),
axis: Vector3(random(-1, 1), random(-1, 1), random(-1, 1)),
friction: 0.5, restitution: 0.1)
solid.velocity = Vector3(random(-1.1, 1.1), random(15.2, 16.6), random(-0.5, 0.5))
solid.angularVelocity = Vector3(random(-3, 3), random(-3, 3), random(-3, 3))
solid.userData = Look(mesh: mesh, color: randomChoice(palette),
breaksAt: random(-1.2, 1.2))
}
/// Break one solid into pieces, each with the parent's motion and a push
/// out of the break.
func burst(_ solid: Body3D, look: Look) {
let turn = solid.rotation
let here = solid.position
let motion = solid.velocity
let spin = solid.angularVelocity
// Where it gives way: off-center, so the cut is not the same twice.
let reach = look.mesh.size.length * 0.22
let impact = look.mesh.centroid
+ Vector3(random(-1, 1), random(-1, 1), random(-1, 1)).normalized * reach
let pieces = look.mesh.fractured(into: Int(random(9, 15)), around: impact,
seed: Int(random(10_000)))
world.remove(solid)
for piece in pieces {
let middle = piece.centroid
let local = piece.mapPositions { $0 - middle }
let shard = world.addBody(.hull(local.positions),
at: here + middle.rotated(by: turn),
rotated: turn.angle, axis: turn.axis,
friction: 0.5, restitution: 0.15)
let push = (middle - impact).normalized.rotated(by: turn) * random(1.2, 3.4)
shard.velocity = motion + push
shard.angularVelocity = spin + Vector3(random(-6, 6), random(-6, 6), random(-6, 6))
shard.userData = Look(mesh: local,
color: look.color.mixed(with: .white, random(0, 0.22)))
}
}
override func mousePressed() {
guard let hit = body(under: mouse, in: world),
let look = hit.body.userData as? Look, look.breaksAt != nil else { return }
burst(hit.body, look: look)
}
override func keyPressed() {
if key == " " { world.removeAll() }
}
override func draw() {
background(Color(hex: 0x0E1116))
lightingPreset(.studio)
perspective(eye: Vector3(0, 1.4, 9.5), target: Vector3(0, 0.6, 0))
if frameCount % 22 == 0 { throwOne() }
world.advance(by: deltaTime)
// Past the top of the arc it lets go: the solid is on its way down, and
// still high enough that the break happens inside the view.
for body in world.bodies {
guard let look = body.userData as? Look, let breaksAt = look.breaksAt else { continue }
if body.velocity.y <= breaksAt && body.position.y > -1.5 {
burst(body, look: look)
}
}
// Anything below the view has had its fall.
for body in world.bodies where body.position.y < -9 {
world.remove(body)
}
material(.dielectric(roughness: 0.45))
for body in world.bodies {
guard let look = body.userData as? Look else { continue }
fill(look.color)
withBody(body) {
drawMesh(look.mesh)
}
}
}
}