Skip to content

Commit b9e0413

Browse files
vincentfretinclaude
andcommitted
Adapt reflection and screenshot components to WebGPURenderer
- 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>
1 parent af114ab commit b9e0413

3 files changed

Lines changed: 86 additions & 21 deletions

File tree

docs/components/screenshot.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,10 @@ To take a screenshot programmatically and get a canvas, call `getCanvas()`:
4343
document.querySelector('a-scene').components.screenshot.getCanvas('equirectangular');
4444
```
4545

46+
> **NOTE:** When using `THREE.WebGPURenderer`, pixels are read back from the GPU
47+
> asynchronously, so `getCanvas()` returns a `Promise` resolving to the canvas
48+
> instead of the canvas itself.
49+
4650
To take a screenshot programmatically and automatically save the file, call `capture()`:
4751

4852
```js

src/components/scene/reflection.js

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@
22
import * as THREE from 'three';
33
import { registerComponent as register } from '../../core/component.js';
44

5+
// WebGLRenderer uses WebGLCubeRenderTarget, WebGPURenderer uses CubeRenderTarget.
6+
// Written so that Webpack can't statically determine the export used;
7+
// only one of the two exists depending on the three.js build.
8+
var cubeRenderTargetImpl = ['WebGLCubeRenderTarget', 'CubeRenderTarget'].find(function (x) { return THREE[x]; });
9+
510
// source: view-source:https://storage.googleapis.com/chromium-webxr-test/r886480/proposals/lighting-estimation.html
611
function updateLights (estimate, probeLight, directionalLight, directionalLightPosition) {
712
var intensityScalar =
@@ -30,9 +35,9 @@ export var Component = register('reflection', {
3035
sceneOnly: true,
3136
init: function () {
3237
var self = this;
33-
this.cubeRenderTarget = new THREE.WebGLCubeRenderTarget(16);
38+
this.cubeRenderTarget = new THREE[cubeRenderTargetImpl](16);
3439
this.cubeCamera = new THREE.CubeCamera(0.1, 1000, this.cubeRenderTarget);
35-
this.lightingEstimationTexture = (new THREE.WebGLCubeRenderTarget(16)).texture;
40+
this.lightingEstimationTexture = (new THREE[cubeRenderTargetImpl](16)).texture;
3641
this.needsVREnvironmentUpdate = true;
3742

3843
// Update WebXR to support light-estimation

src/components/scene/screenshot.js

Lines changed: 75 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@
22
import { registerComponent } from '../../core/component.js';
33
import * as THREE from 'three';
44

5+
// WebGLRenderer uses WebGLCubeRenderTarget, WebGPURenderer uses CubeRenderTarget.
6+
// Written so that Webpack can't statically determine the export used;
7+
// only one of the two exists depending on the three.js build.
8+
var cubeRenderTargetImpl = ['WebGLCubeRenderTarget', 'CubeRenderTarget'].find(function (x) { return THREE[x]; });
9+
510
var VERTEX_SHADER = [
611
'attribute vec3 position;',
712
'attribute vec2 uv;',
@@ -57,13 +62,18 @@ export var Component = registerComponent('screenshot', {
5762
if (this.canvas) { return; }
5863
var gl = el.renderer.getContext();
5964
if (!gl) { return; }
60-
this.cubeMapSize = gl.getParameter(gl.MAX_CUBE_MAP_TEXTURE_SIZE);
61-
this.material = new THREE.RawShaderMaterial({
62-
uniforms: {map: {type: 't', value: null}},
63-
vertexShader: VERTEX_SHADER,
64-
fragmentShader: FRAGMENT_SHADER,
65-
side: THREE.DoubleSide
66-
});
65+
this.cubeMapSize = gl.getParameter ? gl.getParameter(gl.MAX_CUBE_MAP_TEXTURE_SIZE) : 2048;
66+
// WebGPURenderer does not support RawShaderMaterial, use a node material with TSL.
67+
if (THREE.TSL) {
68+
this.material = this.createNodeMaterial();
69+
} else {
70+
this.material = new THREE.RawShaderMaterial({
71+
uniforms: {map: {type: 't', value: null}},
72+
vertexShader: VERTEX_SHADER,
73+
fragmentShader: FRAGMENT_SHADER,
74+
side: THREE.DoubleSide
75+
});
76+
}
6777
this.quad = new THREE.Mesh(
6878
new THREE.PlaneGeometry(1, 1),
6979
this.material
@@ -76,6 +86,26 @@ export var Component = registerComponent('screenshot', {
7686
this.onKeyDown = this.onKeyDown.bind(this);
7787
},
7888

89+
/**
90+
* TSL (node material) equivalent of the raw GLSL shaders above, used with
91+
* WebGPURenderer which does not support RawShaderMaterial.
92+
*/
93+
createNodeMaterial: function () {
94+
var TSL = THREE.TSL;
95+
var material = new THREE.MeshBasicNodeMaterial({side: THREE.DoubleSide});
96+
var uv = TSL.uv();
97+
var longitude = TSL.float(1).sub(uv.x).mul(2 * Math.PI).sub(Math.PI).add(Math.PI / 2);
98+
var latitude = uv.y.mul(Math.PI);
99+
var dir = TSL.vec3(
100+
TSL.sin(longitude).mul(TSL.sin(latitude)).negate(),
101+
TSL.cos(latitude),
102+
TSL.cos(longitude).mul(TSL.sin(latitude)).negate()
103+
);
104+
this.cubeTextureNode = TSL.cubeTexture(new THREE.CubeTexture(), dir);
105+
material.colorNode = TSL.vec4(this.cubeTextureNode.rgb, 1);
106+
return material;
107+
},
108+
79109
getRenderTarget: function (width, height) {
80110
return new THREE.WebGLRenderTarget(width, height, {
81111
colorSpace: this.el.sceneEl.renderer.outputColorSpace,
@@ -140,10 +170,10 @@ export var Component = registerComponent('screenshot', {
140170
} else {
141171
// Use ortho camera.
142172
camera = this.camera;
143-
cubeRenderTarget = new THREE.WebGLCubeRenderTarget(
173+
cubeRenderTarget = new THREE[cubeRenderTargetImpl](
144174
Math.min(this.cubeMapSize, 2048),
145175
{
146-
format: THREE.RGBFormat,
176+
format: THREE.RGBAFormat,
147177
generateMipmaps: true,
148178
minFilter: THREE.LinearMipmapLinearFilter,
149179
colorSpace: THREE.SRGBColorSpace
@@ -155,7 +185,11 @@ export var Component = registerComponent('screenshot', {
155185
el.camera.getWorldQuaternion(cubeCamera.quaternion);
156186
// Render scene with cube camera.
157187
cubeCamera.update(el.renderer, el.object3D);
158-
this.quad.material.uniforms.map.value = cubeCamera.renderTarget.texture;
188+
if (this.cubeTextureNode) {
189+
this.cubeTextureNode.value = cubeCamera.renderTarget.texture;
190+
} else {
191+
this.quad.material.uniforms.map.value = cubeCamera.renderTarget.texture;
192+
}
159193
size = {width: this.data.width, height: this.data.height};
160194
// Use quad to project image taken by the cube camera.
161195
this.quad.visible = true;
@@ -174,19 +208,25 @@ export var Component = registerComponent('screenshot', {
174208
var isVREnabled = this.el.renderer.xr.enabled;
175209
var renderer = this.el.renderer;
176210
var params;
211+
var self = this;
177212
this.setup();
178213
// Disable VR.
179214
renderer.xr.enabled = false;
180215
params = this.setCapture(projection);
181-
this.renderCapture(params.camera, params.size, params.projection);
182-
// Trigger file download.
183-
this.saveCapture();
216+
this.renderCapture(params.camera, params.size, params.projection)
217+
.then(function () {
218+
// Trigger file download.
219+
self.saveCapture();
220+
});
184221
// Restore VR.
185222
renderer.xr.enabled = isVREnabled;
186223
},
187224

188225
/**
189226
* Return canvas instead of triggering download (e.g., for uploading blob to server).
227+
* With WebGLRenderer the canvas is returned synchronously.
228+
* With WebGPURenderer pixels are read back asynchronously, so a Promise
229+
* resolving to the canvas is returned instead.
190230
*/
191231
getCanvas: function (projection) {
192232
var isVREnabled = this.el.renderer.xr.enabled;
@@ -195,22 +235,22 @@ export var Component = registerComponent('screenshot', {
195235
// Disable VR.
196236
var params = this.setCapture(projection);
197237
renderer.xr.enabled = false;
198-
this.renderCapture(params.camera, params.size, params.projection);
238+
var promise = this.renderCapture(params.camera, params.size, params.projection);
199239
// Restore VR.
200240
renderer.xr.enabled = isVREnabled;
201-
return this.canvas;
241+
if (renderer.readRenderTargetPixels) { return this.canvas; }
242+
return promise;
202243
},
203244

204245
renderCapture: function (camera, size, projection) {
205246
var autoClear = this.el.renderer.autoClear;
206247
var el = this.el;
207-
var imageData;
208248
var output;
209249
var pixels;
210250
var renderer = el.renderer;
251+
var self = this;
211252
// Create rendering target and buffer to store the read pixels.
212253
output = this.getRenderTarget(size.width, size.height);
213-
pixels = new Uint8Array(4 * size.width * size.height);
214254
// Resize quad, camera, and canvas.
215255
this.resize(size.width, size.height);
216256
// Render scene to render target.
@@ -220,8 +260,24 @@ export var Component = registerComponent('screenshot', {
220260
renderer.render(el.object3D, camera);
221261
renderer.autoClear = autoClear;
222262
// Read image pixels back.
223-
renderer.readRenderTargetPixels(output, 0, 0, size.width, size.height, pixels);
224-
renderer.setRenderTarget(null);
263+
if (renderer.readRenderTargetPixels) {
264+
pixels = new Uint8Array(4 * size.width * size.height);
265+
renderer.readRenderTargetPixels(output, 0, 0, size.width, size.height, pixels);
266+
renderer.setRenderTarget(null);
267+
this.copyCapture(pixels, size, projection);
268+
return Promise.resolve(this.canvas);
269+
}
270+
// WebGPURenderer only supports reading pixels back asynchronously.
271+
return renderer.readRenderTargetPixelsAsync(output, 0, 0, size.width, size.height)
272+
.then(function (pixels) {
273+
renderer.setRenderTarget(null);
274+
self.copyCapture(pixels, size, projection);
275+
return self.canvas;
276+
});
277+
},
278+
279+
copyCapture: function (pixels, size, projection) {
280+
var imageData;
225281
if (projection === 'perspective') {
226282
pixels = this.flipPixelsVertically(pixels, size.width, size.height);
227283
}

0 commit comments

Comments
 (0)