Top > 制作記録 > FPSつくるよ!

FPSつくるよ!

随時更新
2007/ 8/15 掲載開始

制作日誌

相当端折っていますが、少しでも参考になる情報が含まれるようにがんばって書きます。日付の表記はYYYY/MM/DDです。

2013/ 6/30

[Cascading Shadow Map]

Cascading Shadow Mapによる影

例によって最新のデモプログラムをアップロードした。グラフィックス上の更新が主になる。

Cascading Shadow Map

前回実装した影を改善した。一番単純なシャドウマップ法(SM)は既に時代遅れで、カメラに近いところの影の品質が良くない。SM法の改善版として、透視シャドウマップ(PSM)やライト空間透視シャドウマップ(LiSPSM)というのが提案されたようだ。品質が良いと評判のもんしょの巣穴さんの過去記事を参考にLiSPSMを実装してみた。ところが、実装をミスしたのか、なぜかうまく行かない時がある。視線の角度によって、影の解像度が劇的に悪化、あるいはまったく表示されない、といった状態になってしまった。

問題が解決できなかったので、結局、一番単純なSM法に戻ってきた。今回は影品質改善のため、遠近で別のシャドウマップを作るカスケーディングシャドウマップ法(CSM)を使用した。カメラ付近で影解像度が上がればよい、という発想に愚直に答える手法だ。これほど単純な手法でも、現段階では上々と思える品質の影が出せる。SM+CSMは単純かつ直感的で、パラメータ調整がしやすいという利点もある。

アニメーション

今回から、移動時に専用のモーションを設定するようにした。今までは全力疾走していても手持ち武器は正面を向いたままという、現実ではあり得ない挙動になっていた。モーションデータ作成の手間が増えてしまったが、リアリティは増した。あと、移動中も全力疾走なのか、スタミナ切れなのかが腕の状態で分かるようになった。

このほか、単純ながらカメラ制御が入り、アニメーション中に頭を傾けるような挙動ができるようになった。M16を撃ちきった後リロードすると、頭を傾けてボルトの状態を確認するようになっている。ただし、むやみにカメラを傾ける挙動を入れると3D酔いを誘発する(実体験)ので気をつける必要がある。

はしご

今回、新しい構造として「はしご」を追加した。階段と違って垂直に移動できるので、マップ構造の設計に自由度が増える。マップ内では巨大煙突の裏にはしごの付いた建物を用意してある。

その他の変更

表現規制を改善し、設定が有効なら、死体の代わりに花を表示するようにした。また、遠くの音も聞こえるという、ゲーム的な音響空間を実現するため、実際よりも音源を近づけてやるようにした。さらに、危険物からAIが逃げる時は、人間と同じく、やや遅れてから逃げるようにした。

2012/10/16

手榴弾やクレイモアなどの投げ物を銃で撃つと、NULLポインタにアクセスして落ちるので、プログラムだけ差し替え

2012/10/14

[Shadow]

最新のデモプログラムをアップロードした。前回のものは修正版でさえWindows XPでは動かないらしい…今回のファイルでも多分改善していないように思う。マルチスレッド周りが原因に思えるのだが、十分なテストができていない。まだしばらくは機能実装を優先しておきたい。あと、前回までのコードでは、どうやらメッシュファイルのキャッシュに失敗していたようで、ロードに時間がかかっていた。今回は修正してあるので、2回目以降のロードは高速になっているはずだ。

今回、シャドウマップによる影を実装した。影を表現する手法としてはかなり単純な部類で、時代遅れの手法になる。実際、描画される影はかなりガタガタとしており、品質が悪い。将来的には改良された手法を導入して、なめらかな影を描きたい。

ベクトル画像

3Dモデルは表面にテクスチャ画像を張り付けることでリアリティが増す。実際みすぼらしいモデルでも画像でかなりごまかせる。一般にはテクスチャは、画像がピクセルの集合でできている、ラスタ画像になっている。しかし、ラスタ画像は拡大表示すると、テクスチャの粗が気になってしまう。現代的なゲームとしては、一般には、高解像度画像を使う、もしくは、そもそも粗が気にならない画像を使う、といった対策をする。しかし、高解像度画像は容量が大きい。粗が気にならない画像を作るには相当な技術とセンスが要る。

ゲーム中では精密な射撃のために、倍率の高い光学スコープが登場する。このスコープの照準線はくっきりしていることが望ましいが、ラスタ画像は高解像度にしてもボヤケが発生してしまう。こういった用途には、ベクトル画像が適している。ベクトル画像は端的に言えば描画手順を記録したもので、点Pから点Qへ線を引く等の手順が記録されている。Windowsで一番手軽に使えるベクトル画像はメタファイル(WMF/EMF)である。OSが読み込みをサポートしており手軽だ。しかし、OS標準のWMF/EMF描画ではアンチエイリアスが効かず、荒いピクセルが見えてしまう。アンチエイリアス能力のないGDIが描画するためである。

クロスプラットフォームのグラフィックスライブラリCairoを使えばアンチエイリアスをかけられるようだが、やりたいことに対してライブラリが大きすぎる。Windowsでは新しいグラフィックスAPIであるGDI+と新しいメタファイル形式EMF+を使えばアンチエイリアスのかかった描画が可能らしい。しかし、EMF+の作成は現状一筋縄ではいかないようである。こういった理由で、メタファイルは使いにくい。さらに、メタファイルにはキャンバスサイズが記録されていないようで、画像のサイズを取得すると描画領域のサイズが取得される。大きなテクスチャの真ん中に小さな照準線がある、という用途には使えない。

ベクトル画像としてはWebの世界ではSVG(Scalable Vector Graphics)が標準になっているそうだ。ファイル作成用のツールも多く、Adobe Illustratorのような商用の定番ツールの他、フリーのツールもある。私はInkscapeを使用している。

SVGファイルはXMLになっており、読み込みや解析も比較的容易だ。ただし、Windows+GDI+の環境ではSVG読み込みライブラリがない。そこで、読み込みライブラリSuzumeを作成した。SVGの機能全てには到底対応していないが、実用上十分な機能は持っている。今回の更新では、スコープはSuzumeによるベクトル画像で表現されている。

なお、3Dモデル自体も一種のベクトル画像である。平面的なモデルを作成すれば、画像代わりに使うこともできる。しかし、描画時に頂点を座標変換する3Dモデルでは、ワールドの中心から遠ざかるにつれて座標変換時の誤差が大きくなり、細い線が消えてしまうという問題が起きた。

モーション改善

前回まではAIの死亡時のモーションは、ランダム生成された物を使っていた。ある程度制約条件は設定してあったが、それでも関節があり得ない方向に曲がることが多かった。今回は、手作業で生成したモーションを使っている。この作業でボーンの位置が変わったので、キャラクターのモデルも、普通のテロリストの分は更新した。灰色の体力の高い方はまだ古いモデルなので、モーションがおかしくなっているが見逃してほしい。

2012/ 3/ 8

Windows XPでフリーズする原因が判明したので、修正版(fps_20120308_diff.zip,1.85MB,実行ファイルのみ)を用意した。前々回のファイルに上書きすれば動作するはずだ。

スレッドセーフ?

サウンド周りとマルチスレッドの相性の悪さがフリーズの原因だった。FPSでは足音や銃声など、効果音を頻繁に再生する。再生が終了した効果音はサウンド管理クラスから除去される。サウンド管理クラスでは、効果音が多くなっても処理が遅くならないことを期待して、OpenMPによるマルチスレッドで効果音の除去を処理していた。この方法はWindows 7上では問題がほとんど起きなかったが、Windows XPでフリーズを引き起こしてしまった。マルチスレッドで処理をするときは、スレッドセーフなライブラリを使う必要がある。DirectXは大体スレッドセーフだと思っていたのだが、XAudio2は違うのだろうか?

部分拡大の手法

前々回の部分拡大するスコープは、ステンシルバッファを使って描画している。半透明の部分(=レンズ)をステンシルバッファに描画しておき、レンズを通して見る部分を拡大して描画、レンズを通さない部分を通常通り描画する、という手順を踏んでいる。レンダリングを2回行うので、パフォーマンスは悪いが、リアリティは向上する。

2012/ 2/14

前回のデモプログラムですが、日本語パスが含まれていると失敗するようです。あと、Windows XPだと途中でフリーズすることがあるのでお気を付けください。将来的にも多分Windows XPはサポートしません。

2012/ 2/11

[Standard View]

通常時の視界

[Partial Zoom View]

ズーム時の視界

約一年ぶりに、最新のデモプログラムをアップした(fps_20120211.zip,33.2MB)。今回は、メッシュデータ、テクスチャやモーションなどを改善しているので、データが大きくなった。また、期間が空いた分多くの変更が加わっている。

まず、起動時にメニューが表示されるようになった。設定画面やマップ選択などのメニューがあり、一通りの機能を持たせてある。また、念のために表現規制も実装した。今のプログラムは「残虐な表現」ができるほどの表現力は持っていないが、血しぶきが出なくなるようにはなる。なお、前回の鳥のコードは、一応残してはあるが、コマンド経由で呼び出さない限り使えないようにしてある。

