Accumulation
By default Ollin clears the canvas at the start of every frame. That means draw() paints a fresh picture each time, and motion is the default. noClear() turns the clearing off, so the canvas becomes a persistent surface that survives across frames. Drawing then piles up over time instead of starting blank. Use it for long-exposure stills and paint-on-canvas sketches, and pair it with blendMode(.add) for light that keeps arriving.
A pile only ever grows. Some pictures should converge as samples arrive instead, which is the light-accumulation ("sandpainting") look. Those want the running mean, the sum divided by the number of passes. makeAccumulator() keeps that mean for you. This page covers both.
Contents
- noClear - stop clearing the canvas each frame
- background as the reset - wipe the accumulated canvas
- clearEachFrame - return to the default
- Accumulator - a layer that keeps a running mean, so light converges
- Notes
noClear()
noClear(precision: LayerPrecision = .float16)
Stop clearing the canvas each frame. From this point on, every frame draws on top of the accumulated result of all the frames before it. Most sketches call it once in setup().
override func setup() {
background(.black) // the one base wipe, the canvas starts here
noClear() // then accumulate forever
}
override func draw() {
blendMode(.add) // sum marks as light
fill(Color(white: 1, alpha: 0.05))
noStroke()
drawCircle(random(width), random(height), 3) // one faint dot per frame, piling up
}
Your draw() still records only that frame's new marks, because the geometry resets every frame as usual. What persists is the rendered canvas those marks accumulate onto.
Pair it with blendMode(.add) for the classic look. Each mark then adds a little light, so dense regions glow toward white while sparse ones hold a dim tint. Ollin blends in linear light, so the sum is physically correct. See the Rendering/Accumulation example, where slow pens leave faint traces that deepen where they cross again.
precision is how many bits the canvas keeps per channel. Half float, the default, keeps about three digits. That is plenty for paint and for light that builds quickly. A long exposure of very faint marks wants .float32: half float stops adding a mark once it is under about a thousandth of what the pixel already holds, so the pile levels off short of its true brightness. Single precision keeps adding at twice the memory. In a test pile seeded at linear 0.25 that took eight marks of 0.00005 each frame, half float held 0.25 for three hundred frames and single precision reached 0.37, the sum the arithmetic predicts. Changing the precision starts a fresh pile, as a change of canvas size does.
override func setup() {
noClear(precision: .float32) // ten thousand faint passes still add up
}
background as the reset
While the canvas accumulates, background(_:) wipes it to that color. That is how you reset a long exposure or start a new pass. Call it on the frame you want to clear:
override func draw() {
if frameCount % 600 == 0 { background(.black) } // wipe every 10s at 60fps
// …accumulate…
}
Omit it entirely and the canvas accumulates without bound. Call it every frame and you are back to clearing each frame, which draws onto a fresh canvas every time.
clearEachFrame()
Return to the default and clear the canvas at the start of every frame. This undoes noClear().
clearEachFrame()
Accumulator
An Accumulator is a layer that keeps the running mean of everything drawn into it, frame after frame. A noClear canvas holds a sum that only grows. An accumulator holds the sum and the number of passes, and gives back their ratio. A picture built from faint random samples then settles toward the picture those samples describe. That is how the depth-of-field look converges. Each frame scatters another pass of a million points. The average of a thousand passes is a thousand times less grainy than one pass, at the same brightness.
var light: Accumulator!
override func setup() { light = makeAccumulator() } // make once, store it
override func draw() {
background(.black)
withAccumulator(light, passes: 4) { // this frame's samples
blendMode(.add)
drawParticles(samples, count: count, style: .light)
}
drawImage(light.developed(exposure: 30, ground: Color(hex: 0x151010)).image, 0, 0)
}
withAccumulator(_:passes:_:)draws one pass of samples into the layer and adds it into the sum. The layer is a transient surface that the renderer clears every frame. A block that draws several passes' worth of samples declares that withpasses:, so the mean divides by the right count. SetblendMode(.add)inside the block so the samples sum as light, and draw particles withstyle: .light, the radiometric deposit.light.imageis the mean as anImage, in linear light.light.sumis the raw sum in single-precision float, andlight.passesis the count, for a shader that divides on its own.light.developed(exposure:ground:)prints the mean throughFilter.develop. The mean is scaled byexposure, rolled off through the Reinhard curve, and laid onground. That ground is added after the curve, as a display color.light.filtered(_:)runs any other filter over the mean, andlight.combined(with:_:)makes the mean the base of aCombine.light.reset()starts the average over. Call it when the scene, the camera, or the lens moved, because the samples drawn before no longer describe the picture.LineSpraycalls it for you.- The sum lives in single-precision float whatever the layer's other settings are. A value that only grows then keeps every bit that half float would drop. The mean is served in half float, like any layer. An accumulator is persistent, the way
Feedbackis, so make it once insetup()and hold it.
Exposure is a print setting. It scales the mean once the mean has converged, so changing it does not restart the average. See the Rendering/DepthOfField example, and Depth of field from light for the lens built on top.
Notes
- A pile brightens, but a mean converges. A static scene drawn additively onto a
noClearcanvas keeps getting brighter until it saturates to white. That happens because the surface holds a sum, whatever the tone map does afterward. Keep such a scene moving, with a slow rotation, drifting particles, or a sweep. Light then flows across the canvas and reaches a steady glow. The other option is anAccumulator. Its picture is the sum divided by the passes, so it settles at the same brightness however long it runs. - Float precision. The accumulation surface composites in linear half float. A faint sample, well below 1/255, adds up instead of quantizing away while the pixel is still dark, and light can build past full brightness. Use
toneMap(_:)to roll that built-up light off smoothly rather than clipping it. Half float keeps about three digits, though. A sample stops counting once it falls under roughly one part in a thousand of what the pixel already holds, so a long exposure of faint strokes on a half-float canvas stops brightening. For a long sum, ask for single precision withnoClear(precision: .float32), or draw the pile into aFeedbacklayer made withmakeFeedback(precision: .float32). An accumulator keeps its own sum in single precision. - Window resizing resets it. The persistent surface is sized to the window, so resizing reallocates it and starts the accumulation over.
- Export works the same way. The headless still (
--export --frame N) drives the accumulation across frames just like the live window. The sequence, video, and GIF exports do the same. What you export therefore matches what you see.