@combeenation/3d-viewer 12.4.1-beta4 → 12.4.2-rc1

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.
Files changed (116) hide show
  1. package/README.md +9 -9
  2. package/dist/lib-cjs/api/classes/animationInterface.d.ts +8 -8
  3. package/dist/lib-cjs/api/classes/animationInterface.js +2 -2
  4. package/dist/lib-cjs/api/classes/dottedPath.d.ts +79 -79
  5. package/dist/lib-cjs/api/classes/dottedPath.js +166 -166
  6. package/dist/lib-cjs/api/classes/element.d.ts +153 -153
  7. package/dist/lib-cjs/api/classes/element.js +702 -702
  8. package/dist/lib-cjs/api/classes/event.d.ts +401 -401
  9. package/dist/lib-cjs/api/classes/event.js +424 -424
  10. package/dist/lib-cjs/api/classes/eventBroadcaster.d.ts +26 -26
  11. package/dist/lib-cjs/api/classes/eventBroadcaster.js +49 -49
  12. package/dist/lib-cjs/api/classes/fuzzyMap.d.ts +7 -7
  13. package/dist/lib-cjs/api/classes/fuzzyMap.js +21 -21
  14. package/dist/lib-cjs/api/classes/parameter.d.ts +410 -410
  15. package/dist/lib-cjs/api/classes/parameter.js +642 -642
  16. package/dist/lib-cjs/api/classes/parameterObservable.d.ts +36 -36
  17. package/dist/lib-cjs/api/classes/parameterObservable.js +72 -72
  18. package/dist/lib-cjs/api/classes/parameterizable.d.ts +15 -15
  19. package/dist/lib-cjs/api/classes/parameterizable.js +102 -102
  20. package/dist/lib-cjs/api/classes/placementAnimation.d.ts +45 -45
  21. package/dist/lib-cjs/api/classes/placementAnimation.js +176 -176
  22. package/dist/lib-cjs/api/classes/variant.d.ts +261 -261
  23. package/dist/lib-cjs/api/classes/variant.js +872 -872
  24. package/dist/lib-cjs/api/classes/variantInstance.d.ts +53 -53
  25. package/dist/lib-cjs/api/classes/variantInstance.js +125 -125
  26. package/dist/lib-cjs/api/classes/variantParameterizable.d.ts +17 -17
  27. package/dist/lib-cjs/api/classes/variantParameterizable.js +86 -86
  28. package/dist/lib-cjs/api/classes/viewer.d.ts +215 -215
  29. package/dist/lib-cjs/api/classes/viewer.js +708 -727
  30. package/dist/lib-cjs/api/classes/viewer.js.map +1 -1
  31. package/dist/lib-cjs/api/classes/viewerError.d.ts +43 -43
  32. package/dist/lib-cjs/api/classes/viewerError.js +55 -55
  33. package/dist/lib-cjs/api/classes/viewerLight.d.ts +66 -66
  34. package/dist/lib-cjs/api/classes/viewerLight.js +344 -344
  35. package/dist/lib-cjs/api/internal/lensRendering.d.ts +8 -8
  36. package/dist/lib-cjs/api/internal/lensRendering.js +11 -11
  37. package/dist/lib-cjs/api/internal/sceneSetup.d.ts +13 -13
  38. package/dist/lib-cjs/api/internal/sceneSetup.js +227 -227
  39. package/dist/lib-cjs/api/manager/animationManager.d.ts +30 -30
  40. package/dist/lib-cjs/api/manager/animationManager.js +126 -126
  41. package/dist/lib-cjs/api/manager/gltfExportManager.d.ts +89 -80
  42. package/dist/lib-cjs/api/manager/gltfExportManager.js +311 -299
  43. package/dist/lib-cjs/api/manager/gltfExportManager.js.map +1 -1
  44. package/dist/lib-cjs/api/manager/sceneManager.d.ts +33 -33
  45. package/dist/lib-cjs/api/manager/sceneManager.js +128 -128
  46. package/dist/lib-cjs/api/manager/tagManager.d.ts +118 -118
  47. package/dist/lib-cjs/api/manager/tagManager.js +530 -530
  48. package/dist/lib-cjs/api/manager/textureLoadManager.d.ts +22 -22
  49. package/dist/lib-cjs/api/manager/textureLoadManager.js +107 -98
  50. package/dist/lib-cjs/api/manager/textureLoadManager.js.map +1 -1
  51. package/dist/lib-cjs/api/manager/variantInstanceManager.d.ts +106 -106
  52. package/dist/lib-cjs/api/manager/variantInstanceManager.js +290 -290
  53. package/dist/lib-cjs/api/store/specStorage.d.ts +32 -32
  54. package/dist/lib-cjs/api/store/specStorage.js +65 -65
  55. package/dist/lib-cjs/api/util/babylonHelper.d.ts +238 -238
  56. package/dist/lib-cjs/api/util/babylonHelper.js +825 -825
  57. package/dist/lib-cjs/api/util/debugHelper.d.ts +9 -9
  58. package/dist/lib-cjs/api/util/debugHelper.js +93 -93
  59. package/dist/lib-cjs/api/util/deviceHelper.d.ts +9 -9
  60. package/dist/lib-cjs/api/util/deviceHelper.js +28 -28
  61. package/dist/lib-cjs/api/util/geometryHelper.d.ts +17 -17
  62. package/dist/lib-cjs/api/util/geometryHelper.js +112 -112
  63. package/dist/lib-cjs/api/util/globalTypes.d.ts +490 -490
  64. package/dist/lib-cjs/api/util/globalTypes.js +1 -1
  65. package/dist/lib-cjs/api/util/resourceHelper.d.ts +58 -58
  66. package/dist/lib-cjs/api/util/resourceHelper.js +214 -214
  67. package/dist/lib-cjs/api/util/sceneLoaderHelper.d.ts +58 -58
  68. package/dist/lib-cjs/api/util/sceneLoaderHelper.js +228 -228
  69. package/dist/lib-cjs/api/util/stringHelper.d.ts +13 -13
  70. package/dist/lib-cjs/api/util/stringHelper.js +32 -32
  71. package/dist/lib-cjs/api/util/structureHelper.d.ts +9 -9
  72. package/dist/lib-cjs/api/util/structureHelper.js +57 -57
  73. package/dist/lib-cjs/buildinfo.json +3 -3
  74. package/dist/lib-cjs/commonjs.tsconfig.tsbuildinfo +1 -1
  75. package/dist/lib-cjs/index.d.ts +63 -63
  76. package/dist/lib-cjs/index.js +128 -128
  77. package/package.json +92 -92
  78. package/src/api/classes/animationInterface.ts +10 -10
  79. package/src/api/classes/dottedPath.ts +181 -181
  80. package/src/api/classes/element.ts +766 -766
  81. package/src/api/classes/event.ts +457 -457
  82. package/src/api/classes/eventBroadcaster.ts +52 -52
  83. package/src/api/classes/fuzzyMap.ts +21 -21
  84. package/src/api/classes/parameter.ts +686 -686
  85. package/src/api/classes/parameterObservable.ts +73 -73
  86. package/src/api/classes/parameterizable.ts +87 -87
  87. package/src/api/classes/placementAnimation.ts +162 -162
  88. package/src/api/classes/variant.ts +965 -965
  89. package/src/api/classes/variantInstance.ts +123 -123
  90. package/src/api/classes/variantParameterizable.ts +83 -83
  91. package/src/api/classes/viewer.ts +751 -774
  92. package/src/api/classes/viewerError.ts +63 -63
  93. package/src/api/classes/viewerLight.ts +335 -335
  94. package/src/api/internal/debugViewer.ts +90 -90
  95. package/src/api/internal/lensRendering.ts +9 -9
  96. package/src/api/internal/sceneSetup.ts +208 -208
  97. package/src/api/manager/animationManager.ts +143 -143
  98. package/src/api/manager/gltfExportManager.ts +350 -334
  99. package/src/api/manager/sceneManager.ts +134 -134
  100. package/src/api/manager/tagManager.ts +572 -572
  101. package/src/api/manager/textureLoadManager.ts +107 -96
  102. package/src/api/manager/variantInstanceManager.ts +306 -306
  103. package/src/api/store/specStorage.ts +68 -68
  104. package/src/api/util/babylonHelper.ts +915 -915
  105. package/src/api/util/debugHelper.ts +121 -121
  106. package/src/api/util/deviceHelper.ts +31 -31
  107. package/src/api/util/geometryHelper.ts +142 -142
  108. package/src/api/util/globalTypes.ts +566 -566
  109. package/src/api/util/resourceHelper.ts +201 -201
  110. package/src/api/util/sceneLoaderHelper.ts +247 -247
  111. package/src/api/util/stringHelper.ts +30 -30
  112. package/src/api/util/structureHelper.ts +62 -62
  113. package/src/buildinfo.json +3 -3
  114. package/src/dev.ts +70 -70
  115. package/src/index.ts +116 -116
  116. package/src/types.d.ts +49 -49
