sceneview-web 1.2.0 → 1.4.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/filament/filament.js +1411 -0
- package/filament/filament.wasm +0 -0
- package/model-viewer.min.js +1083 -0
- package/package.json +1 -1
- package/sceneview.js +267 -151
- package/three/OrbitControls.js +1417 -0
- package/three/RGBELoader.js +450 -0
- package/three/RoomEnvironment.js +148 -0
- package/three/three.module.js +53044 -0
package/package.json
CHANGED
package/sceneview.js
CHANGED
|
@@ -4,45 +4,30 @@
|
|
|
4
4
|
* One line to render a 3D model:
|
|
5
5
|
* SceneView.modelViewer("canvas", "model.glb")
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
* Just include one script:
|
|
9
|
-
* <script src="js/sceneview.js"></script>
|
|
10
|
-
*
|
|
11
|
-
* Powered by Filament.js (Google's PBR renderer, WASM).
|
|
7
|
+
* Powered by Filament.js v1.70.1 (Google's PBR renderer, WASM).
|
|
12
8
|
* https://sceneview.github.io
|
|
13
9
|
*
|
|
14
|
-
* @version 1.
|
|
10
|
+
* @version 1.4.0
|
|
15
11
|
* @license MIT
|
|
16
12
|
*/
|
|
17
13
|
(function(global) {
|
|
18
14
|
'use strict';
|
|
19
15
|
|
|
20
|
-
|
|
16
|
+
// Filament.js is loaded via <script> tag in HTML (js/filament/filament.js)
|
|
17
|
+
// This avoids dynamic script injection issues with WASM resolution.
|
|
21
18
|
|
|
22
19
|
/**
|
|
23
|
-
*
|
|
24
|
-
* Returns a Promise that resolves when the Filament global is available.
|
|
20
|
+
* Wait for Filament to be available (loaded by the script tag).
|
|
25
21
|
*/
|
|
26
22
|
function _ensureFilament() {
|
|
27
23
|
return new Promise(function(resolve, reject) {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
if (existing) {
|
|
36
|
-
existing.addEventListener('load', function() { resolve(); });
|
|
37
|
-
existing.addEventListener('error', function() { reject(new Error('SceneView: Failed to load Filament.js from CDN')); });
|
|
38
|
-
return;
|
|
39
|
-
}
|
|
40
|
-
// Inject script tag
|
|
41
|
-
var script = document.createElement('script');
|
|
42
|
-
script.src = FILAMENT_CDN;
|
|
43
|
-
script.onload = function() { resolve(); };
|
|
44
|
-
script.onerror = function() { reject(new Error('SceneView: Failed to load Filament.js from CDN (' + FILAMENT_CDN + ')')); };
|
|
45
|
-
document.head.appendChild(script);
|
|
24
|
+
if (typeof Filament !== 'undefined') { resolve(); return; }
|
|
25
|
+
// Poll briefly in case the script tag hasn't finished loading
|
|
26
|
+
var attempts = 0;
|
|
27
|
+
var check = setInterval(function() {
|
|
28
|
+
if (typeof Filament !== 'undefined') { clearInterval(check); resolve(); }
|
|
29
|
+
if (++attempts > 100) { clearInterval(check); reject(new Error('SceneView: Filament.js not loaded')); }
|
|
30
|
+
}, 50);
|
|
46
31
|
});
|
|
47
32
|
}
|
|
48
33
|
|
|
@@ -61,7 +46,7 @@
|
|
|
61
46
|
this._cameraEntity = cameraEntity;
|
|
62
47
|
this._loader = loader;
|
|
63
48
|
this._asset = null;
|
|
64
|
-
this._angle = 0;
|
|
49
|
+
this._angle = 0.785; // Start at ~45° like model-viewer
|
|
65
50
|
this._autoRotate = true;
|
|
66
51
|
this._orbitRadius = 3.5;
|
|
67
52
|
this._orbitHeight = 0.8;
|
|
@@ -69,6 +54,12 @@
|
|
|
69
54
|
this._running = true;
|
|
70
55
|
this._isDragging = false;
|
|
71
56
|
this._lastMouse = { x: 0, y: 0 };
|
|
57
|
+
// Inertia for smooth orbit deceleration
|
|
58
|
+
this._velocityAngle = 0;
|
|
59
|
+
this._velocityHeight = 0;
|
|
60
|
+
this._dampingFactor = 0.95;
|
|
61
|
+
this._wantsAutoRotate = true; // Remember initial preference for resume after drag
|
|
62
|
+
this._autoRotateTimer = null;
|
|
72
63
|
this._setupControls();
|
|
73
64
|
this._setupResizeObserver();
|
|
74
65
|
this._startRenderLoop();
|
|
@@ -78,34 +69,29 @@
|
|
|
78
69
|
loadModel(url) {
|
|
79
70
|
var self = this;
|
|
80
71
|
return new Promise(function(resolve, reject) {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
try {
|
|
95
|
-
self._showModel(url);
|
|
96
|
-
resolve(self);
|
|
97
|
-
} catch (e) {
|
|
98
|
-
reject(e);
|
|
99
|
-
}
|
|
100
|
-
});
|
|
72
|
+
fetch(url)
|
|
73
|
+
.then(function(resp) { return resp.arrayBuffer(); })
|
|
74
|
+
.then(function(buffer) {
|
|
75
|
+
Filament.assets = Filament.assets || {};
|
|
76
|
+
Filament.assets[url] = new Uint8Array(buffer);
|
|
77
|
+
try {
|
|
78
|
+
self._showModel(url);
|
|
79
|
+
resolve(self);
|
|
80
|
+
} catch (e) {
|
|
81
|
+
reject(e);
|
|
82
|
+
}
|
|
83
|
+
})
|
|
84
|
+
.catch(reject);
|
|
101
85
|
});
|
|
102
86
|
}
|
|
103
87
|
|
|
104
88
|
_showModel(url) {
|
|
105
89
|
// Remove previous model
|
|
106
90
|
if (this._asset) {
|
|
107
|
-
|
|
108
|
-
|
|
91
|
+
try {
|
|
92
|
+
this._asset.getRenderableEntities().forEach(function(e) { this._scene.remove(e); }.bind(this));
|
|
93
|
+
this._scene.remove(this._asset.getRoot());
|
|
94
|
+
} catch (e) { /* ignore cleanup errors */ }
|
|
109
95
|
this._asset = null;
|
|
110
96
|
}
|
|
111
97
|
|
|
@@ -120,7 +106,7 @@
|
|
|
120
106
|
this._scene.addEntities(asset.getRenderableEntities());
|
|
121
107
|
this._asset = asset;
|
|
122
108
|
|
|
123
|
-
// Auto-frame
|
|
109
|
+
// Auto-frame the model
|
|
124
110
|
try {
|
|
125
111
|
var bbox = asset.getBoundingBox();
|
|
126
112
|
var cx = (bbox.min[0] + bbox.max[0]) / 2;
|
|
@@ -132,95 +118,154 @@
|
|
|
132
118
|
var maxDim = Math.max(sx, sy, sz);
|
|
133
119
|
if (maxDim > 0) {
|
|
134
120
|
this._orbitTarget = [cx, cy, cz];
|
|
135
|
-
|
|
121
|
+
// Tighter framing than before (1.8x instead of 2.5x)
|
|
122
|
+
this._orbitRadius = maxDim * 1.8;
|
|
136
123
|
this._orbitHeight = cy;
|
|
137
124
|
}
|
|
138
|
-
} catch (e) {
|
|
139
|
-
// getBoundingBox not available on all assets, use defaults
|
|
140
|
-
}
|
|
125
|
+
} catch (e) { /* use defaults */ }
|
|
141
126
|
}
|
|
142
127
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
128
|
+
setAutoRotate(enabled) { this._autoRotate = enabled; this._wantsAutoRotate = enabled; return this; }
|
|
129
|
+
setCameraDistance(d) { this._orbitRadius = d; return this; }
|
|
130
|
+
|
|
131
|
+
setBackgroundColor(r, g, b, a) {
|
|
132
|
+
this._renderer.setClearOptions({ clearColor: [r, g, b, a !== undefined ? a : 1], clear: true });
|
|
146
133
|
return this;
|
|
147
134
|
}
|
|
148
135
|
|
|
149
|
-
/**
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
return
|
|
136
|
+
/** Add a model to the scene (without removing existing ones) */
|
|
137
|
+
addModel(url) {
|
|
138
|
+
var self = this;
|
|
139
|
+
return new Promise(function(resolve, reject) {
|
|
140
|
+
fetch(url)
|
|
141
|
+
.then(function(resp) { return resp.arrayBuffer(); })
|
|
142
|
+
.then(function(buffer) {
|
|
143
|
+
var data = new Uint8Array(buffer);
|
|
144
|
+
try {
|
|
145
|
+
var asset = self._loader.createAsset(data);
|
|
146
|
+
if (!asset) { reject(new Error('Failed to parse: ' + url)); return; }
|
|
147
|
+
asset.loadResources();
|
|
148
|
+
self._scene.addEntity(asset.getRoot());
|
|
149
|
+
self._scene.addEntities(asset.getRenderableEntities());
|
|
150
|
+
resolve(asset);
|
|
151
|
+
} catch (e) { reject(e); }
|
|
152
|
+
})
|
|
153
|
+
.catch(reject);
|
|
154
|
+
});
|
|
153
155
|
}
|
|
154
156
|
|
|
155
|
-
/**
|
|
156
|
-
|
|
157
|
-
this.
|
|
158
|
-
return
|
|
157
|
+
/** Load a GLB from a Uint8Array buffer directly */
|
|
158
|
+
loadGLBBuffer(buffer, key) {
|
|
159
|
+
var asset = this._loader.createAsset(buffer);
|
|
160
|
+
if (!asset) return null;
|
|
161
|
+
asset.loadResources();
|
|
162
|
+
this._scene.addEntity(asset.getRoot());
|
|
163
|
+
this._scene.addEntities(asset.getRenderableEntities());
|
|
164
|
+
return asset;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** Remove an asset from the scene */
|
|
168
|
+
removeAsset(asset) {
|
|
169
|
+
if (!asset) return;
|
|
170
|
+
try {
|
|
171
|
+
asset.getRenderableEntities().forEach(function(e) { this._scene.remove(e); }.bind(this));
|
|
172
|
+
this._scene.remove(asset.getRoot());
|
|
173
|
+
} catch (e) { /* ignore cleanup errors */ }
|
|
159
174
|
}
|
|
160
175
|
|
|
161
|
-
/**
|
|
176
|
+
/** Access engine for advanced Filament operations */
|
|
177
|
+
get engine() { return this._engine; }
|
|
178
|
+
get scene() { return this._scene; }
|
|
179
|
+
|
|
162
180
|
dispose() {
|
|
163
181
|
this._running = false;
|
|
164
|
-
if (this._resizeObserver)
|
|
165
|
-
|
|
166
|
-
}
|
|
167
|
-
Filament.Engine.destroy(this._engine);
|
|
182
|
+
if (this._resizeObserver) this._resizeObserver.disconnect();
|
|
183
|
+
try { Filament.Engine.destroy(this._engine); } catch (e) { /* already destroyed */ }
|
|
168
184
|
}
|
|
169
185
|
|
|
170
|
-
// --- Private ---
|
|
171
|
-
|
|
172
186
|
_setupControls() {
|
|
173
187
|
var canvas = this._canvas;
|
|
174
188
|
var self = this;
|
|
175
189
|
|
|
176
|
-
// Mouse orbit
|
|
177
190
|
canvas.addEventListener('mousedown', function(e) {
|
|
178
191
|
self._isDragging = true;
|
|
179
192
|
self._lastMouse = { x: e.clientX, y: e.clientY };
|
|
180
193
|
self._autoRotate = false;
|
|
194
|
+
self._velocityAngle = 0;
|
|
195
|
+
self._velocityHeight = 0;
|
|
196
|
+
if (self._autoRotateTimer) { clearTimeout(self._autoRotateTimer); self._autoRotateTimer = null; }
|
|
181
197
|
});
|
|
182
198
|
canvas.addEventListener('mousemove', function(e) {
|
|
183
199
|
if (!self._isDragging) return;
|
|
184
|
-
|
|
185
|
-
|
|
200
|
+
var dx = (e.clientX - self._lastMouse.x) * 0.005;
|
|
201
|
+
var dy = (e.clientY - self._lastMouse.y) * 0.01;
|
|
202
|
+
self._velocityAngle = -dx;
|
|
203
|
+
self._velocityHeight = dy;
|
|
204
|
+
self._angle -= dx;
|
|
205
|
+
self._orbitHeight += dy;
|
|
186
206
|
self._lastMouse = { x: e.clientX, y: e.clientY };
|
|
187
207
|
});
|
|
188
|
-
canvas.addEventListener('mouseup', function() {
|
|
189
|
-
|
|
208
|
+
canvas.addEventListener('mouseup', function() {
|
|
209
|
+
self._isDragging = false;
|
|
210
|
+
// Resume auto-rotate after 3s idle (like model-viewer)
|
|
211
|
+
if (self._wantsAutoRotate) {
|
|
212
|
+
self._autoRotateTimer = setTimeout(function() { self._autoRotate = true; }, 3000);
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
canvas.addEventListener('mouseleave', function() {
|
|
216
|
+
self._isDragging = false;
|
|
217
|
+
if (self._wantsAutoRotate) {
|
|
218
|
+
self._autoRotateTimer = setTimeout(function() { self._autoRotate = true; }, 3000);
|
|
219
|
+
}
|
|
220
|
+
});
|
|
190
221
|
|
|
191
|
-
// Scroll zoom
|
|
192
222
|
canvas.addEventListener('wheel', function(e) {
|
|
193
223
|
e.preventDefault();
|
|
194
224
|
self._orbitRadius *= (1 + e.deltaY * 0.001);
|
|
195
225
|
self._orbitRadius = Math.max(0.5, Math.min(50, self._orbitRadius));
|
|
196
226
|
}, { passive: false });
|
|
197
227
|
|
|
198
|
-
// Touch orbit
|
|
199
228
|
canvas.addEventListener('touchstart', function(e) {
|
|
200
229
|
if (e.touches.length === 1) {
|
|
201
230
|
self._isDragging = true;
|
|
202
231
|
self._lastMouse = { x: e.touches[0].clientX, y: e.touches[0].clientY };
|
|
203
232
|
self._autoRotate = false;
|
|
233
|
+
self._velocityAngle = 0;
|
|
234
|
+
self._velocityHeight = 0;
|
|
235
|
+
if (self._autoRotateTimer) { clearTimeout(self._autoRotateTimer); self._autoRotateTimer = null; }
|
|
204
236
|
}
|
|
205
237
|
});
|
|
206
238
|
canvas.addEventListener('touchmove', function(e) {
|
|
207
239
|
if (!self._isDragging || e.touches.length !== 1) return;
|
|
208
240
|
e.preventDefault();
|
|
209
|
-
|
|
210
|
-
|
|
241
|
+
var dx = (e.touches[0].clientX - self._lastMouse.x) * 0.005;
|
|
242
|
+
var dy = (e.touches[0].clientY - self._lastMouse.y) * 0.01;
|
|
243
|
+
self._velocityAngle = -dx;
|
|
244
|
+
self._velocityHeight = dy;
|
|
245
|
+
self._angle -= dx;
|
|
246
|
+
self._orbitHeight += dy;
|
|
211
247
|
self._lastMouse = { x: e.touches[0].clientX, y: e.touches[0].clientY };
|
|
212
248
|
}, { passive: false });
|
|
213
|
-
canvas.addEventListener('touchend', function() {
|
|
249
|
+
canvas.addEventListener('touchend', function() {
|
|
250
|
+
self._isDragging = false;
|
|
251
|
+
if (self._wantsAutoRotate) {
|
|
252
|
+
self._autoRotateTimer = setTimeout(function() { self._autoRotate = true; }, 3000);
|
|
253
|
+
}
|
|
254
|
+
});
|
|
214
255
|
}
|
|
215
256
|
|
|
216
257
|
_setupResizeObserver() {
|
|
217
258
|
var self = this;
|
|
218
259
|
this._resizeObserver = new ResizeObserver(function() {
|
|
219
260
|
var canvas = self._canvas;
|
|
220
|
-
|
|
221
|
-
canvas.
|
|
261
|
+
var dpr = Math.min(devicePixelRatio, 2); // Cap at 2x for performance
|
|
262
|
+
canvas.width = canvas.clientWidth * dpr;
|
|
263
|
+
canvas.height = canvas.clientHeight * dpr;
|
|
222
264
|
self._view.setViewport([0, 0, canvas.width, canvas.height]);
|
|
223
|
-
self._camera.setProjectionFov(
|
|
265
|
+
self._camera.setProjectionFov(
|
|
266
|
+
self._fov || 45, canvas.width / canvas.height, 0.1, 1000,
|
|
267
|
+
Filament.Camera$Fov.VERTICAL
|
|
268
|
+
);
|
|
224
269
|
});
|
|
225
270
|
this._resizeObserver.observe(this._canvas);
|
|
226
271
|
}
|
|
@@ -229,7 +274,19 @@
|
|
|
229
274
|
var self = this;
|
|
230
275
|
function render() {
|
|
231
276
|
if (!self._running) return;
|
|
232
|
-
|
|
277
|
+
|
|
278
|
+
// Auto-rotate: 30°/sec ÷ 60fps (matches model-viewer)
|
|
279
|
+
if (self._autoRotate) self._angle += 0.00873;
|
|
280
|
+
|
|
281
|
+
// Inertia damping after drag release
|
|
282
|
+
if (!self._isDragging) {
|
|
283
|
+
self._angle += self._velocityAngle;
|
|
284
|
+
self._orbitHeight += self._velocityHeight;
|
|
285
|
+
self._velocityAngle *= self._dampingFactor;
|
|
286
|
+
self._velocityHeight *= self._dampingFactor;
|
|
287
|
+
if (Math.abs(self._velocityAngle) < 0.00005) self._velocityAngle = 0;
|
|
288
|
+
if (Math.abs(self._velocityHeight) < 0.00005) self._velocityHeight = 0;
|
|
289
|
+
}
|
|
233
290
|
|
|
234
291
|
var t = self._orbitTarget;
|
|
235
292
|
var r = self._orbitRadius;
|
|
@@ -240,20 +297,28 @@
|
|
|
240
297
|
[0, 1, 0]
|
|
241
298
|
);
|
|
242
299
|
|
|
243
|
-
if (self._renderer.beginFrame(self._swapChain)) {
|
|
244
|
-
self._renderer.render(self._swapChain, self._view);
|
|
245
|
-
self._renderer.endFrame();
|
|
246
|
-
}
|
|
247
300
|
self._engine.execute();
|
|
301
|
+
try {
|
|
302
|
+
if (self._renderer.beginFrame(self._swapChain)) {
|
|
303
|
+
self._renderer.renderView(self._view);
|
|
304
|
+
self._renderer.endFrame();
|
|
305
|
+
}
|
|
306
|
+
} catch (e) {
|
|
307
|
+
// Filament 1.70 may need different render call
|
|
308
|
+
console.error('SceneView render error:', e.message);
|
|
309
|
+
self._running = false;
|
|
310
|
+
}
|
|
248
311
|
requestAnimationFrame(render);
|
|
249
312
|
}
|
|
250
313
|
render();
|
|
251
314
|
}
|
|
252
315
|
}
|
|
253
316
|
|
|
317
|
+
// Singleton guard — prevent multiple engine creations on same canvas
|
|
318
|
+
var _activeCanvases = new Set();
|
|
319
|
+
|
|
254
320
|
/**
|
|
255
|
-
*
|
|
256
|
-
* Called inside a Filament.init() callback where WASM is guaranteed ready.
|
|
321
|
+
* Set up Filament engine, scene, lights on a canvas.
|
|
257
322
|
*/
|
|
258
323
|
function _createEngine(canvasOrId, options) {
|
|
259
324
|
options = options || {};
|
|
@@ -261,11 +326,21 @@
|
|
|
261
326
|
var canvas = typeof canvasOrId === 'string'
|
|
262
327
|
? document.getElementById(canvasOrId)
|
|
263
328
|
: canvasOrId;
|
|
264
|
-
|
|
265
329
|
if (!canvas) throw new Error('Canvas not found: ' + canvasOrId);
|
|
266
330
|
|
|
267
|
-
|
|
268
|
-
|
|
331
|
+
// Prevent double initialization on the same canvas
|
|
332
|
+
if (_activeCanvases.has(canvas)) {
|
|
333
|
+
console.warn('SceneView: Canvas already initialized, skipping');
|
|
334
|
+
return null;
|
|
335
|
+
}
|
|
336
|
+
_activeCanvases.add(canvas);
|
|
337
|
+
|
|
338
|
+
var dpr = Math.min(devicePixelRatio, 2);
|
|
339
|
+
// Ensure canvas has actual layout dimensions (not default 300x150)
|
|
340
|
+
var cssW = canvas.clientWidth || canvas.offsetWidth || 500;
|
|
341
|
+
var cssH = canvas.clientHeight || canvas.offsetHeight || 500;
|
|
342
|
+
canvas.width = cssW * dpr;
|
|
343
|
+
canvas.height = cssH * dpr;
|
|
269
344
|
|
|
270
345
|
var engine = Filament.Engine.create(canvas);
|
|
271
346
|
var scene = engine.createScene();
|
|
@@ -282,11 +357,19 @@
|
|
|
282
357
|
var bg = options.backgroundColor || [0.05, 0.06, 0.1, 1.0];
|
|
283
358
|
renderer.setClearOptions({ clearColor: bg, clear: true });
|
|
284
359
|
|
|
285
|
-
var
|
|
286
|
-
camera.setProjectionFov(
|
|
360
|
+
var fov = options.fov || 45;
|
|
361
|
+
camera.setProjectionFov(fov, canvas.width / canvas.height, 0.1, 1000, Filament.Camera$Fov.VERTICAL);
|
|
287
362
|
camera.lookAt([0, 1, 5], [0, 0, 0], [0, 1, 0]);
|
|
288
363
|
|
|
289
|
-
//
|
|
364
|
+
// --- Post-processing quality ---
|
|
365
|
+
try {
|
|
366
|
+
view.setAmbientOcclusionOptions({
|
|
367
|
+
enabled: true, radius: 0.3, bias: 0.0005, intensity: 1.0, quality: 1
|
|
368
|
+
});
|
|
369
|
+
} catch (e) { /* skip */ }
|
|
370
|
+
|
|
371
|
+
// --- 3-point studio lighting ---
|
|
372
|
+
// Sun/key light — warm, strong
|
|
290
373
|
var sun = Filament.EntityManager.get().create();
|
|
291
374
|
Filament.LightManager.Builder(Filament.LightManager$Type.SUN)
|
|
292
375
|
.color([0.98, 0.92, 0.89])
|
|
@@ -298,90 +381,123 @@
|
|
|
298
381
|
.build(engine, sun);
|
|
299
382
|
scene.addEntity(sun);
|
|
300
383
|
|
|
301
|
-
// Fill light
|
|
384
|
+
// Fill light — cool, softer
|
|
302
385
|
var fill = Filament.EntityManager.get().create();
|
|
303
386
|
Filament.LightManager.Builder(Filament.LightManager$Type.DIRECTIONAL)
|
|
304
|
-
.color([0.
|
|
305
|
-
.intensity(
|
|
387
|
+
.color([0.7, 0.75, 0.9])
|
|
388
|
+
.intensity(60000)
|
|
306
389
|
.direction([-0.5, 0.5, 1.0])
|
|
307
390
|
.build(engine, fill);
|
|
308
391
|
scene.addEntity(fill);
|
|
309
392
|
|
|
310
|
-
//
|
|
311
|
-
var
|
|
393
|
+
// Back/rim light — edge highlight
|
|
394
|
+
var back = Filament.EntityManager.get().create();
|
|
395
|
+
Filament.LightManager.Builder(Filament.LightManager$Type.DIRECTIONAL)
|
|
396
|
+
.color([0.5, 0.6, 0.9])
|
|
397
|
+
.intensity(50000)
|
|
398
|
+
.direction([0, 0.3, 1.0])
|
|
399
|
+
.build(engine, back);
|
|
400
|
+
scene.addEntity(back);
|
|
401
|
+
|
|
402
|
+
// --- IBL: load real KTX if available, fallback to synthetic SH ---
|
|
403
|
+
var iblUrl = options.iblUrl || 'environments/neutral_ibl.ktx';
|
|
404
|
+
fetch(iblUrl)
|
|
405
|
+
.then(function(r) {
|
|
406
|
+
if (!r.ok) throw new Error('HTTP ' + r.status);
|
|
407
|
+
return r.arrayBuffer().then(function(ab) { return new Uint8Array(ab); });
|
|
408
|
+
})
|
|
409
|
+
.then(function(buffer) {
|
|
410
|
+
try {
|
|
411
|
+
var ibl = engine.createIblFromKtx1(buffer);
|
|
412
|
+
ibl.setIntensity(options.iblIntensity || 40000);
|
|
413
|
+
scene.setIndirectLight(ibl);
|
|
414
|
+
// Create skybox from IBL reflection cubemap if skybox enabled
|
|
415
|
+
if (options.skybox !== false) {
|
|
416
|
+
try {
|
|
417
|
+
var reflections = ibl.getReflectionsTexture();
|
|
418
|
+
if (reflections) {
|
|
419
|
+
var skybox = Filament.Skybox.Builder()
|
|
420
|
+
.environment(reflections)
|
|
421
|
+
.build(engine);
|
|
422
|
+
scene.setSkybox(skybox);
|
|
423
|
+
console.log('SceneView: Skybox created from IBL cubemap');
|
|
424
|
+
}
|
|
425
|
+
} catch (skyErr) {
|
|
426
|
+
// Skybox not supported in this build — that's OK
|
|
427
|
+
console.log('SceneView: Skybox not available (IBL-only mode)');
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
console.log('SceneView: KTX IBL loaded (' + Math.round(buffer.length / 1024) + 'KB)');
|
|
431
|
+
} catch (e) {
|
|
432
|
+
console.warn('SceneView: createIblFromKtx1 failed, using SH fallback', e);
|
|
433
|
+
_applySyntheticIBL(engine, scene);
|
|
434
|
+
}
|
|
435
|
+
})
|
|
436
|
+
.catch(function() {
|
|
437
|
+
_applySyntheticIBL(engine, scene);
|
|
438
|
+
});
|
|
312
439
|
|
|
440
|
+
var loader = engine.createAssetLoader();
|
|
313
441
|
var instance = new SceneViewInstance(canvas, engine, scene, renderer, view, swapChain, camera, cameraEntity, loader);
|
|
442
|
+
instance._fov = fov;
|
|
314
443
|
|
|
315
|
-
if (options.autoRotate === false)
|
|
316
|
-
instance.setAutoRotate(false);
|
|
317
|
-
}
|
|
444
|
+
if (options.autoRotate === false) instance.setAutoRotate(false);
|
|
318
445
|
|
|
319
446
|
return instance;
|
|
320
447
|
}
|
|
321
448
|
|
|
322
|
-
/**
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
449
|
+
/** Fallback IBL from spherical harmonics when KTX not available */
|
|
450
|
+
function _applySyntheticIBL(engine, scene) {
|
|
451
|
+
try {
|
|
452
|
+
var ibl = Filament.IndirectLight.Builder()
|
|
453
|
+
.irradiance(3, [
|
|
454
|
+
0.65, 0.65, 0.70,
|
|
455
|
+
0.10, 0.10, 0.12,
|
|
456
|
+
0.15, 0.15, 0.18,
|
|
457
|
+
-0.02, -0.02, -0.01,
|
|
458
|
+
0.04, 0.04, 0.05,
|
|
459
|
+
0.08, 0.08, 0.10,
|
|
460
|
+
0.01, 0.01, 0.01,
|
|
461
|
+
-0.02, -0.02, -0.02,
|
|
462
|
+
0.03, 0.03, 0.03
|
|
463
|
+
])
|
|
464
|
+
.intensity(35000)
|
|
465
|
+
.build(engine);
|
|
466
|
+
scene.setIndirectLight(ibl);
|
|
467
|
+
console.log('SceneView: Using synthetic SH IBL');
|
|
468
|
+
} catch (e) { /* skip */ }
|
|
469
|
+
}
|
|
470
|
+
|
|
330
471
|
function create(canvasOrId, options) {
|
|
331
472
|
return _ensureFilament().then(function() {
|
|
332
473
|
return new Promise(function(resolve, reject) {
|
|
333
|
-
// If WASM is already initialized (Engine exists), skip Filament.init
|
|
334
474
|
if (typeof Filament.Engine !== 'undefined') {
|
|
335
475
|
try {
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
reject(
|
|
339
|
-
}
|
|
476
|
+
var instance = _createEngine(canvasOrId, options);
|
|
477
|
+
if (instance) resolve(instance);
|
|
478
|
+
else reject(new Error('SceneView: Canvas already initialized'));
|
|
479
|
+
} catch (e) { reject(e); }
|
|
340
480
|
return;
|
|
341
481
|
}
|
|
342
|
-
// First time: initialize WASM
|
|
343
482
|
Filament.init([], function() {
|
|
344
483
|
try {
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
reject(
|
|
348
|
-
}
|
|
484
|
+
var instance = _createEngine(canvasOrId, options);
|
|
485
|
+
if (instance) resolve(instance);
|
|
486
|
+
else reject(new Error('SceneView: Canvas already initialized'));
|
|
487
|
+
} catch (e) { reject(e); }
|
|
349
488
|
});
|
|
350
489
|
});
|
|
351
490
|
});
|
|
352
491
|
}
|
|
353
492
|
|
|
354
|
-
/**
|
|
355
|
-
* One-liner: create viewer and load a model.
|
|
356
|
-
* Filament.js is loaded automatically from CDN if not already present.
|
|
357
|
-
*
|
|
358
|
-
* @param {string|HTMLCanvasElement} canvasOrId
|
|
359
|
-
* @param {string} modelUrl - URL to .glb/.gltf model
|
|
360
|
-
* @param {Object} [options]
|
|
361
|
-
* @returns {Promise<SceneViewInstance>}
|
|
362
|
-
*/
|
|
363
493
|
function modelViewer(canvasOrId, modelUrl, options) {
|
|
364
|
-
return
|
|
365
|
-
return
|
|
366
|
-
// Always use Filament.init with the model URL in the assets array.
|
|
367
|
-
// This works whether WASM is already loaded or not, because Filament
|
|
368
|
-
// needs to fetch the model asset and will call back when done.
|
|
369
|
-
Filament.init([modelUrl], function() {
|
|
370
|
-
try {
|
|
371
|
-
var instance = _createEngine(canvasOrId, options);
|
|
372
|
-
instance._showModel(modelUrl);
|
|
373
|
-
resolve(instance);
|
|
374
|
-
} catch (e) {
|
|
375
|
-
reject(e);
|
|
376
|
-
}
|
|
377
|
-
});
|
|
378
|
-
});
|
|
494
|
+
return create(canvasOrId, options).then(function(instance) {
|
|
495
|
+
return instance.loadModel(modelUrl);
|
|
379
496
|
});
|
|
380
497
|
}
|
|
381
498
|
|
|
382
|
-
// Public API
|
|
383
499
|
global.SceneView = {
|
|
384
|
-
version: '1.
|
|
500
|
+
version: '1.4.0',
|
|
385
501
|
create: create,
|
|
386
502
|
modelViewer: modelViewer
|
|
387
503
|
};
|