DirectXの話 第141回
Parallax-Corrected Cubemap 13/11/03 up
今回やってみたのはParallax-Corrected Cubemapです。
日本語に訳すとなると…視差的に正しいキューブマップ、でしょうか?いまいちかっこいい名前が思い浮かびませんね。
なお、今回の記事はSebastien Lagarde氏の記事を参考にさせていただきました。
英語が問題ない人はこちらの記事を読んでいただいた方が早いかと思います。
さて、まずはキューブマップの簡単な解説からです。知ってる人は読み飛ばしてもかまいません。
キューブマップは立方体の6面それぞれを2Dテクスチャで表現したテクスチャマッピング手法です。
立方体中心からのベクトルによってテクセルをフェッチしますが、その特性から環境マップに使われることが非常に多いです。
昨今ではImage-based Lighting (IBL) のソースとして利用されることも多くなっています。というか、環境マップという考え方がそもそもIBLの一種といっていいでしょう。
IBLのソースとしてのキューブマップはIrradiance Environment Map (IEM) やPrefiltered Mipmaped Radiance Environment Map (PMREM) が基本となりますが、
これらについてはトライエースさんのCEDECやGDCでの記事が参考になることでしょう。
これらのキューブマップは撮影されたキューブマップがあればオープンソース化されているAMDのCubeMapGenを使うと簡単に生成できます。
先にも解説したように、キューブマップは中心からのベクトルを用いてフェッチされます。
そのため、環境マップでは反射ベクトルなどをそのままキューブマップのテクスチャ座標として使用することが多いです。
この場合、キューブマップが非常に大きなもの、例えば天球や遠くにある自然物、人工物などを映しているだけであれば、視線を反射させている位置がどこにあっても中心として扱ってそれほど問題はありません。
このようなキューブマップはグローバルキューブマップなどと呼ばれ、基本的にはシーンによって1つ、もしくはある程度の大まかなエリアごとに1つという具合で用意されます。
例えばオーブンフィールドのゲームなどでは空や遠景の山などが描画されたキューブマップを用いることである程度のIBL、環境マップの表現が可能です。
しかし、このフィールド内にいくつかの小さな家が点在し、それらの家に入れるような場合はどうでしょう?
家に入ったのに主人公のヘルメットに青空が映り込んでいる、地面の草からの照り返しで足元が緑色でライティングされる…これは明らかにおかしいですね。
このような場合、ある狭い範囲内ではその範囲内でしか有効ではないキューブマップを用いたりします。これらはローカルキューブマップなどと呼ばれます。
この範囲に入ったら参照するキューブマップをグローバルからローカルに変更すればいいのですが、ここで1つの問題が出てきます。
グローバルキューブマップは非常に大きな範囲を描画しているものなので、それに比して非常に小さな位置の移動は中心位置から動いていないものとして無視することが可能でした。
しかしローカルキューブマップの場合はそうはいきません。以下の図をご覧ください。
カメラの角度、キャラのヘルメットに対する反射ベクトルの双方は同じ角度です。
しかし、位置が異なることでフェッチすべきテクセルはAとBというように大きく分かれているのがわかるでしょう。
普通にキューブマップをフェッチした場合、この2パターンでフェッチされるテクセルは全く同一になります。
AとBが全く同じ素材の壁であればある程度ごまかしも効きますが、Aの位置には絵画が飾られていてBの位置には壁しかない、となったら反射がおかしいことがすぐにわかってしまいます。
そこで出てくるのが今回のParallax-Corrected Cubemapです。
この技術を用いることで上図のような場合でも正しいテクセルをフェッチしてきてくれます。
実装方法は以下の通りです。
点Cにてキューブマップを撮影する。キューブマップは通常通り、+X, -X, +Y, -Y, +Z, -Z方向に画角90度で描画。
点Cを内包する矩形範囲(AABB、もしくはOBB)を設定する。矩形範囲は壁、床、天井などの正しく反射させたいものを極力外接させた範囲とする。
また、この矩形範囲内のオブジェクトは極力キューブマップに描画しない。
反射ベクトルRと矩形範囲の接点Pを求める。
点Cから点Pへのベクトルをキューブマップのテクスチャ座標として用いる。
たったこれだけです。下の図はこの処理を図示したものです。
V が視線ベクトル、N が法線ベクトル、斜線は反射素材でできている床で、紫のラインは正しく反射させたいオブジェクトです。
ここで勘違いしやすいことなのですが、矩形範囲とキューブマップは必ずしも一致していません。もちろん、一致させてもかまわないのですが、基本的には一致しません。
例えば上図の点Pが存在する面はキューブマップの+X軸方向のスクリーンと一致するわけではないのです。
例えばこの矩形範囲がOBBであっても、キューブマップ撮影をそれに合わせて回転させる必要もないのです。
まあ、実際のソースコードで計算式を見ていただければどのような処理なのかわかるかと思います。
実装方法でも書いていますが、キューブマップ撮影時の注意点としては、
キューブマップの撮影位置は矩形内部で行うこと(矩形の境界面と接するのは基本的にNG)
矩形範囲内のものは撮影しない。ただし、矩形範囲と接している部分は描画してもよい(上図の床など)
矩形外部は極力凸包体の内部のような形状であり、それらの形状のほとんどがキューブマップに描画されるように撮影する
詳しい実装についてはサンプルコードをご覧ください。
今回のサンプルでは前回やったRLRと組み合わせて処理を行っています。
前回はRLRの正しく描画されにくい部分は床面の色との間で補間していましたが、今回はキューブマップとの間で補間をし、これをフレネル反射的に床の色とのブレンディングを行っています。
おかげでだいぶそれらしい反射になったのではないかと思います。
もちろん、まだまだ正確とは言えないのですが…
では、サンプルを以下からダウンロードしてください。
Download:Sample132.zip
操作はいつも通りです。
今回のキューブマップは毎フレーム描画していますので、シャドウやライティングの結果が割と正しく表現されています。
IEMやPMREMはリアルタイムで生成するのは少し厳しいです。IEMはともかく、PMREMはかなり骨が折れるでしょう。
通常はこれらのキューブマップは事前に生成して利用するものと思いますが、
撮影機能をゲーム内で作成しておくと環境の大きな変化が起こった場合に一部のキューブマップを生成し直すことができるようになります。
そうでなくてもツールなどで撮影機能を作成しておくといろいろ楽ができるのでお勧めです。
さて、今回のサンプルはここまでの話を実装していますが、ローカルキューブマップを利用する場合には考えなければならないことがあります。
それはキューブマップのブレンドです。
Lagarde氏の記事でも重要視されている項目で、これはローカル-グローバル間、もしくはローカル-ローカル間で突然反射のテイストが変わってしまうのを防ぐためです。
突然の変化を気にしないという方法もありますが、ある程度ごまかせる状態でなければ大きな違和感となります。
ローカルキューブマップがシーンに点在し、ローカルキューブマップ同士が接触しない状態にあるのであれば、
ローカルキューブマップの範囲境界付近でグローバルキューブマップとブレンドするだけなのでそれほど難しくありません。
これがローカルキューブマップ同士のブレンドではそうもいきません。
Deferred Renderingの場合、IBLによるライティングを行う前に各ピクセルでフェッチすべきキューブマップのテクセルを事前に計算する方法があります。
キューブマップが適用される矩形範囲を描画し、この範囲内のピクセルにウェイト値を積算したキューブマップの結果を加算、最後にウェイト値の合計で割り算すればOKです。
キューブマップの数がそれほど多くないのであればこの方法は有効ですが、Forward Renderingには対応できないため、半透明オブジェクトには使用できません。
Forward Renderingでも対応できる方法としてはPosition of Interest (POI) を用いる方法があり、Lagarde氏は『Remember Me』にてこの方法を用いたようです。
POIとはそのシーンにおいて重要となる位置であり、ユーザの注意が注がれるポイントといっても差し支えありません。
例えば三人称視点のゲームであれば主人公の位置であったり、カットシーンなら注目すべきボスキャラや重要アイテムの位置などです。
この位置に関係するキューブマップを別のキューブマップにそれぞれ6面そのままを描画し、この位置に従ったウェイト値を積算し、結果を加算していきます。最後にウェイト値の合計で割るのは変わりません。
IEMならみっぷマップは無視できますが、PMREMの場合は6面*ミップマップレベル分だけの画面を描画する必要があります。
問題になるのはParallax-Corrected Cubemapでローカルキューブマップをブレンドする場合です。これは各面の各テクセルをそのままブレンドするわけにはいきません。
Lagarde氏の実装では床面をメインの反射面として設定し、POIをカメラの位置としているようです。
考え方としては、メインの反射面に対してミラーリングしたカメラ位置からキューブマップを描画した場合という形でキューブマップのブレンドを行います。
最初に描画するキューブマップの面とスクリーン上のXY座標からキューブマップをフェッチするテクスチャ座標(つまりはベクトル)を求めます。
ミラーリングしたカメラ位置からこのベクトル方向に対してローカルキューブマップの矩形範囲との接点を求めます。
この接点は反射ベクトルとの接点と同一になりますので、ローカルキューブマップの撮影位置からこの接点へのベクトルを用いてローカルキューブマップをフェッチします。
これをブレンドキューブマップに加算していき、実際に使用する際にウェイト値の合計を考慮すればOKです。
反射している体でブレンドキューブマップが生成されているので、実際のIBLとしてフェッチする場合は反射ベクトルである必要はなく、ワールド上のカメラの位置からワールド上の反射位置へのベクトルを使用します。
この方法はメイン反射面が床、壁といった平面で対応することで成り立つ手法ですので、反射する床面が起伏に富んだ地形などの場合はうまくいきません。
まあ、ある程度なら平面でごまかしても問題ないとは思いますが。
ブレンド方法やサンプルコードはLagarde氏の記事をご覧いただくのが早いかと思います。
やっていることはそれほど難しくはないので、きちんと記事を読めばわかると思います。