また、今後のことを考えて、多言語対応機能を実装した。特定の識別子を対応する言葉で置き換えていく方式で、対応する言葉がない場合は識別子がそのまま表示される。展開したフォルダのdata/languages/ja-JP等に言語ファイルがある。

グラフィックスに関して言えば、まずシェーダー対応が大きい。影はまだ実装していないが、バンプマップやPer-pixel Lightingは実装されている。設定から選択できる固定機能モードよりも、見た目がずっと良くなっていることが分かるはずだ。また、今後大量の画像を使うことになると予想されるため、リソース管理を改善した。これまでは、いわゆるどんぶり勘定で管理していた。つまり、重複ロードをしない、プログラム終了時にはリソースを解放する、だけの機能しかなかった。今回は、参照カウンタを実装し、リソースを即座に解放できるようにした。コンソールのcfg_gc_???あたりのコマンドで、不要なリソース解放を実行する際のリソース個数の基準値が指定できる。

AIも改良してあり、プレイヤーが物陰に隠れた際、一定の確率で制圧射撃を行うようになった。この時、付近の仲間に呼びかけて集団で制圧射撃する。制圧射撃とは、弾が当たらなくても良いので、相手の良そうな場所に弾を撃ち込み続けることである。プレイヤーはうかつに行動できなくなる上、薄い壁だと弾が貫通するため、脅威になる。プレイヤーは存分に苦しんでほしい。

最後に、新しい武器をいくつか追加した。特にこだわりポイントが多いのが今回の画像の光学照準器付きのM16A2である。M16A2はアメリカ軍で制式採用されていたライフルだが、射撃モードは単発および3点バーストという特徴がある。このライフルだが、3点バースト時に2発しか撃っていない段階で射撃を止めると、次の射撃時には1発しか弾が出ないという特徴がある。そこで、今回はこの"不完全な"バーストモードを再現した。また、バースト射撃を中断しても次回に本来の弾数でバースト射撃ができる、"完全な"バーストモードも用意した。こちらはG36Cに実装してある。射撃モード切替も実装されているので、触ってみてほしい。

しかしなんと言っても一番のこだわりポイントは、今回の画像の光学照準器である。ズーム機能を持った光学照準器をのぞき込むと、本来はレンズを通して見る部分だけが拡大され、それ以外の部分は拡大されないはずである。しかし、一般的なゲームでは、レンズを通さない部分も一緒に拡大されてしまう(おそらくはレンダリング負荷の都合だろう)。このプログラムでは、負荷よりもこだわりを追求し、画像のように、レンズを通した部分だけが拡大されるようにした。

2011/10/10

鳥怖い

突然ですが鳥怖い。表情ないし、嘴ついてるし。その上空を飛ぶ、これだけでも人類には勝ち目なさげなのに、時に泳いだり、さらに人語を真似したりと高機能すぎる。人類鳥に勝てない。まぁなんだかんだいっても鳥かわいいんだけど。でも群れたりするからやっぱり鳥怖い。

…というわけでこの動画を見てほしい。なお、動画中で投げているのは発炎筒のようなもので、鳥を引きつける役目をする。鳥は人間を襲うが、人間は鳥に反撃できない。こういう群れにおそわれるシチュエーションはある種のホラーで、個人的には気に入っている。

この鳥は、BOIDと呼ばれる群れシミュレーションで制御されている。ある場所で使うために、BOIDコードをFPSエンジンに組み込むことになった。既に目的は果たしたので、BOIDコードを無効化しても良いのだが、せっかくなので残しておくことにする。

テクスチャ圧縮

画像圧縮と言えば、JPEGやPNGが一般的だ。こういった形式は汎用的で、ディスク上の画像サイズをかなり小さく圧縮してくれるし、画質も良い。しかし、DirectXで表示させる場合、圧縮されている画像は展開され、非圧縮データとしてメモリ上に配置される。ディスク上でサイズが小さくても、大きな画像はGPU(ビデオカード)上のメモリを消費してしまうため、気軽には使えない。

GPU上で大きな画像を扱いたい場合、DirectXではブロック圧縮フォーマットと呼ばれるデータ形式を使う。DirectX 9ではDXTC(DXT1-DXT5)、DirectX 10以降ではBC(BC1-BC3)と呼ばれる形式である。この形式は不可逆圧縮で、4x4の16ピクセルを64bit/128bitにエンコードする。ざっと1/4から1/8にまで圧縮できることになる。逆に言えば、同じメモリ量で解像度を倍にできる。うれしいことに、メモリが小さくなるため、読み出しの負荷が下がり、速度向上につながるらしい。アルファ値の有無によってフォーマットを切替えると効率的だ。なお、この圧縮フォーマットは、DirectX SDKに含まれるDXTexツールで作成できる。

画像作成

前に煙画像生成プログラムを作ったと書いたが、Blender等の既存ソフトで生成させた方がよほど手っ取り早く、しかもクオリティも高いということに気付いた。いずれ差し替える。

AI速度改善

今のところ、AIの思考で一番重いのはノード探索だ。これまでは、ゲームの更新のスレッドとAIのスレッドで処理を分け、並列に処理をさせていたが、それでもAIのノード探索が遅かった。ノード探索が発生するのは主人公が敵に発見された場合だが、AIが一斉に思考を開始するため、プチフリーズ状態になっていた。そこで、一回の思考にかける時間を制限して、思考を複数回に分けることにした。

2011/ 2/18

[Hands with weapon]

武器と手

[Weapon holding position]

武器と持ち手の関係

[Markers on weapon]

マーカー

[Bones]

ボーン

最新のデモプログラムをアップした(fps_20110218.zip,19.9MB)。テクスチャデータはそれほど高解像度である必要がない事に気づいたので、大幅にデータが圧縮できた。また、zlibを使うようにしたので、画像データはgz圧縮された状態で保存されている。

今回は見た目の上で大きな変更がある。つまり、武器を持つプレイヤー自身の手が描画されるようになった。また、武器自身もアニメーションするようになり、リロード動作などで実際にマガジンを抜き取るような動作も実現できた。これまでも人体はアニメーションしていたので、その延長上にあると言ってしまえばそれまでなのだが、実際はもう少し複雑だ。

まず、武器を持たせる方法で悩んだ。

始めに思いついたのは、武器と人体を一緒にモデリングしてしまう方法だった。この方法だと、アニメーションコントローラの実装を変更する必要がない。しかし、敵にも武器を持たせることを考えると、(敵の種類)×(武器の種類)だけデータを作らなければならず、現実には不可能な方法だった。

次に考えたのは、武器と人体は別にモデリングして、武器を選択したときに、動的にフレームを統合して、その場で単一のメッシュにしてしまう方法だった。プログラム上で動的に武器と人体が一体化したモデルを生成するというアイデアだ。現実的な方法だと思ったので、ある程度まで実装したのだが、完成前にもっと簡単な方法を思いついた。

最終的に採用したのは、人体と武器を別々に扱う方法だった。まず、人体を適当にアニメーションさせる。次に、変形後の持ち手フレームの行列(位置と回転)を使って武器のオフセット行列を決定する。これで武器の位置が決まるので、最後に武器をアニメーションさせる。

武器を持たせる事が出来るようになったので、今度は武器のアニメーションを作成した。実際、2010年末〜2011年始は、Metasequoia+Keynoteで延々とアニメーション制作をやっていた。また、アニメーションを持ち込めるように、このページで前に載せたPythonスクリプトを改良して、アニメーションを全自動で変換できるようなスクリプトを作成した。

目の付け所が…

さて、実際に武器を持たせてみて分かったのだが、人体の本来あるべき位置で武器や腕を表示してしまうと、ほとんどが画面外に出てしまう。

よく分からなければ、試しに前を向いたまま、腕を真正面に伸ばしてみて、どのくらい見えるかやってみて欲しい。一般的なFPSではプレイヤーの腕は画面中に表示されているので、腕が頭から生えている、もしくは、目が喉に付いていることになる。

違和感なくゲーム画面をレンダリングするには、武器や腕を少し上に移動させる必要があった。

照準

アイアンサイト時の武器モーションも作った。ここで問題になるのが武器の向きだ。モーションを付けていると、どうしても武器の向きが視線の向きとずれてしまう。すると照準器の向きがずれるので、照準できなくなる。そもそも、視線に一致するように武器の場所を動かすことからして難しい(もちろん、武器の構えモーションを丁寧に作れば、手作業でも正確な照準をつくることはできるが、とてつもなく面倒だ)。

そこで、モデルを作るときに、マーカーを埋め込むようにした。武器を構えたモーションを作れば、あとはマーカーの位置から自動的に武器の向きと位置を決定する。これで、マーカーの位置さえあわせておけば、正確に照準機が視線と一致するように補正される。モーションデータが少々荒くても問題ない。

ちなみに、このマーカーは、アイアンサイトの他に、薬莢排出とマズルフラッシュのエフェクト位置を決めるのにも使う。

GPUとサンプルの罠

