Edited at
Siv3DDay 11

ODEを使って遊ぶ

More than 3 years have passed since last update.

まだ3D機能が未熟なSiv3Dですが、3Dのオープンソースな物理(剛体シミュレーション)エンジンである「ODE(Open Dynamics Engine)」を組み合わせて遊んでみたいと思います。

ODEは単純に物理情報と挙動、および衝突判定に関する部分のみを取り扱っており、結果の描画に関しては全く関与していないのでSiv3Dにかぎらずどんなレンダラでも併用することが可能になっています。回転もクォータニオンによるものと行列によるものの2種類が用意されているので、クォータニオンの実装がないレンダラでも難なく使用可能です。


環境

Visual Studio 2013、Siv3D(June 2015 v2)、ODE 0.13でやっていきます。


ODEのダウンロードとビルド

ODEは普通の外部ライブラリになるので、先に単体でビルドをする必要があります。

1. まずはsourceforgeのページからファイルをダウンロードします。tar.bz2とtar.gzがありますがどちらも同じです。

2. ファイルを解凍し、適当なフォルダに置きます。私は利便性とかあといろいろがあるのでC:/libsというフォルダを作ってそこに置いています。

3. ODEのフォルダ内でコマンドプロンプトを開き、buildディレクトリに移動します(またはbuildディレクトリ内でコマンドプロンプトを開きます)。

4. 次のコマンドを実行します。

premake4 --with-demos vs2010

2013以降には対応していないみたいなので、2010向けにプロジェクトファイルを作ってから2013で変換して開きます。premake4でエラーが発生しなければbuild/vs2010/以下にode.slnファイルができます。

5. 「ソリューション設定」で「DebugDoubleLib」を選んでビルド(F7)します。Singleもありますが精度が担保できないのと、float(Single)よりdoubleのほうが計算がほんのちょっぴりだけ早いことがあるらしいのでDoubleを選びます。Debugはエラー箇所の特定をしやすくするため、LibはDLLの場所によるややこしいあれこれを防ぐためです。


サンプル(初期化とかまで)

とりあえずこんな感じに初期化コードを用意します。ODEはdInitODE()で始まってdCloseODE()で終了です。あ、パスはインクルードをodeディレクトリ内のincludeに、ライブラリをlib/DebugDoubleLibに通しておいてください。また、動かすにはワールドが必要なのでとりあえず作っておきます(本当に初期化するだけなら必要なし)。あと、後々これを拡張して作りたいので継承可能なように作っておきます。

#include <Siv3D.hpp>


#define dDOUBLE
#include <ode/ode.h>
#pragma comment(lib, "ode_doubled")

class DynamicsWorld
{
// DynamicsWorld: ODEとワールド

dWorldID world;
public:
DynamicsWorld()
{
// ODEの初期化
dInitODE();

// ワールドの作成と重力設定(Siv3Dは+Yが上なので)
this->world = dWorldCreate();
dWorldSetGravity(this->world, 0.0, -9.81, 0.0);
}
virtual ~DynamicsWorld()
{
// ワールドの削除とODEの後始末
dWorldDestroy(this->world);
dCloseODE();
}
};

template<typename WorldT>
class DynamicsManager
{
// DynamicsManager: シングルトンのためのクラス

static std::unique_ptr<WorldT> _instance;
public:
static WorldT* init()
{
if (!_instance) _instance = std::make_unique<WorldT>();
return _instance.get();
}
static WorldT* get() { return _instance.get(); }
};
template<typename WorldT>
std::unique_ptr<WorldT> DynamicsManager<WorldT>::_instance;
// こうしておくことで、独自のワールドクラスを拡張して作った場合にここだけ修正すれば対応が完了するようになる。
using DynamicsSystem = DynamicsManager<DynamicsWorld>;

void Main()
{
auto dynamics = DynamicsSystem::init();

while (System::Update())
{

}
}

