はじめに
前回は Domain Warping を理論的に整理した。
- ノイズは値を揺らす技術ではない
- 空間そのものを変形させる技術である
- スカラー場をベクトル場へ拡張することで、方向性が生まれる
- FBM と組み合わせることで、自己相似構造を破壊できる
そこまで理解した。
しかし、理論だけでは“世界”は生まれない。
数式はまだ抽象のままだ。
[Noise 入門 #06] Domain Warping — 座標をねじると世界が壊れる
Noise 入門シリーズ第6回。Domain Warping(ドメインワーピング)の仕組みを、座標変形という視点から図解で解説。通常ノイズとの違い、FBMとの組み合わせ、strengthパラメータの効果、3D/時間拡張まで体系的に整理します。
https://humanxai.info/posts/noise-intro-06-domain-warping/今回やること
今回はそれを Three.js + GLSL で実装する。
やることはシンプルだ:
- ベースとなるノイズを用意する
- 座標を歪ませる(Domain Warping)
- FBM と組み合わせる
- strength パラメータで空間を制御する
- 時間を加えて 4D に拡張する
理論を、GPU上で動かす。
ここから先、ノイズは概念ではない。
描画アルゴリズムになる。
1. なぜ Shader でやるのか?
Domain Warping は「値の操作」ではない。
空間そのものを変形するアルゴリズムである。
この処理は、圧倒的に GPU 向きだ。
ノイズは GPU 向き
ノイズは基本的に
という形の関数であり、 座標 $\mathbf{p}$ に対して値を返す。
つまり:
- 各ピクセルごとに独立して計算できる
- 隣接ピクセルとの依存関係がほぼない
- 同じ処理を大量に繰り返す
これは GPU の得意分野そのものだ。
1ピクセルごとの計算
フラグメントシェーダーでは、
- 画面上のすべてのピクセルに対して
- 同じコードが
- 並列に実行される
Domain Warping も同じ。
各ピクセルがそれぞれ:
- 座標を受け取り
- ノイズを計算し
- 座標を歪ませ
- 再度ノイズを評価する
この処理は CPU でループさせるより、 GPUで並列処理した方が圧倒的に高速。
CPUではなく並列処理
CPU は:
- ロジック制御が得意
- 分岐が多い処理に強い
GPU は:
- 同じ計算を大量に行う処理に強い
- 数学関数に強い
- ベクトル演算に最適化されている
ノイズは
- sin
- dot
- mix
- smoothstep
といったベクトル・補間・内積演算の集合体。
これは GPU のために設計されたかのような処理だ。
WebGL の本質
Three.js は JavaScript ライブラリだが、
実際に絵を描いているのは GPU である。
そして WebGL の本質は、
JavaScriptでロジックを書き、 GPUで空間を計算すること
Domain Warping を Shader で実装するということは、
「ノイズを数学として理解する」段階から
「ノイズを描画エンジンの内部処理として扱う」段階へ進むことを意味する。
ここまでで読者の立ち位置は変わる。
ノイズは“数式”ではない。
GPU上で動く、空間変形アルゴリズムである。
2. ベースとなる 2D ノイズ実装
まずはシンプルな 2D ノイズ関数を用意する。
float noise(vec2 p);
ここでは Perlin Noise を使う。
アルゴリズムの詳細は #04 で解説しているため、 今回は“理解済みの部品”として扱う。
シンプルな 2D Perlin Noise 実装例
vec2 hash(vec2 p) {
p = vec2(
dot(p, vec2(127.1, 311.7)),
dot(p, vec2(269.5, 183.3))
);
return -1.0 + 2.0 * fract(sin(p) * 43758.5453123);
}
float noise(vec2 p) {
vec2 i = floor(p);
vec2 f = fract(p);
vec2 u = f * f * (3.0 - 2.0 * f); // smoothstep補間
float a = dot(hash(i + vec2(0.0, 0.0)), f - vec2(0.0, 0.0));
float b = dot(hash(i + vec2(1.0, 0.0)), f - vec2(1.0, 0.0));
float c = dot(hash(i + vec2(0.0, 1.0)), f - vec2(0.0, 1.0));
float d = dot(hash(i + vec2(1.0, 1.0)), f - vec2(1.0, 1.0));
return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);
}
今回重要なのはここではない
重要なのはアルゴリズムの再理解ではない。
重要なのは:
- 座標 p を入力すると
- 連続的な値が返る
- 周波数を変えれば模様が変わる
という事実だけ。
ノイズは
という関数であり、 座標空間をスカラー場へ変換する。
この関数が“基準世界”になる。
次の章で、この座標を歪ませる。
ここから空間が壊れる。
3. Domain Warping の実装
まず理論を思い出す。
しかしここで重要なのは、
空間は2次元(または3次元)であるということ。
単一の float 値では方向を持たない。
Domain Warping では、
スカラー場を ベクトル場へ拡張 する必要がある。
ベクトル場を作る
Shader では次のように書ける:
vec2 warp = vec2(
noise(p + 0.0),
noise(p + 10.0)
);
p += warp * strength;
ここでやっていることは単純だ:
noise(p)を2回評価する- 異なるオフセットを与える
- それを
vec2にまとめる
これで 方向を持つ歪みベクトル が生まれる。
なぜオフセットが必要か?
もしこう書いたら:
vec2 warp = vec2(noise(p), noise(p));
x と y が同じ値になる。
つまり、対角線方向にしか歪まない。
空間が単調になる。
だから、
noise(p + 0.0)
noise(p + 10.0)
のようにオフセットを与えて、
独立した揺らぎを生成する。
これは“疑似的なベクトル場”を作るテクニック。
strength の意味
p += warp * strength;
- strength が小さい → 柔らかい流れ
- strength が中程度 → 有機的構造
- strength が大きい → 空間が破壊される
ここで初めて、
「ノイズが模様を作る」のではなく
「空間が変形している」
と実感できる。
4. FBMと組み合わせる
単一のノイズでは、まだ単調さが残る。
それを壊すのが FBM(Fractal Brownian Motion)。
float fbm(vec2 p) {
float v = 0.0;
float a = 0.5;
for(int i = 0; i < 5; i++){
v += noise(p) * a;
p *= 2.0; // frequency を上げる
a *= 0.5; // amplitude を下げる
}
return v;
}
ここでは:
- 周波数を倍々に増やし
- 振幅を半分ずつ減らす
ことで、自己相似構造を持つ複雑なパターンを生成している。
Domain Warping に FBM を使う
単純な noise() の代わりに fbm() を使う。
vec2 warp = vec2(
fbm(p + 0.0),
fbm(p + 5.2)
);
p += warp * strength;
これで歪みベクトル自体が多層構造になる。
何が起きているのか?
1回のノイズ歪みは、滑らかな流れを作る。
FBMを使うと:
- 小さな歪み
- 中程度の歪み
- 大きな歪み
が同時に混在する。
その結果、
- 川のような流れ
- 雲の層構造
- 有機的な模様
- 地形の侵食感
が現れる。
ここで初めて
“自然っぽさ”が出る。
それは偶然ではない。
フラクタル構造を持つ歪みが、 空間そのものに埋め込まれているからだ。
5. strength を変えると何が起きるか
Domain Warping は、strength によって世界の歪み量が決まる。
p += warp * strength;
たったこれだけの係数が、空間の秩序を左右する。
strength が小さい場合(例:0.1〜0.3)
- 柔らかい流れ
- 水面の揺らぎ
- 軽い風のような変形
元のノイズ構造はまだ保たれている。
「歪み」というより「揺れ」。
strength が中程度(例:0.5〜1.0)
- 有機的な模様
- 雲の層構造
- 地形の侵食感
- マーブル模様のような構造
ここで一気に“自然らしさ”が出る。
ノイズはもはや単なるパターンではなく、 空間そのものが流動しているように見える。
strength が大きい(例:2.0以上)
- 空間が裂ける
- 自己相似構造が崩壊する
- 渦巻き・断裂・歪曲
ここまで来ると、もはや“自然”ではない。
空間そのものが破壊される。
ブラックホール、電磁嵐、プラズマ、量子揺らぎ。
この領域に入る。
なぜここで興奮するのか
strength を変えるだけで、
- 世界の物理法則が変わる
- 模様の秩序が崩れる
- 自然と異常の境界が揺れる
たった1つのスカラー値が、 空間の性質を支配している。
ここで初めて気づく。
ノイズは“模様生成”ではない。
空間設計である。
ここは必ずスクリーンショットを並べる。
- strength = 0.2
- strength = 0.8
- strength = 2.0
並べた瞬間、読者は理解する。
理論が、視覚になる。
6. 時間を追加する(4D拡張)
これまで扱ってきたのは 2D 空間だった。
しかしノイズは、次元を増やせる。
時間を加えることで、
になる。
Shader での実装
p += warp * strength;
float n = fbm(p + time * 0.1);
ここで time は uniform として渡す。
uniforms: {
time: { value: 0.0 }
}
そして useFrame で更新する:
material.uniforms.time.value = clock.getElapsedTime();
何が起きているのか?
時間は“値”ではない。
座標の一部として扱われている。
つまり、
空間そのものが時間方向にスライドしている。
これは単なるアニメーションではない。
4次元空間の断面を見ている状態だ。
なぜ自然に見えるのか?
自然界の多くは、
- 風
- 水流
- 雲
- 炎
- 霧
時間とともに変化する。
ノイズに時間を加えることで、
“静的模様”が
“動的構造”へ変わる。
世界が呼吸する
strength が空間の歪み量なら、
time は空間の流速。
FBM + Domain Warping + time。
これで:
- 雲が流れ
- 水が揺れ
- エネルギーが渦巻く
ノイズは完全に“世界生成アルゴリズム”になる。
7. Three.js 側コード
Domain Warping のロジックはすべて Shader 内で完結する。
Three.js 側では、
uniformを渡すtimeを更新する
それだけでよい。
ShaderMaterial の設定
const material = new THREE.ShaderMaterial({
uniforms: {
time: { value: 0.0 },
strength: { value: 0.5 }
},
vertexShader,
fragmentShader
});
ここで渡しているのは:
time… 4D拡張のための時間軸strength… 空間歪みの強度
空間を制御するための、最小限のパラメータ。
time の更新(R3F例)
useFrame((state) => {
material.uniforms.time.value = state.clock.getElapsedTime();
});
これで毎フレーム、時間が進む。
ノイズは再計算され、
空間が動き続ける。
本質
Three.js は「描画エンジン」ではあるが、
実際に空間を計算しているのは GPU である。
JavaScript は命令を渡すだけ。
ノイズは CPU でループしていない。
ピクセル単位で並列に評価され、
空間が変形している。
これで完成。
Domain Warping は理論ではない。
実装された、空間変形アルゴリズムである。
8. まとめ
今回の到達点は明確だ。
- Domain Warping は理論ではない
- 空間を書き換える GPU アルゴリズムである
- FBM と組み合わせることで自然構造を生成できる
- 2D → 3D → 時間へと拡張可能
ここまで来ると、
ノイズは“模様生成テクニック”ではない。
空間設計の技術になる。
値を揺らすのではない。
座標を歪ませる。
スカラー場をベクトル場に拡張し、
さらに時間を含める。
その結果、
ノイズは
- 雲になり
- 水になり
- 地形になり
- エネルギー表現になる
数学は抽象ではない。
GPU上で実行される、世界生成アルゴリズムである。
ここから先は応用領域に入る。
ノイズはもう理解した。
あとは“何を作るか”だ。
次回は、
- ノイズで水を作るか
- ノイズで雲を作るか
- あるいは Curl Noise に進むか
理論は終わった。
制作のフェーズに入る。
💬 コメント