Ollin

Noise

Perlin noise is coherent, which means nearby inputs give nearby outputs. That reads as smooth, organic variation, so noise is the smooth counterpart to the uncorrelated Random. It lives on the sketch and takes a seed, and the output is contrast-calibrated to fill its range.

Two framed strips: the top a jagged hash of random heights, the bottom a smooth rolling curve from noise

Contents

Functions

noise

noise(_ x: Double[, _ y: Double[, _ z: Double]]) -> Double

1D/2D/3D Perlin noise in 0...1, contrast-calibrated to fill the range.

let n = noise(x * 0.01, y * 0.01)        // 0...1, smooth across the canvas
drawCircle(x, y, n * 20)

Two facts about every Perlin form here, the looping ones included, follow from how it is built:

  • The field repeats every 256 units on each axis. The lattice is hashed through a table of 256 entries, so noise(x) and noise(x + 256) are the same value, and so are two reads 256 apart in y or z. An offset used to give each copy of something a noise of its own should not be a multiple of 256, and a step whose multiples land on 256 repeats: copies offset by 32 * i read the same field every eighth copy. An offset that is not a power of two (37.3 * i) or a seed of its own per copy avoids it.
  • The ends of the range are flat. Contrast-calibrated means the raw field is scaled up and the overshoot clamped, which is what lets it reach 0 and 1. The price is that a share of the field sits exactly on an end: over 800,000 samples, 1.9% of noise(x, y) is exactly 0 and 2.0% exactly 1, and the looping forms 2 to 3.5% at each end. A curve read straight from the field runs flat there. Two reads at unrelated offsets averaged and rescaled are flat only where both clamp at once.

signedNoise

signedNoise(_ x: Double[, _ y: Double[, _ z: Double]]) -> Double

The same field in -1...1, following the ofSignedNoise and OPENRNDR convention. Use it for offsets that swing both ways.

let dx = signedNoise(time * 0.5) * 40   // drift left and right
drawCircle(width / 2 + dx, height / 2, 30)

Looping noise: loop:

noise(loop: Double, radius: Double = 1) -> Double
noise(_ x: Double, loop: Double, radius: Double = 1) -> Double
noise(_ x: Double, _ y: Double, loop: Double, radius: Double = 1) -> Double
noise(_ x: Double, _ y: Double, _ z: Double, loop: Double, radius: Double = 1) -> Double
signedNoise(/* same four forms */) -> Double

Noise that loops. As loop runs 0...1, the sample follows a closed circle through the field and lands exactly where it started. A drift driven by loop: therefore returns to its starting value every lap, which is what a seamless GIF needs. A growing input never does that, because noise(x, y, time * 0.1) walks a straight line through the field and never comes back. Feed loop: looping progress instead:

let lap = loopProgress(over: 6)                      // 0...1, every 6 seconds
let n = noise(x * 0.006, y * 0.006, loop: lap)       // a field that drifts and returns
let sway = signedNoise(x * 0.002, loop: lap) * 40    // a looping offset

radius is how much of the field one lap covers, so a bigger lap changes more per cycle. The same parameter closes loops in space too. Sample around a ring by its angle, and the variation meets itself where the ring closes:

let bump = noise(2.5, loop: angle / .tau, radius: 0.8)   // seamless around a circle

A whole volume loops the same way. noise(x, y, z, loop:) is the field a point cloud, a swarm, or a block of boxes reads at its own place in space, and it comes back to where it started every lap:

let points = (0 ..< 400).map { _ in Vector3(random(-5, 5), random(-5, 5), random(-5, 5)) }
let lap = loopProgress(over: 12)
for p in points {
    let drift = signedNoise(p.x * 0.2, p.y * 0.2, p.z * 0.2, loop: lap)   // a volume that returns
    withState { translate(p + Vector3(0, drift, 0)); drawSphere(radius: 0.05) }
}

