3d-underwater-god-rays について
水中シーンを作る開発者向けの、再利用可能なThree.jsシェーダーモジュールです。うねりとコースティクスを視線に沿って積分し、カメラ移動時に光条がシーン内の物体に対して動きます。
主な機能
- 01Three.js向け水中光条シェーダー
- 02うねりと太陽方向に沿うワールド空間の光条
- 03コースティクスの筋を統合
- 04半解像度のHalfFloatレイマーチ
- 05動きの軽減とページ表示状態への対応
活用シーン
- 01Three.jsの水中シーンに太陽光条を追加
- 02カメラ移動に応じて魚に対する光条の位置を変化
- 03デモの操作で光条の見た目を調整
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ mengto/ skills --skill 3d-underwater-god-raysインストール後、Claude Code で/3d-underwater-god-raysを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
agents/openai.yamlassets/underwater-light.mjsdemo/build.mjsPROMPT.mddemo/src/ui.cssui.htmlui.jswater.js詳細
3D 水中ゴッドレイ
カメラが水中にあり、光が水面を通って差し込む場合に使います。次の場合は、代わりに関連する別の手法を使ってください。
- 空気中の太陽光線が木々や屋根の輪郭で遮られる場合:
3d-sky-raysを使います。遮蔽物マスクに対するスクリーンスペースの放射状ブラーが適しています。視差や波紋には対応できないため、水中では魚の影を補う控えめな効果としてのみ使ってください(Blue Hour では 0.25 で実行しています)。 - 水平線のある、上空から見た海面:
3d-ultra-realistic-waterを使います。 - 照明された室内の窓から差し込む、作り込まれた光線の平面:
3d-wood-lighting-scorecardを使います。
出典:Blue Hour から抽出しました。これは、Three.js を使った iPhone Duo の研究で制作された、海が息づく壁紙です。2,200 匹の Blender 製イワシの群れが、ガラス製のデバイスの背後を泳ぎます。カメラが 38 秒かけてドリー移動する間、またデバイス自体の画面上でも、水中に差し込む太陽光として光を表現する必要がありました。
検証済みの構成:Three.js r170、WebGL2、GLSL の ShaderMaterial、HalfFloat レンダーターゲット 1 つ、手書きの ACES パス。ポストプロセッシングライブラリは使っていません。
仕組み
太陽の方向に沿って、漂ううねりのフィールドを 1 つサンプリングし、すべての視線に沿って積分します。 水中の各点は、太陽に垂直な方向から測った位置でのうねりの値によって照らされます。そのため、光線は世界空間で平行な柱になります。消失点に向かう線路のように太陽へ向かって収束し、カメラが動くと魚のそばを通り過ぎていきます。同じ光線をスクリーンスペースに描くと、レンズに固定されたままになります。
動作するエフェクトを再利用する
assets/underwater-light.mjs をコピーします。独自の THREE を渡すと、このモジュールは別のコピーをインポートしません。デモ は、Blue Hour のレンダラー内で同じモジュールを実行します。demo/build.mjs は Blue Hour のソースからこれを再構築します。
import { createUnderwaterLight, LIGHT_GLSL, WATER_GLSL } from './underwater-light.mjs';
// One uniforms object shared by the water, fish and particle materials.
const uniforms = {
uSunDirection: { value: new THREE.Vector3(.25, .40, -1).normalize() }, // toward the sun
uTime: { value: 0 },
uCameraPosition: { value: camera.position },
uCameraWorld: { value: camera.matrixWorld },
uInverseProjection: { value: camera.projectionMatrixInverse },
};
const light = createUnderwaterLight(THREE, { uniforms }); // before creating the materials below
light.setSize(innerWidth * dpr, innerHeight * dpr); // drawing-buffer pixels, again on resize
// Water: a full-screen quad. Declare uSunDirection, uCameraPosition, uTime,
// uShafts and uShaftTexel, then add ${LIGHT_GLSL}${WATER_GLSL} and in main():
// vec3 sunRay = ray;
// color += surfaceUnderside(ray, sunRay); // also bends sunRay through the ripples
// ...draw your sun with sunRay...
// color += underwaterShafts(vUv, ray);
// Fish: declare uSunDirection, then add ${LIGHT_GLSL} and multiply the sun key
// by underwaterDapple(worldPosition, uTime).
// Every frame, in this order:
uniforms.uTime.value = time;
camera.updateMatrixWorld();
light.render(renderer); // the march, into its own half-resolution target
// then the water, the fish, and one tone map at the end
実際に採用した数値
| レイヤー | 定数 |
|---|---|
| うねり | (.86, .51)、(−.42, .91)、(.13, −.99)、(−.97, −.24) 方向の 4 つの正弦波を ×1、1.23、0.87、1.41 で使用。ドリフトは 0.29、0.24、0.19、0.15 rad/s。ドメインワープは 0.62 |
| 太陽を横切る方向 | スケール 1.3。群れの位置で、幅約 22 ワールド単位のフレームに対し、主周期は約 4.8 ワールド単位 |
| 光線の焦点 | smoothstep(−0.05, 1, swell)²。太陽に沿った水面までの距離に応じて exp(−0.012 · distance to the surface along the sun) で減衰 |
| コースティクスの網目 | うねりを ×2.6、速度を 1.35 倍にして再利用し、`(1 − |
| マーチ | 26 ステップ、t = 4 + 50·f²、重みは exp(−0.06t)·(0.08 + f)。重みの合計で割ります。時間的ノイズの代わりに、ピクセルごとに安定したジッター(インターリーブグラディエントノイズ)を使用 |
| 太陽とのブレンド | total = mix(total, 0.16, μ^28)。μ = ray·sun |
| 位相 | 0.7 + 1.1μ^5 + 0.9μ^40。地平線より下の光線には mix(0.45, 1, smoothstep(−0.45, 0.35, ray.y)) を乗算 |
| ゲイン | (0.16, 0.40, 0.47) × 3.6。リニア HDR で、最後に一度だけトーンマッピング |
| バッファー | 描画バッファーの 0.5 倍、HalfFloat、シングルサンプル(マーチはソフトなため、MSAA を使っても効果はありません)。½ texel の位置で 4 タップのテントフィルターを使ってアップサンプリング |
| 水面の裏側 | y = 12 の平面。ネットワークは `(1 − |
| 魚 | 太陽キーライト × 0.62 + 1.5 · shaftLight |
| フレームのその他の部分 | 太陽コロナを半分にして (0.043, 0.095, 0.123)·μ^48。スクリーンスペースの光線パスを 0.82 から 0.25 に低減 |
ルールと、それによって防げる失敗
これらは、Blue Hour のフレームを A/B レンダリングして得られたものです。同じカメラ、同じ静止した魚で、一度に変えるのは 1 つだけです。
- うねりは尾根ではなく、まばらなピークにします。
1 − |swell|は薄いシートの網を作り、複数のシートを横切るレイはそれらを平均化して平坦なかすみになります。静止フレームでは、尾根の網によって水面の平均輝度が 20% 上がり、ピクセル列間のコントラストが 0.247 から 0.118 に半減しました。デモの Ridge web スイッチで確認できます。 - マーチングは 4 ユニット先から始めます。 1.5 ユニットから始めると、カメラの上を通る光柱がフレームの 4 分の 1 を 1 つの明るいくさびで埋めます。サンプルに
exp(−0.06t)の重みを付けます。近くのサンプルほどぼけが少ないため、ビームの縁が保たれます。 - レイが太陽を向くとき、光柱を太陽のグローに溶け込ませます。 1 列を真上に見上げる場合、積分値は 1 つのうねり値になります。カメラが光柱の間にある場所では、その結果、太陽に暗い穴ができました。
mix(total, 0.16, μ^28)でその穴を埋めます。 - コースティクスの網はマーチングの内側で積分します。 フル解像度で 4 回サンプリングすると、網は水面に塗り重ねられたプールの底のように見えます。ハーフ解像度の 26 ステップのマーチング内で処理すると、各ビームに沿って走る細い筋になります。デモの Painted on screen スイッチで確認できます。
- フレームにすでにある均一なグローは弱めます。 Blue Hour のコロナとスクリーンスペースのレイパスは、フレーム上部を均等に照らしていました。後に採用されたゲインに近い値では、合成後に最初の光柱はほとんど見えませんでした。コロナを半分にし、放射状パスを 0.82 から 0.25 に下げて初めて光柱が見えるようになりました。光を担うのは光柱で、隙間は暗いままです。
- ゲインは生のキャンバスではなく、合成後のページで判断します。 ゲイン 2.2 はキャンバス上では適切に見えました。しかしページでは、キャンバスは暗くする CSS シェードの下で不透明度 82% になっており、同じフレームはほとんど見えませんでした。3.6 ならページ上でも保たれます。
- フレーム全体にビームを広げるには、太陽を上端より上に置きます。 21° では太陽がフレーム内にあり、収束点はグローになってビームは短くなります。30° から 44° の間では上端より上に抜け、ビームがフレーム全体に広がります。デモの Sun スライダーで動かせます。
- 各シェーダーで各 uniform を 1 回だけ宣言します。
LIGHT_GLSLは独自の uniform(uSurfaceY、uShaftScale、uShaftFloor、uCompare)を宣言し、ホスト側でその前にuSunDirectionを宣言することを想定しています。同じものを 2 回宣言するとコンパイルエラーになり、マテリアルは何も描画しなくなります。
コスト
マシンに高い負荷がかかっている状態(ロードアベレージ約 60)で、ANGLE Metal 上のヘッドレス Chrome は、Blue Hour のフレーム全体を 2048×1440 で 6〜10 ms でレンダリングしました。このフレームには、2,200 匹のインスタンス化されたイワシ、水面、1024×720 のマーチング、スクリーンスペースのレイパス、トーンマップが含まれます。5 回の交互ラウンドで各 20 フレームを計測したところ、マーチングを省略した場合の中央値の変化は実行ごとのノイズ(約 ±2 ms)より小さく、ここではボトルネックではありません。プロファイルで負荷が目立つ場合は、バッファースケールかステップ数を下げてください。光柱は低周波なので、どちらを下げても主に柔らかさが損なわれますが、頼りにする前に計測してください。
ライフサイクルとアクセシビリティ
- シミュレーションは 30 fps で進め、各ステップを 0.1 s に制限します。Blue Hour は水槽を固定サイズの 2048×1440 キャンバス(800 px 未満では 1600×1125)にレンダリングしてカバー表示するため、マーチングのコストは画面サイズに応じて増えません。
prefers-reduced-motion: reduceの場合はフレームを t = 0 で固定し、コントロールが変更されたときだけ再レンダリングします。ジッターはピクセルごとに安定しているため、静止フレームはちらつきません。document.hiddenの間は停止し、visibilitychange時にフレームクロックをリセットします。これにより、復帰後の最初のフレームで停止中の時間全体を積分することがありません。
確認
- カメラを回転させます。光の筋が魚に対して移動し、太陽に向かって収束します。レンズと一緒に動く場合は、スクリーン空間に描画されています。
- Ridge web をオンにすると、水面がかすみます。スパースピークスに戻すと、水面の暗い部分の間に、はっきりとした光線が見えます。
- Painted on screen をオンにすると、プールの底面のウェブが水面を覆います。光の筋の中では、細い筋が各光線に沿って走ります。
- 太陽を正面から見ても、暗い穴はありません。
- 太陽の角度を21°から44°まで動かします。収束点が上端の外へ移動し、光線が画面全体に長く伸びます。
- 魚が光線を横切ると明るくなり、通り過ぎると再び暗くなります。
- モーションの低減を有効にすると、構図を整えた静止画が保たれます。コントロールを操作すると引き続き再描画され、1440×900および390×844でコンソールにエラーはありません。