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))
}
}