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 mathutils
import Matrix
, Euler
, Vector
22 from . import parse_fbx
, fbx_utils
24 from .parse_fbx
import (
28 from .fbx_utils
import (
30 units_blender_to_fbx_factor
,
38 # global singleton, assign on execution
42 convert_deg_to_rad_iter
= units_convertor_iter("degree", "radian")
44 MAT_CONVERT_BONE
= fbx_utils
.MAT_CONVERT_BONE
.inverted()
45 MAT_CONVERT_LIGHT
= fbx_utils
.MAT_CONVERT_LIGHT
.inverted()
46 MAT_CONVERT_CAMERA
= fbx_utils
.MAT_CONVERT_CAMERA
.inverted()
49 def validate_blend_names(name
):
50 assert(type(name
) == bytes
)
51 # Blender typically does not accept names over 63 bytes...
54 h
= hashlib
.sha1(name
).hexdigest()
56 name_utf8
= name
[:n
].decode('utf-8', 'replace') + "_" + h
[:7]
57 while len(name_utf8
.encode()) > 63:
59 name_utf8
= name
[:n
].decode('utf-8', 'replace') + "_" + h
[:7]
62 # We use 'replace' even though FBX 'specs' say it should always be utf8, see T53841.
63 return name
.decode('utf-8', 'replace')
66 def elem_find_first(elem
, id_search
, default
=None):
67 for fbx_item
in elem
.elems
:
68 if fbx_item
.id == id_search
:
73 def elem_find_iter(elem
, id_search
):
74 for fbx_item
in elem
.elems
:
75 if fbx_item
.id == id_search
:
79 def elem_find_first_string(elem
, id_search
):
80 fbx_item
= elem_find_first(elem
, id_search
)
81 if fbx_item
is not None and fbx_item
.props
: # Do not error on complete empty properties (see T45291).
82 assert(len(fbx_item
.props
) == 1)
83 assert(fbx_item
.props_type
[0] == data_types
.STRING
)
84 return fbx_item
.props
[0].decode('utf-8', 'replace')
88 def elem_find_first_string_as_bytes(elem
, id_search
):
89 fbx_item
= elem_find_first(elem
, id_search
)
90 if fbx_item
is not None and fbx_item
.props
: # Do not error on complete empty properties (see T45291).
91 assert(len(fbx_item
.props
) == 1)
92 assert(fbx_item
.props_type
[0] == data_types
.STRING
)
93 return fbx_item
.props
[0] # Keep it as bytes as requested...
97 def elem_find_first_bytes(elem
, id_search
, decode
=True):
98 fbx_item
= elem_find_first(elem
, id_search
)
99 if fbx_item
is not None and fbx_item
.props
: # Do not error on complete empty properties (see T45291).
100 assert(len(fbx_item
.props
) == 1)
101 assert(fbx_item
.props_type
[0] == data_types
.BYTES
)
102 return fbx_item
.props
[0]
107 return "%s: props[%d=%r], elems=(%r)" % (
110 ", ".join([repr(p
) for p
in elem
.props
]),
112 b
", ".join([e
.id for e
in elem
.elems
]),
116 def elem_split_name_class(elem
):
117 assert(elem
.props_type
[-2] == data_types
.STRING
)
118 elem_name
, elem_class
= elem
.props
[-2].split(b
'\x00\x01')
119 return elem_name
, elem_class
122 def elem_name_ensure_class(elem
, clss
=...):
123 elem_name
, elem_class
= elem_split_name_class(elem
)
125 assert(elem_class
== clss
)
126 return validate_blend_names(elem_name
)
129 def elem_name_ensure_classes(elem
, clss
=...):
130 elem_name
, elem_class
= elem_split_name_class(elem
)
132 assert(elem_class
in clss
)
133 return validate_blend_names(elem_name
)
136 def elem_split_name_class_nodeattr(elem
):
137 assert(elem
.props_type
[-2] == data_types
.STRING
)
138 elem_name
, elem_class
= elem
.props
[-2].split(b
'\x00\x01')
139 assert(elem_class
== b
'NodeAttribute')
140 assert(elem
.props_type
[-1] == data_types
.STRING
)
141 elem_class
= elem
.props
[-1]
142 return elem_name
, elem_class
146 assert(elem
.props_type
[0] == data_types
.INT64
)
150 def elem_prop_first(elem
, default
=None):
151 return elem
.props
[0] if (elem
is not None) and elem
.props
else default
156 # Properties70: { ... P:
157 def elem_props_find_first(elem
, elem_prop_id
):
159 # When properties are not found... Should never happen, but happens - as usual.
161 # support for templates (tuple of elems)
162 if type(elem
) is not FBXElem
:
163 assert(type(elem
) is tuple)
165 result
= elem_props_find_first(e
, elem_prop_id
)
166 if result
is not None:
168 assert(len(elem
) > 0)
171 for subelem
in elem
.elems
:
172 assert(subelem
.id == b
'P')
173 if subelem
.props
[0] == elem_prop_id
:
178 def elem_props_get_color_rgb(elem
, elem_prop_id
, default
=None):
179 elem_prop
= elem_props_find_first(elem
, elem_prop_id
)
180 if elem_prop
is not None:
181 assert(elem_prop
.props
[0] == elem_prop_id
)
182 if elem_prop
.props
[1] == b
'Color':
184 assert(elem_prop
.props
[1] == b
'Color')
185 assert(elem_prop
.props
[2] == b
'')
187 assert(elem_prop
.props
[1] == b
'ColorRGB')
188 assert(elem_prop
.props
[2] == b
'Color')
189 assert(elem_prop
.props_type
[4:7] == bytes((data_types
.FLOAT64
,)) * 3)
190 return elem_prop
.props
[4:7]
194 def elem_props_get_vector_3d(elem
, elem_prop_id
, default
=None):
195 elem_prop
= elem_props_find_first(elem
, elem_prop_id
)
196 if elem_prop
is not None:
197 assert(elem_prop
.props_type
[4:7] == bytes((data_types
.FLOAT64
,)) * 3)
198 return elem_prop
.props
[4:7]
202 def elem_props_get_number(elem
, elem_prop_id
, default
=None):
203 elem_prop
= elem_props_find_first(elem
, elem_prop_id
)
204 if elem_prop
is not None:
205 assert(elem_prop
.props
[0] == elem_prop_id
)
206 if elem_prop
.props
[1] == b
'double':
207 assert(elem_prop
.props
[1] == b
'double')
208 assert(elem_prop
.props
[2] == b
'Number')
210 assert(elem_prop
.props
[1] == b
'Number')
211 assert(elem_prop
.props
[2] == b
'')
213 # we could allow other number types
214 assert(elem_prop
.props_type
[4] == data_types
.FLOAT64
)
216 return elem_prop
.props
[4]
220 def elem_props_get_integer(elem
, elem_prop_id
, default
=None):
221 elem_prop
= elem_props_find_first(elem
, elem_prop_id
)
222 if elem_prop
is not None:
223 assert(elem_prop
.props
[0] == elem_prop_id
)
224 if elem_prop
.props
[1] == b
'int':
225 assert(elem_prop
.props
[1] == b
'int')
226 assert(elem_prop
.props
[2] == b
'Integer')
227 elif elem_prop
.props
[1] == b
'ULongLong':
228 assert(elem_prop
.props
[1] == b
'ULongLong')
229 assert(elem_prop
.props
[2] == b
'')
231 # we could allow other number types
232 assert(elem_prop
.props_type
[4] in {data_types
.INT32
, data_types
.INT64
})
234 return elem_prop
.props
[4]
238 def elem_props_get_bool(elem
, elem_prop_id
, default
=None):
239 elem_prop
= elem_props_find_first(elem
, elem_prop_id
)
240 if elem_prop
is not None:
241 assert(elem_prop
.props
[0] == elem_prop_id
)
242 # b'Bool' with a capital seems to be used for animated property... go figure...
243 assert(elem_prop
.props
[1] in {b
'bool', b
'Bool'})
244 assert(elem_prop
.props
[2] == b
'')
246 # we could allow other number types
247 assert(elem_prop
.props_type
[4] == data_types
.INT32
)
248 assert(elem_prop
.props
[4] in {0, 1})
250 return bool(elem_prop
.props
[4])
254 def elem_props_get_enum(elem
, elem_prop_id
, default
=None):
255 elem_prop
= elem_props_find_first(elem
, elem_prop_id
)
256 if elem_prop
is not None:
257 assert(elem_prop
.props
[0] == elem_prop_id
)
258 assert(elem_prop
.props
[1] == b
'enum')
259 assert(elem_prop
.props
[2] == b
'')
260 assert(elem_prop
.props
[3] == b
'')
262 # we could allow other number types
263 assert(elem_prop
.props_type
[4] == data_types
.INT32
)
265 return elem_prop
.props
[4]
269 def elem_props_get_visibility(elem
, elem_prop_id
, default
=None):
270 elem_prop
= elem_props_find_first(elem
, elem_prop_id
)
271 if elem_prop
is not None:
272 assert(elem_prop
.props
[0] == elem_prop_id
)
273 assert(elem_prop
.props
[1] == b
'Visibility')
274 assert(elem_prop
.props
[2] == b
'')
276 # we could allow other number types
277 assert(elem_prop
.props_type
[4] == data_types
.FLOAT64
)
279 return elem_prop
.props
[4]
283 # ----------------------------------------------------------------------------
288 from collections
import namedtuple
291 FBXTransformData
= namedtuple("FBXTransformData", (
293 "rot", "rot_ofs", "rot_piv", "pre_rot", "pst_rot", "rot_ord", "rot_alt_mat", "geom_rot",
294 "sca", "sca_ofs", "sca_piv", "geom_sca",
298 def blen_read_custom_properties(fbx_obj
, blen_obj
, settings
):
299 # There doesn't seem to be a way to put user properties into templates, so this only get the object properties:
300 fbx_obj_props
= elem_find_first(fbx_obj
, b
'Properties70')
302 for fbx_prop
in fbx_obj_props
.elems
:
303 assert(fbx_prop
.id == b
'P')
305 if b
'U' in fbx_prop
.props
[3]:
306 if fbx_prop
.props
[0] == b
'UDP3DSMAX':
307 # Special case for 3DS Max user properties:
308 assert(fbx_prop
.props
[1] == b
'KString')
309 assert(fbx_prop
.props_type
[4] == data_types
.STRING
)
310 items
= fbx_prop
.props
[4].decode('utf-8', 'replace')
311 for item
in items
.split('\r\n'):
313 split_item
= item
.split('=', 1)
314 if len(split_item
) != 2:
315 split_item
= item
.split(':', 1)
316 if len(split_item
) != 2:
317 print("cannot parse UDP3DSMAX custom property '%s', ignoring..." % item
)
319 prop_name
, prop_value
= split_item
320 prop_name
= validate_blend_names(prop_name
.strip().encode('utf-8'))
321 blen_obj
[prop_name
] = prop_value
.strip()
323 prop_name
= validate_blend_names(fbx_prop
.props
[0])
324 prop_type
= fbx_prop
.props
[1]
325 if prop_type
in {b
'Vector', b
'Vector3D', b
'Color', b
'ColorRGB'}:
326 assert(fbx_prop
.props_type
[4:7] == bytes((data_types
.FLOAT64
,)) * 3)
327 blen_obj
[prop_name
] = fbx_prop
.props
[4:7]
328 elif prop_type
in {b
'Vector4', b
'ColorRGBA'}:
329 assert(fbx_prop
.props_type
[4:8] == bytes((data_types
.FLOAT64
,)) * 4)
330 blen_obj
[prop_name
] = fbx_prop
.props
[4:8]
331 elif prop_type
== b
'Vector2D':
332 assert(fbx_prop
.props_type
[4:6] == bytes((data_types
.FLOAT64
,)) * 2)
333 blen_obj
[prop_name
] = fbx_prop
.props
[4:6]
334 elif prop_type
in {b
'Integer', b
'int'}:
335 assert(fbx_prop
.props_type
[4] == data_types
.INT32
)
336 blen_obj
[prop_name
] = fbx_prop
.props
[4]
337 elif prop_type
== b
'KString':
338 assert(fbx_prop
.props_type
[4] == data_types
.STRING
)
339 blen_obj
[prop_name
] = fbx_prop
.props
[4].decode('utf-8', 'replace')
340 elif prop_type
in {b
'Number', b
'double', b
'Double'}:
341 assert(fbx_prop
.props_type
[4] == data_types
.FLOAT64
)
342 blen_obj
[prop_name
] = fbx_prop
.props
[4]
343 elif prop_type
in {b
'Float', b
'float'}:
344 assert(fbx_prop
.props_type
[4] == data_types
.FLOAT32
)
345 blen_obj
[prop_name
] = fbx_prop
.props
[4]
346 elif prop_type
in {b
'Bool', b
'bool'}:
347 assert(fbx_prop
.props_type
[4] == data_types
.INT32
)
348 blen_obj
[prop_name
] = fbx_prop
.props
[4] != 0
349 elif prop_type
in {b
'Enum', b
'enum'}:
350 assert(fbx_prop
.props_type
[4:6] == bytes((data_types
.INT32
, data_types
.STRING
)))
351 val
= fbx_prop
.props
[4]
352 if settings
.use_custom_props_enum_as_string
and fbx_prop
.props
[5]:
353 enum_items
= fbx_prop
.props
[5].decode('utf-8', 'replace').split('~')
354 if val
>= 0 and val
< len(enum_items
):
355 blen_obj
[prop_name
] = enum_items
[val
]
357 print ("WARNING: User property '%s' has wrong enum value, skipped" % prop_name
)
359 blen_obj
[prop_name
] = val
361 print ("WARNING: User property type '%s' is not supported" % prop_type
.decode('utf-8', 'replace'))
364 def blen_read_object_transform_do(transform_data
):
365 # This is a nightmare. FBX SDK uses Maya way to compute the transformation matrix of a node - utterly simple:
367 # WorldTransform = ParentWorldTransform @ T @ Roff @ Rp @ Rpre @ R @ Rpost @ Rp-1 @ Soff @ Sp @ S @ Sp-1
369 # Where all those terms are 4 x 4 matrices that contain:
370 # WorldTransform: Transformation matrix of the node in global space.
371 # ParentWorldTransform: Transformation matrix of the parent node in global space.
373 # Roff: Rotation offset
377 # Rpost: Post-rotation
378 # Rp-1: Inverse of the rotation pivot
379 # Soff: Scaling offset
382 # Sp-1: Inverse of the scaling pivot
384 # But it was still too simple, and FBX notion of compatibility is... quite specific. So we also have to
385 # support 3DSMax way:
387 # WorldTransform = ParentWorldTransform @ T @ R @ S @ OT @ OR @ OS
389 # Where all those terms are 4 x 4 matrices that contain:
390 # WorldTransform: Transformation matrix of the node in global space
391 # ParentWorldTransform: Transformation matrix of the parent node in global space
395 # OT: Geometric transform translation
396 # OR: Geometric transform rotation
397 # OS: Geometric transform translation
400 # Geometric transformations ***are not inherited***: ParentWorldTransform does not contain the OT, OR, OS
401 # of WorldTransform's parent node.
403 # Taken from http://download.autodesk.com/us/fbx/20112/FBX_SDK_HELP/
404 # index.html?url=WS1a9193826455f5ff1f92379812724681e696651.htm,topicNumber=d0e7429
407 lcl_translation
= Matrix
.Translation(transform_data
.loc
)
408 geom_loc
= Matrix
.Translation(transform_data
.geom_loc
)
411 to_rot
= lambda rot
, rot_ord
: Euler(convert_deg_to_rad_iter(rot
), rot_ord
).to_matrix().to_4x4()
412 lcl_rot
= to_rot(transform_data
.rot
, transform_data
.rot_ord
) @ transform_data
.rot_alt_mat
413 pre_rot
= to_rot(transform_data
.pre_rot
, transform_data
.rot_ord
)
414 pst_rot
= to_rot(transform_data
.pst_rot
, transform_data
.rot_ord
)
415 geom_rot
= to_rot(transform_data
.geom_rot
, transform_data
.rot_ord
)
417 rot_ofs
= Matrix
.Translation(transform_data
.rot_ofs
)
418 rot_piv
= Matrix
.Translation(transform_data
.rot_piv
)
419 sca_ofs
= Matrix
.Translation(transform_data
.sca_ofs
)
420 sca_piv
= Matrix
.Translation(transform_data
.sca_piv
)
424 lcl_scale
[0][0], lcl_scale
[1][1], lcl_scale
[2][2] = transform_data
.sca
425 geom_scale
= Matrix();
426 geom_scale
[0][0], geom_scale
[1][1], geom_scale
[2][2] = transform_data
.geom_sca
435 rot_piv
.inverted_safe() @
439 sca_piv
.inverted_safe()
441 geom_mat
= geom_loc
@ geom_rot
@ geom_scale
442 # We return mat without 'geometric transforms' too, because it is to be used for children, sigh...
443 return (base_mat
@ geom_mat
, base_mat
, geom_mat
)
446 # XXX This might be weak, now that we can add vgroups from both bones and shapes, name collisions become
447 # more likely, will have to make this more robust!!!
448 def add_vgroup_to_objects(vg_indices
, vg_weights
, vg_name
, objects
):
449 assert(len(vg_indices
) == len(vg_weights
))
452 # We replace/override here...
453 vg
= obj
.vertex_groups
.get(vg_name
)
455 vg
= obj
.vertex_groups
.new(name
=vg_name
)
456 for i
, w
in zip(vg_indices
, vg_weights
):
457 vg
.add((i
,), w
, 'REPLACE')
460 def blen_read_object_transform_preprocess(fbx_props
, fbx_obj
, rot_alt_mat
, use_prepost_rot
):
461 # This is quite involved, 'fbxRNode.cpp' from openscenegraph used as a reference
462 const_vector_zero_3d
= 0.0, 0.0, 0.0
463 const_vector_one_3d
= 1.0, 1.0, 1.0
465 loc
= list(elem_props_get_vector_3d(fbx_props
, b
'Lcl Translation', const_vector_zero_3d
))
466 rot
= list(elem_props_get_vector_3d(fbx_props
, b
'Lcl Rotation', const_vector_zero_3d
))
467 sca
= list(elem_props_get_vector_3d(fbx_props
, b
'Lcl Scaling', const_vector_one_3d
))
469 geom_loc
= list(elem_props_get_vector_3d(fbx_props
, b
'GeometricTranslation', const_vector_zero_3d
))
470 geom_rot
= list(elem_props_get_vector_3d(fbx_props
, b
'GeometricRotation', const_vector_zero_3d
))
471 geom_sca
= list(elem_props_get_vector_3d(fbx_props
, b
'GeometricScaling', const_vector_one_3d
))
473 rot_ofs
= elem_props_get_vector_3d(fbx_props
, b
'RotationOffset', const_vector_zero_3d
)
474 rot_piv
= elem_props_get_vector_3d(fbx_props
, b
'RotationPivot', const_vector_zero_3d
)
475 sca_ofs
= elem_props_get_vector_3d(fbx_props
, b
'ScalingOffset', const_vector_zero_3d
)
476 sca_piv
= elem_props_get_vector_3d(fbx_props
, b
'ScalingPivot', const_vector_zero_3d
)
478 is_rot_act
= elem_props_get_bool(fbx_props
, b
'RotationActive', False)
482 pre_rot
= elem_props_get_vector_3d(fbx_props
, b
'PreRotation', const_vector_zero_3d
)
483 pst_rot
= elem_props_get_vector_3d(fbx_props
, b
'PostRotation', const_vector_zero_3d
)
485 pre_rot
= const_vector_zero_3d
486 pst_rot
= const_vector_zero_3d
494 6: 'XYZ', # XXX eSphericXYZ, not really supported...
495 }.get(elem_props_get_enum(fbx_props
, b
'RotationOrder', 0))
497 pre_rot
= const_vector_zero_3d
498 pst_rot
= const_vector_zero_3d
501 return FBXTransformData(loc
, geom_loc
,
502 rot
, rot_ofs
, rot_piv
, pre_rot
, pst_rot
, rot_ord
, rot_alt_mat
, geom_rot
,
503 sca
, sca_ofs
, sca_piv
, geom_sca
)
508 def blen_read_animations_curves_iter(fbx_curves
, blen_start_offset
, fbx_start_offset
, fps
):
510 Get raw FBX AnimCurve list, and yield values for all curves at each singular curves' keyframes,
511 together with (blender) timing, in frames.
512 blen_start_offset is expected in frames, while fbx_start_offset is expected in FBX ktime.
514 # As a first step, assume linear interpolation between key frames, we'll (try to!) handle more
515 # of FBX curves later.
516 from .fbx_utils
import FBX_KTIME
517 timefac
= fps
/ FBX_KTIME
520 elem_prop_first(elem_find_first(c
[2], b
'KeyTime')),
521 elem_prop_first(elem_find_first(c
[2], b
'KeyValueFloat')),
525 allkeys
= sorted({item
for sublist
in curves
for item
in sublist
[1]})
526 for curr_fbxktime
in allkeys
:
529 idx
, times
, values
, fbx_curve
= item
531 if times
[idx
] < curr_fbxktime
:
534 if idx
>= len(times
):
535 # We have reached our last element for this curve, stay on it from now on...
539 if times
[idx
] >= curr_fbxktime
:
541 curr_values
.append((values
[idx
], fbx_curve
))
543 # Interpolate between this key and the previous one.
544 ifac
= (curr_fbxktime
- times
[idx
- 1]) / (times
[idx
] - times
[idx
- 1])
545 curr_values
.append(((values
[idx
] - values
[idx
- 1]) * ifac
+ values
[idx
- 1], fbx_curve
))
546 curr_blenkframe
= (curr_fbxktime
- fbx_start_offset
) * timefac
+ blen_start_offset
547 yield (curr_blenkframe
, curr_values
)
550 def blen_read_animations_action_item(action
, item
, cnodes
, fps
, anim_offset
, global_scale
):
552 'Bake' loc/rot/scale into the action,
553 taking any pre_ and post_ matrix into account to transform from fbx into blender space.
555 from bpy
.types
import Object
, PoseBone
, ShapeKey
, Material
, Camera
556 from itertools
import chain
559 for curves
, fbxprop
in cnodes
.values():
560 for (fbx_acdata
, _blen_data
), channel
in curves
.values():
561 fbx_curves
.append((fbxprop
, channel
, fbx_acdata
))
563 # Leave if no curves are attached (if a blender curve is attached to scale but without keys it defaults to 0).
564 if len(fbx_curves
) == 0:
571 # Add each keyframe to the keyframe dict
572 def store_keyframe(fc
, frame
, value
):
573 fc_key
= (fc
.data_path
, fc
.array_index
)
574 if not keyframes
.get(fc_key
):
575 keyframes
[fc_key
] = []
576 keyframes
[fc_key
].extend((frame
, value
))
578 if isinstance(item
, Material
):
580 props
= [("diffuse_color", 3, grpname
or "Diffuse Color")]
581 elif isinstance(item
, ShapeKey
):
582 props
= [(item
.path_from_id("value"), 1, "Key")]
583 elif isinstance(item
, Camera
):
584 props
= [(item
.path_from_id("lens"), 1, "Camera"), (item
.dof
.path_from_id("focus_distance"), 1, "Camera")]
585 else: # Object or PoseBone:
587 bl_obj
= item
.bl_obj
.pose
.bones
[item
.bl_bone
]
591 # We want to create actions for objects, but for bones we 'reuse' armatures' actions!
592 grpname
= bl_obj
.name
594 # Since we might get other channels animated in the end, due to all FBX transform magic,
595 # we need to add curves for whole loc/rot/scale in any case.
596 props
= [(bl_obj
.path_from_id("location"), 3, grpname
or "Location"),
598 (bl_obj
.path_from_id("scale"), 3, grpname
or "Scale")]
599 rot_mode
= bl_obj
.rotation_mode
600 if rot_mode
== 'QUATERNION':
601 props
[1] = (bl_obj
.path_from_id("rotation_quaternion"), 4, grpname
or "Quaternion Rotation")
602 elif rot_mode
== 'AXIS_ANGLE':
603 props
[1] = (bl_obj
.path_from_id("rotation_axis_angle"), 4, grpname
or "Axis Angle Rotation")
605 props
[1] = (bl_obj
.path_from_id("rotation_euler"), 3, grpname
or "Euler Rotation")
607 blen_curves
= [action
.fcurves
.new(prop
, index
=channel
, action_group
=grpname
)
608 for prop
, nbr_channels
, grpname
in props
for channel
in range(nbr_channels
)]
610 if isinstance(item
, Material
):
611 for frame
, values
in blen_read_animations_curves_iter(fbx_curves
, anim_offset
, 0, fps
):
613 for v
, (fbxprop
, channel
, _fbx_acdata
) in values
:
614 assert(fbxprop
== b
'DiffuseColor')
615 assert(channel
in {0, 1, 2})
618 for fc
, v
in zip(blen_curves
, value
):
619 store_keyframe(fc
, frame
, v
)
621 elif isinstance(item
, ShapeKey
):
622 for frame
, values
in blen_read_animations_curves_iter(fbx_curves
, anim_offset
, 0, fps
):
624 for v
, (fbxprop
, channel
, _fbx_acdata
) in values
:
625 assert(fbxprop
== b
'DeformPercent')
629 for fc
, v
in zip(blen_curves
, (value
,)):
630 store_keyframe(fc
, frame
, v
)
632 elif isinstance(item
, Camera
):
633 for frame
, values
in blen_read_animations_curves_iter(fbx_curves
, anim_offset
, 0, fps
):
636 for v
, (fbxprop
, channel
, _fbx_acdata
) in values
:
637 assert(fbxprop
== b
'FocalLength' or fbxprop
== b
'FocusDistance' )
639 if (fbxprop
== b
'FocalLength' ):
641 elif(fbxprop
== b
'FocusDistance'):
642 focus_distance
= v
/ 1000 * global_scale
644 for fc
, v
in zip(blen_curves
, (focal_length
, focus_distance
)):
645 store_keyframe(fc
, frame
, v
)
647 else: # Object or PoseBone:
649 bl_obj
= item
.bl_obj
.pose
.bones
[item
.bl_bone
]
653 transform_data
= item
.fbx_transform_data
654 rot_eul_prev
= bl_obj
.rotation_euler
.copy()
655 rot_quat_prev
= bl_obj
.rotation_quaternion
.copy()
657 # Pre-compute inverted local rest matrix of the bone, if relevant.
658 restmat_inv
= item
.get_bind_matrix().inverted_safe() if item
.is_bone
else None
660 for frame
, values
in blen_read_animations_curves_iter(fbx_curves
, anim_offset
, 0, fps
):
661 for v
, (fbxprop
, channel
, _fbx_acdata
) in values
:
662 if fbxprop
== b
'Lcl Translation':
663 transform_data
.loc
[channel
] = v
664 elif fbxprop
== b
'Lcl Rotation':
665 transform_data
.rot
[channel
] = v
666 elif fbxprop
== b
'Lcl Scaling':
667 transform_data
.sca
[channel
] = v
668 mat
, _
, _
= blen_read_object_transform_do(transform_data
)
670 # compensate for changes in the local matrix during processing
671 if item
.anim_compensation_matrix
:
672 mat
= mat
@ item
.anim_compensation_matrix
674 # apply pre- and post matrix
675 # post-matrix will contain any correction for lights, camera and bone orientation
676 # pre-matrix will contain any correction for a parent's correction matrix or the global matrix
678 mat
= item
.pre_matrix
@ mat
680 mat
= mat
@ item
.post_matrix
682 # And now, remove that rest pose matrix from current mat (also in parent space).
684 mat
= restmat_inv
@ mat
686 # Now we have a virtual matrix of transform from AnimCurves, we can insert keyframes!
687 loc
, rot
, sca
= mat
.decompose()
688 if rot_mode
== 'QUATERNION':
689 if rot_quat_prev
.dot(rot
) < 0.0:
692 elif rot_mode
== 'AXIS_ANGLE':
693 vec
, ang
= rot
.to_axis_angle()
694 rot
= ang
, vec
.x
, vec
.y
, vec
.z
696 rot
= rot
.to_euler(rot_mode
, rot_eul_prev
)
699 # Add each keyframe and its value to the keyframe dict
700 for fc
, value
in zip(blen_curves
, chain(loc
, rot
, sca
)):
701 store_keyframe(fc
, frame
, value
)
703 # Add all keyframe points to the fcurves at once and modify them after
704 for fc_key
, key_values
in keyframes
.items():
705 data_path
, index
= fc_key
707 # Add all keyframe points at once
708 fcurve
= action
.fcurves
.find(data_path
=data_path
, index
=index
)
709 num_keys
= len(key_values
) // 2
710 fcurve
.keyframe_points
.add(num_keys
)
711 fcurve
.keyframe_points
.foreach_set('co', key_values
)
712 linear_enum_value
= bpy
.types
.Keyframe
.bl_rna
.properties
['interpolation'].enum_items
['LINEAR'].value
713 fcurve
.keyframe_points
.foreach_set('interpolation', (linear_enum_value
,) * num_keys
)
715 # Since we inserted our keyframes in 'ultra-fast' mode, we have to update the fcurves now.
716 for fc
in blen_curves
:
720 def blen_read_animations(fbx_tmpl_astack
, fbx_tmpl_alayer
, stacks
, scene
, anim_offset
, global_scale
):
722 Recreate an action per stack/layer/object combinations.
723 Only the first found action is linked to objects, more complex setups are not handled,
724 it's up to user to reproduce them!
726 from bpy
.types
import ShapeKey
, Material
, Camera
729 for as_uuid
, ((fbx_asdata
, _blen_data
), alayers
) in stacks
.items():
730 stack_name
= elem_name_ensure_class(fbx_asdata
, b
'AnimStack')
731 for al_uuid
, ((fbx_aldata
, _blen_data
), items
) in alayers
.items():
732 layer_name
= elem_name_ensure_class(fbx_aldata
, b
'AnimLayer')
733 for item
, cnodes
in items
.items():
734 if isinstance(item
, Material
):
736 elif isinstance(item
, ShapeKey
):
737 id_data
= item
.id_data
738 elif isinstance(item
, Camera
):
741 id_data
= item
.bl_obj
742 # XXX Ignore rigged mesh animations - those are a nightmare to handle, see note about it in
743 # FbxImportHelperNode class definition.
744 if id_data
and id_data
.type == 'MESH' and id_data
.parent
and id_data
.parent
.type == 'ARMATURE':
749 # Create new action if needed (should always be needed, except for keyblocks from shapekeys cases).
750 key
= (as_uuid
, al_uuid
, id_data
)
751 action
= actions
.get(key
)
753 if stack_name
== layer_name
:
754 action_name
= "|".join((id_data
.name
, stack_name
))
756 action_name
= "|".join((id_data
.name
, stack_name
, layer_name
))
757 actions
[key
] = action
= bpy
.data
.actions
.new(action_name
)
758 action
.use_fake_user
= True
759 # If none yet assigned, assign this action to id_data.
760 if not id_data
.animation_data
:
761 id_data
.animation_data_create()
762 if not id_data
.animation_data
.action
:
763 id_data
.animation_data
.action
= action
764 # And actually populate the action!
765 blen_read_animations_action_item(action
, item
, cnodes
, scene
.render
.fps
, anim_offset
, global_scale
)
771 def blen_read_geom_layerinfo(fbx_layer
):
773 validate_blend_names(elem_find_first_string_as_bytes(fbx_layer
, b
'Name')),
774 elem_find_first_string_as_bytes(fbx_layer
, b
'MappingInformationType'),
775 elem_find_first_string_as_bytes(fbx_layer
, b
'ReferenceInformationType'),
779 def blen_read_geom_array_setattr(generator
, blen_data
, blen_attr
, fbx_data
, stride
, item_size
, descr
, xform
):
780 """Generic fbx_layer to blen_data setter, generator is expected to yield tuples (ble_idx, fbx_idx)."""
781 max_blen_idx
= len(blen_data
) - 1
782 max_fbx_idx
= len(fbx_data
) - 1
785 def check_skip(blen_idx
, fbx_idx
):
787 if fbx_idx
< 0: # Negative values mean 'skip'.
789 if blen_idx
> max_blen_idx
:
791 print("ERROR: too much data in this Blender layer, compared to elements in mesh, skipping!")
794 if fbx_idx
+ item_size
- 1 > max_fbx_idx
:
796 print("ERROR: not enough data in this FBX layer, skipping!")
801 if xform
is not None:
802 if isinstance(blen_data
, list):
804 def _process(blend_data
, blen_attr
, fbx_data
, xform
, item_size
, blen_idx
, fbx_idx
):
805 blen_data
[blen_idx
] = xform(fbx_data
[fbx_idx
])
807 def _process(blend_data
, blen_attr
, fbx_data
, xform
, item_size
, blen_idx
, fbx_idx
):
808 blen_data
[blen_idx
] = xform(fbx_data
[fbx_idx
:fbx_idx
+ item_size
])
811 def _process(blend_data
, blen_attr
, fbx_data
, xform
, item_size
, blen_idx
, fbx_idx
):
812 setattr(blen_data
[blen_idx
], blen_attr
, xform(fbx_data
[fbx_idx
]))
814 def _process(blend_data
, blen_attr
, fbx_data
, xform
, item_size
, blen_idx
, fbx_idx
):
815 setattr(blen_data
[blen_idx
], blen_attr
, xform(fbx_data
[fbx_idx
:fbx_idx
+ item_size
]))
817 if isinstance(blen_data
, list):
819 def _process(blend_data
, blen_attr
, fbx_data
, xform
, item_size
, blen_idx
, fbx_idx
):
820 blen_data
[blen_idx
] = fbx_data
[fbx_idx
]
822 def _process(blend_data
, blen_attr
, fbx_data
, xform
, item_size
, blen_idx
, fbx_idx
):
823 blen_data
[blen_idx
] = fbx_data
[fbx_idx
:fbx_idx
+ item_size
]
826 def _process(blend_data
, blen_attr
, fbx_data
, xform
, item_size
, blen_idx
, fbx_idx
):
827 setattr(blen_data
[blen_idx
], blen_attr
, fbx_data
[fbx_idx
])
829 def _process(blend_data
, blen_attr
, fbx_data
, xform
, item_size
, blen_idx
, fbx_idx
):
830 setattr(blen_data
[blen_idx
], blen_attr
, fbx_data
[fbx_idx
:fbx_idx
+ item_size
])
832 for blen_idx
, fbx_idx
in generator
:
833 if check_skip(blen_idx
, fbx_idx
):
835 _process(blen_data
, blen_attr
, fbx_data
, xform
, item_size
, blen_idx
, fbx_idx
)
838 # generic generators.
839 def blen_read_geom_array_gen_allsame(data_len
):
840 return zip(*(range(data_len
), (0,) * data_len
))
843 def blen_read_geom_array_gen_direct(fbx_data
, stride
):
844 fbx_data_len
= len(fbx_data
)
845 return zip(*(range(fbx_data_len
// stride
), range(0, fbx_data_len
, stride
)))
848 def blen_read_geom_array_gen_indextodirect(fbx_layer_index
, stride
):
849 return ((bi
, fi
* stride
) for bi
, fi
in enumerate(fbx_layer_index
))
852 def blen_read_geom_array_gen_direct_looptovert(mesh
, fbx_data
, stride
):
853 fbx_data_len
= len(fbx_data
) // stride
855 for p
in mesh
.polygons
:
856 for lidx
in p
.loop_indices
:
857 vidx
= loops
[lidx
].vertex_index
858 if vidx
< fbx_data_len
:
859 yield lidx
, vidx
* stride
862 # generic error printers.
863 def blen_read_geom_array_error_mapping(descr
, fbx_layer_mapping
, quiet
=False):
865 print("warning layer %r mapping type unsupported: %r" % (descr
, fbx_layer_mapping
))
868 def blen_read_geom_array_error_ref(descr
, fbx_layer_ref
, quiet
=False):
870 print("warning layer %r ref type unsupported: %r" % (descr
, fbx_layer_ref
))
873 def blen_read_geom_array_mapped_vert(
874 mesh
, blen_data
, blen_attr
,
875 fbx_layer_data
, fbx_layer_index
,
876 fbx_layer_mapping
, fbx_layer_ref
,
877 stride
, item_size
, descr
,
878 xform
=None, quiet
=False,
880 if fbx_layer_mapping
== b
'ByVertice':
881 if fbx_layer_ref
== b
'Direct':
882 assert(fbx_layer_index
is None)
883 blen_read_geom_array_setattr(blen_read_geom_array_gen_direct(fbx_layer_data
, stride
),
884 blen_data
, blen_attr
, fbx_layer_data
, stride
, item_size
, descr
, xform
)
886 blen_read_geom_array_error_ref(descr
, fbx_layer_ref
, quiet
)
887 elif fbx_layer_mapping
== b
'AllSame':
888 if fbx_layer_ref
== b
'IndexToDirect':
889 assert(fbx_layer_index
is None)
890 blen_read_geom_array_setattr(blen_read_geom_array_gen_allsame(len(blen_data
)),
891 blen_data
, blen_attr
, fbx_layer_data
, stride
, item_size
, descr
, xform
)
893 blen_read_geom_array_error_ref(descr
, fbx_layer_ref
, quiet
)
895 blen_read_geom_array_error_mapping(descr
, fbx_layer_mapping
, quiet
)
900 def blen_read_geom_array_mapped_edge(
901 mesh
, blen_data
, blen_attr
,
902 fbx_layer_data
, fbx_layer_index
,
903 fbx_layer_mapping
, fbx_layer_ref
,
904 stride
, item_size
, descr
,
905 xform
=None, quiet
=False,
907 if fbx_layer_mapping
== b
'ByEdge':
908 if fbx_layer_ref
== b
'Direct':
909 blen_read_geom_array_setattr(blen_read_geom_array_gen_direct(fbx_layer_data
, stride
),
910 blen_data
, blen_attr
, fbx_layer_data
, stride
, item_size
, descr
, xform
)
912 blen_read_geom_array_error_ref(descr
, fbx_layer_ref
, quiet
)
913 elif fbx_layer_mapping
== b
'AllSame':
914 if fbx_layer_ref
== b
'IndexToDirect':
915 assert(fbx_layer_index
is None)
916 blen_read_geom_array_setattr(blen_read_geom_array_gen_allsame(len(blen_data
)),
917 blen_data
, blen_attr
, fbx_layer_data
, stride
, item_size
, descr
, xform
)
919 blen_read_geom_array_error_ref(descr
, fbx_layer_ref
, quiet
)
921 blen_read_geom_array_error_mapping(descr
, fbx_layer_mapping
, quiet
)
926 def blen_read_geom_array_mapped_polygon(
927 mesh
, blen_data
, blen_attr
,
928 fbx_layer_data
, fbx_layer_index
,
929 fbx_layer_mapping
, fbx_layer_ref
,
930 stride
, item_size
, descr
,
931 xform
=None, quiet
=False,
933 if fbx_layer_mapping
== b
'ByPolygon':
934 if fbx_layer_ref
== b
'IndexToDirect':
935 # XXX Looks like we often get no fbx_layer_index in this case, shall not happen but happens...
936 # We fallback to 'Direct' mapping in this case.
937 #~ assert(fbx_layer_index is not None)
938 if fbx_layer_index
is None:
939 blen_read_geom_array_setattr(blen_read_geom_array_gen_direct(fbx_layer_data
, stride
),
940 blen_data
, blen_attr
, fbx_layer_data
, stride
, item_size
, descr
, xform
)
942 blen_read_geom_array_setattr(blen_read_geom_array_gen_indextodirect(fbx_layer_index
, stride
),
943 blen_data
, blen_attr
, fbx_layer_data
, stride
, item_size
, descr
, xform
)
945 elif fbx_layer_ref
== b
'Direct':
946 blen_read_geom_array_setattr(blen_read_geom_array_gen_direct(fbx_layer_data
, stride
),
947 blen_data
, blen_attr
, fbx_layer_data
, stride
, item_size
, descr
, xform
)
949 blen_read_geom_array_error_ref(descr
, fbx_layer_ref
, quiet
)
950 elif fbx_layer_mapping
== b
'AllSame':
951 if fbx_layer_ref
== b
'IndexToDirect':
952 assert(fbx_layer_index
is None)
953 blen_read_geom_array_setattr(blen_read_geom_array_gen_allsame(len(blen_data
)),
954 blen_data
, blen_attr
, fbx_layer_data
, stride
, item_size
, descr
, xform
)
956 blen_read_geom_array_error_ref(descr
, fbx_layer_ref
, quiet
)
958 blen_read_geom_array_error_mapping(descr
, fbx_layer_mapping
, quiet
)
963 def blen_read_geom_array_mapped_polyloop(
964 mesh
, blen_data
, blen_attr
,
965 fbx_layer_data
, fbx_layer_index
,
966 fbx_layer_mapping
, fbx_layer_ref
,
967 stride
, item_size
, descr
,
968 xform
=None, quiet
=False,
970 if fbx_layer_mapping
== b
'ByPolygonVertex':
971 if fbx_layer_ref
== b
'IndexToDirect':
972 # XXX Looks like we often get no fbx_layer_index in this case, shall not happen but happens...
973 # We fallback to 'Direct' mapping in this case.
974 #~ assert(fbx_layer_index is not None)
975 if fbx_layer_index
is None:
976 blen_read_geom_array_setattr(blen_read_geom_array_gen_direct(fbx_layer_data
, stride
),
977 blen_data
, blen_attr
, fbx_layer_data
, stride
, item_size
, descr
, xform
)
979 blen_read_geom_array_setattr(blen_read_geom_array_gen_indextodirect(fbx_layer_index
, stride
),
980 blen_data
, blen_attr
, fbx_layer_data
, stride
, item_size
, descr
, xform
)
982 elif fbx_layer_ref
== b
'Direct':
983 blen_read_geom_array_setattr(blen_read_geom_array_gen_direct(fbx_layer_data
, stride
),
984 blen_data
, blen_attr
, fbx_layer_data
, stride
, item_size
, descr
, xform
)
986 blen_read_geom_array_error_ref(descr
, fbx_layer_ref
, quiet
)
987 elif fbx_layer_mapping
== b
'ByVertice':
988 if fbx_layer_ref
== b
'Direct':
989 assert(fbx_layer_index
is None)
990 blen_read_geom_array_setattr(blen_read_geom_array_gen_direct_looptovert(mesh
, fbx_layer_data
, stride
),
991 blen_data
, blen_attr
, fbx_layer_data
, stride
, item_size
, descr
, xform
)
993 blen_read_geom_array_error_ref(descr
, fbx_layer_ref
, quiet
)
994 elif fbx_layer_mapping
== b
'AllSame':
995 if fbx_layer_ref
== b
'IndexToDirect':
996 assert(fbx_layer_index
is None)
997 blen_read_geom_array_setattr(blen_read_geom_array_gen_allsame(len(blen_data
)),
998 blen_data
, blen_attr
, fbx_layer_data
, stride
, item_size
, descr
, xform
)
1000 blen_read_geom_array_error_ref(descr
, fbx_layer_ref
, quiet
)
1002 blen_read_geom_array_error_mapping(descr
, fbx_layer_mapping
, quiet
)
1007 def blen_read_geom_layer_material(fbx_obj
, mesh
):
1008 fbx_layer
= elem_find_first(fbx_obj
, b
'LayerElementMaterial')
1010 if fbx_layer
is None:
1016 ) = blen_read_geom_layerinfo(fbx_layer
)
1018 layer_id
= b
'Materials'
1019 fbx_layer_data
= elem_prop_first(elem_find_first(fbx_layer
, layer_id
))
1021 blen_data
= mesh
.polygons
1022 blen_read_geom_array_mapped_polygon(
1023 mesh
, blen_data
, "material_index",
1024 fbx_layer_data
, None,
1025 fbx_layer_mapping
, fbx_layer_ref
,
1030 def blen_read_geom_layer_uv(fbx_obj
, mesh
):
1031 for layer_id
in (b
'LayerElementUV',):
1032 for fbx_layer
in elem_find_iter(fbx_obj
, layer_id
):
1033 # all should be valid
1037 ) = blen_read_geom_layerinfo(fbx_layer
)
1039 fbx_layer_data
= elem_prop_first(elem_find_first(fbx_layer
, b
'UV'))
1040 fbx_layer_index
= elem_prop_first(elem_find_first(fbx_layer
, b
'UVIndex'))
1042 # Always init our new layers with (0, 0) UVs.
1043 uv_lay
= mesh
.uv_layers
.new(name
=fbx_layer_name
, do_init
=False)
1045 print("Failed to add {%r %r} UVLayer to %r (probably too many of them?)"
1046 "" % (layer_id
, fbx_layer_name
, mesh
.name
))
1049 blen_data
= uv_lay
.data
1051 # some valid files omit this data
1052 if fbx_layer_data
is None:
1053 print("%r %r missing data" % (layer_id
, fbx_layer_name
))
1056 blen_read_geom_array_mapped_polyloop(
1057 mesh
, blen_data
, "uv",
1058 fbx_layer_data
, fbx_layer_index
,
1059 fbx_layer_mapping
, fbx_layer_ref
,
1064 def blen_read_geom_layer_color(fbx_obj
, mesh
, colors_type
):
1065 if colors_type
== 'NONE':
1067 use_srgb
= colors_type
== 'SRGB'
1068 layer_type
= 'BYTE_COLOR' if use_srgb
else 'FLOAT_COLOR'
1069 color_prop_name
= "color_srgb" if use_srgb
else "color"
1070 # almost same as UV's
1071 for layer_id
in (b
'LayerElementColor',):
1072 for fbx_layer
in elem_find_iter(fbx_obj
, layer_id
):
1073 # all should be valid
1077 ) = blen_read_geom_layerinfo(fbx_layer
)
1079 fbx_layer_data
= elem_prop_first(elem_find_first(fbx_layer
, b
'Colors'))
1080 fbx_layer_index
= elem_prop_first(elem_find_first(fbx_layer
, b
'ColorIndex'))
1082 color_lay
= mesh
.color_attributes
.new(name
=fbx_layer_name
, type=layer_type
, domain
='CORNER')
1084 if color_lay
is None:
1085 print("Failed to add {%r %r} vertex color layer to %r (probably too many of them?)"
1086 "" % (layer_id
, fbx_layer_name
, mesh
.name
))
1089 blen_data
= color_lay
.data
1091 # some valid files omit this data
1092 if fbx_layer_data
is None:
1093 print("%r %r missing data" % (layer_id
, fbx_layer_name
))
1096 blen_read_geom_array_mapped_polyloop(
1097 mesh
, blen_data
, color_prop_name
,
1098 fbx_layer_data
, fbx_layer_index
,
1099 fbx_layer_mapping
, fbx_layer_ref
,
1104 def blen_read_geom_layer_smooth(fbx_obj
, mesh
):
1105 fbx_layer
= elem_find_first(fbx_obj
, b
'LayerElementSmoothing')
1107 if fbx_layer
is None:
1110 # all should be valid
1114 ) = blen_read_geom_layerinfo(fbx_layer
)
1116 layer_id
= b
'Smoothing'
1117 fbx_layer_data
= elem_prop_first(elem_find_first(fbx_layer
, layer_id
))
1119 # udk has 'Direct' mapped, with no Smoothing, not sure why, but ignore these
1120 if fbx_layer_data
is None:
1123 if fbx_layer_mapping
== b
'ByEdge':
1124 # some models have bad edge data, we can't use this info...
1126 print("warning skipping sharp edges data, no valid edges...")
1129 blen_data
= mesh
.edges
1130 blen_read_geom_array_mapped_edge(
1131 mesh
, blen_data
, "use_edge_sharp",
1132 fbx_layer_data
, None,
1133 fbx_layer_mapping
, fbx_layer_ref
,
1135 xform
=lambda s
: not s
,
1137 # We only set sharp edges here, not face smoothing itself...
1138 mesh
.use_auto_smooth
= True
1140 elif fbx_layer_mapping
== b
'ByPolygon':
1141 blen_data
= mesh
.polygons
1142 return blen_read_geom_array_mapped_polygon(
1143 mesh
, blen_data
, "use_smooth",
1144 fbx_layer_data
, None,
1145 fbx_layer_mapping
, fbx_layer_ref
,
1147 xform
=lambda s
: (s
!= 0), # smoothgroup bitflags, treat as booleans for now
1150 print("warning layer %r mapping type unsupported: %r" % (fbx_layer
.id, fbx_layer_mapping
))
1153 def blen_read_geom_layer_edge_crease(fbx_obj
, mesh
):
1154 from math
import sqrt
1156 fbx_layer
= elem_find_first(fbx_obj
, b
'LayerElementEdgeCrease')
1158 if fbx_layer
is None:
1161 # all should be valid
1165 ) = blen_read_geom_layerinfo(fbx_layer
)
1167 if fbx_layer_mapping
!= b
'ByEdge':
1170 layer_id
= b
'EdgeCrease'
1171 fbx_layer_data
= elem_prop_first(elem_find_first(fbx_layer
, layer_id
))
1173 # some models have bad edge data, we can't use this info...
1175 print("warning skipping edge crease data, no valid edges...")
1178 if fbx_layer_mapping
== b
'ByEdge':
1179 # some models have bad edge data, we can't use this info...
1181 print("warning skipping edge crease data, no valid edges...")
1184 blen_data
= mesh
.edges
1185 return blen_read_geom_array_mapped_edge(
1186 mesh
, blen_data
, "crease",
1187 fbx_layer_data
, None,
1188 fbx_layer_mapping
, fbx_layer_ref
,
1190 # Blender squares those values before sending them to OpenSubdiv, when other software don't,
1191 # so we need to compensate that to get similar results through FBX...
1195 print("warning layer %r mapping type unsupported: %r" % (fbx_layer
.id, fbx_layer_mapping
))
1198 def blen_read_geom_layer_normal(fbx_obj
, mesh
, xform
=None):
1199 fbx_layer
= elem_find_first(fbx_obj
, b
'LayerElementNormal')
1201 if fbx_layer
is None:
1207 ) = blen_read_geom_layerinfo(fbx_layer
)
1209 layer_id
= b
'Normals'
1210 fbx_layer_data
= elem_prop_first(elem_find_first(fbx_layer
, layer_id
))
1211 fbx_layer_index
= elem_prop_first(elem_find_first(fbx_layer
, b
'NormalsIndex'))
1213 if fbx_layer_data
is None:
1214 print("warning %r %r missing data" % (layer_id
, fbx_layer_name
))
1217 # try loops, then vertices.
1218 tries
= ((mesh
.loops
, "Loops", False, blen_read_geom_array_mapped_polyloop
),
1219 (mesh
.polygons
, "Polygons", True, blen_read_geom_array_mapped_polygon
),
1220 (mesh
.vertices
, "Vertices", True, blen_read_geom_array_mapped_vert
))
1221 for blen_data
, blen_data_type
, is_fake
, func
in tries
:
1222 bdata
= [None] * len(blen_data
) if is_fake
else blen_data
1223 if func(mesh
, bdata
, "normal",
1224 fbx_layer_data
, fbx_layer_index
, fbx_layer_mapping
, fbx_layer_ref
, 3, 3, layer_id
, xform
, True):
1225 if blen_data_type
== "Polygons":
1226 for pidx
, p
in enumerate(mesh
.polygons
):
1227 for lidx
in range(p
.loop_start
, p
.loop_start
+ p
.loop_total
):
1228 mesh
.loops
[lidx
].normal
[:] = bdata
[pidx
]
1229 elif blen_data_type
== "Vertices":
1230 # We have to copy vnors to lnors! Far from elegant, but simple.
1231 for l
in mesh
.loops
:
1232 l
.normal
[:] = bdata
[l
.vertex_index
]
1235 blen_read_geom_array_error_mapping("normal", fbx_layer_mapping
)
1236 blen_read_geom_array_error_ref("normal", fbx_layer_ref
)
1240 def blen_read_geom(fbx_tmpl
, fbx_obj
, settings
):
1241 from itertools
import chain
1244 # Vertices are in object space, but we are post-multiplying all transforms with the inverse of the
1245 # global matrix, so we need to apply the global matrix to the vertices to get the correct result.
1246 geom_mat_co
= settings
.global_matrix
if settings
.bake_space_transform
else None
1247 # We need to apply the inverse transpose of the global matrix when transforming normals.
1248 geom_mat_no
= Matrix(settings
.global_matrix_inv_transposed
) if settings
.bake_space_transform
else None
1249 if geom_mat_no
is not None:
1250 # Remove translation & scaling!
1251 geom_mat_no
.translation
= Vector()
1252 geom_mat_no
.normalize()
1254 # TODO, use 'fbx_tmpl'
1255 elem_name_utf8
= elem_name_ensure_class(fbx_obj
, b
'Geometry')
1257 fbx_verts
= elem_prop_first(elem_find_first(fbx_obj
, b
'Vertices'))
1258 fbx_polys
= elem_prop_first(elem_find_first(fbx_obj
, b
'PolygonVertexIndex'))
1259 fbx_edges
= elem_prop_first(elem_find_first(fbx_obj
, b
'Edges'))
1261 if geom_mat_co
is not None:
1262 def _vcos_transformed_gen(raw_cos
, m
=None):
1263 # Note: we could most likely get much better performances with numpy, but will leave this as TODO for now.
1264 return chain(*(m
@ Vector(v
) for v
in zip(*(iter(raw_cos
),) * 3)))
1265 fbx_verts
= array
.array(fbx_verts
.typecode
, _vcos_transformed_gen(fbx_verts
, geom_mat_co
))
1267 if fbx_verts
is None:
1269 if fbx_polys
is None:
1272 mesh
= bpy
.data
.meshes
.new(name
=elem_name_utf8
)
1273 mesh
.vertices
.add(len(fbx_verts
) // 3)
1274 mesh
.vertices
.foreach_set("co", fbx_verts
)
1277 mesh
.loops
.add(len(fbx_polys
))
1278 poly_loop_starts
= []
1279 poly_loop_totals
= []
1281 for i
, l
in enumerate(mesh
.loops
):
1282 index
= fbx_polys
[i
]
1284 poly_loop_starts
.append(poly_loop_prev
)
1285 poly_loop_totals
.append((i
- poly_loop_prev
) + 1)
1286 poly_loop_prev
= i
+ 1
1288 l
.vertex_index
= index
1290 mesh
.polygons
.add(len(poly_loop_starts
))
1291 mesh
.polygons
.foreach_set("loop_start", poly_loop_starts
)
1292 mesh
.polygons
.foreach_set("loop_total", poly_loop_totals
)
1294 blen_read_geom_layer_material(fbx_obj
, mesh
)
1295 blen_read_geom_layer_uv(fbx_obj
, mesh
)
1296 blen_read_geom_layer_color(fbx_obj
, mesh
, settings
.colors_type
)
1299 # edges in fact index the polygons (NOT the vertices)
1301 tot_edges
= len(fbx_edges
)
1302 edges_conv
= array
.array('i', [0]) * (tot_edges
* 2)
1308 e_b
= fbx_polys
[i
+ 1]
1312 # Last index of polygon, wrap back to the start.
1314 # ideally we wouldn't have to search back,
1315 # but it should only be 2-3 iterations.
1317 while j
>= 0 and fbx_polys
[j
] >= 0:
1320 e_b
= fbx_polys
[j
+ 1]
1322 edges_conv
[edge_index
] = e_a
1323 edges_conv
[edge_index
+ 1] = e_b
1326 mesh
.edges
.add(tot_edges
)
1327 mesh
.edges
.foreach_set("vertices", edges_conv
)
1329 # must be after edge, face loading.
1330 ok_smooth
= blen_read_geom_layer_smooth(fbx_obj
, mesh
)
1332 ok_crease
= blen_read_geom_layer_edge_crease(fbx_obj
, mesh
)
1335 if settings
.use_custom_normals
:
1336 # Note: we store 'temp' normals in loops, since validate() may alter final mesh,
1337 # we can only set custom lnors *after* calling it.
1338 mesh
.create_normals_split()
1339 if geom_mat_no
is None:
1340 ok_normals
= blen_read_geom_layer_normal(fbx_obj
, mesh
)
1343 return geom_mat_no
@ Vector(v
)
1344 ok_normals
= blen_read_geom_layer_normal(fbx_obj
, mesh
, nortrans
)
1346 mesh
.validate(clean_customdata
=False) # *Very* important to not remove lnors here!
1349 clnors
= array
.array('f', [0.0] * (len(mesh
.loops
) * 3))
1350 mesh
.loops
.foreach_get("normal", clnors
)
1353 mesh
.polygons
.foreach_set("use_smooth", [True] * len(mesh
.polygons
))
1356 mesh
.normals_split_custom_set(tuple(zip(*(iter(clnors
),) * 3)))
1357 mesh
.use_auto_smooth
= True
1361 if settings
.use_custom_normals
:
1362 mesh
.free_normals_split()
1365 mesh
.polygons
.foreach_set("use_smooth", [True] * len(mesh
.polygons
))
1368 mesh
.use_customdata_edge_crease
= True
1370 if settings
.use_custom_props
:
1371 blen_read_custom_properties(fbx_obj
, mesh
, settings
)
1376 def blen_read_shape(fbx_tmpl
, fbx_sdata
, fbx_bcdata
, meshes
, scene
):
1377 elem_name_utf8
= elem_name_ensure_class(fbx_sdata
, b
'Geometry')
1378 indices
= elem_prop_first(elem_find_first(fbx_sdata
, b
'Indexes'), default
=())
1379 dvcos
= tuple(co
for co
in zip(*[iter(elem_prop_first(elem_find_first(fbx_sdata
, b
'Vertices'), default
=()))] * 3))
1380 # We completely ignore normals here!
1381 weight
= elem_prop_first(elem_find_first(fbx_bcdata
, b
'DeformPercent'), default
=100.0) / 100.0
1382 vgweights
= tuple(vgw
/ 100.0 for vgw
in elem_prop_first(elem_find_first(fbx_bcdata
, b
'FullWeights'), default
=()))
1384 # Special case, in case all weights are the same, FullWeight can have only one element - *sigh!*
1385 nbr_indices
= len(indices
)
1386 if len(vgweights
) == 1 and nbr_indices
> 1:
1387 vgweights
= (vgweights
[0],) * nbr_indices
1389 assert(len(vgweights
) == nbr_indices
== len(dvcos
))
1390 create_vg
= bool(set(vgweights
) - {1.0})
1394 for me
, objects
in meshes
:
1395 vcos
= tuple((idx
, me
.vertices
[idx
].co
+ Vector(dvco
)) for idx
, dvco
in zip(indices
, dvcos
))
1396 objects
= list({node
.bl_obj
for node
in objects
})
1399 if me
.shape_keys
is None:
1400 objects
[0].shape_key_add(name
="Basis", from_mix
=False)
1401 kb
= objects
[0].shape_key_add(name
=elem_name_utf8
, from_mix
=False)
1402 me
.shape_keys
.use_relative
= True # Should already be set as such.
1404 for idx
, co
in vcos
:
1405 kb
.data
[idx
].co
[:] = co
1408 # Add vgroup if necessary.
1410 vgoups
= add_vgroup_to_objects(indices
, vgweights
, kb
.name
, objects
)
1411 kb
.vertex_group
= kb
.name
1413 keyblocks
.append(kb
)
1421 def blen_read_material(fbx_tmpl
, fbx_obj
, settings
):
1422 from bpy_extras
import node_shader_utils
1423 from math
import sqrt
1425 elem_name_utf8
= elem_name_ensure_class(fbx_obj
, b
'Material')
1427 nodal_material_wrap_map
= settings
.nodal_material_wrap_map
1428 ma
= bpy
.data
.materials
.new(name
=elem_name_utf8
)
1430 const_color_white
= 1.0, 1.0, 1.0
1431 const_color_black
= 0.0, 0.0, 0.0
1433 fbx_props
= (elem_find_first(fbx_obj
, b
'Properties70'),
1434 elem_find_first(fbx_tmpl
, b
'Properties70', fbx_elem_nil
))
1435 fbx_props_no_template
= (fbx_props
[0], fbx_elem_nil
)
1437 ma_wrap
= node_shader_utils
.PrincipledBSDFWrapper(ma
, is_readonly
=False, use_nodes
=True)
1438 ma_wrap
.base_color
= elem_props_get_color_rgb(fbx_props
, b
'DiffuseColor', const_color_white
)
1439 # No specular color in Principled BSDF shader, assumed to be either white or take some tint from diffuse one...
1440 # TODO: add way to handle tint option (guesstimate from spec color + intensity...)?
1441 ma_wrap
.specular
= elem_props_get_number(fbx_props
, b
'SpecularFactor', 0.25) * 2.0
1442 # XXX Totally empirical conversion, trying to adapt it (and protect against invalid negative values, see T96076):
1443 # From [1.0 - 0.0] Principled BSDF range to [0.0 - 100.0] FBX shininess range)...
1444 fbx_shininess
= max(elem_props_get_number(fbx_props
, b
'Shininess', 20.0), 0.0)
1445 ma_wrap
.roughness
= 1.0 - (sqrt(fbx_shininess
) / 10.0)
1446 # Sweetness... Looks like we are not the only ones to not know exactly how FBX is supposed to work (see T59850).
1447 # According to one of its developers, Unity uses that formula to extract alpha value:
1449 # alpha = 1 - TransparencyFactor
1450 # if (alpha == 1 or alpha == 0):
1451 # alpha = 1 - TransparentColor.r
1453 # Until further info, let's assume this is correct way to do, hence the following code for TransparentColor.
1454 # However, there are some cases (from 3DSMax, see T65065), where we do have TransparencyFactor only defined
1455 # in the template to 0.0, and then materials defining TransparentColor to pure white (1.0, 1.0, 1.0),
1456 # and setting alpha value in Opacity... try to cope with that too. :((((
1457 alpha
= 1.0 - elem_props_get_number(fbx_props
, b
'TransparencyFactor', 0.0)
1458 if (alpha
== 1.0 or alpha
== 0.0):
1459 alpha
= elem_props_get_number(fbx_props_no_template
, b
'Opacity', None)
1461 alpha
= 1.0 - elem_props_get_color_rgb(fbx_props
, b
'TransparentColor', const_color_black
)[0]
1462 ma_wrap
.alpha
= alpha
1463 ma_wrap
.metallic
= elem_props_get_number(fbx_props
, b
'ReflectionFactor', 0.0)
1464 # We have no metallic (a.k.a. reflection) color...
1465 # elem_props_get_color_rgb(fbx_props, b'ReflectionColor', const_color_white)
1466 ma_wrap
.normalmap_strength
= elem_props_get_number(fbx_props
, b
'BumpFactor', 1.0)
1467 # Emission strength and color
1468 ma_wrap
.emission_strength
= elem_props_get_number(fbx_props
, b
'EmissiveFactor', 1.0)
1469 ma_wrap
.emission_color
= elem_props_get_color_rgb(fbx_props
, b
'EmissiveColor', const_color_black
)
1471 nodal_material_wrap_map
[ma
] = ma_wrap
1473 if settings
.use_custom_props
:
1474 blen_read_custom_properties(fbx_obj
, ma
, settings
)
1482 def blen_read_texture_image(fbx_tmpl
, fbx_obj
, basedir
, settings
):
1484 from bpy_extras
import image_utils
1486 def pack_data_from_content(image
, fbx_obj
):
1487 data
= elem_find_first_bytes(fbx_obj
, b
'Content')
1489 data_len
= len(data
)
1491 image
.pack(data
=data
, data_len
=data_len
)
1493 elem_name_utf8
= elem_name_ensure_classes(fbx_obj
, {b
'Texture', b
'Video'})
1495 image_cache
= settings
.image_cache
1497 # Yet another beautiful logic demonstration by Master FBX:
1498 # * RelativeFilename in both Video and Texture nodes.
1499 # * FileName in texture nodes.
1500 # * Filename in video nodes.
1501 # Aaaaaaaarrrrrrrrgggggggggggg!!!!!!!!!!!!!!
1502 filepath
= elem_find_first_string(fbx_obj
, b
'RelativeFilename')
1504 # Make sure we do handle a relative path, and not an absolute one (see D5143).
1505 filepath
= filepath
.lstrip(os
.path
.sep
).lstrip(os
.path
.altsep
)
1506 filepath
= os
.path
.join(basedir
, filepath
)
1508 filepath
= elem_find_first_string(fbx_obj
, b
'FileName')
1510 filepath
= elem_find_first_string(fbx_obj
, b
'Filename')
1512 print("Error, could not find any file path in ", fbx_obj
)
1513 print(" Falling back to: ", elem_name_utf8
)
1514 filepath
= elem_name_utf8
1516 filepath
= filepath
.replace('\\', '/') if (os
.sep
== '/') else filepath
.replace('/', '\\')
1518 image
= image_cache
.get(filepath
)
1519 if image
is not None:
1520 # Data is only embedded once, we may have already created the image but still be missing its data!
1521 if not image
.has_data
:
1522 pack_data_from_content(image
, fbx_obj
)
1525 image
= image_utils
.load_image(
1529 recursive
=settings
.use_image_search
,
1532 # Try to use embedded data, if available!
1533 pack_data_from_content(image
, fbx_obj
)
1535 image_cache
[filepath
] = image
1536 # name can be ../a/b/c
1537 image
.name
= os
.path
.basename(elem_name_utf8
)
1539 if settings
.use_custom_props
:
1540 blen_read_custom_properties(fbx_obj
, image
, settings
)
1545 def blen_read_camera(fbx_tmpl
, fbx_obj
, global_scale
):
1549 elem_name_utf8
= elem_name_ensure_class(fbx_obj
, b
'NodeAttribute')
1551 fbx_props
= (elem_find_first(fbx_obj
, b
'Properties70'),
1552 elem_find_first(fbx_tmpl
, b
'Properties70', fbx_elem_nil
))
1554 camera
= bpy
.data
.cameras
.new(name
=elem_name_utf8
)
1556 camera
.type = 'ORTHO' if elem_props_get_enum(fbx_props
, b
'CameraProjectionType', 0) == 1 else 'PERSP'
1558 camera
.dof
.focus_distance
= elem_props_get_number(fbx_props
, b
'FocusDistance', 10 * 1000) / 1000 * global_scale
1559 if (elem_props_get_bool(fbx_props
, b
'UseDepthOfField', False)):
1560 camera
.dof
.use_dof
= True
1562 camera
.lens
= elem_props_get_number(fbx_props
, b
'FocalLength', 35.0)
1563 camera
.sensor_width
= elem_props_get_number(fbx_props
, b
'FilmWidth', 32.0 * M2I
) / M2I
1564 camera
.sensor_height
= elem_props_get_number(fbx_props
, b
'FilmHeight', 32.0 * M2I
) / M2I
1566 camera
.ortho_scale
= elem_props_get_number(fbx_props
, b
'OrthoZoom', 1.0)
1568 filmaspect
= camera
.sensor_width
/ camera
.sensor_height
1570 camera
.shift_x
= elem_props_get_number(fbx_props
, b
'FilmOffsetX', 0.0) / (M2I
* camera
.sensor_width
)
1571 camera
.shift_y
= elem_props_get_number(fbx_props
, b
'FilmOffsetY', 0.0) / (M2I
* camera
.sensor_height
* filmaspect
)
1573 camera
.clip_start
= elem_props_get_number(fbx_props
, b
'NearPlane', 0.01) * global_scale
1574 camera
.clip_end
= elem_props_get_number(fbx_props
, b
'FarPlane', 100.0) * global_scale
1579 def blen_read_light(fbx_tmpl
, fbx_obj
, global_scale
):
1581 elem_name_utf8
= elem_name_ensure_class(fbx_obj
, b
'NodeAttribute')
1583 fbx_props
= (elem_find_first(fbx_obj
, b
'Properties70'),
1584 elem_find_first(fbx_tmpl
, b
'Properties70', fbx_elem_nil
))
1589 2: 'SPOT'}.get(elem_props_get_enum(fbx_props
, b
'LightType', 0), 'POINT')
1591 lamp
= bpy
.data
.lights
.new(name
=elem_name_utf8
, type=light_type
)
1593 if light_type
== 'SPOT':
1594 spot_size
= elem_props_get_number(fbx_props
, b
'OuterAngle', None)
1595 if spot_size
is None:
1597 spot_size
= elem_props_get_number(fbx_props
, b
'Cone angle', 45.0)
1598 lamp
.spot_size
= math
.radians(spot_size
)
1600 spot_blend
= elem_props_get_number(fbx_props
, b
'InnerAngle', None)
1601 if spot_blend
is None:
1603 spot_blend
= elem_props_get_number(fbx_props
, b
'HotSpot', 45.0)
1604 lamp
.spot_blend
= 1.0 - (spot_blend
/ spot_size
)
1606 # TODO, cycles nodes???
1607 lamp
.color
= elem_props_get_color_rgb(fbx_props
, b
'Color', (1.0, 1.0, 1.0))
1608 lamp
.energy
= elem_props_get_number(fbx_props
, b
'Intensity', 100.0) / 100.0
1609 lamp
.distance
= elem_props_get_number(fbx_props
, b
'DecayStart', 25.0) * global_scale
1610 lamp
.use_shadow
= elem_props_get_bool(fbx_props
, b
'CastShadow', True)
1611 if hasattr(lamp
, "cycles"):
1612 lamp
.cycles
.cast_shadow
= lamp
.use_shadow
1613 # Keeping this for now, but this is not used nor exposed anymore afaik...
1614 lamp
.shadow_color
= elem_props_get_color_rgb(fbx_props
, b
'ShadowColor', (0.0, 0.0, 0.0))
1619 # ### Import Utility class
1620 class FbxImportHelperNode
:
1622 Temporary helper node to store a hierarchy of fbxNode objects before building Objects, Armatures and Bones.
1623 It tries to keep the correction data in one place so it can be applied consistently to the imported data.
1627 '_parent', 'anim_compensation_matrix', 'is_global_animation', 'armature_setup', 'armature', 'bind_matrix',
1628 'bl_bone', 'bl_data', 'bl_obj', 'bone_child_matrix', 'children', 'clusters',
1629 'fbx_elem', 'fbx_name', 'fbx_transform_data', 'fbx_type',
1630 'is_armature', 'has_bone_children', 'is_bone', 'is_root', 'is_leaf',
1631 'matrix', 'matrix_as_parent', 'matrix_geom', 'meshes', 'post_matrix', 'pre_matrix')
1633 def __init__(self
, fbx_elem
, bl_data
, fbx_transform_data
, is_bone
):
1634 self
.fbx_name
= elem_name_ensure_class(fbx_elem
, b
'Model') if fbx_elem
else 'Unknown'
1635 self
.fbx_type
= fbx_elem
.props
[2] if fbx_elem
else None
1636 self
.fbx_elem
= fbx_elem
1638 self
.bl_data
= bl_data
1639 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!)
1640 self
.fbx_transform_data
= fbx_transform_data
1641 self
.is_root
= False
1642 self
.is_bone
= is_bone
1643 self
.is_armature
= False
1644 self
.armature
= None # For bones only, relevant armature node.
1645 self
.has_bone_children
= False # True if the hierarchy below this node contains bones, important to support mixed hierarchies.
1646 self
.is_leaf
= False # True for leaf-bones added to the end of some bone chains to set the lengths.
1647 self
.pre_matrix
= None # correction matrix that needs to be applied before the FBX transform
1648 self
.bind_matrix
= None # for bones this is the matrix used to bind to the skin
1649 if fbx_transform_data
:
1650 self
.matrix
, self
.matrix_as_parent
, self
.matrix_geom
= blen_read_object_transform_do(fbx_transform_data
)
1652 self
.matrix
, self
.matrix_as_parent
, self
.matrix_geom
= (None, None, None)
1653 self
.post_matrix
= None # correction matrix that needs to be applied after the FBX transform
1654 self
.bone_child_matrix
= None # Objects attached to a bone end not the beginning, this matrix corrects for that
1656 # XXX Those two are to handle the fact that rigged meshes are not linked to their armature in FBX, which implies
1657 # that their animation is in global space (afaik...).
1658 # This is actually not really solvable currently, since anim_compensation_matrix is not valid if armature
1659 # itself is animated (we'd have to recompute global-to-local anim_compensation_matrix for each frame,
1660 # and for each armature action... beyond being an insane work).
1661 # Solution for now: do not read rigged meshes animations at all! sic...
1662 self
.anim_compensation_matrix
= None # a mesh moved in the hierarchy may have a different local matrix. This compensates animations for this.
1663 self
.is_global_animation
= False
1665 self
.meshes
= None # List of meshes influenced by this bone.
1666 self
.clusters
= [] # Deformer Cluster nodes
1667 self
.armature_setup
= {} # mesh and armature matrix when the mesh was bound
1677 def parent(self
, value
):
1678 if self
._parent
is not None:
1679 self
._parent
.children
.remove(self
)
1680 self
._parent
= value
1681 if self
._parent
is not None:
1682 self
._parent
.children
.append(self
)
1686 # Separating leaf status from ignore status itself.
1687 # Currently they are equivalent, but this may change in future.
1692 return self
.fbx_elem
.props
[1].decode()
1696 def print_info(self
, indent
=0):
1697 print(" " * indent
+ (self
.fbx_name
if self
.fbx_name
else "(Null)")
1698 + ("[root]" if self
.is_root
else "")
1699 + ("[leaf]" if self
.is_leaf
else "")
1700 + ("[ignore]" if self
.ignore
else "")
1701 + ("[armature]" if self
.is_armature
else "")
1702 + ("[bone]" if self
.is_bone
else "")
1703 + ("[HBC]" if self
.has_bone_children
else "")
1705 for c
in self
.children
:
1706 c
.print_info(indent
+ 1)
1708 def mark_leaf_bones(self
):
1709 if self
.is_bone
and len(self
.children
) == 1:
1710 child
= self
.children
[0]
1711 if child
.is_bone
and len(child
.children
) == 0:
1712 child
.is_leaf
= True
1713 for child
in self
.children
:
1714 child
.mark_leaf_bones()
1716 def do_bake_transform(self
, settings
):
1717 return (settings
.bake_space_transform
and self
.fbx_type
in (b
'Mesh', b
'Null') and
1718 not self
.is_armature
and not self
.is_bone
)
1720 def find_correction_matrix(self
, settings
, parent_correction_inv
=None):
1721 from bpy_extras
.io_utils
import axis_conversion
1723 if self
.parent
and (self
.parent
.is_root
or self
.parent
.do_bake_transform(settings
)):
1724 self
.pre_matrix
= settings
.global_matrix
1726 if parent_correction_inv
:
1727 self
.pre_matrix
= parent_correction_inv
@ (self
.pre_matrix
if self
.pre_matrix
else Matrix())
1729 correction_matrix
= None
1732 if settings
.automatic_bone_orientation
:
1733 # find best orientation to align bone with
1734 bone_children
= tuple(child
for child
in self
.children
if child
.is_bone
)
1735 if len(bone_children
) == 0:
1736 # no children, inherit the correction from parent (if possible)
1737 if self
.parent
and self
.parent
.is_bone
:
1738 correction_matrix
= parent_correction_inv
.inverted() if parent_correction_inv
else None
1740 # else find how best to rotate the bone to align the Y axis with the children
1741 best_axis
= (1, 0, 0)
1742 if len(bone_children
) == 1:
1743 vec
= bone_children
[0].get_bind_matrix().to_translation()
1744 best_axis
= Vector((0, 0, 1 if vec
[2] >= 0 else -1))
1745 if abs(vec
[0]) > abs(vec
[1]):
1746 if abs(vec
[0]) > abs(vec
[2]):
1747 best_axis
= Vector((1 if vec
[0] >= 0 else -1, 0, 0))
1748 elif abs(vec
[1]) > abs(vec
[2]):
1749 best_axis
= Vector((0, 1 if vec
[1] >= 0 else -1, 0))
1751 # get the child directions once because they may be checked several times
1752 child_locs
= (child
.get_bind_matrix().to_translation() for child
in bone_children
)
1753 child_locs
= tuple(loc
.normalized() for loc
in child_locs
if loc
.magnitude
> 0.0)
1755 # I'm not sure which one I like better...
1760 s
= -1 if i
% 2 == 1 else 1
1761 test_axis
= Vector((s
if a
== 0 else 0, s
if a
== 1 else 0, s
if a
== 2 else 0))
1763 # find max angle to children
1765 for loc
in child_locs
:
1766 max_angle
= min(max_angle
, test_axis
.dot(loc
))
1768 # is it better than the last one?
1769 if best_angle
< max_angle
:
1770 best_angle
= max_angle
1771 best_axis
= test_axis
1774 for vec
in child_locs
:
1775 test_axis
= Vector((0, 0, 1 if vec
[2] >= 0 else -1))
1776 if abs(vec
[0]) > abs(vec
[1]):
1777 if abs(vec
[0]) > abs(vec
[2]):
1778 test_axis
= Vector((1 if vec
[0] >= 0 else -1, 0, 0))
1779 elif abs(vec
[1]) > abs(vec
[2]):
1780 test_axis
= Vector((0, 1 if vec
[1] >= 0 else -1, 0))
1782 # find max angle to children
1784 for loc
in child_locs
:
1785 max_angle
= min(max_angle
, test_axis
.dot(loc
))
1787 # is it better than the last one?
1788 if best_angle
< max_angle
:
1789 best_angle
= max_angle
1790 best_axis
= test_axis
1792 # convert best_axis to axis string
1793 to_up
= 'Z' if best_axis
[2] >= 0 else '-Z'
1794 if abs(best_axis
[0]) > abs(best_axis
[1]):
1795 if abs(best_axis
[0]) > abs(best_axis
[2]):
1796 to_up
= 'X' if best_axis
[0] >= 0 else '-X'
1797 elif abs(best_axis
[1]) > abs(best_axis
[2]):
1798 to_up
= 'Y' if best_axis
[1] >= 0 else '-Y'
1799 to_forward
= 'X' if to_up
not in {'X', '-X'} else 'Y'
1801 # Build correction matrix
1802 if (to_up
, to_forward
) != ('Y', 'X'):
1803 correction_matrix
= axis_conversion(from_forward
='X',
1805 to_forward
=to_forward
,
1809 correction_matrix
= settings
.bone_correction_matrix
1811 # camera and light can be hard wired
1812 if self
.fbx_type
== b
'Camera':
1813 correction_matrix
= MAT_CONVERT_CAMERA
1814 elif self
.fbx_type
== b
'Light':
1815 correction_matrix
= MAT_CONVERT_LIGHT
1817 self
.post_matrix
= correction_matrix
1819 if self
.do_bake_transform(settings
):
1820 self
.post_matrix
= settings
.global_matrix_inv
@ (self
.post_matrix
if self
.post_matrix
else Matrix())
1823 correction_matrix_inv
= correction_matrix
.inverted_safe() if correction_matrix
else None
1824 for child
in self
.children
:
1825 child
.find_correction_matrix(settings
, correction_matrix_inv
)
1827 def find_armature_bones(self
, armature
):
1828 for child
in self
.children
:
1830 child
.armature
= armature
1831 child
.find_armature_bones(armature
)
1833 def find_armatures(self
):
1834 needs_armature
= False
1835 for child
in self
.children
:
1837 needs_armature
= True
1840 if self
.fbx_type
in {b
'Null', b
'Root'}:
1841 # if empty then convert into armature
1842 self
.is_armature
= True
1845 # otherwise insert a new node
1846 # XXX Maybe in case self is virtual FBX root node, we should instead add one armature per bone child?
1847 armature
= FbxImportHelperNode(None, None, None, False)
1848 armature
.fbx_name
= "Armature"
1849 armature
.is_armature
= True
1851 for child
in tuple(self
.children
):
1853 child
.parent
= armature
1855 armature
.parent
= self
1857 armature
.find_armature_bones(armature
)
1859 for child
in self
.children
:
1860 if child
.is_armature
or child
.is_bone
:
1862 child
.find_armatures()
1864 def find_bone_children(self
):
1865 has_bone_children
= False
1866 for child
in self
.children
:
1867 has_bone_children |
= child
.find_bone_children()
1868 self
.has_bone_children
= has_bone_children
1869 return self
.is_bone
or has_bone_children
1871 def find_fake_bones(self
, in_armature
=False):
1872 if in_armature
and not self
.is_bone
and self
.has_bone_children
:
1874 # if we are not a null node we need an intermediate node for the data
1875 if self
.fbx_type
not in {b
'Null', b
'Root'}:
1876 node
= FbxImportHelperNode(self
.fbx_elem
, self
.bl_data
, None, False)
1877 self
.fbx_elem
= None
1881 for child
in self
.children
:
1882 if child
.is_bone
or child
.has_bone_children
:
1889 if self
.is_armature
:
1891 for child
in self
.children
:
1892 child
.find_fake_bones(in_armature
)
1894 def get_world_matrix_as_parent(self
):
1895 matrix
= self
.parent
.get_world_matrix_as_parent() if self
.parent
else Matrix()
1896 if self
.matrix_as_parent
:
1897 matrix
= matrix
@ self
.matrix_as_parent
1900 def get_world_matrix(self
):
1901 matrix
= self
.parent
.get_world_matrix_as_parent() if self
.parent
else Matrix()
1903 matrix
= matrix
@ self
.matrix
1906 def get_matrix(self
):
1907 matrix
= self
.matrix
if self
.matrix
else Matrix()
1909 matrix
= self
.pre_matrix
@ matrix
1910 if self
.post_matrix
:
1911 matrix
= matrix
@ self
.post_matrix
1914 def get_bind_matrix(self
):
1915 matrix
= self
.bind_matrix
if self
.bind_matrix
else Matrix()
1917 matrix
= self
.pre_matrix
@ matrix
1918 if self
.post_matrix
:
1919 matrix
= matrix
@ self
.post_matrix
1922 def make_bind_pose_local(self
, parent_matrix
=None):
1923 if parent_matrix
is None:
1924 parent_matrix
= Matrix()
1926 if self
.bind_matrix
:
1927 bind_matrix
= parent_matrix
.inverted_safe() @ self
.bind_matrix
1929 bind_matrix
= self
.matrix
.copy() if self
.matrix
else None
1931 self
.bind_matrix
= bind_matrix
1933 parent_matrix
= parent_matrix
@ bind_matrix
1935 for child
in self
.children
:
1936 child
.make_bind_pose_local(parent_matrix
)
1938 def collect_skeleton_meshes(self
, meshes
):
1939 for _
, m
in self
.clusters
:
1941 for child
in self
.children
:
1942 if not child
.meshes
:
1943 child
.collect_skeleton_meshes(meshes
)
1945 def collect_armature_meshes(self
):
1946 if self
.is_armature
:
1947 armature_matrix_inv
= self
.get_world_matrix().inverted_safe()
1950 for child
in self
.children
:
1951 # Children meshes may be linked to children armatures, in which case we do not want to link them
1952 # to a parent one. See T70244.
1953 child
.collect_armature_meshes()
1954 if not child
.meshes
:
1955 child
.collect_skeleton_meshes(meshes
)
1957 old_matrix
= m
.matrix
1958 m
.matrix
= armature_matrix_inv
@ m
.get_world_matrix()
1959 m
.anim_compensation_matrix
= old_matrix
.inverted_safe() @ m
.matrix
1960 m
.is_global_animation
= True
1962 self
.meshes
= meshes
1964 for child
in self
.children
:
1965 child
.collect_armature_meshes()
1967 def build_skeleton(self
, arm
, parent_matrix
, parent_bone_size
=1, force_connect_children
=False):
1968 def child_connect(par_bone
, child_bone
, child_head
, connect_ctx
):
1969 # child_bone or child_head may be None.
1970 force_connect_children
, connected
= connect_ctx
1971 if child_bone
is not None:
1972 child_bone
.parent
= par_bone
1973 child_head
= child_bone
.head
1975 if similar_values_iter(par_bone
.tail
, child_head
):
1976 if child_bone
is not None:
1977 child_bone
.use_connect
= True
1978 # Disallow any force-connection at this level from now on, since that child was 'really'
1979 # connected, we do not want to move current bone's tail anymore!
1981 elif force_connect_children
and connected
is not None:
1982 # We only store position where tail of par_bone should be in the end.
1983 # Actual tail moving and force connection of compatible child bones will happen
1984 # once all have been checked.
1985 if connected
is ...:
1986 connected
= ([child_head
.copy(), 1], [child_bone
] if child_bone
is not None else [])
1988 connected
[0][0] += child_head
1989 connected
[0][1] += 1
1990 if child_bone
is not None:
1991 connected
[1].append(child_bone
)
1992 connect_ctx
[1] = connected
1994 def child_connect_finalize(par_bone
, connect_ctx
):
1995 force_connect_children
, connected
= connect_ctx
1996 # Do nothing if force connection is not enabled!
1997 if force_connect_children
and connected
is not None and connected
is not ...:
1998 # Here again we have to be wary about zero-length bones!!!
1999 par_tail
= connected
[0][0] / connected
[0][1]
2000 if (par_tail
- par_bone
.head
).magnitude
< 1e-2:
2001 par_bone_vec
= (par_bone
.tail
- par_bone
.head
).normalized()
2002 par_tail
= par_bone
.head
+ par_bone_vec
* 0.01
2003 par_bone
.tail
= par_tail
2004 for child_bone
in connected
[1]:
2005 if similar_values_iter(par_tail
, child_bone
.head
):
2006 child_bone
.use_connect
= True
2008 # Create the (edit)bone.
2009 bone
= arm
.bl_data
.edit_bones
.new(name
=self
.fbx_name
)
2011 self
.bl_obj
= arm
.bl_obj
2012 self
.bl_data
= arm
.bl_data
2013 self
.bl_bone
= bone
.name
# Could be different from the FBX name!
2015 # get average distance to children
2018 for child
in self
.children
:
2020 bone_size
+= child
.get_bind_matrix().to_translation().magnitude
2023 bone_size
/= bone_count
2025 bone_size
= parent_bone_size
2027 # So that our bone gets its final length, but still Y-aligned in armature space.
2028 # 0-length bones are automatically collapsed into their parent when you leave edit mode,
2029 # so this enforces a minimum length.
2030 bone_tail
= Vector((0.0, 1.0, 0.0)) * max(0.01, bone_size
)
2031 bone
.tail
= bone_tail
2033 # And rotate/move it to its final "rest pose".
2034 bone_matrix
= parent_matrix
@ self
.get_bind_matrix().normalized()
2036 bone
.matrix
= bone_matrix
2038 # Correction for children attached to a bone. FBX expects to attach to the head of a bone,
2039 # while Blender attaches to the tail.
2040 self
.bone_child_matrix
= Matrix
.Translation(-bone_tail
)
2042 connect_ctx
= [force_connect_children
, ...]
2043 for child
in self
.children
:
2044 if child
.is_leaf
and force_connect_children
:
2045 # Arggggggggggggggggg! We do not want to create this bone, but we need its 'virtual head' location
2046 # to orient current one!!!
2047 child_head
= (bone_matrix
@ child
.get_bind_matrix().normalized()).translation
2048 child_connect(bone
, None, child_head
, connect_ctx
)
2049 elif child
.is_bone
and not child
.ignore
:
2050 child_bone
= child
.build_skeleton(arm
, bone_matrix
, bone_size
,
2051 force_connect_children
=force_connect_children
)
2052 # Connection to parent.
2053 child_connect(bone
, child_bone
, None, connect_ctx
)
2055 child_connect_finalize(bone
, connect_ctx
)
2058 def build_node_obj(self
, fbx_tmpl
, settings
):
2062 if self
.is_bone
or not self
.fbx_elem
:
2065 # create when linking since we need object data
2066 elem_name_utf8
= self
.fbx_name
2068 # Object data must be created already
2069 self
.bl_obj
= obj
= bpy
.data
.objects
.new(name
=elem_name_utf8
, object_data
=self
.bl_data
)
2071 fbx_props
= (elem_find_first(self
.fbx_elem
, b
'Properties70'),
2072 elem_find_first(fbx_tmpl
, b
'Properties70', fbx_elem_nil
))
2077 obj
.color
[0:3] = elem_props_get_color_rgb(fbx_props
, b
'Color', (0.8, 0.8, 0.8))
2078 obj
.hide_viewport
= not bool(elem_props_get_visibility(fbx_props
, b
'Visibility', 1.0))
2080 obj
.matrix_basis
= self
.get_matrix()
2082 if settings
.use_custom_props
:
2083 blen_read_custom_properties(self
.fbx_elem
, obj
, settings
)
2087 def build_skeleton_children(self
, fbx_tmpl
, settings
, scene
, view_layer
):
2089 for child
in self
.children
:
2092 child
.build_skeleton_children(fbx_tmpl
, settings
, scene
, view_layer
)
2095 # child is not a bone
2096 obj
= self
.build_node_obj(fbx_tmpl
, settings
)
2101 for child
in self
.children
:
2104 child
.build_skeleton_children(fbx_tmpl
, settings
, scene
, view_layer
)
2107 view_layer
.active_layer_collection
.collection
.objects
.link(obj
)
2108 obj
.select_set(True)
2112 def link_skeleton_children(self
, fbx_tmpl
, settings
, scene
):
2114 for child
in self
.children
:
2117 child_obj
= child
.bl_obj
2118 if child_obj
and child_obj
!= self
.bl_obj
:
2119 child_obj
.parent
= self
.bl_obj
# get the armature the bone belongs to
2120 child_obj
.parent_bone
= self
.bl_bone
2121 child_obj
.parent_type
= 'BONE'
2122 child_obj
.matrix_parent_inverse
= Matrix()
2124 # Blender attaches to the end of a bone, while FBX attaches to the start.
2125 # bone_child_matrix corrects for that.
2126 if child
.pre_matrix
:
2127 child
.pre_matrix
= self
.bone_child_matrix
@ child
.pre_matrix
2129 child
.pre_matrix
= self
.bone_child_matrix
2131 child_obj
.matrix_basis
= child
.get_matrix()
2132 child
.link_skeleton_children(fbx_tmpl
, settings
, scene
)
2137 for child
in self
.children
:
2140 child_obj
= child
.link_skeleton_children(fbx_tmpl
, settings
, scene
)
2142 child_obj
.parent
= obj
2146 def set_pose_matrix(self
, arm
):
2147 pose_bone
= arm
.bl_obj
.pose
.bones
[self
.bl_bone
]
2148 pose_bone
.matrix_basis
= self
.get_bind_matrix().inverted_safe() @ self
.get_matrix()
2150 for child
in self
.children
:
2154 child
.set_pose_matrix(arm
)
2156 def merge_weights(self
, combined_weights
, fbx_cluster
):
2157 indices
= elem_prop_first(elem_find_first(fbx_cluster
, b
'Indexes', default
=None), default
=())
2158 weights
= elem_prop_first(elem_find_first(fbx_cluster
, b
'Weights', default
=None), default
=())
2160 for index
, weight
in zip(indices
, weights
):
2161 w
= combined_weights
.get(index
)
2163 combined_weights
[index
] = [weight
]
2167 def set_bone_weights(self
):
2168 ignored_children
= tuple(child
for child
in self
.children
2169 if child
.is_bone
and child
.ignore
and len(child
.clusters
) > 0)
2171 if len(ignored_children
) > 0:
2172 # If we have an ignored child bone we need to merge their weights into the current bone weights.
2173 # This can happen both intentionally and accidentally when skinning a model. Either way, they
2174 # need to be moved into a parent bone or they cause animation glitches.
2175 for fbx_cluster
, meshes
in self
.clusters
:
2176 combined_weights
= {}
2177 self
.merge_weights(combined_weights
, fbx_cluster
)
2179 for child
in ignored_children
:
2180 for child_cluster
, child_meshes
in child
.clusters
:
2181 if not meshes
.isdisjoint(child_meshes
):
2182 self
.merge_weights(combined_weights
, child_cluster
)
2184 # combine child weights
2187 for i
, w
in combined_weights
.items():
2190 weights
.append(sum(w
) / len(w
))
2192 weights
.append(w
[0])
2194 add_vgroup_to_objects(indices
, weights
, self
.bl_bone
, [node
.bl_obj
for node
in meshes
])
2196 # clusters that drive meshes not included in a parent don't need to be merged
2197 all_meshes
= set().union(*[meshes
for _
, meshes
in self
.clusters
])
2198 for child
in ignored_children
:
2199 for child_cluster
, child_meshes
in child
.clusters
:
2200 if all_meshes
.isdisjoint(child_meshes
):
2201 indices
= elem_prop_first(elem_find_first(child_cluster
, b
'Indexes', default
=None), default
=())
2202 weights
= elem_prop_first(elem_find_first(child_cluster
, b
'Weights', default
=None), default
=())
2203 add_vgroup_to_objects(indices
, weights
, self
.bl_bone
, [node
.bl_obj
for node
in child_meshes
])
2205 # set the vertex weights on meshes
2206 for fbx_cluster
, meshes
in self
.clusters
:
2207 indices
= elem_prop_first(elem_find_first(fbx_cluster
, b
'Indexes', default
=None), default
=())
2208 weights
= elem_prop_first(elem_find_first(fbx_cluster
, b
'Weights', default
=None), default
=())
2209 add_vgroup_to_objects(indices
, weights
, self
.bl_bone
, [node
.bl_obj
for node
in meshes
])
2211 for child
in self
.children
:
2212 if child
.is_bone
and not child
.ignore
:
2213 child
.set_bone_weights()
2215 def build_hierarchy(self
, fbx_tmpl
, settings
, scene
, view_layer
):
2216 if self
.is_armature
:
2217 # create when linking since we need object data
2218 elem_name_utf8
= self
.fbx_name
2220 self
.bl_data
= arm_data
= bpy
.data
.armatures
.new(name
=elem_name_utf8
)
2222 # Object data must be created already
2223 self
.bl_obj
= arm
= bpy
.data
.objects
.new(name
=elem_name_utf8
, object_data
=arm_data
)
2225 arm
.matrix_basis
= self
.get_matrix()
2228 fbx_props
= (elem_find_first(self
.fbx_elem
, b
'Properties70'),
2229 elem_find_first(fbx_tmpl
, b
'Properties70', fbx_elem_nil
))
2231 if settings
.use_custom_props
:
2232 blen_read_custom_properties(self
.fbx_elem
, arm
, settings
)
2235 view_layer
.active_layer_collection
.collection
.objects
.link(arm
)
2236 arm
.select_set(True)
2240 # Switch to Edit mode.
2241 view_layer
.objects
.active
= arm
2242 is_hidden
= arm
.hide_viewport
2243 arm
.hide_viewport
= False # Can't switch to Edit mode hidden objects...
2244 bpy
.ops
.object.mode_set(mode
='EDIT')
2246 for child
in self
.children
:
2250 child
.build_skeleton(self
, Matrix(), force_connect_children
=settings
.force_connect_children
)
2252 bpy
.ops
.object.mode_set(mode
='OBJECT')
2254 arm
.hide_viewport
= is_hidden
2257 for child
in self
.children
:
2261 child
.set_pose_matrix(self
)
2263 # Add bone children:
2264 for child
in self
.children
:
2267 child_obj
= child
.build_skeleton_children(fbx_tmpl
, settings
, scene
, view_layer
)
2270 elif self
.fbx_elem
and not self
.is_bone
:
2271 obj
= self
.build_node_obj(fbx_tmpl
, settings
)
2273 # walk through children
2274 for child
in self
.children
:
2275 child
.build_hierarchy(fbx_tmpl
, settings
, scene
, view_layer
)
2278 view_layer
.active_layer_collection
.collection
.objects
.link(obj
)
2279 obj
.select_set(True)
2283 for child
in self
.children
:
2284 child
.build_hierarchy(fbx_tmpl
, settings
, scene
, view_layer
)
2288 def link_hierarchy(self
, fbx_tmpl
, settings
, scene
):
2289 if self
.is_armature
:
2292 # Link bone children:
2293 for child
in self
.children
:
2296 child_obj
= child
.link_skeleton_children(fbx_tmpl
, settings
, scene
)
2298 child_obj
.parent
= arm
2300 # Add armature modifiers to the meshes
2302 for mesh
in self
.meshes
:
2303 (mmat
, amat
) = mesh
.armature_setup
[self
]
2304 me_obj
= mesh
.bl_obj
2306 # bring global armature & mesh matrices into *Blender* global space.
2307 # Note: Usage of matrix_geom (local 'diff' transform) here is quite brittle.
2308 # Among other things, why in hell isn't it taken into account by bindpose & co???
2309 # Probably because org app (max) handles it completely aside from any parenting stuff,
2310 # which we obviously cannot do in Blender. :/
2312 amat
= self
.bind_matrix
2313 amat
= settings
.global_matrix
@ (Matrix() if amat
is None else amat
)
2314 if self
.matrix_geom
:
2315 amat
= amat
@ self
.matrix_geom
2316 mmat
= settings
.global_matrix
@ mmat
2317 if mesh
.matrix_geom
:
2318 mmat
= mmat
@ mesh
.matrix_geom
2320 # Now that we have armature and mesh in there (global) bind 'state' (matrix),
2321 # we can compute inverse parenting matrix of the mesh.
2322 me_obj
.matrix_parent_inverse
= amat
.inverted_safe() @ mmat
@ me_obj
.matrix_basis
.inverted_safe()
2324 mod
= mesh
.bl_obj
.modifiers
.new(arm
.name
, 'ARMATURE')
2327 # Add bone weights to the deformers
2328 for child
in self
.children
:
2332 child
.set_bone_weights()
2338 # walk through children
2339 for child
in self
.children
:
2340 child_obj
= child
.link_hierarchy(fbx_tmpl
, settings
, scene
)
2342 child_obj
.parent
= obj
2346 for child
in self
.children
:
2347 child
.link_hierarchy(fbx_tmpl
, settings
, scene
)
2352 def load(operator
, context
, filepath
="",
2353 use_manual_orientation
=False,
2357 bake_space_transform
=False,
2358 use_custom_normals
=True,
2359 use_image_search
=False,
2360 use_alpha_decals
=False,
2365 use_custom_props
=True,
2366 use_custom_props_enum_as_string
=True,
2367 ignore_leaf_bones
=False,
2368 force_connect_children
=False,
2369 automatic_bone_orientation
=False,
2370 primary_bone_axis
='Y',
2371 secondary_bone_axis
='X',
2372 use_prepost_rot
=True,
2373 colors_type
='SRGB'):
2376 fbx_elem_nil
= FBXElem('', (), (), ())
2380 from bpy_extras
.io_utils
import axis_conversion
2382 from . import parse_fbx
2383 from .fbx_utils
import RIGHT_HAND_AXES
, FBX_FRAMERATES
2385 start_time_proc
= time
.process_time()
2386 start_time_sys
= time
.time()
2390 perfmon
.step("FBX Import: start importing %s" % filepath
)
2393 # Detect ASCII files.
2395 # Typically it's bad practice to fail silently on any error,
2396 # however the file may fail to read for many reasons,
2397 # and this situation is handled later in the code,
2398 # right now we only want to know if the file successfully reads as ascii.
2400 with
open(filepath
, 'r', encoding
="utf-8") as fh
:
2407 operator
.report({'ERROR'}, "ASCII FBX files are not supported %r" % filepath
)
2408 return {'CANCELLED'}
2410 # End ascii detection.
2413 elem_root
, version
= parse_fbx
.parse(filepath
)
2414 except Exception as e
:
2416 traceback
.print_exc()
2418 operator
.report({'ERROR'}, "Couldn't open file %r (%s)" % (filepath
, e
))
2419 return {'CANCELLED'}
2422 operator
.report({'ERROR'}, "Version %r unsupported, must be %r or later" % (version
, 7100))
2423 return {'CANCELLED'}
2425 print("FBX version: %r" % version
)
2427 if bpy
.ops
.object.mode_set
.poll():
2428 bpy
.ops
.object.mode_set(mode
='OBJECT', toggle
=False)
2431 if bpy
.ops
.object.select_all
.poll():
2432 bpy
.ops
.object.select_all(action
='DESELECT')
2434 basedir
= os
.path
.dirname(filepath
)
2436 nodal_material_wrap_map
= {}
2439 # Tables: (FBX_byte_id -> [FBX_data, None or Blender_datablock])
2440 fbx_table_nodes
= {}
2442 if use_alpha_decals
:
2443 material_decals
= set()
2445 material_decals
= None
2447 scene
= context
.scene
2448 view_layer
= context
.view_layer
2450 # #### Get some info from GlobalSettings.
2452 perfmon
.step("FBX import: Prepare...")
2454 fbx_settings
= elem_find_first(elem_root
, b
'GlobalSettings')
2455 fbx_settings_props
= elem_find_first(fbx_settings
, b
'Properties70')
2456 if fbx_settings
is None or fbx_settings_props
is None:
2457 operator
.report({'ERROR'}, "No 'GlobalSettings' found in file %r" % filepath
)
2458 return {'CANCELLED'}
2460 # FBX default base unit seems to be the centimeter, while raw Blender Unit is equivalent to the meter...
2461 unit_scale
= elem_props_get_number(fbx_settings_props
, b
'UnitScaleFactor', 1.0)
2462 unit_scale_org
= elem_props_get_number(fbx_settings_props
, b
'OriginalUnitScaleFactor', 1.0)
2463 global_scale
*= (unit_scale
/ units_blender_to_fbx_factor(context
.scene
))
2464 # Compute global matrix and scale.
2465 if not use_manual_orientation
:
2466 axis_forward
= (elem_props_get_integer(fbx_settings_props
, b
'FrontAxis', 1),
2467 elem_props_get_integer(fbx_settings_props
, b
'FrontAxisSign', 1))
2468 axis_up
= (elem_props_get_integer(fbx_settings_props
, b
'UpAxis', 2),
2469 elem_props_get_integer(fbx_settings_props
, b
'UpAxisSign', 1))
2470 axis_coord
= (elem_props_get_integer(fbx_settings_props
, b
'CoordAxis', 0),
2471 elem_props_get_integer(fbx_settings_props
, b
'CoordAxisSign', 1))
2472 axis_key
= (axis_up
, axis_forward
, axis_coord
)
2473 axis_up
, axis_forward
= {v
: k
for k
, v
in RIGHT_HAND_AXES
.items()}.get(axis_key
, ('Z', 'Y'))
2474 global_matrix
= (Matrix
.Scale(global_scale
, 4) @
2475 axis_conversion(from_forward
=axis_forward
, from_up
=axis_up
).to_4x4())
2477 # To cancel out unwanted rotation/scale on nodes.
2478 global_matrix_inv
= global_matrix
.inverted()
2479 # For transforming mesh normals.
2480 global_matrix_inv_transposed
= global_matrix_inv
.transposed()
2482 # Compute bone correction matrix
2483 bone_correction_matrix
= None # None means no correction/identity
2484 if not automatic_bone_orientation
:
2485 if (primary_bone_axis
, secondary_bone_axis
) != ('Y', 'X'):
2486 bone_correction_matrix
= axis_conversion(from_forward
='X',
2488 to_forward
=secondary_bone_axis
,
2489 to_up
=primary_bone_axis
,
2492 # Compute framerate settings.
2493 custom_fps
= elem_props_get_number(fbx_settings_props
, b
'CustomFrameRate', 25.0)
2494 time_mode
= elem_props_get_enum(fbx_settings_props
, b
'TimeMode')
2495 real_fps
= {eid
: val
for val
, eid
in FBX_FRAMERATES
[1:]}.get(time_mode
, custom_fps
)
2498 scene
.render
.fps
= round(real_fps
)
2499 scene
.render
.fps_base
= scene
.render
.fps
/ real_fps
2501 # store global settings that need to be accessed during conversion
2502 settings
= FBXImportSettings(
2503 operator
.report
, (axis_up
, axis_forward
), global_matrix
, global_scale
,
2504 bake_space_transform
, global_matrix_inv
, global_matrix_inv_transposed
,
2505 use_custom_normals
, use_image_search
,
2506 use_alpha_decals
, decal_offset
,
2507 use_anim
, anim_offset
,
2509 use_custom_props
, use_custom_props_enum_as_string
,
2510 nodal_material_wrap_map
, image_cache
,
2511 ignore_leaf_bones
, force_connect_children
, automatic_bone_orientation
, bone_correction_matrix
,
2512 use_prepost_rot
, colors_type
,
2515 # #### And now, the "real" data.
2517 perfmon
.step("FBX import: Templates...")
2519 fbx_defs
= elem_find_first(elem_root
, b
'Definitions') # can be None
2520 fbx_nodes
= elem_find_first(elem_root
, b
'Objects')
2521 fbx_connections
= elem_find_first(elem_root
, b
'Connections')
2523 if fbx_nodes
is None:
2524 operator
.report({'ERROR'}, "No 'Objects' found in file %r" % filepath
)
2525 return {'CANCELLED'}
2526 if fbx_connections
is None:
2527 operator
.report({'ERROR'}, "No 'Connections' found in file %r" % filepath
)
2528 return {'CANCELLED'}
2531 # First load property templates
2532 # Load 'PropertyTemplate' values.
2533 # Key is a tuple, (ObjectType, FBXNodeType)
2534 # eg, (b'Texture', b'KFbxFileTexture')
2535 # (b'Geometry', b'KFbxMesh')
2539 if fbx_defs
is not None:
2540 for fbx_def
in fbx_defs
.elems
:
2541 if fbx_def
.id == b
'ObjectType':
2542 for fbx_subdef
in fbx_def
.elems
:
2543 if fbx_subdef
.id == b
'PropertyTemplate':
2544 assert(fbx_def
.props_type
== b
'S')
2545 assert(fbx_subdef
.props_type
== b
'S')
2546 # (b'Texture', b'KFbxFileTexture') - eg.
2547 key
= fbx_def
.props
[0], fbx_subdef
.props
[0]
2548 fbx_templates
[key
] = fbx_subdef
2551 def fbx_template_get(key
):
2552 ret
= fbx_templates
.get(key
, fbx_elem_nil
)
2553 if ret
is fbx_elem_nil
:
2554 # Newest FBX (7.4 and above) use no more 'K' in their type names...
2555 key
= (key
[0], key
[1][1:])
2556 return fbx_templates
.get(key
, fbx_elem_nil
)
2559 perfmon
.step("FBX import: Nodes...")
2562 # Build FBX node-table
2564 for fbx_obj
in fbx_nodes
.elems
:
2565 # TODO, investigate what other items after first 3 may be
2566 assert(fbx_obj
.props_type
[:3] == b
'LSS')
2567 fbx_uuid
= elem_uuid(fbx_obj
)
2568 fbx_table_nodes
[fbx_uuid
] = [fbx_obj
, None]
2573 # http://download.autodesk.com/us/fbx/20112/FBX_SDK_HELP/index.html?url=
2574 # WS73099cc142f487551fea285e1221e4f9ff8-7fda.htm,topicNumber=d0e6388
2576 perfmon
.step("FBX import: Connections...")
2578 fbx_connection_map
= {}
2579 fbx_connection_map_reverse
= {}
2582 for fbx_link
in fbx_connections
.elems
:
2583 c_type
= fbx_link
.props
[0]
2584 if fbx_link
.props_type
[1:3] == b
'LL':
2585 c_src
, c_dst
= fbx_link
.props
[1:3]
2586 fbx_connection_map
.setdefault(c_src
, []).append((c_dst
, fbx_link
))
2587 fbx_connection_map_reverse
.setdefault(c_dst
, []).append((c_src
, fbx_link
))
2590 perfmon
.step("FBX import: Meshes...")
2595 fbx_tmpl
= fbx_template_get((b
'Geometry', b
'KFbxMesh'))
2597 for fbx_uuid
, fbx_item
in fbx_table_nodes
.items():
2598 fbx_obj
, blen_data
= fbx_item
2599 if fbx_obj
.id != b
'Geometry':
2601 if fbx_obj
.props
[-1] == b
'Mesh':
2602 assert(blen_data
is None)
2603 fbx_item
[1] = blen_read_geom(fbx_tmpl
, fbx_obj
, settings
)
2606 perfmon
.step("FBX import: Materials & Textures...")
2609 # Load material data
2611 fbx_tmpl
= fbx_template_get((b
'Material', b
'KFbxSurfacePhong'))
2612 # b'KFbxSurfaceLambert'
2614 for fbx_uuid
, fbx_item
in fbx_table_nodes
.items():
2615 fbx_obj
, blen_data
= fbx_item
2616 if fbx_obj
.id != b
'Material':
2618 assert(blen_data
is None)
2619 fbx_item
[1] = blen_read_material(fbx_tmpl
, fbx_obj
, settings
)
2623 # Load image & textures data
2625 fbx_tmpl_tex
= fbx_template_get((b
'Texture', b
'KFbxFileTexture'))
2626 fbx_tmpl_img
= fbx_template_get((b
'Video', b
'KFbxVideo'))
2628 # Important to run all 'Video' ones first, embedded images are stored in those nodes.
2629 # XXX Note we simplify things here, assuming both matching Video and Texture will use same file path,
2630 # this may be a bit weak, if issue arise we'll fallback to plain connection stuff...
2631 for fbx_uuid
, fbx_item
in fbx_table_nodes
.items():
2632 fbx_obj
, blen_data
= fbx_item
2633 if fbx_obj
.id != b
'Video':
2635 fbx_item
[1] = blen_read_texture_image(fbx_tmpl_img
, fbx_obj
, basedir
, settings
)
2636 for fbx_uuid
, fbx_item
in fbx_table_nodes
.items():
2637 fbx_obj
, blen_data
= fbx_item
2638 if fbx_obj
.id != b
'Texture':
2640 fbx_item
[1] = blen_read_texture_image(fbx_tmpl_tex
, fbx_obj
, basedir
, settings
)
2643 perfmon
.step("FBX import: Cameras & Lamps...")
2648 fbx_tmpl
= fbx_template_get((b
'NodeAttribute', b
'KFbxCamera'))
2650 for fbx_uuid
, fbx_item
in fbx_table_nodes
.items():
2651 fbx_obj
, blen_data
= fbx_item
2652 if fbx_obj
.id != b
'NodeAttribute':
2654 if fbx_obj
.props
[-1] == b
'Camera':
2655 assert(blen_data
is None)
2656 fbx_item
[1] = blen_read_camera(fbx_tmpl
, fbx_obj
, global_scale
)
2662 fbx_tmpl
= fbx_template_get((b
'NodeAttribute', b
'KFbxLight'))
2664 for fbx_uuid
, fbx_item
in fbx_table_nodes
.items():
2665 fbx_obj
, blen_data
= fbx_item
2666 if fbx_obj
.id != b
'NodeAttribute':
2668 if fbx_obj
.props
[-1] == b
'Light':
2669 assert(blen_data
is None)
2670 fbx_item
[1] = blen_read_light(fbx_tmpl
, fbx_obj
, global_scale
)
2675 def connection_filter_ex(fbx_uuid
, fbx_id
, dct
):
2676 return [(c_found
[0], c_found
[1], c_type
)
2677 for (c_uuid
, c_type
) in dct
.get(fbx_uuid
, ())
2678 # 0 is used for the root node, which isn't in fbx_table_nodes
2679 for c_found
in (() if c_uuid
== 0 else (fbx_table_nodes
.get(c_uuid
, (None, None)),))
2680 if (fbx_id
is None) or (c_found
[0] and c_found
[0].id == fbx_id
)]
2682 def connection_filter_forward(fbx_uuid
, fbx_id
):
2683 return connection_filter_ex(fbx_uuid
, fbx_id
, fbx_connection_map
)
2685 def connection_filter_reverse(fbx_uuid
, fbx_id
):
2686 return connection_filter_ex(fbx_uuid
, fbx_id
, fbx_connection_map_reverse
)
2688 perfmon
.step("FBX import: Objects & Armatures...")
2690 # -- temporary helper hierarchy to build armatures and objects from
2691 # lookup from uuid to helper node. Used to build parent-child relations and later to look up animated nodes.
2692 fbx_helper_nodes
= {}
2695 # We build an intermediate hierarchy used to:
2696 # - Calculate and store bone orientation correction matrices. The same matrices will be reused for animation.
2697 # - Find/insert armature nodes.
2698 # - Filter leaf bones.
2701 fbx_helper_nodes
[0] = root_helper
= FbxImportHelperNode(None, None, None, False)
2702 root_helper
.is_root
= True
2705 fbx_tmpl
= fbx_template_get((b
'Model', b
'KFbxNode'))
2706 for a_uuid
, a_item
in fbx_table_nodes
.items():
2707 fbx_obj
, bl_data
= a_item
2708 if fbx_obj
is None or fbx_obj
.id != b
'Model':
2711 fbx_props
= (elem_find_first(fbx_obj
, b
'Properties70'),
2712 elem_find_first(fbx_tmpl
, b
'Properties70', fbx_elem_nil
))
2714 transform_data
= blen_read_object_transform_preprocess(fbx_props
, fbx_obj
, Matrix(), use_prepost_rot
)
2715 # Note: 'Root' "bones" are handled as (armature) objects.
2716 # Note: See T46912 for first FBX file I ever saw with 'Limb' bones - thought those were totally deprecated.
2717 is_bone
= fbx_obj
.props
[2] in {b
'LimbNode', b
'Limb'}
2718 fbx_helper_nodes
[a_uuid
] = FbxImportHelperNode(fbx_obj
, bl_data
, transform_data
, is_bone
)
2720 # add parent-child relations and add blender data to the node
2721 for fbx_link
in fbx_connections
.elems
:
2722 if fbx_link
.props
[0] != b
'OO':
2724 if fbx_link
.props_type
[1:3] == b
'LL':
2725 c_src
, c_dst
= fbx_link
.props
[1:3]
2726 parent
= fbx_helper_nodes
.get(c_dst
)
2730 child
= fbx_helper_nodes
.get(c_src
)
2732 # add blender data (meshes, lights, cameras, etc.) to a helper node
2733 fbx_sdata
, bl_data
= p_item
= fbx_table_nodes
.get(c_src
, (None, None))
2734 if fbx_sdata
is None:
2736 if fbx_sdata
.id not in {b
'Geometry', b
'NodeAttribute'}:
2738 parent
.bl_data
= bl_data
2741 child
.parent
= parent
2743 # find armatures (either an empty below a bone or a new node inserted at the bone
2744 root_helper
.find_armatures()
2746 # mark nodes that have bone children
2747 root_helper
.find_bone_children()
2749 # mark nodes that need a bone to attach child-bones to
2750 root_helper
.find_fake_bones()
2752 # mark leaf nodes that are only required to mark the end of their parent bone
2753 if settings
.ignore_leaf_bones
:
2754 root_helper
.mark_leaf_bones()
2756 # What a mess! Some bones have several BindPoses, some have none, clusters contain a bind pose as well,
2757 # and you can have several clusters per bone!
2758 # Maybe some conversion can be applied to put them all into the same frame of reference?
2760 # get the bind pose from pose elements
2761 for a_uuid
, a_item
in fbx_table_nodes
.items():
2762 fbx_obj
, bl_data
= a_item
2765 if fbx_obj
.id != b
'Pose':
2767 if fbx_obj
.props
[2] != b
'BindPose':
2769 for fbx_pose_node
in fbx_obj
.elems
:
2770 if fbx_pose_node
.id != b
'PoseNode':
2772 node_elem
= elem_find_first(fbx_pose_node
, b
'Node')
2773 node
= elem_uuid(node_elem
)
2774 matrix_elem
= elem_find_first(fbx_pose_node
, b
'Matrix')
2775 matrix
= array_to_matrix4(matrix_elem
.props
[0]) if matrix_elem
else None
2776 bone
= fbx_helper_nodes
.get(node
)
2778 # Store the matrix in the helper node.
2779 # There may be several bind pose matrices for the same node, but in tests they seem to be identical.
2780 bone
.bind_matrix
= matrix
# global space
2782 # get clusters and bind pose
2783 for helper_uuid
, helper_node
in fbx_helper_nodes
.items():
2784 if not helper_node
.is_bone
:
2786 for cluster_uuid
, cluster_link
in fbx_connection_map
.get(helper_uuid
, ()):
2787 if cluster_link
.props
[0] != b
'OO':
2789 fbx_cluster
, _
= fbx_table_nodes
.get(cluster_uuid
, (None, None))
2790 if fbx_cluster
is None or fbx_cluster
.id != b
'Deformer' or fbx_cluster
.props
[2] != b
'Cluster':
2793 # Get the bind pose from the cluster:
2794 tx_mesh_elem
= elem_find_first(fbx_cluster
, b
'Transform', default
=None)
2795 tx_mesh
= array_to_matrix4(tx_mesh_elem
.props
[0]) if tx_mesh_elem
else Matrix()
2797 tx_bone_elem
= elem_find_first(fbx_cluster
, b
'TransformLink', default
=None)
2798 tx_bone
= array_to_matrix4(tx_bone_elem
.props
[0]) if tx_bone_elem
else None
2800 tx_arm_elem
= elem_find_first(fbx_cluster
, b
'TransformAssociateModel', default
=None)
2801 tx_arm
= array_to_matrix4(tx_arm_elem
.props
[0]) if tx_arm_elem
else None
2803 mesh_matrix
= tx_mesh
2804 armature_matrix
= tx_arm
2807 mesh_matrix
= tx_bone
@ mesh_matrix
2808 helper_node
.bind_matrix
= tx_bone
# overwrite the bind matrix
2810 # Get the meshes driven by this cluster: (Shouldn't that be only one?)
2812 for skin_uuid
, skin_link
in fbx_connection_map
.get(cluster_uuid
):
2813 if skin_link
.props
[0] != b
'OO':
2815 fbx_skin
, _
= fbx_table_nodes
.get(skin_uuid
, (None, None))
2816 if fbx_skin
is None or fbx_skin
.id != b
'Deformer' or fbx_skin
.props
[2] != b
'Skin':
2818 for mesh_uuid
, mesh_link
in fbx_connection_map
.get(skin_uuid
):
2819 if mesh_link
.props
[0] != b
'OO':
2821 fbx_mesh
, _
= fbx_table_nodes
.get(mesh_uuid
, (None, None))
2822 if fbx_mesh
is None or fbx_mesh
.id != b
'Geometry' or fbx_mesh
.props
[2] != b
'Mesh':
2824 for object_uuid
, object_link
in fbx_connection_map
.get(mesh_uuid
):
2825 if object_link
.props
[0] != b
'OO':
2827 mesh_node
= fbx_helper_nodes
[object_uuid
]
2830 # If we get a valid mesh matrix (in bone space), store armature and
2831 # mesh global matrices, we need them to compute mesh's matrix_parent_inverse
2832 # when actually binding them via the modifier.
2833 # Note we assume all bones were bound with the same mesh/armature (global) matrix,
2834 # we do not support otherwise in Blender anyway!
2835 mesh_node
.armature_setup
[helper_node
.armature
] = (mesh_matrix
, armature_matrix
)
2836 meshes
.add(mesh_node
)
2838 helper_node
.clusters
.append((fbx_cluster
, meshes
))
2840 # convert bind poses from global space into local space
2841 root_helper
.make_bind_pose_local()
2843 # collect armature meshes
2844 root_helper
.collect_armature_meshes()
2846 # find the correction matrices to align FBX objects with their Blender equivalent
2847 root_helper
.find_correction_matrix(settings
)
2849 # build the Object/Armature/Bone hierarchy
2850 root_helper
.build_hierarchy(fbx_tmpl
, settings
, scene
, view_layer
)
2852 # Link the Object/Armature/Bone hierarchy
2853 root_helper
.link_hierarchy(fbx_tmpl
, settings
, scene
)
2855 # root_helper.print_info(0)
2858 perfmon
.step("FBX import: ShapeKeys...")
2860 # We can handle shapes.
2861 blend_shape_channels
= {} # We do not need Shapes themselves, but keyblocks, for anim.
2864 fbx_tmpl
= fbx_template_get((b
'Geometry', b
'KFbxShape'))
2866 for s_uuid
, s_item
in fbx_table_nodes
.items():
2867 fbx_sdata
, bl_sdata
= s_item
= fbx_table_nodes
.get(s_uuid
, (None, None))
2868 if fbx_sdata
is None or fbx_sdata
.id != b
'Geometry' or fbx_sdata
.props
[2] != b
'Shape':
2871 # shape -> blendshapechannel -> blendshape -> mesh.
2872 for bc_uuid
, bc_ctype
in fbx_connection_map
.get(s_uuid
, ()):
2873 if bc_ctype
.props
[0] != b
'OO':
2875 fbx_bcdata
, _bl_bcdata
= fbx_table_nodes
.get(bc_uuid
, (None, None))
2876 if fbx_bcdata
is None or fbx_bcdata
.id != b
'Deformer' or fbx_bcdata
.props
[2] != b
'BlendShapeChannel':
2880 for bs_uuid
, bs_ctype
in fbx_connection_map
.get(bc_uuid
, ()):
2881 if bs_ctype
.props
[0] != b
'OO':
2883 fbx_bsdata
, _bl_bsdata
= fbx_table_nodes
.get(bs_uuid
, (None, None))
2884 if fbx_bsdata
is None or fbx_bsdata
.id != b
'Deformer' or fbx_bsdata
.props
[2] != b
'BlendShape':
2886 for m_uuid
, m_ctype
in fbx_connection_map
.get(bs_uuid
, ()):
2887 if m_ctype
.props
[0] != b
'OO':
2889 fbx_mdata
, bl_mdata
= fbx_table_nodes
.get(m_uuid
, (None, None))
2890 if fbx_mdata
is None or fbx_mdata
.id != b
'Geometry' or fbx_mdata
.props
[2] != b
'Mesh':
2892 # Blenmeshes are assumed already created at that time!
2893 assert(isinstance(bl_mdata
, bpy
.types
.Mesh
))
2894 # And we have to find all objects using this mesh!
2896 for o_uuid
, o_ctype
in fbx_connection_map
.get(m_uuid
, ()):
2897 if o_ctype
.props
[0] != b
'OO':
2899 node
= fbx_helper_nodes
[o_uuid
]
2901 objects
.append(node
)
2902 meshes
.append((bl_mdata
, objects
))
2903 # BlendShape deformers are only here to connect BlendShapeChannels to meshes, nothing else to do.
2905 # keyblocks is a list of tuples (mesh, keyblock) matching that shape/blendshapechannel, for animation.
2906 keyblocks
= blen_read_shape(fbx_tmpl
, fbx_sdata
, fbx_bcdata
, meshes
, scene
)
2907 blend_shape_channels
[bc_uuid
] = keyblocks
2910 if settings
.use_subsurf
:
2911 perfmon
.step("FBX import: Subdivision surfaces")
2913 # Look through connections for subsurf in meshes and add it to the parent object
2915 for fbx_link
in fbx_connections
.elems
:
2916 if fbx_link
.props
[0] != b
'OO':
2918 if fbx_link
.props_type
[1:3] == b
'LL':
2919 c_src
, c_dst
= fbx_link
.props
[1:3]
2920 parent
= fbx_helper_nodes
.get(c_dst
)
2924 child
= fbx_helper_nodes
.get(c_src
)
2926 fbx_sdata
, bl_data
= fbx_table_nodes
.get(c_src
, (None, None))
2927 if fbx_sdata
.id != b
'Geometry':
2930 preview_levels
= elem_prop_first(elem_find_first(fbx_sdata
, b
'PreviewDivisionLevels'))
2931 render_levels
= elem_prop_first(elem_find_first(fbx_sdata
, b
'RenderDivisionLevels'))
2932 if isinstance(preview_levels
, int) and isinstance(render_levels
, int):
2933 mod
= parent
.bl_obj
.modifiers
.new('subsurf', 'SUBSURF')
2934 mod
.levels
= preview_levels
2935 mod
.render_levels
= render_levels
2936 boundary_rule
= elem_prop_first(elem_find_first(fbx_sdata
, b
'BoundaryRule'), default
=1)
2937 if boundary_rule
== 1:
2938 mod
.boundary_smooth
= "PRESERVE_CORNERS"
2940 mod
.boundary_smooth
= "ALL"
2945 perfmon
.step("FBX import: Animations...")
2949 fbx_tmpl_astack
= fbx_template_get((b
'AnimationStack', b
'FbxAnimStack'))
2950 fbx_tmpl_alayer
= fbx_template_get((b
'AnimationLayer', b
'FbxAnimLayer'))
2954 for as_uuid
, fbx_asitem
in fbx_table_nodes
.items():
2955 fbx_asdata
, _blen_data
= fbx_asitem
2956 if fbx_asdata
.id != b
'AnimationStack' or fbx_asdata
.props
[2] != b
'':
2958 stacks
[as_uuid
] = (fbx_asitem
, {})
2961 # (mixing is completely ignored for now, each layer results in an independent set of actions).
2962 def get_astacks_from_alayer(al_uuid
):
2963 for as_uuid
, as_ctype
in fbx_connection_map
.get(al_uuid
, ()):
2964 if as_ctype
.props
[0] != b
'OO':
2966 fbx_asdata
, _bl_asdata
= fbx_table_nodes
.get(as_uuid
, (None, None))
2967 if (fbx_asdata
is None or fbx_asdata
.id != b
'AnimationStack' or
2968 fbx_asdata
.props
[2] != b
'' or as_uuid
not in stacks
):
2971 for al_uuid
, fbx_alitem
in fbx_table_nodes
.items():
2972 fbx_aldata
, _blen_data
= fbx_alitem
2973 if fbx_aldata
.id != b
'AnimationLayer' or fbx_aldata
.props
[2] != b
'':
2975 for as_uuid
in get_astacks_from_alayer(al_uuid
):
2976 _fbx_asitem
, alayers
= stacks
[as_uuid
]
2977 alayers
[al_uuid
] = (fbx_alitem
, {})
2979 # AnimationCurveNodes (also the ones linked to actual animated data!).
2981 for acn_uuid
, fbx_acnitem
in fbx_table_nodes
.items():
2982 fbx_acndata
, _blen_data
= fbx_acnitem
2983 if fbx_acndata
.id != b
'AnimationCurveNode' or fbx_acndata
.props
[2] != b
'':
2985 cnode
= curvenodes
[acn_uuid
] = {}
2987 for n_uuid
, n_ctype
in fbx_connection_map
.get(acn_uuid
, ()):
2988 if n_ctype
.props
[0] != b
'OP':
2990 lnk_prop
= n_ctype
.props
[3]
2991 if lnk_prop
in {b
'Lcl Translation', b
'Lcl Rotation', b
'Lcl Scaling'}:
2992 # n_uuid can (????) be linked to root '0' node, instead of a mere object node... See T41712.
2993 ob
= fbx_helper_nodes
.get(n_uuid
, None)
2994 if ob
is None or ob
.is_root
:
2996 items
.append((ob
, lnk_prop
))
2997 elif lnk_prop
== b
'DeformPercent': # Shape keys.
2998 keyblocks
= blend_shape_channels
.get(n_uuid
, None)
2999 if keyblocks
is None:
3001 items
+= [(kb
, lnk_prop
) for kb
in keyblocks
]
3002 elif lnk_prop
== b
'FocalLength': # Camera lens.
3003 from bpy
.types
import Camera
3004 fbx_item
= fbx_table_nodes
.get(n_uuid
, None)
3005 if fbx_item
is None or not isinstance(fbx_item
[1], Camera
):
3008 items
.append((cam
, lnk_prop
))
3009 elif lnk_prop
== b
'FocusDistance': # Camera focus.
3010 from bpy
.types
import Camera
3011 fbx_item
= fbx_table_nodes
.get(n_uuid
, None)
3012 if fbx_item
is None or not isinstance(fbx_item
[1], Camera
):
3015 items
.append((cam
, lnk_prop
))
3016 elif lnk_prop
== b
'DiffuseColor':
3017 from bpy
.types
import Material
3018 fbx_item
= fbx_table_nodes
.get(n_uuid
, None)
3019 if fbx_item
is None or not isinstance(fbx_item
[1], Material
):
3022 items
.append((mat
, lnk_prop
))
3023 print("WARNING! Importing material's animation is not supported for Nodal materials...")
3024 for al_uuid
, al_ctype
in fbx_connection_map
.get(acn_uuid
, ()):
3025 if al_ctype
.props
[0] != b
'OO':
3027 fbx_aldata
, _blen_aldata
= fbx_alitem
= fbx_table_nodes
.get(al_uuid
, (None, None))
3028 if fbx_aldata
is None or fbx_aldata
.id != b
'AnimationLayer' or fbx_aldata
.props
[2] != b
'':
3030 for as_uuid
in get_astacks_from_alayer(al_uuid
):
3031 _fbx_alitem
, anim_items
= stacks
[as_uuid
][1][al_uuid
]
3032 assert(_fbx_alitem
== fbx_alitem
)
3033 for item
, item_prop
in items
:
3034 # No need to keep curvenode FBX data here, contains nothing useful for us.
3035 anim_items
.setdefault(item
, {})[acn_uuid
] = (cnode
, item_prop
)
3037 # AnimationCurves (real animation data).
3038 for ac_uuid
, fbx_acitem
in fbx_table_nodes
.items():
3039 fbx_acdata
, _blen_data
= fbx_acitem
3040 if fbx_acdata
.id != b
'AnimationCurve' or fbx_acdata
.props
[2] != b
'':
3042 for acn_uuid
, acn_ctype
in fbx_connection_map
.get(ac_uuid
, ()):
3043 if acn_ctype
.props
[0] != b
'OP':
3045 fbx_acndata
, _bl_acndata
= fbx_table_nodes
.get(acn_uuid
, (None, None))
3046 if (fbx_acndata
is None or fbx_acndata
.id != b
'AnimationCurveNode' or
3047 fbx_acndata
.props
[2] != b
'' or acn_uuid
not in curvenodes
):
3049 # Note this is an infamous simplification of the compound props stuff,
3050 # seems to be standard naming but we'll probably have to be smarter to handle more exotic files?
3052 b
'd|X': 0, b
'd|Y': 1, b
'd|Z': 2,
3053 b
'd|DeformPercent': 0,
3054 b
'd|FocalLength': 0,
3055 b
'd|FocusDistance': 0
3056 }.get(acn_ctype
.props
[3], None)
3059 curvenodes
[acn_uuid
][ac_uuid
] = (fbx_acitem
, channel
)
3061 # And now that we have sorted all this, apply animations!
3062 blen_read_animations(fbx_tmpl_astack
, fbx_tmpl_alayer
, stacks
, scene
, settings
.anim_offset
, global_scale
)
3066 perfmon
.step("FBX import: Assign materials...")
3069 # link Material's to Geometry (via Model's)
3070 for fbx_uuid
, fbx_item
in fbx_table_nodes
.items():
3071 fbx_obj
, blen_data
= fbx_item
3072 if fbx_obj
.id != b
'Geometry':
3075 mesh
= fbx_table_nodes
.get(fbx_uuid
, (None, None))[1]
3077 # can happen in rare cases
3081 # In Blender, we link materials to data, typically (meshes), while in FBX they are linked to objects...
3082 # So we have to be careful not to re-add endlessly the same material to a mesh!
3083 # This can easily happen with 'baked' dupliobjects, see T44386.
3084 # TODO: add an option to link materials to objects in Blender instead?
3085 done_materials
= set()
3087 for (fbx_lnk
, fbx_lnk_item
, fbx_lnk_type
) in connection_filter_forward(fbx_uuid
, b
'Model'):
3089 fbx_lnk_uuid
= elem_uuid(fbx_lnk
)
3090 for (fbx_lnk_material
, material
, fbx_lnk_material_type
) in connection_filter_reverse(fbx_lnk_uuid
, b
'Material'):
3091 if material
not in done_materials
:
3092 mesh
.materials
.append(material
)
3093 done_materials
.add(material
)
3095 # We have to validate mesh polygons' ma_idx, see T41015!
3096 # Some FBX seem to have an extra 'default' material which is not defined in FBX file.
3097 if mesh
.validate_material_indices():
3098 print("WARNING: mesh '%s' had invalid material indices, those were reset to first material" % mesh
.name
)
3101 perfmon
.step("FBX import: Assign textures...")
3104 material_images
= {}
3106 fbx_tmpl
= fbx_template_get((b
'Material', b
'KFbxSurfacePhong'))
3107 # b'KFbxSurfaceLambert'
3109 def texture_mapping_set(fbx_obj
, node_texture
):
3110 assert(fbx_obj
.id == b
'Texture')
3112 fbx_props
= (elem_find_first(fbx_obj
, b
'Properties70'),
3113 elem_find_first(fbx_tmpl
, b
'Properties70', fbx_elem_nil
))
3114 loc
= elem_props_get_vector_3d(fbx_props
, b
'Translation', (0.0, 0.0, 0.0))
3115 rot
= tuple(-r
for r
in elem_props_get_vector_3d(fbx_props
, b
'Rotation', (0.0, 0.0, 0.0)))
3116 scale
= tuple(((1.0 / s
) if s
!= 0.0 else 1.0)
3117 for s
in elem_props_get_vector_3d(fbx_props
, b
'Scaling', (1.0, 1.0, 1.0)))
3118 clamp
= (bool(elem_props_get_enum(fbx_props
, b
'WrapModeU', 0)) or
3119 bool(elem_props_get_enum(fbx_props
, b
'WrapModeV', 0)))
3121 if (loc
== (0.0, 0.0, 0.0) and
3122 rot
== (0.0, 0.0, 0.0) and
3123 scale
== (1.0, 1.0, 1.0) and
3127 node_texture
.translation
= loc
3128 node_texture
.rotation
= rot
3129 node_texture
.scale
= scale
3131 node_texture
.extension
= 'EXTEND'
3133 for fbx_uuid
, fbx_item
in fbx_table_nodes
.items():
3134 fbx_obj
, blen_data
= fbx_item
3135 if fbx_obj
.id != b
'Material':
3138 material
= fbx_table_nodes
.get(fbx_uuid
, (None, None))[1]
3141 fbx_lnk_type
) in connection_filter_reverse(fbx_uuid
, b
'Texture'):
3143 if fbx_lnk_type
.props
[0] == b
'OP':
3144 lnk_type
= fbx_lnk_type
.props
[3]
3146 ma_wrap
= nodal_material_wrap_map
[material
]
3148 if lnk_type
in {b
'DiffuseColor', b
'3dsMax|maps|texmap_diffuse'}:
3149 ma_wrap
.base_color_texture
.image
= image
3150 texture_mapping_set(fbx_lnk
, ma_wrap
.base_color_texture
)
3151 elif lnk_type
in {b
'SpecularColor', b
'SpecularFactor'}:
3152 # Intensity actually, not color...
3153 ma_wrap
.specular_texture
.image
= image
3154 texture_mapping_set(fbx_lnk
, ma_wrap
.specular_texture
)
3155 elif lnk_type
in {b
'ReflectionColor', b
'ReflectionFactor', b
'3dsMax|maps|texmap_reflection'}:
3156 # Intensity actually, not color...
3157 ma_wrap
.metallic_texture
.image
= image
3158 texture_mapping_set(fbx_lnk
, ma_wrap
.metallic_texture
)
3159 elif lnk_type
in {b
'TransparentColor', b
'TransparentFactor'}:
3160 ma_wrap
.alpha_texture
.image
= image
3161 texture_mapping_set(fbx_lnk
, ma_wrap
.alpha_texture
)
3162 if use_alpha_decals
:
3163 material_decals
.add(material
)
3164 elif lnk_type
== b
'ShininessExponent':
3165 # That is probably reversed compared to expected results? TODO...
3166 ma_wrap
.roughness_texture
.image
= image
3167 texture_mapping_set(fbx_lnk
, ma_wrap
.roughness_texture
)
3168 # XXX, applications abuse bump!
3169 elif lnk_type
in {b
'NormalMap', b
'Bump', b
'3dsMax|maps|texmap_bump'}:
3170 ma_wrap
.normalmap_texture
.image
= image
3171 texture_mapping_set(fbx_lnk
, ma_wrap
.normalmap_texture
)
3173 elif lnk_type == b'Bump':
3174 # TODO displacement...
3176 elif lnk_type
in {b
'EmissiveColor'}:
3177 ma_wrap
.emission_color_texture
.image
= image
3178 texture_mapping_set(fbx_lnk
, ma_wrap
.emission_color_texture
)
3179 elif lnk_type
in {b
'EmissiveFactor'}:
3180 ma_wrap
.emission_strength_texture
.image
= image
3181 texture_mapping_set(fbx_lnk
, ma_wrap
.emission_strength_texture
)
3183 print("WARNING: material link %r ignored" % lnk_type
)
3185 material_images
.setdefault(material
, {})[lnk_type
] = image
3187 # Check if the diffuse image has an alpha channel,
3188 # if so, use the alpha channel.
3190 # Note: this could be made optional since images may have alpha but be entirely opaque
3191 for fbx_uuid
, fbx_item
in fbx_table_nodes
.items():
3192 fbx_obj
, blen_data
= fbx_item
3193 if fbx_obj
.id != b
'Material':
3195 material
= fbx_table_nodes
.get(fbx_uuid
, (None, None))[1]
3196 image
= material_images
.get(material
, {}).get(b
'DiffuseColor', None)
3198 if image
and image
.depth
== 32:
3199 if use_alpha_decals
:
3200 material_decals
.add(material
)
3202 ma_wrap
= nodal_material_wrap_map
[material
]
3203 ma_wrap
.alpha_texture
.use_alpha
= True
3204 ma_wrap
.alpha_texture
.copy_from(ma_wrap
.base_color_texture
)
3206 # Propagate mapping from diffuse to all other channels which have none defined.
3207 # XXX Commenting for now, I do not really understand the logic here, why should diffuse mapping
3208 # be applied to all others if not defined for them???
3209 # ~ ma_wrap = nodal_material_wrap_map[material]
3210 # ~ ma_wrap.mapping_set_from_diffuse()
3214 perfmon
.step("FBX import: Cycles z-offset workaround...")
3217 # Annoying workaround for cycles having no z-offset
3218 if material_decals
and use_alpha_decals
:
3219 for fbx_uuid
, fbx_item
in fbx_table_nodes
.items():
3220 fbx_obj
, blen_data
= fbx_item
3221 if fbx_obj
.id != b
'Geometry':
3223 if fbx_obj
.props
[-1] == b
'Mesh':
3226 if decal_offset
!= 0.0:
3227 for material
in mesh
.materials
:
3228 if material
in material_decals
:
3229 for v
in mesh
.vertices
:
3230 v
.co
+= v
.normal
* decal_offset
3233 for obj
in (obj
for obj
in bpy
.data
.objects
if obj
.data
== mesh
):
3234 obj
.visible_shadow
= False
3237 perfmon
.level_down()
3239 perfmon
.level_down("Import finished.")