@threenative/core 0.3.2 → 0.3.3

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
  #!/usr/bin/env node
2
- import { realpathSync, readFileSync, existsSync } from 'fs';
2
+ import { realpathSync, readFileSync, existsSync, mkdirSync, appendFileSync } from 'fs';
3
3
  import path from 'path';
4
4
  import { createInterface } from 'readline';
5
5
  import { fileURLToPath } from 'url';
@@ -417,6 +417,22 @@ var TOOL_DEFINITIONS = [
417
417
  function toolDefinitions() {
418
418
  return TOOL_DEFINITIONS;
419
419
  }
420
+ var DEFAULT_LOG_FILE = path.join(".threenative", "engine-mcp.log");
421
+ var logBroken = false;
422
+ function logToolCall(entry) {
423
+ if (logBroken) return;
424
+ const target = process.env.THREENATIVE_ENGINE_MCP_LOG ?? DEFAULT_LOG_FILE;
425
+ if (target === "off") return;
426
+ try {
427
+ mkdirSync(path.dirname(path.resolve(target)), { recursive: true });
428
+ appendFileSync(target, `${JSON.stringify({ time: (/* @__PURE__ */ new Date()).toISOString(), ...entry })}
429
+ `);
430
+ } catch (error) {
431
+ logBroken = true;
432
+ process.stderr.write(`engine-mcp log disabled (${target}): ${String(error)}
433
+ `);
434
+ }
435
+ }
420
436
  function jsonRpcError(id, code, message) {
421
437
  return JSON.stringify({ id, jsonrpc: "2.0", error: { code, message } });
422
438
  }
@@ -439,12 +455,22 @@ function handleToolCall(params, manifestFile) {
439
455
  const scope = argumentsValue.scope ?? "mechanic";
440
456
  if (scope !== "request" && scope !== "mechanic")
441
457
  throw new Error("engine_search_capabilities scope must be 'request' or 'mechanic'.");
442
- return toolText(searchCapabilities(argumentsValue.situation, manifestFile, scope));
458
+ const response = searchCapabilities(argumentsValue.situation, manifestFile, scope);
459
+ logToolCall({
460
+ results: response.results.map((result) => result.symbol),
461
+ scope,
462
+ situation: argumentsValue.situation,
463
+ tool: name,
464
+ verdict: response.verdict
465
+ });
466
+ return toolText(response);
443
467
  }
444
468
  if (name === "engine_capability_detail") {
445
469
  if (typeof argumentsValue.symbol !== "string")
446
470
  throw new Error("engine_capability_detail requires a string 'symbol' argument.");
447
- return toolText(capabilityDetail(argumentsValue.symbol, manifestFile));
471
+ const detail = capabilityDetail(argumentsValue.symbol, manifestFile);
472
+ logToolCall({ symbol: argumentsValue.symbol, tool: name });
473
+ return toolText(detail);
448
474
  }
449
475
  throw new Error(`Unknown engine MCP tool '${String(name)}'.`);
450
476
  }
@@ -464,7 +490,7 @@ function handleLine(line, manifestFile) {
464
490
  capabilities: { tools: { listChanged: false } },
465
491
  instructions: AUTHORING_INSTRUCTIONS,
466
492
  protocolVersion: "2025-06-18",
467
- serverInfo: { name: "threenative-engine-mcp", version: "0.2.2" }
493
+ serverInfo: { name: "threenative-engine-mcp", version: "0.2.3" }
468
494
  });
469
495
  }
470
496
  if (request.method === "tools/list") {
@@ -476,7 +502,9 @@ function handleLine(line, manifestFile) {
476
502
  }
477
503
  return jsonRpcError(request.id, -32601, `Method not found: ${request.method}`);
478
504
  } catch (error) {
479
- return jsonRpcError(request.id, -32e3, error instanceof Error ? error.message : String(error));
505
+ const message = error instanceof Error ? error.message : String(error);
506
+ if (request.method === "tools/call") logToolCall({ error: message, request: request.params });
507
+ return jsonRpcError(request.id, -32e3, message);
480
508
  }
