Closed77

WebXR body-trackingモジュールをBabylon.jsでわいわいする

にー兄さんにー兄さん

まぁこの機能は今のところ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な引数は撮らないっぽいか

にー兄さんにー兄さん

使い方としては

  1. body-trackingで初期化
  2. XRFrameから下記を取得できる
    3. joint
    4. size(なんのsizeだろう)
    5. jointをキーにしたXRBodySpace


あれ、各Jointの姿勢はどうやって取得するんだろう

にー兄さんにー兄さん

ここら辺を読んでいて、個々のJointにおける座標変換がめんどくさそうに感じた

にー兄さんにー兄さん

最近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
vite.config.ts
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ブラウザ上で動いているアプリのデバッグを行いたい
設定ファイルはこのようにしたらできた

launch.json
{
  // 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で行けるんだなぁという知見を得ました

にー兄さんにー兄さん

さてやっていくか

実機で内容を確認する前に、もう一回draftを読んでおこう

にー兄さんにー兄さん

まぁでも中身は単純で、XRFeatureを初期化したらXRFrameの中に入ってるJointの情報が撮れるよって感じなのかな(やってみないとわからないけど)

とりまやってみるか

にー兄さんにー兄さん

そういえば、ここに載ってるコマンドを使うと端末のIPアドレスを取得できるっぽい

https://qiita.com/niusounds/items/fe82e811aa243363c66c

にー兄さんにー兄さん

しやしかしコマンドが長いな

adb shell ip addr show | grep -oP 'inet \d+\.\d+\.\d+\.\d+' | grep -v 127.0.0.1 | grep -oP '\d+\.\d+\.\d+\.\d+'

adb shell ip addr showでやってみると、結構色々出てくるが、
その中の inetから始まる文字列が該当するっぽい

にー兄さんにー兄さん

つまりリモートUSBデバッグする時は

  1. USBで繋ぐ
  2. adb tcpip 5555
  3. adb shell ip addr show
  4. adb connect ip:5555

という感じか

にー兄さんにー兄さん

まず、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はちゃんと起動した
にー兄さんにー兄さん

WebXR Experimentsを無効にしているときに出てくるBabylonの場合のコンソール画面

Feature 'body-tracking' is not permitted by permissions policy
にー兄さんにー兄さん

課題解決のためにできそうなことは下記

  • Babylon自体のデバッグを行い、どの時点でクラッシュするのかを確認して原因を突き詰める
  • logcatの内容を解析してそれっぽいエラーを見つける
  • threejsでもやってみる
  • Questブラウザのほうで解決されるのを待つ
にー兄さんにー兄さん

GitHub ActionsとGitHub Pages整えるとかしようかなぁ
たぶんQuestブラウザが治らないと進まない案件なので

にー兄さんにー兄さん

Babylon.jsフォーラムでも回答したが、再度確認したところどうやらブラウザのアプデにより治ってるっぽい挙動を確認した

https://forum.babylonjs.com/t/meta-quest-browser-with-enabling-webxr-experiments-flag-crashes-when-enter-webxr/50227/4?u=drumath2237

にー兄さんにー兄さん

バージョンの変化はこちらの通り

checked timing version
yesterday 33.0.0.68.29.592499619
now 33.0.0.121.29.594097662
にー兄さんにー兄さん

いや~タイミングが絶妙でしたな、まぁフォーラムへの投稿もできたし良しとするか

にー兄さんにー兄さん

そして私の環境でも無事、XRBodyの取得を確認できました

さてこいつ、なんかイテレータらしいからな、なんとか情報見て見るか

にー兄さんにー兄さん

中身を見て見た

どうやらイテレータの中身は

["joint-name", XRBodySpace]

というタプルが入ってるらしい

そしてXRBodySpaceとは、XRSpaceを継承してjointNameを付け加えたものっぽい

ということはこれを理解するためにはXRSpaceを理解しなくてはいけないんだな

にー兄さんにー兄さん

試したところ、自分の環境ではreference space typeがlocalだと全然座標を取得出来なかった
今回はlocal-floorやbounded-floorならいい感じに取得できましたね

ん-ここらへんはわかなんな……

にー兄さんにー兄さん

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;
}
にー兄さんにー兄さん

ん~完全にこれになってる、TypeScriptこれできないのかな?
https://twitter.com/ninisan_drumath/status/1787173977494102130

にー兄さんにー兄さん

候補は以下だけど

  • Record型
    • インデックスによる参照はできる
  • インデックス型
    • インデックスによる参照はできる
  • Map型
    • forによるループはできる
    • インデックス参照はできない
      • getメソッドによる取得はできる
にー兄さんにー兄さん

recordをループさせるためには

const a:{[key: string]:string} = {A:"aa"};

for(key in a) {
  const data = a[key];
}

こうするしかないのかな

にー兄さんにー兄さん

この操作感が一番理想に近い(それでもインデックスには"clear"とか"get"が指定できちゃう)けど、
このオブジェクトを初期化することができない気がする

にー兄さんにー兄さん

Jointの取得方法について再考する

今のところJointはMapで提供されるので、

const headJoint = bodyTracking.joints.get("head");

このように取得できる

これを、例えばMapからRecordに変換するようなgetterがあるとうれしいのでは?と思った

const headJoint = bodyTracking.jointsRecord["head"];

みたいな感じ?

にー兄さんにー兄さん

Map⇔Object変換はこのようになるらしい
なるほど、MapはそのままだとJSON.stringfyできないんだな、そういう意味ではObject用意していても良さそうだ

MapもObjectもそれぞれいいところがあるから、どちらもAPIとして提供できるはめっちゃ良さそう

にー兄さんにー兄さん

んや、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

な気がする
これがわかればよい

にー兄さんにー兄さん

とりま、featureの試作までできたので
このスクラップはクローズするかぁ

このスクラップは6ヶ月前にクローズされました