Ollin

ManyCasters

Several casters at once. A warm key is joined by a swinging spot or by two circling point lamps, and each throws its own shadow.

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

/// ManyCasters: several lights in the room, each throwing its own shadow.
///
/// `castShadows()` casts from every light in the frame, up to four of them, so a room
/// with several lamps reads like one. A warm key rakes in from the left in both rigs,
/// sweeping slowly so its shadows swing; press any key to cycle what plays beside it.
/// The **spot** rig swings a cool stage light in from the right, and each prop drops a
/// shadow away from each light: two shadows per object, crossing where the lights
/// overlap and coloring the floor with whichever light still reaches it. The **point**
/// rig hangs two lamps circling the props instead. A point light casts every way at
/// once, so it is the expensive kind: it needs a whole cube of depth around it, or a
/// set of rays traced from each lit pixel, and a frame can hold several of them all
/// the same. The renderer picks the cheaper route for the GPU it is on, and nothing
/// here changes with it (on Apple silicon each lamp traces its own rays; elsewhere
/// each renders its own six-face cube of depth).
///
/// The last light is the counter-example. It is a dim overhead fill, and it is told
/// `castsShadow: false`, so it lifts the shaded sides and throws nothing. Take that
/// argument away and it throws another shadow across the same floor. Every caster
/// renders its own pass over the scene from its own point of view, so a light that
/// adds nothing but brightness should stay out of the shadow work.
@main
final class ManyCasters: Sketch {

    /// Press any key to cycle the lamp kind playing beside the key light.
    var usesPointLamps = false

    override func draw() {
        background(Color(hex: 0x090B11))

        cameraShowcase(.turntable(period: .tau / 0.12), target: Vector3(0, 0.9, 0),
                       radius: 16, elevation: 0.78, fieldOfView: .pi / 4.4)

        ambientLight(Color(white: 0.05))

        // The key: warm, high on the left, sweeping slowly so its shadows swing.
        directionalLight(Color(hue: 0.08, saturation: 0.30, brightness: 1.0),
                         direction: Vector3(0.95, -0.95, -0.2 + sin(time * 0.22) * 0.25),
                         intensity: 0.85)

        if usesPointLamps {
            // The warm lamp, circling the props at head height.
            let warmAngle = time * 0.35
            pointLight(Color(hue: 0.07, saturation: 0.45, brightness: 1.0),
                       at: Vector3(cos(warmAngle) * 6.0, 3.4, sin(warmAngle) * 6.0),
                       intensity: 1.5, specular: .white)

            // The cool lamp, crossing it the other way and a little lower.
            let coolAngle = -time * 0.27 + .pi
            pointLight(Color(hue: 0.55, saturation: 0.5, brightness: 1.0),
                       at: Vector3(cos(coolAngle) * 5.2, 2.6, sin(coolAngle) * 5.2),
                       intensity: 1.3, specular: .white)
        } else {
            // The stage light: cool, from the right, circling the props. It casts beside
            // the key rather than waiting its turn.
            let aim = Vector3(cos(time * 0.26) * 1.2, 0.4, sin(time * 0.26) * 1.2)
            let spotPos = Vector3(-7.0, 7.0, 2.0)
            spotLight(Color(hue: 0.56, saturation: 0.42, brightness: 1.0),
                      at: spotPos, direction: (aim - spotPos).normalized,
                      coneAngle: .pi / 4.2, penumbra: 0.35, intensity: 1.7, specular: .white)
        }

        // The fill, kept out of the shadow work.
        directionalLight(Color(hex: 0xBFD2FF), direction: Vector3(0, -1, 0.55),
                         intensity: 0.3, castsShadow: false)

        castShadows()
        shadowSoftness(0.45)

        // The floor that catches every set of shadows.
        withState {
            fill(Color(white: 0.72))
            specular(0.06)
            drawPlane(width: 26, depth: 26)
        }

        specular(0.28)
        specularSharpness(42)

        withState {
            translate(-1.0, 1.1, 2.4)
            rotateY(time * 0.4)
            fill(Color(hue: 0.02, saturation: 0.55, brightness: 0.95))
            drawBox(size: 2.2)
        }
        withState {
            translate(2.8, 1.4, -0.4)
            fill(Color(hue: 0.42, saturation: 0.45, brightness: 0.95))
            drawSphere(radius: 1.4)
        }
        withState {
            translate(0.4, 2.9 + sin(time * 0.9) * 0.35, -3.2)
            rotateX(time * 0.5)
            rotateZ(time * 0.3)
            fill(Color(hue: 0.13, saturation: 0.68, brightness: 1.0))
            drawTorus(radius: 1.1, tube: 0.36)
        }
        withState {
            translate(-3.0, 1.0, -1.0)
            rotateY(-time * 0.6)
            fill(Color(hue: 0.75, saturation: 0.42, brightness: 0.95))
            drawCone(radius: 0.9, height: 2.0)
        }

        drawCaption(usesPointLamps
            ? "Three casters: a key and two circling lamps (any key: spot)"
            : "Two casters: a key and a spot (any key: point lamps)")
    }

    override func keyPressed() { usesPointLamps.toggle() }
}

↑↓ move ↩ open