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. 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. The number is for reading comparison order not measured GPU time. 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.

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

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.

#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. Set glslVersion: THREE.GLSL3 and remove #version 300 es from the shared setup.

import * as THREE from 'three';

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

	void main() {
		vUv = uv;

		gl_Position = projectionMatrix
			* modelViewMatrix
			* vec4( position, 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. Pass the shared setup plus each section's noise() as the fragmentShader string.

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 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. A sense 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.

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.

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.

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.

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.

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. 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);
}

Particles are not moved so this is a visualization of the 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. 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.

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.