Needle Engine

Changes between version 3.37.7-beta.1 and 3.37.8-alpha
Files changed (15) hide show
  1. src/engine-components/export/usdz/extensions/behavior/BehaviourComponents.ts +2 -2
  2. src/engine-components/Camera.ts +10 -2
  3. src/engine-components/Collider.ts +2 -2
  4. src/engine/engine_context.ts +5 -2
  5. src/engine/extensions/extensions.ts +1 -1
  6. src/engine-components/ui/Graphic.ts +1 -1
  7. src/engine/extensions/NEEDLE_progressive.ts +1 -878
  8. src/engine-components/ParticleSystem.ts +1 -1
  9. src/engine-components/Renderer.ts +1 -462
  10. src/engine-components/RendererInstancing.ts +1 -1
  11. src/engine-components/ScreenCapture.ts +6 -0
  12. src/engine-components/SpriteRenderer.ts +4 -4
  13. src/engine-components/export/usdz/USDZExporter.ts +3 -10
  14. src/engine-components/webxr/controllers/XRControllerModel.ts +2 -2
  15. src/engine/engine_lods.ts +162 -0
src/engine-components/export/usdz/extensions/behavior/BehaviourComponents.ts CHANGED
@@ -229,10 +229,10 @@
229
229
 
230
230
  // Ensure that the progressive textures have been loaded for all variants and materials
231
231
  if (this.materialToSwitch) {
232
- await NEEDLE_progressive.assignTextureLOD(this.context, this.sourceId!, this.materialToSwitch, 0);
232
+ await NEEDLE_progressive.assignTextureLOD(this.materialToSwitch, 0);
233
233
  }
234
234
  if (this.variantMaterial) {
235
- await NEEDLE_progressive.assignTextureLOD(this.context, this.sourceId!, this.variantMaterial, 0);
235
+ await NEEDLE_progressive.assignTextureLOD(this.variantMaterial, 0);
236
236
  }
237
237
  }
238
238
 
src/engine-components/Camera.ts CHANGED
@@ -242,6 +242,10 @@
242
242
  }
243
243
 
244
244
  private _frustum?: Frustum;
245
+ /**
246
+ * Get a frustum - it will be created the first time this method is called and updated every frame in onBeforeRender when it exists.
247
+ * You can also manually update it using the updateFrustum method.
248
+ */
245
249
  public getFrustum(): Frustum {
246
250
  if (!this._frustum) {
247
251
  this._frustum = new Frustum();
@@ -254,14 +258,19 @@
254
258
  if (!this._frustum) this._frustum = new Frustum();
255
259
  this._frustum.setFromProjectionMatrix(this.getProjectionScreenMatrix(this._projScreenMatrix, true), this.context.renderer.coordinateSystem);
256
260
  }
261
+ /**
262
+ * @returns {Matrix4} this camera's projection screen matrix.
263
+ */
257
264
  public getProjectionScreenMatrix(target: Matrix4, forceUpdate?: boolean) {
258
265
  if (forceUpdate) {
259
266
  this._projScreenMatrix.multiplyMatrices(this.cam.projectionMatrix, this.cam.matrixWorldInverse);
260
267
  }
261
268
  if (target === this._projScreenMatrix) return target;
262
269
  return target.copy(this._projScreenMatrix);
263
- }
270
+ }
271
+ private readonly _projScreenMatrix = new Matrix4();
264
272
 
273
+
265
274
  /** @internal */
