Ollin

Caustics

The light a glass or a polished metal focuses onto what is around it.

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

/// Caustics: the light a glass or a polished metal focuses onto what's around it.
///
/// A shadow says where light *can't* reach; a caustic says where a lens or a
/// mirror *concentrated* it instead. `caustics()` traces photons from the light
/// through every transmissive and mirror-polished surface and draws where they
/// land, so a clear sphere throws the classic bright spot into its own shadow, a
/// tinted sphere throws a colored one, and a chrome ring folds light into the
/// curved fan a wedding band leaves on a table:
///
/// ```swift
/// directionalLight(.white, direction: Vector3(-0.3, -1, -0.2))
/// caustics()
/// material(.glass(thickness: 1.8))
/// drawSphere(radius: 0.9)
/// ```
///
/// The pattern's sharpness comes from how each light path focused, not from a
/// blur pass, so the hot spot under a sphere stays a point while the ring's fan
/// stays a fine curve. `dispersion:` splits refracted paths by wavelength for
/// prism rainbows; `causticsQuality(_:)` trades photons for frame rate. This
/// sketch sets `intensity: 1.6`, past the physical `1`, so the patterns stand
/// out. Needs a ray-tracing GPU (Apple silicon), a camera, and a light. **Hold
/// the space bar** to switch the caustics off and compare: the shadows go back
/// to plain darkness.
@main
final class Caustics: Sketch {

    override func draw() {
        background(Color(hex: 0x101318))
        cameraShowcase(.sway(amplitude: 0.2, period: .tau / 0.08),
                       target: Vector3(0, 0.6, 0), radius: 10.5, elevation: 0.42,
                       fieldOfView: .pi / 4, near: 1, far: 40)
        environment(.studio.intensified(to: 0.55).backgroundBlurred(0.6))
        directionalLight(.white, direction: Vector3(-0.35, -1, -0.2), intensity: 2.2)
        castShadows()
        rayTracedReflections()   // the glass shows the actual scene through itself
        // Turned up past physical for the demo: at 1 a pane gives back the light
        // its shadow took, less what the glass reflects, and the patterns read
        // quieter on a floor this bright.
        if !isKeyDown(" ") { caustics(intensity: 1.6) }

        // A matte floor: the screen the light patterns land on.
        withState {
            material(.dielectric(roughness: 0.85))
            fill(Color(hex: 0x878c99))
            translate(0, -0.5, 0)
            drawBox(width: 26, height: 1.0, depth: 16)
        }

        // A clear solid sphere: a lens, hovering at the height that puts its focal
        // point right on the floor (a ball lens focuses about half a radius past
        // its surface), so the bright spot lands tight inside its own shadow,
        // where direct light can't be.
        withState {
            material(.glass(thickness: 2.4))
            fill(.white)
            translate(-2.4, 1.7, 0)
            drawSphere(radius: 1.2)
        }

        // A tinted absorbing sphere: the same lens in bottle green, so the spot
        // it throws is colored by the glass it crossed.
        withState {
            material(.glass(thickness: 2.0,
                            attenuationColor: Color(hex: 0x2e8f5b),
                            attenuationDistance: 1.6))
            fill(.white)
            translate(2.5, 1.15, -0.4)
            drawSphere(radius: 1.0)
        }

        // A chrome ring lying almost flat: its inner wall folds the light into
        // the curved fan a ring leaves beside itself on a sunlit table.
        withState {
            material(.metal(roughness: 0.06))
            fill(Color(hex: 0xf2f4f8))
            translate(0.3, 0.42, 2.6)
            rotateX(.pi / 2 * 0.92)
            drawTorus(radius: 1.15, tube: 0.16)
        }

        drawCaption(isKeyDown(" ")
            ? "Caustics off (release space to focus the light again)"
            : "Caustics on (hold space to compare)")
    }
}

↑↓ move ↩ open