Ollin

Tumble

A rain of mixed solids (boxes, balls, capsules, drums) piling up. Drag any shape to fling it. Shows the Collider3D catalog plus grabBody/dragGrab.

Run
swift run --package-path Examples Example-3D-Physics-Tumble
Imports
OllinPhysics
Source
Examples/3D/Physics/Tumble/Sketch.swift
import Ollin
import OllinPhysics

/// A slow rain of solids (crates, balls, capsules, drums) piling up on the
/// floor, and a hand that can reach in: **drag** any shape to fling it around,
/// and the pile makes room; **space** clears it. The mixed-collider showcase
/// for `World3D`: every form is one `addBody` with a different `Collider3D`,
/// drawn by matching a mesh to `body.collider` inside `withBody`.
///
/// Grabbing is the two sketch calls: `grabBody(at:in:)` ray-picks the body
/// under the cursor through the camera and hangs it on a soft drag spring,
/// `dragGrab(_:to:)` pulls it along the cursor at the depth it was picked.
/// The camera stays hand-set here so the mouse belongs to the pile.
@main
final class Tumble3D: Sketch {
    let world = World3D()
    let maxBodies = 90
    var grabbed: Joint3D?

    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.ground = 0
        world.restitution = 0.15
    }

    /// Drop one random solid from above the pile with a little spin.
    func spawn() {
        guard world.bodies.count < maxBodies else { return }
        let collider: Collider3D
        switch Int(random(4)) {
        case 0: collider = .box(width: random(0.5, 0.9), height: random(0.4, 0.8),
                                depth: random(0.5, 0.9))
        case 1: collider = .sphere(radius: random(0.25, 0.45))
        case 2: collider = .capsule(height: random(0.4, 0.8), radius: random(0.16, 0.26))
        default: collider = .cylinder(height: random(0.4, 0.8), radius: random(0.22, 0.38))
        }
        let body = world.addBody(collider,
                                 at: Vector3(random(-1.6, 1.6), random(6, 8), random(-1.6, 1.6)),
                                 rotated: random(.tau),
                                 axis: Vector3(random(-1, 1), random(-1, 1), random(-1, 1)),
                                 friction: 0.5, restitution: random(0.05, 0.3))
        body.angularVelocity = Vector3(random(-3, 3), random(-3, 3), random(-3, 3))
        body.userData = randomChoice(palette)
    }

    override func mousePressed() {
        grabbed = grabBody(at: mouse, in: world)
    }

    override func mouseReleased() {
        grabbed?.remove()
        grabbed = nil
    }

    override func keyPressed() {
        if key == " " { world.removeAll() }
    }

    override func draw() {
        background(Color(hex: 0x10131A))
        lightingPreset(.studio)
        castShadows()
        perspective(eye: Vector3(5.2, 4.6, 7.8), target: Vector3(0, 1.2, 0))

        if frameCount % 18 == 0 { spawn() }
        if let grabbed { dragGrab(grabbed, to: mouse) }
        world.advance(by: deltaTime)

        fill(Color(hex: 0x1C222C))
        material(.dielectric(roughness: 0.85))
        drawGround(size: 24)

        material(.dielectric(roughness: 0.5))
        for body in world.bodies {
            fill(body.userData as? Color ?? .white)
            drawBody(body)
        }
    }
}

↑↓ move ↩ open