@@ -1,334 +1,350 @@
1
- import { Viewer } from '../classes/viewer';
2
- import { injectMetadata } from '../util/babylonHelper';
3
- import { getIsScaledDownDevice } from '../util/deviceHelper';
4
- import { bakeGeometryOfMesh, createMeshFromInstancedMesh, resetTransformation } from '../util/geometryHelper';
5
- import { isNodeIncludedInExclusionList } from '../util/structureHelper';
6
- import { PBRMaterial } from '@babylonjs/core/Materials/PBR/pbrMaterial';
7
- import { DynamicTexture } from '@babylonjs/core/Materials/Textures/dynamicTexture';
8
- import { RenderTargetTexture } from '@babylonjs/core/Materials/Textures/renderTargetTexture';
9
- import { Vector3 } from '@babylonjs/core/Maths/math.vector';
10
- import { InstancedMesh } from '@babylonjs/core/Meshes/instancedMesh';
11
- import { Mesh } from '@babylonjs/core/Meshes/mesh';
12
- import { TransformNode } from '@babylonjs/core/Meshes/transformNode';
13
- import { Node as BjsNode } from '@babylonjs/core/node';
14
- import '@babylonjs/serializers/glTF/2.0/Extensions/KHR_texture_transform';
15
- import { GLTF2Export } from '@babylonjs/serializers/glTF/2.0/glTFSerializer';
16
-
17
- export class GltfExportManager {
18
- protected static readonly _METADATA_PROPS = {
19
- exportNode: 'exportNode',
20
- deleteAfterExport: 'deleteAfterExport',
21
- exchangeMaterialWith: 'exchangeMaterialWith',
22
- };
23
- protected static readonly _EXPORT_ROOT_NAME = '__export_root__';
24
-
25
- protected _maxTextureSize: number;
26
-
27
- protected constructor(protected viewer: Viewer) {
28
- // store initial max texture size, so that we can restore it in the post processing
29
- this._maxTextureSize = viewer.engine.getCaps().maxTextureSize;
30
- }
31
-
32
- public static async create(viewer: Viewer): Promise<GltfExportManager> {
33
- return new GltfExportManager(viewer);
34
- }
35
-
36
- /**
37
- * Exports selected nodes to a file.
38
- * @param filename Optional name of the exported .GLB file.
39
- * @param optimizeForAR Adjusts the exported GLB so that known issues get automatically fixed, this
40
- * is mostly targeting Apples .usdz format.
41
- * @param excluded Optional list of geometry (meshes, elements, variants,...) to be excluded from the export.
42
- */
43
- public async exportGlb(filename = 'glb-export.glb', optimizeForAR: boolean = false, excluded?: ExcludedGeometryList) {
44
- await this._exportPreProcess(optimizeForAR, excluded);
45
-
46
- const glbData = await GLTF2Export.GLBAsync(
47
- this.viewer.scene,
48
- 'dummy',
49
- GltfExportManager._gltfExportOptions(optimizeForAR, excluded)
50
- );
51
-
52
- await this._exportPostProcess(optimizeForAR);
53
-
54
- const resBlob = glbData.glTFFiles['dummy.glb'];
55
- // check if result is valid, according to the typings this could also be a string
56
- if (resBlob instanceof Blob) {
57
- if (!filename.endsWith('.glb')) {
58
- filename += '.glb';
59
- }
60
- return new File([resBlob], filename);
61
- } else {
62
- // result was not a valid blob
63
- return undefined;
64
- }
65
- }
66
-
67
- /**
68
- * Exports selected nodes to GLTF. This may result in more than one file, since textures are exported seperately.
69
- * @param filename Name of the main (text-based) .GLTF file referring to separate texture files.
70
- * @param optimizeForAR Adjusts the exported GLB so that known issues get automatically fixed, this
71
- * is mostly targeting Apples .usdz format.
72
- * @param excluded Optional list of geometry (meshes, elements, variants,...) to be excluded from the export.
73
- */
74
- public async exportGltfToFile(filename: string, optimizeForAR: boolean = false, excluded?: ExcludedGeometryList) {
75
- await this._exportPreProcess(optimizeForAR, excluded);
76
-
77
- const gltf = await GLTF2Export.GLTFAsync(
78
- this.viewer.scene,
79
- filename,
80
- GltfExportManager._gltfExportOptions(optimizeForAR, excluded)
81
- );
82
- gltf.downloadFiles();
83
-
84
- this._exportPostProcess(optimizeForAR);
85
- }
86
-
87
- /**
88
- * Exports selected nodes to GLB. This results in one binary file.
89
- * @param filename Name of the main (text-based) .GLTF file referring to seperate texture files.
90
- * @param optimizeForAR Adjusts the exported GLB so that known issues get automatically fixed, this
91
- * is mostly targeting Apples .usdz format.
92
- * @param excluded Optional list of geometry (meshes, elements, variants,...) to be excluded from the export.
93
- */
94
- public async exportGlbToFile(filename: string, optimizeForAR: boolean = false, excluded?: ExcludedGeometryList) {
95
- await this._exportPreProcess(optimizeForAR, excluded);
96
-
97
- const glb = await GLTF2Export.GLBAsync(
98
- this.viewer.scene,
99
- filename,
100
- GltfExportManager._gltfExportOptions(optimizeForAR, excluded)
101
- );
102
- glb.downloadFiles();
103
-
104
- await this._exportPostProcess(optimizeForAR);
105
- }
106
-
107
- /**
108
- * Prepares scene for GLB export.
109
- * This is very important for AR exports, since we have to do a lot of conversions to satisfy Apples .usdz format.
110
- */
111
- protected async _exportPreProcess(optimizeForAR: boolean, excluded?: ExcludedGeometryList): Promise<void> {
112
- if (!optimizeForAR) {
113
- // actually nothing to do if AR optimization is not required
114
- return;
115
- }
116
-
117
- // pause rendering, since we are altering the active scene, which should not be visible to the user
118
- this.viewer.pauseRendering();
119
-
120
- // exchange materials if required
121
- // the AR export should never contain textures larger than 1024 px:
122
- // - iOS devices will crash most likely when trying to access AR endpoints with such files
123
- // - file size will be reduced, which leads to faster loading times
124
- // - textures > 1024 px shouldn't make a difference on mobiles anyway
125
- // we don't have to rescale anything if are already on a downscaled device, since the textures are already <= 1024
126
- // also we have to be very cautios with copying textures on these devices, since we are potentially very limited
127
- // with the available memory
128
- const isScaledDownDevice = getIsScaledDownDevice();
129
- if (!isScaledDownDevice) {
130
- // the idea is to re-create all textures with a smaller texture size
131
- // we have to exchange all materials for this to work
132
- this.viewer.engine.clearInternalTexturesCache();
133
- this.viewer.engine.getCaps().maxTextureSize = 1024;
134
-
135
- this.viewer.scene.materials
136
- .filter(material => material instanceof PBRMaterial)
137
- .forEach(material => GltfExportManager._exchangeMaterial(material as PBRMaterial));
138
- }
139
-
140
- // since Babylon.js v6 the conversion to right handed GLB coordinate system is done differently
141
- // previously the geometry itself has been altered, now they negate the scaling of all root nodes
142
- // this is an issue for us, since we will receive this negated scalings in the exported GLB, which leads to issues
143
- // on iOS devices
144
- // we fix that by adding a top level root node with a negative scaling as well
145
- // the exporter just removes this node as he detects a "noop root node" (implementation details of Babylon.js)
146
- // everything beneath this node remains untouched
147
- // TODO BJS Update: Test AR export on iPhones as well and double check if we still need this special logic
148
- const exportRootNode = new TransformNode(GltfExportManager._EXPORT_ROOT_NAME, this.viewer.scene);
149
- exportRootNode.scaling = new Vector3(-1, 1, 1);
150
- injectMetadata(exportRootNode!, {
151
- [GltfExportManager._METADATA_PROPS.exportNode]: true,
152
- [GltfExportManager._METADATA_PROPS.deleteAfterExport]: true,
153
- });
154
-
155
- // create clones of each node (recursively), optionally exchange with cloned materials and mark these nodes for the
156
- // export
157
- this.viewer.scene.rootNodes
158
- .filter(rootNode => rootNode.name !== GltfExportManager._EXPORT_ROOT_NAME)
159
- .forEach(rootNode => this._prepareNodeForExport(rootNode, exportRootNode, excluded));
160
-
161
- // bake transformation of all meshes, so that no negative scalings are left
162
- // it's important that this is done AFTER instanced meshes have been converted (_prepareNodeForExport)
163
- this._getNodesMarkedForExport(true).forEach(mesh => bakeGeometryOfMesh(mesh as Mesh));
164
-
165
- // reset transformation of all "TransformNodes", which couldn't be handled by the geometry baking algorithm
166
- // it's important that this is done AFTER all geometries have been baked
167
- this._getNodesMarkedForExport().forEach(node => resetTransformation(node as TransformNode));
168
- }
169
-
170
- /**
171
- * Cleans up scene after export
172
- */
173
- protected async _exportPostProcess(optimizeForAR: boolean): Promise<void> {
174
- if (!optimizeForAR) {
175
- // nothing to do, since no changes have been made without AR optimizations
176
- return;
177
- }
178
-
179
- // dispose all nodes, materials and textures that have only been created for the export
180
- this.viewer.scene.rootNodes
181
- .filter(rootNode => rootNode.metadata?.[GltfExportManager._METADATA_PROPS.deleteAfterExport])
182
- .forEach(rootNode => rootNode.dispose());
183
-
184
- this.viewer.scene.materials
185
- .filter(mat => !!mat.metadata?.[GltfExportManager._METADATA_PROPS.deleteAfterExport])
186
- .forEach(material => material.dispose(false, false));
187
-
188
- this.viewer.scene.textures
189
- .filter(texture => !!texture.metadata?.[GltfExportManager._METADATA_PROPS.deleteAfterExport])
190
- .forEach(texture => texture.dispose());
191
-
192
- // reset engines max texture size and resume rendering
193
- this.viewer.engine.getCaps().maxTextureSize = this._maxTextureSize;
194
- this.viewer.resumeRendering();
195
- }
196
-
197
- /**
198
- * Creates a clone of the node which should be used for the export.
199
- * Also switches to the cloned material if required.
200
- */
201
- protected _prepareNodeForExport(node: BjsNode, clonedParent: TransformNode, excluded?: ExcludedGeometryList) {
202
- if (!GltfExportManager._shouldExportNode(node, excluded)) {
203
- return;
204
- }
205
-
206
- // from here on we only have TransformNodes
207
- const transformNode = node as TransformNode;
208
-
209
- // clone original node and create unique name (via uniqueId) for the export
210
- const clonedNodeName = `${transformNode.name}_${transformNode.uniqueId}`;
211
- const clonedNode =
212
- transformNode instanceof InstancedMesh
213
- ? createMeshFromInstancedMesh(transformNode, clonedNodeName, clonedParent)
214
- : transformNode.clone(clonedNodeName, clonedParent, true)!;
215
-
216
- // exchange material
217
- if (clonedNode instanceof Mesh) {
218
- const exchangeWithMaterial =
219
- clonedNode.material?.metadata?.[GltfExportManager._METADATA_PROPS.exchangeMaterialWith];
220
- if (exchangeWithMaterial) {
221
- clonedNode.material = this.viewer.scene.getMaterialByUniqueID(exchangeWithMaterial);
222
- }
223
- }
224
-
225
- // signalize that this is a cloned node
226
- injectMetadata(clonedNode, {
227
- [GltfExportManager._METADATA_PROPS.exportNode]: true,
228
- [GltfExportManager._METADATA_PROPS.deleteAfterExport]: true,
229
- });
230
-
231
- // handle children
232
- const childs = transformNode.getChildTransformNodes(true);
233
- childs.forEach(child => this._prepareNodeForExport(child, clonedNode, excluded));
234
- }
235
-
236
- /**
237
- * Help function for receiving all nodes that are marked for the export
238
- */
239
- protected _getNodesMarkedForExport(meshesOnly?: boolean) {
240
- const nodes: TransformNode[] = [...this.viewer.scene.meshes];
241
- if (!meshesOnly) {
242
- nodes.push(...this.viewer.scene.transformNodes);
243
- }
244
-
245
- const filteredNodes = nodes.filter(
246
- node =>
247
- !!node.metadata?.[GltfExportManager._METADATA_PROPS.exportNode] &&
248
- // in the desired use cases we want to exclude the export root node
249
- // maybe add a parameter if we have to include it in certain scenarios in the future
250
- node.name !== GltfExportManager._EXPORT_ROOT_NAME
251
- );
252
-
253
- return filteredNodes;
254
- }
255
-
256
- /**
257
- * Defines options for the export.
258
- * We don't allow the user to overwrite certain settings, since we rely on properties like `removeNoopRootNodes` to
259
- * stay `true` in order to make the AR export work.
260
- * We could theoretically allow it if AR optimization is not desired, but this may confuse the user.
261
- */
262
- protected static _gltfExportOptions(optimizeForAR: boolean, excluded?: ExcludedGeometryList): IExportOptions {
263
- return {
264
- shouldExportNode: function (node: BjsNode) {
265
- if (optimizeForAR) {
266
- // we explicitely marked nodes, that should be exported in AR mode
267
- return !!node.metadata?.[GltfExportManager._METADATA_PROPS.exportNode];
268
- } else {
269
- // use the default export node check (enabled state, exclusion list, etc...)
270
- return GltfExportManager._shouldExportNode(node, excluded);
271
- }
272
- },
273
- };
274
- }
275
-
276
- /**
277
- * Checks if a node should be available in the export
278
- */
279
- protected static _shouldExportNode(node: BjsNode, excluded?: ExcludedGeometryList): boolean {
280
- if (!(node instanceof TransformNode)) {
281
- return false;
282
- }
283
- // TODO: think of adding "BackgroundHelper" and nodes with "infiniteDistance" here as well, at least in AR mode
284
- if (!node.isEnabled()) {
285
- return false;
286
- }
287
- if (excluded && isNodeIncludedInExclusionList(node, excluded)) {
288
- return false;
289
- }
290
-
291
- return true;
292
- }
293
-
294
- /**
295
- * Creates a clone of the material which should be used for the export.
296
- * This is mostly required for recreating textures with lower sizes.
297
- * CAUTION: Material exchanging is not supported for materials that contain certain texture types:
298
- * - Dynamic textures (Paintables): Cloning dynamic textures doesn't clone the canvas context
299
- * => so the clone is just empty
300
- * - Render target textures: Disposing the clone will leave the scene in a "not ready" state
301
- * => this scenario is not fully analyzed yet, but it's not really worth the effort right now, since this kind of
302
- * of texture is not really used ATM
303
- */
304
- protected static _exchangeMaterial(material: Material) {
305
- const baseTextures = material.getActiveTextures();
306
- const hasDynamicTextures = baseTextures.some(texture => texture instanceof DynamicTexture);
307
- const hasRenderTargetTextures = baseTextures.some(texture => texture instanceof RenderTargetTexture);
308
- if (hasDynamicTextures || hasRenderTargetTextures) {
309
- const textureTypesString = [
310
- hasDynamicTextures ? 'Dynamic Textures' : '',
311
- hasRenderTargetTextures ? 'Render Target Textures' : '',
312
- ]
313
- .filter(Boolean)
314
- .join();
315
- console.warn(
316
- `Couldn't exchange material "${material.name}" in GLB export, as it contains unsupported texture type(s) (${textureTypesString}). The export will still work, but the textures of this material will keep their original size.`
317
- );
318
-
319
- return;
320
- }
321
-
322
- const newName = `${material.name}_clone`;
323
- const clonedMaterial = material.clone(newName)!;
324
- const clonedTextures = clonedMaterial.getActiveTextures();
325
-
326
- // mark all exported textures, so that they will be deleted after the export
327
- clonedTextures.forEach(texture =>
328
- injectMetadata(texture, { [GltfExportManager._METADATA_PROPS.deleteAfterExport]: true })
329
- );
330
-
331
- injectMetadata(material, { [GltfExportManager._METADATA_PROPS.exchangeMaterialWith]: clonedMaterial.uniqueId });
332
- injectMetadata(clonedMaterial, { [GltfExportManager._METADATA_PROPS.deleteAfterExport]: true });
333
- }
334
- }
1
+ import { Viewer } from '../classes/viewer';
2
+ import { injectMetadata } from '../util/babylonHelper';
3
+ import { getIsScaledDownDevice } from '../util/deviceHelper';
4
+ import { bakeGeometryOfMesh, createMeshFromInstancedMesh, resetTransformation } from '../util/geometryHelper';
5
+ import { isNodeIncludedInExclusionList } from '../util/structureHelper';
6
+ import type { AnimationGroup } from '@babylonjs/core/Animations/animationGroup';
7
+ import { PBRMaterial } from '@babylonjs/core/Materials/PBR/pbrMaterial';
8
+ import { DynamicTexture } from '@babylonjs/core/Materials/Textures/dynamicTexture';
9
+ import { RenderTargetTexture } from '@babylonjs/core/Materials/Textures/renderTargetTexture';
10
+ import { Vector3 } from '@babylonjs/core/Maths/math.vector';
11
+ import { InstancedMesh } from '@babylonjs/core/Meshes/instancedMesh';
12
+ import { Mesh } from '@babylonjs/core/Meshes/mesh';
13
+ import { TransformNode } from '@babylonjs/core/Meshes/transformNode';
14
+ import { Node as BjsNode } from '@babylonjs/core/node';
15
+ import '@babylonjs/serializers/glTF/2.0/Extensions/KHR_texture_transform';
16
+ import { GLTF2Export } from '@babylonjs/serializers/glTF/2.0/glTFSerializer';
17
+
18
+ export class GltfExportManager {
19
+ protected static readonly _METADATA_PROPS = {
20
+ exportNode: 'exportNode',
21
+ deleteAfterExport: 'deleteAfterExport',
22
+ exchangeMaterialWith: 'exchangeMaterialWith',
23
+ };
24
+ protected static readonly _EXPORT_ROOT_NAME = '__export_root__';
25
+
26
+ protected _maxTextureSize: number;
27
+
28
+ /**
29
+ * Animation groups which have been removed from the scene before export for AR and which need to be re-added
30
+ * afterwards.
31
+ *
32
+ * AR does not work on Android (i.e. the modelviewer) when animation groups are present in the scene.
33
+ * See CB-10055 for more details.
34
+ */
35
+ protected _animationGroupsToRestore: AnimationGroup[] = [];
36
+
37
+ protected constructor(protected viewer: Viewer) {
38
+ // store initial max texture size, so that we can restore it in the post processing
39
+ this._maxTextureSize = viewer.engine.getCaps().maxTextureSize;
40
+ }
41
+
42
+ public static async create(viewer: Viewer): Promise<GltfExportManager> {
43
+ return new GltfExportManager(viewer);
44
+ }
45
+
46
+ /**
47
+ * Exports selected nodes to a file.
48
+ * @param filename Optional name of the exported .GLB file.
49
+ * @param optimizeForAR Adjusts the exported GLB so that known issues get automatically fixed, this
50
+ * is mostly targeting Apples .usdz format.
51
+ * @param excluded Optional list of geometry (meshes, elements, variants,...) to be excluded from the export.
52
+ */
53
+ public async exportGlb(filename = 'glb-export.glb', optimizeForAR: boolean = false, excluded?: ExcludedGeometryList) {
54
+ await this._exportPreProcess(optimizeForAR, excluded);
55
+
56
+ const glbData = await GLTF2Export.GLBAsync(
57
+ this.viewer.scene,
58
+ 'dummy',
59
+ GltfExportManager._gltfExportOptions(optimizeForAR, excluded)
60
+ );
61
+
62
+ await this._exportPostProcess(optimizeForAR);
63
+
64
+ const resBlob = glbData.glTFFiles['dummy.glb'];
65
+ // check if result is valid, according to the typings this could also be a string
66
+ if (resBlob instanceof Blob) {
67
+ if (!filename.endsWith('.glb')) {
68
+ filename += '.glb';
69
+ }
70
+ return new File([resBlob], filename);
71
+ } else {
72
+ // result was not a valid blob
73
+ return undefined;
74
+ }
75
+ }
76
+
77
+ /**
78
+ * Exports selected nodes to GLTF. This may result in more than one file, since textures are exported seperately.
79
+ * @param filename Name of the main (text-based) .GLTF file referring to separate texture files.
80
+ * @param optimizeForAR Adjusts the exported GLB so that known issues get automatically fixed, this
81
+ * is mostly targeting Apples .usdz format.
82
+ * @param excluded Optional list of geometry (meshes, elements, variants,...) to be excluded from the export.
83
+ */
84
+ public async exportGltfToFile(filename: string, optimizeForAR: boolean = false, excluded?: ExcludedGeometryList) {
85
+ await this._exportPreProcess(optimizeForAR, excluded);
86
+
87
+ const gltf = await GLTF2Export.GLTFAsync(
88
+ this.viewer.scene,
89
+ filename,
90
+ GltfExportManager._gltfExportOptions(optimizeForAR, excluded)
91
+ );
92
+ gltf.downloadFiles();
93
+
94
+ this._exportPostProcess(optimizeForAR);
95
+ }
96
+
97
+ /**
98
+ * Exports selected nodes to GLB. This results in one binary file.
99
+ * @param filename Name of the main (text-based) .GLTF file referring to seperate texture files.
100
+ * @param optimizeForAR Adjusts the exported GLB so that known issues get automatically fixed, this
101
+ * is mostly targeting Apples .usdz format.
102
+ * @param excluded Optional list of geometry (meshes, elements, variants,...) to be excluded from the export.
103
+ */
104
+ public async exportGlbToFile(filename: string, optimizeForAR: boolean = false, excluded?: ExcludedGeometryList) {
105
+ await this._exportPreProcess(optimizeForAR, excluded);
106
+
107
+ const glb = await GLTF2Export.GLBAsync(
108
+ this.viewer.scene,
109
+ filename,
110
+ GltfExportManager._gltfExportOptions(optimizeForAR, excluded)
111
+ );
112
+ glb.downloadFiles();
113
+
114
+ await this._exportPostProcess(optimizeForAR);
115
+ }
116
+
117
+ /**
118
+ * Prepares scene for GLB export.
119
+ * This is very important for AR exports, since we have to do a lot of conversions to satisfy Apples .usdz format.
120
+ */
121
+ protected async _exportPreProcess(optimizeForAR: boolean, excluded?: ExcludedGeometryList): Promise<void> {
122
+ if (!optimizeForAR) {
123
+ // actually nothing to do if AR optimization is not required
124
+ return;
125
+ }
126
+
127
+ // pause rendering, since we are altering the active scene, which should not be visible to the user
128
+ this.viewer.pauseRendering();
129
+
130
+ this._animationGroupsToRestore = [...this.viewer.scene.animationGroups];
131
+ this._animationGroupsToRestore.forEach(x => this.viewer.scene.removeAnimationGroup(x));
132
+
133
+ // exchange materials if required
134
+ // the AR export should never contain textures larger than 1024 px:
135
+ // - iOS devices will crash most likely when trying to access AR endpoints with such files
136
+ // - file size will be reduced, which leads to faster loading times
137
+ // - textures > 1024 px shouldn't make a difference on mobiles anyway
138
+ // we don't have to rescale anything if are already on a downscaled device, since the textures are already <= 1024
139
+ // also we have to be very cautios with copying textures on these devices, since we are potentially very limited
140
+ // with the available memory
141
+ const isScaledDownDevice = getIsScaledDownDevice();
142
+ if (!isScaledDownDevice) {
143
+ // the idea is to re-create all textures with a smaller texture size
144
+ // we have to exchange all materials for this to work
145
+ this.viewer.engine.clearInternalTexturesCache();
146
+ this.viewer.engine.getCaps().maxTextureSize = 1024;
147
+
148
+ this.viewer.scene.materials
149
+ .filter(material => material instanceof PBRMaterial)
150
+ .forEach(material => GltfExportManager._exchangeMaterial(material as PBRMaterial));
151
+ }
152
+
153
+ // since Babylon.js v6 the conversion to right handed GLB coordinate system is done differently
154
+ // previously the geometry itself has been altered, now they negate the scaling of all root nodes
155
+ // this is an issue for us, since we will receive this negated scalings in the exported GLB, which leads to issues
156
+ // on iOS devices
157
+ // we fix that by adding a top level root node with a negative scaling as well
158
+ // the exporter just removes this node as he detects a "noop root node" (implementation details of Babylon.js)
159
+ // everything beneath this node remains untouched
160
+ // TODO BJS Update: Test AR export on iPhones as well and double check if we still need this special logic
161
+ const exportRootNode = new TransformNode(GltfExportManager._EXPORT_ROOT_NAME, this.viewer.scene);
162
+ exportRootNode.scaling = new Vector3(-1, 1, 1);
163
+ injectMetadata(exportRootNode!, {
164
+ [GltfExportManager._METADATA_PROPS.exportNode]: true,
165
+ [GltfExportManager._METADATA_PROPS.deleteAfterExport]: true,
166
+ });
167
+
168
+ // create clones of each node (recursively), optionally exchange with cloned materials and mark these nodes for the
169
+ // export
170
+ this.viewer.scene.rootNodes
171
+ .filter(rootNode => rootNode.name !== GltfExportManager._EXPORT_ROOT_NAME)
172
+ .forEach(rootNode => this._prepareNodeForExport(rootNode, exportRootNode, excluded));
173
+
174
+ // bake transformation of all meshes, so that no negative scalings are left
175
+ // it's important that this is done AFTER instanced meshes have been converted (_prepareNodeForExport)
176
+ this._getNodesMarkedForExport(true).forEach(mesh => bakeGeometryOfMesh(mesh as Mesh));
177
+
178
+ // reset transformation of all "TransformNodes", which couldn't be handled by the geometry baking algorithm
179
+ // it's important that this is done AFTER all geometries have been baked
180
+ this._getNodesMarkedForExport().forEach(node => resetTransformation(node as TransformNode));
181
+ }
182
+
183
+ /**
184
+ * Cleans up scene after export
185
+ */
186
+ protected async _exportPostProcess(optimizeForAR: boolean): Promise<void> {
187
+ if (!optimizeForAR) {
188
+ // nothing to do, since no changes have been made without AR optimizations
189
+ return;
190
+ }
191
+
192
+ this._animationGroupsToRestore.forEach(x => this.viewer.scene.addAnimationGroup(x));
193
+ this._animationGroupsToRestore = [];
194
+
195
+ // dispose all nodes, materials and textures that have only been created for the export
196
+ this.viewer.scene.rootNodes
197
+ .filter(rootNode => rootNode.metadata?.[GltfExportManager._METADATA_PROPS.deleteAfterExport])
198
+ .forEach(rootNode => rootNode.dispose());
199
+
200
+ this.viewer.scene.materials
201
+ .filter(mat => !!mat.metadata?.[GltfExportManager._METADATA_PROPS.deleteAfterExport])
202
+ .forEach(material => material.dispose(false, false));
203
+
204
+ this.viewer.scene.textures
205
+ .filter(texture => !!texture.metadata?.[GltfExportManager._METADATA_PROPS.deleteAfterExport])
206
+ .forEach(texture => texture.dispose());
207
+
208
+ // reset engines max texture size and resume rendering
209
+ this.viewer.engine.getCaps().maxTextureSize = this._maxTextureSize;
210
+ this.viewer.resumeRendering();
211
+ }
212
+
213
+ /**
214
+ * Creates a clone of the node which should be used for the export.
215
+ * Also switches to the cloned material if required.
216
+ */
217
+ protected _prepareNodeForExport(node: BjsNode, clonedParent: TransformNode, excluded?: ExcludedGeometryList) {
218
+ if (!GltfExportManager._shouldExportNode(node, excluded)) {
219
+ return;
220
+ }
221
+
222
+ // from here on we only have TransformNodes
223
+ const transformNode = node as TransformNode;
224
+
225
+ // clone original node and create unique name (via uniqueId) for the export
226
+ const clonedNodeName = `${transformNode.name}_${transformNode.uniqueId}`;
227
+ const clonedNode =
228
+ transformNode instanceof InstancedMesh
229
+ ? createMeshFromInstancedMesh(transformNode, clonedNodeName, clonedParent)
230
+ : transformNode.clone(clonedNodeName, clonedParent, true)!;
231
+
232
+ // exchange material
233
+ if (clonedNode instanceof Mesh) {
234
+ const exchangeWithMaterial =
235
+ clonedNode.material?.metadata?.[GltfExportManager._METADATA_PROPS.exchangeMaterialWith];
236
+ if (exchangeWithMaterial) {
237
+ clonedNode.material = this.viewer.scene.getMaterialByUniqueID(exchangeWithMaterial);
238
+ }
239
+ }
240
+
241
+ // signalize that this is a cloned node
242
+ injectMetadata(clonedNode, {
243
+ [GltfExportManager._METADATA_PROPS.exportNode]: true,
244
+ [GltfExportManager._METADATA_PROPS.deleteAfterExport]: true,
245
+ });
246
+
247
+ // handle children
248
+ const childs = transformNode.getChildTransformNodes(true);
249
+ childs.forEach(child => this._prepareNodeForExport(child, clonedNode, excluded));
250
+ }
251
+
252
+ /**
253
+ * Help function for receiving all nodes that are marked for the export
254
+ */
255
+ protected _getNodesMarkedForExport(meshesOnly?: boolean) {
256
+ const nodes: TransformNode[] = [...this.viewer.scene.meshes];
257
+ if (!meshesOnly) {
258
+ nodes.push(...this.viewer.scene.transformNodes);
259
+ }
260
+
261
+ const filteredNodes = nodes.filter(
262
+ node =>
263
+ !!node.metadata?.[GltfExportManager._METADATA_PROPS.exportNode] &&
264
+ // in the desired use cases we want to exclude the export root node
265
+ // maybe add a parameter if we have to include it in certain scenarios in the future
266
+ node.name !== GltfExportManager._EXPORT_ROOT_NAME
267
+ );
268
+
269
+ return filteredNodes;
270
+ }
271
+
272
+ /**
273
+ * Defines options for the export.
274
+ * We don't allow the user to overwrite certain settings, since we rely on properties like `removeNoopRootNodes` to
275
+ * stay `true` in order to make the AR export work.
276
+ * We could theoretically allow it if AR optimization is not desired, but this may confuse the user.
277
+ */
278
+ protected static _gltfExportOptions(optimizeForAR: boolean, excluded?: ExcludedGeometryList): IExportOptions {
279
+ return {
280
+ shouldExportNode: function (node: BjsNode) {
281
+ if (optimizeForAR) {
282
+ // we explicitely marked nodes, that should be exported in AR mode
283
+ return !!node.metadata?.[GltfExportManager._METADATA_PROPS.exportNode];
284
+ } else {
285
+ // use the default export node check (enabled state, exclusion list, etc...)
286
+ return GltfExportManager._shouldExportNode(node, excluded);
287
+ }
288
+ },
289
+ };
290
+ }
291
+
292
+ /**
293
+ * Checks if a node should be available in the export
294
+ */
295
+ protected static _shouldExportNode(node: BjsNode, excluded?: ExcludedGeometryList): boolean {
296
+ if (!(node instanceof TransformNode)) {
297
+ return false;
298
+ }
299
+ // TODO: think of adding "BackgroundHelper" and nodes with "infiniteDistance" here as well, at least in AR mode
300
+ if (!node.isEnabled()) {
301
+ return false;
302
+ }
303
+ if (excluded && isNodeIncludedInExclusionList(node, excluded)) {
304
+ return false;
305
+ }
306
+
307
+ return true;
308
+ }
309
+
310
+ /**
311
+ * Creates a clone of the material which should be used for the export.
312
+ * This is mostly required for recreating textures with lower sizes.
313
+ * CAUTION: Material exchanging is not supported for materials that contain certain texture types:
314
+ * - Dynamic textures (Paintables): Cloning dynamic textures doesn't clone the canvas context
315
+ * => so the clone is just empty
316
+ * - Render target textures: Disposing the clone will leave the scene in a "not ready" state
317
+ * => this scenario is not fully analyzed yet, but it's not really worth the effort right now, since this kind of
318
+ * of texture is not really used ATM
319
+ */
320
+ protected static _exchangeMaterial(material: Material) {
321
+ const baseTextures = material.getActiveTextures();
322
+ const hasDynamicTextures = baseTextures.some(texture => texture instanceof DynamicTexture);
323
+ const hasRenderTargetTextures = baseTextures.some(texture => texture instanceof RenderTargetTexture);
324
+ if (hasDynamicTextures || hasRenderTargetTextures) {
325
+ const textureTypesString = [
326
+ hasDynamicTextures ? 'Dynamic Textures' : '',
327
+ hasRenderTargetTextures ? 'Render Target Textures' : '',
328
+ ]
329
+ .filter(Boolean)
330
+ .join();
331
+ console.warn(
332
+ `Couldn't exchange material "${material.name}" in GLB export, as it contains unsupported texture type(s) (${textureTypesString}). The export will still work, but the textures of this material will keep their original size.`
333
+ );
334
+
335
+ return;
336
+ }
337
+
338
+ const newName = `${material.name}_clone`;
339
+ const clonedMaterial = material.clone(newName)!;
340
+ const clonedTextures = clonedMaterial.getActiveTextures();
341
+
342
+ // mark all exported textures, so that they will be deleted after the export
343
+ clonedTextures.forEach(texture =>
344
+ injectMetadata(texture, { [GltfExportManager._METADATA_PROPS.deleteAfterExport]: true })
345
+ );
346
+
347
+ injectMetadata(material, { [GltfExportManager._METADATA_PROPS.exchangeMaterialWith]: clonedMaterial.uniqueId });
348
+ injectMetadata(clonedMaterial, { [GltfExportManager._METADATA_PROPS.deleteAfterExport]: true });
349
+ }
350
+ }