Skip to main content
GDN8 PATIO

Comparing Various Noise Implementations and Relative Load

This note lines up 2D noises that can be checked as color in a Fragment Shader alone. They are not used for vertex displacement normals or lighting. Each one builds a 0–1 value from vUv and passes it into the same color palette. 2D Curl field alone is a vector field so direction is shown as hue and strength as brightness.

How to read relative load

Each noise has a relative load with Hash Noise as 1. This is not measured GPU time. It is an implementation-side guide based on hash evaluations neighborhood searches octave count interpolation and distance work when sampling the noise once for the same pixel.

Noise Relative load Main evaluations
Hash Noise 1 Hash for 1 cell
Value Noise 5 Hash and interpolation at 4 corners
Periodic Value Noise 5 Period wrapping hash and interpolation at 4 corners
Simplex Noise 8 Gradient evaluation at 3 vertices
Gradient Noise 10 2-component hash dot product and interpolation at 4 corners
2D Curl field 20 Evaluate Value Noise 4 times and take central differences
Cellular F1 23 Distances to feature points in the surrounding 9 cells
Cellular F2 − F1 24 Search the nearest 2 points among the surrounding 9 cells
fBm 25 Evaluate Value Noise over 5 octaves
Turbulence 26 fBm-level evaluation plus absolute value
Ridged fBm 27 fBm-level evaluation plus ridging
Hybrid Multifractal 28 Weight 5 octaves by the preceding value
Domain Warp 75 Evaluate 5-octave fBm 3 times

Relative load here is a number for reading comparison order. A load of 10 does not mean exactly 10× the draw time of a load of 1. The cost of sin() sqrt() pow() and similar calls differs by GPU and shader compiler optimization also comes into play.

Shared setup

The following assumes WebGL 2 / GLSL ES 3.00. A 0–1 vUv is passed from the Vertex Shader. Add each section's noise() and the functions it needs to this shared setup. When using Three.js ShaderMaterial set glslVersion: THREE.GLSL3 and remove the #version 300 es line below from the Fragment Shader string. Three.js adds the same version directive on its side.

#version 300 es

precision highp float;

in vec2 vUv;
out vec4 fragColor;

uniform float uTime;

// The permutation polynomial is adapted from webgl-noise (MIT).
// The full copyright and permission notice is included below.
float permute289(float x) {
	return mod(((x * 34.0) + 10.0) * x, 289.0);
}

float hash12(vec2 cell) {
	vec2 wrapped = mod(cell, 289.0);
	float seed = permute289(permute289(wrapped.y) + wrapped.x);

	return seed / 289.0;
}

vec2 hash22(vec2 cell) {
	return vec2(
		hash12(cell),
		hash12(cell + vec2(37.0, 101.0))
	);
}

float noise(vec2 p);

vec3 colorize(float value) {
	float amount = smoothstep(0.0, 1.0, value);

	return mix(
		vec3(0.06, 0.12, 0.20),
		vec3(0.96, 0.42, 0.18),
		amount
	);
}

void main() {
	vec2 p = vUv * 8.0;
	p += vec2(0.0, uTime * 0.15);

	float n = noise(p);

	fragColor = vec4(colorize(n), 1.0);
}

colorize() is shared by every example so it is not included in relative load. For grayscale change the final line to vec4(vec3(n), 1.0).

Using Three.js ShaderMaterial

In Three.js you pass the Vertex Shader and Fragment Shader as strings. Keep the noise calculation in the Fragment Shader only and use a minimal Vertex Shader that forwards uv as vUv.

The next listing is a full setup with Value Noise.

import * as THREE from 'three';

const vertexShader = /* glsl */`
	out vec2 vUv;

	void main() {
		vUv = uv;

		gl_Position = projectionMatrix
			* modelViewMatrix
			* vec4( position, 1.0 );
	}
`;

