> ## Documentation Index
> Fetch the complete documentation index at: https://hyperscape-ai-mintlify-docs-update.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Arena rendering optimizations

# 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

1. **InstancedMesh Conversion** - 846 individual meshes → 20 instanced draw calls
2. **PointLight Removal** - 28 CPU-animated lights → GPU-driven TSL emissive materials
3. **Fire Particle Rewrite** - Enhanced shader with value noise and turbulent motion
4. **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.arenaId` for raycasting
* Need layer 0+2 for click-to-move and minimap
* Sharing one geometry + material is still efficient

**Forfeit Pillars** (12 individual meshes):

* Need unique `userData.entityId` for interaction system
* Each pillar is a clickable entity

**Banner Cloths** (12 individual meshes):

* 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`

```typescript theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
// TSL emissive node with per-instance phase offset
mat.emissiveNode = Fn(() => {
  const wp = positionWorld;
  // Quantize world position so all vertices of one brazier share phase
  const quantized = vec2(tslFloor(wp.x.add(0.5)), tslFloor(wp.z.add(0.5)));
  const phase = tslHash(quantized).mul(6.28);
  
  // Multi-frequency sine flicker + high-freq noise
  const flicker = sin(t.mul(10.0).add(phase))
    .mul(0.15)
    .add(sin(t.mul(7.3).add(phase.mul(1.7))).mul(0.08));
  const noise = fract(sin(t.mul(43.7).add(phase)).mul(9827.3)).mul(0.05);
  const intensity = float(0.6).add(flicker).add(noise);
  
  // Only top face glows (fire opening)
  const topMask = smoothstep(float(0.7), float(0.95), normalWorld.y);
  
  return vec3(1.0, 0.4, 0.0).mul(intensity).mul(topMask);
})();
```

**Key Features:**

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

**After** (TSL emissive):

* CPU: Update one time uniform per frame
* GPU: Emissive calculation in fragment shader (already running)
* Memory: One material + one uniform

**Result**: \~80% CPU reduction, no per-pixel lighting overhead.

## Fire Particle Shader Rewrite

### Enhanced Fire Preset

The `fire` particle preset was rewritten with a new fragment shader for better visual quality and additive blending.

**Old Shader** (simple radial glow):

```glsl theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
float dist = length(uv - 0.5) * 2.0;
float glow = pow(max(1.0 - dist, 0.0), sharpness);
color = particleColor * glow;
alpha = glow;
```

**New Shader** (value noise + soft falloff):

```glsl theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
// Smooth value noise via bilinear interpolation
float noise = valueNoise(uv * 4.0 + scrollY);

// Soft radial falloff (no hard edges)
float radialDist = length(uv - 0.5) * 2.0;
float softFalloff = pow(max(1.0 - radialDist, 0.0), 0.8);

// Noise modulates mask
float glow = softFalloff * (0.7 + noise * 0.3);

// Color gradient (bright core → particle color at edges)
vec3 coreColor = vec3(1.0, 0.9, 0.4);
vec3 fireColor = mix(particleColor, coreColor, coreness);

color = fireColor * glow * 1.5;
alpha = glow * opacity;
```

**Improvements:**

* **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:

```typescript theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
// Flame-like turbulence — visible flicker that fades with height
const turbAmp = mul(float(0.04), sub(float(1.0), mul(t, float(0.7))));
const turbX = mul(sin(add(mul(time, float(7.0)), phase)), turbAmp);
const turbZ = mul(cos(add(mul(time, float(5.5)), mul(phase, float(1.5)))), turbAmp);

particleCenter = add(
  aEmitterPos,
  vec3(add(spreadX, turbX), riseY, add(spreadZ, turbZ))
);
```

**Effect**: Particles jitter and sway like real flames, not just rising straight up.

### Torch Preset Removed

The `torch` preset was removed and unified with the enhanced `fire` preset.

**Migration**:

```typescript theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
// Before
particleSystem.register('torch_id', {
  type: 'glow',
  preset: 'torch',
  position: { x, y, z }
});

// After
particleSystem.register('torch_id', {
  type: 'glow',
  preset: 'fire',  // Use fire preset
  position: { x, y, z }
});
```

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

