Add Meta IOBT body tracking: emit BodyMeta from the Quest client - #6
Add Meta IOBT body tracking: emit BodyMeta from the Quest client#6zerenluo123 wants to merge 2 commits into
Conversation
Replaces the "Coming soon" placeholder with a working Mode dropdown (Off / Body (IOBT) / Motion (PICO only)) that streams 70 upper-body joints from the headset cameras, published under a new "BodyMeta" key. BodyMeta rather than reusing PICO's "Body": the skeletons are not interchangeable (70 joints vs 24, and IOBT reports no velocity or acceleration), so sharing the key would either truncate joints or break existing PICO consumers. Docs/BodyMeta.md specifies the format, including the two things consumers get wrong: isActive must be checked before reading joints (the array keeps stale values once the headset comes off), and calib does not need to reach Valid (joints stream complete throughout Calibrating). The five project settings body tracking depends on all fail silently when wrong -- the app runs, the Inspector looks right, and no data arrives -- so they are scripted in BodyTrackingSetup.cs (Tools > Body Tracking) rather than left as manual Inspector steps. LogWindow.Info is made thread-safe: body tracking logs from a non-main thread and the previous static-instance access could crash there. Controller, head and hand paths are untouched. Verified on Quest 3: joints=70 calib=Valid fidelity=High confidence=1.00.
|
The PC-side counterpart that parses this key is XR-Robotics/XRoboToolkit-PC-Service-Pybind#16 |
|
Thank you for the PR! I have a question. It seems that the full-body mode (84 joints) is not included in this PR, right? |
Correct — this PR is UpperBody (70 joints) only. The reason is that FullBody's extra 14 joints are predicted rather than measured: the legs come from Generative Legs, inferred from head and upper-body motion, since the cameras can't see your legs while the headset is on your head. For teleoperation we want ground truth for every joint we publish, and we want it without any external hardware — no base stations, no strap-on trackers. There's no official Meta leg tracker that would give real leg data (the SDK exposes no external tracker input at all), and the third-party full-body setups for Quest route through SteamVR. UpperBody at fidelity=High is the set that is both camera-measured and hardware-free. |
The Mode dropdown becomes Off / Upper Body (70) / Full Body (84), switching the joint set at runtime via OVRBody.SetRequestedJointSet instead of fixing it at build time. FullBody appends 14 lower-body joints inferred by Generative Legs, keeping the first 70 ids unchanged so existing consumers keep working. Drops TrackingType.Motion, which had no Quest equivalent and was rejected back to Off, along with the code that existed only to explain that. Also shrinks the reused "joints" JSON array when the joint set does. It was only ever overwritten in place, so switching 84 -> 70 left the 14 leg entries behind holding stale coordinates, contradicting count and looking downstream like tracking that froze. Bumps bundleVersion to 1.0.2 and refreshes Docs/ui.png and the panel reference. Verified on Quest 3: 70 <-> 84 both directions, all 84 position-valid.
|
We just added the Full Body feature — the Mode dropdown is now Upper Body — 70 joints
Full Body — 84 joints
Full Body appends ids 70–83 and leaves 0–69 unchanged, so a consumer that only understands the first 70 keeps working. Those 14 joints come from Generative Legs — inferred from head and upper-body motion, since the cameras cannot see the operator's legs while the headset is worn. Different in kind from 0–69, so One note on project settings:
Verified on a Quest 3: switches both directions, all 84 joints position-valid, legs move with the operator. Also bumped |



Meta IOBT body tracking: emit
BodyMetafrom the Quest clientReplaces the "Coming soon ......" placeholder in the Body Tracking panel with a working
implementation: 70 upper-body joints inferred from the headset's own cameras
(Meta Inside-Out Body Tracking), published to PC-Service under a new
BodyMetakey.No extra hardware — no base stations, no wearable trackers.

