Real-time 3D visualization for ResQ autonomous drone swarms. A tactical dark-theme dashboard that streams live telemetry, mesh topology, hazard zones, and detection events at 10 Hz over SignalR — rendered in Three.js with a post-processing pipeline, procedural terrain, and a Unity-style free-fly camera.
- Live telemetry — SignalR WebSocket streaming at 10 Hz, ACES filmic tonemapping, PCF soft shadows
- 5 procedural terrain presets — each backed by a distinct noise algorithm (domain-warped FBM, ridged multifractal, island-mask, terrace+canyon, anisotropic dunes)
- Canvas-drawn tree billboard sprites — 5-tier pine silhouettes and deciduous blobs rendered with Canvas 2D; 8 triangles per tree, two draw calls for the entire forest
- Displaced boulder geometry — per-vertex hash displacement on IcosahedronGeometry gives every rock a unique craggy profile
- Geometry cache with deflate compression — browser-native
CompressionStream/DecompressionStream(RFC 1951); 572 KB → ~210 KB per preset (~63 % reduction); two-level L1/L2 cache survives page refresh viasessionStorage - Post-processing — selective
UnrealBloomPass(only emissive LEDs and nav lights glow),SSAOPassambient occlusion,OutputPasstone mapping - Unity-style camera — LMB orbit, RMB free-fly (WASD/QE/Shift), MMB pan, scroll zoom; collision prevention keeps the camera above terrain
- Drone interaction — click to select, WASD/QE to nudge in world space, click terrain to issue GoTo command,
Fto follow - Visual overlays — position trails, altitude halos, velocity component arrows, mesh topology links, hazard zone discs, detection markers
- Settings persistence — bloom, fog density, FOV, fly speed, trail length, detection rings, battery warning threshold; stored in
localStorage
git clone --recurse-submodules <repo>
cd viz/src/ResQ.Viz.Web
dotnet run # http://localhost:5000dotnet run compiles the TypeScript frontend through Vite automatically via Vite.AspNetCore — no separate npm run dev is needed.
flowchart TB
subgraph SDK["ResQ SDK (git submodule · lib/dotnet-sdk)"]
ENGINE["ResQ.Simulation.Engine\nphysics · pathfinding · weather"]
MAVLINK["ResQ.Mavlink\nMAVLink gateway · mesh routing"]
end
subgraph BACKEND["ASP.NET Core Host (src/ResQ.Viz.Web)"]
SIM["SimulationService\nIHostedService · 60 Hz loop"]
VFB["VizFrameBuilder\nsnapshot → VizFrame JSON"]
HUB["VizHub\nSignalR hub"]
REST["SimController\nREST API /api/sim/*"]
end
subgraph BROWSER["Browser"]
direction TB
SC["SignalR client"]
subgraph THREEJS["Three.js Scene"]
TER["Terrain + GeoCache\n5 preset algorithms"]
DRN["DroneManager\nInstancedMesh · LEDs"]
EFX["EffectsManager\nTrails · Halos · Hazards"]
OVL["OverlayManager\nVelocity · Formation"]
end
UI["HUD · DronePanel · WindCompass\nSettings · Controls"]
end
ENGINE --> SIM
MAVLINK --> SIM
SIM -->|"every 6th tick → 10 Hz"| VFB
VFB --> HUB
HUB -->|"WebSocket ReceiveFrame"| SC
SC --> TER & DRN & EFX & OVL & UI
UI -->|"fetch /api/sim/*"| REST
REST --> SIM
sequenceDiagram
participant Eng as ResQ.Simulation.Engine
participant Svc as SimulationService (60 Hz)
participant VFB as VizFrameBuilder
participant Hub as VizHub (SignalR)
participant CLI as Browser
loop every 16.7 ms
Eng ->> Svc : SimulationWorld.Step()
alt every 6th tick → 10 Hz
Svc ->> VFB : SnapshotFrame(world)
VFB ->> Hub : BroadcastFrameAsync(VizFrame)
Hub -->> CLI : ReceiveFrame(JSON)
CLI ->> CLI : DroneManager.update(drones)
CLI ->> CLI : EffectsManager.update(frame)
CLI ->> CLI : OverlayManager.update(drones)
CLI ->> CLI : HUD.update(count, time, battery)
end
end
flowchart LR
APP["app.ts\nentry · wiring"]
SCENE["scene.ts\nrenderer · raycasting"]
CAM["cameraControl.ts\nUnityCamera"]
POSTFX["postfx.ts\nbloom · SSAO · output"]
TERRAIN["terrain.ts\nheightmap · obstacles"]
PRESETS["terrainPresets.ts\n5 height functions\n5 GLSL biome shaders"]
GEOCACHE["geoCache.ts\ndeflate-raw L1 / L2"]
SPRITES["treeSprites.ts\nCanvas 2D billboards"]
DRONES["drones.ts\nInstancedMesh · LEDs\nselection · nudge"]
EFFECTS["effects.ts\ntrails · halos · hazards\nmesh links"]
OVERLAYS["overlays.ts\nvelocity arrows\nformation lines"]
CONTROLS["controls.ts\nREST panel · keyboard"]
UI["ui/\nhud.ts\ndronePanel.ts\nwindCompass.ts"]
SETTINGS["settings.ts\nlocalStorage"]
TYPES["types.ts\nVizFrame · DroneState"]
APP --> SCENE & TERRAIN & DRONES & EFFECTS & OVERLAYS & CONTROLS & UI & SETTINGS & GEOCACHE & PRESETS & TYPES
SCENE --> CAM & POSTFX
TERRAIN --> PRESETS & GEOCACHE & SPRITES
DRONES --> TYPES
EFFECTS --> TYPES
Five presets selectable at runtime from the sidebar. Each builds its own GLSL biome fragment shader, atmosphere (fog colour + density), obstacle distribution, and water level. Switching a preset disposes all Three.js objects and GPU resources, then rebuilds — using the geometry cache when available.
| Preset | Algorithm | Height range | Water level | Character |
|---|---|---|---|---|
| 🏔 Alpine | Domain-warped FBM | −60 … +220 m | −3 m | Sweeping ridges, snow caps, 4 mountain peaks |
| ⛰ Ridgeline | Ridged multifractal | −15 … +210 m | −15 m | Knife-edge ridges, dense conifer valleys |
| 🏝 Coastal | Island-mask × FBM | −∞ … +90 m | +3 m | Tropical archipelago, sandy beaches |
| 🏜 Canyon | Terrace + canyon cuts | −80 … +85 m | −60 m | Sandstone mesas, deep gorge networks |
| 🌵 Dunes | Anisotropic ridge noise | −25 … +60 m | −25 m | Wind-driven barchan dune fields |
flowchart LR
A1["Domain-Warped FBM\nQuilez 2002\nwarp scale 260 m\n6-octave final FBM"] --> P1["🏔 Alpine"]
A2["Ridged Multifractal\nMusgrave 1994\nsignal = 1 − |2n−1|\n8 octaves, gain 1.8"] --> P2["⛰ Ridgeline"]
A3["Island-Mask FBM\nradial falloff per island\n× 5-octave topo FBM\ncoastline perturbation"] --> P3["🏝 Coastal"]
A4["Terrace Function\nsmoothstep(0, 0.18, frac)\n+ noise threshold\ncanyon cut depth 80 m"] --> P4["🏜 Canyon"]
A5["Anisotropic Ridge Noise\ntent(n)^2.8 primary axis\n15° rotated secondary\nmega-dune field modulation"] --> P5["🌵 Dunes"]
subgraph SHARED["Shared noise primitives (terrainPresets.ts)"]
direction LR
H["_h(ix, iz)\nWang hash\nno float drift"]
N["_noise(x, z)\nquintic bilinear\nC² continuity"]
F["_fbm(x, z, oct)\nfractional Brownian motion\nlacunarity 2.09 gain 0.47"]
R["_ridged(x, z, oct)\nridged multifractal\nweight chaining"]
H --> N --> F --> A1 & A2 & A3 & A4 & A5
N --> R --> A2
end
Terrain vertex positions (572 KB per preset as Float32Array) are cached at two levels to make preset switches fast and page reloads instant.
flowchart TD
SW["switchPreset(key)"]
L1{{"L1 In-Memory\nMap · Float32Array\n~0 ms lookup"}}
MISS["Compute heights\n48 841 × terrainHeight()"]
UPLOAD["BufferGeometry → GPU\ncomputeVertexNormals()"]
FFW["fire-and-forget\nasync compress"]
CS["CompressionStream\ndeflate-raw RFC 1951\n572 KB → ~210 KB (−63 %)"]
SS[("sessionStorage\nbase64 string\n~280 KB per preset")]
INIT["geoCache.init()\nat app startup"]
DS["DecompressionStream\ninflate-raw\n~3 ms"]
SW --> L1
L1 -->|"hit"| UPLOAD
L1 -->|"miss"| MISS
MISS --> UPLOAD
MISS --> FFW --> CS --> SS
INIT --> SS --> DS --> L1
Five presets cached: ~1.0 MB in sessionStorage vs 2.8 MB uncompressed. Compression ratio is logged to the browser console at runtime.
Bloom is selective: a first composer pass blacks out all non-emissive objects so only drone LEDs, nav lights, and detection markers glow. A blend shader additively composites this onto the full scene render before the final OutputPass applies ACES filmic tone mapping and gamma correction.
RenderPass ──► UnrealBloomPass ──► ShaderPass (blend) ──► OutputPass
(bloom (emissive only) base + bloom.rgb ACES + gamma
composer)
RenderPass ──► ShaderPass (blend) ──► OutputPass
(final ↑ bloom texture ACES + gamma
composer)
viz/
├── src/ResQ.Viz.Web/
│ ├── client/
│ │ ├── app.ts Entry point — wires all modules together
│ │ ├── scene.ts Three.js renderer, camera, post-processing, raycasting
│ │ ├── cameraControl.ts Unity-style free-fly camera with terrain collision
│ │ ├── postfx.ts Selective bloom pipeline (two EffectComposer passes)
│ │ │
│ │ ├── terrain.ts Ground mesh, water, trees, rocks, buildings
│ │ ├── terrainPresets.ts 5 height functions + GLSL biome shaders + obstacle config
│ │ ├── treeSprites.ts Canvas 2D tree textures + cross-billboard geometry
│ │ ├── geoCache.ts deflate-raw geometry cache (CompressionStream / sessionStorage)
│ │ │
│ │ ├── drones.ts Quadrotor InstancedMesh, PBR materials, LED status, selection
│ │ ├── effects.ts Trails, hazard zone discs, detection markers, mesh links
│ │ ├── overlays.ts Velocity arrows, altitude halos, formation lines
│ │ ├── controls.ts Sidebar REST calls, scenario and command wiring
│ │ │
│ │ ├── settings.ts User settings with localStorage persistence
│ │ ├── types.ts VizFrame · DroneState · HazardState · DetectionState
│ │ ├── dom.ts Typed getEl<T>() helper
│ │ │
│ │ └── ui/
│ │ ├── hud.ts Top bar — connection, drone count, FPS, battery, selected chip
│ │ ├── dronePanel.ts Drone detail panel — position, velocity, battery, commands
│ │ └── windCompass.ts Canvas wind rose compass
│ │
│ ├── Controllers/ SimController — REST API
│ ├── Hubs/ VizHub — SignalR frame broadcast
│ ├── Models/ Request / response records
│ ├── Services/ SimulationService · VizFrameBuilder · ScenarioService
│ ├── styles/main.css CSS custom properties, glassmorphism panels, HUD
│ └── wwwroot/ Vite build output (committed for zero-install deploys)
│
├── tests/ResQ.Viz.Web.Tests/ xUnit + FluentAssertions + Moq
└── lib/dotnet-sdk/ Git submodule — ResQ .NET SDK
| Method | Path | Body / Params | Description |
|---|---|---|---|
POST |
/api/sim/start |
— | Resume simulation |
POST |
/api/sim/stop |
— | Pause simulation |
POST |
/api/sim/reset |
— | Clear all drones |
POST |
/api/sim/drone |
{ position: [x,y,z] } |
Spawn a drone |
POST |
/api/sim/drone/{id}/cmd |
{ type, target? } |
Send flight command |
POST |
/api/sim/weather |
{ mode, windSpeed, windDirection } |
Update weather |
POST |
/api/sim/fault |
{ droneId, faultType } |
Inject fault |
GET |
/api/sim/state |
— | Current drone snapshots |
GET |
/api/sim/scenarios |
— | Available scenario names |
POST |
/api/sim/scenario/{name} |
— | Load a preset scenario |
Flight commands — type field: hover · land · rtl · goto (goto requires target: [x, y, z])
Weather modes: calm · steady · turbulent
Scenarios: single · swarm-5 · swarm-20 · sar
| Input | Action |
|---|---|
LMB drag |
Orbit around target |
RMB hold |
Enter free-fly mode |
MMB drag |
Pan |
Scroll |
Zoom |
WASD |
Free-fly strafe / forward · Nudge selected drone (when RMB released) |
Q / E |
Fly up / down · Nudge drone altitude |
Shift |
×5 speed multiplier |
Click drone |
Select — opens detail panel, activates WASD nudge |
Click terrain |
Send selected drone to that world position |
Click selected drone |
Pass-through to terrain GoTo (re-click = GoTo) |
| Key | Action |
|---|---|
F |
Follow / unfollow selected drone |
Home |
Fit view to entire swarm |
V |
Toggle velocity component arrows |
H |
Toggle altitude halos |
G |
Toggle formation lines |
Space |
Stop simulation |
R |
Reset simulation |
Tab |
Toggle sidebar |
1 |
Scenario: single drone |
2 |
Scenario: swarm-5 |
3 |
Scenario: swarm-20 |
4 |
Scenario: SAR |
? |
Toggle keyboard shortcuts panel |
# Run development server (Vite HMR + ASP.NET Core)
dotnet run --project src/ResQ.Viz.Web/
# Production build (TypeScript check + Vite bundle → wwwroot/)
dotnet build src/ResQ.Viz.Web/
# Run xUnit test suite
dotnet test tests/ResQ.Viz.Web.Tests/
# TypeScript type-check only (no emit)
cd src/ResQ.Viz.Web && npx tsc --noEmit
# Vite bundle only
cd src/ResQ.Viz.Web && npx vite build
# Initialise the SDK submodule after a fresh clone
git submodule update --init --recursive| Layer | Technology | Notes |
|---|---|---|
| Runtime | .NET 9 / ASP.NET Core | IHostedService simulation loop |
| Real-time | SignalR (WebSocket) | 10 Hz frame broadcast |
| 3D | Three.js r175 (npm) | PBR, InstancedMesh, custom GLSL |
| Post-processing | Three.js EffectComposer |
Selective bloom, SSAO, ACES |
| Frontend build | TypeScript 5 + Vite 6 | Hot module replacement in dev |
| Compression | Web Streams API | CompressionStream · DecompressionStream · deflate-raw |
| Simulation | ResQ.Simulation.Engine | Git submodule — physics, terrain, weather |
| Tests | xUnit + FluentAssertions + Moq | Backend unit tests |
Apache-2.0 — Copyright 2024 ResQ Technologies Ltd.