Simam AI Lab Our applied AI research division is now open. Visit the lab
Simam Digital product / Real-time 3D and XR

Replaying a match in 3D, from any angle, in a browser tab.

Simam XR turns positional tracking data into a match you can walk around — broadcast, tactical, pitch level, or a player’s own eye line. It runs in a browser tab and enters VR or AR on a headset or phone. Nine sports arrive through one data contract, because every provider quirk is normalised at the edge and the engine never sees it.

A football match rendered in 3D from a broadcast camera angle, with live telemetry, possession and camera preset controls overlaid
Nine sports and twenty-nine venues through one internal model
Live wire cost of 143 bytes per frame, behind the same interface as replay
Around twelve thousand spectators in a single draw call
Flat screen, VR and AR from one build, reached by a link
The decision everything rests on

Every tracking provider disagrees about almost everything.

Sample rate, units, pitch dimensions, where the origin sits, which axis points where. StatsBomb gives freeze-frames at events. Tracab and Metrica give continuous 25 Hz. Some measure in yards from a corner, others in metres from the centre spot. A renderer that knows about any of that becomes unmaintainable the moment a second provider arrives.

So none of it reaches the renderer. Each format has an adapter that normalises at the edge into one internal model — metres, origin at the centre circle, +X toward the right goal — and the engine only ever sees that. Adding a sport or a provider touches an adapter and nothing downstream. It is the single decision the whole project rests on, and it is why nine sports ship through one contract rather than nine integrations.

Stage 01

Adapter

Vendor quirks die here. Units, origin, axis convention and sample rate are resolved once, at the boundary, and never again.

Stage 02

Worker

Parsing runs off the main thread. In a browser you have one thread and roughly sixteen milliseconds — parse on it and the world stops.

Stage 03

Interpolator

25 Hz source to 60 fps output, driven by wall clock rather than frame count, so playback speed and seeking stay honest.

Stage 04

Render loop

One frame callback that mutates objects in place. State changes are event-driven only — play, pause, seek, camera switch.

The invariant that keeps it at frame rate: zero React or store writes per frame. React exists to re-render on change, and a 3D scene changes every frame — so the scene graph is held outside it entirely, and positions are written into flat Float32Array buffers rather than arrays of objects, which keeps interpolating many entities cache-friendly and allocation-free.

The build in use

Seven cameras over the same instant.

The camera set is the product. A tactical top-down view answers a different question from a pitch-level one, and being able to cut between them mid-replay — on the same moment, with the same data underneath — is the thing a flat highlight reel cannot do.

What actually proved hard

It was almost never the rendering.

Three failures worth recording, because each one cost real time and none of them was where it looked.

The instrument was lying

A verification tool that quietly agreed

Animation was signed off for weeks from headless renders of a posed frame. Blender 4.4 introduced slotted actions: assigning an action no longer binds it without also assigning the slot, so the rig silently rendered its bind pose. That does not look like an error — it looks like a person standing there. Caught only by printing a bone’s quaternion and finding an identity rotation where a running leg should have been.

One thing still rendered

Building the stadium hid the pitch

Adding the bowl turned the pitch black in every preset. The near stand’s roof sat at 26 m and the broadcast ray from [0, 30, 62] crossed that height behind the roof line. It read as a lighting bug because the shirt-number labels draw with depth testing disabled, so numbers still hovered exactly where players should be. Fixed by leaving the camera-side stand roofless and pushing the bowl back to 12 m — and the sight line is now asserted in tests.

The premise was false

Three fixes behind a wrong theory

An arm swung up beside the head at the top of the stride. Three attempted fixes all computed motion as a delta from the source rig’s rest pose. The source files are animation only — no mesh, therefore no bind pose — so the reconstructed rests from two clips of the same rig disagreed by 105 to 141 degrees. The formula had been subtracting a fiction. Aiming each bone directly at its counterpart’s direction instead is rest-agnostic by construction: residual aim error 0.00 degrees.

The transferable lesson: a broken test fails loudly, but a broken measuring instrument just quietly agrees with you. Before trusting a verification tool, make it fail on purpose and check that it says so. A wrong theory with three fixes behind it is more expensive than no theory, because each fix makes the premise underneath look better established.

The part we would want judged

Numbers that are measured, and numbers that are not.

The Match Centre shows possession, shots, expected goals, passes, corners and fouls. All of it is generated — produced from team strength and the scoreline through a seeded random number generator, because a demo fixture has no event feed behind it. It is plausible, deterministic and entirely invented.

