@preference-sl/pref-viewer 2.12.0-beta.4 → 2.12.0-beta.6
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, DefaultRenderingPipeline, DirectionalLight, Engine, HDRCubeTexture, HemisphericLight, IblShadowsRenderPipeline, LoadAssetContainerAsync, MeshBuilder, PBRMaterial, PointerEventTypes, PointLight, Scene, ShadowGenerator, SpotLight, SSAORenderingPipeline, 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";
|
|
@@ -118,20 +118,25 @@ export default class BabylonJSController {
|
|
|
118
118
|
#shadowGen = [];
|
|
119
119
|
#XRExperience = null;
|
|
120
120
|
#canvasResizeObserver = null;
|
|
121
|
-
|
|
121
|
+
|
|
122
122
|
#containers = {};
|
|
123
123
|
#options = {};
|
|
124
|
-
|
|
124
|
+
|
|
125
125
|
#gltfResolver = null; // GLTFResolver instance
|
|
126
126
|
#babylonJSAnimationController = null; // AnimationController instance
|
|
127
|
-
|
|
127
|
+
|
|
128
128
|
#handlers = {
|
|
129
129
|
onKeyUp: null,
|
|
130
130
|
onPointerObservable: null,
|
|
131
131
|
renderLoop: null,
|
|
132
132
|
};
|
|
133
|
-
|
|
134
|
-
#
|
|
133
|
+
|
|
134
|
+
#settings = {
|
|
135
|
+
antiAliasingEnabled: true,
|
|
136
|
+
ambientOcclusionEnabled: true,
|
|
137
|
+
iblEnabled: true,
|
|
138
|
+
shadowsEnabled: false,
|
|
139
|
+
};
|
|
135
140
|
|
|
136
141
|
/**
|
|
137
142
|
* Constructs a new BabylonJSController instance.
|
|
@@ -298,7 +303,7 @@ export default class BabylonJSController {
|
|
|
298
303
|
* @returns {void}
|
|
299
304
|
*/
|
|
300
305
|
#createLights() {
|
|
301
|
-
if (this.#options.ibl && this.#options.ibl.url) {
|
|
306
|
+
if (this.#settings.iblEnabled && this.#options.ibl && this.#options.ibl.url) {
|
|
302
307
|
this.#hemiLight = this.#dirLight = this.#cameraLight = null;
|
|
303
308
|
this.#initializeEnvironmentTexture();
|
|
304
309
|
}
|
|
@@ -308,18 +313,27 @@ export default class BabylonJSController {
|
|
|
308
313
|
}
|
|
309
314
|
|
|
310
315
|
// 1) Stronger ambient fill
|
|
311
|
-
this.#hemiLight =
|
|
312
|
-
this.#hemiLight
|
|
316
|
+
this.#hemiLight = this.#scene.getLightByName("PrefViewerHemiLight");
|
|
317
|
+
if (!this.#hemiLight) {
|
|
318
|
+
this.#hemiLight = new HemisphericLight("PrefViewerHemiLight", new Vector3(-10, 10, -10), this.#scene);
|
|
319
|
+
this.#hemiLight.intensity = 0.6;
|
|
320
|
+
}
|
|
313
321
|
|
|
314
322
|
// 2) Directional light from the front-right, angled slightly down
|
|
315
|
-
this.#dirLight =
|
|
316
|
-
this.#dirLight
|
|
317
|
-
|
|
323
|
+
this.#dirLight = this.#scene.getLightByName("PrefViewerDirLight");
|
|
324
|
+
if (!this.#dirLight) {
|
|
325
|
+
this.#dirLight = new DirectionalLight("PrefViewerDirLight", new Vector3(-10, 10, -10), this.#scene);
|
|
326
|
+
this.#dirLight.position = new Vector3(5, 4, 5); // light is IN FRONT + ABOVE + to the RIGHT
|
|
327
|
+
this.#dirLight.intensity = 0.6;
|
|
328
|
+
}
|
|
318
329
|
|
|
319
330
|
// 3) Camera‐attached headlight
|
|
320
|
-
this.#cameraLight =
|
|
321
|
-
this.#cameraLight
|
|
322
|
-
|
|
331
|
+
this.#cameraLight = this.#scene.getLightByName("PrefViewerCameraLight");
|
|
332
|
+
if (!this.#cameraLight) {
|
|
333
|
+
this.#cameraLight = new PointLight("PrefViewerCameraLight", this.#camera.position, this.#scene);
|
|
334
|
+
this.#cameraLight.parent = this.#camera;
|
|
335
|
+
this.#cameraLight.intensity = 0.3;
|
|
336
|
+
}
|
|
323
337
|
}
|
|
324
338
|
|
|
325
339
|
/**
|
|
@@ -334,6 +348,10 @@ export default class BabylonJSController {
|
|
|
334
348
|
return false;
|
|
335
349
|
}
|
|
336
350
|
|
|
351
|
+
if (!this.#settings.ambientOcclusionEnabled) {
|
|
352
|
+
return false;
|
|
353
|
+
}
|
|
354
|
+
|
|
337
355
|
const pipelineName = "PrefViewerSSAORenderingPipeline";
|
|
338
356
|
|
|
339
357
|
const supportedPipelines = this.#scene.postProcessRenderPipelineManager.supportedPipelines;
|
|
@@ -366,6 +384,13 @@ export default class BabylonJSController {
|
|
|
366
384
|
ssaoPipeline.radius = 0.0001;
|
|
367
385
|
ssaoPipeline.totalStrength = 1;
|
|
368
386
|
ssaoPipeline.base = 0.6;
|
|
387
|
+
|
|
388
|
+
// Configure SSAO to calculate only once instead of every frame for better performance
|
|
389
|
+
if (ssaoPipeline._ssaoPostProcess) {
|
|
390
|
+
ssaoPipeline._ssaoPostProcess.autoClear = false;
|
|
391
|
+
ssaoPipeline._ssaoPostProcess.samples = 1;
|
|
392
|
+
}
|
|
393
|
+
|
|
369
394
|
this.#scene.postProcessRenderPipelineManager.update();
|
|
370
395
|
return true;
|
|
371
396
|
} else {
|
|
@@ -402,6 +427,10 @@ export default class BabylonJSController {
|
|
|
402
427
|
this.#scene.postProcessRenderPipelineManager.update();
|
|
403
428
|
}
|
|
404
429
|
|
|
430
|
+
if (!this.#settings.antiAliasingEnabled) {
|
|
431
|
+
return false;
|
|
432
|
+
}
|
|
433
|
+
|
|
405
434
|
const defaultPipeline = new DefaultRenderingPipeline(pipelineName, true, this.#scene, [this.#scene.activeCamera], true);
|
|
406
435
|
|
|
407
436
|
if (!defaultPipeline){
|
|
@@ -434,6 +463,14 @@ export default class BabylonJSController {
|
|
|
434
463
|
defaultPipeline.grain.animated = false;
|
|
435
464
|
defaultPipeline.grain.intensity = 3;
|
|
436
465
|
|
|
466
|
+
// Configure post-processes to calculate only once instead of every frame for better performance
|
|
467
|
+
if (defaultPipeline.fxaa?._postProcess) {
|
|
468
|
+
defaultPipeline.fxaa._postProcess.autoClear = false;
|
|
469
|
+
}
|
|
470
|
+
if (defaultPipeline.grain?._postProcess) {
|
|
471
|
+
defaultPipeline.grain._postProcess.autoClear = false;
|
|
472
|
+
}
|
|
473
|
+
|
|
437
474
|
this.#scene.postProcessRenderPipelineManager.update();
|
|
438
475
|
return true;
|
|
439
476
|
} else {
|
|
@@ -520,8 +557,15 @@ export default class BabylonJSController {
|
|
|
520
557
|
|
|
521
558
|
Object.assign(iblShadowsRenderPipeline, pipelineProps);
|
|
522
559
|
|
|
560
|
+
if (iblShadowsRenderPipeline._ssaoPostProcess) {
|
|
561
|
+
iblShadowsRenderPipeline._ssaoPostProcess.autoClear = false;
|
|
562
|
+
iblShadowsRenderPipeline._ssaoPostProcess.samples = 1;
|
|
563
|
+
}
|
|
564
|
+
|
|
523
565
|
this.#scene.meshes.forEach((mesh) => {
|
|
524
|
-
|
|
566
|
+
const isRootMesh = mesh.id.startsWith("__root__");
|
|
567
|
+
const isHDRIMesh = mesh.name?.toLowerCase() === "hdri";
|
|
568
|
+
if (isRootMesh || isHDRIMesh) {
|
|
525
569
|
return false;
|
|
526
570
|
}
|
|
527
571
|
iblShadowsRenderPipeline.addShadowCastingMesh(mesh);
|
|
@@ -553,15 +597,19 @@ export default class BabylonJSController {
|
|
|
553
597
|
*/
|
|
554
598
|
#initializeDefaultLightShadows() {
|
|
555
599
|
this.#shadowGen = [];
|
|
600
|
+
this.#dirLight.autoUpdateExtends = false;
|
|
556
601
|
const shadowGenerator = new ShadowGenerator(1024, this.#dirLight);
|
|
557
602
|
shadowGenerator.useBlurExponentialShadowMap = true;
|
|
558
603
|
shadowGenerator.blurKernel = 16;
|
|
559
604
|
shadowGenerator.darkness = 0.5;
|
|
605
|
+
shadowGenerator.bias = 0.0005;
|
|
606
|
+
shadowGenerator.normalBias = 0.02;
|
|
607
|
+
shadowGenerator.getShadowMap().refreshRate = RenderTargetTexture.REFRESHRATE_RENDER_ONCE;
|
|
560
608
|
this.#scene.meshes.forEach((mesh) => {
|
|
561
609
|
if (mesh.id.startsWith("__root__")) {
|
|
562
610
|
return false;
|
|
563
611
|
}
|
|
564
|
-
if (mesh.name !== "hdri") {
|
|
612
|
+
if (mesh.name?.toLowerCase() !== "hdri") {
|
|
565
613
|
shadowGenerator.addShadowCaster(mesh, true);
|
|
566
614
|
}
|
|
567
615
|
mesh.receiveShadows = true;
|
|
@@ -589,15 +637,19 @@ export default class BabylonJSController {
|
|
|
589
637
|
if (!(light instanceof DirectionalLight || light instanceof SpotLight)) {
|
|
590
638
|
return;
|
|
591
639
|
}
|
|
640
|
+
light.autoUpdateExtends = false;
|
|
592
641
|
const shadowGenerator = new ShadowGenerator(1024, light);
|
|
593
642
|
shadowGenerator.useBlurExponentialShadowMap = true;
|
|
594
643
|
shadowGenerator.blurKernel = 16;
|
|
595
644
|
shadowGenerator.darkness = 0.5;
|
|
645
|
+
shadowGenerator.bias = 0.0005;
|
|
646
|
+
shadowGenerator.normalBias = 0.02;
|
|
647
|
+
shadowGenerator.getShadowMap().refreshRate = RenderTargetTexture.REFRESHRATE_RENDER_ONCE;
|
|
596
648
|
this.#scene.meshes.forEach((mesh) => {
|
|
597
649
|
if (mesh.id.startsWith("__root__")) {
|
|
598
650
|
return false;
|
|
599
651
|
}
|
|
600
|
-
if (mesh.name !== "hdri") {
|
|
652
|
+
if (mesh.name?.toLowerCase() !== "hdri") {
|
|
601
653
|
shadowGenerator.addShadowCaster(mesh, true);
|
|
602
654
|
}
|
|
603
655
|
});
|
|
@@ -614,7 +666,7 @@ export default class BabylonJSController {
|
|
|
614
666
|
* Otherwise, sets up shadow casting and receiving for all relevant meshes using the shadow generator.
|
|
615
667
|
*/
|
|
616
668
|
#initializeShadows() {
|
|
617
|
-
if (!this.#shadowsEnabled) {
|
|
669
|
+
if (!this.#settings.shadowsEnabled) {
|
|
618
670
|
return false;
|
|
619
671
|
}
|
|
620
672
|
if (this.#scene.environmentTexture) {
|
|
@@ -815,7 +867,7 @@ export default class BabylonJSController {
|
|
|
815
867
|
*/
|
|
816
868
|
#onPointerMove(event, pickInfo) {
|
|
817
869
|
if (this.#babylonJSAnimationController) {
|
|
818
|
-
this.#babylonJSAnimationController.
|
|
870
|
+
this.#babylonJSAnimationController.highlightMeshes(pickInfo);
|
|
819
871
|
}
|
|
820
872
|
}
|
|
821
873
|
|
|
@@ -954,11 +1006,12 @@ export default class BabylonJSController {
|
|
|
954
1006
|
* @returns {boolean} True when lights were refreshed due to pending IBL changes, otherwise false.
|
|
955
1007
|
*/
|
|
956
1008
|
#setOptions_IBL() {
|
|
957
|
-
if (this.#options.ibl.isPending) {
|
|
1009
|
+
if (this.#options.ibl.isPending && this.#settings.iblEnabled) {
|
|
958
1010
|
this.#options.ibl.setSuccess(true);
|
|
959
1011
|
this.#createLights();
|
|
960
1012
|
return true;
|
|
961
1013
|
}
|
|
1014
|
+
this.#createLights();
|
|
962
1015
|
return false;
|
|
963
1016
|
}
|
|
964
1017
|
|
|
@@ -1224,7 +1277,8 @@ export default class BabylonJSController {
|
|
|
1224
1277
|
this.#checkModelMetadata(oldModelMetadata, newModelMetadata);
|
|
1225
1278
|
this.#setMaxSimultaneousLights();
|
|
1226
1279
|
this.#loadCameraDepentEffects();
|
|
1227
|
-
this.#babylonJSAnimationController = new BabylonJSAnimationController(this.#
|
|
1280
|
+
this.#babylonJSAnimationController = new BabylonJSAnimationController(this.#containers.model.assetContainer);
|
|
1281
|
+
this.#forceReflectionsInModelGlasses();
|
|
1228
1282
|
this.#startRender();
|
|
1229
1283
|
});
|
|
1230
1284
|
return detail;
|
|
@@ -1318,6 +1372,28 @@ export default class BabylonJSController {
|
|
|
1318
1372
|
}
|
|
1319
1373
|
}
|
|
1320
1374
|
|
|
1375
|
+
/**
|
|
1376
|
+
* Configures realistic glass material properties for all materials whose names include "Glass".
|
|
1377
|
+
* @private
|
|
1378
|
+
* @param {AssetContainer} [assetContainer] - The asset container with materials to process. If undefined, uses the model container.
|
|
1379
|
+
* @returns {void}
|
|
1380
|
+
* @note This method assumes that glass materials are named with the substring "Glass".
|
|
1381
|
+
*/
|
|
1382
|
+
#forceReflectionsInModelGlasses(assetContainer) {
|
|
1383
|
+
if (assetContainer === undefined) {
|
|
1384
|
+
assetContainer = this.#containers.model.assetContainer;
|
|
1385
|
+
}
|
|
1386
|
+
if (!this.#scene || !assetContainer) {
|
|
1387
|
+
return;
|
|
1388
|
+
}
|
|
1389
|
+
assetContainer.materials?.forEach(material => {
|
|
1390
|
+
if (material && material.name && material.name.includes("Glass")) {
|
|
1391
|
+
material.metallic = 0.25;
|
|
1392
|
+
material.environmentIntensity = 1.0;
|
|
1393
|
+
}
|
|
1394
|
+
});
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1321
1397
|
/**
|
|
1322
1398
|
* Translates a node along the scene's vertical (Y) axis by the provided value.
|
|
1323
1399
|
* @private
|
|
@@ -1483,7 +1559,6 @@ export default class BabylonJSController {
|
|
|
1483
1559
|
|
|
1484
1560
|
this.#scene.clearColor = new Color4(1, 1, 1, 1);
|
|
1485
1561
|
this.#createCamera();
|
|
1486
|
-
this.#createLights();
|
|
1487
1562
|
this.#enableInteraction();
|
|
1488
1563
|
await this.#createXRExperience();
|
|
1489
1564
|
this.#startRender();
|
package/src/file-storage.js
CHANGED
|
@@ -169,7 +169,7 @@ export class FileStorage {
|
|
|
169
169
|
let file = undefined;
|
|
170
170
|
return new Promise((resolve) => {
|
|
171
171
|
const xhr = new XMLHttpRequest();
|
|
172
|
-
xhr.open("GET", uri, true);
|
|
172
|
+
xhr.open("GET", encodeURI(uri), true);
|
|
173
173
|
xhr.responseType = "blob";
|
|
174
174
|
xhr.onload = () => {
|
|
175
175
|
if (xhr.status === 200) {
|
|
@@ -205,7 +205,7 @@ export class FileStorage {
|
|
|
205
205
|
let timeStamp = null;
|
|
206
206
|
return new Promise((resolve) => {
|
|
207
207
|
const xhr = new XMLHttpRequest();
|
|
208
|
-
xhr.open("HEAD", uri, true);
|
|
208
|
+
xhr.open("HEAD", encodeURI(uri), true);
|
|
209
209
|
xhr.responseType = "blob";
|
|
210
210
|
xhr.onload = () => {
|
|
211
211
|
if (xhr.status === 200) {
|
|
@@ -238,7 +238,7 @@ export class FileStorage {
|
|
|
238
238
|
let size = 0;
|
|
239
239
|
return new Promise((resolve) => {
|
|
240
240
|
const xhr = new XMLHttpRequest();
|
|
241
|
-
xhr.open("HEAD", uri, true);
|
|
241
|
+
xhr.open("HEAD", encodeURI(uri), true);
|
|
242
242
|
xhr.responseType = "blob";
|
|
243
243
|
xhr.onload = () => {
|
|
244
244
|
if (xhr.status === 200) {
|
package/src/pref-viewer-3d.js
CHANGED
|
@@ -336,12 +336,14 @@ export default class PrefViewer3D extends HTMLElement {
|
|
|
336
336
|
let intensity = undefined;
|
|
337
337
|
|
|
338
338
|
if (options.ibl.url) {
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
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
|
+
// }
|
|
345
347
|
}
|
|
346
348
|
if (options.ibl.shadows !== undefined) {
|
|
347
349
|
shadows = options.ibl.shadows;
|