@preference-sl/pref-viewer 2.12.0-beta.3 → 2.12.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 +1 -1
- package/src/babylonjs-controller.js +341 -80
- package/src/pref-viewer-3d-data.js +50 -0
- package/src/pref-viewer-3d.js +63 -3
package/package.json
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ArcRotateCamera, AssetContainer, Camera, Color4, DirectionalLight, Engine, HDRCubeTexture, HemisphericLight, IblShadowsRenderPipeline, LoadAssetContainerAsync, MeshBuilder, PointerEventTypes, PointLight, Scene, ShadowGenerator, Tools, Vector3, WebXRDefaultExperience, WebXRFeatureName, WebXRSessionManager, WebXRState } from "@babylonjs/core";
|
|
1
|
+
import { ArcRotateCamera, AssetContainer, Camera, Color4, DefaultRenderingPipeline, DirectionalLight, Engine, HDRCubeTexture, HemisphericLight, IblShadowsRenderPipeline, LoadAssetContainerAsync, MeshBuilder, PBRMaterial, PointerEventTypes, PointLight, Scene, ShadowGenerator, SpotLight, SSAORenderingPipeline, Tools, Vector3, WebXRDefaultExperience, WebXRFeatureName, WebXRSessionManager, WebXRState } 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";
|
|
@@ -38,15 +38,17 @@ import BabylonJSAnimationController from "./babylonjs-animation-controller.js";
|
|
|
38
38
|
* - Disable rendering: controller.disable();
|
|
39
39
|
*
|
|
40
40
|
* Public Methods:
|
|
41
|
-
* -
|
|
42
|
-
* -
|
|
43
|
-
* -
|
|
44
|
-
* -
|
|
45
|
-
* -
|
|
46
|
-
* -
|
|
47
|
-
* -
|
|
48
|
-
* -
|
|
49
|
-
* -
|
|
41
|
+
* - constructor(canvas, containers, options): Creates the controller, wires container state, and stores runtime options.
|
|
42
|
+
* - enable(): Boots the Babylon.js engine, scene, camera, baseline lights, XR support, and the render loop.
|
|
43
|
+
* - disable(): Stops rendering and disposes the engine, lights, XR experience, and observers.
|
|
44
|
+
* - load(): Reloads every pending asset container, re-applies options, and resolves with the loading summary { success, error }.
|
|
45
|
+
* - setCameraOptions(): Applies the pending camera selection, reinstalls dependent pipelines, and restarts rendering safely.
|
|
46
|
+
* - setMaterialOptions(): Re-applies all configured material overrides across visible containers and restarts rendering.
|
|
47
|
+
* - setIBLOptions(): Pushes pending HDR/IBL updates, refreshes dependent effects, and resumes the render loop.
|
|
48
|
+
* - setContainerVisibility(name, show): Toggles model/environment containers, syncing wall/floor helpers and component attributes.
|
|
49
|
+
* - downloadGLB(content): Exports the selected scope (scene/model/environment) into a time-stamped GLB and triggers the download.
|
|
50
|
+
* - downloadGLTF(content): Generates a glTF + BIN + textures ZIP for the requested scope, adding metadata comments for traceability.
|
|
51
|
+
* - downloadUSDZ(content): Builds an Apple USDZ archive for the requested scope and downloads it via blob streaming.
|
|
50
52
|
*
|
|
51
53
|
* Private Methods (using ECMAScript private fields):
|
|
52
54
|
* - #bindHandlers(): Pre-binds reusable event handlers to preserve stable references.
|
|
@@ -56,9 +58,13 @@ import BabylonJSAnimationController from "./babylonjs-animation-controller.js";
|
|
|
56
58
|
* - #createXRExperience(): Initializes WebXR AR experience.
|
|
57
59
|
* - #createCamera(): Creates and configures the main camera.
|
|
58
60
|
* - #createLights(): Creates and configures scene lights and shadows.
|
|
61
|
+
* - #initializeAmbientOcclussion(): Rebuilds the SSAO pipeline for the active camera.
|
|
62
|
+
* - #initializeVisualImprovements(): Reinstalls the default rendering pipeline (MSAA/FXAA/grain).
|
|
59
63
|
* - #initializeEnvironmentTexture(): Loads and sets the HDR environment texture.
|
|
60
64
|
* - #initializeIBLShadows(): Sets up IBL shadow pipeline and assigns meshes/materials.
|
|
61
65
|
* - #initializeShadows(): Sets up standard or IBL shadows for meshes.
|
|
66
|
+
* - #initializeDefaultLightShadows(): Configures soft shadows when no HDR environment exists.
|
|
67
|
+
* - #initializeEnvironmentShadows(): Rebuilds environment-provided shadow generators.
|
|
62
68
|
* - #setMaxSimultaneousLights(): Updates max simultaneous lights for all materials.
|
|
63
69
|
* - #onPointerObservable(info): Handles pointer events and dispatches to pointer/mouse handlers.
|
|
64
70
|
* - #onPointerUp(event, pickInfo): Handles pointer up events (e.g., right-click for animation menu).
|
|
@@ -73,6 +79,7 @@ import BabylonJSAnimationController from "./babylonjs-animation-controller.js";
|
|
|
73
79
|
* - #setOptionsMaterial(optionMaterial): Applies a material option to relevant meshes.
|
|
74
80
|
* - #setOptions_Materials(): Applies all material options from configuration.
|
|
75
81
|
* - #setOptions_Camera(): Applies camera options from configuration.
|
|
82
|
+
* - #setOptions_IBL(): Applies pending HDR/IBL option updates and refreshes lights.
|
|
76
83
|
* - #findContainerByName(name): Finds a container by its name.
|
|
77
84
|
* - #addContainer(container, updateVisibility): Adds a container to the scene and updates visibility.
|
|
78
85
|
* - #removeContainer(container, updateVisibility): Removes a container from the scene and updates visibility.
|
|
@@ -85,6 +92,7 @@ import BabylonJSAnimationController from "./babylonjs-animation-controller.js";
|
|
|
85
92
|
* - #startRender(): Starts the Babylon.js render loop.
|
|
86
93
|
* - #loadAssetContainer(container): Loads an asset container asynchronously.
|
|
87
94
|
* - #loadContainers(): Loads all asset containers and adds them to the scene.
|
|
95
|
+
* - #loadCameraDepentEffects(): Re-initializes camera-bound post-processes after reloads or option changes.
|
|
88
96
|
* - #checkModelMetadata(oldMetadata, newMetadata): Processes metadata changes after loading.
|
|
89
97
|
* - #checkInnerFloorTranslation(oldMetadata, newMetadata): Applies inner floor Y translations from metadata.
|
|
90
98
|
* - #translateNodeY(name, deltaY): Adjusts the Y position of a scene node.
|
|
@@ -107,7 +115,7 @@ export default class BabylonJSController {
|
|
|
107
115
|
#hemiLight = null;
|
|
108
116
|
#dirLight = null;
|
|
109
117
|
#cameraLight = null;
|
|
110
|
-
#shadowGen =
|
|
118
|
+
#shadowGen = [];
|
|
111
119
|
#XRExperience = null;
|
|
112
120
|
#canvasResizeObserver = null;
|
|
113
121
|
|
|
@@ -123,6 +131,8 @@ export default class BabylonJSController {
|
|
|
123
131
|
renderLoop: null,
|
|
124
132
|
};
|
|
125
133
|
|
|
134
|
+
#shadowsEnabled = false;
|
|
135
|
+
|
|
126
136
|
/**
|
|
127
137
|
* Constructs a new BabylonJSController instance.
|
|
128
138
|
* Initializes the canvas, asset containers, and options for the Babylon.js scene.
|
|
@@ -288,33 +298,151 @@ export default class BabylonJSController {
|
|
|
288
298
|
* @returns {void}
|
|
289
299
|
*/
|
|
290
300
|
#createLights() {
|
|
291
|
-
this.#
|
|
301
|
+
if (this.#options.ibl && this.#options.ibl.url) {
|
|
302
|
+
this.#hemiLight = this.#dirLight = this.#cameraLight = null;
|
|
303
|
+
this.#initializeEnvironmentTexture();
|
|
304
|
+
}
|
|
292
305
|
|
|
293
306
|
if (this.#scene.environmentTexture) {
|
|
294
307
|
return true;
|
|
295
308
|
}
|
|
296
309
|
|
|
297
310
|
// 1) Stronger ambient fill
|
|
298
|
-
this.#hemiLight = new HemisphericLight("
|
|
311
|
+
this.#hemiLight = new HemisphericLight("PrefViewerHemiLight", new Vector3(-10, 10, -10), this.#scene);
|
|
299
312
|
this.#hemiLight.intensity = 0.6;
|
|
300
313
|
|
|
301
314
|
// 2) Directional light from the front-right, angled slightly down
|
|
302
|
-
this.#dirLight = new DirectionalLight("
|
|
315
|
+
this.#dirLight = new DirectionalLight("PrefViewerDirLight", new Vector3(-10, 10, -10), this.#scene);
|
|
303
316
|
this.#dirLight.position = new Vector3(5, 4, 5); // light is IN FRONT + ABOVE + to the RIGHT
|
|
304
317
|
this.#dirLight.intensity = 0.6;
|
|
305
318
|
|
|
306
|
-
//
|
|
307
|
-
this.#
|
|
308
|
-
this.#shadowGen.useBlurExponentialShadowMap = true;
|
|
309
|
-
this.#shadowGen.blurKernel = 16;
|
|
310
|
-
this.#shadowGen.darkness = 0.5;
|
|
311
|
-
|
|
312
|
-
// 4) Camera‐attached headlight
|
|
313
|
-
this.#cameraLight = new PointLight("pl", this.#camera.position, this.#scene);
|
|
319
|
+
// 3) Camera‐attached headlight
|
|
320
|
+
this.#cameraLight = new PointLight("PrefViewerCameraLight", this.#camera.position, this.#scene);
|
|
314
321
|
this.#cameraLight.parent = this.#camera;
|
|
315
322
|
this.#cameraLight.intensity = 0.3;
|
|
316
323
|
}
|
|
317
324
|
|
|
325
|
+
/**
|
|
326
|
+
* Rebuilds the SSAO post-process pipeline to inject screenspace ambient occlusion on the active camera.
|
|
327
|
+
* Disposes previous SSAO pipelines, instantiates a tuned `SSAORenderingPipeline`, and attaches it to the
|
|
328
|
+
* current camera so contact shadows enhance depth perception once assets reload or the camera changes.
|
|
329
|
+
* @private
|
|
330
|
+
* @returns {boolean} True if the SSAO pipeline is supported and enabled, otherwise false.
|
|
331
|
+
*/
|
|
332
|
+
#initializeAmbientOcclussion() {
|
|
333
|
+
if (!this.#scene || !this.#scene.postProcessRenderPipelineManager || this.#scene.activeCamera === null) {
|
|
334
|
+
return false;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const pipelineName = "PrefViewerSSAORenderingPipeline";
|
|
338
|
+
|
|
339
|
+
const supportedPipelines = this.#scene.postProcessRenderPipelineManager.supportedPipelines;
|
|
340
|
+
|
|
341
|
+
if (!supportedPipelines) {
|
|
342
|
+
return false;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const oldSsaoPipeline = supportedPipelines ? supportedPipelines.find((pipeline) => pipeline.name === pipelineName) : false;
|
|
346
|
+
|
|
347
|
+
if (oldSsaoPipeline) {
|
|
348
|
+
oldSsaoPipeline.dispose();
|
|
349
|
+
this.#scene.postProcessRenderPipelineManager.update();
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const ssaoRatio = {
|
|
353
|
+
ssaoRatio: 0.5,
|
|
354
|
+
combineRatio: 1.0
|
|
355
|
+
};
|
|
356
|
+
|
|
357
|
+
const ssaoPipeline = new SSAORenderingPipeline(pipelineName, this.#scene, ssaoRatio, [this.#scene.activeCamera]);
|
|
358
|
+
|
|
359
|
+
if (!ssaoPipeline){
|
|
360
|
+
return false;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
if (ssaoPipeline.isSupported) {
|
|
364
|
+
ssaoPipeline.fallOff = 0.000001;
|
|
365
|
+
ssaoPipeline.area = 1;
|
|
366
|
+
ssaoPipeline.radius = 0.0001;
|
|
367
|
+
ssaoPipeline.totalStrength = 1;
|
|
368
|
+
ssaoPipeline.base = 0.6;
|
|
369
|
+
this.#scene.postProcessRenderPipelineManager.update();
|
|
370
|
+
return true;
|
|
371
|
+
} else {
|
|
372
|
+
ssaoPipeline.dispose();
|
|
373
|
+
this.#scene.postProcessRenderPipelineManager.update();
|
|
374
|
+
return false;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* Rebuilds the custom default rendering pipeline (MSAA, FXAA, film grain) for the active camera.
|
|
380
|
+
* Disposes any previous pipeline instance to avoid duplicates, then attaches a fresh
|
|
381
|
+
* `DefaultRenderingPipeline` with tuned settings for sharper anti-aliasing and subtle grain.
|
|
382
|
+
* @private
|
|
383
|
+
* @returns {boolean} True when the pipeline is supported and active, otherwise false.
|
|
384
|
+
* @see {@link https://doc.babylonjs.com/features/featuresDeepDive/postProcesses/defaultRenderingPipeline|Using the Default Rendering Pipeline | Babylon.js Documentation}
|
|
385
|
+
*/
|
|
386
|
+
#initializeVisualImprovements() {
|
|
387
|
+
if (!this.#scene || !this.#scene.postProcessRenderPipelineManager || this.#scene.activeCamera === null) {
|
|
388
|
+
return false;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
const pipelineName = "PrefViewerDefaultRenderingPipeline";
|
|
392
|
+
const supportedPipelines = this.#scene.postProcessRenderPipelineManager.supportedPipelines;
|
|
393
|
+
|
|
394
|
+
if (!supportedPipelines) {
|
|
395
|
+
return false;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
const oldDefaultPipeline = supportedPipelines ? supportedPipelines.find((pipeline) => pipeline.name === pipelineName) : false;
|
|
399
|
+
|
|
400
|
+
if (oldDefaultPipeline) {
|
|
401
|
+
oldDefaultPipeline.dispose();
|
|
402
|
+
this.#scene.postProcessRenderPipelineManager.update();
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
const defaultPipeline = new DefaultRenderingPipeline(pipelineName, true, this.#scene, [this.#scene.activeCamera], true);
|
|
406
|
+
|
|
407
|
+
if (!defaultPipeline){
|
|
408
|
+
return false;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
if (defaultPipeline.isSupported) {
|
|
412
|
+
// MSAA - Multisample Anti-Aliasing
|
|
413
|
+
const caps = this.#scene.getEngine()?.getCaps?.() || {};
|
|
414
|
+
const maxSamples = typeof caps.maxMSAASamples === "number" ? caps.maxMSAASamples : 4;
|
|
415
|
+
defaultPipeline.samples = Math.max(1, Math.min(8, maxSamples));
|
|
416
|
+
|
|
417
|
+
// FXAA - Fast Approximate Anti-Aliasing
|
|
418
|
+
defaultPipeline.fxaaEnabled = true;
|
|
419
|
+
defaultPipeline.fxaa.samples = 8;
|
|
420
|
+
defaultPipeline.fxaa.adaptScaleToCurrentViewport = true;
|
|
421
|
+
if (defaultPipeline.fxaa.edgeThreshold !== undefined) {
|
|
422
|
+
defaultPipeline.fxaa.edgeThreshold = 0.125;
|
|
423
|
+
}
|
|
424
|
+
if (defaultPipeline.fxaa.edgeThresholdMin !== undefined) {
|
|
425
|
+
defaultPipeline.fxaa.edgeThresholdMin = 0.0625;
|
|
426
|
+
}
|
|
427
|
+
if (defaultPipeline.fxaa.subPixelQuality !== undefined) {
|
|
428
|
+
defaultPipeline.fxaa.subPixelQuality = 0.75;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// Grain
|
|
432
|
+
defaultPipeline.grainEnabled = true;
|
|
433
|
+
defaultPipeline.grain.adaptScaleToCurrentViewport = true;
|
|
434
|
+
defaultPipeline.grain.animated = false;
|
|
435
|
+
defaultPipeline.grain.intensity = 3;
|
|
436
|
+
|
|
437
|
+
this.#scene.postProcessRenderPipelineManager.update();
|
|
438
|
+
return true;
|
|
439
|
+
} else {
|
|
440
|
+
defaultPipeline.dispose();
|
|
441
|
+
this.#scene.postProcessRenderPipelineManager.update();
|
|
442
|
+
return false;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
318
446
|
/**
|
|
319
447
|
* Initializes the environment texture for the Babylon.js scene.
|
|
320
448
|
* Loads an HDR texture from a predefined URI and assigns it to the scene's environmentTexture property.
|
|
@@ -323,15 +451,9 @@ export default class BabylonJSController {
|
|
|
323
451
|
* @returns {boolean}
|
|
324
452
|
*/
|
|
325
453
|
#initializeEnvironmentTexture() {
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
}
|
|
330
|
-
const hdrTextureURI = "../src/environments/noon_grass.hdr";
|
|
331
|
-
const hdrTexture = new HDRCubeTexture(hdrTextureURI, this.#scene, 128);
|
|
332
|
-
hdrTexture.gammaSpace = true;
|
|
333
|
-
hdrTexture._noMipmap = false;
|
|
334
|
-
hdrTexture.level = 2.0;
|
|
454
|
+
const hdrTextureURI = this.#options.ibl.url;
|
|
455
|
+
const hdrTexture = new HDRCubeTexture(hdrTextureURI, this.#scene, 128, false, false, false, true);
|
|
456
|
+
hdrTexture.level = this.#options.ibl.intensity;
|
|
335
457
|
this.#scene.environmentTexture = hdrTexture;
|
|
336
458
|
return true;
|
|
337
459
|
}
|
|
@@ -345,61 +467,141 @@ export default class BabylonJSController {
|
|
|
345
467
|
* @returns {void|false} Returns false if no environment texture is set; otherwise void.
|
|
346
468
|
*/
|
|
347
469
|
#initializeIBLShadows() {
|
|
348
|
-
if (!this.#scene
|
|
470
|
+
if (!this.#scene || !this.#scene.postProcessRenderPipelineManager || this.#scene.activeCamera === null) {
|
|
349
471
|
return false;
|
|
350
472
|
}
|
|
351
473
|
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
474
|
+
// if (!this.#scene.environmentTexture || !this.#scene.environmentTexture.isReady()) {
|
|
475
|
+
if (!this.#scene.environmentTexture) {
|
|
476
|
+
return false;
|
|
477
|
+
}
|
|
478
|
+
const pipelineName = "PrefViewerIblShadowsRenderPipeline";
|
|
479
|
+
const supportedPipelines = this.#scene.postProcessRenderPipelineManager.supportedPipelines;
|
|
480
|
+
|
|
481
|
+
if (!supportedPipelines) {
|
|
482
|
+
return false;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
const oldIblShadowsRenderPipeline = supportedPipelines ? supportedPipelines.find((pipeline) => pipeline.name === pipelineName) : false;
|
|
486
|
+
|
|
487
|
+
if (oldIblShadowsRenderPipeline) {
|
|
488
|
+
oldIblShadowsRenderPipeline.dispose();
|
|
489
|
+
this.#scene.postProcessRenderPipelineManager.update();
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
const pipelineOptions = {
|
|
493
|
+
resolutionExp: 8, // Higher resolution for better shadow quality
|
|
494
|
+
sampleDirections: 4, // More sample directions for smoother shadows
|
|
495
|
+
ssShadowsEnabled: true,
|
|
496
|
+
shadowRemanence: 0.85,
|
|
497
|
+
triPlanarVoxelization: true,
|
|
498
|
+
shadowOpacity: 0.85,
|
|
499
|
+
};
|
|
500
|
+
|
|
501
|
+
const iblShadowsRenderPipeline = new IblShadowsRenderPipeline(pipelineName, this.#scene, pipelineOptions, [this.#scene.activeCamera]);
|
|
502
|
+
|
|
503
|
+
if (!iblShadowsRenderPipeline) {
|
|
504
|
+
return false;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
if (iblShadowsRenderPipeline.isSupported) {
|
|
375
508
|
// Disable all debug passes for performance
|
|
376
509
|
const pipelineProps = {
|
|
377
510
|
allowDebugPasses: false,
|
|
378
511
|
gbufferDebugEnabled: false,
|
|
379
512
|
importanceSamplingDebugEnabled: false,
|
|
380
513
|
voxelDebugEnabled: false,
|
|
381
|
-
voxelDebugDisplayMip:
|
|
514
|
+
voxelDebugDisplayMip: 1,
|
|
382
515
|
voxelDebugAxis: 0,
|
|
383
516
|
voxelTracingDebugEnabled: false,
|
|
384
517
|
spatialBlurPassDebugEnabled: false,
|
|
385
518
|
accumulationPassDebugEnabled: false,
|
|
386
519
|
};
|
|
387
|
-
Object.assign(pipeline, pipelineProps);
|
|
388
|
-
return pipeline;
|
|
389
|
-
};
|
|
390
520
|
|
|
391
|
-
|
|
521
|
+
Object.assign(iblShadowsRenderPipeline, pipelineProps);
|
|
392
522
|
|
|
523
|
+
this.#scene.meshes.forEach((mesh) => {
|
|
524
|
+
if (mesh.id.startsWith("__root__") || mesh.name === "hdri") {
|
|
525
|
+
return false;
|
|
526
|
+
}
|
|
527
|
+
iblShadowsRenderPipeline.addShadowCastingMesh(mesh);
|
|
528
|
+
iblShadowsRenderPipeline.updateSceneBounds();
|
|
529
|
+
mesh.receiveShadows = true; // Not necessary for IBL shadows, but yes for standard shadows
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
this.#scene.materials.forEach((material) => {
|
|
533
|
+
if (material instanceof PBRMaterial) {
|
|
534
|
+
material.enableSpecularAntiAliasing = false;
|
|
535
|
+
}
|
|
536
|
+
iblShadowsRenderPipeline.addShadowReceivingMaterial(material);
|
|
537
|
+
});
|
|
538
|
+
|
|
539
|
+
iblShadowsRenderPipeline.updateVoxelization();
|
|
540
|
+
this.#scene.postProcessRenderPipelineManager.update();
|
|
541
|
+
return true;
|
|
542
|
+
} else {
|
|
543
|
+
iblShadowsRenderPipeline.dispose();
|
|
544
|
+
this.#scene.postProcessRenderPipelineManager.update();
|
|
545
|
+
return false;
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
/**
|
|
550
|
+
* Configures soft shadows for the built-in directional light used when no HDR environment is present.
|
|
551
|
+
* @private
|
|
552
|
+
* @returns {void}
|
|
553
|
+
*/
|
|
554
|
+
#initializeDefaultLightShadows() {
|
|
555
|
+
this.#shadowGen = [];
|
|
556
|
+
const shadowGenerator = new ShadowGenerator(1024, this.#dirLight);
|
|
557
|
+
shadowGenerator.useBlurExponentialShadowMap = true;
|
|
558
|
+
shadowGenerator.blurKernel = 16;
|
|
559
|
+
shadowGenerator.darkness = 0.5;
|
|
393
560
|
this.#scene.meshes.forEach((mesh) => {
|
|
394
|
-
if (mesh.id.startsWith("__root__")
|
|
561
|
+
if (mesh.id.startsWith("__root__")) {
|
|
562
|
+
return false;
|
|
563
|
+
}
|
|
564
|
+
if (mesh.name !== "hdri") {
|
|
565
|
+
shadowGenerator.addShadowCaster(mesh, true);
|
|
566
|
+
}
|
|
567
|
+
mesh.receiveShadows = true;
|
|
568
|
+
});
|
|
569
|
+
this.#shadowGen.push(shadowGenerator);
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
/**
|
|
573
|
+
* Rebuilds the shadow generators contributed by the environment container.
|
|
574
|
+
* Keeps only the generator bound to the built-in `PrefViewerDirLight` so its shadows persist,
|
|
575
|
+
* then adds generators for every directional or spot light coming from the environment asset container.
|
|
576
|
+
* @private
|
|
577
|
+
* @returns {void}
|
|
578
|
+
*/
|
|
579
|
+
#initializeEnvironmentShadows() {
|
|
580
|
+
this.#shadowGen = this.#shadowGen.filter((generator) => {
|
|
581
|
+
if (!generator || typeof generator.getLight !== "function") {
|
|
395
582
|
return false;
|
|
396
583
|
}
|
|
397
|
-
|
|
398
|
-
iblShadowsPipeline.updateSceneBounds();
|
|
584
|
+
return generator.getLight()?.name === "PrefViewerDirLight";
|
|
399
585
|
});
|
|
400
586
|
|
|
401
|
-
this.#
|
|
402
|
-
|
|
587
|
+
this.#containers.environment?.AssetContainer?.lights.forEach((light) => {
|
|
588
|
+
// Only shadows for DirectionalLight and SpotLight types
|
|
589
|
+
if (!(light instanceof DirectionalLight || light instanceof SpotLight)) {
|
|
590
|
+
return;
|
|
591
|
+
}
|
|
592
|
+
const shadowGenerator = new ShadowGenerator(1024, light);
|
|
593
|
+
shadowGenerator.useBlurExponentialShadowMap = true;
|
|
594
|
+
shadowGenerator.blurKernel = 16;
|
|
595
|
+
shadowGenerator.darkness = 0.5;
|
|
596
|
+
this.#scene.meshes.forEach((mesh) => {
|
|
597
|
+
if (mesh.id.startsWith("__root__")) {
|
|
598
|
+
return false;
|
|
599
|
+
}
|
|
600
|
+
if (mesh.name !== "hdri") {
|
|
601
|
+
shadowGenerator.addShadowCaster(mesh, true);
|
|
602
|
+
}
|
|
603
|
+
});
|
|
604
|
+
this.#shadowGen.push(shadowGenerator);
|
|
403
605
|
});
|
|
404
606
|
}
|
|
405
607
|
|
|
@@ -412,20 +614,24 @@ export default class BabylonJSController {
|
|
|
412
614
|
* Otherwise, sets up shadow casting and receiving for all relevant meshes using the shadow generator.
|
|
413
615
|
*/
|
|
414
616
|
#initializeShadows() {
|
|
415
|
-
if (this.#
|
|
416
|
-
|
|
417
|
-
return true;
|
|
617
|
+
if (!this.#shadowsEnabled) {
|
|
618
|
+
return false;
|
|
418
619
|
}
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
620
|
+
if (this.#scene.environmentTexture) {
|
|
621
|
+
if (this.#options.ibl.shadows) {
|
|
622
|
+
if (this.#scene.environmentTexture.isReady()) {
|
|
623
|
+
this.#initializeIBLShadows();
|
|
624
|
+
} else {
|
|
625
|
+
const self = this;
|
|
626
|
+
this.#scene.environmentTexture.onLoadObservable.addOnce(() => {
|
|
627
|
+
self.#initializeIBLShadows();
|
|
628
|
+
});
|
|
629
|
+
}
|
|
427
630
|
}
|
|
428
|
-
}
|
|
631
|
+
} else {
|
|
632
|
+
this.#initializeDefaultLightShadows();
|
|
633
|
+
}
|
|
634
|
+
this.#initializeEnvironmentShadows();
|
|
429
635
|
}
|
|
430
636
|
|
|
431
637
|
/**
|
|
@@ -435,7 +641,7 @@ export default class BabylonJSController {
|
|
|
435
641
|
* @returns {void}
|
|
436
642
|
*/
|
|
437
643
|
#setMaxSimultaneousLights() {
|
|
438
|
-
let lightsNumber = 1; //
|
|
644
|
+
let lightsNumber = 1; // At least one light coming from the environment texture contribution
|
|
439
645
|
this.#scene.lights.forEach((light) => {
|
|
440
646
|
if (light.isEnabled()) {
|
|
441
647
|
++lightsNumber;
|
|
@@ -543,7 +749,7 @@ export default class BabylonJSController {
|
|
|
543
749
|
this.#engine.dispose();
|
|
544
750
|
this.#engine = this.#scene = this.#camera = null;
|
|
545
751
|
this.#hemiLight = this.#dirLight = this.#cameraLight = null;
|
|
546
|
-
this.#shadowGen =
|
|
752
|
+
this.#shadowGen = [];
|
|
547
753
|
}
|
|
548
754
|
|
|
549
755
|
/**
|
|
@@ -741,6 +947,21 @@ export default class BabylonJSController {
|
|
|
741
947
|
return true;
|
|
742
948
|
}
|
|
743
949
|
|
|
950
|
+
/**
|
|
951
|
+
* Applies pending image-based lighting (IBL) option updates.
|
|
952
|
+
* Marks the IBL state as successful, recreates lights so the new environment takes effect, and reports whether anything changed.
|
|
953
|
+
* @private
|
|
954
|
+
* @returns {boolean} True when lights were refreshed due to pending IBL changes, otherwise false.
|
|
955
|
+
*/
|
|
956
|
+
#setOptions_IBL() {
|
|
957
|
+
if (this.#options.ibl.isPending) {
|
|
958
|
+
this.#options.ibl.setSuccess(true);
|
|
959
|
+
this.#createLights();
|
|
960
|
+
return true;
|
|
961
|
+
}
|
|
962
|
+
return false;
|
|
963
|
+
}
|
|
964
|
+
|
|
744
965
|
/**
|
|
745
966
|
* Finds and returns the asset container object by its name.
|
|
746
967
|
* @private
|
|
@@ -951,6 +1172,7 @@ export default class BabylonJSController {
|
|
|
951
1172
|
*/
|
|
952
1173
|
async #loadContainers() {
|
|
953
1174
|
this.#stopRender();
|
|
1175
|
+
this.#scene.postProcessRenderPipelineManager?.dispose();
|
|
954
1176
|
|
|
955
1177
|
let oldModelMetadata = { ...(this.#containers.model?.state?.metadata ?? {}) };
|
|
956
1178
|
let newModelMetadata = {};
|
|
@@ -988,6 +1210,7 @@ export default class BabylonJSController {
|
|
|
988
1210
|
|
|
989
1211
|
this.#setOptions_Materials();
|
|
990
1212
|
this.#setOptions_Camera();
|
|
1213
|
+
this.#setOptions_IBL();
|
|
991
1214
|
this.#setVisibilityOfWallAndFloorInModel();
|
|
992
1215
|
detail.success = true;
|
|
993
1216
|
})
|
|
@@ -1000,13 +1223,25 @@ export default class BabylonJSController {
|
|
|
1000
1223
|
.finally(async () => {
|
|
1001
1224
|
this.#checkModelMetadata(oldModelMetadata, newModelMetadata);
|
|
1002
1225
|
this.#setMaxSimultaneousLights();
|
|
1003
|
-
this.#
|
|
1226
|
+
this.#loadCameraDepentEffects();
|
|
1004
1227
|
this.#babylonJSAnimationController = new BabylonJSAnimationController(this.#scene);
|
|
1005
1228
|
this.#startRender();
|
|
1006
1229
|
});
|
|
1007
1230
|
return detail;
|
|
1008
1231
|
}
|
|
1009
1232
|
|
|
1233
|
+
/**
|
|
1234
|
+
* Reinstalls every camera-sensitive post-process (default pipeline, SSAO, shadows) after loads or option changes.
|
|
1235
|
+
* Ensures the active camera always owns fresh pipelines so render quality remains consistent across reload cycles.
|
|
1236
|
+
* @private
|
|
1237
|
+
* @returns {void}
|
|
1238
|
+
*/
|
|
1239
|
+
#loadCameraDepentEffects() {
|
|
1240
|
+
this.#initializeVisualImprovements();
|
|
1241
|
+
this.#initializeAmbientOcclussion();
|
|
1242
|
+
this.#initializeShadows();
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1010
1245
|
/**
|
|
1011
1246
|
* Checks and applies model metadata changes after asset loading.
|
|
1012
1247
|
* @private
|
|
@@ -1235,6 +1470,17 @@ export default class BabylonJSController {
|
|
|
1235
1470
|
this.#engine = new Engine(this.#canvas, true, { alpha: true, stencil: true, preserveDrawingBuffer: false });
|
|
1236
1471
|
this.#engine.disableUniformBuffers = true;
|
|
1237
1472
|
this.#scene = new Scene(this.#engine);
|
|
1473
|
+
|
|
1474
|
+
// Activate the rendering of geometry data into a G-buffer, essential for advanced effects like deferred shading,
|
|
1475
|
+
// SSAO, and Velocity-Texture-Animation (VAT), allowing for complex post-processing by separating rendering into
|
|
1476
|
+
// different buffers (depth, normals, velocity) for later use in shaders.
|
|
1477
|
+
const geometryBufferRenderer = this.#scene.enableGeometryBufferRenderer();
|
|
1478
|
+
if (geometryBufferRenderer) {
|
|
1479
|
+
geometryBufferRenderer.enableScreenspaceDepth = true;
|
|
1480
|
+
geometryBufferRenderer.enableDepth = false;
|
|
1481
|
+
geometryBufferRenderer.generateNormalsInWorldSpace = true;
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1238
1484
|
this.#scene.clearColor = new Color4(1, 1, 1, 1);
|
|
1239
1485
|
this.#createCamera();
|
|
1240
1486
|
this.#createLights();
|
|
@@ -1278,6 +1524,7 @@ export default class BabylonJSController {
|
|
|
1278
1524
|
setCameraOptions() {
|
|
1279
1525
|
this.#stopRender();
|
|
1280
1526
|
const cameraOptionsSetted = this.#setOptions_Camera();
|
|
1527
|
+
this.#loadCameraDepentEffects();
|
|
1281
1528
|
this.#startRender();
|
|
1282
1529
|
return cameraOptionsSetted;
|
|
1283
1530
|
}
|
|
@@ -1295,6 +1542,20 @@ export default class BabylonJSController {
|
|
|
1295
1542
|
return materialsOptionsSetted;
|
|
1296
1543
|
}
|
|
1297
1544
|
|
|
1545
|
+
/**
|
|
1546
|
+
* Reapplies image-based lighting configuration (HDR URL, intensity, shadow mode).
|
|
1547
|
+
* Stops rendering, pushes pending IBL state into the scene, rebuilds camera-dependent effects, then resumes rendering.
|
|
1548
|
+
* @public
|
|
1549
|
+
* @returns {void}
|
|
1550
|
+
*/
|
|
1551
|
+
setIBLOptions() {
|
|
1552
|
+
this.#stopRender();
|
|
1553
|
+
const IBLOptionsSetted = this.#setOptions_IBL();
|
|
1554
|
+
this.#loadCameraDepentEffects();
|
|
1555
|
+
this.#startRender();
|
|
1556
|
+
return IBLOptionsSetted;
|
|
1557
|
+
}
|
|
1558
|
+
|
|
1298
1559
|
/**
|
|
1299
1560
|
* Sets the visibility of a container (model, environment, etc.) by name.
|
|
1300
1561
|
* Adds or removes the container from the scene and updates wall/floor visibility.
|
|
@@ -181,3 +181,53 @@ export class CameraData {
|
|
|
181
181
|
return this.update.success === true;
|
|
182
182
|
}
|
|
183
183
|
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* IBLData - Tracks configurable settings for image-based lighting assets (IBL/HDR environments).
|
|
187
|
+
*
|
|
188
|
+
* Responsibilities:
|
|
189
|
+
* - Stores the HDR url, intensity scalar, whether environment-provided shadows should render, and cache timestamp.
|
|
190
|
+
* - Exposes a lightweight pending/success state machine so UI flows know when a new IBL selection is being fetched.
|
|
191
|
+
* - Provides helpers to update values atomically via `setValues`, toggle pending state, and mark completion.
|
|
192
|
+
*
|
|
193
|
+
* Usage:
|
|
194
|
+
* - Instantiate with defaults: `const ibl = new IBLData();`
|
|
195
|
+
* - Call `setPending()` before kicking off an async download, `setValues()` as metadata streams in, and `setSuccess(true)` once loading finishes.
|
|
196
|
+
* - Inspect `isPending`/`isSuccess` to drive UI or re-render logic.
|
|
197
|
+
*/
|
|
198
|
+
export class IBLData {
|
|
199
|
+
constructor(url = null, intensity = 1.0, shadows = false, timeStamp = null) {
|
|
200
|
+
this.url = url;
|
|
201
|
+
this.intensity = intensity;
|
|
202
|
+
this.shadows = shadows;
|
|
203
|
+
this.timeStamp = timeStamp;
|
|
204
|
+
this.reset();
|
|
205
|
+
}
|
|
206
|
+
reset() {
|
|
207
|
+
this.pending = false;
|
|
208
|
+
this.success = false;
|
|
209
|
+
}
|
|
210
|
+
setValues(url, intensity, shadows, timeStamp) {
|
|
211
|
+
this.url = url !== undefined ? url : this.url;
|
|
212
|
+
this.intensity = intensity !== undefined ? intensity : this.intensity;
|
|
213
|
+
this.shadows = shadows !== undefined ? shadows : this.shadows;
|
|
214
|
+
this.timeStamp = timeStamp !== undefined ? timeStamp : this.timeStamp;
|
|
215
|
+
}
|
|
216
|
+
setSuccess(success = false) {
|
|
217
|
+
if (success) {
|
|
218
|
+
this.success = true;
|
|
219
|
+
} else {
|
|
220
|
+
this.success = false;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
setPending() {
|
|
224
|
+
this.pending = true;
|
|
225
|
+
this.success = false;
|
|
226
|
+
}
|
|
227
|
+
get isPending() {
|
|
228
|
+
return this.pending === true;
|
|
229
|
+
}
|
|
230
|
+
get isSuccess() {
|
|
231
|
+
return this.success === true;
|
|
232
|
+
}
|
|
233
|
+
}
|
package/src/pref-viewer-3d.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { CameraData, ContainerData, MaterialData } from "./pref-viewer-3d-data.js";
|
|
1
|
+
import { CameraData, ContainerData, MaterialData, IBLData } from "./pref-viewer-3d-data.js";
|
|
2
2
|
import BabylonJSController from "./babylonjs-controller.js";
|
|
3
3
|
import { PrefViewer3DStyles } from "./styles.js";
|
|
4
|
+
import { FileStorage } from "./file-storage.js";
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* PrefViewer3D - Custom Web Component for interactive 3D visualization and configuration.
|
|
@@ -8,7 +9,7 @@ import { PrefViewer3DStyles } from "./styles.js";
|
|
|
8
9
|
* Overview:
|
|
9
10
|
* - Encapsulates a Babylon.js-powered 3D viewer for displaying models, environments, and materials.
|
|
10
11
|
* - Manages internal state for containers (model, environment, materials) and options (camera, materials).
|
|
11
|
-
* - Handles asset loading, configuration, and option updates through attributes and public methods.
|
|
12
|
+
* - Handles asset loading, configuration, and option updates (camera, materials, IBL) through attributes and public methods.
|
|
12
13
|
* - Provides API for showing/hiding the viewer, model, and environment, and for downloading assets.
|
|
13
14
|
* - Emits custom events for loading, loaded, and option-setting states.
|
|
14
15
|
*
|
|
@@ -26,7 +27,7 @@ import { PrefViewer3DStyles } from "./styles.js";
|
|
|
26
27
|
* - show(): Shows the 3D viewer component.
|
|
27
28
|
* - hide(): Hides the 3D viewer component.
|
|
28
29
|
* - load(config): Loads the provided configuration into the viewer.
|
|
29
|
-
* - setOptions(options): Sets viewer options such as camera and
|
|
30
|
+
* - setOptions(options): Sets viewer options such as camera, materials, and image-based lighting (IBL).
|
|
30
31
|
* - showModel(): Shows the 3D model.
|
|
31
32
|
* - hideModel(): Hides the 3D model.
|
|
32
33
|
* - showEnvironment(): Shows the 3D environment/scene.
|
|
@@ -46,6 +47,14 @@ import { PrefViewer3DStyles } from "./styles.js";
|
|
|
46
47
|
* - isLoaded: Indicates whether the GLTF/GLB content is loaded and ready.
|
|
47
48
|
* - isVisible: Indicates whether the component is currently visible.
|
|
48
49
|
*
|
|
50
|
+
* Internal Helpers:
|
|
51
|
+
* - #checkNeedToUpdateContainers(config): Flags model/environment/material containers that must reload.
|
|
52
|
+
* - #checkNeedToUpdateCamera(options): Detects camera option changes and marks them pending.
|
|
53
|
+
* - #checkNeedToUpdateMaterials(options): Resolves material overrides that require updates.
|
|
54
|
+
* - #checkNeedToUpdateIBL(options): Fetches HDR URLs/timestamps and enqueues IBL updates when needed.
|
|
55
|
+
* - #resetUpdateFlags(): Clears pending/success flags after loads or option-setting flows.
|
|
56
|
+
* - #onLoading/#onLoaded/#onSettingOptions/#onSetOptions(): Dispatch lifecycle events and synchronize attributes.
|
|
57
|
+
*
|
|
49
58
|
* Events:
|
|
50
59
|
* - "scene-loading": Dispatched when a loading operation starts.
|
|
51
60
|
* - "scene-loaded": Dispatched when a loading operation completes.
|
|
@@ -202,6 +211,7 @@ export default class PrefViewer3D extends HTMLElement {
|
|
|
202
211
|
innerFloor: new MaterialData("innerFloor", undefined, undefined, ["innerFloor"]),
|
|
203
212
|
outerFloor: new MaterialData("outerFloor", undefined, undefined, ["outerFloor"]),
|
|
204
213
|
},
|
|
214
|
+
ibl: new IBLData(),
|
|
205
215
|
},
|
|
206
216
|
};
|
|
207
217
|
}
|
|
@@ -225,6 +235,7 @@ export default class PrefViewer3D extends HTMLElement {
|
|
|
225
235
|
Object.values(this.#data.containers).forEach((container) => container.reset());
|
|
226
236
|
Object.values(this.#data.options.materials).forEach((material) => material.reset());
|
|
227
237
|
this.#data.options.camera.reset();
|
|
238
|
+
this.#data.options.ibl.reset();
|
|
228
239
|
}
|
|
229
240
|
|
|
230
241
|
/**
|
|
@@ -306,6 +317,51 @@ export default class PrefViewer3D extends HTMLElement {
|
|
|
306
317
|
return someNeedUpdate;
|
|
307
318
|
}
|
|
308
319
|
|
|
320
|
+
/**
|
|
321
|
+
* Resolves incoming IBL settings (HDR URL, timestamp, intensity, shadows) and marks the option as pending when changed.
|
|
322
|
+
* Fetches signed URLs/time stamps when storage keys are provided so the Babylon controller can reload the environment map.
|
|
323
|
+
* @private
|
|
324
|
+
* @param {object} options - Options payload that may contain an `ibl` block with url, intensity, or shadow flags.
|
|
325
|
+
* @returns {Promise<boolean>} Resolves to true when any IBL property differs from the cached state, otherwise false.
|
|
326
|
+
*/
|
|
327
|
+
async #checkNeedToUpdateIBL(options) {
|
|
328
|
+
if (!options || options.ibl === undefined) {
|
|
329
|
+
return false;
|
|
330
|
+
}
|
|
331
|
+
const iblState = this.#data.options.ibl;
|
|
332
|
+
|
|
333
|
+
let url = undefined;
|
|
334
|
+
let timeStamp = undefined;
|
|
335
|
+
let shadows = undefined;
|
|
336
|
+
let intensity = undefined;
|
|
337
|
+
|
|
338
|
+
if (options.ibl.url) {
|
|
339
|
+
const fileStorage = new FileStorage("PrefViewer", "Files");
|
|
340
|
+
const newURL = await fileStorage.getURL(options.ibl.url);
|
|
341
|
+
if (newURL) {
|
|
342
|
+
url = newURL;
|
|
343
|
+
timeStamp = await fileStorage.getTimeStamp(options.ibl.url);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
if (options.ibl.shadows !== undefined) {
|
|
347
|
+
shadows = options.ibl.shadows;
|
|
348
|
+
}
|
|
349
|
+
if (options.ibl.intensity !== undefined) {
|
|
350
|
+
intensity = options.ibl.intensity;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
const needUpdate = url !== undefined && url !== iblState.url ||
|
|
354
|
+
timeStamp !== undefined && timeStamp !== iblState.timeStamp ||
|
|
355
|
+
shadows !== undefined && shadows !== iblState.shadows ||
|
|
356
|
+
intensity !== undefined && intensity !== iblState.intensity;
|
|
357
|
+
if (needUpdate) {
|
|
358
|
+
iblState.setValues(url, intensity, shadows, timeStamp);
|
|
359
|
+
iblState.setPending(true);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
return needUpdate;
|
|
363
|
+
}
|
|
364
|
+
|
|
309
365
|
/**
|
|
310
366
|
* Dispatches a "prefviewer3d-loading" event and updates loading state attributes.
|
|
311
367
|
* Used internally when a loading operation starts.
|
|
@@ -490,6 +546,7 @@ export default class PrefViewer3D extends HTMLElement {
|
|
|
490
546
|
if (config.options) {
|
|
491
547
|
this.#checkNeedToUpdateCamera(config.options);
|
|
492
548
|
this.#checkNeedToUpdateMaterials(config.options);
|
|
549
|
+
this.#checkNeedToUpdateIBL(config.options);
|
|
493
550
|
}
|
|
494
551
|
|
|
495
552
|
const loadDetail = await this.#babylonJSController.load();
|
|
@@ -518,6 +575,9 @@ export default class PrefViewer3D extends HTMLElement {
|
|
|
518
575
|
if (this.#checkNeedToUpdateMaterials(options)) {
|
|
519
576
|
someSetted = someSetted || this.#babylonJSController.setMaterialOptions();
|
|
520
577
|
}
|
|
578
|
+
if (this.#checkNeedToUpdateIBL(options)) {
|
|
579
|
+
someSetted = someSetted || this.#babylonJSController.setIBLOptions();
|
|
580
|
+
}
|
|
521
581
|
const detail = this.#onSetOptions();
|
|
522
582
|
return { success: someSetted, detail: detail };
|
|
523
583
|
}
|