Under the hood, the loop uses extra noise dimensions: the circle a lap follows takes two axes of its own beside the point's. So the two-coordinate form samples a 4D field and the three-coordinate form a 5D one. That is the standard construction for a looping animation, and nothing is blended between two reads, so the field keeps one character all the way round. The 5D field is scaled to spread like the 3D one: moving a sketch from noise(x, y, z) to its looping form keeps its contrast. Exports and snapshots stay deterministic, because loop wraps, so loop: 0 and loop: 1 are the same sample bit for bit.

fbm / signedFbm

fbm(_ x[, _ y[, _ z]], octaves: Int = 4, gain: Double = 0.5, lacunarity: Double = 2) -> Double
fbm(_ x, _ y[, _ z], loop: Double, radius: Double = 1, octaves: Int = 4, ...) -> Double
signedFbm(/* the same forms */) -> Double

Fractal (layered) noise in 0...1. It sums octaves samples of the field, and each octave is lacunarity× smaller in feature size and gain× lighter in weight. The sum is normalized, so the result still fills the range. One call gives you the shape-plus-detail layering you would otherwise write by hand, such as noise(x * s) * 0.7 + noise(x * s * 8) * 0.3. With octaves: 1 you get exactly noise. The name and defaults match the shader library's fbm, so the same vocabulary carries into per-pixel code.

Three framed strips: a slow big-scale noise curve labeled shape, a busy small-scale curve labeled detail, and their weighted sum showing gentle terrain with fine texture on top
let ridge = fbm(x * 0.004)                            // big moves plus fine grain
let ground = fbm(x * 0.003, y * 0.003, octaves: 5)    // a busier field
let weather = fbm(x * 0.003, y * 0.003, loop: loopProgress(over: 8))

signedFbm is the same value in -1...1. In the looping form, every octave follows its own closed circle, so the layered field loops too.

Tiling noise: a picture that repeats

tilingNoise(_ u: Double, _ v: Double, detail: Double = 4) -> Double
signedTilingNoise(_ u: Double, _ v: Double, detail: Double = 4) -> Double
tilingFbm(_ u: Double, _ v: Double, detail: Double = 4, octaves: Int = 4, gain: Double = 0.5, lacunarity: Double = 2) -> Double
signedTilingFbm(/* the same form */) -> Double

Noise that tiles. As u and v each run 0...1, the sample follows a closed circle in both directions, so the field meets itself at every edge. Lay the picture down beside itself and you cannot find the join.

Use it whenever a picture will be repeated, and a projected picture always is. For example, triplanarTextured carries its picture across the whole surface, so a map written the ordinary way draws a line wherever the picture wraps. A normal map shows this most clearly. The two sides of the join light differently, so the line reads as a crease in the surface.

// filling a 512-square map, one texel at a time
let u = (Double(x) + 0.5) / 512, v = (Double(y) + 0.5) / 512
let shade = Color(white: tilingFbm(u, v, detail: 5, octaves: 5))

detail is roughly how many features fit across one tile. It is the frequency you would otherwise multiply into the coordinates, so moving a map across is a direct swap. Write tilingFbm(u, v, detail: 8) where you had fbm(u * 8, v * 8), and the grain stays the same. Under the hood, both directions use the same 4D construction as the looping forms. Those forms spend it on time, and tiling spends it on space twice.

The example is Randomness/TilingNoise. It lays one tile down nine times, from fbm and from tilingFbm side by side.

curlNoise

curlNoise(_ x: Double, _ y: Double) -> Vector2
curlNoise(_ p: Vector2) -> Vector2
curlNoise(_ x: Double, _ y: Double, _ z: Double) -> Vector3
curlNoise(_ p: Vector3) -> Vector3

A divergence-free flow vector, the curl of the Perlin field. The flow only ever swirls. It never gathers into a sink or drains from a source, so points carried by it stay evenly spread however long they travel, which is what makes it the usual basis for flow fields. Take .normalized for the direction alone, and sample on scaled-down coordinates such as x * 0.003 for broad swirls. See the FlowField example.

var p = center
for _ in 0..<100 {                         // trace a streamline through the field
    let next = p + curlNoise(p * 0.003).normalized * 4
    drawLine(p, next)
    p = next
}

