diff --git a/.github/workflows/codeql-code-scanning.yml b/.github/workflows/codeql-code-scanning.yml index 6bec2a91b974f9..98a8bc71879a32 100644 --- a/.github/workflows/codeql-code-scanning.yml +++ b/.github/workflows/codeql-code-scanning.yml @@ -33,16 +33,16 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4 + uses: github/codeql-action/init@e46ed2cbd01164d986452f91f178727624ae40d7 # v4 with: languages: ${{ matrix.language }} config-file: ./.github/codeql-config.yml queries: security-and-quality - name: Autobuild - uses: github/codeql-action/autobuild@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4 + uses: github/codeql-action/autobuild@e46ed2cbd01164d986452f91f178727624ae40d7 # v4 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4 + uses: github/codeql-action/analyze@e46ed2cbd01164d986452f91f178727624ae40d7 # v4 with: category: "/language:${{matrix.language}}" diff --git a/examples/files.json b/examples/files.json index 73def8a8159447..87c447172f0279 100644 --- a/examples/files.json +++ b/examples/files.json @@ -475,6 +475,7 @@ "webgpu_storage_buffer", "webgpu_struct_drawindirect", "webgpu_test_memory", + "webgpu_texturegather", "webgpu_texturegrad", "webgpu_textures_2d-array", "webgpu_textures_2d-array_compressed", diff --git a/examples/screenshots/webgpu_texturegather.jpg b/examples/screenshots/webgpu_texturegather.jpg new file mode 100644 index 00000000000000..3835bb96a21058 Binary files /dev/null and b/examples/screenshots/webgpu_texturegather.jpg differ diff --git a/examples/webgpu_texturegather.html b/examples/webgpu_texturegather.html new file mode 100644 index 00000000000000..1bd061254e0cd7 --- /dev/null +++ b/examples/webgpu_texturegather.html @@ -0,0 +1,183 @@ + + + three.js webgpu - texture gather + + + + + + +
+ + +
+ three.jsTexture Gather +
+ + + This example demonstrates texture gather +
Left canvas is using WebGPU Backend, right canvas is WebGL Backend. +
The top half gathers color values and the bottom half gathers depth comparison. +
+
+ + + + + + diff --git a/package-lock.json b/package-lock.json index 6c74da34006e60..4353bfa6796fd4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1822,9 +1822,9 @@ "license": "MIT" }, "node_modules/eslint-plugin-compat": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-compat/-/eslint-plugin-compat-7.0.1.tgz", - "integrity": "sha512-wDID2fVIAfxV9R1uSkCn5HscnNu8yMxDF1IaQGyD1C6XuWwJbuaDgMOSkVgOom0LzY8z0fXXXCy7AQQTERQUvQ==", + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-compat/-/eslint-plugin-compat-7.0.2.tgz", + "integrity": "sha512-gN8hF+4NzMsHUbr4m/TYZK0FtW3DcV4g8rXpTsY2EV5xiRD8jsilUlB9lNSkGGX0veDCxMhKSWbSd+faJByQDA==", "dev": true, "license": "MIT", "dependencies": { @@ -1837,7 +1837,7 @@ "semver": "^7.6.2" }, "engines": { - "node": ">=18.x" + "node": ">=22.x" }, "peerDependencies": { "eslint": "^9.0.0 || ^10.0.0" @@ -2244,9 +2244,9 @@ } }, "node_modules/globals": { - "version": "17.5.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-17.5.0.tgz", - "integrity": "sha512-qoV+HK2yFl/366t2/Cb3+xxPUo5BuMynomoDmiaZBIdbs+0pYbjfZU+twLhGKp4uCZ/+NbtpVepH5bGCxRyy2g==", + "version": "17.6.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.6.0.tgz", + "integrity": "sha512-sepffkT8stwnIYbsMBpoCHJuJM5l98FUF2AnE07hfvE0m/qp3R586hw4jF4uadbhvg1ooIdzuu7CsfD2jzCaNA==", "dev": true, "license": "MIT", "engines": { diff --git a/src/nodes/accessors/TextureNode.js b/src/nodes/accessors/TextureNode.js index cc0e061ef21dec..dbb66cb074ec1c 100644 --- a/src/nodes/accessors/TextureNode.js +++ b/src/nodes/accessors/TextureNode.js @@ -99,6 +99,15 @@ class TextureNode extends UniformNode { */ this.gradNode = null; + /** + * Represents the optional index constant of the channel to gather. + * This must be in range [0, 3] and a compile-time constant. + * + * @type {?Node} + * @default null + */ + this.gatherNode = null; + /** * Represents the optional texel offset applied to the unnormalized texture * coordinate before sampling the texture. @@ -219,7 +228,13 @@ class TextureNode extends UniformNode { */ generateNodeType( /*builder*/ ) { - if ( this.value.isDepthTexture === true ) return 'float'; + if ( this.value.isDepthTexture === true ) { + + if ( this.gatherNode === null ) return 'float'; + + return 'vec4'; + + } if ( this.value.type === UnsignedIntType ) { @@ -429,6 +444,7 @@ class TextureNode extends UniformNode { properties.compareNode = compareNode; properties.compareStepNode = compareStepNode; properties.gradNode = this.gradNode; + properties.gatherNode = this.gatherNode; properties.depthNode = this.depthNode; properties.offsetNode = this.offsetNode; @@ -471,10 +487,12 @@ class TextureNode extends UniformNode { * @param {?string} depthSnippet - The depth snippet. * @param {?string} compareSnippet - The compare snippet. * @param {?Array} gradSnippet - The grad snippet. + * @param {?string} gatherSnippet - The gather snippet. * @param {?string} offsetSnippet - The offset snippet. + * @param {?string} flipYSnippet - The y-flip snippet. Only used for WebGL. * @return {string} The generated code snippet. */ - generateSnippet( builder, textureProperty, uvSnippet, levelSnippet, biasSnippet, depthSnippet, compareSnippet, gradSnippet, offsetSnippet ) { + generateSnippet( builder, textureProperty, uvSnippet, levelSnippet, biasSnippet, depthSnippet, compareSnippet, gradSnippet, gatherSnippet, offsetSnippet, flipYSnippet ) { const texture = this.value; @@ -488,6 +506,18 @@ class TextureNode extends UniformNode { snippet = builder.generateTextureGrad( texture, textureProperty, uvSnippet, gradSnippet, depthSnippet, offsetSnippet ); + } else if ( gatherSnippet ) { + + if ( compareSnippet ) { + + snippet = builder.generateTextureGatherCompare( texture, textureProperty, uvSnippet, compareSnippet, depthSnippet, offsetSnippet, flipYSnippet ); + + } else { + + snippet = builder.generateTextureGather( texture, textureProperty, uvSnippet, gatherSnippet, depthSnippet, offsetSnippet, flipYSnippet ); + + } + } else if ( compareSnippet ) { snippet = builder.generateTextureCompare( texture, textureProperty, uvSnippet, compareSnippet, depthSnippet, offsetSnippet ); @@ -536,13 +566,13 @@ class TextureNode extends UniformNode { const nodeData = builder.getDataFromNode( this ); - const nodeType = this.getNodeType( builder ); + let nodeType = this.getNodeType( builder ); let propertyName = nodeData.propertyName; if ( propertyName === undefined ) { - const { uvNode, levelNode, biasNode, compareNode, compareStepNode, depthNode, gradNode, offsetNode } = properties; + const { uvNode, levelNode, biasNode, compareNode, compareStepNode, depthNode, gradNode, gatherNode, offsetNode } = properties; const uvSnippet = this.generateUV( builder, uvNode ); const levelSnippet = levelNode ? levelNode.build( builder, 'float' ) : null; @@ -551,7 +581,15 @@ class TextureNode extends UniformNode { const compareSnippet = compareNode ? compareNode.build( builder, 'float' ) : null; const compareStepSnippet = compareStepNode ? compareStepNode.build( builder, 'float' ) : null; const gradSnippet = gradNode ? [ gradNode[ 0 ].build( builder, 'vec2' ), gradNode[ 1 ].build( builder, 'vec2' ) ] : null; + const gatherSnippet = gatherNode ? gatherNode.build( builder, 'int' ) : null; const offsetSnippet = offsetNode ? this.generateOffset( builder, offsetNode ) : null; + const flipYSnippet = this._flipYUniform ? this._flipYUniform.build( builder, 'bool' ) : null; + + if ( gatherSnippet ) { + + nodeType = 'vec4'; + + } let finalDepthSnippet = depthSnippet; @@ -565,7 +603,7 @@ class TextureNode extends UniformNode { propertyName = builder.getPropertyName( nodeVar ); - let snippet = this.generateSnippet( builder, textureProperty, uvSnippet, levelSnippet, biasSnippet, finalDepthSnippet, compareSnippet, gradSnippet, offsetSnippet ); + let snippet = this.generateSnippet( builder, textureProperty, uvSnippet, levelSnippet, biasSnippet, finalDepthSnippet, compareSnippet, gradSnippet, gatherSnippet, offsetSnippet, flipYSnippet ); if ( compareStepSnippet !== null ) { @@ -772,6 +810,22 @@ class TextureNode extends UniformNode { } + /** + * Gathers four texels from the texture. + * + * @param {[Node]} gatherNode - The index of the channel to read. This must be in range [0, 3] and a compile-time constant. + * @return {TextureNode} A texture node representing the texture sample. + */ + gather( gatherNode = 0 ) { + + const textureNode = this.clone(); + textureNode.gatherNode = nodeObject( gatherNode ); + textureNode.referenceNode = this.getBase(); + + return nodeObject( textureNode ); + + } + /** * Samples the texture by defining a depth node. * @@ -868,6 +922,7 @@ class TextureNode extends UniformNode { newNode.depthNode = this.depthNode; newNode.compareNode = this.compareNode; newNode.gradNode = this.gradNode; + newNode.gatherNode = this.gatherNode; newNode.offsetNode = this.offsetNode; return newNode; diff --git a/src/nodes/display/PassNode.js b/src/nodes/display/PassNode.js index 8f43867fec0d29..bab01cc00b212b 100644 --- a/src/nodes/display/PassNode.js +++ b/src/nodes/display/PassNode.js @@ -155,6 +155,7 @@ class PassMultipleTextureNode extends PassTextureNode { newNode.depthNode = this.depthNode; newNode.compareNode = this.compareNode; newNode.gradNode = this.gradNode; + newNode.gatherNode = this.gatherNode; newNode.offsetNode = this.offsetNode; return newNode; diff --git a/src/nodes/utils/ReflectorNode.js b/src/nodes/utils/ReflectorNode.js index 5c5227a60e1ebb..75670fe1a274ad 100644 --- a/src/nodes/utils/ReflectorNode.js +++ b/src/nodes/utils/ReflectorNode.js @@ -163,6 +163,7 @@ class ReflectorNode extends TextureNode { newNode.depthNode = this.depthNode; newNode.compareNode = this.compareNode; newNode.gradNode = this.gradNode; + newNode.gatherNode = this.gatherNode; newNode.offsetNode = this.offsetNode; newNode._reflectorBaseNode = this._reflectorBaseNode; diff --git a/src/renderers/common/Backend.js b/src/renderers/common/Backend.js index c0631941e33b35..fb09e3259eaa0d 100644 --- a/src/renderers/common/Backend.js +++ b/src/renderers/common/Backend.js @@ -277,9 +277,10 @@ class Backend { * * @abstract * @param {Texture} texture - The texture to update the sampler for. + * @param {TextureNode} textureNode - The texture node to update the sampler with. * @return {string} The current sampler key. */ - updateSampler( /*texture*/ ) { } + updateSampler( /*texture, textureNode*/ ) { } /** * Creates a default texture for the given texture that can be used diff --git a/src/renderers/common/Bindings.js b/src/renderers/common/Bindings.js index f2f906f55e9479..e0902417499a3b 100644 --- a/src/renderers/common/Bindings.js +++ b/src/renderers/common/Bindings.js @@ -202,7 +202,7 @@ class Bindings extends DataMap { } else if ( binding.isSampler ) { - this.textures.updateSampler( binding.texture ); + this.textures.updateSampler( binding.texture, binding.textureNode ); } else if ( binding.isStorageBuffer ) { @@ -410,7 +410,7 @@ class Bindings extends DataMap { if ( updated ) { - const samplerKey = this.textures.updateSampler( binding.texture ); + const samplerKey = this.textures.updateSampler( binding.texture, binding.textureNode ); if ( binding.samplerKey !== samplerKey ) { diff --git a/src/renderers/common/Textures.js b/src/renderers/common/Textures.js index 145e7463406fb2..bd7ac761a2fb46 100644 --- a/src/renderers/common/Textures.js +++ b/src/renderers/common/Textures.js @@ -419,11 +419,12 @@ class Textures extends DataMap { * them when the texture parameters match. * * @param {Texture} texture - The texture to update the sampler for. + * @param {TextureNode} textureNode - The texture node to update the sampler with. * @return {string} The current sampler key. */ - updateSampler( texture ) { + updateSampler( texture, textureNode ) { - return this.backend.updateSampler( texture ); + return this.backend.updateSampler( texture, textureNode ); } diff --git a/src/renderers/webgl-fallback/WebGLBackend.js b/src/renderers/webgl-fallback/WebGLBackend.js index 707c12022bf3f4..eefa2b2e04a839 100644 --- a/src/renderers/webgl-fallback/WebGLBackend.js +++ b/src/renderers/webgl-fallback/WebGLBackend.js @@ -1418,9 +1418,10 @@ class WebGLBackend extends Backend { * This method does nothing since WebGL 2 has no concept of samplers. * * @param {Texture} texture - The texture to update the sampler for. + * @param {TextureNode} textureNode - The texture node to update the sampler with. * @return {string} The current sampler key. */ - updateSampler( /*texture*/ ) { + updateSampler( /*texture, textureNode*/ ) { return ''; diff --git a/src/renderers/webgl-fallback/nodes/GLSLNodeBuilder.js b/src/renderers/webgl-fallback/nodes/GLSLNodeBuilder.js index 5e18fb3c051398..a2653e6679c4d2 100644 --- a/src/renderers/webgl-fallback/nodes/GLSLNodeBuilder.js +++ b/src/renderers/webgl-fallback/nodes/GLSLNodeBuilder.js @@ -11,7 +11,67 @@ import { error } from '../../../utils.js'; const glslPolyfills = { bitcast_int_uint: new CodeNode( /* glsl */'uint tsl_bitcast_int_to_uint ( int x ) { return floatBitsToUint( intBitsToFloat ( x ) ); }' ), - bitcast_uint_int: new CodeNode( /* glsl */'uint tsl_bitcast_uint_to_int ( uint x ) { return floatBitsToInt( uintBitsToFloat ( x ) ); }' ) + bitcast_uint_int: new CodeNode( /* glsl */'uint tsl_bitcast_uint_to_int ( uint x ) { return floatBitsToInt( uintBitsToFloat ( x ) ); }' ), + textureGather: new CodeNode( /* glsl */` +vec4 tsl_textureGather( const int comp, sampler2D map, vec2 coord, ivec2 offset, bool flipY ) { + if ( flipY ) offset.y = - offset.y; + vec2 size = vec2( textureSize( map, 0 ) ); + vec2 st = floor( coord * size + vec2( offset ) - 0.5 ); + vec4 ij = vec4( st + 0.5, st + 1.5 ) / size.xyxy; + vec4 ret = vec4( + textureLod( map, ij.xw, 0.0 )[ comp ], + textureLod( map, ij.zw, 0.0 )[ comp ], + textureLod( map, ij.zy, 0.0 )[ comp ], + textureLod( map, ij.xy, 0.0 )[ comp ] + ); + return flipY ? ret.wzyx : ret; +} +` ), + textureGatherArray: new CodeNode( /* glsl */` +vec4 tsl_textureGather_array( const int comp, sampler2DArray map, vec3 coord, ivec2 offset, bool flipY ) { + if ( flipY ) offset.y = - offset.y; + vec2 size = vec2( textureSize( map, 0 ).xy ); + vec2 st = floor( coord.xy * size + vec2( offset ) - 0.5 ); + vec4 ij = vec4( st + 0.5, st + 1.5 ) / size.xyxy; + vec4 ret = vec4( + textureLod( map, vec3( ij.xw, coord.z ), 0.0 )[ comp ], + textureLod( map, vec3( ij.zw, coord.z ), 0.0 )[ comp ], + textureLod( map, vec3( ij.zy, coord.z ), 0.0 )[ comp ], + textureLod( map, vec3( ij.xy, coord.z ), 0.0 )[ comp ] + ); + return flipY ? ret.wzyx : ret; +} +` ), + textureGatherCompare: new CodeNode( /* glsl */` +vec4 tsl_textureGatherCompare( sampler2DShadow map, vec2 coord, ivec2 offset, float ref, bool flipY ) { + if ( flipY ) offset.y = - offset.y; + vec2 size = vec2( textureSize( map, 0 ) ); + vec2 st = floor( coord * size + vec2( offset ) - 0.5 ); + vec4 ij = vec4( st + 0.5, st + 1.5 ) / size.xyxy; + vec4 ret = vec4( + textureLod( map, vec3( ij.xw, ref ), 0.0 ), + textureLod( map, vec3( ij.zw, ref ), 0.0 ), + textureLod( map, vec3( ij.zy, ref ), 0.0 ), + textureLod( map, vec3( ij.xy, ref ), 0.0 ) + ); + return flipY ? ret.wzyx : ret; +} +` ), + textureGatherCompareArray: new CodeNode( /* glsl */` +vec4 tsl_textureGatherCompare_array( sampler2DArrayShadow map, vec3 coord, ivec2 offset, float ref, bool flipY ) { + if ( flipY ) offset.y = - offset.y; + vec2 size = vec2( textureSize( map, 0 ).xy ); + vec2 st = floor( coord.xy * size + vec2( offset ) - 0.5 ); + vec4 ij = vec4( st + 0.5, st + 1.5 ) / size.xyxy; + vec4 ret = vec4( + texture( map, vec4( ij.xw, coord.z, ref ) ), + texture( map, vec4( ij.zw, coord.z, ref ) ), + texture( map, vec4( ij.zy, coord.z, ref ) ), + texture( map, vec4( ij.xy, coord.z, ref ) ) + ); + return flipY ? ret.wzyx : ret; +} +` ) }; const glslMethods = { @@ -182,7 +242,7 @@ class GLSLNodeBuilder extends NodeBuilder { * * @param {string} type - The output type to bitcast to. * @param {string} inputType - The input type of the. - * @return {string} The resolved WGSL bitcast invocation. + * @return {string} The resolved GLSL bitcast invocation. */ getBitcastMethod( type, inputType ) { @@ -665,6 +725,72 @@ ${ flowData.code } } + /** + * Generates the GLSL snippet for gathering four texels from the given texture. + * + * @param {Texture} texture - The texture. + * @param {string} textureProperty - The name of the texture uniform in the shader. + * @param {string} uvSnippet - A GLSL snippet that represents texture coordinates used for sampling. + * @param {string} gatherSnippet - A GLSL snippet that represents the index of the channel to read. + * @param {?string} depthSnippet - A GLSL snippet that represents 0-based texture array index to sample. + * @param {?string} offsetSnippet - A GLSL snippet that represents the offset that will be applied to the unnormalized texture coordinate before sampling the texture. + * @param {?string} flipYSnippet - A GLSL snippet that represents the y-flip. Only used for WebGL. + * @return {string} The GLSL snippet. + */ + generateTextureGather( texture, textureProperty, uvSnippet, gatherSnippet, depthSnippet, offsetSnippet, flipYSnippet ) { + + if ( texture.isDepthTexture ) gatherSnippet = '0'; + + if ( offsetSnippet === null ) offsetSnippet = 'ivec2( 0 )'; + + if ( flipYSnippet === null ) flipYSnippet = 'false'; + + if ( depthSnippet ) { + + this._include( 'textureGatherArray' ); + + return `tsl_textureGather_array( ${gatherSnippet}, ${ textureProperty }, vec3( ${ uvSnippet }, ${ depthSnippet } ), ${ offsetSnippet }, ${ flipYSnippet } )`; + + } + + this._include( 'textureGather' ); + + return `tsl_textureGather( ${gatherSnippet}, ${ textureProperty }, ${ uvSnippet }, ${ offsetSnippet }, ${ flipYSnippet } )`; + + } + + /** + * Generates the GLSL snippet for performing a depth comparison on four texels in the given depth texture. + * + * @param {Texture} texture - The texture. + * @param {string} textureProperty - The name of the texture uniform in the shader. + * @param {string} uvSnippet - A GLSL snippet that represents texture coordinates used for sampling. + * @param {string} compareSnippet - A GLSL snippet that represents the reference value. + * @param {?string} depthSnippet - A GLSL snippet that represents 0-based texture array index to sample. + * @param {?string} offsetSnippet - A GLSL snippet that represents the offset that will be applied to the unnormalized texture coordinate before sampling the texture. + * @param {?string} flipYSnippet - A GLSL snippet that represents the y-flip. Only used for WebGL. + * @return {string} The GLSL snippet. + */ + generateTextureGatherCompare( texture, textureProperty, uvSnippet, compareSnippet, depthSnippet, offsetSnippet, flipYSnippet ) { + + if ( offsetSnippet === null ) offsetSnippet = 'ivec2( 0 )'; + + if ( flipYSnippet === null ) flipYSnippet = 'false'; + + if ( depthSnippet ) { + + this._include( 'textureGatherCompareArray' ); + + return `tsl_textureGatherCompare_array( ${ textureProperty }, vec3( ${ uvSnippet }, ${depthSnippet} ), ${ offsetSnippet }, ${ compareSnippet }, ${ flipYSnippet } )`; + + } + + this._include( 'textureGatherCompare' ); + + return `tsl_textureGatherCompare( ${ textureProperty }, ${ uvSnippet }, ${ offsetSnippet }, ${ compareSnippet }, ${ flipYSnippet } )`; + + } + /** * Returns the uniforms of the given shader stage as a GLSL string. * @@ -685,7 +811,8 @@ ${ flowData.code } if ( uniform.type === 'texture' || uniform.type === 'texture3D' ) { - const texture = uniform.node.value; + const textureNode = uniform.node; + const texture = textureNode.value; let typePrefix = ''; @@ -707,7 +834,7 @@ ${ flowData.code } snippet = `${typePrefix}sampler3D ${ uniform.name };`; - } else if ( texture.compareFunction ) { + } else if ( texture.compareFunction && textureNode.compareNode !== null ) { if ( texture.isArrayTexture === true ) { diff --git a/src/renderers/webgpu/WebGPUBackend.js b/src/renderers/webgpu/WebGPUBackend.js index 0ffb056329cfb0..bb055354ebd5d9 100644 --- a/src/renderers/webgpu/WebGPUBackend.js +++ b/src/renderers/webgpu/WebGPUBackend.js @@ -1950,11 +1950,12 @@ class WebGPUBackend extends Backend { * Updates a GPU sampler for the given texture. * * @param {Texture} texture - The texture to update the sampler for. + * @param {TextureNode} textureNode - The texture node to update the sampler with. * @return {string} The current sampler key. */ - updateSampler( texture ) { + updateSampler( texture, textureNode ) { - return this.textureUtils.updateSampler( texture ); + return this.textureUtils.updateSampler( texture, textureNode ); } diff --git a/src/renderers/webgpu/nodes/WGSLNodeBuilder.js b/src/renderers/webgpu/nodes/WGSLNodeBuilder.js index 45dc7fef7d8d22..b640bfb959f531 100644 --- a/src/renderers/webgpu/nodes/WGSLNodeBuilder.js +++ b/src/renderers/webgpu/nodes/WGSLNodeBuilder.js @@ -352,7 +352,7 @@ class WGSLNodeBuilder extends NodeBuilder { */ generateWrapFunction( texture ) { - const functionName = `tsl_coord_${ wrapNames[ texture.wrapS ] }S_${ wrapNames[ texture.wrapT ] }_${ texture.is3DTexture || texture.isData3DTexture ? '3d' : '2d' }T`; + const functionName = `tsl_coord_${ wrapNames[ texture.wrapS ] }S_${ wrapNames[ texture.wrapT ] }T_${ texture.is3DTexture || texture.isData3DTexture ? '3d' : '2d' }`; let nodeCode = wgslCodeCache[ functionName ]; @@ -850,6 +850,80 @@ class WGSLNodeBuilder extends NodeBuilder { } + /** + * Generates the WGSL snippet for gathering four texels from the given texture. + * + * @param {Texture} texture - The texture. + * @param {string} textureProperty - The name of the texture uniform in the shader. + * @param {string} uvSnippet - A WGSL snippet that represents texture coordinates used for sampling. + * @param {string} gatherSnippet - A WGSL snippet that represents the index of the channel to read. + * @param {?string} depthSnippet - A WGSL snippet that represents 0-based texture array index to sample. + * @param {?string} offsetSnippet - A WGSL snippet that represents the offset that will be applied to the unnormalized texture coordinate before sampling the texture. + * @param {?string} flipYSnippet - A WGSL snippet that represents the y-flip. Only used for WebGL. + * @return {string} The WGSL snippet. + */ + generateTextureGather( texture, textureProperty, uvSnippet, gatherSnippet, depthSnippet, offsetSnippet ) { + + const componentSnippet = texture.isDepthTexture === true ? '' : `${gatherSnippet}, `; + + if ( depthSnippet ) { + + if ( offsetSnippet ) { + + return `textureGather( ${componentSnippet}${ textureProperty }, ${ textureProperty }_sampler, ${ uvSnippet }, ${ depthSnippet }, ${ offsetSnippet } )`; + + } + + return `textureGather( ${componentSnippet}${ textureProperty }, ${ textureProperty }_sampler, ${ uvSnippet }, ${ depthSnippet } )`; + + } + + if ( offsetSnippet ) { + + return `textureGather( ${componentSnippet}${ textureProperty }, ${ textureProperty }_sampler, ${ uvSnippet }, ${ offsetSnippet } )`; + + } + + return `textureGather( ${componentSnippet}${ textureProperty }, ${ textureProperty }_sampler, ${ uvSnippet })`; + + } + + /** + * Generates the WGSL snippet for performing a depth comparison on four texels in the given depth texture. + * + * @param {Texture} texture - The texture. + * @param {string} textureProperty - The name of the texture uniform in the shader. + * @param {string} uvSnippet - A WGSL snippet that represents texture coordinates used for sampling. + * @param {string} compareSnippet - A WGSL snippet that represents the reference value. + * @param {?string} depthSnippet - A WGSL snippet that represents 0-based texture array index to sample. + * @param {?string} offsetSnippet - A WGSL snippet that represents the offset that will be applied to the unnormalized texture coordinate before sampling the texture. + * @param {?string} flipYSnippet - A WGSL snippet that represents the y-flip. Only used for WebGL. + * @return {string} The WGSL snippet. + */ + generateTextureGatherCompare( texture, textureProperty, uvSnippet, compareSnippet, depthSnippet, offsetSnippet ) { + + if ( depthSnippet ) { + + if ( offsetSnippet ) { + + return `textureGatherCompare( ${ textureProperty }, ${ textureProperty }_sampler, ${ uvSnippet }, ${ depthSnippet }, ${ compareSnippet }, ${ offsetSnippet } )`; + + } + + return `textureGatherCompare( ${ textureProperty }, ${ textureProperty }_sampler, ${ uvSnippet }, ${ depthSnippet }, ${ compareSnippet })`; + + } + + if ( offsetSnippet ) { + + return `textureGatherCompare( ${ textureProperty }, ${ textureProperty }_sampler, ${ uvSnippet }, ${ compareSnippet }, ${ offsetSnippet } )`; + + } + + return `textureGatherCompare( ${ textureProperty }, ${ textureProperty }_sampler, ${ uvSnippet }, ${ compareSnippet })`; + + } + /** * Generates the WGSL snippet when sampling textures with explicit mip level. * @@ -1127,7 +1201,8 @@ class WGSLNodeBuilder extends NodeBuilder { texture.setVisibility( gpuShaderStageLib[ shaderStage ] ); // Cube textures always need samplers (they use textureSampleLevel, not textureLoad) - const needsSampler = node.value.isCubeTexture === true || ( this.isUnfilterable( node.value ) === false && texture.store === false ); + // Also textureGather always need sampler. + const needsSampler = node.value.isCubeTexture === true || ( this.isUnfilterable( node.value ) === false && texture.store === false ) || node.gatherNode !== null; if ( needsSampler ) { @@ -1908,14 +1983,16 @@ ${ flowData.code } if ( uniform.type === 'texture' || uniform.type === 'cubeTexture' || uniform.type === 'cubeDepthTexture' || uniform.type === 'storageTexture' || uniform.type === 'texture3D' ) { - const texture = uniform.node.value; + const textureNode = uniform.node; + const texture = textureNode.value; // Cube textures always need samplers (they use textureSampleLevel, not textureLoad) - const needsSampler = texture.isCubeTexture === true || ( this.isUnfilterable( texture ) === false && uniform.node.isStorageTextureNode !== true ); + // Also textureGather always need sampler. + const needsSampler = texture.isCubeTexture === true || ( this.isUnfilterable( texture ) === false && textureNode.isStorageTextureNode !== true ) || textureNode.gatherNode !== null; if ( needsSampler ) { - if ( this.isSampleCompare( texture ) ) { + if ( this.isSampleCompare( texture ) && textureNode.compareNode !== null ) { bindingSnippets.push( `@binding( ${ uniformIndexes.binding ++ } ) @group( ${ uniformIndexes.group } ) var ${ uniform.name }_sampler : sampler_comparison;` ); diff --git a/src/renderers/webgpu/utils/WebGPUBindingUtils.js b/src/renderers/webgpu/utils/WebGPUBindingUtils.js index 7871c35c99ad18..8bf4dfadf0e454 100644 --- a/src/renderers/webgpu/utils/WebGPUBindingUtils.js +++ b/src/renderers/webgpu/utils/WebGPUBindingUtils.js @@ -533,7 +533,7 @@ class WebGPUBindingUtils { if ( binding.texture.isDepthTexture ) { - if ( binding.texture.compareFunction !== null && backend.hasCompatibility( Compatibility.TEXTURE_COMPARE ) ) { + if ( binding.texture.compareFunction !== null && binding.textureNode.compareNode !== null && backend.hasCompatibility( Compatibility.TEXTURE_COMPARE ) ) { sampler.type = GPUSamplerBindingType.Comparison; diff --git a/src/renderers/webgpu/utils/WebGPUTextureUtils.js b/src/renderers/webgpu/utils/WebGPUTextureUtils.js index 9905bfb0433589..4fbbefa933e40b 100644 --- a/src/renderers/webgpu/utils/WebGPUTextureUtils.js +++ b/src/renderers/webgpu/utils/WebGPUTextureUtils.js @@ -125,15 +125,17 @@ class WebGPUTextureUtils { * Creates a GPU sampler for the given texture. * * @param {Texture} texture - The texture to create the sampler for. + * @param {TextureNode} textureNode - The texture node to update the sampler with. * @return {string} The current sampler key. */ - updateSampler( texture ) { + updateSampler( texture, textureNode ) { const backend = this.backend; const samplerKey = texture.minFilter + '-' + texture.magFilter + '-' + texture.wrapS + '-' + texture.wrapT + '-' + ( texture.wrapR || '0' ) + '-' + - texture.anisotropy + '-' + ( texture.compareFunction || 0 ); + texture.anisotropy + '-' + ( texture.isDepthTexture === true ? 1 : 0 ) + '-' + + ( texture.compareFunction !== null && textureNode.compareNode !== null ? texture.compareFunction : 0 ); let samplerData = this._samplerCache.get( samplerKey ); @@ -150,7 +152,7 @@ class WebGPUTextureUtils { }; // Depth textures without compare function must use non-filtering (nearest) sampling - if ( texture.isDepthTexture && texture.compareFunction === null ) { + if ( texture.isDepthTexture && ( texture.compareFunction === null || textureNode.compareNode === null ) ) { samplerDescriptorGPU.magFilter = GPUFilterMode.Nearest; samplerDescriptorGPU.minFilter = GPUFilterMode.Nearest; @@ -166,7 +168,7 @@ class WebGPUTextureUtils { } - if ( texture.isDepthTexture && texture.compareFunction !== null && backend.hasCompatibility( Compatibility.TEXTURE_COMPARE ) ) { + if ( texture.isDepthTexture && texture.compareFunction !== null && textureNode.compareNode !== null && backend.hasCompatibility( Compatibility.TEXTURE_COMPARE ) ) { samplerDescriptorGPU.compare = _compareToWebGPU[ texture.compareFunction ];