2次元物理シミュレーションPhysics2Dライブラリの基本的な使い方

この文書では2次元物理シミュレーションPhysics2Dライブラリの基本的な使い方について説明します。

物理シミュレーションシーンを作る最小限の流れ

2次元物理シミュレーションPhysics2Dライブラリの基本的な使い方を学ぶためにサンプルシーンPrimitiveSceneを例に物理シーンの設定方法を見ていきます。

"sample/Physics2D/Physics2DSample/BasicScene/PrimitiveScene.cs"を参照してください。

alternate text

物理シミュレーションのシーンを作る最小限の流れは以下の形になります。

  1. PhysicsSceneを継承したシーンクラスを作る
  2. 継承したシーンクラスにおいてInitScene()をオーバーライドする
  3. InitScene()が継承したシーンクラスのコンストラクタで呼ばれるようにする

実際にPrimitiveSceneを例としてシーン生成のコードの流れを見てみます。

  1. PhysicsSceneを継承したクラスを作る
using System;
using System.Text;

using Sce.PlayStation.Core;
using Sce.PlayStation.Core.Graphics;
using Sce.PlayStation.Core.Input;
using Sce.PlayStation.Core.Environment;

// Include 2D Physics Framework
using Sce.PlayStation.HighLevel.Physics2D;

public class PrimitiveScene : PhysicsScene
{

}
  1. 継承したシーンクラスにおいてInitScene()をオーバーライドする
using System;
using System.Text;

using Sce.PlayStation.Core;
using Sce.PlayStation.Core.Graphics;
using Sce.PlayStation.Core.Input;
using Sce.PlayStation.Core.Environment;

// Include 2D Physics Framework
using Sce.PlayStation.HighLevel.Physics2D;

public class PrimitiveScene : PhysicsScene
{
        public override void InitScene ()
        {
                // Create an empty simulation scene
                base.InitScene();
        }
}
  1. InitScene()が継承したシーンクラスのコンストラクタで呼ばれるようにする
using System;
using System.Text;

using Sce.PlayStation.Core;
using Sce.PlayStation.Core.Graphics;
using Sce.PlayStation.Core.Input;
using Sce.PlayStation.Core.Environment;

// Include 2D Physics Framework
using Sce.PlayStation.HighLevel.Physics2D;

public class PrimitiveScene : PhysicsScene
{
        public PrimitiveScene()
        {
            InitScene();
        }

        public override void InitScene ()
        {
                // Create an empty simulation scene
                base.InitScene();
        }
}
以上で最小限の物理シミュレーションシーンのセットアップは完了です。

ただし、このままでは物理オブジェクトをシーンの中に何も配置していないので、シミュレーションは何も行われません。
そこで実際にはInitScene()の中で、必要な剛体オブジェクトの初期配置が必要になります。

それでは、InitScene()での剛体オブジェクトの初期配置を見ていきます。

剛体オブジェクトの初期配置

剛体オブジェクトの初期配置の基本的な流れは以下の形になります。

  1. 衝突形状データを作成する
  2. 衝突形状データと関連付けた剛体オブジェクトを作成する

衝突形状データを作成する

衝突形状として設定可能なのは基本的に凸形状のみです。球、ボックス、凸多角形などを含みます。
凸多角形の頂点数の制限は30頂点に設定されています。ただし、これはソースコードを改変することで上限を変えることは可能です。
しかしながら、頂点数が増えるほど、衝突判定のための計算コストは大きくなりますので気をつけてください。

alternate text
凹形形状は直接に衝突形状に設定することはできません。
凹形状を衝突形状として持つ剛体オブジェクトは剛体を複合させる必要があります。
alternate text
シーンの衝突形状はシーンのメンバ変数sceneShapesに登録をしていきます。
また、その際、シーンに登録されている衝突形状の数をnumShapeに設定するのを忘れないようにしてください。

それでは実際にPrimitiveSceneではどのように形状を設定していくのかを見ていきます。
public override void InitScene ()
{
        // Create an empty simulation scene
        base.InitScene();

        ...
        ...
        ...

        // Box Shape Setting PhysicsShape( "width", "height" )
        Vector2 wall_width = new Vector2(50, 8);
        sceneShapes[0] = new PhysicsShape(wall_width);

        Vector2 wall_height = new Vector2(8, 30);
        sceneShapes[1] = new PhysicsShape(wall_height);

        Vector2 box_width = new Vector2(2.0f, 2.0f);
        sceneShapes[2] = new PhysicsShape(box_width);

        // Sphere Shape Setting PhysicsShape( "radius" )
        float sphere_width = 2.0f;
        sceneShapes[3] = new PhysicsShape(sphere_width);


        Vector2[] test_point = new Vector2[10];

        for (int i = 0; i < 10; i++)
        {
            test_point[i] = new Vector2(rand_gen.Next(-1000, 1000), rand_gen.Next(-1000, 1000)) * 2.0f / 1000.0f;
        }

        // Convex Shape Setting (by using random points)
        sceneShapes[4] = PhysicsShape.CreateConvexHull(test_point, 10);

        numShape = 5;

        ...
        ...
        ...
}

ボックス形状の設定例

ボックスの縦横半分の長さをPhysicsShapeコンストラクタの引数に与えます。

Vector2 wall_width = new Vector2(50, 8);
sceneShapes[0] = new PhysicsShape(wall_width);

球形状の設定例

球の半径を設定したfloat値をPhysicsShapeコンストラクタの引数に与えます。

float sphere_width = 2.0f;
sceneShapes[3] = new PhysicsShape(sphere_width);

凸多角形の設定例

ランダムな頂点を10個作り、ランダムな10個の頂点をもとに凸包を作って凸多角形を生成しています。

Vector2[] test_point = new Vector2[10];

for (int i = 0; i < 10; i++)
{
        test_point[i] = new Vector2(rand_gen.Next(-1000, 1000), rand_gen.Next(-1000, 1000)) * 2.0f / 1000.0f;
}

sceneShapes[4] = PhysicsShape.CreateConvexHull(test_point, 10);

ここでCreateConvexHullは任意の頂点列を与えて、凸包を生成するAPIです。

衝突形状の数の設定例

1つずつ衝突形状を登録するたびにnumShapeをインクリメントしてもいいですが、PrimitiveSceneでは衝突形状を登録した後でnumShapeを設定しています。

numShape = 5;
以上で、シーンPrimitiveSceneにおける衝突形状の登録が完了です。

