movie 1. 実行結果のアニメーション
C/C++ 言語で作成されたオープンソースの物理エンジンであるOpen Dynamics Engine(ODE)を,C# 言語,特に XNA Game Studio から利用する方法についてまとめておきます.大まかな利用手順は以下の通りとなります.
- ODE の DLL 構築
- ODE.NET の小改造
- XNA GS からの呼び出し
今回は,マネージコードから利用できる準標準的な ODE ライブラリである,ODE.NET を少々改変して利用します.その結果,ODE の全ての API は「CsOde」名前空間の「Ode」クラスに,静的メソッドとしてまとめられます.まず C/C++ の ODE コード例を示します.
1
2
3
4
5
6
7
8
9
10
11
|
| #include
...
dMass mass;
BodyID body = dBodyCreate(world);
dMassSetBoxTotal(out mass, 10.0, 10.0, 10.0, 10.0);
dBodySetMass(body, &mass);
dGeomID geom = dCreateBox(space, 10.0, 10.0, 10.0);
dGeomSetBody(geom, body);
...
|
一方,C# のコードは次のようになります.型名や関数名のプレフィクス"d"がなくなり,代わりに「Ode.」というクラス指定が加わっています.
1
2
3
4
5
6
7
8
9
10
|
| using CsOde;
...
Ode.Mass mass;
BodyID body = Ode.BodyCreate(world);
Ode.MassSetBoxTotal(out mass, 10.0, 10.0, 10.0, 10.0);
Ode.BodySetMass(body, ref mass);
GeomID geom = Ode.CreateBox(space, 10.0, 10.0, 10.0);
Ode.GeomSetBody(geom, body);
...
|
サンプルプログラム †
サンプルプログラムは Visual Studio 2005 Professional と XNA Game Studio 2.0 を用いて作成しました.また,ODE の DLL 名は "ode32.dll" と仮定しています.なお,"ode32.dll" は SourceForge からダウンロードしたソースファイル(ode-src-0.9.zip)から構築した,x86 版の DLL です.
プログラムを実行すると,赤い直方体が空中から落下し,地面にバウンドしながら転がるようなアニメーションが再生されます.
ODE.NETの小改造 †
C# 向けの ODE ライブラリとして,ODE.NET が準公式に用意されています.これをそのまま用いても全く問題ないのですが,私の個人的な好みで以下のカスタマイズを施しました.もちろん,これらは本質的な改変ではないので,無視しても問題ありません.
- 全ての「DLLImport」属性において,DLL ファイル名を「ode32」に変更.
- ODE.NETでは全てのクラス,列挙子,構造体を「ODE.NET」という名前空間に含めていますが,これは「ODE」名前空間の「NET」名前空間に含めているように見えて,あまり気持ちよくありません.そこで本プログラムでは「CsOde」という名前空間を利用します.
- オリジナルのクラス名は「d」と短くてわかりにくいので,本プログラムではクラス名を「Ode」に変更しています.(C/C++版のプレフィクスをクラス名にして,言語共通の可読性を確保するという意図には賛成ですが...)
- C/C++ 版の API にはポインタを返り値とする関数がいくつかあり,ODE.NET では忠実な移植を行うために unsafe メソッドを用いています.しかし,unsafe メソッドはマネージプログラミングでは好ましくありません.そのため,本サンプルでは以下の方策を採りました.
- ポインタを返す API は,そのまま IntPtr を返すメソッドとする.
- 例) dVector3* dBodyGetLinearVel(dBodyID body) → IntPtr BodyGetLinearVel(BodyID body)
- 目的の値を構造体のポインタとして返す API のみが定義されている場合は,引数の構造体に目的の値をコピーするメソッドを C# で作成.
- 例 1)「dVector3* dBodyGetLinearVel(dBodyID body)」+「void dBodyCopyLinearVel(dBodyID body, dVector3 *vel)」の 2 つが存在する場合は,前者は後者で代替可能なので特に処置なし.
- 例 2)「dVector3* dBodyGetTorque(dBodyID body)」の値を取得するため, C# 側で「void BodyCopyTorque(BodyID body, out Vector3 torque)」メソッドを作成(内部で「IntPtr BodyGetTorque(BodyID body)」を呼び出す)
- 追加メソッド一覧: BodyCopyAngularVel(BodyGetAngularVelの代替),BodyCopyTorque(BodyGetTorque),BodyCopyForce(BodyGetForce),BodyCopyLinearVel(BodyGetLinearVel), GeomTriMeshCopyLastTransform(GeomTriMeshGetLastTransform),JointCopyFeedback(JointGetFeedback)
- 行列の構造体フィールド名を,XNA に合わせて変更しました.
- Matrix4構造体は,コンストラクタを省略し,添え字の範囲を [0,3] から [1,4] に変更しました.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| -
!
-
|
|
|
|
-
|
|
|
|
!
|
|
|
|
!
| [StructLayout(LayoutKind.Sequential)]
public struct Matrix4
{
public Matrix4(dReal m00, dReal m10, dReal m20, dReal m30,
dReal m01, dReal m11, dReal m21, dReal m31,
dReal m02, dReal m12, dReal m22, dReal m32,
dReal m03, dReal m13, dReal m23, dReal m33)
{
M00 = m00; M10 = m10; M20 = m20; M30 = m30;
M01 = m01; M11 = m11; M21 = m21; M31 = m31;
M02 = m02; M12 = m12; M22 = m22; M32 = m32;
M03 = m03; M13 = m13; M23 = m23; M33 = m33;
}
public dReal M00, M10, M20, M30;
public dReal M01, M11, M21, M31;
public dReal M02, M12, M22, M32;
public dReal M03, M13, M23, M33;
}
|
1
2
3
4
5
6
7
8
9
| -
!
-
|
|
|
|
!
| [StructLayout(LayoutKind.Sequential)]
public struct Matrix4
{
public Real M11, M21, M31, M41;
public Real M12, M22, M32, M42;
public Real M13, M23, M33, M43;
public Real M14, M24, M34, M44;
}
|
- Matrix3構造体は,コンストラクタを省略,添え字の範囲を [0,3] から [1,4] に変更,そして private 指定されているフィールドも public 公開しました.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| -
!
-
|
-
|
|
|
!
|
|
|
|
|
|
!
| [StructLayout(LayoutKind.Sequential)]
public struct Matrix3
{
public Matrix3(dReal m00, dReal m10, dReal m20, dReal m01, dReal m11, dReal m21, dReal m02, dReal m12, dReal m22)
{
M00 = m00; M10 = m10; M20 = m20; _m30 = 0.0f;
M01 = m01; M11 = m11; M21 = m21; _m31 = 0.0f;
M02 = m02; M12 = m12; M22 = m22; _m32 = 0.0f;
}
public dReal M00, M10, M20;
private dReal _m30;
public dReal M01, M11, M21;
private dReal _m31;
public dReal M02, M12, M22;
private dReal _m32;
}
|
1
2
3
4
5
6
7
8
| -
!
-
|
|
|
!
| [StructLayout(LayoutKind.Sequential)]
public struct Matrix3
{
public Real M11, M21, M31, M41;
public Real M12, M22, M32, M42;
public Real M13, M23, M33, M43;
}
|
XNAからの利用 †
ODE そのものの詳しい解説は demura.net さんなどに譲るとして,ここでは XNA 上での簡単な実装例を示します.
まず,あらかじめよく使う型名のエイリアスを,using ステートメントを用いて作成しておきます.
1
2
3
4
5
6
7
8
9
|
| using WorldID = IntPtr;
using SpaceID = IntPtr;
using BodyID = IntPtr;
using GeomID = IntPtr;
using JointID = IntPtr;
using JointGroupID = IntPtr;
using HeightfieldDataID = IntPtr;
using TriMeshDataID = IntPtr;
using Real = Single;
|
なお,上記のエイリアスは本質的には不要です.ただし,C# ではほとんどの ODE 固有の型は IntPtr 型で扱われるため,可読性を向上させる意味でもエイリアスを作成するのが良いでしょう.
次に,ODE 関連のフィールドを追加します.前半では,物理シミュレーション空間のID(world),衝突判定空間のID(space)など,お決まりのフィールドを宣言しています.後半では,本サンプル特有のフィールドとして,地面ジオメトリ(ground)と,立方体のボディとジオメトリに対応するフィールド(body1,geom1)を宣言しています.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| -
!
-
!
-
!
-
!
-
!
-
!
-
!
| private WorldID world = WorldID.Zero;
private SpaceID space = SpaceID.Zero;
private JointGroupID contactGroup = JointGroupID.Zero;
Ode.NearCallback collisionCallback = null;
private GeomID ground = GeomID.Zero;
private BodyID body1 = BodyID.Zero;
private GeomID geom1 = GeomID.Zero;
|
次に,XNA フレームワークの Initialize メソッドに ODE の初期化コードを記述しています.ODE お決まりのコードなので詳細は省きます.C# 特有の処理として,衝突判定用のコールバック関数をデリゲートとして作成する点が異なっています.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
-
-
|
|
|
|
|
!
|
|
|
|
|
|
|
|
!
| protected override void Initialize()
{
Ode.InitODE();
world = Ode.WorldCreate();
Ode.WorldSetGravity(world, 0, -9.8e2f, 0);
space = Ode.HashSpaceCreate(SpaceID.Zero);
contactGroup = Ode.JointGroupCreate(0);
collisionCallback = new Ode.NearCallback(DefaultCollisionCallback);
base.Initialize();
}
|
次に,シミュレートする剛体を構築するメソッドを作成し,XNA フレームワークの LoadContent メソッドから呼び出します.この CreateModel メソッドでは簡単な立方体オブジェクト(ボディとジオメトリ)を構築しました.立方体剛体の初期姿勢は,視線に沿った軸(Z軸)に沿って時計回りに π/3 rad 回転し,鉛直上方 100 m の上空に設定するものとしました.
立方体の幾何学的/物理的特性は次の通りです.
- 立方体の各辺長さは 10 m
- 立方体の質量は 10 kg とし,密度は一定と仮定(したがって慣性テンソルは対角行列)
なお,シミュレーション系の単位はユーザが適当に決定します.つまり,各辺 10 cm の 10 g の立方体と考えても差し支えありません.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
-
|
|
-
!
|
-
|
|
|
|
|
|
!
|
|
|
|
|
|
|
-
!
|
!
| protected void CreateModel()
{
Matrix initTransform = Matrix.CreateRotationY(MathHelper.PiOver2);
ground = Ode.CreatePlane(space, 0, 1.0f, 0, 0);
Ode.Mass mass;
body1 = Ode.BodyCreate(world);
Ode.MassSetBoxTotal(out mass, 10.0f, 10.0f, 10.0f, 10.0f);
Ode.BodySetMass(body1, ref mass);
geom1 = Ode.CreateBox(space, 10.0f, 10.0f, 10.0f);
Ode.GeomSetBody(geom1, body1);
Matrix c = Matrix.CreateRotationZ(MathHelper.Pi / 3.0f) * Matrix.CreateTranslation(0, 100.0f, 0);
SetBodyCoordinate(body1, c);
}
|
CreateModel メソッドで参照されている SetBodyCoordinate メソッドと,その対となる GetBodyCoordinate メソッドの実装を示します.オブジェクトの位置や回転は ODE のフレームワーク内で管理し,XNA 側では適切な型に変換して取得/設定するだけです.Ode.Matrix3 構造体と,XNA の Matrix 構造体のフィールド名を統一しているので,対応するフィールド間で値をコピーするだけの実装になります.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
|
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
!
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
!
| protected void SetBodyCoordinate(BodyID body, Matrix src)
{
Ode.BodySetPosition(body, src.M41, src.M42, src.M43);
Ode.Matrix3 rot = new Ode.Matrix3();
src = BasicGeometry.Orthogonalize(src);
rot.M11 = src.M11;
rot.M12 = src.M12;
rot.M13 = src.M13;
rot.M21 = src.M21;
rot.M22 = src.M22;
rot.M23 = src.M23;
rot.M31 = src.M31;
rot.M32 = src.M32;
rot.M33 = src.M33;
Ode.BodySetRotation(body, ref rot);
}
protected Matrix GetBodyCoordinate(BodyID body)
{
Ode.Matrix3 m;
Ode.Vector3 pos;
Ode.BodyCopyPosition(body, out pos);
Ode.BodyCopyRotation(body, out m);
Matrix dest = Matrix.Identity;
dest.M11 = m.M11;
dest.M12 = m.M12;
dest.M13 = m.M13;
dest.M21 = m.M21;
dest.M22 = m.M22;
dest.M23 = m.M23;
dest.M31 = m.M31;
dest.M32 = m.M32;
dest.M33 = m.M33;
dest.M41 = pos.X;
dest.M42 = pos.Y;
dest.M43 = pos.Z;
return dest;
}
|
最後に,XNA の UnloadContent メソッドに ODE の後片付け処理を記述します(※ 記述箇所としては OnExiting メソッドのほうが適切かもしれませんが,初期コードの流用を優先しています).Initialize メソッドで構築したシミュレーション空間を,お決まりの処理で解放しています.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
-
-
|
|
|
|
!
|
|
|
|
|
|
|
|
|
!
| protected override void UnloadContent()
{
Ode.JointGroupDestroy(contactGroup);
Ode.SpaceDestroy(space);
Ode.WorldDestroy(world);
contactGroup = JointGroupID.Zero;
space = SpaceID.Zero;
contactGroup = JointGroupID.Zero;
Ode.CloseODE();
}
|
Update メソッドでは,シミュレーションを 1 ステップずつ実行します.衝突判定→シミュレーションステップ→衝突判定後処理と,これも ODE ではお決まりのパターンです.なお,シミュレーション刻み幅は 1/200 sec としていますので,立方体がゆっくりと落下するアニメーションが生成されます.
1
2
3
4
5
6
7
|
-
|
|
|
|
|
| protected override void Update(GameTime gameTime)
{
Ode.SpaceCollide(space, IntPtr.Zero, collisionCallback);
Ode.WorldStep(world, 1.0f / 200.0f);
Ode.JointGroupEmpty(contactGroup);
...
|
ついでに,衝突判定用コールバックメソッドの処理を示します.衝突現象は弾性衝突(はねかえり係数 0.5)とし,0.1 [m/sec] 以下の速度では衝突による跳ね返りを生じない設定としました.また,地面との摩擦係数は無限大(実際には float 型の最大値)としています.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
-
!
|
-
|
|
|
!
!
| public void DefaultCollisionCallback(IntPtr data, GeomID geom0, GeomID geom1)
{
const int N = 20;
Ode.ContactGeom[] contacts = new Ode.ContactGeom[N];
BodyID b0 = Ode.GeomGetBody(geom0);
BodyID b1 = Ode.GeomGetBody(geom1);
if (b0 != BodyID.Zero && b1 != BodyID.Zero && Ode.AreConnected(b0, b1) == true)
return;
if (b0 != BodyID.Zero && b1 != BodyID.Zero && Ode.AreConnectedExcluding(b0, b1, Ode.JointType.Contact) == true)
return;
Ode.Contact contact = new Ode.Contact();
contact.surface.mode = Ode.ContactFlags.Bounce;
contact.surface.mu = Real.MaxValue;
contact.surface.mu2 = Real.MaxValue;
contact.surface.bounce = 0.5f;
contact.surface.bounce_vel = 0.1f;
int n = Ode.Collide(geom0, geom1, N, contacts, Ode.ContactGeom.SizeOf);
for (int i = 0; i < n; ++i)
{
contact.geom = contacts[i];
JointID c = Ode.JointCreateContact(world, contactGroup, ref contact);
Ode.JointAttach(c, b0, b1);
}
}
|
まとめ †
XNA 上での物理シミュレーションの導入編として,ODE.NET の簡単な改変とお決まりのコードを紹介しました.ODE の API を薄くラップしているだけなので,C/C++ で ODE を触ったことがある方ならスムーズに移行できると思います.また,XNA(C#)で初めて物理シミュレータに触れる方も,demura.netさんなどの日本語リソースが充実しているので,比較的導入は簡単だと思います.
ただし,全く調べてすらいませんが,今回説明した方法は Xbox 上では利用できないと思います.それでもなお,Windows + XNA でお手軽に物理シミュレーションを試したいという用途にはお勧めです.
|