@@ -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),
|
@@ -9,18 +9,31 @@
|
|
9
9
|
return "include/poster.webp";
|
10
10
|
}
|
11
11
|
|
12
|
-
|
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(
|
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:
|
67
|
+
children: scriptContent,
|
49
68
|
injectTo: 'body',
|
50
69
|
},
|
51
70
|
];
|
@@ -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);
|
@@ -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";
|
@@ -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
|
-
|
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> {
|
@@ -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
|
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
|
-
|
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");
|
@@ -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 = {
|
@@ -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
|
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
|
-
|
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
|
-
|
379
|
-
|
380
|
-
|
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
|
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;
|
@@ -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
|
110
|
-
if (
|
111
|
-
|
112
|
-
|
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
|
-
|
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>
|
307
|
+
private getSourceFiles(): Array<string> {
|
307
308
|
const src: string | null | string[] = this.getAttribute("src");
|
308
|
-
if (!src) return
|
309
|
+
if (!src) return [];
|
309
310
|
|
310
311
|
let filesToLoad: Array<string>;
|
311
312
|
// When using globalThis the src is an array already
|
@@ -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)
|
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();
|
@@ -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
|
}
|
@@ -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) {
|
@@ -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>();
|
@@ -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";
|
@@ -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;
|
@@ -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
|
+
}
|
@@ -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<
|
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)
|
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) {
|
@@ -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
|
673
|
+
private static _audioBuffers: Map<string, Promise<AudioBuffer | null>> = new Map();
|
674
|
+
|
665
675
|
public static dispose() {
|
666
|
-
AudioTrackHandler.
|
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.
|
677
|
-
const promise = AudioTrackHandler.
|
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.
|
707
|
+
AudioTrackHandler._audioBuffers.set(path, loadingPromise);
|
698
708
|
return loadingPromise;
|
699
709
|
}
|
700
710
|
}
|
@@ -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
|
}
|
@@ -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;
|
@@ -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()
|
@@ -30,7 +30,7 @@
|
|
30
30
|
export class VolumeProfile {
|
31
31
|
|
32
32
|
@serializeable([d => resolveComponentType(d), PostProcessingEffect])
|
33
|
-
components
|
33
|
+
components: PostProcessingEffect[] = [];
|
34
34
|
|
35
35
|
/** call init on all components */
|
36
36
|
init() {
|
@@ -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);
|