**World-Space UVs**: Uses `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

**World-Space UVs**: Uses `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 once
* `buildFenceInstances()` - Builds fence InstancedMesh (posts, caps, rails)
* `buildPillarInstances()` - Builds pillar InstancedMesh (bases, shafts, capitals)
* `buildBrazierInstances()` - Builds brazier InstancedMesh with TSL glow
* `buildBorderInstances()` - Builds floor border InstancedMesh
* `buildBannerPoleInstances()` - Builds banner pole InstancedMesh
* `createArenaFloors()` - 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:

```typescript theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
private stoneFenceMat: MeshStandardNodeMaterial | null = null;
private arenaFloorMat: MeshStandardNodeMaterial | null = null;
private borderMat: MeshStandardNodeMaterial | null = null;
private pillarStoneMat: MeshStandardNodeMaterial | null = null;
private brazierGlowMat: MeshStandardNodeMaterial | null = null;
private forfeitPillarMat: MeshStandardNodeMaterial | null = null;
private bannerPoleMat: MeshStandardNodeMaterial | null = null;
private lobbyStandMat: MeshStandardNodeMaterial | null = null;
```

**Benefits:**

* One shader compilation per material
* Shared GPU resources
* Easier cleanup (dispose once)

## Performance Metrics

### Draw Call Reduction

**Before**:

```
Arena 1: 141 meshes
Arena 2: 141 meshes
Arena 3: 141 meshes
Arena 4: 141 meshes
Arena 5: 141 meshes
Arena 6: 141 meshes
Total: 846 draw calls
```

**After**:

```
Fence Posts: 1 InstancedMesh (288 instances)
Fence Caps: 1 InstancedMesh (288 instances)
Fence Rails X: 1 InstancedMesh (36 instances)
Fence Rails Z: 1 InstancedMesh (36 instances)
Pillar Bases: 1 InstancedMesh (32 instances)
Pillar Shafts: 1 InstancedMesh (32 instances)
Pillar Capitals: 1 InstancedMesh (32 instances)
Braziers: 1 InstancedMesh (24 instances)
Border N/S: 1 InstancedMesh (12 instances)
Border E/W: 1 InstancedMesh (12 instances)
Banner Poles: 1 InstancedMesh (12 instances)
Arena Floors: 6 individual meshes
Forfeit Pillars: 12 individual meshes
Banner Cloths: 3 materials × 4 meshes = 12 meshes
Lobby/Hospital: 2 individual meshes
Total: ~22 draw calls
```

### Lighting Overhead Elimination

**Before** (28 PointLights):

```javascript theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
// CPU: Update every frame
for (let i = 0; i < 28; i++) {
  light.intensity = BASE + sin(time * 10 + i * 1.7) * 0.15 + random() * 0.05;
}

// GPU: Per-pixel lighting for each light
for each pixel in scene:
  for each of 28 lights:
    calculate distance, attenuation, diffuse, specular
    accumulate lighting contribution
```

**After** (TSL emissive):

```javascript theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
// CPU: Update one uniform
timeUniform.value += deltaTime;

// GPU: Emissive calculation (already in fragment shader)
emissive = baseColor * flickerIntensity * topMask;
```

**Result**: No per-pixel lighting calculations, no CPU light updates.

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

**New Fire**:

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

**Particle Count**: Increased from 18 to 28 per emitter (more particles, zero CPU cost).

### Brazier Glow

**Old Braziers**:

* Static emissive material (no animation)
* PointLight for glow (expensive)
* Uniform brightness (no flicker)

**New Braziers**:

* Animated TSL emissive (GPU-driven flicker)
* No PointLight (zero lighting cost)
* Multi-frequency flicker (realistic fire)
* Per-instance phase offset (each brazier unique)

## Implementation Guide

### Creating InstancedMesh

```typescript theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
// 1. Create geometry (shared by all instances)
const geometry = new THREE.BoxGeometry(width, height, depth);

// 2. Create material (shared by all instances)
const material = new MeshStandardNodeMaterial({ color: 0x8b7355 });

// 3. Create InstancedMesh
const instancedMesh = new THREE.InstancedMesh(
  geometry,
  material,
  instanceCount  // Max number of instances
);

// 4. Set instance transforms
const matrix = new THREE.Matrix4();
for (let i = 0; i < instanceCount; i++) {
  matrix.makeTranslation(x, y, z);
  instancedMesh.setMatrixAt(i, matrix);
}

// 5. Update instance matrix
instancedMesh.instanceMatrix.needsUpdate = true;

// 6. Add to scene
scene.add(instancedMesh);
```

