@needle-tools/gltf-progressive 3.6.0-alpha.3 → 3.6.0-canary.5401de9
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 +1 -0
- package/README.md +35 -1
- package/examples/offscreen/index.html +15 -0
- package/examples/offscreen/main.js +66 -0
- package/examples/react-three-fiber/index.html +2 -2
- package/examples/react-three-fiber/src/App.tsx +61 -20
- 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 +225 -0
- package/examples/shared/example.css +27 -0
- package/examples/shared/runtime.js +33 -0
- package/examples/threejs/index.html +2 -6
- package/examples/threejs/main.js +5 -23
- package/examples/webgpu/index.html +15 -0
- package/examples/webgpu/main.js +70 -0
- package/examples/worker-rendering/index.html +15 -0
- package/examples/worker-rendering/main.js +91 -0
- package/examples/worker-rendering/worker.js +77 -0
- package/gltf-progressive.js +402 -329
- package/gltf-progressive.min.js +9 -9
- package/gltf-progressive.umd.cjs +9 -9
- package/lib/extension.js +5 -7
- 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 +46 -6
- package/lib/utils.d.ts +1 -1
- package/lib/utils.internal.d.ts +27 -0
- package/lib/utils.internal.js +60 -23
- package/lib/worker/loader.mainthread.js +6 -4
- package/package.json +9 -4
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
|
|
|
6
6
|
|
|
7
7
|
## [3.6.0-alpha.3] - 2026-05-27
|
|
8
8
|
- Fix: stale progressive mesh and texture LOD requests no longer overwrite newer explicit targets, while still allowing useful intermediate LODs to apply during rapid target changes.
|
|
9
|
+
- Fix: progressive worker/debug imports no longer assume `window` exists, and texture LOD selection now uses the renderer pixel ratio so main-thread and worker/offscreen renderers can make matching LOD decisions.
|
|
9
10
|
|
|
10
11
|
## [3.6.0-alpha.2] - 2026-05-26
|
|
11
12
|
- Add: mesh LOD selection can now be reused by renderers that manage their own batching, keeping progressive instanced meshes on independent LOD levels.
|
package/README.md
CHANGED
|
@@ -26,8 +26,13 @@ useNeedleProgressive(gltf_loader, webgl_renderer)
|
|
|
26
26
|
|
|
27
27
|
Examples are in the `/examples` directory. Live versions can be found in the links below.
|
|
28
28
|
|
|
29
|
+
To view them locally, run `npx serve examples` and open the printed URL.
|
|
30
|
+
|
|
29
31
|
- [Loading comparisons](https://stackblitz.com/edit/gltf-progressive-comparison?file=package.json,index.html)
|
|
30
32
|
- [Vanilla three.js](https://engine.needle.tools/demos/gltf-progressive/threejs/) - multiple models and animations
|
|
33
|
+
- `examples/webgpu` - WebGPU renderer
|
|
34
|
+
- `examples/offscreen` - main-thread OffscreenCanvas rendering
|
|
35
|
+
- `examples/worker-rendering` - OffscreenCanvas rendering in a Worker
|
|
31
36
|
- [React Three Fiber](https://engine.needle.tools/demos/gltf-progressive/r3f/)
|
|
32
37
|
- \<model-viewer\>
|
|
33
38
|
- [single \<model-viewer> element](https://engine.needle.tools/demos/gltf-progressive/modelviewer)
|
|
@@ -144,6 +149,36 @@ Create a new class extending `NEEDLE_progressive_plugin` and add your plugin by
|
|
|
144
149
|
### Wait for LODs being loaded
|
|
145
150
|
Call `lodsManager.awaitLoading(<opts?>)` to receive a promise that will resolve when all object LODs that start loading during the next frame have finished to update. Use the optional options parameter to e.g. wait for more frames.
|
|
146
151
|
|
|
152
|
+
### Worker loading and device pixel ratio
|
|
153
|
+
Progressive mesh and texture LOD files can be loaded in a worker by enabling the `gltf-progressive-worker` URL parameter.
|
|
154
|
+
|
|
155
|
+
```txt
|
|
156
|
+
https://example.test/viewer?gltf-progressive-worker
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
LOD selection uses the renderer pixel ratio. For matching results across main-thread, worker, or offscreen rendering, configure the renderer with the same device pixel ratio before calling `useNeedleProgressive` or `LODsManager.get`.
|
|
160
|
+
|
|
161
|
+
```ts
|
|
162
|
+
const renderer = new WebGLRenderer({ canvas });
|
|
163
|
+
renderer.setPixelRatio(devicePixelRatio);
|
|
164
|
+
|
|
165
|
+
useNeedleProgressive(gltfLoader, renderer);
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
In a worker or offscreen renderer, pass the display DPR from the main thread and apply it to the renderer there as well.
|
|
169
|
+
|
|
170
|
+
```ts
|
|
171
|
+
// main thread
|
|
172
|
+
worker.postMessage({ type: "init", devicePixelRatio: window.devicePixelRatio });
|
|
173
|
+
|
|
174
|
+
// worker / offscreen renderer
|
|
175
|
+
const renderer = new WebGLRenderer({ canvas: offscreenCanvas });
|
|
176
|
+
renderer.setPixelRatio(message.devicePixelRatio);
|
|
177
|
+
useNeedleProgressive(gltfLoader, renderer);
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
If the renderer does not expose a pixel ratio, gltf-progressive falls back to `globalThis.devicePixelRatio` and then `1`.
|
|
181
|
+
|
|
147
182
|
### Global LOD level override
|
|
148
183
|
|
|
149
184
|
### LOD Manager settings
|
|
@@ -180,4 +215,3 @@ Read more about the [NEEDLE_progressive extension](./NEEDLE_progressive/README.m
|
|
|
180
215
|
[Twitter](https://twitter.com/NeedleTools) •
|
|
181
216
|
[Discord](https://discord.needle.tools) •
|
|
182
217
|
[Forum](https://forum.needle.tools)
|
|
183
|
-
|
|
@@ -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 Offscreen 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,66 @@
|
|
|
1
|
+
import { loadRuntime } from "../shared/runtime.js";
|
|
2
|
+
import {
|
|
3
|
+
addLoadedScene,
|
|
4
|
+
createOrbitControls,
|
|
5
|
+
createExampleState,
|
|
6
|
+
getModelUrl,
|
|
7
|
+
markError,
|
|
8
|
+
markReady,
|
|
9
|
+
resizeRenderer,
|
|
10
|
+
setupScene,
|
|
11
|
+
setupRoomEnvironment,
|
|
12
|
+
} from "../shared/example-utils.js";
|
|
13
|
+
|
|
14
|
+
const state = createExampleState("offscreen");
|
|
15
|
+
|
|
16
|
+
try {
|
|
17
|
+
if (!globalThis.OffscreenCanvas) throw new Error("OffscreenCanvas is not available in this browser.");
|
|
18
|
+
|
|
19
|
+
const runtime = await loadRuntime();
|
|
20
|
+
const THREE = runtime.THREE;
|
|
21
|
+
const visibleCanvas = document.getElementById("view");
|
|
22
|
+
const visibleContext = visibleCanvas.getContext("bitmaprenderer");
|
|
23
|
+
if (!visibleContext) throw new Error("bitmaprenderer is not available in this browser.");
|
|
24
|
+
|
|
25
|
+
let width = window.innerWidth;
|
|
26
|
+
let height = window.innerHeight;
|
|
27
|
+
let pixelRatio = window.devicePixelRatio || 1;
|
|
28
|
+
const offscreenCanvas = new OffscreenCanvas(Math.max(1, width * pixelRatio), Math.max(1, height * pixelRatio));
|
|
29
|
+
const renderer = new THREE.WebGLRenderer({ canvas: offscreenCanvas, antialias: true });
|
|
30
|
+
const { scene, camera } = setupScene(THREE, width, height);
|
|
31
|
+
setupRoomEnvironment(THREE, runtime.RoomEnvironment, renderer, scene);
|
|
32
|
+
const controls = createOrbitControls(runtime.OrbitControls, camera, visibleCanvas);
|
|
33
|
+
|
|
34
|
+
function resize() {
|
|
35
|
+
width = window.innerWidth;
|
|
36
|
+
height = window.innerHeight;
|
|
37
|
+
pixelRatio = window.devicePixelRatio || 1;
|
|
38
|
+
visibleCanvas.width = Math.max(1, Math.floor(width * pixelRatio));
|
|
39
|
+
visibleCanvas.height = Math.max(1, Math.floor(height * pixelRatio));
|
|
40
|
+
resizeRenderer(renderer, camera, width, height, pixelRatio);
|
|
41
|
+
}
|
|
42
|
+
resize();
|
|
43
|
+
|
|
44
|
+
const loader = new runtime.GLTFLoader();
|
|
45
|
+
runtime.useNeedleProgressive(loader, renderer);
|
|
46
|
+
const root = await new Promise((resolve, reject) => {
|
|
47
|
+
loader.load(getModelUrl(), gltf => resolve(addLoadedScene(THREE, scene, gltf, { camera, controls })), undefined, reject);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
window.addEventListener("resize", resize);
|
|
51
|
+
|
|
52
|
+
function render() {
|
|
53
|
+
state.frames += 1;
|
|
54
|
+
root.rotation.y += 0.01;
|
|
55
|
+
controls.update();
|
|
56
|
+
renderer.render(scene, camera);
|
|
57
|
+
visibleContext.transferFromImageBitmap(offscreenCanvas.transferToImageBitmap());
|
|
58
|
+
requestAnimationFrame(render);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
markReady(state, "offscreen-webgl");
|
|
62
|
+
render();
|
|
63
|
+
}
|
|
64
|
+
catch (error) {
|
|
65
|
+
markError(state, error);
|
|
66
|
+
}
|
|
@@ -14,11 +14,11 @@
|
|
|
14
14
|
<meta property="og:image" content="" />
|
|
15
15
|
|
|
16
16
|
<meta name="robots" content="index,follow">
|
|
17
|
-
<link rel="stylesheet" href="./src/
|
|
17
|
+
<link rel="stylesheet" href="./src/styles.css">
|
|
18
18
|
</head>
|
|
19
19
|
|
|
20
20
|
<body>
|
|
21
21
|
<script type="module" src="./src/index.tsx"></script>
|
|
22
22
|
<div id="root" style="width:100vw; height:100vh;"></div>
|
|
23
23
|
</body>
|
|
24
|
-
</html>
|
|
24
|
+
</html>
|
|
@@ -2,37 +2,78 @@
|
|
|
2
2
|
|
|
3
3
|
/* eslint-disable */
|
|
4
4
|
import * as React from 'react'
|
|
5
|
+
import * as THREE from 'three'
|
|
5
6
|
import { Canvas, useThree } from '@react-three/fiber'
|
|
7
|
+
import { OrbitControls, useGLTF } from '@react-three/drei'
|
|
8
|
+
import { RoomEnvironment } from 'three/examples/jsm/environments/RoomEnvironment.js'
|
|
6
9
|
|
|
7
10
|
import { useNeedleProgressive } from '@needle-tools/gltf-progressive'
|
|
8
|
-
import { Environment, OrbitControls, useGLTF } from '@react-three/drei'
|
|
9
11
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
const { scene } =
|
|
14
|
-
|
|
12
|
+
const modelUrl = 'https://cloud.needle.tools/-/assets/Z23hmXBZ21QnG-Yt9m7-world/the-forgotten-knight-baked.glb'
|
|
13
|
+
|
|
14
|
+
function RoomEnvironmentSetup() {
|
|
15
|
+
const { gl, scene } = useThree()
|
|
16
|
+
|
|
17
|
+
React.useEffect(() => {
|
|
18
|
+
const pmrem = new THREE.PMREMGenerator(gl)
|
|
19
|
+
const environment = pmrem.fromScene(new RoomEnvironment(), 0.04).texture
|
|
20
|
+
scene.environment = environment
|
|
21
|
+
|
|
22
|
+
return () => {
|
|
23
|
+
scene.environment = null
|
|
24
|
+
environment.dispose()
|
|
25
|
+
pmrem.dispose()
|
|
26
|
+
}
|
|
27
|
+
}, [gl, scene])
|
|
28
|
+
|
|
29
|
+
return null
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function Model({ controlsRef }: { controlsRef: React.MutableRefObject<any> }) {
|
|
33
|
+
const { gl, camera } = useThree()
|
|
34
|
+
const { scene } = useGLTF(modelUrl, false, false, (loader) => {
|
|
35
|
+
useNeedleProgressive(loader as any, gl as any)
|
|
15
36
|
})
|
|
37
|
+
|
|
38
|
+
React.useLayoutEffect(() => {
|
|
39
|
+
const box = new THREE.Box3().setFromObject(scene)
|
|
40
|
+
const size = box.getSize(new THREE.Vector3())
|
|
41
|
+
const center = box.getCenter(new THREE.Vector3())
|
|
42
|
+
const maxSize = Math.max(size.x, size.y, size.z, 0.0001)
|
|
43
|
+
const perspectiveCamera = camera as THREE.PerspectiveCamera
|
|
44
|
+
const distance = maxSize / (2 * Math.tan(THREE.MathUtils.degToRad(perspectiveCamera.fov) / 2)) * 1.55
|
|
45
|
+
const direction = new THREE.Vector3(0.45, 0.35, 1).normalize()
|
|
46
|
+
|
|
47
|
+
perspectiveCamera.position.copy(center).addScaledVector(direction, distance)
|
|
48
|
+
perspectiveCamera.near = Math.max(0.01, distance / 100)
|
|
49
|
+
perspectiveCamera.far = Math.max(100, distance * 100)
|
|
50
|
+
perspectiveCamera.updateProjectionMatrix()
|
|
51
|
+
|
|
52
|
+
const controls = controlsRef.current
|
|
53
|
+
if (controls) {
|
|
54
|
+
controls.target.copy(center)
|
|
55
|
+
controls.minDistance = distance * 0.1
|
|
56
|
+
controls.maxDistance = distance * 10
|
|
57
|
+
controls.update()
|
|
58
|
+
}
|
|
59
|
+
}, [camera, controlsRef, scene])
|
|
60
|
+
|
|
16
61
|
return <primitive object={scene} />
|
|
17
62
|
}
|
|
18
63
|
|
|
19
|
-
|
|
20
64
|
export default function App() {
|
|
65
|
+
const controlsRef = React.useRef<any>(null)
|
|
66
|
+
|
|
21
67
|
return (
|
|
22
68
|
<Canvas
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
<
|
|
26
|
-
<
|
|
27
|
-
<
|
|
28
|
-
<
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
// ground={{ height: 5, radius: 40, scale: 10 }}
|
|
32
|
-
/>
|
|
33
|
-
<MyModel />
|
|
69
|
+
camera={{ position: [0.5, 1.3, 2], fov: 60, near: 0.01, far: 200 }}>
|
|
70
|
+
<RoomEnvironmentSetup />
|
|
71
|
+
<gridHelper args={[50, 50, 0x444444, 0x666666]} />
|
|
72
|
+
<directionalLight position={[-50, 20, 50]} intensity={1} />
|
|
73
|
+
<OrbitControls ref={controlsRef} enableDamping dampingFactor={0.08} target={[0, 0.5, 0]} />
|
|
74
|
+
<React.Suspense fallback={null}>
|
|
75
|
+
<Model controlsRef={controlsRef} />
|
|
76
|
+
</React.Suspense>
|
|
34
77
|
</Canvas>
|
|
35
78
|
)
|
|
36
79
|
}
|
|
37
|
-
|
|
38
|
-
|
|
@@ -14,7 +14,7 @@ export default defineConfig(async (command) => {
|
|
|
14
14
|
plugins: [
|
|
15
15
|
react(),
|
|
16
16
|
basicSsl(),
|
|
17
|
-
viteCompression({ deleteOriginFile:
|
|
17
|
+
viteCompression({ deleteOriginFile: false }),
|
|
18
18
|
],
|
|
19
19
|
|
|
20
20
|
server: {
|
|
@@ -36,4 +36,4 @@ export default defineConfig(async (command) => {
|
|
|
36
36
|
}
|
|
37
37
|
}
|
|
38
38
|
}
|
|
39
|
-
});
|
|
39
|
+
});
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
export const DEFAULT_MODEL_URL = "https://cloud.needle.tools/-/assets/Z23hmXBZ21QnG-Yt9m7-world/the-forgotten-knight-baked.glb";
|
|
2
|
+
|
|
3
|
+
export function createExampleState(label) {
|
|
4
|
+
const state = {
|
|
5
|
+
label,
|
|
6
|
+
done: false,
|
|
7
|
+
ok: false,
|
|
8
|
+
errors: [],
|
|
9
|
+
frames: 0,
|
|
10
|
+
loaded: false,
|
|
11
|
+
renderer: "",
|
|
12
|
+
};
|
|
13
|
+
globalThis.__GLTF_PROGRESSIVE_EXAMPLE__ = state;
|
|
14
|
+
|
|
15
|
+
globalThis.addEventListener?.("error", event => {
|
|
16
|
+
state.errors.push(event.message || String(event.error || event));
|
|
17
|
+
});
|
|
18
|
+
globalThis.addEventListener?.("unhandledrejection", event => {
|
|
19
|
+
state.errors.push(event.reason?.message || String(event.reason || event));
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
return state;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function updateStatus(message) {
|
|
26
|
+
const element = globalThis.document?.getElementById("status");
|
|
27
|
+
if (element) element.textContent = message;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function markReady(state, rendererLabel) {
|
|
31
|
+
state.loaded = true;
|
|
32
|
+
state.ok = true;
|
|
33
|
+
state.done = true;
|
|
34
|
+
state.renderer = rendererLabel;
|
|
35
|
+
updateStatus(rendererLabel);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function markError(state, error) {
|
|
39
|
+
const message = error?.stack || error?.message || String(error);
|
|
40
|
+
state.errors.push(message);
|
|
41
|
+
state.ok = false;
|
|
42
|
+
state.done = true;
|
|
43
|
+
updateStatus(message);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function getModelUrl(params = new URLSearchParams(globalThis.location?.search || "")) {
|
|
47
|
+
const asset = params.get("asset");
|
|
48
|
+
if (asset === "minimal") return createMinimalGltfUrl();
|
|
49
|
+
if (asset) return new URL(asset, globalThis.location?.href).href;
|
|
50
|
+
return DEFAULT_MODEL_URL;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function createMinimalGltfUrl() {
|
|
54
|
+
const positions = new Float32Array([
|
|
55
|
+
-0.8, -0.5, 0,
|
|
56
|
+
0.8, -0.5, 0,
|
|
57
|
+
0, 0.8, 0,
|
|
58
|
+
]);
|
|
59
|
+
const normals = new Float32Array([
|
|
60
|
+
0, 0, 1,
|
|
61
|
+
0, 0, 1,
|
|
62
|
+
0, 0, 1,
|
|
63
|
+
]);
|
|
64
|
+
const indices = new Uint16Array([0, 1, 2]);
|
|
65
|
+
const bytes = concatBytes(
|
|
66
|
+
new Uint8Array(positions.buffer),
|
|
67
|
+
new Uint8Array(normals.buffer),
|
|
68
|
+
new Uint8Array(indices.buffer),
|
|
69
|
+
);
|
|
70
|
+
const gltf = {
|
|
71
|
+
asset: { version: "2.0", generator: "gltf-progressive example" },
|
|
72
|
+
scene: 0,
|
|
73
|
+
scenes: [{ nodes: [0] }],
|
|
74
|
+
nodes: [{ mesh: 0, name: "MinimalTriangle" }],
|
|
75
|
+
meshes: [{
|
|
76
|
+
primitives: [{
|
|
77
|
+
attributes: { POSITION: 0, NORMAL: 1 },
|
|
78
|
+
indices: 2,
|
|
79
|
+
material: 0,
|
|
80
|
+
}],
|
|
81
|
+
}],
|
|
82
|
+
materials: [{
|
|
83
|
+
pbrMetallicRoughness: {
|
|
84
|
+
baseColorFactor: [0.2, 0.55, 1, 1],
|
|
85
|
+
roughnessFactor: 0.55,
|
|
86
|
+
metallicFactor: 0,
|
|
87
|
+
},
|
|
88
|
+
}],
|
|
89
|
+
buffers: [{
|
|
90
|
+
uri: `data:application/octet-stream;base64,${base64(bytes)}`,
|
|
91
|
+
byteLength: bytes.byteLength,
|
|
92
|
+
}],
|
|
93
|
+
bufferViews: [
|
|
94
|
+
{ buffer: 0, byteOffset: 0, byteLength: positions.byteLength, target: 34962 },
|
|
95
|
+
{ buffer: 0, byteOffset: positions.byteLength, byteLength: normals.byteLength, target: 34962 },
|
|
96
|
+
{ buffer: 0, byteOffset: positions.byteLength + normals.byteLength, byteLength: indices.byteLength, target: 34963 },
|
|
97
|
+
],
|
|
98
|
+
accessors: [
|
|
99
|
+
{ bufferView: 0, componentType: 5126, count: 3, type: "VEC3", min: [-0.8, -0.5, 0], max: [0.8, 0.8, 0] },
|
|
100
|
+
{ bufferView: 1, componentType: 5126, count: 3, type: "VEC3" },
|
|
101
|
+
{ bufferView: 2, componentType: 5123, count: 3, type: "SCALAR" },
|
|
102
|
+
],
|
|
103
|
+
};
|
|
104
|
+
return `data:model/gltf+json;charset=utf-8,${encodeURIComponent(JSON.stringify(gltf))}`;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function setupScene(THREE, width, height) {
|
|
108
|
+
const scene = new THREE.Scene();
|
|
109
|
+
scene.background = new THREE.Color(0x555555);
|
|
110
|
+
|
|
111
|
+
const camera = new THREE.PerspectiveCamera(60, width / height, 0.01, 200);
|
|
112
|
+
camera.position.set(0.5, 1.3, 2);
|
|
113
|
+
|
|
114
|
+
const grid = new THREE.GridHelper(50, 50, 0x444444, 0x666666);
|
|
115
|
+
scene.add(grid);
|
|
116
|
+
|
|
117
|
+
const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
|
|
118
|
+
directionalLight.position.set(-50, 20, 50);
|
|
119
|
+
scene.add(directionalLight);
|
|
120
|
+
|
|
121
|
+
return { scene, camera };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function setupRoomEnvironment(THREE, RoomEnvironment, renderer, scene, options = {}) {
|
|
125
|
+
if (!RoomEnvironment) return () => { };
|
|
126
|
+
const PMREMGenerator = options.PMREMGenerator || THREE.PMREMGenerator;
|
|
127
|
+
const pmremGenerator = new PMREMGenerator(renderer);
|
|
128
|
+
const environment = pmremGenerator.fromScene(new RoomEnvironment(), 0.04).texture;
|
|
129
|
+
scene.environment = environment;
|
|
130
|
+
return () => {
|
|
131
|
+
environment.dispose?.();
|
|
132
|
+
pmremGenerator.dispose();
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function createOrbitControls(OrbitControls, camera, domElement) {
|
|
137
|
+
const controls = new OrbitControls(camera, domElement);
|
|
138
|
+
controls.enableDamping = true;
|
|
139
|
+
controls.dampingFactor = 0.08;
|
|
140
|
+
controls.target.set(0, 0.5, 0);
|
|
141
|
+
controls.update();
|
|
142
|
+
return controls;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function addLoadedScene(THREE, scene, gltf, options = {}) {
|
|
146
|
+
const root = gltf.scene;
|
|
147
|
+
scene.add(root);
|
|
148
|
+
const fit = fitObjectToView(THREE, options.camera, root, options.controls);
|
|
149
|
+
if (options.onFit) options.onFit(fit);
|
|
150
|
+
return root;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export function fitObjectToView(THREE, camera, root, controls) {
|
|
154
|
+
if (!camera) return null;
|
|
155
|
+
const box = new THREE.Box3().setFromObject(root);
|
|
156
|
+
const size = box.getSize(new THREE.Vector3());
|
|
157
|
+
const center = box.getCenter(new THREE.Vector3());
|
|
158
|
+
const maxSize = Math.max(size.x, size.y, size.z, 0.0001);
|
|
159
|
+
const distance = maxSize / (2 * Math.tan(THREE.MathUtils.degToRad(camera.fov) / 2)) * 1.55;
|
|
160
|
+
const direction = new THREE.Vector3(0.45, 0.35, 1).normalize();
|
|
161
|
+
|
|
162
|
+
camera.position.copy(center).addScaledVector(direction, distance);
|
|
163
|
+
camera.near = Math.max(0.01, distance / 100);
|
|
164
|
+
camera.far = Math.max(100, distance * 100);
|
|
165
|
+
camera.updateProjectionMatrix();
|
|
166
|
+
|
|
167
|
+
if (controls) {
|
|
168
|
+
controls.target.copy(center);
|
|
169
|
+
controls.minDistance = distance * 0.1;
|
|
170
|
+
controls.maxDistance = distance * 10;
|
|
171
|
+
controls.update();
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return {
|
|
175
|
+
target: center.toArray(),
|
|
176
|
+
minDistance: distance * 0.1,
|
|
177
|
+
maxDistance: distance * 10,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export function resizeRenderer(renderer, camera, width, height, pixelRatio = 1) {
|
|
182
|
+
renderer.setPixelRatio?.(pixelRatio);
|
|
183
|
+
renderer.setSize(width, height, false);
|
|
184
|
+
camera.aspect = width / height;
|
|
185
|
+
camera.updateProjectionMatrix();
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export function serializeCamera(camera) {
|
|
189
|
+
return {
|
|
190
|
+
position: camera.position.toArray(),
|
|
191
|
+
quaternion: camera.quaternion.toArray(),
|
|
192
|
+
near: camera.near,
|
|
193
|
+
far: camera.far,
|
|
194
|
+
fov: camera.fov,
|
|
195
|
+
aspect: camera.aspect,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export function applyCameraState(camera, state) {
|
|
200
|
+
if (!state) return;
|
|
201
|
+
camera.position.fromArray(state.position);
|
|
202
|
+
camera.quaternion.fromArray(state.quaternion);
|
|
203
|
+
camera.near = state.near;
|
|
204
|
+
camera.far = state.far;
|
|
205
|
+
camera.fov = state.fov;
|
|
206
|
+
camera.aspect = state.aspect;
|
|
207
|
+
camera.updateProjectionMatrix();
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function concatBytes(...arrays) {
|
|
211
|
+
const total = arrays.reduce((sum, array) => sum + array.byteLength, 0);
|
|
212
|
+
const result = new Uint8Array(total);
|
|
213
|
+
let offset = 0;
|
|
214
|
+
for (const array of arrays) {
|
|
215
|
+
result.set(array, offset);
|
|
216
|
+
offset += array.byteLength;
|
|
217
|
+
}
|
|
218
|
+
return result;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function base64(bytes) {
|
|
222
|
+
let binary = "";
|
|
223
|
+
for (const byte of bytes) binary += String.fromCharCode(byte);
|
|
224
|
+
return btoa(binary);
|
|
225
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
html,
|
|
2
|
+
body {
|
|
3
|
+
height: 100%;
|
|
4
|
+
margin: 0;
|
|
5
|
+
overflow: hidden;
|
|
6
|
+
background: #20242a;
|
|
7
|
+
color: #f5f7fa;
|
|
8
|
+
font-family: system-ui, sans-serif;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
canvas {
|
|
12
|
+
display: block;
|
|
13
|
+
width: 100vw;
|
|
14
|
+
height: 100vh;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.example-status {
|
|
18
|
+
position: fixed;
|
|
19
|
+
left: 12px;
|
|
20
|
+
top: 12px;
|
|
21
|
+
z-index: 10;
|
|
22
|
+
padding: 6px 8px;
|
|
23
|
+
background: rgba(18, 22, 28, 0.78);
|
|
24
|
+
color: #f5f7fa;
|
|
25
|
+
font-size: 12px;
|
|
26
|
+
line-height: 1.3;
|
|
27
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
const DEFAULT_THREE_VERSION = "0.184.0";
|
|
2
|
+
const DEFAULT_PROGRESSIVE_VERSION = "3.6.0-canary.027a4c9";
|
|
3
|
+
|
|
4
|
+
export async function loadRuntime(options = {}) {
|
|
5
|
+
const params = options.params || new URLSearchParams(options.search || globalThis.location?.search || "");
|
|
6
|
+
const runtimeUrl = params.get("runtime");
|
|
7
|
+
if (runtimeUrl) {
|
|
8
|
+
return await import(new URL(runtimeUrl, globalThis.location?.href).href);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const threeVersion = params.get("three") || DEFAULT_THREE_VERSION;
|
|
12
|
+
const threeBase = `https://esm.sh/three@${threeVersion}`;
|
|
13
|
+
const progressiveUrl = params.get("progressive") || `https://esm.sh/@needle-tools/gltf-progressive@${DEFAULT_PROGRESSIVE_VERSION}?deps=three@${threeVersion}`;
|
|
14
|
+
|
|
15
|
+
const [THREE, THREE_WEBGPU, gltfLoaderModule, orbitControlsModule, roomEnvironmentModule, progressiveModule] = await Promise.all([
|
|
16
|
+
import(threeBase),
|
|
17
|
+
import(`${threeBase}/webgpu`),
|
|
18
|
+
import(`${threeBase}/examples/jsm/loaders/GLTFLoader.js`),
|
|
19
|
+
import(`${threeBase}/examples/jsm/controls/OrbitControls.js`),
|
|
20
|
+
import(`${threeBase}/examples/jsm/environments/RoomEnvironment.js`),
|
|
21
|
+
import(progressiveUrl),
|
|
22
|
+
]);
|
|
23
|
+
|
|
24
|
+
return {
|
|
25
|
+
THREE,
|
|
26
|
+
THREE_WEBGPU,
|
|
27
|
+
GLTFLoader: gltfLoaderModule.GLTFLoader,
|
|
28
|
+
OrbitControls: orbitControlsModule.OrbitControls,
|
|
29
|
+
RoomEnvironment: roomEnvironmentModule.RoomEnvironment,
|
|
30
|
+
useNeedleProgressive: progressiveModule.useNeedleProgressive,
|
|
31
|
+
LODsManager: progressiveModule.LODsManager,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
@@ -5,11 +5,7 @@
|
|
|
5
5
|
<meta charset="utf-8">
|
|
6
6
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
7
7
|
<title>Threejs Progressive Loading</title>
|
|
8
|
-
<
|
|
9
|
-
body {
|
|
10
|
-
margin: 0;
|
|
11
|
-
}
|
|
12
|
-
</style>
|
|
8
|
+
<link rel="stylesheet" href="../shared/example.css">
|
|
13
9
|
<script type="importmap">
|
|
14
10
|
{
|
|
15
11
|
"imports": {
|
|
@@ -49,4 +45,4 @@
|
|
|
49
45
|
});
|
|
50
46
|
</script>
|
|
51
47
|
|
|
52
|
-
</html>
|
|
48
|
+
</html>
|
package/examples/threejs/main.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import * as THREE from 'three';
|
|
2
|
-
import { EXRLoader } from 'three/addons/loaders/EXRLoader.js';
|
|
3
2
|
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
|
|
3
|
+
import { RoomEnvironment } from 'three/addons/environments/RoomEnvironment.js';
|
|
4
4
|
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
|
|
5
5
|
import { useNeedleProgressive, getRaycastMesh, useRaycastMeshes } from "@needle-tools/gltf-progressive";
|
|
6
|
+
import { fitObjectToView, setupRoomEnvironment } from "../shared/example-utils.js";
|
|
6
7
|
import { Pane } from 'https://cdn.jsdelivr.net/npm/tweakpane@4.0.3/dist/tweakpane.min.js';
|
|
7
8
|
|
|
8
9
|
|
|
@@ -34,6 +35,8 @@ const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
|
|
|
34
35
|
directionalLight.position.set(-50, 20, 50);
|
|
35
36
|
scene.add(directionalLight);
|
|
36
37
|
|
|
38
|
+
setupRoomEnvironment(THREE, RoomEnvironment, renderer, scene);
|
|
39
|
+
|
|
37
40
|
|
|
38
41
|
// Animate the scene
|
|
39
42
|
function animate() {
|
|
@@ -43,19 +46,6 @@ function animate() {
|
|
|
43
46
|
}
|
|
44
47
|
animate();
|
|
45
48
|
|
|
46
|
-
const environmentTextureUrl = "https://dl.polyhaven.org/file/ph-assets/HDRIs/exr/1k/studio_small_09_1k.exr";
|
|
47
|
-
const pmremGenerator = new THREE.PMREMGenerator(renderer);
|
|
48
|
-
pmremGenerator.compileEquirectangularShader();
|
|
49
|
-
new EXRLoader().load(environmentTextureUrl, texture => {
|
|
50
|
-
const envMap = pmremGenerator.fromEquirectangular(texture).texture;
|
|
51
|
-
scene.environment = envMap;
|
|
52
|
-
texture.dispose();
|
|
53
|
-
pmremGenerator.dispose();
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
49
|
const modelUrls = [
|
|
60
50
|
"https://engine.needle.tools/demos/gltf-progressive/assets/putti gruppe/model.glb",
|
|
61
51
|
"https://engine.needle.tools/demos/gltf-progressive/assets/cyberpunk/model.glb",
|
|
@@ -100,15 +90,7 @@ function loadScene() {
|
|
|
100
90
|
currentScene?.removeFromParent();
|
|
101
91
|
currentScene = gltf.scene;
|
|
102
92
|
scene.add(gltf.scene)
|
|
103
|
-
gltf.scene
|
|
104
|
-
|
|
105
|
-
// the church is huge - scaling it down so we don't have a big difference between the models
|
|
106
|
-
if (url.includes("church")) {
|
|
107
|
-
gltf.scene.scale.multiplyScalar(.1);
|
|
108
|
-
}
|
|
109
|
-
else if (url.includes("cyberpunk")) {
|
|
110
|
-
gltf.scene.scale.multiplyScalar(15);
|
|
111
|
-
}
|
|
93
|
+
fitObjectToView(THREE, camera, gltf.scene, orbit);
|
|
112
94
|
|
|
113
95
|
if (gltf.animations?.length) {
|
|
114
96
|
console.log("Playing animation", gltf.animations)
|
|
@@ -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 WebGPU</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>
|