DirectXの話 第129回

Rectilinear Texture Warped Shadow Map 

今回はAdaptive Shadow Map (ASM) の1つ、Rectilinear Texture Warped Shadow Map (RTWSM) をやってみました。

上の図は左側がRTWSMで右側が標準的なShadow Mapです。

実装が面倒なASMの中でも実装しやすく、かつ処理速度面でも結構有利な技法です。

ASMに限らず、最近はAdaptiveという言葉をグラフィクス関係ではよく見かけます。

日本語では“適応した”とか“適応的な”という意味になります。

単一の計算式による処理というわけではなく、シーンの細部に適応させた、という意味合いがあるような感じでしょうか。

Shadow Mapを例に取ってみましょう。

Shadow MapのクオリティはShadow Mapの解像度に強く依存します。

解像度が高ければその分クオリティも上がりますが、無限に高くするわけにもいきません。

そこで、Shadow Mapのピクセルを極力無駄遣いしないようにする手法がいくつか存在します。

Cascade Shadow Map (CSM) やPerspective Shadow Map (PSM) 系の技術などですね。

これらは基本的に、カメラに近い位置に多めにピクセルを割り振るようにするものです。

しかし、カメラに近い位置に影が落とされないようであれば効果は薄いですし、

カメラから少し離れたところに木の葉の影のように細かい影が落ちるようですとそこにピクセルを多く振りたいと考えるのが妥当でしょう。

カメラに近い位置はピクセルを多く割り振りたい場所であるのは間違いないのですが、

シーンを細かく分析すればそこだけピクセルを多くすればいいというわけではないとわかるわけです。

そこで登場するのがASMです。

ASMはシーンを何らかの方法で分析し、本当に必要な場所に多めにピクセルを割り振るようにします。

本当に必要な部分というのは、カメラに近い位置、影のエッジ、面がカメラを向いている部分などです。

ASMの手法としてはIterativeな方法とDirectな方法があります。

Iterativeな方法はシャドウを描画するたびにその結果を分析し、少しずつ結果をよくしていくように修正をかけていく方法です。

この方法は1回の処理が軽くなりやすいですが、よい結果が得られるまでに少々時間がかかります。

Directな方法はシーンを直接分析し、1発で結果を求められますが、その分処理が重めになります。

今回のRTWSMはDirectな方法ですが、私の環境(RADEON5870)では通常のシャドウマップに対して2.8ms程度の差しかありませんでした。

シンプルなシーンなので決して軽いとは言えませんが、モデル描画が十分高速であればよい結果が得られますので、悪くはないと思います。

と言うわけでRTWSMの解説にやっと入ってくるわけですが、論文はこちらになります。

この技術は上図のように重要な部分をXY方向に広げます。

重要な部分の広げ方はX軸、Y軸それぞれに独立して行われますので、各軸用の1Dのワープマップを生成すれば事足りるという寸法です。

では、そのワープマップ生成までの流れを説明します。

まず必要なのは2D重要度マップです。これはライトビューで描画される必要があります。

基本的にはShadow Mapと同じような映像になりますが、解像度はShadow Mapと同一でなくてもかまいません。

ただ、シーンの細部がつぶれずに描画される必要がありますので、それなりの解像度は必要になります。

生成するにはシーンを分析する必要があるのですが、分析方法はForward Analysis, Backward Analysis, Hybrid Analysis の3つがあります。

Forward はライトから見た状態での分析です。ライトビューで深度値を描画したもの、つまり通常のShadow Mapを分析に用います。

Backward はカメラから見た状態での分析です。通常のシーン描画を行い、その深度値、及び法線を分析に用います。

Hybrid はこの2つの分析結果を合わせたものです。

分析には複数の重要度関数を用います。

各重要度関数の結果は積算され、その結果が各ピクセルの重要度となります。

論文では4つの重要度関数が挙げられています。

1つめのDesired View Function はカメラから見た映像に入っているか否かを求めます。入っているなら1.0、いないなら0.0となります。

Forward、Backward のどちらでも使用されますが、Forwardの場合はキャスタがカメラに入るかどうかではなく、

レシーバがカメラに入るかどうかをチェックする必要があるという点に注意してください。

2つめのDistance to Eye Function はカメラからの距離を元にした関数です。Forward、Backward 双方で使用されます。

3つめのShadow Edge Function は影のエッジ部分を用いる関数です。

Forward のみで使用されるもので、当然エッジ部分は重要となるような結果を返します。

最後のSurface Normal Function はBackward でのみ使用される、ビュー方向と法線方向の内積から求められる関数です。

