Skip to content

Adapt reflection and screenshot components to WebGPURenderer#5848

Open
vincentfretin wants to merge 3 commits into
aframevr:masterfrom
vincentfretin:webgpu-reflection-screenshot
Open

Adapt reflection and screenshot components to WebGPURenderer#5848
vincentfretin wants to merge 3 commits into
aframevr:masterfrom
vincentfretin:webgpu-reflection-screenshot

Conversation

@vincentfretin

Copy link
Copy Markdown
Contributor

Makes the reflection and screenshot components work with WebGPURenderer (three.webgpu.js build, see #5749 for context).

Changes

  • Cube render target: WebGLCubeRenderTarget is not exported by the three.webgpu.js build, which uses CubeRenderTarget instead. Both components now select the class at runtime with the same trick a-scene uses to select the renderer implementation, so webpack can't statically resolve the export. This also removes the WebGLCubeRenderTarget webpack warning when building with WEBGPU=true. THREE.CubeCamera works unchanged with either target.
  • Equirectangular projection: WebGPURenderer does not support RawShaderMaterial/ShaderMaterial (not in its node library). When THREE.TSL is available, the cubemap-to-equirectangular projection is built as a MeshBasicNodeMaterial with TSL nodes implementing the same longitude/latitude math as the GLSL shaders; the cube texture is swapped per capture through the texture node's .value.
  • Pixel readback: WebGPURenderer only supports asynchronous read back. renderCapture uses readRenderTargetPixelsAsync when readRenderTargetPixels is not available. getCanvas() still returns the canvas synchronously with WebGLRenderer (no breaking change) and returns a Promise resolving to the canvas with WebGPURenderer (documented in screenshot.md). capture() works on both.
  • RGBAFormat: the equirect cube render target used RGBFormat, which is not color-renderable with the WebGL 2 backend of WebGPURenderer (incomplete framebuffer, black capture). Switched to RGBAFormat; the projection outputs alpha 1 and the readback path already assumed 4 bytes per pixel.

Verification

Tested with a scene with distinct sky/box/ground colors on both builds (npm run dev and npm run start:webgpu) driving headless Chrome, probing pixels of the resulting canvas:

  • Perspective and equirectangular captures produce identical colors/orientation on WebGLRenderer and on WebGPURenderer (WebGL 2 fallback backend — no WebGPU adapters on Linux Chrome).
  • The reflection component sets a live cube texture as scene.environment on both renderers without errors.
  • Scene component tests and lint pass.

Note: the actual WebGPU backend readback couldn't be tested on Linux; worth a quick check of equirect orientation in a desktop Chrome with WebGPU support.

ar-hit-test.js uses the same readRenderTargetPixels pattern but is WebXR-only, so it's left untouched until WebXR support lands for WebGPURenderer.

🤖 Generated with Claude Code

- Use CubeRenderTarget with WebGPURenderer (WebGLCubeRenderTarget is not
  exported by the three.webgpu.js build), selected at runtime like the
  renderer implementation in a-scene. This also removes a webpack warning
  when building with WEBGPU=true.
- WebGPURenderer does not support RawShaderMaterial; the equirectangular
  cubemap-to-equirect projection is implemented with TSL nodes when
  THREE.TSL is available.
- WebGPURenderer only supports asynchronous pixels read back, use
  readRenderTargetPixelsAsync when readRenderTargetPixels is not
  available. getCanvas() returns a Promise resolving to the canvas with
  WebGPURenderer; it still returns the canvas synchronously with
  WebGLRenderer.
- Use RGBAFormat instead of RGBFormat for the cube render target;
  RGBFormat is not color-renderable with the WebGL 2 backend of
  WebGPURenderer.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@vincentfretin

Copy link
Copy Markdown
Contributor Author

Testing help wanted: on this machine I could only verify WebGPURenderer running on its WebGL 2 fallback backend (Chrome on Linux has no WebGPU adapters). If someone has a machine with WebGPU support (chrome://gpu shows WebGPU: Hardware accelerated), could you test this PR with the actual WebGPU backend?

gh pr checkout 5848
# or without the GitHub CLI:
# git fetch https://github.com/vincentfretin/aframe.git webgpu-reflection-screenshot && git checkout FETCH_HEAD
npm ci
npm run dist
npm run start:webgpu

Then open https://localhost:8080/examples/ (accept the self-signed certificate), pick any example and check in the devtools console that the WebGPU backend is really active:

AFRAME.scenes[0].renderer.backend.isWebGPUBackend // should be true

Things to verify:

  • <ctrl> + <alt> + s: perspective screenshot downloads, correct content, not upside down.
  • <ctrl> + <alt> + <shift> + s: equirectangular screenshot, correct content and orientation. This exercises the new TSL projection material and the async pixels read back, and is the part I couldn't fully verify: the readback orientation could differ between the WebGPU backend and the WebGL 2 fallback backend.
  • Reflection: run document.querySelector('a-scene').setAttribute('reflection', '') in the console, check there is no error and AFRAME.scenes[0].object3D.environment is set.

Comment thread src/components/scene/screenshot.js Outdated
@@ -57,13 +62,18 @@ export var Component = registerComponent('screenshot', {
if (this.canvas) { return; }
var gl = el.renderer.getContext();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nitpick: the name gl is no longer accurate as it might be a GPUCanvasContext. Maybe ctx would be an apt alternative covering both?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Renamed to ctx in 2a9ab7d.

Comment thread src/components/scene/screenshot.js Outdated
fragmentShader: FRAGMENT_SHADER,
side: THREE.DoubleSide
});
this.cubeMapSize = gl.getParameter ? gl.getParameter(gl.MAX_CUBE_MAP_TEXTURE_SIZE) : 2048;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wasn't immediately clear why 2048 would be a safe, though it appears that maxTextureDimension2D (which applies for cube map faces AFAICT) is at least 8192 and 4096 in the recent compatibility mode. Since the code later on clamps it to 2048 either way, it's fine using that value.

Ideally we'd query maxTextureDimension2D, or at the very least add a comment describing why this is safe to do.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 2a9ab7d: it now queries maxTextureDimension2D from the device limits (with a 2048 fallback if the device is somehow not available yet).

Comment thread src/components/scene/screenshot.js Outdated
Comment on lines +188 to +192
if (this.cubeTextureNode) {
this.cubeTextureNode.value = cubeCamera.renderTarget.texture;
} else {
this.quad.material.uniforms.map.value = cubeCamera.renderTarget.texture;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since both the uniform and TextureNode have the texture in a .value property, we can store this.cubeTextureUniform for both cases (TSL and ShaderMaterial). In fact TSL.uniformTexture is the same as TSL.texture so should be fine to change cubeTextureNode to cubeTextureUniform semantically.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice, done in 2a9ab7d: this.cubeTextureUniform is now set in both branches (the map uniform for ShaderMaterial and the cubeTexture node for TSL) and setCapture has a single .value assignment.

@mrxz

mrxz commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

Testing help wanted: on this machine I could only verify WebGPURenderer running on its WebGL 2 fallback backend (Chrome on Linux has no WebGPU adapters). If someone has a machine with WebGPU support (chrome://gpu shows WebGPU: Hardware accelerated), could you test this PR with the actual WebGPU backend?

It should be possible to enable WebGPU on Linux on most systems, see: https://github.com/gpuweb/gpuweb/wiki/Implementation-Status#chromium-chrome-edge-etc

In any case, a quick test shows that both the perspective and equirectangular results are upside down when using WebGPU. Also, the screenshot shortcuts no longer seem to work, but this appears to be a regression introduced with #5442. Essentially the onKeyDown is no longer binding this by the time play is called, since the entire init is renamed to setup and deferred. Programmatically triggering the captures does still work, which I've used.

Edit: opened a PR with a fix: #5849

  • Reflection: run document.querySelector('a-scene').setAttribute('reflection', '') in the console, check there is no error and AFRAME.scenes[0].object3D.environment is set.

The environment property appears to be set correctly. At first blush the result looks correct visually as well, but haven't tested/compared extensively.

@vincentfretin

Copy link
Copy Markdown
Contributor Author

Oh I completely missed that it was working now on Linux behind additional cli flags, thank you!
I got webpgu working on Ubuntu 22.04, AMD Ryzen 5 5600X, nvidia driver 610.

cp /usr/share/applications/google-chrome.desktop ~/.local/share/applications/
edit ~/.local/share/applications/google-chrome.desktop and change Exec to:
Exec=/usr/bin/google-chrome-stable --enable-unsafe-webgpu --ozone-platform=x11 --use-angle=vulkan --enable-features=Vulkan,VulkanFromANGLE %U
update-desktop-database ~/.local/share/applications/

…mments

- Pixels read back from WebGL are bottom-up but top-down with the WebGPU
  backend of WebGPURenderer, so the vertical flip is now based on the
  renderer coordinate system. The equirectangular projection quad renders
  vertically inverted so it needs the opposite flip of the perspective
  projection. Verified on an actual WebGPU backend (NVIDIA, Vulkan), on
  the WebGL 2 fallback backend and on WebGLRenderer.
- Query maxTextureDimension2D from the WebGPU device limits for
  cubeMapSize instead of hardcoding 2048.
- Rename gl to ctx in setup() since it can be a GPUCanvasContext.
- Store this.cubeTextureUniform for both the ShaderMaterial uniform and
  the TSL cubeTexture node, both expose the texture as a value property.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@vincentfretin

Copy link
Copy Markdown
Contributor Author

@mrxz Thanks a lot for testing and for the Linux flags pointer! I got the actual WebGPU backend working locally (NVIDIA, Vulkan) with --enable-unsafe-webgpu --ozone-platform=x11 --use-angle=vulkan --enable-features=Vulkan,VulkanFromANGLE and could reproduce and fix the upside down captures in 2a9ab7d.

The cause: pixels read back from WebGL are bottom-up but top-down with the WebGPU backend, and the equirectangular projection quad renders vertically inverted so it needs the opposite flip of the perspective projection. The vertical flip is now based on renderer.coordinateSystem. Note it depends on the backend, not the renderer class: WebGPURenderer on its WebGL 2 fallback backend reads back bottom-up like WebGLRenderer, which is why my initial fallback-only testing looked correct.

Verified on the three configurations by probing pixels of the resulting canvas on a scene with distinct sky/box/ground colors:

Configuration perspective equirectangular reflection
WebGPU backend (NVIDIA/Vulkan)
WebGL 2 fallback backend
WebGLRenderer ✅ (sync) ✅ (sync)

Review comments also addressed in the same commit. And thanks for tracking down the keyboard shortcut regression separately in #5849.

TSL and MeshBasicNodeMaterial only exist in the three.js build with
WebGPURenderer, access them so that Webpack can't statically determine
the export used, like for CubeRenderTarget.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@vincentfretin vincentfretin force-pushed the webgpu-reflection-screenshot branch from ab0734d to f445281 Compare July 5, 2026 16:13
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