Ollin

BrushedMetal

Anisotropic specular: brushed, turned, and satin finishes whose highlight is a streak instead of a dot.

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

/// Anisotropic specular: the brushed, turned, and satin finishes whose highlight is a
/// streak instead of a dot.
///
/// `Material.anisotropy` (`-1…1`) stretches the physically-based specular lobe along
/// one surface direction, the way the micro-grooves of a brushed or lathed metal do.
/// The **top row** sweeps the strength on the same metal: at `0` the highlight is
/// round, and each step pulls it into a longer streak; a negative value runs the
/// streak the other way. The **bottom row** spins the streak with
/// `anisotropyRotation` (radians), and ends on a cylinder, where the stable world
/// frame the lobe uses on an unmapped mesh reads as a turned, on-the-lathe finish.
///
/// The streak follows the surface's `u` axis on a mesh with a normal or surface map;
/// everywhere else it runs around a stable world frame, so primitives just work. The
/// environment gather follows the stretch too (the reflection smears along the
/// brushing), and `.brushedMetal` carries the finish ready-made.
@main
final class BrushedMetal: Sketch {

    let spacing = 2.4

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

        cameraShowcase(.sway(amplitude: 0.22, period: .tau / 0.1), target: .zero, radius: 15,
                       elevation: 0.22, fieldOfView: .pi / 4)
        environment(.studio.intensified(to: 1.05).backgroundBlurred(0.55))

        // A warm key sweeping with time so the streaks travel; the environment
        // supplies the smeared reflections and the fill light.
        let lightPos = Vector3(cos(time * 0.5) * 8, 6, sin(time * 0.5) * 8)
        pointLight(Color(kelvin: 5200), at: lightPos, intensity: 1.2)

        let steel = Color(white: 0.78)
        let gold = Color(hue: 0.11, saturation: 0.62, brightness: 0.85)

        // Top row: the strength. The same metal from a round highlight to a long
        // streak, and the sign running it the other way.
        row(y: spacing / 2, labels: ["isotropic", "0.4", "0.8", "-0.8"],
            shape: { _ in drawSphere(radius: 0.92) }) { col in
            fill(steel)
            let strength = [0.0, 0.4, 0.8, -0.8][col]
            material(Material(shading: .physicallyBased, metallic: 1,
                              roughness: 0.4, anisotropy: strength))
        }

        // Bottom row: the rotation spinning the streak, then the turned look on a
        // rotational body (the ready-made preset on a tilted torus, whose surface
        // curves through every frame direction and shows the streak the way a
        // machined ring does; a flat face has one constant frame, so a curved body
        // shows the finish best).
        row(y: -spacing / 2, labels: ["turn 0", "turn 45°", "turn 90°", "turned"],
            shape: { col in
                if col == 3 {
                    rotateX(.pi / 2 - 0.5)
                    drawTorus(radius: 0.68, tube: 0.3)
                } else {
                    drawSphere(radius: 0.92)
                }
            }) { col in
            fill(gold)
            if col == 3 {
                material(.brushedMetal)
            } else {
                material(Material(shading: .physicallyBased, metallic: 1, roughness: 0.4,
                                  anisotropy: 0.8,
                                  anisotropyRotation: Double(col) * .pi / 4))
            }
        }

        drawCaption("anisotropy: the brushed streak on the physically-based material")
    }

    /// One row of four bodies with a projected label under each.
    private func row(y: Double, labels: [String],
                     shape: (Int) -> Void, configure: (Int) -> Void) {
        for col in 0..<4 {
            let x = (Double(col) - 1.5) * spacing
            withState {
                translate(x, y, 0)
                configure(col)
                shape(col)
            }
            withState {
                textFont(OutlineFont.system)
                textSize(24)
                textAlign(.center)
                noStroke()
                fill(Color(white: 0.8))
                if let p = project(Vector3(x, y - 1.35, 0)) {
                    drawText(labels[col], at: p)
                }
            }
        }
    }
}

↑↓ move ↩ open