ただし、ここまでは衝突形状を登録しただけで、実際の剛体が存在している訳ではありません。
次に登録された衝突形状にもとづいて、剛体オブジェクトの作成を行っていきます。

衝突形状データと関連付けた剛体オブジェクトを作成する

まず、剛体にはいくつかのタイプがあります。

Dynamic剛体

  • 通常の良く使われる剛体です
  • 速度や加速度を持ち、シーンの中を移動します

Static剛体

  • シーンの開始から終了まで全く動かない剛体です
  • 無限大の質量を持つと仮定して計算されます

Kinematic剛体

  • 一時的に強制的に静止状態、または意図的に移動させられる剛体です
  • 一時的に無限大の質量を持つと仮定して計算されます

Trigger剛体

  • シーンの中のある領域に入ったかどうかの検出目的に用いられる特殊な剛体です
  • 衝突判定はありますが、衝突による反発計算は行われません
まずは上記どのタイプの剛体を作りたいのかを明確にしておく必要があります。
動かない壁や床を作る時にはStatic剛体を用い、床の上を転がる球を作る時にはDynamic剛体を用いることになります。

剛体を生成する際には上記の剛体のタイプと、以下の値を設定して初期配置することが一般的です。

剛体の初期設定に良く使われる属性値

  • 質量
  • 位置
  • 角度
  • 速度(並進方向の速度)
  • 角速度(回転方向の速度)
alternate text
それでは実際にPrimitiveSceneではどのように剛体オブジェクトを初期配置していくのかを見ていきます。
まずはシーンの中に壁、床、天井となる剛体オブジェクトをStatic剛体として生成します。これらの剛体はシーンの開始から終了まで動かないためです。

Static剛体の設定例

public override void InitScene()
{
        ...
        ...
        ...

    // Create static walls to limit the range of action of active rigid sceneBodies
    {
        // new PhysicsBody( "shape of the body",  "mass of the body(kg)" )
        sceneBodies[numBody] = new PhysicsBody(sceneShapes[0], PhysicsUtility.FltMax);

        // Set the position & the rotation
        sceneBodies[numBody].position = new Vector2(0, -wall_height.Y);
        sceneBodies[numBody].rotation = 0;

        // Make shapeIndex consistent with what we set as convex shape
        sceneBodies[numBody].shapeIndex = 0;
        numBody++;

        sceneBodies[numBody] = new PhysicsBody(sceneShapes[1], PhysicsUtility.FltMax);
        sceneBodies[numBody].position = new Vector2(wall_width.X, 0);
        sceneBodies[numBody].rotation = 0;
        sceneBodies[numBody].shapeIndex = 1;
        numBody++;

        sceneBodies[numBody] = new PhysicsBody(sceneShapes[1], PhysicsUtility.FltMax);
        sceneBodies[numBody].position = new Vector2(-wall_width.X, 0);
        sceneBodies[numBody].rotation = 0;
        sceneBodies[numBody].shapeIndex = 1;
        numBody++;

        sceneBodies[numBody] = new PhysicsBody(sceneShapes[0], PhysicsUtility.FltMax);
        sceneBodies[numBody].position = new Vector2(0, wall_height.Y);
        sceneBodies[numBody].rotation = 0;
        sceneBodies[numBody].shapeIndex = 0;
        numBody++;
    }

        ...
        ...
        ...
}

ここでは床となるStatic剛体の設定を行っています。
sceneBodies[numBody] = new PhysicsBody(sceneShapes[0], PhysicsUtility.FltMax);
sceneBodies[numBody].position = new Vector2(0, -wall_height.Y);
sceneBodies[numBody].rotation = 0;
sceneBodies[numBody].shapeIndex = 0;
numBody++;
  1. 衝突形状のボックス形状sceneShape[0]、およびStaticタイプであるため質量無限大PhysicsUtility.FltMaxを引数としてPhysicsBodyのコンストラクタを呼び出す
  2. position属性として位置にVector2(0, -wall_height.Y)を設定
  3. rotation属性として角度に0を設定
  4. shapeIndexとしてPhysicsBodyのコンストラクタに与えた形状のインデックスを与えています
  5. シーン中の剛体オブジェクトの数を保持する変数numBodyをインクリメントしておきます
壁、天井となるStatic剛体の設定も同様です。

ここで床と天井の剛体の衝突形状は同じなので、関連付けられている衝突形状が同じになります。
衝突形状は複数の剛体と関連付けることが可能ですので、同じ衝突形状を複数作成しないように気をつけてください。

alternate text

次にシーンの中に自由に動き回れる球、ボックス、凸多角形のDynamic剛体を作っていきます。

Dynamic剛体の設定例

// Create dynamic rigid sceneBodies
{
    // Create a box-shaped dynamic rigid body
    {
        sceneBodies[numBody] = new PhysicsBody(sceneShapes[2], 1.0f);
        sceneBodies[numBody].position = new Vector2(-10.0f, -5.0f);
        sceneBodies[numBody].rotation = PhysicsUtility.GetRadian(30.0f);
        sceneBodies[numBody].shapeIndex = 2;
        numBody++;
    }

    // Create a sphere-shaped dynamic rigid body
    {
        sceneBodies[numBody] = new PhysicsBody(sceneShapes[3], 1.0f);
        sceneBodies[numBody].position = new Vector2(0.0f, -5.0f);
        sceneBodies[numBody].rotation = 0;
        sceneBodies[numBody].shapeIndex = 3;
        sceneBodies[numBody].colFriction = 0.01f;
        numBody++;
    }

    // Create a convex-shaped dynamic rigid body
    {
        sceneBodies[numBody] = new PhysicsBody(sceneShapes[4], 1.0f);
        sceneBodies[numBody].position = new Vector2(10.0f, -5.0f);
        sceneBodies[numBody].rotation = 0;
        sceneBodies[numBody].shapeIndex = 4;
        numBody++;
    }
}

ここではボックスとなるDynamic剛体の設定を行っています。

sceneBodies[numBody] = new PhysicsBody(sceneShapes[2], 1.0f);
sceneBodies[numBody].position = new Vector2(-10.0f, -5.0f);
sceneBodies[numBody].rotation = PhysicsUtility.GetRadian(30.0f);
sceneBodies[numBody].shapeIndex = 2;
numBody++;
  1. 衝突形状となるボックス形状sceneShape[2]、および質量1.0fを引数としてPhysicsBodyのコンストラクタを呼び出す
  2. position属性として位置にVector2(-10.0f, -5.0f)を設定
  3. rotation属性として角度にPhysicsUtility.GetRadian(30.0f)を設定(ラジアン指定)
  4. shapeIndexとしてPhysicsBodyのコンストラクタに与えた形状のインデックスを与える
  5. シーン中の剛体オブジェクトの数を保持する変数numBodyをインクリメント
