じゅころぐAR/VR

AR/VRメインのブログ。時々ドローン。

A-FrameでPhysicsを扱う

前回に引き続き、A-Frameで簡単なVRアプリを実装してみます。
今回は、aframe-physics-systemによる物理演算のサンプルです。

動作デモ

先に何ができるかイメージした方がよさそうなので、デモ動画を載せておきます。
前回同様にgdgd妖精sMMDを使用したので、サーバ公開はなしで動画のみです。

model: シルシル、コロコロ、ピクピク
copyright: (c) gdgd妖精s (ぐだぐだフェアリーーーズ), 2代目gdgd妖精s

カメラの向いている方向にボールを飛ばし、的にぶつけて落としています。また、衝突を検知してパーティクルによるエフェクトの実装も試しました。

aframe-physics-systemの利用

A-Frameで物理演算を行うには、aframe-physics-systemを使うのが簡単です。
aframe-physics-systemを単体で利用してもよいですが、aframe-extrasにインクルードされているため、他の機能を利用することも考えて、aframe-extrasを使った方が便利です。

HTML内で"aframe-extras.min.js"をリンクすれば物理演算を含む拡張機能が使えるようになります。

<script src="https://cdn.rawgit.com/donmccurdy/aframe-extras/v3.12.4/dist/aframe-extras.min.js"></script>

今回はサンプルなのでCDNからリンクしました。もちろんローカルコピーでもOKです。
規模が大きくなったらnpmで管理した方がいいかなと思いますが、まあそのうち。

シーンの設定

物理演算を有効にするには、<a-scene>タグにphysics属性を付けます。

 <a-scene physics="debug: false;">

プロパティでは重力、反発係数、摩擦係数など指定できます。
※シーン全体の設定であることに注意

debugプロパティをtrueにすると、physicsの当たり判定(collider)がワイヤーフレームで表示されます。 f:id:jyuko49:20171126210708j:plain

static-body、dynamic-bodyの指定

物理演算を行うentityに、static-bodydynamic-bodyのいずれかの属性を付与します。
どちらも付与していないentityは他の物体に関与せず、衝突せずに通り抜けます。

static-bodyを付与すると位置は固定され、衝突した物体に影響を与えます。床や壁などに指定します。

<a-entity id="board"
  geometry="primitive: box; depth: 10; height: 0.5; width: 40" material="color: #deb887"
  static-body
  position="0 0 -20">
</a-entity>

dynamic-bodyを付与すると、重力や外部からの力を受けて反射・摩擦を行います。ボールを飛ばす場合や動かせる障害物などに指定します。
dynamic-bodyの場合は、プロパティでcolliderの形状(shape)や物体の質量(mass)が指定できます。

<a-entity id="コロコロ"
  mmd-model="model: コロコロ/コロコロ.pmx;"
  dynamic-body="shape: box; mass: 10"
  position="0 5 -20" scale="0.5 0.5 0.5">
</a-entity>

shapeのデフォルト値はautoになっており、基本的なオブジェクトであれば適切な形状を設定してくれます。
ただし、各種3Dフォーマットからロードした複雑な形状のオブジェクトの場合、autoだとcolliderが設定されないことがあり、boxcylindersphereのいずれかが無難です。
この辺りは、シーンの設定でdebugを有効にしながら設定するとわかりやすいです。

ボールを投げる

シューティングゲームやボールを投げてモンスターを捕まえるゲーム用に、ボールを投げる処理を実装してみます。

dynamic-body属性を付与された要素は、自動的にvelocity属性を持ちます。このvelocity属性に飛ばしたい方向の速度ベクトルを設定してあげれば、後は物理演算ライブラリがよしなにしてくれます。
処理はJavaScriptになりますが、HTML要素の操作になるのでgetAttribute()setAttribute()appendChild()など、Web制作で見慣れたコードになります。

サンプルコードにコメントを入れたので参考にしてください。

