WebXR AR Paint その2

はじめに
こんにちは。ここのところ業務でThree.jsを触れておらず、この先もしばらくは無理そうなので、こうなったらプライベートでThree.js充するしかないと画策しているあんどうです。
WebXR AR Paint その1ではThree.js r111で新しく追加されたサンプルWebXR AR Paintを試してみるための準備と使い方を簡単に紹介しました。その続編となる今回は遂にサンプルのソースコードを見ていきます。いわゆる「ただツールやライブラリの使い方を紹介するだけ」のエントリです。Advent Calendarはこんなんでええんですわ。
HTML
必要なファイル
まずはエントリポイントとなるHTMLを確認してみましょう。対象のファイルはexamples/webxr_ar_paint.htmlです。
examples/webxr_ar_paint.html
import * as THREE from '../build/three.module.js';
import { TubePainter } from './jsm/misc/TubePainter.js';
import { ARButton } from './jsm/webxr/ARButton.js';
WebXR AR Paintの主な処理はTubePainter.jsとARButton.jsに納められています。TubePainter.jsは今回のサンプル専用のファイルで、画面をなぞったときに表示される白線を作成する処理が記述されています。一方、ARButton.jsにはWebXR Device APIを使用して没入型のARセッションを取得するためのボタンを作成する処理が記述されています。現時点ではこのARButton.jsは今回のサンプル専用ですが、おいおい登場するであろうAR機能を使用した別のサンプルでも使用するつもりのようです。実際、ARButton.jsと同じディレクトリにあるVRButton.jsはいくつかのサンプルで共有されています。
描画
上記2つのファイルに関わる部分以外で主にXRに関係するのはTHREE.WebGLRendererです。
examples/webxr_ar_paint.html
renderer = new THREE.WebGLRenderer( { antialias: true, alpha: true } );
renderer.setPixelRatio( window.devicePixelRatio );
renderer.setSize( window.innerWidth, window.innerHeight );
renderer.xr.enabled = true;
container.appendChild( renderer.domElement );
まずコンストラクタのオプションにalpha:trueを指定しなければいけません。WebXR AR Paintではカメラ映像の上にオーバーレイする形でWebGLの描画を行うので当然でしょう。次にrenderer.xr.enabledの値をtrueに設定します。renderer.xrプロパティはWebXRManagerオブジェクトを保持していて、このenabledプロパティをtrueに設定することで、描画時に使用するカメラがWebXRManagerオブジェクトの持つステレオ表示可能なXR用カメラと置き換えられます。
renderers/WebGLRenderer.js
if ( xr.enabled && xr.isPresenting() ) {
camera = xr.getCamera( camera );
}
操作
XR表示に関してはrenderer.xrのgetCamera()が利用できました。入力についてもrenderer.xrを利用しますが、使用するメソッドはgetController()です。
examples/webxr_ar_paint.html
controller = renderer.xr.getController( 0 );
controller.addEventListener( 'selectstart', onSelectStart );
controller.addEventListener( 'selectend', onSelectEnd );
controller.userData.points = [ new THREE.Vector3(), new THREE.Vector3() ];
controller.userData.matrices = [ new THREE.Matrix4(), new THREE.Matrix4() ];
controller.userData.skipFrames = 0;
scene.add( controller ); // これは不要な気がする
function onSelectStart() {
// ここでのthisはcontrollerオブジェクト
this.userData.isSelecting = true;
this.userData.skipFrames = 2;
}
function onSelectEnd() {
// ここでのthisはcontrollerオブジェクト
this.userData.isSelecting = false;
}
getController()メソッドの返すコントローラーは不可視なTRHEE.Groupオブジェクトです。このオブジェクトの目的は、XRセッションのプロパティにアクセスしたり、そこで発生したイベント(ユーザー操作など)をプロキシすることです。今回のアプリでは後ほどcontroller.matrixWorldを使用してデバイスの姿勢にアクセスします。またこのコントローラーにイベントハンドラを追加すると、XRセッションで発生したイベントを処理できるため、selectstartイベントとselectendイベントをリスンして、ユーザーが画面をタップ中かどうかをcontroller.userData.isSelectingで管理できるようにします。
なお、ここで使用しているuserDataプロパティはthree.jsが用意している、任意の情報を保持するためのプロパティです。userDataの持つその他のpointsやmatrices、skipFramesなどのプロパティについては後ほど説明します。
ARButton
ではTubePainterとARButtonの使い方の説明に移ります。といってもHTMLファイル内でのARButtonの扱いについて特に説明することはありません。
examples/webxr_ar_paint.html
document.body.appendChild( ARButton.createButton( renderer ) );
createButton()メソッドにrendererを渡して「START AR」ボタンを作成し、<body>要素に追加します。createButton()メソッドの内部については後ほど説明します。
TubePainter
WebXR AR Painterの主な機能はこのTubePainterで定義されています。まずは空間上の線に対応するメッシュをシーンに追加します。
examples/webxr_ar_paint.html
painter = new TubePainter();
painter.setSize( 0.4 ); // 線の太さ
painter.mesh.material.side = THREE.DoubleSide;
scene.add( painter.mesh );
TubePainterオブジェクトのmeshプロパティが、空間に描かれる線を表すので、sceneに追加します。インスタンス化直後はジオメトリを描画しないように設定されている(geometry.drawRange.count = 0)のでシーンに追加してもなにも表示されません。
実際にユーザーの操作に応じて線を描画する処理はrender()関数から呼び出されるhandleController()にあります。
examples/webxr_ar_paint.html
function render() {
handleController( controller );
renderer.render( scene, camera );
}
examples/webxr_ar_paint.html
function handleController( controller ) {
var userData = controller.userData;
var point1 = userData.points[ 0 ];
var point2 = userData.points[ 1 ];
var matrix1 = userData.matrices[ 0 ];
var matrix2 = userData.matrices[ 1 ];
// 端末のスクリーンの奥20cm (0, 0, -0.2)の位置を表すAR空間上の座標
point1.set( 0, 0, - 0.2 ).applyMatrix4( controller.matrixWorld );
matrix1.lookAt( point2, point1, up );
if ( userData.isSelecting === true ) {
if ( userData.skipFrames >= 0 ) {
// TODO(mrdoob) Revisit this
userData.skipFrames --;
} else {
var count = painter.mesh.geometry.drawRange.count;
painter.stroke( point1, point2, matrix1, matrix2 );
painter.updateGeometry( count, painter.mesh.geometry.drawRange.count );
}
}
point2.copy( point1 );
matrix2.copy( matrix1 );
}
controller.userDataのpointsは座標を保持する2要素の配列で、1つ目の要素が現在の座標、2つめの要素が前回の座標です。matrixも2要素の配列で、前回の座標から今回の座標に向かう回転を表します。先ほどと同様に2つ目の要素が前回の値です。これらの値を使用してuserData.isSelectingがtrueのときにだけpainter.stroke()メソッドを呼び出すと、端末をタップしている間、ジオメトリが更新されて空間上に白線が描画されます。
HTMLファイル内のWebXR AR Paintに関係する処理は以上です。次にARButton.jsとTubePaint.jsの中身を確認してみましょう。
ARButton
ARButtonはXRセッションの開始と終了を管理するためのボタンです。
examples/jsm/webxr/ARButton.js
button.onclick = function () {
if ( currentSession === null ) {
navigator.xr.requestSession( 'immersive-ar' ).then( onSessionStarted );
} else {
currentSession.end();
}
};
ボタンをクリックするとimmersive-arモードのXRセッションをリクエストします。このimmersive-arモードは前回の手順に従ってChromeのフラグを有効にしていなければ利用できません。セッションが得られるとonSessionStarted()関数が呼び出されます。
examples/jsm/webxr/ARButton.js
function onSessionStarted( session ) {
session.addEventListener( 'end', onSessionEnded );
renderer.xr.setReferenceSpaceType( 'local' );
renderer.xr.setSession( session );
button.textContent = 'STOP AR';
currentSession = session;
}
onSessionStarted()関数ではXRセッションのReferenceSpaceTypeを'local'に設定しています。またセッション終了時のイベントリスナもここで設定します。
examples/jsm/webxr/ARButton.js
function onSessionEnded() {
currentSession.removeEventListener( 'end', onSessionEnded );
renderer.xr.setSession( null );
button.textContent = 'START AR';
currentSession = null;
}
onSessionEnded()関数はonSessionStarted()関数のほぼ逆の処理を行います。ARButtonの処理はこれでほぼすべてです。
TubePainter
初期化
TubePainterオブジェクトは3D白線を表すオブジェクトです。実際に描画されるメッシュはTubePainterオブジェクト内で次のように定義されています。
examples/jsm/misc/TubePainter.js
const BUFFER_SIZE = 1000000 * 3;
let positions = new BufferAttribute( new Float32Array( BUFFER_SIZE ), 3 );
positions.usage = DynamicDrawUsage;
let normals = new BufferAttribute( new Float32Array( BUFFER_SIZE ), 3 );
normals.usage = DynamicDrawUsage;
let colors = new BufferAttribute( new Float32Array( BUFFER_SIZE ), 3 );
colors.usage = DynamicDrawUsage;
let geometry = new BufferGeometry();
geometry.setAttribute( 'position', positions );
geometry.setAttribute( 'normal', normals );
geometry.setAttribute( 'color', colors );
geometry.drawRange.count = 0;
let material = new MeshStandardMaterial( {
roughness: 0.9,
metalness: 0.0,
vertexColors: VertexColors
} );
let mesh = new Mesh( geometry, material );
mesh.frustumCulled = false;
BufferedGeometryを使用した普通のメッシュです。ユーザーが操作するたびにジオメトリが更新されるので、頂点(positions)、法線(normals)、頂点カラー(colors)などの属性のusageはデフォルトのStaticDrawUsageからDynamicDrawUsageに変更されています。また100万ポリゴン分のバッファが確保されていますが、実際に描画に使用されるのはgeometry.drawRange.count個の頂点だけです。
頂点の設定
このジオメトリに頂点などの属性を設定するのはHTMLファイル内のhandleController()関数内で呼び出されていたstroke()メソッドです。
examples/jsm/misc/TubePainter.js
function stroke( position1, position2, matrix1, matrix2 ) {
if ( position1.distanceToSquared( position2 ) === 0 ) return;
let count = geometry.drawRange.count;
let points = getPoints( size );
for ( let i = 0, il = points.length; i < il; i ++ ) {
let vertex1 = points[ i ];
let vertex2 = points[ ( i + 1 ) % il ];
// positions
vector1.copy( vertex1 ).applyMatrix4( matrix2 ).add( position2 );
vector2.copy( vertex2 ).applyMatrix4( matrix2 ).add( position2 );
vector3.copy( vertex2 ).applyMatrix4( matrix1 ).add( position1 );
vector4.copy( vertex1 ).applyMatrix4( matrix1 ).add( position1 );
vector1.toArray( positions.array, ( count + 0 ) * 3 );
vector2.toArray( positions.array, ( count + 1 ) * 3 );
vector4.toArray( positions.array, ( count + 2 ) * 3 );
vector2.toArray( positions.array, ( count + 3 ) * 3 );
vector3.toArray( positions.array, ( count + 4 ) * 3 );
vector4.toArray( positions.array, ( count + 5 ) * 3 );
// normals...
// colors...
count += 6;
}
geometry.drawRange.count = count;
}
stroke()メソッドの引数はpoint1とmatrix1が現在の端末の座標と向き、point2とmatrix2が前回の座標と向きです。getPoints()メソッドを呼び出すと白線の断面に当たるリング状に並んだ頂点が生成されます。その頂点をpoint1とmatrix1、point2とmatrix2を使用して移動し、それらをBufferGeometryの属性に設定します。その後、geometry.drawRange.countを追加した頂点数分増やすことで、表示対象にします。
ジオメトリの更新
stroke()メソッドの次にhandleController()関数内で呼び出されるのがupdateGeometry()メソッドです。
examples/jsm/misc/TubePainter.js
function updateGeometry( start, end ) {
if ( start === end ) return;
let offset = start * 3;
let count = ( end - start ) * 3;
positions.updateRange.offset = offset;
positions.updateRange.count = count;
positions.needsUpdate = true;
// normals...
// colors...
}
このメソッドは追加された頂点の範囲を受け取り、その範囲に含まれる頂点だけを更新しています。
最後に
長々と書いてしまいましたが、そもそもこの記事を読む大多数の人のモチベーションは「自分もThree.jsでARアプリケーションを作りたい」ではないでしょうか?その目的だと白線の描画に関する諸々の説明は不要なので、最後にThree.jsでARアプリケーションを作るために必要な最低限の手順を簡単にまとめておきます。(未確認)
表示
WebGLRendererオブジェクトの初期化パラメータとしてalpha: trueを設定し、xr.enabledプロパティをtrueに設定してください。後はdocument.body.appendChild(ARButton.createButton(renderer))で追加したボタンをクリックすればAR表示されるはずです。
操作
renderer.xr.getController(0)を使用してコントローラーオブジェクトを取得します。Three.js内ではこのオブジェクトがXRセッションの代わりになるので、イベントリスナを追加したり、プロパティを参照していい感じにThree.jsを使用した3Dオブジェクトを操作してください。
株式会社カブクでは弊社業務でのWebXRの使いどころを一緒に探してくれるフロントエンドエンジニアを募集していますが、今はそれよりThree.js Advent Calendar 2019の参加者を募集しています。よろしくお願いします。
その他の記事
Other Articles


関連職種
Recruit

