From 8b772c48c1355e07425601fa06082d59fb28e9e2 Mon Sep 17 00:00:00 2001 From: Michael Herzog Date: Mon, 10 Aug 2026 13:55:47 +0200 Subject: [PATCH 1/7] TextureNode: Refactor `isPlainGather()`. (#34201) --- src/nodes/accessors/TextureNode.js | 31 ++++++++++++--- src/nodes/core/UniformNode.js | 38 +++++++++++++------ .../webgl-fallback/nodes/GLSLNodeBuilder.js | 2 +- src/renderers/webgpu/nodes/WGSLNodeBuilder.js | 2 +- .../webgpu/utils/WebGPUBindingUtils.js | 2 +- .../webgpu/utils/WebGPUTextureUtils.js | 2 +- 6 files changed, 56 insertions(+), 21 deletions(-) diff --git a/src/nodes/accessors/TextureNode.js b/src/nodes/accessors/TextureNode.js index 3105bd1fc870e8..0479c485b6df59 100644 --- a/src/nodes/accessors/TextureNode.js +++ b/src/nodes/accessors/TextureNode.js @@ -180,6 +180,15 @@ class TextureNode extends UniformNode { */ this._flipYUniform = null; + /** + * Whether the node is used as a comparison sampler, e.g. via `samplerComparison()`. + * + * @private + * @type {boolean} + * @default false + */ + this._samplerComparison = false; + this.setUpdateMatrix( uvNode === null ); } @@ -545,6 +554,18 @@ class TextureNode extends UniformNode { if ( /^sampler/.test( output ) ) { + if ( output === 'samplerComparison' ) { + + this._samplerComparison = true; + + // texture nodes with the same texture share a single uniform so it's + // important to set the flag on the node the binding refers to as well + + const sharedNode = this.getSharedNode( builder ); + sharedNode._samplerComparison = true; + + } + return textureProperty + '_sampler'; } else if ( builder.isReference( output ) ) { @@ -824,14 +845,14 @@ class TextureNode extends UniformNode { } /** - * Returns `true` if the texture is sampled with a plain gather (`textureGather`), - * meaning a gather without a compare value. + * Returns `true` if the texture is sampled with a depth comparison, + * meaning the node must be bound with a comparison sampler. * - * @return {boolean} Whether a plain gather is used or not. + * @return {boolean} Whether comparison sampling is used or not. */ - isPlainGather() { + isSampleCompare() { - return this.gatherNode !== null && this.compareNode === null; + return this.compareNode !== null || this._samplerComparison === true; } diff --git a/src/nodes/core/UniformNode.js b/src/nodes/core/UniformNode.js index 1093dfc0102a50..dc779eb05be494 100644 --- a/src/nodes/core/UniformNode.js +++ b/src/nodes/core/UniformNode.js @@ -123,6 +123,31 @@ class UniformNode extends InputNode { } + /** + * Uniform nodes with the same hash share a single uniform. This method returns the node + * the shared uniform refers to which is the first node registered for the hash. + * + * @param {NodeBuilder} builder - The current node builder. + * @return {UniformNode} The node the shared uniform refers to. + */ + getSharedNode( builder ) { + + const hash = this.getUniformHash( builder ); + + let sharedNode = builder.getNodeFromHash( hash ); + + if ( sharedNode === undefined ) { + + builder.setHashNode( this, hash ); + + sharedNode = this; + + } + + return sharedNode; + + } + onUpdate( callback, updateType ) { callback = callback.bind( this ); @@ -159,18 +184,7 @@ class UniformNode extends InputNode { const type = this.getNodeType( builder ); - const hash = this.getUniformHash( builder ); - - let sharedNode = builder.getNodeFromHash( hash ); - - if ( sharedNode === undefined ) { - - builder.setHashNode( this, hash ); - - sharedNode = this; - - } - + const sharedNode = this.getSharedNode( builder ); const sharedNodeType = sharedNode.getInputType( builder ); const nodeUniform = builder.getUniformFromNode( sharedNode, sharedNodeType, builder.shaderStage, this.name || builder.context.nodeName ); diff --git a/src/renderers/webgl-fallback/nodes/GLSLNodeBuilder.js b/src/renderers/webgl-fallback/nodes/GLSLNodeBuilder.js index dce34d6b939b45..a8f9f75ba3b498 100644 --- a/src/renderers/webgl-fallback/nodes/GLSLNodeBuilder.js +++ b/src/renderers/webgl-fallback/nodes/GLSLNodeBuilder.js @@ -883,7 +883,7 @@ ${ flowData.code } snippet = `${typePrefix}sampler3D ${ uniform.name };`; - } else if ( texture.compareFunction && textureNode.isPlainGather() === false ) { + } else if ( texture.compareFunction && textureNode.isSampleCompare() ) { if ( texture.isArrayTexture === true ) { diff --git a/src/renderers/webgpu/nodes/WGSLNodeBuilder.js b/src/renderers/webgpu/nodes/WGSLNodeBuilder.js index 1faa094ea94182..dde0337e0ac2a0 100644 --- a/src/renderers/webgpu/nodes/WGSLNodeBuilder.js +++ b/src/renderers/webgpu/nodes/WGSLNodeBuilder.js @@ -2142,7 +2142,7 @@ ${ flowData.code } if ( needsSampler ) { - if ( this.isSampleCompare( texture ) && textureNode.isPlainGather() === false ) { + if ( this.isSampleCompare( texture ) && textureNode.isSampleCompare() ) { 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 cf4d82e08b50a8..b3f020af46e94c 100644 --- a/src/renderers/webgpu/utils/WebGPUBindingUtils.js +++ b/src/renderers/webgpu/utils/WebGPUBindingUtils.js @@ -584,7 +584,7 @@ class WebGPUBindingUtils { if ( binding.texture.isDepthTexture ) { - if ( binding.texture.compareFunction !== null && binding.textureNode.isPlainGather() === false && backend.hasCompatibility( Compatibility.TEXTURE_COMPARE ) ) { + if ( binding.texture.compareFunction !== null && binding.textureNode.isSampleCompare() && 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 70425e6b9f2879..0b02ff45ed682a 100644 --- a/src/renderers/webgpu/utils/WebGPUTextureUtils.js +++ b/src/renderers/webgpu/utils/WebGPUTextureUtils.js @@ -157,7 +157,7 @@ class WebGPUTextureUtils { const texture = binding.texture; const textureNode = binding.textureNode; - const isComparison = texture.isDepthTexture === true && texture.compareFunction !== null && textureNode.isPlainGather() === false && backend.hasCompatibility( Compatibility.TEXTURE_COMPARE ); + const isComparison = texture.isDepthTexture === true && texture.compareFunction !== null && textureNode.isSampleCompare() && backend.hasCompatibility( Compatibility.TEXTURE_COMPARE ); const samplerKey = texture.minFilter + '-' + texture.magFilter + '-' + texture.wrapS + '-' + texture.wrapT + '-' + ( texture.wrapR || '0' ) + '-' + From 4cab82c2997ed88c02994b1ac4fabd14b45b3e5e Mon Sep 17 00:00:00 2001 From: sunag Date: Mon, 10 Aug 2026 10:15:48 -0300 Subject: [PATCH 2/7] TSL: Add `horizontalRotation` option to `billboarding()` (#34197) --- .../webgpu_compute_particles_rain.jpg | Bin 37318 -> 37917 bytes .../screenshots/webgpu_tsl_vfx_flames.jpg | Bin 10043 -> 10131 bytes examples/webgpu_compute_particles_rain.html | 5 +- examples/webgpu_tsl_vfx_flames.html | 4 +- src/nodes/utils/SpriteUtils.js | 81 +++++++++++++----- 5 files changed, 62 insertions(+), 28 deletions(-) diff --git a/examples/screenshots/webgpu_compute_particles_rain.jpg b/examples/screenshots/webgpu_compute_particles_rain.jpg index ad44b454bde21d28f8120330ba8b9557b02cc82f..978d9192a9482fc963c933ce71e63c105b30aae4 100644 GIT binary patch delta 36593 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 Q)d)fw>epik=)WWX1DPL$4*&oF delta 36011 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 V)d$Q5N}=$uXOzL$!?9~0{sD&S_t^jd diff --git a/examples/screenshots/webgpu_tsl_vfx_flames.jpg b/examples/screenshots/webgpu_tsl_vfx_flames.jpg index c055af4fddd2b93fa9bfcf568b536c56dada5d6a..27233b0d7c132adc5219ef1ad85dbda74157a50e 100644 GIT binary patch delta 7354 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 [config.position=null] - Can be used to define the vertex positions in world space. + * @param {?Node} [config.position=null] - Can be used to define the billboard center position directly. + * When null, the center is derived automatically from `positionWorld`. * @param {boolean} [config.horizontal=true] - Whether to follow the camera rotation horizontally or not. * @param {boolean} [config.vertical=false] - Whether to follow the camera rotation vertically or not. + * @param {boolean} [config.horizontalRotation=false] - Whether to rotate around the Y axis to face the camera. * @return {Node} The updated vertex position in clip space. */ -export const billboarding = /*@__PURE__*/ Fn( ( { position = null, horizontal = true, vertical = false } ) => { +export const billboarding = /*@__PURE__*/ Fn( ( { position = null, horizontal = true, vertical = false, horizontalRotation = false } ) => { - let worldMatrix; + let center; if ( position !== null ) { - worldMatrix = modelWorldMatrix.toVar(); - worldMatrix[ 3 ][ 0 ] = position.x; - worldMatrix[ 3 ][ 1 ] = position.y; - worldMatrix[ 3 ][ 2 ] = position.z; + center = nodeObject( position ); } else { - worldMatrix = modelWorldMatrix; + center = positionWorld.sub( modelWorldMatrix.mul( vec4( positionGeometry, 0 ) ).xyz ); } + const worldMatrix = modelWorldMatrix.toVar(); + worldMatrix[ 3 ][ 0 ] = center.x; + worldMatrix[ 3 ][ 1 ] = center.y; + worldMatrix[ 3 ][ 2 ] = center.z; + const modelViewMatrix = cameraViewMatrix.mul( worldMatrix ); - if ( defined( horizontal ) ) { + const scaleX = modelWorldMatrix[ 0 ].length(); + const scaleY = modelWorldMatrix[ 1 ].length(); + const scaleZ = modelWorldMatrix[ 2 ].length(); + + let right, up, forward; + + if ( defined( horizontalRotation ) ) { + + const worldPosition = worldMatrix[ 3 ].xyz; + const look = cameraPosition.sub( worldPosition ); + const lookXZ = vec3( look.x, 0, look.z ).normalize(); + + const right_w = vec3( lookXZ.z, 0, lookXZ.x.negate() ); + + right = cameraViewMatrix.mul( vec4( right_w, 0 ) ).xyz.mul( scaleX ); + up = cameraViewMatrix[ 1 ].xyz.mul( scaleY ); + forward = cameraViewMatrix.mul( vec4( lookXZ, 0 ) ).xyz.mul( scaleZ ); + + } else { + + if ( defined( horizontal ) ) right = vec3( scaleX, 0, 0 ); + if ( defined( vertical ) ) up = vec3( 0, scaleY, 0 ); + + forward = vec3( 0, 0, 1 ); + + } + + if ( right ) { - modelViewMatrix[ 0 ][ 0 ] = modelWorldMatrix[ 0 ].length(); - modelViewMatrix[ 0 ][ 1 ] = 0; - modelViewMatrix[ 0 ][ 2 ] = 0; + modelViewMatrix[ 0 ][ 0 ] = right.x; + modelViewMatrix[ 0 ][ 1 ] = right.y; + modelViewMatrix[ 0 ][ 2 ] = right.z; } - if ( defined( vertical ) ) { + if ( up ) { - modelViewMatrix[ 1 ][ 0 ] = 0; - modelViewMatrix[ 1 ][ 1 ] = modelWorldMatrix[ 1 ].length(); - modelViewMatrix[ 1 ][ 2 ] = 0; + modelViewMatrix[ 1 ][ 0 ] = up.x; + modelViewMatrix[ 1 ][ 1 ] = up.y; + modelViewMatrix[ 1 ][ 2 ] = up.z; } - modelViewMatrix[ 2 ][ 0 ] = 0; - modelViewMatrix[ 2 ][ 1 ] = 0; - modelViewMatrix[ 2 ][ 2 ] = 1; + if ( forward ) { + + modelViewMatrix[ 2 ][ 0 ] = forward.x; + modelViewMatrix[ 2 ][ 1 ] = forward.y; + modelViewMatrix[ 2 ][ 2 ] = forward.z; + + } - return cameraProjectionMatrix.mul( modelViewMatrix ).mul( positionLocal ); + return cameraProjectionMatrix.mul( modelViewMatrix ).mul( positionGeometry ); } ); From 515d3ee92319549ae98219bd8cd9991f2ab9e4b3 Mon Sep 17 00:00:00 2001 From: "Mr.doob" Date: Mon, 10 Aug 2026 23:15:14 +0900 Subject: [PATCH 3/7] Updated builds. --- build/three.core.js | 184 ++++++++++----------- build/three.module.js | 32 +++- build/three.webgpu.js | 312 +++++++++++++++++++++++++++--------- build/three.webgpu.nodes.js | 312 +++++++++++++++++++++++++++--------- 4 files changed, 584 insertions(+), 256 deletions(-) diff --git a/build/three.core.js b/build/three.core.js index ce0ce4330b7d19..3d9343f14f380c 100644 --- a/build/three.core.js +++ b/build/three.core.js @@ -13185,13 +13185,15 @@ class Object3D extends EventDispatcher { object.uuid = this.uuid; object.type = this.type; - if ( this.name !== '' ) object.name = this.name; - if ( this.castShadow === true ) object.castShadow = true; - if ( this.receiveShadow === true ) object.receiveShadow = true; - if ( this.visible === false ) object.visible = false; - if ( this.frustumCulled === false ) object.frustumCulled = false; - if ( this.renderOrder !== 0 ) object.renderOrder = this.renderOrder; - if ( this.static !== false ) object.static = this.static; + object.name = this.name; + object.castShadow = this.castShadow; + object.receiveShadow = this.receiveShadow; + object.visible = this.visible; + object.frustumCulled = this.frustumCulled; + object.renderOrder = this.renderOrder; + object.static = this.static; + object.matrixAutoUpdate = this.matrixAutoUpdate; + if ( Object.keys( this.userData ).length > 0 ) object.userData = this.userData; object.layers = this.layers.mask; @@ -13200,8 +13202,6 @@ class Object3D extends EventDispatcher { if ( this.pivot !== null ) object.pivot = this.pivot.toArray(); - if ( this.matrixAutoUpdate === false ) object.matrixAutoUpdate = false; - if ( this.morphTargetDictionary !== undefined ) object.morphTargetDictionary = Object.assign( {}, this.morphTargetDictionary ); if ( this.morphTargetInfluences !== undefined ) object.morphTargetInfluences = this.morphTargetInfluences.slice(); @@ -15331,11 +15331,11 @@ class Scene extends Object3D { if ( this.fog !== null ) data.object.fog = this.fog.toJSON(); - if ( this.backgroundBlurriness > 0 ) data.object.backgroundBlurriness = this.backgroundBlurriness; - if ( this.backgroundIntensity !== 1 ) data.object.backgroundIntensity = this.backgroundIntensity; + data.object.backgroundBlurriness = this.backgroundBlurriness; + data.object.backgroundIntensity = this.backgroundIntensity; data.object.backgroundRotation = this.backgroundRotation.toArray(); - if ( this.environmentIntensity !== 1 ) data.object.environmentIntensity = this.environmentIntensity; + data.object.environmentIntensity = this.environmentIntensity; data.object.environmentRotation = this.environmentRotation.toArray(); return data; @@ -17560,9 +17560,9 @@ class BufferAttribute extends EventDispatcher { normalized: this.normalized }; - if ( this.name !== '' ) data.name = this.name; - if ( this.usage !== StaticDrawUsage ) data.usage = this.usage; - if ( this.gpuType !== FloatType ) data.gpuType = this.gpuType; + data.name = this.name; + data.usage = this.usage; + data.gpuType = this.gpuType; return data; @@ -19578,7 +19578,7 @@ class BufferGeometry extends EventDispatcher { data.uuid = this.uuid; data.type = ( this.parameters !== undefined && this._transformed === true ) ? 'BufferGeometry' : this.type; - if ( this.name !== '' ) data.name = this.name; + data.name = this.name; if ( Object.keys( this.userData ).length > 0 ) data.userData = this.userData; if ( this.parameters !== undefined && this._transformed !== true ) { @@ -20097,7 +20097,7 @@ class InterleavedBuffer { stride: this.stride }; - if ( this.usage !== StaticDrawUsage ) json.usage = this.usage; + json.usage = this.usage; return json; @@ -21663,10 +21663,61 @@ class Material extends EventDispatcher { }; // standard Material serialization + data.uuid = this.uuid; data.type = this.type; - if ( this.name !== '' ) data.name = this.name; + data.blending = this.blending; + data.side = this.side; + data.shadowSide = this.shadowSide; + data.vertexColors = this.vertexColors; + + data.opacity = this.opacity; + data.transparent = this.transparent; + + data.blendSrc = this.blendSrc; + data.blendDst = this.blendDst; + data.blendEquation = this.blendEquation; + data.blendSrcAlpha = this.blendSrcAlpha; + data.blendDstAlpha = this.blendDstAlpha; + data.blendEquationAlpha = this.blendEquationAlpha; + data.blendColor = this.blendColor.getHex(); + data.blendAlpha = this.blendAlpha; + + data.depthFunc = this.depthFunc; + data.depthTest = this.depthTest; + data.depthWrite = this.depthWrite; + data.colorWrite = this.colorWrite; + + data.clipIntersection = this.clipIntersection; + data.clipShadows = this.clipShadows; + + data.stencilWriteMask = this.stencilWriteMask; + data.stencilFunc = this.stencilFunc; + data.stencilRef = this.stencilRef; + data.stencilFuncMask = this.stencilFuncMask; + data.stencilFail = this.stencilFail; + data.stencilZFail = this.stencilZFail; + data.stencilZPass = this.stencilZPass; + data.stencilWrite = this.stencilWrite; + + data.polygonOffset = this.polygonOffset; + data.polygonOffsetFactor = this.polygonOffsetFactor; + data.polygonOffsetUnits = this.polygonOffsetUnits; + + data.dithering = this.dithering; + + data.alphaTest = this.alphaTest; + data.alphaHash = this.alphaHash; + data.alphaToCoverage = this.alphaToCoverage; + data.premultipliedAlpha = this.premultipliedAlpha; + data.forceSinglePass = this.forceSinglePass; + data.allowOverride = this.allowOverride; + + data.visible = this.visible; + data.toneMapped = this.toneMapped; + + data.name = this.name; if ( this.color && this.color.isColor ) data.color = this.color.getHex(); @@ -21677,7 +21728,7 @@ class Material extends EventDispatcher { if ( this.sheenColor && this.sheenColor.isColor ) data.sheenColor = this.sheenColor.getHex(); if ( this.sheenRoughness !== undefined ) data.sheenRoughness = this.sheenRoughness; if ( this.emissive && this.emissive.isColor ) data.emissive = this.emissive.getHex(); - if ( this.emissiveIntensity !== undefined && this.emissiveIntensity !== 1 ) data.emissiveIntensity = this.emissiveIntensity; + if ( this.emissiveIntensity !== undefined ) data.emissiveIntensity = this.emissiveIntensity; if ( this.specular && this.specular.isColor ) data.specular = this.specular.getHex(); if ( this.specularIntensity !== undefined ) data.specularIntensity = this.specularIntensity; @@ -21817,90 +21868,39 @@ class Material extends EventDispatcher { if ( this.transmissionMap && this.transmissionMap.isTexture ) data.transmissionMap = this.transmissionMap.toJSON( meta ).uuid; if ( this.thickness !== undefined ) data.thickness = this.thickness; if ( this.thicknessMap && this.thicknessMap.isTexture ) data.thicknessMap = this.thicknessMap.toJSON( meta ).uuid; - if ( this.attenuationDistance !== undefined && this.attenuationDistance !== Infinity ) data.attenuationDistance = this.attenuationDistance; + if ( this.attenuationDistance !== undefined ) data.attenuationDistance = this.attenuationDistance; if ( this.attenuationColor !== undefined ) data.attenuationColor = this.attenuationColor.getHex(); if ( this.size !== undefined ) data.size = this.size; - if ( this.shadowSide !== null ) data.shadowSide = this.shadowSide; if ( this.sizeAttenuation !== undefined ) data.sizeAttenuation = this.sizeAttenuation; - if ( this.blending !== NormalBlending ) data.blending = this.blending; - if ( this.side !== FrontSide ) data.side = this.side; - if ( this.vertexColors === true ) data.vertexColors = true; - - if ( this.opacity < 1 ) data.opacity = this.opacity; - if ( this.transparent === true ) data.transparent = true; - - if ( this.blendSrc !== SrcAlphaFactor ) data.blendSrc = this.blendSrc; - if ( this.blendDst !== OneMinusSrcAlphaFactor ) data.blendDst = this.blendDst; - if ( this.blendEquation !== AddEquation ) data.blendEquation = this.blendEquation; - if ( this.blendSrcAlpha !== null ) data.blendSrcAlpha = this.blendSrcAlpha; - if ( this.blendDstAlpha !== null ) data.blendDstAlpha = this.blendDstAlpha; - if ( this.blendEquationAlpha !== null ) data.blendEquationAlpha = this.blendEquationAlpha; - if ( this.blendColor && this.blendColor.isColor ) data.blendColor = this.blendColor.getHex(); - if ( this.blendAlpha !== 0 ) data.blendAlpha = this.blendAlpha; - - if ( this.depthFunc !== LessEqualDepth ) data.depthFunc = this.depthFunc; - if ( this.depthTest === false ) data.depthTest = this.depthTest; - if ( this.depthWrite === false ) data.depthWrite = this.depthWrite; - if ( this.colorWrite === false ) data.colorWrite = this.colorWrite; - if ( Array.isArray( this.clippingPlanes ) && this.clippingPlanes.length > 0 ) { data.clippingPlanes = this.clippingPlanes.map( plane => plane.toJSON() ); } - if ( this.clipIntersection === true ) data.clipIntersection = true; - if ( this.clipShadows === true ) data.clipShadows = true; - - if ( this.stencilWriteMask !== 0xff ) data.stencilWriteMask = this.stencilWriteMask; - if ( this.stencilFunc !== AlwaysStencilFunc ) data.stencilFunc = this.stencilFunc; - if ( this.stencilRef !== 0 ) data.stencilRef = this.stencilRef; - if ( this.stencilFuncMask !== 0xff ) data.stencilFuncMask = this.stencilFuncMask; - if ( this.stencilFail !== KeepStencilOp ) data.stencilFail = this.stencilFail; - if ( this.stencilZFail !== KeepStencilOp ) data.stencilZFail = this.stencilZFail; - if ( this.stencilZPass !== KeepStencilOp ) data.stencilZPass = this.stencilZPass; - if ( this.stencilWrite === true ) data.stencilWrite = this.stencilWrite; - // rotation (SpriteMaterial) - if ( this.rotation !== undefined && this.rotation !== 0 ) data.rotation = this.rotation; + if ( this.rotation !== undefined ) data.rotation = this.rotation; // depthPacking (MeshDepthMaterial) - if ( this.depthPacking !== undefined && this.depthPacking !== BasicDepthPacking ) data.depthPacking = this.depthPacking; + if ( this.depthPacking !== undefined ) data.depthPacking = this.depthPacking; - if ( this.polygonOffset === true ) data.polygonOffset = true; - if ( this.polygonOffsetFactor !== 0 ) data.polygonOffsetFactor = this.polygonOffsetFactor; - if ( this.polygonOffsetUnits !== 0 ) data.polygonOffsetUnits = this.polygonOffsetUnits; - - if ( this.linewidth !== undefined && this.linewidth !== 1 ) data.linewidth = this.linewidth; - if ( this.linecap !== undefined && this.linecap !== 'round' ) data.linecap = this.linecap; - if ( this.linejoin !== undefined && this.linejoin !== 'round' ) data.linejoin = this.linejoin; + if ( this.linewidth !== undefined ) data.linewidth = this.linewidth; + if ( this.linecap !== undefined ) data.linecap = this.linecap; + if ( this.linejoin !== undefined ) data.linejoin = this.linejoin; if ( this.dashSize !== undefined ) data.dashSize = this.dashSize; if ( this.gapSize !== undefined ) data.gapSize = this.gapSize; if ( this.scale !== undefined ) data.scale = this.scale; - if ( this.dithering === true ) data.dithering = true; - - if ( this.alphaTest > 0 ) data.alphaTest = this.alphaTest; - if ( this.alphaHash === true ) data.alphaHash = true; - if ( this.alphaToCoverage === true ) data.alphaToCoverage = true; - if ( this.premultipliedAlpha === true ) data.premultipliedAlpha = true; - if ( this.forceSinglePass === true ) data.forceSinglePass = true; - if ( this.allowOverride === false ) data.allowOverride = false; - - if ( this.wireframe === true ) data.wireframe = true; - if ( this.wireframeLinewidth > 1 ) data.wireframeLinewidth = this.wireframeLinewidth; - if ( this.wireframeLinecap !== 'round' ) data.wireframeLinecap = this.wireframeLinecap; - if ( this.wireframeLinejoin !== 'round' ) data.wireframeLinejoin = this.wireframeLinejoin; - - if ( this.flatShading === true ) data.flatShading = true; - - if ( this.visible === false ) data.visible = false; + if ( this.wireframe !== undefined ) data.wireframe = this.wireframe; + if ( this.wireframeLinewidth !== undefined ) data.wireframeLinewidth = this.wireframeLinewidth; + if ( this.wireframeLinecap !== undefined ) data.wireframeLinecap = this.wireframeLinecap; + if ( this.wireframeLinejoin !== undefined ) data.wireframeLinejoin = this.wireframeLinejoin; - if ( this.toneMapped === false ) data.toneMapped = false; + if ( this.flatShading !== undefined ) data.flatShading = this.flatShading; - if ( this.fog === false ) data.fog = false; + if ( this.fog !== undefined ) data.fog = this.fog; if ( Object.keys( this.userData ).length > 0 ) data.userData = this.userData; @@ -22950,7 +22950,7 @@ class LOD extends Object3D { const data = super.toJSON( meta ); - if ( this.autoUpdate === false ) data.object.autoUpdate = false; + data.object.autoUpdate = this.autoUpdate; data.object.levels = []; @@ -29655,7 +29655,7 @@ class DepthTexture extends Texture { const data = super.toJSON( meta ); - if ( this.compareFunction !== null ) data.compareFunction = this.compareFunction; + data.compareFunction = this.compareFunction; return data; @@ -46447,12 +46447,12 @@ class LightShadow { const object = {}; - if ( this.intensity !== 1 ) object.intensity = this.intensity; - if ( this.bias !== 0 ) object.bias = this.bias; - if ( this.normalBias !== 0 ) object.normalBias = this.normalBias; - if ( this.radius !== 1 ) object.radius = this.radius; - if ( this.blurSamples !== 8 ) object.blurSamples = this.blurSamples; - if ( this.mapSize.x !== 512 || this.mapSize.y !== 512 ) object.mapSize = this.mapSize.toArray(); + object.intensity = this.intensity; + object.bias = this.bias; + object.normalBias = this.normalBias; + object.radius = this.radius; + object.blurSamples = this.blurSamples; + object.mapSize = this.mapSize.toArray(); object.camera = this.camera.toJSON( false ).object; delete object.camera.matrix; @@ -47101,8 +47101,8 @@ class SpotLightShadow extends LightShadow { const object = super.toJSON(); - if ( this.focus !== 1 ) object.focus = this.focus; - if ( this.aspect !== 1 ) object.aspect = this.aspect; + object.focus = this.focus; + object.aspect = this.aspect; return object; diff --git a/build/three.module.js b/build/three.module.js index 15315555085096..47f1a5b349733b 100644 --- a/build/three.module.js +++ b/build/three.module.js @@ -2351,8 +2351,8 @@ function WebGLCapabilities( gl, extensions, parameters, utils ) { const halfFloatSupportedByExt = ( textureType === HalfFloatType ) && ( extensions.has( 'EXT_color_buffer_half_float' ) || extensions.has( 'EXT_color_buffer_float' ) ); - if ( textureType !== UnsignedByteType && utils.convert( textureType ) !== gl.getParameter( gl.IMPLEMENTATION_COLOR_READ_TYPE ) && // Edge and Chrome Mac < 52 (#9513) - textureType !== FloatType && ! halfFloatSupportedByExt ) { + if ( textureType !== UnsignedByteType && textureType !== FloatType && ! halfFloatSupportedByExt && + utils.convert( textureType ) !== gl.getParameter( gl.IMPLEMENTATION_COLOR_READ_TYPE ) ) { // Edge and Chrome Mac < 52 (#9513) return false; @@ -19000,6 +19000,23 @@ class WebGLRenderer { }; + function getReadableState( texture ) { + + const textureProperties = properties.get( texture ); + + if ( textureProperties.__readFormat !== texture.format || textureProperties.__readType !== texture.type ) { + + textureProperties.__readFormat = texture.format; + textureProperties.__readType = texture.type; + textureProperties.__formatReadable = capabilities.textureFormatReadable( texture.format ); + textureProperties.__typeReadable = capabilities.textureTypeReadable( texture.type ); + + } + + return textureProperties; + + } + /** * Reads the pixel data from the given render target into the given buffer. * @@ -19043,14 +19060,16 @@ class WebGLRenderer { if ( renderTarget.textures.length > 1 ) _gl.readBuffer( _gl.COLOR_ATTACHMENT0 + textureIndex ); - if ( ! capabilities.textureFormatReadable( textureFormat ) ) { + const readableState = getReadableState( texture ); + + if ( readableState.__formatReadable === false ) { error( 'WebGLRenderer.readRenderTargetPixels: renderTarget is not in RGBA or implementation defined format.' ); return; } - if ( ! capabilities.textureTypeReadable( textureType ) ) { + if ( readableState.__typeReadable === false ) { error( 'WebGLRenderer.readRenderTargetPixels: renderTarget is not in UnsignedByteType or implementation defined type.' ); return; @@ -19125,14 +19144,15 @@ class WebGLRenderer { if ( renderTarget.textures.length > 1 ) _gl.readBuffer( _gl.COLOR_ATTACHMENT0 + textureIndex ); + const readableState = getReadableState( texture ); - if ( ! capabilities.textureFormatReadable( textureFormat ) ) { + if ( readableState.__formatReadable === false ) { throw new Error( 'THREE.WebGLRenderer.readRenderTargetPixelsAsync: renderTarget is not in RGBA or implementation defined format.' ); } - if ( ! capabilities.textureTypeReadable( textureType ) ) { + if ( readableState.__typeReadable === false ) { throw new Error( 'THREE.WebGLRenderer.readRenderTargetPixelsAsync: renderTarget is not in UnsignedByteType or implementation defined type.' ); diff --git a/build/three.webgpu.js b/build/three.webgpu.js index b355f8870290c2..f871534f076875 100644 --- a/build/three.webgpu.js +++ b/build/three.webgpu.js @@ -3,7 +3,7 @@ * Copyright 2010-2026 Three.js Authors * SPDX-License-Identifier: MIT */ -import { DynamicDrawUsage, RenderObjectRefreshType, Color, Vector2, Vector3, Vector4, Matrix2, Matrix3, Matrix4, UnsignedIntType, IntType, error, RedFormat, RedIntegerFormat, DepthFormat, DepthStencilFormat, AlphaFormat, RGFormat, RGIntegerFormat, RGBFormat, RGBIntegerFormat, EventDispatcher, MathUtils, warn, WebGLCoordinateSystem, WebGPUCoordinateSystem, ColorManagement, SRGBTransfer, NoToneMapping, StaticDrawUsage, InterleavedBufferAttribute, InterleavedBuffer, NoColorSpace, log as log$1, warnOnce, Texture, Compatibility, LessCompare, LessEqualCompare, GreaterCompare, GreaterEqualCompare, NearestFilter, Sphere, BackSide, DoubleSide, CubeTexture, CubeReflectionMapping, CubeRefractionMapping, TangentSpaceNormalMap, NoNormalPacking, NormalRGPacking, NormalGAPacking, ObjectSpaceNormalMap, RED_GREEN_RGTC2_Format, RG11_EAC_Format, InstancedBufferAttribute, InstancedInterleavedBuffer, DataTexture, DataArrayTexture, FloatType, FramebufferTexture, LinearMipmapLinearFilter, DepthTexture, Material, LineBasicMaterial, LineDashedMaterial, NoBlending, MeshNormalMaterial, SRGBColorSpace, RenderTarget, BoxGeometry, Mesh, Scene, LinearFilter, CubeCamera, EquirectangularReflectionMapping, EquirectangularRefractionMapping, AddOperation, MixOperation, MultiplyOperation, MeshBasicMaterial, MeshLambertMaterial, MeshPhongMaterial, HalfFloatType, ClampToEdgeWrapping, BufferGeometry, OrthographicCamera, PerspectiveCamera, LinearSRGBColorSpace, RGBAFormat, CubeUVReflectionMapping, BufferAttribute, MeshStandardMaterial, MeshPhysicalMaterial, MeshToonMaterial, MeshMatcapMaterial, SpriteMaterial, PointsMaterial, ShadowMaterial, Uint32BufferAttribute, Uint16BufferAttribute, ByteType, UnsignedByteType, ShortType, UnsignedShortType, UnsignedShort4444Type, UnsignedShort5551Type, UnsignedInt248Type, UnsignedInt5999Type, UnsignedInt101111Type, NormalBlending, SrcAlphaFactor, OneMinusSrcAlphaFactor, AddEquation, MaterialBlending, Object3D, LinearMipMapLinearFilter, Plane, Float32BufferAttribute, UVMapping, PCFShadowMap, VSMShadowMap, BasicShadowMap, CubeDepthTexture, SphereGeometry, LinearMipmapNearestFilter, NearestMipmapLinearFilter, Float16BufferAttribute, yieldToMain, REVISION, ArrayCamera, PlaneGeometry, FrontSide, CustomBlending, ZeroFactor, CylinderGeometry, Quaternion, WebXRController, RAD2DEG, PCFSoftShadowMap, FrustumArray, Frustum, RGBAIntegerFormat, TimestampQuery, createCanvasElement, MaxEquation, MinEquation, ReverseSubtractEquation, SubtractEquation, OneMinusConstantAlphaFactor, ConstantAlphaFactor, OneMinusConstantColorFactor, ConstantColorFactor, OneMinusDstAlphaFactor, OneMinusDstColorFactor, OneMinusSrcColorFactor, DstAlphaFactor, DstColorFactor, SrcAlphaSaturateFactor, SrcColorFactor, OneFactor, CullFaceNone, CullFaceBack, CullFaceFront, MultiplyBlending, SubtractiveBlending, AdditiveBlending, NotEqualDepth, GreaterDepth, GreaterEqualDepth, EqualDepth, LessEqualDepth, LessDepth, AlwaysDepth, NeverDepth, ReversedDepthFuncs, RGB_S3TC_DXT1_Format, RGBA_S3TC_DXT1_Format, RGBA_S3TC_DXT3_Format, RGBA_S3TC_DXT5_Format, RGB_PVRTC_4BPPV1_Format, RGB_PVRTC_2BPPV1_Format, RGBA_PVRTC_4BPPV1_Format, RGBA_PVRTC_2BPPV1_Format, RGB_ETC1_Format, RGB_ETC2_Format, RGBA_ETC2_EAC_Format, R11_EAC_Format, SIGNED_R11_EAC_Format, SIGNED_RG11_EAC_Format, RGBA_ASTC_4x4_Format, RGBA_ASTC_5x4_Format, RGBA_ASTC_5x5_Format, RGBA_ASTC_6x5_Format, RGBA_ASTC_6x6_Format, RGBA_ASTC_8x5_Format, RGBA_ASTC_8x6_Format, RGBA_ASTC_8x8_Format, RGBA_ASTC_10x5_Format, RGBA_ASTC_10x6_Format, RGBA_ASTC_10x8_Format, RGBA_ASTC_10x10_Format, RGBA_ASTC_12x10_Format, RGBA_ASTC_12x12_Format, RGBA_BPTC_Format, RGB_BPTC_SIGNED_Format, RGB_BPTC_UNSIGNED_Format, RED_RGTC1_Format, SIGNED_RED_RGTC1_Format, SIGNED_RED_GREEN_RGTC2_Format, MirroredRepeatWrapping, RepeatWrapping, NearestMipmapNearestFilter, NotEqualCompare, EqualCompare, AlwaysCompare, NeverCompare, LinearTransfer, getByteLength, isTypedArray, NotEqualStencilFunc, GreaterStencilFunc, GreaterEqualStencilFunc, EqualStencilFunc, LessEqualStencilFunc, LessStencilFunc, AlwaysStencilFunc, NeverStencilFunc, DecrementWrapStencilOp, IncrementWrapStencilOp, DecrementStencilOp, IncrementStencilOp, InvertStencilOp, ReplaceStencilOp, ZeroStencilOp, KeepStencilOp, SpotLight, PointLight, DirectionalLight, RectAreaLight, AmbientLight, HemisphereLight, LightProbe, LinearToneMapping, ReinhardToneMapping, CineonToneMapping, ACESFilmicToneMapping, AgXToneMapping, NeutralToneMapping, Group, Loader, FileLoader, MaterialLoader, ObjectLoader } from './three.core.js'; +import { DynamicDrawUsage, RenderObjectRefreshType, Color, Vector2, Vector3, Vector4, Matrix2, Matrix3, Matrix4, UnsignedIntType, IntType, error, RedFormat, RedIntegerFormat, DepthFormat, DepthStencilFormat, AlphaFormat, RGFormat, RGIntegerFormat, RGBFormat, RGBIntegerFormat, EventDispatcher, MathUtils, warn, WebGLCoordinateSystem, WebGPUCoordinateSystem, ColorManagement, SRGBTransfer, NoToneMapping, StaticDrawUsage, InterleavedBufferAttribute, InterleavedBuffer, NoColorSpace, log as log$1, warnOnce, Texture, Compatibility, LessCompare, LessEqualCompare, GreaterCompare, GreaterEqualCompare, NearestFilter, Sphere, BackSide, DoubleSide, CubeTexture, CubeReflectionMapping, CubeRefractionMapping, TangentSpaceNormalMap, NoNormalPacking, NormalRGPacking, NormalGAPacking, ObjectSpaceNormalMap, RED_GREEN_RGTC2_Format, RG11_EAC_Format, InstancedBufferAttribute, InstancedInterleavedBuffer, DataTexture, RGBAFormat, FloatType, DataArrayTexture, FramebufferTexture, LinearMipmapLinearFilter, DepthTexture, Material, LineBasicMaterial, LineDashedMaterial, NoBlending, MeshNormalMaterial, SRGBColorSpace, RenderTarget, BoxGeometry, Mesh, Scene, LinearFilter, CubeCamera, EquirectangularReflectionMapping, EquirectangularRefractionMapping, AddOperation, MixOperation, MultiplyOperation, MeshBasicMaterial, MeshLambertMaterial, MeshPhongMaterial, HalfFloatType, ClampToEdgeWrapping, BufferGeometry, OrthographicCamera, PerspectiveCamera, LinearSRGBColorSpace, CubeUVReflectionMapping, BufferAttribute, MeshStandardMaterial, MeshPhysicalMaterial, MeshToonMaterial, MeshMatcapMaterial, SpriteMaterial, PointsMaterial, ShadowMaterial, Uint32BufferAttribute, Uint16BufferAttribute, ByteType, UnsignedByteType, ShortType, UnsignedShortType, UnsignedShort4444Type, UnsignedShort5551Type, UnsignedInt248Type, UnsignedInt5999Type, UnsignedInt101111Type, NormalBlending, SrcAlphaFactor, OneMinusSrcAlphaFactor, AddEquation, MaterialBlending, Object3D, LinearMipMapLinearFilter, Plane, Float32BufferAttribute, UVMapping, PCFShadowMap, VSMShadowMap, BasicShadowMap, CubeDepthTexture, SphereGeometry, LinearMipmapNearestFilter, NearestMipmapLinearFilter, Float16BufferAttribute, yieldToMain, REVISION, ArrayCamera, PlaneGeometry, FrontSide, CustomBlending, ZeroFactor, CylinderGeometry, Quaternion, WebXRController, RAD2DEG, PCFSoftShadowMap, FrustumArray, Frustum, RGBAIntegerFormat, TimestampQuery, createCanvasElement, MaxEquation, MinEquation, ReverseSubtractEquation, SubtractEquation, OneMinusConstantAlphaFactor, ConstantAlphaFactor, OneMinusConstantColorFactor, ConstantColorFactor, OneMinusDstAlphaFactor, OneMinusDstColorFactor, OneMinusSrcColorFactor, DstAlphaFactor, DstColorFactor, SrcAlphaSaturateFactor, SrcColorFactor, OneFactor, CullFaceNone, CullFaceBack, CullFaceFront, MultiplyBlending, SubtractiveBlending, AdditiveBlending, NotEqualDepth, GreaterDepth, GreaterEqualDepth, EqualDepth, LessEqualDepth, LessDepth, AlwaysDepth, NeverDepth, ReversedDepthFuncs, RGB_S3TC_DXT1_Format, RGBA_S3TC_DXT1_Format, RGBA_S3TC_DXT3_Format, RGBA_S3TC_DXT5_Format, RGB_PVRTC_4BPPV1_Format, RGB_PVRTC_2BPPV1_Format, RGBA_PVRTC_4BPPV1_Format, RGBA_PVRTC_2BPPV1_Format, RGB_ETC1_Format, RGB_ETC2_Format, RGBA_ETC2_EAC_Format, R11_EAC_Format, SIGNED_R11_EAC_Format, SIGNED_RG11_EAC_Format, RGBA_ASTC_4x4_Format, RGBA_ASTC_5x4_Format, RGBA_ASTC_5x5_Format, RGBA_ASTC_6x5_Format, RGBA_ASTC_6x6_Format, RGBA_ASTC_8x5_Format, RGBA_ASTC_8x6_Format, RGBA_ASTC_8x8_Format, RGBA_ASTC_10x5_Format, RGBA_ASTC_10x6_Format, RGBA_ASTC_10x8_Format, RGBA_ASTC_10x10_Format, RGBA_ASTC_12x10_Format, RGBA_ASTC_12x12_Format, RGBA_BPTC_Format, RGB_BPTC_SIGNED_Format, RGB_BPTC_UNSIGNED_Format, RED_RGTC1_Format, SIGNED_RED_RGTC1_Format, SIGNED_RED_GREEN_RGTC2_Format, MirroredRepeatWrapping, RepeatWrapping, NearestMipmapNearestFilter, NotEqualCompare, EqualCompare, AlwaysCompare, NeverCompare, LinearTransfer, getByteLength, isTypedArray, NotEqualStencilFunc, GreaterStencilFunc, GreaterEqualStencilFunc, EqualStencilFunc, LessEqualStencilFunc, LessStencilFunc, AlwaysStencilFunc, NeverStencilFunc, DecrementWrapStencilOp, IncrementWrapStencilOp, DecrementStencilOp, IncrementStencilOp, InvertStencilOp, ReplaceStencilOp, ZeroStencilOp, KeepStencilOp, SpotLight, PointLight, DirectionalLight, RectAreaLight, AmbientLight, HemisphereLight, LightProbe, LinearToneMapping, ReinhardToneMapping, CineonToneMapping, ACESFilmicToneMapping, AgXToneMapping, NeutralToneMapping, Group, Loader, FileLoader, MaterialLoader, ObjectLoader } from './three.core.js'; export { AdditiveAnimationBlendMode, AnimationAction, AnimationClip, AnimationLoader, AnimationMixer, AnimationObjectGroup, AnimationUtils, ArcCurve, ArrowHelper, AttachedBindMode, Audio, AudioAnalyser, AudioContext, AudioListener, AudioLoader, AxesHelper, BasicDepthPacking, BatchedMesh, BezierInterpolant, Bone, BooleanKeyframeTrack, Box2, Box3, Box3Helper, BoxHelper, BufferGeometryLoader, Cache, Camera, CameraHelper, CanvasTexture, CapsuleGeometry, CatmullRomCurve3, CircleGeometry, Clock, ColorKeyframeTrack, CompressedArrayTexture, CompressedCubeTexture, CompressedTexture, CompressedTextureLoader, ConeGeometry, Controls, CubeTextureLoader, CubicBezierCurve, CubicBezierCurve3, CubicInterpolant, CullFaceFrontBack, Curve, CurvePath, CustomToneMapping, Cylindrical, Data3DTexture, DataTextureLoader, DataUtils, DefaultLoadingManager, DetachedBindMode, DirectionalLightHelper, DiscreteInterpolant, DodecahedronGeometry, DynamicCopyUsage, DynamicReadUsage, EdgesGeometry, EllipseCurve, Euler, ExternalTexture, ExtrudeGeometry, Fog, FogExp2, GLBufferAttribute, GLSL1, GLSL3, GridHelper, HTMLTexture, HemisphereLightHelper, IcosahedronGeometry, ImageBitmapLoader, ImageLoader, ImageUtils, InstancedBufferGeometry, InstancedMesh, Int16BufferAttribute, Int32BufferAttribute, Int8BufferAttribute, Interpolant, InterpolateBezier, InterpolateDiscrete, InterpolateLinear, InterpolateSmooth, InterpolationSamplingMode, InterpolationSamplingType, KeyframeTrack, LOD, LatheGeometry, Layers, Light, Line, Line3, LineCurve, LineCurve3, LineLoop, LineSegments, LinearInterpolant, LinearMipMapNearestFilter, LoaderUtils, LoadingManager, LoopOnce, LoopPingPong, LoopRepeat, MOUSE, MeshDepthMaterial, MeshDistanceMaterial, NearestMipMapLinearFilter, NearestMipMapNearestFilter, NormalAnimationBlendMode, NumberKeyframeTrack, OctahedronGeometry, Path, PlaneHelper, PointLightHelper, Points, PolarGridHelper, PolyhedronGeometry, PositionalAudio, PropertyBinding, PropertyMixer, QuadraticBezierCurve, QuadraticBezierCurve3, QuaternionKeyframeTrack, QuaternionLinearInterpolant, RGBADepthPacking, RGBDepthPacking, RGDepthPacking, RawShaderMaterial, Ray, Raycaster, RenderTarget3D, RingGeometry, ShaderMaterial, Shape, ShapeGeometry, ShapePath, ShapeUtils, Skeleton, SkeletonHelper, SkinnedMesh, Source, Spherical, SphericalHarmonics3, SplineCurve, SpotLightHelper, Sprite, StaticCopyUsage, StaticReadUsage, StereoCamera, StreamCopyUsage, StreamDrawUsage, StreamReadUsage, StringKeyframeTrack, TOUCH, TetrahedronGeometry, TextureLoader, TextureUtils, Timer, TorusGeometry, TorusKnotGeometry, Triangle, TriangleFanDrawMode, TriangleStripDrawMode, TrianglesDrawMode, TubeGeometry, Uint8BufferAttribute, Uint8ClampedBufferAttribute, Uniform, UniformsGroup, VectorKeyframeTrack, VideoFrameTexture, VideoTexture, WebGL3DRenderTarget, WebGLArrayRenderTarget, WebGLRenderTarget, WireframeGeometry, WrapAroundEnding, ZeroCurvatureEnding, ZeroSlopeEnding, getConsoleFunction, setConsoleFunction } from './three.core.js'; const refreshUniforms = [ @@ -5782,6 +5782,31 @@ class UniformNode extends InputNode { } + /** + * Uniform nodes with the same hash share a single uniform. This method returns the node + * the shared uniform refers to which is the first node registered for the hash. + * + * @param {NodeBuilder} builder - The current node builder. + * @return {UniformNode} The node the shared uniform refers to. + */ + getSharedNode( builder ) { + + const hash = this.getUniformHash( builder ); + + let sharedNode = builder.getNodeFromHash( hash ); + + if ( sharedNode === undefined ) { + + builder.setHashNode( this, hash ); + + sharedNode = this; + + } + + return sharedNode; + + } + onUpdate( callback, updateType ) { callback = callback.bind( this ); @@ -5818,18 +5843,7 @@ class UniformNode extends InputNode { const type = this.getNodeType( builder ); - const hash = this.getUniformHash( builder ); - - let sharedNode = builder.getNodeFromHash( hash ); - - if ( sharedNode === undefined ) { - - builder.setHashNode( this, hash ); - - sharedNode = this; - - } - + const sharedNode = this.getSharedNode( builder ); const sharedNodeType = sharedNode.getInputType( builder ); const nodeUniform = builder.getUniformFromNode( sharedNode, sharedNodeType, builder.shaderStage, this.name || builder.context.nodeName ); @@ -12694,6 +12708,15 @@ class TextureNode extends UniformNode { */ this._flipYUniform = null; + /** + * Whether the node is used as a comparison sampler, e.g. via `samplerComparison()`. + * + * @private + * @type {boolean} + * @default false + */ + this._samplerComparison = false; + this.setUpdateMatrix( uvNode === null ); } @@ -13059,6 +13082,18 @@ class TextureNode extends UniformNode { if ( /^sampler/.test( output ) ) { + if ( output === 'samplerComparison' ) { + + this._samplerComparison = true; + + // texture nodes with the same texture share a single uniform so it's + // important to set the flag on the node the binding refers to as well + + const sharedNode = this.getSharedNode( builder ); + sharedNode._samplerComparison = true; + + } + return textureProperty + '_sampler'; } else if ( builder.isReference( output ) ) { @@ -13338,14 +13373,14 @@ class TextureNode extends UniformNode { } /** - * Returns `true` if the texture is sampled with a plain gather (`textureGather`), - * meaning a gather without a compare value. + * Returns `true` if the texture is sampled with a depth comparison, + * meaning the node must be bound with a comparison sampler. * - * @return {boolean} Whether a plain gather is used or not. + * @return {boolean} Whether comparison sampling is used or not. */ - isPlainGather() { + isSampleCompare() { - return this.gatherNode !== null && this.compareNode === null; + return this.compareNode !== null || this._samplerComparison === true; } @@ -14118,7 +14153,7 @@ class ScreenNode extends Node { /** * Constructs a new screen node. * - * @param {('coordinate'|'viewport'|'size'|'uv'|'dpr')} scope - The node's scope. + * @param {('coordinate'|'viewport'|'size'|'uv')} scope - The node's scope. */ constructor( scope ) { @@ -14131,21 +14166,11 @@ class ScreenNode extends Node { * - `ScreenNode.VIEWPORT`: The current viewport defined as a four-dimensional vector. * - `ScreenNode.SIZE`: The dimensions of the current bound framebuffer. * - `ScreenNode.UV`: Normalized coordinates. - * - `ScreenNode.DPR`: Device pixel ratio. * - * @type {('coordinate'|'viewport'|'size'|'uv'|'dpr')} + * @type {('coordinate'|'viewport'|'size'|'uv')} */ this.scope = scope; - /** - * This output node. - * - * @private - * @type {?Node} - * @default null - */ - this._output = null; - /** * This flag can be used for type testing. * @@ -14160,11 +14185,10 @@ class ScreenNode extends Node { /** * This method is overwritten since the node type depends on the selected scope. * - * @return {('float'|'vec2'|'vec4')} The node type. + * @return {('vec2'|'vec4')} The node type. */ generateNodeType() { - if ( this.scope === ScreenNode.DPR ) return 'float'; if ( this.scope === ScreenNode.VIEWPORT ) return 'vec4'; else return 'vec2'; @@ -14179,7 +14203,7 @@ class ScreenNode extends Node { let updateType = NodeUpdateType.NONE; - if ( this.scope === ScreenNode.SIZE || this.scope === ScreenNode.VIEWPORT || this.scope === ScreenNode.DPR ) { + if ( this.scope === ScreenNode.SIZE || this.scope === ScreenNode.VIEWPORT ) { updateType = NodeUpdateType.RENDER; @@ -14215,10 +14239,6 @@ class ScreenNode extends Node { } - } else if ( this.scope === ScreenNode.DPR ) { - - this._output.value = renderer.getPixelRatio(); - } else { if ( renderTarget !== null ) { @@ -14250,18 +14270,12 @@ class ScreenNode extends Node { output = uniform( _viewportVec || ( _viewportVec = new Vector4() ) ).setGroup( renderGroup ); - } else if ( scope === ScreenNode.DPR ) { - - output = uniform( 1 ).setGroup( renderGroup ); - } else { output = vec2( screenCoordinate.div( screenSize ) ); } - this._output = output; - return output; } @@ -14296,7 +14310,6 @@ ScreenNode.COORDINATE = 'coordinate'; ScreenNode.VIEWPORT = 'viewport'; ScreenNode.SIZE = 'size'; ScreenNode.UV = 'uv'; -ScreenNode.DPR = 'dpr'; // Screen @@ -14304,9 +14317,9 @@ ScreenNode.DPR = 'dpr'; * TSL object that represents the current DPR. * * @tsl - * @type {ScreenNode} + * @type {UniformNode} */ -const screenDPR = /*@__PURE__*/ nodeImmutable( ScreenNode, ScreenNode.DPR ); +const screenDPR = /*@__PURE__*/ uniform( 1 ).setGroup( renderGroup ).onRenderUpdate( ( { renderer } ) => renderer.getPixelRatio() ); /** * TSL object that represents normalized screen coordinates, unitless in `[0, 1]`. @@ -18981,6 +18994,76 @@ const batch = /*@__PURE__*/ Fn( ( [ batchMesh ], builder ) => { const _skeletonsUpdated = /*@__PURE__*/ new WeakMap(); const _previousBoneMatricesData = /*@__PURE__*/ new WeakMap(); +/** + * Creates an accessor for bone matrices stored in a bone texture. + * + * @param {TextureNode} boneTexture - The bone texture node. + * @returns {Object} An accessor with the same `element()` interface as a buffer node. + */ +function getBoneTextureMatrices( boneTexture ) { + + return { + element: ( i ) => { + + const size = int( textureSize( boneTexture ).x ).toConst(); + const j = int( i ).mul( 4 ).toConst(); + const y = j.div( size ).toConst(); + const x = j.sub( y.mul( size ) ).toConst(); + + return mat4( + boneTexture.load( ivec2( x, y ) ), + boneTexture.load( ivec2( x.add( 1 ), y ) ), + boneTexture.load( ivec2( x.add( 2 ), y ) ), + boneTexture.load( ivec2( x.add( 3 ), y ) ) + ); + + } + }; + +} + +/** + * Creates the bone matrices node. Skeletons that fit within the uniform buffer limit + * use a uniform buffer, larger skeletons fall back to a bone texture. + * + * @param {NodeBuilder} builder - The current node builder. + * @param {Skeleton} skeleton - The skeleton. + * @returns {Object} The bone matrices node. + */ +function getBoneMatricesNode( builder, skeleton ) { + + let node; + + const uniformBufferSize = skeleton.bones.length * 16 * 4; + + if ( uniformBufferSize <= builder.getUniformBufferLimit() ) { + + node = referenceBuffer( 'skeleton.boneMatrices', 'mat4', skeleton.bones.length ); + + } else { + + if ( skeleton.boneTexture === null ) skeleton.computeBoneTexture(); + + const boneTexture = texture( skeleton.boneTexture ); + + OnObjectUpdate( ( { object } ) => { + + const skeleton = object.skeleton; + + if ( skeleton.boneTexture === null ) skeleton.computeBoneTexture(); + + boneTexture.value = skeleton.boneTexture; + + } ); + + node = getBoneTextureMatrices( boneTexture ); + + } + + return node; + +} + /** * Computes the skinned position by applying bone matrices based on weights. * @@ -19057,6 +19140,7 @@ function getSkinnedNormalAndTangent( boneMatrices, normal, tangent, bindMatrix, * Retrieves or initializes the previous frame skinned position node for motion vectors. * Uses a WeakMap to cache previous frame bone matrix arrays and their TSL buffer nodes. * + * @param {NodeBuilder} builder - The current node builder. * @param {SkinnedMesh} skinnedMesh - The skinned mesh. * @param {Node} bindMatrixNode - The bind matrix node. * @param {Node} bindMatrixInverseNode - The inverse bind matrix node. @@ -19064,7 +19148,7 @@ function getSkinnedNormalAndTangent( boneMatrices, normal, tangent, bindMatrix, * @param {Node} skinWeightNode - The skin weight attribute. * @returns {Node} The skinned position from the previous frame. */ -function getPreviousSkinnedPosition( skinnedMesh, bindMatrixNode, bindMatrixInverseNode, skinIndexNode, skinWeightNode ) { +function getPreviousSkinnedPosition( builder, skinnedMesh, bindMatrixNode, bindMatrixInverseNode, skinIndexNode, skinWeightNode ) { const skeleton = skinnedMesh.skeleton; @@ -19074,12 +19158,35 @@ function getPreviousSkinnedPosition( skinnedMesh, bindMatrixNode, bindMatrixInve skeleton.update(); - const previousBoneMatrices = new Float32Array( skeleton.boneMatrices ); + const uniformBufferSize = skeleton.bones.length * 16 * 4; - data = { - previousBoneMatrices, - node: buffer( previousBoneMatrices, 'mat4', skeleton.bones.length ) - }; + if ( uniformBufferSize <= builder.getUniformBufferLimit() ) { + + const previousBoneMatrices = new Float32Array( skeleton.boneMatrices ); + + data = { + previousBoneMatrices, + previousBoneTexture: null, + node: buffer( previousBoneMatrices, 'mat4', skeleton.bones.length ) + }; + + } else { + + if ( skeleton.boneTexture === null ) skeleton.computeBoneTexture(); + + const { width, height } = skeleton.boneTexture.image; + + const previousBoneMatrices = new Float32Array( skeleton.boneMatrices ); + const previousBoneTexture = new DataTexture( previousBoneMatrices, width, height, RGBAFormat, FloatType ); + previousBoneTexture.needsUpdate = true; + + data = { + previousBoneMatrices, + previousBoneTexture, + node: getBoneTextureMatrices( texture( previousBoneTexture ) ) + }; + + } _previousBoneMatricesData.set( skeleton, data ); @@ -19103,7 +19210,7 @@ const skinning = /*@__PURE__*/ Fn( ( [ skinnedMesh ], builder ) => { const skinWeightNode = attribute( 'skinWeight', 'vec4' ); const bindMatrixNode = reference( 'bindMatrix', 'mat4' ); const bindMatrixInverseNode = reference( 'bindMatrixInverse', 'mat4' ); - const boneMatricesNode = referenceBuffer( 'skeleton.boneMatrices', 'mat4', skinnedMesh.skeleton.bones.length ); + const boneMatricesNode = getBoneMatricesNode( builder, skinnedMesh.skeleton ); OnObjectUpdate( ( { object, frameId } ) => { @@ -19119,6 +19226,12 @@ const skinning = /*@__PURE__*/ Fn( ( [ skinnedMesh ], builder ) => { skeletonData.previousBoneMatrices.set( skeleton.boneMatrices ); + if ( skeletonData.previousBoneTexture !== null ) { + + skeletonData.previousBoneTexture.needsUpdate = true; + + } + } skeleton.update(); @@ -19129,7 +19242,7 @@ const skinning = /*@__PURE__*/ Fn( ( [ skinnedMesh ], builder ) => { if ( builder.needsPreviousData() ) { - const previousSkinnedPosition = getPreviousSkinnedPosition( skinnedMesh, bindMatrixNode, bindMatrixInverseNode, skinIndexNode, skinWeightNode ); + const previousSkinnedPosition = getPreviousSkinnedPosition( builder, skinnedMesh, bindMatrixNode, bindMatrixInverseNode, skinIndexNode, skinWeightNode ); positionPrevious.assign( previousSkinnedPosition ); @@ -19186,6 +19299,12 @@ const computeSkinning = /*@__PURE__*/ Fn( ( [ skinnedMesh, toPosition = null ], state.previousBoneMatrices.set( skeleton.boneMatrices ); + if ( state.previousBoneTexture !== null ) { + + state.previousBoneTexture.needsUpdate = true; + + } + } skeleton.update(); @@ -19196,7 +19315,7 @@ const computeSkinning = /*@__PURE__*/ Fn( ( [ skinnedMesh, toPosition = null ], if ( builder.needsPreviousData() ) { - const previousSkinnedPosition = getPreviousSkinnedPosition( skinnedMesh, bindMatrixNode, bindMatrixInverseNode, skinIndexNode, skinWeightNode ); + const previousSkinnedPosition = getPreviousSkinnedPosition( builder, skinnedMesh, bindMatrixNode, bindMatrixInverseNode, skinIndexNode, skinWeightNode ); positionPrevious.assign( previousSkinnedPosition ); @@ -37745,51 +37864,86 @@ const spherizeUV = /*@__PURE__*/ Fn( ( [ uv, strength, center = vec2( 0.5 ) ] ) * @tsl * @function * @param {Object} config - The configuration object. - * @param {?Node} [config.position=null] - Can be used to define the vertex positions in world space. + * @param {?Node} [config.position=null] - Can be used to define the billboard center position directly. + * When null, the center is derived automatically from `positionWorld`. * @param {boolean} [config.horizontal=true] - Whether to follow the camera rotation horizontally or not. * @param {boolean} [config.vertical=false] - Whether to follow the camera rotation vertically or not. + * @param {boolean} [config.horizontalRotation=false] - Whether to rotate around the Y axis to face the camera. * @return {Node} The updated vertex position in clip space. */ -const billboarding = /*@__PURE__*/ Fn( ( { position = null, horizontal = true, vertical = false } ) => { +const billboarding = /*@__PURE__*/ Fn( ( { position = null, horizontal = true, vertical = false, horizontalRotation = false } ) => { - let worldMatrix; + let center; if ( position !== null ) { - worldMatrix = modelWorldMatrix.toVar(); - worldMatrix[ 3 ][ 0 ] = position.x; - worldMatrix[ 3 ][ 1 ] = position.y; - worldMatrix[ 3 ][ 2 ] = position.z; + center = nodeObject( position ); } else { - worldMatrix = modelWorldMatrix; + center = positionWorld.sub( modelWorldMatrix.mul( vec4( positionGeometry, 0 ) ).xyz ); } + const worldMatrix = modelWorldMatrix.toVar(); + worldMatrix[ 3 ][ 0 ] = center.x; + worldMatrix[ 3 ][ 1 ] = center.y; + worldMatrix[ 3 ][ 2 ] = center.z; + const modelViewMatrix = cameraViewMatrix.mul( worldMatrix ); - if ( defined( horizontal ) ) { + const scaleX = modelWorldMatrix[ 0 ].length(); + const scaleY = modelWorldMatrix[ 1 ].length(); + const scaleZ = modelWorldMatrix[ 2 ].length(); + + let right, up, forward; + + if ( defined( horizontalRotation ) ) { + + const worldPosition = worldMatrix[ 3 ].xyz; + const look = cameraPosition.sub( worldPosition ); + const lookXZ = vec3( look.x, 0, look.z ).normalize(); + + const right_w = vec3( lookXZ.z, 0, lookXZ.x.negate() ); + + right = cameraViewMatrix.mul( vec4( right_w, 0 ) ).xyz.mul( scaleX ); + up = cameraViewMatrix[ 1 ].xyz.mul( scaleY ); + forward = cameraViewMatrix.mul( vec4( lookXZ, 0 ) ).xyz.mul( scaleZ ); + + } else { - modelViewMatrix[ 0 ][ 0 ] = modelWorldMatrix[ 0 ].length(); - modelViewMatrix[ 0 ][ 1 ] = 0; - modelViewMatrix[ 0 ][ 2 ] = 0; + if ( defined( horizontal ) ) right = vec3( scaleX, 0, 0 ); + if ( defined( vertical ) ) up = vec3( 0, scaleY, 0 ); + + forward = vec3( 0, 0, 1 ); + + } + + if ( right ) { + + modelViewMatrix[ 0 ][ 0 ] = right.x; + modelViewMatrix[ 0 ][ 1 ] = right.y; + modelViewMatrix[ 0 ][ 2 ] = right.z; } - if ( defined( vertical ) ) { + if ( up ) { - modelViewMatrix[ 1 ][ 0 ] = 0; - modelViewMatrix[ 1 ][ 1 ] = modelWorldMatrix[ 1 ].length(); - modelViewMatrix[ 1 ][ 2 ] = 0; + modelViewMatrix[ 1 ][ 0 ] = up.x; + modelViewMatrix[ 1 ][ 1 ] = up.y; + modelViewMatrix[ 1 ][ 2 ] = up.z; } - modelViewMatrix[ 2 ][ 0 ] = 0; - modelViewMatrix[ 2 ][ 1 ] = 0; - modelViewMatrix[ 2 ][ 2 ] = 1; + if ( forward ) { + + modelViewMatrix[ 2 ][ 0 ] = forward.x; + modelViewMatrix[ 2 ][ 1 ] = forward.y; + modelViewMatrix[ 2 ][ 2 ] = forward.z; + + } - return cameraProjectionMatrix.mul( modelViewMatrix ).mul( positionLocal ); + return cameraProjectionMatrix.mul( modelViewMatrix ).mul( positionGeometry ); } ); @@ -66196,7 +66350,7 @@ ${ flowData.code } snippet = `${typePrefix}sampler3D ${ uniform.name };`; - } else if ( texture.compareFunction && textureNode.isPlainGather() === false ) { + } else if ( texture.compareFunction && textureNode.isSampleCompare() ) { if ( texture.isArrayTexture === true ) { @@ -77777,7 +77931,7 @@ class WebGPUTextureUtils { const texture = binding.texture; const textureNode = binding.textureNode; - const isComparison = texture.isDepthTexture === true && texture.compareFunction !== null && textureNode.isPlainGather() === false && backend.hasCompatibility( Compatibility.TEXTURE_COMPARE ); + const isComparison = texture.isDepthTexture === true && texture.compareFunction !== null && textureNode.isSampleCompare() && backend.hasCompatibility( Compatibility.TEXTURE_COMPARE ); const samplerKey = texture.minFilter + '-' + texture.magFilter + '-' + texture.wrapS + '-' + texture.wrapT + '-' + ( texture.wrapR || '0' ) + '-' + @@ -81827,7 +81981,7 @@ ${ flowData.code } if ( needsSampler ) { - if ( this.isSampleCompare( texture ) && textureNode.isPlainGather() === false ) { + if ( this.isSampleCompare( texture ) && textureNode.isSampleCompare() ) { bindingSnippets.push( `@binding( ${ uniformIndexes.binding ++ } ) @group( ${ uniformIndexes.group } ) var ${ uniform.name }_sampler : sampler_comparison;` ); @@ -83568,7 +83722,7 @@ class WebGPUBindingUtils { if ( binding.texture.isDepthTexture ) { - if ( binding.texture.compareFunction !== null && binding.textureNode.isPlainGather() === false && backend.hasCompatibility( Compatibility.TEXTURE_COMPARE ) ) { + if ( binding.texture.compareFunction !== null && binding.textureNode.isSampleCompare() && backend.hasCompatibility( Compatibility.TEXTURE_COMPARE ) ) { sampler.type = GPUSamplerBindingType.Comparison; diff --git a/build/three.webgpu.nodes.js b/build/three.webgpu.nodes.js index 45a97d57d8bd11..5a07916f678909 100644 --- a/build/three.webgpu.nodes.js +++ b/build/three.webgpu.nodes.js @@ -3,7 +3,7 @@ * Copyright 2010-2026 Three.js Authors * SPDX-License-Identifier: MIT */ -import { DynamicDrawUsage, RenderObjectRefreshType, Color, Vector2, Vector3, Vector4, Matrix2, Matrix3, Matrix4, UnsignedIntType, IntType, error, RedFormat, RedIntegerFormat, DepthFormat, DepthStencilFormat, AlphaFormat, RGFormat, RGIntegerFormat, RGBFormat, RGBIntegerFormat, EventDispatcher, MathUtils, warn, WebGLCoordinateSystem, WebGPUCoordinateSystem, ColorManagement, SRGBTransfer, NoToneMapping, StaticDrawUsage, InterleavedBufferAttribute, InterleavedBuffer, NoColorSpace, log as log$1, warnOnce, Texture, Compatibility, LessCompare, LessEqualCompare, GreaterCompare, GreaterEqualCompare, NearestFilter, Sphere, BackSide, DoubleSide, CubeTexture, CubeReflectionMapping, CubeRefractionMapping, TangentSpaceNormalMap, NoNormalPacking, NormalRGPacking, NormalGAPacking, ObjectSpaceNormalMap, RED_GREEN_RGTC2_Format, RG11_EAC_Format, InstancedBufferAttribute, InstancedInterleavedBuffer, DataTexture, DataArrayTexture, FloatType, FramebufferTexture, LinearMipmapLinearFilter, DepthTexture, Material, LineBasicMaterial, LineDashedMaterial, NoBlending, MeshNormalMaterial, SRGBColorSpace, RenderTarget, BoxGeometry, Mesh, Scene, LinearFilter, CubeCamera, EquirectangularReflectionMapping, EquirectangularRefractionMapping, AddOperation, MixOperation, MultiplyOperation, MeshBasicMaterial, MeshLambertMaterial, MeshPhongMaterial, HalfFloatType, ClampToEdgeWrapping, BufferGeometry, OrthographicCamera, PerspectiveCamera, LinearSRGBColorSpace, RGBAFormat, CubeUVReflectionMapping, BufferAttribute, MeshStandardMaterial, MeshPhysicalMaterial, MeshToonMaterial, MeshMatcapMaterial, SpriteMaterial, PointsMaterial, ShadowMaterial, Uint32BufferAttribute, Uint16BufferAttribute, ByteType, UnsignedByteType, ShortType, UnsignedShortType, UnsignedShort4444Type, UnsignedShort5551Type, UnsignedInt248Type, UnsignedInt5999Type, UnsignedInt101111Type, NormalBlending, SrcAlphaFactor, OneMinusSrcAlphaFactor, AddEquation, MaterialBlending, Object3D, LinearMipMapLinearFilter, Plane, Float32BufferAttribute, UVMapping, PCFShadowMap, VSMShadowMap, BasicShadowMap, CubeDepthTexture, SphereGeometry, LinearMipmapNearestFilter, NearestMipmapLinearFilter, Float16BufferAttribute, yieldToMain, REVISION, ArrayCamera, PlaneGeometry, FrontSide, CustomBlending, ZeroFactor, CylinderGeometry, Quaternion, WebXRController, RAD2DEG, PCFSoftShadowMap, FrustumArray, Frustum, RGBAIntegerFormat, TimestampQuery, createCanvasElement, MaxEquation, MinEquation, ReverseSubtractEquation, SubtractEquation, OneMinusConstantAlphaFactor, ConstantAlphaFactor, OneMinusConstantColorFactor, ConstantColorFactor, OneMinusDstAlphaFactor, OneMinusDstColorFactor, OneMinusSrcColorFactor, DstAlphaFactor, DstColorFactor, SrcAlphaSaturateFactor, SrcColorFactor, OneFactor, CullFaceNone, CullFaceBack, CullFaceFront, MultiplyBlending, SubtractiveBlending, AdditiveBlending, NotEqualDepth, GreaterDepth, GreaterEqualDepth, EqualDepth, LessEqualDepth, LessDepth, AlwaysDepth, NeverDepth, ReversedDepthFuncs, RGB_S3TC_DXT1_Format, RGBA_S3TC_DXT1_Format, RGBA_S3TC_DXT3_Format, RGBA_S3TC_DXT5_Format, RGB_PVRTC_4BPPV1_Format, RGB_PVRTC_2BPPV1_Format, RGBA_PVRTC_4BPPV1_Format, RGBA_PVRTC_2BPPV1_Format, RGB_ETC1_Format, RGB_ETC2_Format, RGBA_ETC2_EAC_Format, R11_EAC_Format, SIGNED_R11_EAC_Format, SIGNED_RG11_EAC_Format, RGBA_ASTC_4x4_Format, RGBA_ASTC_5x4_Format, RGBA_ASTC_5x5_Format, RGBA_ASTC_6x5_Format, RGBA_ASTC_6x6_Format, RGBA_ASTC_8x5_Format, RGBA_ASTC_8x6_Format, RGBA_ASTC_8x8_Format, RGBA_ASTC_10x5_Format, RGBA_ASTC_10x6_Format, RGBA_ASTC_10x8_Format, RGBA_ASTC_10x10_Format, RGBA_ASTC_12x10_Format, RGBA_ASTC_12x12_Format, RGBA_BPTC_Format, RGB_BPTC_SIGNED_Format, RGB_BPTC_UNSIGNED_Format, RED_RGTC1_Format, SIGNED_RED_RGTC1_Format, SIGNED_RED_GREEN_RGTC2_Format, MirroredRepeatWrapping, RepeatWrapping, NearestMipmapNearestFilter, NotEqualCompare, EqualCompare, AlwaysCompare, NeverCompare, LinearTransfer, getByteLength, isTypedArray, NotEqualStencilFunc, GreaterStencilFunc, GreaterEqualStencilFunc, EqualStencilFunc, LessEqualStencilFunc, LessStencilFunc, AlwaysStencilFunc, NeverStencilFunc, DecrementWrapStencilOp, IncrementWrapStencilOp, DecrementStencilOp, IncrementStencilOp, InvertStencilOp, ReplaceStencilOp, ZeroStencilOp, KeepStencilOp, SpotLight, PointLight, DirectionalLight, RectAreaLight, AmbientLight, HemisphereLight, LightProbe, LinearToneMapping, ReinhardToneMapping, CineonToneMapping, ACESFilmicToneMapping, AgXToneMapping, NeutralToneMapping, Group, Loader, FileLoader, MaterialLoader, ObjectLoader } from './three.core.js'; +import { DynamicDrawUsage, RenderObjectRefreshType, Color, Vector2, Vector3, Vector4, Matrix2, Matrix3, Matrix4, UnsignedIntType, IntType, error, RedFormat, RedIntegerFormat, DepthFormat, DepthStencilFormat, AlphaFormat, RGFormat, RGIntegerFormat, RGBFormat, RGBIntegerFormat, EventDispatcher, MathUtils, warn, WebGLCoordinateSystem, WebGPUCoordinateSystem, ColorManagement, SRGBTransfer, NoToneMapping, StaticDrawUsage, InterleavedBufferAttribute, InterleavedBuffer, NoColorSpace, log as log$1, warnOnce, Texture, Compatibility, LessCompare, LessEqualCompare, GreaterCompare, GreaterEqualCompare, NearestFilter, Sphere, BackSide, DoubleSide, CubeTexture, CubeReflectionMapping, CubeRefractionMapping, TangentSpaceNormalMap, NoNormalPacking, NormalRGPacking, NormalGAPacking, ObjectSpaceNormalMap, RED_GREEN_RGTC2_Format, RG11_EAC_Format, InstancedBufferAttribute, InstancedInterleavedBuffer, DataTexture, RGBAFormat, FloatType, DataArrayTexture, FramebufferTexture, LinearMipmapLinearFilter, DepthTexture, Material, LineBasicMaterial, LineDashedMaterial, NoBlending, MeshNormalMaterial, SRGBColorSpace, RenderTarget, BoxGeometry, Mesh, Scene, LinearFilter, CubeCamera, EquirectangularReflectionMapping, EquirectangularRefractionMapping, AddOperation, MixOperation, MultiplyOperation, MeshBasicMaterial, MeshLambertMaterial, MeshPhongMaterial, HalfFloatType, ClampToEdgeWrapping, BufferGeometry, OrthographicCamera, PerspectiveCamera, LinearSRGBColorSpace, CubeUVReflectionMapping, BufferAttribute, MeshStandardMaterial, MeshPhysicalMaterial, MeshToonMaterial, MeshMatcapMaterial, SpriteMaterial, PointsMaterial, ShadowMaterial, Uint32BufferAttribute, Uint16BufferAttribute, ByteType, UnsignedByteType, ShortType, UnsignedShortType, UnsignedShort4444Type, UnsignedShort5551Type, UnsignedInt248Type, UnsignedInt5999Type, UnsignedInt101111Type, NormalBlending, SrcAlphaFactor, OneMinusSrcAlphaFactor, AddEquation, MaterialBlending, Object3D, LinearMipMapLinearFilter, Plane, Float32BufferAttribute, UVMapping, PCFShadowMap, VSMShadowMap, BasicShadowMap, CubeDepthTexture, SphereGeometry, LinearMipmapNearestFilter, NearestMipmapLinearFilter, Float16BufferAttribute, yieldToMain, REVISION, ArrayCamera, PlaneGeometry, FrontSide, CustomBlending, ZeroFactor, CylinderGeometry, Quaternion, WebXRController, RAD2DEG, PCFSoftShadowMap, FrustumArray, Frustum, RGBAIntegerFormat, TimestampQuery, createCanvasElement, MaxEquation, MinEquation, ReverseSubtractEquation, SubtractEquation, OneMinusConstantAlphaFactor, ConstantAlphaFactor, OneMinusConstantColorFactor, ConstantColorFactor, OneMinusDstAlphaFactor, OneMinusDstColorFactor, OneMinusSrcColorFactor, DstAlphaFactor, DstColorFactor, SrcAlphaSaturateFactor, SrcColorFactor, OneFactor, CullFaceNone, CullFaceBack, CullFaceFront, MultiplyBlending, SubtractiveBlending, AdditiveBlending, NotEqualDepth, GreaterDepth, GreaterEqualDepth, EqualDepth, LessEqualDepth, LessDepth, AlwaysDepth, NeverDepth, ReversedDepthFuncs, RGB_S3TC_DXT1_Format, RGBA_S3TC_DXT1_Format, RGBA_S3TC_DXT3_Format, RGBA_S3TC_DXT5_Format, RGB_PVRTC_4BPPV1_Format, RGB_PVRTC_2BPPV1_Format, RGBA_PVRTC_4BPPV1_Format, RGBA_PVRTC_2BPPV1_Format, RGB_ETC1_Format, RGB_ETC2_Format, RGBA_ETC2_EAC_Format, R11_EAC_Format, SIGNED_R11_EAC_Format, SIGNED_RG11_EAC_Format, RGBA_ASTC_4x4_Format, RGBA_ASTC_5x4_Format, RGBA_ASTC_5x5_Format, RGBA_ASTC_6x5_Format, RGBA_ASTC_6x6_Format, RGBA_ASTC_8x5_Format, RGBA_ASTC_8x6_Format, RGBA_ASTC_8x8_Format, RGBA_ASTC_10x5_Format, RGBA_ASTC_10x6_Format, RGBA_ASTC_10x8_Format, RGBA_ASTC_10x10_Format, RGBA_ASTC_12x10_Format, RGBA_ASTC_12x12_Format, RGBA_BPTC_Format, RGB_BPTC_SIGNED_Format, RGB_BPTC_UNSIGNED_Format, RED_RGTC1_Format, SIGNED_RED_RGTC1_Format, SIGNED_RED_GREEN_RGTC2_Format, MirroredRepeatWrapping, RepeatWrapping, NearestMipmapNearestFilter, NotEqualCompare, EqualCompare, AlwaysCompare, NeverCompare, LinearTransfer, getByteLength, isTypedArray, NotEqualStencilFunc, GreaterStencilFunc, GreaterEqualStencilFunc, EqualStencilFunc, LessEqualStencilFunc, LessStencilFunc, AlwaysStencilFunc, NeverStencilFunc, DecrementWrapStencilOp, IncrementWrapStencilOp, DecrementStencilOp, IncrementStencilOp, InvertStencilOp, ReplaceStencilOp, ZeroStencilOp, KeepStencilOp, SpotLight, PointLight, DirectionalLight, RectAreaLight, AmbientLight, HemisphereLight, LightProbe, LinearToneMapping, ReinhardToneMapping, CineonToneMapping, ACESFilmicToneMapping, AgXToneMapping, NeutralToneMapping, Group, Loader, FileLoader, MaterialLoader, ObjectLoader } from './three.core.js'; export { AdditiveAnimationBlendMode, AnimationAction, AnimationClip, AnimationLoader, AnimationMixer, AnimationObjectGroup, AnimationUtils, ArcCurve, ArrowHelper, AttachedBindMode, Audio, AudioAnalyser, AudioContext, AudioListener, AudioLoader, AxesHelper, BasicDepthPacking, BatchedMesh, BezierInterpolant, Bone, BooleanKeyframeTrack, Box2, Box3, Box3Helper, BoxHelper, BufferGeometryLoader, Cache, Camera, CameraHelper, CanvasTexture, CapsuleGeometry, CatmullRomCurve3, CircleGeometry, Clock, ColorKeyframeTrack, CompressedArrayTexture, CompressedCubeTexture, CompressedTexture, CompressedTextureLoader, ConeGeometry, Controls, CubeTextureLoader, CubicBezierCurve, CubicBezierCurve3, CubicInterpolant, CullFaceFrontBack, Curve, CurvePath, CustomToneMapping, Cylindrical, Data3DTexture, DataTextureLoader, DataUtils, DefaultLoadingManager, DetachedBindMode, DirectionalLightHelper, DiscreteInterpolant, DodecahedronGeometry, DynamicCopyUsage, DynamicReadUsage, EdgesGeometry, EllipseCurve, Euler, ExternalTexture, ExtrudeGeometry, Fog, FogExp2, GLBufferAttribute, GLSL1, GLSL3, GridHelper, HTMLTexture, HemisphereLightHelper, IcosahedronGeometry, ImageBitmapLoader, ImageLoader, ImageUtils, InstancedBufferGeometry, InstancedMesh, Int16BufferAttribute, Int32BufferAttribute, Int8BufferAttribute, Interpolant, InterpolateBezier, InterpolateDiscrete, InterpolateLinear, InterpolateSmooth, InterpolationSamplingMode, InterpolationSamplingType, KeyframeTrack, LOD, LatheGeometry, Layers, Light, Line, Line3, LineCurve, LineCurve3, LineLoop, LineSegments, LinearInterpolant, LinearMipMapNearestFilter, LoaderUtils, LoadingManager, LoopOnce, LoopPingPong, LoopRepeat, MOUSE, MeshDepthMaterial, MeshDistanceMaterial, NearestMipMapLinearFilter, NearestMipMapNearestFilter, NormalAnimationBlendMode, NumberKeyframeTrack, OctahedronGeometry, Path, PlaneHelper, PointLightHelper, Points, PolarGridHelper, PolyhedronGeometry, PositionalAudio, PropertyBinding, PropertyMixer, QuadraticBezierCurve, QuadraticBezierCurve3, QuaternionKeyframeTrack, QuaternionLinearInterpolant, RGBADepthPacking, RGBDepthPacking, RGDepthPacking, RawShaderMaterial, Ray, Raycaster, RenderTarget3D, RingGeometry, ShaderMaterial, Shape, ShapeGeometry, ShapePath, ShapeUtils, Skeleton, SkeletonHelper, SkinnedMesh, Source, Spherical, SphericalHarmonics3, SplineCurve, SpotLightHelper, Sprite, StaticCopyUsage, StaticReadUsage, StereoCamera, StreamCopyUsage, StreamDrawUsage, StreamReadUsage, StringKeyframeTrack, TOUCH, TetrahedronGeometry, TextureLoader, TextureUtils, Timer, TorusGeometry, TorusKnotGeometry, Triangle, TriangleFanDrawMode, TriangleStripDrawMode, TrianglesDrawMode, TubeGeometry, Uint8BufferAttribute, Uint8ClampedBufferAttribute, Uniform, UniformsGroup, VectorKeyframeTrack, VideoFrameTexture, VideoTexture, WebGL3DRenderTarget, WebGLArrayRenderTarget, WebGLRenderTarget, WireframeGeometry, WrapAroundEnding, ZeroCurvatureEnding, ZeroSlopeEnding, getConsoleFunction, setConsoleFunction } from './three.core.js'; const refreshUniforms = [ @@ -5782,6 +5782,31 @@ class UniformNode extends InputNode { } + /** + * Uniform nodes with the same hash share a single uniform. This method returns the node + * the shared uniform refers to which is the first node registered for the hash. + * + * @param {NodeBuilder} builder - The current node builder. + * @return {UniformNode} The node the shared uniform refers to. + */ + getSharedNode( builder ) { + + const hash = this.getUniformHash( builder ); + + let sharedNode = builder.getNodeFromHash( hash ); + + if ( sharedNode === undefined ) { + + builder.setHashNode( this, hash ); + + sharedNode = this; + + } + + return sharedNode; + + } + onUpdate( callback, updateType ) { callback = callback.bind( this ); @@ -5818,18 +5843,7 @@ class UniformNode extends InputNode { const type = this.getNodeType( builder ); - const hash = this.getUniformHash( builder ); - - let sharedNode = builder.getNodeFromHash( hash ); - - if ( sharedNode === undefined ) { - - builder.setHashNode( this, hash ); - - sharedNode = this; - - } - + const sharedNode = this.getSharedNode( builder ); const sharedNodeType = sharedNode.getInputType( builder ); const nodeUniform = builder.getUniformFromNode( sharedNode, sharedNodeType, builder.shaderStage, this.name || builder.context.nodeName ); @@ -12694,6 +12708,15 @@ class TextureNode extends UniformNode { */ this._flipYUniform = null; + /** + * Whether the node is used as a comparison sampler, e.g. via `samplerComparison()`. + * + * @private + * @type {boolean} + * @default false + */ + this._samplerComparison = false; + this.setUpdateMatrix( uvNode === null ); } @@ -13059,6 +13082,18 @@ class TextureNode extends UniformNode { if ( /^sampler/.test( output ) ) { + if ( output === 'samplerComparison' ) { + + this._samplerComparison = true; + + // texture nodes with the same texture share a single uniform so it's + // important to set the flag on the node the binding refers to as well + + const sharedNode = this.getSharedNode( builder ); + sharedNode._samplerComparison = true; + + } + return textureProperty + '_sampler'; } else if ( builder.isReference( output ) ) { @@ -13338,14 +13373,14 @@ class TextureNode extends UniformNode { } /** - * Returns `true` if the texture is sampled with a plain gather (`textureGather`), - * meaning a gather without a compare value. + * Returns `true` if the texture is sampled with a depth comparison, + * meaning the node must be bound with a comparison sampler. * - * @return {boolean} Whether a plain gather is used or not. + * @return {boolean} Whether comparison sampling is used or not. */ - isPlainGather() { + isSampleCompare() { - return this.gatherNode !== null && this.compareNode === null; + return this.compareNode !== null || this._samplerComparison === true; } @@ -14118,7 +14153,7 @@ class ScreenNode extends Node { /** * Constructs a new screen node. * - * @param {('coordinate'|'viewport'|'size'|'uv'|'dpr')} scope - The node's scope. + * @param {('coordinate'|'viewport'|'size'|'uv')} scope - The node's scope. */ constructor( scope ) { @@ -14131,21 +14166,11 @@ class ScreenNode extends Node { * - `ScreenNode.VIEWPORT`: The current viewport defined as a four-dimensional vector. * - `ScreenNode.SIZE`: The dimensions of the current bound framebuffer. * - `ScreenNode.UV`: Normalized coordinates. - * - `ScreenNode.DPR`: Device pixel ratio. * - * @type {('coordinate'|'viewport'|'size'|'uv'|'dpr')} + * @type {('coordinate'|'viewport'|'size'|'uv')} */ this.scope = scope; - /** - * This output node. - * - * @private - * @type {?Node} - * @default null - */ - this._output = null; - /** * This flag can be used for type testing. * @@ -14160,11 +14185,10 @@ class ScreenNode extends Node { /** * This method is overwritten since the node type depends on the selected scope. * - * @return {('float'|'vec2'|'vec4')} The node type. + * @return {('vec2'|'vec4')} The node type. */ generateNodeType() { - if ( this.scope === ScreenNode.DPR ) return 'float'; if ( this.scope === ScreenNode.VIEWPORT ) return 'vec4'; else return 'vec2'; @@ -14179,7 +14203,7 @@ class ScreenNode extends Node { let updateType = NodeUpdateType.NONE; - if ( this.scope === ScreenNode.SIZE || this.scope === ScreenNode.VIEWPORT || this.scope === ScreenNode.DPR ) { + if ( this.scope === ScreenNode.SIZE || this.scope === ScreenNode.VIEWPORT ) { updateType = NodeUpdateType.RENDER; @@ -14215,10 +14239,6 @@ class ScreenNode extends Node { } - } else if ( this.scope === ScreenNode.DPR ) { - - this._output.value = renderer.getPixelRatio(); - } else { if ( renderTarget !== null ) { @@ -14250,18 +14270,12 @@ class ScreenNode extends Node { output = uniform( _viewportVec || ( _viewportVec = new Vector4() ) ).setGroup( renderGroup ); - } else if ( scope === ScreenNode.DPR ) { - - output = uniform( 1 ).setGroup( renderGroup ); - } else { output = vec2( screenCoordinate.div( screenSize ) ); } - this._output = output; - return output; } @@ -14296,7 +14310,6 @@ ScreenNode.COORDINATE = 'coordinate'; ScreenNode.VIEWPORT = 'viewport'; ScreenNode.SIZE = 'size'; ScreenNode.UV = 'uv'; -ScreenNode.DPR = 'dpr'; // Screen @@ -14304,9 +14317,9 @@ ScreenNode.DPR = 'dpr'; * TSL object that represents the current DPR. * * @tsl - * @type {ScreenNode} + * @type {UniformNode} */ -const screenDPR = /*@__PURE__*/ nodeImmutable( ScreenNode, ScreenNode.DPR ); +const screenDPR = /*@__PURE__*/ uniform( 1 ).setGroup( renderGroup ).onRenderUpdate( ( { renderer } ) => renderer.getPixelRatio() ); /** * TSL object that represents normalized screen coordinates, unitless in `[0, 1]`. @@ -18981,6 +18994,76 @@ const batch = /*@__PURE__*/ Fn( ( [ batchMesh ], builder ) => { const _skeletonsUpdated = /*@__PURE__*/ new WeakMap(); const _previousBoneMatricesData = /*@__PURE__*/ new WeakMap(); +/** + * Creates an accessor for bone matrices stored in a bone texture. + * + * @param {TextureNode} boneTexture - The bone texture node. + * @returns {Object} An accessor with the same `element()` interface as a buffer node. + */ +function getBoneTextureMatrices( boneTexture ) { + + return { + element: ( i ) => { + + const size = int( textureSize( boneTexture ).x ).toConst(); + const j = int( i ).mul( 4 ).toConst(); + const y = j.div( size ).toConst(); + const x = j.sub( y.mul( size ) ).toConst(); + + return mat4( + boneTexture.load( ivec2( x, y ) ), + boneTexture.load( ivec2( x.add( 1 ), y ) ), + boneTexture.load( ivec2( x.add( 2 ), y ) ), + boneTexture.load( ivec2( x.add( 3 ), y ) ) + ); + + } + }; + +} + +/** + * Creates the bone matrices node. Skeletons that fit within the uniform buffer limit + * use a uniform buffer, larger skeletons fall back to a bone texture. + * + * @param {NodeBuilder} builder - The current node builder. + * @param {Skeleton} skeleton - The skeleton. + * @returns {Object} The bone matrices node. + */ +function getBoneMatricesNode( builder, skeleton ) { + + let node; + + const uniformBufferSize = skeleton.bones.length * 16 * 4; + + if ( uniformBufferSize <= builder.getUniformBufferLimit() ) { + + node = referenceBuffer( 'skeleton.boneMatrices', 'mat4', skeleton.bones.length ); + + } else { + + if ( skeleton.boneTexture === null ) skeleton.computeBoneTexture(); + + const boneTexture = texture( skeleton.boneTexture ); + + OnObjectUpdate( ( { object } ) => { + + const skeleton = object.skeleton; + + if ( skeleton.boneTexture === null ) skeleton.computeBoneTexture(); + + boneTexture.value = skeleton.boneTexture; + + } ); + + node = getBoneTextureMatrices( boneTexture ); + + } + + return node; + +} + /** * Computes the skinned position by applying bone matrices based on weights. * @@ -19057,6 +19140,7 @@ function getSkinnedNormalAndTangent( boneMatrices, normal, tangent, bindMatrix, * Retrieves or initializes the previous frame skinned position node for motion vectors. * Uses a WeakMap to cache previous frame bone matrix arrays and their TSL buffer nodes. * + * @param {NodeBuilder} builder - The current node builder. * @param {SkinnedMesh} skinnedMesh - The skinned mesh. * @param {Node} bindMatrixNode - The bind matrix node. * @param {Node} bindMatrixInverseNode - The inverse bind matrix node. @@ -19064,7 +19148,7 @@ function getSkinnedNormalAndTangent( boneMatrices, normal, tangent, bindMatrix, * @param {Node} skinWeightNode - The skin weight attribute. * @returns {Node} The skinned position from the previous frame. */ -function getPreviousSkinnedPosition( skinnedMesh, bindMatrixNode, bindMatrixInverseNode, skinIndexNode, skinWeightNode ) { +function getPreviousSkinnedPosition( builder, skinnedMesh, bindMatrixNode, bindMatrixInverseNode, skinIndexNode, skinWeightNode ) { const skeleton = skinnedMesh.skeleton; @@ -19074,12 +19158,35 @@ function getPreviousSkinnedPosition( skinnedMesh, bindMatrixNode, bindMatrixInve skeleton.update(); - const previousBoneMatrices = new Float32Array( skeleton.boneMatrices ); + const uniformBufferSize = skeleton.bones.length * 16 * 4; - data = { - previousBoneMatrices, - node: buffer( previousBoneMatrices, 'mat4', skeleton.bones.length ) - }; + if ( uniformBufferSize <= builder.getUniformBufferLimit() ) { + + const previousBoneMatrices = new Float32Array( skeleton.boneMatrices ); + + data = { + previousBoneMatrices, + previousBoneTexture: null, + node: buffer( previousBoneMatrices, 'mat4', skeleton.bones.length ) + }; + + } else { + + if ( skeleton.boneTexture === null ) skeleton.computeBoneTexture(); + + const { width, height } = skeleton.boneTexture.image; + + const previousBoneMatrices = new Float32Array( skeleton.boneMatrices ); + const previousBoneTexture = new DataTexture( previousBoneMatrices, width, height, RGBAFormat, FloatType ); + previousBoneTexture.needsUpdate = true; + + data = { + previousBoneMatrices, + previousBoneTexture, + node: getBoneTextureMatrices( texture( previousBoneTexture ) ) + }; + + } _previousBoneMatricesData.set( skeleton, data ); @@ -19103,7 +19210,7 @@ const skinning = /*@__PURE__*/ Fn( ( [ skinnedMesh ], builder ) => { const skinWeightNode = attribute( 'skinWeight', 'vec4' ); const bindMatrixNode = reference( 'bindMatrix', 'mat4' ); const bindMatrixInverseNode = reference( 'bindMatrixInverse', 'mat4' ); - const boneMatricesNode = referenceBuffer( 'skeleton.boneMatrices', 'mat4', skinnedMesh.skeleton.bones.length ); + const boneMatricesNode = getBoneMatricesNode( builder, skinnedMesh.skeleton ); OnObjectUpdate( ( { object, frameId } ) => { @@ -19119,6 +19226,12 @@ const skinning = /*@__PURE__*/ Fn( ( [ skinnedMesh ], builder ) => { skeletonData.previousBoneMatrices.set( skeleton.boneMatrices ); + if ( skeletonData.previousBoneTexture !== null ) { + + skeletonData.previousBoneTexture.needsUpdate = true; + + } + } skeleton.update(); @@ -19129,7 +19242,7 @@ const skinning = /*@__PURE__*/ Fn( ( [ skinnedMesh ], builder ) => { if ( builder.needsPreviousData() ) { - const previousSkinnedPosition = getPreviousSkinnedPosition( skinnedMesh, bindMatrixNode, bindMatrixInverseNode, skinIndexNode, skinWeightNode ); + const previousSkinnedPosition = getPreviousSkinnedPosition( builder, skinnedMesh, bindMatrixNode, bindMatrixInverseNode, skinIndexNode, skinWeightNode ); positionPrevious.assign( previousSkinnedPosition ); @@ -19186,6 +19299,12 @@ const computeSkinning = /*@__PURE__*/ Fn( ( [ skinnedMesh, toPosition = null ], state.previousBoneMatrices.set( skeleton.boneMatrices ); + if ( state.previousBoneTexture !== null ) { + + state.previousBoneTexture.needsUpdate = true; + + } + } skeleton.update(); @@ -19196,7 +19315,7 @@ const computeSkinning = /*@__PURE__*/ Fn( ( [ skinnedMesh, toPosition = null ], if ( builder.needsPreviousData() ) { - const previousSkinnedPosition = getPreviousSkinnedPosition( skinnedMesh, bindMatrixNode, bindMatrixInverseNode, skinIndexNode, skinWeightNode ); + const previousSkinnedPosition = getPreviousSkinnedPosition( builder, skinnedMesh, bindMatrixNode, bindMatrixInverseNode, skinIndexNode, skinWeightNode ); positionPrevious.assign( previousSkinnedPosition ); @@ -37745,51 +37864,86 @@ const spherizeUV = /*@__PURE__*/ Fn( ( [ uv, strength, center = vec2( 0.5 ) ] ) * @tsl * @function * @param {Object} config - The configuration object. - * @param {?Node} [config.position=null] - Can be used to define the vertex positions in world space. + * @param {?Node} [config.position=null] - Can be used to define the billboard center position directly. + * When null, the center is derived automatically from `positionWorld`. * @param {boolean} [config.horizontal=true] - Whether to follow the camera rotation horizontally or not. * @param {boolean} [config.vertical=false] - Whether to follow the camera rotation vertically or not. + * @param {boolean} [config.horizontalRotation=false] - Whether to rotate around the Y axis to face the camera. * @return {Node} The updated vertex position in clip space. */ -const billboarding = /*@__PURE__*/ Fn( ( { position = null, horizontal = true, vertical = false } ) => { +const billboarding = /*@__PURE__*/ Fn( ( { position = null, horizontal = true, vertical = false, horizontalRotation = false } ) => { - let worldMatrix; + let center; if ( position !== null ) { - worldMatrix = modelWorldMatrix.toVar(); - worldMatrix[ 3 ][ 0 ] = position.x; - worldMatrix[ 3 ][ 1 ] = position.y; - worldMatrix[ 3 ][ 2 ] = position.z; + center = nodeObject( position ); } else { - worldMatrix = modelWorldMatrix; + center = positionWorld.sub( modelWorldMatrix.mul( vec4( positionGeometry, 0 ) ).xyz ); } + const worldMatrix = modelWorldMatrix.toVar(); + worldMatrix[ 3 ][ 0 ] = center.x; + worldMatrix[ 3 ][ 1 ] = center.y; + worldMatrix[ 3 ][ 2 ] = center.z; + const modelViewMatrix = cameraViewMatrix.mul( worldMatrix ); - if ( defined( horizontal ) ) { + const scaleX = modelWorldMatrix[ 0 ].length(); + const scaleY = modelWorldMatrix[ 1 ].length(); + const scaleZ = modelWorldMatrix[ 2 ].length(); + + let right, up, forward; + + if ( defined( horizontalRotation ) ) { + + const worldPosition = worldMatrix[ 3 ].xyz; + const look = cameraPosition.sub( worldPosition ); + const lookXZ = vec3( look.x, 0, look.z ).normalize(); + + const right_w = vec3( lookXZ.z, 0, lookXZ.x.negate() ); + + right = cameraViewMatrix.mul( vec4( right_w, 0 ) ).xyz.mul( scaleX ); + up = cameraViewMatrix[ 1 ].xyz.mul( scaleY ); + forward = cameraViewMatrix.mul( vec4( lookXZ, 0 ) ).xyz.mul( scaleZ ); + + } else { - modelViewMatrix[ 0 ][ 0 ] = modelWorldMatrix[ 0 ].length(); - modelViewMatrix[ 0 ][ 1 ] = 0; - modelViewMatrix[ 0 ][ 2 ] = 0; + if ( defined( horizontal ) ) right = vec3( scaleX, 0, 0 ); + if ( defined( vertical ) ) up = vec3( 0, scaleY, 0 ); + + forward = vec3( 0, 0, 1 ); + + } + + if ( right ) { + + modelViewMatrix[ 0 ][ 0 ] = right.x; + modelViewMatrix[ 0 ][ 1 ] = right.y; + modelViewMatrix[ 0 ][ 2 ] = right.z; } - if ( defined( vertical ) ) { + if ( up ) { - modelViewMatrix[ 1 ][ 0 ] = 0; - modelViewMatrix[ 1 ][ 1 ] = modelWorldMatrix[ 1 ].length(); - modelViewMatrix[ 1 ][ 2 ] = 0; + modelViewMatrix[ 1 ][ 0 ] = up.x; + modelViewMatrix[ 1 ][ 1 ] = up.y; + modelViewMatrix[ 1 ][ 2 ] = up.z; } - modelViewMatrix[ 2 ][ 0 ] = 0; - modelViewMatrix[ 2 ][ 1 ] = 0; - modelViewMatrix[ 2 ][ 2 ] = 1; + if ( forward ) { + + modelViewMatrix[ 2 ][ 0 ] = forward.x; + modelViewMatrix[ 2 ][ 1 ] = forward.y; + modelViewMatrix[ 2 ][ 2 ] = forward.z; + + } - return cameraProjectionMatrix.mul( modelViewMatrix ).mul( positionLocal ); + return cameraProjectionMatrix.mul( modelViewMatrix ).mul( positionGeometry ); } ); @@ -66196,7 +66350,7 @@ ${ flowData.code } snippet = `${typePrefix}sampler3D ${ uniform.name };`; - } else if ( texture.compareFunction && textureNode.isPlainGather() === false ) { + } else if ( texture.compareFunction && textureNode.isSampleCompare() ) { if ( texture.isArrayTexture === true ) { @@ -77777,7 +77931,7 @@ class WebGPUTextureUtils { const texture = binding.texture; const textureNode = binding.textureNode; - const isComparison = texture.isDepthTexture === true && texture.compareFunction !== null && textureNode.isPlainGather() === false && backend.hasCompatibility( Compatibility.TEXTURE_COMPARE ); + const isComparison = texture.isDepthTexture === true && texture.compareFunction !== null && textureNode.isSampleCompare() && backend.hasCompatibility( Compatibility.TEXTURE_COMPARE ); const samplerKey = texture.minFilter + '-' + texture.magFilter + '-' + texture.wrapS + '-' + texture.wrapT + '-' + ( texture.wrapR || '0' ) + '-' + @@ -81827,7 +81981,7 @@ ${ flowData.code } if ( needsSampler ) { - if ( this.isSampleCompare( texture ) && textureNode.isPlainGather() === false ) { + if ( this.isSampleCompare( texture ) && textureNode.isSampleCompare() ) { bindingSnippets.push( `@binding( ${ uniformIndexes.binding ++ } ) @group( ${ uniformIndexes.group } ) var ${ uniform.name }_sampler : sampler_comparison;` ); @@ -83568,7 +83722,7 @@ class WebGPUBindingUtils { if ( binding.texture.isDepthTexture ) { - if ( binding.texture.compareFunction !== null && binding.textureNode.isPlainGather() === false && backend.hasCompatibility( Compatibility.TEXTURE_COMPARE ) ) { + if ( binding.texture.compareFunction !== null && binding.textureNode.isSampleCompare() && backend.hasCompatibility( Compatibility.TEXTURE_COMPARE ) ) { sampler.type = GPUSamplerBindingType.Comparison; From f3d43901f56ee1d3b8eccdffbb19b6ce7b535c94 Mon Sep 17 00:00:00 2001 From: Ben Houston Date: Mon, 10 Aug 2026 11:22:53 -0400 Subject: [PATCH 4/7] Optimize Gaussian Splat loading (#34205) --- .../GLTFGaussianSplatLoaderExtension.js | 2 +- examples/jsm/loaders/KSPLATLoader.js | 2 +- examples/jsm/loaders/SPZLoader.js | 165 +++++++++++------- examples/jsm/utils/GaussianSplatUtils.js | 23 ++- 4 files changed, 111 insertions(+), 81 deletions(-) diff --git a/examples/jsm/loaders/GLTFGaussianSplatLoaderExtension.js b/examples/jsm/loaders/GLTFGaussianSplatLoaderExtension.js index c769580c7959b8..e7b84bd5fd824d 100644 --- a/examples/jsm/loaders/GLTFGaussianSplatLoaderExtension.js +++ b/examples/jsm/loaders/GLTFGaussianSplatLoaderExtension.js @@ -171,7 +171,7 @@ function createGaussianSplatMesh( geometry, primitiveDef ) { const centers = new Float32Array( count * 3 ); const covariances = new Float32Array( count * 6 ); - const colors = new Uint8Array( count * 4 ); + const colors = new Uint8ClampedArray( count * 4 ); for ( let i = 0; i < count; i ++ ) { diff --git a/examples/jsm/loaders/KSPLATLoader.js b/examples/jsm/loaders/KSPLATLoader.js index 6cf7e5e7800834..bb4e84a6aa3f0b 100644 --- a/examples/jsm/loaders/KSPLATLoader.js +++ b/examples/jsm/loaders/KSPLATLoader.js @@ -169,7 +169,7 @@ class KSPLATLoader extends Loader { const compression = COMPRESSION_LEVELS[ header.compressionLevel ]; const centers = new Float32Array( header.splatCount * 3 ); const covariances = new Float32Array( header.splatCount * 6 ); - const colors = new Uint8Array( header.splatCount * 4 ); + const colors = new Uint8ClampedArray( header.splatCount * 4 ); let splatOffset = 0; let sectionBase = sectionDataOffset; diff --git a/examples/jsm/loaders/SPZLoader.js b/examples/jsm/loaders/SPZLoader.js index dcb6a8974c13ac..67b9eaca7f356b 100644 --- a/examples/jsm/loaders/SPZLoader.js +++ b/examples/jsm/loaders/SPZLoader.js @@ -5,7 +5,7 @@ import { } from 'three'; import { gunzipSync } from '../libs/fflate.module.js'; -import { SH_C0, createGaussianSplatGeometry, writeColorBytes, writeCovariance } from '../utils/GaussianSplatUtils.js'; +import { SH_C0, createGaussianSplatGeometry, writeCovariance } from '../utils/GaussianSplatUtils.js'; const SPZ_MAGIC = 0x5053474e; const HEADER_SIZE_BYTES = 16; @@ -14,6 +14,33 @@ const SPZ_COLOR_SCALE = SH_C0 / 0.15; const FLAG_LOD = 0x80; const SH_DEGREE_TO_VECTORS = [ 0, 3, 8, 15 ]; +// Scales and colors are stored as single bytes, so all 256 possible outputs +// of their decode functions can be precomputed once. +const SCALE_LUT = new Float32Array( 256 ); +const COLOR_LUT = new Uint8ClampedArray( 256 ); + +for ( let i = 0; i < 256; i ++ ) { + + SCALE_LUT[ i ] = Math.exp( i / 16 - 10 ); + COLOR_LUT[ i ] = ( ( i / 255 - 0.5 ) * SPZ_COLOR_SCALE + 0.5 ) * 255; + +} + +// Quaternion components are 10-bit sign-magnitude values (bit 9 is the sign, +// bits 0-8 are the magnitude scaled to [0, 1/sqrt(2)]), so all 1024 possible +// decoded values can be precomputed, avoiding an unpredictable sign branch in +// the hot loop. +const QUAT_COMPONENT_LUT = new Float64Array( 1024 ); + +for ( let i = 0; i < 1024; i ++ ) { + + const value = Math.SQRT1_2 * ( ( i & 511 ) / 511 ); + QUAT_COMPONENT_LUT[ i ] = ( i & 512 ) !== 0 ? - value : value; + +} + +const _quaternion = [ 0, 0, 0, 0 ]; + /** * A loader for compressed Gaussian splat `.spz` files. * @@ -150,7 +177,7 @@ class SPZLoader extends Loader { let offset = HEADER_SIZE_BYTES; const centers = new Float32Array( count * 3 ); const covariances = new Float32Array( count * 6 ); - const colors = new Uint8Array( count * 4 ); + const colors = new Uint8ClampedArray( count * 4 ); const positionsSize = count * 3 * ( version === 1 ? 2 : 3 ); const rotationsSize = count * ( version === 3 ? 4 : 3 ); const shSize = count * SH_DEGREE_TO_VECTORS[ shDegree ] * 3; @@ -163,7 +190,7 @@ class SPZLoader extends Loader { } - offset = readCenters( view, centers, offset, count, version, fractionalBits ); + offset = readCenters( bytes, centers, offset, count, version, fractionalBits ); const alphaOffset = offset; offset += count; @@ -176,25 +203,39 @@ class SPZLoader extends Loader { const rotationOffset = offset; + // Copy the rotation section into an aligned Uint32Array so the hot loop + // avoids per-splat DataView reads (the section offset within the file is + // not guaranteed to be 4-byte aligned). + const packedRotations = version === 3 ? + new Uint32Array( bytes.buffer.slice( bytes.byteOffset + rotationOffset, bytes.byteOffset + rotationOffset + count * 4 ) ) : + null; + + const quaternion = _quaternion; + for ( let i = 0; i < count; i ++ ) { const i3 = i * 3; - const sx = Math.exp( bytes[ scaleOffset + i3 ] / 16 - 10 ); - const sy = Math.exp( bytes[ scaleOffset + i3 + 1 ] / 16 - 10 ); - const sz = Math.exp( bytes[ scaleOffset + i3 + 2 ] / 16 - 10 ); - const rotation = version === 3 ? - readSmallestThreeQuaternion( view, rotationOffset + i * 4 ) : - readXYZQuaternion( bytes, rotationOffset + i * 3 ); - - writeCovariance( covariances, i * 6, sx, sy, sz, rotation[ 0 ], rotation[ 1 ], rotation[ 2 ], rotation[ 3 ] ); - writeColorBytes( - colors, - i * 4, - ( ( bytes[ colorOffset + i3 ] / 255 - 0.5 ) * SPZ_COLOR_SCALE + 0.5 ) * 255, - ( ( bytes[ colorOffset + i3 + 1 ] / 255 - 0.5 ) * SPZ_COLOR_SCALE + 0.5 ) * 255, - ( ( bytes[ colorOffset + i3 + 2 ] / 255 - 0.5 ) * SPZ_COLOR_SCALE + 0.5 ) * 255, - bytes[ alphaOffset + i ] - ); + const i4 = i * 4; + const sx = SCALE_LUT[ bytes[ scaleOffset + i3 ] ]; + const sy = SCALE_LUT[ bytes[ scaleOffset + i3 + 1 ] ]; + const sz = SCALE_LUT[ bytes[ scaleOffset + i3 + 2 ] ]; + + if ( version === 3 ) { + + readSmallestThreeQuaternion( packedRotations[ i ], quaternion ); + + } else { + + readXYZQuaternion( bytes, rotationOffset + i3, quaternion ); + + } + + writeCovariance( covariances, i * 6, sx, sy, sz, quaternion[ 0 ], quaternion[ 1 ], quaternion[ 2 ], quaternion[ 3 ] ); + + colors[ i4 ] = COLOR_LUT[ bytes[ colorOffset + i3 ] ]; + colors[ i4 + 1 ] = COLOR_LUT[ bytes[ colorOffset + i3 + 1 ] ]; + colors[ i4 + 2 ] = COLOR_LUT[ bytes[ colorOffset + i3 + 2 ] ]; + colors[ i4 + 3 ] = bytes[ alphaOffset + i ]; } @@ -204,7 +245,7 @@ class SPZLoader extends Loader { } -function readCenters( view, centers, offset, count, version, fractionalBits ) { +function readCenters( bytes, centers, offset, count, version, fractionalBits ) { if ( version === 1 ) { @@ -213,9 +254,9 @@ function readCenters( view, centers, offset, count, version, fractionalBits ) { const i3 = i * 3; const rowOffset = offset + i3 * 2; - centers[ i3 ] = DataUtils.fromHalfFloat( view.getUint16( rowOffset, true ) ); - centers[ i3 + 1 ] = DataUtils.fromHalfFloat( view.getUint16( rowOffset + 2, true ) ); - centers[ i3 + 2 ] = DataUtils.fromHalfFloat( view.getUint16( rowOffset + 4, true ) ); + centers[ i3 ] = DataUtils.fromHalfFloat( bytes[ rowOffset ] | ( bytes[ rowOffset + 1 ] << 8 ) ); + centers[ i3 + 1 ] = DataUtils.fromHalfFloat( bytes[ rowOffset + 2 ] | ( bytes[ rowOffset + 3 ] << 8 ) ); + centers[ i3 + 2 ] = DataUtils.fromHalfFloat( bytes[ rowOffset + 4 ] | ( bytes[ rowOffset + 5 ] << 8 ) ); } @@ -230,9 +271,9 @@ function readCenters( view, centers, offset, count, version, fractionalBits ) { const i3 = i * 3; const rowOffset = offset + i * 9; - centers[ i3 ] = readInt24( view, rowOffset ) * fixedScale; - centers[ i3 + 1 ] = readInt24( view, rowOffset + 3 ) * fixedScale; - centers[ i3 + 2 ] = readInt24( view, rowOffset + 6 ) * fixedScale; + centers[ i3 ] = readInt24( bytes, rowOffset ) * fixedScale; + centers[ i3 + 1 ] = readInt24( bytes, rowOffset + 3 ) * fixedScale; + centers[ i3 + 2 ] = readInt24( bytes, rowOffset + 6 ) * fixedScale; } @@ -240,64 +281,56 @@ function readCenters( view, centers, offset, count, version, fractionalBits ) { } -function readInt24( view, offset ) { - - let value = view.getUint8( offset ) | ( view.getUint8( offset + 1 ) << 8 ) | ( view.getUint8( offset + 2 ) << 16 ); - - if ( ( value & 0x800000 ) !== 0 ) { +function readInt24( bytes, offset ) { - value |= 0xff000000; - - } - - return value; + // The left shift by 8 followed by an arithmetic right shift sign-extends + // the 24-bit value. + return ( ( bytes[ offset ] << 8 ) | ( bytes[ offset + 1 ] << 16 ) | ( bytes[ offset + 2 ] << 24 ) ) >> 8; } -function readXYZQuaternion( bytes, offset ) { +function readXYZQuaternion( bytes, offset, target ) { const qx = bytes[ offset ] / 127.5 - 1; const qy = bytes[ offset + 1 ] / 127.5 - 1; const qz = bytes[ offset + 2 ] / 127.5 - 1; - const qw = Math.sqrt( Math.max( 0, 1 - qx * qx - qy * qy - qz * qz ) ); - return [ qx, qy, qz, qw ]; + target[ 0 ] = qx; + target[ 1 ] = qy; + target[ 2 ] = qz; + target[ 3 ] = Math.sqrt( Math.max( 0, 1 - qx * qx - qy * qy - qz * qz ) ); } -function readSmallestThreeQuaternion( view, offset ) { +function readSmallestThreeQuaternion( packed, target ) { - const maxValue = Math.SQRT1_2; - const valueMask = ( 1 << 9 ) - 1; - const quaternion = [ 0, 0, 0, 0 ]; - const packed = view.getUint32( offset, true ); const largestIndex = packed >>> 30; - let remainingValues = packed; - let sumSquares = 0; - - for ( let i = 3; i >= 0; i -- ) { - if ( i === largestIndex ) continue; - - const value = remainingValues & valueMask; - const sign = ( remainingValues >>> 9 ) & 1; - remainingValues >>>= 10; - - quaternion[ i ] = maxValue * ( value / valueMask ); - - if ( sign !== 0 ) { - - quaternion[ i ] = - quaternion[ i ]; - - } - - sumSquares += quaternion[ i ] * quaternion[ i ]; + // The three smallest components are packed from the lowest bits upward, + // filling the non-largest indices in descending order: the low 10 bits go + // to the highest remaining index, the top 10 bits to the lowest. + const a = QUAT_COMPONENT_LUT[ packed & 1023 ]; + const b = QUAT_COMPONENT_LUT[ ( packed >>> 10 ) & 1023 ]; + const c = QUAT_COMPONENT_LUT[ ( packed >>> 20 ) & 1023 ]; + + switch ( largestIndex ) { + + case 0: + target[ 1 ] = c; target[ 2 ] = b; target[ 3 ] = a; + break; + case 1: + target[ 0 ] = c; target[ 2 ] = b; target[ 3 ] = a; + break; + case 2: + target[ 0 ] = c; target[ 1 ] = b; target[ 3 ] = a; + break; + default: + target[ 0 ] = c; target[ 1 ] = b; target[ 2 ] = a; + break; } - quaternion[ largestIndex ] = Math.sqrt( Math.max( 0, 1 - sumSquares ) ); - - return quaternion; + target[ largestIndex ] = Math.sqrt( Math.max( 0, 1 - ( a * a + b * b + c * c ) ) ); } diff --git a/examples/jsm/utils/GaussianSplatUtils.js b/examples/jsm/utils/GaussianSplatUtils.js index a0ab56ad282b10..22ef7339fcdb0f 100644 --- a/examples/jsm/utils/GaussianSplatUtils.js +++ b/examples/jsm/utils/GaussianSplatUtils.js @@ -11,24 +11,20 @@ const GAUSSIAN_SPLAT_PLY_PROPERTY_MAPPING = { opacity: [ 'opacity' ] }; -function clampByte( value ) { - - return Math.min( 255, Math.max( 0, Math.round( value ) ) ); - -} - function sigmoid( value ) { return 1 / ( 1 + Math.exp( - value ) ); } +// The target is expected to be a Uint8ClampedArray, which clamps and rounds +// assigned values natively. function writeColorBytes( target, offset, r, g, b, a ) { - target[ offset ] = clampByte( r ); - target[ offset + 1 ] = clampByte( g ); - target[ offset + 2 ] = clampByte( b ); - target[ offset + 3 ] = clampByte( a ); + target[ offset ] = r; + target[ offset + 1 ] = g; + target[ offset + 2 ] = b; + target[ offset + 3 ] = a; } @@ -59,7 +55,9 @@ function writeColorBytesFromSH0( target, offset, r, g, b, a ) { function writeCovariance( target, offset, sx, sy, sz, qx, qy, qz, qw ) { - const length = Math.hypot( qx, qy, qz, qw ); + // Math.sqrt is significantly faster than Math.hypot, and the overflow + // protection of Math.hypot is unnecessary for quaternion components. + const length = Math.sqrt( qx * qx + qy * qy + qz * qz + qw * qw ); if ( length === 0 ) { @@ -168,7 +166,7 @@ function createGaussianSplatGeometryFromPLYGeometry( geometry, { const centers = new Float32Array( count * 3 ); const covariances = new Float32Array( count * 6 ); - const colors = new Uint8Array( count * 4 ); + const colors = new Uint8ClampedArray( count * 4 ); for ( let i = 0; i < count; i ++ ) { @@ -206,7 +204,6 @@ function createGaussianSplatGeometryFromPLYGeometry( geometry, { export { GAUSSIAN_SPLAT_PLY_PROPERTY_MAPPING, SH_C0, - clampByte, createGaussianSplatGeometry, createGaussianSplatGeometryFromPLYGeometry, linearToSH0, From 3b7d8368854922c3503f3edcf0e4a43b633edfd4 Mon Sep 17 00:00:00 2001 From: sunag Date: Mon, 10 Aug 2026 12:47:37 -0300 Subject: [PATCH 5/7] Transpiler: Add GLSL preprocessor directives and macro support (#34206) --- examples/jsm/transpiler/GLSLDecoder.js | 392 ++++++++++++++++++++++++- 1 file changed, 391 insertions(+), 1 deletion(-) diff --git a/examples/jsm/transpiler/GLSLDecoder.js b/examples/jsm/transpiler/GLSLDecoder.js index 1d9203a6df857e..9c7f3ad75cfe05 100644 --- a/examples/jsm/transpiler/GLSLDecoder.js +++ b/examples/jsm/transpiler/GLSLDecoder.js @@ -1190,8 +1190,399 @@ class GLSLDecoder { } + evaluateCondition( expr, macros ) { + + let str = expr; + + str = str.replace( /defined\s*\(\s*(\w+)\s*\)/g, ( _, name ) => macros.has( name ) ? '1' : '0' ); + str = str.replace( /defined\s+(\w+)/g, ( _, name ) => macros.has( name ) ? '1' : '0' ); + + str = str.replace( /\b([A-Za-z_]\w*)\b/g, ( _, name ) => { + + if ( name === 'true' ) return '1'; + if ( name === 'false' ) return '0'; + + if ( macros.has( name ) ) { + + const macro = macros.get( name ); + const val = macro.body; + return val !== '' ? val : '1'; + + } + + return '0'; + + } ); + + try { + + if ( /^[\d\s+\-*/%&|^!=<>~()]+$/.test( str ) ) { + + return Boolean( Function( `"use strict"; return (${ str });` )() ); + + } + + } catch ( e ) { + + return false; + + } + + return false; + + } + + extractMacroArgs( str ) { + + const args = []; + let currentArg = ''; + let parenDepth = 0; + + for ( let i = 0; i < str.length; i ++ ) { + + const char = str[ i ]; + + if ( char === '(' || char === '[' || char === '{' ) { + + parenDepth ++; + currentArg += char; + + } else if ( char === ')' || char === ']' || char === '}' ) { + + parenDepth --; + currentArg += char; + + } else if ( char === ',' && parenDepth === 0 ) { + + args.push( currentArg.trim() ); + currentArg = ''; + + } else { + + currentArg += char; + + } + + } + + if ( currentArg.trim() !== '' || args.length > 0 ) { + + args.push( currentArg.trim() ); + + } + + return args; + + } + + expandMacros( line, macros ) { + + if ( macros.size === 0 ) return line; + + let result = line; + let passes = 0; + const maxPasses = 10; + + while ( passes < maxPasses ) { + + let changed = false; + + for ( const [ name, macro ] of macros ) { + + if ( macro.params !== null ) { + + const regex = new RegExp( `\\b${ name }\\s*\\(`, 'g' ); + let match; + + while ( ( match = regex.exec( result ) ) !== null ) { + + const startIdx = match.index; + const parenStartIdx = startIdx + match[ 0 ].length - 1; + + let parenDepth = 1; + let parenEndIdx = - 1; + + for ( let i = parenStartIdx + 1; i < result.length; i ++ ) { + + if ( result[ i ] === '(' ) parenDepth ++; + else if ( result[ i ] === ')' ) parenDepth --; + + if ( parenDepth === 0 ) { + + parenEndIdx = i; + break; + + } + + } + + if ( parenEndIdx !== - 1 ) { + + const rawArgs = result.slice( parenStartIdx + 1, parenEndIdx ); + const args = this.extractMacroArgs( rawArgs ); + + let substitutedBody = macro.body; + + for ( let p = 0; p < macro.params.length; p ++ ) { + + const paramName = macro.params[ p ]; + const argVal = args[ p ] !== undefined ? args[ p ] : ''; + const paramRegex = new RegExp( `\\b${ paramName }\\b`, 'g' ); + + substitutedBody = substitutedBody.replace( paramRegex, argVal ); + + } + + result = result.slice( 0, startIdx ) + substitutedBody + result.slice( parenEndIdx + 1 ); + changed = true; + + break; + + } + + } + + } else { + + if ( macro.body === '' ) continue; + + const regex = new RegExp( `\\b${ name }\\b`, 'g' ); + + if ( regex.test( result ) ) { + + result = result.replace( regex, macro.body ); + changed = true; + + } + + } + + } + + if ( ! changed ) break; + + passes ++; + + } + + return result; + + } + + preprocess( source ) { + + const macros = new Map(); + const conditionalStack = []; + + const isExecuting = () => conditionalStack.every( frame => frame.active ); + + const lines = source.split( '\n' ); + const outputLines = []; + + let inBlockComment = false; + + for ( let i = 0; i < lines.length; i ++ ) { + + let line = lines[ i ]; + + while ( line.endsWith( '\\' ) && i + 1 < lines.length ) { + + line = line.slice( 0, - 1 ) + ' ' + lines[ i + 1 ]; + i ++; + outputLines.push( '' ); + + } + + let trimmedLine = line.trim(); + + if ( inBlockComment ) { + + const endCommentIndex = trimmedLine.indexOf( '*/' ); + + if ( endCommentIndex !== - 1 ) { + + inBlockComment = false; + trimmedLine = trimmedLine.slice( endCommentIndex + 2 ).trim(); + + } else { + + outputLines.push( line ); + continue; + + } + + } + + if ( trimmedLine.startsWith( '/*' ) ) { + + const endCommentIndex = trimmedLine.indexOf( '*/', 2 ); + + if ( endCommentIndex === - 1 ) { + + inBlockComment = true; + outputLines.push( line ); + continue; + + } + + } + + const directiveMatch = trimmedLine.match( /^#\s*(\w+)(?:\s+(.*))?$/ ); + + if ( directiveMatch ) { + + const directive = directiveMatch[ 1 ]; + let args = directiveMatch[ 2 ] || ''; + + args = args.replace( /\/\/.*$/, '' ).replace( /\/\*.*?\*\//g, '' ).trim(); + + if ( directive === 'define' ) { + + if ( isExecuting() ) { + + const fnMatch = args.match( /^(\w+)\((.*?)\)\s*(.*)$/ ); + + if ( fnMatch ) { + + const name = fnMatch[ 1 ]; + const params = fnMatch[ 2 ].split( ',' ).map( p => p.trim() ).filter( p => p !== '' ); + const body = fnMatch[ 3 ] !== undefined ? fnMatch[ 3 ].trim() : ''; + + macros.set( name, { params, body } ); + + } else { + + const objMatch = args.match( /^(\w+)(?:\s+(.*))?$/ ); + + if ( objMatch ) { + + const name = objMatch[ 1 ]; + const value = objMatch[ 2 ] !== undefined ? objMatch[ 2 ].trim() : ''; + + macros.set( name, { params: null, body: value } ); + + } + + } + + } + + } else if ( directive === 'undef' ) { + + if ( isExecuting() ) { + + const name = args.trim(); + + macros.delete( name ); + + } + + } else if ( directive === 'ifdef' ) { + + const name = args.trim(); + const parentActive = isExecuting(); + const condition = parentActive && macros.has( name ); + + conditionalStack.push( { active: condition, anyBranchExecuted: condition } ); + + } else if ( directive === 'ifndef' ) { + + const name = args.trim(); + const parentActive = isExecuting(); + const condition = parentActive && ! macros.has( name ); + + conditionalStack.push( { active: condition, anyBranchExecuted: condition } ); + + } else if ( directive === 'if' ) { + + const parentActive = isExecuting(); + const condition = parentActive && this.evaluateCondition( args, macros ); + + conditionalStack.push( { active: Boolean( condition ), anyBranchExecuted: Boolean( condition ) } ); + + } else if ( directive === 'elif' ) { + + if ( conditionalStack.length > 0 ) { + + const currentFrame = conditionalStack[ conditionalStack.length - 1 ]; + const parentActive = conditionalStack.slice( 0, - 1 ).every( frame => frame.active ); + + if ( ! parentActive || currentFrame.anyBranchExecuted ) { + + currentFrame.active = false; + + } else { + + const condition = this.evaluateCondition( args, macros ); + + currentFrame.active = Boolean( condition ); + + if ( condition ) { + + currentFrame.anyBranchExecuted = true; + + } + + } + + } + + } else if ( directive === 'else' ) { + + if ( conditionalStack.length > 0 ) { + + const currentFrame = conditionalStack[ conditionalStack.length - 1 ]; + const parentActive = conditionalStack.slice( 0, - 1 ).every( frame => frame.active ); + + if ( ! parentActive || currentFrame.anyBranchExecuted ) { + + currentFrame.active = false; + + } else { + + currentFrame.active = true; + currentFrame.anyBranchExecuted = true; + + } + + } + + } else if ( directive === 'endif' ) { + + if ( conditionalStack.length > 0 ) { + + conditionalStack.pop(); + + } + + } + + outputLines.push( '' ); + + } else { + + if ( isExecuting() ) { + + outputLines.push( this.expandMacros( line, macros ) ); + + } else { + + outputLines.push( '' ); + + } + + } + + } + + return outputLines.join( '\n' ); + + } + parse( source ) { + source = this.preprocess( source ); + let polyfill = ''; for ( const keyword of this.keywords ) { @@ -1220,7 +1611,6 @@ class GLSLDecoder { return program; - } } From 7525fc9b67642c599f1e951715daf3fde675ac33 Mon Sep 17 00:00:00 2001 From: sunag Date: Mon, 10 Aug 2026 14:03:48 -0300 Subject: [PATCH 6/7] Transpiler: Add `array` support (#34208) --- examples/jsm/transpiler/AST.js | 20 +++++++- examples/jsm/transpiler/GLSLDecoder.js | 71 +++++++++++++++++++++++--- examples/jsm/transpiler/TSLEncoder.js | 20 ++++---- examples/jsm/transpiler/WGSLEncoder.js | 31 +++++++++-- 4 files changed, 121 insertions(+), 21 deletions(-) diff --git a/examples/jsm/transpiler/AST.js b/examples/jsm/transpiler/AST.js index ac89ff5899a62a..549c116c0b74b0 100644 --- a/examples/jsm/transpiler/AST.js +++ b/examples/jsm/transpiler/AST.js @@ -1,4 +1,4 @@ -import { toFloatType } from './TranspilerUtils.js'; +import { toFloatType, isBuiltinType } from './TranspilerUtils.js'; export class ASTNode { @@ -436,6 +436,24 @@ export class FunctionCall extends ASTNode { } + getType() { + + if ( isBuiltinType( this.name ) ) { + + return this.name; + + } + + if ( this.linker.reference ) { + + return this.linker.reference.getType(); + + } + + return super.getType(); + + } + } export class Return extends ASTNode { diff --git a/examples/jsm/transpiler/GLSLDecoder.js b/examples/jsm/transpiler/GLSLDecoder.js index 9c7f3ad75cfe05..3810eb90a4b718 100644 --- a/examples/jsm/transpiler/GLSLDecoder.js +++ b/examples/jsm/transpiler/GLSLDecoder.js @@ -281,6 +281,25 @@ class GLSLDecoder { } + getTokenPosition( token ) { + + if ( ! token || token.pos === undefined || ! token.tokenizer || ! token.tokenizer.source ) { + + return ''; + + } + + const source = token.tokenizer.source; + const textBefore = source.slice( 0, token.pos ); + + const lines = textBefore.split( '\n' ); + const lineNumber = lines.length; + const columnNumber = lines[ lines.length - 1 ].length + 1; + + return ` (line ${ lineNumber }, column ${ columnNumber })`; + + } + getToken( offset = 0 ) { return this.tokens[ this.index + offset ]; @@ -481,6 +500,14 @@ class GLSLDecoder { return left; + } else if ( firstToken.str === '{' ) { + + const internalTokens = tokens.slice( 1, tokens.length - 1 ); + + const paramsTokens = this.parseFunctionParametersFromTokens( internalTokens ); + + return new FunctionCall( 'array', paramsTokens ); + } // primitives and accessors @@ -558,6 +585,20 @@ class GLSLDecoder { } else if ( secondToken.str === '[' ) { + const bracketTokens = this.getTokensUntil( ']', tokens, 1 ); + const parenToken = tokens[ 1 + bracketTokens.length ]; + + if ( parenToken && parenToken.str === '(' ) { + + // array constructor: type[N](args...) or type[](args...) + const parenIndex = 1 + bracketTokens.length; + const internalTokens = this.getTokensUntil( ')', tokens, parenIndex ).slice( 1, - 1 ); + const paramsTokens = this.parseFunctionParametersFromTokens( internalTokens ); + + return new FunctionCall( 'array', paramsTokens ); + + } + // array accessor const elements = this.parseAccessorElementsFromTokens( tokens.slice( 1 ) ); @@ -572,6 +613,8 @@ class GLSLDecoder { } + throw new Error( 'THREE.GLSLDecoder: Unexpected token "' + firstToken.str + '"' + this.getTokenPosition( firstToken ) ); + } parseAccessorElementsFromTokens( tokens ) { @@ -606,9 +649,7 @@ class GLSLDecoder { } else { - console.error( 'Unknown accessor expression', token ); - - break; + throw new Error( 'THREE.GLSLDecoder: Unknown accessor expression token "' + token.str + '"' + this.getTokenPosition( token ) ); } @@ -623,19 +664,26 @@ class GLSLDecoder { if ( tokens.length === 0 ) return []; const expression = this.parseExpressionFromTokens( tokens ); + + if ( ! expression ) { + + throw new Error( 'THREE.GLSLDecoder: Invalid parameter expression' + this.getTokenPosition( tokens[ 0 ] ) ); + + } + const params = []; let current = expression; - while ( current.type === ',' ) { + while ( current && current.type === ',' ) { - params.push( current.left ); + if ( current.left ) params.push( current.left ); current = current.right; } - params.push( current ); + if ( current ) params.push( current ); return params; @@ -711,11 +759,20 @@ class GLSLDecoder { type = type || tokens[ index ++ ].str; const name = tokens[ index ++ ].str; - const token = tokens[ index ]; + let token = tokens[ index ]; let init = null; let next = null; + if ( token && token.str === '[' ) { + + const bracketTokens = this.getTokensUntil( ']', tokens, index ); + + index += bracketTokens.length; + token = tokens[ index ]; + + } + if ( token ) { const initTokens = this.getTokensUntil( ',', tokens, index ); diff --git a/examples/jsm/transpiler/TSLEncoder.js b/examples/jsm/transpiler/TSLEncoder.js index 1039e9131036c5..1f977828df8346 100644 --- a/examples/jsm/transpiler/TSLEncoder.js +++ b/examples/jsm/transpiler/TSLEncoder.js @@ -188,9 +188,13 @@ class TSLEncoder { } - // handle texture lookup function calls in separate branch + if ( node.name === 'array' ) { - if ( textureLookupFunctions.includes( node.name ) ) { + this.addImport( 'array' ); + + code = `array( [ ${ params.join( ', ' ) } ] )`; + + } else if ( textureLookupFunctions.includes( node.name ) ) { code = `${ params[ 0 ] }.sample( ${ params[ 1 ] } )`; @@ -368,12 +372,10 @@ class TSLEncoder { } else { - console.warn( 'Unknown node type', node ); + throw new Error( 'THREE.TSLEncoder: Unknown AST node type "' + node.constructor.name + '"' ); } - if ( ! code ) code = '/* unknown statement */'; - return code; } @@ -504,7 +506,7 @@ ${ this.tab }} )`; } else if ( node.afterthought.isOperator ) { - if ( node.afterthought.right.isAccessor || node.afterthought.right.isNumber ) { + if ( node.afterthought.right && ( node.afterthought.right.isAccessor || node.afterthought.right.isNumber ) ) { updateParam = `, update: ${ this.emitExpression( node.afterthought.right ) }`; @@ -590,10 +592,10 @@ ${ this.tab }} )`; const { initialization, condition, afterthought } = node; if ( ( initialization && initialization.isVariableDeclaration && initialization.next === null ) && - ( condition && condition.left.isAccessor && condition.left.property === initialization.name ) && + ( condition && condition.left && condition.left.isAccessor && condition.left.property === initialization.name ) && ( afterthought && ( - ( afterthought.isUnary && ( initialization.name === afterthought.expression.property ) ) || - ( afterthought.isOperator && ( initialization.name === afterthought.left.property ) ) + ( afterthought.isUnary && afterthought.expression && ( initialization.name === afterthought.expression.property ) ) || + ( afterthought.isOperator && afterthought.left && ( initialization.name === afterthought.left.property ) ) ) ) ) { diff --git a/examples/jsm/transpiler/WGSLEncoder.js b/examples/jsm/transpiler/WGSLEncoder.js index 0588c36575491b..7995860711eca7 100644 --- a/examples/jsm/transpiler/WGSLEncoder.js +++ b/examples/jsm/transpiler/WGSLEncoder.js @@ -194,6 +194,23 @@ class WGSLEncoder { code += `( ${ params } )`; + } else if ( fnName === 'array' ) { + + const params = node.params.map( p => this.emitExpression( p ) ); + + if ( node.params.length > 0 && node.params[ 0 ].getType() ) { + + const elemType = this.getWgslType( node.params[ 0 ].getType() ); + const count = node.params.length; + + code = `array<${ elemType }, ${ count }>( ${ params.join( ', ' ) } )`; + + } else { + + code = `array( ${ params.join( ', ' ) } )`; + + } + } else if ( fnName.startsWith( 'texture' ) ) { // Handle texture functions separately due to sampler handling @@ -335,9 +352,7 @@ class WGSLEncoder { } else { - console.warn( 'Unknown node type in WGSL Encoder:', node ); - - code = `/* unknown node: ${ node.constructor.name } */`; + throw new Error( 'THREE.WGSLEncoder: Unknown AST node type "' + node.constructor.name + '"' ); } @@ -577,7 +592,15 @@ class WGSLEncoder { } - declarations.push( `${ keyword } ${ current.name }: ${ type }${ valueStr }` ); + let typeStr = `: ${ type }`; + + if ( current.value && current.value.isFunctionCall && current.value.name === 'array' ) { + + typeStr = ''; + + } + + declarations.push( `${ keyword } ${ current.name }${ typeStr }${ valueStr }` ); current = current.next; From c8706dcee0a72177cba2566f8f3e1cb9bdca125d Mon Sep 17 00:00:00 2001 From: sunag Date: Mon, 10 Aug 2026 14:18:11 -0300 Subject: [PATCH 7/7] Inspector: Fix split screen overlay alignment on sub-window canvases (#34203) --- examples/jsm/inspector/tabs/Viewer.js | 78 +++++++++++++++++++++------ examples/jsm/inspector/ui/Style.js | 4 +- 2 files changed, 64 insertions(+), 18 deletions(-) diff --git a/examples/jsm/inspector/tabs/Viewer.js b/examples/jsm/inspector/tabs/Viewer.js index e72ee8ad7d9462..72450aa751e3b6 100644 --- a/examples/jsm/inspector/tabs/Viewer.js +++ b/examples/jsm/inspector/tabs/Viewer.js @@ -616,18 +616,28 @@ class Viewer extends Tab { startSplitMode( canvasData ) { + if ( this.profiler && this.profiler.panel.classList.contains( 'visible' ) ) { + + this.profiler.togglePanel(); + + } + this.splitActive = true; this.splitCanvasData = canvasData; const renderer = this.inspector.getRenderer(); const mainCanvas = renderer.domElement; const rect = mainCanvas.getBoundingClientRect(); + const targetParent = document.fullscreenElement || this.profiler.domElement; + const parentRect = targetParent.getBoundingClientRect(); + const localLeft = rect.left - parentRect.left; + const localTop = rect.top - parentRect.top; // Position target canvas on top of main canvas if ( ! this.splitCanvas ) { this.splitCanvas = document.createElement( 'canvas' ); - this.splitCanvas.style.position = 'fixed'; + this.splitCanvas.style.position = 'absolute'; this.splitCanvas.style.pointerEvents = 'none'; this.splitCanvas.style.zIndex = '998'; @@ -636,15 +646,15 @@ class Viewer extends Tab { } - this.splitCanvas.style.left = `${ rect.left }px`; - this.splitCanvas.style.top = `${ rect.top }px`; + this.splitCanvas.style.left = `${ localLeft }px`; + this.splitCanvas.style.top = `${ localTop }px`; this.splitCanvas.style.width = `${ rect.width }px`; this.splitCanvas.style.height = `${ rect.height }px`; this.splitCanvasTarget.setSize( rect.width, rect.height ); renderer.backend.delete( this.splitCanvasTarget ); - document.body.appendChild( this.splitCanvas ); + targetParent.appendChild( this.splitCanvas ); // Overlay divider line (only in split/non-fullscreen mode) if ( ! this.splitFullscreen ) { @@ -652,7 +662,7 @@ class Viewer extends Tab { if ( ! this.splitOverlay ) { const overlay = document.createElement( 'div' ); - overlay.className = 'split-screen-overlay'; + overlay.className = 'split-screen-overlay three-inspector'; const line = document.createElement( 'div' ); line.className = 'split-screen-line'; @@ -672,9 +682,9 @@ class Viewer extends Tab { if ( ! isDragging ) return; - const minPadding = 10; // Keep the line at least 15px away from the edges for easy grabbing - const x = Math.max( minPadding, Math.min( window.innerWidth - minPadding, e.clientX ) ); - const pct = x / window.innerWidth; + const r = mainCanvas.getBoundingClientRect(); + const localX = e.clientX - r.left; + const pct = Math.max( 0, Math.min( 1, localX / r.width ) ); this.splitX = pct; line.style.left = `${ pct * 100 }%`; @@ -701,9 +711,13 @@ class Viewer extends Tab { } + this.splitOverlay.style.left = `${ localLeft }px`; + this.splitOverlay.style.top = `${ localTop }px`; + this.splitOverlay.style.width = `${ rect.width }px`; + this.splitOverlay.style.height = `${ rect.height }px`; this.splitLine.style.left = '50%'; - this.profiler.domElement.appendChild( this.splitOverlay ); + targetParent.appendChild( this.splitOverlay ); } else { @@ -720,13 +734,13 @@ class Viewer extends Tab { this.splitUniforms = { splitX: uniform( 0.5 ), - viewportWidth: uniform( window.innerWidth ) + viewportWidth: uniform( rect.width ) }; } this.splitUniforms.splitX.value = 0.5; - this.splitUniforms.viewportWidth.value = renderer.domElement.width; + this.splitUniforms.viewportWidth.value = rect.width; // Recreate or setup material for split screen comparison const node = canvasData.node; @@ -931,19 +945,49 @@ class Viewer extends Tab { if ( this.splitActive ) { - // Resize canvas target to match the main canvas if window resized const mainCanvas = renderer.domElement; const rect = mainCanvas.getBoundingClientRect(); + const targetParent = document.fullscreenElement || this.profiler.domElement; + const parentRect = targetParent.getBoundingClientRect(); + const localLeft = rect.left - parentRect.left; + const localTop = rect.top - parentRect.top; - if ( this.splitCanvasTarget.domElement.width !== rect.width || this.splitCanvasTarget.domElement.height !== rect.height ) { + if ( this.splitCanvas.parentElement !== targetParent ) { + + targetParent.appendChild( this.splitCanvas ); + + } + + if ( this.splitOverlay && ! this.splitFullscreen && this.splitOverlay.parentElement !== targetParent ) { + + targetParent.appendChild( this.splitOverlay ); + + } + + this.splitCanvas.style.left = `${ localLeft }px`; + this.splitCanvas.style.top = `${ localTop }px`; + this.splitCanvas.style.width = `${ rect.width }px`; + this.splitCanvas.style.height = `${ rect.height }px`; + + if ( this.splitOverlay && ! this.splitFullscreen ) { - this.splitCanvas.style.width = `${ rect.width }px`; - this.splitCanvas.style.height = `${ rect.height }px`; - this.splitCanvas.style.left = `${ rect.left }px`; - this.splitCanvas.style.top = `${ rect.top }px`; + this.splitOverlay.style.left = `${ localLeft }px`; + this.splitOverlay.style.top = `${ localTop }px`; + this.splitOverlay.style.width = `${ rect.width }px`; + this.splitOverlay.style.height = `${ rect.height }px`; + + } + + if ( this.splitCanvasTarget.domElement.width !== rect.width || this.splitCanvasTarget.domElement.height !== rect.height ) { this.splitCanvasTarget.setSize( rect.width, rect.height ); + if ( this.splitUniforms && this.splitUniforms.viewportWidth ) { + + this.splitUniforms.viewportWidth.value = rect.width; + + } + renderer.backend.delete( this.splitCanvasTarget ); } diff --git a/examples/jsm/inspector/ui/Style.js b/examples/jsm/inspector/ui/Style.js index ceeb16b324b299..c1f9d5fde54afb 100644 --- a/examples/jsm/inspector/ui/Style.js +++ b/examples/jsm/inspector/ui/Style.js @@ -28,6 +28,7 @@ export class Style { height: 100%; pointer-events: none; z-index: 1000; + overflow: hidden; } :scope * { @@ -2081,7 +2082,7 @@ export class Style { } .split-screen-overlay { - position: fixed; + position: absolute; top: 0; left: 0; width: 100%; @@ -2089,6 +2090,7 @@ export class Style { pointer-events: none !important; z-index: 999; touch-action: none; + overflow: hidden; } .split-screen-line {