A static, browser-based 3D viewer compatible with VelociDrone and MRSIM tracks, built with Three.js and hosted on GitHub Pages. Styled after the official AUFPV / Mission Foods Australian Drone Nationals track posters: green grid mat (1 m / 5 m lines), khaki racing line, white Mission gates, feather flags and a checkered start.
Bundled tracks (dropdown, driven by tracks/manifest.json): 2024 AU NATS
Quali (6 gates, 6 flags, 1 elevated dive gate — validated against the
official 2D layout poster), 2022 Mission Foods Australian Drone Nationals,
Dutch Drone Madness 2021 Race 1, FAI World Cup Italy 2024,
2025 MultiGP European Champs and DDR Race Series – Track 5.
- 3D orbit view (drag to rotate, scroll to zoom, right-drag to pan)
- 2D layout button — top-down view matching the official poster orientation
- Race gates as 2 m square frames (white; dive gates red with yellow tips), flags as printed feather flags, checkered start/finish with crossed flags and a yellow direction arrow
- Optimised racing line through every gate aperture with an estimated race-pace lap time, plus a flat ground projection to read altitude
- Fly lap — camera flight along the racing line that brakes into corners and sprints the straights, following the optimiser's speed profile
- Layer toggles: gates, flags, sequence numbers, racing line, scenery, hidden checkpoints
- Track materials card (poster style): gate/flag/dive counts, lap length, course area, max altitude
- Multiple tracks via a dropdown (driven by
tracks/manifest.json) - Collapsible menus — click a card title (Layers / Track Materials / Legend) to minimise it to its title bar; the choice is remembered. On phones and other small screens the menus start minimised, the header switches to compact icon buttons, and touch targets are enlarged.
- Open track — view any VelociDrone
.trktrack file or MRSIM.xmltrack (header button or drag-and-drop it onto the page). Files are decoded entirely in the browser; nothing is uploaded anywhere. - Browse — search VelociDrone's ~2000 official public tracks and view any of them with one click (catalogue format documented by bolagnaise/vdrone-tracks; the AES-encrypted list is decoded client-side, downloads come straight from VelociDrone's public API).
Alongside the computed line, the viewer can overlay a real human racing line — the actual world-record flight from the VelociDrone leaderboard, drawn in cyan with the pilot's name and lap time, a WR lap button that replays it in first person at its true recorded pace, and a Fetch WR line button that pulls the current record live.
VelociDrone stores a leaderboard flight ("ghost") as
base64( zlib( .NET-BinaryFormatter( List<TransformRecord> ) ) ), where each
TransformRecord carries a position (Unity metres, same world space as the
track), quaternion, throttle and timing.
getLeaderBoard / getFlight take a single AES-encrypted post_data field
(key seed Bat Cave Games) and ghostfetch.js decodes the reply — zlib +
a small MS-NRBF reader — entirely in the browser.
One catch shapes the setup: the API drops its CORS header on responses over ~4 KB, so a browser can't read a flight (always ≥100 KB) cross-origin. Small responses keep the header, so the leaderboard works from anywhere — including GitHub Pages, no setup — fetched in 15-row pages that stay under the limit. Fetching a flight therefore goes through a CORS proxy (the request carries no account data — no email or login is needed). Two ways to have one:
-
Locally:
serve.pyserves the viewer and proxies/vd/*to VelociDrone on the same origin:python3 serve.py # http://localhost:8099 (viewer + /vd proxy) -
Hosted (GitHub Pages):
proxy-worker.jsdeployed as a Cloudflare Worker (free tier). The viewer defaults to this repo's instance (vd-proxy.skermiebro.workers.dev), so the hosted site works with no setup. To run your own instead — recommended if you fork this — deploy it withnpx wrangler deploy proxy-worker.js --name vd-proxy(or paste the file into a new Worker at dash.cloudflare.com) and put thehttps://<name>.<account>.workers.devURL into the proxy URL field under Human lines (shown whenever the viewer isn't on localhost, remembered in the browser). The worker only forwards the same endpoints serve.py allows, and nothing is logged.
The button appears for tracks whose online leaderboard id is known (any track
opened via Browse, plus the bundled AU NATS Quali).
protected_track_value is 1 for official tracks, 2 for user tracks.
Without serve.py, a line can also be prepared offline and bundled:
# in vd-ghost-capture/ : decode a captured getFlight payload
python3 ghost_decode.py out/021_getFlight.txt --which faster --out line.jsonghost_decode.py (uses the nrbf package) emits a compact
{pilot, lap_time, frames:[{t, p:[x,y,z]}]}. Drop it at
ghosts/<track-slug>-wr.json (e.g. ghosts/2024-au-nats-quali-wr.json) and
the viewer loads it automatically. ghost_extract.py instead pulls ghosts
from the local user11.db after a Nemesis race. Bundled WR lines: IQ0's
26.30 s lap for the 2024 AU NATS Quali and BMSThomas's 53.36 s run for
the 2022 Mission Foods Nationals (auto-loaded from ghosts/<track-slug>-wr.json).
Browsers block fetch() from file://, so serve the folder over HTTP:
cd track-viewer
python3 serve.py # viewer + /vd proxy (needed for live Fetch WR line)
# or: python3 -m http.server 8099 (everything except live fetch)
# open http://localhost:8099- Push these files to a repo:
git init && git add . && git commit -m "VelociDrone track viewer" git branch -M main git remote add origin git@github.com:<you>/<repo>.git git push -u origin main
- On GitHub: Settings → Pages → Deploy from a branch, pick
main/ root. - Live at
https://<you>.github.io/<repo>/. (.nojekyllincluded.)
Everything works on Pages out of the box: the live leaderboard is fetched straight from VelociDrone, and flight lines go through the bundled proxy worker (see Live fetch — deploy your own instance if you fork this).
The quickest way to view a track is Open .trk in the header (or drop the
file onto the page): VelociDrone's shared .trk files are AES-encrypted
(trk.js decodes them client-side, format documented in
FPVTracksideCore). Imported
tracks are session-only; to publish one on the site, export it as JSON:
Track layouts live in the VelociDrone user database
(~/.config/unity3d/velocidrone/velocidrone/user11.db, table tracks, column
value = plain JSON of gates + barriers). Export with:
python3 export_tracks.py --list # list recent tracks with ids
python3 export_tracks.py 2864 2863 # export these ids into tracks/ + manifestCommit the new tracks/*.json + tracks/manifest.json and push.
MRSIM (Multi Rotor SIM)
tracks are plain-XML <Simulation> scene graphs: nested <Transform>s place
entities that <Include> library objects (5x5Gate.xml, Flag.xml,
7x7Mat.xml…) or <Instance> macros (PaddedPole, PipeDoubleCube…).
Coordinates are metres, right-handed Z-up; rotations are axis-angle
attributes (rz="-1" angleDegrees="110").
mrsim.js walks the scene graph and renders each object's collision
primitives — the thin <Box>es are exactly the gate fabric panels and the
<Cylinder>s the PVC pipes / padded poles, so every object type (gates,
pole cubes, ladders, hurdles, dive gates…) draws faithfully with no
per-type code. The <CheckpointList> names (trkCube1.lower.backEntry)
resolve to <Checkpoint> reference points and crossing directions, so the
racing line is exact — including multi-pass elements and cube entries/exits.
The library objects are embedded in mrsim-lib.js — the object geometry
(collision-primitive coordinates + sizes) needed to place and draw any MRSIM
track.
MRSIM's rendered meshes and textures (gates, flags, mat, terrain, drone frames, logo…) are not redistributed with this project — they are the developer's copyrighted art. The hosted viewer draws every MRSIM object from those collision primitives instead, which is faithful (the thin boxes are the fabric panels, the cylinders are the PVC/poles). See Assets & attribution.
- Positions: integer centimetres, Unity left-handed Y-up. Imported as
(x/100, y/100, -z/100)so the three.js top view matches the poster. - Rotations: quaternions stored as
int × 1000in (w, x, y, z) order, imported as(-x, -y, z, w)(handedness mirror). Under this reading every gate/flag is an upright yaw and the 2024 dive gate comes out 22.5° from horizontal — matching the "≤ 40°" rule on the poster. - Scale: percent, relative to each prefab's native size. Race gates render at a fixed 2 m × scale; checkpoint prefabs (scale 10000) are invisible in-game and hidden behind a toggle here.
- Prefab origins are at the object's base, not its centre.
tracks/prefabs.jsonis the game's own prefab catalog (settings.dbtrackprefabs: 3394 prefabs with name/type/gate-flag, plus scene titles). Objects classify from it: typeInvisible→ hidden checkpoint,Tools→ editor helpers (not rendered), names containingFlag→ flags.models/*.glbare the actual in-game meshes (gates, flags, hurdles, trees, cones, rocks, gazebos, banners… ~250 prefabs). Stretchable blocks/nets/neon generate their meshes at runtime in-game, so they render procedurally using the exact material colours (all 13 block colours, name-derived neon tints, sphere primitives).tracks/prefab-dims.jsonholds the real local-space bounding box + a representative colour for every prefab in the catalog, so scenery without a shipped mesh still draws as a correctly sized, category-tinted box instead of a raw-scale cube. Particle-only prefabs (fog, smoke) are skipped entirely.- Click any object in the viewer to open the inspector with its raw database values (position, quaternion, scale, prefab id/name) and derived info.
This is a non-commercial, fan-made track viewer. It is not affiliated with, endorsed by, or associated with VelociDrone or its developers, or MRSIM / Multi Rotor SIM or its developers. "VelociDrone", "MRSIM", their names and logos, and all in-game 3D models, textures and other art are the property of their respective owners.
The viewer works by reading each game's file formats from your own licensed copy of the game (and public, already-shareable data such as the community track catalogue and public leaderboard times). What that means for game art:
- MRSIM meshes and textures are not redistributed here. MRSIM objects render procedurally from their collision-primitive geometry.
- VelociDrone: the bundled example tracks include a small subset of VelociDrone scenery meshes needed to display those tracks; objects with no shipped mesh are drawn procedurally.
If you are a rights holder and would like something changed, please open an issue.
| File | Purpose |
|---|---|
index.html |
UI shell, poster-style cards, import map |
app.js |
Three.js scene, track builder, fly-through |
raceline.js |
Racing-line optimiser (TOGT-inspired) |
trk.js |
VelociDrone .trk decoder/encoder (base64 + AES-128-ECB, pure JS) |
mrsim.js |
MRSIM .xml track decoder (scene-graph walker) |
mrsim-lib.js |
Embedded MRSIM object library (generated) |
test/ |
Viewer test suite (npm test, node:test) |
ghostfetch.js |
Live leaderboard/flight fetch + ghost decoding (zlib + MS-NRBF) |
serve.py |
Local dev server: static files + same-origin /vd proxy |
proxy-worker.js |
Personal Cloudflare Worker proxy for flight fetch on a hosted viewer |
ghosts/*.json |
Bundled human WR lines ({pilot, lap_time, frames}) |
tracks/manifest.json |
Tracks shown in the dropdown |
tracks/*.json |
Exported layouts (meta + gates + barriers) |
export_tracks.py |
Export tracks from the VelociDrone user DB |
ghost_extract.py |
Pull ghost lines from the local VelociDrone user11.db |
.nojekyll |
Serve files verbatim on GitHub Pages |