const fragmentShader = /* glsl */`
	in vec2 vUv;
	out vec4 fragColor;

	uniform float uTime;

	// The permutation polynomial is adapted from webgl-noise (MIT).
	float permute289( float x ) {
		return mod( ( ( x * 34.0 ) + 10.0 ) * x, 289.0 );
	}

	float hash12( vec2 cell ) {
		vec2 wrapped = mod( cell, 289.0 );
		float seed = permute289( permute289( wrapped.y ) + wrapped.x );

		return seed / 289.0;
	}

	float valueNoise( vec2 p ) {
		vec2 i = floor( p );
		vec2 f = fract( p );
		vec2 u = f * f * ( 3.0 - 2.0 * f );

		float a = hash12( i );
		float b = hash12( i + vec2( 1.0, 0.0 ) );
		float c = hash12( i + vec2( 0.0, 1.0 ) );
		float d = hash12( i + vec2( 1.0, 1.0 ) );

		return mix(
			mix( a, b, u.x ),
			mix( c, d, u.x ),
			u.y
		);
	}

	float noise( vec2 p ) {
		return valueNoise( p );
	}

	vec3 colorize( float value ) {
		float amount = smoothstep( 0.0, 1.0, value );

		return mix(
			vec3( 0.06, 0.12, 0.20 ),
			vec3( 0.96, 0.42, 0.18 ),
			amount
		);
	}

	void main() {
		vec2 p = vUv * 8.0;
		p += vec2( 0.0, uTime * 0.15 );

		float n = noise( p );

		fragColor = vec4( colorize( n ), 1.0 );
	}
`;

const material = new THREE.ShaderMaterial( {
	glslVersion: THREE.GLSL3,
	uniforms: {
		uTime: { value: 0 }
	},
	vertexShader,
	fragmentShader
} );

position uv modelViewMatrix and projectionMatrix are provided to ShaderMaterial by Three.js so they are not declared again in the shader. #version 300 es is also added by Three.js from glslVersion. To switch to another noise replace noise() inside the Fragment Shader and any functions it depends on such as valueNoise() or cellularDistances() with the code from each section.

Hash Noise

Relative load: 1

Pass floored cell coordinates straight into the hash. Without interpolation there is no continuity between neighboring cells so the result becomes fine grain or blocky variation. It suits grain dithering and random selection. It is not a strong fit when coordinates need to move continuously.

float noise(vec2 p) {
	return hash12(floor(p));
}

For grain at the pixel level rather than the cell level use floor(gl_FragCoord.xy) as the input.

Value Noise

Relative load: 5

Place scalar values at the four corners of a grid and interpolate between them. The implementation is short and comparatively light so it works well as a first stage before breaking a shape further or as low-frequency wobble. A hint of the axis-aligned grid still remains.

float valueNoise(vec2 p) {
	vec2 i = floor(p);
	vec2 f = fract(p);
	vec2 u = f * f * (3.0 - 2.0 * f);

	float a = hash12(i);
	float b = hash12(i + vec2(1.0, 0.0));
	float c = hash12(i + vec2(0.0, 1.0));
	float d = hash12(i + vec2(1.0, 1.0));

	return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);
}

float noise(vec2 p) {
	return valueNoise(p);
}

Periodic Value Noise

Relative load: 5

Wrapping Value Noise lattice coordinates with mod() gives it a period. Use this when a background or looping asset needs seamless joins left–right and top–bottom.

float periodicValueNoise(vec2 p, vec2 period) {
	vec2 i = floor(p);
	vec2 f = fract(p);
	vec2 u = f * f * (3.0 - 2.0 * f);

	vec2 i00 = mod(i, period);
	vec2 i10 = mod(i + vec2(1.0, 0.0), period);
	vec2 i01 = mod(i + vec2(0.0, 1.0), period);
	vec2 i11 = mod(i + vec2(1.0, 1.0), period);

	float a = hash12(i00);
	float b = hash12(i10);
	float c = hash12(i01);
	float d = hash12(i11);

	return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);
}

float noise(vec2 p) {
	return periodicValueNoise(p, vec2(8.0));
}

period is in lattice units. Adding another scale or a non-integer transform partway through the period brings seams back so take care.

Gradient Noise

Relative load: 10

Place directions rather than values at the four grid corners and interpolate the dot products of each direction with the position inside the cell. It tends to produce smoother more natural shading than Value Noise. On the other hand each of the four corners needs a 2-component hash.

float gradientNoise(vec2 p) {
	vec2 i = floor(p);
	vec2 f = fract(p);
	vec2 u = f * f * f * (f * (f * 6.0 - 15.0) + 10.0);

	vec2 g00 = hash22(i) * 2.0 - 1.0;
	vec2 g10 = hash22(i + vec2(1.0, 0.0)) * 2.0 - 1.0;
	vec2 g01 = hash22(i + vec2(0.0, 1.0)) * 2.0 - 1.0;
	vec2 g11 = hash22(i + vec2(1.0, 1.0)) * 2.0 - 1.0;

	float a = dot(g00, f);
	float b = dot(g10, f - vec2(1.0, 0.0));
	float c = dot(g01, f - vec2(0.0, 1.0));
	float d = dot(g11, f - vec2(1.0, 1.0));

	float n = mix(mix(a, b, u.x), mix(c, d, u.x), u.y);

	return clamp(n * 0.7 + 0.5, 0.0, 1.0);
}

float noise(vec2 p) {
	return gradientNoise(p);
}

Gradient vectors are not normalized here to keep the code short. If you want a stricter distribution add normalize() to the four gradients. That also increases the work.

Simplex Noise

Relative load: 8

A 2D lattice is split into triangles rather than squares and the value is taken from gradients at three vertices. Compared with Gradient Noise which looks at four corners there is one fewer evaluation point and the axis-aligned grid is relatively harder to see.

float simplexNoise(vec2 p) {
	// The simplex coordinate structure is adapted from webgl-noise (MIT).
	const float SKEW = 0.36602540378;
	const float UNSKEW = 0.21132486540;

	vec2 i = floor(p + (p.x + p.y) * SKEW);
	vec2 a = p - i + (i.x + i.y) * UNSKEW;
	vec2 o = a.x > a.y ? vec2(1.0, 0.0) : vec2(0.0, 1.0);
	vec2 b = a - o + UNSKEW;
	vec2 c = a - 1.0 + 2.0 * UNSKEW;

	vec3 h = max(0.5 - vec3(dot(a, a), dot(b, b), dot(c, c)), 0.0);
	h *= h;
	h *= h;

	vec3 n = vec3(
		dot(a, hash22(i) * 2.0 - 1.0),
		dot(b, hash22(i + o) * 2.0 - 1.0),
		dot(c, hash22(i + 1.0) * 2.0 - 1.0)
	);

	return clamp(0.5 + 35.0 * dot(h, n), 0.0, 1.0);
}

float noise(vec2 p) {
	return simplexNoise(p);
}

This is a short comparison-oriented implementation that shares hash22(). When you need periodization analytical derivatives or 3D / 4D input Stefan Gustavson's webgl-noise or psrdnoise is usually easier to work with.

Cellular Noise / Worley F1

Relative load: 23

Place one feature point in each cell and return the distance F1 to the nearest point from the current position. The result is a cellular distribution that becomes a base for bubbles spots and cracks.

First prepare a function that returns the distances to the nearest two points together.

vec2 cellularDistances(vec2 p) {
	vec2 i = floor(p);
	vec2 f = fract(p);
	float f1 = 8.0;
	float f2 = 8.0;

	for (int y = -1; y <= 1; y++) {
		for (int x = -1; x <= 1; x++) {
			vec2 cell = vec2(float(x), float(y));
			vec2 point = hash22(i + cell);
			vec2 delta = cell + point - f;
			float d = dot(delta, delta);

			if (d < f1) {
				f2 = f1;
				f1 = d;
			} else if (d < f2) {
				f2 = d;
			}
		}
	}

	return sqrt(vec2(f1, f2));
}

float noise(vec2 p) {
	float f1 = cellularDistances(p).x;

	return 1.0 - clamp(f1, 0.0, 1.0);
}

Dropping sqrt() and using squared distance as-is can make it a bit lighter. The light–dark distribution changes though.

