@preference-sl/pref-viewer 2.13.0-beta.2 → 2.13.0-beta.21
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/Readme.md +1 -1
- package/package.json +5 -5
- package/src/babylonjs-animation-controller.js +187 -76
- package/src/babylonjs-animation-opening.js +58 -2
- package/src/babylonjs-controller.js +1008 -359
- package/src/file-storage.js +405 -24
- package/src/gltf-resolver.js +65 -9
- package/src/gltf-storage.js +47 -35
- package/src/localization/i18n.js +1 -1
- package/src/localization/translations.js +3 -3
- package/src/pref-viewer-3d-data.js +102 -52
- package/src/pref-viewer-3d.js +71 -15
- package/src/pref-viewer-menu-3d.js +44 -3
- package/src/pref-viewer.js +134 -17
- package/src/styles.js +21 -5
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ArcRotateCamera, AssetContainer, Camera, Color4, DefaultRenderingPipeline, DirectionalLight, Engine, FreeCamera, HDRCubeTexture, HemisphericLight, IblShadowsRenderPipeline, LoadAssetContainerAsync, MeshBuilder, PBRMaterial, PointerEventTypes, PointLight, RenderTargetTexture, Scene, ShadowGenerator, SpotLight, SSAORenderingPipeline, Tools, UniversalCamera, Vector3, WebXRDefaultExperience, WebXRFeatureName, WebXRSessionManager, WebXRState } from "@babylonjs/core";
|
|
1
|
+
import { ArcRotateCamera, AssetContainer, Camera, Color4, DefaultRenderingPipeline, DirectionalLight, Engine, FreeCamera, HDRCubeTexture, HemisphericLight, IblShadowsRenderPipeline, LoadAssetContainerAsync, Material, MeshBuilder, PBRMaterial, PointerEventTypes, PointLight, RenderTargetTexture, Scene, ShadowGenerator, SpotLight, SSAORenderingPipeline, Tools, UniversalCamera, Vector3, WebXRDefaultExperience, WebXRFeatureName, WebXRSessionManager, WebXRState, WhenTextureReadyAsync } from "@babylonjs/core";
|
|
2
2
|
import { DracoCompression } from "@babylonjs/core/Meshes/Compression/dracoCompression.js";
|
|
3
3
|
import "@babylonjs/loaders";
|
|
4
4
|
import "@babylonjs/loaders/glTF/2.0/Extensions/KHR_draco_mesh_compression.js";
|
|
@@ -8,59 +8,73 @@ import JSZip from "jszip";
|
|
|
8
8
|
import GLTFResolver from "./gltf-resolver.js";
|
|
9
9
|
import { MaterialData } from "./pref-viewer-3d-data.js";
|
|
10
10
|
import BabylonJSAnimationController from "./babylonjs-animation-controller.js";
|
|
11
|
+
import OpeningAnimation from "./babylonjs-animation-opening.js";
|
|
11
12
|
import { translate } from "./localization/i18n.js";
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
|
-
* BabylonJSController
|
|
15
|
-
* rebuilds
|
|
16
|
-
*
|
|
17
|
-
*
|
|
15
|
+
* BabylonJSController coordinates the PrefViewer 3D runtime: it bootstraps Babylon.js, manages asset containers,
|
|
16
|
+
* rebuilds camera-dependent pipelines once textures are ready, brokers XR/download interactions, and persists
|
|
17
|
+
* render toggles so the UI can stay declarative. PrefViewer hands over container + option state, while this class
|
|
18
|
+
* turns it into a deterministic scene lifecycle with exports.
|
|
18
19
|
*
|
|
19
20
|
* Overview
|
|
20
|
-
* -
|
|
21
|
-
*
|
|
22
|
-
* -
|
|
23
|
-
*
|
|
24
|
-
* -
|
|
21
|
+
* - Creates the Babylon.js engine/scene/camera stack, configures Draco decoders, wires render loops plus a throttled
|
|
22
|
+
* ResizeObserver, and exposes download/XR helpers.
|
|
23
|
+
* - Resolves GLTF/GLB sources via GLTFResolver, loads them into `AssetContainer`s, and toggles visibility by
|
|
24
|
+
* mutating container state plus `show-model/show-scene` attributes.
|
|
25
|
+
* - Applies/persists AA, SSAO, IBL, and shadow flags; when they change it stops the render loop, tears down pipelines,
|
|
26
|
+
* reloads containers, and reinstalls effects after environment textures finish loading.
|
|
27
|
+
* - Manages keyboard/pointer/wheel handlers, animation menus, and WebXR so PrefViewer menus and DOM attributes stay
|
|
28
|
+
* synchronized with Babylon state, using sampled pointer picking and last-picked tracking to reduce raycast/highlight
|
|
29
|
+
* cost on dense scenes.
|
|
25
30
|
* - Generates GLB, glTF+ZIP, or USDZ exports with timestamped names and localized dialog copy.
|
|
26
31
|
* - Translates metadata (inner floor offsets, cast/receive shadows, camera locks) into scene adjustments after reloads.
|
|
27
32
|
*
|
|
28
33
|
* Runtime Flow
|
|
29
34
|
* 1. Instantiate with `new BabylonJSController(canvas, containers, options)`.
|
|
30
|
-
* 2. Call `enable()` to configure Draco,
|
|
31
|
-
* 3.
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
35
|
+
* 2. Call `enable()` to configure Draco, spin up the engine/scene, attach interaction + XR hooks, and start the render loop.
|
|
36
|
+
* 3. When PrefViewer marks containers/options pending, invoke `load()` (or `reloadWithCurrentSettings()`) so the controller
|
|
37
|
+
* fetches sources, rebuilds containers, and reattaches pipelines.
|
|
38
|
+
* 4. Use `scheduleRenderSettingsReload()` to merge/persist toggles; when it reports `changed: true`, call
|
|
39
|
+
* `reloadWithCurrentSettings()` to apply the staged settings.
|
|
40
|
+
* 5. Use `setContainerVisibility`, `setMaterialOptions`, `setCameraOptions`, or `setIBLOptions` for targeted updates; these
|
|
41
|
+
* helpers stop/restart the render loop while they rebuild camera-dependent resources.
|
|
42
|
+
* 6. Invoke `disable()` when the element disconnects to tear down scenes, XR sessions, observers, and handlers.
|
|
43
|
+
* `disable()` is asynchronous; it waits for XR/session shutdown before engine disposal and also disposes the
|
|
44
|
+
* shared GLTF resolver, which closes its internal IndexedDB handle.
|
|
36
45
|
*
|
|
37
46
|
* Public API Highlights
|
|
38
47
|
* - constructor(canvas, containers, options)
|
|
39
48
|
* - enable() / disable()
|
|
40
|
-
* - load()
|
|
49
|
+
* - load() / reloadWithCurrentSettings()
|
|
41
50
|
* - downloadGLB(content) / downloadGLTF(content) / downloadUSDZ(content)
|
|
42
|
-
* - getRenderSettings() /
|
|
43
|
-
* -
|
|
51
|
+
* - getRenderSettings() / scheduleRenderSettingsReload(settings)
|
|
52
|
+
* - setContainerVisibility(name, show)
|
|
53
|
+
* - setMaterialOptions() / setCameraOptions() / setIBLOptions()
|
|
44
54
|
*
|
|
45
|
-
* Key
|
|
46
|
-
* -
|
|
47
|
-
*
|
|
48
|
-
* -
|
|
49
|
-
*
|
|
50
|
-
* -
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
* -
|
|
59
|
-
*
|
|
60
|
-
* -
|
|
55
|
+
* Key Invariants
|
|
56
|
+
* - Asset containers must expose `setPendingWithCurrentStorage`/`setPending` before calling load/reload; the controller
|
|
57
|
+
* reads those flags to resolve fresh sources and avoids touching the DOM until data is ready.
|
|
58
|
+
* - Camera-dependent pipelines (DefaultRenderingPipeline, SSAO, IBL shadows, directional shadow generators) are rebuilt
|
|
59
|
+
* only after the active camera and environment textures are ready; render-loop restarts gate those transitions.
|
|
60
|
+
* - `show-model`/`show-scene` DOM attributes reflect container visibility; there are no direct `showModel()/hideModel()` APIs.
|
|
61
|
+
* - IBL shadows require `iblEnabled` plus `options.ibl.shadows` and a loaded HDR texture; otherwise fallback directional
|
|
62
|
+
* lights and environment-contributed lights supply classic shadow generators.
|
|
63
|
+
* - IBL lifecycle: when `options.ibl.cachedUrl` is present, a new `HDRCubeTexture` is created and cloned into `#hdrTexture`;
|
|
64
|
+
* then `options.ibl.consumeCachedUrl(true)` clears/revokes the temporary URL. Subsequent reloads reuse `#hdrTexture.clone()`
|
|
65
|
+
* while `options.ibl.valid` remains true.
|
|
66
|
+
* - Browser-only features guard `window`, localStorage, and XR APIs before use so the controller is safe to construct
|
|
67
|
+
* in SSR/Node contexts (though functionality activates only in browsers).
|
|
68
|
+
* - Pointer-pick lifecycle: hover/highlight raycasts are sampled on POINTERMOVE (time + distance thresholds), wheel
|
|
69
|
+
* input avoids picks entirely, and right-click POINTERUP performs an on-demand pick for context-menu targeting.
|
|
70
|
+
* - Runtime bookkeeping is split into mutable `#state` and tuning constants under `#config` to keep behavior changes
|
|
71
|
+
* explicit and reduce cross-field drift.
|
|
72
|
+
* - Resize lifecycle: canvas resize notifications are throttled (with trailing execution) before calling `engine.resize()`
|
|
73
|
+
* and any queued resize callback is canceled during teardown.
|
|
74
|
+
* - Teardown lifecycle: concurrent `disable()` calls are coalesced into a single in-flight promise to avoid races
|
|
75
|
+
* during XR exit and engine disposal.
|
|
61
76
|
*/
|
|
62
77
|
export default class BabylonJSController {
|
|
63
|
-
|
|
64
78
|
#RENDER_SETTINGS_STORAGE_KEY = "pref-viewer/render-settings";
|
|
65
79
|
|
|
66
80
|
// Default render settings
|
|
@@ -72,7 +86,7 @@ export default class BabylonJSController {
|
|
|
72
86
|
iblEnabled: true,
|
|
73
87
|
shadowsEnabled: false,
|
|
74
88
|
};
|
|
75
|
-
|
|
89
|
+
|
|
76
90
|
// Canvas HTML element
|
|
77
91
|
#canvas = null;
|
|
78
92
|
|
|
@@ -91,9 +105,11 @@ export default class BabylonJSController {
|
|
|
91
105
|
#XRExperience = null;
|
|
92
106
|
#canvasResizeObserver = null;
|
|
93
107
|
|
|
108
|
+
#hdrTexture = null; // reusable in-memory HDR source cloned into scene.environmentTexture across reloads
|
|
109
|
+
|
|
94
110
|
#containers = {};
|
|
95
111
|
#options = {};
|
|
96
|
-
|
|
112
|
+
|
|
97
113
|
#gltfResolver = null; // GLTFResolver instance
|
|
98
114
|
#babylonJSAnimationController = null; // AnimationController instance
|
|
99
115
|
|
|
@@ -104,13 +120,63 @@ export default class BabylonJSController {
|
|
|
104
120
|
};
|
|
105
121
|
|
|
106
122
|
#handlers = {
|
|
123
|
+
onAnimationGroupChanged: null,
|
|
107
124
|
onKeyUp: null,
|
|
108
125
|
onPointerObservable: null,
|
|
126
|
+
onResize: null,
|
|
109
127
|
renderLoop: null,
|
|
110
128
|
};
|
|
111
|
-
|
|
129
|
+
|
|
112
130
|
#settings = { ...BabylonJSController.DEFAULT_RENDER_SETTINGS };
|
|
113
131
|
|
|
132
|
+
// Runtime mutable state (changes while the app is running).
|
|
133
|
+
#state = {
|
|
134
|
+
// Pointer-picking sampling state avoids expensive scene.pick on every move.
|
|
135
|
+
pointerPicking: {
|
|
136
|
+
lastMovePickAt: 0,
|
|
137
|
+
lastMovePickX: NaN,
|
|
138
|
+
lastMovePickY: NaN,
|
|
139
|
+
lastPickedMeshId: null,
|
|
140
|
+
},
|
|
141
|
+
// Render loop state balances performance with responsiveness.
|
|
142
|
+
render: {
|
|
143
|
+
isLoopRunning: false,
|
|
144
|
+
dirtyFrames: 0,
|
|
145
|
+
continuousUntil: 0,
|
|
146
|
+
lastRenderAt: 0,
|
|
147
|
+
},
|
|
148
|
+
// Resize state batches frequent ResizeObserver notifications.
|
|
149
|
+
resize: {
|
|
150
|
+
isScheduled: false,
|
|
151
|
+
timeoutId: null,
|
|
152
|
+
lastAppliedAt: 0,
|
|
153
|
+
},
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
// Runtime configuration constants (tuning knobs, not per-frame state).
|
|
157
|
+
#config = {
|
|
158
|
+
pointerPicking: {
|
|
159
|
+
movePickIntervalMs: 50, // cap expensive scene.pick calls to ~20 Hz while moving the pointer
|
|
160
|
+
movePickMinDistancePx: 2, // skip picks for sub-pixel jitter
|
|
161
|
+
},
|
|
162
|
+
render: {
|
|
163
|
+
burstFramesBase: 2,
|
|
164
|
+
burstFramesEnhanced: 32, // when AA/SSAO/IBL is enabled, more frames are needed to reach stable output
|
|
165
|
+
interactionMs: 250,
|
|
166
|
+
animationMs: 200,
|
|
167
|
+
idleThrottleMs: 1000 / 15,
|
|
168
|
+
},
|
|
169
|
+
resize: {
|
|
170
|
+
throttleMs: 50, // cap resize work to ~20 Hz while dragging/resizing containers
|
|
171
|
+
},
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
// Promises to track async disable() lifecycle when XR and general teardown may run concurrently; ensures idempotent disable calls are safe and callers can await full teardown completion.
|
|
175
|
+
#disablingPromises = {
|
|
176
|
+
xr: null,
|
|
177
|
+
general: null,
|
|
178
|
+
};
|
|
179
|
+
|
|
114
180
|
/**
|
|
115
181
|
* Constructs a new BabylonJSController instance.
|
|
116
182
|
* Initializes the canvas, asset containers, and options for the Babylon.js scene.
|
|
@@ -143,8 +209,10 @@ export default class BabylonJSController {
|
|
|
143
209
|
* @returns {void}
|
|
144
210
|
*/
|
|
145
211
|
#bindHandlers() {
|
|
212
|
+
this.#handlers.onAnimationGroupChanged = this.#onAnimationGroupChanged.bind(this);
|
|
146
213
|
this.#handlers.onKeyUp = this.#onKeyUp.bind(this);
|
|
147
214
|
this.#handlers.onPointerObservable = this.#onPointerObservable.bind(this);
|
|
215
|
+
this.#handlers.onResize = this.#onResize.bind(this);
|
|
148
216
|
this.#handlers.renderLoop = this.#renderLoop.bind(this);
|
|
149
217
|
}
|
|
150
218
|
|
|
@@ -173,7 +241,7 @@ export default class BabylonJSController {
|
|
|
173
241
|
* @param {object} [settings={}] - Partial map of render settings (AA, SSAO, IBL, shadows, etc.).
|
|
174
242
|
* @returns {boolean} True when any setting changed and was saved.
|
|
175
243
|
*/
|
|
176
|
-
#
|
|
244
|
+
#saveRenderSettings(settings = {}) {
|
|
177
245
|
if (!settings) {
|
|
178
246
|
return false;
|
|
179
247
|
}
|
|
@@ -186,12 +254,8 @@ export default class BabylonJSController {
|
|
|
186
254
|
}
|
|
187
255
|
});
|
|
188
256
|
|
|
189
|
-
if (changed && settings.iblEnabled === false && this.#scene) {
|
|
190
|
-
this.#scene.environmentTexture = null;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
257
|
if (changed) {
|
|
194
|
-
this.#
|
|
258
|
+
this.#storeRenderSettings();
|
|
195
259
|
}
|
|
196
260
|
|
|
197
261
|
return changed;
|
|
@@ -229,14 +293,14 @@ export default class BabylonJSController {
|
|
|
229
293
|
* @private
|
|
230
294
|
* @returns {void}
|
|
231
295
|
*/
|
|
232
|
-
#
|
|
296
|
+
#storeRenderSettings() {
|
|
233
297
|
if (typeof window === "undefined" || !window?.localStorage) {
|
|
234
298
|
return;
|
|
235
299
|
}
|
|
236
300
|
try {
|
|
237
301
|
window.localStorage.setItem(this.#RENDER_SETTINGS_STORAGE_KEY, JSON.stringify(this.#settings));
|
|
238
302
|
} catch (error) {
|
|
239
|
-
console.warn("PrefViewer: unable to
|
|
303
|
+
console.warn("PrefViewer: unable to store render settings", error);
|
|
240
304
|
}
|
|
241
305
|
}
|
|
242
306
|
|
|
@@ -269,21 +333,191 @@ export default class BabylonJSController {
|
|
|
269
333
|
if (this.#options?.materials) {
|
|
270
334
|
Object.values(this.#options.materials).forEach((material) => material?.setPendingWithCurrent?.());
|
|
271
335
|
}
|
|
272
|
-
|
|
273
|
-
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Starts Babylon's engine render loop if it is not already running.
|
|
340
|
+
* @private
|
|
341
|
+
* @returns {boolean} True when the loop was started, false when no engine is available or it was already running.
|
|
342
|
+
*/
|
|
343
|
+
#startEngineRenderLoop() {
|
|
344
|
+
if (!this.#engine || this.#state.render.isLoopRunning) {
|
|
345
|
+
return false;
|
|
346
|
+
}
|
|
347
|
+
this.#engine.runRenderLoop(this.#handlers.renderLoop);
|
|
348
|
+
this.#state.render.isLoopRunning = true;
|
|
349
|
+
return true;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Stops Babylon's engine render loop when it is currently active.
|
|
354
|
+
* @private
|
|
355
|
+
* @returns {boolean} True when the loop was stopped, false when no engine is available or it was already stopped.
|
|
356
|
+
*/
|
|
357
|
+
#stopEngineRenderLoop() {
|
|
358
|
+
if (!this.#engine || !this.#state.render.isLoopRunning) {
|
|
359
|
+
return false;
|
|
360
|
+
}
|
|
361
|
+
this.#engine.stopRenderLoop(this.#handlers.renderLoop);
|
|
362
|
+
this.#state.render.isLoopRunning = false;
|
|
363
|
+
return true;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Resets transient render-loop bookkeeping so the next render request starts from a clean baseline.
|
|
368
|
+
* Clears queued burst frames, ends any active continuous-render window, and drops the idle-throttle timestamp.
|
|
369
|
+
* @private
|
|
370
|
+
* @returns {void}
|
|
371
|
+
*/
|
|
372
|
+
#resetRenderState() {
|
|
373
|
+
this.#state.render.dirtyFrames = 0;
|
|
374
|
+
this.#state.render.continuousUntil = 0;
|
|
375
|
+
this.#state.render.lastRenderAt = 0;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* Marks the scene as dirty and optionally extends a short continuous-render window.
|
|
380
|
+
* Ensures the engine loop is running so the requested frames can be produced.
|
|
381
|
+
* @private
|
|
382
|
+
* @param {{frames?:number, continuousMs?:number}} [options={}] - Render request options.
|
|
383
|
+
* @param {number} [options.frames=1] - Minimum number of frames to render.
|
|
384
|
+
* @param {number} [options.continuousMs=0] - Milliseconds to keep continuous rendering active.
|
|
385
|
+
* @returns {boolean} True when the request was accepted, false when scene/engine are unavailable.
|
|
386
|
+
*/
|
|
387
|
+
#requestRender({ frames = 1, continuousMs = 0 } = {}) {
|
|
388
|
+
if (!this.#scene || !this.#engine) {
|
|
389
|
+
return false;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
const now = typeof performance !== "undefined" && performance.now ? performance.now() : Date.now();
|
|
393
|
+
this.#state.render.dirtyFrames = Math.max(this.#state.render.dirtyFrames, Math.max(1, frames));
|
|
394
|
+
if (continuousMs > 0) {
|
|
395
|
+
this.#state.render.continuousUntil = Math.max(this.#state.render.continuousUntil, now + continuousMs);
|
|
396
|
+
}
|
|
397
|
+
this.#startEngineRenderLoop();
|
|
398
|
+
return true;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* Checks whether an ArcRotateCamera still has non-zero inertial movement.
|
|
403
|
+
* @private
|
|
404
|
+
* @param {ArcRotateCamera} camera - Camera to evaluate.
|
|
405
|
+
* @returns {boolean} True when any inertial offset is still active.
|
|
406
|
+
*/
|
|
407
|
+
#isArcRotateCameraInMotion(camera) {
|
|
408
|
+
const EPSILON = 0.00001;
|
|
409
|
+
return Math.abs(camera?.inertialAlphaOffset || 0) > EPSILON || Math.abs(camera?.inertialBetaOffset || 0) > EPSILON || Math.abs(camera?.inertialRadiusOffset || 0) > EPSILON || Math.abs(camera?.inertialPanningX || 0) > EPSILON || Math.abs(camera?.inertialPanningY || 0) > EPSILON;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* Checks whether a FreeCamera/UniversalCamera is currently moving or rotating.
|
|
414
|
+
* @private
|
|
415
|
+
* @param {FreeCamera|UniversalCamera} camera - Camera to evaluate.
|
|
416
|
+
* @returns {boolean} True when translation or rotation deltas are active.
|
|
417
|
+
*/
|
|
418
|
+
#isUniversalOrFreeCameraInMotion(camera) {
|
|
419
|
+
const EPSILON = 0.00001;
|
|
420
|
+
const direction = camera?.cameraDirection;
|
|
421
|
+
const rotation = camera?.cameraRotation;
|
|
422
|
+
const directionMoving = !!direction && (Math.abs(direction.x) > EPSILON || Math.abs(direction.y) > EPSILON || Math.abs(direction.z) > EPSILON);
|
|
423
|
+
const rotationMoving = !!rotation && (Math.abs(rotation.x) > EPSILON || Math.abs(rotation.y) > EPSILON || Math.abs(rotation.z) > EPSILON);
|
|
424
|
+
return directionMoving || rotationMoving;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* Detects motion for the current active camera based on its concrete camera type.
|
|
429
|
+
* @private
|
|
430
|
+
* @returns {boolean} True when the active camera is moving, otherwise false.
|
|
431
|
+
*/
|
|
432
|
+
#isCameraInMotion() {
|
|
433
|
+
const camera = this.#scene?.activeCamera;
|
|
434
|
+
if (!camera) {
|
|
435
|
+
return false;
|
|
436
|
+
}
|
|
437
|
+
if (camera instanceof ArcRotateCamera) {
|
|
438
|
+
return this.#isArcRotateCameraInMotion(camera);
|
|
439
|
+
}
|
|
440
|
+
if (camera instanceof UniversalCamera || camera instanceof FreeCamera) {
|
|
441
|
+
return this.#isUniversalOrFreeCameraInMotion(camera);
|
|
442
|
+
}
|
|
443
|
+
return false;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* Determines whether scene animations are currently running.
|
|
448
|
+
* @private
|
|
449
|
+
* @returns {boolean} True when at least one animation group is playing.
|
|
450
|
+
*/
|
|
451
|
+
#isAnimationRunning() {
|
|
452
|
+
if (!this.#scene) {
|
|
453
|
+
return false;
|
|
454
|
+
}
|
|
455
|
+
const hasAnimatables = (this.#scene.animatables?.length || 0) > 0;
|
|
456
|
+
if (!hasAnimatables) {
|
|
457
|
+
return false;
|
|
458
|
+
}
|
|
459
|
+
return this.#scene.animationGroups?.some((group) => group?.isPlaying) || false;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* Evaluates whether the renderer should stay in continuous mode.
|
|
464
|
+
* XR always forces continuous rendering; animation/camera motion also extends the
|
|
465
|
+
* continuous deadline window to avoid abrupt stop-start behavior.
|
|
466
|
+
* @private
|
|
467
|
+
* @param {number} now - Current high-resolution timestamp.
|
|
468
|
+
* @returns {boolean} True when continuous rendering should remain active.
|
|
469
|
+
*/
|
|
470
|
+
#shouldRenderContinuously(now) {
|
|
471
|
+
const inXR = this.#XRExperience?.baseExperience?.state === WebXRState.IN_XR;
|
|
472
|
+
if (inXR) {
|
|
473
|
+
return true;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
const animationRunning = this.#isAnimationRunning();
|
|
477
|
+
const cameraInMotion = this.#isCameraInMotion();
|
|
478
|
+
|
|
479
|
+
if (animationRunning) {
|
|
480
|
+
this.#state.render.continuousUntil = Math.max(this.#state.render.continuousUntil, now + this.#config.render.animationMs);
|
|
274
481
|
}
|
|
482
|
+
if (cameraInMotion) {
|
|
483
|
+
this.#state.render.continuousUntil = Math.max(this.#state.render.continuousUntil, now + this.#config.render.interactionMs);
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
return animationRunning || cameraInMotion || this.#state.render.continuousUntil > now;
|
|
275
487
|
}
|
|
276
488
|
|
|
277
489
|
/**
|
|
278
490
|
* Render loop callback for Babylon.js.
|
|
491
|
+
* Runs only while scene state is dirty, interactive motion is active, animations are running, or XR is active.
|
|
492
|
+
* It self-stops when the scene becomes idle.
|
|
279
493
|
* @private
|
|
280
494
|
* @returns {void}
|
|
281
|
-
* @description
|
|
282
|
-
* Continuously renders the current scene if it exists.
|
|
283
|
-
* Used by the engine's runRenderLoop method to update the view.
|
|
284
495
|
*/
|
|
285
496
|
#renderLoop() {
|
|
286
|
-
|
|
497
|
+
if (!this.#scene) {
|
|
498
|
+
this.#stopEngineRenderLoop();
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
const now = typeof performance !== "undefined" && performance.now ? performance.now() : Date.now();
|
|
503
|
+
const continuous = this.#shouldRenderContinuously(now);
|
|
504
|
+
const needsRender = continuous || this.#state.render.dirtyFrames > 0;
|
|
505
|
+
|
|
506
|
+
if (!needsRender) {
|
|
507
|
+
this.#stopEngineRenderLoop();
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
if (!continuous && this.#state.render.lastRenderAt > 0 && now - this.#state.render.lastRenderAt < this.#config.render.idleThrottleMs) {
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
this.#scene.render();
|
|
516
|
+
this.#state.render.lastRenderAt = now;
|
|
517
|
+
|
|
518
|
+
if (this.#state.render.dirtyFrames > 0) {
|
|
519
|
+
this.#state.render.dirtyFrames -= 1;
|
|
520
|
+
}
|
|
287
521
|
}
|
|
288
522
|
|
|
289
523
|
/**
|
|
@@ -382,49 +616,75 @@ export default class BabylonJSController {
|
|
|
382
616
|
* Adds a hemispheric ambient light, a directional light for shadows, a shadow generator, and a camera-attached point light.
|
|
383
617
|
* Sets light intensities and shadow properties for realistic rendering.
|
|
384
618
|
* @private
|
|
385
|
-
* @returns {
|
|
619
|
+
* @returns {Promise<boolean>} Returns true if lights were changed, false otherwise.
|
|
620
|
+
* @description
|
|
621
|
+
* IBL path is considered available when either:
|
|
622
|
+
* - `options.ibl.cachedUrl` is present (new pending URL), or
|
|
623
|
+
* - `options.ibl.valid === true` (reusable in-memory `#hdrTexture` exists).
|
|
386
624
|
*/
|
|
387
|
-
#createLights() {
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
}
|
|
625
|
+
async #createLights() {
|
|
626
|
+
const hemiLightName = "PrefViewerHemiLight";
|
|
627
|
+
const cameraLightName = "PrefViewerCameraLight";
|
|
628
|
+
const dirLightName = "PrefViewerDirLight";
|
|
392
629
|
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
630
|
+
const hemiLight = this.#scene.getLightByName(hemiLightName);
|
|
631
|
+
const cameraLight = this.#scene.getLightByName(cameraLightName);
|
|
632
|
+
const dirLight = this.#scene.getLightByName(dirLightName);
|
|
396
633
|
|
|
397
|
-
|
|
398
|
-
this.#hemiLight = this.#scene.getLightByName("PrefViewerHemiLight");
|
|
399
|
-
if (!this.#hemiLight) {
|
|
400
|
-
this.#hemiLight = new HemisphericLight("PrefViewerHemiLight", new Vector3(-10, 10, -10), this.#scene);
|
|
401
|
-
this.#hemiLight.intensity = 0.6;
|
|
402
|
-
}
|
|
634
|
+
let lightsChanged = false;
|
|
403
635
|
|
|
404
|
-
|
|
405
|
-
this.#dirLight = this.#scene.getLightByName("PrefViewerDirLight");
|
|
406
|
-
if (!this.#dirLight) {
|
|
407
|
-
this.#dirLight = new DirectionalLight("PrefViewerDirLight", new Vector3(-10, 10, -10), this.#scene);
|
|
408
|
-
this.#dirLight.position = new Vector3(5, 4, 5); // light is IN FRONT + ABOVE + to the RIGHT
|
|
409
|
-
this.#dirLight.intensity = 0.6;
|
|
410
|
-
}
|
|
636
|
+
const iblEnabled = this.#settings.iblEnabled && (this.#options.ibl?.valid === true || !!this.#options.ibl?.cachedUrl);
|
|
411
637
|
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
638
|
+
if (iblEnabled) {
|
|
639
|
+
if (hemiLight) {
|
|
640
|
+
hemiLight.dispose();
|
|
641
|
+
}
|
|
642
|
+
if (cameraLight) {
|
|
643
|
+
cameraLight.dispose();
|
|
644
|
+
}
|
|
645
|
+
if (dirLight) {
|
|
646
|
+
dirLight.dispose();
|
|
647
|
+
}
|
|
648
|
+
this.#hemiLight = this.#dirLight = this.#cameraLight = null;
|
|
649
|
+
lightsChanged = await this.#initializeEnvironmentTexture();
|
|
650
|
+
} else {
|
|
651
|
+
// If IBL is disabled but an environment texture exists, dispose it to save resources and ensure it doesn't affect the lighting
|
|
652
|
+
if (this.#scene.environmentTexture) {
|
|
653
|
+
this.#scene.environmentTexture.dispose();
|
|
654
|
+
this.#scene.environmentTexture = null;
|
|
655
|
+
lightsChanged = true;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
// Add a hemispheric light for basic ambient illumination
|
|
659
|
+
if (!this.#hemiLight) {
|
|
660
|
+
this.#hemiLight = new HemisphericLight(hemiLightName, new Vector3(-10, 10, -10), this.#scene);
|
|
661
|
+
this.#hemiLight.intensity = 0.6;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
// Add a directional light to cast shadows and provide stronger directional illumination
|
|
665
|
+
if (!this.#dirLight) {
|
|
666
|
+
this.#dirLight = new DirectionalLight(dirLightName, new Vector3(-10, 10, -10), this.#scene);
|
|
667
|
+
this.#dirLight.position = new Vector3(5, 4, 5); // light is IN FRONT + ABOVE + to the RIGHT
|
|
668
|
+
this.#dirLight.intensity = 0.6;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
// Add a point light that follows the camera to ensure the model is always well-lit from the viewer's perspective
|
|
672
|
+
if (!this.#cameraLight) {
|
|
673
|
+
this.#cameraLight = new PointLight(cameraLightName, this.#camera.position, this.#scene);
|
|
674
|
+
this.#cameraLight.parent = this.#camera;
|
|
675
|
+
this.#cameraLight.intensity = 0.3;
|
|
676
|
+
}
|
|
418
677
|
}
|
|
678
|
+
return lightsChanged;
|
|
419
679
|
}
|
|
420
680
|
|
|
421
681
|
/**
|
|
422
682
|
* Detaches and disposes the SSAO render pipeline from the active camera when it exists.
|
|
423
683
|
* Guards against missing scene resources or absent pipelines, returning false when no cleanup is needed.
|
|
424
684
|
* @private
|
|
425
|
-
* @returns {boolean} Returns true when the SSAO pipeline was disabled, false otherwise.
|
|
685
|
+
* @returns {Promise<boolean>} Returns true when the SSAO pipeline was disabled, false otherwise.
|
|
426
686
|
*/
|
|
427
|
-
#disableAmbientOcclusion() {
|
|
687
|
+
async #disableAmbientOcclusion() {
|
|
428
688
|
const pipelineManager = this.#scene?.postProcessRenderPipelineManager;
|
|
429
689
|
|
|
430
690
|
if (!this.#scene || !pipelineManager || this.#scene?.activeCamera === null) {
|
|
@@ -432,24 +692,24 @@ export default class BabylonJSController {
|
|
|
432
692
|
}
|
|
433
693
|
|
|
434
694
|
const supportedPipelines = pipelineManager.supportedPipelines;
|
|
435
|
-
|
|
695
|
+
|
|
436
696
|
if (supportedPipelines === undefined) {
|
|
437
697
|
return false;
|
|
438
698
|
}
|
|
439
699
|
|
|
440
|
-
if (!this.#renderPipelines.ssao) {
|
|
700
|
+
if (!this.#renderPipelines.ssao || !this.#renderPipelines.ssao?.name) {
|
|
441
701
|
return false;
|
|
442
702
|
}
|
|
443
703
|
|
|
444
704
|
const pipelineName = this.#renderPipelines.ssao.name;
|
|
445
|
-
|
|
705
|
+
let ssaoPipeline = supportedPipelines ? supportedPipelines.find((pipeline) => pipeline.name === pipelineName) : false;
|
|
446
706
|
|
|
447
707
|
if (ssaoPipeline) {
|
|
448
708
|
pipelineManager.detachCamerasFromRenderPipeline(pipelineName, [this.#scene.activeCamera]);
|
|
449
|
-
ssaoPipeline.dispose();
|
|
450
709
|
pipelineManager.removePipeline(pipelineName);
|
|
451
710
|
pipelineManager.update();
|
|
452
|
-
|
|
711
|
+
ssaoPipeline.dispose();
|
|
712
|
+
this.#renderPipelines.ssao = ssaoPipeline = null;
|
|
453
713
|
}
|
|
454
714
|
|
|
455
715
|
return true;
|
|
@@ -460,9 +720,9 @@ export default class BabylonJSController {
|
|
|
460
720
|
* Disposes previous SSAO pipelines, instantiates a tuned `SSAORenderingPipeline`, and attaches it to the
|
|
461
721
|
* current camera so contact shadows enhance depth perception once assets reload or the camera changes.
|
|
462
722
|
* @private
|
|
463
|
-
* @returns {boolean} True if the SSAO pipeline is supported and enabled, otherwise false.
|
|
723
|
+
* @returns {Promise<boolean>} True if the SSAO pipeline is supported and enabled, otherwise false.
|
|
464
724
|
*/
|
|
465
|
-
#initializeAmbientOcclussion() {
|
|
725
|
+
async #initializeAmbientOcclussion() {
|
|
466
726
|
const pipelineManager = this.#scene?.postProcessRenderPipelineManager;
|
|
467
727
|
if (!this.#scene || !pipelineManager || this.#scene?.activeCamera === null) {
|
|
468
728
|
return false;
|
|
@@ -472,46 +732,47 @@ export default class BabylonJSController {
|
|
|
472
732
|
return false;
|
|
473
733
|
}
|
|
474
734
|
const supportedPipelines = pipelineManager.supportedPipelines;
|
|
475
|
-
|
|
735
|
+
|
|
476
736
|
if (supportedPipelines === undefined) {
|
|
477
737
|
return false;
|
|
478
738
|
}
|
|
479
|
-
|
|
739
|
+
|
|
480
740
|
const pipelineName = "PrefViewerSSAORenderingPipeline";
|
|
481
741
|
|
|
482
742
|
const ssaoRatio = {
|
|
483
743
|
ssaoRatio: 0.5,
|
|
484
|
-
combineRatio: 1.0
|
|
744
|
+
combineRatio: 1.0,
|
|
485
745
|
};
|
|
486
746
|
|
|
487
|
-
|
|
747
|
+
let ssaoPipeline = new SSAORenderingPipeline(pipelineName, this.#scene, ssaoRatio, [this.#scene.activeCamera]);
|
|
488
748
|
|
|
489
|
-
if (!
|
|
749
|
+
if (!ssaoPipeline) {
|
|
490
750
|
return false;
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
if (
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
if (ssaoPipeline.isSupported) {
|
|
754
|
+
ssaoPipeline.fallOff = 0.000001;
|
|
755
|
+
ssaoPipeline.area = 1;
|
|
756
|
+
ssaoPipeline.radius = 0.0001;
|
|
757
|
+
ssaoPipeline.totalStrength = 1;
|
|
758
|
+
ssaoPipeline.base = 0.6;
|
|
759
|
+
|
|
500
760
|
// Configure SSAO to calculate only once instead of every frame for better performance
|
|
501
|
-
if (
|
|
502
|
-
|
|
503
|
-
|
|
761
|
+
if (ssaoPipeline._ssaoPostProcess) {
|
|
762
|
+
ssaoPipeline._ssaoPostProcess.autoClear = false;
|
|
763
|
+
ssaoPipeline._ssaoPostProcess.samples = 1;
|
|
504
764
|
}
|
|
505
|
-
if (
|
|
506
|
-
|
|
507
|
-
|
|
765
|
+
if (ssaoPipeline._combinePostProcess) {
|
|
766
|
+
ssaoPipeline._combinePostProcess.autoClear = false;
|
|
767
|
+
ssaoPipeline._combinePostProcess.samples = 1;
|
|
508
768
|
}
|
|
509
|
-
|
|
769
|
+
|
|
770
|
+
this.#renderPipelines.ssao = ssaoPipeline;
|
|
510
771
|
pipelineManager.update();
|
|
511
772
|
return true;
|
|
512
773
|
} else {
|
|
513
|
-
|
|
514
|
-
this.#renderPipelines.ssao = null;
|
|
774
|
+
ssaoPipeline.dispose();
|
|
775
|
+
this.#renderPipelines.ssao = ssaoPipeline = null;
|
|
515
776
|
pipelineManager.update();
|
|
516
777
|
return false;
|
|
517
778
|
}
|
|
@@ -521,9 +782,9 @@ export default class BabylonJSController {
|
|
|
521
782
|
* Tears down the default rendering pipeline (MSAA/FXAA/grain) for the active camera when present.
|
|
522
783
|
* Ensures stale pipelines detach cleanly so a fresh one can be installed on the next load.
|
|
523
784
|
* @private
|
|
524
|
-
* @returns {boolean} Returns true when the pipeline was removed, false otherwise.
|
|
785
|
+
* @returns {Promise<boolean>} Returns true when the pipeline was removed, false otherwise.
|
|
525
786
|
*/
|
|
526
|
-
#disableVisualImprovements() {
|
|
787
|
+
async #disableVisualImprovements() {
|
|
527
788
|
const pipelineManager = this.#scene?.postProcessRenderPipelineManager;
|
|
528
789
|
|
|
529
790
|
if (!this.#scene || !pipelineManager || this.#scene?.activeCamera === null) {
|
|
@@ -531,24 +792,24 @@ export default class BabylonJSController {
|
|
|
531
792
|
}
|
|
532
793
|
|
|
533
794
|
const supportedPipelines = pipelineManager.supportedPipelines;
|
|
534
|
-
|
|
795
|
+
|
|
535
796
|
if (supportedPipelines === undefined) {
|
|
536
797
|
return false;
|
|
537
798
|
}
|
|
538
799
|
|
|
539
|
-
if (!this.#renderPipelines.default) {
|
|
800
|
+
if (!this.#renderPipelines.default || !this.#renderPipelines.default?.name) {
|
|
540
801
|
return false;
|
|
541
802
|
}
|
|
542
803
|
|
|
543
804
|
const pipelineName = this.#renderPipelines.default.name;
|
|
544
|
-
|
|
805
|
+
let defaultPipeline = supportedPipelines ? supportedPipelines.find((pipeline) => pipeline.name === pipelineName) : false;
|
|
545
806
|
|
|
546
807
|
if (defaultPipeline) {
|
|
547
808
|
pipelineManager.detachCamerasFromRenderPipeline(pipelineName, [this.#scene.activeCamera]);
|
|
548
|
-
defaultPipeline.dispose();
|
|
549
809
|
pipelineManager.removePipeline(pipelineName);
|
|
550
810
|
pipelineManager.update();
|
|
551
|
-
|
|
811
|
+
defaultPipeline.dispose();
|
|
812
|
+
this.#renderPipelines.default = defaultPipeline = null;
|
|
552
813
|
}
|
|
553
814
|
|
|
554
815
|
return true;
|
|
@@ -559,10 +820,10 @@ export default class BabylonJSController {
|
|
|
559
820
|
* Disposes any previous pipeline instance to avoid duplicates, then attaches a fresh
|
|
560
821
|
* `DefaultRenderingPipeline` with tuned settings for sharper anti-aliasing and subtle grain.
|
|
561
822
|
* @private
|
|
562
|
-
* @returns {boolean} True when the pipeline is supported and active, otherwise false.
|
|
823
|
+
* @returns {Promise<boolean>} True when the pipeline is supported and active, otherwise false.
|
|
563
824
|
* @see {@link https://doc.babylonjs.com/features/featuresDeepDive/postProcesses/defaultRenderingPipeline|Using the Default Rendering Pipeline | Babylon.js Documentation}
|
|
564
825
|
*/
|
|
565
|
-
#initializeVisualImprovements() {
|
|
826
|
+
async #initializeVisualImprovements() {
|
|
566
827
|
const pipelineManager = this.#scene?.postProcessRenderPipelineManager;
|
|
567
828
|
if (!this.#scene || !pipelineManager || this.#scene?.activeCamera === null) {
|
|
568
829
|
return false;
|
|
@@ -572,56 +833,58 @@ export default class BabylonJSController {
|
|
|
572
833
|
return false;
|
|
573
834
|
}
|
|
574
835
|
const supportedPipelines = pipelineManager.supportedPipelines;
|
|
575
|
-
|
|
836
|
+
|
|
576
837
|
if (supportedPipelines === undefined) {
|
|
577
838
|
return false;
|
|
578
839
|
}
|
|
579
|
-
|
|
840
|
+
|
|
580
841
|
const pipelineName = "PrefViewerDefaultRenderingPipeline";
|
|
581
842
|
|
|
582
|
-
|
|
843
|
+
let defaultPipeline = new DefaultRenderingPipeline(pipelineName, true, this.#scene, [this.#scene.activeCamera], true);
|
|
583
844
|
|
|
584
|
-
if (!
|
|
845
|
+
if (!defaultPipeline) {
|
|
585
846
|
return false;
|
|
586
|
-
}
|
|
847
|
+
}
|
|
587
848
|
|
|
588
|
-
if (
|
|
849
|
+
if (defaultPipeline.isSupported) {
|
|
589
850
|
// MSAA - Multisample Anti-Aliasing
|
|
590
851
|
const caps = this.#scene.getEngine()?.getCaps?.() || {};
|
|
591
852
|
const maxSamples = typeof caps.maxMSAASamples === "number" ? caps.maxMSAASamples : 4;
|
|
592
|
-
|
|
853
|
+
defaultPipeline.samples = Math.max(1, Math.min(8, maxSamples));
|
|
593
854
|
// FXAA - Fast Approximate Anti-Aliasing
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
if (
|
|
598
|
-
|
|
855
|
+
defaultPipeline.fxaaEnabled = true;
|
|
856
|
+
defaultPipeline.fxaa.samples = 8;
|
|
857
|
+
defaultPipeline.fxaa.adaptScaleToCurrentViewport = true;
|
|
858
|
+
if (defaultPipeline.fxaa.edgeThreshold !== undefined) {
|
|
859
|
+
defaultPipeline.fxaa.edgeThreshold = 0.125;
|
|
599
860
|
}
|
|
600
|
-
if (
|
|
601
|
-
|
|
861
|
+
if (defaultPipeline.fxaa.edgeThresholdMin !== undefined) {
|
|
862
|
+
defaultPipeline.fxaa.edgeThresholdMin = 0.0625;
|
|
602
863
|
}
|
|
603
|
-
if (
|
|
604
|
-
|
|
864
|
+
if (defaultPipeline.fxaa.subPixelQuality !== undefined) {
|
|
865
|
+
defaultPipeline.fxaa.subPixelQuality = 0.75;
|
|
605
866
|
}
|
|
606
867
|
|
|
607
868
|
// Grain
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
869
|
+
defaultPipeline.grainEnabled = true;
|
|
870
|
+
defaultPipeline.grain.adaptScaleToCurrentViewport = true;
|
|
871
|
+
defaultPipeline.grain.animated = false;
|
|
872
|
+
defaultPipeline.grain.intensity = 3;
|
|
612
873
|
|
|
613
874
|
// Configure post-processes to calculate only once instead of every frame for better performance
|
|
614
|
-
if (
|
|
615
|
-
|
|
875
|
+
if (defaultPipeline.fxaa?._postProcess) {
|
|
876
|
+
defaultPipeline.fxaa._postProcess.autoClear = false;
|
|
616
877
|
}
|
|
617
|
-
if (
|
|
618
|
-
|
|
878
|
+
if (defaultPipeline.grain?._postProcess) {
|
|
879
|
+
defaultPipeline.grain._postProcess.autoClear = false;
|
|
619
880
|
}
|
|
620
881
|
|
|
882
|
+
this.#renderPipelines.default = defaultPipeline;
|
|
621
883
|
pipelineManager.update();
|
|
622
884
|
return true;
|
|
623
885
|
} else {
|
|
624
|
-
|
|
886
|
+
defaultPipeline.dispose();
|
|
887
|
+
this.#renderPipelines.default = defaultPipeline = null;
|
|
625
888
|
pipelineManager.update();
|
|
626
889
|
return false;
|
|
627
890
|
}
|
|
@@ -629,16 +892,46 @@ export default class BabylonJSController {
|
|
|
629
892
|
|
|
630
893
|
/**
|
|
631
894
|
* Initializes the environment texture for the Babylon.js scene.
|
|
632
|
-
*
|
|
633
|
-
*
|
|
895
|
+
* Resolves the active HDR environment texture using either a fresh `cachedUrl`
|
|
896
|
+
* or the reusable in-memory clone (`#hdrTexture`), then assigns it to `scene.environmentTexture`.
|
|
634
897
|
* @private
|
|
635
|
-
* @returns {boolean}
|
|
898
|
+
* @returns {Promise<boolean>} Returns true if the environment texture was changed, false if it was already up to date or failed to load.
|
|
899
|
+
* @description
|
|
900
|
+
* Lifecycle implemented here:
|
|
901
|
+
* 1. If `options.ibl.cachedUrl` exists, create `HDRCubeTexture` from it.
|
|
902
|
+
* 2. Wait for readiness, clone it into `#hdrTexture` for reuse.
|
|
903
|
+
* 3. Call `options.ibl.consumeCachedUrl(true)` to revoke temporary object URLs and clear `cachedUrl`.
|
|
904
|
+
* 4. On following reloads, if `options.ibl.valid === true` and no `cachedUrl` is present, use `#hdrTexture.clone()`.
|
|
636
905
|
*/
|
|
637
|
-
#initializeEnvironmentTexture() {
|
|
638
|
-
|
|
639
|
-
|
|
906
|
+
async #initializeEnvironmentTexture() {
|
|
907
|
+
if (this.#scene.environmentTexture) {
|
|
908
|
+
this.#scene.environmentTexture.dispose();
|
|
909
|
+
this.#scene.environmentTexture = null;
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
let hdrTexture = null;
|
|
913
|
+
if (this.#options.ibl?.cachedUrl) {
|
|
914
|
+
const hdrTextureURI = this.#options.ibl.cachedUrl;
|
|
915
|
+
hdrTexture = new HDRCubeTexture(hdrTextureURI, this.#scene, 1024, false, false, false, true, undefined, undefined, false, true, true);
|
|
916
|
+
} else if (this.#hdrTexture && this.#options.ibl?.valid === true) {
|
|
917
|
+
hdrTexture = this.#hdrTexture.clone();
|
|
918
|
+
} else {
|
|
919
|
+
return false;
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
await WhenTextureReadyAsync(hdrTexture);
|
|
923
|
+
|
|
924
|
+
if (this.#options.ibl?.cachedUrl) {
|
|
925
|
+
if (this.#hdrTexture) {
|
|
926
|
+
this.#hdrTexture.dispose();
|
|
927
|
+
}
|
|
928
|
+
this.#hdrTexture = hdrTexture.clone();
|
|
929
|
+
this.#options.ibl?.consumeCachedUrl?.(true);
|
|
930
|
+
}
|
|
931
|
+
|
|
640
932
|
hdrTexture.level = this.#options.ibl.intensity;
|
|
641
933
|
this.#scene.environmentTexture = hdrTexture;
|
|
934
|
+
this.#scene.markAllMaterialsAsDirty(Material.TextureDirtyFlag);
|
|
642
935
|
return true;
|
|
643
936
|
}
|
|
644
937
|
|
|
@@ -646,9 +939,9 @@ export default class BabylonJSController {
|
|
|
646
939
|
* Removes the IBL shadow render pipeline from the active camera when present.
|
|
647
940
|
* Ensures voxelized shadow data is disposed so reloading environments installs a clean pipeline.
|
|
648
941
|
* @private
|
|
649
|
-
* @returns {boolean} Returns true when the pipeline was removed, false otherwise.
|
|
942
|
+
* @returns {Promise<boolean>} Returns true when the pipeline was removed, false otherwise.
|
|
650
943
|
*/
|
|
651
|
-
#disableIBLShadows() {
|
|
944
|
+
async #disableIBLShadows() {
|
|
652
945
|
const pipelineManager = this.#scene?.postProcessRenderPipelineManager;
|
|
653
946
|
|
|
654
947
|
if (!this.#scene || !pipelineManager || this.#scene?.activeCamera === null) {
|
|
@@ -656,24 +949,28 @@ export default class BabylonJSController {
|
|
|
656
949
|
}
|
|
657
950
|
|
|
658
951
|
const supportedPipelines = pipelineManager.supportedPipelines;
|
|
659
|
-
|
|
952
|
+
|
|
660
953
|
if (supportedPipelines === undefined) {
|
|
661
954
|
return false;
|
|
662
955
|
}
|
|
663
956
|
|
|
664
|
-
if (!this.#renderPipelines.iblShadows) {
|
|
957
|
+
if (!this.#renderPipelines.iblShadows || !this.#renderPipelines.iblShadows?.name) {
|
|
665
958
|
return false;
|
|
666
959
|
}
|
|
667
960
|
|
|
668
961
|
const pipelineName = this.#renderPipelines.iblShadows.name;
|
|
669
|
-
|
|
962
|
+
let iblShadowsPipeline = supportedPipelines ? supportedPipelines.find((pipeline) => pipeline.name === pipelineName) : false;
|
|
670
963
|
|
|
671
|
-
if (
|
|
964
|
+
if (iblShadowsPipeline) {
|
|
965
|
+
iblShadowsPipeline.toggleShadow(false);
|
|
966
|
+
iblShadowsPipeline.clearShadowCastingMeshes();
|
|
967
|
+
iblShadowsPipeline.clearShadowReceivingMaterials();
|
|
968
|
+
iblShadowsPipeline.resetAccumulation();
|
|
672
969
|
pipelineManager.detachCamerasFromRenderPipeline(pipelineName, [this.#scene.activeCamera]);
|
|
673
|
-
defaultPipeline.dispose();
|
|
674
970
|
pipelineManager.removePipeline(pipelineName);
|
|
675
971
|
pipelineManager.update();
|
|
676
|
-
|
|
972
|
+
iblShadowsPipeline.dispose();
|
|
973
|
+
this.#renderPipelines.iblShadows = iblShadowsPipeline = null;
|
|
677
974
|
}
|
|
678
975
|
|
|
679
976
|
return true;
|
|
@@ -685,24 +982,57 @@ export default class BabylonJSController {
|
|
|
685
982
|
* Configures pipeline options for resolution, sampling, opacity, and debugging.
|
|
686
983
|
* Only applies if the scene has an environment texture set.
|
|
687
984
|
* @private
|
|
688
|
-
* @returns {void|
|
|
985
|
+
* @returns {Promise<void|boolean>} Returns false if no environment texture is set; otherwise void.
|
|
689
986
|
*/
|
|
690
|
-
#initializeIBLShadows() {
|
|
987
|
+
async #initializeIBLShadows() {
|
|
691
988
|
const pipelineManager = this.#scene?.postProcessRenderPipelineManager;
|
|
692
989
|
|
|
693
|
-
if (!this.#scene || !
|
|
990
|
+
if (!this.#scene || !this.#scene?.activeCamera || !this.#scene?.environmentTexture || !pipelineManager) {
|
|
991
|
+
return false;
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
if (!this.#scene.environmentTexture.isReady()) {
|
|
995
|
+
const self = this;
|
|
996
|
+
this.#scene.environmentTexture.onLoadObservable.addOnce(() => {
|
|
997
|
+
self.#initializeIBLShadows();
|
|
998
|
+
});
|
|
694
999
|
return false;
|
|
695
1000
|
}
|
|
696
1001
|
|
|
697
|
-
|
|
1002
|
+
const meshesForCastingShadows = this.#scene.meshes.filter((mesh) => {
|
|
1003
|
+
const isRootMesh = mesh.id.startsWith("__root__");
|
|
1004
|
+
if (isRootMesh) {
|
|
1005
|
+
return false;
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
const isHDRIMesh = mesh.name?.toLowerCase() === "hdri";
|
|
1009
|
+
const extrasCastShadows = mesh.metadata?.gltf?.extras?.castShadows;
|
|
1010
|
+
const meshGenerateShadows = typeof extrasCastShadows === "boolean" ? extrasCastShadows : isHDRIMesh ? false : true;
|
|
1011
|
+
|
|
1012
|
+
if (meshGenerateShadows) {
|
|
1013
|
+
return true;
|
|
1014
|
+
}
|
|
1015
|
+
return false;
|
|
1016
|
+
});
|
|
1017
|
+
const materialsForReceivingShadows = this.#scene.materials.filter((material) => {
|
|
1018
|
+
if (material instanceof PBRMaterial) {
|
|
1019
|
+
material.enableSpecularAntiAliasing = false;
|
|
1020
|
+
}
|
|
1021
|
+
return true;
|
|
1022
|
+
});
|
|
1023
|
+
|
|
1024
|
+
if (meshesForCastingShadows.length === 0 || materialsForReceivingShadows.length === 0) {
|
|
698
1025
|
return false;
|
|
699
1026
|
}
|
|
1027
|
+
|
|
700
1028
|
const supportedPipelines = pipelineManager.supportedPipelines;
|
|
701
|
-
|
|
1029
|
+
|
|
702
1030
|
if (!supportedPipelines) {
|
|
703
1031
|
return false;
|
|
704
1032
|
}
|
|
705
|
-
|
|
1033
|
+
|
|
1034
|
+
await this.#scene.whenReadyAsync();
|
|
1035
|
+
|
|
706
1036
|
const pipelineName = "PrefViewerIblShadowsRenderPipeline";
|
|
707
1037
|
|
|
708
1038
|
const pipelineOptions = {
|
|
@@ -714,13 +1044,13 @@ export default class BabylonJSController {
|
|
|
714
1044
|
shadowOpacity: 0.85,
|
|
715
1045
|
};
|
|
716
1046
|
|
|
717
|
-
|
|
1047
|
+
let iblShadowsPipeline = new IblShadowsRenderPipeline(pipelineName, this.#scene, pipelineOptions, [this.#scene.activeCamera]);
|
|
718
1048
|
|
|
719
|
-
if (!
|
|
1049
|
+
if (!iblShadowsPipeline) {
|
|
720
1050
|
return false;
|
|
721
1051
|
}
|
|
722
|
-
|
|
723
|
-
if (
|
|
1052
|
+
|
|
1053
|
+
if (iblShadowsPipeline.isSupported) {
|
|
724
1054
|
// Disable all debug passes for performance
|
|
725
1055
|
const pipelineProps = {
|
|
726
1056
|
allowDebugPasses: false,
|
|
@@ -734,41 +1064,20 @@ export default class BabylonJSController {
|
|
|
734
1064
|
accumulationPassDebugEnabled: false,
|
|
735
1065
|
};
|
|
736
1066
|
|
|
737
|
-
Object.assign(
|
|
738
|
-
|
|
739
|
-
if (this.#renderPipelines.iblShadows._ssaoPostProcess) {
|
|
740
|
-
this.#renderPipelines.iblShadows._ssaoPostProcess.autoClear = false;
|
|
741
|
-
this.#renderPipelines.iblShadows._ssaoPostProcess.samples = 1;
|
|
742
|
-
}
|
|
743
|
-
|
|
744
|
-
this.#scene.meshes.forEach((mesh) => {
|
|
745
|
-
const isRootMesh = mesh.id.startsWith("__root__");
|
|
746
|
-
if (isRootMesh) {
|
|
747
|
-
return false;
|
|
748
|
-
}
|
|
749
|
-
|
|
750
|
-
const isHDRIMesh = mesh.name?.toLowerCase() === "hdri";
|
|
751
|
-
const extrasCastShadows = mesh.metadata?.gltf?.extras?.castShadows;
|
|
752
|
-
const meshGenerateShadows = typeof extrasCastShadows === "boolean" ? extrasCastShadows : isHDRIMesh ? false : true;
|
|
753
|
-
|
|
754
|
-
if (meshGenerateShadows) {
|
|
755
|
-
this.#renderPipelines.iblShadows.addShadowCastingMesh(mesh);
|
|
756
|
-
this.#renderPipelines.iblShadows.updateSceneBounds();
|
|
757
|
-
}
|
|
758
|
-
});
|
|
1067
|
+
Object.assign(iblShadowsPipeline, pipelineProps);
|
|
759
1068
|
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
material.enableSpecularAntiAliasing = false;
|
|
763
|
-
}
|
|
764
|
-
this.#renderPipelines.iblShadows.addShadowReceivingMaterial(material);
|
|
765
|
-
});
|
|
1069
|
+
meshesForCastingShadows.forEach((mesh) => iblShadowsPipeline.addShadowCastingMesh(mesh));
|
|
1070
|
+
materialsForReceivingShadows.forEach((material) => iblShadowsPipeline.addShadowReceivingMaterial(material));
|
|
766
1071
|
|
|
767
|
-
|
|
1072
|
+
iblShadowsPipeline.updateSceneBounds();
|
|
1073
|
+
iblShadowsPipeline.toggleShadow(true);
|
|
1074
|
+
iblShadowsPipeline.updateVoxelization();
|
|
1075
|
+
this.#renderPipelines.iblShadows = iblShadowsPipeline;
|
|
768
1076
|
pipelineManager.update();
|
|
769
1077
|
return true;
|
|
770
1078
|
} else {
|
|
771
|
-
|
|
1079
|
+
iblShadowsPipeline.dispose();
|
|
1080
|
+
this.#renderPipelines.iblShadows = iblShadowsPipeline = null;
|
|
772
1081
|
pipelineManager.update();
|
|
773
1082
|
return false;
|
|
774
1083
|
}
|
|
@@ -802,7 +1111,7 @@ export default class BabylonJSController {
|
|
|
802
1111
|
* @private
|
|
803
1112
|
* @returns {void}
|
|
804
1113
|
*/
|
|
805
|
-
#initializeDefaultLightShadows() {
|
|
1114
|
+
async #initializeDefaultLightShadows() {
|
|
806
1115
|
if (!this.#dirLight) {
|
|
807
1116
|
return;
|
|
808
1117
|
}
|
|
@@ -825,7 +1134,7 @@ export default class BabylonJSController {
|
|
|
825
1134
|
* @private
|
|
826
1135
|
* @returns {void}
|
|
827
1136
|
*/
|
|
828
|
-
#initializeEnvironmentShadows() {
|
|
1137
|
+
async #initializeEnvironmentShadows() {
|
|
829
1138
|
this.#shadowGen = this.#shadowGen.filter((generator) => {
|
|
830
1139
|
if (!generator || typeof generator.getLight !== "function") {
|
|
831
1140
|
return false;
|
|
@@ -874,12 +1183,12 @@ export default class BabylonJSController {
|
|
|
874
1183
|
* @private
|
|
875
1184
|
* @returns {void}
|
|
876
1185
|
*/
|
|
877
|
-
#disableShadows() {
|
|
1186
|
+
async #disableShadows() {
|
|
878
1187
|
this.#shadowGen.forEach((shadowGenerator) => {
|
|
879
1188
|
shadowGenerator.dispose();
|
|
880
1189
|
});
|
|
881
1190
|
this.#shadowGen = [];
|
|
882
|
-
this.#disableIBLShadows();
|
|
1191
|
+
await this.#disableIBLShadows();
|
|
883
1192
|
}
|
|
884
1193
|
|
|
885
1194
|
/**
|
|
@@ -890,28 +1199,22 @@ export default class BabylonJSController {
|
|
|
890
1199
|
* If no environment texture is set, initializes IBL shadows.
|
|
891
1200
|
* Otherwise, sets up shadow casting and receiving for all relevant meshes using the shadow generator.
|
|
892
1201
|
*/
|
|
893
|
-
#initializeShadows() {
|
|
1202
|
+
async #initializeShadows() {
|
|
894
1203
|
if (!this.#settings.shadowsEnabled) {
|
|
895
1204
|
return false;
|
|
896
1205
|
}
|
|
897
1206
|
|
|
898
1207
|
this.#ensureMeshesReceiveShadows();
|
|
899
1208
|
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
const self = this;
|
|
906
|
-
this.#scene.environmentTexture.onLoadObservable.addOnce(() => {
|
|
907
|
-
self.#initializeIBLShadows();
|
|
908
|
-
});
|
|
909
|
-
}
|
|
910
|
-
}
|
|
1209
|
+
const iblEnabled = this.#settings.iblEnabled && (this.#options.ibl?.valid === true || !!this.#options.ibl?.cachedUrl);
|
|
1210
|
+
const iblShadowsEnabled = iblEnabled && this.#options.ibl.shadows;
|
|
1211
|
+
|
|
1212
|
+
if (iblShadowsEnabled) {
|
|
1213
|
+
await this.#initializeIBLShadows();
|
|
911
1214
|
} else {
|
|
912
|
-
this.#initializeDefaultLightShadows();
|
|
1215
|
+
await this.#initializeDefaultLightShadows();
|
|
913
1216
|
}
|
|
914
|
-
this.#initializeEnvironmentShadows();
|
|
1217
|
+
await this.#initializeEnvironmentShadows();
|
|
915
1218
|
}
|
|
916
1219
|
|
|
917
1220
|
/**
|
|
@@ -933,20 +1236,52 @@ export default class BabylonJSController {
|
|
|
933
1236
|
}
|
|
934
1237
|
|
|
935
1238
|
/**
|
|
936
|
-
*
|
|
1239
|
+
* Resets pointer-picking sampling state.
|
|
937
1240
|
* @private
|
|
938
|
-
* @param {PointerInfo} info - The pointer event information from Babylon.js.
|
|
939
1241
|
* @returns {void}
|
|
940
1242
|
*/
|
|
941
|
-
#
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
1243
|
+
#resetPointerPickingState() {
|
|
1244
|
+
this.#state.pointerPicking.lastMovePickAt = 0;
|
|
1245
|
+
this.#state.pointerPicking.lastMovePickX = NaN;
|
|
1246
|
+
this.#state.pointerPicking.lastMovePickY = NaN;
|
|
1247
|
+
this.#state.pointerPicking.lastPickedMeshId = null;
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
/**
|
|
1251
|
+
* Decides whether a POINTERMOVE event should trigger a scene raycast.
|
|
1252
|
+
* Uses time + distance sampling to avoid expensive pick calls on every mouse move.
|
|
1253
|
+
* @private
|
|
1254
|
+
* @returns {boolean}
|
|
1255
|
+
*/
|
|
1256
|
+
#shouldPickOnPointerMove() {
|
|
1257
|
+
if (!this.#scene || !this.#babylonJSAnimationController) {
|
|
1258
|
+
return false;
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
const now = typeof performance !== "undefined" && performance.now ? performance.now() : Date.now();
|
|
1262
|
+
const x = this.#scene.pointerX;
|
|
1263
|
+
const y = this.#scene.pointerY;
|
|
1264
|
+
const state = this.#state.pointerPicking;
|
|
1265
|
+
const config = this.#config.pointerPicking;
|
|
1266
|
+
|
|
1267
|
+
if (!Number.isFinite(state.lastMovePickX) || !Number.isFinite(state.lastMovePickY)) {
|
|
1268
|
+
state.lastMovePickX = x;
|
|
1269
|
+
state.lastMovePickY = y;
|
|
1270
|
+
state.lastMovePickAt = now;
|
|
1271
|
+
return true;
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
const elapsed = now - state.lastMovePickAt >= config.movePickIntervalMs;
|
|
1275
|
+
const moved = Math.abs(x - state.lastMovePickX) >= config.movePickMinDistancePx || Math.abs(y - state.lastMovePickY) >= config.movePickMinDistancePx;
|
|
1276
|
+
|
|
1277
|
+
if (!elapsed || !moved) {
|
|
1278
|
+
return false;
|
|
949
1279
|
}
|
|
1280
|
+
|
|
1281
|
+
state.lastMovePickX = x;
|
|
1282
|
+
state.lastMovePickY = y;
|
|
1283
|
+
state.lastMovePickAt = now;
|
|
1284
|
+
return true;
|
|
950
1285
|
}
|
|
951
1286
|
|
|
952
1287
|
/**
|
|
@@ -961,6 +1296,10 @@ export default class BabylonJSController {
|
|
|
961
1296
|
if (this.#scene) {
|
|
962
1297
|
this.#scene.onPointerObservable.add(this.#handlers.onPointerObservable);
|
|
963
1298
|
}
|
|
1299
|
+
if (this.#engine) {
|
|
1300
|
+
this.#canvasResizeObserver = new ResizeObserver(this.#handlers.onResize);
|
|
1301
|
+
this.#canvasResizeObserver.observe(this.#canvas);
|
|
1302
|
+
}
|
|
964
1303
|
}
|
|
965
1304
|
|
|
966
1305
|
/**
|
|
@@ -975,6 +1314,11 @@ export default class BabylonJSController {
|
|
|
975
1314
|
if (this.#scene !== null) {
|
|
976
1315
|
this.#scene.onPointerObservable.removeCallback(this.#handlers.onPointerObservable);
|
|
977
1316
|
}
|
|
1317
|
+
this.#cancelScheduledResize();
|
|
1318
|
+
this.#canvasResizeObserver?.disconnect();
|
|
1319
|
+
this.#canvasResizeObserver = null;
|
|
1320
|
+
this.#detachAnimationChangedListener();
|
|
1321
|
+
this.#resetPointerPickingState();
|
|
978
1322
|
}
|
|
979
1323
|
|
|
980
1324
|
/**
|
|
@@ -990,31 +1334,56 @@ export default class BabylonJSController {
|
|
|
990
1334
|
}
|
|
991
1335
|
|
|
992
1336
|
/**
|
|
993
|
-
* Disposes the
|
|
1337
|
+
* Disposes the shared GLTFResolver instance and closes its underlying storage handle.
|
|
994
1338
|
* @private
|
|
995
1339
|
* @returns {void}
|
|
996
1340
|
*/
|
|
997
|
-
#
|
|
998
|
-
if (
|
|
1341
|
+
#disposeGLTFResolver() {
|
|
1342
|
+
if (this.#gltfResolver) {
|
|
1343
|
+
this.#gltfResolver.dispose();
|
|
1344
|
+
this.#gltfResolver = null;
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
/**
|
|
1349
|
+
* Disposes the Babylon.js WebXR experience if it exists.
|
|
1350
|
+
* If XR is currently active, waits for `exitXRAsync()` before disposing to avoid
|
|
1351
|
+
* tearing down the engine while the XR session is still shutting down.
|
|
1352
|
+
* Concurrent calls share the same in-flight promise so disposal runs only once.
|
|
1353
|
+
* @private
|
|
1354
|
+
* @returns {Promise<void>}
|
|
1355
|
+
*/
|
|
1356
|
+
async #disposeXRExperience() {
|
|
1357
|
+
if (this.#disablingPromises.xr) {
|
|
1358
|
+
return await this.#disablingPromises.xr;
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
const xrExperience = this.#XRExperience;
|
|
1362
|
+
if (!xrExperience) {
|
|
999
1363
|
return;
|
|
1000
1364
|
}
|
|
1001
1365
|
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
.
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1366
|
+
this.#disablingPromises.xr = (async () => {
|
|
1367
|
+
try {
|
|
1368
|
+
if (xrExperience.baseExperience?.state === WebXRState.IN_XR) {
|
|
1369
|
+
await xrExperience.baseExperience.exitXRAsync();
|
|
1370
|
+
}
|
|
1371
|
+
} catch (error) {
|
|
1372
|
+
console.warn("PrefViewer: Error exiting XR experience:", error);
|
|
1373
|
+
} finally {
|
|
1374
|
+
try {
|
|
1375
|
+
xrExperience.dispose();
|
|
1376
|
+
} catch (error) {
|
|
1377
|
+
console.warn("PrefViewer: Error disposing XR experience:", error);
|
|
1378
|
+
}
|
|
1379
|
+
if (this.#XRExperience === xrExperience) {
|
|
1012
1380
|
this.#XRExperience = null;
|
|
1013
|
-
}
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1381
|
+
}
|
|
1382
|
+
this.#disablingPromises.xr = null;
|
|
1383
|
+
}
|
|
1384
|
+
})();
|
|
1385
|
+
|
|
1386
|
+
await this.#disablingPromises.xr;
|
|
1018
1387
|
}
|
|
1019
1388
|
|
|
1020
1389
|
/**
|
|
@@ -1026,11 +1395,88 @@ export default class BabylonJSController {
|
|
|
1026
1395
|
if (!this.#engine) {
|
|
1027
1396
|
return;
|
|
1028
1397
|
}
|
|
1398
|
+
if (this.#hdrTexture) {
|
|
1399
|
+
this.#hdrTexture.dispose();
|
|
1400
|
+
this.#hdrTexture = null;
|
|
1401
|
+
}
|
|
1029
1402
|
this.#engine.dispose();
|
|
1030
1403
|
this.#engine = this.#scene = this.#camera = null;
|
|
1031
1404
|
this.#hemiLight = this.#dirLight = this.#cameraLight = null;
|
|
1032
1405
|
}
|
|
1033
1406
|
|
|
1407
|
+
/**
|
|
1408
|
+
* Handles animation state events emitted by `OpeningAnimation` instances.
|
|
1409
|
+
* Routes opening/closing states to continuous rendering and all other states
|
|
1410
|
+
* (paused/opened/closed) to a short final render burst.
|
|
1411
|
+
* @private
|
|
1412
|
+
* @param {CustomEvent} event - Event carrying animation state in `event.detail.state`.
|
|
1413
|
+
* @returns {void}
|
|
1414
|
+
*/
|
|
1415
|
+
#onAnimationGroupChanged(event) {
|
|
1416
|
+
const state = event?.detail?.state;
|
|
1417
|
+
if (state === undefined) {
|
|
1418
|
+
return;
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
if (state === OpeningAnimation.states.opening || state === OpeningAnimation.states.closing) {
|
|
1422
|
+
this.#onAnimationGroupPlay();
|
|
1423
|
+
} else {
|
|
1424
|
+
this.#onAnimationGroupStop();
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1428
|
+
/**
|
|
1429
|
+
* Marks animation playback as active and requests short continuous rendering so animated transforms remain smooth while state is changing.
|
|
1430
|
+
* @private
|
|
1431
|
+
* @returns {void}
|
|
1432
|
+
*/
|
|
1433
|
+
#onAnimationGroupPlay() {
|
|
1434
|
+
this.#requestRender({ frames: 1, continuousMs: this.#config.render.animationMs });
|
|
1435
|
+
}
|
|
1436
|
+
|
|
1437
|
+
/**
|
|
1438
|
+
* Handles animation stop/pause/end transitions by requesting a final render burst.
|
|
1439
|
+
* @private
|
|
1440
|
+
* @returns {void}
|
|
1441
|
+
*/
|
|
1442
|
+
#onAnimationGroupStop() {
|
|
1443
|
+
if (this.#settings.iblEnabled && this.#renderPipelines.iblShadows) {
|
|
1444
|
+
this.#renderPipelines.iblShadows.updateVoxelization();
|
|
1445
|
+
this.#scene?.postProcessRenderPipelineManager?.update();
|
|
1446
|
+
}
|
|
1447
|
+
const frames = this.#settings.antiAliasingEnabled || this.#settings.ambientOcclusionEnabled || this.#settings.iblEnabled ? this.#config.render.burstFramesEnhanced : this.#config.render.burstFramesBase;
|
|
1448
|
+
this.#requestRender({ frames: frames });
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1451
|
+
/**
|
|
1452
|
+
* Attaches the `prefviewer-animation-changed` listener to the nearest `pref-viewer-3d` host.
|
|
1453
|
+
* Removes any previous registration first to avoid duplicate callbacks across reload cycles.
|
|
1454
|
+
* @private
|
|
1455
|
+
* @returns {boolean} True when the listener is attached, false when no host is available.
|
|
1456
|
+
*/
|
|
1457
|
+
#attachAnimationChangedListener() {
|
|
1458
|
+
this.#getPrefViewer3DComponent();
|
|
1459
|
+
if (!this.#prefViewer3D) {
|
|
1460
|
+
return false;
|
|
1461
|
+
}
|
|
1462
|
+
this.#detachAnimationChangedListener();
|
|
1463
|
+
this.#prefViewer3D.addEventListener("prefviewer-animation-changed", this.#handlers.onAnimationGroupChanged);
|
|
1464
|
+
return true;
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
/**
|
|
1468
|
+
* Detaches the `prefviewer-animation-changed` listener from the cached `pref-viewer-3d` host.
|
|
1469
|
+
* @private
|
|
1470
|
+
* @returns {boolean} True when a host exists and the listener removal was attempted, false otherwise.
|
|
1471
|
+
*/
|
|
1472
|
+
#detachAnimationChangedListener() {
|
|
1473
|
+
if (!this.#prefViewer3D) {
|
|
1474
|
+
return false;
|
|
1475
|
+
}
|
|
1476
|
+
this.#prefViewer3D.removeEventListener("prefviewer-animation-changed", this.#handlers.onAnimationGroupChanged);
|
|
1477
|
+
return true;
|
|
1478
|
+
}
|
|
1479
|
+
|
|
1034
1480
|
/**
|
|
1035
1481
|
* Handles keyup events on the Babylon.js canvas for triggering model and scene downloads.
|
|
1036
1482
|
* @private
|
|
@@ -1038,10 +1484,10 @@ export default class BabylonJSController {
|
|
|
1038
1484
|
* @returns {void}
|
|
1039
1485
|
*/
|
|
1040
1486
|
#onKeyUp(event) {
|
|
1041
|
-
// CTRL + ALT + letter
|
|
1042
|
-
if (event.ctrlKey && event.altKey && event.
|
|
1043
|
-
switch (event.
|
|
1044
|
-
case "
|
|
1487
|
+
// CTRL + ALT + letter (uses event.code for physical key, layout-independent — fixes Mac Option+D producing "∂" instead of "d")
|
|
1488
|
+
if (event.ctrlKey && event.altKey && event.code !== undefined) {
|
|
1489
|
+
switch (event.code) {
|
|
1490
|
+
case "KeyD":
|
|
1045
1491
|
this.#openDownloadDialog();
|
|
1046
1492
|
break;
|
|
1047
1493
|
default:
|
|
@@ -1058,11 +1504,12 @@ export default class BabylonJSController {
|
|
|
1058
1504
|
* @returns {void|false} Returns false if there is no active camera; otherwise, void.
|
|
1059
1505
|
*/
|
|
1060
1506
|
#onMouseWheel(event, pickInfo) {
|
|
1507
|
+
event.preventDefault();
|
|
1061
1508
|
const camera = this.#scene?.activeCamera;
|
|
1062
1509
|
if (!camera) {
|
|
1063
1510
|
return false;
|
|
1064
1511
|
}
|
|
1065
|
-
if (!camera.metadata?.locked) {
|
|
1512
|
+
if (!camera.metadata?.locked) {
|
|
1066
1513
|
if (camera instanceof ArcRotateCamera) {
|
|
1067
1514
|
camera.wheelPrecision = camera.wheelPrecision || 3.0;
|
|
1068
1515
|
camera.inertialRadiusOffset -= event.deltaY * camera.wheelPrecision * 0.001;
|
|
@@ -1075,8 +1522,8 @@ export default class BabylonJSController {
|
|
|
1075
1522
|
const movementVector = direction.scale(zoomSpeed);
|
|
1076
1523
|
camera.position = camera.position.add(movementVector);
|
|
1077
1524
|
}
|
|
1525
|
+
this.#requestRender({ frames: 1, continuousMs: this.#config.render.interactionMs });
|
|
1078
1526
|
}
|
|
1079
|
-
event.preventDefault();
|
|
1080
1527
|
}
|
|
1081
1528
|
|
|
1082
1529
|
/**
|
|
@@ -1088,7 +1535,9 @@ export default class BabylonJSController {
|
|
|
1088
1535
|
*/
|
|
1089
1536
|
#onPointerUp(event, pickInfo) {
|
|
1090
1537
|
if (this.#babylonJSAnimationController) {
|
|
1091
|
-
|
|
1538
|
+
if (event.button !== 2 || !pickInfo || !pickInfo.pickedMesh) {
|
|
1539
|
+
this.#babylonJSAnimationController.hideMenu();
|
|
1540
|
+
}
|
|
1092
1541
|
// Right click for showing animation menu
|
|
1093
1542
|
if (event.button === 2) {
|
|
1094
1543
|
this.#babylonJSAnimationController.showMenu(pickInfo);
|
|
@@ -1100,13 +1549,105 @@ export default class BabylonJSController {
|
|
|
1100
1549
|
* Handles pointer move events on the Babylon.js scene.
|
|
1101
1550
|
* @private
|
|
1102
1551
|
* @param {PointerEvent} event - The pointer move event.
|
|
1103
|
-
* @param {PickInfo} pickInfo - The result of the scene pick operation.
|
|
1552
|
+
* @param {PickInfo|null} pickInfo - The sampled result of the scene pick operation (may be null when sampling skips a raycast).
|
|
1104
1553
|
* @returns {void}
|
|
1105
1554
|
*/
|
|
1106
1555
|
#onPointerMove(event, pickInfo) {
|
|
1107
|
-
|
|
1108
|
-
|
|
1556
|
+
const camera = this.#scene?.activeCamera;
|
|
1557
|
+
if (camera && !camera.metadata?.locked) {
|
|
1558
|
+
this.#requestRender({ frames: 1, continuousMs: this.#config.render.interactionMs });
|
|
1559
|
+
}
|
|
1560
|
+
if (this.#babylonJSAnimationController && pickInfo) {
|
|
1561
|
+
const pickedMeshId = pickInfo?.pickedMesh?.id || null;
|
|
1562
|
+
if (this.#state.pointerPicking.lastPickedMeshId !== pickedMeshId) {
|
|
1563
|
+
const highlightResult = this.#babylonJSAnimationController.highlightMeshes(pickInfo);
|
|
1564
|
+
if (highlightResult.changed) {
|
|
1565
|
+
this.#requestRender({ frames: 1, continuousMs: this.#config.render.interactionMs });
|
|
1566
|
+
}
|
|
1567
|
+
}
|
|
1568
|
+
}
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1571
|
+
/**
|
|
1572
|
+
* Handles pointer events observed on the Babylon.js scene.
|
|
1573
|
+
* Uses pointer-move sampling to avoid running expensive `scene.pick()` on every event.
|
|
1574
|
+
* @private
|
|
1575
|
+
* @param {PointerInfo} info - The pointer event information from Babylon.js.
|
|
1576
|
+
* @returns {void}
|
|
1577
|
+
*/
|
|
1578
|
+
#onPointerObservable(info) {
|
|
1579
|
+
if (info.type === PointerEventTypes.POINTERMOVE) {
|
|
1580
|
+
let pickInfo = null;
|
|
1581
|
+
let lastPickedMeshId = null;
|
|
1582
|
+
if (this.#shouldPickOnPointerMove()) {
|
|
1583
|
+
pickInfo = this.#scene.pick(this.#scene.pointerX, this.#scene.pointerY);
|
|
1584
|
+
lastPickedMeshId = pickInfo?.pickedMesh?.id || null;
|
|
1585
|
+
}
|
|
1586
|
+
this.#onPointerMove(info.event, pickInfo);
|
|
1587
|
+
this.#state.pointerPicking.lastPickedMeshId = lastPickedMeshId;
|
|
1588
|
+
} else if (info.type === PointerEventTypes.POINTERUP) {
|
|
1589
|
+
const pickInfo = info.event?.button === 2 ? this.#scene.pick(this.#scene.pointerX, this.#scene.pointerY) : null;
|
|
1590
|
+
if (pickInfo) {
|
|
1591
|
+
this.#state.pointerPicking.lastPickedMeshId = pickInfo?.pickedMesh?.id || null;
|
|
1592
|
+
}
|
|
1593
|
+
this.#onPointerUp(info.event, pickInfo);
|
|
1594
|
+
} else if (info.type === PointerEventTypes.POINTERWHEEL) {
|
|
1595
|
+
this.#onMouseWheel(info.event, null);
|
|
1596
|
+
}
|
|
1597
|
+
}
|
|
1598
|
+
|
|
1599
|
+
/**
|
|
1600
|
+
* Handles canvas resize notifications.
|
|
1601
|
+
* Resizes the Babylon engine and requests a short on-demand render burst so camera-dependent
|
|
1602
|
+
* buffers and post-process pipelines are redrawn at the new viewport size.
|
|
1603
|
+
* @private
|
|
1604
|
+
* @returns {void}
|
|
1605
|
+
*/
|
|
1606
|
+
#onResize() {
|
|
1607
|
+
if (!this.#engine) {
|
|
1608
|
+
return;
|
|
1609
|
+
}
|
|
1610
|
+
const now = typeof performance !== "undefined" && performance.now ? performance.now() : Date.now();
|
|
1611
|
+
const elapsed = now - this.#state.resize.lastAppliedAt;
|
|
1612
|
+
const applyResize = () => {
|
|
1613
|
+
this.#state.resize.timeoutId = null;
|
|
1614
|
+
this.#state.resize.isScheduled = false;
|
|
1615
|
+
if (!this.#engine) {
|
|
1616
|
+
return;
|
|
1617
|
+
}
|
|
1618
|
+
this.#state.resize.lastAppliedAt = typeof performance !== "undefined" && performance.now ? performance.now() : Date.now();
|
|
1619
|
+
console.log(`PrefViewer: Applying resize after ${Math.round(elapsed)}ms`);
|
|
1620
|
+
this.#engine.resize();
|
|
1621
|
+
this.#requestRender({ frames: this.#config.render.burstFramesBase, continuousMs: this.#config.render.interactionMs });
|
|
1622
|
+
};
|
|
1623
|
+
|
|
1624
|
+
if (elapsed >= this.#config.resize.throttleMs) {
|
|
1625
|
+
applyResize();
|
|
1626
|
+
return;
|
|
1627
|
+
}
|
|
1628
|
+
|
|
1629
|
+
if (this.#state.resize.isScheduled) {
|
|
1630
|
+
return;
|
|
1631
|
+
}
|
|
1632
|
+
|
|
1633
|
+
this.#state.resize.isScheduled = true;
|
|
1634
|
+
const waitMs = Math.max(0, this.#config.resize.throttleMs - elapsed);
|
|
1635
|
+
this.#state.resize.timeoutId = setTimeout(() => {
|
|
1636
|
+
applyResize();
|
|
1637
|
+
}, waitMs);
|
|
1638
|
+
}
|
|
1639
|
+
|
|
1640
|
+
/**
|
|
1641
|
+
* Clears any queued throttled resize callback.
|
|
1642
|
+
* @private
|
|
1643
|
+
* @returns {void}
|
|
1644
|
+
*/
|
|
1645
|
+
#cancelScheduledResize() {
|
|
1646
|
+
if (this.#state.resize.timeoutId !== null) {
|
|
1647
|
+
clearTimeout(this.#state.resize.timeoutId);
|
|
1109
1648
|
}
|
|
1649
|
+
this.#state.resize.timeoutId = null;
|
|
1650
|
+
this.#state.resize.isScheduled = false;
|
|
1110
1651
|
}
|
|
1111
1652
|
|
|
1112
1653
|
/**
|
|
@@ -1148,7 +1689,7 @@ export default class BabylonJSController {
|
|
|
1148
1689
|
.forEach((mesh) => {
|
|
1149
1690
|
mesh.material = material;
|
|
1150
1691
|
someSetted = true;
|
|
1151
|
-
})
|
|
1692
|
+
}),
|
|
1152
1693
|
);
|
|
1153
1694
|
|
|
1154
1695
|
if (someSetted) {
|
|
@@ -1243,15 +1784,12 @@ export default class BabylonJSController {
|
|
|
1243
1784
|
* Marks the IBL state as successful, recreates lights so the new environment takes effect, and reports whether anything changed.
|
|
1244
1785
|
* @private
|
|
1245
1786
|
* @returns {boolean} True when lights were refreshed due to pending IBL changes, otherwise false.
|
|
1787
|
+
* @description
|
|
1788
|
+
* Delegates to `#createLights()`, which executes the full IBL URL-to-texture lifecycle:
|
|
1789
|
+
* `cachedUrl -> HDRCubeTexture -> #hdrTexture clone -> consumeCachedUrl(true)`.
|
|
1246
1790
|
*/
|
|
1247
|
-
#setOptions_IBL() {
|
|
1248
|
-
|
|
1249
|
-
this.#options.ibl.setSuccess(true);
|
|
1250
|
-
this.#createLights();
|
|
1251
|
-
return true;
|
|
1252
|
-
}
|
|
1253
|
-
this.#createLights();
|
|
1254
|
-
return false;
|
|
1791
|
+
async #setOptions_IBL() {
|
|
1792
|
+
return await this.#createLights();
|
|
1255
1793
|
}
|
|
1256
1794
|
|
|
1257
1795
|
/**
|
|
@@ -1265,34 +1803,47 @@ export default class BabylonJSController {
|
|
|
1265
1803
|
}
|
|
1266
1804
|
|
|
1267
1805
|
/**
|
|
1268
|
-
*
|
|
1806
|
+
* Resolves and caches the closest `pref-viewer-3d` host associated with the rendering canvas.
|
|
1269
1807
|
* @private
|
|
1270
1808
|
* @returns {void}
|
|
1271
1809
|
*/
|
|
1272
1810
|
#getPrefViewer3DComponent() {
|
|
1273
|
-
if (this.#prefViewer3D
|
|
1274
|
-
|
|
1275
|
-
this.#prefViewer3D = grandParentElement && grandParentElement.nodeName === "PREF-VIEWER-3D" ? grandParentElement : null;
|
|
1811
|
+
if (this.#prefViewer3D !== undefined) {
|
|
1812
|
+
return;
|
|
1276
1813
|
}
|
|
1814
|
+
|
|
1815
|
+
let prefViewer3D = this.#canvas?.closest?.("pref-viewer-3d") || undefined;
|
|
1816
|
+
if (!prefViewer3D) {
|
|
1817
|
+
const host = this.#canvas?.getRootNode?.()?.host;
|
|
1818
|
+
prefViewer3D = host?.nodeName === "PREF-VIEWER-3D" ? host : undefined;
|
|
1819
|
+
}
|
|
1820
|
+
|
|
1821
|
+
this.#prefViewer3D = prefViewer3D;
|
|
1277
1822
|
}
|
|
1278
1823
|
|
|
1279
1824
|
/**
|
|
1280
|
-
*
|
|
1825
|
+
* Resolves and caches the closest `pref-viewer` host associated with `#prefViewer3D`.
|
|
1281
1826
|
* @private
|
|
1282
1827
|
* @returns {void}
|
|
1283
1828
|
*/
|
|
1284
1829
|
#getPrefViewerComponent() {
|
|
1285
|
-
if (this.#prefViewer
|
|
1286
|
-
|
|
1287
|
-
this.#getPrefViewer3DComponent();
|
|
1288
|
-
}
|
|
1289
|
-
if (!this.#prefViewer3D) {
|
|
1290
|
-
this.#prefViewer = null;
|
|
1291
|
-
return;
|
|
1292
|
-
}
|
|
1293
|
-
const rootNode = this.#prefViewer3D ? this.#prefViewer3D.getRootNode().host : null;
|
|
1294
|
-
this.#prefViewer = rootNode && rootNode.nodeName === "PREF-VIEWER" ? rootNode : null;
|
|
1830
|
+
if (this.#prefViewer !== undefined) {
|
|
1831
|
+
return;
|
|
1295
1832
|
}
|
|
1833
|
+
|
|
1834
|
+
this.#getPrefViewer3DComponent();
|
|
1835
|
+
if (this.#prefViewer3D === undefined) {
|
|
1836
|
+
this.#prefViewer = undefined;
|
|
1837
|
+
return;
|
|
1838
|
+
}
|
|
1839
|
+
|
|
1840
|
+
let prefViewer = this.#prefViewer3D.closest?.("pref-viewer") || undefined;
|
|
1841
|
+
if (!prefViewer) {
|
|
1842
|
+
const host = this.#prefViewer3D.getRootNode?.()?.host;
|
|
1843
|
+
prefViewer = host?.nodeName === "PREF-VIEWER" ? host : undefined;
|
|
1844
|
+
}
|
|
1845
|
+
|
|
1846
|
+
this.#prefViewer = prefViewer;
|
|
1296
1847
|
}
|
|
1297
1848
|
|
|
1298
1849
|
/**
|
|
@@ -1373,6 +1924,52 @@ export default class BabylonJSController {
|
|
|
1373
1924
|
return this.#addContainer(container, false);
|
|
1374
1925
|
}
|
|
1375
1926
|
|
|
1927
|
+
/**
|
|
1928
|
+
* Stops every animation group on the provided asset container to guarantee new loads start from a clean state.
|
|
1929
|
+
* @private
|
|
1930
|
+
* @param {AssetContainer} assetContainer - Container whose animation groups should be halted.
|
|
1931
|
+
*/
|
|
1932
|
+
#assetContainer_stopAnimations(assetContainer) {
|
|
1933
|
+
if (!assetContainer.animationGroups || assetContainer.animationGroups.length === 0) {
|
|
1934
|
+
return;
|
|
1935
|
+
}
|
|
1936
|
+
assetContainer.animationGroups.forEach((animationGroup) => {
|
|
1937
|
+
animationGroup.stop();
|
|
1938
|
+
});
|
|
1939
|
+
}
|
|
1940
|
+
|
|
1941
|
+
/**
|
|
1942
|
+
* Disposes every imported light so subsequent reloads avoid duplicating scene illumination.
|
|
1943
|
+
* @private
|
|
1944
|
+
* @param {AssetContainer} assetContainer - Container whose lights should be cleaned up.
|
|
1945
|
+
* @returns {void}
|
|
1946
|
+
*/
|
|
1947
|
+
#assetContainer_deleteLights(assetContainer) {
|
|
1948
|
+
if (!assetContainer.lights || assetContainer.lights.length === 0) {
|
|
1949
|
+
return;
|
|
1950
|
+
}
|
|
1951
|
+
assetContainer.lights.forEach((light) => {
|
|
1952
|
+
light.dispose();
|
|
1953
|
+
});
|
|
1954
|
+
assetContainer.lights = [];
|
|
1955
|
+
}
|
|
1956
|
+
|
|
1957
|
+
/**
|
|
1958
|
+
* Assigns unique ids to every imported camera so Babylon.js does not reuse stale SSAO effects between reloads.
|
|
1959
|
+
* @private
|
|
1960
|
+
* @param {AssetContainer} assetContainer - Container whose cameras need deterministic id regeneration.
|
|
1961
|
+
* @returns {void}
|
|
1962
|
+
*/
|
|
1963
|
+
#assetContainer_retagCameras(assetContainer) {
|
|
1964
|
+
if (!assetContainer.cameras || assetContainer.cameras.length === 0) {
|
|
1965
|
+
return;
|
|
1966
|
+
}
|
|
1967
|
+
assetContainer.cameras.forEach((camera) => {
|
|
1968
|
+
const sufix = "_" + Date.now();
|
|
1969
|
+
camera.id = `${camera.id || camera.name || "camera"}${sufix}`;
|
|
1970
|
+
});
|
|
1971
|
+
}
|
|
1972
|
+
|
|
1376
1973
|
/**
|
|
1377
1974
|
* Sets the visibility of wall and floor meshes in the model container based on the provided value or environment visibility.
|
|
1378
1975
|
* @private
|
|
@@ -1394,10 +1991,12 @@ export default class BabylonJSController {
|
|
|
1394
1991
|
* @private
|
|
1395
1992
|
* @returns {void}
|
|
1396
1993
|
*/
|
|
1397
|
-
#stopRender() {
|
|
1398
|
-
this.#
|
|
1399
|
-
this.#
|
|
1994
|
+
async #stopRender() {
|
|
1995
|
+
this.#stopEngineRenderLoop();
|
|
1996
|
+
this.#resetRenderState();
|
|
1997
|
+
await this.#unloadCameraDependentEffects();
|
|
1400
1998
|
}
|
|
1999
|
+
|
|
1401
2000
|
/**
|
|
1402
2001
|
* Starts the Babylon.js render loop for the current scene.
|
|
1403
2002
|
* Waits until the scene is ready before beginning continuous rendering.
|
|
@@ -1405,21 +2004,29 @@ export default class BabylonJSController {
|
|
|
1405
2004
|
* @returns {Promise<void>}
|
|
1406
2005
|
*/
|
|
1407
2006
|
async #startRender() {
|
|
1408
|
-
this.#loadCameraDependentEffects();
|
|
2007
|
+
await this.#loadCameraDependentEffects();
|
|
1409
2008
|
await this.#scene.whenReadyAsync();
|
|
1410
|
-
this.#
|
|
2009
|
+
const frames = this.#settings.antiAliasingEnabled || this.#settings.ambientOcclusionEnabled || this.#settings.iblEnabled ? this.#config.render.burstFramesEnhanced : this.#config.render.burstFramesBase;
|
|
2010
|
+
this.#requestRender({ frames: frames, continuousMs: this.#config.render.interactionMs });
|
|
1411
2011
|
}
|
|
1412
2012
|
|
|
1413
2013
|
/**
|
|
1414
|
-
* Loads
|
|
2014
|
+
* Loads a single asset container (model, environment, materials, etc.) based on the container state flags.
|
|
2015
|
+
* Skips work when nothing is pending, otherwise resolves the GLTF source, refreshes cache metadata and streams it
|
|
2016
|
+
* into the Babylon.js scene via `LoadAssetContainerAsync`.
|
|
1415
2017
|
* @private
|
|
1416
|
-
* @param {object} container -
|
|
1417
|
-
* @
|
|
2018
|
+
* @param {object} container - Container descriptor that carries the GLTF storage pointer and current cache info.
|
|
2019
|
+
* @param {boolean} [force=false] - When true, bypasses cached size/timestamp so the resolver re-downloads the asset.
|
|
2020
|
+
* @returns {Promise<[object, AssetContainer|boolean]>} Resolves to `[container, assetContainer]` on success, or
|
|
2021
|
+
* `[container, false]` when loading was skipped or failed.
|
|
1418
2022
|
* @description
|
|
1419
|
-
*
|
|
1420
|
-
*
|
|
2023
|
+
* 1. Validates that the container has pending data and initializes the shared `GLTFResolver` if needed.
|
|
2024
|
+
* 2. Requests the source blob (respecting the cached size/timestamp unless `force` is set) and stores the new cache
|
|
2025
|
+
* metadata via `setPendingCacheData`.
|
|
2026
|
+
* 3. Builds the Babylon plugin options so extras are surfaced as metadata, then imports the container with
|
|
2027
|
+
* `LoadAssetContainerAsync`, returning the tuple so the caller can decide how to attach it to the scene.
|
|
1421
2028
|
*/
|
|
1422
|
-
async #loadAssetContainer(container) {
|
|
2029
|
+
async #loadAssetContainer(container, force = false) {
|
|
1423
2030
|
if (container?.state?.update?.storage === undefined || container?.state?.size === undefined || container?.state?.timeStamp === undefined) {
|
|
1424
2031
|
return [container, false];
|
|
1425
2032
|
}
|
|
@@ -1431,9 +2038,12 @@ export default class BabylonJSController {
|
|
|
1431
2038
|
if (!this.#gltfResolver) {
|
|
1432
2039
|
this.#gltfResolver = new GLTFResolver();
|
|
1433
2040
|
}
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
2041
|
+
|
|
2042
|
+
const currentSize = force ? 0 : container.state.size;
|
|
2043
|
+
const currentTimeStamp = force ? null : container.state.timeStamp;
|
|
2044
|
+
|
|
2045
|
+
let sourceData = await this.#gltfResolver.getSource(container.state.update.storage, currentSize, currentTimeStamp);
|
|
2046
|
+
|
|
1437
2047
|
if (!sourceData) {
|
|
1438
2048
|
return [container, false];
|
|
1439
2049
|
}
|
|
@@ -1462,6 +2072,8 @@ export default class BabylonJSController {
|
|
|
1462
2072
|
return [container, assetContainer];
|
|
1463
2073
|
} catch (error) {
|
|
1464
2074
|
return [container, assetContainer];
|
|
2075
|
+
} finally {
|
|
2076
|
+
this.#gltfResolver.revokeObjectURLs(sourceData.objectURLs);
|
|
1465
2077
|
}
|
|
1466
2078
|
}
|
|
1467
2079
|
|
|
@@ -1474,15 +2086,16 @@ export default class BabylonJSController {
|
|
|
1474
2086
|
* Applies material and camera options, sets wall/floor visibility, and initializes lights and shadows.
|
|
1475
2087
|
* Returns an object with success status and error details.
|
|
1476
2088
|
*/
|
|
1477
|
-
async #loadContainers() {
|
|
1478
|
-
this.#
|
|
2089
|
+
async #loadContainers(force = false) {
|
|
2090
|
+
this.#detachAnimationChangedListener();
|
|
2091
|
+
await this.#stopRender();
|
|
1479
2092
|
|
|
1480
2093
|
let oldModelMetadata = { ...(this.#containers.model?.state?.metadata ?? {}) };
|
|
1481
2094
|
let newModelMetadata = {};
|
|
1482
2095
|
|
|
1483
2096
|
const promiseArray = [];
|
|
1484
2097
|
Object.values(this.#containers).forEach((container) => {
|
|
1485
|
-
promiseArray.push(this.#loadAssetContainer(container));
|
|
2098
|
+
promiseArray.push(this.#loadAssetContainer(container, force));
|
|
1486
2099
|
});
|
|
1487
2100
|
|
|
1488
2101
|
let detail = {
|
|
@@ -1491,22 +2104,19 @@ export default class BabylonJSController {
|
|
|
1491
2104
|
};
|
|
1492
2105
|
|
|
1493
2106
|
await Promise.allSettled(promiseArray)
|
|
1494
|
-
.then((values) => {
|
|
2107
|
+
.then(async (values) => {
|
|
1495
2108
|
this.#disposeAnimationController();
|
|
1496
2109
|
values.forEach((result) => {
|
|
1497
2110
|
const container = result.value ? result.value[0] : null;
|
|
1498
2111
|
const assetContainer = result.value ? result.value[1] : null;
|
|
1499
2112
|
if (result.status === "fulfilled" && assetContainer) {
|
|
1500
2113
|
if (container.state.name === "model") {
|
|
1501
|
-
assetContainer
|
|
2114
|
+
this.#assetContainer_deleteLights(assetContainer);
|
|
2115
|
+
this.#assetContainer_stopAnimations(assetContainer);
|
|
1502
2116
|
newModelMetadata = { ...(container.state.update.metadata ?? {}) };
|
|
1503
2117
|
}
|
|
1504
2118
|
if (container.state.name === "model" || container.state.name === "environment") {
|
|
1505
|
-
assetContainer
|
|
1506
|
-
// To avoid conflicts when reloading the model we rename the id because Babylon.js caches the camera's SSAO effect by id.
|
|
1507
|
-
const sufix = "_" + Date.now();
|
|
1508
|
-
camera.id = `${camera.id || camera.name || "camera"}${sufix}`;
|
|
1509
|
-
});
|
|
2119
|
+
this.#assetContainer_retagCameras(assetContainer);
|
|
1510
2120
|
}
|
|
1511
2121
|
this.#replaceContainer(container, assetContainer);
|
|
1512
2122
|
container.state.setSuccess(true);
|
|
@@ -1520,7 +2130,7 @@ export default class BabylonJSController {
|
|
|
1520
2130
|
|
|
1521
2131
|
this.#setOptions_Materials();
|
|
1522
2132
|
this.#setOptions_Camera();
|
|
1523
|
-
this.#setOptions_IBL();
|
|
2133
|
+
await this.#setOptions_IBL();
|
|
1524
2134
|
this.#setVisibilityOfWallAndFloorInModel();
|
|
1525
2135
|
detail.success = true;
|
|
1526
2136
|
})
|
|
@@ -1534,7 +2144,10 @@ export default class BabylonJSController {
|
|
|
1534
2144
|
this.#checkModelMetadata(oldModelMetadata, newModelMetadata);
|
|
1535
2145
|
this.#setMaxSimultaneousLights();
|
|
1536
2146
|
this.#babylonJSAnimationController = new BabylonJSAnimationController(this.#containers.model.assetContainer);
|
|
1537
|
-
this.#
|
|
2147
|
+
if (this.#babylonJSAnimationController?.hasAnimations?.()) {
|
|
2148
|
+
this.#attachAnimationChangedListener();
|
|
2149
|
+
}
|
|
2150
|
+
await this.#startRender();
|
|
1538
2151
|
});
|
|
1539
2152
|
return detail;
|
|
1540
2153
|
}
|
|
@@ -1545,10 +2158,10 @@ export default class BabylonJSController {
|
|
|
1545
2158
|
* @private
|
|
1546
2159
|
* @returns {void}
|
|
1547
2160
|
*/
|
|
1548
|
-
#loadCameraDependentEffects() {
|
|
1549
|
-
this.#initializeVisualImprovements();
|
|
1550
|
-
this.#initializeAmbientOcclussion();
|
|
1551
|
-
this.#initializeShadows();
|
|
2161
|
+
async #loadCameraDependentEffects() {
|
|
2162
|
+
await this.#initializeVisualImprovements();
|
|
2163
|
+
await this.#initializeAmbientOcclussion();
|
|
2164
|
+
await this.#initializeShadows();
|
|
1552
2165
|
}
|
|
1553
2166
|
|
|
1554
2167
|
/**
|
|
@@ -1557,10 +2170,10 @@ export default class BabylonJSController {
|
|
|
1557
2170
|
* @private
|
|
1558
2171
|
* @returns {void}
|
|
1559
2172
|
*/
|
|
1560
|
-
#unloadCameraDependentEffects() {
|
|
1561
|
-
this.#disableVisualImprovements();
|
|
1562
|
-
this.#disableAmbientOcclusion();
|
|
1563
|
-
this.#disableShadows();
|
|
2173
|
+
async #unloadCameraDependentEffects() {
|
|
2174
|
+
await this.#disableVisualImprovements();
|
|
2175
|
+
await this.#disableAmbientOcclusion();
|
|
2176
|
+
await this.#disableShadows();
|
|
1564
2177
|
}
|
|
1565
2178
|
|
|
1566
2179
|
/**
|
|
@@ -1793,10 +2406,10 @@ export default class BabylonJSController {
|
|
|
1793
2406
|
this.#engine = new Engine(this.#canvas, true, { alpha: true, stencil: true, preserveDrawingBuffer: false });
|
|
1794
2407
|
this.#engine.disableUniformBuffers = true;
|
|
1795
2408
|
this.#scene = new Scene(this.#engine);
|
|
1796
|
-
|
|
2409
|
+
|
|
1797
2410
|
// Activate the rendering of geometry data into a G-buffer, essential for advanced effects like deferred shading,
|
|
1798
|
-
// SSAO, and Velocity-Texture-Animation (VAT), allowing for complex post-processing by separating rendering into
|
|
1799
|
-
// different buffers (depth, normals, velocity) for later use in shaders.
|
|
2411
|
+
// SSAO, and Velocity-Texture-Animation (VAT), allowing for complex post-processing by separating rendering into
|
|
2412
|
+
// different buffers (depth, normals, velocity) for later use in shaders.
|
|
1800
2413
|
const geometryBufferRenderer = this.#scene.enableGeometryBufferRenderer();
|
|
1801
2414
|
if (geometryBufferRenderer) {
|
|
1802
2415
|
geometryBufferRenderer.enableScreenspaceDepth = true;
|
|
@@ -1804,28 +2417,62 @@ export default class BabylonJSController {
|
|
|
1804
2417
|
geometryBufferRenderer.generateNormalsInWorldSpace = true;
|
|
1805
2418
|
}
|
|
1806
2419
|
|
|
1807
|
-
this.#scene.clearColor = new Color4(1, 1, 1, 1);
|
|
2420
|
+
this.#scene.clearColor = new Color4(1, 1, 1, 1).toLinearSpace();
|
|
2421
|
+
|
|
2422
|
+
// Lowered exposure to prevent scenes from looking blown out when the DefaultRenderingPipeline (Antialiasing) is enabled.
|
|
2423
|
+
this.#scene.imageProcessingConfiguration.exposure = 0.75;
|
|
2424
|
+
this.#scene.imageProcessingConfiguration.contrast = 1.0;
|
|
2425
|
+
this.#scene.imageProcessingConfiguration.toneMappingEnabled = false;
|
|
2426
|
+
this.#scene.imageProcessingConfiguration.vignetteEnabled = false;
|
|
2427
|
+
this.#scene.imageProcessingConfiguration.colorCurvesEnabled = false;
|
|
2428
|
+
|
|
2429
|
+
// Skip the built-in pointer picking logic since the controller implements its own optimized raycasting for interaction.
|
|
2430
|
+
this.#scene.skipPointerMovePicking = true;
|
|
2431
|
+
this.#scene.skipPointerDownPicking = true;
|
|
2432
|
+
this.#scene.skipPointerUpPicking = true;
|
|
2433
|
+
|
|
1808
2434
|
this.#createCamera();
|
|
1809
2435
|
this.#enableInteraction();
|
|
1810
2436
|
await this.#createXRExperience();
|
|
1811
|
-
this.#startRender();
|
|
1812
|
-
this.#canvasResizeObserver = new ResizeObserver(() => this.#engine && this.#engine.resize());
|
|
1813
|
-
this.#canvasResizeObserver.observe(this.#canvas);
|
|
1814
2437
|
}
|
|
1815
2438
|
|
|
1816
2439
|
/**
|
|
1817
2440
|
* Disposes the Babylon.js engine and disconnects the canvas resize observer.
|
|
1818
|
-
* Cleans up all scene, camera, light, and
|
|
2441
|
+
* Cleans up all scene, camera, light, XR, and GLTF resolver resources.
|
|
2442
|
+
* The teardown is asynchronous: it waits for XR/session-dependent shutdown work
|
|
2443
|
+
* before disposing the engine, and coalesces concurrent calls into one in-flight promise.
|
|
1819
2444
|
* @public
|
|
1820
|
-
* @returns {void}
|
|
2445
|
+
* @returns {Promise<void>}
|
|
1821
2446
|
*/
|
|
1822
|
-
disable() {
|
|
1823
|
-
this.#
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
this.#
|
|
1828
|
-
|
|
2447
|
+
async disable() {
|
|
2448
|
+
if (this.#disablingPromises.general) {
|
|
2449
|
+
return await this.#disablingPromises.general;
|
|
2450
|
+
}
|
|
2451
|
+
|
|
2452
|
+
this.#disablingPromises.general = (async () => {
|
|
2453
|
+
this.#disableInteraction();
|
|
2454
|
+
this.#disposeAnimationController();
|
|
2455
|
+
this.#disposeGLTFResolver();
|
|
2456
|
+
try {
|
|
2457
|
+
await this.#disposeXRExperience();
|
|
2458
|
+
} catch (error) {
|
|
2459
|
+
console.warn("PrefViewer: Error while disposing XR experience:", error);
|
|
2460
|
+
}
|
|
2461
|
+
try {
|
|
2462
|
+
await this.#unloadCameraDependentEffects();
|
|
2463
|
+
} catch (error) {
|
|
2464
|
+
console.warn("PrefViewer: Error while unloading camera-dependent effects:", error);
|
|
2465
|
+
} finally {
|
|
2466
|
+
this.#stopEngineRenderLoop();
|
|
2467
|
+
this.#disposeEngine();
|
|
2468
|
+
}
|
|
2469
|
+
})();
|
|
2470
|
+
|
|
2471
|
+
try {
|
|
2472
|
+
await this.#disablingPromises.general;
|
|
2473
|
+
} finally {
|
|
2474
|
+
this.#disablingPromises.general = null;
|
|
2475
|
+
}
|
|
1829
2476
|
}
|
|
1830
2477
|
|
|
1831
2478
|
/**
|
|
@@ -1946,28 +2593,8 @@ export default class BabylonJSController {
|
|
|
1946
2593
|
* @public
|
|
1947
2594
|
* @returns {Promise<boolean>} Resolves to true if loading succeeds, false otherwise.
|
|
1948
2595
|
*/
|
|
1949
|
-
async load() {
|
|
1950
|
-
return await this.#loadContainers();
|
|
1951
|
-
}
|
|
1952
|
-
|
|
1953
|
-
/**
|
|
1954
|
-
* Merges incoming render flags with the current configuration, persists them, and marks
|
|
1955
|
-
* all dependent loaders/options as pending when something actually changed.
|
|
1956
|
-
* @public
|
|
1957
|
-
* @param {{antiAliasingEnabled?:boolean, ambientOcclusionEnabled?:boolean, iblEnabled?:boolean, shadowsEnabled?:boolean}} settings Partial set of render toggles to apply.
|
|
1958
|
-
* @returns {{changed:boolean, settings:{antiAliasingEnabled:boolean, ambientOcclusionEnabled:boolean, iblEnabled:boolean, shadowsEnabled:boolean}}}
|
|
1959
|
-
* @description
|
|
1960
|
-
* Callers can inspect the `changed` flag to decide whether to trigger a reload with
|
|
1961
|
-
* `reloadWithCurrentSettings()` or simply reuse the returned snapshot.
|
|
1962
|
-
*/
|
|
1963
|
-
scheduleRenderSettingsReload(settings = {}) {
|
|
1964
|
-
const changed = this.#applyRenderSettings(settings);
|
|
1965
|
-
if (!changed) {
|
|
1966
|
-
return { changed: false, settings: this.getRenderSettings() };
|
|
1967
|
-
}
|
|
1968
|
-
this.#markContainersForReload();
|
|
1969
|
-
this.#markOptionsForReload();
|
|
1970
|
-
return { changed: true, settings: this.getRenderSettings() };
|
|
2596
|
+
async load(force = false) {
|
|
2597
|
+
return await this.#loadContainers(force);
|
|
1971
2598
|
}
|
|
1972
2599
|
|
|
1973
2600
|
/**
|
|
@@ -2000,13 +2627,15 @@ export default class BabylonJSController {
|
|
|
2000
2627
|
* Reapplies image-based lighting configuration (HDR URL, intensity, shadow mode).
|
|
2001
2628
|
* Stops rendering, pushes pending IBL state into the scene, rebuilds camera-dependent effects, then resumes rendering.
|
|
2002
2629
|
* @public
|
|
2003
|
-
* @returns {
|
|
2630
|
+
* @returns {boolean} True if IBL options were set successfully, false otherwise.
|
|
2004
2631
|
*/
|
|
2005
|
-
setIBLOptions() {
|
|
2006
|
-
this.#stopRender();
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2632
|
+
async setIBLOptions() {
|
|
2633
|
+
await this.#stopRender();
|
|
2634
|
+
try {
|
|
2635
|
+
return await this.#setOptions_IBL();
|
|
2636
|
+
} finally {
|
|
2637
|
+
await this.#startRender();
|
|
2638
|
+
}
|
|
2010
2639
|
}
|
|
2011
2640
|
|
|
2012
2641
|
/**
|
|
@@ -2033,6 +2662,26 @@ export default class BabylonJSController {
|
|
|
2033
2662
|
this.#startRender();
|
|
2034
2663
|
}
|
|
2035
2664
|
|
|
2665
|
+
/**
|
|
2666
|
+
* Merges incoming render flags with the current configuration, persists them, and marks
|
|
2667
|
+
* all dependent loaders/options as pending when something actually changed.
|
|
2668
|
+
* @public
|
|
2669
|
+
* @param {{antiAliasingEnabled?:boolean, ambientOcclusionEnabled?:boolean, iblEnabled?:boolean, shadowsEnabled?:boolean}} settings Partial set of render toggles to apply.
|
|
2670
|
+
* @returns {{changed:boolean, settings:{antiAliasingEnabled:boolean, ambientOcclusionEnabled:boolean, iblEnabled:boolean, shadowsEnabled:boolean}}}
|
|
2671
|
+
* @description
|
|
2672
|
+
* Callers can inspect the `changed` flag to decide whether to trigger a reload with
|
|
2673
|
+
* `reloadWithCurrentSettings()` or simply reuse the returned snapshot.
|
|
2674
|
+
*/
|
|
2675
|
+
scheduleRenderSettingsReload(settings = {}) {
|
|
2676
|
+
const changed = this.#saveRenderSettings(settings);
|
|
2677
|
+
if (!changed) {
|
|
2678
|
+
return { changed: false, settings: this.getRenderSettings() };
|
|
2679
|
+
}
|
|
2680
|
+
this.#markContainersForReload();
|
|
2681
|
+
this.#markOptionsForReload();
|
|
2682
|
+
return { changed: true, settings: this.getRenderSettings() };
|
|
2683
|
+
}
|
|
2684
|
+
|
|
2036
2685
|
/**
|
|
2037
2686
|
* Reloads every asset container using the latest staged render settings.
|
|
2038
2687
|
* Intended to be called after `scheduleRenderSettingsReload()` marks data as pending.
|