Skip to main content

Participant

Regular • File: ParticipantTracking.csv • Writer: ParticipantObservationRecorder.cs via observer ParticipantTrackingObserver.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.csv in the run directory.
  • Config SO: ParticipantTrackingDataCapture — assigned on the experience.

Configuration

ParticipantTrackingDataCapture SO fields:

FieldWhat it does
enabledLoggingMaster enable/disable.
fileNameOverride the default name. Default ParticipantTracking.csv.
flushEveryNRowsBatched flush cadence. Default 30.
writeHeaderWrite the header row on first emission. Default true.
samplingLoopUpdate / FixedUpdate / Both.
multipleSamplesPerFrameIf true, allow multiple samples within the same frame.
recordModeContinuous (every sample) / MovementAndEvents (only when moving or on event boundaries) / EventsOnly.
emitEveryNSamplesEmit every Nth sample. Min 1, default 1.
positionThresholdMinimum position change (metres) before MovementAndEvents emits. Default 0.01.
movementThresholdMinimum rotation / movement magnitude before MovementAndEvents emits. Default 0.5.
cameraSourceReference to the camera providing the viewpoint pose.
characterControllerReference to the locomotion CharacterController.
useEulerAnglesIf 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

ColumnTypeUnitsDescription
cameraPosX, cameraPosY, cameraPosZfloatmetresViewpoint position in environment coordinates.
cameraRotX, cameraRotY, cameraRotZ, cameraRotWfloatunit quaternionViewpoint rotation (quaternion mode).
cameraRotX, cameraRotY, cameraRotZfloatdegreesViewpoint rotation (Euler mode — no RotW).
controllerPosX, controllerPosY, controllerPosZfloatmetresCharacterController position.
controllerRotX, controllerRotY, controllerRotZ, controllerRotWfloatunit quaternion / degreesCharacterController rotation (quaternion or Euler, matching useEulerAngles).
controllerVelX, controllerVelY, controllerVelZfloatm/sCharacterController linear velocity in environment coordinates.
controllerIsGroundedint0 / 11 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 recordMode filters samples. With MovementAndEvents or EventsOnly, stationary frames produce no row. Don't assume every UpdateLoop.csv frame has a matching Participant row. Use left joins; forward-fill with pandas .ffill() if you need a dense series.
  • useEulerAngles changes column count. Seven quaternion columns per pose become six Euler columns. Always read the header line rather than hard-coding indices.
  • Quaternion sign ambiguity. q and -q are the same rotation. Numeric differences between rows don't equal angular distance. Convert via 2 * 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/Z comes from Unity's CharacterController, not from differencing positions. For head velocity you have to derive it yourself from successive cameraPos rows.

Analysis recipes

  • Gaze paths — often combines camera pose from Participant with gaze direction from Eye.
  • Cross-device sync — use MonotonicExecutionTime to align head-pose samples with external motion capture.