Foundations, part 3

Growing noise from one random number

Smoke, ripples and sparkles without a single texture. Let's grow them all from one random number per grid cell.

  • Part 38 min dose
  • Beginner

The swirling smoke on my witch hat needs no texture. Neither do the ripples and glitter in my glass, or the fine fibres a procedural iris can have. They all grow from one tiny tool, a function that turns a position into a random looking number. Let’s build noise from that tool together, in five small steps. By the end, every piece of the orb below will make sense.

A glass orb full of noise
Drag to turn the view, or use the arrow keys

Turn on JavaScript to run this demo.

How far a second noise pushes the smoke noise around.1.50
Share of the cells that hold a star.0.45
lib-noise.hlsl and hero.hlsl draw the demo above
Fig. 1

Here’s where this is going. The smoke is layered noise, the streaks are noise pushed by noise, and the stars sit in cells. Drag the orb to turn it. The noise inside is 3D, so the streaks and stars turn with the glass. Slide Warp to 0 and watch the streaks relax into smooth loops.

One random number per cell

The first puzzle is that a shader has no dice, and every pixel is worked out on its own. Instead, it uses a hash, a few multiplications and additions that keep only the digits after the decimal point. That scrambles the input into a random looking number. The same input always gives the same output, so the pattern holds still.

Feed a hash every pixel position and you get TV static. To calm it down, let’s cut the picture into a grid of square cells and hash the number of each cell instead:

h=hash⁡(⌊Np⌋)h = \operatorname{hash}(\lfloor N p \rfloor)

pp is the position, measured so the short side of the picture is 1. NN is how many cells fit across it (Cells N). The ⌊ ⌋\lfloor\ \rfloor brackets round down, so every pixel in one cell gets the same input.

One random shade per cell
Drag the marked part, or press Tab to reach it and use the arrow keys

Turn on JavaScript to run this demo.

What the hash is given as its input.
How many grid cells fit across the short side.8
hash.hlsl draws the demo above
Fig. 2

Drag the point around. Wherever it goes, the whole cell under it shares one number. Then switch Hash of to Every pixel and the static comes back.

Need more than one random number in a cell? Add a different fixed number to the cell’s number before each hash, say one for a star’s size and another for its twinkle. Each one is a fresh roll.

Rolling hills between random heights

Static is too harsh for smoke. Value noise smooths things out by putting a random height on every grid corner and blending between them. Along one line:

u=f2(3−2f)n=lerp⁡(h0,h1,u)\begin{aligned} u &= f^2 (3 - 2f) \\ n &= \operatorname{lerp}(h_0, h_1, u) \end{aligned}

h0h_0 and h1h_1 are the hashed heights of the corners to the left and right. ff is how far the pixel is between them, from 0 to 1. Lerp (linear interpolation; lerp in Unity and in the demos) slides from h0h_0 to h1h_1 as uu goes from 0 to 1. And uu is the fade, the smoothstep curve you may know from the smooth edges lesson. In 2D, the same blend mixes four corners.

Smoothstep arrives flat at each corner, so the noise has no kinks. Now a subtle bit, so take it slow. Light also shows how fast the slope bends, and smoothstep still bends sharply at the corners. The quintic fade, 6f5−15f4+10f36f^5 - 15f^4 + 10f^3 from Ken Perlin’s improved noise, bends smoothly there too. That’s why my glass uses it for its ripples.

Three blends between corners

Turn on JavaScript to run this demo.

Code for the plot above
  • f1(x):lerp(h(floor(x)), h(floor(x) + 1.0), frac(x))
  • f2(x):lerp(h(floor(x)), h(floor(x) + 1.0), smoothstep(0.0, 1.0, frac(x)))
  • f3(x):lerp(h(floor(x)), h(floor(x) + 1.0), quintic(frac(x)))
Fig. 3

All three curves meet the corner heights at the whole numbers, marked with thin lines. The straight blend turns sharply there, while the other two glide in flat.

Value noise over its grid

Turn on JavaScript to run this demo.

The curve that blends between the corners.
Shade the noise as if it were a bumpy surface lit from the top left.
Show the grid cells and the random height on each corner.
value.hlsl draws the demo above
Fig. 4

Each dot’s shade is its corner’s random height. Now turn off Grid, turn on Light it and try each Fade. Straight shows creases, Smoothstep faint lines, and Quintic none. Light is picky like that.

Stacking up detail with fbm

Real smoke has big shapes with smaller wisps on top, so let’s stack noise the same way. fbm (fractional Brownian motion, a big name for layered noise) adds several layers of noise, called octaves:

s=n(p)+g n(Lp)+g2n(L2p)+…fbm⁡=s / (1+g+g2+… )\begin{aligned} s &= n(p) + g\,n(Lp) + g^2 n(L^2 p) + \dots \\ \operatorname{fbm} &= s \,/\, (1 + g + g^2 + \dots) \end{aligned}

nn is the value noise from the last section. Each term is one octave, and Octaves sets how many. LL is the lacunarity. Multiplying the position by LL fits LL times as many blobs in the same space, so each layer is LL times finer. gg is the gain, and it makes each layer gg times as strong as the one before. Dividing by the sum of the strengths keeps the result between 0 and 1.

There’s one last detail. Each layer is also shifted and turned by about 37 degrees, as Inigo Quilez does, so the layers’ grids never line up.

Stacked layers of noise

Turn on JavaScript to run this demo.

How many layers of noise are added.4
How much finer each layer is than the one before.2.03
How much weaker each layer is than the one before.0.50
fbm.hlsl draws the demo above
Fig. 5

Set Octaves to 1 and the square grid peeks through. Add layers and it hides under the detail. Raise Lacunarity L and each layer jumps further from big to fine. A higher Gain g makes the noise rougher.