ちょっと長くなりましたが、後々の拡張性を考えるとこうなるかなと思います。普通にMain内でdInitODE()とdCloseODE()を呼んでワールド管理しても構いませんが、作っていくうちに大変なことになると思います。また、ODEシステムはアプリケーション内で一つ限定なのでSingletonにしています。


床を作って箱を落とす

物理エンジン入門では定番のアレです(人によっては球を落としますが、見た目が地味すぎるので私は箱にしています)。

// includeは省略...

class DynamicsWorld
{
...省略...

///// ここから追加 //////
auto getWorld() const -> decltype(world) { return this->world; }
// ワールドの時間をすすめる
void step(dReal time) { dWorldStep(this->world, time); }
///// ここまで追加 /////
};
///// ここから追加クラス /////
class SpaceWorld : public DynamicsWorld
{
// SpaceWorld: 衝突関係(スペースと衝突点グループ)

dSpaceID collSpace;
dJointGroupID contactGroup;
public:
SpaceWorld() : DynamicsWorld()
{
this->collSpace = dHashSpaceCreate(0);
this->contactGroup = dJointGroupCreate(0);
}
virtual ~SpaceWorld()
{
dJointGroupDestroy(this->contactGroup);
dSpaceDestroy(this->collSpace);
}

auto getCollisionSpace() const -> decltype(collSpace) { return this->collSpace; }
auto getContactGroup() const -> decltype(contactGroup) { return this->contactGroup; }
};
class StaticBox final
{
// StaticBox: 動かない箱

Vec3 size;
dGeomID geom;
public:
StaticBox(const Vec3& size);
~StaticBox();
};
class DynamicsBox final
{
// DynamicsBox: 動く箱

Vec3 size;
dBodyID body;
dGeomID geom;
public:
DynamicsBox(const Vec3& size, dReal totalMass);
DynamicsBox(const Vec3& size) : DynamicsBox(size, 1.0) {}
~DynamicsBox();

// 位置を設定
void setPosition(const Vec3& size);
// 位置を取得
Vec3 getPosition() const;
// 回転を設定
void setRotation(const Quaternion& q);
// 回転を取得
Quaternion getRotation() const;
// サイズを取得
Vec3 getSize() const { return this->size; }
};
class EntityWorld final : public SpaceWorld
{
// EntityWorld: 実体とか

std::unique_ptr<StaticBox> pGround;
std::unique_ptr<DynamicsBox> pBox;

static void nearCallback(void*, dGeomID, dGeomID);
public:
EntityWorld() : SpaceWorld() {}
~EntityWorld() = default;

// オブジェクトのセットアップを行う
void setupWorld();

// ワールドを更新
void update();

auto getGround() const -> decltype(pGround.get()) { return this->pGround.get(); }
auto getBox() const -> decltype(pBox.get()) { return this->pBox.get(); }
};
///// ここまで追加クラス /////

template<typename WorldT> class DynamicsManager { ...省略... }
...ここも省略...
// 変更: DynamicsWorld -> EntityWorld
using DynamicsSystem = DynamicsManager<EntityWorld>;

void Main()
{
auto dynamics = DynamicsSystem::init();
// 追加コード: オブジェクトの初期化
dynamics->setupWorld();

while (System::Update())
{
// 追加コード
// ワールドを更新
dynamics->update();

Box(Vec3::Zero, Vec3(20.0, 0.125, 20.0)).draw(Palette::White);
Box(dynamics->getBox()->getPosition(), Vec3(2.0, 1.0, 1.0), dynamics->getBox()->getRotation()).draw(Palette::Red);
}
}

///// ここから最後まで追加分 /////
StaticBox::StaticBox(const Vec3& size)
{
this->size = size;

// ジオメトリの作成
this->geom = dCreateBox(DynamicsSystem::get()->getCollisionSpace(), size.x, size.y, size.z);
}
StaticBox::~StaticBox()
{
dGeomDestroy(this->geom);
}
DynamicsBox::DynamicsBox(const Vec3& size, dReal totalMass)
{
this->size = size;

// ボディを作って質量を設定する
this->body = dBodyCreate(DynamicsSystem::get()->getWorld());
dMass mass;
dMassSetZero(&mass);
dMassSetBoxTotal(&mass, totalMass, size.x, size.y, size.z);
dBodySetMass(this->body, &mass);

// ジオメトリを作成してボディをアタッチ
this->geom = dCreateBox(DynamicsSystem::get()->getCollisionSpace(), size.x, size.y, size.z);
dGeomSetBody(this->geom, this->body);
}
DynamicsBox::~DynamicsBox()
{
dGeomDestroy(this->geom);
dBodyDestroy(this->body);
}
void DynamicsBox::setPosition(const Vec3& pos)
{
dBodySetPosition(this->body, pos.x, pos.y, pos.z);
}
Vec3 DynamicsBox::getPosition() const
{
auto values = dBodyGetPosition(this->body);
return Vec3(values[0], values[1], values[2]);
}
void DynamicsBox::setRotation(const Quaternion& q)
{
// ODEのQuaternionは(w, x, y, z)の順
dReal qValues[4] = { q.component.fv.w, q.component.fv.x, q.component.fv.y, q.component.fv.z };
dBodySetQuaternion(this->body, qValues);
}
Quaternion DynamicsBox::getRotation() const
{
auto qValues = dBodyGetQuaternion(this->body);
return Quaternion(qValues[1], qValues[2], qValues[3], qValues[0]);
}

void EntityWorld::setupWorld()
{
// 地面と箱の初期化
this->pGround = std::make_unique<StaticBox>(Vec3(20.0, 0.125, 20.0));
this->pBox = std::make_unique<DynamicsBox>(Vec3(2.0, 1.0, 1.0));

// ちょっと上に
this->pBox->setPosition(Vec3(0.0, 5.0, 0.0));
}

void EntityWorld::update()
{
// 衝突判定
dSpaceCollide(this->getCollisionSpace(), nullptr, &EntityWorld::nearCallback);
// ワールド時間をすすめる
this->step(1.0 / 60.0);
// 追加された衝突点を全消去する(次のstepで作用してほしくないので)
dJointGroupEmpty(this->getContactGroup());
}
void EntityWorld::nearCallback(void*, dGeomID o1, dGeomID o2)
{
// nearCallback(近似的に衝突判定された2つのジオメトリの詳細な当たり判定を行う)
// 最初何やってるかわからんかったけど、たぶんdCollideで最大N(ここでは10)個の衝突点を取って
// それをもとにContactJointを作ってJointGroupに登録してるっぽい
// 参照: http://www.natural-science.or.jp/article/20110211121736.php
const int N = 10;
dContact contact[N];
auto _this = DynamicsSystem::get();

int n = dCollide(o1, o2, N, &contact[0].geom, sizeof(dContact));
for (int i = 0; i < n; i++)
{
contact[i].surface.mode = dContactBounce;
contact[i].surface.mu = dInfinity;
contact[i].surface.bounce = 0.25;
contact[i].surface.bounce_vel = 0.0;
auto joint = dJointCreateContact(_this->getWorld(), _this->getContactGroup(), &contact[i]);
dJointAttach(joint, dGeomGetBody(contact[i].geom.g1), dGeomGetBody(contact[i].geom.g2));
}
}

実行結果はこう:



45度回転させる(setupWorldでsetRotationを行う)とこんな感じ:



ひたすらにオブジェクト化しているのでかなりややこしくなってます。

簡単に説明すると、床は物理挙動を行わず衝突判定のみとれるものとして、赤い箱は物理挙動を行うものとしてODEに登録しています。


ワールドを更新する

