これは
three.jsからWebXRを扱う上で、tracked-pointer入力をどう処理したら良いかのメモ。全てballshooterのサンプルコードに書かれてる事だけど、特にクラスについての解説がないので、書き換えて挙動を確認しながらじゃないと、なかなか何をやってるのか掴めなかったため、メモとして残します。
デバイスの表示
コントローラ本体(グリップ)と、レーザー状のポインタ、2種類の表示を行う。
グリップ
//examples/jsm.webxr/XRControllerModelFactory.js
を使えば、ほぼ全てを自動的に処理できる。
import { XRControllerModelFactory } from
'./node_modules/three/examples/jsm/webxr/XRControllerModelFactory.js';
const scene = new THREE.Scene();
// その他、必要な初期化処理
const factory = new XRControllerModelFactory();
const grip = renderer.xr.getControllerGrip(0);
grip.add(factory.createControllerModel(grip));
scene.add(grip);
コントローラ周りの初期化はXRモードに入っていない状態でも大丈夫。接続されたら自動的にプロファイルを識別して適切なモデルを取得し表示してくれる。2つのデバイスを扱いたい場合には0番だけでなく1番のグリップに対しても同じ処理をしてあげればOK。
ポインタ
こちらに関してはコントローラ接続時だけTHREE.Line
でポインタを描画します。コントローラにイベントハンドラを追加し、接続されたらモデルを追加、切断されたら削除。コントローラ自体はグリップ同様にシーンに貼り付けたままにしておく事で描画ループ内でシーンと一緒に描画します。
const controller = renderer.xr.getController(0);
controller.addEventListener('connected', e => {
// 頂点情報の作成
const geometry = new THREE.BufferGeometry();
geometry.setAttribute(
'position',
// 3次元の頂点座標、始点(0,0,0)と終点(0,0,-1)の2点で初期化
new THREE.Float32BufferAttribute([0, 0, 0, 0, 0, -1], 3));
geometry.setAttribute(
'color',
// 頂点の色指定、始点(R:0.5,G:0.5,B:0.5)と終点(R:0,G:0,B:0)
// 線分を描画する際には補完されて灰色から黒へのグラデーションになる
new THREE.Float32BufferAttribute([0.5, 0.5, 0.5, 0, 0, 0], 3));
// THREE.Lineに頂点を渡して線分のモデルを作る
controller.add(new THREE.Line(
geometry,
new THREE.LineBasicMaterial({
// 頂点の色を補完して線分の色を決定する
vertexColors: true,
// 加算合成するので黒は無色となり自然なポインタが描画できる
blending: THREE.AdditiveBlending })
));
});
controller.addEventListener('disconnected', e => {
// connected時に追加したものを削除。XRモードに出入りするために必要
controller.remove(controller.children[0]);
});
scene.add(controller)
イベントに関しては他にselectstart
等、トリガーボタンのプレス・リリースに対応したイベントも取れる。他のボタン情報に関してはTHREE.WebGLRenderer.xr.getSession()
でWebXR API
のXRSession
オブジェクトを経由して、XRSession.inputSources[0].gamepad
に公開されているGamepadインタフェースを経由すれば良い。これは生でWebXRを触る場合と同じ。詳しくはWeb XR Gamepads Module - Level 1あたりを参照。ちなみにXRSession.inputSources
はセッション開始直後には何も含まれていません。このあたりはWeb MIDIとかと同じで、まずは空の配列でインタフェースが作成され、その後非同期的に発生するデバイス追加イベント追加されます。なので、初期化時に参照を保持するとかはできなくて、描画ループ内で見に行って期待したインデックス値のデバイスが存在してたら値を取る、といった処理にするのが良いです。
ポインタの衝突判定
ポインタで何かを指し示す以上、その向きに従って判定が必要な事がほとんど。このあたりの情報はMDNのXRInputSourceの項目に少し書かれてるくらいしか情報がないためソースを少し読み解く。サンプルだとポインタの方向にランダムにボールを吐き出すって処理になっていて、そのものずばりな方向の計算はなかったりするので、基本的な事がわかってないと混乱するかも。
ポインタの向きの取得
直接取得する方法がわからなかったんだけど、サンプルコードの方法だとコントローラのクォータニオンを使って単位ベクトルを回して取得するのが良いようです。
const renderer = new THREE.WebGLRenderer({...});
// その他、所々の初期化
renderer.setAnimationLoop(() => {
// ポインタのモデルと同様にZ方向奥に向かった単位ベクトルを基準にする
const v = new THREE.Vector3(0, 0, -1);
const controller = renderer.xr.getController(0);
// コントローラのクォータニオンを適用するとポインタの向きに回転する
v.applyQuaternion(controller.quaternion);
// 以下、ポインタ情報を使った処理
});
ポインタと壁の衝突判定
ポインタの起点はコントローラの位置が使えるので、位置と向きを使って、例えばZ=-10に位置する壁をポイントした際のXY座標は簡単な方程式で求めることができる。
// 向きを取得するコードの続き
// - ポインタ上の座標 = 起点位置 + 向きベクトル x D ... (1)
// (1)をZ座標に適用してZ=-10の時のDを求める
// - 起点Z + 向きZ x D = -10
const d = (-10 - controller.position.z) / v.z;
const x = controller.position.x + v.x * d;
const y = controller.position.y + v.y * d;
こんな感じで壁上の座標は計算できる。特定の点との距離なら、点からポインタの軌跡に対して垂線を降ろして距離を求めればOK。このあたりは3次元の衝突判定として他の分野でも色々と資料があると思うのでバリエーションは割愛。
WebXR APIを生で触る場合
XRFrame.getPose
にXRInputSource.targetRaySpace
を渡すことでXRPose
が取得できる。XRPose.transform.matrix
がthree.jsで見ていたクォータニオンだと思うんだけど、確認はしていない。次回調査する事があった場合の導入ヒントとしてメモを残す。
まとめ
わりと少ない数十行程度のコードで、コントローラのモデルを表示してレーザー出して……って処理ができちゃうので便利。WebXRさいこー。