Do not debug everything at once
A blank canvas can come from a missing canvas size, an offscreen camera, a clipped object, a black material, a failed import, or a render loop that never runs. If you change all of those at once, you may hide the actual cause. Debug in layers and keep each test boring.
First prove the renderer can draw a basic cube with a MeshNormalMaterial. This material does not need lights, so it removes lighting from the question. If the cube appears, the renderer, camera, and loop are basically alive. If it does not, look at canvas sizing, WebGL context errors, and camera placement.
Check the camera next
A camera can look in the wrong direction, sit inside the object, or clip the object with near and far planes. Add a CameraHelper when using shadow cameras or complex rigs. For a PerspectiveCamera, start with a visible position, call `lookAt(0, 0, 0)`, and render a known object at the origin.
When loading unknown models, compute a bounding box and log its size. If the box is enormous or tiny, the camera may be fine and the scale may be the problem. If the box center is far from the origin, the object may be outside the camera's view.
Separate material and light issues
MeshStandardMaterial needs light. MeshBasicMaterial does not. MeshNormalMaterial provides an even stronger diagnostic because it colors surfaces by normal direction. Swap materials temporarily. If the object appears with a diagnostic material but disappears with its original material, inspect textures, color management, alpha settings, side settings, and lights.
For imported models, traverse the scene and count meshes. Log material names and visibility. It is possible for a loader to succeed while the visible content is hidden inside a nested hierarchy or assigned a fully transparent material.
Use helpers without shipping them
GridHelper, AxesHelper, BoxHelper, and light helpers are excellent during development. They reveal scale, orientation, and positions that are otherwise invisible. Add them through a debug flag so they can be toggled without editing the scene.
Before publishing, remove or hide debug helpers. A production page should feel intentional, but a development workflow should be full of instruments. The balance is simple: make problems visible while building, then keep only the helpful visual structure for users.
Return to a known-good rendered object
The GLB Viewer sample proves that the renderer, camera, geometry, material, and lights can produce a visible frame.
- Visible cube
- Non-zero geometry metrics
- Camera fitted from bounds
Replace the unknown scene with a diagnostic cube
Debugging every imported asset and material assumption at once hides the first failing layer.
loader.load(assetUrl, ({ scene: model }) => {
scene.add(model);
startPostprocessing();
});scene.add(new THREE.Mesh(
new THREE.BoxGeometry(1, 1, 1),
new THREE.MeshBasicMaterial({ color: 0xff5533 })
));
camera.position.set(0, 0, 3);
renderer.render(scene, camera);Diagnostic layer record
A layer passes only when it produces a visible or inspectable result.
| Layer | Pass evidence |
|---|---|
| Renderer | Canvas has a non-empty frame |
| Camera | Diagnostic cube is inside frustum |
| Asset | Bounds and mesh count are finite |
| Material / light | Basic material works before PBR |
Bisect a blank canvas
Keep each successful layer in place while adding the next one back.
- Set a clear color and render a BasicMaterial cube.
- Add camera and BoxHelper diagnostics.
- Load the asset but keep the diagnostic material.
- Restore project materials, lights, controls, and effects one group at a time.
Known-good diagnostic cube
scene.add(new THREE.GridHelper(10, 10));
scene.add(new THREE.AxesHelper(2));
const cube = new THREE.Mesh(
new THREE.BoxGeometry(1, 1, 1),
new THREE.MeshNormalMaterial()
);
scene.add(cube);
camera.position.set(3, 2, 4);
camera.lookAt(0, 0, 0);
renderer.render(scene, camera);
Sources and further reading
Update record
- : Rewritten around a reproducible demo, explicit test record, code comparison, and known platform limits.