@threenative/core 0.3.2 → 0.3.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,5 @@
1
1
  diff --git a/build/three.cjs b/build/three.cjs
2
- index 8939a67dd76325ccd25ee8806fc4d22b1caea88a..b0f917241f1f2cd25baa21dec20edda32176ef6a 100644
2
+ index 8939a67..b0f9172 100644
3
3
  --- a/build/three.cjs
4
4
  +++ b/build/three.cjs
5
5
  @@ -26116,6 +26116,8 @@ function copyArrayContents( src, target ) {
@@ -12,7 +12,7 @@ index 8939a67dd76325ccd25ee8806fc4d22b1caea88a..b0f917241f1f2cd25baa21dec20edda3
12
12
  * Constructs a new batched mesh.
13
13
  *
14
14
  diff --git a/build/three.core.js b/build/three.core.js
15
- index 0bb9262cd029f411933d077fd44197a51ec5e8e9..868a634b4294be5f32664912a50813d55d6dd6e7 100644
15
+ index 0bb9262..868a634 100644
16
16
  --- a/build/three.core.js
17
17
  +++ b/build/three.core.js
18
18
  @@ -26136,6 +26136,8 @@ function copyArrayContents( src, target ) {
@@ -25,10 +25,277 @@ index 0bb9262cd029f411933d077fd44197a51ec5e8e9..868a634b4294be5f32664912a50813d5
25
25
  * Constructs a new batched mesh.
26
26
  *
27
27
  diff --git a/build/three.webgpu.js b/build/three.webgpu.js
28
- index 2e490796d4d4aa9479b04a5c46ddbf31cc05b1a1..d05599255744ccfca19dbfd0433da612583ce6ea 100644
28
+ index 2e49079..7aa62ad 100644
29
29
  --- a/build/three.webgpu.js
30
30
  +++ b/build/three.webgpu.js
31
- @@ -18500,7 +18500,8 @@ const instance = /*@__PURE__*/ Fn( ( [ matrices, colors = null ], builder ) => {
31
+ @@ -141,6 +141,15 @@ class NodeMaterialObserver {
32
+ */
33
+ this.renderId = 0;
34
+
35
+ + /**
36
+ + * Whether the current render produces motion vectors. Objects keep refreshing
37
+ + * fully while this is the case.
38
+ + *
39
+ + * @type {boolean}
40
+ + * @default false
41
+ + */
42
+ + this.renderingVelocity = false;
43
+ +
44
+ }
45
+
46
+ /**
47
+ @@ -240,6 +249,88 @@ class NodeMaterialObserver {
48
+
49
+ }
50
+
51
+ + /**
52
+ + * Returns `true` if the given render object is a static object whose per-object state - world
53
+ + * matrix, material, geometry and the scene environment - did not change since the last
54
+ + * refresh. A settled object is still drawn every frame and its FRAME and RENDER nodes still
55
+ + * have to be updated; what it may skip is the work whose inputs it froze: its OBJECT node
56
+ + * updates, its object uniform group upload and its per-draw attribute scan.
57
+ + *
58
+ + * @param {RenderObject} renderObject - The render object.
59
+ + * @return {boolean} Whether the given render object is settled or not.
60
+ + */
61
+ + isSettled( renderObject ) {
62
+ +
63
+ + const data = this.renderObjects.get( renderObject );
64
+ +
65
+ + return data !== undefined && data.settledState === true;
66
+ +
67
+ + }
68
+ +
69
+ + /**
70
+ + * Recomputes {@link NodeMaterialObserver#isSettled} for the given render object, once per draw,
71
+ + * so every step of that draw reads the same answer.
72
+ + *
73
+ + * @param {RenderObject} renderObject - The render object.
74
+ + * @return {boolean} Whether the given render object is settled or not.
75
+ + */
76
+ + updateSettled( renderObject ) {
77
+ +
78
+ + const { object, material, geometry, scene } = renderObject;
79
+ + const data = this.getRenderObjectData( renderObject );
80
+ +
81
+ + if ( object.static !== true || this.renderingVelocity === true ) {
82
+ +
83
+ + data.settledState = false;
84
+ +
85
+ + return false;
86
+ +
87
+ + }
88
+ +
89
+ + let settled = data.settled;
90
+ +
91
+ + if ( settled === undefined ) {
92
+ +
93
+ + data.settled = settled = {
94
+ + matrix: object.matrixWorld.clone(),
95
+ + environmentRotation: scene.environmentRotation.clone(),
96
+ + environmentIntensity: scene.environmentIntensity,
97
+ + materialVersion: material.version,
98
+ + geometryVersion: geometry.version
99
+ + };
100
+ +
101
+ + data.settledState = false;
102
+ +
103
+ + return false;
104
+ +
105
+ + }
106
+ +
107
+ + if ( settled.matrix.equals( object.matrixWorld ) !== true ||
108
+ + settled.environmentRotation.equals( scene.environmentRotation ) !== true ||
109
+ + settled.environmentIntensity !== scene.environmentIntensity ||
110
+ + settled.materialVersion !== material.version ||
111
+ + settled.geometryVersion !== geometry.version ) {
112
+ +
113
+ + // re-arm with the current state, so the object settles again once it stops changing
114
+ +
115
+ + settled.matrix.copy( object.matrixWorld );
116
+ + settled.environmentRotation.copy( scene.environmentRotation );
117
+ + settled.environmentIntensity = scene.environmentIntensity;
118
+ + settled.materialVersion = material.version;
119
+ + settled.geometryVersion = geometry.version;
120
+ +
121
+ + data.settledState = false;
122
+ +
123
+ + return false;
124
+ +
125
+ + }
126
+ +
127
+ + data.settledState = true;
128
+ +
129
+ + return true;
130
+ +
131
+ + }
132
+ +
133
+ /**
134
+ * Returns an attribute data structure holding the attributes versions for
135
+ * monitoring.
136
+ @@ -306,7 +397,6 @@ class NodeMaterialObserver {
137
+
138
+ data = {
139
+ _renderId: -1,
140
+ - _equal: false,
141
+
142
+ attributes: this.getAttributesData( geometry.attributes ),
143
+ indexId: geometry.index ? geometry.index.id : null,
144
+ @@ -335,7 +425,7 @@ class NodeMaterialObserver {
145
+
146
+ if ( data === undefined ) {
147
+
148
+ - data = { _renderId: -1, _equal: false };
149
+ + data = { _renderId: -1 };
150
+
151
+ for ( const property of this.refreshUniforms ) {
152
+
153
+ @@ -419,7 +509,6 @@ class NodeMaterialObserver {
154
+
155
+ value.copy( mtlValue );
156
+
157
+ - materialData._equal = false;
158
+ return false;
159
+
160
+ }
161
+ @@ -431,7 +520,6 @@ class NodeMaterialObserver {
162
+ value.id = mtlValue.id;
163
+ value.version = mtlValue.version;
164
+
165
+ - materialData._equal = false;
166
+ return false;
167
+
168
+ }
169
+ @@ -440,7 +528,6 @@ class NodeMaterialObserver {
170
+
171
+ materialData[ property ] = mtlValue;
172
+
173
+ - materialData._equal = false;
174
+ return false;
175
+
176
+ }
177
+ @@ -456,18 +543,12 @@ class NodeMaterialObserver {
178
+ renderObjectData.bufferWidth = width;
179
+ renderObjectData.bufferHeight = height;
180
+
181
+ - materialData._equal = false;
182
+ return false;
183
+
184
+ }
185
+
186
+ }
187
+
188
+ - materialData._equal = true;
189
+ -
190
+ - } else {
191
+ -
192
+ - if ( materialData._equal === false ) return false;
193
+
194
+ }
195
+
196
+ @@ -510,7 +591,6 @@ class NodeMaterialObserver {
197
+ // attribute was removed
198
+ delete storedAttributes[ name ];
199
+
200
+ - geometryData._equal = false;
201
+ return false;
202
+
203
+ }
204
+ @@ -523,7 +603,6 @@ class NodeMaterialObserver {
205
+ storedAttributeData.id = id;
206
+ storedAttributeData.version = version;
207
+
208
+ - geometryData._equal = false;
209
+ return false;
210
+
211
+ }
212
+ @@ -534,7 +613,6 @@ class NodeMaterialObserver {
213
+
214
+ geometryData.attributes = this.getAttributesData( attributes );
215
+
216
+ - geometryData._equal = false;
217
+ return false;
218
+
219
+ }
220
+ @@ -552,7 +630,6 @@ class NodeMaterialObserver {
221
+ geometryData.indexId = currentIndexId;
222
+ geometryData.indexVersion = currentIndexVersion;
223
+
224
+ - geometryData._equal = false;
225
+ return false;
226
+
227
+ }
228
+ @@ -564,16 +641,10 @@ class NodeMaterialObserver {
229
+ geometryData.drawRange.start = geometry.drawRange.start;
230
+ geometryData.drawRange.count = geometry.drawRange.count;
231
+
232
+ - geometryData._equal = false;
233
+ return false;
234
+
235
+ }
236
+
237
+ - geometryData._equal = true;
238
+ -
239
+ - } else {
240
+ -
241
+ - if ( geometryData._equal === false ) return false;
242
+
243
+ }
244
+
245
+ @@ -724,7 +795,14 @@ class NodeMaterialObserver {
246
+ */
247
+ needsRefresh( renderObject, nodeFrame ) {
248
+
249
+ - if ( this.hasNode || this.hasAnimation || this.firstInitialization( renderObject ) || this.needsVelocity( nodeFrame.renderer ) )
250
+ + this.renderingVelocity = this.needsVelocity( nodeFrame.renderer );
251
+ +
252
+ + // Answered for the whole draw, so updateBefore, the geometry scan, the node updates and
253
+ + // the object uniform upload of this object all skip or all refresh.
254
+ +
255
+ + this.updateSettled( renderObject );
256
+ +
257
+ + if ( this.hasNode || this.hasAnimation || this.firstInitialization( renderObject ) || this.renderingVelocity )
258
+ return true;
259
+
260
+ const { renderId } = nodeFrame;
261
+ @@ -737,10 +815,14 @@ class NodeMaterialObserver {
262
+
263
+ }
264
+
265
+ - const isStatic = renderObject.object.static === true;
266
+ const isBundle = renderObject.bundle !== null && renderObject.bundle.static === true && this.getRenderObjectData( renderObject ).version === renderObject.bundle.version;
267
+
268
+ - if ( isStatic || isBundle )
269
+ + // `object.static` is not the question - it is a switch, and a brand new object has it set
270
+ + // before it has been drawn once, so answering from it skips the draw that creates this
271
+ + // object's bind groups and leaves the pipeline reading a layout that was never built. A
272
+ + // static object that moved, or whose material or geometry was edited, has to refresh like
273
+ + // any other; only a settled one may skip, and nothing is settled before its first refresh.
274
+ + if ( this.isSettled( renderObject ) === true || isBundle )
275
+ return false;
276
+
277
+ const lightsData = this.getLights( renderObject.lightsNode, renderId );
278
+ @@ -18320,7 +18402,8 @@ function createInstanceMatrixNode( builder, instanceMatrix ) {
279
+
280
+ const uniformBufferSize = matrixCount * 16 * 4;
281
+
282
+ - if ( uniformBufferSize <= builder.getUniformBufferLimit() ) {
283
+ + // threenative: the uniform path re-uploads every draw; the attribute path uploads on version change
284
+ + if ( false ) {
285
+
286
+ instanceMatrixNode = buffer( instanceMatrix.array, 'mat4', matrixCount ).element( instanceIndex );
287
+
288
+ @@ -18414,7 +18497,8 @@ const instance = /*@__PURE__*/ Fn( ( [ matrices, colors = null ], builder ) => {
289
+
290
+ const uniformBufferSize = Math.max( matrices.count, 1 ) * 16 * 4;
291
+
292
+ - if ( uniformBufferSize > builder.getUniformBufferLimit() ) {
293
+ + // threenative: the uniform path re-uploads every draw; the attribute path uploads on version change
294
+ + if ( true ) {
295
+
296
+ interleavedMatrix = _matrixBuffers.get( matrices );
297
+
298
+ @@ -18500,7 +18584,8 @@ const instance = /*@__PURE__*/ Fn( ( [ matrices, colors = null ], builder ) => {
32
299
 
33
300
  const previousInstanceData = _previousInstanceMatrices.get( object );
34
301
 
@@ -38,7 +305,7 @@ index 2e490796d4d4aa9479b04a5c46ddbf31cc05b1a1..d05599255744ccfca19dbfd0433da612
38
305
 
39
306
  } );
40
307
 
41
- @@ -18599,6 +18600,7 @@ const batch = /*@__PURE__*/ Fn( ( [ batchMesh ], builder ) => {
308
+ @@ -18599,6 +18684,7 @@ const batch = /*@__PURE__*/ Fn( ( [ batchMesh ], builder ) => {
42
309
  const indirectId = getIndirectIndex( batchMesh._indirectTexture, int( batchingIdNode ) );
43
310
 
44
311
  const matricesTexture = batchMesh._matricesTexture;
@@ -46,7 +313,7 @@ index 2e490796d4d4aa9479b04a5c46ddbf31cc05b1a1..d05599255744ccfca19dbfd0433da612
46
313
 
47
314
  const size = int( textureSize( textureLoad( matricesTexture ), 0 ).x ).toConst();
48
315
  const j = float( indirectId ).mul( 4 ).toInt().toConst();
49
- @@ -18626,6 +18628,19 @@ const batch = /*@__PURE__*/ Fn( ( [ batchMesh ], builder ) => {
316
+ @@ -18626,6 +18712,19 @@ const batch = /*@__PURE__*/ Fn( ( [ batchMesh ], builder ) => {
50
317
 
51
318
  positionLocal.assign( batchingMatrix.mul( positionLocal ) );
52
319
 
@@ -66,7 +333,7 @@ index 2e490796d4d4aa9479b04a5c46ddbf31cc05b1a1..d05599255744ccfca19dbfd0433da612
66
333
  const transformedNormal = normalLocal.div( vec3( bm[ 0 ].dot( bm[ 0 ] ), bm[ 1 ].dot( bm[ 1 ] ), bm[ 2 ].dot( bm[ 2 ] ) ) );
67
334
 
68
335
  const batchingNormal = bm.mul( transformedNormal ).xyz;
69
- @@ -18777,7 +18792,8 @@ const skinning = /*@__PURE__*/ Fn( ( [ skinnedMesh ], builder ) => {
336
+ @@ -18777,7 +18876,8 @@ const skinning = /*@__PURE__*/ Fn( ( [ skinnedMesh ], builder ) => {
70
337
 
71
338
  if ( skeletonData !== undefined ) {
72
339
 
@@ -76,7 +343,7 @@ index 2e490796d4d4aa9479b04a5c46ddbf31cc05b1a1..d05599255744ccfca19dbfd0433da612
76
343
 
77
344
  }
78
345
 
79
- @@ -18844,7 +18860,8 @@ const computeSkinning = /*@__PURE__*/ Fn( ( [ skinnedMesh, toPosition = null ],
346
+ @@ -18844,7 +18944,8 @@ const computeSkinning = /*@__PURE__*/ Fn( ( [ skinnedMesh, toPosition = null ],
80
347
 
81
348
  if ( state !== undefined ) {
82
349
 
@@ -86,7 +353,7 @@ index 2e490796d4d4aa9479b04a5c46ddbf31cc05b1a1..d05599255744ccfca19dbfd0433da612
86
353
 
87
354
  }
88
355
 
89
- @@ -29911,6 +29928,18 @@ class RenderObject {
356
+ @@ -29911,6 +30012,18 @@ class RenderObject {
90
357
  */
91
358
  this._sourceMaterial = renderer._currentSourceMaterial;
92
359
 
@@ -105,7 +372,33 @@ index 2e490796d4d4aa9479b04a5c46ddbf31cc05b1a1..d05599255744ccfca19dbfd0433da612
105
372
  /**
106
373
  * An event listener which is defined by `RenderObjects`. It performs
107
374
  * clean up tasks when `dispose()` on this render object.
108
- @@ -30707,9 +30736,18 @@ class RenderObjects {
375
+ @@ -30098,7 +30211,7 @@ class RenderObject {
376
+ */
377
+ getChainArray() {
378
+
379
+ - return [ this.object, this.material, this.context, this.lightsNode ];
380
+ + return [ this.object, this.material, this.context, this.lightsNode, this._sourceMaterial ?? this.material ];
381
+
382
+ }
383
+
384
+ @@ -30548,6 +30661,7 @@ class RenderObject {
385
+ }
386
+
387
+ cacheKey = hash$1( cacheKey, this.renderer.contextNode.id, this.renderer.contextNode.version );
388
+ + cacheKey = hash$1( cacheKey, this.context.sampleCount );
389
+
390
+ return cacheKey;
391
+
392
+ @@ -30680,6 +30794,8 @@ class RenderObjects {
393
+ _chainKeys$3[ 1 ] = material;
394
+ _chainKeys$3[ 2 ] = renderContext;
395
+ _chainKeys$3[ 3 ] = lightsNode;
396
+ + // Material groups share the shadow override, but must retain separate GPU bindings.
397
+ + _chainKeys$3[ 4 ] = this.renderer._currentSourceMaterial ?? material;
398
+
399
+ //
400
+
401
+ @@ -30707,9 +30823,18 @@ class RenderObjects {
109
402
 
110
403
  }
111
404
 
@@ -118,121 +411,1426 @@ index 2e490796d4d4aa9479b04a5c46ddbf31cc05b1a1..d05599255744ccfca19dbfd0433da612
118
411
  + const sourceTracked = sourceMaterial !== null && sourceMaterial !== undefined;
119
412
  + const sourceReplaced = sourceTracked && renderObject._sourceMaterial !== sourceMaterial;
120
413
  + const sourceChanged = sourceTracked && ( sourceReplaced || renderObject.sourceVersion !== sourceMaterial.version );
414
+ +
415
+ + if ( renderObject.version !== material.version || sourceChanged || renderObject.needsUpdate ) {
121
416
 
122
417
  - if ( renderObject.initialCacheKey !== renderObject.getCacheKey() ) {
123
- + if ( renderObject.version !== material.version || sourceChanged || renderObject.needsUpdate ) {
124
- +
125
418
  + if ( sourceReplaced || renderObject.initialCacheKey !== renderObject.getCacheKey() ) {
126
419
 
127
- renderObject.dispose();
420
+ renderObject.dispose();
421
+
422
+ @@ -30719,6 +30844,8 @@ class RenderObjects {
423
+
424
+ renderObject.version = material.version;
425
+
426
+ + if ( sourceTracked ) renderObject.sourceVersion = sourceMaterial.version;
427
+ +
428
+ }
429
+
430
+ }
431
+ @@ -30731,6 +30858,7 @@ class RenderObjects {
432
+ _chainKeys$3[ 1 ] = null;
433
+ _chainKeys$3[ 2 ] = null;
434
+ _chainKeys$3[ 3 ] = null;
435
+ + _chainKeys$3[ 4 ] = null;
436
+
437
+ //
438
+
439
+ @@ -31165,6 +31293,14 @@ class Geometries extends DataMap {
440
+ */
441
+ this._geometryDisposeListeners = new Map();
442
+
443
+ + /**
444
+ + * This Weak Map holds the attribute versions a settled render object was last drawn with,
445
+ + * so a write into one of them is what brings the object back out of the settled path.
446
+ + *
447
+ + * @type {WeakMap<RenderObject,Map<BufferAttribute,number>>}
448
+ + */
449
+ + this._settledAttributes = new WeakMap();
450
+ +
451
+ }
452
+
453
+ /**
454
+ @@ -31190,10 +31326,69 @@ class Geometries extends DataMap {
455
+
456
+ if ( this.has( renderObject ) === false ) this.initGeometry( renderObject );
457
+
458
+ + // A settled static object has no attribute that could have changed since the last refresh,
459
+ + // unless a writer bumped one. An InstancedMesh's instance buffer is a vertex attribute of
460
+ + // this draw, and a BufferGeometry carries no version of its own to announce the write, so
461
+ + // the attributes' own versions are the signal - and the only one there is.
462
+ +
463
+ + const monitor = renderObject.getMonitor();
464
+ +
465
+ + if ( monitor !== undefined && monitor.isSettled( renderObject ) === true && this.attributesChanged( renderObject ) === false ) return;
466
+ +
467
+ this.updateAttributes( renderObject );
468
+
469
+ }
470
+
471
+ + /**
472
+ + * Returns `true` if an attribute of the given render object - or its index - was written to
473
+ + * since the last time its versions were read, and records the versions it holds now.
474
+ + *
475
+ + * @param {RenderObject} renderObject - The render object.
476
+ + * @return {boolean} Whether any of the render object's versions moved.
477
+ + */
478
+ + attributesChanged( renderObject ) {
479
+ +
480
+ + const attributes = renderObject.getAttributes();
481
+ + const index = this.getIndex( renderObject );
482
+ + const settled = this._settledAttributes.get( renderObject );
483
+ +
484
+ + if ( settled !== undefined && settled.size === attributes.length + ( index === null ? 0 : 1 ) ) {
485
+ +
486
+ + let changed = false;
487
+ +
488
+ + for ( const attribute of attributes ) {
489
+ +
490
+ + if ( settled.get( attribute.isInterleavedBufferAttribute ? attribute.data : attribute ) !== attribute.version ) {
491
+ +
492
+ + changed = true;
493
+ + break;
494
+ +
495
+ + }
496
+ +
497
+ + }
498
+ +
499
+ + if ( changed === false && ( index === null || settled.get( index ) === index.version ) ) return false;
500
+ +
501
+ + }
502
+ +
503
+ + const versions = new Map();
504
+ +
505
+ + for ( const attribute of attributes ) {
506
+ +
507
+ + const data = attribute.isInterleavedBufferAttribute ? attribute.data : attribute;
508
+ +
509
+ + versions.set( data, data.version );
510
+ +
511
+ + }
512
+ +
513
+ + if ( index !== null ) versions.set( index, index.version );
514
+ +
515
+ + this._settledAttributes.set( renderObject, versions );
516
+ +
517
+ + return true;
518
+ +
519
+ + }
520
+ +
521
+ /**
522
+ * Initializes the geometry of the given render object.
523
+ *
524
+ @@ -32771,7 +32966,7 @@ class Bindings extends DataMap {
525
+ */
526
+ updateForRender( renderObject ) {
527
+
528
+ - this._updateBindings( this.getForRender( renderObject ) );
529
+ + this._updateBindings( this.getForRender( renderObject ), this.nodes.isSettled( renderObject ) );
530
+
531
+ }
532
+
533
+ @@ -32918,12 +33113,13 @@ class Bindings extends DataMap {
534
+ * Updates the given array of bindings.
535
+ *
536
+ * @param {Array<BindGroup>} bindings - The bind groups.
537
+ + * @param {boolean} [settled] - Whether the render object is a settled static object.
538
+ */
539
+ - _updateBindings( bindings ) {
540
+ + _updateBindings( bindings, settled ) {
541
+
542
+ for ( const bindGroup of bindings ) {
543
+
544
+ - this._update( bindGroup, bindings );
545
+ + this._update( bindGroup, bindings, settled );
546
+
547
+ }
548
+
549
+ @@ -32934,11 +33130,27 @@ class Bindings extends DataMap {
550
+ *
551
+ * @param {BindGroup} bindGroup - The bind group to update.
552
+ * @param {Array<BindGroup>} bindings - The bind groups.
553
+ + * @param {boolean} [settled] - Whether the render object is a settled static object.
554
+ */
555
+ - _update( bindGroup, bindings ) {
556
+ + _update( bindGroup, bindings, settled ) {
557
+
558
+ const { backend } = this;
559
+
560
+ + // A group a node added after this object's first draw has no buffers and no layout yet, and
561
+ + // the pipeline reads that layout whatever this draw decides to do with it - so it is built
562
+ + // here, on every draw. A group that did not exist cannot have been written either, so it is
563
+ + // written now: `settled` is what tells the loop below to write it, not to skip it.
564
+ + if ( this.get( bindGroup ).bindGroup === undefined ) {
565
+ +
566
+ + this._createBindings( [ bindGroup ] );
567
+ +
568
+ + settled = false;
569
+ +
570
+ + }
571
+ +
572
+ + // a settled object keeps the values it uploaded the last time it refreshed, so an object
573
+ + // group it has already written is the one thing in here it may skip
574
+ +
575
+ let needsBindingsUpdate = false;
576
+ let cacheBindings = true;
577
+ let cacheIndex = 0;
578
+ @@ -32948,7 +33160,7 @@ class Bindings extends DataMap {
579
+
580
+ for ( const binding of bindGroup.bindings ) {
581
+
582
+ - const updatedGroup = this.nodes.updateGroup( binding );
583
+ + const updatedGroup = this.nodes.updateGroup( binding, settled );
584
+
585
+ // every uniforms group is a uniform buffer. So if no update is required,
586
+ // we move one with the next binding. Otherwise the next if block will update the group.
587
+ @@ -33952,7 +34164,8 @@ class RenderContexts {
588
+
589
+ }
590
+
591
+ - if ( renderTarget !== null ) renderState.sampleCount = renderTarget.samples === 0 ? 1 : renderTarget.samples;
592
+ + const sampleCount = renderTarget !== null ? renderTarget.samples : this.renderer.currentSamples;
593
+ + renderState.sampleCount = sampleCount === 0 ? 1 : sampleCount;
594
+
595
+ renderState.clearDepthValue = this.renderer.getClearDepth();
596
+ renderState.clearStencilValue = this.renderer.getClearStencil();
597
+ @@ -34181,6 +34394,21 @@ class Textures extends DataMap {
598
+
599
+ backend.destroyTexture( texture );
600
+
601
+ + // delete cached bind groups so they don't point to the destroyed texture
602
+ +
603
+ + if ( textureData.bindGroups ) {
604
+ +
605
+ + for ( const bindGroup of textureData.bindGroups ) {
606
+ +
607
+ + const bindingsData = this.backend.get( bindGroup );
608
+ +
609
+ + bindingsData.groups = undefined;
610
+ + bindingsData.versions = undefined;
611
+ +
612
+ + }
613
+ +
614
+ + }
615
+ +
616
+ }
617
+
618
+ //
619
+ @@ -39725,7 +39953,8 @@ class VelocityNode extends TempNode {
620
+
621
+ const previousModelMatrix = getPreviousMatrix( object );
622
+
623
+ - this.previousModelWorldMatrix.value.copy( previousModelMatrix );
624
+ + const scheduledPrevious = object[ Symbol.for( 'threenative.velocity.previousWorldMatrix' ) ];
625
+ + this.previousModelWorldMatrix.value.copy( scheduledPrevious ?? previousModelMatrix );
626
+
627
+ //
628
+
629
+ @@ -53841,7 +54070,7 @@ class NodeBuilder {
630
+
631
+ const mrt = this.renderer.getMRT();
632
+
633
+ - return ( mrt && mrt.has( 'velocity' ) ) || getDataFromObject( this.object ).useVelocity === true;
634
+ + return ( mrt && mrt.has( 'velocity' ) ) || getDataFromObject( this.object ).useVelocity === true || this.object.userData?.useVelocity === true;
635
+
636
+ }
637
+
638
+ @@ -55244,15 +55473,17 @@ class NodeManager extends DataMap {
639
+ * Returns `true` if the given node uniforms group must be updated or not.
640
+ *
641
+ * @param {NodeUniformsGroup} nodeUniformsGroup - The node uniforms group.
642
+ + * @param {boolean} [settled] - Whether the render object is a settled static object.
643
+ * @return {boolean} Whether the node uniforms group requires an update or not.
644
+ */
645
+ - updateGroup( nodeUniformsGroup ) {
646
+ + updateGroup( nodeUniformsGroup, settled ) {
647
+
648
+ const groupNode = nodeUniformsGroup.groupNode;
649
+
650
+ - // groups that are updated per object always require an update so no further checks are needed
651
+ + // groups that are updated per object always require an update so no further checks are needed.
652
+ + // A settled static object keeps the values it uploaded the last time it was refreshed.
653
+
654
+ - if ( groupNode.updateType === NodeUpdateType.OBJECT ) return true;
655
+ + if ( groupNode.updateType === NodeUpdateType.OBJECT ) return settled !== true;
656
+
657
+ // check for update
658
+
659
+ @@ -56056,6 +56287,21 @@ class NodeManager extends DataMap {
660
+
661
+ }
662
+
663
+ + /**
664
+ + * Returns `true` if the per-object updates of the given render object can be skipped
665
+ + * because the object is static and nothing it depends on changed since its last refresh.
666
+ + *
667
+ + * @param {RenderObject} renderObject - The render object.
668
+ + * @return {boolean} Whether the render object is settled or not.
669
+ + */
670
+ + isSettled( renderObject ) {
671
+ +
672
+ + const monitor = renderObject.getMonitor();
673
+ +
674
+ + return monitor !== undefined ? monitor.isSettled( renderObject ) : false;
675
+ +
676
+ + }
677
+ +
678
+ /**
679
+ * Triggers the call of `updateBefore()` methods
680
+ * for all nodes of the given render object.
681
+ @@ -56065,9 +56311,14 @@ class NodeManager extends DataMap {
682
+ updateBefore( renderObject ) {
683
+
684
+ const nodeBuilder = renderObject.getNodeBuilderState();
685
+ + const settled = this.isSettled( renderObject );
686
+
687
+ for ( const node of nodeBuilder.updateBeforeNodes ) {
688
+
689
+ + // a settled static object has nothing to update per object
690
+ +
691
+ + if ( settled === true && node.getUpdateBeforeType() === NodeUpdateType.OBJECT ) continue;
692
+ +
693
+ // update frame state for each node
694
+
695
+ this.getNodeFrameForRender( renderObject ).updateBeforeNode( node );
696
+ @@ -56085,10 +56336,11 @@ class NodeManager extends DataMap {
697
+ updateAfter( renderObject ) {
698
+
699
+ const nodeBuilder = renderObject.getNodeBuilderState();
700
+ + const settled = this.isSettled( renderObject );
701
+
702
+ for ( const node of nodeBuilder.updateAfterNodes ) {
703
+
704
+ - // update frame state for each node
705
+ + if ( settled === true && node.getUpdateAfterType() === NodeUpdateType.OBJECT ) continue;
706
+
707
+ this.getNodeFrameForRender( renderObject ).updateAfterNode( node );
708
+
709
+ @@ -56125,9 +56377,14 @@ class NodeManager extends DataMap {
710
+
711
+ const nodeFrame = this.getNodeFrameForRender( renderObject );
712
+ const nodeBuilder = renderObject.getNodeBuilderState();
713
+ + const settled = this.isSettled( renderObject );
714
+
715
+ for ( const node of nodeBuilder.updateNodes ) {
716
+
717
+ + // a settled static object keeps the per-object values it uploaded last time
718
+ +
719
+ + if ( settled === true && node.getUpdateType() === NodeUpdateType.OBJECT ) continue;
720
+ +
721
+ nodeFrame.updateNode( node );
722
+
723
+ }
724
+ @@ -62731,6 +62988,7 @@ class Renderer {
725
+ let materialDisplacementMap;
726
+ let materialDisplacementScale;
727
+ let materialDisplacementBias;
728
+ + let materialAlphaTest;
729
+
730
+ const previousSourceMaterial = this._currentSourceMaterial;
731
+
732
+ @@ -62756,6 +63014,7 @@ class Renderer {
733
+ materialDisplacementMap = overrideMaterial.displacementMap;
734
+ materialDisplacementScale = overrideMaterial.displacementScale;
735
+ materialDisplacementBias = overrideMaterial.displacementBias;
736
+ + materialAlphaTest = overrideMaterial.alphaTest;
737
+
738
+ if ( material.positionNode && material.positionNode.isNode ) {
739
+
740
+ @@ -62763,7 +63022,15 @@ class Renderer {
741
+
742
+ }
743
+
744
+ - overrideMaterial.alphaTest = material.alphaTest;
745
+ + // `Material.alphaTest`'s setter bumps `version` whenever alpha-test-ness flips, and this
746
+ + // override material is shared by every caster in the pass. A scene that mixes alpha-tested
747
+ + // casters with opaque ones therefore moves that shared version several times per frame,
748
+ + // and every OTHER render object drawn with it then fails the version gate in
749
+ + // `RenderObjects.get` and recomputes a full material cache key that always matches.
750
+ + // Writing the backing field leaves the `alphaTest` getter — and so the cache key — still
751
+ + // reporting this caster's value, while each render object tracks the staleness signal
752
+ + // that belongs to it: its own source material's version.
753
+ + overrideMaterial._alphaTest = material.alphaTest;
754
+ overrideMaterial.alphaMap = material.alphaMap;
755
+ overrideMaterial.displacementMap = material.displacementMap;
756
+ overrideMaterial.displacementScale = material.displacementScale;
757
+ @@ -62825,6 +63092,7 @@ class Renderer {
758
+ scene.overrideMaterial.displacementMap = materialDisplacementMap;
759
+ scene.overrideMaterial.displacementScale = materialDisplacementScale;
760
+ scene.overrideMaterial.displacementBias = materialDisplacementBias;
761
+ + scene.overrideMaterial._alphaTest = materialAlphaTest;
762
+
763
+ }
764
+
765
+ @@ -62886,18 +63154,17 @@ class Renderer {
766
+
767
+ //
768
+
769
+ - const needsRefresh = this._nodes.needsRefresh( renderObject );
770
+ + this._nodes.needsRefresh( renderObject );
771
+
772
+ - if ( needsRefresh ) {
773
+ -
774
+ - this._nodes.updateBefore( renderObject );
775
+ -
776
+ - this._geometries.updateForRender( renderObject );
777
+ -
778
+ - this._nodes.updateForRender( renderObject );
779
+ - this._bindings.updateForRender( renderObject );
780
+ -
781
+ - }
782
+ + // Every step of a draw runs, settled or not. What a settled static object skips is decided
783
+ + // inside each of them - its OBJECT node updates, its object uniform group write and its
784
+ + // attribute scan - because each of those answers for itself, and a step left out here
785
+ + // cannot: a group a node added after this object's first draw still has to be created, and
786
+ + // the pipeline reads that group's layout whatever this draw decided to do with it.
787
+ + this._geometries.updateForRender( renderObject );
788
+ + this._nodes.updateBefore( renderObject );
789
+ + this._nodes.updateForRender( renderObject );
790
+ + this._bindings.updateForRender( renderObject );
791
+
792
+ this._pipelines.updateForRender( renderObject );
793
+
794
+ @@ -62907,7 +63174,7 @@ class Renderer {
795
+
796
+ this.backend.draw( renderObject, this.info );
797
+
798
+ - if ( needsRefresh ) this._nodes.updateAfter( renderObject );
799
+ + this._nodes.updateAfter( renderObject );
800
+
801
+ }
802
+
803
+ @@ -79004,7 +79271,14 @@ class WGSLNodeBuilder extends NodeBuilder {
804
+
805
+ } else {
806
+
807
+ - snippet = `textureLoad( ${ textureProperty }, ${ uvIndexSnippet }, u32( ${ levelSnippet } ) )`;
808
+ + // naga v25 (wgpu-native v25.0.2.2) rejects a u32 sample index on a
809
+ + // multisampled textureLoad; i32 is accepted by every backend. Mip-level
810
+ + // operands (including the array form above) stay u32.
811
+ + const sampleData = texture ? this.renderer?.backend?.utils?.getTextureSampleData?.( texture ) : null;
812
+ + const backendMultisampled = sampleData !== null && sampleData !== undefined && sampleData.primarySamples > 1;
813
+ + const targetMultisampled = Boolean( texture && texture.renderTarget && texture.renderTarget.samples > 1 );
814
+ + const indexType = ( backendMultisampled || targetMultisampled ) ? 'i32' : 'u32';
815
+ + snippet = `textureLoad( ${ textureProperty }, ${ uvIndexSnippet }, ${ indexType }( ${ levelSnippet } ) )`;
816
+
817
+ if ( this.renderer.backend.compatibilityMode && texture.isDepthTexture ) {
818
+
819
+ @@ -79586,7 +79860,8 @@ class WGSLNodeBuilder extends NodeBuilder {
820
+
821
+ uniformGPU = buffer;
822
+
823
+ - uniformNode.name = name ? name : 'NodeBuffer_' + uniformNode.id;
824
+ + // The base builder supplies a unique name within this program. A process-wide
825
+ + // allocation id changes otherwise identical shader source for every buffer.
826
+
827
+ } else {
828
+
829
+ @@ -80329,9 +80604,12 @@ ${ flowData.code }
830
+ const textureNode = uniform.node;
831
+ const texture = textureNode.value;
832
+
833
+ - // Cube textures always need samplers (they use textureSampleLevel, not textureLoad)
834
+ - // Also textureGather always need sampler.
835
+ - const needsSampler = texture.isCubeTexture === true || ( this.isUnfilterable( texture ) === false && textureNode.isStorageTextureNode !== true ) || textureNode.gatherNode !== null;
836
+ + // The bind group layout freezes sampler inclusion once, in getUniformFromNode
837
+ + // (nodeData.uniformGPU). Re-deriving it here from live texture filter state can
838
+ + // disagree with that layout and shift every later binding index.
839
+ + const nodeData = this.getDataFromNode( textureNode, shaderStage, this.globalCache );
840
+ + const uniformGPU = nodeData.uniformGPU;
841
+ + const needsSampler = Array.isArray( uniformGPU ) && uniformGPU[ 0 ].isSampler === true && uniformGPU[ 0 ].isSampledTexture !== true;
842
+
843
+ if ( needsSampler ) {
844
+
845
+ @@ -81295,9 +81573,15 @@ class WebGPUAttributeUtils {
846
+ destroyAttribute( attribute ) {
847
+
848
+ const backend = this.backend;
849
+ - const data = backend.get( this._getBufferAttribute( attribute ) );
850
+ + const bufferAttribute = this._getBufferAttribute( attribute );
851
+ + const data = backend.get( bufferAttribute );
852
+
853
+ - data.buffer.destroy();
854
+ + // An interleaved attribute's GPU buffer lives on its InterleavedBuffer, and several attributes
855
+ + // share it: destroy it once and forget it there too, as the WebGL backend does, or the next
856
+ + // createAttribute() finds the destroyed buffer and submits it (threenative patch).
857
+ + if ( data.buffer !== undefined ) data.buffer.destroy();
858
+ +
859
+ + if ( bufferAttribute !== attribute ) backend.delete( bufferAttribute );
860
+
861
+ backend.delete( attribute );
862
+
863
+ @@ -81598,18 +81882,16 @@ class WebGPUBindingUtils {
864
+
865
+ const bindingsData = backend.get( bindGroup );
866
+
867
+ - // check if the the bind group already has a layout
868
+ + const entries = this._createLayoutEntries( bindGroup );
869
+ + const bindGroupLayoutKey = hashString( JSON.stringify( entries ) );
870
+
871
+ if ( bindingsData.layout ) {
872
+
873
+ - return bindingsData.layout.layoutGPU;
874
+ + if ( bindingsData.layoutKey === bindGroupLayoutKey ) return bindingsData.layout.layoutGPU;
875
+
876
+ - }
877
+ -
878
+ - // if not, assing one
879
+ + this.deleteBindGroupData( bindGroup );
880
+
881
+ - const entries = this._createLayoutEntries( bindGroup );
882
+ - const bindGroupLayoutKey = hashString( JSON.stringify( entries ) );
883
+ + }
884
+
885
+ // try to find an existing layout in the cache
886
+
887
+ @@ -82131,6 +82413,9 @@ class WebGPUBindingUtils {
888
+
889
+ }
890
+
891
+ + bindingsData.groups = [];
892
+ + bindingsData.versions = [];
893
+ +
894
+ }
895
+
896
+ /**
897
+ @@ -83814,6 +84099,14 @@ class WebGPUBackend extends Backend {
898
+ */
899
+ this.compatibilityMode = null;
900
+
901
+ + /**
902
+ + * A reference to the WebGPU instance that creates adapters.
903
+ + *
904
+ + * @type {?GPU}
905
+ + * @default null
906
+ + */
907
+ + this.gpu = null;
908
+ +
909
+ /**
910
+ * A reference to the device.
911
+ *
912
+ @@ -83926,7 +84219,9 @@ class WebGPUBackend extends Backend {
913
+ xrCompatible: renderer.xr.enabled
914
+ };
915
+
916
+ - const adapter = ( typeof navigator !== 'undefined' ) ? await navigator.gpu.requestAdapter( adapterOptions ) : null;
917
+ + this.gpu = ( typeof navigator !== 'undefined' ) ? navigator.gpu : null;
918
+ +
919
+ + const adapter = this.gpu !== null ? await this.gpu.requestAdapter( adapterOptions ) : null;
920
+
921
+ if ( adapter === null ) {
922
+
923
+ @@ -85461,13 +85756,12 @@ class WebGPUBackend extends Backend {
924
+ for ( let i = 0, l = bindings.length; i < l; i ++ ) {
925
+
926
+ const bindGroup = bindings[ i ];
927
+ + const bindingsData = this.get( bindGroup );
928
+
929
+ - if ( currentBindingGroups[ i ] !== bindGroup.id ) {
930
+ -
931
+ - const bindingsData = this.get( bindGroup );
932
+ + if ( currentBindingGroups[ i ] !== bindingsData.group ) {
933
+
934
+ passEncoderGPU.setBindGroup( i, bindingsData.group );
935
+ - currentBindingGroups[ i ] = bindGroup.id;
936
+ + currentBindingGroups[ i ] = bindingsData.group;
937
+
938
+ }
939
+
940
+ @@ -85766,6 +86060,7 @@ class WebGPUBackend extends Backend {
941
+ const utils = this.utils;
942
+
943
+ const sampleCount = utils.getSampleCountRenderContext( renderObject.context );
944
+ + const bindGroupLayoutKeys = renderObject.getBindings().map( bindGroup => this.get( bindGroup ).layoutKey ).join( '|' );
945
+ const colorSpace = utils.getCurrentColorSpace( renderObject.context );
946
+ const colorFormat = utils.getCurrentColorFormat( renderObject.context );
947
+ const depthStencilFormat = utils.getCurrentDepthStencilFormat( renderObject.context );
948
+ @@ -85787,7 +86082,8 @@ class WebGPUBackend extends Backend {
949
+ data.colorFormat !== colorFormat || data.depthStencilFormat !== depthStencilFormat ||
950
+ data.primitiveTopology !== primitiveTopology ||
951
+ data.frontFaceCW !== frontFaceCW ||
952
+ - data.clippingContextCacheKey !== renderObject.clippingContextCacheKey
953
+ + data.clippingContextCacheKey !== renderObject.clippingContextCacheKey ||
954
+ + data.bindGroupLayoutKeys !== bindGroupLayoutKeys
955
+ ) {
956
+
957
+ data.material = material; data.materialVersion = material.version;
958
+ @@ -85807,6 +86103,7 @@ class WebGPUBackend extends Backend {
959
+ data.primitiveTopology = primitiveTopology;
960
+ data.frontFaceCW = frontFaceCW;
961
+ data.clippingContextCacheKey = renderObject.clippingContextCacheKey;
962
+ + data.bindGroupLayoutKeys = bindGroupLayoutKeys;
963
+
964
+ needsUpdate = true;
965
+
966
+ @@ -85828,6 +86125,7 @@ class WebGPUBackend extends Backend {
967
+
968
+ const utils = this.utils;
969
+ const renderContext = renderObject.context;
970
+ + const bindGroupLayoutKeys = renderObject.getBindings().map( bindGroup => this.get( bindGroup ).layoutKey ).join( '|' );
971
+
972
+ // meshes with negative scale have a different frontFace render pipeline
973
+ // descriptor value so the following must be honored in the cache key
974
+ @@ -85846,6 +86144,7 @@ class WebGPUBackend extends Backend {
975
+ material.side,
976
+ frontFaceCW,
977
+ utils.getSampleCountRenderContext( renderContext ),
978
+ + bindGroupLayoutKeys,
979
+ utils.getCurrentColorSpace( renderContext ), utils.getCurrentColorFormat( renderContext ), utils.getCurrentDepthStencilFormat( renderContext ),
980
+ utils.getPrimitiveTopology( object, material ),
981
+ renderObject.getGeometryCacheKey(),
982
+ diff --git a/build/three.webgpu.nodes.js b/build/three.webgpu.nodes.js
983
+ index f707acc..b8a7b2f 100644
984
+ --- a/build/three.webgpu.nodes.js
985
+ +++ b/build/three.webgpu.nodes.js
986
+ @@ -141,6 +141,15 @@ class NodeMaterialObserver {
987
+ */
988
+ this.renderId = 0;
989
+
990
+ + /**
991
+ + * Whether the current render produces motion vectors. Objects keep refreshing
992
+ + * fully while this is the case.
993
+ + *
994
+ + * @type {boolean}
995
+ + * @default false
996
+ + */
997
+ + this.renderingVelocity = false;
998
+ +
999
+ }
1000
+
1001
+ /**
1002
+ @@ -240,6 +249,88 @@ class NodeMaterialObserver {
1003
+
1004
+ }
1005
+
1006
+ + /**
1007
+ + * Returns `true` if the given render object is a static object whose per-object state - world
1008
+ + * matrix, material, geometry and the scene environment - did not change since the last
1009
+ + * refresh. A settled object is still drawn every frame and its FRAME and RENDER nodes still
1010
+ + * have to be updated; what it may skip is the work whose inputs it froze: its OBJECT node
1011
+ + * updates, its object uniform group upload and its per-draw attribute scan.
1012
+ + *
1013
+ + * @param {RenderObject} renderObject - The render object.
1014
+ + * @return {boolean} Whether the given render object is settled or not.
1015
+ + */
1016
+ + isSettled( renderObject ) {
1017
+ +
1018
+ + const data = this.renderObjects.get( renderObject );
1019
+ +
1020
+ + return data !== undefined && data.settledState === true;
1021
+ +
1022
+ + }
1023
+ +
1024
+ + /**
1025
+ + * Recomputes {@link NodeMaterialObserver#isSettled} for the given render object, once per draw,
1026
+ + * so every step of that draw reads the same answer.
1027
+ + *
1028
+ + * @param {RenderObject} renderObject - The render object.
1029
+ + * @return {boolean} Whether the given render object is settled or not.
1030
+ + */
1031
+ + updateSettled( renderObject ) {
1032
+ +
1033
+ + const { object, material, geometry, scene } = renderObject;
1034
+ + const data = this.getRenderObjectData( renderObject );
1035
+ +
1036
+ + if ( object.static !== true || this.renderingVelocity === true ) {
1037
+ +
1038
+ + data.settledState = false;
1039
+ +
1040
+ + return false;
1041
+ +
1042
+ + }
1043
+ +
1044
+ + let settled = data.settled;
1045
+ +
1046
+ + if ( settled === undefined ) {
1047
+ +
1048
+ + data.settled = settled = {
1049
+ + matrix: object.matrixWorld.clone(),
1050
+ + environmentRotation: scene.environmentRotation.clone(),
1051
+ + environmentIntensity: scene.environmentIntensity,
1052
+ + materialVersion: material.version,
1053
+ + geometryVersion: geometry.version
1054
+ + };
1055
+ +
1056
+ + data.settledState = false;
1057
+ +
1058
+ + return false;
1059
+ +
1060
+ + }
1061
+ +
1062
+ + if ( settled.matrix.equals( object.matrixWorld ) !== true ||
1063
+ + settled.environmentRotation.equals( scene.environmentRotation ) !== true ||
1064
+ + settled.environmentIntensity !== scene.environmentIntensity ||
1065
+ + settled.materialVersion !== material.version ||
1066
+ + settled.geometryVersion !== geometry.version ) {
1067
+ +
1068
+ + // re-arm with the current state, so the object settles again once it stops changing
1069
+ +
1070
+ + settled.matrix.copy( object.matrixWorld );
1071
+ + settled.environmentRotation.copy( scene.environmentRotation );
1072
+ + settled.environmentIntensity = scene.environmentIntensity;
1073
+ + settled.materialVersion = material.version;
1074
+ + settled.geometryVersion = geometry.version;
1075
+ +
1076
+ + data.settledState = false;
1077
+ +
1078
+ + return false;
1079
+ +
1080
+ + }
1081
+ +
1082
+ + data.settledState = true;
1083
+ +
1084
+ + return true;
1085
+ +
1086
+ + }
1087
+ +
1088
+ /**
1089
+ * Returns an attribute data structure holding the attributes versions for
1090
+ * monitoring.
1091
+ @@ -306,7 +397,6 @@ class NodeMaterialObserver {
1092
+
1093
+ data = {
1094
+ _renderId: -1,
1095
+ - _equal: false,
1096
+
1097
+ attributes: this.getAttributesData( geometry.attributes ),
1098
+ indexId: geometry.index ? geometry.index.id : null,
1099
+ @@ -335,7 +425,7 @@ class NodeMaterialObserver {
1100
+
1101
+ if ( data === undefined ) {
1102
+
1103
+ - data = { _renderId: -1, _equal: false };
1104
+ + data = { _renderId: -1 };
1105
+
1106
+ for ( const property of this.refreshUniforms ) {
1107
+
1108
+ @@ -419,7 +509,6 @@ class NodeMaterialObserver {
1109
+
1110
+ value.copy( mtlValue );
1111
+
1112
+ - materialData._equal = false;
1113
+ return false;
1114
+
1115
+ }
1116
+ @@ -431,7 +520,6 @@ class NodeMaterialObserver {
1117
+ value.id = mtlValue.id;
1118
+ value.version = mtlValue.version;
1119
+
1120
+ - materialData._equal = false;
1121
+ return false;
1122
+
1123
+ }
1124
+ @@ -440,7 +528,6 @@ class NodeMaterialObserver {
1125
+
1126
+ materialData[ property ] = mtlValue;
1127
+
1128
+ - materialData._equal = false;
1129
+ return false;
1130
+
1131
+ }
1132
+ @@ -456,18 +543,12 @@ class NodeMaterialObserver {
1133
+ renderObjectData.bufferWidth = width;
1134
+ renderObjectData.bufferHeight = height;
1135
+
1136
+ - materialData._equal = false;
1137
+ return false;
1138
+
1139
+ }
1140
+
1141
+ }
1142
+
1143
+ - materialData._equal = true;
1144
+ -
1145
+ - } else {
1146
+ -
1147
+ - if ( materialData._equal === false ) return false;
1148
+
1149
+ }
1150
+
1151
+ @@ -510,7 +591,6 @@ class NodeMaterialObserver {
1152
+ // attribute was removed
1153
+ delete storedAttributes[ name ];
1154
+
1155
+ - geometryData._equal = false;
1156
+ return false;
1157
+
1158
+ }
1159
+ @@ -523,7 +603,6 @@ class NodeMaterialObserver {
1160
+ storedAttributeData.id = id;
1161
+ storedAttributeData.version = version;
1162
+
1163
+ - geometryData._equal = false;
1164
+ return false;
1165
+
1166
+ }
1167
+ @@ -534,7 +613,6 @@ class NodeMaterialObserver {
1168
+
1169
+ geometryData.attributes = this.getAttributesData( attributes );
1170
+
1171
+ - geometryData._equal = false;
1172
+ return false;
1173
+
1174
+ }
1175
+ @@ -552,7 +630,6 @@ class NodeMaterialObserver {
1176
+ geometryData.indexId = currentIndexId;
1177
+ geometryData.indexVersion = currentIndexVersion;
1178
+
1179
+ - geometryData._equal = false;
1180
+ return false;
1181
+
1182
+ }
1183
+ @@ -564,16 +641,10 @@ class NodeMaterialObserver {
1184
+ geometryData.drawRange.start = geometry.drawRange.start;
1185
+ geometryData.drawRange.count = geometry.drawRange.count;
1186
+
1187
+ - geometryData._equal = false;
1188
+ return false;
1189
+
1190
+ }
1191
+
1192
+ - geometryData._equal = true;
1193
+ -
1194
+ - } else {
1195
+ -
1196
+ - if ( geometryData._equal === false ) return false;
1197
+
1198
+ }
1199
+
1200
+ @@ -724,7 +795,14 @@ class NodeMaterialObserver {
1201
+ */
1202
+ needsRefresh( renderObject, nodeFrame ) {
1203
+
1204
+ - if ( this.hasNode || this.hasAnimation || this.firstInitialization( renderObject ) || this.needsVelocity( nodeFrame.renderer ) )
1205
+ + this.renderingVelocity = this.needsVelocity( nodeFrame.renderer );
1206
+ +
1207
+ + // Answered for the whole draw, so updateBefore, the geometry scan, the node updates and
1208
+ + // the object uniform upload of this object all skip or all refresh.
1209
+ +
1210
+ + this.updateSettled( renderObject );
1211
+ +
1212
+ + if ( this.hasNode || this.hasAnimation || this.firstInitialization( renderObject ) || this.renderingVelocity )
1213
+ return true;
1214
+
1215
+ const { renderId } = nodeFrame;
1216
+ @@ -737,10 +815,14 @@ class NodeMaterialObserver {
1217
+
1218
+ }
1219
+
1220
+ - const isStatic = renderObject.object.static === true;
1221
+ const isBundle = renderObject.bundle !== null && renderObject.bundle.static === true && this.getRenderObjectData( renderObject ).version === renderObject.bundle.version;
1222
+
1223
+ - if ( isStatic || isBundle )
1224
+ + // `object.static` is not the question - it is a switch, and a brand new object has it set
1225
+ + // before it has been drawn once, so answering from it skips the draw that creates this
1226
+ + // object's bind groups and leaves the pipeline reading a layout that was never built. A
1227
+ + // static object that moved, or whose material or geometry was edited, has to refresh like
1228
+ + // any other; only a settled one may skip, and nothing is settled before its first refresh.
1229
+ + if ( this.isSettled( renderObject ) === true || isBundle )
1230
+ return false;
1231
+
1232
+ const lightsData = this.getLights( renderObject.lightsNode, renderId );
1233
+ @@ -18320,7 +18402,8 @@ function createInstanceMatrixNode( builder, instanceMatrix ) {
1234
+
1235
+ const uniformBufferSize = matrixCount * 16 * 4;
1236
+
1237
+ - if ( uniformBufferSize <= builder.getUniformBufferLimit() ) {
1238
+ + // threenative: the uniform path re-uploads every draw; the attribute path uploads on version change
1239
+ + if ( false ) {
1240
+
1241
+ instanceMatrixNode = buffer( instanceMatrix.array, 'mat4', matrixCount ).element( instanceIndex );
1242
+
1243
+ @@ -18414,7 +18497,8 @@ const instance = /*@__PURE__*/ Fn( ( [ matrices, colors = null ], builder ) => {
1244
+
1245
+ const uniformBufferSize = Math.max( matrices.count, 1 ) * 16 * 4;
1246
+
1247
+ - if ( uniformBufferSize > builder.getUniformBufferLimit() ) {
1248
+ + // threenative: the uniform path re-uploads every draw; the attribute path uploads on version change
1249
+ + if ( true ) {
1250
+
1251
+ interleavedMatrix = _matrixBuffers.get( matrices );
1252
+
1253
+ @@ -31165,6 +31249,14 @@ class Geometries extends DataMap {
1254
+ */
1255
+ this._geometryDisposeListeners = new Map();
1256
+
1257
+ + /**
1258
+ + * This Weak Map holds the attribute versions a settled render object was last drawn with,
1259
+ + * so a write into one of them is what brings the object back out of the settled path.
1260
+ + *
1261
+ + * @type {WeakMap<RenderObject,Map<BufferAttribute,number>>}
1262
+ + */
1263
+ + this._settledAttributes = new WeakMap();
1264
+ +
1265
+ }
1266
+
1267
+ /**
1268
+ @@ -31190,10 +31282,69 @@ class Geometries extends DataMap {
1269
+
1270
+ if ( this.has( renderObject ) === false ) this.initGeometry( renderObject );
1271
+
1272
+ + // A settled static object has no attribute that could have changed since the last refresh,
1273
+ + // unless a writer bumped one. An InstancedMesh's instance buffer is a vertex attribute of
1274
+ + // this draw, and a BufferGeometry carries no version of its own to announce the write, so
1275
+ + // the attributes' own versions are the signal - and the only one there is.
1276
+ +
1277
+ + const monitor = renderObject.getMonitor();
1278
+ +
1279
+ + if ( monitor !== undefined && monitor.isSettled( renderObject ) === true && this.attributesChanged( renderObject ) === false ) return;
1280
+ +
1281
+ this.updateAttributes( renderObject );
1282
+
1283
+ }
1284
+
1285
+ + /**
1286
+ + * Returns `true` if an attribute of the given render object - or its index - was written to
1287
+ + * since the last time its versions were read, and records the versions it holds now.
1288
+ + *
1289
+ + * @param {RenderObject} renderObject - The render object.
1290
+ + * @return {boolean} Whether any of the render object's versions moved.
1291
+ + */
1292
+ + attributesChanged( renderObject ) {
1293
+ +
1294
+ + const attributes = renderObject.getAttributes();
1295
+ + const index = this.getIndex( renderObject );
1296
+ + const settled = this._settledAttributes.get( renderObject );
1297
+ +
1298
+ + if ( settled !== undefined && settled.size === attributes.length + ( index === null ? 0 : 1 ) ) {
1299
+ +
1300
+ + let changed = false;
1301
+ +
1302
+ + for ( const attribute of attributes ) {
1303
+ +
1304
+ + if ( settled.get( attribute.isInterleavedBufferAttribute ? attribute.data : attribute ) !== attribute.version ) {
1305
+ +
1306
+ + changed = true;
1307
+ + break;
1308
+ +
1309
+ + }
1310
+ +
1311
+ + }
1312
+ +
1313
+ + if ( changed === false && ( index === null || settled.get( index ) === index.version ) ) return false;
1314
+ +
1315
+ + }
1316
+ +
1317
+ + const versions = new Map();
1318
+ +
1319
+ + for ( const attribute of attributes ) {
1320
+ +
1321
+ + const data = attribute.isInterleavedBufferAttribute ? attribute.data : attribute;
1322
+ +
1323
+ + versions.set( data, data.version );
1324
+ +
1325
+ + }
1326
+ +
1327
+ + if ( index !== null ) versions.set( index, index.version );
1328
+ +
1329
+ + this._settledAttributes.set( renderObject, versions );
1330
+ +
1331
+ + return true;
1332
+ +
1333
+ + }
1334
+ +
1335
+ /**
1336
+ * Initializes the geometry of the given render object.
1337
+ *
1338
+ @@ -32771,7 +32922,7 @@ class Bindings extends DataMap {
1339
+ */
1340
+ updateForRender( renderObject ) {
1341
+
1342
+ - this._updateBindings( this.getForRender( renderObject ) );
1343
+ + this._updateBindings( this.getForRender( renderObject ), this.nodes.isSettled( renderObject ) );
1344
+
1345
+ }
1346
+
1347
+ @@ -32918,12 +33069,13 @@ class Bindings extends DataMap {
1348
+ * Updates the given array of bindings.
1349
+ *
1350
+ * @param {Array<BindGroup>} bindings - The bind groups.
1351
+ + * @param {boolean} [settled] - Whether the render object is a settled static object.
1352
+ */
1353
+ - _updateBindings( bindings ) {
1354
+ + _updateBindings( bindings, settled ) {
1355
+
1356
+ for ( const bindGroup of bindings ) {
1357
+
1358
+ - this._update( bindGroup, bindings );
1359
+ + this._update( bindGroup, bindings, settled );
1360
+
1361
+ }
1362
+
1363
+ @@ -32934,11 +33086,27 @@ class Bindings extends DataMap {
1364
+ *
1365
+ * @param {BindGroup} bindGroup - The bind group to update.
1366
+ * @param {Array<BindGroup>} bindings - The bind groups.
1367
+ + * @param {boolean} [settled] - Whether the render object is a settled static object.
1368
+ */
1369
+ - _update( bindGroup, bindings ) {
1370
+ + _update( bindGroup, bindings, settled ) {
1371
+
1372
+ const { backend } = this;
1373
+
1374
+ + // A group a node added after this object's first draw has no buffers and no layout yet, and
1375
+ + // the pipeline reads that layout whatever this draw decides to do with it - so it is built
1376
+ + // here, on every draw. A group that did not exist cannot have been written either, so it is
1377
+ + // written now: `settled` is what tells the loop below to write it, not to skip it.
1378
+ + if ( this.get( bindGroup ).bindGroup === undefined ) {
1379
+ +
1380
+ + this._createBindings( [ bindGroup ] );
1381
+ +
1382
+ + settled = false;
1383
+ +
1384
+ + }
1385
+ +
1386
+ + // a settled object keeps the values it uploaded the last time it refreshed, so an object
1387
+ + // group it has already written is the one thing in here it may skip
1388
+ +
1389
+ let needsBindingsUpdate = false;
1390
+ let cacheBindings = true;
1391
+ let cacheIndex = 0;
1392
+ @@ -32948,7 +33116,7 @@ class Bindings extends DataMap {
1393
+
1394
+ for ( const binding of bindGroup.bindings ) {
1395
+
1396
+ - const updatedGroup = this.nodes.updateGroup( binding );
1397
+ + const updatedGroup = this.nodes.updateGroup( binding, settled );
1398
+
1399
+ // every uniforms group is a uniform buffer. So if no update is required,
1400
+ // we move one with the next binding. Otherwise the next if block will update the group.
1401
+ @@ -34181,6 +34349,21 @@ class Textures extends DataMap {
1402
+
1403
+ backend.destroyTexture( texture );
1404
+
1405
+ + // delete cached bind groups so they don't point to the destroyed texture
1406
+ +
1407
+ + if ( textureData.bindGroups ) {
1408
+ +
1409
+ + for ( const bindGroup of textureData.bindGroups ) {
1410
+ +
1411
+ + const bindingsData = this.backend.get( bindGroup );
1412
+ +
1413
+ + bindingsData.groups = undefined;
1414
+ + bindingsData.versions = undefined;
1415
+ +
1416
+ + }
1417
+ +
1418
+ + }
1419
+ +
1420
+ }
1421
+
1422
+ //
1423
+ @@ -55244,15 +55427,17 @@ class NodeManager extends DataMap {
1424
+ * Returns `true` if the given node uniforms group must be updated or not.
1425
+ *
1426
+ * @param {NodeUniformsGroup} nodeUniformsGroup - The node uniforms group.
1427
+ + * @param {boolean} [settled] - Whether the render object is a settled static object.
1428
+ * @return {boolean} Whether the node uniforms group requires an update or not.
1429
+ */
1430
+ - updateGroup( nodeUniformsGroup ) {
1431
+ + updateGroup( nodeUniformsGroup, settled ) {
1432
+
1433
+ const groupNode = nodeUniformsGroup.groupNode;
1434
+
1435
+ - // groups that are updated per object always require an update so no further checks are needed
1436
+ + // groups that are updated per object always require an update so no further checks are needed.
1437
+ + // A settled static object keeps the values it uploaded the last time it was refreshed.
1438
+
1439
+ - if ( groupNode.updateType === NodeUpdateType.OBJECT ) return true;
1440
+ + if ( groupNode.updateType === NodeUpdateType.OBJECT ) return settled !== true;
1441
+
1442
+ // check for update
1443
+
1444
+ @@ -56056,6 +56241,21 @@ class NodeManager extends DataMap {
1445
+
1446
+ }
1447
+
1448
+ + /**
1449
+ + * Returns `true` if the per-object updates of the given render object can be skipped
1450
+ + * because the object is static and nothing it depends on changed since its last refresh.
1451
+ + *
1452
+ + * @param {RenderObject} renderObject - The render object.
1453
+ + * @return {boolean} Whether the render object is settled or not.
1454
+ + */
1455
+ + isSettled( renderObject ) {
1456
+ +
1457
+ + const monitor = renderObject.getMonitor();
1458
+ +
1459
+ + return monitor !== undefined ? monitor.isSettled( renderObject ) : false;
1460
+ +
1461
+ + }
1462
+ +
1463
+ /**
1464
+ * Triggers the call of `updateBefore()` methods
1465
+ * for all nodes of the given render object.
1466
+ @@ -56065,9 +56265,14 @@ class NodeManager extends DataMap {
1467
+ updateBefore( renderObject ) {
1468
+
1469
+ const nodeBuilder = renderObject.getNodeBuilderState();
1470
+ + const settled = this.isSettled( renderObject );
1471
+
1472
+ for ( const node of nodeBuilder.updateBeforeNodes ) {
1473
+
1474
+ + // a settled static object has nothing to update per object
1475
+ +
1476
+ + if ( settled === true && node.getUpdateBeforeType() === NodeUpdateType.OBJECT ) continue;
1477
+ +
1478
+ // update frame state for each node
1479
+
1480
+ this.getNodeFrameForRender( renderObject ).updateBeforeNode( node );
1481
+ @@ -56085,10 +56290,11 @@ class NodeManager extends DataMap {
1482
+ updateAfter( renderObject ) {
1483
+
1484
+ const nodeBuilder = renderObject.getNodeBuilderState();
1485
+ + const settled = this.isSettled( renderObject );
1486
+
1487
+ for ( const node of nodeBuilder.updateAfterNodes ) {
1488
+
1489
+ - // update frame state for each node
1490
+ + if ( settled === true && node.getUpdateAfterType() === NodeUpdateType.OBJECT ) continue;
1491
+
1492
+ this.getNodeFrameForRender( renderObject ).updateAfterNode( node );
1493
+
1494
+ @@ -56125,9 +56331,14 @@ class NodeManager extends DataMap {
1495
+
1496
+ const nodeFrame = this.getNodeFrameForRender( renderObject );
1497
+ const nodeBuilder = renderObject.getNodeBuilderState();
1498
+ + const settled = this.isSettled( renderObject );
1499
+
1500
+ for ( const node of nodeBuilder.updateNodes ) {
1501
+
1502
+ + // a settled static object keeps the per-object values it uploaded last time
1503
+ +
1504
+ + if ( settled === true && node.getUpdateType() === NodeUpdateType.OBJECT ) continue;
1505
+ +
1506
+ nodeFrame.updateNode( node );
1507
+
1508
+ }
1509
+ @@ -62886,18 +63097,17 @@ class Renderer {
1510
+
1511
+ //
1512
+
1513
+ - const needsRefresh = this._nodes.needsRefresh( renderObject );
1514
+ -
1515
+ - if ( needsRefresh ) {
1516
+ -
1517
+ - this._nodes.updateBefore( renderObject );
1518
+ -
1519
+ - this._geometries.updateForRender( renderObject );
1520
+ -
1521
+ - this._nodes.updateForRender( renderObject );
1522
+ - this._bindings.updateForRender( renderObject );
1523
+ + this._nodes.needsRefresh( renderObject );
1524
+
1525
+ - }
1526
+ + // Every step of a draw runs, settled or not. What a settled static object skips is decided
1527
+ + // inside each of them - its OBJECT node updates, its object uniform group write and its
1528
+ + // attribute scan - because each of those answers for itself, and a step left out here
1529
+ + // cannot: a group a node added after this object's first draw still has to be created, and
1530
+ + // the pipeline reads that group's layout whatever this draw decided to do with it.
1531
+ + this._geometries.updateForRender( renderObject );
1532
+ + this._nodes.updateBefore( renderObject );
1533
+ + this._nodes.updateForRender( renderObject );
1534
+ + this._bindings.updateForRender( renderObject );
1535
+
1536
+ this._pipelines.updateForRender( renderObject );
1537
+
1538
+ @@ -62907,7 +63117,7 @@ class Renderer {
1539
+
1540
+ this.backend.draw( renderObject, this.info );
1541
+
1542
+ - if ( needsRefresh ) this._nodes.updateAfter( renderObject );
1543
+ + this._nodes.updateAfter( renderObject );
1544
+
1545
+ }
1546
+
1547
+ @@ -79004,7 +79214,14 @@ class WGSLNodeBuilder extends NodeBuilder {
1548
+
1549
+ } else {
1550
+
1551
+ - snippet = `textureLoad( ${ textureProperty }, ${ uvIndexSnippet }, u32( ${ levelSnippet } ) )`;
1552
+ + // naga v25 (wgpu-native v25.0.2.2) rejects a u32 sample index on a
1553
+ + // multisampled textureLoad; i32 is accepted by every backend. Mip-level
1554
+ + // operands (including the array form above) stay u32.
1555
+ + const sampleData = texture ? this.renderer?.backend?.utils?.getTextureSampleData?.( texture ) : null;
1556
+ + const backendMultisampled = sampleData !== null && sampleData !== undefined && sampleData.primarySamples > 1;
1557
+ + const targetMultisampled = Boolean( texture && texture.renderTarget && texture.renderTarget.samples > 1 );
1558
+ + const indexType = ( backendMultisampled || targetMultisampled ) ? 'i32' : 'u32';
1559
+ + snippet = `textureLoad( ${ textureProperty }, ${ uvIndexSnippet }, ${ indexType }( ${ levelSnippet } ) )`;
1560
+
1561
+ if ( this.renderer.backend.compatibilityMode && texture.isDepthTexture ) {
1562
+
1563
+ @@ -81295,9 +81512,15 @@ class WebGPUAttributeUtils {
1564
+ destroyAttribute( attribute ) {
1565
+
1566
+ const backend = this.backend;
1567
+ - const data = backend.get( this._getBufferAttribute( attribute ) );
1568
+ + const bufferAttribute = this._getBufferAttribute( attribute );
1569
+ + const data = backend.get( bufferAttribute );
1570
+ +
1571
+ + // An interleaved attribute's GPU buffer lives on its InterleavedBuffer, and several attributes
1572
+ + // share it: destroy it once and forget it there too, as the WebGL backend does, or the next
1573
+ + // createAttribute() finds the destroyed buffer and submits it (threenative patch).
1574
+ + if ( data.buffer !== undefined ) data.buffer.destroy();
1575
+
1576
+ - data.buffer.destroy();
1577
+ + if ( bufferAttribute !== attribute ) backend.delete( bufferAttribute );
1578
+
1579
+ backend.delete( attribute );
1580
+
1581
+ diff --git a/src/materials/nodes/manager/NodeMaterialObserver.js b/src/materials/nodes/manager/NodeMaterialObserver.js
1582
+ index 1f4c254..7e2cb99 100644
1583
+ --- a/src/materials/nodes/manager/NodeMaterialObserver.js
1584
+ +++ b/src/materials/nodes/manager/NodeMaterialObserver.js
1585
+ @@ -133,6 +133,15 @@ class NodeMaterialObserver {
1586
+ */
1587
+ this.renderId = 0;
1588
+
1589
+ + /**
1590
+ + * Whether the current render produces motion vectors. Objects keep refreshing
1591
+ + * fully while this is the case.
1592
+ + *
1593
+ + * @type {boolean}
1594
+ + * @default false
1595
+ + */
1596
+ + this.renderingVelocity = false;
1597
+ +
1598
+ }
1599
+
1600
+ /**
1601
+ @@ -232,6 +241,88 @@ class NodeMaterialObserver {
1602
+
1603
+ }
1604
+
1605
+ + /**
1606
+ + * Returns `true` if the given render object is a static object whose per-object state - world
1607
+ + * matrix, material, geometry and the scene environment - did not change since the last
1608
+ + * refresh. A settled object is still drawn every frame and its FRAME and RENDER nodes still
1609
+ + * have to be updated; what it may skip is the work whose inputs it froze: its OBJECT node
1610
+ + * updates, its object uniform group upload and its per-draw attribute scan.
1611
+ + *
1612
+ + * @param {RenderObject} renderObject - The render object.
1613
+ + * @return {boolean} Whether the given render object is settled or not.
1614
+ + */
1615
+ + isSettled( renderObject ) {
1616
+ +
1617
+ + const data = this.renderObjects.get( renderObject );
1618
+ +
1619
+ + return data !== undefined && data.settledState === true;
1620
+ +
1621
+ + }
1622
+ +
1623
+ + /**
1624
+ + * Recomputes {@link NodeMaterialObserver#isSettled} for the given render object, once per draw,
1625
+ + * so every step of that draw reads the same answer.
1626
+ + *
1627
+ + * @param {RenderObject} renderObject - The render object.
1628
+ + * @return {boolean} Whether the given render object is settled or not.
1629
+ + */
1630
+ + updateSettled( renderObject ) {
1631
+ +
1632
+ + const { object, material, geometry, scene } = renderObject;
1633
+ + const data = this.getRenderObjectData( renderObject );
1634
+ +
1635
+ + if ( object.static !== true || this.renderingVelocity === true ) {
1636
+ +
1637
+ + data.settledState = false;
1638
+ +
1639
+ + return false;
1640
+ +
1641
+ + }
1642
+ +
1643
+ + let settled = data.settled;
1644
+ +
1645
+ + if ( settled === undefined ) {
1646
+ +
1647
+ + data.settled = settled = {
1648
+ + matrix: object.matrixWorld.clone(),
1649
+ + environmentRotation: scene.environmentRotation.clone(),
1650
+ + environmentIntensity: scene.environmentIntensity,
1651
+ + materialVersion: material.version,
1652
+ + geometryVersion: geometry.version
1653
+ + };
1654
+ +
1655
+ + data.settledState = false;
1656
+ +
1657
+ + return false;
1658
+ +
1659
+ + }
1660
+ +
1661
+ + if ( settled.matrix.equals( object.matrixWorld ) !== true ||
1662
+ + settled.environmentRotation.equals( scene.environmentRotation ) !== true ||
1663
+ + settled.environmentIntensity !== scene.environmentIntensity ||
1664
+ + settled.materialVersion !== material.version ||
1665
+ + settled.geometryVersion !== geometry.version ) {
1666
+ +
1667
+ + // re-arm with the current state, so the object settles again once it stops changing
1668
+ +
1669
+ + settled.matrix.copy( object.matrixWorld );
1670
+ + settled.environmentRotation.copy( scene.environmentRotation );
1671
+ + settled.environmentIntensity = scene.environmentIntensity;
1672
+ + settled.materialVersion = material.version;
1673
+ + settled.geometryVersion = geometry.version;
1674
+ +
1675
+ + data.settledState = false;
1676
+ +
1677
+ + return false;
1678
+ +
1679
+ + }
1680
+ +
1681
+ + data.settledState = true;
1682
+ +
1683
+ + return true;
1684
+ +
1685
+ + }
1686
+ +
1687
+ /**
1688
+ * Returns an attribute data structure holding the attributes versions for
1689
+ * monitoring.
1690
+ @@ -298,7 +389,6 @@ class NodeMaterialObserver {
1691
+
1692
+ data = {
1693
+ _renderId: - 1,
1694
+ - _equal: false,
1695
+
1696
+ attributes: this.getAttributesData( geometry.attributes ),
1697
+ indexId: geometry.index ? geometry.index.id : null,
1698
+ @@ -327,7 +417,7 @@ class NodeMaterialObserver {
1699
+
1700
+ if ( data === undefined ) {
1701
+
1702
+ - data = { _renderId: - 1, _equal: false };
1703
+ + data = { _renderId: - 1 };
1704
+
1705
+ for ( const property of this.refreshUniforms ) {
1706
+
1707
+ @@ -411,7 +501,6 @@ class NodeMaterialObserver {
1708
+
1709
+ value.copy( mtlValue );
1710
+
1711
+ - materialData._equal = false;
1712
+ return false;
1713
+
1714
+ }
1715
+ @@ -423,7 +512,6 @@ class NodeMaterialObserver {
1716
+ value.id = mtlValue.id;
1717
+ value.version = mtlValue.version;
1718
+
1719
+ - materialData._equal = false;
1720
+ return false;
1721
+
1722
+ }
1723
+ @@ -432,7 +520,6 @@ class NodeMaterialObserver {
1724
+
1725
+ materialData[ property ] = mtlValue;
128
1726
 
129
- @@ -30719,6 +30757,8 @@ class RenderObjects {
1727
+ - materialData._equal = false;
1728
+ return false;
130
1729
 
131
- renderObject.version = material.version;
1730
+ }
1731
+ @@ -448,18 +535,12 @@ class NodeMaterialObserver {
1732
+ renderObjectData.bufferWidth = width;
1733
+ renderObjectData.bufferHeight = height;
1734
+
1735
+ - materialData._equal = false;
1736
+ return false;
132
1737
 
133
- + if ( sourceTracked ) renderObject.sourceVersion = sourceMaterial.version;
134
- +
135
1738
  }
136
1739
 
137
1740
  }
138
- @@ -39725,7 +39765,8 @@ class VelocityNode extends TempNode {
139
-
140
- const previousModelMatrix = getPreviousMatrix( object );
141
1741
 
142
- - this.previousModelWorldMatrix.value.copy( previousModelMatrix );
143
- + const scheduledPrevious = object[ Symbol.for( 'threenative.velocity.previousWorldMatrix' ) ];
144
- + this.previousModelWorldMatrix.value.copy( scheduledPrevious ?? previousModelMatrix );
1742
+ - materialData._equal = true;
1743
+ -
1744
+ - } else {
1745
+ -
1746
+ - if ( materialData._equal === false ) return false;
145
1747
 
146
- //
1748
+ }
147
1749
 
148
- @@ -53841,7 +53882,7 @@ class NodeBuilder {
1750
+ @@ -502,7 +583,6 @@ class NodeMaterialObserver {
1751
+ // attribute was removed
1752
+ delete storedAttributes[ name ];
149
1753
 
150
- const mrt = this.renderer.getMRT();
1754
+ - geometryData._equal = false;
1755
+ return false;
151
1756
 
152
- - return ( mrt && mrt.has( 'velocity' ) ) || getDataFromObject( this.object ).useVelocity === true;
153
- + return ( mrt && mrt.has( 'velocity' ) ) || getDataFromObject( this.object ).useVelocity === true || this.object.userData?.useVelocity === true;
1757
+ }
1758
+ @@ -515,7 +595,6 @@ class NodeMaterialObserver {
1759
+ storedAttributeData.id = id;
1760
+ storedAttributeData.version = version;
154
1761
 
155
- }
1762
+ - geometryData._equal = false;
1763
+ return false;
156
1764
 
157
- @@ -62731,6 +62772,7 @@ class Renderer {
158
- let materialDisplacementMap;
159
- let materialDisplacementScale;
160
- let materialDisplacementBias;
161
- + let materialAlphaTest;
1765
+ }
1766
+ @@ -526,7 +605,6 @@ class NodeMaterialObserver {
162
1767
 
163
- const previousSourceMaterial = this._currentSourceMaterial;
1768
+ geometryData.attributes = this.getAttributesData( attributes );
164
1769
 
165
- @@ -62756,6 +62798,7 @@ class Renderer {
166
- materialDisplacementMap = overrideMaterial.displacementMap;
167
- materialDisplacementScale = overrideMaterial.displacementScale;
168
- materialDisplacementBias = overrideMaterial.displacementBias;
169
- + materialAlphaTest = overrideMaterial.alphaTest;
1770
+ - geometryData._equal = false;
1771
+ return false;
170
1772
 
171
- if ( material.positionNode && material.positionNode.isNode ) {
1773
+ }
1774
+ @@ -544,7 +622,6 @@ class NodeMaterialObserver {
1775
+ geometryData.indexId = currentIndexId;
1776
+ geometryData.indexVersion = currentIndexVersion;
172
1777
 
173
- @@ -62763,7 +62806,15 @@ class Renderer {
1778
+ - geometryData._equal = false;
1779
+ return false;
174
1780
 
175
1781
  }
1782
+ @@ -556,16 +633,10 @@ class NodeMaterialObserver {
1783
+ geometryData.drawRange.start = geometry.drawRange.start;
1784
+ geometryData.drawRange.count = geometry.drawRange.count;
176
1785
 
177
- - overrideMaterial.alphaTest = material.alphaTest;
178
- + // `Material.alphaTest`'s setter bumps `version` whenever alpha-test-ness flips, and this
179
- + // override material is shared by every caster in the pass. A scene that mixes alpha-tested
180
- + // casters with opaque ones therefore moves that shared version several times per frame,
181
- + // and every OTHER render object drawn with it then fails the version gate in
182
- + // `RenderObjects.get` and recomputes a full material cache key that always matches.
183
- + // Writing the backing field leaves the `alphaTest` getter — and so the cache key — still
184
- + // reporting this caster's value, while each render object tracks the staleness signal
185
- + // that belongs to it: its own source material's version.
186
- + overrideMaterial._alphaTest = material.alphaTest;
187
- overrideMaterial.alphaMap = material.alphaMap;
188
- overrideMaterial.displacementMap = material.displacementMap;
189
- overrideMaterial.displacementScale = material.displacementScale;
190
- @@ -62825,6 +62876,7 @@ class Renderer {
191
- scene.overrideMaterial.displacementMap = materialDisplacementMap;
192
- scene.overrideMaterial.displacementScale = materialDisplacementScale;
193
- scene.overrideMaterial.displacementBias = materialDisplacementBias;
194
- + scene.overrideMaterial._alphaTest = materialAlphaTest;
1786
+ - geometryData._equal = false;
1787
+ return false;
195
1788
 
196
- }
1789
+ }
197
1790
 
198
- @@ -79586,7 +79638,8 @@ class WGSLNodeBuilder extends NodeBuilder {
1791
+ - geometryData._equal = true;
1792
+ -
1793
+ - } else {
1794
+ -
1795
+ - if ( geometryData._equal === false ) return false;
199
1796
 
200
- uniformGPU = buffer;
1797
+ }
201
1798
 
202
- - uniformNode.name = name ? name : 'NodeBuffer_' + uniformNode.id;
203
- + // The base builder supplies a unique name within this program. A process-wide
204
- + // allocation id changes otherwise identical shader source for every buffer.
1799
+ @@ -716,7 +787,14 @@ class NodeMaterialObserver {
1800
+ */
1801
+ needsRefresh( renderObject, nodeFrame ) {
205
1802
 
206
- } else {
1803
+ - if ( this.hasNode || this.hasAnimation || this.firstInitialization( renderObject ) || this.needsVelocity( nodeFrame.renderer ) )
1804
+ + this.renderingVelocity = this.needsVelocity( nodeFrame.renderer );
1805
+ +
1806
+ + // Answered for the whole draw, so updateBefore, the geometry scan, the node updates and
1807
+ + // the object uniform upload of this object all skip or all refresh.
1808
+ +
1809
+ + this.updateSettled( renderObject );
1810
+ +
1811
+ + if ( this.hasNode || this.hasAnimation || this.firstInitialization( renderObject ) || this.renderingVelocity )
1812
+ return true;
207
1813
 
208
- @@ -83814,6 +83867,14 @@ class WebGPUBackend extends Backend {
209
- */
210
- this.compatibilityMode = null;
1814
+ const { renderId } = nodeFrame;
1815
+ @@ -729,10 +807,14 @@ class NodeMaterialObserver {
211
1816
 
212
- + /**
213
- + * A reference to the WebGPU instance that creates adapters.
214
- + *
215
- + * @type {?GPU}
216
- + * @default null
217
- + */
218
- + this.gpu = null;
219
- +
220
- /**
221
- * A reference to the device.
222
- *
223
- @@ -83926,7 +83987,9 @@ class WebGPUBackend extends Backend {
224
- xrCompatible: renderer.xr.enabled
225
- };
1817
+ }
226
1818
 
227
- - const adapter = ( typeof navigator !== 'undefined' ) ? await navigator.gpu.requestAdapter( adapterOptions ) : null;
228
- + this.gpu = ( typeof navigator !== 'undefined' ) ? navigator.gpu : null;
229
- +
230
- + const adapter = this.gpu !== null ? await this.gpu.requestAdapter( adapterOptions ) : null;
1819
+ - const isStatic = renderObject.object.static === true;
1820
+ const isBundle = renderObject.bundle !== null && renderObject.bundle.static === true && this.getRenderObjectData( renderObject ).version === renderObject.bundle.version;
231
1821
 
232
- if ( adapter === null ) {
1822
+ - if ( isStatic || isBundle )
1823
+ + // `object.static` is not the question - it is a switch, and a brand new object has it set
1824
+ + // before it has been drawn once, so answering from it skips the draw that creates this
1825
+ + // object's bind groups and leaves the pipeline reading a layout that was never built. A
1826
+ + // static object that moved, or whose material or geometry was edited, has to refresh like
1827
+ + // any other; only a settled one may skip, and nothing is settled before its first refresh.
1828
+ + if ( this.isSettled( renderObject ) === true || isBundle )
1829
+ return false;
233
1830
 
1831
+ const lightsData = this.getLights( renderObject.lightsNode, renderId );
234
1832
  diff --git a/src/nodes/accessors/Batch.js b/src/nodes/accessors/Batch.js
235
- index ab62e98dadab2539d7e1d663fd42267bbc8ebcaa..2cf4bb6039853d5c922e89041419250b243694ef 100644
1833
+ index ab62e98..2cf4bb6 100644
236
1834
  --- a/src/nodes/accessors/Batch.js
237
1835
  +++ b/src/nodes/accessors/Batch.js
238
1836
  @@ -1,6 +1,6 @@
@@ -278,10 +1876,30 @@ index ab62e98dadab2539d7e1d663fd42267bbc8ebcaa..2cf4bb6039853d5c922e89041419250b
278
1876
  -
279
1877
  -
280
1878
  diff --git a/src/nodes/accessors/Instance.js b/src/nodes/accessors/Instance.js
281
- index b89834b9166c75fd946ade24db265bf8c367d244..c7b7bdd7a31d852a581d4bc75a1e1afc1d214e5e 100644
1879
+ index b89834b..ed00e75 100644
282
1880
  --- a/src/nodes/accessors/Instance.js
283
1881
  +++ b/src/nodes/accessors/Instance.js
284
- @@ -220,7 +220,8 @@ export const instance = /*@__PURE__*/ Fn( ( [ matrices, colors = null ], builder
1882
+ @@ -40,7 +40,8 @@ function createInstanceMatrixNode( builder, instanceMatrix ) {
1883
+
1884
+ const uniformBufferSize = matrixCount * 16 * 4;
1885
+
1886
+ - if ( uniformBufferSize <= builder.getUniformBufferLimit() ) {
1887
+ + // threenative: the uniform path re-uploads every draw; the attribute path uploads on version change
1888
+ + if ( false ) {
1889
+
1890
+ instanceMatrixNode = buffer( instanceMatrix.array, 'mat4', matrixCount ).element( instanceIndex );
1891
+
1892
+ @@ -134,7 +135,8 @@ export const instance = /*@__PURE__*/ Fn( ( [ matrices, colors = null ], builder
1893
+
1894
+ const uniformBufferSize = Math.max( matrices.count, 1 ) * 16 * 4;
1895
+
1896
+ - if ( uniformBufferSize > builder.getUniformBufferLimit() ) {
1897
+ + // threenative: the uniform path re-uploads every draw; the attribute path uploads on version change
1898
+ + if ( true ) {
1899
+
1900
+ interleavedMatrix = _matrixBuffers.get( matrices );
1901
+
1902
+ @@ -220,7 +222,8 @@ export const instance = /*@__PURE__*/ Fn( ( [ matrices, colors = null ], builder
285
1903
 
286
1904
  const previousInstanceData = _previousInstanceMatrices.get( object );
287
1905
 
@@ -292,7 +1910,7 @@ index b89834b9166c75fd946ade24db265bf8c367d244..c7b7bdd7a31d852a581d4bc75a1e1afc
292
1910
  } );
293
1911
 
294
1912
  diff --git a/src/nodes/accessors/Skinning.js b/src/nodes/accessors/Skinning.js
295
- index 2356aed8656dceb1ea74b73ea2e739371a960484..aaeea633a8ece291be5faf868e312867ddad01b6 100644
1913
+ index 2356aed..aaeea63 100644
296
1914
  --- a/src/nodes/accessors/Skinning.js
297
1915
  +++ b/src/nodes/accessors/Skinning.js
298
1916
  @@ -149,7 +149,8 @@ export const skinning = /*@__PURE__*/ Fn( ( [ skinnedMesh ], builder ) => {
@@ -316,7 +1934,7 @@ index 2356aed8656dceb1ea74b73ea2e739371a960484..aaeea633a8ece291be5faf868e312867
316
1934
  }
317
1935
 
318
1936
  diff --git a/src/nodes/accessors/VelocityNode.js b/src/nodes/accessors/VelocityNode.js
319
- index 373548b0b587e7da3ef0993bfe316af420e7f66c..c48002abbce20ee8b29908cf314d7b03afe0db03 100644
1937
+ index 373548b..c48002a 100644
320
1938
  --- a/src/nodes/accessors/VelocityNode.js
321
1939
  +++ b/src/nodes/accessors/VelocityNode.js
322
1940
  @@ -106,7 +106,8 @@ class VelocityNode extends TempNode {
@@ -330,7 +1948,7 @@ index 373548b0b587e7da3ef0993bfe316af420e7f66c..c48002abbce20ee8b29908cf314d7b03
330
1948
  //
331
1949
 
332
1950
  diff --git a/src/nodes/core/NodeBuilder.js b/src/nodes/core/NodeBuilder.js
333
- index be00b81ef36e1af83b0066603e616f8f1f81a115..1990da8fe8f1f29a7b53030756dfd966d8de3cb6 100644
1951
+ index be00b81..1990da8 100644
334
1952
  --- a/src/nodes/core/NodeBuilder.js
335
1953
  +++ b/src/nodes/core/NodeBuilder.js
336
1954
  @@ -3450,7 +3450,7 @@ class NodeBuilder {
@@ -343,7 +1961,7 @@ index be00b81ef36e1af83b0066603e616f8f1f81a115..1990da8fe8f1f29a7b53030756dfd966
343
1961
  }
344
1962
 
345
1963
  diff --git a/src/objects/BatchedMesh.js b/src/objects/BatchedMesh.js
346
- index 2ef7cb257438826059eb2408b3756c4891f06a10..35cc0ffe83a69ea9fa5fca6dec2d820ca193f88d 100644
1964
+ index 2ef7cb2..35cc0ff 100644
347
1965
  --- a/src/objects/BatchedMesh.js
348
1966
  +++ b/src/objects/BatchedMesh.js
349
1967
  @@ -181,6 +181,8 @@ function copyArrayContents( src, target ) {
@@ -356,9 +1974,18 @@ index 2ef7cb257438826059eb2408b3756c4891f06a10..35cc0ffe83a69ea9fa5fca6dec2d820c
356
1974
  * Constructs a new batched mesh.
357
1975
  *
358
1976
  diff --git a/src/renderers/common/Bindings.js b/src/renderers/common/Bindings.js
359
- index 5f3d213a016b37db3b2e5d022da3dd0234d86f6d..7a9a168f29773f37ef9a51ef9f07ade9f3ea95ef 100644
1977
+ index 5f3d213..a3cec4e 100644
360
1978
  --- a/src/renderers/common/Bindings.js
361
1979
  +++ b/src/renderers/common/Bindings.js
1980
+ @@ -145,7 +145,7 @@ class Bindings extends DataMap {
1981
+ */
1982
+ updateForRender( renderObject ) {
1983
+
1984
+ - this._updateBindings( this.getForRender( renderObject ) );
1985
+ + this._updateBindings( this.getForRender( renderObject ), this.nodes.isSettled( renderObject ) );
1986
+
1987
+ }
1988
+
362
1989
  @@ -206,10 +206,14 @@ class Bindings extends DataMap {
363
1990
 
364
1991
  } else if ( binding.isSampledTexture ) {
@@ -374,8 +2001,165 @@ index 5f3d213a016b37db3b2e5d022da3dd0234d86f6d..7a9a168f29773f37ef9a51ef9f07ade9
374
2001
  this.textures.updateSampler( binding );
375
2002
 
376
2003
  } else if ( binding.isStorageBuffer ) {
2004
+ @@ -292,12 +296,13 @@ class Bindings extends DataMap {
2005
+ * Updates the given array of bindings.
2006
+ *
2007
+ * @param {Array<BindGroup>} bindings - The bind groups.
2008
+ + * @param {boolean} [settled] - Whether the render object is a settled static object.
2009
+ */
2010
+ - _updateBindings( bindings ) {
2011
+ + _updateBindings( bindings, settled ) {
2012
+
2013
+ for ( const bindGroup of bindings ) {
2014
+
2015
+ - this._update( bindGroup, bindings );
2016
+ + this._update( bindGroup, bindings, settled );
2017
+
2018
+ }
2019
+
2020
+ @@ -308,11 +313,27 @@ class Bindings extends DataMap {
2021
+ *
2022
+ * @param {BindGroup} bindGroup - The bind group to update.
2023
+ * @param {Array<BindGroup>} bindings - The bind groups.
2024
+ + * @param {boolean} [settled] - Whether the render object is a settled static object.
2025
+ */
2026
+ - _update( bindGroup, bindings ) {
2027
+ + _update( bindGroup, bindings, settled ) {
2028
+
2029
+ const { backend } = this;
2030
+
2031
+ + // A group a node added after this object's first draw has no buffers and no layout yet, and
2032
+ + // the pipeline reads that layout whatever this draw decides to do with it - so it is built
2033
+ + // here, on every draw. A group that did not exist cannot have been written either, so it is
2034
+ + // written now: `settled` is what tells the loop below to write it, not to skip it.
2035
+ + if ( this.get( bindGroup ).bindGroup === undefined ) {
2036
+ +
2037
+ + this._createBindings( [ bindGroup ] );
2038
+ +
2039
+ + settled = false;
2040
+ +
2041
+ + }
2042
+ +
2043
+ + // a settled object keeps the values it uploaded the last time it refreshed, so an object
2044
+ + // group it has already written is the one thing in here it may skip
2045
+ +
2046
+ let needsBindingsUpdate = false;
2047
+ let cacheBindings = true;
2048
+ let cacheIndex = 0;
2049
+ @@ -322,7 +343,7 @@ class Bindings extends DataMap {
2050
+
2051
+ for ( const binding of bindGroup.bindings ) {
2052
+
2053
+ - const updatedGroup = this.nodes.updateGroup( binding );
2054
+ + const updatedGroup = this.nodes.updateGroup( binding, settled );
2055
+
2056
+ // every uniforms group is a uniform buffer. So if no update is required,
2057
+ // we move one with the next binding. Otherwise the next if block will update the group.
2058
+ diff --git a/src/renderers/common/Geometries.js b/src/renderers/common/Geometries.js
2059
+ index eb332c4..979da2f 100644
2060
+ --- a/src/renderers/common/Geometries.js
2061
+ +++ b/src/renderers/common/Geometries.js
2062
+ @@ -139,6 +139,14 @@ class Geometries extends DataMap {
2063
+ */
2064
+ this._geometryDisposeListeners = new Map();
2065
+
2066
+ + /**
2067
+ + * This Weak Map holds the attribute versions a settled render object was last drawn with,
2068
+ + * so a write into one of them is what brings the object back out of the settled path.
2069
+ + *
2070
+ + * @type {WeakMap<RenderObject,Map<BufferAttribute,number>>}
2071
+ + */
2072
+ + this._settledAttributes = new WeakMap();
2073
+ +
2074
+ }
2075
+
2076
+ /**
2077
+ @@ -164,10 +172,69 @@ class Geometries extends DataMap {
2078
+
2079
+ if ( this.has( renderObject ) === false ) this.initGeometry( renderObject );
2080
+
2081
+ + // A settled static object has no attribute that could have changed since the last refresh,
2082
+ + // unless a writer bumped one. An InstancedMesh's instance buffer is a vertex attribute of
2083
+ + // this draw, and a BufferGeometry carries no version of its own to announce the write, so
2084
+ + // the attributes' own versions are the signal - and the only one there is.
2085
+ +
2086
+ + const monitor = renderObject.getMonitor();
2087
+ +
2088
+ + if ( monitor !== undefined && monitor.isSettled( renderObject ) === true && this.attributesChanged( renderObject ) === false ) return;
2089
+ +
2090
+ this.updateAttributes( renderObject );
2091
+
2092
+ }
2093
+
2094
+ + /**
2095
+ + * Returns `true` if an attribute of the given render object - or its index - was written to
2096
+ + * since the last time its versions were read, and records the versions it holds now.
2097
+ + *
2098
+ + * @param {RenderObject} renderObject - The render object.
2099
+ + * @return {boolean} Whether any of the render object's versions moved.
2100
+ + */
2101
+ + attributesChanged( renderObject ) {
2102
+ +
2103
+ + const attributes = renderObject.getAttributes();
2104
+ + const index = this.getIndex( renderObject );
2105
+ + const settled = this._settledAttributes.get( renderObject );
2106
+ +
2107
+ + if ( settled !== undefined && settled.size === attributes.length + ( index === null ? 0 : 1 ) ) {
2108
+ +
2109
+ + let changed = false;
2110
+ +
2111
+ + for ( const attribute of attributes ) {
2112
+ +
2113
+ + if ( settled.get( attribute.isInterleavedBufferAttribute ? attribute.data : attribute ) !== attribute.version ) {
2114
+ +
2115
+ + changed = true;
2116
+ + break;
2117
+ +
2118
+ + }
2119
+ +
2120
+ + }
2121
+ +
2122
+ + if ( changed === false && ( index === null || settled.get( index ) === index.version ) ) return false;
2123
+ +
2124
+ + }
2125
+ +
2126
+ + const versions = new Map();
2127
+ +
2128
+ + for ( const attribute of attributes ) {
2129
+ +
2130
+ + const data = attribute.isInterleavedBufferAttribute ? attribute.data : attribute;
2131
+ +
2132
+ + versions.set( data, data.version );
2133
+ +
2134
+ + }
2135
+ +
2136
+ + if ( index !== null ) versions.set( index, index.version );
2137
+ +
2138
+ + this._settledAttributes.set( renderObject, versions );
2139
+ +
2140
+ + return true;
2141
+ +
2142
+ + }
2143
+ +
2144
+ /**
2145
+ * Initializes the geometry of the given render object.
2146
+ *
2147
+ diff --git a/src/renderers/common/RenderContexts.js b/src/renderers/common/RenderContexts.js
2148
+ index 64ecdee..7319e25 100644
2149
+ --- a/src/renderers/common/RenderContexts.js
2150
+ +++ b/src/renderers/common/RenderContexts.js
2151
+ @@ -77,7 +77,8 @@ class RenderContexts {
2152
+
2153
+ }
2154
+
2155
+ - if ( renderTarget !== null ) renderState.sampleCount = renderTarget.samples === 0 ? 1 : renderTarget.samples;
2156
+ + const sampleCount = renderTarget !== null ? renderTarget.samples : this.renderer.currentSamples;
2157
+ + renderState.sampleCount = sampleCount === 0 ? 1 : sampleCount;
2158
+
2159
+ renderState.clearDepthValue = this.renderer.getClearDepth();
2160
+ renderState.clearStencilValue = this.renderer.getClearStencil();
377
2161
  diff --git a/src/renderers/common/RenderObject.js b/src/renderers/common/RenderObject.js
378
- index c900fa4c9654f8cf3512e96de52a2c1c83e189a6..d0a87eabffc8f667ebe9b2f0eccda0ddf3309652 100644
2162
+ index c900fa4..a3dcba2 100644
379
2163
  --- a/src/renderers/common/RenderObject.js
380
2164
  +++ b/src/renderers/common/RenderObject.js
381
2165
  @@ -299,6 +299,18 @@ class RenderObject {
@@ -397,11 +2181,37 @@ index c900fa4c9654f8cf3512e96de52a2c1c83e189a6..d0a87eabffc8f667ebe9b2f0eccda0dd
397
2181
  /**
398
2182
  * An event listener which is defined by `RenderObjects`. It performs
399
2183
  * clean up tasks when `dispose()` on this render object.
2184
+ @@ -486,7 +498,7 @@ class RenderObject {
2185
+ */
2186
+ getChainArray() {
2187
+
2188
+ - return [ this.object, this.material, this.context, this.lightsNode ];
2189
+ + return [ this.object, this.material, this.context, this.lightsNode, this._sourceMaterial ?? this.material ];
2190
+
2191
+ }
2192
+
2193
+ @@ -936,6 +948,7 @@ class RenderObject {
2194
+ }
2195
+
2196
+ cacheKey = hash( cacheKey, this.renderer.contextNode.id, this.renderer.contextNode.version );
2197
+ + cacheKey = hash( cacheKey, this.context.sampleCount );
2198
+
2199
+ return cacheKey;
2200
+
400
2201
  diff --git a/src/renderers/common/RenderObjects.js b/src/renderers/common/RenderObjects.js
401
- index 676cc2bd4169427ed420f2467b1e6684bd2aeebc..0fa661db793548254a15ff43f39207abcc43ef49 100644
2202
+ index 676cc2b..72569d0 100644
402
2203
  --- a/src/renderers/common/RenderObjects.js
403
2204
  +++ b/src/renderers/common/RenderObjects.js
404
- @@ -124,9 +124,18 @@ class RenderObjects {
2205
+ @@ -97,6 +97,8 @@ class RenderObjects {
2206
+ _chainKeys[ 1 ] = material;
2207
+ _chainKeys[ 2 ] = renderContext;
2208
+ _chainKeys[ 3 ] = lightsNode;
2209
+ + // Material groups share the shadow override, but must retain separate GPU bindings.
2210
+ + _chainKeys[ 4 ] = this.renderer._currentSourceMaterial ?? material;
2211
+
2212
+ //
2213
+
2214
+ @@ -124,9 +126,18 @@ class RenderObjects {
405
2215
 
406
2216
  }
407
2217
 
@@ -422,7 +2232,7 @@ index 676cc2bd4169427ed420f2467b1e6684bd2aeebc..0fa661db793548254a15ff43f39207ab
422
2232
 
423
2233
  renderObject.dispose();
424
2234
 
425
- @@ -136,6 +145,8 @@ class RenderObjects {
2235
+ @@ -136,6 +147,8 @@ class RenderObjects {
426
2236
 
427
2237
  renderObject.version = material.version;
428
2238
 
@@ -431,8 +2241,16 @@ index 676cc2bd4169427ed420f2467b1e6684bd2aeebc..0fa661db793548254a15ff43f39207ab
431
2241
  }
432
2242
 
433
2243
  }
2244
+ @@ -148,6 +161,7 @@ class RenderObjects {
2245
+ _chainKeys[ 1 ] = null;
2246
+ _chainKeys[ 2 ] = null;
2247
+ _chainKeys[ 3 ] = null;
2248
+ + _chainKeys[ 4 ] = null;
2249
+
2250
+ //
2251
+
434
2252
  diff --git a/src/renderers/common/Renderer.js b/src/renderers/common/Renderer.js
435
- index 8dfc2ae055ad7b9d4c00e91f7d52a39e747caebe..e9d27b3ddad3b1f080a717d1f6683b60afd2b259 100644
2253
+ index 8dfc2ae..89b33ef 100644
436
2254
  --- a/src/renderers/common/Renderer.js
437
2255
  +++ b/src/renderers/common/Renderer.js
438
2256
  @@ -3550,6 +3550,7 @@ class Renderer {
@@ -476,8 +2294,162 @@ index 8dfc2ae055ad7b9d4c00e91f7d52a39e747caebe..e9d27b3ddad3b1f080a717d1f6683b60
476
2294
 
477
2295
  }
478
2296
 
2297
+ @@ -3705,18 +3716,17 @@ class Renderer {
2298
+
2299
+ //
2300
+
2301
+ - const needsRefresh = this._nodes.needsRefresh( renderObject );
2302
+ + this._nodes.needsRefresh( renderObject );
2303
+
2304
+ - if ( needsRefresh ) {
2305
+ -
2306
+ - this._nodes.updateBefore( renderObject );
2307
+ -
2308
+ - this._geometries.updateForRender( renderObject );
2309
+ -
2310
+ - this._nodes.updateForRender( renderObject );
2311
+ - this._bindings.updateForRender( renderObject );
2312
+ -
2313
+ - }
2314
+ + // Every step of a draw runs, settled or not. What a settled static object skips is decided
2315
+ + // inside each of them - its OBJECT node updates, its object uniform group write and its
2316
+ + // attribute scan - because each of those answers for itself, and a step left out here
2317
+ + // cannot: a group a node added after this object's first draw still has to be created, and
2318
+ + // the pipeline reads that group's layout whatever this draw decided to do with it.
2319
+ + this._geometries.updateForRender( renderObject );
2320
+ + this._nodes.updateBefore( renderObject );
2321
+ + this._nodes.updateForRender( renderObject );
2322
+ + this._bindings.updateForRender( renderObject );
2323
+
2324
+ this._pipelines.updateForRender( renderObject );
2325
+
2326
+ @@ -3726,7 +3736,7 @@ class Renderer {
2327
+
2328
+ this.backend.draw( renderObject, this.info );
2329
+
2330
+ - if ( needsRefresh ) this._nodes.updateAfter( renderObject );
2331
+ + this._nodes.updateAfter( renderObject );
2332
+
2333
+ }
2334
+
2335
+ diff --git a/src/renderers/common/Textures.js b/src/renderers/common/Textures.js
2336
+ index f42b90e..be47645 100644
2337
+ --- a/src/renderers/common/Textures.js
2338
+ +++ b/src/renderers/common/Textures.js
2339
+ @@ -215,6 +215,21 @@ class Textures extends DataMap {
2340
+
2341
+ backend.destroyTexture( texture );
2342
+
2343
+ + // delete cached bind groups so they don't point to the destroyed texture
2344
+ +
2345
+ + if ( textureData.bindGroups ) {
2346
+ +
2347
+ + for ( const bindGroup of textureData.bindGroups ) {
2348
+ +
2349
+ + const bindingsData = this.backend.get( bindGroup );
2350
+ +
2351
+ + bindingsData.groups = undefined;
2352
+ + bindingsData.versions = undefined;
2353
+ +
2354
+ + }
2355
+ +
2356
+ + }
2357
+ +
2358
+ }
2359
+
2360
+ //
2361
+ diff --git a/src/renderers/common/nodes/NodeManager.js b/src/renderers/common/nodes/NodeManager.js
2362
+ index f6d4f0b..033128c 100644
2363
+ --- a/src/renderers/common/nodes/NodeManager.js
2364
+ +++ b/src/renderers/common/nodes/NodeManager.js
2365
+ @@ -109,15 +109,17 @@ class NodeManager extends DataMap {
2366
+ * Returns `true` if the given node uniforms group must be updated or not.
2367
+ *
2368
+ * @param {NodeUniformsGroup} nodeUniformsGroup - The node uniforms group.
2369
+ + * @param {boolean} [settled] - Whether the render object is a settled static object.
2370
+ * @return {boolean} Whether the node uniforms group requires an update or not.
2371
+ */
2372
+ - updateGroup( nodeUniformsGroup ) {
2373
+ + updateGroup( nodeUniformsGroup, settled ) {
2374
+
2375
+ const groupNode = nodeUniformsGroup.groupNode;
2376
+
2377
+ - // groups that are updated per object always require an update so no further checks are needed
2378
+ + // groups that are updated per object always require an update so no further checks are needed.
2379
+ + // A settled static object keeps the values it uploaded the last time it was refreshed.
2380
+
2381
+ - if ( groupNode.updateType === NodeUpdateType.OBJECT ) return true;
2382
+ + if ( groupNode.updateType === NodeUpdateType.OBJECT ) return settled !== true;
2383
+
2384
+ // check for update
2385
+
2386
+ @@ -921,6 +923,21 @@ class NodeManager extends DataMap {
2387
+
2388
+ }
2389
+
2390
+ + /**
2391
+ + * Returns `true` if the per-object updates of the given render object can be skipped
2392
+ + * because the object is static and nothing it depends on changed since its last refresh.
2393
+ + *
2394
+ + * @param {RenderObject} renderObject - The render object.
2395
+ + * @return {boolean} Whether the render object is settled or not.
2396
+ + */
2397
+ + isSettled( renderObject ) {
2398
+ +
2399
+ + const monitor = renderObject.getMonitor();
2400
+ +
2401
+ + return monitor !== undefined ? monitor.isSettled( renderObject ) : false;
2402
+ +
2403
+ + }
2404
+ +
2405
+ /**
2406
+ * Triggers the call of `updateBefore()` methods
2407
+ * for all nodes of the given render object.
2408
+ @@ -930,9 +947,14 @@ class NodeManager extends DataMap {
2409
+ updateBefore( renderObject ) {
2410
+
2411
+ const nodeBuilder = renderObject.getNodeBuilderState();
2412
+ + const settled = this.isSettled( renderObject );
2413
+
2414
+ for ( const node of nodeBuilder.updateBeforeNodes ) {
2415
+
2416
+ + // a settled static object has nothing to update per object
2417
+ +
2418
+ + if ( settled === true && node.getUpdateBeforeType() === NodeUpdateType.OBJECT ) continue;
2419
+ +
2420
+ // update frame state for each node
2421
+
2422
+ this.getNodeFrameForRender( renderObject ).updateBeforeNode( node );
2423
+ @@ -950,10 +972,11 @@ class NodeManager extends DataMap {
2424
+ updateAfter( renderObject ) {
2425
+
2426
+ const nodeBuilder = renderObject.getNodeBuilderState();
2427
+ + const settled = this.isSettled( renderObject );
2428
+
2429
+ for ( const node of nodeBuilder.updateAfterNodes ) {
2430
+
2431
+ - // update frame state for each node
2432
+ + if ( settled === true && node.getUpdateAfterType() === NodeUpdateType.OBJECT ) continue;
2433
+
2434
+ this.getNodeFrameForRender( renderObject ).updateAfterNode( node );
2435
+
2436
+ @@ -990,9 +1013,14 @@ class NodeManager extends DataMap {
2437
+
2438
+ const nodeFrame = this.getNodeFrameForRender( renderObject );
2439
+ const nodeBuilder = renderObject.getNodeBuilderState();
2440
+ + const settled = this.isSettled( renderObject );
2441
+
2442
+ for ( const node of nodeBuilder.updateNodes ) {
2443
+
2444
+ + // a settled static object keeps the per-object values it uploaded last time
2445
+ +
2446
+ + if ( settled === true && node.getUpdateType() === NodeUpdateType.OBJECT ) continue;
2447
+ +
2448
+ nodeFrame.updateNode( node );
2449
+
2450
+ }
479
2451
  diff --git a/src/renderers/webgpu/WebGPUBackend.js b/src/renderers/webgpu/WebGPUBackend.js
480
- index 06e203693ed9b168a2bddf773cd8ce8dc23196e4..37b5e0be09967e8acbd38e570b71eb201fc9bad7 100644
2452
+ index 06e2036..69b8b67 100644
481
2453
  --- a/src/renderers/webgpu/WebGPUBackend.js
482
2454
  +++ b/src/renderers/webgpu/WebGPUBackend.js
483
2455
  @@ -102,6 +102,14 @@ class WebGPUBackend extends Backend {
@@ -506,11 +2478,86 @@ index 06e203693ed9b168a2bddf773cd8ce8dc23196e4..37b5e0be09967e8acbd38e570b71eb20
506
2478
 
507
2479
  if ( adapter === null ) {
508
2480
 
2481
+ @@ -1749,13 +1759,12 @@ class WebGPUBackend extends Backend {
2482
+ for ( let i = 0, l = bindings.length; i < l; i ++ ) {
2483
+
2484
+ const bindGroup = bindings[ i ];
2485
+ + const bindingsData = this.get( bindGroup );
2486
+
2487
+ - if ( currentBindingGroups[ i ] !== bindGroup.id ) {
2488
+ -
2489
+ - const bindingsData = this.get( bindGroup );
2490
+ + if ( currentBindingGroups[ i ] !== bindingsData.group ) {
2491
+
2492
+ passEncoderGPU.setBindGroup( i, bindingsData.group );
2493
+ - currentBindingGroups[ i ] = bindGroup.id;
2494
+ + currentBindingGroups[ i ] = bindingsData.group;
2495
+
2496
+ }
2497
+
2498
+ @@ -2054,6 +2063,7 @@ class WebGPUBackend extends Backend {
2499
+ const utils = this.utils;
2500
+
2501
+ const sampleCount = utils.getSampleCountRenderContext( renderObject.context );
2502
+ + const bindGroupLayoutKeys = renderObject.getBindings().map( bindGroup => this.get( bindGroup ).layoutKey ).join( '|' );
2503
+ const colorSpace = utils.getCurrentColorSpace( renderObject.context );
2504
+ const colorFormat = utils.getCurrentColorFormat( renderObject.context );
2505
+ const depthStencilFormat = utils.getCurrentDepthStencilFormat( renderObject.context );
2506
+ @@ -2075,7 +2085,8 @@ class WebGPUBackend extends Backend {
2507
+ data.colorFormat !== colorFormat || data.depthStencilFormat !== depthStencilFormat ||
2508
+ data.primitiveTopology !== primitiveTopology ||
2509
+ data.frontFaceCW !== frontFaceCW ||
2510
+ - data.clippingContextCacheKey !== renderObject.clippingContextCacheKey
2511
+ + data.clippingContextCacheKey !== renderObject.clippingContextCacheKey ||
2512
+ + data.bindGroupLayoutKeys !== bindGroupLayoutKeys
2513
+ ) {
2514
+
2515
+ data.material = material; data.materialVersion = material.version;
2516
+ @@ -2095,6 +2106,7 @@ class WebGPUBackend extends Backend {
2517
+ data.primitiveTopology = primitiveTopology;
2518
+ data.frontFaceCW = frontFaceCW;
2519
+ data.clippingContextCacheKey = renderObject.clippingContextCacheKey;
2520
+ + data.bindGroupLayoutKeys = bindGroupLayoutKeys;
2521
+
2522
+ needsUpdate = true;
2523
+
2524
+ @@ -2116,6 +2128,7 @@ class WebGPUBackend extends Backend {
2525
+
2526
+ const utils = this.utils;
2527
+ const renderContext = renderObject.context;
2528
+ + const bindGroupLayoutKeys = renderObject.getBindings().map( bindGroup => this.get( bindGroup ).layoutKey ).join( '|' );
2529
+
2530
+ // meshes with negative scale have a different frontFace render pipeline
2531
+ // descriptor value so the following must be honored in the cache key
2532
+ @@ -2134,6 +2147,7 @@ class WebGPUBackend extends Backend {
2533
+ material.side,
2534
+ frontFaceCW,
2535
+ utils.getSampleCountRenderContext( renderContext ),
2536
+ + bindGroupLayoutKeys,
2537
+ utils.getCurrentColorSpace( renderContext ), utils.getCurrentColorFormat( renderContext ), utils.getCurrentDepthStencilFormat( renderContext ),
2538
+ utils.getPrimitiveTopology( object, material ),
2539
+ renderObject.getGeometryCacheKey(),
509
2540
  diff --git a/src/renderers/webgpu/nodes/WGSLNodeBuilder.js b/src/renderers/webgpu/nodes/WGSLNodeBuilder.js
510
- index be07536301cd47ef2544e4a54f6abe17407f52af..6144b98ddeb5d53c5cf280bb51401385a4427547 100644
2541
+ index be07536..2acc832 100644
511
2542
  --- a/src/renderers/webgpu/nodes/WGSLNodeBuilder.js
512
2543
  +++ b/src/renderers/webgpu/nodes/WGSLNodeBuilder.js
513
- @@ -1323,7 +1323,8 @@ class WGSLNodeBuilder extends NodeBuilder {
2544
+ @@ -741,7 +741,14 @@ class WGSLNodeBuilder extends NodeBuilder {
2545
+
2546
+ } else {
2547
+
2548
+ - snippet = `textureLoad( ${ textureProperty }, ${ uvIndexSnippet }, u32( ${ levelSnippet } ) )`;
2549
+ + // naga v25 (wgpu-native v25.0.2.2) rejects a u32 sample index on a
2550
+ + // multisampled textureLoad; i32 is accepted by every backend. Mip-level
2551
+ + // operands (including the array form above) stay u32.
2552
+ + const sampleData = texture ? this.renderer?.backend?.utils?.getTextureSampleData?.( texture ) : null;
2553
+ + const backendMultisampled = sampleData !== null && sampleData !== undefined && sampleData.primarySamples > 1;
2554
+ + const targetMultisampled = Boolean( texture && texture.renderTarget && texture.renderTarget.samples > 1 );
2555
+ + const indexType = ( backendMultisampled || targetMultisampled ) ? 'i32' : 'u32';
2556
+ + snippet = `textureLoad( ${ textureProperty }, ${ uvIndexSnippet }, ${ indexType }( ${ levelSnippet } ) )`;
2557
+
2558
+ if ( this.renderer.backend.compatibilityMode && texture.isDepthTexture ) {
2559
+
2560
+ @@ -1323,7 +1330,8 @@ class WGSLNodeBuilder extends NodeBuilder {
514
2561
 
515
2562
  uniformGPU = buffer;
516
2563
 
@@ -520,3 +2567,79 @@ index be07536301cd47ef2544e4a54f6abe17407f52af..6144b98ddeb5d53c5cf280bb51401385
520
2567
 
521
2568
  } else {
522
2569
 
2570
+ @@ -2066,9 +2074,12 @@ ${ flowData.code }
2571
+ const textureNode = uniform.node;
2572
+ const texture = textureNode.value;
2573
+
2574
+ - // Cube textures always need samplers (they use textureSampleLevel, not textureLoad)
2575
+ - // Also textureGather always need sampler.
2576
+ - const needsSampler = texture.isCubeTexture === true || ( this.isUnfilterable( texture ) === false && textureNode.isStorageTextureNode !== true ) || textureNode.gatherNode !== null;
2577
+ + // The bind group layout freezes sampler inclusion once, in getUniformFromNode
2578
+ + // (nodeData.uniformGPU). Re-deriving it here from live texture filter state can
2579
+ + // disagree with that layout and shift every later binding index.
2580
+ + const nodeData = this.getDataFromNode( textureNode, shaderStage, this.globalCache );
2581
+ + const uniformGPU = nodeData.uniformGPU;
2582
+ + const needsSampler = Array.isArray( uniformGPU ) && uniformGPU[ 0 ].isSampler === true && uniformGPU[ 0 ].isSampledTexture !== true;
2583
+
2584
+ if ( needsSampler ) {
2585
+
2586
+ diff --git a/src/renderers/webgpu/utils/WebGPUAttributeUtils.js b/src/renderers/webgpu/utils/WebGPUAttributeUtils.js
2587
+ index f683d74..83bbe18 100644
2588
+ --- a/src/renderers/webgpu/utils/WebGPUAttributeUtils.js
2589
+ +++ b/src/renderers/webgpu/utils/WebGPUAttributeUtils.js
2590
+ @@ -361,9 +361,15 @@ class WebGPUAttributeUtils {
2591
+ destroyAttribute( attribute ) {
2592
+
2593
+ const backend = this.backend;
2594
+ - const data = backend.get( this._getBufferAttribute( attribute ) );
2595
+ + const bufferAttribute = this._getBufferAttribute( attribute );
2596
+ + const data = backend.get( bufferAttribute );
2597
+ +
2598
+ + // An interleaved attribute's GPU buffer lives on its InterleavedBuffer, and several attributes
2599
+ + // share it: destroy it once and forget it there too, as the WebGL backend does, or the next
2600
+ + // createAttribute() finds the destroyed buffer and submits it (threenative patch).
2601
+ + if ( data.buffer !== undefined ) data.buffer.destroy();
2602
+
2603
+ - data.buffer.destroy();
2604
+ + if ( bufferAttribute !== attribute ) backend.delete( bufferAttribute );
2605
+
2606
+ backend.delete( attribute );
2607
+
2608
+ diff --git a/src/renderers/webgpu/utils/WebGPUBindingUtils.js b/src/renderers/webgpu/utils/WebGPUBindingUtils.js
2609
+ index da4bd47..a5da7ca 100644
2610
+ --- a/src/renderers/webgpu/utils/WebGPUBindingUtils.js
2611
+ +++ b/src/renderers/webgpu/utils/WebGPUBindingUtils.js
2612
+ @@ -97,18 +97,16 @@ class WebGPUBindingUtils {
2613
+
2614
+ const bindingsData = backend.get( bindGroup );
2615
+
2616
+ - // check if the the bind group already has a layout
2617
+ + const entries = this._createLayoutEntries( bindGroup );
2618
+ + const bindGroupLayoutKey = hashString( JSON.stringify( entries ) );
2619
+
2620
+ if ( bindingsData.layout ) {
2621
+
2622
+ - return bindingsData.layout.layoutGPU;
2623
+ -
2624
+ - }
2625
+ + if ( bindingsData.layoutKey === bindGroupLayoutKey ) return bindingsData.layout.layoutGPU;
2626
+
2627
+ - // if not, assing one
2628
+ + this.deleteBindGroupData( bindGroup );
2629
+
2630
+ - const entries = this._createLayoutEntries( bindGroup );
2631
+ - const bindGroupLayoutKey = hashString( JSON.stringify( entries ) );
2632
+ + }
2633
+
2634
+ // try to find an existing layout in the cache
2635
+
2636
+ @@ -630,6 +628,9 @@ class WebGPUBindingUtils {
2637
+
2638
+ }
2639
+
2640
+ + bindingsData.groups = [];
2641
+ + bindingsData.versions = [];
2642
+ +
2643
+ }
2644
+
2645
+ /**