mcpField
3
3d-ultra-realistic-water

3d-ultra-realistic-water

github.com/mengtoクリエイティブ・メディア7.1k449 回インストール3 日前に更新
リポジトリ

3d-ultra-realistic-water について

移動カメラ、地平線、太陽のある外洋シーン向けのThree.js水面描画スキルです。再利用可能な海洋モジュールと、波、反射、泡、航跡、性能に関する実装指針を提供します。

主な機能

  1. 01解析的なGerstner波シェーディング
  2. 02距離に応じた波のディテール低減
  3. 03空と平面の反射
  4. 04泡と船の航跡エフェクト
  5. 05波スペクトルに基づく高さ取得
  6. 06モーション低減への対応

活用シーン

  1. 01地平線まで続く外洋の描画
  2. 02Three.jsシーンへの波と反射の追加
  3. 03遠方の海面ディテールの安定化

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/mengto/skills --skill 3d-ultra-realistic-water

インストール後、Claude Code で/3d-ultra-realistic-waterを入力して読み込まれたか確認します。

同梱ファイル

SKILL.md と一緒にインストールされる参考資料やスクリプト。

agents/
openai.yaml
assets/
ocean.mjs
demo/
build.mjs
PROMPT.md
demo/src/
ui.css
ui.html
ui.js
references/
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.3519 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 の両方でコンソールにエラーがありません。

コメント