1 # SPDX-License-Identifier: GPL-2.0-or-later
3 # Script copyright (C) Blender Foundation
5 # FBX 7.1.0 -> 7.4.0 loader for Blender
7 # Not totally pep8 compliant.
8 # pep8 import_fbx.py --ignore=E501,E123,E702,E125
12 if "parse_fbx" in locals():
13 importlib
.reload(parse_fbx
)
14 if "fbx_utils" in locals():
15 importlib
.reload(fbx_utils
)
18 from bpy
.app
.translations
import pgettext_tip
as tip_
19 from mathutils
import Matrix
, Euler
, Vector
23 from . import parse_fbx
, fbx_utils
25 from .parse_fbx
import (
29 from .fbx_utils
import (
31 units_blender_to_fbx_factor
,
39 # global singleton, assign on execution
43 convert_deg_to_rad_iter
= units_convertor_iter("degree", "radian")
45 MAT_CONVERT_BONE
= fbx_utils
.MAT_CONVERT_BONE
.inverted()
46 MAT_CONVERT_LIGHT
= fbx_utils
.MAT_CONVERT_LIGHT
.inverted()
47 MAT_CONVERT_CAMERA
= fbx_utils
.MAT_CONVERT_CAMERA
.inverted()
50 def validate_blend_names(name
):
51 assert(type(name
) == bytes
)
52 # Blender typically does not accept names over 63 bytes...
55 h
= hashlib
.sha1(name
).hexdigest()
57 name_utf8
= name
[:n
].decode('utf-8', 'replace') + "_" + h
[:7]
58 while len(name_utf8
.encode()) > 63:
60 name_utf8
= name
[:n
].decode('utf-8', 'replace') + "_" + h
[:7]
63 # We use 'replace' even though FBX 'specs' say it should always be utf8, see T53841.
64 return name
.decode('utf-8', 'replace')
67 def elem_find_first(elem
, id_search
, default
=None):
68 for fbx_item
in elem
.elems
:
69 if fbx_item
.id == id_search
:
74 def elem_find_iter(elem
, id_search
):
75 for fbx_item
in elem
.elems
:
76 if fbx_item
.id == id_search
:
80 def elem_find_first_string(elem
, id_search
):
81 fbx_item
= elem_find_first(elem
, id_search
)
82 if fbx_item
is not None and fbx_item
.props
: # Do not error on complete empty properties (see T45291).
83 assert(len(fbx_item
.props
) == 1)
84 assert(fbx_item
.props_type
[0] == data_types
.STRING
)
85 return fbx_item
.props
[0].decode('utf-8', 'replace')
89 def elem_find_first_string_as_bytes(elem
, id_search
):
90 fbx_item
= elem_find_first(elem
, id_search
)
91 if fbx_item
is not None and fbx_item
.props
: # Do not error on complete empty properties (see T45291).
92 assert(len(fbx_item
.props
) == 1)
93 assert(fbx_item
.props_type
[0] == data_types
.STRING
)
94 return fbx_item
.props
[0] # Keep it as bytes as requested...
98 def elem_find_first_bytes(elem
, id_search
, decode
=True):
99 fbx_item
= elem_find_first(elem
, id_search
)
100 if fbx_item
is not None and fbx_item
.props
: # Do not error on complete empty properties (see T45291).
101 assert(len(fbx_item
.props
) == 1)
102 assert(fbx_item
.props_type
[0] == data_types
.BYTES
)
103 return fbx_item
.props
[0]
108 return "%s: props[%d=%r], elems=(%r)" % (
111 ", ".join([repr(p
) for p
in elem
.props
]),
113 b
", ".join([e
.id for e
in elem
.elems
]),
117 def elem_split_name_class(elem
):
118 assert(elem
.props_type
[-2] == data_types
.STRING
)
119 elem_name
, elem_class
= elem
.props
[-2].split(b
'\x00\x01')
120 return elem_name
, elem_class
123 def elem_name_ensure_class(elem
, clss
=...):
124 elem_name
, elem_class
= elem_split_name_class(elem
)
126 assert(elem_class
== clss
)
127 return validate_blend_names(elem_name
)
130 def elem_name_ensure_classes(elem
, clss
=...):
131 elem_name
, elem_class
= elem_split_name_class(elem
)
133 assert(elem_class
in clss
)
134 return validate_blend_names(elem_name
)
137 def elem_split_name_class_nodeattr(elem
):
138 assert(elem
.props_type
[-2] == data_types
.STRING
)
139 elem_name
, elem_class
= elem
.props
[-2].split(b
'\x00\x01')
140 assert(elem_class
== b
'NodeAttribute')
141 assert(elem
.props_type
[-1] == data_types
.STRING
)
142 elem_class
= elem
.props
[-1]
143 return elem_name
, elem_class
147 assert(elem
.props_type
[0] == data_types
.INT64
)
151 def elem_prop_first(elem
, default
=None):
152 return elem
.props
[0] if (elem
is not None) and elem
.props
else default
157 # Properties70: { ... P:
158 def elem_props_find_first(elem
, elem_prop_id
):
160 # When properties are not found... Should never happen, but happens - as usual.
162 # support for templates (tuple of elems)
163 if type(elem
) is not FBXElem
:
164 assert(type(elem
) is tuple)
166 result
= elem_props_find_first(e
, elem_prop_id
)
167 if result
is not None:
169 assert(len(elem
) > 0)
172 for subelem
in elem
.elems
:
173 assert(subelem
.id == b
'P')
174 if subelem
.props
[0] == elem_prop_id
:
179 def elem_props_get_color_rgb(elem
, elem_prop_id
, default
=None):
180 elem_prop
= elem_props_find_first(elem
, elem_prop_id
)
181 if elem_prop
is not None:
182 assert(elem_prop
.props
[0] == elem_prop_id
)
183 if elem_prop
.props
[1] == b
'Color':
185 assert(elem_prop
.props
[1] == b
'Color')
186 assert(elem_prop
.props
[2] == b
'')
188 assert(elem_prop
.props
[1] == b
'ColorRGB')
189 assert(elem_prop
.props
[2] == b
'Color')
190 assert(elem_prop
.props_type
[4:7] == bytes((data_types
.FLOAT64
,)) * 3)
191 return elem_prop
.props
[4:7]
195 def elem_props_get_vector_3d(elem
, elem_prop_id
, default
=None):
196 elem_prop
= elem_props_find_first(elem
, elem_prop_id
)
197 if elem_prop
is not None:
198 assert(elem_prop
.props_type
[4:7] == bytes((data_types
.FLOAT64
,)) * 3)
199 return elem_prop
.props
[4:7]
203 def elem_props_get_number(elem
, elem_prop_id
, default
=None):
204 elem_prop
= elem_props_find_first(elem
, elem_prop_id
)
205 if elem_prop
is not None:
206 assert(elem_prop
.props
[0] == elem_prop_id
)
207 if elem_prop
.props
[1] == b
'double':
208 assert(elem_prop
.props
[1] == b
'double')
209 assert(elem_prop
.props
[2] == b
'Number')
211 assert(elem_prop
.props
[1] == b
'Number')
212 assert(elem_prop
.props
[2] == b
'')
214 # we could allow other number types
215 assert(elem_prop
.props_type
[4] == data_types
.FLOAT64
)
217 return elem_prop
.props
[4]
221 def elem_props_get_integer(elem
, elem_prop_id
, default
=None):
222 elem_prop
= elem_props_find_first(elem
, elem_prop_id
)
223 if elem_prop
is not None:
224 assert(elem_prop
.props
[0] == elem_prop_id
)
225 if elem_prop
.props
[1] == b
'int':
226 assert(elem_prop
.props
[1] == b
'int')
227 assert(elem_prop
.props
[2] == b
'Integer')
228 elif elem_prop
.props
[1] == b
'ULongLong':
229 assert(elem_prop
.props
[1] == b
'ULongLong')
230 assert(elem_prop
.props
[2] == b
'')
232 # we could allow other number types
233 assert(elem_prop
.props_type
[4] in {data_types
.INT32
, data_types
.INT64
})
235 return elem_prop
.props
[4]
239 def elem_props_get_bool(elem
, elem_prop_id
, default
=None):
240 elem_prop
= elem_props_find_first(elem
, elem_prop_id
)
241 if elem_prop
is not None:
242 assert(elem_prop
.props
[0] == elem_prop_id
)
243 # b'Bool' with a capital seems to be used for animated property... go figure...
244 assert(elem_prop
.props
[1] in {b
'bool', b
'Bool'})
245 assert(elem_prop
.props
[2] == b
'')
247 # we could allow other number types
248 assert(elem_prop
.props_type
[4] == data_types
.INT32
)
249 assert(elem_prop
.props
[4] in {0, 1})
251 return bool(elem_prop
.props
[4])
255 def elem_props_get_enum(elem
, elem_prop_id
, default
=None):
256 elem_prop
= elem_props_find_first(elem
, elem_prop_id
)
257 if elem_prop
is not None:
258 assert(elem_prop
.props
[0] == elem_prop_id
)
259 assert(elem_prop
.props
[1] == b
'enum')
260 assert(elem_prop
.props
[2] == b
'')
261 assert(elem_prop
.props
[3] == b
'')
263 # we could allow other number types
264 assert(elem_prop
.props_type
[4] == data_types
.INT32
)
266 return elem_prop
.props
[4]
270 def elem_props_get_visibility(elem
, elem_prop_id
, default
=None):
271 elem_prop
= elem_props_find_first(elem
, elem_prop_id
)
272 if elem_prop
is not None:
273 assert(elem_prop
.props
[0] == elem_prop_id
)
274 assert(elem_prop
.props
[1] == b
'Visibility')
275 assert(elem_prop
.props
[2] == b
'')
277 # we could allow other number types
278 assert(elem_prop
.props_type
[4] == data_types
.FLOAT64
)
280 return elem_prop
.props
[4]
284 # ----------------------------------------------------------------------------
289 from collections
import namedtuple
292 FBXTransformData
= namedtuple("FBXTransformData", (
294 "rot", "rot_ofs", "rot_piv", "pre_rot", "pst_rot", "rot_ord", "rot_alt_mat", "geom_rot",
295 "sca", "sca_ofs", "sca_piv", "geom_sca",
299 def blen_read_custom_properties(fbx_obj
, blen_obj
, settings
):
300 # There doesn't seem to be a way to put user properties into templates, so this only get the object properties:
301 fbx_obj_props
= elem_find_first(fbx_obj
, b
'Properties70')
303 for fbx_prop
in fbx_obj_props
.elems
:
304 assert(fbx_prop
.id == b
'P')
306 if b
'U' in fbx_prop
.props
[3]:
307 if fbx_prop
.props
[0] == b
'UDP3DSMAX':
308 # Special case for 3DS Max user properties:
309 assert(fbx_prop
.props
[1] == b
'KString')
310 assert(fbx_prop
.props_type
[4] == data_types
.STRING
)
311 items
= fbx_prop
.props
[4].decode('utf-8', 'replace')
312 for item
in items
.split('\r\n'):
314 split_item
= item
.split('=', 1)
315 if len(split_item
) != 2:
316 split_item
= item
.split(':', 1)
317 if len(split_item
) != 2:
318 print("cannot parse UDP3DSMAX custom property '%s', ignoring..." % item
)
320 prop_name
, prop_value
= split_item
321 prop_name
= validate_blend_names(prop_name
.strip().encode('utf-8'))
322 blen_obj
[prop_name
] = prop_value
.strip()
324 prop_name
= validate_blend_names(fbx_prop
.props
[0])
325 prop_type
= fbx_prop
.props
[1]
326 if prop_type
in {b
'Vector', b
'Vector3D', b
'Color', b
'ColorRGB'}:
327 assert(fbx_prop
.props_type
[4:7] == bytes((data_types
.FLOAT64
,)) * 3)
328 blen_obj
[prop_name
] = fbx_prop
.props
[4:7]
329 elif prop_type
in {b
'Vector4', b
'ColorRGBA'}:
330 assert(fbx_prop
.props_type
[4:8] == bytes((data_types
.FLOAT64
,)) * 4)
331 blen_obj
[prop_name
] = fbx_prop
.props
[4:8]
332 elif prop_type
== b
'Vector2D':
333 assert(fbx_prop
.props_type
[4:6] == bytes((data_types
.FLOAT64
,)) * 2)
334 blen_obj
[prop_name
] = fbx_prop
.props
[4:6]
335 elif prop_type
in {b
'Integer', b
'int'}:
336 assert(fbx_prop
.props_type
[4] == data_types
.INT32
)
337 blen_obj
[prop_name
] = fbx_prop
.props
[4]
338 elif prop_type
== b
'KString':
339 assert(fbx_prop
.props_type
[4] == data_types
.STRING
)
340 blen_obj
[prop_name
] = fbx_prop
.props
[4].decode('utf-8', 'replace')
341 elif prop_type
in {b
'Number', b
'double', b
'Double'}:
342 assert(fbx_prop
.props_type
[4] == data_types
.FLOAT64
)
343 blen_obj
[prop_name
] = fbx_prop
.props
[4]
344 elif prop_type
in {b
'Float', b
'float'}:
345 assert(fbx_prop
.props_type
[4] == data_types
.FLOAT32
)
346 blen_obj
[prop_name
] = fbx_prop
.props
[4]
347 elif prop_type
in {b
'Bool', b
'bool'}:
348 assert(fbx_prop
.props_type
[4] == data_types
.INT32
)
349 blen_obj
[prop_name
] = fbx_prop
.props
[4] != 0
350 elif prop_type
in {b
'Enum', b
'enum'}:
351 assert(fbx_prop
.props_type
[4:6] == bytes((data_types
.INT32
, data_types
.STRING
)))
352 val
= fbx_prop
.props
[4]
353 if settings
.use_custom_props_enum_as_string
and fbx_prop
.props
[5]:
354 enum_items
= fbx_prop
.props
[5].decode('utf-8', 'replace').split('~')
355 if val
>= 0 and val
< len(enum_items
):
356 blen_obj
[prop_name
] = enum_items
[val
]
358 print ("WARNING: User property '%s' has wrong enum value, skipped" % prop_name
)
360 blen_obj
[prop_name
] = val
362 print ("WARNING: User property type '%s' is not supported" % prop_type
.decode('utf-8', 'replace'))
365 def blen_read_object_transform_do(transform_data
):
366 # This is a nightmare. FBX SDK uses Maya way to compute the transformation matrix of a node - utterly simple:
368 # WorldTransform = ParentWorldTransform @ T @ Roff @ Rp @ Rpre @ R @ Rpost @ Rp-1 @ Soff @ Sp @ S @ Sp-1
370 # Where all those terms are 4 x 4 matrices that contain:
371 # WorldTransform: Transformation matrix of the node in global space.
372 # ParentWorldTransform: Transformation matrix of the parent node in global space.
374 # Roff: Rotation offset
378 # Rpost: Post-rotation
379 # Rp-1: Inverse of the rotation pivot
380 # Soff: Scaling offset
383 # Sp-1: Inverse of the scaling pivot
385 # But it was still too simple, and FBX notion of compatibility is... quite specific. So we also have to
386 # support 3DSMax way:
388 # WorldTransform = ParentWorldTransform @ T @ R @ S @ OT @ OR @ OS
390 # Where all those terms are 4 x 4 matrices that contain:
391 # WorldTransform: Transformation matrix of the node in global space
392 # ParentWorldTransform: Transformation matrix of the parent node in global space
396 # OT: Geometric transform translation
397 # OR: Geometric transform rotation
398 # OS: Geometric transform translation
401 # Geometric transformations ***are not inherited***: ParentWorldTransform does not contain the OT, OR, OS
402 # of WorldTransform's parent node.
404 # Taken from http://download.autodesk.com/us/fbx/20112/FBX_SDK_HELP/
405 # index.html?url=WS1a9193826455f5ff1f92379812724681e696651.htm,topicNumber=d0e7429
408 lcl_translation
= Matrix
.Translation(transform_data
.loc
)
409 geom_loc
= Matrix
.Translation(transform_data
.geom_loc
)
412 to_rot
= lambda rot
, rot_ord
: Euler(convert_deg_to_rad_iter(rot
), rot_ord
).to_matrix().to_4x4()
413 lcl_rot
= to_rot(transform_data
.rot
, transform_data
.rot_ord
) @ transform_data
.rot_alt_mat
414 pre_rot
= to_rot(transform_data
.pre_rot
, transform_data
.rot_ord
)
415 pst_rot
= to_rot(transform_data
.pst_rot
, transform_data
.rot_ord
)
416 geom_rot
= to_rot(transform_data
.geom_rot
, transform_data
.rot_ord
)
418 rot_ofs
= Matrix
.Translation(transform_data
.rot_ofs
)
419 rot_piv
= Matrix
.Translation(transform_data
.rot_piv
)
420 sca_ofs
= Matrix
.Translation(transform_data
.sca_ofs
)
421 sca_piv
= Matrix
.Translation(transform_data
.sca_piv
)
425 lcl_scale
[0][0], lcl_scale
[1][1], lcl_scale
[2][2] = transform_data
.sca
426 geom_scale
= Matrix();
427 geom_scale
[0][0], geom_scale
[1][1], geom_scale
[2][2] = transform_data
.geom_sca
436 rot_piv
.inverted_safe() @
440 sca_piv
.inverted_safe()
442 geom_mat
= geom_loc
@ geom_rot
@ geom_scale
443 # We return mat without 'geometric transforms' too, because it is to be used for children, sigh...
444 return (base_mat
@ geom_mat
, base_mat
, geom_mat
)
447 # XXX This might be weak, now that we can add vgroups from both bones and shapes, name collisions become
448 # more likely, will have to make this more robust!!!
449 def add_vgroup_to_objects(vg_indices
, vg_weights
, vg_name
, objects
):
450 assert(len(vg_indices
) == len(vg_weights
))
453 # We replace/override here...
454 vg
= obj
.vertex_groups
.get(vg_name
)
456 vg
= obj
.vertex_groups
.new(name
=vg_name
)
457 for i
, w
in zip(vg_indices
, vg_weights
):
458 vg
.add((i
,), w
, 'REPLACE')
461 def blen_read_object_transform_preprocess(fbx_props
, fbx_obj
, rot_alt_mat
, use_prepost_rot
):
462 # This is quite involved, 'fbxRNode.cpp' from openscenegraph used as a reference
463 const_vector_zero_3d
= 0.0, 0.0, 0.0
464 const_vector_one_3d
= 1.0, 1.0, 1.0
466 loc
= list(elem_props_get_vector_3d(fbx_props
, b
'Lcl Translation', const_vector_zero_3d
))
467 rot
= list(elem_props_get_vector_3d(fbx_props
, b
'Lcl Rotation', const_vector_zero_3d
))
468 sca
= list(elem_props_get_vector_3d(fbx_props
, b
'Lcl Scaling', const_vector_one_3d
))
470 geom_loc
= list(elem_props_get_vector_3d(fbx_props
, b
'GeometricTranslation', const_vector_zero_3d
))
471 geom_rot
= list(elem_props_get_vector_3d(fbx_props
, b
'GeometricRotation', const_vector_zero_3d
))
472 geom_sca
= list(elem_props_get_vector_3d(fbx_props
, b
'GeometricScaling', const_vector_one_3d
))
474 rot_ofs
= elem_props_get_vector_3d(fbx_props
, b
'RotationOffset', const_vector_zero_3d
)
475 rot_piv
= elem_props_get_vector_3d(fbx_props
, b
'RotationPivot', const_vector_zero_3d
)
476 sca_ofs
= elem_props_get_vector_3d(fbx_props
, b
'ScalingOffset', const_vector_zero_3d
)
477 sca_piv
= elem_props_get_vector_3d(fbx_props
, b
'ScalingPivot', const_vector_zero_3d
)
479 is_rot_act
= elem_props_get_bool(fbx_props
, b
'RotationActive', False)
483 pre_rot
= elem_props_get_vector_3d(fbx_props
, b
'PreRotation', const_vector_zero_3d
)
484 pst_rot
= elem_props_get_vector_3d(fbx_props
, b
'PostRotation', const_vector_zero_3d
)
486 pre_rot
= const_vector_zero_3d
487 pst_rot
= const_vector_zero_3d
495 6: 'XYZ', # XXX eSphericXYZ, not really supported...
496 }.get(elem_props_get_enum(fbx_props
, b
'RotationOrder', 0))
498 pre_rot
= const_vector_zero_3d
499 pst_rot
= const_vector_zero_3d
502 return FBXTransformData(loc
, geom_loc
,
503 rot
, rot_ofs
, rot_piv
, pre_rot
, pst_rot
, rot_ord
, rot_alt_mat
, geom_rot
,
504 sca
, sca_ofs
, sca_piv
, geom_sca
)
509 def blen_read_animations_curves_iter(fbx_curves
, blen_start_offset
, fbx_start_offset
, fps
):
511 Get raw FBX AnimCurve list, and yield values for all curves at each singular curves' keyframes,
512 together with (blender) timing, in frames.
513 blen_start_offset is expected in frames, while fbx_start_offset is expected in FBX ktime.
515 # As a first step, assume linear interpolation between key frames, we'll (try to!) handle more
516 # of FBX curves later.
517 from .fbx_utils
import FBX_KTIME
518 timefac
= fps
/ FBX_KTIME
521 elem_prop_first(elem_find_first(c
[2], b
'KeyTime')),
522 elem_prop_first(elem_find_first(c
[2], b
'KeyValueFloat')),
526 allkeys
= sorted({item
for sublist
in curves
for item
in sublist
[1]})
527 for curr_fbxktime
in allkeys
:
530 idx
, times
, values
, fbx_curve
= item
532 if times
[idx
] < curr_fbxktime
:
535 if idx
>= len(times
):
536 # We have reached our last element for this curve, stay on it from now on...
540 if times
[idx
] >= curr_fbxktime
:
542 curr_values
.append((values
[idx
], fbx_curve
))
544 # Interpolate between this key and the previous one.
545 ifac
= (curr_fbxktime
- times
[idx
- 1]) / (times
[idx
] - times
[idx
- 1])
546 curr_values
.append(((values
[idx
] - values
[idx
- 1]) * ifac
+ values
[idx
- 1], fbx_curve
))
547 curr_blenkframe
= (curr_fbxktime
- fbx_start_offset
) * timefac
+ blen_start_offset
548 yield (curr_blenkframe
, curr_values
)
551 def blen_read_animations_action_item(action
, item
, cnodes
, fps
, anim_offset
, global_scale
):
553 'Bake' loc/rot/scale into the action,
554 taking any pre_ and post_ matrix into account to transform from fbx into blender space.
556 from bpy
.types
import Object
, PoseBone
, ShapeKey
, Material
, Camera
557 from itertools
import chain
560 for curves
, fbxprop
in cnodes
.values():
561 for (fbx_acdata
, _blen_data
), channel
in curves
.values():
562 fbx_curves
.append((fbxprop
, channel
, fbx_acdata
))
564 # Leave if no curves are attached (if a blender curve is attached to scale but without keys it defaults to 0).
565 if len(fbx_curves
) == 0:
572 # Add each keyframe to the keyframe dict
573 def store_keyframe(fc
, frame
, value
):
574 fc_key
= (fc
.data_path
, fc
.array_index
)
575 if not keyframes
.get(fc_key
):
576 keyframes
[fc_key
] = []
577 keyframes
[fc_key
].extend((frame
, value
))
579 if isinstance(item
, Material
):
581 props
= [("diffuse_color", 3, grpname
or "Diffuse Color")]
582 elif isinstance(item
, ShapeKey
):
583 props
= [(item
.path_from_id("value"), 1, "Key")]
584 elif isinstance(item
, Camera
):
585 props
= [(item
.path_from_id("lens"), 1, "Camera"), (item
.dof
.path_from_id("focus_distance"), 1, "Camera")]
586 else: # Object or PoseBone:
588 bl_obj
= item
.bl_obj
.pose
.bones
[item
.bl_bone
]
592 # We want to create actions for objects, but for bones we 'reuse' armatures' actions!
593 grpname
= bl_obj
.name
595 # Since we might get other channels animated in the end, due to all FBX transform magic,
596 # we need to add curves for whole loc/rot/scale in any case.
597 props
= [(bl_obj
.path_from_id("location"), 3, grpname
or "Location"),
599 (bl_obj
.path_from_id("scale"), 3, grpname
or "Scale")]
600 rot_mode
= bl_obj
.rotation_mode
601 if rot_mode
== 'QUATERNION':
602 props
[1] = (bl_obj
.path_from_id("rotation_quaternion"), 4, grpname
or "Quaternion Rotation")
603 elif rot_mode
== 'AXIS_ANGLE':
604 props
[1] = (bl_obj
.path_from_id("rotation_axis_angle"), 4, grpname
or "Axis Angle Rotation")
606 props
[1] = (bl_obj
.path_from_id("rotation_euler"), 3, grpname
or "Euler Rotation")
608 blen_curves
= [action
.fcurves
.new(prop
, index
=channel
, action_group
=grpname
)
609 for prop
, nbr_channels
, grpname
in props
for channel
in range(nbr_channels
)]
611 if isinstance(item
, Material
):
612 for frame
, values
in blen_read_animations_curves_iter(fbx_curves
, anim_offset
, 0, fps
):
614 for v
, (fbxprop
, channel
, _fbx_acdata
) in values
:
615 assert(fbxprop
== b
'DiffuseColor')
616 assert(channel
in {0, 1, 2})
619 for fc
, v
in zip(blen_curves
, value
):
620 store_keyframe(fc
, frame
, v
)
622 elif isinstance(item
, ShapeKey
):
623 for frame
, values
in blen_read_animations_curves_iter(fbx_curves
, anim_offset
, 0, fps
):
625 for v
, (fbxprop
, channel
, _fbx_acdata
) in values
:
626 assert(fbxprop
== b
'DeformPercent')
630 for fc
, v
in zip(blen_curves
, (value
,)):
631 store_keyframe(fc
, frame
, v
)
633 elif isinstance(item
, Camera
):
634 for frame
, values
in blen_read_animations_curves_iter(fbx_curves
, anim_offset
, 0, fps
):
637 for v
, (fbxprop
, channel
, _fbx_acdata
) in values
:
638 assert(fbxprop
== b
'FocalLength' or fbxprop
== b
'FocusDistance' )
640 if (fbxprop
== b
'FocalLength' ):
642 elif(fbxprop
== b
'FocusDistance'):
643 focus_distance
= v
/ 1000 * global_scale
645 for fc
, v
in zip(blen_curves
, (focal_length
, focus_distance
)):
646 store_keyframe(fc
, frame
, v
)
648 else: # Object or PoseBone:
650 bl_obj
= item
.bl_obj
.pose
.bones
[item
.bl_bone
]
654 transform_data
= item
.fbx_transform_data
655 rot_eul_prev
= bl_obj
.rotation_euler
.copy()
656 rot_quat_prev
= bl_obj
.rotation_quaternion
.copy()
658 # Pre-compute inverted local rest matrix of the bone, if relevant.
659 restmat_inv
= item
.get_bind_matrix().inverted_safe() if item
.is_bone
else None
661 for frame
, values
in blen_read_animations_curves_iter(fbx_curves
, anim_offset
, 0, fps
):
662 for v
, (fbxprop
, channel
, _fbx_acdata
) in values
:
663 if fbxprop
== b
'Lcl Translation':
664 transform_data
.loc
[channel
] = v
665 elif fbxprop
== b
'Lcl Rotation':
666 transform_data
.rot
[channel
] = v
667 elif fbxprop
== b
'Lcl Scaling':
668 transform_data
.sca
[channel
] = v
669 mat
, _
, _
= blen_read_object_transform_do(transform_data
)
671 # compensate for changes in the local matrix during processing
672 if item
.anim_compensation_matrix
:
673 mat
= mat
@ item
.anim_compensation_matrix
675 # apply pre- and post matrix
676 # post-matrix will contain any correction for lights, camera and bone orientation
677 # pre-matrix will contain any correction for a parent's correction matrix or the global matrix
679 mat
= item
.pre_matrix
@ mat
681 mat
= mat
@ item
.post_matrix
683 # And now, remove that rest pose matrix from current mat (also in parent space).
685 mat
= restmat_inv
@ mat
687 # Now we have a virtual matrix of transform from AnimCurves, we can insert keyframes!
688 loc
, rot
, sca
= mat
.decompose()
689 if rot_mode
== 'QUATERNION':
690 if rot_quat_prev
.dot(rot
) < 0.0:
693 elif rot_mode
== 'AXIS_ANGLE':
694 vec
, ang
= rot
.to_axis_angle()
695 rot
= ang
, vec
.x
, vec
.y
, vec
.z
697 rot
= rot
.to_euler(rot_mode
, rot_eul_prev
)
700 # Add each keyframe and its value to the keyframe dict
701 for fc
, value
in zip(blen_curves
, chain(loc
, rot
, sca
)):
702 store_keyframe(fc
, frame
, value
)
704 # Add all keyframe points to the fcurves at once and modify them after
705 for fc_key
, key_values
in keyframes
.items():
706 data_path
, index
= fc_key
708 # Add all keyframe points at once
709 fcurve
= action
.fcurves
.find(data_path
=data_path
, index
=index
)
710 num_keys
= len(key_values
) // 2
711 fcurve
.keyframe_points
.add(num_keys
)
712 fcurve
.keyframe_points
.foreach_set('co', key_values
)
713 linear_enum_value
= bpy
.types
.Keyframe
.bl_rna
.properties
['interpolation'].enum_items
['LINEAR'].value
714 fcurve
.keyframe_points
.foreach_set('interpolation', (linear_enum_value
,) * num_keys
)
716 # Since we inserted our keyframes in 'ultra-fast' mode, we have to update the fcurves now.
717 for fc
in blen_curves
:
721 def blen_read_animations(fbx_tmpl_astack
, fbx_tmpl_alayer
, stacks
, scene
, anim_offset
, global_scale
):
723 Recreate an action per stack/layer/object combinations.
724 Only the first found action is linked to objects, more complex setups are not handled,
725 it's up to user to reproduce them!
727 from bpy
.types
import ShapeKey
, Material
, Camera
730 for as_uuid
, ((fbx_asdata
, _blen_data
), alayers
) in stacks
.items():
731 stack_name
= elem_name_ensure_class(fbx_asdata
, b
'AnimStack')
732 for al_uuid
, ((fbx_aldata
, _blen_data
), items
) in alayers
.items():
733 layer_name
= elem_name_ensure_class(fbx_aldata
, b
'AnimLayer')
734 for item
, cnodes
in items
.items():
735 if isinstance(item
, Material
):
737 elif isinstance(item
, ShapeKey
):
738 id_data
= item
.id_data
739 elif isinstance(item
, Camera
):
742 id_data
= item
.bl_obj
743 # XXX Ignore rigged mesh animations - those are a nightmare to handle, see note about it in
744 # FbxImportHelperNode class definition.
745 if id_data
and id_data
.type == 'MESH' and id_data
.parent
and id_data
.parent
.type == 'ARMATURE':
750 # Create new action if needed (should always be needed, except for keyblocks from shapekeys cases).
751 key
= (as_uuid
, al_uuid
, id_data
)
752 action
= actions
.get(key
)
754 if stack_name
== layer_name
:
755 action_name
= "|".join((id_data
.name
, stack_name
))
757 action_name
= "|".join((id_data
.name
, stack_name
, layer_name
))
758 actions
[key
] = action
= bpy
.data
.actions
.new(action_name
)
759 action
.use_fake_user
= True
760 # If none yet assigned, assign this action to id_data.
761 if not id_data
.animation_data
:
762 id_data
.animation_data_create()
763 if not id_data
.animation_data
.action
:
764 id_data
.animation_data
.action
= action
765 # And actually populate the action!
766 blen_read_animations_action_item(action
, item
, cnodes
, scene
.render
.fps
, anim_offset
, global_scale
)
772 def blen_read_geom_layerinfo(fbx_layer
):
774 validate_blend_names(elem_find_first_string_as_bytes(fbx_layer
, b
'Name')),
775 elem_find_first_string_as_bytes(fbx_layer
, b
'MappingInformationType'),
776 elem_find_first_string_as_bytes(fbx_layer
, b
'ReferenceInformationType'),
780 def blen_read_geom_array_setattr(generator
, blen_data
, blen_attr
, fbx_data
, stride
, item_size
, descr
, xform
):
781 """Generic fbx_layer to blen_data setter, generator is expected to yield tuples (ble_idx, fbx_idx)."""
782 max_blen_idx
= len(blen_data
) - 1
783 max_fbx_idx
= len(fbx_data
) - 1
786 def check_skip(blen_idx
, fbx_idx
):
788 if fbx_idx
< 0: # Negative values mean 'skip'.
790 if blen_idx
> max_blen_idx
:
792 print("ERROR: too much data in this Blender layer, compared to elements in mesh, skipping!")
795 if fbx_idx
+ item_size
- 1 > max_fbx_idx
:
797 print("ERROR: not enough data in this FBX layer, skipping!")
802 if xform
is not None:
803 if isinstance(blen_data
, list):
805 def _process(blend_data
, blen_attr
, fbx_data
, xform
, item_size
, blen_idx
, fbx_idx
):
806 blen_data
[blen_idx
] = xform(fbx_data
[fbx_idx
])
808 def _process(blend_data
, blen_attr
, fbx_data
, xform
, item_size
, blen_idx
, fbx_idx
):
809 blen_data
[blen_idx
] = xform(fbx_data
[fbx_idx
:fbx_idx
+ item_size
])
812 def _process(blend_data
, blen_attr
, fbx_data
, xform
, item_size
, blen_idx
, fbx_idx
):
813 setattr(blen_data
[blen_idx
], blen_attr
, xform(fbx_data
[fbx_idx
]))
815 def _process(blend_data
, blen_attr
, fbx_data
, xform
, item_size
, blen_idx
, fbx_idx
):
816 setattr(blen_data
[blen_idx
], blen_attr
, xform(fbx_data
[fbx_idx
:fbx_idx
+ item_size
]))
818 if isinstance(blen_data
, list):
820 def _process(blend_data
, blen_attr
, fbx_data
, xform
, item_size
, blen_idx
, fbx_idx
):
821 blen_data
[blen_idx
] = fbx_data
[fbx_idx
]
823 def _process(blend_data
, blen_attr
, fbx_data
, xform
, item_size
, blen_idx
, fbx_idx
):
824 blen_data
[blen_idx
] = fbx_data
[fbx_idx
:fbx_idx
+ item_size
]
827 def _process(blend_data
, blen_attr
, fbx_data
, xform
, item_size
, blen_idx
, fbx_idx
):
828 setattr(blen_data
[blen_idx
], blen_attr
, fbx_data
[fbx_idx
])
830 def _process(blend_data
, blen_attr
, fbx_data
, xform
, item_size
, blen_idx
, fbx_idx
):
831 setattr(blen_data
[blen_idx
], blen_attr
, fbx_data
[fbx_idx
:fbx_idx
+ item_size
])
833 for blen_idx
, fbx_idx
in generator
:
834 if check_skip(blen_idx
, fbx_idx
):
836 _process(blen_data
, blen_attr
, fbx_data
, xform
, item_size
, blen_idx
, fbx_idx
)
839 # generic generators.
840 def blen_read_geom_array_gen_allsame(data_len
):
841 return zip(*(range(data_len
), (0,) * data_len
))
844 def blen_read_geom_array_gen_direct(fbx_data
, stride
):
845 fbx_data_len
= len(fbx_data
)
846 return zip(*(range(fbx_data_len
// stride
), range(0, fbx_data_len
, stride
)))
849 def blen_read_geom_array_gen_indextodirect(fbx_layer_index
, stride
):
850 return ((bi
, fi
* stride
) for bi
, fi
in enumerate(fbx_layer_index
))
853 def blen_read_geom_array_gen_direct_looptovert(mesh
, fbx_data
, stride
):
854 fbx_data_len
= len(fbx_data
) // stride
856 for p
in mesh
.polygons
:
857 for lidx
in p
.loop_indices
:
858 vidx
= loops
[lidx
].vertex_index
859 if vidx
< fbx_data_len
:
860 yield lidx
, vidx
* stride
863 # generic error printers.
864 def blen_read_geom_array_error_mapping(descr
, fbx_layer_mapping
, quiet
=False):
866 print("warning layer %r mapping type unsupported: %r" % (descr
, fbx_layer_mapping
))
869 def blen_read_geom_array_error_ref(descr
, fbx_layer_ref
, quiet
=False):
871 print("warning layer %r ref type unsupported: %r" % (descr
, fbx_layer_ref
))
874 def blen_read_geom_array_mapped_vert(
875 mesh
, blen_data
, blen_attr
,
876 fbx_layer_data
, fbx_layer_index
,
877 fbx_layer_mapping
, fbx_layer_ref
,
878 stride
, item_size
, descr
,
879 xform
=None, quiet
=False,
881 if fbx_layer_mapping
== b
'ByVertice':
882 if fbx_layer_ref
== b
'Direct':
883 assert(fbx_layer_index
is None)
884 blen_read_geom_array_setattr(blen_read_geom_array_gen_direct(fbx_layer_data
, stride
),
885 blen_data
, blen_attr
, fbx_layer_data
, stride
, item_size
, descr
, xform
)
887 blen_read_geom_array_error_ref(descr
, fbx_layer_ref
, quiet
)
888 elif fbx_layer_mapping
== b
'AllSame':
889 if fbx_layer_ref
== b
'IndexToDirect':
890 assert(fbx_layer_index
is None)
891 blen_read_geom_array_setattr(blen_read_geom_array_gen_allsame(len(blen_data
)),
892 blen_data
, blen_attr
, fbx_layer_data
, stride
, item_size
, descr
, xform
)
894 blen_read_geom_array_error_ref(descr
, fbx_layer_ref
, quiet
)
896 blen_read_geom_array_error_mapping(descr
, fbx_layer_mapping
, quiet
)
901 def blen_read_geom_array_mapped_edge(
902 mesh
, blen_data
, blen_attr
,
903 fbx_layer_data
, fbx_layer_index
,
904 fbx_layer_mapping
, fbx_layer_ref
,
905 stride
, item_size
, descr
,
906 xform
=None, quiet
=False,
908 if fbx_layer_mapping
== b
'ByEdge':
909 if fbx_layer_ref
== b
'Direct':
910 blen_read_geom_array_setattr(blen_read_geom_array_gen_direct(fbx_layer_data
, stride
),
911 blen_data
, blen_attr
, fbx_layer_data
, stride
, item_size
, descr
, xform
)
913 blen_read_geom_array_error_ref(descr
, fbx_layer_ref
, quiet
)
914 elif fbx_layer_mapping
== b
'AllSame':
915 if fbx_layer_ref
== b
'IndexToDirect':
916 assert(fbx_layer_index
is None)
917 blen_read_geom_array_setattr(blen_read_geom_array_gen_allsame(len(blen_data
)),
918 blen_data
, blen_attr
, fbx_layer_data
, stride
, item_size
, descr
, xform
)
920 blen_read_geom_array_error_ref(descr
, fbx_layer_ref
, quiet
)
922 blen_read_geom_array_error_mapping(descr
, fbx_layer_mapping
, quiet
)
927 def blen_read_geom_array_mapped_polygon(
928 mesh
, blen_data
, blen_attr
,
929 fbx_layer_data
, fbx_layer_index
,
930 fbx_layer_mapping
, fbx_layer_ref
,
931 stride
, item_size
, descr
,
932 xform
=None, quiet
=False,
934 if fbx_layer_mapping
== b
'ByPolygon':
935 if fbx_layer_ref
== b
'IndexToDirect':
936 # XXX Looks like we often get no fbx_layer_index in this case, shall not happen but happens...
937 # We fallback to 'Direct' mapping in this case.
938 #~ assert(fbx_layer_index is not None)
939 if fbx_layer_index
is None:
940 blen_read_geom_array_setattr(blen_read_geom_array_gen_direct(fbx_layer_data
, stride
),
941 blen_data
, blen_attr
, fbx_layer_data
, stride
, item_size
, descr
, xform
)
943 blen_read_geom_array_setattr(blen_read_geom_array_gen_indextodirect(fbx_layer_index
, stride
),
944 blen_data
, blen_attr
, fbx_layer_data
, stride
, item_size
, descr
, xform
)
946 elif fbx_layer_ref
== b
'Direct':
947 blen_read_geom_array_setattr(blen_read_geom_array_gen_direct(fbx_layer_data
, stride
),
948 blen_data
, blen_attr
, fbx_layer_data
, stride
, item_size
, descr
, xform
)
950 blen_read_geom_array_error_ref(descr
, fbx_layer_ref
, quiet
)
951 elif fbx_layer_mapping
== b
'AllSame':
952 if fbx_layer_ref
== b
'IndexToDirect':
953 assert(fbx_layer_index
is None)
954 blen_read_geom_array_setattr(blen_read_geom_array_gen_allsame(len(blen_data
)),
955 blen_data
, blen_attr
, fbx_layer_data
, stride
, item_size
, descr
, xform
)
957 blen_read_geom_array_error_ref(descr
, fbx_layer_ref
, quiet
)
959 blen_read_geom_array_error_mapping(descr
, fbx_layer_mapping
, quiet
)
964 def blen_read_geom_array_mapped_polyloop(
965 mesh
, blen_data
, blen_attr
,
966 fbx_layer_data
, fbx_layer_index
,
967 fbx_layer_mapping
, fbx_layer_ref
,
968 stride
, item_size
, descr
,
969 xform
=None, quiet
=False,
971 if fbx_layer_mapping
== b
'ByPolygonVertex':
972 if fbx_layer_ref
== b
'IndexToDirect':
973 # XXX Looks like we often get no fbx_layer_index in this case, shall not happen but happens...
974 # We fallback to 'Direct' mapping in this case.
975 #~ assert(fbx_layer_index is not None)
976 if fbx_layer_index
is None:
977 blen_read_geom_array_setattr(blen_read_geom_array_gen_direct(fbx_layer_data
, stride
),
978 blen_data
, blen_attr
, fbx_layer_data
, stride
, item_size
, descr
, xform
)
980 blen_read_geom_array_setattr(blen_read_geom_array_gen_indextodirect(fbx_layer_index
, stride
),
981 blen_data
, blen_attr
, fbx_layer_data
, stride
, item_size
, descr
, xform
)
983 elif fbx_layer_ref
== b
'Direct':
984 blen_read_geom_array_setattr(blen_read_geom_array_gen_direct(fbx_layer_data
, stride
),
985 blen_data
, blen_attr
, fbx_layer_data
, stride
, item_size
, descr
, xform
)
987 blen_read_geom_array_error_ref(descr
, fbx_layer_ref
, quiet
)
988 elif fbx_layer_mapping
== b
'ByVertice':
989 if fbx_layer_ref
== b
'Direct':
990 assert(fbx_layer_index
is None)
991 blen_read_geom_array_setattr(blen_read_geom_array_gen_direct_looptovert(mesh
, fbx_layer_data
, stride
),
992 blen_data
, blen_attr
, fbx_layer_data
, stride
, item_size
, descr
, xform
)
994 blen_read_geom_array_error_ref(descr
, fbx_layer_ref
, quiet
)
995 elif fbx_layer_mapping
== b
'AllSame':
996 if fbx_layer_ref
== b
'IndexToDirect':
997 assert(fbx_layer_index
is None)
998 blen_read_geom_array_setattr(blen_read_geom_array_gen_allsame(len(blen_data
)),
999 blen_data
, blen_attr
, fbx_layer_data
, stride
, item_size
, descr
, xform
)
1001 blen_read_geom_array_error_ref(descr
, fbx_layer_ref
, quiet
)
1003 blen_read_geom_array_error_mapping(descr
, fbx_layer_mapping
, quiet
)
1008 def blen_read_geom_layer_material(fbx_obj
, mesh
):
1009 fbx_layer
= elem_find_first(fbx_obj
, b
'LayerElementMaterial')
1011 if fbx_layer
is None:
1017 ) = blen_read_geom_layerinfo(fbx_layer
)
1019 layer_id
= b
'Materials'
1020 fbx_layer_data
= elem_prop_first(elem_find_first(fbx_layer
, layer_id
))
1022 blen_data
= mesh
.polygons
1023 blen_read_geom_array_mapped_polygon(
1024 mesh
, blen_data
, "material_index",
1025 fbx_layer_data
, None,
1026 fbx_layer_mapping
, fbx_layer_ref
,
1031 def blen_read_geom_layer_uv(fbx_obj
, mesh
):
1032 for layer_id
in (b
'LayerElementUV',):
1033 for fbx_layer
in elem_find_iter(fbx_obj
, layer_id
):
1034 # all should be valid
1038 ) = blen_read_geom_layerinfo(fbx_layer
)
1040 fbx_layer_data
= elem_prop_first(elem_find_first(fbx_layer
, b
'UV'))
1041 fbx_layer_index
= elem_prop_first(elem_find_first(fbx_layer
, b
'UVIndex'))
1043 # Always init our new layers with (0, 0) UVs.
1044 uv_lay
= mesh
.uv_layers
.new(name
=fbx_layer_name
, do_init
=False)
1046 print("Failed to add {%r %r} UVLayer to %r (probably too many of them?)"
1047 "" % (layer_id
, fbx_layer_name
, mesh
.name
))
1050 blen_data
= uv_lay
.data
1052 # some valid files omit this data
1053 if fbx_layer_data
is None:
1054 print("%r %r missing data" % (layer_id
, fbx_layer_name
))
1057 blen_read_geom_array_mapped_polyloop(
1058 mesh
, blen_data
, "uv",
1059 fbx_layer_data
, fbx_layer_index
,
1060 fbx_layer_mapping
, fbx_layer_ref
,
1065 def blen_read_geom_layer_color(fbx_obj
, mesh
, colors_type
):
1066 if colors_type
== 'NONE':
1068 use_srgb
= colors_type
== 'SRGB'
1069 layer_type
= 'BYTE_COLOR' if use_srgb
else 'FLOAT_COLOR'
1070 color_prop_name
= "color_srgb" if use_srgb
else "color"
1071 # almost same as UVs
1072 for layer_id
in (b
'LayerElementColor',):
1073 for fbx_layer
in elem_find_iter(fbx_obj
, layer_id
):
1074 # all should be valid
1078 ) = blen_read_geom_layerinfo(fbx_layer
)
1080 fbx_layer_data
= elem_prop_first(elem_find_first(fbx_layer
, b
'Colors'))
1081 fbx_layer_index
= elem_prop_first(elem_find_first(fbx_layer
, b
'ColorIndex'))
1083 color_lay
= mesh
.color_attributes
.new(name
=fbx_layer_name
, type=layer_type
, domain
='CORNER')
1085 if color_lay
is None:
1086 print("Failed to add {%r %r} vertex color layer to %r (probably too many of them?)"
1087 "" % (layer_id
, fbx_layer_name
, mesh
.name
))
1090 blen_data
= color_lay
.data
1092 # some valid files omit this data
1093 if fbx_layer_data
is None:
1094 print("%r %r missing data" % (layer_id
, fbx_layer_name
))
1097 blen_read_geom_array_mapped_polyloop(
1098 mesh
, blen_data
, color_prop_name
,
1099 fbx_layer_data
, fbx_layer_index
,
1100 fbx_layer_mapping
, fbx_layer_ref
,
1105 def blen_read_geom_layer_smooth(fbx_obj
, mesh
):
1106 fbx_layer
= elem_find_first(fbx_obj
, b
'LayerElementSmoothing')
1108 if fbx_layer
is None:
1111 # all should be valid
1115 ) = blen_read_geom_layerinfo(fbx_layer
)
1117 layer_id
= b
'Smoothing'
1118 fbx_layer_data
= elem_prop_first(elem_find_first(fbx_layer
, layer_id
))
1120 # udk has 'Direct' mapped, with no Smoothing, not sure why, but ignore these
1121 if fbx_layer_data
is None:
1124 if fbx_layer_mapping
== b
'ByEdge':
1125 # some models have bad edge data, we can't use this info...
1127 print("warning skipping sharp edges data, no valid edges...")
1130 blen_data
= mesh
.edges
1131 blen_read_geom_array_mapped_edge(
1132 mesh
, blen_data
, "use_edge_sharp",
1133 fbx_layer_data
, None,
1134 fbx_layer_mapping
, fbx_layer_ref
,
1136 xform
=lambda s
: not s
,
1138 # We only set sharp edges here, not face smoothing itself...
1139 mesh
.use_auto_smooth
= True
1141 elif fbx_layer_mapping
== b
'ByPolygon':
1142 blen_data
= mesh
.polygons
1143 return blen_read_geom_array_mapped_polygon(
1144 mesh
, blen_data
, "use_smooth",
1145 fbx_layer_data
, None,
1146 fbx_layer_mapping
, fbx_layer_ref
,
1148 xform
=lambda s
: (s
!= 0), # smoothgroup bitflags, treat as booleans for now
1151 print("warning layer %r mapping type unsupported: %r" % (fbx_layer
.id, fbx_layer_mapping
))
1154 def blen_read_geom_layer_edge_crease(fbx_obj
, mesh
):
1155 from math
import sqrt
1157 fbx_layer
= elem_find_first(fbx_obj
, b
'LayerElementEdgeCrease')
1159 if fbx_layer
is None:
1162 # all should be valid
1166 ) = blen_read_geom_layerinfo(fbx_layer
)
1168 if fbx_layer_mapping
!= b
'ByEdge':
1171 layer_id
= b
'EdgeCrease'
1172 fbx_layer_data
= elem_prop_first(elem_find_first(fbx_layer
, layer_id
))
1174 # some models have bad edge data, we can't use this info...
1176 print("warning skipping edge crease data, no valid edges...")
1179 if fbx_layer_mapping
== b
'ByEdge':
1180 # some models have bad edge data, we can't use this info...
1182 print("warning skipping edge crease data, no valid edges...")
1185 blen_data
= mesh
.edges
1186 return blen_read_geom_array_mapped_edge(
1187 mesh
, blen_data
, "crease",
1188 fbx_layer_data
, None,
1189 fbx_layer_mapping
, fbx_layer_ref
,
1191 # Blender squares those values before sending them to OpenSubdiv, when other software don't,
1192 # so we need to compensate that to get similar results through FBX...
1196 print("warning layer %r mapping type unsupported: %r" % (fbx_layer
.id, fbx_layer_mapping
))
1199 def blen_read_geom_layer_normal(fbx_obj
, mesh
, xform
=None):
1200 fbx_layer
= elem_find_first(fbx_obj
, b
'LayerElementNormal')
1202 if fbx_layer
is None:
1208 ) = blen_read_geom_layerinfo(fbx_layer
)
1210 layer_id
= b
'Normals'
1211 fbx_layer_data
= elem_prop_first(elem_find_first(fbx_layer
, layer_id
))
1212 fbx_layer_index
= elem_prop_first(elem_find_first(fbx_layer
, b
'NormalsIndex'))
1214 if fbx_layer_data
is None:
1215 print("warning %r %r missing data" % (layer_id
, fbx_layer_name
))
1218 # try loops, then vertices.
1219 tries
= ((mesh
.loops
, "Loops", False, blen_read_geom_array_mapped_polyloop
),
1220 (mesh
.polygons
, "Polygons", True, blen_read_geom_array_mapped_polygon
),
1221 (mesh
.vertices
, "Vertices", True, blen_read_geom_array_mapped_vert
))
1222 for blen_data
, blen_data_type
, is_fake
, func
in tries
:
1223 bdata
= [None] * len(blen_data
) if is_fake
else blen_data
1224 if func(mesh
, bdata
, "normal",
1225 fbx_layer_data
, fbx_layer_index
, fbx_layer_mapping
, fbx_layer_ref
, 3, 3, layer_id
, xform
, True):
1226 if blen_data_type
== "Polygons":
1227 for pidx
, p
in enumerate(mesh
.polygons
):
1228 for lidx
in range(p
.loop_start
, p
.loop_start
+ p
.loop_total
):
1229 mesh
.loops
[lidx
].normal
[:] = bdata
[pidx
]
1230 elif blen_data_type
== "Vertices":
1231 # We have to copy vnors to lnors! Far from elegant, but simple.
1232 for l
in mesh
.loops
:
1233 l
.normal
[:] = bdata
[l
.vertex_index
]
1236 blen_read_geom_array_error_mapping("normal", fbx_layer_mapping
)
1237 blen_read_geom_array_error_ref("normal", fbx_layer_ref
)
1241 def blen_read_geom(fbx_tmpl
, fbx_obj
, settings
):
1242 from itertools
import chain
1245 # Vertices are in object space, but we are post-multiplying all transforms with the inverse of the
1246 # global matrix, so we need to apply the global matrix to the vertices to get the correct result.
1247 geom_mat_co
= settings
.global_matrix
if settings
.bake_space_transform
else None
1248 # We need to apply the inverse transpose of the global matrix when transforming normals.
1249 geom_mat_no
= Matrix(settings
.global_matrix_inv_transposed
) if settings
.bake_space_transform
else None
1250 if geom_mat_no
is not None:
1251 # Remove translation & scaling!
1252 geom_mat_no
.translation
= Vector()
1253 geom_mat_no
.normalize()
1255 # TODO, use 'fbx_tmpl'
1256 elem_name_utf8
= elem_name_ensure_class(fbx_obj
, b
'Geometry')
1258 fbx_verts
= elem_prop_first(elem_find_first(fbx_obj
, b
'Vertices'))
1259 fbx_polys
= elem_prop_first(elem_find_first(fbx_obj
, b
'PolygonVertexIndex'))
1260 fbx_edges
= elem_prop_first(elem_find_first(fbx_obj
, b
'Edges'))
1262 if geom_mat_co
is not None:
1263 def _vcos_transformed_gen(raw_cos
, m
=None):
1264 # Note: we could most likely get much better performances with numpy, but will leave this as TODO for now.
1265 return chain(*(m
@ Vector(v
) for v
in zip(*(iter(raw_cos
),) * 3)))
1266 fbx_verts
= array
.array(fbx_verts
.typecode
, _vcos_transformed_gen(fbx_verts
, geom_mat_co
))
1268 if fbx_verts
is None:
1270 if fbx_polys
is None:
1273 mesh
= bpy
.data
.meshes
.new(name
=elem_name_utf8
)
1274 mesh
.vertices
.add(len(fbx_verts
) // 3)
1275 mesh
.vertices
.foreach_set("co", fbx_verts
)
1278 mesh
.loops
.add(len(fbx_polys
))
1279 poly_loop_starts
= []
1280 poly_loop_totals
= []
1282 for i
, l
in enumerate(mesh
.loops
):
1283 index
= fbx_polys
[i
]
1285 poly_loop_starts
.append(poly_loop_prev
)
1286 poly_loop_totals
.append((i
- poly_loop_prev
) + 1)
1287 poly_loop_prev
= i
+ 1
1289 l
.vertex_index
= index
1291 mesh
.polygons
.add(len(poly_loop_starts
))
1292 mesh
.polygons
.foreach_set("loop_start", poly_loop_starts
)
1293 mesh
.polygons
.foreach_set("loop_total", poly_loop_totals
)
1295 blen_read_geom_layer_material(fbx_obj
, mesh
)
1296 blen_read_geom_layer_uv(fbx_obj
, mesh
)
1297 blen_read_geom_layer_color(fbx_obj
, mesh
, settings
.colors_type
)
1300 # edges in fact index the polygons (NOT the vertices)
1302 tot_edges
= len(fbx_edges
)
1303 edges_conv
= array
.array('i', [0]) * (tot_edges
* 2)
1309 e_b
= fbx_polys
[i
+ 1]
1313 # Last index of polygon, wrap back to the start.
1315 # ideally we wouldn't have to search back,
1316 # but it should only be 2-3 iterations.
1318 while j
>= 0 and fbx_polys
[j
] >= 0:
1321 e_b
= fbx_polys
[j
+ 1]
1323 edges_conv
[edge_index
] = e_a
1324 edges_conv
[edge_index
+ 1] = e_b
1327 mesh
.edges
.add(tot_edges
)
1328 mesh
.edges
.foreach_set("vertices", edges_conv
)
1330 # must be after edge, face loading.
1331 ok_smooth
= blen_read_geom_layer_smooth(fbx_obj
, mesh
)
1333 blen_read_geom_layer_edge_crease(fbx_obj
, mesh
)
1336 if settings
.use_custom_normals
:
1337 # Note: we store 'temp' normals in loops, since validate() may alter final mesh,
1338 # we can only set custom lnors *after* calling it.
1339 mesh
.create_normals_split()
1340 if geom_mat_no
is None:
1341 ok_normals
= blen_read_geom_layer_normal(fbx_obj
, mesh
)
1344 return geom_mat_no
@ Vector(v
)
1345 ok_normals
= blen_read_geom_layer_normal(fbx_obj
, mesh
, nortrans
)
1347 mesh
.validate(clean_customdata
=False) # *Very* important to not remove lnors here!
1350 clnors
= array
.array('f', [0.0] * (len(mesh
.loops
) * 3))
1351 mesh
.loops
.foreach_get("normal", clnors
)
1354 mesh
.polygons
.foreach_set("use_smooth", [True] * len(mesh
.polygons
))
1357 mesh
.normals_split_custom_set(tuple(zip(*(iter(clnors
),) * 3)))
1358 mesh
.use_auto_smooth
= True
1362 if settings
.use_custom_normals
:
1363 mesh
.free_normals_split()
1366 mesh
.polygons
.foreach_set("use_smooth", [True] * len(mesh
.polygons
))
1368 if settings
.use_custom_props
:
1369 blen_read_custom_properties(fbx_obj
, mesh
, settings
)
1374 def blen_read_shape(fbx_tmpl
, fbx_sdata
, fbx_bcdata
, meshes
, scene
):
1375 elem_name_utf8
= elem_name_ensure_class(fbx_sdata
, b
'Geometry')
1376 indices
= elem_prop_first(elem_find_first(fbx_sdata
, b
'Indexes'), default
=())
1377 dvcos
= tuple(co
for co
in zip(*[iter(elem_prop_first(elem_find_first(fbx_sdata
, b
'Vertices'), default
=()))] * 3))
1378 # We completely ignore normals here!
1379 weight
= elem_prop_first(elem_find_first(fbx_bcdata
, b
'DeformPercent'), default
=100.0) / 100.0
1380 vgweights
= tuple(vgw
/ 100.0 for vgw
in elem_prop_first(elem_find_first(fbx_bcdata
, b
'FullWeights'), default
=()))
1382 # Special case, in case all weights are the same, FullWeight can have only one element - *sigh!*
1383 nbr_indices
= len(indices
)
1384 if len(vgweights
) == 1 and nbr_indices
> 1:
1385 vgweights
= (vgweights
[0],) * nbr_indices
1387 assert(len(vgweights
) == nbr_indices
== len(dvcos
))
1388 create_vg
= bool(set(vgweights
) - {1.0})
1392 for me
, objects
in meshes
:
1393 vcos
= tuple((idx
, me
.vertices
[idx
].co
+ Vector(dvco
)) for idx
, dvco
in zip(indices
, dvcos
))
1394 objects
= list({node
.bl_obj
for node
in objects
})
1397 if me
.shape_keys
is None:
1398 objects
[0].shape_key_add(name
="Basis", from_mix
=False)
1399 kb
= objects
[0].shape_key_add(name
=elem_name_utf8
, from_mix
=False)
1400 me
.shape_keys
.use_relative
= True # Should already be set as such.
1402 for idx
, co
in vcos
:
1403 kb
.data
[idx
].co
[:] = co
1406 # Add vgroup if necessary.
1408 vgoups
= add_vgroup_to_objects(indices
, vgweights
, kb
.name
, objects
)
1409 kb
.vertex_group
= kb
.name
1411 keyblocks
.append(kb
)
1419 def blen_read_material(fbx_tmpl
, fbx_obj
, settings
):
1420 from bpy_extras
import node_shader_utils
1421 from math
import sqrt
1423 elem_name_utf8
= elem_name_ensure_class(fbx_obj
, b
'Material')
1425 nodal_material_wrap_map
= settings
.nodal_material_wrap_map
1426 ma
= bpy
.data
.materials
.new(name
=elem_name_utf8
)
1428 const_color_white
= 1.0, 1.0, 1.0
1429 const_color_black
= 0.0, 0.0, 0.0
1431 fbx_props
= (elem_find_first(fbx_obj
, b
'Properties70'),
1432 elem_find_first(fbx_tmpl
, b
'Properties70', fbx_elem_nil
))
1433 fbx_props_no_template
= (fbx_props
[0], fbx_elem_nil
)
1435 ma_wrap
= node_shader_utils
.PrincipledBSDFWrapper(ma
, is_readonly
=False, use_nodes
=True)
1436 ma_wrap
.base_color
= elem_props_get_color_rgb(fbx_props
, b
'DiffuseColor', const_color_white
)
1437 # No specular color in Principled BSDF shader, assumed to be either white or take some tint from diffuse one...
1438 # TODO: add way to handle tint option (guesstimate from spec color + intensity...)?
1439 ma_wrap
.specular
= elem_props_get_number(fbx_props
, b
'SpecularFactor', 0.25) * 2.0
1440 # XXX Totally empirical conversion, trying to adapt it (and protect against invalid negative values, see T96076):
1441 # From [1.0 - 0.0] Principled BSDF range to [0.0 - 100.0] FBX shininess range)...
1442 fbx_shininess
= max(elem_props_get_number(fbx_props
, b
'Shininess', 20.0), 0.0)
1443 ma_wrap
.roughness
= 1.0 - (sqrt(fbx_shininess
) / 10.0)
1444 # Sweetness... Looks like we are not the only ones to not know exactly how FBX is supposed to work (see T59850).
1445 # According to one of its developers, Unity uses that formula to extract alpha value:
1447 # alpha = 1 - TransparencyFactor
1448 # if (alpha == 1 or alpha == 0):
1449 # alpha = 1 - TransparentColor.r
1451 # Until further info, let's assume this is correct way to do, hence the following code for TransparentColor.
1452 # However, there are some cases (from 3DSMax, see T65065), where we do have TransparencyFactor only defined
1453 # in the template to 0.0, and then materials defining TransparentColor to pure white (1.0, 1.0, 1.0),
1454 # and setting alpha value in Opacity... try to cope with that too. :((((
1455 alpha
= 1.0 - elem_props_get_number(fbx_props
, b
'TransparencyFactor', 0.0)
1456 if (alpha
== 1.0 or alpha
== 0.0):
1457 alpha
= elem_props_get_number(fbx_props_no_template
, b
'Opacity', None)
1459 alpha
= 1.0 - elem_props_get_color_rgb(fbx_props
, b
'TransparentColor', const_color_black
)[0]
1460 ma_wrap
.alpha
= alpha
1461 ma_wrap
.metallic
= elem_props_get_number(fbx_props
, b
'ReflectionFactor', 0.0)
1462 # We have no metallic (a.k.a. reflection) color...
1463 # elem_props_get_color_rgb(fbx_props, b'ReflectionColor', const_color_white)
1464 ma_wrap
.normalmap_strength
= elem_props_get_number(fbx_props
, b
'BumpFactor', 1.0)
1465 # Emission strength and color
1466 ma_wrap
.emission_strength
= elem_props_get_number(fbx_props
, b
'EmissiveFactor', 1.0)
1467 ma_wrap
.emission_color
= elem_props_get_color_rgb(fbx_props
, b
'EmissiveColor', const_color_black
)
1469 nodal_material_wrap_map
[ma
] = ma_wrap
1471 if settings
.use_custom_props
:
1472 blen_read_custom_properties(fbx_obj
, ma
, settings
)
1480 def blen_read_texture_image(fbx_tmpl
, fbx_obj
, basedir
, settings
):
1482 from bpy_extras
import image_utils
1484 def pack_data_from_content(image
, fbx_obj
):
1485 data
= elem_find_first_bytes(fbx_obj
, b
'Content')
1487 data_len
= len(data
)
1489 image
.pack(data
=data
, data_len
=data_len
)
1491 elem_name_utf8
= elem_name_ensure_classes(fbx_obj
, {b
'Texture', b
'Video'})
1493 image_cache
= settings
.image_cache
1495 # Yet another beautiful logic demonstration by Master FBX:
1496 # * RelativeFilename in both Video and Texture nodes.
1497 # * FileName in texture nodes.
1498 # * Filename in video nodes.
1499 # Aaaaaaaarrrrrrrrgggggggggggg!!!!!!!!!!!!!!
1500 filepath
= elem_find_first_string(fbx_obj
, b
'RelativeFilename')
1502 # Make sure we do handle a relative path, and not an absolute one (see D5143).
1503 filepath
= filepath
.lstrip(os
.path
.sep
).lstrip(os
.path
.altsep
)
1504 filepath
= os
.path
.join(basedir
, filepath
)
1506 filepath
= elem_find_first_string(fbx_obj
, b
'FileName')
1508 filepath
= elem_find_first_string(fbx_obj
, b
'Filename')
1510 print("Error, could not find any file path in ", fbx_obj
)
1511 print(" Falling back to: ", elem_name_utf8
)
1512 filepath
= elem_name_utf8
1514 filepath
= filepath
.replace('\\', '/') if (os
.sep
== '/') else filepath
.replace('/', '\\')
1516 image
= image_cache
.get(filepath
)
1517 if image
is not None:
1518 # Data is only embedded once, we may have already created the image but still be missing its data!
1519 if not image
.has_data
:
1520 pack_data_from_content(image
, fbx_obj
)
1523 image
= image_utils
.load_image(
1527 recursive
=settings
.use_image_search
,
1530 # Try to use embedded data, if available!
1531 pack_data_from_content(image
, fbx_obj
)
1533 image_cache
[filepath
] = image
1534 # name can be ../a/b/c
1535 image
.name
= os
.path
.basename(elem_name_utf8
)
1537 if settings
.use_custom_props
:
1538 blen_read_custom_properties(fbx_obj
, image
, settings
)
1543 def blen_read_camera(fbx_tmpl
, fbx_obj
, global_scale
):
1547 elem_name_utf8
= elem_name_ensure_class(fbx_obj
, b
'NodeAttribute')
1549 fbx_props
= (elem_find_first(fbx_obj
, b
'Properties70'),
1550 elem_find_first(fbx_tmpl
, b
'Properties70', fbx_elem_nil
))
1552 camera
= bpy
.data
.cameras
.new(name
=elem_name_utf8
)
1554 camera
.type = 'ORTHO' if elem_props_get_enum(fbx_props
, b
'CameraProjectionType', 0) == 1 else 'PERSP'
1556 camera
.dof
.focus_distance
= elem_props_get_number(fbx_props
, b
'FocusDistance', 10 * 1000) / 1000 * global_scale
1557 if (elem_props_get_bool(fbx_props
, b
'UseDepthOfField', False)):
1558 camera
.dof
.use_dof
= True
1560 camera
.lens
= elem_props_get_number(fbx_props
, b
'FocalLength', 35.0)
1561 camera
.sensor_width
= elem_props_get_number(fbx_props
, b
'FilmWidth', 32.0 * M2I
) / M2I
1562 camera
.sensor_height
= elem_props_get_number(fbx_props
, b
'FilmHeight', 32.0 * M2I
) / M2I
1564 camera
.ortho_scale
= elem_props_get_number(fbx_props
, b
'OrthoZoom', 1.0)
1566 filmaspect
= camera
.sensor_width
/ camera
.sensor_height
1568 camera
.shift_x
= elem_props_get_number(fbx_props
, b
'FilmOffsetX', 0.0) / (M2I
* camera
.sensor_width
)
1569 camera
.shift_y
= elem_props_get_number(fbx_props
, b
'FilmOffsetY', 0.0) / (M2I
* camera
.sensor_height
* filmaspect
)
1571 camera
.clip_start
= elem_props_get_number(fbx_props
, b
'NearPlane', 0.01) * global_scale
1572 camera
.clip_end
= elem_props_get_number(fbx_props
, b
'FarPlane', 100.0) * global_scale
1577 def blen_read_light(fbx_tmpl
, fbx_obj
, global_scale
):
1579 elem_name_utf8
= elem_name_ensure_class(fbx_obj
, b
'NodeAttribute')
1581 fbx_props
= (elem_find_first(fbx_obj
, b
'Properties70'),
1582 elem_find_first(fbx_tmpl
, b
'Properties70', fbx_elem_nil
))
1587 2: 'SPOT'}.get(elem_props_get_enum(fbx_props
, b
'LightType', 0), 'POINT')
1589 lamp
= bpy
.data
.lights
.new(name
=elem_name_utf8
, type=light_type
)
1591 if light_type
== 'SPOT':
1592 spot_size
= elem_props_get_number(fbx_props
, b
'OuterAngle', None)
1593 if spot_size
is None:
1595 spot_size
= elem_props_get_number(fbx_props
, b
'Cone angle', 45.0)
1596 lamp
.spot_size
= math
.radians(spot_size
)
1598 spot_blend
= elem_props_get_number(fbx_props
, b
'InnerAngle', None)
1599 if spot_blend
is None:
1601 spot_blend
= elem_props_get_number(fbx_props
, b
'HotSpot', 45.0)
1602 lamp
.spot_blend
= 1.0 - (spot_blend
/ spot_size
)
1604 # TODO, cycles nodes???
1605 lamp
.color
= elem_props_get_color_rgb(fbx_props
, b
'Color', (1.0, 1.0, 1.0))
1606 lamp
.energy
= elem_props_get_number(fbx_props
, b
'Intensity', 100.0) / 100.0
1607 lamp
.distance
= elem_props_get_number(fbx_props
, b
'DecayStart', 25.0) * global_scale
1608 lamp
.use_shadow
= elem_props_get_bool(fbx_props
, b
'CastShadow', True)
1609 if hasattr(lamp
, "cycles"):
1610 lamp
.cycles
.cast_shadow
= lamp
.use_shadow
1611 # Keeping this for now, but this is not used nor exposed anymore afaik...
1612 lamp
.shadow_color
= elem_props_get_color_rgb(fbx_props
, b
'ShadowColor', (0.0, 0.0, 0.0))
1617 # ### Import Utility class
1618 class FbxImportHelperNode
:
1620 Temporary helper node to store a hierarchy of fbxNode objects before building Objects, Armatures and Bones.
1621 It tries to keep the correction data in one place so it can be applied consistently to the imported data.
1625 '_parent', 'anim_compensation_matrix', 'is_global_animation', 'armature_setup', 'armature', 'bind_matrix',
1626 'bl_bone', 'bl_data', 'bl_obj', 'bone_child_matrix', 'children', 'clusters',
1627 'fbx_elem', 'fbx_name', 'fbx_transform_data', 'fbx_type',
1628 'is_armature', 'has_bone_children', 'is_bone', 'is_root', 'is_leaf',
1629 'matrix', 'matrix_as_parent', 'matrix_geom', 'meshes', 'post_matrix', 'pre_matrix')
1631 def __init__(self
, fbx_elem
, bl_data
, fbx_transform_data
, is_bone
):
1632 self
.fbx_name
= elem_name_ensure_class(fbx_elem
, b
'Model') if fbx_elem
else 'Unknown'
1633 self
.fbx_type
= fbx_elem
.props
[2] if fbx_elem
else None
1634 self
.fbx_elem
= fbx_elem
1636 self
.bl_data
= bl_data
1637 self
.bl_bone
= None # Name of bone if this is a bone (this may be different to fbx_name if there was a name conflict in Blender!)
1638 self
.fbx_transform_data
= fbx_transform_data
1639 self
.is_root
= False
1640 self
.is_bone
= is_bone
1641 self
.is_armature
= False
1642 self
.armature
= None # For bones only, relevant armature node.
1643 self
.has_bone_children
= False # True if the hierarchy below this node contains bones, important to support mixed hierarchies.
1644 self
.is_leaf
= False # True for leaf-bones added to the end of some bone chains to set the lengths.
1645 self
.pre_matrix
= None # correction matrix that needs to be applied before the FBX transform
1646 self
.bind_matrix
= None # for bones this is the matrix used to bind to the skin
1647 if fbx_transform_data
:
1648 self
.matrix
, self
.matrix_as_parent
, self
.matrix_geom
= blen_read_object_transform_do(fbx_transform_data
)
1650 self
.matrix
, self
.matrix_as_parent
, self
.matrix_geom
= (None, None, None)
1651 self
.post_matrix
= None # correction matrix that needs to be applied after the FBX transform
1652 self
.bone_child_matrix
= None # Objects attached to a bone end not the beginning, this matrix corrects for that
1654 # XXX Those two are to handle the fact that rigged meshes are not linked to their armature in FBX, which implies
1655 # that their animation is in global space (afaik...).
1656 # This is actually not really solvable currently, since anim_compensation_matrix is not valid if armature
1657 # itself is animated (we'd have to recompute global-to-local anim_compensation_matrix for each frame,
1658 # and for each armature action... beyond being an insane work).
1659 # Solution for now: do not read rigged meshes animations at all! sic...
1660 self
.anim_compensation_matrix
= None # a mesh moved in the hierarchy may have a different local matrix. This compensates animations for this.
1661 self
.is_global_animation
= False
1663 self
.meshes
= None # List of meshes influenced by this bone.
1664 self
.clusters
= [] # Deformer Cluster nodes
1665 self
.armature_setup
= {} # mesh and armature matrix when the mesh was bound
1675 def parent(self
, value
):
1676 if self
._parent
is not None:
1677 self
._parent
.children
.remove(self
)
1678 self
._parent
= value
1679 if self
._parent
is not None:
1680 self
._parent
.children
.append(self
)
1684 # Separating leaf status from ignore status itself.
1685 # Currently they are equivalent, but this may change in future.
1690 return self
.fbx_elem
.props
[1].decode()
1694 def print_info(self
, indent
=0):
1695 print(" " * indent
+ (self
.fbx_name
if self
.fbx_name
else "(Null)")
1696 + ("[root]" if self
.is_root
else "")
1697 + ("[leaf]" if self
.is_leaf
else "")
1698 + ("[ignore]" if self
.ignore
else "")
1699 + ("[armature]" if self
.is_armature
else "")
1700 + ("[bone]" if self
.is_bone
else "")
1701 + ("[HBC]" if self
.has_bone_children
else "")
1703 for c
in self
.children
:
1704 c
.print_info(indent
+ 1)
1706 def mark_leaf_bones(self
):
1707 if self
.is_bone
and len(self
.children
) == 1:
1708 child
= self
.children
[0]
1709 if child
.is_bone
and len(child
.children
) == 0:
1710 child
.is_leaf
= True
1711 for child
in self
.children
:
1712 child
.mark_leaf_bones()
1714 def do_bake_transform(self
, settings
):
1715 return (settings
.bake_space_transform
and self
.fbx_type
in (b
'Mesh', b
'Null') and
1716 not self
.is_armature
and not self
.is_bone
)
1718 def find_correction_matrix(self
, settings
, parent_correction_inv
=None):
1719 from bpy_extras
.io_utils
import axis_conversion
1721 if self
.parent
and (self
.parent
.is_root
or self
.parent
.do_bake_transform(settings
)):
1722 self
.pre_matrix
= settings
.global_matrix
1724 if parent_correction_inv
:
1725 self
.pre_matrix
= parent_correction_inv
@ (self
.pre_matrix
if self
.pre_matrix
else Matrix())
1727 correction_matrix
= None
1730 if settings
.automatic_bone_orientation
:
1731 # find best orientation to align bone with
1732 bone_children
= tuple(child
for child
in self
.children
if child
.is_bone
)
1733 if len(bone_children
) == 0:
1734 # no children, inherit the correction from parent (if possible)
1735 if self
.parent
and self
.parent
.is_bone
:
1736 correction_matrix
= parent_correction_inv
.inverted() if parent_correction_inv
else None
1738 # else find how best to rotate the bone to align the Y axis with the children
1739 best_axis
= (1, 0, 0)
1740 if len(bone_children
) == 1:
1741 vec
= bone_children
[0].get_bind_matrix().to_translation()
1742 best_axis
= Vector((0, 0, 1 if vec
[2] >= 0 else -1))
1743 if abs(vec
[0]) > abs(vec
[1]):
1744 if abs(vec
[0]) > abs(vec
[2]):
1745 best_axis
= Vector((1 if vec
[0] >= 0 else -1, 0, 0))
1746 elif abs(vec
[1]) > abs(vec
[2]):
1747 best_axis
= Vector((0, 1 if vec
[1] >= 0 else -1, 0))
1749 # get the child directions once because they may be checked several times
1750 child_locs
= (child
.get_bind_matrix().to_translation() for child
in bone_children
)
1751 child_locs
= tuple(loc
.normalized() for loc
in child_locs
if loc
.magnitude
> 0.0)
1753 # I'm not sure which one I like better...
1758 s
= -1 if i
% 2 == 1 else 1
1759 test_axis
= Vector((s
if a
== 0 else 0, s
if a
== 1 else 0, s
if a
== 2 else 0))
1761 # find max angle to children
1763 for loc
in child_locs
:
1764 max_angle
= min(max_angle
, test_axis
.dot(loc
))
1766 # is it better than the last one?
1767 if best_angle
< max_angle
:
1768 best_angle
= max_angle
1769 best_axis
= test_axis
1772 for vec
in child_locs
:
1773 test_axis
= Vector((0, 0, 1 if vec
[2] >= 0 else -1))
1774 if abs(vec
[0]) > abs(vec
[1]):
1775 if abs(vec
[0]) > abs(vec
[2]):
1776 test_axis
= Vector((1 if vec
[0] >= 0 else -1, 0, 0))
1777 elif abs(vec
[1]) > abs(vec
[2]):
1778 test_axis
= Vector((0, 1 if vec
[1] >= 0 else -1, 0))
1780 # find max angle to children
1782 for loc
in child_locs
:
1783 max_angle
= min(max_angle
, test_axis
.dot(loc
))
1785 # is it better than the last one?
1786 if best_angle
< max_angle
:
1787 best_angle
= max_angle
1788 best_axis
= test_axis
1790 # convert best_axis to axis string
1791 to_up
= 'Z' if best_axis
[2] >= 0 else '-Z'
1792 if abs(best_axis
[0]) > abs(best_axis
[1]):
1793 if abs(best_axis
[0]) > abs(best_axis
[2]):
1794 to_up
= 'X' if best_axis
[0] >= 0 else '-X'
1795 elif abs(best_axis
[1]) > abs(best_axis
[2]):
1796 to_up
= 'Y' if best_axis
[1] >= 0 else '-Y'
1797 to_forward
= 'X' if to_up
not in {'X', '-X'} else 'Y'
1799 # Build correction matrix
1800 if (to_up
, to_forward
) != ('Y', 'X'):
1801 correction_matrix
= axis_conversion(from_forward
='X',
1803 to_forward
=to_forward
,
1807 correction_matrix
= settings
.bone_correction_matrix
1809 # camera and light can be hard wired
1810 if self
.fbx_type
== b
'Camera':
1811 correction_matrix
= MAT_CONVERT_CAMERA
1812 elif self
.fbx_type
== b
'Light':
1813 correction_matrix
= MAT_CONVERT_LIGHT
1815 self
.post_matrix
= correction_matrix
1817 if self
.do_bake_transform(settings
):
1818 self
.post_matrix
= settings
.global_matrix_inv
@ (self
.post_matrix
if self
.post_matrix
else Matrix())
1821 correction_matrix_inv
= correction_matrix
.inverted_safe() if correction_matrix
else None
1822 for child
in self
.children
:
1823 child
.find_correction_matrix(settings
, correction_matrix_inv
)
1825 def find_armature_bones(self
, armature
):
1826 for child
in self
.children
:
1828 child
.armature
= armature
1829 child
.find_armature_bones(armature
)
1831 def find_armatures(self
):
1832 needs_armature
= False
1833 for child
in self
.children
:
1835 needs_armature
= True
1838 if self
.fbx_type
in {b
'Null', b
'Root'}:
1839 # if empty then convert into armature
1840 self
.is_armature
= True
1843 # otherwise insert a new node
1844 # XXX Maybe in case self is virtual FBX root node, we should instead add one armature per bone child?
1845 armature
= FbxImportHelperNode(None, None, None, False)
1846 armature
.fbx_name
= "Armature"
1847 armature
.is_armature
= True
1849 for child
in tuple(self
.children
):
1851 child
.parent
= armature
1853 armature
.parent
= self
1855 armature
.find_armature_bones(armature
)
1857 for child
in self
.children
:
1858 if child
.is_armature
or child
.is_bone
:
1860 child
.find_armatures()
1862 def find_bone_children(self
):
1863 has_bone_children
= False
1864 for child
in self
.children
:
1865 has_bone_children |
= child
.find_bone_children()
1866 self
.has_bone_children
= has_bone_children
1867 return self
.is_bone
or has_bone_children
1869 def find_fake_bones(self
, in_armature
=False):
1870 if in_armature
and not self
.is_bone
and self
.has_bone_children
:
1872 # if we are not a null node we need an intermediate node for the data
1873 if self
.fbx_type
not in {b
'Null', b
'Root'}:
1874 node
= FbxImportHelperNode(self
.fbx_elem
, self
.bl_data
, None, False)
1875 self
.fbx_elem
= None
1879 for child
in self
.children
:
1880 if child
.is_bone
or child
.has_bone_children
:
1887 if self
.is_armature
:
1889 for child
in self
.children
:
1890 child
.find_fake_bones(in_armature
)
1892 def get_world_matrix_as_parent(self
):
1893 matrix
= self
.parent
.get_world_matrix_as_parent() if self
.parent
else Matrix()
1894 if self
.matrix_as_parent
:
1895 matrix
= matrix
@ self
.matrix_as_parent
1898 def get_world_matrix(self
):
1899 matrix
= self
.parent
.get_world_matrix_as_parent() if self
.parent
else Matrix()
1901 matrix
= matrix
@ self
.matrix
1904 def get_matrix(self
):
1905 matrix
= self
.matrix
if self
.matrix
else Matrix()
1907 matrix
= self
.pre_matrix
@ matrix
1908 if self
.post_matrix
:
1909 matrix
= matrix
@ self
.post_matrix
1912 def get_bind_matrix(self
):
1913 matrix
= self
.bind_matrix
if self
.bind_matrix
else Matrix()
1915 matrix
= self
.pre_matrix
@ matrix
1916 if self
.post_matrix
:
1917 matrix
= matrix
@ self
.post_matrix
1920 def make_bind_pose_local(self
, parent_matrix
=None):
1921 if parent_matrix
is None:
1922 parent_matrix
= Matrix()
1924 if self
.bind_matrix
:
1925 bind_matrix
= parent_matrix
.inverted_safe() @ self
.bind_matrix
1927 bind_matrix
= self
.matrix
.copy() if self
.matrix
else None
1929 self
.bind_matrix
= bind_matrix
1931 parent_matrix
= parent_matrix
@ bind_matrix
1933 for child
in self
.children
:
1934 child
.make_bind_pose_local(parent_matrix
)
1936 def collect_skeleton_meshes(self
, meshes
):
1937 for _
, m
in self
.clusters
:
1939 for child
in self
.children
:
1940 if not child
.meshes
:
1941 child
.collect_skeleton_meshes(meshes
)
1943 def collect_armature_meshes(self
):
1944 if self
.is_armature
:
1945 armature_matrix_inv
= self
.get_world_matrix().inverted_safe()
1948 for child
in self
.children
:
1949 # Children meshes may be linked to children armatures, in which case we do not want to link them
1950 # to a parent one. See T70244.
1951 child
.collect_armature_meshes()
1952 if not child
.meshes
:
1953 child
.collect_skeleton_meshes(meshes
)
1955 old_matrix
= m
.matrix
1956 m
.matrix
= armature_matrix_inv
@ m
.get_world_matrix()
1957 m
.anim_compensation_matrix
= old_matrix
.inverted_safe() @ m
.matrix
1958 m
.is_global_animation
= True
1960 self
.meshes
= meshes
1962 for child
in self
.children
:
1963 child
.collect_armature_meshes()
1965 def build_skeleton(self
, arm
, parent_matrix
, parent_bone_size
=1, force_connect_children
=False):
1966 def child_connect(par_bone
, child_bone
, child_head
, connect_ctx
):
1967 # child_bone or child_head may be None.
1968 force_connect_children
, connected
= connect_ctx
1969 if child_bone
is not None:
1970 child_bone
.parent
= par_bone
1971 child_head
= child_bone
.head
1973 if similar_values_iter(par_bone
.tail
, child_head
):
1974 if child_bone
is not None:
1975 child_bone
.use_connect
= True
1976 # Disallow any force-connection at this level from now on, since that child was 'really'
1977 # connected, we do not want to move current bone's tail anymore!
1979 elif force_connect_children
and connected
is not None:
1980 # We only store position where tail of par_bone should be in the end.
1981 # Actual tail moving and force connection of compatible child bones will happen
1982 # once all have been checked.
1983 if connected
is ...:
1984 connected
= ([child_head
.copy(), 1], [child_bone
] if child_bone
is not None else [])
1986 connected
[0][0] += child_head
1987 connected
[0][1] += 1
1988 if child_bone
is not None:
1989 connected
[1].append(child_bone
)
1990 connect_ctx
[1] = connected
1992 def child_connect_finalize(par_bone
, connect_ctx
):
1993 force_connect_children
, connected
= connect_ctx
1994 # Do nothing if force connection is not enabled!
1995 if force_connect_children
and connected
is not None and connected
is not ...:
1996 # Here again we have to be wary about zero-length bones!!!
1997 par_tail
= connected
[0][0] / connected
[0][1]
1998 if (par_tail
- par_bone
.head
).magnitude
< 1e-2:
1999 par_bone_vec
= (par_bone
.tail
- par_bone
.head
).normalized()
2000 par_tail
= par_bone
.head
+ par_bone_vec
* 0.01
2001 par_bone
.tail
= par_tail
2002 for child_bone
in connected
[1]:
2003 if similar_values_iter(par_tail
, child_bone
.head
):
2004 child_bone
.use_connect
= True
2006 # Create the (edit)bone.
2007 bone
= arm
.bl_data
.edit_bones
.new(name
=self
.fbx_name
)
2009 self
.bl_obj
= arm
.bl_obj
2010 self
.bl_data
= arm
.bl_data
2011 self
.bl_bone
= bone
.name
# Could be different from the FBX name!
2013 # get average distance to children
2016 for child
in self
.children
:
2018 bone_size
+= child
.get_bind_matrix().to_translation().magnitude
2021 bone_size
/= bone_count
2023 bone_size
= parent_bone_size
2025 # So that our bone gets its final length, but still Y-aligned in armature space.
2026 # 0-length bones are automatically collapsed into their parent when you leave edit mode,
2027 # so this enforces a minimum length.
2028 bone_tail
= Vector((0.0, 1.0, 0.0)) * max(0.01, bone_size
)
2029 bone
.tail
= bone_tail
2031 # And rotate/move it to its final "rest pose".
2032 bone_matrix
= parent_matrix
@ self
.get_bind_matrix().normalized()
2034 bone
.matrix
= bone_matrix
2036 # Correction for children attached to a bone. FBX expects to attach to the head of a bone,
2037 # while Blender attaches to the tail.
2038 self
.bone_child_matrix
= Matrix
.Translation(-bone_tail
)
2040 connect_ctx
= [force_connect_children
, ...]
2041 for child
in self
.children
:
2042 if child
.is_leaf
and force_connect_children
:
2043 # Arggggggggggggggggg! We do not want to create this bone, but we need its 'virtual head' location
2044 # to orient current one!!!
2045 child_head
= (bone_matrix
@ child
.get_bind_matrix().normalized()).translation
2046 child_connect(bone
, None, child_head
, connect_ctx
)
2047 elif child
.is_bone
and not child
.ignore
:
2048 child_bone
= child
.build_skeleton(arm
, bone_matrix
, bone_size
,
2049 force_connect_children
=force_connect_children
)
2050 # Connection to parent.
2051 child_connect(bone
, child_bone
, None, connect_ctx
)
2053 child_connect_finalize(bone
, connect_ctx
)
2056 def build_node_obj(self
, fbx_tmpl
, settings
):
2060 if self
.is_bone
or not self
.fbx_elem
:
2063 # create when linking since we need object data
2064 elem_name_utf8
= self
.fbx_name
2066 # Object data must be created already
2067 self
.bl_obj
= obj
= bpy
.data
.objects
.new(name
=elem_name_utf8
, object_data
=self
.bl_data
)
2069 fbx_props
= (elem_find_first(self
.fbx_elem
, b
'Properties70'),
2070 elem_find_first(fbx_tmpl
, b
'Properties70', fbx_elem_nil
))
2075 obj
.color
[0:3] = elem_props_get_color_rgb(fbx_props
, b
'Color', (0.8, 0.8, 0.8))
2076 obj
.hide_viewport
= not bool(elem_props_get_visibility(fbx_props
, b
'Visibility', 1.0))
2078 obj
.matrix_basis
= self
.get_matrix()
2080 if settings
.use_custom_props
:
2081 blen_read_custom_properties(self
.fbx_elem
, obj
, settings
)
2085 def build_skeleton_children(self
, fbx_tmpl
, settings
, scene
, view_layer
):
2087 for child
in self
.children
:
2090 child
.build_skeleton_children(fbx_tmpl
, settings
, scene
, view_layer
)
2093 # child is not a bone
2094 obj
= self
.build_node_obj(fbx_tmpl
, settings
)
2099 for child
in self
.children
:
2102 child
.build_skeleton_children(fbx_tmpl
, settings
, scene
, view_layer
)
2105 view_layer
.active_layer_collection
.collection
.objects
.link(obj
)
2106 obj
.select_set(True)
2110 def link_skeleton_children(self
, fbx_tmpl
, settings
, scene
):
2112 for child
in self
.children
:
2115 child_obj
= child
.bl_obj
2116 if child_obj
and child_obj
!= self
.bl_obj
:
2117 child_obj
.parent
= self
.bl_obj
# get the armature the bone belongs to
2118 child_obj
.parent_bone
= self
.bl_bone
2119 child_obj
.parent_type
= 'BONE'
2120 child_obj
.matrix_parent_inverse
= Matrix()
2122 # Blender attaches to the end of a bone, while FBX attaches to the start.
2123 # bone_child_matrix corrects for that.
2124 if child
.pre_matrix
:
2125 child
.pre_matrix
= self
.bone_child_matrix
@ child
.pre_matrix
2127 child
.pre_matrix
= self
.bone_child_matrix
2129 child_obj
.matrix_basis
= child
.get_matrix()
2130 child
.link_skeleton_children(fbx_tmpl
, settings
, scene
)
2135 for child
in self
.children
:
2138 child_obj
= child
.link_skeleton_children(fbx_tmpl
, settings
, scene
)
2140 child_obj
.parent
= obj
2144 def set_pose_matrix(self
, arm
):
2145 pose_bone
= arm
.bl_obj
.pose
.bones
[self
.bl_bone
]
2146 pose_bone
.matrix_basis
= self
.get_bind_matrix().inverted_safe() @ self
.get_matrix()
2148 for child
in self
.children
:
2152 child
.set_pose_matrix(arm
)
2154 def merge_weights(self
, combined_weights
, fbx_cluster
):
2155 indices
= elem_prop_first(elem_find_first(fbx_cluster
, b
'Indexes', default
=None), default
=())
2156 weights
= elem_prop_first(elem_find_first(fbx_cluster
, b
'Weights', default
=None), default
=())
2158 for index
, weight
in zip(indices
, weights
):
2159 w
= combined_weights
.get(index
)
2161 combined_weights
[index
] = [weight
]
2165 def set_bone_weights(self
):
2166 ignored_children
= tuple(child
for child
in self
.children
2167 if child
.is_bone
and child
.ignore
and len(child
.clusters
) > 0)
2169 if len(ignored_children
) > 0:
2170 # If we have an ignored child bone we need to merge their weights into the current bone weights.
2171 # This can happen both intentionally and accidentally when skinning a model. Either way, they
2172 # need to be moved into a parent bone or they cause animation glitches.
2173 for fbx_cluster
, meshes
in self
.clusters
:
2174 combined_weights
= {}
2175 self
.merge_weights(combined_weights
, fbx_cluster
)
2177 for child
in ignored_children
:
2178 for child_cluster
, child_meshes
in child
.clusters
:
2179 if not meshes
.isdisjoint(child_meshes
):
2180 self
.merge_weights(combined_weights
, child_cluster
)
2182 # combine child weights
2185 for i
, w
in combined_weights
.items():
2188 weights
.append(sum(w
) / len(w
))
2190 weights
.append(w
[0])
2192 add_vgroup_to_objects(indices
, weights
, self
.bl_bone
, [node
.bl_obj
for node
in meshes
])
2194 # clusters that drive meshes not included in a parent don't need to be merged
2195 all_meshes
= set().union(*[meshes
for _
, meshes
in self
.clusters
])
2196 for child
in ignored_children
:
2197 for child_cluster
, child_meshes
in child
.clusters
:
2198 if all_meshes
.isdisjoint(child_meshes
):
2199 indices
= elem_prop_first(elem_find_first(child_cluster
, b
'Indexes', default
=None), default
=())
2200 weights
= elem_prop_first(elem_find_first(child_cluster
, b
'Weights', default
=None), default
=())
2201 add_vgroup_to_objects(indices
, weights
, self
.bl_bone
, [node
.bl_obj
for node
in child_meshes
])
2203 # set the vertex weights on meshes
2204 for fbx_cluster
, meshes
in self
.clusters
:
2205 indices
= elem_prop_first(elem_find_first(fbx_cluster
, b
'Indexes', default
=None), default
=())
2206 weights
= elem_prop_first(elem_find_first(fbx_cluster
, b
'Weights', default
=None), default
=())
2207 add_vgroup_to_objects(indices
, weights
, self
.bl_bone
, [node
.bl_obj
for node
in meshes
])
2209 for child
in self
.children
:
2210 if child
.is_bone
and not child
.ignore
:
2211 child
.set_bone_weights()
2213 def build_hierarchy(self
, fbx_tmpl
, settings
, scene
, view_layer
):
2214 if self
.is_armature
:
2215 # create when linking since we need object data
2216 elem_name_utf8
= self
.fbx_name
2218 self
.bl_data
= arm_data
= bpy
.data
.armatures
.new(name
=elem_name_utf8
)
2220 # Object data must be created already
2221 self
.bl_obj
= arm
= bpy
.data
.objects
.new(name
=elem_name_utf8
, object_data
=arm_data
)
2223 arm
.matrix_basis
= self
.get_matrix()
2226 fbx_props
= (elem_find_first(self
.fbx_elem
, b
'Properties70'),
2227 elem_find_first(fbx_tmpl
, b
'Properties70', fbx_elem_nil
))
2229 if settings
.use_custom_props
:
2230 blen_read_custom_properties(self
.fbx_elem
, arm
, settings
)
2233 view_layer
.active_layer_collection
.collection
.objects
.link(arm
)
2234 arm
.select_set(True)
2238 # Switch to Edit mode.
2239 view_layer
.objects
.active
= arm
2240 is_hidden
= arm
.hide_viewport
2241 arm
.hide_viewport
= False # Can't switch to Edit mode hidden objects...
2242 bpy
.ops
.object.mode_set(mode
='EDIT')
2244 for child
in self
.children
:
2248 child
.build_skeleton(self
, Matrix(), force_connect_children
=settings
.force_connect_children
)
2250 bpy
.ops
.object.mode_set(mode
='OBJECT')
2252 arm
.hide_viewport
= is_hidden
2255 for child
in self
.children
:
2259 child
.set_pose_matrix(self
)
2261 # Add bone children:
2262 for child
in self
.children
:
2265 child_obj
= child
.build_skeleton_children(fbx_tmpl
, settings
, scene
, view_layer
)
2268 elif self
.fbx_elem
and not self
.is_bone
:
2269 obj
= self
.build_node_obj(fbx_tmpl
, settings
)
2271 # walk through children
2272 for child
in self
.children
:
2273 child
.build_hierarchy(fbx_tmpl
, settings
, scene
, view_layer
)
2276 view_layer
.active_layer_collection
.collection
.objects
.link(obj
)
2277 obj
.select_set(True)
2281 for child
in self
.children
:
2282 child
.build_hierarchy(fbx_tmpl
, settings
, scene
, view_layer
)
2286 def link_hierarchy(self
, fbx_tmpl
, settings
, scene
):
2287 if self
.is_armature
:
2290 # Link bone children:
2291 for child
in self
.children
:
2294 child_obj
= child
.link_skeleton_children(fbx_tmpl
, settings
, scene
)
2296 child_obj
.parent
= arm
2298 # Add armature modifiers to the meshes
2300 for mesh
in self
.meshes
:
2301 (mmat
, amat
) = mesh
.armature_setup
[self
]
2302 me_obj
= mesh
.bl_obj
2304 # bring global armature & mesh matrices into *Blender* global space.
2305 # Note: Usage of matrix_geom (local 'diff' transform) here is quite brittle.
2306 # Among other things, why in hell isn't it taken into account by bindpose & co???
2307 # Probably because org app (max) handles it completely aside from any parenting stuff,
2308 # which we obviously cannot do in Blender. :/
2310 amat
= self
.bind_matrix
2311 amat
= settings
.global_matrix
@ (Matrix() if amat
is None else amat
)
2312 if self
.matrix_geom
:
2313 amat
= amat
@ self
.matrix_geom
2314 mmat
= settings
.global_matrix
@ mmat
2315 if mesh
.matrix_geom
:
2316 mmat
= mmat
@ mesh
.matrix_geom
2318 # Now that we have armature and mesh in there (global) bind 'state' (matrix),
2319 # we can compute inverse parenting matrix of the mesh.
2320 me_obj
.matrix_parent_inverse
= amat
.inverted_safe() @ mmat
@ me_obj
.matrix_basis
.inverted_safe()
2322 mod
= mesh
.bl_obj
.modifiers
.new(arm
.name
, 'ARMATURE')
2325 # Add bone weights to the deformers
2326 for child
in self
.children
:
2330 child
.set_bone_weights()
2336 # walk through children
2337 for child
in self
.children
:
2338 child_obj
= child
.link_hierarchy(fbx_tmpl
, settings
, scene
)
2340 child_obj
.parent
= obj
2344 for child
in self
.children
:
2345 child
.link_hierarchy(fbx_tmpl
, settings
, scene
)
2350 def load(operator
, context
, filepath
="",
2351 use_manual_orientation
=False,
2355 bake_space_transform
=False,
2356 use_custom_normals
=True,
2357 use_image_search
=False,
2358 use_alpha_decals
=False,
2363 use_custom_props
=True,
2364 use_custom_props_enum_as_string
=True,
2365 ignore_leaf_bones
=False,
2366 force_connect_children
=False,
2367 automatic_bone_orientation
=False,
2368 primary_bone_axis
='Y',
2369 secondary_bone_axis
='X',
2370 use_prepost_rot
=True,
2371 colors_type
='SRGB'):
2374 fbx_elem_nil
= FBXElem('', (), (), ())
2378 from bpy_extras
.io_utils
import axis_conversion
2380 from . import parse_fbx
2381 from .fbx_utils
import RIGHT_HAND_AXES
, FBX_FRAMERATES
2383 start_time_proc
= time
.process_time()
2384 start_time_sys
= time
.time()
2388 perfmon
.step("FBX Import: start importing %s" % filepath
)
2391 # Detect ASCII files.
2393 # Typically it's bad practice to fail silently on any error,
2394 # however the file may fail to read for many reasons,
2395 # and this situation is handled later in the code,
2396 # right now we only want to know if the file successfully reads as ascii.
2398 with
open(filepath
, 'r', encoding
="utf-8") as fh
:
2405 operator
.report({'ERROR'}, tip_("ASCII FBX files are not supported %r") % filepath
)
2406 return {'CANCELLED'}
2408 # End ascii detection.
2411 elem_root
, version
= parse_fbx
.parse(filepath
)
2412 except Exception as e
:
2414 traceback
.print_exc()
2416 operator
.report({'ERROR'}, tip_("Couldn't open file %r (%s)") % (filepath
, e
))
2417 return {'CANCELLED'}
2420 operator
.report({'ERROR'}, tip_("Version %r unsupported, must be %r or later") % (version
, 7100))
2421 return {'CANCELLED'}
2423 print("FBX version: %r" % version
)
2425 if bpy
.ops
.object.mode_set
.poll():
2426 bpy
.ops
.object.mode_set(mode
='OBJECT', toggle
=False)
2429 if bpy
.ops
.object.select_all
.poll():
2430 bpy
.ops
.object.select_all(action
='DESELECT')
2432 basedir
= os
.path
.dirname(filepath
)
2434 nodal_material_wrap_map
= {}
2437 # Tables: (FBX_byte_id -> [FBX_data, None or Blender_datablock])
2438 fbx_table_nodes
= {}
2440 if use_alpha_decals
:
2441 material_decals
= set()
2443 material_decals
= None
2445 scene
= context
.scene
2446 view_layer
= context
.view_layer
2448 # #### Get some info from GlobalSettings.
2450 perfmon
.step("FBX import: Prepare...")
2452 fbx_settings
= elem_find_first(elem_root
, b
'GlobalSettings')
2453 fbx_settings_props
= elem_find_first(fbx_settings
, b
'Properties70')
2454 if fbx_settings
is None or fbx_settings_props
is None:
2455 operator
.report({'ERROR'}, tip_("No 'GlobalSettings' found in file %r") % filepath
)
2456 return {'CANCELLED'}
2458 # FBX default base unit seems to be the centimeter, while raw Blender Unit is equivalent to the meter...
2459 unit_scale
= elem_props_get_number(fbx_settings_props
, b
'UnitScaleFactor', 1.0)
2460 unit_scale_org
= elem_props_get_number(fbx_settings_props
, b
'OriginalUnitScaleFactor', 1.0)
2461 global_scale
*= (unit_scale
/ units_blender_to_fbx_factor(context
.scene
))
2462 # Compute global matrix and scale.
2463 if not use_manual_orientation
:
2464 axis_forward
= (elem_props_get_integer(fbx_settings_props
, b
'FrontAxis', 1),
2465 elem_props_get_integer(fbx_settings_props
, b
'FrontAxisSign', 1))
2466 axis_up
= (elem_props_get_integer(fbx_settings_props
, b
'UpAxis', 2),
2467 elem_props_get_integer(fbx_settings_props
, b
'UpAxisSign', 1))
2468 axis_coord
= (elem_props_get_integer(fbx_settings_props
, b
'CoordAxis', 0),
2469 elem_props_get_integer(fbx_settings_props
, b
'CoordAxisSign', 1))
2470 axis_key
= (axis_up
, axis_forward
, axis_coord
)
2471 axis_up
, axis_forward
= {v
: k
for k
, v
in RIGHT_HAND_AXES
.items()}.get(axis_key
, ('Z', 'Y'))
2472 global_matrix
= (Matrix
.Scale(global_scale
, 4) @
2473 axis_conversion(from_forward
=axis_forward
, from_up
=axis_up
).to_4x4())
2475 # To cancel out unwanted rotation/scale on nodes.
2476 global_matrix_inv
= global_matrix
.inverted()
2477 # For transforming mesh normals.
2478 global_matrix_inv_transposed
= global_matrix_inv
.transposed()
2480 # Compute bone correction matrix
2481 bone_correction_matrix
= None # None means no correction/identity
2482 if not automatic_bone_orientation
:
2483 if (primary_bone_axis
, secondary_bone_axis
) != ('Y', 'X'):
2484 bone_correction_matrix
= axis_conversion(from_forward
='X',
2486 to_forward
=secondary_bone_axis
,
2487 to_up
=primary_bone_axis
,
2490 # Compute framerate settings.
2491 custom_fps
= elem_props_get_number(fbx_settings_props
, b
'CustomFrameRate', 25.0)
2492 time_mode
= elem_props_get_enum(fbx_settings_props
, b
'TimeMode')
2493 real_fps
= {eid
: val
for val
, eid
in FBX_FRAMERATES
[1:]}.get(time_mode
, custom_fps
)
2496 scene
.render
.fps
= round(real_fps
)
2497 scene
.render
.fps_base
= scene
.render
.fps
/ real_fps
2499 # store global settings that need to be accessed during conversion
2500 settings
= FBXImportSettings(
2501 operator
.report
, (axis_up
, axis_forward
), global_matrix
, global_scale
,
2502 bake_space_transform
, global_matrix_inv
, global_matrix_inv_transposed
,
2503 use_custom_normals
, use_image_search
,
2504 use_alpha_decals
, decal_offset
,
2505 use_anim
, anim_offset
,
2507 use_custom_props
, use_custom_props_enum_as_string
,
2508 nodal_material_wrap_map
, image_cache
,
2509 ignore_leaf_bones
, force_connect_children
, automatic_bone_orientation
, bone_correction_matrix
,
2510 use_prepost_rot
, colors_type
,
2513 # #### And now, the "real" data.
2515 perfmon
.step("FBX import: Templates...")
2517 fbx_defs
= elem_find_first(elem_root
, b
'Definitions') # can be None
2518 fbx_nodes
= elem_find_first(elem_root
, b
'Objects')
2519 fbx_connections
= elem_find_first(elem_root
, b
'Connections')
2521 if fbx_nodes
is None:
2522 operator
.report({'ERROR'}, tip_("No 'Objects' found in file %r") % filepath
)
2523 return {'CANCELLED'}
2524 if fbx_connections
is None:
2525 operator
.report({'ERROR'}, tip_("No 'Connections' found in file %r") % filepath
)
2526 return {'CANCELLED'}
2529 # First load property templates
2530 # Load 'PropertyTemplate' values.
2531 # Key is a tuple, (ObjectType, FBXNodeType)
2532 # eg, (b'Texture', b'KFbxFileTexture')
2533 # (b'Geometry', b'KFbxMesh')
2537 if fbx_defs
is not None:
2538 for fbx_def
in fbx_defs
.elems
:
2539 if fbx_def
.id == b
'ObjectType':
2540 for fbx_subdef
in fbx_def
.elems
:
2541 if fbx_subdef
.id == b
'PropertyTemplate':
2542 assert(fbx_def
.props_type
== b
'S')
2543 assert(fbx_subdef
.props_type
== b
'S')
2544 # (b'Texture', b'KFbxFileTexture') - eg.
2545 key
= fbx_def
.props
[0], fbx_subdef
.props
[0]
2546 fbx_templates
[key
] = fbx_subdef
2549 def fbx_template_get(key
):
2550 ret
= fbx_templates
.get(key
, fbx_elem_nil
)
2551 if ret
is fbx_elem_nil
:
2552 # Newest FBX (7.4 and above) use no more 'K' in their type names...
2553 key
= (key
[0], key
[1][1:])
2554 return fbx_templates
.get(key
, fbx_elem_nil
)
2557 perfmon
.step("FBX import: Nodes...")
2560 # Build FBX node-table
2562 for fbx_obj
in fbx_nodes
.elems
:
2563 # TODO, investigate what other items after first 3 may be
2564 assert(fbx_obj
.props_type
[:3] == b
'LSS')
2565 fbx_uuid
= elem_uuid(fbx_obj
)
2566 fbx_table_nodes
[fbx_uuid
] = [fbx_obj
, None]
2571 # http://download.autodesk.com/us/fbx/20112/FBX_SDK_HELP/index.html?url=
2572 # WS73099cc142f487551fea285e1221e4f9ff8-7fda.htm,topicNumber=d0e6388
2574 perfmon
.step("FBX import: Connections...")
2576 fbx_connection_map
= {}
2577 fbx_connection_map_reverse
= {}
2580 for fbx_link
in fbx_connections
.elems
:
2581 c_type
= fbx_link
.props
[0]
2582 if fbx_link
.props_type
[1:3] == b
'LL':
2583 c_src
, c_dst
= fbx_link
.props
[1:3]
2584 fbx_connection_map
.setdefault(c_src
, []).append((c_dst
, fbx_link
))
2585 fbx_connection_map_reverse
.setdefault(c_dst
, []).append((c_src
, fbx_link
))
2588 perfmon
.step("FBX import: Meshes...")
2593 fbx_tmpl
= fbx_template_get((b
'Geometry', b
'KFbxMesh'))
2595 for fbx_uuid
, fbx_item
in fbx_table_nodes
.items():
2596 fbx_obj
, blen_data
= fbx_item
2597 if fbx_obj
.id != b
'Geometry':
2599 if fbx_obj
.props
[-1] == b
'Mesh':
2600 assert(blen_data
is None)
2601 fbx_item
[1] = blen_read_geom(fbx_tmpl
, fbx_obj
, settings
)
2604 perfmon
.step("FBX import: Materials & Textures...")
2607 # Load material data
2609 fbx_tmpl
= fbx_template_get((b
'Material', b
'KFbxSurfacePhong'))
2610 # b'KFbxSurfaceLambert'
2612 for fbx_uuid
, fbx_item
in fbx_table_nodes
.items():
2613 fbx_obj
, blen_data
= fbx_item
2614 if fbx_obj
.id != b
'Material':
2616 assert(blen_data
is None)
2617 fbx_item
[1] = blen_read_material(fbx_tmpl
, fbx_obj
, settings
)
2621 # Load image & textures data
2623 fbx_tmpl_tex
= fbx_template_get((b
'Texture', b
'KFbxFileTexture'))
2624 fbx_tmpl_img
= fbx_template_get((b
'Video', b
'KFbxVideo'))
2626 # Important to run all 'Video' ones first, embedded images are stored in those nodes.
2627 # XXX Note we simplify things here, assuming both matching Video and Texture will use same file path,
2628 # this may be a bit weak, if issue arise we'll fallback to plain connection stuff...
2629 for fbx_uuid
, fbx_item
in fbx_table_nodes
.items():
2630 fbx_obj
, blen_data
= fbx_item
2631 if fbx_obj
.id != b
'Video':
2633 fbx_item
[1] = blen_read_texture_image(fbx_tmpl_img
, fbx_obj
, basedir
, settings
)
2634 for fbx_uuid
, fbx_item
in fbx_table_nodes
.items():
2635 fbx_obj
, blen_data
= fbx_item
2636 if fbx_obj
.id != b
'Texture':
2638 fbx_item
[1] = blen_read_texture_image(fbx_tmpl_tex
, fbx_obj
, basedir
, settings
)
2641 perfmon
.step("FBX import: Cameras & Lamps...")
2646 fbx_tmpl
= fbx_template_get((b
'NodeAttribute', b
'KFbxCamera'))
2648 for fbx_uuid
, fbx_item
in fbx_table_nodes
.items():
2649 fbx_obj
, blen_data
= fbx_item
2650 if fbx_obj
.id != b
'NodeAttribute':
2652 if fbx_obj
.props
[-1] == b
'Camera':
2653 assert(blen_data
is None)
2654 fbx_item
[1] = blen_read_camera(fbx_tmpl
, fbx_obj
, global_scale
)
2660 fbx_tmpl
= fbx_template_get((b
'NodeAttribute', b
'KFbxLight'))
2662 for fbx_uuid
, fbx_item
in fbx_table_nodes
.items():
2663 fbx_obj
, blen_data
= fbx_item
2664 if fbx_obj
.id != b
'NodeAttribute':
2666 if fbx_obj
.props
[-1] == b
'Light':
2667 assert(blen_data
is None)
2668 fbx_item
[1] = blen_read_light(fbx_tmpl
, fbx_obj
, global_scale
)
2673 def connection_filter_ex(fbx_uuid
, fbx_id
, dct
):
2674 return [(c_found
[0], c_found
[1], c_type
)
2675 for (c_uuid
, c_type
) in dct
.get(fbx_uuid
, ())
2676 # 0 is used for the root node, which isn't in fbx_table_nodes
2677 for c_found
in (() if c_uuid
== 0 else (fbx_table_nodes
.get(c_uuid
, (None, None)),))
2678 if (fbx_id
is None) or (c_found
[0] and c_found
[0].id == fbx_id
)]
2680 def connection_filter_forward(fbx_uuid
, fbx_id
):
2681 return connection_filter_ex(fbx_uuid
, fbx_id
, fbx_connection_map
)
2683 def connection_filter_reverse(fbx_uuid
, fbx_id
):
2684 return connection_filter_ex(fbx_uuid
, fbx_id
, fbx_connection_map_reverse
)
2686 perfmon
.step("FBX import: Objects & Armatures...")
2688 # -- temporary helper hierarchy to build armatures and objects from
2689 # lookup from uuid to helper node. Used to build parent-child relations and later to look up animated nodes.
2690 fbx_helper_nodes
= {}
2693 # We build an intermediate hierarchy used to:
2694 # - Calculate and store bone orientation correction matrices. The same matrices will be reused for animation.
2695 # - Find/insert armature nodes.
2696 # - Filter leaf bones.
2699 fbx_helper_nodes
[0] = root_helper
= FbxImportHelperNode(None, None, None, False)
2700 root_helper
.is_root
= True
2703 fbx_tmpl
= fbx_template_get((b
'Model', b
'KFbxNode'))
2704 for a_uuid
, a_item
in fbx_table_nodes
.items():
2705 fbx_obj
, bl_data
= a_item
2706 if fbx_obj
is None or fbx_obj
.id != b
'Model':
2709 fbx_props
= (elem_find_first(fbx_obj
, b
'Properties70'),
2710 elem_find_first(fbx_tmpl
, b
'Properties70', fbx_elem_nil
))
2712 transform_data
= blen_read_object_transform_preprocess(fbx_props
, fbx_obj
, Matrix(), use_prepost_rot
)
2713 # Note: 'Root' "bones" are handled as (armature) objects.
2714 # Note: See T46912 for first FBX file I ever saw with 'Limb' bones - thought those were totally deprecated.
2715 is_bone
= fbx_obj
.props
[2] in {b
'LimbNode', b
'Limb'}
2716 fbx_helper_nodes
[a_uuid
] = FbxImportHelperNode(fbx_obj
, bl_data
, transform_data
, is_bone
)
2718 # add parent-child relations and add blender data to the node
2719 for fbx_link
in fbx_connections
.elems
:
2720 if fbx_link
.props
[0] != b
'OO':
2722 if fbx_link
.props_type
[1:3] == b
'LL':
2723 c_src
, c_dst
= fbx_link
.props
[1:3]
2724 parent
= fbx_helper_nodes
.get(c_dst
)
2728 child
= fbx_helper_nodes
.get(c_src
)
2730 # add blender data (meshes, lights, cameras, etc.) to a helper node
2731 fbx_sdata
, bl_data
= p_item
= fbx_table_nodes
.get(c_src
, (None, None))
2732 if fbx_sdata
is None:
2734 if fbx_sdata
.id not in {b
'Geometry', b
'NodeAttribute'}:
2736 parent
.bl_data
= bl_data
2739 child
.parent
= parent
2741 # find armatures (either an empty below a bone or a new node inserted at the bone
2742 root_helper
.find_armatures()
2744 # mark nodes that have bone children
2745 root_helper
.find_bone_children()
2747 # mark nodes that need a bone to attach child-bones to
2748 root_helper
.find_fake_bones()
2750 # mark leaf nodes that are only required to mark the end of their parent bone
2751 if settings
.ignore_leaf_bones
:
2752 root_helper
.mark_leaf_bones()
2754 # What a mess! Some bones have several BindPoses, some have none, clusters contain a bind pose as well,
2755 # and you can have several clusters per bone!
2756 # Maybe some conversion can be applied to put them all into the same frame of reference?
2758 # get the bind pose from pose elements
2759 for a_uuid
, a_item
in fbx_table_nodes
.items():
2760 fbx_obj
, bl_data
= a_item
2763 if fbx_obj
.id != b
'Pose':
2765 if fbx_obj
.props
[2] != b
'BindPose':
2767 for fbx_pose_node
in fbx_obj
.elems
:
2768 if fbx_pose_node
.id != b
'PoseNode':
2770 node_elem
= elem_find_first(fbx_pose_node
, b
'Node')
2771 node
= elem_uuid(node_elem
)
2772 matrix_elem
= elem_find_first(fbx_pose_node
, b
'Matrix')
2773 matrix
= array_to_matrix4(matrix_elem
.props
[0]) if matrix_elem
else None
2774 bone
= fbx_helper_nodes
.get(node
)
2776 # Store the matrix in the helper node.
2777 # There may be several bind pose matrices for the same node, but in tests they seem to be identical.
2778 bone
.bind_matrix
= matrix
# global space
2780 # get clusters and bind pose
2781 for helper_uuid
, helper_node
in fbx_helper_nodes
.items():
2782 if not helper_node
.is_bone
:
2784 for cluster_uuid
, cluster_link
in fbx_connection_map
.get(helper_uuid
, ()):
2785 if cluster_link
.props
[0] != b
'OO':
2787 fbx_cluster
, _
= fbx_table_nodes
.get(cluster_uuid
, (None, None))
2788 if fbx_cluster
is None or fbx_cluster
.id != b
'Deformer' or fbx_cluster
.props
[2] != b
'Cluster':
2791 # Get the bind pose from the cluster:
2792 tx_mesh_elem
= elem_find_first(fbx_cluster
, b
'Transform', default
=None)
2793 tx_mesh
= array_to_matrix4(tx_mesh_elem
.props
[0]) if tx_mesh_elem
else Matrix()
2795 tx_bone_elem
= elem_find_first(fbx_cluster
, b
'TransformLink', default
=None)
2796 tx_bone
= array_to_matrix4(tx_bone_elem
.props
[0]) if tx_bone_elem
else None
2798 tx_arm_elem
= elem_find_first(fbx_cluster
, b
'TransformAssociateModel', default
=None)
2799 tx_arm
= array_to_matrix4(tx_arm_elem
.props
[0]) if tx_arm_elem
else None
2801 mesh_matrix
= tx_mesh
2802 armature_matrix
= tx_arm
2805 mesh_matrix
= tx_bone
@ mesh_matrix
2806 helper_node
.bind_matrix
= tx_bone
# overwrite the bind matrix
2808 # Get the meshes driven by this cluster: (Shouldn't that be only one?)
2810 for skin_uuid
, skin_link
in fbx_connection_map
.get(cluster_uuid
):
2811 if skin_link
.props
[0] != b
'OO':
2813 fbx_skin
, _
= fbx_table_nodes
.get(skin_uuid
, (None, None))
2814 if fbx_skin
is None or fbx_skin
.id != b
'Deformer' or fbx_skin
.props
[2] != b
'Skin':
2816 for mesh_uuid
, mesh_link
in fbx_connection_map
.get(skin_uuid
):
2817 if mesh_link
.props
[0] != b
'OO':
2819 fbx_mesh
, _
= fbx_table_nodes
.get(mesh_uuid
, (None, None))
2820 if fbx_mesh
is None or fbx_mesh
.id != b
'Geometry' or fbx_mesh
.props
[2] != b
'Mesh':
2822 for object_uuid
, object_link
in fbx_connection_map
.get(mesh_uuid
):
2823 if object_link
.props
[0] != b
'OO':
2825 mesh_node
= fbx_helper_nodes
[object_uuid
]
2828 # If we get a valid mesh matrix (in bone space), store armature and
2829 # mesh global matrices, we need them to compute mesh's matrix_parent_inverse
2830 # when actually binding them via the modifier.
2831 # Note we assume all bones were bound with the same mesh/armature (global) matrix,
2832 # we do not support otherwise in Blender anyway!
2833 mesh_node
.armature_setup
[helper_node
.armature
] = (mesh_matrix
, armature_matrix
)
2834 meshes
.add(mesh_node
)
2836 helper_node
.clusters
.append((fbx_cluster
, meshes
))
2838 # convert bind poses from global space into local space
2839 root_helper
.make_bind_pose_local()
2841 # collect armature meshes
2842 root_helper
.collect_armature_meshes()
2844 # find the correction matrices to align FBX objects with their Blender equivalent
2845 root_helper
.find_correction_matrix(settings
)
2847 # build the Object/Armature/Bone hierarchy
2848 root_helper
.build_hierarchy(fbx_tmpl
, settings
, scene
, view_layer
)
2850 # Link the Object/Armature/Bone hierarchy
2851 root_helper
.link_hierarchy(fbx_tmpl
, settings
, scene
)
2853 # root_helper.print_info(0)
2856 perfmon
.step("FBX import: ShapeKeys...")
2858 # We can handle shapes.
2859 blend_shape_channels
= {} # We do not need Shapes themselves, but keyblocks, for anim.
2862 fbx_tmpl
= fbx_template_get((b
'Geometry', b
'KFbxShape'))
2864 for s_uuid
, s_item
in fbx_table_nodes
.items():
2865 fbx_sdata
, bl_sdata
= s_item
= fbx_table_nodes
.get(s_uuid
, (None, None))
2866 if fbx_sdata
is None or fbx_sdata
.id != b
'Geometry' or fbx_sdata
.props
[2] != b
'Shape':
2869 # shape -> blendshapechannel -> blendshape -> mesh.
2870 for bc_uuid
, bc_ctype
in fbx_connection_map
.get(s_uuid
, ()):
2871 if bc_ctype
.props
[0] != b
'OO':
2873 fbx_bcdata
, _bl_bcdata
= fbx_table_nodes
.get(bc_uuid
, (None, None))
2874 if fbx_bcdata
is None or fbx_bcdata
.id != b
'Deformer' or fbx_bcdata
.props
[2] != b
'BlendShapeChannel':
2878 for bs_uuid
, bs_ctype
in fbx_connection_map
.get(bc_uuid
, ()):
2879 if bs_ctype
.props
[0] != b
'OO':
2881 fbx_bsdata
, _bl_bsdata
= fbx_table_nodes
.get(bs_uuid
, (None, None))
2882 if fbx_bsdata
is None or fbx_bsdata
.id != b
'Deformer' or fbx_bsdata
.props
[2] != b
'BlendShape':
2884 for m_uuid
, m_ctype
in fbx_connection_map
.get(bs_uuid
, ()):
2885 if m_ctype
.props
[0] != b
'OO':
2887 fbx_mdata
, bl_mdata
= fbx_table_nodes
.get(m_uuid
, (None, None))
2888 if fbx_mdata
is None or fbx_mdata
.id != b
'Geometry' or fbx_mdata
.props
[2] != b
'Mesh':
2890 # Blenmeshes are assumed already created at that time!
2891 assert(isinstance(bl_mdata
, bpy
.types
.Mesh
))
2892 # And we have to find all objects using this mesh!
2894 for o_uuid
, o_ctype
in fbx_connection_map
.get(m_uuid
, ()):
2895 if o_ctype
.props
[0] != b
'OO':
2897 node
= fbx_helper_nodes
[o_uuid
]
2899 objects
.append(node
)
2900 meshes
.append((bl_mdata
, objects
))
2901 # BlendShape deformers are only here to connect BlendShapeChannels to meshes, nothing else to do.
2903 # keyblocks is a list of tuples (mesh, keyblock) matching that shape/blendshapechannel, for animation.
2904 keyblocks
= blen_read_shape(fbx_tmpl
, fbx_sdata
, fbx_bcdata
, meshes
, scene
)
2905 blend_shape_channels
[bc_uuid
] = keyblocks
2908 if settings
.use_subsurf
:
2909 perfmon
.step("FBX import: Subdivision surfaces")
2911 # Look through connections for subsurf in meshes and add it to the parent object
2913 for fbx_link
in fbx_connections
.elems
:
2914 if fbx_link
.props
[0] != b
'OO':
2916 if fbx_link
.props_type
[1:3] == b
'LL':
2917 c_src
, c_dst
= fbx_link
.props
[1:3]
2918 parent
= fbx_helper_nodes
.get(c_dst
)
2922 child
= fbx_helper_nodes
.get(c_src
)
2924 fbx_sdata
, bl_data
= fbx_table_nodes
.get(c_src
, (None, None))
2925 if fbx_sdata
.id != b
'Geometry':
2928 preview_levels
= elem_prop_first(elem_find_first(fbx_sdata
, b
'PreviewDivisionLevels'))
2929 render_levels
= elem_prop_first(elem_find_first(fbx_sdata
, b
'RenderDivisionLevels'))
2930 if isinstance(preview_levels
, int) and isinstance(render_levels
, int):
2931 mod
= parent
.bl_obj
.modifiers
.new('subsurf', 'SUBSURF')
2932 mod
.levels
= preview_levels
2933 mod
.render_levels
= render_levels
2934 boundary_rule
= elem_prop_first(elem_find_first(fbx_sdata
, b
'BoundaryRule'), default
=1)
2935 if boundary_rule
== 1:
2936 mod
.boundary_smooth
= "PRESERVE_CORNERS"
2938 mod
.boundary_smooth
= "ALL"
2943 perfmon
.step("FBX import: Animations...")
2947 fbx_tmpl_astack
= fbx_template_get((b
'AnimationStack', b
'FbxAnimStack'))
2948 fbx_tmpl_alayer
= fbx_template_get((b
'AnimationLayer', b
'FbxAnimLayer'))
2952 for as_uuid
, fbx_asitem
in fbx_table_nodes
.items():
2953 fbx_asdata
, _blen_data
= fbx_asitem
2954 if fbx_asdata
.id != b
'AnimationStack' or fbx_asdata
.props
[2] != b
'':
2956 stacks
[as_uuid
] = (fbx_asitem
, {})
2959 # (mixing is completely ignored for now, each layer results in an independent set of actions).
2960 def get_astacks_from_alayer(al_uuid
):
2961 for as_uuid
, as_ctype
in fbx_connection_map
.get(al_uuid
, ()):
2962 if as_ctype
.props
[0] != b
'OO':
2964 fbx_asdata
, _bl_asdata
= fbx_table_nodes
.get(as_uuid
, (None, None))
2965 if (fbx_asdata
is None or fbx_asdata
.id != b
'AnimationStack' or
2966 fbx_asdata
.props
[2] != b
'' or as_uuid
not in stacks
):
2969 for al_uuid
, fbx_alitem
in fbx_table_nodes
.items():
2970 fbx_aldata
, _blen_data
= fbx_alitem
2971 if fbx_aldata
.id != b
'AnimationLayer' or fbx_aldata
.props
[2] != b
'':
2973 for as_uuid
in get_astacks_from_alayer(al_uuid
):
2974 _fbx_asitem
, alayers
= stacks
[as_uuid
]
2975 alayers
[al_uuid
] = (fbx_alitem
, {})
2977 # AnimationCurveNodes (also the ones linked to actual animated data!).
2979 for acn_uuid
, fbx_acnitem
in fbx_table_nodes
.items():
2980 fbx_acndata
, _blen_data
= fbx_acnitem
2981 if fbx_acndata
.id != b
'AnimationCurveNode' or fbx_acndata
.props
[2] != b
'':
2983 cnode
= curvenodes
[acn_uuid
] = {}
2985 for n_uuid
, n_ctype
in fbx_connection_map
.get(acn_uuid
, ()):
2986 if n_ctype
.props
[0] != b
'OP':
2988 lnk_prop
= n_ctype
.props
[3]
2989 if lnk_prop
in {b
'Lcl Translation', b
'Lcl Rotation', b
'Lcl Scaling'}:
2990 # n_uuid can (????) be linked to root '0' node, instead of a mere object node... See T41712.
2991 ob
= fbx_helper_nodes
.get(n_uuid
, None)
2992 if ob
is None or ob
.is_root
:
2994 items
.append((ob
, lnk_prop
))
2995 elif lnk_prop
== b
'DeformPercent': # Shape keys.
2996 keyblocks
= blend_shape_channels
.get(n_uuid
, None)
2997 if keyblocks
is None:
2999 items
+= [(kb
, lnk_prop
) for kb
in keyblocks
]
3000 elif lnk_prop
== b
'FocalLength': # Camera lens.
3001 from bpy
.types
import Camera
3002 fbx_item
= fbx_table_nodes
.get(n_uuid
, None)
3003 if fbx_item
is None or not isinstance(fbx_item
[1], Camera
):
3006 items
.append((cam
, lnk_prop
))
3007 elif lnk_prop
== b
'FocusDistance': # Camera focus.
3008 from bpy
.types
import Camera
3009 fbx_item
= fbx_table_nodes
.get(n_uuid
, None)
3010 if fbx_item
is None or not isinstance(fbx_item
[1], Camera
):
3013 items
.append((cam
, lnk_prop
))
3014 elif lnk_prop
== b
'DiffuseColor':
3015 from bpy
.types
import Material
3016 fbx_item
= fbx_table_nodes
.get(n_uuid
, None)
3017 if fbx_item
is None or not isinstance(fbx_item
[1], Material
):
3020 items
.append((mat
, lnk_prop
))
3021 print("WARNING! Importing material's animation is not supported for Nodal materials...")
3022 for al_uuid
, al_ctype
in fbx_connection_map
.get(acn_uuid
, ()):
3023 if al_ctype
.props
[0] != b
'OO':
3025 fbx_aldata
, _blen_aldata
= fbx_alitem
= fbx_table_nodes
.get(al_uuid
, (None, None))
3026 if fbx_aldata
is None or fbx_aldata
.id != b
'AnimationLayer' or fbx_aldata
.props
[2] != b
'':
3028 for as_uuid
in get_astacks_from_alayer(al_uuid
):
3029 _fbx_alitem
, anim_items
= stacks
[as_uuid
][1][al_uuid
]
3030 assert(_fbx_alitem
== fbx_alitem
)
3031 for item
, item_prop
in items
:
3032 # No need to keep curvenode FBX data here, contains nothing useful for us.
3033 anim_items
.setdefault(item
, {})[acn_uuid
] = (cnode
, item_prop
)
3035 # AnimationCurves (real animation data).
3036 for ac_uuid
, fbx_acitem
in fbx_table_nodes
.items():
3037 fbx_acdata
, _blen_data
= fbx_acitem
3038 if fbx_acdata
.id != b
'AnimationCurve' or fbx_acdata
.props
[2] != b
'':
3040 for acn_uuid
, acn_ctype
in fbx_connection_map
.get(ac_uuid
, ()):
3041 if acn_ctype
.props
[0] != b
'OP':
3043 fbx_acndata
, _bl_acndata
= fbx_table_nodes
.get(acn_uuid
, (None, None))
3044 if (fbx_acndata
is None or fbx_acndata
.id != b
'AnimationCurveNode' or
3045 fbx_acndata
.props
[2] != b
'' or acn_uuid
not in curvenodes
):
3047 # Note this is an infamous simplification of the compound props stuff,
3048 # seems to be standard naming but we'll probably have to be smarter to handle more exotic files?
3050 b
'd|X': 0, b
'd|Y': 1, b
'd|Z': 2,
3051 b
'd|DeformPercent': 0,
3052 b
'd|FocalLength': 0,
3053 b
'd|FocusDistance': 0
3054 }.get(acn_ctype
.props
[3], None)
3057 curvenodes
[acn_uuid
][ac_uuid
] = (fbx_acitem
, channel
)
3059 # And now that we have sorted all this, apply animations!
3060 blen_read_animations(fbx_tmpl_astack
, fbx_tmpl_alayer
, stacks
, scene
, settings
.anim_offset
, global_scale
)
3064 perfmon
.step("FBX import: Assign materials...")
3067 # link Material's to Geometry (via Model's)
3068 for fbx_uuid
, fbx_item
in fbx_table_nodes
.items():
3069 fbx_obj
, blen_data
= fbx_item
3070 if fbx_obj
.id != b
'Geometry':
3073 mesh
= fbx_table_nodes
.get(fbx_uuid
, (None, None))[1]
3075 # can happen in rare cases
3079 # In Blender, we link materials to data, typically (meshes), while in FBX they are linked to objects...
3080 # So we have to be careful not to re-add endlessly the same material to a mesh!
3081 # This can easily happen with 'baked' dupliobjects, see T44386.
3082 # TODO: add an option to link materials to objects in Blender instead?
3083 done_materials
= set()
3085 for (fbx_lnk
, fbx_lnk_item
, fbx_lnk_type
) in connection_filter_forward(fbx_uuid
, b
'Model'):
3087 fbx_lnk_uuid
= elem_uuid(fbx_lnk
)
3088 for (fbx_lnk_material
, material
, fbx_lnk_material_type
) in connection_filter_reverse(fbx_lnk_uuid
, b
'Material'):
3089 if material
not in done_materials
:
3090 mesh
.materials
.append(material
)
3091 done_materials
.add(material
)
3093 # We have to validate mesh polygons' ma_idx, see T41015!
3094 # Some FBX seem to have an extra 'default' material which is not defined in FBX file.
3095 if mesh
.validate_material_indices():
3096 print("WARNING: mesh '%s' had invalid material indices, those were reset to first material" % mesh
.name
)
3099 perfmon
.step("FBX import: Assign textures...")
3102 material_images
= {}
3104 fbx_tmpl
= fbx_template_get((b
'Material', b
'KFbxSurfacePhong'))
3105 # b'KFbxSurfaceLambert'
3107 def texture_mapping_set(fbx_obj
, node_texture
):
3108 assert(fbx_obj
.id == b
'Texture')
3110 fbx_props
= (elem_find_first(fbx_obj
, b
'Properties70'),
3111 elem_find_first(fbx_tmpl
, b
'Properties70', fbx_elem_nil
))
3112 loc
= elem_props_get_vector_3d(fbx_props
, b
'Translation', (0.0, 0.0, 0.0))
3113 rot
= tuple(-r
for r
in elem_props_get_vector_3d(fbx_props
, b
'Rotation', (0.0, 0.0, 0.0)))
3114 scale
= tuple(((1.0 / s
) if s
!= 0.0 else 1.0)
3115 for s
in elem_props_get_vector_3d(fbx_props
, b
'Scaling', (1.0, 1.0, 1.0)))
3116 clamp
= (bool(elem_props_get_enum(fbx_props
, b
'WrapModeU', 0)) or
3117 bool(elem_props_get_enum(fbx_props
, b
'WrapModeV', 0)))
3119 if (loc
== (0.0, 0.0, 0.0) and
3120 rot
== (0.0, 0.0, 0.0) and
3121 scale
== (1.0, 1.0, 1.0) and
3125 node_texture
.translation
= loc
3126 node_texture
.rotation
= rot
3127 node_texture
.scale
= scale
3129 node_texture
.extension
= 'EXTEND'
3131 for fbx_uuid
, fbx_item
in fbx_table_nodes
.items():
3132 fbx_obj
, blen_data
= fbx_item
3133 if fbx_obj
.id != b
'Material':
3136 material
= fbx_table_nodes
.get(fbx_uuid
, (None, None))[1]
3139 fbx_lnk_type
) in connection_filter_reverse(fbx_uuid
, b
'Texture'):
3141 if fbx_lnk_type
.props
[0] == b
'OP':
3142 lnk_type
= fbx_lnk_type
.props
[3]
3144 ma_wrap
= nodal_material_wrap_map
[material
]
3146 if lnk_type
in {b
'DiffuseColor', b
'3dsMax|maps|texmap_diffuse'}:
3147 ma_wrap
.base_color_texture
.image
= image
3148 texture_mapping_set(fbx_lnk
, ma_wrap
.base_color_texture
)
3149 elif lnk_type
in {b
'SpecularColor', b
'SpecularFactor'}:
3150 # Intensity actually, not color...
3151 ma_wrap
.specular_texture
.image
= image
3152 texture_mapping_set(fbx_lnk
, ma_wrap
.specular_texture
)
3153 elif lnk_type
in {b
'ReflectionColor', b
'ReflectionFactor', b
'3dsMax|maps|texmap_reflection'}:
3154 # Intensity actually, not color...
3155 ma_wrap
.metallic_texture
.image
= image
3156 texture_mapping_set(fbx_lnk
, ma_wrap
.metallic_texture
)
3157 elif lnk_type
in {b
'TransparentColor', b
'TransparencyFactor'}:
3158 ma_wrap
.alpha_texture
.image
= image
3159 texture_mapping_set(fbx_lnk
, ma_wrap
.alpha_texture
)
3160 if use_alpha_decals
:
3161 material_decals
.add(material
)
3162 elif lnk_type
== b
'ShininessExponent':
3163 # That is probably reversed compared to expected results? TODO...
3164 ma_wrap
.roughness_texture
.image
= image
3165 texture_mapping_set(fbx_lnk
, ma_wrap
.roughness_texture
)
3166 # XXX, applications abuse bump!
3167 elif lnk_type
in {b
'NormalMap', b
'Bump', b
'3dsMax|maps|texmap_bump'}:
3168 ma_wrap
.normalmap_texture
.image
= image
3169 texture_mapping_set(fbx_lnk
, ma_wrap
.normalmap_texture
)
3171 elif lnk_type == b'Bump':
3172 # TODO displacement...
3174 elif lnk_type
in {b
'EmissiveColor'}:
3175 ma_wrap
.emission_color_texture
.image
= image
3176 texture_mapping_set(fbx_lnk
, ma_wrap
.emission_color_texture
)
3177 elif lnk_type
in {b
'EmissiveFactor'}:
3178 ma_wrap
.emission_strength_texture
.image
= image
3179 texture_mapping_set(fbx_lnk
, ma_wrap
.emission_strength_texture
)
3181 print("WARNING: material link %r ignored" % lnk_type
)
3183 material_images
.setdefault(material
, {})[lnk_type
] = image
3185 # Check if the diffuse image has an alpha channel,
3186 # if so, use the alpha channel.
3188 # Note: this could be made optional since images may have alpha but be entirely opaque
3189 for fbx_uuid
, fbx_item
in fbx_table_nodes
.items():
3190 fbx_obj
, blen_data
= fbx_item
3191 if fbx_obj
.id != b
'Material':
3193 material
= fbx_table_nodes
.get(fbx_uuid
, (None, None))[1]
3194 image
= material_images
.get(material
, {}).get(b
'DiffuseColor', None)
3196 if image
and image
.depth
== 32:
3197 if use_alpha_decals
:
3198 material_decals
.add(material
)
3200 ma_wrap
= nodal_material_wrap_map
[material
]
3201 ma_wrap
.alpha_texture
.use_alpha
= True
3202 ma_wrap
.alpha_texture
.copy_from(ma_wrap
.base_color_texture
)
3204 # Propagate mapping from diffuse to all other channels which have none defined.
3205 # XXX Commenting for now, I do not really understand the logic here, why should diffuse mapping
3206 # be applied to all others if not defined for them???
3207 # ~ ma_wrap = nodal_material_wrap_map[material]
3208 # ~ ma_wrap.mapping_set_from_diffuse()
3212 perfmon
.step("FBX import: Cycles z-offset workaround...")
3215 # Annoying workaround for cycles having no z-offset
3216 if material_decals
and use_alpha_decals
:
3217 for fbx_uuid
, fbx_item
in fbx_table_nodes
.items():
3218 fbx_obj
, blen_data
= fbx_item
3219 if fbx_obj
.id != b
'Geometry':
3221 if fbx_obj
.props
[-1] == b
'Mesh':
3224 if decal_offset
!= 0.0:
3225 for material
in mesh
.materials
:
3226 if material
in material_decals
:
3227 for v
in mesh
.vertices
:
3228 v
.co
+= v
.normal
* decal_offset
3231 for obj
in (obj
for obj
in bpy
.data
.objects
if obj
.data
== mesh
):
3232 obj
.visible_shadow
= False
3235 perfmon
.level_down()
3237 perfmon
.level_down("Import finished.")