Ollin

VolumetricLight

Beams, gobos, and shafts you can see in the air.

Run
swift run --package-path Examples Example-3D-Lighting-VolumetricLight
Source
Examples/3D/Lighting/VolumetricLight/Sketch.swift
import Ollin

/// Volumetric light: beams, gobos, and shafts you can see in the air.
///
/// A light normally shows only where it lands; `volumetricLight()` marches the
/// air itself, so a spot's cone becomes a stage beam and everything the light
/// carries shapes that beam too. Two spots light this set:
///
/// - the **key**, high on the left, projecting a window-frame cookie through
///   thin haze: the panes read as tilted bars of bright air before they land
///   on the floor, and `castShadows()` lets the standing props carve their own
///   dark shafts out of the beam,
/// - a bare **rim** spot low behind the set, aimed toward the camera: with a
///   forward-leaning `anisotropy` its cone flares as the orbit swings through
///   it, the looking-into-the-light glow.
///
/// A whisper of `fog` gives the beams a medium to live in and settles the room
/// into haze; drop the `fog` call and the air goes clear but the beams stay
/// (the extinction is the fog's, the glow is the march's). The beams are the
/// same lights that shade the surfaces, so cookie, cone, and shadow agree
/// between the air and the floor.
@main
final class VolumetricLight: Sketch {

    // The window gobo, drawn in code: 2×2 panes behind mullions (the cookie
    // resamples the image once at init, so build it once and keep it).
    private let windowGobo: LightCookie = {
        var frame = Image(width: 128, height: 128, color: .black)
        for y in 0..<128 {
            for x in 0..<128 {
                let inFrame = x > 10 && x < 117 && y > 10 && y < 117
                let onMullion = abs(x - 64) < 6 || abs(y - 64) < 6
                if inFrame && !onMullion { frame[x, y] = .white }
            }
        }
        return LightCookie(frame)!
    }()

    override func draw() {
        background(Color(hex: 0x04050A))
        ambientLight(Color(white: 0.015))   // a whisper so unlit backs aren't dead black

        cameraShowcase(.orbitAndRise(period: 48), target: Vector3(0, 0.9, 0),
                       radius: 10.5, elevation: 0.16, fieldOfView: .pi / 4.2)

        // The key: warm, high on the left, throwing the window across the set.
        spotLight(Color(hue: 0.10, saturation: 0.28, brightness: 1.0),
                  at: Vector3(-4.6, 6.0, 2.6), direction: Vector3(0.62, -0.74, -0.28),
                  coneAngle: .pi / 8, penumbra: 0.22, intensity: 3.2,
                  cookie: windowGobo, roll: 0.18)

        // The rim: cool and faint, crossing the set laterally behind the props (a
        // beam reads best crossing the view; one aimed down the lens floods it).
        spotLight(Color(hue: 0.58, saturation: 0.45, brightness: 1.0),
                  at: Vector3(5.6, 2.6, -4.8), direction: Vector3(-0.92, -0.18, 0.36),
                  coneAngle: .pi / 10, penumbra: 0.5, intensity: 0.7)

        castShadows()            // the props carve shafts out of the key's beam
        volumetricLight(0.9, anisotropy: 0.45)
        fog(Color(hex: 0x0A0E18), density: 0.02)   // thin haze for the beams to live in

        // --- The set: a matte floor and a few standing props ---
        fill(Color(hex: 0x2E3138))
        drawGround(size: 22, thickness: 1.1)

        fill(Color(hex: 0x8A8478))
        withState {
            translate(-0.4, 1.35, -0.3)
            drawCylinder(radius: 0.42, height: 2.7)
        }
        fill(Color(hex: 0x707A86))
        withState {
            translate(1.7, 0.62, 1.3)
            drawSphere(radius: 0.62)
        }
        fill(Color(hex: 0x66605A))
        withState {
            translate(-2.1, 0.85, 1.8)
            rotateY(0.5)
            drawBox(width: 0.75, height: 1.7, depth: 0.75)
        }
        fill(Color(hex: 0x5A6472))
        withState {
            translate(2.6, 1.05, -1.9)
            drawCone(radius: 0.55, height: 2.1)
        }
    }
}

↑↓ move ↩ open