Skip to main content
GameDev.net gamedev.net

Randomness from a Hash

by GameDev.net · GLSL ES 3.00 (WebGL2) · 25 Aug 2026

Use in your engine

Run the shader to adjust these controls.

What it demonstrates

Procedural shaders need randomness, but a fragment shader has no random number generator and no memory of earlier frames. This shader builds repeatable randomness from coordinates alone: every grid cell asks a hash function for a number, and that number sets the cell brightness and the size of the dot stamped inside it.

Source Revision 1

Author notes are linked to specific lines.

Shader inputs

void mainImage(out vec4 fragColor, in vec2 fragCoord)

Called once per pixel. Write the colour to fragColor.

iResolution vec3
Viewport size in pixels (z is the pixel aspect ratio).
iTime float
Seconds since the shader started.
iTimeDelta float
Seconds since the previous frame.
iFrameRate float
Frames per second, smoothed.
iFrame int
Frames rendered since the start.
iMouse vec4
Mouse position: xy while held, zw of the last click.
iDate vec4
Year, month, day, and seconds within the day.
iChannel0 sampler2D
Texture bound to channel 0.
iChannel1 sampler2D
Texture bound to channel 1.
iChannel2 sampler2D
Texture bound to channel 2.
iChannel3 sampler2D
Texture bound to channel 3.
iChannelResolution vec3[4]
Pixel size of each bound channel texture.
iChannelTime float[4]
Playback time of each channel, in seconds.
iSampleRate float
Audio sample rate, always 44100.
Main Image6
1 uniform float uSeed; // @param 0.0..10.0 = 2.5 "Seed"
2 uniform int uGridScale; // @param 1..40 = 14 "Grid scale"
3
4 float hash21(vec2 cell) {
5 return fract(sin(dot(cell, vec2(12.9898, 78.233)) + uSeed) * 43758.5453);
6 }
7
8 void mainImage(out vec4 fragColor, in vec2 fragCoord) {
9 vec2 uv = fragCoord / iResolution.xy;
10 uv.x *= iResolution.x / iResolution.y;
11 vec2 grid = uv * float(uGridScale);
12 vec2 cell = floor(grid);
13 vec2 local = fract(grid) - 0.5;
14 float value = hash21(cell);
15 float radius = 0.10 + 0.32 * hash21(cell + vec2(37.7, 19.3));
16 float dotMask = 1.0 - smoothstep(radius, radius + 0.03, length(local));
17 vec3 base = mix(vec3(0.03, 0.04, 0.08), vec3(0.20, 0.26, 0.38), value);
18 vec3 color = mix(base, vec3(0.98, 0.82, 0.36), dotMask * value);
19 fragColor = vec4(color, 1.0);
20 }
21

Learn from this shader

How it works

The hash projects a cell coordinate onto a fixed direction with a dot product, feeds the result through sine, multiplies by a large constant, and keeps only the fractional part. Sine is not here to make a wave; it scrambles neighbouring inputs so that similar coordinates produce dissimilar outputs. Because the result depends only on position, it is stable across frames. Two independent values per cell come from a second call with a large offset, so brightness and radius vary without appearing linked.

Try changing

Raise Grid scale and watch the field stay coherent instead of flickering, which is the practical proof that the value is a function of position. Sweep Seed to shuffle the whole field. In a fork, offset the second call by a very small amount and watch the two values become visibly correlated, then compare this sine based hash against an integer bit mixing hash on mobile hardware where float precision is lower.

Using it in a game

This is the base layer for scattering, variation, and jitter: per tile texture choice, per instance colour, dithering, sampling offsets, and every noise function built on top. Precision differs between GPUs, so the exact pattern is not guaranteed to match everywhere; never use it for gameplay logic that has to agree across clients.

Explore the techniques

Continue with curated explanations and progressively related examples.

LicenseMIT
Views0
Forks0

Discussion

Loading comments...