Skip to content

Commit 7260beb

Browse files
committed
Add support for WebGPU on top of WebXR
1 parent 04b1f83 commit 7260beb

File tree

9 files changed

+626
-95
lines changed

9 files changed

+626
-95
lines changed

src/core/RenderTarget.js

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,8 @@ class RenderTarget extends EventDispatcher {
3636
* @property {number} [samples=0] - The MSAA samples count.
3737
* @property {number} [count=1] - Defines the number of color attachments . Must be at least `1`.
3838
* @property {number} [depth=1] - The texture depth.
39-
* @property {boolean} [multiview=false] - Whether this target is used for multiview rendering.
39+
* @property {boolean} [multiview=false] - Whether this target is used for multiview rendering (WebGL OVR_multiview2 extension).
40+
* @property {boolean} [useArrayDepthTexture=false] - Whether to create the depth texture as an array texture for per-layer depth testing. This is separate from multiview to allow array depth textures without the multiview extension (e.g., for WebGPU XR).
4041
*/
4142

4243
/**
@@ -62,7 +63,8 @@ class RenderTarget extends EventDispatcher {
6263
samples: 0,
6364
count: 1,
6465
depth: 1,
65-
multiview: false
66+
multiview: false,
67+
useArrayDepthTexture: false
6668
}, options );
6769

6870
/**
@@ -200,6 +202,16 @@ class RenderTarget extends EventDispatcher {
200202
*/
201203
this.multiview = options.multiview;
202204

205+
/**
206+
* Whether to create the depth texture as an array texture for per-layer depth testing.
207+
* This is separate from multiview to allow array depth textures without the multiview
208+
* extension (e.g., for WebGPU XR which doesn't use OVR_multiview2).
209+
*
210+
* @type {boolean}
211+
* @default false
212+
*/
213+
this.useArrayDepthTexture = options.useArrayDepthTexture;
214+
203215
}
204216