266
275
  awake() {
267
276
  if (debugscreenpointtoray) {
@@ -292,7 +301,6 @@
292
301
  this.context.removeCamera(this);
293
302
  }
294
303
 
295
- private readonly _projScreenMatrix = new Matrix4();
296
304
 
297
305
  /** @internal */
298
306
  onBeforeRender() {
src/engine-components/Collider.ts CHANGED
@@ -185,7 +185,7 @@
185
185
 
186
186
  if (this.sharedMesh?.isMesh) {
187
187
  this.context.physics.engine.addMeshCollider(this, this.sharedMesh, this.convex, getWorldScale(this.gameObject));
188
- NEEDLE_progressive.assignMeshLOD(this.context, this.sourceId!, this.sharedMesh, LOD).then(res => {
188
+ NEEDLE_progressive.assignMeshLOD(this.sharedMesh, LOD).then(res => {
189
189
  if (res && this.activeAndEnabled && this.context.physics.engine && this.sharedMesh) {
190
190
  this.context.physics.engine.removeBody(this);
191
191
  this.sharedMesh.geometry = res;
@@ -202,7 +202,7 @@
202
202
  const child = group.children[ch] as Mesh;
203
203
  if (child.isMesh) {
204
204
  this.context.physics.engine.addMeshCollider(this, child, this.convex, getWorldScale(this.gameObject));
205
- promises.push(NEEDLE_progressive.assignMeshLOD(this.context, this.sourceId!, child, LOD));
205
+ promises.push(NEEDLE_progressive.assignMeshLOD(child, LOD));
206
206
  }
207
207
  }
208
208
  Promise.all(promises).then(res => {
src/engine/engine_context.ts CHANGED
@@ -19,9 +19,9 @@
19
19
  import { Input } from './engine_input.js';
20
20
  import { invokeLifecycleFunctions } from './engine_lifecycle_functions_internal.js';
21
21
  import { type ILightDataRegistry, LightDataRegistry } from './engine_lightdata.js';
22
+ import { LODsManager } from "./engine_lods.js";
22
23
  import * as looputils from './engine_mainloop_utils.js';
23
24
  import { NetworkConnection } from './engine_networking.js';
24
- import { isLocalNetwork } from './engine_networking_utils.js';
25
25
  import { Physics } from './engine_physics.js';
26
26
  import { PlayerViewManager } from './engine_playerview.js';
27
27
  import { RendererData as SceneLighting } from './engine_scenelighting.js';
@@ -376,6 +376,7 @@
376
376
  addressables: Addressables;
377
377
  lightmaps: ILightDataRegistry;
378
378
  players: PlayerViewManager;
379
+ readonly lodsManager: LODsManager;
379
380
  readonly menu: NeedleMenu;
380
381
 
381
382
  get isCreated() { return this._isCreated; }
@@ -413,6 +414,7 @@
413
414
  this.lightmaps = new LightDataRegistry(this);
414
415
  this.players = new PlayerViewManager(this);
415
416
  this.menu = new NeedleMenu(this);
417
+ this.lodsManager = new LODsManager(this);
416
418
 
417
419
 
418
420
  const resizeCallback = () => this._sizeChanged = true;
@@ -463,6 +465,7 @@
463
465
  this.renderer.shadowMap.type = PCFSoftShadowMap;
464
466
  this.renderer.setSize(this.domWidth, this.domHeight);
465
467
  this.renderer.outputColorSpace = SRGBColorSpace;
468
+ this.lodsManager.setRenderer(this.renderer);
466
469
 
467
470
  this.input.bindEvents();
468
471
  }
@@ -786,7 +789,7 @@
786
789
  console.error("Needle Engine dependencies failed to load", err)
787
790
  })
788
791
  .then(() => {
789
- if(debug) console.log("Needle Engine dependencies are ready");
792
+ if (debug) console.log("Needle Engine dependencies are ready");
790
793
  });
791
794
  }
792
795
 
src/engine/extensions/extensions.ts CHANGED
@@ -88,7 +88,7 @@
88
88
  loader.register(p => new NEEDLE_lighting_settings(p, sourceId, context));
89
89
  loader.register(p => new NEEDLE_techniques_webgl(p, sourceId));
90
90
  loader.register(p => new NEEDLE_render_objects(p, sourceId));
91
- loader.register(p => new NEEDLE_progressive(p, sourceId, context));
91
+ loader.register(p => new NEEDLE_progressive(p, sourceId));
92
92
  loader.register(p => new EXT_texture_exr(p));
93
93
  if (isResourceTrackingEnabled()) loader.register(p => new InternalUsageTrackerPlugin(p))
94
94
 
src/engine-components/ui/Graphic.ts CHANGED
@@ -222,7 +222,7 @@
222
222
  }
223
223
  // }
224
224
  this.setOptions({ backgroundImage: tex, borderRadius: 0, backgroundOpacity: this.color.alpha, backgroundSize: "stretch" });
225
- NEEDLE_progressive.assignTextureLOD(this.context, this.sourceId!, tex, 0).then(res => {
225
+ NEEDLE_progressive.assignTextureLOD(tex, 0).then(res => {
226
226
  if (res instanceof Texture) {
227
227
  this.setOptions({ backgroundImage: res });
228
228
  }
src/engine/extensions/NEEDLE_progressive.ts CHANGED
@@ -1,878 +1,1 @@
1
- import { BufferGeometry, Group, Material, Mesh, Object3D, RawShaderMaterial, Texture, TextureLoader } from "three";
2
- import { type GLTF, GLTFLoader, type GLTFLoaderPlugin, GLTFParser } from "three/examples/jsm/loaders/GLTFLoader.js";
3
-
4
- import { showBalloonMessage } from "../debug/index.js";
5
- import { findResourceUsers, getResourceUserCount } from "../engine_assetdatabase.js";
6
- import { ContextEvent, NeedleEngine } from "../engine_context_registry.js";
7
- import { addDracoAndKTX2Loaders } from "../engine_loaders.js";
8
- import { getRaycastMesh, setRaycastMesh } from "../engine_physics.js";
9
- import { Context } from "../engine_setup.js";
10
- import { type SourceIdentifier } from "../engine_types.js";
11
- import { delay, getParam, PromiseAllWithErrors, PromiseErrorResult, resolveUrl } from "../engine_utils.js";
12
-
13
- export const EXTENSION_NAME = "NEEDLE_progressive";
14
-
15
- const debug = getParam("debugprogressive");
16
-
17
- const $progressiveTextureExtension = Symbol("needle-progressive-texture");
18
-
19
- /** Removes the readonly attribute from all properties of an object */
20
- type DeepWriteable<T> = { -readonly [P in keyof T]: DeepWriteable<T[P]> };
21
-
22
-
23
- const debug_toggle_maps: Map<object, { keys: string[], sourceId: string }> = new Map();
24
- if (debug) {
25
- let currentDebugLodLevel = -1;
26
- let maxLevel = 2;
27
- function debugToggleProgressive() {
28
- currentDebugLodLevel += 1;
29
- showBalloonMessage(`Toggle LOD level: ${currentDebugLodLevel}<br/>Registered objects: ${debug_toggle_maps.size}`);
30
- console.log("Toggle LOD level", currentDebugLodLevel, debug_toggle_maps);
31
- const context = NeedleEngine.Current!;
32
- debug_toggle_maps.forEach((arr, obj) => {
33
- for (const key of arr.keys) {
34
- const cur = obj[key];
35
- if (cur instanceof BufferGeometry) {
36
- const info = NEEDLE_progressive.getMeshLODInformation(cur);
37
- const level = !info ? 0 : Math.min(currentDebugLodLevel, info.lods.length);
38
- obj["DEBUG:LOD"] = level;
39
- NEEDLE_progressive.assignMeshLOD(context, arr.sourceId, obj as Mesh, level);
40
- if (info) maxLevel = Math.max(maxLevel, info.lods.length - 1);
41
- }
42
- else if (obj instanceof Material) {
43
- NEEDLE_progressive.assignTextureLOD(context, arr.sourceId, obj, currentDebugLodLevel);
44
- break;
45
- }
46
- }
47
- });
48
- if (currentDebugLodLevel >= maxLevel) {
49
- currentDebugLodLevel = -1;
50
- }
51
- }
52
- window.addEventListener("keyup", evt => {
53
- if (evt.key === "p") debugToggleProgressive();
54
- });
55
- NeedleEngine.registerCallback(ContextEvent.ContextCreated, (ctx) => {
56
- const button = document.createElement("button");
57
- button.innerText = "Toggle Progressive";
58
- button.onclick = debugToggleProgressive;
59
- ctx.context.menu.appendChild(button);
60
- });
61
- }
62
- function registerDebug(obj: object, key: string, sourceId: string,) {
63
- if (!debug) return;
64
- if (!debug_toggle_maps.has(obj)) {
65
- debug_toggle_maps.set(obj, { keys: [], sourceId });
66
- }
67
- const existing = debug_toggle_maps.get(obj);
68
- if (existing?.keys?.includes(key) == false) {
69
- existing.keys.push(key);
70
- }
71
- }
72
-
73
- declare type NEEDLE_progressive_model_LOD = {
74
- path: string,
75
- hash?: string,
76
- }
77
-
78
- /** This is the data structure we have in the NEEDLE_progressive extension */
79
- declare type NEEDLE_progressive_model = {
80
- guid: string,
81
- lods: Array<NEEDLE_progressive_model_LOD>
82
- }
83
-
84
- declare type NEEDLE_progressive_texture_model = NEEDLE_progressive_model & {
85
-
86
- }
87
- declare type NEEDLE_progressive_mesh_model = NEEDLE_progressive_model & {
88
- density: number;
89
- lods: Array<NEEDLE_progressive_model_LOD & {
90
- density: number,
91
- indexCount: number;
92
- vertexCount: number;
93
- }>
94
- }
95
-
96
- /**
97
- * This is the result of a progressive texture loading event for a material's texture slot in {@link NEEDLE_progressive.assignTextureLOD}
98
- * @internal
99
- */
100
- export declare type ProgressiveMaterialTextureLoadingResult = {
101
- /** the material the progressive texture was loaded for */
102
- material: Material,
103
- /** the slot in the material where the texture was loaded */
104
- slot: string,
105
- /** the texture that was loaded (if any) */
106
- texture: Texture | null;
107
- /** the level of detail that was loaded */
108
- level: number;
109
- }
110
-
111
- /**
112
- * The NEEDLE_progressive extension for the GLTFLoader is responsible for loading progressive LODs for meshes and textures.
113
- * This extension can be used to load different resolutions of a mesh or texture at runtime (e.g. for LODs or progressive textures).
114
- * @example
115
- * ```javascript
116
- * const loader = new GLTFLoader();
117
- * loader.register(new NEEDLE_progressive());
118
- * loader.load("model.glb", (gltf) => {
119
- * const mesh = gltf.scene.children[0] as Mesh;
120
- * NEEDLE_progressive.assignMeshLOD(context, sourceId, mesh, 1).then(mesh => {
121
- * console.log("Mesh with LOD level 1 loaded", mesh);
122
- * });
123
- * });
124
- * ```
125
- */
126
- export class NEEDLE_progressive implements GLTFLoaderPlugin {
127
-
128
- /** The name of the extension */
129
- get name(): string {
130
- return EXTENSION_NAME;
131
- }
132
-
133
- static getMeshLODInformation(geo: BufferGeometry) {
134
- const info = this.getAssignedLODInformation(geo);
135
- if (info?.key) {
136
- return this.lodInfos.get(info.key) as NEEDLE_progressive_mesh_model;
137
- }
138
- return null;
139
- }
140
-
141
- /** Check if a LOD level is available for a mesh or a texture
142
- * @param obj the mesh or texture to check
143
- * @param level the level of detail to check for (0 is the highest resolution). If undefined, the function checks if any LOD level is available
144
- * @returns true if the LOD level is available (or if any LOD level is available if level is undefined)
145
- */
146
- static hasLODLevelAvailable(obj: Mesh | Texture | Material, level?: number): boolean {
147
-
148
- if (obj instanceof Material) {
149
- for (const slot of Object.keys(obj)) {
150
- const val = obj[slot];
151
- if (val instanceof Texture) {
152
- if (this.hasLODLevelAvailable(val, level)) return true;
153
- }
154
- }
155
- return false;
156
- }
157
- else if (obj instanceof Group) {
158
- for (const child of obj.children) {
159
- if (child instanceof Mesh) {
160
- if (this.hasLODLevelAvailable(child, level)) return true;
161
- }
162
- }
163
- }
164
-
165
-
166
- let lodObject: ObjectThatMightHaveLODs | undefined;
167
- let lodInformation: NEEDLE_progressive_model | undefined;
168
-
169
- if (obj instanceof Mesh) {
170
- lodObject = obj.geometry as BufferGeometry;
171
- }
172
- else if (obj instanceof Texture) {
173
- lodObject = obj;
174
- }
175
- if (lodObject) {
176
- if (lodObject?.userData?.LODS) {
177
- const lods = lodObject.userData.LODS;
178
- lodInformation = this.lodInfos.get(lods.key);
179
- if (level === undefined) return lodInformation != undefined;
180
- if (lodInformation) {
181
- if (Array.isArray(lodInformation.lods)) {
182
- return level < lodInformation.lods.length;
183
- }
184
- return level === 0;
185
- }
186
- }
187
- }
188
-
189
- return false;
190
- }
191
-
192
- /** Load a different resolution of a mesh (if available)
193
- * @param context the context
194
- * @param source the sourceid of the file from which the mesh is loaded (this is usually the component's sourceId)
195
- * @param mesh the mesh to load the LOD for
196
- * @param level the level of detail to load (0 is the highest resolution)
197
- * @returns a promise that resolves to the mesh with the requested LOD level
198
- * @example
199
- * ```javascript
200
- * const mesh = this.gameObject as Mesh;
201
- * NEEDLE_progressive.assignMeshLOD(context, sourceId, mesh, 1).then(mesh => {
202
- * console.log("Mesh with LOD level 1 loaded", mesh);
203
- * });
204
- * ```
205
- */
206
- static assignMeshLOD(context: Context, source: SourceIdentifier, mesh: Mesh, level: number): Promise<BufferGeometry | null> {
207
-
208
- if (!mesh) return Promise.resolve(null);
209
-
210
-
211
- if (mesh instanceof Mesh) {
212
-
213
- const currentGeometry = mesh.geometry;
214
- const lodinfo = this.getAssignedLODInformation(currentGeometry);
215
- if (!lodinfo) {
216
- return Promise.resolve(null);
217
- }
218
-
219
- if (!getRaycastMesh(mesh)) {
220
- setRaycastMesh(mesh, mesh.geometry as BufferGeometry);
221
- }
222
-
223
- const info = this.onProgressiveLoadStart(context, source, mesh, null);
224
- mesh["LOD:requested level"] = level;
225
- return NEEDLE_progressive.getOrLoadLOD<BufferGeometry>(context, source, currentGeometry, level).then(geo => {
226
- if (mesh["LOD:requested level"] === level) {
227
- delete mesh["LOD:requested level"];
228
-
229
- if (Array.isArray(geo)) {
230
- const index = lodinfo.index || 0;
231
- geo = geo[index];
232
- }
233
-
234
- if (geo && currentGeometry != geo) {
235
- if (debug == "verbose") console.log("Progressive Mesh " + mesh.name + " loaded", currentGeometry, "→", geo, "\n", mesh)
236
- if (geo instanceof BufferGeometry) {
237
- mesh.geometry = geo;
238
- if (debug) registerDebug(mesh, "geometry", source);
239
- }
240
- }
241
- }
242
- this.onProgressiveLoadEnd(info);
243
- return geo;
244
-
245
- }).catch(err => {
246
- this.onProgressiveLoadEnd(info);
247
- console.error("Error loading mesh LOD", mesh, err);
248
- return null;
249
- });
250
- }
251
- else if (debug) {
252
- console.error("Invalid call to assignMeshLOD: Request mesh LOD but the object is not a mesh", mesh);
253
- }
254
-
255
- return Promise.resolve(null);
256
- }
257
-
258
- /** Load a different resolution of a texture (if available)
259
- * @param context the context
260
- * @param source the sourceid of the file from which the texture is loaded (this is usually the component's sourceId)
261
- * @param materialOrTexture the material or texture to load the LOD for (if passing in a material all textures in the material will be loaded)
262
- * @param level the level of detail to load (0 is the highest resolution) - currently only 0 is supported
263
- * @returns a promise that resolves to the material or texture with the requested LOD level
264
- */
265
- static assignTextureLOD(context: Context, source: SourceIdentifier, materialOrTexture: Material | Texture, level: number = 0)
266
- : Promise<Array<ProgressiveMaterialTextureLoadingResult> | Texture | null> {
267
-
268
- if (!materialOrTexture) return Promise.resolve(null);
269
-
270
- if (materialOrTexture instanceof Material) {
271
- const material = materialOrTexture;
272
- const promises: Array<Promise<Texture | null>> = [];
273
- const slots = new Array<string>();
274
-
275
- if (material instanceof RawShaderMaterial) {
276
- // iterate uniforms of custom shaders
277
- for (const slot of Object.keys(material.uniforms)) {
278
- const val = material.uniforms[slot].value;
279
- if (val instanceof Texture) {
280
- const task = this.assignTextureLODForSlot(context, source, val, level, material, slot);
281
- promises.push(task);
282
- slots.push(slot);
283
- }
284
- }
285
- }
286
- else {
287
- for (const slot of Object.keys(material)) {
288
- const val = material[slot];
289
- if (val instanceof Texture) {
290
- const task = this.assignTextureLODForSlot(context, source, val, level, material, slot);
291
- promises.push(task);
292
- slots.push(slot);
293
- }
294
- }
295
- }
296
- return PromiseAllWithErrors(promises).then(res => {
297
- const textures = new Array<ProgressiveMaterialTextureLoadingResult>();
298
- for (let i = 0; i < res.results.length; i++) {
299
- const tex = res.results[i];
300
- const slot = slots[i];
301
- if (tex instanceof Texture) {
302
- textures.push({ material, slot, texture: tex, level });
303
- }
304
- else {
305
- textures.push({ material, slot, texture: null, level });
306
- }
307
- }
308
- return textures;
309
- });
310
- }
311
-
312
- if (materialOrTexture instanceof Texture) {
313
- const texture = materialOrTexture;
314
- return this.assignTextureLODForSlot(context, source, texture, level, null, null);
315
- }
316
-
317
- return Promise.resolve(null);
318
- }
319
-
320
- private static assignTextureLODForSlot(context: Context, source: SourceIdentifier, current: Texture, level: number, material: Material | null, slot: string | null): Promise<Texture | null> {
321
- if (current?.isTexture !== true) return Promise.resolve(null);
322
-
323
- // if (debug) console.log("-----------\n", "FIND", material?.name, slot, current?.name, current?.userData, current, material);
324
-
325
- const info = this.onProgressiveLoadStart(context, source, material, slot);
326
- return NEEDLE_progressive.getOrLoadLOD<Texture>(context, source, current, level).then(tex => {
327
-
328
- // this can currently not happen
329
- if (Array.isArray(tex)) return null;
330
-
331
- if (tex?.isTexture === true) {
332
- if (tex != current) {
333
- // if (debug) console.warn("Assign LOD", material?.name, slot, tex.name, tex["guid"], material, "Prev:", current, "Now:", tex, "\n--------------");
334
-
335
- // tex.needsUpdate = true;
336
-
337
- if (material && slot) {
338
- material[slot] = tex;
339
- // material.needsUpdate = true;
340
- }
341
-
342
- if (debug && slot && material) registerDebug(material, slot, source);
343
-
344
- // check if the old texture is still used by other objects
345
- // if not we dispose it...
346
- // this could also be handled elsewhere and not be done immediately
347
- // const users = getResourceUserCount(current);
348
- // if (!users) {
349
- // if (debug) console.log("Progressive: Dispose texture", current.name, current.source.data, current.uuid);
350
- // current?.dispose();
351
- // }
352
- }
353
-
354
- this.onProgressiveLoadEnd(info);
355
- return tex;
356
- }
357
- else if (debug == "verbose") {
358
- console.warn("No LOD found for", current, level);
359
- }
360
-
361
- this.onProgressiveLoadEnd(info);
362
- return null;
363
-
364
- }).catch(err => {
365
- this.onProgressiveLoadEnd(info);
366
- console.error("Error loading LOD", current, err);
367
- return null;
368
- });
369
- }
370
-
371
-
372
-
373
-
374
- private readonly parser: GLTFParser;
375
- private readonly sourceId: SourceIdentifier;
376
- private readonly context: Context;
377
-
378
- constructor(parser: GLTFParser, sourceId: SourceIdentifier, context: Context) {
379
- this.parser = parser;
380
- this.sourceId = sourceId;
381
- this.context = context;
382
- }
383
-
384
- afterRoot(gltf: GLTF): null {
385
- if (debug)
386
- console.log("AFTER", this.sourceId, gltf);
387
-
388
- this.parser.json.textures?.forEach((textureInfo, index) => {
389
- if (textureInfo?.extensions) {
390
- const ext: NEEDLE_progressive_texture_model = textureInfo?.extensions[EXTENSION_NAME];
391
- if (ext) {
392
- for (const key of this.parser.associations.keys()) {
393
- if (key instanceof Texture) {
394
- const val = this.parser.associations.get(key) as { textures: number };
395
- if (val.textures === index) {
396
- const tex = key;
397
- if (debug)
398
- console.log("> Progressive: register texture", index, tex.name, tex.uuid, tex, ext);
399
- // Put the extension info into the source (seems like tiled textures are cloned and the userdata etc is not properly copied BUT the source of course is not cloned)
400
- // see https://github.com/needle-tools/needle-engine-support/issues/133
401
- if (tex.source)
402
- tex.source[$progressiveTextureExtension] = ext;
403
- const LODKEY = tex.uuid;
404
- NEEDLE_progressive.assignLODInformation(tex, LODKEY, 0, 0, undefined);
405
- NEEDLE_progressive.lodInfos.set(LODKEY, ext);
406
- NEEDLE_progressive.lowresCache.set(LODKEY, tex);
407
- }
408
- }
409
-
410
- }
411
- }
412
- }
413
- });
414
-
415
-
416
- const applyMeshLOD = (key: string, mesh: Mesh, level: number, index: number | undefined, ext: NEEDLE_progressive_mesh_model) => {
417
- const geometry = mesh.geometry as BufferGeometry;
418
- geometry["needle:raycast-mesh"] = true;
419
- // save the low res mesh as a raycast mesh (if we have a geometry and no mesh assigned already)
420
- if (geometry && !getRaycastMesh(mesh)) {
421
- if (debug) console.log("Set raycast mesh", mesh.name, mesh.uuid, geometry);
422
- setRaycastMesh(mesh, geometry);
423
- findResourceUsers(geometry, true).forEach(user => {
424
- if (user instanceof Mesh) {
425
- setRaycastMesh(user, geometry);
426
- }
427
- });
428
- }
429
- if (!geometry.userData) geometry.userData = {};
430
- NEEDLE_progressive.assignLODInformation(geometry, key, level, index, ext.density);
431
- NEEDLE_progressive.lodInfos.set(key, ext);
432
- };
433
- this.parser.json.meshes?.forEach((meshInfo, index: number) => {
434
- if (meshInfo?.extensions) {
435
- const ext = meshInfo?.extensions[EXTENSION_NAME] as NEEDLE_progressive_mesh_model;
436
- if (ext && ext.lods) {
437
- for (const key of this.parser.associations.keys()) {
438
- if (key instanceof Mesh) {
439
- const val = this.parser.associations.get(key) as { meshes: number, primitives: number };
440
- if (val.meshes === index) {
441
- const obj = key;
442
- if (debug) console.log("> Progressive: register mesh", index, obj.name, ext, obj.uuid, obj);
443
- const LODKEY = obj.uuid;
444
- const LODLEVEL = ext.lods.length;
445
- if (obj instanceof Mesh) {
446
- applyMeshLOD(LODKEY, obj, LODLEVEL, val.primitives, ext);
447
- let existing = NEEDLE_progressive.lowresCache.get(LODKEY) as unknown as BufferGeometry[] | undefined;
448
- if (existing) {
449
- existing.push(obj.geometry as BufferGeometry);
450
- }
451
- else existing = [obj.geometry as BufferGeometry];
452
- NEEDLE_progressive.lowresCache.set(LODKEY, existing);
453
- }
454
- }
455
- }
456
- }
457
-
458
- }
459
- }
460
- });
461
-
462
- return null;
463
- }
464
-
465
- /** A map of key = asset uuid and value = LOD information */
466
- private static readonly lodInfos = new Map<string, NEEDLE_progressive_model>();
467
- /** cache of already loaded mesh lods */
468
- private static readonly previouslyLoaded: Map<string, Promise<null | Texture | BufferGeometry | BufferGeometry[]>> = new Map();
469
- /** this contains the geometry/textures that were originally loaded */
470
- private static readonly lowresCache: Map<string, Texture | BufferGeometry[]> = new Map();
471
-
472
- private static async getOrLoadLOD<T extends Texture | BufferGeometry>(
473
- context: Context, source: SourceIdentifier | undefined, current: T & ObjectThatMightHaveLODs, level: number
474
- ): Promise<T | null> {
475
-
476
- const debugverbose = debug == "verbose";
477
-
478
- /** this key is used to lookup the LOD information */
479
- const LOD: LODInformation | undefined = current.userData.LODS;
480
-
481
- if (!LOD) {
482
- return null;
483
- }
484
-
485
- const LODKEY = LOD?.key;
486
-
487
- let progressiveInfo: NEEDLE_progressive_model | undefined;
488
-
489
- // See https://github.com/needle-tools/needle-engine-support/issues/133
490
- if (current instanceof Texture) {
491
- if (current.source && current.source[$progressiveTextureExtension])
492
- progressiveInfo = current.source[$progressiveTextureExtension];
493
- }
494
-
495
-
496
- if (!progressiveInfo) progressiveInfo = NEEDLE_progressive.lodInfos.get(LODKEY);
497
-
498
- if (progressiveInfo) {
499
-
500
- if (level > 0) {
501
- let useLowRes = false;
502
- const hasMultipleLevels = Array.isArray(progressiveInfo.lods);
503
- if (hasMultipleLevels && level >= progressiveInfo.lods.length) {
504
- useLowRes = true;
505
- }
506
- else if (!hasMultipleLevels) {
507
- useLowRes = true;
508
- }
509
- if (useLowRes) {
510
- const lowres = this.lowresCache.get(LODKEY) as T;
511
- return lowres;
512
- }
513
- }
514
-
515
- /** the unresolved LOD url */
516
- const unresolved_lod_url = Array.isArray(progressiveInfo.lods) ? progressiveInfo.lods[level].path : progressiveInfo.lods;
517
-
518
- // check if we have a uri
519
- if (!unresolved_lod_url) {
520
- if (debug && !progressiveInfo["missing:uri"]) {
521
- progressiveInfo["missing:uri"] = true;
522
- console.warn("Missing uri for progressive asset for LOD " + level, progressiveInfo);
523
- }
524
- return null;
525
- }
526
-
527
- /** the resolved LOD url */
528
- let lod_url = resolveUrl(source, unresolved_lod_url);
529
-
530
- // check if the requested file needs to be loaded via a GLTFLoader
531
- if (lod_url.endsWith(".glb") || lod_url.endsWith(".gltf")) {
532
- if (!progressiveInfo.guid) {
533
- console.warn("missing pointer for glb/gltf texture", progressiveInfo);
534
- return null;
535
- }
536
- // check if the requested file has already been loaded
537
- const KEY = lod_url + "_" + progressiveInfo.guid;
538
-
539
- // check if the requested file is currently being loaded
540
- const existing = this.previouslyLoaded.get(KEY);
541
- if (existing !== undefined) {
542
- if (debugverbose) console.log(`LOD ${level} was already loading/loaded: ${KEY}`);
543
- let res = await existing.catch(err => {
544
- console.error(`Error loading LOD ${level} from ${lod_url}\n`, err);
545
- return null;
546
- });
547
- let resouceIsDisposed = false;
548
- if (res == null) {
549
- // if the resource is null the last loading result didnt succeed (maybe because the url doesnt exist)
550
- // in which case we don't attempt to load it again
551
- }
552
- else if (res instanceof Texture && current instanceof Texture) {
553
- // check if the texture has been disposed or not
554
- if (res.image?.data || res.source?.data) {
555
- res = this.copySettings(current, res);
556
- }
557
- // if it has been disposed we need to load it again
558
- else {
559
- resouceIsDisposed = true;
560
- this.previouslyLoaded.delete(KEY);
561
- }
562
- }
563
- else if (res instanceof BufferGeometry && current instanceof BufferGeometry) {
564
- if (res.attributes.position?.array) {
565
- // the geometry is OK
566
- }
567
- else {
568
- resouceIsDisposed = true;
569
- this.previouslyLoaded.delete(KEY);
570
- }
571
- }
572
- if (!resouceIsDisposed) {
573
- return res as T;
574
- }
575
- }
576
-
577
- const ext = progressiveInfo;
578
- const request = new Promise<null | Texture | BufferGeometry | BufferGeometry[]>(async (resolve, _) => {
579
- const loader = new GLTFLoader();
580
- addDracoAndKTX2Loaders(loader, context);
581
-
582
- if (progressiveInfo && Array.isArray(progressiveInfo.lods)) {
583
- const lodinfo = progressiveInfo.lods[level];
584
- if (lodinfo.hash) {
585
- lod_url += "?v=" + lodinfo.hash;
586
- }
587
- }
588
-
589
-
590
- if (debug) {
591
- await delay(Math.random() * 1000);
592
- if (debugverbose) console.warn("Start loading (delayed) " + lod_url, ext.guid);
593
- }
594
-
595
- const gltf = await loader.loadAsync(lod_url).catch(err => {
596
- console.error(`Error loading LOD ${level} from ${lod_url}\n`, err);
597
- return null;
598
- });
599
- if (!gltf) return null;
600
-
601
- const parser = gltf.parser;
602
- if (debugverbose) console.log("Loading finished " + lod_url, ext.guid);
603
- let index = 0;
604
-
605
- if (gltf.parser.json.textures) {
606
- let found = false;
607
- for (const tex of gltf.parser.json.textures) {
608
- // find the texture index
609
- if (tex?.extensions) {
610
- const other: NEEDLE_progressive_model = tex?.extensions[EXTENSION_NAME];
611
- if (other?.guid) {
612
- if (other.guid === ext.guid) {
613
- found = true;
614
- break;
615
- }
616
- }
617
- }
618
- index++;
619
- }
620
- if (found) {
621
- let tex = await parser.getDependency("texture", index) as Texture;
622
- if (debugverbose) console.log("change \"" + current.name + "\" → \"" + tex.name + "\"", lod_url, index, tex, KEY);
623
- if (current instanceof Texture)
624
- tex = this.copySettings(current, tex);
625
- if (tex) {
626
- (tex as any).guid = ext.guid;
627
- }
628
- return resolve(tex);
629
- }
630
- }
631
-
632
- index = 0;
633
-
634
- if (gltf.parser.json.meshes) {
635
- let found = false;
636
- for (const mesh of gltf.parser.json.meshes) {
637
- // find the mesh index
638
- if (mesh?.extensions) {
639
- const other: NEEDLE_progressive_model = mesh?.extensions[EXTENSION_NAME];
640
- if (other?.guid) {
641
- if (other.guid === ext.guid) {
642
- found = true;
643
- break;
644
- }
645
- }
646
- }
647
- index++;
648
- }
649
- if (found) {
650
- const mesh = await parser.getDependency("mesh", index) as Mesh | Group;
651
-
652
- const meshExt = ext as NEEDLE_progressive_mesh_model;
653
-
654
- if (debugverbose) console.log(`Loaded Mesh \"${mesh.name}\"`, lod_url, index, mesh, KEY);
655
-
656
- if (mesh instanceof Mesh) {
657
- const geo = mesh.geometry as BufferGeometry;
658
- NEEDLE_progressive.assignLODInformation(geo, LODKEY, level, undefined, meshExt.density);
659
- return resolve(geo);
660
- }
661
- else {
662
- const geometries = new Array<BufferGeometry>();
663
- for (let i = 0; i < mesh.children.length; i++) {
664
- const child = mesh.children[i];
665
- if (child instanceof Mesh) {
666
- const geo = child.geometry as BufferGeometry;
667
- NEEDLE_progressive.assignLODInformation(geo, LODKEY, level, i, meshExt.density);
668
- geometries.push(geo);
669
- }
670
- }
671
- return resolve(geometries);
672
- }
673
- }
674
- }
675
-
676
- // we could not find a texture or mesh with the given guid
677
- return resolve(null);
678
- });
679
- this.previouslyLoaded.set(KEY, request);
680
- const res = await request;
681
- return res as T;
682
- }
683
- else {
684
- if (current instanceof Texture) {
685
- if (debugverbose) console.log("Load texture from uri: " + lod_url);
686
- const loader = new TextureLoader();
687
- const tex = await loader.loadAsync(lod_url);
688
- if (tex) {
689
- (tex as any).guid = progressiveInfo.guid;
690
- tex.flipY = false;
691
- tex.needsUpdate = true;
692
- tex.colorSpace = current.colorSpace;
693
- if (debugverbose)
694
- console.log(progressiveInfo, tex);
695
- }
696
- else if (debug) console.warn("failed loading", lod_url);
697
- return tex as T;
698
- }
699
- }
700
- }
701
- else {
702
- if (debug)
703
- console.warn(`Can not load LOD ${level}: no LOD info found for \"${LODKEY}\" ${current.name}`, current.type);
704
- }
705
- return null;
706
- }
707
-
708
- private static assignLODInformation(res: DeepWriteable<ObjectThatMightHaveLODs>, key: string, level: number, index?: number, density?: number) {
709
- if (!res) return;
710
- if (!res.userData) res.userData = {};
711
- const info: LODInformation = new LODInformation(key, level, index, density);
712
- res.userData.LODS = info;
713
- res.userData.LOD = level;
714
- }
715
- private static getAssignedLODInformation(res: ObjectThatMightHaveLODs | null | undefined): null | LODInformation {
716
- return res?.userData?.LODS || null;
717
- }
718
-
719
- private static readonly _copiedTextures: WeakMap<Texture, Texture> = new Map();
720
-
721
- private static copySettings(source: Texture, target: Texture): Texture {
722
- // don't copy again if the texture was processed before
723
- const existingClone = this._copiedTextures.get(source);
724
- if (existingClone) {
725
- return existingClone;
726
- }
727
- // We need to clone e.g. when the same texture is used multiple times (but with e.g. different wrap settings)
728
- // This is relatively cheap since it only stores settings
729
- // This should only happen once ever for every texture
730
- target = target.clone();
731
- this._copiedTextures.set(source, target);
732
- // we re-use the offset and repeat settings because it might be animated
733
- target.offset = source.offset;
734
- target.repeat = source.repeat;
735
- target.colorSpace = source.colorSpace;
736
- return target;
737
-
738
- }
739
-
740
-
741
- /** subscribe to events whenever a loading event starts, invoked for every single loading process that starts */
742
- static beginListenStart(context: Context, evt: ProgressiveLoadingEvent) {
743
- if (!this._progressiveEventListeners.has(context)) {
744
- this._progressiveEventListeners.set(context, new ProgressiveLoadingEventHandler());
745
- }
746
- this._progressiveEventListeners.get(context)!.start.push(evt);
747
- }
748
- static stopListenStart(context: Context, evt: ProgressiveLoadingEvent) {
749
- if (!this._progressiveEventListeners.has(context)) {
750
- return;
751
- }
752
- const listeners = this._progressiveEventListeners.get(context)!.start;
753
- const index = listeners.indexOf(evt);
754
- if (index >= 0) {
755
- listeners.splice(index, 1);
756
- }
757
- }
758
-
759
- /** subscribe to loading event ended event */
760
- static beginListenEnd(context: Context, evt: ProgressiveLoadingEvent) {
761
- if (!this._progressiveEventListeners.has(context)) {
762
- this._progressiveEventListeners.set(context, new ProgressiveLoadingEventHandler());
763
- }
764
- this._progressiveEventListeners.get(context)!.end.push(evt);
765
- }
766
- static stopListenEnd(context: Context, evt: ProgressiveLoadingEvent) {
767
- if (!this._progressiveEventListeners.has(context)) {
768
- return;
769
- }
770
- const listeners = this._progressiveEventListeners.get(context)!.end;
771
- const index = listeners.indexOf(evt);
772
- if (index >= 0) {
773
- listeners.splice(index, 1);
774
- }
775
- }
776
-
777
- /** event listeners per context */
778
- private static _progressiveEventListeners: Map<Context, ProgressiveLoadingEventHandler> = new Map();
779
- //** loading info per context, contains an array of urls that are currently being loaded */
780
- private static _currentProgressiveLoadingInfo: Map<Context, ProgressiveLoadingInfo[]> = new Map();
781
-
782
- // called whenever a progressive loading event starts
783
- private static onProgressiveLoadStart(context: Context, source: SourceIdentifier | undefined, material: Mesh | Material | null, slot: string | null): ProgressiveLoadingInfo {
784
- if (!this._currentProgressiveLoadingInfo.has(context)) {
785
- this._currentProgressiveLoadingInfo.set(context, []);
786
- }
787
- const info = new ProgressiveLoadingInfo(context, source, material, slot);
788
- const current = this._currentProgressiveLoadingInfo.get(context)!;
789
- const listener = this._progressiveEventListeners.get(context);
790
- if (listener) listener.onStart(info);
791
-
792
- current.push(info);
793
- return info;
794
- }
795
-
796
- private static onProgressiveLoadEnd(info: ProgressiveLoadingInfo) {
797
- if (!info) return;
798
- const context = info.context;
799
- if (!this._currentProgressiveLoadingInfo.has(context)) {
800
- return;
801
- }
802
- const current = this._currentProgressiveLoadingInfo.get(context)!;
803
- const index = current.indexOf(info);
804
- if (index < 0) {
805
- return;
806
- }
807
- current.splice(index, 1);
808
- const listener = this._progressiveEventListeners.get(context);
809
- if (listener) listener.onEnd(info);
810
- }
811
- }
812
-
813
- declare type ObjectThatMightHaveLODs = { userData?: { LODS?: LODInformation, readonly LOD?: number } };
814
-
815
- // declare type GetLODInformation = () => LODInformation | null;
816
-
817
- class LODInformation {
818
- /** the key to lookup the LOD information */
819
- readonly key: string;
820
- readonly level: number;
821
- /** For multi objects (e.g. a group of meshes) this is the index of the object */
822
- readonly index?: number;
823
- /** the mesh density */
824
- readonly density?: number;
825
-
826
- constructor(key: string, level: number, index?: number, density?: number) {
827
- this.key = key;
828
- this.level = level;
829
- if (index != undefined)
830
- this.index = index;
831
- if (density != undefined)
832
- this.density = density;
833
- }
834
- };
835
-
836
-
837
-
838
- /** info object that holds information about a file that is currently being loaded */
839
- export class ProgressiveLoadingInfo {
840
- readonly context: Context;
841
- readonly source: SourceIdentifier | undefined;
842
- readonly material?: Material | null;
843
- readonly slot?: string | null;
844
- readonly mesh?: Mesh | null;
845
- // TODO: can contain information if the event is a background process / preloading or if the object is currently visible
846
-
847
- constructor(context: Context, source: SourceIdentifier | undefined, object?: Mesh | Material | null, slot?: string | null) {
848
- this.context = context;
849
- this.source = source;
850
- if (object instanceof Mesh) {
851
- this.mesh = object;
852
- }
853
- else
854
- this.material = object;
855
- this.slot = slot;
856
- }
857
- };
858
-
859
-
860
- export declare type ProgressiveLoadingEvent = (info: ProgressiveLoadingInfo) => void;
861
-
862
- /** progressive loading event handler implementation */
863
- class ProgressiveLoadingEventHandler {
864
- start: ProgressiveLoadingEvent[] = [];
865
- end: ProgressiveLoadingEvent[] = [];
866
-
867
- onStart(listener: ProgressiveLoadingInfo) {
868
- for (const l of this.start) {
869
- l(listener);
870
- }
871
- }
872
-
873
- onEnd(listener: ProgressiveLoadingInfo) {
874
- for (const l of this.end) {
875
- l(listener);
876
- }
877
- }
878
- }
1
+ export * from "@needle-tools/gltf-progressive"
src/engine-components/ParticleSystem.ts CHANGED
@@ -102,7 +102,7 @@
102
102
  if (debugProgressiveLoading) {
103
103
  console.log("Load material LOD", material.name);
104
104
  }
105
- NEEDLE_progressive.assignTextureLOD(this.context, this.sourceId!, material);
105
+ NEEDLE_progressive.assignTextureLOD(material, 0);
106
106
  }
107
107
 
108
108
  return material;
src/engine-components/Renderer.ts CHANGED
@@ -29,26 +29,6 @@
29
29
  const debugRenderer = getParam("debugrenderer");
30
30
  const debugskinnedmesh = getParam("debugskinnedmesh");
31
31
  const suppressInstancing = getParam("noinstancing");
32
- let debugProgressiveLoading = getParam("debugprogressive");
33
- const suppressProgressiveLoading = getParam("noprogressive");
34
-
35
- if (debugProgressiveLoading && document) {
36
- onStart(ctx => {
37
- const button = document.createElement("button");
38
- button.innerText = "Prog Debug Mode: " + debugProgressiveLoading;
39
- button.onclick = () => {
40
- if (debugProgressiveLoading == "density")
41
- debugProgressiveLoading = false;
42
- else if (debugProgressiveLoading == false)
43
- debugProgressiveLoading = true;
44
- else if (debugProgressiveLoading)
45
- debugProgressiveLoading = "density";
46
- button.innerText = "Prog Debug Mode: " + debugProgressiveLoading;
47
- };
48
- ctx.menu.appendChild(button);
49
- });
50
- }
51
-
52
32
  const showWireframe = getParam("wireframe");
53
33
 
54
34
  export enum ReflectionProbeUsage {
@@ -620,7 +600,7 @@
620
600
 
621
601
  this.updateReflectionProbe();
622
602
 
623
- this.testIfLODLevelsAreAvailable();
603
+ // this.testIfLODLevelsAreAvailable();
624
604
  }
625
605
 
626
606
  onDisable() {
@@ -701,33 +681,14 @@
701
681
  }
702
682
  }
703
683
 
704
- if (this._wasVisible && this.allowProgressiveLoading && !suppressProgressiveLoading) {
705
- if (this.automaticallyUpdateLODLevel) {
706
- this.updateLODs();
707
- }
708
- // else {
709
- // for (const mat of this.sharedMaterials) {
710
- // if (!mat) continue;
711
- // this.loadProgressiveTextures(mat, 0)
712
- // }
713
- // }
714
- }
715
-
716
684
  if (this.reflectionProbeUsage !== ReflectionProbeUsage.Off && this._reflectionProbe) {
717
685
  this._reflectionProbe.onSet(this);
718
686
  }
719
687
 
720
688
  }
721
689
 
722
- /** true when the object was rendered - this is used to start loading LOD levels */
723
- private _wasVisible = false;
724
-
725
690
  private onBeforeRenderThree = (_renderer, _scene, _camera, _geometry, material, _group) => {
726
691
 
727
- if (!this._wasVisible && this.context.time.frame > this._firstFrame + 1) {
728
- if (debugProgressiveLoading) console.debug("onBeforeRenderThree: Object becomes visible for the first time", this.name, this.context.time.frame);
729
- this._wasVisible = true;
730
- }
731
692
 
732
693
  if (material.envMapIntensity !== undefined) {
733
694
  const factor = this.hasLightmap ? Math.PI : 1;
@@ -780,7 +741,6 @@
780
741
  if (this._isInstancingEnabled && this.handles) {
781
742
  for (let i = 0; i < this.handles.length; i++) {
782
743
  const handle = this.handles[i];
783
- this._wasVisible = true;
784
744
  setCustomVisibility(handle.object, true);
785
745
  }
786
746
  }
@@ -788,437 +748,16 @@
788
748
  if (this.reflectionProbeUsage !== ReflectionProbeUsage.Off && this._reflectionProbe) {
789
749
  this._reflectionProbe.onUnset(this)
790
750
  }
791
-
792
- if (debugProgressiveLoading && this._lastLodLevel >= 0)
793
- this.drawGizmoLodLevel(false);
794
751
  }
795
752
 
796
- private testIfLODLevelsAreAvailable() {
797
- this.automaticallyUpdateLODLevel = false;
798
- this.hasMeshLODs = false;
799
- this.hasTextureLODs = false;
800
-
801
- for (const mesh of this.sharedMeshes) {
802
- if (NEEDLE_progressive.hasLODLevelAvailable(mesh)) {
803
- this.automaticallyUpdateLODLevel = true;
804
- this.hasMeshLODs = true;
805
- break;
806
- }
807
- }
808
- for (const mat of this.sharedMaterials) {
809
- if (!mat) continue;
810
- if (NEEDLE_progressive.hasLODLevelAvailable(mat)) {
811
- this.automaticallyUpdateLODLevel = true;
812
- this.hasTextureLODs = true;
813
- break;
814
- }
815
- }
816
- if (debugProgressiveLoading) {
817
- console.log(`${this.name}\nMesh LODs available? ${this.hasMeshLODs ? "YES" : "NO"}\nTexture LODs available? ${this.hasTextureLODs ? "YES" : "NO"}`);
818
- }
819
- }
820
-
821
- /** Enable to automatically update the LOD level for the renderers meshes and materials.
822
- * When disabled you can manually call `updateLODLevels` to update the LOD levels.
823
- */
824
- automaticallyUpdateLODLevel = true;
825
- private hasTextureLODs: boolean = true;
826
- private hasMeshLODs: boolean = true;
827
-
828
- /** Update the LOD levels for the renderer. */
829
- updateLODs() {
830
- let level = 0;
831
- // we currently only support auto LOD changes for meshes
832
- if (this.hasMeshLODs) {
833
- level = this.calculateLodLevel();
834
- }
835
-
836
- // TODO: we currently can not switch texture lods because we need better caching for the textures internally (see copySettings in progressive + NE-4431)
837
- const textureLOD = 0;// Math.max(0, LODlevel - 2)
838
-
839
- for (const mat of this.sharedMaterials) {
840
- if (mat) this.loadProgressiveTextures(mat, textureLOD);
841
- }
842
-
843
- if (level >= 0) {
844
- for (let i = 0; i < this.sharedMeshes.length; i++) {
845
- const mesh = this.sharedMeshes[i];
846
- if (!mesh) continue;
847
- this.loadProgressiveMeshes(mesh, level);
848
- }
849
- }
850
- }
851
-
852
- private _lastLodLevel = -1;
853
- private _lastScreenCoverage = 0;
854
- private _lastScreenspaceVolume = new Vector3();
855
- private _lastCentrality = 0;
856
- private _nextLodTestTime = 0;
857
- private _randomLodLevelCheckFrameOffset = Math.floor(Math.random() * 100);
858
- private readonly _sphere = new Sphere();
859
- private readonly _box = new Box3();
860
- private static readonly tempMatrix = new Matrix4();
861
-
862
- private calculateLodLevel(force: boolean = false): number {
863
-
864
- // if this is using instancing we always load level 0
865
- // if (this.isInstancingActive) return 0;
866
-
867
- const interval = 3;
868
-
869
- if (!(debugProgressiveLoading == "density")) {
870
- if (!force && (this.context.time.frame + this._randomLodLevelCheckFrameOffset) % interval != 0) {
871
- return this._lastLodLevel;
872
- }
873
-
874
- if (this.context.time.realtimeSinceStartup < this._nextLodTestTime) {
875
- return this._lastLodLevel;
876
- }
877
- }
878
-
879
- /** rough measure of "triangles on quadratic screen" – we're switching LODs based on this metric. */
880
- const desiredDensity = 100_000;
881
- /** highest LOD level we'd ever expect to be generated */
882
- const maxLevel = 10;
883
-
884
- let currentAllowedDensity = desiredDensity;
885
- let level = maxLevel + 1;
886
-
887
- if (this.context.mainCamera) {
888
-
889
- const isLowPerformanceDevice = isMobileDevice();
890
- /** We probably want to make these numbers configurable. */
891
- currentAllowedDensity = desiredDensity * (isLowPerformanceDevice ? 0.7 : 1.0);
892
-
893
- // Experiment: quick & dirty performance-adaptive LODs
894
- /*
895
- if (this.context.time.smoothedFps < 59) {
896
- currentAllowedDensity *= 0.5;
897
- }
898
- else if (this.context.time.smoothedFps >= 59) {
899
- currentAllowedDensity *= 1.25;
900
- }
901
- */
902
-
903
- // TODO: we should save the LOD level in the shared mesh and not just calculate one level per renderer
904
- for (const mesh of this.sharedMeshes) {
905
- if (level <= 0) break;
906
- if (!mesh) continue;
907
-
908
- if (debugProgressiveLoading && mesh["DEBUG:LOD"] != undefined) {
909
- return this._lastLodLevel = mesh["DEBUG:LOD"];
910
- }
911
-
912
- // The mesh info contains also the density for all available LOD level so we can use this for selecting which level to show
913
- const lodsInfo = NEEDLE_progressive.getMeshLODInformation(mesh.geometry);
914
- const lods = lodsInfo?.lods;
915
-
916
- // TODO: we can skip all this if we dont have any LOD information - we can ask the progressive extension for that
917
- const frustum = this.context.mainCameraComponent?.getFrustum();
918
- if (!frustum?.intersectsObject(mesh)) {
919
- if (debugProgressiveLoading && mesh.geometry.boundingSphere) {
920
- const bounds = mesh.geometry.boundingSphere;
921
- this._sphere.copy(bounds);
922
- this._sphere.applyMatrix4(mesh.matrixWorld);
923
- Gizmos.DrawWireSphere(this._sphere.center, this._sphere.radius * 1.01, 0xff5555, .5);
924
- }
925
- // the object is not visible by the camera
926
- continue;
927
- }
928
-
929
- const box = mesh.geometry.boundingBox;
930
- if (box && this.context.mainCamera instanceof PerspectiveCamera) {
931
-
932
- // hack: if the mesh has vertex colors, has less than 100 vertices we always select the highest LOD
933
- if (mesh.geometry.attributes.color && mesh.geometry.attributes.color.count < 100) {
934
- if (mesh.geometry.boundingSphere) {
935
- this._sphere.copy(mesh.geometry.boundingSphere);
936
- this._sphere.applyMatrix4(mesh.matrixWorld);
937
- if (this._sphere.containsPoint(getWorldPosition(this.context.mainCamera))) {
938
- level = 0;
939
- break;
940
- }
941
- }
942
- }
943
-
944
- // calculate size on screen
945
- this._box.copy(box);
946
- this._box.applyMatrix4(mesh.matrixWorld);
947
- const cam = this.context.mainCamera;
948
-
949
- // Converting into projection space has the disadvantage that objects further to the side
950
- // will have a much larger coverage, especially with high-field-of-view situations like in VR.
951
- // Alternatively, we could attempt to calculate angular coverage (some kind of polar coordinates maybe?)
952
- // or introduce a correction factor based on "expected distortion" of the object.
953
- // High distortions would lead to lower LOD levels.
954
- // "Centrality" of the calculated screen-space bounding box could be a factor here –
955
- // what's the distance of the bounding box to the center of the screen?
956
- const mat = this.context.mainCameraComponent!.getProjectionScreenMatrix(Renderer.tempMatrix);
957
- this._box.applyMatrix4(mat);
958
-
959
- // TODO might need to be adjusted for cameras that are rendered during an XR session but are
960
- // actually not XR cameras (e.g. a render texture)
961
- if (this.context.isInXR && cam.fov > 70) {
962
- // calculate centrality of the bounding box - how close is it to the screen center
963
- const min = this._box.min;
964
- const max = this._box.max;
965
-
966
- let minX = min.x;
967
- let minY = min.y;
968
- let maxX = max.x;
969
- let maxY = max.y;
970
-
971
- // enlarge
972
- const enlargementFactor = 2.0;
973
- const centerBoost = 1.5;
974
- const centerX = (min.x + max.x) * 0.5;
975
- const centerY = (min.y + max.y) * 0.5;
976
- minX = (minX - centerX) * enlargementFactor + centerX;
977
- minY = (minY - centerY) * enlargementFactor + centerY;
978
- maxX = (maxX - centerX) * enlargementFactor + centerX;
979
- maxY = (maxY - centerY) * enlargementFactor + centerY;
980
-
981
- const xCentrality = minX < 0 && maxX > 0 ? 0 : Math.min(Math.abs(min.x), Math.abs(max.x));
982
- const yCentrality = minY < 0 && maxY > 0 ? 0 : Math.min(Math.abs(min.y), Math.abs(max.y));
983
- const centrality = Math.max(xCentrality, yCentrality);
984
-
985
- // heuristically determined to lower quality for objects at the edges of vision
986
- this._lastCentrality = (centerBoost - centrality) * (centerBoost - centrality) * (centerBoost - centrality);
987
- }
988
- else {
989
- this._lastCentrality = 1;
990
- }
991
- const boxSize = this._box.getSize(getTempVector());
992
- boxSize.multiplyScalar(0.5); // goes from -1..1, we want -0.5..0.5 for coverage in percent
993
- if (screen.availHeight > 0)
994
- boxSize.multiplyScalar(this.context.domHeight / screen.availHeight); // correct for size of context on screen
995
- boxSize.x *= cam.aspect;
996
-
997
- const matView = cam.matrixWorldInverse;
998
- const box2 = new Box3();
999
- box2.copy(box);
1000
- box2.applyMatrix4(mesh.matrixWorld);
1001
- box2.applyMatrix4(matView);
1002
- const boxSize2 = box2.getSize(getTempVector());
1003
-
1004
- // approximate depth coverage in relation to screenspace size
1005
- const max2 = Math.max(boxSize2.x, boxSize2.y);
1006
- const max1 = Math.max(boxSize.x, boxSize.y);
1007
- if (max1 != 0 && max2 != 0)
1008
- boxSize.z = boxSize2.z / Math.max(boxSize2.x, boxSize2.y) * Math.max(boxSize.x, boxSize.y);
1009
-
1010
- this._lastScreenCoverage = Math.max(boxSize.x, boxSize.y, boxSize.z);
1011
- this._lastScreenspaceVolume.copy(boxSize);
1012
- this._lastScreenCoverage *= this._lastCentrality;
1013
-
1014
- // draw screen size box
1015
- if (debugProgressiveLoading) {
1016
- mat.invert();
1017
-
1018
- // get box corners, transform with camera space, and draw as quad lines
1019
- const corner0 = getTempVector();
1020
- corner0.copy(this._box.min);
1021
- const corner1 = getTempVector();
1022
- corner1.copy(this._box.max);
1023
- corner1.x = corner0.x;
1024
- const corner2 = getTempVector();
1025
- corner2.copy(this._box.max);
1026
- corner2.y = corner0.y;
1027
- const corner3 = getTempVector();
1028
- corner3.copy(this._box.max);
1029
- // draw outlines at the center of the box
1030
- const z = (corner0.z + corner3.z) * 0.5;
1031
- // all outlines should have the same depth in screen space
1032
- corner0.z = corner1.z = corner2.z = corner3.z = z;
1033
-
1034
- corner0.applyMatrix4(mat);
1035
- corner1.applyMatrix4(mat);
1036
- corner2.applyMatrix4(mat);
1037
- corner3.applyMatrix4(mat);
1038
-
1039
- Gizmos.DrawLine(corner0, corner1, 0x0000ff);
1040
- Gizmos.DrawLine(corner0, corner2, 0x0000ff);
1041
- Gizmos.DrawLine(corner1, corner3, 0x0000ff);
1042
- Gizmos.DrawLine(corner2, corner3, 0x0000ff);
1043
- }
1044
-
1045
- let expectedLevel = 999;
1046
- const framerate = this.context.time.smoothedFps;
1047
- if (lods && this._lastScreenCoverage > 0) {
1048
- for (let l = 0; l < lods.length; l++) {
1049
- const densityForThisLevel = lods[l].density;
1050
- if (densityForThisLevel / this._lastScreenCoverage < currentAllowedDensity) {
1051
- expectedLevel = l;
1052
- break;
1053
- }
1054
- }
1055
- }
1056
-
1057
- // expectedLevel -= meshDensity - 5;
1058
- // expectedLevel += meshDensity;
1059
- const isLowerLod = expectedLevel < level;
1060
- if (isLowerLod) {
1061
- level = expectedLevel;
1062
- }
1063
- }
1064
- }
1065
- }
1066
-
1067
- level = Math.round(level);
1068
-
1069
- if (this._lastLodLevel != level) {
1070
- this._nextLodTestTime = this.context.time.realtimeSinceStartup + .5;
1071
- if (debugProgressiveLoading) {
1072
- if (debugProgressiveLoading == "verbose") console.warn(`LOD Level changed from ${this._lastLodLevel} to ${level} for ${this.name}`);
1073
- this.drawGizmoLodLevel(true);
1074
- }
1075
- }
1076
-
1077
- this._lastLodLevel = level;
1078
- return level;
1079
- }
1080
-
1081
- private drawGizmoLodLevel(changed: boolean) {
1082
- // Will be (maxLod + 1) (11) if no lod level is found
1083
- const _level = this._lastLodLevel;
1084
-
1085
- const cam = this.context.mainCamera as any as PerspectiveCamera;
1086
- const camGO = cam as any as GameObject;
1087
- const camForward = camGO.worldForward;
1088
- const camWorld = camGO.worldPosition;
1089
-
1090
- for (const mesh of this.sharedMeshes) {
1091
- if (!mesh) continue;
1092
- if (mesh.geometry.boundingSphere) {
1093
- const bounds = mesh.geometry.boundingSphere;
1094
- this._sphere.copy(bounds);
1095
- this._sphere.applyMatrix4(mesh.matrixWorld);
1096
- const boundsCenter = this._sphere.center;
1097
- const radius = this._sphere.radius;
1098
- const colors = ["#76c43e", "#bcc43e", "#c4ac3e", "#c4673e", "#ff3e3e"];
1099
- // if the lod has changed we just want to draw the gizmo for the changed mesh
1100
- if (changed) {
1101
- Gizmos.DrawWireSphere(boundsCenter, radius, colors[_level], .1);
1102
- }
1103
- else {
1104
- // Mesh Density is calculated as: triangle count per square meter of surface area, normalized to the bounding box size of the model.
1105
- // Our goal for automatic switching of LODs is that the resulting triangle count per screen area is constant.
1106
- // We assume a uniform distribution of triangles over the surface area; which means that
1107
- // we can express a ratio of "screen area to surface area".
1108
- const triangleCount = mesh.geometry.index!.count / 3;
1109
- const lods = NEEDLE_progressive.getMeshLODInformation(mesh.geometry)?.lods;
1110
- const level = lods ? Math.min(lods?.length - 1, _level) : 0;
1111
- let allLods = "";
1112
- if (lods && this._lastScreenCoverage > 0) {
1113
- for (let i = 0; i < lods.length; i++) {
1114
- const d = lods[i].density;
1115
- const last = i == lods.length - 1;
1116
- allLods += d.toFixed(0) + ">" + (d / this._lastScreenCoverage).toFixed(0) + (last ? "" : ",");
1117
- }
1118
- }
1119
- const density = lods ? lods[level]?.density : -1;
1120
- const box = mesh.geometry.boundingBox;
1121
- const boxSize = box ? box.getSize(getTempVector()) : new Vector3();
1122
- const maxBoxSize = Math.max(boxSize.x, boxSize.y, boxSize.z);
1123
-
1124
- // Surface area is in local space of the model;
1125
- // we need to scale it by the model's world scale and the model's geometry bounding box size.
1126
- const ws = mesh.getWorldScale(getTempVector());
1127
- const wsMedian = (ws.x + ws.y + ws.z) / 3;
1128
- // Area is squared, so both maxBoxSize and wsMedian are squared here
1129
- // Here, we're basically reverting the calculations that have happened in the pipeline for debugging.
1130
- const surfaceArea = 1 / density * triangleCount * (maxBoxSize * maxBoxSize) * (wsMedian * wsMedian);
1131
- let text = "LOD " + level;
1132
- if(debugProgressiveLoading == "density") {
1133
- text +=
1134
- "\n" + triangleCount + " tris" +
1135
- // This is key – basically how we're switching
1136
- "\n" + (density / this._lastScreenCoverage).toFixed(0) + " dens" +
1137
- "\n" + (this._lastScreenCoverage * 100).toFixed(1) + "% cov" +
1138
- // "\n" + (this._lastScreenspaceVolume.x.toFixed(2) + "x" + this._lastScreenspaceVolume.y.toFixed(2) + "x" + this._lastScreenspaceVolume.z.toFixed(2)) + " vol" +
1139
- // + "\n" + (surfaceArea).toFixed(2) + " m2" +
1140
- "\n" + (this._lastCentrality * 100).toFixed(1) + "% centr" +
1141
- "\n" + (this._box.min.x.toFixed(2) + "-" + this._box.max.x.toFixed(2) + "x" + this._box.min.y.toFixed(2) + "-" + this._box.max.y.toFixed(2)) + " scr" +
1142
- // "\n" + (ws.x).toFixed(2) + "x" + " " + maxBoxSize.toFixed(2) + "b" + "\n" +
1143
- // allLods + "\n" +
1144
- //"----" + "\n" +
1145
- // "1000" + " ideal dens"
1146
- "";
1147
- }
1148
-
1149
- // if (helper) {
1150
- // helper?.setText(text);
1151
- // continue;
1152
- // }
1153
- const fwd = getTempVector(camForward);
1154
- // for debugging very close LDOs, we need to flip the radius...
1155
- const pos = fwd.multiplyScalar(radius * .7).add(boundsCenter);
1156
- const distance = pos.distanceTo(camWorld);
1157
- // const vertexCount = mesh.geometry.index!.count / 3;
1158
- // const vertexCountFactor = Math.min(1, vertexCount / 1000);
1159
- const col = colors[Math.min(colors.length - 1, level)] + "88";
1160
- // const size = Math.min(10, radius);
1161
- const windowScale = this.context.domHeight > 0 ? screen.height / this.context.domHeight : 1;
1162
- const fieldOfViewScale = Math.tan(cam.fov * Math.PI / 180 / 2);
1163
- Gizmos.DrawLabel(pos, text, distance * .01 * windowScale * fieldOfViewScale, undefined, 0xffffff, col);
1164
- // mesh["LOD_level_label"] = helper;
1165
- }
1166
-
1167
- }
1168
- }
1169
- }
1170
-
1171
753
  /** Applies stencil settings for this renderer's objects (if stencil settings are available) */
1172
754
  applyStencil() {
1173
755
  NEEDLE_render_objects.applyStencil(this);
1174
756
  }
1175
757
 
1176
758
 
1177
- /** Load progressive textures for the given material
1178
- * @param material the material to load the textures for
1179
- * @param level the LOD level to load. Level 0 is the best quality, higher levels are lower quality
1180
- * @returns Promise with true if the LOD was loaded, false if not
1181
- */
1182
- loadProgressiveTextures(material: Material, level: number): Promise<ProgressiveMaterialTextureLoadingResult[] | Texture | null> {
1183
- if (!material) return Promise.resolve(null);
1184
- if (material.userData && material.userData.LOD !== level) {
1185
- material.userData.LOD = level;
1186
- return NEEDLE_progressive.assignTextureLOD(this.context, this.sourceId!, material, level);
1187
- }
1188
- return Promise.resolve(null);
1189
- }
1190
759
 
1191
- /** Load progressive meshes for the given mesh
1192
- * @param mesh the mesh to load the LOD for
1193
- * @param index the index of the mesh if it's part of a group
1194
- * @param level the LOD level to load. Level 0 is the best quality, higher levels are lower quality
1195
- * @returns Promise with true if the LOD was loaded, false if not
1196
- */
1197
- loadProgressiveMeshes(mesh: Mesh, level: number) : Promise<BufferGeometry | null> {
1198
- if (!mesh) return Promise.resolve(null);
1199
- if (!mesh.userData) mesh.userData = {};
1200
- if (mesh.userData.LOD !== level) {
1201
- mesh.userData.LOD = level;
1202
- const originalGeometry = mesh.geometry;
1203
- return NEEDLE_progressive.assignMeshLOD(this.context, this.sourceId!, mesh, level).then(res => {
1204
- if (res && mesh.userData.LOD == level && originalGeometry != mesh.geometry) {
1205
- // update the lightmap
1206
- this.applyLightmapping();
1207
760
 
1208
- if (this.handles) {
1209
- for (const inst of this.handles) {
1210
- // if (inst["LOD"] < level) continue;
1211
- // inst["LOD"] = level;
1212
- inst.setGeometry(mesh.geometry);
1213
- }
1214
- }
1215
- }
1216
- return res;
1217
- })
1218
- }
1219
- return Promise.resolve(null);
1220
- }
1221
-
1222
761
  /** Apply the settings of this renderer to the given object
1223
762
  * Settings include shadow casting and receiving (e.g. this.receiveShadows, this.shadowCastingMode)
1224
763
  */
src/engine-components/RendererInstancing.ts CHANGED
@@ -28,7 +28,7 @@
28
28
  renderer.applySettings(obj);
29
29
  const res = this.tryCreateOrAddInstance(obj, context, args);
30
30
  if (res) {
31
- renderer.loadProgressiveTextures(res.renderer.material, 0);
31
+ NEEDLE_progressive.assignTextureLOD(res.renderer.material, 0);
32
32
  // renderer.loadProgressiveMeshes(res.instancer.mesh, 0);
33
33
  if (handlesArray === null) handlesArray = [];
34
34
  handlesArray.push(res);
src/engine-components/ScreenCapture.ts CHANGED
@@ -45,17 +45,20 @@
45
45
  @serializable()
46
46
  allowStartOnClick: boolean = true;
47
47
 
48
+ /** @internal */
48
49
  onPointerEnter() {
49
50
  if (this.context.connection.allowEditing == false) return;
50
51
  if (!this.allowStartOnClick) return;
51
52
  this.context.input.setCursorPointer();
52
53
  }
54
+ /** @internal */
53
55
  onPointerExit() {
54
56
  if (this.context.connection.allowEditing == false) return;
55
57
  if (!this.allowStartOnClick) return;
56
58
  this.context.input.setCursorNormal();
57
59
  }
58
60
 
61
+ /** @internal */
59
62
  onPointerClick(evt: PointerEventData) {
60
63
  if (this.context.connection.allowEditing == false) return;
61
64
  if (!this.allowStartOnClick) return;
@@ -130,6 +133,7 @@
130
133
  private _currentStream: MediaStream | null = null;
131
134
  private _currentMode: ScreenCaptureMode = ScreenCaptureMode.Idle;
132
135
 
136
+ /** @internal */
133
137
  awake() {
134
138
  if (debug)
135
139
  console.log("Screensharing", this.name, this);
@@ -143,6 +147,7 @@
143
147
  this._net = new NetworkedStreams(this.context, handle);
144
148
  }
145
149
 
150
+ /** @internal */
146
151
  onEnable(): void {
147
152
  this._net?.enable();
148
153
  //@ts-ignore
@@ -159,6 +164,7 @@
159
164
  }
160
165
  }
161
166
 
167
+ /** @internal */
162
168
  onDisable(): void {
163
169
  //@ts-ignore
164
170
  this._net?.removeEventListener(NetworkedStreamEvents.StreamReceived, this.onReceiveStream);
src/engine-components/SpriteRenderer.ts CHANGED
@@ -146,7 +146,7 @@
146
146
  @serializable()
147
147
  index: number = 0;
148
148
 
149
- update(context: Context, sourceId: string | undefined, material: Material | undefined) {
149
+ update(material: Material | undefined) {
150
150
  if (!this.spriteSheet) return;
151
151
  const index = this.index;
152
152
  if (index < 0 || index >= this.spriteSheet.sprites.length)
@@ -163,7 +163,7 @@
163
163
  if (!sprite["__hasLoadedProgressive"]) {
164
164
  sprite["__hasLoadedProgressive"] = true;
165
165
  const previousTexture = tex;
166
- NEEDLE_progressive.assignTextureLOD(context, sourceId!, tex, 0).then(res => {
166
+ NEEDLE_progressive.assignTextureLOD(tex, 0).then(res => {
167
167
  if (res instanceof Texture) {
168
168
  sprite.texture = res;
169
169
  const shouldUpdateInMaterial = material?.["map"] === previousTexture;
@@ -300,7 +300,7 @@
300
300
  }
301
301
  this.sharedMaterial = mat;
302
302
  this._currentSprite = new Mesh(SpriteUtils.getOrCreateGeometry(sprite), mat);
303
- NEEDLE_progressive.assignTextureLOD(this.context, this.sourceId!, mat, 0);
303
+ NEEDLE_progressive.assignTextureLOD(mat, 0);
304
304
  }
305
305
  else {
306
306
  this._currentSprite.geometry = SpriteUtils.getOrCreateGeometry(sprite);
@@ -323,6 +323,6 @@
323
323
  this.sharedMaterial.transparent = this.transparent;
324
324
  }
325
325
  this._currentSprite.castShadow = this.castShadows;
326
- this._spriteSheet?.update(this.context, this.sourceId, this.sharedMaterial);
326
+ this._spriteSheet?.update( this.sharedMaterial);
327
327
  }
328
328
  }
src/engine-components/export/usdz/USDZExporter.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { NEEDLE_progressive } from "@needle-tools/gltf-progressive";
1
2
  import { Matrix4, Mesh, Object3D, Quaternion, Vector3 } from "three";
2
3
 
3
4
  import { showBalloonMessage, showBalloonWarning } from "../../../engine/debug/index.js";
@@ -299,11 +300,9 @@
299
300
  let progressiveTasks = 0;
300
301
  // TODO: it would be better to directly integrate this into the exporter and *on export* request the correct LOD level for textures and meshes instead of relying on the renderer etc
301
302
  for (const rend of renderers) {
302
- rend["didAutomaticallyUpdateLODLevel"] = rend.automaticallyUpdateLODLevel;
303
- rend.automaticallyUpdateLODLevel = false;
304
303
  for (const mesh of rend.sharedMeshes) {
305
304
  if (mesh) {
306
- const task = rend.loadProgressiveMeshes(mesh, 0);
305
+ const task = NEEDLE_progressive.assignMeshLOD(mesh, 0);
307
306
  if (task instanceof Promise)
308
307
  progressiveLoading.push(new Promise<void>((resolve, reject) => {
309
308
  task.then(() => {
@@ -316,7 +315,7 @@
316
315
  }
317
316
  for (const mat of rend.sharedMaterials) {
318
317
  if (mat) {
319
- const task = rend.loadProgressiveTextures(mat, 0);
318
+ const task = NEEDLE_progressive.assignTextureLOD(mat, 0);
320
319
  if (task instanceof Promise)
321
320
  progressiveLoading.push(new Promise<void>((resolve, reject) => {
322
321
  task.then(() => {
@@ -415,12 +414,6 @@
415
414
  for (const go of implicitBehaviors) {
416
415
  GameObject.destroy(go);
417
416
  }
418
- // restore renderer state
419
- for (const rend of renderers) {
420
- const prevState = rend["didAutomaticallyUpdateLODLevel"];
421
- if (prevState != undefined)
422
- rend.automaticallyUpdateLODLevel = prevState;
423
- }
424
417
 
425
418
  // restore XR flags
426
419
  XRState.Global.Set(currentXRState);
src/engine-components/webxr/controllers/XRControllerModel.ts CHANGED
@@ -104,7 +104,7 @@
104
104
  if (!entry) return;
105
105
 
106
106
  this._models.splice(indexInArray, 1);
107
-
107
+
108
108
  if (entry.handmesh) {
109
109
  entry.handmesh.handModel?.removeFromParent();
110
110
  entry.handmesh = undefined;
@@ -272,7 +272,7 @@
272
272
  if (NeedleXRSession.active?.isPassThrough)
273
273
  this.makeOccluder(child);
274
274
  if (child instanceof Mesh) {
275
- NEEDLE_progressive.assignMeshLOD(context, this.sourceId!, child, 0);
275
+ NEEDLE_progressive.assignMeshLOD(child, 0);
276
276
  }
277
277
  });
278
278
  if (!controller.connected) {
src/engine/engine_lods.ts ADDED
@@ -0,0 +1,162 @@
1
+ import { LODsManager as _LODsManager, NEEDLE_progressive,NEEDLE_progressive_mesh_model, NEEDLE_progressive_plugin } from "@needle-tools/gltf-progressive";
2
+ import { Box3, BufferGeometry, Camera, Mesh, PerspectiveCamera, Scene, Sphere, Vector3, WebGLRenderer } from "three";
3
+
4
+ import { findResourceUsers } from "./engine_assetdatabase.js";
5
+ import type { Context } from "./engine_context.js";
6
+ import { Gizmos } from "./engine_gizmos.js";
7
+ import { getRaycastMesh, setRaycastMesh } from "./engine_physics.js";
8
+ import { getTempVector } from "./engine_three_utils.js";
9
+ import { IGameObject } from "./engine_types.js";
10
+ import { getParam } from "./engine_utils.js";
11
+
12
+ const debug = getParam("debugprogressive");
13
+
14
+ const _tempBox: Box3 = new Box3();
15
+ const _tempSphere: Sphere = new Sphere();
16
+
17
+ /**
18
+ * Needle Engine LODs manager. Wrapper around the internal LODs manager.
19
+ */
20
+ export class LODsManager implements NEEDLE_progressive_plugin {
21
+ readonly context: Context;
22
+ private _lodsManager?: _LODsManager;
23
+
24
+ constructor(context: Context) {
25
+ this.context = context;
26
+ }
27
+
28
+ /** @internal */
29
+ setRenderer(renderer: WebGLRenderer) {
30
+ this._lodsManager?.disable();
31
+ this._lodsManager = new _LODsManager(renderer);
32
+ this._lodsManager.plugins.push(this);
33
+ this._lodsManager.enable();
34
+ _LODsManager.debugDrawLine = Gizmos.DrawLine;
35
+ }
36
+
37
+ /** @internal */
38
+ onBeforeGetLODMesh(mesh: Mesh): void {
39
+ if (!getRaycastMesh(mesh)) {
40
+ setRaycastMesh(mesh, mesh.geometry as BufferGeometry);
41
+ }
42
+ }
43
+
44
+ /** @internal */
45
+ onRegisteredNewMesh(mesh: Mesh, _ext: NEEDLE_progressive_mesh_model): void {
46
+ const geometry = mesh.geometry as BufferGeometry;
47
+ if (geometry) {
48
+ geometry["needle:raycast-mesh"] = true;
49
+ if (!getRaycastMesh(mesh)) {
50
+ if (debug) console.log("Set raycast mesh", mesh.name, mesh.uuid, geometry);
51
+ setRaycastMesh(mesh, geometry);
52
+ findResourceUsers(geometry, true).forEach(user => {
53
+ if (user instanceof Mesh) {
54
+ setRaycastMesh(user, geometry);
55
+ }
56
+ });
57
+ }
58
+ }
59
+ }
60
+
61
+
62
+
63
+ /** @internal */
64
+ onAfterUpdatedLOD(_renderer: WebGLRenderer, _scene: Scene, camera: Camera, mesh: Mesh, level: number): void {
65
+ if(debug) this.onRenderDebug(camera, mesh, level);
66
+ }
67
+
68
+ private onRenderDebug(camera: Camera, mesh: Mesh, level: number) {
69
+
70
+ if (!mesh.geometry) return;
71
+ if (!NEEDLE_progressive.hasLODLevelAvailable(mesh.geometry)) return;
72
+
73
+ const state = _LODsManager.getObjectLODState(mesh);
74
+ if (!state) return;
75
+
76
+
77
+ const changed = level != state.lastLodLevel;;
78
+
79
+ if (debug && mesh.geometry.boundingSphere) {
80
+ const bounds = mesh.geometry.boundingSphere;
81
+ _tempSphere.copy(bounds);
82
+ _tempSphere.applyMatrix4(mesh.matrixWorld);
83
+ const boundsCenter = _tempSphere.center;
84
+ const radius = _tempSphere.radius;
85
+ const colors = ["#76c43e", "#bcc43e", "#c4ac3e", "#c4673e", "#ff3e3e"];
86
+ // if the lod has changed we just want to draw the gizmo for the changed mesh
87
+ if (changed) {
88
+ Gizmos.DrawWireSphere(boundsCenter, radius, colors[level], .1);
89
+ }
90
+ else {
91
+ // Mesh Density is calculated as: triangle count per square meter of surface area, normalized to the bounding box size of the model.
92
+ // Our goal for automatic switching of LODs is that the resulting triangle count per screen area is constant.
93
+ // We assume a uniform distribution of triangles over the surface area; which means that
94
+ // we can express a ratio of "screen area to surface area".
95
+ const triangleCount = mesh.geometry.index?.count ?? 0 / 3;
96
+ const lods = NEEDLE_progressive.getMeshLODInformation(mesh.geometry)?.lods;
97
+ level = lods ? Math.min(lods?.length - 1, level) : 0;
98
+ let allLods = "";
99
+ if (lods && state.lastScreenCoverage > 0) {
100
+ for (let i = 0; i < lods.length; i++) {
101
+ const d = lods[i].density;
102
+ const last = i == lods.length - 1;
103
+ allLods += d.toFixed(0) + ">" + (d / state.lastScreenCoverage).toFixed(0) + (last ? "" : ",");
104
+ }
105
+ }
106
+ const density = lods ? lods[level]?.density : -1;
107
+
108
+ // const box = mesh.geometry.boundingBox;
109
+ // const boxSize = box ? box.getSize(getTempVector()) : new Vector3();
110
+ // const maxBoxSize = Math.max(boxSize.x, boxSize.y, boxSize.z);
111
+
112
+ // Surface area is in local space of the model;
113
+ // we need to scale it by the model's world scale and the model's geometry bounding box size.
114
+ // const ws = mesh.getWorldScale(getTempVector());
115
+ // const wsMedian = (ws.x + ws.y + ws.z) / 3;
116
+ // Area is squared, so both maxBoxSize and wsMedian are squared here
117
+ // Here, we're basically reverting the calculations that have happened in the pipeline for debugging.
118
+ // const surfaceArea = 1 / density * triangleCount * (maxBoxSize * maxBoxSize) * (wsMedian * wsMedian);
119
+ let text = "LOD " + level;
120
+ if (debug == "density") {
121
+ text +=
122
+ "\n" + triangleCount + " tris" +
123
+ // This is key – basically how we're switching
124
+ "\n" + (density / state.lastScreenCoverage).toFixed(0) + " dens" +
125
+ "\n" + (state.lastScreenCoverage * 100).toFixed(1) + "% cov" +
126
+ // "\n" + (this._lastScreenspaceVolume.x.toFixed(2) + "x" + this._lastScreenspaceVolume.y.toFixed(2) + "x" + this._lastScreenspaceVolume.z.toFixed(2)) + " vol" +
127
+ // + "\n" + (surfaceArea).toFixed(2) + " m2" +
128
+ "\n" + (state.lastCentrality * 100).toFixed(1) + "% centr" +
129
+ "\n" + (_tempBox.min.x.toFixed(2) + "-" + _tempBox.max.x.toFixed(2) + "x" + _tempBox.min.y.toFixed(2) + "-" + _tempBox.max.y.toFixed(2)) + " scr" +
130
+ // "\n" + (ws.x).toFixed(2) + "x" + " " + maxBoxSize.toFixed(2) + "b" + "\n" +
131
+ // allLods + "\n" +
132
+ //"----" + "\n" +
133
+ // "1000" + " ideal dens"
134
+ "";
135
+ }
136
+
137
+ // if (helper) {
138
+ // helper?.setText(text);
139
+ // continue;
140
+ // }
141
+ const cam = camera as any as IGameObject;
142
+ const camForward = cam.worldForward;
143
+ const camWorld = cam.worldPosition;
144
+
145
+ const fwd = getTempVector(camForward);
146
+ // for debugging very close LDOs, we need to flip the radius...
147
+ const pos = fwd.multiplyScalar(radius * .7).add(boundsCenter);
148
+ const distance = pos.distanceTo(camWorld);
149
+ // const vertexCount = mesh.geometry.index!.count / 3;
150
+ // const vertexCountFactor = Math.min(1, vertexCount / 1000);
151
+ const col = colors[Math.min(colors.length - 1, level)] + "88";
152
+ // const size = Math.min(10, radius);
153
+ const windowScale = this.context.domHeight > 0 ? screen.height / this.context.domHeight : 1;
154
+ const fieldOfViewScale = (camera as PerspectiveCamera).isPerspectiveCamera ? Math.tan((camera as PerspectiveCamera).fov * Math.PI / 180 / 2) : 1;
155
+ Gizmos.DrawLabel(pos, text, distance * .01 * windowScale * fieldOfViewScale, undefined, 0xffffff, col);
156
+ // mesh["LOD_level_label"] = helper;
157
+ }
158
+
159
+ }
160
+ }
161
+
162
+ }