Fluid
a real-time incompressible fluid that carries color. Drag to swirl it (makeSimField(.fluid(...)))
- Run
swift run --package-path Examples Example-Simulation-Fluid- Source
- Examples/Simulation/Fluid/Sketch.swift
import Ollin
/// A real-time **fluid** evolving on the GPU through the effects substrate — the
/// multi-field sibling of reaction-diffusion and Game of Life. A `.fluid` `SimField`
/// keeps a velocity field and a dye (color) field across frames; each frame it carries
/// the flow along itself (advection), confines its vorticity for fine swirling detail,
/// and stays incompressible through a Jacobi pressure solve. You *draw into it* to
/// inject dye (the mark's color) and pass `withField`'s `force:` to push the flow.
///
/// A single brush wanders on its own so the fluid is always moving; press and drag to
/// stir it yourself. The dye color cycles over time, and a bloom gives the swirls glow.
/// See `Simulation/GrayScott` and `Simulation/Automata` for the single-field stateful sims.
@main
final class Fluid_Example: Sketch {
private var fluid: SimField!
private var prev = Vector2.zero
private var wasPressed = false
override func setup() {
// A half-resolution field: cheaper, with broad, creamy swirls. A gentle `curl`
// and a little more velocity damping keep the flow soft rather than gusty.
fluid = makeSimField(.fluid(curl: 12, velocityDissipation: 0.8), scale: 0.5)
prev = Vector2(width * 0.5, height * 0.5) // the brush starts at center (orbit's t = 0)
}
override func draw() {
// The brush: the mouse while pressed, otherwise a smooth Lissajous orbit that
// passes through the center at t = 0, so there's no jump on the first frame.
let brush: Vector2
if mouseIsPressed {
brush = mouse
} else {
let t = time * 0.6
brush = Vector2(width * (0.5 + 0.32 * sin(t * 1.3)),
height * (0.5 + 0.32 * sin(t * 0.9)))
}
// Grabbing or releasing the mouse shouldn't fling the fluid, so zero the push on
// the frame the brush mode changes (otherwise the jump becomes a huge impulse).
if mouseIsPressed != wasPressed { prev = brush }
wasPressed = mouseIsPressed
// The brush's motion gives the push (softened a little so it nudges rather than
// gusts); its slowly cycling color is the dye it lays down (hue/sat/value 0…1).
let force = (brush - prev) * 0.5
prev = brush
withField(fluid, force: force) {
noStroke()
fill(Color(hue: time * 0.08, saturation: 0.9, brightness: 1, alpha: 0.6))
drawCircle(brush.x, brush.y, 16)
}
// The dye covers the canvas, bloomed for glow. (`drawImage` already fills the
// frame, so no `background` is needed — and a main-canvas `background` here would
// wipe the seed drawn above, since it runs before the field is composited.)
drawImage(fluid.filtered(.bloom(threshold: 0.15, amount: 0.9)).image, 0, 0)
drawCaption("Fluid · a SimField evolving on the GPU · drag to stir")
}
}