sceneview-web 1.4.0 → 1.5.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/package.json +1 -1
- package/sceneview.js +218 -132
- package/model-viewer.min.js +0 -1083
- package/three/OrbitControls.js +0 -1417
- package/three/RGBELoader.js +0 -450
- package/three/RoomEnvironment.js +0 -148
- package/three/three.module.js +0 -53044
package/package.json
CHANGED
package/sceneview.js
CHANGED
|
@@ -7,22 +7,15 @@
|
|
|
7
7
|
* Powered by Filament.js v1.70.1 (Google's PBR renderer, WASM).
|
|
8
8
|
* https://sceneview.github.io
|
|
9
9
|
*
|
|
10
|
-
* @version 1.
|
|
10
|
+
* @version 1.5.0
|
|
11
11
|
* @license MIT
|
|
12
12
|
*/
|
|
13
13
|
(function(global) {
|
|
14
14
|
'use strict';
|
|
15
15
|
|
|
16
|
-
// Filament.js is loaded via <script> tag in HTML (js/filament/filament.js)
|
|
17
|
-
// This avoids dynamic script injection issues with WASM resolution.
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Wait for Filament to be available (loaded by the script tag).
|
|
21
|
-
*/
|
|
22
16
|
function _ensureFilament() {
|
|
23
17
|
return new Promise(function(resolve, reject) {
|
|
24
18
|
if (typeof Filament !== 'undefined') { resolve(); return; }
|
|
25
|
-
// Poll briefly in case the script tag hasn't finished loading
|
|
26
19
|
var attempts = 0;
|
|
27
20
|
var check = setInterval(function() {
|
|
28
21
|
if (typeof Filament !== 'undefined') { clearInterval(check); resolve(); }
|
|
@@ -31,9 +24,6 @@
|
|
|
31
24
|
});
|
|
32
25
|
}
|
|
33
26
|
|
|
34
|
-
/**
|
|
35
|
-
* SceneView instance — wraps Filament engine, scene, camera, renderer.
|
|
36
|
-
*/
|
|
37
27
|
class SceneViewInstance {
|
|
38
28
|
constructor(canvas, engine, scene, renderer, view, swapChain, camera, cameraEntity, loader) {
|
|
39
29
|
this._canvas = canvas;
|
|
@@ -46,7 +36,7 @@
|
|
|
46
36
|
this._cameraEntity = cameraEntity;
|
|
47
37
|
this._loader = loader;
|
|
48
38
|
this._asset = null;
|
|
49
|
-
this._angle = 0.785;
|
|
39
|
+
this._angle = 0.785;
|
|
50
40
|
this._autoRotate = true;
|
|
51
41
|
this._orbitRadius = 3.5;
|
|
52
42
|
this._orbitHeight = 0.8;
|
|
@@ -54,18 +44,19 @@
|
|
|
54
44
|
this._running = true;
|
|
55
45
|
this._isDragging = false;
|
|
56
46
|
this._lastMouse = { x: 0, y: 0 };
|
|
57
|
-
// Inertia for smooth orbit deceleration
|
|
58
47
|
this._velocityAngle = 0;
|
|
59
48
|
this._velocityHeight = 0;
|
|
60
49
|
this._dampingFactor = 0.95;
|
|
61
|
-
this._wantsAutoRotate = true;
|
|
50
|
+
this._wantsAutoRotate = true;
|
|
62
51
|
this._autoRotateTimer = null;
|
|
52
|
+
this._userLights = [];
|
|
63
53
|
this._setupControls();
|
|
64
54
|
this._setupResizeObserver();
|
|
65
55
|
this._startRenderLoop();
|
|
66
56
|
}
|
|
67
57
|
|
|
68
|
-
|
|
58
|
+
// ── Model loading ──
|
|
59
|
+
|
|
69
60
|
loadModel(url) {
|
|
70
61
|
var self = this;
|
|
71
62
|
return new Promise(function(resolve, reject) {
|
|
@@ -74,75 +65,54 @@
|
|
|
74
65
|
.then(function(buffer) {
|
|
75
66
|
Filament.assets = Filament.assets || {};
|
|
76
67
|
Filament.assets[url] = new Uint8Array(buffer);
|
|
77
|
-
try {
|
|
78
|
-
self._showModel(url);
|
|
79
|
-
resolve(self);
|
|
80
|
-
} catch (e) {
|
|
81
|
-
reject(e);
|
|
82
|
-
}
|
|
68
|
+
try { self._showModel(url); resolve(self); } catch (e) { reject(e); }
|
|
83
69
|
})
|
|
84
70
|
.catch(reject);
|
|
85
71
|
});
|
|
86
72
|
}
|
|
87
73
|
|
|
88
74
|
_showModel(url) {
|
|
89
|
-
// Remove previous model
|
|
90
75
|
if (this._asset) {
|
|
91
76
|
try {
|
|
92
77
|
this._asset.getRenderableEntities().forEach(function(e) { this._scene.remove(e); }.bind(this));
|
|
93
78
|
this._scene.remove(this._asset.getRoot());
|
|
94
|
-
} catch (e) {
|
|
79
|
+
} catch (e) {}
|
|
95
80
|
this._asset = null;
|
|
96
81
|
}
|
|
97
|
-
|
|
98
82
|
var data = Filament.assets[url];
|
|
99
83
|
if (!data) throw new Error('Failed to fetch model: ' + url);
|
|
100
|
-
|
|
101
84
|
var asset = this._loader.createAsset(data);
|
|
102
85
|
if (!asset) throw new Error('Failed to parse model: ' + url);
|
|
103
|
-
|
|
104
86
|
asset.loadResources();
|
|
105
87
|
this._scene.addEntity(asset.getRoot());
|
|
106
88
|
this._scene.addEntities(asset.getRenderableEntities());
|
|
107
89
|
this._asset = asset;
|
|
90
|
+
this._autoFrame(asset);
|
|
91
|
+
}
|
|
108
92
|
|
|
109
|
-
|
|
93
|
+
_autoFrame(asset) {
|
|
110
94
|
try {
|
|
111
95
|
var bbox = asset.getBoundingBox();
|
|
112
96
|
var cx = (bbox.min[0] + bbox.max[0]) / 2;
|
|
113
97
|
var cy = (bbox.min[1] + bbox.max[1]) / 2;
|
|
114
98
|
var cz = (bbox.min[2] + bbox.max[2]) / 2;
|
|
115
|
-
var
|
|
116
|
-
var sy = bbox.max[1] - bbox.min[1];
|
|
117
|
-
var sz = bbox.max[2] - bbox.min[2];
|
|
118
|
-
var maxDim = Math.max(sx, sy, sz);
|
|
99
|
+
var maxDim = Math.max(bbox.max[0] - bbox.min[0], bbox.max[1] - bbox.min[1], bbox.max[2] - bbox.min[2]);
|
|
119
100
|
if (maxDim > 0) {
|
|
120
101
|
this._orbitTarget = [cx, cy, cz];
|
|
121
|
-
// Tighter framing than before (1.8x instead of 2.5x)
|
|
122
102
|
this._orbitRadius = maxDim * 1.8;
|
|
123
103
|
this._orbitHeight = cy;
|
|
124
104
|
}
|
|
125
|
-
} catch (e) {
|
|
126
|
-
}
|
|
127
|
-
|
|
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 });
|
|
133
|
-
return this;
|
|
105
|
+
} catch (e) {}
|
|
134
106
|
}
|
|
135
107
|
|
|
136
|
-
/** Add a model to the scene (without removing existing ones) */
|
|
137
108
|
addModel(url) {
|
|
138
109
|
var self = this;
|
|
139
110
|
return new Promise(function(resolve, reject) {
|
|
140
111
|
fetch(url)
|
|
141
112
|
.then(function(resp) { return resp.arrayBuffer(); })
|
|
142
113
|
.then(function(buffer) {
|
|
143
|
-
var data = new Uint8Array(buffer);
|
|
144
114
|
try {
|
|
145
|
-
var asset = self._loader.createAsset(
|
|
115
|
+
var asset = self._loader.createAsset(new Uint8Array(buffer));
|
|
146
116
|
if (!asset) { reject(new Error('Failed to parse: ' + url)); return; }
|
|
147
117
|
asset.loadResources();
|
|
148
118
|
self._scene.addEntity(asset.getRoot());
|
|
@@ -154,8 +124,7 @@
|
|
|
154
124
|
});
|
|
155
125
|
}
|
|
156
126
|
|
|
157
|
-
|
|
158
|
-
loadGLBBuffer(buffer, key) {
|
|
127
|
+
loadGLBBuffer(buffer) {
|
|
159
128
|
var asset = this._loader.createAsset(buffer);
|
|
160
129
|
if (!asset) return null;
|
|
161
130
|
asset.loadResources();
|
|
@@ -164,23 +133,191 @@
|
|
|
164
133
|
return asset;
|
|
165
134
|
}
|
|
166
135
|
|
|
167
|
-
/** Remove an asset from the scene */
|
|
168
136
|
removeAsset(asset) {
|
|
169
137
|
if (!asset) return;
|
|
170
138
|
try {
|
|
171
139
|
asset.getRenderableEntities().forEach(function(e) { this._scene.remove(e); }.bind(this));
|
|
172
140
|
this._scene.remove(asset.getRoot());
|
|
173
|
-
} catch (e) {
|
|
141
|
+
} catch (e) {}
|
|
174
142
|
}
|
|
175
143
|
|
|
176
|
-
/** Access engine for advanced Filament operations */
|
|
177
144
|
get engine() { return this._engine; }
|
|
178
145
|
get scene() { return this._scene; }
|
|
146
|
+
get view() { return this._view; }
|
|
147
|
+
get camera() { return this._camera; }
|
|
148
|
+
|
|
149
|
+
// ── Basic setters ──
|
|
150
|
+
|
|
151
|
+
setAutoRotate(enabled) { this._autoRotate = enabled; this._wantsAutoRotate = enabled; return this; }
|
|
152
|
+
setCameraDistance(d) { this._orbitRadius = d; return this; }
|
|
153
|
+
|
|
154
|
+
setBackgroundColor(r, g, b, a) {
|
|
155
|
+
this._renderer.setClearOptions({ clearColor: [r, g, b, a !== undefined ? a : 1], clear: true });
|
|
156
|
+
return this;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// ── Post-processing ──
|
|
160
|
+
|
|
161
|
+
setBloom(opts) {
|
|
162
|
+
try { this._view.setBloomOptions(Object.assign({ enabled: true, strength: 0.1, resolution: 360, levels: 6, blendMode: 0 }, opts)); } catch (e) {}
|
|
163
|
+
return this;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
setFog(opts) {
|
|
167
|
+
try { this._view.setFogOptions(Object.assign({ enabled: true, distance: 10, maximumOpacity: 0.8, color: [0.5, 0.55, 0.65] }, opts)); } catch (e) {}
|
|
168
|
+
return this;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
setVignette(opts) {
|
|
172
|
+
try { this._view.setVignetteOptions(Object.assign({ enabled: true, midPoint: 0.5, roundness: 0.5, feather: 0.5 }, opts)); } catch (e) {}
|
|
173
|
+
return this;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
setDOF(opts) {
|
|
177
|
+
try { this._view.setDepthOfFieldOptions(Object.assign({ enabled: true, cocScale: 1.0 }, opts)); } catch (e) {}
|
|
178
|
+
return this;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
setMSAA(opts) {
|
|
182
|
+
try { this._view.setMultiSampleAntiAliasingOptions(Object.assign({ enabled: true, sampleCount: 4 }, opts)); } catch (e) {}
|
|
183
|
+
return this;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
setTAA(opts) {
|
|
187
|
+
try { this._view.setTemporalAntiAliasingOptions(Object.assign({ enabled: true }, opts)); } catch (e) {}
|
|
188
|
+
return this;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
setSSR(opts) {
|
|
192
|
+
try { this._view.setScreenSpaceReflectionsOptions(Object.assign({ enabled: true }, opts)); } catch (e) {}
|
|
193
|
+
return this;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
setQuality(level) {
|
|
197
|
+
if (level === 'low') {
|
|
198
|
+
this.setBloom({ enabled: false }); this.setMSAA({ enabled: false });
|
|
199
|
+
} else if (level === 'medium') {
|
|
200
|
+
this.setBloom({ enabled: true, strength: 0.05 }); this.setMSAA({ enabled: true, sampleCount: 2 });
|
|
201
|
+
} else if (level === 'high') {
|
|
202
|
+
this.setBloom({ enabled: true, strength: 0.1 }); this.setMSAA({ enabled: true, sampleCount: 4 }); this.setVignette({ enabled: true });
|
|
203
|
+
} else if (level === 'ultra') {
|
|
204
|
+
this.setBloom({ enabled: true, strength: 0.15 }); this.setMSAA({ enabled: true, sampleCount: 4 });
|
|
205
|
+
this.setTAA({ enabled: true }); this.setSSR({ enabled: true }); this.setVignette({ enabled: true });
|
|
206
|
+
}
|
|
207
|
+
return this;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// ── Camera helpers ──
|
|
211
|
+
|
|
212
|
+
setCameraPosition(x, y, z) {
|
|
213
|
+
this._orbitRadius = Math.sqrt(x * x + z * z);
|
|
214
|
+
this._orbitHeight = y;
|
|
215
|
+
this._angle = Math.atan2(x, z);
|
|
216
|
+
return this;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
setCameraTarget(x, y, z) { this._orbitTarget = [x, y, z]; return this; }
|
|
220
|
+
|
|
221
|
+
setCameraFOV(degrees) {
|
|
222
|
+
this._fov = degrees;
|
|
223
|
+
var c = this._canvas;
|
|
224
|
+
this._camera.setProjectionFov(degrees, c.width / c.height, 0.1, 1000, Filament.Camera$Fov.VERTICAL);
|
|
225
|
+
return this;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
setOrbitSpeed(speed) { this._orbitSpeed = speed; return this; }
|
|
229
|
+
|
|
230
|
+
setOrbitLimits(opts) {
|
|
231
|
+
if (opts.minDistance !== undefined) this._minRadius = opts.minDistance;
|
|
232
|
+
if (opts.maxDistance !== undefined) this._maxRadius = opts.maxDistance;
|
|
233
|
+
return this;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
animateCamera(opts) {
|
|
237
|
+
var self = this;
|
|
238
|
+
var duration = opts.duration || 1000;
|
|
239
|
+
var startTime = performance.now();
|
|
240
|
+
var sA = this._angle, sR = this._orbitRadius, sH = this._orbitHeight;
|
|
241
|
+
var sT = this._orbitTarget.slice();
|
|
242
|
+
var eT = opts.target || sT;
|
|
243
|
+
var eR = opts.distance !== undefined ? opts.distance : sR;
|
|
244
|
+
var eH = opts.height !== undefined ? opts.height : sH;
|
|
245
|
+
var eA = opts.angle !== undefined ? opts.angle : sA;
|
|
246
|
+
var wasAuto = this._autoRotate;
|
|
247
|
+
this._autoRotate = false;
|
|
248
|
+
this._cameraAnimating = true;
|
|
249
|
+
|
|
250
|
+
function ease(t) { return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2; }
|
|
251
|
+
function lerp(a, b, t) { return a + (b - a) * t; }
|
|
252
|
+
|
|
253
|
+
function step(now) {
|
|
254
|
+
if (!self._running || !self._cameraAnimating) return;
|
|
255
|
+
var t = ease(Math.min((now - startTime) / duration, 1));
|
|
256
|
+
self._angle = lerp(sA, eA, t);
|
|
257
|
+
self._orbitRadius = lerp(sR, eR, t);
|
|
258
|
+
self._orbitHeight = lerp(sH, eH, t);
|
|
259
|
+
self._orbitTarget = [lerp(sT[0], eT[0], t), lerp(sT[1], eT[1], t), lerp(sT[2], eT[2], t)];
|
|
260
|
+
if (t < 1) { requestAnimationFrame(step); }
|
|
261
|
+
else { self._cameraAnimating = false; if (wasAuto) self._autoRotate = true; if (opts.onComplete) opts.onComplete(); }
|
|
262
|
+
}
|
|
263
|
+
requestAnimationFrame(step);
|
|
264
|
+
return this;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// ── Dynamic lights ──
|
|
268
|
+
|
|
269
|
+
addLight(type, opts) {
|
|
270
|
+
opts = opts || {};
|
|
271
|
+
var entity = Filament.EntityManager.get().create();
|
|
272
|
+
var typeMap = { sun: Filament.LightManager$Type.SUN, directional: Filament.LightManager$Type.DIRECTIONAL, point: Filament.LightManager$Type.POINT, spot: Filament.LightManager$Type.SPOT };
|
|
273
|
+
var builder = Filament.LightManager.Builder(typeMap[type] || Filament.LightManager$Type.POINT)
|
|
274
|
+
.color(opts.color || [1, 1, 1]).intensity(opts.intensity || 100000);
|
|
275
|
+
if (opts.direction) builder.direction(opts.direction);
|
|
276
|
+
if (opts.position) builder.position(opts.position);
|
|
277
|
+
if (opts.falloff) builder.falloff(opts.falloff);
|
|
278
|
+
if (opts.castShadows) builder.castShadows(true);
|
|
279
|
+
if (type === 'spot') builder.spotLightCone(opts.innerCone || 0.5, opts.outerCone || 0.7);
|
|
280
|
+
if (type === 'sun') { builder.sunAngularRadius(opts.angularRadius || 1.9); builder.sunHaloSize(opts.haloSize || 10.0); builder.sunHaloFalloff(opts.haloFalloff || 80.0); }
|
|
281
|
+
builder.build(this._engine, entity);
|
|
282
|
+
this._scene.addEntity(entity);
|
|
283
|
+
this._userLights.push(entity);
|
|
284
|
+
return entity;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
removeLight(entity) {
|
|
288
|
+
this._scene.remove(entity);
|
|
289
|
+
var i = this._userLights.indexOf(entity);
|
|
290
|
+
if (i >= 0) this._userLights.splice(i, 1);
|
|
291
|
+
return this;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
clearLights() {
|
|
295
|
+
this._userLights.forEach(function(e) { this._scene.remove(e); }.bind(this));
|
|
296
|
+
this._userLights = [];
|
|
297
|
+
return this;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// ── Skybox control ──
|
|
301
|
+
|
|
302
|
+
setSkyboxColor(r, g, b) {
|
|
303
|
+
try { this._scene.setSkybox(Filament.Skybox.Builder().color([r, g, b, 1]).build(this._engine)); } catch (e) {}
|
|
304
|
+
return this;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
removeSkybox() { try { this._scene.setSkybox(null); } catch (e) {} return this; }
|
|
308
|
+
|
|
309
|
+
// ── Asset querying ──
|
|
310
|
+
|
|
311
|
+
getEntitiesByName(name) { if (!this._asset) return []; try { return this._asset.getEntitiesByName(name); } catch (e) { return []; } }
|
|
312
|
+
getEntitiesByPrefix(prefix) { if (!this._asset) return []; try { return this._asset.getEntitiesByPrefix(prefix); } catch (e) { return []; } }
|
|
313
|
+
|
|
314
|
+
// ── Lifecycle ──
|
|
179
315
|
|
|
180
316
|
dispose() {
|
|
181
317
|
this._running = false;
|
|
182
318
|
if (this._resizeObserver) this._resizeObserver.disconnect();
|
|
183
|
-
|
|
319
|
+
_activeCanvases.delete(this._canvas);
|
|
320
|
+
try { Filament.Engine.destroy(this._engine); } catch (e) {}
|
|
184
321
|
}
|
|
185
322
|
|
|
186
323
|
_setupControls() {
|
|
@@ -207,7 +344,6 @@
|
|
|
207
344
|
});
|
|
208
345
|
canvas.addEventListener('mouseup', function() {
|
|
209
346
|
self._isDragging = false;
|
|
210
|
-
// Resume auto-rotate after 3s idle (like model-viewer)
|
|
211
347
|
if (self._wantsAutoRotate) {
|
|
212
348
|
self._autoRotateTimer = setTimeout(function() { self._autoRotate = true; }, 3000);
|
|
213
349
|
}
|
|
@@ -222,7 +358,9 @@
|
|
|
222
358
|
canvas.addEventListener('wheel', function(e) {
|
|
223
359
|
e.preventDefault();
|
|
224
360
|
self._orbitRadius *= (1 + e.deltaY * 0.001);
|
|
225
|
-
self.
|
|
361
|
+
var minR = self._minRadius || 0.5;
|
|
362
|
+
var maxR = self._maxRadius || 50;
|
|
363
|
+
self._orbitRadius = Math.max(minR, Math.min(maxR, self._orbitRadius));
|
|
226
364
|
}, { passive: false });
|
|
227
365
|
|
|
228
366
|
canvas.addEventListener('touchstart', function(e) {
|
|
@@ -258,7 +396,7 @@
|
|
|
258
396
|
var self = this;
|
|
259
397
|
this._resizeObserver = new ResizeObserver(function() {
|
|
260
398
|
var canvas = self._canvas;
|
|
261
|
-
var dpr = Math.min(devicePixelRatio, 2);
|
|
399
|
+
var dpr = Math.min(devicePixelRatio, 2);
|
|
262
400
|
canvas.width = canvas.clientWidth * dpr;
|
|
263
401
|
canvas.height = canvas.clientHeight * dpr;
|
|
264
402
|
self._view.setViewport([0, 0, canvas.width, canvas.height]);
|
|
@@ -274,12 +412,8 @@
|
|
|
274
412
|
var self = this;
|
|
275
413
|
function render() {
|
|
276
414
|
if (!self._running) return;
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
if (self._autoRotate) self._angle += 0.00873;
|
|
280
|
-
|
|
281
|
-
// Inertia damping after drag release
|
|
282
|
-
if (!self._isDragging) {
|
|
415
|
+
if (self._autoRotate) self._angle += (self._orbitSpeed || 0.00873);
|
|
416
|
+
if (!self._isDragging && !self._cameraAnimating) {
|
|
283
417
|
self._angle += self._velocityAngle;
|
|
284
418
|
self._orbitHeight += self._velocityHeight;
|
|
285
419
|
self._velocityAngle *= self._dampingFactor;
|
|
@@ -287,16 +421,13 @@
|
|
|
287
421
|
if (Math.abs(self._velocityAngle) < 0.00005) self._velocityAngle = 0;
|
|
288
422
|
if (Math.abs(self._velocityHeight) < 0.00005) self._velocityHeight = 0;
|
|
289
423
|
}
|
|
290
|
-
|
|
291
424
|
var t = self._orbitTarget;
|
|
292
425
|
var r = self._orbitRadius;
|
|
293
426
|
var h = self._orbitHeight;
|
|
294
427
|
self._camera.lookAt(
|
|
295
428
|
[t[0] + Math.sin(self._angle) * r, h, t[2] + Math.cos(self._angle) * r],
|
|
296
|
-
t,
|
|
297
|
-
[0, 1, 0]
|
|
429
|
+
t, [0, 1, 0]
|
|
298
430
|
);
|
|
299
|
-
|
|
300
431
|
self._engine.execute();
|
|
301
432
|
try {
|
|
302
433
|
if (self._renderer.beginFrame(self._swapChain)) {
|
|
@@ -304,7 +435,6 @@
|
|
|
304
435
|
self._renderer.endFrame();
|
|
305
436
|
}
|
|
306
437
|
} catch (e) {
|
|
307
|
-
// Filament 1.70 may need different render call
|
|
308
438
|
console.error('SceneView render error:', e.message);
|
|
309
439
|
self._running = false;
|
|
310
440
|
}
|
|
@@ -314,29 +444,16 @@
|
|
|
314
444
|
}
|
|
315
445
|
}
|
|
316
446
|
|
|
317
|
-
// Singleton guard — prevent multiple engine creations on same canvas
|
|
318
447
|
var _activeCanvases = new Set();
|
|
319
448
|
|
|
320
|
-
/**
|
|
321
|
-
* Set up Filament engine, scene, lights on a canvas.
|
|
322
|
-
*/
|
|
323
449
|
function _createEngine(canvasOrId, options) {
|
|
324
450
|
options = options || {};
|
|
325
|
-
|
|
326
|
-
var canvas = typeof canvasOrId === 'string'
|
|
327
|
-
? document.getElementById(canvasOrId)
|
|
328
|
-
: canvasOrId;
|
|
451
|
+
var canvas = typeof canvasOrId === 'string' ? document.getElementById(canvasOrId) : canvasOrId;
|
|
329
452
|
if (!canvas) throw new Error('Canvas not found: ' + canvasOrId);
|
|
330
|
-
|
|
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
|
-
}
|
|
453
|
+
if (_activeCanvases.has(canvas)) { console.warn('SceneView: Canvas already initialized, skipping'); return null; }
|
|
336
454
|
_activeCanvases.add(canvas);
|
|
337
455
|
|
|
338
456
|
var dpr = Math.min(devicePixelRatio, 2);
|
|
339
|
-
// Ensure canvas has actual layout dimensions (not default 300x150)
|
|
340
457
|
var cssW = canvas.clientWidth || canvas.offsetWidth || 500;
|
|
341
458
|
var cssH = canvas.clientHeight || canvas.offsetHeight || 500;
|
|
342
459
|
canvas.width = cssW * dpr;
|
|
@@ -361,45 +478,27 @@
|
|
|
361
478
|
camera.setProjectionFov(fov, canvas.width / canvas.height, 0.1, 1000, Filament.Camera$Fov.VERTICAL);
|
|
362
479
|
camera.lookAt([0, 1, 5], [0, 0, 0], [0, 1, 0]);
|
|
363
480
|
|
|
364
|
-
|
|
365
|
-
try {
|
|
366
|
-
view.setAmbientOcclusionOptions({
|
|
367
|
-
enabled: true, radius: 0.3, bias: 0.0005, intensity: 1.0, quality: 1
|
|
368
|
-
});
|
|
369
|
-
} catch (e) { /* skip */ }
|
|
481
|
+
try { view.setAmbientOcclusionOptions({ enabled: true, radius: 0.3, bias: 0.0005, intensity: 1.0, quality: 1 }); } catch (e) {}
|
|
370
482
|
|
|
371
|
-
// --- 3-point studio lighting ---
|
|
372
|
-
// Sun/key light — warm, strong
|
|
373
483
|
var sun = Filament.EntityManager.get().create();
|
|
374
484
|
Filament.LightManager.Builder(Filament.LightManager$Type.SUN)
|
|
375
|
-
.color([0.98, 0.92, 0.89])
|
|
376
|
-
.
|
|
377
|
-
.direction([0.6, -1.0, -0.8])
|
|
378
|
-
.sunAngularRadius(1.9)
|
|
379
|
-
.sunHaloSize(10.0)
|
|
380
|
-
.sunHaloFalloff(80.0)
|
|
485
|
+
.color([0.98, 0.92, 0.89]).intensity(options.lightIntensity || 110000)
|
|
486
|
+
.direction([0.6, -1.0, -0.8]).sunAngularRadius(1.9).sunHaloSize(10.0).sunHaloFalloff(80.0)
|
|
381
487
|
.build(engine, sun);
|
|
382
488
|
scene.addEntity(sun);
|
|
383
489
|
|
|
384
|
-
// Fill light — cool, softer
|
|
385
490
|
var fill = Filament.EntityManager.get().create();
|
|
386
491
|
Filament.LightManager.Builder(Filament.LightManager$Type.DIRECTIONAL)
|
|
387
|
-
.color([0.7, 0.75, 0.9])
|
|
388
|
-
.intensity(60000)
|
|
389
|
-
.direction([-0.5, 0.5, 1.0])
|
|
492
|
+
.color([0.7, 0.75, 0.9]).intensity(60000).direction([-0.5, 0.5, 1.0])
|
|
390
493
|
.build(engine, fill);
|
|
391
494
|
scene.addEntity(fill);
|
|
392
495
|
|
|
393
|
-
// Back/rim light — edge highlight
|
|
394
496
|
var back = Filament.EntityManager.get().create();
|
|
395
497
|
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])
|
|
498
|
+
.color([0.5, 0.6, 0.9]).intensity(50000).direction([0, 0.3, 1.0])
|
|
399
499
|
.build(engine, back);
|
|
400
500
|
scene.addEntity(back);
|
|
401
501
|
|
|
402
|
-
// --- IBL: load real KTX if available, fallback to synthetic SH ---
|
|
403
502
|
var iblUrl = options.iblUrl || 'environments/neutral_ibl.ktx';
|
|
404
503
|
fetch(iblUrl)
|
|
405
504
|
.then(function(r) {
|
|
@@ -411,21 +510,14 @@
|
|
|
411
510
|
var ibl = engine.createIblFromKtx1(buffer);
|
|
412
511
|
ibl.setIntensity(options.iblIntensity || 40000);
|
|
413
512
|
scene.setIndirectLight(ibl);
|
|
414
|
-
// Create skybox from IBL reflection cubemap if skybox enabled
|
|
415
513
|
if (options.skybox !== false) {
|
|
416
514
|
try {
|
|
417
515
|
var reflections = ibl.getReflectionsTexture();
|
|
418
516
|
if (reflections) {
|
|
419
|
-
|
|
420
|
-
.environment(reflections)
|
|
421
|
-
.build(engine);
|
|
422
|
-
scene.setSkybox(skybox);
|
|
517
|
+
scene.setSkybox(Filament.Skybox.Builder().environment(reflections).build(engine));
|
|
423
518
|
console.log('SceneView: Skybox created from IBL cubemap');
|
|
424
519
|
}
|
|
425
|
-
} catch (skyErr) {
|
|
426
|
-
// Skybox not supported in this build — that's OK
|
|
427
|
-
console.log('SceneView: Skybox not available (IBL-only mode)');
|
|
428
|
-
}
|
|
520
|
+
} catch (skyErr) { console.log('SceneView: Skybox not available (IBL-only mode)'); }
|
|
429
521
|
}
|
|
430
522
|
console.log('SceneView: KTX IBL loaded (' + Math.round(buffer.length / 1024) + 'KB)');
|
|
431
523
|
} catch (e) {
|
|
@@ -433,39 +525,35 @@
|
|
|
433
525
|
_applySyntheticIBL(engine, scene);
|
|
434
526
|
}
|
|
435
527
|
})
|
|
436
|
-
.catch(function() {
|
|
437
|
-
_applySyntheticIBL(engine, scene);
|
|
438
|
-
});
|
|
528
|
+
.catch(function() { _applySyntheticIBL(engine, scene); });
|
|
439
529
|
|
|
440
530
|
var loader = engine.createAssetLoader();
|
|
441
531
|
var instance = new SceneViewInstance(canvas, engine, scene, renderer, view, swapChain, camera, cameraEntity, loader);
|
|
442
532
|
instance._fov = fov;
|
|
443
533
|
|
|
444
534
|
if (options.autoRotate === false) instance.setAutoRotate(false);
|
|
535
|
+
if (options.quality) instance.setQuality(options.quality);
|
|
536
|
+
if (options.bloom) instance.setBloom(typeof options.bloom === 'object' ? options.bloom : {});
|
|
537
|
+
if (options.fog) instance.setFog(typeof options.fog === 'object' ? options.fog : {});
|
|
538
|
+
if (options.vignette) instance.setVignette(typeof options.vignette === 'object' ? options.vignette : {});
|
|
539
|
+
if (options.dof) instance.setDOF(typeof options.dof === 'object' ? options.dof : {});
|
|
540
|
+
if (options.msaa) instance.setMSAA(typeof options.msaa === 'object' ? options.msaa : {});
|
|
445
541
|
|
|
446
542
|
return instance;
|
|
447
543
|
}
|
|
448
544
|
|
|
449
|
-
/** Fallback IBL from spherical harmonics when KTX not available */
|
|
450
545
|
function _applySyntheticIBL(engine, scene) {
|
|
451
546
|
try {
|
|
452
547
|
var ibl = Filament.IndirectLight.Builder()
|
|
453
548
|
.irradiance(3, [
|
|
454
|
-
0.65, 0.65, 0.70,
|
|
455
|
-
|
|
456
|
-
0.
|
|
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
|
|
549
|
+
0.65, 0.65, 0.70, 0.10, 0.10, 0.12, 0.15, 0.15, 0.18,
|
|
550
|
+
-0.02, -0.02, -0.01, 0.04, 0.04, 0.05, 0.08, 0.08, 0.10,
|
|
551
|
+
0.01, 0.01, 0.01, -0.02, -0.02, -0.02, 0.03, 0.03, 0.03
|
|
463
552
|
])
|
|
464
|
-
.intensity(35000)
|
|
465
|
-
.build(engine);
|
|
553
|
+
.intensity(35000).build(engine);
|
|
466
554
|
scene.setIndirectLight(ibl);
|
|
467
555
|
console.log('SceneView: Using synthetic SH IBL');
|
|
468
|
-
} catch (e) {
|
|
556
|
+
} catch (e) {}
|
|
469
557
|
}
|
|
470
558
|
|
|
471
559
|
function create(canvasOrId, options) {
|
|
@@ -474,16 +562,14 @@
|
|
|
474
562
|
if (typeof Filament.Engine !== 'undefined') {
|
|
475
563
|
try {
|
|
476
564
|
var instance = _createEngine(canvasOrId, options);
|
|
477
|
-
if (instance) resolve(instance);
|
|
478
|
-
else reject(new Error('SceneView: Canvas already initialized'));
|
|
565
|
+
if (instance) resolve(instance); else reject(new Error('SceneView: Canvas already initialized'));
|
|
479
566
|
} catch (e) { reject(e); }
|
|
480
567
|
return;
|
|
481
568
|
}
|
|
482
569
|
Filament.init([], function() {
|
|
483
570
|
try {
|
|
484
571
|
var instance = _createEngine(canvasOrId, options);
|
|
485
|
-
if (instance) resolve(instance);
|
|
486
|
-
else reject(new Error('SceneView: Canvas already initialized'));
|
|
572
|
+
if (instance) resolve(instance); else reject(new Error('SceneView: Canvas already initialized'));
|
|
487
573
|
} catch (e) { reject(e); }
|
|
488
574
|
});
|
|
489
575
|
});
|
|
@@ -497,7 +583,7 @@
|
|
|
497
583
|
}
|
|
498
584
|
|
|
499
585
|
global.SceneView = {
|
|
500
|
-
version: '1.
|
|
586
|
+
version: '1.5.0',
|
|
501
587
|
create: create,
|
|
502
588
|
modelViewer: modelViewer
|
|
503
589
|
};
|