threejs
Installation
SKILL.md
Three.js / React Three Fiber Development Skill
Engine Detection
Look for: package.json with three, @react-three/fiber, @react-three/drei, .glb, .gltf, .hdr
Project Structure (Vanilla Three.js)
src/
main.ts # Entry point, renderer setup
scene/
SceneManager.ts # Scene lifecycle
LevelLoader.ts
entities/
Player.ts
Enemy.ts
systems/
InputSystem.ts
PhysicsSystem.ts
AudioSystem.ts
rendering/
MaterialLibrary.ts
PostProcessing.ts
ShaderChunks/
utils/
ObjectPool.ts
MathUtils.ts
types/
GameTypes.ts
public/
models/
textures/
audio/
Project Structure (React Three Fiber)
src/
App.tsx
components/
canvas/
GameCanvas.tsx # Canvas + providers
Scene.tsx # Main scene composition
entities/
Player.tsx
Enemy.tsx
environment/
Terrain.tsx
Skybox.tsx
Lighting.tsx
ui/
HUD.tsx
MainMenu.tsx
effects/
PostProcessing.tsx
Particles.tsx
hooks/
useGameLoop.ts
useInput.ts
usePhysics.ts
stores/
gameStore.ts # Zustand store
types/
game.ts
utils/
pool.ts
public/
models/
textures/
Scene Graph & Disposal
Proper resource management is critical. Three.js does NOT garbage collect GPU resources:
// Creating resources
const geometry = new THREE.BoxGeometry(1, 1, 1);
const material = new THREE.MeshStandardMaterial({ color: 0xff0000 });
const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
// MUST dispose when removing
scene.remove(mesh);
geometry.dispose();
material.dispose();
if (material.map) material.map.dispose();
// Dispose ALL textures: map, normalMap, roughnessMap, etc.
// Helper for deep disposal
function disposeObject(obj: THREE.Object3D): void {
obj.traverse((child) => {
if (child instanceof THREE.Mesh) {
child.geometry.dispose();
if (Array.isArray(child.material)) {
child.material.forEach(disposeMaterial);
} else {
disposeMaterial(child.material);
}
}
});
obj.removeFromParent();
}
function disposeMaterial(mat: THREE.Material): void {
for (const value of Object.values(mat)) {
if (value instanceof THREE.Texture) {
value.dispose();
}
}
mat.dispose();
}
Game Loop (Vanilla Three.js)
class Game {
private renderer: THREE.WebGLRenderer;
private scene: THREE.Scene;
private camera: THREE.PerspectiveCamera;
private clock = new THREE.Clock();
private systems: GameSystem[] = [];
constructor(canvas: HTMLCanvasElement) {
this.renderer = new THREE.WebGLRenderer({ canvas, antialias: true });
this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
this.renderer.setSize(window.innerWidth, window.innerHeight);
this.scene = new THREE.Scene();
this.camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
window.addEventListener('resize', this.onResize);
this.animate();
}
private animate = (): void => {
requestAnimationFrame(this.animate);
const delta = this.clock.getDelta();
const elapsed = this.clock.getElapsedTime();
for (const system of this.systems) {
system.update(delta, elapsed);
}
this.renderer.render(this.scene, this.camera);
};
private onResize = (): void => {
this.camera.aspect = window.innerWidth / window.innerHeight;
this.camera.updateProjectionMatrix();
this.renderer.setSize(window.innerWidth, window.innerHeight);
};
dispose(): void {
window.removeEventListener('resize', this.onResize);
this.renderer.dispose();
// Dispose all scene objects
disposeObject(this.scene);
}
}
React Three Fiber Patterns
// GameCanvas.tsx - Entry point
import { Canvas } from '@react-three/fiber';
import { Physics } from '@react-three/rapier';
export function GameCanvas() {
return (
<Canvas
camera={{ position: [0, 5, 10], fov: 60 }}
shadows
gl={{ antialias: true }}
>
<Physics>
<Scene />
</Physics>
</Canvas>
);
}
// Player.tsx - Entity component
import { useFrame } from '@react-three/fiber';
import { useRef } from 'react';
import { useInput } from '../hooks/useInput';
export function Player() {
const meshRef = useRef<THREE.Mesh>(null!);
const input = useInput();
useFrame((state, delta) => {
// Runs every frame inside the render loop
meshRef.current.position.x += input.horizontal * 5 * delta;
meshRef.current.position.z += input.vertical * 5 * delta;
});
return (
<mesh ref={meshRef} castShadow>
<boxGeometry args={[1, 2, 1]} />
<meshStandardMaterial color="blue" />
</mesh>
);
}
Zustand for Game State
// gameStore.ts
import { create } from 'zustand';
interface GameState {
health: number;
score: number;
isPaused: boolean;
takeDamage: (amount: number) => void;
addScore: (points: number) => void;
togglePause: () => void;
}
export const useGameStore = create<GameState>((set) => ({
health: 100,
score: 0,
isPaused: false,
takeDamage: (amount) =>
set((state) => ({ health: Math.max(0, state.health - amount) })),
addScore: (points) =>
set((state) => ({ score: state.score + points })),
togglePause: () =>
set((state) => ({ isPaused: !state.isPaused })),
}));
Object Pooling
class ObjectPool<T extends THREE.Object3D> {
private available: T[] = [];
private active = new Set<T>();
constructor(
private factory: () => T,
initialSize: number,
) {
for (let i = 0; i < initialSize; i++) {
this.available.push(factory());
}
}
acquire(): T {
const obj = this.available.pop() ?? this.factory();
obj.visible = true;
this.active.add(obj);
return obj;
}
release(obj: T): void {
obj.visible = false;
this.active.delete(obj);
this.available.push(obj);
}
get activeCount(): number {
return this.active.size;
}
}
Performance Optimization
// Use instancing for many identical objects
const instancedMesh = new THREE.InstancedMesh(geometry, material, count);
const matrix = new THREE.Matrix4();
for (let i = 0; i < count; i++) {
matrix.setPosition(positions[i]);
instancedMesh.setMatrixAt(i, matrix);
}
instancedMesh.instanceMatrix.needsUpdate = true;
// LOD (Level of Detail)
const lod = new THREE.LOD();
lod.addLevel(highDetailMesh, 0); // 0-50 units
lod.addLevel(mediumDetailMesh, 50); // 50-150 units
lod.addLevel(lowDetailMesh, 150); // 150+ units
// Texture optimization
const loader = new THREE.TextureLoader();
const texture = loader.load('texture.jpg');
texture.minFilter = THREE.LinearMipMapLinearFilter;
texture.generateMipmaps = true;
texture.anisotropy = renderer.capabilities.getMaxAnisotropy();
Key Rules
- Always dispose GPU resources - geometry, material, texture, render targets
- Use delta time in useFrame/animate - Frame-rate independent updates
- Limit pixel ratio -
Math.min(window.devicePixelRatio, 2)prevents GPU overload - Use instancing for repeated geometry - Massively reduces draw calls
- Remove event listeners on cleanup - Prevent memory leaks
- Use refs for Three.js objects in R3F - Not React state
- Keep React state out of the render loop - Use Zustand or refs for per-frame data
- Use drei helpers - OrbitControls, Environment, useGLTF, etc.
- Compress textures - KTX2/Basis Universal for GPU compression
- Profile with Stats.js and renderer.info - Monitor draw calls, triangles, textures
Common Anti-Patterns
- Creating new materials/geometries every frame
- Using
setStateinsideuseFrame- causes React re-renders - Not disposing loaded GLTF models when removed
- Using
THREE.Groupparent without disposing children - Large textures without power-of-2 dimensions or compression
Related skills
More from davincidreams/agent-team-plugins
blender
Blender interface, workflows, and 3D production pipeline
220rigging
Rigging fundamentals, skeleton setup, and animation controls
16animation
Animation principles, techniques, and best practices for 3D animation
13vroid
Vroid Studio, VRM format, and VTuber avatar creation
10technical-writing
Technical writing principles and best practices for creating clear, accurate documentation
9unreal
Unreal Engine patterns, Actor/Component model, Blueprints vs C++, and best practices
8