Needle Engine

Changes between version 3.6.0-alpha.4 and 3.6.0-beta
Files changed (23) hide show
  1. plugins/vite/index.js +1 -1
  2. plugins/vite/poster.js +22 -3
  3. src/engine/codegen/register_types.js +2 -0
  4. src/engine-components/codegen/components.ts +1 -0
  5. src/engine/engine_addressables.ts +15 -1
  6. src/engine/engine_assetdatabase.ts +58 -8
  7. src/engine/engine_context_registry.ts +4 -0
  8. src/engine/engine_context.ts +49 -32
  9. src/engine/engine_element.ts +18 -17
  10. src/engine/engine_gameobject.ts +7 -1
  11. src/engine/engine_lightdata.ts +5 -0
  12. src/engine/engine_physics_rapier.ts +2 -0
  13. src/engine/engine_physics.ts +3 -0
  14. src/engine-components/postprocessing/index.ts +2 -1
  15. src/engine-components/OrbitControls.ts +7 -2
  16. src/engine-components/postprocessing/PostProcessingEffect.ts +35 -3
  17. src/engine-components/Skybox.ts +12 -5
  18. src/engine-components/timeline/TimelineTracks.ts +15 -5
  19. plugins/types/userconfig.d.ts +9 -0
  20. src/engine-components/postprocessing/Volume.ts +8 -2
  21. src/engine-components/postprocessing/VolumeParameter.ts +1 -0
  22. src/engine-components/postprocessing/VolumeProfile.ts +1 -1
  23. src/engine-components/postprocessing/Effects/ScreenspaceAmbientOcclusionN8.ts +99 -0
plugins/vite/index.js CHANGED
@@ -58,7 +58,7 @@
58
58
  needleLicense(command, config, userSettings),
59
59
  needleViteAlias(command, config, userSettings),
60
60
  needleMeta(command, config, userSettings),
61
- needlePoster(command),
61
+ needlePoster(command, config, userSettings),
62
62
  needleReload(command, config, userSettings),
63
63
  needleBuild(command, config, userSettings),
64
64
  needleCopyFiles(command, config, userSettings),
plugins/vite/poster.js CHANGED
@@ -9,18 +9,31 @@
9
9
  return "include/poster.webp";
10
10
  }
11
11
 
