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; 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 { 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/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 1d9203a6df857e..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 ); @@ -1190,8 +1247,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 +1668,6 @@ class GLSLDecoder { return program; - } } 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; 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, diff --git a/examples/screenshots/webgpu_compute_particles_rain.jpg b/examples/screenshots/webgpu_compute_particles_rain.jpg index ad44b454bde21d..978d9192a9482f 100644 Binary files a/examples/screenshots/webgpu_compute_particles_rain.jpg and b/examples/screenshots/webgpu_compute_particles_rain.jpg differ diff --git a/examples/screenshots/webgpu_tsl_vfx_flames.jpg b/examples/screenshots/webgpu_tsl_vfx_flames.jpg index c055af4fddd2b9..27233b0d7c132a 100644 Binary files a/examples/screenshots/webgpu_tsl_vfx_flames.jpg and b/examples/screenshots/webgpu_tsl_vfx_flames.jpg differ diff --git a/examples/webgpu_compute_particles_rain.html b/examples/webgpu_compute_particles_rain.html index 0785889dba9091..5b65c1e37c915a 100644 --- a/examples/webgpu_compute_particles_rain.html +++ b/examples/webgpu_compute_particles_rain.html @@ -185,10 +185,9 @@ const rainMaterial = new THREE.MeshBasicNodeMaterial(); rainMaterial.colorNode = uv().distance( vec2( .5, 0 ) ).oneMinus().mul( 3 ).exp().mul( .1 ); - rainMaterial.vertexNode = billboarding( { position: positionBuffer.toAttribute() } ); + rainMaterial.positionNode = positionGeometry.add( positionBuffer.toAttribute() ); + rainMaterial.vertexNode = billboarding( { horizontalRotation: true } ); rainMaterial.opacity = .2; - rainMaterial.side = THREE.DoubleSide; - rainMaterial.forceSinglePass = true; rainMaterial.depthWrite = false; rainMaterial.depthTest = true; rainMaterial.transparent = true; diff --git a/examples/webgpu_tsl_vfx_flames.html b/examples/webgpu_tsl_vfx_flames.html index ee19b5afe5e125..4e5f1a1a8c6aa4 100644 --- a/examples/webgpu_tsl_vfx_flames.html +++ b/examples/webgpu_tsl_vfx_flames.html @@ -186,8 +186,8 @@ // billboarding - follow the camera rotation only horizontally - flame1Material.vertexNode = billboarding(); - flame2Material.vertexNode = billboarding(); + flame1Material.vertexNode = billboarding( { horizontalRotation: true } ); + flame2Material.vertexNode = billboarding( { horizontalRotation: true } ); // meshes 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/nodes/utils/SpriteUtils.js b/src/nodes/utils/SpriteUtils.js index fce1f56e343fa5..32ca4a27482cee 100644 --- a/src/nodes/utils/SpriteUtils.js +++ b/src/nodes/utils/SpriteUtils.js @@ -1,7 +1,7 @@ import { modelWorldMatrix } from '../accessors/ModelNode.js'; -import { cameraViewMatrix, cameraProjectionMatrix } from '../accessors/Camera.js'; -import { positionLocal } from '../accessors/Position.js'; -import { Fn, defined } from '../tsl/TSLBase.js'; +import { cameraViewMatrix, cameraProjectionMatrix, cameraPosition } from '../accessors/Camera.js'; +import { positionGeometry, positionWorld } from '../accessors/Position.js'; +import { Fn, defined, nodeObject, vec3, vec4 } from '../tsl/TSLBase.js'; /** * This can be used to achieve a billboarding behavior for flat meshes. That means they are @@ -14,50 +14,85 @@ import { Fn, defined } from '../tsl/TSLBase.js'; * @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. */ -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 ); } ); 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' ) + '-' +