Noise

Seeded Perlin, value and cellular noise in 2D and 3D, with fractal layers and domain warping.

Commands

Import blitz.noise As N gives your program these, with N. in front. Each name leads to its part of the reference.

Functions

Types

Enums

Guide

Noise makes smooth randomness: numbers that change gently from place to place, instead of jumping about the way Rnd does. Use it for hills and valleys, coastlines, clouds, or a torch that flickers. Give it a seed and a position, and it gives back a number. The same seed and position always give the same number, so you can make a whole world again from its seed instead of saving it.

A map of islands, coloured from deep sea through sand, grass and forest to rock and snow

Try it

Open world.bb and run it. Press Space to make a new world from the next seed.

; A map of a made-up world, coloured by height: sea, sand, grass, forest, rock
; and snow. Space makes a new world from the next seed. Escape quits.
Dialect "modern"
Import blitz.noise As N

Const SPOT:Int = 5 ; Each spot on the map is 5 pixels square.
Graphics 640, 480, 0, 2
SetBuffer BackBuffer()
ClsColor 18, 52, 79

; Four layers of noise: big shapes first, then finer and finer detail.
Local options:N.NoiseOptions = New N.NoiseOptions
options.octaves = 4
Local seed:Long = 1L
Local land:N.Noise = Null
Local picture:Int = CreateImage(120 * SPOT, 84 * SPOT)
Local changed:Int = True
Local x:Int
Local y:Int

While Not KeyDown(1)
    If KeyHit(57) ; Space
        seed = seed + 1L
        changed = True
    EndIf

    If changed
        ; The same seed always makes the same world.
        Delete land
        land = N.CreateNoise(seed, options)
        ; Colour the map once, into a picture, rather than every frame.
        SetBuffer ImageBuffer(picture)
        For y = 0 To 83
            For x = 0 To 119
                ; Dividing by 30 sets the size of the land. Smaller numbers
                ; make smaller islands.
                Local height:Double = N.Sample2(land, Double(x) / 30D, Double(y) / 30D)
                ; Heights stay close to 0, so the bands are narrow.
                If height < -0.12D
                    Color 25, 70, 140   ; Deep sea
                ElseIf height < 0D
                    Color 45, 110, 180  ; Shallow sea
                ElseIf height < 0.03D
                    Color 220, 205, 145 ; Sand
                ElseIf height < 0.12D
                    Color 95, 160, 70   ; Grass
                ElseIf height < 0.19D
                    Color 45, 115, 55   ; Forest
                ElseIf height < 0.25D
                    Color 130, 120, 110 ; Rock
                Else
                    Color 240, 240, 245 ; Snow
                EndIf
                Rect x * SPOT, y * SPOT, SPOT, SPOT, True
            Next
        Next
        SetBuffer BackBuffer()
        changed = False
    EndIf

    Cls
    DrawImage picture, 20, 20
    Color 255, 255, 255
    Text 20, 448, "Seed " + seed + "    Space: a new world"
    Flip
Wend
FreeImage picture
Delete land
Delete options
End

Each spot on the map asks the noise for a height at its position, and the height picks its colour. Spots next to each other get similar heights, so the land comes out in smooth shapes rather than speckles. The map is coloured once for each world, not every frame, because sampling ten thousand spots takes a moment.

How do I...

Make the land bigger or smaller?

Divide the positions by a bigger number for bigger shapes, or a smaller one for smaller shapes. The noise has about one bump per unit, so dividing by 30 spreads each bump over about 30 spots.

Local height:Double = N.Sample2(land, Double(x) / 80D, Double(y) / 80D) ; Continents.

Make it smoother or rougher?

Each layer, or octave, adds finer detail on top of the ones before. persistence says how much each layer counts compared with the one before it, from 0 to 1, and lacunarity how much finer it is.

Local options:N.NoiseOptions = New N.NoiseOptions
options.octaves = 6        ; More layers, more detail.
options.persistence = 0.6D ; Rougher. Lower is smoother.
Local rough:N.Noise = N.CreateNoise(42L, options)
Delete options ; The noise keeps its own copy.

Give a 3D landscape hills?

Set each point of a terrain from the noise. Terrain heights go from 0 to 1, so add a half.

