Skip to content

Command & Conquer-style WebGL interface prototype. Units are displayed on a 3D map layer with real-time movement data provided by a backend simulation server.

Notifications You must be signed in to change notification settings

rdig/ghost-deck

Repository files navigation

Ghost Deck

Command & Conquer-style WebGL interface prototype. Units are displayed on a 3D map layer with real-time movement data provided by a backend simulation server.

'Drone Intro'

Features

  • 3D vehicle models — two-part GLB drones (body + spinning propellers) rendered via deck.gl ScenegraphLayer
  • Real-time simulation — 20Hz server-side tick with state machine (landed → takeoff → holding → goto → land)
  • Smooth interpolation — 60fps client-side lerp of position and heading between 5Hz server polls
  • Vehicle selection — click, rubber-band drag-select, double-click to select all in viewport
  • Goto with spread formation — multi-vehicle goto fans targets into a circle to prevent stacking
  • Follow camera — lock the map view to a single selected vehicle
  • Fog of war — PixiJS-based reveal system that uncovers the map as vehicles move
  • Configurable settings — vehicle count, model scale, propeller speed, anti-aliasing, fog of war, map style
  • Stats HUD — live FPS (color-coded), total/landed/flying/selected vehicle counts

Requirements

Setup

nvm use
pnpm install
cp .env.example .env
# Add your Mapbox token to .env

Running

The frontend and backend run as separate processes. Start both in separate terminals:

# Terminal 1 — Vite dev server (frontend)
pnpm dev

# Terminal 2 — Express API server (port 3001)
pnpm server

The Vite dev server proxies /api requests to the backend automatically.

Environment Variables

Frontend (.env)

Variable Default Description
VITE_MAPBOX_TOKEN Mapbox GL access token (required)
VITE_MAPBOX_STYLE mapbox://styles/mapbox/standard Mapbox style URL
VITE_VEHICLE_SCALE 10 Vehicle model scale coefficient

Server

Variable Default Description
PORT 3001 Server listen port
TICK_MS 50 Simulation tick interval (20Hz)
HORIZONTAL_SPEED 5 Vehicle horizontal speed (m/s)
VERTICAL_SPEED 2 Vehicle vertical speed (m/s)
ROTATION_SPEED 90 Vehicle rotation speed (deg/s)

Controls

Mouse

Input Action
Right-click drag Pan the map
Middle-click drag up/down Tilt (pitch) the map
Middle-click drag left/right Rotate (bearing) the map
Scroll wheel Zoom in/out
Left-click on vehicle Select that vehicle (deselects others)
Left-click on empty space Deselect all
Left-click drag Drag-select (rubber band) all vehicles in the rectangle
Double-click anywhere Select all vehicles in viewport

Keyboard

Key Action
W / A / S / D Pan map up / left / down / right (hold for continuous)
O Reset pitch to top-down view
T Takeoff selected vehicle(s)
L Land selected vehicle(s)
F Toggle follow camera on single selected vehicle

Mouse + Keyboard

Input Action
Hold G + left-click Send selected vehicle(s) to the clicked position (multi-vehicle goto uses spread formation)

Tech Stack

Layer Technology
Frontend Vue 3, Vite 7, TypeScript, Tailwind CSS v4
Map Mapbox GL JS
3D overlays deck.gl (ScenegraphLayer, PathLayer, PolygonLayer, etc.)
2D effects PixiJS (fog of war)
Gestures Hammer.js
Backend Node.js, Express 5
Icons Font Awesome 7

Architecture

Ghost Deck uses a three-layer rendering stack, each layer handling a specific concern.

Rendering Layers

Layer 1: Mapbox GL (base map). Provides the satellite/terrain imagery. Custom mouse bindings replace the defaults (right-click to pan, middle-click for pitch/bearing). WASD keyboard panning runs on its own requestAnimationFrame loop for smooth continuous movement.

Layer 2: deck.gl (3D geometry). Integrated as a MapboxOverlay, which keeps the deck.gl camera in sync with Mapbox. This is the key architectural decision. Instead of rendering vehicles through Mapbox's WebGL context (which forces a full tile re-render on every update), deck.gl sits as an independent layer. The map only re-renders when the viewport changes, not when vehicles move.