ワールド更新処理は(基本的には)dWorldStepで行います。ここで、タイマーを用意して正確な差分時間で更新していけば...と考える人がいるかもしれませんが、ODEのドキュメントに「むやみに時間を変更してはいけません」みたいに書いてあったのでやめたほうがいいです、と記憶しているのですが、0.13ではドキュメントのどこにもそんな感じの記述が見当たらないのでタイマー使って実際の差分時間で更新してもいいかもしれません。たぶん。時間の単位は秒で、小数点数が使えます。


衝突判定まわり

ワールド時間を進める前に衝突判定処理を行っていますが、これはワールド更新の間に衝突による反発を処理してもらうためです。さらにワールド更新の間のみ反発してもらいたいので、時間を進めた後はすぐに衝突点ジョイントをすべて消しています。


精密な衝突判定

2オブジェクト間の精密判定(EntityWorld::nearCallback)がかなりややこしいことになっていますが(コメントでも書いてますが)、要するに衝突点を得てそれを反発ジョイントに作り変えているだけです。これをしないと衝突してもすり抜けます。


そもそもなぜ精密な判定が別に必要なのか

精密な衝突判定はかなり重い処理です。凸包同士の衝突判定ではミンコフスキー差を取ってサポート写像とかいろいろ求めながら2凸包のミンコフスキー差が原点を含んでいるかを求めて衝突判定をする、GJK法と呼ばれるアルゴリズムを適用するらしいですが(GJK法はBulletで使われてるらしい)、これはループを使うので状況次第では処理が長くなります。

膨大(ほぼ無限大)な3D空間上でオブジェクトが衝突するのは極稀なので、毎回重い処理をしていたら性能に大きく響きます。なので、おおまかな当たり判定(空間切ってAABBとかで判定してると思う)を行って、それで当たったと判定されたら先に書いた精密な衝突判定を行う、という仕組みを用いることが多いです。3次元のAABBなら計算式3つだけで判定できますので。


応用

ここまでくればもうなんでもできるみたいなもんです(おおげさ)。

ここで紹介したクラス構成とは若干違っていますが、FPSみたいな何かを作ってみました(これがやってみたかった)。



ソースコードです。参考までに


Main.cpp

#include <Siv3D.hpp>


#define dDOUBLE
#include <ode/ode.h>
#pragma comment(lib, "ode_doubled")

// Key Input Shorten Macro
struct CameraController
{
static const double MouseSensitivity;
static const double MoveSpeed;

Vec3 point = Vec3::Backward * 5.0 + Vec3::Up * 1.0;
Vec2 rotate = Vec2(0.0, 4.0);
public:
void update()
{
const auto mouseDeltas = Window::Center() - Mouse::Pos();
this->rotate -= mouseDeltas * MouseSensitivity;
const auto camForward = this->getQuaternion();
auto moveVector = Vec3::Zero;
if (Key('W').pressed) moveVector += Vec3::Forward;
if (Key('S').pressed) moveVector += Vec3::Backward;
if (Key('A').pressed) moveVector += Vec3::Left;
if (Key('D').pressed) moveVector += Vec3::Right;
this->point += camForward * moveVector * MoveSpeed;
if (Key('Q').pressed) this->point += Vec3::Down * MoveSpeed;
if (Key('E').pressed) this->point += Vec3::Up * MoveSpeed;

Graphics3D::SetCamera(Camera(this->point, this->point + camForward * Vec3::Forward, Vec3::Up, 60.0, 0.1));
}
void printControls()
{
Println(L"Controls: ");
Println(L" WASD: Camera Move / Cursor: Camera Direction");
Println(L" Q: Down / E: Up");
}

Quaternion getQuaternion() const
{
return Quaternion::Pitch(Math::Radians(this->rotate.y)).yaw(Math::Radians(this->rotate.x));
}
Vec3 getForward() const { return this->getQuaternion() * Vec3::Forward; }
};
const double CameraController::MouseSensitivity = 1.0 / 8.0;
const double CameraController::MoveSpeed = 1.0 / 8.0;

