@needle-tools/gltf-progressive 3.5.0-rc → 3.6.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,4 @@
1
- import { Camera, Material, Object3D, Scene, Texture, Vector3, WebGLRenderer } from "three";
1
+ import { Camera, Color, Material, Object3D, Scene, Texture, Vector3, WebGLRenderer } from "three";
2
2
  import { NEEDLE_progressive_plugin } from "./plugins/plugin.js";
3
3
  import { PromiseGroupOptions } from "./lods.promise.js";
4
4
  export type LODManagerContext = {
@@ -8,6 +8,7 @@ export declare type LOD_Results = {
8
8
  mesh_lod: number;
9
9
  texture_lod: number;
10
10
  };
11
+ export declare const lodDebugColors: number[];
11
12
  declare type LODChangedEventListener = (args: {
12
13
  type: "mesh" | "texture";
13
14
  level: number;
@@ -98,7 +99,31 @@ export declare class LODsManager {
98
99
  private readonly _newPromiseGroups;
99
100
  private _promiseGroupIds;
100
101
  /**
101
- * Call to await LODs loading during the next render cycle.
102
+ * Returns a promise that resolves once all LOD requests initiated during the next render cycles have finished loading.
103
+ * This is useful for hiding low-resolution placeholders (e.g. with a loading overlay or CSS blur) until high-quality assets are ready.
104
+ *
105
+ * By default, the returned promise captures LOD loading requests for 2 frames and resolves when all of them complete.
106
+ * Use `waitForFirstCapture` if no LOD requests may happen immediately (e.g. after a scene switch).
107
+ *
108
+ * @param opts - Optional configuration for how long to capture and what to wait for. See {@link PromiseGroupOptions}.
109
+ * @returns A promise that resolves with `{ cancelled, awaited_count, resolved_count }` once all captured LOD loads complete (or the signal aborts).
110
+ *
111
+ * @example
112
+ * ```ts
113
+ * // Wait for initial LODs to finish loading, then remove a blur overlay
114
+ * const result = await lodsManager.awaitLoading({
115
+ * frames: 5,
116
+ * signal: AbortSignal.timeout(10_000),
117
+ * });
118
+ * console.log(`Loaded ${result.resolved_count} of ${result.awaited_count} LODs`);
119
+ * document.querySelector('.blur-overlay')?.remove();
120
+ * ```
121
+ *
122
+ * @example
123
+ * ```ts
124
+ * // Wait until at least one LOD starts loading before resolving
125
+ * await lodsManager.awaitLoading({ waitForFirstCapture: true });
126
+ * ```
102
127
  */
103
128
  awaitLoading(opts?: PromiseGroupOptions): Promise<{
104
129
  cancelled: boolean;
@@ -107,8 +132,29 @@ export declare class LODsManager {
107
132
  }>;
108
133
  private _postprocessPromiseGroups;
109
134
  private readonly _lodchangedlisteners;
110
- addEventListener(evt: "changed", listener: LODChangedEventListener): void;
111
- removeEventListener(evt: "changed", listener: LODChangedEventListener): void;
135
+ /**
136
+ * Register a listener that is called whenever a mesh or texture LOD level has finished loading and has been applied.
137
+ * The listener receives the type of asset (`"mesh"` or `"texture"`), the new LOD level, and the affected object.
138
+ *
139
+ * @param evt - The event type. Currently only `"changed"` is supported.
140
+ * @param listener - Callback invoked after a LOD swap completes.
141
+ * @return A function to unregister the listener.
142
+ *
143
+ * @example
144
+ * ```ts
145
+ * lodsManager.addEventListener("changed", ({ type, level, object }) => {
146
+ * console.log(`${type} LOD changed to level ${level}`, object);
147
+ * });
148
+ * ```
149
+ */
150
+ addEventListener(evt: "changed", listener: LODChangedEventListener): () => void;
151
+ /**
152
+ * Remove a previously registered `"changed"` event listener.
153
+ * @param evt - The event type (`"changed"`).
154
+ * @param listener - The listener to remove.
155
+ * @return `true` if the listener was found and removed, `false` otherwise.
156
+ */
157
+ removeEventListener(evt: "changed", listener: LODChangedEventListener): boolean;
112
158
  private constructor();
113
159
  private _fpsBuffer;
114
160
  /**
@@ -116,6 +162,21 @@ export declare class LODsManager {
116
162
  */
117
163
  enable(): void;
118
164
  disable(): void;
165
+ /**
166
+ * Manually trigger a LOD update for a scene and camera.
167
+ * Only needed when {@link manual} is set to `true` — otherwise LOD updates happen automatically on each render call.
168
+ *
169
+ * @param scene - The scene containing objects with progressive LODs.
170
+ * @param camera - The camera used to determine screen coverage and LOD levels.
171
+ *
172
+ * @example
173
+ * ```ts
174
+ * const lodsManager = LODsManager.get(renderer);
175
+ * lodsManager.manual = true;
176
+ * // ... later, trigger an update at a specific point:
177
+ * lodsManager.update(scene, camera);
178
+ * ```
179
+ */
119
180
  update(scene: Scene, camera: Camera): void;
120
181
  private onAfterRender;
121
182
  /**
@@ -162,4 +223,5 @@ declare class LOD_state {
162
223
  readonly lastScreenspaceVolume: Vector3;
163
224
  lastCentrality: number;
164
225
  }
226
+ export declare function getLODColor(level: number, target: Color): Color;
165
227
  export {};
@@ -1,4 +1,4 @@
1
- import { Box3, Clock, Matrix4, Mesh, MeshStandardMaterial, Sphere, Vector3 } from "three";
1
+ import { Box3, Clock, Color, Matrix4, Mesh, Sphere, Vector3 } from "three";
2
2
  import { NEEDLE_progressive } from "./extension.js";
3
3
  import { createLoaders } from "./loaders.js";
4
4
  import { getParam, isDevelopmentServer, isMobileDevice } from "./utils.internal.js";
@@ -7,11 +7,47 @@ import { getRaycastMesh } from "./utils.js";
7
7
  import { applyDebugSettings, debug, debug_OverrideLodLevel } from "./lods.debug.js";
8
8
  import { PromiseGroup } from "./lods.promise.js";
9
9
  const debugProgressiveLoading = getParam("debugprogressive");
10
+ const debugProgressiveLODColors = debugProgressiveLoading === "colors";
10
11
  const suppressProgressiveLoading = getParam("noprogressive");
11
12
  const $lodsManager = Symbol("Needle:LODSManager");
12
13
  const $lodstate = Symbol("Needle:LODState");
13
14
  const $currentLOD = Symbol("Needle:CurrentLOD");
14
15
  const levels = { mesh_lod: -1, texture_lod: -1 };
16
+ const debugLODColor = new Color();
17
+ export const lodDebugColors = [
18
+ 0x35d05f,
19
+ 0xa8d83a,
20
+ 0xf3d13b,
21
+ 0xf29332,
22
+ 0xf0523b,
23
+ 0xa856f0,
24
+ 0x49a7f2,
25
+ 0x32d7c4,
26
+ 0xff6b9d,
27
+ 0x6f7df7,
28
+ 0xd66fd2,
29
+ 0x35a853,
30
+ 0xb7a51f,
31
+ 0xe05d2f,
32
+ 0x3c78d8,
33
+ 0x00a6a6,
34
+ 0xd7263d,
35
+ 0x7f52ff,
36
+ 0x46b450,
37
+ 0xf7a531,
38
+ 0x2f9be0,
39
+ 0xb84592,
40
+ 0x8a9a2a,
41
+ 0x1f6f8b,
42
+ 0xf05a9d,
43
+ 0x9b5de5,
44
+ 0x00bbf9,
45
+ 0x00f5d4,
46
+ 0xfee440,
47
+ 0xf15bb5,
48
+ 0x4d908e,
49
+ 0x555555,
50
+ ];
15
51
  /**
16
52
  * The LODsManager class is responsible for managing the LODs and progressive assets in the scene. It will automatically update the LODs based on the camera position, screen coverage and mesh density of the objects.
17
53
  * It must be enabled by calling the `enable` method.
@@ -116,7 +152,31 @@ export class LODsManager {
116
152
  _newPromiseGroups = [];
117
153
  _promiseGroupIds = 0;
118
154
  /**
119
- * Call to await LODs loading during the next render cycle.
155
+ * Returns a promise that resolves once all LOD requests initiated during the next render cycles have finished loading.
156
+ * This is useful for hiding low-resolution placeholders (e.g. with a loading overlay or CSS blur) until high-quality assets are ready.
157
+ *
158
+ * By default, the returned promise captures LOD loading requests for 2 frames and resolves when all of them complete.
159
+ * Use `waitForFirstCapture` if no LOD requests may happen immediately (e.g. after a scene switch).
160
+ *
161
+ * @param opts - Optional configuration for how long to capture and what to wait for. See {@link PromiseGroupOptions}.
162
+ * @returns A promise that resolves with `{ cancelled, awaited_count, resolved_count }` once all captured LOD loads complete (or the signal aborts).
163
+ *
164
+ * @example
165
+ * ```ts
166
+ * // Wait for initial LODs to finish loading, then remove a blur overlay
167
+ * const result = await lodsManager.awaitLoading({
168
+ * frames: 5,
169
+ * signal: AbortSignal.timeout(10_000),
170
+ * });
171
+ * console.log(`Loaded ${result.resolved_count} of ${result.awaited_count} LODs`);
172
+ * document.querySelector('.blur-overlay')?.remove();
173
+ * ```
174
+ *
175
+ * @example
176
+ * ```ts
177
+ * // Wait until at least one LOD starts loading before resolving
178
+ * await lodsManager.awaitLoading({ waitForFirstCapture: true });
179
+ * ```
120
180
  */
121
181
  awaitLoading(opts) {
122
182
  const id = this._promiseGroupIds++;
@@ -145,17 +205,46 @@ export class LODsManager {
145
205
  }
146
206
  }
147
207
  _lodchangedlisteners = [];
208
+ /**
209
+ * Register a listener that is called whenever a mesh or texture LOD level has finished loading and has been applied.
210
+ * The listener receives the type of asset (`"mesh"` or `"texture"`), the new LOD level, and the affected object.
211
+ *
212
+ * @param evt - The event type. Currently only `"changed"` is supported.
213
+ * @param listener - Callback invoked after a LOD swap completes.
214
+ * @return A function to unregister the listener.
215
+ *
216
+ * @example
217
+ * ```ts
218
+ * lodsManager.addEventListener("changed", ({ type, level, object }) => {
219
+ * console.log(`${type} LOD changed to level ${level}`, object);
220
+ * });
221
+ * ```
222
+ */
148
223
  addEventListener(evt, listener) {
149
224
  if (evt === "changed") {
150
225
  this._lodchangedlisteners.push(listener);
226
+ return () => {
227
+ this.removeEventListener(evt, listener);
228
+ };
151
229
  }
230
+ return () => { };
152
231
  }
232
+ /**
233
+ * Remove a previously registered `"changed"` event listener.
234
+ * @param evt - The event type (`"changed"`).
235
+ * @param listener - The listener to remove.
236
+ * @return `true` if the listener was found and removed, `false` otherwise.
237
+ */
153
238
  removeEventListener(evt, listener) {
239
+ let removed = false;
154
240
  if (evt === "changed") {
155
241
  const index = this._lodchangedlisteners.indexOf(listener);
156
- if (index >= 0)
242
+ if (index >= 0) {
157
243
  this._lodchangedlisteners.splice(index, 1);
244
+ removed = true;
245
+ }
158
246
  }
247
+ return removed;
159
248
  }
160
249
  // readonly plugins: NEEDLE_progressive_plugin[] = [];
161
250
  constructor(renderer, context) {
@@ -217,6 +306,21 @@ export class LODsManager {
217
306
  this.renderer.render = this.#originalRender;
218
307
  this.#originalRender = undefined;
219
308
  }
309
+ /**
310
+ * Manually trigger a LOD update for a scene and camera.
311
+ * Only needed when {@link manual} is set to `true` — otherwise LOD updates happen automatically on each render call.
312
+ *
313
+ * @param scene - The scene containing objects with progressive LODs.
314
+ * @param camera - The camera used to determine screen coverage and LOD levels.
315
+ *
316
+ * @example
317
+ * ```ts
318
+ * const lodsManager = LODsManager.get(renderer);
319
+ * lodsManager.manual = true;
320
+ * // ... later, trigger an update at a specific point:
321
+ * lodsManager.update(scene, camera);
322
+ * ```
323
+ */
220
324
  update(scene, camera) {
221
325
  this.internalUpdate(scene, camera);
222
326
  }
@@ -306,16 +410,6 @@ export class LODsManager {
306
410
  case "MeshDepthMaterial":
307
411
  continue;
308
412
  }
309
- if (debugProgressiveLoading === "color") {
310
- if (entry.material) {
311
- if (!entry.object["progressive_debug_color"]) {
312
- entry.object["progressive_debug_color"] = true;
313
- const randomColor = Math.random() * 0xffffff;
314
- const newMaterial = new MeshStandardMaterial({ color: randomColor });
315
- entry.object.material = newMaterial;
316
- }
317
- }
318
- }
319
413
  const object = entry.object;
320
414
  if (object instanceof Mesh || (object.isMesh)) {
321
415
  this.updateLODs(scene, camera, object, desiredDensity);
@@ -374,6 +468,9 @@ export class LODsManager {
374
468
  if (debug && object.material && !object["isGizmo"]) {
375
469
  applyDebugSettings(object.material);
376
470
  }
471
+ if (debugProgressiveLODColors && object.material && !object["isGizmo"] && !object["isBatchedMesh"]) {
472
+ applyLODColor(object.material, levels.mesh_lod);
473
+ }
377
474
  for (const plugin of plugins) {
378
475
  plugin.onAfterUpdatedLOD?.(this.renderer, scene, camera, object, levels);
379
476
  }
@@ -390,7 +487,7 @@ export class LODsManager {
390
487
  return;
391
488
  if (Array.isArray(material)) {
392
489
  for (const mat of material) {
393
- this.loadProgressiveTextures(mat, level);
490
+ this.loadProgressiveTextures(mat, level, overrideLodLevel);
394
491
  }
395
492
  return;
396
493
  }
@@ -403,13 +500,15 @@ export class LODsManager {
403
500
  else if (level < material[$currentLOD]) {
404
501
  update = true;
405
502
  }
406
- if (overrideLodLevel !== undefined && overrideLodLevel >= 0) {
503
+ const forceExactTextureLOD = overrideLodLevel !== undefined && overrideLodLevel >= 0;
504
+ if (forceExactTextureLOD) {
407
505
  update = material[$currentLOD] != overrideLodLevel;
408
506
  level = overrideLodLevel;
409
507
  }
410
508
  if (update) {
411
509
  material[$currentLOD] = level;
412
- const promise = NEEDLE_progressive.assignTextureLOD(material, level).then(_ => {
510
+ const options = forceExactTextureLOD ? { force: true } : undefined;
511
+ const promise = NEEDLE_progressive.assignTextureLOD(material, level, options).then(_ => {
413
512
  this._lodchangedlisteners.forEach(l => l({ type: "texture", level, object: material }));
414
513
  });
415
514
  PromiseGroup.addPromise("texture", material, promise, this._newPromiseGroups);
@@ -745,3 +844,21 @@ class LOD_state {
745
844
  lastScreenspaceVolume = new Vector3();
746
845
  lastCentrality = 0;
747
846
  }
847
+ function applyLODColor(material, level) {
848
+ if (level < 0)
849
+ return;
850
+ if (Array.isArray(material)) {
851
+ for (const mat of material) {
852
+ applyLODColor(mat, level);
853
+ }
854
+ return;
855
+ }
856
+ if ("color" in material && material.color instanceof Color) {
857
+ material.color.copy(getLODColor(level, debugLODColor));
858
+ material.needsUpdate = true;
859
+ }
860
+ }
861
+ export function getLODColor(level, target) {
862
+ const index = Math.max(0, Math.min(lodDebugColors.length - 1, Math.floor(level)));
863
+ return target.setHex(lodDebugColors[index]);
864
+ }
@@ -77,7 +77,7 @@ export class PromiseGroup {
77
77
  }
78
78
  if (this._maxPromisesPerObject >= 1) {
79
79
  if (this._seen.has(object)) {
80
- let count = this._seen.get(object);
80
+ const count = this._seen.get(object);
81
81
  if (count >= this._maxPromisesPerObject) {
82
82
  if (debug)
83
83
  console.warn(`PromiseGroup: Already awaiting object ignoring new promise for it.`);
@@ -108,7 +108,7 @@ function _patchModelViewer(modelviewer) {
108
108
  function renderFrames() {
109
109
  if (needsRender) {
110
110
  let forcedFrames = 0;
111
- let interval = setInterval(() => {
111
+ const interval = setInterval(() => {
112
112
  if (forcedFrames++ > 5) {
113
113
  clearInterval(interval);
114
114
  return;
package/lib/version.js CHANGED
@@ -1,4 +1,4 @@
1
1
  // replaced at build time
2
- export const version = "3.5.0-rc";
2
+ export const version = "3.6.0-alpha.1";
3
3
  globalThis["GLTF_PROGRESSIVE_VERSION"] = version;
4
4
  console.debug(`[gltf-progressive] version ${version || "-"}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@needle-tools/gltf-progressive",
3
- "version": "3.5.0-rc",
3
+ "version": "3.6.0-alpha.1",
4
4
  "description": "three.js support for loading glTF or GLB files that contain progressive loading data",
5
5
  "homepage": "https://needle.tools",
6
6
  "author": {
@@ -12,6 +12,7 @@
12
12
  "type": "git",
13
13
  "url": "git+https://github.com/needle-tools/gltf-progressive"
14
14
  },
15
+ "license": "MIT",
15
16
  "readme": "README.md",
16
17
  "keywords": [
17
18
  "three.js",
@@ -41,7 +42,8 @@
41
42
  "gltf-progressive.js",
42
43
  "gltf-progressive.min.js",
43
44
  "gltf-progressive.umd.cjs",
44
- "NEEDLE_progressive"
45
+ "NEEDLE_progressive",
46
+ "LICENSE"
45
47
  ],
46
48
  "watch": {
47
49
  "build:lib": {