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',