@needle-tools/gltf-progressive 3.6.0-canary.5401de9 → 3.6.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 +6 -0
- package/README.md +16 -14
- package/examples/modelviewer-multiple.html +4 -4
- package/examples/modelviewer.html +4 -4
- package/examples/offscreen/main.js +39 -7
- 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 +28 -14
- package/examples/shared/example-utils.js +75 -3
- package/examples/shared/example.css +17 -0
- package/examples/shared/runtime.js +2 -1
- package/examples/threejs/index.html +4 -4
- package/examples/webgpu/main.js +42 -7
- package/examples/worker-rendering/main.js +19 -1
- package/examples/worker-rendering/worker.js +108 -19
- package/gltf-progressive.js +100 -99
- package/gltf-progressive.min.js +7 -7
- package/gltf-progressive.umd.cjs +2 -2
- package/lib/loaders.d.ts +8 -1
- package/lib/lods.manager.js +16 -12
- package/lib/utils.d.ts +1 -1
- package/lib/utils.internal.js +8 -2
- package/lib/version.js +1 -1
- package/package.json +3 -3
|
@@ -4,17 +4,17 @@
|
|
|
4
4
|
"main": "src/index.tsx",
|
|
5
5
|
"dependencies": {
|
|
6
6
|
"@needle-tools/gltf-progressive": "file:../..",
|
|
7
|
-
"@react-three/drei": "
|
|
8
|
-
"@react-three/fiber": "
|
|
7
|
+
"@react-three/drei": "9.122.0",
|
|
8
|
+
"@react-three/fiber": "8.18.0",
|
|
9
9
|
"react": "^18.0.0",
|
|
10
10
|
"react-dom": "^18.0.0",
|
|
11
|
-
"three": "0.
|
|
11
|
+
"three": "0.184.0",
|
|
12
12
|
"typescript": "4.7.4"
|
|
13
13
|
},
|
|
14
14
|
"devDependencies": {
|
|
15
15
|
"@types/react": "18.0.15",
|
|
16
16
|
"@types/react-dom": "17.0.0",
|
|
17
|
-
"@types/three": "0.
|
|
17
|
+
"@types/three": "0.184.0",
|
|
18
18
|
"@vitejs/plugin-basic-ssl": "^1.0.1",
|
|
19
19
|
"@vitejs/plugin-react": "^1.3.0",
|
|
20
20
|
"typescript": "4.1.3",
|
|
@@ -8,8 +8,8 @@ import { OrbitControls, useGLTF } from '@react-three/drei'
|
|
|
8
8
|
import { RoomEnvironment } from 'three/examples/jsm/environments/RoomEnvironment.js'
|
|
9
9
|
|
|
10
10
|
import { useNeedleProgressive } from '@needle-tools/gltf-progressive'
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
// @ts-ignore shared plain JS example helpers
|
|
12
|
+
import { MODEL_URLS } from '../../shared/example-utils.js'
|
|
13
13
|
|
|
14
14
|
function RoomEnvironmentSetup() {
|
|
15
15
|
const { gl, scene } = useThree()
|
|
@@ -29,9 +29,9 @@ function RoomEnvironmentSetup() {
|
|
|
29
29
|
return null
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
function Model({ controlsRef }: { controlsRef: React.MutableRefObject<any
|
|
32
|
+
function Model({ controlsRef, url }: { controlsRef: React.MutableRefObject<any>, url: string }) {
|
|
33
33
|
const { gl, camera } = useThree()
|
|
34
|
-
const { scene } = useGLTF(
|
|
34
|
+
const { scene } = useGLTF(url, false, false, (loader) => {
|
|
35
35
|
useNeedleProgressive(loader as any, gl as any)
|
|
36
36
|
})
|
|
37
37
|
|
|
@@ -63,17 +63,31 @@ function Model({ controlsRef }: { controlsRef: React.MutableRefObject<any> }) {
|
|
|
63
63
|
|
|
64
64
|
export default function App() {
|
|
65
65
|
const controlsRef = React.useRef<any>(null)
|
|
66
|
+
const [sceneIndex, setSceneIndex] = React.useState(0)
|
|
67
|
+
const modelUrl = MODEL_URLS[sceneIndex % MODEL_URLS.length]
|
|
68
|
+
|
|
69
|
+
React.useEffect(() => {
|
|
70
|
+
;(globalThis as any).__GLTF_PROGRESSIVE_R3F_EXAMPLE__ = {
|
|
71
|
+
currentUrl: modelUrl,
|
|
72
|
+
sceneIndex,
|
|
73
|
+
}
|
|
74
|
+
}, [modelUrl, sceneIndex])
|
|
66
75
|
|
|
67
76
|
return (
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
<
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
<
|
|
76
|
-
|
|
77
|
-
|
|
77
|
+
<>
|
|
78
|
+
<div className="example-toolbar">
|
|
79
|
+
<button type="button" onClick={() => setSceneIndex((sceneIndex + 1) % MODEL_URLS.length)}>Change scene</button>
|
|
80
|
+
</div>
|
|
81
|
+
<Canvas
|
|
82
|
+
camera={{ position: [0.5, 1.3, 2], fov: 60, near: 0.01, far: 200 }}>
|
|
83
|
+
<RoomEnvironmentSetup />
|
|
84
|
+
<gridHelper args={[50, 50, 0x444444, 0x666666]} />
|
|
85
|
+
<directionalLight position={[-50, 20, 50]} intensity={1} />
|
|
86
|
+
<OrbitControls ref={controlsRef} enableDamping dampingFactor={0.08} target={[0, 0.5, 0]} />
|
|
87
|
+
<React.Suspense fallback={null}>
|
|
88
|
+
<Model key={modelUrl} controlsRef={controlsRef} url={modelUrl} />
|
|
89
|
+
</React.Suspense>
|
|
90
|
+
</Canvas>
|
|
91
|
+
</>
|
|
78
92
|
)
|
|
79
93
|
}
|
|
@@ -1,4 +1,13 @@
|
|
|
1
|
-
export const
|
|
1
|
+
export const MODEL_URLS = [
|
|
2
|
+
"https://engine.needle.tools/demos/gltf-progressive/assets/church/model.glb",
|
|
3
|
+
"https://engine.needle.tools/demos/gltf-progressive/assets/putti gruppe/model.glb",
|
|
4
|
+
"https://engine.needle.tools/demos/gltf-progressive/assets/cyberpunk/model.glb",
|
|
5
|
+
"https://engine.needle.tools/demos/gltf-progressive/assets/robot/model.glb",
|
|
6
|
+
"https://engine.needle.tools/demos/gltf-progressive/assets/vase/model.glb",
|
|
7
|
+
"https://engine.needle.tools/demos/gltf-progressive/assets/jupiter_und_ganymed/model.glb",
|
|
8
|
+
];
|
|
9
|
+
|
|
10
|
+
export const DEFAULT_MODEL_URL = MODEL_URLS[0];
|
|
2
11
|
|
|
3
12
|
export function createExampleState(label) {
|
|
4
13
|
const state = {
|
|
@@ -9,6 +18,12 @@ export function createExampleState(label) {
|
|
|
9
18
|
frames: 0,
|
|
10
19
|
loaded: false,
|
|
11
20
|
renderer: "",
|
|
21
|
+
currentUrl: "",
|
|
22
|
+
sceneIndex: 0,
|
|
23
|
+
sceneLoads: 0,
|
|
24
|
+
progressiveObjects: 0,
|
|
25
|
+
lodChanges: 0,
|
|
26
|
+
lodChangeTypes: [],
|
|
12
27
|
};
|
|
13
28
|
globalThis.__GLTF_PROGRESSIVE_EXAMPLE__ = state;
|
|
14
29
|
|
|
@@ -43,11 +58,68 @@ export function markError(state, error) {
|
|
|
43
58
|
updateStatus(message);
|
|
44
59
|
}
|
|
45
60
|
|
|
46
|
-
export function getModelUrl(params = new URLSearchParams(globalThis.location?.search || "")) {
|
|
61
|
+
export function getModelUrl(params = new URLSearchParams(globalThis.location?.search || ""), sceneIndex = 0) {
|
|
47
62
|
const asset = params.get("asset");
|
|
48
63
|
if (asset === "minimal") return createMinimalGltfUrl();
|
|
49
64
|
if (asset) return new URL(asset, globalThis.location?.href).href;
|
|
50
|
-
return
|
|
65
|
+
return MODEL_URLS[normalizeSceneIndex(sceneIndex)];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function normalizeSceneIndex(sceneIndex) {
|
|
69
|
+
return ((sceneIndex % MODEL_URLS.length) + MODEL_URLS.length) % MODEL_URLS.length;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function getInitialSceneIndex(params = new URLSearchParams(globalThis.location?.search || "")) {
|
|
73
|
+
const scene = Number(params.get("scene"));
|
|
74
|
+
return Number.isFinite(scene) ? normalizeSceneIndex(scene) : 0;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function createSceneChangeButton(onChange) {
|
|
78
|
+
const document = globalThis.document;
|
|
79
|
+
if (!document) return null;
|
|
80
|
+
|
|
81
|
+
const toolbar = document.createElement("div");
|
|
82
|
+
toolbar.className = "example-toolbar";
|
|
83
|
+
const button = document.createElement("button");
|
|
84
|
+
button.type = "button";
|
|
85
|
+
button.textContent = "Change scene";
|
|
86
|
+
button.addEventListener("click", () => onChange());
|
|
87
|
+
toolbar.append(button);
|
|
88
|
+
document.body.append(toolbar);
|
|
89
|
+
return button;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function markSceneLoading(state, sceneIndex, url) {
|
|
93
|
+
state.sceneIndex = normalizeSceneIndex(sceneIndex);
|
|
94
|
+
state.currentUrl = url;
|
|
95
|
+
updateStatus("loading");
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function markSceneLoaded(state, runtime, root) {
|
|
99
|
+
state.loaded = true;
|
|
100
|
+
state.sceneLoads += 1;
|
|
101
|
+
state.progressiveObjects = countProgressiveObjects(runtime, root);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function trackLODChanges(lodsManager, state, notify) {
|
|
105
|
+
return lodsManager.addEventListener?.("changed", event => {
|
|
106
|
+
state.lodChanges += 1;
|
|
107
|
+
state.lodChangeTypes.push(event.type);
|
|
108
|
+
if (notify) notify(event);
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function countProgressiveObjects(runtime, root) {
|
|
113
|
+
const progressive = runtime.NEEDLE_progressive;
|
|
114
|
+
if (!progressive || !root) return 0;
|
|
115
|
+
|
|
116
|
+
let count = 0;
|
|
117
|
+
root.traverse?.(object => {
|
|
118
|
+
if (object?.isMesh && progressive.hasLODLevelAvailable(object)) count += 1;
|
|
119
|
+
const material = object?.material;
|
|
120
|
+
if (material && progressive.hasLODLevelAvailable(material)) count += 1;
|
|
121
|
+
});
|
|
122
|
+
return count;
|
|
51
123
|
}
|
|
52
124
|
|
|
53
125
|
export function createMinimalGltfUrl() {
|
|
@@ -25,3 +25,20 @@ canvas {
|
|
|
25
25
|
font-size: 12px;
|
|
26
26
|
line-height: 1.3;
|
|
27
27
|
}
|
|
28
|
+
|
|
29
|
+
.example-toolbar {
|
|
30
|
+
position: fixed;
|
|
31
|
+
right: 12px;
|
|
32
|
+
top: 12px;
|
|
33
|
+
z-index: 10;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
.example-toolbar button {
|
|
37
|
+
padding: 6px 9px;
|
|
38
|
+
border: 1px solid rgba(245, 247, 250, 0.35);
|
|
39
|
+
background: rgba(18, 22, 28, 0.78);
|
|
40
|
+
color: #f5f7fa;
|
|
41
|
+
font: inherit;
|
|
42
|
+
font-size: 12px;
|
|
43
|
+
cursor: pointer;
|
|
44
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
const DEFAULT_THREE_VERSION = "0.184.0";
|
|
2
|
-
const DEFAULT_PROGRESSIVE_VERSION = "3.6.0-canary.
|
|
2
|
+
const DEFAULT_PROGRESSIVE_VERSION = "3.6.0-canary.5401de9";
|
|
3
3
|
|
|
4
4
|
export async function loadRuntime(options = {}) {
|
|
5
5
|
const params = options.params || new URLSearchParams(options.search || globalThis.location?.search || "");
|
|
@@ -29,5 +29,6 @@ export async function loadRuntime(options = {}) {
|
|
|
29
29
|
RoomEnvironment: roomEnvironmentModule.RoomEnvironment,
|
|
30
30
|
useNeedleProgressive: progressiveModule.useNeedleProgressive,
|
|
31
31
|
LODsManager: progressiveModule.LODsManager,
|
|
32
|
+
NEEDLE_progressive: progressiveModule.NEEDLE_progressive,
|
|
32
33
|
};
|
|
33
34
|
}
|
|
@@ -9,10 +9,10 @@
|
|
|
9
9
|
<script type="importmap">
|
|
10
10
|
{
|
|
11
11
|
"imports": {
|
|
12
|
-
"three": "https://cdn.jsdelivr.net/npm/three@
|
|
13
|
-
"three/addons/": "https://cdn.jsdelivr.net/npm/three@
|
|
14
|
-
"three/examples/": "https://cdn.jsdelivr.net/npm/three@
|
|
15
|
-
"@needle-tools/gltf-progressive": "https://cdn.jsdelivr.net/npm/@needle-tools/gltf-progressive/gltf-progressive.min.js"
|
|
12
|
+
"three": "https://cdn.jsdelivr.net/npm/three@0.184.0/build/three.module.js",
|
|
13
|
+
"three/addons/": "https://cdn.jsdelivr.net/npm/three@0.184.0/examples/jsm/",
|
|
14
|
+
"three/examples/": "https://cdn.jsdelivr.net/npm/three@0.184.0/examples/",
|
|
15
|
+
"@needle-tools/gltf-progressive": "https://cdn.jsdelivr.net/npm/@needle-tools/gltf-progressive@3.6.0-canary.5401de9/gltf-progressive.min.js"
|
|
16
16
|
}
|
|
17
17
|
}
|
|
18
18
|
</script>
|
package/examples/webgpu/main.js
CHANGED
|
@@ -3,16 +3,23 @@ import {
|
|
|
3
3
|
addLoadedScene,
|
|
4
4
|
createOrbitControls,
|
|
5
5
|
createExampleState,
|
|
6
|
+
createSceneChangeButton,
|
|
7
|
+
getInitialSceneIndex,
|
|
6
8
|
getModelUrl,
|
|
7
9
|
markError,
|
|
8
10
|
markReady,
|
|
11
|
+
markSceneLoaded,
|
|
12
|
+
markSceneLoading,
|
|
13
|
+
normalizeSceneIndex,
|
|
9
14
|
resizeRenderer,
|
|
10
15
|
setupScene,
|
|
11
16
|
setupRoomEnvironment,
|
|
17
|
+
trackLODChanges,
|
|
12
18
|
updateStatus,
|
|
13
19
|
} from "../shared/example-utils.js";
|
|
14
20
|
|
|
15
21
|
const state = createExampleState("webgpu");
|
|
22
|
+
const params = new URLSearchParams(location.search);
|
|
16
23
|
|
|
17
24
|
try {
|
|
18
25
|
const runtime = await loadRuntime();
|
|
@@ -32,20 +39,45 @@ try {
|
|
|
32
39
|
});
|
|
33
40
|
const controls = createOrbitControls(runtime.OrbitControls, camera, canvas);
|
|
34
41
|
|
|
35
|
-
|
|
36
|
-
|
|
42
|
+
let root = null;
|
|
43
|
+
let sceneIndex = getInitialSceneIndex(params);
|
|
44
|
+
let loadToken = 0;
|
|
45
|
+
let rendererLabel = "";
|
|
37
46
|
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
47
|
+
const firstLoader = new runtime.GLTFLoader();
|
|
48
|
+
const lodsManager = runtime.useNeedleProgressive(firstLoader, renderer);
|
|
49
|
+
trackLODChanges(lodsManager, state);
|
|
41
50
|
|
|
42
51
|
window.addEventListener("resize", () => {
|
|
43
52
|
resizeRenderer(renderer, camera, window.innerWidth, window.innerHeight, window.devicePixelRatio);
|
|
44
53
|
});
|
|
45
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
|
+
|
|
46
79
|
function render() {
|
|
47
80
|
state.frames += 1;
|
|
48
|
-
root.rotation.y += 0.01;
|
|
49
81
|
controls.update();
|
|
50
82
|
renderer.render(scene, camera);
|
|
51
83
|
requestAnimationFrame(render);
|
|
@@ -53,7 +85,10 @@ try {
|
|
|
53
85
|
|
|
54
86
|
const backend = getBackendType(renderer);
|
|
55
87
|
if (backend !== "webgpu") throw new Error(`Expected WebGPU backend, got ${backend}.`);
|
|
56
|
-
|
|
88
|
+
rendererLabel = backend;
|
|
89
|
+
createSceneChangeButton(() => loadScene(sceneIndex + 1).catch(error => markError(state, error)));
|
|
90
|
+
await loadScene(sceneIndex);
|
|
91
|
+
markReady(state, rendererLabel);
|
|
57
92
|
render();
|
|
58
93
|
}
|
|
59
94
|
catch (error) {
|
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
applyCameraState,
|
|
4
4
|
createExampleState,
|
|
5
5
|
createOrbitControls,
|
|
6
|
+
createSceneChangeButton,
|
|
6
7
|
markError,
|
|
7
8
|
markReady,
|
|
8
9
|
serializeCamera,
|
|
@@ -10,6 +11,8 @@ import {
|
|
|
10
11
|
} from "../shared/example-utils.js";
|
|
11
12
|
|
|
12
13
|
const state = createExampleState("worker-rendering");
|
|
14
|
+
const params = new URLSearchParams(location.search);
|
|
15
|
+
const rendererMode = params.get("renderer") === "webgpu" ? "webgpu" : "webgl";
|
|
13
16
|
|
|
14
17
|
try {
|
|
15
18
|
if (!globalThis.OffscreenCanvas) throw new Error("OffscreenCanvas is not available in this browser.");
|
|
@@ -36,9 +39,20 @@ try {
|
|
|
36
39
|
sendCamera();
|
|
37
40
|
markReady(state, message.renderer || "worker-webgl");
|
|
38
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
|
+
}
|
|
39
49
|
else if (message.type === "frame") {
|
|
40
50
|
state.frames = message.frames;
|
|
41
51
|
}
|
|
52
|
+
else if (message.type === "lod-change") {
|
|
53
|
+
state.lodChanges += 1;
|
|
54
|
+
state.lodChangeTypes.push(message.lodType);
|
|
55
|
+
}
|
|
42
56
|
else if (message.type === "error") {
|
|
43
57
|
markError(state, message.message || "Worker rendering failed.");
|
|
44
58
|
}
|
|
@@ -67,6 +81,9 @@ try {
|
|
|
67
81
|
}
|
|
68
82
|
|
|
69
83
|
controls.addEventListener("change", sendCamera);
|
|
84
|
+
createSceneChangeButton(() => {
|
|
85
|
+
worker.postMessage({ type: "change-scene" });
|
|
86
|
+
});
|
|
70
87
|
|
|
71
88
|
function updateControls() {
|
|
72
89
|
controls.update();
|
|
@@ -82,9 +99,10 @@ try {
|
|
|
82
99
|
pixelRatio: window.devicePixelRatio || 1,
|
|
83
100
|
camera: serializeCamera(camera),
|
|
84
101
|
search: location.search,
|
|
102
|
+
renderer: rendererMode,
|
|
85
103
|
}, [offscreenCanvas]);
|
|
86
104
|
window.addEventListener("resize", postResize);
|
|
87
|
-
updateStatus("worker-webgl");
|
|
105
|
+
updateStatus(rendererMode === "webgpu" ? "worker-webgpu" : "worker-webgl");
|
|
88
106
|
}
|
|
89
107
|
catch (error) {
|
|
90
108
|
markError(state, error);
|
|
@@ -2,20 +2,44 @@ import { loadRuntime } from "../shared/runtime.js";
|
|
|
2
2
|
import {
|
|
3
3
|
addLoadedScene,
|
|
4
4
|
applyCameraState,
|
|
5
|
+
getInitialSceneIndex,
|
|
5
6
|
getModelUrl,
|
|
7
|
+
markSceneLoaded,
|
|
8
|
+
markSceneLoading,
|
|
9
|
+
normalizeSceneIndex,
|
|
6
10
|
resizeRenderer,
|
|
7
11
|
serializeCamera,
|
|
8
12
|
setupScene,
|
|
9
13
|
setupRoomEnvironment,
|
|
14
|
+
trackLODChanges,
|
|
10
15
|
} from "../shared/example-utils.js";
|
|
11
16
|
|
|
12
17
|
let renderer;
|
|
13
18
|
let camera;
|
|
19
|
+
let scene;
|
|
14
20
|
let root;
|
|
21
|
+
let runtime;
|
|
22
|
+
let THREE;
|
|
23
|
+
let lodsManager;
|
|
24
|
+
let rendererMode = "webgl";
|
|
25
|
+
let rendererLabel = "worker-webgl";
|
|
15
26
|
let frames = 0;
|
|
16
27
|
let width = 1;
|
|
17
28
|
let height = 1;
|
|
18
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
|
+
};
|
|
19
43
|
|
|
20
44
|
self.addEventListener("message", event => {
|
|
21
45
|
const message = event.data || {};
|
|
@@ -34,44 +58,109 @@ self.addEventListener("message", event => {
|
|
|
34
58
|
else if (message.type === "camera") {
|
|
35
59
|
if (camera && message.camera) applyCameraState(camera, message.camera);
|
|
36
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
|
+
}
|
|
37
66
|
});
|
|
38
67
|
|
|
39
68
|
async function init(message) {
|
|
40
69
|
width = message.width || width;
|
|
41
70
|
height = message.height || height;
|
|
42
71
|
pixelRatio = message.pixelRatio || pixelRatio;
|
|
72
|
+
rendererMode = message.renderer === "webgpu" ? "webgpu" : "webgl";
|
|
73
|
+
rendererLabel = rendererMode === "webgpu" ? "worker-webgpu" : "worker-webgl";
|
|
43
74
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
75
|
+
params = new URLSearchParams(message.search || "");
|
|
76
|
+
sceneIndex = getInitialSceneIndex(params);
|
|
77
|
+
runtime = await loadRuntime({ params });
|
|
78
|
+
THREE = runtime.THREE;
|
|
47
79
|
|
|
48
|
-
renderer =
|
|
80
|
+
renderer = await createRenderer(message.canvas);
|
|
49
81
|
const setup = setupScene(THREE, width, height);
|
|
50
|
-
|
|
82
|
+
scene = setup.scene;
|
|
51
83
|
camera = setup.camera;
|
|
52
84
|
resizeRenderer(renderer, camera, width, height, pixelRatio);
|
|
53
85
|
if (message.camera) applyCameraState(camera, message.camera);
|
|
54
|
-
setupRoomEnvironment(THREE, runtime.RoomEnvironment, renderer, scene
|
|
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);
|
|
55
130
|
|
|
56
131
|
const loader = new runtime.GLTFLoader();
|
|
57
|
-
runtime.useNeedleProgressive(loader, renderer);
|
|
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
|
+
|
|
58
139
|
let fit = null;
|
|
59
|
-
|
|
60
|
-
loader.load(
|
|
140
|
+
const nextRoot = await new Promise((resolve, reject) => {
|
|
141
|
+
loader.load(url, gltf => resolve(addLoadedScene(THREE, scene, gltf, {
|
|
61
142
|
camera,
|
|
62
143
|
onFit: result => fit = result,
|
|
63
144
|
})), undefined, reject);
|
|
64
145
|
});
|
|
65
146
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
function render() {
|
|
70
|
-
frames += 1;
|
|
71
|
-
root.rotation.y += 0.01;
|
|
72
|
-
renderer.render(scene, camera);
|
|
73
|
-
if (frames % 10 === 0) self.postMessage({ type: "frame", frames });
|
|
74
|
-
requestFrame(render);
|
|
147
|
+
if (token !== loadToken) {
|
|
148
|
+
nextRoot.removeFromParent();
|
|
149
|
+
return null;
|
|
75
150
|
}
|
|
76
|
-
|
|
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 };
|
|
77
166
|
}
|