今のコードでは、スキンメッシュはDXSDKのSkinnedMeshサンプルを元に、インデックス付き頂点ブレンディング処理を行う部分だけを取り出して使っている。インデックス付き頂点ブレンディング処理は、GeForce系では固定機能ではハードウェア処理ができないために、インデックス無し頂点ブレンディング処理よりも遅くなる(実測で確認)。しかし、将来的にシェーダを用いたスキンメッシュアニメーションを行う事を考えると、インデックス付き頂点ブレンディング処理のほうが将来に繋がる。

さて、スキンメッシュアニメーションを実装したプログラムをいくつかの自分が使える環境で走らせてみた。そのとき、GeForce 8800GTを積んだWindows XPマシンで、なぜか武器のメッシュがロードできない問題に遭遇した。この環境でも、人体メッシュは正常に読み込める。しかし、武器メッシュの読み込みにおいては、ConvertToIndexedBlendedMesh()でD3DERR_INVALIDCALLが返ってくる。デバイス作成時には、D3DCREATE_MIXED_VERTEXPROCESSINGを指定してあるので、ハードウェアでの処理に失敗してもソフトウェアによる代替処理が走るはずなのだが…。

DirectX Caps ViewerでGPUの能力を確認すると、GeForce 8800GTは MaxVertexBlendMatrixIndex=0 となっている。MaxVertexBlendMatrixIndex=0は、ハードウェアでのインデックス付き頂点ブレンディング処理が実装されていない事を示す。

SkinnedMeshサンプルの、GenerateSkinnedMesh()に当たる部分に、ConvertToIndexedBlendedMesh()を呼ぶための前処理がある。ここでは、MaxVertexBlendMatrixIndexを元に、ソフトウェア処理かハードウェア処理かを切り替えている。

SkinnedMeshサンプル
if( d3dCaps.MaxVertexBlendMatrixIndex+1 < NumMaxFaceInfl ){
    // HW does not support indexed vertex blending. Use SW instead
    ソフトウェアでの代替処理
}else{
    ハードウェアでの処理
}

どうやらここが良くなかったようだ。NumMaxFaceInflは一つの面に影響するボーンの最大数だ。人体メッシュにおいては、基本的に関節、つまり複数のボーンが重なり合う場所が存在し、そこでブレンディング処理が起こるので、NumMaxFaceInfl>=2となる。一方、武器メッシュはなめらかな変形を起こす関節が存在しない。どのパーツも一本のボーンからしか影響を受けないので、NumMaxFaceInfl==1となる。

オリジナルのコードでは、d3dCaps.MaxVertexBlendMatrixIndex==0かつNumMaxFaceInfl==1のとき、ハードウェアでの処理に行って落ちる。おそらくは、分割パレット数(pMeshContainer->NumPaletteEntries)が0になってしまうために分割処理に失敗するのだと思う。

正しくは、次のようになる。

if( d3dCaps.MaxVertexBlendMatrixIndex==0 || d3dCaps.MaxVertexBlendMatrixIndex+1 < NumMaxFaceInfl ){
    // HW does not support indexed vertex blending. Use SW instead
    ソフトウェアでの代替処理
}else{
    ハードウェアでの処理
}

独自形式

X形式ファイルの読み込みが遅かったので、ここを改善するべく独自形式のメッシュフォーマットを作ってみた。頂点データや面のデータなどを単純にダンプしたもので、今のところは.custommeshの拡張子を持っている。一応読み込みは高速化されたようだ。また、ファイルもX形式よりは小さいようだ。

なお、スキンメッシュにはまだ対応していない。

2010/10/24

最新のデモプログラムをアップした(fps_20101024.7z,44.2MB)。容量の関係上7zでアップしたので、解凍できない場合は拙作のLhaForge等を使って欲しい。いずれデータ容量の節約法を考える必要がある。

今回は見た目以上に内部が大きく変更されている。

XAudio2

このプロジェクトで使っているライブラリのうち、一番古い物はDirectX Audio(Direct Sound/Music)のラッパライブラリだ。別のプログラムのために作っていた物で、2005年頃に原型が出来た。当時はDirectX 8用に作成していたが、時とともにDirectX 9に差し替えることになった(Audioは8と変わらないが)。その後Windows Vistaが登場して、Direct Sound/Musicが時代遅れとなってしまい、最近のDXSDKではコンパイルすら出来なくなってしまった。

Microsoft的にはXAudio2というAPIを使って欲しいらしい。しかしこのXAudio2ではMIDIがサポートされない。DirectX Audio/Direct MusicはMIDIが再生できたため、BGM等で便利だったのだが、MIDI自体はDirectXに頼らずに再生も可能なので、必要ならそちらで処理する事になるだろう。

従って、今回は音回りをXAudio2を使用した物に書き換えた。まだライブラリの完成度が低く、時々落ちてしまうことがある。

その他の変更点

ドアについて、引き戸はおそらくエレベータにも代用できるので、ゲームデザイン上の可能性が広がる。また、タクティカルリロードは、多くの場合、銃の薬室に弾を送り込む操作が不要になるため、通常のリロードよりも所要時間を短縮できる。今回の実装でそれを表現可能にした。

地雷は、AIが賢くなり、隠れたプレイヤーを追いかける動作が実現可能となったため、実装する意味が出てきた。単純に見える範囲のみ追いかけるAIでは、地雷を仕掛けてもタイマー式の爆弾とあまり変わらないためだ。

2010/ 9/ 6

先日の差し替え版です(fps_20100906.zip,57.7MB)。ランタイム不足で起動しなかった問題を修正しました。あと、ダメージインジケータの追加や処理の高速化などを行っています。

2010/9/27追記:一部CPUで起動しないようです

2010/ 9/ 4

[Nodes on map]

マップ上に配置されたノード

[Astar algorithm]

A*アルゴリズム

[Node generation algorithm]

ノード生成アルゴリズム

最新のデモプログラムをアップした(fps_2010904.zip,58.9MB)(動作不良で配布停止)。このプログラムは、Intel Parallel Studio 2011を用いてコンパイルした。どの環境でも動くように作成したつもりだが、動かなければ連絡して欲しい。

今回の目玉はAI強化である。他にも多数の改良を加えている。以下は、このプログラムのAIについての説明である。今回のAIはかなり賢くなった自信がある。普通にプレイしても、おそらく生き残る事は出来ないだろう。

経路探索

FPSのシングルプレイにあってマルチプレイに無いものと言えば、AIである。AIの出来はゲームの質に大きく関わるので、適度な強さを持った上質な物が求められる。特に無料FPSにおいてシングルプレイが少ない理由には、「マルチプレイの方が盛り上がるから」の他にも、「実用に耐えるAIを作成する事が難しいから」があるのではないかと思う。

上質なAIと言ったが、ゲームのデザインによって求められるAIは大きく異なる。敵が人間であれば、高度な戦術を使い、プレイヤーを飽きさせないように設計する事が多い。例として、初代Half Lifeは賢いAIを備えていると話題になったらしい。一方、敵がクリーチャー系である場合には、Serious Samの様に敵を大量に発生させ、AIは単純な行動に限定することもあるようである。

現在作業中のコードについては、敵を人間として設計している。つまりは、AIとしてある程度知性が求められる。求められる知性として一番大きい物は、おそらく経路探索だろう。実際のAIに求められる行動は、大体次のようになると思われる。

この行動だが、移動・回避のどちらの行動も、経路探索が鍵となっている。移動は最短経路探索であるし、回避は「敵から見えない位置への経路探索」で実現できるはずである。なお、戦況を判断して戦術を変える、といった高度な処理は経路探索だけでは出来ない。

今回、経路探索については「A*アルゴリズム」を用いた。このアルゴリズムについて、Wikipedia英語版の記事を元に動作確認用にテストプログラムを実装した(astar_gltest_20100904.zip, 55.9KB)。使用方法はコンソールに出力される。また、実行にはGLUTが必要である。このテストプログラムでのA*実装は、実際にゲーム上で使うには遅いため、データ構造の変更や探索結果のキャッシュといった最適化が必要である。

ノード生成

経路探索には、経由点となるノードが必要となる。このノードは、手作業で用意する事も多いようだが、私は面倒くさがりなので、プログラムによる自動生成を行った。

  1. マップの一番下の水平面上に等間隔に点を用意する
  2. 各点から真上に向かってレイを伸ばす
  3. レイがオブジェクトと交差する点を計算する
  4. 交差点の少し上にノードを生成する

生成したノードの近い物同士を連結する。ノード間に障害物がある場合にはノードを連結しない。ただし、破壊可能な障害物であれば、破壊時にノードを接続し直す。今回のプログラムでノードの処理を確認したいなら、コンソールを開いて(F11キー)、次のコマンドを入力して欲しい。途中まで入力してTabキーを押せばコマンドが補完される。

  1. noclip
  2. ignoreall
  3. d_rendernode

2010/ 8/ 5

スクリプト

規模の大きなゲームとなると大体何らかのスクリプトエンジンを持っている。ゲーム中のイベント処理やAI・BGM・エフェクトといったものの制御、場合によってはゲームシステムそのものを記述するために、スクリプト言語をゲームに組み込むのである。

