@preference-sl/pref-viewer 2.13.0-beta.6 → 2.13.0-beta.8
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/src/babylonjs-animation-controller.js +154 -63
- package/src/babylonjs-animation-opening.js +58 -2
- package/src/babylonjs-controller.js +468 -114
- package/src/file-storage.js +382 -21
- package/src/gltf-resolver.js +52 -8
- package/src/pref-viewer-3d-data.js +60 -2
- package/src/pref-viewer-3d.js +11 -4
|
@@ -8,6 +8,7 @@ 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
|
/**
|
|
@@ -56,11 +57,13 @@ import { translate } from "./localization/i18n.js";
|
|
|
56
57
|
* - `show-model`/`show-scene` DOM attributes reflect container visibility; there are no direct `showModel()/hideModel()` APIs.
|
|
57
58
|
* - IBL shadows require `iblEnabled` plus `options.ibl.shadows` and a loaded HDR texture; otherwise fallback directional
|
|
58
59
|
* lights and environment-contributed lights supply classic shadow generators.
|
|
60
|
+
* - IBL lifecycle: when `options.ibl.cachedUrl` is present, a new `HDRCubeTexture` is created and cloned into `#hdrTexture`;
|
|
61
|
+
* then `options.ibl.consumeCachedUrl(true)` clears/revokes the temporary URL. Subsequent reloads reuse `#hdrTexture.clone()`
|
|
62
|
+
* while `options.ibl.valid` remains true.
|
|
59
63
|
* - Browser-only features guard `window`, localStorage, and XR APIs before use so the controller is safe to construct
|
|
60
64
|
* in SSR/Node contexts (though functionality activates only in browsers).
|
|
61
65
|
*/
|
|
62
66
|
export default class BabylonJSController {
|
|
63
|
-
|
|
64
67
|
#RENDER_SETTINGS_STORAGE_KEY = "pref-viewer/render-settings";
|
|
65
68
|
|
|
66
69
|
// Default render settings
|
|
@@ -72,7 +75,7 @@ export default class BabylonJSController {
|
|
|
72
75
|
iblEnabled: true,
|
|
73
76
|
shadowsEnabled: false,
|
|
74
77
|
};
|
|
75
|
-
|
|
78
|
+
|
|
76
79
|
// Canvas HTML element
|
|
77
80
|
#canvas = null;
|
|
78
81
|
|
|
@@ -90,10 +93,13 @@ export default class BabylonJSController {
|
|
|
90
93
|
#shadowGen = [];
|
|
91
94
|
#XRExperience = null;
|
|
92
95
|
#canvasResizeObserver = null;
|
|
93
|
-
|
|
96
|
+
|
|
97
|
+
#hdrTexture = null; // reusable in-memory HDR source cloned into scene.environmentTexture across reloads
|
|
98
|
+
#lastPickedMeshId = null;
|
|
99
|
+
|
|
94
100
|
#containers = {};
|
|
95
101
|
#options = {};
|
|
96
|
-
|
|
102
|
+
|
|
97
103
|
#gltfResolver = null; // GLTFResolver instance
|
|
98
104
|
#babylonJSAnimationController = null; // AnimationController instance
|
|
99
105
|
|
|
@@ -106,11 +112,28 @@ export default class BabylonJSController {
|
|
|
106
112
|
#handlers = {
|
|
107
113
|
onKeyUp: null,
|
|
108
114
|
onPointerObservable: null,
|
|
115
|
+
onAnimationGroupChanged: null,
|
|
116
|
+
onResize: null,
|
|
109
117
|
renderLoop: null,
|
|
110
118
|
};
|
|
111
|
-
|
|
119
|
+
|
|
112
120
|
#settings = { ...BabylonJSController.DEFAULT_RENDER_SETTINGS };
|
|
113
121
|
|
|
122
|
+
#renderState = {
|
|
123
|
+
isLoopRunning: false,
|
|
124
|
+
dirtyFrames: 0,
|
|
125
|
+
continuousUntil: 0,
|
|
126
|
+
lastRenderAt: 0,
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
#renderConfig = {
|
|
130
|
+
burstFramesBase: 2,
|
|
131
|
+
burstFramesEnhanced: 32, // when AA/SSAO/IBL is enabled, more frames are needed to reach stable output
|
|
132
|
+
interactionMs: 250,
|
|
133
|
+
animationMs: 200,
|
|
134
|
+
idleThrottleMs: 1000 / 15,
|
|
135
|
+
};
|
|
136
|
+
|
|
114
137
|
/**
|
|
115
138
|
* Constructs a new BabylonJSController instance.
|
|
116
139
|
* Initializes the canvas, asset containers, and options for the Babylon.js scene.
|
|
@@ -143,8 +166,10 @@ export default class BabylonJSController {
|
|
|
143
166
|
* @returns {void}
|
|
144
167
|
*/
|
|
145
168
|
#bindHandlers() {
|
|
169
|
+
this.#handlers.onAnimationGroupChanged = this.#onAnimationGroupChanged.bind(this);
|
|
146
170
|
this.#handlers.onKeyUp = this.#onKeyUp.bind(this);
|
|
147
171
|
this.#handlers.onPointerObservable = this.#onPointerObservable.bind(this);
|
|
172
|
+
this.#handlers.onResize = this.#onResize.bind(this);
|
|
148
173
|
this.#handlers.renderLoop = this.#renderLoop.bind(this);
|
|
149
174
|
}
|
|
150
175
|
|
|
@@ -267,16 +292,177 @@ export default class BabylonJSController {
|
|
|
267
292
|
}
|
|
268
293
|
}
|
|
269
294
|
|
|
295
|
+
/**
|
|
296
|
+
* Starts Babylon's engine render loop if it is not already running.
|
|
297
|
+
* @private
|
|
298
|
+
* @returns {boolean} True when the loop was started, false when no engine is available or it was already running.
|
|
299
|
+
*/
|
|
300
|
+
#startEngineRenderLoop() {
|
|
301
|
+
if (!this.#engine || this.#renderState.isLoopRunning) {
|
|
302
|
+
return false;
|
|
303
|
+
}
|
|
304
|
+
this.#engine.runRenderLoop(this.#handlers.renderLoop);
|
|
305
|
+
this.#renderState.isLoopRunning = true;
|
|
306
|
+
return true;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Stops Babylon's engine render loop when it is currently active.
|
|
311
|
+
* @private
|
|
312
|
+
* @returns {boolean} True when the loop was stopped, false when no engine is available or it was already stopped.
|
|
313
|
+
*/
|
|
314
|
+
#stopEngineRenderLoop() {
|
|
315
|
+
if (!this.#engine || !this.#renderState.isLoopRunning) {
|
|
316
|
+
return false;
|
|
317
|
+
}
|
|
318
|
+
this.#engine.stopRenderLoop(this.#handlers.renderLoop);
|
|
319
|
+
this.#renderState.isLoopRunning = false;
|
|
320
|
+
return true;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Marks the scene as dirty and optionally extends a short continuous-render window.
|
|
325
|
+
* Ensures the engine loop is running so the requested frames can be produced.
|
|
326
|
+
* @private
|
|
327
|
+
* @param {{frames?:number, continuousMs?:number}} [options={}] - Render request options.
|
|
328
|
+
* @param {number} [options.frames=1] - Minimum number of frames to render.
|
|
329
|
+
* @param {number} [options.continuousMs=0] - Milliseconds to keep continuous rendering active.
|
|
330
|
+
* @returns {boolean} True when the request was accepted, false when scene/engine are unavailable.
|
|
331
|
+
*/
|
|
332
|
+
#requestRender({ frames = 1, continuousMs = 0 } = {}) {
|
|
333
|
+
if (!this.#scene || !this.#engine) {
|
|
334
|
+
return false;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const now = typeof performance !== "undefined" && performance.now ? performance.now() : Date.now();
|
|
338
|
+
this.#renderState.dirtyFrames = Math.max(this.#renderState.dirtyFrames, Math.max(1, frames));
|
|
339
|
+
if (continuousMs > 0) {
|
|
340
|
+
this.#renderState.continuousUntil = Math.max(this.#renderState.continuousUntil, now + continuousMs);
|
|
341
|
+
}
|
|
342
|
+
this.#startEngineRenderLoop();
|
|
343
|
+
return true;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Checks whether an ArcRotateCamera still has non-zero inertial movement.
|
|
348
|
+
* @private
|
|
349
|
+
* @param {ArcRotateCamera} camera - Camera to evaluate.
|
|
350
|
+
* @returns {boolean} True when any inertial offset is still active.
|
|
351
|
+
*/
|
|
352
|
+
#isArcRotateCameraInMotion(camera) {
|
|
353
|
+
const EPSILON = 0.00001;
|
|
354
|
+
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;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Checks whether a FreeCamera/UniversalCamera is currently moving or rotating.
|
|
359
|
+
* @private
|
|
360
|
+
* @param {FreeCamera|UniversalCamera} camera - Camera to evaluate.
|
|
361
|
+
* @returns {boolean} True when translation or rotation deltas are active.
|
|
362
|
+
*/
|
|
363
|
+
#isUniversalOrFreeCameraInMotion(camera) {
|
|
364
|
+
const EPSILON = 0.00001;
|
|
365
|
+
const direction = camera?.cameraDirection;
|
|
366
|
+
const rotation = camera?.cameraRotation;
|
|
367
|
+
const directionMoving = !!direction && (Math.abs(direction.x) > EPSILON || Math.abs(direction.y) > EPSILON || Math.abs(direction.z) > EPSILON);
|
|
368
|
+
const rotationMoving = !!rotation && (Math.abs(rotation.x) > EPSILON || Math.abs(rotation.y) > EPSILON || Math.abs(rotation.z) > EPSILON);
|
|
369
|
+
return directionMoving || rotationMoving;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Detects motion for the current active camera based on its concrete camera type.
|
|
374
|
+
* @private
|
|
375
|
+
* @returns {boolean} True when the active camera is moving, otherwise false.
|
|
376
|
+
*/
|
|
377
|
+
#isCameraInMotion() {
|
|
378
|
+
const camera = this.#scene?.activeCamera;
|
|
379
|
+
if (!camera) {
|
|
380
|
+
return false;
|
|
381
|
+
}
|
|
382
|
+
if (camera instanceof ArcRotateCamera) {
|
|
383
|
+
return this.#isArcRotateCameraInMotion(camera);
|
|
384
|
+
}
|
|
385
|
+
if (camera instanceof UniversalCamera || camera instanceof FreeCamera) {
|
|
386
|
+
return this.#isUniversalOrFreeCameraInMotion(camera);
|
|
387
|
+
}
|
|
388
|
+
return false;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Determines whether scene animations are currently running.
|
|
393
|
+
* @private
|
|
394
|
+
* @returns {boolean} True when at least one animation group is playing.
|
|
395
|
+
*/
|
|
396
|
+
#isAnimationRunning() {
|
|
397
|
+
if (!this.#scene) {
|
|
398
|
+
return false;
|
|
399
|
+
}
|
|
400
|
+
const hasAnimatables = (this.#scene.animatables?.length || 0) > 0;
|
|
401
|
+
if (!hasAnimatables) {
|
|
402
|
+
return false;
|
|
403
|
+
}
|
|
404
|
+
return this.#scene.animationGroups?.some((group) => group?.isPlaying) || false;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Evaluates whether the renderer should stay in continuous mode.
|
|
409
|
+
* XR always forces continuous rendering; animation/camera motion also extends the
|
|
410
|
+
* continuous deadline window to avoid abrupt stop-start behavior.
|
|
411
|
+
* @private
|
|
412
|
+
* @param {number} now - Current high-resolution timestamp.
|
|
413
|
+
* @returns {boolean} True when continuous rendering should remain active.
|
|
414
|
+
*/
|
|
415
|
+
#shouldRenderContinuously(now) {
|
|
416
|
+
const inXR = this.#XRExperience?.baseExperience?.state === WebXRState.IN_XR;
|
|
417
|
+
if (inXR) {
|
|
418
|
+
return true;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
const animationRunning = this.#isAnimationRunning();
|
|
422
|
+
const cameraInMotion = this.#isCameraInMotion();
|
|
423
|
+
|
|
424
|
+
if (animationRunning) {
|
|
425
|
+
this.#renderState.continuousUntil = Math.max(this.#renderState.continuousUntil, now + this.#renderConfig.animationMs);
|
|
426
|
+
}
|
|
427
|
+
if (cameraInMotion) {
|
|
428
|
+
this.#renderState.continuousUntil = Math.max(this.#renderState.continuousUntil, now + this.#renderConfig.interactionMs);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
return animationRunning || cameraInMotion || this.#renderState.continuousUntil > now;
|
|
432
|
+
}
|
|
433
|
+
|
|
270
434
|
/**
|
|
271
435
|
* Render loop callback for Babylon.js.
|
|
436
|
+
* Runs only while scene state is dirty, interactive motion is active, animations are running, or XR is active.
|
|
437
|
+
* It self-stops when the scene becomes idle.
|
|
272
438
|
* @private
|
|
273
439
|
* @returns {void}
|
|
274
|
-
* @description
|
|
275
|
-
* Continuously renders the current scene if it exists.
|
|
276
|
-
* Used by the engine's runRenderLoop method to update the view.
|
|
277
440
|
*/
|
|
278
441
|
#renderLoop() {
|
|
279
|
-
|
|
442
|
+
if (!this.#scene) {
|
|
443
|
+
this.#stopEngineRenderLoop();
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
const now = typeof performance !== "undefined" && performance.now ? performance.now() : Date.now();
|
|
448
|
+
const continuous = this.#shouldRenderContinuously(now);
|
|
449
|
+
const needsRender = continuous || this.#renderState.dirtyFrames > 0;
|
|
450
|
+
|
|
451
|
+
if (!needsRender) {
|
|
452
|
+
this.#stopEngineRenderLoop();
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
if (!continuous && this.#renderState.lastRenderAt > 0 && now - this.#renderState.lastRenderAt < this.#renderConfig.idleThrottleMs) {
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
this.#scene.render();
|
|
461
|
+
this.#renderState.lastRenderAt = now;
|
|
462
|
+
|
|
463
|
+
if (this.#renderState.dirtyFrames > 0) {
|
|
464
|
+
this.#renderState.dirtyFrames -= 1;
|
|
465
|
+
}
|
|
280
466
|
}
|
|
281
467
|
|
|
282
468
|
/**
|
|
@@ -376,6 +562,10 @@ export default class BabylonJSController {
|
|
|
376
562
|
* Sets light intensities and shadow properties for realistic rendering.
|
|
377
563
|
* @private
|
|
378
564
|
* @returns {Promise<boolean>} Returns true if lights were changed, false otherwise.
|
|
565
|
+
* @description
|
|
566
|
+
* IBL path is considered available when either:
|
|
567
|
+
* - `options.ibl.cachedUrl` is present (new pending URL), or
|
|
568
|
+
* - `options.ibl.valid === true` (reusable in-memory `#hdrTexture` exists).
|
|
379
569
|
*/
|
|
380
570
|
async #createLights() {
|
|
381
571
|
const hemiLightName = "PrefViewerHemiLight";
|
|
@@ -388,7 +578,7 @@ export default class BabylonJSController {
|
|
|
388
578
|
|
|
389
579
|
let lightsChanged = false;
|
|
390
580
|
|
|
391
|
-
const iblEnabled = this.#settings.iblEnabled && this.#options.ibl?.
|
|
581
|
+
const iblEnabled = this.#settings.iblEnabled && (this.#options.ibl?.valid === true || !!this.#options.ibl?.cachedUrl);
|
|
392
582
|
|
|
393
583
|
if (iblEnabled) {
|
|
394
584
|
if (hemiLight) {
|
|
@@ -415,14 +605,14 @@ export default class BabylonJSController {
|
|
|
415
605
|
this.#hemiLight = new HemisphericLight(hemiLightName, new Vector3(-10, 10, -10), this.#scene);
|
|
416
606
|
this.#hemiLight.intensity = 0.6;
|
|
417
607
|
}
|
|
418
|
-
|
|
608
|
+
|
|
419
609
|
// Add a directional light to cast shadows and provide stronger directional illumination
|
|
420
610
|
if (!this.#dirLight) {
|
|
421
611
|
this.#dirLight = new DirectionalLight(dirLightName, new Vector3(-10, 10, -10), this.#scene);
|
|
422
612
|
this.#dirLight.position = new Vector3(5, 4, 5); // light is IN FRONT + ABOVE + to the RIGHT
|
|
423
613
|
this.#dirLight.intensity = 0.6;
|
|
424
614
|
}
|
|
425
|
-
|
|
615
|
+
|
|
426
616
|
// Add a point light that follows the camera to ensure the model is always well-lit from the viewer's perspective
|
|
427
617
|
if (!this.#cameraLight) {
|
|
428
618
|
this.#cameraLight = new PointLight(cameraLightName, this.#camera.position, this.#scene);
|
|
@@ -447,7 +637,7 @@ export default class BabylonJSController {
|
|
|
447
637
|
}
|
|
448
638
|
|
|
449
639
|
const supportedPipelines = pipelineManager.supportedPipelines;
|
|
450
|
-
|
|
640
|
+
|
|
451
641
|
if (supportedPipelines === undefined) {
|
|
452
642
|
return false;
|
|
453
643
|
}
|
|
@@ -487,23 +677,23 @@ export default class BabylonJSController {
|
|
|
487
677
|
return false;
|
|
488
678
|
}
|
|
489
679
|
const supportedPipelines = pipelineManager.supportedPipelines;
|
|
490
|
-
|
|
680
|
+
|
|
491
681
|
if (supportedPipelines === undefined) {
|
|
492
682
|
return false;
|
|
493
683
|
}
|
|
494
|
-
|
|
684
|
+
|
|
495
685
|
const pipelineName = "PrefViewerSSAORenderingPipeline";
|
|
496
686
|
|
|
497
687
|
const ssaoRatio = {
|
|
498
688
|
ssaoRatio: 0.5,
|
|
499
|
-
combineRatio: 1.0
|
|
689
|
+
combineRatio: 1.0,
|
|
500
690
|
};
|
|
501
691
|
|
|
502
692
|
let ssaoPipeline = new SSAORenderingPipeline(pipelineName, this.#scene, ssaoRatio, [this.#scene.activeCamera]);
|
|
503
693
|
|
|
504
|
-
if (!ssaoPipeline){
|
|
694
|
+
if (!ssaoPipeline) {
|
|
505
695
|
return false;
|
|
506
|
-
}
|
|
696
|
+
}
|
|
507
697
|
|
|
508
698
|
if (ssaoPipeline.isSupported) {
|
|
509
699
|
ssaoPipeline.fallOff = 0.000001;
|
|
@@ -511,7 +701,7 @@ export default class BabylonJSController {
|
|
|
511
701
|
ssaoPipeline.radius = 0.0001;
|
|
512
702
|
ssaoPipeline.totalStrength = 1;
|
|
513
703
|
ssaoPipeline.base = 0.6;
|
|
514
|
-
|
|
704
|
+
|
|
515
705
|
// Configure SSAO to calculate only once instead of every frame for better performance
|
|
516
706
|
if (ssaoPipeline._ssaoPostProcess) {
|
|
517
707
|
ssaoPipeline._ssaoPostProcess.autoClear = false;
|
|
@@ -521,7 +711,7 @@ export default class BabylonJSController {
|
|
|
521
711
|
ssaoPipeline._combinePostProcess.autoClear = false;
|
|
522
712
|
ssaoPipeline._combinePostProcess.samples = 1;
|
|
523
713
|
}
|
|
524
|
-
|
|
714
|
+
|
|
525
715
|
this.#renderPipelines.ssao = ssaoPipeline;
|
|
526
716
|
pipelineManager.update();
|
|
527
717
|
return true;
|
|
@@ -547,7 +737,7 @@ export default class BabylonJSController {
|
|
|
547
737
|
}
|
|
548
738
|
|
|
549
739
|
const supportedPipelines = pipelineManager.supportedPipelines;
|
|
550
|
-
|
|
740
|
+
|
|
551
741
|
if (supportedPipelines === undefined) {
|
|
552
742
|
return false;
|
|
553
743
|
}
|
|
@@ -588,18 +778,18 @@ export default class BabylonJSController {
|
|
|
588
778
|
return false;
|
|
589
779
|
}
|
|
590
780
|
const supportedPipelines = pipelineManager.supportedPipelines;
|
|
591
|
-
|
|
781
|
+
|
|
592
782
|
if (supportedPipelines === undefined) {
|
|
593
783
|
return false;
|
|
594
784
|
}
|
|
595
|
-
|
|
785
|
+
|
|
596
786
|
const pipelineName = "PrefViewerDefaultRenderingPipeline";
|
|
597
787
|
|
|
598
788
|
let defaultPipeline = new DefaultRenderingPipeline(pipelineName, true, this.#scene, [this.#scene.activeCamera], true);
|
|
599
789
|
|
|
600
|
-
if (!defaultPipeline){
|
|
790
|
+
if (!defaultPipeline) {
|
|
601
791
|
return false;
|
|
602
|
-
}
|
|
792
|
+
}
|
|
603
793
|
|
|
604
794
|
if (defaultPipeline.isSupported) {
|
|
605
795
|
// MSAA - Multisample Anti-Aliasing
|
|
@@ -647,19 +837,43 @@ export default class BabylonJSController {
|
|
|
647
837
|
|
|
648
838
|
/**
|
|
649
839
|
* Initializes the environment texture for the Babylon.js scene.
|
|
650
|
-
*
|
|
651
|
-
*
|
|
840
|
+
* Resolves the active HDR environment texture using either a fresh `cachedUrl`
|
|
841
|
+
* or the reusable in-memory clone (`#hdrTexture`), then assigns it to `scene.environmentTexture`.
|
|
652
842
|
* @private
|
|
653
843
|
* @returns {Promise<boolean>} Returns true if the environment texture was changed, false if it was already up to date or failed to load.
|
|
844
|
+
* @description
|
|
845
|
+
* Lifecycle implemented here:
|
|
846
|
+
* 1. If `options.ibl.cachedUrl` exists, create `HDRCubeTexture` from it.
|
|
847
|
+
* 2. Wait for readiness, clone it into `#hdrTexture` for reuse.
|
|
848
|
+
* 3. Call `options.ibl.consumeCachedUrl(true)` to revoke temporary object URLs and clear `cachedUrl`.
|
|
849
|
+
* 4. On following reloads, if `options.ibl.valid === true` and no `cachedUrl` is present, use `#hdrTexture.clone()`.
|
|
654
850
|
*/
|
|
655
851
|
async #initializeEnvironmentTexture() {
|
|
656
852
|
if (this.#scene.environmentTexture) {
|
|
657
853
|
this.#scene.environmentTexture.dispose();
|
|
658
854
|
this.#scene.environmentTexture = null;
|
|
659
855
|
}
|
|
660
|
-
|
|
661
|
-
|
|
856
|
+
|
|
857
|
+
let hdrTexture = null;
|
|
858
|
+
if (this.#options.ibl?.cachedUrl) {
|
|
859
|
+
const hdrTextureURI = this.#options.ibl.cachedUrl;
|
|
860
|
+
hdrTexture = new HDRCubeTexture(hdrTextureURI, this.#scene, 1024, false, false, false, true, undefined, undefined, false, true, true);
|
|
861
|
+
} else if (this.#hdrTexture && this.#options.ibl?.valid === true) {
|
|
862
|
+
hdrTexture = this.#hdrTexture.clone();
|
|
863
|
+
} else {
|
|
864
|
+
return false;
|
|
865
|
+
}
|
|
866
|
+
|
|
662
867
|
await WhenTextureReadyAsync(hdrTexture);
|
|
868
|
+
|
|
869
|
+
if (this.#options.ibl?.cachedUrl) {
|
|
870
|
+
if (this.#hdrTexture) {
|
|
871
|
+
this.#hdrTexture.dispose();
|
|
872
|
+
}
|
|
873
|
+
this.#hdrTexture = hdrTexture.clone();
|
|
874
|
+
this.#options.ibl?.consumeCachedUrl?.(true);
|
|
875
|
+
}
|
|
876
|
+
|
|
663
877
|
hdrTexture.level = this.#options.ibl.intensity;
|
|
664
878
|
this.#scene.environmentTexture = hdrTexture;
|
|
665
879
|
this.#scene.markAllMaterialsAsDirty(Material.TextureDirtyFlag);
|
|
@@ -680,7 +894,7 @@ export default class BabylonJSController {
|
|
|
680
894
|
}
|
|
681
895
|
|
|
682
896
|
const supportedPipelines = pipelineManager.supportedPipelines;
|
|
683
|
-
|
|
897
|
+
|
|
684
898
|
if (supportedPipelines === undefined) {
|
|
685
899
|
return false;
|
|
686
900
|
}
|
|
@@ -716,16 +930,9 @@ export default class BabylonJSController {
|
|
|
716
930
|
* @returns {Promise<void|boolean>} Returns false if no environment texture is set; otherwise void.
|
|
717
931
|
*/
|
|
718
932
|
async #initializeIBLShadows() {
|
|
719
|
-
|
|
720
|
-
await this.#scene.whenReadyAsync();
|
|
721
|
-
|
|
722
933
|
const pipelineManager = this.#scene?.postProcessRenderPipelineManager;
|
|
723
934
|
|
|
724
|
-
if (!this.#scene || !
|
|
725
|
-
return false;
|
|
726
|
-
}
|
|
727
|
-
|
|
728
|
-
if (!this.#scene.environmentTexture) {
|
|
935
|
+
if (!this.#scene || !this.#scene?.activeCamera || !this.#scene?.environmentTexture || !pipelineManager) {
|
|
729
936
|
return false;
|
|
730
937
|
}
|
|
731
938
|
|
|
@@ -736,13 +943,41 @@ export default class BabylonJSController {
|
|
|
736
943
|
});
|
|
737
944
|
return false;
|
|
738
945
|
}
|
|
739
|
-
|
|
946
|
+
|
|
947
|
+
const meshesForCastingShadows = this.#scene.meshes.filter((mesh) => {
|
|
948
|
+
const isRootMesh = mesh.id.startsWith("__root__");
|
|
949
|
+
if (isRootMesh) {
|
|
950
|
+
return false;
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
const isHDRIMesh = mesh.name?.toLowerCase() === "hdri";
|
|
954
|
+
const extrasCastShadows = mesh.metadata?.gltf?.extras?.castShadows;
|
|
955
|
+
const meshGenerateShadows = typeof extrasCastShadows === "boolean" ? extrasCastShadows : isHDRIMesh ? false : true;
|
|
956
|
+
|
|
957
|
+
if (meshGenerateShadows) {
|
|
958
|
+
return true;
|
|
959
|
+
}
|
|
960
|
+
return false;
|
|
961
|
+
});
|
|
962
|
+
const materialsForReceivingShadows = this.#scene.materials.filter((material) => {
|
|
963
|
+
if (material instanceof PBRMaterial) {
|
|
964
|
+
material.enableSpecularAntiAliasing = false;
|
|
965
|
+
}
|
|
966
|
+
return true;
|
|
967
|
+
});
|
|
968
|
+
|
|
969
|
+
if (meshesForCastingShadows.length === 0 || materialsForReceivingShadows.length === 0) {
|
|
970
|
+
return false;
|
|
971
|
+
}
|
|
972
|
+
|
|
740
973
|
const supportedPipelines = pipelineManager.supportedPipelines;
|
|
741
|
-
|
|
974
|
+
|
|
742
975
|
if (!supportedPipelines) {
|
|
743
976
|
return false;
|
|
744
977
|
}
|
|
745
978
|
|
|
979
|
+
await this.#scene.whenReadyAsync();
|
|
980
|
+
|
|
746
981
|
const pipelineName = "PrefViewerIblShadowsRenderPipeline";
|
|
747
982
|
|
|
748
983
|
const pipelineOptions = {
|
|
@@ -759,7 +994,7 @@ export default class BabylonJSController {
|
|
|
759
994
|
if (!iblShadowsPipeline) {
|
|
760
995
|
return false;
|
|
761
996
|
}
|
|
762
|
-
|
|
997
|
+
|
|
763
998
|
if (iblShadowsPipeline.isSupported) {
|
|
764
999
|
// Disable all debug passes for performance
|
|
765
1000
|
const pipelineProps = {
|
|
@@ -775,30 +1010,11 @@ export default class BabylonJSController {
|
|
|
775
1010
|
};
|
|
776
1011
|
|
|
777
1012
|
Object.assign(iblShadowsPipeline, pipelineProps);
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
}
|
|
784
|
-
|
|
785
|
-
const isHDRIMesh = mesh.name?.toLowerCase() === "hdri";
|
|
786
|
-
const extrasCastShadows = mesh.metadata?.gltf?.extras?.castShadows;
|
|
787
|
-
const meshGenerateShadows = typeof extrasCastShadows === "boolean" ? extrasCastShadows : isHDRIMesh ? false : true;
|
|
788
|
-
|
|
789
|
-
if (meshGenerateShadows) {
|
|
790
|
-
iblShadowsPipeline.addShadowCastingMesh(mesh);
|
|
791
|
-
iblShadowsPipeline.updateSceneBounds();
|
|
792
|
-
}
|
|
793
|
-
});
|
|
794
|
-
|
|
795
|
-
this.#scene.materials.forEach((material) => {
|
|
796
|
-
if (material instanceof PBRMaterial) {
|
|
797
|
-
material.enableSpecularAntiAliasing = false;
|
|
798
|
-
}
|
|
799
|
-
iblShadowsPipeline.addShadowReceivingMaterial(material);
|
|
800
|
-
});
|
|
801
|
-
|
|
1013
|
+
|
|
1014
|
+
meshesForCastingShadows.forEach((mesh) => iblShadowsPipeline.addShadowCastingMesh(mesh));
|
|
1015
|
+
materialsForReceivingShadows.forEach((material) => iblShadowsPipeline.addShadowReceivingMaterial(material));
|
|
1016
|
+
|
|
1017
|
+
iblShadowsPipeline.updateSceneBounds();
|
|
802
1018
|
iblShadowsPipeline.toggleShadow(true);
|
|
803
1019
|
iblShadowsPipeline.updateVoxelization();
|
|
804
1020
|
this.#renderPipelines.iblShadows = iblShadowsPipeline;
|
|
@@ -935,7 +1151,7 @@ export default class BabylonJSController {
|
|
|
935
1151
|
|
|
936
1152
|
this.#ensureMeshesReceiveShadows();
|
|
937
1153
|
|
|
938
|
-
const iblEnabled = this.#settings.iblEnabled && this.#options.ibl?.
|
|
1154
|
+
const iblEnabled = this.#settings.iblEnabled && (this.#options.ibl?.valid === true || !!this.#options.ibl?.cachedUrl);
|
|
939
1155
|
const iblShadowsEnabled = iblEnabled && this.#options.ibl.shadows;
|
|
940
1156
|
|
|
941
1157
|
if (iblShadowsEnabled) {
|
|
@@ -964,23 +1180,6 @@ export default class BabylonJSController {
|
|
|
964
1180
|
}
|
|
965
1181
|
}
|
|
966
1182
|
|
|
967
|
-
/**
|
|
968
|
-
* Handles pointer events observed on the Babylon.js scene.
|
|
969
|
-
* @private
|
|
970
|
-
* @param {PointerInfo} info - The pointer event information from Babylon.js.
|
|
971
|
-
* @returns {void}
|
|
972
|
-
*/
|
|
973
|
-
#onPointerObservable(info) {
|
|
974
|
-
const pickInfo = this.#scene.pick(this.#scene.pointerX, this.#scene.pointerY);
|
|
975
|
-
if (info.type === PointerEventTypes.POINTERUP) {
|
|
976
|
-
this.#onPointerUp(info.event, pickInfo);
|
|
977
|
-
} else if (info.type === PointerEventTypes.POINTERMOVE) {
|
|
978
|
-
this.#onPointerMove(info.event, pickInfo);
|
|
979
|
-
} else if (info.type === PointerEventTypes.POINTERWHEEL) {
|
|
980
|
-
this.#onMouseWheel(info.event, pickInfo);
|
|
981
|
-
}
|
|
982
|
-
}
|
|
983
|
-
|
|
984
1183
|
/**
|
|
985
1184
|
* Sets up interaction handlers for the Babylon.js canvas and scene.
|
|
986
1185
|
* @private
|
|
@@ -993,6 +1192,13 @@ export default class BabylonJSController {
|
|
|
993
1192
|
if (this.#scene) {
|
|
994
1193
|
this.#scene.onPointerObservable.add(this.#handlers.onPointerObservable);
|
|
995
1194
|
}
|
|
1195
|
+
if (this.#engine) {
|
|
1196
|
+
this.#canvasResizeObserver = new ResizeObserver(() => {
|
|
1197
|
+
this.#engine.resize();
|
|
1198
|
+
this.#requestRender({ frames: this.#renderConfig.burstFramesBase, continuousMs: this.#renderConfig.interactionMs });
|
|
1199
|
+
});
|
|
1200
|
+
this.#canvasResizeObserver.observe(this.#canvas);
|
|
1201
|
+
}
|
|
996
1202
|
}
|
|
997
1203
|
|
|
998
1204
|
/**
|
|
@@ -1007,6 +1213,9 @@ export default class BabylonJSController {
|
|
|
1007
1213
|
if (this.#scene !== null) {
|
|
1008
1214
|
this.#scene.onPointerObservable.removeCallback(this.#handlers.onPointerObservable);
|
|
1009
1215
|
}
|
|
1216
|
+
this.#canvasResizeObserver?.disconnect();
|
|
1217
|
+
this.#canvasResizeObserver = null;
|
|
1218
|
+
this.#detachAnimationChangedListener();
|
|
1010
1219
|
}
|
|
1011
1220
|
|
|
1012
1221
|
/**
|
|
@@ -1058,11 +1267,84 @@ export default class BabylonJSController {
|
|
|
1058
1267
|
if (!this.#engine) {
|
|
1059
1268
|
return;
|
|
1060
1269
|
}
|
|
1270
|
+
if (this.#hdrTexture) {
|
|
1271
|
+
this.#hdrTexture.dispose();
|
|
1272
|
+
this.#hdrTexture = null;
|
|
1273
|
+
}
|
|
1061
1274
|
this.#engine.dispose();
|
|
1062
1275
|
this.#engine = this.#scene = this.#camera = null;
|
|
1063
1276
|
this.#hemiLight = this.#dirLight = this.#cameraLight = null;
|
|
1064
1277
|
}
|
|
1065
1278
|
|
|
1279
|
+
/**
|
|
1280
|
+
* Handles animation state events emitted by `OpeningAnimation` instances.
|
|
1281
|
+
* Routes opening/closing states to continuous rendering and all other states
|
|
1282
|
+
* (paused/opened/closed) to a short final render burst.
|
|
1283
|
+
* @private
|
|
1284
|
+
* @param {CustomEvent} event - Event carrying animation state in `event.detail.state`.
|
|
1285
|
+
* @returns {void}
|
|
1286
|
+
*/
|
|
1287
|
+
#onAnimationGroupChanged(event) {
|
|
1288
|
+
const state = event?.detail?.state;
|
|
1289
|
+
if (state === undefined) {
|
|
1290
|
+
return;
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
if (state === OpeningAnimation.states.opening || state === OpeningAnimation.states.closing) {
|
|
1294
|
+
this.#onAnimationGroupPlay();
|
|
1295
|
+
} else {
|
|
1296
|
+
this.#onAnimationGroupStop();
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
/**
|
|
1301
|
+
* Marks animation playback as active and requests short continuous rendering so animated transforms remain smooth while state is changing.
|
|
1302
|
+
* @private
|
|
1303
|
+
* @returns {void}
|
|
1304
|
+
*/
|
|
1305
|
+
#onAnimationGroupPlay() {
|
|
1306
|
+
this.#requestRender({ frames: 1, continuousMs: this.#renderConfig.animationMs });
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1309
|
+
/**
|
|
1310
|
+
* Handles animation stop/pause/end transitions by requesting a final render burst.
|
|
1311
|
+
* @private
|
|
1312
|
+
* @returns {void}
|
|
1313
|
+
*/
|
|
1314
|
+
#onAnimationGroupStop() {
|
|
1315
|
+
const frames = this.#settings.antiAliasingEnabled || this.#settings.ambientOcclusionEnabled || this.#settings.iblEnabled ? this.#renderConfig.burstFramesEnhanced : this.#renderConfig.burstFramesBase;
|
|
1316
|
+
this.#requestRender({ frames: frames });
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1319
|
+
/**
|
|
1320
|
+
* Attaches the `prefviewer-animation-changed` listener to the nearest `pref-viewer-3d` host.
|
|
1321
|
+
* Removes any previous registration first to avoid duplicate callbacks across reload cycles.
|
|
1322
|
+
* @private
|
|
1323
|
+
* @returns {boolean} True when the listener is attached, false when no host is available.
|
|
1324
|
+
*/
|
|
1325
|
+
#attachAnimationChangedListener() {
|
|
1326
|
+
this.#getPrefViewer3DComponent();
|
|
1327
|
+
if (!this.#prefViewer3D) {
|
|
1328
|
+
return false;
|
|
1329
|
+
}
|
|
1330
|
+
this.#detachAnimationChangedListener();
|
|
1331
|
+
this.#prefViewer3D.addEventListener("prefviewer-animation-changed", this.#handlers.onAnimationGroupChanged);
|
|
1332
|
+
return true;
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
/**
|
|
1336
|
+
* Detaches the `prefviewer-animation-changed` listener from the cached `pref-viewer-3d` host.
|
|
1337
|
+
* @private
|
|
1338
|
+
* @returns {boolean} True when a host exists and the listener removal was attempted, false otherwise.
|
|
1339
|
+
*/
|
|
1340
|
+
#detachAnimationChangedListener() {
|
|
1341
|
+
if (!this.#prefViewer3D) {
|
|
1342
|
+
return false;
|
|
1343
|
+
}
|
|
1344
|
+
this.#prefViewer3D.removeEventListener("prefviewer-animation-changed", this.#handlers.onAnimationGroupChanged);
|
|
1345
|
+
return true;
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1066
1348
|
/**
|
|
1067
1349
|
* Handles keyup events on the Babylon.js canvas for triggering model and scene downloads.
|
|
1068
1350
|
* @private
|
|
@@ -1090,11 +1372,12 @@ export default class BabylonJSController {
|
|
|
1090
1372
|
* @returns {void|false} Returns false if there is no active camera; otherwise, void.
|
|
1091
1373
|
*/
|
|
1092
1374
|
#onMouseWheel(event, pickInfo) {
|
|
1375
|
+
event.preventDefault();
|
|
1093
1376
|
const camera = this.#scene?.activeCamera;
|
|
1094
1377
|
if (!camera) {
|
|
1095
1378
|
return false;
|
|
1096
1379
|
}
|
|
1097
|
-
if (!camera.metadata?.locked) {
|
|
1380
|
+
if (!camera.metadata?.locked) {
|
|
1098
1381
|
if (camera instanceof ArcRotateCamera) {
|
|
1099
1382
|
camera.wheelPrecision = camera.wheelPrecision || 3.0;
|
|
1100
1383
|
camera.inertialRadiusOffset -= event.deltaY * camera.wheelPrecision * 0.001;
|
|
@@ -1107,8 +1390,8 @@ export default class BabylonJSController {
|
|
|
1107
1390
|
const movementVector = direction.scale(zoomSpeed);
|
|
1108
1391
|
camera.position = camera.position.add(movementVector);
|
|
1109
1392
|
}
|
|
1393
|
+
this.#requestRender({ frames: 1, continuousMs: this.#renderConfig.interactionMs });
|
|
1110
1394
|
}
|
|
1111
|
-
event.preventDefault();
|
|
1112
1395
|
}
|
|
1113
1396
|
|
|
1114
1397
|
/**
|
|
@@ -1136,9 +1419,54 @@ export default class BabylonJSController {
|
|
|
1136
1419
|
* @returns {void}
|
|
1137
1420
|
*/
|
|
1138
1421
|
#onPointerMove(event, pickInfo) {
|
|
1422
|
+
const camera = this.#scene?.activeCamera;
|
|
1423
|
+
if (camera && !camera.metadata?.locked) {
|
|
1424
|
+
this.#requestRender({ frames: 1, continuousMs: this.#renderConfig.interactionMs });
|
|
1425
|
+
}
|
|
1139
1426
|
if (this.#babylonJSAnimationController) {
|
|
1140
|
-
|
|
1427
|
+
const pickedMeshId = pickInfo?.pickedMesh?.id || null;
|
|
1428
|
+
if (this.#lastPickedMeshId !== pickedMeshId) {
|
|
1429
|
+
const highlightResult = this.#babylonJSAnimationController.highlightMeshes(pickInfo);
|
|
1430
|
+
if (highlightResult.changed) {
|
|
1431
|
+
this.#requestRender({ frames: 1, continuousMs: this.#renderConfig.interactionMs });
|
|
1432
|
+
}
|
|
1433
|
+
}
|
|
1434
|
+
}
|
|
1435
|
+
}
|
|
1436
|
+
|
|
1437
|
+
/**
|
|
1438
|
+
* Handles pointer events observed on the Babylon.js scene.
|
|
1439
|
+
* @private
|
|
1440
|
+
* @param {PointerInfo} info - The pointer event information from Babylon.js.
|
|
1441
|
+
* @returns {void}
|
|
1442
|
+
*/
|
|
1443
|
+
#onPointerObservable(info) {
|
|
1444
|
+
const pickInfo = this.#scene.pick(this.#scene.pointerX, this.#scene.pointerY);
|
|
1445
|
+
const pickedMeshId = pickInfo?.pickedMesh?.id || null;
|
|
1446
|
+
|
|
1447
|
+
if (info.type === PointerEventTypes.POINTERMOVE) {
|
|
1448
|
+
this.#onPointerMove(info.event, pickInfo);
|
|
1449
|
+
} else if (info.type === PointerEventTypes.POINTERUP) {
|
|
1450
|
+
this.#onPointerUp(info.event, pickInfo);
|
|
1451
|
+
} else if (info.type === PointerEventTypes.POINTERWHEEL) {
|
|
1452
|
+
this.#onMouseWheel(info.event, pickInfo);
|
|
1141
1453
|
}
|
|
1454
|
+
this.#lastPickedMeshId = pickedMeshId;
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1457
|
+
/**
|
|
1458
|
+
* Handles canvas resize notifications.
|
|
1459
|
+
* Resizes the Babylon engine and requests a short on-demand render burst so camera-dependent
|
|
1460
|
+
* buffers and post-process pipelines are redrawn at the new viewport size.
|
|
1461
|
+
* @private
|
|
1462
|
+
* @returns {void}
|
|
1463
|
+
*/
|
|
1464
|
+
#onResize() {
|
|
1465
|
+
if (!this.#engine) {
|
|
1466
|
+
return;
|
|
1467
|
+
}
|
|
1468
|
+
this.#engine.resize();
|
|
1469
|
+
this.#requestRender({ frames: this.#renderConfig.burstFramesBase, continuousMs: this.#renderConfig.interactionMs });
|
|
1142
1470
|
}
|
|
1143
1471
|
|
|
1144
1472
|
/**
|
|
@@ -1180,7 +1508,7 @@ export default class BabylonJSController {
|
|
|
1180
1508
|
.forEach((mesh) => {
|
|
1181
1509
|
mesh.material = material;
|
|
1182
1510
|
someSetted = true;
|
|
1183
|
-
})
|
|
1511
|
+
}),
|
|
1184
1512
|
);
|
|
1185
1513
|
|
|
1186
1514
|
if (someSetted) {
|
|
@@ -1275,6 +1603,9 @@ export default class BabylonJSController {
|
|
|
1275
1603
|
* Marks the IBL state as successful, recreates lights so the new environment takes effect, and reports whether anything changed.
|
|
1276
1604
|
* @private
|
|
1277
1605
|
* @returns {boolean} True when lights were refreshed due to pending IBL changes, otherwise false.
|
|
1606
|
+
* @description
|
|
1607
|
+
* Delegates to `#createLights()`, which executes the full IBL URL-to-texture lifecycle:
|
|
1608
|
+
* `cachedUrl -> HDRCubeTexture -> #hdrTexture clone -> consumeCachedUrl(true)`.
|
|
1278
1609
|
*/
|
|
1279
1610
|
async #setOptions_IBL() {
|
|
1280
1611
|
return await this.#createLights();
|
|
@@ -1291,34 +1622,47 @@ export default class BabylonJSController {
|
|
|
1291
1622
|
}
|
|
1292
1623
|
|
|
1293
1624
|
/**
|
|
1294
|
-
*
|
|
1625
|
+
* Resolves and caches the closest `pref-viewer-3d` host associated with the rendering canvas.
|
|
1295
1626
|
* @private
|
|
1296
1627
|
* @returns {void}
|
|
1297
1628
|
*/
|
|
1298
1629
|
#getPrefViewer3DComponent() {
|
|
1299
|
-
if (this.#prefViewer3D
|
|
1300
|
-
|
|
1301
|
-
|
|
1630
|
+
if (this.#prefViewer3D !== undefined) {
|
|
1631
|
+
return;
|
|
1632
|
+
}
|
|
1633
|
+
|
|
1634
|
+
let prefViewer3D = this.#canvas?.closest?.("pref-viewer-3d") || undefined;
|
|
1635
|
+
if (!prefViewer3D) {
|
|
1636
|
+
const host = this.#canvas?.getRootNode?.()?.host;
|
|
1637
|
+
prefViewer3D = host?.nodeName === "PREF-VIEWER-3D" ? host : undefined;
|
|
1302
1638
|
}
|
|
1639
|
+
|
|
1640
|
+
this.#prefViewer3D = prefViewer3D;
|
|
1303
1641
|
}
|
|
1304
1642
|
|
|
1305
1643
|
/**
|
|
1306
|
-
*
|
|
1644
|
+
* Resolves and caches the closest `pref-viewer` host associated with `#prefViewer3D`.
|
|
1307
1645
|
* @private
|
|
1308
1646
|
* @returns {void}
|
|
1309
1647
|
*/
|
|
1310
1648
|
#getPrefViewerComponent() {
|
|
1311
|
-
if (this.#prefViewer
|
|
1312
|
-
|
|
1313
|
-
this.#getPrefViewer3DComponent();
|
|
1314
|
-
}
|
|
1315
|
-
if (!this.#prefViewer3D) {
|
|
1316
|
-
this.#prefViewer = null;
|
|
1317
|
-
return;
|
|
1318
|
-
}
|
|
1319
|
-
const rootNode = this.#prefViewer3D ? this.#prefViewer3D.getRootNode().host : null;
|
|
1320
|
-
this.#prefViewer = rootNode && rootNode.nodeName === "PREF-VIEWER" ? rootNode : null;
|
|
1649
|
+
if (this.#prefViewer !== undefined) {
|
|
1650
|
+
return;
|
|
1321
1651
|
}
|
|
1652
|
+
|
|
1653
|
+
this.#getPrefViewer3DComponent();
|
|
1654
|
+
if (this.#prefViewer3D === undefined) {
|
|
1655
|
+
this.#prefViewer = undefined;
|
|
1656
|
+
return;
|
|
1657
|
+
}
|
|
1658
|
+
|
|
1659
|
+
let prefViewer = this.#prefViewer3D.closest?.("pref-viewer") || undefined;
|
|
1660
|
+
if (!prefViewer) {
|
|
1661
|
+
const host = this.#prefViewer3D.getRootNode?.()?.host;
|
|
1662
|
+
prefViewer = host?.nodeName === "PREF-VIEWER" ? host : undefined;
|
|
1663
|
+
}
|
|
1664
|
+
|
|
1665
|
+
this.#prefViewer = prefViewer;
|
|
1322
1666
|
}
|
|
1323
1667
|
|
|
1324
1668
|
/**
|
|
@@ -1467,7 +1811,10 @@ export default class BabylonJSController {
|
|
|
1467
1811
|
* @returns {void}
|
|
1468
1812
|
*/
|
|
1469
1813
|
async #stopRender() {
|
|
1470
|
-
this.#
|
|
1814
|
+
this.#stopEngineRenderLoop();
|
|
1815
|
+
this.#renderState.dirtyFrames = 0;
|
|
1816
|
+
this.#renderState.continuousUntil = 0;
|
|
1817
|
+
this.#renderState.lastRenderAt = 0;
|
|
1471
1818
|
await this.#unloadCameraDependentEffects();
|
|
1472
1819
|
}
|
|
1473
1820
|
/**
|
|
@@ -1478,9 +1825,9 @@ export default class BabylonJSController {
|
|
|
1478
1825
|
*/
|
|
1479
1826
|
async #startRender() {
|
|
1480
1827
|
await this.#loadCameraDependentEffects();
|
|
1481
|
-
this.#scene.
|
|
1482
|
-
|
|
1483
|
-
});
|
|
1828
|
+
await this.#scene.whenReadyAsync();
|
|
1829
|
+
const frames = this.#settings.antiAliasingEnabled || this.#settings.ambientOcclusionEnabled || this.#settings.iblEnabled ? this.#renderConfig.burstFramesEnhanced : this.#renderConfig.burstFramesBase;
|
|
1830
|
+
this.#requestRender({ frames: frames, continuousMs: this.#renderConfig.interactionMs });
|
|
1484
1831
|
}
|
|
1485
1832
|
|
|
1486
1833
|
/**
|
|
@@ -1500,7 +1847,6 @@ export default class BabylonJSController {
|
|
|
1500
1847
|
* `LoadAssetContainerAsync`, returning the tuple so the caller can decide how to attach it to the scene.
|
|
1501
1848
|
*/
|
|
1502
1849
|
async #loadAssetContainer(container, force = false) {
|
|
1503
|
-
|
|
1504
1850
|
if (container?.state?.update?.storage === undefined || container?.state?.size === undefined || container?.state?.timeStamp === undefined) {
|
|
1505
1851
|
return [container, false];
|
|
1506
1852
|
}
|
|
@@ -1546,6 +1892,8 @@ export default class BabylonJSController {
|
|
|
1546
1892
|
return [container, assetContainer];
|
|
1547
1893
|
} catch (error) {
|
|
1548
1894
|
return [container, assetContainer];
|
|
1895
|
+
} finally {
|
|
1896
|
+
this.#gltfResolver.revokeObjectURLs(sourceData.objectURLs);
|
|
1549
1897
|
}
|
|
1550
1898
|
}
|
|
1551
1899
|
|
|
@@ -1559,6 +1907,7 @@ export default class BabylonJSController {
|
|
|
1559
1907
|
* Returns an object with success status and error details.
|
|
1560
1908
|
*/
|
|
1561
1909
|
async #loadContainers() {
|
|
1910
|
+
this.#detachAnimationChangedListener();
|
|
1562
1911
|
await this.#stopRender();
|
|
1563
1912
|
|
|
1564
1913
|
let oldModelMetadata = { ...(this.#containers.model?.state?.metadata ?? {}) };
|
|
@@ -1615,6 +1964,9 @@ export default class BabylonJSController {
|
|
|
1615
1964
|
this.#checkModelMetadata(oldModelMetadata, newModelMetadata);
|
|
1616
1965
|
this.#setMaxSimultaneousLights();
|
|
1617
1966
|
this.#babylonJSAnimationController = new BabylonJSAnimationController(this.#containers.model.assetContainer);
|
|
1967
|
+
if (this.#babylonJSAnimationController?.hasAnimations?.()) {
|
|
1968
|
+
this.#attachAnimationChangedListener();
|
|
1969
|
+
}
|
|
1618
1970
|
await this.#startRender();
|
|
1619
1971
|
});
|
|
1620
1972
|
return detail;
|
|
@@ -1874,10 +2226,10 @@ export default class BabylonJSController {
|
|
|
1874
2226
|
this.#engine = new Engine(this.#canvas, true, { alpha: true, stencil: true, preserveDrawingBuffer: false });
|
|
1875
2227
|
this.#engine.disableUniformBuffers = true;
|
|
1876
2228
|
this.#scene = new Scene(this.#engine);
|
|
1877
|
-
|
|
2229
|
+
|
|
1878
2230
|
// Activate the rendering of geometry data into a G-buffer, essential for advanced effects like deferred shading,
|
|
1879
|
-
// SSAO, and Velocity-Texture-Animation (VAT), allowing for complex post-processing by separating rendering into
|
|
1880
|
-
// different buffers (depth, normals, velocity) for later use in shaders.
|
|
2231
|
+
// SSAO, and Velocity-Texture-Animation (VAT), allowing for complex post-processing by separating rendering into
|
|
2232
|
+
// different buffers (depth, normals, velocity) for later use in shaders.
|
|
1881
2233
|
const geometryBufferRenderer = this.#scene.enableGeometryBufferRenderer();
|
|
1882
2234
|
if (geometryBufferRenderer) {
|
|
1883
2235
|
geometryBufferRenderer.enableScreenspaceDepth = true;
|
|
@@ -1894,12 +2246,14 @@ export default class BabylonJSController {
|
|
|
1894
2246
|
this.#scene.imageProcessingConfiguration.vignetteEnabled = false;
|
|
1895
2247
|
this.#scene.imageProcessingConfiguration.colorCurvesEnabled = false;
|
|
1896
2248
|
|
|
2249
|
+
// Skip the built-in pointer picking logic since the controller implements its own optimized raycasting for interaction.
|
|
2250
|
+
this.#scene.skipPointerMovePicking = true;
|
|
2251
|
+
this.#scene.skipPointerDownPicking = true;
|
|
2252
|
+
this.#scene.skipPointerUpPicking = true;
|
|
2253
|
+
|
|
1897
2254
|
this.#createCamera();
|
|
1898
2255
|
this.#enableInteraction();
|
|
1899
2256
|
await this.#createXRExperience();
|
|
1900
|
-
this.#startRender();
|
|
1901
|
-
this.#canvasResizeObserver = new ResizeObserver(() => this.#engine && this.#engine.resize());
|
|
1902
|
-
this.#canvasResizeObserver.observe(this.#canvas);
|
|
1903
2257
|
}
|
|
1904
2258
|
|
|
1905
2259
|
/**
|
|
@@ -1909,11 +2263,11 @@ export default class BabylonJSController {
|
|
|
1909
2263
|
* @returns {void}
|
|
1910
2264
|
*/
|
|
1911
2265
|
disable() {
|
|
1912
|
-
this.#canvasResizeObserver.disconnect();
|
|
1913
2266
|
this.#disableInteraction();
|
|
1914
2267
|
this.#disposeAnimationController();
|
|
1915
2268
|
this.#disposeXRExperience();
|
|
1916
2269
|
this.#unloadCameraDependentEffects();
|
|
2270
|
+
this.#stopEngineRenderLoop();
|
|
1917
2271
|
this.#disposeEngine();
|
|
1918
2272
|
}
|
|
1919
2273
|
|