The three-coordinate form is the same idea in space. Its potential is three copies of the field, each read at its own offset, and the curl of that is a flow that swirls in every direction. It is the field to bend geometry with, since a shape it pushes bends as if a fluid had passed through it rather than tearing:

let bent = p + curlNoise(p * 0.5) * 0.3                     // a vertex nudged along the flow
let lean = curlNoise(p * 0.15).normalized * 0.7             // a direction with its own strength

Neither form is normalized, and neither is bounded the way noise is. The vector is the field's slope, so it grows with the frequency you sample at: multiply the coordinates by 2 and the vectors double. Scale it to the distance you mean, or take .normalized and scale that. Both forms are worked out in closed form, from the slope of the field itself rather than by sampling the field on either side, so a curl costs about the same as three reads of noise. The Rendering/LineSpray example bends a sphere of rings with the three-coordinate form.

simplexNoise / signedSimplexNoise

simplexNoise(_ x: Double[, _ y: Double[, _ z: Double]]) -> Double
signedSimplexNoise(/* the same forms */) -> Double

Another kind of the same idea as noise. It is smooth, seeded, contrast-calibrated the same way, and in 0...1, with the signed form in -1...1. The classic field interpolates over a square grid, while simplex sums smooth kernels over triangles, so its grain is more even in every direction. The classic field carries a faint bias along its grid axes that shows in large flat washes, and simplex does not. Simplex also reads slightly crisper at the same scale. Try both. They take the same inputs, so swapping is a one-word edit.

let n = simplexNoise(x * 0.008, y * 0.008)             // rounder grain than noise()
let sway = signedSimplexNoise(time * 0.4, 7) * 30

Both fields take the same seed from noiseSeed, but they stay separate fields, so noise and simplexNoise at the same point do not correlate.

Six gray field panels from one seed: classic noise, simplex noise, warped fbm, cellular worley, ridged fbm, and turbulence

The Randomness/NoiseKinds example reads four of these off one set of coordinates under one noiseSeed. It shows simplexNoise, signedSimplexNoise split at zero, worley with its feature and jitter parameters, and turbulence.

worley

worley(_ x: Double, _ y: Double[, _ z: Double],
       feature: WorleyFeature = .nearest, jitter: Double = 1) -> Double

Cellular noise. Space is divided into unit cells, each holding one seeded feature point, and the value is the distance to the nearest point. Shading by that value draws organic cells such as stone, foam, cracked earth, and water caustics. The value is near zero at each cell's core and rises toward its walls. It runs roughly 0...1, occasionally a little above that where cells run sparse. Sample on scaled-down coordinates just like noise.

feature picks the reading:

  • .nearest (the default): the classic cell field.
  • .second: the distance to the second-nearest point, which gives a blunter plateau.
  • .border: the gap between those two distances, which is zero exactly on the walls between cells. Threshold it small for crack and vein line work.
Two panels of cellular noise: distances shaded so each hidden point sits in a dark core, and the border reading drawing dark walls between the cells

jitter runs the cells from a regular grid at 0 to fully organic at 1. Drift z over time and the 3D form bubbles in place:

let cell = worley(x * 0.02, y * 0.02)                       // stone-wall shading
let crack = worley(x * 0.02, y * 0.02, feature: .border)    // 0 on the cell walls
if crack < 0.05 { drawPoint(x, y) }                         // trace the cracks
let foam = worley(x * 0.02, y * 0.02, time * 0.3)           // cells that reform

For cellular geometry, meaning polygonal cells you can stroke, offset, or clip, see Voronoi. worley is the per-sample field reading of the same idea.

ridgedFbm / turbulence

ridgedFbm(_ x[, _ y[, _ z]], octaves: Int = 4, gain: Double = 0.5, lacunarity: Double = 2) -> Double
turbulence(/* the same forms */) -> Double
ridgedFbm(_ x, _ y[, _ z], loop: Double, radius: Double = 1, ...) -> Double
turbulence(_ x, _ y[, _ z], loop: Double, radius: Double = 1, ...) -> Double