Cellular Noise / Worley F2 − F1

Relative load: 24

Subtracting F1 from the distance to the second-nearest point F2 makes the cell borders dark and the centers brighter. The search range is the same as F1 so the load difference is small.

float noise(vec2 p) {
	vec2 distance = cellularDistances(p);

	return clamp((distance.y - distance.x) * 2.0, 0.0, 1.0);
}

The important part is not computing F1 and F2 separately but returning both from the same search.

fBm

Relative load: 25

fBm (Fractional Brownian Motion) adds several noises while raising frequency and lowering amplitude. Large and fine shapes can coexist so it becomes easier to form masses like clouds or terrain.

The listing below uses the earlier valueNoise() five times.

const mat2 OCTAVE_ROTATION = mat2(
	0.7648421873, -0.6442176872,
	0.6442176872, 0.7648421873
);

float fbm(vec2 p) {
	float value = 0.0;
	float amplitude = 0.516129;

	for (int octave = 0; octave < 5; octave++) {
		value += amplitude * valueNoise(p);
		p = OCTAVE_ROTATION * p * 2.0;
		amplitude *= 0.5;
	}

	return value;
}

float noise(vec2 p) {
	return fbm(p);
}

OCTAVE_ROTATION is a rotation matrix of 0.7 radians. It reduces lattice directions lining up too strongly when octaves are stacked. The initial amplitude is 0.516129 so the five-octave amplitude sum is nearly 1.

Turbulence

Relative load: 26

Remap each octave to −1–1 take the absolute value then stack. Valleys fold back so more streaks appear than in fBm. It is a useful base for smoke fire and marble flow.

float turbulence(vec2 p) {
	float value = 0.0;
	float amplitude = 0.516129;

	for (int octave = 0; octave < 5; octave++) {
		float n = valueNoise(p) * 2.0 - 1.0;

		value += amplitude * abs(n);
		p = OCTAVE_ROTATION * p * 2.0;
		amplitude *= 0.5;
	}

	return value;
}

float noise(vec2 p) {
	return turbulence(p);
}

Ridged fBm

Relative load: 27

Inverting the absolute value and squaring it turns the middle of the original noise into thin ridges. It leans toward shapes like mountain ranges cracks and electric streaks.

float ridgedFbm(vec2 p) {
	float value = 0.0;
	float amplitude = 0.516129;

	for (int octave = 0; octave < 5; octave++) {
		float ridge = 1.0 - abs(valueNoise(p) * 2.0 - 1.0);

		value += amplitude * ridge * ridge;
		p = OCTAVE_ROTATION * p * 2.0;
		amplitude *= 0.5;
	}

	return value;
}

float noise(vec2 p) {
	return ridgedFbm(p);
}

Hybrid Multifractal

Relative load: 28

Each octave's contribution is changed by the value accumulated so far. Ordinary fBm adds the same fineness everywhere. Here the appearance of detail changes with the low-frequency shape.

float hybridMultifractal(vec2 p) {
	const float offset = 0.35;
	float spectralWeight = 0.5;
	float value = (valueNoise(p) + offset) * spectralWeight;
	float weight = value;

	p = OCTAVE_ROTATION * p * 2.0;
	spectralWeight *= 0.5;

	for (int octave = 1; octave < 5; octave++) {
		weight = clamp(weight, 0.0, 1.0);
		float signal = (valueNoise(p) + offset) * spectralWeight;

		value += weight * signal;
		weight *= signal;
		p = OCTAVE_ROTATION * p * 2.0;
		spectralWeight *= 0.5;
	}

	return clamp(value, 0.0, 1.0);
}

float noise(vec2 p) {
	return hybridMultifractal(p);
}

Bias in the detail changes quite a bit with how weight is updated. This is less a fixed drop-in replacement for fBm and more a process you tune to the density you need.

2D Curl field

Relative load: 20

Taking partial derivatives of a 2D scalar field and rotating them 90 degrees yields a divergence-free vector field. This is not a scalar noise that returns a 0–1 value. It is a vector field with a 2D direction at each coordinate.

