サイトトップ

Director Flash 書籍 業務内容 プロフィール
 

HTML5 / Flash ActionScript講座
gihyo.jp連載
「HTML5のCanvasでつくる
ダイナミックな表現
―CreateJSを使う」
Twitter: @FumioNonaka
Facebook Page: CreateJS

CreateJS Workshop

CreatJS Style Book
【coming soon】
WebクリエイターのためのCreateJSスタイルブック
  • 著者:野中文雄
  • 定価:本体価格2,980円+税
  • ISBN978-4-8399-4517-6
  • 仕様:B5変型、344ページ(予定)
  • 刊行:2013年6月下旬予定
*画像はイメージです。

EaselJSでBox2Dを使った物理シミュレーションを試す

先に、インストールについて簡単に紹介する。Box2dWebのサイトに「Downloads」のリンクがある(図001)。

図001■Box2dWebのサイト
図001

ダウンロードしたZip圧縮ファイル(Box2dWeb-2.1a.3.zip)を展開すると、ふたつのJavaScript(JS)ファィルが入っている(図002)。どちらかひとつのJSファイルをライブラリ用の適切なフォルダに納めればよい。サーバーに上げるにはmin(コンパクト)版が少ない容量で済む。実装を調べるときは通常版を用いる。

図002■ダウンロードして展開したBox2Dライブラリのファィル
図002


01 物理空間と剛体を定める

Box2Dのスクリプティングにかかる前に、物理演算シミュレーションで落とすオブジェクトをひとつCanvasに置く。矩形のShapeインスタンスを使うことにする。位置は定めないので、ステージの基準点である左上角に描かれる(図03-03-004)。後に全体を掲げるコード001から抜出したscript要素は以下のとおり。

図003■Canvas左上角に青い正方形のShapeオブジェクトを置く
図003

  1. <script>
  2. var createjs = window;
  3. </script>
  1. <script src="easeljs/utils/UID.js"></script>
  2. <script src="easeljs/geom/Matrix2D.js"></script>
  3. <script src="easeljs/events/EventDispatcher.js"></script>
  4. <script src="easeljs/events/MouseEvent.js"></script>
  5. <script src="easeljs/display/DisplayObject.js"></script>
  6. <script src="easeljs/display/Container.js"></script>
  7. <script src="easeljs/display/Stage.js"></script>
  8. <script src="easeljs/display/Graphics.js"></script>
  9. <script src="easeljs/display/Shape.js"></script>
  10. <script>
  11. var stage;
  1. var boxEdge = 20;
  2. function initialize() {
  3.   var canvasElement = document.getElementById("myCanvas");
  4.   stage = new Stage(canvasElement);
  5.   initializeBox2D(canvasElement.width);
  6.   stage.update();
  7. }
  8. function initializeBox2D(nWidth) {
  1.   var myShape = createShape(bodyDef, boxEdge, boxEdge, "#0000FF");
  2.   stage.addChild(myShape);
  3. }
  1. function createShape(bodyDef, nWidth, nHeight, nColor) {
  2.   var myShape = new Shape();
  3.   draw(myShape.graphics, nWidth, nHeight, nColor);
  4.   myShape.regX = nWidth / 2;
  5.   myShape.regY = nHeight / 2;
  1.   return myShape;
  2. }
  3. function draw(myGraphics, nWidth, nHeight, nColor) {
  4.   myGraphics.beginFill(nColor);
  5.   myGraphics.drawRect(0, 0, nWidth, nHeight);
  6. }
  7. </script>

物理演算は計算なので、結果は数値。数値はステージに描いたオブジェクトのプロパティに当てはめて、初めてオブジェクトの動きとして目に見える。ステージのオブジェクトとBox2Dとの間は、人形と黒子のような関係になる。

Box2dWebライブラリのJavaScript(JS)ファイルBox2dWeb-2.1.a.3.min.jsを、script要素で読込んでおく(ファイルを納めたフォルダはlibとした)。

  1. <script src="lib/Box2dWeb-2.1.a.3.min.js"></script>

物理演算を行うにために、まずは3つの仕込みを行う。

    【物理演算シミュレーションの準備】
  1. 物理空間をつくる
  2. 剛体を定義する
  3. 剛体定義に表示オブジェクトを関連づける

01-01 物理空間をつくる

物理空間は、Box2Dのb2Worldクラスのオブジェクトとしてつくる。コンストラクタの第1引数に渡す重力は、2次元のベクトルを表すb2Vec2オブジェクトで定める。第2引数のスリープはブール(論理)値で、trueを渡すと動かなくなった剛体はシミュレーションから外して(スリープして)効率を高める。

var world = new Box2D.Dynamics.b2World(重力, スリープ);

Box2dWebは、数多くのクラスをJavaScriptファイルでは分けず、代わりに名前空間により分類・識別する。ひとつの関数の中で同じクラスを何度も用いる場合には、いちいち名前空間から書上げるのは煩わしく、コードも見にくい。そういうときは、クラスを予め変数に入れて参照すると便利だ。

var b2World = Box2D.Dynamics.b2World;
var world = new b2World(重力, スリープ);

