@preference-sl/pref-viewer 2.13.0-beta.2 → 2.13.0-beta.4
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 +5 -5
- package/src/babylonjs-controller.js +330 -241
- 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,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@preference-sl/pref-viewer",
|
|
3
|
-
"version": "2.13.0-beta.
|
|
3
|
+
"version": "2.13.0-beta.4",
|
|
4
4
|
"description": "Web Component to preview GLTF models with Babylon.js",
|
|
5
5
|
"author": "Alex Moreno Palacio <amoreno@preference.es>",
|
|
6
6
|
"scripts": {
|
|
@@ -35,11 +35,11 @@
|
|
|
35
35
|
"index.d.ts"
|
|
36
36
|
],
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@babylonjs/core": "^8.
|
|
39
|
-
"@babylonjs/loaders": "^8.
|
|
40
|
-
"@babylonjs/serializers": "^8.
|
|
38
|
+
"@babylonjs/core": "^8.50.5",
|
|
39
|
+
"@babylonjs/loaders": "^8.50.5",
|
|
40
|
+
"@babylonjs/serializers": "^8.50.5",
|
|
41
41
|
"@panzoom/panzoom": "^4.6.0",
|
|
42
|
-
"babylonjs-gltf2interface": "^8.
|
|
42
|
+
"babylonjs-gltf2interface": "^8.50.5",
|
|
43
43
|
"buffer": "^6.0.3",
|
|
44
44
|
"idb": "^8.0.3",
|
|
45
45
|
"is-svg": "^6.1.0",
|
|
@@ -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
|
|
|
@@ -173,7 +173,7 @@ export default class BabylonJSController {
|
|
|
173
173
|
* @param {object} [settings={}] - Partial map of render settings (AA, SSAO, IBL, shadows, etc.).
|
|
174
174
|
* @returns {boolean} True when any setting changed and was saved.
|
|
175
175
|
*/
|
|
176
|
-
#
|
|
176
|
+
#saveRenderSettings(settings = {}) {
|
|
177
177
|
if (!settings) {
|
|
178
178
|
return false;
|
|
179
179
|
}
|
|
@@ -186,12 +186,8 @@ export default class BabylonJSController {
|
|
|
186
186
|
}
|
|
187
187
|
});
|
|
188
188
|
|
|
189
|
-
if (changed && settings.iblEnabled === false && this.#scene) {
|
|
190
|
-
this.#scene.environmentTexture = null;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
189
|
if (changed) {
|
|
194
|
-
this.#
|
|
190
|
+
this.#storeRenderSettings();
|
|
195
191
|
}
|
|
196
192
|
|
|
197
193
|
return changed;
|
|
@@ -229,14 +225,14 @@ export default class BabylonJSController {
|
|
|
229
225
|
* @private
|
|
230
226
|
* @returns {void}
|
|
231
227
|
*/
|
|
232
|
-
#
|
|
228
|
+
#storeRenderSettings() {
|
|
233
229
|
if (typeof window === "undefined" || !window?.localStorage) {
|
|
234
230
|
return;
|
|
235
231
|
}
|
|
236
232
|
try {
|
|
237
233
|
window.localStorage.setItem(this.#RENDER_SETTINGS_STORAGE_KEY, JSON.stringify(this.#settings));
|
|
238
234
|
} catch (error) {
|
|
239
|
-
console.warn("PrefViewer: unable to
|
|
235
|
+
console.warn("PrefViewer: unable to store render settings", error);
|
|
240
236
|
}
|
|
241
237
|
}
|
|
242
238
|
|
|
@@ -269,9 +265,6 @@ export default class BabylonJSController {
|
|
|
269
265
|
if (this.#options?.materials) {
|
|
270
266
|
Object.values(this.#options.materials).forEach((material) => material?.setPendingWithCurrent?.());
|
|
271
267
|
}
|
|
272
|
-
if (this.#options?.ibl?.setPending) {
|
|
273
|
-
this.#options.ibl.setPending();
|
|
274
|
-
}
|
|
275
268
|
}
|
|
276
269
|
|
|
277
270
|
/**
|
|
@@ -382,49 +375,71 @@ export default class BabylonJSController {
|
|
|
382
375
|
* Adds a hemispheric ambient light, a directional light for shadows, a shadow generator, and a camera-attached point light.
|
|
383
376
|
* Sets light intensities and shadow properties for realistic rendering.
|
|
384
377
|
* @private
|
|
385
|
-
* @returns {
|
|
378
|
+
* @returns {Promise<boolean>} Returns true if lights were changed, false otherwise.
|
|
386
379
|
*/
|
|
387
|
-
#createLights() {
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
}
|
|
380
|
+
async #createLights() {
|
|
381
|
+
const hemiLightName = "PrefViewerHemiLight";
|
|
382
|
+
const cameraLightName = "PrefViewerCameraLight";
|
|
383
|
+
const dirLightName = "PrefViewerDirLight";
|
|
392
384
|
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
385
|
+
const hemiLight = this.#scene.getLightByName(hemiLightName);
|
|
386
|
+
const cameraLight = this.#scene.getLightByName(cameraLightName);
|
|
387
|
+
const dirLight = this.#scene.getLightByName(dirLightName);
|
|
396
388
|
|
|
397
|
-
|
|
398
|
-
this.#hemiLight = this.#scene.getLightByName("PrefViewerHemiLight");
|
|
399
|
-
if (!this.#hemiLight) {
|
|
400
|
-
this.#hemiLight = new HemisphericLight("PrefViewerHemiLight", new Vector3(-10, 10, -10), this.#scene);
|
|
401
|
-
this.#hemiLight.intensity = 0.6;
|
|
402
|
-
}
|
|
389
|
+
let lightsChanged = false;
|
|
403
390
|
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
if (
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
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
|
+
}
|
|
403
|
+
this.#hemiLight = this.#dirLight = this.#cameraLight = null;
|
|
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
|
+
}
|
|
411
412
|
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
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
|
+
}
|
|
418
432
|
}
|
|
433
|
+
return lightsChanged;
|
|
419
434
|
}
|
|
420
435
|
|
|
421
436
|
/**
|
|
422
437
|
* Detaches and disposes the SSAO render pipeline from the active camera when it exists.
|
|
423
438
|
* Guards against missing scene resources or absent pipelines, returning false when no cleanup is needed.
|
|
424
439
|
* @private
|
|
425
|
-
* @returns {boolean} Returns true when the SSAO pipeline was disabled, false otherwise.
|
|
440
|
+
* @returns {Promise<boolean>} Returns true when the SSAO pipeline was disabled, false otherwise.
|
|
426
441
|
*/
|
|
427
|
-
#disableAmbientOcclusion() {
|
|
442
|
+
async #disableAmbientOcclusion() {
|
|
428
443
|
const pipelineManager = this.#scene?.postProcessRenderPipelineManager;
|
|
429
444
|
|
|
430
445
|
if (!this.#scene || !pipelineManager || this.#scene?.activeCamera === null) {
|
|
@@ -437,19 +452,19 @@ export default class BabylonJSController {
|
|
|
437
452
|
return false;
|
|
438
453
|
}
|
|
439
454
|
|
|
440
|
-
if (!this.#renderPipelines.ssao) {
|
|
455
|
+
if (!this.#renderPipelines.ssao || !this.#renderPipelines.ssao?.name) {
|
|
441
456
|
return false;
|
|
442
457
|
}
|
|
443
458
|
|
|
444
459
|
const pipelineName = this.#renderPipelines.ssao.name;
|
|
445
|
-
|
|
460
|
+
let ssaoPipeline = supportedPipelines ? supportedPipelines.find((pipeline) => pipeline.name === pipelineName) : false;
|
|
446
461
|
|
|
447
462
|
if (ssaoPipeline) {
|
|
448
463
|
pipelineManager.detachCamerasFromRenderPipeline(pipelineName, [this.#scene.activeCamera]);
|
|
449
|
-
ssaoPipeline.dispose();
|
|
450
464
|
pipelineManager.removePipeline(pipelineName);
|
|
451
465
|
pipelineManager.update();
|
|
452
|
-
|
|
466
|
+
ssaoPipeline.dispose();
|
|
467
|
+
this.#renderPipelines.ssao = ssaoPipeline = null;
|
|
453
468
|
}
|
|
454
469
|
|
|
455
470
|
return true;
|
|
@@ -460,9 +475,9 @@ export default class BabylonJSController {
|
|
|
460
475
|
* Disposes previous SSAO pipelines, instantiates a tuned `SSAORenderingPipeline`, and attaches it to the
|
|
461
476
|
* current camera so contact shadows enhance depth perception once assets reload or the camera changes.
|
|
462
477
|
* @private
|
|
463
|
-
* @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.
|
|
464
479
|
*/
|
|
465
|
-
#initializeAmbientOcclussion() {
|
|
480
|
+
async #initializeAmbientOcclussion() {
|
|
466
481
|
const pipelineManager = this.#scene?.postProcessRenderPipelineManager;
|
|
467
482
|
if (!this.#scene || !pipelineManager || this.#scene?.activeCamera === null) {
|
|
468
483
|
return false;
|
|
@@ -484,34 +499,35 @@ export default class BabylonJSController {
|
|
|
484
499
|
combineRatio: 1.0
|
|
485
500
|
};
|
|
486
501
|
|
|
487
|
-
|
|
502
|
+
let ssaoPipeline = new SSAORenderingPipeline(pipelineName, this.#scene, ssaoRatio, [this.#scene.activeCamera]);
|
|
488
503
|
|
|
489
|
-
if (!
|
|
504
|
+
if (!ssaoPipeline){
|
|
490
505
|
return false;
|
|
491
506
|
}
|
|
492
507
|
|
|
493
|
-
if (
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
508
|
+
if (ssaoPipeline.isSupported) {
|
|
509
|
+
ssaoPipeline.fallOff = 0.000001;
|
|
510
|
+
ssaoPipeline.area = 1;
|
|
511
|
+
ssaoPipeline.radius = 0.0001;
|
|
512
|
+
ssaoPipeline.totalStrength = 1;
|
|
513
|
+
ssaoPipeline.base = 0.6;
|
|
499
514
|
|
|
500
515
|
// Configure SSAO to calculate only once instead of every frame for better performance
|
|
501
|
-
if (
|
|
502
|
-
|
|
503
|
-
|
|
516
|
+
if (ssaoPipeline._ssaoPostProcess) {
|
|
517
|
+
ssaoPipeline._ssaoPostProcess.autoClear = false;
|
|
518
|
+
ssaoPipeline._ssaoPostProcess.samples = 1;
|
|
504
519
|
}
|
|
505
|
-
if (
|
|
506
|
-
|
|
507
|
-
|
|
520
|
+
if (ssaoPipeline._combinePostProcess) {
|
|
521
|
+
ssaoPipeline._combinePostProcess.autoClear = false;
|
|
522
|
+
ssaoPipeline._combinePostProcess.samples = 1;
|
|
508
523
|
}
|
|
509
524
|
|
|
525
|
+
this.#renderPipelines.ssao = ssaoPipeline;
|
|
510
526
|
pipelineManager.update();
|
|
511
527
|
return true;
|
|
512
528
|
} else {
|
|
513
|
-
|
|
514
|
-
this.#renderPipelines.ssao = null;
|
|
529
|
+
ssaoPipeline.dispose();
|
|
530
|
+
this.#renderPipelines.ssao = ssaoPipeline = null;
|
|
515
531
|
pipelineManager.update();
|
|
516
532
|
return false;
|
|
517
533
|
}
|
|
@@ -521,9 +537,9 @@ export default class BabylonJSController {
|
|
|
521
537
|
* Tears down the default rendering pipeline (MSAA/FXAA/grain) for the active camera when present.
|
|
522
538
|
* Ensures stale pipelines detach cleanly so a fresh one can be installed on the next load.
|
|
523
539
|
* @private
|
|
524
|
-
* @returns {boolean} Returns true when the pipeline was removed, false otherwise.
|
|
540
|
+
* @returns {Promise<boolean>} Returns true when the pipeline was removed, false otherwise.
|
|
525
541
|
*/
|
|
526
|
-
#disableVisualImprovements() {
|
|
542
|
+
async #disableVisualImprovements() {
|
|
527
543
|
const pipelineManager = this.#scene?.postProcessRenderPipelineManager;
|
|
528
544
|
|
|
529
545
|
if (!this.#scene || !pipelineManager || this.#scene?.activeCamera === null) {
|
|
@@ -536,19 +552,19 @@ export default class BabylonJSController {
|
|
|
536
552
|
return false;
|
|
537
553
|
}
|
|
538
554
|
|
|
539
|
-
if (!this.#renderPipelines.default) {
|
|
555
|
+
if (!this.#renderPipelines.default || !this.#renderPipelines.default?.name) {
|
|
540
556
|
return false;
|
|
541
557
|
}
|
|
542
558
|
|
|
543
559
|
const pipelineName = this.#renderPipelines.default.name;
|
|
544
|
-
|
|
560
|
+
let defaultPipeline = supportedPipelines ? supportedPipelines.find((pipeline) => pipeline.name === pipelineName) : false;
|
|
545
561
|
|
|
546
562
|
if (defaultPipeline) {
|
|
547
563
|
pipelineManager.detachCamerasFromRenderPipeline(pipelineName, [this.#scene.activeCamera]);
|
|
548
|
-
defaultPipeline.dispose();
|
|
549
564
|
pipelineManager.removePipeline(pipelineName);
|
|
550
565
|
pipelineManager.update();
|
|
551
|
-
|
|
566
|
+
defaultPipeline.dispose();
|
|
567
|
+
this.#renderPipelines.default = defaultPipeline = null;
|
|
552
568
|
}
|
|
553
569
|
|
|
554
570
|
return true;
|
|
@@ -559,10 +575,10 @@ export default class BabylonJSController {
|
|
|
559
575
|
* Disposes any previous pipeline instance to avoid duplicates, then attaches a fresh
|
|
560
576
|
* `DefaultRenderingPipeline` with tuned settings for sharper anti-aliasing and subtle grain.
|
|
561
577
|
* @private
|
|
562
|
-
* @returns {boolean} True when the pipeline is supported and active, otherwise false.
|
|
578
|
+
* @returns {Promise<boolean>} True when the pipeline is supported and active, otherwise false.
|
|
563
579
|
* @see {@link https://doc.babylonjs.com/features/featuresDeepDive/postProcesses/defaultRenderingPipeline|Using the Default Rendering Pipeline | Babylon.js Documentation}
|
|
564
580
|
*/
|
|
565
|
-
#initializeVisualImprovements() {
|
|
581
|
+
async #initializeVisualImprovements() {
|
|
566
582
|
const pipelineManager = this.#scene?.postProcessRenderPipelineManager;
|
|
567
583
|
if (!this.#scene || !pipelineManager || this.#scene?.activeCamera === null) {
|
|
568
584
|
return false;
|
|
@@ -579,49 +595,51 @@ export default class BabylonJSController {
|
|
|
579
595
|
|
|
580
596
|
const pipelineName = "PrefViewerDefaultRenderingPipeline";
|
|
581
597
|
|
|
582
|
-
|
|
598
|
+
let defaultPipeline = new DefaultRenderingPipeline(pipelineName, true, this.#scene, [this.#scene.activeCamera], true);
|
|
583
599
|
|
|
584
|
-
if (!
|
|
600
|
+
if (!defaultPipeline){
|
|
585
601
|
return false;
|
|
586
602
|
}
|
|
587
603
|
|
|
588
|
-
if (
|
|
604
|
+
if (defaultPipeline.isSupported) {
|
|
589
605
|
// MSAA - Multisample Anti-Aliasing
|
|
590
606
|
const caps = this.#scene.getEngine()?.getCaps?.() || {};
|
|
591
607
|
const maxSamples = typeof caps.maxMSAASamples === "number" ? caps.maxMSAASamples : 4;
|
|
592
|
-
|
|
608
|
+
defaultPipeline.samples = Math.max(1, Math.min(8, maxSamples));
|
|
593
609
|
// FXAA - Fast Approximate Anti-Aliasing
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
if (
|
|
598
|
-
|
|
610
|
+
defaultPipeline.fxaaEnabled = true;
|
|
611
|
+
defaultPipeline.fxaa.samples = 8;
|
|
612
|
+
defaultPipeline.fxaa.adaptScaleToCurrentViewport = true;
|
|
613
|
+
if (defaultPipeline.fxaa.edgeThreshold !== undefined) {
|
|
614
|
+
defaultPipeline.fxaa.edgeThreshold = 0.125;
|
|
599
615
|
}
|
|
600
|
-
if (
|
|
601
|
-
|
|
616
|
+
if (defaultPipeline.fxaa.edgeThresholdMin !== undefined) {
|
|
617
|
+
defaultPipeline.fxaa.edgeThresholdMin = 0.0625;
|
|
602
618
|
}
|
|
603
|
-
if (
|
|
604
|
-
|
|
619
|
+
if (defaultPipeline.fxaa.subPixelQuality !== undefined) {
|
|
620
|
+
defaultPipeline.fxaa.subPixelQuality = 0.75;
|
|
605
621
|
}
|
|
606
622
|
|
|
607
623
|
// Grain
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
624
|
+
defaultPipeline.grainEnabled = true;
|
|
625
|
+
defaultPipeline.grain.adaptScaleToCurrentViewport = true;
|
|
626
|
+
defaultPipeline.grain.animated = false;
|
|
627
|
+
defaultPipeline.grain.intensity = 3;
|
|
612
628
|
|
|
613
629
|
// Configure post-processes to calculate only once instead of every frame for better performance
|
|
614
|
-
if (
|
|
615
|
-
|
|
630
|
+
if (defaultPipeline.fxaa?._postProcess) {
|
|
631
|
+
defaultPipeline.fxaa._postProcess.autoClear = false;
|
|
616
632
|
}
|
|
617
|
-
if (
|
|
618
|
-
|
|
633
|
+
if (defaultPipeline.grain?._postProcess) {
|
|
634
|
+
defaultPipeline.grain._postProcess.autoClear = false;
|
|
619
635
|
}
|
|
620
636
|
|
|
637
|
+
this.#renderPipelines.default = defaultPipeline;
|
|
621
638
|
pipelineManager.update();
|
|
622
639
|
return true;
|
|
623
640
|
} else {
|
|
624
|
-
|
|
641
|
+
defaultPipeline.dispose();
|
|
642
|
+
this.#renderPipelines.default = defaultPipeline = null;
|
|
625
643
|
pipelineManager.update();
|
|
626
644
|
return false;
|
|
627
645
|
}
|
|
@@ -632,13 +650,19 @@ export default class BabylonJSController {
|
|
|
632
650
|
* Loads an HDR texture from a predefined URI and assigns it to the scene's environmentTexture property.
|
|
633
651
|
* Configures gamma space, mipmaps, and intensity level for realistic lighting.
|
|
634
652
|
* @private
|
|
635
|
-
* @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.
|
|
636
654
|
*/
|
|
637
|
-
#initializeEnvironmentTexture() {
|
|
638
|
-
|
|
639
|
-
|
|
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);
|
|
640
663
|
hdrTexture.level = this.#options.ibl.intensity;
|
|
641
664
|
this.#scene.environmentTexture = hdrTexture;
|
|
665
|
+
this.#scene.markAllMaterialsAsDirty(Material.TextureDirtyFlag);
|
|
642
666
|
return true;
|
|
643
667
|
}
|
|
644
668
|
|
|
@@ -646,9 +670,9 @@ export default class BabylonJSController {
|
|
|
646
670
|
* Removes the IBL shadow render pipeline from the active camera when present.
|
|
647
671
|
* Ensures voxelized shadow data is disposed so reloading environments installs a clean pipeline.
|
|
648
672
|
* @private
|
|
649
|
-
* @returns {boolean} Returns true when the pipeline was removed, false otherwise.
|
|
673
|
+
* @returns {Promise<boolean>} Returns true when the pipeline was removed, false otherwise.
|
|
650
674
|
*/
|
|
651
|
-
#disableIBLShadows() {
|
|
675
|
+
async #disableIBLShadows() {
|
|
652
676
|
const pipelineManager = this.#scene?.postProcessRenderPipelineManager;
|
|
653
677
|
|
|
654
678
|
if (!this.#scene || !pipelineManager || this.#scene?.activeCamera === null) {
|
|
@@ -661,19 +685,23 @@ export default class BabylonJSController {
|
|
|
661
685
|
return false;
|
|
662
686
|
}
|
|
663
687
|
|
|
664
|
-
if (!this.#renderPipelines.iblShadows) {
|
|
688
|
+
if (!this.#renderPipelines.iblShadows || !this.#renderPipelines.iblShadows?.name) {
|
|
665
689
|
return false;
|
|
666
690
|
}
|
|
667
691
|
|
|
668
692
|
const pipelineName = this.#renderPipelines.iblShadows.name;
|
|
669
|
-
|
|
693
|
+
let iblShadowsPipeline = supportedPipelines ? supportedPipelines.find((pipeline) => pipeline.name === pipelineName) : false;
|
|
670
694
|
|
|
671
|
-
if (
|
|
695
|
+
if (iblShadowsPipeline) {
|
|
696
|
+
iblShadowsPipeline.toggleShadow(false);
|
|
697
|
+
iblShadowsPipeline.clearShadowCastingMeshes();
|
|
698
|
+
iblShadowsPipeline.clearShadowReceivingMaterials();
|
|
699
|
+
iblShadowsPipeline.resetAccumulation();
|
|
672
700
|
pipelineManager.detachCamerasFromRenderPipeline(pipelineName, [this.#scene.activeCamera]);
|
|
673
|
-
defaultPipeline.dispose();
|
|
674
701
|
pipelineManager.removePipeline(pipelineName);
|
|
675
702
|
pipelineManager.update();
|
|
676
|
-
|
|
703
|
+
iblShadowsPipeline.dispose();
|
|
704
|
+
this.#renderPipelines.iblShadows = iblShadowsPipeline = null;
|
|
677
705
|
}
|
|
678
706
|
|
|
679
707
|
return true;
|
|
@@ -685,24 +713,36 @@ export default class BabylonJSController {
|
|
|
685
713
|
* Configures pipeline options for resolution, sampling, opacity, and debugging.
|
|
686
714
|
* Only applies if the scene has an environment texture set.
|
|
687
715
|
* @private
|
|
688
|
-
* @returns {void|
|
|
716
|
+
* @returns {Promise<void|boolean>} Returns false if no environment texture is set; otherwise void.
|
|
689
717
|
*/
|
|
690
|
-
#initializeIBLShadows() {
|
|
718
|
+
async #initializeIBLShadows() {
|
|
719
|
+
|
|
720
|
+
await this.#scene.whenReadyAsync();
|
|
721
|
+
|
|
691
722
|
const pipelineManager = this.#scene?.postProcessRenderPipelineManager;
|
|
692
723
|
|
|
693
724
|
if (!this.#scene || !pipelineManager || this.#scene?.activeCamera === null) {
|
|
694
725
|
return false;
|
|
695
726
|
}
|
|
696
|
-
|
|
727
|
+
|
|
697
728
|
if (!this.#scene.environmentTexture) {
|
|
698
729
|
return false;
|
|
699
730
|
}
|
|
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
|
+
|
|
700
740
|
const supportedPipelines = pipelineManager.supportedPipelines;
|
|
701
741
|
|
|
702
742
|
if (!supportedPipelines) {
|
|
703
743
|
return false;
|
|
704
744
|
}
|
|
705
|
-
|
|
745
|
+
|
|
706
746
|
const pipelineName = "PrefViewerIblShadowsRenderPipeline";
|
|
707
747
|
|
|
708
748
|
const pipelineOptions = {
|
|
@@ -714,13 +754,13 @@ export default class BabylonJSController {
|
|
|
714
754
|
shadowOpacity: 0.85,
|
|
715
755
|
};
|
|
716
756
|
|
|
717
|
-
|
|
757
|
+
let iblShadowsPipeline = new IblShadowsRenderPipeline(pipelineName, this.#scene, pipelineOptions, [this.#scene.activeCamera]);
|
|
718
758
|
|
|
719
|
-
if (!
|
|
759
|
+
if (!iblShadowsPipeline) {
|
|
720
760
|
return false;
|
|
721
761
|
}
|
|
722
762
|
|
|
723
|
-
if (
|
|
763
|
+
if (iblShadowsPipeline.isSupported) {
|
|
724
764
|
// Disable all debug passes for performance
|
|
725
765
|
const pipelineProps = {
|
|
726
766
|
allowDebugPasses: false,
|
|
@@ -734,13 +774,8 @@ export default class BabylonJSController {
|
|
|
734
774
|
accumulationPassDebugEnabled: false,
|
|
735
775
|
};
|
|
736
776
|
|
|
737
|
-
Object.assign(
|
|
738
|
-
|
|
739
|
-
if (this.#renderPipelines.iblShadows._ssaoPostProcess) {
|
|
740
|
-
this.#renderPipelines.iblShadows._ssaoPostProcess.autoClear = false;
|
|
741
|
-
this.#renderPipelines.iblShadows._ssaoPostProcess.samples = 1;
|
|
742
|
-
}
|
|
743
|
-
|
|
777
|
+
Object.assign(iblShadowsPipeline, pipelineProps);
|
|
778
|
+
|
|
744
779
|
this.#scene.meshes.forEach((mesh) => {
|
|
745
780
|
const isRootMesh = mesh.id.startsWith("__root__");
|
|
746
781
|
if (isRootMesh) {
|
|
@@ -750,25 +785,28 @@ export default class BabylonJSController {
|
|
|
750
785
|
const isHDRIMesh = mesh.name?.toLowerCase() === "hdri";
|
|
751
786
|
const extrasCastShadows = mesh.metadata?.gltf?.extras?.castShadows;
|
|
752
787
|
const meshGenerateShadows = typeof extrasCastShadows === "boolean" ? extrasCastShadows : isHDRIMesh ? false : true;
|
|
753
|
-
|
|
788
|
+
|
|
754
789
|
if (meshGenerateShadows) {
|
|
755
|
-
|
|
756
|
-
|
|
790
|
+
iblShadowsPipeline.addShadowCastingMesh(mesh);
|
|
791
|
+
iblShadowsPipeline.updateSceneBounds();
|
|
757
792
|
}
|
|
758
793
|
});
|
|
759
|
-
|
|
794
|
+
|
|
760
795
|
this.#scene.materials.forEach((material) => {
|
|
761
796
|
if (material instanceof PBRMaterial) {
|
|
762
797
|
material.enableSpecularAntiAliasing = false;
|
|
763
798
|
}
|
|
764
|
-
|
|
799
|
+
iblShadowsPipeline.addShadowReceivingMaterial(material);
|
|
765
800
|
});
|
|
766
|
-
|
|
767
|
-
|
|
801
|
+
|
|
802
|
+
iblShadowsPipeline.toggleShadow(true);
|
|
803
|
+
iblShadowsPipeline.updateVoxelization();
|
|
804
|
+
this.#renderPipelines.iblShadows = iblShadowsPipeline;
|
|
768
805
|
pipelineManager.update();
|
|
769
806
|
return true;
|
|
770
807
|
} else {
|
|
771
|
-
|
|
808
|
+
iblShadowsPipeline.dispose();
|
|
809
|
+
this.#renderPipelines.iblShadows = iblShadowsPipeline = null;
|
|
772
810
|
pipelineManager.update();
|
|
773
811
|
return false;
|
|
774
812
|
}
|
|
@@ -802,7 +840,7 @@ export default class BabylonJSController {
|
|
|
802
840
|
* @private
|
|
803
841
|
* @returns {void}
|
|
804
842
|
*/
|
|
805
|
-
#initializeDefaultLightShadows() {
|
|
843
|
+
async #initializeDefaultLightShadows() {
|
|
806
844
|
if (!this.#dirLight) {
|
|
807
845
|
return;
|
|
808
846
|
}
|
|
@@ -825,7 +863,7 @@ export default class BabylonJSController {
|
|
|
825
863
|
* @private
|
|
826
864
|
* @returns {void}
|
|
827
865
|
*/
|
|
828
|
-
#initializeEnvironmentShadows() {
|
|
866
|
+
async #initializeEnvironmentShadows() {
|
|
829
867
|
this.#shadowGen = this.#shadowGen.filter((generator) => {
|
|
830
868
|
if (!generator || typeof generator.getLight !== "function") {
|
|
831
869
|
return false;
|
|
@@ -874,12 +912,12 @@ export default class BabylonJSController {
|
|
|
874
912
|
* @private
|
|
875
913
|
* @returns {void}
|
|
876
914
|
*/
|
|
877
|
-
#disableShadows() {
|
|
915
|
+
async #disableShadows() {
|
|
878
916
|
this.#shadowGen.forEach((shadowGenerator) => {
|
|
879
917
|
shadowGenerator.dispose();
|
|
880
918
|
});
|
|
881
919
|
this.#shadowGen = [];
|
|
882
|
-
this.#disableIBLShadows();
|
|
920
|
+
await this.#disableIBLShadows();
|
|
883
921
|
}
|
|
884
922
|
|
|
885
923
|
/**
|
|
@@ -890,28 +928,22 @@ export default class BabylonJSController {
|
|
|
890
928
|
* If no environment texture is set, initializes IBL shadows.
|
|
891
929
|
* Otherwise, sets up shadow casting and receiving for all relevant meshes using the shadow generator.
|
|
892
930
|
*/
|
|
893
|
-
#initializeShadows() {
|
|
931
|
+
async #initializeShadows() {
|
|
894
932
|
if (!this.#settings.shadowsEnabled) {
|
|
895
933
|
return false;
|
|
896
934
|
}
|
|
897
935
|
|
|
898
936
|
this.#ensureMeshesReceiveShadows();
|
|
899
937
|
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
const self = this;
|
|
906
|
-
this.#scene.environmentTexture.onLoadObservable.addOnce(() => {
|
|
907
|
-
self.#initializeIBLShadows();
|
|
908
|
-
});
|
|
909
|
-
}
|
|
910
|
-
}
|
|
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();
|
|
911
943
|
} else {
|
|
912
|
-
this.#initializeDefaultLightShadows();
|
|
944
|
+
await this.#initializeDefaultLightShadows();
|
|
913
945
|
}
|
|
914
|
-
this.#initializeEnvironmentShadows();
|
|
946
|
+
await this.#initializeEnvironmentShadows();
|
|
915
947
|
}
|
|
916
948
|
|
|
917
949
|
/**
|
|
@@ -1244,14 +1276,8 @@ export default class BabylonJSController {
|
|
|
1244
1276
|
* @private
|
|
1245
1277
|
* @returns {boolean} True when lights were refreshed due to pending IBL changes, otherwise false.
|
|
1246
1278
|
*/
|
|
1247
|
-
#setOptions_IBL() {
|
|
1248
|
-
|
|
1249
|
-
this.#options.ibl.setSuccess(true);
|
|
1250
|
-
this.#createLights();
|
|
1251
|
-
return true;
|
|
1252
|
-
}
|
|
1253
|
-
this.#createLights();
|
|
1254
|
-
return false;
|
|
1279
|
+
async #setOptions_IBL() {
|
|
1280
|
+
return await this.#createLights();
|
|
1255
1281
|
}
|
|
1256
1282
|
|
|
1257
1283
|
/**
|
|
@@ -1373,6 +1399,52 @@ export default class BabylonJSController {
|
|
|
1373
1399
|
return this.#addContainer(container, false);
|
|
1374
1400
|
}
|
|
1375
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
|
+
|
|
1376
1448
|
/**
|
|
1377
1449
|
* Sets the visibility of wall and floor meshes in the model container based on the provided value or environment visibility.
|
|
1378
1450
|
* @private
|
|
@@ -1394,9 +1466,9 @@ export default class BabylonJSController {
|
|
|
1394
1466
|
* @private
|
|
1395
1467
|
* @returns {void}
|
|
1396
1468
|
*/
|
|
1397
|
-
#stopRender() {
|
|
1469
|
+
async #stopRender() {
|
|
1398
1470
|
this.#engine.stopRenderLoop(this.#handlers.renderLoop);
|
|
1399
|
-
this.#unloadCameraDependentEffects();
|
|
1471
|
+
await this.#unloadCameraDependentEffects();
|
|
1400
1472
|
}
|
|
1401
1473
|
/**
|
|
1402
1474
|
* Starts the Babylon.js render loop for the current scene.
|
|
@@ -1405,21 +1477,30 @@ export default class BabylonJSController {
|
|
|
1405
1477
|
* @returns {Promise<void>}
|
|
1406
1478
|
*/
|
|
1407
1479
|
async #startRender() {
|
|
1408
|
-
this.#loadCameraDependentEffects();
|
|
1409
|
-
|
|
1410
|
-
|
|
1480
|
+
await this.#loadCameraDependentEffects();
|
|
1481
|
+
this.#scene.executeWhenReady(() => {
|
|
1482
|
+
this.#engine.runRenderLoop(this.#handlers.renderLoop);
|
|
1483
|
+
});
|
|
1411
1484
|
}
|
|
1412
1485
|
|
|
1413
1486
|
/**
|
|
1414
|
-
* 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`.
|
|
1415
1490
|
* @private
|
|
1416
|
-
* @param {object} container -
|
|
1417
|
-
* @
|
|
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.
|
|
1418
1495
|
* @description
|
|
1419
|
-
*
|
|
1420
|
-
*
|
|
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.
|
|
1421
1501
|
*/
|
|
1422
|
-
async #loadAssetContainer(container) {
|
|
1502
|
+
async #loadAssetContainer(container, force = false) {
|
|
1503
|
+
|
|
1423
1504
|
if (container?.state?.update?.storage === undefined || container?.state?.size === undefined || container?.state?.timeStamp === undefined) {
|
|
1424
1505
|
return [container, false];
|
|
1425
1506
|
}
|
|
@@ -1431,9 +1512,12 @@ export default class BabylonJSController {
|
|
|
1431
1512
|
if (!this.#gltfResolver) {
|
|
1432
1513
|
this.#gltfResolver = new GLTFResolver();
|
|
1433
1514
|
}
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
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
|
+
|
|
1437
1521
|
if (!sourceData) {
|
|
1438
1522
|
return [container, false];
|
|
1439
1523
|
}
|
|
@@ -1475,7 +1559,7 @@ export default class BabylonJSController {
|
|
|
1475
1559
|
* Returns an object with success status and error details.
|
|
1476
1560
|
*/
|
|
1477
1561
|
async #loadContainers() {
|
|
1478
|
-
this.#stopRender();
|
|
1562
|
+
await this.#stopRender();
|
|
1479
1563
|
|
|
1480
1564
|
let oldModelMetadata = { ...(this.#containers.model?.state?.metadata ?? {}) };
|
|
1481
1565
|
let newModelMetadata = {};
|
|
@@ -1491,22 +1575,19 @@ export default class BabylonJSController {
|
|
|
1491
1575
|
};
|
|
1492
1576
|
|
|
1493
1577
|
await Promise.allSettled(promiseArray)
|
|
1494
|
-
.then((values) => {
|
|
1578
|
+
.then(async (values) => {
|
|
1495
1579
|
this.#disposeAnimationController();
|
|
1496
1580
|
values.forEach((result) => {
|
|
1497
1581
|
const container = result.value ? result.value[0] : null;
|
|
1498
1582
|
const assetContainer = result.value ? result.value[1] : null;
|
|
1499
1583
|
if (result.status === "fulfilled" && assetContainer) {
|
|
1500
1584
|
if (container.state.name === "model") {
|
|
1501
|
-
assetContainer
|
|
1585
|
+
this.#assetContainer_deleteLights(assetContainer);
|
|
1586
|
+
this.#assetContainer_stopAnimations(assetContainer);
|
|
1502
1587
|
newModelMetadata = { ...(container.state.update.metadata ?? {}) };
|
|
1503
1588
|
}
|
|
1504
1589
|
if (container.state.name === "model" || container.state.name === "environment") {
|
|
1505
|
-
assetContainer
|
|
1506
|
-
// To avoid conflicts when reloading the model we rename the id because Babylon.js caches the camera's SSAO effect by id.
|
|
1507
|
-
const sufix = "_" + Date.now();
|
|
1508
|
-
camera.id = `${camera.id || camera.name || "camera"}${sufix}`;
|
|
1509
|
-
});
|
|
1590
|
+
this.#assetContainer_retagCameras(assetContainer);
|
|
1510
1591
|
}
|
|
1511
1592
|
this.#replaceContainer(container, assetContainer);
|
|
1512
1593
|
container.state.setSuccess(true);
|
|
@@ -1520,7 +1601,7 @@ export default class BabylonJSController {
|
|
|
1520
1601
|
|
|
1521
1602
|
this.#setOptions_Materials();
|
|
1522
1603
|
this.#setOptions_Camera();
|
|
1523
|
-
this.#setOptions_IBL();
|
|
1604
|
+
await this.#setOptions_IBL();
|
|
1524
1605
|
this.#setVisibilityOfWallAndFloorInModel();
|
|
1525
1606
|
detail.success = true;
|
|
1526
1607
|
})
|
|
@@ -1534,7 +1615,7 @@ export default class BabylonJSController {
|
|
|
1534
1615
|
this.#checkModelMetadata(oldModelMetadata, newModelMetadata);
|
|
1535
1616
|
this.#setMaxSimultaneousLights();
|
|
1536
1617
|
this.#babylonJSAnimationController = new BabylonJSAnimationController(this.#containers.model.assetContainer);
|
|
1537
|
-
this.#startRender();
|
|
1618
|
+
await this.#startRender();
|
|
1538
1619
|
});
|
|
1539
1620
|
return detail;
|
|
1540
1621
|
}
|
|
@@ -1545,10 +1626,10 @@ export default class BabylonJSController {
|
|
|
1545
1626
|
* @private
|
|
1546
1627
|
* @returns {void}
|
|
1547
1628
|
*/
|
|
1548
|
-
#loadCameraDependentEffects() {
|
|
1549
|
-
this.#initializeVisualImprovements();
|
|
1550
|
-
this.#initializeAmbientOcclussion();
|
|
1551
|
-
this.#initializeShadows();
|
|
1629
|
+
async #loadCameraDependentEffects() {
|
|
1630
|
+
await this.#initializeVisualImprovements();
|
|
1631
|
+
await this.#initializeAmbientOcclussion();
|
|
1632
|
+
await this.#initializeShadows();
|
|
1552
1633
|
}
|
|
1553
1634
|
|
|
1554
1635
|
/**
|
|
@@ -1557,10 +1638,10 @@ export default class BabylonJSController {
|
|
|
1557
1638
|
* @private
|
|
1558
1639
|
* @returns {void}
|
|
1559
1640
|
*/
|
|
1560
|
-
#unloadCameraDependentEffects() {
|
|
1561
|
-
this.#disableVisualImprovements();
|
|
1562
|
-
this.#disableAmbientOcclusion();
|
|
1563
|
-
this.#disableShadows();
|
|
1641
|
+
async #unloadCameraDependentEffects() {
|
|
1642
|
+
await this.#disableVisualImprovements();
|
|
1643
|
+
await this.#disableAmbientOcclusion();
|
|
1644
|
+
await this.#disableShadows();
|
|
1564
1645
|
}
|
|
1565
1646
|
|
|
1566
1647
|
/**
|
|
@@ -1804,7 +1885,15 @@ export default class BabylonJSController {
|
|
|
1804
1885
|
geometryBufferRenderer.generateNormalsInWorldSpace = true;
|
|
1805
1886
|
}
|
|
1806
1887
|
|
|
1807
|
-
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
|
+
|
|
1808
1897
|
this.#createCamera();
|
|
1809
1898
|
this.#enableInteraction();
|
|
1810
1899
|
await this.#createXRExperience();
|
|
@@ -1950,26 +2039,6 @@ export default class BabylonJSController {
|
|
|
1950
2039
|
return await this.#loadContainers();
|
|
1951
2040
|
}
|
|
1952
2041
|
|
|
1953
|
-
/**
|
|
1954
|
-
* Merges incoming render flags with the current configuration, persists them, and marks
|
|
1955
|
-
* all dependent loaders/options as pending when something actually changed.
|
|
1956
|
-
* @public
|
|
1957
|
-
* @param {{antiAliasingEnabled?:boolean, ambientOcclusionEnabled?:boolean, iblEnabled?:boolean, shadowsEnabled?:boolean}} settings Partial set of render toggles to apply.
|
|
1958
|
-
* @returns {{changed:boolean, settings:{antiAliasingEnabled:boolean, ambientOcclusionEnabled:boolean, iblEnabled:boolean, shadowsEnabled:boolean}}}
|
|
1959
|
-
* @description
|
|
1960
|
-
* Callers can inspect the `changed` flag to decide whether to trigger a reload with
|
|
1961
|
-
* `reloadWithCurrentSettings()` or simply reuse the returned snapshot.
|
|
1962
|
-
*/
|
|
1963
|
-
scheduleRenderSettingsReload(settings = {}) {
|
|
1964
|
-
const changed = this.#applyRenderSettings(settings);
|
|
1965
|
-
if (!changed) {
|
|
1966
|
-
return { changed: false, settings: this.getRenderSettings() };
|
|
1967
|
-
}
|
|
1968
|
-
this.#markContainersForReload();
|
|
1969
|
-
this.#markOptionsForReload();
|
|
1970
|
-
return { changed: true, settings: this.getRenderSettings() };
|
|
1971
|
-
}
|
|
1972
|
-
|
|
1973
2042
|
/**
|
|
1974
2043
|
* Applies camera options from the configuration to the active camera.
|
|
1975
2044
|
* Stops and restarts the render loop to apply changes.
|
|
@@ -2000,7 +2069,7 @@ export default class BabylonJSController {
|
|
|
2000
2069
|
* Reapplies image-based lighting configuration (HDR URL, intensity, shadow mode).
|
|
2001
2070
|
* Stops rendering, pushes pending IBL state into the scene, rebuilds camera-dependent effects, then resumes rendering.
|
|
2002
2071
|
* @public
|
|
2003
|
-
* @returns {
|
|
2072
|
+
* @returns {boolean} True if IBL options were set successfully, false otherwise.
|
|
2004
2073
|
*/
|
|
2005
2074
|
setIBLOptions() {
|
|
2006
2075
|
this.#stopRender();
|
|
@@ -2033,6 +2102,26 @@ export default class BabylonJSController {
|
|
|
2033
2102
|
this.#startRender();
|
|
2034
2103
|
}
|
|
2035
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
|
+
|
|
2036
2125
|
/**
|
|
2037
2126
|
* Reloads every asset container using the latest staged render settings.
|
|
2038
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.
|