class DynamicsWorldBase
{
dWorldID world;
public:
DynamicsWorldBase()
{
dInitODE2(0);
this->world = dWorldCreate();
dWorldSetGravity(this->world, 0.0, -9.81, 0.0);
}
virtual ~DynamicsWorldBase()
{
dWorldDestroy(this->world);
dCloseODE();
}

auto getWorld() const -> decltype(world) { return this->world; }
void stepWorld(dReal time) { dWorldStep(this->world, time); }
};
class DynamicsWorld : public DynamicsWorldBase
{
dSpaceID collSpace;
dJointGroupID contactGroup;
public:
DynamicsWorld() : DynamicsWorldBase()
{
this->collSpace = dHashSpaceCreate(0);
this->contactGroup = dJointGroupCreate(0);
}
virtual ~DynamicsWorld()
{
dJointGroupDestroy(this->contactGroup);
dSpaceDestroy(this->collSpace);
}

auto getCollisionSpace() const -> decltype(collSpace) { return this->collSpace; }
auto getContactGroup() const -> decltype(contactGroup) { return this->contactGroup; }
void clearContactJoints() { dJointGroupEmpty(this->contactGroup); }
};
struct DynamicsBox
{
dBodyID body; dMass mass; dGeomID geometry;
Vec3 size; ColorF color;

DynamicsBox(dReal totalMass, dReal lx, dReal ly, dReal lz, const ColorF& col);
DynamicsBox(DynamicsBox&& t)
{
OutputLog(LogDescription::App, L"Move ctor...");
this->body = t.body;
this->mass = std::move(t.mass);
this->geometry = t.geometry;
this->size = t.size;
this->color = t.color;
t.body = nullptr;
t.geometry = nullptr;
}
~DynamicsBox();

Vec3 getPosition() const;
Quaternion getRotation() const;
void setPosition(const Vec3& v3);
void realize() const;
};
class EntityWorld final : public DynamicsWorld
{
dGeomID ground;
std::list<DynamicsBox> boxes, projectiles;

static void nearCallback(void* pData, dGeomID o1, dGeomID o2);
void initGroundGeometry(dReal sizeXZ)
{
this->ground = dCreateBox(this->getCollisionSpace(), sizeXZ, 0.25, sizeXZ);
}
public:
EntityWorld() : DynamicsWorld()
{
this->initGroundGeometry(20.0);
}
~EntityWorld()
{
dGeomDestroy(this->ground);
}

void update();
void addRandomBox();
void addProjectile(const Vec3& origin, const Quaternion& fw);
void initBoxPositions();
void clearProjectiles();
void realizeAll() const;
};
template<typename WorldT>
struct DynamicsManager
{
static std::unique_ptr<WorldT> pWorldInstance;
public:
static WorldT* init()
{
if (pWorldInstance == nullptr) pWorldInstance = std::make_unique<WorldT>();
return pWorldInstance.get();
}
static WorldT* get() { return pWorldInstance.get(); }
};
template<typename WorldT> std::unique_ptr<WorldT> DynamicsManager<WorldT>::pWorldInstance;
using EntityManager = DynamicsManager<EntityWorld>;