重力は通常垂直(y軸)方向の力なので、b2Vec2()コンストラクタの第1引数に渡すx座標値は0にした(第30行目)。また、垂直方向のy座標値は、変数(gravityVertical)に定めている(第18行目)。生成したb2Worldインスタンスは、変数(world)に納めた(第17および第30行目)。

  1. var world;
  2. var gravityVertical = 10;
  1. function initialize() {
  1.   initializeBox2D(canvasElement.width);
  1. }
  2. function initializeBox2D(nWidth) {
  3.   var b2Vec2 = Box2D.Common.Math.b2Vec2;
  1.   var b2World = Box2D.Dynamics.b2World;
  2.   world = new b2World(new b2Vec2(0, gravityVertical), true);
  1. }

01-02 剛体を定義する

後で落下させる予定の剛体は、1辺20ピクセルの正方形とし、その長さを変数(boxEdge)に置く(第19行目)。剛体を定義する関数(defineBody())は別に定めた(第35〜41行目)。関数の第1および第2引数(nXとnY)は、剛体のxy座標だ。第3引数(nType)は剛体の種類で、b2Bodyクラスの定数(静的プロパティ)として備わる整数で定める(表001)。

表001■剛体の種類を定めるb2Body定数
マウスイベント 剛体の種類
b2_dynamicBody 動的 2
b2_kinematicBody キネマティック 1
b2_staticBody 静的 0

剛体を定義する関数(defineBody())は、b2BodyDef()コンストラクタでインスタンスをつくったら、剛体の位置と種類を決める(第36〜39行目)。位置はb2BodyDef.positionプロパティでb2Vec2オブジェクトを参照し、b2Vec2.Set()メソッドにより座標を決める。剛体の種類はb2BodyDef.typeプロパティに、b2Body.b2_dynamicBodyを定めた。関数は、定義の済んだb2BodyDefインスタンスを返す(第40行目)。

  1. var SCALE = 1 / 30;
  1. var boxEdge = 20;
  2. function initialize() {
  3.   var canvasElement = document.getElementById("myCanvas");
  1.   initializeBox2D(canvasElement.width);
  1. }
  2. function initializeBox2D(nWidth) {
  1.   var b2Body = Box2D.Dynamics.b2Body;
  1.   var bodyDef = defineBody(nWidth / 2, boxEdge, b2Body.b2_dynamicBody);
  1. }
  2. function defineBody(nX , nY, nType) {
  3.   var b2BodyDef = Box2D.Dynamics.b2BodyDef;
  4.   var bodyDef = new b2BodyDef();
  5.   bodyDef.position.Set(nX * SCALE, nY * SCALE);
  6.   bodyDef.type = nType;
  7.   return bodyDef;
  8. }

なお、Box2Dは物理学と同じメートル(m)・キログラム(kg)・秒の単位にもとづいてシミュレーションする。そこで、ピクセルからメートルへの換算比率(メートル/ピクセル)を定数(SCALE)にした(上記コード第16行目)。したがって、ピクセルで与えられた数値をBox2Dの座標として渡すときには、この定数値を乗じる(第38行目)。

01-03 剛体定義に表示オブジェクトを関連づける

ここまでの物理空間と剛体の定義は、Box2Dの仕込だった。ステージ上で動かそうとしているオブジェクトは、まだ蚊帳の外だ。そこで、Shapeオブジェクトをつくる関数(createShape())に、剛体定義とのつながりを加える。関数の第1引数には剛体定義のb2BodyDefオブジェクトが渡される(第32および第42行目)。そのb2BodyDef.userDataプロパティにShapeオブジェクト(myShape)を与えた(第47行目)。

  1. function initialize() {
  1.   initializeBox2D(canvasElement.width);
  1. }
  2. function initializeBox2D(nWidth) {
  1.   var bodyDef = defineBody(nWidth / 2, boxEdge, b2Body.b2_dynamicBody);
  2.   var myShape = createShape(bodyDef, boxEdge, boxEdge, "#0000FF");
  1. }
  2. function defineBody(nX , nY, nType) {
  3.   var b2BodyDef = Box2D.Dynamics.b2BodyDef;
  4.   var bodyDef = new b2BodyDef();
  1.   return bodyDef;
  2. }
  3. function createShape(bodyDef, nWidth, nHeight, nColor) {
  1.   bodyDef.userData = myShape;
  2.   return myShape;
  3. }

3つの準備を整えたscript要素全体は、つぎのコード03-03-001のとおりだ。もっとも、まだ物理演算のシミュレーションは行っていないので、青い正方形のShapeインスタンスがCanvasの左上角に表れるだけだ(前掲図003)。

