@combeenation/3d-viewer 12.0.0-alpha4 → 12.0.0-beta2
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/dist/lib-cjs/api/classes/element.d.ts +1 -1
- package/dist/lib-cjs/api/classes/element.js.map +1 -1
- package/dist/lib-cjs/api/classes/viewer.js +18 -2
- package/dist/lib-cjs/api/classes/viewer.js.map +1 -1
- package/dist/lib-cjs/api/internal/sceneSetup.js +5 -2
- package/dist/lib-cjs/api/internal/sceneSetup.js.map +1 -1
- package/dist/lib-cjs/api/manager/gltfExportManager.d.ts +58 -11
- package/dist/lib-cjs/api/manager/gltfExportManager.js +162 -70
- package/dist/lib-cjs/api/manager/gltfExportManager.js.map +1 -1
- package/dist/lib-cjs/api/util/babylonHelper.d.ts +2 -2
- package/dist/lib-cjs/api/util/babylonHelper.js.map +1 -1
- package/dist/lib-cjs/api/util/deviceHelper.d.ts +9 -0
- package/dist/lib-cjs/api/util/deviceHelper.js +29 -0
- package/dist/lib-cjs/api/util/deviceHelper.js.map +1 -0
- package/dist/lib-cjs/api/util/geometryHelper.d.ts +10 -0
- package/dist/lib-cjs/api/util/geometryHelper.js +17 -7
- package/dist/lib-cjs/api/util/geometryHelper.js.map +1 -1
- package/dist/lib-cjs/api/util/globalTypes.d.ts +13 -19
- package/dist/lib-cjs/api/util/structureHelper.d.ts +1 -1
- package/dist/lib-cjs/api/util/structureHelper.js +2 -2
- package/dist/lib-cjs/api/util/structureHelper.js.map +1 -1
- package/dist/lib-cjs/buildinfo.json +1 -1
- package/dist/lib-cjs/commonjs.tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -1
- package/src/api/classes/element.ts +1 -1
- package/src/api/classes/viewer.ts +20 -1
- package/src/api/internal/sceneSetup.ts +7 -2
- package/src/api/manager/gltfExportManager.ts +189 -77
- package/src/api/util/babylonHelper.ts +4 -4
- package/src/api/util/deviceHelper.ts +31 -0
- package/src/api/util/geometryHelper.ts +19 -9
- package/src/api/util/globalTypes.ts +14 -20
- package/src/api/util/structureHelper.ts +6 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@combeenation/3d-viewer",
|
|
3
|
-
"version": "12.0.0-
|
|
3
|
+
"version": "12.0.0-beta2",
|
|
4
4
|
"description": "Combeenation 3D Viewer",
|
|
5
5
|
"homepage": "https://github.com/Combeenation/3d-viewer#readme",
|
|
6
6
|
"bugs": {
|
|
@@ -51,6 +51,7 @@
|
|
|
51
51
|
"@babylonjs/loaders": "6.27.1",
|
|
52
52
|
"@babylonjs/materials": "6.27.1",
|
|
53
53
|
"@babylonjs/node-editor": "6.27.1",
|
|
54
|
+
"@babylonjs/node-geometry-editor": "6.27.1",
|
|
54
55
|
"@babylonjs/serializers": "6.27.1",
|
|
55
56
|
"eventemitter3": "4.0.7",
|
|
56
57
|
"gsap": "3.11.2",
|
|
@@ -304,7 +304,7 @@ export class Element extends VariantParameterizable {
|
|
|
304
304
|
/**
|
|
305
305
|
* Draws a `ImageBitmap` or `OffscreenCanvas` onto a `paintable` defined via {@link PaintableDefinition}.
|
|
306
306
|
*/
|
|
307
|
-
public drawPaintable(paintable: string, imageSource:
|
|
307
|
+
public drawPaintable(paintable: string, imageSource: HTMLImageElement): Element {
|
|
308
308
|
// node and material checks and preperation
|
|
309
309
|
const node = this.getPaintableNode(paintable);
|
|
310
310
|
if (!(node instanceof AbstractMesh)) {
|
|
@@ -7,6 +7,7 @@ import { TagManager } from '../manager/tagManager';
|
|
|
7
7
|
import { VariantInstanceManager } from '../manager/variantInstanceManager';
|
|
8
8
|
import { SpecStorage } from '../store/specStorage';
|
|
9
9
|
import { backgroundDomeName, envHelperMetadataName } from '../util/babylonHelper';
|
|
10
|
+
import { getIsScaledDownDevice } from '../util/deviceHelper';
|
|
10
11
|
import { debounce, loadJson } from '../util/resourceHelper';
|
|
11
12
|
import { getCustomCbnBabylonLoaderPlugin } from '../util/sceneLoaderHelper';
|
|
12
13
|
import { replaceDots } from '../util/stringHelper';
|
|
@@ -97,6 +98,15 @@ export class Viewer extends EventBroadcaster {
|
|
|
97
98
|
stencil: true,
|
|
98
99
|
xrCompatible: false,
|
|
99
100
|
},
|
|
101
|
+
// important for visual quality on iOS devices, this setting will set the hardware scaling proportional to the
|
|
102
|
+
// devices pixel ratio
|
|
103
|
+
adaptToDeviceRatio: true,
|
|
104
|
+
// limit texture size on iOS devices by default, in this way models (and AR exports) tend to work smoothly
|
|
105
|
+
// 1024 px should still be fine on mobiles
|
|
106
|
+
limitTextureSize: {
|
|
107
|
+
devices: 'iPhone',
|
|
108
|
+
size: 1024,
|
|
109
|
+
},
|
|
100
110
|
},
|
|
101
111
|
scene: {
|
|
102
112
|
globals: {},
|
|
@@ -359,6 +369,7 @@ export class Viewer extends EventBroadcaster {
|
|
|
359
369
|
// load additional packages for certain inspector features like "node material editor"
|
|
360
370
|
// this is done after showing the debug layer to save time
|
|
361
371
|
await import(/* webpackChunkName: "node-material-editor" */ '@babylonjs/node-editor');
|
|
372
|
+
await import(/* webpackChunkName: "node-geometry-editor" */ '@babylonjs/node-geometry-editor');
|
|
362
373
|
|
|
363
374
|
return true;
|
|
364
375
|
} else {
|
|
@@ -715,8 +726,16 @@ The inspector can only be used in development builds.`);
|
|
|
715
726
|
const engine = new Engine(
|
|
716
727
|
this.canvas as HTMLCanvasElement,
|
|
717
728
|
sceneJson.engine?.antialiasing ?? false,
|
|
718
|
-
sceneJson.engine?.options
|
|
729
|
+
sceneJson.engine?.options,
|
|
730
|
+
sceneJson.engine?.adaptToDeviceRatio ?? false
|
|
719
731
|
);
|
|
732
|
+
|
|
733
|
+
const isScaledDownDevice = getIsScaledDownDevice();
|
|
734
|
+
if (isScaledDownDevice) {
|
|
735
|
+
// reduce texture size if required
|
|
736
|
+
engine.getCaps().maxTextureSize = (sceneJson.engine!.limitTextureSize as LimitTextureSizeConfig).size;
|
|
737
|
+
}
|
|
738
|
+
|
|
720
739
|
const scene = await sceneSetup(engine, sceneJson);
|
|
721
740
|
if (sceneJson.meshPicking) {
|
|
722
741
|
new HighlightLayer('default', scene);
|
|
@@ -60,10 +60,15 @@ const processGround = async function (
|
|
|
60
60
|
* @param scene
|
|
61
61
|
*/
|
|
62
62
|
const defaultCamera = async function (scene: Scene): Promise<Camera> {
|
|
63
|
-
|
|
63
|
+
const camera = (await processCamera(scene, 'default_camera', {
|
|
64
64
|
type: 'arc',
|
|
65
65
|
active: true,
|
|
66
|
-
});
|
|
66
|
+
})) as ArcRotateCamera;
|
|
67
|
+
|
|
68
|
+
// disables camera centering on double click
|
|
69
|
+
camera.useInputToRestoreState = false;
|
|
70
|
+
|
|
71
|
+
return camera;
|
|
67
72
|
};
|
|
68
73
|
|
|
69
74
|
/**
|
|
@@ -1,10 +1,15 @@
|
|
|
1
|
-
import { Node as BjsNode, Color3, InstancedMesh, PBRMaterial, Vector3 } from '../../index';
|
|
2
1
|
import { Viewer } from '../classes/viewer';
|
|
3
2
|
import { injectMetadata } from '../util/babylonHelper';
|
|
3
|
+
import { getIsScaledDownDevice } from '../util/deviceHelper';
|
|
4
4
|
import { bakeGeometryOfMesh, createMeshFromInstancedMesh, resetTransformation } from '../util/geometryHelper';
|
|
5
5
|
import { isNodeIncludedInExclusionList } from '../util/structureHelper';
|
|
6
|
+
import { PBRMaterial } from '@babylonjs/core/Materials/PBR/pbrMaterial';
|
|
7
|
+
import { RenderTargetTexture } from '@babylonjs/core/Materials/Textures/renderTargetTexture';
|
|
8
|
+
import { Vector3 } from '@babylonjs/core/Maths/math.vector';
|
|
9
|
+
import { InstancedMesh } from '@babylonjs/core/Meshes/instancedMesh';
|
|
6
10
|
import { Mesh } from '@babylonjs/core/Meshes/mesh';
|
|
7
11
|
import { TransformNode } from '@babylonjs/core/Meshes/transformNode';
|
|
12
|
+
import { Node as BjsNode } from '@babylonjs/core/node';
|
|
8
13
|
import { GLTF2Export } from '@babylonjs/serializers/glTF/2.0/glTFSerializer';
|
|
9
14
|
|
|
10
15
|
export class GltfExportManager {
|
|
@@ -15,18 +20,34 @@ export class GltfExportManager {
|
|
|
15
20
|
};
|
|
16
21
|
protected static readonly _EXPORT_ROOT_NAME = '__export_root__';
|
|
17
22
|
|
|
18
|
-
protected
|
|
23
|
+
protected _maxTextureSize: number;
|
|
24
|
+
|
|
25
|
+
protected constructor(protected viewer: Viewer) {
|
|
26
|
+
// store initial max texture size, so that we can restore it in the post processing
|
|
27
|
+
this._maxTextureSize = viewer.engine.getCaps().maxTextureSize;
|
|
28
|
+
}
|
|
19
29
|
|
|
20
30
|
public static async create(viewer: Viewer): Promise<GltfExportManager> {
|
|
21
31
|
return new GltfExportManager(viewer);
|
|
22
32
|
}
|
|
23
33
|
|
|
24
|
-
|
|
25
|
-
|
|
34
|
+
/**
|
|
35
|
+
* Exports selected nodes to a file.
|
|
36
|
+
* @param filename Optional name of the exported .GLB file.
|
|
37
|
+
* @param optimizeForAR Adjusts the exported GLB so that known issues get automatically fixed, this
|
|
38
|
+
* is mostly targeting Apples .usdz format.
|
|
39
|
+
* @param excluded Optional list of geometry (meshes, elements, variants,...) to be excluded from the export.
|
|
40
|
+
*/
|
|
41
|
+
public async exportGlb(filename = 'glb-export.glb', optimizeForAR: boolean = false, excluded?: ExcludedGeometryList) {
|
|
42
|
+
await this._exportPreProcess(optimizeForAR, excluded);
|
|
26
43
|
|
|
27
|
-
const glbData = await GLTF2Export.GLBAsync(
|
|
44
|
+
const glbData = await GLTF2Export.GLBAsync(
|
|
45
|
+
this.viewer.scene,
|
|
46
|
+
'dummy',
|
|
47
|
+
GltfExportManager._gltfExportOptions(optimizeForAR, excluded)
|
|
48
|
+
);
|
|
28
49
|
|
|
29
|
-
await this._exportPostProcess();
|
|
50
|
+
await this._exportPostProcess(optimizeForAR);
|
|
30
51
|
|
|
31
52
|
const resBlob = glbData.glTFFiles['dummy.glb'];
|
|
32
53
|
// check if result is valid, according to the typings this could also be a string
|
|
@@ -41,32 +62,87 @@ export class GltfExportManager {
|
|
|
41
62
|
}
|
|
42
63
|
}
|
|
43
64
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
65
|
+
/**
|
|
66
|
+
* Exports selected nodes to GLTF. This may result in more than one file, since textures are exported seperately.
|
|
67
|
+
* @param filename Name of the main (text-based) .GLTF file referring to separate texture files.
|
|
68
|
+
* @param optimizeForAR Adjusts the exported GLB so that known issues get automatically fixed, this
|
|
69
|
+
* is mostly targeting Apples .usdz format.
|
|
70
|
+
* @param excluded Optional list of geometry (meshes, elements, variants,...) to be excluded from the export.
|
|
71
|
+
*/
|
|
72
|
+
public async exportGltfToFile(filename: string, optimizeForAR: boolean = false, excluded?: ExcludedGeometryList) {
|
|
73
|
+
await this._exportPreProcess(optimizeForAR, excluded);
|
|
74
|
+
|
|
75
|
+
const gltf = await GLTF2Export.GLTFAsync(
|
|
76
|
+
this.viewer.scene,
|
|
77
|
+
filename,
|
|
78
|
+
GltfExportManager._gltfExportOptions(optimizeForAR, excluded)
|
|
79
|
+
);
|
|
48
80
|
gltf.downloadFiles();
|
|
49
81
|
|
|
50
|
-
this._exportPostProcess();
|
|
82
|
+
this._exportPostProcess(optimizeForAR);
|
|
51
83
|
}
|
|
52
84
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
85
|
+
/**
|
|
86
|
+
* Exports selected nodes to GLB. This results in one binary file.
|
|
87
|
+
* @param filename Name of the main (text-based) .GLTF file referring to seperate texture files.
|
|
88
|
+
* @param optimizeForAR Adjusts the exported GLB so that known issues get automatically fixed, this
|
|
89
|
+
* is mostly targeting Apples .usdz format.
|
|
90
|
+
* @param excluded Optional list of geometry (meshes, elements, variants,...) to be excluded from the export.
|
|
91
|
+
*/
|
|
92
|
+
public async exportGlbToFile(filename: string, optimizeForAR: boolean = false, excluded?: ExcludedGeometryList) {
|
|
93
|
+
await this._exportPreProcess(optimizeForAR, excluded);
|
|
94
|
+
|
|
95
|
+
const glb = await GLTF2Export.GLBAsync(
|
|
96
|
+
this.viewer.scene,
|
|
97
|
+
filename,
|
|
98
|
+
GltfExportManager._gltfExportOptions(optimizeForAR, excluded)
|
|
99
|
+
);
|
|
57
100
|
glb.downloadFiles();
|
|
58
101
|
|
|
59
|
-
await this._exportPostProcess();
|
|
102
|
+
await this._exportPostProcess(optimizeForAR);
|
|
60
103
|
}
|
|
61
104
|
|
|
62
|
-
|
|
105
|
+
/**
|
|
106
|
+
* Prepares scene for GLB export.
|
|
107
|
+
* This is very important for AR exports, since we have to do a lot of conversions to satisfy Apples .usdz format.
|
|
108
|
+
*/
|
|
109
|
+
protected async _exportPreProcess(optimizeForAR: boolean, excluded?: ExcludedGeometryList): Promise<void> {
|
|
110
|
+
if (!optimizeForAR) {
|
|
111
|
+
// actually nothing to do if AR optimization is not required
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// pause rendering, since we are altering the active scene, which should not be visible to the user
|
|
63
116
|
this.viewer.pauseRendering();
|
|
64
117
|
|
|
65
|
-
//
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
118
|
+
// exchange materials if required
|
|
119
|
+
// the AR export should never contain textures larger than 1024 px:
|
|
120
|
+
// - iOS devices will crash most likely when trying to access AR endpoints with such files
|
|
121
|
+
// - file size will be reduced, which leads to faster loading times
|
|
122
|
+
// - textures > 1024 px shouldn't make a difference on mobiles anyway
|
|
123
|
+
// we don't have to rescale anything if are already on a downscaled device, since the textures are already <= 1024
|
|
124
|
+
// also we have to be very cautios with copying textures on these devices, since we are potentially very limited
|
|
125
|
+
// with the available memory
|
|
126
|
+
const isScaledDownDevice = getIsScaledDownDevice();
|
|
127
|
+
if (!isScaledDownDevice) {
|
|
128
|
+
// the idea is to re-create all textures with a smaller texture size
|
|
129
|
+
// we have to exchange all materials for this to work
|
|
130
|
+
this.viewer.engine.clearInternalTexturesCache();
|
|
131
|
+
this.viewer.engine.getCaps().maxTextureSize = 1024;
|
|
132
|
+
|
|
133
|
+
this.viewer.scene.materials
|
|
134
|
+
.filter(material => material instanceof PBRMaterial)
|
|
135
|
+
.forEach(material => GltfExportManager._exchangeMaterial(material as PBRMaterial));
|
|
136
|
+
}
|
|
69
137
|
|
|
138
|
+
// since Babylon.js v6 the conversion to right handed GLB coordinate system is done differently
|
|
139
|
+
// previously the geometry itself has been altered, now they negate the scaling of all root nodes
|
|
140
|
+
// this is an issue for us, since we will receive this negated scalings in the exported GLB, which leads to issues
|
|
141
|
+
// on iOS devices
|
|
142
|
+
// we fix that by adding a top level root node with a negative scaling as well
|
|
143
|
+
// the exporter just removes this node as he detects a "noop root node" (implementation details of Babylon.js)
|
|
144
|
+
// everything beneath this node remains untouched
|
|
145
|
+
// TODO BJS Update: Test AR export on iPhones as well and double check if we still need this special logic
|
|
70
146
|
const exportRootNode = new TransformNode(GltfExportManager._EXPORT_ROOT_NAME, this.viewer.scene);
|
|
71
147
|
exportRootNode.scaling = new Vector3(-1, 1, 1);
|
|
72
148
|
injectMetadata(exportRootNode!, {
|
|
@@ -74,30 +150,31 @@ export class GltfExportManager {
|
|
|
74
150
|
[GltfExportManager._METADATA_PROPS.deleteAfterExport]: true,
|
|
75
151
|
});
|
|
76
152
|
|
|
77
|
-
//
|
|
153
|
+
// create clones of each node (recursively), optionally exchange with cloned materials and mark these nodes for the
|
|
154
|
+
// export
|
|
78
155
|
this.viewer.scene.rootNodes
|
|
79
156
|
.filter(rootNode => rootNode.name !== GltfExportManager._EXPORT_ROOT_NAME)
|
|
80
157
|
.forEach(rootNode => this._prepareNodeForExport(rootNode, exportRootNode, excluded));
|
|
81
158
|
|
|
82
159
|
// bake transformation of all meshes, so that no negative scalings are left
|
|
83
160
|
// it's important that this is done AFTER instanced meshes have been converted (_prepareNodeForExport)
|
|
84
|
-
this.
|
|
85
|
-
.filter(mesh => !!mesh.metadata?.[GltfExportManager._METADATA_PROPS.exportNode])
|
|
86
|
-
.forEach(mesh => bakeGeometryOfMesh(mesh as Mesh));
|
|
161
|
+
this._getNodesMarkedForExport(true).forEach(mesh => bakeGeometryOfMesh(mesh as Mesh));
|
|
87
162
|
|
|
88
163
|
// reset transformation of all "TransformNodes", which couldn't be handled by the geometry baking algorithm
|
|
89
164
|
// it's important that this is done AFTER all geometries have been baked
|
|
90
|
-
|
|
91
|
-
.filter(
|
|
92
|
-
node =>
|
|
93
|
-
!!node.metadata?.[GltfExportManager._METADATA_PROPS.exportNode] &&
|
|
94
|
-
node.name !== GltfExportManager._EXPORT_ROOT_NAME
|
|
95
|
-
)
|
|
96
|
-
.forEach(node => resetTransformation(node as TransformNode));
|
|
165
|
+
this._getNodesMarkedForExport().forEach(node => resetTransformation(node as TransformNode));
|
|
97
166
|
}
|
|
98
167
|
|
|
99
|
-
|
|
100
|
-
|
|
168
|
+
/**
|
|
169
|
+
* Cleans up scene after export
|
|
170
|
+
*/
|
|
171
|
+
protected async _exportPostProcess(optimizeForAR: boolean): Promise<void> {
|
|
172
|
+
if (!optimizeForAR) {
|
|
173
|
+
// nothing to do, since no changes have been made without AR optimizations
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// dispose all nodes, materials and textures that have only been created for the export
|
|
101
178
|
this.viewer.scene.rootNodes
|
|
102
179
|
.filter(rootNode => rootNode.metadata?.[GltfExportManager._METADATA_PROPS.deleteAfterExport])
|
|
103
180
|
.forEach(rootNode => rootNode.dispose());
|
|
@@ -106,43 +183,45 @@ export class GltfExportManager {
|
|
|
106
183
|
.filter(mat => !!mat.metadata?.[GltfExportManager._METADATA_PROPS.deleteAfterExport])
|
|
107
184
|
.forEach(material => material.dispose(false, false));
|
|
108
185
|
|
|
109
|
-
this.viewer.
|
|
110
|
-
|
|
186
|
+
this.viewer.scene.textures
|
|
187
|
+
.filter(texture => !!texture.metadata?.[GltfExportManager._METADATA_PROPS.deleteAfterExport])
|
|
188
|
+
.forEach(texture => texture.dispose());
|
|
111
189
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
return !!node.metadata?.[GltfExportManager._METADATA_PROPS.exportNode];
|
|
116
|
-
},
|
|
117
|
-
};
|
|
190
|
+
// reset engines max texture size and resume rendering
|
|
191
|
+
this.viewer.engine.getCaps().maxTextureSize = this._maxTextureSize;
|
|
192
|
+
this.viewer.resumeRendering();
|
|
118
193
|
}
|
|
119
194
|
|
|
195
|
+
/**
|
|
196
|
+
* Creates a clone of the node which should be used for the export.
|
|
197
|
+
* Also switches to the cloned material if required.
|
|
198
|
+
*/
|
|
120
199
|
protected _prepareNodeForExport(node: BjsNode, clonedParent: TransformNode, excluded?: ExcludedGeometryList) {
|
|
121
|
-
if (!
|
|
200
|
+
if (!GltfExportManager._shouldExportNode(node, excluded)) {
|
|
122
201
|
return;
|
|
123
202
|
}
|
|
124
203
|
|
|
125
204
|
// from here on we only have TransformNodes
|
|
126
205
|
const transformNode = node as TransformNode;
|
|
127
206
|
|
|
128
|
-
// clone original node and create unique name (via uniqueId)
|
|
129
|
-
const
|
|
207
|
+
// clone original node and create unique name (via uniqueId) for the export
|
|
208
|
+
const clonedNodeName = `${transformNode.name}_${transformNode.uniqueId}`;
|
|
130
209
|
const clonedNode =
|
|
131
210
|
transformNode instanceof InstancedMesh
|
|
132
|
-
? createMeshFromInstancedMesh(transformNode,
|
|
133
|
-
: transformNode.clone(
|
|
211
|
+
? createMeshFromInstancedMesh(transformNode, clonedNodeName, clonedParent)
|
|
212
|
+
: transformNode.clone(clonedNodeName, clonedParent, true)!;
|
|
134
213
|
|
|
135
214
|
// exchange material
|
|
136
215
|
if (clonedNode instanceof Mesh) {
|
|
137
216
|
const exchangeWithMaterial =
|
|
138
217
|
clonedNode.material?.metadata?.[GltfExportManager._METADATA_PROPS.exchangeMaterialWith];
|
|
139
218
|
if (exchangeWithMaterial) {
|
|
140
|
-
clonedNode.material = this.viewer.scene.
|
|
219
|
+
clonedNode.material = this.viewer.scene.getMaterialByUniqueID(exchangeWithMaterial);
|
|
141
220
|
}
|
|
142
221
|
}
|
|
143
222
|
|
|
144
223
|
// signalize that this is a cloned node
|
|
145
|
-
injectMetadata(clonedNode
|
|
224
|
+
injectMetadata(clonedNode, {
|
|
146
225
|
[GltfExportManager._METADATA_PROPS.exportNode]: true,
|
|
147
226
|
[GltfExportManager._METADATA_PROPS.deleteAfterExport]: true,
|
|
148
227
|
});
|
|
@@ -152,10 +231,54 @@ export class GltfExportManager {
|
|
|
152
231
|
childs.forEach(child => this._prepareNodeForExport(child, clonedNode, excluded));
|
|
153
232
|
}
|
|
154
233
|
|
|
155
|
-
|
|
234
|
+
/**
|
|
235
|
+
* Help function for receiving all nodes that are marked for the export
|
|
236
|
+
*/
|
|
237
|
+
protected _getNodesMarkedForExport(meshesOnly?: boolean) {
|
|
238
|
+
const nodes: TransformNode[] = [...this.viewer.scene.meshes];
|
|
239
|
+
if (!meshesOnly) {
|
|
240
|
+
nodes.push(...this.viewer.scene.transformNodes);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const filteredNodes = nodes.filter(
|
|
244
|
+
node =>
|
|
245
|
+
!!node.metadata?.[GltfExportManager._METADATA_PROPS.exportNode] &&
|
|
246
|
+
// in the desired use cases we want to exclude the export root node
|
|
247
|
+
// maybe add a parameter if we have to include it in certain scenarios in the future
|
|
248
|
+
node.name !== GltfExportManager._EXPORT_ROOT_NAME
|
|
249
|
+
);
|
|
250
|
+
|
|
251
|
+
return filteredNodes;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Defines options for the export.
|
|
256
|
+
* We don't allow the user to overwrite certain settings, since we rely on properties like `removeNoopRootNodes` to
|
|
257
|
+
* stay `true` in order to make the AR export work.
|
|
258
|
+
* We could theoretically allow it if AR optimization is not desired, but this may confuse the user.
|
|
259
|
+
*/
|
|
260
|
+
protected static _gltfExportOptions(optimizeForAR: boolean, excluded?: ExcludedGeometryList): IExportOptions {
|
|
261
|
+
return {
|
|
262
|
+
shouldExportNode: function (node: BjsNode) {
|
|
263
|
+
if (optimizeForAR) {
|
|
264
|
+
// we explicitely marked nodes, that should be exported in AR mode
|
|
265
|
+
return !!node.metadata?.[GltfExportManager._METADATA_PROPS.exportNode];
|
|
266
|
+
} else {
|
|
267
|
+
// use the default export node check (enabled state, exclusion list, etc...)
|
|
268
|
+
return GltfExportManager._shouldExportNode(node, excluded);
|
|
269
|
+
}
|
|
270
|
+
},
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Checks if a node should be available in the export
|
|
276
|
+
*/
|
|
277
|
+
protected static _shouldExportNode(node: BjsNode, excluded?: ExcludedGeometryList): boolean {
|
|
156
278
|
if (!(node instanceof TransformNode)) {
|
|
157
279
|
return false;
|
|
158
280
|
}
|
|
281
|
+
// TODO: think of adding "BackgroundHelper" and nodes with "infiniteDistance" here as well, at least in AR mode
|
|
159
282
|
if (node.name === Viewer.BOUNDING_BOX_NAME) {
|
|
160
283
|
return false;
|
|
161
284
|
}
|
|
@@ -169,36 +292,25 @@ export class GltfExportManager {
|
|
|
169
292
|
return true;
|
|
170
293
|
}
|
|
171
294
|
|
|
172
|
-
protected _shouldReplaceMaterial(material: Material): boolean {
|
|
173
|
-
if (!(material instanceof PBRMaterial)) {
|
|
174
|
-
return false;
|
|
175
|
-
}
|
|
176
|
-
if (!material.subSurface.isRefractionEnabled) {
|
|
177
|
-
return false;
|
|
178
|
-
}
|
|
179
|
-
if (material.transparencyMode !== PBRMaterial.PBRMATERIAL_OPAQUE) {
|
|
180
|
-
return false;
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
return true;
|
|
184
|
-
}
|
|
185
|
-
|
|
186
295
|
/**
|
|
187
|
-
*
|
|
296
|
+
* Creates a clone of the material which should be used for the export.
|
|
297
|
+
* This is mostly required for recreating textures with lower sizes.
|
|
298
|
+
* Also refraction fixes will be applied.
|
|
188
299
|
*/
|
|
189
|
-
protected
|
|
300
|
+
protected static _exchangeMaterial(material: Material) {
|
|
190
301
|
const newName = `${material.name}_clone`;
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
clonedMaterial
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
302
|
+
const clonedMaterial = material.clone(newName)!;
|
|
303
|
+
|
|
304
|
+
// mark all exported textures, so that they will be deleted after the export
|
|
305
|
+
// CAUTION: disposing `RenderTargetTextures` will leave the scene in a "not ready" state
|
|
306
|
+
// I haven't found a solution for this in a certain time, so I'd just leave the in the scene, there are not used
|
|
307
|
+
// anyway since their parent material is disposed
|
|
308
|
+
clonedMaterial
|
|
309
|
+
.getActiveTextures()
|
|
310
|
+
.filter(texture => !(texture instanceof RenderTargetTexture))
|
|
311
|
+
.forEach(texture => injectMetadata(texture, { [GltfExportManager._METADATA_PROPS.deleteAfterExport]: true }));
|
|
312
|
+
|
|
313
|
+
injectMetadata(material, { [GltfExportManager._METADATA_PROPS.exchangeMaterialWith]: clonedMaterial.uniqueId });
|
|
202
314
|
injectMetadata(clonedMaterial, { [GltfExportManager._METADATA_PROPS.deleteAfterExport]: true });
|
|
203
315
|
}
|
|
204
316
|
}
|
|
@@ -176,7 +176,7 @@ const cloneTransformNodeMaterial = function (
|
|
|
176
176
|
* @param deep
|
|
177
177
|
* @param metadata
|
|
178
178
|
*/
|
|
179
|
-
const injectMetadata = function (object: Node | Material, metadata: {}, deep: boolean = true) {
|
|
179
|
+
const injectMetadata = function (object: Node | Material | BaseTexture, metadata: {}, deep: boolean = true) {
|
|
180
180
|
object.metadata = merge({}, object.metadata, metadata);
|
|
181
181
|
if (deep && object instanceof TransformNode) {
|
|
182
182
|
const children = object.getChildTransformNodes(true);
|
|
@@ -832,14 +832,14 @@ const reportDuplicateNodeNames = function (nodeNames: string[]): void {
|
|
|
832
832
|
|
|
833
833
|
const drawPaintableOnMaterial = function (
|
|
834
834
|
material: Material,
|
|
835
|
-
imageSource:
|
|
835
|
+
imageSource: HTMLImageElement,
|
|
836
836
|
scene: Scene,
|
|
837
837
|
options?: PaintableOptions
|
|
838
838
|
) {
|
|
839
839
|
// always take width and height from image source, scaling is done with uvScale properties
|
|
840
840
|
const widthAndHeight = {
|
|
841
|
-
width: imageSource.width
|
|
842
|
-
height: imageSource.height
|
|
841
|
+
width: imageSource.width,
|
|
842
|
+
height: imageSource.height,
|
|
843
843
|
};
|
|
844
844
|
|
|
845
845
|
const texture = new DynamicTexture(`${material.id}.paintable_texture`, widthAndHeight, scene);
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { SpecStorage } from '../store/specStorage';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Checks if the current device is an iPhone
|
|
5
|
+
*/
|
|
6
|
+
const getIsIPhone = () => {
|
|
7
|
+
const isIPhone = /iPhone/.test(navigator.userAgent);
|
|
8
|
+
|
|
9
|
+
return isIPhone;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Checks if the current device is "scaled down", as defined in the Spec
|
|
14
|
+
*/
|
|
15
|
+
const getIsScaledDownDevice = () => {
|
|
16
|
+
const sceneJson = SpecStorage.get<SceneJson>('scene');
|
|
17
|
+
|
|
18
|
+
if (!sceneJson.engine?.limitTextureSize) {
|
|
19
|
+
// no down scaling defined
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// check device
|
|
24
|
+
const deviceFits =
|
|
25
|
+
sceneJson.engine.limitTextureSize.devices === 'all' ||
|
|
26
|
+
(sceneJson.engine.limitTextureSize.devices === 'iPhone' && getIsIPhone());
|
|
27
|
+
|
|
28
|
+
return deviceFits;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export { getIsIPhone, getIsScaledDownDevice };
|
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
import { VertexBuffer } from '@babylonjs/core/Buffers/buffer';
|
|
2
2
|
import { Vector3 } from '@babylonjs/core/Maths/math.vector';
|
|
3
|
-
import {
|
|
3
|
+
import type { Geometry } from '@babylonjs/core/Meshes/geometry';
|
|
4
4
|
import { InstancedMesh } from '@babylonjs/core/Meshes/instancedMesh';
|
|
5
5
|
import { Mesh } from '@babylonjs/core/Meshes/mesh';
|
|
6
6
|
import { TransformNode } from '@babylonjs/core/Meshes/transformNode';
|
|
7
|
-
import type { MorphTarget } from '@babylonjs/core/Morph';
|
|
7
|
+
import type { MorphTarget } from '@babylonjs/core/Morph/morphTarget';
|
|
8
8
|
import { MorphTargetManager } from '@babylonjs/core/Morph/morphTargetManager';
|
|
9
9
|
import type { FloatArray } from '@babylonjs/core/types';
|
|
10
10
|
|
|
11
|
+
/**
|
|
12
|
+
* Creates a "standard" mesh from an instanced mesh, by cloning the source mesh and applying transformation data
|
|
13
|
+
*/
|
|
11
14
|
const createMeshFromInstancedMesh = function (
|
|
12
15
|
instancedMesh: InstancedMesh,
|
|
13
16
|
newName: string,
|
|
@@ -32,6 +35,10 @@ const createMeshFromInstancedMesh = function (
|
|
|
32
35
|
return newMesh;
|
|
33
36
|
};
|
|
34
37
|
|
|
38
|
+
/**
|
|
39
|
+
* Removes transformation data from the mesh and stores it in the geometry, which is called "baking".
|
|
40
|
+
* Also considers the geometry change from morph targets.
|
|
41
|
+
*/
|
|
35
42
|
const bakeGeometryOfMesh = function (mesh: Mesh) {
|
|
36
43
|
if (!mesh.geometry) {
|
|
37
44
|
// no geometry available, nothing to do
|
|
@@ -60,6 +67,16 @@ const bakeGeometryOfMesh = function (mesh: Mesh) {
|
|
|
60
67
|
mesh.bakeCurrentTransformIntoVertices();
|
|
61
68
|
};
|
|
62
69
|
|
|
70
|
+
/**
|
|
71
|
+
* Resets transformation to initial state
|
|
72
|
+
*/
|
|
73
|
+
const resetTransformation = function (node: TransformNode) {
|
|
74
|
+
node.position = new Vector3(0, 0, 0);
|
|
75
|
+
node.rotation = new Vector3(0, 0, 0);
|
|
76
|
+
node.rotationQuaternion = null;
|
|
77
|
+
node.scaling = new Vector3(1, 1, 1);
|
|
78
|
+
};
|
|
79
|
+
|
|
63
80
|
/**
|
|
64
81
|
* @param kind morph targets can affect various vertices kinds, whereas "position" is the most common one
|
|
65
82
|
* still other kinds (like normals or tangents) can be affected as well and can be provided on this input
|
|
@@ -108,11 +125,4 @@ const _getVerticesDataFromMorphTarget = function (kind: string, morphTarget: Mor
|
|
|
108
125
|
return null;
|
|
109
126
|
};
|
|
110
127
|
|
|
111
|
-
const resetTransformation = function (node: TransformNode) {
|
|
112
|
-
node.position = new Vector3(0, 0, 0);
|
|
113
|
-
node.rotation = new Vector3(0, 0, 0);
|
|
114
|
-
node.rotationQuaternion = null;
|
|
115
|
-
node.scaling = new Vector3(1, 1, 1);
|
|
116
|
-
};
|
|
117
|
-
|
|
118
128
|
export { createMeshFromInstancedMesh, bakeGeometryOfMesh, resetTransformation };
|
|
@@ -193,6 +193,15 @@ type SceneJson = {
|
|
|
193
193
|
engine?: {
|
|
194
194
|
antialiasing?: boolean;
|
|
195
195
|
options?: EngineOptions;
|
|
196
|
+
adaptToDeviceRatio?: boolean;
|
|
197
|
+
/**
|
|
198
|
+
* Possibility to limit the size of textures when getting loaded by the engine.
|
|
199
|
+
* Activating this feature potentially saves a lot of memory and can be used to avoid crashes when loading expensive
|
|
200
|
+
* models on weaker devices.
|
|
201
|
+
* In praxis this has been an issue with iPhones a lot, therefore it's possible to activate this feature for iPhones
|
|
202
|
+
* only as well.
|
|
203
|
+
*/
|
|
204
|
+
limitTextureSize?: LimitTextureSizeConfig | false;
|
|
196
205
|
};
|
|
197
206
|
scene: SceneDefinition;
|
|
198
207
|
animations?: AnimationDefinitions;
|
|
@@ -204,6 +213,11 @@ type SceneJson = {
|
|
|
204
213
|
cloneMaterialsOnMutation?: boolean;
|
|
205
214
|
};
|
|
206
215
|
|
|
216
|
+
type LimitTextureSizeConfig = {
|
|
217
|
+
size: 512 | 1024;
|
|
218
|
+
devices: 'iPhone' | 'all';
|
|
219
|
+
};
|
|
220
|
+
|
|
207
221
|
/**
|
|
208
222
|
* Extends the `SceneJson` by removing the optional flag from some properties which surely exist in the auto generated
|
|
209
223
|
* spec
|
|
@@ -468,26 +482,6 @@ type AnimationDefinitions = {
|
|
|
468
482
|
*/
|
|
469
483
|
type AnimationDefinition = GSAPTweenVars & { shortestWay?: boolean };
|
|
470
484
|
|
|
471
|
-
/**
|
|
472
|
-
* Extends Babylon.js's {@link IExportOptions} by some viewer specific settings
|
|
473
|
-
*/
|
|
474
|
-
interface IExportOptionsExtended extends IExportOptions {
|
|
475
|
-
/**
|
|
476
|
-
* Refraction textures get lost when exporting the scene to GLB.\
|
|
477
|
-
* The short explanation is that refraction is calculated by "RenderTargetTexture", which is created in real time from
|
|
478
|
-
* the camera perspective. This data is not available in the GLB export, therefore the texture gets removed.\
|
|
479
|
-
* However if `exchangeRefractionMaterials` is set, a fallback material with a similar look is exported instead. This
|
|
480
|
-
* fallback material solves the refraction with a combination of certain color, roughness, metallic and transparency
|
|
481
|
-
* mode settings.
|
|
482
|
-
*/
|
|
483
|
-
exchangeRefractionMaterials?: boolean;
|
|
484
|
-
/**
|
|
485
|
-
* iOS devices crash when exporting textures which are larger than 1024.\
|
|
486
|
-
* If this flag is set, then all textures will be limited to a maximum texture size of 1024.
|
|
487
|
-
*/
|
|
488
|
-
limitTextureSize?: boolean;
|
|
489
|
-
}
|
|
490
|
-
|
|
491
485
|
type SpecGenerationData = {
|
|
492
486
|
name: string;
|
|
493
487
|
url: string;
|
|
@@ -10,7 +10,11 @@ import { Tags } from '@babylonjs/core/Misc/tags';
|
|
|
10
10
|
* @param list list of excluded geometry
|
|
11
11
|
* @returns boolean based on whether node (or one of its parents) was found in list
|
|
12
12
|
*/
|
|
13
|
-
const isNodeIncludedInExclusionList = function (
|
|
13
|
+
const isNodeIncludedInExclusionList = function (
|
|
14
|
+
node: TransformNode,
|
|
15
|
+
list: ExcludedGeometryList,
|
|
16
|
+
skipParentCheck?: boolean
|
|
17
|
+
): boolean {
|
|
14
18
|
const checkNode = (inputNode: TransformNode, nodeToCheck: TransformNode) => {
|
|
15
19
|
return inputNode.name === nodeToCheck.name;
|
|
16
20
|
};
|
|
@@ -48,7 +52,7 @@ const isNodeIncludedInExclusionList = function (node: TransformNode, list: Exclu
|
|
|
48
52
|
};
|
|
49
53
|
|
|
50
54
|
let isExcluded = list.some(geometryToExclude => check(geometryToExclude, node));
|
|
51
|
-
if (!isExcluded && node.parent instanceof TransformNode) {
|
|
55
|
+
if (!isExcluded && !skipParentCheck && node.parent instanceof TransformNode) {
|
|
52
56
|
isExcluded = isNodeIncludedInExclusionList(node.parent, list);
|
|
53
57
|
}
|
|
54
58
|
|