| name | graphics-troubleshooting |
| description | Debug and fix common issues in Three.js, React Three Fiber, and 3D asset pipelines. Use when encountering visual bugs, performance problems, loading failures, or unexpected behavior. Covers black screens, Z-fighting, memory leaks, shader errors, and physics glitches. |
Graphics Troubleshooting
Diagnose and fix 3D web graphics issues fast. Organized by symptom (what you see) for rapid diagnosis.
Library Versions (2026)
- Three.js: r171+
- React Three Fiber: v9.5+
- @react-three/drei: v9.116+
- @react-three/rapier: v2+
Quick Diagnosis
Nothing Renders (Black/Empty Screen)
| Check | Command/Action |
|---|---|
| Camera position | Is camera inside object? Move to [0, 0, 5] |
| Camera target | Is camera looking at scene? Check lookAt |
| Object scale | Is object too small/large? Check scale isn't 0 |
| Lights | Are there any lights? Add <ambientLight /> |
| Material | Using MeshStandardMaterial without lights? |
| Render loop | Is animate() being called? |
| Canvas size | Is container height 0? Check CSS |
| WebGPU support | Browser support? Check console for errors |
R3F Minimum Visible Scene:
<Canvas>
<ambientLight intensity={0.5} />
<directionalLight position={[5, 5, 5]} />
<mesh position={[0, 0, 0]}>
<boxGeometry />
<meshStandardMaterial color="red" />
</mesh>
</Canvas>
Vanilla Three.js Minimum Scene:
import * as THREE from 'three/webgpu'
const scene = new THREE.Scene()
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000)
camera.position.z = 5
const renderer = new THREE.WebGPURenderer({ antialias: true })
await renderer.init()
renderer.setSize(window.innerWidth, window.innerHeight)
document.body.appendChild(renderer.domElement)
// Add light - required for Standard materials
scene.add(new THREE.AmbientLight(0xffffff, 0.5))
scene.add(new THREE.DirectionalLight(0xffffff, 1))
// Add visible object
const mesh = new THREE.Mesh(
new THREE.BoxGeometry(),
new THREE.MeshStandardMaterial({ color: 'red' })
)
scene.add(mesh)
function animate() {
renderer.render(scene, camera)
}
renderer.setAnimationLoop(animate)
Object Renders But Looks Wrong
| Symptom | Cause | Fix |
|---|---|---|
| All black | No lights | Add ambient + directional light |
| Flickering faces | Z-fighting | Increase camera near, offset overlapping geometry |
| Inside-out | Inverted normals | material.side = THREE.DoubleSide or fix in Blender |
| Pixelated edges | Low DPR | renderer.setPixelRatio(Math.min(devicePixelRatio, 2)) |
| Washed out colors | No tone mapping | renderer.toneMapping = THREE.ACESFilmicToneMapping |
| Colors look wrong | Color space | renderer.outputColorSpace = THREE.SRGBColorSpace |
| Texture blurry | Filtering | texture.minFilter = THREE.LinearMipmapLinearFilter |
| Texture stretched | Wrong UVs | Check UV mapping in Blender, use texture.repeat |
Performance Issues
| Symptom | Diagnosis | Target | Fix |
|---|---|---|---|
| Low FPS (<30) | Check renderer.info.render.calls |
<100 draw calls | Use InstancedMesh, merge geometries |
| Stuttering/hitching | GC pauses | Avoid allocations | Object pooling, reuse Vector3s |
| Memory growing | Check renderer.info.memory |
Stable | Call .dispose() on removal |
| Slow initial load | Large assets | <5MB total | Compress with Meshopt, resize textures |
| Slow on mobile | Too many triangles | <100K | Simplify geometry, use LOD |
Quick Performance Check:
// Add to console or scene
console.log('Draw calls:', renderer.info.render.calls)
console.log('Triangles:', renderer.info.render.triangles)
console.log('Geometries:', renderer.info.memory.geometries)
console.log('Textures:', renderer.info.memory.textures)
See references/performance.md for detailed profiling.
Loading Failures
| Error | Cause | Fix |
|---|---|---|
| 404 Not Found | Wrong path | Check file exists, use absolute path from public |
| CORS error | Cross-origin | Serve from same origin or configure CORS headers |
| "Unexpected token" | Wrong file format | Ensure file is valid GLTF/GLB |
| Draco decode error | Missing decoder | Set dracoLoader.setDecoderPath('/draco/') |
| KTX2 decode error | Missing transcoder | Set ktx2Loader.setTranscoderPath('/basis/') |
| "Invalid glTF" | Corrupted file | Validate at https://gltf.report |
| Model invisible | Wrong scale | Check scale (Blender default: 1 unit = 1 meter) |
Loader Setup Pattern:
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js'
import { DRACOLoader } from 'three/addons/loaders/DRACOLoader.js'
import { KTX2Loader } from 'three/addons/loaders/KTX2Loader.js'
import { MeshoptDecoder } from 'three/addons/libs/meshopt_decoder.module.js'
const loader = new GLTFLoader()
// Draco (for Draco-compressed models)
const dracoLoader = new DRACOLoader()
dracoLoader.setDecoderPath('https://www.gstatic.com/draco/versioned/decoders/1.5.6/')
loader.setDRACOLoader(dracoLoader)
// Meshopt (for Meshopt-compressed models)
loader.setMeshoptDecoder(MeshoptDecoder)
// KTX2 (for Basis Universal textures)
const ktx2Loader = new KTX2Loader()
ktx2Loader.setTranscoderPath('https://cdn.jsdelivr.net/npm/three@0.171.0/examples/jsm/libs/basis/')
ktx2Loader.detectSupport(renderer)
loader.setKTX2Loader(ktx2Loader)
See references/loading.md for more patterns.
Physics Issues (@react-three/rapier)
| Symptom | Cause | Fix |
|---|---|---|
| Objects fall through floor | Missing collider | Add <RigidBody type="fixed"> to ground |
| Objects stuck in air | Wrong body type | Use type="dynamic" for moving objects |
| Jittery movement | High velocity + low mass | Increase mass or reduce forces |
| Tunneling (fast objects pass through) | CCD disabled | Enable ccd={true} on fast bodies |
| Collider wrong shape | Auto-collider mismatch | Use explicit <CuboidCollider> etc. |
| Physics not updating | Wrong loop | Check updateLoop prop on <Physics> |
Minimum Physics Setup:
import { Physics, RigidBody } from '@react-three/rapier'
<Physics gravity={[0, -9.81, 0]} debug>
{/* Ground - fixed, doesn't move */}
<RigidBody type="fixed">
<mesh position={[0, -1, 0]}>
<boxGeometry args={[10, 0.5, 10]} />
<meshStandardMaterial />
</mesh>
</RigidBody>
{/* Falling object - dynamic */}
<RigidBody type="dynamic">
<mesh>
<sphereGeometry />
<meshStandardMaterial />
</mesh>
</RigidBody>
</Physics>
Shader/Material Errors
| Error | Cause | Fix |
|---|---|---|
| "X is not defined" in shader | Missing uniform/varying | Declare all variables |
| Black material | Shader compile error | Check console for GLSL/WGSL errors |
| Material not updating | Missing needsUpdate |
Set material.needsUpdate = true |
| TSL error | Wrong import | Use import { x } from 'three/tsl' |
| NodeMaterial black | Missing colorNode | Set material.colorNode = ... |
TSL Debug Pattern:
import { color, uniform } from 'three/tsl'
// Start simple, add complexity
const material = new THREE.MeshBasicNodeMaterial()
material.colorNode = color(0xff0000) // Should be red
// If this works, add your custom logic incrementally
Related Skills
| When you need... | Use skill |
|---|---|
| Optimize assets before loading | asset-pipeline-3d |
| Build scenes with vanilla Three.js | threejs |
| Build scenes with React | react-three-fiber |
Reference Files
- references/visual-bugs.md - Z-fighting, clipping, shadow artifacts, transparency issues
- references/performance.md - Profiling, optimization, memory management
- references/loading.md - Asset loading, decoder setup, CORS, caching