12
- export const needlePoster = (command) => {
12
+ /**
13
+ * @param {import('../types').userSettings} userSettings
14
+ */
15
+ export const needlePoster = (command, config, userSettings) => {
13
16
  // only relevant for local development
14
17
  if (command === 'build') return [];
15
18
 
19
+ if (userSettings.noPoster) return;
20
+
16
21
  return {
17
22
  name: 'needle-poster',
18
23
  configureServer(server) {
19
24
  server.ws.on('needle:screenshot', async (data, client) => {
20
- if(!data?.data){
25
+ if (userSettings.noPoster) return;
26
+ if (!data?.data) {
21
27
  console.warn("Received empty screenshot data, ignoring");
22
28
  return;
23
29
  }
30
+
31
+ if (userSettings.posterGenerationMode === "once") {
32
+ if (fs.existsSync(getPosterPath())) {
33
+ console.log("Poster already exists, ignoring screenshot");
34
+ return;
35
+ }
36
+ }
24
37
  const targetPath = "./" + getPosterPath();
25
38
  console.log("Received poster, saving to " + targetPath);
26
39
  // remove data:image/png;base64, from the beginning of the string
@@ -39,13 +52,19 @@
39
52
  enforce: 'pre',
40
53
  transform(html, ctx) {
41
54
  const file = path.join(__dirname, 'poster-client.js');
55
+ let scriptContent = fs.readFileSync(file, 'utf8');
56
+ switch (userSettings.posterFormat) {
57
+ case "image/png":
58
+ scriptContent = scriptContent.replace("image/webp", "image/png");
59
+ break;
60
+ }
42
61
  return [
43
62
  {
44
63
  tag: 'script',
45
64
  attrs: {
46
65
  type: 'module',
47
66
  },
48
- children: fs.readFileSync(file, 'utf8'),
67
+ children: scriptContent,
49
68
  injectTo: 'body',
50
69
  },
51
70
  ];
src/engine/codegen/register_types.js CHANGED
@@ -143,6 +143,7 @@
143
143
  import { SceneSwitcher } from "../../engine-components/SceneSwitcher";
144
144
  import { ScreenCapture } from "../../engine-components/ScreenCapture";
145
145
  import { ScreenSpaceAmbientOcclusion } from "../../engine-components/postprocessing/Effects/ScreenspaceAmbientOcclusion";
146
+ import { ScreenSpaceAmbientOcclusionN8 } from "../../engine-components/postprocessing/Effects/ScreenspaceAmbientOcclusionN8";
146
147
  import { SetActiveOnClick } from "../../engine-components/export/usdz/extensions/behavior/BehaviourComponents";
147
148
  import { ShadowCatcher } from "../../engine-components/ShadowCatcher";
148
149
  import { ShapeModule } from "../../engine-components/ParticleSystemModules";
@@ -359,6 +360,7 @@
359
360
  TypeStore.add("SceneSwitcher", SceneSwitcher);
360
361
  TypeStore.add("ScreenCapture", ScreenCapture);
361
362
  TypeStore.add("ScreenSpaceAmbientOcclusion", ScreenSpaceAmbientOcclusion);
363
+ TypeStore.add("ScreenSpaceAmbientOcclusionN8", ScreenSpaceAmbientOcclusionN8);
362
364
  TypeStore.add("SetActiveOnClick", SetActiveOnClick);
363
365
  TypeStore.add("ShadowCatcher", ShadowCatcher);
364
366
  TypeStore.add("ShapeModule", ShapeModule);
src/engine-components/codegen/components.ts CHANGED
@@ -138,6 +138,7 @@
138
138
  export { SceneSwitcher } from "../SceneSwitcher";
139
139
  export { ScreenCapture } from "../ScreenCapture";
140
140
  export { ScreenSpaceAmbientOcclusion } from "../postprocessing/Effects/ScreenspaceAmbientOcclusion";
141
+ export { ScreenSpaceAmbientOcclusionN8 } from "../postprocessing/Effects/ScreenspaceAmbientOcclusionN8";
141
142
  export { SetActiveOnClick } from "../export/usdz/extensions/behavior/BehaviourComponents";
142
143
  export { ShadowCatcher } from "../ShadowCatcher";
143
144
  export { ShapeModule } from "../ParticleSystemModules";
src/engine/engine_addressables.ts CHANGED
@@ -42,6 +42,15 @@
42
42
  }
43
43
  return ref;
44
44
  }
45
+
46
+ clear() {
47
+ this._assetReferences = {};
48
+ }
49
+
50
+ unregisterAssetReference(ref: AssetReference) {
51
+ if (!ref.uri) return;
52
+ delete this._assetReferences[ref.uri];
53
+ }
45
54
  }
46
55
 
47
56
  export type ProgressCallback = (asset: AssetReference, prog: ProgressEvent) => void;
@@ -122,10 +131,15 @@
122
131
  // TODO: we need a way to remove objects from the context (including components) without actually "destroying" them
123
132
  if (this.asset.scene)
124
133
  destroy(this.asset.scene, true, true);
125
- else destroy(this.asset, true, true);
134
+ destroy(this.asset, true, true);
126
135
  }
127
136
  this.asset = null;
128
137
  this._rawBinary = undefined;
138
+ this._glbRoot = null;
139
+ this._loading = undefined;
140
+ if (Context.Current) {
141
+ Context.Current.addressables.unregisterAssetReference(this);
142
+ }
129
143
  }
130
144
 
131
145
  async preload(): Promise<ArrayBuffer | null> {
src/engine/engine_assetdatabase.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { InternalUsageTrackerPlugin } from "./extensions/usage_tracker";
2
- import { Bone, BufferAttribute, BufferGeometry, InterleavedBuffer, InterleavedBufferAttribute, Material, Mesh, Object3D, Skeleton, SkinnedMesh, Texture, WebGLRenderer } from "three";
2
+ import { Bone, BufferAttribute, BufferGeometry, InterleavedBuffer, InterleavedBufferAttribute, Material, Mesh, Object3D, Scene, Skeleton, SkinnedMesh, Source, Texture, Uniform, WebGLRenderer } from "three";
3
3
  import { addPatch } from "./engine_patcher";
4
4
  import { getParam } from "./engine_utils";
5
5
 
@@ -36,31 +36,61 @@
36
36
  return allowUsageTracking;
37
37
  }
38
38
 
39
+ const $disposable = Symbol("disposable");
40
+ export function setDisposable(obj: object | null | undefined, disposable: boolean) {
41
+ if (!obj) return;
42
+ obj[$disposable] = disposable;
43
+ if(debug) console.warn("Set disposable", disposable, obj);
44
+ }
39
45
 
40
46
  /** Recursive disposes all referenced resources by this object. Does not traverse children */
41
- export function disposeObjectResources(obj: object) {
47
+ export function disposeObjectResources(obj: object | null | undefined) {
42
48
  if (!obj) return;
49
+ if (obj[$disposable] === false) {
50
+ if(debug) console.warn("Object is marked as not disposable", obj);
51
+ return;
52
+ }
43
53
 
44
- if (obj instanceof SkinnedMesh) {
54
+ if (obj instanceof Scene) {
55
+ disposeObjectResources(obj.environment);
56
+ disposeObjectResources(obj.background);
57
+ disposeObjectResources(obj.customDepthMaterial);
58
+ disposeObjectResources(obj.customDistanceMaterial);
59
+ }
60
+ else if (obj instanceof SkinnedMesh) {
45
61
  disposeObjectResources(obj.geometry);
46
62
  disposeObjectResources(obj.material);
47
63
  disposeObjectResources(obj.skeleton);
64
+ disposeObjectResources(obj.bindMatrix);
65
+ disposeObjectResources(obj.bindMatrixInverse);
66
+ disposeObjectResources(obj.customDepthMaterial);
67
+ disposeObjectResources(obj.customDistanceMaterial);
68
+ obj.geometry = null;
69
+ obj.material = null;
48
70
  }
49
71
  else if (obj instanceof Mesh) {
50
72
  disposeObjectResources(obj.geometry);
51
73
  disposeObjectResources(obj.material);
74
+ disposeObjectResources(obj.customDepthMaterial);
75
+ disposeObjectResources(obj.customDistanceMaterial);
76
+ obj.geometry = null;
77
+ obj.material = null;
52
78
  }
53
79
  else if (obj instanceof BufferGeometry) {
54
80
  free(obj);
55
81
  for (const key of Object.keys(obj.attributes)) {
56
82
  const value = obj.attributes[key];
57
83
  disposeObjectResources(value);
84
+ // deleting the attribute might lead to errors when raycasting
85
+ // obj.deleteAttribute(key);
58
86
  }
59
87
  }
60
88
  else if (obj instanceof BufferAttribute || obj instanceof InterleavedBufferAttribute) {
61
89
  // Currently not supported by three
62
90
  // https://github.com/mrdoob/three.js/issues/15261
63
91
  // https://github.com/mrdoob/three.js/pull/17063#issuecomment-737993363
92
+ if (debug)
93
+ console.warn("BufferAttribute dispose not supported", obj.count);
64
94
  }
65
95
  else if (obj instanceof Array<Material>) {
66
96
  for (const entry of obj) {
@@ -69,24 +99,41 @@
69
99
  }
70
100
  }
71
101
  else if (obj instanceof Material) {
102
+ free(obj);
72
103
  for (const key of Object.keys(obj)) {
73
104
  const value = obj[key];
74
- if (value instanceof Texture)
105
+ if (value instanceof Texture) {
75
106
  disposeObjectResources(value);
107
+ obj[key] = null;
108
+ }
76
109
  }
77
- free(obj);
110
+ const uniforms = obj["uniforms"];
111
+ if (uniforms) {
112
+ for (const key of Object.keys(uniforms)) {
113
+ const value = uniforms[key];
114
+ if (value instanceof Texture) {
115
+ disposeObjectResources(value);
116
+ uniforms[key] = null;
117
+ }
118
+ else if (value instanceof Uniform) {
119
+ disposeObjectResources(value.value);
120
+ value.value = null;
121
+ }
122
+ }
123
+ }
78
124
  }
79
125
  else if (obj instanceof Texture) {
80
126
  free(obj);
127
+ free(obj.source);
81
128
  if (obj.source?.data instanceof ImageBitmap) {
82
129
  free(obj.source.data);
83
130
  }
84
131
  }
85
132
  else if (obj instanceof Skeleton) {
86
133
  free(obj.boneTexture);
134
+ obj.boneTexture = null;
87
135
  }
88
136
  else if (obj instanceof Bone) {
89
-
90
137
  }
91
138
  else {
92
139
  if (!(obj instanceof Object3D) && debug)
@@ -95,13 +142,16 @@
95
142
  }
96
143
 
97
144
  function free(obj: any) {
98
- if (debug || autoDispose() || trackUsageParam) console.warn("🧨 FREE", obj);
99
145
  if (!obj) {
100
146
  return;
101
147
  }
148
+ if (debug || autoDispose() || trackUsageParam) console.warn("🧨 FREE", obj);
102
149
  if (obj instanceof ImageBitmap) {
103
150
  obj.close();
104
151
  }
152
+ else if (obj instanceof Source) {
153
+ obj.data = null;
154
+ }
105
155
  else {
106
156
  obj.dispose();
107
157
  }
@@ -153,7 +203,7 @@
153
203
 
154
204
 
155
205
 
156
- const debug = getParam("debugusers");
206
+ const debug = getParam("debugusers") || getParam("debugmemory");
157
207
 
158
208
  // Should we check if the type has the
159
209
  const $objectUsersKey = Symbol("needle-users");
src/engine/engine_context_registry.ts CHANGED
@@ -8,6 +8,10 @@
8
8
  ContextCreated = "ContextCreated",
9
9
  ContextDestroyed = "ContextDestroyed",
10
10
  MissingCamera = "MissingCamera",
11
+ /** Called before the context is being cleared */
12
+ ContextClearing = "ContextClearing",
13
+ /** Called after the context has been cleared */
14
+ ContextCleared = "ContextCleared",
11
15
  }
12
16
 
13
17
  export type ContextEventArgs = {
src/engine/engine_context.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import {
2
- BufferGeometry, Camera, Clock, Color, DepthTexture, Group,
2
+ BufferGeometry, Cache, Camera, Clock, Color, DepthTexture, Group,
3
3
  Material, NearestFilter, NoToneMapping, Object3D, PCFSoftShadowMap,
4
4
  PerspectiveCamera, RGBAFormat, Scene, sRGBEncoding,
5
5
  Texture, WebGLRenderer, WebGLRendererParameters, WebGLRenderTarget
@@ -188,7 +188,7 @@
188
188
  private _currentFrameEvent: FrameEvent = FrameEvent.Undefined;
189
189
 
190
190
  scene: Scene;
191
- renderer: WebGLRenderer;
191
+ renderer!: WebGLRenderer;
192
192
  composer: EffectComposer | null = null;
193
193
 
194
194
  // all scripts
@@ -260,30 +260,7 @@
260
260
  this.isManagedExternally = true;
261
261
  }
262
262
  else {
263
- const params: WebGLRendererParameters = {
264
- antialias: true,
265
- };
266
-
267
- // get canvas already configured in the Needle Engine Web Component
268
- const canvas = args?.domElement?.shadowRoot?.querySelector("canvas");
269
- if (canvas) params.canvas = canvas;
270
-
271
- this.renderer = new WebGLRenderer(params);
272
-
273
- // some tonemapping other than "NONE" is required for adjusting exposure with EXR environments
274
- this.renderer.toneMappingExposure = 1; // range [0...inf] instead of the usual -15..15
275
- this.renderer.toneMapping = NoToneMapping; // could also set to LinearToneMapping, ACESFilmicToneMapping
276
-
277
- this.renderer.setClearColor(new Color('lightgrey'), 0);
278
- // @ts-ignore
279
- this.renderer.antialias = true;
280
- // @ts-ignore
281
- this.renderer.alpha = false;
282
- this.renderer.shadowMap.enabled = true;
283
- this.renderer.shadowMap.type = PCFSoftShadowMap;
284
- this.renderer.setSize(this.domWidth, this.domHeight);
285
- this.renderer.outputEncoding = sRGBEncoding;
286
- this.renderer.physicallyCorrectLights = true;
263
+ this.createRenderer();
287
264
  }
288
265
 
289
266
  this.scene = new Scene();
@@ -315,7 +292,36 @@
315
292
  this._disposeCallbacks.push(() => this._intersectionObserver?.disconnect());
316
293
  }
317
294
 
295
+ private createRenderer(domElement?: HTMLElement) {
296
+ this.renderer?.dispose();
318
297
 
298
+ const params: WebGLRendererParameters = {
299
+ antialias: true,
300
+ };
301
+
302
+ // get canvas already configured in the Needle Engine Web Component
303
+ const canvas = domElement?.shadowRoot?.querySelector("canvas");
304
+ if (canvas) params.canvas = canvas;
305
+
306
+ this.renderer = new WebGLRenderer(params);
307
+
308
+ // some tonemapping other than "NONE" is required for adjusting exposure with EXR environments
309
+ this.renderer.toneMappingExposure = 1; // range [0...inf] instead of the usual -15..15
310
+ this.renderer.toneMapping = NoToneMapping; // could also set to LinearToneMapping, ACESFilmicToneMapping
311
+
312
+ this.renderer.setClearColor(new Color('lightgrey'), 0);
313
+ // @ts-ignore
314
+ this.renderer.antialias = true;
315
+ // @ts-ignore
316
+ this.renderer.alpha = false;
317
+ this.renderer.shadowMap.enabled = true;
318
+ this.renderer.shadowMap.type = PCFSoftShadowMap;
319
+ this.renderer.setSize(this.domWidth, this.domHeight);
320
+ this.renderer.outputEncoding = sRGBEncoding;
321
+ this.renderer.physicallyCorrectLights = true;
322
+ }
323
+
324
+
319
325
  private _intersectionObserver: IntersectionObserver | null = null;
320
326
  private internalOnUpdateVisible() {
321
327
  this._intersectionObserver?.disconnect();
@@ -375,12 +381,23 @@
375
381
  /** Will destroy all scenes and objects in the scene
376
382
  */
377
383
  clear() {
378
- if (this.scene?.children.length) {
379
- destroy(this.scene, true, true);
380
- this.scene = new Scene();
384
+ ContextRegistry.dispatchCallback(ContextEvent.ContextClearing, this);
385
+ // NOTE: this does dispose the environment/background image too
386
+ // which is probably not desired if it is set via the skybox-image attribute
387
+ destroy(this.scene, true, true);
388
+ this.scene = new Scene();
389
+ this.addressables?.clear();
390
+ this.lightmaps?.clear();
391
+ this.physics?.engine?.clearCaches();
392
+
393
+ if (!this.isManagedExternally) {
394
+ this.renderer.renderLists.dispose();
395
+ this.renderer.state.reset();
396
+ this.renderer.resetState();
381
397
  }
382
398
  // We do not want to clear the renderer here because when switching src we want to keep the last rendered frame in case the loading screen is not visible
383
399
  // if a user wants to see the background they can still call setClearAlpha(0) and clear manually
400
+ ContextRegistry.dispatchCallback(ContextEvent.ContextCleared, this);
384
401
  }
385
402
 
386
403
  dispose() {
@@ -397,8 +414,9 @@
397
414
  this.renderer.clear();
398
415
  }
399
416
  if (!this.isManagedExternally) {
400
- this.renderer?.dispose();
417
+ this.renderer.dispose();
401
418
  }
419
+ this.renderer = null!;
402
420
  for (const cb of this._disposeCallbacks) {
403
421
  try {
404
422
  cb();
@@ -410,7 +428,6 @@
410
428
  if (this.domElement?.parentElement) {
411
429
  this.domElement.parentElement.removeChild(this.domElement);
412
430
  }
413
-
414
431
  ContextRegistry.dispatchCallback(ContextEvent.ContextDestroyed, this);
415
432
  ContextRegistry.unregister(this);
416
433
  }
@@ -681,7 +698,7 @@
681
698
  this._isCreating = false;
682
699
  this.restartRenderLoop();
683
700
  this._dispatchReadyAfterFrame = true;
684
- return ContextRegistry.dispatchCallback(ContextEvent.ContextCreated, this, { files: loadedFiles});
701
+ return ContextRegistry.dispatchCallback(ContextEvent.ContextCreated, this, { files: loadedFiles });
685
702
  }
686
703
 
687
704
  private _accumulatedTime = 0;
src/engine/engine_element.ts CHANGED
@@ -10,7 +10,6 @@
10
10
  import { isLocalNetwork } from "./engine_networking_utils";
11
11
  import { showBalloonWarning } from "./debug";
12
12
  import { destroy } from "./engine_gameobject";
13
- import { ContextRegistry } from "./engine_context_registry";
14
13
 
15
14
  //
16
15
  // registering loader here too to make sure it's imported when using engine via vanilla js
@@ -106,17 +105,23 @@
106
105
  this.onSetupDesktop();
107
106
 
108
107
  if (!this.getAttribute("src")) {
109
- const glob = globalThis["needle:codegen_files"];
110
- if (glob)
111
- this.setAttribute("src", glob);
112
- return;
108
+ const global = globalThis["needle:codegen_files"];
109
+ if (global) {
110
+ if (debug) console.log("globalThis[\"needle:codegen_files\"]", global);
111
+ this.setAttribute("src", global);
112
+ }
113
113
  }
114
-
115
- const src = this.getAttribute("src");
116
- if (src && src.length > 0) {
117
- await this.onLoad();
118
- }
119
114
  this.onSetupDesktop();
115
+ // we have to wait because codegen does set the src attribute when it's loaded
116
+ // which might happen after the element is connected
117
+ // if the `src` is then still null we want to initialize the default scene
118
+ setTimeout(() => {
119
+ if (this.isConnected === false) return;
120
+ const src = this.getAttribute("src");
121
+ if (src === undefined || src === null) {
122
+ this.onLoad();
123
+ }
124
+ }, 1);
120
125
  }
121
126
 
122
127
  disconnectedCallback() {
@@ -177,7 +182,6 @@
177
182
  if (filesToLoad === null || filesToLoad === undefined || filesToLoad.length <= 0) {
178
183
  if (debug) console.warn("Clear scene", filesToLoad);
179
184
  this._context.clear();
180
- return;
181
185
  }
182
186
  else if (!this.checkIfSourceHasChanged(filesToLoad, this._lastSourceFiles)) {
183
187
  return;
@@ -273,10 +277,7 @@
273
277
  return loadedFiles;
274
278
  };
275
279
  }
276
- if (!loadFunction) {
277
- console.error("Needle Engine: No files to load", filesToLoad);
278
- return;
279
- }
280
+
280
281
  const currentHash = this.getAttribute("hash");
281
282
  if (currentHash !== null && currentHash !== undefined)
282
283
  this._context.hash = currentHash;
@@ -303,9 +304,9 @@
303
304
  private onReady = () => this._loadingView?.onLoadingFinished();
304
305
  private onError = () => this._loadingView?.setMessage("Loading failed!");
305
306
 
306
- private getSourceFiles(): Array<string> | null {
307
+ private getSourceFiles(): Array<string> {
307
308
  const src: string | null | string[] = this.getAttribute("src");
308
- if (!src) return null;
309
+ if (!src) return [];
309
310
 
310
311
  let filesToLoad: Array<string>;
311
312
  // When using globalThis the src is an array already
src/engine/engine_gameobject.ts CHANGED
@@ -120,13 +120,17 @@
120
120
  if (comp.isComponent) {
121
121
  comp.__internalDisable();
122
122
  comp.__internalDestroy();
123
+ comp.gameObject = null!;
124
+ comp.context = null;
123
125
  return;
124
126
  }
125
127
 
126
128
 
127
129
  const obj = instance as GameObject;
128
130
  setDestroyed(obj, true);
129
- if (dispose) disposeObjectResources(obj);
131
+ if (dispose) {
132
+ disposeObjectResources(obj);
133
+ }
130
134
  // This needs to be called after disposing because it removes the references to resources
131
135
  __internalRemoveReferences(obj);
132
136
 
@@ -145,6 +149,8 @@
145
149
  const comp: Component = components[i];
146
150
  comp.__internalDisable();
147
151
  comp.__internalDestroy();
152
+ comp.gameObject = null!;
153
+ comp.context = null;
148
154
  // if (comp.destroy) {
149
155
  // if (debug) console.log("destroying", comp);
150
156
  // comp.destroy();
src/engine/engine_lightdata.ts CHANGED
@@ -8,6 +8,7 @@
8
8
  const debugLightmap = getParam("debuglightmaps") ? true : false;
9
9
 
10
10
  export interface ILightDataRegistry {
11
+ clear();
11
12
  registerTexture(sourceId: SourceIdentifier, type: LightmapType, texture: Texture, index?: number);
12
13
  tryGet(sourceId: SourceIdentifier | undefined, type: LightmapType, index: number): Texture | null;
13
14
  tryGetLightmap(sourceId: SourceIdentifier | null | undefined, index: number): Texture | null;
@@ -28,6 +29,10 @@
28
29
  private _context: Context;
29
30
  private _lightmaps: Map<SourceIdentifier, Map<LightmapType, Texture[]>> = new Map();
30
31
 
32
+ clear() {
33
+ this._lightmaps.clear();
34
+ }
35
+
31
36
  constructor(context: Context) {
32
37
  this._context = context;
33
38
  }
src/engine/engine_physics_rapier.ts CHANGED
@@ -455,6 +455,8 @@
455
455
 
456
456
  clearCaches() {
457
457
  this._meshCache.clear();
458
+ this.eventQueue?.free();
459
+ this.world?.free();
458
460
  }
459
461
 
460
462
  async addBoxCollider(collider: ICollider, center: Vector3, size: Vector3) {
src/engine/engine_physics.ts CHANGED
@@ -182,9 +182,12 @@
182
182
  let targets = options.targets;
183
183
  if (!targets) {
184
184
  targets = this.targetBuffer;
185
+ targets.length = 1;
185
186
  targets[0] = this.context.scene;
186
187
  }
187
188
  let results = options.results;
189
+ if (this.defaultRaycastOptions.results)
190
+ this.defaultRaycastOptions.results.length = 0;
188
191
  if (!results) {
189
192
  if (!this.defaultRaycastOptions.results)
190
193
  this.defaultRaycastOptions.results = new Array<Intersection>();
src/engine-components/postprocessing/index.ts CHANGED
@@ -1,3 +1,4 @@
1
1
  export * from "./VolumeParameter"
2
2
  export * from "./PostProcessingHandler"
3
- export * from "./PostProcessingEffect";
3
+ export * from "./PostProcessingEffect";
4
+ export * from "./VolumeProfile";
src/engine-components/OrbitControls.ts CHANGED
@@ -6,7 +6,7 @@
6
6
  import { serializable } from "../engine/engine_serialization_decorator";
7
7
  import { getParam, isMobileDevice } from "../engine/engine_utils";
8
8
 
9
- import { Camera as ThreeCamera, Box3, Object3D, PerspectiveCamera, Vector2, Vector3, Box3Helper } from "three";
9
+ import { Camera as ThreeCamera, Box3, Object3D, PerspectiveCamera, Vector2, Vector3, Box3Helper, GridHelper } from "three";
10
10
  import { OrbitControls as ThreeOrbitControls } from "three/examples/jsm/controls/OrbitControls";
11
11
  import { AfterHandleInputEvent, EventSystem, EventSystemEvents } from "./ui/EventSystem";
12
12
  import { ICameraController } from "../engine/engine_types";
@@ -361,6 +361,7 @@
361
361
  const camera = this._cameraObject as PerspectiveCamera;
362
362
  const controls = this._controls as ThreeOrbitControls | null;
363
363
  if (!objects?.length) objects = this.context.scene.children;
364
+ if (objects.length <= 0) return;
364
365
 
365
366
  if (!camera || !controls) return;
366
367
 
@@ -377,12 +378,12 @@
377
378
  for (const object of objects) {
378
379
  // ignore Box3Helpers
379
380
  if (object instanceof Box3Helper) continue;
381
+ if (object instanceof GridHelper) continue;
380
382
  box.expandByObject(object, true);
381
383
  }
382
384
 
383
385
  camera.updateMatrixWorld();
384
386
  camera.updateProjectionMatrix();
385
-
386
387
  box.getCenter(center);
387
388
 
388
389
  // project this box into camera space
@@ -390,6 +391,10 @@
390
391
 
391
392
  box.getSize(size);
392
393
  box.setFromCenterAndSize(center, size);
394
+ if (size.length() <= 0.0000000001) {
395
+ if(debugCameraFit) console.warn("Camera fit size is zero", box, [...objects]);
396
+ return;
397
+ }
393
398
 
394
399
  const verticalFov = camera.fov;
395
400
  const horizontalFov = 2 * Math.atan(Math.tan(verticalFov * Math.PI / 360 / 2) * camera.aspect) / Math.PI * 360;
src/engine-components/postprocessing/PostProcessingEffect.ts CHANGED
@@ -17,6 +17,20 @@
17
17
 
18
18
  export abstract class PostProcessingEffect extends Component implements IEffectProvider, ISerializable, IEditorModification {
19
19
 
20
+ constructor(params: any = undefined) {
21
+ super();
22
+ if (params) {
23
+ this.ensureVolumeParameters();
24
+ for (const key of Object.keys(params)) {
25
+ const value = params[key];
26
+ const param = this[key];
27
+ if (param instanceof VolumeParameter) {
28
+ param.value = value;
29
+ }
30
+ }
31
+ }
32
+ }
33
+
20
34
  abstract get typeName(): string;
21
35
 
22
36
  onEnable(): void {
@@ -43,6 +57,7 @@
43
57
 
44
58
  /** Apply post settings. Make sure to call super.apply() if you also create an effect */
45
59
  apply(): void | undefined | EffectProviderResult {
60
+ this.ensureVolumeParameters();
46
61
  if (!this._result) {
47
62
  this._result = this.onCreateEffect?.call(this);
48
63
  }
@@ -59,7 +74,7 @@
59
74
  onCreateEffect?(): EffectProviderResult | undefined
60
75
 
61
76
  dispose() {
62
- if(debug) console.warn("DISPOSE", this)
77
+ if (debug) console.warn("DISPOSE", this)
63
78
  if (this._result) {
64
79
  if (Array.isArray(this._result)) {
65
80
  this._result.forEach(r => r.dispose());
@@ -111,7 +126,24 @@
111
126
  return true;
112
127
  }
113
128
  }
114
- }
115
129
 
130
+ private _didCreateVolumeParameters: boolean = false;
131
+ /** Creates volume parameter fields that have not been initialized yet */
132
+ protected ensureVolumeParameters() {
133
+ if (this._didCreateVolumeParameters) return;
134
+ this._didCreateVolumeParameters = true;
116
135
 
117
-
136
+ const types = this["$serializedTypes"];
137
+ if (types) {
138
+ for (const fieldName of Object.keys(types)) {
139
+ const type = types[fieldName];
140
+ if (type === VolumeParameter) {
141
+ const parameter = this[fieldName];
142
+ if (!parameter) {
143
+ this[fieldName] = new VolumeParameter();
144
+ }
145
+ }
146
+ }
147
+ }
148
+ }
149
+ }
src/engine-components/Skybox.ts CHANGED
@@ -9,6 +9,7 @@
9
9
  import { ContextRegistry } from "../engine/engine_context_registry";
10
10
  import { registerObservableAttribute } from "../engine/engine_element_extras";
11
11
  import { type IContext } from "../engine/engine_types";
12
+ import { disposeObjectResources, setDisposable } from "../engine/engine_assetdatabase";
12
13
 
13
14
  const debug = getParam("debugskybox");
14
15
 
@@ -57,11 +58,11 @@
57
58
  return Promise.resolve();
58
59
  });
59
60
 
60
-
61
+ declare type SkyboxCacheEntry = { src: string, texture: Promise<Texture> };
61
62
  function ensureGlobalCache() {
62
63
  if (!globalThis["NEEDLE_ENGINE_SKYBOX_TEXTURES"])
63
- globalThis["NEEDLE_ENGINE_SKYBOX_TEXTURES"] = new Array<{ src: string, texture: Promise<Texture> }>();
64
- return globalThis["NEEDLE_ENGINE_SKYBOX_TEXTURES"];
64
+ globalThis["NEEDLE_ENGINE_SKYBOX_TEXTURES"] = new Array<SkyboxCacheEntry>();
65
+ return globalThis["NEEDLE_ENGINE_SKYBOX_TEXTURES"] as Array<SkyboxCacheEntry>;
65
66
  }
66
67
 
67
68
  function tryGetPreviouslyLoadedTexture(src: string) {
@@ -77,8 +78,11 @@
77
78
  const cache = ensureGlobalCache();
78
79
  // Make sure the cache doesnt get too big
79
80
  while (cache.length > 5) {
80
- cache.shift();
81
+ const entry = cache.shift();
82
+ entry?.texture.then(t => setDisposable(t, true));
83
+ disposeObjectResources(entry?.texture);
81
84
  }
85
+ texture.then(t => setDisposable(t, false));
82
86
  cache.push({ src, texture });
83
87
  }
84
88
 
@@ -164,7 +168,10 @@
164
168
 
165
169
  private async loadTexture(url: string) {
166
170
  const cached = tryGetPreviouslyLoadedTexture(url);
167
- if (cached) return await cached;
171
+ if (cached) {
172
+ const res = await cached;
173
+ if (res.source?.data?.length > 0) return res;
174
+ }
168
175
  const isEXR = url.endsWith(".exr");
169
176
  const isHdr = url.endsWith(".hdr");
170
177
  if (isEXR) {
src/engine-components/timeline/TimelineTracks.ts CHANGED
@@ -546,6 +546,15 @@
546
546
  }
547
547
  }
548
548
 
549
+ onDestroy() {
550
+ for (const audio of this.audio) {
551
+ if (audio.source)
552
+ audio?.disconnect();
553
+ }
554
+ this.audio.length = 0;
555
+ // TODO: dispose loaded audio buffers by this track
556
+ }
557
+
549
558
  onMuteChanged() {
550
559
  if (this.muted) {
551
560
  for (let i = 0; i < this.audio.length; i++) {
@@ -661,9 +670,10 @@
661
670
  return false;
662
671
  }
663
672
 
664
- private static _currentlyLoading: Map<string, Promise<AudioBuffer | null>> = new Map();
673
+ private static _audioBuffers: Map<string, Promise<AudioBuffer | null>> = new Map();
674
+
665
675
  public static dispose() {
666
- AudioTrackHandler._currentlyLoading.clear();
676
+ AudioTrackHandler._audioBuffers.clear();
667
677
  }
668
678
 
669
679
  private handleAudioLoading(model: Models.ClipModel, audio: THREE.Audio): Promise<AudioBuffer | null> | null {
@@ -673,8 +683,8 @@
673
683
  // TODO: maybe we should cache the loaders / buffers here by path
674
684
  const path = this.getAudioFilePath(model.asset.clip);
675
685
 
676
- if (AudioTrackHandler._currentlyLoading.get(path)) {
677
- const promise = AudioTrackHandler._currentlyLoading.get(path)!
686
+ if (AudioTrackHandler._audioBuffers.get(path)) {
687
+ const promise = AudioTrackHandler._audioBuffers.get(path)!
678
688
  promise.then((buffer) => {
679
689
  if (buffer) audio.setBuffer(buffer);
680
690
  });
@@ -694,7 +704,7 @@
694
704
  resolve(null);
695
705
  });
696
706
  });
697
- AudioTrackHandler._currentlyLoading.set(path, loadingPromise);
707
+ AudioTrackHandler._audioBuffers.set(path, loadingPromise);
698
708
  return loadingPromise;
699
709
  }
700
710
  }
plugins/types/userconfig.d.ts CHANGED
@@ -22,6 +22,15 @@
22
22
  noReload: boolean;
23
23
  noCodegenTransform: boolean;
24
24
 
25
+ /** set to true to disable poster generation */
26
+ noPoster?: boolean;
27
+ // posterFormat?: "image/webp";// | "image/png";
28
+ /**
29
+ * Use "default" to always generate the poster after 'src' has changed
30
+ * Use "once" to generate the poster only once, when no poster already exists
31
+ */
32
+ posterGenerationMode?: "default" | "once";
33
+
25
34
  /** used by nextjs config to forward the webpack module */
26
35
  modules: needleModules
27
36
  }
src/engine-components/postprocessing/Volume.ts CHANGED
@@ -12,6 +12,7 @@
12
12
 
13
13
  const debug = getParam("debugpost");
14
14
 
15
+ /** Handles PostProcessing effects */
15
16
  export class Volume extends Behaviour implements IEditorModificationReceiver {
16
17
 
17
18
  @serializeable(VolumeProfile)
@@ -20,6 +21,10 @@
20
21
  private _postprocessing?: PostProcessingHandler;
21
22
  private _effects: PostProcessingEffect[] = [];
22
23
 
24
+ markDirty() {
25
+ this._isDirty = true;
26
+ }
27
+
23
28
  awake() {
24
29
  // ensure the profile is initialized
25
30
  this.sharedProfile?.init();
@@ -49,7 +54,7 @@
49
54
 
50
55
  // Wait for the first frame to be rendered before creating because then we know we have a camera (issue 135)
51
56
  if (this.context.mainCamera) {
52
- if (!this._postprocessing || !this._postprocessing.isActive) {
57
+ if (!this._postprocessing || !this._postprocessing.isActive || this._isDirty) {
53
58
  this.apply();
54
59
  }
55
60
  }
@@ -69,6 +74,7 @@
69
74
 
70
75
  private _lastApplyTime?: number;
71
76
  private _rapidApplyCount = 0;
77
+ private _isDirty: boolean = false;
72
78
 
73
79
  private apply() {
74
80
  if (debug) console.log("Apply PostProcessing", this, this.context.mainCamera?.name);
@@ -82,7 +88,7 @@
82
88
  this._lastApplyTime = Date.now();
83
89
  }
84
90
 
85
-
91
+ this._isDirty = false;
86
92
  this.unapply();
87
93
 
88
94
  this._effects.length = 0;
src/engine-components/postprocessing/VolumeParameter.ts CHANGED
@@ -8,6 +8,7 @@
8
8
  constructor(value?: any) {
9
9
  this._value = value;
10
10
  this._defaultValue = value;
11
+ this._valueRaw = value;
11
12
  }
12
13
 
13
14
  @serializable()
src/engine-components/postprocessing/VolumeProfile.ts CHANGED
@@ -30,7 +30,7 @@
30
30
  export class VolumeProfile {
31
31
 
32
32
  @serializeable([d => resolveComponentType(d), PostProcessingEffect])
33
- components?: PostProcessingEffect[];
33
+ components: PostProcessingEffect[] = [];
34
34
 
35
35
  /** call init on all components */
36
36
  init() {
src/engine-components/postprocessing/Effects/ScreenspaceAmbientOcclusionN8.ts ADDED
@@ -0,0 +1,99 @@
1
+ import { Color, NeverDepth, PerspectiveCamera } from "three";
2
+ import { serializable } from "../../../engine/engine_serialization";
3
+ import { EffectProviderResult, PostProcessingEffect } from "../PostProcessingEffect";
4
+ import { VolumeParameter } from "../VolumeParameter";
5
+ import { registerCustomEffectType } from "../VolumeProfile";
6
+ import { N8AOPostPass } from "n8ao";
7
+ import { validate } from "../../../engine/engine_util_decorator";
8
+
9
+ // https://github.com/N8python/n8ao
10
+
11
+ /**See (N8AO documentation)[https://github.com/N8python/n8ao] */
12
+ export enum ScreenSpaceAmbientOcclusionN8QualityMode {
13
+ Performance = 0,
14
+ Low = 1,
15
+ Medium = 2,
16
+ High = 3,
17
+ Ultra = 4,
18
+ }
19
+
20
+ /**See [N8AO documentation](https://github.com/N8python/n8ao) */
21
+ export class ScreenSpaceAmbientOcclusionN8 extends PostProcessingEffect {
22
+
23
+ get typeName() {
24
+ return "ScreenSpaceAmbientOcclusionN8";
25
+ }
26
+
27
+ @validate()
28
+ @serializable()
29
+ gammaCorrection: boolean = true;
30
+
31
+ @serializable(VolumeParameter)
32
+ intensity!: VolumeParameter;
33
+
34
+ @serializable(VolumeParameter)
35
+ falloff!: VolumeParameter;
36
+
37
+ @serializable(VolumeParameter)
38
+ aoRadius!: VolumeParameter;
39
+
40
+ @validate()
41
+ @serializable()
42
+ screenspaceRadius: boolean = false;
43
+
44
+ @serializable(VolumeParameter)
45
+ color!: VolumeParameter;
46
+
47
+ @validate()
48
+ @serializable()
49
+ quality: ScreenSpaceAmbientOcclusionN8QualityMode = ScreenSpaceAmbientOcclusionN8QualityMode.Medium;
50
+
51
+ private _ssao?: N8AOPostPass;
52
+
53
+ onValidate(): void {
54
+ if (this._ssao) {
55
+ this._ssao.setQualityMode(ScreenSpaceAmbientOcclusionN8QualityMode[this.quality]);
56
+ this._ssao.configuration.gammaCorrection = this.gammaCorrection;
57
+ this._ssao.configuration.screenSpaceRadius = this.screenspaceRadius;
58
+
59
+ }
60
+ }
61
+
62
+ onCreateEffect(): EffectProviderResult {
63
+
64
+ const cam = this.context.mainCamera! as PerspectiveCamera;
65
+
66
+ const ssao = this._ssao = new N8AOPostPass(
67
+ this.context.scene,
68
+ cam,
69
+ this.context.domWidth,
70
+ this.context.domHeight
71
+ );
72
+ const mode = ScreenSpaceAmbientOcclusionN8QualityMode[this.quality];
73
+ ssao.setQualityMode(mode);
74
+
75
+ ssao.configuration.gammaCorrection = this.gammaCorrection;
76
+
77
+ ssao.configuration.screenSpaceRadius = this.screenspaceRadius;
78
+
79
+ this.intensity.onValueChanged = newValue => {
80
+ ssao.configuration.intensity = newValue;
81
+ }
82
+ this.falloff.onValueChanged = newValue => {
83
+ ssao.configuration.distanceFalloff = newValue;
84
+ }
85
+ this.aoRadius.onValueChanged = newValue => {
86
+ ssao.configuration.aoRadius = newValue;
87
+ }
88
+ this.color.onValueChanged = newValue => {
89
+ if (!ssao.color) ssao.color = new Color();
90
+ ssao.configuration.color.copy(newValue);
91
+ }
92
+
93
+ const arr = new Array();
94
+ arr.push(ssao);
95
+ return arr;
96
+ }
97
+
98
+ }
99
+ registerCustomEffectType("ScreenSpaceAmbientOcclusionN8", ScreenSpaceAmbientOcclusionN8);