Lighting
The three light kinds placed by hand to shade solids: a fixed directional key, an orbiting point bulb, and a sweeping spot. A row of spheres shows rising specularSharpness.
- Run
swift run --package-path Examples Example-3D-Lighting-Lighting- Source
- Examples/3D/Lighting/Lighting/Sketch.swift
import Ollin
/// Lights & materials — the three light kinds shading solid 3D primitives.
///
/// A solid takes the current `fill` as its surface color and is shaded by a
/// Blinn-Phong material. With no lights at all a solid still looks 3D (the auto-lit
/// default rig); here we set the lights by hand to show each kind:
///
/// - **directional** — a fixed warm key from the upper-left (parallel rays, the sun).
/// - **point** — a cyan bulb orbiting the scene, so its highlight slides across
/// the spheres.
/// - **spot** — a magenta cone sweeping left-to-right from above, lighting only
/// what falls inside it, with a soft penumbra edge.
///
/// The front row of spheres shares one fill but climbs in `specularSharpness` left→right, so
/// the specular highlight tightens from a broad sheen to a sharp glint. Lighting is
/// per-frame (one setup shades every mesh this frame), so the lights are placed once
/// at the top of `draw()`. Small balls mark the moving lights' positions.
@main
final class Lighting3D: Sketch {
override func draw() {
background(Color(hex: 0x0A0B10))
cameraShowcase(.sway(amplitude: 0.5, period: .tau / 0.15), target: Vector3(0, -0.2, 0), radius: 11,
elevation: 0.35, fieldOfView: .pi / 4)
// The moving lights' positions, reused for their marker balls below.
let pointPos = Vector3(cos(time * 0.7) * 3.5, 1.8, sin(time * 0.7) * 3.5)
let spotPos = Vector3(sin(time * 0.5) * 3.0, 4.5, 0.5)
let spotAim = Vector3(sin(time * 0.5) * 1.2, -1, 0).normalized
// --- Lights (per-frame; set once, shade every mesh drawn this frame) ---
ambientLight(Color(white: 0.12))
directionalLight(Color(hue: 0.09, saturation: 0.35, brightness: 1.0),
direction: Vector3(-0.5, -0.8, -0.4), intensity: 0.8)
pointLight(Color(hue: 0.5, saturation: 0.8, brightness: 1.0), at: pointPos, intensity: 1.0)
spotLight(Color(hue: 0.85, saturation: 0.7, brightness: 1.0),
at: spotPos, direction: spotAim, coneAngle: .pi / 5, penumbra: 0.5, intensity: 1.4)
// Ground plane — matte, so it shows the spot's pool and the moving highlights.
withState {
translate(0, -1.3, 0)
fill(Color(white: 0.5))
drawPlane(width: 14, depth: 14)
}
// Front row: one fill, rising specularSharpness, so the highlight tightens left to right.
let sharpnesses = [4.0, 16, 48, 128, 320]
for (i, s) in sharpnesses.enumerated() {
withState {
translate(-4 + Double(i) * 2, -0.4, 1.5)
fill(Color(hue: 0.05, saturation: 0.55, brightness: 0.9))
specular(0.7)
specularSharpness(s)
drawSphere(radius: 0.8)
}
}
// Back row: a few other solids, spinning, with a softer material.
let solids: [Mesh] = [.box(size: 1.4), .torus(radius: 0.7, tube: 0.28),
.icosahedron(radius: 0.95), .cylinder(radius: 0.6, height: 1.6)]
for (i, mesh) in solids.enumerated() {
withState {
translate(-3.6 + Double(i) * 2.4, 0.1, -1.8)
rotateY(time * 0.4 + Double(i))
rotateX(0.3)
fill(Color(hue: 0.55 + Double(i) * 0.12, saturation: 0.5, brightness: 0.9))
specular(0.35)
specularSharpness(40)
drawMesh(mesh)
}
}
// Mark the moving lights with little balls in their own color. (They're shaded
// like everything else — bright because they sit right at the source.)
markLight(at: pointPos, color: Color(hue: 0.5, saturation: 0.8, brightness: 1.0))
markLight(at: spotPos, color: Color(hue: 0.85, saturation: 0.7, brightness: 1.0))
drawCaption("Lights & materials: directional + orbiting point + sweeping spot; specularSharpness rises left to right")
}
private func markLight(at position: Vector3, color: Color) {
withState {
translate(position.x, position.y, position.z)
fill(color)
drawSphere(radius: 0.16)
}
}
}