Explorer
The material explorer. Every finish is shown on one shape, with its parameters to adjust.
- Run
swift run --package-path Examples Example-3D-Materials-Explorer- Source
- Examples/3D/Materials/Explorer/Sketch.swift
import AppKit
import Ollin
/// The material explorer: every finish in the hand.
///
/// The whole curated `Material` library on one Preset menu, every finish scalar on
/// its own parameter, and the stage around the surface (lighting preset, environment,
/// fog, shadows, the solid it's judged on) switchable while you look. Picking a
/// preset snaps the parameters to its values; drag any parameter to tweak from there, and the
/// caption notes when the finish has left its preset. Press a key to step through
/// the presets; the mouse stays free for the camera (drag to orbit, scroll to
/// dolly). Press C to copy the parameters as the `Material(...)` expression that
/// rebuilds them (`swiftSource`), ready to paste into a sketch.
///
/// Things worth trying: `polishedMetal` under the `city` backdrop (a mirror needs
/// something to reflect), `frostedGlass` over `sunset` (transmission needs an
/// environment to transmit), `sequin` while the camera orbits (the flakes flash
/// with the view), and `felt` with a `sheenColor` far from the surface color (the
/// two-tone velvet look).
@main
final class MaterialExplorer: Sketch {
enum PreviewShape: CaseIterable, ParamOption {
case sphere, torus, knot, box, cylinder
}
enum Backdrop: CaseIterable, ParamOption {
case none, studio, city, courtyard, forest, interior, night, sunrise, sunset, sky
var environment: Environment? {
switch self {
case .none: nil
case .studio: .studio
case .city: .city
case .courtyard: .courtyard
case .forest: .forest
case .interior: .interior
case .night: .night
case .sunrise: .sunrise
case .sunset: .sunset
case .sky: .sky()
}
}
}
enum ShadingChoice: CaseIterable, ParamOption {
case standard, toon, gooch, physicallyBased
var model: Material.Shading {
switch self {
case .standard: .standard
case .toon: .toon
case .gooch: .gooch
case .physicallyBased: .physicallyBased
}
}
init(_ shading: Material.Shading) {
switch shading {
case .standard: self = .standard
case .toon: self = .toon
case .gooch: self = .gooch
case .physicallyBased: self = .physicallyBased
}
}
}
// The starting point, and what it's judged on.
@Param(group: "Preset") var preset: Material = .glossy
@Param(group: "Preset") var shape: PreviewShape = .sphere
@Param(group: "Preset") var surfaceColor = Color(hue: 0.05, saturation: 0.55, brightness: 0.85)
// The stage around it.
@Param(group: "Stage") var mood: LightingPreset = .studio
@Param(group: "Stage") var backdrop: Backdrop = .studio
@Param(group: "Stage") var shadows = true
@Param(group: "Stage") var haze = false
@Param("Air", icon: "cloud.fog", group: "Stage") var air: Fog = .haze
// The finish, parameter by parameter. Defaults spell out `.glossy`, the opening preset.
@Param(group: "Finish") var shading: ShadingChoice = .standard
@Param(1...12, group: "Finish") var toonBands = 4.0
@Param("Specular", 0...1, group: "Finish") var specularLevel = 0.9
@Param("Shininess", 1...256, group: "Finish") var shininessLevel = 160.0
@Param(0...1, group: "Physically based") var metallic = 0.0
@Param(0...1, group: "Physically based") var roughness = 0.5
@Param(-1...1, group: "Physically based") var anisotropy = 0.0
@Param(0...1, group: "Glass") var transmission = 0.0
@Param("IOR", 1...2.5, group: "Glass") var ior = 1.5
@Param(0...3, group: "Glass") var thickness = 0.0
@Param(0...1, group: "Coat and cloth") var clearcoat = 0.0
@Param("Coat roughness", 0...1, group: "Coat and cloth") var clearcoatRoughness = 0.0
@Param(0...1, group: "Coat and cloth") var sheen = 0.0
@Param(0...1, group: "Coat and cloth") var sheenRoughness = 0.5
@Param(group: "Coat and cloth") var sheenColor: Color = .white
@Param(0...1, group: "Shimmer") var iridescence = 0.0
@Param(0...3, group: "Shimmer") var iridescenceScale = 1.0
@Param(0...1, group: "Shimmer") var sparkle = 0.0
@Param(0.05...12, group: "Shimmer") var sparkleSize = 1.0
@Param(1...100, group: "Shimmer") var sparkleSharpness = 48.0
@Param(0...1, group: "Glow") var rim = 0.0
@Param(0.1...6, group: "Glow") var rimSharpness = 2.0
@Param(0...1, group: "Glow") var subsurface = 0.0
@Param(group: "Glow") var subsurfaceColor: Color = .white
private var lastPreset: Material?
private var copiedUntil = -1.0
/// Parameters hide while the shader ignores them, so the panel only offers what
/// can change the picture: the physically-based set acts under that shading
/// model alone, the specular pair only outside it, and each dependent scalar
/// waits for the parameter that turns its term on. IOR stays with the
/// physically-based set (not behind transmission) because it also shapes the
/// normal-incidence reflectance of an opaque surface.
override func setup() {
$toonBands.show(when: $shading) { $0 == .toon }
$specularLevel.show(when: $shading) { $0 != .physicallyBased }
$shininessLevel.show(when: $shading) { $0 != .physicallyBased }
$metallic.show(when: $shading) { $0 == .physicallyBased }
$roughness.show(when: $shading) { $0 == .physicallyBased }
$anisotropy.show(when: $shading) { $0 == .physicallyBased }
$transmission.show(when: $shading) { $0 == .physicallyBased }
$ior.show(when: $shading) { $0 == .physicallyBased }
$thickness.show(when: $transmission) { $0 > 0 }
$clearcoat.show(when: $shading) { $0 == .physicallyBased }
$clearcoatRoughness.show(when: $clearcoat) { $0 > 0 }
$sheen.show(when: $shading) { $0 == .physicallyBased }
$sheenRoughness.show(when: $sheen) { $0 > 0 }
$sheenColor.show(when: $sheen) { $0 > 0 }
$iridescenceScale.show(when: $iridescence) { $0 > 0 }
$sparkleSize.show(when: $sparkle) { $0 > 0 }
$sparkleSharpness.show(when: $sparkle) { $0 > 0 }
$rimSharpness.show(when: $rim) { $0 > 0 }
$subsurfaceColor.show(when: $subsurface) { $0 > 0 }
}
override func keyPressed() {
if key == "c" || key == "C" {
copyFinish()
return
}
let roster = Material.paramChoices
let index = roster.firstIndex { $0.value == preset } ?? 0
preset = roster[(index + 1) % roster.count].value
}
/// Put the parameters on the clipboard as the expression that rebuilds them,
/// and print it, so a terminal run shows what was copied.
private func copyFinish() {
let source = tunedMaterial.swiftSource
NSPasteboard.general.clearContents()
NSPasteboard.general.setString(source, forType: .string)
print(source)
copiedUntil = time + 2.5
}
/// Copy a preset's values onto the parameters, so tweaking starts from it.
private func snap(to m: Material) {
shading = ShadingChoice(m.shading)
toonBands = m.toonBands
specularLevel = m.specular
shininessLevel = m.specularSharpness
metallic = m.metallic
roughness = m.roughness
anisotropy = m.anisotropy
transmission = m.transmission
ior = m.ior
thickness = m.thickness
clearcoat = m.clearcoat
clearcoatRoughness = m.clearcoatRoughness
sheen = m.sheen
sheenRoughness = m.sheenRoughness
sheenColor = m.sheenColor
iridescence = m.iridescence
iridescenceScale = m.iridescenceScale
sparkle = m.sparkle
sparkleSize = m.sparkleSize
sparkleSharpness = m.sparkleSharpness
rim = m.rim
rimSharpness = m.rimSharpness
subsurface = m.subsurface
subsurfaceColor = m.subsurfaceColor
}
/// The material the parameters currently spell.
private var tunedMaterial: Material {
var m = Material()
m.shading = shading.model
m.toonBands = toonBands
m.specular = specularLevel
m.specularSharpness = shininessLevel
m.metallic = metallic
m.roughness = roughness
m.anisotropy = anisotropy
m.transmission = transmission
m.ior = ior
m.thickness = thickness
m.clearcoat = clearcoat
m.clearcoatRoughness = clearcoatRoughness
m.sheen = sheen
m.sheenRoughness = sheenRoughness
m.sheenColor = sheenColor
m.iridescence = iridescence
m.iridescenceScale = iridescenceScale
m.sparkle = sparkle
m.sparkleSize = sparkleSize
m.sparkleSharpness = sparkleSharpness
m.rim = rim
m.rimSharpness = rimSharpness
m.subsurface = subsurface
m.subsurfaceColor = subsurfaceColor
return m
}
override func draw() {
// Snap the parameters when the Preset menu changes while running. The first
// frame only records the selection, so values restored across a reload
// (or tuned before a save) aren't clobbered.
if lastPreset == nil {
lastPreset = preset
} else if preset != lastPreset {
snap(to: preset)
lastPreset = preset
}
background(Color(white: 0.05))
cameraShowcase(.autoOrbit(), target: Vector3(0, -0.15, 0), radius: 4.8,
elevation: 0.26, fieldOfView: .pi / 4)
lightingPreset(mood)
if let environment = backdrop.environment { self.environment(environment) }
castShadows(shadows)
if haze { fog(air) }
// The ground that catches the shadow: matte, neutral.
withState {
translate(0, -1.4, 0)
fill(Color(white: 0.5))
material(.matte)
drawPlane(width: 18, depth: 18)
}
// The surface under judgment.
let m = tunedMaterial
withState {
fill(surfaceColor)
material(m)
switch shape {
case .sphere: drawSphere(radius: 1.15, segments: 96, rings: 48)
case .torus: drawTorus(radius: 0.9, tube: 0.42, segments: 96, sides: 48)
case .knot: drawTorusKnot(p: 2, q: 3, radius: 0.8, tube: 0.28)
case .box:
rotateY(0.5)
drawBox(size: 1.7)
case .cylinder: drawCylinder(radius: 0.8, height: 1.9, segments: 96)
}
}
if time < copiedUntil {
drawCaption("Copied as Swift to the clipboard")
} else {
let name = Material.paramChoices.first { $0.value == preset }?.name ?? "custom"
let tweaked = m != preset ? " (tweaked)" : ""
drawCaption("Material: \(name)\(tweaked) · press a key for the next preset, C to copy as Swift")
}
}
}