Skip to main content
GDN8 PATIO

いろいろなNoise実装と負荷の比較

Fragment Shaderだけで色として確認できる2Dノイズを並べます。頂点の変形や法線、ライティングには使わずvUvから0〜1の値をつくり、同じカラーパレットへ渡します。2D Curl fieldだけはベクトル場なので方向を色相、強さを明るさで表示します。

相対負荷の見方

各ノイズにはHash Noiseを1とした相対負荷を付けています。これは実測したGPU時間ではなく同じピクセルでノイズを1回求める際のハッシュ評価数、近傍探索数、オクターブ数、補間や距離計算をもとにした実装上の目安です。

ノイズ 相対負荷 主な評価内容
Hash Noise 1 1セルのハッシュ
Value Noise 5 4隅のハッシュと補間
Periodic Value Noise 5 4隅の周期化、ハッシュ、補間
Simplex Noise 8 3頂点の勾配評価
Gradient Noise 10 4隅の2成分ハッシュ、内積、補間
2D Curl field 20 Value Noiseを4回評価して中央差分を取る
Cellular F1 23 周囲9セルの特徴点との距離
Cellular F2 − F1 24 周囲9セルから近い2点を探索
fBm 25 Value Noiseを5オクターブ評価
Turbulence 26 fBm相当の評価と絶対値
Ridged fBm 27 fBm相当の評価と稜線化
Hybrid Multifractal 28 5オクターブを前段の値で重み付け
Domain Warp 75 5オクターブのfBmを3回評価

ここでいう相対負荷は比較の順序を見るための数字です。負荷10が負荷1のちょうど10倍の描画時間になるという意味ではありません。sin()sqrt()pow()などの処理コストはGPUで異なり、シェーダーコンパイラによる最適化も入ります。

共通部分

以下はWebGL 2 / GLSL ES 3.00を前提とします。Vertex Shaderから0〜1のvUvが渡されている状態です。各節のnoise()と必要な関数をこの共通部分へ追加します。Three.jsのShaderMaterialで使う場合はglslVersion: THREE.GLSL3を指定し、下の#version 300 esはFragment Shaderの文字列から外します。Three.js側で同じバージョン指定が追加されるためです。

#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()はすべての例で共通なので相対負荷には含めていません。グレースケールで確認する場合は最後をvec4(vec3(n), 1.0)へ変更します。

Three.jsのShaderMaterialで使う

Three.jsではVertex ShaderとFragment Shaderを文字列で渡します。ノイズの計算はFragment Shaderだけに置き、Vertex ShaderはuvvUvとして渡す最小構成にします。

次は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
} );

positionuvmodelViewMatrixprojectionMatrixShaderMaterialへThree.jsから渡されるため、シェーダー内で改めて宣言しません。#version 300 esもThree.jsがglslVersionから追加します。ほかのノイズへ変える場合はFragment Shader内のnoise()と、その関数が依存するvalueNoise()cellularDistances()などを各節のコードへ差し替えます。

Hash Noise

相対負荷:1

整数化したセル座標をそのままハッシュへ渡します。補間しないため隣接セルとのつながりはなく、細かな粒やブロック状のばらつきになります。粒状感、ディザ、ランダムな選択に相性が良い一方で座標を連続的に動かす用途にはあまり向きません。

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

セルではなくピクセル単位の粒にする場合はfloor(gl_FragCoord.xy)を入力に使います。

Value Noise

相対負荷:5

格子の4隅にスカラー値を置き、その間を補間します。実装が短く負荷も比較的軽いので、形を大きく崩す前段や低い周波数の揺らぎに使いやすいノイズです。ただ軸に沿った格子の気配は残ります。

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

相対負荷:5

Value Noiseの格子座標をmod()で折り返すと周期を持たせられます。左右、上下をつなぐ背景やループ素材で継ぎ目をなくしたい場合に使います。

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は格子単位です。周期の途中で別のスケールや非整数の座標変換を加えると継ぎ目が戻るので注意です。

Gradient Noise

相対負荷:10

格子の4隅に値ではなく方向を置き、各方向と格子内の位置との内積を補間します。Value Noiseより滑らかで自然な濃淡をつくりやすい一方で、4隅それぞれに2成分のハッシュが必要です。

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

ここではコードを短くするため勾配ベクトルを正規化していません。分布を厳密に揃えたい場合は4つの勾配へnormalize()を加えますが、その分だけ処理も増えます。

Simplex Noise

相対負荷:8

2Dの格子を正方形ではなく三角形へ分け、3頂点の勾配から値を求めます。4隅を見るGradient Noiseより評価点が一つ少なく、軸方向の格子も比較的見えにくくなります。

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

これは比較用にhash22()を共通化した短い実装です。周期化、解析的な微分、3Dや4D入力が必要な場合はStefan Gustavsonらのwebgl-noisepsrdnoiseを使った方が扱いやすいと思います。

