@needle-tools/engine 2.57.0-pre → 2.58.1-pre

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 (54) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/dist/needle-engine.d.ts +43 -4
  3. package/dist/needle-engine.js +240 -240
  4. package/dist/needle-engine.js.map +4 -4
  5. package/dist/needle-engine.min.js +26 -26
  6. package/dist/needle-engine.min.js.map +4 -4
  7. package/dist/needle-engine.tsbuildinfo +1 -1
  8. package/lib/engine/codegen/register_types.js +2 -0
  9. package/lib/engine/codegen/register_types.js.map +1 -1
  10. package/lib/engine/engine.d.ts +1 -0
  11. package/lib/engine/engine.js +1 -0
  12. package/lib/engine/engine.js.map +1 -1
  13. package/lib/engine/engine_hot_reload.d.ts +3 -0
  14. package/lib/engine/engine_hot_reload.js +168 -0
  15. package/lib/engine/engine_hot_reload.js.map +1 -0
  16. package/lib/engine/engine_input.js +1 -1
  17. package/lib/engine/engine_mainloop_utils.d.ts +3 -0
  18. package/lib/engine/engine_mainloop_utils.js +61 -0
  19. package/lib/engine/engine_mainloop_utils.js.map +1 -1
  20. package/lib/engine/engine_scenetools.js +3 -0
  21. package/lib/engine/engine_scenetools.js.map +1 -1
  22. package/lib/engine/engine_setup.js +42 -36
  23. package/lib/engine/engine_setup.js.map +1 -1
  24. package/lib/engine-components/Fog.d.ts +20 -0
  25. package/lib/engine-components/Fog.js +61 -0
  26. package/lib/engine-components/Fog.js.map +1 -0
  27. package/lib/engine-components/Renderer.d.ts +7 -2
  28. package/lib/engine-components/Renderer.js +62 -2
  29. package/lib/engine-components/Renderer.js.map +1 -1
  30. package/lib/engine-components/codegen/components.d.ts +1 -0
  31. package/lib/engine-components/codegen/components.js +1 -0
  32. package/lib/engine-components/codegen/components.js.map +1 -1
  33. package/lib/engine-components/ui/EventSystem.d.ts +1 -1
  34. package/lib/engine-components/ui/EventSystem.js +25 -10
  35. package/lib/engine-components/ui/EventSystem.js.map +1 -1
  36. package/lib/engine-components/ui/PointerEvents.d.ts +3 -1
  37. package/lib/engine-components/ui/PointerEvents.js +1 -0
  38. package/lib/engine-components/ui/PointerEvents.js.map +1 -1
  39. package/lib/engine-components/ui/Raycaster.js.map +1 -1
  40. package/lib/tsconfig.tsbuildinfo +1 -1
  41. package/package.json +1 -1
  42. package/src/engine/codegen/register_types.js +2 -0
  43. package/src/engine/engine.ts +2 -0
  44. package/src/engine/engine_hot_reload.ts +186 -0
  45. package/src/engine/engine_input.ts +1 -1
  46. package/src/engine/engine_mainloop_utils.ts +61 -1
  47. package/src/engine/engine_scenetools.ts +3 -0
  48. package/src/engine/engine_setup.ts +40 -36
  49. package/src/engine-components/Fog.ts +60 -0
  50. package/src/engine-components/Renderer.ts +59 -2
  51. package/src/engine-components/codegen/components.ts +1 -0
  52. package/src/engine-components/ui/EventSystem.ts +30 -15
  53. package/src/engine-components/ui/PointerEvents.ts +7 -4
  54. package/src/engine-components/ui/Raycaster.ts +6 -3
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@needle-tools/engine",
3
- "version": "2.57.0-pre",
3
+ "version": "2.58.1-pre",
4
4
  "description": "Needle Engine is a web-based runtime for 3D apps. It runs on your machine for development, and can be deployed anywhere. It is flexible, extensible, and collaboration and XR come naturally.",
5
5
  "main": "dist/needle-engine.js",
6
6
  "module": "src/needle-engine.ts",
@@ -55,6 +55,7 @@ import { EventTrigger } from "../../engine-components/EventTrigger.ts";
55
55
  import { FieldWithDefault } from "../../engine-components/Renderer.ts";
