Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

iTechGuides is reader-supported. When you buy through links on our site, we may earn an affiliate commission. As an Amazon Associate I earn from qualifying purchases. Learn more

If a Three.js canvas looks correct on screen but canvas.toDataURL() returns a blank, black, or transparent image in Chromium, render the scene again immediately before reading the canvas. WebGL normally clears its drawing buffer after presenting a frame, so a later read can capture an already-cleared buffer. For a one-time export, this synchronous re-render is usually the simplest and fastest fix.

The first fix: render immediately before export

Keep the code that draws the scene separate from the code that schedules animation frames. Set the exact camera, animation, controls, and scene state you want, call renderer.render(scene, camera), and export in the same task. Do not wait for a later requestAnimationFrame callback or assume that the pixels from the last displayed frame remain available.

function render() {
  // Update scene and camera state here if required.
  renderer.render(scene, camera);
}

function capturePng() {
  render(); // Draw the exact state that will be captured.

  canvas.toBlob((blob) => {
    if (!blob) {
      console.error('Canvas export returned no Blob');
      return;
    }

    const link = document.createElement('a');
    link.href = URL.createObjectURL(blob);
    link.download = 'three-scene.png';
    link.click();
    URL.revokeObjectURL(link.href);
  }, 'image/png');
}

document.querySelector('#capture').addEventListener('click', capturePng);

Three.js documentation describes this pattern as calling the rendering code just before capturing. toBlob() is generally preferable for image export because it avoids putting the entire encoded file into a JavaScript data URL string. If another part of your application specifically needs a data URL, use the same timing rule:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
function captureDataUrl() {
  render();
  const dataUrl = canvas.toDataURL('image/png');
  return dataUrl;
}

Call this after all state changes have been applied. If you change the canvas size, camera aspect ratio, renderer pixel ratio, or scene contents, render again after those changes and before export.

Why a visible canvas can export as blank or black

WebGL uses a drawing buffer for rendering. With the default context behavior, the buffer may be cleared after the browser composites the frame to the page. The displayed frame can therefore look correct while a later toDataURL() call reads cleared contents. This is a buffer-lifetime issue, not necessarily a Three.js rendering failure.

  • Blank or black export: render synchronously immediately before reading the canvas.
  • Exception from toDataURL(): inspect the exact exception. A security error points to an origin-clean or cross-origin resource problem, not merely a cleared buffer.
  • Transparent or dark edges: inspect alpha, premultiplied alpha, scene background, and clear color separately.
  • Wrong dimensions: check the drawing-buffer size, which can differ from the canvas’s CSS size.

Chromium-specific debugging should record the Chromium version, operating system, GPU, Three.js version, context attributes, whether the context was created by Three.js or supplied manually, and the exact output or exception. The available guidance describes general WebGL behavior; it does not establish a regression in a particular Chromium release.

Option 1: re-render for a one-off screenshot

This is the preferred approach when a user clicks an export button or a job needs one image. It leaves the normal transient-buffer behavior intact and avoids the performance cost of preserving every frame.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
  1. Stop or pause animation if the capture must represent a stable moment.
  2. Apply the desired scene, camera, control, lighting, and material state.
  3. Update any animations or uniforms needed for that exact state.
  4. Call renderer.render(scene, camera) synchronously.
  5. Immediately call toBlob() or toDataURL().
  6. Resume animation only after the read has been initiated.
let animationPaused = false;

function animate(time) {
  requestAnimationFrame(animate);
  if (!animationPaused) {
    updateScene(time);
    renderer.render(scene, camera);
  }
}

async function exportCurrentView() {
  animationPaused = true;
  updateScene(performance.now());
  renderer.render(scene, camera);

  const blob = await new Promise((resolve) => {
    canvas.toBlob(resolve, 'image/png');
  });

  animationPaused = false;
  if (!blob) throw new Error('PNG encoding failed');
  return blob;
}

The render must target the same canvas whose pixels you read. Rendering another renderer or another canvas does not populate this canvas’s drawing buffer.

Option 2: preserve the default drawing buffer

If the application must capture later—after another callback, a UI interaction, or delayed processing—you can request preservation when the WebGL context is created:

const renderer = new THREE.WebGLRenderer({
  canvas,
  preserveDrawingBuffer: true
});

Context attributes are creation-time settings. If Three.js creates the context, pass the option to WebGLRenderer. If your application creates the context first, set the attribute in getContext() before giving that context to Three.js:

const gl = canvas.getContext('webgl2', {
  preserveDrawingBuffer: true
});

if (!gl) throw new Error('WebGL2 is unavailable');

const renderer = new THREE.WebGLRenderer({
  canvas,
  context: gl
});

Passing preserveDrawingBuffer later as a renderer option cannot change attributes on an already-created context. This is a common reason the option appears ineffective when an application supplies its own context.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Preservation has a trade-off: Khronos warns that it can cause significant performance loss on some platforms. Leave it disabled when immediate re-rendering or a render target meets your needs. Resizing the canvas can also clear its contents, so always render again after a resize.

renderer.autoClearColor = false is a separate Three.js setting. It controls whether Three.js clears its color buffer during its own render flow; it does not replace the WebGL context’s preserveDrawingBuffer attribute.

Option 3: render to a WebGLRenderTarget

For deliberate pixel readback, an off-screen render target is more explicit than relying on the displayed canvas. Render the scene into the target, then read that target’s pixels. Three.js exposes synchronous and asynchronous APIs; use the asynchronous form where possible.

const target = new THREE.WebGLRenderTarget(1024, 1024, {
  format: THREE.RGBAFormat,
  type: THREE.UnsignedByteType
});

