ScreenshotNeo

BlogHow-to

How to Capture WebGL Content in Screenshots

Capture WebGL canvas pixels with correct timing, preserveDrawingBuffer trade-offs, and browser screenshots when you need the whole page.

By the ScreenshotNeo team4 October 20269 min read

To capture WebGL content from an application you control, export its canvas with toBlob() or toDataURL() while the intended frame is still available. With the default WebGL drawing-buffer behavior, the browser may clear the buffer after compositing, so a later read can produce a blank or stale image. If you need a screenshot of the visible browser page instead of just the canvas, use browser screenshot capture.

This guide covers direct canvas export, timing, buffer preservation, transparency, DOM reconstruction limits, and browser-level capture. The exact result depends on the browser, graphics implementation, and when capture runs.

1. Choose the capture method

What you need Use Trade-off
Only the WebGL render, with access to app code Export the WebGL canvas using toBlob() or toDataURL() while its pixels are readable. Capture timing matters when drawing-buffer preservation is disabled.
Canvas export after the render callback has returned Consider creating the context with preserveDrawingBuffer: true, or arrange a read during rendering. Preservation can reduce performance on some platforms and must be set when the context is created.
The visible browser tab, including surrounding HTML Use browser or extension screenshot capture, or automate a real browser. This captures the visible view, not a direct canvas-pixel export.
A DOM-based recreation of a page section Use a DOM reconstruction library such as html2canvas. It reconstructs from DOM and styles; it is not a native screenshot and may not reproduce WebGL accurately.

2. Export a WebGL canvas from your application

A WebGL canvas is an HTML canvas. Its standard image-export methods work when the drawing buffer still contains the frame you want. Prefer toBlob() for saving an image because it avoids building a large base64 string in JavaScript memory.

Runnable browser example

Call the capture function immediately after the code that renders the desired frame, in the same task. Replace the placeholder render function with your renderer’s draw call.

<canvas id="scene" width="800" height="600"></canvas>
<button id="save">Save WebGL frame</button>
<script>
  const canvas = document.querySelector('#scene');
  const gl = canvas.getContext('webgl');
  if (!gl) throw new Error('WebGL is unavailable');

  function renderFrame() {
    gl.viewport(0, 0, canvas.width, canvas.height);
    gl.clearColor(0.05, 0.12, 0.24, 1);
    gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
    // Draw your scene here.
  }

  function saveFrame() {
    canvas.toBlob((blob) => {
      if (!blob) {
        console.error('Canvas export returned no image. Check capture timing and canvas origin.');
        return;
      }
      const link = document.createElement('a');
      const objectUrl = URL.createObjectURL(blob);
      link.href = objectUrl;
      link.download = 'webgl-frame.png';
      link.click();
      URL.revokeObjectURL(objectUrl);
    }, 'image/png');
  }

  document.querySelector('#save').addEventListener('click', () => {
    renderFrame();
    saveFrame();
  });
</script>

If your render loop runs continuously, place saveFrame() directly after the draw call for the frame to save. A click handler that runs later may execute after compositing, when the default drawing buffer is no longer readable as expected.

Using toDataURL()

For small images or APIs that specifically need a data URL, use toDataURL() at the same point in the render sequence. It creates a base64-encoded string, which can use substantially more memory than a Blob for large captures.

function captureDataUrl(canvas) {
  const dataUrl = canvas.toDataURL('image/png');
  if (dataUrl === 'data:,') throw new Error('Canvas export failed');
  return dataUrl;
}

// Call immediately after rendering the frame to capture.
renderFrame();
const imageDataUrl = captureDataUrl(canvas);

3. Handle drawing-buffer timing

The WebGL drawing buffer is not guaranteed to retain its contents after the browser composites the canvas. With preserveDrawingBuffer set to false, using the context as an image source after the rendering function returns can have undefined behavior. This is why an otherwise valid export can be blank or stale.

Option A: capture during the render sequence

When possible, arrange the export directly after rendering, before control returns to the browser for compositing. This avoids requesting long-term preservation of the drawing buffer. If your renderer has a callback for “frame rendered,” perform the export there rather than scheduling it in a later timer or unrelated event.

Option B: enable preservation at context creation

If capture must happen later, request buffer preservation in the initial getContext() call:

const canvas = document.querySelector('#scene');
const gl = canvas.getContext('webgl', {
  preserveDrawingBuffer: true
});
if (!gl) throw new Error('WebGL is unavailable');

// Set up shaders, buffers, and scene state, then render as usual.
renderFrame();
// A later canvas.toBlob() or canvas.toDataURL() can read the retained frame,
// unless the application clears or overwrites it.

Context attributes cannot be changed after the first successful getContext() call. If another part of the application already created the context, passing a different attribute later will not reconfigure it. The Khronos WebGL specification warns that preservation can cause significant performance loss on some platforms, so use it deliberately.

Option C: read pixels or render to an offscreen target

For a renderer you control, another design is to read pixels synchronously while the desired frame is available or render the capture into a dedicated framebuffer. This gives the application tighter control over timing and which image is exported, but requires handling pixel format, row orientation, and conversion to an image format. Use the Khronos specification’s drawing-buffer and readPixels() guidance when implementing this route.

4. Capture the visible browser output

