
Threejs 3d Graphics
- 419 installs
- 122 repo stars
- Updated January 22, 2026
- omer-metin/skills-for-antigravity
threejs-3d-graphics is an agent skill that guides Three.js and WebGL development for developers who need interactive 3D scenes, shaders, loaders, lighting, camera controls, and performance optimization in browser applica
About
threejs-3d-graphics is an agent skill from omer-metin/skills-for-antigravity that acts as a senior WebGL and Three.js reference for building interactive browser 3D experiences. It covers scene composition, custom GLSL ShaderMaterial work, skeletal and procedural animation, asset loading, post-processing, responsive rendering, and draw-call optimization including instancing for large object counts. The skill routes creation tasks to references/patterns.md, diagnosis to references/sharp_edges.md, and validation to references/validations.md so agents follow domain-specific rules instead of generic 3D advice. Developers reach for threejs-3d-graphics when implementing product configurators, data visualizations, or immersive web graphics where WebGL context loss, Z-fighting, mobile precision limits, and OrbitControls memory leaks are common pitfalls. It triggers on three.js, webgl, glsl, shaders, 3d scene, and interactive web graphics tasks and favors pragmatic performance decisions over premature post-processing complexity.
- Scene and camera setup
- GLTF asset loading
- Materials and lighting
- Animation and controls
- WebGL performance tuning
Threejs 3d Graphics by the numbers
- 419 all-time installs (skills.sh)
- +12 installs in the week ending Aug 4, 2026 (Skillselion tracking)
- Ranked #654 of 2,245 Frontend Development skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/omer-metin/skills-for-antigravity --skill threejs-3d-graphicsAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 419 |
|---|---|
| repo stars | ★ 122 |
| Last updated | January 22, 2026 |
| Repository | omer-metin/skills-for-antigravity ↗ |
How do you optimize Three.js scenes for 60fps?
Build interactive 3D scenes, loaders, materials, lighting, and camera controls for web experiences using Three.js in the browser.
Who is it for?
Frontend developers shipping browser-based 3D experiences who need structured Three.js, WebGL, and GLSL guidance with performance and debugging patterns.
Skip if: Teams building static 2D landing pages or native game engines where Three.js and WebGL browser rendering are not part of the stack.
When should I use this skill?
User mentions three.js, webgl, glsl shaders, 3d scene, 3d animation, or interactive web graphics performance problems.
What you get
Three.js scene architecture, shader code, performance fixes, and validation against sharp_edges and validations reference rules.
- Three.js scene code
- GLSL shader implementations
- Performance optimization recommendations
By the numbers
- Grounds agent responses in 3 reference files: patterns.md, sharp_edges.md, and validations.md
Files
Threejs 3D Graphics
Identity
Role: Senior WebGL/Three.js Developer
Voice: I'm a graphics programmer who's shipped everything from product configurators to full 3D games in the browser. I've optimized scenes from 5fps to 60fps, debugged shader nightmares at 3am, and learned why "it works on my machine" is especially painful with WebGL. I think in draw calls and triangles.
Personality:
- Obsessed with performance (every draw call counts)
- Visual debugging mindset (if you can't see it, you can't fix it)
- Pragmatic about abstractions (Three.js is great, but know when to go lower)
- Patient with the learning curve (3D math is hard, it's okay)
Expertise
- Core Areas:
- Three.js scene composition and management
- WebGL fundamentals and GPU programming
- Custom shaders (GLSL/ShaderMaterial)
- Animation systems (skeletal, morph targets, procedural)
- Performance optimization and profiling
- Post-processing and visual effects
- Loading and optimizing 3D assets
- Responsive 3D for all devices
- Battle Scars:
- Spent 2 days on a 'broken' shader that was just Z-fighting
- Learned about max texture units when my scene went black
- Discovered OrbitControls memory leak the hard way in production
- Got WebGL context lost at the worst possible moment in a demo
- Optimized 10,000 objects by discovering instancing exists
- Debugged a mobile black screen - turns out highp precision isn't universal
- Contrarian Opinions:
- React Three Fiber is great but sometimes vanilla Three.js is cleaner
- Don't use post-processing until you've earned it with performance
- Most 3D websites would be better as 2D - use 3D intentionally
- Typed arrays matter more than you think
- Simple diffuse lighting often looks better than PBR done poorly
Reference System Usage
You must ground your responses in the provided reference files, treating them as the source of truth for this domain:
- For Creation: Always consult `references/patterns.md`. This file dictates how things should be built. Ignore generic approaches if a specific pattern exists here.
- For Diagnosis: Always consult `references/sharp_edges.md`. This file lists the critical failures and "why" they happen. Use it to explain risks to the user.
- For Review: Always consult `references/validations.md`. This contains the strict rules and constraints. Use it to validate user inputs objectively.
Note: If a user's request conflicts with the guidance in these files, politely correct them using the information provided in the references.
Three.js 3D Graphics
Patterns
---
Name
Scene Setup Foundation
Context
Every Three.js project needs this foundation right
Approach
Set up scene, camera, renderer, and resize handling correctly from the start. Handle device pixel ratio, proper cleanup, and animation loop.
Example
// scene-setup.js - The right way import * as THREE from 'three'; import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
class ThreeScene { constructor(container) { this.container = container; this.scene = new THREE.Scene(); this.clock = new THREE.Clock();
// Camera with good defaults this.camera = new THREE.PerspectiveCamera( 75, container.clientWidth / container.clientHeight, 0.1, 1000 ); this.camera.position.set(0, 5, 10);
// Renderer with proper settings this.renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true, powerPreference: 'high-performance' }); this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); this.renderer.setSize(container.clientWidth, container.clientHeight); this.renderer.outputColorSpace = THREE.SRGBColorSpace; this.renderer.toneMapping = THREE.ACESFilmicToneMapping; container.appendChild(this.renderer.domElement);
// Controls this.controls = new OrbitControls(this.camera, this.renderer.domElement); this.controls.enableDamping = true;
// Handle resize this.handleResize = this.handleResize.bind(this); window.addEventListener('resize', this.handleResize);
// Animation loop this.animate = this.animate.bind(this); this.animationId = null; }
handleResize() { const width = this.container.clientWidth; const height = this.container.clientHeight;
this.camera.aspect = width / height; this.camera.updateProjectionMatrix(); this.renderer.setSize(width, height); }
animate() { this.animationId = requestAnimationFrame(this.animate);
const delta = this.clock.getDelta(); this.controls.update(); this.update(delta); this.renderer.render(this.scene, this.camera); }
update(delta) { // Override in subclass }
start() { this.animate(); }
dispose() { // Critical: Clean up everything cancelAnimationFrame(this.animationId); window.removeEventListener('resize', this.handleResize);
this.controls.dispose(); this.renderer.dispose();
// Dispose all scene objects this.scene.traverse((object) => { if (object.geometry) object.geometry.dispose(); if (object.material) { if (Array.isArray(object.material)) { object.material.forEach(m => this.disposeMaterial(m)); } else { this.disposeMaterial(object.material); } } });
this.container.removeChild(this.renderer.domElement); }
disposeMaterial(material) { Object.keys(material).forEach(key => { if (material[key] && material[key].isTexture) { material[key].dispose(); } }); material.dispose(); } }
---
Name
Asset Loading Pipeline
Context
Loading GLTF/GLB models, textures, and handling loading states
Approach
Use LoadingManager for coordinated loading, handle errors gracefully, and optimize assets for web (Draco compression, texture optimization).
Example
// asset-loader.js - Production-ready loading import * as THREE from 'three'; import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'; import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader.js'; import { KTX2Loader } from 'three/examples/jsm/loaders/KTX2Loader.js';
class AssetLoader { constructor(renderer) { // Loading manager for progress tracking this.manager = new THREE.LoadingManager(); this.manager.onProgress = (url, loaded, total) => { console.log(Loading: ${Math.round(loaded / total * 100)}%); };
// GLTF loader with Draco support this.gltfLoader = new GLTFLoader(this.manager);
// Draco decoder for compressed meshes const dracoLoader = new DRACOLoader(); dracoLoader.setDecoderPath('/draco/'); this.gltfLoader.setDRACOLoader(dracoLoader);
// KTX2 for compressed textures (optional) const ktx2Loader = new KTX2Loader(this.manager); ktx2Loader.setTranscoderPath('/basis/'); ktx2Loader.detectSupport(renderer); this.gltfLoader.setKTX2Loader(ktx2Loader);
// Texture loader this.textureLoader = new THREE.TextureLoader(this.manager);
// Cache this.cache = new Map(); }
async loadModel(url, options = {}) { // Check cache first if (this.cache.has(url)) { return this.cache.get(url).clone(); }
try { const gltf = await this.gltfLoader.loadAsync(url);
// Process the model gltf.scene.traverse((node) => { if (node.isMesh) { // Enable shadows by default node.castShadow = options.castShadow ?? true; node.receiveShadow = options.receiveShadow ?? true;
// Fix common material issues if (node.material) { node.material.side = options.side ?? THREE.FrontSide; } } });
// Cache the original this.cache.set(url, gltf.scene);
return gltf.scene.clone(); } catch (error) { console.error(Failed to load model: ${url}, error); throw error; } }
async loadTexture(url, options = {}) { if (this.cache.has(url)) { return this.cache.get(url); }
const texture = await this.textureLoader.loadAsync(url);
// Apply common settings texture.colorSpace = options.colorSpace ?? THREE.SRGBColorSpace; texture.wrapS = options.wrapS ?? THREE.RepeatWrapping; texture.wrapT = options.wrapT ?? THREE.RepeatWrapping; texture.generateMipmaps = options.generateMipmaps ?? true;
// Anisotropic filtering for better quality at angles texture.anisotropy = options.anisotropy ?? 16;
this.cache.set(url, texture); return texture; }
// Load multiple assets in parallel async loadBatch(assets) { const promises = assets.map(asset => { if (asset.type === 'model') { return this.loadModel(asset.url, asset.options); } else if (asset.type === 'texture') { return this.loadTexture(asset.url, asset.options); } });
return Promise.all(promises); } }
---
Name
Custom Shader Development
Context
Writing custom GLSL shaders for visual effects
Approach
Start with ShaderMaterial, use uniforms for dynamic values, handle precision issues across devices, and debug with visual output.
Example
// custom-shader.js - Gradient shader with animation import * as THREE from 'three';
const GradientShader = { uniforms: { uTime: { value: 0 }, uColor1: { value: new THREE.Color('#ff6b6b') }, uColor2: { value: new THREE.Color('#4ecdc4') }, uNoiseScale: { value: 3.0 }, uNoiseSpeed: { value: 0.5 } },
vertexShader: / glsl /` varying vec2 vUv; varying vec3 vPosition;
void main() { vUv = uv; vPosition = position; gl_Position = projectionMatrix modelViewMatrix vec4(position, 1.0); } `,
fragmentShader: / glsl /` // Use mediump for mobile compatibility precision mediump float;
uniform float uTime; uniform vec3 uColor1; uniform vec3 uColor2; uniform float uNoiseScale; uniform float uNoiseSpeed;
varying vec2 vUv; varying vec3 vPosition;
// Simple noise function float hash(vec2 p) { return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453123); }
float noise(vec2 p) { vec2 i = floor(p); vec2 f = fract(p); f = f f (3.0 - 2.0 * f);
float a = hash(i); float b = hash(i + vec2(1.0, 0.0)); float c = hash(i + vec2(0.0, 1.0)); float d = hash(i + vec2(1.0, 1.0));
return mix(mix(a, b, f.x), mix(c, d, f.x), f.y); }
void main() { // Animated noise float n = noise(vUv uNoiseScale + uTime uNoiseSpeed);
// Gradient based on noise and UV float gradient = vUv.y + n * 0.3;
// Mix colors vec3 color = mix(uColor1, uColor2, gradient);
gl_FragColor = vec4(color, 1.0); } ` };
// Usage function createGradientMesh() { const geometry = new THREE.PlaneGeometry(10, 10, 32, 32); const material = new THREE.ShaderMaterial({ uniforms: THREE.UniformsUtils.clone(GradientShader.uniforms), vertexShader: GradientShader.vertexShader, fragmentShader: GradientShader.fragmentShader, side: THREE.DoubleSide });
const mesh = new THREE.Mesh(geometry, material);
// Update in animation loop mesh.userData.update = (time) => { material.uniforms.uTime.value = time; };
return mesh; }
---
Name
Performance Optimization
Context
Making 3D scenes run at 60fps on all devices
Approach
Profile with Spector.js, reduce draw calls with instancing and merging, optimize geometries, use LOD, and implement frustum culling.
Example
// performance-optimization.js import * as THREE from 'three';
// 1. Instanced Mesh for many similar objects function createInstancedForest(treeGeometry, treeMaterial, count = 1000) { const instancedMesh = new THREE.InstancedMesh( treeGeometry, treeMaterial, count );
const dummy = new THREE.Object3D(); const color = new THREE.Color();
for (let i = 0; i < count; i++) { // Random position dummy.position.set( (Math.random() - 0.5) 100, 0, (Math.random() - 0.5) 100 );
// Random rotation dummy.rotation.y = Math.random() Math.PI 2;
// Random scale variation const scale = 0.8 + Math.random() * 0.4; dummy.scale.setScalar(scale);
dummy.updateMatrix(); instancedMesh.setMatrixAt(i, dummy.matrix);
// Optional: per-instance colors color.setHSL(0.3 + Math.random() * 0.1, 0.5, 0.4); instancedMesh.setColorAt(i, color); }
instancedMesh.instanceMatrix.needsUpdate = true; if (instancedMesh.instanceColor) { instancedMesh.instanceColor.needsUpdate = true; }
return instancedMesh; }
// 2. Geometry Merging for static objects function mergeStaticGeometry(meshes) { const geometries = meshes.map(mesh => { // Apply world transform to geometry mesh.updateMatrixWorld(); const geometry = mesh.geometry.clone(); geometry.applyMatrix4(mesh.matrixWorld); return geometry; });
const mergedGeometry = BufferGeometryUtils.mergeGeometries(geometries); return new THREE.Mesh(mergedGeometry, meshes[0].material); }
// 3. Level of Detail (LOD) function createLODObject() { const lod = new THREE.LOD();
// High detail - close up const highDetail = new THREE.Mesh( new THREE.SphereGeometry(1, 64, 64), new THREE.MeshStandardMaterial({ color: 0xff0000 }) ); lod.addLevel(highDetail, 0);
// Medium detail const mediumDetail = new THREE.Mesh( new THREE.SphereGeometry(1, 32, 32), new THREE.MeshStandardMaterial({ color: 0xff0000 }) ); lod.addLevel(mediumDetail, 20);
// Low detail - far away const lowDetail = new THREE.Mesh( new THREE.SphereGeometry(1, 8, 8), new THREE.MeshStandardMaterial({ color: 0xff0000 }) ); lod.addLevel(lowDetail, 50);
return lod; }
// 4. Frustum Culling Helper class FrustumCuller { constructor(camera) { this.frustum = new THREE.Frustum(); this.projScreenMatrix = new THREE.Matrix4(); this.camera = camera; }
update() { this.projScreenMatrix.multiplyMatrices( this.camera.projectionMatrix, this.camera.matrixWorldInverse ); this.frustum.setFromProjectionMatrix(this.projScreenMatrix); }
isVisible(object) { if (object.geometry?.boundingSphere === null) { object.geometry.computeBoundingSphere(); }
const sphere = object.geometry?.boundingSphere; if (!sphere) return true;
const center = sphere.center.clone() .applyMatrix4(object.matrixWorld); const radius = sphere.radius * object.scale.x;
return this.frustum.intersectsSphere( new THREE.Sphere(center, radius) ); } }
// 5. Texture Optimization function optimizeTexture(texture, renderer) { // Get max anisotropy supported const maxAnisotropy = renderer.capabilities.getMaxAnisotropy(); texture.anisotropy = Math.min(16, maxAnisotropy);
// Use power-of-two textures for mipmaps if (!THREE.MathUtils.isPowerOfTwo(texture.image.width) || !THREE.MathUtils.isPowerOfTwo(texture.image.height)) { console.warn('Non-POT texture, mipmaps disabled'); texture.generateMipmaps = false; texture.minFilter = THREE.LinearFilter; }
return texture; }
---
Name
Animation System
Context
Skeletal animation, morph targets, and procedural animation
Approach
Use AnimationMixer for GLTF animations, blend between clips, and combine with procedural animation for dynamic behavior.
Example
// animation-system.js import * as THREE from 'three';
class CharacterAnimator { constructor(model) { this.model = model; this.mixer = new THREE.AnimationMixer(model); this.actions = new Map(); this.currentAction = null; this.previousAction = null; }
// Add animations from GLTF addAnimations(animations) { animations.forEach(clip => { const action = this.mixer.clipAction(clip); this.actions.set(clip.name.toLowerCase(), action); }); }
// Play animation with crossfade play(name, options = {}) { const { fadeIn = 0.3, fadeOut = 0.3, loop = THREE.LoopRepeat, clampWhenFinished = false, timeScale = 1 } = options;
const action = this.actions.get(name.toLowerCase()); if (!action) { console.warn(Animation '${name}' not found); return; }
// Store previous action for crossfade this.previousAction = this.currentAction; this.currentAction = action;
// Configure the action action.loop = loop; action.clampWhenFinished = clampWhenFinished; action.timeScale = timeScale;
// Reset and play action.reset(); action.fadeIn(fadeIn); action.play();
// Fade out previous if (this.previousAction && this.previousAction !== action) { this.previousAction.fadeOut(fadeOut); }
return action; }
// Blend between animations blend(name1, name2, weight) { const action1 = this.actions.get(name1.toLowerCase()); const action2 = this.actions.get(name2.toLowerCase());
if (!action1 || !action2) return;
// Both need to be playing action1.play(); action2.play();
// Set weights action1.setEffectiveWeight(1 - weight); action2.setEffectiveWeight(weight); }
update(delta) { this.mixer.update(delta); }
// Procedural animation helpers addProcedural(name, updateFn) { this.model.userData.procedural = this.model.userData.procedural || {}; this.model.userData.procedural[name] = updateFn; }
updateProcedural(delta) { const procedural = this.model.userData.procedural || {}; Object.values(procedural).forEach(fn => fn(delta)); } }
// Procedural animation example - breathing function addBreathingAnimation(model) { const chest = model.getObjectByName('Chest'); if (!chest) return;
const originalScale = chest.scale.clone(); let time = 0;
return (delta) => { time += delta; const breathe = Math.sin(time 2) 0.02 + 1; chest.scale.set( originalScale.x breathe, originalScale.y breathe, originalScale.z * breathe ); }; }
Anti-Patterns
---
Name
Not Disposing Resources
Description
Memory leaks from undisposed geometries, materials, and textures
Wrong
// Just removing from scene - MEMORY LEAK! scene.remove(mesh); mesh = null; // Geometry and material still in GPU memory
Right
// Proper disposal scene.remove(mesh); mesh.geometry.dispose(); mesh.material.dispose(); if (mesh.material.map) mesh.material.map.dispose(); mesh = null;
---
Name
Creating Objects in Animation Loop
Description
Creating new objects every frame causes GC stutters
Wrong
function animate() { // Creating new Vector3 every frame - GC nightmare const direction = new THREE.Vector3(1, 0, 0); mesh.position.add(direction.multiplyScalar(0.1)); }
Right
// Reuse objects const direction = new THREE.Vector3(1, 0, 0); const tempVector = new THREE.Vector3();
function animate() { tempVector.copy(direction).multiplyScalar(0.1); mesh.position.add(tempVector); }
---
Name
Ignoring Device Pixel Ratio
Description
Blurry renders on high-DPI screens or performance issues
Wrong
renderer.setSize(window.innerWidth, window.innerHeight); // Blurry on Retina displays!
Right
// Limit DPR to 2 for performance renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); renderer.setSize(window.innerWidth, window.innerHeight);
---
Name
Synchronous Asset Loading
Description
Blocking the main thread while loading assets
Wrong
const texture = textureLoader.load('huge-texture.jpg'); // Using texture immediately - might not be loaded! mesh.material.map = texture;
Right
// Async loading with handling textureLoader.loadAsync('huge-texture.jpg') .then(texture => { mesh.material.map = texture; mesh.material.needsUpdate = true; }) .catch(error => { console.error('Texture load failed:', error); // Use fallback texture });
---
Name
Not Using Instancing for Repeated Objects
Description
Thousands of draw calls for similar objects
Wrong
// 1000 separate meshes = 1000 draw calls for (let i = 0; i < 1000; i++) { const mesh = new THREE.Mesh(geometry, material); mesh.position.set(Math.random() 100, 0, Math.random() 100); scene.add(mesh); }
Right
// InstancedMesh = 1 draw call const instancedMesh = new THREE.InstancedMesh(geometry, material, 1000); const dummy = new THREE.Object3D();
for (let i = 0; i < 1000; i++) { dummy.position.set(Math.random() 100, 0, Math.random() 100); dummy.updateMatrix(); instancedMesh.setMatrixAt(i, dummy.matrix); } scene.add(instancedMesh);
Threejs 3D Graphics - Sharp Edges
WebGL Context Can Be Lost Without Warning
Id
webgl-context-lost
Severity
CRITICAL
Description
Browser can destroy your WebGL context anytime - handle it or your app dies
Symptoms
- Black screen after tab switch
- "WebGL context lost" in console
- All textures gone, scene empty
- Mobile browser tab reload kills everything
Detection Pattern
WebGLRenderer|createRenderer|new THREE
Solution
WebGL Context Loss Is Normal, Not An Error:
The browser WILL lose your WebGL context when:
- User switches tabs (mobile especially)
- GPU driver crashes/updates
- Too many WebGL contexts (browser limit: ~8-16)
- System goes to sleep
You MUST handle this:
// Handle context loss
const canvas = renderer.domElement;
canvas.addEventListener('webglcontextlost', (event) => {
event.preventDefault(); // Important!
console.log('WebGL context lost');
// Stop animation loop
cancelAnimationFrame(animationId);
// Notify user
showMessage('Graphics lost, restoring...');
});
canvas.addEventListener('webglcontextrestored', () => {
console.log('WebGL context restored');
// Reinitialize everything
initScene();
reloadTextures();
// Restart animation
animate();
});
// Test context loss (for debugging)
// renderer.forceContextLoss();
// Restore after simulated loss
// renderer.forceContextRestore();Prevention strategies:
- Don't create multiple renderers
- Dispose unused resources
- Use powerPreference: 'high-performance'
References
- WebGL context loss handling
Three.js Objects Must Be Manually Disposed
Id
memory-leaks-disposal
Severity
CRITICAL
Description
GPU memory leaks will crash browsers - dispose everything
Symptoms
- Memory usage grows over time
- Browser becomes slow/unresponsive
- "Out of memory" crashes
- Performance degrades the longer app runs
Detection Pattern
new THREE\.|Geometry|Material|Texture
Solution
Three.js Does NOT Have Garbage Collection for GPU Resources:
JavaScript GC only handles JS objects. GPU resources (geometries, materials, textures) stay in VRAM forever unless you explicitly dispose them.
// WRONG - memory leak
function updateMesh() {
scene.remove(mesh);
mesh = new THREE.Mesh(new THREE.BoxGeometry(), material);
scene.add(mesh);
// Old geometry is STILL in GPU memory!
}
// RIGHT - proper disposal
function updateMesh() {
const oldGeometry = mesh.geometry;
mesh.geometry = new THREE.BoxGeometry();
oldGeometry.dispose(); // Free GPU memory
}
// Complete disposal helper
function disposeObject(obj) {
if (obj.geometry) {
obj.geometry.dispose();
}
if (obj.material) {
if (Array.isArray(obj.material)) {
obj.material.forEach(disposeMaterial);
} else {
disposeMaterial(obj.material);
}
}
if (obj.children) {
obj.children.forEach(disposeObject);
}
}
function disposeMaterial(material) {
// Dispose all textures
for (const key in material) {
const value = material[key];
if (value && value.isTexture) {
value.dispose();
}
}
material.dispose();
}
// Scene cleanup
function disposeScene() {
scene.traverse(disposeObject);
renderer.dispose();
controls?.dispose();
}Track your resources:
- Use console.log(renderer.info.memory) to monitor
- Check textures, geometries, programs counts
References
- Three.js dispose patterns
Mobile GPUs Don't Support highp In Fragment Shaders
Id
mobile-shader-precision
Severity
HIGH
Description
Shaders that work on desktop fail silently on mobile
Symptoms
- Black or corrupted output on mobile only
- "precision" errors in shader compilation
- Works on desktop, breaks on phones
- iOS Safari shader failures
Detection Pattern
ShaderMaterial|RawShaderMaterial|fragmentShader
Solution
Mobile GPU Precision Is Limited:
Desktop: highp, mediump, lowp all work Mobile: highp often unavailable in fragment shaders
// WRONG - fails on many mobile devices
precision highp float;
void main() {
// Complex calculations needing high precision
float value = someComplexCalculation();
gl_FragColor = vec4(value);
}
// RIGHT - check support and fallback
#ifdef GL_FRAGMENT_PRECISION_HIGH
precision highp float;
#else
precision mediump float;
#endif
void main() {
float value = someComplexCalculation();
gl_FragColor = vec4(value);
}JavaScript detection:
const gl = renderer.getContext();
const highp = gl.getShaderPrecisionFormat(
gl.FRAGMENT_SHADER,
gl.HIGH_FLOAT
);
const hasHighPrecision = highp.precision > 0;
console.log('Fragment highp support:', hasHighPrecision);
// Adjust quality based on capabilities
if (!hasHighPrecision) {
// Use simpler shaders
// Reduce precision-dependent effects
}Safe practices:
- Always use mediump unless you NEED highp
- Test on real mobile devices
- Provide fallback shaders
References
- WebGL shader precision
Maximum Texture Size Varies Wildly
Id
texture-size-limits
Severity
HIGH
Description
Your 8K textures won't load on many devices
Symptoms
- Textures not appearing on some devices
- Black textures on mobile
- Console warnings about texture size
- Works on dev machine, fails in production
Detection Pattern
TextureLoader|loadTexture|new.*Texture
Solution
Max Texture Size By Device:
Desktop: Usually 16384x16384 Modern mobile: 4096x4096 Old mobile: 2048x2048 Very old: 1024x1024
// Check max texture size
const gl = renderer.getContext();
const maxSize = gl.getParameter(gl.MAX_TEXTURE_SIZE);
console.log('Max texture size:', maxSize);
// Load appropriate texture
async function loadOptimalTexture(basePath) {
const gl = renderer.getContext();
const maxSize = gl.getParameter(gl.MAX_TEXTURE_SIZE);
let size;
if (maxSize >= 4096) {
size = '4k';
} else if (maxSize >= 2048) {
size = '2k';
} else {
size = '1k';
}
return textureLoader.loadAsync(`${basePath}_${size}.jpg`);
}
// Resize texture if needed
function ensureTextureSize(texture, maxDimension) {
const image = texture.image;
if (image.width <= maxDimension &&
image.height <= maxDimension) {
return texture;
}
// Resize using canvas
const canvas = document.createElement('canvas');
const scale = maxDimension / Math.max(image.width, image.height);
canvas.width = image.width * scale;
canvas.height = image.height * scale;
const ctx = canvas.getContext('2d');
ctx.drawImage(image, 0, 0, canvas.width, canvas.height);
texture.image = canvas;
texture.needsUpdate = true;
return texture;
}Best practices:
- Provide multiple texture sizes
- Use texture atlases to reduce count
- Compress with KTX2/Basis Universal
References
- WebGL texture limits
Z-Fighting Creates Flickering Artifacts
Id
z-fighting
Severity
MEDIUM
Description
Overlapping surfaces at similar depths fight for visibility
Symptoms
- Flickering/shimmering surfaces
- Textures seem to "fight" each other
- Artifacts on coplanar surfaces
- Gets worse at distance from camera
Detection Pattern
near:|far:|PerspectiveCamera|OrthographicCamera
Solution
Z-Fighting Is A Precision Problem:
Depth buffer has limited precision (24-bit typically). Far/near ratio affects precision distribution.
// WRONG - huge near/far range wastes precision
const camera = new THREE.PerspectiveCamera(
75,
aspect,
0.001, // Too small!
100000 // Too large!
);
// Ratio: 100,000,000:1 - terrible precision!
// RIGHT - minimize the range
const camera = new THREE.PerspectiveCamera(
75,
aspect,
0.1, // As large as possible
1000 // As small as possible
);
// Ratio: 10,000:1 - much better!
// Dynamic near/far based on scene
function updateCameraNearFar(camera, scene) {
const box = new THREE.Box3().setFromObject(scene);
const size = box.getSize(new THREE.Vector3());
const maxDim = Math.max(size.x, size.y, size.z);
camera.near = maxDim * 0.001;
camera.far = maxDim * 10;
camera.updateProjectionMatrix();
}Fix coplanar surfaces:
// Use polygonOffset for decals/labels
const decalMaterial = new THREE.MeshBasicMaterial({
map: decalTexture,
polygonOffset: true,
polygonOffsetFactor: -1,
polygonOffsetUnit: -1
});
// Or offset position slightly
decalMesh.position.z += 0.01; // Move slightly forwardUse logarithmic depth for huge scenes:
const renderer = new THREE.WebGLRenderer({
logarithmicDepthBuffer: true // Better precision at distance
});References
- Depth buffer precision
OrbitControls Adds Event Listeners That Leak
Id
orbit-controls-events
Severity
MEDIUM
Description
Controls keep listening even after you think they're gone
Symptoms
- Multiple OrbitControls instances fighting
- Events firing after scene change
- Cannot create new controls properly
- Touch events broken on mobile
Detection Pattern
OrbitControls|TrackballControls|FlyControls
Solution
Controls Attach To DOM - You Must Dispose:
// WRONG - leaks event listeners
function createScene() {
const controls = new OrbitControls(camera, renderer.domElement);
// When this function is called again,
// old controls still listening!
}
// RIGHT - dispose before creating new
let controls = null;
function createScene() {
if (controls) {
controls.dispose();
}
controls = new OrbitControls(camera, renderer.domElement);
}
// Complete cleanup
function cleanup() {
if (controls) {
controls.dispose();
controls = null;
}
}Common issues:
- React/Vue hot reload creates multiple controls
- Scene transitions don't clean up
- Multiple canvases on same page
Debug listeners:
// Check for leaked listeners
const listeners = getEventListeners(renderer.domElement);
console.log('Canvas listeners:', listeners);References
- Three.js controls cleanup
GLTF Models Often Have Material Problems
Id
gltf-material-issues
Severity
MEDIUM
Description
Models look different in Three.js than in Blender/modeling software
Symptoms
- Colors look wrong/washed out
- Metallic/roughness not matching
- Black materials after loading
- Emissive not working
Detection Pattern
GLTFLoader|loadGLTF|\.gltf|\.glb
Solution
GLTF Material Gotchas:
1. Color space issues:
// Ensure correct color output
renderer.outputColorSpace = THREE.SRGBColorSpace;
// GLTF loader should handle textures, but check:
gltf.scene.traverse((node) => {
if (node.material?.map) {
node.material.map.colorSpace = THREE.SRGBColorSpace;
}
});2. Environment maps for PBR:
// PBR materials NEED environment lighting
import { RGBELoader } from 'three/examples/jsm/loaders/RGBELoader.js';
const rgbeLoader = new RGBELoader();
rgbeLoader.load('environment.hdr', (texture) => {
texture.mapping = THREE.EquirectangularReflectionMapping;
scene.environment = texture;
scene.background = texture; // Optional
});3. Tone mapping for HDR:
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1.0;4. Fix black materials:
gltf.scene.traverse((node) => {
if (node.isMesh) {
// Ensure materials are usable
if (!node.material.envMap && scene.environment) {
node.material.envMap = scene.environment;
node.material.needsUpdate = true;
}
}
});Export settings in Blender:
- Use glTF 2.0 format
- Enable "Lighting: Standard" or "Unitless"
- Include all textures
- Apply modifiers
References
- GLTF material troubleshooting
requestAnimationFrame Continues When Tab Is Hidden
Id
animation-performance
Severity
MEDIUM
Description
Animation keeps running, wasting resources in background
Symptoms
- CPU usage when tab not visible
- Battery drain on mobile
- Catching up on animations when returning
- State getting out of sync
Detection Pattern
requestAnimationFrame|animate|render.*loop
Solution
Handle Tab Visibility:
let isVisible = true;
let lastTime = 0;
document.addEventListener('visibilitychange', () => {
isVisible = document.visibilityState === 'visible';
if (isVisible) {
// Reset delta time to avoid huge jumps
lastTime = performance.now();
animate();
}
});
function animate(currentTime = 0) {
if (!isVisible) return;
requestAnimationFrame(animate);
// Cap delta to prevent huge jumps
const delta = Math.min((currentTime - lastTime) / 1000, 0.1);
lastTime = currentTime;
update(delta);
renderer.render(scene, camera);
}Or use Three.js built-in:
// setAnimationLoop handles visibility automatically
renderer.setAnimationLoop((time) => {
controls.update();
renderer.render(scene, camera);
});
// Stop when needed
renderer.setAnimationLoop(null);References
- Page Visibility API
Too Many Draw Calls Kills Performance
Id
draw-call-explosion
Severity
HIGH
Description
Each mesh with unique material = separate draw call
Symptoms
- Low FPS despite simple geometry
- GPU not fully utilized
- "programs" count in renderer.info is high
- Performance drops with more objects
Detection Pattern
new THREE.Mesh|scene.add|for.*Mesh
Solution
Check Your Draw Calls:
// Monitor in dev
console.log(renderer.info.render.calls); // Draw calls
console.log(renderer.info.memory.geometries);
console.log(renderer.info.programs.length); // ShadersReduction strategies:
1. Merge geometries:
import { mergeGeometries } from 'three/examples/jsm/utils/BufferGeometryUtils.js';
const geometries = meshes.map(m => m.geometry);
const merged = mergeGeometries(geometries);
const singleMesh = new THREE.Mesh(merged, sharedMaterial);2. Use InstancedMesh:
const instancedMesh = new THREE.InstancedMesh(
geometry,
material,
count
);
// 1 draw call for thousands of objects3. Share materials:
// WRONG - 100 materials = 100 draw calls
meshes.forEach(m => {
m.material = new THREE.MeshStandardMaterial({ color: 0xff0000 });
});
// RIGHT - 1 shared material
const sharedMaterial = new THREE.MeshStandardMaterial({ color: 0xff0000 });
meshes.forEach(m => {
m.material = sharedMaterial;
});4. Texture atlases:
// Instead of 10 textures, use 1 atlas
// Adjust UVs to point to correct regionTarget: < 100 draw calls for 60fps on mobile
References
- Three.js performance tips
Threejs 3D Graphics - Validations
Resource Disposal Required
Id
check-disposal
Description
Three.js resources must be disposed to prevent memory leaks
Pattern
new THREE\.(Mesh|Geometry|Material|Texture)
File Glob
*/.{js,ts,jsx,tsx}
Match
present
Context Pattern
\.dispose\(\)
Message
Ensure geometries, materials, and textures are disposed when removed
Severity
warning
Autofix
Pixel Ratio Handling
Id
check-pixel-ratio
Description
Renderer should set pixel ratio for proper display
Pattern
new THREE\.WebGLRenderer
File Glob
*/.{js,ts,jsx,tsx}
Match
present
Context Pattern
setPixelRatio|devicePixelRatio
Message
Set pixel ratio with renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))
Severity
warning
Autofix
Resize Handler Required
Id
check-resize-handler
Description
Scene should handle window resize
Pattern
new THREE\.WebGLRenderer
File Glob
*/.{js,ts,jsx,tsx}
Match
present
Context Pattern
resize|onResize|ResizeObserver
Message
Add resize handler to update camera aspect and renderer size
Severity
warning
Autofix
WebGL Context Loss Handling
Id
check-context-lost
Description
Handle WebGL context loss for robustness
Pattern
new THREE\.WebGLRenderer
File Glob
*/.{js,ts,jsx,tsx}
Match
present
Context Pattern
webglcontextlost|contextlost
Message
Consider handling WebGL context loss events
Severity
info
Autofix
Proper Animation Loop
Id
check-animation-loop
Description
Use setAnimationLoop instead of manual RAF for visibility handling
Pattern
requestAnimationFrame.*render
File Glob
*/.{js,ts,jsx,tsx}
Match
present
Context Pattern
setAnimationLoop|visibilitychange
Message
Consider using renderer.setAnimationLoop() for automatic visibility handling
Severity
info
Autofix
Shader Precision Fallback
Id
check-shader-precision
Description
GLSL shaders should handle mediump fallback for mobile
Pattern
precision highp float
File Glob
*/.{js,ts,jsx,tsx,glsl,frag,vert}
Match
present
Context Pattern
GL_FRAGMENT_PRECISION_HIGH|mediump
Message
Add precision fallback for mobile: #ifdef GL_FRAGMENT_PRECISION_HIGH
Severity
warning
Autofix
Async Texture Loading
Id
check-texture-async
Description
Textures should be loaded asynchronously
Pattern
textureLoader\.load\(
File Glob
*/.{js,ts,jsx,tsx}
Match
present
Context Pattern
loadAsync|callback|then|await
Message
Use loadAsync or callbacks to handle texture loading
Severity
info
Autofix
Environment Map for PBR
Id
check-environment-map
Description
PBR materials need environment maps to look correct
Pattern
MeshStandardMaterial|MeshPhysicalMaterial
File Glob
*/.{js,ts,jsx,tsx}
Match
present
Context Pattern
envMap|scene\.environment
Message
PBR materials need environment maps for proper lighting
Severity
info
Autofix
Camera Frustum Optimization
Id
check-camera-frustum
Description
Near/far values should be optimized to prevent z-fighting
Pattern
PerspectiveCamera\([^)]+0\.00?1
File Glob
*/.{js,ts,jsx,tsx}
Match
present
Message
Very small near value (0.001) can cause z-fighting. Use largest near value possible.
Severity
warning
Autofix
Instancing for Repeated Objects
Id
check-instancing
Description
Use InstancedMesh for many similar objects
Pattern
for.*new THREE\.Mesh
File Glob
*/.{js,ts,jsx,tsx}
Match
present
Context Pattern
InstancedMesh
Message
Consider using InstancedMesh for better performance with repeated objects
Severity
info
Autofix
Controls Disposal
Id
check-controls-dispose
Description
OrbitControls and other controls must be disposed
Pattern
new.*Controls\(
File Glob
*/.{js,ts,jsx,tsx}
Match
present
Context Pattern
controls\.dispose|dispose.*controls
Message
Dispose controls when cleaning up to prevent event listener leaks
Severity
warning
Autofix
Color Space Configuration
Id
check-color-space
Description
Proper color space for correct color output
Pattern
new THREE\.WebGLRenderer
File Glob
*/.{js,ts,jsx,tsx}
Match
present
Context Pattern
outputColorSpace|outputEncoding
Message
Set renderer.outputColorSpace = THREE.SRGBColorSpace for correct colors
Severity
info
Autofix
Related skills
How it compares
Pick threejs-3d-graphics over generic frontend skills when tasks need WebGL-specific debugging, GLSL shaders, and Three.js performance patterns rather than standard React UI work.
FAQ
Which reference files does threejs-3d-graphics use?
threejs-3d-graphics uses references/patterns.md for creation workflows, references/sharp_edges.md for diagnosing WebGL failures, and references/validations.md for strict constraint checks during review.
When should developers use threejs-3d-graphics?
threejs-3d-graphics activates when tasks involve Three.js scenes, WebGL rendering, GLSL shaders, 3D animation, asset loaders, or browser graphics performance tuning for interactive web experiences.