WebXR body-trackingモジュールをBabylon.jsでわいわいする
わいわいしたい
Babylon.jsにこんなissueが出ていた
これは前からちょいちょい話題になっていた、WebXR Body Tracking Moduleに関するissueで、Babylon.jsのWebXR Featuresに実装したい旨のものだと思う
これはwakufactoryさんの投稿 自分はこれで知った
explanerはこちら
公式ではないけど、ドラフトがある
まぁこの機能は今のところQuest3のMeta Quest Browserでしか使えない機能だと思う
explanerのほうにはこんな感じ
navigator.xr.requestSession({optionalFeatures: ["body-tracking"]}).then(...);
function renderFrame(session, frame) {
// ...
if (frame.body.hand) {
// render a body
}
}
}
depth-sensingの時とは違い、Optionalな引数は撮らないっぽいか
うわ~でもDraftと全然違うな
あーDraftを見ていると、割とシンプルなデータ構造ではあるんだな
使い方としては
- body-trackingで初期化
- XRFrameから下記を取得できる
3. joint
4. size(なんのsizeだろう)
5. jointをキーにしたXRBodySpace
か
あれ、各Jointの姿勢はどうやって取得するんだろう
ここら辺を読んでいて、個々のJointにおける座標変換がめんどくさそうに感じた
あとあれか、iteratableにしなくちゃいけないのか
最近Quest3を購入し、セットアップを終えたので
Quest3のQuest Browserのデバッグ環境を作ることにした
まずは手元のBabylon.jsアプリケーションでWebARができるようにする
そういえばbody-trackingってARでも動くのかな?
コードはこんな感じ
import { Engine, MeshBuilder, Scene } from "@babylonjs/core";
import "./style.css";
const main = async () => {
const renderCanvas =
document.querySelector<HTMLCanvasElement>("#renderCanvas");
if (!renderCanvas) {
return;
}
const engine = new Engine(renderCanvas);
const scene = new Scene(engine);
scene.createDefaultCameraOrLight(true, true, true);
MeshBuilder.CreateBox("box", { size: 0.2 });
await scene.createDefaultXRExperienceAsync({
uiOptions: {
sessionMode: "immersive-ar",
referenceSpaceType: "local",
},
});
window.addEventListener("resize", () => engine.resize());
engine.runRenderLoop(() => scene.render());
};
main();
reference spaceはlocalだと良い感じに動いた
例のごとくhttps化の対応も必要です
viteの場合は下記コマンドと設定ファイルにより実現
pnpm add -D @vitejs/plugin-basic-ssl
import basicSsl from "@vitejs/plugin-basic-ssl";
import { defineConfig } from "vite";
export default defineConfig({
plugins: [basicSsl()],
});
次にデバッグ環境
自分はまだQuest3を開発者モードにしていなかったので、Meta Questアプリから設定した
なぜか自分の手元では接続されてるんだけどされてない、みたいな状態になってて結構手こずった
結局Androidを再起動したら治るなど。なんだったんや……
この設定を行うことで、USBデバッグができるようになる
USBをつなぐとデバッグモードで接続しますよ?ってダイアログが出るので許可をする
するとadb devices
で反応するようになる
次に無線USBデバッグを設定する、といってもこれはスマホの時と変わらずで
adb tcpip 5555
adb connect <ip>:5555
をやればいいということである
Quest3のローカルIPは設定から確認できる
これでUSBに繋いでいなくてもデバッグができるようになったので快適
Chromeのinspect deviceでも普通にQuestブラウザのインスぺクトができるようになりました
次にvscodeのAndroid WebView Debugging拡張機能を使って、
Questブラウザ上で動いているアプリのデバッグを行いたい
設定ファイルはこのようにしたらできた
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "android-webview",
"request": "attach",
"name": "Attach to Android WebView",
"device": "192.168.11.60:5555",
"application": "com.android.chrome"
}
]
}
一応、QuestブラウザのバンドルIDはcom.oculus.browser
だと思うのだけど、ここで指定する際にはcom.android.chrome
で行けるんだなぁという知見を得ました
すると、vscodeのデバッガが動きました!
Questの体験の様子を撮影する手段を調査しよう
さてやっていくか
実機で内容を確認する前に、もう一回draftを読んでおこう
まぁでも中身は単純で、XRFeatureを初期化したらXRFrameの中に入ってるJointの情報が撮れるよって感じなのかな(やってみないとわからないけど)
とりまやってみるか
そういえば、ここに載ってるコマンドを使うと端末のIPアドレスを取得できるっぽい
色々詰まっているのでここにメモを残しておく
まず、Body Tracking を試行したところ動かない、というかブラウザがクラッシュした(なんで)
もともとBTを動かすためにはWebXR Esperimentsフラグを有効化しなくてはいけないみたいな話があったので有効化したら、Babylon.jsのWebXRが起動しない問題にぶち当たった
ここはlogcatでログをとっていた
色々エラーっぽいのが履かれていたけどまだここの解析は終わっておらず、原因はわかっていない
そしてWebXR Featureを有効化しないと、Body Tracking is not permitted by permission profileというエラーが出て、WebXRは起動するがやはりBTは動かない
WebXRの軌道について試したのは3種類
- 自前で作ったBabylon制でBTをfeatureに含めたプロジェクト
- はがさんが公開されているPlaycanvasのプロジェクト
- wakufactoryさんが公開されているBTのプロジェクト
そしてWebXR自体の確認用に、WebXR Samplesも試した
結果は、
WebXR experiments無効自
- 自前のBabylonは起動、しかしBTは動作せず
- Playcnvasのデモも上記と同様
- wakufactoryさんのデモも同様
- WebXRSamplesも起動
WebXRExperiments有効時
- BabylonとPlayncanvasはブラウザ事クラッシュ
- wakufactoryさんのデモはちゃんとBTまで動作した
- WebARでも動作している
- WebXRSamplesはちゃんと起動した
環境については、
Quest OS: v64
Quest ブラウザ:v33
であった
課題解決のためにできそうなことは下記
- Babylon自体のデバッグを行い、どの時点でクラッシュするのかを確認して原因を突き詰める
- logcatの内容を解析してそれっぽいエラーを見つける
- threejsでもやってみる
- Questブラウザのほうで解決されるのを待つ
あれからなんと、はがさんにPlaycanvasのデモが動くように修正してもらった!
ありがとうございます......!
どうやら試していた当初とPlaycanvasのバージョンが変わっているらしく、最新バージョンでは動かなくなっているんだとか
にっしさんからきいたところ、どうやらこういう現象はほかでも起きているんだとか
まぁBabylonとPlaycanvasで起きている時点でなんかブラウザの問題な気はしていましたね
自分の環境以外でも再現しそうな気がしたため、Babylonのフォーラムで報告
GitHub ActionsとGitHub Pages整えるとかしようかなぁ
たぶんQuestブラウザが治らないと進まない案件なので
ActionsとPagesのセットアップに際して、リポジトリを公開した
Babylon.jsフォーラムでも回答したが、再度確認したところどうやらブラウザのアプデにより治ってるっぽい挙動を確認した
そして私の環境でも無事、XRBodyの取得を確認できました
さてこいつ、なんかイテレータらしいからな、なんとか情報見て見るか
中身を見て見た
どうやらイテレータの中身は
["joint-name", XRBodySpace]
というタプルが入ってるらしい
そしてXRBodySpaceとは、XRSpaceを継承してjointNameを付け加えたものっぽい
ということはこれを理解するためにはXRSpaceを理解しなくてはいけないんだな
BodySpaceからジョイントの姿勢情報を取得出来た!
どうやらXRFrame.getPoseをすればよいらしい
SpaceはOriginを持つのか
ここら辺ちゃんと見ないとわからなさそうだな
body-tracking自体の動作を確認できたので、いったん可視化してみると
座標系とか気にしないで普通に可視化するだけでいい感じに動いていた、スバラシイ
試したところ、自分の環境ではreference space typeがlocalだと全然座標を取得出来なかった
今回はlocal-floorやbounded-floorならいい感じに取得できましたね
ん-ここらへんはわかなんな……
次はWebXRFeratureを仮実装してみる
Featureの実装に際してAPIを考えるか
なんとなく使用感のスケッチをしてみた
const bodyTracking = featureManager.enableFeature(
WebXRFeatureName.BODY_TRACKING,
{
baseXRSpace: xrSpace,
}
);
bodyTracking.setBaseSpace(xrSpace);
bodyTracking.onTrackedObservable.add((joints, size: number) => {});
sessionManager.onFrameObservable.add(() => {
const size = bodyTracking.size; // number | null
const joints: BodyJoints = bodyTracking.joints;
const headJoint = joints["head"];
headJoint.name;
headJoint.position;
headJoint.rotation;
});
type BodyJoints = Record<JointType, Joint>;
type JointType = "head" | "neck";
interface Joint {
name: JointType;
position: Vector3;
rotation: Quaternion;
}
あ~でも、Record型だとforループできないのか
ん~完全にこれになってる、TypeScriptこれできないのかな?
候補は以下だけど
- Record型
- インデックスによる参照はできる
- インデックス型
- インデックスによる参照はできる
- Map型
- forによるループはできる
- インデックス参照はできない
- getメソッドによる取得はできる
recordをループさせるためには
const a:{[key: string]:string} = {A:"aa"};
for(key in a) {
const data = a[key];
}
こうするしかないのかな
これな気がしてきた
ほな、マップがええか~
この操作感が一番理想に近い(それでもインデックスには"clear"とか"get"が指定できちゃう)けど、
このオブジェクトを初期化することができない気がする
Map型だな!これが落としどころとしていいんじゃないだろうか!
Jointの取得方法について再考する
今のところJointはMapで提供されるので、
const headJoint = bodyTracking.joints.get("head");
このように取得できる
これを、例えばMapからRecordに変換するようなgetterがあるとうれしいのでは?と思った
const headJoint = bodyTracking.jointsRecord["head"];
みたいな感じ?
API仕様を決めるために、XRSpaceと向き合わないといけないな
んや、XRSpaceについて理解してから使用を考えようと思ったけど、こいつが割としっかり勉強しないといけないかもしれないのと、実験するのにもちょっとひと手間必要
そうなるといつまでたっても実装が進まないので、いったん現状の提案仕様を実装してから考えるcar~となっています
さて、Map→Object変換を試したら、まさかのキーの型がstringになった
type JointType =
| "root"
| "head"
| "neck"
;
type JointMap = Map<JointType, number>;
const jointMap: JointMap = new Map<JointType, number>();
jointMap.set("root", 0)
jointMap.set("head", 1)
jointMap.set("neck", 2)
type JointRecord = {
[k in JointType]: number
}
const obj = Object.fromEntries(jointMap)
// const obj: {
// [k: string]: number; <--- キーがJointTypeではなくstringになってる
// }
ここにきて、いうほどIndexによるアクセスって必要か????という気になってきた
もちろんできたほうがかっこいいんだけど、いや、これ必要ないなやっぱり
実装コストに見合うメリットが無いぞ
下手にハックな方法で変換してもコードが見にくくなるわけで
そうしたら普通にただMap返すだけでいい気がしてきた
そしてMapが普通に便利である
いや、全然Mapだけでいいじゃんという気持ちになってきた
type JointType =
| "root"
| "head"
| "neck"
;
type JointMap = Map<JointType, number>;
const jointMap: JointMap = new Map<JointType, number>();
jointMap.set("root", 0)
jointMap.set("head", 1)
jointMap.set("neck", 2)
for (const [_, n] of jointMap) {
console.log(n)
}
// Output:
// 0
// 1
// 2
jointMap.forEach(n => console.log(n))
// Output:
// 0
// 1
// 2
結局ユーザが知りたいのって
- size
- jointに含まれる姿勢
- jointの名前
- 姿勢計算の時に使うBaseSpace
な気がする
これがわかればよい
ReadonlyMap<K,T>
という方があるらしく非常にいい感じだ
そうだ、そういえばmirrorZ
なんてものがあったな
これは適用せねば
API仕様を考えて、一応ここまで動くように作れた
ということで、とりま整えたリポジトリ
そしてフォーラムにも投稿
とりま、featureの試作までできたので
このスクラップはクローズするかぁ