こんにちは。
最近の推し漫画はジャンプで連載中の鬼滅の刃なjujunjun110です。基本的に展開はシリアスでよくできているし、その中に突如ぶっこまれるシュールギャグがまた良い。まだ4巻しか出ていないのでぜひ。
さて、先日VRをやっている方を集めた飲み会を開催しまして、参加者の皆様から事前に集めた話題を選ぶために、勉強も兼ねてWebVRフレームワークA-Frameで簡単なサイコロWebアプリを作成したので、その技術について書いてみます。
↑こんな感じで、おはようからおやすみまで暮らしを見つめてくれそうなテイストのアプリケーションです。
目次
物理演算コンポーネントaframe-physics-system
まず、A-Frameの基本機能にはまだ物理演算の機能がないので、外部コンポーネントを利用する必要があります。
今回利用するaframe-physics-systemは、javascriptの物理計算エンジンである CANON.js のA-Frame向けラッパーです。*1
A-FrameはTHREE.jsの上に構築されていますが、もともとTHREE.jsとCANNON.jsの相性がいいのでうまく統合できているということのようです。
インストール
まずはnpmでインストールします。
npm install aframe-physics-system
browserifyなどでjsをまとめる際はただrequireするだけではなく、以下のようにregisterAll()というメソッドを叩く必要があるので注意しましょう。
require('aframe'); require('html2canvas'); require('aframe-html-shader'); var physics = require('aframe-physics-system'); physics.registerAll();
基本的な使い方
使い方は非常にシンプル。
sceneにphysics
コンポーネントを物理演算の対象にしたいオブジェクトにstatic-body
コンポーネントもしくはdynamic-body
コンポーネントを付加して使います。
static-body
... 物理演算の対象になるが、それ自身は移動しないオブジェクト。壁や床など。dynamic-body
... 物理演算の対象になり、それ自身が移動するオブジェクト。今回の場合はサイコロ。
ただ立方体を真下に落とすだけのシンプルな実装はこんな感じ。
<a-scene physics="debug: false"> <!-- 物理演算用サイコロオブジェクト --> <a-entity dynamic-body="mass:100;linearDamping: 0.01;angularDamping: 0.0001;" geometry="primitive: box" position="0 10 -10" ></a-entity> <!-- 床 --> <a-entity static-body geometry="primitive: box; width: 300; depth: 300; height: 1" position="0 0 0" material="src:floor.jpg; repeat: 20 20;"></a-entity> <a-scene>
飛び跳ねてますね。物理演算できてますね。
physics
やdynamic-body
のパラメータで重力、反発係数、回転しやすさなどを決めることができます。アプリケーションが完成してきたら微調整してみましょう。
サイコロをころがす
次にさいころに力を与えて投げられるようにしましょう。
function throwSai() { var sai = document.querySelector('#sai-physics').body; // サイコロが力の影響を(再び)受けるように設定する sai.wakeUp(); // サイコロを初期ポジションにセット sai.position = new CANNON.Vec3(0, 10, -10); // サイコロ全体にZ方向の力を与える sai.velocity.set(0, 0, -4); // サイコロにランダムな回転を与えて面白い転がり方にする sai.applyImpulse( new CANNON.Vec3(Math.random() * 20, Math.random() * 20, Math.random() * 20), // 与えるベクトル new CANNON.Vec3(0.5, Math.random() * 10, Math.random() * 10) // 力を与える点 ); }
ここでは、CANNON.js のメソッドであるvelocity.set(x, y, z)
やapplyImpulse(impulse, world_position)
などを直接叩いて力を与えています。
aframe-physics-system
はCANNON.jsの全てのメソッドをラッピングしているわけではないので、より高度なことをやりたくなったら、el.body
という名前で取得できるCANNON.jsオブジェクトに対して、CANNON.jsのメソッドを直接叩くことになります。
サイコロの面を追加する
立方体を投げることができるようになったら、各面にテキストを表示していきましょう。
日本語を表示するために、a-box
の子要素としてa-plane
を追加し、それぞれにHTMLシェーダーを当てはめていきます。
↓ 詳細は以下の記事が詳しいのでこちらもご覧ください。
えいっ!
すり抜けました...😱
おそらくですが、aframe-physics-system
が物理演算をするためにA-Frameオブジェクトのメッシュを利用する際、オブジェクトに子要素があると正しく単一のメッシュが取得できず、結果的に衝突判定などがうまく行われないのでないかと思います。
これはかなり困ったんですが、今回は物理演算用のサイコロと表示用のサイコロを分けることでなんとか解決できました。
子要素があるオブジェクトに物理演算が効かないなら子要素がないオブジェクトで物理演算を行い、子要素ありのオブジェクトに回転と座標をコピーしつづければ擬似的に物理演算できるという発想ですね。
(ちなみに書いてて思ったのですが、明示的にmeshの形状を指定してあげればこんな面倒なことしなくてもうまく動くかもしれないです。)
<!-- 物理演算用サイコロ --> <a-entity id="sai-physics" pos-tracer="target: sai-display" dynamic-body="mass:100;linearDamping: 0.01;angularDamping: 0.0001;" geometry="primitive: box" position="0 10 -10" material="opacity:0;" ></a-entity> <!-- 表示用サイコロ --> <a-entity id="sai-display" geometry="primitive: box" position="0 10 -3"> <a-entity geometry="primitive: plane" material="shader:html; target: #target1; fps:1" position=" 0 0.5 0" rotation="270 0 0"></a-entity> <a-entity geometry="primitive: plane" material="shader:html; target: #target2; fps:1" position=" 0 -0.5 0" rotation=" 90 0 0"></a-entity> <a-entity geometry="primitive: plane" material="shader:html; target: #target3; fps:1" position=" 0 0 -0.5" rotation=" 0 180 0"></a-entity> <a-entity geometry="primitive: plane" material="shader:html; target: #target4; fps:1" position=" 0 0 0.5" rotation=" 0 0 0"></a-entity> <a-entity geometry="primitive: plane" material="shader:html; target: #target5; fps:1" position="-0.5 0 0" rotation=" 0 270 0"></a-entity> <a-entity geometry="primitive: plane" material="shader:html; target: #target6; fps:1" position=" 0.5 0 0" rotation=" 0 90 0"></a-entity> </a-entity>
AFRAME.registerComponent('pos-tracer', { init: function() { this.targetEl = document.getElementById(this.data.target); }, tick: function() { // ラジアンから度数への変換係数 var radToDeg = 180 / Math.PI; var rot = AFRAME.utils.coordinates.stringify({ x: this.el.object3D.rotation._x * radToDeg, y: this.el.object3D.rotation._y * radToDeg, z: this.el.object3D.rotation._z * radToDeg }); this.targetEl.setAttribute("position", this.el.object3D.position); this.targetEl.setAttribute("rotation", rot); } });
今回はpos-tracer
というコンポーネントを書きました。コンポーネントのtick
イベントは毎フレーム発火するので、ここで座標と回転をコピーしています。
いい感じに動きました!
あとは、表示するキーワードをランダムにしたり、もう一回サイコロを振れるボタンをつけたり、カメラをズームしたりできるようにしたら完成です!
(↑ボタンを押したりぐりぐりしたりできます)
まとめとお知らせ
というわけで、A-Frameの物理演算の記事を書いてみました。
A-Frame、基本機能に取り込んでもいいような機能であっても外部コンポーネントを使わないと実現できないようなことが多くあります。
おそらく公式としては、各種コンポーネントを公式機能としてサポートすることよりも、便利な機能を外部ディベロッパーが追加・利用しやすいエコシステムを作る方に労力を割き、便利な機能自体は外部ディベロッパーにどんどん開発していってもらおうという思想なのだと思います。
A-Frame 0.4.0から追加されたA-Frame-Registry もまさにそんな感じですね。
一方で、そのミニマルな作りのため、A-Frameの公式サイトを見ただけではUnity等と比較してできないことが多いと思われがちな側面もあるような気がしています。(A-Frameを触り始めたばかりのときの僕がそうでした。)
これは結構かなりもったいないと思うので、今週末開催されるVR Tech Tokyo #5 というイベントで、「ここまできた!2017年 Web VRでできること・できないこと」というタイトルでLTをしてきます!(現在資料の進捗ゼロ😱)
その際の模様はまたこのブログに書きたいと思いますので、お楽しみに!
次回はVR合コンの開催をもくろむDayBySayが、それを支える技術について書く予定です!
それでは皆様、ごきげんよう。
*1:A-Frame開発者のKevin氏が作ったaframe-physics-componentというのもありますが、そちらは若干物理演算の挙動が怪しいのでこちらのほうがオススメです。