Rigging
Three lines hanging from a gantry, all built from a polyline. One is a limp rope. One is a chain whose links interlock, because every other link is rolled a quarter turn about the rope's own axis. One is a vine stiff enough to hold a curve, with leaves carried along it. Drag any of them, and press space to stop the wind. Shows addRope, Rope3D.segments, withSegment, and bend.
- Run
swift run --package-path Examples Example-3D-Physics-Rigging- Imports
OllinPhysics- Source
- Examples/3D/Physics/Rigging/Sketch.swift
import Foundation
import Ollin
import OllinPhysics
/// Rope, and the thing that makes a rope more than a line of points: every
/// segment carries an orientation of its own.
///
/// Three lines hang from the gantry, all built the same way, from a polyline.
/// The **rope** on the left is limp and drawn as a plain tube. The **chain** in
/// the middle is the same rope with a link drawn on each segment, every other
/// one turned a quarter turn about the rope's own axis so that they interlock;
/// that turn is only possible because each rod knows how it is rolled, which a
/// chain of springs never does. The **vine** on the right resists bending, so
/// it holds a curve of its own, and its leaves are carried and twisted by the
/// stem the same way the links are.
///
/// **Drag** any of them. **Space** stills the wind.
@main
final class Rigging: Sketch {
let world = World3D()
var rope: Rope3D!
var chain: Rope3D!
var vine: Rope3D!
var grip: SoftGrip?
/// How hard the air pushes, in newtons. The gusts are what show a limp rope
/// and a stiff stem answering the same push differently.
@Param(0 ... 20, icon: "wind") var wind = 9.0
var blowing = true
static let beamHeight = 3.2
override func setup() {
world.ground = 0
world.restitution = 0.05
// A hanging line is a polyline running straight down from the beam.
// Anything that makes points makes a rope, so this could as easily be a
// Contour, a randomWalk, or a ridge off a Heightfield.
func hangingLine(_ n: Int, spacing: Double) -> [Vector3] {
(0 ..< n).map { Vector3(0, -Double($0) * spacing, 0) }
}
// Long enough that the end of it lies on the floor, which is what
// reads as rope rather than as a dowel.
rope = try? world.addRope(through: hangingLine(56, spacing: 0.09),
at: Vector3(-1.25, Rigging.beamHeight, 0),
radius: 0.035, mass: 1.6,
bend: 0.02, damping: 0.25, friction: 0.7,
pinned: { $0.y > -0.001 })
chain = try? world.addRope(through: hangingLine(20, spacing: 0.15),
at: Vector3(0, Rigging.beamHeight, 0),
radius: 0.05, mass: 4,
bend: 0.06, damping: 0.2, friction: 0.6,
pinned: { $0.y > -0.001 },
// A chain does not stretch, whatever hangs on it.
maxStretch: 1)
// A stem holds a shape of its own, so it is built already curving.
let stem = (0 ..< 26).map { i -> Vector3 in
let t = Double(i) / 25
return Vector3(sin(t * 2.1) * -0.5 * t, -t * 2.1, cos(t * 1.6) * 0.22 * t)
}
vine = try? world.addRope(through: stem,
at: Vector3(1.3, Rigging.beamHeight, 0),
radius: 0.022, mass: 0.5,
bend: 0.62, damping: 0.35,
iterations: 10,
pinned: { $0.y > -0.001 })
}
override func draw() {
background(Color(hex: 0x121820))
environment(.sky(turbidity: 3.4, sunElevation: 0.62))
lightingPreset(.standard)
castShadows()
camera(.perspective(eye: Vector3(0.7, 2.6, 6.6),
target: Vector3(0, 1.6, 0), fieldOfView: .pi / 4.4))
if blowing {
// Gusts, not a steady breeze: two rates that never line up, so the
// three lines are never caught doing the same thing.
// Across the view rather than into it, so a leaning line reads.
let gust = wind * (0.55 + 0.45 * sin(time * 0.9) * cos(time * 0.37))
for line in [rope, chain, vine] {
line?.applyForce(Vector3(gust, 0, gust * 0.3))
}
}
if let grip { dragSoftGrip(grip, to: mouse) }
world.advance(by: deltaTime)
drawSetting()
// The rope: nothing but its own tube.
material(.dielectric(roughness: 0.85))
fill(Color(hex: 0xC8A87A))
drawSoftBody(rope)
drawChain()
drawVine()
drawCaption("a limp rope, a chain of links, a stem that holds its shape drag one, space for the wind")
}
/// A link on every segment, each one turned a quarter turn from the last
/// about the rope's own axis. `withSegment(_:)` stands in the middle of a
/// segment with +y running along it, so a torus laid on its side is a link
/// the rope passes through, and the alternating roll is what interlocks
/// them.
func drawChain() {
material(.metal(roughness: 0.35))
fill(Color(hex: 0x8A94A6))
for segment in chain.segments {
withSegment(segment) {
rotate(.pi / 2, axis: Vector3(1, 0, 0))
if segment.index.isMultiple(of: 2) {
rotate(.pi / 2, axis: Vector3(0, 0, 1))
}
drawTorus(radius: segment.length * 0.62, tube: 0.026,
segments: 20, sides: 8)
}
}
}
/// Leaves along the stem, each one standing off its own segment. They are
/// placed by the rod's frame rather than by the line between neighboring
/// points, so they turn with the stem as it twists rather than only as it
/// bends.
func drawVine() {
material(.dielectric(roughness: 0.7))
for segment in vine.segments {
fill(Color(hex: 0x2F6B33))
withSegment(segment) { drawCylinder(radius: 0.022, height: segment.length * 1.15) }
guard segment.index % 3 == 1, segment.index > 2 else { continue }
let side: Double = segment.index.isMultiple(of: 2) ? 1 : -1
fill(Color(hex: 0x4E9A4A))
withSegment(segment) {
rotate(side * 0.9, axis: Vector3(0, 1, 0))
translate(0.11, 0, 0)
rotate(.pi / 2.6, axis: Vector3(0, 0, 1))
scale(1, 0.12, 0.55)
drawSphere(radius: 0.19, segments: 18, rings: 10)
}
}
}
func drawSetting() {
material(.dielectric(roughness: 0.9))
fill(Color(hex: 0x2A3341))
drawGround(size: 16, thickness: 0.2)
fill(Color(hex: 0x4A4137))
withState {
translate(0, Rigging.beamHeight + 0.1, 0)
drawBox(width: 4.1, height: 0.2, depth: 0.2)
}
for x in [-1.95, 1.95] {
withState {
translate(x, (Rigging.beamHeight + 0.2) / 2, 0)
drawBox(width: 0.18, height: Rigging.beamHeight + 0.2, depth: 0.18)
}
}
}
override func mousePressed() {
grip = grabSoftBody(at: mouse, in: world)
}
override func mouseReleased() {
if let grip { releaseSoftGrip(grip) }
grip = nil
}
override func keyPressed() {
if key == " " { blowing.toggle() }
}
}