🥳

Babylon.jsでWebXR空間内の写真を撮る

2022/07/20に公開

小話、記事の対象者

先日Babylon.js勉強会主催の「ゆるほめLT」にてWebXR使用中の写真撮影機能について発表しました。
その発表までには間に合わなかったのですが、後日でモーションコントローラーを使った撮影についても機能追加できたので、開発のポイントと参考情報を記事にまとめました。
Babylon.jsを使用したWebXR対応、コントローラー操作、スクリーンショットあたりの事を知りたい方が対象になります。

作ったもの

こちらになります。
(この記事で以降サンプルと言ったときはこちらの作ったコードを指します。)
Oculus Quest 2(Meta Quest 2)で右手コントローラーのトリガーボタン(人差し指のボタン)を押すと、空間内でのカメラの目線の方向の写真を撮ることができます。
https://playground.babylonjs.com/#WGZLGJ#5917

このように

  1. XRで使用できる(HMDを被って使用できる)ようにして
  2. コントローラーのボタンを押すと
  3. 写真を撮る

ことについて、全てBabylon.jsを使って機能を実装することができる(素晴らしい!)ので、それらについて順番に書きます。

1.XRで使用できる(HMDを被って使用できる)ようにする

まずはXRで使用できるようにします。
サンプルで言うと以下の部分が実装箇所になります。1つ1つ要点を書きます。

