All posts
3D PipelineJuly 25, 20263 min read

Five Airframes, Zero Asset Files

Every aircraft in TaskFlight — 154 selectable parts, a 2.5 km runway, the lot — is procedural geometry built in code. No downloaded models, no asset licences, no CAD provenance questions, and a first load that got lighter as the fleet grew.

TaskFlight renders an AH-64D, a UH-60L, a CH-47F, an AH-6I and a Boeing 737-800, and not one of them came from a model marketplace. There is no glTF in the repository, no FBX, no downloaded asset of any kind. Every airframe is procedural: geometry constructed in code from primitives, lathes and extrusions, textured from a shared PBR material bank with per-airframe paint schemes. The five models carry 37, 30, 35, 25 and 27 selectable parts respectively — 154 components a student can click, isolate and open maintenance data for.

Why not just buy models?

  • Provenance. The AH-64 is an ITAR-controlled article. A marketplace model of unknown derivation is a question we would rather not have to answer in a government procurement. A training representation authored from public reference imagery, in code we wrote, has a one-sentence provenance story.
  • Licensing. No per-seat asset licences, no redistribution clauses to audit before an offline classroom deployment, nothing that complicates handing a customer a fully static build.
  • Selectability is the product. A purchased mesh is one object; we need every inspection-relevant component to be an individually pickable, isolatable, nameable part. Retro-fitting that onto someone else’s topology is more work than authoring geometry that is born segmented.
  • Diffs. Geometry in code is reviewable in a pull request. A binary asset is not.

The coupling is string ids — so it is validated

The 3D model and the maintenance database are coupled by nothing but string ids: a part mesh declares id="ah64_tail_rotor_gearbox" and the catalogue holds a record with the same id. Both failure modes are silent at runtime — an unmatched mesh is merely unclickable, an unmatched record merely unreachable — so the build runs a validator that cross-checks every id in both directions across all five airframes before Next.js starts. This validator has caught real defects, including an entire airframe’s catalogue that was authored but never imported, which no amount of clicking on the other aircraft would have surfaced.

The runway is procedural too — and measured

When the 737 joined the fleet it spawned on a 220 m helicopter ramp, which is not a thing you can take off from. The fix is a 2,500 m × 45 m runway with ICAO markings, a parallel taxiway with holding points and edge lighting — all procedural vertex-coloured geometry costing five draw calls. The length is measured, not guessed: the flight model reaches Vr at 1,053 m of roll and lifts off at 1,448 m, so 2,500 m carries the ~1,000 m margin a real narrow-body runway does. The correctness risk was the runway surface disagreeing with the terrain height function and floating or sinking the aircraft; both are built from the same constants, and 2,505 samples across the footprint deviate by 0.000 m — a 2 cm gear-to-terrain error across the entire takeoff roll.

Scale is data, not tuning

A fleet spanning a 1.4-tonne Little Bird and a 737 broke every camera number tuned to the Apache — framed with the trainer’s original presets, an airliner four times the Apache’s size sits mostly off-screen. Framing now derives from a per-aircraft orbit radius in the registry, with the Apache’s ratio landing at exactly 1.0 so the original airframe is provably unchanged. The same registry drives camera offsets in the simulator: adding a sixth aircraft means authoring its numbers, not re-tuning cameras.

Growing the fleet made the app lighter

Five airframes of maintenance content is 2.3 MB of source, and importing it statically put every airframe into the first load of both routes — a trainee works on one aircraft at a time and was paying to download four they never opened. The catalogue now reaches each airframe through a dynamic import, so each becomes its own chunk fetched on selection; concurrent callers share a single in-flight request, and a failed load stays retryable instead of leaving a permanently rejected promise behind.

/ (trainer) First Load JS
638 kB → 136 kB
/fly (simulator) First Load JS
660 kB → 158 kB
Original single-aircraft build
251 kB
External 3D assets
0
Five aircraft, lighter than one — after per-airframe code splitting