Perlin noise is a smooth pseudo-random function originally developed by Ken Perlin in the early 1980s for the film Tron, as a way to generate more natural-looking procedural textures.
The technique became enormously influential in computer graphics and has since been used for everything from surface textures and clouds to terrain generation and spatial distribution. It became so canonical that many graphics and creative-coding environments provide Perlin noise as a built-in function.
Perlin noise, however, is only one particular approach to constructing procedural noise. Since its introduction, many other methods have emerged, including Simplex noise, Worley noise, value noise, and wavelet noise.
Utomata does not provide a dedicated Perlin noise primitive. Instead, it exposes the more general mechanism from which it can be constructed. This allows us to recreate Perlin noise directly, but also to derive different noise functions from the same basic ingredients.
Below is a basic implementation of Perlin noise in Utomata:
#perlin {
dim = (256, 256);
run = LATT.RECT.QUINTIC[mlt(C, 8)] {
dot(nrm(R), D)
};
}
~perlin {
col = add(mlt(#perlin, 0.5), 0.5);
}
In this guide, we will build a Perlin noise function step by step, use it to generate a simple terrain, and then experiment with variations on the same construction.
Setup
We begin with a field and a formation to visualize it:
#map {
dim = (256, 256);
}
~vis {
col = #map;
}
Our noise function will be evaluated once for every cell in the field #map.
To determine where each cell should sample the function, we use its coordinate C. Field coordinates range from -1 to 1, so we scale them to cover a larger region:
&coord = mlt(C, 8);
The value 8 determines the spatial scale at which we sample the noise. We will return to this later. For now, &coord simply gives us a useful sample position for every cell in the field.
The lattice
Perlin noise is constructed from values associated with points on a regular lattice.
In Utomata, this kind of operation is expressed with LATT:
LATT.RECT.QUINTIC[&coord] {
...
}
The coordinate supplied in square brackets determines where the lattice is sampled.
For a two-dimensional coordinate, RECT places the sample inside a rectangular lattice cell surrounded by four lattice points:
1 ──────── 2
│ │
│ × │
│ │
3 ──────── 4
× represents a sample position, which may be anywhere between exactly four points in a rectangular lattice.
The expression inside the LATT block is evaluated once relative to each surrounding lattice point. The resulting values are then interpolated back to the sample position.
In this case,QUINTIC determines the interpolation method used between the four expressions.
This means that LATT does not itself describe Perlin noise. It provides a more general structure: evaluate something at the surrounding points of a lattice, then interpolate the results.
Values at each lattice point
While the body of a LATT expression is being evaluated, it has access to information relative to the current lattice point.
Two values are particularly useful here:
point ● ────────
│ \
│ \
│ × sample
-
Dis the displacement from the current lattice point to the sample position. -
Ris a stable pseudo-random signed vector associated with that lattice point.
The important word here is stable. A given lattice point always produces the same random vector, so nearby sample positions agree about the values assigned to their shared lattice points.
Conceptually, a rectangular lattice therefore contains a repeatable field of random vectors.
As we sample different positions within the lattice, the surrounding vectors remain fixed while only the displacement from them changes. This gives us the ingredients needed to construct Perlin noise.
Constructing the gradients
Perlin noise associates each lattice point with a gradient direction.
We already have a random vector at every lattice point through R. Normalizing it converts that value into a direction vector:
nrm(R)
The gradient itself does not yet tell us what value to assign to the sample position. For that, we compare the direction of the gradient with the displacement D.
This is done using a dot product:
dot(nrm(R), D)
The result is positive when the displacement points broadly in the same direction as the gradient, negative when it points in the opposite direction, and approaches zero when the two are perpendicular.
Inside LATT, this calculation is performed independently for each of the four surrounding lattice points, and the results are interpolated across the space between them.
This gives us the essential construction of Perlin noise. We can now use it directly as the update rule for our field:
&coord = mlt(C, 8);
#map {
dim = (256, 256);
run = LATT.RECT.QUINTIC[&coord] {
dot(nrm(R), D)
};
}
Visualizing the result
Our field produces signed values, while surface color is easier to inspect in the visible range between 0 and 1.
We can remap the result when it is displayed:
~perlin {
col = add(mlt(#perlin, 0.5), 0.5);
}
This does not change the values stored in #perlin. It only transforms them for visualization.
Alternatively, we can visualize the result in 3D as a displaced surface. Instead of applying #map to the surface color, we use the disp property to control the position of each vertex along the z axis.
We can rotate the formation and use a custom viewport for a better lit scene:
~terrain {
dim = (256, 256);
disp = (C.x, C.y, mlt(#map,0.1));
rot = (HALF_PI, 0,0);
}
{
= (0.6);
= (0.2);
}
Sample coordinates
We can now return to the coordinate scale introduced at the beginning. Changing the expression mlt(C, 8) does not alter the noise function itself, nor the resolution of the field. It changes the sample resolution, or the way in which the lattice is placed relative to the field's coordinate domain.
A smaller value samples a smaller region:
&coord = mlt(C, 2);
A larger value samples more of the lattice over the same field:
&coord = mlt(C, 24);
This distinction is useful to keep in mind: the field resolution remains 256 × 256, while &coord determines the scale of the space being sampled.
The sample coordinate itself is not required to be static:
&coord =
add(
mlt(C, 4),
(0, mlt(step,0.01), 0)
);
The coordinate expression above uses the global step counter to move the sample coordinate over time, causing continuous motion along the y axis. We can apply arbitrarily compound operations on the sample coordinate to create moving, morphing and evolving terrains. For example, the sample coordinate itself can be derived by values in a different field, which itself may be using the LATT operator.
Where to go next
We have built a complete Perlin noise function from Utomata's lattice primitives and used it as a continuously sampled spatial field.
The important distinction is that the noise itself and the coordinates used to sample it are separate parts of the construction. Changing the sample coordinates lets us move through the noise field, alter its scale, or transform the space in which it is evaluated without changing the underlying Perlin function.
This is also where the construction begins to open up. Multiple noise samples can be combined into richer structures, coordinates can be distorted by other fields, and the expression evaluated inside LATT can be changed entirely to produce different forms of procedural noise.
Those techniques move beyond the basic Perlin construction explored here, but they all build on the same ingredients: a sample coordinate, a lattice, and an expression evaluated relative to its surrounding points.