サイトのトップへ戻る

libGDX ドキュメント 日本語訳

サイト内検索

libGDX 3D 物理演算のBullet ラッパーを使用する - パート2

Bullet は衝突判定と剛体力学のライブラリです。 このチュートリアルの第二部では、 剛体力学libGDX Bullet ラッパーを使用する方法について学びます。 このパートでは、力の適用や衝突への反応など現実世界の物理現象を再現します。

このチュートリアルの最初のパートでは、Bullet ラッパーが何なのかと、それを衝突判定に使用する方法について見てきました。 あなたが既にこれを読んでいるものとして説明を行います。 今回のチュートリアルでは、前回のコードをベースとして使用します。

Bullet ラッパーのソースを見ると、 5つのパーツで構成されてそれぞれが独自の java パッケージに属していることが分かります。 パッケージは以下の通りです:

  • linearmath
  • collision
  • dynamics
  • softbody
  • extras

linearmath パッケージには、物理演算には直接関係しない、いくつかの一般的なクラスとメソッドが含まれています。 これは例えばbtVector3クラスなどで、LibGDX のVector3クラスへの橋渡しをします。 collisionパッケージには、衝突形状やcollision worldやブロードフェイズやニアフェイズのクラスといった、これまで見てきた衝突判定に関わる全てのものが含まれています。 dynamics には、このチュートリアルで見ていく剛体力学に関する全てのものが含まれています。 softbodyパッケージでは、(剛体とは対照的に)柔体シミュレーションとクロスシミュレーションに関する全てのものが含まれています。 そして最後に、extraパッケージにはいくつかの便利なヘルパークラス/ツールが含まれています。現時点では以前に保存した bullet worldをインポートする機能のみが含まれています (例えば、Blenderなどから)。

これらパッケージの順番は重要なので注意してください。 collision パッケージはlinearmath パッケージのクラスを使用しますが、dynamics パッケージのクラスは使用しません。 同様にdynamics パッケージはcollision パッケージには依存していますが、softbody パッケージには依存していません。 もしくはこう言い換えられます。dynamicscollisionの上で構築されていると。 dynamicsを扱う時、collisionパッケージの機能も使用することができます。



dynamic のプロパティを追加する

dynamicsを適用する前に、Bullet が計算を実行するのに必要ないくつかのプロパティを設定しなければなりません。 例えばオブジェクトを押す(力を適用する)時、結果が発生するうえではオブジェクトの重さ(質量)が重要になります。 オブジェクトが非常に重い場合は全く動かないかもしれません。しかし軽い場合はかなりの距離を移動するかもしれません。 オブジェクトとそれが接している面との間で生じる摩擦のような、他のプロパティも関連してきます。 btCollisionObject にそうしたプロパティは含まれていないので、btRigidBodyと呼ばれるサブクラスを使用する必要があります。


public class BulletTest implements ApplicationListener {
    ...
    static class GameObject extends ModelInstance implements Disposable {
        public final btRigidBody body;
        public boolean moving;

        public GameObject (Model model, String node, btRigidBody.btRigidBodyConstructionInfo constructionInfo) {
            super(model, node);
            body = new btRigidBody(constructionInfo);
        }

        @Override
        public void dispose () {
            body.dispose();
        }

        static class Constructor implements Disposable {
            public final Model model;
            public final String node;
            public final btCollisionShape shape;
            public final btRigidBody.btRigidBodyConstructionInfo constructionInfo;
            private static Vector3 localInertia = new Vector3();

            public Constructor (Model model, String node, btCollisionShape shape, float mass) {
                this.model = model;
                this.node = node;
                this.shape = shape;
                if (mass > 0f)
                    shape.calculateLocalInertia(mass, localInertia);
                else
                    localInertia.set(0, 0, 0);
                this.constructionInfo = new btRigidBody.btRigidBodyConstructionInfo(mass, null, shape, localInertia);
            }

            public GameObject construct () {
                return new GameObject(model, node, constructionInfo);
            }

            @Override
            public void dispose () {
                shape.dispose();
                constructionInfo.dispose();
            }
        }
    }
    ...
    @Override
    public void create () {
        ...
        constructors = new ArrayMap<String, GameObject.Constructor>(String.class, GameObject.Constructor.class);
        constructors.put("ground", new GameObject.Constructor(model, "ground", new btBoxShape(new Vector3(2.5f, 0.5f, 2.5f)), 0f));
        constructors.put("sphere", new GameObject.Constructor(model, "sphere", new btSphereShape(0.5f), 1f));
        constructors.put("box", new GameObject.Constructor(model, "box", new btBoxShape(new Vector3(0.5f, 0.5f, 0.5f)), 1f));
        constructors.put("cone", new GameObject.Constructor(model, "cone", new btConeShape(0.5f, 2f), 1f));
        constructors.put("capsule", new GameObject.Constructor(model, "capsule", new btCapsuleShape(.5f, 1f), 1f));
        constructors.put("cylinder", new GameObject.Constructor(model, "cylinder", new btCylinderShape(new Vector3(.5f, 1f, .5f)), 1f));
        ...
    }
}

githubにある全ソースコードを参照してください

上記コードでは、 GameObjectクラス内で使用していた btCollisionObjectクラス を、btRigidBodyクラスに置き換えています。 btRigidBodybtCollisionObjectを継承したクラスです。 body のインスタンスを作成するために、今回は btRigidBodyConstructionInfoクラスを使用しています。 このクラスには、これまでに使用してきたような btCollisionShapeが含まれていますが、 massfrictiondampingなどのような他のプロパティも含まれています。 今回は Constructor クラスにもこの constructionInfoが含まれており、 これにより、これまで見てきたような同一オブジェクトの複数のインスタンスを作成する便利な方法が使えるようになります。