The
Modedropdown replaces the placeholder text, and the Status line below is a live readout(refreshed a few times a second) so an operator wearing the headset can confirm data is flowing.
What's in the box
Modedropdown:Off/Body (IOBT)/Motion (PICO only). SelectingBody (IOBT)starts publishing;
Motionis rejected up front with a notice rather than silently producingnothing, since PICO's motion-tracker mode has no Quest counterpart.
BodyMetawire format — 70 joint poses, per-joint validity flags, plusjointSet/fidelity/calib/confidence/isActive/countand the tracking-space origin.Specified in
Docs/BodyMeta.md.BodyTrackingSetup.cs— scripts the five project settings body tracking needs(
Tools > Body Tracking > Configure Project Settings).dropdown-change time.
Why a new key instead of reusing
BodyPICO's
Bodyis a fixed 24-joint layout with velocity and acceleration per joint. Meta IOBT gives70 (or 84 for
FullBody) and reports no rates at all —OVRPlugin.BodyJointLocationcarries onlyLocationFlagsandPose. Sharing the key would mean either truncating 46 joints or redefining aformat existing PICO consumers already parse, so Quest publishes
BodyMetaand old consumers areunaffected. Consumers read the layout from
jointSetrather than assuming one.Or headless:
This exists because all five settings fail silently when wrong — the app runs, every Inspector
checkbox looks right, and no data arrives with nothing in logcat to say why:
bodyTrackingSupportOculusProjectConfig1bodyTrackingFidelityOculusRuntimeSettings2(High) —1silently degrades to IK-onlybodyTrackingJointSetOculusRuntimeSettings0(UpperBody)requestBodyTrackingPermissionOnStartupOculusProjectConfigtrue— otherwisecom.oculus.permission.BODY_TRACKINGis never requestedOVRBodycomponentWorth flagging: the Meta XR SDK resets
bodyTrackingFidelityto1when the project is openedwithout running this, so a fresh clone builds an IK-only APK unless the step is run. Happy to
restructure this if you'd prefer the settings committed differently.
Two behaviours consumers need to know
Both are documented in
Docs/BodyMeta.md; noting them here because they surprised us.calibis not a gate. Joints stream complete from the first frame. Calibration runs inside theruntime, refining the skeleton's scale;
Validonly means it stopped refining. Measured on aQuest 3:
All 70 joints flowed for 12 s before
Validarrived — and Unity-Movement's sample scene likewisedrives its avatar throughout
Calibrating. Calibration can't be disabled or pre-seeded(
BodyTrackingCalibrationInfoexposes onlyBodyHeight, andSuggestBodyTrackingCalibrationOverrideis a suggestion the runtime may ignore) and it restarts on every re-don, so gating on it would stall
the stream 30–60 s each time for no gain.
isActivemust be checked first. When the headset leaves the head thejointsarray keeps itslast values — the JSON objects are reused rather than cleared, and
calib/countgo stale too(row three above). Nothing in the numbers reveals it. Body tracking requires the headset actually
worn; hanging it on the neck stops the data, which differs from controller tracking.
Also included
LogWindow.Infois made thread-safe. Body tracking logs from a non-main thread and the previousstatic-instance access could crash there — hit while developing this, so it's fixed here rather
than left as a trap.
Scope
Controller, head and hand paths are untouched. Verified by extracting the body of each
controller/head/hand method and diffing against
main:OnControllerTog,OnHeadTog,OnHandTrackingTog,OnSendTog,OnReconnectBtn,TcpConnect,SetControllerOn,SetHeadOn,SetHandTrackingOn,SetTrackingType,GetControllerPose,IsControllerActive,GetHeadsetPose,GetJoints,GetActiveInputDevice,IsHandTrackingActive— all 16 byte-identical.The two code deletions are
OnHighAccuracyand theHighAccuracytoggle'sSetActivecall: Metahas no per-session fidelity switch (it's a build-time project setting, not a call like PICO's
StartBodyTracking(mode)), so the toggle is hidden rather than left implying an inactive control.App identity (
applicationIdentifier,productName,bundleVersion) is deliberately left atupstream's values.
Testing
Verified end-to-end on a Quest 3 with PC-Service and the Python SDK:
The joint parent table was validated geometrically rather than by eye: of 69 bones, 62 vary by
under 5 mm in length; upper arm 23.8 cm, forearm 23.8 cm, shoulder width 33.3 cm, and left/right
match to the hundredth of a centimetre — a wrong parent link could not produce that symmetry.
Built with Unity 2021.3.45f2 (the version this repo's README specifies), 0 errors.
Happy to split this up, rename the key, or move the docs if you'd prefer a different shape.