@needle-tools/gltf-progressive 3.6.0-alpha.3 → 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.
Files changed (37) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/README.md +42 -8
  3. package/examples/modelviewer-multiple.html +4 -4
  4. package/examples/modelviewer.html +4 -4
  5. package/examples/offscreen/index.html +15 -0
  6. package/examples/offscreen/main.js +98 -0
  7. package/examples/react-three-fiber/index.html +2 -2
  8. package/examples/react-three-fiber/package-lock.json +482 -484
  9. package/examples/react-three-fiber/package.json +4 -4
  10. package/examples/react-three-fiber/src/App.tsx +76 -21
  11. package/examples/react-three-fiber/src/styles.css +2 -4
  12. package/examples/react-three-fiber/vite.config.js +2 -2
  13. package/examples/shared/example-utils.js +297 -0
  14. package/examples/shared/example.css +44 -0
  15. package/examples/shared/runtime.js +34 -0
  16. package/examples/threejs/index.html +6 -10
  17. package/examples/threejs/main.js +5 -23
  18. package/examples/webgpu/index.html +15 -0
  19. package/examples/webgpu/main.js +105 -0
  20. package/examples/worker-rendering/index.html +15 -0
  21. package/examples/worker-rendering/main.js +109 -0
  22. package/examples/worker-rendering/worker.js +166 -0
  23. package/gltf-progressive.js +429 -355
  24. package/gltf-progressive.min.js +9 -9
  25. package/gltf-progressive.umd.cjs +9 -9
  26. package/lib/extension.js +5 -7
  27. package/lib/loaders.d.ts +1 -8
  28. package/lib/loaders.js +15 -2
  29. package/lib/lods.debug.js +1 -1
  30. package/lib/lods.manager.d.ts +3 -0
  31. package/lib/lods.manager.js +62 -18
  32. package/lib/utils.d.ts +1 -1
  33. package/lib/utils.internal.d.ts +27 -0
  34. package/lib/utils.internal.js +68 -25
  35. package/lib/version.js +1 -1
  36. package/lib/worker/loader.mainthread.js +6 -4
  37. package/package.json +8 -3
