Ollin

RetainedBatch

150k stars recorded once into a Batch and replayed at no cost, plus a rosette stamped at many placements (makeBatch/drawBatch; flip the parameter to see the per-frame cost)

Run
swift run --package-path Examples Example-Rendering-RetainedBatch
Source
Examples/Rendering/RetainedBatch/Sketch.swift
import Ollin

/// A star chart drawn once, replayed every frame. `setup()` scatters 150,000
/// stars into a spiral galaxy and records them into a `Batch`: the geometry
/// uploads to the GPU once, and each frame's `drawBatch(field)` replays it for
/// (almost) nothing. The transform in force at `drawBatch` time moves the whole
/// recording as a unit, so the galaxy turns with one `rotate` even though no
/// star is ever re-recorded.
///
/// The second batch shows the other use: a compass rosette recorded once and
/// *stamped* five times around the chart, each at its own position, angle, and
/// size. One recording, many placements.
///
/// Flip the **retained** parameter off to draw the same stars through the ordinary
/// per-frame path (the exact same helper the recording captured) and watch the
/// frame time in the inspector: that gap is the recording cost the batch pays
/// once instead of every frame.
@main
final class RetainedBatch: Sketch {
    override var loopDuration: Double? { 40 }

    @Param var retained = true   // off = re-record all 150k stars every frame

    private struct Star {
        var position: Vector2
        var radius: Double
        var color: Color
    }

    private var stars: [Star] = []
    private var field: Batch!
    private var rosette: Batch!

    override func setup() {
        seed(31_417)
        makeStars()
        // Record once. The body is the same call the dynamic path makes below;
        // the batch simply remembers its geometry so the frames don't redo it.
        field = makeBatch { drawStars() }
        rosette = makeBatch { drawRosette() }
        noStroke()
    }

    override func draw() {
        background(Color(hex: 0x060612))

        // The galaxy, turning: the draw-time transform moves the whole replay,
        // so the 150k recorded stars turn with one rotate about the canvas center.
        withState {
            translate(center)
            rotate(loopProgress(over: 40) * .tau)
            translate(-width / 2, -height / 2)
            if retained {
                drawBatch(field)
            } else {
                drawStars()          // the same stars, re-recorded every frame
            }
        }

        // The rosette, stamped: one recording, five placements. Each stamp is a
        // fresh transform around the same batch.
        for i in 0 ..< 5 {
            let a = Double(i) / 5 * .tau - .tau / 4
            let orbit = shortSide * 0.42
            withState {
                translate(width / 2 + cos(a) * orbit, height / 2 + sin(a) * orbit)
                rotate(a + loopProgress(over: 40) * .tau)
                scale(i == 0 ? 1.0 : 0.55)
                drawBatch(rosette)
            }
        }
    }

    /// The static composition: two log-spiral arms of gaussian-scattered stars,
    /// a warm dense core, and a faint halo. Built once in `setup()` so the
    /// retained and per-frame paths draw the identical field.
    private func makeStars() {
        let center = center
        let reach = shortSide * 0.46
        stars.reserveCapacity(150_000)

        let armColors = [Color(hex: 0x8DB4FF), Color(hex: 0xC8D9FF)]
        for i in 0 ..< 110_000 {
            // Two arms: angle marches with radius (a log spiral), scatter widens outward.
            let t = pow(random(1), 0.6)
            let arm = Double(i % 2) * .pi
            let angle = arm + t * 3.4 + randomGaussian() * 0.18
            let r = reach * t
            let p = center + Vector2(angle: angle) * r
                  + Vector2(randomGaussian(), randomGaussian()) * reach * 0.035
            let bright = random(0.25, 1)
            stars.append(Star(position: p,
                              radius: random(0.5, 1.4) * (bright > 0.97 ? 2.2 : 1),
                              color: randomChoice(armColors).withAlpha(bright * 0.8)))
        }
        for _ in 0 ..< 25_000 {   // the core: tight, warm
            let p = center + Vector2(randomGaussian(), randomGaussian()) * reach * 0.08
            stars.append(Star(position: p, radius: random(0.6, 1.8),
                              color: Color(hex: 0xFFD9A0).withAlpha(random(0.3, 0.9))))
        }
        for _ in 0 ..< 15_000 {   // the halo: sparse, dim, everywhere
            let p = Vector2(random(width), random(height))
            stars.append(Star(position: p, radius: random(0.4, 1),
                              color: Color(hex: 0x9FB0D8).withAlpha(random(0.1, 0.4))))
        }
    }

    private func drawStars() {
        for star in stars {
            fill(star.color)
            drawCircle(star.position.x, star.position.y, star.radius)
        }
    }

    /// A small engraved compass rosette: a hollow ring, a tick ring, and an
    /// eight-point star, all region shapes so the whole motif is a handful of
    /// SDF instances.
    private func drawRosette() {
        let ink = Color(hex: 0xE8DDC4)
        noFill()
        stroke(ink.withAlpha(0.9))
        strokeWeight(1.5)
        drawCircle(0, 0, 46)
        drawCircle(0, 0, 34)
        noStroke()
        fill(ink.withAlpha(0.9))
        for i in 0 ..< 16 {
            withState {
                rotate(Double(i) / 16 * .tau)
                drawRect(-0.8, -44, 1.6, i % 4 == 0 ? 7 : 4)
            }
        }
        drawStar(0, 0, 30, 7, points: 8)
        fill(Color(hex: 0x060612))
        drawCircle(0, 0, 5)
        fill(ink)
        drawCircle(0, 0, 2.5)
    }
}

↑↓ move ↩ open