@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,18 +1,38 @@
|
|
|
1
1
|
/* eslint-disable @typescript-eslint/naming-convention, jsdoc/require-param, jsdoc/require-returns */
|
|
2
|
-
import {
|
|
2
|
+
import { extractNodeTransform } from "./nodeTransform.js";
|
|
3
|
+
import { extractUserProperties, getPropertyEntries, extractPropertyTemplates, getPropertyTemplate, resolveNumberProperty, resolvePropertyValue, resolveVector3Property, } from "./propertyTemplates.js";
|
|
4
|
+
import { findDocumentNode, findChildByName, getPropertyValue, cleanFBXName, getNodeArray } from "../types/fbxTypes.js";
|
|
3
5
|
import { resolveConnections, getChildren } from "./connections.js";
|
|
4
6
|
import { extractGeometry } from "./geometry.js";
|
|
5
|
-
import {
|
|
7
|
+
import { extractLineGeometry, extractNurbsCurveGeometry, nurbsSurfaceToGeometry } from "./nurbs.js";
|
|
8
|
+
import { isLegacyDocument, upgradeLegacyDocument } from "./legacyDocument.js";
|
|
9
|
+
import { extractConstraints } from "./constraints.js";
|
|
10
|
+
import { extractMaterial, extractTextureRef } from "./materials.js";
|
|
6
11
|
import { extractSkins } from "./skeleton.js";
|
|
7
12
|
import { resolveRigs } from "./rig.js";
|
|
8
13
|
import { extractAnimations } from "./animation.js";
|
|
9
14
|
import { extractBlendShapes } from "./blendShapes.js";
|
|
10
15
|
import { extractSceneDiagnostics } from "./sceneDiagnostics.js";
|
|
11
|
-
import { extractPropertyTemplates, getPropertyTemplate, resolveNumberProperty, resolvePropertyValue, resolveVector3Property, } from "./propertyTemplates.js";
|
|
12
16
|
/**
|
|
13
|
-
*
|
|
17
|
+
* Tessellated NURBS vertices carry no control point mapping, so skin clusters and blend shapes bound to the surface
|
|
18
|
+
* cannot be applied; they are reported and the surface is imported undeformed.
|
|
14
19
|
*/
|
|
15
|
-
|
|
20
|
+
function flagNurbsDeformers(objectMap, geometryId, geometry) {
|
|
21
|
+
const kinds = getChildren(objectMap, geometryId, "Deformer")
|
|
22
|
+
.map((d) => getPropertyValue(d.node, 2) ?? "")
|
|
23
|
+
.filter((kind) => kind === "Skin" || kind === "BlendShape");
|
|
24
|
+
if (kinds.length === 0) {
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
geometry.diagnostics.push({
|
|
28
|
+
type: "nurbs-deformer-ignored",
|
|
29
|
+
message: `${Array.from(new Set(kinds)).join(" and ")} deformer on NURBS surface '${geometry.name}' is ignored: the tessellated mesh has no control point mapping.`,
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
export function interpretFBX(doc, options = {}) {
|
|
33
|
+
if (isLegacyDocument(doc)) {
|
|
34
|
+
doc = upgradeLegacyDocument(doc);
|
|
35
|
+
}
|
|
16
36
|
const objectMap = resolveConnections(doc);
|
|
17
37
|
const propertyTemplates = extractPropertyTemplates(doc);
|
|
18
38
|
// Extract global settings
|
|
@@ -24,32 +44,70 @@ export function interpretFBX(doc) {
|
|
|
24
44
|
materials.push(extractMaterial(node, id, objectMap, propertyTemplates));
|
|
25
45
|
}
|
|
26
46
|
}
|
|
47
|
+
attachLegacyLayerTextures(objectMap, materials, propertyTemplates);
|
|
27
48
|
// Extract all geometries
|
|
28
49
|
const geometries = [];
|
|
50
|
+
const curves = [];
|
|
29
51
|
for (const [id, node] of Array.from(objectMap.objects)) {
|
|
30
52
|
if (node.name === "Geometry") {
|
|
31
53
|
const subType = getPropertyValue(node, 2);
|
|
32
54
|
if (subType === "Mesh") {
|
|
33
|
-
|
|
55
|
+
const geometry = extractGeometry(node, id);
|
|
56
|
+
// Edge-only or empty meshes have nothing to render; keep the node but drop the mesh.
|
|
57
|
+
if (geometry.positions.length > 0 && geometry.indices.length > 0) {
|
|
58
|
+
geometries.push(geometry);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
else if (subType === "NurbsSurface") {
|
|
62
|
+
const geometry = nurbsSurfaceToGeometry(node, id, options.nurbsSubdivision);
|
|
63
|
+
if (geometry) {
|
|
64
|
+
flagNurbsDeformers(objectMap, id, geometry);
|
|
65
|
+
geometries.push(geometry);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
else if (subType === "TrimNurbsSurface") {
|
|
69
|
+
// The trimmed surface is rendered untrimmed: the underlying NurbsSurface is a child of the trim.
|
|
70
|
+
const surfaceChild = getChildren(objectMap, id, "Geometry").find((child) => getPropertyValue(child.node, 2) === "NurbsSurface");
|
|
71
|
+
const geometry = surfaceChild ? nurbsSurfaceToGeometry(surfaceChild.node, id, options.nurbsSubdivision) : null;
|
|
72
|
+
if (geometry) {
|
|
73
|
+
flagNurbsDeformers(objectMap, id, geometry);
|
|
74
|
+
geometry.diagnostics.push({
|
|
75
|
+
type: "nurbs-trim-ignored",
|
|
76
|
+
message: `Trim curves of NURBS surface '${geometry.name}' are ignored; the surface is rendered untrimmed.`,
|
|
77
|
+
});
|
|
78
|
+
geometries.push(geometry);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
else if (subType === "NurbsCurve") {
|
|
82
|
+
curves.push(extractNurbsCurveGeometry(node, id));
|
|
83
|
+
}
|
|
84
|
+
else if (subType === "Line") {
|
|
85
|
+
curves.push(extractLineGeometry(node, id));
|
|
34
86
|
}
|
|
35
87
|
}
|
|
36
88
|
}
|
|
37
89
|
// Extract skeleton/skinning data
|
|
38
|
-
const skins = extractSkins(objectMap);
|
|
90
|
+
const skins = extractSkins(objectMap, propertyTemplates);
|
|
39
91
|
const rigs = resolveRigs(objectMap, skins);
|
|
40
92
|
// Extract blend shape data
|
|
41
93
|
const blendShapes = extractBlendShapes(objectMap);
|
|
42
94
|
// Extract animation data
|
|
43
|
-
const animations = extractAnimations(objectMap);
|
|
95
|
+
const animations = extractAnimations(objectMap, doc, { rebaseKeyframes: options.rebaseKeyframes ?? true });
|
|
44
96
|
// Extract cameras and lights from NodeAttribute objects
|
|
45
97
|
const cameras = extractCameras(objectMap, propertyTemplates);
|
|
46
98
|
const lights = extractLights(objectMap, propertyTemplates);
|
|
47
99
|
const diagnostics = extractSceneDiagnostics(objectMap);
|
|
100
|
+
const constraints = extractConstraints(objectMap);
|
|
48
101
|
// Build model hierarchy
|
|
49
|
-
const rootModels = buildModelHierarchy(objectMap, geometries, materials, propertyTemplates);
|
|
102
|
+
const rootModels = buildModelHierarchy(objectMap, geometries, curves, materials, propertyTemplates);
|
|
103
|
+
const { displayLayers, selectionSets } = extractCollections(objectMap, rootModels);
|
|
50
104
|
return {
|
|
51
105
|
rootModels,
|
|
106
|
+
displayLayers,
|
|
107
|
+
selectionSets,
|
|
108
|
+
constraints,
|
|
52
109
|
geometries,
|
|
110
|
+
curves,
|
|
53
111
|
materials,
|
|
54
112
|
skins,
|
|
55
113
|
rigs,
|
|
@@ -61,12 +119,115 @@ export function interpretFBX(doc) {
|
|
|
61
119
|
...globalSettings,
|
|
62
120
|
};
|
|
63
121
|
}
|
|
122
|
+
// ── Legacy textures ────────────────────────────────────────────────────────────
|
|
123
|
+
/**
|
|
124
|
+
* Pre-7000 files connect textures to the Model and select them per polygon through the mesh's LayerElementTexture
|
|
125
|
+
* (TextureId indexes the model's textures), instead of connecting textures to material properties. Assign those
|
|
126
|
+
* textures to the diffuse slot of the material used by the same polygons.
|
|
127
|
+
*/
|
|
128
|
+
function attachLegacyLayerTextures(objectMap, materials, templates) {
|
|
129
|
+
const materialById = new Map(materials.map((m) => [m.id, m]));
|
|
130
|
+
const textureTemplate = getPropertyTemplate(templates, "Texture", "FbxFileTexture") ?? getPropertyTemplate(templates, "Texture");
|
|
131
|
+
for (const [modelId, modelNode] of Array.from(objectMap.objects)) {
|
|
132
|
+
if (modelNode.name !== "Model") {
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
const textureChildren = getChildren(objectMap, modelId, "Texture");
|
|
136
|
+
if (textureChildren.length === 0) {
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
const modelMaterials = getChildren(objectMap, modelId, "Material")
|
|
140
|
+
.filter((c) => !c.propertyName)
|
|
141
|
+
.map((c) => materialById.get(c.id))
|
|
142
|
+
.filter((m) => m !== undefined);
|
|
143
|
+
if (modelMaterials.length === 0) {
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
const geometryNode = getChildren(objectMap, modelId, "Geometry")[0]?.node ?? modelNode;
|
|
147
|
+
const materialLayer = findChildByName(geometryNode, "LayerElementMaterial");
|
|
148
|
+
const materialIds = materialLayer ? getNodeArray(findChildByName(materialLayer, "Materials")) : null;
|
|
149
|
+
// Each texture kind has its own layer element: LayerElementTexture (diffuse), LayerElementEmissiveTextures,
|
|
150
|
+
// LayerElementSpecularTextures, LayerElementBumpTextures, ...
|
|
151
|
+
const slotForElement = (elementName) => {
|
|
152
|
+
const kind = elementName.replace(/^LayerElement/, "").replace(/Textures?$/, "");
|
|
153
|
+
switch (kind) {
|
|
154
|
+
case "":
|
|
155
|
+
case "Diffuse":
|
|
156
|
+
return { prop: "DiffuseColor", fbx: "diffuseColor", pbr: "baseColor" };
|
|
157
|
+
case "Emissive":
|
|
158
|
+
return { prop: "EmissiveColor", fbx: "emissionColor", pbr: "emissionColor" };
|
|
159
|
+
case "Specular":
|
|
160
|
+
return { prop: "SpecularColor", fbx: "specularColor", pbr: "specularColor" };
|
|
161
|
+
case "Ambient":
|
|
162
|
+
return { prop: "AmbientColor", fbx: "ambientColor", pbr: "ambientOcclusion" };
|
|
163
|
+
case "Bump":
|
|
164
|
+
case "NormalMap":
|
|
165
|
+
return { prop: "NormalMap", fbx: "normalMap", pbr: "normalMap" };
|
|
166
|
+
case "Transparent":
|
|
167
|
+
return { prop: "TransparentColor", fbx: "transparencyColor", pbr: "opacity" };
|
|
168
|
+
case "Reflection":
|
|
169
|
+
return { prop: "ReflectionColor", fbx: "reflectionColor", pbr: "specularColor" };
|
|
170
|
+
case "Shininess":
|
|
171
|
+
return { prop: "ShininessExponent", fbx: "specularExponent", pbr: "roughness" };
|
|
172
|
+
case "Displacement":
|
|
173
|
+
return { prop: "DisplacementColor", fbx: "displacement", pbr: "displacementMap" };
|
|
174
|
+
default:
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
const layerElements = geometryNode.children.filter((c) => /^LayerElement\w*Textures?$/.test(c.name));
|
|
179
|
+
const textureRefs = textureChildren.map(({ id, node }) => extractTextureRef(id, node, undefined, objectMap, textureTemplate));
|
|
180
|
+
const assign = (materialIndex, textureIndex, slot) => {
|
|
181
|
+
var _a, _b, _c, _d;
|
|
182
|
+
const material = modelMaterials[materialIndex];
|
|
183
|
+
const source = textureRefs[textureIndex];
|
|
184
|
+
if (!material || !source || material.textures.some((t) => t.propertyName === slot.prop)) {
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
const ref = { ...source, propertyName: slot.prop };
|
|
188
|
+
material.textures.push(ref);
|
|
189
|
+
const fbxMap = ((_a = material.model.fbx)[_b = slot.fbx] ?? (_a[_b] = { valueComponents: 0, textureEnabled: false }));
|
|
190
|
+
fbxMap.texture = ref;
|
|
191
|
+
fbxMap.textureEnabled = true;
|
|
192
|
+
const pbrMap = ((_c = material.model.pbr)[_d = slot.pbr] ?? (_c[_d] = { valueComponents: 0, textureEnabled: false }));
|
|
193
|
+
pbrMap.texture = ref;
|
|
194
|
+
pbrMap.textureEnabled = true;
|
|
195
|
+
};
|
|
196
|
+
if (layerElements.length === 0) {
|
|
197
|
+
// No layer element: a single texture on a single-material model is the diffuse map.
|
|
198
|
+
if (modelMaterials.length === 1 && textureRefs.length === 1) {
|
|
199
|
+
assign(0, 0, slotForElement("LayerElementTexture"));
|
|
200
|
+
}
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
for (const element of layerElements) {
|
|
204
|
+
const slot = slotForElement(element.name);
|
|
205
|
+
const textureIds = getNodeArray(findChildByName(element, "TextureId"));
|
|
206
|
+
if (!slot || !textureIds || textureIds.length === 0) {
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
const polygonCount = Math.max(textureIds.length, materialIds?.length ?? 0);
|
|
210
|
+
for (let poly = 0; poly < polygonCount; poly++) {
|
|
211
|
+
const texIndex = textureIds[Math.min(poly, textureIds.length - 1)];
|
|
212
|
+
const matIndex = materialIds && materialIds.length > 0 ? materialIds[Math.min(poly, materialIds.length - 1)] : 0;
|
|
213
|
+
if (texIndex >= 0) {
|
|
214
|
+
assign(matIndex, texIndex, slot);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
64
220
|
// ── Model Hierarchy ────────────────────────────────────────────────────────────
|
|
65
|
-
|
|
221
|
+
const MAX_MODEL_DEPTH = 512;
|
|
222
|
+
function buildModelHierarchy(objectMap, geometries, curves, materials, propertyTemplates) {
|
|
66
223
|
const geometryMap = new Map();
|
|
67
224
|
for (const g of geometries) {
|
|
68
225
|
geometryMap.set(g.id, g);
|
|
69
226
|
}
|
|
227
|
+
const curveMap = new Map();
|
|
228
|
+
for (const c of curves) {
|
|
229
|
+
curveMap.set(c.id, c);
|
|
230
|
+
}
|
|
70
231
|
const materialMap = new Map();
|
|
71
232
|
for (const m of materials) {
|
|
72
233
|
materialMap.set(m.id, m);
|
|
@@ -74,20 +235,44 @@ function buildModelHierarchy(objectMap, geometries, materials, propertyTemplates
|
|
|
74
235
|
// Find root models (those connected to ID 0, which is the scene root)
|
|
75
236
|
const rootChildren = objectMap.childrenOf.get(0) ?? [];
|
|
76
237
|
const rootModels = [];
|
|
238
|
+
const rootIds = new Set();
|
|
239
|
+
const buildStack = new Set();
|
|
77
240
|
for (const { id } of rootChildren) {
|
|
78
241
|
const node = objectMap.objects.get(id);
|
|
79
|
-
if (node && node.name === "Model") {
|
|
80
|
-
|
|
242
|
+
if (node && node.name === "Model" && !rootIds.has(id)) {
|
|
243
|
+
rootIds.add(id);
|
|
244
|
+
rootModels.push(buildModel(id, node, objectMap, geometryMap, curveMap, materialMap, propertyTemplates, buildStack));
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
// Models without any Model (or scene root) parent connection also live at the root, as in the FBX SDK.
|
|
248
|
+
// Typical examples are the default "Producer" cameras and the camera switcher written by Maya, MotionBuilder
|
|
249
|
+
// and Blender in 6.x files, which are never connected.
|
|
250
|
+
const hasModelParent = new Set();
|
|
251
|
+
for (const conn of objectMap.connections) {
|
|
252
|
+
if (conn.type !== "OO") {
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
const parentNode = objectMap.objects.get(conn.parentId);
|
|
256
|
+
if (conn.parentId === 0 || parentNode?.name === "Model") {
|
|
257
|
+
hasModelParent.add(conn.childId);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
for (const [id, node] of Array.from(objectMap.objects)) {
|
|
261
|
+
if (node.name === "Model" && !hasModelParent.has(id) && !rootIds.has(id)) {
|
|
262
|
+
rootIds.add(id);
|
|
263
|
+
rootModels.push(buildModel(id, node, objectMap, geometryMap, curveMap, materialMap, propertyTemplates, buildStack));
|
|
81
264
|
}
|
|
82
265
|
}
|
|
83
266
|
return rootModels;
|
|
84
267
|
}
|
|
85
|
-
function buildModel(modelId, modelNode, objectMap, geometryMap, materialMap, propertyTemplates) {
|
|
268
|
+
function buildModel(modelId, modelNode, objectMap, geometryMap, curveMap, materialMap, propertyTemplates, buildStack) {
|
|
86
269
|
const name = cleanFBXName(getPropertyValue(modelNode, 1) ?? "Model");
|
|
270
|
+
buildStack.add(modelId);
|
|
87
271
|
const subType = getPropertyValue(modelNode, 2) ?? "Null";
|
|
88
272
|
// Find attached geometry
|
|
89
273
|
const geomChildren = getChildren(objectMap, modelId, "Geometry");
|
|
90
|
-
const geometry = geomChildren.
|
|
274
|
+
const geometry = geomChildren.map((child) => geometryMap.get(child.id)).find((g) => g !== undefined);
|
|
275
|
+
const curve = geomChildren.map((child) => curveMap.get(child.id)).find((c) => c !== undefined);
|
|
91
276
|
// Find attached materials
|
|
92
277
|
const matChildren = getChildren(objectMap, modelId, "Material");
|
|
93
278
|
const modelMaterials = [];
|
|
@@ -100,16 +285,52 @@ function buildModel(modelId, modelNode, objectMap, geometryMap, materialMap, pro
|
|
|
100
285
|
// Extract transform
|
|
101
286
|
const transform = extractTransform(modelNode, getPropertyTemplate(propertyTemplates, "Model", "FbxNode") ?? getPropertyTemplate(propertyTemplates, "Model"));
|
|
102
287
|
// Recursively build child models
|
|
103
|
-
|
|
288
|
+
// Only object-object connections express parenting; OP connections (look-at targets, constraint sources) do not.
|
|
289
|
+
const childModelNodes = getChildren(objectMap, modelId, "Model").filter((child) => !child.propertyName);
|
|
104
290
|
const children = [];
|
|
291
|
+
const cycleDiagnostics = [];
|
|
105
292
|
for (const { id, node } of childModelNodes) {
|
|
106
|
-
|
|
293
|
+
if (buildStack.size > MAX_MODEL_DEPTH) {
|
|
294
|
+
cycleDiagnostics.push(`Model hierarchy deeper than ${MAX_MODEL_DEPTH} levels; deeper children were skipped.`);
|
|
295
|
+
break;
|
|
296
|
+
}
|
|
297
|
+
if (buildStack.has(id)) {
|
|
298
|
+
// Malformed files (duplicate ids, recursive connections) can make a model its own ancestor.
|
|
299
|
+
cycleDiagnostics.push(`Model connection cycle detected at '${cleanFBXName(getPropertyValue(node, 1) ?? "")}'; the recursive child was skipped.`);
|
|
300
|
+
continue;
|
|
301
|
+
}
|
|
302
|
+
children.push(buildModel(id, node, objectMap, geometryMap, curveMap, materialMap, propertyTemplates, buildStack));
|
|
107
303
|
}
|
|
304
|
+
buildStack.delete(modelId);
|
|
108
305
|
// Extract culling
|
|
109
306
|
const cullingNode = modelNode.children.find((c) => c.name === "Culling");
|
|
110
307
|
const cullingOff = cullingNode ? getPropertyValue(cullingNode, 0) === "CullingOff" : false;
|
|
111
308
|
// Extract user-defined custom properties
|
|
112
309
|
const customProperties = extractCustomProperties(modelNode);
|
|
310
|
+
// LOD groups carry their thresholds on the LodGroup node attribute
|
|
311
|
+
let lodGroup;
|
|
312
|
+
if (subType === "LodGroup") {
|
|
313
|
+
const attribute = getChildren(objectMap, modelId, "NodeAttribute").find((a) => getPropertyValue(a.node, 2) === "LodGroup");
|
|
314
|
+
const entries = attribute ? getPropertyEntries(attribute.node) : [];
|
|
315
|
+
const thresholds = [];
|
|
316
|
+
const displayLevels = [];
|
|
317
|
+
for (let i = 0;; i++) {
|
|
318
|
+
const entry = entries.find((e) => e.name === `Thresholds|Level${i}`);
|
|
319
|
+
if (!entry) {
|
|
320
|
+
break;
|
|
321
|
+
}
|
|
322
|
+
thresholds.push(typeof entry.values[0] === "number" ? entry.values[0] : 0);
|
|
323
|
+
}
|
|
324
|
+
for (let i = 0;; i++) {
|
|
325
|
+
const entry = entries.find((e) => e.name === `DisplayLevels|Level${i}`);
|
|
326
|
+
if (!entry) {
|
|
327
|
+
break;
|
|
328
|
+
}
|
|
329
|
+
displayLevels.push(typeof entry.values[0] === "number" ? entry.values[0] : 0);
|
|
330
|
+
}
|
|
331
|
+
const relativeEntry = entries.find((e) => e.name === "ThresholdsUsedAsPercentage");
|
|
332
|
+
lodGroup = { thresholds, relative: !!relativeEntry && relativeEntry.values[0] !== 0 && relativeEntry.values[0] !== false, displayLevels };
|
|
333
|
+
}
|
|
113
334
|
return {
|
|
114
335
|
id: modelId,
|
|
115
336
|
name,
|
|
@@ -119,47 +340,87 @@ function buildModel(modelId, modelNode, objectMap, geometryMap, materialMap, pro
|
|
|
119
340
|
children,
|
|
120
341
|
cullingOff,
|
|
121
342
|
customProperties,
|
|
343
|
+
lodGroup,
|
|
344
|
+
curve,
|
|
122
345
|
...transform,
|
|
346
|
+
diagnostics: [...transform.diagnostics, ...cycleDiagnostics],
|
|
123
347
|
};
|
|
124
348
|
}
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
const
|
|
128
|
-
const
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
const rotationOffset = resolveVector3Property(modelNode, template, "RotationOffset", [0, 0, 0]);
|
|
134
|
-
const scalingOffset = resolveVector3Property(modelNode, template, "ScalingOffset", [0, 0, 0]);
|
|
135
|
-
const geometricTranslation = resolveVector3Property(modelNode, template, "GeometricTranslation", [0, 0, 0]);
|
|
136
|
-
const geometricRotation = resolveVector3Property(modelNode, template, "GeometricRotation", [0, 0, 0]);
|
|
137
|
-
const geometricScaling = resolveVector3Property(modelNode, template, "GeometricScaling", [1, 1, 1]);
|
|
138
|
-
const rotationOrder = resolveNumberProperty(modelNode, template, "RotationOrder", 0);
|
|
139
|
-
const inheritType = resolveNumberProperty(modelNode, template, "InheritType", 1);
|
|
140
|
-
const diagnostics = inheritType !== 1 && inheritType !== 2
|
|
141
|
-
? [
|
|
142
|
-
`InheritType ${inheritType} is parsed and preserved; runtime parent-scale inheritance remains gated to avoid changing existing visual behavior without a fixture-specific baseline.`,
|
|
143
|
-
]
|
|
144
|
-
: [];
|
|
145
|
-
return {
|
|
146
|
-
translation,
|
|
147
|
-
rotation,
|
|
148
|
-
scale,
|
|
149
|
-
preRotation,
|
|
150
|
-
postRotation,
|
|
151
|
-
rotationPivot,
|
|
152
|
-
scalingPivot,
|
|
153
|
-
rotationOffset,
|
|
154
|
-
scalingOffset,
|
|
155
|
-
geometricTranslation,
|
|
156
|
-
geometricRotation,
|
|
157
|
-
geometricScaling,
|
|
158
|
-
rotationOrder,
|
|
159
|
-
inheritType,
|
|
160
|
-
diagnostics,
|
|
349
|
+
/** Display layers and selection sets: which models belong to which collection. */
|
|
350
|
+
function extractCollections(objectMap, rootModels) {
|
|
351
|
+
const modelById = new Map();
|
|
352
|
+
const walk = (models) => {
|
|
353
|
+
for (const m of models) {
|
|
354
|
+
modelById.set(m.id, m);
|
|
355
|
+
walk(m.children);
|
|
356
|
+
}
|
|
161
357
|
};
|
|
358
|
+
walk(rootModels);
|
|
359
|
+
const displayLayers = [];
|
|
360
|
+
const selectionSets = [];
|
|
361
|
+
for (const [id, node] of Array.from(objectMap.objects)) {
|
|
362
|
+
if (node.name === "CollectionExclusive") {
|
|
363
|
+
const entries = getPropertyEntries(node);
|
|
364
|
+
const num = (name, fallback) => {
|
|
365
|
+
const v = entries.find((e) => e.name === name)?.values[0];
|
|
366
|
+
return typeof v === "number" ? v : typeof v === "boolean" ? (v ? 1 : 0) : fallback;
|
|
367
|
+
};
|
|
368
|
+
const colorValues = entries.find((e) => e.name === "Color")?.values;
|
|
369
|
+
const color = colorValues && typeof colorValues[0] === "number" && typeof colorValues[1] === "number" && typeof colorValues[2] === "number"
|
|
370
|
+
? [colorValues[0], colorValues[1], colorValues[2]]
|
|
371
|
+
: [0.8, 0.8, 0.8];
|
|
372
|
+
const layer = {
|
|
373
|
+
id,
|
|
374
|
+
name: cleanFBXName(getPropertyValue(node, 1) ?? "DisplayLayer"),
|
|
375
|
+
show: num("Show", 1) !== 0,
|
|
376
|
+
freeze: num("Freeze", 0) !== 0,
|
|
377
|
+
color,
|
|
378
|
+
modelIds: getChildren(objectMap, id, "Model").map((c) => c.id),
|
|
379
|
+
};
|
|
380
|
+
displayLayers.push(layer);
|
|
381
|
+
for (const modelId of layer.modelIds) {
|
|
382
|
+
const model = modelById.get(modelId);
|
|
383
|
+
if (model) {
|
|
384
|
+
model.displayLayer = layer;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
else if (node.name === "SelectionSet" || (node.name === "Collection" && getPropertyValue(node, 2) === "SelectionSet")) {
|
|
389
|
+
const members = [];
|
|
390
|
+
for (const child of getChildren(objectMap, id)) {
|
|
391
|
+
if (child.node.name === "Model") {
|
|
392
|
+
members.push({ modelId: child.id, includeNode: true });
|
|
393
|
+
}
|
|
394
|
+
else if (child.node.name === "SelectionNode") {
|
|
395
|
+
const indices = (name) => {
|
|
396
|
+
const arrayNode = findChildByName(child.node, name);
|
|
397
|
+
if (!arrayNode) {
|
|
398
|
+
return undefined;
|
|
399
|
+
}
|
|
400
|
+
return Array.from(getNodeArray(arrayNode));
|
|
401
|
+
};
|
|
402
|
+
const inSet = findChildByName(child.node, "IsTheNodeInSet")?.properties[0]?.value;
|
|
403
|
+
for (const target of getChildren(objectMap, child.id, "Model")) {
|
|
404
|
+
members.push({
|
|
405
|
+
modelId: target.id,
|
|
406
|
+
includeNode: inSet === undefined || inSet === 1 || inSet === true,
|
|
407
|
+
vertices: indices("VertexIndexArray"),
|
|
408
|
+
edges: indices("EdgeIndexArray"),
|
|
409
|
+
faces: indices("PolygonIndexArray"),
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
selectionSets.push({ id, name: cleanFBXName(getPropertyValue(node, 1) ?? "SelectionSet"), members });
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
return { displayLayers, selectionSets };
|
|
418
|
+
}
|
|
419
|
+
function extractTransform(modelNode, template) {
|
|
420
|
+
return extractNodeTransform(modelNode, template);
|
|
162
421
|
}
|
|
422
|
+
// Frame rates per FBX TimeMode enum value (eDefaultMode, eFrames120, ... eFrames59dot94).
|
|
423
|
+
const TIME_MODE_FPS = [30, 120, 100, 60, 50, 48, 30, 30, 29.97, 29.97, 25, 24, 1000, 23.976, 24, 96, 72, 59.94];
|
|
163
424
|
function extractGlobalSettings(doc) {
|
|
164
425
|
const defaults = {
|
|
165
426
|
upAxis: 1,
|
|
@@ -169,21 +430,16 @@ function extractGlobalSettings(doc) {
|
|
|
169
430
|
coordAxis: 0,
|
|
170
431
|
coordAxisSign: 1,
|
|
171
432
|
unitScaleFactor: 1,
|
|
433
|
+
frameRate: 30,
|
|
172
434
|
};
|
|
173
435
|
const gsNode = findDocumentNode(doc, "GlobalSettings");
|
|
174
436
|
if (!gsNode) {
|
|
175
437
|
return defaults;
|
|
176
438
|
}
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
for (const p of props70.children) {
|
|
182
|
-
if (p.name !== "P") {
|
|
183
|
-
continue;
|
|
184
|
-
}
|
|
185
|
-
const propName = getPropertyValue(p, 0);
|
|
186
|
-
const value = toNumber(p.properties[4]?.value);
|
|
439
|
+
let timeMode = 0;
|
|
440
|
+
let customFrameRate = 24;
|
|
441
|
+
for (const { name: propName, values } of getPropertyEntries(gsNode)) {
|
|
442
|
+
const value = toNumber(values[0]);
|
|
187
443
|
if (propName && value !== undefined) {
|
|
188
444
|
switch (propName) {
|
|
189
445
|
case "UpAxis":
|
|
@@ -207,9 +463,21 @@ function extractGlobalSettings(doc) {
|
|
|
207
463
|
case "UnitScaleFactor":
|
|
208
464
|
defaults.unitScaleFactor = value;
|
|
209
465
|
break;
|
|
466
|
+
case "TimeMode":
|
|
467
|
+
timeMode = value;
|
|
468
|
+
break;
|
|
469
|
+
case "CustomFrameRate":
|
|
470
|
+
customFrameRate = value;
|
|
471
|
+
break;
|
|
210
472
|
}
|
|
211
473
|
}
|
|
212
474
|
}
|
|
475
|
+
if (timeMode === 14) {
|
|
476
|
+
defaults.frameRate = customFrameRate > 0 ? customFrameRate : 24;
|
|
477
|
+
}
|
|
478
|
+
else {
|
|
479
|
+
defaults.frameRate = TIME_MODE_FPS[timeMode] ?? 30;
|
|
480
|
+
}
|
|
213
481
|
return defaults;
|
|
214
482
|
}
|
|
215
483
|
// ── Cameras & Lights ──────────────────────────────────────────────────────────
|
|
@@ -235,38 +503,17 @@ const SYSTEM_PROPERTIES = new Set([
|
|
|
235
503
|
"lockInfluenceWeights",
|
|
236
504
|
]);
|
|
237
505
|
function extractCustomProperties(modelNode) {
|
|
238
|
-
|
|
239
|
-
|
|
506
|
+
// Properties flagged "U" are user-defined. Some writers (MotionBuilder) flag their own template properties
|
|
507
|
+
// as user properties too, so the well-known node properties are filtered out again.
|
|
508
|
+
const flagged = extractUserProperties(modelNode);
|
|
509
|
+
if (!flagged) {
|
|
240
510
|
return undefined;
|
|
241
511
|
}
|
|
242
|
-
const custom = {};
|
|
243
512
|
let hasAny = false;
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
const propName = getPropertyValue(p, 0);
|
|
249
|
-
if (!propName || SYSTEM_PROPERTIES.has(propName)) {
|
|
250
|
-
continue;
|
|
251
|
-
}
|
|
252
|
-
// Accept user-defined properties (type starts with something other than standard types)
|
|
253
|
-
// Standard FBX types: "KString", "Number", "double", "int", "bool", "Lcl"...
|
|
254
|
-
// User properties often have types like "KString", but are in the UDP (User Defined Properties) section
|
|
255
|
-
// Heuristic: if not in SYSTEM_PROPERTIES set, it's user-defined
|
|
256
|
-
const val = p.properties[4]?.value;
|
|
257
|
-
if (val === undefined) {
|
|
258
|
-
continue;
|
|
259
|
-
}
|
|
260
|
-
if (typeof val === "string") {
|
|
261
|
-
custom[propName] = val;
|
|
262
|
-
hasAny = true;
|
|
263
|
-
}
|
|
264
|
-
else if (typeof val === "number") {
|
|
265
|
-
custom[propName] = val;
|
|
266
|
-
hasAny = true;
|
|
267
|
-
}
|
|
268
|
-
else if (typeof val === "boolean") {
|
|
269
|
-
custom[propName] = val;
|
|
513
|
+
const custom = {};
|
|
514
|
+
for (const name of Object.keys(flagged)) {
|
|
515
|
+
if (!SYSTEM_PROPERTIES.has(name)) {
|
|
516
|
+
custom[name] = flagged[name];
|
|
270
517
|
hasAny = true;
|
|
271
518
|
}
|
|
272
519
|
}
|
|
@@ -329,7 +576,6 @@ function extractCameras(objectMap, templates) {
|
|
|
329
576
|
const name = cleanFBXName(getPropertyValue(parentNode, 1) ?? "Camera");
|
|
330
577
|
const nearPlane = resolveNumberProperty(node, cameraTemplate, "NearPlane", 0.1);
|
|
331
578
|
const farPlane = resolveNumberProperty(node, cameraTemplate, "FarPlane", 10000);
|
|
332
|
-
const aspectRatio = resolveCameraAspectRatio(node, cameraTemplate);
|
|
333
579
|
const projectionType = resolveNumberProperty(node, cameraTemplate, "CameraProjectionType", 0) === 1 || resolveNumberProperty(node, cameraTemplate, "ProjectionType", 0) === 1
|
|
334
580
|
? "orthographic"
|
|
335
581
|
: "perspective";
|
|
@@ -338,7 +584,9 @@ function extractCameras(objectMap, templates) {
|
|
|
338
584
|
const filmHeight = toNumber(resolvePropertyValue(node, cameraTemplate, "FilmHeight")) ?? toNumber(resolvePropertyValue(node, cameraTemplate, "ApertureHeight"));
|
|
339
585
|
const orthoZoom = toNumber(resolvePropertyValue(node, cameraTemplate, "OrthoZoom"));
|
|
340
586
|
const roll = toNumber(resolvePropertyValue(node, cameraTemplate, "Roll"));
|
|
341
|
-
const
|
|
587
|
+
const resolved = resolveCameraParameters(node, cameraTemplate);
|
|
588
|
+
const aspectRatio = resolved.aspectRatio;
|
|
589
|
+
const fieldOfView = resolved.fovDegY;
|
|
342
590
|
const diagnostics = [];
|
|
343
591
|
if (projectionType === "orthographic" && orthoZoom === undefined) {
|
|
344
592
|
diagnostics.push("Orthographic camera has no OrthoZoom; runtime orthographic bounds use a fallback.");
|
|
@@ -348,8 +596,14 @@ function extractCameras(objectMap, templates) {
|
|
|
348
596
|
}
|
|
349
597
|
cameras.push({
|
|
350
598
|
modelId: parent.id,
|
|
599
|
+
attributeId: id,
|
|
600
|
+
userProperties: extractUserProperties(node),
|
|
351
601
|
name,
|
|
352
602
|
fieldOfView,
|
|
603
|
+
fieldOfViewX: resolved.fovDegX,
|
|
604
|
+
apertureMode: resolved.apertureMode,
|
|
605
|
+
apertureSizeInch: resolved.apertureSizeInch,
|
|
606
|
+
orthographicSize: resolved.orthographicSize,
|
|
353
607
|
nearPlane,
|
|
354
608
|
farPlane,
|
|
355
609
|
aspectRatio,
|
|
@@ -408,6 +662,13 @@ function extractLights(objectMap, templates) {
|
|
|
408
662
|
}
|
|
409
663
|
lights.push({
|
|
410
664
|
modelId: parent.id,
|
|
665
|
+
attributeId: id,
|
|
666
|
+
userProperties: extractUserProperties(node),
|
|
667
|
+
nearAttenuationStart: toNumber(resolvePropertyValue(node, lightTemplate, "NearAttenuationStart")),
|
|
668
|
+
nearAttenuationEnd: toNumber(resolvePropertyValue(node, lightTemplate, "NearAttenuationEnd")),
|
|
669
|
+
farAttenuationStart: toNumber(resolvePropertyValue(node, lightTemplate, "FarAttenuationStart")),
|
|
670
|
+
farAttenuationEnd: toNumber(resolvePropertyValue(node, lightTemplate, "FarAttenuationEnd")),
|
|
671
|
+
areaLightShape: toNumber(resolvePropertyValue(node, lightTemplate, "AreaLightShape")),
|
|
411
672
|
name,
|
|
412
673
|
lightType,
|
|
413
674
|
color,
|
|
@@ -442,34 +703,131 @@ function toBoolean(value) {
|
|
|
442
703
|
}
|
|
443
704
|
return undefined;
|
|
444
705
|
}
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
706
|
+
/** Film back sizes in thousandths of an inch per FBX ApertureFormat enum value. */
|
|
707
|
+
const APERTURE_FORMATS = [
|
|
708
|
+
[1000, 1000],
|
|
709
|
+
[404, 295],
|
|
710
|
+
[493, 292],
|
|
711
|
+
[864, 630],
|
|
712
|
+
[816, 612],
|
|
713
|
+
[980, 735],
|
|
714
|
+
[825, 446],
|
|
715
|
+
[864, 732],
|
|
716
|
+
[2066, 906],
|
|
717
|
+
[1485, 991],
|
|
718
|
+
[2080, 1480],
|
|
719
|
+
[2772, 2072],
|
|
720
|
+
];
|
|
721
|
+
/**
|
|
722
|
+
* Resolves the camera's projection parameters the way the FBX SDK does: aspect ratio from AspectWidth/Height and
|
|
723
|
+
* the aspect mode, aperture (film gate) from the film size, squeeze ratio and gate fit, and the horizontal and
|
|
724
|
+
* vertical field of view from the aperture mode (horizontal, vertical, both, or focal length).
|
|
725
|
+
*/
|
|
726
|
+
function resolveCameraParameters(node, template) {
|
|
727
|
+
const num = (name, fallback) => toNumber(resolvePropertyValue(node, template, name)) ?? fallback;
|
|
728
|
+
const aspectMode = num("AspectRatioMode", 0);
|
|
729
|
+
const apertureMode = num("ApertureMode", 2);
|
|
730
|
+
const apertureFormat = Math.max(0, Math.min(APERTURE_FORMATS.length - 1, Math.round(num("ApertureFormat", 0))));
|
|
731
|
+
const gateFit = num("GateFit", 0);
|
|
732
|
+
let aspectX = num("AspectW", 0);
|
|
733
|
+
let aspectY = num("AspectH", 0);
|
|
734
|
+
aspectX = num("AspectWidth", aspectX);
|
|
735
|
+
aspectY = num("AspectHeight", aspectY);
|
|
736
|
+
const fov = num("FieldOfView", 0);
|
|
737
|
+
const fovX = num("FieldOfViewX", 0);
|
|
738
|
+
const fovY = num("FieldOfViewY", 0);
|
|
739
|
+
const focalLength = num("FocalLength", 0);
|
|
740
|
+
const orthoExtent = 30 * num("OrthoZoom", 1);
|
|
741
|
+
const format = APERTURE_FORMATS[apertureFormat];
|
|
742
|
+
let filmX = num("FilmWidth", format[0] * 0.001);
|
|
743
|
+
let filmY = num("FilmHeight", format[1] * 0.001);
|
|
744
|
+
const squeeze = num("FilmSqueezeRatio", apertureFormat === 7 ? 2 : 1);
|
|
745
|
+
if (aspectX <= 0 && aspectY <= 0) {
|
|
746
|
+
aspectX = filmX > 0 ? filmX : 1;
|
|
747
|
+
aspectY = filmY > 0 ? filmY : 1;
|
|
449
748
|
}
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
if (aspectWidth !== undefined && aspectHeight !== undefined && aspectWidth > 0 && aspectHeight > 0) {
|
|
453
|
-
return aspectWidth / aspectHeight;
|
|
749
|
+
else if (aspectX <= 0) {
|
|
750
|
+
aspectX = filmX > 0 && filmY > 0 ? (aspectY / filmY) * filmX : aspectY;
|
|
454
751
|
}
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
function resolveCameraFieldOfView(node, template, aspectRatio, focalLength, filmHeight) {
|
|
458
|
-
const verticalFov = toNumber(resolvePropertyValue(node, template, "FieldOfViewY")) ?? toNumber(resolvePropertyValue(node, template, "FieldOfView"));
|
|
459
|
-
if (verticalFov !== undefined) {
|
|
460
|
-
return verticalFov;
|
|
752
|
+
else if (aspectY <= 0) {
|
|
753
|
+
aspectY = filmX > 0 && filmY > 0 ? (aspectX / filmX) * filmY : aspectX;
|
|
461
754
|
}
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
755
|
+
filmY *= squeeze;
|
|
756
|
+
let resX = aspectX;
|
|
757
|
+
let resY = aspectY;
|
|
758
|
+
switch (aspectMode) {
|
|
759
|
+
case 3: // fixed width
|
|
760
|
+
resX = aspectX;
|
|
761
|
+
resY = aspectX * aspectY;
|
|
762
|
+
break;
|
|
763
|
+
case 4: // fixed height
|
|
764
|
+
resX = aspectY * aspectX;
|
|
765
|
+
resY = aspectY;
|
|
766
|
+
break;
|
|
767
|
+
default:
|
|
768
|
+
break;
|
|
769
|
+
}
|
|
770
|
+
const aspectRatio = resY !== 0 ? resX / resY : 1;
|
|
771
|
+
const filmRatio = filmY !== 0 ? filmX / filmY : 1;
|
|
772
|
+
let effectiveFit = gateFit;
|
|
773
|
+
if (effectiveFit === 3) {
|
|
774
|
+
effectiveFit = aspectRatio > filmRatio ? 2 : 1;
|
|
775
|
+
}
|
|
776
|
+
else if (effectiveFit === 4) {
|
|
777
|
+
effectiveFit = aspectRatio < filmRatio ? 2 : 1;
|
|
778
|
+
}
|
|
779
|
+
let apertureX = filmX;
|
|
780
|
+
let apertureY = filmY;
|
|
781
|
+
let orthoX = orthoExtent;
|
|
782
|
+
let orthoY = orthoExtent;
|
|
783
|
+
switch (effectiveFit) {
|
|
784
|
+
case 1: // vertical
|
|
785
|
+
apertureX = filmY * aspectRatio;
|
|
786
|
+
apertureY = filmY;
|
|
787
|
+
orthoX = orthoExtent * aspectRatio;
|
|
788
|
+
break;
|
|
789
|
+
case 2: // horizontal
|
|
790
|
+
apertureX = filmX;
|
|
791
|
+
apertureY = filmX / aspectRatio;
|
|
792
|
+
orthoY = orthoExtent / aspectRatio;
|
|
793
|
+
break;
|
|
794
|
+
default:
|
|
795
|
+
break;
|
|
468
796
|
}
|
|
469
|
-
|
|
470
|
-
|
|
797
|
+
filmX = apertureX;
|
|
798
|
+
void filmX;
|
|
799
|
+
const d2r = Math.PI / 180;
|
|
800
|
+
const r2d = 180 / Math.PI;
|
|
801
|
+
let tanX;
|
|
802
|
+
let tanY;
|
|
803
|
+
switch (apertureMode) {
|
|
804
|
+
case 0:
|
|
805
|
+
tanX = Math.tan(fovX * d2r * 0.5);
|
|
806
|
+
tanY = Math.tan(fovY * d2r * 0.5);
|
|
807
|
+
break;
|
|
808
|
+
case 1:
|
|
809
|
+
tanX = Math.tan(fov * d2r * 0.5);
|
|
810
|
+
tanY = tanX / aspectRatio;
|
|
811
|
+
break;
|
|
812
|
+
case 3: {
|
|
813
|
+
const focalInch = focalLength / 25.4;
|
|
814
|
+
tanX = focalInch > 0 ? (apertureX / focalInch) * 0.5 : Math.tan(fov * d2r * 0.5) * aspectRatio;
|
|
815
|
+
tanY = focalInch > 0 ? (apertureY / focalInch) * 0.5 : Math.tan(fov * d2r * 0.5);
|
|
816
|
+
break;
|
|
817
|
+
}
|
|
818
|
+
default:
|
|
819
|
+
tanY = Math.tan(fov * d2r * 0.5);
|
|
820
|
+
tanX = tanY * aspectRatio;
|
|
821
|
+
break;
|
|
471
822
|
}
|
|
472
|
-
return
|
|
823
|
+
return {
|
|
824
|
+
fovDegX: Math.atan(tanX) * r2d * 2,
|
|
825
|
+
fovDegY: Math.atan(tanY) * r2d * 2,
|
|
826
|
+
aspectRatio,
|
|
827
|
+
apertureMode,
|
|
828
|
+
apertureSizeInch: [apertureX, apertureY],
|
|
829
|
+
orthographicSize: [orthoX, orthoY],
|
|
830
|
+
};
|
|
473
831
|
}
|
|
474
832
|
function collectUnknownLocalProperties(node, known) {
|
|
475
833
|
const unknown = new Set();
|
|
@@ -487,10 +845,4 @@ function collectUnknownLocalProperties(node, known) {
|
|
|
487
845
|
}
|
|
488
846
|
return Array.from(unknown).sort();
|
|
489
847
|
}
|
|
490
|
-
function degreesToRadians(degrees) {
|
|
491
|
-
return (degrees * Math.PI) / 180;
|
|
492
|
-
}
|
|
493
|
-
function radiansToDegrees(radians) {
|
|
494
|
-
return (radians * 180) / Math.PI;
|
|
495
|
-
}
|
|
496
848
|
//# sourceMappingURL=fbxInterpreter.js.map
|