| name | threejs-best-practices |
| description | Enforces @react-three/fiber and @react-three/drei best practices for Black Trigram,
including proper Canvas setup, Korean-themed lighting, resource cleanup patterns,
Html overlay vs 3D mesh decision guide, performance optimization (useFrame, useMemo),
testing patterns, and common Three.js anti-patterns to avoid.
|
| license | MIT |
Three.js Best Practices Skill
Purpose
This skill ensures that all Three.js implementations in Black Trigram follow React Three Fiber best practices, maintain 60fps performance, properly manage resources, and create immersive 3D Korean martial arts experiences with proper separation between 3D world and UI overlays.
When to Apply
Automatically trigger this skill when:
- Creating or modifying Three.js scenes or components
- Implementing 3D models, characters, or environments
- Adding visual effects, particles, or shaders
- Working with cameras, lights, or materials
- Creating UI overlays with
<Html> components
- Implementing animations with
useFrame
- Debugging performance issues in 3D rendering
- Implementing camera controls or orbital navigation
- Optimizing rendering performance (instancing, LOD, culling)
- Adding textures, geometries, or Three.js objects
- Optimizing 3D rendering performance
- Reviewing pull requests with Three.js changes
Core Principles
1. Canvas Setup and Scene Configuration
ALWAYS use proper Canvas configuration:
✅ Standard Canvas Setup for Black Trigram
import { Canvas } from '@react-three/fiber';
import { PerspectiveCamera, Environment, Stats } from '@react-three/drei';
import { KOREAN_COLORS } from '@/types/constants';
import * as THREE from 'three';
interface Scene3DWrapperProps {
readonly width: number;
readonly height: number;
readonly children: React.ReactNode;
readonly showStats?: boolean;
readonly enableShadows?: boolean;
}
export const Scene3DWrapper: React.FC<Scene3DWrapperProps> = ({
width,
height,
children,
showStats = false,
enableShadows = true,
}) => {
return (
<Canvas
style={{ width, height }}
gl={{
antialias: true, // Smooth edges
alpha: false, // Opaque background (better performance)
powerPreference: 'high-performance', // Use discrete GPU if available
preserveDrawingBuffer: false, // Better performance
}}
dpr={[1, 2]} // Adaptive pixel ratio (1x on low-end, 2x on high-end)
shadows={enableShadows} // Enable shadow rendering
frameloop="always" // Always render (for games)
onCreated={({ gl, scene }) => {
// Set Korean cyberpunk background
gl.setClearColor(KOREAN_COLORS.UI_BACKGROUND_DARK, 1);
// Add fog for depth perception
scene.fog = new THREE.Fog(
KOREAN_COLORS.UI_BACKGROUND_DARK,
10, // Near distance
50 // Far distance
);
// Enable tone mapping for better colors
gl.toneMapping = THREE.ACESFilmicToneMapping;
gl.toneMappingExposure = 1.0;
}}
data-testid="game-canvas"
>
{/* Korean-themed lighting */}
<ambientLight intensity={0.4} color={KOREAN_COLORS.PRIMARY_CYAN} />
<directionalLight
position={[10, 10, 5]}
intensity={1}
castShadow
shadow-camera-left={-10}
shadow-camera-right={10}
shadow-camera-top={10}
shadow-camera-bottom={-10}
shadow-mapSize={[2048, 2048]}
color={KOREAN_COLORS.ACCENT_GOLD}
/>
{/* Environment for reflections */}
<Environment preset="city" />
{/* Default camera */}
<PerspectiveCamera makeDefault position={[0, 5, 10]} fov={75} />
{/* Game content */}
{children}
{/* Performance stats in development */}
{showStats && process.env.NODE_ENV === 'development' && <Stats />}
</Canvas>
);
};
✅ Canvas Performance Configuration
<Canvas
gl={{
antialias: true,
alpha: false,
powerPreference: 'high-performance',
stencil: false,
depth: true,
}}
dpr={[1, 2]}
shadows
frameloop="always"
performance={{
current: 1,
min: 0.5,
max: 1,
debounce: 200,
}}
/>
<Canvas
gl={{ antialias: true, alpha: true }} // Alpha = performance cost
dpr={3} // Fixed 3x DPR = performance cost
frameloop="demand" // For games, use "always"
/>
2. Korean-Themed Lighting and Aesthetics
ALWAYS apply Korean cyberpunk lighting:
✅ Standard Three-Light Setup
import { KOREAN_COLORS } from '@/types/constants';
export const KoreanSceneLighting: React.FC = () => {
return (
<>
{/* 1. Ambient light for base illumination (Korean cyan) */}
<ambientLight
intensity={0.4}
color={KOREAN_COLORS.PRIMARY_CYAN}
/>
{/* 2. Directional light for main shadows (Korean gold) */}
<directionalLight
position={[10, 10, 5]}
intensity={1}
castShadow
shadow-mapSize={[2048, 2048]}
shadow-camera-far={50}
shadow-camera-left={-10}
shadow-camera-right={10}
shadow-camera-top={10}
shadow-camera-bottom={-10}
color={KOREAN_COLORS.ACCENT_GOLD}
/>
{/* 3. Accent point light for highlights (Korean blue) */}
<pointLight
position={[-10, 5, -5]}
intensity={0.5}
distance={20}
decay={2}
color={KOREAN_COLORS.ACCENT_BLUE}
/>
</>
);
};
export const UnoptimizedLighting: React.FC = () => {
return (
<>
{/* 50 point lights = FPS drops to 10! */}
{Array.from({ length: 50 }).map((_, i) => (
<pointLight key={i} position={[i, 0, 0]} castShadow />
))}
</>
);
};
✅ Korean-Themed Materials
import { useMemo, useEffect } from 'react';
import * as THREE from 'three';
import { KOREAN_COLORS } from '@/types/constants';
export const KoreanStyledMesh: React.FC<{ stance: TrigramStance }> = ({ stance }) => {
const material = useMemo(() => {
const stanceColor = TRIGRAM_STANCES[stance].color;
return new THREE.MeshStandardMaterial({
color: stanceColor,
emissive: KOREAN_COLORS.PRIMARY_CYAN,
emissiveIntensity: 0.2,
metalness: 0.5,
roughness: 0.5,
envMapIntensity: 1.0,
});
}, [stance]);
useEffect(() => {
return () => material.dispose();
}, [material]);
return (
<mesh material={material} castShadow receiveShadow>
<boxGeometry args={[1, 1, 1]} />
</mesh>
);
};
export const EnhancedKoreanMaterial: React.FC = () => {
return (
<mesh>
<boxGeometry args={[1, 2, 0.1]} />
<meshStandardMaterial
color={KOREAN_COLORS.UI_BACKGROUND_MEDIUM}
emissive={KOREAN_COLORS.PRIMARY_CYAN}
emissiveIntensity={0.3}
metalness={0.6}
roughness={0.4}
transparent={true}
opacity={0.9}
/>
</mesh>
);
};
3. Html Overlay vs 3D Mesh Decision Guide
ALWAYS choose the right approach:
✅ Use Html Overlays For:
import { Html } from '@react-three/drei';
import { FONT_FAMILY, KOREAN_COLORS } from '@/types/constants';
export const PlayerNametag: React.FC<{ name: string; health: number }> = ({ name, health }) => {
return (
<Html position={[0, 2.5, 0]} center distanceFactor={10}>
<div style={{
background: `#${KOREAN_COLORS.UI_BACKGROUND_DARK.toString(16).padStart(6, '0')}dd`,
border: `2px solid #${KOREAN_COLORS.PRIMARY_CYAN.toString(16).padStart(6, '0')}`,
borderRadius: '8px',
padding: '8px 12px',
fontFamily: FONT_FAMILY.KOREAN,
color: `#${KOREAN_COLORS.TEXT_PRIMARY.toString(16).padStart(6, '0')}`,
pointerEvents: 'none',
}}>
<div>{name}</div>
<HealthBar value={health} />
</div>
</Html>
);
};
export const CombatMenu: React.FC<{ onAction: (action: string) => void }> = ({ onAction }) => {
return (
<Html fullscreen>
<div style={{
position: 'absolute',
bottom: 20,
left: '50%',
transform: 'translateX(-50%)',
display: 'flex',
gap: '10px',
}}>
<button
onClick={() => onAction('attack')}
style={{
fontFamily: FONT_FAMILY.KOREAN,
background: `#${KOREAN_COLORS.PRIMARY_CYAN.toString(16)}`,
border: 'none',
padding: '10px 20px',
cursor: 'pointer',
}}
>
공격 | Attack
</button>
<button onClick={() => onAction('defend')}>
방어 | Defend
</button>
</div>
</Html>
);
};
export const CombatHUD: React.FC = () => {
return (
<Html fullscreen>
<div style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: '100%',
pointerEvents: 'none',
}}>
<div style={{ position: 'absolute', top: 20, left: 20 }}>
<PlayerStatus />
</div>
<div style={{ position: 'absolute', top: 20, right: 20 }}>
<EnemyStatus />
</div>
<div style={{ position: 'absolute', bottom: 20, left: '50%', transform: 'translateX(-50%)' }}>
<StanceIndicator />
</div>
</div>
</Html>
);
};
✅ Use 3D Meshes For:
export const CombatCharacter3D: React.FC<{
position: Vector3;
stance: TrigramStance;
isAttacking: boolean;
}> = ({ position, stance, isAttacking }) => {
return (
<group position={position}>
{/* Character mesh */}
<mesh castShadow receiveShadow>
<capsuleGeometry args={[0.5, 1.5, 16, 32]} />
<meshStandardMaterial
color={TRIGRAM_STANCES[stance].color}
emissive={KOREAN_COLORS.PRIMARY_CYAN}
emissiveIntensity={isAttacking ? 0.5 : 0.1}
/>
</mesh>
{/* Stance aura (3D effect) */}
<StanceAura3D stance={stance} />
</group>
);
};
export const AttackEffect3D: React.FC<{ position: Vector3 }> = ({ position }) => {
return (
<group position={position}>
{/* Point light for impact */}
<pointLight
intensity={2}
distance={5}
decay={2}
color={KOREAN_COLORS.ACCENT_GOLD}
/>
{/* Particle system */}
<ImpactParticles count={50} spread={2} />
</group>
);
};
export const CombatArena3D: React.FC = () => {
return (
<group>
{/* Floor */}
<mesh rotation={[-Math.PI / 2, 0, 0]} receiveShadow>
<planeGeometry args={[20, 20]} />
<meshStandardMaterial
color={KOREAN_COLORS.UI_BACKGROUND_DARK}
roughness={0.8}
/>
</mesh>
{/* Arena walls */}
<ArenaWalls />
{/* Background elements */}
<KoreanArchitecture />
</group>
);
};
✅ Hybrid Approach (Recommended)
export const CombatSceneHybrid: React.FC = () => {
return (
<Scene3DWrapper width={1200} height={800}>
{/* 3D WORLD: Characters, environment, effects */}
<CombatArena3D />
<CombatCharacter3D position={[-5, 0, 0]} stance="geon" />
<CombatCharacter3D position={[5, 0, 0]} stance="gon" />
<ParticleSystem3D />
{/* HTML OVERLAYS: UI elements, text, menus */}
<Html position={[-5, 2.5, 0]} center>
<PlayerNametag name="Player 1" health={85} />
</Html>
<Html fullscreen>
<CombatHUD />
<ControlPanel />
</Html>
</Scene3DWrapper>
);
};
4. Proper Three.js Resource Cleanup
ALWAYS dispose of Three.js resources:
✅ Geometry and Material Disposal
import { useEffect, useMemo } from 'react';
import * as THREE from 'three';
export const ProperCleanup: React.FC = () => {
const geometry = useMemo(() => new THREE.BoxGeometry(1, 1, 1), []);
const material = useMemo(
() => new THREE.MeshStandardMaterial({ color: 0x00ffff }),
[]
);
useEffect(() => {
return () => {
geometry.dispose();
material.dispose();
};
}, [geometry, material]);
return (
<mesh geometry={geometry} material={material}>
{/* No inline geometry/material = proper cleanup */}
</mesh>
);
};
export const MemoryLeak: React.FC = () => {
const geometry = useMemo(() => new THREE.BoxGeometry(1, 1, 1), []);
const material = useMemo(
() => new THREE.MeshStandardMaterial({ color: 0x00ffff }),
[]
);
return <mesh geometry={geometry} material={material} />;
};
✅ Texture Disposal
import { useTexture } from '@react-three/drei';
import { useEffect } from 'react';
export const TextureCleanup: React.FC = () => {
const texture = useTexture('/textures/character.jpg');
useEffect(() => {
return () => {
texture.dispose();
};
}, [texture]);
return (
<mesh>
<boxGeometry />
<meshStandardMaterial map={texture} />
</mesh>
);
};
✅ Complete Cleanup Pattern
export const CompleteCleanup: React.FC = () => {
const geometryRef = useRef<THREE.BufferGeometry | null>(null);
const materialRef = useRef<THREE.Material | null>(null);
const textureRef = useRef<THREE.Texture | null>(null);
useEffect(() => {
const loader = new THREE.TextureLoader();
const texture = loader.load('/texture.jpg');
textureRef.current = texture;
const geometry = new THREE.BoxGeometry(1, 1, 1);
geometryRef.current = geometry;
const material = new THREE.MeshStandardMaterial({ map: texture });
materialRef.current = material;
return () => {
geometry.dispose();
material.dispose();
texture.dispose();
};
}, []);
if (!geometryRef.current || !materialRef.current) {
return null;
}
return (
<mesh
geometry={geometryRef.current}
material={materialRef.current}
/>
);
};
5. Efficient useFrame Patterns
ALWAYS optimize useFrame usage:
✅ Batch Operations in useFrame
import { useFrame } from '@react-three/fiber';
import { useRef } from 'react';
export const BatchedAnimations: React.FC = () => {
const groupRef = useRef<THREE.Group>(null);
useFrame((state, delta) => {
if (!groupRef.current) return;
groupRef.current.children.forEach((child, i) => {
child.rotation.y += delta * 0.5;
child.position.y = Math.sin(state.clock.elapsedTime + i) * 0.5;
});
});
return (
<group ref={groupRef}>
{Array.from({ length: 100 }).map((_, i) => (
<mesh key={i} position={[i * 2, 0, 0]}>
<boxGeometry />
<meshStandardMaterial />
</mesh>
))}
</group>
);
};
export const UnbatchedAnimations: React.FC = () => {
return (
<>
{Array.from({ length: 100 }).map((_, i) => (
<AnimatedObject key={i} index={i} />
))}
</>
);
};
const AnimatedObject: React.FC<{ index: number }> = ({ index }) => {
const meshRef = useRef<THREE.Mesh>(null);
useFrame((state, delta) => {
if (!meshRef.current) return;
meshRef.current.rotation.y += delta * 0.5;
});
return <mesh ref={meshRef}><boxGeometry /></mesh>;
};
✅ Reuse Temporary Objects
import { useMemo } from 'react';
export const EfficientUseFrame: React.FC = () => {
const meshRef = useRef<THREE.Mesh>(null);
const tempVector = useMemo(() => new THREE.Vector3(), []);
const tempQuaternion = useMemo(() => new THREE.Quaternion(), []);
useFrame((state, delta) => {
if (!meshRef.current) return;
tempVector.set(
Math.sin(state.clock.elapsedTime),
Math.cos(state.clock.elapsedTime),
0
);
meshRef.current.position.copy(tempVector);
tempQuaternion.setFromAxisAngle(new THREE.Vector3(0, 1, 0), delta);
meshRef.current.quaternion.multiply(tempQuaternion);
});
return (
<mesh ref={meshRef}>
<boxGeometry />
<meshStandardMaterial />
</mesh>
);
};
export const WastefulUseFrame: React.FC = () => {
const meshRef = useRef<THREE.Mesh>(null);
useFrame((state, delta) => {
if (!meshRef.current) return;
const tempVector = new THREE.Vector3(
Math.sin(state.clock.elapsedTime),
Math.cos(state.clock.elapsedTime),
0
);
meshRef.current.position.copy(tempVector);
});
return <mesh ref={meshRef}><boxGeometry /></mesh>;
};
✅ Conditional useFrame Execution
export const ConditionalFrame: React.FC<{ isActive: boolean }> = ({ isActive }) => {
const meshRef = useRef<THREE.Mesh>(null);
useFrame((state, delta) => {
if (!isActive || !meshRef.current) return;
meshRef.current.rotation.y += delta;
});
return <mesh ref={meshRef}><boxGeometry /></mesh>;
};
6. Performance Optimization Patterns
ALWAYS use efficient Three.js patterns:
✅ Instancing for Repeated Geometry
import { Instances, Instance } from '@react-three/drei';
export const OptimizedParticles: React.FC<{ count: number }> = ({ count }) => {
const positions = useMemo(
() => Array.from({ length: count }, () => [
Math.random() * 20 - 10,
Math.random() * 10,
Math.random() * 20 - 10,
] as [number, number, number]),
[count]
);
return (
<Instances limit={count}>
<sphereGeometry args={[0.1, 8, 8]} />
<meshBasicMaterial color={KOREAN_COLORS.PRIMARY_CYAN} />
{positions.map((pos, i) => (
<Instance key={i} position={pos} />
))}
</Instances>
);
};
export const UnoptimizedParticles: React.FC<{ count: number }> = ({ count }) => {
return (
<>
{Array.from({ length: count }).map((_, i) => (
<mesh key={i} position={[Math.random() * 20 - 10, 0, 0]}>
<sphereGeometry args={[0.1, 8, 8]} />
<meshBasicMaterial color={0x00ffff} />
</mesh>
))}
</>
);
};
✅ Level of Detail (LOD)
import { Detailed } from '@react-three/drei';
export const OptimizedCharacter: React.FC = () => {
return (
<Detailed distances={[0, 10, 20, 40]}>
{/* High detail (0-10 units) */}
<HighDetailMesh triangles={5000} />
{/* Medium detail (10-20 units) */}
<MediumDetailMesh triangles={1500} />
{/* Low detail (20-40 units) */}
<LowDetailMesh triangles={500} />
{/* Billboard (40+ units) */}
<CharacterBillboard triangles={2} />
</Detailed>
);
};
✅ Memoize Expensive Calculations
export const MemoizedComponent: React.FC<{ data: ComplexData }> = ({ data }) => {
const processedData = useMemo(() => {
return expensiveProcessing(data);
}, [data]);
const geometry = useMemo(
() => new THREE.BoxGeometry(1, 1, 1),
[]
);
return (
<mesh geometry={geometry}>
<meshStandardMaterial />
</mesh>
);
};
export const UnmemoizedComponent: React.FC<{ data: ComplexData }> = ({ data }) => {
const processedData = expensiveProcessing(data);
return (
<mesh>
<boxGeometry /> {/* New geometry every render! */}
<meshStandardMaterial />
</mesh>
);
};
7. Testing Three.js Components
ALWAYS test Three.js components:
✅ Basic Three.js Component Test
import { render } from '@testing-library/react';
import { Canvas } from '@react-three/fiber';
import { describe, it, expect } from 'vitest';
import { Suspense } from 'react';
function render3D(component: React.ReactElement) {
return render(
<Canvas>
<Suspense fallback={null}>
{component}
</Suspense>
</Canvas>
);
}
describe('CombatCharacter3D', () => {
it('should render without crashing', () => {
const { container } = render3D(
<CombatCharacter3D
position={[0, 0, 0]}
stance="geon"
isAttacking={false}
/>
);
expect(container.querySelector('canvas')).toBeInTheDocument();
});
it('should apply Korean theming colors', () => {
const { container } = render3D(
<CombatCharacter3D
position={[0, 0, 0]}
stance="geon"
isAttacking={false}
/>
);
expect(container).toBeTruthy();
});
});
✅ Testing with @react-three/test-renderer
import { render } from '@react-three/test-renderer';
describe('Advanced Three.js Tests', () => {
it('should update position on animation', async () => {
const { scene, rerender, advance } = await render(
<AnimatedMesh initialPosition={[0, 0, 0]} />
);
await advance(1);
const mesh = scene.children[0] as THREE.Mesh;
expect(mesh.position.y).not.toBe(0);
});
});
8. Common Three.js Anti-Patterns to REJECT
Immediately flag and reject these patterns:
❌ Creating Objects in Render/useFrame
useFrame(() => {
const vector = new THREE.Vector3();
mesh.position.copy(vector);
});
const tempVector = useMemo(() => new THREE.Vector3(), []);
useFrame(() => {
tempVector.set(x, y, z);
mesh.position.copy(tempVector);
});
❌ Missing Resource Cleanup
const geometry = new THREE.BoxGeometry();
const material = new THREE.MeshStandardMaterial();
useEffect(() => {
const geometry = new THREE.BoxGeometry();
const material = new THREE.MeshStandardMaterial();
return () => {
geometry.dispose();
material.dispose();
};
}, []);
❌ Too Many Lights with Shadows
{lights.map((light, i) => (
<pointLight key={i} castShadow />
))}
<directionalLight castShadow position={[10, 10, 5]} />
<pointLight intensity={0.5} /> {}
❌ Inline Geometry/Material Creation
return (
<mesh>
<boxGeometry args={[1, 1, 1]} /> {/* New every render! */}
<meshStandardMaterial color={0xff0000} />
</mesh>
);
const geometry = useMemo(() => new THREE.BoxGeometry(1, 1, 1), []);
const material = useMemo(() => new THREE.MeshStandardMaterial({ color: 0xff0000 }), []);
Enforcement Rules
Rule 1: All Canvas Must Use Standard Configuration
IF (Canvas component created)
THEN (use Scene3DWrapper with Korean theming and performance settings)
ELSE (reject non-standard Canvas setup)
Rule 2: Resource Cleanup Required
IF (Three.js objects created: geometry, material, texture)
THEN (add cleanup in useEffect return)
ELSE (reject PR for memory leak)
Rule 3: Html for UI, Meshes for 3D
IF (adding UI element like button, text, menu)
THEN (use Html overlay)
ELSE IF (adding 3D game object, character, effect)
THEN (use 3D mesh)
ELSE (clarify use case and choose appropriate approach)
Rule 4: Batch useFrame Operations
IF (multiple objects need animation)
THEN (use single useFrame callback with group ref)
ELSE (reject multiple useFrame hooks)
Rule 5: Use Performance Optimization Patterns
IF (rendering repeated geometry OR distant objects)
THEN (use Instances for repeated, Detailed for LOD)
ELSE (optimize before merging)
Three.js Best Practices Checklist
Before approving any Three.js change:
ISO 27001 Alignment
This skill enforces controls from:
- A.12.1 - Operational procedures (3D rendering best practices)
- A.12.6 - Technical vulnerability management (resource cleanup prevents crashes)
NIST CSF 2.0 Alignment
- PR.DS-06: Integrity checking mechanisms (verify 3D scenes render correctly)
CIS Controls v8.1 Alignment
- Control 16: Application Software Security (secure Three.js implementation)
Korean Philosophy Integration
삼차원의 도 (The Way of Three Dimensions)
Core Three.js Principles:
- 효율성 (Efficiency) - Optimize every frame, every draw call
- 간결성 (Simplicity) - Use minimal resources for maximum effect
- 조화 (Harmony) - Balance visual quality with performance
- 정리 (Cleanup) - Properly dispose of all resources
- 명료성 (Clarity) - Clear separation between UI and 3D world
흑괘 Three.js 철학 (Black Trigram Three.js Philosophy):
- 몰입감 (Immersion) - Create authentic 3D martial arts experience
- 성능 (Performance) - 60fps is non-negotiable
- 미학 (Aesthetics) - Korean cyberpunk visual identity
Remember
Three.js mastery requires discipline—every resource must be managed, every frame optimized.
When implementing Three.js:
- SETUP - Use Scene3DWrapper with Korean theming
- SEPARATE - Html for UI, meshes for 3D world
- OPTIMIZE - Use Instances, LOD, and batching
- CLEANUP - Dispose all geometries, materials, textures
- BATCH - Single useFrame for multiple objects
- TEST - Verify components render correctly
- MEASURE - Profile and maintain 60fps
흑괘의 삼차원을 지켜라 - Protect the Three Dimensions of the Black Trigram