Noise as an Event Generator

Buried in an old folder of code samples is a fragment shader about a hundred lines long, most of which is somebody else's noise function. The twenty lines that are mine contain one idea I've reused since, and it isn't about graphics.

The effect is a screen glitch: occasionally the image tears sideways and the colors separate, then settles. The interesting question is how a shader with no memory decides when "occasionally" is.

This continues the series taking one system at a time out of my custom engines.

The Problem With Random

A glitch that fires at random intervals needs a source of timing. The obvious approaches all have a flaw.

Calling a random function per frame gives a value with no relationship to the previous frame, so the effect flickers on and off within single frames rather than lasting. Fragment shaders also have no persistent state, so there's nowhere to keep a countdown between frames without allocating a texture and ping-ponging it, which is a lot of machinery for a visual flourish.

A timer passed in from the application works, and now the effect's timing lives in C++ while its appearance lives in GLSL, so tuning it means editing two files and rebuilding one of them.

Threshold the Noise

The approach here is one line of intent. Take smooth noise, sample it along a line through time, and cut off everything below a threshold:

float intensity = (snoise(vec2(time / speed, 0)) + 1.0) / 2.0;
float threshold = 0.85;
if (intensity < threshold) intensity = 0.0;
else intensity = intensity - threshold;
intensity = intensity * 10.0;

Simplex noise is continuous, so sampling it along time gives a value that drifts smoothly rather than jumping. Normalizing takes it into zero-to-one. Then the threshold at 0.85 discards the bottom eighty-five percent of the range, and subtracting the threshold rescales what's left so it starts from zero rather than jumping to 0.85.

What comes out is a signal that is exactly zero most of the time, and occasionally rises, peaks, and falls back. The effect is off, then it ramps in, hits some magnitude, and ramps out.

The properties this gets for free are the reason I still like it. The bursts are irregular, because noise is irregular, without any randomness at runtime. They ramp rather than snap, because the underlying signal is continuous. They vary in strength, because a peak that barely clears the threshold produces a weak glitch and one that reaches high produces a strong one. And they vary in duration, because how long the noise stays above the line depends on how steeply it crossed.

That's four characteristics of organic-feeling timing, and all of them fall out of the noise itself rather than being specified. A hand-written system with a random interval, a random magnitude, a ramp-in, and a ramp-out would be dozens of lines and would need state.

Two constants control the whole thing. speed divides time, so it sets how often bursts happen. threshold sets how rare and how strong they are: raise it and the glitches become less frequent and weaker, because less of the noise clears the line.

The Effect Itself Is Cheap

The visible part is two ideas applied with the intensity as their amount.

Horizontal displacement, driven by noise sampled across the screen rather than through time:

float n = (snoise(texCoords * size) + 1.0) / 2.0 / 50.0 * intensity;
vec2 distortTexCoords = texCoords;
distortTexCoords.x = distortTexCoords.x + n;

Spatial noise means the displacement varies down the screen, producing a tearing look rather than the whole image sliding. Multiplying by intensity means it only happens during a burst.

Then a color split:

vec2 separateTexCoords = distortTexCoords;
separateTexCoords.x += intensity / 100.0;
separateTexCoords.y -= intensity / 100.0;

vec4 baseColor     = texture2D(tex, distortTexCoords);
vec4 separateColor = texture2D(tex, separateTexCoords);

gl_FragColor = vec4(separateColor.r, baseColor.g, baseColor.b, baseColor.a);

Chromatic aberration, taking the red channel from a slightly offset sample and green, blue and alpha from the first one. Two texture reads rather than the three a per-channel split would need, and at glitch magnitudes nobody can tell the difference between splitting one channel and splitting all of them.

That's the whole effect: two samples, some arithmetic, and a textureless noise function. It runs on hardware from a decade before it was written.

The Noise Isn't Mine

The bulk of the file is Ashima Arts' simplex noise implementation, MIT licensed, with the attribution header intact:

// Description : Array and textureless GLSL 2D simplex noise function.
//      Author : Ian McEwan, Ashima Arts.
//     License : Copyright (C) 2011 Ashima Arts. All rights reserved.
//               Distributed under the MIT License.

Textureless is the property that matters. Classic noise implementations sample a permutation texture, which costs a texture unit and a memory read. This one computes its permutations arithmetically, so it needs no setup, no bound texture, and no state.

It's also the fourth codebase of mine where the borrowed part still carries its original header, which by now I'd call a habit rather than a coincidence.

The Material Shader Beside It

The same folder holds a Phong shader, and putting them next to each other shows what the post-process sits on top of:

uniform sampler2D diffuseMap;
uniform sampler2D specularMap;
uniform sampler2D opacityMap;
uniform sampler2D glowMap;
uniform sampler2D reflectMap;

uniform int diffuseMapSlot;
uniform int specularMapSlot;

Five texture channels, each with its own texture-coordinate slot index, so a material can use a different UV set per map. That's a real material model, and the slot indirection is the detail that makes it one: without it, every map is stuck on the same coordinates and a lightmap or a detail texture with its own layout can't be expressed.

Ambient is a hardcoded 0.25. Some of the model is properly parameterized and some of it is a constant somebody typed, which is what a material system looks like partway through becoming one.

What Transfers

The threshold trick is the part with nothing to do with rendering. Any system that needs occasional, organic, varying events can get them by sampling a continuous signal and cutting off the bottom of its range, and gets ramping, variable magnitude, and variable duration thrown in.

I've since wanted that shape in places with no graphics in them at all: deciding when a background job should do extra work, when an ambient sound should trigger, when an idle animation should play. The alternative in each case is a random timer plus a magnitude roll plus an envelope, which is more code and looks more mechanical.

The other transferable piece is smaller. Two constants, speed and threshold, control frequency and rarity independently. When an effect has a small number of knobs that map onto words a person would use to describe what they want, it can be tuned by someone who doesn't understand the implementation, and that is most of what makes a technique usable by anyone other than its author.