That is defensible in a preview. What is not defensible is letting it sit indistinguishably beside numbers that are real, because the invented ones then borrow the credibility of the measured ones — and lend them their own status the moment anyone finds out. So the derived statistics live in a separate tab that states what they are computed from: distance covered, top speed, sprint counts, high-intensity distance, and collective shape.

Sampling

Sample at the data’s own rate

Finer than the source integrates our own interpolation and inflates distance. Coarser cuts the corner off every change of direction and under-reports it. The rate comes from the source’s declared frame rate — 12.5 Hz on one real match, 20 Hz on a synthetic one.

Outliers

Reject implausible steps, never clamp them

One glitched frame produces an instantaneous speed in the hundreds of km/h, and because top speed is a maximum rather than an average, that single sample becomes the headline. Faults are dropped and the path re-anchored. Clamping would credit a sprint the player never ran.

Missing data

Return NaN, never zero

A metric that cannot be computed honestly returns NaN. A missing defensive line reported as 0 reads as “the line was on halfway”, and nothing downstream can tell that apart from a real measurement.

A parallel strand explored reconstructing a team’s shape from GPS vests. Real vest data has no ground truth, so before building any of it a simulator produced a team moving as a tactical system with exact known positions, then degraded that truth the way a real vest would. The decisive choice was modelling GPS error as correlated drift rather than random noise — real satellite error wanders, and random noise would have averaged out across four defenders and made the defensive line look far more precise than it can ever be. Every metric was then run against the truth it came from:

2.60 m against a 4.0 m budget

Gap: defence to midfield

Shipped.

2.85 m against a 4.0 m budget

Gap: midfield to attack

Shipped.

4.03 m against a 5.0 m budget

Team depth

Shipped.

4.06 m against a 3.0 m budget

Defensive line height — withheld

It decomposes into 3.77 m of constant bias and only 1.52 m of scatter, so the metric beats sensor noise comfortably and the fault is a registration problem: fitting a pitch rectangle to one team’s positions centres it on that team’s centre of mass, not the pitch centre.

5.09 m against a 5.0 m budget

Team width — withheld

Missed its budget and was kept out of the interface rather than shown with a caveat.

The guard on the guard

A test that asserts they still fail

When someone fixes the registration, that test breaks and tells them to remove the entry — otherwise a known-issues list quietly becomes a rubber stamp.

Stated plainly: a number with no error bound is not a measurement, it is a decoration. Telling a coach “the line broke” when that is indistinguishable from sensor drift is worse than telling them nothing, because they will act on it.

A property no ordinary test would catch

The analytics SDK is not downloaded before consent.

Measured in the running application before any choice is made: zero cookies, zero stored keys. The SDK is not merely inert before consent — it is never fetched, because it sits behind a dynamic import inside the function that consent gates.

That is an architectural property. It holds because of how the imports are arranged, and a single careless static import in an unrelated file, or one analytics snippet pasted into the HTML shell, would undo it completely and silently. No behavioural test would notice.

Enforced by reading the source

A test that inspects imports, not behaviour

The SDK may only be imported for types or dynamically, no module but the analytics one may reference it at all, the HTML shell may contain no third-party script, the consent guard must exist, and only two storage keys may ever be written.

Then broken on purpose

A guard you have never seen fail is not a guard

A tag-manager snippet was pasted into the shell and the suite re-run. It failed, as it should. The same exercise surfaced a real gap: consent could be given but not withdrawn, and the policy’s answer was “clear this site’s data”, which is not as easy as giving it. Withdrawal now lives in the policy and stops collection rather than merely stopping the asking.

The platform decision

Why a browser, and what it costs.

The most common question about this project is why it is not built in Unity, and the honest answer is that Unity would probably look better. Motion matching, level-of-detail systems, baked lighting, a real asset pipeline, a profiler that works — every hard problem here has a mature solution sitting in an engine we chose not to use.

What it costs

One thread, and 2012 graphics

Roughly sixteen milliseconds on a single thread, so parsing moves to a worker or the world stops. WebGL 2 is OpenGL ES 3.0 — no compute shaders — so anything repeated must be instanced. No asset pipeline, which is a real part of why the venues read as procedural. Manual GPU memory discipline, because nothing garbage-collects a texture. And React held sideways.

What it buys

A link

No install, no store, no per-platform build. WebXR means flat screen, VR and AR are one build rather than three targets. A native engine’s advantage is largest at the photoreal end; down in stylised low-poly geometry it narrows sharply, and most of what is still wrong with how this looks is lighting and animation quality, neither of which the browser is what blocks.

The trap worth naming: it is easy to choose the platform that makes the demo most impressive rather than the one that gets the demo in front of the most people.

What is not finished.

A case study that only lists wins is a brochure. The build is in beta, and these are the open items we would raise ourselves before anyone else did.

