From 73e505c01f58c3ac4e1823d64147cb7bd18110bc Mon Sep 17 00:00:00 2001 From: Michael Herzog Date: Thu, 6 Aug 2026 16:30:58 +0200 Subject: [PATCH 1/2] WebGPURenderer: Introduce refresh types for render objects. (#34162) --- src/constants.js | 21 ++++ src/materials/nodes/PointsNodeMaterial.js | 3 +- .../nodes/manager/NodeMaterialObserver.js | 110 ++++++++++++++++-- src/nodes/accessors/Instance.js | 6 +- src/nodes/display/ScreenNode.js | 7 +- src/renderers/common/Bindings.js | 37 ++++++ src/renderers/common/Renderer.js | 28 ++++- src/renderers/common/nodes/NodeManager.js | 4 +- 8 files changed, 193 insertions(+), 23 deletions(-) diff --git a/src/constants.js b/src/constants.js index 10f2debb3440de..4a1b468b6953bf 100644 --- a/src/constants.js +++ b/src/constants.js @@ -1710,6 +1710,18 @@ export const Compatibility = { TEXTURE_COMPARE: 'depthTextureCompare' }; +/** + * Represents the refresh types of render objects. + * + * @type {ConstantsRenderObjectRefreshType} + * @constant + */ +export const RenderObjectRefreshType = { + NONE: 0, + SHARED: 1, + FULL: 2 +}; + /** * This type represents mouse buttons and interaction types in context of controls. * @@ -1759,3 +1771,12 @@ export const Compatibility = { * @property {string} FIRST - Flat interpolation using the first vertex. * @property {string} EITHER - Flat interpolation using either vertex. */ + +/** + * Represents the refresh types of render objects. + * + * @typedef {Object} ConstantsRenderObjectRefreshType + * @property {number} NONE - No refresh required. + * @property {number} SHARED - Only shared uniform buffers require an update. + * @property {number} FULL - The render object requires a full refresh. + */ diff --git a/src/materials/nodes/PointsNodeMaterial.js b/src/materials/nodes/PointsNodeMaterial.js index 18fb5eeb418c1c..311b8c20a6d207 100644 --- a/src/materials/nodes/PointsNodeMaterial.js +++ b/src/materials/nodes/PointsNodeMaterial.js @@ -5,6 +5,7 @@ import { modelViewMatrix } from '../../nodes/accessors/ModelNode.js'; import { materialPointSize } from '../../nodes/accessors/MaterialNode.js'; import { rotate } from '../../nodes/utils/RotateNode.js'; import { float, uniform, vec2, vec3, vec4 } from '../../nodes/tsl/TSLBase.js'; +import { renderGroup } from '../../nodes/core/UniformGroupNode.js'; import { PointsMaterial } from '../PointsMaterial.js'; import { Vector2 } from '../../math/Vector2.js'; @@ -200,7 +201,7 @@ class PointsNodeMaterial extends SpriteNodeMaterial { } -const scale = /*@__PURE__*/ uniform( 1 ).onFrameUpdate( function ( { renderer } ) { +const scale = /*@__PURE__*/ uniform( 1 ).setGroup( renderGroup ).onRenderUpdate( function ( { renderer } ) { const size = renderer.getSize( _size ); // logical units diff --git a/src/materials/nodes/manager/NodeMaterialObserver.js b/src/materials/nodes/manager/NodeMaterialObserver.js index 755d51b0e463af..c437779d943997 100644 --- a/src/materials/nodes/manager/NodeMaterialObserver.js +++ b/src/materials/nodes/manager/NodeMaterialObserver.js @@ -1,3 +1,5 @@ +import { RenderObjectRefreshType, DynamicDrawUsage } from '../../../constants.js'; + const refreshUniforms = [ 'alphaMap', 'alphaTest', @@ -9,28 +11,36 @@ const refreshUniforms = [ 'attenuationColor', 'attenuationDistance', 'bumpMap', + 'bumpScale', 'clearcoat', 'clearcoatMap', 'clearcoatNormalMap', 'clearcoatNormalScale', 'clearcoatRoughness', 'color', + 'dashOffset', + 'dashSize', 'dispersion', + 'displacementBias', 'displacementMap', + 'displacementScale', 'emissive', 'emissiveIntensity', 'emissiveMap', 'envMap', 'envMapIntensity', 'envMapRotation', + 'gapSize', 'gradientMap', 'ior', 'iridescence', 'iridescenceIOR', 'iridescenceMap', + 'iridescenceThickness', 'iridescenceThicknessMap', 'lightMap', 'lightMapIntensity', + 'linewidth', 'map', 'matcap', 'metalness', @@ -38,20 +48,26 @@ const refreshUniforms = [ 'normalMap', 'normalScale', 'opacity', + 'reflectivity', 'retroreflectivity', + 'rotation', 'roughness', 'roughnessMap', + 'scale', 'sheen', 'sheenColor', 'sheenColorMap', + 'sheenRoughness', 'sheenRoughnessMap', 'shininess', + 'size', 'specular', 'specularColor', 'specularColorMap', 'specularIntensity', 'specularIntensityMap', 'specularMap', + 'steps', 'thickness', 'transmission', 'transmissionMap' @@ -158,6 +174,20 @@ class NodeMaterialObserver { } + /** + * Returns `true` if the given 3D object uses instance buffers with dynamic draw usage. + * Such buffers must be uploaded once per render so the render object requires a full refresh. + * + * @param {Object3D} object - The 3D object. + * @return {boolean} Whether the given 3D object uses instance buffers with dynamic draw usage or not. + */ + hasDynamicInstancing( object ) { + + return object.isInstancedMesh === true && ( object.instanceMatrix.usage === DynamicDrawUsage || + ( object.instanceColor !== null && object.instanceColor.usage === DynamicDrawUsage ) ); + + } + /** * Returns `true` if the current rendering produces motion vectors. * @@ -205,6 +235,22 @@ class NodeMaterialObserver { } + if ( object.isInstancedMesh === true ) { + + data.instanceMatrixVersion = object.instanceMatrix.version; + data.instanceColorVersion = object.instanceColor !== null ? object.instanceColor.version : null; + data.morphTextureVersion = object.morphTexture !== null ? object.morphTexture.version : null; + + } + + if ( object.isBatchedMesh === true ) { + + data.matricesTextureVersion = object._matricesTexture.version; + data.colorsTextureVersion = object._colorsTexture !== null ? object._colorsTexture.version : null; + data.indirectTextureVersion = object._indirectTexture.version; + + } + if ( renderObject.bundle !== null ) { data.version = renderObject.bundle.version; @@ -599,6 +645,47 @@ class NodeMaterialObserver { } + // instancing + + if ( object.isInstancedMesh === true ) { + + const instanceColorVersion = object.instanceColor !== null ? object.instanceColor.version : null; + const morphTextureVersion = object.morphTexture !== null ? object.morphTexture.version : null; + + if ( renderObjectData.instanceMatrixVersion !== object.instanceMatrix.version || + renderObjectData.instanceColorVersion !== instanceColorVersion || + renderObjectData.morphTextureVersion !== morphTextureVersion ) { + + renderObjectData.instanceMatrixVersion = object.instanceMatrix.version; + renderObjectData.instanceColorVersion = instanceColorVersion; + renderObjectData.morphTextureVersion = morphTextureVersion; + + return false; + + } + + } + + // batching + + if ( object.isBatchedMesh === true ) { + + const colorsTextureVersion = object._colorsTexture !== null ? object._colorsTexture.version : null; + + if ( renderObjectData.matricesTextureVersion !== object._matricesTexture.version || + renderObjectData.colorsTextureVersion !== colorsTextureVersion || + renderObjectData.indirectTextureVersion !== object._indirectTexture.version ) { + + renderObjectData.matricesTextureVersion = object._matricesTexture.version; + renderObjectData.colorsTextureVersion = colorsTextureVersion; + renderObjectData.indirectTextureVersion = object._indirectTexture.version; + + return false; + + } + + } + // lights if ( renderObjectData.lights ) { @@ -721,16 +808,16 @@ class NodeMaterialObserver { * * @param {RenderObject} renderObject - The render object. * @param {NodeFrame} nodeFrame - The current node frame. - * @return {boolean} Whether the given render object requires a refresh or not. + * @return {number} The refresh type, see {@link RenderObjectRefreshType}. */ needsRefresh( renderObject, nodeFrame ) { - if ( this.hasNode || this.hasAnimation || this.firstInitialization( renderObject ) || this.needsVelocity( nodeFrame.renderer ) ) - return true; + if ( this.hasNode || this.hasAnimation || this.hasDynamicInstancing( renderObject.object ) || this.firstInitialization( renderObject ) || this.needsVelocity( nodeFrame.renderer ) ) + return RenderObjectRefreshType.FULL; const { renderId } = nodeFrame; - let force = false; + let refreshType = RenderObjectRefreshType.NONE; // shared UBOs are potentially never updated when objects don't change. Below block // make sure these UBOs are updated at least once. @@ -739,9 +826,9 @@ class NodeMaterialObserver { this.renderId = renderId; - // no early out here. instead, force the render object to use the equals() code path so its internal cache state gets synched + // no early out here. instead, use the equals() code path below so the internal cache state gets synched - force = true; + refreshType = RenderObjectRefreshType.SHARED; } @@ -749,12 +836,17 @@ class NodeMaterialObserver { const isBundle = renderObject.bundle !== null && renderObject.bundle.static === true && this.getRenderObjectData( renderObject ).version === renderObject.bundle.version; if ( isStatic || isBundle ) - return force; + return refreshType; const lightsData = this.getLights( renderObject.lightsNode, renderId ); - const notEqual = this.equals( renderObject, lightsData, renderId ) !== true; - return ( force || notEqual ); + if ( this.equals( renderObject, lightsData, renderId ) === false ) { + + refreshType = RenderObjectRefreshType.FULL; + + } + + return refreshType; } diff --git a/src/nodes/accessors/Instance.js b/src/nodes/accessors/Instance.js index 80a14cbb3c76bb..2723fa918f28e2 100644 --- a/src/nodes/accessors/Instance.js +++ b/src/nodes/accessors/Instance.js @@ -1,6 +1,6 @@ import { vec3, mat4, Fn } from '../tsl/TSLBase.js'; -import { OnAfterObjectUpdate, OnFrameUpdate } from '../utils/EventNode.js'; +import { OnAfterObjectUpdate, OnBeforeFrameUpdate } from '../utils/EventNode.js'; import { normalLocal, transformNormal } from './Normal.js'; import { positionLocal, positionPrevious } from './Position.js'; import { varyingProperty } from '../core/PropertyNode.js'; @@ -172,10 +172,10 @@ export const instance = /*@__PURE__*/ Fn( ( [ matrices, colors = null ], builder } - // Synchronization of dynamic buffer updates per frame + // Synchronization of dynamic buffer updates per frame. if ( interleavedMatrix !== null || interleavedColor !== null ) { - OnFrameUpdate( () => { + OnBeforeFrameUpdate( () => { if ( interleavedMatrix !== null && interleavedMatrix.version !== matrices.version ) { diff --git a/src/nodes/display/ScreenNode.js b/src/nodes/display/ScreenNode.js index acaefe32da5b19..eb1a9c084c4233 100644 --- a/src/nodes/display/ScreenNode.js +++ b/src/nodes/display/ScreenNode.js @@ -1,5 +1,6 @@ import Node from '../core/Node.js'; import { NodeUpdateType } from '../core/constants.js'; +import { renderGroup } from '../core/UniformGroupNode.js'; import { uniform } from '../core/UniformNode.js'; import { nodeImmutable, vec2 } from '../tsl/TSLBase.js'; import { Vector2 } from '../../math/Vector2.js'; @@ -151,15 +152,15 @@ class ScreenNode extends Node { if ( scope === ScreenNode.SIZE ) { - output = uniform( _screenSizeVec || ( _screenSizeVec = new Vector2() ) ); + output = uniform( _screenSizeVec || ( _screenSizeVec = new Vector2() ) ).setGroup( renderGroup ); } else if ( scope === ScreenNode.VIEWPORT ) { - output = uniform( _viewportVec || ( _viewportVec = new Vector4() ) ); + output = uniform( _viewportVec || ( _viewportVec = new Vector4() ) ).setGroup( renderGroup ); } else if ( scope === ScreenNode.DPR ) { - output = uniform( 1 ); + output = uniform( 1 ).setGroup( renderGroup ); } else { diff --git a/src/renderers/common/Bindings.js b/src/renderers/common/Bindings.js index 2b9c5923279ba8..747162e74e9dc1 100644 --- a/src/renderers/common/Bindings.js +++ b/src/renderers/common/Bindings.js @@ -149,6 +149,43 @@ class Bindings extends DataMap { } + /** + * Updates only the shared uniform buffers of the given render object. + * + * @param {RenderObject} renderObject - The render object. + */ + updateSharedForRender( renderObject ) { + + const bindings = this.getForRender( renderObject ); + + for ( const bindGroup of bindings ) { + + for ( const binding of bindGroup.bindings ) { + + if ( binding.isNodeUniformsGroup === true && binding.groupNode.shared === true ) { + + const updatedGroup = this.nodes.updateGroup( binding ); + + if ( updatedGroup === false ) continue; + + const updated = binding.update(); + + if ( updated ) { + + this.backend.updateBinding( binding ); + + } + + if ( binding.updateRanges.length > 0 ) binding.clearUpdateRanges(); + + } + + } + + } + + } + /** * Deletes the bindings for the given compute node. * diff --git a/src/renderers/common/Renderer.js b/src/renderers/common/Renderer.js index b906c1eaca6d54..59adb81850d2a2 100644 --- a/src/renderers/common/Renderer.js +++ b/src/renderers/common/Renderer.js @@ -30,7 +30,7 @@ import { Matrix4 } from '../../math/Matrix4.js'; import { Vector2 } from '../../math/Vector2.js'; import { Vector4 } from '../../math/Vector4.js'; import { RenderTarget } from '../../core/RenderTarget.js'; -import { DoubleSide, BackSide, FrontSide, SRGBColorSpace, NoToneMapping, LinearFilter, HalfFloatType, RGBAFormat, PCFShadowMap, PCFSoftShadowMap, VSMShadowMap } from '../../constants.js'; +import { DoubleSide, BackSide, FrontSide, SRGBColorSpace, NoToneMapping, LinearFilter, HalfFloatType, RGBAFormat, PCFShadowMap, PCFSoftShadowMap, VSMShadowMap, RenderObjectRefreshType } from '../../constants.js'; import { float, vec3, vec4, Fn } from '../../nodes/tsl/TSLCore.js'; import { reference } from '../../nodes/accessors/ReferenceNode.js'; @@ -1450,7 +1450,9 @@ class Renderer { const renderObject = renderObjects[ i ]; - if ( this._nodes.needsRefresh( renderObject ) ) { + const refreshType = this._nodes.needsRefresh( renderObject ); + + if ( refreshType === RenderObjectRefreshType.FULL ) { this._nodes.updateBefore( renderObject ); @@ -1460,6 +1462,15 @@ class Renderer { this._nodes.updateAfter( renderObject ); + } else if ( refreshType === RenderObjectRefreshType.SHARED ) { + + this._nodes.updateBefore( renderObject ); + + this._nodes.updateForRender( renderObject ); + this._bindings.updateSharedForRender( renderObject ); + + this._nodes.updateAfter( renderObject ); + } } @@ -3861,9 +3872,9 @@ class Renderer { // - const needsRefresh = this._nodes.needsRefresh( renderObject ); + const refreshType = this._nodes.needsRefresh( renderObject ); - if ( needsRefresh ) { + if ( refreshType === RenderObjectRefreshType.FULL ) { this._nodes.updateBefore( renderObject ); @@ -3872,6 +3883,13 @@ class Renderer { this._nodes.updateForRender( renderObject ); this._bindings.updateForRender( renderObject ); + } else if ( refreshType === RenderObjectRefreshType.SHARED ) { + + this._nodes.updateBefore( renderObject ); + + this._nodes.updateForRender( renderObject ); + this._bindings.updateSharedForRender( renderObject ); + } this._pipelines.updateForRender( renderObject ); @@ -3882,7 +3900,7 @@ class Renderer { this.backend.draw( renderObject, this.info ); - if ( needsRefresh ) this._nodes.updateAfter( renderObject ); + if ( refreshType !== RenderObjectRefreshType.NONE ) this._nodes.updateAfter( renderObject ); } diff --git a/src/renderers/common/nodes/NodeManager.js b/src/renderers/common/nodes/NodeManager.js index 0b5faf6a111e1c..13f023ce9f3538 100644 --- a/src/renderers/common/nodes/NodeManager.js +++ b/src/renderers/common/nodes/NodeManager.js @@ -1066,10 +1066,10 @@ class NodeManager extends DataMap { } /** - * Returns `true` if the given render object requires a refresh. + * Returns the refresh type of the given render object. * * @param {RenderObject} renderObject - The render object. - * @return {boolean} Whether the given render object requires a refresh or not. + * @return {number} The refresh type, see `RenderObjectRefreshType`. */ needsRefresh( renderObject ) { From 19567418fa4c798413d22c19b215171f3eaadb4c Mon Sep 17 00:00:00 2001 From: Michael Herzog Date: Thu, 6 Aug 2026 17:57:48 +0200 Subject: [PATCH 2/2] TrackballControls: Avoid updating `_movePrev` too often. (#34173) --- examples/jsm/controls/TrackballControls.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/examples/jsm/controls/TrackballControls.js b/examples/jsm/controls/TrackballControls.js index 469456a9a8e142..a3722763f0b16a 100644 --- a/examples/jsm/controls/TrackballControls.js +++ b/examples/jsm/controls/TrackballControls.js @@ -839,7 +839,6 @@ function onMouseMove( event ) { if ( state === _STATE.ROTATE && ! this.noRotate ) { - this._movePrev.copy( this._moveCurr ); this._moveCurr.copy( this._getMouseOnCircle( event.pageX, event.pageY ) ); } else if ( state === _STATE.ZOOM && ! this.noZoom ) { @@ -939,7 +938,6 @@ function onTouchMove( event ) { switch ( this._pointers.length ) { case 1: - this._movePrev.copy( this._moveCurr ); this._moveCurr.copy( this._getMouseOnCircle( event.pageX, event.pageY ) ); break;