Skip to main content

Body

Regular • File: BodyTracking.csv • Writer: BodyTrackingDataWriter.cs

Media

At a glance

Body tracking captures the full-body skeletal pose — one set of position + rotation columns per tracked bone. The exact column set depends on the active SilicoSkeletonBody (OpenXR body, Mocopi, pose-estimated fallback, etc.), so every experience's Body CSV may have a different header. The row cadence is one per LateUpdate when the capture is enabled and a skeleton is assigned.

When it writes

One row per LateUpdate, provided:

  • ExperienceUtilities.CaptureBodyData is true, AND
  • a SilicoSkeletonBody is assigned on the experience.

If the skeleton is swapped mid-run, columns can change — avoid this. If the skeleton is unassigned mid-run, writes pause until it's reassigned.

File & location

  • Default: BodyTracking.csv in the run directory.
  • Writer: BodyTrackingDataWriter.cs in Runtime/DataCapture/DataWriters/.
  • Columns come from BodyUtility.ColumnNamesSkeleton(skeleton) in Runtime/ExperienceObjects/Skeletons/Body/BodyUtility.cs.

Configuration

Gated by ExperienceUtilities.CaptureBodyData and the skeleton assignment — no dedicated ScriptableObject. The skeleton itself (SilicoSkeletonBody asset) determines which bones are in the output.

Columns

Shared prefix

See Column conventions — The shared prefix.

Per-bone blocks

One block per bone in the active skeleton, contributed in skeleton-defined order. A typical block for bone <Bone>:

ColumnTypeUnitsDescription
<Bone>_PosX, <Bone>_PosY, <Bone>_PosZfloatmetresWorld-space position of the bone.
<Bone>_RotX, <Bone>_RotY, <Bone>_RotZ, <Bone>_RotWfloatunit quaternionWorld-space rotation of the bone.

Typical <Bone> names (vary by skeleton): Hips, Spine, Chest, UpperChest, Neck, Head, LeftUpperArm, LeftLowerArm, LeftHand, RightUpperArm, RightLowerArm, RightHand, LeftUpperLeg, LeftLowerLeg, LeftFoot, RightUpperLeg, RightLowerLeg, RightFoot.

A 20-bone skeleton therefore contributes ~140 columns after the shared prefix.

Sample rows

...shared...,Hips_PosX,Hips_PosY,Hips_PosZ,Hips_RotX,Hips_RotY,Hips_RotZ,Hips_RotW,Spine_PosX,Spine_PosY,Spine_PosZ,...
...,0.0,0.95,-2.5,0.0,0.0,0.0,1.0,0.0,1.15,-2.5,...
...,0.01,0.95,-2.48,0.0,0.02,0.0,0.9998,0.01,1.15,-2.48,...

Trailing columns omitted for readability — a real row has one block per bone.

Join with other streams

import pandas as pd
exp = pd.read_csv("ExperienceState.csv")
body = pd.read_csv("BodyTracking.csv")
joined = exp.merge(body, on="FrameNumber", how="left", suffixes=("_exp", "_body"))

Use how="left" because gated frames (no skeleton) won't have Body rows. Forward-fill pose columns with .ffill() if a dense series is needed.

Gotchas

  • Columns vary by skeleton. Two different recordings may have different Body CSV headers if different skeletons were assigned. Scripts that hard-code column names must handle the missing-column case.
  • File only exists if the gate passes at run-start. If CaptureBodyData = false or no skeleton was assigned when the run started, there is no BodyTracking.csv. This is the most common reason for a "missing file" complaint.
  • World-space, not participant-local. Positions are in environment coordinates. If the participant walks across the room, the Hips position changes accordingly. Subtract Participant.controllerPos if you want participant-local coordinates.
  • Quaternion sign flips. See Participant gotchas — same rule: quaternion differences aren't angular distance without hemisphere normalisation.
  • Pose-estimated bones are noisy. If the skeleton is partly estimated (e.g. OpenXR body extension fills in torso from arm+head poses), those bone columns will be smoother but have systematic bias. Treat "tracked" and "estimated" bones differently if you know which is which.

Analysis recipes

  • Reaction time — for motion-based responses (e.g. hand-raise), differentiate bone position over time to detect the response onset.
  • Stimulus-locked averaging — epoch-align body pose around stimulus onset.