Ollin

LiveSurface

The camera frame wraps a globe and lights it, so a live feed serves as a surface.

Run
swift run --package-path Examples Example-3D-Materials-LiveSurface
Imports
OllinVision OllinSamplePhotos
Source
Examples/3D/Materials/LiveSurface/Sketch.swift
import Ollin
import OllinSamplePhotos
import OllinVision

/// The camera as a surface: the webcam's frame wrapped around a globe, and lighting it.
///
/// A camera frame is an `Image`, and an `Image` goes wherever an image goes: onto a
/// mesh through `textured(_:)`, and around the scene as its light through
/// `environment(.feed(feed))`. A live picture on a surface therefore asks for no
/// API of its own:
///
/// ```swift
/// environment(.feed(feed))                    // the room as the light
/// if let frame = feed.frame {
///     drawMesh(globe.textured(frame))         // the room as the surface
/// }
/// ```
///
/// Two things shape the sketch instead of the framework. The material is set every
/// frame, since each capture arrives as a fresh `Image`; and a fresh `Image` builds
/// its texture the first time it is drawn, so the feed costs one upload per new
/// frame and nothing while the frame holds. That is cheap for one surface and adds
/// up across many, so put the feed on the thing that matters and let the same feed
/// light the rest. Walk past the camera and the globe carries you around with it.
/// With no camera, or under `--photo`, the globe shows the bundled city instead.
@main
final class LiveSurface: Sketch {

    let feed = Camera.orStill(SamplePhoto.city.load())
    /// Built once; only its material changes, once per new frame.
    let globe = Mesh.sphere(radius: 1.7, segments: 64, rings: 32)

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

        // Before the first frame, the standard waiting notice; nothing to show yet.
        guard let frame = feed.frame else { drawFrame(feed); return }

        // The room as the light: the same frames the globe shows.
        environment(.feed(feed))

        cameraShowcase(.sway(amplitude: 0.3, period: .tau / 0.08), target: .zero,
                       radius: 7.5, elevation: 0.14, fieldOfView: .pi / 4.6)

        // The room as the surface: a fresh image each frame, one upload each.
        withState {
            rotateY(time * 0.25)
            fill(.white)
            material(.dielectric(roughness: 0.5))
            drawMesh(globe.textured(frame))
        }

        // A chrome ball beside it, reflecting the room the globe is showing.
        withState {
            translate(2.9, -0.9, 0.6)
            fill(.white)
            material(.metal(roughness: 0.05))
            drawSphere(radius: 0.65)
        }

        drawCaption("The camera frame as a surface, and as the light")
    }
}

↑↓ move ↩ open