- 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**
```typescript
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**
```typescript
// GOOD: Optimized Canvas for 60fps
<Canvas
gl={{
antialias: true,
alpha: false, // Opaque background = better performance
powerPreference: 'high-performance',
stencil: false, // Disable if not using
depth: true, // Enable depth testing
}}
dpr={[1, 2]} // Adaptive DPR based on device
shadows // Enable shadows (moderate cost)
frameloop="always" // For games, always render
performance={{
current: 1, // Performance factor (1 = normal)
min: 0.5, // Minimum performance factor
max: 1, // Maximum performance factor
debounce: 200, // Debounce time for performance adjustments
}}
/>
// BAD: Unoptimized Canvas
<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**
```typescript
// GOOD: Efficient three-light setup with Korean colors
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}
/>
</>
);
};
// BAD: Too many lights (kills performance)
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**
```typescript
// GOOD: Korean cyberpunk materials with emissive effects
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>
);
};
// Enhanced material with Korean aesthetics
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:**
```typescript
import { Html } from '@react-three/drei';
import { FONT_FAMILY, KOREAN_COLORS } from '@/types/constants';
// 1. UI ELEMENTS (buttons, menus, text)
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>
);
};
// 2. INTERACTIVE MENUS
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>
);
};
// 3. FULLSCREEN UI OVERLAYS
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:**
```typescript
// 1. GAME OBJECTS AND CHARACTERS
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>
);
};
// 2. VISUAL EFFECTS AND PARTICLES
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>
);
};
// 3. ENVIRONMENT AND WORLD OBJECTS
export const CombatArena3D: React.FC = () => {
return (
<group>
{/* Floor */}
<mesh rotation={[-Math.PI / 2, 0, 0]} receiveShadow>
<planeGeometry args={[20, 20]} />
<meshStandardMaterial
在 GitHub 查看