481
509
  }
482
510
  function runServer(manifestFile = defaultManifestPath()) {
package/mcp/servers.mjs CHANGED
@@ -38,10 +38,10 @@ export const MCP_SERVERS = Object.freeze({
38
38
  * separately. The pinned engine version remains an emergency fallback for a legacy development
39
39
  * checkout whose bundle has not been built. */
40
40
  export const MCP_PACKAGES = Object.freeze({
41
- assets: Object.freeze({ name: "threenative-asset-mcp", version: "0.8.0" }),
41
+ assets: Object.freeze({ name: "threenative-asset-mcp", version: "0.9.1" }),
42
42
  sculpt: Object.freeze({ name: "threenative-sculpt-mcp", version: "0.1.1" }),
43
- engine: Object.freeze({ name: "threenative-engine-mcp", version: "0.2.2" }),
44
- blender: Object.freeze({ name: "threenative-blender-mcp", version: "0.1.2" }),
43
+ engine: Object.freeze({ name: "threenative-engine-mcp", version: "0.2.3" }),
44
+ blender: Object.freeze({ name: "threenative-blender-mcp", version: "0.1.3" }),
45
45
  });
46
46
 
47
47
  /** How one host spells a stdio MCP server. Hosts agree on what to run and disagree on where to
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@threenative/core",
3
- "version": "0.3.2",
3
+ "version": "0.3.3",
4
4
  "license": "MIT AND GPL-2.0-or-later",
5
5
  "description": "The small vanilla Three.js game runtime foundation.",
6
6
  "keywords": [
@@ -76,7 +76,7 @@
76
76
  "dependencies": {
77
77
  "three": "0.185.1",
78
78
  "three-mesh-bvh": "0.9.14",
79
- "threenative-asset-mcp": "0.8.0",
79
+ "threenative-asset-mcp": "0.9.1",
80
80
  "threenative-sculpt-mcp": "0.1.1",
81
81
  "zustand": "5.0.14"
82
82
  },
@@ -89,12 +89,12 @@
89
89
  "react-reconciler": "0.33.0",
90
90
  "tsup": "8.5.1",
91
91
  "typescript": "5.9.3",
92
- "threenative-blender-mcp": "0.1.2",
93
- "@threenative/playtest": "0.3.2",
94
- "threenative-engine-mcp": "0.2.2"
92
+ "@threenative/playtest": "0.3.3",
93
+ "threenative-engine-mcp": "0.2.3",
94
+ "threenative-blender-mcp": "0.1.3"
95
95
  },
96
96
  "peerDependencies": {
97
- "@threenative/playtest": ">=0.3.2 <0.4.0",
97
+ "@threenative/playtest": ">=0.3.3 <0.4.0",
98
98
  "react": ">=19.2.0 <20",
99
99
  "react-reconciler": ">=0.33.0 <0.34"
100
100
  },
@@ -25,7 +25,7 @@ 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 2e490796d4d4aa9479b04a5c46ddbf31cc05b1a1..f7de72cffef2eb577abb0b90051a18392c88f865 100644
29
29
  --- a/build/three.webgpu.js
30
30
  +++ b/build/three.webgpu.js
31
31
  @@ -18500,7 +18500,8 @@ const instance = /*@__PURE__*/ Fn( ( [ matrices, colors = null ], builder ) => {
@@ -105,7 +105,25 @@ index 2e490796d4d4aa9479b04a5c46ddbf31cc05b1a1..d05599255744ccfca19dbfd0433da612
105
105
  /**
106
106
  * An event listener which is defined by `RenderObjects`. It performs
107
107
  * clean up tasks when `dispose()` on this render object.
108
- @@ -30707,9 +30736,18 @@ class RenderObjects {
108
+ @@ -30098,7 +30127,7 @@ class RenderObject {
109
+ */
110
+ getChainArray() {
111
+
112
+ - return [ this.object, this.material, this.context, this.lightsNode ];
113
+ + return [ this.object, this.material, this.context, this.lightsNode, this._sourceMaterial ?? this.material ];
114
+
115
+ }
116
+
117
+ @@ -30680,6 +30709,8 @@ class RenderObjects {
118
+ _chainKeys$3[ 1 ] = material;
119
+ _chainKeys$3[ 2 ] = renderContext;
120
+ _chainKeys$3[ 3 ] = lightsNode;
121
+ + // Material groups share the shadow override, but must retain separate GPU bindings.
122
+ + _chainKeys$3[ 4 ] = this.renderer._currentSourceMaterial ?? material;
123
+
124
+ //
125
+
126
+ @@ -30707,9 +30738,18 @@ class RenderObjects {
109
127
 
110
128
  }
111
129
 
@@ -118,15 +136,15 @@ index 2e490796d4d4aa9479b04a5c46ddbf31cc05b1a1..d05599255744ccfca19dbfd0433da612
118
136
  + const sourceTracked = sourceMaterial !== null && sourceMaterial !== undefined;
119
137
  + const sourceReplaced = sourceTracked && renderObject._sourceMaterial !== sourceMaterial;
120
138
  + const sourceChanged = sourceTracked && ( sourceReplaced || renderObject.sourceVersion !== sourceMaterial.version );
139
+ +
140
+ + if ( renderObject.version !== material.version || sourceChanged || renderObject.needsUpdate ) {
121
141
 
122
142
  - if ( renderObject.initialCacheKey !== renderObject.getCacheKey() ) {
123
- + if ( renderObject.version !== material.version || sourceChanged || renderObject.needsUpdate ) {
124
- +
125
143
  + if ( sourceReplaced || renderObject.initialCacheKey !== renderObject.getCacheKey() ) {
126
144
 
127
145
  renderObject.dispose();
128
146
 
129
- @@ -30719,6 +30757,8 @@ class RenderObjects {
147
+ @@ -30719,6 +30759,8 @@ class RenderObjects {
130
148
 
131
149
  renderObject.version = material.version;
132
150
 
@@ -135,7 +153,15 @@ index 2e490796d4d4aa9479b04a5c46ddbf31cc05b1a1..d05599255744ccfca19dbfd0433da612
135
153
  }
136
154
 
137
155
  }
138
- @@ -39725,7 +39765,8 @@ class VelocityNode extends TempNode {
156
+ @@ -30731,6 +30773,7 @@ class RenderObjects {
157
+ _chainKeys$3[ 1 ] = null;
158
+ _chainKeys$3[ 2 ] = null;
159
+ _chainKeys$3[ 3 ] = null;
160
+ + _chainKeys$3[ 4 ] = null;
161
+
162
+ //
163
+
164
+ @@ -39725,7 +39768,8 @@ class VelocityNode extends TempNode {
139
165
 
140
166
  const previousModelMatrix = getPreviousMatrix( object );
141
167
 
@@ -145,7 +171,7 @@ index 2e490796d4d4aa9479b04a5c46ddbf31cc05b1a1..d05599255744ccfca19dbfd0433da612
145
171
 
146
172
  //
147
173
 
148
- @@ -53841,7 +53882,7 @@ class NodeBuilder {
174
+ @@ -53841,7 +53885,7 @@ class NodeBuilder {
149
175
 
150
176
  const mrt = this.renderer.getMRT();
151
177
 
@@ -154,7 +180,7 @@ index 2e490796d4d4aa9479b04a5c46ddbf31cc05b1a1..d05599255744ccfca19dbfd0433da612
154
180
 
155
181
  }
156
182
 
157
- @@ -62731,6 +62772,7 @@ class Renderer {
183
+ @@ -62731,6 +62775,7 @@ class Renderer {
158
184
  let materialDisplacementMap;
159
185
  let materialDisplacementScale;
160
186
  let materialDisplacementBias;
@@ -162,7 +188,7 @@ index 2e490796d4d4aa9479b04a5c46ddbf31cc05b1a1..d05599255744ccfca19dbfd0433da612
162
188
 
163
189
  const previousSourceMaterial = this._currentSourceMaterial;
164
190
 
165
- @@ -62756,6 +62798,7 @@ class Renderer {
191
+ @@ -62756,6 +62801,7 @@ class Renderer {
166
192
  materialDisplacementMap = overrideMaterial.displacementMap;
167
193
  materialDisplacementScale = overrideMaterial.displacementScale;
168
194
  materialDisplacementBias = overrideMaterial.displacementBias;
@@ -170,7 +196,7 @@ index 2e490796d4d4aa9479b04a5c46ddbf31cc05b1a1..d05599255744ccfca19dbfd0433da612
170
196
 
171
197
  if ( material.positionNode && material.positionNode.isNode ) {
172
198
 
173
- @@ -62763,7 +62806,15 @@ class Renderer {
199
+ @@ -62763,7 +62809,15 @@ class Renderer {
174
200
 
175
201
  }
176
202
 
@@ -187,7 +213,7 @@ index 2e490796d4d4aa9479b04a5c46ddbf31cc05b1a1..d05599255744ccfca19dbfd0433da612
187
213
  overrideMaterial.alphaMap = material.alphaMap;
188
214
  overrideMaterial.displacementMap = material.displacementMap;
189
215
  overrideMaterial.displacementScale = material.displacementScale;
190
- @@ -62825,6 +62876,7 @@ class Renderer {
216
+ @@ -62825,6 +62879,7 @@ class Renderer {
191
217
  scene.overrideMaterial.displacementMap = materialDisplacementMap;
192
218
  scene.overrideMaterial.displacementScale = materialDisplacementScale;
193
219
  scene.overrideMaterial.displacementBias = materialDisplacementBias;
@@ -195,7 +221,31 @@ index 2e490796d4d4aa9479b04a5c46ddbf31cc05b1a1..d05599255744ccfca19dbfd0433da612
195
221
 
196
222
  }
197
223
 
198
- @@ -79586,7 +79638,8 @@ class WGSLNodeBuilder extends NodeBuilder {
224
+ @@ -78999,15 +78999,22 @@ class WGSLNodeBuilder extends NodeBuilder {
225
+ let snippet;
226
+
227
+ if ( depthSnippet ) {
228
+
229
+ snippet = `textureLoad( ${ textureProperty }, ${ uvIndexSnippet }, ${ depthSnippet }, u32( ${ levelSnippet } ) )`;
230
+
231
+ } else {
232
+
233
+ - snippet = `textureLoad( ${ textureProperty }, ${ uvIndexSnippet }, u32( ${ levelSnippet } ) )`;
234
+ + // naga v25 (wgpu-native v25.0.2.2) rejects a u32 sample index on a
235
+ + // multisampled textureLoad; i32 is accepted by every backend. Mip-level
236
+ + // operands (including the array form above) stay u32.
237
+ + const sampleData = texture ? this.renderer?.backend?.utils?.getTextureSampleData?.( texture ) : null;
238
+ + const backendMultisampled = sampleData !== null && sampleData !== undefined && sampleData.primarySamples > 1;
239
+ + const targetMultisampled = Boolean( texture && texture.renderTarget && texture.renderTarget.samples > 1 );
240
+ + const indexType = ( backendMultisampled || targetMultisampled ) ? 'i32' : 'u32';
241
+ + snippet = `textureLoad( ${ textureProperty }, ${ uvIndexSnippet }, ${ indexType }( ${ levelSnippet } ) )`;
242
+
243
+ if ( this.renderer.backend.compatibilityMode && texture.isDepthTexture ) {
244
+
245
+ snippet += '.x';
246
+
247
+ }
248
+ @@ -79586,7 +79641,8 @@ class WGSLNodeBuilder extends NodeBuilder {
199
249
 
200
250
  uniformGPU = buffer;
201
251
 
@@ -205,7 +255,23 @@ index 2e490796d4d4aa9479b04a5c46ddbf31cc05b1a1..d05599255744ccfca19dbfd0433da612
205
255
 
206
256
  } else {
207
257
 
208
- @@ -83814,6 +83867,14 @@ class WebGPUBackend extends Backend {
258
+ @@ -80329,9 +80385,12 @@ ${ flowData.code }
259
+ const textureNode = uniform.node;
260
+ const texture = textureNode.value;
261
+
262
+ - // Cube textures always need samplers (they use textureSampleLevel, not textureLoad)
263
+ - // Also textureGather always need sampler.
264
+ - const needsSampler = texture.isCubeTexture === true || ( this.isUnfilterable( texture ) === false && textureNode.isStorageTextureNode !== true ) || textureNode.gatherNode !== null;
265
+ + // The bind group layout freezes sampler inclusion once, in getUniformFromNode
266
+ + // (nodeData.uniformGPU). Re-deriving it here from live texture filter state can
267
+ + // disagree with that layout and shift every later binding index.
268
+ + const nodeData = this.getDataFromNode( textureNode, shaderStage, this.globalCache );
269
+ + const uniformGPU = nodeData.uniformGPU;
270
+ + const needsSampler = Array.isArray( uniformGPU ) && uniformGPU[ 0 ].isSampler === true && uniformGPU[ 0 ].isSampledTexture !== true;
271
+
272
+ if ( needsSampler ) {
273
+
274
+ @@ -83814,6 +83873,14 @@ class WebGPUBackend extends Backend {
209
275
  */
210
276
  this.compatibilityMode = null;
211
277
 
@@ -220,7 +286,7 @@ index 2e490796d4d4aa9479b04a5c46ddbf31cc05b1a1..d05599255744ccfca19dbfd0433da612
220
286
  /**
221
287
  * A reference to the device.
222
288
  *
223
- @@ -83926,7 +83987,9 @@ class WebGPUBackend extends Backend {
289
+ @@ -83926,7 +83993,9 @@ class WebGPUBackend extends Backend {
224
290
  xrCompatible: renderer.xr.enabled
225
291
  };
226
292
 
@@ -375,7 +441,7 @@ index 5f3d213a016b37db3b2e5d022da3dd0234d86f6d..7a9a168f29773f37ef9a51ef9f07ade9
375
441
 
376
442
  } else if ( binding.isStorageBuffer ) {
377
443
  diff --git a/src/renderers/common/RenderObject.js b/src/renderers/common/RenderObject.js
378
- index c900fa4c9654f8cf3512e96de52a2c1c83e189a6..d0a87eabffc8f667ebe9b2f0eccda0ddf3309652 100644
444
+ index c900fa4c9654f8cf3512e96de52a2c1c83e189a6..3a8b0012abf054f50ab66466bab7efa16fe1bc09 100644
379
445
  --- a/src/renderers/common/RenderObject.js
380
446
  +++ b/src/renderers/common/RenderObject.js
381
447
  @@ -299,6 +299,18 @@ class RenderObject {
@@ -397,11 +463,29 @@ index c900fa4c9654f8cf3512e96de52a2c1c83e189a6..d0a87eabffc8f667ebe9b2f0eccda0dd
397
463
  /**
398
464
  * An event listener which is defined by `RenderObjects`. It performs
399
465
  * clean up tasks when `dispose()` on this render object.
466
+ @@ -486,7 +498,7 @@ class RenderObject {
467
+ */
468
+ getChainArray() {
469
+
470
+ - return [ this.object, this.material, this.context, this.lightsNode ];
471
+ + return [ this.object, this.material, this.context, this.lightsNode, this._sourceMaterial ?? this.material ];
472
+
473
+ }
474
+
400
475
  diff --git a/src/renderers/common/RenderObjects.js b/src/renderers/common/RenderObjects.js
401
- index 676cc2bd4169427ed420f2467b1e6684bd2aeebc..0fa661db793548254a15ff43f39207abcc43ef49 100644
476
+ index 676cc2bd4169427ed420f2467b1e6684bd2aeebc..72569d0339314d26eb7a35ff2a8462a803c852a6 100644
402
477
  --- a/src/renderers/common/RenderObjects.js
403
478
  +++ b/src/renderers/common/RenderObjects.js
404
- @@ -124,9 +124,18 @@ class RenderObjects {
479
+ @@ -97,6 +97,8 @@ class RenderObjects {
480
+ _chainKeys[ 1 ] = material;
481
+ _chainKeys[ 2 ] = renderContext;
482
+ _chainKeys[ 3 ] = lightsNode;
483
+ + // Material groups share the shadow override, but must retain separate GPU bindings.
484
+ + _chainKeys[ 4 ] = this.renderer._currentSourceMaterial ?? material;
485
+
486
+ //
487
+
488
+ @@ -124,9 +126,18 @@ class RenderObjects {
405
489
 
406
490
  }
407
491
 
@@ -422,7 +506,7 @@ index 676cc2bd4169427ed420f2467b1e6684bd2aeebc..0fa661db793548254a15ff43f39207ab
422
506
 
423
507
  renderObject.dispose();
424
508
 
425
- @@ -136,6 +145,8 @@ class RenderObjects {
509
+ @@ -136,6 +147,8 @@ class RenderObjects {
426
510
 
427
511
  renderObject.version = material.version;
428
512
 
@@ -431,6 +515,14 @@ index 676cc2bd4169427ed420f2467b1e6684bd2aeebc..0fa661db793548254a15ff43f39207ab
431
515
  }
432
516
 
433
517
  }
518
+ @@ -148,6 +161,7 @@ class RenderObjects {
519
+ _chainKeys[ 1 ] = null;
520
+ _chainKeys[ 2 ] = null;
521
+ _chainKeys[ 3 ] = null;
522
+ + _chainKeys[ 4 ] = null;
523
+
524
+ //
525
+
434
526
  diff --git a/src/renderers/common/Renderer.js b/src/renderers/common/Renderer.js
435
527
  index 8dfc2ae055ad7b9d4c00e91f7d52a39e747caebe..e9d27b3ddad3b1f080a717d1f6683b60afd2b259 100644
436
528
  --- a/src/renderers/common/Renderer.js
@@ -507,9 +599,33 @@ index 06e203693ed9b168a2bddf773cd8ce8dc23196e4..37b5e0be09967e8acbd38e570b71eb20
507
599
  if ( adapter === null ) {
508
600
 
509
601
  diff --git a/src/renderers/webgpu/nodes/WGSLNodeBuilder.js b/src/renderers/webgpu/nodes/WGSLNodeBuilder.js
510
- index be07536301cd47ef2544e4a54f6abe17407f52af..6144b98ddeb5d53c5cf280bb51401385a4427547 100644
602
+ index be07536301cd47ef2544e4a54f6abe17407f52af..3b064a25a73fb35bcaed653794bda420bb004dec 100644
511
603
  --- a/src/renderers/webgpu/nodes/WGSLNodeBuilder.js
512
604
  +++ b/src/renderers/webgpu/nodes/WGSLNodeBuilder.js
605
+ @@ -736,15 +736,22 @@ generateTextureLoad( texture, textureProperty, uvIndexSnippet, levelSnippet, depthSnippet, offsetSnippet ) {
606
+ let snippet;
607
+
608
+ if ( depthSnippet ) {
609
+
610
+ snippet = `textureLoad( ${ textureProperty }, ${ uvIndexSnippet }, ${ depthSnippet }, u32( ${ levelSnippet } ) )`;
611
+
612
+ } else {
613
+
614
+ - snippet = `textureLoad( ${ textureProperty }, ${ uvIndexSnippet }, u32( ${ levelSnippet } ) )`;
615
+ + // naga v25 (wgpu-native v25.0.2.2) rejects a u32 sample index on a
616
+ + // multisampled textureLoad; i32 is accepted by every backend. Mip-level
617
+ + // operands (including the array form above) stay u32.
618
+ + const sampleData = texture ? this.renderer?.backend?.utils?.getTextureSampleData?.( texture ) : null;
619
+ + const backendMultisampled = sampleData !== null && sampleData !== undefined && sampleData.primarySamples > 1;
620
+ + const targetMultisampled = Boolean( texture && texture.renderTarget && texture.renderTarget.samples > 1 );
621
+ + const indexType = ( backendMultisampled || targetMultisampled ) ? 'i32' : 'u32';
622
+ + snippet = `textureLoad( ${ textureProperty }, ${ uvIndexSnippet }, ${ indexType }( ${ levelSnippet } ) )`;
623
+
624
+ if ( this.renderer.backend.compatibilityMode && texture.isDepthTexture ) {
625
+
626
+ snippet += '.x';
627
+
628
+ }
513
629
  @@ -1323,7 +1323,8 @@ class WGSLNodeBuilder extends NodeBuilder {
514
630
 
515
631
  uniformGPU = buffer;
@@ -520,3 +636,47 @@ index be07536301cd47ef2544e4a54f6abe17407f52af..6144b98ddeb5d53c5cf280bb51401385
520
636
 
521
637
  } else {
522
638
 
639
+ @@ -2066,9 +2067,12 @@ ${ flowData.code }
640
+ const textureNode = uniform.node;
641
+ const texture = textureNode.value;
642
+
643
+ - // Cube textures always need samplers (they use textureSampleLevel, not textureLoad)
644
+ - // Also textureGather always need sampler.
645
+ - const needsSampler = texture.isCubeTexture === true || ( this.isUnfilterable( texture ) === false && textureNode.isStorageTextureNode !== true ) || textureNode.gatherNode !== null;
646
+ + // The bind group layout freezes sampler inclusion once, in getUniformFromNode
647
+ + // (nodeData.uniformGPU). Re-deriving it here from live texture filter state can
648
+ + // disagree with that layout and shift every later binding index.
649
+ + const nodeData = this.getDataFromNode( textureNode, shaderStage, this.globalCache );
650
+ + const uniformGPU = nodeData.uniformGPU;
651
+ + const needsSampler = Array.isArray( uniformGPU ) && uniformGPU[ 0 ].isSampler === true && uniformGPU[ 0 ].isSampledTexture !== true;
652
+
653
+ if ( needsSampler ) {
654
+
655
+ diff --git a/build/three.webgpu.nodes.js b/build/three.webgpu.nodes.js
656
+ index f707acc24f5711fdae065ce920b08435b0869c1b..4c679d417f79af7994f296c005fcfaf3416c3f7a 100644
657
+ --- a/build/three.webgpu.nodes.js
658
+ +++ b/build/three.webgpu.nodes.js
659
+ @@ -78999,15 +78999,22 @@ class WGSLNodeBuilder extends NodeBuilder {
660
+ let snippet;
661
+
662
+ if ( depthSnippet ) {
663
+
664
+ snippet = `textureLoad( ${ textureProperty }, ${ uvIndexSnippet }, ${ depthSnippet }, u32( ${ levelSnippet } ) )`;
665
+
666
+ } else {
667
+
668
+ - snippet = `textureLoad( ${ textureProperty }, ${ uvIndexSnippet }, u32( ${ levelSnippet } ) )`;
669
+ + // naga v25 (wgpu-native v25.0.2.2) rejects a u32 sample index on a
670
+ + // multisampled textureLoad; i32 is accepted by every backend. Mip-level
671
+ + // operands (including the array form above) stay u32.
672
+ + const sampleData = texture ? this.renderer?.backend?.utils?.getTextureSampleData?.( texture ) : null;
673
+ + const backendMultisampled = sampleData !== null && sampleData !== undefined && sampleData.primarySamples > 1;
674
+ + const targetMultisampled = Boolean( texture && texture.renderTarget && texture.renderTarget.samples > 1 );
675
+ + const indexType = ( backendMultisampled || targetMultisampled ) ? 'i32' : 'u32';
676
+ + snippet = `textureLoad( ${ textureProperty }, ${ uvIndexSnippet }, ${ indexType }( ${ levelSnippet } ) )`;
677
+
678
+ if ( this.renderer.backend.compatibilityMode && texture.isDepthTexture ) {
679
+
680
+ snippet += '.x';
681
+
682
+ }