From b5aef65c87654e672c6e0a7f51ce443311c33c5b Mon Sep 17 00:00:00 2001 From: Shota Matsuda Date: Mon, 4 May 2026 01:06:25 +0900 Subject: [PATCH 1/3] TSL: Add support for `textureGather` and `textureGatherCompare` (#33475) Co-authored-by: sunag --- examples/files.json | 1 + examples/screenshots/webgpu_texturegather.jpg | Bin 0 -> 23721 bytes examples/webgpu_texturegather.html | 183 ++++++++++++++++++ src/nodes/accessors/TextureNode.js | 65 ++++++- src/nodes/display/PassNode.js | 1 + src/nodes/utils/ReflectorNode.js | 1 + src/renderers/common/Backend.js | 3 +- src/renderers/common/Bindings.js | 4 +- src/renderers/common/Textures.js | 5 +- src/renderers/webgl-fallback/WebGLBackend.js | 3 +- .../webgl-fallback/nodes/GLSLNodeBuilder.js | 135 ++++++++++++- src/renderers/webgpu/WebGPUBackend.js | 5 +- src/renderers/webgpu/nodes/WGSLNodeBuilder.js | 87 ++++++++- .../webgpu/utils/WebGPUBindingUtils.js | 2 +- .../webgpu/utils/WebGPUTextureUtils.js | 10 +- 15 files changed, 478 insertions(+), 27 deletions(-) create mode 100644 examples/screenshots/webgpu_texturegather.jpg create mode 100644 examples/webgpu_texturegather.html 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 0000000000000000000000000000000000000000..3835bb96a2105807f72b3c26342e204a5444d27d GIT binary patch literal 23721 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 <`w?(6Z* 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 literal 0 HcmV?d00001 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/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 ]; From e5b86829b965863b58f4e7f540ecdd1f986ae766 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 3 May 2026 16:50:42 +0000 Subject: [PATCH 2/3] chore(deps): update devdependencies (non-major) (#33522) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package-lock.json | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) 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": { From 831b0c1bf66cf0d32d9cb03c5b56b8710ab9c8c3 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 3 May 2026 18:57:18 +0200 Subject: [PATCH 3/3] chore(deps): update github/codeql-action digest to e46ed2c (#33521) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/codeql-code-scanning.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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}}"