Body
Regular • File:
BodyTracking.csv• Writer:BodyTrackingDataWriter.cs
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.CaptureBodyDataistrue, AND- a
SilicoSkeletonBodyis 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.csvin the run directory. - Writer:
BodyTrackingDataWriter.csinRuntime/DataCapture/DataWriters/. - Columns come from
BodyUtility.ColumnNamesSkeleton(skeleton)inRuntime/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>:
| Column | Type | Units | Description |
|---|---|---|---|
<Bone>_PosX, <Bone>_PosY, <Bone>_PosZ | float | metres | World-space position of the bone. |
<Bone>_RotX, <Bone>_RotY, <Bone>_RotZ, <Bone>_RotW | float | unit quaternion | World-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 = falseor no skeleton was assigned when the run started, there is noBodyTracking.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.controllerPosif 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.