Ollin

LinearFrame

the frame before the tone map: --export-exr writes an OpenEXR file whose color runs past white where the highlights are, with the frame's coverage and the distance from the eye beside it, which is what a compositing program wants

Run
swift run --package-path Examples Example-Export-LinearFrame
Source
Examples/Export/LinearFrame/Sketch.swift
import Ollin

/// LinearFrame: a scene whose file is worth more than its picture.
///
/// The window shows what a screen can show: the bright side of each sphere is
/// white, and the fog decides what reads as far away. `--export-exr` writes the
/// frame one step earlier, in linear light, where the highlights are still
/// several times brighter than white and every pixel carries its distance from
/// the eye. That file is what a compositing program wants: the look can be
/// graded, the glow put on by hand, the haze replaced, none of it guessing at
/// numbers that were already thrown away.
///
/// ```sh
/// swift run Example-Export-LinearFrame --export-exr /tmp/frame.exr
/// swift run Example-Export-LinearFrame --export /tmp/frame.png        # the same frame, clipped
/// swift run Example-Export-LinearFrame --export-sequence /tmp/frames --seconds 2 --exr
/// ```
///
/// The printed line says what came through: the brightest component as a
/// multiple of white, and the nearest and farthest distances in the `Z` channel.
@main
final class LinearFrame: Sketch {
    @Param("Lamp", 2.0 ... 20.0, icon: "lightbulb.max") var lamp = 9.0
    @Param("Gloss", 0.0 ... 1.0, icon: "circle.lefthalf.filled") var gloss = 0.85
    @Param("Haze", 0.0 ... 0.12, icon: "cloud.fog") var haze = 0.045
    @Param("Turn", 0.0 ... 0.4, icon: "arrow.clockwise") var turn = 0.12

    override func setup() {
        seed(5)
    }

    override func draw() {
        background(Color(hex: 0x05070C))
        // A long lane down the scene, so the distances in the depth channel spread
        // from a couple of units to the far plane.
        perspective(eye: Vector3(0, 3.1, 7.4), target: Vector3(0, -0.5, -9),
                    fieldOfView: .pi / 3.2, near: 0.5, far: 40)
        fog(Color(hex: 0x0C121C), density: haze)

        ambientLight(Color(white: 0.06))
        // One hard lamp, close and bright, which is what puts the highlights past
        // white. A screen has to fold them back in; the linear file does not.
        let swing = cos(time * turn * .tau) * 2.2
        pointLight(Color(hue: 0.09, saturation: 0.22, brightness: 1),
                   at: Vector3(2.6 + swing, 2.8, 3.4), intensity: lamp)
        pointLight(Color(hue: 0.55, saturation: 0.55, brightness: 1),
                   at: Vector3(-3.8, 1.4, -6), intensity: lamp * 0.55)

        // A polished floor, so the lamp lays a long streak down the lane.
        withState {
            translate(0, -0.6, 0)
            fill(Color(white: 0.16))
            specular(0.55)
            specularSharpness(260)
            drawPlane(width: 30, depth: 44)
        }

        // Nine spheres walking away from the camera, alternating sides, so the
        // near ones and the far ones sit in the same frame.
        for i in 0..<9 {
            let t = Double(i) / 8
            withState {
                translate(i.isMultiple(of: 2) ? -1.9 : 1.9, -0.05 + t * 0.05,
                          2.4 - Double(i) * 2.2)
                fill(Color(hue: 0.02 + t * 0.55, saturation: 0.5, brightness: 0.9))
                specular(gloss)
                specularSharpness(90 + t * 260)
                drawSphere(radius: 0.55)
            }
        }
    }
}

↑↓ move ↩ open