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

# Instanced Rendering

> GPU-instanced rendering for resource entities with LOD and depleted model support

## Overview

Hyperscape uses GPU instancing to render thousands of resource entities (rocks, ores, herbs, trees) with minimal draw calls. This system was introduced in PR #946 (February 2026) and provides dramatic performance improvements.

**Performance Impact:**

* Reduces draw calls from O(n) per resource to O(1) per unique model per LOD level
* Distance-based LOD switching with hysteresis to prevent flickering
* Supports depleted models (stumps, empty rocks) with separate instance pools
* Highlight mesh support for hover/selection effects

***

## Architecture

### Instancer Systems

Hyperscape provides two specialized instancers:

| Instancer                | Purpose                                 | Location                                       |
| ------------------------ | --------------------------------------- | ---------------------------------------------- |
| **GLBResourceInstancer** | Rocks, ores, herbs (non-tree resources) | `systems/shared/world/GLBResourceInstancer.ts` |
| **GLBTreeInstancer**     | Tree resources with dissolve materials  | `systems/shared/world/GLBTreeInstancer.ts`     |

Both instancers share the same architecture:

* Load each model once, extract geometry by reference
* Render all instances via single `THREE.InstancedMesh` per LOD level
* Distance-based LOD switching per-instance every frame
* Matrix swap-and-pop for efficient instance removal

### Visual Strategy Pattern

Resources use the **Strategy Pattern** to delegate rendering:

```typescript theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
// From packages/shared/src/entities/world/visuals/createVisualStrategy.ts
export function createVisualStrategy(
  config: ResourceEntityConfig
): ResourceVisualStrategy {
  // Trees with GLB models use instanced rendering
  if (config.resourceType === "tree" && hasModel(config))
    return new TreeGLBVisualStrategy();
  
  // All other resources with models use instanced rendering
  if (hasModel(config))
    return new InstancedModelVisualStrategy();
  
  // Fallback to placeholder cubes
  return new PlaceholderVisualStrategy();
}
```

**Visual Strategies:**

1. **TreeGLBVisualStrategy** - GLB tree models via GLBTreeInstancer
2. **InstancedModelVisualStrategy** - Rocks, ores, herbs via GLBResourceInstancer
3. **StandardModelVisualStrategy** - Fallback for non-instanced rendering
4. **FishingSpotVisualStrategy** - Fishing spot particles
5. **PlaceholderVisualStrategy** - Colored cubes for missing models

***

## ResourceVisualStrategy API

### Interface

```typescript theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
export interface ResourceVisualStrategy {
  createVisual(ctx: ResourceVisualContext): Promise<void>;
  
  /**
   * @returns true if the strategy handled depletion visuals (instanced stump),
   *          false if ResourceEntity should load an individual depleted model.
   */
  onDepleted(ctx: ResourceVisualContext): Promise<boolean>;
  
  onRespawn(ctx: ResourceVisualContext): Promise<void>;
  update(ctx: ResourceVisualContext, deltaTime: number): void;
  destroy(ctx: ResourceVisualContext): void>;
  
  /** Return a temporary mesh positioned at this instance for the outline pass. */
  getHighlightMesh?(ctx: ResourceVisualContext): THREE.Object3D | null;
}
```

### BREAKING CHANGE: onDepleted Return Type

