| 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, }}
=
, //
, // ( )
'', //
, //
}}
= ]} // ( , )
= //
= // ( )
= , }) => {
// 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 */}
{/* Environment for reflections */}
{/* Default camera */}
{/* Game content */}
{children}
{/* Performance stats in development */}
{showStats && process.env.NODE_ENV === 'development' && }
);
};
✅ 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 =
= // , ""
/>
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]}
=
=
=
=
/>
);
};
: . = {
(
);
};
✅ 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 (
);
};
: . = {
(
);
};
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',
,
`#${()(, '')}`,
'',
}}>
{name}
);
};
: .<{ : }> = {
(
);
};
: . = {
(
);
};
✅ 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: .<{ : }> = {
(
);
};
: . = {
(
);
};
✅ 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 />
</>
);
};
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 : . = {
geometry = ( .(, , ), []);
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;
{
geometry.();
material.();
texture.();
};
}, []);
(!geometryRef. || !materialRef.) {
;
}
(
);
};
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, ]}>
))}
);
};
: . = {
(
);
};
: .<{ : }> = {
meshRef = useRef<.>();
( {
(!meshRef.) ;
meshRef... += delta * ;
});
;
};
✅ 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( .(, , ), delta);
meshRef...(tempQuaternion);
});
(
);
};
: . = {
meshRef = useRef<.>();
( {
(!meshRef.) ;
tempVector = .(
.(state..),
.(state..),
);
meshRef...(tempVector);
});
;
};
✅ 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) => (
< = = />
))}
);
};
: .<{ : }> = {
(
);
};
✅ 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>
{/* New geometry every render! */}
);
};
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.()).();
});
(, {
{ container } = (
);
(container).();
});
});
✅ 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