UNPKG

three

Version:

JavaScript 3D library

1,809 lines (1,192 loc) 80 kB
// debugger tools // import 'https://greggman.github.io/webgpu-avoid-redundant-state-setting/webgpu-check-redundant-state-setting.js'; import { GPUFeatureName, GPULoadOp, GPUStoreOp, GPUIndexFormat, GPUTextureViewDimension, GPUFeatureMap, GPUShaderStage } from './utils/WebGPUConstants.js'; import WGSLNodeBuilder from './nodes/WGSLNodeBuilder.js'; import Backend from '../common/Backend.js'; import WebGPUUtils, { submit } from './utils/WebGPUUtils.js'; import WebGPUAttributeUtils from './utils/WebGPUAttributeUtils.js'; import WebGPUBindingUtils from './utils/WebGPUBindingUtils.js'; import WebGPUCapabilities from './utils/WebGPUCapabilities.js'; import WebGPUPipelineUtils from './utils/WebGPUPipelineUtils.js'; import WebGPUTextureUtils from './utils/WebGPUTextureUtils.js'; import { WebGPUCoordinateSystem, TimestampQuery, REVISION, HalfFloatType, Compatibility } from '../../constants.js'; import WebGPUTimestampQueryPool from './utils/WebGPUTimestampQueryPool.js'; import { error } from '../../utils.js'; import GPUBufferDescriptor from './descriptors/GPUBufferDescriptor.js'; import GPUCommandEncoderDescriptor from './descriptors/GPUCommandEncoderDescriptor.js'; import GPUComputePassDescriptor from './descriptors/GPUComputePassDescriptor.js'; import GPUQuerySetDescriptor from './descriptors/GPUQuerySetDescriptor.js'; import GPUShaderModuleDescriptor from './descriptors/GPUShaderModuleDescriptor.js'; import GPURenderPassColorAttachment from './descriptors/GPURenderPassColorAttachment.js'; import GPURenderPassDepthStencilAttachment from './descriptors/GPURenderPassDepthStencilAttachment.js'; import GPURenderPassDescriptor from './descriptors/GPURenderPassDescriptor.js'; import GPURenderPassTimestampWrites from './descriptors/GPURenderPassTimestampWrites.js'; import GPUTexelCopyTextureInfo from './descriptors/GPUTexelCopyTextureInfo.js'; import GPUTextureViewDescriptor from './descriptors/GPUTextureViewDescriptor.js'; import GPUExtent3D from './descriptors/GPUExtent3D.js'; const _clearValue = { r: 0, g: 0, b: 0, a: 1 }; const _bufferDescriptor = new GPUBufferDescriptor(); const _commandEncoderDescriptor = new GPUCommandEncoderDescriptor(); const _computePassDescriptor = new GPUComputePassDescriptor(); const _querySetDescriptor = new GPUQuerySetDescriptor(); const _shaderModuleDescriptor = new GPUShaderModuleDescriptor(); const _renderPassTimestampWrites = new GPURenderPassTimestampWrites(); const _texelCopyTextureInfoSrc = new GPUTexelCopyTextureInfo(); const _texelCopyTextureInfoDst = new GPUTexelCopyTextureInfo(); const _viewDescriptor = new GPUTextureViewDescriptor(); const _extent3D = new GPUExtent3D(); /** * A backend implementation targeting WebGPU. * * @private * @augments Backend */ class WebGPUBackend extends Backend { /** * WebGPUBackend options. * * @typedef {Object} WebGPUBackend~Options * @property {boolean} [logarithmicDepthBuffer=false] - Whether logarithmic depth buffer is enabled or not. * @property {boolean} [reversedDepthBuffer=false] - Whether reversed depth buffer is enabled or not. * @property {boolean} [alpha=true] - Whether the default framebuffer (which represents the final contents of the canvas) should be transparent or opaque. * @property {boolean} [depth=true] - Whether the default framebuffer should have a depth buffer or not. * @property {boolean} [stencil=false] - Whether the default framebuffer should have a stencil buffer or not. * @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. Set this parameter to any other integer value than 0 to overwrite the default. * @property {boolean} [forceWebGL=false] - If set to `true`, the renderer uses a WebGL 2 backend no matter if WebGPU is supported or not. * @property {boolean} [trackTimestamp=false] - Whether to track timestamps with a Timestamp Query API or not. * @property {string} [powerPreference=undefined] - The power preference. * @property {Object} [requiredLimits=undefined] - Specifies the limits that are required by the device request. The request will fail if the adapter cannot provide these limits. * @property {GPUDevice} [device=undefined] - If there is an existing GPU device on app level, it can be passed to the renderer as a parameter. * @property {number} [outputType=undefined] - Texture type for output to canvas. By default, device's preferred format is used; other formats may incur overhead. */ /** * Constructs a new WebGPU backend. * * @param {WebGPUBackend~Options} [parameters] - The configuration parameter. */ constructor( parameters = {} ) { super( parameters ); /** * This flag can be used for type testing. * * @type {boolean} * @readonly * @default true */ this.isWebGPUBackend = true; // some parameters require default values other than "undefined" this.parameters.alpha = ( parameters.alpha === undefined ) ? true : parameters.alpha; this.parameters.requiredLimits = ( parameters.requiredLimits === undefined ) ? {} : parameters.requiredLimits; /** * Indicates whether the backend is in WebGPU compatibility mode or not. * The backend must be initialized before the property can be evaluated. * * @type {?boolean} * @readonly * @default null */ this.compatibilityMode = null; /** * A reference to the device. * * @type {?GPUDevice} * @default null */ this.device = null; /** * A reference to the default render pass descriptor. * * @type {?Object} * @default null */ this.defaultRenderPassdescriptor = null; /** * A reference to a backend module holding common utility functions. * * @type {WebGPUUtils} */ this.utils = new WebGPUUtils( this ); /** * A reference to a backend module holding shader attribute-related * utility functions. * * @type {WebGPUAttributeUtils} */ this.attributeUtils = new WebGPUAttributeUtils( this ); /** * A reference to a backend module holding shader binding-related * utility functions. * * @type {WebGPUBindingUtils} */ this.bindingUtils = new WebGPUBindingUtils( this ); /** * A reference to a backend module holding device capability related * utility functions. * * @type {WebGPUCapabilities} */ this.capabilities = new WebGPUCapabilities( this ); /** * A reference to a backend module holding shader pipeline-related * utility functions. * * @type {WebGPUPipelineUtils} */ this.pipelineUtils = new WebGPUPipelineUtils( this ); /** * A reference to a backend module holding shader texture-related * utility functions. * * @type {WebGPUTextureUtils} */ this.textureUtils = new WebGPUTextureUtils( this ); /** * A map that manages the resolve buffers for occlusion queries. * * @type {Map<number,GPUBuffer>} */ this.occludedResolveCache = new Map(); // compatibility checks const compatibilityTextureCompare = typeof navigator === 'undefined' ? true : /Android/.test( navigator.userAgent ) === false; /** * A map of compatibility checks. * * @type {Object} */ this._compatibility = { [ Compatibility.TEXTURE_COMPARE ]: compatibilityTextureCompare }; } /** * Initializes the backend so it is ready for usage. * * @async * @param {Renderer} renderer - The renderer. * @return {Promise} A Promise that resolves when the backend has been initialized. */ async init( renderer ) { await super.init( renderer ); // const parameters = this.parameters; // create the device if it is not passed with parameters let device; if ( parameters.device === undefined ) { const adapterOptions = { powerPreference: parameters.powerPreference, featureLevel: 'compatibility', xrCompatible: renderer.xr.enabled }; const adapter = ( typeof navigator !== 'undefined' ) ? await navigator.gpu.requestAdapter( adapterOptions ) : null; if ( adapter === null ) { throw new Error( 'THREE.WebGPUBackend: Unable to create WebGPU adapter.' ); } // feature support const features = Object.values( GPUFeatureName ); const supportedFeatures = []; for ( const name of features ) { if ( adapter.features.has( name ) ) { supportedFeatures.push( name ); } } const deviceDescriptor = { requiredFeatures: supportedFeatures, requiredLimits: parameters.requiredLimits }; device = await adapter.requestDevice( deviceDescriptor ); } else { device = parameters.device; } this.compatibilityMode = ! device.features.has( 'core-features-and-limits' ); if ( this.compatibilityMode ) { renderer._samples = 0; } device.lost.then( ( info ) => { if ( info.reason === 'destroyed' ) return; const deviceLossInfo = { api: 'WebGPU', message: info.message || 'Unknown reason', reason: info.reason || null, originalEvent: info }; renderer.onDeviceLost( deviceLossInfo ); } ); device.onuncapturederror = ( event ) => { const gpuError = event.error; const type = gpuError && gpuError.constructor ? gpuError.constructor.name : 'GPUError'; const message = ( gpuError && gpuError.message ) || 'Unknown uncaptured GPU error'; renderer.onError( { api: 'WebGPU', type, message, originalEvent: event } ); }; this.device = device; this.trackTimestamp = this.trackTimestamp && this.hasFeature( GPUFeatureName.TimestampQuery ); this.updateSize(); } /** * Registers external GPU textures from `XRGPUBinding` for use in rendering. * * @param {RenderTarget} renderTarget - The render target to register the textures for. * @param {GPUTexture} colorTexture - The shared XR color GPUTexture. * @param {?Array<Object>} [viewDescriptors=null] - Optional view descriptors, one per XR view. */ setXRRenderTargetTextures( renderTarget, colorTexture, viewDescriptors = null ) { this.set( renderTarget.texture, { texture: colorTexture, format: colorTexture.format, externalTexture: true, xrViewDescriptors: viewDescriptors, initialized: true } ); } /** * A reference to the context. * * @type {?GPUCanvasContext} * @default null */ get context() { const canvasTarget = this.renderer.getCanvasTarget(); const canvasData = this.get( canvasTarget ); let context = canvasData.context; if ( context === undefined ) { const parameters = this.parameters; if ( canvasTarget.isDefaultCanvasTarget === true && parameters.context !== undefined ) { context = parameters.context; } else { 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 = parameters.outputType === HalfFloatType ? 'extended' : 'standard'; 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; } /** * The coordinate system of the backend. * * @type {number} * @readonly */ get coordinateSystem() { return WebGPUCoordinateSystem; } /** * Whether the backend supports query timestamps or not. * * @type {boolean} * @readonly */ get hasTimestamp() { return true; } /** * This method performs a readback operation by moving buffer data from * a storage buffer attribute from the GPU to the CPU. ReadbackBuffer can * be used to retain and reuse handles to the intermediate buffers and prevent * new allocation. * * @async * @param {BufferAttribute} attribute - The storage buffer attribute to read frm. * @param {number} count - The offset from which to start reading the * @param {number} offset - The storage buffer attribute. * @param {ReadbackBuffer|ArrayBuffer} target - The storage buffer attribute. * @return {Promise<ArrayBuffer|ReadbackBuffer>} A promise that resolves with the buffer data when the data are ready. */ async getArrayBufferAsync( attribute, target = null, offset = 0, count = - 1 ) { return await this.attributeUtils.getArrayBufferAsync( attribute, target, offset, count ); } /** * Returns the backend's rendering context. * * @return {GPUCanvasContext} The rendering context. */ getContext() { return this.context; } /** * Returns the default render pass descriptor. * * In WebGPU, the default framebuffer must be configured * like custom framebuffers so the backend needs a render * pass descriptor even when rendering directly to screen. * * @private * @return {Object} The render pass descriptor. */ _getDefaultRenderPassDescriptor() { const renderer = this.renderer; const canvasTarget = renderer.getCanvasTarget(); const canvasData = this.get( canvasTarget ); const samples = renderer.currentSamples; let descriptor = canvasData.descriptor; if ( descriptor === undefined || canvasData.samples !== samples ) { descriptor = new GPURenderPassDescriptor(); descriptor.colorAttachments.push( new GPURenderPassColorAttachment() ); if ( renderer.depth === true || renderer.stencil === true ) { const depthStencilAttachment = new GPURenderPassDepthStencilAttachment(); depthStencilAttachment.view = this.textureUtils.getDepthBuffer( renderer.depth, renderer.stencil ).createView(); descriptor.depthStencilAttachment = depthStencilAttachment; } const colorAttachment = descriptor.colorAttachments[ 0 ]; if ( samples > 0 ) { colorAttachment.view = this.textureUtils.getColorBuffer().createView(); } else { colorAttachment.resolveTarget = undefined; } canvasData.descriptor = descriptor; canvasData.samples = samples; } const colorAttachment = descriptor.colorAttachments[ 0 ]; if ( samples > 0 ) { colorAttachment.resolveTarget = this.context.getCurrentTexture().createView(); } else { colorAttachment.view = this.context.getCurrentTexture().createView(); } return descriptor; } /** * Returns whether the render target is a render target array with depth 2D array texture. * * @param {RenderContext} renderContext - The render context. * @return {boolean} Whether the render target is a render target array with depth 2D array texture. * * @private */ _isRenderCameraDepthArray( renderContext ) { const camera = renderContext.camera; return renderContext.depthTexture && renderContext.depthTexture.isArrayTexture === true && camera !== null && camera.isArrayCamera === true; } /** * Returns whether the current render context references external textures. * * External textures can change every frame, so their descriptors must not be cached. * * @private * @param {RenderContext} renderContext - The render context. * @return {boolean} Whether the render context uses external textures. */ _hasExternalTexture( renderContext ) { const textures = renderContext.textures; if ( textures === null ) return false; for ( let i = 0; i < textures.length; i ++ ) { if ( this.get( textures[ i ] ).externalTexture === true ) return true; } return false; } /** * Creates attachment views for an external texture render target. * * @private * @param {RenderContext} renderContext - The render context. * @param {Object} textureData - The backend data for the texture. * @return {Array<Object>} The attachment view descriptors. */ _createExternalTextureViews( renderContext, textureData ) { const textureViews = []; const camera = renderContext.camera; if ( textureData.xrViewDescriptors && camera !== null && camera.isArrayCamera === true ) { for ( let i = 0; i < textureData.xrViewDescriptors.length; i ++ ) { textureViews.push( { view: textureData.texture.createView( textureData.xrViewDescriptors[ i ] ), resolveTarget: undefined, depthSlice: undefined } ); } } else { textureViews.push( { view: textureData.texture.createView( { dimension: GPUTextureViewDimension.TwoD, baseArrayLayer: renderContext.activeCubeFace, arrayLayerCount: 1 } ), resolveTarget: undefined, depthSlice: undefined } ); } return textureViews; } /** * Returns the render pass descriptor for the given render context. * * @private * @param {RenderContext} renderContext - The render context. * @param {Object} colorAttachmentsConfig - Configuration object for the color attachments. * @return {Object} The render pass descriptor. */ _getRenderPassDescriptor( renderContext, colorAttachmentsConfig = {} ) { const renderTarget = renderContext.renderTarget; const renderTargetData = this.get( renderTarget ); const hasExternalTexture = this._hasExternalTexture( renderContext ); let descriptors = renderTargetData.descriptors; if ( descriptors === undefined || renderTargetData.width !== renderTarget.width || renderTargetData.height !== renderTarget.height || renderTargetData.samples !== renderTarget.samples || hasExternalTexture ) { descriptors = {}; renderTargetData.descriptors = descriptors; } const cacheKey = renderContext.getCacheKey(); let descriptorBase = descriptors[ cacheKey ]; if ( descriptorBase === undefined || hasExternalTexture ) { const textures = renderContext.textures; const textureViews = []; let sliceIndex; const isRenderCameraDepthArray = this._isRenderCameraDepthArray( renderContext ); for ( let i = 0; i < textures.length; i ++ ) { const textureData = this.get( textures[ i ] ); if ( textureData.externalTexture === true ) { textureViews.push( ...this._createExternalTextureViews( renderContext, textureData ) ); continue; } _viewDescriptor.label = `colorAttachment_${ i }`; _viewDescriptor.baseMipLevel = renderContext.activeMipmapLevel; _viewDescriptor.mipLevelCount = 1; _viewDescriptor.baseArrayLayer = renderContext.activeCubeFace; _viewDescriptor.arrayLayerCount = 1; _viewDescriptor.dimension = GPUTextureViewDimension.TwoD; if ( renderTarget.isRenderTarget3D ) { sliceIndex = renderContext.activeCubeFace; _viewDescriptor.baseArrayLayer = 0; _viewDescriptor.dimension = GPUTextureViewDimension.ThreeD; } else if ( renderTarget.isRenderTarget && textures[ i ].image.depth > 1 ) { if ( isRenderCameraDepthArray === true ) { const cameras = renderContext.camera.cameras; for ( let layer = 0; layer < cameras.length; layer ++ ) { _viewDescriptor.baseArrayLayer = layer; _viewDescriptor.arrayLayerCount = 1; _viewDescriptor.dimension = GPUTextureViewDimension.TwoD; const textureView = textureData.texture.createView( _viewDescriptor ); textureViews.push( { view: textureView, resolveTarget: undefined, depthSlice: undefined } ); } } else { _viewDescriptor.dimension = GPUTextureViewDimension.TwoDArray; } } if ( isRenderCameraDepthArray !== true ) { const textureView = textureData.texture.createView( _viewDescriptor ); let view, resolveTarget; if ( textureData.msaaTexture !== undefined ) { view = textureData.msaaTexture.createView(); resolveTarget = textureView; } else { view = textureView; resolveTarget = undefined; } textureViews.push( { view, resolveTarget, depthSlice: sliceIndex } ); } _viewDescriptor.reset(); } const colorAttachments = []; for ( let i = 0; i < textureViews.length; i ++ ) { const viewInfo = textureViews[ i ]; const attachment = new GPURenderPassColorAttachment(); attachment.view = viewInfo.view; attachment.depthSlice = viewInfo.depthSlice; attachment.resolveTarget = viewInfo.resolveTarget; colorAttachments.push( attachment ); } descriptorBase = { textureViews, colorAttachments, descriptor: new GPURenderPassDescriptor() }; if ( renderContext.depth ) { const depthTextureData = this.get( renderContext.depthTexture ); if ( renderContext.depthTexture.isArrayTexture || renderContext.depthTexture.isCubeTexture ) { _viewDescriptor.dimension = GPUTextureViewDimension.TwoD; _viewDescriptor.arrayLayerCount = 1; _viewDescriptor.baseArrayLayer = renderContext.activeCubeFace; } const depthStencilAttachment = new GPURenderPassDepthStencilAttachment(); depthStencilAttachment.view = depthTextureData.texture.createView( _viewDescriptor ); descriptorBase.depthStencilAttachment = depthStencilAttachment; _viewDescriptor.reset(); } descriptors[ cacheKey ] = descriptorBase; renderTargetData.width = renderTarget.width; renderTargetData.height = renderTarget.height; renderTargetData.samples = renderTarget.samples; renderTargetData.activeMipmapLevel = renderContext.activeMipmapLevel; renderTargetData.activeCubeFace = renderContext.activeCubeFace; } const descriptor = descriptorBase.descriptor; descriptor.reset(); // Apply dynamic properties to cached attachments for ( let i = 0; i < descriptorBase.colorAttachments.length; i ++ ) { const attachment = descriptorBase.colorAttachments[ i ]; let clearValue = { r: 0, g: 0, b: 0, a: 1 }; if ( i === 0 && colorAttachmentsConfig.clearValue ) { clearValue = colorAttachmentsConfig.clearValue; } attachment.loadOp = colorAttachmentsConfig.loadOp || GPULoadOp.Load; attachment.storeOp = colorAttachmentsConfig.storeOp || GPUStoreOp.Store; attachment.clearValue = clearValue; descriptor.colorAttachments.push( attachment ); } if ( descriptorBase.depthStencilAttachment ) { descriptor.depthStencilAttachment = descriptorBase.depthStencilAttachment; } return descriptor; } /** * This method is executed at the beginning of a render call and prepares * the WebGPU state for upcoming render calls * * @param {RenderContext} renderContext - The render context. */ beginRender( renderContext ) { const renderContextData = this.get( renderContext ); // const device = this.device; const occlusionQueryCount = renderContext.occlusionQueryCount; let occlusionQuerySet; if ( occlusionQueryCount > 0 ) { if ( renderContextData.currentOcclusionQuerySet ) renderContextData.currentOcclusionQuerySet.destroy(); if ( renderContextData.currentOcclusionQueryBuffer ) renderContextData.currentOcclusionQueryBuffer.destroy(); // Get a reference to the array of objects with queries. The renderContextData property // can be changed by another render pass before the buffer.mapAsyc() completes. renderContextData.currentOcclusionQuerySet = renderContextData.occlusionQuerySet; renderContextData.currentOcclusionQueryBuffer = renderContextData.occlusionQueryBuffer; renderContextData.currentOcclusionQueryObjects = renderContextData.occlusionQueryObjects; // _querySetDescriptor.label = `occlusionQuerySet_${ renderContext.id }`; _querySetDescriptor.type = 'occlusion'; _querySetDescriptor.count = occlusionQueryCount; occlusionQuerySet = device.createQuerySet( _querySetDescriptor ); _querySetDescriptor.reset(); renderContextData.occlusionQuerySet = occlusionQuerySet; renderContextData.occlusionQueryIndex = 0; renderContextData.occlusionQueryObjects = new Array( occlusionQueryCount ); renderContextData.lastOcclusionObject = null; } let descriptor; if ( renderContext.textures === null ) { descriptor = this._getDefaultRenderPassDescriptor(); } else { descriptor = this._getRenderPassDescriptor( renderContext, { loadOp: GPULoadOp.Load } ); } this.initTimestampQuery( TimestampQuery.RENDER, this.getTimestampUID( renderContext ), descriptor ); descriptor.occlusionQuerySet = occlusionQuerySet; const depthStencilAttachment = descriptor.depthStencilAttachment; if ( renderContext.textures !== null ) { const colorAttachments = descriptor.colorAttachments; for ( let i = 0; i < colorAttachments.length; i ++ ) { const colorAttachment = colorAttachments[ i ]; if ( renderContext.clearColor ) { if ( i === 0 ) { colorAttachment.clearValue = renderContext.clearColorValue; } else { _clearValue.r = 0; _clearValue.g = 0; _clearValue.b = 0; _clearValue.a = 1; colorAttachment.clearValue = _clearValue; } colorAttachment.loadOp = GPULoadOp.Clear; } else { colorAttachment.loadOp = GPULoadOp.Load; } colorAttachment.storeOp = GPUStoreOp.Store; } } else { const colorAttachment = descriptor.colorAttachments[ 0 ]; if ( renderContext.clearColor ) { colorAttachment.clearValue = renderContext.clearColorValue; colorAttachment.loadOp = GPULoadOp.Clear; } else { colorAttachment.loadOp = GPULoadOp.Load; } colorAttachment.storeOp = GPUStoreOp.Store; } // if ( renderContext.depth ) { if ( renderContext.clearDepth ) { depthStencilAttachment.depthClearValue = renderContext.clearDepthValue; depthStencilAttachment.depthLoadOp = GPULoadOp.Clear; } else { depthStencilAttachment.depthLoadOp = GPULoadOp.Load; } depthStencilAttachment.depthStoreOp = GPUStoreOp.Store; } if ( renderContext.stencil ) { if ( renderContext.clearStencil ) { depthStencilAttachment.stencilClearValue = renderContext.clearStencilValue; depthStencilAttachment.stencilLoadOp = GPULoadOp.Clear; } else { depthStencilAttachment.stencilLoadOp = GPULoadOp.Load; } depthStencilAttachment.stencilStoreOp = GPUStoreOp.Store; } // _commandEncoderDescriptor.label = 'renderContext_' + renderContext.id; const encoder = device.createCommandEncoder( _commandEncoderDescriptor ); _commandEncoderDescriptor.reset(); // Layered render targets: prepare bundle encoders for each camera in the array camera. if ( this._isRenderCameraDepthArray( renderContext ) === true ) { const cameras = renderContext.camera.cameras; if ( ! renderContextData.layerDescriptors || renderContextData.layerDescriptors.length !== cameras.length ) { this._createArrayCameraLayerDescriptors( renderContext, renderContextData, descriptor, cameras ); } else { this._updateArrayCameraLayerDescriptors( renderContext, renderContextData, cameras ); } // Create bundle encoders for each layer renderContextData.bundleEncoders = []; renderContextData.bundleSets = []; // Create separate bundle encoders for each camera in the array for ( let i = 0; i < cameras.length; i ++ ) { const bundleEncoder = this.pipelineUtils.createBundleEncoder( renderContext, 'renderBundleArrayCamera_' + i ); // Initialize state tracking for this bundle const bundleSets = { attributes: {}, bindingGroups: [], pipeline: null, index: null }; renderContextData.bundleEncoders.push( bundleEncoder ); renderContextData.bundleSets.push( bundleSets ); } // We'll complete the bundles in finishRender renderContextData.currentPass = null; } else { const currentPass = encoder.beginRenderPass( descriptor ); renderContextData.currentPass = currentPass; if ( renderContext.viewport ) { this.updateViewport( renderContext ); } if ( renderContext.scissor ) { this.updateScissor( renderContext ); } } // renderContextData.descriptor = descriptor; renderContextData.encoder = encoder; renderContextData.currentSets = { attributes: {}, bindingGroups: [], pipeline: null, index: null }; renderContextData.renderBundles = []; } /** * Creates render pass descriptors for each camera in an array camera. * * @param {RenderContext} renderContext - The render context. * @param {Object} renderContextData - The render context data. * @param {Object} descriptor - The render pass descriptor. * @param {ArrayCamera} cameras - The array camera. * * @private */ _createArrayCameraLayerDescriptors( renderContext, renderContextData, descriptor, cameras ) { const depthStencilAttachment = descriptor.depthStencilAttachment; renderContextData.layerDescriptors = []; const depthTextureData = this.get( renderContext.depthTexture ); if ( ! depthTextureData.viewCache ) { depthTextureData.viewCache = []; } for ( let i = 0; i < cameras.length; i ++ ) { const sourceAttachment = descriptor.colorAttachments[ 0 ]; const layerColorAttachment = new GPURenderPassColorAttachment(); layerColorAttachment.view = descriptor.colorAttachments[ i ].view; layerColorAttachment.depthSlice = sourceAttachment.depthSlice; layerColorAttachment.resolveTarget = sourceAttachment.resolveTarget; layerColorAttachment.loadOp = sourceAttachment.loadOp; layerColorAttachment.storeOp = sourceAttachment.storeOp; layerColorAttachment.clearValue = sourceAttachment.clearValue; const layerDescriptor = new GPURenderPassDescriptor(); layerDescriptor.label = descriptor.label; layerDescriptor.occlusionQuerySet = descriptor.occlusionQuerySet; layerDescriptor.timestampWrites = descriptor.timestampWrites; layerDescriptor.colorAttachments.push( layerColorAttachment ); if ( descriptor.depthStencilAttachment ) { const layerIndex = i; if ( ! depthTextureData.viewCache[ layerIndex ] ) { _viewDescriptor.dimension = GPUTextureViewDimension.TwoD; _viewDescriptor.baseArrayLayer = i; _viewDescriptor.arrayLayerCount = 1; depthTextureData.viewCache[ layerIndex ] = depthTextureData.texture.createView( _viewDescriptor ); _viewDescriptor.reset(); } const layerDepthStencilAttachment = new GPURenderPassDepthStencilAttachment(); layerDepthStencilAttachment.view = depthTextureData.viewCache[ layerIndex ]; layerDepthStencilAttachment.depthLoadOp = depthStencilAttachment.depthLoadOp || GPULoadOp.Clear; layerDepthStencilAttachment.depthStoreOp = depthStencilAttachment.depthStoreOp || GPUStoreOp.Store; layerDepthStencilAttachment.depthClearValue = depthStencilAttachment.depthClearValue || 1.0; if ( renderContext.stencil ) { layerDepthStencilAttachment.stencilLoadOp = depthStencilAttachment.stencilLoadOp; layerDepthStencilAttachment.stencilStoreOp = depthStencilAttachment.stencilStoreOp; layerDepthStencilAttachment.stencilClearValue = depthStencilAttachment.stencilClearValue; } layerDescriptor.depthStencilAttachment = layerDepthStencilAttachment; } else { const layerDepthStencilAttachment = new GPURenderPassDepthStencilAttachment(); layerDepthStencilAttachment.view = depthStencilAttachment.view; layerDepthStencilAttachment.depthLoadOp = depthStencilAttachment.depthLoadOp; layerDepthStencilAttachment.depthStoreOp = depthStencilAttachment.depthStoreOp; layerDepthStencilAttachment.depthClearValue = depthStencilAttachment.depthClearValue; layerDepthStencilAttachment.depthReadOnly = depthStencilAttachment.depthReadOnly; layerDepthStencilAttachment.stencilLoadOp = depthStencilAttachment.stencilLoadOp; layerDepthStencilAttachment.stencilStoreOp = depthStencilAttachment.stencilStoreOp; layerDepthStencilAttachment.stencilClearValue = depthStencilAttachment.stencilClearValue; layerDepthStencilAttachment.stencilReadOnly = depthStencilAttachment.stencilReadOnly; layerDescriptor.depthStencilAttachment = layerDepthStencilAttachment; } renderContextData.layerDescriptors.push( layerDescriptor ); } } /** * Updates render pass descriptors for each camera in an array camera. * * @param {RenderContext} renderContext - The render context. * @param {Object} renderContextData - The render context data. * @param {ArrayCamera} cameras - The array camera. * */ _updateArrayCameraLayerDescriptors( renderContext, renderContextData, cameras ) { for ( let i = 0; i < cameras.length; i ++ ) { const layerDescriptor = renderContextData.layerDescriptors[ i ]; if ( layerDescriptor.depthStencilAttachment ) { const depthAttachment = layerDescriptor.depthStencilAttachment; if ( renderContext.depth ) { if ( renderContext.clearDepth ) { depthAttachment.depthClearValue = renderContext.clearDepthValue; depthAttachment.depthLoadOp = GPULoadOp.Clear; } else { depthAttachment.depthLoadOp = GPULoadOp.Load; } } if ( renderContext.stencil ) { if ( renderContext.clearStencil ) { depthAttachment.stencilClearValue = renderContext.clearStencilValue; depthAttachment.stencilLoadOp = GPULoadOp.Clear; } else { depthAttachment.stencilLoadOp = GPULoadOp.Load; } } } } } /** * This method is executed at the end of a render call and finalizes work * after draw calls. * * @param {RenderContext} renderContext - The render context. */ finishRender( renderContext ) { const renderContextData = this.get( renderContext ); const occlusionQueryCount = renderContext.occlusionQueryCount; if ( renderContextData.renderBundles.length > 0 ) { renderContextData.currentPass.executeBundles( renderContextData.renderBundles ); } if ( occlusionQueryCount > renderContextData.occlusionQueryIndex ) { renderContextData.currentPass.endOcclusionQuery(); } // Layered render targets: execute the bundle for each layer. const encoder = renderContextData.encoder; if ( this._isRenderCameraDepthArray( renderContext ) === true ) { const bundles = []; for ( let i = 0; i < renderContextData.bundleEncoders.length; i ++ ) { const bundleEncoder = renderContextData.bundleEncoders[ i ]; bundles.push( bundleEncoder.finish() ); } for ( let i = 0; i < renderContextData.layerDescriptors.length; i ++ ) { if ( i < bundles.length ) { const layerDescriptor = renderContextData.layerDescriptors[ i ]; const renderPass = encoder.beginRenderPass( layerDescriptor ); if ( renderContext.viewport ) { const { x, y, width, height, minDepth, maxDepth } = renderContext.viewportValue; renderPass.setViewport( x, y, width, height, minDepth, maxDepth ); } if ( renderContext.scissor ) { const { x, y, width, height } = renderContext.scissorValue; renderPass.setScissorRect( x, y, width, height ); } renderPass.executeBundles( [ bundles[ i ] ] ); renderPass.end(); } } } else if ( renderContextData.currentPass ) { renderContextData.currentPass.end(); } if ( occlusionQueryCount > 0 ) { const bufferSize = occlusionQueryCount * 8; // 8 byte entries for query results // let queryResolveBuffer = this.occludedResolveCache.get( bufferSize ); if ( queryResolveBuffer === undefined ) { _bufferDescriptor.size = bufferSize; _bufferDescriptor.usage = GPUBufferUsage.QUERY_RESOLVE | GPUBufferUsage.COPY_SRC; queryResolveBuffer = this.device.createBuffer( _bufferDescriptor ); _bufferDescriptor.reset(); this.occludedResolveCache.set( bufferSize, queryResolveBuffer ); } // _bufferDescriptor.size = bufferSize; _bufferDescriptor.usage = GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ; const readBuffer = this.device.createBuffer( _bufferDescriptor ); _bufferDescriptor.reset(); // two buffers required here - WebGPU doesn't allow usage of QUERY_RESOLVE & MAP_READ to be combined renderContextData.encoder.resolveQuerySet( renderContextData.occlusionQuerySet, 0, occlusionQueryCount, queryResolveBuffer, 0 ); renderContextData.encoder.copyBufferToBuffer( queryResolveBuffer, 0, readBuffer, 0, bufferSize ); renderContextData.occlusionQueryBuffer = readBuffer; // this.resolveOccludedAsync( renderContext ); } submit( this.device, renderContextData.encoder.finish() ); // if ( renderContext.textures !== null ) { const textures = renderContext.textures; for ( let i = 0; i < textures.length; i ++ ) { const texture = textures[ i ]; if ( texture.generateMipmaps === true ) { this.textureUtils.generateMipmaps( texture ); } } } } /** * Returns `true` if the given 3D object is fully occluded by other * 3D objects in the scene. * * @param {RenderContext} renderContext - The render context. * @param {Object3D} object - The 3D object to test. * @return {boolean} Whether the 3D object is fully occluded or not. */ isOccluded( renderContext, object ) { const renderContextData = this.get( renderContext ); return renderContextData.occluded && renderContextData.occluded.has( object ); } /** * This method processes the result of occlusion queries and writes it * into render context data. * * @async * @param {RenderContext} renderContext - The render context. * @return {Promise} A Promise that resolves when the occlusion query results have been processed. */ async resolveOccludedAsync( renderContext ) { const renderContextData = this.get( renderContext ); // handle occlusion query results const { currentOcclusionQueryBuffer, currentOcclusionQueryObjects } = renderContextData; if ( currentOcclusionQueryBuffer && currentOcclusionQueryObjects ) { const occluded = new WeakSet(); renderContextData.currentOcclusionQueryObjects = null; renderContextData.currentOcclusionQueryBuffer = null; await currentOcclusionQueryBuffer.mapAsync( GPUMapMode.READ ); const buffer = currentOcclusionQueryBuffer.getMappedRange(); const results = new BigUint64Array( buffer ); for ( let i = 0; i < currentOcclusionQueryObjects.length; i ++ ) { if ( results[ i ] === BigInt( 0 ) ) { occluded.add( currentOcclusionQueryObjects[ i ] ); } } currentOcclusionQueryBuffer.destroy(); renderContextData.occluded = occluded; } } /** * Updates the viewport with the values from the given render context. * * @param {RenderContext} renderContext - The render context. */ updateViewport( renderContext ) { const { currentPass } = this.get( renderContext ); const { x, y, width, height, minDepth, maxDepth } = renderContext.viewportValue; currentPass.setViewport( x, y, width, height, minDepth, maxDepth ); } /** * Updates the scissor with the values from the given render context. * * @param {RenderContext} renderContext - The render context. */ updateScissor( renderContext ) { const { currentPass } = this.get( renderContext ); const { x, y, width, height } = renderContext.scissorValue; currentPass.setScissorRect( x, y, width, height ); } /** * Returns the clear color and alpha into a single * color object. * * @return {Color4} The clear color. */ getClearColor() { const clearColor = super.getClearColor(); // only premultiply alpha when alphaMode is "premultiplied" if ( this.renderer.alpha === true ) { clearColor.r *= clearColor.a; clearColor.g *= clearColor.a; clearColor.b *= clearColor.a; } return clearColor; } /** * Performs a clear operation. * * @param {boolean} color - Whether the color buffer should be cleared or not. * @param {boolean} depth - Whether the depth buffer should be cleared or not. * @param {boolean} stencil - Whether the stencil buffer should be cleared or not. * @param {?RenderContext} [renderTargetContext=null] - The render context of the current set render target. */ clear( color, depth, stencil, renderTargetContext = null ) { const device = this.device; const renderer = this.renderer; let colorAttachments = []; let depthStencilAttachment; let supportsDepth; let supportsStencil; if ( color ) { const clearColor = this.getClearColor(); _clearValue.r = clearColor.r; _clearValue.g = clearColor.g; _clearValue.b = clearColor.b; _clearValue.a = clearColor.a; } if ( renderTargetContext === null ) { supportsDepth = renderer.depth; supportsStencil = renderer.stencil; const descriptor = this._getDefaultRenderPassDescriptor(); if ( color ) { colorAttachments = descriptor.colorAttachments; const colorAttachment = colorAttachments[ 0 ]; colorAttachment.clearValue = _clearValue; colorAttachment.loadOp = GPULoadOp.Clear; colorAttachment.storeOp = GPUStoreOp.Store; } if ( supportsDepth || supportsStencil ) { depthStencilAttachment = descriptor.depthStencilAttachment; } } else { supportsDepth = renderTargetContext.depth; supportsStencil = renderTargetContext.stencil; const clearConfig = { loadOp: color ? GPULoadOp.Clear : GPULoadOp.Load, clearValue: color ? _clearValue : undefined }; if ( supportsDepth ) { clearConfig.depthLoadOp = depth ? GPULoadOp.Clear : GPULoadOp.Load; clearConfig.depthClearValue = depth ? renderer.getClearDepth() : undefined; clearConfig.depthStoreOp = GPUStoreOp.Store; } if ( supportsStencil ) { clearConfig.stencilLoadOp = stencil ? GPULoadOp.Clear : GPULoadOp.Load; clearConfig.stencilClearValue = stencil ? renderer.getClearStencil() : undefined; clearConfig.stencilStoreOp = GPUStoreOp.Store; } const descriptor = this._getRenderPassDescriptor( renderTargetContext, clearConfig ); colorAttachments = descriptor.colorAttachments; depthStencilAttachment = descriptor.depthStencilAttachment; } if ( supportsDepth && depthStencilAttachment ) { if ( depth ) { depthStencilAttachment.depthLoadOp = GPULoadOp.Clear; depthStencilAttachment.depthClearValue = renderer.getClearDepth(); depthStencilAttachment.depthStoreOp = GPUStoreOp.Store; } else { depthStencilAttachment.depthLoadOp = GPULoadOp.Load; depthStencilAttachment.depthStoreOp = GPUStoreOp.Store; } } // if ( supportsStencil && depthStencilAttachment ) { if ( stencil ) { depthStencilAttachment.stencilLoadOp = GPULoadOp.Clear; depthStencilAttachment.stencilClearValue = renderer.getClearStencil(); depthStencilAttachment.stencilStoreOp = GPUStoreOp.Store; } else { depthStencilAttachment.stencilLoadOp = GPULoadOp.Load; depthStencilAttachment.stencilStoreOp = GPUStoreOp.Store; } } // _commandEncoderDescriptor.label = 'clear'; const encoder = device.createCommandEncoder( _commandEncoderDescriptor ); _commandEncoderDescriptor.reset(); const currentPass = encoder.beginRenderPass( { colorAttachments, depthStencilAttachment } ); currentPass.end(); submit( device, encoder.finish() ); } // compute /** * This method is executed at the beginning of a compute call and * prepares the state for upcoming compute tasks. * * @param {Node|Array<Node>} computeGroup - The compute node(s). */ beginCompute( computeGroup ) { const groupGPU = this.get( computeGroup ); // const label = 'computeGroup_' + computeGroup.id; _computePassDescriptor.label = label; _commandEncoderDescriptor.label = label; this.initTimestampQuery( TimestampQuery.COMPUTE, this.getTimestampUID( computeGroup ), _computePassDescriptor ); groupGPU.cmdEncoderGPU = this.device.createCommandEncoder( _commandEncoderDescriptor ); groupGPU.passEncoderGPU = groupGPU.cmdEncoderGPU.beginComputePass( _computePassDescriptor ); groupGPU.currentPipeline = null; _commandEncoderDescriptor.reset(); _computePassDescriptor.reset(); } /** * Executes a compute command for the given compute node. * * @param {Node|Array<Node>} computeGroup - The group of compute nodes of a compute call. Can be a single compute node. * @param {Node} computeNode - The compute node. * @param {Array<BindGroup>} bindings - The bindings. * @param {ComputePipeline} pipeline - The compute pipeline. * @param {number|Array<number>|IndirectStorageBufferAttribute} [dispatchSize=null] * - A single number representing count, or * - An array [x, y, z] representing dispatch size, or * - A IndirectStorageBufferAttribute for indirect dispatch size. */ compute( computeGroup, computeNode, bindings, pipeline, dispatchSize = null ) { const computeNodeData = this.get( computeNode ); const groupGPU = this.get( computeGroup ); const { passEncoderGPU } = groupGPU; // pipeline const pipelineGPU = this.get( pipeline ).pipeline; if ( groupGPU.currentPipeline !== pipelineGPU ) { passEncoderGPU.setPipeline( pipelineGPU ); groupGPU.currentPipeline = pipelineGPU; } // bind groups for ( let i = 0, l = bindings.length; i < l; i ++ ) { const bindGroup = bindings[ i ]; const bindingsData = this.get( bindGroup ); passEncoderGPU.setBindGroup( i, bindingsData.group ); } if ( dispatchSize === null ) { dispatchSize = computeNode.dispatchSize || computeNode.count; } // When the dispatchSize is set with a StorageBuffer from the GPU. if ( dispatchSize && dispatchSize.isIndirectStorageBufferAttribute ) { const dispatchBuffer = this.get( dispatchSize ).buffer; passEncoderGPU.dispatchWorkgroupsIndirect( dispatchBuffer, 0 ); return; } if ( typeof dispatchSize === 'number' ) { // If a single number is given, we calculate the dispatch size based on the workgroup size const count = dispatchSize; if ( computeNodeData.dispatchSize === undefined || computeNodeData.count !== count ) { // cache dispatch size to avoid recalculating it every time computeNodeData.dispatchSize = [ 0, 1, 1 ]; computeNodeData.count = count; const workgroupSize = computeNode.workgroupSize; let size = workgroupSize[ 0 ]; for ( let i = 1; i < workgroupSize.length; i ++ ) size *= workgroupSize[ i ]; const dispatchCount = Math.ceil( count / size ); // const maxComputeWorkgroupsPerDimension = this.device.limits.maxComputeWorkgroupsPerDimension; dispatchSize = [ dispatchCount, 1, 1 ]; if ( dispatchCount > maxComputeWorkgroupsPerDimension ) { dispatchSize[ 0 ] = Math.min( dispatchCount, maxComputeWorkgroupsPerDimension ); dispatchSize[ 1 ] = Math.ceil( dispatchCount / maxComputeWorkgroupsPerDimension ); } computeNodeData.dispatchSize = dispatchSize; } dispatchSize = computeNodeData.dispatchSize; } // passEncoderGPU.dispatchWorkgroups( dispatchSize[ 0 ], dispatchSize[ 1 ] || 1, dispatchSize[ 2 ] || 1 ); } /** * This method is executed at the end of a compute call and * finalizes work after compute tasks. * * @param {Node|Array<Node>} computeGroup - The compute node(s). */ finishCompute( computeGroup ) { const groupData = this.get( computeGroup ); groupData.passEncoderGPU.end(); submit( this.device, groupData.cmdEncoderGPU.finish() ); } /** * Internal draw function that performs the draw with the given pass encoder. * * @private * @param {RenderObject} renderObject - The render object. * @param {Info} info - Holds a series of statistical information about the GPU memory and the rendering process. * @param {Object} renderContextData - The render context data object, holding current pass state and occlusion query tracking. * @param {GPURenderPipeline} pipelineGPU - The GPU render pipeline. * @param {Array<BindGroup>} bindings - The bind groups. * @param {Array<BufferAttribute>} vertexBuffers - The vertex buffers. * @param {{vertexCount: number, firstVertex: number, instanceCount: number, firstInstance: number}} drawParams - The draw parameters. * @param {GPURenderPassEncoder|GPURenderBundleEncoder} passEncoderGPU - The GPU pass encoder used for recording draw commands. * @param {Object} currentSets - Tracking object for currently set pipeline, attributes, bind groups, and index state. */ _draw( renderObject, info, renderContextData, pipelineGPU, bindings, vertexBuffers, drawParams, passEncoderGPU, currentSets ) { const { object, material, context } = renderObject; const index = renderObject.getIndex(); const hasIndex = ( index !== null ); // pipeline if ( currentSets.pipeline !== pipelineGPU ) { passEncoderGPU.setPipeline( pipelineGPU ); currentSets.pipeline = pipelineGPU; } // bind groups const currentBindingGroups = currentSets.bindingGroups; for ( let i = 0, l = bindings.length; i < l; i ++ ) { const bindGroup = bindings[ i ]; if ( currentBindingGroups[ i ] !== bindGroup.id ) { const bindingsData = this.get( bindGroup ); passEncoderGPU.setBindGroup( i, bindingsData.group ); currentBindingGroups[ i ] = bindGroup.id; } } // attributes // index if ( hasIndex === true ) { if ( currentSets.index !== index ) { const buffer = this.get( index ).buffer; const indexFormat = ( index.array instanceof Uint16Array ) ? GPUIndexFormat.Uint16 : GPUIndexFormat.Uint32; passEncoderGPU.setIndexBuffer( buffer, indexFormat ); currentSets.index = index; } } for ( let i = 0, l = vertexBuffers.length; i < l; i ++ ) { const vertexBuffer = vertexBuffers[ i ]; if ( currentSets.attributes[ i ] !== vertexBuffer ) { const buffer = this.get( vertexBuffer ).buffer; passEncoderGPU.setVertexBuffer( i, buffer ); currentSets.attributes[ i ] = vertexBuffer; } } // stencil if ( context.stencil === true && material.stencilWrite === true && renderContextData.currentStencilRef !== material.stencilRef ) { passEncoderGPU.setStencilReference( material.stencilRef ); renderContextData.currentStencilRef = material.stencilRef; } if ( object.isBatchedMesh === true ) { const starts = object._multiDrawStarts; const counts = object._multiDr