Unmeasured

The frame rate target

Sixty frames per second on a mid-tier laptop is the stated target and it has not been measured on real hardware — the preview environment throttles the frame loop, so any number from it would be an artefact of the harness. Until it is measured we will not claim it, which is uncomfortable, because it is the headline property of a real-time engine.

Wrong model

Blending, where motion matching belongs

The gait blends between three clips by speed. It is the standard approach and it works, but it is why the movement is acceptable rather than right. Motion matching — choosing the pose that fits the movement instead of averaging toward it — is the upgrade with the best ratio of payoff to effort, and it is a rewrite rather than a fix.

Deferred too long

Bundle size

Over 1.6 MB in the main chunk with a second larger again, and no meaningful code splitting: opening the landing page downloads the analysis tooling, the XR support, the audio engine and eight sports nobody is looking at. Deferred every time because it never blocked anything, which is exactly how it got this bad.

Coverage gap

No visual regression testing

Everything visual is verified through geometric proxies — is the camera outside the structure, does the sight line clear, do two elements overlap. Those are good tests and they have caught real defects. Nothing in the suite would notice if the pitch turned purple.

Found by measuring the DOM

Mobile needs a touch pass

The telemetry HUD is fixed at z-index 24; the transport bar wraps to two rows on a phone at 122 px tall, and the HUD offset had been set when that bar was one row. They overlapped by 64 px, precisely where the play button and speed control sit. Both were on screen, visible in a screenshot, and untappable. Only measuring the DOM found it — looking would not have.

Parked on judgement, not difficulty

The GPS team-shape strand

A tested beta on its own branch, stopped on a product call: vests give you no ball and no opponents, so they can support analysis of a team’s own shape but never a faithful replay of a match.

And one that measurement could not settle. The landing page briefly ran a dusk lighting preset. Every value was correct and verifiable at runtime — key light placed, four floodlights emitting where there had been none, the match view provably unchanged. Then it looked too dark, and it went back to daylight. An hour reverted, and the clearest boundary in the project: measurement establishes whether something is correct, and says nothing at all about whether it is good.

Case study decision record

The commercial case, in one view.

What the project set out to prove, what it demonstrates, and what it deliberately does not claim.

Business challenge
Positional tracking data is collected for almost every professional match and is read by almost nobody, because it arrives as numbers. Turning it into something a fan, a coach or a rights holder can look at has historically meant a broadcast graphics budget and a per-platform application.
Why the project mattered
It tests whether a browser is now a serious enough platform to carry a real-time 3D product with an audience — and whether one engine can serve nine sports without becoming nine engines.
What Simam Digital designed and built
Simam XR: a real-time 3D match engine in TypeScript and Three.js via React Three Fiber, with per-provider adapters, off-thread parsing, wall-clock interpolation from 25 Hz to 60 fps, seven camera presets, procedural venues with an instanced crowd, derived player and team analytics, a live binary transport, and WebXR for VR pitchside and AR tabletop from the same build.
Important engineering decisions
Normalise every provider quirk at the edge so the engine sees one model. Never write React or store state in the render loop. Hold entity state in flat typed arrays rather than objects. Put the live transport behind the same interface as replay, at 143 bytes per frame. Separate generated statistics from measured ones in the interface, and gate the analytics SDK behind a dynamic import that a source-reading test enforces.
Delivered
A deployed, publicly reachable build covering nine sports and twenty-nine procedurally generated venues, with 564 unit tests across 61 files, an instanced crowd of roughly twelve thousand spectators in a single draw call, zero React state writes per frame, and zero cookies or stored data before a visitor consents.
Honest limitations
Marked beta. The 60 fps target is unmeasured on real hardware. Match Centre statistics are generated rather than measured, and are separated in the interface for that reason. Two team-shape metrics missed their error budgets and are withheld. Locomotion blends clips rather than motion matches. The bundle is oversized and uncodesplit. There is no visual regression testing.
Where it goes next
Measure the frame rate on real hardware and publish it. Motion matching for locomotion. Code splitting by sport and by feature. A touch pass on mobile rather than more media queries. And, commercially, a real event feed behind the Match Centre so the generated statistics can be retired rather than merely labelled.

Credits. Built by Junaid Malik. Inspired by the BBC and Immersiv.io 3D match experience. Demo match data © SkillCorner opendata, CC BY-NC.

Have data nobody can read, and an audience who would look at it?

The valuable asset here is not the renderer. It is the demonstrated path from an incompatible vendor feed to a person on a phone watching a match from an angle no camera was in — and the discipline of never letting a generated number sit next to a measured one.