From 522bc18d8beaf8a145bc69f4c261108071d827fb Mon Sep 17 00:00:00 2001 From: Michael Herzog Date: Thu, 4 Jun 2026 09:59:45 +0200 Subject: [PATCH 1/7] Update TSL.md Fix typo. --- docs/TSL.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/TSL.md b/docs/TSL.md index e1dd74fd10e165..a6d21a4f60785e 100644 --- a/docs/TSL.md +++ b/docs/TSL.md @@ -490,7 +490,7 @@ It's possible use `xyzw`, `rgba` or `stpq`. | Name | Description | | -- | -- | | `.add( node \| value, ... )` | Return the addition of two or more value. | -| `.sub( node \| value )` | Return the subraction of two or more value. | +| `.sub( node \| value )` | Return the subtraction of two or more value. | | `.mul( node \| value )` | Return the multiplication of two or more value. | | `.div( node \| value )` | Return the division of two or more value. | | `.mod( node \| value )` | Computes the remainder of dividing the first node by the second. | From cd1c890403d1c5c7d17d9716fe6726924fd85b0e Mon Sep 17 00:00:00 2001 From: Michael Herzog Date: Thu, 4 Jun 2026 10:01:13 +0200 Subject: [PATCH 2/7] MaterialLoader: Add `registerMaterial()` and `Material.fromJSON()`. (#33714) --- src/loaders/MaterialLoader.js | 302 ++++---------------------------- src/materials/Material.js | 191 ++++++++++++++++++++ src/materials/ShaderMaterial.js | 86 +++++++++ 3 files changed, 313 insertions(+), 266 deletions(-) diff --git a/src/loaders/MaterialLoader.js b/src/loaders/MaterialLoader.js index 2422c4a0af4626..1a2c4a0e4b8cb5 100644 --- a/src/loaders/MaterialLoader.js +++ b/src/loaders/MaterialLoader.js @@ -1,9 +1,3 @@ -import { Color } from '../math/Color.js'; -import { Vector2 } from '../math/Vector2.js'; -import { Vector3 } from '../math/Vector3.js'; -import { Vector4 } from '../math/Vector4.js'; -import { Matrix3 } from '../math/Matrix3.js'; -import { Matrix4 } from '../math/Matrix4.js'; import { FileLoader } from './FileLoader.js'; import { Loader } from './Loader.js'; import { @@ -26,7 +20,9 @@ import { LineBasicMaterial, Material, } from '../materials/Materials.js'; -import { error, warn } from '../utils.js'; +import { error, warnOnce } from '../utils.js'; + +const _customMaterials = {}; /** * Class for loading materials. The files are internally @@ -110,265 +106,9 @@ class MaterialLoader extends Loader { */ parse( json ) { - const textures = this.textures; - - function getTexture( name ) { - - if ( textures[ name ] === undefined ) { - - warn( 'MaterialLoader: Undefined texture', name ); - - } - - return textures[ name ]; - - } - const material = this.createMaterialFromType( json.type ); - if ( json.uuid !== undefined ) material.uuid = json.uuid; - if ( json.name !== undefined ) material.name = json.name; - if ( json.color !== undefined && material.color !== undefined ) material.color.setHex( json.color ); - if ( json.roughness !== undefined ) material.roughness = json.roughness; - if ( json.metalness !== undefined ) material.metalness = json.metalness; - if ( json.sheen !== undefined ) material.sheen = json.sheen; - if ( json.sheenColor !== undefined ) material.sheenColor = new Color().setHex( json.sheenColor ); - if ( json.sheenRoughness !== undefined ) material.sheenRoughness = json.sheenRoughness; - if ( json.emissive !== undefined && material.emissive !== undefined ) material.emissive.setHex( json.emissive ); - if ( json.specular !== undefined && material.specular !== undefined ) material.specular.setHex( json.specular ); - if ( json.specularIntensity !== undefined ) material.specularIntensity = json.specularIntensity; - if ( json.specularColor !== undefined && material.specularColor !== undefined ) material.specularColor.setHex( json.specularColor ); - if ( json.shininess !== undefined ) material.shininess = json.shininess; - if ( json.clearcoat !== undefined ) material.clearcoat = json.clearcoat; - if ( json.clearcoatRoughness !== undefined ) material.clearcoatRoughness = json.clearcoatRoughness; - if ( json.dispersion !== undefined ) material.dispersion = json.dispersion; - if ( json.iridescence !== undefined ) material.iridescence = json.iridescence; - if ( json.iridescenceIOR !== undefined ) material.iridescenceIOR = json.iridescenceIOR; - if ( json.iridescenceThicknessRange !== undefined ) material.iridescenceThicknessRange = json.iridescenceThicknessRange; - if ( json.transmission !== undefined ) material.transmission = json.transmission; - if ( json.thickness !== undefined ) material.thickness = json.thickness; - if ( json.attenuationDistance !== undefined ) material.attenuationDistance = json.attenuationDistance; - if ( json.attenuationColor !== undefined && material.attenuationColor !== undefined ) material.attenuationColor.setHex( json.attenuationColor ); - if ( json.anisotropy !== undefined ) material.anisotropy = json.anisotropy; - if ( json.anisotropyRotation !== undefined ) material.anisotropyRotation = json.anisotropyRotation; - if ( json.fog !== undefined ) material.fog = json.fog; - if ( json.flatShading !== undefined ) material.flatShading = json.flatShading; - if ( json.blending !== undefined ) material.blending = json.blending; - if ( json.combine !== undefined ) material.combine = json.combine; - if ( json.side !== undefined ) material.side = json.side; - if ( json.shadowSide !== undefined ) material.shadowSide = json.shadowSide; - if ( json.opacity !== undefined ) material.opacity = json.opacity; - if ( json.transparent !== undefined ) material.transparent = json.transparent; - if ( json.alphaTest !== undefined ) material.alphaTest = json.alphaTest; - if ( json.alphaHash !== undefined ) material.alphaHash = json.alphaHash; - if ( json.depthFunc !== undefined ) material.depthFunc = json.depthFunc; - if ( json.depthTest !== undefined ) material.depthTest = json.depthTest; - if ( json.depthWrite !== undefined ) material.depthWrite = json.depthWrite; - if ( json.colorWrite !== undefined ) material.colorWrite = json.colorWrite; - if ( json.blendSrc !== undefined ) material.blendSrc = json.blendSrc; - if ( json.blendDst !== undefined ) material.blendDst = json.blendDst; - if ( json.blendEquation !== undefined ) material.blendEquation = json.blendEquation; - if ( json.blendSrcAlpha !== undefined ) material.blendSrcAlpha = json.blendSrcAlpha; - if ( json.blendDstAlpha !== undefined ) material.blendDstAlpha = json.blendDstAlpha; - if ( json.blendEquationAlpha !== undefined ) material.blendEquationAlpha = json.blendEquationAlpha; - if ( json.blendColor !== undefined && material.blendColor !== undefined ) material.blendColor.setHex( json.blendColor ); - if ( json.blendAlpha !== undefined ) material.blendAlpha = json.blendAlpha; - if ( json.stencilWriteMask !== undefined ) material.stencilWriteMask = json.stencilWriteMask; - if ( json.stencilFunc !== undefined ) material.stencilFunc = json.stencilFunc; - if ( json.stencilRef !== undefined ) material.stencilRef = json.stencilRef; - if ( json.stencilFuncMask !== undefined ) material.stencilFuncMask = json.stencilFuncMask; - if ( json.stencilFail !== undefined ) material.stencilFail = json.stencilFail; - if ( json.stencilZFail !== undefined ) material.stencilZFail = json.stencilZFail; - if ( json.stencilZPass !== undefined ) material.stencilZPass = json.stencilZPass; - if ( json.stencilWrite !== undefined ) material.stencilWrite = json.stencilWrite; - - if ( json.wireframe !== undefined ) material.wireframe = json.wireframe; - if ( json.wireframeLinewidth !== undefined ) material.wireframeLinewidth = json.wireframeLinewidth; - if ( json.wireframeLinecap !== undefined ) material.wireframeLinecap = json.wireframeLinecap; - if ( json.wireframeLinejoin !== undefined ) material.wireframeLinejoin = json.wireframeLinejoin; - - if ( json.rotation !== undefined ) material.rotation = json.rotation; - - if ( json.linewidth !== undefined ) material.linewidth = json.linewidth; - if ( json.dashSize !== undefined ) material.dashSize = json.dashSize; - if ( json.gapSize !== undefined ) material.gapSize = json.gapSize; - if ( json.scale !== undefined ) material.scale = json.scale; - - if ( json.polygonOffset !== undefined ) material.polygonOffset = json.polygonOffset; - if ( json.polygonOffsetFactor !== undefined ) material.polygonOffsetFactor = json.polygonOffsetFactor; - if ( json.polygonOffsetUnits !== undefined ) material.polygonOffsetUnits = json.polygonOffsetUnits; - - if ( json.dithering !== undefined ) material.dithering = json.dithering; - - if ( json.alphaToCoverage !== undefined ) material.alphaToCoverage = json.alphaToCoverage; - if ( json.premultipliedAlpha !== undefined ) material.premultipliedAlpha = json.premultipliedAlpha; - if ( json.forceSinglePass !== undefined ) material.forceSinglePass = json.forceSinglePass; - if ( json.allowOverride !== undefined ) material.allowOverride = json.allowOverride; - - if ( json.visible !== undefined ) material.visible = json.visible; - - if ( json.toneMapped !== undefined ) material.toneMapped = json.toneMapped; - - if ( json.userData !== undefined ) material.userData = json.userData; - - if ( json.vertexColors !== undefined ) { - - if ( typeof json.vertexColors === 'number' ) { - - material.vertexColors = json.vertexColors > 0; - - } else { - - material.vertexColors = json.vertexColors; - - } - - } - - // Shader Material - - if ( json.uniforms !== undefined ) { - - for ( const name in json.uniforms ) { - - const uniform = json.uniforms[ name ]; - - material.uniforms[ name ] = {}; - - switch ( uniform.type ) { - - case 't': - material.uniforms[ name ].value = getTexture( uniform.value ); - break; - - case 'c': - material.uniforms[ name ].value = new Color().setHex( uniform.value ); - break; - - case 'v2': - material.uniforms[ name ].value = new Vector2().fromArray( uniform.value ); - break; - - case 'v3': - material.uniforms[ name ].value = new Vector3().fromArray( uniform.value ); - break; - - case 'v4': - material.uniforms[ name ].value = new Vector4().fromArray( uniform.value ); - break; - - case 'm3': - material.uniforms[ name ].value = new Matrix3().fromArray( uniform.value ); - break; - - case 'm4': - material.uniforms[ name ].value = new Matrix4().fromArray( uniform.value ); - break; - - default: - material.uniforms[ name ].value = uniform.value; - - } - - } - - } - - if ( json.defines !== undefined ) material.defines = json.defines; - if ( json.vertexShader !== undefined ) material.vertexShader = json.vertexShader; - if ( json.fragmentShader !== undefined ) material.fragmentShader = json.fragmentShader; - if ( json.glslVersion !== undefined ) material.glslVersion = json.glslVersion; - - if ( json.extensions !== undefined ) { - - for ( const key in json.extensions ) { - - material.extensions[ key ] = json.extensions[ key ]; - - } - - } - - if ( json.lights !== undefined ) material.lights = json.lights; - if ( json.clipping !== undefined ) material.clipping = json.clipping; - - // for PointsMaterial - - if ( json.size !== undefined ) material.size = json.size; - if ( json.sizeAttenuation !== undefined ) material.sizeAttenuation = json.sizeAttenuation; - - // maps - - if ( json.map !== undefined ) material.map = getTexture( json.map ); - if ( json.matcap !== undefined ) material.matcap = getTexture( json.matcap ); - - if ( json.alphaMap !== undefined ) material.alphaMap = getTexture( json.alphaMap ); - - if ( json.bumpMap !== undefined ) material.bumpMap = getTexture( json.bumpMap ); - if ( json.bumpScale !== undefined ) material.bumpScale = json.bumpScale; - - if ( json.normalMap !== undefined ) material.normalMap = getTexture( json.normalMap ); - if ( json.normalMapType !== undefined ) material.normalMapType = json.normalMapType; - if ( json.normalScale !== undefined ) { - - let normalScale = json.normalScale; - - if ( Array.isArray( normalScale ) === false ) { - - // Blender exporter used to export a scalar. See #7459 - - normalScale = [ normalScale, normalScale ]; - - } - - material.normalScale = new Vector2().fromArray( normalScale ); - - } - - if ( json.displacementMap !== undefined ) material.displacementMap = getTexture( json.displacementMap ); - if ( json.displacementScale !== undefined ) material.displacementScale = json.displacementScale; - if ( json.displacementBias !== undefined ) material.displacementBias = json.displacementBias; - - if ( json.roughnessMap !== undefined ) material.roughnessMap = getTexture( json.roughnessMap ); - if ( json.metalnessMap !== undefined ) material.metalnessMap = getTexture( json.metalnessMap ); - - if ( json.emissiveMap !== undefined ) material.emissiveMap = getTexture( json.emissiveMap ); - if ( json.emissiveIntensity !== undefined ) material.emissiveIntensity = json.emissiveIntensity; - - if ( json.specularMap !== undefined ) material.specularMap = getTexture( json.specularMap ); - if ( json.specularIntensityMap !== undefined ) material.specularIntensityMap = getTexture( json.specularIntensityMap ); - if ( json.specularColorMap !== undefined ) material.specularColorMap = getTexture( json.specularColorMap ); - - if ( json.envMap !== undefined ) material.envMap = getTexture( json.envMap ); - if ( json.envMapRotation !== undefined ) material.envMapRotation.fromArray( json.envMapRotation ); - if ( json.envMapIntensity !== undefined ) material.envMapIntensity = json.envMapIntensity; - - if ( json.reflectivity !== undefined ) material.reflectivity = json.reflectivity; - if ( json.refractionRatio !== undefined ) material.refractionRatio = json.refractionRatio; - - if ( json.lightMap !== undefined ) material.lightMap = getTexture( json.lightMap ); - if ( json.lightMapIntensity !== undefined ) material.lightMapIntensity = json.lightMapIntensity; - - if ( json.aoMap !== undefined ) material.aoMap = getTexture( json.aoMap ); - if ( json.aoMapIntensity !== undefined ) material.aoMapIntensity = json.aoMapIntensity; - - if ( json.gradientMap !== undefined ) material.gradientMap = getTexture( json.gradientMap ); - - if ( json.clearcoatMap !== undefined ) material.clearcoatMap = getTexture( json.clearcoatMap ); - if ( json.clearcoatRoughnessMap !== undefined ) material.clearcoatRoughnessMap = getTexture( json.clearcoatRoughnessMap ); - if ( json.clearcoatNormalMap !== undefined ) material.clearcoatNormalMap = getTexture( json.clearcoatNormalMap ); - if ( json.clearcoatNormalScale !== undefined ) material.clearcoatNormalScale = new Vector2().fromArray( json.clearcoatNormalScale ); - - if ( json.iridescenceMap !== undefined ) material.iridescenceMap = getTexture( json.iridescenceMap ); - if ( json.iridescenceThicknessMap !== undefined ) material.iridescenceThicknessMap = getTexture( json.iridescenceThicknessMap ); - - if ( json.transmissionMap !== undefined ) material.transmissionMap = getTexture( json.transmissionMap ); - if ( json.thicknessMap !== undefined ) material.thicknessMap = getTexture( json.thicknessMap ); - - if ( json.anisotropyMap !== undefined ) material.anisotropyMap = getTexture( json.anisotropyMap ); - - if ( json.sheenColorMap !== undefined ) material.sheenColorMap = getTexture( json.sheenColorMap ); - if ( json.sheenRoughnessMap !== undefined ) material.sheenRoughnessMap = getTexture( json.sheenRoughnessMap ); + material.fromJSON( json, this.textures ); return material; @@ -427,10 +167,40 @@ class MaterialLoader extends Loader { MeshMatcapMaterial, LineDashedMaterial, LineBasicMaterial, - Material + Material, + ... _customMaterials }; - return new materialLib[ type ](); + const MaterialType = materialLib[ type ]; + + let materialInstance; + + if ( MaterialType === undefined ) { + + warnOnce( `MaterialLoader: Unknown material type "${ type }". Use .registerMaterial() before starting the deserialization process.` ); + materialInstance = new Material(); + + } else { + + materialInstance = new MaterialType(); + + } + + return materialInstance; + + } + + /** + * Registers the given material at the internal + * material library. + * + * @static + * @param {string} type - The material type. + * @param {Material.constructor} materialClass - The material class. + */ + static registerMaterial( type, materialClass ) { + + _customMaterials[ type ] = materialClass; } diff --git a/src/materials/Material.js b/src/materials/Material.js index 7ba34cd4777865..c2c43804562f10 100644 --- a/src/materials/Material.js +++ b/src/materials/Material.js @@ -3,6 +3,7 @@ import { EventDispatcher } from '../core/EventDispatcher.js'; import { FrontSide, NormalBlending, LessEqualDepth, AddEquation, OneMinusSrcAlphaFactor, SrcAlphaFactor, AlwaysStencilFunc, KeepStencilOp } from '../constants.js'; import { generateUUID } from '../math/MathUtils.js'; import { warn } from '../utils.js'; +import { Vector2 } from '../math/Vector2.js'; let _materialId = 0; @@ -885,6 +886,196 @@ class Material extends EventDispatcher { } + /** + * Deserializes the material from the given JSON. + * + * @param {Object} json - The JSON holding the serialized material. + * @param {Object} textures - A dictionary holding textures referenced by the material. + * @return {Material} A reference to this material. + */ + fromJSON( json, textures ) { + + if ( json.uuid !== undefined ) this.uuid = json.uuid; + if ( json.name !== undefined ) this.name = json.name; + if ( json.color !== undefined && this.color !== undefined ) this.color.setHex( json.color ); + if ( json.roughness !== undefined ) this.roughness = json.roughness; + if ( json.metalness !== undefined ) this.metalness = json.metalness; + if ( json.sheen !== undefined ) this.sheen = json.sheen; + if ( json.sheenColor !== undefined ) this.sheenColor = new Color().setHex( json.sheenColor ); + if ( json.sheenRoughness !== undefined ) this.sheenRoughness = json.sheenRoughness; + if ( json.emissive !== undefined && this.emissive !== undefined ) this.emissive.setHex( json.emissive ); + if ( json.specular !== undefined && this.specular !== undefined ) this.specular.setHex( json.specular ); + if ( json.specularIntensity !== undefined ) this.specularIntensity = json.specularIntensity; + if ( json.specularColor !== undefined && this.specularColor !== undefined ) this.specularColor.setHex( json.specularColor ); + if ( json.shininess !== undefined ) this.shininess = json.shininess; + if ( json.clearcoat !== undefined ) this.clearcoat = json.clearcoat; + if ( json.clearcoatRoughness !== undefined ) this.clearcoatRoughness = json.clearcoatRoughness; + if ( json.dispersion !== undefined ) this.dispersion = json.dispersion; + if ( json.iridescence !== undefined ) this.iridescence = json.iridescence; + if ( json.iridescenceIOR !== undefined ) this.iridescenceIOR = json.iridescenceIOR; + if ( json.iridescenceThicknessRange !== undefined ) this.iridescenceThicknessRange = json.iridescenceThicknessRange; + if ( json.transmission !== undefined ) this.transmission = json.transmission; + if ( json.thickness !== undefined ) this.thickness = json.thickness; + if ( json.attenuationDistance !== undefined ) this.attenuationDistance = json.attenuationDistance; + if ( json.attenuationColor !== undefined && this.attenuationColor !== undefined ) this.attenuationColor.setHex( json.attenuationColor ); + if ( json.anisotropy !== undefined ) this.anisotropy = json.anisotropy; + if ( json.anisotropyRotation !== undefined ) this.anisotropyRotation = json.anisotropyRotation; + if ( json.fog !== undefined ) this.fog = json.fog; + if ( json.flatShading !== undefined ) this.flatShading = json.flatShading; + if ( json.blending !== undefined ) this.blending = json.blending; + if ( json.combine !== undefined ) this.combine = json.combine; + if ( json.side !== undefined ) this.side = json.side; + if ( json.shadowSide !== undefined ) this.shadowSide = json.shadowSide; + if ( json.opacity !== undefined ) this.opacity = json.opacity; + if ( json.transparent !== undefined ) this.transparent = json.transparent; + if ( json.alphaTest !== undefined ) this.alphaTest = json.alphaTest; + if ( json.alphaHash !== undefined ) this.alphaHash = json.alphaHash; + if ( json.depthFunc !== undefined ) this.depthFunc = json.depthFunc; + if ( json.depthTest !== undefined ) this.depthTest = json.depthTest; + if ( json.depthWrite !== undefined ) this.depthWrite = json.depthWrite; + if ( json.colorWrite !== undefined ) this.colorWrite = json.colorWrite; + if ( json.blendSrc !== undefined ) this.blendSrc = json.blendSrc; + if ( json.blendDst !== undefined ) this.blendDst = json.blendDst; + if ( json.blendEquation !== undefined ) this.blendEquation = json.blendEquation; + if ( json.blendSrcAlpha !== undefined ) this.blendSrcAlpha = json.blendSrcAlpha; + if ( json.blendDstAlpha !== undefined ) this.blendDstAlpha = json.blendDstAlpha; + if ( json.blendEquationAlpha !== undefined ) this.blendEquationAlpha = json.blendEquationAlpha; + if ( json.blendColor !== undefined && this.blendColor !== undefined ) this.blendColor.setHex( json.blendColor ); + if ( json.blendAlpha !== undefined ) this.blendAlpha = json.blendAlpha; + if ( json.stencilWriteMask !== undefined ) this.stencilWriteMask = json.stencilWriteMask; + if ( json.stencilFunc !== undefined ) this.stencilFunc = json.stencilFunc; + if ( json.stencilRef !== undefined ) this.stencilRef = json.stencilRef; + if ( json.stencilFuncMask !== undefined ) this.stencilFuncMask = json.stencilFuncMask; + if ( json.stencilFail !== undefined ) this.stencilFail = json.stencilFail; + if ( json.stencilZFail !== undefined ) this.stencilZFail = json.stencilZFail; + if ( json.stencilZPass !== undefined ) this.stencilZPass = json.stencilZPass; + if ( json.stencilWrite !== undefined ) this.stencilWrite = json.stencilWrite; + + if ( json.wireframe !== undefined ) this.wireframe = json.wireframe; + if ( json.wireframeLinewidth !== undefined ) this.wireframeLinewidth = json.wireframeLinewidth; + if ( json.wireframeLinecap !== undefined ) this.wireframeLinecap = json.wireframeLinecap; + if ( json.wireframeLinejoin !== undefined ) this.wireframeLinejoin = json.wireframeLinejoin; + + if ( json.rotation !== undefined ) this.rotation = json.rotation; + + if ( json.linewidth !== undefined ) this.linewidth = json.linewidth; + if ( json.dashSize !== undefined ) this.dashSize = json.dashSize; + if ( json.gapSize !== undefined ) this.gapSize = json.gapSize; + if ( json.scale !== undefined ) this.scale = json.scale; + + if ( json.polygonOffset !== undefined ) this.polygonOffset = json.polygonOffset; + if ( json.polygonOffsetFactor !== undefined ) this.polygonOffsetFactor = json.polygonOffsetFactor; + if ( json.polygonOffsetUnits !== undefined ) this.polygonOffsetUnits = json.polygonOffsetUnits; + + if ( json.dithering !== undefined ) this.dithering = json.dithering; + + if ( json.alphaToCoverage !== undefined ) this.alphaToCoverage = json.alphaToCoverage; + if ( json.premultipliedAlpha !== undefined ) this.premultipliedAlpha = json.premultipliedAlpha; + if ( json.forceSinglePass !== undefined ) this.forceSinglePass = json.forceSinglePass; + if ( json.allowOverride !== undefined ) this.allowOverride = json.allowOverride; + + if ( json.visible !== undefined ) this.visible = json.visible; + + if ( json.toneMapped !== undefined ) this.toneMapped = json.toneMapped; + + if ( json.userData !== undefined ) this.userData = json.userData; + + if ( json.vertexColors !== undefined ) { + + if ( typeof json.vertexColors === 'number' ) { + + this.vertexColors = json.vertexColors > 0; + + } else { + + this.vertexColors = json.vertexColors; + + } + + } + + // for PointsMaterial + + if ( json.size !== undefined ) this.size = json.size; + if ( json.sizeAttenuation !== undefined ) this.sizeAttenuation = json.sizeAttenuation; + + // maps + + if ( json.map !== undefined ) this.map = textures[ json.map ] || null; + if ( json.matcap !== undefined ) this.matcap = textures[ json.matcap ] || null; + + if ( json.alphaMap !== undefined ) this.alphaMap = textures[ json.alphaMap ] || null; + + if ( json.bumpMap !== undefined ) this.bumpMap = textures[ json.bumpMap ] || null; + if ( json.bumpScale !== undefined ) this.bumpScale = json.bumpScale; + + if ( json.normalMap !== undefined ) this.normalMap = textures[ json.normalMap ] || null; + if ( json.normalMapType !== undefined ) this.normalMapType = json.normalMapType; + if ( json.normalScale !== undefined ) { + + let normalScale = json.normalScale; + + if ( Array.isArray( normalScale ) === false ) { + + // Blender exporter used to export a scalar. See #7459 + + normalScale = [ normalScale, normalScale ]; + + } + + this.normalScale = new Vector2().fromArray( normalScale ); + + } + + if ( json.displacementMap !== undefined ) this.displacementMap = textures[ json.displacementMap ] || null; + if ( json.displacementScale !== undefined ) this.displacementScale = json.displacementScale; + if ( json.displacementBias !== undefined ) this.displacementBias = json.displacementBias; + + if ( json.roughnessMap !== undefined ) this.roughnessMap = textures[ json.roughnessMap ] || null; + if ( json.metalnessMap !== undefined ) this.metalnessMap = textures[ json.metalnessMap ] || null; + + if ( json.emissiveMap !== undefined ) this.emissiveMap = textures[ json.emissiveMap ] || null; + if ( json.emissiveIntensity !== undefined ) this.emissiveIntensity = json.emissiveIntensity; + + if ( json.specularMap !== undefined ) this.specularMap = textures[ json.specularMap ] || null; + if ( json.specularIntensityMap !== undefined ) this.specularIntensityMap = textures[ json.specularIntensityMap ] || null; + if ( json.specularColorMap !== undefined ) this.specularColorMap = textures[ json.specularColorMap ] || null; + + if ( json.envMap !== undefined ) this.envMap = textures[ json.envMap ] || null; + if ( json.envMapRotation !== undefined ) this.envMapRotation.fromArray( json.envMapRotation ); + if ( json.envMapIntensity !== undefined ) this.envMapIntensity = json.envMapIntensity; + + if ( json.reflectivity !== undefined ) this.reflectivity = json.reflectivity; + if ( json.refractionRatio !== undefined ) this.refractionRatio = json.refractionRatio; + + if ( json.lightMap !== undefined ) this.lightMap = textures[ json.lightMap ] || null; + if ( json.lightMapIntensity !== undefined ) this.lightMapIntensity = json.lightMapIntensity; + + if ( json.aoMap !== undefined ) this.aoMap = textures[ json.aoMap ] || null; + if ( json.aoMapIntensity !== undefined ) this.aoMapIntensity = json.aoMapIntensity; + + if ( json.gradientMap !== undefined ) this.gradientMap = textures[ json.gradientMap ] || null; + + if ( json.clearcoatMap !== undefined ) this.clearcoatMap = textures[ json.clearcoatMap ] || null; + if ( json.clearcoatRoughnessMap !== undefined ) this.clearcoatRoughnessMap = textures[ json.clearcoatRoughnessMap ] || null; + if ( json.clearcoatNormalMap !== undefined ) this.clearcoatNormalMap = textures[ json.clearcoatNormalMap ] || null; + if ( json.clearcoatNormalScale !== undefined ) this.clearcoatNormalScale = new Vector2().fromArray( json.clearcoatNormalScale ); + + if ( json.iridescenceMap !== undefined ) this.iridescenceMap = textures[ json.iridescenceMap ] || null; + if ( json.iridescenceThicknessMap !== undefined ) this.iridescenceThicknessMap = textures[ json.iridescenceThicknessMap ] || null; + + if ( json.transmissionMap !== undefined ) this.transmissionMap = textures[ json.transmissionMap ] || null; + if ( json.thicknessMap !== undefined ) this.thicknessMap = textures[ json.thicknessMap ] || null; + + if ( json.anisotropyMap !== undefined ) this.anisotropyMap = textures[ json.anisotropyMap ] || null; + + if ( json.sheenColorMap !== undefined ) this.sheenColorMap = textures[ json.sheenColorMap ] || null; + if ( json.sheenRoughnessMap !== undefined ) this.sheenRoughnessMap = textures[ json.sheenRoughnessMap ] || null; + + return this; + + } + /** * Returns a new material with copied values from this instance. * diff --git a/src/materials/ShaderMaterial.js b/src/materials/ShaderMaterial.js index 4c98615e76c400..f557ef1aa2250b 100644 --- a/src/materials/ShaderMaterial.js +++ b/src/materials/ShaderMaterial.js @@ -1,5 +1,11 @@ import { Material } from './Material.js'; import { cloneUniforms, cloneUniformsGroups } from '../renderers/shaders/UniformsUtils.js'; +import { Color } from '../math/Color.js'; +import { Vector2 } from '../math/Vector2.js'; +import { Vector3 } from '../math/Vector3.js'; +import { Vector4 } from '../math/Vector4.js'; +import { Matrix3 } from '../math/Matrix3.js'; +import { Matrix4 } from '../math/Matrix4.js'; import default_vertex from '../renderers/shaders/ShaderChunk/default_vertex.glsl.js'; import default_fragment from '../renderers/shaders/ShaderChunk/default_fragment.glsl.js'; @@ -397,6 +403,86 @@ class ShaderMaterial extends Material { } + /** + * Deserializes the material from the given JSON. + * + * @param {Object} json - The JSON holding the serialized material. + * @param {Object} textures - A dictionary holding textures referenced by the material. + * @return {ShaderMaterial} A reference to this material. + */ + fromJSON( json, textures ) { + + super.fromJSON( json, textures ); + + if ( json.uniforms !== undefined ) { + + for ( const name in json.uniforms ) { + + const uniform = json.uniforms[ name ]; + + this.uniforms[ name ] = {}; + + switch ( uniform.type ) { + + case 't': + this.uniforms[ name ].value = textures[ uniform.value ] || null; + break; + + case 'c': + this.uniforms[ name ].value = new Color().setHex( uniform.value ); + break; + + case 'v2': + this.uniforms[ name ].value = new Vector2().fromArray( uniform.value ); + break; + + case 'v3': + this.uniforms[ name ].value = new Vector3().fromArray( uniform.value ); + break; + + case 'v4': + this.uniforms[ name ].value = new Vector4().fromArray( uniform.value ); + break; + + case 'm3': + this.uniforms[ name ].value = new Matrix3().fromArray( uniform.value ); + break; + + case 'm4': + this.uniforms[ name ].value = new Matrix4().fromArray( uniform.value ); + break; + + default: + this.uniforms[ name ].value = uniform.value; + + } + + } + + } + + if ( json.defines !== undefined ) this.defines = json.defines; + if ( json.vertexShader !== undefined ) this.vertexShader = json.vertexShader; + if ( json.fragmentShader !== undefined ) this.fragmentShader = json.fragmentShader; + if ( json.glslVersion !== undefined ) this.glslVersion = json.glslVersion; + + if ( json.extensions !== undefined ) { + + for ( const key in json.extensions ) { + + this.extensions[ key ] = json.extensions[ key ]; + + } + + } + + if ( json.lights !== undefined ) this.lights = json.lights; + if ( json.clipping !== undefined ) this.clipping = json.clipping; + + return this; + + } + } /** From 43f6f914505a9a0ab4711d8073c44ad982e5fb7e Mon Sep 17 00:00:00 2001 From: arpu Date: Thu, 4 Jun 2026 10:02:57 +0200 Subject: [PATCH 3/7] KTX2Loader: Disable etc1 on Chrome Mesa (Linux). (#33697) Co-authored-by: Don McCurdy <1848368+donmccurdy@users.noreply.github.com> Co-authored-by: mrdoob Co-authored-by: Michael Herzog --- examples/jsm/loaders/KTX2Loader.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/examples/jsm/loaders/KTX2Loader.js b/examples/jsm/loaders/KTX2Loader.js index 6032c6bdda1992..8290fec1026319 100644 --- a/examples/jsm/loaders/KTX2Loader.js +++ b/examples/jsm/loaders/KTX2Loader.js @@ -257,16 +257,17 @@ class KTX2Loader extends Loader { if ( typeof navigator !== 'undefined' && typeof navigator.platform !== 'undefined' && typeof navigator.userAgent !== 'undefined' && - navigator.platform.indexOf( 'Linux' ) >= 0 && navigator.userAgent.indexOf( 'Firefox' ) >= 0 && + navigator.platform.indexOf( 'Linux' ) >= 0 && navigator.userAgent.indexOf( 'Android' ) < 0 && this.workerConfig.astcSupported && this.workerConfig.etc2Supported && this.workerConfig.bptcSupported && this.workerConfig.dxtSupported ) { - // On Linux, Mesa drivers for AMD and Intel GPUs expose ETC2 and ASTC even though the hardware doesn't support these. + // On Linux, Mesa drivers for AMD and Intel GPUs expose ETC1,ETC2 and ASTC even though the hardware doesn't support these. // Using these extensions will result in expensive software decompression on the main thread inside the driver, causing performance issues. - // When using ANGLE (e.g. via Chrome), these extensions are not exposed except for some specific Intel GPU models - however, Firefox doesn't perform this filtering. + // In general, browsers should not expose extensions for emulated formats, but Chrome and Firefox currently do so on Linux. // Since a granular filter is a little too fragile and we can transcode into other GPU formats, disable formats that are likely to be emulated. this.workerConfig.astcSupported = false; + this.workerConfig.etc1Supported = false; this.workerConfig.etc2Supported = false; } From 70410f4f0854494e5cfe6c3a274b4d6236d30a68 Mon Sep 17 00:00:00 2001 From: Garrett Johnson Date: Thu, 4 Jun 2026 17:21:42 +0900 Subject: [PATCH 4/7] DRACOLoader: Add exported urls for GLTF decoder (#33691) --- examples/jsm/loaders/DRACOLoader.js | 66 ++++++++++++++----------- examples/webgl_animation_keyframes.html | 4 +- 2 files changed, 40 insertions(+), 30 deletions(-) diff --git a/examples/jsm/loaders/DRACOLoader.js b/examples/jsm/loaders/DRACOLoader.js index 265bb6d1ffd580..413798f963908b 100644 --- a/examples/jsm/loaders/DRACOLoader.js +++ b/examples/jsm/loaders/DRACOLoader.js @@ -8,7 +8,8 @@ import { LinearSRGBColorSpace, SRGBColorSpace, InterleavedBuffer, - InterleavedBufferAttribute + InterleavedBufferAttribute, + LoaderUtils } from 'three'; const _taskCache = new WeakMap(); @@ -17,6 +18,11 @@ const WASM_BIN_URL = new URL( '../libs/draco/draco_decoder.wasm', import.meta.ur const WASM_JS_URL = new URL( '../libs/draco/draco_wasm_wrapper.js', import.meta.url ).toString(); const JS_URL = new URL( '../libs/draco/draco_decoder.js', import.meta.url ).toString(); +const DRACO_GLTF_CONFIG = { + js: new URL( '../libs/draco/gltf/draco_wasm_wrapper.js', import.meta.url ).toString(), + wasm: new URL( '../libs/draco/gltf/draco_decoder.wasm', import.meta.url ).toString(), +}; + /** * A loader for the Draco format. * @@ -38,12 +44,10 @@ const JS_URL = new URL( '../libs/draco/draco_decoder.js', import.meta.url ).toSt * * ```js * const loader = new DRACOLoader(); - * loader.setDecoderPath( '/examples/jsm/libs/draco/' ); - * - * const geometry = await dracoLoader.loadAsync( 'models/draco/bunny.drc' ); + * const geometry = await loader.loadAsync( 'models/draco/bunny.drc' ); * geometry.computeVertexNormals(); // optional * - * dracoLoader.dispose(); + * loader.dispose(); * ``` * * @augments Loader @@ -60,7 +64,11 @@ class DRACOLoader extends Loader { super( manager ); - this.decoderPath = ''; + this.decoderPaths = { + js: WASM_JS_URL, + wasm: WASM_BIN_URL, + dep_js: JS_URL, + }; this.decoderConfig = {}; this.decoderBinary = null; this.decoderPending = null; @@ -88,12 +96,25 @@ class DRACOLoader extends Loader { /** * Provides configuration for the decoder libraries. Configuration cannot be changed after decoding begins. * - * @param {string} path - The decoder path. + * @param {string|{js:string, wasm:string}} path - The decoder path, or a config object with explicit URLs for each decoder file. * @return {DRACOLoader} A reference to this loader. */ setDecoderPath( path ) { - this.decoderPath = path; + const { decoderPaths } = this; + if ( typeof path === 'object' ) { + + decoderPaths.js = path.js; + decoderPaths.wasm = path.wasm; + decoderPaths.dep_js = null; + + } else { + + decoderPaths.js = LoaderUtils.resolveURL( 'draco_wasm_wrapper.js', path ); + decoderPaths.wasm = LoaderUtils.resolveURL( 'draco_decoder.wasm', path ); + decoderPaths.dep_js = LoaderUtils.resolveURL( 'draco_decoder.js', path ); + + } return this; @@ -337,7 +358,6 @@ class DRACOLoader extends Loader { _loadLibrary( url, responseType ) { const loader = new FileLoader( this.manager ); - loader.setPath( this.decoderPath ); loader.setResponseType( responseType ); loader.setWithCredentials( this.withCredentials ); @@ -364,31 +384,21 @@ class DRACOLoader extends Loader { const useJS = typeof WebAssembly !== 'object' || this.decoderConfig.type === 'js'; const librariesPending = []; - if ( this.decoderPath === '' ) { + const { decoderPaths } = this; + if ( useJS ) { - if ( useJS ) { + if ( decoderPaths.dep_js === null ) { - librariesPending.push( this._loadLibrary( JS_URL, 'text' ) ); - - } else { - - librariesPending.push( this._loadLibrary( WASM_JS_URL, 'text' ) ); - librariesPending.push( this._loadLibrary( WASM_BIN_URL, 'arraybuffer' ) ); + throw new Error( 'THREE.DRACOLoader: WebAssembly is required when using a custom decoder paths.' ); } - } else { - - if ( useJS ) { - - librariesPending.push( this._loadLibrary( 'draco_decoder.js', 'text' ) ); + librariesPending.push( this._loadLibrary( decoderPaths.dep_js, 'text' ) ); - } else { - - librariesPending.push( this._loadLibrary( 'draco_wasm_wrapper.js', 'text' ) ); - librariesPending.push( this._loadLibrary( 'draco_decoder.wasm', 'arraybuffer' ) ); + } else { - } + librariesPending.push( this._loadLibrary( decoderPaths.js, 'text' ) ); + librariesPending.push( this._loadLibrary( decoderPaths.wasm, 'arraybuffer' ) ); } @@ -759,4 +769,4 @@ function DRACOWorker() { } -export { DRACOLoader }; +export { DRACOLoader, DRACO_GLTF_CONFIG }; diff --git a/examples/webgl_animation_keyframes.html b/examples/webgl_animation_keyframes.html index ba6a20ef6306e2..e72348813bf140 100644 --- a/examples/webgl_animation_keyframes.html +++ b/examples/webgl_animation_keyframes.html @@ -50,7 +50,7 @@ import { Sky } from 'three/addons/objects/Sky.js'; import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js'; - import { DRACOLoader } from 'three/addons/loaders/DRACOLoader.js'; + import { DRACOLoader, DRACO_GLTF_CONFIG } from 'three/addons/loaders/DRACOLoader.js'; let mixer; @@ -95,7 +95,7 @@ controls.update(); const dracoLoader = new DRACOLoader(); - dracoLoader.setDecoderPath( 'jsm/libs/draco/gltf/' ); + dracoLoader.setDecoderPath( DRACO_GLTF_CONFIG ); const loader = new GLTFLoader(); loader.setDRACOLoader( dracoLoader ); From f94596bfd7eee7044bef209b19e8c16b9b18003b Mon Sep 17 00:00:00 2001 From: Michael Herzog Date: Thu, 4 Jun 2026 10:30:57 +0200 Subject: [PATCH 5/7] Editor: Fix collision detection in arkanoid demo. (#33718) --- editor/examples/arkanoid.app.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/editor/examples/arkanoid.app.json b/editor/examples/arkanoid.app.json index b998d1b156f4b7..1ad5209125d535 100644 --- a/editor/examples/arkanoid.app.json +++ b/editor/examples/arkanoid.app.json @@ -270,7 +270,7 @@ "31517222-A9A7-4EAF-B5F6-60751C0BABA3": [ { "name": "Game Logic", - "source": "var ball = this.getObjectByName( 'Ball' );\n\nvar direction = new THREE.Vector3();\ndirection.x = Math.random() - 0.5;\ndirection.z = - 0.5;\ndirection.normalize();\n\nvar speed = new THREE.Vector3();\n\n//\n\nvar group = new THREE.Group();\nthis.add( group );\n\nvar paddle = this.getObjectByName( 'Paddle' );\npaddle.material.visible = false;\ngroup.add( paddle );\n\nvar brick = this.getObjectByName( 'Brick' );\n\nfor ( var j = 0; j < 8; j ++ ) {\n\n\tvar material = new THREE.MeshPhongMaterial( { color: Math.random() * 0xffffff } );\n\n\tfor ( var i = 0; i < 12; i ++ ) {\n\t\t\n\t\tvar object = brick.clone();\n\t\tobject.position.x = i * 2.2 - 12;\n\t\tobject.position.z = j * 1.4 - 12;\n\t\tgroup.add( object );\n\n\t\tvar cylinder = object.getObjectByName( 'Cylinder' );\n\t\tcylinder.material = material;\n\n\t}\n\t\n}\n\nbrick.visible = false;\nbrick.material.visible = false;\n\n//\n\nvar raycaster = new THREE.Raycaster();\n\nfunction update( event ) {\n\t\n\tif ( ball.position.x < - 15 || ball.position.x > 15 ) direction.x = - direction.x;\n\tif ( ball.position.z < - 20 || ball.position.z > 20 ) direction.z = - direction.z;\n\n\tball.position.x = Math.max( - 15, Math.min( 15, ball.position.x ) );\n\tball.position.z = Math.max( - 20, Math.min( 20, ball.position.z ) );\n\t\t\n\traycaster.set( ball.position, direction );\n\t\n\tvar intersections = raycaster.intersectObjects( group.children );\n\t\n\tif ( intersections.length > 0 ) {\n\t\n\t\tvar intersection = intersections[ 0 ];\n\t\t\n\t\tif ( intersection.distance < 0.5 ) {\n\t\t\t\n\t\t\tif ( intersection.object !== paddle ) {\n\n\t\t\t\tgroup.remove( intersection.object );\n\t\t\t\t\n\t\t\t}\n\t\t\t\n\t\t\tdirection.reflect( intersection.face.normal );\n\t\t\t\n\t\t}\n\t\t\n\t}\n\n\tball.position.add( speed.copy( direction ).multiplyScalar( event.delta / 40 ) );\n\t\n}" + "source": "var ball = this.getObjectByName( 'Ball' );\n\nvar direction = new THREE.Vector3();\ndirection.x = Math.random() - 0.5;\ndirection.z = - 0.5;\ndirection.normalize();\n\nvar speed = new THREE.Vector3();\n\n//\n\nvar group = new THREE.Group();\nthis.add( group );\n\nvar paddle = this.getObjectByName( 'Paddle' );\npaddle.material.visible = false;\ngroup.add( paddle );\n\nvar brick = this.getObjectByName( 'Brick' );\n\nfor ( var j = 0; j < 8; j ++ ) {\n\n\tvar material = new THREE.MeshPhongMaterial( { color: Math.random() * 0xffffff } );\n\n\tfor ( var i = 0; i < 12; i ++ ) {\n\t\t\n\t\tvar object = brick.clone();\n\t\tobject.position.x = i * 2.2 - 12;\n\t\tobject.position.z = j * 1.4 - 12;\n\t\tgroup.add( object );\n\n\t\tvar cylinder = object.getObjectByName( 'Cylinder' );\n\t\tcylinder.material = material;\n\n\t}\n\t\n}\n\nbrick.visible = false;\nbrick.material.visible = false;\n\n//\n\nvar raycaster = new THREE.Raycaster();\n\nfunction update( event ) {\n\t\n\tif ( ball.position.x < - 15 || ball.position.x > 15 ) direction.x = - direction.x;\n\tif ( ball.position.z < - 20 || ball.position.z > 20 ) direction.z = - direction.z;\n\n\tball.position.x = Math.max( - 15, Math.min( 15, ball.position.x ) );\n\tball.position.z = Math.max( - 20, Math.min( 20, ball.position.z ) );\n\t\t\n\tvar step = event.delta / 40;\n\t\n\traycaster.set( ball.position, direction );\n\traycaster.far = step + 0.5;\n\t\n\tvar intersections = raycaster.intersectObjects( group.children );\n\t\n\tif ( intersections.length > 0 ) {\n\t\n\t\tvar intersection = intersections[ 0 ];\n\t\t\n\t\tif ( intersection.object !== paddle ) {\n\n\t\t\tgroup.remove( intersection.object );\n\t\t\t\n\t\t}\n\t\t\n\t\tdirection.reflect( intersection.face.normal );\n\t\t\n\t}\n\n\tball.position.add( speed.copy( direction ).multiplyScalar( step ) );\n\t\n}" }] } } From 716b6448c37c77de9a0ec6a7f0368d34eb7b0c48 Mon Sep 17 00:00:00 2001 From: Michael Herzog Date: Thu, 4 Jun 2026 10:49:30 +0200 Subject: [PATCH 6/7] Update arkanoid.app.json Fix script with correct version (PR #33718 used an outdated one). --- editor/examples/arkanoid.app.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/editor/examples/arkanoid.app.json b/editor/examples/arkanoid.app.json index 1ad5209125d535..ae3c3f31acf643 100644 --- a/editor/examples/arkanoid.app.json +++ b/editor/examples/arkanoid.app.json @@ -270,7 +270,7 @@ "31517222-A9A7-4EAF-B5F6-60751C0BABA3": [ { "name": "Game Logic", - "source": "var ball = this.getObjectByName( 'Ball' );\n\nvar direction = new THREE.Vector3();\ndirection.x = Math.random() - 0.5;\ndirection.z = - 0.5;\ndirection.normalize();\n\nvar speed = new THREE.Vector3();\n\n//\n\nvar group = new THREE.Group();\nthis.add( group );\n\nvar paddle = this.getObjectByName( 'Paddle' );\npaddle.material.visible = false;\ngroup.add( paddle );\n\nvar brick = this.getObjectByName( 'Brick' );\n\nfor ( var j = 0; j < 8; j ++ ) {\n\n\tvar material = new THREE.MeshPhongMaterial( { color: Math.random() * 0xffffff } );\n\n\tfor ( var i = 0; i < 12; i ++ ) {\n\t\t\n\t\tvar object = brick.clone();\n\t\tobject.position.x = i * 2.2 - 12;\n\t\tobject.position.z = j * 1.4 - 12;\n\t\tgroup.add( object );\n\n\t\tvar cylinder = object.getObjectByName( 'Cylinder' );\n\t\tcylinder.material = material;\n\n\t}\n\t\n}\n\nbrick.visible = false;\nbrick.material.visible = false;\n\n//\n\nvar raycaster = new THREE.Raycaster();\n\nfunction update( event ) {\n\t\n\tif ( ball.position.x < - 15 || ball.position.x > 15 ) direction.x = - direction.x;\n\tif ( ball.position.z < - 20 || ball.position.z > 20 ) direction.z = - direction.z;\n\n\tball.position.x = Math.max( - 15, Math.min( 15, ball.position.x ) );\n\tball.position.z = Math.max( - 20, Math.min( 20, ball.position.z ) );\n\t\t\n\tvar step = event.delta / 40;\n\t\n\traycaster.set( ball.position, direction );\n\traycaster.far = step + 0.5;\n\t\n\tvar intersections = raycaster.intersectObjects( group.children );\n\t\n\tif ( intersections.length > 0 ) {\n\t\n\t\tvar intersection = intersections[ 0 ];\n\t\t\n\t\tif ( intersection.object !== paddle ) {\n\n\t\t\tgroup.remove( intersection.object );\n\t\t\t\n\t\t}\n\t\t\n\t\tdirection.reflect( intersection.face.normal );\n\t\t\n\t}\n\n\tball.position.add( speed.copy( direction ).multiplyScalar( step ) );\n\t\n}" + "source": "var ball = this.getObjectByName( 'Ball' );\n\nvar direction = new THREE.Vector3();\ndirection.x = Math.random() - 0.5;\ndirection.z = - 0.5;\ndirection.normalize();\n\n//\n\nvar group = new THREE.Group();\nthis.add( group );\n\nvar paddle = this.getObjectByName( 'Paddle' );\npaddle.material.visible = false;\ngroup.add( paddle );\n\nvar brick = this.getObjectByName( 'Brick' );\n\nfor ( var j = 0; j < 8; j ++ ) {\n\n\tvar material = new THREE.MeshPhongMaterial( { color: Math.random() * 0xffffff } );\n\n\tfor ( var i = 0; i < 12; i ++ ) {\n\t\t\n\t\tvar object = brick.clone();\n\t\tobject.position.x = i * 2.2 - 12;\n\t\tobject.position.z = j * 1.4 - 12;\n\t\tgroup.add( object );\n\n\t\tvar cylinder = object.getObjectByName( 'Cylinder' );\n\t\tcylinder.material = material;\n\n\t}\n\t\n}\n\nbrick.visible = false;\nbrick.material.visible = false;\n\n//\n\nvar radius = 0.5;\n\nvar box = new THREE.Box3();\nvar closest = new THREE.Vector3();\nvar normal = new THREE.Vector3();\n\nfunction collide( object ) {\n\n\tbox.setFromObject( object );\n\tbox.clampPoint( ball.position, closest );\n\n\tif ( closest.distanceToSquared( ball.position ) >= radius * radius ) return false;\n\n\tif ( closest.equals( ball.position ) ) {\n\n\t\tnormal.copy( direction ).negate();\n\n\t} else {\n\n\t\tnormal.subVectors( ball.position, closest ).setY( 0 ).normalize();\n\n\t}\n\n\tdirection.reflect( normal );\n\n\tbox.clampPoint( ball.position, closest );\n\tball.position.copy( closest ).addScaledVector( normal, radius );\n\n\treturn true;\n\n}\n\nfunction update( event ) {\n\n\tvar distance = event.delta / 40;\n\tvar steps = Math.max( 1, Math.ceil( distance / radius ) );\n\tvar delta = distance / steps;\n\n\tfor ( var s = 0; s < steps; s ++ ) {\n\n\t\tball.position.addScaledVector( direction, delta );\n\n\t\tif ( ball.position.x < - 15 || ball.position.x > 15 ) {\n\n\t\t\tdirection.x = - direction.x;\n\t\t\tball.position.x = Math.max( - 15, Math.min( 15, ball.position.x ) );\n\n\t\t}\n\n\t\tif ( ball.position.z < - 20 || ball.position.z > 20 ) {\n\n\t\t\tdirection.z = - direction.z;\n\t\t\tball.position.z = Math.max( - 20, Math.min( 20, ball.position.z ) );\n\n\t\t}\n\n\t\tfor ( var i = group.children.length - 1; i >= 0; i -- ) {\n\n\t\t\tvar object = group.children[ i ];\n\n\t\t\tif ( collide( object ) ) {\n\n\t\t\t\tif ( object !== paddle ) group.remove( object );\n\t\t\t\tbreak;\n\n\t\t\t}\n\n\t\t}\n\n\t}\n\n}" }] } } From c20f3ffb1a9fbe3ee5eba8b06f3c782476ba1be4 Mon Sep 17 00:00:00 2001 From: Shota Matsuda Date: Thu, 4 Jun 2026 19:41:39 +0900 Subject: [PATCH 7/7] Docs: Fix Safari scrolling to anchors (#33719) --- utils/docs/template/static/scripts/page.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/utils/docs/template/static/scripts/page.js b/utils/docs/template/static/scripts/page.js index b828f04ccf682e..04b7e24304e6e3 100644 --- a/utils/docs/template/static/scripts/page.js +++ b/utils/docs/template/static/scripts/page.js @@ -30,9 +30,15 @@ if ( typeof hljs !== 'undefined' ) { if ( hash ) { - const element = document.getElementById( hash ); + window.history.scrollRestoration = 'manual'; - if ( element ) element.scrollIntoView(); + window.addEventListener( 'pageshow', function () { + + const element = document.getElementById( hash ); + + if ( element ) element.scrollIntoView(); + + }, { once: true } ); }