コード001■Box2Dの物理空間に剛体を定義する
  1. <script>
  2. var createjs = window;
  3. </script>
  4. <script src="lib/Box2dWeb-2.1.a.3.min.js"></script>
  5. <script src="easeljs/utils/UID.js"></script>
  6. <script src="easeljs/geom/Matrix2D.js"></script>
  7. <script src="easeljs/events/EventDispatcher.js"></script>
  8. <script src="easeljs/events/MouseEvent.js"></script>
  9. <script src="easeljs/display/DisplayObject.js"></script>
  10. <script src="easeljs/display/Container.js"></script>
  11. <script src="easeljs/display/Stage.js"></script>
  12. <script src="easeljs/display/Graphics.js"></script>
  13. <script src="easeljs/display/Shape.js"></script>
  14. <script>
  15. var stage;
  16. var SCALE = 1 / 30;
  17. var world;
  18. var gravityVertical = 10;
  19. var boxEdge = 20;
  20. function initialize() {
  21.   var canvasElement = document.getElementById("myCanvas");
  22.   stage = new Stage(canvasElement);
  23.   initializeBox2D(canvasElement.width);
  24.   stage.update();
  25. }
  26. function initializeBox2D(nWidth) {
  27.   var b2Vec2 = Box2D.Common.Math.b2Vec2;
  28.   var b2Body = Box2D.Dynamics.b2Body;
  29.   var b2World = Box2D.Dynamics.b2World;
  30.   world = new b2World(new b2Vec2(0, gravityVertical), true);
  31.   var bodyDef = defineBody(nWidth / 2, boxEdge, b2Body.b2_dynamicBody);
  32.   var myShape = createShape(bodyDef, boxEdge, boxEdge, "#0000FF");
  33.   stage.addChild(myShape);
  34. }
  35. function defineBody(nX , nY, nType) {
  36.   var b2BodyDef = Box2D.Dynamics.b2BodyDef;
  37.   var bodyDef = new b2BodyDef();
  38.   bodyDef.position.Set(nX * SCALE, nY * SCALE);
  39.   bodyDef.type = nType;
  40.   return bodyDef;
  41. }
  42. function createShape(bodyDef, nWidth, nHeight, nColor) {
  43.   var myShape = new Shape();
  44.   draw(myShape.graphics, nWidth, nHeight, nColor);
  45.   myShape.regX = nWidth / 2;
  46.   myShape.regY = nHeight / 2;
  47.   bodyDef.userData = myShape;
  48.   return myShape;
  49. }
  50. function draw(myGraphics, nWidth, nHeight, nColor) {
  51.   myGraphics.beginFill(nColor);
  52.   myGraphics.drawRect(0, 0, nWidth, nHeight);
  53. }
  54. </script>

02 剛体を落下させる − 物理演算シミュレーションの実行

Box2Dで剛体の物理演算をするには、まず剛体定義のb2BodyDefオブジェクトをb2World.CreateBody()メソッドの引数に渡して、物理空間に剛体(Bodyオブジェクト)をつくらなければならない。メソッドからは、剛体定義にもとづいたBodyオブジェクトが返される。

b2Worldオブジェクト.CreateBody(b2BodyDefオブジェクト)

これから手を加えるJavaScriptのステートメントは、後にコード002として全体を掲げる。b2World.CreateBody()メソッドは、新たに定める関数(createBody())から呼出す(第61行目)。そして、この関数の呼出しは、Box2Dを初期設定する関数(initializeBox2D())に加える(38行目)。引数にはb2Worldとb2BodyDefのオブジェクト(worldとbodyDef)を渡す。

つぎに、物理シミュレートするため、物理空間のb2Worldオブジェクトに対して時間を進めるメソッドが、b2World.Step()だ。第1引数には、シミュレーションのために進める時間を秒数で渡す。第2および第3引数は、物理的な制約に合わせて調整の再計算をさせる回数だ。前者は速度、後者が位置についての演算を定める。

b2Worldオブジェクト.Step(経過秒数, 速度再計算, 位置再計算)

人形のオブジェクトはTickerクラスで動かす(第14および第29行目)。リスナー関数(update())から、b2World.Step()メソッドを呼出す(第63〜64行目)。物理シミュレーションで進める秒数は予め変数(time)にとり(第22行目)、b2World.Step()メソッドの第1引数に渡すとともに、Ticker.setInterval()メソッド(前掲Syntax 02-01-005)でTicker.tickイベントの間隔も合わせた(第28行目)。

物理演算の時間を進めるたら、剛体の幾何情報を人形のオブジェクトに伝える。物理空間に加えられた剛体のうち初めのb2Bodyオブジェクトを取出すのが、b2World.GetBodyList()メソッドだ。剛体の位置は、b2Body.GetPosition()メソッドでb2Vec2オブジェクトとして得られる。また、その回転角は、b2Body.GetAngle()メソッドによりラジアン角が返される。

なお、剛体定義のb2BodyDef.userDataプロパティに与えた人形のオブジェクトの参照は、定義からつくられた剛体のオブジェクトにb2Body.GetUserData()メソッドを呼出すと得られる。また、DisplayObject.rotationプロパティの単位は度数なので、ラジアン角はMatrix2D.DEG_TO_RAD定数で除す。

  1. <script src="easeljs/utils/Ticker.js"></script>
  2. <script>
  1. var velocityIterations = 8;
  2. var positionIterations = 3;
  3. var time = 1 / 24;
  1. function initialize() {
  1.   initializeBox2D(canvasElement.width);
      // stage.update();
  2.   Ticker.setInterval(time * 1000);
  3.   Ticker.addEventListener("tick", update);
  4. }
  5. function initializeBox2D(nWidth) {
  1.   world = new b2World(new b2Vec2(0, gravityVertical), true);
  2.   var bodyDef = defineBody(nWidth / 2, boxEdge, b2Body.b2_dynamicBody);
  1.   createBody(world, bodyDef);
  1. }
  1. function createBody(world, bodyDef) {
  2.   var body = world.CreateBody(bodyDef);
  3. }
  4. function update() {
  5.   world.Step(time, velocityIterations, positionIterations);
  6.   var body = world.GetBodyList();
  7.   var myObject = body.GetUserData();
  8.   if (myObject) {
  9.     var position = body.GetPosition();
  10.     myObject.x = position.x / SCALE;
  11.     myObject.y = position.y / SCALE;
  12.     myObject.rotation = body.GetAngle() / Matrix2D.DEG_TO_RAD;
  13.     stage.update();
  14.   }
  15. }
  16. </script>

