Case study
SJDEC Digital Twin
The real-time layer of the digital twin built for the SJ Digital Experience Centre — pins that stay readable at city scale, procedural zones and live traffic drawn from APIs, on Unreal Engine 5 and Cesium.
The brief
Live building and city data, legible in one real-time twin.
The SJ Digital Experience Centre (SJDEC) is where SJ Group shows its clients what its digital tools can do. The SJDEC Digital Twin was built for it — one application that brings the group’s digital-twin work together in a single real-time experience. It renders on Unreal Engine with Cesium geospatial streaming and takes live building-system and city data over MQTT and HTTP, so at any moment thousands of data points can be competing for the same screen.
As Real-Time 3D Developer Lead I owned the layer between that data and the screen: how pins, zones and traffic read at every distance and how data reaches the engine without stalling the frame. I also built editor tools for the team. The codebase was shared with the client’s own developers, and I reviewed and merged their pull requests into the integration branch.
What I delivered
Five systems between the data and the screen.
- Pins that stay readable at any density. A native Slate pin renderer driven by a single projection pass per frame, with a declutter solver that groups, moves or fades overlapping pins so the scene never collapses into a pile of labels.
- Live traffic from APIs. Fifteen live data streams — road flow, incidents, road works, transit, car parks and weather among them — drawn on the Cesium globe with custom flow shaders and congestion coloring.
- Zones you can read from anywhere. Zone footprints built as meshes at runtime from the vertex coordinates in the data feed, plus a striped highlight volume that marks where it cuts through the scene — both shaded by custom C++ material nodes.
- A data pipeline that doesn’t stall the frame. Background parsing, game-thread hand-off and budgeted spawning in the engine, plus a batched data loader on the backend API — startup data went from minutes to seconds.
- Editor tooling for the team. An editor plugin that imports asset mappings from JSON straight into Blueprint variables, alongside an object-outline plugin used across the project.
Pins & decluttering
One projection pass, three ways to declutter.
The work started with two prototypes on world-space widgets: screen-space avoidance that pushes overlapping widgets apart by priority and procedural callout lines that tie each widget back to its actor. Both gave way to a dedicated pin plugin, where pins are plain components with no tick of their own and one subsystem projects every pin to the screen in a single pass per frame. Its first renderer was an embedded browser; a native Slate renderer took over within two months.
On top sits a declutter solver that re-runs every eight frames, or as soon as the pin set changes. Each pin style picks one of three modes. Cluster merges overlapping pins into a count badge. Callout moves them to free screen space and draws leader lines back to their anchors — the clip at the top of this page shows both. Fade leaves every pin in place and dims the ones covered by nearer pins, with the nearest painted on top. Grace periods hold each merge and split until it’s stable, so badges don’t flicker as the camera moves, and members animate between their own position and the badge.
Live traffic
Traffic drawn straight from the feeds.
The traffic plugin subscribes to fifteen live data streams over MQTT and places them on the Cesium globe. Road segments go from latitude and longitude through the Cesium georeference into spline-mesh ribbons, each colored by its congestion band. Three custom material nodes, written in C++ and compiled to HLSL, draw animated flow along roads, scrolling chevrons along bus routes and solid rail lines with dashed links from stations to their exits.
New segments and markers are prepared in parallel and spawned under a per-tick budget, and most are keyed by a stable ID, so fresh data updates actors in place instead of respawning them.
Zones
Marking space without hiding it.
Two ways to mark a zone, both shaded by custom C++ material nodes. The zebra box runs flowing diagonal stripes across its vertical faces and uses scene depth to draw a bright line wherever it cuts through scene geometry. The animation lives entirely in the shader, so the component never ticks.
Zone footprints arrive as vertex coordinates in the data feed and become meshes at runtime: polygons with hundreds of vertices are triangulated with constrained Delaunay, extruded into prisms and anchored to the globe. Each zone renders either as an outline or as a fill. Outline edges hold a constant on-screen width at any distance, and in fill mode a post-process pass colors the zone by status using a per-pixel point-in-polygon test.
Built for production
Fast to start, light on every frame.
The numbers that mattered: how long the twin takes to get going, what it costs on every frame, how long the team spends on setup and how precisely zones land on the globe.
- Portfolio data that took 3–4 minutes to load now arrives in 3–5 seconds — a batched loader on the backend API replaces per-record queries, caches zone meshes and builds its trees in memory
- A 30–45 second freeze at startup removed: portfolio parsing and spawn preparation moved onto background tasks, with spawns drained from a lock-free queue under a per-tick budget — later generalized into a reusable action-queue plugin
- Thousands of per-widget ticks replaced by one projection pass per frame for every pin on screen
- 1–2 hours of manual Blueprint setup cut to 10–20 seconds by the JSON import editor plugin
- Geo-anchoring widened from single to double precision, removing up to 0.4 m of placement error
Stack