OpenSiv3Dで物理エンジンのBulletを使ってみた話
OpenSiv3Dには2D物理演算の機能があります、でも折角なら3Dのゲームが作りたいなと思いましたが、OpenSiv3Dの現行バージョン(0.6.6)には3D空間での物理演算を行う機能はありません。なのでBulletPhysicsを用いて3D物理演算してやろう!!って話です。
やってみよう!OpenSiv3DとBulletの導入
1. Visual Studio 2022のインストール
公式サイトからCommunity 2022をダウンロードします
https://visualstudio.microsoft.com/ja/vs/
指示にそってインストールします。 今回はC++での開発をするので「C++によるデスクトップ開発」のみ選択してインストールします!
2. OpenSiv3Dのインストール
公式サイトの手順に沿ってインストール
https://siv3d.github.io/ja-jp/
以上!
3. Bulletのインストール
1. ソースコードのダウンロードとサンプルの実行
Bullet公式Githubから最新のzipもしくはrawをダウンロード
https://github.com/bulletphysics/bullet3/releases/
(展開したフォルダ名を以降 Bullet とします。)
Bullet/build_visual_studio_vr_pybullet_double.bat を実行
Bullet/build3/vs2010 にある 0_Bullet3Solution.sln を実行してプロジェクトを開く
ソリューション操作の再ターゲットと出てくるので
Windows SDK バージョンを最新(10.0) プラットフォームツールセットを v143へのアップグレード
にしてすべてにチェックを付けたらOK
(そのままOK)

サンプルが動くか確認します。
ReleaseにしてF5キーを押してデバッグを開始してみます
このような画面が出ていたら成功です。

画面に出てきた箱を左クリックで持ち上げることができます。 (CtrlかAltと左クリックで画面を動かすことができます。)
他にもいろいろなサンプルがあるので遊んでみてください!
2. OpenSiv3Dで使うためにビルドする
ソリューションエクスプローラーから以下のプロジェクト
- BulletCollision
 - BulletDynamics
 - BulletSoftBody
 - LinearMath
 
をビルドします

この時それぞれのプロジェクトのプロパティで
構成: DebugとReleaseそれぞれ プラットフォーム: x64
構成プロパティ → C/C++ → コード生成 → ランタイムライブラリが
Releaseの時: マルチスレッド(/MT) Debugの時: マルチスレッドデバッグ(/MTd)
になっていることを確認してください。

バッチビルドで先ほどのプロジェクトを
x64でDebug | Releaseそれぞれ(計8つ)にチェックを付けビルドします。

これでBulletのインストールとビルドは完了です!
4. プロジェクトの作成と設定
1. プロジェクトの作成
Visual Studioを開き、新しいプロジェクトの作成
右側のリストからOpenSiv3D_0.6.6のテンプレートを探し、選択して次へ

名前を決めて作成!
2. プロジェクトの設定
プロジェクトのプロパティで構成をすべての構成になっているのを確認し、
C/C++ → 全般 → 追加のインクルードディレクトリ に Bullet/src を追加

リンカー → 全般 → 追加のライブラリディレクトリ に Bullet/bin を追加

C/C++ → プリプロセッサ -> プリプロセッサの定義 に
BT_THREADSAFE=1
BT_USE_DOUBLE_PRECISIONを追加

適用を押して設定を保存し、構成をDebugに変更 リンカー → 入力 → 追加の依存ファイルに
BulletCollision_vs2010_x64_debug.lib
BulletDynamics_vs2010_x64_debug.lib
BulletSoftBody_vs2010_x64_debug.lib
LinearMath_vs2010_x64_debug.libを追加