56
56
  import { FixedJoint } from "../../engine-components/Joints.ts";
57
57
  import { FlyControls } from "../../engine-components/FlyControls.ts";
58
+ import { Fog } from "../../engine-components/Fog.ts";
58
59
  import { GltfExport } from "../../engine-components/export/gltf/GltfExport.ts";
59
60
  import { GltfExportBox } from "../../engine-components/export/gltf/GltfExport.ts";
60
61
  import { Gradient } from "../../engine-components/ParticleSystemModules.ts";
@@ -225,6 +226,7 @@ TypeStore.add("EventTrigger", EventTrigger);
225
226
  TypeStore.add("FieldWithDefault", FieldWithDefault);
226
227
  TypeStore.add("FixedJoint", FixedJoint);
227
228
  TypeStore.add("FlyControls", FlyControls);
229
+ TypeStore.add("Fog", Fog);
228
230
  TypeStore.add("GltfExport", GltfExport);
229
231
  TypeStore.add("GltfExportBox", GltfExportBox);
230
232
  TypeStore.add("Gradient", Gradient);
@@ -1,3 +1,5 @@
1
+ import "./engine_hot_reload"
2
+
1
3
  import * as layers from "./js-extensions/Layers";
2
4
  layers.patchLayers();
3
5
 
@@ -0,0 +1,186 @@
1
+ import { IComponent } from "./engine_types";
2
+ import { TypeStore } from "./engine_typestore";
3
+ import { addScriptToArrays, removeScriptFromContext } from "./engine_mainloop_utils"
4
+ import { showBalloonWarning } from "./debug/debug";
5
+ import { getParam } from "./engine_utils";
6
+
7
+ const debug = getParam("debughotreload");
8
+
9
+ declare type BeforeUpdateArgs = {
10
+ type: string,
11
+ updates: Array<{ path: string, timestamp: number, acceptedPath: string, explicitImportRequired: boolean, type: string }>,
12
+ }
13
+
14
+ //@ts-ignore
15
+ if (import.meta.hot) {
16
+ //@ts-ignore
17
+ import.meta.hot.on('vite:beforeUpdate', (cb: BeforeUpdateArgs) => {
18
+ if (debug) console.log(cb);
19
+ for (const update of cb.updates) {
20
+ console.log("[Needle Engine] Hot reloading " + update.path);
21
+ }
22
+ });
23
+ }
24
+
25
+
26
+ let isApplyingChanges = false;
27
+
28
+ const instances: Map<string, object[]> = new Map();
29
+
30
+ export function register(instance: object) {
31
+ if (isApplyingChanges) return;
32
+ const type = instance.constructor;
33
+ const name = type.name;
34
+ if (!instances.has(name)) {
35
+ instances.set(name, [instance]);
36
+ }
37
+ else {
38
+ instances.get(name)?.push(instance);
39
+ }
40
+ }
41
+
42
+ export function unregister(instance: object) {
43
+ if (isApplyingChanges) return;
44
+ const type = instance.constructor;
45
+ const name = type.name;
46
+ const instancesOfType = instances.get(name);
47
+ if (!instancesOfType) return;
48
+ const idx = instancesOfType.indexOf(instance);
49
+ if (idx === -1) return;
50
+ instancesOfType.splice(idx, 1);
51
+ }
52
+
53
+
54
+ let didRegisterUnhandledExceptionListener = false;
55
+ function reloadPageOnHotReloadError() {
56
+ if (debug) return;
57
+ if (didRegisterUnhandledExceptionListener) return;
58
+ didRegisterUnhandledExceptionListener = true;
59
+
60
+ const error = console.error;
61
+ console.error = (...args: any[]) => {
62
+ if (args.length) {
63
+ const arg: string = args[0];
64
+ // When making changes in e.g. the engine package and then making changes in project scripts again that import the engine package: hot reload fails and reports redefinitions of types, we just reload the page in those cases for now
65
+ // editing a script in one package seems to work for now so it should be good enough for a start
66
+ if (typeof arg === "string" && arg.includes("[hmr] Failed to reload ")) {
67
+ console.log("[Needle Engine] Hot reloading failed")
68
+ window.location.reload();
69
+ return;
70
+ }
71
+
72
+ }
73
+ error.apply(console, args);
74
+ };
75
+ }
76
+
77
+
78
+ export function applyChanges(newModule): boolean {
79
+
80
+ reloadPageOnHotReloadError();
81
+
82
+ // showBalloonMessage("Hot reloading");
83
+
84
+ // console.dir(newModule);
85
+
86
+ for (const key of Object.keys(newModule)) {
87
+ try {
88
+ isApplyingChanges = true;
89
+
90
+ const typeToUpdate = TypeStore.get(key);
91
+ if (!typeToUpdate)
92
+ {
93
+ continue;
94
+ }
95
+ const newType = newModule[key];
96
+
97
+ console.log("[Needle Engine] Updating type: " + key);
98
+
99
+ // Update prototype (methods and properties)
100
+ const previousMethods = Object.getOwnPropertyNames(typeToUpdate.prototype);
101
+ const methodsAndProperties = Object.getOwnPropertyDescriptors(newType.prototype);
102
+ for (const typeKey in methodsAndProperties) {
103
+ const desc = methodsAndProperties[typeKey];
104
+ if (!desc.writable) continue;
105
+ typeToUpdate.prototype[typeKey] = newModule[key].prototype[typeKey];
106
+ }
107
+ // Remove methods that are no longer present
108
+ for (const typeKey of previousMethods) {
109
+ if (!methodsAndProperties[typeKey]) {
110
+ delete typeToUpdate.prototype[typeKey];
111
+ }
112
+ }
113
+
114
+ // Update fields (we only add new fields if they are undefined)
115
+ // we create a instance to get access to the fields
116
+ const instancesOfType = instances.get(newType.name);
117
+ if (instancesOfType) {
118
+ const newTypeInstance = new newType();
119
+ const keys = Object.getOwnPropertyDescriptors(newTypeInstance);
120
+ for (const inst of instancesOfType) {
121
+ const componentInstance = inst as unknown as IComponent;
122
+ const isComponent = componentInstance.isComponent === true;
123
+ const active = isComponent ? componentInstance.activeAndEnabled : true;
124
+ const context = isComponent ? componentInstance.context : undefined;
125
+ try {
126
+ if (isComponent) {
127
+ removeScriptFromContext(componentInstance, context);
128
+ }
129
+ if (isComponent && active) {
130
+ componentInstance.enabled = false;
131
+ }
132
+
133
+ if (inst["onBeforeHotReloadFields"]) {
134
+ const res = inst["onBeforeHotReloadFields"]();
135
+ if (res === false) continue;
136
+ }
137
+ for (const key in keys) {
138
+ const desc = keys[key];
139
+ if (!desc.writable) continue;
140
+ if (inst[key] === undefined) {
141
+ inst[key] = newTypeInstance[key];
142
+ }
143
+ // if its a function but not on the prototype
144
+ // then its a bound method that needs to be rebound
145
+ else if (typeof inst[key] === "function" && !inst[key].prototype) {
146
+ const boundMethod = inst[key];
147
+ // try to get the target method name
148
+ const targetMethodName = boundMethod.name;
149
+ const prefix = "bound "; // < magic prefix
150
+ if (targetMethodName === prefix) continue;
151
+ const name = boundMethod.name.substring(prefix.length);
152
+ // if the target method name still exists on the new prototype
153
+ // we want to rebind it and assign it to the field
154
+ // Beware that this will not work if the method is added to some event listener etc
155
+ const newTarget = newType.prototype[name];
156
+ if (newTarget)
157
+ inst[key] = newTarget.bind(inst);
158
+ }
159
+ }
160
+ if (inst["onAfterHotReloadFields"]) inst["onAfterHotReloadFields"]();
161
+ }
162
+ finally {
163
+ if (isComponent) {
164
+ addScriptToArrays(componentInstance, context);
165
+ }
166
+ if (isComponent && active) {
167
+ componentInstance.enabled = true;
168
+ }
169
+ }
170
+ }
171
+ }
172
+ }
173
+ catch (err) {
174
+ if (debug) console.error(err);
175
+ // we only want to invalidate changes if we debug hot reload
176
+ else return false;
177
+ }
178
+ finally {
179
+ isApplyingChanges = false;
180
+ }
181
+ }
182
+
183
+ return true;
184
+ }
185
+
186
+
@@ -170,7 +170,7 @@ export class Input extends EventTarget {
170
170
  *foreachPointerId(pointerType?: string | PointerType | string[] | PointerType[]): Generator<number> {
171
171
  for (let i = 0; i < this._pointerTypes.length; i++) {
172
172
  // check if the pointer is active
173
- if (this._pointerIsActive[i]) {
173
+ if (this._pointerIsActive(i)) {
174
174
  // if specific pointer types are requested
175
175
  if (pointerType !== undefined) {
176
176
  const type = this._pointerTypes[i];
@@ -2,7 +2,7 @@ import { Context } from './engine_setup';
2
2
  import * as utils from "./engine_generic_utils";
3
3
  import * as constants from "./engine_constants";
4
4
  import { getParam } from './engine_utils';
5
- import { Object3D } from 'three';
5
+ import { CubeCamera, Object3D, WebGLCubeRenderTarget } from 'three';
6
6
  import { IComponent } from './engine_types';
7
7
  import { isActiveSelf, setActive } from './engine_gameobject';
8
8
 
@@ -83,6 +83,8 @@ export function processNewScripts(context: Context) {
83
83
  updateActiveInHierarchyWithoutEventCall(script.gameObject);
84
84
  if (script.activeAndEnabled)
85
85
  utils.safeInvoke(script.__internalAwake.bind(script));
86
+
87
+ // registerPrewarmObject(script.gameObject, context);
86
88
  }
87
89
  }
88
90
  catch (err) {
@@ -320,4 +322,62 @@ function perComponent(go: THREE.Object3D, evt: (comp: IComponent) => void) {
320
322
  evt(comp);
321
323
  }
322
324
  }
325
+ }
326
+
327
+
328
+ const prewarmList: Map<Context, Object3D[]> = new Map();
329
+ const $prewarmedFlag = Symbol("prewarmFlag");
330
+ const $waitingForPrewarm = Symbol("waitingForPrewarm");
331
+ const debugPrewarm = getParam("debugprewarm");
332
+
333
+ export function registerPrewarmObject(obj: Object3D, context: Context) {
334
+ if (!obj) return;
335
+ // allow objects to be marked as prewarmed in which case we dont need to register them again
336
+ if (obj[$prewarmedFlag] === true) return;
337
+ if (obj[$waitingForPrewarm] === true) return;
338
+ if (!prewarmList.has(context)) {
339
+ prewarmList.set(context, []);
340
+ }
341
+ obj[$waitingForPrewarm] = true;
342
+ const list = prewarmList.get(context);
343
+ list!.push(obj);
344
+ if(debugPrewarm) console.debug("register prewarm", obj.name);
345
+ }
346
+
347
+ let prewarmTarget: WebGLCubeRenderTarget | null = null;
348
+ let prewarmCamera: CubeCamera | null = null;
349
+
350
+ // called by the engine to remove scroll or animation hiccup when objects are rendered/compiled for the first time
351
+ export function runPrewarm(context: Context) {
352
+ if (!context) return;
353
+ const list = prewarmList.get(context);
354
+ if (!list?.length) return;
355
+
356
+ const cam = context.mainCamera;
357
+ if (cam) {
358
+ if(debugPrewarm) console.log("prewarm", list.length, "objects", [...list]);
359
+ const renderer = context.renderer;
360
+ const scene = context.scene;
361
+ renderer.compile(scene, cam!)
362
+ prewarmTarget ??= new WebGLCubeRenderTarget(64)
363
+ prewarmCamera ??= new CubeCamera(0.001, 9999999, prewarmTarget);
364
+ prewarmCamera.update(renderer, scene);
365
+ for (const obj of list) {
366
+ obj[$prewarmedFlag] = true;
367
+ obj[$waitingForPrewarm] = false;
368
+ }
369
+ list.length = 0;
370
+ if(debugPrewarm) console.log("prewarm done");
371
+ }
372
+ }
373
+
374
+ export function clearPrewarmList(context: Context) {
375
+ const list = prewarmList.get(context);
376
+ if (list) {
377
+ for (const obj of list) {
378
+ obj[$waitingForPrewarm] = false;
379
+ }
380
+ list.length = 0;
381
+ }
382
+ prewarmList.delete(context);
323
383
  }
@@ -12,6 +12,7 @@ import { createBuiltinComponents, writeBuiltinComponentData } from "./engine_glt
12
12
  import { SerializationContext } from "./engine_serialization_core";
13
13
  import { NEEDLE_components } from "./extensions/NEEDLE_components";
14
14
  import { addNewComponent, getComponentInChildren } from "./engine_components";
15
+ import { registerPrewarmObject } from "./engine_mainloop_utils";
15
16
 
16
17
 
17
18
  export class NeedleGltfLoader implements INeedleGltfLoader {
@@ -116,6 +117,7 @@ export function parseSync(context: Context, data, path: string, seed: number | U
116
117
  invokeEvents(GltfLoadEventType.AfterLoaded, new GltfLoadEvent(context, path, loader, data));
117
118
  await handleLoadedGltf(context, path, data, seed, componentsExtension);
118
119
  invokeEvents(GltfLoadEventType.FinishedSetup, new GltfLoadEvent(context, path, loader, data));
120
+ registerPrewarmObject(data.scene, context);
119
121
  resolve(data);
120
122
 
121
123
  }, err => {
@@ -146,6 +148,7 @@ export function loadSync(context: Context, url: string, seed: number | UIDProvid
146
148
  invokeEvents(GltfLoadEventType.AfterLoaded, new GltfLoadEvent(context, url, loader, data));
147
149
  await handleLoadedGltf(context, url, data, seed, componentsExtension);
148
150
  invokeEvents(GltfLoadEventType.FinishedSetup, new GltfLoadEvent(context, url, loader, data));
151
+ registerPrewarmObject(data.scene, context);
149
152
  resolve(data);
150
153
 
151
154
  }, evt => {
@@ -701,6 +701,7 @@ export class Context {
701
701
 
702
702
 
703
703
  if (!this.isManagedExternally) {
704
+ looputils.runPrewarm(this);
704
705
  this._currentFrameEvent = -10;
705
706
  this.renderNow();
706
707
  this._currentFrameEvent = FrameEvent.OnAfterRender;
@@ -809,47 +810,50 @@ export class Context {
809
810
  if (this.coroutines[evt]) {
810
811
  const evts = this.coroutines[evt];
811
812
  for (let i = 0; i < evts.length; i++) {
812
- const evt = evts[i];
813
- // TODO we might want to keep coroutines playing even if the component is disabled or inactive
814
- const remove = !evt.comp || evt.comp.destroyed || !evt.main || evt.comp["enabled"] === false;
815
- if (remove) {
816
- evts.splice(i, 1);
817
- --i;
818
- continue;
819
- }
820
- const iter = evt.chained;
821
- if (iter && iter.length > 0) {
822
- const last: Generator = iter[iter.length - 1];
823
- const res = last.next();
824
- if (res.done) {
825
- iter.pop();
813
+ try {
814
+ const evt = evts[i];
815
+ // TODO we might want to keep coroutines playing even if the component is disabled or inactive
816
+ const remove = !evt.comp || evt.comp.destroyed || !evt.main || evt.comp["enabled"] === false;
817
+ if (remove) {
818
+ evts.splice(i, 1);
819
+ --i;
820
+ continue;
826
821
  }
827
- if (isGenerator(res)) {
828
- if (!evt.chained) evt.chained = [];
829
- evt.chained.push(res.value);
822
+ const iter = evt.chained;
823
+ if (iter && iter.length > 0) {
824
+ const last: Generator = iter[iter.length - 1];
825
+ const res = last.next();
826
+ if (res.done) {
827
+ iter.pop();
828
+ }
829
+ if (isGenerator(res)) {
830
+ if (!evt.chained) evt.chained = [];
831
+ evt.chained.push(res.value);
832
+ }
833
+ if (!res.done) continue;
830
834
  }
831
- if (!res.done) continue;
832
- }
833
835
 
834
- const res = evt.main.next();
835
- if (res.done === true) {
836
- evts.splice(i, 1);
837
- --i;
838
- continue;
836
+ const res = evt.main.next();
837
+ if (res.done === true) {
838
+ evts.splice(i, 1);
839
+ --i;
840
+ continue;
841
+ }
842
+ const val = res.value;
843
+ if (isGenerator(val)) {
844
+ // invoke once if its a generator
845
+ // this means e.g. WaitForFrame(1) works and will capture
846
+ // the frame it was created
847
+ const gen = val as Generator;
848
+ const res = gen.next();
849
+ if (res.done) continue;
850
+ if (!evt.chained) evt.chained = [];
851
+ evt.chained.push(val as Generator);
852
+ }
839
853
  }
840
- const val = res.value;
841
- if (isGenerator(val)) {
842
- // invoke once if its a generator
843
- // this means e.g. WaitForFrame(1) works and will capture
844
- // the frame it was created
845
- const gen = val as Generator;
846
- const res = gen.next();
847
- if (res.done) continue;
848
- if (!evt.chained) evt.chained = [];
849
- evt.chained.push(val as Generator);
854
+ catch (e) {
855
+ console.error(e);
850
856
  }
851
-
852
-
853
857
  }
854
858
  }
855
859
 
@@ -0,0 +1,60 @@
1
+ import { Behaviour } from "./Component";
2
+ import { Color, Fog as Fog3 } from "three";
3
+ import { serializable } from "../engine/engine_serialization";
4
+
5
+
6
+ export enum FogMode {
7
+
8
+ Linear = 1,
9
+ Exponential = 2,
10
+ ExponentialSquared = 3,
11
+ }
12
+
13
+ export class Fog extends Behaviour {
14
+
15
+ get fog() {
16
+ if (!this._fog) this._fog = new Fog3(0x000000, 0, 50);
17
+ return this._fog;
18
+ }
19
+
20
+ get mode() {
21
+ return FogMode.Linear;
22
+ }
23
+
24
+ @serializable()
25
+ set near(value: number) {
26
+ this.fog.near = value;
27
+ }
28
+ get near() {
29
+ return this.fog.near;
30
+ }
31
+
32
+ @serializable()
33
+ set far(value: number) {
34
+ this.fog.far = value;
35
+ }
36
+ get far() {
37
+ return this.fog.far;
38
+ }
39
+
40
+ @serializable(Color)
41
+ set color(value: Color) {
42
+ this.fog.color.copy(value);
43
+ }
44
+ get color() {
45
+ return this.fog.color;
46
+ }
47
+
48
+ private _fog?: Fog3;
49
+
50
+ onEnable() {
51
+ this.scene.fog = this.fog;
52
+ }
53
+
54
+ onDisable() {
55
+ if (this.scene.fog === this._fog)
56
+ this.scene.fog = null;
57
+ }
58
+
59
+
60
+ }
@@ -12,6 +12,8 @@ import { NEED_UPDATE_INSTANCE_KEY } from "../engine/engine_instancing";
12
12
  import { IRenderer, ISharedMaterials } from "../engine/engine_types";
13
13
  import { debug, ReflectionProbe } from "./ReflectionProbe";
14
14
  import { setCustomVisibility } from "../engine/js-extensions/Layers";
15
+ import { isLocalNetwork } from "../engine/engine_networking_utils";
16
+ import { showBalloonWarning } from "../engine/debug/debug";
15
17
 
16
18
  // for staying compatible with old code
17
19
  export { InstancingUtil } from "../engine/engine_instancing";
@@ -53,12 +55,14 @@ class SharedMaterialArray implements ISharedMaterials {
53
55
  private _renderer: Renderer;
54
56
  private _targets: THREE.Object3D[] = [];
55
57
 
58
+ private _indexMapMaxIndex?: number;
59
+ private _indexMap?: Map<number, number>;
56
60
 
57
61
  is(renderer: Renderer) {
58
62
  return this._renderer === renderer;
59
63
  }
60
64
 
61
- constructor(renderer: Renderer) {
65
+ constructor(renderer: Renderer, originalMaterials: Material[]) {
62
66
  this._renderer = renderer;
63
67
  const setMaterial = this.setMaterial.bind(this);
64
68
  const getMaterial = this.getMaterial.bind(this);
@@ -76,6 +80,38 @@ class SharedMaterialArray implements ISharedMaterials {
76
80
  }
77
81
  }
78
82
 
83
+ // this is useful to have an index map when e.g. materials are trying to be assigned by index
84
+ let hasMissingMaterials = false;
85
+ let indexMap: Map<number, number> | undefined = undefined;
86
+ let maxIndex: number = 0;
87
+ for (let i = 0; i < this._targets.length; i++) {
88
+ const target = this._targets[i] as Mesh;
89
+ if (!target) continue;
90
+ const mat = target.material as Material;
91
+ if (!mat) continue;
92
+ for (let k = 0; k < originalMaterials.length; k++) {
93
+ const orig = originalMaterials[k];
94
+ if (!orig) {
95
+ hasMissingMaterials = true;
96
+ continue;
97
+ }
98
+ if (mat.name === orig.name) {
99
+ if (indexMap === undefined) indexMap = new Map();
100
+ indexMap.set(k, i);
101
+ maxIndex = Math.max(maxIndex, k);
102
+ // console.log(`Material ${mat.name} at ${k} was found at index ${i} in renderer ${renderer.name}.`)
103
+ break;
104
+ }
105
+ }
106
+ }
107
+ if (hasMissingMaterials) {
108
+ this._indexMapMaxIndex = maxIndex;
109
+ this._indexMap = indexMap;
110
+ const warningMessage = `Renderer ${renderer.name} was initialized with missing materials - this may lead to unexpected behaviour when trying to access sharedMaterials by index.`;
111
+ console.warn(warningMessage);
112
+ if(isLocalNetwork()) showBalloonWarning("Found renderer with missing materials: please check the console for details.");
113
+ }
114
+
79
115
  // this lets us override the javascript indexer, only works in ES6 tho
80
116
  // but like that we can use sharedMaterials[index] and it will be assigned to the object directly
81
117
  return new Proxy(this, {
@@ -98,10 +134,22 @@ class SharedMaterialArray implements ISharedMaterials {
98
134
  }
99
135
 
100
136
  get length(): number {
137
+ if (this._indexMapMaxIndex !== undefined) return this._indexMapMaxIndex + 1;
101
138
  return this._targets.length;
102
139
  }
103
140
 
141
+ private resolveIndex(index: number): number {
142
+ const map = this._indexMap;
143
+ // if we have a index map it means that some materials were missing
144
+ if (map) {
145
+ if (map.has(index)) return map.get(index) as number;
146
+ // return -1;
147
+ }
148
+ return index;
149
+ }
150
+
104
151
  private setMaterial(mat: Material, index: number) {
152
+ index = this.resolveIndex(index);
105
153
  if (index < 0 || index >= this._targets.length) return;
106
154
  const target = this._targets[index];
107
155
  if (!target || target["material"] === undefined) return;
@@ -109,6 +157,7 @@ class SharedMaterialArray implements ISharedMaterials {
109
157
  }
110
158
 
111
159
  private getMaterial(index: number) {
160
+ index = this.resolveIndex(index);
112
161
  if (index < 0) return null;
113
162
  const obj = this._targets;
114
163
  if (index >= obj.length) return null;
@@ -193,10 +242,18 @@ export class Renderer extends Behaviour implements IRenderer {
193
242
  }
194
243
 
195
244
  private _sharedMaterials!: SharedMaterialArray;
245
+ private _originalMaterials: Material[] = [];
246
+
247
+ // this is just available during deserialization
248
+ private set sharedMaterials(_val: Array<Material | null>) {
249
+ // TODO: elements in the array might be missing at the moment which leads to problems if an index is serialized
250
+ this._originalMaterials = _val as Material[];
251
+ }
196
252
 
253
+ //@ts-ignore
197
254
  get sharedMaterials(): SharedMaterialArray {
198
255
  if (!this._sharedMaterials || !this._sharedMaterials.is(this))
199
- this._sharedMaterials = new SharedMaterialArray(this);
256
+ this._sharedMaterials = new SharedMaterialArray(this, this._originalMaterials);
200
257
  return this._sharedMaterials!;
201
258
  }
202
259
 
@@ -53,6 +53,7 @@ export { EventTrigger } from "../EventTrigger";
53
53
  export { FieldWithDefault } from "../Renderer";
54
54
  export { FixedJoint } from "../Joints";
55
55
  export { FlyControls } from "../FlyControls";
56
+ export { Fog } from "../Fog";
56
57
  export { GltfExport } from "../export/gltf/GltfExport";
57
58
  export { GltfExportBox } from "../export/gltf/GltfExport";
58
59
  export { Gradient } from "../ParticleSystemModules";