@needle-tools/gltf-progressive 4.0.0-alpha → 4.0.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.
@@ -0,0 +1,239 @@
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
+ }
30
+ const debug = getParam("debugprogressive");
31
+ export function isDebugMode() {
32
+ return debug;
33
+ }
34
+ export function getParam(name) {
35
+ if (typeof window === "undefined")
36
+ return false;
37
+ const url = new URL(window.location.href);
38
+ const param = url.searchParams.get(name);
39
+ if (param == null || param === "0" || param === "false")
40
+ return false;
41
+ if (param === "")
42
+ return true;
43
+ return param;
44
+ }
45
+ export function resolveUrl(source, uri) {
46
+ if (uri === undefined || source === undefined) {
47
+ return uri;
48
+ }
49
+ if (uri.startsWith("./") ||
50
+ uri.startsWith("http") ||
51
+ uri.startsWith("data:") ||
52
+ uri.startsWith("blob:")) {
53
+ return uri;
54
+ }
55
+ // TODO: why not just use new URL(uri, source).href ?
56
+ const pathIndex = source.lastIndexOf("/");
57
+ if (pathIndex >= 0) {
58
+ // Take the source uri as the base path
59
+ const basePath = source.substring(0, pathIndex + 1);
60
+ // make sure we don't have double slashes
61
+ while (basePath.endsWith("/") && uri.startsWith("/"))
62
+ uri = uri.substring(1);
63
+ // Append the relative uri
64
+ const newUri = basePath + uri;
65
+ // newUri = new URL(newUri, globalThis.location.href).href;
66
+ return newUri;
67
+ }
68
+ return uri;
69
+ }
70
+ /** Check if the current device is a mobile device.
71
+ * @returns `true` if it's a phone or tablet
72
+ */
73
+ export function isMobileDevice() {
74
+ if (_ismobile !== undefined)
75
+ return _ismobile;
76
+ _ismobile = /iPhone|iPad|iPod|Android|IEMobile/i.test(navigator.userAgent);
77
+ if (getParam("debugprogressive"))
78
+ console.log("[glTF Progressive]: isMobileDevice", _ismobile);
79
+ return _ismobile;
80
+ }
81
+ let _ismobile;
82
+ /**
83
+ * Check if we are running in a development server (localhost or ip address).
84
+ * @returns `true` if we are running in a development server (localhost or ip address).
85
+ */
86
+ export function isDevelopmentServer() {
87
+ if (typeof window === "undefined")
88
+ return false;
89
+ const url = new URL(window.location.href);
90
+ const isLocalhostOrIpAddress = url.hostname === "localhost" || /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(url.hostname);
91
+ const isDevelopment = url.hostname === "127.0.0.1" || isLocalhostOrIpAddress;
92
+ return isDevelopment;
93
+ }
94
+ /**
95
+ * A promise queue that limits the number of concurrent promises.
96
+ * Use the `slot` method to request a slot for a promise with a specific key. The returned promise resolves to an object with a `use` method that can be called to add the promise to the queue.
97
+ */
98
+ export class PromiseQueue {
99
+ maxConcurrent;
100
+ _running = new Map();
101
+ _queue = [];
102
+ debug = false;
103
+ constructor(maxConcurrent, opts = {}) {
104
+ this.maxConcurrent = maxConcurrent;
105
+ this.debug = opts.debug ?? false;
106
+ window.requestAnimationFrame(this.tick);
107
+ }
108
+ tick = () => {
109
+ this.internalUpdate();
110
+ setTimeout(this.tick, 10);
111
+ };
112
+ /**
113
+ * Request a slot for a promise with a specific key. This function returns a promise with a `use` method that can be called to add the promise to the queue.
114
+ */
115
+ slot(key) {
116
+ if (this.debug)
117
+ console.debug(`[PromiseQueue]: Requesting slot for key ${key}, running: ${this._running.size}, waiting: ${this._queue.length}`);
118
+ return new Promise((resolve) => {
119
+ this._queue.push({ key, resolve });
120
+ });
121
+ }
122
+ add(key, promise) {
123
+ if (this._running.has(key))
124
+ return;
125
+ this._running.set(key, promise);
126
+ promise.finally(() => {
127
+ this._running.delete(key);
128
+ if (this.debug)
129
+ console.debug(`[PromiseQueue]: Promise finished now running: ${this._running.size}, waiting: ${this._queue.length}. (finished ${key})`);
130
+ });
131
+ if (this.debug)
132
+ console.debug(`[PromiseQueue]: Added new promise, now running: ${this._running.size}, waiting: ${this._queue.length}. (added ${key})`);
133
+ }
134
+ internalUpdate() {
135
+ // Run for as many free slots as we can
136
+ const diff = this.maxConcurrent - this._running.size;
137
+ for (let i = 0; i < diff && this._queue.length > 0; i++) {
138
+ if (this.debug)
139
+ console.debug(`[PromiseQueue]: Running ${this._running.size} promises, waiting for ${this._queue.length} more.`);
140
+ const { key, resolve } = this._queue.shift();
141
+ resolve({
142
+ use: (promise) => this.add(key, promise)
143
+ });
144
+ }
145
+ }
146
+ }
147
+ // #region Texture Memory
148
+ export function determineTextureMemoryInBytes(texture) {
149
+ const img = texture.image;
150
+ const width = img?.width ?? 0;
151
+ const height = img?.height ?? 0;
152
+ const depth = img?.depth ?? 1;
153
+ const mipLevels = Math.floor(Math.log2(Math.max(width, height, depth))) + 1;
154
+ const bytesPerPixel = getBytesPerPixel(texture);
155
+ const totalBytes = (width * height * depth * bytesPerPixel * (1 - Math.pow(0.25, mipLevels))) / (1 - 0.25);
156
+ return totalBytes;
157
+ }
158
+ function getBytesPerPixel(texture) {
159
+ // Determine channel count from format
160
+ let channels = 4; // Default RGBA
161
+ const format = texture.format;
162
+ if (format === RedFormat)
163
+ channels = 1;
164
+ else if (format === RedIntegerFormat)
165
+ channels = 1;
166
+ else if (format === RGFormat)
167
+ channels = 2;
168
+ else if (format === RGIntegerFormat)
169
+ channels = 2;
170
+ else if (format === RGBFormat)
171
+ channels = 3;
172
+ else if (format === 1029)
173
+ channels = 3; // RGBIntegerFormat (not exported in r183)
174
+ else if (format === RGBAFormat)
175
+ channels = 4;
176
+ else if (format === RGBAIntegerFormat)
177
+ channels = 4;
178
+ // Determine bytes per channel from type
179
+ let bytesPerChannel = 1; // UnsignedByteType default
180
+ const type = texture.type;
181
+ if (type === 1009)
182
+ bytesPerChannel = 1; // UnsignedByteType
183
+ else if (type === 1010)
184
+ bytesPerChannel = 1; // ByteType
185
+ else if (type === 1011)
186
+ bytesPerChannel = 2; // ShortType
187
+ else if (type === 1012)
188
+ bytesPerChannel = 2; // UnsignedShortType
189
+ else if (type === 1013)
190
+ bytesPerChannel = 4; // IntType
191
+ else if (type === 1014)
192
+ bytesPerChannel = 4; // UnsignedIntType
193
+ else if (type === 1015)
194
+ bytesPerChannel = 4; // FloatType
195
+ else if (type === 1016)
196
+ bytesPerChannel = 2; // HalfFloatType
197
+ const bytesPerPixel = channels * bytesPerChannel;
198
+ return bytesPerPixel;
199
+ }
200
+ // #region GPU
201
+ let rendererInfo;
202
+ /**
203
+ * Detect the GPU memory of the current device. This is a very rough estimate based on the renderer information, and may not be accurate. It returns the estimated memory in MB, or `undefined` if it cannot be detected.
204
+ */
205
+ export function detectGPUMemory() {
206
+ if (rendererInfo !== undefined) {
207
+ return rendererInfo?.estimatedMemory;
208
+ }
209
+ const canvas = document.createElement('canvas');
210
+ const powerPreference = "high-performance";
211
+ const gl = canvas.getContext('webgl', { powerPreference }) || canvas.getContext('experimental-webgl', { powerPreference });
212
+ if (!gl) {
213
+ return undefined;
214
+ }
215
+ if ("getExtension" in gl) {
216
+ const debugInfo = gl.getExtension('WEBGL_debug_renderer_info');
217
+ if (debugInfo) {
218
+ const vendor = gl.getParameter(debugInfo.UNMASKED_VENDOR_WEBGL);
219
+ const renderer = gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL);
220
+ // Estimate memory based on renderer information (this is a very rough estimate)
221
+ let estimatedMemory = 512;
222
+ if (/NVIDIA/i.test(renderer)) {
223
+ estimatedMemory = 2048;
224
+ }
225
+ else if (/AMD/i.test(renderer)) {
226
+ estimatedMemory = 1024;
227
+ }
228
+ else if (/Intel/i.test(renderer)) {
229
+ estimatedMemory = 512;
230
+ }
231
+ rendererInfo = { vendor, renderer, estimatedMemory };
232
+ return estimatedMemory;
233
+ }
234
+ }
235
+ else {
236
+ rendererInfo = null;
237
+ }
238
+ return undefined;
239
+ }
package/lib/utils.js ADDED
@@ -0,0 +1,82 @@
1
+ import { BufferGeometry, Mesh } from "three";
2
+ export const isSSR = typeof window === "undefined" && typeof document === "undefined";
3
+ const $raycastmesh = Symbol("needle:raycast-mesh");
4
+ /**
5
+ * 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
6
+ * @param obj the object to get the raycast mesh from
7
+ * @returns the raycast mesh or null if not set
8
+ */
9
+ export function getRaycastMesh(obj) {
10
+ if (obj?.[$raycastmesh] instanceof BufferGeometry) {
11
+ return obj[$raycastmesh];
12
+ }
13
+ return null;
14
+ }
15
+ /**
16
+ * Set the raycast mesh for an object.
17
+ * 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
18
+ * @param obj the object to set the raycast mesh for
19
+ * @param geom the raycast mesh
20
+ */
21
+ export function registerRaycastMesh(obj, geom) {
22
+ if (obj.type === "Mesh" || obj.type === "SkinnedMesh") {
23
+ const existing = getRaycastMesh(obj);
24
+ if (!existing) {
25
+ const clone = shallowCloneGeometry(geom);
26
+ // remove LODs userdata to not update the geometry if the raycast mesh is rendered in the scene
27
+ clone.userData = { isRaycastMesh: true };
28
+ obj[$raycastmesh] = clone;
29
+ }
30
+ }
31
+ }
32
+ /**
33
+ * Call this method to enable raycasting with the lowpoly raycast meshes (if available) for all meshes in the scene.
34
+ * This is useful for performance optimization when the scene contains high poly meshes that are not visible to the camera.
35
+ * @example
36
+ * ```ts
37
+ * // call to enable raycasting with low poly raycast meshes
38
+ * useRaycastMeshes();
39
+ *
40
+ * // then use the raycaster as usual
41
+ * const raycaster = new Raycaster();
42
+ * raycaster.setFromCamera(mouse, camera);
43
+ * const intersects = raycaster.intersectObjects(scene.children, true);
44
+ * ```
45
+ */
46
+ export function useRaycastMeshes(enabled = true) {
47
+ if (enabled) {
48
+ // if the method is already patched we don't need to do it again
49
+ if (_originalRaycastMethod)
50
+ return;
51
+ const originalMethod = _originalRaycastMethod = Mesh.prototype.raycast;
52
+ Mesh.prototype.raycast = function (raycaster, intersects) {
53
+ const self = this;
54
+ const raycastMesh = getRaycastMesh(self);
55
+ let prevGeomtry;
56
+ if (raycastMesh && self.isMesh) {
57
+ prevGeomtry = self.geometry;
58
+ self.geometry = raycastMesh;
59
+ }
60
+ originalMethod.call(this, raycaster, intersects);
61
+ if (prevGeomtry) {
62
+ self.geometry = prevGeomtry;
63
+ }
64
+ };
65
+ }
66
+ else {
67
+ if (!_originalRaycastMethod)
68
+ return;
69
+ Mesh.prototype.raycast = _originalRaycastMethod;
70
+ _originalRaycastMethod = null;
71
+ }
72
+ }
73
+ let _originalRaycastMethod = null;
74
+ /** Creates a clone without copying the data */
75
+ function shallowCloneGeometry(geom) {
76
+ const clone = new BufferGeometry();
77
+ for (const key in geom.attributes) {
78
+ clone.setAttribute(key, geom.getAttribute(key));
79
+ }
80
+ clone.setIndex(geom.getIndex());
81
+ return clone;
82
+ }
@@ -0,0 +1 @@
1
+ export declare const version = "";
package/lib/version.js ADDED
@@ -0,0 +1,4 @@
1
+ // replaced at build time
2
+ export const version = "4.0.0-alpha";
3
+ globalThis["GLTF_PROGRESSIVE_VERSION"] = version;
4
+ console.debug(`[gltf-progressive] version ${version || "-"}`);
@@ -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
+ // }
@@ -0,0 +1,45 @@
1
+ import { BufferGeometry, Texture, WebGLRenderer } from "three";
2
+ import type { KTX2LoaderWorkerConfig } from "three/examples/jsm/loaders/KTX2Loader.js";
3
+ type GLTFLoaderWorkerOptions = {
4
+ debug?: boolean;
5
+ };
6
+ type WorkerLoadResult = {
7
+ url: string;
8
+ geometries: Array<{
9
+ geometry: BufferGeometry;
10
+ meshIndex: number;
11
+ primitiveIndex: number;
12
+ extensions: Record<string, any>;
13
+ }>;
14
+ textures: Array<{
15
+ texture: Texture;
16
+ textureIndex: number;
17
+ extensions: Record<string, any>;
18
+ }>;
19
+ };
20
+ export declare function getWorker(opts?: GLTFLoaderWorkerOptions): Promise<GLTFLoaderWorker>;
21
+ export type { GLTFLoaderWorker, GLTFLoaderWorkerOptions, WorkerLoadResult };
22
+ /** @internal */
23
+ export type GLTFLoaderWorker_Message = {
24
+ type: 'init';
25
+ } | {
26
+ type: 'load';
27
+ url: string;
28
+ dracoDecoderPath: string;
29
+ ktx2TranscoderPath: string;
30
+ ktx2LoaderConfig: KTX2LoaderWorkerConfig;
31
+ } | {
32
+ type: "loaded-gltf";
33
+ result: WorkerLoadResult;
34
+ };
35
+ declare class GLTFLoaderWorker {
36
+ private readonly worker;
37
+ static createWorker(opts: GLTFLoaderWorkerOptions): Promise<GLTFLoaderWorker>;
38
+ private _running;
39
+ private _webglRenderer;
40
+ load(url: string | URL, opts?: {
41
+ renderer?: WebGLRenderer;
42
+ }): Promise<WorkerLoadResult>;
43
+ private _debug;
44
+ private constructor();
45
+ }