Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 16 additions & 2 deletions src/core/RenderTarget.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@ class RenderTarget extends EventDispatcher {
* @property {number} [samples=0] - The MSAA samples count.
* @property {number} [count=1] - Defines the number of color attachments . Must be at least `1`.
* @property {number} [depth=1] - The texture depth.
* @property {boolean} [multiview=false] - Whether this target is used for multiview rendering.
* @property {boolean} [multiview=false] - Whether this target is used for multiview rendering (WebGL OVR_multiview2 extension).
* @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).
*/

/**
Expand All @@ -62,7 +63,8 @@ class RenderTarget extends EventDispatcher {
samples: 0,
count: 1,
depth: 1,
multiview: false
multiview: false,
useArrayDepthTexture: false
}, options );

/**
Expand Down Expand Up @@ -200,6 +202,16 @@ class RenderTarget extends EventDispatcher {
*/
this.multiview = options.multiview;

/**
* 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 which doesn't use OVR_multiview2).
*
* @type {boolean}
* @default false
*/
this.useArrayDepthTexture = options.useArrayDepthTexture;

}

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

this.samples = source.samples;
this.multiview = source.multiview;
this.useArrayDepthTexture = source.useArrayDepthTexture;

return this;

Expand Down
129 changes: 127 additions & 2 deletions src/renderers/common/Renderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -1338,9 +1338,32 @@ class Renderer {
frameBufferTarget.scissor.multiplyScalar( canvasTarget._pixelRatio );
frameBufferTarget.scissorTest = canvasTarget._scissorTest;
frameBufferTarget.multiview = outputRenderTarget !== null ? outputRenderTarget.multiview : false;
frameBufferTarget.useArrayDepthTexture = outputRenderTarget !== null ? outputRenderTarget.useArrayDepthTexture : false;
frameBufferTarget.resolveDepthBuffer = outputRenderTarget !== null ? outputRenderTarget.resolveDepthBuffer : true;
frameBufferTarget._autoAllocateDepthBuffer = outputRenderTarget !== null ? outputRenderTarget._autoAllocateDepthBuffer : false;

// Propagate samples from output render target (important for XR which may have samples=0)
// However, when the renderer has MSAA enabled (this.samples > 0), use the renderer's samples
// to ensure MSAA is applied during scene rendering. The MSAA resolve happens when copying to output.
if ( outputRenderTarget !== null ) {

frameBufferTarget.samples = this.samples > 0 ? this.samples : outputRenderTarget.samples;

}

// Propagate array texture flag for XR/multi-layer rendering
// IMPORTANT: When MSAA is enabled (this.samples > 0), we must NOT use array textures because
// WebGPU doesn't support multisampled array textures. Per-eye rendering will be used instead.
if ( outputRenderTarget !== null && outputRenderTarget.texture.isArrayTexture && this.samples === 0 ) {

frameBufferTarget.texture.isArrayTexture = true;

} else {

frameBufferTarget.texture.isArrayTexture = false;

}

return frameBufferTarget;

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

// For XR with MSAA and ArrayCamera, we need to render each eye separately
// because MSAA textures cannot be array textures in WebGPU.
// Flow: left eye scene → left tone mapping → right eye scene → right tone mapping
if ( camera.isArrayCamera && this.samples > 0 && frameBufferTarget !== null ) {

const xrCameras = camera.cameras;
const xrOutputRenderTarget = outputRenderTarget;

// Render each eye separately using the EXISTING render infrastructure
for ( let eyeIndex = 0; eyeIndex < xrCameras.length; eyeIndex ++ ) {

const eyeCamera = xrCameras[ eyeIndex ];

// Temporarily disable XR so _renderScene uses the single-layer framebuffer normally
xr.enabled = false;

// For scene rendering, output goes to the single-layer MSAA framebuffer
// (not directly to XR output - that happens in the tone mapping step)
this.setRenderTarget( frameBufferTarget, 0, activeMipmapLevel );

// Call the existing _renderScene to render this eye's scene
// This goes through the full initialization path
this._renderScene( scene, eyeCamera, false );

// Re-enable XR temporarily for proper output handling
xr.enabled = true;

// Now output the rendered frame to the appropriate XR layer
this.setRenderTarget( xrOutputRenderTarget, eyeIndex, activeMipmapLevel );
this._activeCubeFace = eyeIndex;

// Render the output quad (tone mapping) to copy to XR layer
const quad = this._quad;

if ( this._nodes.hasOutputChange( frameBufferTarget.texture ) ) {

quad.material.fragmentNode = this._nodes.getOutputNode( frameBufferTarget.texture );
quad.material.needsUpdate = true;

}

// Disable XR again for the quad render to avoid ArrayCamera handling
xr.enabled = false;
const savedAutoClear = this.autoClear;
this.autoClear = false;

this._renderScene( quad, quad.camera, false );

this.autoClear = savedAutoClear;

}

// Restore XR state
xr.enabled = true;

// Clean up and return
nodeFrame.renderId = previousRenderId;
this._currentRenderContext = previousRenderContext;
this._currentRenderObjectFunction = previousRenderObjectFunction;
this._handleObjectFunction = previousHandleObjectFunction;

this._callDepth --;

sceneRef.onAfterRender( this, scene, camera, renderTarget );
this.inspector.finishRender( null );

return null;

}

}

//
Expand Down Expand Up @@ -1668,7 +1761,34 @@ class Renderer {
this.autoClear = false;
this.xr.enabled = false;

this._renderScene( quad, quad.camera, false );
// For XR array textures, we need to render the output quad once per layer
// Each layer needs to sample from the correct array slice and write to the correct output layer
if ( renderTarget.texture.isArrayTexture && renderTarget.texture.image.depth > 1 ) {

const layerCount = renderTarget.texture.image.depth;

for ( let layer = 0; layer < layerCount; layer ++ ) {

// Set the layer index for sampling from the array texture
// This updates a renderGroup uniform that's synced before each draw
this._nodes.setOutputLayerIndex( layer );

// Set the active layer for the render target
this._activeCubeFace = layer;

this._renderScene( quad, quad.camera, false );

}

// Reset the layer index
this._nodes.setOutputLayerIndex( 0 );
this._activeCubeFace = 0;

} else {

this._renderScene( quad, quad.camera, false );

}

this.autoClear = currentAutoClear;
this.xr.enabled = currentXR;
Expand Down Expand Up @@ -2411,7 +2531,12 @@ class Renderer {
*/
_resetXRState() {

this.backend.setXRTarget( null );
if ( this.backend.isWebGPUBackend !== true ) {

this.backend.setXRTarget( null );

}

this.setOutputRenderTarget( null );
this.setRenderTarget( null );

Expand Down
8 changes: 7 additions & 1 deletion src/renderers/common/Textures.js
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,13 @@ class Textures extends DataMap {
depthTexture.image.height = mipHeight;
depthTexture.image.depth = size.depth;
depthTexture.renderTarget = renderTarget;
depthTexture.isArrayTexture = renderTarget.multiview === true && size.depth > 1;

// Determine if depth texture should be an array texture:
// - Either useArrayDepthTexture is explicitly set, OR multiview is enabled (for backward compat)
// - AND there are multiple layers (depth > 1)
// - AND MSAA is not enabled (WebGPU doesn't support multisampled depth array textures)
const useArrayDepth = renderTarget.useArrayDepthTexture || renderTarget.multiview;
depthTexture.isArrayTexture = size.depth > 1 && renderTarget.samples <= 1 && useArrayDepth;

depthTextureMips[ activeMipmapLevel ] = depthTexture;

Expand Down
Loading