This time direction is mapped to hue and strength to brightness so the field can be checked in a Fragment Shader alone. In this section replace the shared noise() and main() with the code below. The color mapping is display work so it is not included in relative load.

vec2 curlField(vec2 p) {
	const float epsilon = 0.02;

	float left = valueNoise(p - vec2(epsilon, 0.0));
	float right = valueNoise(p + vec2(epsilon, 0.0));
	float bottom = valueNoise(p - vec2(0.0, epsilon));
	float top = valueNoise(p + vec2(0.0, epsilon));
	float dx = (right - left) / (2.0 * epsilon);
	float dy = (top - bottom) / (2.0 * epsilon);

	return vec2(dy, -dx);
}

vec3 hueToRgb(float hue) {
	vec3 rgb = abs(
		fract(hue + vec3(0.0, 2.0 / 3.0, 1.0 / 3.0))
		* 6.0 - 3.0
	) - 1.0;

	return clamp(rgb, 0.0, 1.0);
}

void main() {
	vec2 p = vUv * 8.0;
	p += vec2(0.0, uTime * 0.15);

	vec2 field = curlField(p);
	float hue = atan(field.y, field.x) / 6.28318530718 + 0.5;
	float strength = clamp(length(field) * 0.35, 0.0, 1.0);
	vec3 color = hueToRgb(hue) * mix(0.15, 1.0, strength);

	fragColor = vec4(color, 1.0);
}

Continuous color change shows vector direction and brightness shows strength. Particles are not moved so this is a visualization of the 2D Curl field itself not a fluid trail.

With Simplex Noise that returns analytical derivatives the four finite-difference evaluations are unnecessary. When using it as a real vector field a derivative-aware implementation such as psrdnoise may end up lighter.

Domain Warp

Relative load: 75

Warp the noise input coordinates with another noise. Straight lattices break down and large flows like fluid or clouds appear. The implementation below evaluates fBm twice to build a 2-component displacement and once more for the warped result for three fBm evaluations in total.

float domainWarp(vec2 p) {
	vec2 displacement = vec2(
		fbm(p + vec2(13.0, 29.0)),
		fbm(OCTAVE_ROTATION * p + vec2(-23.0, 7.0))
	);

	return fbm(p + (displacement * 2.0 - 1.0) * 3.5);
}

float noise(vec2 p) {
	return domainWarp(p);
}

The look changes a lot and the load rises with it. Cutting fBm to 3 octaves first storing only one component in a lower-resolution Render Target or lowering the update rate all help.

How to choose

First candidates can be narrowed as follows.

  • Fine grain or random selection: Hash Noise
  • Light continuous wobble: Value Noise
  • Want less axis-aligned habit: Gradient Noise or Simplex Noise
  • Bubbles spots compartments: Cellular F1 or F2 − F1
  • Want large and small shapes stacked: fBm
  • Need streaks or ridges: Turbulence or Ridged fBm
  • Large flowing shapes: Domain Warp
  • Want to tile and repeat: Periodic Value Noise

In practice Value Noise Simplex Noise Cellular F1 and fBm already cover a wide range. Domain Warp is easy to change visually but using it full-screen as-is tends to get heavy.

The hash here is based on the webgl-noise permutation polynomial. Input repeats every 289 cells so it is not a fit for huge coordinates or long periods. In that case replace it with an integer-based PCG Hash or similar after checking usage terms. Blue Noise is usually sampled from a texture so it is left out of this note which stays with texture-free Fragment Shaders only.

Code sources and license

This note and the demo refer to algorithm names and common calculation steps. The GLSL and Three.js structure is rewritten for the article. The shared hash permute289() and the Simplex Noise coordinate structure adapt webgl-noise. webgl-noise is published under the MIT License so the full copyright and permission notice is included below.

Copyright (C) 2011 by Ashima Arts (Simplex noise)
Copyright (C) 2011-2016 by Stefan Gustavson (Classic noise and others)

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

References

  • ※ This article is an AI translation of the Japanese original.