[Noise 入門 #13] Voronoi Noiseの実践 — 数式で「岩肌」「ひび割れ」「クリスタル」を錬成する

概要

Noise 入門シリーズ第13回。

前回学んだVoronoi / Worley Noiseの「距離計算」の概念をGLSLで実装し、実際のテクスチャ表現へと応用します。最も近い点までの距離($F_1$)と、2番目近い点までの距離($F_2$)を組み合わせることで、細胞、水面の反射(コースティクス)、ひび割れた岩肌、クリスタルなど「構造を持つ自然物」を数式だけで生成するレシピを解説します。

前回の記事:

1. Voronoi魔法の基礎パラメータ: $F_1$ と $F_2$

Voronoi NoiseをShaderで実装する際、視覚的な出力を決定づけるのは「特徴点(ランダムな点)までの距離」です。具体的には以下の2つの変数が主役になります。

  • $F_1$ (Distance to 1st nearest point): 最も近い特徴点までの距離。
  • $F_2$ (Distance to 2nd nearest point): 2番目に近い特徴点までの距離。

距離 $D$ を求める計算は、ピクセルの座標 $(x, y)$ と特徴点の座標 $(p_x, p_y)$ を用いて以下のユークリッド距離の公式(またはGLSLの組み込み関数 distance())を使用します。

$$D = \sqrt{(x - p_x)^2 + (y - p_y)^2}$$

この $F_1$ と $F_2$ という2つの「距離データ」をどう足し引きするかで、生成される世界が劇的に変化します。

ピクセルから最も近い点を探す(GLSLの考え方)

理論上は、画面上のすべてのランダムな点との距離を計算すれば と は求まります。しかし、GPUで毎フレーム何万ものピクセルに対して全探索を行うのは現実的ではありません。

この「3×3マスの探索」だけで、確実に最も近い点($F_1$)と2番目に近い点($F_2$)を見つけ出すことができます。

  1. 空間を $1.0 \times 1.0$ のグリッドに分割する。
  2. 各グリッドの中に、ランダムな特徴点を1つずつ配置する。
  3. 現在のピクセルがいるグリッドと、その周囲8個のグリッド(合計9マス)の中にある特徴点との距離だけを計算する。

この「3×3マスの探索」だけで、確実に最も近い点()と2番目に近い点()を見つけ出すことができます。

GLSLでの実装例:F1とF2の取得

これをフラグメントシェーダーで表現すると、以下のようなループ処理になります。

// 2D座標から2Dのランダムなベクトルを返すハッシュ関数
vec2 random2(vec2 p) {
    return fract(sin(vec2(dot(p, vec2(127.1, 311.7)), dot(p, vec2(269.5, 183.3)))) * 43758.5453);
}

// Voronoi関数の本体
vec2 voronoi(vec2 uv) {
    vec2 currentGrid = floor(uv); // 現在のグリッドのインデックス(整数)
    vec2 f = fract(uv);           // グリッド内のローカル座標(0.0 ~ 1.0)

    float f1 = 1.0; // 最も近い距離(初期値は十分大きな値)
    float f2 = 1.0; // 2番目に近い距離

    // 周囲3x3のグリッドを探索
    for (int y = -1; y <= 1; y++) {
        for (int x = -1; x <= 1; x++) {
            vec2 neighbor = vec2(float(x), float(y)); // 隣接グリッドの相対位置

            // 隣接グリッド内のランダムな特徴点を取得
            vec2 point = random2(currentGrid + neighbor);

            // アニメーションさせる場合はここで point に時間を足す
            // point = 0.5 + 0.5 * sin(u_time + 6.2831 * point);

            // ピクセルから特徴点までのベクトルと距離を計算
            vec2 diff = neighbor + point - f;
            float dist = length(diff);

            // F1とF2の更新
            if (dist < f1) {
                f2 = f1;   // 今までの1位を2位に降格
                f1 = dist; // 新しい1位を登録
            } else if (dist < f2) {
                f2 = dist; // 新しい2位を登録
            }
        }
    }

    return vec2(f1, f2); // F1とF2を返す
}

この voronoi() 関数が、これからあらゆる物質を錬成するための心臓部になります。手元に と さえ抽出できれば、あとは足し算と引き算の魔法をかけるだけです。

2. 視覚化レシピ①:細胞と水面の揺らぎ(Caustics)

まずは一番シンプルな だけを使った表現です。

仕組み: $1.0 - F_1$

$F_1$ は「点に近いほど値が小さく(黒)、離れるほど値が大きく(白)」なります。これを反転させて $1.0 - F_1$ とすると、「点の中心が一番明るく、境界線に向かって暗くなる」というドット状のグラデーションが生まれます。

応用とエフェクト

  • 細胞(Cellular): そのまま出力すれば、顕微鏡で見た細胞のような模様になります。
  • 水面の反射(Caustics): 特徴点の位置に時間(u_time)を足してアニメーションさせ、閾値(smoothstepなど)でコントラストをパキッとさせると、プールの底でゆらゆら揺れる光の網目(コースティクス)が簡単に生成できます。

GLSLでの実装例:水面コースティクスの錬成

先ほど作成した voronoi(uv) 関数を呼び出し、フラグメントシェーダー内で実際に色(gl_FragColor)に変換してみましょう。

// uv座標を少しスケールしてグリッドを細かくする
vec2 uv = vUv * 5.0;

// voronoi関数から F1 と F2 を取得
// (内部のランダム点生成時に u_time を加算してアニメーションさせておく)
vec2 v = voronoi(uv);
float f1 = v.x;

// 1. 細胞表現(Cellular)
// 単純に反転させるだけで、中心が光るドットになる
float cellular = 1.0 - f1;

// 2. 水面コースティクス(Caustics)
// smoothstepで「明るい部分だけ」を鋭く切り出し、光の集光現象を表現する
float caustics = smoothstep(0.5, 0.8, cellular);

// 水のような青色を乗せて出力
vec3 waterColor = vec3(0.1, 0.6, 0.9) + caustics * 0.5;

gl_FragColor = vec4(waterColor, 1.0);

解説: smoothstep(0.5, 0.8, cellular) が魔法のスパイスです。

$1.0 - F_1$ の値が 0.5 以下の暗い部分は完全に切り捨てて真っ黒にし、0.8 以上の明るい部分だけを白く際立たせることで、光が水底に集束しているような「鋭利で有機的な光の網目」が生まれます。

第11回で解説した「Procedural Water」の水面レイヤーの下にこのコースティクスを合成すれば、圧倒的な説得力を持つ水景ができあがります。

水面の表現だけでも強力ですが、Voronoiの本当の恐ろしさは「境界線」を抽出したときに現れます。

3. 視覚化レシピ②:ひび割れた大地とマグマ(Cracks)

Voronoiの真骨頂はここからです。「境界線」だけを抽出して、鋭いエッジを作り出します。

仕組み:$F_2 - F_1$

「1番近い点」と「2番目に近い点」の距離の差分を取ります。もしあるピクセルが、2つの特徴点のちょうど中間(境界線上)にあった場合、 $F_1 \approx F_2$ となるため、その差はほぼ 0.0(黒)になります。

応用とエフェクト

  • ひび割れ(Cracked Earth): $F_2 - F_1$ を出力すると、細胞の境界線だけが黒く鋭い線として浮かび上がります。干ばつで割れた土や、岩肌のクレバスの表現に最適です。
  • マグマ(Magma): この黒いひび割れ部分にだけ「赤〜オレンジ」の発光色を乗せ(Emission)、それ以外の部分を暗い岩肌の色にすれば、燃えたぎる溶岩の地表が完成します。

GLSLでの実装例:ひび割れとマグマの錬成

実際に $F_2 - F_1$ を計算し、岩肌の間からマグマが覗くようなテクスチャを作ってみましょう。

// uv座標をスケール
vec2 uv = vUv * 4.0;

// voronoi関数から F1 と F2 を取得
vec2 v = voronoi(uv);
float f1 = v.x;
float f2 = v.y;

// 1. ひび割れの抽出(Cracks)
// F2 - F1 を計算。境界線上(2つの点から等距離の場所)が 0.0(黒)になる
float cracks = f2 - f1;

// 2. エッジの調整
// smoothstepを使って線を細く鋭くする
// 0.0 ~ 0.1 の狭い範囲だけグラデーションにし、それ以上は1.0(白)で塗りつぶす
cracks = smoothstep(0.0, 0.1, cracks);

// 3. マグマと岩肌の着色
// 燃えるようなマグマのオレンジと、冷えた暗い岩の色を定義
vec3 magmaColor = vec3(1.0, 0.3, 0.0);
vec3 rockColor = vec3(0.15, 0.12, 0.12);

// ひび割れ(cracksが0.0)の部分はマグマ、それ以外(1.0)は岩肌になるように合成
vec3 finalColor = mix(magmaColor, rockColor, cracks);

// マグマ部分を少し発光させるための小技(加算)
finalColor += magmaColor * (1.0 - cracks) * 0.5;

gl_FragColor = vec4(finalColor, 1.0);

解説: ここでのポイントは mix(magmaColor, rockColor, cracks) です。 cracks の値は、ひび割れの中心(境界線)で 0.0 になり、そこから少し離れるとすぐに 1.0 になります。これを mix 関数の補間係数として使うことで、境界線の部分にだけ綺麗に magmaColor が入り込みます。

第10回で作成した「Procedural Terrain(地形生成)」のハイトマップにこのひび割れテクスチャを組み合わせれば、火山バイオームの表現が一気に完成します。

4. 視覚化レシピ③:クリスタルとステンドグラス(Facets)

距離だけでなく、「どの特徴点に属しているか」というID(固有情報)を使うと、面(ポリゴン)のような表現が可能になります。

仕組み:Cell IDによる着色

グリッドごとのランダム値を計算する際、距離のグラデーションではなく、「そのセルが持つ固有のランダム値(フラットな色)」をそのままピクセルに適用します。

応用とエフェクト

  • ステンドグラス: セルごとにランダムな色(赤、青、緑など)を割り当て、境界線($F_2 - F_1$)を黒く縁取るとステンドグラスになります。
  • クリスタル(宝石): をハイトマップ(高さ)として扱い、そこから法線(Normal)を計算して擬似的なライティングを当てると、面が乱反射する美しい鉱石やクリスタルの質感が生まれます。

GLSLでの実装例:ステンドグラスの錬成

この表現を行うには、先ほどの voronoi 関数を少しだけ拡張し、「最も近い特徴点の固有ID(座標など)」も一緒に返すようにしておく必要があります。ここでは、その固有ID(cell_id)が取得できた前提で色を作っていきます。

// uv座標をスケール
vec2 uv = vUv * 5.0;

// 拡張版voronoi関数から F1, F2, そして Cell ID を取得したと仮定
// v.x = F1, v.y = F2, v.z = cell_id
vec3 v = voronoi_with_id(uv);
float f1 = v.x;
float f2 = v.y;
float cell_id = v.z;

// 1. セルごとのランダムカラー生成(ステンドグラスのガラス)
// cell_idをシード値として、セル領域全体にフラットな色を割り当てる
vec3 cellColor = vec3(
    fract(sin(cell_id * 12.9898) * 43758.5453),
    fract(sin(cell_id * 78.233) * 43758.5453),
    fract(sin(cell_id * 39.346) * 43758.5453)
);

// 2. 黒い境界線(F2 - F1)の抽出
// 視覚化レシピ②の「ひび割れ」を応用して、ステンドグラスの「鉛桟(なまりざん)」を作る
float border = f2 - f1;
border = smoothstep(0.0, 0.05, border); // 0.05以下の細い範囲だけを黒くする

// 3. 合成
// 境界線(borderが0.0)は真っ黒になり、それ以外(1.0)はセルの色になる
vec3 finalColor = cellColor * border;

gl_FragColor = vec4(finalColor, 1.0);

解説: ここでの最大のポイントは、「同じセル内にいるピクセルは、すべて同じ cell_id を共有している」という事実です。 グラデーションを持っていた とは異なり、cell_id を使って色を生成すると、ピクセルごとに色が変化せず「面(Facet)」としてフラットに塗りつぶされます。

Three.jsの ShaderMaterial でこのテクスチャをBoxやSphereに貼り付け、少しだけカメラを動かして環境光を反射させると、それだけで非常に見栄えの良いクリスタル表現になります。短い動画映えするエフェクトを作る際にも、この「面を割る」テクニックは重宝します。

5. 神領域への第一歩:Voronoi × Domain Warping

「滑らかさ」のPerlin Noiseと、「構造的」なVoronoi Noise。これらは対立するものではなく、融合させることができます。

Voronoi関数に渡す入力座標(UV)に対して、事前にFBM(Fractal Brownian Motion)でノイズを加えて空間を歪ませます(Domain Warping)。 すると、直線的で幾何学的だったVoronoiの境界線がグニャグニャと歪み、「エイリアンの皮膚」「ドラゴンの鱗」「有機的な魔界の地形」のような、極めて複雑で自然なディテールへと進化します。

「滑らかさ(Gradient)」と「構造(Distance)」の2つの武器が揃ったことで、表現の幅は無限大に広がりました。

GLSLでの実装例:空間を歪めた有機的Voronoiの錬成

第7回で実装した fbm() 関数を使ってUV座標を歪曲させ、その結果を voronoi() に流し込みます。

// uv座標をスケール
vec2 uv = vUv * 6.0;

// 1. FBMによる空間の歪曲(Domain Warping)
// uv座標のXとYそれぞれに異なるFBMノイズを適用して「ズレ」を作る
vec2 warp;
warp.x = fbm(uv + vec2(0.0, 0.0) + u_time * 0.1);
warp.y = fbm(uv + vec2(5.2, 1.3) + u_time * 0.1);

// 歪みの強さを調整(この値を大きくするほどドロドロに溶ける)
float warpStrength = 1.5;

// 歪んだ新しいUV座標を生成
vec2 warpedUV = uv + warp * warpStrength;

// 2. 歪んだUVを使ってVoronoiを計算
// ここではひび割れ(F2 - F1)のロジックを使用
vec2 v = voronoi(warpedUV);
float f1 = v.x;
float f2 = v.y;

// 有機的なひび割れ(エイリアンの皮膚や魔界の地面など)
float organicCracks = f2 - f1;
organicCracks = smoothstep(0.0, 0.08, organicCracks);

// 暗い緑色の生々しいトーンで着色
vec3 skinColor = vec3(0.1, 0.3, 0.1);
vec3 crackColor = vec3(0.0, 0.1, 0.0);
vec3 finalColor = mix(crackColor, skinColor, organicCracks);

gl_FragColor = vec4(finalColor, 1.0);

解説: 数式の流れは非常にシンプルですが、視覚的な効果は絶大です。 本来、Voronoiの境界線は「直線的でカクカクしたポリゴン」になりがちですが、事前に入力座標(warpedUV)をうねらせておくことで、境界線が自然界の細胞膜や葉脈のように柔らかく歪みます。

このテクニックは、Three.jsなどの3Dシーンに組み込んでパラメーターを動的に変化させると、非常に不気味で美しい有機的なアニメーションを生み出します。視覚的なインパクトが強いため、実装結果をショート動画として切り出すのにも最高の題材になります。


次回予告

「構造的な自然」をマスターし、これでPerlin NoiseとVoronoi Noiseという2大巨頭を完全に手中に収めました。

しかし、ノイズの旅路はまだ終わりません。次回は再び「アルゴリズムの進化」に目を向けます。 Perlin Noiseが抱えていた「グリッドのアーティファクト(カクつき)」や「高次元(3D, 4D)での計算コストの重さ」を劇的に解消した天才的なアルゴリズム、『Simplex Noise(シンプレックス・ノイズ)』の仕組みに迫ります。