Ollin

LensFlare

The light a camera adds to a picture on its own.

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

/// Lens flare: the light a camera adds to a picture all by itself.
///
/// Everything else in the renderer models the light and the surface. This one
/// models the *camera*. Some of a bright source reflects off the lens's own
/// interfaces instead of passing through them, comes back down the barrel, and
/// lands on the sensor somewhere it does not belong, which is what strings
/// ghosts along the line from the source through the middle of the frame.
///
/// So the call asks for a lens rather than for a look. Both bundled lenses are
/// real prescriptions, and their surfaces decide how many ghosts there are, where
/// each one sits, how large it is and what color it comes out. The double Gauss
/// has more surfaces, so it strings the busier chain, with ghosts on both sides
/// of the middle of the frame: some land beyond the lamp and some across the
/// center from it. The Heliar throws fewer and wider ones, more of a veil. Stop
/// the iris down with `fStop` and the ghosts it shapes shrink and brighten, the
/// same light in a smaller shape. Give the camera blades and they take the
/// blades' shape, because a ghost is a picture of the opening the light came
/// through. That is also the opening the path-traced export makes its
/// out-of-focus highlights from, so the two cannot disagree.
///
/// Look closely at a ghost, because it is not a flat copy of the iris. Every
/// ghost is followed ray by ray through the real glass, so its sides are a
/// little stretched, more the further the lamp sits from the middle. Where a
/// ghost ends in a curve instead of a straight side, that is the round barrel of
/// the lens stopping rays the iris let past. A bright rim along one edge is a
/// caustic, where neighboring rays landed on top of one another. And the rim
/// carries a thin fringe of color, since glass bends red and blue by different
/// amounts. Drag `sourceSize` up and all of that melts, the way it does from a
/// big soft lamp.
///
/// The star on the lamp itself is the other half of a flare, and it comes from
/// the same opening: it is the far-field diffraction of the iris, so six blades
/// put six arms on it and a round iris puts none. Turn the blades to 0 and watch
/// both the ghosts and the star lose their corners together. `wear` is the wear
/// on that opening: at 0 the arms are ruled lines, and as it rises they split
/// and fray and fine needles fill in between them.
///
/// Three more things are off until you ask for them, under *Extras*. `streak`
/// is what cylindrical glass does to a light, the front of an anamorphic lens or
/// the fine grooves of a streak filter: it fans the light out to either side, so
/// the fans add up to one long thin line through the lamp. `dirt` is grime on the front element, so far
/// out of focus that every speck is a soft picture of the iris, glowing where it
/// sits near the lamp. `halo` is the thin rainbow ring around a light, which is a
/// look and not optics, and the reference says so. `squeeze` puts an anamorphic
/// front group on the lens: at 2 the ghosts and the star stretch two to one
/// sideways. It wants the streak with it, since the same cylindrical glass
/// throws both.
///
/// Watch the lamp go behind the slab. The flare does not switch off: it fades
/// as the slab covers the source, because the strength follows how much of the
/// lamp the camera can actually see.
@main
final class LensFlare: Sketch {

    /// The two bundled prescriptions. Few surfaces make few, large, clean
    /// ghosts; more surfaces string a busier chain across the frame.
    enum Glass: CaseIterable, ParamOption {
        case heliar, doubleGauss

        var lens: Lens {
            switch self {
            case .heliar: .heliar
            case .doubleGauss: .doubleGauss
            }
        }
    }

    @Param(icon: "sun.max", group: "Flare")
    var flare = true

    @Param(icon: "camera", group: "Flare")
    var glass: Glass = .doubleGauss

    @Param(0...2.5, icon: "dial.medium", group: "Flare")
    var strength = 1.0

    @Param(1.4...22, icon: "camera.aperture", group: "Iris")
    var fStop = 8.0

    @Param(0...11, icon: "hexagon", group: "Iris")
    var blades = 6

    @Param(icon: "swatchpalette", group: "Flare")
    var multicoated = true

    @Param(0...2, icon: "sparkle", group: "Star")
    var star = 1.0

    @Param(0.1...0.8, icon: "arrow.up.left.and.arrow.down.right", group: "Star")
    var starSize = 0.35

    @Param(0...1, icon: "aqi.medium", group: "Star")
    var wear = 0.5

    @Param(0...1.5, icon: "minus", group: "Extras")
    var streak = 0.0

    @Param(0...1.5, icon: "circle", group: "Extras")
    var halo = 0.0

    @Param(0...1, icon: "aqi.low", group: "Extras")
    var dirt = 0.0

    @Param(1...2, icon: "arrow.left.and.right", group: "Extras")
    var squeeze = 1.0

    @Param(0.002...0.06, icon: "circle.dashed", group: "Flare")
    var sourceSize = 0.006

    // A flat white matcap for the bulb prop (cached; an `Image` keeps its texture).
    private let bulbGlow = Image(width: 1, height: 1, color: Color(hex: 0xFFF6E2))

    override func draw() {
        background(Color(white: 0.03))

        var camera = Camera3D(eye: Vector3(0, 1.5, 7.4), target: Vector3(0, 1.5, 0),
                              near: 0.2, far: 60,
                              projection: .perspective(fieldOfView: .pi / 3.2))
        camera.apertureBlades = blades
        self.camera(camera)

        ambientLight(Color(white: 0.06))
        directionalLight(Color(white: 0.85), direction: Vector3(-0.5, -0.8, -0.4), intensity: 0.35)

        // The lamp drifts across the frame and passes behind the slab. It is a
        // real point light, so it lights the room as well as flaring.
        let lamp = Vector3(-2.9 * cos(time * 0.35), 3.35 + 0.45 * sin(time * 0.27), -2.0)
        pointLight(Color(hex: 0xFFF2D6), at: lamp, intensity: 18)
        let coated = multicoated ? glass.lens.multicoated() : glass.lens
        let lens = coated.anamorphic(squeeze: squeeze)
        if flare {
            // The bulb is a small one, a few thousandths of the frame's height
            // across as the camera sees it. A ghost is a picture
            // taken with the source, so telling the flare how large the source is
            // sets how soft every ghost's edge comes out. Drag `sourceSize` up and
            // the ghosts melt; drag it down and they sharpen into the iris's own
            // shape.
            lensFlare(Ollin.LensFlare(lens: lens.stopped(to: fStop), amount: strength,
                                      star: star, starSize: starSize, wear: wear,
                                      streak: streak, halo: halo, dirt: dirt,
                                      sourceSize: sourceSize))
        }

        // The bulb itself, so there is something on screen for the flare to come
        // from. A flat single-color matcap ignores the scene lighting, which is
        // what a glowing thing looks like.
        withState {
            translate(lamp)
            fill(.white)
            matcap(bulbGlow)
            drawSphere(radius: 0.06)
        }
        matcap(nil)

        // The slab the lamp passes behind, and a floor to catch the light.
        fill(Color(white: 0.30))
        withState {
            translate(-1.5, 1.9, -1.2)
            drawBox(width: 1.1, height: 3.4, depth: 0.5)
        }
        fill(Color(white: 0.22))
        drawGround(size: 22, thickness: 0.1)
        fill(Color(hex: 0x2E4658))
        for i in 0..<5 {
            withState {
                translate(-4.4 + Double(i) * 2.2, 0.45, -5.0)
                drawBox(width: 0.7, height: 0.9, depth: 0.7)
            }
        }

        // A bloom after the flare, so the ghosts glow the way everything else
        // bright in the frame does. The flare composites before the filters for
        // exactly this reason.
        postProcess(.bloom(threshold: 0.7, amount: 0.8, radius: 6))
    }
}

↑↓ move ↩ open