適用を押して設定を保存し、構成をReleaseに変更 リンカー → 入力 → 追加の依存ファイルに
BulletCollision_vs2010_x64_release.lib
BulletDynamics_vs2010_x64_release.lib
BulletSoftBody_vs2010_x64_release.lib
LinearMath_vs2010_x64_release.libを追加
以上でプロジェクトの設定が完了です!
実際に動かしてみる
Hello World! (?)
サンプルコード
#include <Siv3D.hpp> // OpenSiv3D v0.6.6
#include <btBulletDynamicsCommon.h>
void Main() {
    ///			 ///
    /// 空間の作成 ///
    ///			 ///
    
    // 重力
    btVector3 gravity{ 0, -9.8, 0 };
    // 衝突検知方法の設定
    btCollisionConfiguration* config = new btDefaultCollisionConfiguration{ };
    btCollisionDispatcher* dispatcher = new btCollisionDispatcher{ config };
    // ブロードフェーズ法の設定
    btDbvtBroadphase* broadphase = new btDbvtBroadphase{ };
    // 拘束(剛体間リンク)のソルバ設定
    btSequentialImpulseConstraintSolver* solver = new btSequentialImpulseConstraintSolver{ };
    // 空間の作成
    btDynamicsWorld* world = new btDiscreteDynamicsWorld{ dispatcher, broadphase, solver, config };
    // 重力の設定
    world->setGravity(gravity);
    ///			 ///
    /// 剛体の作成 ///
    ///			 ///
    // 質量
    double mass = 1.0;
    // 中心位置
    btVector3 center{ 0, 0, 0 };
    // 形状の設定 (箱)
    btCollisionShape* shape = new btBoxShape{ btVector3{ 1, 1, 1 } };
    // 初期位置と姿勢
    btMotionState* state = new btDefaultMotionState{ btTransform{ btQuaternion::getIdentity(), center } };
    // 剛体の作成
    btRigidBody* box = new btRigidBody{ mass, state, shape, center };
    // 空間に追加
    world->addRigidBody(box);
    // システムループ
    while (System::Update()) {
        ClearPrint();
        // Bulletのステップを進める
        world->stepSimulation(Scene::DeltaTime());
        // 位置の取得
        btVector3 pos = box->getCenterOfMassPosition();
        // 位置の表示
        Print << U"box position: {:.2F}, {:.2F}, {:.2F}"_fmt(pos.x(), pos.y(), pos.z());
    }
    // 剛体を空間から削除
    world->removeRigidBody(box);
    // 剛体の削除
    delete shape;
    delete state;
    delete box;
    // 空間の削除
    delete config;
    delete dispatcher;
    delete broadphase;
    delete solver;
    delete world;
}
実際にこれを動かしてみると画面に
box position: 0.00, 0.00, 0.00
と表示され、真ん中の数字がどんどん小さくなっているかと思います。
これは空間に質量 1.0 の箱を置き、箱が落ち続けるだけのプログラムです。
まず、Bulletエンジンで物理演算をするにはシミュレーション空間を設定しないといけません。 それらをしているのが9行目から26行目のところです。
// 重力
btVector3 gravity{ 0, -9.8, 0 };
// 衝突検知方法の設定
btCollisionConfiguration* config = new btDefaultCollisionConfiguration{ };
btCollisionDispatcher* dispatcher = new btCollisionDispatcher{ config };
// ブロードフェーズ法の設定
btDbvtBroadphase* broadphase = new btDbvtBroadphase{ };
// 拘束(剛体間リンク)のソルバ設定
btSequentialImpulseConstraintSolver* solver = new btSequentialImpulseConstraintSolver{ };
// 空間の作成
btDynamicsWorld* world = new btDiscreteDynamicsWorld{ dispatcher, broadphase, solver, config };
// 重力の設定
world->setGravity(gravity);シミュレーション空間の定義ができたので、次は空間に箱を置きます。 それらをしているのが32行目から48行目です。
// 質量
double mass = 1.0;
// 中心位置
btVector3 center{ 0, 0, 0 };
// 形状の設定 (箱)
btCollisionShape* shape = new btBoxShape{ btVector3{ 1, 1, 1 } };
// 初期位置と姿勢
btMotionState* state = new btDefaultMotionState{ btTransform{ btQuaternion::getIdentity(), center } };
// 剛体の作成
btRigidBody* box = new btRigidBody{ mass, state, shape, center };
// 空間に追加
world->addRigidBody(box);これで空間に箱が設置できました! でもこのままだと箱が動かないのでシミュレーション空間を
// Bulletのステップを進める
world->stepSimulation(Scene::DeltaTime());で動かします。 これで箱の設置と物理演算ができます!
以下のgetCenterOfMassPositionで剛体の中心位置を取得します。
// 位置の取得
btVector3 pos = box->getCenterOfMassPosition();受け取る値の型はbtVector3(Bulletの3次元ベクトル)で、
そのままではSiv3DのPrintで使えないので
// 位置の表示
Print << U"box position: {:.2F}, {:.2F}, {:.2F}"_fmt(pos.x(), pos.y(), pos.z());で表示します。
使い終わったら剛体を空間から取り除いてnewした変数をdeleteします。
// 剛体を空間から削除
world->removeRigidBody(box);
// 剛体の削除
delete shape;
delete state;
delete box;
// 空間の削除
delete config;
delete dispatcher;
delete broadphase;
delete solver;
delete world;Siv3Dで描画
このままだとコードが助長になりみにくいので簡単に使えるように適当にライブラリを作ったので こちらからヘッダファイルをコピペして
https://github.com/tas9n/OpenSiv3DBulletPhysics
こちら↓のコードを実行してみましょう!
コード
#include <Siv3D.hpp> // OpenSiv3D v0.6.6
#include "SivBullet.hpp"
void Main() {
    // 3D シーンを描く、マルチサンプリング対応レンダーテクスチャ
    const MSRenderTexture rt{ Scene::Size(), TextureFormat::R8G8B8A8_Unorm_SRGB, HasDepth::Yes };
    // 背景色 (removeSRGBCurve() で sRGB カーブを除去)
    Color bgColor = Palette::Skyblue.removeSRGBCurve();
    // デバッグカメラ
    DebugCamera3D camera{ rt.size(), 60_deg, Vec3{0, 30, -100}, Vec3{0, 0, 0} };
    // シミュレーション空間の定義
    DynamicsWorld world{ Vec3{ 0, -9.8, 0 } };
    // 床
    // 質量 0.0 で { 0, 0, 0 } に { 100, 0.1, 100 } の大きさの箱
    auto ground = world.createBox(Vec3{ 0, 0, 0 }, Vec3{ 100, 0.1, 100 }, 0.0);
    // 箱の配列
    Array<BulletBody> boxes;
    // システムループ
    while (System::Update()) {
        // 空間の更新
        world.update(Scene::DeltaTime());
        // Enterキーで箱を追加
        if (KeyEnter.down()) {
            boxes << world.createBox(Vec3{ Random(-50, 50), 50, Random(-50, 50)}, Vec3{1, 1, 1}, 1.0);
        }
        // カメラの更新
        camera.update();
        // カメラを3D空間に設定
        Graphics3D::SetCameraTransform(camera);
        // [3D 描画]
        {
            const ScopedRenderTarget3D target{ rt.clear(bgColor) };
            // 床の描画
            ground.draw();
            // 箱の描画
            for (const auto& box : boxes) {
                box.draw();
            }
        }
        // [3D を 2D に描画]
        {
            Graphics3D::Flush();
            rt.resolve();
            Shader::LinearToScreen(rt);
        }
    }
}Enterキーを押すごとにランダムな位置から箱が降ってきます!
#include "SivBullet.hpp"でSivBullet.hppをincludeして
DynamicsWorldクラスでシミュレーション空間を定義します。
剛体の作成にはDynamicsWorldクラスのcreate〇〇メソッドを使い、剛体の作成と空間へ追加をしてくれます。
戻り値がBulletBodyクラスになっているので、戻り値をArrayに追加してdrawメソッドでそれぞれの形状を描画しています。
DynamicsWorldやBulletBodyは自動でメモリの開放をしてくれるので、deleteはしなくてOK!
以上!!!!
※ヘッダファイルはBoxやSphereなど、簡単な形の剛体を作って空間に追加するくらいなので 剛体の設定などいろいろは自分で書き足してみてください。
参考記事
OpenSiv3Dリファレンス
https://zenn.dev/reputeless/books/siv3d-documentation
Bulletについての記事
http://slis.tsukuba.ac.jp/~fujisawa.makoto.fu/lecture/iml/text/2_bullet.html