PhysicalMaterials
The physically-based finish as a metallic × roughness sweep. Material.physicallyBased shades one sphere grid from tight mirror highlights to matte, and from dielectric to metal.
- Run
swift run --package-path Examples Example-3D-Materials-PhysicalMaterials- Source
- Examples/3D/Materials/PhysicalMaterials/Sketch.swift
import Ollin
/// Physically-based materials: a metallic × roughness sweep.
///
/// `Material.physicallyBased` shades a mesh with an energy-conserving Cook-Torrance
/// microfacet model: the surface *color* is the current `fill`, and two numbers set the
/// rest. **Roughness** runs left to right (a tight, mirror-like highlight smearing into a
/// broad, soft one), and **metalness** runs top to bottom: the bottom row is a dielectric
/// (a colored diffuse body with a neutral highlight), the top row a metal (no diffuse, so
/// the highlight is tinted by the surface color, the way gold and copper are). The middle row
/// is the unphysical in-between, there to read the transition.
///
/// Lit by direct lights alone here, a smooth metal is mostly dark with a few bright moving
/// hotspots; that's physically what a metal with nothing to reflect does. Set an
/// `environment(_:)` and those darks fill in with reflections of the surroundings
/// (image-based lighting), which is what makes metals read as metal.
@main
final class PhysicalMaterials: Sketch {
let columns = 6 // roughness 0 … 1
let rows = 3 // metalness 1 (metal) … 0 (dielectric)
let spacing = 2.3
// One albedo per metalness row: a warm gold for the metal (so the tinted reflection
// reads), a neutral half, and a saturated blue dielectric.
let albedos = [
Color(hue: 0.11, saturation: 0.65, brightness: 0.95), // gold-ish metal
Color(white: 0.72), // neutral
Color(hue: 0.58, saturation: 0.70, brightness: 0.90), // blue dielectric
]
override func draw() {
background(Color(hex: 0x0B0C12))
// A gentle sway: the highlights slide because the point light below orbits, so
// the camera barely needs to move; this keeps the chart centered and its labels
// on-canvas. The radius/FOV leave a margin past the outer spheres for the labels.
cameraShowcase(.sway(amplitude: 0.22, period: .tau / 0.1), target: .zero, radius: 19,
elevation: 0.26, fieldOfView: .pi / 4)
// A warm key plus a cool fill, and a point light orbiting with us to keep the
// hotspots moving across every sphere.
let lightPos = Vector3(cos(time * 0.5) * 9, 7, sin(time * 0.5) * 9)
ambientLight(Color(white: 0.10))
directionalLight(Color(kelvin: 5400), direction: Vector3(-0.4, -0.7, -0.5), intensity: 1.1)
directionalLight(Color(kelvin: 8000), direction: Vector3(0.6, 0.2, 0.4), intensity: 0.35)
pointLight(.white, at: lightPos, intensity: 1.4)
for row in 0..<rows {
let metallic = 1.0 - Double(row) / Double(rows - 1) // 1 … 0
for col in 0..<columns {
let roughness = mapRoughness(col)
let pos = gridPosition(col: col, row: row)
withState {
translate(pos.x, pos.y, pos.z)
fill(albedos[row])
material(Material(shading: .physicallyBased,
metallic: metallic, roughness: roughness))
drawSphere(radius: 0.9)
}
}
}
drawAxisLabels()
}
// Roughness sweep, kept just off 0 so the smoothest sphere still shows a finite
// highlight rather than a single aliased pixel.
private func mapRoughness(_ col: Int) -> Double {
map(Double(col), 0, Double(columns - 1), 0.05, 1.0)
}
private func gridPosition(col: Int, row: 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)
}
// Labels are placed under the spheres (projected from world space) so they stay readable
// and on-canvas as the chart sways: each row's metalness under its leftmost sphere,
// and a roughness caption centered under the bottom row.
private func drawAxisLabels() {
withState {
textFont(OutlineFont.system)
textSize(26)
textAlign(.center)
noStroke()
fill(Color(white: 0.85))
let names = ["metal", "mixed", "dielectric"]
for row in 0..<rows {
let first = gridPosition(col: 0, row: row)
if let p = project(first + Vector3(0, -1.4, 0)) {
drawText(names[row], at: p)
}
}
// Centered under the bottom row (x = 0, the grid's axis of symmetry).
let bottomY = gridPosition(col: 0, row: rows - 1).y
if let p = project(Vector3(0, bottomY - 1.4, 0)) {
drawText("rougher →", at: p)
}
}
}
}