var _shot = function () {
  // カメラの位置と向きを取得する
  var camera = document.getElementById('camera');
  var rotation = camera.getAttribute('rotation');
  var position = camera.getAttribute('position');

  // カメラ前方方向に速さを掛けて、速度ベクトルを求める
  var velocity = new THREE.Vector3();
  var speed = 50;
  velocity.x = Math.sin(rotation.y * Math.PI / 180) * Math.cos(rotation.x * Math.PI / 180) * -speed;
  velocity.y = Math.sin(rotation.x * Math.PI / 180) * speed;
  velocity.z = Math.cos(rotation.y * Math.PI / 180) * Math.cos(rotation.x * Math.PI / 180) * -speed;

  // <a-entity>を新規作成して速度を与える
  var ball = document.createElement('a-entity');
  ball.setAttribute('id', 'ball');
  ball.setAttribute('velocity', velocity);
  ball.setAttribute('position', position);
  ball.setAttribute('geometry', 'primitive: sphere; radius: 0.5');
  ball.setAttribute('dynamic-body', 'mass: 1');

  // <a-scene>に追加する
  var scene = document.querySelector('a-scene');
  scene.appendChild(ball);
};

// keydownイベントでボールを発射する処理を呼び出す
document.addEventListener('keydown', _shot);

dynamic-bodyが付与されていれば、同じ要領でボール以外も飛ばせます。

衝突時に処理を行う

物体が他の物体と衝突した際に、対象オブジェクトを消したり、スコアを加算したり、アニメーションを起こしたりと何らかの処理を行いたいケースは多いです。

static-bodyまたはdynamic-bodyを設定したentityにcollideイベントを登録することで、他の物体と衝突した際の処理を記述できます。

サンプルとして、パーティクルを使ったエフェクトを実装してみました。

var _init = function () {
  // collideイベントを登録したい要素を取得する
  var targetElement = document.getElementById('コロコロ');

  // collideイベントを登録する
  targetElement.addEventListener('collide', function (e) {
    // ボール(id="ball")と衝突した場合のみ処理を行う
    if (e.detail.body.el.id === 'ball'){
      // 衝突したボールと同じ位置にパーティクルを発生させる
      var effect = document.createElement('a-entity');
      var position = e.detail.body.el.getAttribute('position');
      effect.setAttribute('position', position);
      effect.setAttribute('particle-system', 'color: #EF0000,#44CC00; particleCount: 500; duration: 0.2;');

      // パーティクルを<a-scene>に追加する
      var scene = document.querySelector('a-scene');
      scene.appendChild(effect);
    };
  });
};

// DOMのloadが終わってから処理する
document.addEventListener('DOMContentLoaded', _init);

サンプルでは衝突したオブジェクトの情報をe.detail.body.elから取得しています。同様に、衝突されたオブジェクトや衝突地点の情報も取得できます。
※詳細はこちらを参照

エフェクトには、aframe-particle-systemを使っています。
こちらは、aframe-extrasに含まれないので別途リンクしてください。(下記はCDNの例)

<script src="https://unpkg.com/aframe-particle-system-component@1.0.x/dist/aframe-particle-system-component.min.js"></script>

ソースコード(サンプル)

冒頭の動画のうち、物理演算を使った処理は粗方説明したので、ソースコード全文を載せておきます。
MMDモデルの取得のみコピペでは動かないので、手持ちのモデルデータに置き換えて使ってください。

A-Frameだと100行に満たないHTMLで色々なサンプルが作れちゃうので、Gistとの相性がよいですね。

まとめ

aframe-physics-systemを使った物理演算を行ってみました。

Unityなどのゲームエンジンに比べると機能がシンプルで制約もありますが、基本的な処理は実装できます。

A-Frameの特徴に従い、HTML要素やイベントリスナの操作になるので、Webフロントエンドの基礎レベルの知識があれば実装できてしまう点が一番のメリットかなと思います。