WebXR AR Paint その2

2019/12/04
このエントリーをはてなブックマークに追加

Three.js Advent Calendar 2019の4日目の記事です。

はじめに

こんにちは。ここのところ業務で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.xrgetCamera()が利用できました。入力についても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の持つその他のpointsmatricesskipFramesなどのプロパティについては後ほど説明します。

ARButton

ではTubePainterARButtonの使い方の説明に移ります。といっても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.userDatapointsは座標を保持する2要素の配列で、1つ目の要素が現在の座標、2つめの要素が前回の座標です。matrixも2要素の配列で、前回の座標から今回の座標に向かう回転を表します。先ほどと同様に2つ目の要素が前回の値です。これらの値を使用してuserData.isSelectingtrueのときにだけ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()メソッドの引数はpoint1matrix1が現在の端末の座標と向き、point2matrix2が前回の座標と向きです。getPoints()メソッドを呼び出すと白線の断面に当たるリング状に並んだ頂点が生成されます。その頂点をpoint1matrix1point2matrix2を使用して移動し、それらを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

2020/06/16
[Web フロントエンド] esbuild が爆速すぎて webpack / Rollup にはもう戻れない

2020/03/19
[Web フロントエンド] Elm に心折れ Mint に癒しを求める

2020/02/28
さようなら、TypeScript enum

2020/02/14
受付のLooking Glassに加えたひと工夫

2020/01/28
カブクエンジニア開発合宿に行ってきました 2020冬

2020/01/30
Renovateで依存ライブラリをリノベーションしよう 〜 Bitbucket編 〜

2019/12/27
Cloud Tasks でも deferred ライブラリが使いたい

2019/12/25
*, ::before, ::after { flex: none; }

2019/12/21
Top-level awaitとDual Package Hazard

2019/12/20
Three.jsからWebGLまで行きて帰りし物語

2019/12/18
Three.jsに入門+手を検出してAR.jsと組み合わせてみた

2019/11/06
GraphQLの入門書を翻訳しました

2019/09/20
Kabuku Connect 即時見積機能のバックエンド開発

2019/08/14
Maker Faire Tokyo 2019でARゲームを出展しました

2019/07/25
夏休みだョ!WebAssembly Proposal全員集合!!

2019/07/08
鵜呑みにしないで! —— 書籍『クリーンアーキテクチャ』所感 ≪null 篇≫

2019/07/03
W3C Workshop on Web Games参加レポート

2019/06/28
TypeScriptでObject.assign()に正しい型をつける

2019/06/25
カブクエンジニア開発合宿に行ってきました 2019夏

2019/06/21
Hola! KubeCon Europe 2019の参加レポート

2019/06/19
Clean Resume きれいな環境できれいな履歴書を作成する

2019/05/20
[Web フロントエンド] 状態更新ロジックをフレームワークから独立させる

2019/04/16
C++のenable_shared_from_thisを使う

2019/04/12
OpenAPI 3 ファーストな Web アプリケーション開発(Python で API 編)

2019/04/08
WebGLでレイマーチングを使ったCSGを実現する

2019/04/02
『エンジニア採用最前線』に感化されて2週間でエンジニア主導の求人票更新フローを構築した話

2019/03/29
その1 Jetson TX2でk3s(枯山水)を動かしてみた

2019/03/27
任意のブラウザ上でJestで書いたテストを実行する

2019/02/08
TypeScript で “radian” と “degree” を間違えないようにする

2019/02/05
Python3でGoogle Cloud ML Engineをローカルで動作する方法

2019/01/18
SIGGRAPH Asia 2018 参加レポート

2019/01/08
お正月だョ!ECMAScript Proposal全員集合!!

2019/01/08
カブクエンジニア開発合宿に行ってきました 2018秋

2018/12/25
OpenAPI 3 ファーストな Web アプリケーション開発(環境編)

2018/12/23
いまMLKitカスタムモデル(TF Lite)は使えるのか

2018/12/21
[IoT] Docker on JetsonでMQTTを使ってCloud IoT Coreと通信する

2018/12/11
TypeScriptで実現する型安全な多言語対応(Angularを例に)

2018/12/05
GASでCompute Engineの時間に応じた自動停止/起動ツールを作成する 〜GASで簡単に好きなGoogle APIを叩く方法〜

2018/12/02
single quotes な Black を vendoring して packaging

2018/11/14
3次元データに2次元データの深層学習の技術(Inception V3, ResNet)を適用

2018/11/04
Node Knockout 2018 に参戦しました

2018/10/24
SIGGRAPH 2018参加レポート-後編(VR/AR)

2018/10/11
Angular 4アプリケーションをAngular 6に移行する

2018/10/05
SIGGRAPH 2018参加レポート-特別編(VR@50)

2018/10/03
Three.jsでVRしたい

2018/10/02
SIGGRAPH 2018参加レポート-前編

2018/09/27
ズーム可能なSVGを実装する方法の解説

2018/09/25
Kerasを用いた複数入力モデル精度向上のためのTips

2018/09/21
競技プログラミングの勉強会を開催している話

2018/09/19
Ladder Netwoksによる半教師あり学習

2018/08/10
「Maker Faire Tokyo 2018」に出展しました

2018/08/02
Kerasを用いた複数時系列データを1つの深層学習モデルで学習させる方法

2018/07/26
Apollo GraphQLでWebサービスを開発してわかったこと

2018/07/19
【深層学習】時系列データに対する1次元畳み込み層の出力を可視化

2018/07/11
きたない requirements.txt から Pipenv への移行

2018/06/26
CSS Houdiniを味見する

2018/06/25
不確実性を考慮した時系列データ予測

2018/06/20
Google Colaboratory を自分のマシンで走らせる

2018/06/18
Go言語でWebAssembly