法線がビュー方向に正対している状態というのはカメラから見て目立つ部分と言うことになるので、このような部分は重要度が高くなります。

2D重要度マップ生成の実装ですが、私は3回のモデル描画で対応しています。

1回は通常Shadow Map描画、1回はカメラから見た法線・深度描画、最後の1回が重要度マップ描画です。

重要度マップ描画では深度バッファへの書き込みは行わず、カメラから見たときに描画されない部分はdiscardで棄却します。

描画されるピクセルについては各重要度関数の結果を積算した値を求め、ブレンド計算をMAXにして描画を行います。

2D重要度マップが生成できたらワープマップを生成します。

これにはまずXY軸それぞれに1D重要度マップを生成する必要があります。

これは簡単で、それぞれの軸に沿って重要度の最大値を求めればOKです。

重要度をIxyで表現するとすれば、X軸のある位置xの1D重要度はmax(Ix0, Ix1, Ix2, ...) となります。Y軸も同様です。

次に1D重要度マップにブラーをかけます。

ブラーをかけることである程度の連続性を保持することが目的です。

ブラーは普通にガウスブラーです。ウェイト値は適当な固定値を使っています。

次にワープマップ生成ですが、こちらは論文中にある以下の計算式で求めています。

kがxもしくはyの値です。

kは1~nが範囲となるので、k/nは(x + 1) / n で求めます。

なお、今回の実装では1D重要度マップ生成からワープマップ生成までを1回で行っています。

ワープマップの生成はCompute Shaderを用いており、共有メモリを使って高速化しています。

DirectX9世代でも生成は可能ですが、多分Compute Shaderを使うよりかなり遅くなってしまうんじゃないかと思います。

ワープマップが求められたらワーピングを行ったShadow Mapの描画とカメラからのレシーバ描画を行います。

Shadow Mapの描画では以下の計算式を頂点シェーダに適用します。

P += GetWarp(P)

座標はXYそれぞれの軸で計算します。

注意点として、GetWarp() は入力値が0~1の範囲となります。

P は-1.0~1.0 の範囲なので、加工する必要があります。

詳しくはサンプルコードをお読みください。

レシーバ描画も同様の計算を用います。こちらはピクセルシェーダで計算します。

サンプルは下記からDLしてください。

Download : Sample119.zip

マウス左クリックでカメラ回転、右クリックでライト回転、中クリックでカメラ距離をいじれます。

サンプルではShadow Map、及び重要度マップの解像度は512*512となっています。

512*512では決して品質がいい物にはならないのですが、それでも最初の図のような結果が得られます。

しかし、どうしても頂点でワーピングを行うため、あまりにも歪みがすぎたりポリゴンの割り方が大雑把だったりすると歪みが大きくなりすぎておかしな影が出てしまうことがあります。

サンプルでも真上からライトを当てるような状態にすると大きな歪みが発生してしまいます。

特に戦車のキャタピラ付近でおかしな結果が見られます。

とはいえ、それほど目立つわけでもないので、普通の使い方をしている分にはそれほど問題にならないだろうとは思います。

次回はそろそろテッセレータもやりたいので、事始め的なものでお茶を濁すかもしれません。

追記:

Addaptive Tessellationバージョンを作成しました。

ダウンロードは下記から。

Download : Sample122.zip

Googleサイトの容量がいっぱいいっぱいになってしまったのでDropboxの共有フォルダに突っ込んでいます。

RTW Shadow Mapsを描画する際、ポリゴンの大きさによりテッセレーションを行います。

ポリゴンの大きさは通常Shadow Mapsの投影ポリゴンのサイズを利用しています。

ワーピングしてから求めることも可能ですが、速度面でかなり不利になります。

render_model_tess.hlslのADDAPTIVE_WARPという定義を有効にすればワーピング後のポリゴンサイズからTessFactorを求めるようになります。

比較画像はこちらです。

テッセレーションを行っていないものは見事に歪んでいますが、Addaptive Tessellationは歪みがかなり小さくなっています。

ただ、TessFactorを10.0の固定値にした物はより良い結果となっています。

しかし、FPS表示を見ていただければわかると思いますが、かなり重いです。

Addaptive Tessellationが2msの増加に対して、10.0固定では6ms以上重いですね。

Addaptive Tessellationでも分割数が増大しやすいシーンではかなり重くなるのですが、大きめの値の固定値よりよっぽどいいですね。

まあしかし、予め適切に割っておいた方が速いんじゃないかという気もしますがね。

テッセレーションを使用するのはどうしてもという場合以外はやらない方がいいんじゃないかと思いますね。

ハードによるんでしょうけど、うちの環境(Radeon5870)ではテッセレーションは重すぎです。