Which dot is nearest?

Time for a different shape. Let’s place one random point in each grid cell, using the hash (the number scrambler from the start) for its position. Then every pixel looks for the nearest point. These are Voronoi cells, and they hand you three useful numbers:

  • F1, the distance to the nearest point. It makes round spots, like sparkles.
  • F2, the distance to the second nearest point. F2 minus F1 is zero exactly on the border between two cells, so it draws facet lines.
  • Cell number, which cell the nearest point belongs to. Hash it and each cell gets its own color.

A point never leaves its cell, so the nearest one is almost always in the pixel’s own cell or a neighbour. That’s 9 cells to check in a flat picture, 27 in 3D. The demo also hunts for the second nearest dot, which can sit further out, so it checks 5 by 5 cells.

Voronoi cells around random dots
Drag the marked part, or press Tab to reach it and use the arrow keys

Turn on JavaScript to run this demo.

Which number from the cell search is drawn.
How far each point may wander from the middle of its grid cell.1.00
Show the grid the points come from.
lib-cells.hlsl and cells.hlsl draw the demo above
Fig. 6

Drag the pink point. The solid line is F1, the dashed one F2. Switch Show to Border F2 minus F1 for the facet lines, or to Cell number for one color per cell.

My glass uses cells in several places. Each facet cell gets its own random tilt, so it bends light its own way. Another grid of cells holds one glitter sparkle each, and the Crystals shape grows glass in whole cells.

Noise, pushed around by noise

The last trick is a fun one. It makes noise look like liquid! Before reading fbm (the layered noise from before) at a point, let’s push the point by two more fbm values:

m=(fbm⁡(q), fbm⁡(q+5.2))−0.5w=fbm⁡(q+Wm)\begin{aligned} \mathbf m &= \big(\operatorname{fbm}(q),\ \operatorname{fbm}(q + 5.2)\big) - 0.5 \\ w &= \operatorname{fbm}(q + W \mathbf m) \end{aligned}

This one bends brains a little, so let’s go slowly. qq is the position. m\mathbf m is the push, two numbers that say how far to move sideways and how far up or down. The second is read 5.2 further along, so the two aren’t the same. Since fbm lies between 0 and 1, subtracting 0.5 lets the push go either way. WW is the strength, Warp W.

The positions the noise is read at are called its domain, so this trick is domain warping. The push changes slowly from place to place, so blobs bend and stretch into folds and swirls without breaking apart. And that’s the whole trick.

Noise pushed into marble

Turn on JavaScript to run this demo.

How far the second noise pushes the point before the first noise is read.1.50
Show which way, and how far, each point is pushed.
warp.hlsl draws the demo above
Fig. 7

Slide Warp W from 0 to 4 and watch soft clouds turn into marble. Turn on Arrows to see which way each point gets pushed.

In practice

Shaders use noise for smoke, dissolves, ripples and sparkles. My glass uses a 3D fbm built the same way, with layers that are shifted but not turned. With Pattern Space set to Object, and always for its inner swirl, it reads noise at a point’s place inside the object, so the pattern runs through the mesh. In the code, 0.5 is the gain gg (how much weaker each layer gets), 2.03 the lacunarity LL (how much finer), and the added numbers shift each layer:

DocMe_GlassNoise.cgincRuns in Unity only
float gnFbm(float3 p)
{
    float sum = 0.0;
    float amplitude = 0.5;
    float norm = 0.0;
    [unroll]
    for (int octave = 0; octave < 4; octave++)
    {
        sum += amplitude * gnValueNoise(p);
        norm += amplitude;
        p = p * 2.03 + float3(17.1, 9.3, 3.7);
        amplitude *= 0.5;
    }
    return sum / norm;
}

Read noise at the object position, the point’s place on the mesh itself, so the pattern moves with the avatar. Noise read at the world position stays put while the avatar walks through it. On parts that bend with bones the object position moves too, and the pattern slides. Read the noise at the UVs there instead.

The vertex shader (the part that runs once per mesh corner) passes the object position on. The pixel part reads fbm there, at the position times _Scale, so a larger _Scale gives smaller blobs. This next bit feels backwards at first. Moving where the noise is read moves the picture the opposite way, so to make the smoke drift upward the code subtracts time times _Speed:

v2f vert(appdata v)
{
    v2f o;
    UNITY_SETUP_INSTANCE_ID(v);
    UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(o);
    o.pos = UnityObjectToClipPos(v.vertex);
    o.objectPos = v.vertex.xyz;    // moves with the avatar
    return o;
}

float4 frag(v2f i) : SV_Target
{
    UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(i);
    float n = gnFbm(i.objectPos * _Scale - float3(0.0, _Time.y * _Speed, 0.0));
    return float4(n.xxx, 1.0);
}

Two more things to keep an eye on:

  • Fine detail flickers far away. Cells and fine octaves smaller than a pixel sparkle at random. Fade them out by the pixel size, with fwidth from the smooth edges lesson. My glass does this with its glitter.
  • Every octave costs. In 3D, value noise hashes the 8 corners of a cube, and a cell search checks a 3 by 3 by 3 block of 27 cells. Four octaves of fbm is 32 hashes, and domain warping reads fbm three times: two for the push, one for the picture. It adds up fast, so use few octaves on anything that can fill the screen.

Credits

Takeaways

  • A hash is a shader’s dice. It turns a cell number into a random number that never changes.
  • Value noise blends random corner heights with a fade curve, and fbm stacks finer, weaker layers on top.
  • Voronoi cells give you spots, borders and a random number per cell, and pushing noise by noise makes it swirl.
Search the lessons

Type any word from the lessons, like normalize or smoothstep. The start of a word is enough.