Local ground:Int = CreateTerrain(128) ; A 128 by 128 grid.
ScaleEntity ground, 4, 60, 4          ; Wider, and much taller.
Local x:Int
Local z:Int
For z = 0 To 127
    For x = 0 To 127
        Local height:Double = N.Sample2(land, Double(x) / 30D, Double(z) / 30D)
        ModifyTerrain ground, x, z, Float(height + 0.5D)
    Next
Next

Make a torch flicker?

Sample along a line as time goes by. The light's brightness then wobbles smoothly, instead of jumping.

Local seconds:Double = Double(MilliSecs() - startTime) / 1000D
Local flicker:Double = N.Sample2(land, seconds * 3D, 0.5D) ; Between -0.5 and 0.5.
LightColor torch, 200 + 100 * flicker, 150 + 80 * flicker, 80

Give each torch a different second number, such as 1.5 and 2.5, so they don't all flicker together. For clouds that drift and change, use Sample3, with the time as the third number.

Get a different look?

Choose another kind of noise. Value noise is blotchier, and uses more of the range from -1 to 1. Cellular noise is the distance to the nearest of a scatter of points, from 0 to 1, which looks like cells, stones or scales.

options.kind = N.NoiseKind.Cellular

Make it swirl?

Warp each position with the noise before you sample it. Coastlines and clouds come out twisted.

Local warped:N.Point2 = New N.Point2 ; Make this once, and use it again.
N.Warp2 land, Double(x) / 30D, Double(y) / 30D, 2D, warped ; Move it by up to 2.
Local height:Double = N.Sample2(land, warped.x, warped.y)

Blip's tip

Perlin noise, the usual kind, doesn't use the whole range from -1 to 1. Sample2 never goes beyond -0.5 or 0.5, and the more layers there are, the closer to 0 it stays. Choose your heights for sea, sand and snow by trying them, as the program above does.

Good to know

More examples

Three more complete programs come with the module:

The reference below has every function, option and rule.

Reference

Needs
blitz.random
Examples
domain_warp.bb, greyscale.bb, terrain.bb

0.1.0 local preview; unpublished. Perlin gradient, value and nearest-feature cellular noise in 2D/3D, normalised fractal layers and 2D domain warp. Pure numeric sampling changes no world or global random state. Requires blitz.random 0.1.0 and Extended object-lifecycle-v1 (runtime 1114 / module ABI 5 or later).

Blitz3D+ comes with this module and blitz.random, so any Extended program can Import blitz.noise. Open an example and run it.

Dialect "modern"
Import blitz.noise
Local land:Noise.Noise = Noise.CreateNoise(42L, Null)
Print Noise.Sample2(land, 0.25D, -0.75D)
Print Noise.Sample3(land, 0.25D, -0.75D, 1.125D)
Delete land
End

The second argument is required but nullable. Actual compiler testing found that object parameters cannot default to Null; the plan's default was design notation. Pass explicit Null for defaults, or a caller-owned options object.

API and ownership

DeclarationContract and example
NoiseKindInt enum: Perlin=0, Value=1, Cellular=2. Other values are rejected.
NoiseOptionsPublic fields kind:Int=Perlin, octaves:Int=1, persistence:Double=0.5D, lacunarity:Double=2D. New Noise.NoiseOptions; caller deletes it.
NoiseOpaque field owning one 256-entry Int List. Private constructor/state/helpers. Delete land releases the table.
CreateNoise(seed:Long, options:NoiseOptions):NoiseCaller owns result. Copies options; never retains the options object or any random stream. Validates before allocation. Noise.CreateNoise(42L, Null).
Sample2(noise, x:Double, y:Double):DoubleScalar sample in noise space, no allocation. Noise.Sample2(land, Double(x)/64D, Double(y)/64D).
Sample3(noise, x:Double, y:Double, z:Double):DoubleTrue 3D field with the same contract; not promised equal to Sample2 at z=0.
Point2Public Double x and y, initially zero; caller-owned reusable warp output. New Noise.Point2.
Warp2(noise, x:Double, y:Double, amount:Double, output:Point2)Borrows output and writes both coordinates, no allocation. Noise.Warp2(land, x, y, 3D, point) then Noise.Sample2(land, point.x, point.y).
Local options:Noise.NoiseOptions = New Noise.NoiseOptions
options.kind = Noise.NoiseKind.Value
options.octaves = 4
options.persistence = 0.5D
options.lacunarity = 2D
Local clouds:Noise.Noise = Noise.CreateNoise(2026L, options)
Delete options ; Safe immediately: construction copied the values.
Local point:Noise.Point2 = New Noise.Point2
Noise.Warp2 clouds, 0.25D, -0.75D, 2D, point
Print Noise.Sample2(clouds, point.x, point.y)
Delete point
Delete clouds

