@preference-sl/pref-viewer 2.13.0-beta.1 → 2.13.0-beta.3
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-controller.js +439 -217
- package/src/pref-viewer-3d-data.js +11 -31
- package/src/pref-viewer-3d.js +6 -5
- package/src/pref-viewer-menu-3d.js +9 -2
- package/src/pref-viewer.js +4 -4
package/package.json
CHANGED
|
@@ -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";
|
|
@@ -11,53 +11,53 @@ import BabylonJSAnimationController from "./babylonjs-animation-controller.js";
|
|
|
11
11
|
import { translate } from "./localization/i18n.js";
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
|
-
* BabylonJSController
|
|
15
|
-
* rebuilds
|
|
16
|
-
*
|
|
17
|
-
*
|
|
14
|
+
* BabylonJSController coordinates the PrefViewer 3D runtime: it bootstraps Babylon.js, manages asset containers,
|
|
15
|
+
* rebuilds camera-dependent pipelines once textures are ready, brokers XR/download interactions, and persists
|
|
16
|
+
* render toggles so the UI can stay declarative. PrefViewer hands over container + option state, while this class
|
|
17
|
+
* turns it into a deterministic scene lifecycle with exports.
|
|
18
18
|
*
|
|
19
19
|
* Overview
|
|
20
|
-
* -
|
|
21
|
-
*
|
|
22
|
-
* -
|
|
23
|
-
*
|
|
24
|
-
* -
|
|
20
|
+
* - Creates the Babylon.js engine/scene/camera stack, configures Draco decoders, wires resize + render loops, and
|
|
21
|
+
* exposes download/xR helpers.
|
|
22
|
+
* - Resolves GLTF/GLB sources via GLTFResolver, loads them into `AssetContainer`s, and toggles visibility by
|
|
23
|
+
* mutating container state plus `show-model/show-scene` attributes.
|
|
24
|
+
* - Applies/persists AA, SSAO, IBL, and shadow flags; when they change it stops the render loop, tears down pipelines,
|
|
25
|
+
* reloads containers, and reinstalls effects after environment textures finish loading.
|
|
26
|
+
* - Manages keyboard/pointer/wheel handlers, animation menus, and WebXR so PrefViewer menus and DOM attributes stay
|
|
27
|
+
* synchronized with Babylon state.
|
|
25
28
|
* - Generates GLB, glTF+ZIP, or USDZ exports with timestamped names and localized dialog copy.
|
|
26
29
|
* - Translates metadata (inner floor offsets, cast/receive shadows, camera locks) into scene adjustments after reloads.
|
|
27
30
|
*
|
|
28
31
|
* Runtime Flow
|
|
29
32
|
* 1. Instantiate with `new BabylonJSController(canvas, containers, options)`.
|
|
30
|
-
* 2. Call `enable()` to configure Draco,
|
|
31
|
-
* 3.
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
33
|
+
* 2. Call `enable()` to configure Draco, spin up the engine/scene, attach interaction + XR hooks, and start the render loop.
|
|
34
|
+
* 3. When PrefViewer marks containers/options pending, invoke `load()` (or `reloadWithCurrentSettings()`) so the controller
|
|
35
|
+
* fetches sources, rebuilds containers, and reattaches pipelines.
|
|
36
|
+
* 4. Use `scheduleRenderSettingsReload()` to merge/persist toggles; when it reports `changed: true`, call
|
|
37
|
+
* `reloadWithCurrentSettings()` to apply the staged settings.
|
|
38
|
+
* 5. Use `setContainerVisibility`, `setMaterialOptions`, `setCameraOptions`, or `setIBLOptions` for targeted updates; these
|
|
39
|
+
* helpers stop/restart the render loop while they rebuild camera-dependent resources.
|
|
40
|
+
* 6. Invoke `disable()` when the element disconnects to tear down scenes, XR sessions, observers, and handlers.
|
|
36
41
|
*
|
|
37
42
|
* Public API Highlights
|
|
38
43
|
* - constructor(canvas, containers, options)
|
|
39
44
|
* - enable() / disable()
|
|
40
|
-
* - load()
|
|
45
|
+
* - load() / reloadWithCurrentSettings()
|
|
41
46
|
* - downloadGLB(content) / downloadGLTF(content) / downloadUSDZ(content)
|
|
42
|
-
* - getRenderSettings() /
|
|
43
|
-
* -
|
|
47
|
+
* - getRenderSettings() / scheduleRenderSettingsReload(settings)
|
|
48
|
+
* - setContainerVisibility(name, show)
|
|
49
|
+
* - setMaterialOptions() / setCameraOptions() / setIBLOptions()
|
|
44
50
|
*
|
|
45
|
-
* Key
|
|
46
|
-
* -
|
|
47
|
-
*
|
|
48
|
-
* -
|
|
49
|
-
*
|
|
50
|
-
* -
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
* #downloadZip, #openDownloadDialog.
|
|
56
|
-
*
|
|
57
|
-
* Notes
|
|
58
|
-
* - Designed to be long-lived per PrefViewer instance; it caches parent components to reflect `show-model/show-scene` attributes.
|
|
59
|
-
* - All browser-only features guard against SSR/Node usage by checking `window` before touching localStorage or XR APIs.
|
|
60
|
-
* - Relies on PrefViewerMenu3D events to trigger render-setting updates, ensuring UI and persisted state never drift apart.
|
|
51
|
+
* Key Invariants
|
|
52
|
+
* - Asset containers must expose `setPendingWithCurrentStorage`/`setPending` before calling load/reload; the controller
|
|
53
|
+
* reads those flags to resolve fresh sources and avoids touching the DOM until data is ready.
|
|
54
|
+
* - Camera-dependent pipelines (DefaultRenderingPipeline, SSAO, IBL shadows, directional shadow generators) are rebuilt
|
|
55
|
+
* only after the active camera and environment textures are ready; render-loop restarts gate those transitions.
|
|
56
|
+
* - `show-model`/`show-scene` DOM attributes reflect container visibility; there are no direct `showModel()/hideModel()` APIs.
|
|
57
|
+
* - IBL shadows require `iblEnabled` plus `options.ibl.shadows` and a loaded HDR texture; otherwise fallback directional
|
|
58
|
+
* lights and environment-contributed lights supply classic shadow generators.
|
|
59
|
+
* - Browser-only features guard `window`, localStorage, and XR APIs before use so the controller is safe to construct
|
|
60
|
+
* in SSR/Node contexts (though functionality activates only in browsers).
|
|
61
61
|
*/
|
|
62
62
|
export default class BabylonJSController {
|
|
63
63
|
|
|
@@ -96,7 +96,13 @@ export default class BabylonJSController {
|
|
|
96
96
|
|
|
97
97
|
#gltfResolver = null; // GLTFResolver instance
|
|
98
98
|
#babylonJSAnimationController = null; // AnimationController instance
|
|
99
|
-
|
|
99
|
+
|
|
100
|
+
#renderPipelines = {
|
|
101
|
+
default: null,
|
|
102
|
+
ssao: null,
|
|
103
|
+
iblShadows: null,
|
|
104
|
+
};
|
|
105
|
+
|
|
100
106
|
#handlers = {
|
|
101
107
|
onKeyUp: null,
|
|
102
108
|
onPointerObservable: null,
|
|
@@ -167,7 +173,7 @@ export default class BabylonJSController {
|
|
|
167
173
|
* @param {object} [settings={}] - Partial map of render settings (AA, SSAO, IBL, shadows, etc.).
|
|
168
174
|
* @returns {boolean} True when any setting changed and was saved.
|
|
169
175
|
*/
|
|
170
|
-
#
|
|
176
|
+
#saveRenderSettings(settings = {}) {
|
|
171
177
|
if (!settings) {
|
|
172
178
|
return false;
|
|
173
179
|
}
|
|
@@ -180,12 +186,8 @@ export default class BabylonJSController {
|
|
|
180
186
|
}
|
|
181
187
|
});
|
|
182
188
|
|
|
183
|
-
if (changed && settings.iblEnabled === false && this.#scene) {
|
|
184
|
-
this.#scene.environmentTexture = null;
|
|
185
|
-
}
|
|
186
|
-
|
|
187
189
|
if (changed) {
|
|
188
|
-
this.#
|
|
190
|
+
this.#storeRenderSettings();
|
|
189
191
|
}
|
|
190
192
|
|
|
191
193
|
return changed;
|
|
@@ -223,14 +225,14 @@ export default class BabylonJSController {
|
|
|
223
225
|
* @private
|
|
224
226
|
* @returns {void}
|
|
225
227
|
*/
|
|
226
|
-
#
|
|
228
|
+
#storeRenderSettings() {
|
|
227
229
|
if (typeof window === "undefined" || !window?.localStorage) {
|
|
228
230
|
return;
|
|
229
231
|
}
|
|
230
232
|
try {
|
|
231
233
|
window.localStorage.setItem(this.#RENDER_SETTINGS_STORAGE_KEY, JSON.stringify(this.#settings));
|
|
232
234
|
} catch (error) {
|
|
233
|
-
console.warn("PrefViewer: unable to
|
|
235
|
+
console.warn("PrefViewer: unable to store render settings", error);
|
|
234
236
|
}
|
|
235
237
|
}
|
|
236
238
|
|
|
@@ -263,9 +265,6 @@ export default class BabylonJSController {
|
|
|
263
265
|
if (this.#options?.materials) {
|
|
264
266
|
Object.values(this.#options.materials).forEach((material) => material?.setPendingWithCurrent?.());
|
|
265
267
|
}
|
|
266
|
-
if (this.#options?.ibl?.setPending) {
|
|
267
|
-
this.#options.ibl.setPending();
|
|
268
|
-
}
|
|
269
268
|
}
|
|
270
269
|
|
|
271
270
|
/**
|
|
@@ -376,40 +375,99 @@ export default class BabylonJSController {
|
|
|
376
375
|
* Adds a hemispheric ambient light, a directional light for shadows, a shadow generator, and a camera-attached point light.
|
|
377
376
|
* Sets light intensities and shadow properties for realistic rendering.
|
|
378
377
|
* @private
|
|
379
|
-
* @returns {
|
|
378
|
+
* @returns {Promise<boolean>} Returns true if lights were changed, false otherwise.
|
|
380
379
|
*/
|
|
381
|
-
#createLights() {
|
|
382
|
-
|
|
380
|
+
async #createLights() {
|
|
381
|
+
const hemiLightName = "PrefViewerHemiLight";
|
|
382
|
+
const cameraLightName = "PrefViewerCameraLight";
|
|
383
|
+
const dirLightName = "PrefViewerDirLight";
|
|
384
|
+
|
|
385
|
+
const hemiLight = this.#scene.getLightByName(hemiLightName);
|
|
386
|
+
const cameraLight = this.#scene.getLightByName(cameraLightName);
|
|
387
|
+
const dirLight = this.#scene.getLightByName(dirLightName);
|
|
388
|
+
|
|
389
|
+
let lightsChanged = false;
|
|
390
|
+
|
|
391
|
+
const iblEnabled = this.#settings.iblEnabled && this.#options.ibl && this.#options.ibl.cachedUrl !== null;
|
|
392
|
+
|
|
393
|
+
if (iblEnabled) {
|
|
394
|
+
if (hemiLight) {
|
|
395
|
+
hemiLight.dispose();
|
|
396
|
+
}
|
|
397
|
+
if (cameraLight) {
|
|
398
|
+
cameraLight.dispose();
|
|
399
|
+
}
|
|
400
|
+
if (dirLight) {
|
|
401
|
+
dirLight.dispose();
|
|
402
|
+
}
|
|
383
403
|
this.#hemiLight = this.#dirLight = this.#cameraLight = null;
|
|
384
|
-
this.#initializeEnvironmentTexture();
|
|
404
|
+
lightsChanged = await this.#initializeEnvironmentTexture();
|
|
405
|
+
} else {
|
|
406
|
+
// If IBL is disabled but an environment texture exists, dispose it to save resources and ensure it doesn't affect the lighting
|
|
407
|
+
if (this.#scene.environmentTexture) {
|
|
408
|
+
this.#scene.environmentTexture.dispose();
|
|
409
|
+
this.#scene.environmentTexture = null;
|
|
410
|
+
lightsChanged = true;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// Add a hemispheric light for basic ambient illumination
|
|
414
|
+
if (!this.#hemiLight) {
|
|
415
|
+
this.#hemiLight = new HemisphericLight(hemiLightName, new Vector3(-10, 10, -10), this.#scene);
|
|
416
|
+
this.#hemiLight.intensity = 0.6;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// Add a directional light to cast shadows and provide stronger directional illumination
|
|
420
|
+
if (!this.#dirLight) {
|
|
421
|
+
this.#dirLight = new DirectionalLight(dirLightName, new Vector3(-10, 10, -10), this.#scene);
|
|
422
|
+
this.#dirLight.position = new Vector3(5, 4, 5); // light is IN FRONT + ABOVE + to the RIGHT
|
|
423
|
+
this.#dirLight.intensity = 0.6;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// Add a point light that follows the camera to ensure the model is always well-lit from the viewer's perspective
|
|
427
|
+
if (!this.#cameraLight) {
|
|
428
|
+
this.#cameraLight = new PointLight(cameraLightName, this.#camera.position, this.#scene);
|
|
429
|
+
this.#cameraLight.parent = this.#camera;
|
|
430
|
+
this.#cameraLight.intensity = 0.3;
|
|
431
|
+
}
|
|
385
432
|
}
|
|
433
|
+
return lightsChanged;
|
|
434
|
+
}
|
|
386
435
|
|
|
387
|
-
|
|
388
|
-
|
|
436
|
+
/**
|
|
437
|
+
* Detaches and disposes the SSAO render pipeline from the active camera when it exists.
|
|
438
|
+
* Guards against missing scene resources or absent pipelines, returning false when no cleanup is needed.
|
|
439
|
+
* @private
|
|
440
|
+
* @returns {Promise<boolean>} Returns true when the SSAO pipeline was disabled, false otherwise.
|
|
441
|
+
*/
|
|
442
|
+
async #disableAmbientOcclusion() {
|
|
443
|
+
const pipelineManager = this.#scene?.postProcessRenderPipelineManager;
|
|
444
|
+
|
|
445
|
+
if (!this.#scene || !pipelineManager || this.#scene?.activeCamera === null) {
|
|
446
|
+
return false;
|
|
389
447
|
}
|
|
390
448
|
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
if (
|
|
394
|
-
|
|
395
|
-
this.#hemiLight.intensity = 0.6;
|
|
449
|
+
const supportedPipelines = pipelineManager.supportedPipelines;
|
|
450
|
+
|
|
451
|
+
if (supportedPipelines === undefined) {
|
|
452
|
+
return false;
|
|
396
453
|
}
|
|
397
454
|
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
if (!this.#dirLight) {
|
|
401
|
-
this.#dirLight = new DirectionalLight("PrefViewerDirLight", new Vector3(-10, 10, -10), this.#scene);
|
|
402
|
-
this.#dirLight.position = new Vector3(5, 4, 5); // light is IN FRONT + ABOVE + to the RIGHT
|
|
403
|
-
this.#dirLight.intensity = 0.6;
|
|
455
|
+
if (!this.#renderPipelines.ssao || !this.#renderPipelines.ssao?.name) {
|
|
456
|
+
return false;
|
|
404
457
|
}
|
|
405
458
|
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
459
|
+
const pipelineName = this.#renderPipelines.ssao.name;
|
|
460
|
+
let ssaoPipeline = supportedPipelines ? supportedPipelines.find((pipeline) => pipeline.name === pipelineName) : false;
|
|
461
|
+
|
|
462
|
+
if (ssaoPipeline) {
|
|
463
|
+
pipelineManager.detachCamerasFromRenderPipeline(pipelineName, [this.#scene.activeCamera]);
|
|
464
|
+
pipelineManager.removePipeline(pipelineName);
|
|
465
|
+
pipelineManager.update();
|
|
466
|
+
ssaoPipeline.dispose();
|
|
467
|
+
this.#renderPipelines.ssao = ssaoPipeline = null;
|
|
412
468
|
}
|
|
469
|
+
|
|
470
|
+
return true;
|
|
413
471
|
}
|
|
414
472
|
|
|
415
473
|
/**
|
|
@@ -417,38 +475,31 @@ export default class BabylonJSController {
|
|
|
417
475
|
* Disposes previous SSAO pipelines, instantiates a tuned `SSAORenderingPipeline`, and attaches it to the
|
|
418
476
|
* current camera so contact shadows enhance depth perception once assets reload or the camera changes.
|
|
419
477
|
* @private
|
|
420
|
-
* @returns {boolean} True if the SSAO pipeline is supported and enabled, otherwise false.
|
|
478
|
+
* @returns {Promise<boolean>} True if the SSAO pipeline is supported and enabled, otherwise false.
|
|
421
479
|
*/
|
|
422
|
-
#initializeAmbientOcclussion() {
|
|
423
|
-
|
|
480
|
+
async #initializeAmbientOcclussion() {
|
|
481
|
+
const pipelineManager = this.#scene?.postProcessRenderPipelineManager;
|
|
482
|
+
if (!this.#scene || !pipelineManager || this.#scene?.activeCamera === null) {
|
|
424
483
|
return false;
|
|
425
484
|
}
|
|
426
485
|
|
|
427
486
|
if (!this.#settings.ambientOcclusionEnabled) {
|
|
428
487
|
return false;
|
|
429
488
|
}
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
const supportedPipelines = this.#scene.postProcessRenderPipelineManager.supportedPipelines;
|
|
434
|
-
|
|
435
|
-
if (!supportedPipelines) {
|
|
489
|
+
const supportedPipelines = pipelineManager.supportedPipelines;
|
|
490
|
+
|
|
491
|
+
if (supportedPipelines === undefined) {
|
|
436
492
|
return false;
|
|
437
493
|
}
|
|
438
|
-
|
|
439
|
-
const
|
|
440
|
-
|
|
441
|
-
if (oldSsaoPipeline) {
|
|
442
|
-
oldSsaoPipeline.dispose();
|
|
443
|
-
this.#scene.postProcessRenderPipelineManager.update();
|
|
444
|
-
}
|
|
494
|
+
|
|
495
|
+
const pipelineName = "PrefViewerSSAORenderingPipeline";
|
|
445
496
|
|
|
446
497
|
const ssaoRatio = {
|
|
447
498
|
ssaoRatio: 0.5,
|
|
448
499
|
combineRatio: 1.0
|
|
449
500
|
};
|
|
450
501
|
|
|
451
|
-
|
|
502
|
+
let ssaoPipeline = new SSAORenderingPipeline(pipelineName, this.#scene, ssaoRatio, [this.#scene.activeCamera]);
|
|
452
503
|
|
|
453
504
|
if (!ssaoPipeline){
|
|
454
505
|
return false;
|
|
@@ -466,48 +517,85 @@ export default class BabylonJSController {
|
|
|
466
517
|
ssaoPipeline._ssaoPostProcess.autoClear = false;
|
|
467
518
|
ssaoPipeline._ssaoPostProcess.samples = 1;
|
|
468
519
|
}
|
|
520
|
+
if (ssaoPipeline._combinePostProcess) {
|
|
521
|
+
ssaoPipeline._combinePostProcess.autoClear = false;
|
|
522
|
+
ssaoPipeline._combinePostProcess.samples = 1;
|
|
523
|
+
}
|
|
469
524
|
|
|
470
|
-
this.#
|
|
525
|
+
this.#renderPipelines.ssao = ssaoPipeline;
|
|
526
|
+
pipelineManager.update();
|
|
471
527
|
return true;
|
|
472
528
|
} else {
|
|
473
529
|
ssaoPipeline.dispose();
|
|
474
|
-
this.#
|
|
530
|
+
this.#renderPipelines.ssao = ssaoPipeline = null;
|
|
531
|
+
pipelineManager.update();
|
|
475
532
|
return false;
|
|
476
533
|
}
|
|
477
534
|
}
|
|
478
535
|
|
|
479
536
|
/**
|
|
480
|
-
*
|
|
481
|
-
*
|
|
482
|
-
* `DefaultRenderingPipeline` with tuned settings for sharper anti-aliasing and subtle grain.
|
|
537
|
+
* Tears down the default rendering pipeline (MSAA/FXAA/grain) for the active camera when present.
|
|
538
|
+
* Ensures stale pipelines detach cleanly so a fresh one can be installed on the next load.
|
|
483
539
|
* @private
|
|
484
|
-
* @returns {boolean}
|
|
485
|
-
* @see {@link https://doc.babylonjs.com/features/featuresDeepDive/postProcesses/defaultRenderingPipeline|Using the Default Rendering Pipeline | Babylon.js Documentation}
|
|
540
|
+
* @returns {Promise<boolean>} Returns true when the pipeline was removed, false otherwise.
|
|
486
541
|
*/
|
|
487
|
-
#
|
|
488
|
-
|
|
542
|
+
async #disableVisualImprovements() {
|
|
543
|
+
const pipelineManager = this.#scene?.postProcessRenderPipelineManager;
|
|
544
|
+
|
|
545
|
+
if (!this.#scene || !pipelineManager || this.#scene?.activeCamera === null) {
|
|
489
546
|
return false;
|
|
490
547
|
}
|
|
491
548
|
|
|
492
|
-
const
|
|
493
|
-
|
|
549
|
+
const supportedPipelines = pipelineManager.supportedPipelines;
|
|
550
|
+
|
|
551
|
+
if (supportedPipelines === undefined) {
|
|
552
|
+
return false;
|
|
553
|
+
}
|
|
494
554
|
|
|
495
|
-
if (!
|
|
555
|
+
if (!this.#renderPipelines.default || !this.#renderPipelines.default?.name) {
|
|
496
556
|
return false;
|
|
497
557
|
}
|
|
498
558
|
|
|
499
|
-
const
|
|
559
|
+
const pipelineName = this.#renderPipelines.default.name;
|
|
560
|
+
let defaultPipeline = supportedPipelines ? supportedPipelines.find((pipeline) => pipeline.name === pipelineName) : false;
|
|
500
561
|
|
|
501
|
-
if (
|
|
502
|
-
|
|
503
|
-
|
|
562
|
+
if (defaultPipeline) {
|
|
563
|
+
pipelineManager.detachCamerasFromRenderPipeline(pipelineName, [this.#scene.activeCamera]);
|
|
564
|
+
pipelineManager.removePipeline(pipelineName);
|
|
565
|
+
pipelineManager.update();
|
|
566
|
+
defaultPipeline.dispose();
|
|
567
|
+
this.#renderPipelines.default = defaultPipeline = null;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
return true;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
/**
|
|
574
|
+
* Rebuilds the custom default rendering pipeline (MSAA, FXAA, film grain) for the active camera.
|
|
575
|
+
* Disposes any previous pipeline instance to avoid duplicates, then attaches a fresh
|
|
576
|
+
* `DefaultRenderingPipeline` with tuned settings for sharper anti-aliasing and subtle grain.
|
|
577
|
+
* @private
|
|
578
|
+
* @returns {Promise<boolean>} True when the pipeline is supported and active, otherwise false.
|
|
579
|
+
* @see {@link https://doc.babylonjs.com/features/featuresDeepDive/postProcesses/defaultRenderingPipeline|Using the Default Rendering Pipeline | Babylon.js Documentation}
|
|
580
|
+
*/
|
|
581
|
+
async #initializeVisualImprovements() {
|
|
582
|
+
const pipelineManager = this.#scene?.postProcessRenderPipelineManager;
|
|
583
|
+
if (!this.#scene || !pipelineManager || this.#scene?.activeCamera === null) {
|
|
584
|
+
return false;
|
|
504
585
|
}
|
|
505
586
|
|
|
506
587
|
if (!this.#settings.antiAliasingEnabled) {
|
|
507
588
|
return false;
|
|
508
589
|
}
|
|
590
|
+
const supportedPipelines = pipelineManager.supportedPipelines;
|
|
591
|
+
|
|
592
|
+
if (supportedPipelines === undefined) {
|
|
593
|
+
return false;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
const pipelineName = "PrefViewerDefaultRenderingPipeline";
|
|
509
597
|
|
|
510
|
-
|
|
598
|
+
let defaultPipeline = new DefaultRenderingPipeline(pipelineName, true, this.#scene, [this.#scene.activeCamera], true);
|
|
511
599
|
|
|
512
600
|
if (!defaultPipeline){
|
|
513
601
|
return false;
|
|
@@ -518,7 +606,6 @@ export default class BabylonJSController {
|
|
|
518
606
|
const caps = this.#scene.getEngine()?.getCaps?.() || {};
|
|
519
607
|
const maxSamples = typeof caps.maxMSAASamples === "number" ? caps.maxMSAASamples : 4;
|
|
520
608
|
defaultPipeline.samples = Math.max(1, Math.min(8, maxSamples));
|
|
521
|
-
|
|
522
609
|
// FXAA - Fast Approximate Anti-Aliasing
|
|
523
610
|
defaultPipeline.fxaaEnabled = true;
|
|
524
611
|
defaultPipeline.fxaa.samples = 8;
|
|
@@ -547,11 +634,13 @@ export default class BabylonJSController {
|
|
|
547
634
|
defaultPipeline.grain._postProcess.autoClear = false;
|
|
548
635
|
}
|
|
549
636
|
|
|
550
|
-
this.#
|
|
637
|
+
this.#renderPipelines.default = defaultPipeline;
|
|
638
|
+
pipelineManager.update();
|
|
551
639
|
return true;
|
|
552
640
|
} else {
|
|
553
641
|
defaultPipeline.dispose();
|
|
554
|
-
this.#
|
|
642
|
+
this.#renderPipelines.default = defaultPipeline = null;
|
|
643
|
+
pipelineManager.update();
|
|
555
644
|
return false;
|
|
556
645
|
}
|
|
557
646
|
}
|
|
@@ -561,13 +650,60 @@ export default class BabylonJSController {
|
|
|
561
650
|
* Loads an HDR texture from a predefined URI and assigns it to the scene's environmentTexture property.
|
|
562
651
|
* Configures gamma space, mipmaps, and intensity level for realistic lighting.
|
|
563
652
|
* @private
|
|
564
|
-
* @returns {boolean}
|
|
653
|
+
* @returns {Promise<boolean>} Returns true if the environment texture was changed, false if it was already up to date or failed to load.
|
|
565
654
|
*/
|
|
566
|
-
#initializeEnvironmentTexture() {
|
|
567
|
-
|
|
568
|
-
|
|
655
|
+
async #initializeEnvironmentTexture() {
|
|
656
|
+
if (this.#scene.environmentTexture) {
|
|
657
|
+
this.#scene.environmentTexture.dispose();
|
|
658
|
+
this.#scene.environmentTexture = null;
|
|
659
|
+
}
|
|
660
|
+
const hdrTextureURI = this.#options.ibl.cachedUrl;
|
|
661
|
+
const hdrTexture = new HDRCubeTexture(hdrTextureURI, this.#scene, 1024, false, false, false, true, undefined, undefined, false, true, true);
|
|
662
|
+
await WhenTextureReadyAsync(hdrTexture);
|
|
569
663
|
hdrTexture.level = this.#options.ibl.intensity;
|
|
570
664
|
this.#scene.environmentTexture = hdrTexture;
|
|
665
|
+
this.#scene.markAllMaterialsAsDirty(Material.TextureDirtyFlag);
|
|
666
|
+
return true;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
/**
|
|
670
|
+
* Removes the IBL shadow render pipeline from the active camera when present.
|
|
671
|
+
* Ensures voxelized shadow data is disposed so reloading environments installs a clean pipeline.
|
|
672
|
+
* @private
|
|
673
|
+
* @returns {Promise<boolean>} Returns true when the pipeline was removed, false otherwise.
|
|
674
|
+
*/
|
|
675
|
+
async #disableIBLShadows() {
|
|
676
|
+
const pipelineManager = this.#scene?.postProcessRenderPipelineManager;
|
|
677
|
+
|
|
678
|
+
if (!this.#scene || !pipelineManager || this.#scene?.activeCamera === null) {
|
|
679
|
+
return false;
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
const supportedPipelines = pipelineManager.supportedPipelines;
|
|
683
|
+
|
|
684
|
+
if (supportedPipelines === undefined) {
|
|
685
|
+
return false;
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
if (!this.#renderPipelines.iblShadows || !this.#renderPipelines.iblShadows?.name) {
|
|
689
|
+
return false;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
const pipelineName = this.#renderPipelines.iblShadows.name;
|
|
693
|
+
let iblShadowsPipeline = supportedPipelines ? supportedPipelines.find((pipeline) => pipeline.name === pipelineName) : false;
|
|
694
|
+
|
|
695
|
+
if (iblShadowsPipeline) {
|
|
696
|
+
iblShadowsPipeline.toggleShadow(false);
|
|
697
|
+
iblShadowsPipeline.clearShadowCastingMeshes();
|
|
698
|
+
iblShadowsPipeline.clearShadowReceivingMaterials();
|
|
699
|
+
iblShadowsPipeline.resetAccumulation();
|
|
700
|
+
pipelineManager.detachCamerasFromRenderPipeline(pipelineName, [this.#scene.activeCamera]);
|
|
701
|
+
pipelineManager.removePipeline(pipelineName);
|
|
702
|
+
pipelineManager.update();
|
|
703
|
+
iblShadowsPipeline.dispose();
|
|
704
|
+
this.#renderPipelines.iblShadows = iblShadowsPipeline = null;
|
|
705
|
+
}
|
|
706
|
+
|
|
571
707
|
return true;
|
|
572
708
|
}
|
|
573
709
|
|
|
@@ -577,30 +713,37 @@ export default class BabylonJSController {
|
|
|
577
713
|
* Configures pipeline options for resolution, sampling, opacity, and debugging.
|
|
578
714
|
* Only applies if the scene has an environment texture set.
|
|
579
715
|
* @private
|
|
580
|
-
* @returns {void|
|
|
716
|
+
* @returns {Promise<void|boolean>} Returns false if no environment texture is set; otherwise void.
|
|
581
717
|
*/
|
|
582
|
-
#initializeIBLShadows() {
|
|
583
|
-
|
|
718
|
+
async #initializeIBLShadows() {
|
|
719
|
+
|
|
720
|
+
await this.#scene.whenReadyAsync();
|
|
721
|
+
|
|
722
|
+
const pipelineManager = this.#scene?.postProcessRenderPipelineManager;
|
|
723
|
+
|
|
724
|
+
if (!this.#scene || !pipelineManager || this.#scene?.activeCamera === null) {
|
|
584
725
|
return false;
|
|
585
726
|
}
|
|
586
|
-
|
|
587
|
-
// if (!this.#scene.environmentTexture || !this.#scene.environmentTexture.isReady()) {
|
|
727
|
+
|
|
588
728
|
if (!this.#scene.environmentTexture) {
|
|
589
729
|
return false;
|
|
590
730
|
}
|
|
591
|
-
const pipelineName = "PrefViewerIblShadowsRenderPipeline";
|
|
592
|
-
const supportedPipelines = this.#scene.postProcessRenderPipelineManager.supportedPipelines;
|
|
593
731
|
|
|
732
|
+
if (!this.#scene.environmentTexture.isReady()) {
|
|
733
|
+
const self = this;
|
|
734
|
+
this.#scene.environmentTexture.onLoadObservable.addOnce(() => {
|
|
735
|
+
self.#initializeIBLShadows();
|
|
736
|
+
});
|
|
737
|
+
return false;
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
const supportedPipelines = pipelineManager.supportedPipelines;
|
|
741
|
+
|
|
594
742
|
if (!supportedPipelines) {
|
|
595
743
|
return false;
|
|
596
744
|
}
|
|
597
745
|
|
|
598
|
-
const
|
|
599
|
-
|
|
600
|
-
if (oldIblShadowsRenderPipeline) {
|
|
601
|
-
oldIblShadowsRenderPipeline.dispose();
|
|
602
|
-
this.#scene.postProcessRenderPipelineManager.update();
|
|
603
|
-
}
|
|
746
|
+
const pipelineName = "PrefViewerIblShadowsRenderPipeline";
|
|
604
747
|
|
|
605
748
|
const pipelineOptions = {
|
|
606
749
|
resolutionExp: 1, // Higher resolution for better shadow quality (recomended 8)
|
|
@@ -611,13 +754,13 @@ export default class BabylonJSController {
|
|
|
611
754
|
shadowOpacity: 0.85,
|
|
612
755
|
};
|
|
613
756
|
|
|
614
|
-
|
|
757
|
+
let iblShadowsPipeline = new IblShadowsRenderPipeline(pipelineName, this.#scene, pipelineOptions, [this.#scene.activeCamera]);
|
|
615
758
|
|
|
616
|
-
if (!
|
|
759
|
+
if (!iblShadowsPipeline) {
|
|
617
760
|
return false;
|
|
618
761
|
}
|
|
619
762
|
|
|
620
|
-
if (
|
|
763
|
+
if (iblShadowsPipeline.isSupported) {
|
|
621
764
|
// Disable all debug passes for performance
|
|
622
765
|
const pipelineProps = {
|
|
623
766
|
allowDebugPasses: false,
|
|
@@ -631,13 +774,8 @@ export default class BabylonJSController {
|
|
|
631
774
|
accumulationPassDebugEnabled: false,
|
|
632
775
|
};
|
|
633
776
|
|
|
634
|
-
Object.assign(
|
|
635
|
-
|
|
636
|
-
if (iblShadowsRenderPipeline._ssaoPostProcess) {
|
|
637
|
-
iblShadowsRenderPipeline._ssaoPostProcess.autoClear = false;
|
|
638
|
-
iblShadowsRenderPipeline._ssaoPostProcess.samples = 1;
|
|
639
|
-
}
|
|
640
|
-
|
|
777
|
+
Object.assign(iblShadowsPipeline, pipelineProps);
|
|
778
|
+
|
|
641
779
|
this.#scene.meshes.forEach((mesh) => {
|
|
642
780
|
const isRootMesh = mesh.id.startsWith("__root__");
|
|
643
781
|
if (isRootMesh) {
|
|
@@ -647,26 +785,29 @@ export default class BabylonJSController {
|
|
|
647
785
|
const isHDRIMesh = mesh.name?.toLowerCase() === "hdri";
|
|
648
786
|
const extrasCastShadows = mesh.metadata?.gltf?.extras?.castShadows;
|
|
649
787
|
const meshGenerateShadows = typeof extrasCastShadows === "boolean" ? extrasCastShadows : isHDRIMesh ? false : true;
|
|
650
|
-
|
|
788
|
+
|
|
651
789
|
if (meshGenerateShadows) {
|
|
652
|
-
|
|
653
|
-
|
|
790
|
+
iblShadowsPipeline.addShadowCastingMesh(mesh);
|
|
791
|
+
iblShadowsPipeline.updateSceneBounds();
|
|
654
792
|
}
|
|
655
793
|
});
|
|
656
|
-
|
|
794
|
+
|
|
657
795
|
this.#scene.materials.forEach((material) => {
|
|
658
796
|
if (material instanceof PBRMaterial) {
|
|
659
797
|
material.enableSpecularAntiAliasing = false;
|
|
660
798
|
}
|
|
661
|
-
|
|
799
|
+
iblShadowsPipeline.addShadowReceivingMaterial(material);
|
|
662
800
|
});
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
801
|
+
|
|
802
|
+
iblShadowsPipeline.toggleShadow(true);
|
|
803
|
+
iblShadowsPipeline.updateVoxelization();
|
|
804
|
+
this.#renderPipelines.iblShadows = iblShadowsPipeline;
|
|
805
|
+
pipelineManager.update();
|
|
666
806
|
return true;
|
|
667
807
|
} else {
|
|
668
|
-
|
|
669
|
-
this.#
|
|
808
|
+
iblShadowsPipeline.dispose();
|
|
809
|
+
this.#renderPipelines.iblShadows = iblShadowsPipeline = null;
|
|
810
|
+
pipelineManager.update();
|
|
670
811
|
return false;
|
|
671
812
|
}
|
|
672
813
|
}
|
|
@@ -699,8 +840,7 @@ export default class BabylonJSController {
|
|
|
699
840
|
* @private
|
|
700
841
|
* @returns {void}
|
|
701
842
|
*/
|
|
702
|
-
#initializeDefaultLightShadows() {
|
|
703
|
-
this.#shadowGen = [];
|
|
843
|
+
async #initializeDefaultLightShadows() {
|
|
704
844
|
if (!this.#dirLight) {
|
|
705
845
|
return;
|
|
706
846
|
}
|
|
@@ -723,7 +863,7 @@ export default class BabylonJSController {
|
|
|
723
863
|
* @private
|
|
724
864
|
* @returns {void}
|
|
725
865
|
*/
|
|
726
|
-
#initializeEnvironmentShadows() {
|
|
866
|
+
async #initializeEnvironmentShadows() {
|
|
727
867
|
this.#shadowGen = this.#shadowGen.filter((generator) => {
|
|
728
868
|
if (!generator || typeof generator.getLight !== "function") {
|
|
729
869
|
return false;
|
|
@@ -766,6 +906,20 @@ export default class BabylonJSController {
|
|
|
766
906
|
});
|
|
767
907
|
}
|
|
768
908
|
|
|
909
|
+
/**
|
|
910
|
+
* Disposes every active shadow generator plus the IBL shadow pipeline to avoid stale casters across reloads.
|
|
911
|
+
* Clears the cached `#shadowGen` array so subsequent loads can rebuild fresh generators.
|
|
912
|
+
* @private
|
|
913
|
+
* @returns {void}
|
|
914
|
+
*/
|
|
915
|
+
async #disableShadows() {
|
|
916
|
+
this.#shadowGen.forEach((shadowGenerator) => {
|
|
917
|
+
shadowGenerator.dispose();
|
|
918
|
+
});
|
|
919
|
+
this.#shadowGen = [];
|
|
920
|
+
await this.#disableIBLShadows();
|
|
921
|
+
}
|
|
922
|
+
|
|
769
923
|
/**
|
|
770
924
|
* Initializes shadows for the Babylon.js scene.
|
|
771
925
|
* @private
|
|
@@ -774,28 +928,22 @@ export default class BabylonJSController {
|
|
|
774
928
|
* If no environment texture is set, initializes IBL shadows.
|
|
775
929
|
* Otherwise, sets up shadow casting and receiving for all relevant meshes using the shadow generator.
|
|
776
930
|
*/
|
|
777
|
-
#initializeShadows() {
|
|
931
|
+
async #initializeShadows() {
|
|
778
932
|
if (!this.#settings.shadowsEnabled) {
|
|
779
933
|
return false;
|
|
780
934
|
}
|
|
781
935
|
|
|
782
936
|
this.#ensureMeshesReceiveShadows();
|
|
783
937
|
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
const self = this;
|
|
790
|
-
this.#scene.environmentTexture.onLoadObservable.addOnce(() => {
|
|
791
|
-
self.#initializeIBLShadows();
|
|
792
|
-
});
|
|
793
|
-
}
|
|
794
|
-
}
|
|
938
|
+
const iblEnabled = this.#settings.iblEnabled && this.#options.ibl && this.#options.ibl.cachedUrl !== null;
|
|
939
|
+
const iblShadowsEnabled = iblEnabled && this.#options.ibl.shadows;
|
|
940
|
+
|
|
941
|
+
if (iblShadowsEnabled) {
|
|
942
|
+
await this.#initializeIBLShadows();
|
|
795
943
|
} else {
|
|
796
|
-
this.#initializeDefaultLightShadows();
|
|
944
|
+
await this.#initializeDefaultLightShadows();
|
|
797
945
|
}
|
|
798
|
-
this.#initializeEnvironmentShadows();
|
|
946
|
+
await this.#initializeEnvironmentShadows();
|
|
799
947
|
}
|
|
800
948
|
|
|
801
949
|
/**
|
|
@@ -913,7 +1061,6 @@ export default class BabylonJSController {
|
|
|
913
1061
|
this.#engine.dispose();
|
|
914
1062
|
this.#engine = this.#scene = this.#camera = null;
|
|
915
1063
|
this.#hemiLight = this.#dirLight = this.#cameraLight = null;
|
|
916
|
-
this.#shadowGen = [];
|
|
917
1064
|
}
|
|
918
1065
|
|
|
919
1066
|
/**
|
|
@@ -1129,14 +1276,8 @@ export default class BabylonJSController {
|
|
|
1129
1276
|
* @private
|
|
1130
1277
|
* @returns {boolean} True when lights were refreshed due to pending IBL changes, otherwise false.
|
|
1131
1278
|
*/
|
|
1132
|
-
#setOptions_IBL() {
|
|
1133
|
-
|
|
1134
|
-
this.#options.ibl.setSuccess(true);
|
|
1135
|
-
this.#createLights();
|
|
1136
|
-
return true;
|
|
1137
|
-
}
|
|
1138
|
-
this.#createLights();
|
|
1139
|
-
return false;
|
|
1279
|
+
async #setOptions_IBL() {
|
|
1280
|
+
return await this.#createLights();
|
|
1140
1281
|
}
|
|
1141
1282
|
|
|
1142
1283
|
/**
|
|
@@ -1258,6 +1399,52 @@ export default class BabylonJSController {
|
|
|
1258
1399
|
return this.#addContainer(container, false);
|
|
1259
1400
|
}
|
|
1260
1401
|
|
|
1402
|
+
/**
|
|
1403
|
+
* Stops every animation group on the provided asset container to guarantee new loads start from a clean state.
|
|
1404
|
+
* @private
|
|
1405
|
+
* @param {AssetContainer} assetContainer - Container whose animation groups should be halted.
|
|
1406
|
+
*/
|
|
1407
|
+
#assetContainer_stopAnimations(assetContainer) {
|
|
1408
|
+
if (!assetContainer.animationGroups || assetContainer.animationGroups.length === 0) {
|
|
1409
|
+
return;
|
|
1410
|
+
}
|
|
1411
|
+
assetContainer.animationGroups.forEach((animationGroup) => {
|
|
1412
|
+
animationGroup.stop();
|
|
1413
|
+
});
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1416
|
+
/**
|
|
1417
|
+
* Disposes every imported light so subsequent reloads avoid duplicating scene illumination.
|
|
1418
|
+
* @private
|
|
1419
|
+
* @param {AssetContainer} assetContainer - Container whose lights should be cleaned up.
|
|
1420
|
+
* @returns {void}
|
|
1421
|
+
*/
|
|
1422
|
+
#assetContainer_deleteLights(assetContainer) {
|
|
1423
|
+
if (!assetContainer.lights || assetContainer.lights.length === 0) {
|
|
1424
|
+
return;
|
|
1425
|
+
}
|
|
1426
|
+
assetContainer.lights.forEach((light) => {
|
|
1427
|
+
light.dispose();
|
|
1428
|
+
});
|
|
1429
|
+
assetContainer.lights = [];
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1432
|
+
/**
|
|
1433
|
+
* Assigns unique ids to every imported camera so Babylon.js does not reuse stale SSAO effects between reloads.
|
|
1434
|
+
* @private
|
|
1435
|
+
* @param {AssetContainer} assetContainer - Container whose cameras need deterministic id regeneration.
|
|
1436
|
+
* @returns {void}
|
|
1437
|
+
*/
|
|
1438
|
+
#assetContainer_retagCameras(assetContainer) {
|
|
1439
|
+
if (!assetContainer.cameras || assetContainer.cameras.length === 0) {
|
|
1440
|
+
return;
|
|
1441
|
+
}
|
|
1442
|
+
assetContainer.cameras.forEach((camera) => {
|
|
1443
|
+
const sufix = "_" + Date.now();
|
|
1444
|
+
camera.id = `${camera.id || camera.name || "camera"}${sufix}`;
|
|
1445
|
+
});
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1261
1448
|
/**
|
|
1262
1449
|
* Sets the visibility of wall and floor meshes in the model container based on the provided value or environment visibility.
|
|
1263
1450
|
* @private
|
|
@@ -1279,8 +1466,9 @@ export default class BabylonJSController {
|
|
|
1279
1466
|
* @private
|
|
1280
1467
|
* @returns {void}
|
|
1281
1468
|
*/
|
|
1282
|
-
#stopRender() {
|
|
1469
|
+
async #stopRender() {
|
|
1283
1470
|
this.#engine.stopRenderLoop(this.#handlers.renderLoop);
|
|
1471
|
+
await this.#unloadCameraDependentEffects();
|
|
1284
1472
|
}
|
|
1285
1473
|
/**
|
|
1286
1474
|
* Starts the Babylon.js render loop for the current scene.
|
|
@@ -1289,20 +1477,30 @@ export default class BabylonJSController {
|
|
|
1289
1477
|
* @returns {Promise<void>}
|
|
1290
1478
|
*/
|
|
1291
1479
|
async #startRender() {
|
|
1292
|
-
await this.#
|
|
1293
|
-
this.#
|
|
1480
|
+
await this.#loadCameraDependentEffects();
|
|
1481
|
+
this.#scene.executeWhenReady(() => {
|
|
1482
|
+
this.#engine.runRenderLoop(this.#handlers.renderLoop);
|
|
1483
|
+
});
|
|
1294
1484
|
}
|
|
1295
1485
|
|
|
1296
1486
|
/**
|
|
1297
|
-
* Loads
|
|
1487
|
+
* Loads a single asset container (model, environment, materials, etc.) based on the container state flags.
|
|
1488
|
+
* Skips work when nothing is pending, otherwise resolves the GLTF source, refreshes cache metadata and streams it
|
|
1489
|
+
* into the Babylon.js scene via `LoadAssetContainerAsync`.
|
|
1298
1490
|
* @private
|
|
1299
|
-
* @param {object} container -
|
|
1300
|
-
* @
|
|
1491
|
+
* @param {object} container - Container descriptor that carries the GLTF storage pointer and current cache info.
|
|
1492
|
+
* @param {boolean} [force=false] - When true, bypasses cached size/timestamp so the resolver re-downloads the asset.
|
|
1493
|
+
* @returns {Promise<[object, AssetContainer|boolean]>} Resolves to `[container, assetContainer]` on success, or
|
|
1494
|
+
* `[container, false]` when loading was skipped or failed.
|
|
1301
1495
|
* @description
|
|
1302
|
-
*
|
|
1303
|
-
*
|
|
1496
|
+
* 1. Validates that the container has pending data and initializes the shared `GLTFResolver` if needed.
|
|
1497
|
+
* 2. Requests the source blob (respecting the cached size/timestamp unless `force` is set) and stores the new cache
|
|
1498
|
+
* metadata via `setPendingCacheData`.
|
|
1499
|
+
* 3. Builds the Babylon plugin options so extras are surfaced as metadata, then imports the container with
|
|
1500
|
+
* `LoadAssetContainerAsync`, returning the tuple so the caller can decide how to attach it to the scene.
|
|
1304
1501
|
*/
|
|
1305
|
-
async #loadAssetContainer(container) {
|
|
1502
|
+
async #loadAssetContainer(container, force = false) {
|
|
1503
|
+
|
|
1306
1504
|
if (container?.state?.update?.storage === undefined || container?.state?.size === undefined || container?.state?.timeStamp === undefined) {
|
|
1307
1505
|
return [container, false];
|
|
1308
1506
|
}
|
|
@@ -1314,9 +1512,12 @@ export default class BabylonJSController {
|
|
|
1314
1512
|
if (!this.#gltfResolver) {
|
|
1315
1513
|
this.#gltfResolver = new GLTFResolver();
|
|
1316
1514
|
}
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1515
|
+
|
|
1516
|
+
const currentSize = force ? 0 : container.state.size;
|
|
1517
|
+
const currentTimeStamp = force ? null : container.state.timeStamp;
|
|
1518
|
+
|
|
1519
|
+
let sourceData = await this.#gltfResolver.getSource(container.state.update.storage, currentSize, currentTimeStamp);
|
|
1520
|
+
|
|
1320
1521
|
if (!sourceData) {
|
|
1321
1522
|
return [container, false];
|
|
1322
1523
|
}
|
|
@@ -1358,8 +1559,7 @@ export default class BabylonJSController {
|
|
|
1358
1559
|
* Returns an object with success status and error details.
|
|
1359
1560
|
*/
|
|
1360
1561
|
async #loadContainers() {
|
|
1361
|
-
this.#stopRender();
|
|
1362
|
-
this.#scene.postProcessRenderPipelineManager?.dispose();
|
|
1562
|
+
await this.#stopRender();
|
|
1363
1563
|
|
|
1364
1564
|
let oldModelMetadata = { ...(this.#containers.model?.state?.metadata ?? {}) };
|
|
1365
1565
|
let newModelMetadata = {};
|
|
@@ -1375,16 +1575,20 @@ export default class BabylonJSController {
|
|
|
1375
1575
|
};
|
|
1376
1576
|
|
|
1377
1577
|
await Promise.allSettled(promiseArray)
|
|
1378
|
-
.then((values) => {
|
|
1578
|
+
.then(async (values) => {
|
|
1379
1579
|
this.#disposeAnimationController();
|
|
1380
1580
|
values.forEach((result) => {
|
|
1381
1581
|
const container = result.value ? result.value[0] : null;
|
|
1382
1582
|
const assetContainer = result.value ? result.value[1] : null;
|
|
1383
1583
|
if (result.status === "fulfilled" && assetContainer) {
|
|
1384
1584
|
if (container.state.name === "model") {
|
|
1385
|
-
assetContainer
|
|
1585
|
+
this.#assetContainer_deleteLights(assetContainer);
|
|
1586
|
+
this.#assetContainer_stopAnimations(assetContainer);
|
|
1386
1587
|
newModelMetadata = { ...(container.state.update.metadata ?? {}) };
|
|
1387
1588
|
}
|
|
1589
|
+
if (container.state.name === "model" || container.state.name === "environment") {
|
|
1590
|
+
this.#assetContainer_retagCameras(assetContainer);
|
|
1591
|
+
}
|
|
1388
1592
|
this.#replaceContainer(container, assetContainer);
|
|
1389
1593
|
container.state.setSuccess(true);
|
|
1390
1594
|
} else {
|
|
@@ -1397,7 +1601,7 @@ export default class BabylonJSController {
|
|
|
1397
1601
|
|
|
1398
1602
|
this.#setOptions_Materials();
|
|
1399
1603
|
this.#setOptions_Camera();
|
|
1400
|
-
this.#setOptions_IBL();
|
|
1604
|
+
await this.#setOptions_IBL();
|
|
1401
1605
|
this.#setVisibilityOfWallAndFloorInModel();
|
|
1402
1606
|
detail.success = true;
|
|
1403
1607
|
})
|
|
@@ -1410,9 +1614,8 @@ export default class BabylonJSController {
|
|
|
1410
1614
|
.finally(async () => {
|
|
1411
1615
|
this.#checkModelMetadata(oldModelMetadata, newModelMetadata);
|
|
1412
1616
|
this.#setMaxSimultaneousLights();
|
|
1413
|
-
this.#loadCameraDepentEffects();
|
|
1414
1617
|
this.#babylonJSAnimationController = new BabylonJSAnimationController(this.#containers.model.assetContainer);
|
|
1415
|
-
this.#startRender();
|
|
1618
|
+
await this.#startRender();
|
|
1416
1619
|
});
|
|
1417
1620
|
return detail;
|
|
1418
1621
|
}
|
|
@@ -1423,10 +1626,22 @@ export default class BabylonJSController {
|
|
|
1423
1626
|
* @private
|
|
1424
1627
|
* @returns {void}
|
|
1425
1628
|
*/
|
|
1426
|
-
#
|
|
1427
|
-
this.#initializeVisualImprovements();
|
|
1428
|
-
this.#initializeAmbientOcclussion();
|
|
1429
|
-
this.#initializeShadows();
|
|
1629
|
+
async #loadCameraDependentEffects() {
|
|
1630
|
+
await this.#initializeVisualImprovements();
|
|
1631
|
+
await this.#initializeAmbientOcclussion();
|
|
1632
|
+
await this.#initializeShadows();
|
|
1633
|
+
}
|
|
1634
|
+
|
|
1635
|
+
/**
|
|
1636
|
+
* Shuts down every post-process tied to the active camera before stopping the render loop.
|
|
1637
|
+
* Ensures AA, SSAO, and shadow resources detach cleanly so reloads rebuild from scratch.
|
|
1638
|
+
* @private
|
|
1639
|
+
* @returns {void}
|
|
1640
|
+
*/
|
|
1641
|
+
async #unloadCameraDependentEffects() {
|
|
1642
|
+
await this.#disableVisualImprovements();
|
|
1643
|
+
await this.#disableAmbientOcclusion();
|
|
1644
|
+
await this.#disableShadows();
|
|
1430
1645
|
}
|
|
1431
1646
|
|
|
1432
1647
|
/**
|
|
@@ -1670,7 +1885,15 @@ export default class BabylonJSController {
|
|
|
1670
1885
|
geometryBufferRenderer.generateNormalsInWorldSpace = true;
|
|
1671
1886
|
}
|
|
1672
1887
|
|
|
1673
|
-
this.#scene.clearColor = new Color4(1, 1, 1, 1);
|
|
1888
|
+
this.#scene.clearColor = new Color4(1, 1, 1, 1).toLinearSpace();
|
|
1889
|
+
|
|
1890
|
+
// Lowered exposure to prevent scenes from looking blown out when the DefaultRenderingPipeline (Antialiasing) is enabled.
|
|
1891
|
+
this.#scene.imageProcessingConfiguration.exposure = 0.75;
|
|
1892
|
+
this.#scene.imageProcessingConfiguration.contrast = 1.0;
|
|
1893
|
+
this.#scene.imageProcessingConfiguration.toneMappingEnabled = false;
|
|
1894
|
+
this.#scene.imageProcessingConfiguration.vignetteEnabled = false;
|
|
1895
|
+
this.#scene.imageProcessingConfiguration.colorCurvesEnabled = false;
|
|
1896
|
+
|
|
1674
1897
|
this.#createCamera();
|
|
1675
1898
|
this.#enableInteraction();
|
|
1676
1899
|
await this.#createXRExperience();
|
|
@@ -1690,6 +1913,7 @@ export default class BabylonJSController {
|
|
|
1690
1913
|
this.#disableInteraction();
|
|
1691
1914
|
this.#disposeAnimationController();
|
|
1692
1915
|
this.#disposeXRExperience();
|
|
1916
|
+
this.#unloadCameraDependentEffects();
|
|
1693
1917
|
this.#disposeEngine();
|
|
1694
1918
|
}
|
|
1695
1919
|
|
|
@@ -1815,26 +2039,6 @@ export default class BabylonJSController {
|
|
|
1815
2039
|
return await this.#loadContainers();
|
|
1816
2040
|
}
|
|
1817
2041
|
|
|
1818
|
-
/**
|
|
1819
|
-
* Merges incoming render flags with the current configuration, persists them, and marks
|
|
1820
|
-
* all dependent loaders/options as pending when something actually changed.
|
|
1821
|
-
* @public
|
|
1822
|
-
* @param {{antiAliasingEnabled?:boolean, ambientOcclusionEnabled?:boolean, iblEnabled?:boolean, shadowsEnabled?:boolean}} settings Partial set of render toggles to apply.
|
|
1823
|
-
* @returns {{changed:boolean, settings:{antiAliasingEnabled:boolean, ambientOcclusionEnabled:boolean, iblEnabled:boolean, shadowsEnabled:boolean}}}
|
|
1824
|
-
* @description
|
|
1825
|
-
* Callers can inspect the `changed` flag to decide whether to trigger a reload with
|
|
1826
|
-
* `reloadWithCurrentSettings()` or simply reuse the returned snapshot.
|
|
1827
|
-
*/
|
|
1828
|
-
scheduleRenderSettingsReload(settings = {}) {
|
|
1829
|
-
const changed = this.#applyRenderSettings(settings);
|
|
1830
|
-
if (!changed) {
|
|
1831
|
-
return { changed: false, settings: this.getRenderSettings() };
|
|
1832
|
-
}
|
|
1833
|
-
this.#markContainersForReload();
|
|
1834
|
-
this.#markOptionsForReload();
|
|
1835
|
-
return { changed: true, settings: this.getRenderSettings() };
|
|
1836
|
-
}
|
|
1837
|
-
|
|
1838
2042
|
/**
|
|
1839
2043
|
* Applies camera options from the configuration to the active camera.
|
|
1840
2044
|
* Stops and restarts the render loop to apply changes.
|
|
@@ -1844,7 +2048,6 @@ export default class BabylonJSController {
|
|
|
1844
2048
|
setCameraOptions() {
|
|
1845
2049
|
this.#stopRender();
|
|
1846
2050
|
const cameraOptionsSetted = this.#setOptions_Camera();
|
|
1847
|
-
this.#loadCameraDepentEffects();
|
|
1848
2051
|
this.#startRender();
|
|
1849
2052
|
return cameraOptionsSetted;
|
|
1850
2053
|
}
|
|
@@ -1866,12 +2069,11 @@ export default class BabylonJSController {
|
|
|
1866
2069
|
* Reapplies image-based lighting configuration (HDR URL, intensity, shadow mode).
|
|
1867
2070
|
* Stops rendering, pushes pending IBL state into the scene, rebuilds camera-dependent effects, then resumes rendering.
|
|
1868
2071
|
* @public
|
|
1869
|
-
* @returns {
|
|
2072
|
+
* @returns {boolean} True if IBL options were set successfully, false otherwise.
|
|
1870
2073
|
*/
|
|
1871
2074
|
setIBLOptions() {
|
|
1872
2075
|
this.#stopRender();
|
|
1873
2076
|
const IBLOptionsSetted = this.#setOptions_IBL();
|
|
1874
|
-
this.#loadCameraDepentEffects();
|
|
1875
2077
|
this.#startRender();
|
|
1876
2078
|
return IBLOptionsSetted;
|
|
1877
2079
|
}
|
|
@@ -1900,6 +2102,26 @@ export default class BabylonJSController {
|
|
|
1900
2102
|
this.#startRender();
|
|
1901
2103
|
}
|
|
1902
2104
|
|
|
2105
|
+
/**
|
|
2106
|
+
* Merges incoming render flags with the current configuration, persists them, and marks
|
|
2107
|
+
* all dependent loaders/options as pending when something actually changed.
|
|
2108
|
+
* @public
|
|
2109
|
+
* @param {{antiAliasingEnabled?:boolean, ambientOcclusionEnabled?:boolean, iblEnabled?:boolean, shadowsEnabled?:boolean}} settings Partial set of render toggles to apply.
|
|
2110
|
+
* @returns {{changed:boolean, settings:{antiAliasingEnabled:boolean, ambientOcclusionEnabled:boolean, iblEnabled:boolean, shadowsEnabled:boolean}}}
|
|
2111
|
+
* @description
|
|
2112
|
+
* Callers can inspect the `changed` flag to decide whether to trigger a reload with
|
|
2113
|
+
* `reloadWithCurrentSettings()` or simply reuse the returned snapshot.
|
|
2114
|
+
*/
|
|
2115
|
+
scheduleRenderSettingsReload(settings = {}) {
|
|
2116
|
+
const changed = this.#saveRenderSettings(settings);
|
|
2117
|
+
if (!changed) {
|
|
2118
|
+
return { changed: false, settings: this.getRenderSettings() };
|
|
2119
|
+
}
|
|
2120
|
+
this.#markContainersForReload();
|
|
2121
|
+
this.#markOptionsForReload();
|
|
2122
|
+
return { changed: true, settings: this.getRenderSettings() };
|
|
2123
|
+
}
|
|
2124
|
+
|
|
1903
2125
|
/**
|
|
1904
2126
|
* Reloads every asset container using the latest staged render settings.
|
|
1905
2127
|
* Intended to be called after `scheduleRenderSettingsReload()` marks data as pending.
|
|
@@ -70,9 +70,7 @@ export class ContainerData {
|
|
|
70
70
|
}
|
|
71
71
|
const targetShow = this.update.show !== null ? this.update.show : this.show;
|
|
72
72
|
this.setPending(storedSource, targetShow);
|
|
73
|
-
this.
|
|
74
|
-
this.update.timeStamp = this.timeStamp;
|
|
75
|
-
this.update.metadata = { ...(this.metadata ?? {}) };
|
|
73
|
+
this.setPendingCacheData(this.size, this.timeStamp, this.metadata);
|
|
76
74
|
return true;
|
|
77
75
|
}
|
|
78
76
|
get isPending() {
|
|
@@ -225,38 +223,20 @@ export class CameraData {
|
|
|
225
223
|
* - Inspect `isPending`/`isSuccess` to drive UI or re-render logic.
|
|
226
224
|
*/
|
|
227
225
|
export class IBLData {
|
|
228
|
-
|
|
226
|
+
defaultIntensity = 1.0;
|
|
227
|
+
defaultShadows = false;
|
|
228
|
+
constructor(url = null, intensity = this.defaultIntensity, shadows = this.defaultShadows, timeStamp = null) {
|
|
229
229
|
this.url = url;
|
|
230
|
+
this.cachedUrl = null;
|
|
230
231
|
this.intensity = intensity;
|
|
231
232
|
this.shadows = shadows;
|
|
232
233
|
this.timeStamp = timeStamp;
|
|
233
|
-
this.reset();
|
|
234
|
-
}
|
|
235
|
-
reset() {
|
|
236
|
-
this.pending = false;
|
|
237
|
-
this.success = false;
|
|
238
|
-
}
|
|
239
|
-
setValues(url, intensity, shadows, timeStamp) {
|
|
240
|
-
this.url = url !== undefined ? url : this.url;
|
|
241
|
-
this.intensity = intensity !== undefined ? intensity : this.intensity;
|
|
242
|
-
this.shadows = shadows !== undefined ? shadows : this.shadows;
|
|
243
|
-
this.timeStamp = timeStamp !== undefined ? timeStamp : this.timeStamp;
|
|
244
|
-
}
|
|
245
|
-
setSuccess(success = false) {
|
|
246
|
-
if (success) {
|
|
247
|
-
this.success = true;
|
|
248
|
-
} else {
|
|
249
|
-
this.success = false;
|
|
250
|
-
}
|
|
251
234
|
}
|
|
252
|
-
|
|
253
|
-
this.
|
|
254
|
-
this.
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
}
|
|
259
|
-
get isSuccess() {
|
|
260
|
-
return this.success === true;
|
|
235
|
+
setValues(url, cachedUrl = null, intensity = this.defaultIntensity, shadows = this.defaultShadows, timeStamp = null) {
|
|
236
|
+
this.url = url;
|
|
237
|
+
this.cachedUrl = cachedUrl;
|
|
238
|
+
this.intensity = intensity;
|
|
239
|
+
this.shadows = shadows;
|
|
240
|
+
this.timeStamp = timeStamp;
|
|
261
241
|
}
|
|
262
242
|
}
|
package/src/pref-viewer-3d.js
CHANGED
|
@@ -239,7 +239,6 @@ export default class PrefViewer3D extends HTMLElement {
|
|
|
239
239
|
Object.values(this.#data.containers).forEach((container) => container.reset());
|
|
240
240
|
Object.values(this.#data.options.materials).forEach((material) => material.reset());
|
|
241
241
|
this.#data.options.camera.reset();
|
|
242
|
-
this.#data.options.ibl.reset();
|
|
243
242
|
}
|
|
244
243
|
|
|
245
244
|
/**
|
|
@@ -335,6 +334,7 @@ export default class PrefViewer3D extends HTMLElement {
|
|
|
335
334
|
const iblState = this.#data.options.ibl;
|
|
336
335
|
|
|
337
336
|
let url = undefined;
|
|
337
|
+
let cachedUrl = undefined;
|
|
338
338
|
let timeStamp = undefined;
|
|
339
339
|
let shadows = undefined;
|
|
340
340
|
let intensity = undefined;
|
|
@@ -344,7 +344,7 @@ export default class PrefViewer3D extends HTMLElement {
|
|
|
344
344
|
const fileStorage = new FileStorage("PrefViewer", "Files");
|
|
345
345
|
const newURL = await fileStorage.getURL(options.ibl.url);
|
|
346
346
|
if (newURL) {
|
|
347
|
-
|
|
347
|
+
cachedUrl = newURL;
|
|
348
348
|
timeStamp = await fileStorage.getTimeStamp(options.ibl.url);
|
|
349
349
|
}
|
|
350
350
|
}
|
|
@@ -358,10 +358,11 @@ export default class PrefViewer3D extends HTMLElement {
|
|
|
358
358
|
const needUpdate = url !== undefined && url !== iblState.url ||
|
|
359
359
|
timeStamp !== undefined && timeStamp !== iblState.timeStamp ||
|
|
360
360
|
shadows !== undefined && shadows !== iblState.shadows ||
|
|
361
|
-
|
|
361
|
+
shadows === undefined && iblState.shadows !== iblState.defaultShadows ||
|
|
362
|
+
intensity !== undefined && intensity !== iblState.intensity ||
|
|
363
|
+
intensity === undefined && iblState.intensity !== iblState.defaultIntensity;
|
|
362
364
|
if (needUpdate) {
|
|
363
|
-
iblState.setValues(url, intensity, shadows, timeStamp);
|
|
364
|
-
iblState.setPending(true);
|
|
365
|
+
iblState.setValues(url, cachedUrl, intensity, shadows, timeStamp);
|
|
365
366
|
}
|
|
366
367
|
|
|
367
368
|
return needUpdate;
|
|
@@ -9,7 +9,7 @@ import { DEFAULT_LOCALE, resolveLocale, translate } from "./localization/i18n.js
|
|
|
9
9
|
* - Builds an accessible hover/focus-activated panel with switches for AA, SSAO, IBL, and dynamic shadows.
|
|
10
10
|
* - Caches translated copy in `#texts` and listens for culture changes via the `culture` attribute or `setCulture()`.
|
|
11
11
|
* - Tracks applied vs. draft render settings so pending diffs, button enablement, and status text stay in sync.
|
|
12
|
-
* - Emits `pref-viewer-menu-apply` whenever the user confirms toggles, allowing PrefViewer/BabylonJSController to persist.
|
|
12
|
+
* - Emits `pref-viewer-menu-3d-apply` whenever the user confirms toggles, allowing PrefViewer/BabylonJSController to persist.
|
|
13
13
|
* - Shows transient status/error messages and per-switch pending states while operations complete.
|
|
14
14
|
*
|
|
15
15
|
* Public API:
|
|
@@ -369,8 +369,15 @@ export default class PrefViewerMenu3D extends HTMLElement {
|
|
|
369
369
|
if (this.#isApplying || !this.#hasPendingChanges()) {
|
|
370
370
|
return;
|
|
371
371
|
}
|
|
372
|
+
|
|
372
373
|
const detail = { settings: { ...this.#draftSettings } };
|
|
373
|
-
|
|
374
|
+
const customEventOptions = {
|
|
375
|
+
bubbles: true,
|
|
376
|
+
cancelable: true,
|
|
377
|
+
composed: true,
|
|
378
|
+
detail: detail,
|
|
379
|
+
};
|
|
380
|
+
this.dispatchEvent(new CustomEvent("pref-viewer-menu-3d-apply", customEventOptions));
|
|
374
381
|
}
|
|
375
382
|
|
|
376
383
|
/**
|
package/src/pref-viewer.js
CHANGED
|
@@ -216,7 +216,7 @@ export default class PrefViewer extends HTMLElement {
|
|
|
216
216
|
this.#component3D.remove();
|
|
217
217
|
}
|
|
218
218
|
if (this.#menu3D) {
|
|
219
|
-
this.#menu3D.removeEventListener("pref-viewer-menu-apply", this.#handlers.onMenuApply);
|
|
219
|
+
this.#menu3D.removeEventListener("pref-viewer-menu-3d-apply", this.#handlers.onMenuApply);
|
|
220
220
|
this.#menu3D.remove();
|
|
221
221
|
this.#menu3D = null;
|
|
222
222
|
}
|
|
@@ -264,7 +264,7 @@ export default class PrefViewer extends HTMLElement {
|
|
|
264
264
|
return;
|
|
265
265
|
}
|
|
266
266
|
if (this.#menu3D) {
|
|
267
|
-
this.#menu3D.removeEventListener("pref-viewer-menu-apply", this.#handlers.onMenuApply);
|
|
267
|
+
this.#menu3D.removeEventListener("pref-viewer-menu-3d-apply", this.#handlers.onMenuApply);
|
|
268
268
|
this.#menu3D.remove();
|
|
269
269
|
}
|
|
270
270
|
this.#menu3D = document.createElement("pref-viewer-menu-3d");
|
|
@@ -280,7 +280,7 @@ export default class PrefViewer extends HTMLElement {
|
|
|
280
280
|
this.#handlers.onViewerHoverStart = () => this.#menu3D.setViewerHover(true);
|
|
281
281
|
this.#handlers.onViewerHoverEnd = () => this.#menu3D.setViewerHover(false);
|
|
282
282
|
|
|
283
|
-
this.#menu3D.addEventListener("pref-viewer-menu-apply", this.#handlers.onMenuApply);
|
|
283
|
+
this.#menu3D.addEventListener("pref-viewer-menu-3d-apply", this.#handlers.onMenuApply);
|
|
284
284
|
this.#wrapper.addEventListener("mouseenter", this.#handlers.onViewerHoverStart);
|
|
285
285
|
this.#wrapper.addEventListener("mouseleave", this.#handlers.onViewerHoverEnd);
|
|
286
286
|
|
|
@@ -601,7 +601,7 @@ export default class PrefViewer extends HTMLElement {
|
|
|
601
601
|
}
|
|
602
602
|
|
|
603
603
|
/**
|
|
604
|
-
* Handles the custom "pref-viewer-menu-apply" event emitted by the menu component.
|
|
604
|
+
* Handles the custom "pref-viewer-menu-3d-apply" event emitted by the menu component.
|
|
605
605
|
* Forwards the requested settings to the render-application pipeline.
|
|
606
606
|
* @private
|
|
607
607
|
* @param {CustomEvent} event - Menu event containing the `settings` payload.
|