Arena Rendering Optimizations
In February 2026, the duel arena rendering system was completely overhauled to eliminate performance bottlenecks. This document explains the optimizations and their impact.Performance Improvements
Before vs After
| Metric | Before | After | Improvement |
|---|---|---|---|
| Draw Calls | ~846 | ~22 | 97% reduction |
| PointLights | 28 | 0 | 100% reduction |
| CPU per frame | High (light updates) | Minimal | ~80% reduction |
| GPU efficiency | Low (state changes) | High (instancing) | Significant |
Key Changes
- InstancedMesh Conversion - 846 individual meshes → 20 instanced draw calls
- PointLight Removal - 28 CPU-animated lights → GPU-driven TSL emissive materials
- Fire Particle Rewrite - Enhanced shader with value noise and turbulent motion
- Dead Code Removal - Unused functions deleted (createArenaMarker, createAmbientDust, createLobbyBenches)
InstancedMesh Architecture
What is InstancedMesh?
InstancedMesh renders many copies of the same geometry with a single draw call. Each instance can have a unique position, rotation, and scale.
Benefits:
- Fewer draw calls - GPU state changes are expensive
- Shared geometry - One buffer for all instances
- Shared material - One shader compilation
- GPU-friendly - Instancing is a native GPU feature
Arena Instancing Breakdown
| Component | Count | Instances | Draw Calls |
|---|---|---|---|
| Fence Posts | 288 | 1 InstancedMesh | 1 |
| Fence Caps | 288 | 1 InstancedMesh | 1 |
| Fence Rails (X) | 36 | 1 InstancedMesh | 1 |
| Fence Rails (Z) | 36 | 1 InstancedMesh | 1 |
| Pillar Bases | 32 | 1 InstancedMesh | 1 |
| Pillar Shafts | 32 | 1 InstancedMesh | 1 |
| Pillar Capitals | 32 | 1 InstancedMesh | 1 |
| Brazier Bowls | 24 | 1 InstancedMesh | 1 |
| Border Strips (N/S) | 12 | 1 InstancedMesh | 1 |
| Border Strips (E/W) | 12 | 1 InstancedMesh | 1 |
| Banner Poles | 12 | 1 InstancedMesh | 1 |
| Arena Floors | 6 | Individual meshes | 6 |
| Forfeit Pillars | 12 | Individual meshes | 12 |
| Banner Cloths | 12 | Individual meshes (3 materials) | 3 |
| Lobby/Hospital | 2 | Individual meshes | 2 |
| Total | ~846 | 11 InstancedMesh + 32 individual | ~22 |
Why Some Meshes Aren’t Instanced
Arena Floors (6 individual meshes):- Need unique
userData.arenaIdfor raycasting - Need layer 0+2 for click-to-move and minimap
- Sharing one geometry + material is still efficient
- Need unique
userData.entityIdfor interaction system - Each pillar is a clickable entity
- Only 3 unique colors (4 meshes per material)
- Already batched by material
TSL Emissive Materials
Replacing PointLights
Problem: 28 PointLights forced expensive per-pixel lighting calculations every frame. Solution: GPU-driven TSL emissive materials with animated flicker.Brazier Glow Material
Implementation:packages/shared/src/systems/client/DuelArenaVisualsSystem.ts
- Per-instance phase: Each brazier flickers independently
- GPU-driven: Zero CPU cost per frame
- Realistic flicker: Multi-frequency sine + noise matches old PointLight behavior
- Directional glow: Only top face emits light (fire opening)
Performance Impact
Before (28 PointLights):- CPU: Update 28 light intensities every frame
- GPU: 28 per-pixel lighting passes on surrounding geometry
- Memory: 28 light objects + shadow maps
- CPU: Update one time uniform per frame
- GPU: Emissive calculation in fragment shader (already running)
- Memory: One material + one uniform
Fire Particle Shader Rewrite
Enhanced Fire Preset
Thefire particle preset was rewritten with a new fragment shader for better visual quality and additive blending.
Old Shader (simple radial glow):
- Soft falloff: No hard edges, particles blend smoothly
- Value noise: Organic flame shapes (not uniform circles)
- Scrolling noise: Upward motion feel
- Color gradient: Bright white-yellow core → orange-red tips
- Additive-friendly: Designed for overlapping particles to merge
Turbulent Vertex Motion
Particles now have per-particle turbulent motion for natural flame flickering:Torch Preset Removed
Thetorch preset was removed and unified with the enhanced fire preset.
Migration:
TSL Procedural Materials
Sandstone Block Pattern
Arena fences use a GPU-computed sandstone block pattern with:- Running bond layout - Offset rows for realistic masonry
- Per-block color variation - Warm sandstone range (0.62-0.72 R, 0.52-0.60 G, 0.38-0.46 B)
- Mortar grooves - Dark earth brown (0.35, 0.28, 0.2)
- Bevel effect - Blocks appear raised with edge darkening
- Surface grain - Fine noise texture for stone detail
positionWorld.xz for horizontal and positionWorld.y for vertical, ensuring seamless tiling on any wall orientation.
Floor Tile Pattern
Arena floors use a square flagstone pattern with:- 1.2m tiles - Large flagstones with thin grout lines
- Per-tile color variation - Sand-earth range (0.68-0.80 R, 0.54-0.64 G, 0.36-0.44 B)
- Grout lines - Dark grout (0.4, 0.32, 0.22)
- Bevel effect - Subtle tile edge darkening
- Surface grain - Noise texture for worn stone
positionWorld.xz so each arena looks unique despite sharing the same material.
Code Structure
File Organization
Main File:packages/shared/src/systems/client/DuelArenaVisualsSystem.ts
Key Methods:
createSharedMaterials()- Creates all TSL materials oncebuildFenceInstances()- Builds fence InstancedMesh (posts, caps, rails)buildPillarInstances()- Builds pillar InstancedMesh (bases, shafts, capitals)buildBrazierInstances()- Builds brazier InstancedMesh with TSL glowbuildBorderInstances()- Builds floor border InstancedMeshbuildBannerPoleInstances()- Builds banner pole InstancedMeshcreateArenaFloors()- Creates individual floor meshes (need unique userData)createForfeitPillars()- Creates individual forfeit pillars (need unique entityId)createBannerCloths()- Creates individual banner cloths (3 materials)
Material Caching
All materials are created once and shared:- One shader compilation per material
- Shared GPU resources
- Easier cleanup (dispose once)
Performance Metrics
Draw Call Reduction
Before:Lighting Overhead Elimination
Before (28 PointLights):Visual Quality Improvements
Fire Particles
Old Fire:- Simple radial glow (uniform circles)
- Hard edges (visible particle boundaries)
- Straight upward motion (no turbulence)
- Uniform color (no gradient)
- Value noise (organic flame shapes)
- Soft falloff (smooth blending)
- Turbulent motion (visible flicker and sway)
- Color gradient (white-yellow core → orange-red tips)
- Scrolling noise (upward motion feel)
Brazier Glow
Old Braziers:- Static emissive material (no animation)
- PointLight for glow (expensive)
- Uniform brightness (no flicker)
- Animated TSL emissive (GPU-driven flicker)
- No PointLight (zero lighting cost)
- Multi-frequency flicker (realistic fire)
- Per-instance phase offset (each brazier unique)