スクリプト言語はコンパイルの必要が無い、もしくはコンパイルが非常に容易なため、C/C++等で記述されたコードと比べて変更しやすい。また、ゲーム中ではマップなどはデータとして与えられるが、データ特有の処理(あるボタンを押すとドアが開く等)はデータとともに提供し、ゲームエンジンとは独立して記述したい。

ゲームに組み込む目的のスクリプトエンジンとしては、PythonLuaSquirrelなどがよく使われているようである。他にもAngelScriptXtalといった物もあるらしい。今回は数ある言語の中からSquirrelを選んだ。理由は、以下の通り。

Squirrelは直接使うのではなく、C++との橋渡し(バインディング)を噛ませる事が多い。今回はSqPlusを使った。VM(Virtual Machine;スクリプトエンジンの実体)が一つしか持てないのは少し不満というか不安要素ではあるが、今のところ目立った不自由はしていない。

テクスチャと材質

ポリゴンの表面に模様をつけるためのテクスチャ。荒いポリゴンでもテクスチャが良ければ案外まともに見えてしまう。さて、そのテクスチャだが、このゲームエンジンではただの絵としてだけではなく、材質データとしても活用する事にした。

材質は、ここでは弾丸が当たったときの反応を決める属性という意味である。オブジェクトに弾丸が当たれば、貫通したり、砂埃が上がったり、弾痕が付いたりする。これを制御するのだ。密度や摩擦力といった物理的な属性を持たせる事も出来るが、まともな物理エンジンを実装していないので、今回は意味がない。

テクスチャに材質を割り当てるといっても、実装は単純で、テクスチャのファイル名と材質を関連付けているだけである。画像ファイルにデータを埋め込むわけではない。各オブジェクトにはデフォルトの材質を割り当ててあり、特定のテクスチャが割り当てられた面のみ、別の材質を使用する。

テクスチャと読み込み時間

データロードが遅いので調べてみると、テクスチャ読み込みが時間を食っている事が分かった。もう少し正確には、ミップマップ生成に時間がかかっているようだった。

DDS形式ファイルにはミップマップを持たせる事が出来るので、これを読み込むようにしてロードを高速化できた。また、ポリゴン制作を簡単にするため、そして既存のポリゴンデータを使い回すため、テクスチャの拡張子が.PNG等であっても、同名の.DDSがあればそちらを優先して読み込むようにした。

2010/ 3/31

[Rocket trail using generated smoke image]

生成した煙画像を使用したロケット砲の軌跡

煙画像の生成

最新のデモプログラムをアップした(fps_2010331.zip,34.6MB)。説明を見るのが面倒な人は直接プレイしてみて欲しい。

今回は煙画像の生成である。良い具合に使える画像は持ち合わせていないし、写真で撮ろうにも煙の立っている現場にそう出くわす物でもない。というわけで、これまでも炎の画像はフリーソフトで生成していたが、どうにも気に入らない。形が気に入らなかったり、制御できるパラメータが少なかったりするのだ。

先人は言った、「無いものは作れ」と。

そう言うわけで、煙生成のプログラムを作った。実行にはPythonPILnumpyが必要である。といっても、numpyは必須ではないが、使わない場合コードの修正が必要である。なお、今回使ったのは2.5.1だと思う。ちょっと古い。あと、numpyはscipyもセットで持っているとごく一部で幸せになる人がいるかも知れない。

現実の煙は大量の微細粒子が集まってモヤのように見えている。煙画像を生成するには、現実の煙と同じく、画像上に大量の粒子をまき散らせばよい。大量とは言っても、画像を適当に埋め尽くすぐらいの量でよい。適当に粒子が動けば煙が揺れ動くように見える。こういうプログラムは一般にはパーティクルジェネレータなどと言うらしい。

実際にこのコード(気分で粒子の動き方などの修正を加えて)を使って生成した画像を、上記のデモプログラム中で使用している。

このほか、上記デモプログラムでは前回から大量の更新を加えている。多くは処理速度の向上とゲーム性の向上である。他に、新武器などを追加している。

Python信者の戯れ言

こういうちょっとした補助プログラムや使い捨てプログラムには、スクリプト言語が非常に役に立つ。特にPythonには優秀なライブラリが豊富で、それなりの処理速度と安定性を備えている。覚えて損はない、と言うか覚えておかないと損をすると言えよう。

まぁ、私も偉そうに言う割に単純な処理にしか使ってないのだが…。

2010/ 2/15

[AABB for a triangle]

三角形を包むAABB

判定高速化:球の判定

現在のコードだと、手榴弾や補給アイテムは、当たり判定を球で表現している。この球は2008/3/4の記事で"球の中心から、球に内接する正20面体の頂点に向かって伸ばしたレイを使っている"と書いたとおり、多数のレイを使っている。が、レイの判定は少々重い上、球が大きくなると、どうしても判定に抜けが出る。レイをいくら増やしたところでこれは変わらない(2008/10/15の記事も参照)。

よくよく考えてみれば、ポリゴンは三角形の集まりに過ぎない。ポリゴンと球の判定は、三角形と球の当たり判定を繰り返せばよいという事になる。

三角形とレイの当たり判定なら、DirectXのPickサンプルにコードが付いてくるので、これを改造すればよい。具体的には、球の中心から、三角形面に垂直なベクトルを伸ばしてやればよい。元のコードであれば、レイの向きがどうだとか面倒くさい判定をしていたが、こういった物が無くなる分、むしろコードは(ほんの少しだけ)簡単になる。

三角形面に対して垂直であるという仮定を置いたので、実は尖った辺(立方体の辺など)に斜めから球を当てると、すり抜けてしまう事がある。これを防ぐためには、面だけでなく辺に対しても衝突判定を行えばよい。こういった方針で実装したところ、処理速度が向上した。また、以前見られた、手榴弾がポリゴンに引っかかるといった非現実的な挙動も解消された。

判定高速化:人同士の判定

人同士の衝突判定は、これまで各人の判定フレームに対して、判定レイを飛ばす事で行っていた。しかし、あまりに遅いので、正確さを犠牲にして判定を高速化する。簡単な話である。人を円柱と見なせばよいのだ。「この円柱は必ず地面に垂直である」と仮定してやると、「水平面上での円の衝突(2次元判定)」+「高さ方向の重なり判定(1次元判定)」という単純な判定に持ち込むことができる。これでかなり高速化できる。

判定高速化:レイの判定

さて、問題のレイである。地形と人の衝突判定や、FPSに欠かせない、弾丸の当たり判定など、未だレイを使う部分は多い。

これまではポリゴンとレイの当たり判定は、ポリゴン全体を包むAABBで荒狩りをした後、DirectXのD3DXIntersectに丸投げしていた。

D3DXIntersectは元々「無限に伸びるレイ」とポリゴンの判定を行っている。一方、今のコードで欲しいのは「それなりに短いレイ」とポリゴンの判定である。長さが無限か有限かという違いが重要で、長さが有限であれば、レイ自体をAABBで囲えるようになる。ポリゴン内の各三角形についてもAABBを作る事ができるので、レイと三角形の判定についても、AABB判定による荒狩りができるようになる。あらかじめポリゴンにAABBツリーと呼ばれるAABBの階層構造(8分木と使い方は似ている)を作る事で、この判定はさらに高速化できる。

AABBツリーはボトムアップ式に作成している。手順としては次のようになる。

  1. 一番詳細なデータとして、各ポリゴンを包むAABBを生成する
  2. 荒いデータを、なるべく近い物同士でペアを作る(適当でよい)
  3. ペア同士でAABBを結合する
  4. できたデータを元に、結合作業を繰り返す

これでポリゴンとの判定が高速化できる。

2009/ 7/27

アイアンサイト始めました

[Uzi Iron Sight][AK47 Iron Sight]

アイアンサイト(Uzi/AK47)

市販のFPSだとよく、スコープの付いてない普通の銃の照準(アイアンサイト)をのぞきこむことで命中精度が上がるシステムが付いています。まぁ有名どころでもアイアンサイトが無いやつもありますが、MOD導入で出来るようになったりしますね。

まぁそんなわけで、アイアンサイト無いと寂しいと感じたため、急遽作成しました。あと、いくつかコマンド追加とかバグ修正とかやってます。ダウンロードはこちらから(fps_20090727.zip,28.7MB)

2009/ 7/ 7

ファイル差し替えときますね(fps_20090707.zip,28.7MB)。Vista以外だと起動しないらしいですが、多分マルチスレッドでグラフィックカードにデータを送っていたのが原因かと思います。修正しておきました。あと、敵AIが若干賢くなり、描画も若干速くなったはずです。

2009/ 7/ 4

[デモ画面]

デモ画面

とりあえず技術デモ版置いときますね(fps_20090704.zip,28.7MB)。未完成品なので再配布禁止です。詳しくは同梱のreadme.txtを読んでください。

2009/ 4/21

「骨」/「ボーン」を持ったメッシュをスキンメッシュと呼ぶ。このスキンメッシュのボーンの関節に角度を与えてやると、メッシュが変形し、変形をアニメーションさせることで人体モデルなら歩いているようにも見せることが出来る。