これでBox2Dの物理演算シミュレーションにもとづいて、インスタンスが重力により自由落下する(図004)。ここまでの書替えをすべて加えたscript要素の全体は、つぎのコード002のとおりだ。

図004■正方形のインスタンスが自由落下する
図004

コード002■オブジェクトをBox2Dの物理シミュレーションに合わせて落下させる
  1. <script>
  2. var createjs = window;
  3. </script>
  4. <script src="lib/Box2dWeb-2.1.a.3.min.js"></script>
  5. <script src="easeljs/utils/UID.js"></script>
  6. <script src="easeljs/geom/Matrix2D.js"></script>
  7. <script src="easeljs/events/EventDispatcher.js"></script>
  8. <script src="easeljs/events/MouseEvent.js"></script>
  9. <script src="easeljs/display/DisplayObject.js"></script>
  10. <script src="easeljs/display/Container.js"></script>
  11. <script src="easeljs/display/Stage.js"></script>
  12. <script src="easeljs/display/Graphics.js"></script>
  13. <script src="easeljs/display/Shape.js"></script>
  14. <script src="easeljs/utils/Ticker.js"></script>
  15. <script>
  16. var stage;
  17. var SCALE = 1 / 30;
  18. var world;
  19. var gravityVertical = 10;
  20. var velocityIterations = 8;
  21. var positionIterations = 3;
  22. var time = 1 / 24;
  23. var boxEdge = 20;
  24. function initialize() {
  25.   var canvasElement = document.getElementById("myCanvas");
  26.   stage = new Stage(canvasElement);
  27.   initializeBox2D(canvasElement.width);
  28.   Ticker.setInterval(time * 1000);
  29.   Ticker.addEventListener("tick", update);
  30. }
  31. function initializeBox2D(nWidth) {
  32.   var b2Vec2 = Box2D.Common.Math.b2Vec2;
  33.   var b2Body = Box2D.Dynamics.b2Body;
  34.   var b2World = Box2D.Dynamics.b2World;
  35.   world = new b2World(new b2Vec2(0, gravityVertical), true);
  36.   var bodyDef = defineBody(nWidth / 2, boxEdge, b2Body.b2_dynamicBody);
  37.   var myShape = createShape(bodyDef, boxEdge, boxEdge, "#0000FF");
  38.   createBody(world, bodyDef);
  39.   stage.addChild(myShape);
  40. }
  41. function defineBody(nX , nY, nType) {
  42.   var b2BodyDef = Box2D.Dynamics.b2BodyDef;
  43.   var bodyDef = new b2BodyDef();
  44.   bodyDef.position.Set(nX * SCALE, nY * SCALE);
  45.   bodyDef.type = nType;
  46.   return bodyDef;
  47. }
  48. function createShape(bodyDef, nWidth, nHeight, nColor) {
  49.   var myShape = new Shape();
  50.   draw(myShape.graphics, nWidth, nHeight, nColor);
  51.   myShape.regX = nWidth / 2;
  52.   myShape.regY = nHeight / 2;
  53.   bodyDef.userData = myShape;
  54.   return myShape;
  55. }
  56. function draw(myGraphics, nWidth, nHeight, nColor) {
  57.   myGraphics.beginFill(nColor);
  58.   myGraphics.drawRect(0, 0, nWidth, nHeight);
  59. }
  60. function createBody(world, bodyDef) {
  61.   var body = world.CreateBody(bodyDef);
  62. }
  63. function update() {
  64.   world.Step(time, velocityIterations, positionIterations);
  65.   var body = world.GetBodyList();
  66.   var myObject = body.GetUserData();
  67.   if (myObject) {
  68.     var position = body.GetPosition();
  69.     myObject.x = position.x / SCALE;
  70.     myObject.y = position.y / SCALE;
  71.     myObject.rotation = body.GetAngle() / Matrix2D.DEG_TO_RAD;
  72.     stage.update();
  73.   }
  74. }
  75. </script>

03 静的な剛体を加える − 衝突のシミュレーション

ステージの下端に床を静的な剛体として加える。b2BodyDef.typeプロパティに与える定数はb2Body.b2_staticBodyだ(前掲表001「剛体の種類を定めるb2Body定数」参照)。床の静的な剛体と落下させる動的な剛体は、それぞれ別の関数(createStaticFloor()とcreateDynamicBox())に分けてつくる。ともに剛体の定義(defineBody())と作成(createBody())、およびShapeオブジェクト作成(createShape)の3つの関数を呼出す。今のところ、つくられる剛体の種類が静的か動的かというほかに違いはない。

  1. function initialize() {
  2.   var canvasElement = document.getElementById("myCanvas");
      // initializeBox2D(canvasElement.width);
  1.   initializeBox2D(canvasElement.width, canvasElement.height);
  1. }
    // function initializeBox2D(nWidth) {
  2. function initializeBox2D(stageWidth, stageHeight) {
      // var bodyDef = defineBody(nWidth / 2, boxEdge, b2Body.b2_dynamicBody);
      // var myShape = createShape(bodyDef, boxEdge, boxEdge, "#0000FF");
      // createBody(world, bodyDef);

  1.   var centerX = stageWidth / 2;
  2.   var floorWidth = stageWidth * 0.8;
  3.   var floorHeight = boxEdge;
  4.   var floorShape = createStaticFloor(centerX, stageHeight - floorHeight, floorWidth, floorHeight, "#CCCCCC");
  5.   stage.addChild(floorShape);
  6.   var myShape = createDynamicBox(centerX, boxEdge, boxEdge, boxEdge, "#0000FF");
  7.   stage.addChild(myShape);
  8. }
  9. function createStaticFloor(nX, nY, nWidth, nHeight, nColor) {
  10.   var b2Body = Box2D.Dynamics.b2Body;
  11.   var bodyDef = defineBody(nX, nY, b2Body.b2_staticBody);
  12.   var myShape = createShape(bodyDef, nWidth, nHeight, nColor);
  13.   createBody(world, bodyDef);
  14.   return myShape;
  15. }
  16. function createDynamicBox(nX, nY, nWidth, nHeight, nColor) {
  17.   var b2Body = Box2D.Dynamics.b2Body;
  18.   var bodyDef = defineBody(nX, nY, b2Body.b2_dynamicBody);
  19.   var myShape = createShape(bodyDef, nWidth, nHeight, nColor);
  20.   createBody(world, bodyDef);
  21.   return myShape;
  22. }

