Skip to content
Dimensa

WebGL debugging

Three.js scene not rendering? Prove why before you touch the shader

The canvas is there. The console is clean. No shader compile errors, no crash. And the scene shows nothing: a black or empty area where your geometry should be.

Most people start editing the camera, the point size or the shader and hope. That can take days. There is a faster way: first prove which of two very different problems you have. Is three.js not drawing, or is it drawing something you cannot see? This page shows how we answer that in one step, with the real case from the 3D hero on this site.

Short answer

  • Check the cheap causes first: canvas with zero height, camera inside or behind the object, near/far planes, no lights on a lit material, renderer.render() never called.
  • Still nothing? Patch the draw call. Count drawArrays/drawElements calls and read a pixel with readPixels right after each draw, before the frame is shown.
  • No draw calls means a scene, camera or render-loop problem. Draw calls with pixels near the background colour means a colour, alpha or depth problem in the shader.
  • Works locally, empty in production? Check the network tab for 404s on models and textures, and make sure the 3D component only runs in the browser.

Two failures that look exactly the same

An empty canvas can mean that nothing reaches the GPU: the object is not in the scene, the camera looks the wrong way, or the render loop never runs. It can also mean that everything is drawn correctly, but in a colour so close to the background, or so transparent, that your eye cannot see it.

From the outside these two look identical. Inside they have nothing in common. The fix for the first one is in your scene setup. The fix for the second is in your shader or material. If you do not know which one you have, every change you make is a guess.

Check the cheap causes first

Before you instrument anything, rule out the usual suspects. Each one takes a minute to check.

Canvas size. If the parent element has no height, the canvas is 0 pixels high. Inspect the canvas element and check its real width and height.

Camera. The camera can sit inside the object, behind it, or too far away for the far plane. Log the camera position and the object position, and try a much larger far value.

Lights. MeshStandardMaterial and MeshPhongMaterial need light. Without it the object is black. On a dark background that looks like nothing at all. Swap to MeshBasicMaterial for a quick test.

Render loop and scene. Make sure the object is added to the scene and that renderer.render(scene, camera) actually runs, every frame if you animate.

Lost context. Listen for the webglcontextlost event on the canvas. A lost context also gives an empty canvas without a clear error.

Instrument the draw call

If the cheap checks pass, stop reading the shader and measure. Three.js ends every frame in plain WebGL calls: drawArrays for non-indexed geometry such as points, drawElements for most meshes. Patch them on the prototype, count the calls, and read one pixel straight after each draw. Reading inside the patched call matters: by default the drawing buffer is cleared after the frame is shown, so a later read gives you nothing.

Paste this in the browser console on the page (use WebGLRenderingContext instead for a WebGL1 context). It logs once per second, so it does not flood the console.

const proto = WebGL2RenderingContext.prototype;
let calls = 0;
let vertices = 0;
let lastLog = -Infinity;

for (const name of ["drawArrays", "drawElements"]) {
  const original = proto[name];
  proto[name] = function (...args) {
    const result = original.apply(this, args);
    calls++;
    vertices += name === "drawArrays" ? args[2] : args[1];

    const now = performance.now();
    if (now - lastLog > 1000) {
      lastLog = now;
      const px = new Uint8Array(4);
      this.readPixels(
        this.drawingBufferWidth >> 1,
        this.drawingBufferHeight >> 1,
        1, 1, this.RGBA, this.UNSIGNED_BYTE, px,
      );
      console.log({ calls, vertices, centerPixel: [...px] });
    }
    return result;
  };
}

How to read the result

Zero calls: nothing is being drawn. Go back to the scene: is the object added, is the render loop running, is this the canvas you think it is?

Calls and vertices, and the pixel equals the background: the geometry is drawn, but you cannot see it. Look at colour, alpha, blending and depth in the material or shader. Move the sample point to where the object should be if it is not in the centre.

Calls and vertices, and the pixel has a clear colour: the scene renders. The problem is later: a post-processing pass, CSS on top of the canvas, or an element that covers it.

Remove the patch when you are done. It is a diagnostic tool, not production code.

What we found in our own 3D hero

The point-cloud hero on this site showed an empty dark area. With the patch above we saw the scene draw 30,912 points every frame, correctly. So the render loop, the camera and the geometry were fine. Sampling the pixels showed the real cause: the fragment colours came back almost identical to the background, at very low alpha. The shader's "low" colour, #33445a, sat too close to the page background #0b1017.

The fix was in the shader, not the scene: we moved the colour palette away from the background, removed a stray alpha multiplier, widened the depth fade and raised the point size. On the way we also caught a smoothstep() call with its edges in the wrong order. In GLSL the result is undefined when the first edge is not smaller than the second. It happened to look plausible on our GPU, which is exactly the kind of bug that breaks on another device.

Without the measurement, all of this would have been guessing in the wrong place. With it, the question "is it drawing?" was answered in one step.

Try it yourself below. This is the same bug, rebuilt for this page: 30,912 points, the old colour and alpha, and the same measurement as the snippet above. Switch between before and after and watch the numbers.

Live WebGL: the same bug as in our hero, rebuilt for this page. It starts only when you ask, so the page stays light.

Works locally, empty in production

If the scene works on your machine and not on the live site, the shader is rarely the cause. Check these first.

Asset paths. Open the network tab and look for 404s on .glb, .gltf, texture or HDR files. A path that works in development can break after a build or on a sub-path.

Server rendering. In Next.js, a component that touches window or WebGL must only run in the browser. Load it with a dynamic import and ssr: false, or mount it inside an effect.

Other devices. Mobile GPUs often use lower shader precision and have tighter limits. A scene that is fine on a desktop GPU can fail on a phone. Test on the device that shows the problem, and run the draw-call patch there too.

When this does not help

The patch tells you whether pixels are drawn, not why the GPU disagrees with your expectations. Driver bugs, extension support and precision issues can need device-specific testing. And if your app renders to a render target first, readPixels reads whatever framebuffer is bound at that moment, so read after the final pass to see what the user sees.

Stuck on a WebGL or three.js problem?

Send us the live URL, what you expect to see, what you see instead, and the browser and device where it fails. We first prove where the problem is, then tell you what the fix takes.

Please do not send passwords or API keys. If we need access, we arrange that safely.

Request a quote

FAQ

Frequently asked questions

Why is my three.js canvas black with no errors in the console?

Usually one of two things: nothing is drawn (scene, camera or render loop), or something is drawn but invisible (colour, alpha, lighting). Count the WebGL draw calls and sample a pixel to find out which one you have.

Why does my three.js mesh look completely black?

Lit materials such as MeshStandardMaterial need a light in the scene. Without one they render black. Add an ambient and a directional light, or test with MeshBasicMaterial.

Why does my three.js scene work locally but not on Vercel or in production?

Check for 404s on models and textures first, then make sure the 3D component only runs in the browser and not during server rendering. Shader precision on mobile devices is the next suspect.

Is it safe to patch WebGL functions on a live site?

Only as a temporary check in your own browser console. It changes nothing for other visitors and disappears when you reload the page. Do not ship it in your code.