Use a browser screenshot when the requirement is “what does the user see in this tab?” rather than “export these canvas pixels?” Browser capture can include the WebGL canvas and ordinary page content in one rendered view.

  • Browser extension: supported browser families provide APIs for capturing the visible tab. The html2canvas FAQ points to chrome.tabs.captureVisibleTab() for Chrome, Edge, and Opera. Consult the browser’s current extension documentation for permission and invocation details.
  • Automated browser: Puppeteer and Playwright can drive a real browser to render a page and save a screenshot. Wait for the application to finish initializing and drawing before capture; a page load event alone may happen before a WebGL scene is ready.

Browser capture produces a rendered screenshot of the requested browser view. It does not give your application direct control of the canvas’s pixel buffer, so it is a different choice from toBlob() or readPixels().

5. Understand DOM reconstruction and CORS limits

html2canvas produces a canvas by reconstructing a picture from DOM and style information. Its documentation cautions that the result may not match the browser’s actual rendering. Do not rely on it to faithfully reproduce WebGL output: it is not taking a native screenshot of the browser compositor.

For ordinary DOM content, html2canvas supports scale and crop options and documents a CORS option for eligible cross-origin resources. Cross-origin restrictions still matter: a canvas that has been tainted by content without appropriate CORS permission cannot be exported with standard canvas methods. Use resources served with suitable CORS headers, or capture the visible browser tab when you need the rendered result and cannot control the source.

6. Transparency, image format, and output size

  • Alpha: WebGL context alpha and premultipliedAlpha affect how transparent pixels are represented. The WebGL specification describes possible de-multiplication during toDataURL(); that conversion can be lossy. Test edges and partially transparent objects against the background where the output will be used.
  • Format: PNG is a practical default for lossless output and transparency. Browser canvas export supports PNG; JPEG is lossy and has no transparency. Actual support for requested formats can vary, so check the returned Blob type if selecting a non-default format.
  • Dimensions: Canvas export uses the canvas bitmap dimensions, not necessarily the displayed CSS size. Set the drawing buffer dimensions deliberately for the resolution you need, and account for device pixel ratio if matching a high-density display.
  • Memory: Large canvases and base64 data URLs can consume considerable memory. Use Blob output and release object URLs after downloads or display use.

7. Troubleshooting

Symptom Likely cause Fix
Blank or stale exported image Capture ran after compositing with the default non-preserved drawing buffer, or the app cleared the canvas. Capture directly after the draw call, or create the context with preserveDrawingBuffer: true when later capture is required.
Adding preservation has no effect The context already exists; attributes cannot be changed after context creation. Set the attribute on the first getContext() call and ensure no earlier code created the context.
toBlob() callback receives null The canvas could not be encoded, often because it is not in an exportable state or is origin-tainted. Check timing, confirm the canvas is valid, and ensure cross-origin resources permit access. Log the canvas dimensions and context state.
Export throws a security error Cross-origin image or texture content tainted the canvas. Serve the resource with suitable CORS headers and load it with the appropriate cross-origin setting, or use browser-tab capture for the visible result.
html2canvas output omits or misrenders WebGL The library reconstructs from DOM and styles instead of taking a browser screenshot. Export the WebGL canvas directly and composite it with other content, or use a real browser screenshot.
Image colors or transparent edges look wrong Alpha and premultiplication behavior differs from the expected output pipeline. Check context alpha settings and the target image format; compare against the intended background and avoid assuming alpha conversion is lossless.
Browser automation captures before the scene appears The page loaded before the WebGL application completed setup or its first render. Wait on an application-specific ready signal or scene selector, and verify the canvas has nonzero dimensions before taking the screenshot.

8. Performance, reliability, and cost

Direct canvas export avoids reconstructing the scene from page markup and is usually the most controlled option when you own the renderer. Its reliability depends on capturing at the right point in the render lifecycle, handling CORS, and accounting for alpha. Preserving the drawing buffer can simplify delayed reads but may cause significant performance loss on some platforms. A browser screenshot is useful when you need the rendered page, but automation adds browser startup, page readiness, and resource-loading considerations. The cited browser specifications do not establish universal timing or performance benchmarks, so measure on the browsers and devices your application supports.

For occasional local captures, browser APIs and canvas export have no per-shot service charge, though browser infrastructure and development time still have costs. For repeated captures or server-side workflows, include browser compute, retries, storage, and maintenance in the cost estimate.

Or skip the browser setup

ScreenshotNeo is a website screenshot API and MCP server. For a regular website screenshot, one GET request returns an image or PDF; this is browser-level page capture and does not replace direct WebGL canvas export when you need precise access to your application’s pixel buffer. See the ScreenshotNeo API docs.

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
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)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
const bytes = Buffer.from(await res.arrayBuffer());
await import('node:fs/promises').then(fs => fs.writeFile('shot.webp', bytes));

ScreenshotNeo removes cookie and consent banners, newsletter popups, and chat widgets before capture. Bot checks, blank pages, failed loads, and cache hits are never billed; response headers report the page verdict and billing status. Its MCP server lets AI agents take screenshots. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000 screenshots.

Sign up for ScreenshotNeo free and capture up to 1,000 screenshots a month with no card.

FAQ

Can I call preserveDrawingBuffer after the WebGL context is created?

No. It is a context creation attribute; request it on the first getContext() call.

Does a screenshot service give me the raw WebGL pixels?

A browser screenshot service returns a rendered page image. Use direct canvas export or pixel readback when you need application-controlled canvas pixels.

Should I always enable buffer preservation?

No. It can affect performance. Capture during rendering where possible, and enable preservation only when delayed reads are necessary.

Is html2canvas equivalent to a browser screenshot?

No. It reconstructs an image from DOM and style information, and its output may differ from the browser’s actual rendering.

References