@preference-sl/pref-viewer 2.12.0-beta.3 → 2.12.0-beta.5
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
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Color3, HighlightLayer, Mesh, PickingInfo, Scene } from "@babylonjs/core";
|
|
1
|
+
import { AssetContainer, Color3, HighlightLayer, InstancedMesh, Mesh, PickingInfo, Quaternion, Scene, UtilityLayerRenderer, Vector3 } from "@babylonjs/core";
|
|
2
2
|
import { PrefViewerColors } from "./styles.js";
|
|
3
3
|
import OpeningAnimation from "./babylonjs-animation-opening.js";
|
|
4
4
|
|
|
@@ -18,27 +18,36 @@ import OpeningAnimation from "./babylonjs-animation-opening.js";
|
|
|
18
18
|
*
|
|
19
19
|
* Public Methods:
|
|
20
20
|
* - dispose(): Disposes all resources managed by the animation controller.
|
|
21
|
-
* -
|
|
21
|
+
* - highlightMeshes(pickingInfo): Highlights meshes that are children of an animated node when hovered.
|
|
22
22
|
* - hideMenu(): Hides and disposes the animation control menu if it exists.
|
|
23
23
|
* - showMenu(pickingInfo): Displays the animation control menu for the animated node under the pointer.
|
|
24
24
|
*
|
|
25
25
|
* @class
|
|
26
26
|
*/
|
|
27
27
|
export default class BabylonJSAnimationController {
|
|
28
|
-
#scene = null;
|
|
29
28
|
#canvas = null;
|
|
29
|
+
#scene = null;
|
|
30
|
+
#assetContainer = null;
|
|
31
|
+
|
|
30
32
|
#animatedNodes = [];
|
|
31
|
-
#highlightLayer = null;
|
|
32
|
-
#highlightColor = Color3.FromHexString(PrefViewerColors.primary);
|
|
33
33
|
#openingAnimations = [];
|
|
34
34
|
|
|
35
|
+
#highlightColor = Color3.FromHexString(PrefViewerColors.primary);
|
|
36
|
+
#highlightLayer = null;
|
|
37
|
+
#overlayLayer = null;
|
|
38
|
+
#useHighlightLayer = false; // Set to true to use HighlightLayer (better performance) and false to use overlay meshes (UtilityLayerRenderer - always on top)
|
|
39
|
+
#lastHighlightedMeshId = null; // Cache to avoid redundant highlight updates
|
|
40
|
+
|
|
35
41
|
/**
|
|
36
42
|
* Creates a new BabylonJSAnimationController for a Babylon.js scene.
|
|
37
|
-
* @param {Scene}
|
|
43
|
+
* @param {AssetContainer|Scene} assetContainer - The Babylon.js asset container or scene instance.
|
|
38
44
|
*/
|
|
39
|
-
constructor(
|
|
40
|
-
|
|
41
|
-
|
|
45
|
+
constructor(assetContainer) {
|
|
46
|
+
if (assetContainer instanceof AssetContainer || assetContainer instanceof Scene) {
|
|
47
|
+
this.#scene = assetContainer.scene ? assetContainer.scene : assetContainer;
|
|
48
|
+
this.#assetContainer = assetContainer;
|
|
49
|
+
this.#canvas = this.#scene._engine._renderingCanvas;
|
|
50
|
+
}
|
|
42
51
|
this.#initializeAnimations();
|
|
43
52
|
}
|
|
44
53
|
|
|
@@ -48,7 +57,7 @@ export default class BabylonJSAnimationController {
|
|
|
48
57
|
*/
|
|
49
58
|
#initializeAnimations() {
|
|
50
59
|
this.hideMenu(); // Clean up any existing menus
|
|
51
|
-
if (!this.#
|
|
60
|
+
if (!this.#assetContainer?.animationGroups?.length) {
|
|
52
61
|
return;
|
|
53
62
|
}
|
|
54
63
|
this.#getAnimatedNodes();
|
|
@@ -60,7 +69,7 @@ export default class BabylonJSAnimationController {
|
|
|
60
69
|
* @private
|
|
61
70
|
*/
|
|
62
71
|
#getAnimatedNodes() {
|
|
63
|
-
this.#
|
|
72
|
+
this.#assetContainer.animationGroups.forEach((animationGroup) => {
|
|
64
73
|
animationGroup.stop();
|
|
65
74
|
if (!animationGroup._targetedAnimations.length) {
|
|
66
75
|
return;
|
|
@@ -81,7 +90,7 @@ export default class BabylonJSAnimationController {
|
|
|
81
90
|
*/
|
|
82
91
|
#getOpeningAnimations() {
|
|
83
92
|
const openings = {};
|
|
84
|
-
this.#
|
|
93
|
+
this.#assetContainer.animationGroups.forEach((animationGroup) => {
|
|
85
94
|
const match = animationGroup.name.match(/^animation_(open|close)_(.+)$/);
|
|
86
95
|
if (!match) {
|
|
87
96
|
return;
|
|
@@ -118,7 +127,7 @@ export default class BabylonJSAnimationController {
|
|
|
118
127
|
* @param {Mesh} mesh - The mesh to check.
|
|
119
128
|
* @returns {Array<string>} Array of animated node IDs associated with the mesh.
|
|
120
129
|
*/
|
|
121
|
-
#getNodesAnimatedByMesh
|
|
130
|
+
#getNodesAnimatedByMesh(mesh) {
|
|
122
131
|
let nodeId = [];
|
|
123
132
|
let node = mesh;
|
|
124
133
|
while (node.parent !== null) {
|
|
@@ -128,7 +137,176 @@ export default class BabylonJSAnimationController {
|
|
|
128
137
|
}
|
|
129
138
|
}
|
|
130
139
|
return nodeId;
|
|
131
|
-
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Creates overlay meshes with highlight rendering for a group of meshes.
|
|
144
|
+
* Handles both regular meshes and instanced meshes by creating clones in the utility layer.
|
|
145
|
+
* Synchronizes world matrix transformations between original and overlay meshes only when transforms change.
|
|
146
|
+
* @private
|
|
147
|
+
* @param {Mesh[]} meshes - Array of meshes to highlight with overlay outlines.
|
|
148
|
+
* @returns {Object} Cleanup API object with a dispose() method to release all overlay resources.
|
|
149
|
+
* @description
|
|
150
|
+
* - Uses UtilityLayerRenderer for rendering
|
|
151
|
+
* - Only syncs transforms when original mesh's world matrix changes (performance optimized)
|
|
152
|
+
* - Handles InstancedMeshes by creating instances of a shared overlay base
|
|
153
|
+
* - Removes overlay when mouse moves away from highlighted meshes
|
|
154
|
+
*/
|
|
155
|
+
#addGroupOutlineOverlayForInstances(meshes) {
|
|
156
|
+
// Configuration
|
|
157
|
+
const OUTLINE_ENABLED = false; // Draw outline on individual meshes
|
|
158
|
+
const OUTLINE_COLOR = this.#highlightColor;
|
|
159
|
+
const OUTLINE_WIDTH = 1.5;
|
|
160
|
+
const OVERLAY_ENABLED = true; // Draw semi-transparent fill
|
|
161
|
+
const OVERLAY_COLOR = this.#highlightColor;
|
|
162
|
+
const OVERLAY_ALPHA = 0.25;
|
|
163
|
+
const RENDERING_GROUP_ID = 0; // MUST be > 0 to render on top of main scene
|
|
164
|
+
|
|
165
|
+
// Use UtilityLayerRenderer for rendering
|
|
166
|
+
const uScene = UtilityLayerRenderer.DefaultKeepDepthUtilityLayer.utilityLayerScene;
|
|
167
|
+
|
|
168
|
+
// Cache map: baseMesh -> overlayBaseMesh to reuse for InstancedMeshes
|
|
169
|
+
const baseToOverlayBase = new Map();
|
|
170
|
+
|
|
171
|
+
// Pairs of { original mesh, overlay mesh } for tracking
|
|
172
|
+
const pairs = [];
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Gets the base mesh from either a regular mesh or an InstancedMesh
|
|
176
|
+
* @param {Mesh} mesh - The mesh to check
|
|
177
|
+
* @returns {Mesh} The base mesh or the mesh itself
|
|
178
|
+
*/
|
|
179
|
+
function getBaseMesh(mesh) {
|
|
180
|
+
return mesh instanceof InstancedMesh ? mesh.sourceMesh : mesh;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Creates or retrieves an overlay base mesh in the utility scene
|
|
185
|
+
* Reuses the same base for all instances of the same source mesh
|
|
186
|
+
* @param {Mesh} baseMesh - The base mesh to clone
|
|
187
|
+
* @returns {Mesh} The overlay base mesh
|
|
188
|
+
*/
|
|
189
|
+
function ensureOverlayBase(baseMesh) {
|
|
190
|
+
let overlayBase = baseToOverlayBase.get(baseMesh);
|
|
191
|
+
if (overlayBase) {
|
|
192
|
+
return overlayBase;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Clone the source mesh into the utility scene (deep clone materials/geometry)
|
|
196
|
+
overlayBase = baseMesh.clone(baseMesh.name + "_overlayBase", null, true, false);
|
|
197
|
+
overlayBase._scene = uScene;
|
|
198
|
+
overlayBase.isPickable = false;
|
|
199
|
+
overlayBase.setEnabled(true);
|
|
200
|
+
|
|
201
|
+
// Configure visual appearance for the overlay
|
|
202
|
+
overlayBase.renderOutline = OUTLINE_ENABLED; // Only draw outline if enabled
|
|
203
|
+
overlayBase.outlineColor = OUTLINE_COLOR;
|
|
204
|
+
overlayBase.outlineWidth = OUTLINE_WIDTH;
|
|
205
|
+
|
|
206
|
+
// Overlay color and transparency (semi-transparent colored fill)
|
|
207
|
+
overlayBase.renderOverlay = OVERLAY_ENABLED;
|
|
208
|
+
overlayBase.overlayColor = OVERLAY_COLOR;
|
|
209
|
+
overlayBase.overlayAlpha = OVERLAY_ALPHA;
|
|
210
|
+
|
|
211
|
+
overlayBase.visibility = 1;
|
|
212
|
+
|
|
213
|
+
// Ensure always-on-top rendering or not
|
|
214
|
+
overlayBase.renderingGroupId = RENDERING_GROUP_ID;
|
|
215
|
+
|
|
216
|
+
baseToOverlayBase.set(baseMesh, overlayBase);
|
|
217
|
+
return overlayBase;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Create overlay meshes for each input mesh
|
|
221
|
+
meshes.forEach((mesh) => {
|
|
222
|
+
// Skip meshes without geometry
|
|
223
|
+
if (!mesh.geometry && !mesh.getChildMeshes) {
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const baseMesh = getBaseMesh(mesh);
|
|
228
|
+
const overlayBase = ensureOverlayBase(baseMesh);
|
|
229
|
+
|
|
230
|
+
let overlay;
|
|
231
|
+
if (mesh instanceof InstancedMesh) {
|
|
232
|
+
// For instances, create a new instance of the overlay base
|
|
233
|
+
overlay = overlayBase.createInstance(mesh.name + "_overlayInst");
|
|
234
|
+
overlay._scene = uScene;
|
|
235
|
+
overlay.isPickable = false;
|
|
236
|
+
} else {
|
|
237
|
+
// For regular meshes, use the overlay base directly (avoids double cloning)
|
|
238
|
+
overlay = overlayBase;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Remove parent to allow independent world matrix control
|
|
242
|
+
overlay.parent = null;
|
|
243
|
+
overlay.renderingGroupId = RENDERING_GROUP_ID;
|
|
244
|
+
|
|
245
|
+
pairs.push({ original: mesh, overlay });
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
// Sync world matrix transformations
|
|
249
|
+
const pos = new Vector3();
|
|
250
|
+
const scl = new Vector3();
|
|
251
|
+
const rot = new Quaternion();
|
|
252
|
+
|
|
253
|
+
// Track all observers for cleanup
|
|
254
|
+
const observers = [];
|
|
255
|
+
|
|
256
|
+
// Subscribe to world matrix update events
|
|
257
|
+
// This is more efficient than updating every frame
|
|
258
|
+
pairs.forEach(({ original, overlay }) => {
|
|
259
|
+
// Skip if mesh has no geometry to sync
|
|
260
|
+
if (!original.getWorldMatrix) {
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Initial sync: copy the current world matrix
|
|
265
|
+
original.computeWorldMatrix(true);
|
|
266
|
+
original.getWorldMatrix().decompose(scl, rot, pos);
|
|
267
|
+
|
|
268
|
+
overlay.position.copyFrom(pos);
|
|
269
|
+
overlay.scaling.copyFrom(scl);
|
|
270
|
+
overlay.rotationQuaternion = overlay.rotationQuaternion || new Quaternion();
|
|
271
|
+
overlay.rotationQuaternion.copyFrom(rot);
|
|
272
|
+
overlay.computeWorldMatrix(true);
|
|
273
|
+
|
|
274
|
+
// Subscribe to world matrix changes
|
|
275
|
+
// Only updates when the original mesh's transformation actually changes
|
|
276
|
+
const obs = original.onAfterWorldMatrixUpdateObservable?.add(() => {
|
|
277
|
+
original.getWorldMatrix().decompose(scl, rot, pos);
|
|
278
|
+
|
|
279
|
+
overlay.position.copyFrom(pos);
|
|
280
|
+
overlay.scaling.copyFrom(scl);
|
|
281
|
+
overlay.rotationQuaternion.copyFrom(rot);
|
|
282
|
+
overlay.computeWorldMatrix(true);
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
if (obs) {
|
|
286
|
+
observers.push({ mesh: original, observer: obs });
|
|
287
|
+
}
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
// Cleanup API
|
|
291
|
+
return {
|
|
292
|
+
dispose() {
|
|
293
|
+
// Remove world matrix update observers
|
|
294
|
+
observers.forEach(({ mesh, observer }) => {
|
|
295
|
+
mesh.onAfterWorldMatrixUpdateObservable?.remove(observer);
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
// Dispose overlay instances and meshes
|
|
299
|
+
pairs.forEach(({ original, overlay }) => {
|
|
300
|
+
overlay.dispose();
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
// Dispose overlay base meshes
|
|
304
|
+
baseToOverlayBase.forEach((overlayBase) => {
|
|
305
|
+
overlayBase.dispose();
|
|
306
|
+
});
|
|
307
|
+
},
|
|
308
|
+
};
|
|
309
|
+
}
|
|
132
310
|
|
|
133
311
|
/**
|
|
134
312
|
* ---------------------------
|
|
@@ -148,6 +326,10 @@ export default class BabylonJSAnimationController {
|
|
|
148
326
|
this.#highlightLayer.dispose();
|
|
149
327
|
this.#highlightLayer = null;
|
|
150
328
|
}
|
|
329
|
+
if (this.#overlayLayer) {
|
|
330
|
+
this.#overlayLayer.dispose();
|
|
331
|
+
this.#overlayLayer = null;
|
|
332
|
+
}
|
|
151
333
|
this.hideMenu();
|
|
152
334
|
this.#animatedNodes = [];
|
|
153
335
|
this.#openingAnimations.forEach((openingAnimation) => openingAnimation.dispose());
|
|
@@ -159,39 +341,74 @@ export default class BabylonJSAnimationController {
|
|
|
159
341
|
* @public
|
|
160
342
|
* @param {PickingInfo} pickingInfo - Raycast info from pointer position.
|
|
161
343
|
*/
|
|
162
|
-
|
|
163
|
-
if
|
|
164
|
-
|
|
344
|
+
highlightMeshes(pickingInfo) {
|
|
345
|
+
// Check if we're hovering the same mesh to avoid recreating overlays/highlights
|
|
346
|
+
const pickedMeshId = pickingInfo?.pickedMesh?.id;
|
|
347
|
+
if (this.#lastHighlightedMeshId === pickedMeshId) {
|
|
348
|
+
return; // No need to update if hovering the same mesh
|
|
165
349
|
}
|
|
350
|
+
this.#lastHighlightedMeshId = pickedMeshId;
|
|
166
351
|
|
|
167
|
-
this.#
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
352
|
+
if (this.#useHighlightLayer) {
|
|
353
|
+
if (!this.#highlightLayer) {
|
|
354
|
+
this.#highlightLayer = new HighlightLayer("hl_animations", this.#scene);
|
|
355
|
+
}
|
|
171
356
|
|
|
172
|
-
|
|
173
|
-
if (!nodeIds.length) {
|
|
174
|
-
return;
|
|
175
|
-
}
|
|
357
|
+
this.#highlightLayer.removeAllMeshes();
|
|
176
358
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
const transformNode = this.#scene.getTransformNodeByID(nodeId);
|
|
180
|
-
if (transformNode) {
|
|
181
|
-
transformNodes.push(transformNode);
|
|
359
|
+
if (!pickingInfo?.hit || !pickingInfo?.pickedMesh) {
|
|
360
|
+
return;
|
|
182
361
|
}
|
|
183
|
-
});
|
|
184
362
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
nodeMeshes.forEach((mesh) => {
|
|
189
|
-
if (!this.#highlightLayer.hasMesh(mesh)) {
|
|
190
|
-
this.#highlightLayer.addMesh(mesh, this.#highlightColor);
|
|
191
|
-
}
|
|
192
|
-
});
|
|
363
|
+
const nodeIds = this.#getNodesAnimatedByMesh(pickingInfo.pickedMesh);
|
|
364
|
+
if (!nodeIds.length) {
|
|
365
|
+
return;
|
|
193
366
|
}
|
|
194
|
-
|
|
367
|
+
|
|
368
|
+
const transformNodes = [];
|
|
369
|
+
nodeIds.forEach((nodeId) => {
|
|
370
|
+
const transformNode = this.#scene.getTransformNodeByID(nodeId);
|
|
371
|
+
if (transformNode) {
|
|
372
|
+
transformNodes.push(transformNode);
|
|
373
|
+
}
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
transformNodes.forEach((transformNode) => {
|
|
377
|
+
const transformNodeMeshes = transformNode.getChildMeshes();
|
|
378
|
+
if (transformNodeMeshes.length) {
|
|
379
|
+
transformNodeMeshes.forEach((mesh) => {
|
|
380
|
+
if (!this.#highlightLayer.hasMesh(mesh)) {
|
|
381
|
+
this.#highlightLayer.addMesh(mesh, this.#highlightColor);
|
|
382
|
+
}
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
});
|
|
386
|
+
} else {
|
|
387
|
+
if (this.#overlayLayer) {
|
|
388
|
+
this.#overlayLayer.dispose();
|
|
389
|
+
this.#overlayLayer = null;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
if (!pickingInfo?.hit || !pickingInfo?.pickedMesh) {
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
const nodeIds = this.#getNodesAnimatedByMesh(pickingInfo.pickedMesh);
|
|
397
|
+
if (!nodeIds.length) {
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
const meshes = [];
|
|
402
|
+
nodeIds.forEach((nodeId) => {
|
|
403
|
+
const transformNode = this.#scene.getTransformNodeByID(nodeId);
|
|
404
|
+
if (transformNode) {
|
|
405
|
+
const transformNodeMeshes = transformNode.getChildMeshes(false);
|
|
406
|
+
meshes.push(...transformNodeMeshes);
|
|
407
|
+
}
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
this.#overlayLayer = this.#addGroupOutlineOverlayForInstances(meshes);
|
|
411
|
+
}
|
|
195
412
|
}
|
|
196
413
|
|
|
197
414
|
/**
|
|
@@ -201,7 +418,7 @@ export default class BabylonJSAnimationController {
|
|
|
201
418
|
*/
|
|
202
419
|
hideMenu() {
|
|
203
420
|
this.#openingAnimations.forEach((openingAnimation) => openingAnimation.hideControls());
|
|
204
|
-
this.#canvas
|
|
421
|
+
this.#canvas?.parentElement?.querySelectorAll("div.pref-viewer-3d.animation-menu").forEach((menu) => menu.remove());
|
|
205
422
|
}
|
|
206
423
|
|
|
207
424
|
/**
|
|
@@ -95,6 +95,8 @@ export default class OpeningAnimation {
|
|
|
95
95
|
this.#bindHandlers();
|
|
96
96
|
this.#openAnimation.onAnimationGroupEndObservable.add(this.#handlers.onOpened);
|
|
97
97
|
this.#closeAnimation.onAnimationGroupEndObservable.add(this.#handlers.onClosed);
|
|
98
|
+
|
|
99
|
+
this.#goToClosed();
|
|
98
100
|
}
|
|
99
101
|
|
|
100
102
|
#bindHandlers() {
|
|
@@ -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, RenderTargetTexture, 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,21 +115,28 @@ 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
|
+
|
|
114
122
|
#containers = {};
|
|
115
123
|
#options = {};
|
|
116
|
-
|
|
124
|
+
|
|
117
125
|
#gltfResolver = null; // GLTFResolver instance
|
|
118
126
|
#babylonJSAnimationController = null; // AnimationController instance
|
|
119
|
-
|
|
127
|
+
|
|
120
128
|
#handlers = {
|
|
121
129
|
onKeyUp: null,
|
|
122
130
|
onPointerObservable: null,
|
|
123
131
|
renderLoop: null,
|
|
124
132
|
};
|
|
133
|
+
|
|
134
|
+
#settings = {
|
|
135
|
+
antiAliasingEnabled: true,
|
|
136
|
+
ambientOcclusionEnabled: true,
|
|
137
|
+
iblEnabled: true,
|
|
138
|
+
shadowsEnabled: false,
|
|
139
|
+
};
|
|
125
140
|
|
|
126
141
|
/**
|
|
127
142
|
* Constructs a new BabylonJSController instance.
|
|
@@ -288,33 +303,159 @@ export default class BabylonJSController {
|
|
|
288
303
|
* @returns {void}
|
|
289
304
|
*/
|
|
290
305
|
#createLights() {
|
|
291
|
-
this.#
|
|
306
|
+
if (this.#settings.iblEnabled && this.#options.ibl && this.#options.ibl.url) {
|
|
307
|
+
this.#hemiLight = this.#dirLight = this.#cameraLight = null;
|
|
308
|
+
this.#initializeEnvironmentTexture();
|
|
309
|
+
}
|
|
292
310
|
|
|
293
311
|
if (this.#scene.environmentTexture) {
|
|
294
312
|
return true;
|
|
295
313
|
}
|
|
296
314
|
|
|
297
315
|
// 1) Stronger ambient fill
|
|
298
|
-
this.#hemiLight = new HemisphericLight("
|
|
316
|
+
this.#hemiLight = new HemisphericLight("PrefViewerHemiLight", new Vector3(-10, 10, -10), this.#scene);
|
|
299
317
|
this.#hemiLight.intensity = 0.6;
|
|
300
318
|
|
|
301
319
|
// 2) Directional light from the front-right, angled slightly down
|
|
302
|
-
this.#dirLight = new DirectionalLight("
|
|
320
|
+
this.#dirLight = new DirectionalLight("PrefViewerDirLight", new Vector3(-10, 10, -10), this.#scene);
|
|
303
321
|
this.#dirLight.position = new Vector3(5, 4, 5); // light is IN FRONT + ABOVE + to the RIGHT
|
|
304
322
|
this.#dirLight.intensity = 0.6;
|
|
305
323
|
|
|
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);
|
|
324
|
+
// 3) Camera‐attached headlight
|
|
325
|
+
this.#cameraLight = new PointLight("PrefViewerCameraLight", this.#camera.position, this.#scene);
|
|
314
326
|
this.#cameraLight.parent = this.#camera;
|
|
315
327
|
this.#cameraLight.intensity = 0.3;
|
|
316
328
|
}
|
|
317
329
|
|
|
330
|
+
/**
|
|
331
|
+
* Rebuilds the SSAO post-process pipeline to inject screenspace ambient occlusion on the active camera.
|
|
332
|
+
* Disposes previous SSAO pipelines, instantiates a tuned `SSAORenderingPipeline`, and attaches it to the
|
|
333
|
+
* current camera so contact shadows enhance depth perception once assets reload or the camera changes.
|
|
334
|
+
* @private
|
|
335
|
+
* @returns {boolean} True if the SSAO pipeline is supported and enabled, otherwise false.
|
|
336
|
+
*/
|
|
337
|
+
#initializeAmbientOcclussion() {
|
|
338
|
+
if (!this.#scene || !this.#scene.postProcessRenderPipelineManager || this.#scene.activeCamera === null) {
|
|
339
|
+
return false;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
if (!this.#settings.ambientOcclusionEnabled) {
|
|
343
|
+
return false;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const pipelineName = "PrefViewerSSAORenderingPipeline";
|
|
347
|
+
|
|
348
|
+
const supportedPipelines = this.#scene.postProcessRenderPipelineManager.supportedPipelines;
|
|
349
|
+
|
|
350
|
+
if (!supportedPipelines) {
|
|
351
|
+
return false;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const oldSsaoPipeline = supportedPipelines ? supportedPipelines.find((pipeline) => pipeline.name === pipelineName) : false;
|
|
355
|
+
|
|
356
|
+
if (oldSsaoPipeline) {
|
|
357
|
+
oldSsaoPipeline.dispose();
|
|
358
|
+
this.#scene.postProcessRenderPipelineManager.update();
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
const ssaoRatio = {
|
|
362
|
+
ssaoRatio: 0.5,
|
|
363
|
+
combineRatio: 1.0
|
|
364
|
+
};
|
|
365
|
+
|
|
366
|
+
const ssaoPipeline = new SSAORenderingPipeline(pipelineName, this.#scene, ssaoRatio, [this.#scene.activeCamera]);
|
|
367
|
+
|
|
368
|
+
if (!ssaoPipeline){
|
|
369
|
+
return false;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
if (ssaoPipeline.isSupported) {
|
|
373
|
+
ssaoPipeline.fallOff = 0.000001;
|
|
374
|
+
ssaoPipeline.area = 1;
|
|
375
|
+
ssaoPipeline.radius = 0.0001;
|
|
376
|
+
ssaoPipeline.totalStrength = 1;
|
|
377
|
+
ssaoPipeline.base = 0.6;
|
|
378
|
+
this.#scene.postProcessRenderPipelineManager.update();
|
|
379
|
+
return true;
|
|
380
|
+
} else {
|
|
381
|
+
ssaoPipeline.dispose();
|
|
382
|
+
this.#scene.postProcessRenderPipelineManager.update();
|
|
383
|
+
return false;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Rebuilds the custom default rendering pipeline (MSAA, FXAA, film grain) for the active camera.
|
|
389
|
+
* Disposes any previous pipeline instance to avoid duplicates, then attaches a fresh
|
|
390
|
+
* `DefaultRenderingPipeline` with tuned settings for sharper anti-aliasing and subtle grain.
|
|
391
|
+
* @private
|
|
392
|
+
* @returns {boolean} True when the pipeline is supported and active, otherwise false.
|
|
393
|
+
* @see {@link https://doc.babylonjs.com/features/featuresDeepDive/postProcesses/defaultRenderingPipeline|Using the Default Rendering Pipeline | Babylon.js Documentation}
|
|
394
|
+
*/
|
|
395
|
+
#initializeVisualImprovements() {
|
|
396
|
+
if (!this.#scene || !this.#scene.postProcessRenderPipelineManager || this.#scene.activeCamera === null) {
|
|
397
|
+
return false;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
const pipelineName = "PrefViewerDefaultRenderingPipeline";
|
|
401
|
+
const supportedPipelines = this.#scene.postProcessRenderPipelineManager.supportedPipelines;
|
|
402
|
+
|
|
403
|
+
if (!supportedPipelines) {
|
|
404
|
+
return false;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
const oldDefaultPipeline = supportedPipelines ? supportedPipelines.find((pipeline) => pipeline.name === pipelineName) : false;
|
|
408
|
+
|
|
409
|
+
if (oldDefaultPipeline) {
|
|
410
|
+
oldDefaultPipeline.dispose();
|
|
411
|
+
this.#scene.postProcessRenderPipelineManager.update();
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
if (!this.#settings.antiAliasingEnabled) {
|
|
415
|
+
return false;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
const defaultPipeline = new DefaultRenderingPipeline(pipelineName, true, this.#scene, [this.#scene.activeCamera], true);
|
|
419
|
+
|
|
420
|
+
if (!defaultPipeline){
|
|
421
|
+
return false;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
if (defaultPipeline.isSupported) {
|
|
425
|
+
// MSAA - Multisample Anti-Aliasing
|
|
426
|
+
const caps = this.#scene.getEngine()?.getCaps?.() || {};
|
|
427
|
+
const maxSamples = typeof caps.maxMSAASamples === "number" ? caps.maxMSAASamples : 4;
|
|
428
|
+
defaultPipeline.samples = Math.max(1, Math.min(8, maxSamples));
|
|
429
|
+
|
|
430
|
+
// FXAA - Fast Approximate Anti-Aliasing
|
|
431
|
+
defaultPipeline.fxaaEnabled = true;
|
|
432
|
+
defaultPipeline.fxaa.samples = 8;
|
|
433
|
+
defaultPipeline.fxaa.adaptScaleToCurrentViewport = true;
|
|
434
|
+
if (defaultPipeline.fxaa.edgeThreshold !== undefined) {
|
|
435
|
+
defaultPipeline.fxaa.edgeThreshold = 0.125;
|
|
436
|
+
}
|
|
437
|
+
if (defaultPipeline.fxaa.edgeThresholdMin !== undefined) {
|
|
438
|
+
defaultPipeline.fxaa.edgeThresholdMin = 0.0625;
|
|
439
|
+
}
|
|
440
|
+
if (defaultPipeline.fxaa.subPixelQuality !== undefined) {
|
|
441
|
+
defaultPipeline.fxaa.subPixelQuality = 0.75;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// Grain
|
|
445
|
+
defaultPipeline.grainEnabled = true;
|
|
446
|
+
defaultPipeline.grain.adaptScaleToCurrentViewport = true;
|
|
447
|
+
defaultPipeline.grain.animated = false;
|
|
448
|
+
defaultPipeline.grain.intensity = 3;
|
|
449
|
+
|
|
450
|
+
this.#scene.postProcessRenderPipelineManager.update();
|
|
451
|
+
return true;
|
|
452
|
+
} else {
|
|
453
|
+
defaultPipeline.dispose();
|
|
454
|
+
this.#scene.postProcessRenderPipelineManager.update();
|
|
455
|
+
return false;
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
318
459
|
/**
|
|
319
460
|
* Initializes the environment texture for the Babylon.js scene.
|
|
320
461
|
* Loads an HDR texture from a predefined URI and assigns it to the scene's environmentTexture property.
|
|
@@ -323,15 +464,9 @@ export default class BabylonJSController {
|
|
|
323
464
|
* @returns {boolean}
|
|
324
465
|
*/
|
|
325
466
|
#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;
|
|
467
|
+
const hdrTextureURI = this.#options.ibl.url;
|
|
468
|
+
const hdrTexture = new HDRCubeTexture(hdrTextureURI, this.#scene, 128, false, false, false, true);
|
|
469
|
+
hdrTexture.level = this.#options.ibl.intensity;
|
|
335
470
|
this.#scene.environmentTexture = hdrTexture;
|
|
336
471
|
return true;
|
|
337
472
|
}
|
|
@@ -345,61 +480,149 @@ export default class BabylonJSController {
|
|
|
345
480
|
* @returns {void|false} Returns false if no environment texture is set; otherwise void.
|
|
346
481
|
*/
|
|
347
482
|
#initializeIBLShadows() {
|
|
348
|
-
if (!this.#scene
|
|
483
|
+
if (!this.#scene || !this.#scene.postProcessRenderPipelineManager || this.#scene.activeCamera === null) {
|
|
349
484
|
return false;
|
|
350
485
|
}
|
|
351
486
|
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
487
|
+
// if (!this.#scene.environmentTexture || !this.#scene.environmentTexture.isReady()) {
|
|
488
|
+
if (!this.#scene.environmentTexture) {
|
|
489
|
+
return false;
|
|
490
|
+
}
|
|
491
|
+
const pipelineName = "PrefViewerIblShadowsRenderPipeline";
|
|
492
|
+
const supportedPipelines = this.#scene.postProcessRenderPipelineManager.supportedPipelines;
|
|
493
|
+
|
|
494
|
+
if (!supportedPipelines) {
|
|
495
|
+
return false;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
const oldIblShadowsRenderPipeline = supportedPipelines ? supportedPipelines.find((pipeline) => pipeline.name === pipelineName) : false;
|
|
499
|
+
|
|
500
|
+
if (oldIblShadowsRenderPipeline) {
|
|
501
|
+
oldIblShadowsRenderPipeline.dispose();
|
|
502
|
+
this.#scene.postProcessRenderPipelineManager.update();
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
const pipelineOptions = {
|
|
506
|
+
resolutionExp: 8, // Higher resolution for better shadow quality
|
|
507
|
+
sampleDirections: 4, // More sample directions for smoother shadows
|
|
508
|
+
ssShadowsEnabled: true,
|
|
509
|
+
shadowRemanence: 0.85,
|
|
510
|
+
triPlanarVoxelization: true,
|
|
511
|
+
shadowOpacity: 0.85,
|
|
512
|
+
};
|
|
513
|
+
|
|
514
|
+
const iblShadowsRenderPipeline = new IblShadowsRenderPipeline(pipelineName, this.#scene, pipelineOptions, [this.#scene.activeCamera]);
|
|
515
|
+
|
|
516
|
+
if (!iblShadowsRenderPipeline) {
|
|
517
|
+
return false;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
if (iblShadowsRenderPipeline.isSupported) {
|
|
375
521
|
// Disable all debug passes for performance
|
|
376
522
|
const pipelineProps = {
|
|
377
523
|
allowDebugPasses: false,
|
|
378
524
|
gbufferDebugEnabled: false,
|
|
379
525
|
importanceSamplingDebugEnabled: false,
|
|
380
526
|
voxelDebugEnabled: false,
|
|
381
|
-
voxelDebugDisplayMip:
|
|
527
|
+
voxelDebugDisplayMip: 1,
|
|
382
528
|
voxelDebugAxis: 0,
|
|
383
529
|
voxelTracingDebugEnabled: false,
|
|
384
530
|
spatialBlurPassDebugEnabled: false,
|
|
385
531
|
accumulationPassDebugEnabled: false,
|
|
386
532
|
};
|
|
387
|
-
Object.assign(pipeline, pipelineProps);
|
|
388
|
-
return pipeline;
|
|
389
|
-
};
|
|
390
533
|
|
|
391
|
-
|
|
534
|
+
Object.assign(iblShadowsRenderPipeline, pipelineProps);
|
|
392
535
|
|
|
536
|
+
this.#scene.meshes.forEach((mesh) => {
|
|
537
|
+
if (mesh.id.startsWith("__root__") || mesh.name === "hdri") {
|
|
538
|
+
return false;
|
|
539
|
+
}
|
|
540
|
+
iblShadowsRenderPipeline.addShadowCastingMesh(mesh);
|
|
541
|
+
iblShadowsRenderPipeline.updateSceneBounds();
|
|
542
|
+
mesh.receiveShadows = true; // Not necessary for IBL shadows, but yes for standard shadows
|
|
543
|
+
});
|
|
544
|
+
|
|
545
|
+
this.#scene.materials.forEach((material) => {
|
|
546
|
+
if (material instanceof PBRMaterial) {
|
|
547
|
+
material.enableSpecularAntiAliasing = false;
|
|
548
|
+
}
|
|
549
|
+
iblShadowsRenderPipeline.addShadowReceivingMaterial(material);
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
iblShadowsRenderPipeline.updateVoxelization();
|
|
553
|
+
this.#scene.postProcessRenderPipelineManager.update();
|
|
554
|
+
return true;
|
|
555
|
+
} else {
|
|
556
|
+
iblShadowsRenderPipeline.dispose();
|
|
557
|
+
this.#scene.postProcessRenderPipelineManager.update();
|
|
558
|
+
return false;
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
/**
|
|
563
|
+
* Configures soft shadows for the built-in directional light used when no HDR environment is present.
|
|
564
|
+
* @private
|
|
565
|
+
* @returns {void}
|
|
566
|
+
*/
|
|
567
|
+
#initializeDefaultLightShadows() {
|
|
568
|
+
this.#shadowGen = [];
|
|
569
|
+
this.#dirLight.autoUpdateExtends = false;
|
|
570
|
+
const shadowGenerator = new ShadowGenerator(1024, this.#dirLight);
|
|
571
|
+
shadowGenerator.useBlurExponentialShadowMap = true;
|
|
572
|
+
shadowGenerator.blurKernel = 16;
|
|
573
|
+
shadowGenerator.darkness = 0.5;
|
|
574
|
+
shadowGenerator.bias = 0.0005;
|
|
575
|
+
shadowGenerator.normalBias = 0.02;
|
|
576
|
+
shadowGenerator.getShadowMap().refreshRate = RenderTargetTexture.REFRESHRATE_RENDER_ONCE;
|
|
393
577
|
this.#scene.meshes.forEach((mesh) => {
|
|
394
|
-
if (mesh.id.startsWith("__root__")
|
|
578
|
+
if (mesh.id.startsWith("__root__")) {
|
|
395
579
|
return false;
|
|
396
580
|
}
|
|
397
|
-
|
|
398
|
-
|
|
581
|
+
if (mesh.name !== "hdri") {
|
|
582
|
+
shadowGenerator.addShadowCaster(mesh, true);
|
|
583
|
+
}
|
|
584
|
+
mesh.receiveShadows = true;
|
|
399
585
|
});
|
|
586
|
+
this.#shadowGen.push(shadowGenerator);
|
|
587
|
+
}
|
|
400
588
|
|
|
401
|
-
|
|
402
|
-
|
|
589
|
+
/**
|
|
590
|
+
* Rebuilds the shadow generators contributed by the environment container.
|
|
591
|
+
* Keeps only the generator bound to the built-in `PrefViewerDirLight` so its shadows persist,
|
|
592
|
+
* then adds generators for every directional or spot light coming from the environment asset container.
|
|
593
|
+
* @private
|
|
594
|
+
* @returns {void}
|
|
595
|
+
*/
|
|
596
|
+
#initializeEnvironmentShadows() {
|
|
597
|
+
this.#shadowGen = this.#shadowGen.filter((generator) => {
|
|
598
|
+
if (!generator || typeof generator.getLight !== "function") {
|
|
599
|
+
return false;
|
|
600
|
+
}
|
|
601
|
+
return generator.getLight()?.name === "PrefViewerDirLight";
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
this.#containers.environment?.AssetContainer?.lights.forEach((light) => {
|
|
605
|
+
// Only shadows for DirectionalLight and SpotLight types
|
|
606
|
+
if (!(light instanceof DirectionalLight || light instanceof SpotLight)) {
|
|
607
|
+
return;
|
|
608
|
+
}
|
|
609
|
+
light.autoUpdateExtends = false;
|
|
610
|
+
const shadowGenerator = new ShadowGenerator(1024, light);
|
|
611
|
+
shadowGenerator.useBlurExponentialShadowMap = true;
|
|
612
|
+
shadowGenerator.blurKernel = 16;
|
|
613
|
+
shadowGenerator.darkness = 0.5;
|
|
614
|
+
shadowGenerator.bias = 0.0005;
|
|
615
|
+
shadowGenerator.normalBias = 0.02;
|
|
616
|
+
shadowGenerator.getShadowMap().refreshRate = RenderTargetTexture.REFRESHRATE_RENDER_ONCE;
|
|
617
|
+
this.#scene.meshes.forEach((mesh) => {
|
|
618
|
+
if (mesh.id.startsWith("__root__")) {
|
|
619
|
+
return false;
|
|
620
|
+
}
|
|
621
|
+
if (mesh.name !== "hdri") {
|
|
622
|
+
shadowGenerator.addShadowCaster(mesh, true);
|
|
623
|
+
}
|
|
624
|
+
});
|
|
625
|
+
this.#shadowGen.push(shadowGenerator);
|
|
403
626
|
});
|
|
404
627
|
}
|
|
405
628
|
|
|
@@ -412,20 +635,24 @@ export default class BabylonJSController {
|
|
|
412
635
|
* Otherwise, sets up shadow casting and receiving for all relevant meshes using the shadow generator.
|
|
413
636
|
*/
|
|
414
637
|
#initializeShadows() {
|
|
415
|
-
if (this.#
|
|
416
|
-
|
|
417
|
-
return true;
|
|
638
|
+
if (!this.#settings.shadowsEnabled) {
|
|
639
|
+
return false;
|
|
418
640
|
}
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
641
|
+
if (this.#scene.environmentTexture) {
|
|
642
|
+
if (this.#options.ibl.shadows) {
|
|
643
|
+
if (this.#scene.environmentTexture.isReady()) {
|
|
644
|
+
this.#initializeIBLShadows();
|
|
645
|
+
} else {
|
|
646
|
+
const self = this;
|
|
647
|
+
this.#scene.environmentTexture.onLoadObservable.addOnce(() => {
|
|
648
|
+
self.#initializeIBLShadows();
|
|
649
|
+
});
|
|
650
|
+
}
|
|
427
651
|
}
|
|
428
|
-
}
|
|
652
|
+
} else {
|
|
653
|
+
this.#initializeDefaultLightShadows();
|
|
654
|
+
}
|
|
655
|
+
this.#initializeEnvironmentShadows();
|
|
429
656
|
}
|
|
430
657
|
|
|
431
658
|
/**
|
|
@@ -435,7 +662,7 @@ export default class BabylonJSController {
|
|
|
435
662
|
* @returns {void}
|
|
436
663
|
*/
|
|
437
664
|
#setMaxSimultaneousLights() {
|
|
438
|
-
let lightsNumber = 1; //
|
|
665
|
+
let lightsNumber = 1; // At least one light coming from the environment texture contribution
|
|
439
666
|
this.#scene.lights.forEach((light) => {
|
|
440
667
|
if (light.isEnabled()) {
|
|
441
668
|
++lightsNumber;
|
|
@@ -543,7 +770,7 @@ export default class BabylonJSController {
|
|
|
543
770
|
this.#engine.dispose();
|
|
544
771
|
this.#engine = this.#scene = this.#camera = null;
|
|
545
772
|
this.#hemiLight = this.#dirLight = this.#cameraLight = null;
|
|
546
|
-
this.#shadowGen =
|
|
773
|
+
this.#shadowGen = [];
|
|
547
774
|
}
|
|
548
775
|
|
|
549
776
|
/**
|
|
@@ -609,7 +836,7 @@ export default class BabylonJSController {
|
|
|
609
836
|
*/
|
|
610
837
|
#onPointerMove(event, pickInfo) {
|
|
611
838
|
if (this.#babylonJSAnimationController) {
|
|
612
|
-
this.#babylonJSAnimationController.
|
|
839
|
+
this.#babylonJSAnimationController.highlightMeshes(pickInfo);
|
|
613
840
|
}
|
|
614
841
|
}
|
|
615
842
|
|
|
@@ -741,6 +968,22 @@ export default class BabylonJSController {
|
|
|
741
968
|
return true;
|
|
742
969
|
}
|
|
743
970
|
|
|
971
|
+
/**
|
|
972
|
+
* Applies pending image-based lighting (IBL) option updates.
|
|
973
|
+
* Marks the IBL state as successful, recreates lights so the new environment takes effect, and reports whether anything changed.
|
|
974
|
+
* @private
|
|
975
|
+
* @returns {boolean} True when lights were refreshed due to pending IBL changes, otherwise false.
|
|
976
|
+
*/
|
|
977
|
+
#setOptions_IBL() {
|
|
978
|
+
if (this.#options.ibl.isPending && this.#settings.iblEnabled) {
|
|
979
|
+
this.#options.ibl.setSuccess(true);
|
|
980
|
+
this.#createLights();
|
|
981
|
+
return true;
|
|
982
|
+
}
|
|
983
|
+
this.#createLights();
|
|
984
|
+
return false;
|
|
985
|
+
}
|
|
986
|
+
|
|
744
987
|
/**
|
|
745
988
|
* Finds and returns the asset container object by its name.
|
|
746
989
|
* @private
|
|
@@ -951,6 +1194,7 @@ export default class BabylonJSController {
|
|
|
951
1194
|
*/
|
|
952
1195
|
async #loadContainers() {
|
|
953
1196
|
this.#stopRender();
|
|
1197
|
+
this.#scene.postProcessRenderPipelineManager?.dispose();
|
|
954
1198
|
|
|
955
1199
|
let oldModelMetadata = { ...(this.#containers.model?.state?.metadata ?? {}) };
|
|
956
1200
|
let newModelMetadata = {};
|
|
@@ -988,6 +1232,7 @@ export default class BabylonJSController {
|
|
|
988
1232
|
|
|
989
1233
|
this.#setOptions_Materials();
|
|
990
1234
|
this.#setOptions_Camera();
|
|
1235
|
+
this.#setOptions_IBL();
|
|
991
1236
|
this.#setVisibilityOfWallAndFloorInModel();
|
|
992
1237
|
detail.success = true;
|
|
993
1238
|
})
|
|
@@ -1000,13 +1245,26 @@ export default class BabylonJSController {
|
|
|
1000
1245
|
.finally(async () => {
|
|
1001
1246
|
this.#checkModelMetadata(oldModelMetadata, newModelMetadata);
|
|
1002
1247
|
this.#setMaxSimultaneousLights();
|
|
1003
|
-
this.#
|
|
1004
|
-
this.#babylonJSAnimationController = new BabylonJSAnimationController(this.#
|
|
1248
|
+
this.#loadCameraDepentEffects();
|
|
1249
|
+
this.#babylonJSAnimationController = new BabylonJSAnimationController(this.#containers.model.assetContainer);
|
|
1250
|
+
this.#forceReflectionsInModelGlasses();
|
|
1005
1251
|
this.#startRender();
|
|
1006
1252
|
});
|
|
1007
1253
|
return detail;
|
|
1008
1254
|
}
|
|
1009
1255
|
|
|
1256
|
+
/**
|
|
1257
|
+
* Reinstalls every camera-sensitive post-process (default pipeline, SSAO, shadows) after loads or option changes.
|
|
1258
|
+
* Ensures the active camera always owns fresh pipelines so render quality remains consistent across reload cycles.
|
|
1259
|
+
* @private
|
|
1260
|
+
* @returns {void}
|
|
1261
|
+
*/
|
|
1262
|
+
#loadCameraDepentEffects() {
|
|
1263
|
+
this.#initializeVisualImprovements();
|
|
1264
|
+
this.#initializeAmbientOcclussion();
|
|
1265
|
+
this.#initializeShadows();
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1010
1268
|
/**
|
|
1011
1269
|
* Checks and applies model metadata changes after asset loading.
|
|
1012
1270
|
* @private
|
|
@@ -1083,6 +1341,21 @@ export default class BabylonJSController {
|
|
|
1083
1341
|
}
|
|
1084
1342
|
}
|
|
1085
1343
|
|
|
1344
|
+
#forceReflectionsInModelGlasses(assetContainer) {
|
|
1345
|
+
if (assetContainer === undefined) {
|
|
1346
|
+
assetContainer = this.#containers.model.assetContainer;
|
|
1347
|
+
}
|
|
1348
|
+
if (!this.#scene || !assetContainer) {
|
|
1349
|
+
return;
|
|
1350
|
+
}
|
|
1351
|
+
assetContainer.materials?.forEach(material => {
|
|
1352
|
+
if (material && material.name && material.name.includes("Glass")) {
|
|
1353
|
+
material.metallic = 1.0;
|
|
1354
|
+
material.environmentIntensity = 1.0;
|
|
1355
|
+
}
|
|
1356
|
+
});
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1086
1359
|
/**
|
|
1087
1360
|
* Translates a node along the scene's vertical (Y) axis by the provided value.
|
|
1088
1361
|
* @private
|
|
@@ -1235,9 +1508,19 @@ export default class BabylonJSController {
|
|
|
1235
1508
|
this.#engine = new Engine(this.#canvas, true, { alpha: true, stencil: true, preserveDrawingBuffer: false });
|
|
1236
1509
|
this.#engine.disableUniformBuffers = true;
|
|
1237
1510
|
this.#scene = new Scene(this.#engine);
|
|
1511
|
+
|
|
1512
|
+
// Activate the rendering of geometry data into a G-buffer, essential for advanced effects like deferred shading,
|
|
1513
|
+
// SSAO, and Velocity-Texture-Animation (VAT), allowing for complex post-processing by separating rendering into
|
|
1514
|
+
// different buffers (depth, normals, velocity) for later use in shaders.
|
|
1515
|
+
const geometryBufferRenderer = this.#scene.enableGeometryBufferRenderer();
|
|
1516
|
+
if (geometryBufferRenderer) {
|
|
1517
|
+
geometryBufferRenderer.enableScreenspaceDepth = true;
|
|
1518
|
+
geometryBufferRenderer.enableDepth = false;
|
|
1519
|
+
geometryBufferRenderer.generateNormalsInWorldSpace = true;
|
|
1520
|
+
}
|
|
1521
|
+
|
|
1238
1522
|
this.#scene.clearColor = new Color4(1, 1, 1, 1);
|
|
1239
1523
|
this.#createCamera();
|
|
1240
|
-
this.#createLights();
|
|
1241
1524
|
this.#enableInteraction();
|
|
1242
1525
|
await this.#createXRExperience();
|
|
1243
1526
|
this.#startRender();
|
|
@@ -1278,6 +1561,7 @@ export default class BabylonJSController {
|
|
|
1278
1561
|
setCameraOptions() {
|
|
1279
1562
|
this.#stopRender();
|
|
1280
1563
|
const cameraOptionsSetted = this.#setOptions_Camera();
|
|
1564
|
+
this.#loadCameraDepentEffects();
|
|
1281
1565
|
this.#startRender();
|
|
1282
1566
|
return cameraOptionsSetted;
|
|
1283
1567
|
}
|
|
@@ -1295,6 +1579,20 @@ export default class BabylonJSController {
|
|
|
1295
1579
|
return materialsOptionsSetted;
|
|
1296
1580
|
}
|
|
1297
1581
|
|
|
1582
|
+
/**
|
|
1583
|
+
* Reapplies image-based lighting configuration (HDR URL, intensity, shadow mode).
|
|
1584
|
+
* Stops rendering, pushes pending IBL state into the scene, rebuilds camera-dependent effects, then resumes rendering.
|
|
1585
|
+
* @public
|
|
1586
|
+
* @returns {void}
|
|
1587
|
+
*/
|
|
1588
|
+
setIBLOptions() {
|
|
1589
|
+
this.#stopRender();
|
|
1590
|
+
const IBLOptionsSetted = this.#setOptions_IBL();
|
|
1591
|
+
this.#loadCameraDepentEffects();
|
|
1592
|
+
this.#startRender();
|
|
1593
|
+
return IBLOptionsSetted;
|
|
1594
|
+
}
|
|
1595
|
+
|
|
1298
1596
|
/**
|
|
1299
1597
|
* Sets the visibility of a container (model, environment, etc.) by name.
|
|
1300
1598
|
* 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,53 @@ 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
|
+
url = options.ibl.url;
|
|
340
|
+
// TEMPORARY: Disable FileStorage usage due to efficiency problems
|
|
341
|
+
// const fileStorage = new FileStorage("PrefViewer", "Files");
|
|
342
|
+
// const newURL = await fileStorage.getURL(options.ibl.url);
|
|
343
|
+
// if (newURL) {
|
|
344
|
+
// url = newURL;
|
|
345
|
+
// timeStamp = await fileStorage.getTimeStamp(options.ibl.url);
|
|
346
|
+
// }
|
|
347
|
+
}
|
|
348
|
+
if (options.ibl.shadows !== undefined) {
|
|
349
|
+
shadows = options.ibl.shadows;
|
|
350
|
+
}
|
|
351
|
+
if (options.ibl.intensity !== undefined) {
|
|
352
|
+
intensity = options.ibl.intensity;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const needUpdate = url !== undefined && url !== iblState.url ||
|
|
356
|
+
timeStamp !== undefined && timeStamp !== iblState.timeStamp ||
|
|
357
|
+
shadows !== undefined && shadows !== iblState.shadows ||
|
|
358
|
+
intensity !== undefined && intensity !== iblState.intensity;
|
|
359
|
+
if (needUpdate) {
|
|
360
|
+
iblState.setValues(url, intensity, shadows, timeStamp);
|
|
361
|
+
iblState.setPending(true);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
return needUpdate;
|
|
365
|
+
}
|
|
366
|
+
|
|
309
367
|
/**
|
|
310
368
|
* Dispatches a "prefviewer3d-loading" event and updates loading state attributes.
|
|
311
369
|
* Used internally when a loading operation starts.
|
|
@@ -490,6 +548,7 @@ export default class PrefViewer3D extends HTMLElement {
|
|
|
490
548
|
if (config.options) {
|
|
491
549
|
this.#checkNeedToUpdateCamera(config.options);
|
|
492
550
|
this.#checkNeedToUpdateMaterials(config.options);
|
|
551
|
+
this.#checkNeedToUpdateIBL(config.options);
|
|
493
552
|
}
|
|
494
553
|
|
|
495
554
|
const loadDetail = await this.#babylonJSController.load();
|
|
@@ -518,6 +577,9 @@ export default class PrefViewer3D extends HTMLElement {
|
|
|
518
577
|
if (this.#checkNeedToUpdateMaterials(options)) {
|
|
519
578
|
someSetted = someSetted || this.#babylonJSController.setMaterialOptions();
|
|
520
579
|
}
|
|
580
|
+
if (this.#checkNeedToUpdateIBL(options)) {
|
|
581
|
+
someSetted = someSetted || this.#babylonJSController.setIBLOptions();
|
|
582
|
+
}
|
|
521
583
|
const detail = this.#onSetOptions();
|
|
522
584
|
return { success: someSetted, detail: detail };
|
|
523
585
|
}
|