WebXR Raw Camera Access APIのbabylon.jsでの利用について考える
Raw Camera Access というWebXR Featureが実験的にリリースされているみたい
コアコンセプトというか、ドキュメントはこちらのGitHub
Platform Supportに関しては以下
This will be supported on platforms where Chrome supports AR. Currently, this is only Android. WebView support is planned. There are no technical restrictions specific to this API preventing it from being implemented on other platforms.
ARをサポートしているChromeで、現在はAndroidのみ
モチベとしては、
従来のAPIではプライバシー保護の理由から生のカメラアクセスを許可isていなかったが
他のWebXR APIに影響が出そうとのこと
まずはBabylon.jsで'camera-access'
feature付きでimmersive WebARモードに入ってみて
XRSessionなどから直接APIをたたけるか試したほうが良さそう
もしそのあと余力があったらbabylon.jsの規格に基づいて
WebXRFeatureを作ることもできそうだ
自分の持っているChrome for Androidで対応しているのか
TypeScriptで開発できるのか、などいろいろ心配事はある
Babylon.jsでWebXR APIを使うときには、WebXR APIの型定義がある
いまちょっと気になるのが、
XRViewerPoseにあるViewからcameraを取り出しているけど
cameraプロパティ定義されてなくない?ってところかな
// ... in rAFcb ...
let viewerPose = xrFrame.getViewerPose(xrRefSpace);
for (const view of viewerPose.views) {
if (view.camera) {
// ... handle a view that has a camera image ...
}
}
現行のBabylon.jsの型定義では、WebGLBindingからカメラのWebGLTextureを取得するメソッドがなさそう
これがない
// ... in rAFcb ...
const cameraTexture = binding.getCameraImage(view.camera);
WWWCのDraftによると、WebGLBindingから別のメソッドでWebGLSubImageを取得し
それのcolorTetureというWebGLTextureを取り出せる可能性がある
とりあえず躊躇していても始まらんのでvite-ts+babylonjsで作ってみる
> yarn create vite webxr-raw-camera-access-babylon --template vanilla-ts
yarn add @babylonjs/core@5.0.0-alpha.60
babylonは5.0じゃないとWebGLBindingとかWebXR系の機能が欠けている可能性がある
今日時点の成果は、babylonで豆腐を出して
viteでhttpsの設定をして
immersive-arが実行できたところまでかな
余力があれば、
- GitHub ActionsでLinter走らせる
- GitHub PagesもしくはAzure SWAへのデプロイの自動化
- vscodeで保存時にeslintやprettierのfixを走らせる
adb のリモート接続とchromeのdevice inspectを使ったリモートデバッグができたことを確認した
Babylon.jsのcreateDefaultXRExperienceAsync
でcamera-accessをfeatureとして渡してみたところ、
不明のfeatureとして警告された
これはFeatureManager当たりの実装を見たほうがいいな
WebXR APIでFeatureがenableかどうかを返してくれるメソッドとかあると嬉しいんだけどな
エイヤッと実装してみたら、特にcamera-access APIを使うことなくカメラの内部パラメータを取得できた
え、どゆこと
WebXR Device APIのサイトが結構糸井とドキュメントが載っていて読むと面白そう
カメラの画像をどうやって取得しようか迷っている
WebGLLayerとかXRWebGLBindingなどから何かしらを取得して
WebGLのAPI使って変換する感じになると思うけど
WebGLFrameBufferを取得して別のcanvasに保存して
それからbase64で取り出すという方式を試したい
WebGLのFrameBufferに関する解説
ちょっとずつわかってきた気がするけど難しい
Webでカメラ画像ってどう取得しようかなーってずっとやってたけど
やっぱりgetUserMediaを試しておこう
ここらへんAPIが少なくて困っていたイメージがあるんだけど
今見たら意外と色々あったのでその知見を整理していく
まずお決まりのごとく、
const stream = await navigator.mediaDevices.getUserMedia(constraints);
でMediaStreamを取得する。
constraintsはvideoなのかとか解像度とかカメラの向きとか指定できる奴
そしてMediaStream.getVideoTracks()
でビデオトラックとなるMediaStreamTrackオブジェクトを取得する。
そしたらvideoTrackを引数にしてImageCaptureオブジェクトを作成することができる。
ドキュメントのサンプルを見たほうが速い
const input = document.querySelector('input[type="range"]');
var imageCapture;
navigator.mediaDevices.getUserMedia({video: true})
.then(mediaStream => {
document.querySelector('video').srcObject = mediaStream;
const track = mediaStream.getVideoTracks()[0];
imageCapture = new ImageCapture(track);
return imageCapture.getPhotoCapabilities();
})
ImageCaptureのドキュメントはこちら
写真をキャプチャするだけでいい、みたいな用途には良さそう
ImageCapture.grabFrameもしくはtakePhotoでキャプチャ画像を取得できる
grabFrameではBitmapImageオブジェクトが返される
BitmapImageはcanvasのCanvasBitMapImageContextを使って表示出来たりする
つまりgrabFrameでcanvasに画像を表示してcanvas.toDataURLでbase64エンコできる
takePhotoはblobオブジェクトが返される
<img>タブに表示させるようなんだとか
tこれはbase64エンコできるのかな
var takePhotoButton = document.querySelector('button#takePhoto');
var canvas = document.querySelector('canvas');
takePhotoButton.onclick = takePhoto;
function takePhoto() {
imageCapture.takePhoto().then(function(blob) {
console.log('Took photo:', blob);
img.classList.remove('hidden');
img.src = URL.createObjectURL(blob);
}).catch(function(error) {
console.log('takePhoto() error: ', error);
});
}
canvas要素をいちいち作るのめんどくさいのでoffscreenCanvas使いたいんだけど
こいつには肝心のtoDataURLメソッドがない、悲しい
だからめんどくさいけどcanvasを作るか
blobからbase64エンコすることになるけど
blobの場合pngになってるか心配
ImageCapture、@types/w3c-image-capture
という型宣言がないとtypeエラーになるらしい
カメラのキャプチャはできた
だがしかしカメラのストリームをbabylonと奪い合ってしまって共存するのが無理みたい......ん~~~~
これ気になる、
ただtextureがないと実行できないかも
ここがきになる
面白いサイトがあった
ここでproposalなAPIを試せる
しかし自分のデバイスではcamera-acessが使えなかった(逆にこれだけ使えなかった)
まさかと思って試してみたら、NEXUS 5XのChromeは
camera-accessに対応していたという....マジ??
camera-accessを使って生のCPUイメージの取得に成功
OpenARCloudさんのサンプル通りに変換したらいい感じに動いたけどちょっと重い
Immersalとの連携について考える
現状はApp.ts
にほぼすべてのロジックを詰め込んでいるのでリファクタもしたいと思いつつ
とりあえず先に動くのを確認したいとも思う
Immersal REST API用のインターフェースは一通り定義できた
そこからBabylonで使えるように相互変換するロジックが必要になる
Requestはbase64文字列とか内部パラメータなのであまり考える必要はないけど
ResponseはMatrixに変換して姿勢データを取得しなくちゃいけな
Matrix.decompose()
が使えそう
ここからダイレクトで位置と回転が取得できるっぽい
const matrix = ~~;
matrix.decompose(map.position, map.rotation);
みたいな感じかな
リクエスト送信時の姿勢にレスポンスで返ってきた姿勢の逆行列をかける必要がある
Babylonのmatrix.multiplyの実装を見たところ、
m1.multiply(m2)はM1M2って意味らしい
処理のフローを整理する
- 事前にシーンにはマップ原点に対応するオブジェクトを配置
- EnterWebXR
- 起動直後?もしくはオフセット時間を置いてRawCameraからテクスチャを取得
- 取得時の姿勢を保存
- Immersal REST APIへリクエスト
- apiからレスポンスを受け取る
- レスポンスを変換
- マップの姿勢を計算
ワークフローが基本babylonに依存しているから
ライフサイクルを司るクラスにステートとロジックを詰め込んでしまってる肥大化してる
XR系だけでもWebXR Managerとして分離したり
ロジックも静的にしてユーティリティとして分離したい
ここら辺の実装が割と終わった
あとはいい感じのタイミングでキャプチャ画像の処理とImmersalへのリクエストを送る実装をして
どんなレスが返ってくるかを検証する
なんとかオレオレアーキテクチャのバグを取り除き、リクエストに必要なパラメータの取得と変換を突破
そしてImmersal /localizeb64 apiにリクエストが通った
Immersal REST APIはCORSでContent-Typeヘッダを許可していないので
指定するとCORSでブロックされる
ちなみにImmersalのトークンとマップIDは.env.local
に保存しており、
ViteはVITE_xxxみたいな感じで保存した変数は
import.meta.env.VITE_xxx
で呼び出せる
次のタスクは部屋のglbもしくは.babylonファイルにしてもいいけど
とりあえず部屋のメッシュモデルを読み込む
そして座標変換をして位置合わせがあっているか確認する
部屋のモデルのいらない部分を削って軽くしたモデルをbabylon.js sandboxで表示してみた
多分うまくいってる
glbインポート、そういえば前に失敗していたような......
そんなことを思っているとやまゆさんが反応してくださった
Viteの静的アセットのURLハンドリングについての内容
例だと/img.png
が/assets/img.2d8efhg.png
になってる
この仕様のせいでディレクトリとファイル名を分けて扱わなくちゃいけないSceneLoaderと相性が悪い
結局?urlで取得したパスを処理してディレクトリ名とファイル名に分けるしかないかなぁ
/assets/img.2d8efhg.png
の場合、
path.split("/").slice(0,-1).join("/");
で/assets
が取得できる
Babylon.jsのSceneLoaderによるgltfのインポートは以下を参照
何とか苦戦しながらもglbインポートに成功
Immersalのモデルは重いのでblenderでメッシュを削りに削ったらオンボロスマホでもなんとか読み込めた......
あとは座標変換ができれば完成、というところまで来たけど
案の定詰まってる
Babylonjs Sandboxとか活用しながらもちょっと使い勝手悪いし
BabylonjsのMatrixの挙動が想定していたのと違ったりしてう~~~~nって感じ
やっぱりビジュアルで確認しながらトライアンドエラーしたい感じなのだが
ここでBabylonjs Editorが使えないか、と思った
部屋のglbモデル読み込んで
Immersalのレスポンスをもとに変換行列を作って試してみたい
既出の問題を2つ解決した
一つ目はモデル読み込みで固まる件
これは.glbをsandboxを使って.babylonに変換しておくことによって解決
Immersalのマップを無加工でも読み込めそうだな?
2つ目はcameraのattachControllerが使えなかった件
base64変換用のcnavasが前面に来ていたせいでした()
camera-accessでカメラ画像からImageDataを取得する処理で
WebGLRenderingContext周りで問題が起きているゆえに、
OnXRFrameでglをいじらないといけなくなってコードが煩雑になっている。
しかしgl contextが必要なのはカメラ画像を取得する部分だけなので
ここをまるっとメソッドで囲ってしまって、コールバックにすればよいのではというアイデア
かつここの処理がかなり重いのでWebWorkerに委託できないかと思った
今リポジトリの名前が
WebXR Raw Camera Access Babylonになってるけど
これは後々わからなくなるのでImmersalって単語を入れたほうがいい(戒め)
chrome://flags
にアクセスしてWebXR Incubationをenableにすると
WebXR Device APIの実験的機能にアクセスできるみたい
Nexus5Xはもともとこれをonにしていたんだなぁ
Pixel 4a 5Gで試したところ、無事camera-accessを使用できたので:超嬉しい:
連続で位置合わせが成功することが分かった
Pixelでデバッグできるからかなり快適
babylon.js editorにマップメッシュモデルを読み込んでみる
(リダクション済み)
これ自体は、まず__root__のy軸回転が180度、zのscaleが-1になってて
それをidentityなEmptyの子にして整合性が保たれている
public onStart(): void {
const pos = new Vector3(0.8086, 0.2053, 0.2098);
const rotMatrix = new Matrix();
rotMatrix.setRow(0, new Vector4(0.352, 0.1208, 0.9281, 0));
rotMatrix.setRow(1, new Vector4(0.9345, 0.0102, -0.3557, 0));
rotMatrix.setRow(2, new Vector4(-0.0524, 0.9926, -0.1093, 0));
rotMatrix.setRow(3, new Vector4(0.0, 0.0, 0.0, 1.0));
const rot = Quaternion.FromRotationMatrix(rotMatrix.transpose());
console.log(pos, rot.toEulerAngles());
}
で出てきた姿勢をcameraに指定してみると
以下のようになった
それっぽいけどカメラのz回転が90度かかってる、おしい
Quaternion.FromRotationMatrix(
rotMatrix.invert().transpose()
)
つまり逆行列からの天地という順番でやってみたところ、ニアピンって感じの表示になった
カメラの姿勢はidentity、マップの回転を↑の式から得た
マップ原点でカメラの姿勢を変える時は行列→転置→Quaternion→z軸90度回転Quaternionを掛ける
で対処できた
行列→inverse→転置→Quaternionでマップの逆姿勢っぽいものを得られたが
ここでz軸回転のQuaternionを掛けても何も変わらなかった
ということは、逆行列を得る前もしくは逆行列に対してz軸回転の行列を掛けてから計算する必要がある?
Inspectorを導入してインポートされたモデルを見てみた
やはり階層構造がリセットされていてモデルの見た目も変
__root__というTransformationNodeのtransformationがいい感じに補正されていたやつ
(つまりrotatinoのyが180度、scaleのzが-1になってる)
const node = new BABYLON.TransformNode('MapRoot', this._scene);
BABYLON.SceneLoader.ImportMeshAsync('', dirName, fileName, this._scene).then((result) => {
console.log(result.meshes);
for (const mesh of result.meshes) {
mesh.rotation.y = Math.PI;
mesh.scaling.z = -1.0;
mesh.setParent(node);
}
});
これでMapRootの中に良い感じに調整されたメッシュが子供になった
無理やりだけどカメラ座標系に対するマップ姿勢を計算できた?
const pos = new BABYLON.Vector3(0.8086, 0.2053, 0.2098).negate();
const rotMatrix = new BABYLON.Matrix();
rotMatrix.setRow(0, new BABYLON.Vector4(0.352, 0.1208, 0.9281, 0));
rotMatrix.setRow(1, new BABYLON.Vector4(0.9345, 0.0102, -0.3557, 0));
rotMatrix.setRow(2, new BABYLON.Vector4(-0.0524, 0.9926, -0.1093, 0));
rotMatrix.setRow(3, new BABYLON.Vector4(0.0, 0.0, 0.0, 1.0));
const enhancementMatrix = BABYLON.Matrix.RotationAxis(BABYLON.Vector3.Up(), Math.PI / 2);
const rot = BABYLON.Quaternion.FromRotationMatrix(
rotMatrix.multiply(enhancementMatrix).invert().transpose()
);
this.mapRootNode.position = pos;
this.mapRootNode.rotateAround(
BABYLON.Vector3.Zero(),
BABYLON.Vector3.Right(),
rot.toEulerAngles().x
);
this.mapRootNode.rotateAround(
BABYLON.Vector3.Zero(),
BABYLON.Vector3.Up(),
rot.toEulerAngles().y
);
this.mapRootNode.rotateAround(
BABYLON.Vector3.Zero(),
BABYLON.Vector3.Forward(),
rot.toEulerAngles().z
);
カメラの姿勢はIdentityでマップだけが動いている
キャプチャ時のカメラの姿勢をコールバックに渡すことにした
ちゃんとXRSessionがInitされてから5秒後にローカライズという風に指定した
もう位置合わせのリクエストは当たり前のように通るようになったな
ただなぜかリクエスト2回読んじゃってる気がする......
前に出てきたレスポンスだとうまくいくのに
なぜか直近のレスポンスだとy軸で反対になっちゃうというバグがある
何回かリクエストを飛ばしてみて実験しなくてはいけない
あと今のところ一回のキャプチャにつき2回のリクエストが飛んでしまっている
これはXRFrameが頻繁に飛んでくることによるもので
キャプチャの必要があるかのフラグがfalseになる前に違うOnXRFrameコールバックが呼ばれてしまうからだと思われる
最終フェーズに入って、Immersalのレスポンスから直接座標変換までをつなぎこんだ
実際に座標変換してみたところ、なんか微妙に曲がってる
これ前に見たやつだ~~~~~
座標変換のメソッド、ちゃんと確立しなきゃダメだな
毎回こんな感じに傾くので、Immersalのレスポンスは結構一定値を返してるんだけど
ふと、canvasの表示のされ方がおかしいことに気づいた
気づいたというか前から気になっていたんだけど、
これ普通に送信データがこのままになってるので大丈夫なのか心配になった
XRCameraCaptureUtil.createImageFromTexture
の時点で生成されているImageDataが変になっている
このImageDataはUint8Array生成のときから同じ大きさのはず
出てきた
どうやらViewportの解像度を使うのではなくViewのカメラから解像度を取得するのが正確なのかもしれない
実際のviewのサイズはどうやら886x1920だったらしい
(view as any).camera.witth
などによってわかったし
手動でviewPortを作成して噛ませることにより正確な解像度のCamera Imageを取得することが可能になった
なんか位置合わせが改善された気がする
だけどなんかあと少しなんだよなぁ......
調整しなきゃいけないのだろうか
あと、なぜかかなり計算リソースを食うようになってる
スクショをとろうとしてもエラーが出たり、fpsがかなり低下したりしている
そろそろ位置合わせをボタンクリックで発火させたい
現状を動画撮ってみた
大分出来上がってきたけど惜しい、なんでだ()
もしかしたら、画像の向きに問題があるかもしれない
ARFoundationでやった時に、なぜか画像が左に90度回転した画像を送信していたのを思い出した
直近で試してみたいこと
- カメラ座標系での変換が合っているか
- 画像の向きに問題があるのか
記事を書いていて知ったのが、
explanationのGitHubではXRViewやXRWebGLBindingsに対してpartialで型宣言をしていた
partial interface XRView {
// Non-null iff there exists an associated camera that perfectly aligns with the view:
[SameObject] readonly attribute XRCamera? camera;
};
interface XRCamera {
// Dimensions of the camera image:
readonly attribute long width;
readonly attribute long height;
};
partial interface XRWebGLBinding {
// Access to the camera texture itself:
WebGLTexture? getCameraImage(XRCamera camera);
};
これすればXRViewとかいちいちanyにしなくても型補完が効くかもな
Babylon.jsでWebGLTextureを扱うのに、いかのissueが気になった
ここではUint8Arrayとサイズ指定で生成したRawTextureに対して、
rawTexture._texture = webglTexture;
const data = rawTexture.readPixels();
でUint8Arrayを取得するというやり方か?
rawTexture._texture = webGLTexture
は肩が違うのでできなかったが、
代わりに以下のforumで回答を見つけた
platgroundはこれ
ここにある通り、5.0だと
texture._texture._hardwareTexture._webGLTexture
がいいっぽい
数か月ぶりに再チャレンジしたところ、いい感じになりました