btRigidBodyConstructionInfoを作成する時には、まず引数としてmass (そのオブジェクトの重さ)を指定する必要があります。 二番目の引数にはnull を使用します(理由は後で説明します)。 次に三番目の引数としてbtCollisionShapeを渡します。 最後の引数に localInertiaを設定する必要があります。 各Constructorで再利用できるようにするため、localInertiaはstatic Vector3となっています。 mass に0以下の値だった場合、慣性力(localInertia)にも0を設定するだけです。 0よりも大きい場合は、慣性力を計算する必要があります。 幸い、衝突形状は慣性力を計算するための素晴らしいヘルパーメソッドを持っています。

btRigidBodyConstructionInfobtCollisionShapeの情報を保持しているのに、ConstructorクラスでもbtCollisionShapeを保持したままにしていることに注意してください。 これは、このshapeへの参照を保持したままにしておき、不要になった場合にはdisposeを行う必要があるためです。 もちろん、constructionInfoメンバにも同じことが言えるので、disposeメソッド内で同様にconstructionInfoを破棄するための行も追加しました。

テストで使用するコンストラクタはほとんど同じままですが、今回はConstructorを作成する際にmass を引数として渡す必要があります。 今のところは、地面を除く各オブジェクトには1fの質量(mass)を使用します。地面では 0fの質量を使用します。

0の質量というのは、物理的には有り得ません。 地面は自身に適用されるいかなる力にも反応しない、ということを示すためにこれを使います。 地面はどのような力や衝突が適用されたとしても、常の同じ位置(と同じ回転状態)を保ち続けます。 これは "静的(static)" オブジェクトと呼ばれます。 それ以外のオブジェクト (0より大きい質量を持つ) は "動的(dynamic)" オブジェクトと呼ばれます。



単位について少しメモ

ご存知のように、ほとんどの物理演算プロパティでは使用する単位を指定する必要があります。 例えば、質量は一般的にはキログラム単位で指定されています。 オブジェクトのサイズはメートル単位で指定され、時間は秒単位で指定されます。 これらは国際単位系と呼ばれてます。 可能であればそれらの単位を使用することをお勧めします。 しかし場合によってはそれらを使用することが実用的ではないこともあります。例えば、使用するオブジェクトがとても大きかったりととても小さかったりする場合です。

値が約1(1f)の場合にBullet はベストなパフォーマンスを発揮します。 そこには多くの要因がありますが(浮動小数点の精度など)、実際にはbtRigidBody のプロパティの値は約1に保つのがベストです。 そのため、オブジェクトが1メートルよりはるかに大きくなる可能性が高い、宇宙を舞台にしたゲームを作成している場合は、メートルではなくデカメートル(10)や ヘクトメートル(*100) やキロメートル (*1000)を使用したほうが良いかもしれません。 もしくは、オブジェクトが1メートルよりはるかに小さくなる可能性が高いゲームを作成している場合は、デシメートル(0.1)や センチメートル (*0.01) やミリメートル(*0.001)を使用したほうが良いかもしれません。 同様に、オブジェクトが1キログラムよりもはるかに重くなったりする場合や軽くなったりする場合も、質量の単位を変えたほうが良いかもしれません。

一貫性を保っている限り、単位を変更するのは一般的には問題ありません。 そのため、メートルではなくインチを使用する場合、速度にはインチ毎秒を使用し、加速度(重力のような)にはインチ毎秒毎秒を使用し、力にはキログラム・インチ毎秒毎秒を使用する必要があります。 詳細情報については この記事 を読んでください。

ほとんどの場合において(視覚的) modelのサイズと物理本体のサイズは同じになり、modelインスタンスの変換行列には拡大縮小用のコンポーネントを含むことができない、ということを覚えておいてください。 したがって、modelのサイズは使用する単位と一致させる必要があり、モデリングソフトからmodelをエクスポートする前に、適用すべき拡大縮小を "適用しておく"必要があります。



dynamics worldを追加する

実際に力学を適用するには、 dynamics world(btDynamicsWorld)を追加する必要があります。 これを以前使用していた btCollisionWorldと置き換えます(btCollisionWorldよりも多機能です)。:


public class BulletTest implements ApplicationListener {
    ...
    btDynamicsWorld dynamicsWorld;
    btConstraintSolver constraintSolver;

    @Override
    public void create () {
        ...
        collisionConfig = new btDefaultCollisionConfiguration();
        dispatcher = new btCollisionDispatcher(collisionConfig);
        broadphase = new btDbvtBroadphase();
        constraintSolver = new btSequentialImpulseConstraintSolver();
        dynamicsWorld = new btDiscreteDynamicsWorld(dispatcher, broadphase, constraintSolver, collisionConfig);
        dynamicsWorld.setGravity(new Vector3(0, -10f, 0));
        contactListener = new MyContactListener();

        instances = new Array<GameObject>();
        GameObject object = constructors.get("ground").construct();
        instances.add(object);
        dynamicsWorld.addRigidBody(object.body, GROUND_FLAG, ALL_FLAG);
    }