@@ -0,0 +1,105 @@
1
+ import { loadRuntime } from "../shared/runtime.js";
2
+ import {
3
+ addLoadedScene,
4
+ createOrbitControls,
5
+ createExampleState,
6
+ createSceneChangeButton,
7
+ getInitialSceneIndex,
8
+ getModelUrl,
9
+ markError,
10
+ markReady,
11
+ markSceneLoaded,
12
+ markSceneLoading,
13
+ normalizeSceneIndex,
14
+ resizeRenderer,
15
+ setupScene,
16
+ setupRoomEnvironment,
17
+ trackLODChanges,
18
+ updateStatus,
19
+ } from "../shared/example-utils.js";
20
+
21
+ const state = createExampleState("webgpu");
22
+ const params = new URLSearchParams(location.search);
23
+
24
+ try {
25
+ const runtime = await loadRuntime();
26
+ const THREE = runtime.THREE;
27
+ const WebGPURenderer = runtime.THREE.WebGPURenderer || runtime.THREE_WEBGPU.WebGPURenderer;
28
+ if (!WebGPURenderer) throw new Error("WebGPURenderer is not available in this Three.js runtime.");
29
+ if (!navigator.gpu) throw new Error("WebGPU is not available in this browser.");
30
+
31
+ const canvas = document.getElementById("view");
32
+ const renderer = new WebGPURenderer({ canvas, antialias: true });
33
+ await renderer.init?.();
34
+
35
+ const { scene, camera } = setupScene(THREE, window.innerWidth, window.innerHeight);
36
+ resizeRenderer(renderer, camera, window.innerWidth, window.innerHeight, window.devicePixelRatio);
37
+ setupRoomEnvironment(THREE, runtime.RoomEnvironment, renderer, scene, {
38
+ PMREMGenerator: runtime.THREE_WEBGPU.PMREMGenerator,
39
+ });
40
+ const controls = createOrbitControls(runtime.OrbitControls, camera, canvas);
41
+
42
+ let root = null;
43
+ let sceneIndex = getInitialSceneIndex(params);
44
+ let loadToken = 0;
45
+ let rendererLabel = "";
46
+
47
+ const firstLoader = new runtime.GLTFLoader();
48
+ const lodsManager = runtime.useNeedleProgressive(firstLoader, renderer);
49
+ trackLODChanges(lodsManager, state);
50
+
51
+ window.addEventListener("resize", () => {
52
+ resizeRenderer(renderer, camera, window.innerWidth, window.innerHeight, window.devicePixelRatio);
53
+ });
54
+
55
+ async function loadScene(nextIndex) {
56
+ const token = ++loadToken;
57
+ sceneIndex = normalizeSceneIndex(nextIndex);
58
+ const url = getModelUrl(params, sceneIndex);
59
+ markSceneLoading(state, sceneIndex, url);
60
+
61
+ const loader = token === 1 ? firstLoader : new runtime.GLTFLoader();
62
+ if (token !== 1) runtime.useNeedleProgressive(loader, renderer);
63
+
64
+ const nextRoot = await new Promise((resolve, reject) => {
65
+ loader.load(url, gltf => resolve(addLoadedScene(THREE, scene, gltf, { camera, controls })), undefined, reject);
66
+ });
67
+
68
+ if (token !== loadToken) {
69
+ nextRoot.removeFromParent();
70
+ return;
71
+ }
72
+
73
+ root?.removeFromParent();
74
+ root = nextRoot;
75
+ markSceneLoaded(state, runtime, root);
76
+ if (rendererLabel) markReady(state, rendererLabel);
77
+ }
78
+
79
+ function render() {
80
+ state.frames += 1;
81
+ controls.update();
82
+ renderer.render(scene, camera);
83
+ requestAnimationFrame(render);
84
+ }
85
+
86
+ const backend = getBackendType(renderer);
87
+ if (backend !== "webgpu") throw new Error(`Expected WebGPU backend, got ${backend}.`);
88
+ rendererLabel = backend;
89
+ createSceneChangeButton(() => loadScene(sceneIndex + 1).catch(error => markError(state, error)));
90
+ await loadScene(sceneIndex);
91
+ markReady(state, rendererLabel);
92
+ render();
93
+ }
94
+ catch (error) {
95
+ updateStatus("webgpu unavailable");
96
+ markError(state, error);
97
+ }
98
+
99
+ function getBackendType(renderer) {
100
+ const backend = renderer.backend;
101
+ if (backend?.isWebGPUBackend === true) return "webgpu";
102
+ if (backend?.isWebGLBackend === true) return "webgl";
103
+ if (renderer.isWebGPURenderer === true) return "webgpu";
104
+ return backend?.constructor?.name || renderer.constructor?.name || "unknown";
105
+ }
@@ -0,0 +1,15 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <link rel="icon" href="data:,">
7
+ <link rel="stylesheet" href="../shared/example.css">
8
+ <title>glTF Progressive Worker Rendering</title>
9
+ </head>
10
+ <body>
11
+ <canvas id="view"></canvas>
12
+ <div id="status" class="example-status">loading</div>
13
+ <script type="module" src="./main.js"></script>
14
+ </body>
15
+ </html>
@@ -0,0 +1,109 @@
1
+ import { loadRuntime } from "../shared/runtime.js";
2
+ import {
3
+ applyCameraState,
4
+ createExampleState,
5
+ createOrbitControls,
6
+ createSceneChangeButton,
7
+ markError,
8
+ markReady,
9
+ serializeCamera,
10
+ updateStatus,
11
+ } from "../shared/example-utils.js";
12
+
13
+ const state = createExampleState("worker-rendering");
14
+ const params = new URLSearchParams(location.search);
15
+ const rendererMode = params.get("renderer") === "webgpu" ? "webgpu" : "webgl";
16
+
17
+ try {
18
+ if (!globalThis.OffscreenCanvas) throw new Error("OffscreenCanvas is not available in this browser.");
19
+
20
+ const runtime = await loadRuntime();
21
+ const THREE = runtime.THREE;
22
+ const canvas = document.getElementById("view");
23
+ const camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.01, 200);
24
+ camera.position.set(0.5, 1.3, 2);
25
+ const controls = createOrbitControls(runtime.OrbitControls, camera, canvas);
26
+ const offscreenCanvas = canvas.transferControlToOffscreen();
27
+ const worker = new Worker("./worker.js", { type: "module" });
28
+
29
+ worker.addEventListener("message", event => {
30
+ const message = event.data || {};
31
+ if (message.type === "ready") {
32
+ if (message.camera) applyCameraState(camera, message.camera);
33
+ if (message.fit?.target) {
34
+ controls.target.fromArray(message.fit.target);
35
+ controls.minDistance = message.fit.minDistance;
36
+ controls.maxDistance = message.fit.maxDistance;
37
+ controls.update();
38
+ }
39
+ sendCamera();
40
+ markReady(state, message.renderer || "worker-webgl");
41
+ }
42
+ else if (message.type === "scene-loaded") {
43
+ state.loaded = true;
44
+ state.currentUrl = message.url || "";
45
+ state.sceneIndex = message.sceneIndex || 0;
46
+ state.sceneLoads = message.sceneLoads || state.sceneLoads + 1;
47
+ state.progressiveObjects = message.progressiveObjects || 0;
48
+ }
49
+ else if (message.type === "frame") {
50
+ state.frames = message.frames;
51
+ }
52
+ else if (message.type === "lod-change") {
53
+ state.lodChanges += 1;
54
+ state.lodChangeTypes.push(message.lodType);
55
+ }
56
+ else if (message.type === "error") {
57
+ markError(state, message.message || "Worker rendering failed.");
58
+ }
59
+ });
60
+ worker.addEventListener("error", event => {
61
+ markError(state, event.message || event.error || event);
62
+ });
63
+
64
+ function postResize() {
65
+ camera.aspect = window.innerWidth / window.innerHeight;
66
+ camera.updateProjectionMatrix();
67
+ worker.postMessage({
68
+ type: "resize",
69
+ width: window.innerWidth,
70
+ height: window.innerHeight,
71
+ pixelRatio: window.devicePixelRatio || 1,
72
+ camera: serializeCamera(camera),
73
+ });
74
+ }
75
+
76
+ function sendCamera() {
77
+ worker.postMessage({
78
+ type: "camera",
79
+ camera: serializeCamera(camera),
80
+ });
81
+ }
82
+
83
+ controls.addEventListener("change", sendCamera);
84
+ createSceneChangeButton(() => {
85
+ worker.postMessage({ type: "change-scene" });
86
+ });
87
+
88
+ function updateControls() {
89
+ controls.update();
90
+ requestAnimationFrame(updateControls);
91
+ }
92
+ updateControls();
93
+
94
+ worker.postMessage({
95
+ type: "init",
96
+ canvas: offscreenCanvas,
97
+ width: window.innerWidth,
98
+ height: window.innerHeight,
99
+ pixelRatio: window.devicePixelRatio || 1,
100
+ camera: serializeCamera(camera),
101
+ search: location.search,
102
+ renderer: rendererMode,
103
+ }, [offscreenCanvas]);
104
+ window.addEventListener("resize", postResize);
105
+ updateStatus(rendererMode === "webgpu" ? "worker-webgpu" : "worker-webgl");
106
+ }
107
+ catch (error) {
108
+ markError(state, error);
109
+ }
@@ -0,0 +1,166 @@
1
+ import { loadRuntime } from "../shared/runtime.js";
2
+ import {
3
+ addLoadedScene,
4
+ applyCameraState,
5
+ getInitialSceneIndex,
6
+ getModelUrl,
7
+ markSceneLoaded,
8
+ markSceneLoading,
9
+ normalizeSceneIndex,
10
+ resizeRenderer,
11
+ serializeCamera,
12
+ setupScene,
13
+ setupRoomEnvironment,
14
+ trackLODChanges,
15
+ } from "../shared/example-utils.js";
16
+
17
+ let renderer;
18
+ let camera;
19
+ let scene;
20
+ let root;
21
+ let runtime;
22
+ let THREE;
23
+ let lodsManager;
24
+ let rendererMode = "webgl";
25
+ let rendererLabel = "worker-webgl";
26
+ let frames = 0;
27
+ let width = 1;
28
+ let height = 1;
29
+ let pixelRatio = 1;
30
+ let params = new URLSearchParams();
31
+ let sceneIndex = 0;
32
+ let sceneLoads = 0;
33
+ let loadToken = 0;
34
+ const workerState = {
35
+ loaded: false,
36
+ currentUrl: "",
37
+ sceneIndex: 0,
38
+ sceneLoads: 0,
39
+ progressiveObjects: 0,
40
+ lodChanges: 0,
41
+ lodChangeTypes: [],
42
+ };
43
+
44
+ self.addEventListener("message", event => {
45
+ const message = event.data || {};
46
+ if (message.type === "init") {
47
+ init(message).catch(error => {
48
+ self.postMessage({ type: "error", message: error?.stack || error?.message || String(error) });
49
+ });
50
+ }
51
+ else if (message.type === "resize") {
52
+ width = message.width || width;
53
+ height = message.height || height;
54
+ pixelRatio = message.pixelRatio || pixelRatio;
55
+ if (renderer && camera) resizeRenderer(renderer, camera, width, height, pixelRatio);
56
+ if (camera && message.camera) applyCameraState(camera, message.camera);
57
+ }
58
+ else if (message.type === "camera") {
59
+ if (camera && message.camera) applyCameraState(camera, message.camera);
60
+ }
61
+ else if (message.type === "change-scene") {
62
+ loadScene(sceneIndex + 1).catch(error => {
63
+ self.postMessage({ type: "error", message: error?.stack || error?.message || String(error) });
64
+ });
65
+ }
66
+ });
67
+
68
+ async function init(message) {
69
+ width = message.width || width;
70
+ height = message.height || height;
71
+ pixelRatio = message.pixelRatio || pixelRatio;
72
+ rendererMode = message.renderer === "webgpu" ? "webgpu" : "webgl";
73
+ rendererLabel = rendererMode === "webgpu" ? "worker-webgpu" : "worker-webgl";
74
+
75
+ params = new URLSearchParams(message.search || "");
76
+ sceneIndex = getInitialSceneIndex(params);
77
+ runtime = await loadRuntime({ params });
78
+ THREE = runtime.THREE;
79
+
80
+ renderer = await createRenderer(message.canvas);
81
+ const setup = setupScene(THREE, width, height);
82
+ scene = setup.scene;
83
+ camera = setup.camera;
84
+ resizeRenderer(renderer, camera, width, height, pixelRatio);
85
+ if (message.camera) applyCameraState(camera, message.camera);
86
+ setupRoomEnvironment(THREE, runtime.RoomEnvironment, renderer, scene, rendererMode === "webgpu" ? {
87
+ PMREMGenerator: runtime.THREE_WEBGPU.PMREMGenerator,
88
+ } : undefined);
89
+
90
+ const firstLoad = await loadScene(sceneIndex);
91
+ self.postMessage({ type: "ready", renderer: rendererLabel, camera: serializeCamera(camera), fit: firstLoad?.fit });
92
+
93
+ const requestFrame = callback => setTimeout(() => callback(performance.now()), 16);
94
+ function render() {
95
+ frames += 1;
96
+ renderer.render(scene, camera);
97
+ if (frames === 1 || frames % 10 === 0) self.postMessage({ type: "frame", frames });
98
+ requestFrame(render);
99
+ }
100
+ render();
101
+ }
102
+
103
+ async function createRenderer(canvas) {
104
+ if (rendererMode === "webgpu") {
105
+ if (!self.navigator?.gpu) throw new Error("WebGPU is not available in this worker.");
106
+ const WebGPURenderer = runtime.THREE.WebGPURenderer || runtime.THREE_WEBGPU.WebGPURenderer;
107
+ if (!WebGPURenderer) throw new Error("WebGPURenderer is not available in this Three.js runtime.");
108
+ const webgpuRenderer = new WebGPURenderer({ canvas, antialias: true });
109
+ await webgpuRenderer.init?.();
110
+ const backend = getBackendType(webgpuRenderer);
111
+ if (backend !== "webgpu") throw new Error(`Expected Worker WebGPU backend, got ${backend}.`);
112
+ return webgpuRenderer;
113
+ }
114
+ return new THREE.WebGLRenderer({ canvas, antialias: true });
115
+ }
116
+
117
+ function getBackendType(renderer) {
118
+ const backend = renderer.backend;
119
+ if (backend?.isWebGPUBackend === true) return "webgpu";
120
+ if (backend?.isWebGLBackend === true) return "webgl";
121
+ if (renderer.isWebGPURenderer === true) return "webgpu";
122
+ return backend?.constructor?.name || renderer.constructor?.name || "unknown";
123
+ }
124
+
125
+ async function loadScene(nextIndex) {
126
+ const token = ++loadToken;
127
+ sceneIndex = normalizeSceneIndex(nextIndex);
128
+ const url = getModelUrl(params, sceneIndex);
129
+ markSceneLoading(workerState, sceneIndex, url);
130
+
131
+ const loader = new runtime.GLTFLoader();
132
+ lodsManager = runtime.useNeedleProgressive(loader, renderer);
133
+ if (sceneLoads === 0) {
134
+ trackLODChanges(lodsManager, workerState, event => {
135
+ self.postMessage({ type: "lod-change", lodType: event.type, level: event.level });
136
+ });
137
+ }
138
+
139
+ let fit = null;
140
+ const nextRoot = await new Promise((resolve, reject) => {
141
+ loader.load(url, gltf => resolve(addLoadedScene(THREE, scene, gltf, {
142
+ camera,
143
+ onFit: result => fit = result,
144
+ })), undefined, reject);
145
+ });
146
+
147
+ if (token !== loadToken) {
148
+ nextRoot.removeFromParent();
149
+ return null;
150
+ }
151
+
152
+ root?.removeFromParent();
153
+ root = nextRoot;
154
+ sceneLoads += 1;
155
+ markSceneLoaded(workerState, runtime, root);
156
+ workerState.sceneLoads = sceneLoads;
157
+
158
+ self.postMessage({
159
+ type: "scene-loaded",
160
+ url,
161
+ sceneIndex,
162
+ sceneLoads,
163
+ progressiveObjects: workerState.progressiveObjects,
164
+ });
165
+ return { fit };
166
+ }