Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Create 3D scenes, interactive experiences, and visual effects using Three.js. Use when user requests 3D graphics, WebGL experiences, 3D visualizations, animations, or interactive 3D elements.
| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-04 | ✗→✓ | ▲ Improved | 347% | 0% |
| case-10 | ✗→✓ | ▲ Improved | 111% | 0% |
| case-23 | ✓→✓ | = Same ✓ | 375% | 0% |
| case-03 | ✓→✓ | = Same ✓ | 214% | 0% |
| case-05 | ✓→✓ | = Same ✓ | 180% | 0% |
Systematically create high-quality 3D scenes and interactive experiences using Three.js best practices.
Use ES module import maps for modern Three.js (r183+):
html<script type="importmap"> { "imports": { "three": "https://cdn.jsdelivr.net/npm/three@0.183.0/build/three.module.js", "three/addons/": "https://cdn.jsdelivr.net/npm/three@0.183.0/examples/jsm/" } } </script> <script type="module"> import * as THREE from "three"; import { OrbitControls } from "three/addons/controls/OrbitControls.js"; </script>
For production with npm/vite/webpack:
javascriptimport * as THREE from "three"; import { OrbitControls } from "three/addons/controls/OrbitControls.js";
Every Three.js artifact needs these core components:
javascript// Scene - contains all 3D objects const scene = new THREE.Scene(); // Camera - defines viewing perspective const camera = new THREE.PerspectiveCamera( 75, // Field of view window.innerWidth / window.innerHeight, // Aspect ratio 0.1, // Near clipping plane 1000, // Far clipping plane ); camera.position.z = 5; // Renderer - draws the scene const renderer = new THREE.WebGLRenderer({ antialias: true }); renderer.setSize(window.innerWidth, window.innerHeight); document.body.appendChild(renderer.domElement);
Use renderer.setAnimationLoop() (preferred) or requestAnimationFrame:
javascript// Preferred: setAnimationLoop (handles WebXR compatibility) renderer.setAnimationLoop(() => { mesh.rotation.x += 0.01; mesh.rotation.y += 0.01; renderer.render(scene, camera); }); // Alternative: manual requestAnimationFrame function animate() { requestAnimationFrame(animate); mesh.rotation.x += 0.01; mesh.rotation.y += 0.01; renderer.render(scene, camera); } animate();
Start by identifying:
Choose appropriate geometry types:
Basic Shapes:
BoxGeometry - cubes, rectangular prismsSphereGeometry - spheres, planetsCylinderGeometry - cylinders, tubesPlaneGeometry - flat surfaces, ground planesTorusGeometry - donuts, ringsCapsuleGeometry is available (stable since r142):
javascriptnew THREE.CapsuleGeometry(0.5, 1, 4, 8); // radius, length, capSegments, radialSegments
Choose materials based on visual needs:
Common Materials:
MeshBasicMaterial - unlit, flat colors (no lighting needed)MeshStandardMaterial - physically-based, realistic (needs lighting)MeshPhongMaterial - shiny surfaces with specular highlightsMeshLambertMaterial - matte surfaces, diffuse reflectionjavascriptconst material = new THREE.MeshStandardMaterial({ color: 0x00ff00, metalness: 0.5, roughness: 0.5, });
If using lit materials (Standard, Phong, Lambert), add lights:
javascript// Ambient light - general illumination const ambientLight = new THREE.AmbientLight(0xffffff, 0.5); scene.add(ambientLight); // Directional light - like sunlight const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8); directionalLight.position.set(5, 5, 5); scene.add(directionalLight);
Skip lighting if using MeshBasicMaterial - it's unlit by design.
Always add window resize handling:
javascriptwindow.addEventListener("resize", () => { camera.aspect = window.innerWidth / window.innerHeight; camera.updateProjectionMatrix(); renderer.setSize(window.innerWidth, window.innerHeight); });
javascriptfunction animate() { requestAnimationFrame(animate); mesh.rotation.x += 0.01; mesh.rotation.y += 0.01; renderer.render(scene, camera); }
With import maps or build tools, OrbitControls works directly:
javascriptimport { OrbitControls } from "three/addons/controls/OrbitControls.js"; const controls = new OrbitControls(camera, renderer.domElement); controls.enableDamping = true; // Update in animation loop renderer.setAnimationLoop(() => { controls.update(); renderer.render(scene, camera); });
For lightweight custom controls without importing OrbitControls:
javascriptlet isDragging = false; let previousMousePosition = { x: 0, y: 0 }; renderer.domElement.addEventListener("mousedown", () => { isDragging = true; }); renderer.domElement.addEventListener("mouseup", () => { isDragging = false; }); renderer.domElement.addEventListener("mousemove", (event) => { if (isDragging) { const deltaX = event.clientX - previousMousePosition.x; const deltaY = event.clientY - previousMousePosition.y; // Rotate camera around scene const rotationSpeed = 0.005; camera.position.x += deltaX * rotationSpeed; camera.position.y -= deltaY * rotationSpeed; camera.lookAt(scene.position); } previousMousePosition = { x: event.clientX, y: event.clientY }; }); // Zoom with mouse wheel renderer.domElement.addEventListener("wheel", (event) => { event.preventDefault(); camera.position.z += event.deltaY * 0.01; camera.position.z = Math.max(2, Math.min(20, camera.position.z)); // Clamp });
Detect mouse clicks and hovers on 3D objects:
javascriptconst raycaster = new THREE.Raycaster(); const mouse = new THREE.Vector2(); const clickableObjects = []; // Array of meshes that can be clicked // Update mouse position window.addEventListener("mousemove", (event) => { mouse.x = (event.clientX / window.innerWidth) * 2 - 1; mouse.y = -(event.clientY / window.innerHeight) * 2 + 1; }); // Detect clicks window.addEventListener("click", () => { raycaster.setFromCamera(mouse, camera); const intersects = raycaster.intersectObjects(clickableObjects); if (intersects.length > 0) { const clickedObject = intersects[0].object; // Handle click - change color, scale, etc. clickedObject.material.color.set(0xff0000); } }); // Hover effect in animation loop function animate() { requestAnimationFrame(animate); raycaster.setFromCamera(mouse, camera); const intersects = raycaster.intersectObjects(clickableObjects); // Reset all objects clickableObjects.forEach((obj) => { obj.scale.set(1, 1, 1); }); // Highlight hovered object if (intersects.length > 0) { intersects[0].object.scale.set(1.2, 1.2, 1.2); document.body.style.cursor = "pointer"; } else { document.body.style.cursor = "default"; } renderer.render(scene, camera); }
javascriptconst particlesGeometry = new THREE.BufferGeometry(); const particlesCount = 1000; const posArray = new Float32Array(particlesCount * 3); for (let i = 0; i < particlesCount * 3; i++) { posArray[i] = (Math.random() - 0.5) * 10; } particlesGeometry.setAttribute( "position", new THREE.BufferAttribute(posArray, 3), ); const particlesMaterial = new THREE.PointsMaterial({ size: 0.02, color: 0xffffff, }); const particlesMesh = new THREE.Points(particlesGeometry, particlesMaterial); scene.add(particlesMesh);
javascriptlet mouseX = 0; let mouseY = 0; document.addEventListener("mousemove", (event) => { mouseX = (event.clientX / window.innerWidth) * 2 - 1; mouseY = -(event.clientY / window.innerHeight) * 2 + 1; }); function animate() { requestAnimationFrame(animate); camera.position.x = mouseX * 2; camera.position.y = mouseY * 2; camera.lookAt(scene.position); renderer.render(scene, camera); }
javascriptconst textureLoader = new THREE.TextureLoader(); const texture = textureLoader.load("texture-url.jpg"); const material = new THREE.MeshStandardMaterial({ map: texture, });
BufferGeometry for custom shapes (more efficient)javascript geometry.dispose(); material.dispose(); texture.dispose();
antialias: true on renderer for smooth edgesoutputEncoding instead of outputColorSpace (renamed in r152)scene.add()renderer.render() in animation loopTHREE.Clock without considering THREE.Timer (recommended in r183)User: "Create an interactive 3D sphere that responds to mouse movement"
SphereGeometry(1, 32, 32) for smooth sphereMeshStandardMaterial for realistic lookBlack screen / Nothing renders:
Poor performance:
Objects not visible:
Shadows:
javascript// Enable shadows on renderer renderer.shadowMap.enabled = true; renderer.shadowMap.type = THREE.PCFSoftShadowMap; // Soft shadows // Light that casts shadows const directionalLight = new THREE.DirectionalLight(0xffffff, 1); directionalLight.position.set(5, 10, 5); directionalLight.castShadow = true; // Configure shadow quality directionalLight.shadow.mapSize.width = 2048; directionalLight.shadow.mapSize.height = 2048; directionalLight.shadow.camera.near = 0.5; directionalLight.shadow.camera.far = 50; scene.add(directionalLight); // Objects cast and receive shadows mesh.castShadow = true; mesh.receiveShadow = true; // Ground plane receives shadows const groundGeometry = new THREE.PlaneGeometry(20, 20); const groundMaterial = new THREE.MeshStandardMaterial({ color: 0x808080 }); const ground = new THREE.Mesh(groundGeometry, groundMaterial); ground.rotation.x = -Math.PI / 2; ground.receiveShadow = true; scene.add(ground);
Environment Maps & Reflections:
javascript// Create environment map from cubemap const loader = new THREE.CubeTextureLoader(); const envMap = loader.load([ "px.jpg", "nx.jpg", // positive x, negative x "py.jpg", "ny.jpg", // positive y, negative y "pz.jpg", "nz.jpg", // positive z, negative z ]); scene.environment = envMap; // Affects all PBR materials scene.background = envMap; // Optional: use as skybox // Or apply to specific materials const material = new THREE.MeshStandardMaterial({ metalness: 1.0, roughness: 0.1, envMap: envMap, });
Tone Mapping & Output Encoding:
javascript// Improve color accuracy and HDR rendering renderer.toneMapping = THREE.ACESFilmicToneMapping; renderer.toneMappingExposure = 1.0; renderer.outputColorSpace = THREE.SRGBColorSpace; // Was outputEncoding in older versions // Makes colors more vibrant and realistic
Fog for Depth:
javascript// Linear fog scene.fog = new THREE.Fog(0xcccccc, 10, 50); // color, near, far // Or exponential fog (more realistic) scene.fog = new THREE.FogExp2(0xcccccc, 0.02); // color, density
javascriptconst geometry = new THREE.BufferGeometry(); const vertices = new Float32Array([-1, -1, 0, 1, -1, 0, 1, 1, 0]); geometry.setAttribute("position", new THREE.BufferAttribute(vertices, 3));
Post-processing effects are available via import maps or build tools. See threejs-postprocessing skill for EffectComposer, bloom, DOF, and more.
javascriptconst group = new THREE.Group(); group.add(mesh1); group.add(mesh2); group.rotation.y = Math.PI / 4; scene.add(group);
Three.js artifacts require systematic setup:
setAnimationLoop)renderer.outputColorSpace = THREE.SRGBColorSpaceFollow these patterns for reliable, performant 3D experiences.
javascript// With npm/vite/webpack: import * as THREE from "three"; import { OrbitControls } from "three/addons/controls/OrbitControls.js"; import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js"; import { EffectComposer } from "three/addons/postprocessing/EffectComposer.js";
Three.js r183 includes a WebGPU renderer as an alternative to WebGL:
javascriptimport { WebGPURenderer } from "three/addons/renderers/webgpu/WebGPURenderer.js"; const renderer = new WebGPURenderer({ antialias: true }); await renderer.init(); renderer.setSize(window.innerWidth, window.innerHeight);
WebGPU uses TSL (Three.js Shading Language) instead of GLSL for custom shaders. See threejs-shaders for details.
THREE.Timer is recommended over THREE.Clock as of r183:
javascriptconst timer = new THREE.Timer(); renderer.setAnimationLoop(() => { timer.update(); const delta = timer.getDelta(); const elapsed = timer.getElapsed(); mesh.rotation.y += delta; renderer.render(scene, camera); });
Benefits over Clock:
setAnimationLoopjavascript// Smooth timeline-based animations import gsap from "gsap"; // Instead of manual animation loops: gsap.to(mesh.position, { x: 5, duration: 2, ease: "power2.inOut", }); // Complex sequences: const timeline = gsap.timeline(); timeline .to(mesh.rotation, { y: Math.PI * 2, duration: 2 }) .to(mesh.scale, { x: 2, y: 2, z: 2, duration: 1 }, "-=1");
Why GSAP:
javascript// Sync 3D animations with page scroll let scrollY = window.scrollY; window.addEventListener("scroll", () => { scrollY = window.scrollY; }); function animate() { requestAnimationFrame(animate); // Rotate based on scroll position mesh.rotation.y = scrollY * 0.001; // Move camera through scene camera.position.y = -(scrollY / window.innerHeight) * 10; renderer.render(scene, camera); }
Advanced scroll libraries:
javascript// Level of Detail (LOD) const lod = new THREE.LOD(); lod.addLevel(highDetailMesh, 0); // Close up lod.addLevel(mediumDetailMesh, 10); // Medium distance lod.addLevel(lowDetailMesh, 50); // Far away scene.add(lod); // Instanced meshes for many identical objects const geometry = new THREE.BoxGeometry(); const material = new THREE.MeshStandardMaterial(); const instancedMesh = new THREE.InstancedMesh(geometry, material, 1000); // Set transforms for each instance const matrix = new THREE.Matrix4(); for (let i = 0; i < 1000; i++) { matrix.setPosition( Math.random() * 100, Math.random() * 100, Math.random() * 100, ); instancedMesh.setMatrixAt(i, matrix); }
javascript// In production, load 3D models: import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader"; const loader = new GLTFLoader(); loader.load("model.gltf", (gltf) => { scene.add(gltf.scene); // Traverse and setup materials gltf.scene.traverse((child) => { if (child.isMesh) { child.castShadow = true; child.receiveShadow = true; } }); });
Import Map Approach:
Production Build Approach:
Three.js r183 + Vite
├── GSAP (animations)
├── React Three Fiber (optional - React integration)
├── Drei (helper components)
├── Leva (debug GUI)
└── Post-processing effectsOther measured skills in the registry, with their headline benchmark lift.