Head tracking for games from an iPhone. No infrared camera, no clip on your headset, no app to build yourself, nothing to buy.
"NoIR" — as in no IR. That is the whole pitch: the infrared hardware is the part you don't need.
Turn your head, the camera in the game turns with you — American Truck Simulator, ETS2, DCS, MSFS, anything that speaks TrackIR or FreeTrack.
Set up, calibrated and played in American Truck Simulator. What is not claimed here is a latency figure: five attempts to measure it with a webcam all had error bars wider than the answer, and the honest summary is "the author drives with it and finds it fine", not a number. See Latency, honestly.
An IR head tracker costs $130–170. The sensor to replace it is already on your desk: every iPhone with Face ID has a TrueDepth camera that tracks your head 60 times a second, and Epic gives away the app that streams it — Live Link Face, free on the App Store.
That stream is used by animators for facial capture. Past the 52 face shapes it also carries three numbers nobody in the simulator world seems to have picked up: head yaw, pitch and roll. NoIR reads them and hands them to opentrack, which every TrackIR-aware game already knows how to listen to.
~200 lines of Python between a free app and a free relay.
noir.py — Live Link Face |
ifm.py — iFacialMocap |
|
|---|---|---|
| phone app | free (Epic, App Store) | paid, about $8 |
| turn / tilt your head | yes | yes |
| lean in, peek around | no | yes |
Live Link Face carries no head position, and that is not fixable in code. Its packet is exactly 61 floats — 52 blend shapes, three head angles, six eye angles — and a capture from a real phone is 312 bytes, which is the header plus those 61 and not one byte more. There is nowhere for position to hide.
ARKit itself knows where your head is; Epic's app simply does not send it.
iFacialMocap does, in a documented text
protocol, so ifm.py speaks that instead and gives you the lean.
Before buying anything, try HeadLink — 6DOF straight into opentrack with a free 48-hour trial. If it suits you, none of this is needed.
Windows only on the PC side. The phone can be an iPhone X or newer, or — since June 2025 — an Android with Snapdragon 8 Gen 1 or later, which includes the Samsung S23 Ultra, S24 and S24 Ultra. Epic put Live Link Face on Google Play, so this is no longer an Apple-only trick.
Double-click install.bat. It checks what is missing, runs the self-test and
puts four shortcuts on your Desktop. No administrator rights, nothing
system-wide, nothing but shortcuts you can delete.
Then start with Head tracking – SETUP. It measures where your phone is placed before anything else and refuses to continue if the phone cannot see your face well enough — because that is the failure that no later setting can make up for. Printable one-page guide with the diagram: docs/phone-placement.pdf.
Doing it by hand instead
-
opentrack — install it, then set Input =
UDP over network(port 4242), Output =freetrack 2.0 Enhanced, and Filter =Hamilton.The filter is not a detail, and the size of the head movement changes the answer by two orders of magnitude. Measured into the freetrack shared memory the game reads, medians of nine trials:
40° snap 3° correction Hamilton2.2 ms 2.0 ms Accela(opentrack's default)63.5 ms 368 ms Accelais nonlinear on purpose: it passes big movements and crushes small ones to kill jitter. Benchmark it with a 40° snap and it looks like a 60 ms tax. Use it the way a driver actually uses head tracking — small, constant corrections — and it is a third of a second behind you. The single large-step number in the first version of this table hid that completely.EWMA(12.8 ms at 40°) is the fallback ifHamiltonfeels jittery. -
Live Link Face on the phone — Settings → Live Link → Add Target → your PC's IP. Keep Capture Mode on ARKit (MetaHuman Animator streams something else entirely).
-
NoIR:
py noir.py --calib-sec 3Sit straight for those three seconds — it learns your resting pose, so the game camera does not sit permanently off-centre.
-
Press Start in opentrack. The octopus should follow your head.
-
Launch the game. In ETS2/ATS, head tracking is on by default once opentrack is running.
Order matters. The game hooks opentrack when the GAME starts. Phone first, then the tracker, then opentrack's Start, and only then the game.
Put the phone IN FRONT of you — under or over the monitor, lens on your face. Not off to one side. Measured on a real rig: moved from beside the monitor to in front of it, the reading at rest went from 74° of wander to 7°. Calibration removes a constant offset; it cannot remove wander, and from the driver's seat the two look identical.
If an axis still runs backwards, use --flip-yaw, --flip-pitch or
--flip-roll. The defaults are correct for the placement above — settled in a
truck cab, not derived — and this version of opentrack has no per-axis Invert
checkbox to fall back on.
If nothing arrives, run py noir.py --dump. Ninety percent of the time it
is Windows Firewall blocking Python, or the phone being on a different Wi-Fi
band (2.4 vs 5 GHz on the same router still counts as the same network, guest
networks do not).
| Flag | What it does |
|---|---|
--calib-sec 3 |
learn your resting pose. Recommended, off by default |
--gain N |
how much further the camera turns than your head |
--expo N |
above 1 keeps the centre calm, saves the reach for big turns |
--pitch-gain N |
up/down only. ARKit reports far less pitch than yaw |
--pitch-down N |
extra multiplier for looking DOWN only |
--deadzone N |
degrees to ignore, so gain does not amplify the wander |
--deg N |
degrees per raw unit (default 45) |
--stats-sec N |
print your measured range every N seconds |
Pitch needs its own numbers, and the data says why. Measured over 30 s of normal driving on a real rig: yaw moved through 25°, pitch through barely 9°. A head simply does not tip as far as it turns, and ARKit reports even less of what it does. With one gain for all three axes, looking down never reaches the dashboard. A working starting point:
py noir.py --calib-sec 4 --gain 4 --expo 1.5 --pitch-gain 5 --pitch-down 1.6 --deadzone 1.5
--deadzone matters more than it looks: the tracking wanders a couple of
degrees even when you sit perfectly still, and gain multiplies that wander as
faithfully as it multiplies a real turn.
Not measured to a standard worth quoting. Five attempts with the built-in webcam produced 175, 213 and 226 ms, plus two runs discarded for method errors — but each attempt needed a correction for the webcam's own lag that was larger than the signal being measured, so none of them is rigorous.
What is solid: this program's own share is 2.2 ms, measured into the freetrack shared memory a game reads. Everything else is ARKit on the phone and the network, and published figures for ARKit put movement-to-pose around 140 ms. So the honest position is: the delay is real, it belongs to the phone, and no amount of code here will change it.
Whether that is acceptable is a question about you, not about a number. It was acceptable to the author, in a truck, on a small monitor. If you need certainty, film your head and the monitor with a single second camera and count frames: one lens sees both events, so its own lag cancels and no correction is needed.
Shape the response curve in opentrack, not here. NoIR deliberately sends honest degrees — amplifying in two places at once is how you end up with a camera that snaps and no idea which knob caused it.
--stats-sec prints the range your head actually covers. The scale of the raw
Live Link values is not documented anywhere reliable, so 45°/unit is an
estimate; measure yourself and set --deg to what you see.
py noir.py --selftest # packet layout, parser, garbage rejection
py test_noir.py # end-to-end, with neither phone nor opentrack
test_noir.py runs the simulator into the tracker and decodes what comes out
the far end, asserting the angles sweep by the amount driven. A stream of
well-formed zeros passes a format check; it does not pass this.
- opentrack PR #1423 adds Live Link as a native opentrack input. Open since February 2022, unmerged — using it means building opentrack yourself.
- iOSHeadTrack — gyroscope based, so the phone is strapped to your head; needs a Mac and Xcode to build.
- AITrack — webcam and a neural net. No phone at all. If you do not own an iPhone, start there.
- Eyeware Beam and SmoothTrack do this well and are paid, closed-source apps.
NoIR's only claim is the cheapest path in: an app already in the App Store, a relay already installed, and a script you do not have to compile.
The opentrack UDP input packet is 48 bytes — six float64 in host byte order —
and the field order is x, y, z, yaw, pitch, roll. Translation first.
Most public documentation and forum posts state the opposite order, angles
first. That layout belongs to opentrack's UDP output. Ground truth is
tracker-udp/ftnoir_tracker_udp.cpp,
where the six doubles land in the pose array unchanged and rotation is read
from index 3. Get it backwards and nothing errors — opentrack's octopus simply
never moves. There is a self-test pinning this down.
MIT. Live Link Face is Epic's app and is neither included nor modified here — NoIR only receives the UDP it broadcasts. opentrack is a separate GPL program that NoIR talks to over a socket, not a library it links against.