Two classic reshapings of fbm. They take the same parameters, and both return 0...1:

  • ridgedFbm folds each octave into sharp creases, one minus the absolute value, squared. An octave contributes only where the one below it was strong, so detail gathers on the crest lines instead of filling the valleys. Bright values are the ridges, and this is the standard basis for mountainous terrain. See the RidgeLines example.
  • turbulence layers the folded field without that feedback. You get billows with creased seams, the classic basis for clouds, smoke, and marble.
let peak = ridgedFbm(x * 0.004, 2.5)                  // a mountain skyline
let cloud = turbulence(x * 0.005, y * 0.005)          // billowy, creased
let range = ridgedFbm(x * 0.004, 2.5, loop: loopProgress(over: 10))

Both have the 2D and 3D looping forms (see loop:), so a drifting landscape, or a cloud's volume, can return to its start every lap.

warpedFbm

warpedFbm(_ x: Double, _ y: Double, warp: Double = 1,
          octaves: Int = 4, gain: Double = 0.5, lacunarity: Double = 2) -> Double
warpedFbm(_ x, _ y, warp: Double = 1, loop: Double, radius: Double = 1, ...) -> Double

Domain warping. The fbm field displaces its own sampling coordinates, then displaces them again. That smears the layers into flowing marble and cloud forms that no amount of plain layering produces. warp scales the displacement: 0 is exactly fbm(x, y), 1 is the classic strength, and past 1 you get churn. The result is in 0...1, and it has the same looping form as fbm.

let marble = warpedFbm(x * 0.004, y * 0.004)          // the flowing look, one call
let calm = warpedFbm(x * 0.004, y * 0.004, warp: 0.4)

The same look has two GPU spellings. generate(.noise(scale: 3, warp: 1)) fills a layer with it (see Effects), and the shader library's warpedFbm(p, warp) runs it per pixel. The DomainWarp example opens the recipe up so you can color its intermediate displacements.

gaborNoise

gaborNoise(_ x: Double, _ y: Double, wavelength: Double = 32, bandwidth: Double = 0.5,
           angle: Double = 0, spread: Double = .pi, impulses: Int = 32,
           phase: Double = 0, seed: Int = 0) -> Double
GaborNoise(wavelength:bandwidth:angle:spread:impulses:phase:seed:)   // the value type
    .value(_ x, _ y) / .value(at: Vector2) -> Double                 // 0...1
    .signedValue(_ x, _ y) -> Double                                  // -1...1

Gabor noise. Every field above is built from the same kind of bump. Its grain runs every way at once, and its scale is a blur of scales. Gabor noise is built from small Gaussian blobs that each carry a cosine wave, scattered at random and summed. That gives you the spectrum to design. One principal wavelength, a band of a chosen width around it, and one direction or a range of them.

Three panels of Gabor noise at one wavelength: an even ripple field in every direction, diagonal stripes with the spread at zero, and long interfering waves at a narrow bandwidth
  • wavelength is the wave period in pixels. The coordinates are pixels too, not the scaled-down inputs the other fields take.
  • bandwidth is the width of the band around that wavelength, as a fraction of it. 0.2 is nearly a pure wave and runs long interference patterns; 1 is blobby and close to ordinary noise. The kernel radius is wavelength / bandwidth.
  • angle is the wave direction in radians. 0 oscillates along x, so the stripes stand vertical.
  • spread is how far each kernel's own direction may wander from angle. 0 is one direction, .pi (the default) every direction, the isotropic field.
  • impulses is how many kernels overlap at any point, the quality dial. 32 is smooth; 8 shows the kernels.
  • phase slides every wave along its own direction and is periodic over 2π.
  • seed picks the field. Unlike the others, this one is not pinned by noiseSeed: the seed is a parameter so the CPU and GPU forms stay one field.