Octaves must be 1..16, persistence finite in 0..1, lacunarity finite and >=1. Zero persistence is a valid single effective octave, even with 16 configured layers. All sample coordinates must be finite and every effective octave coordinate must lie in [-1048576,1048576], inclusive. The check happens before flooring or integer conversion. Scale world coordinates yourself. For example, 4 layers with lacunarity 2 require base coordinates within +/-131072. Invalid options, Null/deleted objects, non-finite inputs or coordinates beyond the bound raise contextual RuntimeError. There are no recoverable factory/I/O failures, so no redundant LastError() API. options.octaves = 0 is a programming error.

Warp uses two independently salted versions of the configured field. Signed fields displace directly; cellular is mapped from [0,1] to [-1,1] first. Each axis moves by at most Abs(amount), with finite negative amounts reversing displacement. Zero amount preserves the original coordinate bits, including negative zero, without evaluating higher octaves. The output is updated only after both coordinates pass validation. A later Sample2 still checks its own octave limits.

Delete is explicit; scope exit/assigning Null does not run the destructor. Tables and the construction-only stream are owned by noise, and never exposed. Options and Point2 always remain caller-owned. Independent fields survive each other's deletion. Repeated typed Null/deleted aliases are safe to Delete.

Algorithm identity: blitz-noise-v1

Save this identifier together with seed, all options and coordinate scaling when saving a procedural world. Seed tables, gradient rules and feature placement are part of world compatibility; changing them requires an incompatible version or preserving this implementation. There is no Worker/thread-safety promise.

  1. Start with indices 0..255. Shuffle with blitz.random PCG32, the full seed bit pattern and sequence 1. Release that stream immediately. There is exactly one fixed-size table configuration, no global permutation and no per-sample allocation. Construction is O(256); samples are O(effective octaves).
  2. Hash integer coordinates with p[(p[(p[x&255]+y)&255]+z)&255], then look up p[(hash+salt)&255]. Base samples use salt 0, warp axes use 73 and 151. For 2D, z=0. Lattice coordinates use FloorD, including negative positions. The single-octave field repeats every 256 lattice units on each axis. Layering with non-integer lacunarity does not preserve that common period. There is no arbitrary tile-size API.
  3. Perlin uses quintic fade 6t^5-15t^4+10t^3. 2D uses four diagonal gradients; 3D uses the twelve cube-edge gradients and the four padded directions from Perlin's paper. Dot products are halved, a conservative [-1,1] normalisation; do not expect them to fill the whole interval. Corners are bilinearly or trilinearly interpolated. Value noise instead maps each hash to hash/127.5-1, with the same fade. Both are continuous with smooth cell faces.
  4. Cellular places one point per cell. For cell hash h, feature axis a uses (p[(h+85*a)&255]+0.5)/256. Search expanding cell shells, skip boxes whose lower distance cannot win, stop when all unvisited cells are farther than the best feature. An own-cell feature is within sqrt(dimensions), proving termination by radius 2. Normalise Euclidean distance by sqrt(dimensions), returning [0,1]. It is continuous but may have derivative discontinuities; it does not have the same distribution as gradient noise.
  5. Layers multiply coordinates by lacunarity and amplitude by persistence, divide the weighted sum by total amplitude, and stop on zero amplitude. Final guards contain numerical roundoff. No fresh random draws occur.

Working examples

Use a release build for generation workloads: debug source instrumentation can dominate these tight numeric loops.

Tileable noise, 3D warp and Worker adapters are future features, not incomplete switches. See provenance and licence.