noise

tape
import noise from "noise";

let n: noise.Noise = noise.noise(1234);
let h: f32 = n.perlin2(x * 0.05, y * 0.05);                 // -1 to 1
let terrain: f32 = n.fbm2(x * 0.01, y * 0.01, noise.Fractal {});

noise makes smooth random patterns for generated worlds: terrain heights, clouds, marble, caves, cell patterns, and evenly spread positions for trees and rocks. Everything comes from a seed: the same seed gives the same values in the VM, natively and on every platform, so a world generated from a seed is the same world everywhere.

It is pure and freestanding: no heap, no state, t0 code included. Values are f32. A Noise is a 256-byte table you keep in a variable; copying it is fine.

Making a noise source

noise

tape
pub fn noise(seed: u64) -> Noise

A noise source for seed: a shuffled table of 0..255 that the functions below hash grid points with. Different seeds give unrelated patterns. Any seed works, 0 included.

tape
let ground: noise.Noise = noise.noise(world_seed);
let caves: noise.Noise = noise.noise(world_seed + 1);

Smooth noise

tape
pub fn Noise.perlin2(*const self, x: f32, y: f32) -> f32
pub fn Noise.perlin3(*const self, x: f32, y: f32, z: f32) -> f32
pub fn Noise.simplex2(*const self, x: f32, y: f32) -> f32
pub fn Noise.simplex3(*const self, x: f32, y: f32, z: f32) -> f32
pub fn Noise.value2(*const self, x: f32, y: f32) -> f32
pub fn Noise.value3(*const self, x: f32, y: f32, z: f32) -> f32

Each returns a value from −1 to 1 that changes smoothly with the coordinates: nearby points get nearby values, points a few units apart are unrelated. Features are about one unit across, so scale the coordinates to pick the size: n.perlin2(x * 0.05, y * 0.05) has features about 20 units wide. A third coordinate can be time, for an animated 2D pattern.

FunctionExampleWhat it looks like
perlin2, perlin3n.perlin2(0.5, 0.5)Ken Perlin’s improved noise: soft blobs; exactly 0 at whole-number coordinates
simplex2, simplex3n.simplex3(x, y, t)Simplex noise: like Perlin, fewer grid-aligned features, cheaper in 3D
value2, value3n.value2(x, y)Random heights at grid points, smoothly blended: blockier

The values stay within −1 to 1 and come close to both ends. Perlin noise, value noise and Worley noise repeat every 256 units along each axis; simplex noise does not repeat along the axes. Coordinates must be finite.

tape
// a height map from 0 to 1
for (y in 0..256) {
    for (x in 0..256) {
        let v: f32 = n.simplex2((x as f32) * 0.03, (y as f32) * 0.03);
        heights[y * 256 + x] = v * 0.5 + 0.5;
    }
}

Worley noise

tape
pub fn Noise.worley2(*const self, x: f32, y: f32, kind: Distance) -> Worley
pub fn Noise.worley3(*const self, x: f32, y: f32, z: f32, kind: Distance) -> Worley
pub struct Worley { f1: f32; f2: f32; id: u8; }
pub enum Distance : u8 { Euclidean = 0; Manhattan = 1; Chebyshev = 2; }

Cellular noise: every grid cell holds one random point, and worley2 measures from (x, y) to the nearest of them (f1) and the second nearest (f2). id names the nearest point’s cell (0 to 255): it is the same everywhere in a Voronoi cell, so it can pick a colour per cell. kind chooses how distance is measured: Euclidean gives round cells, Manhattan (|dx| + |dy|) diamond ones, Chebyshev (the larger of |dx| and |dy|) square ones.

tape
let w: noise.Worley = n.worley2(x * 0.1, y * 0.1, noise.Distance.Euclidean);
let cracks: f32 = w.f2 - w.f1;          // near 0 on the borders between cells
let tile: u8 = w.id;                    // the same for a whole cell