さて、この関節角データであるが、このプロジェクトではクォータニオンを使用してアニメーションコントローラを自作したため、DirectX標準のアニメーションスキンメッシュのデータは使えない。そのため、別の方法で関節角を与える必要がある。これまではXYZ各軸まわりの角度をテキストエディタで手書きしていた。当然すごく効率が悪い。てかやってらんない。

さて、この前書いたKeynoteは拡張子.mqxのファイルに独自に変形データを保持しているらしいということが分かった。ファイルを見てみると、中身は普通のXMLで、うれしいことにクォータニオンがそのままの形で記述されていることが分かった。このデータを取り出してやれば、Keynoteを自前のアニメーションコントローラ用のエディタとして流用できる。

座標系をMetasequoia系からDirectX系に変換する([x,y,z,w] → [x,y,-z,-w])必要があったものの、ほぼ流用できることが分かった。

そんなわけで、変換用のPythonスクリプトを書いてみた。とりあえず自分用。変形のない単位クォータニオンはスキップする必要があるが、端折っているので注意。

Hide Source Codeソースコードを隠す Display Source Codeソースコードを表示する
import sys
from xml.dom import minidom,Node

#Metasequoia Quaternion => DirectX Quaternion
def convert(metaQuat):
    return (metaQuat[0],metaQuat[1],-metaQuat[2],-metaQuat[3])

def parseKeyFrame(anim,f):
    for t in anim.childNodes:
        if t.nodeType==Node.ELEMENT_NODE:
            name=t.getAttribute("name")
            q=t.getElementsByTagName("q")
            if len(q)<=0:
                continue
            q=q[0]
            quat=map(float,q.childNodes[0].nodeValue.split())
            f.write("[%s]\n"%(name,))
            f.write("%f,%f,%f,%f\n\n"%convert(quat))


def doProcess(fname):
    doc=minidom.parse(fname)
    f=open(fname+".txt","w")

    root=doc.getElementsByTagName("MetasequoiaDocument")[0]
    root=root.getElementsByTagName("Plugin.BD1224DB.0000002C")[0]
    root=root.getElementsByTagName("XSFormat")[0]
    root=root.getElementsByTagName("XSAnimationContainer")[0]
    animSet=root.getElementsByTagName("XSAnimationSet")

    for key in animSet:
        f.write("---%s---\n"%key.getAttribute("name"))
        for anim in key.childNodes:
            if anim.nodeType==Node.ELEMENT_NODE:
                parseKeyFrame(anim,f)
    f.close()


if len(sys.argv)>=2:
    for t in sys.argv[1:]:
        doProcess(t)

2009/ 4/ 8

[人体モデル]

人体モデル

人体モデル作成中。前みたいなマネキンじゃなくて、もう少しリアルなやつ。とはいえ、自分で1から作り上げるのは技量からというかどう考えても無理なので、MakeHumanで作ったモデルをベースに、Metasequoia + Keynoteで作成している。

ベースにちゃんとしたモデルを使っているので、出来たモデルは私が作ったにしてはそれなりに人の形を保っている。が、テクスチャがよく分からない。UVを展開して素人の塗り絵をやってみた。顔を描ける気がしないのでマスクで隠し、適当に汚れっぽい物を塗って出来たのが写真の通り。

とりあえず出来たからデータを突っ込んでみる…ってあらら?テクスチャがおかしいよ!

いろいろいじってたら直ったようだ。出力時に、オブジェクトがボーンより後に来ていたのが問題だったのかなぁ。

あとはアニメーション作成か…

[乱れた人体モデル]

テクスチャなど乱れ

2009/ 2/22

[Console]

コンソール

[Screen]

ゲーム画面

FPSでおなじみのコンソールが出来た。Windowsユーザーにはコマンドプロンプトといえばわかりやすいかと思う。FPSプレイヤーにとってはチートを含む各種コマンドを入れるためのインターフェイスとしておなじみの奴だ。

機能は限定的で、今のところシングルバイト文字でしか動作を確認していないが、簡単にヒストリー機能などもつけてみた。いくつかコマンドを追加してみたが、思ったよりデバッグに役立つ。noclipも作ったので、建物が重なっていないかなどを、目視で確認できる。

あと、サウンドについて。今サウンド再生のためにDirectX Audioを使っているが、デフォルトのパラメータのままだと、減衰が強すぎるのか、音源から少し離れるとすぐに音が聞こえなくなってしまう。

ということで、減衰係数をいじってみた。ゲーム中では周囲の状況を知るためにも音の情報は重要で、不自然であっても遠くの音が聞こえる方がプレイヤーには有利になる。

実際に遊べるプログラムは、いまはまだデータを整理・作成している段階なので、それが済んだらテスト版を公開したいと思っている。

2008/11/21

テクセルとピクセル

[文字がぼやけている]

文字がぼやけている

[文字はぼやけていない]

文字はぼやけていない

DirectXで2D描画をするときは、一般にテクスチャを座標変換済みポリゴンに貼り付け、表示を行う。スクリーン座標で直接指定できるため、結構便利なのである。が、このように描画すると、上側の図のようにテクスチャ(この場合は文字が書いてある)が妙にぼやけて表示されるときがある。これはどうやらテクスチャのテクセル(画素)と2D描画で使うスクリーン上の頂点の座標がずれているのが原因らしい。詳しい原理は「テクセルとピクセル間の直接マッピング - Google 検索」からたどれるMSDNの記事にある。

記事にあるとおり、ポリゴンの頂点座標をX,Yともに-0.5fしてやると、下側の図のように正しく表示されるようになった。

2008/11/8

Lua使いましょうか

一般にFPSにおいてはスイッチを押すとベルが鳴るとか、特定の領域に踏み込むと敵が沸いてくるとかいった、何かしらのインタラクティブな反応がある。そして、その制御は一般に何らかのスクリプト言語を使って行われる。また、場合によっては、スクリプトの制御対象はゲームロジックにまで拡大されることもある。

今制作中のFPSについても、同じようにスクリプトで制御を行いたいと思っている。ただまぁ、自分で言語を作る、あるいはインタプリタを実装するといったことは愚かなことなので、既存の言語を使うことにする。

そして今回は、スクリプト言語にLuaを採用することにした。組み込み用スクリプトとしては有名で、実績も豊富なようだ。ただ、C/C++との連携でスタック操作が面倒そうなので、そこをうまく遮蔽してもらえるようにもう一人のプログラマに頼んでおいた。さて、完成まで気合い込めて行かねば。

2008/10/15

爆発物の実現

[258本のレイによる爆発物の表現]

258本のレイによる爆発物の表現

前にも述べたように、現在の判定系はレイを多用している。手榴弾などの爆発物もその例に漏れず、爆発を起こした瞬間に全方向に「爆風」を表すたくさんのレイを放出するようにしていた。今までは。

この方式の良いところは、判定系はレイの処理をしさえすればよいので、遮蔽物の処理やダメージの距離による減衰を特別に考慮しなくて済む点だ。「破片」がダメージを与えるタイプの爆発物は、現実にかなり近い状態で再現できる。

この方式の悪いところは、半径が大きな爆発には対処できない点だ。爆心地から遠ざかるにつれ、レイの密度は飛躍的に下がっていく。その結果、爆心地からの距離が同じにもかかわらず、ダメージを受ける部分と受けない部分が出てきてしまい、しかも無傷の領域が飛躍的に増えてしまう。図のように258本のレイを用いた場合でさえ、十分な結果が得られなかった。

この問題点が許容できないと思われたので、爆心地からの距離を用いる実装に変更した。「爆風」がダメージを与えるタイプの爆発物がイメージ的に近い。

この方式には遮蔽物の表現が面倒だという問題点がある。今回は以下のように解決した。

対物体の判定が甘いが、「振動で壊れた」と見なせば許容できるとして、とりあえずOKとする。ゲーム内容により重要な位置を占める対生物判定は、満足できる精度が得られた。今は自分一人でグレネードジャンプをして遊んでいる。

2008/9/28

タイトルとシナリオが固まりました。それに伴って武器類もほぼ決まりました。ただストーリー上エイリアンのようなあまり変わった敵を出せないのが残念ですが。

武器は一般的なFPSと同様に、鈍器/拳銃/SMG/アサルトライフル/スナイパーライフル/手榴弾/軽機関銃/ショットガンぐらいになります。

ところで、最近メインで使ってるGeForce 8600 GTの調子がますます悪くなってきました。しょっちゅうフリーズし、最近はVistaでブルースクリーンに遭遇するようになりました。原因は特定できていませんが、ハードウェア側の問題の臭いがするので、もしかすると買い換えなきゃいけないかもしれません。

2008/9/10

今回は現状報告。ここ2-3ヶ月の間に、大まかに次にあげるようなところの実装を進めました。

前と比べて劇的に変わったところは無く、FPSに必要な要素を少しずつ骨格に肉付けしていく感じだった。

