@needle-tools/engine 4.13.1 → 4.14.0-beta

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.
Files changed (141) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/components.needle.json +1 -1
  3. package/dist/generateMeshBVH.worker-DiCnZlf3.js +21 -0
  4. package/dist/gltf-progressive-Bm_6aEi4.js +1528 -0
  5. package/dist/gltf-progressive-BttGBXw6.umd.cjs +10 -0
  6. package/dist/gltf-progressive-T5WKTux5.min.js +10 -0
  7. package/dist/loader.worker-BqODMeeW.js +23 -0
  8. package/dist/{materialx-B9ddsHcF.min.js → materialx-CJyQZtjt.min.js} +1 -1
  9. package/dist/{materialx-fkoFuRh3.js → materialx-DMs1E08Z.js} +2 -2
  10. package/dist/{materialx-BF23AVE8.umd.cjs → materialx-DaKKOoVk.umd.cjs} +1 -1
  11. package/dist/needle-engine.bundle-BW2VusZV.min.js +1646 -0
  12. package/dist/{needle-engine.bundle-tjI5Fq2c.umd.cjs → needle-engine.bundle-Cb5bKEqa.umd.cjs} +152 -159
  13. package/dist/{needle-engine.bundle-DauZUYl7.js → needle-engine.bundle-D9VPvp5o.js} +7798 -7497
  14. package/dist/needle-engine.d.ts +779 -42
  15. package/dist/needle-engine.js +416 -415
  16. package/dist/needle-engine.min.js +1 -1
  17. package/dist/needle-engine.umd.cjs +1 -1
  18. package/dist/{postprocessing-BVNrgYZK.min.js → postprocessing-CctM1XIO.min.js} +1 -1
  19. package/dist/{postprocessing-DdM-tz1j.js → postprocessing-DGm6qJ-I.js} +2 -2
  20. package/dist/{postprocessing-CI2TjWpu.umd.cjs → postprocessing-Dbl2PJpd.umd.cjs} +1 -1
  21. package/dist/{three-BW2s1Yl-.umd.cjs → three-BjYim-vL.umd.cjs} +4 -4
  22. package/dist/{three-VvRoMeIN.js → three-Bvk2VKbF.js} +4210 -4210
  23. package/dist/{three-I__hSXzr.min.js → three-IG2qPafA.min.js} +33 -33
  24. package/dist/{three-examples-Bpfu6ke_.umd.cjs → three-examples-BMmNgNCN.umd.cjs} +1 -1
  25. package/dist/{three-examples-BhfOE7NG.js → three-examples-CMYCd5nH.js} +1 -1
  26. package/dist/{three-examples-D8zAE_7t.min.js → three-examples-CQl1fFZp.min.js} +1 -1
  27. package/dist/{three-mesh-ui-C3QbemOV.min.js → three-mesh-ui-5HVE2RV-.min.js} +1 -1
  28. package/dist/{three-mesh-ui-CcMp-FQm.js → three-mesh-ui-BlakAItG.js} +1 -1
  29. package/dist/{three-mesh-ui-BU55xDxJ.umd.cjs → three-mesh-ui-D828VbQp.umd.cjs} +1 -1
  30. package/dist/{vendor-DW7zqjuT.min.js → vendor-BxK0WKmT.min.js} +1 -1
  31. package/dist/{vendor-COVQl0b8.umd.cjs → vendor-CIDkyBaO.umd.cjs} +1 -1
  32. package/dist/{vendor-BiyIZ61v.js → vendor-ixwD-vv2.js} +1 -1
  33. package/lib/engine/api.d.ts +1 -0
  34. package/lib/engine/api.js +1 -0
  35. package/lib/engine/api.js.map +1 -1
  36. package/lib/engine/engine_context.js +12 -2
  37. package/lib/engine/engine_context.js.map +1 -1
  38. package/lib/engine/engine_lightdata.js +8 -6
  39. package/lib/engine/engine_lightdata.js.map +1 -1
  40. package/lib/engine/engine_materialpropertyblock.d.ts +259 -0
  41. package/lib/engine/engine_materialpropertyblock.js +682 -0
  42. package/lib/engine/engine_materialpropertyblock.js.map +1 -0
  43. package/lib/engine/engine_utils.d.ts +1 -0
  44. package/lib/engine/engine_utils.js +1 -0
  45. package/lib/engine/engine_utils.js.map +1 -1
  46. package/lib/engine/engine_utils_screenshot.d.ts +171 -14
  47. package/lib/engine/engine_utils_screenshot.js +65 -0
  48. package/lib/engine/engine_utils_screenshot.js.map +1 -1
  49. package/lib/engine/engine_utils_screenshot.xr.d.ts +1 -1
  50. package/lib/engine/engine_utils_screenshot.xr.js +1 -1
  51. package/lib/engine/extensions/NEEDLE_techniques_webgl.js +3 -0
  52. package/lib/engine/extensions/NEEDLE_techniques_webgl.js.map +1 -1
  53. package/lib/engine/xr/NeedleXRSession.d.ts +5 -0
  54. package/lib/engine/xr/NeedleXRSession.js +5 -0
  55. package/lib/engine/xr/NeedleXRSession.js.map +1 -1
  56. package/lib/engine-components/Camera.js.map +1 -1
  57. package/lib/engine-components/DropListener.js.map +1 -1
  58. package/lib/engine-components/Duplicatable.js.map +1 -1
  59. package/lib/engine-components/GroundProjection.js.map +1 -1
  60. package/lib/engine-components/NeedleMenu.js.map +1 -1
  61. package/lib/engine-components/NestedGltf.js.map +1 -1
  62. package/lib/engine-components/ReflectionProbe.d.ts +32 -4
  63. package/lib/engine-components/ReflectionProbe.js +58 -88
  64. package/lib/engine-components/ReflectionProbe.js.map +1 -1
  65. package/lib/engine-components/Renderer.d.ts +2 -0
  66. package/lib/engine-components/Renderer.js +30 -6
  67. package/lib/engine-components/Renderer.js.map +1 -1
  68. package/lib/engine-components/RendererLightmap.d.ts +13 -9
  69. package/lib/engine-components/RendererLightmap.js +67 -81
  70. package/lib/engine-components/RendererLightmap.js.map +1 -1
  71. package/lib/engine-components/SeeThrough.d.ts +0 -2
  72. package/lib/engine-components/SeeThrough.js +114 -88
  73. package/lib/engine-components/SeeThrough.js.map +1 -1
  74. package/lib/engine-components/SmoothFollow.js.map +1 -1
  75. package/lib/engine-components/VideoPlayer.js +6 -0
  76. package/lib/engine-components/VideoPlayer.js.map +1 -1
  77. package/lib/engine-components/ui/Button.js.map +1 -1
  78. package/lib/engine-components/ui/Raycaster.js.map +1 -1
  79. package/lib/engine-components/utils/OpenURL.d.ts +1 -0
  80. package/lib/engine-components/utils/OpenURL.js +1 -0
  81. package/lib/engine-components/utils/OpenURL.js.map +1 -1
  82. package/lib/engine-components/web/CursorFollow.d.ts +1 -0
  83. package/lib/engine-components/web/CursorFollow.js +1 -0
  84. package/lib/engine-components/web/CursorFollow.js.map +1 -1
  85. package/lib/engine-components/web/ScrollFollow.d.ts +1 -0
  86. package/lib/engine-components/web/ScrollFollow.js +1 -0
  87. package/lib/engine-components/web/ScrollFollow.js.map +1 -1
  88. package/lib/engine-components/web/ViewBox.d.ts +2 -2
  89. package/lib/engine-components/web/ViewBox.js +2 -2
  90. package/lib/engine-components/web/ViewBox.js.map +1 -1
  91. package/lib/engine-components/webxr/WebARCameraBackground.d.ts +9 -0
  92. package/lib/engine-components/webxr/WebARCameraBackground.js +9 -0
  93. package/lib/engine-components/webxr/WebARCameraBackground.js.map +1 -1
  94. package/lib/engine-components/webxr/WebXR.d.ts +1 -0
  95. package/lib/engine-components/webxr/WebXR.js +1 -0
  96. package/lib/engine-components/webxr/WebXR.js.map +1 -1
  97. package/lib/engine-components/webxr/WebXRPlaneTracking.js.map +1 -1
  98. package/lib/engine-components/webxr/controllers/XRControllerFollow.js.map +1 -1
  99. package/lib/engine-components/webxr/controllers/XRControllerMovement.js.map +1 -1
  100. package/package.json +5 -5
  101. package/plugins/vite/build-pipeline.js +16 -2
  102. package/src/engine/api.ts +1 -0
  103. package/src/engine/engine_context.ts +17 -3
  104. package/src/engine/engine_lightdata.ts +8 -6
  105. package/src/engine/engine_materialpropertyblock.ts +866 -0
  106. package/src/engine/engine_utils.ts +1 -0
  107. package/src/engine/engine_utils_screenshot.ts +241 -17
  108. package/src/engine/engine_utils_screenshot.xr.ts +1 -1
  109. package/src/engine/extensions/NEEDLE_techniques_webgl.ts +3 -0
  110. package/src/engine/xr/NeedleXRSession.ts +5 -0
  111. package/src/engine-components/Camera.ts +2 -2
  112. package/src/engine-components/ContactShadows.ts +1 -1
  113. package/src/engine-components/DropListener.ts +1 -1
  114. package/src/engine-components/Duplicatable.ts +1 -1
  115. package/src/engine-components/GroundProjection.ts +3 -0
  116. package/src/engine-components/NeedleMenu.ts +3 -0
  117. package/src/engine-components/NestedGltf.ts +1 -1
  118. package/src/engine-components/ReflectionProbe.ts +64 -105
  119. package/src/engine-components/Renderer.ts +34 -6
  120. package/src/engine-components/RendererLightmap.ts +75 -87
  121. package/src/engine-components/SeeThrough.ts +124 -109
  122. package/src/engine-components/SmoothFollow.ts +2 -2
  123. package/src/engine-components/VideoPlayer.ts +6 -0
  124. package/src/engine-components/ui/Button.ts +1 -1
  125. package/src/engine-components/ui/Raycaster.ts +1 -1
  126. package/src/engine-components/utils/OpenURL.ts +1 -0
  127. package/src/engine-components/web/CursorFollow.ts +1 -0
  128. package/src/engine-components/web/ScrollFollow.ts +1 -0
  129. package/src/engine-components/web/ViewBox.ts +9 -2
  130. package/src/engine-components/webxr/WebARCameraBackground.ts +12 -3
  131. package/src/engine-components/webxr/WebXR.ts +1 -0
  132. package/src/engine-components/webxr/WebXRPlaneTracking.ts +3 -3
  133. package/src/engine-components/webxr/controllers/XRControllerFollow.ts +1 -1
  134. package/src/engine-components/webxr/controllers/XRControllerMovement.ts +4 -4
  135. package/dist/generateMeshBVH.worker-iyfPIK6R.js +0 -21
  136. package/dist/gltf-progressive-BURrJW0U.umd.cjs +0 -8
  137. package/dist/gltf-progressive-DHLDFNvQ.min.js +0 -8
  138. package/dist/gltf-progressive-eiJCrjLb.js +0 -1400
  139. package/dist/loader.worker-C1GG9A7C.js +0 -23
  140. package/dist/needle-engine.bundle-BNIUpreS.min.js +0 -1653
  141. package/src/include/three/DragControls.js +0 -232
