Running the Sample
The UDP Body Tracking sample ships a synthetic full-body recording and a Python sender. It needs no hardware, no packages, and no configuration — the UDP row's defaults already match it.
Run it first, even if you plan to write your own sender. It gives you a known-good baseline: once the sample animates a character correctly, anything that goes wrong with your own data is your data and not your setup.
1. Import it
In Unity: Window → Package Manager → LABO → Samples → UDP Body Tracking → Import.
2. Move it out of your project
The Package Manager puts samples under Assets/. That is the wrong place for this one, and LABO will offer to move it for you — a dialog appears once the files land.
Choose Location… and pick a folder outside your Unity project. Somewhere alongside it works well:
MyUnityProject/
MyUnityProject-UdpSample/ <- here
Why it matters: Unity imports everything under Assets/ into its asset database. The sample is a few megabytes of recordings and Python that Unity has no use for, so leaving it there costs reimport time, clutters asset searches, and — if you ever regenerate the recordings — makes Unity reimport every file each time. The scripts run from a terminal, so they work identically anywhere on disk.
The dialog's other options are Keep in Assets, and Keep and Don't Ask Again if you would rather not be asked on future imports. To bring the prompt back later: LABO → Samples → Reset UDP Sample Relocation Prompt.
If you dismissed the dialog, just move the folder in your file manager and delete the leftover from Assets/ in Unity's Project window. Nothing in the sample refers back to the Unity project.
Once it moves, a second dialog shows the new location and the two commands below. Open Instructions opens the sample's own README.
3. Set up Python
The sample needs Python and nothing else — no packages, no virtual environment. If python --version already prints a 3.x version, you are ready.
If not, see Python Setup.
4. Run the test
In Unity:
- Open LABO → Windows → Data Capture → Body.
- Set the tracking row's provider to UDP.
- Leave every setting at its default.
- Turn on Gizmos → Joints and Bones so you can see the skeleton.
- Enter Play mode.
Then, in a terminal, from the sample folder:
python stream_imu.py
Order matters. Play mode first, then the sender. LABO takes its calibration reference from the first packets that arrive, and the recording opens with five seconds of the figure standing still for exactly that purpose. Start the sender first and LABO joins mid-dance, taking its reference from whatever pose it happens to catch.
5. What you should see
The tracking row turns green on the first packet. Then:
- Five seconds standing still, arms at the sides. This is the calibration hold.
- Y — both arms up and out in a wide V.
- M — elbows bent hard, hands in toward the shoulders.
- C — arms sweeping to one side, trunk leaning with them.
- A — both arms straight overhead.
- The letters again, then back to standing.
If it spells Y-M-C-A, everything is correct. That is what the letters are for. Most ways of misreading pose data still produce plausible-looking motion — a coordinate error bends joints the right amount in the wrong direction, a calibration error adds a constant offset — and watching a generic reaching motion will not tell you. Letters are unambiguous. Getting all four right means the packet format, the coordinate conversion, the calibration reference and the calibration pose are all correct together.
If it does not, go to Troubleshooting.
6. Add an avatar
With the skeleton confirmed, assign an Avatar Prefab in the Body tab's Avatar view and run the sender again. The character dances. See Avatars.
Running your own recordings
The same script streams any recording you have.
Interactively — point it at a folder and pick from a list:
python run_recordings.py path/to/my/recordings
It lists what it finds, and you type text to filter or a number to stream. It also finds the calibration trial in the folder and sends that first, so the reference lands on a real static hold. Run it with no argument to reuse the last folder.
With flags — for scripting, or when you know what you want:
python stream_imu.py --data path/to/recording.csv
python stream_imu.py --dataset path/to/folder --list
python stream_imu.py --dataset path/to/folder --trial DRINK
python stream_imu.py --host 192.168.1.42 # Unity on another machine
python stream_imu.py --loop # repeat until stopped
python stream_imu.py --data rec.csv --max-rate 120 # thin a fast recording
Both scripts print the Unity settings each recording needs — the Quaternion order and the Calibration pose — so you are not guessing.
Recordings logged faster than Unity reads
Sensor software often logs an order of magnitude faster than a game loop consumes. Unity applies at most one body frame per Update, so a recording logged at 1200 Hz streamed against a 90 Hz render loop has roughly thirteen of every fourteen packets overwritten before they reach a bone — and the surplus has to pass through the receive buffer first, where a burst can cost you samples you wanted rather than samples you did not.
--max-rate picks a stride that keeps playback at or below the rate you name:
python stream_imu.py --data rec.csv --max-rate 120
A 1200 Hz, 180-second recording sends every 10th sample: 21,578 packets at 120 Hz instead of 215,776 at 1199 Hz. Whole samples are dropped rather than averaged, so every orientation that arrives is one the sensor actually measured. Timing is unaffected — each sample keeps its original timestamp, and playback still runs the full 180 seconds.
Leave it off when the recording is already at or under about 200 Hz; the script prints a note if it sees a rate worth thinning.
Two layouts
The sample ships the same recording twice, because sources differ in how they lay data out:
| Layout | What it is | Run it with | Quaternion order |
|---|---|---|---|
| Processed | One CSV, four columns per sensor | python stream_imu.py | XYZW |
| Raw | One folder per sensor, as sensor software exports | python stream_imu.py --raw | WXYZ |
Both describe identical motion. Use whichever matches what your own hardware produces — the raw layout is the one to copy if you are building a live sender, because it passes the sensors' own component order straight through without rewriting it.