f1 is 0 at a cell’s point and grows away from it. With Euclidean, f1 stays below 1.42 and f2 below 1.81 in 2D (1.74 and 2.07 in 3D); Chebyshev keeps f1 below 1 and f2 below 1.5; Manhattan keeps f1 below 2 and f2 below 2.5 in 2D (3 and 3.5 in 3D).

Fractal sums

tape
pub struct Fractal { basis: Basis = Basis.Perlin; octaves: i64 = 5; lacunarity: f32 = 2.0; gain: f32 = 0.5; }
pub enum Basis : u8 { Perlin = 0; Simplex = 1; Value = 2; }
pub fn Noise.fbm2(*const self, x: f32, y: f32, f: Fractal) -> f32
pub fn Noise.fbm3(*const self, x: f32, y: f32, z: f32, f: Fractal) -> f32
pub fn Noise.ridged2(*const self, x: f32, y: f32, f: Fractal) -> f32
pub fn Noise.ridged3(*const self, x: f32, y: f32, z: f32, f: Fractal) -> f32
pub fn Noise.turbulence2(*const self, x: f32, y: f32, f: Fractal) -> f32
pub fn Noise.turbulence3(*const self, x: f32, y: f32, z: f32, f: Fractal) -> f32

Natural shapes have detail at every size. A fractal sum adds octaves layers of noise (basis), each at lacunarity times the previous frequency (finer) and gain times its strength (fainter). Fractal {} is five layers of Perlin noise, each twice as fine and half as strong.

FunctionExampleRangeUse
fbm2, fbm3n.fbm2(x, y, noise.Fractal {})−1 to 1terrain, clouds
ridged2, ridged3n.ridged2(x, y, noise.Fractal { octaves: 6; })0 to 1mountain ridges, veins
turbulence2, turbulence3n.turbulence3(x, y, t, noise.Fractal { basis: noise.Basis.Simplex; })0 to 1fire, smoke, marble

ridged turns each layer’s zero crossings into sharp crests, turbulence into creases. More octaves cost more time and add finer detail; one octave is the plain noise. octaves below 1 and a negative gain are bugs in the caller and stop the program with a trap.

tape
let shape: noise.Fractal = noise.Fractal { basis: noise.Basis.Simplex; octaves: 6; gain: 0.45; };
let height: f32 = n.fbm2(x * 0.004, y * 0.004, shape) * 120.0;   // metres

Poisson-disk sampling

tape
pub fn poisson_disk(seed: u64, width: f32, height: f32, radius: f32, tries: i64,
                    out: []math3d.Vec2, scratch: []i32) -> i64
pub fn poisson_scratch_len(width: f32, height: f32, radius: f32, max_points: i64) -> i64

Random points spread evenly over the rectangle from (0, 0) to (width, height): no two are closer than radius, and no large gaps are left (every spot is within 2 * radius of a point). Placing trees, rocks or spawn points this way avoids the clumps uniform random points make. poisson_disk writes the points to out and returns how many it wrote; it stops when the area is full or out is. tries is how hard it tries to fit another point next to each one: 30 is the usual choice.

It allocates nothing. scratch is working memory of at least poisson_scratch_len(width, height, radius, out.len) integers; the points are math3d vectors. The same seed and arguments give the same points.

tape
import math3d from "math3d";

var trees: [500]math3d.Vec2;
var scratch: [2000]i32;            // poisson_scratch_len(100.0, 60.0, 4.0, 500) is 1292
let count: i64 = noise.poisson_disk(7, 100.0, 60.0, 4.0, 30, trees[0..500], scratch[0..2000]);
for (i in 0..count) { plant(trees[i]); }

A width, height or radius that is not positive, tries below 1, or scratch too short stops the program with a trap.

Testing

tape test stdlib/noise.tape checks every function against values computed independently in Python, the ranges, smoothness and repetition over thousands of points, and the Poisson spacing and coverage, in the VM and natively.

Last modified: