毎フレーム、材料を作ってから 5 つのシェーダーで描く
左の Udon と真ん中の下ごしらえは、画面には出ない材料作りです。Udon は、見ている人がどの水にいるか、どこに波紋を落とすか、波音がどこまで進んだかを決めます。下ごしらえでは、Custom Render Texture(CRT)の連鎖で波の高さと波紋を計算し、専用のカメラで光の模様を描きます。右の 5 つのシェーダーが、その材料を読んで画面を描きます。流れる点は、材料の受け渡しを表しています。
VRChat ワールドの透明な浅瀬を、エディターで焼き込んだ少しのデータと、毎フレームの GPU の計算で描いています。水面の揺れ、水越しの水底、光の模様、打ち寄せる波、水中からの眺め、空の色、最後の色の整え方を、動く図で順に見ていきます。
屈折率、水の吸収、臨界角、トーンカーブなどの数値は、パッケージの実装と同じ値です。寸法と一部の色は、見やすさのために誇張したり簡略化したりしています。詳しくは docs/technical.md にあります。
左の Udon と真ん中の下ごしらえは、画面には出ない材料作りです。Udon は、見ている人がどの水にいるか、どこに波紋を落とすか、波音がどこまで進んだかを決めます。下ごしらえでは、Custom Render Texture(CRT)の連鎖で波の高さと波紋を計算し、専用のカメラで光の模様を描きます。右の 5 つのシェーダーが、その材料を読んで画面を描きます。流れる点は、材料の受け渡しを表しています。
CRT_FFT_Y1_Surface、256²、半精度)、波紋(CRT_RippleNormals、見ている人の前の 14 m 四方を 512²)、光の模様(RT_Caustics、1024²)。Seabed は深度テクスチャと影の印を通して、Water に水底の影を渡します。水面の揺れは、4.6 m 四方を 256 × 256 に分けた波の模様です。エディターで一度だけ波のスペクトルを作り、毎フレーム、時刻の分だけ位相を進めて、逆 FFT で高さと傾きを求めます。周波数を 2π/60 の倍数に丸めてあるので、波は 60 秒でちょうど元に戻ります。
模様は場所によらないので、海の広さやプールの数が増えても、この計算の負荷は変わりません。ただ、同じ模様を並べると繰り返しが見えてしまいます。そこで水面では、0.41 倍に縮めて回した 2 枚目を小さく重ねています。
水面から見える水底は、画面に描かれた水底のメッシュではありません。水面のシェーダーが、屈折した視線を、焼き込んだ水底の深さの式に当てて求めています。まず水面の真下の深さで交点を仮に置き(1)、その位置の深さで置き直し(2)、もう一度置き直します(3)。3 回で、起伏のある水底にもほぼ正確に当たります。
交わった所の砂の色に、水の中を進んだ距離だけ吸収を掛けます。赤が一番早く消えるので、深い所ほど青緑になります。この方式なら、画面の外や手前の物に隠れた水底でも絵が崩れません。水の中にあるアバターや杭だけは、深度テクスチャで見つけて画面から拾います。
cwFloorDepth水底に揺れる光の網目は、Evan Wallace の方法で作っています。波 1 枚分を覆う格子の各頂点を、水面で屈折した太陽光に沿って深さ 1.6 m まで曲げます。曲げた後に網目が縮んだ所は光が集まった所なので、明るく塗ります。
赤・緑・青で屈折率を少しずつ変えて 3 回描くので、模様の縁にうっすら虹色がにじみます。描く先は 1024² のテクスチャで、専用の正射影カメラが、y = −20000 の地下で Caustics レイヤー(23)だけを描きます。水底・水面・アバターの 3 つが、同じ模様を読みます。
浜に打ち上げる波は、聞こえる波音と合うように動かしています。90 秒の波音のループから、波が崩れる瞬間を 27 か所、あらかじめ検出してあります。実行中は、コントローラーが波音の AudioSource.time を、シェーダーの時計 _SwashClock に渡します。
波打ち際は 4 つの段階で計算します。沖から来るうねりは、浅くなるほど遅く高くなります。高さが水深の 0.8 倍を超えると崩れて泡立ちます。崩れた水は浜を放物線を描いて駆け上がり、上がるときの 0.6 倍の加速で引いていきます。水が引いた後の砂は暗く光り、数秒かけて乾きます。
水から空気へ抜ける光は、水面で大きく曲がります。真上から 48.6°(臨界角)より内側の視線だけが空気へ抜け、空の半球全体がその円の中に縮んで見えます。これがスネルの窓です。窓の外では水面が鏡になり(全反射)、水底や水中の物が映ります。
水面のシェーダーは、上から見るパスと下から見るパスを持ち、カメラの側に合わないほうを潰します。カメラが水面から 60 cm 以内にあるときは、レンズのある所で波が実際にどの高さにあるかを見て、画素ごとに上か下かを決めます。窓の外に映る物は画面からたどり、見つからなければ計算した水底を映します。
時刻の空は、エディターで大気の散乱を一度だけ計算し(5 秒ほど)、表にしてあります。空気の分子(Rayleigh 散乱)、細かい粒子(Mie 散乱)、オゾン、2 回以上の散乱まで入れた地球の大気です。実行中は、Udon の ClearwaterSky が時刻から太陽と月の位置を求め、グローバル変数でシェーダーに渡します。シェーダーは見る方向ごとに表を 1 回引くだけです。
太陽が低いほど、光が空気の中を進む道のりは長くなります。そのあいだに青い光が散らされ、届く光は赤く、暗くなります。図の色は、空気の分子による減り方だけで計算した太陽光の色です。
描いた明るさは、最後に 1 本の曲線(トーンカーブ)で画面の明るさに直します。露出 0.63 を掛けて ACES の近似曲線を通し、0.8 を超えた所だけは別の肩に乗せて、ゆっくり 0.9 に近づけます。画面に 100% の白が出ないので、太陽の周りや雲の縁の明るさにも階調が残ります。
既定では、同じ曲線を 33³ の LUT にして、Post Processing Stack v2 で画面全体に掛けます。シェーダーの中で掛ける方式も選べますが、水越しに拾った Standard の明るい物が白く飛ぶので、既定はポストプロセスです。