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