接線空間変換デュアルクォータニオン in Silverlight 5 Beta + XNA
さらにXModelビューアについて進めました。今回は、「接線空間変換デュアルクォータニオン」(と呼べばいいでしょうか)の描画処理方式を検討しました。
今回のスクリーンショットは、以前ブログで登場させたモデルにしました。簡単な法線マップを追加しています。特に頭部付近の凹凸でわかるでしょうか。(画像クリックして、さらに「オリジナルサイズを表示」を選択できます)
※記事完成前のスクリーンショットのため、描画結果の細部に違いがあるかも知れません。
法線マップ
これまで法線マップの対応として、ピクセルシェーダ方式を実験的に導入していました。しかし、これはシェーダモデル3.0用です。2.0用としては、頂点データとして接線空間を伝達する必要があります。
接線空間(Tangent Space / Tangent Frame)
接線空間(接線/従法線/法線)は、法線マップテクスチャから取得した法線の向きを変換(回転)するのに必要です。
頂点データ方式の接線空間への対応策
接線と従法線(従接線)の計算では、テクスチャ座標を使うのでテクスチャ座標の品質が影響します。
モデルの左右を完全なミラーで作っている場合、境界面を横断するポリゴンをするとその部分のテクスチャ座標に変化がなくなり、接線や従法線が0になる可能性があります。現在のところ計算上の工夫とは別に、モデリングの方針として、ポリゴンでミラー境界をまたぐ代わりに、頂点を境界面上に置いて、ポリゴンをつないだ方がよさそうだと考えています。
頂点をデュアルクォータニオンにする
- 接線空間をそのまま頂点データに与るならば、接線/従法線/法線の3x3成分が必要です。従法線を外積から計算する場合でも、2x3成分および符号が必要です。
- また一方で、スキニングには定数レジスタの節約もあって、スキン変換(ボーン)はデュアルクォータニオンにしています。
- 接線空間変換とスキン変換を結合して法線マップの法線の回転を得ることになります。
- 結合には、行列同士の積か、デュアルクォータニオンの積を使います。後者の方が成分単位での演算子の少なさで有利です。
- デュアルクォータニオンを座標変換に利用する際には、実質的に行列に変換して使うことになります。
以上のような状況から、接線空間変換もクォータニオンで扱って、スキン変換デュアルクォータニオンと、結合をしてから行列を得る作戦を試してみました。さらには、頂点位置も含め、接線空間変換をデュアルクォータニオンで扱うことにしました。
ミラーになった接線空間(従法線でミラーを担当させることにする)の場合に、符号が必要です。デュアルクォータニオンには、正と負に同じ回転を表す(2回転分の)表現力があります。この性質は利用しないので、実部実数値が正になるよう補正して、代わりに接線空間のミラーの符号をシェーダに伝えるために、この符号で補正します。
シェーダでは、その実部実数値の符号を従法線に適用します。
結果的に、命令スロット数は同等でできたようです。
これにより、XModel実行時モデルの頂点宣言の「位置(3)+法線(3)+接線(3)+従法線(3)」部分の代わりに、「空間変換(8)」(位置と接線空間のデュアルクォータニオン)バージョンを迎えることになりました。
頂点データが小さくなることで、転送帯域上で有利となる可能性はあります。
接線空間の算出部分
通常の接線と従法線の算出をした後、きちんと直交座標になった接線空間行列を作成し、位置をまとめて、デュアルクォータニオンを作るようにしました。前述の符号の補正も含みます。
※この頂点宣言は、DynamicVertexBufferを使うようなCPU側で頂点位置の動的処理をするようなメッシュには向かないかも知れません。
余談
シェーダ性能の高い環境下では、メモリからの読み込みよりも、シェーダの計算の余力に期待できるものと思います。
しかし、シェーダモデル2.0の命令スロット数が厳しいことで、特にピクセルシェーダでできることには限りがあります。
適用前 | 適用後 | シェーダモデル2.0上限 | |
---|---|---|---|
頂点シェーダ | 118 | 118 | 256 |
ピクセルシェーダ | 4(テクスチャ読み込み) + 62 | 4(テクスチャ読み込み) + 62 | 32(テクスチャ読み込み) + 64 |
頂点シェーダの仕事の一部をピクセルシェーダ上へのさらなる移行について試したいところですが、シェーダモデル2.0上では難しそうです。
※以上の内容は、この時点の状況を示しているので、理論や実装に誤りを含むかも知れません。
謎の暫定回避
今回の更新に関する頂点宣言の変更のためか、前回までのリリースビルドで起きたVertexBuffer.SetDataでの例外は見られなくなっています。