**Before (pre-PR #946):**

```typescript theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
async onDepleted(ctx: ResourceVisualContext): Promise<void> {
  // Hide visual, ResourceEntity always loads depleted model
}
```

**After (PR #946, February 2026):**

```typescript theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
async onDepleted(ctx: ResourceVisualContext): Promise<boolean> {
  if (this.instanced) {
    setResourceDepleted(ctx.id, true);
    return true; // Strategy handled depletion (instanced stump)
  }
  return false; // ResourceEntity should load individual depleted model
}
```

**Migration Guide:**

All custom visual strategies must update their `onDepleted()` signature:

```typescript theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
// ❌ Old
async onDepleted(ctx: ResourceVisualContext): Promise<void> {
  // ...
}

// ✅ New
async onDepleted(ctx: ResourceVisualContext): Promise<boolean> {
  // ...
  return false; // or true if strategy handles depletion
}
```

### New Method: getHighlightMesh

**Purpose:** Provide a positioned mesh for outline rendering on instanced entities.

**Signature:**

```typescript theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
getHighlightMesh?(ctx: ResourceVisualContext): THREE.Object3D | null
```

**Implementation Example:**

```typescript theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
// From InstancedModelVisualStrategy.ts
getHighlightMesh(ctx: ResourceVisualContext): THREE.Object3D | null {
  if (this.fallback) return null;
  return getResourceHighlightMesh(ctx.id);
}
```

**Usage:**

The `EntityHighlightService` calls this method to get a temporary mesh for outline rendering:

```typescript theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
// From EntityHighlightService.ts
const entity = target.entity as unknown as Record<string, unknown>;
if (typeof entity.getHighlightRoot === "function") {
  const hlRoot = (entity.getHighlightRoot as () => THREE.Object3D | null)();
  if (hlRoot) {
    this.world.stage?.scene?.add?.(hlRoot);
    this.activeHighlightMesh = hlRoot;
    const meshes = this.collectMeshes(hlRoot);
    if (meshes.length > 0) {
      const color = this.getHighlightColor(target.entityType);
      this.composer.setOutlineColor(color);
      this.composer.setOutlineObjects(meshes);
      return;
    }
  }
}
```

***

## GLBResourceInstancer

### Overview

Manages instanced rendering for non-tree resources (rocks, ores, herbs).

**Location:** `packages/shared/src/systems/shared/world/GLBResourceInstancer.ts`

**Features:**

* Pools instances by model path
* Separate `InstancedMesh` per LOD level (LOD0, LOD1, LOD2)
* Depleted model pools for stumps/empty rocks
* Highlight mesh support for hover effects
* Max 512 instances per pool

### API

```typescript theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
// Initialize instancer (called in createClientWorld.ts)
initGLBResourceInstancer(scene: THREE.Scene, world: World): void

// Add instance
addInstance(
  modelPath: string,
  entityId: string,
  position: THREE.Vector3,
  rotation: number,
  scale: number,
  depletedModelPath?: string | null,
  depletedScale?: number
): Promise<boolean>

// Remove instance
removeInstance(entityId: string): void

// Set depleted state
setDepleted(entityId: string, depleted: boolean): void

// Check if entity has depleted pool
hasDepleted(entityId: string): boolean

// Get highlight mesh for entity
getHighlightMesh(entityId: string): THREE.Object3D | null

// Update all instances (call every frame)
updateGLBResourceInstancer(): void

// Cleanup
destroyGLBResourceInstancer(): void
```

### LOD System

The instancer automatically switches LOD levels based on camera distance:

```typescript theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
// From GLBResourceInstancer.ts
const resourceLOD = getLODDistances("resource");

// LOD switching with hysteresis
const lod1DistSq = resourceLOD.lod1DistanceSq;
const lod2DistSq = resourceLOD.lod2DistanceSq;
const hysteresisSq = 0.81; // Prevents flickering

// Distance-based LOD selection
if (distSq < lod1DistSq * hysteresisSq) {
  targetLOD = 0; // High detail
} else if (distSq < lod2DistSq * hysteresisSq) {
  targetLOD = 1; // Medium detail
} else {
  targetLOD = 2; // Low detail
}
```

**LOD File Naming Convention:**

* LOD0: `model.glb` (original file)
* LOD1: `model_lod1.glb` (inferred)
* LOD2: `model_lod2.glb` (inferred)

### Depleted Models

Resources can specify depleted models in their configuration:

```json theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
{
  "id": "oak_tree",
  "model": "trees/oak.glb",
  "modelScale": 3.0,
  "depletedModelPath": "trees/oak_stump.glb",
  "depletedModelScale": 0.3
}
```

**Lifecycle:**

1. Resource spawns → added to normal pool at LOD0
2. Resource depleted → removed from normal pool, added to depleted pool
3. Resource respawns → removed from depleted pool, added back to normal pool

**Benefits:**

* No individual model loading for depleted states
* Instant visual transition (matrix swap)
* Collision proxy persists across transitions
* Separate Y-offset calculation for depleted models

***

## InstancedModelVisualStrategy

### Overview

Thin wrapper that integrates `GLBResourceInstancer` with the `ResourceEntity` lifecycle.

**Location:** `packages/shared/src/entities/world/visuals/InstancedModelVisualStrategy.ts`

### Implementation

```typescript theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
export class InstancedModelVisualStrategy implements ResourceVisualStrategy {
  private instanced = false;
  private fallback: StandardModelVisualStrategy | null = null;

  async createVisual(ctx: ResourceVisualContext): Promise<void> {
    const { config, id, position } = ctx;
    if (!config.model) return;

    const baseScale = config.modelScale ?? 1.0;
    const worldPos = new THREE.Vector3();
    ctx.node.getWorldPosition(worldPos);

    // Deterministic rotation based on position
    const rotHash = ctx.hashString(
      `${id}_${position.x.toFixed(1)}_${position.z.toFixed(1)}`
    );
    const rotation = ((rotHash % 1000) / 1000) * Math.PI * 2;

    const success = await addResourceInstance(
      config.model,
      id,
      worldPos,
      rotation,
      baseScale,
      config.depletedModelPath ?? null,
      config.depletedModelScale ?? 0.3
    );

    if (success) {
      this.instanced = true;
      if (config.depleted) {
        setResourceDepleted(id, true);
      }
      createCollisionProxy(ctx, baseScale);
      return;
    }

    // Fallback to non-instanced rendering
    this.fallback = new StandardModelVisualStrategy();
    await this.fallback.createVisual(ctx);
  }

  async onDepleted(ctx: ResourceVisualContext): Promise<boolean> {
    if (this.fallback) {
      return this.fallback.onDepleted();
    }

    if (this.instanced) {
      setResourceDepleted(ctx.id, true);
    }
    const proxy = ctx.getMesh();
    if (proxy) {
      proxy.userData.depleted = true;
      proxy.userData.interactable = false;
    }
    return hasResourceDepleted(ctx.id);
  }

  getHighlightMesh(ctx: ResourceVisualContext): THREE.Object3D | null {
    if (this.fallback) return null;
    return getResourceHighlightMesh(ctx.id);
  }

  async onRespawn(ctx: ResourceVisualContext): Promise<void> {
    if (this.fallback) {
      await this.fallback.onRespawn(ctx);
      return;
    }

    if (this.instanced) {
      setResourceDepleted(ctx.id, false);
    }

    const proxy = ctx.getMesh();
    if (proxy) {
      proxy.userData.depleted = false;
      proxy.userData.interactable = true;
    }
  }

  update(ctx: ResourceVisualContext, deltaTime: number): void {
    if (this.fallback) {
      this.fallback.update(ctx);
      return;
    }

    updateGLBResourceInstancer();
  }

  destroy(ctx: ResourceVisualContext): void {
    if (this.fallback) {
      this.fallback.destroy(ctx);
      return;
    }

    if (this.instanced) {
      removeResourceInstance(ctx.id);
      this.instanced = false;
    }

    const proxy = ctx.getMesh();
    if (proxy) {
      const mesh = proxy as THREE.Mesh;
      if (mesh.geometry) mesh.geometry.dispose();
      if (mesh.material) (mesh.material as THREE.Material).dispose();
      ctx.node.remove(proxy);
      ctx.setMesh(null);
    }
  }
}
```

### Collision Proxy

Since `InstancedMesh` is not raycastable, the strategy creates an invisible collision proxy:

```typescript theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
function createCollisionProxy(ctx: ResourceVisualContext, scale: number): void {
  const isTree = ctx.config.resourceType === "tree";
  const geometry = isTree
    ? new THREE.CylinderGeometry(0.5 * scale, 0.5 * scale, 2 * scale, 6)
    : new THREE.BoxGeometry(0.8 * scale, 0.8 * scale, 0.8 * scale);

  const material = new MeshBasicNodeMaterial();
  material.visible = false;

  const proxy = new THREE.Mesh(geometry, material);
  if (isTree) proxy.position.y = scale;
  else proxy.position.y = 0.4 * scale;
  
  proxy.name = `InstancedProxy_${ctx.id}`;
  proxy.userData = {
    type: "resource",
    entityId: ctx.id,
    name: ctx.config.name,
    interactable: true,
    resourceType: ctx.config.resourceType,
    depleted: ctx.config.depleted,
  };
  proxy.layers.set(1);

  ctx.node.add(proxy);
  ctx.setMesh(proxy);
}
```

**Key Points:**

* Invisible mesh (`material.visible = false`)
* Proper userData for interaction detection
* Layer 1 for raycasting
* Persists across depletion/respawn transitions

***

## Highlight System

### EntityHighlightService Integration

The highlight service supports instanced entities via the `getHighlightRoot()` method:

```typescript theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
// From packages/shared/src/entities/world/ResourceEntity.ts
public getHighlightRoot(): THREE.Object3D | null {
  if (typeof this.visual.getHighlightMesh === "function") {
    return this.visual.getHighlightMesh(this.getVisualCtx());
  }
  return null;
}
```

**Highlight Flow:**

1. User hovers over instanced entity
2. `EntityHighlightService.setHoverTarget()` called
3. Service calls `entity.getHighlightRoot()`
4. Strategy returns positioned highlight mesh
5. Service adds mesh to scene temporarily
6. Outline pass renders highlight mesh
7. Service removes mesh when hover ends

### Highlight Mesh Lifecycle

```typescript theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
// From EntityHighlightService.ts
private activeHighlightMesh: THREE.Object3D | null = null;

setHoverTarget(target: RaycastTarget | null): void {
  const newId = target?.entityId ?? null;
  if (newId === this.currentTargetId) return;

  this.removeActiveHighlightMesh(); // Remove old highlight
  this.currentTargetId = newId;

  if (!this.composer) return;

  if (!target || !target.entity) {
    this.composer.setOutlineObjects([]);
    return;
  }

  // Try instanced highlight path first
  const entity = target.entity as unknown as Record<string, unknown>;
  if (typeof entity.getHighlightRoot === "function") {
    const hlRoot = (entity.getHighlightRoot as () => THREE.Object3D | null)();
    if (hlRoot) {
      this.world.stage?.scene?.add?.(hlRoot);
      this.activeHighlightMesh = hlRoot;
      const meshes = this.collectMeshes(hlRoot);
      if (meshes.length > 0) {
        const color = this.getHighlightColor(target.entityType);
        this.composer.setOutlineColor(color);
        this.composer.setOutlineObjects(meshes);
        return;
      }
    }
  }

  // Fallback: use entity's own scene-graph mesh
  const mesh = target.entity.mesh;
  const node = target.entity.node;
  const root = mesh ?? node;
  // ...
}

private removeActiveHighlightMesh(): void {
  if (this.activeHighlightMesh) {
    this.world.stage?.scene?.remove?.(this.activeHighlightMesh);
    this.activeHighlightMesh = null;
  }
}
```

**State Transition Handling:**

When a resource transitions between normal and depleted states, the old highlight mesh is removed:

```typescript theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
// From GLBResourceInstancer.ts
export function setDepleted(entityId: string, depleted: boolean): void {
  const modelPath = entityToModel.get(entityId);
  if (!modelPath) return;

  const pool = pools.get(modelPath);
  if (!pool) return;

  const slot = pool.instances.get(entityId);
  if (!slot || slot.depleted === depleted) return;

  // Remove stale highlight mesh from scene
  const oldHlMesh = slot.depleted
    ? pool.depletedHighlightMesh
    : pool.highlightMesh;
  if (oldHlMesh?.parent) {
    oldHlMesh.parent.remove(oldHlMesh);
  }

  slot.depleted = depleted;
  // ... transition logic ...
}
```

***

## Depleted Model System

### Configuration

Resources specify depleted models in their manifest:

```json theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
{
  "id": "oak_tree",
  "name": "Oak tree",
  "resourceType": "tree",
  "model": "trees/oak.glb",
  "modelScale": 3.0,
  "depletedModelPath": "trees/oak_stump.glb",
  "depletedModelScale": 0.3,
  "respawnTime": 30
}
```

### Instancer Implementation

The instancer maintains separate pools for normal and depleted states:

```typescript theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
interface ModelPool {
  modelPath: string;
  lod0: LODPool | null;
  lod1: LODPool | null;
  lod2: LODPool | null;
  depleted: LODPool | null; // NEW: Separate pool for depleted models
  instances: Map<string, ResourceSlot>;
  yOffset: number;
  depletedYOffset: number; // NEW: Y-offset for depleted models
  highlightMesh: THREE.Mesh | null;
  depletedHighlightMesh: THREE.Mesh | null; // NEW: Highlight for depleted state
}
```

**Depletion Flow:**

1. Resource depleted → `setDepleted(entityId, true)` called
2. Remove instance from current LOD pool (LOD0/LOD1/LOD2)
3. Add instance to depleted pool with `depletedScale`
4. Update collision proxy userData (`depleted: true, interactable: false`)
5. Remove old highlight mesh if entity is currently hovered

**Respawn Flow:**

1. Resource respawns → `setDepleted(entityId, false)` called
2. Remove instance from depleted pool
3. Add instance back to LOD0 pool with normal scale
4. Update collision proxy userData (`depleted: false, interactable: true`)

### Y-Offset Calculation

Each model pool calculates Y-offsets to ensure models sit flush on terrain:

```typescript theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
function computeYOffset(root: THREE.Object3D, scale: number): number {
  const saved = root.scale.clone();
  root.scale.set(scale, scale, scale);
  const bbox = new THREE.Box3().setFromObject(root);
  root.scale.copy(saved);
  return -bbox.min.y; // Offset to align bottom of bbox with y=0
}
```

**Separate Offsets:**

* `yOffset` - Normal model offset
* `depletedYOffset` - Depleted model offset (stumps may have different proportions)

***

## Performance Characteristics

### Draw Call Reduction

**Before Instancing:**

* 1000 oak trees = 1000 draw calls (one per tree)
* 500 copper rocks = 500 draw calls
* Total: 1500 draw calls for resources

**After Instancing:**

* 1000 oak trees = 3 draw calls (LOD0, LOD1, LOD2)
* 500 copper rocks = 3 draw calls (LOD0, LOD1, LOD2)
* Total: 6 draw calls for resources

**Reduction:** 99.6% fewer draw calls

### Memory Efficiency

**Shared Resources:**

* Geometry buffers shared across all instances
* Materials shared per LOD level
* Highlight meshes shared per model pool
* Only instance matrices stored per-entity

**Memory Savings:**

* 1000 trees with individual meshes: \~50MB geometry data
* 1000 trees with instancing: \~50KB geometry data + 64KB matrices
* **Savings:** 99% reduction in geometry memory

### LOD Hysteresis

Hysteresis prevents flickering when camera distance oscillates near LOD boundaries:

```typescript theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
const hysteresisSq = 0.81; // 90% of distance

// Switching from LOD0 to LOD1
if (distSq < lod1DistSq * hysteresisSq) {
  targetLOD = 0; // Stay at LOD0
} else if (distSq < lod1DistSq) {
  targetLOD = slot.currentLOD === 0 ? 0 : 1; // Only switch if already at LOD1
}
```

**Effect:**

* LOD0 → LOD1 switch at 100% distance
* LOD1 → LOD0 switch at 90% distance
* 10% hysteresis band prevents rapid switching

***

## Fallback Behavior

### When Instancing Fails

The strategy falls back to `StandardModelVisualStrategy` when:

* Instance pool is full (MAX\_INSTANCES = 512)
* Model loading fails
* Instancer not initialized

```typescript theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
const success = await addResourceInstance(/* ... */);

if (success) {
  this.instanced = true;
  createCollisionProxy(ctx, baseScale);
  return;
}

// Fallback to non-instanced rendering
this.fallback = new StandardModelVisualStrategy();
await this.fallback.createVisual(ctx);
```

**Fallback Behavior:**

* All methods delegate to fallback strategy
* No instancing benefits, but entity still renders
* Prevents visual gaps when pools are full

***

## Integration

### Initialization

The instancers are initialized in `createClientWorld.ts`:

```typescript theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
// From packages/shared/src/runtime/createClientWorld.ts
import {
  initGLBResourceInstancer,
  destroyGLBResourceInstancer,
} from "../systems/shared/world/GLBResourceInstancer";

// Clean up any previous instancer state from prior world
destroyGLBTreeInstancer();
destroyPlaceholderInstancer();
destroyGLBResourceInstancer();

// ... later, after stage system is ready ...

if (stageSystem) {
  stageSystem.THREE = THREE as unknown as StageSystem["THREE"];
  initGLBTreeInstancer(stageSystem.scene as unknown as THREE.Scene, world);
  initPlaceholderInstancer(stageSystem.scene as unknown as THREE.Scene);
  initGLBResourceInstancer(
    stageSystem.scene as unknown as THREE.Scene,
    world
  );
}
```

### Per-Frame Update

The instancer must be updated every frame for LOD switching:

```typescript theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
// From InstancedModelVisualStrategy.ts
update(ctx: ResourceVisualContext, deltaTime: number): void {
  if (this.fallback) {
    this.fallback.update(ctx);
    return;
  }

  updateGLBResourceInstancer(); // Updates all instances
}
```

**Update Logic:**

* Checks camera distance for each instance
* Switches LOD levels as needed
* Updates dissolve material uniforms (camera pos, player pos)
* Marks instance matrices as dirty when instances move between pools

***

## Testing

### Unit Tests

The instanced rendering system includes comprehensive tests:

```typescript theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
// Example test structure
describe("GLBResourceInstancer", () => {
  it("should add instance to LOD0 pool", async () => {
    const success = await addInstance(
      "models/rock.glb",
      "rock_1",
      new THREE.Vector3(10, 0, 10),
      0,
      1.0
    );
    expect(success).toBe(true);
  });

  it("should switch LOD based on distance", () => {
    // Move camera far away
    world.camera.position.set(100, 0, 100);
    updateGLBResourceInstancer();
    
    // Verify instance moved to LOD2 pool
    const slot = getInstanceSlot("rock_1");
    expect(slot.currentLOD).toBe(2);
  });

  it("should handle depletion transition", () => {
    setDepleted("rock_1", true);
    
    // Verify instance moved to depleted pool
    const inDepletedPool = hasDepleted("rock_1");
    expect(inDepletedPool).toBe(true);
  });
});
```

### Integration Tests

Full resource lifecycle tests:

```typescript theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
it("should render instanced resource with depletion", async () => {
  // Spawn resource
  const resource = await world.spawnResource({
    id: "oak_tree",
    position: { x: 10, y: 0, z: 10 }
  });

  // Verify instanced
  expect(hasInstance(resource.id)).toBe(true);

  // Deplete resource
  await resource.deplete();

  // Verify moved to depleted pool
  expect(hasDepleted(resource.id)).toBe(true);

  // Respawn resource
  await resource.respawn();

  // Verify back in normal pool
  expect(hasDepleted(resource.id)).toBe(false);
});
```

***

## Related Documentation

<CardGroup cols={2}>
  <Card title="ECS Architecture" icon="boxes-stacked" href="/wiki/engine/ecs">
    Entity Component System architecture and patterns
  </Card>

  <Card title="Particle System" icon="sparkles" href="/wiki/engine/particles">
    GPU-instanced particle rendering system
  </Card>

  <Card title="Resource Entities" icon="tree" href="/wiki/game-systems/resources">
    Resource entity lifecycle and gathering mechanics
  </Card>

  <Card title="Performance Guide" icon="gauge-high" href="/wiki/engine/performance">
    Performance optimization techniques and benchmarks
  </Card>
</CardGroup>
