3d-ultra-realistic-water について
移動カメラ、地平線、太陽のある外洋シーン向けのThree.js水面描画スキルです。再利用可能な海洋モジュールと、波、反射、泡、航跡、性能に関する実装指針を提供します。
主な機能
- 01解析的なGerstner波シェーディング
- 02距離に応じた波のディテール低減
- 03空と平面の反射
- 04泡と船の航跡エフェクト
- 05波スペクトルに基づく高さ取得
- 06モーション低減への対応
活用シーン
- 01地平線まで続く外洋の描画
- 02Three.jsシーンへの波と反射の追加
- 03遠方の海面ディテールの安定化
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ mengto/ skills --skill 3d-ultra-realistic-waterインストール後、Claude Code で/3d-ultra-realistic-waterを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
agents/openai.yamlassets/ocean.mjsdemo/build.mjsPROMPT.mddemo/src/ui.cssui.htmlui.jsreferences/ocean.md詳細
3D 超リアルな水面
水平線と太陽が見える、移動するカメラから見た外洋に使用します。次の場合は別の方法をお選びください。
- **水面近くから見た、メッシュのない鏡のように平らな水面:**全画面の解析的な水面なら、より低コストです。
- UI サーフェス上でカーソルに追従する波紋:
shaders-cursor-ripplesを使用してください。 - **タッチに反応する小さなプール:**ピンポン方式の高さフィールドを使用してください。
ソース:Pirate Ship Sunsetから抽出したものです。これは、夕暮れの海を進む3本マストの船を、自動でカメラを切り替えながら描画する、単一ファイルのThree.js r169シーンです。水面は、波の上2 mから12 km先の水平線まで、リアルに見える必要がありました。
仕組み
**深水のGerstner波の合計を解析的に微分し、その結果に基づいてピクセルごとに海面をシェーディングし、波長がピクセルフットプリントを下回ると各波をフェードアウトさせます。**ピクセルごとの微分により、水平線では三角形の幅が30 mあるメッシュでも、きらめきと泡を鮮明に保てます。フットプリントフェードによって、遠くの海面にモアレが発生するのを防ぎます。
動作するエフェクトを再利用する
assets/ocean.mjsをコピーしてください。プロジェクト独自のTHREEを渡すため、このモジュールが2つ目のコピーをインポートすることはありません。組み込む前にreferences/ocean.mdを読み、すべての定数、レンダリング順序、浮力、ポスト処理を確認してください。デモでは、元のシーン内で同じモジュールを実行します。demo/build.mjsは、参照ページからデモを再構築します。
import { createWaves, bakeOceanDetail, createOcean, createWake, createPlanarReflection } from './ocean.mjs';
const waves = createWaves({ direction: 1.22 }); // wind heading, radians
const ocean = createOcean(THREE, {
waves,
detailTexture: bakeOceanDetail(THREE, renderer), // baked once, 512²
skyCube, // cube rendered from YOUR sky shader
hazeGLSL: SKY_GLSL + 'vec3 oceanHaze(vec3 d){ return hazeColor(d); }',
hazeUniforms: skyUniforms, // may include uSunDir
wake: createWake(THREE, renderer, { shipLength: 34 }), // optional
hull: { halfWidths, zMin: -18, zMax: 18, bowZ }, // optional, ship-local metres
});
scene.add(ocean.mesh);
const reflection = createPlanarReflection(THREE, renderer, { layer: 1 });
ocean.setReflection(reflection);
// every frame, in this order
waves.update(dt); // CPU phases, before any height() query
ship.y = waves.height(ship.x, ship.z); // buoyancy uses the same spectrum
ocean.update({ time, camera, ship: { x, z, fx, fz } });
wake.update(x, z, fx, fz, speed * dt);
reflection.render(scene, camera); // before the main pass
renderer.render(scene, camera); // into a HalfFloat target, then bloom + ACES
実装時のスタックと数値
| レイヤー | 機能 | 定数 |
|---|---|---|
| スペクトル | 8つのGerstner波、深水分散関係ω = √(g·k) | L 78 → 3.1 m(各ステップで×0.63)、A 0.8 → 0.02 m、風向からの方位角0、+23、−17、+38、−31、+12、−49、+64°、Q 0.17 → 0.03(Σ 0.77) |
| グリッド | カメラの直下を中心とした指数リング | r = 11.92·(e^(0.033·i) − 1)、210リング × 400セグメント = 84,001頂点、167,600三角形、約12.2 kmまで到達、間隔0.033·(r + 12) |
| フットプリントフェード | 両方のシェーダーで波ごとに適用 | fade = 1 − smoothstep(0.18·L, 0.5·L, footprint)、頂点フットプリントは1.2 × ring spacing、ピクセルフットプリントは2 × (fwidth(x) + fwidth(z)) |
| 法線 | ピクセルごとにnormalize(cross(dP/dz, dP/dx)) | ヤコビアンJ = dPdx.x·dPdz.z − dPdx.z·dPdz.xで波頭の泡を制御 |
| 微細ディテール | 整数の波数ベクトルを持つ48個の正弦波を焼き込んだ512²タイル、振幅は∝ |k|^−1.35 | 19 mと7.3 mの2オクターブ(2つ目は90°回転)、強度は0.4·e^(−d/500) + 0.08 |
| 反射 | 水面のF0 = 0.02としたSchlick近似、256²のスカイキューブを0.25秒ごとに再レンダリング、船と島には半解像度の平面パス | 平面UVをN.xz · (0.06, 0.03) · clamp(30/d, 0.2, 1)でオフセット |
| 水面の色 | 深い色と、波頭を通る太陽光による表面下散乱 | 深部は線形空間で(0.006, 0.03, 0.048)、散乱は(0.035, 0.3, 0.27)、散乱 × 太陽方向成分² × 波頭の高さ × (0.35 + 4·(1 − N.y)) |
| きらめき | HDRスペキュラー。ブルームで捉えられるようにします | pow(R·L, 90)·1.2 + pow(R·L, 12)·0.06 + pow(R·L, 1400)·40、太陽の色は(4.2, 2.3, 1.1)、ブルームのしきい値は1.6、ニーは0.8 |
| 泡 | マスクでWorley格子をしきい値処理し、泡をレース状に保ちます | 波頭はsmoothstep(0.6, 0.28, J)、foam = smoothstep(1 − m, 1.2 − m, pattern·0.88 + m·0.22)、1600 mまでに消失 |
| 引き波 | 船の後方に追従する、380 m範囲の512²半精度浮動小数点フィールド | Kelvin波の腕のオフセットは1.2 + 0.35·a(atan 0.35 ≈ 19.3°)、腕の減衰距離は115 m、船尾の航跡は170 m、穏やかな滑走跡は260 m |
| 霞 | 空の水平線と同じ色に溶け込むように霧をかけます | fog = (1 − e^(−d·2.8e-4))^1.6 |
ルールと、それぞれが防ぐ問題
これらはすべて、デモシーンでシミュレーションを停止した状態で A/B テストを行いました。そのため、フレーム間で異なるのは変更点だけです。
- 頂点シェーダーとフラグメントシェーダーの両方で、各波をそれぞれのフットプリントに合わせてフェードさせます。 フェードしないと、波長が 3~8 m の波が波長より細かい間隔でサンプリングされます。遠方の海面は硬い水平線まで高密度の粒状になり、遠方の水面に映る太陽のきらめきも消えます。連続するフレーム間で、遠方の帯域のちらつきは平均で 2.3 から 5.8 グレーレベルに増加し、95 パーセンタイルは 5 から 23 に上昇しました。動きの中では、沸き立つように見えます。
- 解析的な導関数から、フラグメントシェーダーで法線を計算します。 補間された頂点法線を使ってシェーディングすると、近景を越えた先ではディテールが消えます。遠方の海面にはメッシュのリングに沿った長く滑らかな筋ができ、きらめきは砕けて輝点にならず、にじみます。
- ΣQ を 1 未満に保ちます。 急峻度は横方向の変位をスケーリングします。合計が約 1 を超えるとヤコビアンが負になります。水面が自身を貫いて折り重なり、泡のマスクが波頂で飽和して平坦な白い板状になります。
setSeaState()では振幅を上げ続けられますが、急峻度は ΣQ = 0.95 に制限されます。 - 反射ベクトルを水平線より上にクランプします:
R.y = abs(R.y) + 0.002。 波の裏側の一部では反射が下向きになります。クランプしないと、それらの面は空のキューブの下半分をサンプリングし、きらめきのない淡いグレーのにじみになります。かすめる角度からのショットでは、フレームの 3~4% が最大 154 レベル変化しました。 - 空の描画に使うものと同じヘイズ関数にフォグを溶け込ませます。 単色のフォグカラーが空と合うのは、太陽側だけです。それ以外の場所では、水面と空の境目にくっきりした線が残ります。
- ディテールと泡のテクスチャを、サンプリングスケールと同じ除数を使って、原点浮動オフセットに合わせてスクロールします。 参照実装では泡を
/9と/3.3でサンプリングしていましたが、オフセットには/11と/4.1を使っていました。1200 m ごとに原点を再設定するたび、泡のパターンが約 24 タイル分ずれていました。このモジュールではこれを修正しています。 - 原点浮動項も含め、波の位相は CPU 上で float64 を使って [0, 2π) にラップします。 シェーダーに未処理の時間を渡すと、3 m の波では 1 時間以内に
ω·tが 16,000 rad を超えます。その結果、最初に精度が劣化するのは float32 のsin()引数です。このルールは精度に基づくもので、デモでは問題が確認されていません。 - 描画時と同じスペクトルを使って高さを問い合わせます。 ここでの Gerstner 波は点を最大 4.5 m 横方向に移動させるため、
height(x, z)は y を読み取る前に、固定小数点法を 4 回適用して水平変位を逆算します。disp(x, z).yを直接読み取ると、20,000 サンプル全体で平均 0.15 m、急峻な波頂では最大 1.17 m の誤差がありました。4 回適用した後の最大水平誤差は 0.18 m です。
色とパイプライン
- すべての uniform 色はリニア HDR です。シーンを HalfFloat ターゲットにレンダリングし、合成時に一度だけトーンマッピングします。順序は ACES、その後 sRGB です。参照実装では
renderer.toneMapping = NoToneMappingを設定し、露出 0.9、ブルーム 0.35 で ACES を独自に適用しています。 - 空のキューブは、ページ独自の空シェーダーを使って描画し、小さな太陽の円盤は含めません。解析的なスペキュラー項が、水面に太陽の軌跡を描きます。
- 平面反射では、水面に映るオブジェクトを含むレイヤーだけをレンダリングします。y > −0.05 でクリップし、フレームのシャドウマップを再利用します(
shadowMap.autoUpdate = false)。
コスト
海面のメッシュは 167,600 個の三角形で構成されています。フラグメントシェーダーはピクセルごとに 8 波のループを 1 回実行し、さらにテクスチャを 6 回、キューブを 1 回、平面を 1 回サンプリングします。平面反射パスでは、反射レイヤーを半分の解像度で再レンダリングします。マシンに大きな負荷がかかっている状態でも、ヘッドレス Chrome は 1440×900 で Pirate Ship Sunset シーン全体を 1 フレームあたり 10~13 ms で実行しました。海面を非表示にするか、反射パスをスキップしても、実行ごとのノイズ(約 ±2 ms)を超える差はありませんでした。そのため、どちらもボトルネックではありません。参照実装では、解像度の適応調整によって対応します。DPR は 1.6 を上限とし、60 フレーム平均が 26 ms を超えると解像度スケールを 0.6~1.0 の範囲で段階的に下げ、14.5 ms を下回ると上げます。
ライフサイクルとアクセシビリティ
dtを 1/20 s に制限します。参考実装では 1200 m ごとにワールドの原点も再設定し、船、航跡、パーティクル、カメラ、waves.offX/offZをまとめて移動します。- モーションを低減する設定が有効な場合はシミュレーション時間を止め、イントロのフェードが終わるまでレンダリングを続けた後、フレームを保持します。コントロールが変更された場合のみ再レンダリングします。デモの
__OD.hold()がこれを実装しています。 - タブで一時停止します。ページが非表示になると
setAnimationLoopが停止し、再開時の時間差はdtの制限によって吸収されます。
確認
- カメラを水面から 2 m の高さに置き、太陽に向けると、遠方の海は粒状ではなく、霞ときらめきとして表現されます。フットプリントのフェードをオフにすると、遠方の帯がフレーム間でちらつきます。
- 頂点ごとの法線では遠方の細かなディテールが明らかに失われます。そうならない場合、法線はまだ頂点から取得されています。
- 海況を 1.6 に上げます。波頭は繊細なままで、平坦な白い板状になりません。
- すれすれの視点でも、波の背に薄い灰色のにじみが現れません。
- 太陽と反対側の水平線で、水面が境界線なしに空へ溶け込んでいます。
- 浮遊物体が波頭に合わせて動き(
height()を反転)、航跡の両腕が約 19° に開きます。 - モーション低減時には構図の整った静止画が保持され、コントロールを操作すると再レンダリングされ、1440×900 と 390×844 の両方でコンソールにエラーがありません。