class Playground {
    public static CreateScene = async function (engine: BABYLON.Engine, canvas: HTMLCanvasElement) {
        ...省略...

        //XR Helper
        const xrHelper = await scene.createDefaultXRExperienceAsync({
            inputOptions:{
                doNotLoadControllerMeshes:true
            }
        });
        const featuresManager = xrHelper.baseExperience.featuresManager;
        featuresManager.enableFeature(BABYLON.WebXRFeatureName.POINTER_SELECTION, "stable", {
            useUtilityLayer: true //if this is true, pointer laser is not displayed.
        });
	
	...省略...

async/await処理

    public static CreateScene = async function (engine: BABYLON.Engine, canvas: HTMLCanvasElement) {

WebXRを使いたい場合は非同期処理で動かすとのことなのでasync/awaitで書くようにします。

WebXR対応にする

ここなのですが、細かいこと気にしないなら以下の1行で問題ないです。これだけでXR対応します。

        //XR Helper
        const xrHelper = await scene.createDefaultXRExperienceAsync();

1行でできちゃうといった話はBabylon.js勉強会にてユスキィさんがお話されているのですが、今回は写真撮影をする関係でコントローラー周辺のメッシュを表示させたくないので少しオプションをいじっています。
次に記す処理を施すことで、以下図のように撮影した写真にコントローラーとレーザーポイントが映り込んでしまうということが無くなります。

WebXRの初期化処理

ここからサンプルのコードの説明をします。

        //XR Helper
        const xrHelper = await scene.createDefaultXRExperienceAsync({
            inputOptions:{
                doNotLoadControllerMeshes:true
            }
        });

createDefaultXRExperienceAsync関数でXR対応するのですが、そこで色んなオプションを選択することができます。その中のinputOptionsのdoNotLoadControllerMeshesがコントローラーのメッシュを非表示にする機能を持つので、この機能をONにするためにtrueとします。

この辺の細かい説明は以下の公式ドキュメントに記載があります。
https://doc.babylonjs.com/typedoc/interfaces/BABYLON.IWebXRInputOptions#doNotLoadControllerMeshes

WebXR機能の管理

        const featuresManager = xrHelper.baseExperience.featuresManager;
        featuresManager.enableFeature(BABYLON.WebXRFeatureName.POINTER_SELECTION, "stable", {
            useUtilityLayer: true //if this is true, pointer laser is not displayed.
        });

また、featuresManagerはWebXRの色んな機能の管理ができるのですが、今回はコントローラーがどこに向いているかを示すレーザーポインターのメッシュを表示させないようにするためにuseUtilityLayerをtrueにします。

この辺の話も詳しいことは以下の公式ドキュメントに記載があります。
https://doc.babylonjs.com/typedoc/interfaces/BABYLON.IWebXRControllerPointerSelectionOptions#useUtilityLayer

2. コントローラーで操作できるようにする

サンプルでの該当箇所は以下になります。
一つ一つの説明は大変なので、ざっくりとコメントアウトで記載します(というより私もなんとなくでしか理解していないです。今度ちゃんと読もう…)。

    //コントローラーの登録
    xrHelper.input.onControllerAddedObservable.add((controller) => {
        //モーションコントローラーとして使用
        controller.onMotionControllerInitObservable.add((motionController) => {
	    //右手側のコントローラーで
            if (motionController.handness === 'right') {
                const xr_ids = motionController.getComponentIds();
		//トリガーボタン(人差し指のボタン)について
                let triggerComponent = motionController.getComponent(xr_ids[0]);//xr-standard-trigger
                triggerComponent.onButtonStateChangedObservable.add(() => {
		    //ボタン押下の状態に変化があり、
                    if (triggerComponent.changes.pressed) {
		        //ボタンが押された状態なら
                        if(triggerComponent.pressed){
			    //押されたときにしたい処理を実行。今回は写真撮影(スクショ)の機能を記載。
                            BABYLON.Tools.CreateScreenshotUsingRenderTarget(engine, xrHelper.baseExperience.camera ,{width:1200, height:1200}); 
                        }
                    }else{
                        //pass
                    }
                });

            }
        })
    });

ボタンやスティックの種類について

細かい説明はBabylon.jsの公式ドキュメントに書かれています。
https://doc.babylonjs.com/divingDeeper/webXR/webXRInputControllerSupport

上記のコードは以下のPlaygroundからコピーしてきて、そこから内容を変えてます。こちらのPlaygroundはスティックやボタンの種類について一通り網羅して書かれているので参考になります。
https://playground.babylonjs.com/#28EKWI#37

ボタン押下の状態について

ボタンの押下状態については少し手間で、triggerComponentにてchanges.pressedとpressedを併用します。changes.pressedだけだと押したタイミングだけでなく押しを解除したタイミングでも撮影がされてしまい、またpressedだけの場合は押し続けると写真がすごい勢いで何枚も撮られてしまいます。

//ボタン押下の状態に変化があり、
if (triggerComponent.changes.pressed) {
    //ボタンが押された状態なら
    if(triggerComponent.pressed){
        //押されたときにしたい処理を実行。
	(省略)
    }
}

triggerComponentはクラスとしてはBABYLON.WebXRControllerComponentであり、以下のページが参考になります。
https://doc.babylonjs.com/typedoc/classes/BABYLON.WebXRControllerComponent

3.写真を撮る

該当するのはこの部分になります。

BABYLON.Tools.CreateScreenshotUsingRenderTarget(engine, xrHelper.baseExperience.camera ,{width:1200, height:1200}); 

写真を撮ると言っていますが、機能としてはスクリーンショットになります。
参考とするドキュメントはこちらです。
https://doc.babylonjs.com/divingDeeper/scene/renderToPNG

今回使用しているCreateScreenshotUsingRenderTarget関数は引数にBabylon.jsのエンジン、レンダリングに使用したいカメラ、サイズを指定することで使用できます。

今回は自分のXR時の目線をカメラとしたいので、xrHelper.baseExperience.cameraを使用します。CreateScreenshotUsingRenderTargetの引数で使うカメラのクラスはBABYLON.Cameraですが、WebXR CameraはFreeCameraの拡張機能らしく、だからなのか、特に問題なく使用することができました。

また私は写真のサイズは1200x1200にしてみています。パソコンの画面サイズ以上の解像度でスクショすることができるというのも良さの1つですね。

以上です

参考情報として公式ドキュメントをかなり引用しました。Babylon.jsはドキュメントが充実してAPIも細かく書かれており、かつPlaygroundで大量のサンプルを取得することができるので公式情報だけでかなりの情報を得ることができそうです。
全部英語なのは、DeepLがなんとかしてくれます。

読んでいただきありがとうございました。

Discussion