    public void spawn () {
        GameObject obj = constructors.values[1 + MathUtils.random(constructors.size - 2)].construct();
        obj.transform.setFromEulerAngles(MathUtils.random(360f), MathUtils.random(360f), MathUtils.random(360f));
        obj.transform.trn(MathUtils.random(-2.5f, 2.5f), 9f, MathUtils.random(-2.5f, 2.5f));
        obj.body.setWorldTransform(obj.transform);
        obj.body.setUserValue(instances.size);
        obj.body.setCollisionFlags(obj.body.getCollisionFlags() | btCollisionObject.CollisionFlags.CF_CUSTOM_MATERIAL_CALLBACK);
        instances.add(obj);
        dynamicsWorld.addRigidBody(obj.body, OBJECT_FLAG, GROUND_FLAG);
    }
    ...
    @Override
    public void dispose () {
        ...
        dynamicsWorld.dispose();
        constraintSolver.dispose();
        ...
    }
}

collisionWorldメンバの名前をdynamicsWorldに変更し、型をbtDynamicsWorld (btCollisionWorldのサブクラス)にしました。 dynamics worldのインスタンスを作成するには、btConstraintSolverがh必要です。 このチュートリアルではこの制限(constraint)について詳しく説明しませんが、ご想像通り、このクラスはそういった制限を解決するために使用します(簡単に言うと: オブジェクトをお互いに結合する際の制限です)。 dynamics worldのために btDiscreteDynamicsWorld 実装を使用します。

dynamics worldを作成した後、その世界の重力も設定します。 これにより全ての動的オブジェクト(静的な地面以外)に重力が適用されます。 テストのために、Y軸に沿って毎秒-10メートルの重力を使用します。 これは地球の重力に近い値です。

addCollisionObject メソッドを使用する代わりに、今回は addRigidBody メソッドを使ってworldにオブジェクトを追加します。 addCollisionObject メソッドも引き続き使用できますが、addRigidBodyメソッドでは例えば、重力を各オブジェクトに正しく適用できるようになるので覚えておいてください。

今は重力によってオブジェクトが下に落ちるので、オブジェクトを手動で移動させる必要はありません。 その代わりに、重力を適用して物体の変形状態を更新するように、worldに指示を出す必要があります。

    @Override
    public void render () {
        final float delta = Math.min(1f / 30f, Gdx.graphics.getDeltaTime());

        dynamicsWorld.stepSimulation(delta, 5, 1f/60f);

        for (GameObject obj : instances)
            obj.body.getWorldTransform(obj.transform);
        ...
    }

githubにある全ソースコードを参照してください

performDiscreteCollisionDetection メソッドの代わりに、今回はworldの stepSimulation メソッドを呼び出しています。 このメソッドでも、内部でperformDiscreteCollisionDetection メソッド (衝突コールバックを含む)を呼び出しています。

離散力学の世界では、固定時間ステップを使用します。これは基本的には、常に同じdelta 値を使って計算を行うという意味です。 この一定のdelta 値は、 stepSimulationの三番目の引数として渡されます。 実際のdelta 値(最初の引数)が希望する固定delta 値より大きい場合、その計算は複数回行われます。 この計算が行われる最大回数(サブステップの最大回数)は二番目の引数で設定します。 delta値が1f/30fを超えないようにしているので、実際のサブステップの最大回数は2を超えることはないということを覚えておいてください。 これの詳細について知りたい場合、タイムステップの修正に関する情報はたくさんあります。 しかし、実際はBullet が私達に代わってそれを管理しており、引数について理解しているのであれば特に問題はありません。

今はBulletがオブジェクトの変換処理(移動と回転)を行っているので、Bulletに現在の変換状態を問い合わせて、それを ModelInstance#transformに設定します。 これはobj.body.getWorldTransform(obj.transform);を呼び出すことで行います。

このコードを実行すると、希望通りの結果が表示されるでしょう。 オブジェクトは重力の力を受けて静的な地面の上に落ちます。 このチュートリアルの前回のパートで見たような衝突フィルターを使用しているので、オブジェクトはお互いに作用しません(お互いを素通りして落ちていきます)。

bullet6

これでworld は移動するオブジェクトを完全に制御しているので、GameObject クラスのmovingメンバはもう必要ありません。 したがって、このメンバを削除してもよいでしょう。 その変更部分について記載はしていませんが(movingメンバを使用している行を削除するだけです)、空のcontact listenerは残したままにしています。 contact listener がまだ働いていることを確認するため、オブジェクトが地面に当たった時にその色が変わるようにしてみましょう。


public class BulletTest implements ApplicationListener {
    ...
    class MyContactListener extends ContactListener {
        @Override
        public boolean onContactAdded (int userValue0, int partId0, int index0, int userValue1, int partId1, int index1) {
            if (userValue0 != 0)
                ((ColorAttribute)instances.get(userValue0).materials.get(0).get(ColorAttribute.Diffuse)).color.set(Color.WHITE);
            if (userValue1 != 0)
                ((ColorAttribute)instances.get(userValue1).materials.get(0).get(ColorAttribute.Diffuse)).color.set(Color.WHITE);
            return true;
        }
    }
    ...
}

githubにある全ソースコードを参照してください

bullet7



MotionStateを使用する

先ほど追加した以下のコードを見てください:


    @Override
    public void render () {
        ...
        for (GameObject obj : instances)
            obj.body.getWorldTransform(obj.transform);
        ...
    }

基本的には、これは全てのオブジェクトの現在の位置情報と回転情報をbullet へ問い合わせています。 ご想像の通り、大きなworldを使用している場合だと、render呼び出しの度にかなりのオブジェクトに対して位置情報と回転情報のポーリングが発生します。 しかし、実際に移動や回転が行われるオブジェクトは、あったとしても少しだけしかありません。 幸いBulletにはオブジェクトの変形があった時にそれを通知するメカニズムが用意されているので、全てのオブジェクトに対して反復処理を行う必要はありません。 このためには、btMotionStateと呼ばれる小さなコールバックを使用します。