Cellular Noise / Worley F1

相対負荷:23

各セルへ特徴点を一つ置き現在位置から最も近い点までの距離F1を返します。泡、斑点、ひび割れの土台になるセル状の分布です。

まず近い二点までの距離をまとめて返す関数を用意します。

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

sqrt()を外して距離の二乗をそのまま使えば少し軽くできます。ただ明暗の分布は変わります。

Cellular Noise / Worley F2 − F1

相対負荷:24

二番目に近い点までの距離F2からF1を引くとセルの境界付近が暗く中心へ向かって明るくなる形をつくれます。探索範囲はF1と同じなので負荷差は小さめです。

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

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

F1とF2を別々に計算せず、同じ探索から二つとも返すことが重要です。

fBm

相対負荷:25

fBm(Fractional Brownian Motion)は周波数を上げながら振幅を下げ複数のノイズを足していく方法です。大きな形と細かな形を同時に持てるため雲や地形のようなまとまりをつくりやすくなります。

以下では前述のvalueNoise()を5回使います。

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は0.7ラジアンの回転行列です。オクターブを重ねた際に格子方向が揃いすぎるのを抑えます。5オクターブの振幅合計がほぼ1になるよう初期値を0.516129にしています。

Turbulence

相対負荷:26

各オクターブを−1〜1へ戻し絶対値を取ってから重ねます。谷が折り返されるため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

相対負荷:27

絶対値を反転して二乗すると元のノイズの中央付近が細い稜線になります。山脈、亀裂、電気的な筋のような形へ寄せやすい方法です。

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

相対負荷:28

各オクターブの寄与を一つ前までの値で変えます。通常のfBmは全域へ同じ細かさを足しますがこちらは低い周波数の形に応じて細部の出方が変わります。

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

細部の偏りはweightの更新方法でかなり変わります。fBmの置き換えとして固定的に使うというより、必要な密度に合わせて調整する処理です。

2D Curl field

相対負荷:20

2Dのスカラー場から偏微分を取り90度回転させると発散のないベクトル場をつくれます。これは0〜1の値を返すスカラーNoiseではなく各座標に2Dの向きを持つベクトル場です。

今回はFragment Shaderだけで場の状態を確認できるようにベクトルの方向を色相、強さを明るさへ割り当てます。この節では共通部分のnoise()main()を以下のコードへ置き換えます。色への変換は表示処理なので相対負荷には含めていません。

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

色の連続的な変化がベクトルの向き、明暗が強さを表します。粒子を動かしていないため流体の軌跡ではなく2D Curl fieldそのものの可視化です。

解析的な微分を返せるSimplex Noiseを使えば4回の差分評価は不要です。実際にベクトル場として使う場合はpsrdnoiseのような微分付き実装を選んだ方が軽くなる可能性があります。

Domain Warp

相対負荷:75

ノイズの入力座標を別のノイズで歪めます。直線的な格子が崩れ流体や雲のような大きな流れが出ます。下の実装は2成分の歪みをつくるためにfBmを2回、歪めた結果を求めるためにもう1回、合計3回のfBmを評価します。

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

見た目の変化は大きいですが負荷もそのまま増えます。まずfBmを3オクターブへ減らす、片方の成分だけ低解像度のRender Targetへ保存する、更新頻度を落とすといった調整が効きます。

選び方

最初の候補は次のように絞れます。

  • 細かな粒やランダムな選択:Hash Noise
  • 軽い連続的な揺らぎ:Value Noise
  • 軸方向の癖を減らしたい:Gradient NoiseまたはSimplex Noise
  • 泡、斑点、区画:Cellular F1またはF2 − F1
  • 大小の形を重ねたい:fBm
  • 筋や稜線が必要:TurbulenceまたはRidged fBm
  • 大きく流れる形:Domain Warp
  • タイルとして繰り返したい:Periodic Value Noise

現実的にはValue Noise、Simplex Noise、Cellular F1、fBmの4種類があればかなり広い範囲を試せます。Domain Warpは見た目を変えやすい一方でフルスクリーンへそのまま使うと重くなりがちです。

今回のハッシュはwebgl-noiseの順列多項式をもとにしています。入力は289セルで繰り返すため巨大な座標や長い周期が必要な用途には向きません。その場合は利用条件を確認した上で整数ベースのPCG Hashなどへ置き換えます。Blue Noiseは通常テクスチャを参照して使うためテクスチャなしのFragment Shaderだけを扱うこの記事からは外しています。

コードの出典とライセンス

本稿とデモではアルゴリズムの名称や一般的な計算手順を参照しGLSLとThree.jsの構成は記事用に書き起こしています。共通ハッシュのpermute289()とSimplex Noiseの座標構造はwebgl-noiseを変更して使用しています。webgl-noiseはMIT Licenseで公開されているため以下に著作権表示と許諾表示を全文掲載します。

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.

参考