AIはいまのところごく簡単なもので、散漫-警戒-興奮の3状態を「感情」として持っている。索敵は基本的に単純に周りを見回すだけで、物音に対して反応するようにもなっている。ちなみに、AIからプレイヤーに対する可視判定は、プレイヤーの体の回り数ヶ所に可視判定ポイントを設置し、AIの目から判定ポイントにレイを飛ばして行っている。

カルチャーショック

このページの古い部分を読んでも推定できるように、現在作成中のゲームのエンジンはレイを重要な要素として実装している。特に、判定系のうち、正確な判定が必要な部分は全てレイ(D3DXIntersect)をベースとして実装している。なお、体積のある物は基本的に「いがぐり」/「たわし」のようにレイを用意している。マシンパワーによるのかもしれないが、積極的に荒狩りをしているので、案外パフォーマンスは悪くない。と思う。

ただ、これは自分で作ったテスト用データについての話なので、例の3D職人に作ってもらっているデータで試すとどうなるのかやってみた。ちなみに彼はMaya使いだ。

データをOBJ形式で貰ったので、MetasequoiaでXファイルに変換する。が、データのインポートからして異常に遅い…。データを見てみると、頂点数が28万もあった。こちらとしては2〜3桁ほど少ない頂点数のデータしか見たことが無かったので、軽い衝撃を受けた。

ただこれは3D屋さんが悪いのではなく、単にプログラマとデザイナの文化の違いが出てきただけだと思われる。彼もゲームを作るときにはポリゴン数に気を遣うということは知っていたし、頂点数を減らすだけなら自分にも多分出来ることなので問題はない。

今回はとりあえず頂点数を1万弱まで減らしたデータで試した。マップ上に自分を含めて4体のRunningBotを置いたところ、約30フレーム/秒の速度になった。最低でも40フレーム/秒は欲しいところだが、このデータ量でこの速度なら現状問題ないだろう。なにより、今回のデータはマップ上の全ての物が1つのオブジェクトとしてエクスポートされている。オブジェクトごとに正しくデータ分割がしてあれば、既に空間分割木やプログレッシブメッシュなどを実装してあるので、より軽快に動くだろう。

2008/6/22

[スキンメッシュのボーンから作った境界ボックス]

スキンメッシュのボーンから作った境界ボックス

前回の更新からずいぶん時間が経ってる…。困ったもんです。大学が忙しいんですが、それ以上に更新するほど大したことをやっていないというか。プログラムしなかった日はほとんど無いはずなんですけどね。作業対象がFPSじゃないだけで。そんなわけでだいぶん前に出来ていたネタでお茶を濁すことにします。

スキンメッシュとの当たり判定

普通のオブジェクトと当たり判定を取るならD3DXIntersectで楽々。でもFPSでは人体など変形するオブジェクトとの当たり判定も取らないといけない。

「変形するオブジェクトの変形を無視して、固定の判定ボックスを使う」、これは不正確だけど、ゲームの種類によっては有効。でも今回はパス。

変形するオブジェクトはたいていスキンメッシュで表現されている。DirectX SDKのSkinnedMeshサンプルの「Software」モードの実装では、UpdateSkinnedMesh関数でボーン変形適用後のメッシュを得ているようなので、こいつに対してD3DXIntersectをかけてやれば当たり判定は出来そうな気がする(試していません)。これが一番正確だけど、遅そうな気がするのでパス。そもそも頂点シェーダーでスキンメッシュを実装する時代にCPUで頂点処理するのが嫌な感じ。

そこで、それなりに正確で、それなりに速そうな方法として、「スキンメッシュの各パーツごとに境界ボックスを作って、それと当たり判定を取る」方式を採用。なにより、この方法だと部位ダメージ制(ヘッドショットとか)も簡単に実装できる。元ネタはこちら、「電波の缶詰HP」の「ウマイハナシ」より「OBBで衝突判定な話・後編」。

ちょっと困ったバグがあるので修正記事をお待ち下さい(04/12/27)としてばっさりDELされちゃってますが、ファイルへのリンクは生きてます(少なくともコードを書いた時点では生きてました)。で、ソースを参考にしつつ、面倒なOBBをやめてAABBで実装してみた。実用的な面で問題はなさそうなので、このまま使います。

以下がコード。スキンメッシュ読み込みクラスを丸ごと載せてもわかりにくいだけだし、なによりまだ公開できるレベルじゃないので、重要な部分だけ載せます。当たり前な処理も言葉だけで済ませてます。

Hide Source Codeソースコードを隠す Display Source Codeソースコードを表示する

長ったらしいのでJavaScriptなんて使って折りたたんでみました。

#define MAKE_MAX(x,y) (x=fmax(x,y))
#define MAKE_MIN(x,y) (x=fmin(x,y))

struct VERTEXWEIGHT{
    DWORD dwBone;
    float fWeight;
};

struct MINMAX{
    D3DXVECTOR3 vMax;
    D3DXVECTOR3 vMin;
};

DWORD dwNumVertices=頂点の数;
DWORD dwNumBones=ボーンの数;

//頂点の配列
std::vector<D3DXVECTOR3> vertexArray(dwNumVertices);
ここで頂点バッファから頂点座標を取得する

//頂点ごとに、その頂点にもっとも強い影響を与えるボーン番号を登録する
std::vector<VERTEXWEIGHT> vertexWeights(dwNumVertices);
for(DWORD idx=0;idx<dwNumVertices;idx++){
    vertexWeights[idx].dwBone=-1;
    vertexWeights[idx].fWeight=-1.0f;
}

for(DWORD dwBoneIdx=0;dwBoneIdx<dwNumBones;dwBoneIdx++){
    //ボーンが影響を与える頂点数を取得
    DWORD dwInfluences=pSkinningInfo->GetNumBoneInfluences(dwBoneIdx);
    if(dwInfluences){
        std::vector<DWORD> vertexIdxArray(dwInfluences);
        std::vector<float> weightArray(dwInfluences);

        //各ボーンが影響を与える頂点と、その重み付けを得る
        pSkinningInfo->GetBoneInfluence(dwBoneIdx,&vertexIdxArray[0],&weightArray[0]);

        //重み情報を保存
        for(DWORD idx=0;idx<dwInfluences;idx++){
            float fWeight=weightArray[idx];
            DWORD vertexIdx=vertexIdxArray[idx];
            //重み情報を更新
            if(vertexWeights[vertexIdx].fWeight<fWeight){
                vertexWeights[vertexIdx].fWeight=fWeight;
                vertexWeights[vertexIdx].dwBone=dwBoneIdx;
            }
        }
    }
}

//各フレームの境界ボックス
std::vector<MINMAX> frameBBArray(dwNumBones);
for(DWORD idx=0;idx<dwNumBones;idx++){
    frameBBArray[idx].vMax=D3DXVECTOR3(FLT_MIN,FLT_MIN,FLT_MIN);
    frameBBArray[idx].vMin=D3DXVECTOR3(FLT_MAX,FLT_MAX,FLT_MAX);
}

//境界ボックスを更新
for(DWORD idx=0;idx<dwNumVertices;idx++){
    DWORD boneIdx=vertexWeights[idx].dwBone;
    if(boneIdx==-1)continue;

    //ボーンローカル座標に変換
    D3DXVECTOR3 vLocal;
    D3DXVec3TransformCoord( &vLocal, &vertexArray[idx], pSkinningInfo->GetBoneOffsetMatrix(boneIdx));

    MAKE_MAX(frameBBArray[boneIdx].vMax.x,vLocal.x);
    MAKE_MAX(frameBBArray[boneIdx].vMax.y,vLocal.y);
    MAKE_MAX(frameBBArray[boneIdx].vMax.z,vLocal.z);

    MAKE_MIN(frameBBArray[boneIdx].vMin.x,vLocal.x);
    MAKE_MIN(frameBBArray[boneIdx].vMin.y,vLocal.y);
    MAKE_MIN(frameBBArray[boneIdx].vMin.z,vLocal.z);
}

//------------------
// つなぎ目を埋める
//------------------
DWORD dwNumFaces=pMeshContainer->pOrigMesh->GetNumFaces();
std::vector<DWORD> indexArray;
ここでインデックスバッファから面の頂点インデックスをコピー

//フレーム同士の境界をまたぐポリゴンを探し、重心座標を計算する
for(DWORD faceIdx=0;faceIdx<dwNumFaces;faceIdx++){
    VERTEXWEIGHT w[3];
    w[0]=vertexWeights[indexArray[faceIdx*3+0]];
    w[1]=vertexWeights[indexArray[faceIdx*3+1]];
    w[2]=vertexWeights[indexArray[faceIdx*3+2]];

    if(w[0].dwBone!=w[1].dwBone||w[1].dwBone!=w[2].dwBone){
        D3DXVECTOR3 v0(vertexArray[indexArray[faceIdx*3+0]]);
        D3DXVECTOR3 v1(vertexArray[indexArray[faceIdx*3+1]]);
        D3DXVECTOR3 v2(vertexArray[indexArray[faceIdx*3+2]]);

        //重心
        D3DXVECTOR3 _vMiddle((v0+v1+v2)/3.0f);

        //重心で境界ボックスを更新
        for(int i=0;i<3;i++){
            DWORD boneIdx=w[i].dwBone;
            if(boneIdx==-1)continue;
            //ボーンローカル座標に変換
            D3DXVECTOR3 vMiddle;
            D3DXVec3TransformCoord( &vMiddle, &_vMiddle, pSkinningInfo->GetBoneOffsetMatrix(boneIdx));

            MAKE_MAX(frameBBArray[boneIdx].vMax.x,vMiddle.x);
            MAKE_MAX(frameBBArray[boneIdx].vMax.y,vMiddle.y);
            MAKE_MAX(frameBBArray[boneIdx].vMax.z,vMiddle.z);

            MAKE_MIN(frameBBArray[boneIdx].vMin.x,vMiddle.x);
            MAKE_MIN(frameBBArray[boneIdx].vMin.y,vMiddle.y);
            MAKE_MIN(frameBBArray[boneIdx].vMin.z,vMiddle.z);
        }
    }
}