### Creating TSL Emissive Material

```typescript theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
// 1. Create time uniform
const timeUniform = uniform(float(0));

// 2. Create material with emissive node
const material = new MeshStandardNodeMaterial({
  color: 0xff4400,
  roughness: 0.7
});

material.emissiveNode = Fn(() => {
  const t = timeUniform;
  const wp = positionWorld;
  
  // Per-instance phase from world position
  const phase = tslHash(vec2(tslFloor(wp.x), tslFloor(wp.z))).mul(6.28);
  
  // Animated flicker
  const flicker = sin(t.mul(10.0).add(phase)).mul(0.15);
  const intensity = float(0.6).add(flicker);
  
  // Only top face glows
  const topMask = smoothstep(float(0.7), float(0.95), normalWorld.y);
  
  return vec3(1.0, 0.4, 0.0).mul(intensity).mul(topMask);
})();

// 3. Update time uniform every frame
update(deltaTime) {
  timeUniform.value += deltaTime;
}
```

## Migration Guide

### Updating Existing Arena Code

If you have custom arena modifications, update them to use InstancedMesh:

**Before**:

```typescript theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
// Creating 100 fence posts individually
for (let i = 0; i < 100; i++) {
  const post = new THREE.Mesh(postGeometry, material);
  post.position.set(x + i * spacing, y, z);
  scene.add(post);
}
```

**After**:

```typescript theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
// Creating 100 fence posts with InstancedMesh
const posts = new THREE.InstancedMesh(postGeometry, material, 100);
const matrix = new THREE.Matrix4();

for (let i = 0; i < 100; i++) {
  matrix.makeTranslation(x + i * spacing, y, z);
  posts.setMatrixAt(i, matrix);
}

posts.instanceMatrix.needsUpdate = true;
scene.add(posts);
```

### Replacing PointLights with TSL Emissive

**Before**:

```typescript theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
// PointLight with CPU animation
const light = new THREE.PointLight(0xff6600, 0.8, 6);
light.position.set(x, y, z);
scene.add(light);

// Update every frame
update(deltaTime) {
  light.intensity = 0.8 + Math.sin(time * 10) * 0.15;
}
```

**After**:

```typescript theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
// TSL emissive material (GPU animation)
const timeUniform = uniform(float(0));

const material = new MeshStandardNodeMaterial({ color: 0xff4400 });
material.emissiveNode = Fn(() => {
  const flicker = sin(timeUniform.mul(10.0)).mul(0.15);
  return vec3(1.0, 0.4, 0.0).mul(float(0.8).add(flicker));
})();

const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);

// Update time uniform
update(deltaTime) {
  timeUniform.value += deltaTime;
}
```

## Debugging

### Verify Instancing

```javascript theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
// Check instance count
console.log(instancedMesh.count);  // Should match expected count

// Check if instances are visible
instancedMesh.instanceMatrix.needsUpdate = true;

// Verify transforms
const matrix = new THREE.Matrix4();
instancedMesh.getMatrixAt(0, matrix);
console.log(matrix.elements);
```

### Verify TSL Emissive

```javascript theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
// Check if emissive node is set
console.log(material.emissiveNode);  // Should not be null

// Check time uniform
console.log(timeUniform.value);  // Should increase every frame

// Force material update
material.needsUpdate = true;
```

### Performance Profiling

```javascript theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
// Chrome DevTools
// Performance tab → Record → Look for:
// - "Draw calls" (should be ~22)
// - "GPU time" (should be lower)
// - "CPU time" (should be lower)

// Three.js stats
const stats = new Stats();
document.body.appendChild(stats.dom);

// Check draw calls
console.log(renderer.info.render.calls);  // Should be ~22
```

## See Also

* [Three.js InstancedMesh Documentation](https://threejs.org/docs/#api/en/objects/InstancedMesh)
* [Three.js TSL Documentation](https://threejs.org/docs/#api/en/nodes/Nodes)
* [packages/shared/src/systems/client/DuelArenaVisualsSystem.ts](../packages/shared/src/systems/client/DuelArenaVisualsSystem.ts) - Implementation
* [packages/shared/src/entities/managers/particleManager/GlowParticleManager.ts](../packages/shared/src/entities/managers/particleManager/GlowParticleManager.ts) - Fire particle shader
