Back to Overview
01 / 06
PROJECT 01•graphics systems

The Last Empire

GPU-Accelerated WebGL2 Grand Strategy & Geopolitical Simulation Engine

Live Subdomain DemoView on GitHub

Technologies & Stack

Next.js 16React 19TypeScriptWebGL2GLSLDexie.jsTurborepoWeb Audio API
thelastempire.parsadevstudio.ir
IDLE
The Last Empire

System Overview & Purpose

A browser-based, turn-driven geopolitical statecraft simulation. Renders thousands of territorial subdivisions via a dedicated WebGL2 GPU rasterizer operating on bit-packed binary state buffers, achieving smooth 60+ FPS navigation with zero DOM bloat.

Core Architecture

Hexagonal / Clean Architecture with deterministic simulation executors decoupled from UI. Procedural audio synthesized via native Web Audio API with zero asset overhead.

Performance Benchmarks

60+ FPS

Framerate

Constant GPU rendering without DOM overhead

4096×2048

Map Resolution

Real-time shader-based territory rasterization

0 KB

Audio Assets Size

Procedurally synthesized via Web Audio Context

Engineering Challenges & Solutions

Real software roadblocks encountered during production and their architectural resolutions.

01.

Sub-Pixel GLSL Boundary Rasterization over R16UI State Buffers

The Problem: Rendering 4,000+ interactive polygonal subdivisions via DOM SVG or stroked canvas paths caused catastrophic layout thrashing and dropped frame rates below 15 FPS.
The Architectural Solution: Engineered a direct-to-GPU WebGL2 pipeline. Dynamic territorial state resides in a 16-bit unsigned texture buffer (R16UI), while international borders and coastal anti-aliasing compute on-the-fly in fragment shaders using cardinal-neighbor smoothstep evaluations.
Sub-Pixel GLSL Boundary Rasterization over R16UI State Buffers — Source Solutionglsl
1234567891011121314151617181920212223242526272829303132333435// GLSL Fragment Shader: R16UI state lookup, palette sampling & dynamic border rasterizer
#version 300 es
precision highp float;
precision highp usampler2D;
in vec2 v_texCoord;
out vec4 fragColor;

uniform usampler2D u_liveStateTexture;
uniform sampler2D u_paletteTexture;
uniform vec2 u_texelSize;
uniform float u_scale;

vec4 sampleCountryColor(uint pid) {
  float u = (float(pid & 255u) + 0.5) / 256.0;
  float v = (float((pid >> 8u) & 255u) + 0.5) / 256.0;
  return texture(u_paletteTexture, vec2(u, v));
}

void main() {
  uint centerPid = texture(u_liveStateTexture, v_texCoord).r & 4095u;
  if (centerPid <= 1u) discard;

  vec4 centerColor = sampleCountryColor(centerPid);
  int centerCountryId = int(floor(centerColor.a * 255.0 + 0.5));

  // Compute smoothstep antialiased boundary stroke against cardinal neighbors
  vec2 o = u_texelSize;
  uint pW = texture(u_liveStateTexture, v_texCoord + vec2(-o.x, 0.0)).r & 4095u;
  float stroke = clamp(u_scale * 0.95, 0.85, 1.90);
  float edgeSoft = clamp(stroke * 0.45, 0.35, 0.70);
  float distW = fract(v_texCoord.x / o.x) * u_scale;
  float border = (pW != centerPid) ? 1.0 - smoothstep(stroke - edgeSoft, stroke + edgeSoft, distW) : 0.0;

  fragColor = mix(centerColor, vec4(0.01, 0.015, 0.03, 1.0), border * 0.85);
}
02.

Zero-Asset Multi-Layer Procedural Sound Synthesizer

The Problem: Bundling audio recordings for war alerts, industrial feedback, and military mobilization bloated network bundles by megabytes and suffered from initial decoding latency.
The Architectural Solution: Architected a pure Web Audio API synthesizer. Implemented procedural sound presets scheduling sine/sawtooth oscillator ramps, bandpass noise buffers, and biquad filters through a centralized DynamicsCompressorNode to prevent clipping.
Zero-Asset Multi-Layer Procedural Sound Synthesizer — Source Solutiontypescript
1234567891011121314151617181920212223// Multi-layer procedural synthesizer with tone scheduling & dynamics limiting
export class TacticalSoundSynthesizer {
  private audioCtx = new AudioContext();
  private limiter = this.audioCtx.createDynamicsCompressor();

  public scheduleTone(tone: SynthToneLayer, pitchMultiplier: number): void {
    const osc = this.audioCtx.createOscillator();
    const gain = this.audioCtx.createGain();
    const now = this.audioCtx.currentTime + (tone.delay || 0);

    osc.type = tone.type || 'sine';
    osc.frequency.setValueAtTime(tone.freq * pitchMultiplier, now);
    if (tone.endFreq) {
      osc.frequency.exponentialRampToValueAtTime(tone.endFreq * pitchMultiplier, now + tone.decay);
    }
    gain.gain.setValueAtTime(tone.gain, now);
    gain.gain.exponentialRampToValueAtTime(0.0001, now + tone.decay);

    osc.connect(gain).connect(this.limiter);
    osc.start(now);
    osc.stop(now + tone.decay);
  }
}

Interface & System Screens

High-resolution captures of the live interface and management panels.

The Last Empire Screen 1
01
The Last Empire Screen 2
02
The Last Empire Screen 3
03
The Last Empire Screen 4
04
Next System
Fluxio Engine
Back to Overview