FPSを作ってみる@wiki
09)
最終更新:
slice
-
view
(2011/09/30)
放置
例によって例の如く記録を放置してしまった.思い出せる範囲で書く.
前回書いた描画ソートはもちろん既に終わった.
エディットボックスのような内容が変化する文字列など予め頂点バッファに転送せずに描画する物は
すぐに描画命令を呼ばずにある程度メモリに溜めてから一気に処理するようにした.
と,これだけなら簡単そうに思えるが実際には頂点形式や使用するテクスチャが違ったりすると
バッファを結合できないのでその辺の判別方法とかが要る.
具体的には最初にオブジェクトから「描画タグ」と言う物を収集する.
描画タグには先程書いたような使用テクスチャ,頂点形式,(HLSLの)Techniqueやαブレンドの有無といったオブジェクトの描画ソートの材料になる変数が記載されていて,
これらとユーザーの提示した「ソート手順」を用いオブジェクトのソートを行う.
そしてソートの結果「一括描画出来る」と判断した範囲については一括描画,そうでない所は個別にD3DのDrawAPIを呼ぶ.
前回書いた描画ソートはもちろん既に終わった.
エディットボックスのような内容が変化する文字列など予め頂点バッファに転送せずに描画する物は
すぐに描画命令を呼ばずにある程度メモリに溜めてから一気に処理するようにした.
と,これだけなら簡単そうに思えるが実際には頂点形式や使用するテクスチャが違ったりすると
バッファを結合できないのでその辺の判別方法とかが要る.
具体的には最初にオブジェクトから「描画タグ」と言う物を収集する.
描画タグには先程書いたような使用テクスチャ,頂点形式,(HLSLの)Techniqueやαブレンドの有無といったオブジェクトの描画ソートの材料になる変数が記載されていて,
これらとユーザーの提示した「ソート手順」を用いオブジェクトのソートを行う.
そしてソートの結果「一括描画出来る」と判断した範囲については一括描画,そうでない所は個別にD3DのDrawAPIを呼ぶ.
1つのオブジェクトからは複数の描画タグを出力でき,またタグを取得する際に文字列の「キー」を指定する仕組みで
例えば"low"を渡せば遠距離用のローディティールモデル,"high"なら高精度なモデルを描画するよう
仕向ける事も出来る.
(というか別にオブジェクトと1対1にする必要性も無かったからそうなってるだけ)
例えば"low"を渡せば遠距離用のローディティールモデル,"high"なら高精度なモデルを描画するよう
仕向ける事も出来る.
(というか別にオブジェクトと1対1にする必要性も無かったからそうなってるだけ)
毎フレームタグを収集,ソートしてたら重いのではないか?
これについては感覚だがオブジェクトの数が50くらいまでなら問題ないだろうという判断.
大量のパーティクルや2Dシューティングなんかの弾幕を描画したいのなら
それ専用のクラスを作って纏めて管理するとか.
まぁ実際に速度で問題が出たらおいおい改良していくとして・・・
ここまでが上旬の話.
これについては感覚だがオブジェクトの数が50くらいまでなら問題ないだろうという判断.
大量のパーティクルや2Dシューティングなんかの弾幕を描画したいのなら
それ専用のクラスを作って纏めて管理するとか.
まぁ実際に速度で問題が出たらおいおい改良していくとして・・・
ここまでが上旬の話.
中旬はSVNの履歴を見るにカメラクラスの移植,
キー入力クラス,行列クラス,ベクトルクラス,テキストフォントキャッシュ周辺のリファクタリング
それと3D空間内でのテキスト描画などなど.
実を言うと自作エンジンをLuaメインに据えて作り直してから
まともに3D空間を扱ってなかったからちゃんとしました,という.
キー入力クラス,行列クラス,ベクトルクラス,テキストフォントキャッシュ周辺のリファクタリング
それと3D空間内でのテキスト描画などなど.
実を言うと自作エンジンをLuaメインに据えて作り直してから
まともに3D空間を扱ってなかったからちゃんとしました,という.
下旬に入ってからは「Debugビルドとはいえ,な~んか重いなあ・・・」という訳で
前々から気になっていたコリジョンの判定ルーチンを作り直し.
逐一コリジョンタグ(※1)を見て内部リストの展開の有無や衝突判定関数の呼び分けをする関係上
分岐を多用していて,これは計算時間もそうだがキャッシュに悪い.
そもそも双方のコリジョンタグを見た段階で手順は決まる物だから前処理で手順をリスト化するか
初回だけ計算で求めて以降は使い回すのが普通だろう.
前計算すると組み合わせが膨大になるのでここでは後者を選択した.
前々から気になっていたコリジョンの判定ルーチンを作り直し.
逐一コリジョンタグ(※1)を見て内部リストの展開の有無や衝突判定関数の呼び分けをする関係上
分岐を多用していて,これは計算時間もそうだがキャッシュに悪い.
そもそも双方のコリジョンタグを見た段階で手順は決まる物だから前処理で手順をリスト化するか
初回だけ計算で求めて以降は使い回すのが普通だろう.
前計算すると組み合わせが膨大になるのでここでは後者を選択した.
さてパフォーマンスは・・・と思ったら期待した程ではなかったのでここに来てプロファイラを導入(※2).
するとどうだろうか.当たり判定自体よりもLuaからのデータ転送に処理を食っているという・・
よく調べてみるとLuaのAPIを呼ぶ分には速いものの自作のLuaValue互換クラス(※3)が重い.
更に調べるとマルチスレッドに対応するためのリソースロック,アンロックを何かする度にしているので
これがネックという事がわかった.
かといってLua-APIを直接呼ぶのでは本末転倒,改善策を考える.
するとどうだろうか.当たり判定自体よりもLuaからのデータ転送に処理を食っているという・・
よく調べてみるとLuaのAPIを呼ぶ分には速いものの自作のLuaValue互換クラス(※3)が重い.
更に調べるとマルチスレッドに対応するためのリソースロック,アンロックを何かする度にしているので
これがネックという事がわかった.
かといってLua-APIを直接呼ぶのでは本末転倒,改善策を考える.
思案の末「マルチスレッド対応は必要な部分だけする」という至極真っ当な結果に.
単にラッパークラスとして使いたい場合にリソース管理だなんだの同期は無駄な訳で.
要するに「シングルスレッド動作で構わないクラスはロック・アンロックしないバージョンを使う」という事.
言葉にすりゃ簡単だが既にマルチスレッド対応で作ってしまった物を
じゃあ一部だけ速度重視のシングルスレッド動作させようかと思ってもなかなか面倒ではある.(が,やるしかない)
悪あがきとしてテンプレートを使ったポリシーで速度とコード量の両立化を計ってみたりとか.
単にラッパークラスとして使いたい場合にリソース管理だなんだの同期は無駄な訳で.
要するに「シングルスレッド動作で構わないクラスはロック・アンロックしないバージョンを使う」という事.
言葉にすりゃ簡単だが既にマルチスレッド対応で作ってしまった物を
じゃあ一部だけ速度重視のシングルスレッド動作させようかと思ってもなかなか面倒ではある.(が,やるしかない)
悪あがきとしてテンプレートを使ったポリシーで速度とコード量の両立化を計ってみたりとか.
今月はパフォーマンスアップのためのアレコレが多かった.
しかしあと2箇所程修正,改良すれば速度が懸念される部分は無くなるので一安心といったとこか.
しかしあと2箇所程修正,改良すれば速度が懸念される部分は無くなるので一安心といったとこか.
※1 ポリゴン,円柱等の物体の形状やリスト構造を表すID
※2 Game Programming Gems 1に記載されている簡素なアルゴリズムを,ちょこっとアレンジしたバージョン
※3 スタックベースのLua-APIをラップしてテーブルのアクセスを["item"]等C++風に記述できるようにしたクラスを
更にリソースハンドル管理,マルチスレッド対応にした物
※2 Game Programming Gems 1に記載されている簡素なアルゴリズムを,ちょこっとアレンジしたバージョン
※3 スタックベースのLua-APIをラップしてテーブルのアクセスを["item"]等C++風に記述できるようにしたクラスを
更にリソースハンドル管理,マルチスレッド対応にした物
(2011/09/05)
描画ソート
最近なんだかwikiが重いから更新する気も起きない・・・
更新の間が空くと何したかを忘れるからそれ用の時間を決めたい所だな.
更新の間が空くと何したかを忘れるからそれ用の時間を決めたい所だな.
さて.現在描画の最適化をしている.
この前はフォント描画をまとめてやる話をしたけど今度は本腰を入れた大がかりなエンジン改修.
実にエンジン本体クラス8割のコードを書き換え.
というのもTwitterAPIで試しに最大20件の呟きを取得し,その数だけ自前GUIのウィンドウに表示させたら思いの外重かったからだ.
Debugビルドとはいえこの程度で処理落ちとは情けない話ではないか.
原因はというと,描画にはすべてUP系の関数(つまり1つ矩形を描く度に頂点バッファをその都度確保,破棄している)を使う関係上
多大なオーバーヘッドが生じているのは想像に難くない.
丼勘定でウィンドウ1つにつき矩形3つ,枠が2つ,テキスト1つを要し
これが20個だと60, 40, 20回のDrawPrimitiveUP()が呼ばれている計算になる.
この前はフォント描画をまとめてやる話をしたけど今度は本腰を入れた大がかりなエンジン改修.
実にエンジン本体クラス8割のコードを書き換え.
というのもTwitterAPIで試しに最大20件の呟きを取得し,その数だけ自前GUIのウィンドウに表示させたら思いの外重かったからだ.
Debugビルドとはいえこの程度で処理落ちとは情けない話ではないか.
原因はというと,描画にはすべてUP系の関数(つまり1つ矩形を描く度に頂点バッファをその都度確保,破棄している)を使う関係上
多大なオーバーヘッドが生じているのは想像に難くない.
丼勘定でウィンドウ1つにつき矩形3つ,枠が2つ,テキスト1つを要し
これが20個だと60, 40, 20回のDrawPrimitiveUP()が呼ばれている計算になる.
恐らく今時のプロセッサなら1フレームに120回の呼び出しなんぞ余裕だろう.
が,UPなのでバッファの生成・破棄も120回ずつのおまけ付き.途端に厳しい.
そもそも何故UPかと言えば単に自分が勉強不足なだけで動的な頂点バッファ,インデックスバッファを利用したことがなかった為だ.
というわけでいつも通りMSDNを参照しつつ動的バッファの使い方やD3Dのパフォーマンスを引き出す方法を学んだ.
が,UPなのでバッファの生成・破棄も120回ずつのおまけ付き.途端に厳しい.
そもそも何故UPかと言えば単に自分が勉強不足なだけで動的な頂点バッファ,インデックスバッファを利用したことがなかった為だ.
というわけでいつも通りMSDNを参照しつつ動的バッファの使い方やD3Dのパフォーマンスを引き出す方法を学んだ.
パフォーマンスを引き出すと言えば大げさだが特に小難しい話ではない.ざっくりと説明するなら大まかに3つ.
- 「DrawPrimitive関数の呼び出し回数を抑える」
GPUはCPUと独立して動いているという事を念頭に置いた設計(D3DLOCK_DISCARDなどのロックのフラグを適切に選択)
- 「リソースの切り替えを最小限に」
GPUのキャッシュに影響
- 「不透明ポリゴンは手前から描画(Zクリップを期待)」
Zクリップされれば以降のピクセル処理が不要になる
基本的にオブジェクトの描画順を工夫するのだなという事は察しがつくと思う.
エンジンはオブジェクトの描画要求を受け取ったらすぐには描画せずにキューに貯めておき
描画の必要性が生じたらテクスチャやカメラからの距離をキーとしてソートし,まとめてD3Dにコマンドを送る.
ソート対象のキー,優先順位は予め定義されたソートアルゴリズムのインタフェースを配列として表現.
例えばまず距離でソートして,その内距離が10.0以内のオブジェクトを同位置として扱い更にテクスチャでソートをかけたいなら
エンジンはオブジェクトの描画要求を受け取ったらすぐには描画せずにキューに貯めておき
描画の必要性が生じたらテクスチャやカメラからの距離をキーとしてソートし,まとめてD3Dにコマンドを送る.
ソート対象のキー,優先順位は予め定義されたソートアルゴリズムのインタフェースを配列として表現.
例えばまず距離でソートして,その内距離が10.0以内のオブジェクトを同位置として扱い更にテクスチャでソートをかけたいなら
SetSortAlg(new IFSortDistance(true, 10.0f), new IFSortTexture());
とする.ちなみにこの関数は可変引数としてあり,任意の数のソートをかけられる寸法.
IFSortDistanceの第一引数は降順か昇順かのフラグである.
テクスチャのソートは同じ種類が隣に来るという点が大事なのであって降順だろうが何だろうが関係ないので引数無し.
頂点バッファとインデックスバッファについても以下同文.
IFSortDistanceの第一引数は降順か昇順かのフラグである.
テクスチャのソートは同じ種類が隣に来るという点が大事なのであって降順だろうが何だろうが関係ないので引数無し.
頂点バッファとインデックスバッファについても以下同文.
後はソートしたオブジェクトを普通に描画すれば冗長なステート変更はD3D内部で自動的に省略される(※1)ので処理の効率化が望める.
ううむ,言葉にするとそれだけだな.実装は仕様が二転三転して難航したのだが.
ううむ,言葉にするとそれだけだな.実装は仕様が二転三転して難航したのだが.
※1 ドキュメントに書いてあった.http://207.46.16.248/ja-jp/library/bb219721%28VS.85%29.aspx 「D3DCREATE_PUREDEVICE フラグの目的は何ですか」の項を参照.