diff --git a/examples/files.json b/examples/files.json index 4690f643307c29..663e4505222897 100644 --- a/examples/files.json +++ b/examples/files.json @@ -408,6 +408,7 @@ "webgpu_procedural_texture", "webgpu_reflection", "webgpu_refraction", + "webgpu_rendertarget_2d-array_3d", "webgpu_rtt", "webgpu_sandbox", "webgpu_shadertoy", diff --git a/examples/jsm/helpers/TextureHelperGPU.js b/examples/jsm/helpers/TextureHelperGPU.js index 225be3f5842098..3f48dd40ddcb59 100644 --- a/examples/jsm/helpers/TextureHelperGPU.js +++ b/examples/jsm/helpers/TextureHelperGPU.js @@ -1,11 +1,13 @@ import { + NodeMaterial, BoxGeometry, BufferAttribute, Mesh, PlaneGeometry, + DoubleSide, Vector3, } from 'three'; -import { NodeMaterial, texture as textureNode, cubeTexture, texture3D, float, vec4 } from 'three/tsl'; +import { texture as textureNode, cubeTexture, texture3D, float, vec4, attribute } from 'three/tsl'; import { mergeGeometries } from '../utils/BufferGeometryUtils.js'; class TextureHelper extends Mesh { @@ -13,17 +15,25 @@ class TextureHelper extends Mesh { constructor( texture, width = 1, height = 1, depth = 1 ) { const material = new NodeMaterial(); + material.side = DoubleSide; + material.transparent = true; material.name = 'TextureHelper'; let colorNode; + const uvw = attribute( 'uvw' ); + if ( texture.isCubeTexture ) { - colorNode = cubeTexture( texture ); + colorNode = cubeTexture( texture ).sample( uvw ); } else if ( texture.isData3DTexture || texture.isCompressed3DTexture ) { - colorNode = texture3D( texture ); + colorNode = texture3D( texture ).sample( uvw ); + + } else if ( texture.isDataArrayTexture || texture.isCompressedArrayTexture ) { + + colorNode = textureNode( texture ).sample( uvw.xy ).depth( uvw.z ); } else { @@ -122,7 +132,7 @@ function createCubeGeometry( width, height, depth ) { } geometry.deleteAttribute( 'uv' ); - geometry.setAttribute( 'uv', uvw ); + geometry.setAttribute( 'uvw', uvw ); return geometry; @@ -162,7 +172,7 @@ function createSliceGeometry( texture, width, height, depth ) { } geometry.deleteAttribute( 'uv' ); - geometry.setAttribute( 'uv', uvw ); + geometry.setAttribute( 'uvw', uvw ); geometries.push( geometry ); diff --git a/examples/screenshots/webgpu_rendertarget_2d-array_3d.jpg b/examples/screenshots/webgpu_rendertarget_2d-array_3d.jpg new file mode 100644 index 00000000000000..da9ddafedad849 Binary files /dev/null and b/examples/screenshots/webgpu_rendertarget_2d-array_3d.jpg differ diff --git a/examples/screenshots/webgpu_textures_2d-array_compressed.jpg b/examples/screenshots/webgpu_textures_2d-array_compressed.jpg index f3b39b56d4154d..c92bba94c8d2d4 100644 Binary files a/examples/screenshots/webgpu_textures_2d-array_compressed.jpg and b/examples/screenshots/webgpu_textures_2d-array_compressed.jpg differ diff --git a/examples/tags.json b/examples/tags.json index b103ba8298fd06..0ca05a0356c5f6 100644 --- a/examples/tags.json +++ b/examples/tags.json @@ -144,6 +144,7 @@ "webgpu_postprocessing_ssaa": [ "msaa", "multisampled" ], "webgpu_refraction": [ "water" ], "webgpu_rtt": [ "renderTarget", "texture" ], + "webgpu_rendertarget_2d-array_3d": [ "renderTarget", "2d-array", "3d" ], "webgpu_sky": [ "sun" ], "webgpu_tonemapping": [ "gltf" ], "webgpu_tsl_compute_attractors_particles": [ "gpgpu" ], diff --git a/examples/webgpu_rendertarget_2d-array_3d.html b/examples/webgpu_rendertarget_2d-array_3d.html new file mode 100644 index 00000000000000..c1565a64c36113 --- /dev/null +++ b/examples/webgpu_rendertarget_2d-array_3d.html @@ -0,0 +1,339 @@ + + + + three.js webgpu - RenderTargetArray and RenderTarget3D + + + + + + + +
+ three.js - WebGPU - RenderTargetArray and RenderTarget3D
+
+
DataArrayTexture
+
Data3DTexture
+
RenderTarget3D
+
RenderTargetArray
+ + + + + + \ No newline at end of file diff --git a/src/Three.Core.js b/src/Three.Core.js index f7a10261f0014e..caf8342cd5a5ae 100644 --- a/src/Three.Core.js +++ b/src/Three.Core.js @@ -85,6 +85,8 @@ export { AnimationMixer } from './animation/AnimationMixer.js'; export { AnimationClip } from './animation/AnimationClip.js'; export { AnimationAction } from './animation/AnimationAction.js'; export { RenderTarget } from './core/RenderTarget.js'; +export { RenderTarget3D } from './core/RenderTarget3D.js'; +export { RenderTargetArray } from './core/RenderTargetArray.js'; export { Uniform } from './core/Uniform.js'; export { UniformsGroup } from './core/UniformsGroup.js'; export { InstancedBufferGeometry } from './core/InstancedBufferGeometry.js'; diff --git a/src/core/RenderTarget3D.js b/src/core/RenderTarget3D.js new file mode 100644 index 00000000000000..9cd41bb2e2d318 --- /dev/null +++ b/src/core/RenderTarget3D.js @@ -0,0 +1,22 @@ +import { RenderTarget } from './RenderTarget.js'; +import { Data3DTexture } from '../textures/Data3DTexture.js'; + +class RenderTarget3D extends RenderTarget { + + constructor( width = 1, height = 1, depth = 1, options = {} ) { + + super( width, height, options ); + + this.isRenderTarget3D = true; + + this.depth = depth; + + this.texture = new Data3DTexture( null, width, height, depth ); + + this.texture.isRenderTargetTexture = true; + + } + +} + +export { RenderTarget3D }; diff --git a/src/core/RenderTargetArray.js b/src/core/RenderTargetArray.js new file mode 100644 index 00000000000000..7c4b2a25300953 --- /dev/null +++ b/src/core/RenderTargetArray.js @@ -0,0 +1,22 @@ +import { RenderTarget } from './RenderTarget.js'; +import { DataArrayTexture } from '../textures/DataArrayTexture.js'; + +class RenderTargetArray extends RenderTarget { + + constructor( width = 1, height = 1, depth = 1, options = {} ) { + + super( width, height, options ); + + this.isRenderTargetArray = true; + + this.depth = depth; + + this.texture = new DataArrayTexture( null, width, height, depth ); + + this.texture.isRenderTargetTexture = true; + + } + +} + +export { RenderTargetArray }; diff --git a/src/nodes/accessors/Texture3DNode.js b/src/nodes/accessors/Texture3DNode.js index e1f11a326e97d5..71b561ea10054c 100644 --- a/src/nodes/accessors/Texture3DNode.js +++ b/src/nodes/accessors/Texture3DNode.js @@ -1,5 +1,6 @@ import TextureNode from './TextureNode.js'; -import { nodeProxy, vec3, Fn, If } from '../tsl/TSLBase.js'; +import { nodeProxy, vec3, Fn, If, int } from '../tsl/TSLBase.js'; +import { textureSize } from './TextureSizeNode.js'; /** @module Texture3DNode **/ @@ -125,6 +126,22 @@ class Texture3DNode extends TextureNode { */ setupUV( builder, uvNode ) { + const texture = this.value; + + if ( builder.isFlipY() && ( texture.isRenderTargetTexture === true || texture.isFramebufferTexture === true ) ) { + + if ( this.sampler ) { + + uvNode = uvNode.flipY(); + + } else { + + uvNode = uvNode.setY( int( textureSize( this, this.levelNode ).y ).sub( uvNode.y ).sub( 1 ) ); + + } + + } + return uvNode; } diff --git a/src/renderers/common/RenderContexts.js b/src/renderers/common/RenderContexts.js index cdbfc8c26d8100..2e1a9c65a9d6b9 100644 --- a/src/renderers/common/RenderContexts.js +++ b/src/renderers/common/RenderContexts.js @@ -9,9 +9,18 @@ class RenderContexts { } - get( scene, camera, renderTarget = null ) { + get( scene = null, camera = null, renderTarget = null ) { + + const chainKey = []; + if ( scene !== null ) chainKey.push( scene ); + if ( camera !== null ) chainKey.push( camera ); + + if ( chainKey.length === 0 ) { + + chainKey.push( { id: 'default' } ); + + } - const chainKey = [ scene, camera ]; let attachmentState; diff --git a/src/renderers/common/Renderer.js b/src/renderers/common/Renderer.js index 140074613b420a..9685d1d943ab43 100644 --- a/src/renderers/common/Renderer.js +++ b/src/renderers/common/Renderer.js @@ -1053,17 +1053,26 @@ class Renderer { const renderTarget = this._renderTarget || this._getFrameBufferTarget(); - let renderTargetData = null; + let renderContext = null; if ( renderTarget !== null ) { this._textures.updateRenderTarget( renderTarget ); - renderTargetData = this._textures.get( renderTarget ); + const renderTargetData = this._textures.get( renderTarget ); + + renderContext = this._renderContexts.get( null, null, renderTarget ); + renderContext.textures = renderTargetData.textures; + renderContext.depthTexture = renderTargetData.depthTexture; + renderContext.width = renderTargetData.width; + renderContext.height = renderTargetData.height; + renderContext.renderTarget = renderTarget; + renderContext.depth = renderTarget.depthBuffer; + renderContext.stencil = renderTarget.stencilBuffer; } - this.backend.clear( color, depth, stencil, renderTargetData ); + this.backend.clear( color, depth, stencil, renderContext ); if ( renderTarget !== null && this._renderTarget === null ) { diff --git a/src/renderers/webgl-fallback/WebGLBackend.js b/src/renderers/webgl-fallback/WebGLBackend.js index a7e775991f1ca2..14507794fdd504 100644 --- a/src/renderers/webgl-fallback/WebGLBackend.js +++ b/src/renderers/webgl-fallback/WebGLBackend.js @@ -1337,6 +1337,8 @@ class WebGLBackend extends Backend { const { samples, depthBuffer, stencilBuffer } = renderTarget; const isCube = renderTarget.isWebGLCubeRenderTarget === true; + const isRenderTarget3D = renderTarget.isRenderTarget3D === true; + const isRenderTargetArray = renderTarget.isRenderTargetArray === true; let msaaFb = renderTargetContextData.msaaFrameBuffer; let depthRenderbuffer = renderTargetContextData.depthRenderbuffer; @@ -1390,7 +1392,19 @@ class WebGLBackend extends Backend { const attachment = gl.COLOR_ATTACHMENT0 + i; - gl.framebufferTexture2D( gl.FRAMEBUFFER, attachment, gl.TEXTURE_2D, textureData.textureGPU, 0 ); + if ( isRenderTarget3D || isRenderTargetArray ) { + + const layer = this.renderer._activeCubeFace; + + gl.framebufferTextureLayer( gl.FRAMEBUFFER, attachment, textureData.textureGPU, 0, layer ); + + } else { + + gl.framebufferTexture2D( gl.FRAMEBUFFER, attachment, gl.TEXTURE_2D, textureData.textureGPU, 0 ); + + } + + } diff --git a/src/renderers/webgpu/WebGPUBackend.js b/src/renderers/webgpu/WebGPUBackend.js index 795d4717c10d0f..694488fdc10d75 100644 --- a/src/renderers/webgpu/WebGPUBackend.js +++ b/src/renderers/webgpu/WebGPUBackend.js @@ -206,7 +206,7 @@ class WebGPUBackend extends Backend { } - _getRenderPassDescriptor( renderContext ) { + _getRenderPassDescriptor( renderContext, colorAttachmentsConfig = {} ) { const renderTarget = renderContext.renderTarget; const renderTargetData = this.get( renderTarget ); @@ -216,8 +216,11 @@ class WebGPUBackend extends Backend { if ( descriptors === undefined || renderTargetData.width !== renderTarget.width || renderTargetData.height !== renderTarget.height || + renderTargetData.dimensions !== renderTarget.dimensions || renderTargetData.activeMipmapLevel !== renderTarget.activeMipmapLevel || - renderTargetData.samples !== renderTarget.samples + renderTargetData.activeCubeFace !== renderContext.activeCubeFace || + renderTargetData.samples !== renderTarget.samples || + renderTargetData.loadOp !== colorAttachmentsConfig.loadOp ) { descriptors = {}; @@ -247,16 +250,37 @@ class WebGPUBackend extends Backend { const textures = renderContext.textures; const colorAttachments = []; + let sliceIndex; + for ( let i = 0; i < textures.length; i ++ ) { const textureData = this.get( textures[ i ] ); - const textureView = textureData.texture.createView( { + const viewDescriptor = { + label: `colorAttachment_${ i }`, baseMipLevel: renderContext.activeMipmapLevel, mipLevelCount: 1, baseArrayLayer: renderContext.activeCubeFace, + arrayLayerCount: 1, dimension: GPUTextureViewDimension.TwoD - } ); + }; + + if ( renderTarget.isRenderTarget3D ) { + + sliceIndex = renderContext.activeCubeFace; + + viewDescriptor.baseArrayLayer = 0; + viewDescriptor.dimension = GPUTextureViewDimension.ThreeD; + viewDescriptor.depthOrArrayLayers = textures[ i ].image.depth; + + } else if ( renderTarget.isRenderTargetArray ) { + + viewDescriptor.dimension = GPUTextureViewDimension.TwoDArray; + viewDescriptor.depthOrArrayLayers = textures[ i ].image.depth; + + } + + const textureView = textureData.texture.createView( viewDescriptor ); let view, resolveTarget; @@ -274,9 +298,11 @@ class WebGPUBackend extends Backend { colorAttachments.push( { view, + depthSlice: sliceIndex, resolveTarget, loadOp: GPULoadOp.Load, - storeOp: GPUStoreOp.Store + storeOp: GPUStoreOp.Store, + ...colorAttachmentsConfig } ); } @@ -302,7 +328,11 @@ class WebGPUBackend extends Backend { renderTargetData.width = renderTarget.width; renderTargetData.height = renderTarget.height; renderTargetData.samples = renderTarget.samples; - renderTargetData.activeMipmapLevel = renderTarget.activeMipmapLevel; + renderTargetData.activeMipmapLevel = renderContext.activeMipmapLevel; + renderTargetData.activeCubeFace = renderContext.activeCubeFace; + renderTargetData.dimensions = renderTarget.dimensions; + renderTargetData.depthSlice = sliceIndex; + renderTargetData.loadOp = colorAttachments[ 0 ].loadOp; } @@ -350,7 +380,7 @@ class WebGPUBackend extends Backend { } else { - descriptor = this._getRenderPassDescriptor( renderContext ); + descriptor = this._getRenderPassDescriptor( renderContext, { loadOp: GPULoadOp.Load } ); } @@ -612,7 +642,7 @@ class WebGPUBackend extends Backend { } - clear( color, depth, stencil, renderTargetData = null ) { + clear( color, depth, stencil, renderTargetContext = null ) { const device = this.device; const renderer = this.renderer; @@ -645,7 +675,7 @@ class WebGPUBackend extends Backend { } - if ( renderTargetData === null ) { + if ( renderTargetContext === null ) { supportsDepth = renderer.depth; supportsStencil = renderer.stencil; @@ -672,45 +702,20 @@ class WebGPUBackend extends Backend { } else { - supportsDepth = renderTargetData.depth; - supportsStencil = renderTargetData.stencil; + supportsDepth = renderTargetContext.depth; + supportsStencil = renderTargetContext.stencil; if ( color ) { - for ( const texture of renderTargetData.textures ) { + const descriptor = this._getRenderPassDescriptor( renderTargetContext, { loadOp: GPULoadOp.Clear } ); - const textureData = this.get( texture ); - const textureView = textureData.texture.createView(); - - let view, resolveTarget; - - if ( textureData.msaaTexture !== undefined ) { - - view = textureData.msaaTexture.createView(); - resolveTarget = textureView; - - } else { - - view = textureView; - resolveTarget = undefined; - - } - - colorAttachments.push( { - view, - resolveTarget, - clearValue, - loadOp: GPULoadOp.Clear, - storeOp: GPUStoreOp.Store - } ); - - } + colorAttachments = descriptor.colorAttachments; } if ( supportsDepth || supportsStencil ) { - const depthTextureData = this.get( renderTargetData.depthTexture ); + const depthTextureData = this.get( renderTargetContext.depthTexture ); depthStencilAttachment = { view: depthTextureData.texture.createView() diff --git a/src/renderers/webgpu/nodes/WGSLNodeBuilder.js b/src/renderers/webgpu/nodes/WGSLNodeBuilder.js index 497499f2d918be..e46ec3b4c6260d 100644 --- a/src/renderers/webgpu/nodes/WGSLNodeBuilder.js +++ b/src/renderers/webgpu/nodes/WGSLNodeBuilder.js @@ -241,7 +241,7 @@ class WGSLNodeBuilder extends NodeBuilder { generateWrapFunction( texture ) { - const functionName = `tsl_coord_${ wrapNames[ texture.wrapS ] }S_${ wrapNames[ texture.wrapT ] }T`; + const functionName = `tsl_coord_${ wrapNames[ texture.wrapS ] }S_${ wrapNames[ texture.wrapT ] }_${texture.isData3DTexture ? '3d' : '2d'}T`; let nodeCode = wgslCodeCache[ functionName ]; @@ -249,7 +249,9 @@ class WGSLNodeBuilder extends NodeBuilder { const includes = []; - let code = `fn ${ functionName }( coord : vec2f ) -> vec2f {\n\n\treturn vec2f(\n`; + // For 3D textures, use vec3f; for texture arrays, keep vec2f since array index is separate + const coordType = texture.isData3DTexture ? 'vec3f' : 'vec2f'; + let code = `fn ${functionName}( coord : ${coordType} ) -> ${coordType} {\n\n\treturn ${coordType}(\n`; const addWrapSnippet = ( wrap, axis ) => { @@ -287,6 +289,13 @@ class WGSLNodeBuilder extends NodeBuilder { addWrapSnippet( texture.wrapT, 'y' ); + if ( texture.isData3DTexture ) { + + code += ',\n'; + addWrapSnippet( texture.wrapR, 'z' ); + + } + code += '\n\t);\n\n}\n'; wgslCodeCache[ functionName ] = nodeCode = new CodeNode( code, includes ); @@ -310,23 +319,57 @@ class WGSLNodeBuilder extends NodeBuilder { if ( textureData.dimensionsSnippet[ levelSnippet ] === undefined ) { let textureDimensionsParams; + let dimensionType; const { primarySamples } = this.renderer.backend.utils.getTextureSampleData( texture ); + const isMultisampled = primarySamples > 1; + + if ( texture.isData3DTexture ) { - if ( primarySamples > 1 ) { + dimensionType = 'vec3'; + + } else { + + // Regular 2D textures, depth textures, etc. + dimensionType = 'vec2'; + + } + + // Build parameters string based on texture type and multisampling + if ( isMultisampled || texture.isVideoTexture || texture.isStorageTexture ) { textureDimensionsParams = textureProperty; } else { - textureDimensionsParams = `${ textureProperty }, u32( ${ levelSnippet } )`; + textureDimensionsParams = `${textureProperty}${levelSnippet ? `, u32( ${ levelSnippet } )` : ''}`; } - textureDimensionNode = new VarNode( new ExpressionNode( `textureDimensions( ${ textureDimensionsParams } )`, 'uvec2' ) ); + textureDimensionNode = new VarNode( new ExpressionNode( `textureDimensions( ${ textureDimensionsParams } )`, dimensionType ) ); textureData.dimensionsSnippet[ levelSnippet ] = textureDimensionNode; + if ( texture.isDataArrayTexture || texture.isData3DTexture ) { + + textureData.arrayLayerCount = new VarNode( + new ExpressionNode( + `textureNumLayers(${textureProperty})`, + 'u32' + ) + ); + + } + + // For cube textures, we know it's always 6 faces + if ( texture.isTextureCube ) { + + textureData.cubeFaceCount = new VarNode( + new ExpressionNode( '6u', 'u32' ) + ); + + } + } return textureDimensionNode.build( this ); @@ -349,7 +392,8 @@ class WGSLNodeBuilder extends NodeBuilder { const wrapFunction = this.generateWrapFunction( texture ); const textureDimension = this.generateTextureDimension( texture, textureProperty, levelSnippet ); - const coordSnippet = `vec2u( ${ wrapFunction }( ${ uvSnippet } ) * vec2f( ${ textureDimension } ) )`; + const vecType = texture.isData3DTexture ? 'vec3' : 'vec2'; + const coordSnippet = `${vecType}(${wrapFunction}(${uvSnippet}) * ${vecType}(${textureDimension}))`; return this.generateTextureLoad( texture, textureProperty, coordSnippet, depthSnippet, levelSnippet ); diff --git a/src/textures/Data3DTexture.js b/src/textures/Data3DTexture.js index 36348cc1f04a12..e6e99a4f08948a 100644 --- a/src/textures/Data3DTexture.js +++ b/src/textures/Data3DTexture.js @@ -6,9 +6,9 @@ class Data3DTexture extends Texture { constructor( data = null, width = 1, height = 1, depth = 1 ) { // We're going to add .setXXX() methods for setting properties later. - // Users can still set in DataTexture3D directly. + // Users can still set in Data3DTexture directly. // - // const texture = new THREE.DataTexture3D( data, width, height, depth ); + // const texture = new THREE.Data3DTexture( data, width, height, depth ); // texture.anisotropy = 16; // // See #14839 diff --git a/test/e2e/puppeteer.js b/test/e2e/puppeteer.js index 9927554dceb5b7..f913dadda0e3a3 100644 --- a/test/e2e/puppeteer.js +++ b/test/e2e/puppeteer.js @@ -156,6 +156,8 @@ const exceptionList = [ 'webgpu_tsl_vfx_linkedparticles', 'webgpu_tsl_vfx_tornado', 'webgpu_textures_anisotropy', + 'webgpu_textures_2d-array_compressed', + 'webgpu_rendertarget_2d-array_3d', 'webgpu_materials_envmaps_bpcem', 'webgpu_postprocessing_ssr', 'webgpu_postprocessing_sobel',