A retro-style pseudo-3D racing game built with TypeScript and WebGL2, inspired by classic arcade racers like OutRun.
- GPU-accelerated WebGL2 rendering
- Retro 512x384 resolution upscaled for a pixelated aesthetic
- Pseudo-3D road with curves and hills
- Roadside sprites with distance fog
- Track editor with 2D overview, elevation profile, and 3D preview
- Customizable physics parameters
- Track persistence via localStorage
The game uses a WebGL2-based rendering pipeline that maintains visual parity with classic pseudo-3D racing games while leveraging GPU acceleration.
Game.render()
└── WebGLRenderer.render(track, position, playerX, playerY, steerDirection)
├── renderSky() # Fullscreen quad with gradient shader
├── project() loop # CPU-side perspective projection
├── buildSegmentGeometry() # Generate road triangles
├── renderRoad() # Draw road, rumble strips, lane markers
├── renderSprites() # Billboarded sprite quads (back-to-front)
├── renderPlayer() # Player car sprite
└── Upscale to display # 512x384 → window size (nearest filtering)
src/
├── core/
│ ├── Game.ts # Main game loop, physics, input handling
│ ├── Track.ts # Track generation and segment management
│ ├── Input.ts # Keyboard input handling
│ ├── WebGLRenderer.ts # WebGL2 renderer implementation
│ ├── WebGLUtils.ts # Shader compilation, buffer helpers
│ ├── shaders/
│ │ ├── sky.vert # Sky gradient vertex shader
│ │ ├── sky.frag # Sky gradient fragment shader
│ │ ├── road.vert # Road segment vertex shader
│ │ ├── road.frag # Road segment fragment shader (with fog)
│ │ ├── sprite.vert # Sprite billboard vertex shader
│ │ └── sprite.frag # Sprite fragment shader (with fog)
│ └── index.ts # Core module exports
├── editor/
│ └── TrackEditor.ts # Track editor with 2D/3D views
├── sprites/
│ ├── SpriteRegistry.ts # Sprite definitions and metadata
│ ├── spritesheet-data.ts # Generated sprite atlas coordinates
│ ├── types.ts # Sprite type definitions
│ └── index.ts # Sprite module exports
├── config/
│ ├── constants.ts # Game configuration values
│ └── colors.ts # Color definitions (RGB)
├── data/
│ └── TrackData.ts # Track serialization and persistence
├── types/
│ └── game.ts # Core game type definitions
└── main.ts # Application entry point
The WebGLRenderer class handles all GPU rendering:
Shader Programs:
skyProgram- Renders vertical gradient sky using uniform color arrayroadProgram- Renders road geometry with per-vertex colors and fog blendingspriteProgram- Renders textured sprite quads with alpha and fog opacity
Key Features:
- Pre-allocated typed arrays for dynamic geometry (avoids GC pressure)
- Single sprite sheet texture atlas (2048x2048)
- Orthographic projection for 2D screen-space rendering
- NEAREST texture filtering for retro pixelated look
- Linear fog blending in fragment shaders
Render Flow:
- Clear framebuffer
- Draw sky gradient (fullscreen quad)
- Project all visible segments (CPU-side perspective math)
- Build road geometry (grass, rumble, road, lanes as triangles)
- Upload and draw road in single draw call
- Build sprite geometry back-to-front for proper depth ordering
- Upload and draw sprites
- Draw player car
- Blit retro canvas to display canvas with upscaling
Tracks are defined as a sequence of pieces:
- Straight - Flat road sections
- Curve - Left/right turns with configurable intensity
- Hill - Elevation changes with enter/hold/exit pattern
Each piece generates multiple road segments. Sprites can be placed at any segment with a lateral offset.
Key settings in src/config/constants.ts:
RENDER.RETRO_WIDTH/HEIGHT- Internal resolution (512x384)RENDER.DRAW_DISTANCE- Visible segment count (300)CAMERA.DEPTH- Perspective factor (0.84)CAMERA.ROAD_WIDTH- Road width in world units (2000)FOG.DEFAULT_DENSITY- Fog intensity (5)
- Node.js 18+
- npm
npm installnpm run devnpm run buildnpm run dev- Start development server with hot reloadnpm run build- Type check and build for productionnpm run preview- Preview production build
- Arrow Up - Accelerate
- Arrow Down - Brake
- Arrow Left/Right - Steer
- Escape - Return to editor
- Scroll - Zoom in/out
- Drag - Pan view
- Click track - Place sprite
- Click sprite - Select/edit sprite
MIT