DepthCompositing
Depth-aware compositing, with a 2D card standing between two point-cloud orbs. The near orb draws over the card, and the far one is hidden behind it. Each orb has its own billboard pin.
- Run
swift run --package-path Examples Example-3D-Depth-DepthCompositing- Source
- Examples/3D/Depth/DepthCompositing/Sketch.swift
import Ollin
/// Depth-aware compositing: 2D drawing placed *inside* a 3D scene, occluding and
/// occluded by it.
///
/// By default a 2D draw in a 3D frame lays over everything — fine for a HUD, wrong
/// for something that lives in the scene. `depth(at:)` puts subsequent 2D drawing
/// at the depth of a world point, so it z-tests against the 3D geometry: hidden
/// where the scene is nearer, drawn over where it's in front. `project(_:)` maps a
/// world point to its place on the canvas, and `withBillboard(at:)` does both —
/// move the origin to a world point and set its depth — so you draw a 2D mark in
/// local coordinates and it lands in space with correct occlusion.
///
/// Here a translucent card stands at the world origin while a ring of glowing orbs
/// orbits through it: the orbs in front draw over the card, the orbs behind are
/// hidden by it. The number pins on the orbs behave the same way: a pin on the far
/// side of the ring disappears behind the card as it swings around.
@main
final class DepthCompositing: Sketch {
let orbCount = 14
let ringRadius = 1.7
/// A fixed cloud of small offsets, generated once, that turns each orb center
/// into a glowing ball of splats — shared by every orb so the balls hold still
/// (only their centers move with time). Built from a deterministic hash so the
/// render is reproducible (the snapshot test depends on it).
let blob: [Vector3] = {
func h(_ n: Int) -> Double { // a reproducible −1…1 hash
let x = sin(Double(n) * 12.9898) * 43758.5453
return (x - floor(x)) * 2 - 1
}
return (0..<80).map { i in
Vector3(h(i * 4), h(i * 4 + 1), h(i * 4 + 2)) * (abs(h(i * 4 + 3)) * 0.16 + 0.04)
}
}()
override func draw() {
background(Color(hex: 0x070912))
// Orbit the camera, looking along the ring's plane so orbs pass clearly in
// front of and behind the card at the center.
cameraShowcase(.turntable(period: .tau / 0.35), radius: 5.2, elevation: 0.22,
fieldOfView: .pi / 3.6)
// The camera's eye, to seat each pin on the camera-facing surface of its orb
// rather than buried in it; it tracks the viewer when they take the camera.
let eye = activeCamera?.eye ?? .zero
// The ring of orbs (3D point cloud). Each orb is a little cluster of splats
// so it reads as a glowing ball and occludes solidly.
var cloud = PointCloud()
var orbCenters: [Vector3] = []
for i in 0..<orbCount {
let a = Double(i) / Double(orbCount) * .tau + time * 0.25
let center = Vector3(cos(a) * ringRadius, sin(a * 2) * 0.35, sin(a) * ringRadius)
orbCenters.append(center)
let hue = Double(i) / Double(orbCount)
let color = Color(hue: hue, saturation: 0.7, brightness: 1.0)
for off in blob {
cloud.add(center + off, color: color, size: 0.05)
}
}
drawPointCloud(cloud)
// The 2D card, standing at the world origin. `withBillboard(at: .zero)`
// moves the canvas origin to the origin's screen position and sets the
// depth to the origin's depth, so everything inside draws in local
// coordinates and composites against the ring: the near orbs are drawn
// over it, the far orbs are hidden behind it.
withBillboard(at: .zero) {
noStroke()
fill(Color(white: 0.96, alpha: 0.86))
drawRect(center: .zero, width: 280, height: 170, cornerRadius: 18)
fill(Color(hex: 0x070912))
textAlign(.center, .middle)
textSize(34)
drawText("inside the scene", 0, -16)
textSize(17)
fill(Color(white: 0.35))
drawText("2D, depth-tested against the ring", 0, 24)
}
// A numbered pin on each orb, also depth-composited: a pin on the far
// side of the ring vanishes behind the card as it swings around. This is
// the per-point form of the same idea (one billboard per world point).
for (i, center) in orbCenters.enumerated() {
// Seat the pin just in front of the orb (toward the camera) so it isn't
// hidden by its own splats, while still living at the orb's depth.
let toEye = eye - center
let len = toEye.length
let anchor = len > 0 ? center + toEye * (0.26 / len) : center
withBillboard(at: anchor) {
noStroke()
fill(.black)
drawCircle(0, 0, 15)
fill(.white)
drawCircle(0, 0, 13)
fill(.black)
textAlign(.center, .middle)
textSize(16)
drawText("\(i + 1)", 0, 0)
}
}
// A plain top-left HUD, drawn with no depth — it stays over everything, the
// default for 2D in a 3D frame.
fill(Color(white: 0.7))
textAlign(.left, .top)
textSize(16)
drawText("depth-aware compositing", 24, 24)
}
}