@@ -0,0 +1,866 @@
1
+ import { Material, Object3D, Color, Texture, Vector2, Vector3, Vector4, WebGLRenderer, Scene, Camera, BufferGeometry, Group, Euler } from "three";
2
+
3
+
4
+ // @TODO: we need to detect objects with materials both transparent and NOT transparent. These need to be updated in scene.onBeforeRender to have correct renderlists
5
+
6
+ /**
7
+ * Valid types that can be used as material property overrides
8
+ */
9
+ type MaterialPropertyType = number | number[] | Color | Texture | Vector2 | Vector3 | Vector4 | null | Euler;
10
+
11
+ /**
12
+ * Defines offset and repeat transformations for texture coordinates
13
+ */
14
+ export interface TextureTransform {
15
+ /** UV offset applied to the texture */
16
+ offset?: Vector2;
17
+ /** UV repeat/scale applied to the texture */
18
+ repeat?: Vector2;
19
+ }
20
+
21
+ /**
22
+ * Represents a single material property override with optional texture transformation
23
+ * @template T The type of the property value
24
+ */
25
+ export interface PropertyBlockOverride<T extends MaterialPropertyType = MaterialPropertyType> {
26
+ /** The name of the material property to override (e.g., "color", "map", "roughness") */
27
+ name: string;
28
+ /** The value to set for this property */
29
+ value: T;
30
+ /** Optional texture coordinate transformation (only used when value is a Texture) */
31
+ textureTransform?: TextureTransform;
32
+ }
33
+
34
+ /**
35
+ * Utility type that extracts only non-function property names from a type
36
+ * @template T The type to extract property names from
37
+ */
38
+ type NonFunctionPropertyNames<T> = {
39
+ [K in keyof T]: T[K] extends Function ? never : K
40
+ }[keyof T];
41
+
42
+ /**
43
+ * Centralized registry for all property block related data.
44
+ * Uses WeakMaps to allow automatic garbage collection when objects are destroyed.
45
+ * @internal
46
+ */
47
+ class PropertyBlockRegistry {
48
+ // Map from object to its property block
49
+ private objectToBlock = new WeakMap<Object3D, MaterialPropertyBlock>();
50
+
51
+ // Track which materials belong to which property block (to prevent applying to wrong materials)
52
+ // Use WeakSet for automatic cleanup when materials are garbage collected
53
+ // private objectToMaterials = new WeakMap<Object3D, WeakSet<Material>>();
54
+
55
+ // Track which meshes have callbacks for which property block owners
56
+ private meshToOwners = new WeakMap<Object3D, Set<Object3D>>();
57
+
58
+ // Track original callback functions for cleanup (reserved for future use)
59
+ private meshToOriginalCallbacks = new WeakMap<Object3D, {
60
+ onBeforeRender?: ObjectRenderCallback;
61
+ onAfterRender?: ObjectRenderCallback;
62
+ }>();
63
+
64
+ getBlock(object: Object3D): MaterialPropertyBlock | undefined {
65
+ return this.objectToBlock.get(object);
66
+ }
67
+
68
+ setBlock(object: Object3D, block: MaterialPropertyBlock): void {
69
+ this.objectToBlock.set(object, block);
70
+ }
71
+
72
+ deleteBlock(object: Object3D): void {
73
+ this.objectToBlock.delete(object);
74
+ // this.objectToMaterials.delete(object);
75
+ }
76
+
77
+ // addMaterial(object: Object3D, material: Material): void {
78
+ // let materials = this.objectToMaterials.get(object);
79
+ // if (!materials) {
80
+ // materials = new WeakSet();
81
+ // this.objectToMaterials.set(object, materials);
82
+ // }
83
+ // materials.add(material);
84
+ // }
85
+
86
+ // hasMaterial(object: Object3D, material: Material): boolean {
87
+ // return this.objectToMaterials.get(object)?.has(material) ?? false;
88
+ // }
89
+
90
+ isHooked(mesh: Object3D, owner: Object3D): boolean {
91
+ return this.meshToOwners.get(mesh)?.has(owner) ?? false;
92
+ }
93
+
94
+ addHook(mesh: Object3D, owner: Object3D): void {
95
+ let owners = this.meshToOwners.get(mesh);
96
+ if (!owners) {
97
+ owners = new Set();
98
+ this.meshToOwners.set(mesh, owners);
99
+ }
100
+ owners.add(owner);
101
+ }
102
+
103
+ removeHook(mesh: Object3D, owner: Object3D): void {
104
+ const owners = this.meshToOwners.get(mesh);
105
+ if (owners) {
106
+ owners.delete(owner);
107
+ if (owners.size === 0) {
108
+ this.meshToOwners.delete(mesh);
109
+ }
110
+ }
111
+ }
112
+
113
+ getOriginalCallbacks(mesh: Object3D): { onBeforeRender?: ObjectRenderCallback; onAfterRender?: ObjectRenderCallback } | undefined {
114
+ return this.meshToOriginalCallbacks.get(mesh);
115
+ }
116
+
117
+ setOriginalCallbacks(mesh: Object3D, callbacks: { onBeforeRender?: ObjectRenderCallback; onAfterRender?: ObjectRenderCallback }): void {
118
+ this.meshToOriginalCallbacks.set(mesh, callbacks);
119
+ }
120
+ }
121
+
122
+ const registry = new PropertyBlockRegistry();
123
+
124
+ /**
125
+ * MaterialPropertyBlock allows per-object material property overrides without creating new material instances.
126
+ * This is useful for rendering multiple objects with the same base material but different properties
127
+ * (e.g., different colors, textures, or shader parameters).
128
+ *
129
+ * The property block system works by:
130
+ * - Temporarily applying overrides in onBeforeRender
131
+ * - Restoring original values in onAfterRender
132
+ * - Managing shader defines and program cache keys for correct shader compilation
133
+ * - Supporting texture coordinate transforms per object
134
+ *
135
+ * Common use cases:
136
+ * - **Lightmaps**: Apply unique lightmap textures to individual objects sharing the same material
137
+ * - **Reflection Probes**: Apply different environment maps per object for localized reflections
138
+ * - **See-through effects**: Temporarily override transparency/transmission properties for X-ray effects
139
+ *
140
+ * ## Getting a MaterialPropertyBlock
141
+ *
142
+ * **Important**: Do not use the constructor directly. Instead, use the static {@link MaterialPropertyBlock.get} method:
143
+ *
144
+ * ```typescript
145
+ * const block = MaterialPropertyBlock.get(myMesh);
146
+ * ```
147
+ *
148
+ * This method will either return an existing property block or create a new one if it doesn't exist.
149
+ * It automatically:
150
+ * - Creates the property block instance
151
+ * - Registers it in the internal registry
152
+ * - Attaches the necessary render callbacks to the object
153
+ * - Handles Groups by applying overrides to all child meshes
154
+ *
155
+ * @example Basic usage
156
+ * ```typescript
157
+ * // Get or create a property block for an object
158
+ * const block = MaterialPropertyBlock.get(myMesh);
159
+ *
160
+ * // Override the color property
161
+ * block.setOverride("color", new Color(1, 0, 0));
162
+ *
163
+ * // Override a texture with custom UV transform (useful for lightmaps)
164
+ * block.setOverride("lightMap", myLightmapTexture, {
165
+ * offset: new Vector2(0.5, 0.5),
166
+ * repeat: new Vector2(2, 2)
167
+ * });
168
+ *
169
+ * // Set a shader define
170
+ * block.setDefine("USE_CUSTOM_FEATURE", 1);
171
+ * ```
172
+ *
173
+ * @example Lightmap usage
174
+ * ```typescript
175
+ * const block = MaterialPropertyBlock.get(mesh);
176
+ * block.setOverride("lightMap", lightmapTexture);
177
+ * block.setOverride("lightMapIntensity", 1.5);
178
+ * ```
179
+ *
180
+ * @example See-through effect
181
+ * ```typescript
182
+ * const block = MaterialPropertyBlock.get(mesh);
183
+ * block.setOverride("transparent", true);
184
+ * block.setOverride("opacity", 0.3);
185
+ * ```
186
+ *
187
+ * @template T The material type this property block is associated with
188
+ */
189
+ export class MaterialPropertyBlock<T extends Material = Material> {
190
+ private _overrides: PropertyBlockOverride[] = [];
191
+ private _defines: Record<string, string | number | boolean> = {};
192
+ private _object: Object3D | null = null;
193
+
194
+ /** The object this property block is attached to */
195
+ get object(): Object3D | null { return this._object; }
196
+
197
+ /**
198
+ * Creates a new MaterialPropertyBlock
199
+ * @param object The object this property block is for (optional)
200
+ */
201
+ protected constructor(object: Object3D | null = null) {
202
+ this._object = object;
203
+ }
204
+
205
+ /**
206
+ * Gets or creates a MaterialPropertyBlock for the given object.
207
+ * This is the recommended way to obtain a property block instance.
208
+ *
209
+ * @template T The material type
210
+ * @param object The object to get/create a property block for
211
+ * @returns The MaterialPropertyBlock associated with this object
212
+ *
213
+ * @example
214
+ * ```typescript
215
+ * const block = MaterialPropertyBlock.get(myMesh);
216
+ * block.setOverride("roughness", 0.5);
217
+ * ```
218
+ */
219
+ static get<T extends Material = Material>(object: Object3D): MaterialPropertyBlock<T> {
220
+ let block = registry.getBlock(object);
221
+ if (!block) {
222
+ block = new MaterialPropertyBlock(object);
223
+ registry.setBlock(object, block);
224
+ attachPropertyBlockToObject(object, block);
225
+ }
226
+ return block as MaterialPropertyBlock<T>;
227
+ }
228
+
229
+ /**
230
+ * Checks if an object has any property overrides
231
+ * @param object The object to check
232
+ * @returns True if the object has a property block with overrides
233
+ */
234
+ static hasOverrides(object: Object3D): boolean {
235
+ const block = registry.getBlock(object);
236
+ return block ? block.hasOverrides() : false;
237
+ }
238
+
239
+ /**
240
+ * Disposes this property block and cleans up associated resources.
241
+ * After calling dispose, this property block should not be used.
242
+ */
243
+ dispose() {
244
+ if (this._object) {
245
+ registry.deleteBlock(this._object);
246
+ // TODO: Add cleanup for hooked meshes
247
+ }
248
+ this._overrides = [];
249
+ this._object = null;
250
+ }
251
+
252
+ /**
253
+ * Sets or updates a material property override.
254
+ * The override will be applied to the material during rendering.
255
+ *
256
+ * @param name The name of the material property to override (e.g., "color", "map", "roughness")
257
+ * @param value The value to set
258
+ * @param textureTransform Optional UV transform (only used when value is a Texture)
259
+ *
260
+ * @example
261
+ * ```typescript
262
+ * // Override a simple property
263
+ * block.setOverride("roughness", 0.8);
264
+ *
265
+ * // Override a color
266
+ * block.setOverride("color", new Color(0xff0000));
267
+ *
268
+ * // Override a texture with UV transform
269
+ * block.setOverride("map", texture, {
270
+ * offset: new Vector2(0, 0),
271
+ * repeat: new Vector2(2, 2)
272
+ * });
273
+ * ```
274
+ */
275
+ setOverride<K extends NonFunctionPropertyNames<T>>(name: K, value: T[K], textureTransform?: TextureTransform): void;
276
+ setOverride(name: string, value: MaterialPropertyType, textureTransform?: TextureTransform): void;
277
+ setOverride(name: string, value: MaterialPropertyType, textureTransform?: TextureTransform): void {
278
+ const existing = this._overrides.find(o => o.name === name);
279
+ if (existing) {
280
+ existing.value = value;
281
+ existing.textureTransform = textureTransform;
282
+ } else {
283
+ this._overrides.push({ name, value, textureTransform });
284
+ }
285
+ }
286
+
287
+ /**
288
+ * Gets the override for a specific property with type-safe value inference
289
+ * @param name The property name to get
290
+ * @returns The PropertyBlockOverride with correctly typed value if it exists, undefined otherwise
291
+ *
292
+ * @example
293
+ * ```typescript
294
+ * const block = MaterialPropertyBlock.get<MeshStandardMaterial>(mesh);
295
+ *
296
+ * // Value is inferred as number | undefined
297
+ * const roughness = block.getOverride("roughness")?.value;
298
+ *
299
+ * // Value is inferred as Color | undefined
300
+ * const color = block.getOverride("color")?.value;
301
+ *
302
+ * // Value is inferred as Texture | null | undefined
303
+ * const map = block.getOverride("map")?.value;
304
+ *
305
+ * // Explicitly specify the type for properties not on the base material type
306
+ * const transmission = block.getOverride<number>("transmission")?.value;
307
+ *
308
+ * // Or use a more specific material type
309
+ * const physicalBlock = block as MaterialPropertyBlock<MeshPhysicalMaterial>;
310
+ * const transmissionTyped = physicalBlock.getOverride("transmission")?.value; // number
311
+ * ```
312
+ */
313
+ getOverride<K extends NonFunctionPropertyNames<T>>(name: K): PropertyBlockOverride<T[K] & MaterialPropertyType> | undefined;
314
+ getOverride<V extends MaterialPropertyType = MaterialPropertyType>(name: string): PropertyBlockOverride<V> | undefined;
315
+ getOverride(name: string): PropertyBlockOverride | undefined {
316
+ return this._overrides.find(o => o.name === name);
317
+ }
318
+
319
+ /**
320
+ * Removes a specific property override
321
+ * @param name The property name to clear
322
+ */
323
+ removeOveride<K extends NonFunctionPropertyNames<T>>(name: K | ({} & string)): void {
324
+ const index = this._overrides.findIndex(o => o.name === name);
325
+ if (index >= 0) {
326
+ this._overrides.splice(index, 1);
327
+ }
328
+ }
329
+
330
+ /**
331
+ * Removes all property overrides from this block
332
+ */
333
+ clearAllOverrides(): void {
334
+ this._overrides = [];
335
+ }
336
+
337
+ /**
338
+ * Gets all property overrides as a readonly array
339
+ * @returns Array of all property overrides
340
+ */
341
+ get overrides(): readonly PropertyBlockOverride[] {
342
+ return this._overrides;
343
+ }
344
+
345
+ /**
346
+ * Checks if this property block has any overrides
347
+ * @returns True if there are any overrides set
348
+ */
349
+ hasOverrides(): boolean {
350
+ return this._overrides.length > 0;
351
+ }
352
+
353
+ /**
354
+ * Set a shader define that will be included in the program cache key.
355
+ * This allows different objects sharing the same material to have different shader programs.
356
+ *
357
+ * Defines affect shader compilation and are useful for enabling/disabling features per-object.
358
+ *
359
+ * @param name The define name (e.g., "USE_LIGHTMAP", "ENABLE_REFLECTIONS")
360
+ * @param value The define value (typically a boolean, number, or string)
361
+ *
362
+ * @example
363
+ * ```typescript
364
+ * // Enable a feature for this specific object
365
+ * block.setDefine("USE_CUSTOM_SHADER", true);
366
+ * block.setDefine("QUALITY_LEVEL", 2);
367
+ * ```
368
+ */
369
+ setDefine(name: string, value: string | number | boolean): void {
370
+ this._defines[name] = value;
371
+ }
372
+
373
+ /**
374
+ * Remove a shader define
375
+ * @param name The define name to remove
376
+ */
377
+ clearDefine(name: string): void {
378
+ this._defines[name] = undefined as any;
379
+ }
380
+
381
+ /**
382
+ * Get all defines set on this property block
383
+ * @returns A readonly record of all defines
384
+ */
385
+ getDefines(): Readonly<Record<string, string | number | boolean>> {
386
+ return this._defines;
387
+ }
388
+
389
+ /**
390
+ * Generates a cache key based on the current overrides and defines.
391
+ * This key is used internally to ensure correct shader program selection
392
+ * when objects share materials but have different property blocks.
393
+ *
394
+ * @returns A string representing the current state of this property block
395
+ * @internal
396
+ */
397
+ getCacheKey(): string {
398
+ const parts: string[] = [];
399
+
400
+ // Add defines to cache key
401
+ const defineKeys = Object.keys(this._defines).sort();
402
+ for (const key of defineKeys) {
403
+ const value = this._defines[key];
404
+ if (value !== undefined) {
405
+ parts.push(`d:${key}=${value}`);
406
+ }
407
+ }
408
+
409
+ // Add overrides to cache key
410
+ for (const o of this._overrides) {
411
+ if (o.value === null) continue;
412
+ let val = "";
413
+ if (o.value instanceof Texture) {
414
+ val = (o.value as any).uuid || "texture";
415
+ if (o.textureTransform) {
416
+ const t = o.textureTransform;
417
+ if (t.offset) val += `;to:${t.offset.x},${t.offset.y}`;
418
+ if (t.repeat) val += `;tr:${t.repeat.x},${t.repeat.y}`;
419
+ }
420
+ } else if (Array.isArray(o.value)) {
421
+ val = o.value.join(",");
422
+ } else if (o.value && typeof o.value === "object" && "r" in o.value) {
423
+ const c = o.value as any;
424
+ val = `${c.r},${c.g},${c.b},${c.a !== undefined ? c.a : ""}`;
425
+ } else if (o.value && typeof o.value === "object" && "x" in o.value) {
426
+ // Vector2, Vector3, Vector4
427
+ const v = o.value as any;
428
+ val = `${v.x},${v.y}${v.z !== undefined ? `,${v.z}` : ""}${v.w !== undefined ? `,${v.w}` : ""}`;
429
+ } else {
430
+ val = String(o.value);
431
+ }
432
+ parts.push(`${o.name}=${val}`);
433
+ }
434
+ return parts.join(";");
435
+ }
436
+ }
437
+
438
+ /**
439
+ * Symbol used to store original material values on the material object
440
+ * @internal
441
+ */
442
+ const $originalValues = Symbol("originalValues");
443
+
444
+ /**
445
+ * Stores an original material property value before override
446
+ * @internal
447
+ */
448
+ interface OriginalValue {
449
+ name: string;
450
+ value: unknown;
451
+ }
452
+
453
+ /**
454
+ * Stores saved texture transform state for restoration
455
+ * @internal
456
+ */
457
+ interface SavedTextureTransform {
458
+ name: string;
459
+ offsetX: number;
460
+ offsetY: number;
461
+ repeatX: number;
462
+ repeatY: number;
463
+ }
464
+
465
+ /**
466
+ * Symbol used to store saved texture transforms on the material object
467
+ * @internal
468
+ */
469
+ const $savedTextureTransforms = Symbol("savedTextureTransforms");
470
+
471
+ /**
472
+ * Type for Three.js object render callbacks
473
+ * @internal
474
+ */
475
+ type ObjectRenderCallback = (this: Object3D, renderer: WebGLRenderer, scene: Scene, camera: Camera, geometry: BufferGeometry, material: Material, group: Group) => void;
476
+
477
+ /**
478
+ * Collect all materials from an object and its children
479
+ * @internal
480
+ */
481
+ function collectMaterials(object: Object3D, materials: Set<Material>): void {
482
+ const obj = object as Object3D & { material?: Material | Material[] };
483
+ if (obj.material) {
484
+ if (Array.isArray(obj.material)) {
485
+ obj.material.forEach(mat => materials.add(mat));
486
+ } else {
487
+ materials.add(obj.material);
488
+ }
489
+ }
490
+
491
+ // For Groups, collect materials from children too
492
+ if (object.type === "Group") {
493
+ object.children.forEach(child => collectMaterials(child, materials));
494
+ }
495
+ }
496
+
497
+ /**
498
+ * Find property block by checking this object and parent (if parent is a Group).
499
+ * Returns both the block and the owner object.
500
+ *
501
+ * @param obj The object to search from
502
+ * @returns The property block and its owner object, or undefined if not found
503
+ * @internal
504
+ */
505
+ function findPropertyBlockAndOwner(obj: Object3D): { block: MaterialPropertyBlock; owner: Object3D } | undefined {
506
+ // First check if this object itself has a property block
507
+ let block = registry.getBlock(obj);
508
+ if (block) return { block, owner: obj };
509
+
510
+ // If not, check if parent is a Group and has a property block
511
+ if (obj.parent && obj.parent.type === "Group") {
512
+ block = registry.getBlock(obj.parent);
513
+ if (block) return { block, owner: obj.parent };
514
+ }
515
+
516
+ return undefined;
517
+ }
518
+
519
+ /**
520
+ * Symbol to track which materials are currently being rendered for an object
521
+ * @internal
522
+ */
523
+ const currentlyRenderingFlag = Symbol("beforeRenderingFlag");
524
+
525
+ /**
526
+ * Tracks original transparent values that were changed during render list building
527
+ * @internal
528
+ */
529
+ const beforeRenderListTransparentChanged = new WeakMap<Object3D, boolean>();
530
+
531
+ /**
532
+ * Tracks original transmission values that were changed during render list building
533
+ * @internal
534
+ */
535
+ const beforeRenderListTransparentChangedTransmission = new WeakMap<Object3D, number>();
536
+
537
+ /**
538
+ * Callback invoked before an object is added to the render list.
539
+ * Used to temporarily override transparency/transmission for correct render list assignment.
540
+ * @internal
541
+ */
542
+ const onBeforeRenderListPush = function (this: Object3D, _object: Object3D, _geometry: BufferGeometry, material: Material, _group: Group) {
543
+ const block = registry.getBlock(_object);
544
+ if (!block) {
545
+ return;
546
+ }
547
+ if (block.hasOverrides()) {
548
+ const transmission = block.getOverride<number>("transmission")?.value;
549
+ const transparent = block.getOverride("transparent")?.value;
550
+
551
+ if (transmission !== undefined && typeof transmission === "number" && "transmission" in material && transmission !== material.transmission) {
552
+ beforeRenderListTransparentChangedTransmission.set(this, material.transmission as number);
553
+ material.transmission = transmission;
554
+ }
555
+ if (transparent !== undefined && typeof transparent === "boolean" && transparent !== material.transparent) {
556
+ beforeRenderListTransparentChanged.set(this, material.transparent);
557
+ material.transparent = transparent;
558
+ }
559
+ }
560
+ }
561
+ /**
562
+ * Callback invoked after an object is added to the render list.
563
+ * Restores the original transparency/transmission values that were overridden in onBeforeRenderListPush.
564
+ * @internal
565
+ */
566
+ const onAfterRenderListPush = function (this: Object3D, _object: Object3D, _geometry: BufferGeometry, material: Material, _group: Group) {
567
+ const prevTransparent = beforeRenderListTransparentChanged.get(_object);
568
+ if (prevTransparent !== undefined) {
569
+ beforeRenderListTransparentChanged.delete(_object);
570
+ material.transparent = prevTransparent;
571
+ }
572
+
573
+ const prevTransmission = beforeRenderListTransparentChangedTransmission.get(_object);
574
+ if (prevTransmission !== undefined) {
575
+ beforeRenderListTransparentChangedTransmission.delete(_object);
576
+ (material as any).transmission = prevTransmission;
577
+ }
578
+ }
579
+
580
+ // #region OnBeforeRender
581
+ /**
582
+ * Main callback invoked before rendering an object.
583
+ * Applies property block overrides and defines to the material.
584
+ * @internal
585
+ */
586
+ const onBeforeRender_MaterialBlock: ObjectRenderCallback = function (this: Object3D, _renderer: WebGLRenderer, _scene: Scene, _camera: Camera, _geometry: BufferGeometry, material: Material, _group: Group) {
587
+
588
+
589
+ // Only run if the material belongs to this object and is a "regular" material (not depth or other override material)
590
+ const materials = (this as any).material as Array<Material> | Material | undefined;
591
+ if (!materials) return;
592
+ if (Array.isArray(materials)) {
593
+ if (!materials.includes(material)) return;
594
+ }
595
+ else if (materials !== material) {
596
+ return;
597
+ }
598
+
599
+ // Keep track of which materials rendering started for so we can check in onAfterRender if it was processed
600
+ // (in case of override materials like depth material where onBeforeRender runs but we don't want to apply overrides)
601
+ if (this[currentlyRenderingFlag] === undefined) this[currentlyRenderingFlag] = new WeakSet<Material>();
602
+ this[currentlyRenderingFlag].add(material);
603
+
604
+
605
+ // Before rendering, check if this object (or its parent Group) has a property block with overrides for this material.
606
+ const result = findPropertyBlockAndOwner(this);
607
+ if (!result) {
608
+ return;
609
+ }
610
+
611
+ const { block: propertyBlock, owner } = result;
612
+
613
+ // Only apply if this material was registered with this property block
614
+ // if (!registry.hasMaterial(owner, material)) {
615
+ // return;
616
+ // }
617
+
618
+ const overrides = propertyBlock.overrides;
619
+ const mat = material as any;
620
+
621
+ // Apply defines to material - this affects shader compilation
622
+ const defines = propertyBlock.getDefines();
623
+ const defineKeys = Object.keys(defines);
624
+ if (defineKeys.length > 0) {
625
+ if (!mat.defines) mat.defines = {};
626
+ for (const key of defineKeys) {
627
+ const value = defines[key];
628
+ if (value !== undefined) {
629
+ mat.defines[key] = value;
630
+ }
631
+ }
632
+ }
633
+
634
+ // Still set up cache key even if no overrides (defines affect it)
635
+ if (overrides.length === 0 && defineKeys.length === 0) {
636
+ return;
637
+ }
638
+
639
+ // Defines always affect shader compilation → need program change
640
+ let needsProgramChange = defineKeys.length > 0;
641
+
642
+ if (!mat[$originalValues]) {
643
+ mat[$originalValues] = [];
644
+ }
645
+ const originalValues = mat[$originalValues] as OriginalValue[];
646
+
647
+ for (const override of overrides) {
648
+ if (override.value === null) continue;
649
+
650
+ const currentValue = mat[override.name];
651
+
652
+ const existingOriginal = originalValues.find((o: OriginalValue) => o.name === override.name);
653
+ if (existingOriginal) {
654
+ // Update to current value each frame so animations/external changes are preserved
655
+ existingOriginal.value = currentValue;
656
+ } else {
657
+ originalValues.push({ name: override.name, value: currentValue });
658
+ }
659
+
660
+ // Check if this override changes shader features (truthiness change).
661
+ // E.g. null → Texture enables USE_LIGHTMAP, Texture → null disables it.
662
+ // Pure uniform changes (red → blue, textureA → textureB) don't need program switch.
663
+ if (!needsProgramChange && !!currentValue !== !!override.value) {
664
+ needsProgramChange = true;
665
+ }
666
+
667
+ // Set all material properties including lightMap -
668
+ // three.js reads material.lightMap to determine shader parameters and upload uniforms
669
+ mat[override.name] = override.value;
670
+
671
+ // Apply per-object texture transform (offset/repeat) if specified
672
+ if (override.textureTransform && override.value instanceof Texture) {
673
+ const tex = override.value;
674
+ if (!mat[$savedTextureTransforms]) mat[$savedTextureTransforms] = [];
675
+ (mat[$savedTextureTransforms] as SavedTextureTransform[]).push({
676
+ name: override.name,
677
+ offsetX: tex.offset.x, offsetY: tex.offset.y,
678
+ repeatX: tex.repeat.x, repeatY: tex.repeat.y
679
+ });
680
+ const t = override.textureTransform;
681
+ if (t.offset) tex.offset.copy(t.offset);
682
+ if (t.repeat) tex.repeat.copy(t.repeat);
683
+ }
684
+ }
685
+
686
+ // Only set needsUpdate when overrides change shader features (truthiness changes
687
+ // like null↔texture, or defines added). This triggers getProgram() for program switches.
688
+ // Pure uniform overrides (color, roughness) skip this — no version increment needed.
689
+ if (needsProgramChange) {
690
+ mat.needsUpdate = true;
691
+ }
692
+ // _forceRefresh triggers uniform re-upload for consecutive objects sharing
693
+ // the same program and material (without it three.js skips the upload).
694
+ mat._forceRefresh = true;
695
+ };
696
+
697
+ // #region OnAfterRender
698
+ /**
699
+ * Main callback invoked after rendering an object.
700
+ * Restores the original material property values and defines.
701
+ * @internal
702
+ */
703
+ const onAfterRender_MaterialBlock: ObjectRenderCallback = function (this: Object3D, _renderer: WebGLRenderer, _scene: Scene, _camera: Camera, _geometry: BufferGeometry, material: Material, _group: Group) {
704
+
705
+ // We don't want to run this logic if onBeforeRender didn't run for this material (e.g. due to DepthMaterial or other override material), so we check the flag set in onBeforeRender
706
+ if (this[currentlyRenderingFlag] === undefined) return;
707
+ if (!this[currentlyRenderingFlag].has(material)) return;
708
+ this[currentlyRenderingFlag].delete(material);
709
+
710
+
711
+ const result = findPropertyBlockAndOwner(this);
712
+ if (!result) {
713
+ return;
714
+ }
715
+
716
+ const { block: propertyBlock, owner } = result;
717
+
718
+ // Only restore if this material was registered with this property block
719
+ // if (!registry.hasMaterial(owner, material)) {
720
+ // return;
721
+ // }
722
+
723
+ const overrides = propertyBlock.overrides;
724
+
725
+ const mat = material as any;
726
+ const originalValues = mat[$originalValues] as OriginalValue[] | undefined;
727
+
728
+ // Clean up defines — this affects shader compilation
729
+ const defines = propertyBlock.getDefines();
730
+ const defineKeys = Object.keys(defines);
731
+ let needsProgramChange = false;
732
+ if (defineKeys.length > 0 && mat.defines) {
733
+ for (const key of defineKeys) {
734
+ delete mat.defines[key];
735
+ }
736
+ needsProgramChange = true;
737
+ }
738
+
739
+ if (overrides.length === 0) {
740
+ if (needsProgramChange) {
741
+ mat.needsUpdate = true;
742
+ mat._forceRefresh = true;
743
+ }
744
+ return;
745
+ }
746
+
747
+ if (!originalValues) return;
748
+
749
+ // Restore texture transforms before restoring material properties
750
+ const savedTransforms = mat[$savedTextureTransforms] as SavedTextureTransform[] | undefined;
751
+ if (savedTransforms && savedTransforms.length > 0) {
752
+ for (const saved of savedTransforms) {
753
+ const override = overrides.find(o => o.name === saved.name);
754
+ if (override?.value instanceof Texture) {
755
+ override.value.offset.set(saved.offsetX, saved.offsetY);
756
+ override.value.repeat.set(saved.repeatX, saved.repeatY);
757
+ }
758
+ }
759
+ savedTransforms.length = 0;
760
+ }
761
+
762
+ for (const override of overrides) {
763
+ const original = originalValues.find(o => o.name === override.name);
764
+ if (original) {
765
+ // Check if restoring changes shader features (truthiness change)
766
+ if (!needsProgramChange && !!override.value !== !!original.value) {
767
+ needsProgramChange = true;
768
+ }
769
+ mat[override.name] = original.value;
770
+ }
771
+ }
772
+
773
+ // Only set needsUpdate when restoring affects shader features
774
+ if (needsProgramChange) {
775
+ mat.needsUpdate = true;
776
+ }
777
+ // Always force uniform refresh so the next object gets correct values
778
+ mat._forceRefresh = true;
779
+ };
780
+
781
+
782
+ // #region Attach Callbacks
783
+ /**
784
+ * Attaches the property block render callbacks to an object and its child meshes.
785
+ * @param object The object to attach callbacks to
786
+ * @param _propertyBlock The property block being attached (unused but kept for clarity)
787
+ * @internal
788
+ */
789
+ function attachPropertyBlockToObject(object: Object3D, _propertyBlock: MaterialPropertyBlock): void {
790
+ // Collect and register all materials that belong to this property block
791
+ // const materials = new Set<Material>();
792
+ // collectMaterials(object, materials);
793
+ // materials.forEach(mat => registry.addMaterial(object, mat));
794
+
795
+ // Attach callbacks to renderable objects (Mesh, SkinnedMesh)
796
+ // Groups don't render themselves but we still need to handle child meshes
797
+ if (object.type === "Group") {
798
+ object.children.forEach(child => {
799
+ if (child.type === "Mesh" || child.type === "SkinnedMesh") {
800
+ attachCallbacksToMesh(child, object);
801
+ }
802
+ });
803
+ } else if (object.type === "Mesh" || object.type === "SkinnedMesh") {
804
+ attachCallbacksToMesh(object, object);
805
+ }
806
+ }
807
+ /**
808
+ * Attaches render callbacks to a specific mesh object.
809
+ * Chains with existing callbacks if they exist.
810
+ * @param mesh The mesh to attach callbacks to
811
+ * @param propertyBlockOwner The object that owns the property block (may be the mesh itself or its parent Group)
812
+ * @internal
813
+ */
814
+ function attachCallbacksToMesh(mesh: Object3D, propertyBlockOwner: Object3D): void {
815
+ // Check if this specific mesh already has our callbacks attached for this property block owner
816
+ if (registry.isHooked(mesh, propertyBlockOwner)) {
817
+ // Already hooked for this property block owner
818
+ return;
819
+ }
820
+
821
+ registry.addHook(mesh, propertyBlockOwner);
822
+
823
+ if (!mesh.onBeforeRender) {
824
+ mesh.onBeforeRender = onBeforeRender_MaterialBlock;
825
+ } else {
826
+ const original = mesh.onBeforeRender;
827
+ mesh.onBeforeRender = function (renderer, scene, camera, geometry, material, group) {
828
+ original.call(this, renderer, scene, camera, geometry, material, group);
829
+ onBeforeRender_MaterialBlock.call(this, renderer, scene, camera, geometry, material, group);
830
+ };
831
+ }
832
+
833
+ if (!mesh.onAfterRender) {
834
+ mesh.onAfterRender = onAfterRender_MaterialBlock;
835
+ } else {
836
+ const original = mesh.onAfterRender;
837
+ mesh.onAfterRender = function (renderer, scene, camera, geometry, material, group) {
838
+ onAfterRender_MaterialBlock.call(this, renderer, scene, camera, geometry, material, group);
839
+ original.call(this, renderer, scene, camera, geometry, material, group);
840
+ };
841
+ }
842
+
843
+ /** @ts-ignore patched in three.js */
844
+ mesh.onBeforeRenderListPush = onBeforeRenderListPush;
845
+ /** @ts-ignore patched in three.js */
846
+ mesh.onAfterRenderListPush = onAfterRenderListPush;
847
+
848
+ }
849
+ //#endregion
850
+
851
+ /**
852
+ * Checks if an object has a MaterialPropertyBlock attached to it.
853
+ *
854
+ * @param object The object to check
855
+ * @returns True if the object has a property block registered
856
+ *
857
+ * @example
858
+ * ```typescript
859
+ * if (objectHasPropertyBlock(myMesh)) {
860
+ * console.log("This mesh has property overrides");
861
+ * }
862
+ * ```
863
+ */
864
+ export function objectHasPropertyBlock(object: Object3D): boolean {
865
+ return registry.getBlock(object) !== undefined;
866
+ }