[Next.js #20] GLSLドメイン・ワーピング:外部ライブラリ不要の発光と流体表現

1. はじめに:21回のノイズ連載を、ついに R3F で具現化する

これまで、「lainのAIと創作ブログ」 では、21回にわたる連載を通じて「ノイズ」という数学的な混沌をいかに制御し、表現に変えるかを追求してきました。

平面のコード上で数式と格闘し、テクスチャや模様を生成してきたあの日々は、すべてこの瞬間のためにあったと言っても過言ではありません。

今回、その集大成として挑むのは、Next.js と React Three Fiber (R3F) を舞台にした、三次元空間での「生命体」の構築です。

「数式」から「実体」へ

これまでの連載で扱ってきたノイズは、主に2Dのピクセルを制御するための「模様」でした。しかし、R3Fという強力なレンダリング環境を手に入れた今、ノイズは単なる模様を超え、ジオメトリ(形状)を直接削り出す彫刻のノミへと進化します。

今回のプロジェクト 「Chroma Flow」 では、以下の要素を高度に融合させています。

  • 空間の歪み: 21回の連載で培ったノイズの知見を 3D の頂点シェーダーへ移植。
  • 動的な流体表現: 座標そのものをノイズで歪ませる「ドメイン・ワーピング」の採用。
  • 数学的な彩色: 数式で色を定義する Cosine Palettes による虹色のスペクトル。

なぜ今、R3F なのか

前回の記事(#19)では影の表現(Shadow Map)と Leva によるパラメーター制御を学びました。今回はその応用編として、外部の Bloom ライブラリなどに頼り切るのではなく、「シェーダーコードそのもので視覚的なインパクトを完結させる」 という、より職人的なアプローチに踏み込みます。

一見すると複雑な生命体のように見えるこの物体も、分解してみれば私たちが連載で学んできた単純な数式の積み重ねに過ぎません。しかし、その「積み重ね」が生み出すインパクトは、時に作り手である私たちの想像をも超えていきます。

前回の記事:

動画

動画(Youtube):

動画(PC):

2. Domain Warping の数理:座標を座標で歪ませる「流体表現」の仕組み

第20回の技術的ハイライトであり、今回の生命体に「得体の知れない流動感」を与えている正体が、この ドメイン・ワーピング(Domain Warping) です。

通常、ノイズを用いて形状を変化させる場合、ある地点 $p$ に対して $noise(p)$ という値を計算し、それを高さとして利用します。しかし、これだけでは「等高線のような規則的なデコボコ」に留まってしまいます。

空間そのものを「ノイズ」で歪ませる

ドメイン・ワーピングの考え方は、「ノイズを計算する前に、入力する座標 $p$ そのものを別のノイズで歪ませてしまう」 というものです。

数式で表すと以下のようになります。

$$f(p + g(p))$$

ここで、$g(p)$ は座標 $p$ を引数に取る別のノイズ関数です。この数式が意味するのは、「本来そこにあるはずの空間が、別のノイズの力によって別の場所へ引きずり込まれている」 という状態です。

今回の実装:3段階のネスト構造

今回の「Chroma Flow」では、このワーピングをさらに多層化し、3段階の入れ子構造にしています。

  1. 第一段階 ($q$): 基礎となる空間を Simplex Noise で緩やかに歪ませる。
  2. 第二段階 ($r$): 歪んだ空間 ($q$) に対して、さらに別の方向からノイズを加えて「かき混ぜる」。
  3. 最終段階: 限界まで複雑に絡み合った座標系を用いて、最終的な形状(Displacement)を決定する。

この「歪みの連鎖」によって、静止したジオメトリの上で、液体がうねり、煙が立ち昇るような、有機的で予測不可能な視覚効果が生まれます。

なぜこれが「流体」に見えるのか

自然界の煙やマーブル模様は、一箇所に留まろうとする力と、外から加わる不規則な力が複雑に干渉し合うことで生まれます。ドメイン・ワーピングは、物理演算(流体シミュレーション)を一切行わず、「数式のみ」でその複雑な干渉結果をシミュレートしている のです。

「単純な計算の積み重ねが、高度な物理現象を模倣する」。これこそが、私たちが21回の連載で追い求めてきた「数式の魔力」の正体です。

3. シェーダー実装の解剖:彫刻としての Vertex と、光を操る Fragment

ここでは、先ほどのドメイン・ワーピングの理論を、どのように GLSL コードとして落とし込んでいるかを解説します。今回のコードの面白さは、「GPU上での彫刻」と「計算による光のエミュレーション」の 2 段構えにあります。

Vertex Shader:ノイズによる「彫刻」とマウス追従

頂点シェーダーの役割は、ただの球体を「未知の生命体」へと変形させることです。

  • ドメイン・ワーピングの実装: コード内の qr という変数に注目してください。snoise(Simplex Noise)を入れ子状に計算し、座標 $p$ を多段階で歪ませています。これにより、トゲの先端が波打ち、生き物のような「粘り気」のある形状が生まれます。
  • マウスとの対話: uMouse と頂点座標の距離を length(adjustedUv - uMouse) で計算し、その近傍の displacement(変位量)を底上げしています。これにより、カーソルを近づけるとスライムが敏感に反応し、そこだけが大きく隆起するインタラクティブ性が宿ります。
  • 法線方向への移動: 計算された warpedNoise を、頂点の法線ベクトル(normal)に掛け合わせて位置をずらしています。これは、球体を内側から「数式の力」で押し広げているような状態です。

Fragment Shader:ライブラリ不要の「自作 Bloom」計算式

通常、発光エフェクト(Bloom)を実現するにはポストプロセスライブラリを使いますが、今回はあえてフラグメントシェーダー内での「色の加算」で表現しています。

  • スペクトル・アニメーション: palette 関数を用い、時間 uTime に応じて虹色に変化する色彩を定義しています。
  • 光の密度を作る pow 関数: ここが最大のポイントです。 finalColor += pow(brightness, 1.5) * 0.4; 明るさの値を累乗(pow)することで、一定以上の明るさを持つ部分だけを爆発的に白く飛ばしています。ライブラリを使わずとも、この一行で「眩しくて直視できない」ようなエネルギー感を生み出すことができます。
  • タイリングによるディテール: vUv * 14.0 で UV 座標を分割し、球体表面に細かな「細胞状の光」を敷き詰めています。これがドメイン・ワーピングの大きなうねりと重なり合うことで、情報の密度が跳ね上がります。

外部ライブラリのバージョンに振り回されず、たった数十行の数式だけでこれだけの密度を表現できる。これこそが、シェーダーという「最小の道具で最大の結果を出す」技術の真髄です。 す。

4. R3F 実装のポイント:useMemo による Uniforms の固定とパフォーマンス最適化

シェーダーコードが「魂」だとしたら、それを React という現代のフレームワークの上で正しく、かつ効率的に動かすための「器」が React Three Fiber (R3F) です。

今回の実装では、R3F 初学者が必ずと言っていいほど直面する「再レンダリングの壁」を乗り越えるための工夫を凝らしています。


「再レンダリング」でアニメーションが止まる理由

Leva のスライダーを動かした際、コンポーネント全体が再レンダリングされます。このとき、<shaderMaterial>uniforms プロパティに生のオブジェクトを直接渡していると、再レンダリングのたびに「新しいオブジェクト」が作成されてしまいます。

R3F はそれを検知し、シェーダーの値を初期値(uTime: 0 など)にリセットしてしまうため、アニメーションがカクついたり、止まって見えたりする現象が起きます。

useMemo による参照の固定

この問題を解決するのが useMemo です。

const uniforms = useMemo(() => ({
  uTime: { value: 0 },
  uDistortion: { value: 0.2 },
  uFrequency: { value: 2.0 },
  uGlow: { value: 0.02 },
  uMouse: { value: new THREE.Vector2(0, 0) },
}), []);

このように Uniforms オブジェクトをメモ化することで、再レンダリングが走っても同じメモリ空間(参照)を使い続けることができます。これにより、Leva でパラメーターをいじりながら、リアルタイムに「スライム」の表情が滑らかに変化する様子を観察できるようになります。

useFrame と delta:時間の連続性を守る

時間の更新には、state.clock.getElapsedTime() ではなく、前フレームからの差分である delta を累積させる方式を採用しました。

useFrame((state, delta) => {
  if (materialRef.current) {
    // 累積させることでスピード変更時も時間がジャンプしない
    materialRef.current.uniforms.uTime.value += delta * speed;
  }
});

delta を使うことで、Leva で speed を動的に変更しても、アニメーションが途切れることなくスムーズに加速・減速します。また、PC のスペックによってフレームレートが変動しても、一定の速度で動き続ける「フレームレート独立」な実装にもなっています。

R3F ツリー特有のエラー:「Text is not allowed」

開発中に遭遇しやすいのが、<mesh> タグの間にコメントや改行を入れた際に発生する Text is not allowed in the R3F tree! というエラーです。

R3F のレンダリングツリーは非常に厳密で、タグの直下に文字列(テキストノード)が混入することを許しません。コードを綺麗に保とうとして入れた不用意な改行やコメントアウトが、時として描画をクラッシュさせる原因になる、という学びもこの記事に記しておきます。

5. おわりに: 単純な要素の掛け算が「理解を超えるインパクト」を生む。

21回のノイズ連載、そしてこの R3F シリーズを通じて私たちが目撃したのは、個々には単純な数式が積み重なり、掛け合わされることで生まれる「創発」の瞬間でした。

完成した 「Chroma Flow」 は、もはや単なるプログラムの断片ではなく、意志を持ってのた打ち回る未知の生命体や、宇宙の深淵で輝く超新星のような圧倒的な実在感を放っています。


「手品」の種を明かせば単純である

この物体を構成している要素を一つずつ分解すれば、私たちがこれまで学んできた基礎的なパーツばかりです。

  • ノイズ: 規則性を壊し、有機的なゆらぎを与える道具。
  • ドメイン・ワーピング: 座標を歪ませ、流体のような動きを作るテクニック。
  • サイン波と時間: 静止した数式に、呼吸という命を吹き込む拍動。
  • 指数関数: 光を物理的な「輝き」へと昇華させる魔法。

しかし、これらが R3F という一つの器の中で同期し、マウスという観測者の干渉を受けることで、仕組みを知らない人にとっては「理解不能なインパクト」へと変貌します。

「lainのAIと創作ブログ」のこれから

このブログ 「lainのAIと創作ブログ」 では、これからも「技術的な理屈」と「創作的な衝動」が交差する場所でありたいと考えています。

今回磨き上げたこの「デジタルの泥団子」は、21回の連載という長い旅路の終着点であると同時に、さらに高度な表現——例えば音との同期や、WebGPU を駆使したさらなる高精細なシミュレーション——へと向かうための新たな出発点でもあります。

「なぜこれを作るのか?」という問いに、「やりたいからやっている」 以上の答えは必要ありません。

数式という冷徹なロジックから、誰かの心を動かす「熱量」が生まれる瞬間を、これからも一緒に追い求めていきましょう。