Skip to content

WebGPURenderer: Make canvas optional - #34525

Draft
donmccurdy wants to merge 3 commits into
mrdoob:devfrom
donmccurdy:feat/node-webgpu-compat
Draft

WebGPURenderer: Make canvas optional#34525
donmccurdy wants to merge 3 commits into
mrdoob:devfrom
donmccurdy:feat/node-webgpu-compat

Conversation

@donmccurdy

@donmccurdy donmccurdy commented Sep 9, 2026

Copy link
Copy Markdown
Collaborator

Draft for discussion. I don't expect this can merge as a whole, but I'm hoping at least some parts of it might be OK.

In the past, if you wanted to use three.js without a browser, you'd need to use headless-gl, and a bunch of polyfills to replace browser and DOM APIs. More recently headless-gl becoming a dead end: it doesn't support WebGL 2, which three.js has now required for several years. In theory, we could be better off today, with either node-webgpu or Deno's WebGPU API.

I tried with node-webgpu, starting with a simple background color (no textures or network requests!) and ran into some small problems. Several of these look like they might be good things to fix regardless of Node.js usage, based on the "TODO: Unify CanvasTarget and OutputRenderTarget" comment already in the code?

Is it reasonable to think that if we're using WebGPU, and the user provides both device and outputRenderTarget, the canvas could become optional? That would allow running a script like this in Node.js, for example:

import { create, globals } from 'webgpu';
import { Color, PerspectiveCamera, RenderTarget, Scene, WebGPURenderer } from 'three/webgpu';

Object.assign(
  globalThis,
  globals,
  {
    self: globalThis,
    requestAnimationFrame: () => {}, // ⚠️ Prevent crash in renderer.init()
    cancelAnimationFrame: () => {}, // ⚠️ Prevent crash in renderer.dispose()
  }
);

const WIDTH = 4;
const HEIGHT = 4;

const navigator = { gpu: create( [] ) };
const adapter = await navigator.gpu.requestAdapter();
const device = await adapter.requestDevice();

const renderTarget = new RenderTarget( WIDTH, HEIGHT );
const renderer = new WebGPURenderer( { device } );
renderer.setOutputRenderTarget( renderTarget );
renderer.setAnimationLoop( null );

await renderer.init();

const scene = new Scene();
scene.background = new Color(1.0, 0.0, 0.0);

const camera = new PerspectiveCamera();

renderer.render(scene, camera);

const pixels = await renderer.readRenderTargetPixelsAsync(renderTarget, 0, 0, 1, 1);
console.log(pixels);

//
renderTarget.dispose();
renderer.dispose();
device.destroy();

With the changes proposed in this PR, the script above runs and reads back pixels from the GPU.

@github-actions

github-actions Bot commented Sep 9, 2026

Copy link
Copy Markdown

📦 Bundle size

Full ESM build, minified and gzipped.

Before After Diff
Core 389.51
102.17
389.51
102.17
+0 B
+0 B
WebGL 378.45
88.68
378.45
88.68
+0 B
+0 B
WebGPU 711.13
195.69
711.24
195.72
+109 B
+27 B
WebGPU Nodes 709.12
195.38
709.23
195.41
+109 B
+30 B

🌳 Bundle size after tree-shaking

Minimal build including a renderer, camera, empty scene, and dependencies.

Before After Diff
WebGL 523.08
125.84
523.08
125.84
+0 B
+0 B
WebGPU 765.12
205.27
765.23
205.3
+109 B
+33 B
WebGPU Nodes 711.21
191.93
711.32
191.97
+109 B
+33 B

Comment thread src/renderers/common/Renderer.js Fixed
@github-actions

github-actions Bot commented Sep 9, 2026

Copy link
Copy Markdown

🖼️ E2E screenshot tests

2 example(s) failed (full artifacts).

Example Expected Actual Diff
webgpu_texturegather
webgpu_xr_native_layers

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants