ホームゲームつくろー!衝突判定編

Havok編
その2 Havok導入!


 前章ではHavok(Try Havok)を使うためのライセンスについて確認しました。とても大切なので未読の方は一読を。
では早速Havokを導入してみましょう。



@ Havok SDKのダウンロード

 Havok SDKはTry Havokのホームページからダウンロード出来ます。サイトに行くと個人情報の記入欄があります。嘘つかずに記入しましょう(^-^)。記入後Submitすると前章で紹介したHavok使用におけるライセンス条文が出てきます。十分な確認をして下さい。その後Acceptに進むとDLサイトにジャンプします。

 幾つかある項目の内「Havok Physics and Havok Animation SDKs for Programmers (7.1.0, VS2008)」がSDKです。今回はVS2008で開発しようと思いますので、こちらをDLすることにしました。Havok SDKのサイズはかなりでかいです(750MBくらい)。気をつけてください。

 DLファイルを回答すると幾つかのフォルダが出現します。特定の場所に移しておきましょう。私はC:/prj/havokフォルダを作り、そこに解凍したフォルダ内を移動させました:

 さて、ここからどうするか…。上のフォルダのDocs内にマニュアルがあり、そこに「Havok_Physics_Animation_710_Pc_Xs_Quickstart_Guide.chm」というクイックスタートがあるのですが、ん〜どうも微妙。何だかわかるようなわからないような感じです。メインマニュアルにもチュートリアルは無しと…ん〜、Havokさんは一見さんお断りな状態だ!さー大変です(^-^;;;



A 床に落ちるキューブを目指して

 チュートリアルはありませんが、Havokには500を越えるサンプルプログラムがあります。これらとマニュアルとをにらめっこしながら、少しずつ進めていきます。

 目標を「Havokを使ってキューブを8個を高さ100mから高さ0の床めがけて垂直落下させる」という物理現象を再現する事に定めましょう。複数のキューブを設定することでオブジェクト同士の衝突の仕組みが理解できます。高さの概念から重力の設定方法を学べ、床の概念は重力に影響されない動かないものの定義、垂直落下はステップ実行の仕組みを学べるというわけです。えっへん。



B Havokは最低限の動作環境を作るのが大変なのです

 まずはプロジェクトを立ち上げます。Visual Studioに新規で空のWin32アプリケーションを作ります。続いてプロジェクトのプロパティに次のような設定を行ってください:

デバッグ C/C++ 追加のインクルードディレクトリ C:\prj\havok\Source
リンカ 追加のライブラリディレクトリ C:\prj\havok\Lib\win32_net_9-0\debug_multithreaded
C/C++ 追加のインクルードディレクトリ C:\prj\havok\Source
リンカ 追加のライブラリディレクトリ C:\prj\havok\Lib\win32_net_9-0\release_multithreaded

これでヘッダーとLibへのパスが通りました。

 続いてテスト用の.cppとなるmain.cppを一つ作ってください。そこに次のライブラリ定義とヘッダーを全部書きます:

#pragma comment(lib, "hkBase.lib")
#pragma comment(lib, "hkInternal.lib")
#pragma comment(lib, "hkpCollide.lib")
#pragma comment(lib, "hkpconstraintsolver.lib")
#pragma comment(lib, "hkpDynamics.lib")
#pragma comment(lib, "hkpinternal.lib")
#pragma comment(lib, "hkputilities.lib")

#include <Common/Base/keycode.cxx>
#include <Common/Base/hkBase.h>
#include <Common/Base/Memory/System/Util/hkMemoryInitUtil.h>

#include <Physics/Dynamics/World/hkpWorld.h>
#include <Physics/Dynamics/Entity/hkpRigidBody.h>
#include <Physics/Dynamics/Entity/hkpRigidBodyCinfo.h>
#include <Physics/Utilities/Dynamics/Inertia/hkpInertiaTensorComputer.h>
#include <Physics/Collide/Shape/Convex/Box/hkpBoxShape.h>
#include <Physics/Collide/Dispatch/hkpAgentRegisterUtil.h>

#include <windows.h>
#include <tchar.h>
#include <d3d9.h>
#include <d3dx9.h>
#include <stdio.h>

#pragma comment(lib, "dxguid.lib")
#pragma comment(lib, "d3d9.lib")
#pragma comment(lib, "d3dx9.lib")

うげ〜っとなるのですが、これ全部必要です。Havokはライブラリが細かく分離されているため沢山プロジェクトに加える必要があります。またヘッダーには宣言順番の依存関係があり、keycode.cxxは最初の方で宣言しないとリンカーエラーが出ます。よって上の順番を守ってください。

 Havokの主幹となるヘッダーはhkBase.hです。またメモリ管理サポートをするhkMemoryInitUtil.hが必要になります。Havokの世界(空間)を作るのがその名の通りhkpWorld.h内で宣言されています。その世界の中で存在するオブジェクトがhkpRigidBody.h、剛体としての性質をつけてくれるのがhkpInertiaTensorComputer.h…などとなっています。

 windows.h以下は描画周りのヘッダーで、これはHavokと直接は関係ありません。今回はDirectXで描画をしてみます。今更ですが、Havok(Havok Physics)は物理現象を数値的に計算する仕組みなので、その結果はプログラマが可視化する必要があります。



C 世界を定義しよう

 物理現象は何らかの「場」で発生します。その場をHavokでは「World」と読んでいます。

 Worldを作ってみましょう:

hkMemoryRouter* memoryRouter = hkMemoryInitUtil::initDefault();
hkBaseSystem::init(memoryRouter, HavokErrorReportFunction);

hkpWorldCinfo info;
info.m_simulationType = hkpWorldCinfo::SIMULATION_TYPE_CONTINUOUS;
info.m_collisionTolerance = 0.1f;
info.setBroadPhaseWorldSize( 10000.0f );
info.setupSolverInfo( hkpWorldCinfo::SOLVER_TYPE_4ITERS_MEDIUM );
info.m_gravity = hkVector4(0.0f, -9.8f, 0.0f);
hkpWorld *world = new hkpWorld(info);

hkpAgentRegisterUtil::registerAllAgents( world->getCollisionDispatcher() );

ここ、スンゴイ大切なので上から余すこと無く説明していきますよ。

 まずhkMemoryRouterクラスのオブジェクトを一つ作ります。このクラスはいわゆる「メモリアロケータ」です。メモリアロケータというのは、メモリを確保したり開放したりする仕組みを提供してくれる人の事です。通常私たちは何気なくnewやdeleteをしていますが、実はあの裏ではデフォルトのメモリアロケータが頑張ってメモリ管理をしてくれているんです。Havokのような複雑なライブラリの場合、デフォルトではなく独自のメモリアロケータを使って効率良くメモリ管理をするのが普通です(商用ゲームもそうです)。そのメモリアロケータを作っているのが最初の1行目というわけです。

 続いて、それをhkBaseSystem::initメソッドに渡しています。Havokのシステムのメモリアロケータをこれで設定したわけです。第2引数のHavokErrorReportFuntionというのは関数ポインタで、私は次のように一応定義しておきました:

void HavokErrorReportFunction(const char* s, void* errorReportObject) {
    // HAvok内でエラーが起きた時によばれます
}

この関数はHavok内でエラーが起きた時に呼ばれます。一先ず空実装で構いません。ここまででHavokが動くための基盤設定ができました。


 さて続き。

 下から2行目を御覧ください。ここで「new hkpWorld」とWorldオブジェクトを作っています。この引数に渡している「info」。ここに世界の性質を決めるパラメータが定義されているわけです。infoはhkpWorldCinfoクラスです。hkpWorldCinfoクラスはWorldに関するパラメータを非常に細かく設定できます。取り敢えず上で設定している分だけを見ていきましょう。

 m_simulationTypeはHavok内での物理シミュレーションの計算方式を定義します:

SIMULATION_TYPE_DISCRETE 離散形式。離散化する事で浮動小数点誤差等が起こりにくくなり安定化する…のかな
SIMULATION_TYPE_CONTINUOUS 連続形式。正確なシミュレーションを行なうのでしょう
SIMULATION_TYPE_MULTITHREADED マルチスレッド形式。計算をマルチスレッドで行うため計算速度が飛躍的に向上します。

仕様によりけりですが、一先ず上では連続形式を選択しています。

 次のm_collisionToleranceは衝突判定の許容誤差(tolerance)を設定します。単位はmと考えておきます。余り小さ過ぎるとブルブル震えたりと挙動不審になりますが、大き過ぎると衝突判定自体がおかしくなってきます。程々ですね。

 setBroadPhaseWorldSizeメソッドでは世界の大きさ(立方体の辺の長さ)を決めます。「無限に大きくしておけばいいじゃん」と思うかもしれませんが、それはいけません。これはメモリ効率と精度とパフォーマンスに直結します。世界が小さいほど高精度でハイパフォーマンスで動きます。但し、余り小さくし過ぎて世界にあるものがはみ出てしまうとエラーになります。ですから、これは作るゲームやスケールによるんです。

 次のsetupSolverInfoメソッドは衝突判定等での計算の繰り返しタイプを以下から設定します:

SOLVER_TYPE_2ITERS_SOFT
SOLVER_TYPE_2ITERS_MEDIUM
SOLVER_TYPE_2ITERS_HARD
SOLVER_TYPE_4ITERS_SOFT
SOLVER_TYPE_4ITERS_MEDIUM
SOLVER_TYPE_4ITERS_HARD
SOLVER_TYPE_8ITERS_SOFT
SOLVER_TYPE_8ITERS_MEDIUM
SOLVER_TYPE_8ITERS_HARD

下の方ほど高精度になりますが、当然の事ながらパフォーマンスが激しく落ちていきます。これもゲームが許容する範囲で決めてあげれば良いのかなと思います。

 次にあるm_gravityは、名前の通り重力の方向(重力加速度)を決めます。地球上であればY軸のマイナス方向に9.8m/s^2なので、

info.m_gravity = hkVector4(0.0f, -9.8f, 0.0f);

のように設定します。hkVector4はHavokライブラリが定義する4次ベクトルです(第4引数は省略可能)。

 他にもい〜〜っぱい設定項目があるのですが、とりあえず以上くらいを設定しておけば世界として成り立ちます。こうして設定したhkpWorldCinfo構造体をhkpWorldのコンストラクタに渡せば、世界が作られます。

 作った後に、

hkpAgentRegisterUtil::registerAllAgents( world->getCollisionDispatcher() );

という一行があります。hkpAgentRegisterUtil::registerAllAgentsメソッドは「CollisionDispatcer」という衝突計算の計算方法を管理するクラスをシステムに登録します。これが無いと衝突判定が起こりません。上の一行ではWorldが持っているCollisionDispatcherをシステムに登録しているわけです。



D 床を作ろう

 Cで世界が整いました。では、世界に置く物を作っていきましょう。重力のある世界ではオブジェクトやキャラクタは床が無いとどこまでも落ちていってしまいます。この「床」、実は非常に特別な存在です。床も物ですから重力のある世界に置けば落ちてしまうはずです。でも地球上の地面は下に落ちません。また、床にはどのような重たいものが落っこちてきても微動だにしない強さが必要です。ゲームの世界で床を作るには、そういう特殊な性質を付ける必要があるんです。

 床は、ものすごーくだだっ広くて薄っぺらな箱です。そこで次のように床を作ります:

const hkVector4 halfExtents(2000.0f, 20.0f, 2000.0f);
hkpShape* groundShape = new hkpBoxShape(halfExtents);

hkpRigidBodyCinfo bodyInfo;
bodyInfo.m_mass = 0.0f;
bodyInfo.m_shape = groundShape;
bodyInfo.m_motionType = hkpMotion::MOTION_FIXED;
bodyInfo.m_position.set(0.0f, -10.0f, 0.0f);

hkpRigidBody* groundBody = new hkpRigidBody(bodyInfo);
groundShape->removeReference();

world->addEntity(groundBody);
groundBody->removeReference();

これも上から説明です。

 まず、衝突の形状を表すオブジェクトは「シェイプ(Shape)」と呼ばれています。上の場合「hkpBoxShape」つまりボックス状の衝突図形を作っています。hkpBoxShapeの第1引数はボックスの縦横高さをhkVector4で指定します。XYZの順です。第2引数もあるのですが今は省略で構いません。

 シェイプはあくまでも衝突形状です。この衝突形状に色々な情報を付加したのが「リジッド(Rigid)」です。そのリジッドの情報を設定するのがhkpRigidBodyCinfoクラスです。
massはリジットの質量を設定します。上の床の場合は質量がいらないので0にしています。
m_shapeはその名の通りリジットの衝突形状を設定します。
m_motionTypeは「慣性テンソル」という物理パラメータの種類を設定します。慣性テンソルというのは…えと、まぁ、Wikipediaを見て下さい。ざくっと言えば、物の回転しにくさです。野球ボールは手首できゅいきゅい回転させられますが、長ーい棒の真ん中を持って回すと抵抗があります。そういう感じのものです。ここにはhkpMotion::MotionType列挙型で指定されている列挙子を設定します。上のMOTION_FIXEDというのは大変に特殊な慣性テンソルで、何物にも屈しません。つまりどれほど大きくて重たいものが当たってもびくともしない物ができあがります。
m_position.setメソッドはリジットの位置を設定します。ついでにですが、m_rotationという回転姿勢を定義するパラメータもあります。

 これらのリジットパラメータをhkpRigidBodyクラスのコンストラクタに渡すとリジットが作成できます。

 作成したリジッドはWorldに登録できます!登録するにはhkpWorld::addEntityメソッドにリジッドを渡します。

 さて、上のソースで床シェイプ(groundShape)や床リジッド(groundBody)のremoveReferenceメソッドが呼ばれています。このメソッドはいわゆる「参照カウンタ」を減らします。これらのオブジェクトがnewされた段階で内部参照カウンタが1つ増加しています。そしてワールドに登録するともう一つ増加します。つまり、作った自分に削除責任が生じているわけです。そこで作った段階でremoveReferenceメソッドを呼んで削除責任を放棄します。こうすると、リジットやシェイプの寿命はワールドの寿命と一緒になるわけです。

 これで世界の中にだだっ広い床ができあがりました。



E 8つのキューブを作ろう

 続いて、世界に8つのキューブを置いてみます:

const float bx = 1.0f, by = 2.0f, bz = 1.0f;
const int nx = 2, ny = 2, nz = 2;
const int NUM_BODIES = nx * ny * nz;
hkpRigidBody *bodies[NUM_BODIES];
{
    const hkVector4 halfExtents(bx, by, bz);
    hkpShape* shape = new hkpBoxShape(halfExtents, 0.0f);

    hkpMassProperties massProperties;
    hkpInertiaTensorComputer::computeShapeVolumeMassProperties(shape, 5.0f, massProperties);

    hkpRigidBodyCinfo bodyInfo;
    bodyInfo.m_mass = massProperties.m_mass;
    bodyInfo.m_centerOfMass = massProperties.m_centerOfMass;
    bodyInfo.m_inertiaTensor = massProperties.m_inertiaTensor;
    bodyInfo.m_shape = shape;
    bodyInfo.m_motionType = hkpMotion::MOTION_BOX_INERTIA;

    int i = 0;
    for(int x = 0; x < nx; x++)
    for(int y = 0; y < ny; y++)
    for(int z = 0; z < nz; z++) {
        hkpRigidBody *body = new hkpRigidBody(bodyInfo);
        body->setPosition(hkVector4(x * bx * 2, 100.0f + y * by * 2, z * bz * 2));
        bodies[i] = body;
        world->addEntity(body);
        body->removeReference();
        i++;
    }
    shape->removeReference();
}

最初のbx,by,bzはキューブのXYZ方向の辺の長さの「半分」です。続くnx,ny,nzはキューブの各軸方向の数です。2*2*2で8つというわけです。bodies配列には作成した8つのキューブリジッドを保持しておきます。

 床と同じようにhkpRigidBodyCinfo構造体に必要な情報を格納していきます。シェイプはもちろんキューブ(hkpBoxShape)です。今回はXZ軸が2m、Y軸が4mのキューブを作ります。よって第1引数にその大きさを渡します。次のhkpMassPropertiesは質量等の情報をまとめる構造体です。この構造体をhkpInertiaTensorComputer::computeShapeVolumeMassPropertiesメソッドに渡すと、シェイプの形状から慣性テンソルなど各種情報を自動計算してくれます。上のように第1引数にシェイプ、第2引数に質量を渡すと第3引数の構造体に結果が返ります。

 bodyInfoはこの構造体の結果を使うと楽です。m_massは質量、m_centerOfMassは重心位置、m_inertiaTensorは慣性テンソル行列で、全部massPropertiesに結果があります。重さと重心と慣性テンソルがあると剛体が定義できるわけです。後はm_shapeにシェイプを、m_motionTypeに慣性テンソルタイプを渡せばOK。

 リジッドはfor文内で作っています。hkpRigidBodyオブジェクトにbodyInfoを渡せば、その情報通りのリジッドができます。hkpRigidBody::setPositionメソッドは、その名の通りリジッドの位置を移動します。今回は積み木のように積んだ状態で高さ100mの位置にオフセットしています。後はワールドにエントリーすれば世界に置かれます。エントリー後、作成したbodyに対して削除責任を放棄しています。

 shapeを使いまわしていることに注目です。形状が全く同じであればこれで問題ありません。

 さ!これで、世界にだだっ広い床と8つのキューブが置かれました。後は世界の時間を進めるだけです!



F 世界の時間を進めよう

 世界の時間をすすめると、各リジッドは自分自身にかかる力(物理法則)に従って動き出します。時間を動かす方法は幾つかあるのですが、一番簡単なステップ実行をさせてます:

world->stepDeltaTime(1.0f/60.0f)

この1行で時間が60分の1秒進みます。

 時間を進めた後、各キューブを描画するにはその位置(姿勢)が必要です。



G リジッドの姿勢

 リジッドの姿勢を得るにはhkpRigidBody::getTransformメソッドからhkTransformオブジェクトを得ます:

const hkTransform &t = groundBody->getTransform();
D3DXMATRIX transMat(
    t(0,0), t(1,0), t(2,0), 0.0f,
    t(0,1), t(1,1), t(2,1), 0.0f,
    t(0,2), t(1,2), t(2,2), 0.0f,
    t(0,3), t(1,3), t(2,3), 1.0f);

hkTransformは内部に4×4の姿勢行列を持っています。ただ、この行列は「列メジャー」、つまり列方向主体です。一方DirectXは行メジャーです。そのため、行列情報を転置する必要があります。

 姿勢行列の要素を得るメソッドが実はhkTransformクラスにはありません。そのかわり()演算子が定義されています。引数は「行」「列」の順で、その要素の値を得ることができます。

 姿勢を得ることができれば、後はもうそれをキューブ描画に適用するだけです。



H 後片付け

 ワールド及びHavokのシステムが必要なくなったら、それを解放します。これはお決まりで次ようにします:

delete world;

hkBaseSystem::quit();
hkMemoryInitUtil::quit();

worldをdeleteした段階で世界が抱えているオブジェクトがすべて解放されます。システムの後片付けも必要で、上のようにhkBaseSystem::quitメソッド及びhkMemoryInitUtil::quitメソッドを呼び出します。



 このように、Havokで簡単な図形を世界において物理現象を起こすのは割と簡単です。どちらかというとヘッダーやライブラリなどを組み込むのが長々として面倒かもしれません。この章の動きを再現するサンプルプログラムを作りましたのでお試しください。