const double Rate = 800.0;
void Main()
{
CameraController camctrl;
TimerMillisec retrigger;

auto dynamics = EntityManager::init();

// Graphics Settings
Graphics3D::SetAmbientLight(ColorF(0.25f));
// Graphics3D::SetLight(0, Light::Directional(Vec3(0.25f, 0.5f, 1.0f), Palette::White));
Graphics3D::SetLight(0, Light::Point(Vec3(4.0, 0.5, 4.0), 10.0));
Graphics3D::SetLight(1, Light::Point(Vec3(-4.0, 0.5, 4.0), 10.0));
Graphics3D::SetLight(2, Light::Point(Vec3(4.0, 0.5, -4.0), 10.0));
Graphics3D::SetLight(3, Light::Point(Vec3(-4.0, 0.5, -4.0), 10.0));

// Dynamics Settings
dynamics->initBoxPositions();

Cursor::SetPos(Window::Center());
while (System::Update())
{
ClearPrint();

// camctrl
camctrl.printControls();
camctrl.update();
Cursor::SetPos(Window::Center());

// dynamics control
Println(L"R: Reset Positions");
Println(L"Z: Add new one and reset positions");
if (Key('R').clicked)
{
dynamics->clearProjectiles();
dynamics->initBoxPositions();
}
if (Key('Z').clicked)
{
dynamics->addRandomBox();
dynamics->clearProjectiles();
dynamics->initBoxPositions();
}
if (Input::MouseL.pressed)
{
if (!retrigger.isActive)
{
dynamics->addProjectile(camctrl.point, camctrl.getQuaternion());
retrigger.restart();
}
}
if (Input::MouseL.released)
{
retrigger.reset();
}
if (retrigger.elapsed() > (60 * 1000) / Rate)
{
dynamics->addProjectile(camctrl.point, camctrl.getQuaternion());
retrigger.restart();
}
dynamics->update();

Box(Vec3::Down * 0.125, Vec3(20.0, 0.25, 20.0), Quaternion::Identity()).draw(Palette::White);
dynamics->realizeAll();
}
}

// Physics Core
void EntityWorld::clearProjectiles()
{
this->projectiles.clear();
}
void EntityWorld::initBoxPositions()
{
Vec3 nextPoint = Vec3::Up * 5.0;
for (auto& e : this->boxes)
{
e.setPosition(nextPoint);
nextPoint += Vec3::Up * 1.5 + Quaternion(Vec3::Up, Random(0.0, 360.0)) * Vec3::Forward * Random();
}
}
void EntityWorld::addRandomBox()
{
this->boxes.emplace_back(Random(0.125, 4.0), Random(0.25, 1.0), Random(0.25, 1.0), Random(0.25, 1.0), RandomColorH(1.0, 1.0));
}
void EntityWorld::addProjectile(const Vec3& origin, const Quaternion& fw)
{
this->projectiles.emplace_back(4.0, 0.125, 0.125, 1.0, Palette::Orangered);
dQuaternion dq = { fw.component.fv.w, fw.component.fv.x, fw.component.fv.y, fw.component.fv.z };
auto fwvec = fw * Vec3::Forward * 1000.0f * 20.0f;
fwvec += Quaternion(Vec3::Forward, Random(0.0, 360.0)) * Vec3::Left * Random(0.0, 0.4) * 1000.0f;
dBodySetQuaternion(this->projectiles.back().body, dq);
dBodySetPosition(this->projectiles.back().body, origin.x, origin.y, origin.z);
dBodySetForce(this->projectiles.back().body, fwvec.x, fwvec.y, fwvec.z);
}
void EntityWorld::realizeAll() const
{
for (const auto& e : this->boxes) e.realize();
for (const auto& e : this->projectiles) e.realize();
}

DynamicsBox::DynamicsBox(dReal totalMass, dReal lx, dReal ly, dReal lz, const ColorF& col)
: size(lx, ly, lz), color(col)
{
this->body = dBodyCreate(EntityManager::get()->getWorld());
dMassSetZero(&this->mass);
dMassSetBoxTotal(&this->mass, totalMass, lx, ly, lz);
dBodySetMass(this->body, &this->mass);
this->geometry = dCreateBox(EntityManager::get()->getCollisionSpace(), lx, ly, lz);
dGeomSetBody(this->geometry, this->body);
}
DynamicsBox::~DynamicsBox()
{
dGeomSetData(this->geometry, nullptr);
dGeomDestroy(this->geometry);
dBodyDestroy(this->body);
}

