Participant
Regular • File:
ParticipantTracking.csv• Writer:ParticipantObservationRecorder.csvia observerParticipantTrackingObserver.cs
At a glance
Participant tracking captures the viewpoint pose (HMD or main camera) and the locomotion state (CharacterController-driven controller: position, rotation, velocity, grounded flag). Distinct from Body tracking, which captures the full skeletal pose — Participant is always the participant-vs-world state, regardless of whether a body skeleton is assigned.
Use Participant for:
- Head pose over time (gaze direction proxy, locomotion analysis).
- Controller velocity (locomotion speed, acceleration, pauses).
- Grounded-state transitions (jumps, falls, teleports).
When it writes
One row per LateUpdate, FixedUpdate, or both, depending on the Sampling Loop setting on the config SO. Optionally filtered:
- Distance / rotation threshold — only emit if the participant moved / rotated more than the configured minimum.
- Skip N — emit every Nth sample.
When the participant is stationary (within the threshold), no row is emitted — expect gaps in FrameNumber under those settings.
File & location
- Default:
ParticipantTracking.csvin the run directory. - Config SO:
ParticipantTrackingDataCapture— assigned on the experience.
Configuration
ParticipantTrackingDataCapture SO fields:
| Field | What it does |
|---|---|
enabledLogging | Master enable/disable. |
fileName | Override the default name. Default ParticipantTracking.csv. |
flushEveryNRows | Batched flush cadence. Default 30. |
writeHeader | Write the header row on first emission. Default true. |
samplingLoop | Update / FixedUpdate / Both. |
multipleSamplesPerFrame | If true, allow multiple samples within the same frame. |
recordMode | Continuous (every sample) / MovementAndEvents (only when moving or on event boundaries) / EventsOnly. |
emitEveryNSamples | Emit every Nth sample. Min 1, default 1. |
positionThreshold | Minimum position change (metres) before MovementAndEvents emits. Default 0.01. |
movementThreshold | Minimum rotation / movement magnitude before MovementAndEvents emits. Default 0.5. |
cameraSource | Reference to the camera providing the viewpoint pose. |
characterController | Reference to the locomotion CharacterController. |
useEulerAngles | If true, rotation columns are Euler (X/Y/Z in degrees). If false, quaternion (X/Y/Z/W). |
Columns
Shared prefix
See Column conventions — The shared prefix.
Domain columns
| Column | Type | Units | Description |
|---|---|---|---|
cameraPosX, cameraPosY, cameraPosZ | float | metres | Viewpoint position in environment coordinates. |
cameraRotX, cameraRotY, cameraRotZ, cameraRotW | float | unit quaternion | Viewpoint rotation (quaternion mode). |
cameraRotX, cameraRotY, cameraRotZ | float | degrees | Viewpoint rotation (Euler mode — no RotW). |
controllerPosX, controllerPosY, controllerPosZ | float | metres | CharacterController position. |
controllerRotX, controllerRotY, controllerRotZ, controllerRotW | float | unit quaternion / degrees | CharacterController rotation (quaternion or Euler, matching useEulerAngles). |
controllerVelX, controllerVelY, controllerVelZ | float | m/s | CharacterController linear velocity in environment coordinates. |
controllerIsGrounded | int | 0 / 1 | 1 if the controller is grounded this frame, else 0. |
Rotation column set depends on useEulerAngles. The column count differs (7 vs 6 per pose); always check the header line before hard-coding column indices.
Sample rows
Quaternion mode:
...shared...,cameraPosX,cameraPosY,cameraPosZ,cameraRotX,cameraRotY,cameraRotZ,cameraRotW,controllerPosX,controllerPosY,controllerPosZ,controllerRotX,controllerRotY,controllerRotZ,controllerRotW,controllerVelX,controllerVelY,controllerVelZ,controllerIsGrounded
...,0.0,1.72,-2.5,0.0,0.7071,0.0,0.7071,0.0,0.0,-2.5,0.0,0.7071,0.0,0.7071,0.0,0.0,0.0,1
...,0.03,1.72,-2.48,0.0,0.7071,0.0,0.7071,0.01,0.0,-2.49,0.0,0.7071,0.0,0.7071,1.2,0.0,0.8,1
Join with other streams
Standard FrameNumber join to ExperienceState:
import pandas as pd
exp = pd.read_csv("UpdateLoop.csv")
part = pd.read_csv("ParticipantTracking.csv")
joined = exp.merge(part, on="FrameNumber", how="left", suffixes=("_exp", "_part"))
Use how="left" rather than inner — if recordMode = MovementAndEvents or EventsOnly is set, Participant rows will not exist for every frame, and you want the full row set from UpdateLoop.csv with NaN controller values during stationary frames.
Gotchas
- Gaps when
recordModefilters samples. WithMovementAndEventsorEventsOnly, stationary frames produce no row. Don't assume everyUpdateLoop.csvframe has a matching Participant row. Use left joins; forward-fill with pandas.ffill()if you need a dense series. useEulerAngleschanges column count. Seven quaternion columns per pose become six Euler columns. Always read the header line rather than hard-coding indices.- Quaternion sign ambiguity.
qand-qare the same rotation. Numeric differences between rows don't equal angular distance. Convert via2 * acos(|q1·q2|)or axis-angle before computing "how much did the head rotate." - Camera vs. controller may differ. The viewpoint (HMD) moves with head motion; the CharacterController moves with locomotion input. In a seated experience they may be approximately equal; in roomscale they're not. Don't conflate.
- Velocity is engine-reported, not derived.
controllerVelX/Y/Zcomes from Unity's CharacterController, not from differencing positions. For head velocity you have to derive it yourself from successivecameraPosrows.
Analysis recipes
- Gaze paths — often combines camera pose from Participant with gaze direction from Eye.
- Cross-device sync — use
MonotonicExecutionTimeto align head-pose samples with external motion capture.