物理空間の中の初めのb2Bodyオブジェクトは、b2World.GetBodyList()メソッドから参照を得た。その後、物理空間から順につぎの剛体を取出すには、b2Bodyオブジェクトに対してb2Body.GetNext()メソッド(前掲03-03-001-2)を呼出す。

そこで、Ticker.tickイベントのリスナー関数(update())で複数の剛体が扱えるように、while文を加えて繰返し処理するように書替えた(第67〜76行目)。ループの終わりにb2Body.GetNext()メソッドでつぎの剛体を取出し、オブジェクトがあればシミュレーションを続ける。

  1. function update() {
  2.   world.Step(time, velocityIterations, positionIterations);
  3.   var body = world.GetBodyList();
  4.   while (body) {
  5.     var myObject = body.GetUserData();
  6.     if (myObject) {
  7.       var position = body.GetPosition();
  8.       myObject.x = position.x / SCALE;
  9.       myObject.y = position.y / SCALE;
  10.       myObject.rotation = body.GetAngle() / Matrix2D.DEG_TO_RAD;
          // stage.update();
  11.     }
  12.     body = body.GetNext();
  13.   }
  14.   stage.update();
  15. }

なお、矩形と床の剛体はTicker.tickイベントごとにまとめて描画すればよい。そのため、Stage.update()メソッドの呼出しは、whileループの外に出した(第77行目)。

ここまでの手を加えたJavvaScriptコードが、以下のコード003だ。試してみると、落下したオブジェクトが床を抜けるイリュージョンになってしまう(図005)。

図005■落下するオブジェクトが床をすり抜ける
図005

コード003■落下する剛体と静的な剛体の物理シミュレーション
  1. var stage;
  2. var SCALE = 1 / 30;
  3. var world;
  4. var gravityVertical = 10;
  5. var velocityIterations = 8;
  6. var positionIterations = 3;
  7. var time = 1 / 24;
  8. var boxEdge = 20;
  9. function initialize() {
  10.   var canvasElement = document.getElementById("myCanvas");
  11.   stage = new Stage(canvasElement);
  12.   initializeBox2D(canvasElement.width, canvasElement.height);
  13.   Ticker.setInterval(time * 1000);
  14.   Ticker.addEventListener("tick", update);
  15. }
  16. function initializeBox2D(stageWidth, stageHeight) {
  17.   var b2Vec2 = Box2D.Common.Math.b2Vec2;
  18.   var b2World = Box2D.Dynamics.b2World;
  19.   world = new b2World(new b2Vec2(0, gravityVertical), true);
  20.   var centerX = stageWidth / 2;
  21.   var floorWidth = stageWidth * 0.8;
  22.   var floorHeight = boxEdge;
  23.   var floorShape = createStaticFloor(centerX, stageHeight - floorHeight, floorWidth, floorHeight, "#CCCCCC");
  24.   stage.addChild(floorShape);
  25.   var myShape = createDynamicBox(centerX, boxEdge, boxEdge, boxEdge, "#0000FF");
  26.   stage.addChild(myShape);
  27. }
  28. function createStaticFloor(nX, nY, nWidth, nHeight, nColor) {
  29.   var b2Body = Box2D.Dynamics.b2Body;
  30.   var bodyDef = defineBody(nX, nY, b2Body.b2_staticBody);
  31.   var myShape = createShape(bodyDef, nWidth, nHeight, nColor);
  32.   createBody(world, bodyDef);
  33.   return myShape;
  34. }
  35. function createDynamicBox(nX, nY, nWidth, nHeight, nColor) {
  36.   var b2Body = Box2D.Dynamics.b2Body;
  37.   var bodyDef = defineBody(nX, nY, b2Body.b2_dynamicBody);
  38.   var myShape = createShape(bodyDef, nWidth, nHeight, nColor);
  39.   createBody(world, bodyDef);
  40.   return myShape;
  41. }
  42. function defineBody(nX , nY, nType) {
  43.   var b2BodyDef = Box2D.Dynamics.b2BodyDef;
  44.   var bodyDef = new b2BodyDef();
  45.   bodyDef.position.Set(nX * SCALE, nY * SCALE);
  46.   bodyDef.type = nType;
  47.   return bodyDef;
  48. }
  49. function createShape(bodyDef, nWidth, nHeight, nColor) {
  50.   var myShape = new Shape();
  51.   draw(myShape.graphics, nWidth, nHeight, nColor);
  52.   myShape.regX = nWidth / 2;
  53.   myShape.regY = nHeight / 2;
  54.   bodyDef.userData = myShape;
  55.   return myShape;
  56. }
  57. function draw(myGraphics, nWidth, nHeight, nColor) {
  58.   myGraphics.beginFill(nColor);
  59.   myGraphics.drawRect(0, 0, nWidth, nHeight);
  60. }
  61. function createBody(world, bodyDef) {
  62.   var body = world.CreateBody(bodyDef);
  63. }
  64. function update() {
  65.   world.Step(time, velocityIterations, positionIterations);
  66.   var body = world.GetBodyList();
  67.   while (body) {
  68.     var myObject = body.GetUserData();
  69.     if (myObject) {
  70.       var position = body.GetPosition();
  71.       myObject.x = position.x / SCALE;
  72.       myObject.y = position.y / SCALE;
  73.       myObject.rotation = body.GetAngle() / Matrix2D.DEG_TO_RAD;
  74.     }
  75.     body = body.GetNext();
  76.   }
  77.   stage.update();
  78. }

