Agent skill
threejs-patterns
Vanilla Three.js patterns for Next.js App Router. Scene setup, GLTF loading, camera scroll animation, particle systems, environment lighting, post-processing, OrbitControls. Pin three@0.169.0. Import from 'three/addons/'.
Install this agent skill to your Project
npx add-skill https://github.com/adilkalam/orca/tree/main/skills/threejs-patterns
SKILL.md
Three.js Patterns for Next.js
Version: three@0.169.0 + @types/three@0.169.0
Framework: Vanilla Three.js ONLY -- NOT React Three Fiber.
Import path for addons: import { X } from 'three/addons/...'
Installation
npm install three@0.169.0 @types/three@0.169.0
Pattern 1: Scene Setup
Base pattern for all Three.js work in Next.js.
'use client';
import { useEffect, useRef } from 'react';
import * as THREE from 'three';
export function ThreeScene({ className }: { className?: string }) {
const canvasRef = useRef<HTMLCanvasElement>(null);
useEffect(() => {
const canvas = canvasRef.current;
if (!canvas) return;
// Renderer
const renderer = new THREE.WebGLRenderer({
canvas,
antialias: true,
alpha: true,
});
renderer.setSize(canvas.clientWidth, canvas.clientHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
// Scene
const scene = new THREE.Scene();
// Camera
const camera = new THREE.PerspectiveCamera(
75,
canvas.clientWidth / canvas.clientHeight,
0.1,
1000
);
camera.position.z = 5;
// Lighting
const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
directionalLight.position.set(5, 5, 5);
scene.add(directionalLight);
// Animation loop
let animationId: number;
const animate = () => {
animationId = requestAnimationFrame(animate);
renderer.render(scene, camera);
};
animate();
// Resize handler
const handleResize = () => {
const width = canvas.clientWidth;
const height = canvas.clientHeight;
renderer.setSize(width, height);
camera.aspect = width / height;
camera.updateProjectionMatrix();
};
window.addEventListener('resize', handleResize);
// Cleanup
return () => {
cancelAnimationFrame(animationId);
window.removeEventListener('resize', handleResize);
renderer.dispose();
scene.traverse((object) => {
if (object instanceof THREE.Mesh) {
object.geometry.dispose();
if (Array.isArray(object.material)) {
object.material.forEach((m) => m.dispose());
} else {
object.material.dispose();
}
}
});
};
}, []);
return (
<canvas
ref={canvasRef}
className={className}
style={{ width: '100%', height: '100%', display: 'block' }}
/>
);
}
Pattern 2: GLTF Model Loading
'use client';
import { useEffect, useRef } from 'react';
import * as THREE from 'three';
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
import { DRACOLoader } from 'three/addons/loaders/DRACOLoader.js';
export function ModelViewer({ modelPath, className }: {
modelPath: string;
className?: string;
}) {
const canvasRef = useRef<HTMLCanvasElement>(null);
useEffect(() => {
const canvas = canvasRef.current;
if (!canvas) return;
const renderer = new THREE.WebGLRenderer({ canvas, antialias: true, alpha: true });
renderer.setSize(canvas.clientWidth, canvas.clientHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, canvas.clientWidth / canvas.clientHeight, 0.1, 1000);
camera.position.z = 5;
// Lighting
scene.add(new THREE.AmbientLight(0xffffff, 0.5));
const dirLight = new THREE.DirectionalLight(0xffffff, 1);
dirLight.position.set(5, 5, 5);
scene.add(dirLight);
// DRACO decoder for compressed models
const dracoLoader = new DRACOLoader();
dracoLoader.setDecoderPath('https://www.gstatic.com/draco/versioned/decoders/1.5.6/');
// GLTF loader
const gltfLoader = new GLTFLoader();
gltfLoader.setDRACOLoader(dracoLoader);
let model: THREE.Group | null = null;
gltfLoader.load(
modelPath,
(gltf) => {
model = gltf.scene;
// Auto-center and scale
const box = new THREE.Box3().setFromObject(model);
const center = box.getCenter(new THREE.Vector3());
const size = box.getSize(new THREE.Vector3());
const maxDim = Math.max(size.x, size.y, size.z);
model.scale.multiplyScalar(3 / maxDim);
model.position.sub(center.multiplyScalar(3 / maxDim));
scene.add(model);
},
undefined,
(error) => console.error('GLTF load error:', error)
);
// Animation
let animationId: number;
const animate = () => {
animationId = requestAnimationFrame(animate);
if (model) model.rotation.y += 0.005;
renderer.render(scene, camera);
};
animate();
// Resize
const handleResize = () => {
const w = canvas.clientWidth;
const h = canvas.clientHeight;
renderer.setSize(w, h);
camera.aspect = w / h;
camera.updateProjectionMatrix();
};
window.addEventListener('resize', handleResize);
return () => {
cancelAnimationFrame(animationId);
window.removeEventListener('resize', handleResize);
dracoLoader.dispose();
renderer.dispose();
scene.traverse((obj) => {
if (obj instanceof THREE.Mesh) {
obj.geometry.dispose();
if (Array.isArray(obj.material)) obj.material.forEach((m) => m.dispose());
else obj.material.dispose();
}
});
};
}, [modelPath]);
return <canvas ref={canvasRef} className={className} style={{ width: '100%', height: '100%', display: 'block' }} />;
}
Pattern 3: Camera Scroll Animation
Link camera position/rotation to scroll progress.
'use client';
import { useEffect, useRef } from 'react';
import * as THREE from 'three';
import { gsap } from 'gsap';
import { ScrollTrigger } from 'gsap/ScrollTrigger';
gsap.registerPlugin(ScrollTrigger);
export function ScrollCamera({ className }: { className?: string }) {
const canvasRef = useRef<HTMLCanvasElement>(null);
const containerRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const canvas = canvasRef.current;
const container = containerRef.current;
if (!canvas || !container) return;
const renderer = new THREE.WebGLRenderer({ canvas, antialias: true, alpha: true });
renderer.setSize(canvas.clientWidth, canvas.clientHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, canvas.clientWidth / canvas.clientHeight, 0.1, 1000);
camera.position.set(0, 0, 10);
// Add scene content (meshes, lights, etc.)
scene.add(new THREE.AmbientLight(0xffffff, 0.5));
// Scroll-driven camera animation via GSAP
const ctx = gsap.context(() => {
gsap.to(camera.position, {
z: 2,
y: 3,
ease: 'none',
scrollTrigger: {
trigger: container,
start: 'top top',
end: 'bottom bottom',
scrub: 1,
},
});
gsap.to(camera.rotation, {
x: -0.3,
ease: 'none',
scrollTrigger: {
trigger: container,
start: 'top top',
end: 'bottom bottom',
scrub: 1,
},
});
}, container);
// Render loop
let animationId: number;
const animate = () => {
animationId = requestAnimationFrame(animate);
renderer.render(scene, camera);
};
animate();
const handleResize = () => {
const w = canvas.clientWidth;
const h = canvas.clientHeight;
renderer.setSize(w, h);
camera.aspect = w / h;
camera.updateProjectionMatrix();
};
window.addEventListener('resize', handleResize);
return () => {
ctx.revert();
cancelAnimationFrame(animationId);
window.removeEventListener('resize', handleResize);
renderer.dispose();
};
}, []);
return (
<div ref={containerRef} style={{ height: '300vh', position: 'relative' }}>
<canvas
ref={canvasRef}
className={className}
style={{ position: 'sticky', top: 0, width: '100%', height: '100vh', display: 'block' }}
/>
</div>
);
}
Pattern 4: Particle System
useEffect(() => {
// ... renderer, scene, camera setup (see Pattern 1) ...
// Particle system
const particleCount = 5000;
const positions = new Float32Array(particleCount * 3);
for (let i = 0; i < particleCount * 3; i++) {
positions[i] = (Math.random() - 0.5) * 20;
}
const geometry = new THREE.BufferGeometry();
geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
const material = new THREE.PointsMaterial({
size: 0.05,
color: 0xffffff,
transparent: true,
opacity: 0.8,
sizeAttenuation: true,
});
const particles = new THREE.Points(geometry, material);
scene.add(particles);
const animate = () => {
animationId = requestAnimationFrame(animate);
particles.rotation.y += 0.001;
particles.rotation.x += 0.0005;
renderer.render(scene, camera);
};
animate();
return () => {
// ... cleanup ...
geometry.dispose();
material.dispose();
};
}, []);
Pattern 5: Environment Lighting (HDR)
import { RGBELoader } from 'three/addons/loaders/RGBELoader.js';
useEffect(() => {
// ... renderer, scene, camera setup ...
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1.0;
const rgbeLoader = new RGBELoader();
rgbeLoader.load('/environment.hdr', (texture) => {
texture.mapping = THREE.EquirectangularReflectionMapping;
scene.environment = texture;
scene.background = texture; // Optional: use as background
});
return () => {
// ... cleanup ...
};
}, []);
Pattern 6: Post-Processing
import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js';
import { RenderPass } from 'three/addons/postprocessing/RenderPass.js';
import { UnrealBloomPass } from 'three/addons/postprocessing/UnrealBloomPass.js';
useEffect(() => {
// ... renderer, scene, camera setup ...
const composer = new EffectComposer(renderer);
composer.addPass(new RenderPass(scene, camera));
composer.addPass(new UnrealBloomPass(
new THREE.Vector2(window.innerWidth, window.innerHeight),
0.5, // strength
0.4, // radius
0.85 // threshold
));
const animate = () => {
animationId = requestAnimationFrame(animate);
composer.render(); // Use composer.render() instead of renderer.render()
};
animate();
return () => {
composer.dispose();
// ... cleanup ...
};
}, []);
Pattern 7: OrbitControls
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
useEffect(() => {
// ... renderer, scene, camera setup ...
const controls = new OrbitControls(camera, canvas);
controls.enableDamping = true;
controls.dampingFactor = 0.05;
controls.maxPolarAngle = Math.PI / 2;
const animate = () => {
animationId = requestAnimationFrame(animate);
controls.update(); // Required for damping
renderer.render(scene, camera);
};
animate();
return () => {
controls.dispose();
// ... cleanup ...
};
}, []);
Cleanup Checklist (ALL Patterns)
Every Three.js component MUST clean up on unmount:
cancelAnimationFrame(animationId)-- stop render loopwindow.removeEventListener('resize', handleResize)-- remove listenersrenderer.dispose()-- release WebGL context- Traverse scene and dispose all geometries and materials
- Dispose loaders (
dracoLoader.dispose()) - Dispose post-processing (
composer.dispose()) - Dispose controls (
controls.dispose())
Failure to clean up causes WebGL context leaks and memory issues.
Constraints
- NO React Three Fiber -- vanilla Three.js only
- NO custom GLSL shaders in v1 -- use built-in materials and post-processing
- Pin version: three@0.169.0
- Import addons from:
'three/addons/...'(NOT'three/examples/jsm/...') - Canvas sizing: Use
canvas.clientWidth/clientHeight, notwindow.innerWidth/innerHeight - Pixel ratio: Cap at 2 with
Math.min(window.devicePixelRatio, 2)
Recommended Agent Skills
Expand your agent's capabilities with these related and highly-rated skills.
web-interface-guidelines
Web UI quality rules: interactions, forms, loading, animations, layout, content, performance, accessibility, design. Apply to all web UI work. Adapted from Vercel Design Guidelines.
react-performance
React/Next.js performance patterns with wrong/right code examples. Apply when building or reviewing React components. Adapted from Vercel React Best Practices by @shuding.
stripe-integration
Payment integration patterns for Stripe. Covers checkout sessions, subscriptions, webhooks, idempotency, and the sharp edges that cause real-money bugs. Backend-agnostic with examples for Next.js App Router and Django REST Framework.
testing-strategy
Testing strategies and best practices for unit, integration, and end-to-end tests. Use when setting up test infrastructure, writing test cases, or deciding what to test. Covers test pyramid, mocking strategies, and coverage guidelines.
frontend-aesthetics
Global frontend aesthetics skill that helps Claude avoid generic "AI slop" UI and make bold, intentional visual decisions while still honoring each project's design-dna, tokens, and architecture.
pg-style-editor
Edit writing to adopt Paul Graham's exceptionally clear style for research and long-form content - concrete language, varied sentence rhythm, accessible formality, specific evidence. Use when user wants to rewrite content in PG's style or asks to "make this clearer" or "simplify research writing."
Didn't find tool you were looking for?