Layer 3: PixiJS (2D effects). An absolutely positioned canvas with pointer-events: none, used for fog of war. Renders on top of everything else.

Why deck.gl Over Mapbox WebGL

Mapbox GL's rendering pipeline is optimized for cartographic data, not real-time object animation. When you add custom WebGL content through its API, every position update triggers a full re-render of the map tiles. With many vehicles updating at 20Hz, this becomes the bottleneck.

deck.gl solves this by maintaining its own WebGL context layered above the map. Vehicle position updates, model rendering, path overlays, all of it happens without touching the map tiles. The map is just a background.

3D Model Rendering

Each vehicle is a low-poly drone model (~800 triangles) split into two GLB files: body and rotor/hub assembly.

The body renders once per vehicle as a static ScenegraphLayer. The rotor assembly renders four times per vehicle (one per propeller arm), each with an independent spin angle computed per frame. Rotation directions alternate CW/CCW across the four arms. Spin speeds are randomized per vehicle on creation (720 to 1800 deg/s) and persist across page reloads via localStorage.

This is performant because deck.gl reuses the same model instance. Rendering one body and four rotors per vehicle is nearly the same cost as rendering just the body. It's basically geometry instancing at the application level.

spinning-model.mp4

(If the video is no longer loading due to Github garbage collecting the upload, you can find it here: assets/media/spinning-model.mp4)

Client-Side Interpolation

The server simulation runs at 20Hz. The client polls at 5Hz (200ms intervals). To keep movement smooth at 60fps, the client interpolates between the last two known positions using linear interpolation gated by requestAnimationFrame.

deck.gl's built-in transition system wasn't suitable for this. It's designed for data visualization transitions (like morphing a bar chart), not continuous real-time position updates where new data arrives before the previous transition completes. Custom lerp was the only viable option.

Heading interpolation uses a shortest-angle-difference function to handle the 350-to-10 degree wraparound correctly, so vehicles always rotate through the shortest path.

The RAF loop only runs while there are active interpolations. When all vehicles are stationary, it stops, avoiding unnecessary CPU usage.

Anti-Aliasing

Three independent AA strategies, each toggleable and persisted to localStorage:

Option What it does
WebGL MSAA Hardware multi-sample anti-aliasing at 4x (if GPU supports it)
Device Pixel Ratio Renders at native screen density, capped at 2x to avoid GPU overload on high-DPI displays
FXAA Post-process fast approximate anti-aliasing via luma.gl's shader effect

Each one adds rendering cost. All three active together is the most expensive configuration.

Fog of War

PixiJS renders a semi-transparent black overlay across the entire viewport. A render texture acts as an inverse mask: white areas reveal the map, transparent areas stay fogged.

As vehicles move, radial gradient sprites (64px, white center fading to transparent) are stamped along their trail paths. A 2m deduplication threshold prevents over-stamping when vehicles are moving slowly. Sprites are pooled and reused rather than allocated fresh, and off-screen stamps are culled with margin.

The pixel size of each reveal stamp is recalculated dynamically based on map zoom level, converting a fixed 80m reveal radius into screen pixels.

Performance

This stack handles 3D model rendering with spinning animated propellers at display refresh rate on machines with integrated GPUs. For 2D use cases (which is the actual production target), there's even more headroom since we drop the 3D model overhead entirely.

The only scenario that causes frame drops: Mapbox 3D building extrusions visible on screen while constantly panning/tilting, all three AA layers active, and fog of war enabled simultaneously. Each of those is fine individually. They only become a problem when stacked together, and the fog of war implementation specifically has room for optimization.

Vehicle count is configurable at runtime (up to 10,000) via the settings panel, making it easy to find the performance ceiling for any given hardware.

About

Command & Conquer-style WebGL interface prototype. Units are displayed on a 3D map layer with real-time movement data provided by a backend simulation server.

Topics

Resources

Stars

Watchers

Forks

Languages