04 物理シミュレーションにフィクスチャを加える

私たちが「矩形」とか「床」とわかるのは、Shapeインスタンスのかたちを見ているからだ。しかし、黒子のBox2Dには、剛体定義の関数(defineBody())で座標と動くか動かないかしか伝えていない。剛体のかたちや大きさは、フィクスチャというオブジェクトで定めなければならない。

剛体にフィクスチャを加えたコード03-03-004は後にまとめて掲げる。フィクスチャの定義をb2FixtureDefオブジェクトでつくり、b2FixtureDef.shapeプロパティにかたちを与える。かたちを定めるのはBox2Dのシェイブ(b2Shape)オブジェクトだ。矩形はb2PolygonShapeオブジェクトにb2PolygonShape.SetAsBox()メソッドで幅と高さを引数に渡してつくる。

剛体をつくる関数(createBody())の引数には、b2FixtureDefオブジェクトを加えた(第77行目)。剛体にフィクスチャを与えるのはb2Body.CreateFixture()メソッドで、b2FixtureDefオブジェクトを引数に渡す(第79行目)。

  1. function createStaticFloor(nX, nY, nWidth, nHeight, nColor) {
  1.   var boxShape = createBoxShape(nWidth / 2, nHeight / 2);
  2.   var fixtureDef = defineFixture(boxShape);
      // createBody(world, bodyDef);
  3.   createBody(world, bodyDef, fixtureDef);
  1. }
  2. function createDynamicBox(nX, nY, nWidth, nHeight, nColor) {
  1.   var boxShape = createBoxShape(nWidth / 2, nHeight / 2);
  2.   var fixtureDef = defineFixture(boxShape);
      // createBody(world, bodyDef);
  3.   createBody(world, bodyDef, fixtureDef);
  1. }
  1. function defineFixture(myShape) {
  2.   var b2FixtureDef = Box2D.Dynamics.b2FixtureDef;
  3.   var fixtureDef = new b2FixtureDef();
  4.   fixtureDef.shape = myShape;
  5.   return fixtureDef;
  6. }
  7. function createBoxShape(nX, nY) {
  8.   var b2PolygonShape = Box2D.Collision.Shapes.b2PolygonShape;
  9.   var myPolygonShape = new b2PolygonShape();
  10.   myPolygonShape.SetAsBox(nX * SCALE, nY * SCALE);
  11.   return myPolygonShape;
  12. }
    // function createBody(world, bodyDef) {
  13. function createBody(world, bodyDef, fixtureDef) {
  14.   var body = world.CreateBody(bodyDef);
  15.   body.CreateFixture(fixtureDef);
  16. }

これでBox2Dにふたつの剛体のかたちは伝わり、互いの衝突がわかるということです。JavaScriptコード全体は、以下のコード004のとおりだ。けれど、矩形は床に落ちても弾みもせず、まるで完璧な体操の着地のごとく、床に吸いつくように止まる(図006)。

図006■落下したオブジェクトは床の上に吸いつくように着地する
図006

