@needle-tools/engine 4.11.4 → 4.11.5-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 (50) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/dist/{needle-engine.bundle-CQdk7IvU.min.js → needle-engine.bundle-78QepNU6.min.js} +115 -115
  3. package/dist/{needle-engine.bundle-BxK1-fWD.umd.cjs → needle-engine.bundle-CtMdIm9E.umd.cjs} +120 -120
  4. package/dist/{needle-engine.bundle-DmMrUPFQ.js → needle-engine.bundle-DLreKDiJ.js} +3417 -3384
  5. package/dist/needle-engine.js +2 -2
  6. package/dist/needle-engine.min.js +1 -1
  7. package/dist/needle-engine.umd.cjs +1 -1
  8. package/lib/engine/api.d.ts +1 -1
  9. package/lib/engine/api.js +1 -1
  10. package/lib/engine/api.js.map +1 -1
  11. package/lib/engine/engine_addressables.d.ts +74 -11
  12. package/lib/engine/engine_addressables.js +74 -11
  13. package/lib/engine/engine_addressables.js.map +1 -1
  14. package/lib/engine/engine_camera.fit.d.ts +48 -3
  15. package/lib/engine/engine_camera.fit.js +29 -0
  16. package/lib/engine/engine_camera.fit.js.map +1 -1
  17. package/lib/engine/engine_context.d.ts +18 -3
  18. package/lib/engine/engine_context.js +18 -3
  19. package/lib/engine/engine_context.js.map +1 -1
  20. package/lib/engine/engine_utils_qrcode.js +1 -1
  21. package/lib/engine/engine_utils_qrcode.js.map +1 -1
  22. package/lib/engine/extensions/NEEDLE_components.d.ts +4 -4
  23. package/lib/engine/extensions/NEEDLE_components.js +36 -17
  24. package/lib/engine/extensions/NEEDLE_components.js.map +1 -1
  25. package/lib/engine/webcomponents/buttons.d.ts +3 -1
  26. package/lib/engine/webcomponents/buttons.js +3 -1
  27. package/lib/engine/webcomponents/buttons.js.map +1 -1
  28. package/lib/engine/webcomponents/needle menu/needle-menu.d.ts +39 -2
  29. package/lib/engine/webcomponents/needle menu/needle-menu.js +39 -2
  30. package/lib/engine/webcomponents/needle menu/needle-menu.js.map +1 -1
  31. package/lib/engine-components/Renderer.js +14 -40
  32. package/lib/engine-components/Renderer.js.map +1 -1
  33. package/lib/engine-components/RendererLightmap.d.ts +1 -0
  34. package/lib/engine-components/RendererLightmap.js +24 -18
  35. package/lib/engine-components/RendererLightmap.js.map +1 -1
  36. package/lib/engine-components/timeline/PlayableDirector.d.ts +2 -1
  37. package/lib/engine-components/timeline/PlayableDirector.js +16 -9
  38. package/lib/engine-components/timeline/PlayableDirector.js.map +1 -1
  39. package/package.json +1 -1
  40. package/src/engine/api.ts +1 -1
  41. package/src/engine/engine_addressables.ts +75 -11
  42. package/src/engine/engine_camera.fit.ts +49 -4
  43. package/src/engine/engine_context.ts +18 -3
  44. package/src/engine/engine_utils_qrcode.ts +1 -1
  45. package/src/engine/extensions/NEEDLE_components.ts +47 -26
  46. package/src/engine/webcomponents/buttons.ts +3 -1
  47. package/src/engine/webcomponents/needle menu/needle-menu.ts +40 -3
  48. package/src/engine-components/Renderer.ts +14 -42
  49. package/src/engine-components/RendererLightmap.ts +27 -17
  50. package/src/engine-components/timeline/PlayableDirector.ts +16 -10
@@ -1,6 +1,7 @@
1
1
  import { Camera, Object3D, PerspectiveCamera, Vector3, Vector3Like } from "three";
2
2
 
3
3
  import { GroundProjectedEnv } from "../engine-components/GroundProjection.js";
4
+ import type { OrbitControls } from "../engine-components/OrbitControls.js";
4
5
  import { findObjectOfType } from "./engine_components.js";