球、凸多角形の形状を持つDynamic剛体の設定も同様です。

ここでは球となるDynamic剛体の設定を行っています。
sceneBodies[numBody] = new PhysicsBody(sceneShapes[3], 1.0f);
sceneBodies[numBody].position = new Vector2(0.0f, -5.0f);
sceneBodies[numBody].rotation = 0;
sceneBodies[numBody].shapeIndex = 3;
sceneBodies[numBody].colFriction = 0.01f;
numBody++;
  1. 衝突形状となるボックス形状sceneShape[3]、および質量1.0fを引数としてPhysicsBodyのコンストラクタを呼び出す
  2. position属性として位置にVector2(0.0f, -5.0f)を設定
  3. rotation属性として角度に0を設定
  4. shapeIndexとしてPhysicsBodyのコンストラクタに与えた形状のインデックスを与える
  5. 衝突摩擦係数0.01fを設定
  6. シーン中の剛体オブジェクトの数を保持する変数numBodyをインクリメント

ここでは凸多角形となるDynamic剛体の設定を行っています。
sceneBodies[numBody] = new PhysicsBody(sceneShapes[4], 1.0f);
sceneBodies[numBody].position = new Vector2(10.0f, -5.0f);
sceneBodies[numBody].rotation = 0;
sceneBodies[numBody].shapeIndex = 4;
numBody++;
  1. 衝突形状となるボックス形状sceneShape[4]、および質量1.0fを引数としてPhysicsBodyのコンストラクタを呼び出す
  2. position属性として位置にVector2(10.0f, -5.0f)を設定
  3. rotation属性として角度に0を設定
  4. shapeIndexとしてPhysicsBodyのコンストラクタに与えた形状のインデックスを与える
  5. シーン中の剛体オブジェクトの数を保持する変数numBodyをインクリメント
なお、Dynamic剛体の場合、質量を含む剛体のスケールについては注意が必要です。

alternate text
質量差の激しい剛体同士が衝突した場合には、計算の不安定性が生じることがあります。
Dynamic剛体の場合、大まかな目安として以下の範囲で剛体を生成することが望ましいです。
  • 質量スケール : 0.2[Kg] - 20.0[Kg]
  • 長さスケール : 0.2[m] - 20.0[m]
では、次に一時的に強制的に静止されられたりするKinematic剛体を作っていきます。

Kinematic剛体の設定例

 // Create a kinematic rigid body whose shape is box
{
    sceneBodies[numBody] = new PhysicsBody(sceneShapes[2], 1.0f);
    sceneBodies[numBody].position = new Vector2(-10.0f, 15.0f);
    sceneBodies[numBody].rotation = PhysicsUtility.GetRadian(30.0f);
    sceneBodies[numBody].shapeIndex = 2;
    sceneBodies[numBody].SetBodyKinematic();
    index0 = numBody++;
}

// Create a sphere-shaped kinematic rigid body
{
    sceneBodies[numBody] = new PhysicsBody(sceneShapes[3], 1.0f);
    sceneBodies[numBody].position = new Vector2(0.0f, 15.0f);
    sceneBodies[numBody].rotation = 0;
    sceneBodies[numBody].shapeIndex = 3;
    sceneBodies[numBody].colFriction = 0.01f;
    sceneBodies[numBody].SetBodyKinematic();
    index1 = numBody++;
}

// Create a convex-shaped kinematic rigid body
{
    sceneBodies[numBody] = new PhysicsBody(sceneShapes[4], 1.0f);
    sceneBodies[numBody].position = new Vector2(10.0f, 15.0f);
    sceneBodies[numBody].rotation = 0;
    sceneBodies[numBody].shapeIndex = 4;
    sceneBodies[numBody].SetBodyKinematic();
    index2 = numBody++;
}
ここではボックスとなるKinematic剛体の設定を行っていますが、実はDynamic剛体の設定とほぼ同じ手順です。違いはSetBodyKinematic()を呼び出す部分です。
sceneBodies[numBody] = new PhysicsBody(sceneShapes[2], 1.0f);
sceneBodies[numBody].position = new Vector2(-10.0f, 15.0f);
sceneBodies[numBody].rotation = PhysicsUtility.GetRadian(30.0f);
sceneBodies[numBody].shapeIndex = 2;
sceneBodies[numBody].SetBodyKinematic();
index0 = numBody++;
  1. 衝突形状となるボックス形状sceneShape[2]、および質量1.0fを引数としてPhysicsBodyのコンストラクタを呼び出す
  2. position属性として位置にVector2(-10.0f, 15.0f)を設定
  3. rotation属性として角度にPhysicsUtility.GetRadian(30.0f)を設定(ラジアン指定)
  4. shapeIndexとしてPhysicsBodyのコンストラクタに与えた形状のインデックスを与える
  5. Kinematic剛体にするためにSetBodyKinematic()を呼び出すことでDynamic剛体として一度設定された情報を退避する
  6. シーン中の剛体オブジェクトの数を保持する変数numBodyをインクリメント
球、凸多角形の形状を持つKinematic剛体の設定も同様です。

なお、Kinematic剛体と設定された剛体はDynamic剛体に戻す(退避した情報)ことが可能です。

PrimitiveSceneではボタン入力によって一度Kinematic剛体に設定された剛体をDynamic剛体に戻したり、再びKinematic剛体にしたりしています。
Kinematic剛体の設定でindex0にインデックスを保存していたのはこのためです。
// Game controller handling
public override void KeyboradFunc(GamePadButtons button)
{
    switch (button)
    {
        case GamePadButtons.Square:

                //
            // isKinematicOrStatic() checks the mass of the rigid body and determines whether a body is kinematic or static
            // backToDynamic() cannot be called for rigid sceneBodies not set up with setKinematic()
            //

            if (sceneBodies[index0].IsKinematic())
                sceneBodies[index0].BackToDynamic();
            else
                sceneBodies[index0].SetBodyKinematic();

            if (sceneBodies[index1].IsKinematic())
                sceneBodies[index1].BackToDynamic();
            else
                sceneBodies[index1].SetBodyKinematic();

            if (sceneBodies[index2].IsKinematic())
                sceneBodies[index2].BackToDynamic();
            else
                sceneBodies[index2].SetBodyKinematic();

            break;
        default:
            break;
    }
}
IsKinematic APIによって剛体がKinematic剛体であるかを判定して、Kinematic剛体であれば、退避していた情報を戻してDynamic剛体に変化させています。
退避していた情報を戻すためにもKinamtic剛体も最初はDynamic剛体に設定しておく必要があります。Dynamic剛体に戻す可能性がない剛体は最初からStatic剛体として設定してください。