btMotionState クラスは二つのメソッドを持っており、両方とも上書きする必要があります。 Bulletが動的オブジェクトの変換を行う度に、 setWorldTransformが呼び出されます。 Bulletが現在のオブジェクトの変換情報を知る必要がある度に、getWorldTransformが呼び出されます(例えば、worldにオブジェクトが追加された時など)。


    static class MyMotionState extends btMotionState {
        Matrix4 transform;
        @Override
        public void getWorldTransform (Matrix4 worldTrans) {
            worldTrans.set(transform);
        }
        @Override
        public void setWorldTransform (Matrix4 worldTrans) {
            transform.set(worldTrans);
        }
    }

上記コードでは、Matrix4インスタンスを更新するたけの非常に基本的なMotionState を実装しています。 もちろん、同様に他の操作を行うことも可能です。 例えば今回のテストでは、オブジェクトが地面から落ちた場合(位置情報の y値が、0もしくは特定の閾値を下回った場合)、そのオブジェクトをworldから削除します。

今度は、このMotionStateを使用することはBulletに通知する必要があります。


    static class GameObject extends ModelInstance implements Disposable {
        public final btRigidBody body;
        public final MyMotionState motionState;

        public GameObject (Model model, String node, btRigidBody.btRigidBodyConstructionInfo constructionInfo) {
            super(model, node);
            motionState = new MyMotionState();
            motionState.transform = transform;
            body = new btRigidBody(constructionInfo);
            body.setMotionState(motionState);
        }

        @Override
        public void dispose () {
            body.dispose();
            motionState.dispose();
        }
        ...
    }

上記コードでは、MotionStateのインスタンスを作成し、そのtransformにModelInstanceのtransformメンバを設定します。 これは参照によって行われているので、MotionState内からMotionStateのtransformメンバが直接更新されることを注意してください。 setMotionState を呼び出して、このMotionStateを使用することをBulletへ通知します。 これによって、BulletはMotionStateの getWorldTransformを呼び出してそのオブジェクトの現在の変換情報を取得することもできます。

また、spawn メソッドも少し修正する必要があります。


    public void spawn () {
        GameObject obj = constructors.values[1 + MathUtils.random(constructors.size - 2)].construct();
        obj.transform.setFromEulerAngles(MathUtils.random(360f), MathUtils.random(360f), MathUtils.random(360f));
        obj.transform.trn(MathUtils.random(-2.5f, 2.5f), 9f, MathUtils.random(-2.5f, 2.5f));
        obj.body.proceedToTransform(obj.transform);
        obj.body.setUserValue(instances.size);
        obj.body.setCollisionFlags(obj.body.getCollisionFlags() | btCollisionObject.CollisionFlags.CF_CUSTOM_MATERIAL_CALLBACK);
        instances.add(obj);
        dynamicsWorld.addRigidBody(obj.body, OBJECT_FLAG, GROUND_FLAG);
    }

上記コードの唯一の変更は、bodyのsetWorldTransform 呼び出しを削除して代わりに proceedToTransformメソッドを呼び出しているということです。 これはBulletに対して、オブジェクトのワールド変換行列だけではなく、その他全ての関連メンバも更新するように指示を出しています。

最後に、renderメソッド内では各オブジェクトの変換状態をポーリングする必要はもうないので、そのコードを削除しましょう。


    @Override
    public void render () {
        final float delta = Math.min(1f / 30f, Gdx.graphics.getDeltaTime());

        dynamicsWorld.stepSimulation(delta, 5, 1f/60f);

        if ((spawnTimer -= delta) < 0) {
            spawn();
            spawnTimer = 1.5f;
        }
        ...
    }

githubにある全ソースコードを参照してください

上手く理解できれば、MotionStateは非常に強力で非常に簡単に扱えます。 しかし、明示的なものではありませんがmotion stateを使用する利点がもう1つあります。 これを理解するために、dynamicsWorld.stepSimulation(delta, 5, 1f/60f);を呼び出した時に何が起きるかを考えてみてください。 Bullet は、渡された実際の経過差分(delta)時間に達するまで、指定された1f/60fを差分時間として使って複数回の計算を行います(ただし計算回数は指定された最大値を超えません)。 当然のことながら、delta値が常に正確な1f/60fの倍数になるということは非常にまれです。 実際、場合によってはdelta 値が指定された1f/60fよりも小さくなる可能性があり、そうなるとBullet は必要な計算が一切行えなくなります。

したがって、obj.body.getWorldTransform() メソッドを使って取得した変換状態は現在の時点でのものではなく、 一定の時間ステップの最後の時点で計算されたものになります。 これを補うために、Bullet は計算した変換状態を補完することで現時点の変換状態に近づけます。 この補完された変換状態は、MotionStateのsetWorldTransform メソッド内で引数として受け取れます。

そのため、MotionStateを使用すると時間ステップ間の状態遷移が視覚的にスムーズになります。 ですが 視覚的に という言葉に注意してください。実際の衝突判定と動的計算(コールバックを含む)は固定時間ステップでのみ行われます。



舞台裏を覗いてみる

Bullet が補完された変換行列を我々に提供してくれるのは素晴らしいことです。 これは些細なことかもしれませんが、Bulletの内部がどうなっているのか気になることでしょう。 例えば、この補完された変換情報はどのように計算されるのかを見てみましょう。 ちょうどテストクラスでやったのと同じ様に、ボールが落ちるのを考えてみましょう。ただしボールの他にオブジェクトはありません。 ボールの開始位置はx=0, y=0, z=0としますが、簡単にするためこの例ではy座標のみ見ていきます。 シミュレーションの開始時には、ボールには速度がありません (毎秒0メートルの速度で "落ちます" )。 ボールには重力のみを適用します。これは10メートル毎秒毎秒の加速で落下します。:


position(0) = 0f;
velocity(0) = 0f;
acceleration = -10f;

ご存知のように、この情報を使って任意の時点でのボールの位置を計算できます。 1秒後、速度は毎秒10メール増加し、ボールは5メートル下に落下しています、など:


position(t) = 0.5 * acceleration * t * t;
position(0) = 0;
position(1) = -5;
position(2) = -20;
position(3) = -45;
position(4) = -80;

この例の方程式を理解する必要はありませんが、全く見たことがないのであれば一度読み解いてみることをお勧めします。

では、3.5秒が経過した状態でのボールの位置はどこか知りたいとしましょう。 最も分かりやすいやり方は、上記の公式: position(3.5) = 0.5 * -10 * 3.5 * 3.5 = -61.25;を使って新しい位置を計算することです。 これは最も正確なやり方ですが、ほとんどの状況では上手く動作しません。 実際には、可変時間ステップを使うのと同じことになってしまいます。この可変時間ステップには多くの欠点があります。 例えば、time=0 とtime=3.5の間で発生する全ての衝突を取り逃がしてしまいます。 そして、デルタ時間は毎回違うので、結果の再現性がなくなってしまうのです(物理シミュレーションのリプレイができないということです)。 stepSimulationメソッドの二番目の引数maxSubstepsに0の値を設定することでこのやり方をするようBullet へ指示することはできますが、はっきり言ってお勧めはしません。

先ほど説明したように、私達はゲームで固定時間ステップを使用しています。 例えば、固定時間ステップが1秒であるとしましょう。 time = 1の位置を計算します(そして衝突判定を実行してその結果に応じて処理を行います)。 次にtime = 2の位置を計算します(また衝突判定を実行してその結果に応じて処理を行います)。 最後にtime = 3の位置を計算します(そして衝突判定を実行してその結果に応じて処理を行います)。 それから、どうにかしてtime = 3.5の位置の近似値を求める必要があります。 それでは、どんな計算方法があるのか見てみましょう:

  1. time = 4の位置を計算して、単にtime = 3 と time = 4の間の値を補間します。:

position(3.5) = position(3) + 0.5 * (position(4) - position(3)) = -45 + 0.5 * -35 = -62.5

まあ、これはかなり近い値です。しかし、大きな欠点が1つあります: we can't look into the future. 我々は未来を見ることはできません。 time = 4の位置を計算するには、それが実際に起こる前に衝突判定を実行してその結果に応じて処理を行う必要があります。 これは、例えば接触がまだ発生していないのにContactListener の呼び出しが彦起こされる原因になります。 このやり方には、同様にいくつかの実用的な問題があります。 例えば、二つの変換処理の間で補間を行うことで、結果がおかしくなる可能性があります。

  1. time = 3 の時点でのボールの速度が分かっているので、それを使ってtime = 3.5 の時点でのボールの位置の近似値を求める:

velocity(3) = 3 * -10 = -30;
position(3.5) = position(3) + (3.5 - 3) * velocity(3) = -45 + 0.5 * -30 = -60

これもかなり近い値です。実際、Bullet ではかなり以前からこれが既定の方法でした。 The main problem with this approach, however, is that the approximated transformation doesn't have to line up with the actual location at time = 4. For example if a collision is ahead or for some other reason the velocity is changed.

  1. 前の二つのやり方の主な問題は、あなたが未来を見ることができないという点です。 そのためこの代替案では、文字通り過去を見ることでこれに対処します。 time = 3の位置の近似値を求めるのではなく、このやり方では time = 2.5の位置の近似値を求めます:

velocity(3) = 3 * -10 = -30;
position(2.5) = position(3) - (3 - 2.5) * velocity(3) = -45 - 0.5 * -30 = -30

このやり方の重要な要素は、一貫性です。 The visual representation is always exactly one time step behind. Although this is in most scenario's not noticeable, you could compensate for this in your game logic.

Bullet では、setLatencyMotionStateInterpolationメソッドを使って二番目と三番目のやり方のうちどちらかを選ぶことができます。 例えば、二番目の方法を選ぶ場合は以下のようにします:


((btDiscreteDynamicsWorld)dynamicsWorld).setLatencyMotionStateInterpolation(false);

これらのやり方は、読みやすくするために計算を簡略化しています。 実際には、Bullet はこれよりもかなり複雑で正確な計算を駆使して希望するtransformationを見積もっています。 この計算の入力した値は、衝突オブジェクトの専用のメンバに保存されます。 専用メンバの名前は InterpolationWorldTransformInterpolationLinearVelocityInterpolationAngularVelocityです。 それぞれのメソッドを使ってそれら専用メンバの取得と設定を行えます。例えば: object.setInterpolationWorldTransform(transform);setWorldTransformの代わりにproceedToTransformメソッドを使用する際に、どのようにspawn()メソッドを変更したか覚えていますか? proceedToTransform メソッドは補間値の更新も行います。



contact callback フィルタリングを使用する

このチュートリアルの前回のパートでは、衝突フィルターを使ってオブジェクトを地面のみと衝突させました。 それでは、このフィルターを削除してオブジェクトもお互いに衝突させてみましょう。


    public void create () {
        ...
        dynamicsWorld.addRigidBody(object.body);
    }

    public void spawn () {
        ...
        dynamicsWorld.addRigidBody(obj.body);
    }

この変更は簡単で、addRigidBodyの呼び出しからGOUND_FLAG, ALL_FLAG 引数と OBJECT_FLAG, GROUND_FLAG 引数を削除するだけです。 これを実行すると、オブジェクトもお互いに衝突するようになったのが分かります。:

bullet8

しかしこれでは、オブジェクトが地面と衝突した時だけ色を白く変えたいのに、オブジェクトが他のオブジェクトに衝突した時にも色が変わってしまいます。 ContactListenerを少し編集することで、これを簡単に解決できます:


    class MyContactListener extends ContactListener {
        @Override
        public boolean onContactAdded (int userValue0, int partId0, int index0, int userValue1, int partId1, int index1) {
            if (userValue1 == 0)
                ((ColorAttribute)instances.get(userValue0).materials.get(0).get(ColorAttribute.Diffuse)).color.set(Color.WHITE);
            if (userValue0 == 0)
                ((ColorAttribute)instances.get(userValue1).materials.get(0).get(ColorAttribute.Diffuse)).color.set(Color.WHITE);
            return true;
        }
    }

上記コードでは、2つ目のオブジェクトが地面だった場合にのみ1つ目のオブジェクトの色を変更し、同様に1つ目のオブジェクトが地面だった場合にのみ2つ目のオブジェクトの色を変更します。 ここで使用している userValueには、instances配列内のオブジェクトのインデックスが設定されているので覚えておいてください。 配列内で一番目にあるオブジェクトは地面なので、地面は 0のインデックス値を持っています。

bullet9

これはテストでは動作しますが、最適なやり方ではありません。 Bulletが我々に代わって衝突の検知を行うだけではなく、これら衝突に対してオブジェクトがどのように反応するかも制御します。 ほとんどの場合は、Bulletがこれを行うに当たって衝突の度に毎回あなたへ通知するようなことはしないで欲しいかもしれません。 衝突の回数が少ない場合のみ、実際に通知を受ける必要があります。 先ほど修正したContactListener内でやったように、コールバック内で衝突の確認を明示的に行うことができます。 しかしその場合でもまだ、ラッパーはネイティブbulletコードとjavaコードとの橋渡しを衝突の度に行う必要があります。 幸い、ラッパーを使用することでContactListenerへ橋渡ししたい衝突を指定することができます。 これはコンタクトコールバックフィルタリング(コンタクトフィルタリングやコールバックフィルタリングと略される場合もあります)として知られており、Bullet ラッパー特有のものです。

コンタクトコールバックフィルタリングは衝突フィルタリングと非常に良く似ています(ですが関連はありません)。 各衝突オブジェクト用のビット単位フラグと、コールバックを呼び出したいオブジェクトのビット単位フィルタ(マスク)が必要です。


    public void create () {
        ...
        dynamicsWorld.addRigidBody(object.body);
        object.body.setContactCallbackFlag(GROUND_FLAG);
        object.body.setContactCallbackFilter(0);
    }

    public void spawn () {
        ...
        dynamicsWorld.addRigidBody(obj.body);
        obj.body.setContactCallbackFlag(OBJECT_FLAG);
        obj.body.setContactCallbackFilter(GROUND_FLAG);
    }

上記コードではラッパーに対して、地面はビット単位フラグGROUND_FLAG (このチュートリアルの前のパートで見たように、9番目のビットにフラグを立てます) を持ち、地面が他のオブジェクトと衝突した時はそれを通知する必要ない(地面のフィルタは0です)、と指示を出しています。 次にspawnメソッド内でラッパーに対して、各オブジェクトはビット単位フラグOBJECT_FLAG を持ち、このオブジェクトが地面と衝突した時は通知して欲しい(オブジェクトのフィルタは GROUND_FLAGです)、と指示を出します。 必要な場合は、複数のフラグを組み合わせてフラグを作成できます。例えば: obj.body.setContactCallbackFilter(GROUND_FLAG | WALL_FLAG);

これは衝突フィルタリングと比較すると異なっているので注意してください。 衝突フィルタリングの場合、両方のオブジェクトのフィルタにおいて、衝突が発生する相手側オブジェクトとフラグが一致している必要があります。 しかし接触フィルタリングの場合、フィルタの一方のみにおいて、コールバックを呼び出す相手側オブジェクトとフラグが一致している必要があります。

次に、コンタクトコールバックフィルタリングを実際に使用することラッパーに対して通知する必要があります。 コールバックメソッドに別のメソッドシグネチャを使うことでこれを行えます。 前回のパートで見たように、ContactListenerクラスは上書き可能ないくつかのメソッドを持っており、 上書きするメソッドシグネチャに応じてラッパーは可能な限り最適化を行います。


    @Override
    public boolean onContactAdded (int userValue0, int partId0, int index0, boolean match0, 
                    int userValue1, int partId1, int index1, boolean match1) {
        if (match0)
            ((ColorAttribute)instances.get(userValue0).materials.get(0).get(ColorAttribute.Diffuse)).color.set(Color.WHITE);
        if (match1)
            ((ColorAttribute)instances.get(userValue1).materials.get(0).get(ColorAttribute.Diffuse)).color.set(Color.WHITE);
        return true;
    }

githubにある全ソースコードを参照してください

上記コードでは、メソッドシグネチャを変更して二つのboolean型の引数: match0match1を持たせました。 ラッパーはこの引数を "見る "ことによって、コンタクトコールバックフィルタリングを適用します。 既定ではコンタクトコールバックフィルターには0が設定されているので、コンタクトコールバックフラグとフィルタ値を設定しないでこのメソッドを上書きすると、コールバックは決して呼び出されません。注意してください。 また、コールバックメソッドごとにコンタクトコールバックフィルターを使用するかどうか選ぶことができるので、覚えておいてください。 例えば以下では、 onContactAdded コールバックではコンタクトコールバックフィルターを使用しますが、 onContactProcessed コールバックでは使用しません:


    class MyContactListener extends ContactListener {
        @Override
        public boolean onContactAdded (int userValue0, int partId0, int index0, boolean match0,
                        int userValue1, int partId1, int index1, boolean match1) {
            ...
        }
        @Override
        public void onContactProcessed(int userValue0, int userValue1) {
            ...
        }
    }