コード004■剛体のかたちをフィクスチャで定めた物理シミュレーション
  1. var stage;
  2. var SCALE = 1 / 30;
  3. var world;
  4. var gravityVertical = 10;
  5. var velocityIterations = 8;
  6. var positionIterations = 3;
  7. var time = 1 / 24;
  8. var boxEdge = 20;
  9. function initialize() {
  10.   var canvasElement = document.getElementById("myCanvas");
  11.   stage = new Stage(canvasElement);
  12.   initializeBox2D(canvasElement.width, canvasElement.height);
  13.   Ticker.setInterval(time * 1000);
  14.   Ticker.addEventListener("tick", update);
  15. }
  16. function initializeBox2D(stageWidth, stageHeight) {
  17.   var b2Vec2 = Box2D.Common.Math.b2Vec2;
  18.   var b2World = Box2D.Dynamics.b2World;
  19.   world = new b2World(new b2Vec2(0, gravityVertical), true);
  20.   var centerX = stageWidth / 2;
  21.   var floorWidth = stageWidth * 0.8;
  22.   var floorHeight = boxEdge;
  23.   var floorShape = createStaticFloor(centerX, stageHeight - floorHeight, floorWidth, floorHeight, "#CCCCCC");
  24.   stage.addChild(floorShape);
  25.   var myShape = createDynamicBox(centerX, boxEdge, boxEdge, boxEdge, "#0000FF");
  26.   stage.addChild(myShape);
  27. }
  28. function createStaticFloor(nX, nY, nWidth, nHeight, nColor) {
  29.   var b2Body = Box2D.Dynamics.b2Body;
  30.   var bodyDef = defineBody(nX, nY, b2Body.b2_staticBody);
  31.   var myShape = createShape(bodyDef, nWidth, nHeight, nColor);
  32.   var boxShape = createBoxShape(nWidth / 2, nHeight / 2);
  33.   var fixtureDef = defineFixture(boxShape);
  34.   createBody(world, bodyDef, fixtureDef);
  35.   return myShape;
  36. }
  37. function createDynamicBox(nX, nY, nWidth, nHeight, nColor) {
  38.   var b2Body = Box2D.Dynamics.b2Body;
  39.   var bodyDef = defineBody(nX, nY, b2Body.b2_dynamicBody);
  40.   var myShape = createShape(bodyDef, nWidth, nHeight, nColor);
  41.   var boxShape = createBoxShape(nWidth / 2, nHeight / 2);
  42.   var fixtureDef = defineFixture(boxShape);
  43.   createBody(world, bodyDef, fixtureDef);
  44.   return myShape;
  45. }
  46. function defineBody(nX , nY, nType) {
  47.   var b2BodyDef = Box2D.Dynamics.b2BodyDef;
  48.   var bodyDef = new b2BodyDef();
  49.   bodyDef.position.Set(nX * SCALE, nY * SCALE);
  50.   bodyDef.type = nType;
  51.   return bodyDef;
  52. }
  53. function createShape(bodyDef, nWidth, nHeight, nColor) {
  54.   var myShape = new Shape();
  55.   draw(myShape.graphics, nWidth, nHeight, nColor);
  56.   myShape.regX = nWidth / 2;
  57.   myShape.regY = nHeight / 2;
  58.   bodyDef.userData = myShape;
  59.   return myShape;
  60. }
  61. function draw(myGraphics, nWidth, nHeight, nColor) {
  62.   myGraphics.beginFill(nColor);
  63.   myGraphics.drawRect(0, 0, nWidth, nHeight);
  64. }
  65. function defineFixture(myShape) {
  66.   var b2FixtureDef = Box2D.Dynamics.b2FixtureDef;
  67.   var fixtureDef = new b2FixtureDef();
  68.   fixtureDef.shape = myShape;
  69.   return fixtureDef;
  70. }
  71. function createBoxShape(nX, nY) {
  72.   var b2PolygonShape = Box2D.Collision.Shapes.b2PolygonShape;
  73.   var myPolygonShape = new b2PolygonShape();
  74.   myPolygonShape.SetAsBox(nX * SCALE, nY * SCALE);
  75.   return myPolygonShape;
  76. }
  77. function createBody(world, bodyDef, fixtureDef) {
  78.   var body = world.CreateBody(bodyDef);
  79.   body.CreateFixture(fixtureDef);
  80. }
  81. function update() {
  82.   world.Step(time, velocityIterations, positionIterations);
  83.   var body = world.GetBodyList();
  84.   while (body) {
  85.     var myObject = body.GetUserData();
  86.     if (myObject) {
  87.       var position = body.GetPosition();
  88.       myObject.x = position.x / SCALE;
  89.       myObject.y = position.y / SCALE;
  90.       myObject.rotation = body.GetAngle() / Matrix2D.DEG_TO_RAD;
  91.     }
  92.     body = body.GetNext();
  93.   }
  94.   stage.update();
  95. }

前掲コード004では、落とした剛体が弾むような材質であることを、まだBox2Dに教えていない。フィクスチャの定義には、つぎの表002のように密度と摩擦および弾性の3つの性質がプロパティで定められる。

表002■フィクスチャを定義するb2FixtureDefオブジェクトに定めるプロパティと値
プロパティ 意味
density 密度 重さ(kg) / 大きさ(m2)
friction 摩擦 0から1の間の数値
restitution 弾性 0から1の間の数値

フィスクチャに質感を加えよう。JavaScriptコード全体は後にコード03-03-005として掲げる。つぎのように、密度と摩擦および弾性の3つを新たな関数(setFixtureDef())で定め(第81〜85行目)、落下する剛体をつくる関数(createDynamicBox())から呼出した(第46行目)。

  1. var density = 1;
  2. var friction = 0.1;
  3. var restitution = 0.5;
  1. function createDynamicBox(nX, nY, nWidth, nHeight, nColor) {
  1.   var fixtureDef = defineFixture(boxShape);
  2.   setFixtureDef(fixtureDef, density, friction, restitution);
  1. }
  1. function setFixtureDef(fixtureDef, density, friction, restitution) {
  2.   fixtureDef.density = density;
  3.   fixtureDef.friction = friction;
  4.   fixtureDef.restitution = restitution;
  5. }

これで、動的な剛体に弾力などの質感が加わった。落ちたオブジェクトは床で軽く何度か弾んだ後、やがて止まる(図007)。できあがったJavaScriptコード全体は、以下のコード005のとおりだ。

図007■然落下したオブジェクトが床で弾む
図007