5
6
  import { Context } from "./engine_context.js";
6
7
  import { Gizmos } from "./engine_gizmos.js";
@@ -9,7 +10,10 @@ import { NeedleXRSession } from "./xr/NeedleXRSession.js";
9
10
 
10
11
 
11
12
  /**
12
- * Options for fitting the camera to the scene. Used in {@link OrbitControls.fitCamera}
13
+ * Options for fitting a camera to the scene or specific objects.
14
+ *
15
+ * Used by {@link OrbitControls.fitCamera} and the {@link fitCamera}.
16
+ *
13
17
  */
14
18
  export type FitCameraOptions = {
15
19
  /** When enabled debug rendering will be shown */
@@ -31,8 +35,17 @@ export type FitCameraOptions = {
31
35
  */
32
36
  camera?: Camera,
33
37
 
38
+ /**
39
+ * The current zoom level of the camera (used to avoid clipping when fitting)
40
+ */
34
41
  currentZoom?: number,
42
+ /**
43
+ * Minimum and maximum zoom levels for the camera (e.g. if zoom is constrained by OrbitControls)
44
+ */
35
45
  minZoom?: number,
46
+ /**
47
+ * Maximum zoom level for the camera (e.g. if zoom is constrained by OrbitControls)
48
+ */
36
49
  maxZoom?: number,
37
50
 
38
51
  /**
@@ -40,7 +53,11 @@ export type FitCameraOptions = {
40
53
  */
41
54
  objects?: Object3D[] | Object3D;
42
55
 
43
- /** Fit offset: A factor to multiply the distance to the objects by
56
+ /**
57
+ * A factor to control padding around the fitted objects.
58
+ *
59
+ * Values > 1 will add more space around the fitted objects, values < 1 will zoom in closer.
60
+ *
44
61
  * @default 1.1
45
62
  */
46
63
  fitOffset?: number,
@@ -74,7 +91,7 @@ export type FitCameraOptions = {
74
91
  relativeTargetOffset?: Partial<Vector3Like>,
75
92
 
76
93
  /**
77
- * Field of view (FOV) for the camera
94
+ * Target field of view (FOV) for the camera
78
95
  */
79
96
  fov?: number,
80
97
  }
@@ -86,7 +103,35 @@ export type FitCameraReturnType = {
86
103
  fov: number | undefined
87
104
  }
88
105
 
89
-
106
+ /**
107
+ * Fit the camera to the specified objects or the whole scene.
108
+ * Adjusts the camera position and optionally the FOV to ensure all objects are visible.
109
+ *
110
+ * @example Fit the main camera to the entire scene:
111
+ * ```ts
112
+ * import { fitCamera } from '@needle-tools/engine';
113
+ *
114
+ * // Fit the main camera to the entire scene
115
+ * fitCamera();
116
+ * ```
117
+ * @example Fit a specific camera to specific objects with custom options:
118
+ * ```ts
119
+ * import { fitCamera } from '@needle-tools/engine';
120
+ *
121
+ * // Fit a specific camera to specific objects with custom options
122
+ * const myCamera = ...; // your camera
123
+ * const objectsToFit = [...]; // array of objects to fit
124
+ * fitCamera({
125
+ * camera: myCamera,
126
+ * objects: objectsToFit,
127
+ * fitOffset: 1,
128
+ * fov: 20,
129
+ * });
130
+ * ```
131
+ *
132
+ * @param options Options for fitting the camera
133
+ * @returns
134
+ */
90
135
  export function fitCamera(options?: FitCameraOptions): null | FitCameraReturnType {
91
136
 
92
137
  if (NeedleXRSession.active) {
@@ -129,9 +129,12 @@ export function registerComponent(script: IComponent, context?: Context) {
129
129
  }
130
130
 
131
131
  /**
132
- * The context is the main object that holds all the data and state of the Needle Engine.
133
- * It can be used to access the scene, renderer, camera, input, physics, networking, and more.
134
- * @example
132
+ * The Needle Engine context is the main access point that holds all the data and state of a Needle Engine application.
133
+ * It can be used to access the {@link Context.scene}, {@link Context.renderer}, {@link Context.mainCamera}, {@link Context.input}, {@link Context.physics}, {@link Context.time}, {@link Context.connection} (networking), and more.
134
+ *
135
+ * The context is automatically created when using the `<needle-engine>` web component.
136
+ *
137
+ * @example Accessing the context from a [component](https://engine.needle.tools/docs/api/Behaviour):
135
138
  * ```typescript
136
139
  * import { Behaviour } from "@needle-tools/engine";
137
140
  * import { Mesh, BoxGeometry, MeshBasicMaterial } from "three";
@@ -142,6 +145,18 @@ export function registerComponent(script: IComponent, context?: Context) {
142
145
  * }
143
146
  * }
144
147
  * ```
148
+ *
149
+ * @example Accessing the context from a [hook](https://engine.needle.tools/docs/scripting.html#hooks) without a component e.g. from a javascript module or svelte or react component.
150
+ *
151
+ * ```typescript
152
+ * import { onStart } from "@needle-tools/engine";
153
+ *
154
+ * onStart((context) => {
155
+ * console.log("Hello from onStart hook");
156
+ * context.scene.add(new Mesh(new BoxGeometry(), new MeshBasicMaterial()));
157
+ * });
158
+ * ```
159
+ *
145
160
  */
146
161
  export class Context implements IContext {
147
162
 
@@ -166,7 +166,7 @@ async function internalRenderQRCodeOverlays(canvas: HTMLCanvasElement, args: { s
166
166
  haveLogo = await new Promise((resolve, _reject) => {
167
167
  image.onload = () => resolve(true);
168
168
  image.onerror = (err) => {
169
- let errorUrl = logoSrc !== needleLogoOnlySVG ? "'" + logoSrc + "'" : null;
169
+ const errorUrl = logoSrc !== needleLogoOnlySVG ? "'" + logoSrc + "'" : null;
170
170
  console.error("[QR Code] Error loading logo image for QR code", errorUrl, isDevEnvironment() ? err : "");
171
171
  resolve(false);
172
172
  };
@@ -2,12 +2,13 @@ import { Object3D } from "three";
2
2
  import { GLTFExporter } from 'three/examples/jsm/exporters/GLTFExporter.js';
3
3
  import { type GLTF, type GLTFLoaderPlugin, GLTFParser } from "three/examples/jsm/loaders/GLTFLoader.js";
4
4
 
5
+ import { isDevEnvironment } from "../debug/debug.js";
5
6
  import { builtinComponentKeyName } from "../engine_constants.js";
6
7
  import { debugExtension } from "../engine_default_parameters.js";
7
8
  import { getLoader } from "../engine_gltf.js";
8
9
  import { type NodeToObjectMap, type ObjectToNodeMap, SerializationContext } from "../engine_serialization_core.js";
9
10
  import { apply } from "../js-extensions/index.js";
10
- import { maskGltfAssociation,resolveReferences } from "./extension_utils.js";
11
+ import { maskGltfAssociation, resolveReferences } from "./extension_utils.js";
11
12
 
12
13
  export const debug = debugExtension
13
14
  const componentsArrayExportKey = "$___Export_Components";
@@ -15,7 +16,7 @@ const componentsArrayExportKey = "$___Export_Components";
15
16
  export const EXTENSION_NAME = "NEEDLE_components";
16
17
 
17
18
  class ExtensionData {
18
- [builtinComponentKeyName]?: Array<object | null>
19
+ [builtinComponentKeyName]?: Array<Record<string, any> | null>
19
20
  }
20
21
 
21
22
  class ExportData {
@@ -35,14 +36,7 @@ export class NEEDLE_components implements GLTFLoaderPlugin {
35
36
  get name(): string {
36
37
  return EXTENSION_NAME;
37
38
  }
38
-
39
- // import
40
- parser?: GLTFParser;
41
- nodeToObjectMap: NodeToObjectMap = {};
42
- /** The loaded gltf */
43
- gltf: GLTF | null = null;
44
-
45
- // export
39
+ // #region export
46
40
  exportContext!: { [nodeIndex: number]: ExportData };
47
41
  objectToNodeMap: ObjectToNodeMap = {};
48
42
  context!: SerializationContext;
@@ -112,7 +106,7 @@ export class NEEDLE_components implements GLTFLoaderPlugin {
112
106
 
113
107
  writeNode(node: Object3D, nodeDef) {
114
108
  const nodeIndex = this.writer.json.nodes.length;
115
- if (debug)
109
+ if (debug)
116
110
  console.log(node.name, nodeIndex, node.uuid);
117
111
  const context = new ExportData(node, nodeIndex, nodeDef);
118
112
  this.exportContext[nodeIndex] = context;
@@ -158,8 +152,13 @@ export class NEEDLE_components implements GLTFLoaderPlugin {
158
152
 
159
153
 
160
154
  // -------------------------------------
161
- // LOADING
162
- // called by GLTFLoader
155
+ // #region import
156
+ parser?: GLTFParser;
157
+ nodeToObjectMap: NodeToObjectMap = {};
158
+ /** The loaded gltf */
159
+ gltf: GLTF | null = null;
160
+
161
+
163
162
  beforeRoot() {
164
163
  if (debug)
165
164
  console.log("BEGIN LOAD");
@@ -167,7 +166,6 @@ export class NEEDLE_components implements GLTFLoaderPlugin {
167
166
  return null;
168
167
  }
169
168
 
170
- // called by GLTFLoader
171
169
  async afterRoot(result: GLTF): Promise<void> {
172
170
  this.gltf = result;
173
171
 
@@ -175,8 +173,7 @@ export class NEEDLE_components implements GLTFLoaderPlugin {
175
173
  const ext = parser?.extensions;
176
174
  if (!ext) return;
177
175
  const hasExtension = ext[this.name];
178
- if (debug)
179
- console.log("After root", result, this.parser, ext);
176
+ if (debug) console.log("After root", result, this.parser, ext);
180
177
 
181
178
  const loadComponents: Array<Promise<void>> = [];
182
179
  if (hasExtension === true) {
@@ -204,7 +201,7 @@ export class NEEDLE_components implements GLTFLoaderPlugin {
204
201
 
205
202
  apply(obj);
206
203
 
207
- loadComponents.push(this.createComponents(obj, data));
204
+ loadComponents.push(this.createComponents(result, node, obj, data));
208
205
  }
209
206
  }
210
207
  }
@@ -220,7 +217,7 @@ export class NEEDLE_components implements GLTFLoaderPlugin {
220
217
  }
221
218
  }
222
219
 
223
- private async createComponents(obj: Object3D, data: ExtensionData) {
220
+ private async createComponents(result: GLTF, node: Node, obj: Object3D, data: ExtensionData) {
224
221
  if (!data) return;
225
222
  const componentData = data[builtinComponentKeyName];
226
223
  if (componentData) {
@@ -228,20 +225,42 @@ export class NEEDLE_components implements GLTFLoaderPlugin {
228
225
  if (debug)
229
226
  console.log(obj.name, componentData);
230
227
  for (const i in componentData) {
231
- const serializedData = componentData[i];
232
- if (debug)
233
- console.log("Serialized data", JSON.parse(JSON.stringify(serializedData)));
228
+ const data = componentData[i];
229
+
230
+ if (debug) console.log("Serialized data", JSON.parse(JSON.stringify(data)));
231
+
232
+ // Fix for https://linear.app/needle/issue/NE-6779/blender-export-has-missing-sharedmaterials
233
+ if (data?.name === "MeshRenderer" || data?.name === "SkinnedMeshRenderer") {
234
+ if (!data.sharedMaterials) {
235
+ let success = false;
236
+ if ("mesh" in node) {
237
+ const meshIndex = node.mesh;
238
+ if (typeof meshIndex === "number" && result.parser) {
239
+ const meshDef = result.parser.json.meshes?.[meshIndex];
240
+ if (meshDef?.primitives) {
241
+ data.sharedMaterials = meshDef.primitives.map(prim => {
242
+ return "/materials/" + (prim.material ?? 0);
243
+ });
244
+ success = true;
245
+ }
246
+ }
247
+ }
248
+ if(!success && (debug || isDevEnvironment())) {
249
+ console.warn(`[NEEDLE_components] Component '${data.name}' on object '${obj.name}' is not added to a mesh or failed to retrieve materials from glTF.`);
250
+ }
251
+ }
252
+ }
234
253
 
235
- if (serializedData && this.parser) {
254
+ if (data && this.parser) {
236
255
  tasks.push(
237
- resolveReferences(this.parser, serializedData)
238
- .catch(e => console.error(`Error while resolving references (see console for details)\n`, e, obj, serializedData))
256
+ resolveReferences(this.parser, data)
257
+ .catch(e => console.error(`Error while resolving references (see console for details)\n`, e, obj, data))
239
258
  );
240
259
  }
241
260
 
242
261
  obj.userData = obj.userData || {};
243
262
  obj.userData[builtinComponentKeyName] = obj.userData[builtinComponentKeyName] || [];
244
- obj.userData[builtinComponentKeyName].push(serializedData);
263
+ obj.userData[builtinComponentKeyName].push(data);
245
264
  }
246
265
  await Promise.all(tasks).catch((e) => {
247
266
  console.error("Error while loading components", e);
@@ -266,4 +285,6 @@ export class NEEDLE_components implements GLTFLoaderPlugin {
266
285
  // // console.log(components);
267
286
  // return null;
268
287
  // }
269
- }
288
+ }
289
+
290
+
@@ -8,7 +8,9 @@ import { getIconElement } from "./icons.js";
8
8
  * Use the ButtonsFactory to create buttons with icons and functionality
9
9
  * Get access to the default buttons by using `ButtonsFactory.instance`
10
10
  * The factory will create the buttons if they don't exist yet, and return the existing ones if they do (this allows you to reparent or modify created buttons)
11
- */
11
+ *
12
+ * @category HTML
13
+ */
12
14
  export class ButtonsFactory {
13
15
 
14
16
  private static _instance?: ButtonsFactory;
@@ -48,11 +48,13 @@ export declare type ButtonInfo = {
48
48
  }
49
49
 
50
50
  /**
51
- * The NeedleMenu is a menu that can be displayed in the needle engine webcomponent or in VR/AR sessions.
51
+ * The NeedleMenu is a menu that can be displayed in the needle engine webcomponent or in VR/AR sessions.
52
+ *
52
53
  * The menu can be used to add buttons to the needle engine that can be used to interact with the application.
53
- * The menu can be positioned at the top or the bottom of the needle engine webcomponent
54
54
  *
55
- * @example Create a button using the NeedleMenu
55
+ * The menu can be positioned at the top or the bottom of the <needle-engine> webcomponent.
56
+ *
57
+ * @example Add a new button using the NeedleMenu
56
58
  * ```typescript
57
59
  * onStart(ctx => {
58
60
  * ctx.menu.appendChild({
@@ -76,6 +78,20 @@ export declare type ButtonInfo = {
76
78
  * }
77
79
  * }, "*");
78
80
  * ```
81
+ *
82
+ * @example Access the menu from a component
83
+ * ```typescript
84
+ * import { Behaviour, OnStart } from '@needle-tools/engine';
85
+ *
86
+ * export class MyComponent extends Behaviour {
87
+ *
88
+ * start() {
89
+ * this.context.menu.appendChild({ ... });
90
+ * }
91
+ * }
92
+ * ```
93
+ *
94
+ * @category HTML
79
95
  */
80
96
  export class NeedleMenu {
81
97
  private readonly _context: Context;
@@ -909,6 +925,27 @@ export class NeedleMenuElement extends HTMLElement {
909
925
  }
910
926
  }
911
927
  }
928
+ /**
929
+ * Appends a button or HTML element to the needle-menu options.
930
+ * @param node a Node or ButtonInfo to create a button from
931
+ * @returns the appended Node
932
+ *
933
+ * @example Append a button
934
+ * ```javascript
935
+ * const button = document.createElement("button");
936
+ * button.textContent = "Click Me";
937
+ * needleMenu.appendChild(button);
938
+ * ```
939
+ * @example Append a button using ButtonInfo
940
+ * ```javascript
941
+ * needleMenu.appendChild({
942
+ * label: "Click Me",
943
+ * onClick: () => { alert("Button clicked!"); },
944
+ * icon: "info",
945
+ * title: "This is a button",
946
+ * });
947
+ * ```
948
+ */
912
949
  appendChild<T extends Node>(node: T | ButtonInfo): T {
913
950
 
914
951
  if (!(node instanceof Node)) {
@@ -69,7 +69,7 @@ class SharedMaterialArray implements ISharedMaterials {
69
69
  set changed(value: boolean) {
70
70
  if (value === true) {
71
71
  if (debugRenderer)
72
- console.warn("SharedMaterials have changed: " + this._renderer.name, this);
72
+ console.warn("SharedMaterials have changed: " + this._renderer.name);
73
73
  }
74
74
  this._changed = value;
75
75
  }
@@ -358,11 +358,15 @@ export class Renderer extends Behaviour implements IRenderer {
358
358
  //@ts-ignore
359
359
  get sharedMaterials(): SharedMaterialArray {
360
360
 
361
- // @ts-ignore (original materials will be set during deserialization)
362
- if (this._originalMaterials === undefined) return null;
363
-
364
- // @ts-ignore during deserialization code might access this property *before* the setter and then create an empty array
365
- if (this.__isDeserializing === true) return null;
361
+ if (this._originalMaterials === undefined) {
362
+ if (!this.__didAwake) {
363
+ // @ts-ignore (original materials will be set during deserialization)
364
+ return null;
365
+ }
366
+ else {
367
+ this._originalMaterials = [];
368
+ }
369
+ }
366
370
 
367
371
  if (!this._sharedMaterials || !this._sharedMaterials.is(this)) {
368
372
  if (!this._originalMaterials) this._originalMaterials = [];
@@ -454,6 +458,7 @@ export class Renderer extends Behaviour implements IRenderer {
454
458
  this.context.addBeforeRenderListener(this.gameObject, this.onBeforeRenderThree);
455
459
  }
456
460
 
461
+ this._lightmaps = undefined;
457
462
  this.applyLightmapping();
458
463
 
459
464
  if (showWireframe) {
@@ -468,8 +473,8 @@ export class Renderer extends Behaviour implements IRenderer {
468
473
  }
469
474
 
470
475
  private applyLightmapping() {
471
- if (this.lightmapIndex >= 0) {
472
- const type = this.gameObject.type;
476
+ if (this.lightmapIndex >= 0 && !this._lightmaps) {
477
+ // const type = this.gameObject.type;
473
478
 
474
479
  // use the override lightmap if its not undefined
475
480
  const tex = this._lightmapTextureOverride !== undefined
@@ -477,45 +482,12 @@ export class Renderer extends Behaviour implements IRenderer {
477
482
  : this.context.lightmaps.tryGetLightmap(this.sourceId, this.lightmapIndex);
478
483
  if (tex) {
479
484
  if (!this._lightmaps) this._lightmaps = [];
480
-
481
-
482
485
  const rm = new RendererLightmap(this);
483
486
  rm.init(this.lightmapIndex, this.lightmapScaleOffset, tex);
484
487
  this._lightmaps.push(rm);
485
-
486
- // if (type === "Mesh") {
487
- // const mat = this.gameObject["material"];
488
- // if (!mat?.isMeshBasicMaterial) {
489
- // if (this._lightmaps.length <= 0) {
490
- // }
491
- // const rm = this._lightmaps[0];
492
- // rm.init(this.lightmapIndex, this.lightmapScaleOffset, tex);
493
- // }
494
- // else {
495
- // if (mat)
496
- // console.warn("Lightmapping is not supported on MeshBasicMaterial", mat.name)
497
- // }
498
- // }
499
- // // for multi materials we need to loop through children
500
- // // and then we add a lightmap renderer component to each of them
501
- // else if (this.isMultiMaterialObject(this.gameObject) && this.sharedMaterials.length > 0) {
502
- // for (let i = 0; i < this.gameObject.children.length; i++) {
503
- // const child = this.gameObject.children[i];
504
- // if (!child["material"]?.isMeshBasicMaterial) {
505
- // let rm: RendererLightmap | undefined = undefined;
506
- // if (i >= this._lightmaps.length) {
507
- // rm = new RendererLightmap(child as Mesh, this.context);
508
- // this._lightmaps.push(rm);
509
- // }
510
- // else
511
- // rm = this._lightmaps[i];
512
- // rm.init(this.lightmapIndex, this.lightmapScaleOffset, tex);
513
- // }
514
- // }
515
- // }
516
488
  }
517
489
  else {
518
- if (debugRenderer) console.warn("Lightmap not found", this.sourceId, this.lightmapIndex);
490
+ if (debugRenderer) console.warn(`[Renderer] No lightmaps found ${this.name} (${this.sourceId}, ${this.lightmapIndex})`);
519
491
  }
520
492
  }
521
493
 
@@ -9,6 +9,10 @@ const debug = getParam("debuglightmaps");
9
9
 
10
10
  declare type MaterialWithLightmap = Material & { lightMap?: Texture | null };
11
11
 
12
+ let cloningCounter = 0;
13
+
14
+ const $lightmapVersion = Symbol("lightmap-material-version");
15
+
12
16
 
13
17
  /**
14
18
  * This component is automatically added by the {@link Renderer} component if the object has lightmap uvs AND we have a lightmap.
@@ -37,6 +41,8 @@ export class RendererLightmap {
37
41
  private lightmapScaleOffset: Vector4 = new Vector4(1, 1, 0, 0);
38
42
 
39
43
  private readonly renderer: Renderer;
44
+ private readonly clonedMaterials = new Array<Material>();
45
+
40
46
  private get context(): Context { return this.renderer.context; }
41
47
  private get gameObject() { return this.renderer.gameObject; }
42
48
  private lightmapTexture: Texture | null = null;
@@ -89,8 +95,7 @@ export class RendererLightmap {
89
95
 
90
96
  const mat = this.renderer.sharedMaterials[i];
91
97
  if (!mat) continue;
92
-
93
- const newMat = this.ensureLightmapMaterial(mat);
98
+ const newMat = this.ensureLightmapMaterial(mat, i);
94
99
  if (mat !== newMat) {
95
100
  this.renderer.sharedMaterials[i] = newMat;
96
101
  }
@@ -118,23 +123,28 @@ export class RendererLightmap {
118
123
  }
119
124
  }
120
125
 
121
- private ensureLightmapMaterial(material: Material) {
126
+ private ensureLightmapMaterial(material: Material, index: number) {
122
127
  if (!material.userData) material.userData = {};
123
128
  // if (material instanceof MeshPhysicalMaterial) {
124
129
  // return material;
125
130
  // }
126
131
  // check if the material version has changed and only then clone the material
127
- if (material["NEEDLE:lightmap-material-version"] != material.version) {
128
- if (material["NEEDLE:lightmap-material-version"] == undefined) {
129
- if (debug) console.warn("Cloning material for lightmap " + material.name);
130
- const mat: Material = material.clone();
131
- if (!mat.name?.includes("(lightmap)")) mat.name = material.name + " (lightmap)";
132
- material = mat;
133
- material.onBeforeCompile = this.onBeforeCompile;
134
- }
135
- else {
136
- // we need to clone the material
132
+ if (this.clonedMaterials[index] !== material) {
133
+ if (debug) {
134
+ ++cloningCounter;
135
+ if (cloningCounter++ < 1000) {
136
+ console.warn(`Cloning material for lightmap ${this.renderer.name}: '${material.name}'`);
137
+ }
138
+ else if (cloningCounter === 1000) {
139
+ console.warn(`Further material cloning for lightmaps suppressed to avoid flooding the console.`);
140
+ }
137
141
  }
142
+ const mat: Material = material.clone();
143
+ if (!mat.name?.includes("(lightmap)")) mat.name = material.name + " (lightmap)";
144
+ material = mat;
145
+ material.onBeforeCompile = this.onBeforeCompile;
146
+ this.clonedMaterials[index] = material;
147
+
138
148
  }
139
149
  return material;
140
150
  }
@@ -144,22 +154,22 @@ export class RendererLightmap {
144
154
  if (material instanceof MeshPhysicalMaterial && material.transmission > 0) {
145
155
  return;
146
156
  }
147
- const hasChanged = material.lightMap !== this.lightmapTexture || material["NEEDLE:lightmap-material-version"] !== material.version;
157
+ const hasChanged = material.lightMap !== this.lightmapTexture || material[$lightmapVersion] !== material.version;
148
158
  if (!hasChanged) {
149
159
  return;
150
160
  }
151
161
 
152
- if (debug) console.log("Assigning lightmap", material.name, material.version, material);
162
+ if (debug) console.log(`Assigning lightmap texture ${this.renderer.name}: '${material.name}' (${material.version} ${material[$lightmapVersion]})`);
153
163
 
154
164
  // assign the lightmap
155
165
  material.lightMap = this.lightmapTexture;
156
166
  material.needsUpdate = true;
157
167
  // store the version of the material
158
- material["NEEDLE:lightmap-material-version"] = material.version;
168
+ material[$lightmapVersion] = material.version;
159
169
  }
160
170
 
161
171
  private onBeforeCompile = (shader: WebGLProgramParametersWithUniforms, _) => {
162
- if (debug) console.log("Lightmaps, before compile\n", shader)
172
+ if (debug === "verbose") console.log("Lightmaps, before compile\n", shader)
163
173
  this.lightmapScaleOffsetUniform.value = this.lightmapScaleOffset;
164
174
  this.lightmapUniform.value = this.lightmapTexture;
165
175
  shader.uniforms.lightmapScaleOffset = this.lightmapScaleOffsetUniform;
@@ -114,8 +114,7 @@ export class PlayableDirector extends Behaviour {
114
114
 
115
115
  /** @internal */
116
116
  awake(): void {
117
- if (debug)
118
- console.log(this, this.playableAsset);
117
+ if (debug) console.log(`[Timeline] Awake '${this.name}'`, this);
119
118
 
120
119
  this.rebuildGraph();
121
120
 
@@ -134,6 +133,8 @@ export class PlayableDirector extends Behaviour {
134
133
 
135
134
  /** @internal */
136
135
  onEnable() {
136
+ if (debug) console.log("[Timeline] OnEnable", this.name, this.playOnAwake);
137
+
137
138
  for (const track of this._audioTracks) {
138
139
  track.onEnable?.();
139
140
  }
@@ -162,6 +163,8 @@ export class PlayableDirector extends Behaviour {
162
163
 
163
164
  /** @internal */
164
165
  onDisable(): void {
166
+ if (debug) console.log("[Timeline] OnDisable", this.name);
167
+
165
168
  this.stop();
166
169
  for (const track of this._audioTracks) {
167
170
  track.onDisable?.();
@@ -360,14 +363,17 @@ export class PlayableDirector extends Behaviour {
360
363
  private readonly _controlTracks: Array<Tracks.ControlTrackHandler> = [];
361
364
  private readonly _customTracks: Array<Tracks.TrackHandler> = [];
362
365
 
363
- private readonly _allTracks: Array<Array<Tracks.TrackHandler>> = [
364
- this._animationTracks,
365
- this._audioTracks,
366
- this._signalTracks,
367
- this._markerTracks,
368
- this._controlTracks,
369
- this._customTracks
370
- ];
366
+ private readonly _tracksArray: Array<Array<Tracks.TrackHandler>> = [];
367
+ private get _allTracks(): Array<Array<Tracks.TrackHandler>> {
368
+ this._tracksArray.length = 0;
369
+ this._tracksArray.push(this._animationTracks);
370
+ this._tracksArray.push(this._audioTracks);
371
+ this._tracksArray.push(this._signalTracks);
372
+ this._tracksArray.push(this._markerTracks);
373
+ this._tracksArray.push(this._controlTracks);
374
+ this._tracksArray.push(this._customTracks);
375
+ return this._tracksArray;
376
+ }
371
377
 
372
378
  /** should be called after evaluate if the director was playing */
373
379
  private invokePauseChangedMethodsOnTracks() {