#概要
UE4で海を作るサンプルプロジェクトを作りました。
エンジン改造はせず、プロジェクトプラグインとして実装しています。
Githubにあげています。
何かあれば何でもご指摘ください。
レイトレーシングについては現状では配慮できてません。
いきなりこの記事やソースコードを読んで訳がわからない場合は、まず前々回の記事とソースコードを読んでみてください。
[前々回の記事]
(https://qiita.com/monguri/items/ae73e57571b2495be1b2)
#Githubレポジトリ
https://github.com/monguri/UE4ShaderPluginSandbox
#スクリーンショット
なぜかQiitaへのgifの貼り付けでエラーが出るので、pngを貼っておきます。
動いてるものを見たい場合は、すみませんが、上記githubレポジトリのReadmeに貼ったgifを見るか、プロジェクトをダウンロードしてビルドして見ていただければ。
#今回の海でやったこと、やってないこと
やったこと
- 広大な海の表現。
- 海面の動きのリアルタイムシミュレーション。
- メッシュのライティングがそれなりに見栄えのする絵になっていること
- カメラと海面の距離に関係なく、海面の絵に破綻のないこと
やってないこと(まだやってないこと)
- 「やったこと」以外です。つまり
- 水中の表現
- 屈折、透過、反射へのこだわり。
- 表面の白い泡、浅瀬に打ち寄せる波、などのエフェクト的な部分。
- オブジェクトが波に揺られるなどの海面の動きの別オブジェクトへの作用
- オブジェクトの動きによる波紋など、オブジェクトの海面への作用
#海の作り方の概要
今回海を作るに際し、インターネット上に公開されている海の技術資料を多く漁りましたが、おおむね以下のような作り方になっています。
- 海面の動きをいくつかのパラメータ(風速など)からシミュレーションし、メッシュの頂点の移動値となるべき値をテクスチャに出力する。
- 画面を埋めるだけのメッシュを生成する。
- メッシュをテクスチャに従って変形させる。
- メッシュ法線もメッシュの変形に合わせて変化させる。
- 海面のエフェクト、ライティング表現を行う
各種ゲームの事例を見ると、1-5のそれぞれの項目で、リアルタイム(毎フレーム)でやっているケースもあれば、事前にDCCツールで準備しているケースもあります。
今回は、ゲームに使えるような、なるべくインタラクティブで動的にパラメータ制御できるものを目指しているので、すべてリアルタイムに行っています。
今回のプロジェクトでは、各項目の技術選定でもっとも多く採用したのは[1]の資料となります。
そちらを見ると概要はつかめるはずなので、以下の文章はそちらの資料を見ながら読むことをお勧めします。
とはいえ英語の資料ですし、以降では、各項目について事例をあげつつ、ソースコードを読み解けるように最低限の説明をしていきます。
#海面の動きのシミュレーションとテクスチャへの出力
前回までの記事では、グリッドメッシュの頂点をコンピュートシェーダで直接移動させていました。
しかし、海面のシミュレーションでは、一旦テクスチャに移動値を入れて頂点シェーダでテクスチャ(今後ディスプレイスメントマップと呼びます)を参照して頂点を移動させる手法が一般的です。
その理由は、広大な海を扱うので頂点数が非常に多いため、直接に頂点を扱うと負荷が大きいからです。
海全体に一枚のディスプレイスメントマップを用意するのではなく、せいぜいが縦横512ピクセル程度のものを出力し、海全体を一定の面積(このプロジェクトのものは20m x 20m)で分割してディスプレイスメントマップを繰り返し適用します。
頂点数がもしテクスチャの解像度より高くても、テクスチャサンプリングの補間によってある程度なめらかな変形が保証されるでしょう。
当然、タイリングパターンは現れます。
その回避法についてもいくつか手法があるようですが後で説明します。
シミュレーションの手法について最も有名なのは、TessendorfさんがSiggraph2001で講演したPhillips Spectrumを使う方法でしょう。[2]
これは物理的なモデルから演繹された計算式というわけでなく、データから統計的に立てた数式らしいです。
Phillips Spectrumは周波数空間(正確には空間軸の波なので波数空間)での波の強さと風速ベクトルとの関係式です。
適当なサイズのテクスチャで波数スペクトルのマップ(ハイトマップとでも呼んでおきます)を作っておき、逆フーリエ変換をして、実空間での波の形状を取り出し、ディスプレイスメントマップとして書き出す、という方法です。
Tessendorfさんの手法は定番になっているようで、映画のタイタニック、とあるスクエニのゲーム[6]、Sea of Thieves[8]でも採用されています。
このプロジェクトのソースコードですと、OceanSimulator.cppのCreateInitialHeightMap()とかSimulateOcean()が該当します。
Tessendorfさんの手法だと、ハイトマップの波数はピクセルサイズだけの離散値があります。
ですが、もっと数を限定した波数(たとえば8離散値とか)のスペクトルをスライダで直接制御する手法もあるようです。
UnityのプラグインであるCREST[4]はそうなっているようでした。
数少ない波数であっても、それなりに良い見栄えになってくれるようなので、逆フーリエ変換を実装する手間を省きたいなら、これもひとつの手だと思います。
フーリエ変換だと、所詮サインカーブの足し合わせなので、高周波に上限がある以上、波の尖った部分(Choppyと呼ぶらしいです)の表現が難しいということがあります。
本来は海の波はゲルストナー波という、より尖った形状のカーブで表現したほうがよいようです。[1][5]
数少ない波数で足し合わせるやり方であれば最初からサインカーブでなくゲルストナー波を8パターンの波数で足し合わせてもそれなりによい見栄えになりそうです。
このプロジェクトでは、X方向とY方向の変位をパラメータで少しスケールさせることで尖りを制御できるようにしています。
DCCツールだと、Houdini16時点のOceanTools[9]はTessendorfさんの手法であることを確認しています。
インストールフォルダ内のocean.hにソースコードがあります。
リアルタイム性やインタラクティブ性や動的制御がそこまで必要なければ、OceanToolsでディスプレイスメントマップを生成し、それをUVスクロールする、あるいは数フレーム分用意してフリップブックにする、という方法でも十分なクオリティになるかもしれませんね。
#画面を埋めるメッシュを生成する。
今回は、[1]で紹介されている四分木(Quadtree)によるメッシュ生成アルゴリズムを使っています。
これは、テレインでよく使われてきた方法のようです。
[1]の資料にわかりやすい画像がのっています。
パフォーマンス面を考慮すると、カメラから近い部分のメッシュは割りが密で、遠い部分のメッシュは割りが粗くなってほしいところです。
最初から広大な面積に正方形メッシュを敷き詰めておいて通常のLODシステムで割りを変えることもできますが、必要なのは正方形の等分割に割られたシンプルなメッシュなので、プログラムで動的に生成するのは難しくありません。
動作をわかりやすく見えるようにしたのが以下のgifになります。
黒い枠線で区切られているのが各正方形メッシュです。
赤いものがLODレベル0で、LODレベル8まであります。
LODレベルが上がるごとに青みがかっていくようにしています。
LODレベル0のものは20m x 20mです。
LODレベルが上がるごとにメッシュサイズを2倍(面積は4倍)にしていきます。
LODレベル8だと5km四方くらいのサイズになり、おおむねこれだけあれば、カメラが原点付近にあるうちは、水平線まで埋め尽くせます。
カメラが動くと敷き詰めの様子も動的に変わっていくのがわかるかと思います。
カメラに描画されない部分のメッシュは動的に削除されています。
このプロジェクトのソースコードで言うと、Quadtree.cppのBuildQuadtree()やCreateQuadMeshes()が該当する処理になります。
この手法は、ワールド座標系基準でのメッシュの配置です。
このプロジェクトで言うと、OceanQuadtreeActorを中心にしてワールド座標系でメッシュを動的に生成配置します。
カメラを動かしてもメッシュの位置は変わりません。(カメラとの距離を変えるとLODが変わるのでメッシュ自体のサイズが面積2^nあるいは2^-n倍のものに差し替えられるのはありますが)
メッシュのXY方向の軸もワールド座標系のXY軸に沿っています。
CREST[4]も、四分木こそ使っていませんがワールド座標系基準です。
なんと、カメラの位置を中心に、スケールを2倍していく8枚の正方形メッシュを使うという大胆な手法です。
一方、[1]でも述べられていますが、スクリーン座標(フラスタム)基準でのメッシュの配置という手法があります。
スクエニのゲーム[6]やアズールレーンのゲーム[7]はスクリーン座標基準と言えるでしょう。
スクエニのゲームは長方形メッシュ数枚で、アズールレーンのゲームは一枚の扇型のメッシュで、フラスタムを埋めています。
[1]でワールド座標基準とスクリーン座標基準の長所短所が述べられています。
ワールド座標基準だと、四分木であれ、CRESTのような正方形8枚を使う手法であれ、画面に描画されないメッシュ領域が比較的多く生まれ、そこの部分についても処理を行ってしまうコストが生じます。
スクリーン座標基準だと、メッシュのサイズ調整によって、ほぼ画面ぴったりのメッシュにできますので比較的コストが小さくなります。
また、四分木を使うなら実装が複雑になるというのもありますし、LODの切り替わりのぱかつきが少し見えるという短所もあります。
一方、スクリーン座標系基準だと、FOVを上げた時はメッシュを横にスケールさせる、あるいは横に継ぎ足したものを生成する面倒がありそうです。
また、カメラを動かすと追従してメッシュも動かさねばならないため、メッシュの参照するディスプレイスメントマップのUVをカメラの動きに合わせて変えねばなりません。
そのときにアーティファクトが出そうで、それどうごまかすかという問題が生じるかもしれません。(作ったことないので本当に出るのかわかりませんが、[1]や[4]の資料だと心配されてました。実際どうなんでしょう。。。)
それと、カメラが海面近くでカメラ方向が水平に近ければいいですが、上空から見下ろす台形や扇型形状では画面の長方形を埋めるのは難しそうです。
最近のバトルロワイアルゲームのようにカメラが上空からなめらかに移動していくケースで特別な対応が必要かもしれませんね。
まとめると、スクリーン座標系基準は、カメラの動きを海面近くに限定できるゲームであればメリットがお大きいと言えるのではないかと思います。
もうひとつ、動的なメッシュ生成で必ず生じる問題として、割の違うメッシュ同士の境界部分を連続的につなげなければならないということがあります。
これは図を見ながらでないとわかりにくいのでまずはこのプロジェクトの手法を画像で説明します。
ワイヤーフレームの赤い部分がLOD0のメッシュで、ピンクのものがLOD1のメッシュです。
各メッシュの端の部分を黒くしています。
LOD0のメッシュが正方形のある角の部分で3枚のLOD1のメッシュと接しています。
LOD0のメッシュが、境界部分のみ三角形が2個分の大きさとなり、LOD1のメッシュと割りが連続的になっているのがわかると思います。
割りが連続的になってないと、境界部分で海面のメッシュに隙間が見えます。
このプロジェクトはLODがなんであろうと128x128にグリッド分割したメッシュを使っており、LOD0だと20m x 20mにスケールし、LODがあがるごとに2倍(面積は4倍)のスケールにしています。
なので、境界部分のみエッジを一つ抜いて、三角形を他の三角形2つ分にしておけば、このように割りが連続的になります。
隣り合うメッシュのLODレベル差が2になる場合もありますが、その場合は一辺が4個分の三角形にしておきます。
アズールレーンのゲームの場合は、一枚のメッシュなので、割りはいい感じにしてるのではないかと推測します。
スクエニのゲームの場合も、四分木のように動的に隣り合うメッシュの割りが変わるわけではないので、最初からいい感じの割りにしているのかもしれませんね。
メッシュの割りについてはテッセレーションで制御する方法もありますが、現世代では動作する環境が限定されてしまうのでこのプロジェクトでは採用していません。
#メッシュをテクスチャに従って変形させる。
UE4のマテリアルにはWorld Position Offsetという機能があり、頂点シェーダで指定したUVに対応する頂点を自由に動かすことができます。
こにテクスチャを参照した値を入れればよいです。
このプロジェクトだとOceanGridMaterial.uassetで行っています。
UE4以外のエンジンであっても、頂点シェーダあるいはコンピュートシェーダでテクスチャから頂点を変形させればよいです。
ここで考えねばならないのはタイリングパターン対策です。
今回のディスプレイスメントマップは20m x 20mに使っているので、カメラを離すと20m x 20mの周期的な波形状のパターンが見えることになります。
これについては、数枚のディスプレイスメントマップを用意して周期を変えて重ね合わせる(カスケードという名前で呼ぶことがある模様)という手法があるようですが、今回は、[1]に倣ってPerlinノイズでごまかしています。
カメラが遠くなるほど、ディスプレイスメントマップでなくPerlinノイズから波の高さを取得する率を高めています。
Perlinノイズも、テクスチャサイズが限られる以上、周期性は出るのですが、UVスケールを変えた参照結果を数枚重ね合わせることでごまかしています。
#メッシュ法線をメッシュの変形に合わせて変化させる。
これは、ディスプレイスメントマップからコンピュートシェーダなりピクセルシェーダなりで法線を計算して法線マップを作っておくだけです。
それをマテリアルで使用します。
このプロジェクトだと法線がそのまま入っているわけではないのですが、GradientAndFoldingMap.uassetに出力し、OceanGridMaterial.uassetで使用しています。
法線でもタイリングパターンが出ないようにPerlinノイズを使ってごまかしています。
#海面のエフェクト、ライティング表現を行う
これについては、このプロジェクトではほぼ何もしていません。
法線マップを作ったくらいなもので、BaseColor、Roughness、Metallicに適当な定数を入れているだけです。
今回はシミュレーションを重視してるのでエフェクト、ライティングにはこだわっていません。
それでもそれなりの見栄えになってくれるのはUE4の楽ちんさですね。
むしろそちらの方について重点を置いた資料は、ここにあげたものを含め多く公開されているので、見てみるといいでしょう。
ただ、実用レベルにするなら、海の白い泡の表現(Foamと呼ばれる)や、海岸近くの浅瀬での打ち寄せる波や浅瀬の海底の透過屈折は必要になると思うのでいずれやりたいです。
#課題
「やってないこと」、に書いたことがすべて課題です。
また、最適化にも気を使っていません。
実用レベルにするにはまだまだやらねばならないことは多いと思います。
特に、風速が強い時の荒れた海を表現するのは、今の実装だと、パラメータ設定がかなり難しいと思います。
ビューフォート風力階級[10]みたいな公式のパラメータもあるので、少ないパラメータで簡単に制御できるようにしたいです。
#参考資料
##[1]NVIDIAのOceanCSの資料
http://www-evasion.imag.fr/~Fabrice.Neyret/images/fluids-nuages/waves/Jonathan/articlesCG/NV_OceanCS_Slides.pdf
##[2]TessendorfさんのSiggraph2001の講演資料
http://jtessen.people.clemson.edu/papers_files/waterslides2001.pdf
有名な資料です。
Tessendorfさんのサイトから、Siggraph2001以外の資料も読むことができます。
http://jtessen.people.clemson.edu/reports.html
##[3]NVIDIA GameworksのWaveWorksの資料
https://github.com/NVIDIAGameWorks/WaveWorks/blob/master/doc/WaveWorks-PartnerInfo.pdf
こちらはGameworksのgithubプライベートレポジトリへのメンバー登録をしないと見ることはできません。
##[4]CrestのSiggraph2019の資料
http://advances.realtimerendering.com/s2019/CrestSIGGRAPH2019-Final-for_web.pptx
##[5]GpuGems1 Chapter 1. Effective Water Simulation from Physical Models
https://developer.nvidia.com/gpugems/gpugems/part-i-natural-effects/chapter-1-effective-water-simulation-physical-models
ゲルストナー波について書かれた随分昔の資料。
##[6]とあるスクエニのゲームの海の資料
https://www.slideshare.net/EpicGamesJapan/ue4-139190798
12ページ目から
##[7]アズールレーンのゲームの海の資料
https://www.slideshare.net/EpicGamesJapan/ue4-festeast2019-azurlane
##[8]Sea of Thievesの海の資料
https://dl.acm.org/doi/10.1145/3214745.3214820
##[9]HoudiniのOceanToolsのチュートリアル動画
https://www.sidefx.com/ja/tutorials/houdini-16-ocean-tools/
##[10]Wikipediaのビューフォート風力階級のページ
https://ja.wikipedia.org/wiki/%E3%83%93%E3%83%A5%E3%83%BC%E3%83%95%E3%82%A9%E3%83%BC%E3%83%88%E9%A2%A8%E5%8A%9B%E9%9A%8E%E7%B4%9A