Ollin

SpecularAntialias

Highlights smaller than their pixel, held still instead of crawling.

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

/// Specular anti-aliasing: highlights on fine shiny detail, held still.
///
/// The scene is a bed of small polished balls under one hard light, seen from
/// far enough away that each ball is a handful of pixels across. Its highlight
/// is then narrower than the pixel it sits in, so the single shading sample per
/// pixel either lands on the speck or misses it, and the bed crawls with
/// sparkle as the camera sways. `specularAntialiasing()` widens the roughness of
/// each pixel by how much its own surface normal turns across it, which turns
/// the missed speck into a slightly broader highlight that stays where it is.
///
/// Turn `ballSize` down to make the problem worse, and `strength` up to answer
/// it harder. The plate at the back is the counterfactual: it is flat, so its
/// normal never turns across a pixel, and the filter leaves it alone whatever
/// the strength.
///
/// Worth knowing while toggling it here: the same frame rendered sixteen samples
/// to a pixel (`--render-scale 4`) sits *between* the two. Off, the bed is too
/// dark because most of its specks are missed; on, it is brighter than the truth
/// because a widened highlight spreads further than the surface really scatters.
/// What the call buys on a bed like this is steadiness rather than accuracy.
@main
final class SpecularAntialias: Sketch {

    @Param(icon: "sparkles", group: "Image")
    var specularAA = true

    /// How far the roughness is widened. 1 is the published pixel filter, and
    /// 2 the conservative form of it, which trades more of the finish for more
    /// calm.
    @Param(0.25...3, icon: "dial.medium", group: "Image")
    var strength = 1.0

    /// Smaller balls put more of the surface under each pixel, which is exactly
    /// what makes the highlight too small to sample. Turn it up and the problem
    /// goes away on its own.
    @Param(0.03...0.25, icon: "circle.grid.3x3", group: "Scene")
    var ballSize = 0.07

    /// The finish the sketch asked for. A polished ball has the narrowest
    /// highlight and the worst crawl.
    @Param(0.04...0.45, icon: "camera.filters", group: "Scene")
    var polish = 0.10

    private let ball = Mesh.sphere(radius: 1, segments: 24, rings: 12)

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

        cameraShowcase(.sway(period: 34), target: Vector3(0, 0.1, 0),
                       radius: 12, elevation: 0.34, fieldOfView: .pi / 3.4)

        directionalLight(.white, direction: Vector3(0.35, -0.5, 0.79), intensity: 0.9)
        // Metal has no color of its own without something to reflect, and the
        // environment is a second thing the widening reaches: a rougher pixel
        // reads the environment through a blurrier level of it.
        environment(.studio.intensified(to: 0.35))
        if specularAA { specularAntialiasing(strength: strength) }

        // The bed of balls: 57 by 57 of them over a ten-unit square, each one
        // nudged off the grid so the sparkle does not read as a pattern.
        randomSeed(7)
        var balls: [MeshInstance] = []
        let side = 57
        for row in 0..<side {
            for column in 0..<side {
                let x = -5.0 + 10.0 * Double(column) / Double(side - 1)
                let z = -5.0 + 10.0 * Double(row) / Double(side - 1)
                balls.append(MeshInstance(
                    position: Vector3(x + random(-0.03, 0.03), ballSize,
                                      z + random(-0.03, 0.03)),
                    scale: ballSize * random(0.85, 1.15)))
            }
        }
        fill(Color(white: 0.85))
        material(.metal(roughness: polish))
        drawMesh(ball, instances: balls)

        // The counterfactual: one flat plate of the same metal standing behind
        // the bed. It holds a single normal, so there is no spread to widen by
        // and it renders identically with the filter on or off.
        withState {
            fill(Color(white: 0.8))
            material(.metal(roughness: polish))
            translate(0, 1.5, -5.6)
            drawBox(width: 10.6, height: 3.0, depth: 0.08)
        }

        withState {
            fill(Color(white: 0.16))
            material(.dielectric(roughness: 0.7))
            translate(0, -0.02, 0)
            drawBox(width: 22, height: 0.04, depth: 22)
        }

        let says = specularAA ? "on at strength \(String(format: "%.2f", strength))" : "off"
        drawCaption("specularAntialiasing() \(says) - toggle it in the inspector")
    }
}

↑↓ move ↩ open