2009/08/24
Direct3D ファントムダストと破壊の穴
Posted by: oga
at 23:55
昔 ファントムダスト Phantomdust というゲームがありました。
今更ですが…忘れないうちにいくつか書いておきます。
マップの破壊表現は複数の手法の組み合わせで出来ています。
オブジェクト、パーティクル、破裂シェーダー、破壊のへこみ穴 等。
今回説明するのは「穴」の表現です。
攻撃やダメージ等によって、地形モデルのどの位置にでも削れたような穴があきます。
地面だけでなく、壁でも天井でも同様に破壊の痕跡が残ります。
このへこんだ形状は別モデルとして用意してあり、ステンシルマスクを用いて背景の
描画時に合成しています。
戦っているうちに地形の至る所が穴だらけになるわけです。
例えば下記リンク先のイレースシェルの地面がそうです。
いくつも穴がありますが、もともとは平らな地面のモデルデータです。
・famitsu.com
・Impress GAME Watch
●特徴
メリット
・デカールと違い立体的に見えます。
・背景モデルデータを書き換えることなく形状を変化出来ます。
・立体的なデータなのでヒット位置を厳密に割り出さなくても描画できます。
・ノーマルマップの背景に対して低コストで適用できます。
デメリット
実装時に様々な制限が生じます。問題をどのように解決したのかは後述します。
欠点というか実装できなかったのはコリジョンの追従です。コリジョンの変更はコストが
高く、ゲーム内容に関わるため通信同期も必要でした。残念ながら見送りました。
●この方法を用いた理由
破壊表現は当初からの課題でした。
プロトタイプの段階では通常のマテリアルを使っていたので、レイヤテクスチャ+
メッシュを分割+動的に頂点を書き換えていました。
問題となったのはノーマルマップです。
Xbox 1 の当時はまだ珍しかったのですが、最終的に背景もキャラも全面にノーマルマップを
使っています。これはハイポリのディテールを少ないポリゴンで再現可能な手法なので、
結果として頂点数を減らすように方針転換しました。よって
・頂点を動かすだけのポリゴンの細分化が出来ない
頂点を減らせるのがメリットだし、TS の容量も計算も減らしたい
・変形した形状の Tangent Space の再計算はコストがかかる
・PixelShader の 8+4 stage をノーマルマップで使い切っている
レイヤを増やすとマルチパスになる
●破壊穴モデルの形状
実際は平べったい形状ですが便宜上球と思って構いません。
穴の中のへこみを表現するモデルなので、面が裏返った内側を向いたデータです。
破壊の穴と地面の交差を判定するボリュームモデルと、実際に穴の中を描画する
描画モデルの 2つが必要となります。
・交差判定のためのボリュームモデル
・穴の中を描画するモデルデータ
描画モデルのマテリアルが単純かつ軽量なものだったので、この両者を兼用しています。
つまり描画モデルをそのまま断面の判定にも使用しています。
●断面の生成
シャドウボリュームと同じです。
テクスチャをフェッチしない専用の軽量シェーダーを割り当てて、ボリューム形状を
ステンシルバッファに 2回書きます。
(1) depth test かつ 表面、stencil +1
(2) depth test かつ 裏面、stencil -1
裏面と面面の差分が地面との交差面となります。上記の設定だと stencil が 0以外で
交差面です。
この条件を元に、同じ位置に描画モデルをレンダリングすれば合成ができます。
余談ですが、両面ステンシル機能がある GPU なら一度の描画で差分が求まります。
両面ステンシルが無くても両面マテリアルで代用可能で、例えばフレームバッファの
Alpha (DestAlpha) に加算で書き込むと一回で生成できます。
このとき描画頂点数は減りますがピクセル負荷は変わりません。
多くの GPU は Depth + Stencil の書き込みに特化したモードがあり、フィルレート
が倍になるように最適化されています。なので DestAlpha を使ってもあまりメリット
はありませんでした。
それにこのゲームは DestAlpha をすでに別の用途で利用していました。
●形状の変更
このゲームは表現上ステンシルバッファを多用しています。
8bit しかない stencil を bitmask して複数使い分けているところもあります。
・影
・オーラ
・地面や建物の破壊穴
もしゲーム画面か古い CGWORLD を見ることが出来るなら、穴の中にも形状に沿って影が
落ちていることがわかると思います。
でも描画しているモデルデータは平らなままです。
背景の影もシャドウボリュームを使っているのは、このように破壊によって動的に
地形が変わる可能性があるため。テクスチャや頂点への焼き込みだと、地形の変化に
対応できなくなります。
●負荷の相殺
破壊が進むと描画の負荷が増えて重くなりそうに見えますが実はそうでもありません。
このゲームは背景もキャラもすべてノーマルマップを使った重いシェーダーを適用
しています。最初から背景のピクセルが一番重い前提で負荷を計算しているので、
地形の描画面積を減らす処理は描画負荷の軽減につながります。
ノーマルマップを適用していない破壊の穴モデルは、数が増えれば増えるほど地形の
描画負荷を軽くすることになります。
ステンシルバッファへのレンダリングが増えますが相殺して軽くなります。
(でも頂点は軽くなりません。この判断は今思うと微妙だったかもしれません。)
同じようにキャラクタの描画負荷も相殺するのでアップになっても速度は変わりません。
半透明のエフェクトはまずいです。
●矛盾の解決
実際に組み込んでみると単純な表現でもさまざまな問題が生じます。
他の描画や表現と共存が必要だからで、1つ 1つ対策を施していきます。
・穴が開くと困る場所
破壊モデル合成時に、特殊な形状や半透明など矛盾が生じる場所があります。
あらかじめマテリアルの設定で描画しないように除外しています。
これは描画パスを分けることで実現しています。
・穴同士の重なり
Z test は別の判定に用いるので、描画の重なりの解決に Z バッファが使えません。
同じ位置にポリゴンが重なると矛盾が生じるので、描画位置を決める際に当たり判定を
行っています。必ず他の穴と重ならない位置に配置しています。
・地面の下だけ描画
破壊の穴形状は多少ずれても問題がないように球形をしています。
上半分が見えてしまわないように、地面の下に潜り込んだ部分だけ切り取るため
depth test を併用しています。Stencil がパスしてかつ、Z Fail が条件です。
地面の下にめり込んだ部分だけが描画されるようになります。
・depth 強制更新
ただ描画するだけでは不十分であることがわかります。
見た目と Z buffer に食い違いが生じるからです。
たとえば穴の上に置いた物や影が、地面の位置でまっすぐに切れてしまいます。
よって描画モデルを描き込むときに同時に depth を強制的に上書きします。
depth (Z buffer) が更新できれば、その後に描画する Shadow Volume も穴の形に
沿って落ちてくれます。
・遠くの穴が見えてしまう
地面が階層化している場所など、上の段の破壊穴と下の段の穴が重なって描画される
ことがあります。depth test の条件に Z Fail を設定しているということは、常に
遠くのオブジェクトが優先されることに等しいからです。
穴の中にさらに遠くの穴が見えてしまいます。
そこで穴モデル自体を Zソートして手前から描画し、同時にステンシルに 0 を書き
込んで消していきます。遠くの穴を描画する段階では depth test の Z Fail を
通っても、stencil テストに失敗するので描画されなくなります。
・地形との合成
上の問題を対処するためステンシルバッファを 0 で消してしまうと、今度はその
位置だけ地形を除外できなくなります。つまり穴の位置に上書きしてしまいます。
ステンシルをクリアする場合、別の条件で区別できる値を書き込まなければなりません。
元々ステンシル値の 0 または 0 以外で判定していたので思ったより簡単ではないです。
もしくは描画順番で何とかします。交差面を求めた後、地形の穴の中を描画する前に
先に地面を描画し、その後破壊穴モデルを描画すれば 0 クリアでも問題ありません。
・穴の上に重なるモデルの表示順番
地面に穴が開くはずの場所に他のモデルが置いてある場合も描画順番が問題となります。
例えばキャラが地面に立っていて、わずかに地面にめり込んでいる場合。(本当は
それが良くないのですが)
穴を開ける前の Z バッファで描画してしまうとめり込んだ部分が切れてしまいます。
だから地形に乗っているものは、穴を開けて Z バッファを更新したあとに描画する
ことになります。
・個数制限と消去
破壊の穴は同時に 30個描画しています。上限はただの定数値で、何らかの制限があった
わけではないです。これを超えた場合、視野に入っていないものから消しています。
●描画手順のまとめ
(1) 破壊可能部分の depth のみ
(2) 破壊穴モデルの交差判定
(3) 破壊穴の描画と depth 更新+識別可能な stencil で書き直す(要確認)
(4) 地形の上に乗っているモデル描画、不透明物一般
(5) 破壊不可能部分描画
(6) 破壊可能部分のカラー描画
(7) シャドウボリューム
(8) 影の合成
(9) 半透明地形の描画
(10) 半透明エフェクト
記憶だけで書いているので、おそらく異なっている部分や足りない部分があります。
これ以外にも特殊なエフェクトや表現があったので、実際はもっと描画手順が複雑です。
具体的に設定したステートの組み合わせなどは、おそらくもう一度実際に作ってみないと
わからないと思います。
それに今のハードならもっと簡単でしょう。ShaderModel 1.x の当時とは違うので、
別の表現方法も使えると思います。
今更ですが…忘れないうちにいくつか書いておきます。
マップの破壊表現は複数の手法の組み合わせで出来ています。
オブジェクト、パーティクル、破裂シェーダー、破壊のへこみ穴 等。
今回説明するのは「穴」の表現です。
攻撃やダメージ等によって、地形モデルのどの位置にでも削れたような穴があきます。
地面だけでなく、壁でも天井でも同様に破壊の痕跡が残ります。
このへこんだ形状は別モデルとして用意してあり、ステンシルマスクを用いて背景の
描画時に合成しています。
戦っているうちに地形の至る所が穴だらけになるわけです。
例えば下記リンク先のイレースシェルの地面がそうです。
いくつも穴がありますが、もともとは平らな地面のモデルデータです。
・famitsu.com
・Impress GAME Watch
●特徴
メリット
・デカールと違い立体的に見えます。
・背景モデルデータを書き換えることなく形状を変化出来ます。
・立体的なデータなのでヒット位置を厳密に割り出さなくても描画できます。
・ノーマルマップの背景に対して低コストで適用できます。
デメリット
実装時に様々な制限が生じます。問題をどのように解決したのかは後述します。
欠点というか実装できなかったのはコリジョンの追従です。コリジョンの変更はコストが
高く、ゲーム内容に関わるため通信同期も必要でした。残念ながら見送りました。
●この方法を用いた理由
破壊表現は当初からの課題でした。
プロトタイプの段階では通常のマテリアルを使っていたので、レイヤテクスチャ+
メッシュを分割+動的に頂点を書き換えていました。
問題となったのはノーマルマップです。
Xbox 1 の当時はまだ珍しかったのですが、最終的に背景もキャラも全面にノーマルマップを
使っています。これはハイポリのディテールを少ないポリゴンで再現可能な手法なので、
結果として頂点数を減らすように方針転換しました。よって
・頂点を動かすだけのポリゴンの細分化が出来ない
頂点を減らせるのがメリットだし、TS の容量も計算も減らしたい
・変形した形状の Tangent Space の再計算はコストがかかる
・PixelShader の 8+4 stage をノーマルマップで使い切っている
レイヤを増やすとマルチパスになる
●破壊穴モデルの形状
実際は平べったい形状ですが便宜上球と思って構いません。
穴の中のへこみを表現するモデルなので、面が裏返った内側を向いたデータです。
破壊の穴と地面の交差を判定するボリュームモデルと、実際に穴の中を描画する
描画モデルの 2つが必要となります。
・交差判定のためのボリュームモデル
・穴の中を描画するモデルデータ
描画モデルのマテリアルが単純かつ軽量なものだったので、この両者を兼用しています。
つまり描画モデルをそのまま断面の判定にも使用しています。
●断面の生成
シャドウボリュームと同じです。
テクスチャをフェッチしない専用の軽量シェーダーを割り当てて、ボリューム形状を
ステンシルバッファに 2回書きます。
(1) depth test かつ 表面、stencil +1
(2) depth test かつ 裏面、stencil -1
裏面と面面の差分が地面との交差面となります。上記の設定だと stencil が 0以外で
交差面です。
この条件を元に、同じ位置に描画モデルをレンダリングすれば合成ができます。
余談ですが、両面ステンシル機能がある GPU なら一度の描画で差分が求まります。
両面ステンシルが無くても両面マテリアルで代用可能で、例えばフレームバッファの
Alpha (DestAlpha) に加算で書き込むと一回で生成できます。
このとき描画頂点数は減りますがピクセル負荷は変わりません。
多くの GPU は Depth + Stencil の書き込みに特化したモードがあり、フィルレート
が倍になるように最適化されています。なので DestAlpha を使ってもあまりメリット
はありませんでした。
それにこのゲームは DestAlpha をすでに別の用途で利用していました。
●形状の変更
このゲームは表現上ステンシルバッファを多用しています。
8bit しかない stencil を bitmask して複数使い分けているところもあります。
・影
・オーラ
・地面や建物の破壊穴
もしゲーム画面か古い CGWORLD を見ることが出来るなら、穴の中にも形状に沿って影が
落ちていることがわかると思います。
でも描画しているモデルデータは平らなままです。
背景の影もシャドウボリュームを使っているのは、このように破壊によって動的に
地形が変わる可能性があるため。テクスチャや頂点への焼き込みだと、地形の変化に
対応できなくなります。
●負荷の相殺
破壊が進むと描画の負荷が増えて重くなりそうに見えますが実はそうでもありません。
このゲームは背景もキャラもすべてノーマルマップを使った重いシェーダーを適用
しています。最初から背景のピクセルが一番重い前提で負荷を計算しているので、
地形の描画面積を減らす処理は描画負荷の軽減につながります。
ノーマルマップを適用していない破壊の穴モデルは、数が増えれば増えるほど地形の
描画負荷を軽くすることになります。
ステンシルバッファへのレンダリングが増えますが相殺して軽くなります。
(でも頂点は軽くなりません。この判断は今思うと微妙だったかもしれません。)
同じようにキャラクタの描画負荷も相殺するのでアップになっても速度は変わりません。
半透明のエフェクトはまずいです。
●矛盾の解決
実際に組み込んでみると単純な表現でもさまざまな問題が生じます。
他の描画や表現と共存が必要だからで、1つ 1つ対策を施していきます。
・穴が開くと困る場所
破壊モデル合成時に、特殊な形状や半透明など矛盾が生じる場所があります。
あらかじめマテリアルの設定で描画しないように除外しています。
これは描画パスを分けることで実現しています。
・穴同士の重なり
Z test は別の判定に用いるので、描画の重なりの解決に Z バッファが使えません。
同じ位置にポリゴンが重なると矛盾が生じるので、描画位置を決める際に当たり判定を
行っています。必ず他の穴と重ならない位置に配置しています。
・地面の下だけ描画
破壊の穴形状は多少ずれても問題がないように球形をしています。
上半分が見えてしまわないように、地面の下に潜り込んだ部分だけ切り取るため
depth test を併用しています。Stencil がパスしてかつ、Z Fail が条件です。
地面の下にめり込んだ部分だけが描画されるようになります。
・depth 強制更新
ただ描画するだけでは不十分であることがわかります。
見た目と Z buffer に食い違いが生じるからです。
たとえば穴の上に置いた物や影が、地面の位置でまっすぐに切れてしまいます。
よって描画モデルを描き込むときに同時に depth を強制的に上書きします。
depth (Z buffer) が更新できれば、その後に描画する Shadow Volume も穴の形に
沿って落ちてくれます。
・遠くの穴が見えてしまう
地面が階層化している場所など、上の段の破壊穴と下の段の穴が重なって描画される
ことがあります。depth test の条件に Z Fail を設定しているということは、常に
遠くのオブジェクトが優先されることに等しいからです。
穴の中にさらに遠くの穴が見えてしまいます。
そこで穴モデル自体を Zソートして手前から描画し、同時にステンシルに 0 を書き
込んで消していきます。遠くの穴を描画する段階では depth test の Z Fail を
通っても、stencil テストに失敗するので描画されなくなります。
・地形との合成
上の問題を対処するためステンシルバッファを 0 で消してしまうと、今度はその
位置だけ地形を除外できなくなります。つまり穴の位置に上書きしてしまいます。
ステンシルをクリアする場合、別の条件で区別できる値を書き込まなければなりません。
元々ステンシル値の 0 または 0 以外で判定していたので思ったより簡単ではないです。
もしくは描画順番で何とかします。交差面を求めた後、地形の穴の中を描画する前に
先に地面を描画し、その後破壊穴モデルを描画すれば 0 クリアでも問題ありません。
・穴の上に重なるモデルの表示順番
地面に穴が開くはずの場所に他のモデルが置いてある場合も描画順番が問題となります。
例えばキャラが地面に立っていて、わずかに地面にめり込んでいる場合。(本当は
それが良くないのですが)
穴を開ける前の Z バッファで描画してしまうとめり込んだ部分が切れてしまいます。
だから地形に乗っているものは、穴を開けて Z バッファを更新したあとに描画する
ことになります。
・個数制限と消去
破壊の穴は同時に 30個描画しています。上限はただの定数値で、何らかの制限があった
わけではないです。これを超えた場合、視野に入っていないものから消しています。
●描画手順のまとめ
(1) 破壊可能部分の depth のみ
(2) 破壊穴モデルの交差判定
(3) 破壊穴の描画と depth 更新+識別可能な stencil で書き直す(要確認)
(4) 地形の上に乗っているモデル描画、不透明物一般
(5) 破壊不可能部分描画
(6) 破壊可能部分のカラー描画
(7) シャドウボリューム
(8) 影の合成
(9) 半透明地形の描画
(10) 半透明エフェクト
記憶だけで書いているので、おそらく異なっている部分や足りない部分があります。
これ以外にも特殊なエフェクトや表現があったので、実際はもっと描画手順が複雑です。
具体的に設定したステートの組み合わせなどは、おそらくもう一度実際に作ってみないと
わからないと思います。
それに今のハードならもっと簡単でしょう。ShaderModel 1.x の当時とは違うので、
別の表現方法も使えると思います。