The result fills 0...1, three standard deviations mapped onto the range, so a rare peak clips. It is the same field generate(.gaborNoise(...)) paints (see Effects). With the same parameters, gaborNoise(x, y) is the value at pixel (x, y). So a sketch can place marks where the GPU painted light. Sampling many points a frame? Build one GaborNoise and ask it, so the kernel is set up once. The kernels are filtered for a one-pixel footprint, so a wavelength driven toward two pixels fades to gray instead of aliasing.

let grain = gaborNoise(x, y, wavelength: 20, angle: .pi / 2, spread: 0.1)   // wood grain
let field = GaborNoise(wavelength: 24, bandwidth: 0.3, seed: 7)            // once, then
let n = field.value(x, y)                                                   // per point

The shader library's gaborNoise(p, wavelength, bandwidth, ...) runs the same field inside a shader of your own. The GaborNoise example dots the crests the generator painted.

Choosing a scale

The multiplier on noise's input is a zoom control. It decides how far apart your samples land on the field, and it is the number you will tune most.

Three framed panels sampling the same noise field with multipliers 0.004, 0.015, and 0.06, the curve going from one gentle valley to rolling hills to busy wiggles
  • When feeding pixel coordinates, multiply by something small, usually 0.001...0.02. A 1080-pixel canvas times 0.006 spans about six of the field's features. That is big enough to read as shapes and small enough to stay interesting.
  • If the output looks like static, the multiplier is too big, because successive samples land on unrelated parts of the field. Shrink it until the result glides.
  • When feeding time, the same rule holds. noise(time * 0.2) drifts, and noise(time * 3) twitches.
  • For several independent glides from one field, do not use several noise functions. Sample far-apart rows instead. Both drifts below move at the same speed through unrelated terrain:
let dx = signedNoise(time * 0.3, 10.5) * 300   // row 10.5
let dy = signedNoise(time * 0.3, 99.5) * 300   // row 99.5, unrelated to row 10.5

Keep a row off the whole numbers. On a whole-number row the two-dimensional field reads its lattice's own line, where some gradients have no component along it, so the curve rests flat between one pair of lattice points in seven; half a step off, every gradient counts.

Seeding

noiseSeed

noiseSeed(_ seed: Int)

Seed every noise field at once: noise, signedNoise, curlNoise, the fbm family, plus simplexNoise and worley. To reseed random as well, see seed.

noiseSeed(7)

seed

seed(_ seed: Int)

Seed both noise and random from one value. That locks the whole sketch's randomness, so it reproduces exactly. Use noiseSeed or randomSeed to reseed only one of them.

seed(7)   // noise and random both reproducible

The fields as a value: noiseFields

noiseFields -> NoiseFields                 // the sketch's fields, seeded as the sketch is
NoiseFields(seed: Int)                     // the same fields, made anywhere

Every function on this page is a method of NoiseFields too, and noiseFields is the sketch's own copy of them. It reads exactly what the sketch reads. noise(x, y) on the sketch and noiseFields.noise(x, y) are the same number, and noiseSeed reaches both. The difference is what you can do with it. A sketch lives on the main thread, so its noise can only be called there. NoiseFields is a plain value that any thread can read, which is what a heavy job wants:

let fields = noiseFields                              // take a copy on the main thread
var bent = [Vector3](repeating: .zero, count: points.count)
bent.withUnsafeMutableBufferPointer { out in
    DispatchQueue.concurrentPerform(iterations: points.count) { i in
        let p = points[i]
        out[i] = p + fields.curlNoise(p * 0.5) * 0.3   // every core reads the same field
    }
}

The copy is a value, so it keeps the seed it was taken with. Reseed the sketch and a copy taken earlier goes on reading the old field, while noiseFields reads the new one. Take the copy inside draw() when the seed can change between frames.

A Formula reads a field through noiseField() instead, a plain function of three coordinates, since a formula wants one number back and nothing more. Both come from the same place.

The Rendering/LineSpray example's Drift parameter bends its thirty-eight thousand lines every frame this way, one core per slice of the scene.

↑↓ move ↩ open