Hologram
The hologram finish: a diffraction grating in a polished metal whose rainbow sits where the angle between the light and the eye says, on a flat plate, a sphere, and a crumpled sheet whose facets turn the grooves (Material.hologram, gratingSpacing, gratingRotation).
- Run
swift run --package-path Examples Example-3D-Materials-Hologram- Source
- Examples/3D/Materials/Hologram/Sketch.swift
import Ollin
/// The hologram finish: a diffraction grating ruled into a metal, the rainbow
/// of a hologram sticker or the back of a disc. Fine parallel grooves send each
/// color of a light off at its own angle, so the color at a point follows the
/// angle between the light and the eye and walks through the spectrum as
/// either moves. `Material.hologram` turns it on; `gratingSpacing` is the
/// distance between the grooves in **nanometers**, light's own scale, and
/// `gratingRotation` turns them in the surface.
///
/// Three bodies share the finish under one light that circles. The **plate**
/// shows the grating equation plainly: the rainbow is a band whose colors sit
/// where their wavelength says, and it slides across the plate as the light
/// moves. The **sphere** puts the first order on both sides of the highlight,
/// with the fainter second and third orders beyond. The **sheet** carries a
/// crumpled normal map, so every facet turns the grooves its own way and the
/// rainbow breaks into pieces: holographic foil in 3D. The map is authored in
/// `setup()` from a `FacetField`, the same sheet of facets the 2D foil filter
/// reads, and `crinkle` is its strength.
@main
final class Hologram: Sketch {
@Param("Grating spacing", 600 ... 2000, icon: "line.3.horizontal") var spacing = 1000.0
@Param("Roughness", 0.05 ... 0.8, icon: "circle.lefthalf.filled") var roughness = 0.3
@Param("Crinkle", 0 ... 2, icon: "scribble") var crinkle = 1.0
@Param("Light speed", 0 ... 1, icon: "sun.max") var lightSpeed = 0.25
private var crumple: Image?
override func setup() {
// A sheet of flat facets as a green-up normal map: each texel holds the
// normal of the facet under it, so the grooves turn per facet.
let size = 512
let field = FacetField.crumpled(in: Rectangle(x: 0, y: 0, width: Double(size), height: Double(size)),
spacing: 64, crinkle: 0.8, seed: 7)
var bytes = [UInt8](repeating: 255, count: size * size * 4)
for y in 0..<size {
for x in 0..<size {
let p = Vector2(Double(x) + 0.5, Double(y) + 0.5)
let n = field.facet(containing: p)?.normal ?? Vector3(0, 0, 1)
let i = (y * size + x) * 4
bytes[i] = UInt8((n.x * 0.5 + 0.5) * 255)
bytes[i + 1] = UInt8((n.y * 0.5 + 0.5) * 255)
bytes[i + 2] = UInt8((n.z * 0.5 + 0.5) * 255)
}
}
crumple = Image(width: size, height: size, premultipliedRGBA: bytes)
}
override func draw() {
background(Color(hex: 0x0B0D12))
cameraShowcase(.sway(amplitude: 0.14, period: .tau / 0.07), target: Vector3(0, 0.1, 0),
radius: 12.2, elevation: 0.14, fieldOfView: .pi / 4.6)
// A dim room so the metal between the orders reads as metal; the
// rainbow itself answers the circling key light alone.
environment(.studio.intensified(to: 0.35).lightingOnly())
toneMap(.aces, exposure: 1.0)
// The key swings across the front of the three bodies, so the plate's
// band slides across it and comes back; at the start it sits mid-plate.
let a = time * lightSpeed * .tau
directionalLight(.white,
direction: Vector3(-(0.35 + 0.5 * sin(a)), -0.45, -(0.55 + 0.35 * cos(a))),
intensity: 1.3)
var finish = Material.hologram
finish.gratingSpacing = spacing
finish.roughness = roughness
fill(Color(white: 0.82))
// Left: the flat plate.
withState {
translate(-2.75, 0.1, 0)
rotateX(.pi / 2)
material(finish)
drawPlane(width: 2.3, depth: 2.3)
}
// Middle: the sphere.
withState {
translate(0, 0.1, 0)
material(finish)
drawSphere(radius: 1.1)
}
// Right: the crumpled sheet, the grooves turned by every facet.
if let crumple {
withState {
translate(2.75, 0.1, 0)
rotateX(.pi / 2)
material(finish)
drawMesh(Mesh.plane(width: 2.3, depth: 2.3).normalMapped(crumple, scale: crinkle))
}
}
// A plain metal floor, so the three read as objects in a room.
withState {
translate(0, -1.35, 0)
fill(Color(white: 0.45))
material(.metal(roughness: 0.5))
drawPlane(width: 11, depth: 6)
}
drawCaption("a grating in the metal: the rainbow sits where the angle says")
}
}