205217
_setTextureOptions( options = {} ) {
@@ -370,6 +382,8 @@ class RenderTarget extends EventDispatcher {
370382
if ( source.depthTexture !== null ) this.depthTexture = source.depthTexture.clone();
371383

372384
this.samples = source.samples;
385+
this.multiview = source.multiview;
386+
this.useArrayDepthTexture = source.useArrayDepthTexture;
373387

374388
return this;
375389

src/renderers/common/Renderer.js

Lines changed: 127 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1338,9 +1338,32 @@ class Renderer {
13381338
frameBufferTarget.scissor.multiplyScalar( canvasTarget._pixelRatio );
13391339
frameBufferTarget.scissorTest = canvasTarget._scissorTest;
13401340
frameBufferTarget.multiview = outputRenderTarget !== null ? outputRenderTarget.multiview : false;
1341+
frameBufferTarget.useArrayDepthTexture = outputRenderTarget !== null ? outputRenderTarget.useArrayDepthTexture : false;
13411342
frameBufferTarget.resolveDepthBuffer = outputRenderTarget !== null ? outputRenderTarget.resolveDepthBuffer : true;
13421343
frameBufferTarget._autoAllocateDepthBuffer = outputRenderTarget !== null ? outputRenderTarget._autoAllocateDepthBuffer : false;
13431344

1345+
// Propagate samples from output render target (important for XR which may have samples=0)
1346+
// However, when the renderer has MSAA enabled (this.samples > 0), use the renderer's samples
1347+
// to ensure MSAA is applied during scene rendering. The MSAA resolve happens when copying to output.
1348+
if ( outputRenderTarget !== null ) {
1349+
1350+
frameBufferTarget.samples = this.samples > 0 ? this.samples : outputRenderTarget.samples;
1351+
1352+
}
1353+
1354+
// Propagate array texture flag for XR/multi-layer rendering
1355+
// IMPORTANT: When MSAA is enabled (this.samples > 0), we must NOT use array textures because
1356+
// WebGPU doesn't support multisampled array textures. Per-eye rendering will be used instead.
1357+
if ( outputRenderTarget !== null && outputRenderTarget.texture.isArrayTexture && this.samples === 0 ) {
1358+
1359+
frameBufferTarget.texture.isArrayTexture = true;
1360+
1361+
} else {
1362+
1363+
frameBufferTarget.texture.isArrayTexture = false;
1364+
1365+
}
1366+
13441367
return frameBufferTarget;
13451368

13461369
}
@@ -1454,6 +1477,76 @@ class Renderer {
14541477
if ( xr.cameraAutoUpdate === true ) xr.updateCamera( camera );
14551478
camera = xr.getCamera(); // use XR camera for rendering
14561479

1480+
// For XR with MSAA and ArrayCamera, we need to render each eye separately
1481+
// because MSAA textures cannot be array textures in WebGPU.
1482+
// Flow: left eye scene → left tone mapping → right eye scene → right tone mapping
1483+
if ( camera.isArrayCamera && this.samples > 0 && frameBufferTarget !== null ) {
1484+
1485+
const xrCameras = camera.cameras;
1486+
const xrOutputRenderTarget = outputRenderTarget;
1487+
1488+
// Render each eye separately using the EXISTING render infrastructure
1489+
for ( let eyeIndex = 0; eyeIndex < xrCameras.length; eyeIndex ++ ) {
1490+
1491+
const eyeCamera = xrCameras[ eyeIndex ];
1492+
1493+
// Temporarily disable XR so _renderScene uses the single-layer framebuffer normally
1494+
xr.enabled = false;
1495+
1496+
// For scene rendering, output goes to the single-layer MSAA framebuffer
1497+
// (not directly to XR output - that happens in the tone mapping step)
1498+
this.setRenderTarget( frameBufferTarget, 0, activeMipmapLevel );
1499+
1500+
// Call the existing _renderScene to render this eye's scene
1501+
// This goes through the full initialization path
1502+
this._renderScene( scene, eyeCamera, false );
1503+
1504+
// Re-enable XR temporarily for proper output handling
1505+
xr.enabled = true;
1506+
1507+
// Now output the rendered frame to the appropriate XR layer
1508+
this.setRenderTarget( xrOutputRenderTarget, eyeIndex, activeMipmapLevel );
1509+
this._activeCubeFace = eyeIndex;
1510+
1511+
// Render the output quad (tone mapping) to copy to XR layer
1512+
const quad = this._quad;
1513+
1514+
if ( this._nodes.hasOutputChange( frameBufferTarget.texture ) ) {
1515+
1516+
quad.material.fragmentNode = this._nodes.getOutputNode( frameBufferTarget.texture );
1517+
quad.material.needsUpdate = true;
1518+
1519+
}
1520+
1521+
// Disable XR again for the quad render to avoid ArrayCamera handling
1522+
xr.enabled = false;
1523+
const savedAutoClear = this.autoClear;
1524+
this.autoClear = false;
1525+
1526+
this._renderScene( quad, quad.camera, false );
1527+
1528+
this.autoClear = savedAutoClear;
1529+
1530+
}
1531+
1532+
// Restore XR state
1533+
xr.enabled = true;
1534+
1535+
// Clean up and return
1536+
nodeFrame.renderId = previousRenderId;
1537+
this._currentRenderContext = previousRenderContext;
1538+
this._currentRenderObjectFunction = previousRenderObjectFunction;
1539+
this._handleObjectFunction = previousHandleObjectFunction;
1540+
1541+
this._callDepth --;
1542+
1543+
sceneRef.onAfterRender( this, scene, camera, renderTarget );
1544+
this.inspector.finishRender( null );
1545+
1546+
return null;
1547+
1548+
}
1549+
14571550
}
14581551

14591552
//
@@ -1668,7 +1761,34 @@ class Renderer {
16681761
this.autoClear = false;
16691762
this.xr.enabled = false;
16701763

1671-
this._renderScene( quad, quad.camera, false );
1764+
// For XR array textures, we need to render the output quad once per layer
1765+
// Each layer needs to sample from the correct array slice and write to the correct output layer
1766+
if ( renderTarget.texture.isArrayTexture && renderTarget.texture.image.depth > 1 ) {
1767+
1768+
const layerCount = renderTarget.texture.image.depth;
1769+
1770+
for ( let layer = 0; layer < layerCount; layer ++ ) {
1771+
1772+
// Set the layer index for sampling from the array texture
1773+
// This updates a renderGroup uniform that's synced before each draw
1774+
this._nodes.setOutputLayerIndex( layer );
1775+
1776+
// Set the active layer for the render target
1777+
this._activeCubeFace = layer;
1778+
1779+
this._renderScene( quad, quad.camera, false );
1780+
1781+
}
1782+
1783+
// Reset the layer index
1784+
this._nodes.setOutputLayerIndex( 0 );
1785+
this._activeCubeFace = 0;
1786+
1787+
} else {
1788+
1789+
this._renderScene( quad, quad.camera, false );
1790+
1791+
}
16721792

16731793
this.autoClear = currentAutoClear;
16741794
this.xr.enabled = currentXR;
@@ -2411,7 +2531,12 @@ class Renderer {
24112531
*/
24122532
_resetXRState() {
24132533

2414-
this.backend.setXRTarget( null );
2534+
if ( this.backend.isWebGPUBackend !== true ) {
2535+
2536+
this.backend.setXRTarget( null );
2537+
2538+
}
2539+
24152540
this.setOutputRenderTarget( null );
24162541
this.setRenderTarget( null );
24172542

src/renderers/common/Textures.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,13 @@ class Textures extends DataMap {
8686
depthTexture.image.height = mipHeight;
8787
depthTexture.image.depth = size.depth;
8888
depthTexture.renderTarget = renderTarget;
89-
depthTexture.isArrayTexture = renderTarget.multiview === true && size.depth > 1;
89+
90+
// Determine if depth texture should be an array texture:
91+
// - Either useArrayDepthTexture is explicitly set, OR multiview is enabled (for backward compat)
92+
// - AND there are multiple layers (depth > 1)
93+
// - AND MSAA is not enabled (WebGPU doesn't support multisampled depth array textures)
94+
const useArrayDepth = renderTarget.useArrayDepthTexture || renderTarget.multiview;
95+
depthTexture.isArrayTexture = size.depth > 1 && renderTarget.samples <= 1 && useArrayDepth;
9096

9197
depthTextureMips[ activeMipmapLevel ] = depthTexture;
9298

0 commit comments

Comments
 (0)