match0値と match1 値はオブジェクトがどのフィルタに一致するかを示すために使用されます。 したがって今回の場合、match 変数は地面と衝突するオブジェクトに対してのみ設定されますが、地面自体に対しては設定されません。



キネマティックボディ

地面を上下に動かして、テストを少し面白くしてみましょう。 地面は静止物体なので物理演算の影響を受けませんが、物理演算は地面の影響を受けます。 一目瞭然ですが、動いている地面というのはもはや静的(static)ではありません。 そのような移動はしても衝突には反応しないオブジェクトは、キネマティックボディと呼ばれます。 実際、キネマティックボディと静止オブジェクトは非常に似ています。コードを介して位置情報と回転情報を変更できることを除けば。 地面を動かす前に、地面は今回はキネマティックボディであることをBullet に通知する必要があります。


    public void create () {
        ...
        instances = new Array<GameObject>();
        GameObject object = constructors.get("ground").construct();
        object.body.setCollisionFlags(object.body.getCollisionFlags()
            | btCollisionObject.CollisionFlags.CF_KINEMATIC_OBJECT);
        instances.add(object);
        dynamicsWorld.addRigidBody(object.body);
        object.body.setContactCallbackFlag(GROUND_FLAG);
        object.body.setContactCallbackFilter(0);
    }

上記コードの変更点は1つだけで、 setCollisionFlagsメソッドを呼び出して、地面のbodyにCF_KINEMATIC_OBJECTフラグを追加している点です。 このメソッドは以前にもspawnメソッド内で見ました。その際にはこのメソッドを使ってCF_CUSTOM_MATERIAL_CALLBACKフラグを追加し、 onContactAddedコールバックを受け取りたいということをBullet に通知しました。 同様にCF_KINEMATIC_OBJECTフラグでは、地面はキネマティックボディでありその変形情報を変更したいということをBullet に通知します。

それでは、地面を動かしてみましょう:


    float angle, speed = 90f;
    @Override
    public void render () {
        final float delta = Math.min(1f / 30f, Gdx.graphics.getDeltaTime());

        angle = (angle + delta * speed) % 360f;
        instances.get(0).transform.setTranslation(0, MathUtils.sinDeg(angle) * 2.5f, 0f);
        instances.get(0).body.setWorldTransform(instances.get(0).transform);

        dynamicsWorld.stepSimulation(delta, 5, 1f/60f);
        ...
    }

上記コードでは、angle変数と speed 変数を追加しました。 angle 変数には、地面の位置を基準とした現在の角度が格納されます。 speed 変数には、地面が毎秒移動する角度単位の速度が格納されます。 render メソッド内では、それに応じて angle 変数を更新します。 次に、地面の位置のy座標にこの角度の正弦を設定します。 これにより、地面の上下移動が滑らかになります。 最後に、そうして得た値を地面の物理的ボディのワールドトランスフォームを設定します。 こうして、Bulletをこの新しい位置情報を使って次のシミュレーションを実行します。

このコードを実行すると、望む結果が実際に表示されます。 地面は滑らかに上下に移動し、動的オブジェクトはその動きに応じて反応します。 しかししばらくすると、このシミュレーションに欠陥があることが分かるでしょう。 オブジェクトが空中に浮いていたり、地面を飛び跳ねたりしています。

bullet10

なぜこのようなことが起こっているのかを理解するために、たくさんの動的オブジェクトが上に乗った非常に大きな静的地面を想像してください。 各動的オブジェクトと地面との間で衝突が発生してるのは明らかなので、 全てのオブジェクトに対して、コンタクトコールバックを含むブロードフェイズとニアフェイスのアルゴリズムを両方とも実行する必要があります。 そして、これを毎フレームごとに実行する必要があります。実行すべき動きが存在しない場合でさえも。

お分かりの通り、これはとても非効率です。 こういった理由から、Bulletではどのオブジェクトの衝突を確認するかを指定することができます。 そのため、 全てのオブジェクトについて全てのオブジェクトに対する衝突を確認するのではなく、 指定したオブジェクトについて全てのオブジェクトに対する衝突を確認します。 衝突を確認する必要があるオブジェクトはアクティブオブジェクトと呼ばれます。 同様に、確認する必要がないオブジェクトはスリーピング (もしくは非アクティブ) オブジェクトと呼ばれます。

これ自体は純粋な衝突判定機能ですが、ダイナミックレイヤーで必要に応じて自動的にオブジェクトのアクティブ化や非アクティブ化を行うことで、非常に強力な機能になります。 これはオブジェクトの速度を監視することで行います。 bodyがworldに追加された時やbodyが移動している時は、そのbodyはアクティブになります。 一定時間内(setDeactivationTimeメソッドを使って設定できます)の速度が一定の閾値(setSleepingThresholds メソッドを使用して設定できます)を下回ると、そのオブジェクトは非アクティブとなります。 隣接する全てのオブジェクトが非アクティブと見なされた場合も、そのオブジェクトは非アクティブとなります。 これはActivation Stateを介して行われます:

  • ACTIVE_TAG: このオブジェクトはアクティブであり、衝突を確認する必要があります。 bodyがworldに追加された時やbodyが移動している時にこの状態となります。
  • WANTS_DEACTIVATION: このオブジェクトの規定時間内の速度が閾値を下回ってます。この状態はBulletが内部的に使用するものなので、あなたはこれを使用する必要はありません。
  • ISLAND_SLEEPING: このオブジェクトと隣接する全てのオブジェクトは非アクティブ状態です。islandというのは、隣接するbodyのグループのことです。例えば、積み重なっている箱などは全て同じislandに属します。
そのため、テストクラスでは以下のような流れが起きています:
  1. オブジェクトが world に追加されると、ACTIVE_TAG 状態が設定されます
  2. 重力がオブジェクトに適用されてそれによって落下し、その速度が必要な閾値を上回るのでACTIVE_TAG 状態が維持されます。
  3. オブジェクトが地面にぶつかるとそれによって落下速度が必要な閾値を下回り、 bullet は時間のカウントを始めます。
  4. 速度が規定時間の間に閾値を下回り、状態は WANTS_DEACTIVATIONに設定されます。
  5. 隣接する全てのオブジェクトも ACTIVE_TAG 状態ではないので、オブジェクトの状態は ISLAND_SLEEPINGが設定されます。
  6. もうこのオブジェクトの衝突は確認されません。
  7. 地面は動いていますがそれの状態はACTIVE_TAG ではないので、これによりオブジェクトが空中に浮いてしまいます。
キネマティックボディの位置情報と回転情報はコード上から設定されて速度は適用されないので、Bulletによって自動的にアクティブ化されることはありません。 そのため基本的には、我々自身でキネマティックボディの状態を設定する必要があります。


    public void render () {
        ...
        instances.get(0).body.setWorldTransform(instances.get(0).transform);
        instances.get(0).body.setActivationState(Collision.ACTIVE_TAG);
        ...
    }

今回のテストではこれで十分ですが、これはbodyをアクティブにする際の推奨方法ではありません。 タイマーを使ってbodyの速度が閾値をどれくらい下回っているのかを判定するためです。 コード上からbodyをアクティブ化する時、同時にタイマーもリセットする必要があります。 幸いbullet には、これを行うための activate()という素晴らしいヘルパーメソッドがあります。:


    public void render () {
        ...
        instances.get(0).body.setWorldTransform(instances.get(0).transform);
        instances.get(0).body.activate();
        ...
    }

そのためbodyを移動させる場合は、body をアクティブ化して、Bulletにその衝突を確認させるようにする必要があります。 しばらくするとまた、Bullet は自動的にbody を非アクティブ化します。 しかし今回のテストでは、地面を常に動かしているので、Bulletが自動的にbodyを非アクティブ化する必要はありません。 Bullet にはさらに二つ、以下のようなアクティベーション状態があります。:

  • DISABLE_DEACTIVATION: body は決して(自動的に)非アクティブ化されることはありません。
  • DISABLE_SIMULATION: body は決して(自動的に)アクティブ化されることはありません。
そのため、地面を作成した直後にそのアクティベーション状態をDISABLE_DEACTIVATIONに設定することで、render メソッド内で手動で地面をアクティブ化する必要がなくなります。


    public void create () {
        ...
        object.body.setActivationState(Collision.DISABLE_DEACTIVATION);
    }
    ...
    public void render () {
        ...
        instances.get(0).body.setWorldTransform(instances.get(0).transform);

        dynamicsWorld.stepSimulation(delta, 5, 1f/60f);
    }

以前に、Motion Stateを使ってbullet 物理演算bodyのワールド変換行列と視覚的なゲームオブジェクトを同期する方法について見てきました。 Bullet はアクティブ状態のキネマティックボディの MotionStateの getWorldTransform を自動的に呼び出して、最新の変換状態を取得します。 そのため、body を動かした後にそれを手動で更新する必要はありません:


    public void render () {
        final float delta = Math.min(1f / 30f, Gdx.graphics.getDeltaTime());

        angle = (angle + delta * speed) % 360f;
        instances.get(0).transform.setTranslation(0, MathUtils.sinDeg(angle) * 2.5f, 0f);

        dynamicsWorld.stepSimulation(delta, 5, 1f/60f);
        ...
    }

githubにある全ソースコードを参照してください

注意: キネマティックボディがアクティブ状態である限り、その MotionStateのgetWorldTransform メソッドが毎回呼び出されます。 実際にbodyを移動させたり回転させたりする場合のみ、body をアクティブ状態にするようにしてください。



次は何をするか

これで、LibGDX Bulletラッパーに関するチュートリアルの第二部は終了です。 明らかに、bullet 物理エンジンとlibgdx bullet ラッパーの全ての側面を説明することは不可能です。 例えばここでは説明していませんが、Bullet の内部時間ステップに手を加えて 、シミュレーションをより細かく制御することさえもできるのです。 bullet を使ってプレイヤーキャラクターを操作することもできます。 もしくは、 bullet に対してオブジェクトをトリガーとして使うよう指示を出し、衝突を通知させることもできます。ただし、その通知結果に対して処理を行うことはできましせん。 そして、Bullet にまだまだたくさんの機能があります!

まだの場合は、Bullet マニュアルを読むことを強くお勧めします。これは良い読み物で、Bullet ライブラリを使用する時はここがスタート地点となります。 そのページは LibGDX Bulletラッパーを対象としてはいませんが、その大部分の内容はLibGDX Bulletラッパーでも役に立ちます。 また、Bullet ウェブサイト と特に Bullet wikiには、多くの実用的な情報があります。 このフォーラム も非常に活発です。

このwiki ページには、特にLibGDX Bulletラッパーに関する多くの情報があります。