シーンの特性の設定

さて、InitScene()の中で剛体の初期配置について見てきましたが、InitScene()で使われているシーンの特性の設定について説明します。

public override void InitScene ()
{
        // Create an empty simulation scene
        base.InitScene();
        sceneName = "PrimitiveScene";

        // Set the restitution coefficient a bit stronger
        this.restitutionCoeff = 0.8f;

sceneNameはシーン名を保持する文字列の変数で"PrimitiveScene"をセットしています。

this.restitutionCoeff = 0.8f;

シーンの特性である反発に対する反発係数の設定をしていて、反発をきちんと生じるシーンに設定しています。

シーン内での衝突の特性を決めるパラメータとしては以下の値が設定可能です。

  • float penetrationRepulse (default 0.2f) ... めり込みに対する反発力の加速係数
  • float penetLimit (default 0.03f) ... めり込みに対する許容度
  • float tangentFriction (default 0.3f) ... 反発に対する接線方向の摩擦係数
  • float restitutionCoeff (default 0.0f) ... 反発に対する反発係数
alternate text
これらの値を変えることで、反発のしやすいシーンになったり、反発のしにくいシーンになったり、摩擦のないシーンにすることが可能です。
シーンの特性のパラメータについては"sample/Physics2D/Physics2DSample/TutorialScene/ScenePropertyScene.cs"にサンプルがありますので、参考にしてください。

剛体のスリープについて


計算コストを抑えるため、剛体がほぼ動いておらず静止していると見なせる状態になっている時には、剛体はスリープ状態に入ります。
剛体がスリープに入っている状態では、衝突点検出、衝突反発の計算は行われなくなります。

PrimitiveSceneにおいても床と接触している剛体の間の衝突点計算がスリープ状態に入ると行われなくなります。
しかし、新たな衝突が発生して速度を生じる時点で剛体はスリープ状態から抜けだします。

alternate text
なお、Static剛体やKinematic剛体については強制的に静止状態になっている時は、常にスリープ状態と見なされます。

剛体オブジェクトの描画ついにて

剛体オブジェクトの描画について簡単に解説をします。描画方法そのものについての説明はGraphicsサンプルのドキュメントを参照してください。
ここではPhysics2Dと関係のある部分についてのみ解説します。

通常、剛体オブジェクトの描画は剛体に設定された衝突形状にもとづいて描画されます。
例で見てきた通り、衝突形状オブジェクトはsceneShapesに保存されていました。sceneShapesはPhsyicsShapeの配列です。

頂点バッファの準備

まずはシェーダープログラムに与える頂点バッファが必要になります。

public class PrimitiveScene : PhysicsScene
{
    // Vertex Buffer for Body Rendering
    private VertexBuffer[] vertices = new VertexBuffer[100];
PrimitiveSceneでは100個の衝突形状は出てきませんが、ここではとりあえず100個分の衝突形状を描画できるように頂点バッファを宣言します。
なお、頂点バッファは剛体ごとに使用するのではなく、同じ衝突形状を持つ剛体間でシェアしています。
これはPrimitiveSceneのためであり、テクスチャを剛体ごとに変える場合には剛体ごとに頂点バッファを確保する必要になることもあります。

次に頂点バッファのコンストラクタで頂点数、頂点のフォーマットを指定して頂点バッファを作成します。
public PrimitiveScene()
{
    // Simulation Scene Set Up
    InitScene();

    // Setup for Rendering Object
    for (int i = 0; i < numShape; i++)
    {
        if (sceneShapes[i].numVert == 0)
        {
            vertices[i] = new VertexBuffer(37, VertexFormat.Float3);
        }
        else
        {
            vertices[i] = new VertexBuffer(sceneShapes[i].numVert + 1, VertexFormat.Float3);
        }

衝突形状PhysicsShapeの構造

ここでPhysicsShapeの構造を確認しておきます。

public partial class PhysicsShape
{
    public int numVert;

    public Vector2[] vertList = new Vector2[30];
        ...
        ...
        ...
}
これは定義の一部ですが、PhysicsShapeは頂点数を保存するnumVertと、実際に頂点列を保存するvertListを保持しています。
この頂点列の情報にもとづいて描画をすれば良いのですが、numVert==0の場合は球として設定されており、vertList[0].Xに半径が保存されている点に注意が必要です。
alternate text

PhysicsShapeの構造にもとづき、頂点バッファに頂点をセットしている部分は以下です。

// Line Rendering for Object
private void MakeLineListConvex(PhysicsShape con, VertexBuffer vertices)
{

    if (con.numVert == 0)
    {
        float[] vertex = new float[3 * 37];

        int i = 0;
        float rad = con.vertList[0].X;

        for (float th1 = 0.0f; th1 < 360.0f; th1 = th1 + 10.0f)
        {
            float th1_rad = th1 / 180.0f * PhysicsUtility.Pi;

            float x1 = rad * (float)Math.Cos(th1_rad);
            float y1 = rad * (float)Math.Sin(th1_rad);

            vertex[3 * i + 0] = x1;
            vertex[3 * i + 1] = y1;
            vertex[3 *  i + 2] = 0.0f;
            i++;
        }

        vertex[3 * i + 0] = vertex[3 * 0 + 0];
        vertex[3 * i + 1] = vertex[3 * 0 + 1];
        vertex[3 * i + 2] = vertex[3 * 0 + 2];

        vertices.SetVertices(0, vertex);

    }
    else
    {
        float[] vertex = new float[3 * (con.numVert + 1)];

        int i;

        for (i = 0; i < con.numVert; i++)
        {
            Vector2 v1;
            v1 = con.vertList[i];

            vertex[3 * i + 0] = v1.X;
            vertex[3 * i + 1] = v1.Y;
            vertex[3 * i + 2] = 0.0f;
        }

        vertex[3 * i + 0] = vertex[3 * 0 + 0];
        vertex[3 * i + 1] = vertex[3 * 0 + 1];
        vertex[3 * i + 2] = vertex[3 * 0 + 2];

        vertices.SetVertices(0, vertex);
    }
}
con.numVert == 0の場合と、それ以外で場合分けをしています。これは述べたように、球の場合は特殊な形で情報が保持されているからです。
球の場合は半径の情報のみがPhysicsShapeインスタンスに保存されているため、自分で描画用の頂点列を生成しなければなりません。
そこで半径をもとに円周上に何点か頂点をピックアップすることで頂点列を作ります。
ここでは0度から350度までの10度刻みの36頂点と、最後に最初の頂点と同じ座標に頂点を生成しているので合計37頂点になります。

一方、ボックスや一般の凸多角形ではPhysicsShapeインスタンスに保持されている頂点列を頂点バッファにセットします。

球の場合の頂点データの取得

if (con.numVert == 0)
{
    float[] vertex = new float[3 * 37];

    int i = 0;
    float rad = con.vertList[0].X;

    for (float th1 = 0.0f; th1 < 360.0f; th1 = th1 + 10.0f)
    {
        float th1_rad = th1 / 180.0f * PhysicsUtility.Pi;

        float x1 = rad * (float)Math.Cos(th1_rad);
        float y1 = rad * (float)Math.Sin(th1_rad);

        vertex[3 * i + 0] = x1;
        vertex[3 * i + 1] = y1;
        vertex[3 * i + 2] = 0.0f;
        i++;
    }

    vertex[3 * i + 0] = vertex[3 * 0 + 0];
    vertex[3 * i + 1] = vertex[3 * 0 + 1];
    vertex[3 * i + 2] = vertex[3 * 0 + 2];

    vertices.SetVertices(0, vertex);

}
描画のために使用する頂点のバッファは3次元の頂点座標を持ちますが、Z方向を無視するために常に0.0fをZ座標に指定しています。
形状をライン描画するために最後の頂点として最初の頂点と同じ座標を与えています。

ボックスや一般の凸多角形の場合の頂点データの取得

else
{
    float[] vertex = new float[3 * (con.numVert + 1)];

    int i;

    for (i = 0; i < con.numVert; i++)
    {
        Vector2 v1;
        v1 = con.vertList[i];

        vertex[3 * i + 0] = v1.X;
        vertex[3 * i + 1] = v1.Y;
        vertex[3 * i + 2] = 0.0f;
    }

    vertex[3 * i + 0] = vertex[3 * 0 + 0];
    vertex[3 * i + 1] = vertex[3 * 0 + 1];
    vertex[3 * i + 2] = vertex[3 * 0 + 2];

    vertices.SetVertices(0, vertex);
}

形状をライン描画するために最後の頂点として最初の頂点と同じ座標を与えています。

剛体形状の描画コード


実際のシーンの描画は、単に形状を描画するだけでなく、剛体オブジェクトの各フレームにおける位置や角度の変化に合わせて描画する必要があります。
そのため、剛体オブジェクトのposition, rotation, localPosition, localRotationの値を使って、描画に用いられる変換行列を設定する必要があります。
この行列とは、もとの形状を示す頂点列から実際に剛体の位置、角度などを反映した頂点列に変換するための行列です。
複合形状でなければ、localPositionとlocalRotationは考えなくて良く、position, rotationのみの考慮でかまいません。

alternate text
// Draw objects
public override void DrawAllBody(ref GraphicsContext graphics, ref ShaderProgram program, Matrix4 renderMatrix, int click_index)
{
    for (int j = 0; j < numShape; j++)
    {
        graphics.SetVertexBuffer(0, vertices[j]);

        for (int i = 0; i < numBody; i++)
        {
            uint index = sceneBodies[i].shapeIndex;

            if (j != index) continue;

            Matrix4 rotationMatrix = Matrix4.RotationZ(sceneBodies[i].rotation);

            Matrix4 transMatrix = Matrix4.Translation(
                new Vector3(sceneBodies[i].position.X, sceneBodies[i].position.Y, 0.0f));

            Matrix4 local_rotationMatrix = Matrix4.RotationZ(sceneBodies[i].localRotation);

            Matrix4 local_transMatrix = Matrix4.Translation(
                new Vector3(sceneBodies[i].localPosition.X, sceneBodies[i].localPosition.Y, 0.0f));

            Matrix4 WorldMatrix = renderMatrix * transMatrix * rotationMatrix * local_transMatrix * local_rotationMatrix;

            program.SetUniformValue(0, ref WorldMatrix);

            if (i == click_index)
            {
                Vector3 color = new Vector3(1.0f, 0.0f, 0.0f);
                program.SetUniformValue(1, ref color);
            }
            else
            {
                Vector3 color = new Vector3(0.0f, 1.0f, 1.0f);
                program.SetUniformValue(1, ref color);
            }

            if (sceneShapes[index].numVert == 0)
                graphics.DrawArrays(DrawMode.LineStrip, 0, 37);
            else
                graphics.DrawArrays(DrawMode.LineStrip, 0, sceneShapes[index].numVert + 1);

        }

    }
}

順番に内容を見ていきます。前半はシェーダープログラムの設定のためのワールド行列やビュー行列の設定になっています。
シェーダープログラムそのものの設定方法についてはGraphicsサンプルを参照してください。

Matrix4 rotationMatrix = Matrix4.RotationZ(sceneBodies[i].rotation);

Matrix4 transMatrix = Matrix4.Translation(
    new Vector3(sceneBodies[i].position.X, sceneBodies[i].position.Y, 0.0f));

Matrix4 local_rotationMatrix = Matrix4.RotationZ(sceneBodies[i].localRotation);

Matrix4 local_transMatrix = Matrix4.Translation(
    new Vector3(sceneBodies[i].localPosition.X, sceneBodies[i].localPosition.Y, 0.0f));

Matrix4 WorldMatrix = renderMatrix * transMatrix * rotationMatrix * local_transMatrix * local_rotationMatrix;

program.SetUniformValue(0, ref WorldMatrix);
上記の行列演算において、(transMatrix * rotationMatrix * local_transMatrix * local_rotationMatrix)で乗算が行われています。
複合形状でなければ、(transMatrix * rotationMatrix)のみの考慮でかまいません。

描画において剛体に設定された衝突形状の元の各頂点は次の順で乗算が行われ、現在の剛体の位置や角度などを反映した頂点に変換されることになります。
  1. local_rotationMatrix
  2. local_transMatrix
  3. rotationMatrix
  4. transMatrix

変換の行列を設定したら、後は元の衝突形状の頂点をシェーダーに与えて描画を行うだけで良いのです。

if (sceneShapes[index].numVert == 0)
    graphics.DrawArrays(DrawMode.LineStrip, 0, 37);
else
    graphics.DrawArrays(DrawMode.LineStrip, 0, sceneShapes[index].numVert + 1);

デバッグ情報の描画について

正しく物理シミュレーションが行われているかデバッグ描画を行って状態を確認したい場合があります。
デバッグ描画に良く使われるのが衝突点の位置情報です。他にもオブジェクトのバウンディングボックスやジョイントの情報がデバッグ描画に良く使われます。

衝突点のデバッグ描画

alternate text

剛体Aと剛体Bが衝突している状態の時、衝突点の情報は剛体Aと剛体Bのそれぞれに生成されます。
通常は剛体Aと剛体Bの衝突点情報の位置座標はほぼ同じですが、強い外力などにより、
一瞬、剛体Aと剛体Bの間にめり込みが生じた場合は、剛体Aと剛体Bに対する衝突点情報の位置座標にずれが生じます。

実際に衝突点を描画するコードを見てます。まずは衝突点を描画するために、小さい正方形をデバッグ描画用の頂点バッファにセットしておきます。

public class PrimitiveScene : PhysicsScene
    {
            ...
            ...
            ...

    private VertexBuffer colVert = null;

            ...
            ...
            ...


    public PrimitiveScene()
    {

            ...
            ...
            ...

        // VertexBuffer for contact points debug rendering
        {
            colVert = new VertexBuffer(4, VertexFormat.Float3);

            const float scale = 0.2f;

            float[] vertex = new float[]
            {
                -1.0f, -1.0f, 0.0f,
                1.0f, -1.0f, 0.0f,
                1.0f, 1.0f, 0.0f,
                -1.0f, 1.0f, 0.0f
            };

            for (int i = 0; i < 12; i++)
                vertex[i] = vertex[i] * scale;

            colVert.SetVertices(0, vertex);
        }
衝突点を描画するための描画オブジェクトは正方形でなくとも良いのですが、ここでは簡易な情報でかまわないので正方形としています。
// Debug rendering for contact points(RigidBody A <=> RigidBody B) and AABB(Axis Aligned Bounding Box)
public override void DrawAdditionalInfo(ref GraphicsContext graphics, ref ShaderProgram program, Matrix4 renderMatrix)
{
    // Draw contact points
    graphics.SetVertexBuffer(0, colVert);

    for (uint i = 0; i < numPhysicsSolverPair; i++)
    {
        // Collision point for RigidBody A
        {
            Matrix4 transMatrix = Matrix4.Translation(
                new Vector3(solverPair[i].resA.X, solverPair[i].resA.Y, 0.0f));

            Matrix4 WorldMatrix = renderMatrix * transMatrix;
            program.SetUniformValue(0, ref WorldMatrix);

            Vector3 color = new Vector3(1.0f, 0.0f, 0.0f);
            program.SetUniformValue(1, ref color);

            graphics.DrawArrays(DrawMode.TriangleFan, 0, 4);
        }

        // Collision point for RigidBody B
        {
            Matrix4 transMatrix = Matrix4.Translation(
                new Vector3(solverPair[i].resB.X, solverPair[i].resB.Y, 0.0f));

            Matrix4 WorldMatrix = renderMatrix * transMatrix;
            program.SetUniformValue(0, ref WorldMatrix);

            Vector3 color = new Vector3(1.0f, 0.0f, 0.0f);
            program.SetUniformValue(1, ref color);

            graphics.DrawArrays(DrawMode.TriangleFan, 0, 4);
        }
    }
シーン中の衝突点の数はnumPhysicsSolverPairに保存さており、衝突点の情報はsolverPair配列に保存されているので
numPhysicsSolverPairの回数だけ衝突点の描画処理を順番に行っていきます。

衝突点情報における剛体A側の衝突点の位置は以下によって取得しています。

Matrix4 transMatrix = Matrix4.Translation(
    new Vector3(solverPair[i].resA.X, solverPair[i].resA.Y, 0.0f));
衝突点情報における剛体B側の衝突点の位置は以下によって取得しています。
Matrix4 transMatrix = Matrix4.Translation(
    new Vector3(solverPair[i].resB.X, solverPair[i].resB.Y, 0.0f));

バウンディングボックスのデバッグ描画


次にバウンディングボックス情報をデバッグ描画している部分について見ていきます。
バウンディングボックスとはX軸、Y軸に平行であり、かつ剛体オブジェクトを包む最小限のボックスのことです。
alternate text
まず衝突点描画と同様に、バウンディングボックスを描画するための頂点バッファを最初に用意します。
public class PrimitiveScene : PhysicsScene
    {
            ...
            ...
            ...

    private VertexBuffer aabbVert = null;

            ...
            ...
            ...

    public PrimitiveScene()
    {

            ...
            ...
            ...

        // VertexBuffer for AABB debug rendering
        {
            aabbVert = new VertexBuffer(5, VertexFormat.Float3);

            float[] vertex = new float[]
            {
                0.0f, 0.0f, 0.0f,
                1.0f, 0.0f, 0.0f,
                1.0f, 1.0f, 0.0f,
                0.0f, 1.0f, 0.0f,
                0.0f, 0.0f, 0.0f,
            };

            aabbVert.SetVertices(0, vertex);
        }

バウンディングボックスはライン描画を行うために頂点としては5点用意しておき、最後の頂点を最初の頂点と同じにしています。

// Debug rendering for contact points(RigidBody A <=> RigidBody B) and AABB(Axis Aligned Bounding Box)
public override void DrawAdditionalInfo(ref GraphicsContext graphics, ref ShaderProgram program, Matrix4 renderMatrix)
{

        ...
        ...
        ...

    // Draw AABB Bounding Box
    graphics.SetVertexBuffer(0, aabbVert);

    for (uint i = 0; i < numBody; i++)
    {

        Matrix4 scaleMatrix = new Matrix4(
             sceneBodies[i].aabbMax.X - sceneBodies[i].aabbMin.X, 0.0f, 0.0f, 0.0f,
             0.0f, sceneBodies[i].aabbMax.Y - sceneBodies[i].aabbMin.Y, 0.0f, 0.0f,
             0.0f, 0.0f, 1.0f, 0.0f,
             0.0f, 0.0f, 0.0f, 1.0f
         );

        Matrix4 transMatrix = Matrix4.Translation(
            new Vector3(sceneBodies[i].aabbMin.X, sceneBodies[i].aabbMin.Y, 0.0f));

        Matrix4 WorldMatrix = renderMatrix * transMatrix * scaleMatrix;
        program.SetUniformValue(0, ref WorldMatrix);

        Vector3 color = new Vector3(1.0f, 1.0f, 0.0f);
        program.SetUniformValue(1, ref color);

        graphics.DrawArrays(DrawMode.LineStrip, 0, 5);
    }
バウンディングボックスの大きさや位置は剛体によって変わってきます。バウンディングボックスの角度は剛体によらず常にX軸、Y軸に平行になります。
sceneBodies配列に剛体の状態が保存されており、aabbMaxおよびaabbMinの値にバウンディングボックスの大きさの情報が保存されていますので、それを利用して描画していきます。

バウンディングボックスの大きさの取得

Matrix4 scaleMatrix = new Matrix4(
     sceneBodies[i].aabbMax.X - sceneBodies[i].aabbMin.X, 0.0f, 0.0f, 0.0f,
     0.0f, sceneBodies[i].aabbMax.Y - sceneBodies[i].aabbMin.Y, 0.0f, 0.0f,
     0.0f, 0.0f, 1.0f, 0.0f,
     0.0f, 0.0f, 0.0f, 1.0f
 );

バウンディングボックスの位置の取得

Matrix4 transMatrix = Matrix4.Translation(
    new Vector3(sceneBodies[i].aabbMin.X, sceneBodies[i].aabbMin.Y, 0.0f));

バウンディングボックスの大きさを調整した後に、正しい位置に描画するため、以下の順番で描画のための行列の設定を行います。

  1. scaleMatrix
  2. transMatrix
Matrix4 WorldMatrix = renderMatrix * transMatrix * scaleMatrix;
なお、これらのデバッグ描画用に用意した頂点バッファなどのリソースはシーンの終了や切り替え時に解放することを忘れないようにしてください。
PrimitiveSceneでは以下において剛体描画用の頂点バッファ、デバッグ情報描画用の頂点バッファの解放を行っています。
public override void ReleaseScene()
{
    for (int i = 0; i < numShape; i++)
        if(vertices[i] != null)
            vertices[i].Dispose();

    if(aabbVert != null) aabbVert.Dispose();
    if(colVert != null) colVert.Dispose();
}

以上でPrimitiveSceneの概要を見てきましたが、実際にいろいろな値を変えたりすることで、 結果がどのように変わるのかを試してみることをお勧めします。

2次元物理シミュレーションPhysics2Dライブラリ各機能の使い方

ジョイントの設定

ジョイントの設定の例としてサンプルシーンJointSceneを見ていきます。
"sample/Physics2D/Physics2DSample/BasicScene/JointScene.cs"

ジョイントとは2つの剛体の間に設定する拘束のことです。2つの剛体間の拘束には並進方向と回転方向の拘束があります。
  1. 回転方向の拘束(回転が自由、固定、制約付き自由)
  2. 並進方向の拘束(並進移動が自由、固定、制約付き自由)
なお、Static剛体とStatic剛体の間のジョイントは意味を成さないため、設定をしないように気をつけてください。
alternate text
ジョイント接続においては、どの点で剛体と剛体を接続するかのアンカーポイントの設定と、
回転方向の拘束の指定、および、並進方向の拘束の指定が必要になります。
alternate text

回転方向の拘束とは、ジョイントで結合された剛体Aと剛体Bがアンカーポイントを中心として、

  • 自由に回転できる
  • 完全に固定されている
  • 角度制約の範囲内で自由に回転できる

を設定します。

alternate text

並進方向の拘束とは、ジョイントで結合された剛体Aと剛体Bがアンカーポイントを中心として、

  • 自由に移動できる
  • 完全に固定されている
  • 移動制約の範囲内で自由に移動できる

を設定します。

alternate text
並進方向の拘束にはお互いに直行する2つの軸に対して設定を行います。通常はX軸とY軸に対して指定しますが、
X軸とY軸でなくとも、お互いに直行する2つの軸であれば設定できます。例えばVector2(1, 1)の方向とVector2(-1, 1)の方向。
alternate text

回転方向、並進方向ともに固定の例

public override void InitScene ()
{

        ...
        ...
        ...

        // Link a box rigid body to the scene with a fixed joint.
        // If you just want to fix a rigid body to the scene perfectly, it is best simply making a static rigidbody.
        {
                sceneBodies[numBody] = new PhysicsBody(sceneShapes[2], 10.0f);
                sceneBodies[numBody].position = new Vector2(-30.0f, 0.0f);
                sceneBodies[numBody].shapeIndex = 2;
                numBody++;

                PhysicsBody b1 = sceneBodies[0];
                PhysicsBody b2 = sceneBodies[numBody-1];
                sceneJoints[numJoint] = new PhysicsJoint(b1, b2, (b2.position), 0, (uint)numBody-1);
                sceneJoints[numJoint].axis1Lim = new Vector2(1, 0);
                sceneJoints[numJoint].axis2Lim = new Vector2(0, 1);
                sceneJoints[numJoint].angleLim = 1;
                numJoint++;
        }
ここでは0番目の剛体と(numBody-1)番目の剛体の間にジョイントを生成しています。0番目の剛体はシーン内の床のStatic剛体です。

ジョイントの生成方法としては、ジョイントを作る剛体ペアの指定と、アンカーポイントの指定が必要になります。
PhysicsBody b1 = sceneBodies[0];
PhysicsBody b2 = sceneBodies[numBody-1];
sceneJoints[numJoint] = new PhysicsJoint(b1, b2, (b2.position), 0, (uint)numBody-1);
ここでアンカーポイントは(b2.position)と指定しているので、(numBody-1)番目の剛体の中心をアンカーポイントにしています。
ジョイントを生成した後に拘束条件の指定をしている部分が以下です。
sceneJoints[numJoint].axis1Lim = new Vector2(1, 0);
sceneJoints[numJoint].axis2Lim = new Vector2(0, 1);
sceneJoints[numJoint].angleLim = 1;
  1. Vector2(1, 0)方向を固定
  2. Vector2(0, 1)方向を固定
  3. 回転方向を固定

の順で指定が行われています。

ここで仮に

sceneJoints[numJoint].angleLim = 0;

と指定していたなら、回転方向は自由になります。

同様に仮に

sceneJoints[numJoint].axis1Lim = new Vector2(0, 0);

と指定していたなら、X軸方向の拘束がなくなり自由移動になります。

同様に仮に

sceneJoints[numJoint].axis2Lim = new Vector2(0, 0);

と指定していたなら、Y軸方向の拘束がなくなり自由移動になります。

もしも仮に

sceneJoints[numJoint].axis1Lim = new Vector2(0, 0);
sceneJoints[numJoint].axis2Lim = new Vector2(0, 0);
sceneJoints[numJoint].angleLim = 0;

とするならば、ジョイントの拘束条件を何も指定していないことになるので、ジョイントを設定していないのと同じになります。

回転方向における角度制約の例

public override void InitScene ()
{

...
...
...

        // Link a box rigid body to the scene with a rotation joint(with angle constraints)
        {
                sceneBodies[numBody] = new PhysicsBody(sceneShapes[2], 10.0f);
                sceneBodies[numBody].position = new Vector2(-20.0f, 0.0f);
                sceneBodies[numBody].shapeIndex = 2;
                numBody++;

                PhysicsBody b1 = sceneBodies[0];
                PhysicsBody b2 = sceneBodies[numBody-1];
                sceneJoints[numJoint] = new PhysicsJoint(b1, b2, (b2.position), 0, (uint)numBody-1);
                sceneJoints[numJoint].axis1Lim = new Vector2(1, 0);
                sceneJoints[numJoint].axis2Lim = new Vector2(0, 1);
                sceneJoints[numJoint].angleLim = 1;
                sceneJoints[numJoint].angleLower = PhysicsUtility.GetRadian(-45.0f);
                sceneJoints[numJoint].angleUpper = PhysicsUtility.GetRadian(45.0f);
                numJoint++;
        }

-45度から45度の制約付き角度制限を行っているのは以下の部分です。

sceneJoints[numJoint].angleLim = 1;
sceneJoints[numJoint].angleLower = PhysicsUtility.GetRadian(-45.0f);
sceneJoints[numJoint].angleUpper = PhysicsUtility.GetRadian(45.0f);
回転方向の制限がかかっていますが、angleLimだけでなくangleLowerとangleUpperも指定されており、
それぞれ-45度と45度が指定されています。angleLowerとangleUpperは角度制限の下限と上限を設定する変数です。
なお、角度はラジアンで指定する必要があります。

垂直方向における移動制約の例

public override void InitScene ()
{

...
...
...

        // Link a box rigid body to the scene with a horizontal slider joint (with movement constraints)
        {
                sceneBodies[numBody] = new PhysicsBody(sceneShapes[2], 10.0f);
                sceneBodies[numBody].position = new Vector2(0.0f, 0.0f);
                sceneBodies[numBody].shapeIndex = 2;
                numBody++;

                PhysicsBody b1 = sceneBodies[0];
                PhysicsBody b2 = sceneBodies[numBody-1];
                sceneJoints[numJoint] = new PhysicsJoint(b1, b2, (b2.position), 0, (uint)numBody-1);
                sceneJoints[numJoint].axis1Lim = new Vector2(1, 0);
                sceneJoints[numJoint].axis2Lim = new Vector2(0, 1);
                sceneJoints[numJoint].axis2Lower = -10.0f;
                sceneJoints[numJoint].axis2Upper = 10.0f;
                sceneJoints[numJoint].angleLim = 1;
                numJoint++;
        }

-10mから+10mまでのY軸方向の制約付き拘束を行っているのは以下の部分です。

sceneJoints[numJoint].axis2Lim = new Vector2(0, 1);
sceneJoints[numJoint].axis2Lower = -10.0f;
sceneJoints[numJoint].axis2Upper = 10.0f;
Y軸方向の拘束がありますが、axis2Limだけでなく、axis2Lowerとaxis2Upperも指定されています。
これによりアンカーポイントより-10mから+10mまではY軸方向に制約付きで自由に動けるようになっています。
axis2Lowerとaxis2Upperは並進移動制限の下限と上限を設定する変数です。

水平方向における移動制約の例

public override void InitScene ()
{

...
...
...

// Link a box rigid body to the scene with a horizontal slider joint (with movement constraints)
{
        sceneBodies[numBody] = new PhysicsBody(sceneShapes[2], 10.0f);
        sceneBodies[numBody].position = new Vector2(0.0f, 0.0f);
        sceneBodies[numBody].shapeIndex = 2;
        numBody++;

        PhysicsBody b1 = sceneBodies[0];
        PhysicsBody b2 = sceneBodies[numBody-1];
        sceneJoints[numJoint] = new PhysicsJoint(b1, b2, (b2.position), 0, (uint)numBody-1);
        sceneJoints[numJoint].axis1Lim = new Vector2(1, 0);
        sceneJoints[numJoint].axis1Lower = -10.0f;
        sceneJoints[numJoint].axis1Upper = 10.0f;
        sceneJoints[numJoint].axis2Lim = new Vector2(0, 1);
        sceneJoints[numJoint].angleLim = 1;
        numJoint++;

    // The horizontal slider joint is quite slippy,
    // so to make the rigid body stop we add some air friction
        b2.airFriction = 0.01f;
}

垂直方向における移動制約の例と同様に水平方向の移動制約の設定を行っています。

sceneJoints[numJoint].axis1Lim = new Vector2(1, 0);
sceneJoints[numJoint].axis1Lower = -10.0f;
sceneJoints[numJoint].axis1Upper = 10.0f;
これによりアンカーポイントより-10[m]から+10[m]まではX軸方向に制約付きで自由に動けるようになっています。

ここで水平方向に制約付きで移動を許可した場合、水平方向には重力がかからないため、水平方向に動いている物体がなかなか静止しない
ということが生じる可能性があるため、空気抵抗を剛体に与えるように設定しています。

// The horizontal slider joint is quite slippy,
// so to make the rigid body stop we add some air friction
b2.airFriction = 0.01f;
(numBody-1)番目の剛体に対して空気抵抗係数0.01fを与え、空気抵抗によって水平方向に微小に滑り続けるのを防止しています。