From e2c2cd1f40b4c980e18c7d1581d3ad6723e9e584 Mon Sep 17 00:00:00 2001 From: sunag Date: Tue, 11 Nov 2025 09:35:10 -0300 Subject: [PATCH 01/21] wip filter instances --- src/materials/nodes/NodeMaterial.js | 6 +- src/nodes/core/NodeBuilder.js | 7 + src/renderers/common/RenderList.js | 5 +- src/renderers/common/RenderObject.js | 202 ++---------------- src/renderers/common/RenderObjectUtils.js | 189 ++++++++++++++++ src/renderers/common/RenderObjects.js | 14 +- src/renderers/common/Renderer.js | 100 ++++++++- .../common/nodes/NodeBuilderState.js | 10 +- src/renderers/common/nodes/Nodes.js | 4 +- 9 files changed, 339 insertions(+), 198 deletions(-) create mode 100644 src/renderers/common/RenderObjectUtils.js diff --git a/src/materials/nodes/NodeMaterial.js b/src/materials/nodes/NodeMaterial.js index a976d86a22db20..222518a2bebd8e 100644 --- a/src/materials/nodes/NodeMaterial.js +++ b/src/materials/nodes/NodeMaterial.js @@ -797,10 +797,14 @@ class NodeMaterial extends Material { } - if ( ( object.isInstancedMesh && object.instanceMatrix && object.instanceMatrix.isInstancedBufferAttribute === true ) ) { + if ( object.isInstancedMesh && object.instanceMatrix && object.instanceMatrix.isInstancedBufferAttribute === true ) { instancedMesh( object ).toStack(); + } else if ( builder.instances !== null ) { + + console.log( '>>', builder.instances ); + } if ( this.positionNode !== null ) { diff --git a/src/nodes/core/NodeBuilder.js b/src/nodes/core/NodeBuilder.js index f53619c682f7c6..c1fbacfeadca9d 100644 --- a/src/nodes/core/NodeBuilder.js +++ b/src/nodes/core/NodeBuilder.js @@ -379,6 +379,13 @@ class NodeBuilder { */ this.tab = '\t'; + /** + * Reference to the current instance data. + * + * @type {?Array} + */ + this.instances = null; + /** * Reference to the current function node. * diff --git a/src/renderers/common/RenderList.js b/src/renderers/common/RenderList.js index 6d588e83e2b9f2..736a6348fa0235 100644 --- a/src/renderers/common/RenderList.js +++ b/src/renderers/common/RenderList.js @@ -240,7 +240,8 @@ class RenderList { renderOrder: object.renderOrder, z: z, group: group, - clippingContext: clippingContext + clippingContext: clippingContext, + instances: null }; this.renderItems[ this.renderItemsIndex ] = renderItem; @@ -256,6 +257,7 @@ class RenderList { renderItem.z = z; renderItem.group = group; renderItem.clippingContext = clippingContext; + renderItem.instances = null; } @@ -394,6 +396,7 @@ class RenderList { renderItem.z = null; renderItem.group = null; renderItem.clippingContext = null; + renderItem.instances = null; } diff --git a/src/renderers/common/RenderObject.js b/src/renderers/common/RenderObject.js index 812f4a6f021d73..c2a785673cb4c3 100644 --- a/src/renderers/common/RenderObject.js +++ b/src/renderers/common/RenderObject.js @@ -1,41 +1,8 @@ -import { hash, hashString } from '../../nodes/core/NodeUtils.js'; +import { hash } from '../../nodes/core/NodeUtils.js'; +import { getGeometryCacheKey, getProgramCacheKey } from './RenderObjectUtils.js'; let _id = 0; -function getKeys( obj ) { - - const keys = Object.keys( obj ); - - let proto = Object.getPrototypeOf( obj ); - - while ( proto ) { - - const descriptors = Object.getOwnPropertyDescriptors( proto ); - - for ( const key in descriptors ) { - - if ( descriptors[ key ] !== undefined ) { - - const descriptor = descriptors[ key ]; - - if ( descriptor && typeof descriptor.get === 'function' ) { - - keys.push( key ); - - } - - } - - } - - proto = Object.getPrototypeOf( proto ); - - } - - return keys; - -} - /** * A render object is the renderer's representation of single entity that gets drawn * with a draw command. There is no unique mapping of render objects to 3D objects in the @@ -69,8 +36,9 @@ class RenderObject { * @param {LightsNode} lightsNode - The lights node. * @param {RenderContext} renderContext - The render context. * @param {ClippingContext} clippingContext - The clipping context. + * @param {Array} [instances=null] - An array of instances for instanced rendering. */ - constructor( nodes, geometries, renderer, object, material, scene, camera, lightsNode, renderContext, clippingContext ) { + constructor( nodes, geometries, renderer, object, material, scene, camera, lightsNode, renderContext, clippingContext, instances = null ) { this.id = _id ++; @@ -153,6 +121,13 @@ class RenderObject { */ this.version = material.version; + /** + * An array of instances for instanced rendering. + * + * @type {?Array} + */ + this.instances = instances; + /** * The draw range of the geometry. * @@ -451,7 +426,7 @@ class RenderObject { */ getChainArray() { - return [ this.object, this.material, this.context, this.lightsNode ]; + return [ this.object.geometry, this.material, this.context, this.lightsNode ]; } @@ -626,50 +601,7 @@ class RenderObject { */ getGeometryCacheKey() { - const { geometry } = this; - - let cacheKey = ''; - - for ( const name of Object.keys( geometry.attributes ).sort() ) { - - const attribute = geometry.attributes[ name ]; - - cacheKey += name + ','; - - if ( attribute.data ) cacheKey += attribute.data.stride + ','; - if ( attribute.offset ) cacheKey += attribute.offset + ','; - if ( attribute.itemSize ) cacheKey += attribute.itemSize + ','; - if ( attribute.normalized ) cacheKey += 'n,'; - - } - - // structural equality isn't sufficient for morph targets since the - // data are maintained in textures. only if the targets are all equal - // the texture and thus the instance of `MorphNode` can be shared. - - for ( const name of Object.keys( geometry.morphAttributes ).sort() ) { - - const targets = geometry.morphAttributes[ name ]; - - cacheKey += 'morph-' + name + ','; - - for ( let i = 0, l = targets.length; i < l; i ++ ) { - - const attribute = targets[ i ]; - - cacheKey += attribute.id + ','; - - } - - } - - if ( geometry.index ) { - - cacheKey += 'index,'; - - } - - return cacheKey; + return getGeometryCacheKey( this.geometry ); } @@ -682,105 +614,7 @@ class RenderObject { */ getMaterialCacheKey() { - const { object, material, renderer } = this; - - let cacheKey = material.customProgramCacheKey(); - - for ( const property of getKeys( material ) ) { - - if ( /^(is[A-Z]|_)|^(visible|version|uuid|name|opacity|userData)$/.test( property ) ) continue; - - const value = material[ property ]; - - let valueKey; - - if ( value !== null ) { - - // some material values require a formatting - - const type = typeof value; - - if ( type === 'number' ) { - - valueKey = value !== 0 ? '1' : '0'; // Convert to on/off, important for clearcoat, transmission, etc - - } else if ( type === 'object' ) { - - valueKey = '{'; - - if ( value.isTexture ) { - - valueKey += value.mapping; - - // WebGPU must honor the sampler data because they are part of the bindings - - if ( renderer.backend.isWebGPUBackend === true ) { - - valueKey += value.magFilter; - valueKey += value.minFilter; - valueKey += value.wrapS; - valueKey += value.wrapT; - valueKey += value.wrapR; - - } - - } - - valueKey += '}'; - - } else { - - valueKey = String( value ); - - } - - } else { - - valueKey = String( value ); - - } - - cacheKey += /*property + ':' +*/ valueKey + ','; - - } - - cacheKey += this.clippingContextCacheKey + ','; - - if ( object.geometry ) { - - cacheKey += this.getGeometryCacheKey(); - - } - - if ( object.skeleton ) { - - cacheKey += object.skeleton.bones.length + ','; - - } - - if ( object.isBatchedMesh ) { - - cacheKey += object._matricesTexture.uuid + ','; - - if ( object._colorsTexture !== null ) { - - cacheKey += object._colorsTexture.uuid + ','; - - } - - } - - if ( object.isInstancedMesh || object.count > 1 || Array.isArray( object.morphTargetInfluences ) ) { - - // TODO: https://github.com/mrdoob/three.js/pull/29066#issuecomment-2269400850 - - cacheKey += object.uuid + ','; - - } - - cacheKey += object.receiveShadow + ','; - - return hashString( cacheKey ); + return getProgramCacheKey( this.object, this.material, this.renderer, this.clippingContext ); } @@ -869,6 +703,14 @@ class RenderObject { } + if ( this.instances !== null ) { + + const base = Math.ceil( this.instances.length / 8 ); + + cacheKey = hash( cacheKey, base ); + + } + cacheKey = hash( cacheKey, this.camera.id ); return cacheKey; diff --git a/src/renderers/common/RenderObjectUtils.js b/src/renderers/common/RenderObjectUtils.js new file mode 100644 index 00000000000000..0f028e2c90e4e4 --- /dev/null +++ b/src/renderers/common/RenderObjectUtils.js @@ -0,0 +1,189 @@ +import { hashString } from '../../nodes/core/NodeUtils.js'; + +function getKeys( obj ) { + + const keys = Object.keys( obj ); + + let proto = Object.getPrototypeOf( obj ); + + while ( proto ) { + + const descriptors = Object.getOwnPropertyDescriptors( proto ); + + for ( const key in descriptors ) { + + if ( descriptors[ key ] !== undefined ) { + + const descriptor = descriptors[ key ]; + + if ( descriptor && typeof descriptor.get === 'function' ) { + + keys.push( key ); + + } + + } + + } + + proto = Object.getPrototypeOf( proto ); + + } + + return keys; + +} + +export function getGeometryCacheKey( geometry ) { + + let cacheKey = ''; + + for ( const name of Object.keys( geometry.attributes ).sort() ) { + + const attribute = geometry.attributes[ name ]; + + cacheKey += name + ','; + + if ( attribute.data ) cacheKey += attribute.data.stride + ','; + if ( attribute.offset ) cacheKey += attribute.offset + ','; + if ( attribute.itemSize ) cacheKey += attribute.itemSize + ','; + if ( attribute.normalized ) cacheKey += 'n,'; + + } + + // structural equality isn't sufficient for morph targets since the + // data are maintained in textures. only if the targets are all equal + // the texture and thus the instance of `MorphNode` can be shared. + + for ( const name of Object.keys( geometry.morphAttributes ).sort() ) { + + const targets = geometry.morphAttributes[ name ]; + + cacheKey += 'morph-' + name + ','; + + for ( let i = 0, l = targets.length; i < l; i ++ ) { + + const attribute = targets[ i ]; + + cacheKey += attribute.id + ','; + + } + + } + + if ( geometry.index ) { + + cacheKey += 'index,'; + + } + + return cacheKey; + + +} + +export function getProgramCacheKey( object, material, renderer, clippingContext = null ) { + + let cacheKey = material.customProgramCacheKey(); + + for ( const property of getKeys( material ) ) { + + if ( /^(is[A-Z]|_)|^(visible|version|uuid|name|opacity|userData)$/.test( property ) ) continue; + + const value = material[ property ]; + + let valueKey; + + if ( value !== null ) { + + // some material values require a formatting + + const type = typeof value; + + if ( type === 'number' ) { + + valueKey = value !== 0 ? '1' : '0'; // Convert to on/off, important for clearcoat, transmission, etc + + } else if ( type === 'object' ) { + + valueKey = '{'; + + if ( value.isTexture ) { + + valueKey += value.mapping; + + // WebGPU must honor the sampler data because they are part of the bindings + + if ( renderer.backend.isWebGPUBackend === true ) { + + valueKey += value.magFilter; + valueKey += value.minFilter; + valueKey += value.wrapS; + valueKey += value.wrapT; + valueKey += value.wrapR; + + } + + } + + valueKey += '}'; + + } else { + + valueKey = String( value ); + + } + + } else { + + valueKey = String( value ); + + } + + cacheKey += /*property + ':' +*/ valueKey + ','; + + } + + if ( clippingContext !== null ) { + + cacheKey += clippingContext.cacheKey + ','; + + } + + if ( object.geometry ) { + + cacheKey += getGeometryCacheKey( object.geometry ); + + } + + if ( object.skeleton ) { + + cacheKey += object.skeleton.bones.length + ','; + + } + + if ( object.isBatchedMesh ) { + + cacheKey += object._matricesTexture.uuid + ','; + + if ( object._colorsTexture !== null ) { + + cacheKey += object._colorsTexture.uuid + ','; + + } + + } + + if ( object.isInstancedMesh || object.count > 1 || Array.isArray( object.morphTargetInfluences ) ) { + + // TODO: https://github.com/mrdoob/three.js/pull/29066#issuecomment-2269400850 + + cacheKey += object.uuid + ','; + + } + + cacheKey += object.receiveShadow + ','; + + return hashString( cacheKey ); + +} diff --git a/src/renderers/common/RenderObjects.js b/src/renderers/common/RenderObjects.js index 0495aa7d50effa..8a23fc89b40f76 100644 --- a/src/renderers/common/RenderObjects.js +++ b/src/renderers/common/RenderObjects.js @@ -85,14 +85,15 @@ class RenderObjects { * @param {RenderContext} renderContext - The render context. * @param {ClippingContext} clippingContext - The clipping context. * @param {string} [passId] - An optional ID for identifying the pass. + * @param {Array} [instances=null] - An optional array of instances to render. * @return {RenderObject} The render object. */ - get( object, material, scene, camera, lightsNode, renderContext, clippingContext, passId ) { + get( object, material, scene, camera, lightsNode, renderContext, clippingContext, passId, instances = null ) { const chainMap = this.getChainMap( passId ); // reuse chainArray - _chainKeys[ 0 ] = object; + _chainKeys[ 0 ] = object.geometry; _chainKeys[ 1 ] = material; _chainKeys[ 2 ] = renderContext; _chainKeys[ 3 ] = lightsNode; @@ -101,7 +102,7 @@ class RenderObjects { if ( renderObject === undefined ) { - renderObject = this.createRenderObject( this.nodes, this.geometries, this.renderer, object, material, scene, camera, lightsNode, renderContext, clippingContext, passId ); + renderObject = this.createRenderObject( this.nodes, this.geometries, this.renderer, object, material, scene, camera, lightsNode, renderContext, clippingContext, passId, instances ); chainMap.set( _chainKeys, renderObject ); @@ -121,7 +122,7 @@ class RenderObjects { renderObject.dispose(); - renderObject = this.get( object, material, scene, camera, lightsNode, renderContext, clippingContext, passId ); + renderObject = this.get( object, material, scene, camera, lightsNode, renderContext, clippingContext, passId, instances ); } else { @@ -174,13 +175,14 @@ class RenderObjects { * @param {RenderContext} renderContext - The render context. * @param {ClippingContext} clippingContext - The clipping context. * @param {string} [passId] - An optional ID for identifying the pass. + * @param {Array} [instances] - An optional array of instances to render. * @return {RenderObject} The render object. */ - createRenderObject( nodes, geometries, renderer, object, material, scene, camera, lightsNode, renderContext, clippingContext, passId ) { + createRenderObject( nodes, geometries, renderer, object, material, scene, camera, lightsNode, renderContext, clippingContext, passId, instances = null ) { const chainMap = this.getChainMap( passId ); - const renderObject = new RenderObject( nodes, geometries, renderer, object, material, scene, camera, lightsNode, renderContext, clippingContext ); + const renderObject = new RenderObject( nodes, geometries, renderer, object, material, scene, camera, lightsNode, renderContext, clippingContext, instances ); renderObject.onDispose = () => { diff --git a/src/renderers/common/Renderer.js b/src/renderers/common/Renderer.js index 074ac7ee2e54ba..50274c175205ff 100644 --- a/src/renderers/common/Renderer.js +++ b/src/renderers/common/Renderer.js @@ -1484,6 +1484,10 @@ class Renderer { // + this._filterRenderList( renderList, camera, sceneRef ); + + // + if ( renderTarget !== null ) { this._textures.updateRenderTarget( renderTarget, activeMipmapLevel ); @@ -2416,6 +2420,83 @@ class Renderer { } + /** + * Filters the render list to remove duplicate objects based on their + * geometry, material, scene, camera, lightsNode, render context and clipping context. + * + * @private + * @param {RenderList} renderList - The render list. + * @param {Camera} camera - The camera. + * @param {Scene} scene - The scene. + * @param {?string} [passId=null] - An optional ID for identifying the pass. + */ + _filterRenderList( renderList, camera, scene, passId = null ) { + + const lightsNode = renderList.lightsNode; + + renderList.opaque = this._filterObjects( renderList.opaque, camera, scene, lightsNode, passId ); + renderList.transparent = this._filterObjects( renderList.transparent, camera, scene, lightsNode, passId ); + renderList.transparentDoublePass = this._filterObjects( renderList.transparentDoublePass, camera, scene, lightsNode, passId ); + + } + + /** + * Filters the given render items to remove duplicate objects based on their + * geometry, material, scene, camera, lightsNode, render context and clipping context. + * + * @private + * @param {Array} renderItems - The render items. + * @param {Camera} camera - The camera. + * @param {Scene} scene - The scene. + * @param {LightsNode} lightsNode - The lights node. + * @param {?string} [passId=null] - An optional ID for identifying the pass. + * @return {Array} The filtered render items. + */ + _filterObjects( renderItems, camera, scene, lightsNode, passId = null ) { + + const renderList = []; + const renderId = this._nodes.nodeFrame.renderId; + + for ( const renderItem of renderItems ) { + + const { geometry, material, clippingContext } = renderItem; + + const chainMap = this._objects.getChainMap( passId ); + const chainKeys = [ material, geometry, scene, camera, lightsNode, this._currentRenderContext, clippingContext ]; + + let map = chainMap.get( chainKeys ); + + if ( map === undefined ) { + + map = { + renderItem: null, + renderId: - 1 + }; + + chainMap.set( chainKeys, map ); + + } + + if ( map.renderId !== renderId ) { + + map.renderId = renderId; + map.renderItem = renderItem; + + renderList.push( renderItem ); + + } else { + + map.renderItem.instances = map.renderItem.instances || []; + map.renderItem.instances.push( renderItem.object ); + + } + + } + + return renderList; + + } + /** * Execute a single or an array of compute nodes. This method can only be called * if the renderer has been initialized. @@ -2965,9 +3046,9 @@ class Renderer { for ( let i = 0, il = renderList.length; i < il; i ++ ) { - const { object, geometry, material, group, clippingContext } = renderList[ i ]; + const { object, geometry, material, group, clippingContext, instances } = renderList[ i ]; - this._currentRenderObjectFunction( object, scene, camera, geometry, material, group, lightsNode, clippingContext, passId ); + this._currentRenderObjectFunction( object, scene, camera, geometry, material, group, lightsNode, clippingContext, passId, instances ); } @@ -3074,8 +3155,9 @@ class Renderer { * @param {LightsNode} lightsNode - The current lights node. * @param {?ClippingContext} clippingContext - The clipping context. * @param {?string} [passId=null] - An optional ID for identifying the pass. + * @param {?Array} [instances=null] - An optional array of instances if the objects are instanced. */ - renderObject( object, scene, camera, geometry, material, group, lightsNode, clippingContext = null, passId = null ) { + renderObject( object, scene, camera, geometry, material, group, lightsNode, clippingContext = null, passId = null, instances = null ) { let materialOverride = false; let materialColorNode; @@ -3134,16 +3216,16 @@ class Renderer { if ( material.transparent === true && material.side === DoubleSide && material.forceSinglePass === false ) { material.side = BackSide; - this._handleObjectFunction( object, material, scene, camera, lightsNode, group, clippingContext, 'backSide' ); // create backSide pass id + this._handleObjectFunction( object, material, scene, camera, lightsNode, group, clippingContext, 'backSide', instances ); // create backSide pass id material.side = FrontSide; - this._handleObjectFunction( object, material, scene, camera, lightsNode, group, clippingContext, passId ); // use default pass id + this._handleObjectFunction( object, material, scene, camera, lightsNode, group, clippingContext, passId, instances ); // use default pass id material.side = DoubleSide; } else { - this._handleObjectFunction( object, material, scene, camera, lightsNode, group, clippingContext, passId ); + this._handleObjectFunction( object, material, scene, camera, lightsNode, group, clippingContext, passId, instances ); } @@ -3177,10 +3259,12 @@ class Renderer { * @param {?{start: number, count: number}} group - Only relevant for objects using multiple materials. This represents a group entry from the respective `BufferGeometry`. * @param {ClippingContext} clippingContext - The clipping context. * @param {string} [passId] - An optional ID for identifying the pass. + * @param {?Array} [instances=null] - An optional array of instances if the objects are instanced. */ - _renderObjectDirect( object, material, scene, camera, lightsNode, group, clippingContext, passId ) { + _renderObjectDirect( object, material, scene, camera, lightsNode, group, clippingContext, passId, instances = null ) { - const renderObject = this._objects.get( object, material, scene, camera, lightsNode, this._currentRenderContext, clippingContext, passId ); + const renderObject = this._objects.get( object, material, scene, camera, lightsNode, this._currentRenderContext, clippingContext, passId, instances ); + renderObject.instances = instances; renderObject.drawRange = object.geometry.drawRange; renderObject.group = group; diff --git a/src/renderers/common/nodes/NodeBuilderState.js b/src/renderers/common/nodes/NodeBuilderState.js index 5e9b47832e43e7..a10ee7cf5ae1b2 100644 --- a/src/renderers/common/nodes/NodeBuilderState.js +++ b/src/renderers/common/nodes/NodeBuilderState.js @@ -24,8 +24,9 @@ class NodeBuilderState { * @param {Array} updateAfterNodes - An array of nodes that implement their `updateAfter()` method. * @param {NodeMaterialObserver} observer - A node material observer. * @param {Array} transforms - An array with transform attribute objects. Only relevant when using compute shaders with WebGL 2. + * @param {Array|null} instances - An array of instance data or null if instancing is not used. */ - constructor( vertexShader, fragmentShader, computeShader, nodeAttributes, bindings, updateNodes, updateBeforeNodes, updateAfterNodes, observer, transforms = [] ) { + constructor( vertexShader, fragmentShader, computeShader, nodeAttributes, bindings, updateNodes, updateBeforeNodes, updateAfterNodes, observer, transforms = [], instances = null ) { /** * The native vertex shader code. @@ -56,6 +57,13 @@ class NodeBuilderState { */ this.transforms = transforms; + /** + * An array of instance data or null if instancing is not used. + * + * @type {Array|null} + */ + this.instances = instances; + /** * An array of node attributes representing * the attributes of the shaders. diff --git a/src/renderers/common/nodes/Nodes.js b/src/renderers/common/nodes/Nodes.js index 832544018c93a7..2ae98912e4647d 100644 --- a/src/renderers/common/nodes/Nodes.js +++ b/src/renderers/common/nodes/Nodes.js @@ -198,6 +198,7 @@ class Nodes extends DataMap { const createNodeBuilder = ( material ) => { const nodeBuilder = this.backend.createNodeBuilder( renderObject.object, this.renderer ); + nodeBuilder.instances = renderObject.instances; nodeBuilder.scene = renderObject.scene; nodeBuilder.material = material; nodeBuilder.camera = renderObject.camera; @@ -318,7 +319,8 @@ class Nodes extends DataMap { nodeBuilder.updateBeforeNodes, nodeBuilder.updateAfterNodes, nodeBuilder.observer, - nodeBuilder.transforms + nodeBuilder.transforms, + nodeBuilder.instances ); } From 9ba2f7974d3b1b849dc5cf17a27cd1e16aeb28f2 Mon Sep 17 00:00:00 2001 From: sunag Date: Mon, 24 Nov 2025 14:34:49 -0300 Subject: [PATCH 02/21] Update RenderObjectUtils.js --- src/renderers/common/RenderObjectUtils.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/renderers/common/RenderObjectUtils.js b/src/renderers/common/RenderObjectUtils.js index 0f028e2c90e4e4..63d94e5b957ce6 100644 --- a/src/renderers/common/RenderObjectUtils.js +++ b/src/renderers/common/RenderObjectUtils.js @@ -82,7 +82,7 @@ export function getGeometryCacheKey( geometry ) { } -export function getProgramCacheKey( object, material, renderer, clippingContext = null ) { +export function getProgramCacheKey( object, material, renderer, context, clippingContext = null ) { let cacheKey = material.customProgramCacheKey(); @@ -182,6 +182,8 @@ export function getProgramCacheKey( object, material, renderer, clippingContext } + cacheKey += context.id + ','; + cacheKey += object.receiveShadow + ','; return hashString( cacheKey ); From edc1efc24a7cc79767bc3537c5ac4e259269b54f Mon Sep 17 00:00:00 2001 From: sunag Date: Sun, 7 Dec 2025 02:25:37 -0300 Subject: [PATCH 03/21] Introduce `objectIndex` --- src/nodes/core/IndexNode.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/nodes/core/IndexNode.js b/src/nodes/core/IndexNode.js index 5908cc694d75ea..fe79684b6bce8e 100644 --- a/src/nodes/core/IndexNode.js +++ b/src/nodes/core/IndexNode.js @@ -131,6 +131,14 @@ export const vertexIndex = /*@__PURE__*/ nodeImmutable( IndexNode, IndexNode.VER */ export const instanceIndex = /*@__PURE__*/ nodeImmutable( IndexNode, IndexNode.INSTANCE ); +/** + * TSL object that represents the index of either a mesh instance or an invocation of a compute shader. + * + * @tsl + * @type {IndexNode} + */ +export const objectIndex = instanceIndex; + /** * TSL object that represents the index of the subgroup the current compute invocation belongs to. * From 21e9ff0cb41833d9071351fb9a20ed1f0b6df933 Mon Sep 17 00:00:00 2001 From: sunag Date: Sun, 7 Dec 2025 02:31:56 -0300 Subject: [PATCH 04/21] add `mediumpModelWorldMatrix` and new `modelWorldMatrix` --- src/nodes/accessors/ModelNode.js | 51 +++++++++++++++++++++++++++++--- 1 file changed, 47 insertions(+), 4 deletions(-) diff --git a/src/nodes/accessors/ModelNode.js b/src/nodes/accessors/ModelNode.js index 196c29d9f09ce1..c10af34d03c468 100644 --- a/src/nodes/accessors/ModelNode.js +++ b/src/nodes/accessors/ModelNode.js @@ -1,10 +1,13 @@ import Object3DNode from './Object3DNode.js'; +import { Matrix4 } from '../../math/Matrix4.js'; +import { Matrix3 } from '../../math/Matrix3.js'; + import { Fn, nodeImmutable } from '../tsl/TSLBase.js'; import { uniform } from '../core/UniformNode.js'; - -import { Matrix4 } from '../../math/Matrix4.js'; +import { buffer } from './BufferNode.js'; +import { OnObjectUpdate } from '../utils/EventNode.js'; import { cameraViewMatrix } from './Camera.js'; -import { Matrix3 } from '../../math/Matrix3.js'; +import { objectIndex } from '../core/IndexNode.js'; /** * This type of node is a specialized version of `Object3DNode` @@ -59,13 +62,53 @@ export default ModelNode; */ export const modelDirection = /*@__PURE__*/ nodeImmutable( ModelNode, ModelNode.DIRECTION ); +/** + * TSL object that represents the object's world matrix in `mediump` precision. + * + * @tsl + * @type {ModelNode} + */ +export const mediumpModelWorldMatrix = /*@__PURE__*/ nodeImmutable( ModelNode, ModelNode.WORLD_MATRIX ); + /** * TSL object that represents the object's world matrix. * * @tsl * @type {ModelNode} */ -export const modelWorldMatrix = /*@__PURE__*/ nodeImmutable( ModelNode, ModelNode.WORLD_MATRIX ); +export const modelWorldMatrix = /*@__PURE__*/ ( Fn( ( builder ) => { + + if ( ! builder.instances ) return mediumpModelWorldMatrix; + + const count = builder.getCount(); + + const matrixArray = new Float32Array( count * 16 ); + const worldMatrix = buffer( matrixArray, 'mat4', count ); + + const updateObjects = ( objects ) => { + + for ( let i = 0; i < objects.length; i ++ ) { + + const object = objects[ i ]; + + object.matrixWorld.toArray( matrixArray, i * 16 ); + + } + + }; + + // initial update - just for test purposes + updateObjects( builder.instances ); + + // + + OnObjectUpdate( ( frame ) => updateObjects( frame.instances ) ); + + // + + return worldMatrix.element( objectIndex ); + +} ).once() )().toVar( 'modelWorldMatrix' ); /** * TSL object that represents the object's position in world space. From e5620534c0e7638ca3a1601e249fe64a01174878 Mon Sep 17 00:00:00 2001 From: sunag Date: Sun, 7 Dec 2025 02:32:49 -0300 Subject: [PATCH 05/21] Add `instances` property --- src/nodes/core/NodeFrame.js | 7 +++++++ src/renderers/common/nodes/Nodes.js | 5 +++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/nodes/core/NodeFrame.js b/src/nodes/core/NodeFrame.js index a58c94fb1beec9..433fadb79b0425 100644 --- a/src/nodes/core/NodeFrame.js +++ b/src/nodes/core/NodeFrame.js @@ -106,6 +106,13 @@ class NodeFrame { */ this.scene = null; + /** + * A reference to the current instances array. + * @type {?Array} + * @default null + */ + this.instances = null; + } /** diff --git a/src/renderers/common/nodes/Nodes.js b/src/renderers/common/nodes/Nodes.js index 2ae98912e4647d..db33f5315a43de 100644 --- a/src/renderers/common/nodes/Nodes.js +++ b/src/renderers/common/nodes/Nodes.js @@ -662,7 +662,7 @@ class Nodes extends DataMap { } - getNodeFrame( renderer = this.renderer, scene = null, object = null, camera = null, material = null ) { + getNodeFrame( renderer = this.renderer, scene = null, object = null, camera = null, material = null, instances = null ) { const nodeFrame = this.nodeFrame; nodeFrame.renderer = renderer; @@ -670,6 +670,7 @@ class Nodes extends DataMap { nodeFrame.object = object; nodeFrame.camera = camera; nodeFrame.material = material; + nodeFrame.instances = instances; return nodeFrame; @@ -677,7 +678,7 @@ class Nodes extends DataMap { getNodeFrameForRender( renderObject ) { - return this.getNodeFrame( renderObject.renderer, renderObject.scene, renderObject.object, renderObject.camera, renderObject.material ); + return this.getNodeFrame( renderObject.renderer, renderObject.scene, renderObject.object, renderObject.camera, renderObject.material, renderObject.instances ); } From 1afa9d610140fdae2baee187f14d04941dd1ffde Mon Sep 17 00:00:00 2001 From: sunag Date: Sun, 7 Dec 2025 02:32:59 -0300 Subject: [PATCH 06/21] fix first renderItem object --- src/renderers/common/Renderer.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/renderers/common/Renderer.js b/src/renderers/common/Renderer.js index bdbc440ed8004b..c2056a1e0f6620 100644 --- a/src/renderers/common/Renderer.js +++ b/src/renderers/common/Renderer.js @@ -2487,7 +2487,7 @@ class Renderer { } else { - map.renderItem.instances = map.renderItem.instances || []; + map.renderItem.instances = map.renderItem.instances || [ map.renderItem.object ]; map.renderItem.instances.push( renderItem.object ); } From 0983f2670192bb17cb0776f6c9c2fd5a7d444d5c Mon Sep 17 00:00:00 2001 From: sunag Date: Sun, 7 Dec 2025 02:33:05 -0300 Subject: [PATCH 07/21] add `count` --- src/renderers/common/RenderObject.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/renderers/common/RenderObject.js b/src/renderers/common/RenderObject.js index 0556972c99775a..f978f57331c879 100644 --- a/src/renderers/common/RenderObject.js +++ b/src/renderers/common/RenderObject.js @@ -128,6 +128,13 @@ class RenderObject { */ this.instances = instances; + /** + * The count of instances. + * + * @type {number} + */ + this.count = instances !== null ? instances.length : 1; + /** * The draw range of the geometry. * @@ -703,7 +710,7 @@ class RenderObject { } - if ( this.instances !== null ) { + if ( this.instances !== null && this.instances.length > this.count ) { const base = Math.ceil( this.instances.length / 8 ); From e757ea0cfb8f29712229a3fb3ef67d5d815ef361 Mon Sep 17 00:00:00 2001 From: sunag Date: Sun, 7 Dec 2025 02:33:49 -0300 Subject: [PATCH 08/21] get buffer count --- src/nodes/core/NodeBuilder.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/nodes/core/NodeBuilder.js b/src/nodes/core/NodeBuilder.js index fa1a6b310b6290..1903810ab6ac12 100644 --- a/src/nodes/core/NodeBuilder.js +++ b/src/nodes/core/NodeBuilder.js @@ -2881,6 +2881,14 @@ class NodeBuilder { } + getCount() { + + const count = this.instances.length; + + return Math.ceil( count / 8 ) * 8; + + } + /** * Central build method which controls the build for the given object. * From 160f538503e8e33f0d44f31390c50fcb00c16f3c Mon Sep 17 00:00:00 2001 From: sunag Date: Sun, 7 Dec 2025 02:34:05 -0300 Subject: [PATCH 09/21] add new logic --- src/renderers/webgpu/WebGPUBackend.js | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/src/renderers/webgpu/WebGPUBackend.js b/src/renderers/webgpu/WebGPUBackend.js index 93cdc4cc9f6a93..4985eec9320423 100644 --- a/src/renderers/webgpu/WebGPUBackend.js +++ b/src/renderers/webgpu/WebGPUBackend.js @@ -1593,7 +1593,7 @@ class WebGPUBackend extends Backend { } else if ( hasIndex === true ) { - const { vertexCount: indexCount, instanceCount, firstVertex: firstIndex } = drawParams; + const { vertexCount: indexCount, instanceCount, firstVertex: firstIndex, firstInstance } = drawParams; const indirect = renderObject.getIndirect(); @@ -1605,7 +1605,7 @@ class WebGPUBackend extends Backend { } else { - passEncoderGPU.drawIndexed( indexCount, instanceCount, firstIndex, 0, 0 ); + passEncoderGPU.drawIndexed( indexCount, instanceCount, firstIndex, 0, firstInstance ); } @@ -1747,7 +1747,22 @@ class WebGPUBackend extends Backend { } - draw( renderContextData.currentPass, renderContextData.currentSets ); + if ( renderObject.instances ) { + + for ( let i = 0; i < renderObject.instances.length; i ++ ) { + + drawParams.instanceCount = 1; + drawParams.firstInstance = i; + + draw( renderContextData.currentPass, renderContextData.currentSets ); + + } + + } else { + + draw( renderContextData.currentPass, renderContextData.currentSets ); + + } } From 745c1029c6bc864793f13dbd14f98844b1191ab9 Mon Sep 17 00:00:00 2001 From: sunag Date: Sun, 7 Dec 2025 02:34:11 -0300 Subject: [PATCH 10/21] Update NodeMaterial.js --- src/materials/nodes/NodeMaterial.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/materials/nodes/NodeMaterial.js b/src/materials/nodes/NodeMaterial.js index 2a8bf37ef63f51..f6f252d4ad5c44 100644 --- a/src/materials/nodes/NodeMaterial.js +++ b/src/materials/nodes/NodeMaterial.js @@ -845,7 +845,7 @@ class NodeMaterial extends Material { } else if ( builder.instances !== null ) { - console.log( '>>', builder.instances ); + console.log( '>>', builder.instances.length ); } From bb4726bf09c7b44ffbb92892bdbaa7d767da5575 Mon Sep 17 00:00:00 2001 From: sunag Date: Sun, 7 Dec 2025 03:09:00 -0300 Subject: [PATCH 11/21] cleanup --- src/nodes/accessors/StorageBufferNode.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/nodes/accessors/StorageBufferNode.js b/src/nodes/accessors/StorageBufferNode.js index 6ebad4a219d74d..2226968e9ddf99 100644 --- a/src/nodes/accessors/StorageBufferNode.js +++ b/src/nodes/accessors/StorageBufferNode.js @@ -397,7 +397,7 @@ export default StorageBufferNode; * @param {number} [count=0] - The buffer count. * @returns {StorageBufferNode} */ -export const storage = ( value, type = null, count = 0 ) => nodeObject( new StorageBufferNode( value, type, count ) ); +export const storage = ( value, type = null, count = 0 ) => new StorageBufferNode( value, type, count ); /** * @tsl From 9c69b218cf6be89c1f9986023ccad307c922e477 Mon Sep 17 00:00:00 2001 From: sunag Date: Sun, 7 Dec 2025 03:09:52 -0300 Subject: [PATCH 12/21] use attribute as fallback --- src/nodes/accessors/ModelNode.js | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/nodes/accessors/ModelNode.js b/src/nodes/accessors/ModelNode.js index c10af34d03c468..d268ef03f83ee7 100644 --- a/src/nodes/accessors/ModelNode.js +++ b/src/nodes/accessors/ModelNode.js @@ -8,6 +8,9 @@ import { buffer } from './BufferNode.js'; import { OnObjectUpdate } from '../utils/EventNode.js'; import { cameraViewMatrix } from './Camera.js'; import { objectIndex } from '../core/IndexNode.js'; +import { instancedDynamicBufferAttribute } from '../accessors/BufferAttributeNode.js'; +import { instancedArray } from '../accessors/Arrays.js'; +import { DynamicDrawUsage } from '../../constants.js'; /** * This type of node is a specialized version of `Object3DNode` @@ -82,8 +85,20 @@ export const modelWorldMatrix = /*@__PURE__*/ ( Fn( ( builder ) => { const count = builder.getCount(); + let worldMatrix; + const matrixArray = new Float32Array( count * 16 ); - const worldMatrix = buffer( matrixArray, 'mat4', count ); + + if ( count < 1000 ) { + + worldMatrix = buffer( matrixArray, 'mat4', count ); + + } else { + + worldMatrix = instancedArray( matrixArray, 'mat4' ); + worldMatrix.value.setUsage( DynamicDrawUsage ); + + } const updateObjects = ( objects ) => { From 1eb6d6d0c4dbb65e1f2652e9b7fc69c4cda7c439 Mon Sep 17 00:00:00 2001 From: sunag Date: Sun, 7 Dec 2025 03:10:57 -0300 Subject: [PATCH 13/21] add examples for debug propose --- examples/webgpu_sprites_debug.html | 208 +++++++++++++++++++++++++++++ examples/webgpu_sprites_old.html | 208 +++++++++++++++++++++++++++++ 2 files changed, 416 insertions(+) create mode 100644 examples/webgpu_sprites_debug.html create mode 100644 examples/webgpu_sprites_old.html diff --git a/examples/webgpu_sprites_debug.html b/examples/webgpu_sprites_debug.html new file mode 100644 index 00000000000000..577a01b9f15129 --- /dev/null +++ b/examples/webgpu_sprites_debug.html @@ -0,0 +1,208 @@ + + + three.js webgpu - sprites + + + + + + +
+ + +
+ three.jsSprites +
+ + + Example of using a custom SpriteMaterial with nodes. + +
+ + + + + + diff --git a/examples/webgpu_sprites_old.html b/examples/webgpu_sprites_old.html new file mode 100644 index 00000000000000..e12669a29b3440 --- /dev/null +++ b/examples/webgpu_sprites_old.html @@ -0,0 +1,208 @@ + + + three.js webgpu - sprites + + + + + + +
+ + +
+ three.jsSprites +
+ + + Example of using a custom SpriteMaterial with nodes. + +
+ + + + + + From 2950c64b2e03848e5e4872c68dae1d176a615f94 Mon Sep 17 00:00:00 2001 From: sunag Date: Sun, 7 Dec 2025 04:06:10 -0300 Subject: [PATCH 14/21] revision --- src/materials/nodes/NodeMaterial.js | 4 ++++ src/nodes/accessors/ModelNode.js | 26 ++++++++++++++++++++++---- src/renderers/common/RenderObject.js | 14 ++++++++++++++ src/renderers/common/RenderObjects.js | 7 +++++++ src/renderers/common/Renderer.js | 1 - src/renderers/webgpu/WebGPUBackend.js | 6 ++++-- 6 files changed, 51 insertions(+), 7 deletions(-) diff --git a/src/materials/nodes/NodeMaterial.js b/src/materials/nodes/NodeMaterial.js index 433a2d19aaba30..785569a538e62b 100644 --- a/src/materials/nodes/NodeMaterial.js +++ b/src/materials/nodes/NodeMaterial.js @@ -847,6 +847,10 @@ class NodeMaterial extends Material { console.log( '>>', builder.instances.length ); + } else { + + console.log( '>> no instances' ); + } if ( this.positionNode !== null ) { diff --git a/src/nodes/accessors/ModelNode.js b/src/nodes/accessors/ModelNode.js index d268ef03f83ee7..036b13b9f0ba94 100644 --- a/src/nodes/accessors/ModelNode.js +++ b/src/nodes/accessors/ModelNode.js @@ -112,12 +112,30 @@ export const modelWorldMatrix = /*@__PURE__*/ ( Fn( ( builder ) => { }; - // initial update - just for test purposes - updateObjects( builder.instances ); - // - OnObjectUpdate( ( frame ) => updateObjects( frame.instances ) ); + OnObjectUpdate( ( frame ) => { + + const objects = frame.instances; + + if ( objects ) { + + for ( let i = 0; i < objects.length; i ++ ) { + + const object = objects[ i ]; + + object.matrixWorld.toArray( matrixArray, i * 16 ); + + } + + } else { + + frame.object.matrixWorld.toArray( matrixArray, 0 ); + + } + + + } ); // diff --git a/src/renderers/common/RenderObject.js b/src/renderers/common/RenderObject.js index 3ed6147827056c..35004f6fadd198 100644 --- a/src/renderers/common/RenderObject.js +++ b/src/renderers/common/RenderObject.js @@ -351,6 +351,20 @@ class RenderObject { } + getDrawInstances() { + + const builderInstances = this.getNodeBuilderState().instances; + + if ( builderInstances !== null && this.instances === null ) { + + this.instances = [ this.object ]; + + } + + return this.instances; + + } + /** * Returns the node builder state of this render object. * diff --git a/src/renderers/common/RenderObjects.js b/src/renderers/common/RenderObjects.js index 8a23fc89b40f76..03984f0f788eb8 100644 --- a/src/renderers/common/RenderObjects.js +++ b/src/renderers/common/RenderObjects.js @@ -134,6 +134,13 @@ class RenderObjects { } + // + + renderObject.object = object; + renderObject.instances = instances; + + // + _chainKeys.length = 0; return renderObject; diff --git a/src/renderers/common/Renderer.js b/src/renderers/common/Renderer.js index e9b3d5e569b9c5..273b3f4f73f438 100644 --- a/src/renderers/common/Renderer.js +++ b/src/renderers/common/Renderer.js @@ -3265,7 +3265,6 @@ class Renderer { _renderObjectDirect( object, material, scene, camera, lightsNode, group, clippingContext, passId, instances = null ) { const renderObject = this._objects.get( object, material, scene, camera, lightsNode, this._currentRenderContext, clippingContext, passId, instances ); - renderObject.instances = instances; renderObject.drawRange = object.geometry.drawRange; renderObject.group = group; diff --git a/src/renderers/webgpu/WebGPUBackend.js b/src/renderers/webgpu/WebGPUBackend.js index e47ebdd50a6c2c..f50b6f974d0d82 100644 --- a/src/renderers/webgpu/WebGPUBackend.js +++ b/src/renderers/webgpu/WebGPUBackend.js @@ -1764,9 +1764,11 @@ class WebGPUBackend extends Backend { } - if ( renderObject.instances ) { + const drawInstances = renderObject.getDrawInstances(); - for ( let i = 0; i < renderObject.instances.length; i ++ ) { + if ( drawInstances ) { + + for ( let i = 0; i < drawInstances.length; i ++ ) { drawParams.instanceCount = 1; drawParams.firstInstance = i; From 33e1d0fd8b25d8efb7c8558b2886e4887faa2d45 Mon Sep 17 00:00:00 2001 From: sunag Date: Sun, 7 Dec 2025 22:50:33 -0300 Subject: [PATCH 15/21] revisions --- src/materials/nodes/NodeMaterial.js | 2 +- src/nodes/core/NodeBuilder.js | 6 ++---- src/nodes/core/NodeUtils.js | 6 ++++++ src/renderers/common/RenderObject.js | 14 +++++++++----- src/renderers/webgpu/WebGPUBackend.js | 4 ++-- 5 files changed, 20 insertions(+), 12 deletions(-) diff --git a/src/materials/nodes/NodeMaterial.js b/src/materials/nodes/NodeMaterial.js index 785569a538e62b..82f8e9718130b1 100644 --- a/src/materials/nodes/NodeMaterial.js +++ b/src/materials/nodes/NodeMaterial.js @@ -845,7 +845,7 @@ class NodeMaterial extends Material { } else if ( builder.instances !== null ) { - console.log( '>>', builder.instances.length ); + console.log( '>>', builder.getCount() ); } else { diff --git a/src/nodes/core/NodeBuilder.js b/src/nodes/core/NodeBuilder.js index e84a5b506cf082..9a72d985e729f0 100644 --- a/src/nodes/core/NodeBuilder.js +++ b/src/nodes/core/NodeBuilder.js @@ -8,7 +8,7 @@ import ParameterNode from './ParameterNode.js'; import StructType from './StructType.js'; import FunctionNode from '../code/FunctionNode.js'; import NodeMaterial from '../../materials/nodes/NodeMaterial.js'; -import { getTypeFromLength } from './NodeUtils.js'; +import { getTypeFromLength, roundInstances } from './NodeUtils.js'; import { NodeUpdateType, defaultBuildStages, shaderStages } from './constants.js'; import { @@ -2888,9 +2888,7 @@ class NodeBuilder { getCount() { - const count = this.instances.length; - - return Math.ceil( count / 8 ) * 8; + return roundInstances( this.instances.length ); } diff --git a/src/nodes/core/NodeUtils.js b/src/nodes/core/NodeUtils.js index 4d08e75b8a9c5c..dff2676d9fec23 100644 --- a/src/nodes/core/NodeUtils.js +++ b/src/nodes/core/NodeUtils.js @@ -345,6 +345,12 @@ export function getValueFromType( type, ...params ) { } +export function roundInstances( count ) { + + return Math.ceil( count / 32 ) * 32; + +} + /** * Gets the object data that can be shared between different rendering steps. * diff --git a/src/renderers/common/RenderObject.js b/src/renderers/common/RenderObject.js index 35004f6fadd198..22ac6727f4c89d 100644 --- a/src/renderers/common/RenderObject.js +++ b/src/renderers/common/RenderObject.js @@ -1,4 +1,4 @@ -import { hash } from '../../nodes/core/NodeUtils.js'; +import { hash, roundInstances } from '../../nodes/core/NodeUtils.js'; import { getGeometryCacheKey, getProgramCacheKey } from './RenderObjectUtils.js'; let _id = 0; @@ -133,7 +133,7 @@ class RenderObject { * * @type {number} */ - this.count = instances !== null ? instances.length : 1; + this.count = instances !== null ? roundInstances( instances.length ) : 1; /** * The draw range of the geometry. @@ -735,15 +735,19 @@ class RenderObject { } + let count; + if ( this.instances !== null && this.instances.length > this.count ) { - const base = Math.ceil( this.instances.length / 8 ); + count = roundInstances( this.instances.length ); + + } else { - cacheKey = hash( cacheKey, base ); + count = this.count; } - cacheKey = hash( cacheKey, this.camera.id, this.renderer.contextNode.id, this.renderer.contextNode.version ); + cacheKey = hash( cacheKey, count, this.camera.id, this.renderer.contextNode.id, this.renderer.contextNode.version ); return cacheKey; diff --git a/src/renderers/webgpu/WebGPUBackend.js b/src/renderers/webgpu/WebGPUBackend.js index f50b6f974d0d82..784d520957260a 100644 --- a/src/renderers/webgpu/WebGPUBackend.js +++ b/src/renderers/webgpu/WebGPUBackend.js @@ -1621,7 +1621,7 @@ class WebGPUBackend extends Backend { } else { - const { vertexCount, instanceCount, firstVertex } = drawParams; + const { vertexCount, instanceCount, firstVertex, firstInstance } = drawParams; const indirect = renderObject.getIndirect(); @@ -1640,7 +1640,7 @@ class WebGPUBackend extends Backend { } else { - passEncoderGPU.draw( vertexCount, instanceCount, firstVertex, 0 ); + passEncoderGPU.draw( vertexCount, instanceCount, firstVertex, firstInstance ); } From 6b6640dde2e8376a29eafb588e6684bf97fa6cbb Mon Sep 17 00:00:00 2001 From: sunag Date: Sun, 7 Dec 2025 22:50:39 -0300 Subject: [PATCH 16/21] Introduce `mediumpModelNormalMatrix` --- src/nodes/accessors/ModelNode.js | 79 +++++++++++++++++++++++++------- 1 file changed, 63 insertions(+), 16 deletions(-) diff --git a/src/nodes/accessors/ModelNode.js b/src/nodes/accessors/ModelNode.js index 036b13b9f0ba94..bffba603c1e6dc 100644 --- a/src/nodes/accessors/ModelNode.js +++ b/src/nodes/accessors/ModelNode.js @@ -84,11 +84,10 @@ export const modelWorldMatrix = /*@__PURE__*/ ( Fn( ( builder ) => { if ( ! builder.instances ) return mediumpModelWorldMatrix; const count = builder.getCount(); + const matrixArray = new Float32Array( count * 16 ); let worldMatrix; - const matrixArray = new Float32Array( count * 16 ); - if ( count < 1000 ) { worldMatrix = buffer( matrixArray, 'mat4', count ); @@ -100,18 +99,6 @@ export const modelWorldMatrix = /*@__PURE__*/ ( Fn( ( builder ) => { } - const updateObjects = ( objects ) => { - - for ( let i = 0; i < objects.length; i ++ ) { - - const object = objects[ i ]; - - object.matrixWorld.toArray( matrixArray, i * 16 ); - - } - - }; - // OnObjectUpdate( ( frame ) => { @@ -134,7 +121,6 @@ export const modelWorldMatrix = /*@__PURE__*/ ( Fn( ( builder ) => { } - } ); // @@ -181,7 +167,68 @@ export const modelRadius = /*@__PURE__*/ nodeImmutable( ModelNode, ModelNode.RAD * @tsl * @type {UniformNode} */ -export const modelNormalMatrix = /*@__PURE__*/ uniform( new Matrix3() ).onObjectUpdate( ( { object }, self ) => self.value.getNormalMatrix( object.matrixWorld ) ); +export const mediumpModelNormalMatrix = /*@__PURE__*/ uniform( new Matrix3() ).onObjectUpdate( ( { object }, self ) => self.value.getNormalMatrix( object.matrixWorld ) ); + +/** + * TSL object that represents the object's normal matrix. + * + * @tsl + * @type {UniformNode} + */ +export const modelNormalMatrix = /*@__PURE__*/ ( Fn( ( builder ) => { + + if ( ! builder.instances ) return mediumpModelNormalMatrix; + + const matrix = new Matrix3(); + const offset = 12; + + const count = builder.getCount(); + const matrixArray = new Float32Array( count * offset ); + + let normalMatrix; + + if ( count < 1000 ) { + + normalMatrix = buffer( matrixArray, 'mat3', count ); + + } else { + + normalMatrix = instancedArray( matrixArray, 'mat3' ); + normalMatrix.value.setUsage( DynamicDrawUsage ); + + } + + // + + OnObjectUpdate( ( frame ) => { + + const objects = frame.instances; + + if ( objects ) { + + for ( let i = 0; i < objects.length; i ++ ) { + + const object = objects[ i ]; + + matrix.getNormalMatrix( object.matrixWorld ); + matrix.toArray( matrixArray, i * offset ); + + } + + } else { + + matrix.getNormalMatrix( frame.object.matrixWorld ); + matrix.toArray( matrixArray, 0 ); + + } + + } ); + + // + + return normalMatrix.element( objectIndex ); + +} ).once() )().toVar( 'modelNormalMatrix' ); /** * TSL object that represents the object's inverse world matrix. From c11e7de7bc4a812385cdd6e0b1c5148772459d38 Mon Sep 17 00:00:00 2001 From: sunag Date: Mon, 8 Dec 2025 19:47:32 -0300 Subject: [PATCH 17/21] Update ModelNode.js --- src/nodes/accessors/ModelNode.js | 29 +++++++++++++++++++++++++---- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/src/nodes/accessors/ModelNode.js b/src/nodes/accessors/ModelNode.js index bffba603c1e6dc..2cd9d0dccbb5fe 100644 --- a/src/nodes/accessors/ModelNode.js +++ b/src/nodes/accessors/ModelNode.js @@ -200,25 +200,46 @@ export const modelNormalMatrix = /*@__PURE__*/ ( Fn( ( builder ) => { // + function updateObject( object, index ) { + + matrix.getNormalMatrix( object.matrixWorld ); + + matrixArray[ index * offset + 0 ] = matrix.elements[ 0 ]; + matrixArray[ index * offset + 1 ] = matrix.elements[ 1 ]; + matrixArray[ index * offset + 2 ] = matrix.elements[ 2 ]; + matrixArray[ index * offset + 3 ] = 0; + matrixArray[ index * offset + 4 ] = matrix.elements[ 3 ]; + matrixArray[ index * offset + 5 ] = matrix.elements[ 4 ]; + matrixArray[ index * offset + 6 ] = matrix.elements[ 5 ]; + matrixArray[ index * offset + 7 ] = 0; + matrixArray[ index * offset + 8 ] = matrix.elements[ 6 ]; + matrixArray[ index * offset + 9 ] = matrix.elements[ 7 ]; + matrixArray[ index * offset + 10 ] = matrix.elements[ 8 ]; + matrixArray[ index * offset + 11 ] = 0; + + } + OnObjectUpdate( ( frame ) => { const objects = frame.instances; if ( objects ) { - for ( let i = 0; i < objects.length; i ++ ) { + for ( let index = 0; index < objects.length; index ++ ) { - const object = objects[ i ]; + const object = objects[ index ]; matrix.getNormalMatrix( object.matrixWorld ); - matrix.toArray( matrixArray, i * offset ); + + updateObject( object, index ); } } else { matrix.getNormalMatrix( frame.object.matrixWorld ); - matrix.toArray( matrixArray, 0 ); + + updateObject( frame.object, 0 ); } From 4a54aa2d26101abba0b5ddf121eb052b9ff3b0c6 Mon Sep 17 00:00:00 2001 From: sunag Date: Mon, 8 Dec 2025 20:47:53 -0300 Subject: [PATCH 18/21] Update WebGLBackend.js --- src/renderers/webgl-fallback/WebGLBackend.js | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/renderers/webgl-fallback/WebGLBackend.js b/src/renderers/webgl-fallback/WebGLBackend.js index 167ef97af0cb0f..51fc5162ccae02 100644 --- a/src/renderers/webgl-fallback/WebGLBackend.js +++ b/src/renderers/webgl-fallback/WebGLBackend.js @@ -939,7 +939,7 @@ class WebGLBackend extends Backend { */ draw( renderObject/*, info*/ ) { - const { object, pipeline, material, context, hardwareClippingPlanes } = renderObject; + const { object, pipeline, material, context, hardwareClippingPlanes, instances } = renderObject; const { programGPU } = this.get( pipeline ); const { gl, state } = this; @@ -1094,7 +1094,15 @@ class WebGLBackend extends Backend { } else { - renderer.render( firstVertex, vertexCount ); + if ( instances !== null ) { + + renderer.renderInstances( firstVertex, vertexCount, instances.length ); + + } else { + + renderer.render( firstVertex, vertexCount ); + + } } From b643bcee307624f743a760bcd599d26c23a56f3b Mon Sep 17 00:00:00 2001 From: sunag Date: Mon, 8 Dec 2025 21:22:38 -0300 Subject: [PATCH 19/21] ignore multi-material --- src/renderers/common/Renderer.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/renderers/common/Renderer.js b/src/renderers/common/Renderer.js index 273b3f4f73f438..25496abfcab562 100644 --- a/src/renderers/common/Renderer.js +++ b/src/renderers/common/Renderer.js @@ -2478,7 +2478,7 @@ class Renderer { } - if ( map.renderId !== renderId ) { + if ( map.renderId !== renderId || Array.isArray( renderItem.object.material ) ) { map.renderId = renderId; map.renderItem = renderItem; From 3344a4a4dfb98ded34ad8aa54c4073955c4d1e03 Mon Sep 17 00:00:00 2001 From: sunag Date: Tue, 9 Dec 2025 00:41:25 -0300 Subject: [PATCH 20/21] Update RenderObjects.js --- src/renderers/common/RenderObjects.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/renderers/common/RenderObjects.js b/src/renderers/common/RenderObjects.js index 03984f0f788eb8..2ad1b03979bcdc 100644 --- a/src/renderers/common/RenderObjects.js +++ b/src/renderers/common/RenderObjects.js @@ -93,7 +93,7 @@ class RenderObjects { const chainMap = this.getChainMap( passId ); // reuse chainArray - _chainKeys[ 0 ] = object.geometry; + _chainKeys[ 0 ] = instances !== null ? object.geometry : object; _chainKeys[ 1 ] = material; _chainKeys[ 2 ] = renderContext; _chainKeys[ 3 ] = lightsNode; From 2702bbde3880c750d08a4460e9ededa5b23a65f9 Mon Sep 17 00:00:00 2001 From: sunag Date: Tue, 9 Dec 2025 01:20:54 -0300 Subject: [PATCH 21/21] Update WebGPUBackend.js --- src/renderers/webgpu/WebGPUBackend.js | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/renderers/webgpu/WebGPUBackend.js b/src/renderers/webgpu/WebGPUBackend.js index 784d520957260a..ee2ecbb4e43c83 100644 --- a/src/renderers/webgpu/WebGPUBackend.js +++ b/src/renderers/webgpu/WebGPUBackend.js @@ -1768,14 +1768,10 @@ class WebGPUBackend extends Backend { if ( drawInstances ) { - for ( let i = 0; i < drawInstances.length; i ++ ) { + drawParams.firstInstance = 0; + drawParams.instanceCount = drawInstances.length; - drawParams.instanceCount = 1; - drawParams.firstInstance = i; - - draw( renderContextData.currentPass, renderContextData.currentSets ); - - } + draw( renderContextData.currentPass, renderContextData.currentSets ); } else {