async function readTarget() {
  renderer.setRenderTarget(target);
  renderer.render(scene, camera);
  renderer.setRenderTarget(null);

  const pixels = new Uint8Array(1024 * 1024 * 4);
  await renderer.readRenderTargetPixelsAsync(
    target,
    0,
    0,
    1024,
    1024,
    pixels
  );

  return pixels;
}

The returned bytes are raw pixels, not a PNG or data URL. You must convert them with an appropriate image or canvas-encoding pipeline. Remember that WebGL readback is commonly bottom-to-top, so an image conversion may need to flip rows. Check the texture format, type, color space, and alpha handling required by your application.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

If asynchronous readback is unavailable for the renderer or target, readRenderTargetPixels() is the synchronous alternative, but it can block the main thread more noticeably.

Alpha, transparency, and dark fringes

A wholly blank export caused by a cleared buffer is different from an image that has the wrong background or halos. Investigate these settings independently:

  • alpha: determines whether the drawing buffer supports an alpha channel and how the canvas composites with the page.
  • premultipliedAlpha: controls how color channels relate to alpha during compositing.
  • Scene background: a scene background produces opaque color instead of transparency.
  • Clear color and clear alpha: explicit clears affect the pixels you export.
  • Image conversion: converting premultiplied data back to ordinary RGBA can be lossy at partially transparent edges.
const renderer = new THREE.WebGLRenderer({
  canvas,
  alpha: true,
  premultipliedAlpha: false
});

renderer.setClearColor(0x000000, 0);

These choices cannot restore pixels that were already cleared. First establish correct capture timing, then tune compositing.

Canvas size, pixel ratio, and resizing

The exported image uses the drawing-buffer dimensions, not necessarily the CSS dimensions visible on screen. A canvas styled at 800 × 600 CSS pixels may have a different internal width and height because of device pixel ratio or explicit renderer settings.

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
function resizeRenderer() {
  const width = canvas.clientWidth;
  const height = canvas.clientHeight;

  if (canvas.width !== width || canvas.height !== height) {
    renderer.setSize(width, height, false);
    camera.aspect = width / height;
    camera.updateProjectionMatrix();
    renderer.render(scene, camera);
  }
}

For a high-resolution export, choose the drawing-buffer size deliberately, render at that size, capture, then restore the interactive size and render again. A resize can clear the buffer, so never resize and export without an intervening render.

Troubleshooting checklist

The page looks right, but the file is black

  • Call renderer.render(scene, camera) immediately before export.
  • Confirm that the render uses the same canvas passed to toDataURL().
  • Check that no resize, renderer switch, or post-processing pass occurred between rendering and reading.
  • If delayed capture is unavoidable, create the context with preserveDrawingBuffer: true or use a render target.

preserveDrawingBuffer did nothing

  • Determine who created the context.
  • If you supplied context to WebGLRenderer, set the attribute in canvas.getContext('webgl', ...) or getContext('webgl2', ...) first.
  • Do not expect renderer options to modify an existing context.
  • Re-render after any canvas resize.

toDataURL() throws a security error

This is a different class of failure from a cleared drawing buffer. Inspect the exception and the origin-clean state. External images, textures, fonts, or other resources may have been served in a way that prevents readback. The material available here does not establish a universal CORS configuration; verify how each resource is served and reproduce with a minimal same-origin scene before changing browser settings.

The result is transparent, has a black background, or shows halos

  • Inspect alpha, premultipliedAlpha, clear alpha, and scene background.
  • Check whether the receiving application expects premultiplied or straight alpha.
  • Use a known opaque background temporarily to distinguish compositing from missing geometry.

The export has the wrong size

  • Log canvas.width and canvas.height, not only CSS dimensions.
  • Account for device pixel ratio and renderer pixel-ratio settings.
  • Set the intended drawing-buffer size, update the camera, render, and then export.

A suspected Chromium regression cannot be reproduced consistently

Record Chromium version, operating system, GPU, Three.js version, WebGL or WebGL2 choice, context attributes, renderer initialization code, exact exception or image result, and a minimal reproduction. Compare immediate re-render, preserved buffer, and render-target capture before assigning the cause to Chromium.

Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Choosing the right capture method

Method Best for Timing Main trade-off
Render immediately, then export One-off screenshots and button actions Same synchronous capture operation Requires a fresh render each time
preserveDrawingBuffer: true Later reads from the default canvas Buffer remains available after presentation Can reduce performance; must be set at context creation
WebGLRenderTarget Explicit off-screen rendering and pixel pipelines Read the selected target More code; pixels still need image encoding

For a single screenshot, start with immediate rendering. For recurring or delayed capture, choose preservation or a render-target pipeline after considering performance, output format, and implementation complexity.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Or skip the browser setup

If the requirement is a website screenshot rather than a Three.js framebuffer export, ScreenshotNeo can capture the page through one request. It accepts consent banners like a visitor and removes more than 60 known consent platforms, newsletter popups, and chat widgets before capture. Bot checks, CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed; the response identifies the page verdict and billing status in headers. Its MCP server provides take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients.

Example using cURL:

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp

Python:

import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)

Node.js:

const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);

See the ScreenshotNeo documentation for capture options. The free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000 screenshots. Create a free ScreenshotNeo account.

Frequently asked questions

Should I always enable preserveDrawingBuffer?

No. Use immediate re-rendering for one-off exports, because preservation can impose a platform-dependent performance cost.

Can toBlob() fix a cleared WebGL buffer?

No. It changes encoding and memory behavior, not when the buffer is read. Render immediately before either export method.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Is a black image proof that Chromium is broken?

No. Buffer clearing, alpha configuration, resizing, resource security, and application timing can all produce similar symptoms. Capture the exact environment and exception before diagnosing a browser defect.

When should I use a render target?

Use one when you need controlled off-screen rendering, repeated pixel reads, or a pipeline independent of the displayed canvas.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.