SkinnedScene
A scene file's deforming animation: skins that bend meshes and morph targets that blend them. F swaps the glTF tidepool for a USD pond, whose UsdSkel rig is read by Ollin's own parser.
- Run
swift run --package-path Examples Example-3D-Geometry-SkinnedScene- Beside it
- scene.gltf, stage.usda
- Source
- Examples/3D/Geometry/SkinnedScene/Sketch.swift
import Ollin
import Foundation
/// Play a scene file's *deforming* animation: skins that bend meshes and morph
/// targets that blend them. Press **F** to swap the bundled glTF tidepool for
/// its USD twin, a pond, loaded through the very same `loadScene` call.
///
/// Rigid node tracks move whole objects; the deforming tier moves the vertices
/// themselves. A skin poses a mesh through a joint hierarchy (each vertex
/// blending up to four joints by weight), and morph targets displace vertices
/// between authored shapes, mixed by the node's `weights`. Both play through
/// the same `apply(_:at:)` used for rigid tracks, and `drawScene` poses
/// automatically, so nothing new is needed to draw them.
///
/// The bundled tidepool (regenerate it with `Scripts/make-skinned-scene.swift`)
/// sways three kelp blades on four-joint skins, a wave traveling up each, while
/// the anemone pulses on two morph targets: a puff that swells it and a ripple
/// that scallops its rim. Its `weights` are also just a node property: set
/// `scene["anemone"]?.weights = [1, 0]` by hand to pose a blend shape.
///
/// The bundled pond (regenerate it with `Scripts/make-usd-skinned-scene.swift`)
/// is the same tier from the USD side, whose UsdSkel leg runs on the project's
/// own parser (no platform importer carries it): each Skeleton prim's joints
/// become ordinary scene nodes you can pose by name, skinned meshes bend
/// through the shipped deforming tier, and blend-shape weights use the same
/// animation the transform tracks do, so `apply(_:at:)` plays the whole rig
/// with no new API. It exercises the envelope: a sea serpent swaying on a
/// five-joint skinned chain (two blended influences per point, a geometry bind
/// transform) and a lotus breathing on two blend shapes, one dense with normal
/// offsets and one sparse, driven by an animation bound with no skeleton at
/// all.
///
/// Point this at any skinned or morphing scene by setting `OLLIN_SCENE` to its
/// path; a file with neither draws exactly as `AnimatedScene` would.
@main
final class SkinnedScene: Sketch {
override var loopDuration: Double? { showingUSD ? 8 : 6 } // each file's own lap
private var scene: Scene!
/// True while the USD pond is on stage; **F** flips the bundled files.
private var showingUSD = false
/// A scene of the user's own, from `OLLIN_SCENE`; the toggle stays out of its way.
private var overridden = false
override func setup() {
if let path = ProcessInfo.processInfo.environment["OLLIN_SCENE"],
let s = (try? loadScene(path)) {
scene = s
overridden = true
} else {
loadStage()
}
}
/// Both formats come through the same call: only the file name changes.
private func loadStage() {
scene = showingUSD
? (try? Scene(resource: "stage", withExtension: "usda", in: Bundle.module))
: (try? Scene(resource: "scene", withExtension: "gltf", in: Bundle.module))
}
override func keyPressed() {
guard !overridden, key == "f" || key == "F" else { return }
showingUSD.toggle()
loadStage()
}
override func draw() {
background(Color(hex: 0x081016))
cameraControl(from: scene.camera ?? .orbiting(target: Vector3(0, 0.6, 0),
radius: 5, elevation: 0.3))
ambientLight(Color(white: 0.2))
for l in scene.lights { light(l) }
castShadows()
// Sample the authored animation on the sketch clock, wrapped over its
// duration so it loops; the skins and morphs pose along with it.
var clock = ""
if let sway = scene.animations.first, sway.duration > 0 {
let at = time.truncatingRemainder(dividingBy: sway.duration)
scene.apply(sway, at: at)
clock = String(format: " · %.1f s", at)
}
fill(.white) // white fill shows each node's authored material color untouched
drawScene(scene)
let what = showingUSD
? "UsdSkel skins + blend shapes"
: "animation \"" + scene.animations.map { $0.name }.joined(separator: ", ") + "\""
drawCaption("\(scene.name ?? "scene") · \(what)\(clock)"
+ (overridden ? "" : " · F for the \(showingUSD ? "glTF" : "USD") twin"))
}
}