2018/06/15
カブクエンジニア開発合宿に行ってきました 2018春

2018/06/08
2018 年の tree shaking

2018/06/07
隠れマルコフモデル 入門

2018/05/30
DASKによる探索的データ分析(EDA)

2018/05/10
TensorFlowをソースからビルドする方法とその効果

2018/04/23
EGLとOpenGLを使用するコードのビルド方法〜libGLからlibOpenGLへ

2018/04/23
技術書典4にサークル参加してきました

2018/04/13
Python で Cura をバッチ実行するためには

2018/04/04
ARCoreで3Dプリント風エフェクトを実現する〜呪文による積層造形映像制作の舞台裏〜

2018/04/02
深層学習を用いた時系列データにおける異常検知

2018/04/01
音声ユーザーインターフェースを用いた新方式積層造形装置の提案

2018/03/31
Container builderでコンテナイメージをBuildしてSlackで結果を受け取る開発スタイルが捗る

2018/03/23
ngUpgrade を使って AngularJS から Angular に移行

2018/03/14
Three.jsのパフォーマンスTips

2018/02/14
C++17の新機能を試す〜その1「3次元版hypot」

2018/01/17
時系列データにおける異常検知

2018/01/11
異常検知の基礎

2018/01/09
three.ar.jsを使ったスマホAR入門

2017/12/17
Python OpenAPIライブラリ bravado-core の発展的な使い方

2017/12/15
WebAssembly(wat)を手書きする

2017/12/14
AngularJS を Angular に移行: ng-annotate 相当の機能を TypeScrpt ファイルに適用

2017/12/08
Android Thingsで4足ロボットを作る ~ Android ThingsとPCA9685でサーボ制御)

2017/12/06
Raspberry PIとDialogflow & Google Cloud Platformを利用した、3Dプリンターボット(仮)の開発 (概要編)

2017/11/20
カブクエンジニア開発合宿に行ってきました 2017秋

2017/10/19
Android Thingsを使って3Dプリント戦車を作ろう ① ハードウェア準備編

2017/10/13
第2回 魁!! GPUクラスタ on GKE ~PodからGPUを使う編~

2017/10/05
第1回 魁!! GPUクラスタ on GKE ~GPUクラスタ構築編~

2017/09/13
「Maker Faire Tokyo 2017」に出展しました。

2017/09/11
PyConJP2017に参加しました

2017/09/08
bravado-coreによるOpenAPIを利用したPythonアプリケーション開発

2017/08/23
OpenAPIのご紹介

2017/08/18
EuroPython2017で2名登壇しました。

2017/07/26
3DプリンターでLチカ

2017/07/03
Three.js r86で何が変わったのか

2017/06/21
3次元データへの深層学習の適用

2017/06/01
カブクエンジニア開発合宿に行ってきました 2017春

2017/05/08
Three.js r85で何が変わったのか

2017/04/10
GCPのGPUインスタンスでレンダリングを高速化

2017/02/07
Three.js r84で何が変わったのか

2017/01/27
Google App EngineのFlexible EnvironmentにTmpfsを導入する

2016/12/21
Three.js r83で何が変わったのか

2016/12/02
Three.jsでのクリッピング平面の利用

2016/11/08
Three.js r82で何が変わったのか

2016/12/17
SIGGRAPH 2016 レポート

2016/11/02
カブクエンジニア開発合宿に行ってきました 2016秋

2016/10/28
PyConJP2016 行きました

2016/10/17
EuroPython2016で登壇しました

2016/10/13
Angular 2.0.0ファイナルへのアップグレード

2016/10/04
Three.js r81で何が変わったのか

2016/09/14
カブクのエンジニアインターンシッププログラムについての詩

2016/09/05
カブクのエンジニアインターンとして3ヶ月でやった事 〜高橋知成の場合〜

2016/08/30
Three.js r80で何が変わったのか

2016/07/15
Three.js r79で何が変わったのか

2016/06/02
Vulkanを試してみた

2016/05/20
MakerGoの作り方

2016/05/08
TensorFlow on DockerでGPUを使えるようにする方法

2016/04/27
Blenderの3DデータをMinecraftに送りこむ

2016/04/20
Tensorflowを使ったDeep LearningにおけるGPU性能調査

→
←

関連職種

Recruit

バックエンドエンジニア(Python・Go)

業務内容

当ポジションは弊社Webサービスのバックエンド機能設計及び実装を担当します。 サービス毎の開発チームで2週間スプリントのスクラム開発を実施しています。 週次で開発チームミーティングを実施し、実装設計の相談や工数見積もりを行います。 全ての開発コードはレビューと自動テストによって品質を保っています。 また、リファクタリングやフレームワークのバージョンアップも開発フローに組込み、技術的負債を放置しない開発を目指しています。

フロントエンドエンジニア(TypeScript)

業務内容

当ポジションは弊社Webサービスのフロントエンド機能設計及び実装を担当します。 サービス毎の開発チームで2週間スプリントのスクラム開発を実施しています。 週次で開発チームミーティングを実施し、実装設計の相談や工数見積もりを行います。 全ての開発コードはレビューと自動テストによって品質を保っています。 また、リファクタリングやフレームワークのバージョンアップも開発フローに組込み、技術的負債を放置しない開発を目指しています。

インターン(Webエンジニア)

業務内容

業務から独立した、調査・研究系のタスクをおまかせしています。コードレビュー、 社内での報告会、 ブログ記事執筆を通して着実にスキルアップしていただくことを目指しています。 (希望があれば、プロダクトの開発業務もおまかせします。)

→
←

お客様のご要望に「Kabuku」はお応えいたします。
ぜひお気軽にご相談ください。

お電話でも受け付けております
03-6380-2750
営業時間:09:30~18:00
※土日祝は除く