From 87cd44ad94df6984ff9bf5ae9195cd0d0e2e094e Mon Sep 17 00:00:00 2001 From: sunag Date: Fri, 19 Sep 2025 10:36:48 -0300 Subject: [PATCH 1/7] Introduce `CanvasTarget` --- src/Three.WebGPU.Nodes.js | 1 + src/Three.WebGPU.js | 1 + src/renderers/common/CanvasTarget.js | 402 ++++++++++++++++++ src/renderers/common/Renderer.js | 242 +++++------ src/renderers/webgpu/WebGPUBackend.js | 87 ++-- .../webgpu/utils/WebGPUTextureUtils.js | 40 +- 6 files changed, 573 insertions(+), 200 deletions(-) create mode 100644 src/renderers/common/CanvasTarget.js diff --git a/src/Three.WebGPU.Nodes.js b/src/Three.WebGPU.Nodes.js index 6a9d0983296cd2..28e9bd37b53bba 100644 --- a/src/Three.WebGPU.Nodes.js +++ b/src/Three.WebGPU.Nodes.js @@ -19,6 +19,7 @@ export { default as NodeLoader } from './loaders/nodes/NodeLoader.js'; export { default as NodeObjectLoader } from './loaders/nodes/NodeObjectLoader.js'; export { default as NodeMaterialLoader } from './loaders/nodes/NodeMaterialLoader.js'; export { default as InspectorBase } from './renderers/common/InspectorBase.js'; +export { default as CanvasTarget } from './renderers/common/CanvasTarget.js'; export { ClippingGroup } from './objects/ClippingGroup.js'; export * from './nodes/Nodes.js'; import * as TSL from './nodes/TSL.js'; diff --git a/src/Three.WebGPU.js b/src/Three.WebGPU.js index 59b436436206f9..25a6f220ffa129 100644 --- a/src/Three.WebGPU.js +++ b/src/Three.WebGPU.js @@ -21,6 +21,7 @@ export { default as NodeLoader } from './loaders/nodes/NodeLoader.js'; export { default as NodeObjectLoader } from './loaders/nodes/NodeObjectLoader.js'; export { default as NodeMaterialLoader } from './loaders/nodes/NodeMaterialLoader.js'; export { default as InspectorBase } from './renderers/common/InspectorBase.js'; +export { default as CanvasTarget } from './renderers/common/CanvasTarget.js'; export { ClippingGroup } from './objects/ClippingGroup.js'; export * from './nodes/Nodes.js'; import * as TSL from './nodes/TSL.js'; diff --git a/src/renderers/common/CanvasTarget.js b/src/renderers/common/CanvasTarget.js new file mode 100644 index 00000000000000..05c27c367c3f41 --- /dev/null +++ b/src/renderers/common/CanvasTarget.js @@ -0,0 +1,402 @@ +import { EventDispatcher } from '../../core/EventDispatcher.js'; +import { Vector4 } from '../../math/Vector4.js'; +import { FramebufferTexture } from '../../textures/FramebufferTexture.js'; +import { DepthTexture } from '../../textures/DepthTexture.js'; +import { NoToneMapping, SRGBColorSpace } from '../../constants.js'; + +/** + * CanvasTarget is a class that represents the final output destination of the renderer. + * + * @augments EventDispatcher + */ +class CanvasTarget extends EventDispatcher { + + /** + * CanvasTarget options. + * + * @typedef {Object} CanvasTarget~Options + * @property {boolean} [antialias=false] - Whether MSAA as the default anti-aliasing should be enabled or not. + * @property {number} [samples=0] - When `antialias` is `true`, `4` samples are used by default. This parameter can set to any other integer value than 0 + * to overwrite the default. + */ + + /** + * Constructs a new CanvasTarget. + * + * @param {HTMLCanvasElement|OffscreenCanvas} domElement - The canvas element to render to. + * @param {Object} [parameters={}] - The parameters. + */ + constructor( domElement, parameters = {} ) { + + super(); + + const { + antialias = false, + samples = 0 + } = parameters; + + /** + * A reference to the canvas element the renderer is drawing to. + * This value of this property will automatically be created by + * the renderer. + * + * @type {HTMLCanvasElement|OffscreenCanvas} + */ + this.domElement = domElement; + + /** + * Defines the output color space of the renderer. + * + * @type {string} + * @default SRGBColorSpace + */ + this.outputColorSpace = SRGBColorSpace; + + /** + * Defines the tone mapping of the renderer. + * + * @type {number} + * @default NoToneMapping + */ + this.toneMapping = NoToneMapping; + + /** + * Defines the tone mapping exposure. + * + * @type {number} + * @default 1 + */ + this.toneMappingExposure = 1.0; + + /** + * The renderer's pixel ratio. + * + * @private + * @type {number} + * @default 1 + */ + this._pixelRatio = 1; + + /** + * The width of the renderer's default framebuffer in logical pixel unit. + * + * @private + * @type {number} + */ + this._width = this.domElement.width; + + /** + * The height of the renderer's default framebuffer in logical pixel unit. + * + * @private + * @type {number} + */ + this._height = this.domElement.height; + + /** + * The viewport of the renderer in logical pixel unit. + * + * @private + * @type {Vector4} + */ + this._viewport = new Vector4( 0, 0, this._width, this._height ); + + /** + * The scissor rectangle of the renderer in logical pixel unit. + * + * @private + * @type {Vector4} + */ + this._scissor = new Vector4( 0, 0, this._width, this._height ); + + /** + * Whether the scissor test should be enabled or not. + * + * @private + * @type {boolean} + */ + this._scissorTest = false; + + /** + * The number of MSAA samples. + * + * @private + * @type {number} + * @default 0 + */ + this._samples = samples || ( antialias === true ) ? 4 : 0; + + /** + * The color texture of the default framebuffer. + * + * @type {FramebufferTexture} + */ + this.colorTexture = new FramebufferTexture(); + + /** + * The depth texture of the default framebuffer. + * + * @type {DepthTexture} + */ + this.depthTexture = new DepthTexture(); + + } + + /** + * The number of samples used for multi-sample anti-aliasing (MSAA). + * + * @type {number} + * @default 0 + */ + get samples() { + + return this._samples; + + } + + /** + * Returns the pixel ratio. + * + * @return {number} The pixel ratio. + */ + getPixelRatio() { + + return this._pixelRatio; + + } + + /** + * Returns the drawing buffer size in physical pixels. This method honors the pixel ratio. + * + * @param {Vector2} target - The method writes the result in this target object. + * @return {Vector2} The drawing buffer size. + */ + getDrawingBufferSize( target ) { + + return target.set( this._width * this._pixelRatio, this._height * this._pixelRatio ).floor(); + + } + + /** + * Returns the renderer's size in logical pixels. This method does not honor the pixel ratio. + * + * @param {Vector2} target - The method writes the result in this target object. + * @return {Vector2} The renderer's size in logical pixels. + */ + getSize( target ) { + + return target.set( this._width, this._height ); + + } + + /** + * Sets the given pixel ratio and resizes the canvas if necessary. + * + * @param {number} [value=1] - The pixel ratio. + */ + setPixelRatio( value = 1 ) { + + if ( this._pixelRatio === value ) return; + + this._pixelRatio = value; + + this.setSize( this._width, this._height, false ); + + } + + /** + * This method allows to define the drawing buffer size by specifying + * width, height and pixel ratio all at once. The size of the drawing + * buffer is computed with this formula: + * ```js + * size.x = width * pixelRatio; + * size.y = height * pixelRatio; + * ``` + * + * @param {number} width - The width in logical pixels. + * @param {number} height - The height in logical pixels. + * @param {number} pixelRatio - The pixel ratio. + */ + setDrawingBufferSize( width, height, pixelRatio ) { + + // Renderer can't be resized while presenting in XR. + if ( this.xr && this.xr.isPresenting ) return; + + this._width = width; + this._height = height; + + this._pixelRatio = pixelRatio; + + this.domElement.width = Math.floor( width * pixelRatio ); + this.domElement.height = Math.floor( height * pixelRatio ); + + this.setViewport( 0, 0, width, height ); + + this._dispatchResize(); + + } + + /** + * Sets the size of the renderer. + * + * @param {number} width - The width in logical pixels. + * @param {number} height - The height in logical pixels. + * @param {boolean} [updateStyle=true] - Whether to update the `style` attribute of the canvas or not. + */ + setSize( width, height, updateStyle = true ) { + + // Renderer can't be resized while presenting in XR. + if ( this.xr && this.xr.isPresenting ) return; + + this._width = width; + this._height = height; + + this.domElement.width = Math.floor( width * this._pixelRatio ); + this.domElement.height = Math.floor( height * this._pixelRatio ); + + if ( updateStyle === true ) { + + this.domElement.style.width = width + 'px'; + this.domElement.style.height = height + 'px'; + + } + + this.setViewport( 0, 0, width, height ); + + this._dispatchResize(); + + } + + /** + * Returns the scissor rectangle. + * + * @param {Vector4} target - The method writes the result in this target object. + * @return {Vector4} The scissor rectangle. + */ + getScissor( target ) { + + const scissor = this._scissor; + + target.x = scissor.x; + target.y = scissor.y; + target.width = scissor.width; + target.height = scissor.height; + + return target; + + } + + /** + * Defines the scissor rectangle. + * + * @param {number | Vector4} x - The horizontal coordinate for the lower left corner of the box in logical pixel unit. + * Instead of passing four arguments, the method also works with a single four-dimensional vector. + * @param {number} y - The vertical coordinate for the lower left corner of the box in logical pixel unit. + * @param {number} width - The width of the scissor box in logical pixel unit. + * @param {number} height - The height of the scissor box in logical pixel unit. + */ + setScissor( x, y, width, height ) { + + const scissor = this._scissor; + + if ( x.isVector4 ) { + + scissor.copy( x ); + + } else { + + scissor.set( x, y, width, height ); + + } + + } + + /** + * Returns the scissor test value. + * + * @return {boolean} Whether the scissor test should be enabled or not. + */ + getScissorTest() { + + return this._scissorTest; + + } + + /** + * Defines the scissor test. + * + * @param {boolean} boolean - Whether the scissor test should be enabled or not. + */ + setScissorTest( boolean ) { + + this._scissorTest = boolean; + + } + + /** + * Returns the viewport definition. + * + * @param {Vector4} target - The method writes the result in this target object. + * @return {Vector4} The viewport definition. + */ + getViewport( target ) { + + return target.copy( this._viewport ); + + } + + /** + * Defines the viewport. + * + * @param {number | Vector4} x - The horizontal coordinate for the lower left corner of the viewport origin in logical pixel unit. + * @param {number} y - The vertical coordinate for the lower left corner of the viewport origin in logical pixel unit. + * @param {number} width - The width of the viewport in logical pixel unit. + * @param {number} height - The height of the viewport in logical pixel unit. + * @param {number} minDepth - The minimum depth value of the viewport. WebGPU only. + * @param {number} maxDepth - The maximum depth value of the viewport. WebGPU only. + */ + setViewport( x, y, width, height, minDepth = 0, maxDepth = 1 ) { + + const viewport = this._viewport; + + if ( x.isVector4 ) { + + viewport.copy( x ); + + } else { + + viewport.set( x, y, width, height ); + + } + + viewport.minDepth = minDepth; + viewport.maxDepth = maxDepth; + + } + + /** + * Dispatches the resize event. + * + * @private + */ + _dispatchResize() { + + this.dispatchEvent( { type: 'resize' } ); + + } + + /** + * Frees the GPU-related resources allocated by this instance. Call this + * method whenever this instance is no longer used in your app. + * + * @fires RenderTarget#dispose + */ + dispose() { + + this.dispatchEvent( { type: 'dispose' } ); + + } + +} + +export default CanvasTarget; diff --git a/src/renderers/common/Renderer.js b/src/renderers/common/Renderer.js index b003d26508bb73..319d3df3c94c01 100644 --- a/src/renderers/common/Renderer.js +++ b/src/renderers/common/Renderer.js @@ -18,6 +18,7 @@ import NodeLibrary from './nodes/NodeLibrary.js'; import Lighting from './Lighting.js'; import XRManager from './XRManager.js'; import InspectorBase from './InspectorBase.js'; +import CanvasTarget from './CanvasTarget.js'; import NodeMaterial from '../../materials/nodes/NodeMaterial.js'; @@ -99,15 +100,6 @@ class Renderer { multiview = false } = parameters; - /** - * A reference to the canvas element the renderer is drawing to. - * This value of this property will automatically be created by - * the renderer. - * - * @type {HTMLCanvasElement|OffscreenCanvas} - */ - this.domElement = backend.getDomElement(); - /** * A reference to the current backend. * @@ -115,15 +107,6 @@ class Renderer { */ this.backend = backend; - /** - * The number of MSAA samples. - * - * @private - * @type {number} - * @default 0 - */ - this._samples = samples || ( antialias === true ) ? 4 : 0; - /** * Whether the renderer should automatically clear the current rendering target * before execute a `render()` call. The target can be the canvas (default framebuffer) @@ -271,65 +254,40 @@ class Renderer { // internals - this._inspector = new InspectorBase(); - this._inspector.setRenderer( this ); - - /** - * This callback function can be used to provide a fallback backend, if the primary backend can't be targeted. - * - * @private - * @type {?Function} - */ - this._getFallback = getFallback; - - /** - * The renderer's pixel ratio. - * - * @private - * @type {number} - * @default 1 - */ - this._pixelRatio = 1; - - /** - * The width of the renderer's default framebuffer in logical pixel unit. - * - * @private - * @type {number} - */ - this._width = this.domElement.width; - /** - * The height of the renderer's default framebuffer in logical pixel unit. + * OnCanvasTargetResize callback function. * * @private - * @type {number} + * @type {Function} */ - this._height = this.domElement.height; + this._onCanvasTargetResize = this._onCanvasTargetResize.bind( this ); /** - * The viewport of the renderer in logical pixel unit. + * The canvas target for rendering. * * @private - * @type {Vector4} + * @type {CanvasTarget} */ - this._viewport = new Vector4( 0, 0, this._width, this._height ); + this._canvasTarget = new CanvasTarget( backend.getDomElement() ); + this._canvasTarget.addEventListener( 'resize', this._onCanvasTargetResize ); + this._canvasTarget.isDefaultCanvasTarget = true; /** - * The scissor rectangle of the renderer in logical pixel unit. + * The inspector provides information about the internal renderer state. * * @private - * @type {Vector4} + * @type {InspectorBase} */ - this._scissor = new Vector4( 0, 0, this._width, this._height ); + this._inspector = new InspectorBase(); + this._inspector.setRenderer( this ); /** - * Whether the scissor test should be enabled or not. + * This callback function can be used to provide a fallback backend, if the primary backend can't be targeted. * * @private - * @type {boolean} + * @type {?Function} */ - this._scissorTest = false; + this._getFallback = getFallback; /** * A reference to a renderer module for managing shader attributes. @@ -834,6 +792,19 @@ class Renderer { } + /** + * A reference to the canvas element the renderer is drawing to. + * This value of this property will automatically be created by + * the renderer. + * + * @type {HTMLCanvasElement|OffscreenCanvas} + */ + get domElement() { + + return this._canvasTarget.domElement; + + } + /** * The coordinate system of the renderer. The value of this property * depends on the selected backend. Either `THREE.WebGLCoordinateSystem` or @@ -1315,11 +1286,13 @@ class Renderer { } - frameBufferTarget.viewport.copy( this._viewport ); - frameBufferTarget.scissor.copy( this._scissor ); - frameBufferTarget.viewport.multiplyScalar( this._pixelRatio ); - frameBufferTarget.scissor.multiplyScalar( this._pixelRatio ); - frameBufferTarget.scissorTest = this._scissorTest; + const canvasTarget = this._canvasTarget; + + frameBufferTarget.viewport.copy( canvasTarget._viewport ); + frameBufferTarget.scissor.copy( canvasTarget._scissor ); + frameBufferTarget.viewport.multiplyScalar( canvasTarget._pixelRatio ); + frameBufferTarget.scissor.multiplyScalar( canvasTarget._pixelRatio ); + frameBufferTarget.scissorTest = canvasTarget._scissorTest; frameBufferTarget.multiview = outputRenderTarget !== null ? outputRenderTarget.multiview : false; frameBufferTarget.resolveDepthBuffer = outputRenderTarget !== null ? outputRenderTarget.resolveDepthBuffer : true; frameBufferTarget._autoAllocateDepthBuffer = outputRenderTarget !== null ? outputRenderTarget._autoAllocateDepthBuffer : false; @@ -1437,9 +1410,11 @@ class Renderer { // - let viewport = this._viewport; - let scissor = this._scissor; - let pixelRatio = this._pixelRatio; + const canvasTarget = this._canvasTarget; + + let viewport = canvasTarget._viewport; + let scissor = canvasTarget._scissor; + let pixelRatio = canvasTarget._pixelRatio; if ( renderTarget !== null ) { @@ -1464,7 +1439,7 @@ class Renderer { renderContext.viewport = renderContext.viewportValue.equals( _screen ) === false; renderContext.scissorValue.copy( scissor ).multiplyScalar( pixelRatio ).floor(); - renderContext.scissor = this._scissorTest && renderContext.scissorValue.equals( _screen ) === false; + renderContext.scissor = canvasTarget._scissorTest && renderContext.scissorValue.equals( _screen ) === false; renderContext.scissorValue.width >>= activeMipmapLevel; renderContext.scissorValue.height >>= activeMipmapLevel; @@ -1608,8 +1583,10 @@ class Renderer { _setXRLayerSize( width, height ) { - this._width = width; - this._height = height; + // TODO: Find a better solution to resize the canvas when in XR. + + this._canvasTarget._width = width; + this._canvasTarget._height = height; this.setViewport( 0, 0, width, height ); @@ -1741,7 +1718,7 @@ class Renderer { */ getPixelRatio() { - return this._pixelRatio; + return this._canvasTarget.getPixelRatio(); } @@ -1753,7 +1730,7 @@ class Renderer { */ getDrawingBufferSize( target ) { - return target.set( this._width * this._pixelRatio, this._height * this._pixelRatio ).floor(); + return this._canvasTarget.getDrawingBufferSize( target ); } @@ -1765,7 +1742,7 @@ class Renderer { */ getSize( target ) { - return target.set( this._width, this._height ); + return this._canvasTarget.getSize( target ); } @@ -1776,11 +1753,7 @@ class Renderer { */ setPixelRatio( value = 1 ) { - if ( this._pixelRatio === value ) return; - - this._pixelRatio = value; - - this.setSize( this._width, this._height, false ); + this._canvasTarget.setPixelRatio( value ); } @@ -1802,17 +1775,7 @@ class Renderer { // Renderer can't be resized while presenting in XR. if ( this.xr && this.xr.isPresenting ) return; - this._width = width; - this._height = height; - - this._pixelRatio = pixelRatio; - - this.domElement.width = Math.floor( width * pixelRatio ); - this.domElement.height = Math.floor( height * pixelRatio ); - - this.setViewport( 0, 0, width, height ); - - if ( this._initialized ) this.backend.updateSize(); + this._canvasTarget.setDrawingBufferSize( width, height, pixelRatio ); } @@ -1828,22 +1791,7 @@ class Renderer { // Renderer can't be resized while presenting in XR. if ( this.xr && this.xr.isPresenting ) return; - this._width = width; - this._height = height; - - this.domElement.width = Math.floor( width * this._pixelRatio ); - this.domElement.height = Math.floor( height * this._pixelRatio ); - - if ( updateStyle === true ) { - - this.domElement.style.width = width + 'px'; - this.domElement.style.height = height + 'px'; - - } - - this.setViewport( 0, 0, width, height ); - - if ( this._initialized ) this.backend.updateSize(); + this._canvasTarget.setSize( width, height, updateStyle ); } @@ -1879,14 +1827,7 @@ class Renderer { */ getScissor( target ) { - const scissor = this._scissor; - - target.x = scissor.x; - target.y = scissor.y; - target.width = scissor.width; - target.height = scissor.height; - - return target; + return this._canvasTarget.getScissor( target ); } @@ -1901,17 +1842,7 @@ class Renderer { */ setScissor( x, y, width, height ) { - const scissor = this._scissor; - - if ( x.isVector4 ) { - - scissor.copy( x ); - - } else { - - scissor.set( x, y, width, height ); - - } + this._canvasTarget.setScissor( x, y, width, height ); } @@ -1922,7 +1853,7 @@ class Renderer { */ getScissorTest() { - return this._scissorTest; + return this._canvasTarget.getScissorTest(); } @@ -1933,7 +1864,9 @@ class Renderer { */ setScissorTest( boolean ) { - this._scissorTest = boolean; + this._canvasTarget.setScissorTest( boolean ); + + // TODO: Move it to CanvasTarget event listener. this.backend.setScissorTest( boolean ); @@ -1947,7 +1880,7 @@ class Renderer { */ getViewport( target ) { - return target.copy( this._viewport ); + return this._canvasTarget.getViewport( target ); } @@ -1963,20 +1896,7 @@ class Renderer { */ setViewport( x, y, width, height, minDepth = 0, maxDepth = 1 ) { - const viewport = this._viewport; - - if ( x.isVector4 ) { - - viewport.copy( x ); - - } else { - - viewport.set( x, y, width, height ); - - } - - viewport.minDepth = minDepth; - viewport.maxDepth = maxDepth; + this._canvasTarget.setViewport( x, y, width, height, minDepth, maxDepth ); } @@ -2252,7 +2172,7 @@ class Renderer { */ get samples() { - return this._samples; + return this._canvasTarget.samples; } @@ -2267,7 +2187,7 @@ class Renderer { */ get currentSamples() { - let samples = this._samples; + let samples = this.samples; if ( this._renderTarget !== null ) { @@ -2404,6 +2324,33 @@ class Renderer { } + /** + * Sets the canvas target. The canvas target manages the HTML canvas + * or the offscreen canvas the renderer draws into. + * + * @param {CanvasTarget} canvasTarget - The canvas target. + */ + setCanvasTarget( canvasTarget ) { + + this._canvasTarget.removeEventListener( 'resize', this._onCanvasTargetResize ); + this._canvasTarget.removeEventListener( 'contextlost', this._onCanvasTargetResize ); + + this._canvasTarget = canvasTarget; + this._canvasTarget.addEventListener( 'resize', this._onCanvasTargetResize ); + + } + + /** + * Returns the current canvas target. + * + * @return {CanvasTarget} The current canvas target. + */ + getCanvasTarget() { + + return this._canvasTarget; + + } + /** * Resets the renderer to the initial state before WebXR started. * @@ -3288,6 +3235,17 @@ class Renderer { } + /** + * Callback when the canvas has been resized. + * + * @private + */ + _onCanvasTargetResize() { + + if ( this._initialized ) this.backend.updateSize(); + + } + /** * Alias for `compileAsync()`. * diff --git a/src/renderers/webgpu/WebGPUBackend.js b/src/renderers/webgpu/WebGPUBackend.js index 086238f42a1b4f..b027377d2fb5d0 100644 --- a/src/renderers/webgpu/WebGPUBackend.js +++ b/src/renderers/webgpu/WebGPUBackend.js @@ -84,14 +84,6 @@ class WebGPUBackend extends Backend { */ this.device = null; - /** - * A reference to the context. - * - * @type {?GPUCanvasContext} - * @default null - */ - this.context = null; - /** * A reference to the default render pass descriptor. * @@ -224,28 +216,59 @@ class WebGPUBackend extends Backend { } ); - const context = ( parameters.context !== undefined ) ? parameters.context : renderer.domElement.getContext( 'webgpu' ); - this.device = device; - this.context = context; - const alphaMode = parameters.alpha ? 'premultiplied' : 'opaque'; + this.trackTimestamp = this.trackTimestamp && this.hasFeature( GPUFeatureName.TimestampQuery ); + + this.updateSize(); + + } + + /** + * A reference to the context. + * + * @type {?GPUCanvasContext} + * @default null + */ + get context() { + + const canvasData = this.get( this.renderer.getCanvasTarget() ); + + let context = canvasData.context; + + if ( context === undefined ) { - const toneMappingMode = ColorManagement.getToneMappingMode( this.renderer.outputColorSpace ); + const parameters = this.parameters; + + if ( canvasData.isDefaultCanvasTarget === true && parameters.context !== undefined ) { + + context = parameters.context; + + } else { + + context = this.renderer.domElement.getContext( 'webgpu' ); - this.context.configure( { - device: this.device, - format: this.utils.getPreferredCanvasFormat(), - usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_SRC, - alphaMode: alphaMode, - toneMapping: { - mode: toneMappingMode } - } ); - this.trackTimestamp = this.trackTimestamp && this.hasFeature( GPUFeatureName.TimestampQuery ); + const alphaMode = parameters.alpha ? 'premultiplied' : 'opaque'; - this.updateSize(); + const toneMappingMode = ColorManagement.getToneMappingMode( this.renderer.outputColorSpace ); + + context.configure( { + device: this.device, + format: this.utils.getPreferredCanvasFormat(), + usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_SRC, + alphaMode: alphaMode, + toneMapping: { + mode: toneMappingMode + } + } ); + + canvasData.context = context; + + } + + return context; } @@ -298,11 +321,13 @@ class WebGPUBackend extends Backend { */ _getDefaultRenderPassDescriptor() { - let descriptor = this.defaultRenderPassdescriptor; + const renderer = this.renderer; + const canvasTarget = renderer.getCanvasTarget(); + const canvasData = this.get( canvasTarget ); - if ( descriptor === null ) { + let descriptor = canvasData.descriptor; - const renderer = this.renderer; + if ( descriptor === undefined ) { descriptor = { colorAttachments: [ { @@ -310,7 +335,7 @@ class WebGPUBackend extends Backend { } ], }; - if ( this.renderer.depth === true || this.renderer.stencil === true ) { + if ( renderer.depth === true || renderer.stencil === true ) { descriptor.depthStencilAttachment = { view: this.textureUtils.getDepthBuffer( renderer.depth, renderer.stencil ).createView() @@ -320,7 +345,7 @@ class WebGPUBackend extends Backend { const colorAttachment = descriptor.colorAttachments[ 0 ]; - if ( this.renderer.currentSamples > 0 ) { + if ( renderer.currentSamples > 0 ) { colorAttachment.view = this.textureUtils.getColorBuffer().createView(); @@ -330,13 +355,13 @@ class WebGPUBackend extends Backend { } - this.defaultRenderPassdescriptor = descriptor; + canvasData.descriptor = descriptor; } const colorAttachment = descriptor.colorAttachments[ 0 ]; - if ( this.renderer.currentSamples > 0 ) { + if ( renderer.currentSamples > 0 ) { colorAttachment.resolveTarget = this.context.getCurrentTexture().createView(); @@ -2185,7 +2210,7 @@ class WebGPUBackend extends Backend { */ updateSize() { - this.defaultRenderPassdescriptor = null; + this.delete( this.renderer.getCanvasTarget() ); } diff --git a/src/renderers/webgpu/utils/WebGPUTextureUtils.js b/src/renderers/webgpu/utils/WebGPUTextureUtils.js index 002c7f70b900b0..8a1c551c7641f2 100644 --- a/src/renderers/webgpu/utils/WebGPUTextureUtils.js +++ b/src/renderers/webgpu/utils/WebGPUTextureUtils.js @@ -87,23 +87,6 @@ class WebGPUTextureUtils { */ this.defaultVideoFrame = null; - this.frameBufferData = { - color: { - buffer: null, // TODO: Move to FramebufferTexture - width: 0, - height: 0, - samples: 0 - }, - depth: { - texture: new DepthTexture(), - width: 0, - height: 0, - samples: 0, - depth: false, - stencil: false - } - }; - /** * A cache of shared texture samplers. * @@ -398,20 +381,22 @@ class WebGPUTextureUtils { getColorBuffer() { const backend = this.backend; + const canvasTarget = backend.renderer.getCanvasTarget(); const { width, height } = backend.getDrawingBufferSize(); const samples = backend.renderer.currentSamples; - const frameBufferColor = this.frameBufferData.color; + const colorTexture = canvasTarget.colorTexture; + const colorTextureData = backend.get( colorTexture ); - if ( frameBufferColor.width === width && frameBufferColor.height === height && frameBufferColor.samples === samples ) { + if ( colorTexture.width === width && colorTexture.height === height && colorTexture.samples === samples ) { - return frameBufferColor.buffer; + return colorTextureData.texture; } // recreate - let colorBuffer = frameBufferColor.buffer; + let colorBuffer = colorTextureData.texture; if ( colorBuffer ) colorBuffer.destroy(); @@ -429,10 +414,11 @@ class WebGPUTextureUtils { // - frameBufferColor.buffer = colorBuffer; - frameBufferColor.width = width; - frameBufferColor.height = height; - frameBufferColor.samples = samples; + colorTexture.source.width = width; + colorTexture.source.height = height; + colorTexture.samples = samples; + + colorTextureData.texture = colorBuffer; return colorBuffer; @@ -449,11 +435,11 @@ class WebGPUTextureUtils { getDepthBuffer( depth = true, stencil = false ) { const backend = this.backend; + const canvasTarget = backend.renderer.getCanvasTarget(); const { width, height } = backend.getDrawingBufferSize(); const samples = backend.renderer.currentSamples; - const frameBufferDepth = this.frameBufferData.depth; - const depthTexture = frameBufferDepth.texture; + const depthTexture = canvasTarget.depthTexture; if ( depthTexture.width === width && depthTexture.height === height && From 4c24ecdf141b43a0f83a4fd7c0000f3ba2320a5d Mon Sep 17 00:00:00 2001 From: sunag Date: Fri, 19 Sep 2025 10:37:01 -0300 Subject: [PATCH 2/7] add `webgpu_multiple_canvas` example --- examples/files.json | 1 + .../screenshots/webgpu_multiple_canvas.jpg | Bin 0 -> 33177 bytes examples/webgpu_multiple_canvas.html | 199 ++++++++++++++++++ test/e2e/puppeteer.js | 1 + 4 files changed, 201 insertions(+) create mode 100644 examples/screenshots/webgpu_multiple_canvas.jpg create mode 100644 examples/webgpu_multiple_canvas.html diff --git a/examples/files.json b/examples/files.json index ca5dc08cf753b7..1f571ae8bd4688 100644 --- a/examples/files.json +++ b/examples/files.json @@ -384,6 +384,7 @@ "webgpu_morphtargets", "webgpu_morphtargets_face", "webgpu_mrt", + "webgpu_multiple_canvas", "webgpu_multiple_elements", "webgpu_mrt_mask", "webgpu_multiple_rendertargets", diff --git a/examples/screenshots/webgpu_multiple_canvas.jpg b/examples/screenshots/webgpu_multiple_canvas.jpg new file mode 100644 index 0000000000000000000000000000000000000000..483b3073a95b7c3d4010de7ea6052bbe44a0601d GIT binary patch literal 33177 zcmd?R2UJwumM*#x5CsuIa#TqQB8cQvhzbG%N)!-EKqLuB&J+efvSi69NwVZjNlrqM zb1ZVMA{J2LZU58#zwX=pPQUJRI^274ILbX_*Pd&w8NT_=IX7XPFb`Z&QB+n0h=>4y z2>b&Orht#&)qnF3;U_?S0XQZ4OiV-#oTDZprY0h^5`6<@B>jtIz(4#Dog*e8Jx_Mw zB00qs;2aS#@i`J=Qc@BU@Hc(H{{u*wh=sRs`qDJnfyR?*Vd(bapdZ(#Py{I!Lpm9>+z zi>sTvhv$cn{sDoXf`X%>V`Agtza%7n%gD^i&iS63S6WtHQCU@8Q`_9q+ScCD+0{Kb zG(0joHa;=AfLvT!URhmR-$3u}9~>SXV@^(q0OCJ4`Y#LmC+h(hPIQihgqVcv_j-uV zxq*L)sYyt$-#t%r{~4Ky!_^zY?=H|PM1C!4y2yD?6Gdn0I6zL%C9=Sc{=K9>R`maG zLGS-VEBbRmf3Alx30xv30uPLs8h`+J{7oSJoF6&xfB6UbF%4tua{>?--A26>O7@24 zLK7L=U3scKtx@P10T3qutm(vE8v3wZECIke`cZMqniBx{J^?^?12Qp(@puA&AOIaV zfq%FI@?UiadEy73sOHm+d)K{eq?0~UinP)PFht%b?d0UU7b#DXWhA?kWaf5_Xvfwh z;pZAt|J{yCHQ))Nc1hl;{O91Ze$3M2@8`8YgkGKXEe7xi!@p~?N1i+Yn?o`0?+&Xyqo&by`!Pb`2sbtWFt!V4YP1=3M9F3sh4$gG0 z>6y^a1?S%ah!;fb@~p7`rX~G*w$jo{+)XWSJ$Ov~QsH;~e15g-mneU|d{6Q6y~H)t zBfyg*1U765Ke}s`){XR&>IQPGMtAXWZUWHHnNGzzQnVAVGdyBvN2m9+UPmWUo|Ym3 z0M7pe-U9RF0OHJi6^olD0COiWOdcRN;Xwc(ZjfU<6wl{JRcQ=E%ph)tU$qo75Q0$TO!wgdngM*vRQ04H`yP%YHpxQ`|TV9=%(*vtFZ0-J0k z0a$u!e?~t=0Mr8f1Tb!Rc>@icyCH0?BAp@!uRsB+8ThRkQGPO09gscxDgyAs8FH%S zCeLaP(Ec_>(tq-CK0JZ|T(=d(H;X|}wdep(=wCZ1+EK`oKpt#d@t6Rpn5SRb_$}di zWiE$X9XyEIk$6sBK3oAjt^$@~yYdE#5+568i>FkYix z&QfQPE0)X=QodE0(94o#!v#g2DH*tjh+&kh2Aj=z%Sw&$J=~D+Pt!`+7Uq|%&rCdO zL)Gou&XHJ{=RFL+W*Wv?!En3mb%R@?|L?sfvzWI(p?Cosm$Jv*Do?+ht#QIuNC38s zU^v03-_`)`BDq_N0DNXI@_b6W|wC+9HQv*(g_*{XnRI^Qa>A(egqZ4&n83&TDwhj5&tC6k)DgYgH1;k_PNh`zC^BS{ zjVT#_UbU8@J+Yt>Qmq*?B9*MsGQ27naJ8>=T-`uC(>zFREotCQ(we=#wUlobi)s#3 zf37u@ZmG2P{f4NiIH|?EB^}euD;LSgWcf?Od8vXz;@BHlA8M7wGXKb@JrlpE%)5jz zB>VF6-UXFxVwR5`S|4jHw8(-ofgQXXO@!)Z&{E{Sj~FqD{H$!#iWos%wW`dex70;Wa6THhcRojG+hz2a!Hy0j{WfCA_z7PL5^U zP0=24Jnr?noOE*TJxSEYg;qvGA@+%xnLAUqbu*Pya-Q|8Ql(BY*$1PVj-FXQh88Bq zl0gUJ&AP-bRT&bHDK{AcFcqHk!eu7A_AErbt|!N+;Ykj>=g=ZAh^hp1dYi%Bn~iWM ztjo0x8ENG1Rd?%kduK{FoxwXd+L^nL+%wEOc_h;*mozA7jZg^INV$>Oz;m^hXu&J5 z=uqRy@e;orC;Oe07aVjpV-9y8`g#2mp)axO-u+2B+8(^_N=dWNj@bsi6zTWJ&!phrsS3z} zEiC14GmSVSQv=OZEQV;fivVy|z;PkP;Pu{9GX399i%5Mxs|x39NC4KHiGI1S;_r(Q zfFtTa0-#*9M%)eR-#I@S{>6@M()lz$c8<~!tkzK-PSqe0In{z^8F0B`8G$erZ+^l>F|LAX#k=2&j$ znygVr6#~GWr;6UUKtVp?1svj8^x{~UzQA6aL+NDV`BDrdBoz0ay5}WtG(xV-kBOkE z#@o)uy=5=I4tG);)jwpD6Rpyw^0G|Zy!g{<(HMBNMnyO5E2&hGoVm(JfOBYC;tF$*6u@Dp;D?SsFBcCil3-h5aR zcO1|@3O@?9QdApiel2QmIn*-Hv2at75AwzxW;`ULmZ@)(_>cft{(y)43hJD?AzY&T zGC|n8WQ~Rx3e6K@ zIcyYc9kp|Ac582%plNq`Rj&pVUK}V^uIkdIQPB*`JhG;82ZJfqE7%Dw3%;$A01TO= zQ~3T>{4It?VEqfhaLh)9kD2KwWBNmHQDfWp&TKJC^SU{1^F9>nFgonRfWBF;d{c!r zUsjv(jG8hvYwc8)q@)R3?%+e`9%p+Jhb7^!VM-S5KaB+;HQ21(P8%~E-`jXG71gH* zJ_;}$?6AmQf7)8h<}(+b zLcR(GYa%ZgDaiZ|)u|9m7@7dg>#*(!2UGpH-28 z+K|HdGOAD^b6qE-)>A$C+i1`0+n0GcVocY~O6z-=;?2HG54OFu8EHmZ)#dDrebr1J ztk+relD_k5;3%g>W6~$fMvW6md%b=ooUvZRg7q$mo693x<95*iTK@-ki%SDa& z6tQrl$zxuxN<7VJ#Yj1S|K9ENkyB=l=)z!zda!0p-s0OAjlGI|&(CLRmdo|)>gkxj z?EWcS`dKF?G(c1M!nGypCe~owSCK)h>_jz@okY*<#DVNeiz69xrbj9QD#9wyVhkK& z3~H?knQgR3!>s+Yh0-b6IPeOSP~68i@PlkV-~dif0KN~vwoHd0v2B1$43YX!;T+Ca zl>nqG$+O=ofOQ&bgCH_&7a!9j&om4IQIyZ3{Ru19hya+$!3jW)VJ-d!7*^nBur0x> z;B~e)ody!ThZTH)&sS6+UKtSp+w?!ir`T`q{~E(9GNz$HRHpWGJ|C;>4L^wSRP$J| z=;t7RPDN!*OaSiZhr_lz)Qc|Zp&2j?_*l+cyL&kO^@YIK9gD%Mb-OSlm@viLtASmz zTJW}(#w0r=9=a@rA55^RG~ zYiwcngt?ypklN!!?;Wzk8#M+L{G=houug+TeA5tYT`dc65&n-QTf`}7x8MW8a~LQT z9UrutF(d<7NZ~m)7!}OA#$Dn6k;*}OKC-4eyECIxSfz`3uQ^d&jl0fDFrn?sBC1B0 zm?^=`Vhi=qe~DET(M>SfCL|*9>H@hK`Qv z)F)>c#oCV!7Z>`7uWiyD*;bvX2e;K^B1Ga7KQ0_0zR%34GCnq7G&{0vrcF;zw09){ zi5jP+dqyA_k#gVvv>z^kykzGSnjt0O!k{GBtdc4DR?>xxSMmLe)8TlLO(aiw?x%d+ zXUl0=LXO3bFD}}r@3A^(SAfMEIAXAs88{DwqR+@AjVm>3ufr6v_vN~ zvN4>Z>^Q4w*JmUpP^r@8$uh|iDiT!D%S^&xo;~8c7W3ulo%K5l_mg`*6Ukt6Lg$$) zw~EYfQ(7r1OYkgm74z1q34B9jwN3Lfa`|$s9(+HF+{ZUs+~`7UxGA5i;Y+|=`26yx z-Fd&VI%@Wz>Mb)r$v%MV)L}Dqco=!)wY`70&0ONkK+K8BSw*jlXz9R7gXjsz11t%q zA_wF3f}MHTg>md5nzYs7=Ma6E2+maP$5d~E!khQU)RQmLYKsJ3zkZDTQpGM#wS9g2 zMgv9w)<8LV7{1xPSpu>}1Mt?#dbU=`MyiwUvxPqH)}a9Q%%zmu#WJ})N>ra32!LC_ zFPfYWZIv}9ni4V(vs7YPA7&lTxaXV-|IX=3A=qFL0hvz0)_?Sq!Y>nmP6}lLP)uQu zqc10N1~2c!HhbX*j!(&wK-{A+3gVv2AnsAITO$qyQIFgkTprvqjO^$QM(4ZxkY?(y z&Dt0cxyxM|DrUXlLPc$KUt~bGIZ(-qO>C0skYzdSLl0ebMOc*KmvMLR2g$eN1#*K! zb)Gz&D`_6|%pFU#Rq-sI847mc&8rQ5Z`E}VS$!O0(ljmm>DPklCgW-8X|`T~RrN^w z$j7XUI%Of+Hx;fH8)`2zdflv$inQ1_%yK_gMbCxXiL%)krM(z&Q9aweVpBDA-|cqQ zm~h{io)bcFVvxrsze`&}A&*{xT1TM}CS%6VlV2EAFukdloFf$-5V`*`7oCr&c_%@?+YxA#@U>BBkZrX&#>&1VZrThTgaCc zWGo6lg*O@QZ#fm@UxBYqr(w3>v~Z5<6c-j7Z4S$5(=roA&gw3vYg@XU*^WLw`OMXx zF@Prux(0%)(VPxZ*B0=$;N=L zI3c4xBFO$gA~l<2d>4 zXrl(Tes+HdeT~8d2abxKCg3D!i83-kB{T%1!H=+bd`z1>dpvUPkXaBHYGAQfoqoC0 z@7APC9jLKgu!9S0#BHJxCluCcN7SI^4<=X=OM!(IJXO6d2

9;9C}B1mIxP4f?y# zaJ*%Y^GCuC4iN8iu_|ec;I|{n<*pEbE=d#u_qGv4U4=Mbk3{hCEm!e3Ve8ebbg&@7 zczjfZaW4M0-S>P@a0vlilISA1xzQ;V7&msnQZIHVKGh`f|vGS3rYdn$_bUYp7 zOD-HZ-90w)eQ4x?>-Dv4u)M!7bpI^vOwBF+AAP|hq;y679^AWX9oKo>_*@1leWyH_9J^Je3 zJlWi-mX$o)le9f)@HfV5L1GTd4F=Z#mB8)@hb>(N={yrg*g@ispG+ROSJ(wGiO#|F zV+@a;jW|DE-Lt@aSk$=a{)F%DRb_iC7l%du!a6w@|LEo*d&gDkayJh1$cV`}NhzW3 zLwjsV^cilh&%h%s3N!Krr}WN2oA$&aaS_Gj{bn*cIQO>C!(>;@oOdz2UXPm9sr0!B zK$1O)p-*w!EL))6yr`ja_u{p}%=hc1LXLGN((f4glRpb9WpM;?w2q(by)4lLW4kEl ze)0>3Mb2%{39DR`;%a~dnl~^ay9UId>pl&3N4^nz#9Nt3Y#tmr1iZVlNRmFkqVtaF(uHfG;Wp_fe0C1W7V4TG7q}(*1lphe0dC4PLzJ^qhRXVJ-%v$A z8n2@|0$rBdMN_C$MkGjpu~DHHr3;Pbe1mj+)9)|H1fL{yL^*z#6^?=8YA112M^pj( z&sXbZBQ2g)-La2eu8AtO5-gfuLmHGfEO&2+T3Hub>p2PJKuVTK zT;lW8{HjOIU38e8+m5>vOJhe6O||y_~&vp3FoY z;+PJ;sZ*Wc;Ygy)%(Q5HVcz;i?FmiJoJJ3xY#R4ia9ReikT!a}zNS>%VD_mjyU^ZM zdEH+0G+94NvxTSN#99%`lf{9nVGl&HfHwEhZIQ~)K7>yBaNOC$!zWwT_g4A66%tOz zd}|8c-WbJ4kpVOfzLae^)u27kvC`B#68iS9OG@Id8rnq{WEBNv&UOrA9)Nzs3=`S> zJV%^SxA%ISHm1g$bvE`z;jQltra2)W$3z#I6z)ZpQ%!W^zd?q8|FN*R4lDRlgyJV` z?e1IHo~lN%APW|Bo?{gtJ@TYEGb&2s#2!;NC@%ON5ACb&T8VHu@qHckj;yzd3FMh0HR~#KvxA9Cji6Dfn_c5J&`OBJa59*D}6J@j?t&r z{yy}xFAuEgEhqX;0!n&;D%4p&@W?I#t+=T$vYj}y!?|CtwBVz%`=l|RqLiyrj;*1L zrgKeZM*XIqQ#YxL2^7vf(oO)JrqEcbHgCJ0#+pcT(Uq08g)+U1?U$9EOob{MQk3;Y zVthi=lM8osl)aVqGf-2=z@TgBqJW(v5+=Jvqfx8YXscrfMa37Fg4i6A_m35~Gd-y> zk=T+%b4LA_4j1{7?!$ey@|qgrUl9PKJ3d|nfZ{h|R@*q}BCf1GR5gn%SbqikD)`Kc2#?ddS5cf**x%i#^Ul=nAiPjh{6^#`nQ`g+q}42T~~?+5@p^|1v05Li?}DBDg? zL#|*S!ovGcUr)e3TQ|BbV+cUPqL1pO;vS~rGZ>y2i09iw7Z<@N8+E4FYd>iE5Ukno z3{)-5oe)_;4i~^c|83OoPTI)@L7YbwK3oOZw`T>n$zTrO1SVYTYBPWX7zoMspkVg( zwI1#E9{%R}PU}EVrhDCj#rwk<+Ft8DC0ojrzM6%)sS7JFBg-QLb#sqxfQpxah^@e&M-4J?a*nK04%)5Jj>@;n9lX1y4G?Z=HdmN%s_((8XhKdBAl71b<~rnn$-7@B2#QT5Tch}OA>NuHcl{-rKqNIUjslZH$O#^hc8 z3>Al%q#Vgb%SSqcM`8SD%Bq&_cQhU%)=uGh8V8u0#7dyNiV zh(OO6%Mr^C@@(dSR{5qISkjCF`CjUNvS1sIJ^B&I7AI(mS;GD2T|hz@%i%tSSAsCW z#t|S8!#u!azGM*7cU? z0MT%^S1rzKjQ|h;UVZ_vg5?V$zwe?{Hed$c^C)+m zO;Ud6*Y_85+!t*vyA(1FT@7;9Mk@By5lh}M_GQtsBD!87cWd>^J6E!TTeN~5lQ&Fk zvz#H~87(Q-|wU>uQ}hQEpEpnp3@e88e49Yu!$AaInug z{)Jt<%%UmLD#J^Ml_8IJUCTGCwEDf7e92WxWdUlIEJyx0kTod5M*9*W84Ejljzyb~_m;j1_Y)F?pyX|7y z3B~WcM~VY?t1kVr@b3)J6F%QCccB8jkrc~>Im})k?9ITiAGflTqDON`XnJu;=E%A* z`rW1~&ku^v_1A+6CTt9#lgmLb@xx=~J>n@A^%cSP!HcYPD&;?_Yo9u5v#IDLj(i_k za$Y(dwjEtYIoE+hq8IOI$DmcgIt>BHA90?LAX{pz@TGdN z*X=GF=3(5IgFQ>b6ASLi8TA)bW$1VgrJb;+pays5&RkK2zsQ&$p0t)EQ&o;%_O(0z zRxXDQVJ!SqPg5^yM%FoUWko2yB(P2p*^z-)K4Bj{%?gEW3Km3@k@@`MM|}PDfo!s- z#vEIEmaupx6=CfJJKBPGR^+$`-VfG)m1a12{bA_)L2EHfVtQlYp>SW_#oW_y)3-^= zH`VG6LSSF%-eQ6j`9%1Yt9)+tu3Idx_`uId)2$hP8fNs!vc!=G_{59<_6{8 z;=cp4kaFhqjdC$($~+E}Oe?@}w@6HPgqLD{UWhEd>$-)!cS%jTeL%-NurToFkou{% zd)|*?+R+~^BO4KG=ir`@nr1%{XV?ja73_$*6r>ftd?y~n8sak^fnGG^30{vDY=j|| zRtPcU?{k5tA%eCho5Q*02W~7pQGtG48G)qcEg395&(RP-6kf41CI*> z)Af1{d};zp2NI-(b^J|Q9Ko7F4w`A;Q9`eQnzBAF_N1&oKQPh*&#`3DhlO+Oi;1M2 zfZqQN{3y#2X&U8U2k%$cGnhOcLWBIny?+7F;r{}lSNx{|bTG1lk~O`CygY$f%!OR_ z?T1j7OWwfTMSsw#t{=1Ax@MMxmBYk)+^QZCnV=EbJHS!2({%AvEo{Lj=7*Tz3~u<`oCmMl=KxWIB4PQtL? z$&>)hI6#i5K!2mwd8YDVp?8M)=Hh|YQuBRo^T+*>30!WbbUIi3lqFh{Q~Bnr z>~l78zC3hE-sQ+7$FH!yL(Cpu>^pm4;Ph*v08v)jA2(q;wsU{VFj0ruMF-k zL`fqqV#F3D>l0_6_fKxh@js2f4e=1j_Pc$Z#t@NfJ*wOkQj8F^VAxS|t&D+dR~ox{ zt|2z&$Rx=1c}USbwt>$3-rY&z!WCNx2ue#4M$WO5xd{;;<=QkH%5ZTPY3@b>KNf~GdV zXvMGltw(y5yKEz2OHYjrROxH3uugkjFf<}E5Wq{R z#1vTQ-#Qd#z!>b5rJG!tBH7lisw%Q|5M}+$HO9c|9u`jkZ0#q++tW&59dlQ`dC|As zs%p#{@bC^$mqz45i*KK_40)3-$U3zd^nF->T(WXAmfx<{!r(%3Mo+DLUB{;%uNB`?uJev} zomA$pjBN{SLhj3$vtA~q(xVG1)D*Go&tSOKmziiBsf^W%e)>@>IrCc*6wEZY0?$O& zV4%lq7T|BvoqN@YcFn#q+IS5UJT85w0%`TF_48!U?Wez-jp%vsJp*R$wmk-|$NN(q zuCG~@qbO1KnH&FU?U z8UV2e!`!J$V{z}k#hV^5(l$`xM4$ZWi{DBz6^ZN=z4mfzfu0Z-Z(?N2$>e1Adq zo;!1#qQ>9{2sh{t90%Agxoi$U66_@ahv&a;p?0xhw_sznbEmdx-w$zE-%qqe6Brh} z!WhTVWl)b)Y;O|hM&p&h{xb$B0Z?7~O_YO%eJ*8H)&3M)1$L~i5q|+&2Hu01YKxN! zA8pi67W+qsn)#+DPYBY8LkG3{=^#w7m7QZu>%x<*)a(XeE;%oM>P}&$ zvoh+TG)NPpQJ?gZR20q+9L07imfs%Zmh!HQUrGhpB@$VX>P~TYJjom==OD%etI6%U zq~QJ07Ag8$ zmD+-k=*yGz>AOPDl8uIJQnaE%WObp>e_R`A7nAjsTcv4%AzqDYQl)mT6%} z=lMIaZfMFP`+DgGwFli$A$Q^?@#DmR_$<--v*il(`5%*rV9qZMZ2?)CZPL$`K5le- z8ht;4ug>jZ5knX!zdbqjrQ%BhmlR(X%--tHQ5RW$l{T{0Vy{v4a5#EW7fsMS^|8C3ZwGRr-n?GA_fueW^dFMaP_>{e_m&5$SbFT|(945QS?PkT3 z*ciIp{_;nH|3#CMYESn>VK?Uu#dT$#nE8Vko@gue%lDM)L|tJ*Y2c<~St`&<8tC)6 z#rTdnX6FXIrlIq7Wf%u^h{4D~#8t~y1p^~qMRa)JH){iqAWpB6u)7u8g3THhW_$)4 z29>!kRy?`iv5ouz(J@*6vEmHHWd((z+1;6@!$k)X!eQ}kyml5R0agp8F1&ht`GK+G zySL?pWh%@v?1YNdlQT5;2H6K`x9MXxl-v5CG={LItMK_esq^XQBNiYOSEWP?YEvTG zcF%F${n>jdv+HPEeUbolTT!cDFOM?nuY7vWA3K2><*`z9_oPvt!$ejL9%|$(A#_l4 zqIogaFW>5L_VyOtNaJ(0K63REE7#bQ3νK*ES?z}gfUK-%ZR+37Kl7Py8@)b!4% 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 Fe*hmEk(dAg literal 0 HcmV?d00001 diff --git a/examples/webgpu_multiple_canvas.html b/examples/webgpu_multiple_canvas.html new file mode 100644 index 00000000000000..eb34fa165e00e3 --- /dev/null +++ b/examples/webgpu_multiple_canvas.html @@ -0,0 +1,199 @@ + + + + three.js webgpu - multiple canvas + + + + + + + +

+
three.js - multiple canvas
+
+ + + + + + + \ No newline at end of file diff --git a/test/e2e/puppeteer.js b/test/e2e/puppeteer.js index ffda3042b9b369..0c29e29481f6b6 100644 --- a/test/e2e/puppeteer.js +++ b/test/e2e/puppeteer.js @@ -132,6 +132,7 @@ const exceptionList = [ 'webgpu_compute_texture_pingpong', 'webgpu_compute_water', 'webgpu_materials', + 'webgpu_multiple_canvas', 'webgpu_video_panorama', 'webgpu_postprocessing_bloom_emissive', 'webgpu_lights_tiled', From db336a5242361d5f0a18ff13363405f3db9be46f Mon Sep 17 00:00:00 2001 From: sunag Date: Fri, 19 Sep 2025 10:45:55 -0300 Subject: [PATCH 3/7] cleanup --- src/renderers/common/Renderer.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/renderers/common/Renderer.js b/src/renderers/common/Renderer.js index 319d3df3c94c01..c681d31beb7161 100644 --- a/src/renderers/common/Renderer.js +++ b/src/renderers/common/Renderer.js @@ -268,7 +268,7 @@ class Renderer { * @private * @type {CanvasTarget} */ - this._canvasTarget = new CanvasTarget( backend.getDomElement() ); + this._canvasTarget = new CanvasTarget( backend.getDomElement(), { antialias, samples } ); this._canvasTarget.addEventListener( 'resize', this._onCanvasTargetResize ); this._canvasTarget.isDefaultCanvasTarget = true; From 93f8d9a4f57a1227b8212fa2779e0e529d6016d1 Mon Sep 17 00:00:00 2001 From: sunag Date: Fri, 19 Sep 2025 10:58:47 -0300 Subject: [PATCH 4/7] rev --- src/renderers/common/Renderer.js | 1 - src/renderers/webgpu/WebGPUBackend.js | 12 ++++++++---- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/renderers/common/Renderer.js b/src/renderers/common/Renderer.js index c681d31beb7161..8b56b5e7db46f9 100644 --- a/src/renderers/common/Renderer.js +++ b/src/renderers/common/Renderer.js @@ -2333,7 +2333,6 @@ class Renderer { setCanvasTarget( canvasTarget ) { this._canvasTarget.removeEventListener( 'resize', this._onCanvasTargetResize ); - this._canvasTarget.removeEventListener( 'contextlost', this._onCanvasTargetResize ); this._canvasTarget = canvasTarget; this._canvasTarget.addEventListener( 'resize', this._onCanvasTargetResize ); diff --git a/src/renderers/webgpu/WebGPUBackend.js b/src/renderers/webgpu/WebGPUBackend.js index b027377d2fb5d0..8dd6b0723dfdf6 100644 --- a/src/renderers/webgpu/WebGPUBackend.js +++ b/src/renderers/webgpu/WebGPUBackend.js @@ -13,7 +13,7 @@ import WebGPUBindingUtils from './utils/WebGPUBindingUtils.js'; import WebGPUPipelineUtils from './utils/WebGPUPipelineUtils.js'; import WebGPUTextureUtils from './utils/WebGPUTextureUtils.js'; -import { WebGPUCoordinateSystem, TimestampQuery } from '../../constants.js'; +import { WebGPUCoordinateSystem, TimestampQuery, REVISION } from '../../constants.js'; import WebGPUTimestampQueryPool from './utils/WebGPUTimestampQueryPool.js'; import { warnOnce, error } from '../../utils.js'; import { ColorManagement } from '../../math/ColorManagement.js'; @@ -232,7 +232,8 @@ class WebGPUBackend extends Backend { */ get context() { - const canvasData = this.get( this.renderer.getCanvasTarget() ); + const canvasTarget = this.renderer.getCanvasTarget(); + const canvasData = this.get( canvasTarget ); let context = canvasData.context; @@ -240,16 +241,19 @@ class WebGPUBackend extends Backend { const parameters = this.parameters; - if ( canvasData.isDefaultCanvasTarget === true && parameters.context !== undefined ) { + if ( canvasTarget.isDefaultCanvasTarget === true && parameters.context !== undefined ) { context = parameters.context; } else { - context = this.renderer.domElement.getContext( 'webgpu' ); + context = canvasTarget.domElement.getContext( 'webgpu' ); } + // OffscreenCanvas does not have setAttribute, see #22811 + if ( 'setAttribute' in canvasTarget.domElement ) canvasTarget.domElement.setAttribute( 'data-engine', `three.js r${ REVISION } webgpu` ); + const alphaMode = parameters.alpha ? 'premultiplied' : 'opaque'; const toneMappingMode = ColorManagement.getToneMappingMode( this.renderer.outputColorSpace ); From 06248635deede706a0181b91d55f2bcd765e948d Mon Sep 17 00:00:00 2001 From: sunag Date: Fri, 19 Sep 2025 11:04:15 -0300 Subject: [PATCH 5/7] cleanup --- src/renderers/webgpu/utils/WebGPUTextureUtils.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/renderers/webgpu/utils/WebGPUTextureUtils.js b/src/renderers/webgpu/utils/WebGPUTextureUtils.js index 8a1c551c7641f2..5c0aa5e82c688f 100644 --- a/src/renderers/webgpu/utils/WebGPUTextureUtils.js +++ b/src/renderers/webgpu/utils/WebGPUTextureUtils.js @@ -17,7 +17,6 @@ import { UnsignedInt101111Type, RGBA_BPTC_Format, RGB_ETC1_Format, RGB_S3TC_DXT1_Format, RED_RGTC1_Format, SIGNED_RED_RGTC1_Format, RED_GREEN_RGTC2_Format, SIGNED_RED_GREEN_RGTC2_Format } from '../../../constants.js'; import { CubeTexture } from '../../../textures/CubeTexture.js'; -import { DepthTexture } from '../../../textures/DepthTexture.js'; import { Texture } from '../../../textures/Texture.js'; import { warn, error } from '../../../utils.js'; From b0da05bf1cf2b56d9420728f459c5de2b60cc9cb Mon Sep 17 00:00:00 2001 From: sunag Date: Fri, 19 Sep 2025 11:08:40 -0300 Subject: [PATCH 6/7] cleanup --- src/renderers/common/CanvasTarget.js | 24 ------------------------ 1 file changed, 24 deletions(-) diff --git a/src/renderers/common/CanvasTarget.js b/src/renderers/common/CanvasTarget.js index 05c27c367c3f41..b13f090b51aa04 100644 --- a/src/renderers/common/CanvasTarget.js +++ b/src/renderers/common/CanvasTarget.js @@ -44,30 +44,6 @@ class CanvasTarget extends EventDispatcher { */ this.domElement = domElement; - /** - * Defines the output color space of the renderer. - * - * @type {string} - * @default SRGBColorSpace - */ - this.outputColorSpace = SRGBColorSpace; - - /** - * Defines the tone mapping of the renderer. - * - * @type {number} - * @default NoToneMapping - */ - this.toneMapping = NoToneMapping; - - /** - * Defines the tone mapping exposure. - * - * @type {number} - * @default 1 - */ - this.toneMappingExposure = 1.0; - /** * The renderer's pixel ratio. * From 1dfdb3dad079cff8a0e403858ad350df4e29cb79 Mon Sep 17 00:00:00 2001 From: sunag Date: Fri, 19 Sep 2025 11:13:15 -0300 Subject: [PATCH 7/7] Update CanvasTarget.js --- src/renderers/common/CanvasTarget.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/renderers/common/CanvasTarget.js b/src/renderers/common/CanvasTarget.js index b13f090b51aa04..6e6c1281b723b0 100644 --- a/src/renderers/common/CanvasTarget.js +++ b/src/renderers/common/CanvasTarget.js @@ -2,7 +2,6 @@ import { EventDispatcher } from '../../core/EventDispatcher.js'; import { Vector4 } from '../../math/Vector4.js'; import { FramebufferTexture } from '../../textures/FramebufferTexture.js'; import { DepthTexture } from '../../textures/DepthTexture.js'; -import { NoToneMapping, SRGBColorSpace } from '../../constants.js'; /** * CanvasTarget is a class that represents the final output destination of the renderer.