//最後にフレームに境界ボックスを設定
for(DWORD idx=0;idx<dwNumBones;idx++){
    //情報を適用
    D3DXFRAME_DERIVED* lpFrame=(D3DXFRAME_DERIVED*)D3DXFrameFind(m_lpFrameRoot,pSkinningInfo->GetBoneName(idx));
    ASSERT(lpFrame);

    if( (frameBBArray[idx].vMax.x<=frameBBArray[idx].vMin.x)||
        (frameBBArray[idx].vMax.y<=frameBBArray[idx].vMin.y)||
        (frameBBArray[idx].vMax.z<=frameBBArray[idx].vMin.z)){
        //体積が0なので無効な境界ボックス
        lpFrame->bValidHitBox=false;
    }else{
        lpFrame->vMax=frameBBArray[idx].vMax;
        lpFrame->vMin=frameBBArray[idx].vMin;

        lpFrame->bValidHitBox=true;
    }
}

2008/4/13

[アニメーションのテスト]

アニメーションのテスト

スキンメッシュによるアニメーションを実装中。DirectX SDK付属のTiny.xをアニメーションさせた画像を掲載している。首を傾けている様子が分かるだろうか?

SDKサンプルのSkinnedMeshや各種ページで書かれているアニメーションはたいていデザイナがメッシュにアニメーションデータを埋め込んでいることを前提として書かれている。最近はID3DXAnimationControllerが面倒なアニメーション制御もやってくれるらしい。しかし、FPSで使う場合、足の動きだけならともかく、「視線の角度にあわせて上体を傾ける」ことが必要となる。そういった処理は標準で出来るようには見えなかったので、今回はアニメーション制御を自前で実装し、アニメーションデータは外部のファイルから読み込むようにした。

アニメーション制御の核はクォータニオンで、キーフレーム間を線形補完(D3DXQuaternionSlerp)している。補完後のD3DXQUATERNIONをD3DXMATRIXに変換し、SkinnedMeshで言うところのD3DXFRAME_DERIVED::CombinedTransformationMatrixにうまく格納してやることで実装した。

今回自前でアニメーション制御を実装したことで、モデル作成者はモデルにボーンを埋め込むだけで良くなり、アニメーション作成を別の人間に割り振ることが出来るようになった。

2008/3/4

剛体球シミュレーション

[剛体球シミュレーションのスクリーンショット]

剛体球シミュレーションのスクリーンショット

剛体球シミュレーションが出来上がったので、動画をアップ(WMV,4.55MB)。音はタイミングがズレてしまうのでカットしてあります。動画の中では武器から弾の代わりに剛体球が飛び出すようになってます。

シミュレーション自体は簡易的な物で、回転を考慮していない。そのため反射の処理は簡単なベクトル演算で書ける。ところが、いざ実用に移すとなるとなかなか面倒。面の法線を計算しようとしたら、2頂点が重なっている三角形が出てきてしまい、これの例外処理に手間取った。結局、正面からの衝突と見なして反射させることに。

なお、衝突面の検出には、球の中心から、球に内接する正20面体の頂点に向かって伸ばしたレイを使っている。また、計算を高速化するために、静止した球には当たり判定を行っていない。

で、下のコードは反射処理の部分。

//十分小さいと見なせる数
const float EPSILON=1.0e-6f;

//摩擦係数が無視できない平面に反射するベクトルを返す
//vNorm:平面の法線ベクトル。正規化されている必要がある
//vIn:入射ベクトル
//vRef:反射ベクトル
//e:反発係数:衝突面と垂直なベクトルに影響する。物理学的に意味のある値である
//fr:摩擦係数(減衰係数):衝突面と平行なベクトルの減衰率。近似であり、物理学的に正しい表現ではない
void CalcReflectionVectorWithFriction(D3DXVECTOR3 &vRef,const D3DXVECTOR3 &vNorm,const D3DXVECTOR3 &vIn,float e,float fr)
{
    if(D3DXVec3Length(&vNorm)<EPSILON){ //法線がまともに計算できていない
        vRef=vIn*(-e);  //正面からの衝突と見なす
    }
    //内積を取る;関数呼び出しするほどの処理ではない
    float fDot=vIn.x*vNorm.x+vIn.y*vNorm.y+vIn.z*vNorm.z;
    D3DXVECTOR3 vNormDisplace(vNorm*fDot);  //入射ベクトルの法線方向成分
    D3DXVECTOR3 vH(vIn-vNormDisplace);  //水平方向のベクトル
    vRef=vH*fr-vNormDisplace*e;
}

2008/2/28

Zバッファの使い方

Zバッファのウマい使い方 その1Zバッファのウマい使い方 その2を書いた。

最初はここで書いてしまおうと思ったけど、前振りが長くなってしまったのでTipsに昇格。

2008/1/26

当たり判定の高速化

このエントリは推定ばっかり。結果オーライ。仕事とかで使うような真面目なコードじゃこんなテキトーなことは出来まい。

さて。当たり判定にD3DXIntersectを使っていると、遅いと思うことが増えてきた。特に、建物に向けてマシンガンを連射すると始めの一瞬だけフレームレートが劇落ちする。こんな有様では本格的にステージを作ったらきっとプレイできないぐらい遅くなってしまうに違いない。

そんなわけで、D3DXIntersectを高速化しようと考えたわけだ。

そもそもD3DXIntersectの内部動作を推定してみると(あくまで推定)、

という手順を踏んでいるに違いない。

この中で、まず最適化できそうな場所は、メモリのロック/アンロック。普段メッシュはD3DXMESH_MANAGEDで作成しているので、データがGPUにあるかもしれない。CPUからメモリにアクセスするには、ロックは必須。でもなんかコスト高そう。

考えてみれば、当たり判定で使う頂点情報はいつも変わらないわけで、頂点情報をあらかじめ取得しておいたら、D3DXIntersect相当の処理の中で毎回ロックを行う必要はなくなる。

というわけで、一端メッシュの頂点情報をコピーし、DXSDKのPickサンプルを参考に自前でのD3DXIntersect実装に挑んだ。システムメモリ内にデータが収まっているのだから、GPUのメモリをロックしに行くよりも速いはず。

ところが、現実はそうはいかない。リリースモードでビルドしてもほとんど速くなっていない。それどころか、デバッグモードではコンスタントに遅くなった。まぁ当然だ。最適化がかかっていないのだから。

まぁ、専門家の書いたコードより遅くならなかった、ってことは悪くないってことなんだろう。

でも12フレーム/秒じゃデバッグなんてやってらんない。

ここでふと思い直すと、またしても推定だが、D3DXMESH_MANAGEDで作成したメッシュは普段は描画向けに最適化されてGPUにデータが置かれているに違いない。そして、CPUからアクセスしやすいのはシステムメモリに置かれたデータであるので、アクセス頻度に応じてGPU→システムメモリにデータをコピーしているのだろう。

そう考えると、マシンガン連射直後にフレームレートが落ちたのも理解できる。正確に言うと、低下したフレームレートがメモリ配置の最適化により回復していたにすぎないのだろう。

そうなると、新しい戦略が生まれてくる。当たり判定専用にD3DXMESH_SYSTEMMEMで確保されたメッシュを用意してやれば、CPUからアクセスしやすい。そして、このメッシュに対してD3DXIntersectを呼んでやれば、速度低下は起きないはず。

というわけで、当たり判定専用メッシュ(もちろん頂点情報のみにして)を用意するようにして実装してみた。デバッグモードで軽くはかってみた感じでは、ざっと2割〜8割ほど速くなった。これはうれしい。

というわけで、結論。メッシュを目的に応じて適切な場所に配置すれば速くなる。

#始め、シェーダーを書いてGPUにD3DXIntersectを処理させようと計画していたのは内緒だ

##そんなことをすれば多分完成が大幅に遅れる事態になったんだろうな

2007/12/17

振動問題

ずいぶん古いが、2007/6/17の図を見てほしい。オブジェクトの境界ボックスに当たり判定レイがぴったり収まっているのが分かるだろう。このぴったりという状態が曲者だ。

これまでずっと、物の上にプレイヤーが載ったときにぶるぶる振動するという現象に困っていた。地面に安定して立っているはずなのに、少し地面にめり込んでは反発して浮き上がり、ということを繰り返していたのだ。長い間原因が分からずに困っていたのだが、先日ついに原因が判明した。上で書いた境界ボックスである。

