HTML5のCanvasでつくるダイナミックな表現―CreateJSを使う

第28回 物理演算エンジンBox2Dでボールを落とす

この記事を読むのに必要な時間:およそ 7.5 分

剛体をつくって落とす ー 物理演算シミュレーションの実行

ようやく,物理演算シミュレーションに取りかかれる。そのためには,剛体の定義から剛体をつくらなければならない。そのうえで,時間を進めてシミュレーションし,物理演算の結果を目に見えるオブジェクトに当てはめる。

【物理演算シミュレーションの準備】
  1. 剛体の定義から剛体をつくる
  2. 時間を進めてシミュレーションする

第4の仕事となる剛体づくりは,第1で定めた物理空間b2Worldオブジェクトのメソッドb2World.CreateBody()で行う。引数に渡すのが剛体を定義したb2BodyDefオブジェクトだ。

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

b2World.CreateBody()メソッドは,新たに定める関数(createBody())から呼び出す。そして,この関数の呼出しは,落下するボールをつくる関数(createDynamicBall())に加えた。引数にはb2Worldとb2BodyDefのオブジェクト(worldとbodyDef)を渡す。これで,このボールをつくる関数は,すでに定めてあった剛体の定義と目に見えるボールの生成に加えて,ボールの剛体をつくるまで一手に担うことになった。

function createDynamicBall(nX, nY, radius) {

  createBody(world, bodyDef);

}

function createBody(world, bodyDef) {
  var body = world.CreateBody(bodyDef);
}

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

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

人形であるボールのBitmapオブジェクトは,Ticker.tickイベントのリスナー(tick())から呼出す新たな関数(update())で動かす。この関数がb2World.Step()メソッドを呼び出している。物理シミュレーションで進める秒数は,Ticker.tickイベントリスナーが受け取るイベントオブジェクトのdeltaプロパティから得て,関数の引数(delta)に渡した。単位はミリ秒なので,秒数に直してb2World.Step()メソッドの第1引数に渡している。

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

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

var velocityIterations = 8;
var positionIterations = 3;

function tick(eventObject) {
  var delta = eventObject.delta;
  update(delta);

}

function update(delta) {
  world.Step(delta / 1000, velocityIterations, positionIterations);
  var body = world.GetBodyList();
  var myObject = body.GetUserData();
  if (myObject) {
    var position = body.GetPosition();
    myObject.x = position.x / SCALE;
    myObject.y = position.y / SCALE;
    myObject.rotation = body.GetAngle() / createjs.Matrix2D.DEG_TO_RAD;
  }
}

以上の5つの手順がBox2Dを使う最小限の仕込みだ。これで,物理シミュレーションの人形劇は演じられる。以下のコード1が,これまでのJavaScriptをまとめたものだ。さて,見るといささかがっかりするかもしれない。ステージ上端の真ん中からボールが落ちてきて下端に消えて終わる図5⁠。でも,重力にしたがう剛体を宙に置いたのだから,これが物理演算の結果であることに間違いはない。

図5 ステージ上端から表れたボールが落ちて下端に消える

図5 ステージ上端から表れたボールが落ちて下端に消える

コード1 Box2Dでボールを自由落下させる

var SCALE = 1 / 30;
var stage;
var world;
var gravityVertical = 15;
var velocityIterations = 8;
var positionIterations = 3;
var stageWidth;
var stageHeight;
var ballImage;
var imageRadius;
function initialize() {
  var canvasElement = document.getElementById("myCanvas");
  var gravity = new Box2D.Common.Math.b2Vec2(0, gravityVertical);
  stage = new createjs.Stage(canvasElement);
  stageWidth = canvasElement.width;
  stageHeight = canvasElement.height;
  initializeBox2D(gravity);
  createjs.Ticker.timingMode = createjs.Ticker.RAF;
  preloadImage("images/Pen.png");
}
function initializeBox2D(gravity) {
  world = new Box2D.Dynamics.b2World(gravity, true);
}
function tick(eventObject) {
  var delta = eventObject.delta;
  update(delta);
  stage.update();
}
function addBall() {
  var ball = createDynamicBall(stageWidth / 2, -imageRadius, imageRadius);
  stage.addChild(ball);
}
function createDynamicBall(nX, nY, radius) {
  var dynamicBody = Box2D.Dynamics.b2Body.b2_dynamicBody;
  var bodyDef = defineBody(nX, nY, dynamicBody);
  var ball = createVisualBall(radius, bodyDef);

  createBody(world, bodyDef);
  return ball;
}
function defineBody(nX , nY, bodyType) {
  var bodyDef = new Box2D.Dynamics.b2BodyDef();
  bodyDef.position.Set(nX * SCALE, nY * SCALE);
  bodyDef.type = bodyType;
  return bodyDef;
}
function createBody(world, bodyDef) {
  var body = world.CreateBody(bodyDef);
}
function update(delta) {
  world.Step(delta / 1000, velocityIterations, positionIterations);
  var body = world.GetBodyList();
  var myObject = body.GetUserData();
  if (myObject) {
    var position = body.GetPosition();
    myObject.x = position.x / SCALE;
    myObject.y = position.y / SCALE;
    myObject.rotation = body.GetAngle() / createjs.Matrix2D.DEG_TO_RAD;
  }
}
function createVisualBall(radius, bodyDef) {
  var ball = new createjs.Bitmap(ballImage);
  ball.regX = ballImage.width / 2;
  ball.regY = ballImage.height / 2;
  ball.scaleX = ball.scaleY = radius / imageRadius;
  bodyDef.userData = ball;
  return ball;
}
function preloadImage(file) {
  var loader = new createjs.LoadQueue(false);
  loader.addEventListener("fileload", loadFinished);
  loader.loadFile(file);
}
function loadFinished(eventObject) {
  ballImage = eventObject.result;
  imageRadius = ballImage.width / 2;
  createjs.Ticker.addEventListener("tick", tick);
  addBall();
}

結果がつまらないのは,オブジェクトがひとつしかないからだ。複数の剛体が互いにぶつかり合えば,跳ね返って動きも変わる。ただ,残念ながら今回はここまでとする。すでに剛体をひとつつくって動かした。これを増やすことはさほど難しくない。これまでの長い道のりは次回に活かされるので,ご期待いただきたい。なお,つまらないサンプルとはいえ,ご参考までにjsdo.itにもコード1を掲げた。

※4

なぜ再計算が要るのか。物理法則にしたがった演算結果をただ当てはめると,剛体同士がめり込んでしまうこともある。そこで,重ならないような位置を計算し直す。すると,剛体の移動距離と方向(ベクトル)が変わるので,動きが不自然になるかもしれない。そのため,速度も再計算するのだ。

「Box2D v2.2.0 User Manual」2.4Simulating the World ⁠of Box2D⁠」によると再計算の回数は,速度が8,位置は3が目安としているので,それにしたがった。

著者プロフィール

野中文雄(のなかふみお)

ソフトウェアトレーナー,テクニカルライター,オーサリングエンジニア。上智大学法学部卒,慶応義塾大学大学院経営管理研究科修士課程修了(MBA)。独立系パソコン販売会社で,総務・人事,企画,外資系企業担当営業などに携わる。その後,マルチメディアコンテンツ制作会社に転職。ソフトウェアトレーニング,コンテンツ制作などの業務を担当する。2001年11月に独立。Web制作者に向けた情報発信プロジェクトF-siteにも参加する。株式会社ロクナナ取締役(非常勤)。

URLhttp://www.FumioNonaka.com/

著書