# DepthOfField

depth of field that comes from scattered light: each pass sends a million samples through a bokeh ball and deposits them as light (drawParticles(style: .light)) into a running mean (Accumulator), which converges instead of getting brighter, and the result is printed with developed (drag to change the focus distance)

Run it with `swift run --package-path Examples Example-Rendering-DepthOfField`.

```swift
import COllinShaders
import Ollin
import simd

/// Depth of field, *earned* rather than faked. A blurred photograph isn't a sharp
/// image with a blur filter on top; it's countless rays of light that each landed
/// in a slightly different place because they passed through the lens out of
/// focus. This sketch renders that literally. Nine smooth 3D curves are drawn not
/// as lines but as a spray of faint samples scattered along them, and each sample
/// is pushed to a random spot inside a ball whose radius grows with how far that
/// point sits from the focal plane. Where a curve crosses the focus it stays a
/// crisp bright ribbon; away from it the samples spread into soft bokeh. The blur
/// *emerges* from the statistics of where the light fell. There is no blur filter
/// anywhere.
///
/// **It converges.** Every frame scatters a few passes of a million samples, and
/// they are added into an `Accumulator`, which keeps the running *mean* of all the
/// passes so far. The picture settles instead of brightening without end, and the
/// longer you leave it the smoother the bokeh gets; move the focus or the camera
/// and the average starts over. The pieces, each one a framework feature:
///   • a `ComputeBuffer` of the ribbons' own struct read beside the particle
///     buffer, `compute(_:buffers:)` binding the two at indices 0 and 1;
///   • the camera packed for the kernel by `cameraParams()`, so the kernel projects
///     through the sketch's own `Camera3D` with `ollin_project_eye`;
///   • `ballSample` for the scatter, uniform over the ball's volume;
///   • `drawParticles(style: .light)`, the radiometric deposit: every sample
///     leaves `color × alpha × area` wherever it lands, one texel at one pixel;
///   • `withAccumulator` for the running mean, and `developed(exposure:ground:)`
///     to print it: exposure, a Reinhard roll-off, the ground added after the curve.
///
/// A mild perspective makes near samples bloom into bigger discs than far ones,
/// the look of a fast lens. Each sample is drawn as three particles (its red,
/// green, and blue) scattered to slightly different radii, so the bokeh grows
/// chromatic fringes the same emergent way. **Drag left and right to rack focus**,
/// and turn the parameters in the inspector: *Turn* orbits the camera, which
/// restarts the average every frame (each frame is then its own short exposure).
///
/// Inspired by Anders Hoff's depth-of-field and color-shift technique
/// (inconvergent). `LineSpray` wraps this whole pipeline for lines; the
/// `Rendering/LineSpray` example uses it.
@main
final class DepthOfField_Example: Sketch {
    @Param("Focal distance", 3 ... 10) var focalDistance = 6.6
    @Param("Bokeh", 0 ... 0.3) var bokeh = 0.06
    @Param("Color shift", 0 ... 0.5) var colorShift = 0.2
    @Param("Exposure", 1 ... 120) var exposure = 24.0
    @Param("Passes per frame", 1 ... 8) var passesPerFrame = 4
    @Param("Turn") var turning = false

    /// One ribbon: a closed 3D Lissajous figure and its color. The kernel reads
    /// these straight from a buffer of this struct (three float4 rows, 48 bytes).
    private struct Ribbon {
        var frequency: SIMD4<Float>
        var phase: SIMD4<Float>
        var color: SIMD4<Float>
    }

    private let samplesPerPass = 250_000
    /// Light a ribbon emits per pass, spread along its whole length, so a pass
    /// deposits about this much over the thousand or so pixels it crosses.
    private let ribbonLight = 500.0
    private var ribbons: ComputeBuffer<Ribbon>!
    private var samples: PingPong<OllinParticle>!
    private var light: Accumulator!
    private var azimuth = 0.55
    private var lastView: [Double] = []
    private let ground = Color(red: 0.012, green: 0.012, blue: 0.024)

    /// One thread per channel of one sample: pick a ribbon and a spot on it, take
    /// it into camera space, push it into the bokeh ball, project it, and write
    /// the particle the light path draws.
    private let scatter = ComputeKernel(entry: "scatter", """
        struct Ribbon { float4 frequency; float4 phase; float4 color; };
        struct Lens {
            OllinCameraMatrices camera;
            float4 lens;   // x focal distance, y bokeh strength, z color shift, w unused
            float4 misc;   // x seed, y samples per pass, z ribbon count, w light per sample
        };

        kernel void scatter(device const Ribbon *ribbons [[buffer(0)]],
                            device OllinParticle *out [[buffer(1)]],
                            constant OllinComputeUniforms &u [[buffer(10)]],
                            constant Lens &p [[buffer(11)]],
                            uint id [[thread_position_in_grid]]) {
            if (id >= u.particleCount) { return; }
            const float TAU = 6.28318530718;
            uint sample = id / 3u;
            uint channel = id % 3u;
            float3 seed = float3(float(sample & 4095u), float(sample >> 12u), p.misc.x);

            uint count = max(uint(p.misc.z), 1u);
            uint r = min(uint(hash13(seed) * float(count)), count - 1u);
            Ribbon ribbon = ribbons[r];
            float t = hash13(seed + float3(1.7, 2.9, 0.3));
            float3 world = 0.95 * sin(ribbon.frequency.xyz * t * TAU + ribbon.phase.xyz);

            // Into camera space; the defocus is the distance from the focal plane
            // along the view axis, and it sets the ball the sample scatters in.
            float3 eye = (p.camera.view * float4(world, 1.0)).xyz;
            float defocus = abs(-eye.z - p.lens.x);
            float radius = defocus * p.lens.y;
            // Color shift: red scatters a little wider than green, blue tighter,
            // so the fringes of the bokeh come out chromatic.
            float shift = p.lens.z * min(defocus, 1.0);
            float widen = (channel == 0u) ? 1.0 + shift : (channel == 2u) ? 1.0 - shift : 1.0;
            eye += ballSample(seed + float3(3.3, 7.1, 5.5)) * radius * widen;

            float4 screen = ollin_project_eye(p.camera, eye, u.resolution);
            OllinParticle o;
            o.velocity = float2(0.0);
            o.life = 1.0;
            o.seedA = 0.0;
            o.seedB = 0.0;
            if (screen.w <= 0.0) {
                o.position = float2(-16.0);
                o.size = 0.0;
                o.color = float4(0.0);
                out[id] = o;
                return;
            }
            float3 mask = (channel == 0u) ? float3(1, 0, 0)
                        : (channel == 1u) ? float3(0, 1, 0) : float3(0, 0, 1);
            // A one-pixel point, its whole light in the pixel it lands on: alpha
            // is 4/pi over the disc's area, so the deposit is the radiance itself.
            o.position = screen.xy;
            o.size = 1.0;
            o.color = float4(ribbon.color.rgb * mask * p.misc.w, 1.27323954474);
            out[id] = o;
        }
        """)

    override func setup() {
        // Nine closed curves, fixed by the seed: integer frequencies (so each
        // closes), random phases, and hues spaced evenly round the wheel.
        seed(4)
        var table: [Ribbon] = []
        for i in 0 ..< 9 {
            let c = Color(hue: Double(i) / 9, saturation: 0.8, brightness: 1).linearRGBA
            table.append(Ribbon(
                frequency: SIMD4(Float(Int(random(1, 4))), Float(Int(random(1, 4))), Float(Int(random(1, 4))), 0),
                phase: SIMD4(Float(random(.tau)), Float(random(.tau)), Float(random(.tau)), 0),
                color: SIMD4(c.x, c.y, c.z, 1)))
        }
        ribbons = ComputeBuffer(table)
        samples = PingPong(count: samplesPerPass * 8 * 3)
        light = makeAccumulator()
    }

    override func draw() {
        background(ground)
        if turning { azimuth += 0.15 / max(frameRate, 1) }
        let focal = mouseIsPressed ? map(mouseX, 0, width, 3, 10) : focalDistance
        let view = Camera3D.orbiting(target: .zero, radius: 7, azimuth: azimuth,
                                     elevation: 0.35, fieldOfView: .pi / 6)
        camera(view)

        // Anything the picture depends on restarts the average.
        let signature = [azimuth, focal, bokeh, colorShift, Double(passesPerFrame)]
        if signature != lastView {
            lastView = signature
            light.reset()
        }

        // One frame of passes: the kernel writes every sample, the light path
        // deposits them, and the accumulator folds them into its mean.
        let passes = max(1, passesPerFrame)
        let count = samplesPerPass * passes * 3
        var params = cameraParams()
        params.append(SIMD4<Float>(Float(focal), Float(bokeh), Float(colorShift), 0))
        params.append(SIMD4<Float>(Float(random(100)), Float(samplesPerPass), 9,
                                   Float(ribbonLight * 9 / Double(samplesPerPass))))
        let out = samples.write
        compute(scatter, buffers: [ribbons, out], count: count, params: params)
        samples.swap()
        withAccumulator(light, passes: passes) {
            blendMode(.add)
            drawParticles(out, count: count, style: .light)
        }

        drawImage(light.developed(exposure: exposure, ground: ground).image, 0, 0)
        drawCaption("drag to rack focus · \(light.passes) passes")
    }
}
```