当たり判定を高速化するために、現在境界ボックスを使用して判定対象の足切りを行っている。このとき、境界ボックスが判定対象ぎりぎりの大きさであると、ちょっとした誤差から判定対象にすべきオブジェクトまで足切りされてしまうのだ。流れは次のようになる。

  1. プレイヤーが何かの上に立っている
  2. 境界ボックスがぎりぎり重ならないため、足場の上にいると判定されない
  3. 空中にいると見なし、自由落下開始
  4. 次のフレームで足場にめり込むため、反発力が働きほぼはじめの位置に戻る
  5. 最初から繰り返し

原因が分かれば対策は簡単で、単に境界ボックスを1〜2%ほど太らせればよい。足切り時には大雑把な判定で十分、むしろ微妙なラインは残しておいてもらった方が幸せなのでこれで良いのだ。

ファイルの重複ロード防止

2年以上前から同じことを繰り返している気がする。が、気にせずC++でこんなテンプレートクラスを書いてみた。前よりは汎用性が増したはず。詳細は以下の通り。

構築
テンプレート引数に受け取りたいクラスを渡してやり適宜インスタンスを確保。
被管理クラス
管理されるクラスは参照カウンタの実装を必要としない。また、被管理クラスの特定のインスタンスにデータがロードされているかどうか確認できる手段が必要。
Find()
ファイルが登録されているかどうか調べる。見つかればそのポインタを、なければNULLを返す。ファイルが正しくロードされているかどうかは関知しない。
Allocate()
ファイル名と関連付けられた被管理クラスのインスタンスを確保。解放する必要があるならAllocate()したCFileBank()のインスタンスのDelete()を呼ぶこと。
Delete()
ファイル名と関連付けられた被管理クラスのインスタンスを解放。
DeleteAll()
管理している全ての被管理クラスのインスタンスを解放。
template <typename T,class Str=std::string>
class CFileBank{
protected:
    std::hash_map<Str,T*> m_FileMap;
public:
    CFileBank(){}
    virtual ~CFileBank(){DeleteAll();}
    T* Allocate(Str fname){
        //ファイル名で登録エントリ作成
        if(m_FileMap[fname]){
            //既存の登録を削除
            delete m_FileMap[fname];
        }
        T* lpData=new T;
        m_FileMap[fname]=lpData;
        return lpData;
    }
    T* Find(Str fname){
        //検索
        std::hash_map<Str,T*>::iterator ite=m_FileMap.find(fname);
        if(m_FileMap.end()==ite){
            //見つからない
            return NULL;
        }else{
            //見つかった
            return (*ite).second;
        }
    }
    void Delete(Str fname){
        //削除
        if(m_FileMap[fname]){
            //既存の登録を削除
            delete m_FileMap[fname];
        }
        m_FileMap.erase(fname);
    }
    void DeleteAll(){
        //全て削除
        for(std::hash_map<Str,T*>::iterator ite=m_FileMap.begin();ite!=m_FileMap.end();++ite){
            if((*ite).second){
                delete (*ite).second;
            }
        }
        m_FileMap.clear();
    }
};

要はファイル名とポインタを関連付けて管理しているだけ。「被管理クラスの特定のインスタンスにデータがロードされているかどうか確認できる手段が必要」なのは、ファイルのロードに失敗した場合でもDelete()しない、という使い方を想定しているから。

存在しないファイルを"not_exist_file.txt"として、「Allocate("not_exist_file.txt")」→「"not_exist_file.txt"のロードに失敗」→「Delete("not_exist_file.txt")」→「再びAllocate("not_exist_file.txt")」→…って流れはきわめて効率が悪い。Delete()せずに失敗したということだけ分かれば十分。

上のコードそのままだと少々使いにくいので、適度に派生させて依存部分を吸収したGetTexture()なんてメンバを定義するとよし。

開発中、メッシュ(3D形状)クラスに「ファイル重複ロード管理」と「参照カウンタ」が付き、さらに「ファイルからではなく動的にメッシュを生成する機能」が付いた時点でどこかにバグが潜んで取れなくなった。あるいはstaticメンバ変数が悪さをしたのかもしれない。とにかく汚いコードが恥ずかしかった。シンプルで確実な道具が欲しかった。そして上のコードが出来た。そしてまた車輪は再発明される。

2007/10/22

新しいオーディオプレイヤー(録音機能付き)を買ったので、ちまちまと身の回りの音を録音してみたり。効果音を作るのは楽しい。特に他のゲームの効果音と差し替えてみるとそれっぽく聞こえるのがたまらない。もっとも、プログラムには音を出す機能はまだ無い。

それはそうと、大半のFPSで現実と同じように銃を連射すると弾道がぶれ、命中精度が下がるようになっている。でもこれを単純にランダムなノイズを加えるようにしてしまうと、弾道の方向ベクトルの平均値が照準と同じままになってしまい、リアルさが損なわれてしまう気がする。銃の反動による弾道のずれは基本的に射撃による上向きの反動が原因だと思うので、弾道の方向ベクトルの平均値も上にずれていくべきだと思う。

さて、この平均軌道の中心が上にずれていくのは「照準を反動に合わせて上に上げていき、弾道分布は均一なままにする」方法と「照準はそのままで、弾道だけ上に偏った分布をする」方法が考えられるが、どちらにしても的に当てるように照準を移動させると、射撃後に照準を戻さなくてはいけなくなる。面倒だ。

とはいえ、弾道だけ偏らせる方がマシな気はする。照準を大きく動かさなくてはいけないほど弾道が偏るなら、既に実用的な命中率ではなくなっているだろう。また実験して確かめよう。

2007/9/25

丸一ヶ月開いてしまった…。忙しかったから仕方ないと言いつつ、本当は大ポカに気づかなかったのが原因。

敵役のAIを作ろうとしたのがことの発端。いきなり知能を持ったものを作るのはハードルが高いので、まずはマップ上にただ存在し、前に進むだけのボット(RunningBotと命名)を作ることにした。

クラス継承関係としては、CWorldObject→ICreature→CCreatureRunningBotという感じ。CWorldObjectはマップ上に存在する全ての物体を表す基本となるクラスで、ICreatureは自発的に移動する物体のためのクラス。地形と生き物を全てstd::list<CWorldObject*>に放り込んでやれば、地形と生き物の違いを気にせず当たり判定コードを使用できる。

ここでやらかした大ポカとは、当たり判定のコードをstd::list<CWorldObject*>に対してそのまま行ってしまったこと。当然、自分自身と当たり判定を取る羽目になり、哀れなRunningBotはマップの果てまですっ飛んでいってくれた。こういうのは慣れが大事なんだろう。勘が鈍ったようだ。高校時代ならもうちょっと早く気づいたような気がする。

2007/8/24

2007/8/15のスクリーンショットに出ている鳥居はただのテスト用データで、本編には出さない予定。

2007/8/15

[スクリーンショット]

「FPSつくるよ!」を書いた。日付が飛びまくっているのはご愛敬。そして動画もアップ(WMV,4.04MB)。当たり判定とかジャンプとかのデモンストレーション。画質が悪いのもご愛敬。

時々飛んでいる黄色いものは弾の軌跡で、左端の数字はマウスの座標変化値。

2007/7/28

学科内の友人がプログラムを手伝ってくれることに。作業の高速化を図る。

2007/7/20

学科内の3D職人と出会う。一気に実現の可能性が高まった。

2007/7/19

ご都合主義でいいや。

2007/7/14

シナリオを考えてみると現実には有り得ない物が出来てしまうことに気づく。どうしたものか。

2007/7/5

当たり判定の高速化に成功。本当は出来上がるのはもう少し早かったんだけどバグ修正とか余計な物が入って遅れてしまった。

高速化は、移動方向ベクトルと判定用レイの内積を取り、移動方向と反対方向を向いているレイについては判定を行わないようにする、というもの。あと、当然だけどレイの判定はAABBの境界ボックスで荒狩りを行っているし、オブジェクトとプレイヤーの間でもAABBで荒狩りを行っている。

2007/6/17

3Dオブジェクト同士の当たり判定の作成に成功。まだまだ遅いけど。

[境界ボックスと当たり判定用レイの関係]

境界ボックスと当たり判定用レイの関係

対象は人型のプレイヤーとマップ中のオブジェクト。プレイヤーは箱で近似する。基本的にはオブジェクトの中心から表面の代表点に向かってレイを用意し、レイとオブジェクトで当たり判定を行う。

この手法のいいところはペナルティ法が簡単に使えること。ここでいうペナルティ法とは、物体同士がめり込んだとき、めり込み度を元に圧力を計算して反発させてやる方法のことなのだが、実際のゲーム中ではもっと簡単に、めり込んだ分だけ座標を問答無用で押し出してやる。これでプレイヤーが激しく建物にぶつかってもめり込んだまま動けなくなるという事態を避けられる。

〜2007/6/16

戻る


[Claybird Logo]泥巣 by Claybird <claybird.without.wing@gmail.com>