Ollin

Materials

The material library. One orbiting sphere grid shows each built-in Material (.iridescent/.soapBubble/.velvet/.jade/.toon/.gooch/…), and the view-angle finishes shift as it turns.

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

/// The material library: one surface, eighteen finishes.
///
/// A `Material` sets how a mesh responds to light; the surface *color* stays the
/// current `fill`. The same sphere shows each built-in here, laid out in a grid the
/// camera orbits so the view-angle finishes come alive: the iridescent rainbows shift,
/// the velvet rim slides around the silhouette, the jade and wax glow through their
/// edges. A point light orbits with the camera so every highlight keeps moving.
///
/// The grid runs:
///
/// - **dielectric finishes** — `matte`, `clay`, `rubber`, `plastic` (top row),
///   `ceramic`, `glossy`, `polished`, then the stylized models begin.
/// - **iridescent family** — `iridescent`, `soapBubble`, `oilSlick`, `beetle`.
/// - **sparkle family** — `glitter` (fine flake dust) and `sequin` (chunky facets),
///   flashing as the orbit sweeps.
/// - **rim / subsurface** — `velvet`, `jade`, `wax`.
/// - **non-photorealistic** — `toon` (cel) and `gooch` (warm→cool).
///
/// Each is a plain value, so you'd build your own the same way (or copy and tweak a
/// built-in — `var m = Material.glossy; m.iridescence = 0.4`).
@main
final class Materials3D: Sketch {

    // The material, the fill it's shown over, and a label. Fills are picked so each
    // finish reads: a neutral/dark body under the iridescent ones (so the rainbow
    // shows), a tinted body where the material implies one (jade green, wax cream).
    let entries: [(name: String, material: Material, fill: Color)] = [
        ("matte",      .matte,      Color(hue: 0.00, saturation: 0.55, brightness: 0.85)),
        ("clay",       .clay,       Color(hue: 0.05, saturation: 0.55, brightness: 0.70)),
        ("rubber",     .rubber,     Color(white: 0.22)),
        ("plastic",    .plastic,    Color(hue: 0.58, saturation: 0.70, brightness: 0.90)),
        ("ceramic",    .ceramic,    Color(white: 0.92)),
        ("glossy",     .glossy,     Color(hue: 0.98, saturation: 0.70, brightness: 0.85)),
        ("polished",   .polished,   Color(white: 0.78)),
        ("iridescent", .iridescent, Color(white: 0.18)),
        ("soapBubble", .soapBubble, Color(white: 0.80)),
        ("oilSlick",   .oilSlick,   Color(white: 0.08)),
        ("beetle",     .beetle,     Color(hue: 0.40, saturation: 0.65, brightness: 0.25)),
        ("glitter",    .glitter,    Color(hue: 0.66, saturation: 0.75, brightness: 0.30)),
        ("sequin",     .sequin,     Color(hue: 0.93, saturation: 0.80, brightness: 0.55)),
        ("velvet",     .velvet,     Color(hue: 0.93, saturation: 0.65, brightness: 0.40)),
        ("jade",       .jade,       Color(hue: 0.42, saturation: 0.55, brightness: 0.55)),
        ("wax",        .wax,        Color(hue: 0.10, saturation: 0.30, brightness: 0.90)),
        ("toon",       .toon,       Color(hue: 0.07, saturation: 0.80, brightness: 0.95)),
        ("gooch",      .gooch,      Color(white: 0.55)),
    ]

    let columns = 4
    let spacing = 2.4

    override func draw() {
        background(Color(hex: 0x0B0C12))

        // Orbit the grid slowly so the view-angle finishes (iridescence, rim, jade)
        // shift and shimmer rather than sitting frozen.
        cameraShowcase(.sway(amplitude: 0.6, period: .tau / 0.12), target: .zero, radius: 12.5,
                    elevation: 0.32, fieldOfView: .pi / 4.5)

        // A point light orbiting with us keeps the highlights sliding; a warm key and a
        // soft ambient fill out the modeling.
        let lightPos = Vector3(cos(time * 0.6) * 6, 5, sin(time * 0.6) * 6)
        ambientLight(Color(white: 0.14))
        directionalLight(Color(kelvin: 5600), direction: Vector3(-0.4, -0.7, -0.5), intensity: 0.7)
        pointLight(.white, at: lightPos, intensity: 1.0)

        let rows = (entries.count + columns - 1) / columns
        for (i, entry) in entries.enumerated() {
            let col = i % columns
            let row = i / columns
            let pos = gridPosition(col: col, row: row, rows: rows)
            withState {
                translate(pos.x, pos.y, pos.z)
                fill(entry.fill)
                material(entry.material)
                drawSphere(radius: 0.85)
            }
            drawLabel(entry.name, under: pos)
        }
    }

    private func gridPosition(col: Int, row: Int, rows: Int) -> Vector3 {
        let x = (Double(col) - Double(columns - 1) / 2) * spacing
        let y = (Double(rows - 1) / 2 - Double(row)) * spacing
        return Vector3(x, y, 0)
    }

    // Place a name under each sphere by projecting its world position to the canvas —
    // so the labels follow the spheres around the orbit.
    private func drawLabel(_ name: String, under worldPos: Vector3) {
        guard let p = project(worldPos + Vector3(0, -1.15, 0)) else { return }
        withState {
            textFont(OutlineFont.system)
            textSize(26)
            textAlign(.center)
            noStroke()
            fill(Color(white: 0.85))
            drawText(name, at: p)
        }
    }
}

↑↓ move ↩ open