@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 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/style.css">
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
- function MyModel() {
11
- const { gl } = useThree()
12
- const url = 'https://engine.needle.tools/demos/gltf-progressive/assets/church/model.glb'
13
- const { scene } = useGLTF(url, false, false, (loader) => {
14
- useNeedleProgressive(loader as any, gl as any);
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
- frameloop="demand"
24
- camera={{ position: [25, 15, 25] }}>
25
- <OrbitControls target={[0 , 10, 0]} />
26
- <ambientLight intensity={1} />
27
- <spotLight position={[10, 10, 10]} angle={0.15} penumbra={1} />
28
- <pointLight position={[-10, -10, -10]} />
29
- <Environment
30
- files="https://dl.polyhaven.org/file/ph-assets/HDRIs/hdr/2k/evening_road_01_2k.hdr"
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
-
@@ -1,3 +1,5 @@
1
+ @import '../../shared/example.css';
2
+
1
3
  * {
2
4
  box-sizing: border-box;
3
5
  }
@@ -10,7 +12,3 @@ body,
10
12
  margin: 0;
11
13
  padding: 0;
12
14
  }
13
-
14
- body {
15
- background: #f0f0f0;
16
- }
@@ -14,7 +14,7 @@ export default defineConfig(async (command) => {
14
14
  plugins: [
15
15
  react(),
16
16
  basicSsl(),
17
- viteCompression({ deleteOriginFile: true }),
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
- <style>
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>
@@ -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.position.y += .01;
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>