コード005■動的な剛体を静的な剛体の上に落下させる物理シミュレーション
  1. var stage;
  2. var SCALE = 1 / 30;
  3. var world;
  4. var gravityVertical = 10;
  5. var velocityIterations = 8;
  6. var positionIterations = 3;
  7. var time = 1 / 24;
  8. var boxEdge = 20;
  9. var density = 1;
  10. var friction = 0.1;
  11. var restitution = 0.5;
  12. function initialize() {
  13.   var canvasElement = document.getElementById("myCanvas");
  14.   stage = new Stage(canvasElement);
  15.   initializeBox2D(canvasElement.width, canvasElement.height);
  16.   Ticker.setInterval(time * 1000);
  17.   Ticker.addEventListener("tick", update);
  18. }
  19. function initializeBox2D(stageWidth, stageHeight) {
  20.   var b2Vec2 = Box2D.Common.Math.b2Vec2;
  21.   var b2World = Box2D.Dynamics.b2World;
  22.   world = new b2World(new b2Vec2(0, gravityVertical), true);
  23.   var centerX = stageWidth / 2;
  24.   var floorWidth = stageWidth * 0.8;
  25.   var floorHeight = boxEdge;
  26.   var floorShape = createStaticFloor(centerX, stageHeight - floorHeight, floorWidth, floorHeight, "#CCCCCC");
  27.   stage.addChild(floorShape);
  28.   var myShape = createDynamicBox(centerX, boxEdge, boxEdge, boxEdge, "#0000FF");
  29.   stage.addChild(myShape);
  30. }
  31. function createStaticFloor(nX, nY, nWidth, nHeight, nColor) {
  32.   var b2Body = Box2D.Dynamics.b2Body;
  33.   var bodyDef = defineBody(nX, nY, b2Body.b2_staticBody);
  34.   var myShape = createShape(bodyDef, nWidth, nHeight, nColor);
  35.   var boxShape = createBoxShape(nWidth / 2, nHeight / 2);
  36.   var fixtureDef = defineFixture(boxShape);
  37.   createBody(world, bodyDef, fixtureDef);
  38.   return myShape;
  39. }
  40. function createDynamicBox(nX, nY, nWidth, nHeight, nColor) {
  41.   var b2Body = Box2D.Dynamics.b2Body;
  42.   var bodyDef = defineBody(nX, nY, b2Body.b2_dynamicBody);
  43.   var myShape = createShape(bodyDef, nWidth, nHeight, nColor);
  44.   var boxShape = createBoxShape(nWidth / 2, nHeight / 2);
  45.   var fixtureDef = defineFixture(boxShape);
  46.   setFixtureDef(fixtureDef, density, friction, restitution);
  47.   createBody(world, bodyDef, fixtureDef);
  48.   return myShape;
  49. }
  50. function defineBody(nX , nY, nType) {
  51.   var b2BodyDef = Box2D.Dynamics.b2BodyDef;
  52.   var bodyDef = new b2BodyDef();
  53.   bodyDef.position.Set(nX * SCALE, nY * SCALE);
  54.   bodyDef.type = nType;
  55.   return bodyDef;
  56. }
  57. function createShape(bodyDef, nWidth, nHeight, nColor) {
  58.   var myShape = new Shape();
  59.   draw(myShape.graphics, nWidth, nHeight, nColor);
  60.   myShape.regX = nWidth / 2;
  61.   myShape.regY = nHeight / 2;
  62.   bodyDef.userData = myShape;
  63.   return myShape;
  64. }
  65. function draw(myGraphics, nWidth, nHeight, nColor) {
  66.   myGraphics.beginFill(nColor);
  67.   myGraphics.drawRect(0, 0, nWidth, nHeight);
  68. }
  69. function defineFixture(myShape) {
  70.   var b2FixtureDef = Box2D.Dynamics.b2FixtureDef;
  71.   var fixtureDef = new b2FixtureDef();
  72.   fixtureDef.shape = myShape;
  73.   return fixtureDef;
  74. }
  75. function createBoxShape(nX, nY) {
  76.   var b2PolygonShape = Box2D.Collision.Shapes.b2PolygonShape;
  77.   var myPolygonShape = new b2PolygonShape();
  78.   myPolygonShape.SetAsBox(nX * SCALE, nY * SCALE);
  79.   return myPolygonShape;
  80. }
  81. function setFixtureDef(fixtureDef, density, friction, restitution) {
  82.   fixtureDef.density = density;
  83.   fixtureDef.friction = friction;
  84.   fixtureDef.restitution = restitution;
  85. }
  86. function createBody(world, bodyDef, fixtureDef) {
  87.   var body = world.CreateBody(bodyDef);
  88.   body.CreateFixture(fixtureDef);
  89. }
  90. function update() {
  91.   world.Step(time, velocityIterations, positionIterations);
  92.   var body = world.GetBodyList();
  93.   while (body) {
  94.     var myObject = body.GetUserData();
  95.     if (myObject) {
  96.       var position = body.GetPosition();
  97.       myObject.x = position.x / SCALE;
  98.       myObject.y = position.y / SCALE;
  99.       myObject.rotation = body.GetAngle() / Matrix2D.DEG_TO_RAD;
  100.     }
  101.     body = body.GetNext();
  102.   }
  103.   stage.update();
  104. }

Appendix おまけ

3つほどリンクを加えておく。



作成者: 野中文雄
更新日: 2013年5月22日 ラジアン角を度数値に換算する処理が漏れていたので追加。jsdo.itのコードを掲載。
作成日: 2013年5月13日


Copyright © 2001-2013 Fumio Nonaka.  All rights reserved.