@babylonjs/loaders 9.25.0 → 9.26.1
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/FBX/fbxConstraintBehavior.d.ts +188 -0
- package/FBX/fbxConstraintBehavior.js +532 -0
- package/FBX/fbxConstraintBehavior.js.map +1 -0
- package/FBX/fbxFileLoader.pure.d.ts +175 -6
- package/FBX/fbxFileLoader.pure.js +1521 -286
- package/FBX/fbxFileLoader.pure.js.map +1 -1
- package/FBX/index.d.ts +4 -1
- package/FBX/index.js +1 -0
- package/FBX/index.js.map +1 -1
- package/FBX/interpreter/animation.d.ts +73 -33
- package/FBX/interpreter/animation.js +620 -223
- package/FBX/interpreter/animation.js.map +1 -1
- package/FBX/interpreter/animationCurve.d.ts +91 -0
- package/FBX/interpreter/animationCurve.js +623 -0
- package/FBX/interpreter/animationCurve.js.map +1 -0
- package/FBX/interpreter/blendShapes.js +9 -18
- package/FBX/interpreter/blendShapes.js.map +1 -1
- package/FBX/interpreter/connections.d.ts +1 -1
- package/FBX/interpreter/connections.js +173 -34
- package/FBX/interpreter/connections.js.map +1 -1
- package/FBX/interpreter/constraints.d.ts +68 -0
- package/FBX/interpreter/constraints.js +143 -0
- package/FBX/interpreter/constraints.js.map +1 -0
- package/FBX/interpreter/fbxInterpreter.d.ts +89 -4
- package/FBX/interpreter/fbxInterpreter.js +474 -122
- package/FBX/interpreter/fbxInterpreter.js.map +1 -1
- package/FBX/interpreter/geometry.d.ts +1 -1
- package/FBX/interpreter/geometry.js +150 -92
- package/FBX/interpreter/geometry.js.map +1 -1
- package/FBX/interpreter/legacyDocument.d.ts +11 -0
- package/FBX/interpreter/legacyDocument.js +425 -0
- package/FBX/interpreter/legacyDocument.js.map +1 -0
- package/FBX/interpreter/materialModel.d.ts +71 -0
- package/FBX/interpreter/materialModel.js +622 -0
- package/FBX/interpreter/materialModel.js.map +1 -0
- package/FBX/interpreter/materials.d.ts +13 -1
- package/FBX/interpreter/materials.js +88 -5
- package/FBX/interpreter/materials.js.map +1 -1
- package/FBX/interpreter/nodeTransform.d.ts +32 -0
- package/FBX/interpreter/nodeTransform.js +54 -0
- package/FBX/interpreter/nodeTransform.js.map +1 -0
- package/FBX/interpreter/nurbs.d.ts +99 -0
- package/FBX/interpreter/nurbs.js +627 -0
- package/FBX/interpreter/nurbs.js.map +1 -0
- package/FBX/interpreter/propertyTemplates.d.ts +23 -0
- package/FBX/interpreter/propertyTemplates.js +109 -2
- package/FBX/interpreter/propertyTemplates.js.map +1 -1
- package/FBX/interpreter/rig.js +16 -0
- package/FBX/interpreter/rig.js.map +1 -1
- package/FBX/interpreter/sceneDiagnostics.js +28 -3
- package/FBX/interpreter/sceneDiagnostics.js.map +1 -1
- package/FBX/interpreter/skeleton.d.ts +4 -18
- package/FBX/interpreter/skeleton.js +16 -94
- package/FBX/interpreter/skeleton.js.map +1 -1
- package/FBX/parsers/fbxAsciiParser.js +119 -9
- package/FBX/parsers/fbxAsciiParser.js.map +1 -1
- package/FBX/parsers/fbxBinaryParser.js +74 -22
- package/FBX/parsers/fbxBinaryParser.js.map +1 -1
- package/FBX/pure.d.ts +1 -0
- package/FBX/pure.js +1 -0
- package/FBX/pure.js.map +1 -1
- package/FBX/types/fbxTypes.d.ts +18 -2
- package/FBX/types/fbxTypes.js +40 -12
- package/FBX/types/fbxTypes.js.map +1 -1
- package/SPLAT/gaussianSplattingStream.d.ts +31 -0
- package/SPLAT/gaussianSplattingStream.js +49 -0
- package/SPLAT/gaussianSplattingStream.js.map +1 -1
- package/USD/index.d.ts +2 -0
- package/USD/index.js +4 -0
- package/USD/index.js.map +1 -0
- package/USD/pure.d.ts +3 -0
- package/USD/pure.js +5 -0
- package/USD/pure.js.map +1 -0
- package/USD/usdCommandProtocol.d.ts +83 -0
- package/USD/usdCommandProtocol.js +173 -0
- package/USD/usdCommandProtocol.js.map +1 -0
- package/USD/usdFileLoader.d.ts +6 -0
- package/USD/usdFileLoader.js +9 -0
- package/USD/usdFileLoader.js.map +1 -0
- package/USD/usdFileLoader.metadata.d.ts +20 -0
- package/USD/usdFileLoader.metadata.js +18 -0
- package/USD/usdFileLoader.metadata.js.map +1 -0
- package/USD/usdFileLoader.pure.d.ts +102 -0
- package/USD/usdFileLoader.pure.js +341 -0
- package/USD/usdFileLoader.pure.js.map +1 -0
- package/USD/usdFileLoader.types.d.ts +11 -0
- package/USD/usdFileLoader.types.js +2 -0
- package/USD/usdFileLoader.types.js.map +1 -0
- package/USD/usdLoadingOptions.d.ts +123 -0
- package/USD/usdLoadingOptions.js +3 -0
- package/USD/usdLoadingOptions.js.map +1 -0
- package/USD/usdSceneMaterializer.d.ts +9 -0
- package/USD/usdSceneMaterializer.js +982 -0
- package/USD/usdSceneMaterializer.js.map +1 -0
- package/USD/usdWorkerMessages.d.ts +57 -0
- package/USD/usdWorkerMessages.js +3 -0
- package/USD/usdWorkerMessages.js.map +1 -0
- package/dynamic.js +10 -0
- package/dynamic.js.map +1 -1
- package/index.d.ts +1 -0
- package/index.js +1 -0
- package/index.js.map +1 -1
- package/package.json +4 -3
- package/pure.d.ts +1 -0
- package/pure.js +1 -0
- package/pure.js.map +1 -1
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
/* eslint-disable @typescript-eslint/naming-convention, jsdoc/require-param, jsdoc/require-returns */
|
|
2
2
|
import { RegisterSceneLoaderPlugin, } from "@babylonjs/core/Loading/sceneLoader.js";
|
|
3
3
|
import { Mesh } from "@babylonjs/core/Meshes/mesh.pure.js";
|
|
4
|
+
import { LinesMesh } from "@babylonjs/core/Meshes/linesMesh.pure.js";
|
|
4
5
|
import { SubMesh } from "@babylonjs/core/Meshes/subMesh.js";
|
|
5
6
|
import { VertexData } from "@babylonjs/core/Meshes/mesh.vertexData.js";
|
|
6
7
|
import { StandardMaterial } from "@babylonjs/core/Materials/standardMaterial.pure.js";
|
|
8
|
+
import { PBRMaterial } from "@babylonjs/core/Materials/PBR/pbrMaterial.pure.js";
|
|
7
9
|
import { Material } from "@babylonjs/core/Materials/material.js";
|
|
8
10
|
import { MultiMaterial } from "@babylonjs/core/Materials/multiMaterial.pure.js";
|
|
9
11
|
import { Texture } from "@babylonjs/core/Materials/Textures/texture.pure.js";
|
|
@@ -26,8 +28,10 @@ import { GetMimeType } from "@babylonjs/core/Misc/fileTools.pure.js";
|
|
|
26
28
|
import { parseBinaryFBX } from "./parsers/fbxBinaryParser.js";
|
|
27
29
|
import { parseAsciiFBX } from "./parsers/fbxAsciiParser.js";
|
|
28
30
|
import { interpretFBX } from "./interpreter/fbxInterpreter.js";
|
|
29
|
-
import {
|
|
30
|
-
import {
|
|
31
|
+
import { FBXConstraintBehavior } from "./fbxConstraintBehavior.js";
|
|
32
|
+
import { isPbrShaderType, mapColor, mapScalar } from "./interpreter/materialModel.js";
|
|
33
|
+
import { evaluateLayeredChannel, evaluateLayeredProperty, isChannelSteppedAt, sampleFBXCurveAtTime, } from "./interpreter/animation.js";
|
|
34
|
+
import { computeFBXGeometricDeltaMatrix, computeFBXGeometricMatrix, computeFBXGeometricNormalMatrix, computeFBXLocalMatrix, eulerToMatrix, eulerToMatrixXYZ, } from "./interpreter/transform.js";
|
|
31
35
|
import { FBXFileLoaderMetadata } from "./fbxFileLoader.metadata.js";
|
|
32
36
|
const FBX_ASCII_MAGIC = "; FBX";
|
|
33
37
|
const FBX_BINARY_MAGIC = "Kaydara FBX Binary";
|
|
@@ -53,8 +57,36 @@ export class FBXFileLoader {
|
|
|
53
57
|
this._bindRestBones = new WeakSet();
|
|
54
58
|
this._sourceBonesBySkeleton = new WeakMap();
|
|
55
59
|
this._scaleCompensationHelpersBySkeleton = new WeakMap();
|
|
60
|
+
/** Frame rate of the file being loaded (GlobalSettings TimeMode); animation is baked at this rate. */
|
|
61
|
+
this._frameRate = 30;
|
|
62
|
+
/** Layers of the animation stack currently being converted; used by the transform samplers. */
|
|
63
|
+
this._activeLayers = [];
|
|
64
|
+
/** Parent model per model id, for inherit-mode aware sampling. */
|
|
65
|
+
this._parentModelById = new Map();
|
|
66
|
+
/** Curve nodes per model id for the stack currently being converted. */
|
|
67
|
+
this._curveNodesByModelId = new Map();
|
|
68
|
+
/** Helper nodes inserted above models whose InheritType is not RSrs. */
|
|
69
|
+
this._inheritScaleHelpers = new Map();
|
|
70
|
+
/** First mesh built per (geometry, geometric transform), for geometry sharing between instances. */
|
|
71
|
+
this._meshByGeometryKey = new Map();
|
|
72
|
+
/** Instance mesh -> source mesh whose geometry it shares. */
|
|
73
|
+
this._instanceSource = new Map();
|
|
74
|
+
/** Property curve nodes that were mapped onto Babylon animations; their "not evaluated" diagnostics are dropped. */
|
|
75
|
+
this._evaluatedCurveNodeIds = new Set();
|
|
76
|
+
this._rigBoneModelIds = new Set();
|
|
77
|
+
const full = options.preset === "full";
|
|
56
78
|
this._options = {
|
|
79
|
+
preset: full ? "full" : "compatible",
|
|
57
80
|
normalMapCoordinateSystem: options.normalMapCoordinateSystem ?? "y-up",
|
|
81
|
+
materials: options.materials ?? (full ? "auto" : "standard"),
|
|
82
|
+
unitScale: options.unitScale ?? "preserve",
|
|
83
|
+
shareGeometry: options.shareGeometry ?? full,
|
|
84
|
+
onWarning: options.onWarning ?? (() => { }),
|
|
85
|
+
nurbsSubdivision: options.nurbsSubdivision ?? 0,
|
|
86
|
+
curves: options.curves ?? (full ? "lines" : "skip"),
|
|
87
|
+
constraints: options.constraints ?? (full ? "apply" : "metadata"),
|
|
88
|
+
rebaseAnimations: options.rebaseAnimations ?? !full,
|
|
89
|
+
attachCamerasAndLights: options.attachCamerasAndLights ?? full,
|
|
58
90
|
};
|
|
59
91
|
}
|
|
60
92
|
/**
|
|
@@ -76,9 +108,9 @@ export class FBXFileLoader {
|
|
|
76
108
|
* @returns A promise containing the loaded meshes, particle systems, skeletons, animation groups, transform nodes, geometries, and lights
|
|
77
109
|
*/
|
|
78
110
|
async importMeshAsync(meshesNames, scene, data, rootUrl, _onProgress, _fileName) {
|
|
79
|
-
const
|
|
80
|
-
const
|
|
81
|
-
return
|
|
111
|
+
const fbxScene = this._parseAndInterpret(data);
|
|
112
|
+
const result = this._buildScene(fbxScene, scene, rootUrl, meshesNames);
|
|
113
|
+
return result;
|
|
82
114
|
}
|
|
83
115
|
/**
|
|
84
116
|
* Loads all FBX content into the scene.
|
|
@@ -90,8 +122,7 @@ export class FBXFileLoader {
|
|
|
90
122
|
* @returns A promise that resolves when loading is complete
|
|
91
123
|
*/
|
|
92
124
|
async loadAsync(scene, data, rootUrl, _onProgress, _fileName) {
|
|
93
|
-
const
|
|
94
|
-
const fbxScene = interpretFBX(doc);
|
|
125
|
+
const fbxScene = this._parseAndInterpret(data);
|
|
95
126
|
this._buildScene(fbxScene, scene, rootUrl, null);
|
|
96
127
|
}
|
|
97
128
|
/**
|
|
@@ -104,8 +135,7 @@ export class FBXFileLoader {
|
|
|
104
135
|
* @returns A promise containing the loaded asset container
|
|
105
136
|
*/
|
|
106
137
|
async loadAssetContainerAsync(scene, data, rootUrl, _onProgress, _fileName) {
|
|
107
|
-
const
|
|
108
|
-
const fbxScene = interpretFBX(doc);
|
|
138
|
+
const fbxScene = this._parseAndInterpret(data);
|
|
109
139
|
const container = new AssetContainer(scene);
|
|
110
140
|
// Build the scene into a temporary holder, then move results to container
|
|
111
141
|
const result = this._buildScene(fbxScene, scene, rootUrl, null);
|
|
@@ -134,6 +164,10 @@ export class FBXFileLoader {
|
|
|
134
164
|
this._addTextureToContainer(texture, container);
|
|
135
165
|
}
|
|
136
166
|
for (const mesh of result.meshes) {
|
|
167
|
+
// Lines meshes own an internal colour shader; it is not an asset of the file.
|
|
168
|
+
if (mesh instanceof LinesMesh) {
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
137
171
|
this._addMaterialToContainer(mesh.material, container);
|
|
138
172
|
}
|
|
139
173
|
// Remove all added objects from the scene (container owns them)
|
|
@@ -142,6 +176,17 @@ export class FBXFileLoader {
|
|
|
142
176
|
return container;
|
|
143
177
|
}
|
|
144
178
|
// ── Parsing ────────────────────────────────────────────────────────────
|
|
179
|
+
/**
|
|
180
|
+
* Parses and interprets the file. Parsing is synchronous, so no progress events are emitted: the scene loader's
|
|
181
|
+
* progress callback reports download bytes and must not be fed synthetic counts.
|
|
182
|
+
*/
|
|
183
|
+
_parseAndInterpret(data) {
|
|
184
|
+
const doc = this._parse(data);
|
|
185
|
+
return interpretFBX(doc, {
|
|
186
|
+
nurbsSubdivision: this._options.nurbsSubdivision > 0 ? this._options.nurbsSubdivision : undefined,
|
|
187
|
+
rebaseKeyframes: this._options.rebaseAnimations,
|
|
188
|
+
});
|
|
189
|
+
}
|
|
145
190
|
_parse(data) {
|
|
146
191
|
if (data instanceof ArrayBuffer) {
|
|
147
192
|
return this._parseFromArrayBuffer(data);
|
|
@@ -163,9 +208,11 @@ export class FBXFileLoader {
|
|
|
163
208
|
if (header.startsWith(FBX_BINARY_MAGIC)) {
|
|
164
209
|
return parseBinaryFBX(buffer);
|
|
165
210
|
}
|
|
166
|
-
// Try ASCII
|
|
211
|
+
// Try ASCII. Most files start with "; FBX x.y.z project file", but the header comment is optional:
|
|
212
|
+
// some exporters start straight at FBXHeaderExtension or even at Objects.
|
|
167
213
|
const text = new TextDecoder("utf-8").decode(buffer);
|
|
168
|
-
|
|
214
|
+
const head = text.substring(0, 4096);
|
|
215
|
+
if (head.trimStart().startsWith(FBX_ASCII_MAGIC) || /^\s*(?:;[^\n]*\n\s*)*(?:FBXHeaderExtension|Definitions|Objects|Connections)\s*:/.test(head)) {
|
|
169
216
|
return parseAsciiFBX(text);
|
|
170
217
|
}
|
|
171
218
|
throw new Error("FBXFileLoader: unrecognized FBX format");
|
|
@@ -173,6 +220,8 @@ export class FBXFileLoader {
|
|
|
173
220
|
// ── Scene Building ─────────────────────────────────────────────────────
|
|
174
221
|
_buildScene(fbxScene, scene, rootUrl, meshesNames) {
|
|
175
222
|
const nameFilter = this._buildNameFilter(meshesNames);
|
|
223
|
+
this._frameRate = fbxScene.frameRate > 0 ? fbxScene.frameRate : 30;
|
|
224
|
+
this._inheritScaleHelpers = new Map();
|
|
176
225
|
// Create materials
|
|
177
226
|
const materialCache = new Map();
|
|
178
227
|
for (const matData of fbxScene.materials) {
|
|
@@ -189,6 +238,12 @@ export class FBXFileLoader {
|
|
|
189
238
|
for (const skin of fbxScene.skins) {
|
|
190
239
|
skinById.set(skin.id, skin);
|
|
191
240
|
}
|
|
241
|
+
this._rigBoneModelIds = new Set();
|
|
242
|
+
for (const rig of fbxScene.rigs) {
|
|
243
|
+
for (const bone of rig.bones) {
|
|
244
|
+
this._rigBoneModelIds.add(bone.modelId);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
192
247
|
for (const rig of fbxScene.rigs) {
|
|
193
248
|
const skeleton = this._createSkeleton(rig.id, rig.bones, scene);
|
|
194
249
|
skeletons.push(skeleton);
|
|
@@ -205,13 +260,17 @@ export class FBXFileLoader {
|
|
|
205
260
|
}
|
|
206
261
|
// Collect model data for animation sampling.
|
|
207
262
|
const modelIdToData = new Map();
|
|
208
|
-
|
|
263
|
+
this._parentModelById = new Map();
|
|
264
|
+
const collectModelData = (models, parent) => {
|
|
209
265
|
for (const m of models) {
|
|
210
266
|
modelIdToData.set(m.id, m);
|
|
211
|
-
|
|
267
|
+
if (parent) {
|
|
268
|
+
this._parentModelById.set(m.id, parent);
|
|
269
|
+
}
|
|
270
|
+
collectModelData(m.children, m);
|
|
212
271
|
}
|
|
213
272
|
};
|
|
214
|
-
collectModelData(fbxScene.rootModels);
|
|
273
|
+
collectModelData(fbxScene.rootModels, null);
|
|
215
274
|
const cullingConflictMaterialIds = FBXFileLoader._collectCullingConflictMaterialIds(fbxScene.rootModels);
|
|
216
275
|
const cullingMaterialCloneCache = new Map();
|
|
217
276
|
// Build the FBX hierarchy under the same handedness conversion root that
|
|
@@ -223,6 +282,17 @@ export class FBXFileLoader {
|
|
|
223
282
|
rootNode.rotation.y = Math.PI;
|
|
224
283
|
rootNode.scaling.z = -1;
|
|
225
284
|
}
|
|
285
|
+
// FBX units: one file unit is UnitScaleFactor centimeters.
|
|
286
|
+
const unitScale = this._options.unitScale;
|
|
287
|
+
const cmPerTargetUnit = unitScale === "preserve" ? 0 : unitScale === "meters" ? 100 : unitScale;
|
|
288
|
+
if (cmPerTargetUnit > 0 && fbxScene.unitScaleFactor > 0) {
|
|
289
|
+
const factor = fbxScene.unitScaleFactor / cmPerTargetUnit;
|
|
290
|
+
rootNode.scaling.scaleInPlace(factor);
|
|
291
|
+
}
|
|
292
|
+
rootNode.metadata = { ...(rootNode.metadata ?? {}), fbxUnitScaleFactor: fbxScene.unitScaleFactor, fbxFrameRate: fbxScene.frameRate };
|
|
293
|
+
this._meshByGeometryKey = new Map();
|
|
294
|
+
this._instanceSource = new Map();
|
|
295
|
+
this._evaluatedCurveNodeIds = new Set();
|
|
226
296
|
const meshes = [];
|
|
227
297
|
const transformNodes = [rootNode];
|
|
228
298
|
let assetRoot = rootNode;
|
|
@@ -256,59 +326,56 @@ export class FBXFileLoader {
|
|
|
256
326
|
boneTransformNodes.add(transformNode);
|
|
257
327
|
}
|
|
258
328
|
}
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
const bone = this._getSourceBone(skeleton, boneData.index);
|
|
265
|
-
if (!boneNode || !bone) {
|
|
266
|
-
continue;
|
|
267
|
-
}
|
|
268
|
-
// Find direct children of this bone's TransformNode that aren't bones themselves
|
|
269
|
-
for (const child of [...boneNode.getChildren()]) {
|
|
270
|
-
const childTransform = child;
|
|
271
|
-
if (!boneTransformNodes.has(childTransform)) {
|
|
272
|
-
const childWorld = childTransform.computeWorldMatrix(true).clone();
|
|
273
|
-
const boneReferenceWorld = FBXFileLoader._getBoneReferenceWorldMatrix(skeleton, bone, boneReferenceNode, skinnedMesh);
|
|
274
|
-
const boneReferenceWorldInv = new Matrix();
|
|
275
|
-
boneReferenceWorld.invertToRef(boneReferenceWorldInv);
|
|
276
|
-
const childLocalToBone = childWorld.multiply(boneReferenceWorldInv);
|
|
277
|
-
childTransform.parent = null;
|
|
278
|
-
childTransform.attachToBone(bone, boneReferenceNode);
|
|
279
|
-
FBXFileLoader._applyMatrixToTransform(childTransform, childLocalToBone);
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
}
|
|
329
|
+
// Children of joints (props, end effectors, meshes) stay parented to the joint's TransformNode. Bones are
|
|
330
|
+
// linked to those nodes, so the hierarchy already follows the animation; attaching to the bone's skinning
|
|
331
|
+
// matrix would move them into the skinned mesh's space instead of the node's.
|
|
332
|
+
void boneTransformNodes;
|
|
333
|
+
void boneReferenceNode;
|
|
283
334
|
}
|
|
284
335
|
// Apply blend shapes (morph targets) to meshes
|
|
285
336
|
if (fbxScene.blendShapes.length > 0) {
|
|
286
337
|
this._applyBlendShapes(fbxScene.blendShapes, meshes, scene);
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
const group = this._createAnimationGroup(animStack, fbxScene.rigs, skeletonByRigId, scene, modelIdToNode, modelIdToData, meshes);
|
|
292
|
-
if (group) {
|
|
293
|
-
animationGroups.push(group);
|
|
338
|
+
for (const [instance, source] of Array.from(this._instanceSource)) {
|
|
339
|
+
if (source.morphTargetManager && !instance.morphTargetManager) {
|
|
340
|
+
instance.morphTargetManager = source.morphTargetManager;
|
|
341
|
+
}
|
|
294
342
|
}
|
|
295
343
|
}
|
|
296
344
|
// Create cameras
|
|
297
345
|
const cameras = [];
|
|
346
|
+
const cameraByAttributeId = new Map();
|
|
298
347
|
for (const camData of fbxScene.cameras) {
|
|
299
348
|
const cam = this._createCamera(camData, modelIdToNode, scene);
|
|
300
349
|
if (cam) {
|
|
301
350
|
cameras.push(cam);
|
|
351
|
+
cameraByAttributeId.set(camData.attributeId, cam);
|
|
302
352
|
}
|
|
303
353
|
}
|
|
304
354
|
// Create lights
|
|
305
355
|
const sceneLights = [];
|
|
356
|
+
const lightByAttributeId = new Map();
|
|
306
357
|
for (const lightData of fbxScene.lights) {
|
|
307
358
|
const light = this._createLight(lightData, modelIdToNode, scene);
|
|
308
359
|
if (light) {
|
|
309
360
|
sceneLights.push(light);
|
|
361
|
+
lightByAttributeId.set(lightData.attributeId, light);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
this._applyConstraints(fbxScene, modelIdToNode, rootNode, scene);
|
|
365
|
+
// Create animation groups
|
|
366
|
+
const animationGroups = [];
|
|
367
|
+
for (const animStack of fbxScene.animations) {
|
|
368
|
+
const group = this._createAnimationGroup(animStack, fbxScene.rigs, skeletonByRigId, scene, modelIdToNode, modelIdToData, meshes, {
|
|
369
|
+
cameraByAttributeId,
|
|
370
|
+
lightByAttributeId,
|
|
371
|
+
materialCache,
|
|
372
|
+
});
|
|
373
|
+
if (group) {
|
|
374
|
+
animationGroups.push(group);
|
|
310
375
|
}
|
|
311
376
|
}
|
|
377
|
+
// Reported last so that property curves the animation groups picked up are not flagged as unevaluated.
|
|
378
|
+
this._reportDiagnostics(fbxScene, rootNode, modelIdToNode);
|
|
312
379
|
return {
|
|
313
380
|
meshes,
|
|
314
381
|
particleSystems: [],
|
|
@@ -398,18 +465,60 @@ export class FBXFileLoader {
|
|
|
398
465
|
}
|
|
399
466
|
return Matrix.FromValues(basisRows[0][0], basisRows[0][1], basisRows[0][2], 0, basisRows[1][0], basisRows[1][1], basisRows[1][2], 0, basisRows[2][0], basisRows[2][1], basisRows[2][2], 0, 0, 0, 0, 1);
|
|
400
467
|
}
|
|
401
|
-
_buildModel(model, scene,
|
|
468
|
+
_buildModel(model, scene, parentNode, assetRoot, parentFBXWorldMatrix, materialCache, nameFilter, meshes, transformNodes, skeletonByGeometryId, skinByGeometryId, skinBindingByGeometryId, modelIdToNode, cullingConflictMaterialIds, cullingMaterialCloneCache) {
|
|
469
|
+
let parent = parentNode;
|
|
402
470
|
const localMatrix = FBXFileLoader._computeFBXModelLocalMatrix(model);
|
|
403
471
|
const fbxWorldMatrix = localMatrix.multiply(parentFBXWorldMatrix);
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
472
|
+
// Nodes with RrSs / Rrs inheritance do not inherit their parent's scale the normal way. Insert a helper that
|
|
473
|
+
// cancels the parent scale and re-express the local transform in that unscaled frame (see ufbx's
|
|
474
|
+
// inherit-mode evaluation). Skeleton bones with Rrs are handled by the rig code instead.
|
|
475
|
+
const parentModel = this._parentModelById.get(model.id);
|
|
476
|
+
const parentModelNode = parentModel ? modelIdToNode.get(parentModel.id) : undefined;
|
|
477
|
+
if (parent &&
|
|
478
|
+
parentModel &&
|
|
479
|
+
parentModelNode &&
|
|
480
|
+
parent === parentModelNode &&
|
|
481
|
+
model.inheritType !== 1 &&
|
|
482
|
+
!this._isRigBone(model.id) &&
|
|
483
|
+
!this._isRigBone(parentModel.id) &&
|
|
484
|
+
!(parentModel.geometry && skinByGeometryId.has(parentModel.geometry.id))) {
|
|
485
|
+
// The helper sits beside the parent and carries the parent's rotation and translation with unit scale,
|
|
486
|
+
// i.e. the parent's "unscaled" frame. This avoids inverting the parent scale (which may be zero).
|
|
487
|
+
const helper = new TransformNode(`${model.name}__fbx_inheritScale`, scene);
|
|
488
|
+
helper.parent = parentModelNode.parent;
|
|
489
|
+
helper.position.copyFrom(parentModelNode.position);
|
|
490
|
+
helper.rotationQuaternion = parentModelNode.rotationQuaternion ? parentModelNode.rotationQuaternion.clone() : Quaternion.FromEulerVector(parentModelNode.rotation);
|
|
491
|
+
helper.scaling.set(1, 1, 1);
|
|
492
|
+
transformNodes.push(helper);
|
|
493
|
+
this._inheritScaleHelpers.set(model.id, helper);
|
|
494
|
+
parent = helper;
|
|
495
|
+
}
|
|
496
|
+
if (model.geometry && model.geometry.indices.length > 0 && (!nameFilter || nameFilter(model.name))) {
|
|
497
|
+
// Create mesh. Tessellated NURBS carry no control point mapping, so their deformers are not applied
|
|
498
|
+
// (the interpreter reports them).
|
|
499
|
+
const deformable = model.geometry.controlPointIndices !== null;
|
|
500
|
+
const skeleton = deformable ? skeletonByGeometryId.get(model.geometry.id) : undefined;
|
|
501
|
+
const skin = deformable ? skinByGeometryId.get(model.geometry.id) : undefined;
|
|
502
|
+
const skinBinding = deformable ? skinBindingByGeometryId.get(model.geometry.id) : undefined;
|
|
409
503
|
if (skeleton && skin) {
|
|
410
504
|
skeleton.needInitialSkinMatrix = true;
|
|
411
505
|
}
|
|
412
|
-
|
|
506
|
+
// Models referencing the same geometry (with the same geometric transform) share one Babylon geometry.
|
|
507
|
+
const geometryKey = `${model.geometry.id}|${model.geometricTranslation.join(",")}|${model.geometricRotation.join(",")}|${model.geometricScaling.join(",")}`;
|
|
508
|
+
const source = !skeleton && this._options.shareGeometry ? this._meshByGeometryKey.get(geometryKey) : undefined;
|
|
509
|
+
let mesh;
|
|
510
|
+
if (source) {
|
|
511
|
+
mesh = source.clone(model.name, null, true);
|
|
512
|
+
mesh.sideOrientation = source.sideOrientation;
|
|
513
|
+
mesh.metadata = { ...(source.metadata ?? {}), fbxInstanceOf: source.name };
|
|
514
|
+
this._instanceSource.set(mesh, source);
|
|
515
|
+
}
|
|
516
|
+
else {
|
|
517
|
+
mesh = this._createMesh(model, model.geometry, scene, skeleton, skin, skinBinding);
|
|
518
|
+
if (!skeleton) {
|
|
519
|
+
this._meshByGeometryKey.set(geometryKey, mesh);
|
|
520
|
+
}
|
|
521
|
+
}
|
|
413
522
|
// For skinned meshes: keep bind/pose math in FBX space, but parent
|
|
414
523
|
// the rendered mesh under the same conversion root as non-skinned
|
|
415
524
|
// meshes. The pose matrix cancels the real FBX mesh transform only;
|
|
@@ -426,7 +535,7 @@ export class FBXFileLoader {
|
|
|
426
535
|
if (parent) {
|
|
427
536
|
mesh.parent = parent;
|
|
428
537
|
}
|
|
429
|
-
|
|
538
|
+
this._applyRestTRS(mesh, model);
|
|
430
539
|
}
|
|
431
540
|
// Apply material(s)
|
|
432
541
|
if (model.materials.length > 1 && model.geometry?.materialIndices) {
|
|
@@ -451,8 +560,21 @@ export class FBXFileLoader {
|
|
|
451
560
|
this._buildModel(child, scene, mesh, assetRoot, fbxWorldMatrix, materialCache, nameFilter, meshes, transformNodes, skeletonByGeometryId, skinByGeometryId, skinBindingByGeometryId, modelIdToNode, cullingConflictMaterialIds, cullingMaterialCloneCache);
|
|
452
561
|
}
|
|
453
562
|
}
|
|
563
|
+
else if (model.curve && this._options.curves !== "skip" && model.curve.polylines.length > 0 && (!nameFilter || nameFilter(model.name))) {
|
|
564
|
+
const lines = this._createLinesMesh(model, model.curve, scene);
|
|
565
|
+
if (parent) {
|
|
566
|
+
lines.parent = parent;
|
|
567
|
+
}
|
|
568
|
+
this._applyRestTRS(lines, model);
|
|
569
|
+
meshes.push(lines);
|
|
570
|
+
modelIdToNode.set(model.id, lines);
|
|
571
|
+
FBXFileLoader._applyModelMetadata(lines, model);
|
|
572
|
+
for (const child of model.children) {
|
|
573
|
+
this._buildModel(child, scene, lines, assetRoot, fbxWorldMatrix, materialCache, nameFilter, meshes, transformNodes, skeletonByGeometryId, skinByGeometryId, skinBindingByGeometryId, modelIdToNode, cullingConflictMaterialIds, cullingMaterialCloneCache);
|
|
574
|
+
}
|
|
575
|
+
}
|
|
454
576
|
else {
|
|
455
|
-
if (model.geometry
|
|
577
|
+
if ((model.geometry || model.curve) && nameFilter && !FBXFileLoader._modelSubtreeMatchesNameFilter(model, nameFilter)) {
|
|
456
578
|
return;
|
|
457
579
|
}
|
|
458
580
|
// Transform node (Null type or no geometry)
|
|
@@ -461,7 +583,7 @@ export class FBXFileLoader {
|
|
|
461
583
|
transformNode.parent = parent;
|
|
462
584
|
}
|
|
463
585
|
// Apply full FBX transform chain
|
|
464
|
-
|
|
586
|
+
this._applyRestTRS(transformNode, model);
|
|
465
587
|
transformNodes.push(transformNode);
|
|
466
588
|
modelIdToNode.set(model.id, transformNode);
|
|
467
589
|
FBXFileLoader._applyModelMetadata(transformNode, model);
|
|
@@ -469,6 +591,7 @@ export class FBXFileLoader {
|
|
|
469
591
|
for (const child of model.children) {
|
|
470
592
|
this._buildModel(child, scene, transformNode, assetRoot, fbxWorldMatrix, materialCache, nameFilter, meshes, transformNodes, skeletonByGeometryId, skinByGeometryId, skinBindingByGeometryId, modelIdToNode, cullingConflictMaterialIds, cullingMaterialCloneCache);
|
|
471
593
|
}
|
|
594
|
+
FBXFileLoader._applyLodGroup(model, modelIdToNode);
|
|
472
595
|
}
|
|
473
596
|
}
|
|
474
597
|
_linkSkeletonsToTransformNodes(rigs, skeletonByRigId, modelIdToNode, transformNodes, scene) {
|
|
@@ -512,14 +635,117 @@ export class FBXFileLoader {
|
|
|
512
635
|
return false;
|
|
513
636
|
}
|
|
514
637
|
static _applyModelMetadata(node, model) {
|
|
515
|
-
if (!model.customProperties && model.diagnostics.length === 0) {
|
|
638
|
+
if (!model.customProperties && model.diagnostics.length === 0 && !model.displayLayer && !model.lodGroup) {
|
|
516
639
|
return;
|
|
517
640
|
}
|
|
518
641
|
node.metadata = {
|
|
519
642
|
...(node.metadata ?? {}),
|
|
520
|
-
...(model.customProperties ? { fbxCustomProperties: model.customProperties } : {}),
|
|
643
|
+
...(model.customProperties ? { fbxCustomProperties: model.customProperties, fbxUserProperties: model.customProperties } : {}),
|
|
521
644
|
...(model.diagnostics.length > 0 ? { fbxDiagnostics: model.diagnostics } : {}),
|
|
645
|
+
...(model.displayLayer
|
|
646
|
+
? { fbxDisplayLayer: { name: model.displayLayer.name, show: model.displayLayer.show, freeze: model.displayLayer.freeze, color: model.displayLayer.color } }
|
|
647
|
+
: {}),
|
|
648
|
+
...(model.lodGroup ? { fbxLodGroup: model.lodGroup } : {}),
|
|
649
|
+
};
|
|
650
|
+
// Hidden display layers hide their members, as in the authoring application.
|
|
651
|
+
if (model.displayLayer && !model.displayLayer.show) {
|
|
652
|
+
node.setEnabled(false);
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
/**
|
|
656
|
+
* Wires a LodGroup's children as Babylon LOD levels: the first child holds the highest detail; every further
|
|
657
|
+
* child replaces it beyond the group's threshold distance (or screen coverage when thresholds are percentages).
|
|
658
|
+
* Each child's display mode is honoured first: level 1 (show) stays visible outside the LOD chain, level 2
|
|
659
|
+
* (hide) is disabled, and only level 0 (use LOD) children take part in the distance switching.
|
|
660
|
+
*/
|
|
661
|
+
static _applyLodGroup(model, modelIdToNode) {
|
|
662
|
+
const lod = model.lodGroup;
|
|
663
|
+
if (!lod) {
|
|
664
|
+
return;
|
|
665
|
+
}
|
|
666
|
+
const lodChildren = [];
|
|
667
|
+
for (let level = 0; level < model.children.length; level++) {
|
|
668
|
+
const child = model.children[level];
|
|
669
|
+
const display = lod.displayLevels[level] ?? 0;
|
|
670
|
+
if (display === 2) {
|
|
671
|
+
modelIdToNode.get(child.id)?.setEnabled(false);
|
|
672
|
+
}
|
|
673
|
+
else if (display === 0) {
|
|
674
|
+
lodChildren.push({ child, level });
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
if (lodChildren.length < 2) {
|
|
678
|
+
return;
|
|
679
|
+
}
|
|
680
|
+
const meshesOf = (child) => {
|
|
681
|
+
const node = modelIdToNode.get(child.id);
|
|
682
|
+
if (!node) {
|
|
683
|
+
return [];
|
|
684
|
+
}
|
|
685
|
+
const meshes = node instanceof Mesh ? [node] : [];
|
|
686
|
+
for (const m of node.getChildMeshes(false)) {
|
|
687
|
+
if (m instanceof Mesh) {
|
|
688
|
+
meshes.push(m);
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
return meshes;
|
|
522
692
|
};
|
|
693
|
+
const base = meshesOf(lodChildren[0].child);
|
|
694
|
+
for (let i = 1; i < lodChildren.length; i++) {
|
|
695
|
+
const level = lodChildren[i].level;
|
|
696
|
+
const levelMeshes = meshesOf(lodChildren[i].child);
|
|
697
|
+
const threshold = lod.thresholds[level - 1] ?? lod.thresholds[lod.thresholds.length - 1] ?? 0;
|
|
698
|
+
for (let i = 0; i < base.length; i++) {
|
|
699
|
+
const target = levelMeshes[i] ?? null;
|
|
700
|
+
if (lod.relative) {
|
|
701
|
+
base[i].useLODScreenCoverage = true;
|
|
702
|
+
base[i].addLODLevel(threshold / 100, target);
|
|
703
|
+
}
|
|
704
|
+
else {
|
|
705
|
+
base[i].addLODLevel(threshold, target);
|
|
706
|
+
}
|
|
707
|
+
if (target) {
|
|
708
|
+
target.metadata = { ...(target.metadata ?? {}), fbxLodLevel: level };
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
/** Builds a lines mesh from Line or tessellated NurbsCurve geometry, applying the model's geometric transform. */
|
|
714
|
+
_createLinesMesh(model, curve, scene) {
|
|
715
|
+
const lines = new LinesMesh(model.name, scene);
|
|
716
|
+
const positions = [];
|
|
717
|
+
const indices = [];
|
|
718
|
+
for (const polyline of curve.polylines) {
|
|
719
|
+
const count = polyline.length / 3;
|
|
720
|
+
if (count < 2) {
|
|
721
|
+
continue;
|
|
722
|
+
}
|
|
723
|
+
const base = positions.length / 3;
|
|
724
|
+
for (let i = 0; i < polyline.length; i++) {
|
|
725
|
+
positions.push(polyline[i]);
|
|
726
|
+
}
|
|
727
|
+
for (let i = 0; i < count - 1; i++) {
|
|
728
|
+
indices.push(base + i, base + i + 1);
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
const geometricMatrix = FBXFileLoader._computeFBXGeometricMatrix(model.geometricTranslation, model.geometricRotation, model.geometricScaling);
|
|
732
|
+
if (!geometricMatrix.equals(Matrix.Identity())) {
|
|
733
|
+
for (let i = 0; i < positions.length; i += 3) {
|
|
734
|
+
const v = Vector3.TransformCoordinates(new Vector3(positions[i], positions[i + 1], positions[i + 2]), geometricMatrix);
|
|
735
|
+
positions[i] = v.x;
|
|
736
|
+
positions[i + 1] = v.y;
|
|
737
|
+
positions[i + 2] = v.z;
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
const vertexData = new VertexData();
|
|
741
|
+
vertexData.positions = positions;
|
|
742
|
+
vertexData.indices = indices;
|
|
743
|
+
vertexData.applyToMesh(lines);
|
|
744
|
+
if (curve.color) {
|
|
745
|
+
lines.color = new Color3(curve.color[0], curve.color[1], curve.color[2]);
|
|
746
|
+
}
|
|
747
|
+
lines.metadata = { ...(lines.metadata ?? {}), fbxCurveType: curve.kind, fbxCurveSegments: curve.polylines.length };
|
|
748
|
+
return lines;
|
|
523
749
|
}
|
|
524
750
|
_createMesh(model, geomData, scene, skeleton, skin, skinBinding) {
|
|
525
751
|
const mesh = new Mesh(model.name, scene);
|
|
@@ -651,6 +877,18 @@ export class FBXFileLoader {
|
|
|
651
877
|
*/
|
|
652
878
|
_applyMultiMaterial(mesh, model, materialCache, scene, cullingConflictMaterialIds, cullingMaterialCloneCache) {
|
|
653
879
|
const matIndices = model.geometry.materialIndices;
|
|
880
|
+
const recorded = mesh.metadata?.fbxSubMeshMaterialIndices;
|
|
881
|
+
if (recorded) {
|
|
882
|
+
// Instance of an already split mesh: the index buffer and sub-meshes are shared; only the materials differ.
|
|
883
|
+
const multiMat = new MultiMaterial(model.name + "_multi", scene);
|
|
884
|
+
for (const matIdx of recorded) {
|
|
885
|
+
const fbxMat = model.materials[matIdx];
|
|
886
|
+
const mat = fbxMat ? materialCache.get(fbxMat.id) : undefined;
|
|
887
|
+
multiMat.subMaterials.push(mat ? FBXFileLoader._getModelMaterial(mat, model, cullingMaterialCloneCache, cullingConflictMaterialIds.has(fbxMat.id)) : null);
|
|
888
|
+
}
|
|
889
|
+
mesh.material = multiMat;
|
|
890
|
+
return;
|
|
891
|
+
}
|
|
654
892
|
const indices = mesh.getIndices();
|
|
655
893
|
if (!indices) {
|
|
656
894
|
return;
|
|
@@ -707,6 +945,7 @@ export class FBXFileLoader {
|
|
|
707
945
|
const range = subMeshRanges[i];
|
|
708
946
|
new SubMesh(i, 0, vertexCount, range.start, range.count, mesh);
|
|
709
947
|
}
|
|
948
|
+
mesh.metadata = { ...(mesh.metadata ?? {}), fbxSubMeshMaterialIndices: sortedMatIndices };
|
|
710
949
|
}
|
|
711
950
|
static _collectCullingConflictMaterialIds(models) {
|
|
712
951
|
// Deliberately scan the full scene, not just name-filtered models. This
|
|
@@ -750,7 +989,7 @@ export class FBXFileLoader {
|
|
|
750
989
|
if (cached) {
|
|
751
990
|
return cached;
|
|
752
991
|
}
|
|
753
|
-
const clone = material.clone(`${material.name}_CullingOff`);
|
|
992
|
+
const clone = material.clone(`${material.name}_CullingOff`) ?? material;
|
|
754
993
|
clone.backFaceCulling = false;
|
|
755
994
|
cullingCloneCache?.set(material, clone);
|
|
756
995
|
return clone;
|
|
@@ -761,26 +1000,40 @@ export class FBXFileLoader {
|
|
|
761
1000
|
}
|
|
762
1001
|
if (material instanceof MultiMaterial) {
|
|
763
1002
|
for (const subMaterial of material.subMaterials) {
|
|
764
|
-
if (subMaterial instanceof StandardMaterial) {
|
|
1003
|
+
if (subMaterial instanceof StandardMaterial || subMaterial instanceof PBRMaterial) {
|
|
765
1004
|
this._applyStandardMaterialUVSetCoordinates(subMaterial, geometry);
|
|
766
1005
|
}
|
|
767
1006
|
}
|
|
768
1007
|
return;
|
|
769
1008
|
}
|
|
770
|
-
if (material instanceof StandardMaterial) {
|
|
1009
|
+
if (material instanceof StandardMaterial || material instanceof PBRMaterial) {
|
|
771
1010
|
this._applyStandardMaterialUVSetCoordinates(material, geometry);
|
|
772
1011
|
}
|
|
773
1012
|
}
|
|
774
1013
|
_applyStandardMaterialUVSetCoordinates(material, geometry) {
|
|
775
|
-
for (const texture of
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
1014
|
+
for (const texture of material instanceof PBRMaterial
|
|
1015
|
+
? [
|
|
1016
|
+
material.albedoTexture,
|
|
1017
|
+
material.bumpTexture,
|
|
1018
|
+
material.emissiveTexture,
|
|
1019
|
+
material.ambientTexture,
|
|
1020
|
+
material.metallicTexture,
|
|
1021
|
+
material.reflectivityTexture,
|
|
1022
|
+
material.microSurfaceTexture,
|
|
1023
|
+
material.opacityTexture,
|
|
1024
|
+
material.clearCoat.texture,
|
|
1025
|
+
material.clearCoat.bumpTexture,
|
|
1026
|
+
material.sheen.texture,
|
|
1027
|
+
]
|
|
1028
|
+
: [
|
|
1029
|
+
material.diffuseTexture,
|
|
1030
|
+
material.bumpTexture,
|
|
1031
|
+
material.emissiveTexture,
|
|
1032
|
+
material.ambientTexture,
|
|
1033
|
+
material.specularTexture,
|
|
1034
|
+
material.opacityTexture,
|
|
1035
|
+
material.reflectionTexture,
|
|
1036
|
+
]) {
|
|
784
1037
|
if (!texture) {
|
|
785
1038
|
continue;
|
|
786
1039
|
}
|
|
@@ -804,26 +1057,26 @@ export class FBXFileLoader {
|
|
|
804
1057
|
if (!assignedMat) {
|
|
805
1058
|
return;
|
|
806
1059
|
}
|
|
807
|
-
|
|
808
|
-
if (!
|
|
809
|
-
const clone =
|
|
1060
|
+
const unmodulated = (mat) => {
|
|
1061
|
+
if (mat instanceof StandardMaterial && !mat.diffuseTexture) {
|
|
1062
|
+
const clone = mat.clone(`${mat.name}_VertexColor`);
|
|
810
1063
|
clone.diffuseColor = new Color3(1, 1, 1);
|
|
811
|
-
|
|
1064
|
+
return clone;
|
|
812
1065
|
}
|
|
813
|
-
|
|
814
|
-
|
|
1066
|
+
if (mat instanceof PBRMaterial && !mat.albedoTexture) {
|
|
1067
|
+
const clone = mat.clone(`${mat.name}_VertexColor`);
|
|
1068
|
+
clone.albedoColor = new Color3(1, 1, 1);
|
|
1069
|
+
return clone;
|
|
1070
|
+
}
|
|
1071
|
+
return mat;
|
|
1072
|
+
};
|
|
815
1073
|
if (assignedMat instanceof MultiMaterial) {
|
|
816
1074
|
const multiMat = new MultiMaterial(`${assignedMat.name}_VertexColor`, scene);
|
|
817
|
-
multiMat.subMaterials = assignedMat.subMaterials.map((sub) =>
|
|
818
|
-
if (sub instanceof StandardMaterial && !sub.diffuseTexture) {
|
|
819
|
-
const clone = sub.clone(`${sub.name}_VertexColor`);
|
|
820
|
-
clone.diffuseColor = new Color3(1, 1, 1);
|
|
821
|
-
return clone;
|
|
822
|
-
}
|
|
823
|
-
return sub;
|
|
824
|
-
});
|
|
1075
|
+
multiMat.subMaterials = assignedMat.subMaterials.map((sub) => (sub ? unmodulated(sub) : sub));
|
|
825
1076
|
mesh.material = multiMat;
|
|
1077
|
+
return;
|
|
826
1078
|
}
|
|
1079
|
+
mesh.material = unmodulated(assignedMat);
|
|
827
1080
|
}
|
|
828
1081
|
/**
|
|
829
1082
|
* Build per-polygon-vertex bone indices and weights from the control-point-based skin data.
|
|
@@ -900,6 +1153,278 @@ export class FBXFileLoader {
|
|
|
900
1153
|
};
|
|
901
1154
|
}
|
|
902
1155
|
_createMaterial(matData, scene, rootUrl) {
|
|
1156
|
+
const mode = this._options.materials;
|
|
1157
|
+
const usePbr = mode === "pbr" || (mode === "auto" && isPbrShaderType(matData.model.shaderType));
|
|
1158
|
+
const material = usePbr ? this._createPbrMaterial(matData, scene, rootUrl) : this._createStandardMaterial(matData, scene, rootUrl);
|
|
1159
|
+
material.metadata = {
|
|
1160
|
+
...(material.metadata ?? {}),
|
|
1161
|
+
...(matData.userProperties ? { fbxUserProperties: matData.userProperties } : {}),
|
|
1162
|
+
fbxShaderType: matData.model.shaderType,
|
|
1163
|
+
fbxFeatures: Object.fromEntries(Object.entries(matData.model.features).map(([k, v]) => [k, v?.enabled ?? false])),
|
|
1164
|
+
};
|
|
1165
|
+
return material;
|
|
1166
|
+
}
|
|
1167
|
+
_createPbrMaterial(matData, scene, rootUrl) {
|
|
1168
|
+
const material = new PBRMaterial(matData.name, scene);
|
|
1169
|
+
const pbr = matData.model.pbr;
|
|
1170
|
+
const features = matData.model.features;
|
|
1171
|
+
const enabled = (name) => features[name]?.enabled ?? false;
|
|
1172
|
+
const specGloss = matData.model.shaderType === "3dsMaxPbrSpecGloss";
|
|
1173
|
+
const scaleColor = (c, f) => c ? new Color3(c[0] * (f ?? 1), c[1] * (f ?? 1), c[2] * (f ?? 1)) : undefined;
|
|
1174
|
+
// Base colour
|
|
1175
|
+
const baseColor = mapColor(pbr.baseColor);
|
|
1176
|
+
material.albedoColor = scaleColor(baseColor, pbr.baseColor?.texture ? 1 : mapScalar(pbr.baseFactor)) ?? Color3.White();
|
|
1177
|
+
// Metal / rough or spec / gloss workflow
|
|
1178
|
+
if (specGloss) {
|
|
1179
|
+
material.metallic = null;
|
|
1180
|
+
material.roughness = null;
|
|
1181
|
+
material.reflectivityColor = scaleColor(mapColor(pbr.specularColor), 1) ?? Color3.White();
|
|
1182
|
+
material.microSurface = mapScalar(pbr.glossiness) ?? 1 - (mapScalar(pbr.roughness) ?? 0.5);
|
|
1183
|
+
}
|
|
1184
|
+
else {
|
|
1185
|
+
material.metallic = enabled("metalness") ? (mapScalar(pbr.metalness) ?? 0) : 0;
|
|
1186
|
+
material.roughness = mapScalar(pbr.roughness) ?? (pbr.glossiness?.value ? 1 - pbr.glossiness.value[0] : 0.5);
|
|
1187
|
+
if (enabled("specular") && pbr.specularFactor?.value) {
|
|
1188
|
+
material.metallicF0Factor = mapScalar(pbr.specularFactor) ?? 1;
|
|
1189
|
+
}
|
|
1190
|
+
if (enabled("specular")) {
|
|
1191
|
+
const specularColor = mapColor(pbr.specularColor);
|
|
1192
|
+
if (specularColor) {
|
|
1193
|
+
material.metallicReflectanceColor = new Color3(specularColor[0], specularColor[1], specularColor[2]);
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
// Emission
|
|
1198
|
+
const emission = scaleColor(mapColor(pbr.emissionColor), pbr.emissionColor?.texture ? 1 : mapScalar(pbr.emissionFactor));
|
|
1199
|
+
if (emission && enabled("emission")) {
|
|
1200
|
+
material.emissiveColor = emission;
|
|
1201
|
+
}
|
|
1202
|
+
else if (emission && (emission.r > 0 || emission.g > 0 || emission.b > 0)) {
|
|
1203
|
+
material.emissiveColor = emission;
|
|
1204
|
+
}
|
|
1205
|
+
// Opacity / transparency
|
|
1206
|
+
const opacity = mapColor(pbr.opacity);
|
|
1207
|
+
if (opacity && enabled("opacity")) {
|
|
1208
|
+
material.alpha = (opacity[0] + opacity[1] + opacity[2]) / 3;
|
|
1209
|
+
}
|
|
1210
|
+
else if (!isPbrShaderType(matData.model.shaderType)) {
|
|
1211
|
+
material.alpha = FBXFileLoader._alphaFromClassicTransparency(matData);
|
|
1212
|
+
}
|
|
1213
|
+
if (material.alpha < 1) {
|
|
1214
|
+
material.transparencyMode = Material.MATERIAL_ALPHABLEND;
|
|
1215
|
+
}
|
|
1216
|
+
// Transmission (refraction)
|
|
1217
|
+
const transmission = mapScalar(pbr.transmissionFactor) ?? 0;
|
|
1218
|
+
if (enabled("transmission") && isPbrShaderType(matData.model.shaderType) && transmission > 0) {
|
|
1219
|
+
material.subSurface.isRefractionEnabled = true;
|
|
1220
|
+
material.subSurface.refractionIntensity = transmission;
|
|
1221
|
+
const tint = mapColor(pbr.transmissionColor);
|
|
1222
|
+
if (tint) {
|
|
1223
|
+
material.subSurface.tintColor = new Color3(tint[0], tint[1], tint[2]);
|
|
1224
|
+
}
|
|
1225
|
+
const ior = mapScalar(pbr.specularIor);
|
|
1226
|
+
if (ior && enabled("ior")) {
|
|
1227
|
+
material.subSurface.indexOfRefraction = ior;
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
else {
|
|
1231
|
+
const ior = mapScalar(pbr.specularIor);
|
|
1232
|
+
if (ior && enabled("ior") && ior > 1) {
|
|
1233
|
+
material.indexOfRefraction = ior;
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
// Clear coat
|
|
1237
|
+
const coat = mapScalar(pbr.coatFactor) ?? 0;
|
|
1238
|
+
if ((enabled("coat") && coat > 0) || pbr.coatFactor?.texture) {
|
|
1239
|
+
material.clearCoat.isEnabled = true;
|
|
1240
|
+
material.clearCoat.intensity = coat;
|
|
1241
|
+
const coatRough = mapScalar(pbr.coatRoughness);
|
|
1242
|
+
if (coatRough !== undefined) {
|
|
1243
|
+
material.clearCoat.roughness = coatRough;
|
|
1244
|
+
}
|
|
1245
|
+
const coatIor = mapScalar(pbr.coatIor);
|
|
1246
|
+
if (coatIor !== undefined && coatIor > 1) {
|
|
1247
|
+
material.clearCoat.indexOfRefraction = coatIor;
|
|
1248
|
+
}
|
|
1249
|
+
const coatColor = mapColor(pbr.coatColor);
|
|
1250
|
+
if (coatColor && (coatColor[0] !== 1 || coatColor[1] !== 1 || coatColor[2] !== 1)) {
|
|
1251
|
+
material.clearCoat.isTintEnabled = true;
|
|
1252
|
+
material.clearCoat.tintColor = new Color3(coatColor[0], coatColor[1], coatColor[2]);
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
// Sheen
|
|
1256
|
+
const sheen = mapScalar(pbr.sheenFactor) ?? 0;
|
|
1257
|
+
if ((enabled("sheen") && sheen > 0) || pbr.sheenColor?.texture) {
|
|
1258
|
+
material.sheen.isEnabled = true;
|
|
1259
|
+
material.sheen.intensity = sheen;
|
|
1260
|
+
const sheenColor = mapColor(pbr.sheenColor);
|
|
1261
|
+
if (sheenColor) {
|
|
1262
|
+
material.sheen.color = new Color3(sheenColor[0], sheenColor[1], sheenColor[2]);
|
|
1263
|
+
}
|
|
1264
|
+
const sheenRough = mapScalar(pbr.sheenRoughness);
|
|
1265
|
+
if (sheenRough !== undefined) {
|
|
1266
|
+
material.sheen.roughness = sheenRough;
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
if (enabled("unlit")) {
|
|
1270
|
+
material.unlit = true;
|
|
1271
|
+
}
|
|
1272
|
+
if (enabled("doubleSided")) {
|
|
1273
|
+
material.backFaceCulling = false;
|
|
1274
|
+
}
|
|
1275
|
+
// Textures
|
|
1276
|
+
const textureFor = (map, isNormal = false) => {
|
|
1277
|
+
if (!map?.texture || !map.textureEnabled) {
|
|
1278
|
+
return null;
|
|
1279
|
+
}
|
|
1280
|
+
const texture = FBXFileLoader._createTexture(map.texture, scene, rootUrl, isNormal);
|
|
1281
|
+
if (texture) {
|
|
1282
|
+
FBXFileLoader._applyTextureSettings(texture, map.texture);
|
|
1283
|
+
}
|
|
1284
|
+
return texture;
|
|
1285
|
+
};
|
|
1286
|
+
const albedo = textureFor(pbr.baseColor);
|
|
1287
|
+
if (albedo) {
|
|
1288
|
+
material.albedoTexture = albedo;
|
|
1289
|
+
}
|
|
1290
|
+
const normal = textureFor(pbr.normalMap, true);
|
|
1291
|
+
if (normal) {
|
|
1292
|
+
material.bumpTexture = normal;
|
|
1293
|
+
this._configureNormalTexture(normal, material);
|
|
1294
|
+
}
|
|
1295
|
+
const emissiveTex = textureFor(pbr.emissionColor);
|
|
1296
|
+
if (emissiveTex) {
|
|
1297
|
+
material.emissiveTexture = emissiveTex;
|
|
1298
|
+
if (!emission || (emission.r === 0 && emission.g === 0 && emission.b === 0)) {
|
|
1299
|
+
material.emissiveColor = Color3.White();
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
const ao = textureFor(pbr.ambientOcclusion);
|
|
1303
|
+
if (ao) {
|
|
1304
|
+
material.ambientTexture = ao;
|
|
1305
|
+
material.useAmbientInGrayScale = true;
|
|
1306
|
+
}
|
|
1307
|
+
if (specGloss) {
|
|
1308
|
+
const spec = textureFor(pbr.specularColor);
|
|
1309
|
+
if (spec) {
|
|
1310
|
+
material.reflectivityTexture = spec;
|
|
1311
|
+
}
|
|
1312
|
+
const gloss = textureFor(pbr.glossiness) ?? textureFor(pbr.roughness);
|
|
1313
|
+
if (gloss) {
|
|
1314
|
+
material.microSurfaceTexture = gloss;
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
else {
|
|
1318
|
+
// Babylon reads metalness and roughness from one texture (B = metal, G = rough). Grayscale maps have
|
|
1319
|
+
// identical channels, so either map works alone; two different maps cannot both be honoured. The choice
|
|
1320
|
+
// is made on the references so that only the texture actually used is created.
|
|
1321
|
+
const metalRef = pbr.metalness?.textureEnabled ? pbr.metalness.texture : undefined;
|
|
1322
|
+
const roughSource = pbr.roughness?.textureEnabled && pbr.roughness.texture ? pbr.roughness : pbr.glossiness?.textureEnabled && pbr.glossiness.texture ? pbr.glossiness : undefined;
|
|
1323
|
+
const roughRef = roughSource?.texture;
|
|
1324
|
+
const sameTexture = !!metalRef && !!roughRef && metalRef.id === roughRef.id;
|
|
1325
|
+
let usesMetalTexture = false;
|
|
1326
|
+
if (metalRef && (sameTexture || !roughRef)) {
|
|
1327
|
+
const metalTex = textureFor(pbr.metalness);
|
|
1328
|
+
if (metalTex) {
|
|
1329
|
+
usesMetalTexture = true;
|
|
1330
|
+
material.metallicTexture = metalTex;
|
|
1331
|
+
material.useMetallnessFromMetallicTextureBlue = true;
|
|
1332
|
+
material.useRoughnessFromMetallicTextureGreen = sameTexture;
|
|
1333
|
+
material.useRoughnessFromMetallicTextureAlpha = false;
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
else if (roughRef) {
|
|
1337
|
+
const roughTex = textureFor(roughSource);
|
|
1338
|
+
if (roughTex) {
|
|
1339
|
+
material.metallicTexture = roughTex;
|
|
1340
|
+
material.useMetallnessFromMetallicTextureBlue = false;
|
|
1341
|
+
material.useRoughnessFromMetallicTextureGreen = true;
|
|
1342
|
+
material.useRoughnessFromMetallicTextureAlpha = false;
|
|
1343
|
+
if (metalRef) {
|
|
1344
|
+
material.metadata = { ...(material.metadata ?? {}), fbxDroppedMetalnessTexture: metalRef.relativeFileName };
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
// A roughness-only texture keeps the scalar metalness; a metalness texture needs metallic = 1 to show.
|
|
1349
|
+
if (usesMetalTexture && material.metallic === 0) {
|
|
1350
|
+
material.metallic = 1;
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
if (pbr.opacity?.texture && pbr.opacity.textureEnabled) {
|
|
1354
|
+
if (albedo && pbr.opacity.texture.id === pbr.baseColor?.texture?.id) {
|
|
1355
|
+
material.useAlphaFromAlbedoTexture = true;
|
|
1356
|
+
material.transparencyMode = Material.MATERIAL_ALPHATESTANDBLEND;
|
|
1357
|
+
}
|
|
1358
|
+
else {
|
|
1359
|
+
const opacityTex = textureFor(pbr.opacity);
|
|
1360
|
+
if (opacityTex) {
|
|
1361
|
+
material.opacityTexture = opacityTex;
|
|
1362
|
+
material.transparencyMode = Material.MATERIAL_ALPHATESTANDBLEND;
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
if (material.clearCoat.isEnabled) {
|
|
1367
|
+
const coatTex = textureFor(pbr.coatFactor);
|
|
1368
|
+
if (coatTex) {
|
|
1369
|
+
material.clearCoat.texture = coatTex;
|
|
1370
|
+
}
|
|
1371
|
+
const coatNormal = textureFor(pbr.coatNormal, true);
|
|
1372
|
+
if (coatNormal) {
|
|
1373
|
+
material.clearCoat.bumpTexture = coatNormal;
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1376
|
+
if (material.sheen.isEnabled) {
|
|
1377
|
+
const sheenTex = textureFor(pbr.sheenColor);
|
|
1378
|
+
if (sheenTex) {
|
|
1379
|
+
material.sheen.texture = sheenTex;
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
return material;
|
|
1383
|
+
}
|
|
1384
|
+
/** Alpha of a classic Lambert/Phong material: Opacity when present, otherwise 1 - TransparentColor * TransparencyFactor. */
|
|
1385
|
+
static _alphaFromClassicTransparency(matData) {
|
|
1386
|
+
const props = matData.properties;
|
|
1387
|
+
if (props.opacity !== undefined) {
|
|
1388
|
+
return props.opacity;
|
|
1389
|
+
}
|
|
1390
|
+
const factor = mapScalar(matData.model.fbx.transparencyFactor);
|
|
1391
|
+
if (factor === undefined) {
|
|
1392
|
+
return 1;
|
|
1393
|
+
}
|
|
1394
|
+
const color = mapColor(matData.model.fbx.transparencyColor);
|
|
1395
|
+
const amount = color ? (color[0] + color[1] + color[2]) / 3 : 1;
|
|
1396
|
+
return Math.min(1, Math.max(0, 1 - amount * factor));
|
|
1397
|
+
}
|
|
1398
|
+
static _applyTextureSettings(texture, tex) {
|
|
1399
|
+
if (tex.uvTranslation) {
|
|
1400
|
+
texture.uOffset = tex.uvTranslation[0];
|
|
1401
|
+
texture.vOffset = tex.uvTranslation[1];
|
|
1402
|
+
}
|
|
1403
|
+
if (tex.uvScaling) {
|
|
1404
|
+
texture.uScale = tex.uvScaling[0];
|
|
1405
|
+
texture.vScale = tex.uvScaling[1];
|
|
1406
|
+
}
|
|
1407
|
+
if (tex.uvRotation !== undefined) {
|
|
1408
|
+
texture.wAng = tex.uvRotation * (Math.PI / 180);
|
|
1409
|
+
}
|
|
1410
|
+
if (tex.uvSetIndex !== undefined) {
|
|
1411
|
+
texture.coordinatesIndex = tex.uvSetIndex;
|
|
1412
|
+
}
|
|
1413
|
+
if (tex.wrapU !== undefined) {
|
|
1414
|
+
texture.wrapU = tex.wrapU === 1 ? Texture.CLAMP_ADDRESSMODE : Texture.WRAP_ADDRESSMODE;
|
|
1415
|
+
}
|
|
1416
|
+
if (tex.wrapV !== undefined) {
|
|
1417
|
+
texture.wrapV = tex.wrapV === 1 ? Texture.CLAMP_ADDRESSMODE : Texture.WRAP_ADDRESSMODE;
|
|
1418
|
+
}
|
|
1419
|
+
if (tex.uvSetName || tex.layeredTextureId !== undefined) {
|
|
1420
|
+
texture.metadata = {
|
|
1421
|
+
...(texture.metadata ?? {}),
|
|
1422
|
+
...(tex.uvSetName ? { fbxUVSetName: tex.uvSetName } : {}),
|
|
1423
|
+
...(tex.layeredTextureId !== undefined ? { fbxLayeredTextureId: tex.layeredTextureId } : {}),
|
|
1424
|
+
};
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
_createStandardMaterial(matData, scene, rootUrl) {
|
|
903
1428
|
const material = new StandardMaterial(matData.name, scene);
|
|
904
1429
|
const props = matData.properties;
|
|
905
1430
|
const hasTexture = (...slots) => matData.textures.some((texture) => slots.includes(texture.propertyName));
|
|
@@ -922,12 +1447,7 @@ export class FBXFileLoader {
|
|
|
922
1447
|
const emissiveFactor = hasTexture("EmissiveColor", "Emissive") ? 1 : (props.emissiveFactor ?? 1);
|
|
923
1448
|
material.emissiveColor = new Color3(props.emissiveColor[0] * emissiveFactor, props.emissiveColor[1] * emissiveFactor, props.emissiveColor[2] * emissiveFactor);
|
|
924
1449
|
}
|
|
925
|
-
|
|
926
|
-
material.alpha = props.opacity;
|
|
927
|
-
}
|
|
928
|
-
else if (props.transparencyFactor !== undefined) {
|
|
929
|
-
material.alpha = 1 - props.transparencyFactor;
|
|
930
|
-
}
|
|
1450
|
+
material.alpha = FBXFileLoader._alphaFromClassicTransparency(matData);
|
|
931
1451
|
if (material.alpha < 1) {
|
|
932
1452
|
material.transparencyMode = Material.MATERIAL_ALPHABLEND;
|
|
933
1453
|
}
|
|
@@ -991,27 +1511,7 @@ export class FBXFileLoader {
|
|
|
991
1511
|
// Shininess map — no direct StandardMaterial slot
|
|
992
1512
|
break;
|
|
993
1513
|
}
|
|
994
|
-
|
|
995
|
-
if (tex.uvTranslation) {
|
|
996
|
-
texture.uOffset = tex.uvTranslation[0];
|
|
997
|
-
texture.vOffset = tex.uvTranslation[1];
|
|
998
|
-
}
|
|
999
|
-
if (tex.uvScaling) {
|
|
1000
|
-
texture.uScale = tex.uvScaling[0];
|
|
1001
|
-
texture.vScale = tex.uvScaling[1];
|
|
1002
|
-
}
|
|
1003
|
-
if (tex.uvRotation !== undefined) {
|
|
1004
|
-
texture.wAng = tex.uvRotation * (Math.PI / 180);
|
|
1005
|
-
}
|
|
1006
|
-
if (tex.uvSetIndex !== undefined) {
|
|
1007
|
-
texture.coordinatesIndex = tex.uvSetIndex;
|
|
1008
|
-
}
|
|
1009
|
-
if (tex.uvSetName) {
|
|
1010
|
-
texture.metadata = {
|
|
1011
|
-
...(texture.metadata ?? {}),
|
|
1012
|
-
fbxUVSetName: tex.uvSetName,
|
|
1013
|
-
};
|
|
1014
|
-
}
|
|
1514
|
+
FBXFileLoader._applyTextureSettings(texture, tex);
|
|
1015
1515
|
}
|
|
1016
1516
|
return material;
|
|
1017
1517
|
}
|
|
@@ -1273,6 +1773,8 @@ export class FBXFileLoader {
|
|
|
1273
1773
|
const position = Vector3.TransformCoordinates(Vector3.Zero(), worldMatrix);
|
|
1274
1774
|
const camera = new FreeCamera(camData.name, position, scene);
|
|
1275
1775
|
camera.fov = camData.fieldOfView * (Math.PI / 180);
|
|
1776
|
+
camera.fovMode = Camera.FOVMODE_VERTICAL_FIXED;
|
|
1777
|
+
void position;
|
|
1276
1778
|
camera.minZ = camData.nearPlane;
|
|
1277
1779
|
camera.maxZ = camData.farPlane;
|
|
1278
1780
|
camera.metadata = {
|
|
@@ -1285,25 +1787,44 @@ export class FBXFileLoader {
|
|
|
1285
1787
|
orthoZoom: camData.orthoZoom,
|
|
1286
1788
|
roll: camData.roll,
|
|
1287
1789
|
aspectRatio: camData.aspectRatio,
|
|
1790
|
+
fieldOfViewX: camData.fieldOfViewX,
|
|
1791
|
+
apertureMode: camData.apertureMode,
|
|
1792
|
+
apertureSizeInch: camData.apertureSizeInch,
|
|
1793
|
+
userProperties: camData.userProperties,
|
|
1288
1794
|
unknownProperties: camData.unknownProperties,
|
|
1289
1795
|
diagnostics: camData.diagnostics,
|
|
1290
1796
|
},
|
|
1291
1797
|
};
|
|
1292
1798
|
if (camData.projectionType === "orthographic") {
|
|
1293
|
-
const orthoHeight = camData.orthoZoom && camData.orthoZoom > 0 ? camData.orthoZoom : 1;
|
|
1294
|
-
const aspect = camData.aspectRatio > 0 ? camData.aspectRatio : 1;
|
|
1295
1799
|
camera.mode = Camera.ORTHOGRAPHIC_CAMERA;
|
|
1296
|
-
camera.orthoTop =
|
|
1297
|
-
camera.orthoBottom = -
|
|
1298
|
-
camera.orthoRight =
|
|
1299
|
-
camera.orthoLeft = -
|
|
1300
|
-
}
|
|
1301
|
-
// FBX cameras look down their local +X axis.
|
|
1302
|
-
// node's
|
|
1303
|
-
//
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1800
|
+
camera.orthoTop = camData.orthographicSize[1] / 2;
|
|
1801
|
+
camera.orthoBottom = -camData.orthographicSize[1] / 2;
|
|
1802
|
+
camera.orthoRight = camData.orthographicSize[0] / 2;
|
|
1803
|
+
camera.orthoLeft = -camData.orthographicSize[0] / 2;
|
|
1804
|
+
}
|
|
1805
|
+
// FBX cameras look down their local +X axis. When attached, the camera is parented to its node so it follows
|
|
1806
|
+
// the node's animation, and oriented with a fixed local rotation (camera forward +Z -> node +X), with the FBX
|
|
1807
|
+
// Roll applied around the view axis first. Otherwise it is placed in world space, unparented.
|
|
1808
|
+
if (parentNode && this._options.attachCamerasAndLights) {
|
|
1809
|
+
// The handedness conversion at the root mirrors every node's world matrix. A camera cannot render through
|
|
1810
|
+
// a mirrored parent (its view matrix would flip the image and the winding), so when the node's world
|
|
1811
|
+
// determinant is negative the camera hangs from a pivot whose own mirror cancels it.
|
|
1812
|
+
let cameraParent = parentNode;
|
|
1813
|
+
if (worldMatrix.determinant() < 0) {
|
|
1814
|
+
const pivot = new TransformNode(`${camData.name}__fbx_cameraPivot`, scene);
|
|
1815
|
+
pivot.parent = parentNode;
|
|
1816
|
+
pivot.scaling.set(1, 1, -1);
|
|
1817
|
+
cameraParent = pivot;
|
|
1818
|
+
}
|
|
1819
|
+
camera.parent = cameraParent;
|
|
1820
|
+
camera.position.set(0, 0, 0);
|
|
1821
|
+
const roll = ((camData.roll ?? 0) * Math.PI) / 180;
|
|
1822
|
+
camera.rotationQuaternion = Quaternion.FromRotationMatrix(Matrix.RotationZ(roll).multiply(Matrix.RotationY(Math.PI / 2)));
|
|
1823
|
+
}
|
|
1824
|
+
else {
|
|
1825
|
+
const target = Vector3.TransformCoordinates(new Vector3(1, 0, 0), worldMatrix);
|
|
1826
|
+
camera.setTarget(target);
|
|
1827
|
+
}
|
|
1307
1828
|
return camera;
|
|
1308
1829
|
}
|
|
1309
1830
|
_createLight(lightData, modelIdToNode, scene) {
|
|
@@ -1318,26 +1839,44 @@ export class FBXFileLoader {
|
|
|
1318
1839
|
const forwardPoint = Vector3.TransformCoordinates(new Vector3(0, 0, -1), worldMatrix);
|
|
1319
1840
|
const direction = forwardPoint.subtract(position).normalize();
|
|
1320
1841
|
let light;
|
|
1842
|
+
// When attached, the light is parented to its node (so it follows animation) and its position and direction
|
|
1843
|
+
// are expressed in node-local space: origin and local -Z. Otherwise it is placed in world space, unparented.
|
|
1844
|
+
const attach = !!parentNode && this._options.attachCamerasAndLights;
|
|
1845
|
+
const localPosition = attach ? Vector3.Zero() : position;
|
|
1846
|
+
const localDirection = attach ? new Vector3(0, 0, -1) : direction;
|
|
1321
1847
|
switch (lightData.lightType) {
|
|
1322
1848
|
case 1: // Directional
|
|
1323
|
-
light = new DirectionalLight(lightData.name,
|
|
1849
|
+
light = new DirectionalLight(lightData.name, localDirection, scene);
|
|
1324
1850
|
light.diffuse = color;
|
|
1325
1851
|
light.intensity = lightData.intensity;
|
|
1326
1852
|
break;
|
|
1327
1853
|
case 2: {
|
|
1328
1854
|
// Spot
|
|
1329
1855
|
const angle = lightData.coneAngle * (Math.PI / 180);
|
|
1330
|
-
light = new SpotLight(lightData.name,
|
|
1856
|
+
light = new SpotLight(lightData.name, localPosition, localDirection, angle, 2, scene);
|
|
1331
1857
|
light.diffuse = color;
|
|
1332
1858
|
light.intensity = lightData.intensity;
|
|
1859
|
+
if (lightData.innerAngle !== undefined) {
|
|
1860
|
+
light.innerAngle = Math.min(angle, (lightData.innerAngle * Math.PI) / 180);
|
|
1861
|
+
}
|
|
1333
1862
|
break;
|
|
1334
1863
|
}
|
|
1335
|
-
default: // Point (0)
|
|
1336
|
-
light = new PointLight(lightData.name,
|
|
1864
|
+
default: // Point (0), Area (3) and Volume (4) approximated as point lights
|
|
1865
|
+
light = new PointLight(lightData.name, localPosition, scene);
|
|
1337
1866
|
light.diffuse = color;
|
|
1338
1867
|
light.intensity = lightData.intensity;
|
|
1339
1868
|
break;
|
|
1340
1869
|
}
|
|
1870
|
+
if (attach && parentNode) {
|
|
1871
|
+
light.parent = parentNode;
|
|
1872
|
+
// Babylon only resolves a parented light's world position and direction during rendering; do it now
|
|
1873
|
+
// so getAbsolutePosition() and transformedDirection are usable straight after loading.
|
|
1874
|
+
light.computeTransformedInformation();
|
|
1875
|
+
}
|
|
1876
|
+
light.specular = color;
|
|
1877
|
+
if (lightData.enableFarAttenuation && lightData.farAttenuationEnd !== undefined && lightData.farAttenuationEnd > 0) {
|
|
1878
|
+
light.range = lightData.farAttenuationEnd;
|
|
1879
|
+
}
|
|
1341
1880
|
light.metadata = {
|
|
1342
1881
|
...(light.metadata ?? {}),
|
|
1343
1882
|
fbxLight: {
|
|
@@ -1349,6 +1888,10 @@ export class FBXFileLoader {
|
|
|
1349
1888
|
enableNearAttenuation: lightData.enableNearAttenuation,
|
|
1350
1889
|
enableFarAttenuation: lightData.enableFarAttenuation,
|
|
1351
1890
|
castShadows: lightData.castShadows,
|
|
1891
|
+
farAttenuationStart: lightData.farAttenuationStart,
|
|
1892
|
+
farAttenuationEnd: lightData.farAttenuationEnd,
|
|
1893
|
+
areaLightShape: lightData.areaLightShape,
|
|
1894
|
+
userProperties: lightData.userProperties,
|
|
1352
1895
|
unknownProperties: lightData.unknownProperties,
|
|
1353
1896
|
diagnostics: lightData.diagnostics,
|
|
1354
1897
|
},
|
|
@@ -1363,11 +1906,12 @@ export class FBXFileLoader {
|
|
|
1363
1906
|
const authoredAbsoluteMatrices = [];
|
|
1364
1907
|
const authoredRuntimeLocalMatrices = [];
|
|
1365
1908
|
// Compute authored Lcl matrices for bones that do not carry FBX bind data.
|
|
1909
|
+
const authoredInheritScales = FBXFileLoader._computeBoneInheritScales(bones, bones.map((b) => b.scale));
|
|
1366
1910
|
for (let i = 0; i < bones.length; i++) {
|
|
1367
1911
|
const boneData = bones[i];
|
|
1368
1912
|
const authoredLocal = FBXFileLoader._computeFBXLocalMatrix(boneData.translation, boneData.rotation, boneData.scale, boneData.preRotation, boneData.postRotation, boneData.rotationPivot, boneData.scalingPivot, boneData.rotationOffset, boneData.scalingOffset, boneData.rotationOrder);
|
|
1369
1913
|
authoredLocalMatrices[i] = authoredLocal;
|
|
1370
|
-
authoredRuntimeLocalMatrices[i] = FBXFileLoader._computeFBXRuntimeLocalMatrix(bones, authoredLocal, i);
|
|
1914
|
+
authoredRuntimeLocalMatrices[i] = FBXFileLoader._computeFBXRuntimeLocalMatrix(bones, authoredLocal, i, authoredInheritScales);
|
|
1371
1915
|
}
|
|
1372
1916
|
authoredAbsoluteMatrices.push(...FBXFileLoader._computeFBXAbsoluteMatrices(bones, authoredRuntimeLocalMatrices));
|
|
1373
1917
|
const absoluteBindMatrices = bones.map((boneData, index) => boneData.transformLinkMatrix
|
|
@@ -1391,11 +1935,12 @@ export class FBXFileLoader {
|
|
|
1391
1935
|
// Only bones with that scale disagreement need their animation curves
|
|
1392
1936
|
// remapped into bind-rest space; ordinary child curves are already in
|
|
1393
1937
|
// the expected local animation space.
|
|
1938
|
+
const restInheritScales = FBXFileLoader._computeBoneInheritScales(bones, bones.map((b) => b.scale));
|
|
1394
1939
|
for (let i = 0; i < bones.length; i++) {
|
|
1395
1940
|
let localMatrix = useBindAsRest ? localBindMatrices[i] : authoredRuntimeLocalMatrices[i];
|
|
1396
1941
|
let parentBone = bones[i].parentIndex >= 0 ? sourceBones[bones[i].parentIndex] : null;
|
|
1397
|
-
if (!useBindAsRest && bones[i].inheritType
|
|
1398
|
-
const split = FBXFileLoader._splitParentScaleCompensatedLocalMatrix(authoredLocalMatrices[i],
|
|
1942
|
+
if (!useBindAsRest && bones[i].inheritType !== 1 && bones[i].parentIndex >= 0 && parentBone) {
|
|
1943
|
+
const split = FBXFileLoader._splitParentScaleCompensatedLocalMatrix(authoredLocalMatrices[i], restInheritScales[bones[i].parentIndex], FBXFileLoader._getBoneInheritedScale(bones, i, restInheritScales));
|
|
1399
1944
|
const helper = new Bone(`${bones[i].name}__fbx_scaleCompensation`, skeleton, parentBone, split.helperLocalMatrix, split.helperLocalMatrix.clone(), Matrix.Identity(), -1);
|
|
1400
1945
|
helper.metadata = {
|
|
1401
1946
|
...(helper.metadata ?? {}),
|
|
@@ -1430,6 +1975,9 @@ export class FBXFileLoader {
|
|
|
1430
1975
|
}
|
|
1431
1976
|
return skeleton;
|
|
1432
1977
|
}
|
|
1978
|
+
_isRigBone(modelId) {
|
|
1979
|
+
return this._rigBoneModelIds.has(modelId);
|
|
1980
|
+
}
|
|
1433
1981
|
_getSourceBone(skeleton, sourceIndex) {
|
|
1434
1982
|
return this._sourceBonesBySkeleton.get(skeleton)?.[sourceIndex] ?? skeleton.bones[sourceIndex];
|
|
1435
1983
|
}
|
|
@@ -1448,22 +1996,69 @@ export class FBXFileLoader {
|
|
|
1448
1996
|
}
|
|
1449
1997
|
return absoluteMatrices;
|
|
1450
1998
|
}
|
|
1451
|
-
|
|
1999
|
+
/**
|
|
2000
|
+
* Effective ("inherit") scale of every bone, following the FBX SDK: the local scale for RSrs bones, and for
|
|
2001
|
+
* RrSs / Rrs bones the local scale multiplied by the scale of the bone's inherit-scale node (the parent for RrSs,
|
|
2002
|
+
* the parent's inherit-scale node for Rrs). Bones are ordered parents first.
|
|
2003
|
+
*/
|
|
2004
|
+
static _computeBoneInheritScales(bones, ownScales) {
|
|
2005
|
+
const result = [];
|
|
2006
|
+
for (let i = 0; i < bones.length; i++) {
|
|
2007
|
+
const own = ownScales[i];
|
|
2008
|
+
const inheritedFrom = FBXFileLoader._getBoneInheritScaleNode(bones, i);
|
|
2009
|
+
if (inheritedFrom < 0 || inheritedFrom >= i) {
|
|
2010
|
+
result[i] = [own[0], own[1], own[2]];
|
|
2011
|
+
}
|
|
2012
|
+
else {
|
|
2013
|
+
const p = result[inheritedFrom];
|
|
2014
|
+
result[i] = [own[0] * p[0], own[1] * p[1], own[2] * p[2]];
|
|
2015
|
+
}
|
|
2016
|
+
}
|
|
2017
|
+
return result;
|
|
2018
|
+
}
|
|
2019
|
+
static _getBoneInheritScaleNode(bones, index) {
|
|
1452
2020
|
const parentIndex = bones[index].parentIndex;
|
|
1453
|
-
if (
|
|
2021
|
+
if (parentIndex < 0) {
|
|
2022
|
+
return -1;
|
|
2023
|
+
}
|
|
2024
|
+
if (bones[index].inheritType === 0) {
|
|
2025
|
+
return parentIndex;
|
|
2026
|
+
}
|
|
2027
|
+
if (bones[index].inheritType === 2) {
|
|
2028
|
+
return FBXFileLoader._getBoneInheritScaleNode(bones, parentIndex);
|
|
2029
|
+
}
|
|
2030
|
+
return -1;
|
|
2031
|
+
}
|
|
2032
|
+
/** Scale a bone inherits into its own scale (RrSs chains), or unit scale. */
|
|
2033
|
+
static _getBoneInheritedScale(bones, index, inheritScales) {
|
|
2034
|
+
const node = FBXFileLoader._getBoneInheritScaleNode(bones, index);
|
|
2035
|
+
return node >= 0 ? inheritScales[node] : [1, 1, 1];
|
|
2036
|
+
}
|
|
2037
|
+
static _computeFBXRuntimeLocalMatrix(bones, localMatrix, index, inheritScales) {
|
|
2038
|
+
const parentIndex = bones[index].parentIndex;
|
|
2039
|
+
if (bones[index].inheritType === 1 || parentIndex < 0) {
|
|
1454
2040
|
return localMatrix;
|
|
1455
2041
|
}
|
|
1456
|
-
const
|
|
1457
|
-
|
|
2042
|
+
const scales = inheritScales ??
|
|
2043
|
+
FBXFileLoader._computeBoneInheritScales(bones, bones.map((b) => b.scale));
|
|
2044
|
+
return FBXFileLoader._applyParentScaleCompensation(localMatrix, scales[parentIndex], FBXFileLoader._getBoneInheritedScale(bones, index, scales));
|
|
1458
2045
|
}
|
|
1459
|
-
static _applyParentScaleCompensation(localMatrix, parentScale) {
|
|
1460
|
-
const split = FBXFileLoader._splitParentScaleCompensatedLocalMatrix(localMatrix, parentScale);
|
|
2046
|
+
static _applyParentScaleCompensation(localMatrix, parentScale, inheritedScale) {
|
|
2047
|
+
const split = FBXFileLoader._splitParentScaleCompensatedLocalMatrix(localMatrix, parentScale, inheritedScale);
|
|
1461
2048
|
return split.boneLocalMatrix.multiply(split.helperLocalMatrix);
|
|
1462
2049
|
}
|
|
1463
|
-
|
|
2050
|
+
/**
|
|
2051
|
+
* Splits a bone's FBX local matrix into a helper (which cancels the parent scale and carries the translation,
|
|
2052
|
+
* so the translation still follows the parent scale as the SDK does) and the bone's own rotation/scale. For RrSs
|
|
2053
|
+
* bones the inherited scale is folded into the bone scale.
|
|
2054
|
+
*/
|
|
2055
|
+
static _splitParentScaleCompensatedLocalMatrix(localMatrix, parentScale, inheritedScale) {
|
|
1464
2056
|
const translation = localMatrix.getTranslation();
|
|
1465
|
-
|
|
2057
|
+
let boneLocalMatrix = localMatrix.clone();
|
|
1466
2058
|
boneLocalMatrix.setTranslation(Vector3.Zero());
|
|
2059
|
+
if (inheritedScale && (inheritedScale[0] !== 1 || inheritedScale[1] !== 1 || inheritedScale[2] !== 1)) {
|
|
2060
|
+
boneLocalMatrix = Matrix.Scaling(inheritedScale[0], inheritedScale[1], inheritedScale[2]).multiply(boneLocalMatrix);
|
|
2061
|
+
}
|
|
1467
2062
|
const helperLocalMatrix = Matrix.Compose(FBXFileLoader._getInverseScaleVector(parentScale), Quaternion.Identity(), translation);
|
|
1468
2063
|
return { boneLocalMatrix, helperLocalMatrix };
|
|
1469
2064
|
}
|
|
@@ -1529,34 +2124,16 @@ export class FBXFileLoader {
|
|
|
1529
2124
|
rotationOrder,
|
|
1530
2125
|
});
|
|
1531
2126
|
}
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
// Decompose into TRS
|
|
1539
|
-
const s = new Vector3();
|
|
1540
|
-
const r = new Quaternion();
|
|
1541
|
-
const t = new Vector3();
|
|
1542
|
-
localMatrix.decompose(s, r, t);
|
|
1543
|
-
node.position = t;
|
|
1544
|
-
node.rotationQuaternion = r;
|
|
1545
|
-
node.scaling = s;
|
|
2127
|
+
_applyRestTRS(node, model) {
|
|
2128
|
+
const trs = FBXFileLoader._computeLocalTRS(model, model.translation, model.rotation, model.scale);
|
|
2129
|
+
this._adjustTRSForInheritMode(model, trs, undefined);
|
|
2130
|
+
node.position = trs.position;
|
|
2131
|
+
node.rotationQuaternion = trs.rotationQuaternion;
|
|
2132
|
+
node.scaling = trs.scaling;
|
|
1546
2133
|
}
|
|
1547
2134
|
static _computeFBXModelLocalMatrix(model) {
|
|
1548
2135
|
return FBXFileLoader._computeFBXLocalMatrix(model.translation, model.rotation, model.scale, model.preRotation, model.postRotation, model.rotationPivot, model.scalingPivot, model.rotationOffset, model.scalingOffset, model.rotationOrder);
|
|
1549
2136
|
}
|
|
1550
|
-
static _getBoneReferenceWorldMatrix(skeleton, bone, referenceNode, skinnedMesh) {
|
|
1551
|
-
if (skinnedMesh) {
|
|
1552
|
-
skeleton.getTransformMatrices(skinnedMesh);
|
|
1553
|
-
}
|
|
1554
|
-
else {
|
|
1555
|
-
skeleton.prepare(true);
|
|
1556
|
-
}
|
|
1557
|
-
referenceNode.computeWorldMatrix(true);
|
|
1558
|
-
return bone.getFinalMatrix().multiply(referenceNode.getWorldMatrix());
|
|
1559
|
-
}
|
|
1560
2137
|
static _applyMatrixToTransform(node, matrix) {
|
|
1561
2138
|
const s = new Vector3();
|
|
1562
2139
|
const r = new Quaternion();
|
|
@@ -1566,10 +2143,25 @@ export class FBXFileLoader {
|
|
|
1566
2143
|
node.rotationQuaternion = r;
|
|
1567
2144
|
node.scaling = s;
|
|
1568
2145
|
}
|
|
1569
|
-
_createAnimationGroup(animStack, rigs, skeletonByRigId, scene, modelIdToNode, modelIdToData, meshes) {
|
|
1570
|
-
|
|
2146
|
+
_createAnimationGroup(animStack, rigs, skeletonByRigId, scene, modelIdToNode, modelIdToData, meshes, propertyTargets) {
|
|
2147
|
+
// Stacks may animate only properties (visibility, light colour...) without any transform curve.
|
|
2148
|
+
if (animStack.curveNodes.length === 0 && !animStack.unsupportedCurveNodes.some((c) => c.targetId !== null && c.curves.length > 0)) {
|
|
1571
2149
|
return null;
|
|
1572
2150
|
}
|
|
2151
|
+
this._activeLayers = animStack.layers;
|
|
2152
|
+
this._curveNodesByModelId = new Map();
|
|
2153
|
+
for (const curveNode of animStack.curveNodes) {
|
|
2154
|
+
if (curveNode.type === "DeformPercent") {
|
|
2155
|
+
continue;
|
|
2156
|
+
}
|
|
2157
|
+
const list = this._curveNodesByModelId.get(curveNode.targetModelId);
|
|
2158
|
+
if (list) {
|
|
2159
|
+
list.push(curveNode);
|
|
2160
|
+
}
|
|
2161
|
+
else {
|
|
2162
|
+
this._curveNodesByModelId.set(curveNode.targetModelId, [curveNode]);
|
|
2163
|
+
}
|
|
2164
|
+
}
|
|
1573
2165
|
const animGroup = new AnimationGroup(animStack.name, scene);
|
|
1574
2166
|
const animatedBoneTargetProperties = new Map();
|
|
1575
2167
|
const addBoneAnimation = (animation, bone) => {
|
|
@@ -1635,7 +2227,7 @@ export class FBXFileLoader {
|
|
|
1635
2227
|
// authored Lcl curves remapped onto the bind-rest local space.
|
|
1636
2228
|
const inheritedRigModelIds = new Set();
|
|
1637
2229
|
for (const rig of rigs) {
|
|
1638
|
-
const inheritType2ModelIds = new Set(rig.bones.filter((bone) => bone.inheritType
|
|
2230
|
+
const inheritType2ModelIds = new Set(rig.bones.filter((bone) => bone.inheritType !== 1 && bone.parentIndex >= 0).map((bone) => bone.modelId));
|
|
1639
2231
|
if (inheritType2ModelIds.size === 0) {
|
|
1640
2232
|
continue;
|
|
1641
2233
|
}
|
|
@@ -1686,9 +2278,79 @@ export class FBXFileLoader {
|
|
|
1686
2278
|
animGroup.addTargetedAnimation(animation, node);
|
|
1687
2279
|
}
|
|
1688
2280
|
}
|
|
1689
|
-
//
|
|
2281
|
+
// Inherit-scale helpers follow the inverse of their parent's (possibly animated) scale. Children of animated
|
|
2282
|
+
// parents also need re-sampling even when they carry no curves of their own, since their local transform
|
|
2283
|
+
// depends on the parent scale.
|
|
2284
|
+
for (const [modelId, helper] of Array.from(this._inheritScaleHelpers)) {
|
|
2285
|
+
const modelData = modelIdToData.get(modelId);
|
|
2286
|
+
const parentModel = modelData ? this._parentModelById.get(modelId) : undefined;
|
|
2287
|
+
if (!modelData || !parentModel) {
|
|
2288
|
+
continue;
|
|
2289
|
+
}
|
|
2290
|
+
const fps = this._frameRate;
|
|
2291
|
+
// The helper mirrors the parent's animated rotation/translation (its own curves plus any inherited scale
|
|
2292
|
+
// that feeds its translation), with unit scale.
|
|
2293
|
+
const parentCurves = [
|
|
2294
|
+
...(this._curveNodesByModelId.get(parentModel.id) ?? []),
|
|
2295
|
+
...this._collectInheritScaleCurves(parentModel).filter((cn) => cn.targetModelId !== parentModel.id),
|
|
2296
|
+
];
|
|
2297
|
+
if (parentCurves.length > 0) {
|
|
2298
|
+
const baseTimes = collectAnimationSampleTimes(parentCurves, fps, animStack.startTime, animStack.stopTime);
|
|
2299
|
+
const sampleParent = (time) => this._computeInheritAwareLocalMatrix(parentModel, this._sampleModelLocalMatrix(parentModel, this._curveNodesByModelId.get(parentModel.id) ?? [], time), time);
|
|
2300
|
+
const times = FBXFileLoader._refineSampleTimes(baseTimes, sampleParent);
|
|
2301
|
+
const posKeys = [];
|
|
2302
|
+
const rotKeys = [];
|
|
2303
|
+
let prevQuat = null;
|
|
2304
|
+
for (const time of times) {
|
|
2305
|
+
const sc = new Vector3();
|
|
2306
|
+
const rq = new Quaternion();
|
|
2307
|
+
const tr = new Vector3();
|
|
2308
|
+
sampleParent(time).decompose(sc, rq, tr);
|
|
2309
|
+
if (prevQuat && Quaternion.Dot(prevQuat, rq) < 0) {
|
|
2310
|
+
rq.scaleInPlace(-1);
|
|
2311
|
+
}
|
|
2312
|
+
prevQuat = rq;
|
|
2313
|
+
posKeys.push({ frame: time * fps, value: tr });
|
|
2314
|
+
rotKeys.push({ frame: time * fps, value: rq });
|
|
2315
|
+
}
|
|
2316
|
+
if (!this._isVector3KeysConstant(posKeys)) {
|
|
2317
|
+
const anim = new Animation(`${helper.name}_position`, "position", fps, Animation.ANIMATIONTYPE_VECTOR3, Animation.ANIMATIONLOOPMODE_CYCLE);
|
|
2318
|
+
anim.setKeys(posKeys);
|
|
2319
|
+
animGroup.addTargetedAnimation(anim, helper);
|
|
2320
|
+
}
|
|
2321
|
+
if (!areQuaternionKeysConstant(rotKeys)) {
|
|
2322
|
+
const anim = new Animation(`${helper.name}_rotation`, "rotationQuaternion", fps, Animation.ANIMATIONTYPE_QUATERNION, Animation.ANIMATIONLOOPMODE_CYCLE);
|
|
2323
|
+
anim.setKeys(rotKeys);
|
|
2324
|
+
animGroup.addTargetedAnimation(anim, helper);
|
|
2325
|
+
}
|
|
2326
|
+
}
|
|
2327
|
+
// The child's own local transform depends on the parent scale; re-sample it when that scale animates.
|
|
2328
|
+
const scaleCurves = this._collectInheritScaleCurves(parentModel);
|
|
2329
|
+
const node = modelIdToNode.get(modelId);
|
|
2330
|
+
if (node && scaleCurves.length > 0 && !nonBoneCurves.has(modelId) && !boneCurves.has(modelId)) {
|
|
2331
|
+
for (const animation of this._buildNodeAnimations([], node.name, modelData, animStack.startTime, animStack.stopTime, scaleCurves)) {
|
|
2332
|
+
animGroup.addTargetedAnimation(animation, node);
|
|
2333
|
+
}
|
|
2334
|
+
}
|
|
2335
|
+
}
|
|
2336
|
+
// Process blend shape (morph target) animations. Every layer animating a channel contributes to one
|
|
2337
|
+
// influence animation evaluated through the layer stack.
|
|
2338
|
+
const blendShapeGroups = new Map();
|
|
1690
2339
|
for (const curveNode of blendShapeCurves) {
|
|
1691
|
-
const
|
|
2340
|
+
const group = blendShapeGroups.get(curveNode.targetModelId);
|
|
2341
|
+
if (group) {
|
|
2342
|
+
group.push(curveNode);
|
|
2343
|
+
}
|
|
2344
|
+
else {
|
|
2345
|
+
blendShapeGroups.set(curveNode.targetModelId, [curveNode]);
|
|
2346
|
+
}
|
|
2347
|
+
}
|
|
2348
|
+
for (const [targetChannelId, group] of Array.from(blendShapeGroups)) {
|
|
2349
|
+
group.sort((a, b) => a.layerIndex - b.layerIndex);
|
|
2350
|
+
const sources = group.filter((cn) => cn.curves.length > 0).map((cn) => ({ layerIndex: cn.layerIndex, curves: cn.curves }));
|
|
2351
|
+
if (sources.length === 0) {
|
|
2352
|
+
continue;
|
|
2353
|
+
}
|
|
1692
2354
|
// Find the morph target with matching channel ID across all meshes
|
|
1693
2355
|
let targetFound = false;
|
|
1694
2356
|
for (const mesh of meshes) {
|
|
@@ -1698,15 +2360,15 @@ export class FBXFileLoader {
|
|
|
1698
2360
|
const metadata = mesh.metadata;
|
|
1699
2361
|
const channelTargets = metadata?.fbxBlendShapeChannelTargets;
|
|
1700
2362
|
const targetInfo = channelTargets?.get(targetChannelId);
|
|
1701
|
-
if (targetInfo
|
|
1702
|
-
const fps =
|
|
2363
|
+
if (targetInfo) {
|
|
2364
|
+
const fps = this._frameRate;
|
|
1703
2365
|
for (let shapeIndex = 0; shapeIndex < targetInfo.targetIndices.length; shapeIndex++) {
|
|
1704
2366
|
const target = mesh.morphTargetManager.getTarget(targetInfo.targetIndices[shapeIndex]);
|
|
1705
2367
|
if (!target) {
|
|
1706
2368
|
continue;
|
|
1707
2369
|
}
|
|
1708
2370
|
const anim = new Animation(`${target.name}_influence`, "influence", fps, Animation.ANIMATIONTYPE_FLOAT, Animation.ANIMATIONLOOPMODE_CYCLE);
|
|
1709
|
-
const keys =
|
|
2371
|
+
const keys = this._layeredScalarKeys(sources, animStack, (value) => calculateBlendShapeInfluences(value, targetInfo.fullWeights, targetInfo.targetIndices.length)[shapeIndex] ?? 0);
|
|
1710
2372
|
anim.setKeys(keys);
|
|
1711
2373
|
animGroup.addTargetedAnimation(anim, target);
|
|
1712
2374
|
}
|
|
@@ -1722,26 +2384,53 @@ export class FBXFileLoader {
|
|
|
1722
2384
|
continue;
|
|
1723
2385
|
}
|
|
1724
2386
|
const target = mesh.morphTargetManager.getTarget(targetIndex);
|
|
1725
|
-
if (target
|
|
1726
|
-
const fps =
|
|
2387
|
+
if (target) {
|
|
2388
|
+
const fps = this._frameRate;
|
|
1727
2389
|
const anim = new Animation(`${target.name}_influence`, "influence", fps, Animation.ANIMATIONTYPE_FLOAT, Animation.ANIMATIONLOOPMODE_CYCLE);
|
|
1728
|
-
|
|
1729
|
-
anim.setKeys(keys);
|
|
2390
|
+
anim.setKeys(this._layeredScalarKeys(sources, animStack, (value) => value / 100));
|
|
1730
2391
|
animGroup.addTargetedAnimation(anim, target);
|
|
1731
2392
|
targetFound = true;
|
|
1732
2393
|
}
|
|
1733
2394
|
}
|
|
1734
2395
|
}
|
|
2396
|
+
// Animated non-transform properties: visibility, camera, light and material parameters. Curve nodes of
|
|
2397
|
+
// several layers that animate the same property are evaluated together through the layer stack.
|
|
2398
|
+
const propertyGroups = new Map();
|
|
2399
|
+
for (const curveNode of animStack.unsupportedCurveNodes) {
|
|
2400
|
+
if (curveNode.targetId === null || curveNode.curves.length === 0) {
|
|
2401
|
+
continue;
|
|
2402
|
+
}
|
|
2403
|
+
const key = `${curveNode.targetId}|${curveNode.propertyName ?? curveNode.type}`;
|
|
2404
|
+
const group = propertyGroups.get(key);
|
|
2405
|
+
if (group) {
|
|
2406
|
+
group.push(curveNode);
|
|
2407
|
+
}
|
|
2408
|
+
else {
|
|
2409
|
+
propertyGroups.set(key, [curveNode]);
|
|
2410
|
+
}
|
|
2411
|
+
}
|
|
2412
|
+
for (const group of Array.from(propertyGroups.values())) {
|
|
2413
|
+
group.sort((a, b) => a.layerIndex - b.layerIndex);
|
|
2414
|
+
const built = this._buildPropertyAnimations(group, animStack, modelIdToNode, propertyTargets);
|
|
2415
|
+
if (built.length > 0) {
|
|
2416
|
+
for (const curveNode of group) {
|
|
2417
|
+
this._evaluatedCurveNodeIds.add(curveNode.id);
|
|
2418
|
+
}
|
|
2419
|
+
}
|
|
2420
|
+
for (const { animation, target } of built) {
|
|
2421
|
+
animGroup.addTargetedAnimation(animation, target);
|
|
2422
|
+
}
|
|
2423
|
+
}
|
|
1735
2424
|
// Normalize the animation group
|
|
1736
2425
|
if (animGroup.targetedAnimations.length > 0) {
|
|
1737
|
-
animGroup.normalize(animStack.startTime *
|
|
2426
|
+
animGroup.normalize(animStack.startTime * this._frameRate, animStack.stopTime * this._frameRate);
|
|
1738
2427
|
return animGroup;
|
|
1739
2428
|
}
|
|
1740
2429
|
animGroup.dispose();
|
|
1741
2430
|
return null;
|
|
1742
2431
|
}
|
|
1743
2432
|
_buildInheritedRigBoneAnimations(rig, skeleton, boneCurves, modelIdToData, compensatedModelIds, startTime, stopTime) {
|
|
1744
|
-
const fps =
|
|
2433
|
+
const fps = this._frameRate;
|
|
1745
2434
|
const sampledModelIds = new Set();
|
|
1746
2435
|
for (let i = 0; i < rig.bones.length; i++) {
|
|
1747
2436
|
if (!compensatedModelIds.has(rig.bones[i].modelId)) {
|
|
@@ -1752,10 +2441,18 @@ export class FBXFileLoader {
|
|
|
1752
2441
|
}
|
|
1753
2442
|
}
|
|
1754
2443
|
const rigCurveNodes = rig.bones.filter((bone) => sampledModelIds.has(bone.modelId)).flatMap((bone) => boneCurves.get(bone.modelId) ?? []);
|
|
1755
|
-
|
|
2444
|
+
let times = collectAnimationSampleTimes(rigCurveNodes, fps, startTime, stopTime);
|
|
1756
2445
|
if (times.length === 0) {
|
|
1757
2446
|
return [];
|
|
1758
2447
|
}
|
|
2448
|
+
for (const boneData of rig.bones) {
|
|
2449
|
+
const md = modelIdToData.get(boneData.modelId);
|
|
2450
|
+
const cn = boneCurves.get(boneData.modelId) ?? [];
|
|
2451
|
+
if (!sampledModelIds.has(boneData.modelId) || !md || cn.length === 0) {
|
|
2452
|
+
continue;
|
|
2453
|
+
}
|
|
2454
|
+
times = FBXFileLoader._refineSampleTimes(times, (time) => this._sampleModelLocalMatrix(md, cn, time));
|
|
2455
|
+
}
|
|
1759
2456
|
const keysByBone = rig.bones.map(() => ({
|
|
1760
2457
|
posKeys: [],
|
|
1761
2458
|
rotKeys: [],
|
|
@@ -1796,14 +2493,15 @@ export class FBXFileLoader {
|
|
|
1796
2493
|
const curveNodes = boneCurves.get(boneData.modelId) ?? [];
|
|
1797
2494
|
return modelData ? this._sampleModelScale(modelData, curveNodes, time) : boneData.scale;
|
|
1798
2495
|
});
|
|
2496
|
+
const sampledInheritScales = FBXFileLoader._computeBoneInheritScales(rig.bones, sampledScales);
|
|
1799
2497
|
const frame = time * fps;
|
|
1800
2498
|
for (let i = 0; i < localMatrices.length; i++) {
|
|
1801
2499
|
if (!compensatedModelIds.has(rig.bones[i].modelId)) {
|
|
1802
2500
|
continue;
|
|
1803
2501
|
}
|
|
1804
2502
|
const parentIndex = rig.bones[i].parentIndex;
|
|
1805
|
-
const parentScale = parentIndex >= 0 ?
|
|
1806
|
-
const split = FBXFileLoader._splitParentScaleCompensatedLocalMatrix(localMatrices[i], parentScale);
|
|
2503
|
+
const parentScale = parentIndex >= 0 ? sampledInheritScales[parentIndex] : rig.bones[i].scale;
|
|
2504
|
+
const split = FBXFileLoader._splitParentScaleCompensatedLocalMatrix(localMatrices[i], parentScale, FBXFileLoader._getBoneInheritedScale(rig.bones, i, sampledInheritScales));
|
|
1807
2505
|
FBXFileLoader._pushMatrixKeys(keysByBone[i], frame, split.boneLocalMatrix);
|
|
1808
2506
|
FBXFileLoader._pushMatrixKeys(keysByHelper[i], frame, split.helperLocalMatrix);
|
|
1809
2507
|
}
|
|
@@ -1881,58 +2579,46 @@ export class FBXFileLoader {
|
|
|
1881
2579
|
* Build animations for a non-bone node, correctly handling pivots.
|
|
1882
2580
|
* Computes the full FBX transform matrix at each keyframe and decomposes into TRS.
|
|
1883
2581
|
*/
|
|
1884
|
-
_buildNodeAnimations(curveNodes, nodeName, modelData, startTime, stopTime) {
|
|
1885
|
-
const fps =
|
|
2582
|
+
_buildNodeAnimations(curveNodes, nodeName, modelData, startTime, stopTime, extraTimeCurves = []) {
|
|
2583
|
+
const fps = this._frameRate;
|
|
1886
2584
|
// Separate curves by type
|
|
1887
|
-
const tNode = curveNodes.find((cn) => cn.type === "T");
|
|
1888
2585
|
const rNode = curveNodes.find((cn) => cn.type === "R");
|
|
1889
|
-
const
|
|
1890
|
-
|
|
1891
|
-
if (times.length === 0) {
|
|
2586
|
+
const baseTimes = collectAnimationSampleTimes([...curveNodes, ...extraTimeCurves], fps, startTime, stopTime);
|
|
2587
|
+
if (baseTimes.length === 0) {
|
|
1892
2588
|
return [];
|
|
1893
2589
|
}
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
const
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
// Build keyframes by computing the
|
|
2590
|
+
const sampleTRS = (time) => {
|
|
2591
|
+
const sampled = this._sampleModelTRS(modelData, curveNodes, time);
|
|
2592
|
+
const trs = FBXFileLoader._computeLocalTRS(modelData, sampled.translation, sampled.rotation, sampled.scale);
|
|
2593
|
+
this._adjustTRSForInheritMode(modelData, trs, time);
|
|
2594
|
+
return trs;
|
|
2595
|
+
};
|
|
2596
|
+
const times = FBXFileLoader._refineSampleTimes(baseTimes, (time) => {
|
|
2597
|
+
const trs = sampleTRS(time);
|
|
2598
|
+
return Matrix.Compose(trs.scaling, trs.rotationQuaternion, trs.position);
|
|
2599
|
+
});
|
|
2600
|
+
// Build keyframes by computing the local transform at each time
|
|
1905
2601
|
const posKeys = [];
|
|
1906
2602
|
const rotKeys = [];
|
|
1907
2603
|
const sclKeys = [];
|
|
1908
2604
|
let prevQuat = null;
|
|
1909
2605
|
for (const time of times) {
|
|
1910
2606
|
const frame = time * fps;
|
|
1911
|
-
|
|
1912
|
-
const
|
|
1913
|
-
const
|
|
1914
|
-
const
|
|
1915
|
-
const
|
|
1916
|
-
const
|
|
1917
|
-
const
|
|
1918
|
-
const sx = sampleFBXCurveAtTime(sxCurve, time) ?? modelData.scale[0];
|
|
1919
|
-
const sy = sampleFBXCurveAtTime(syCurve, time) ?? modelData.scale[1];
|
|
1920
|
-
const sz = sampleFBXCurveAtTime(szCurve, time) ?? modelData.scale[2];
|
|
1921
|
-
// Compute the full FBX local transform matrix with pivots
|
|
1922
|
-
const localMatrix = FBXFileLoader._computeFBXLocalMatrix([tx, ty, tz], [rx, ry, rz], [sx, sy, sz], modelData.preRotation, modelData.postRotation, modelData.rotationPivot, modelData.scalingPivot, modelData.rotationOffset, modelData.scalingOffset, modelData.rotationOrder);
|
|
1923
|
-
// Decompose into TRS
|
|
1924
|
-
const s = new Vector3();
|
|
1925
|
-
const r = new Quaternion();
|
|
1926
|
-
const t = new Vector3();
|
|
1927
|
-
localMatrix.decompose(s, r, t);
|
|
2607
|
+
const trs = sampleTRS(time);
|
|
2608
|
+
const s = trs.scaling;
|
|
2609
|
+
const r = trs.rotationQuaternion;
|
|
2610
|
+
const t = trs.position;
|
|
2611
|
+
const stepT = isChannelSteppedAt(curveNodes, "T", time);
|
|
2612
|
+
const stepR = isChannelSteppedAt(curveNodes, "R", time);
|
|
2613
|
+
const stepS = isChannelSteppedAt(curveNodes, "S", time);
|
|
1928
2614
|
// Ensure quaternion continuity
|
|
1929
2615
|
if (prevQuat && Quaternion.Dot(prevQuat, r) < 0) {
|
|
1930
2616
|
r.scaleInPlace(-1);
|
|
1931
2617
|
}
|
|
1932
2618
|
prevQuat = r;
|
|
1933
|
-
posKeys.push({ frame, value: t });
|
|
1934
|
-
rotKeys.push({ frame, value: r });
|
|
1935
|
-
sclKeys.push({ frame, value: s });
|
|
2619
|
+
posKeys.push(stepT ? { frame, value: t, interpolation: 1 /* AnimationKeyInterpolation.STEP */ } : { frame, value: t });
|
|
2620
|
+
rotKeys.push(stepR ? { frame, value: r, interpolation: 1 /* AnimationKeyInterpolation.STEP */ } : { frame, value: r });
|
|
2621
|
+
sclKeys.push(stepS ? { frame, value: s, interpolation: 1 /* AnimationKeyInterpolation.STEP */ } : { frame, value: s });
|
|
1936
2622
|
}
|
|
1937
2623
|
const animations = [];
|
|
1938
2624
|
// Only create position animation if it's not constant
|
|
@@ -1955,6 +2641,404 @@ export class FBXFileLoader {
|
|
|
1955
2641
|
}
|
|
1956
2642
|
return animations;
|
|
1957
2643
|
}
|
|
2644
|
+
/**
|
|
2645
|
+
* Baked keys are interpolated linearly by Babylon. Between two frames an FBX cubic segment can deviate from that
|
|
2646
|
+
* line, so sample times are refined (midpoints inserted, up to two levels) wherever the interpolated transform
|
|
2647
|
+
* differs noticeably from the curve. Flat and linear segments stay at frame resolution.
|
|
2648
|
+
*/
|
|
2649
|
+
static _refineSampleTimes(times, sample, maxDepth = 2) {
|
|
2650
|
+
const decompose = (time) => {
|
|
2651
|
+
const s = new Vector3();
|
|
2652
|
+
const r = new Quaternion();
|
|
2653
|
+
const t = new Vector3();
|
|
2654
|
+
sample(time).decompose(s, r, t);
|
|
2655
|
+
return { s, r, t };
|
|
2656
|
+
};
|
|
2657
|
+
const out = [];
|
|
2658
|
+
const refine = (t0, t1, d0, d1, depth) => {
|
|
2659
|
+
if (depth >= maxDepth) {
|
|
2660
|
+
return;
|
|
2661
|
+
}
|
|
2662
|
+
const tm = (t0 + t1) * 0.5;
|
|
2663
|
+
if (!(tm > t0 && tm < t1)) {
|
|
2664
|
+
return;
|
|
2665
|
+
}
|
|
2666
|
+
const dm = decompose(tm);
|
|
2667
|
+
const lerpT = Vector3.Lerp(d0.t, d1.t, 0.5);
|
|
2668
|
+
const lerpS = Vector3.Lerp(d0.s, d1.s, 0.5);
|
|
2669
|
+
const slerpR = Quaternion.Slerp(d0.r, d1.r, 0.5);
|
|
2670
|
+
const posErr = Vector3.Distance(lerpT, dm.t);
|
|
2671
|
+
const sclErr = Vector3.Distance(lerpS, dm.s);
|
|
2672
|
+
const dot = Math.min(1, Math.abs(Quaternion.Dot(slerpR, dm.r)));
|
|
2673
|
+
const angErr = 2 * Math.acos(dot);
|
|
2674
|
+
const posTol = 1e-4 * (1 + Math.max(Math.abs(dm.t.x), Math.abs(dm.t.y), Math.abs(dm.t.z)));
|
|
2675
|
+
const sclTol = 1e-4 * (1 + Math.max(Math.abs(dm.s.x), Math.abs(dm.s.y), Math.abs(dm.s.z)));
|
|
2676
|
+
if (posErr > posTol || sclErr > sclTol || angErr > 0.0005) {
|
|
2677
|
+
refine(t0, tm, d0, dm, depth + 1);
|
|
2678
|
+
out.push(tm);
|
|
2679
|
+
refine(tm, t1, dm, d1, depth + 1);
|
|
2680
|
+
}
|
|
2681
|
+
};
|
|
2682
|
+
let prev = times.length > 0 ? decompose(times[0]) : null;
|
|
2683
|
+
for (let i = 0; i < times.length; i++) {
|
|
2684
|
+
out.push(times[i]);
|
|
2685
|
+
if (i + 1 < times.length && prev) {
|
|
2686
|
+
const next = decompose(times[i + 1]);
|
|
2687
|
+
refine(times[i], times[i + 1], prev, next, 0);
|
|
2688
|
+
prev = next;
|
|
2689
|
+
}
|
|
2690
|
+
}
|
|
2691
|
+
return out;
|
|
2692
|
+
}
|
|
2693
|
+
/**
|
|
2694
|
+
* Keys of a scalar property animated by one or more layers: the authored keys when a single layer drives it,
|
|
2695
|
+
* otherwise the frame grid evaluated through the layer stack.
|
|
2696
|
+
* @param sources - Per-layer curves of the property (each with at least one curve)
|
|
2697
|
+
* @param animStack - Stack being converted
|
|
2698
|
+
* @param mapValue - Conversion from the FBX value to the Babylon property value
|
|
2699
|
+
* @returns Animation keys
|
|
2700
|
+
*/
|
|
2701
|
+
_layeredScalarKeys(sources, animStack, mapValue) {
|
|
2702
|
+
const fps = this._frameRate;
|
|
2703
|
+
if (sources.length === 0) {
|
|
2704
|
+
return [];
|
|
2705
|
+
}
|
|
2706
|
+
if (sources.length === 1) {
|
|
2707
|
+
return buildScalarAnimationKeys(sources[0].curves[0], fps, animStack.startTime, animStack.stopTime, mapValue);
|
|
2708
|
+
}
|
|
2709
|
+
const channel = sources[0].curves[0].channel;
|
|
2710
|
+
const times = collectAnimationSampleTimes(sources.map((source) => ({ type: "other", targetModelId: 0, curves: Array.from(source.curves), layerIndex: source.layerIndex })), fps, animStack.startTime, animStack.stopTime);
|
|
2711
|
+
return times.map((time) => ({ frame: time * fps, value: mapValue(evaluateLayeredProperty(sources, animStack.layers, [channel], [0], time)[0]) }));
|
|
2712
|
+
}
|
|
2713
|
+
/**
|
|
2714
|
+
* Maps an animated FBX property (anything other than node transforms and blend shape weights) onto the Babylon
|
|
2715
|
+
* property that carries it: mesh visibility, camera field of view and clip planes, light intensity, colour and
|
|
2716
|
+
* cone angles, and material colours, alpha, roughness and metalness. `group` holds the curve nodes of every
|
|
2717
|
+
* layer animating that property, in layer order; several layers are evaluated through the layer stack.
|
|
2718
|
+
*/
|
|
2719
|
+
_buildPropertyAnimations(group, animStack, modelIdToNode, targets) {
|
|
2720
|
+
const fps = this._frameRate;
|
|
2721
|
+
const curveNode = group[0];
|
|
2722
|
+
const prop = curveNode.propertyName ?? curveNode.type;
|
|
2723
|
+
const targetId = curveNode.targetId;
|
|
2724
|
+
const out = [];
|
|
2725
|
+
const scalarCurve = curveNode.curves.find((c) => c.channel === "d|X") ?? curveNode.curves[0];
|
|
2726
|
+
const defaults = curveNode.defaultValues;
|
|
2727
|
+
const layered = group.length > 1;
|
|
2728
|
+
const sources = group.map((cn) => ({ layerIndex: cn.layerIndex, curves: cn.curves, defaultValues: cn.defaultValues }));
|
|
2729
|
+
const sampleTimes = () => collectAnimationSampleTimes(group.map((cn) => ({ type: "other", targetModelId: targetId, curves: cn.curves, layerIndex: cn.layerIndex })), fps, animStack.startTime, animStack.stopTime);
|
|
2730
|
+
const vectorSamples = (staticValue) => {
|
|
2731
|
+
const cx = curveNode.curves.find((c) => c.channel === "d|X");
|
|
2732
|
+
const cy = curveNode.curves.find((c) => c.channel === "d|Y");
|
|
2733
|
+
const cz = curveNode.curves.find((c) => c.channel === "d|Z");
|
|
2734
|
+
return sampleTimes().map((time) => ({
|
|
2735
|
+
frame: time * fps,
|
|
2736
|
+
values: layered
|
|
2737
|
+
? evaluateLayeredProperty(sources, animStack.layers, ["d|X", "d|Y", "d|Z"], staticValue, time)
|
|
2738
|
+
: [sampleFBXCurveAtTime(cx, time) ?? staticValue[0], sampleFBXCurveAtTime(cy, time) ?? staticValue[1], sampleFBXCurveAtTime(cz, time) ?? staticValue[2]],
|
|
2739
|
+
}));
|
|
2740
|
+
};
|
|
2741
|
+
const scalarKeys = (map, step = false) => {
|
|
2742
|
+
const keys = layered
|
|
2743
|
+
? sampleTimes().map((time) => ({
|
|
2744
|
+
frame: time * fps,
|
|
2745
|
+
value: map(evaluateLayeredProperty(sources, animStack.layers, [scalarCurve.channel], [defaults[scalarCurve.channel] ?? 0], time)[0]),
|
|
2746
|
+
}))
|
|
2747
|
+
: buildScalarAnimationKeys(scalarCurve, fps, animStack.startTime, animStack.stopTime, map);
|
|
2748
|
+
if (step) {
|
|
2749
|
+
for (const key of keys) {
|
|
2750
|
+
key.interpolation = 1 /* AnimationKeyInterpolation.STEP */;
|
|
2751
|
+
delete key.inTangent;
|
|
2752
|
+
delete key.outTangent;
|
|
2753
|
+
}
|
|
2754
|
+
}
|
|
2755
|
+
return keys;
|
|
2756
|
+
};
|
|
2757
|
+
const colorKeys = () => vectorSamples([defaults["d|X"] ?? 1, defaults["d|Y"] ?? 1, defaults["d|Z"] ?? 1]).map(({ frame, values }) => ({
|
|
2758
|
+
frame,
|
|
2759
|
+
value: new Color3(values[0], values[1], values[2]),
|
|
2760
|
+
}));
|
|
2761
|
+
const add = (target, property, type, keys, name) => {
|
|
2762
|
+
if (keys.length === 0) {
|
|
2763
|
+
return;
|
|
2764
|
+
}
|
|
2765
|
+
const animation = new Animation(`${name}_${property}`, property, fps, type, Animation.ANIMATIONLOOPMODE_CYCLE);
|
|
2766
|
+
animation.setKeys(keys);
|
|
2767
|
+
out.push({ animation, target });
|
|
2768
|
+
};
|
|
2769
|
+
const node = modelIdToNode.get(targetId);
|
|
2770
|
+
if (node) {
|
|
2771
|
+
if (prop === "Visibility" && node instanceof Mesh) {
|
|
2772
|
+
add(node, "visibility", Animation.ANIMATIONTYPE_FLOAT, scalarKeys((v) => Math.min(1, Math.max(0, v)), true), node.name);
|
|
2773
|
+
return out;
|
|
2774
|
+
}
|
|
2775
|
+
// Animated user properties drive the value stored in the node's metadata.
|
|
2776
|
+
const userProperties = node.metadata?.fbxUserProperties;
|
|
2777
|
+
if (userProperties && prop in userProperties) {
|
|
2778
|
+
const hasVector = curveNode.curves.some((c) => c.channel === "d|Y" || c.channel === "d|Z");
|
|
2779
|
+
if (hasVector) {
|
|
2780
|
+
const current = userProperties[prop];
|
|
2781
|
+
const base = Array.isArray(current) ? current : [0, 0, 0];
|
|
2782
|
+
userProperties[prop] = new Vector3(base[0] ?? 0, base[1] ?? 0, base[2] ?? 0);
|
|
2783
|
+
const keys = vectorSamples([base[0] ?? 0, base[1] ?? 0, base[2] ?? 0]).map(({ frame, values }) => ({
|
|
2784
|
+
frame,
|
|
2785
|
+
value: new Vector3(values[0], values[1], values[2]),
|
|
2786
|
+
}));
|
|
2787
|
+
add(node, `metadata.fbxUserProperties.${prop}`, Animation.ANIMATIONTYPE_VECTOR3, keys, node.name);
|
|
2788
|
+
}
|
|
2789
|
+
else {
|
|
2790
|
+
if (typeof userProperties[prop] === "boolean") {
|
|
2791
|
+
userProperties[prop] = userProperties[prop] ? 1 : 0;
|
|
2792
|
+
}
|
|
2793
|
+
add(node, `metadata.fbxUserProperties.${prop}`, Animation.ANIMATIONTYPE_FLOAT, scalarKeys((v) => v), node.name);
|
|
2794
|
+
}
|
|
2795
|
+
}
|
|
2796
|
+
return out;
|
|
2797
|
+
}
|
|
2798
|
+
const camera = targets.cameraByAttributeId.get(targetId);
|
|
2799
|
+
if (camera) {
|
|
2800
|
+
const meta = camera.metadata?.fbxCamera;
|
|
2801
|
+
const aspect = meta?.aspectRatio || 1;
|
|
2802
|
+
const apertureMode = meta?.apertureMode ?? 2;
|
|
2803
|
+
switch (prop) {
|
|
2804
|
+
case "FieldOfView":
|
|
2805
|
+
// Vertical unless the camera's aperture mode declares FieldOfView as horizontal.
|
|
2806
|
+
add(camera, "fov", Animation.ANIMATIONTYPE_FLOAT, scalarKeys((v) => (apertureMode === 1 ? 2 * Math.atan(Math.tan((v * Math.PI) / 360) / aspect) : (v * Math.PI) / 180)), camera.name);
|
|
2807
|
+
break;
|
|
2808
|
+
case "FieldOfViewY":
|
|
2809
|
+
add(camera, "fov", Animation.ANIMATIONTYPE_FLOAT, scalarKeys((v) => (v * Math.PI) / 180), camera.name);
|
|
2810
|
+
break;
|
|
2811
|
+
case "FieldOfViewX":
|
|
2812
|
+
if (apertureMode !== 0) {
|
|
2813
|
+
add(camera, "fov", Animation.ANIMATIONTYPE_FLOAT, scalarKeys((v) => 2 * Math.atan(Math.tan((v * Math.PI) / 360) / aspect)), camera.name);
|
|
2814
|
+
}
|
|
2815
|
+
break;
|
|
2816
|
+
case "FocalLength": {
|
|
2817
|
+
const apertureY = meta?.apertureSizeInch?.[1];
|
|
2818
|
+
if (apertureY && apertureMode === 3) {
|
|
2819
|
+
add(camera, "fov", Animation.ANIMATIONTYPE_FLOAT, scalarKeys((v) => 2 * Math.atan((apertureY / Math.max(v / 25.4, 1e-6)) * 0.5)), camera.name);
|
|
2820
|
+
}
|
|
2821
|
+
break;
|
|
2822
|
+
}
|
|
2823
|
+
case "NearPlane":
|
|
2824
|
+
add(camera, "minZ", Animation.ANIMATIONTYPE_FLOAT, scalarKeys((v) => v), camera.name);
|
|
2825
|
+
break;
|
|
2826
|
+
case "FarPlane":
|
|
2827
|
+
add(camera, "maxZ", Animation.ANIMATIONTYPE_FLOAT, scalarKeys((v) => v), camera.name);
|
|
2828
|
+
break;
|
|
2829
|
+
default:
|
|
2830
|
+
break;
|
|
2831
|
+
}
|
|
2832
|
+
return out;
|
|
2833
|
+
}
|
|
2834
|
+
const light = targets.lightByAttributeId.get(targetId);
|
|
2835
|
+
if (light) {
|
|
2836
|
+
switch (prop) {
|
|
2837
|
+
case "Intensity":
|
|
2838
|
+
add(light, "intensity", Animation.ANIMATIONTYPE_FLOAT, scalarKeys((v) => v / 100), light.name);
|
|
2839
|
+
break;
|
|
2840
|
+
case "Color":
|
|
2841
|
+
add(light, "diffuse", Animation.ANIMATIONTYPE_COLOR3, colorKeys(), light.name);
|
|
2842
|
+
break;
|
|
2843
|
+
case "OuterAngle":
|
|
2844
|
+
case "ConeAngle":
|
|
2845
|
+
if (light instanceof SpotLight) {
|
|
2846
|
+
add(light, "angle", Animation.ANIMATIONTYPE_FLOAT, scalarKeys((v) => (v * Math.PI) / 180), light.name);
|
|
2847
|
+
}
|
|
2848
|
+
break;
|
|
2849
|
+
case "InnerAngle":
|
|
2850
|
+
if (light instanceof SpotLight) {
|
|
2851
|
+
add(light, "innerAngle", Animation.ANIMATIONTYPE_FLOAT, scalarKeys((v) => (v * Math.PI) / 180), light.name);
|
|
2852
|
+
}
|
|
2853
|
+
break;
|
|
2854
|
+
default:
|
|
2855
|
+
break;
|
|
2856
|
+
}
|
|
2857
|
+
return out;
|
|
2858
|
+
}
|
|
2859
|
+
const material = targets.materialCache.get(targetId);
|
|
2860
|
+
if (material) {
|
|
2861
|
+
const isPbr = material instanceof PBRMaterial;
|
|
2862
|
+
const short = prop.substring(prop.lastIndexOf("|") + 1);
|
|
2863
|
+
if (prop === "DiffuseColor" || prop === "Diffuse" || short === "baseColor" || short === "base_color" || short === "color") {
|
|
2864
|
+
add(material, isPbr ? "albedoColor" : "diffuseColor", Animation.ANIMATIONTYPE_COLOR3, colorKeys(), material.name);
|
|
2865
|
+
}
|
|
2866
|
+
else if (prop === "EmissiveColor" ||
|
|
2867
|
+
prop === "Emissive" ||
|
|
2868
|
+
short === "emissionColor" ||
|
|
2869
|
+
short === "emission_color" ||
|
|
2870
|
+
short === "emissive" ||
|
|
2871
|
+
short === "emit_color") {
|
|
2872
|
+
add(material, "emissiveColor", Animation.ANIMATIONTYPE_COLOR3, colorKeys(), material.name);
|
|
2873
|
+
}
|
|
2874
|
+
else if (prop === "Opacity") {
|
|
2875
|
+
add(material, "alpha", Animation.ANIMATIONTYPE_FLOAT, scalarKeys((v) => v), material.name);
|
|
2876
|
+
}
|
|
2877
|
+
else if (prop === "TransparencyFactor" || prop === "TransparentFactor") {
|
|
2878
|
+
add(material, "alpha", Animation.ANIMATIONTYPE_FLOAT, scalarKeys((v) => 1 - v), material.name);
|
|
2879
|
+
}
|
|
2880
|
+
else if (isPbr && (short === "roughness" || short === "specular_roughness" || short === "specularRoughness")) {
|
|
2881
|
+
add(material, "roughness", Animation.ANIMATIONTYPE_FLOAT, scalarKeys((v) => v), material.name);
|
|
2882
|
+
}
|
|
2883
|
+
else if (isPbr && (short === "metalness" || short === "metallic" || short === "base_metalness")) {
|
|
2884
|
+
add(material, "metallic", Animation.ANIMATIONTYPE_FLOAT, scalarKeys((v) => v), material.name);
|
|
2885
|
+
}
|
|
2886
|
+
else if (!isPbr && prop === "SpecularColor") {
|
|
2887
|
+
add(material, "specularColor", Animation.ANIMATIONTYPE_COLOR3, colorKeys(), material.name);
|
|
2888
|
+
}
|
|
2889
|
+
return out;
|
|
2890
|
+
}
|
|
2891
|
+
return out;
|
|
2892
|
+
}
|
|
2893
|
+
/** Records constraints on their nodes and, unless disabled, attaches the runtime behavior that solves them. */
|
|
2894
|
+
_applyConstraints(fbxScene, modelIdToNode, rootNode, scene) {
|
|
2895
|
+
const nodeName = (id) => (id === undefined ? undefined : modelIdToNode.get(id)?.name);
|
|
2896
|
+
const upSign = fbxScene.upAxisSign >= 0 ? 1 : -1;
|
|
2897
|
+
const sceneUp = new Vector3(fbxScene.upAxis === 0 ? upSign : 0, fbxScene.upAxis === 1 ? upSign : 0, fbxScene.upAxis === 2 ? upSign : 0);
|
|
2898
|
+
const summaries = [];
|
|
2899
|
+
for (const c of fbxScene.constraints) {
|
|
2900
|
+
const summary = {
|
|
2901
|
+
name: c.name,
|
|
2902
|
+
type: c.type,
|
|
2903
|
+
node: nodeName(c.nodeId),
|
|
2904
|
+
targets: c.targets.map((t) => ({ node: nodeName(t.modelId), weight: t.weight })),
|
|
2905
|
+
weight: c.weight,
|
|
2906
|
+
active: c.active,
|
|
2907
|
+
...(c.type === "singleChainIK" ? { firstJoint: nodeName(c.ikFirstJointId), endJoint: nodeName(c.ikEndJointId), effector: nodeName(c.ikEffectorId) } : {}),
|
|
2908
|
+
};
|
|
2909
|
+
summaries.push(summary);
|
|
2910
|
+
const node = c.nodeId !== undefined ? modelIdToNode.get(c.nodeId) : undefined;
|
|
2911
|
+
if (!node) {
|
|
2912
|
+
continue;
|
|
2913
|
+
}
|
|
2914
|
+
const existing = (node.metadata?.fbxConstraints ?? []);
|
|
2915
|
+
node.metadata = { ...(node.metadata ?? {}), fbxConstraints: [...existing, summary] };
|
|
2916
|
+
if (this._options.constraints !== "apply" || c.type === "singleChainIK" || c.type === "unknown") {
|
|
2917
|
+
continue;
|
|
2918
|
+
}
|
|
2919
|
+
const targets = [];
|
|
2920
|
+
for (const t of c.targets) {
|
|
2921
|
+
const targetNode = modelIdToNode.get(t.modelId);
|
|
2922
|
+
if (!targetNode) {
|
|
2923
|
+
continue;
|
|
2924
|
+
}
|
|
2925
|
+
const d2r = Math.PI / 180;
|
|
2926
|
+
const offset = c.type === "parent"
|
|
2927
|
+
? Matrix.Compose(new Vector3(...t.offsetScale), Quaternion.FromRotationMatrix(eulerToMatrixXYZ(t.offsetRotation[0] * d2r, t.offsetRotation[1] * d2r, t.offsetRotation[2] * d2r)), new Vector3(...t.offsetTranslation))
|
|
2928
|
+
: Matrix.Identity();
|
|
2929
|
+
targets.push({ node: targetNode, weight: t.weight, offset });
|
|
2930
|
+
}
|
|
2931
|
+
if (targets.length === 0) {
|
|
2932
|
+
continue;
|
|
2933
|
+
}
|
|
2934
|
+
const upNode = c.worldUpNodeId !== undefined ? (modelIdToNode.get(c.worldUpNodeId) ?? null) : null;
|
|
2935
|
+
node.addBehavior(new FBXConstraintBehavior(c, { root: rootNode, targets, upNode, sceneUp }));
|
|
2936
|
+
}
|
|
2937
|
+
if (summaries.length > 0) {
|
|
2938
|
+
rootNode.metadata = { ...(rootNode.metadata ?? {}), fbxConstraints: summaries };
|
|
2939
|
+
}
|
|
2940
|
+
}
|
|
2941
|
+
/** Collects every recoverable issue the interpreter recorded, stores it on the root node and notifies the caller. */
|
|
2942
|
+
_reportDiagnostics(fbxScene, rootNode, modelIdToNode) {
|
|
2943
|
+
const warnings = [];
|
|
2944
|
+
const push = (source, d, objectName) => {
|
|
2945
|
+
const message = typeof d === "string" ? d : (d?.message ?? JSON.stringify(d));
|
|
2946
|
+
warnings.push({ source, message, objectName, details: typeof d === "string" ? undefined : d });
|
|
2947
|
+
};
|
|
2948
|
+
for (const d of fbxScene.diagnostics) {
|
|
2949
|
+
// Objects with several parents (shared materials, textures, instanced geometry) are normal in FBX.
|
|
2950
|
+
if (d.subType === "duplicate-parent") {
|
|
2951
|
+
continue;
|
|
2952
|
+
}
|
|
2953
|
+
push("scene", d, d.objectName ?? d.nodeName);
|
|
2954
|
+
}
|
|
2955
|
+
for (const g of fbxScene.geometries) {
|
|
2956
|
+
for (const d of g.diagnostics) {
|
|
2957
|
+
push("geometry", d, g.name);
|
|
2958
|
+
}
|
|
2959
|
+
}
|
|
2960
|
+
for (const c of fbxScene.curves) {
|
|
2961
|
+
for (const d of c.diagnostics) {
|
|
2962
|
+
push("geometry", d, c.name);
|
|
2963
|
+
}
|
|
2964
|
+
}
|
|
2965
|
+
for (const skin of fbxScene.skins) {
|
|
2966
|
+
for (const d of skin.diagnostics) {
|
|
2967
|
+
push("skin", d);
|
|
2968
|
+
}
|
|
2969
|
+
for (const bone of skin.bones) {
|
|
2970
|
+
for (const d of bone.diagnostics) {
|
|
2971
|
+
push("skin", d, bone.name);
|
|
2972
|
+
}
|
|
2973
|
+
}
|
|
2974
|
+
}
|
|
2975
|
+
for (const rig of fbxScene.rigs) {
|
|
2976
|
+
for (const w of rig.warnings) {
|
|
2977
|
+
push("rig", w);
|
|
2978
|
+
}
|
|
2979
|
+
}
|
|
2980
|
+
for (const stack of fbxScene.animations) {
|
|
2981
|
+
for (const d of stack.diagnostics) {
|
|
2982
|
+
if (d.type === "unsupported-curve-node" && d.curveNodeId !== undefined && this._evaluatedCurveNodeIds.has(d.curveNodeId)) {
|
|
2983
|
+
continue;
|
|
2984
|
+
}
|
|
2985
|
+
push("animation", d, stack.name);
|
|
2986
|
+
}
|
|
2987
|
+
}
|
|
2988
|
+
for (const cam of fbxScene.cameras) {
|
|
2989
|
+
for (const d of cam.diagnostics) {
|
|
2990
|
+
push("camera", d, cam.name);
|
|
2991
|
+
}
|
|
2992
|
+
}
|
|
2993
|
+
for (const light of fbxScene.lights) {
|
|
2994
|
+
for (const d of light.diagnostics) {
|
|
2995
|
+
push("light", d, light.name);
|
|
2996
|
+
}
|
|
2997
|
+
}
|
|
2998
|
+
const walk = (models) => {
|
|
2999
|
+
for (const m of models) {
|
|
3000
|
+
for (const d of m.diagnostics) {
|
|
3001
|
+
push("model", d, m.name);
|
|
3002
|
+
}
|
|
3003
|
+
walk(m.children);
|
|
3004
|
+
}
|
|
3005
|
+
};
|
|
3006
|
+
walk(fbxScene.rootModels);
|
|
3007
|
+
rootNode.metadata = {
|
|
3008
|
+
...(rootNode.metadata ?? {}),
|
|
3009
|
+
fbxDiagnostics: warnings,
|
|
3010
|
+
fbxDisplayLayers: fbxScene.displayLayers.map((l) => ({ name: l.name, show: l.show, freeze: l.freeze, color: l.color })),
|
|
3011
|
+
fbxSelectionSets: fbxScene.selectionSets.map((set) => ({
|
|
3012
|
+
name: set.name,
|
|
3013
|
+
members: set.members.map((m) => ({
|
|
3014
|
+
node: modelIdToNode.get(m.modelId)?.name ?? String(m.modelId),
|
|
3015
|
+
includeNode: m.includeNode,
|
|
3016
|
+
...(m.vertices ? { vertices: m.vertices } : {}),
|
|
3017
|
+
...(m.edges ? { edges: m.edges } : {}),
|
|
3018
|
+
...(m.faces ? { faces: m.faces } : {}),
|
|
3019
|
+
})),
|
|
3020
|
+
})),
|
|
3021
|
+
};
|
|
3022
|
+
for (const warning of warnings) {
|
|
3023
|
+
this._options.onWarning(warning);
|
|
3024
|
+
}
|
|
3025
|
+
}
|
|
3026
|
+
/** Curve nodes affecting the inherit scale of a model: its own scale curves and those of its inherit-scale chain. */
|
|
3027
|
+
_collectInheritScaleCurves(modelData) {
|
|
3028
|
+
const result = [];
|
|
3029
|
+
let current = modelData;
|
|
3030
|
+
const seen = new Set();
|
|
3031
|
+
while (current && !seen.has(current.id)) {
|
|
3032
|
+
seen.add(current.id);
|
|
3033
|
+
for (const cn of this._curveNodesByModelId.get(current.id) ?? []) {
|
|
3034
|
+
if (cn.type === "S") {
|
|
3035
|
+
result.push(cn);
|
|
3036
|
+
}
|
|
3037
|
+
}
|
|
3038
|
+
current = this._getInheritScaleNode(current);
|
|
3039
|
+
}
|
|
3040
|
+
return result;
|
|
3041
|
+
}
|
|
1958
3042
|
_isVector3KeysConstant(keys) {
|
|
1959
3043
|
if (keys.length < 2) {
|
|
1960
3044
|
return true;
|
|
@@ -1968,43 +3052,126 @@ export class FBXFileLoader {
|
|
|
1968
3052
|
}
|
|
1969
3053
|
return true;
|
|
1970
3054
|
}
|
|
3055
|
+
/** Samples the animated Lcl Translation / Rotation / Scaling of a model, blending all layers of the active stack. */
|
|
3056
|
+
_sampleModelTRS(modelData, curveNodes, time) {
|
|
3057
|
+
const layers = this._activeLayers;
|
|
3058
|
+
return {
|
|
3059
|
+
translation: evaluateLayeredChannel(curveNodes, layers, "T", modelData.translation, modelData.rotationOrder, time),
|
|
3060
|
+
rotation: evaluateLayeredChannel(curveNodes, layers, "R", modelData.rotation, modelData.rotationOrder, time),
|
|
3061
|
+
scale: evaluateLayeredChannel(curveNodes, layers, "S", modelData.scale, modelData.rotationOrder, time),
|
|
3062
|
+
};
|
|
3063
|
+
}
|
|
3064
|
+
/**
|
|
3065
|
+
* Local position/rotation/scale of a model from FBX Lcl values. Without pivots and offsets the components map
|
|
3066
|
+
* directly (rotation = pre * lcl * post⁻¹), which keeps zero and negative scales exact. With pivots the full
|
|
3067
|
+
* matrix is built and decomposed.
|
|
3068
|
+
*/
|
|
3069
|
+
static _computeLocalTRS(modelData, translation, rotation, scale) {
|
|
3070
|
+
const nz = (v) => v[0] !== 0 || v[1] !== 0 || v[2] !== 0;
|
|
3071
|
+
const d2r = Math.PI / 180;
|
|
3072
|
+
let rotM = eulerToMatrix(rotation[0] * d2r, rotation[1] * d2r, rotation[2] * d2r, modelData.rotationOrder);
|
|
3073
|
+
if (nz(modelData.preRotation)) {
|
|
3074
|
+
rotM = rotM.multiply(eulerToMatrixXYZ(modelData.preRotation[0] * d2r, modelData.preRotation[1] * d2r, modelData.preRotation[2] * d2r));
|
|
3075
|
+
}
|
|
3076
|
+
if (nz(modelData.postRotation)) {
|
|
3077
|
+
const post = eulerToMatrixXYZ(modelData.postRotation[0] * d2r, modelData.postRotation[1] * d2r, modelData.postRotation[2] * d2r);
|
|
3078
|
+
const postInv = new Matrix();
|
|
3079
|
+
post.invertToRef(postInv);
|
|
3080
|
+
rotM = postInv.multiply(rotM);
|
|
3081
|
+
}
|
|
3082
|
+
let position = new Vector3(translation[0], translation[1], translation[2]);
|
|
3083
|
+
if (nz(modelData.rotationPivot) || nz(modelData.scalingPivot) || nz(modelData.rotationOffset) || nz(modelData.scalingOffset)) {
|
|
3084
|
+
// Pivots and offsets only move the translation: the linear part of the FBX local matrix is always S·R.
|
|
3085
|
+
position = FBXFileLoader._computeFBXLocalMatrix(translation, rotation, scale, modelData.preRotation, modelData.postRotation, modelData.rotationPivot, modelData.scalingPivot, modelData.rotationOffset, modelData.scalingOffset, modelData.rotationOrder).getTranslation();
|
|
3086
|
+
}
|
|
3087
|
+
return {
|
|
3088
|
+
position,
|
|
3089
|
+
rotationQuaternion: Quaternion.FromRotationMatrix(rotM),
|
|
3090
|
+
scaling: new Vector3(scale[0], scale[1], scale[2]),
|
|
3091
|
+
};
|
|
3092
|
+
}
|
|
3093
|
+
/** Applies inherit-mode adjustments to a local TRS (see _computeInheritAwareLocalMatrix). */
|
|
3094
|
+
_adjustTRSForInheritMode(modelData, trs, time) {
|
|
3095
|
+
const parent = this._parentModelById.get(modelData.id);
|
|
3096
|
+
if (modelData.inheritType === 1 || !parent) {
|
|
3097
|
+
return;
|
|
3098
|
+
}
|
|
3099
|
+
const parentScale = this._getInheritScale(parent, time);
|
|
3100
|
+
trs.position.x *= parentScale[0];
|
|
3101
|
+
trs.position.y *= parentScale[1];
|
|
3102
|
+
trs.position.z *= parentScale[2];
|
|
3103
|
+
const inheritNode = this._getInheritScaleNode(modelData);
|
|
3104
|
+
if (inheritNode) {
|
|
3105
|
+
const inherited = this._getInheritScale(inheritNode, time);
|
|
3106
|
+
trs.scaling.x *= inherited[0];
|
|
3107
|
+
trs.scaling.y *= inherited[1];
|
|
3108
|
+
trs.scaling.z *= inherited[2];
|
|
3109
|
+
}
|
|
3110
|
+
}
|
|
1971
3111
|
_sampleModelLocalMatrix(modelData, curveNodes, time, scaleOverride) {
|
|
1972
|
-
const
|
|
1973
|
-
|
|
1974
|
-
const sNode = curveNodes.find((cn) => cn.type === "S");
|
|
1975
|
-
const txCurve = tNode?.curves.find((c) => c.channel === "d|X");
|
|
1976
|
-
const tyCurve = tNode?.curves.find((c) => c.channel === "d|Y");
|
|
1977
|
-
const tzCurve = tNode?.curves.find((c) => c.channel === "d|Z");
|
|
1978
|
-
const rxCurve = rNode?.curves.find((c) => c.channel === "d|X");
|
|
1979
|
-
const ryCurve = rNode?.curves.find((c) => c.channel === "d|Y");
|
|
1980
|
-
const rzCurve = rNode?.curves.find((c) => c.channel === "d|Z");
|
|
1981
|
-
const sxCurve = sNode?.curves.find((c) => c.channel === "d|X");
|
|
1982
|
-
const syCurve = sNode?.curves.find((c) => c.channel === "d|Y");
|
|
1983
|
-
const szCurve = sNode?.curves.find((c) => c.channel === "d|Z");
|
|
1984
|
-
return FBXFileLoader._computeFBXLocalMatrix([
|
|
1985
|
-
sampleFBXCurveAtTime(txCurve, time) ?? modelData.translation[0],
|
|
1986
|
-
sampleFBXCurveAtTime(tyCurve, time) ?? modelData.translation[1],
|
|
1987
|
-
sampleFBXCurveAtTime(tzCurve, time) ?? modelData.translation[2],
|
|
1988
|
-
], [
|
|
1989
|
-
sampleFBXCurveAtTime(rxCurve, time) ?? modelData.rotation[0],
|
|
1990
|
-
sampleFBXCurveAtTime(ryCurve, time) ?? modelData.rotation[1],
|
|
1991
|
-
sampleFBXCurveAtTime(rzCurve, time) ?? modelData.rotation[2],
|
|
1992
|
-
], scaleOverride ?? [
|
|
1993
|
-
sampleFBXCurveAtTime(sxCurve, time) ?? modelData.scale[0],
|
|
1994
|
-
sampleFBXCurveAtTime(syCurve, time) ?? modelData.scale[1],
|
|
1995
|
-
sampleFBXCurveAtTime(szCurve, time) ?? modelData.scale[2],
|
|
1996
|
-
], modelData.preRotation, modelData.postRotation, modelData.rotationPivot, modelData.scalingPivot, modelData.rotationOffset, modelData.scalingOffset, modelData.rotationOrder);
|
|
3112
|
+
const trs = this._sampleModelTRS(modelData, curveNodes, time);
|
|
3113
|
+
return FBXFileLoader._computeFBXLocalMatrix(trs.translation, trs.rotation, scaleOverride ?? trs.scale, modelData.preRotation, modelData.postRotation, modelData.rotationPivot, modelData.scalingPivot, modelData.rotationOffset, modelData.scalingOffset, modelData.rotationOrder);
|
|
1997
3114
|
}
|
|
1998
3115
|
_sampleModelScale(modelData, curveNodes, time) {
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
3116
|
+
return evaluateLayeredChannel(curveNodes, this._activeLayers, "S", modelData.scale, modelData.rotationOrder, time);
|
|
3117
|
+
}
|
|
3118
|
+
/**
|
|
3119
|
+
* Effective scale of a model for inherit-mode math (`inherit_scale` in ufbx terms): its own local scale, multiplied
|
|
3120
|
+
* componentwise by the inherited scale when the model uses RrSs inheritance. `time` samples animation; undefined
|
|
3121
|
+
* uses the rest pose.
|
|
3122
|
+
*/
|
|
3123
|
+
_getInheritScale(modelData, time) {
|
|
3124
|
+
if (!modelData) {
|
|
3125
|
+
return [1, 1, 1];
|
|
3126
|
+
}
|
|
3127
|
+
const own = time === undefined ? modelData.scale : this._sampleModelScale(modelData, this._curveNodesByModelId.get(modelData.id) ?? [], time);
|
|
3128
|
+
const inheritNode = this._getInheritScaleNode(modelData);
|
|
3129
|
+
if (!inheritNode) {
|
|
3130
|
+
return [own[0], own[1], own[2]];
|
|
3131
|
+
}
|
|
3132
|
+
const parentScale = this._getInheritScale(inheritNode, time);
|
|
3133
|
+
return [own[0] * parentScale[0], own[1] * parentScale[1], own[2] * parentScale[2]];
|
|
3134
|
+
}
|
|
3135
|
+
/** RrSs nodes inherit scale from their parent; Rrs nodes skip their immediate parent (chaining through Rrs parents). */
|
|
3136
|
+
_getInheritScaleNode(modelData) {
|
|
3137
|
+
const parent = this._parentModelById.get(modelData.id);
|
|
3138
|
+
if (!parent) {
|
|
3139
|
+
return undefined;
|
|
3140
|
+
}
|
|
3141
|
+
if (modelData.inheritType === 0) {
|
|
3142
|
+
return parent;
|
|
3143
|
+
}
|
|
3144
|
+
if (modelData.inheritType === 2) {
|
|
3145
|
+
return this._getInheritScaleNode(parent);
|
|
3146
|
+
}
|
|
3147
|
+
return undefined;
|
|
3148
|
+
}
|
|
3149
|
+
/**
|
|
3150
|
+
* Local matrix of a model relative to its Babylon parent frame, accounting for inherit modes. For RSrs (the
|
|
3151
|
+
* default) this is the FBX local matrix. For RrSs / Rrs the node sits under a helper that removes the parent's
|
|
3152
|
+
* scale, so translation is pre-scaled by the parent scale and (for RrSs) scale accumulates componentwise.
|
|
3153
|
+
*/
|
|
3154
|
+
_computeInheritAwareLocalMatrix(modelData, localMatrix, time) {
|
|
3155
|
+
const parent = this._parentModelById.get(modelData.id);
|
|
3156
|
+
if (modelData.inheritType === 1 || !parent) {
|
|
3157
|
+
return localMatrix;
|
|
3158
|
+
}
|
|
3159
|
+
const s = new Vector3();
|
|
3160
|
+
const r = new Quaternion();
|
|
3161
|
+
const t = new Vector3();
|
|
3162
|
+
localMatrix.decompose(s, r, t);
|
|
3163
|
+
const parentScale = this._getInheritScale(parent, time);
|
|
3164
|
+
t.x *= parentScale[0];
|
|
3165
|
+
t.y *= parentScale[1];
|
|
3166
|
+
t.z *= parentScale[2];
|
|
3167
|
+
const inheritNode = this._getInheritScaleNode(modelData);
|
|
3168
|
+
if (inheritNode) {
|
|
3169
|
+
const inherited = this._getInheritScale(inheritNode, time);
|
|
3170
|
+
s.x *= inherited[0];
|
|
3171
|
+
s.y *= inherited[1];
|
|
3172
|
+
s.z *= inherited[2];
|
|
3173
|
+
}
|
|
3174
|
+
return Matrix.Compose(s, r, t);
|
|
2008
3175
|
}
|
|
2009
3176
|
/**
|
|
2010
3177
|
* Build matrix-baked bone animation from full FBX local transforms.
|
|
@@ -2012,25 +3179,13 @@ export class FBXFileLoader {
|
|
|
2012
3179
|
* the same FBX local transform chain as the source skeleton.
|
|
2013
3180
|
*/
|
|
2014
3181
|
_buildBoneAnimations(curveNodes, boneName, modelData, startTime, stopTime, bindLocalMatrix) {
|
|
2015
|
-
const fps =
|
|
2016
|
-
// Separate curves by type
|
|
2017
|
-
const tNode = curveNodes.find((cn) => cn.type === "T");
|
|
3182
|
+
const fps = this._frameRate;
|
|
2018
3183
|
const rNode = curveNodes.find((cn) => cn.type === "R");
|
|
2019
|
-
const
|
|
2020
|
-
|
|
2021
|
-
if (times.length === 0) {
|
|
3184
|
+
const baseTimes = collectAnimationSampleTimes(curveNodes, fps, startTime, stopTime);
|
|
3185
|
+
if (baseTimes.length === 0) {
|
|
2022
3186
|
return [];
|
|
2023
3187
|
}
|
|
2024
|
-
|
|
2025
|
-
const txCurve = tNode?.curves.find((c) => c.channel === "d|X");
|
|
2026
|
-
const tyCurve = tNode?.curves.find((c) => c.channel === "d|Y");
|
|
2027
|
-
const tzCurve = tNode?.curves.find((c) => c.channel === "d|Z");
|
|
2028
|
-
const rxCurve = rNode?.curves.find((c) => c.channel === "d|X");
|
|
2029
|
-
const ryCurve = rNode?.curves.find((c) => c.channel === "d|Y");
|
|
2030
|
-
const rzCurve = rNode?.curves.find((c) => c.channel === "d|Z");
|
|
2031
|
-
const sxCurve = sNode?.curves.find((c) => c.channel === "d|X");
|
|
2032
|
-
const syCurve = sNode?.curves.find((c) => c.channel === "d|Y");
|
|
2033
|
-
const szCurve = sNode?.curves.find((c) => c.channel === "d|Z");
|
|
3188
|
+
const times = FBXFileLoader._refineSampleTimes(baseTimes, (time) => this._sampleModelLocalMatrix(modelData, curveNodes, time));
|
|
2034
3189
|
const posKeys = [];
|
|
2035
3190
|
const rotKeys = [];
|
|
2036
3191
|
const sclKeys = [];
|
|
@@ -2043,18 +3198,8 @@ export class FBXFileLoader {
|
|
|
2043
3198
|
}
|
|
2044
3199
|
for (const time of times) {
|
|
2045
3200
|
const frame = time * fps;
|
|
2046
|
-
//
|
|
2047
|
-
const
|
|
2048
|
-
const ty = sampleFBXCurveAtTime(tyCurve, time) ?? modelData.translation[1];
|
|
2049
|
-
const tz = sampleFBXCurveAtTime(tzCurve, time) ?? modelData.translation[2];
|
|
2050
|
-
const rx = sampleFBXCurveAtTime(rxCurve, time) ?? modelData.rotation[0];
|
|
2051
|
-
const ry = sampleFBXCurveAtTime(ryCurve, time) ?? modelData.rotation[1];
|
|
2052
|
-
const rz = sampleFBXCurveAtTime(rzCurve, time) ?? modelData.rotation[2];
|
|
2053
|
-
const sx = sampleFBXCurveAtTime(sxCurve, time) ?? modelData.scale[0];
|
|
2054
|
-
const sy = sampleFBXCurveAtTime(syCurve, time) ?? modelData.scale[1];
|
|
2055
|
-
const sz = sampleFBXCurveAtTime(szCurve, time) ?? modelData.scale[2];
|
|
2056
|
-
// Compute the full FBX local matrix from animated Lcl values
|
|
2057
|
-
const localMatrix = FBXFileLoader._computeFBXLocalMatrix([tx, ty, tz], [rx, ry, rz], [sx, sy, sz], modelData.preRotation, modelData.postRotation, modelData.rotationPivot, modelData.scalingPivot, modelData.rotationOffset, modelData.scalingOffset, modelData.rotationOrder);
|
|
3201
|
+
// Compute the full FBX local matrix from animated Lcl values, blending all layers
|
|
3202
|
+
const localMatrix = this._sampleModelLocalMatrix(modelData, curveNodes, time);
|
|
2058
3203
|
const correctedLocalMatrix = restLocalInverse && bindLocalMatrix ? bindLocalMatrix.multiply(restLocalInverse).multiply(localMatrix) : localMatrix;
|
|
2059
3204
|
const s = new Vector3();
|
|
2060
3205
|
const r = new Quaternion();
|
|
@@ -2372,17 +3517,31 @@ function calculateBlendShapeInfluences(deformPercent, fullWeights, shapeCount) {
|
|
|
2372
3517
|
function clamp01(value) {
|
|
2373
3518
|
return Math.max(0, Math.min(1, value));
|
|
2374
3519
|
}
|
|
3520
|
+
/** Interior frames kept between two keys when the segment is cubic or extrapolated. */
|
|
3521
|
+
const MAX_DENSE_SEGMENT_SAMPLES = 512;
|
|
3522
|
+
/** Interior frames kept between two keys when every curve is linear or constant there. */
|
|
3523
|
+
const MAX_LINEAR_SEGMENT_SAMPLES = 32;
|
|
3524
|
+
/** Upper bound on samples per animated node, beyond which the frame stride grows. */
|
|
3525
|
+
const MAX_SAMPLES_PER_TARGET = 100000;
|
|
3526
|
+
/**
|
|
3527
|
+
* Times at which the animation is baked: every key time plus the frame grid between keys. Segments where every
|
|
3528
|
+
* curve is linear or constant only get a few interior frames (Babylon interpolates linearly between the baked
|
|
3529
|
+
* keys), cubic or extrapolated segments get the full frame grid up to a cap, so day-long clips with a handful of
|
|
3530
|
+
* keys do not explode into millions of samples.
|
|
3531
|
+
*/
|
|
2375
3532
|
function collectAnimationSampleTimes(curveNodes, fps, startTime, stopTime) {
|
|
2376
3533
|
let minTime = Number.POSITIVE_INFINITY;
|
|
2377
3534
|
let maxTime = Number.NEGATIVE_INFINITY;
|
|
2378
|
-
const
|
|
3535
|
+
const anchorSet = new Set();
|
|
3536
|
+
const curves = [];
|
|
2379
3537
|
for (const curveNode of curveNodes) {
|
|
2380
3538
|
for (const curve of curveNode.curves) {
|
|
3539
|
+
curves.push(curve);
|
|
2381
3540
|
for (const key of curve.keys) {
|
|
2382
3541
|
minTime = Math.min(minTime, key.time);
|
|
2383
3542
|
maxTime = Math.max(maxTime, key.time);
|
|
2384
3543
|
if (key.time >= startTime && key.time <= stopTime) {
|
|
2385
|
-
|
|
3544
|
+
anchorSet.add(key.time);
|
|
2386
3545
|
}
|
|
2387
3546
|
}
|
|
2388
3547
|
}
|
|
@@ -2392,13 +3551,81 @@ function collectAnimationSampleTimes(curveNodes, fps, startTime, stopTime) {
|
|
|
2392
3551
|
}
|
|
2393
3552
|
const rangeStart = stopTime > startTime ? startTime : minTime;
|
|
2394
3553
|
const rangeStop = stopTime > startTime ? stopTime : maxTime;
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
const
|
|
2398
|
-
|
|
2399
|
-
|
|
3554
|
+
anchorSet.add(rangeStart);
|
|
3555
|
+
anchorSet.add(rangeStop);
|
|
3556
|
+
const anchors = Array.from(anchorSet)
|
|
3557
|
+
.filter((t) => t >= rangeStart && t <= rangeStop)
|
|
3558
|
+
.sort((a, b) => a - b);
|
|
3559
|
+
// A segment needs the dense grid when any curve is cubic there or extrapolates a non-constant pattern.
|
|
3560
|
+
const segmentIsDense = (t0) => {
|
|
3561
|
+
for (const curve of curves) {
|
|
3562
|
+
const keys = curve.keys;
|
|
3563
|
+
if (keys.length === 0) {
|
|
3564
|
+
continue;
|
|
3565
|
+
}
|
|
3566
|
+
if (t0 < keys[0].time) {
|
|
3567
|
+
if (curve.preExtrapolation && curve.preExtrapolation.mode !== "constant") {
|
|
3568
|
+
return true;
|
|
3569
|
+
}
|
|
3570
|
+
continue;
|
|
3571
|
+
}
|
|
3572
|
+
if (t0 >= keys[keys.length - 1].time) {
|
|
3573
|
+
if (curve.postExtrapolation && curve.postExtrapolation.mode !== "constant") {
|
|
3574
|
+
return true;
|
|
3575
|
+
}
|
|
3576
|
+
continue;
|
|
3577
|
+
}
|
|
3578
|
+
let lo = 0;
|
|
3579
|
+
let hi = keys.length - 1;
|
|
3580
|
+
while (hi - lo > 1) {
|
|
3581
|
+
const mid = (lo + hi) >> 1;
|
|
3582
|
+
if (keys[mid].time <= t0) {
|
|
3583
|
+
lo = mid;
|
|
3584
|
+
}
|
|
3585
|
+
else {
|
|
3586
|
+
hi = mid;
|
|
3587
|
+
}
|
|
3588
|
+
}
|
|
3589
|
+
if (keys[lo].interpolation === "cubic") {
|
|
3590
|
+
return true;
|
|
3591
|
+
}
|
|
3592
|
+
}
|
|
3593
|
+
return false;
|
|
3594
|
+
};
|
|
3595
|
+
const times = [];
|
|
3596
|
+
const totalFrames = Math.floor(rangeStop * fps) - Math.ceil(rangeStart * fps) + 1;
|
|
3597
|
+
const stride = Math.max(1, Math.ceil(totalFrames / MAX_SAMPLES_PER_TARGET));
|
|
3598
|
+
for (let i = 0; i < anchors.length; i++) {
|
|
3599
|
+
const t0 = anchors[i];
|
|
3600
|
+
times.push(t0);
|
|
3601
|
+
if (i + 1 >= anchors.length) {
|
|
3602
|
+
break;
|
|
3603
|
+
}
|
|
3604
|
+
const t1 = anchors[i + 1];
|
|
3605
|
+
const firstFrame = Math.floor(t0 * fps) + 1;
|
|
3606
|
+
const lastFrame = Math.ceil(t1 * fps) - 1;
|
|
3607
|
+
const interior = lastFrame - firstFrame + 1;
|
|
3608
|
+
if (interior <= 0) {
|
|
3609
|
+
continue;
|
|
3610
|
+
}
|
|
3611
|
+
const cap = segmentIsDense(t0) ? MAX_DENSE_SEGMENT_SAMPLES : MAX_LINEAR_SEGMENT_SAMPLES;
|
|
3612
|
+
const step = Math.max(stride, Math.ceil(interior / cap));
|
|
3613
|
+
for (let frame = firstFrame; frame <= lastFrame; frame += step) {
|
|
3614
|
+
times.push(frame / fps);
|
|
3615
|
+
}
|
|
3616
|
+
}
|
|
3617
|
+
const sorted = times.sort((a, b) => a - b);
|
|
3618
|
+
// Key times and frame times can differ by floating point noise; keep one sample per instant.
|
|
3619
|
+
const deduped = [];
|
|
3620
|
+
for (const t of sorted) {
|
|
3621
|
+
if (t < rangeStart - 1e-9 || t > rangeStop + 1e-9) {
|
|
3622
|
+
continue;
|
|
3623
|
+
}
|
|
3624
|
+
if (deduped.length === 0 || t - deduped[deduped.length - 1] > 1e-9) {
|
|
3625
|
+
deduped.push(t);
|
|
3626
|
+
}
|
|
2400
3627
|
}
|
|
2401
|
-
return
|
|
3628
|
+
return deduped;
|
|
2402
3629
|
}
|
|
2403
3630
|
function areQuaternionKeysConstant(keys) {
|
|
2404
3631
|
if (keys.length < 2) {
|
|
@@ -2428,6 +3655,10 @@ function buildScalarAnimationKeys(curve, fps, startTime, stopTime, mapValue) {
|
|
|
2428
3655
|
time: range.start,
|
|
2429
3656
|
value: sampleFBXCurveAtTime(curve, range.start) ?? 0,
|
|
2430
3657
|
interpolation: "linear",
|
|
3658
|
+
leftDx: 0,
|
|
3659
|
+
leftDy: 0,
|
|
3660
|
+
rightDx: 0,
|
|
3661
|
+
rightDy: 0,
|
|
2431
3662
|
},
|
|
2432
3663
|
frame: range.start * fps,
|
|
2433
3664
|
value: mapValue(sampleFBXCurveAtTime(curve, range.start) ?? 0),
|
|
@@ -2439,6 +3670,10 @@ function buildScalarAnimationKeys(curve, fps, startTime, stopTime, mapValue) {
|
|
|
2439
3670
|
time: range.stop,
|
|
2440
3671
|
value: sampleFBXCurveAtTime(curve, range.stop) ?? 0,
|
|
2441
3672
|
interpolation: "linear",
|
|
3673
|
+
leftDx: 0,
|
|
3674
|
+
leftDy: 0,
|
|
3675
|
+
rightDx: 0,
|
|
3676
|
+
rightDy: 0,
|
|
2442
3677
|
},
|
|
2443
3678
|
frame: range.stop * fps,
|
|
2444
3679
|
value: mapValue(sampleFBXCurveAtTime(curve, range.stop) ?? 0),
|