@needle-tools/gltf-progressive 3.1.1 → 3.2.0
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 +4 -0
- package/README.md +14 -3
- package/examples/react-three-fiber/src/App.tsx +1 -1
- package/examples/threejs/main.js +1 -1
- package/gltf-progressive.js +877 -740
- package/gltf-progressive.min.js +7 -7
- package/gltf-progressive.umd.cjs +7 -7
- package/lib/extension.d.ts +4 -1
- package/lib/extension.js +64 -10
- package/lib/index.d.ts +3 -17
- package/lib/index.js +29 -3
- package/lib/loaders.d.ts +8 -0
- package/lib/loaders.js +33 -22
- package/lib/lods.manager.js +20 -11
- package/lib/utils.internal.d.ts +4 -5
- package/lib/version.js +1 -1
- package/lib/worker/loader.mainthread.d.ts +45 -0
- package/lib/worker/loader.mainthread.js +193 -0
- package/lib/worker/loader.worker.js +165 -0
- package/package.json +2 -2
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { Box3, BufferAttribute, BufferGeometry, CompressedTexture, InterleavedBuffer, InterleavedBufferAttribute, Matrix3, Sphere, Texture, Vector3 } from "three";
|
|
2
|
+
import { createLoaders, GET_LOADER_LOCATION_CONFIG } from "../loaders.js";
|
|
3
|
+
import { isMobileDevice } from "../utils.internal.js";
|
|
4
|
+
import { debug } from "../lods.debug.js";
|
|
5
|
+
const workers = new Array();
|
|
6
|
+
let getWorkerId = 0;
|
|
7
|
+
const maxWorkers = isMobileDevice() ? 2 : 10;
|
|
8
|
+
export function getWorker(opts) {
|
|
9
|
+
if (workers.length < maxWorkers) {
|
|
10
|
+
const index = workers.length;
|
|
11
|
+
if (debug)
|
|
12
|
+
console.warn(`[Worker] Creating new worker #${index}`);
|
|
13
|
+
const worker = GLTFLoaderWorker.createWorker(opts || {});
|
|
14
|
+
workers.push(worker);
|
|
15
|
+
return worker;
|
|
16
|
+
}
|
|
17
|
+
const index = (getWorkerId++) % workers.length;
|
|
18
|
+
const worker = workers[index];
|
|
19
|
+
return worker;
|
|
20
|
+
}
|
|
21
|
+
class GLTFLoaderWorker {
|
|
22
|
+
worker;
|
|
23
|
+
static async createWorker(opts) {
|
|
24
|
+
const worker = new Worker(new URL(`./loader.worker.js`, import.meta.url), {
|
|
25
|
+
type: 'module',
|
|
26
|
+
});
|
|
27
|
+
const instance = new GLTFLoaderWorker(worker, opts);
|
|
28
|
+
return instance;
|
|
29
|
+
}
|
|
30
|
+
_running = [];
|
|
31
|
+
_webglRenderer = null;
|
|
32
|
+
async load(url, opts) {
|
|
33
|
+
const configs = GET_LOADER_LOCATION_CONFIG();
|
|
34
|
+
// Make sure we have a webgl renderer for the KTX transcoder feature detection
|
|
35
|
+
let renderer = opts?.renderer;
|
|
36
|
+
if (!renderer) {
|
|
37
|
+
this._webglRenderer ??= (async () => {
|
|
38
|
+
const { WebGLRenderer } = await import("three");
|
|
39
|
+
return new WebGLRenderer();
|
|
40
|
+
})();
|
|
41
|
+
renderer = await this._webglRenderer;
|
|
42
|
+
}
|
|
43
|
+
const loaders = createLoaders(renderer);
|
|
44
|
+
const ktx2Loader = loaders.ktx2Loader;
|
|
45
|
+
const ktx2LoaderConfig = ktx2Loader.workerConfig;
|
|
46
|
+
if (url instanceof URL) {
|
|
47
|
+
url = url.toString();
|
|
48
|
+
}
|
|
49
|
+
else if (url.startsWith("file:")) {
|
|
50
|
+
// make blob url
|
|
51
|
+
url = URL.createObjectURL(new Blob([url]));
|
|
52
|
+
}
|
|
53
|
+
else if (!url.startsWith("blob:") && !url.startsWith("http:") && !url.startsWith("https:")) {
|
|
54
|
+
url = new URL(url, window.location.href).toString();
|
|
55
|
+
}
|
|
56
|
+
const options = {
|
|
57
|
+
type: "load",
|
|
58
|
+
url: url,
|
|
59
|
+
dracoDecoderPath: configs.dracoDecoderPath,
|
|
60
|
+
ktx2TranscoderPath: configs.ktx2TranscoderPath,
|
|
61
|
+
ktx2LoaderConfig: ktx2LoaderConfig,
|
|
62
|
+
};
|
|
63
|
+
if (this._debug)
|
|
64
|
+
console.debug("[Worker] Sending load request", options);
|
|
65
|
+
this.worker.postMessage(options);
|
|
66
|
+
return new Promise(resolve => {
|
|
67
|
+
this._running.push({
|
|
68
|
+
url: url.toString(),
|
|
69
|
+
resolve,
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
_debug = false;
|
|
74
|
+
constructor(worker, _opts) {
|
|
75
|
+
this.worker = worker;
|
|
76
|
+
this._debug = _opts.debug ?? false;
|
|
77
|
+
worker.onmessage = (event) => {
|
|
78
|
+
const data = event.data;
|
|
79
|
+
if (this._debug)
|
|
80
|
+
console.log("[Worker] EVENT", data);
|
|
81
|
+
switch (data.type) {
|
|
82
|
+
case "loaded-gltf": {
|
|
83
|
+
for (const promise of this._running) {
|
|
84
|
+
if (promise.url === data.result.url) {
|
|
85
|
+
// process received data and resolve
|
|
86
|
+
processReceivedData(data.result);
|
|
87
|
+
promise.resolve(data.result);
|
|
88
|
+
// cleanup
|
|
89
|
+
const url = promise.url;
|
|
90
|
+
if (url.startsWith("blob:")) {
|
|
91
|
+
URL.revokeObjectURL(url);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
worker.onerror = (error) => {
|
|
99
|
+
console.error("[Worker] Error in gltf-progressive worker:", error);
|
|
100
|
+
};
|
|
101
|
+
worker.postMessage({
|
|
102
|
+
type: 'init',
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
function processReceivedData(data) {
|
|
107
|
+
for (const res of data.geometries) {
|
|
108
|
+
const worker_geometry = res.geometry;
|
|
109
|
+
// console.log(worker_geometry)
|
|
110
|
+
const geo = new BufferGeometry();
|
|
111
|
+
geo.name = worker_geometry.name || "";
|
|
112
|
+
if (worker_geometry.index) {
|
|
113
|
+
const index = worker_geometry.index;
|
|
114
|
+
geo.setIndex(cloneAttribute(index));
|
|
115
|
+
}
|
|
116
|
+
// geo.drawRange = receivedGeometry.drawRange || { start: 0, count: Infinity };
|
|
117
|
+
for (const attrName in worker_geometry.attributes) {
|
|
118
|
+
const attribute = worker_geometry.attributes[attrName];
|
|
119
|
+
const clonedAttribute = cloneAttribute(attribute);
|
|
120
|
+
geo.setAttribute(attrName, clonedAttribute);
|
|
121
|
+
}
|
|
122
|
+
// handle morph attributes
|
|
123
|
+
// TODO: one slow aspect that could be moved to the worker is updating the morph target textures
|
|
124
|
+
if (worker_geometry.morphAttributes) {
|
|
125
|
+
for (const morphName in worker_geometry.morphAttributes) {
|
|
126
|
+
const morphAttributes = worker_geometry.morphAttributes[morphName];
|
|
127
|
+
const morphArray = morphAttributes.map(attribute => {
|
|
128
|
+
return cloneAttribute(attribute);
|
|
129
|
+
});
|
|
130
|
+
geo.morphAttributes[morphName] = morphArray;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
geo.morphTargetsRelative = worker_geometry.morphTargetsRelative ?? false;
|
|
134
|
+
geo.boundingBox = new Box3();
|
|
135
|
+
geo.boundingBox.min = new Vector3(worker_geometry.boundingBox?.min.x, worker_geometry.boundingBox?.min.y, worker_geometry.boundingBox?.min.z);
|
|
136
|
+
geo.boundingBox.max = new Vector3(worker_geometry.boundingBox?.max.x, worker_geometry.boundingBox?.max.y, worker_geometry.boundingBox?.max.z);
|
|
137
|
+
geo.boundingSphere = new Sphere(new Vector3(worker_geometry.boundingSphere?.center.x, worker_geometry.boundingSphere?.center.y, worker_geometry.boundingSphere?.center.z), worker_geometry.boundingSphere?.radius);
|
|
138
|
+
// // handle groups
|
|
139
|
+
if (worker_geometry.groups) {
|
|
140
|
+
for (const group of worker_geometry.groups) {
|
|
141
|
+
geo.addGroup(group.start, group.count, group.materialIndex);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
// // handle user data
|
|
145
|
+
if (worker_geometry.userData) {
|
|
146
|
+
geo.userData = worker_geometry.userData;
|
|
147
|
+
}
|
|
148
|
+
res.geometry = geo;
|
|
149
|
+
}
|
|
150
|
+
for (const res of data.textures) {
|
|
151
|
+
const texture = res.texture;
|
|
152
|
+
let newTexture = null;
|
|
153
|
+
if (texture.isCompressedTexture) {
|
|
154
|
+
const mipmaps = texture.mipmaps;
|
|
155
|
+
const width = texture.image?.width || texture.source?.data?.width || -1;
|
|
156
|
+
const height = texture.image?.height || texture.source?.data?.height || -1;
|
|
157
|
+
newTexture = new CompressedTexture(mipmaps, width, height, texture.format, texture.type, texture.mapping, texture.wrapS, texture.wrapT, texture.magFilter, texture.minFilter, texture.anisotropy, texture.colorSpace);
|
|
158
|
+
}
|
|
159
|
+
else {
|
|
160
|
+
newTexture = new Texture(texture.image, texture.mapping, texture.wrapS, texture.wrapT, texture.magFilter, texture.minFilter, texture.format, texture.type, texture.anisotropy, texture.colorSpace);
|
|
161
|
+
newTexture.mipmaps = texture.mipmaps;
|
|
162
|
+
newTexture.channel = texture.channel;
|
|
163
|
+
newTexture.source.data = texture.source.data;
|
|
164
|
+
newTexture.flipY = texture.flipY;
|
|
165
|
+
newTexture.premultiplyAlpha = texture.premultiplyAlpha;
|
|
166
|
+
newTexture.unpackAlignment = texture.unpackAlignment;
|
|
167
|
+
newTexture.matrix = new Matrix3(...texture.matrix.elements);
|
|
168
|
+
}
|
|
169
|
+
if (!newTexture) {
|
|
170
|
+
console.error("[Worker] Failed to create new texture from received data. Texture is not a CompressedTexture or Texture.");
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
res.texture = newTexture;
|
|
174
|
+
}
|
|
175
|
+
return data;
|
|
176
|
+
}
|
|
177
|
+
function cloneAttribute(attribute) {
|
|
178
|
+
let res = attribute;
|
|
179
|
+
if ("isInterleavedBufferAttribute" in attribute && attribute.isInterleavedBufferAttribute) {
|
|
180
|
+
const data = attribute.data;
|
|
181
|
+
const array = data.array;
|
|
182
|
+
const interleavedBuffer = new InterleavedBuffer(array, data.stride);
|
|
183
|
+
res = new InterleavedBufferAttribute(interleavedBuffer, attribute.itemSize, array.byteOffset, attribute.normalized);
|
|
184
|
+
res.offset = attribute.offset;
|
|
185
|
+
}
|
|
186
|
+
else if ("isBufferAttribute" in attribute && attribute.isBufferAttribute) {
|
|
187
|
+
res = new BufferAttribute(attribute.array, attribute.itemSize, attribute.normalized);
|
|
188
|
+
res.usage = attribute.usage;
|
|
189
|
+
res.gpuType = attribute.gpuType;
|
|
190
|
+
res.updateRanges = attribute.updateRanges;
|
|
191
|
+
}
|
|
192
|
+
return res;
|
|
193
|
+
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
// import { GLTFLoader } from "https://cdn.jsdelivr.net/npm/three@0.179.1/examples/jsm/loaders/GLTFLoader.js";
|
|
2
|
+
|
|
3
|
+
import { MeshoptDecoder } from 'three/examples/jsm/libs/meshopt_decoder.module.js';
|
|
4
|
+
import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader.js';
|
|
5
|
+
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'
|
|
6
|
+
import { KTX2Loader } from 'three/examples/jsm/loaders/KTX2Loader.js';
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
let debug = false;
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @typedef {import("./loader.mainthread").GLTFLoaderWorker_Message} GLTFLoaderWorker_Message
|
|
15
|
+
**/
|
|
16
|
+
|
|
17
|
+
self.onmessage = (msg) => {
|
|
18
|
+
/** @type {GLTFLoaderWorker_Message} */
|
|
19
|
+
const request = msg.data;
|
|
20
|
+
|
|
21
|
+
switch (request.type) {
|
|
22
|
+
case "init":
|
|
23
|
+
break;
|
|
24
|
+
case "load":
|
|
25
|
+
loadGLTF(request);
|
|
26
|
+
break;
|
|
27
|
+
default:
|
|
28
|
+
console.error("[Worker] Unknown message type:", request.type);
|
|
29
|
+
break;
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
self.onerror = (error) => {
|
|
34
|
+
console.error("[Worker] Error:", error);
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* @param {GLTFLoaderWorker_Message} data
|
|
39
|
+
*/
|
|
40
|
+
function postMessage(data) {
|
|
41
|
+
self.postMessage(data);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** @type {GLTFLoader | null} */
|
|
45
|
+
let loader = null;
|
|
46
|
+
|
|
47
|
+
/** @type {DRACOLoader | null} */
|
|
48
|
+
let dracoLoader = null;
|
|
49
|
+
|
|
50
|
+
/** @type {KTX2Loader | null } */
|
|
51
|
+
let ktx2Loader = null;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* @param {GLTFLoaderWorker_Message & { type: "load"}} req
|
|
55
|
+
*/
|
|
56
|
+
async function loadGLTF(req) {
|
|
57
|
+
if(debug) console.debug("[Worker] Loading GLTF from URL:", req.dracoDecoderPath);
|
|
58
|
+
|
|
59
|
+
loader ??= new GLTFLoader();
|
|
60
|
+
|
|
61
|
+
loader.setMeshoptDecoder(MeshoptDecoder);
|
|
62
|
+
|
|
63
|
+
dracoLoader ??= new DRACOLoader();
|
|
64
|
+
dracoLoader.setDecoderConfig({ type: 'js' });
|
|
65
|
+
dracoLoader.setDecoderPath(req.dracoDecoderPath);
|
|
66
|
+
loader.setDRACOLoader(dracoLoader);
|
|
67
|
+
|
|
68
|
+
ktx2Loader ??= new KTX2Loader();
|
|
69
|
+
ktx2Loader.workerConfig = req.ktx2LoaderConfig;
|
|
70
|
+
ktx2Loader.setTranscoderPath(req.ktx2TranscoderPath);
|
|
71
|
+
loader.setKTX2Loader(ktx2Loader);
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
loader.load(req.url, gltf => {
|
|
75
|
+
if(debug) console.log("Loaded", req.url, gltf);
|
|
76
|
+
|
|
77
|
+
/** @type {GLTFLoaderWorker_Message & { type: "loaded-gltf"}} */
|
|
78
|
+
const data = {
|
|
79
|
+
type: "loaded-gltf",
|
|
80
|
+
result: {
|
|
81
|
+
url: req.url,
|
|
82
|
+
geometries: [],
|
|
83
|
+
textures: [],
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
collectData(gltf, data);
|
|
87
|
+
postMessage(data);
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* @param {import("three/examples/jsm/loaders/GLTFLoader").GLTF} gltf
|
|
94
|
+
* @param {GLTFLoaderWorker_Message & { type: "loaded-gltf"}} data
|
|
95
|
+
**/
|
|
96
|
+
function collectData(gltf, data) {
|
|
97
|
+
|
|
98
|
+
const { result } = data;
|
|
99
|
+
|
|
100
|
+
for (const key of gltf.parser.associations.keys()) {
|
|
101
|
+
const cache = gltf.parser.associations.get(key);
|
|
102
|
+
|
|
103
|
+
if (!cache) {
|
|
104
|
+
if(debug) console.warn("[Worker] No cache found for key:", key);
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if ("isTexture" in key && key.isTexture) {
|
|
109
|
+
const texture = /** @type {import("three").Texture} */ ( /** @type {unknown} */ (key));
|
|
110
|
+
const gltf_texture = gltf.parser.json.textures[cache.textures ?? -1];
|
|
111
|
+
result.textures.push({
|
|
112
|
+
texture: texture,
|
|
113
|
+
textureIndex: cache.textures ?? -1,
|
|
114
|
+
extensions: gltf_texture?.extensions ?? {},
|
|
115
|
+
})
|
|
116
|
+
}
|
|
117
|
+
else if ("isMesh" in key && key.isMesh) {
|
|
118
|
+
const mesh = /** @type {import("three").Mesh} */ ( /** @type {unknown} */ (key));
|
|
119
|
+
const meshIndex = cache.meshes ?? -1;
|
|
120
|
+
const primitiveIndex = cache.primitives ?? -1;
|
|
121
|
+
const gltf_mesh = gltf.parser.json.meshes[meshIndex];
|
|
122
|
+
result.geometries.push({
|
|
123
|
+
geometry: mesh.geometry,
|
|
124
|
+
meshIndex: meshIndex,
|
|
125
|
+
primitiveIndex: primitiveIndex,
|
|
126
|
+
extensions: gltf_mesh?.extensions ?? {},
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
else if ("isMaterial" in key && key.isMaterial) {
|
|
130
|
+
// Nothing we need to do here
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// function traverseAndDeleteFunctions(gltf) {
|
|
136
|
+
// const textures = [];
|
|
137
|
+
// gltf.traverse((child) => {
|
|
138
|
+
// if (child.isMesh) {
|
|
139
|
+
// geometries.push(child.geometry);
|
|
140
|
+
// if (child.material) {
|
|
141
|
+
// if (child.material.map) {
|
|
142
|
+
// textures.push(child.material.map);
|
|
143
|
+
// }
|
|
144
|
+
// }
|
|
145
|
+
// }
|
|
146
|
+
// });
|
|
147
|
+
// return {
|
|
148
|
+
// geometries: geometries,
|
|
149
|
+
// textures: textures,
|
|
150
|
+
// }
|
|
151
|
+
// }
|
|
152
|
+
|
|
153
|
+
// function traverseAndDeleteFunctions(obj, seen = new WeakSet()) {
|
|
154
|
+
// if (seen.has(obj)) return;
|
|
155
|
+
// seen.add(obj);
|
|
156
|
+
|
|
157
|
+
// for (const key in obj) {
|
|
158
|
+
// if (typeof obj[key] === "function") {
|
|
159
|
+
// delete obj[key];
|
|
160
|
+
// } else if (typeof obj[key] === "object" && obj[key] !== null) {
|
|
161
|
+
// traverseAndDeleteFunctions(obj[key], seen);
|
|
162
|
+
// }
|
|
163
|
+
// }
|
|
164
|
+
// return obj;
|
|
165
|
+
// }
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@needle-tools/gltf-progressive",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.2.0",
|
|
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": {
|
|
@@ -69,7 +69,7 @@
|
|
|
69
69
|
"nodemon": "^3.1.4",
|
|
70
70
|
"npm-watch": "^0.13.0",
|
|
71
71
|
"three": ">= 0.160.0",
|
|
72
|
-
"vite": "
|
|
72
|
+
"vite": "7"
|
|
73
73
|
},
|
|
74
74
|
"types": "./lib/index.d.ts"
|
|
75
75
|
}
|