@needle-tools/gltf-progressive 3.6.0-alpha.2 → 3.6.0-beta.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.
- package/CHANGELOG.md +7 -0
- package/README.md +42 -8
- package/examples/modelviewer-multiple.html +4 -4
- package/examples/modelviewer.html +4 -4
- package/examples/offscreen/index.html +15 -0
- package/examples/offscreen/main.js +98 -0
- package/examples/react-three-fiber/index.html +2 -2
- package/examples/react-three-fiber/package-lock.json +482 -484
- package/examples/react-three-fiber/package.json +4 -4
- package/examples/react-three-fiber/src/App.tsx +76 -21
- package/examples/react-three-fiber/src/styles.css +2 -4
- package/examples/react-three-fiber/vite.config.js +2 -2
- package/examples/shared/example-utils.js +297 -0
- package/examples/shared/example.css +44 -0
- package/examples/shared/runtime.js +34 -0
- package/examples/threejs/index.html +6 -10
- package/examples/threejs/main.js +5 -23
- package/examples/webgpu/index.html +15 -0
- package/examples/webgpu/main.js +105 -0
- package/examples/worker-rendering/index.html +15 -0
- package/examples/worker-rendering/main.js +109 -0
- package/examples/worker-rendering/worker.js +166 -0
- package/gltf-progressive.js +670 -559
- package/gltf-progressive.min.js +9 -9
- package/gltf-progressive.umd.cjs +9 -9
- package/lib/extension.d.ts +6 -0
- package/lib/extension.js +85 -16
- package/lib/loaders.d.ts +1 -8
- package/lib/loaders.js +15 -2
- package/lib/lods.debug.js +1 -1
- package/lib/lods.manager.d.ts +3 -0
- package/lib/lods.manager.js +62 -18
- package/lib/utils.d.ts +1 -1
- package/lib/utils.internal.d.ts +27 -0
- package/lib/utils.internal.js +68 -25
- package/lib/version.js +1 -1
- package/lib/worker/loader.mainthread.js +6 -4
- package/package.json +8 -3
package/lib/extension.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { BufferGeometry, Mesh, Texture, TextureLoader } from "three";
|
|
2
2
|
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
|
|
3
3
|
import { addDracoAndKTX2Loaders } from "./loaders.js";
|
|
4
|
-
import { determineTextureMemoryInBytes, getParam, isMobileDevice, PromiseQueue, resolveUrl } from "./utils.internal.js";
|
|
4
|
+
import { determineTextureMemoryInBytes, getParam, getSourceData, getTextureDimensions, hasPixelData, isMobileDevice, PromiseQueue, resolveUrl } from "./utils.internal.js";
|
|
5
5
|
import { getRaycastMesh, registerRaycastMesh } from "./utils.js";
|
|
6
6
|
// All of this has to be removed
|
|
7
7
|
// import { getRaycastMesh, setRaycastMesh } from "../../engine_physics.js";
|
|
@@ -223,13 +223,19 @@ export class NEEDLE_progressive {
|
|
|
223
223
|
}
|
|
224
224
|
// const info = this.onProgressiveLoadStart(context, source, mesh, null);
|
|
225
225
|
mesh["LOD:requested level"] = level;
|
|
226
|
-
|
|
226
|
+
const shouldLoad = () => mesh["LOD:requested level"] === level || this.shouldApplyStaleMeshLOD(mesh, level);
|
|
227
|
+
return NEEDLE_progressive.getOrLoadLOD(currentGeometry, level, {
|
|
228
|
+
isCurrent: shouldLoad,
|
|
229
|
+
}).then(geo => {
|
|
227
230
|
if (Array.isArray(geo)) {
|
|
228
231
|
const index = lodinfo.index || 0;
|
|
229
232
|
geo = geo[index];
|
|
230
233
|
}
|
|
231
|
-
|
|
232
|
-
|
|
234
|
+
const isLatestRequest = mesh["LOD:requested level"] === level;
|
|
235
|
+
if (isLatestRequest || this.shouldApplyStaleMeshLOD(mesh, level)) {
|
|
236
|
+
if (isLatestRequest) {
|
|
237
|
+
delete mesh["LOD:requested level"];
|
|
238
|
+
}
|
|
233
239
|
if (geo && currentGeometry != geo) {
|
|
234
240
|
const isGeometry = geo?.isBufferGeometry;
|
|
235
241
|
// if (debug == "verbose") console.log("Progressive Mesh " + mesh.name + " loaded", currentGeometry, "→", geo, "\n", mesh)
|
|
@@ -373,7 +379,14 @@ export class NEEDLE_progressive {
|
|
|
373
379
|
return pending.promise;
|
|
374
380
|
}
|
|
375
381
|
}
|
|
376
|
-
const
|
|
382
|
+
const requestId = material && slot ? this.nextTextureSlotRequestId(material, slot, level, force) : 0;
|
|
383
|
+
const isCurrentRequest = () => !material || !slot || this.getLatestTextureSlotRequest(material, slot)?.id === requestId;
|
|
384
|
+
const shouldLoad = () => isCurrentRequest() || this.shouldApplyStaleTextureSlotLOD(material, slot, level, force);
|
|
385
|
+
const promise = NEEDLE_progressive.getOrLoadLOD(current, level, {
|
|
386
|
+
isCurrent: shouldLoad,
|
|
387
|
+
}).then(tex => {
|
|
388
|
+
if (!isCurrentRequest() && !this.shouldApplyStaleTextureSlotLOD(material, slot, level, force))
|
|
389
|
+
return null;
|
|
377
390
|
// this can currently not happen
|
|
378
391
|
if (Array.isArray(tex)) {
|
|
379
392
|
console.warn("Progressive: Got an array of textures for a texture slot, this should not happen...");
|
|
@@ -415,7 +428,7 @@ export class NEEDLE_progressive {
|
|
|
415
428
|
return null;
|
|
416
429
|
});
|
|
417
430
|
if (material && slot) {
|
|
418
|
-
this.setPendingTextureSlotRequest(material, slot, level, force, promise);
|
|
431
|
+
this.setPendingTextureSlotRequest(material, slot, level, force, requestId, promise);
|
|
419
432
|
}
|
|
420
433
|
return promise;
|
|
421
434
|
}
|
|
@@ -423,6 +436,8 @@ export class NEEDLE_progressive {
|
|
|
423
436
|
// referenced by many slots and should only be disposed after every slot moved away.
|
|
424
437
|
static trackedTextureSlots = new WeakMap();
|
|
425
438
|
static pendingTextureSlotRequests = new WeakMap();
|
|
439
|
+
static latestTextureSlotRequests = new WeakMap();
|
|
440
|
+
static textureSlotRequestId = 0;
|
|
426
441
|
static trackCurrentMaterialTextureSlots(material) {
|
|
427
442
|
if (material.uniforms && (material.isRawShaderMaterial || material.isShaderMaterial === true)) {
|
|
428
443
|
const shaderMaterial = material;
|
|
@@ -444,17 +459,52 @@ export class NEEDLE_progressive {
|
|
|
444
459
|
static getPendingTextureSlotRequest(material, slot) {
|
|
445
460
|
return this.pendingTextureSlotRequests.get(material)?.get(slot);
|
|
446
461
|
}
|
|
447
|
-
static
|
|
462
|
+
static nextTextureSlotRequestId(material, slot, level, force) {
|
|
463
|
+
let slots = this.latestTextureSlotRequests.get(material);
|
|
464
|
+
if (!slots) {
|
|
465
|
+
slots = new Map();
|
|
466
|
+
this.latestTextureSlotRequests.set(material, slots);
|
|
467
|
+
}
|
|
468
|
+
const id = ++this.textureSlotRequestId;
|
|
469
|
+
slots.set(slot, { id, level, force });
|
|
470
|
+
return id;
|
|
471
|
+
}
|
|
472
|
+
static getLatestTextureSlotRequest(material, slot) {
|
|
473
|
+
return this.latestTextureSlotRequests.get(material)?.get(slot);
|
|
474
|
+
}
|
|
475
|
+
static shouldApplyStaleTextureSlotLOD(material, slot, level, force) {
|
|
476
|
+
if (!material || !slot)
|
|
477
|
+
return false;
|
|
478
|
+
const latest = this.getLatestTextureSlotRequest(material, slot);
|
|
479
|
+
const assigned = this.getMaterialTextureSlot(material, slot);
|
|
480
|
+
const assignedLODLevel = this.getAssignedLODInformation(assigned)?.level ?? Infinity;
|
|
481
|
+
if (level >= assignedLODLevel)
|
|
482
|
+
return false;
|
|
483
|
+
if (force) {
|
|
484
|
+
if (!latest)
|
|
485
|
+
return false;
|
|
486
|
+
return level >= latest.level;
|
|
487
|
+
}
|
|
488
|
+
return true;
|
|
489
|
+
}
|
|
490
|
+
static shouldApplyStaleMeshLOD(mesh, level) {
|
|
491
|
+
const latestLevel = mesh["LOD:requested level"];
|
|
492
|
+
if (typeof latestLevel !== "number")
|
|
493
|
+
return false;
|
|
494
|
+
const assignedLODLevel = this.getAssignedLODInformation(mesh.geometry)?.level ?? Infinity;
|
|
495
|
+
return level < assignedLODLevel && level >= latestLevel;
|
|
496
|
+
}
|
|
497
|
+
static setPendingTextureSlotRequest(material, slot, level, force, id, promise) {
|
|
448
498
|
let slots = this.pendingTextureSlotRequests.get(material);
|
|
449
499
|
if (!slots) {
|
|
450
500
|
slots = new Map();
|
|
451
501
|
this.pendingTextureSlotRequests.set(material, slots);
|
|
452
502
|
}
|
|
453
|
-
const request = { level, force, promise };
|
|
503
|
+
const request = { level, force, id, promise };
|
|
454
504
|
slots.set(slot, request);
|
|
455
505
|
promise.finally(() => {
|
|
456
506
|
const current = slots.get(slot);
|
|
457
|
-
if (current ===
|
|
507
|
+
if (current?.id === id) {
|
|
458
508
|
slots.delete(slot);
|
|
459
509
|
}
|
|
460
510
|
});
|
|
@@ -656,8 +706,7 @@ export class NEEDLE_progressive {
|
|
|
656
706
|
return;
|
|
657
707
|
}
|
|
658
708
|
if (debug) {
|
|
659
|
-
const width
|
|
660
|
-
const height = tex.image?.height || tex.source?.data?.height || 0;
|
|
709
|
+
const { width, height } = getTextureDimensions(tex);
|
|
661
710
|
console.log(`> gltf-progressive: register texture[${index}] "${tex.name || tex.uuid}", Current: ${width}x${height}, Max: ${ext.lods[0]?.width}x${ext.lods[0]?.height}, uuid: ${tex.uuid}`, ext, tex);
|
|
662
711
|
}
|
|
663
712
|
// 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)
|
|
@@ -771,6 +820,8 @@ export class NEEDLE_progressive {
|
|
|
771
820
|
this.textureRefCounts.clear();
|
|
772
821
|
this.trackedTextureSlots = new WeakMap();
|
|
773
822
|
this.pendingTextureSlotRequests = new WeakMap();
|
|
823
|
+
this.latestTextureSlotRequests = new WeakMap();
|
|
824
|
+
this.textureSlotRequestId = 0;
|
|
774
825
|
}
|
|
775
826
|
}
|
|
776
827
|
/** Dispose a single cache entry's three.js resource(s) to free GPU memory. */
|
|
@@ -877,8 +928,7 @@ export class NEEDLE_progressive {
|
|
|
877
928
|
return false;
|
|
878
929
|
}
|
|
879
930
|
function logDebugInfo(prefix, newCount) {
|
|
880
|
-
|
|
881
|
-
const height = texture.image?.height || texture.source?.data?.height || 0;
|
|
931
|
+
let { width, height } = getTextureDimensions(texture);
|
|
882
932
|
const textureSize = width && height ? `${width}x${height}` : "N/A";
|
|
883
933
|
let memorySize = "N/A";
|
|
884
934
|
if (width && height) {
|
|
@@ -889,7 +939,7 @@ export class NEEDLE_progressive {
|
|
|
889
939
|
}
|
|
890
940
|
static workers = [];
|
|
891
941
|
static _workersIndex = 0;
|
|
892
|
-
static async getOrLoadLOD(current, level) {
|
|
942
|
+
static async getOrLoadLOD(current, level, options) {
|
|
893
943
|
const debugverbose = debug == "verbose";
|
|
894
944
|
/** this key is used to lookup the LOD information */
|
|
895
945
|
const LOD = this.getAssignedLODInformation(current);
|
|
@@ -962,7 +1012,17 @@ export class NEEDLE_progressive {
|
|
|
962
1012
|
const cached = await this.tryResolveLODCacheEntry(this.cache.get(KEY), KEY, lod_url, current, level, debugverbose);
|
|
963
1013
|
if (cached.found)
|
|
964
1014
|
return cached.value;
|
|
1015
|
+
if (options?.isCurrent?.() === false) {
|
|
1016
|
+
if (debugverbose)
|
|
1017
|
+
console.log(`Skipping stale LOD ${level} request before queue: ${lod_url}`);
|
|
1018
|
+
return null;
|
|
1019
|
+
}
|
|
965
1020
|
const slot = await this.queue.slot(lod_url);
|
|
1021
|
+
if (options?.isCurrent?.() === false) {
|
|
1022
|
+
if (debugverbose)
|
|
1023
|
+
console.log(`Skipping stale LOD ${level} request after queue: ${lod_url}`);
|
|
1024
|
+
return null;
|
|
1025
|
+
}
|
|
966
1026
|
// Another request can fill the cache while this one waits for a queue slot.
|
|
967
1027
|
// Re-checking here avoids duplicate loads for heavily instanced assets.
|
|
968
1028
|
const cachedAfterQueue = await this.tryResolveLODCacheEntry(this.cache.get(KEY), KEY, lod_url, current, level, debugverbose);
|
|
@@ -1143,10 +1203,19 @@ export class NEEDLE_progressive {
|
|
|
1143
1203
|
}
|
|
1144
1204
|
else {
|
|
1145
1205
|
if (current instanceof Texture) {
|
|
1206
|
+
if (options?.isCurrent?.() === false) {
|
|
1207
|
+
if (debugverbose)
|
|
1208
|
+
console.log(`Skipping stale texture LOD ${level} request: ${lod_url}`);
|
|
1209
|
+
return null;
|
|
1210
|
+
}
|
|
1146
1211
|
if (debugverbose)
|
|
1147
1212
|
console.log("Load texture from uri: " + lod_url);
|
|
1148
1213
|
const loader = new TextureLoader();
|
|
1149
1214
|
const tex = await loader.loadAsync(lod_url);
|
|
1215
|
+
if (options?.isCurrent?.() === false) {
|
|
1216
|
+
tex?.dispose();
|
|
1217
|
+
return null;
|
|
1218
|
+
}
|
|
1150
1219
|
if (tex) {
|
|
1151
1220
|
tex.guid = lodInfo.guid;
|
|
1152
1221
|
tex.flipY = false;
|
|
@@ -1175,7 +1244,7 @@ export class NEEDLE_progressive {
|
|
|
1175
1244
|
let res = derefed;
|
|
1176
1245
|
let resourceIsDisposed = false;
|
|
1177
1246
|
if (res instanceof Texture && current instanceof Texture) {
|
|
1178
|
-
if (res.image
|
|
1247
|
+
if (hasPixelData(res.image) || getSourceData(res)) {
|
|
1179
1248
|
res = this.copySettings(current, res);
|
|
1180
1249
|
}
|
|
1181
1250
|
else {
|
|
@@ -1205,7 +1274,7 @@ export class NEEDLE_progressive {
|
|
|
1205
1274
|
// Failed loads stay cached as null so we don't retry the same missing resource forever.
|
|
1206
1275
|
}
|
|
1207
1276
|
else if (res instanceof Texture && current instanceof Texture) {
|
|
1208
|
-
if (res.image
|
|
1277
|
+
if (hasPixelData(res.image) || getSourceData(res)) {
|
|
1209
1278
|
res = this.copySettings(current, res);
|
|
1210
1279
|
}
|
|
1211
1280
|
else {
|
package/lib/loaders.d.ts
CHANGED
|
@@ -28,14 +28,7 @@ export declare function setKTX2TranscoderLocation(location: string): void;
|
|
|
28
28
|
export declare function createLoaders(renderer: WebGLRenderer | null): {
|
|
29
29
|
dracoLoader: DRACOLoader;
|
|
30
30
|
ktx2Loader: KTX2Loader;
|
|
31
|
-
meshoptDecoder:
|
|
32
|
-
supported: boolean;
|
|
33
|
-
ready: Promise<void>;
|
|
34
|
-
decodeVertexBuffer: (target: Uint8Array, count: number, size: number, source: Uint8Array, filter?: string) => void;
|
|
35
|
-
decodeIndexBuffer: (target: Uint8Array, count: number, size: number, source: Uint8Array) => void;
|
|
36
|
-
decodeIndexSequence: (target: Uint8Array, count: number, size: number, source: Uint8Array) => void;
|
|
37
|
-
decodeGltfBuffer: (target: Uint8Array, count: number, size: number, source: Uint8Array, mode: string, filter?: string) => void;
|
|
38
|
-
};
|
|
31
|
+
meshoptDecoder: any;
|
|
39
32
|
};
|
|
40
33
|
export declare function addDracoAndKTX2Loaders(loader: GLTFLoader): void;
|
|
41
34
|
/**
|
package/lib/loaders.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
// @ts-ignore @types/three re-exports from meshoptimizer which may not resolve in all configs
|
|
1
2
|
import { MeshoptDecoder } from 'three/examples/jsm/libs/meshopt_decoder.module.js';
|
|
2
3
|
import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader.js';
|
|
3
4
|
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
|
|
@@ -142,8 +143,8 @@ const originalLoadFunction = GLTFLoader.prototype.load;
|
|
|
142
143
|
function onLoad(...args) {
|
|
143
144
|
const config = gltfLoaderConfigurations.get(this);
|
|
144
145
|
let url_str = args[0];
|
|
145
|
-
const url =
|
|
146
|
-
if (url
|
|
146
|
+
const url = tryResolveUrl(url_str);
|
|
147
|
+
if (url?.hostname.endsWith("needle.tools")) {
|
|
147
148
|
const progressive = config?.progressive !== undefined ? config.progressive : true;
|
|
148
149
|
const usecase = config?.usecase ? config.usecase : "default";
|
|
149
150
|
if (progressive) {
|
|
@@ -159,3 +160,15 @@ function onLoad(...args) {
|
|
|
159
160
|
return res;
|
|
160
161
|
}
|
|
161
162
|
GLTFLoader.prototype.load = onLoad;
|
|
163
|
+
function tryResolveUrl(url) {
|
|
164
|
+
try {
|
|
165
|
+
if (url instanceof URL) {
|
|
166
|
+
return url;
|
|
167
|
+
}
|
|
168
|
+
const base = globalThis.location?.href;
|
|
169
|
+
return base ? new URL(url, base) : new URL(url);
|
|
170
|
+
}
|
|
171
|
+
catch {
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
}
|
package/lib/lods.debug.js
CHANGED
|
@@ -2,7 +2,7 @@ import { getParam } from "./utils.internal.js";
|
|
|
2
2
|
export const debug = getParam("debugprogressive");
|
|
3
3
|
let debug_RenderWireframe = undefined;
|
|
4
4
|
export let debug_OverrideLodLevel = -1; // -1 is automatic
|
|
5
|
-
if (debug) {
|
|
5
|
+
if (debug && typeof window !== "undefined") {
|
|
6
6
|
const maxLevel = 6;
|
|
7
7
|
function debugToggleProgressive() {
|
|
8
8
|
debug_OverrideLodLevel += 1;
|
package/lib/lods.manager.d.ts
CHANGED
|
@@ -75,6 +75,8 @@ export declare class LODsManager {
|
|
|
75
75
|
static getObjectLODState(object: Object3D): LOD_state | undefined;
|
|
76
76
|
static addPlugin(plugin: NEEDLE_progressive_plugin): void;
|
|
77
77
|
static removePlugin(plugin: NEEDLE_progressive_plugin): void;
|
|
78
|
+
/** Read-only snapshot of the currently registered plugins, for inspection. Use {@link addPlugin} / {@link removePlugin} to modify the registry. */
|
|
79
|
+
static getPlugins(): readonly NEEDLE_progressive_plugin[];
|
|
78
80
|
/**
|
|
79
81
|
* Gets the LODsManager for the given renderer. If the LODsManager does not exist yet, it will be created.
|
|
80
82
|
* @param renderer The renderer to get the LODsManager for.
|
|
@@ -207,6 +209,7 @@ export declare class LODsManager {
|
|
|
207
209
|
* Update LODs in a scene
|
|
208
210
|
*/
|
|
209
211
|
private internalUpdate;
|
|
212
|
+
private getRenderList;
|
|
210
213
|
/** Update the LOD levels for the renderer. */
|
|
211
214
|
private updateLODs;
|
|
212
215
|
/** Load progressive textures for the given material
|
package/lib/lods.manager.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import
|
|
1
|
+
import * as THREE from "three";
|
|
2
|
+
import { Box3, Color, Matrix4, Mesh, Sphere, Vector3 } from "three";
|
|
2
3
|
import { NEEDLE_progressive } from "./extension.js";
|
|
3
4
|
import { createLoaders } from "./loaders.js";
|
|
4
5
|
import { getParam, isDevelopmentServer, isMobileDevice } from "./utils.internal.js";
|
|
@@ -12,6 +13,7 @@ const suppressProgressiveLoading = getParam("noprogressive");
|
|
|
12
13
|
const $lodsManager = Symbol("Needle:LODSManager");
|
|
13
14
|
const $lodstate = Symbol("Needle:LODState");
|
|
14
15
|
const $currentLOD = Symbol("Needle:CurrentLOD");
|
|
16
|
+
const ThreeRuntime = THREE;
|
|
15
17
|
const levels = { mesh_lod: -1, texture_lod: -1 };
|
|
16
18
|
const debugLODColor = new Color();
|
|
17
19
|
export const lodDebugColors = [
|
|
@@ -48,6 +50,10 @@ export const lodDebugColors = [
|
|
|
48
50
|
0x4d908e,
|
|
49
51
|
0x555555,
|
|
50
52
|
];
|
|
53
|
+
function createLODTimer() {
|
|
54
|
+
const Timer = ThreeRuntime.Timer || ThreeRuntime.Clock;
|
|
55
|
+
return new Timer();
|
|
56
|
+
}
|
|
51
57
|
const _meshLODWorldBox = new Box3();
|
|
52
58
|
const _meshLODProjectedBox = new Box3();
|
|
53
59
|
const _meshLODCameraSpaceBox = new Box3();
|
|
@@ -82,8 +88,10 @@ export function calculateMeshLODLevel(options) {
|
|
|
82
88
|
result.screenCoverage = 0;
|
|
83
89
|
result.screenspaceVolume.set(0, 0, 0);
|
|
84
90
|
result.centrality = 1;
|
|
85
|
-
|
|
86
|
-
|
|
91
|
+
// Note: we intentionally do NOT early-return when there are no mesh LODs.
|
|
92
|
+
// The screen coverage / screenspace volume computed below is also consumed by
|
|
93
|
+
// the texture LOD selection, which must keep working for meshes that only have
|
|
94
|
+
// texture LODs (no mesh LODs). Only the mesh LOD-level selection loop is skipped.
|
|
87
95
|
let boundingBox = options.boundingBox ?? geometry.boundingBox;
|
|
88
96
|
if (!boundingBox) {
|
|
89
97
|
geometry.computeBoundingBox();
|
|
@@ -159,16 +167,18 @@ export function calculateMeshLODLevel(options) {
|
|
|
159
167
|
debugDrawLine(_meshLODCorner1, _meshLODCorner3, 0x0000ff);
|
|
160
168
|
debugDrawLine(_meshLODCorner2, _meshLODCorner3, 0x0000ff);
|
|
161
169
|
}
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
globalThis["NEEDLE:MISSING_LOD_PRIMITIVE_DENSITIES"]
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
170
|
+
if (meshLods?.length) {
|
|
171
|
+
for (let i = 0; i < meshLods.length; i++) {
|
|
172
|
+
const lod = meshLods[i];
|
|
173
|
+
const density = lod.densities?.[primitiveIndex] || lod.density || .00001;
|
|
174
|
+
if (primitiveIndex > 0 && warnMissingPrimitiveDensities && isDevelopmentServer() && !lod.densities && !globalThis["NEEDLE:MISSING_LOD_PRIMITIVE_DENSITIES"]) {
|
|
175
|
+
globalThis["NEEDLE:MISSING_LOD_PRIMITIVE_DENSITIES"] = true;
|
|
176
|
+
console.warn(`[Needle Progressive] Detected usage of mesh without primitive densities. This might cause incorrect LOD level selection: Consider re-optimizing your model by updating your Needle Integration, Needle glTF Pipeline or running optimization again on Needle Cloud.`);
|
|
177
|
+
}
|
|
178
|
+
if (density / screenCoverage < desiredDensity) {
|
|
179
|
+
result.level = i;
|
|
180
|
+
break;
|
|
181
|
+
}
|
|
172
182
|
}
|
|
173
183
|
}
|
|
174
184
|
return result;
|
|
@@ -219,6 +229,10 @@ export class LODsManager {
|
|
|
219
229
|
if (index >= 0)
|
|
220
230
|
plugins.splice(index, 1);
|
|
221
231
|
}
|
|
232
|
+
/** Read-only snapshot of the currently registered plugins, for inspection. Use {@link addPlugin} / {@link removePlugin} to modify the registry. */
|
|
233
|
+
static getPlugins() {
|
|
234
|
+
return plugins;
|
|
235
|
+
}
|
|
222
236
|
/**
|
|
223
237
|
* Gets the LODsManager for the given renderer. If the LODsManager does not exist yet, it will be created.
|
|
224
238
|
* @param renderer The renderer to get the LODsManager for.
|
|
@@ -390,11 +404,11 @@ export class LODsManager {
|
|
|
390
404
|
// })
|
|
391
405
|
}
|
|
392
406
|
#originalRender;
|
|
393
|
-
#clock = new Clock();
|
|
394
407
|
#frame = 0;
|
|
395
408
|
#delta = 0;
|
|
396
409
|
#time = 0;
|
|
397
410
|
#fps = 0;
|
|
411
|
+
#clock = createLODTimer();
|
|
398
412
|
_fpsBuffer = [60, 60, 60, 60, 60];
|
|
399
413
|
/**
|
|
400
414
|
* Enable the LODsManager. This will replace the render method of the renderer with a method that updates the LODs.
|
|
@@ -416,7 +430,8 @@ export class LODsManager {
|
|
|
416
430
|
if (renderTarget == null || ("isXRRenderTarget" in renderTarget && renderTarget.isXRRenderTarget)) {
|
|
417
431
|
stack = 0;
|
|
418
432
|
self.#frame += 1;
|
|
419
|
-
self.#
|
|
433
|
+
self.#clock.update?.();
|
|
434
|
+
self.#delta = Math.max(self.#clock.getDelta(), 1 / 1000);
|
|
420
435
|
self.#time += self.#delta;
|
|
421
436
|
self._fpsBuffer.shift();
|
|
422
437
|
self._fpsBuffer.push(1 / self.#delta);
|
|
@@ -457,7 +472,9 @@ export class LODsManager {
|
|
|
457
472
|
onAfterRender(scene, camera, _stack) {
|
|
458
473
|
if (this.pause)
|
|
459
474
|
return;
|
|
460
|
-
const renderList = this.
|
|
475
|
+
const renderList = this.getRenderList(scene, camera, _stack);
|
|
476
|
+
if (!renderList)
|
|
477
|
+
return;
|
|
461
478
|
const opaque = renderList.opaque;
|
|
462
479
|
let updateLODs = true;
|
|
463
480
|
// check if we're rendering a postprocessing pass
|
|
@@ -514,7 +531,9 @@ export class LODsManager {
|
|
|
514
531
|
* Update LODs in a scene
|
|
515
532
|
*/
|
|
516
533
|
internalUpdate(scene, camera) {
|
|
517
|
-
const renderList = this.
|
|
534
|
+
const renderList = this.getRenderList(scene, camera, 0);
|
|
535
|
+
if (!renderList)
|
|
536
|
+
return;
|
|
518
537
|
const opaque = renderList.opaque;
|
|
519
538
|
this.projectionScreenMatrix.multiplyMatrices(camera.projectionMatrix, camera.matrixWorldInverse);
|
|
520
539
|
const desiredDensity = this.targetTriangleDensity;
|
|
@@ -560,6 +579,30 @@ export class LODsManager {
|
|
|
560
579
|
}
|
|
561
580
|
}
|
|
562
581
|
}
|
|
582
|
+
getRenderList(scene, camera, stack) {
|
|
583
|
+
const renderer = this.renderer;
|
|
584
|
+
let renderList = null;
|
|
585
|
+
if (renderer.isWebGPURenderer === true) {
|
|
586
|
+
const renderLists = renderer._renderLists;
|
|
587
|
+
if (!renderLists)
|
|
588
|
+
return null;
|
|
589
|
+
renderList = renderLists.get(scene, camera);
|
|
590
|
+
}
|
|
591
|
+
else if (renderer.isWebGLRenderer === true) {
|
|
592
|
+
const renderLists = renderer.renderLists;
|
|
593
|
+
if (!renderLists)
|
|
594
|
+
return null;
|
|
595
|
+
renderList = renderLists.get(scene, stack);
|
|
596
|
+
}
|
|
597
|
+
if (!renderList)
|
|
598
|
+
return null;
|
|
599
|
+
return {
|
|
600
|
+
opaque: renderList.opaque || [],
|
|
601
|
+
transparent: renderList.transparent || [],
|
|
602
|
+
transmissive: renderList.transmissive || renderList.transparentDoublePass || [],
|
|
603
|
+
transparentDoublePass: renderList.transparentDoublePass || [],
|
|
604
|
+
};
|
|
605
|
+
}
|
|
563
606
|
/** Update the LOD levels for the renderer. */
|
|
564
607
|
updateLODs(scene, camera, object, desiredDensity) {
|
|
565
608
|
if (!object.userData) {
|
|
@@ -820,7 +863,8 @@ export class LODsManager {
|
|
|
820
863
|
if (this.context?.engine === "model-viewer") {
|
|
821
864
|
factor *= 1.5;
|
|
822
865
|
}
|
|
823
|
-
const
|
|
866
|
+
const devicePixelRatio = this.renderer.getPixelRatio?.() || globalThis.devicePixelRatio || 1;
|
|
867
|
+
const screenSize = canvasHeight / devicePixelRatio;
|
|
824
868
|
const pixelSizeOnScreen = screenSize * factor;
|
|
825
869
|
let foundLod = false;
|
|
826
870
|
for (let i = texture_lods_minmax.lods.length - 1; i >= 0; i--) {
|
package/lib/utils.d.ts
CHANGED
|
@@ -5,7 +5,7 @@ export declare const isSSR: boolean;
|
|
|
5
5
|
* @param obj the object to get the raycast mesh from
|
|
6
6
|
* @returns the raycast mesh or null if not set
|
|
7
7
|
*/
|
|
8
|
-
export declare function getRaycastMesh(obj: Object3D): BufferGeometry<any> | null;
|
|
8
|
+
export declare function getRaycastMesh(obj: Object3D): BufferGeometry<any, any> | null;
|
|
9
9
|
/**
|
|
10
10
|
* Set the raycast mesh for an object.
|
|
11
11
|
* The raycast mesh is a low poly version of the mesh used for raycasting. It is set when a mesh that has LOD level with more vertices is discovered for the first time
|
package/lib/utils.internal.d.ts
CHANGED
|
@@ -1,4 +1,31 @@
|
|
|
1
1
|
import { Texture } from "three";
|
|
2
|
+
/** Represents the possible shapes of texture image/source data in three.js.
|
|
3
|
+
* Source.data is typed as `{}` in r183 but at runtime can be ImageBitmap, HTMLImageElement, etc. */
|
|
4
|
+
export type TextureImageData = {
|
|
5
|
+
width?: number;
|
|
6
|
+
height?: number;
|
|
7
|
+
depth?: number;
|
|
8
|
+
data?: ArrayBufferView | null;
|
|
9
|
+
};
|
|
10
|
+
/** Check if a value has image-like dimensions (width/height) */
|
|
11
|
+
export declare function hasImageDimensions(value: unknown): value is {
|
|
12
|
+
width: number;
|
|
13
|
+
height: number;
|
|
14
|
+
};
|
|
15
|
+
/** Check if a value has pixel data (e.g. typed array from a DataTexture) */
|
|
16
|
+
export declare function hasPixelData(value: unknown): value is {
|
|
17
|
+
data: ArrayBufferView;
|
|
18
|
+
};
|
|
19
|
+
/** Get the source data of a texture, typed for dimension/data access */
|
|
20
|
+
export declare function getSourceData(tex: Texture): TextureImageData | null;
|
|
21
|
+
/** Get the image of a texture, typed for dimension/data access.
|
|
22
|
+
* In r183, Texture.image is typed as `{}` but at runtime is an ImageBitmap, HTMLImageElement, etc. */
|
|
23
|
+
export declare function getTextureImage(tex: Texture): TextureImageData | null;
|
|
24
|
+
/** Get width/height of a texture from image or source data */
|
|
25
|
+
export declare function getTextureDimensions(tex: Texture): {
|
|
26
|
+
width: number;
|
|
27
|
+
height: number;
|
|
28
|
+
};
|
|
2
29
|
export declare function isDebugMode(): string | boolean;
|
|
3
30
|
export declare function getParam(name: string): boolean | string;
|
|
4
31
|
export declare function resolveUrl(source: string | undefined, uri: string): string;
|
package/lib/utils.internal.js
CHANGED
|
@@ -1,11 +1,41 @@
|
|
|
1
|
+
import { RedFormat, RedIntegerFormat, RGFormat, RGIntegerFormat, RGBFormat, RGBAFormat, RGBAIntegerFormat } from "three";
|
|
2
|
+
/** Check if a value has image-like dimensions (width/height) */
|
|
3
|
+
export function hasImageDimensions(value) {
|
|
4
|
+
return value != null && typeof value.width === 'number' && typeof value.height === 'number';
|
|
5
|
+
}
|
|
6
|
+
/** Check if a value has pixel data (e.g. typed array from a DataTexture) */
|
|
7
|
+
export function hasPixelData(value) {
|
|
8
|
+
return value != null && value.data != null;
|
|
9
|
+
}
|
|
10
|
+
/** Get the source data of a texture, typed for dimension/data access */
|
|
11
|
+
export function getSourceData(tex) {
|
|
12
|
+
const data = tex.source?.data;
|
|
13
|
+
return data != null && typeof data === 'object' ? data : null;
|
|
14
|
+
}
|
|
15
|
+
/** Get the image of a texture, typed for dimension/data access.
|
|
16
|
+
* In r183, Texture.image is typed as `{}` but at runtime is an ImageBitmap, HTMLImageElement, etc. */
|
|
17
|
+
export function getTextureImage(tex) {
|
|
18
|
+
const img = tex.image;
|
|
19
|
+
return img != null && typeof img === 'object' ? img : null;
|
|
20
|
+
}
|
|
21
|
+
/** Get width/height of a texture from image or source data */
|
|
22
|
+
export function getTextureDimensions(tex) {
|
|
23
|
+
const img = getTextureImage(tex);
|
|
24
|
+
const src = getSourceData(tex);
|
|
25
|
+
return {
|
|
26
|
+
width: img?.width || src?.width || 0,
|
|
27
|
+
height: img?.height || src?.height || 0,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
1
30
|
const debug = getParam("debugprogressive");
|
|
2
31
|
export function isDebugMode() {
|
|
3
32
|
return debug;
|
|
4
33
|
}
|
|
5
34
|
export function getParam(name) {
|
|
6
|
-
|
|
35
|
+
const href = globalThis.location?.href;
|
|
36
|
+
if (!href)
|
|
7
37
|
return false;
|
|
8
|
-
const url = new URL(
|
|
38
|
+
const url = new URL(href);
|
|
9
39
|
const param = url.searchParams.get(name);
|
|
10
40
|
if (param == null || param === "0" || param === "false")
|
|
11
41
|
return false;
|
|
@@ -44,7 +74,8 @@ export function resolveUrl(source, uri) {
|
|
|
44
74
|
export function isMobileDevice() {
|
|
45
75
|
if (_ismobile !== undefined)
|
|
46
76
|
return _ismobile;
|
|
47
|
-
|
|
77
|
+
const userAgent = globalThis.navigator?.userAgent || "";
|
|
78
|
+
_ismobile = /iPhone|iPad|iPod|Android|IEMobile/i.test(userAgent);
|
|
48
79
|
if (getParam("debugprogressive"))
|
|
49
80
|
console.log("[glTF Progressive]: isMobileDevice", _ismobile);
|
|
50
81
|
return _ismobile;
|
|
@@ -55,9 +86,10 @@ let _ismobile;
|
|
|
55
86
|
* @returns `true` if we are running in a development server (localhost or ip address).
|
|
56
87
|
*/
|
|
57
88
|
export function isDevelopmentServer() {
|
|
58
|
-
|
|
89
|
+
const href = globalThis.location?.href;
|
|
90
|
+
if (!href)
|
|
59
91
|
return false;
|
|
60
|
-
const url = new URL(
|
|
92
|
+
const url = new URL(href);
|
|
61
93
|
const isLocalhostOrIpAddress = url.hostname === "localhost" || /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(url.hostname);
|
|
62
94
|
const isDevelopment = url.hostname === "127.0.0.1" || isLocalhostOrIpAddress;
|
|
63
95
|
return isDevelopment;
|
|
@@ -74,8 +106,14 @@ export class PromiseQueue {
|
|
|
74
106
|
constructor(maxConcurrent, opts = {}) {
|
|
75
107
|
this.maxConcurrent = maxConcurrent;
|
|
76
108
|
this.debug = opts.debug ?? false;
|
|
77
|
-
|
|
78
|
-
|
|
109
|
+
// Dedicated workers can have requestAnimationFrame when they are owned by a window.
|
|
110
|
+
// Other worker-like scopes do not, so keep the frame-based tick when available and fall back to timers otherwise.
|
|
111
|
+
if (typeof globalThis.requestAnimationFrame === "function") {
|
|
112
|
+
globalThis.requestAnimationFrame(this.tick);
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
setTimeout(this.tick, 0);
|
|
116
|
+
}
|
|
79
117
|
}
|
|
80
118
|
tick = () => {
|
|
81
119
|
this.internalUpdate();
|
|
@@ -118,9 +156,10 @@ export class PromiseQueue {
|
|
|
118
156
|
}
|
|
119
157
|
// #region Texture Memory
|
|
120
158
|
export function determineTextureMemoryInBytes(texture) {
|
|
121
|
-
const
|
|
122
|
-
const
|
|
123
|
-
const
|
|
159
|
+
const img = texture.image;
|
|
160
|
+
const width = img?.width ?? 0;
|
|
161
|
+
const height = img?.height ?? 0;
|
|
162
|
+
const depth = img?.depth ?? 1;
|
|
124
163
|
const mipLevels = Math.floor(Math.log2(Math.max(width, height, depth))) + 1;
|
|
125
164
|
const bytesPerPixel = getBytesPerPixel(texture);
|
|
126
165
|
const totalBytes = (width * height * depth * bytesPerPixel * (1 - Math.pow(0.25, mipLevels))) / (1 - 0.25);
|
|
@@ -130,22 +169,22 @@ function getBytesPerPixel(texture) {
|
|
|
130
169
|
// Determine channel count from format
|
|
131
170
|
let channels = 4; // Default RGBA
|
|
132
171
|
const format = texture.format;
|
|
133
|
-
if (format ===
|
|
134
|
-
channels = 1;
|
|
135
|
-
else if (format ===
|
|
136
|
-
channels = 1;
|
|
137
|
-
else if (format ===
|
|
138
|
-
channels = 2;
|
|
139
|
-
else if (format ===
|
|
140
|
-
channels = 2;
|
|
141
|
-
else if (format ===
|
|
142
|
-
channels = 3;
|
|
172
|
+
if (format === RedFormat)
|
|
173
|
+
channels = 1;
|
|
174
|
+
else if (format === RedIntegerFormat)
|
|
175
|
+
channels = 1;
|
|
176
|
+
else if (format === RGFormat)
|
|
177
|
+
channels = 2;
|
|
178
|
+
else if (format === RGIntegerFormat)
|
|
179
|
+
channels = 2;
|
|
180
|
+
else if (format === RGBFormat)
|
|
181
|
+
channels = 3;
|
|
143
182
|
else if (format === 1029)
|
|
144
|
-
channels = 3; // RGBIntegerFormat
|
|
145
|
-
else if (format ===
|
|
146
|
-
channels = 4;
|
|
147
|
-
else if (format ===
|
|
148
|
-
channels = 4;
|
|
183
|
+
channels = 3; // RGBIntegerFormat (not exported in r183)
|
|
184
|
+
else if (format === RGBAFormat)
|
|
185
|
+
channels = 4;
|
|
186
|
+
else if (format === RGBAIntegerFormat)
|
|
187
|
+
channels = 4;
|
|
149
188
|
// Determine bytes per channel from type
|
|
150
189
|
let bytesPerChannel = 1; // UnsignedByteType default
|
|
151
190
|
const type = texture.type;
|
|
@@ -177,6 +216,10 @@ export function detectGPUMemory() {
|
|
|
177
216
|
if (rendererInfo !== undefined) {
|
|
178
217
|
return rendererInfo?.estimatedMemory;
|
|
179
218
|
}
|
|
219
|
+
if (typeof document === "undefined") {
|
|
220
|
+
rendererInfo = null;
|
|
221
|
+
return undefined;
|
|
222
|
+
}
|
|
180
223
|
const canvas = document.createElement('canvas');
|
|
181
224
|
const powerPreference = "high-performance";
|
|
182
225
|
const gl = canvas.getContext('webgl', { powerPreference }) || canvas.getContext('experimental-webgl', { powerPreference });
|
package/lib/version.js
CHANGED