今回は以前の飲み会で話が出たParallax-Corrected Cubemapをマテリアルで実装してみます。
この手法は名前のとおり、視差を正しく計算してキューブマップをサンプリングする手法です。
正しく、と言っても完全に正確なわけではないですが、ある程度の正確さは確保できるので使い方次第では便利です。
理論的な部分については手前味噌ですが、「もんしょの巣穴 Ver2.0」を参照してください。
UE4で取り扱えるキューブマップは、種類と言っていいのかわかりませんが4種類存在しています。
1つ目はスカイライトのキューブマップで、グローバルな空間でのIBLを実装しています。
このキューブマップはオフラインでフィルタ処理が行われ、ラフネスの値に応じて使用するミップマップを変更する機能を有しています。
サンプリング方法は普通の方法で、反射ベクトルをそのままUVW座標として扱います。
2つ目はReflection Captureで、ローカルなIBLを実装するのに利用します。
数段階のレベルでサイズの違うキャプチャ範囲を指定し、主に動的オブジェクトが周囲の環境を反射するのに利用します。
形状はSphereとBoxがあり、実はどちらも視差調整を行っています。
Sphereは球で範囲を指定しますが、この範囲までを描画し、また反射ベクトルとの接触判定を行うのもこの範囲球となります。
Boxの方は配置すればわかりますが、2つの領域が存在しています。
外側の範囲ボックスは描画範囲です。そして、内側のボックスがまさに視差を調整する範囲です。
反射ベクトルはこのボックスとの衝突判定を行い、撮影座標からその点へ向けたベクトルがそのままキューブマップのUVW座標になります。
しかしReflection Captureのキューブマップは解像度が128で限定されており、鏡のようなはっきりとした反射にはあまり適しません。
3つ目は普通にキューブマップをテクスチャリソースとして扱う手段で、普通にアセットとして読み込んでマテリアルでサンプリングすることが出来ます。
必要とあればここでもParallax-Corrected Cubemapを使用することも出来ます。
最後はCube Render Targetを使うもので、使い方そのものは3番目の普通のキューブマップと同様にマテリアルでサンプリングします。
しかし、このRender Targetはゲームのランタイム中に描画を行うことができるので、動的な空間でも使用することが出来るという利点があります。
重いですが毎フレームレンダリングしてプレイヤーキャラを鏡に反射させることも可能です。
まあ、毎フレームレンダリングは十分マシンパワーのあるPCで使うならともかく、スマホなどのモバイル機器では厳しいかもしれませんが。
部屋の様子が変化したフレームのみ撮影を行うことも可能です。
今回はこのCube Render Targetを利用し、マテリアル側でParallax-Corrected Cubemapを実装する手段を紹介します。
なお、すでにMaterial Collectionに追加していますので、そちらを参照してもらっても構いません。
https://github.com/Monsho/UE4MaterialCollection
続きには実装の簡単な解説と各マテリアル等の使い方を書いています。
今回はマテリアル関数、マテリアル、Blueprintが実装されています。
重要なのはマテリアル関数ですが、BlueprintはScene Capture Cubeをそのまま使っただけでは対応しづらいヘルパー関数を実装するためと範囲ボックスを表示するために利用しています。
まずはマテリアル関数から見ていきましょう。
実装したマテリアル関数は2つで、"MF_ParallaxCorrected_AABB"と"MF_ParallaxCorrected_OBB"です。
名前のとおり、軸整列境界ボックス(AABB)と方向付け境界ボックス(OBB)の実装です。
速度面ではAABBの方が高速ですので、ボックスを配置する際に回転を行わないようであればAABBの方を使用しましょう。
関数入力は以下のようになっています。
- 共通
- Capture Position
- Scene Capture Cubeのワールド空間座標
- Capture Position
- MF_ParallaxCorrected_AABB
- AABB Max, Min
- AABBのワールド空間中の最大、最小値
- AABB Max, Min
- MF_ParallaxCorrected_OBB
- WorldToLocal X, Y, Z, T
- ワールド座標をUnitサイズのAABBに変換する行列
自前で計算するのは大変なので、後述のヘルパー関数を使うことを推奨
- ワールド座標をUnitサイズのAABBに変換する行列
- WorldToLocal X, Y, Z, T
関数を使うにはまずCube Render Targetを作成する必要があります。
コンテンツブラウザで[新規追加]→[マテリアル&テクスチャ]→[キューブの描画ターゲット]で作成します。
作成した直後は緑一色ですが、描画を行えば画像が保存されます。
ただし、描画はランタイム中にしか行えないので注意してください。
次にレベルにキャプチャを行うアクターを追加します。
通常はScene Capture Cubeというアクターを利用するのですが、今回は用意したBlueprintを配置しましょう。
Material Collection内に"BP_ParallaxCorrectedCube"というBlueprintがありますので、レベルに配置してください。
レベルに配置するとボックスが表示されますが、これが視差計算用のボックスです。
詳細パネルには"Target Texture"と"Capture Every Frame"というパラメータがあります。
"Target Texture"には先ほど作成したCube Render Targetをドラッグ&ドロップし、毎フレーム更新する場合は"Capture Every Frame"をONにしてください。
Cube Render Targetはマテリアル上で普通のキューブマップとして扱うことが出来ます。
AABBバージョンの鏡用サンプルマテリアルは以下のようになります。
関数に必要なパラメータを設定し、出力をCube Render TargetのUVsに設定しているだけです。
今回はわかりやすくするため、UnlitマテリアルのEmissiveにサンプリング結果を設定しています。
マテリアルパラメータですが、今回は部屋が動いたりしないのでレベルのBeginPlayイベントで設定しました。
大きいので複雑なように見えますが、実際には"BP_ParallaxCorrectedCube"のヘルパー関数で必要なパラメータを取得し、Dynamic Material Instanceに設定しているだけです。
AABBの場合は"GetCubeAABB"という関数でAABBの最大値と最小値を取得できます。
OBBの場合は"GetOBBWorldToLocal"という関数でOBBのWorldToLocal行列の各情報を取得することが出来ます。
これらを対応するパラメータに設定し、ActorLocationをCapture Positionに設定すればOKです。
"Capture Every Frame"がONの場合は問題ないのですが、OFFの場合は正しい描画が行われていない可能性があります。
その場合はキャプチャを行う命令を発行しなければならないのですが、Scene Captureにはそのような命令がありません。
キャプチャはScene Captureが位置を移動すると発行されるようなので、少しだけ動かして元に戻すという処理をしてやることでキャプチャを促せます。
"BP_ParallaxCorrectedCube"のイベント"Event Capture One Shot"を呼び出せばアクターを動かさずに1フレームだけキャプチャを行うことが出来ます。
こうするとFキーを押すとキャプチャ命令を発行します。
で、結果を動画にしました。
ある程度は正しいことがわかるかと思いますが、歪んだり平べったくなったりしている部分も多々ありますね。
この手法はあくまでもボックスに対してのみレイトレースします。
で、キューブマップはあくまでもボックスに貼り付けられている絵なわけです。
つまりキューブマップに貼り付けられた絵には奥行きがないので、正常なレイ判定が行われないのです。
最も正しく描画されるのは衝突判定を取るボックスの境界線上に位置する物体のみで、その内側、外側にある物体はゆがみやすいです。
特にボックス内部の物体は平べったくなっているのがまるわかりなので注意が必要です。
鏡のように綺麗に反射する場合はこの手の問題がバレやすいですね。
また、視差調整ボックス内部に反射オブジェクトがなければいけないという欠点もあります。
外に出てしまうと正しく視差計算が行えなくなってしまうので注意です。
本来この技術はIBLで用いるもので、綺麗な鏡面反射では粗が目立つので使いどころが限られます。
だからといってIBLで使おうとするとScene Captureで描画したRender Targetはミップマップ化することが出来ないため、ミップマップを利用したラフネス表現が出来ません。
ただ、平面反射を用いた環境マップと違って複数の面に対応できるという利点もあります。
現在のUE4では正しい平面反射マップは実装が難しいので狭い範囲の限定的な処理を利用するしかないのが問題でしょう。
リアルになればなるほど、この手のごまかしが難しくなるのが欠点ですね。