void EntityWorld::nearCallback(void*, dGeomID o1, dGeomID o2)
{
const int N = 10;
dContact contact[N];
auto _this = EntityManager::get();

int n = dCollide(o1, o2, N, &contact[0].geom, sizeof(dContact));
if (n > 0)
{
// Projectile->Destroy Flags
dGeomSetData(o1, reinterpret_cast<void*>(1));
dGeomSetData(o2, reinterpret_cast<void*>(1));
}
for (int i = 0; i < n; i++)
{
contact[i].surface.mode = dContactBounce;
contact[i].surface.mu = dInfinity;
contact[i].surface.bounce = 0.25;
contact[i].surface.bounce_vel = 0.0;
dContactJoint c(_this->getWorld(), _this->getContactGroup(), &contact[i]);
c.attach(dGeomGetBody(contact[i].geom.g1), dGeomGetBody(contact[i].geom.g2));
}
}

void EntityWorld::update()
{
dSpaceCollide(this->getCollisionSpace(), nullptr, &EntityWorld::nearCallback);
this->stepWorld(1.0 / 60.0);
this->clearContactJoints();

// Erase_if(remove_if+erase)を使いたいんだけどなぜか不定期にエラーで落ちるので昔ながらの方法で
for (auto& iter = std::begin(this->projectiles); iter != std::end(this->projectiles);)
{
if (reinterpret_cast<int>(dGeomGetData(iter->geometry)) == 1)
{
iter = this->projectiles.erase(iter);
}
else iter++;
}
}

Vec3 DynamicsBox::getPosition() const
{
const auto pVec = dBodyGetPosition(this->body);
return Vec3(pVec[0], pVec[1], pVec[2]);
}
Quaternion DynamicsBox::getRotation() const
{
const auto pVec = dBodyGetQuaternion(this->body);
return Quaternion(pVec[1], pVec[2], pVec[3], pVec[0]);
}
void DynamicsBox::setPosition(const Vec3& v3)
{
dBodySetForce(this->body, 0, 0, 0);
dBodySetLinearVel(this->body, 0, 0, 0);
dBodySetAngularVel(this->body, 0, 0, 0);
dBodySetPosition(this->body, v3.x, v3.y, v3.z);
}
void DynamicsBox::realize() const
{
Box(this->getPosition(), this->size, this->getRotation()).draw(this->color);
}



おわりに

「ODEで遊ぶ」というゆるいテーマの割にあんまりゆるくなかった気がしますが(当たり判定の話とか)、目的は一番最後のやつ(FPSもどき)なので、道中なんて別にどうでもよいのです(?)

それはさておき、物理エンジンは挙動がリアルすぎるゆえメインで使われたゲームタイトルは数える程度しか実際無い気がします。物理挙動を活かしたゲームとなるといろいろ限られてくるところありますし。ただ、苦労せずリアルな表現が手に入るという点では物理エンジンは(脇役として)よく使われています。ODEは初耳でも「Havok」という名前は聞いたことあるのではないでしょうか。参考としてSkyrimでは、(実はHavokはキャラクタの挙動にも使われているらしいですが、物理演算をするという点にフォーカスを当てれば、)例えば机上の小物が転がり落ちるといったような表現に使われています。

ちなみにHavokは機能限定ですが無料で提供されています。フルセットの使用やPC以外での配布、有料($10以上)配布を考えている際はライセンスを購入する必要がありますが、無料でプロユースの物理演算が使えるというのは魅力的なのでぜひODEを触った後にでも少し触ってみるといいと思います。

Siv3Dの3D表現能力はまだまだですが(影すらないので)、最近シェーダのサポートが始まったりTODOにfbxファイルのサポート予定があったりするので3D方面も順々に強化されていくと思います。たぶん。

というわけで、物理エンジンODEのちょっとした紹介とSiv3Dとの併用例の紹介でした。


参考文献