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
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
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)