1 # ##### BEGIN GPL LICENSE BLOCK #####
3 # This program is free software; you can redistribute it and/or
4 # modify it under the terms of the GNU General Public License
5 # as published by the Free Software Foundation; either version 2
6 # of the License, or (at your option) any later version.
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
13 # You should have received a copy of the GNU General Public License
14 # along with this program; if not, write to the Free Software Foundation,
15 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 # ##### END GPL LICENSE BLOCK #####
21 # Script copyright (C) Blender Foundation
23 # FBX 7.1.0 -> 7.4.0 loader for Blender
25 # Not totally pep8 compliant.
26 # pep8 import_fbx.py --ignore=E501,E123,E702,E125
30 if "parse_fbx" in locals():
31 importlib
.reload(parse_fbx
)
32 if "fbx_utils" in locals():
33 importlib
.reload(fbx_utils
)
36 from mathutils
import Matrix
, Euler
, Vector
40 from . import parse_fbx
, fbx_utils
42 from .parse_fbx
import (
46 from .fbx_utils
import (
48 units_blender_to_fbx_factor
,
56 # global singleton, assign on execution
60 convert_deg_to_rad_iter
= units_convertor_iter("degree", "radian")
62 MAT_CONVERT_BONE
= fbx_utils
.MAT_CONVERT_BONE
.inverted()
63 MAT_CONVERT_LIGHT
= fbx_utils
.MAT_CONVERT_LIGHT
.inverted()
64 MAT_CONVERT_CAMERA
= fbx_utils
.MAT_CONVERT_CAMERA
.inverted()
67 def validate_blend_names(name
):
68 assert(type(name
) == bytes
)
69 # Blender typically does not accept names over 63 bytes...
72 h
= hashlib
.sha1(name
).hexdigest()
74 name_utf8
= name
[:n
].decode('utf-8', 'replace') + "_" + h
[:7]
75 while len(name_utf8
.encode()) > 63:
77 name_utf8
= name
[:n
].decode('utf-8', 'replace') + "_" + h
[:7]
80 # We use 'replace' even though FBX 'specs' say it should always be utf8, see T53841.
81 return name
.decode('utf-8', 'replace')
84 def elem_find_first(elem
, id_search
, default
=None):
85 for fbx_item
in elem
.elems
:
86 if fbx_item
.id == id_search
:
91 def elem_find_iter(elem
, id_search
):
92 for fbx_item
in elem
.elems
:
93 if fbx_item
.id == id_search
:
97 def elem_find_first_string(elem
, id_search
):
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
.STRING
)
102 return fbx_item
.props
[0].decode('utf-8', 'replace')
106 def elem_find_first_string_as_bytes(elem
, id_search
):
107 fbx_item
= elem_find_first(elem
, id_search
)
108 if fbx_item
is not None and fbx_item
.props
: # Do not error on complete empty properties (see T45291).
109 assert(len(fbx_item
.props
) == 1)
110 assert(fbx_item
.props_type
[0] == data_types
.STRING
)
111 return fbx_item
.props
[0] # Keep it as bytes as requested...
115 def elem_find_first_bytes(elem
, id_search
, decode
=True):
116 fbx_item
= elem_find_first(elem
, id_search
)
117 if fbx_item
is not None and fbx_item
.props
: # Do not error on complete empty properties (see T45291).
118 assert(len(fbx_item
.props
) == 1)
119 assert(fbx_item
.props_type
[0] == data_types
.BYTES
)
120 return fbx_item
.props
[0]
125 return "%s: props[%d=%r], elems=(%r)" % (
128 ", ".join([repr(p
) for p
in elem
.props
]),
130 b
", ".join([e
.id for e
in elem
.elems
]),
134 def elem_split_name_class(elem
):
135 assert(elem
.props_type
[-2] == data_types
.STRING
)
136 elem_name
, elem_class
= elem
.props
[-2].split(b
'\x00\x01')
137 return elem_name
, elem_class
140 def elem_name_ensure_class(elem
, clss
=...):
141 elem_name
, elem_class
= elem_split_name_class(elem
)
143 assert(elem_class
== clss
)
144 return validate_blend_names(elem_name
)
147 def elem_name_ensure_classes(elem
, clss
=...):
148 elem_name
, elem_class
= elem_split_name_class(elem
)
150 assert(elem_class
in clss
)
151 return validate_blend_names(elem_name
)
154 def elem_split_name_class_nodeattr(elem
):
155 assert(elem
.props_type
[-2] == data_types
.STRING
)
156 elem_name
, elem_class
= elem
.props
[-2].split(b
'\x00\x01')
157 assert(elem_class
== b
'NodeAttribute')
158 assert(elem
.props_type
[-1] == data_types
.STRING
)
159 elem_class
= elem
.props
[-1]
160 return elem_name
, elem_class
164 assert(elem
.props_type
[0] == data_types
.INT64
)
168 def elem_prop_first(elem
, default
=None):
169 return elem
.props
[0] if (elem
is not None) and elem
.props
else default
174 # Properties70: { ... P:
175 def elem_props_find_first(elem
, elem_prop_id
):
177 # When properties are not found... Should never happen, but happens - as usual.
179 # support for templates (tuple of elems)
180 if type(elem
) is not FBXElem
:
181 assert(type(elem
) is tuple)
183 result
= elem_props_find_first(e
, elem_prop_id
)
184 if result
is not None:
186 assert(len(elem
) > 0)
189 for subelem
in elem
.elems
:
190 assert(subelem
.id == b
'P')
191 if subelem
.props
[0] == elem_prop_id
:
196 def elem_props_get_color_rgb(elem
, elem_prop_id
, default
=None):
197 elem_prop
= elem_props_find_first(elem
, elem_prop_id
)
198 if elem_prop
is not None:
199 assert(elem_prop
.props
[0] == elem_prop_id
)
200 if elem_prop
.props
[1] == b
'Color':
202 assert(elem_prop
.props
[1] == b
'Color')
203 assert(elem_prop
.props
[2] == b
'')
205 assert(elem_prop
.props
[1] == b
'ColorRGB')
206 assert(elem_prop
.props
[2] == b
'Color')
207 assert(elem_prop
.props_type
[4:7] == bytes((data_types
.FLOAT64
,)) * 3)
208 return elem_prop
.props
[4:7]
212 def elem_props_get_vector_3d(elem
, elem_prop_id
, default
=None):
213 elem_prop
= elem_props_find_first(elem
, elem_prop_id
)
214 if elem_prop
is not None:
215 assert(elem_prop
.props_type
[4:7] == bytes((data_types
.FLOAT64
,)) * 3)
216 return elem_prop
.props
[4:7]
220 def elem_props_get_number(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
'double':
225 assert(elem_prop
.props
[1] == b
'double')
226 assert(elem_prop
.props
[2] == b
'Number')
228 assert(elem_prop
.props
[1] == b
'Number')
229 assert(elem_prop
.props
[2] == b
'')
231 # we could allow other number types
232 assert(elem_prop
.props_type
[4] == data_types
.FLOAT64
)
234 return elem_prop
.props
[4]
238 def elem_props_get_integer(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 if elem_prop
.props
[1] == b
'int':
243 assert(elem_prop
.props
[1] == b
'int')
244 assert(elem_prop
.props
[2] == b
'Integer')
245 elif elem_prop
.props
[1] == b
'ULongLong':
246 assert(elem_prop
.props
[1] == b
'ULongLong')
247 assert(elem_prop
.props
[2] == b
'')
249 # we could allow other number types
250 assert(elem_prop
.props_type
[4] in {data_types
.INT32
, data_types
.INT64
})
252 return elem_prop
.props
[4]
256 def elem_props_get_bool(elem
, elem_prop_id
, default
=None):
257 elem_prop
= elem_props_find_first(elem
, elem_prop_id
)
258 if elem_prop
is not None:
259 assert(elem_prop
.props
[0] == elem_prop_id
)
260 # b'Bool' with a capital seems to be used for animated property... go figure...
261 assert(elem_prop
.props
[1] in {b
'bool', b
'Bool'})
262 assert(elem_prop
.props
[2] == b
'')
264 # we could allow other number types
265 assert(elem_prop
.props_type
[4] == data_types
.INT32
)
266 assert(elem_prop
.props
[4] in {0, 1})
268 return bool(elem_prop
.props
[4])
272 def elem_props_get_enum(elem
, elem_prop_id
, default
=None):
273 elem_prop
= elem_props_find_first(elem
, elem_prop_id
)
274 if elem_prop
is not None:
275 assert(elem_prop
.props
[0] == elem_prop_id
)
276 assert(elem_prop
.props
[1] == b
'enum')
277 assert(elem_prop
.props
[2] == b
'')
278 assert(elem_prop
.props
[3] == b
'')
280 # we could allow other number types
281 assert(elem_prop
.props_type
[4] == data_types
.INT32
)
283 return elem_prop
.props
[4]
287 def elem_props_get_visibility(elem
, elem_prop_id
, default
=None):
288 elem_prop
= elem_props_find_first(elem
, elem_prop_id
)
289 if elem_prop
is not None:
290 assert(elem_prop
.props
[0] == elem_prop_id
)
291 assert(elem_prop
.props
[1] == b
'Visibility')
292 assert(elem_prop
.props
[2] == b
'')
294 # we could allow other number types
295 assert(elem_prop
.props_type
[4] == data_types
.FLOAT64
)
297 return elem_prop
.props
[4]
301 # ----------------------------------------------------------------------------
306 from collections
import namedtuple
309 FBXTransformData
= namedtuple("FBXTransformData", (
311 "rot", "rot_ofs", "rot_piv", "pre_rot", "pst_rot", "rot_ord", "rot_alt_mat", "geom_rot",
312 "sca", "sca_ofs", "sca_piv", "geom_sca",
316 def blen_read_custom_properties(fbx_obj
, blen_obj
, settings
):
317 # There doesn't seem to be a way to put user properties into templates, so this only get the object properties:
318 fbx_obj_props
= elem_find_first(fbx_obj
, b
'Properties70')
320 for fbx_prop
in fbx_obj_props
.elems
:
321 assert(fbx_prop
.id == b
'P')
323 if b
'U' in fbx_prop
.props
[3]:
324 if fbx_prop
.props
[0] == b
'UDP3DSMAX':
325 # Special case for 3DS Max user properties:
326 assert(fbx_prop
.props
[1] == b
'KString')
327 assert(fbx_prop
.props_type
[4] == data_types
.STRING
)
328 items
= fbx_prop
.props
[4].decode('utf-8', 'replace')
329 for item
in items
.split('\r\n'):
331 prop_name
, prop_value
= item
.split('=', 1)
332 prop_name
= validate_blend_names(prop_name
.strip().encode('utf-8'))
333 blen_obj
[prop_name
] = prop_value
.strip()
335 prop_name
= validate_blend_names(fbx_prop
.props
[0])
336 prop_type
= fbx_prop
.props
[1]
337 if prop_type
in {b
'Vector', b
'Vector3D', b
'Color', b
'ColorRGB'}:
338 assert(fbx_prop
.props_type
[4:7] == bytes((data_types
.FLOAT64
,)) * 3)
339 blen_obj
[prop_name
] = fbx_prop
.props
[4:7]
340 elif prop_type
in {b
'Vector4', b
'ColorRGBA'}:
341 assert(fbx_prop
.props_type
[4:8] == bytes((data_types
.FLOAT64
,)) * 4)
342 blen_obj
[prop_name
] = fbx_prop
.props
[4:8]
343 elif prop_type
== b
'Vector2D':
344 assert(fbx_prop
.props_type
[4:6] == bytes((data_types
.FLOAT64
,)) * 2)
345 blen_obj
[prop_name
] = fbx_prop
.props
[4:6]
346 elif prop_type
in {b
'Integer', b
'int'}:
347 assert(fbx_prop
.props_type
[4] == data_types
.INT32
)
348 blen_obj
[prop_name
] = fbx_prop
.props
[4]
349 elif prop_type
== b
'KString':
350 assert(fbx_prop
.props_type
[4] == data_types
.STRING
)
351 blen_obj
[prop_name
] = fbx_prop
.props
[4].decode('utf-8', 'replace')
352 elif prop_type
in {b
'Number', b
'double', b
'Double'}:
353 assert(fbx_prop
.props_type
[4] == data_types
.FLOAT64
)
354 blen_obj
[prop_name
] = fbx_prop
.props
[4]
355 elif prop_type
in {b
'Float', b
'float'}:
356 assert(fbx_prop
.props_type
[4] == data_types
.FLOAT32
)
357 blen_obj
[prop_name
] = fbx_prop
.props
[4]
358 elif prop_type
in {b
'Bool', b
'bool'}:
359 assert(fbx_prop
.props_type
[4] == data_types
.INT32
)
360 blen_obj
[prop_name
] = fbx_prop
.props
[4] != 0
361 elif prop_type
in {b
'Enum', b
'enum'}:
362 assert(fbx_prop
.props_type
[4:6] == bytes((data_types
.INT32
, data_types
.STRING
)))
363 val
= fbx_prop
.props
[4]
364 if settings
.use_custom_props_enum_as_string
and fbx_prop
.props
[5]:
365 enum_items
= fbx_prop
.props
[5].decode('utf-8', 'replace').split('~')
366 assert(val
>= 0 and val
< len(enum_items
))
367 blen_obj
[prop_name
] = enum_items
[val
]
369 blen_obj
[prop_name
] = val
371 print ("WARNING: User property type '%s' is not supported" % prop_type
.decode('utf-8', 'replace'))
374 def blen_read_object_transform_do(transform_data
):
375 # This is a nightmare. FBX SDK uses Maya way to compute the transformation matrix of a node - utterly simple:
377 # WorldTransform = ParentWorldTransform @ T @ Roff @ Rp @ Rpre @ R @ Rpost @ Rp-1 @ Soff @ Sp @ S @ Sp-1
379 # Where all those terms are 4 x 4 matrices that contain:
380 # WorldTransform: Transformation matrix of the node in global space.
381 # ParentWorldTransform: Transformation matrix of the parent node in global space.
383 # Roff: Rotation offset
387 # Rpost: Post-rotation
388 # Rp-1: Inverse of the rotation pivot
389 # Soff: Scaling offset
392 # Sp-1: Inverse of the scaling pivot
394 # But it was still too simple, and FBX notion of compatibility is... quite specific. So we also have to
395 # support 3DSMax way:
397 # WorldTransform = ParentWorldTransform @ T @ R @ S @ OT @ OR @ OS
399 # Where all those terms are 4 x 4 matrices that contain:
400 # WorldTransform: Transformation matrix of the node in global space
401 # ParentWorldTransform: Transformation matrix of the parent node in global space
405 # OT: Geometric transform translation
406 # OR: Geometric transform rotation
407 # OS: Geometric transform translation
410 # Geometric transformations ***are not inherited***: ParentWorldTransform does not contain the OT, OR, OS
411 # of WorldTransform's parent node.
413 # Taken from http://download.autodesk.com/us/fbx/20112/FBX_SDK_HELP/
414 # index.html?url=WS1a9193826455f5ff1f92379812724681e696651.htm,topicNumber=d0e7429
417 lcl_translation
= Matrix
.Translation(transform_data
.loc
)
418 geom_loc
= Matrix
.Translation(transform_data
.geom_loc
)
421 to_rot
= lambda rot
, rot_ord
: Euler(convert_deg_to_rad_iter(rot
), rot_ord
).to_matrix().to_4x4()
422 lcl_rot
= to_rot(transform_data
.rot
, transform_data
.rot_ord
) @ transform_data
.rot_alt_mat
423 pre_rot
= to_rot(transform_data
.pre_rot
, transform_data
.rot_ord
)
424 pst_rot
= to_rot(transform_data
.pst_rot
, transform_data
.rot_ord
)
425 geom_rot
= to_rot(transform_data
.geom_rot
, transform_data
.rot_ord
)
427 rot_ofs
= Matrix
.Translation(transform_data
.rot_ofs
)
428 rot_piv
= Matrix
.Translation(transform_data
.rot_piv
)
429 sca_ofs
= Matrix
.Translation(transform_data
.sca_ofs
)
430 sca_piv
= Matrix
.Translation(transform_data
.sca_piv
)
434 lcl_scale
[0][0], lcl_scale
[1][1], lcl_scale
[2][2] = transform_data
.sca
435 geom_scale
= Matrix();
436 geom_scale
[0][0], geom_scale
[1][1], geom_scale
[2][2] = transform_data
.geom_sca
445 rot_piv
.inverted_safe() @
449 sca_piv
.inverted_safe()
451 geom_mat
= geom_loc
@ geom_rot
@ geom_scale
452 # We return mat without 'geometric transforms' too, because it is to be used for children, sigh...
453 return (base_mat
@ geom_mat
, base_mat
, geom_mat
)
456 # XXX This might be weak, now that we can add vgroups from both bones and shapes, name collisions become
457 # more likely, will have to make this more robust!!!
458 def add_vgroup_to_objects(vg_indices
, vg_weights
, vg_name
, objects
):
459 assert(len(vg_indices
) == len(vg_weights
))
462 # We replace/override here...
463 vg
= obj
.vertex_groups
.get(vg_name
)
465 vg
= obj
.vertex_groups
.new(name
=vg_name
)
466 for i
, w
in zip(vg_indices
, vg_weights
):
467 vg
.add((i
,), w
, 'REPLACE')
470 def blen_read_object_transform_preprocess(fbx_props
, fbx_obj
, rot_alt_mat
, use_prepost_rot
):
471 # This is quite involved, 'fbxRNode.cpp' from openscenegraph used as a reference
472 const_vector_zero_3d
= 0.0, 0.0, 0.0
473 const_vector_one_3d
= 1.0, 1.0, 1.0
475 loc
= list(elem_props_get_vector_3d(fbx_props
, b
'Lcl Translation', const_vector_zero_3d
))
476 rot
= list(elem_props_get_vector_3d(fbx_props
, b
'Lcl Rotation', const_vector_zero_3d
))
477 sca
= list(elem_props_get_vector_3d(fbx_props
, b
'Lcl Scaling', const_vector_one_3d
))
479 geom_loc
= list(elem_props_get_vector_3d(fbx_props
, b
'GeometricTranslation', const_vector_zero_3d
))
480 geom_rot
= list(elem_props_get_vector_3d(fbx_props
, b
'GeometricRotation', const_vector_zero_3d
))
481 geom_sca
= list(elem_props_get_vector_3d(fbx_props
, b
'GeometricScaling', const_vector_one_3d
))
483 rot_ofs
= elem_props_get_vector_3d(fbx_props
, b
'RotationOffset', const_vector_zero_3d
)
484 rot_piv
= elem_props_get_vector_3d(fbx_props
, b
'RotationPivot', const_vector_zero_3d
)
485 sca_ofs
= elem_props_get_vector_3d(fbx_props
, b
'ScalingOffset', const_vector_zero_3d
)
486 sca_piv
= elem_props_get_vector_3d(fbx_props
, b
'ScalingPivot', const_vector_zero_3d
)
488 is_rot_act
= elem_props_get_bool(fbx_props
, b
'RotationActive', False)
492 pre_rot
= elem_props_get_vector_3d(fbx_props
, b
'PreRotation', const_vector_zero_3d
)
493 pst_rot
= elem_props_get_vector_3d(fbx_props
, b
'PostRotation', const_vector_zero_3d
)
495 pre_rot
= const_vector_zero_3d
496 pst_rot
= const_vector_zero_3d
504 6: 'XYZ', # XXX eSphericXYZ, not really supported...
505 }.get(elem_props_get_enum(fbx_props
, b
'RotationOrder', 0))
507 pre_rot
= const_vector_zero_3d
508 pst_rot
= const_vector_zero_3d
511 return FBXTransformData(loc
, geom_loc
,
512 rot
, rot_ofs
, rot_piv
, pre_rot
, pst_rot
, rot_ord
, rot_alt_mat
, geom_rot
,
513 sca
, sca_ofs
, sca_piv
, geom_sca
)
518 def blen_read_animations_curves_iter(fbx_curves
, blen_start_offset
, fbx_start_offset
, fps
):
520 Get raw FBX AnimCurve list, and yield values for all curves at each singular curves' keyframes,
521 together with (blender) timing, in frames.
522 blen_start_offset is expected in frames, while fbx_start_offset is expected in FBX ktime.
524 # As a first step, assume linear interpolation between key frames, we'll (try to!) handle more
525 # of FBX curves later.
526 from .fbx_utils
import FBX_KTIME
527 timefac
= fps
/ FBX_KTIME
530 elem_prop_first(elem_find_first(c
[2], b
'KeyTime')),
531 elem_prop_first(elem_find_first(c
[2], b
'KeyValueFloat')),
535 allkeys
= sorted({item
for sublist
in curves
for item
in sublist
[1]})
536 for curr_fbxktime
in allkeys
:
539 idx
, times
, values
, fbx_curve
= item
541 if times
[idx
] < curr_fbxktime
:
544 if idx
>= len(times
):
545 # We have reached our last element for this curve, stay on it from now on...
549 if times
[idx
] >= curr_fbxktime
:
551 curr_values
.append((values
[idx
], fbx_curve
))
553 # Interpolate between this key and the previous one.
554 ifac
= (curr_fbxktime
- times
[idx
- 1]) / (times
[idx
] - times
[idx
- 1])
555 curr_values
.append(((values
[idx
] - values
[idx
- 1]) * ifac
+ values
[idx
- 1], fbx_curve
))
556 curr_blenkframe
= (curr_fbxktime
- fbx_start_offset
) * timefac
+ blen_start_offset
557 yield (curr_blenkframe
, curr_values
)
560 def blen_read_animations_action_item(action
, item
, cnodes
, fps
, anim_offset
):
562 'Bake' loc/rot/scale into the action,
563 taking any pre_ and post_ matrix into account to transform from fbx into blender space.
565 from bpy
.types
import Object
, PoseBone
, ShapeKey
, Material
, Camera
566 from itertools
import chain
569 for curves
, fbxprop
in cnodes
.values():
570 for (fbx_acdata
, _blen_data
), channel
in curves
.values():
571 fbx_curves
.append((fbxprop
, channel
, fbx_acdata
))
573 # Leave if no curves are attached (if a blender curve is attached to scale but without keys it defaults to 0).
574 if len(fbx_curves
) == 0:
580 if isinstance(item
, Material
):
582 props
= [("diffuse_color", 3, grpname
or "Diffuse Color")]
583 elif isinstance(item
, ShapeKey
):
584 props
= [(item
.path_from_id("value"), 1, "Key")]
585 elif isinstance(item
, Camera
):
586 props
= [(item
.path_from_id("lens"), 1, "Camera")]
587 else: # Object or PoseBone:
589 bl_obj
= item
.bl_obj
.pose
.bones
[item
.bl_bone
]
593 # We want to create actions for objects, but for bones we 'reuse' armatures' actions!
594 grpname
= item
.bl_obj
.name
596 # Since we might get other channels animated in the end, due to all FBX transform magic,
597 # we need to add curves for whole loc/rot/scale in any case.
598 props
= [(bl_obj
.path_from_id("location"), 3, grpname
or "Location"),
600 (bl_obj
.path_from_id("scale"), 3, grpname
or "Scale")]
601 rot_mode
= bl_obj
.rotation_mode
602 if rot_mode
== 'QUATERNION':
603 props
[1] = (bl_obj
.path_from_id("rotation_quaternion"), 4, grpname
or "Quaternion Rotation")
604 elif rot_mode
== 'AXIS_ANGLE':
605 props
[1] = (bl_obj
.path_from_id("rotation_axis_angle"), 4, grpname
or "Axis Angle Rotation")
607 props
[1] = (bl_obj
.path_from_id("rotation_euler"), 3, grpname
or "Euler Rotation")
609 blen_curves
= [action
.fcurves
.new(prop
, index
=channel
, action_group
=grpname
)
610 for prop
, nbr_channels
, grpname
in props
for channel
in range(nbr_channels
)]
612 if isinstance(item
, Material
):
613 for frame
, values
in blen_read_animations_curves_iter(fbx_curves
, anim_offset
, 0, fps
):
615 for v
, (fbxprop
, channel
, _fbx_acdata
) in values
:
616 assert(fbxprop
== b
'DiffuseColor')
617 assert(channel
in {0, 1, 2})
620 for fc
, v
in zip(blen_curves
, value
):
621 fc
.keyframe_points
.insert(frame
, v
, options
={'NEEDED', 'FAST'}).interpolation
= 'LINEAR'
623 elif isinstance(item
, ShapeKey
):
624 for frame
, values
in blen_read_animations_curves_iter(fbx_curves
, anim_offset
, 0, fps
):
626 for v
, (fbxprop
, channel
, _fbx_acdata
) in values
:
627 assert(fbxprop
== b
'DeformPercent')
631 for fc
, v
in zip(blen_curves
, (value
,)):
632 fc
.keyframe_points
.insert(frame
, v
, options
={'NEEDED', 'FAST'}).interpolation
= 'LINEAR'
634 elif isinstance(item
, Camera
):
635 for frame
, values
in blen_read_animations_curves_iter(fbx_curves
, anim_offset
, 0, fps
):
637 for v
, (fbxprop
, channel
, _fbx_acdata
) in values
:
638 assert(fbxprop
== b
'FocalLength')
642 for fc
, v
in zip(blen_curves
, (value
,)):
643 fc
.keyframe_points
.insert(frame
, v
, options
={'NEEDED', 'FAST'}).interpolation
= 'LINEAR'
645 else: # Object or PoseBone:
647 bl_obj
= item
.bl_obj
.pose
.bones
[item
.bl_bone
]
651 transform_data
= item
.fbx_transform_data
652 rot_eul_prev
= bl_obj
.rotation_euler
.copy()
653 rot_quat_prev
= bl_obj
.rotation_quaternion
.copy()
656 # Pre-compute inverted local rest matrix of the bone, if relevant.
657 restmat_inv
= item
.get_bind_matrix().inverted_safe() if item
.is_bone
else None
659 for frame
, values
in blen_read_animations_curves_iter(fbx_curves
, anim_offset
, 0, fps
):
660 for v
, (fbxprop
, channel
, _fbx_acdata
) in values
:
661 if fbxprop
== b
'Lcl Translation':
662 transform_data
.loc
[channel
] = v
663 elif fbxprop
== b
'Lcl Rotation':
664 transform_data
.rot
[channel
] = v
665 elif fbxprop
== b
'Lcl Scaling':
666 transform_data
.sca
[channel
] = v
667 mat
, _
, _
= blen_read_object_transform_do(transform_data
)
669 # compensate for changes in the local matrix during processing
670 if item
.anim_compensation_matrix
:
671 mat
= mat
@ item
.anim_compensation_matrix
673 # apply pre- and post matrix
674 # post-matrix will contain any correction for lights, camera and bone orientation
675 # pre-matrix will contain any correction for a parent's correction matrix or the global matrix
677 mat
= item
.pre_matrix
@ mat
679 mat
= mat
@ item
.post_matrix
681 # And now, remove that rest pose matrix from current mat (also in parent space).
683 mat
= restmat_inv
@ mat
685 # Now we have a virtual matrix of transform from AnimCurves, we can insert keyframes!
686 loc
, rot
, sca
= mat
.decompose()
687 if rot_mode
== 'QUATERNION':
688 if rot_quat_prev
.dot(rot
) < 0.0:
691 elif rot_mode
== 'AXIS_ANGLE':
692 vec
, ang
= rot
.to_axis_angle()
693 rot
= ang
, vec
.x
, vec
.y
, vec
.z
695 rot
= rot
.to_euler(rot_mode
, rot_eul_prev
)
697 for fc
, value
in zip(blen_curves
, chain(loc
, rot
, sca
)):
698 fc
.keyframe_points
.insert(frame
, value
, options
={'NEEDED', 'FAST'}).interpolation
= 'LINEAR'
700 # Since we inserted our keyframes in 'FAST' mode, we have to update the fcurves now.
701 for fc
in blen_curves
:
705 def blen_read_animations(fbx_tmpl_astack
, fbx_tmpl_alayer
, stacks
, scene
, anim_offset
):
707 Recreate an action per stack/layer/object combinations.
708 Only the first found action is linked to objects, more complex setups are not handled,
709 it's up to user to reproduce them!
711 from bpy
.types
import ShapeKey
, Material
, Camera
714 for as_uuid
, ((fbx_asdata
, _blen_data
), alayers
) in stacks
.items():
715 stack_name
= elem_name_ensure_class(fbx_asdata
, b
'AnimStack')
716 for al_uuid
, ((fbx_aldata
, _blen_data
), items
) in alayers
.items():
717 layer_name
= elem_name_ensure_class(fbx_aldata
, b
'AnimLayer')
718 for item
, cnodes
in items
.items():
719 if isinstance(item
, Material
):
721 elif isinstance(item
, ShapeKey
):
722 id_data
= item
.id_data
723 elif isinstance(item
, Camera
):
726 id_data
= item
.bl_obj
727 # XXX Ignore rigged mesh animations - those are a nightmare to handle, see note about it in
728 # FbxImportHelperNode class definition.
729 if id_data
and id_data
.type == 'MESH' and id_data
.parent
and id_data
.parent
.type == 'ARMATURE':
734 # Create new action if needed (should always be needed, except for keyblocks from shapekeys cases).
735 key
= (as_uuid
, al_uuid
, id_data
)
736 action
= actions
.get(key
)
738 action_name
= "|".join((id_data
.name
, stack_name
, layer_name
))
739 actions
[key
] = action
= bpy
.data
.actions
.new(action_name
)
740 action
.use_fake_user
= True
741 # If none yet assigned, assign this action to id_data.
742 if not id_data
.animation_data
:
743 id_data
.animation_data_create()
744 if not id_data
.animation_data
.action
:
745 id_data
.animation_data
.action
= action
746 # And actually populate the action!
747 blen_read_animations_action_item(action
, item
, cnodes
, scene
.render
.fps
, anim_offset
)
753 def blen_read_geom_layerinfo(fbx_layer
):
755 validate_blend_names(elem_find_first_string_as_bytes(fbx_layer
, b
'Name')),
756 elem_find_first_string_as_bytes(fbx_layer
, b
'MappingInformationType'),
757 elem_find_first_string_as_bytes(fbx_layer
, b
'ReferenceInformationType'),
761 def blen_read_geom_array_setattr(generator
, blen_data
, blen_attr
, fbx_data
, stride
, item_size
, descr
, xform
):
762 """Generic fbx_layer to blen_data setter, generator is expected to yield tuples (ble_idx, fbx_idx)."""
763 max_idx
= len(blen_data
) - 1
766 def check_skip(blen_idx
, fbx_idx
):
768 if fbx_idx
< 0: # Negative values mean 'skip'.
770 if blen_idx
> max_idx
:
772 print("ERROR: too much data in this layer, compared to elements in mesh, skipping!")
777 if xform
is not None:
778 if isinstance(blen_data
, list):
780 def _process(blend_data
, blen_attr
, fbx_data
, xform
, item_size
, blen_idx
, fbx_idx
):
781 blen_data
[blen_idx
] = xform(fbx_data
[fbx_idx
])
783 def _process(blend_data
, blen_attr
, fbx_data
, xform
, item_size
, blen_idx
, fbx_idx
):
784 blen_data
[blen_idx
] = xform(fbx_data
[fbx_idx
:fbx_idx
+ item_size
])
787 def _process(blend_data
, blen_attr
, fbx_data
, xform
, item_size
, blen_idx
, fbx_idx
):
788 setattr(blen_data
[blen_idx
], blen_attr
, xform(fbx_data
[fbx_idx
]))
790 def _process(blend_data
, blen_attr
, fbx_data
, xform
, item_size
, blen_idx
, fbx_idx
):
791 setattr(blen_data
[blen_idx
], blen_attr
, xform(fbx_data
[fbx_idx
:fbx_idx
+ item_size
]))
793 if isinstance(blen_data
, list):
795 def _process(blend_data
, blen_attr
, fbx_data
, xform
, item_size
, blen_idx
, fbx_idx
):
796 blen_data
[blen_idx
] = fbx_data
[fbx_idx
]
798 def _process(blend_data
, blen_attr
, fbx_data
, xform
, item_size
, blen_idx
, fbx_idx
):
799 blen_data
[blen_idx
] = fbx_data
[fbx_idx
:fbx_idx
+ item_size
]
802 def _process(blend_data
, blen_attr
, fbx_data
, xform
, item_size
, blen_idx
, fbx_idx
):
803 setattr(blen_data
[blen_idx
], blen_attr
, fbx_data
[fbx_idx
])
805 def _process(blend_data
, blen_attr
, fbx_data
, xform
, item_size
, blen_idx
, fbx_idx
):
806 setattr(blen_data
[blen_idx
], blen_attr
, fbx_data
[fbx_idx
:fbx_idx
+ item_size
])
808 for blen_idx
, fbx_idx
in generator
:
809 if check_skip(blen_idx
, fbx_idx
):
811 _process(blen_data
, blen_attr
, fbx_data
, xform
, item_size
, blen_idx
, fbx_idx
)
814 # generic generators.
815 def blen_read_geom_array_gen_allsame(data_len
):
816 return zip(*(range(data_len
), (0,) * data_len
))
819 def blen_read_geom_array_gen_direct(fbx_data
, stride
):
820 fbx_data_len
= len(fbx_data
)
821 return zip(*(range(fbx_data_len
// stride
), range(0, fbx_data_len
, stride
)))
824 def blen_read_geom_array_gen_indextodirect(fbx_layer_index
, stride
):
825 return ((bi
, fi
* stride
) for bi
, fi
in enumerate(fbx_layer_index
))
828 def blen_read_geom_array_gen_direct_looptovert(mesh
, fbx_data
, stride
):
829 fbx_data_len
= len(fbx_data
) // stride
831 for p
in mesh
.polygons
:
832 for lidx
in p
.loop_indices
:
833 vidx
= loops
[lidx
].vertex_index
834 if vidx
< fbx_data_len
:
835 yield lidx
, vidx
* stride
838 # generic error printers.
839 def blen_read_geom_array_error_mapping(descr
, fbx_layer_mapping
, quiet
=False):
841 print("warning layer %r mapping type unsupported: %r" % (descr
, fbx_layer_mapping
))
844 def blen_read_geom_array_error_ref(descr
, fbx_layer_ref
, quiet
=False):
846 print("warning layer %r ref type unsupported: %r" % (descr
, fbx_layer_ref
))
849 def blen_read_geom_array_mapped_vert(
850 mesh
, blen_data
, blen_attr
,
851 fbx_layer_data
, fbx_layer_index
,
852 fbx_layer_mapping
, fbx_layer_ref
,
853 stride
, item_size
, descr
,
854 xform
=None, quiet
=False,
856 if fbx_layer_mapping
== b
'ByVertice':
857 if fbx_layer_ref
== b
'Direct':
858 assert(fbx_layer_index
is None)
859 blen_read_geom_array_setattr(blen_read_geom_array_gen_direct(fbx_layer_data
, stride
),
860 blen_data
, blen_attr
, fbx_layer_data
, stride
, item_size
, descr
, xform
)
862 blen_read_geom_array_error_ref(descr
, fbx_layer_ref
, quiet
)
863 elif fbx_layer_mapping
== b
'AllSame':
864 if fbx_layer_ref
== b
'IndexToDirect':
865 assert(fbx_layer_index
is None)
866 blen_read_geom_array_setattr(blen_read_geom_array_gen_allsame(len(blen_data
)),
867 blen_data
, blen_attr
, fbx_layer_data
, stride
, item_size
, descr
, xform
)
869 blen_read_geom_array_error_ref(descr
, fbx_layer_ref
, quiet
)
871 blen_read_geom_array_error_mapping(descr
, fbx_layer_mapping
, quiet
)
876 def blen_read_geom_array_mapped_edge(
877 mesh
, blen_data
, blen_attr
,
878 fbx_layer_data
, fbx_layer_index
,
879 fbx_layer_mapping
, fbx_layer_ref
,
880 stride
, item_size
, descr
,
881 xform
=None, quiet
=False,
883 if fbx_layer_mapping
== b
'ByEdge':
884 if fbx_layer_ref
== b
'Direct':
885 blen_read_geom_array_setattr(blen_read_geom_array_gen_direct(fbx_layer_data
, stride
),
886 blen_data
, blen_attr
, fbx_layer_data
, stride
, item_size
, descr
, xform
)
888 blen_read_geom_array_error_ref(descr
, fbx_layer_ref
, quiet
)
889 elif fbx_layer_mapping
== b
'AllSame':
890 if fbx_layer_ref
== b
'IndexToDirect':
891 assert(fbx_layer_index
is None)
892 blen_read_geom_array_setattr(blen_read_geom_array_gen_allsame(len(blen_data
)),
893 blen_data
, blen_attr
, fbx_layer_data
, stride
, item_size
, descr
, xform
)
895 blen_read_geom_array_error_ref(descr
, fbx_layer_ref
, quiet
)
897 blen_read_geom_array_error_mapping(descr
, fbx_layer_mapping
, quiet
)
902 def blen_read_geom_array_mapped_polygon(
903 mesh
, blen_data
, blen_attr
,
904 fbx_layer_data
, fbx_layer_index
,
905 fbx_layer_mapping
, fbx_layer_ref
,
906 stride
, item_size
, descr
,
907 xform
=None, quiet
=False,
909 if fbx_layer_mapping
== b
'ByPolygon':
910 if fbx_layer_ref
== b
'IndexToDirect':
911 # XXX Looks like we often get no fbx_layer_index in this case, shall not happen but happens...
912 # We fallback to 'Direct' mapping in this case.
913 #~ assert(fbx_layer_index is not None)
914 if fbx_layer_index
is None:
915 blen_read_geom_array_setattr(blen_read_geom_array_gen_direct(fbx_layer_data
, stride
),
916 blen_data
, blen_attr
, fbx_layer_data
, stride
, item_size
, descr
, xform
)
918 blen_read_geom_array_setattr(blen_read_geom_array_gen_indextodirect(fbx_layer_index
, stride
),
919 blen_data
, blen_attr
, fbx_layer_data
, stride
, item_size
, descr
, xform
)
921 elif fbx_layer_ref
== b
'Direct':
922 blen_read_geom_array_setattr(blen_read_geom_array_gen_direct(fbx_layer_data
, stride
),
923 blen_data
, blen_attr
, fbx_layer_data
, stride
, item_size
, descr
, xform
)
925 blen_read_geom_array_error_ref(descr
, fbx_layer_ref
, quiet
)
926 elif fbx_layer_mapping
== b
'AllSame':
927 if fbx_layer_ref
== b
'IndexToDirect':
928 assert(fbx_layer_index
is None)
929 blen_read_geom_array_setattr(blen_read_geom_array_gen_allsame(len(blen_data
)),
930 blen_data
, blen_attr
, fbx_layer_data
, stride
, item_size
, descr
, xform
)
932 blen_read_geom_array_error_ref(descr
, fbx_layer_ref
, quiet
)
934 blen_read_geom_array_error_mapping(descr
, fbx_layer_mapping
, quiet
)
939 def blen_read_geom_array_mapped_polyloop(
940 mesh
, blen_data
, blen_attr
,
941 fbx_layer_data
, fbx_layer_index
,
942 fbx_layer_mapping
, fbx_layer_ref
,
943 stride
, item_size
, descr
,
944 xform
=None, quiet
=False,
946 if fbx_layer_mapping
== b
'ByPolygonVertex':
947 if fbx_layer_ref
== b
'IndexToDirect':
948 # XXX Looks like we often get no fbx_layer_index in this case, shall not happen but happens...
949 # We fallback to 'Direct' mapping in this case.
950 #~ assert(fbx_layer_index is not None)
951 if fbx_layer_index
is None:
952 blen_read_geom_array_setattr(blen_read_geom_array_gen_direct(fbx_layer_data
, stride
),
953 blen_data
, blen_attr
, fbx_layer_data
, stride
, item_size
, descr
, xform
)
955 blen_read_geom_array_setattr(blen_read_geom_array_gen_indextodirect(fbx_layer_index
, stride
),
956 blen_data
, blen_attr
, fbx_layer_data
, stride
, item_size
, descr
, xform
)
958 elif fbx_layer_ref
== b
'Direct':
959 blen_read_geom_array_setattr(blen_read_geom_array_gen_direct(fbx_layer_data
, stride
),
960 blen_data
, blen_attr
, fbx_layer_data
, stride
, item_size
, descr
, xform
)
962 blen_read_geom_array_error_ref(descr
, fbx_layer_ref
, quiet
)
963 elif fbx_layer_mapping
== b
'ByVertice':
964 if fbx_layer_ref
== b
'Direct':
965 assert(fbx_layer_index
is None)
966 blen_read_geom_array_setattr(blen_read_geom_array_gen_direct_looptovert(mesh
, fbx_layer_data
, stride
),
967 blen_data
, blen_attr
, fbx_layer_data
, stride
, item_size
, descr
, xform
)
969 blen_read_geom_array_error_ref(descr
, fbx_layer_ref
, quiet
)
970 elif fbx_layer_mapping
== b
'AllSame':
971 if fbx_layer_ref
== b
'IndexToDirect':
972 assert(fbx_layer_index
is None)
973 blen_read_geom_array_setattr(blen_read_geom_array_gen_allsame(len(blen_data
)),
974 blen_data
, blen_attr
, fbx_layer_data
, stride
, item_size
, descr
, xform
)
976 blen_read_geom_array_error_ref(descr
, fbx_layer_ref
, quiet
)
978 blen_read_geom_array_error_mapping(descr
, fbx_layer_mapping
, quiet
)
983 def blen_read_geom_layer_material(fbx_obj
, mesh
):
984 fbx_layer
= elem_find_first(fbx_obj
, b
'LayerElementMaterial')
986 if fbx_layer
is None:
992 ) = blen_read_geom_layerinfo(fbx_layer
)
994 layer_id
= b
'Materials'
995 fbx_layer_data
= elem_prop_first(elem_find_first(fbx_layer
, layer_id
))
997 blen_data
= mesh
.polygons
998 blen_read_geom_array_mapped_polygon(
999 mesh
, blen_data
, "material_index",
1000 fbx_layer_data
, None,
1001 fbx_layer_mapping
, fbx_layer_ref
,
1006 def blen_read_geom_layer_uv(fbx_obj
, mesh
):
1007 for layer_id
in (b
'LayerElementUV',):
1008 for fbx_layer
in elem_find_iter(fbx_obj
, layer_id
):
1009 # all should be valid
1013 ) = blen_read_geom_layerinfo(fbx_layer
)
1015 fbx_layer_data
= elem_prop_first(elem_find_first(fbx_layer
, b
'UV'))
1016 fbx_layer_index
= elem_prop_first(elem_find_first(fbx_layer
, b
'UVIndex'))
1018 # Always init our new layers with (0, 0) UVs.
1019 uv_lay
= mesh
.uv_layers
.new(name
=fbx_layer_name
, do_init
=False)
1021 print("Failed to add {%r %r} UVLayer to %r (probably too many of them?)"
1022 "" % (layer_id
, fbx_layer_name
, mesh
.name
))
1025 blen_data
= uv_lay
.data
1027 # some valid files omit this data
1028 if fbx_layer_data
is None:
1029 print("%r %r missing data" % (layer_id
, fbx_layer_name
))
1032 blen_read_geom_array_mapped_polyloop(
1033 mesh
, blen_data
, "uv",
1034 fbx_layer_data
, fbx_layer_index
,
1035 fbx_layer_mapping
, fbx_layer_ref
,
1040 def blen_read_geom_layer_color(fbx_obj
, mesh
):
1041 # almost same as UV's
1042 for layer_id
in (b
'LayerElementColor',):
1043 for fbx_layer
in elem_find_iter(fbx_obj
, layer_id
):
1044 # all should be valid
1048 ) = blen_read_geom_layerinfo(fbx_layer
)
1050 fbx_layer_data
= elem_prop_first(elem_find_first(fbx_layer
, b
'Colors'))
1051 fbx_layer_index
= elem_prop_first(elem_find_first(fbx_layer
, b
'ColorIndex'))
1053 # Always init our new layers with full white opaque color.
1054 color_lay
= mesh
.vertex_colors
.new(name
=fbx_layer_name
, do_init
=False)
1055 blen_data
= color_lay
.data
1057 # some valid files omit this data
1058 if fbx_layer_data
is None:
1059 print("%r %r missing data" % (layer_id
, fbx_layer_name
))
1062 blen_read_geom_array_mapped_polyloop(
1063 mesh
, blen_data
, "color",
1064 fbx_layer_data
, fbx_layer_index
,
1065 fbx_layer_mapping
, fbx_layer_ref
,
1070 def blen_read_geom_layer_smooth(fbx_obj
, mesh
):
1071 fbx_layer
= elem_find_first(fbx_obj
, b
'LayerElementSmoothing')
1073 if fbx_layer
is None:
1076 # all should be valid
1080 ) = blen_read_geom_layerinfo(fbx_layer
)
1082 layer_id
= b
'Smoothing'
1083 fbx_layer_data
= elem_prop_first(elem_find_first(fbx_layer
, layer_id
))
1085 # udk has 'Direct' mapped, with no Smoothing, not sure why, but ignore these
1086 if fbx_layer_data
is None:
1089 if fbx_layer_mapping
== b
'ByEdge':
1090 # some models have bad edge data, we cant use this info...
1092 print("warning skipping sharp edges data, no valid edges...")
1095 blen_data
= mesh
.edges
1096 blen_read_geom_array_mapped_edge(
1097 mesh
, blen_data
, "use_edge_sharp",
1098 fbx_layer_data
, None,
1099 fbx_layer_mapping
, fbx_layer_ref
,
1101 xform
=lambda s
: not s
,
1103 # We only set sharp edges here, not face smoothing itself...
1104 mesh
.use_auto_smooth
= True
1106 elif fbx_layer_mapping
== b
'ByPolygon':
1107 blen_data
= mesh
.polygons
1108 return blen_read_geom_array_mapped_polygon(
1109 mesh
, blen_data
, "use_smooth",
1110 fbx_layer_data
, None,
1111 fbx_layer_mapping
, fbx_layer_ref
,
1113 xform
=lambda s
: (s
!= 0), # smoothgroup bitflags, treat as booleans for now
1116 print("warning layer %r mapping type unsupported: %r" % (fbx_layer
.id, fbx_layer_mapping
))
1119 def blen_read_geom_layer_edge_crease(fbx_obj
, mesh
):
1120 from math
import sqrt
1122 fbx_layer
= elem_find_first(fbx_obj
, b
'LayerElementEdgeCrease')
1124 if fbx_layer
is None:
1127 # all should be valid
1131 ) = blen_read_geom_layerinfo(fbx_layer
)
1133 if fbx_layer_mapping
!= b
'ByEdge':
1136 layer_id
= b
'EdgeCrease'
1137 fbx_layer_data
= elem_prop_first(elem_find_first(fbx_layer
, layer_id
))
1139 # some models have bad edge data, we cant use this info...
1141 print("warning skipping edge crease data, no valid edges...")
1144 if fbx_layer_mapping
== b
'ByEdge':
1145 # some models have bad edge data, we cant use this info...
1147 print("warning skipping edge crease data, no valid edges...")
1150 blen_data
= mesh
.edges
1151 return blen_read_geom_array_mapped_edge(
1152 mesh
, blen_data
, "crease",
1153 fbx_layer_data
, None,
1154 fbx_layer_mapping
, fbx_layer_ref
,
1156 # Blender squares those values before sending them to OpenSubdiv, when other softwares don't,
1157 # so we need to compensate that to get similar results through FBX...
1161 print("warning layer %r mapping type unsupported: %r" % (fbx_layer
.id, fbx_layer_mapping
))
1164 def blen_read_geom_layer_normal(fbx_obj
, mesh
, xform
=None):
1165 fbx_layer
= elem_find_first(fbx_obj
, b
'LayerElementNormal')
1167 if fbx_layer
is None:
1173 ) = blen_read_geom_layerinfo(fbx_layer
)
1175 layer_id
= b
'Normals'
1176 fbx_layer_data
= elem_prop_first(elem_find_first(fbx_layer
, layer_id
))
1177 fbx_layer_index
= elem_prop_first(elem_find_first(fbx_layer
, b
'NormalsIndex'))
1179 # try loops, then vertices.
1180 tries
= ((mesh
.loops
, "Loops", False, blen_read_geom_array_mapped_polyloop
),
1181 (mesh
.polygons
, "Polygons", True, blen_read_geom_array_mapped_polygon
),
1182 (mesh
.vertices
, "Vertices", True, blen_read_geom_array_mapped_vert
))
1183 for blen_data
, blen_data_type
, is_fake
, func
in tries
:
1184 bdata
= [None] * len(blen_data
) if is_fake
else blen_data
1185 if func(mesh
, bdata
, "normal",
1186 fbx_layer_data
, fbx_layer_index
, fbx_layer_mapping
, fbx_layer_ref
, 3, 3, layer_id
, xform
, True):
1187 if blen_data_type
== "Polygons":
1188 for pidx
, p
in enumerate(mesh
.polygons
):
1189 for lidx
in range(p
.loop_start
, p
.loop_start
+ p
.loop_total
):
1190 mesh
.loops
[lidx
].normal
[:] = bdata
[pidx
]
1191 elif blen_data_type
== "Vertices":
1192 # We have to copy vnors to lnors! Far from elegant, but simple.
1193 for l
in mesh
.loops
:
1194 l
.normal
[:] = bdata
[l
.vertex_index
]
1197 blen_read_geom_array_error_mapping("normal", fbx_layer_mapping
)
1198 blen_read_geom_array_error_ref("normal", fbx_layer_ref
)
1202 def blen_read_geom(fbx_tmpl
, fbx_obj
, settings
):
1203 from itertools
import chain
1206 # Vertices are in object space, but we are post-multiplying all transforms with the inverse of the
1207 # global matrix, so we need to apply the global matrix to the vertices to get the correct result.
1208 geom_mat_co
= settings
.global_matrix
if settings
.bake_space_transform
else None
1209 # We need to apply the inverse transpose of the global matrix when transforming normals.
1210 geom_mat_no
= Matrix(settings
.global_matrix_inv_transposed
) if settings
.bake_space_transform
else None
1211 if geom_mat_no
is not None:
1212 # Remove translation & scaling!
1213 geom_mat_no
.translation
= Vector()
1214 geom_mat_no
.normalize()
1216 # TODO, use 'fbx_tmpl'
1217 elem_name_utf8
= elem_name_ensure_class(fbx_obj
, b
'Geometry')
1219 fbx_verts
= elem_prop_first(elem_find_first(fbx_obj
, b
'Vertices'))
1220 fbx_polys
= elem_prop_first(elem_find_first(fbx_obj
, b
'PolygonVertexIndex'))
1221 fbx_edges
= elem_prop_first(elem_find_first(fbx_obj
, b
'Edges'))
1223 if geom_mat_co
is not None:
1224 def _vcos_transformed_gen(raw_cos
, m
=None):
1225 # Note: we could most likely get much better performances with numpy, but will leave this as TODO for now.
1226 return chain(*(m
@ Vector(v
) for v
in zip(*(iter(raw_cos
),) * 3)))
1227 fbx_verts
= array
.array(fbx_verts
.typecode
, _vcos_transformed_gen(fbx_verts
, geom_mat_co
))
1229 if fbx_verts
is None:
1231 if fbx_polys
is None:
1234 mesh
= bpy
.data
.meshes
.new(name
=elem_name_utf8
)
1235 mesh
.vertices
.add(len(fbx_verts
) // 3)
1236 mesh
.vertices
.foreach_set("co", fbx_verts
)
1239 mesh
.loops
.add(len(fbx_polys
))
1240 poly_loop_starts
= []
1241 poly_loop_totals
= []
1243 for i
, l
in enumerate(mesh
.loops
):
1244 index
= fbx_polys
[i
]
1246 poly_loop_starts
.append(poly_loop_prev
)
1247 poly_loop_totals
.append((i
- poly_loop_prev
) + 1)
1248 poly_loop_prev
= i
+ 1
1250 l
.vertex_index
= index
1252 mesh
.polygons
.add(len(poly_loop_starts
))
1253 mesh
.polygons
.foreach_set("loop_start", poly_loop_starts
)
1254 mesh
.polygons
.foreach_set("loop_total", poly_loop_totals
)
1256 blen_read_geom_layer_material(fbx_obj
, mesh
)
1257 blen_read_geom_layer_uv(fbx_obj
, mesh
)
1258 blen_read_geom_layer_color(fbx_obj
, mesh
)
1261 # edges in fact index the polygons (NOT the vertices)
1263 tot_edges
= len(fbx_edges
)
1264 edges_conv
= array
.array('i', [0]) * (tot_edges
* 2)
1270 e_b
= fbx_polys
[i
+ 1]
1274 # Last index of polygon, wrap back to the start.
1276 # ideally we wouldn't have to search back,
1277 # but it should only be 2-3 iterations.
1279 while j
>= 0 and fbx_polys
[j
] >= 0:
1282 e_b
= fbx_polys
[j
+ 1]
1284 edges_conv
[edge_index
] = e_a
1285 edges_conv
[edge_index
+ 1] = e_b
1288 mesh
.edges
.add(tot_edges
)
1289 mesh
.edges
.foreach_set("vertices", edges_conv
)
1291 # must be after edge, face loading.
1292 ok_smooth
= blen_read_geom_layer_smooth(fbx_obj
, mesh
)
1294 ok_crease
= blen_read_geom_layer_edge_crease(fbx_obj
, mesh
)
1297 if settings
.use_custom_normals
:
1298 # Note: we store 'temp' normals in loops, since validate() may alter final mesh,
1299 # we can only set custom lnors *after* calling it.
1300 mesh
.create_normals_split()
1301 if geom_mat_no
is None:
1302 ok_normals
= blen_read_geom_layer_normal(fbx_obj
, mesh
)
1305 return geom_mat_no
@ Vector(v
)
1306 ok_normals
= blen_read_geom_layer_normal(fbx_obj
, mesh
, nortrans
)
1308 mesh
.validate(clean_customdata
=False) # *Very* important to not remove lnors here!
1311 clnors
= array
.array('f', [0.0] * (len(mesh
.loops
) * 3))
1312 mesh
.loops
.foreach_get("normal", clnors
)
1315 mesh
.polygons
.foreach_set("use_smooth", [True] * len(mesh
.polygons
))
1318 mesh
.normals_split_custom_set(tuple(zip(*(iter(clnors
),) * 3)))
1319 mesh
.use_auto_smooth
= True
1323 if settings
.use_custom_normals
:
1324 mesh
.free_normals_split()
1327 mesh
.polygons
.foreach_set("use_smooth", [True] * len(mesh
.polygons
))
1330 mesh
.use_customdata_edge_crease
= True
1332 if settings
.use_custom_props
:
1333 blen_read_custom_properties(fbx_obj
, mesh
, settings
)
1338 def blen_read_shape(fbx_tmpl
, fbx_sdata
, fbx_bcdata
, meshes
, scene
):
1339 elem_name_utf8
= elem_name_ensure_class(fbx_sdata
, b
'Geometry')
1340 indices
= elem_prop_first(elem_find_first(fbx_sdata
, b
'Indexes'), default
=())
1341 dvcos
= tuple(co
for co
in zip(*[iter(elem_prop_first(elem_find_first(fbx_sdata
, b
'Vertices'), default
=()))] * 3))
1342 # We completely ignore normals here!
1343 weight
= elem_prop_first(elem_find_first(fbx_bcdata
, b
'DeformPercent'), default
=100.0) / 100.0
1344 vgweights
= tuple(vgw
/ 100.0 for vgw
in elem_prop_first(elem_find_first(fbx_bcdata
, b
'FullWeights'), default
=()))
1346 # Special case, in case all weights are the same, FullWeight can have only one element - *sigh!*
1347 nbr_indices
= len(indices
)
1348 if len(vgweights
) == 1 and nbr_indices
> 1:
1349 vgweights
= (vgweights
[0],) * nbr_indices
1351 assert(len(vgweights
) == nbr_indices
== len(dvcos
))
1352 create_vg
= bool(set(vgweights
) - {1.0})
1356 for me
, objects
in meshes
:
1357 vcos
= tuple((idx
, me
.vertices
[idx
].co
+ Vector(dvco
)) for idx
, dvco
in zip(indices
, dvcos
))
1358 objects
= list({node
.bl_obj
for node
in objects
})
1361 if me
.shape_keys
is None:
1362 objects
[0].shape_key_add(name
="Basis", from_mix
=False)
1363 kb
= objects
[0].shape_key_add(name
=elem_name_utf8
, from_mix
=False)
1364 me
.shape_keys
.use_relative
= True # Should already be set as such.
1366 for idx
, co
in vcos
:
1367 kb
.data
[idx
].co
[:] = co
1370 # Add vgroup if necessary.
1372 vgoups
= add_vgroup_to_objects(indices
, vgweights
, kb
.name
, objects
)
1373 kb
.vertex_group
= kb
.name
1375 keyblocks
.append(kb
)
1383 def blen_read_material(fbx_tmpl
, fbx_obj
, settings
):
1384 from bpy_extras
import node_shader_utils
1385 from math
import sqrt
1387 elem_name_utf8
= elem_name_ensure_class(fbx_obj
, b
'Material')
1389 nodal_material_wrap_map
= settings
.nodal_material_wrap_map
1390 ma
= bpy
.data
.materials
.new(name
=elem_name_utf8
)
1392 const_color_white
= 1.0, 1.0, 1.0
1393 const_color_black
= 0.0, 0.0, 0.0
1395 fbx_props
= (elem_find_first(fbx_obj
, b
'Properties70'),
1396 elem_find_first(fbx_tmpl
, b
'Properties70', fbx_elem_nil
))
1397 fbx_props_no_template
= (fbx_props
[0], fbx_elem_nil
)
1399 ma_wrap
= node_shader_utils
.PrincipledBSDFWrapper(ma
, is_readonly
=False, use_nodes
=True)
1400 ma_wrap
.base_color
= elem_props_get_color_rgb(fbx_props
, b
'DiffuseColor', const_color_white
)
1401 # No specular color in Principled BSDF shader, assumed to be either white or take some tint from diffuse one...
1402 # TODO: add way to handle tint option (guesstimate from spec color + intensity...)?
1403 ma_wrap
.specular
= elem_props_get_number(fbx_props
, b
'SpecularFactor', 0.25) * 2.0
1404 # XXX Totally empirical conversion, trying to adapt it
1405 # (from 1.0 - 0.0 Principled BSDF range to 0.0 - 100.0 FBX shininess range)...
1406 fbx_shininess
= elem_props_get_number(fbx_props
, b
'Shininess', 20.0)
1407 ma_wrap
.roughness
= 1.0 - (sqrt(fbx_shininess
) / 10.0)
1408 # Sweetness... Looks like we are not the only ones to not know exactly how FBX is supposed to work (see T59850).
1409 # According to one of its developers, Unity uses that formula to extract alpha value:
1411 # alpha = 1 - TransparencyFactor
1412 # if (alpha == 1 or alpha == 0):
1413 # alpha = 1 - TransparentColor.r
1415 # Until further info, let's assume this is correct way to do, hence the following code for TransparentColor.
1416 # However, there are some cases (from 3DSMax, see T65065), where we do have TransparencyFactor only defined
1417 # in the template to 0.0, and then materials defining TransparentColor to pure white (1.0, 1.0, 1.0),
1418 # and setting alpha value in Opacity... try to cope with that too. :((((
1419 alpha
= 1.0 - elem_props_get_number(fbx_props
, b
'TransparencyFactor', 0.0)
1420 if (alpha
== 1.0 or alpha
== 0.0):
1421 alpha
= elem_props_get_number(fbx_props_no_template
, b
'Opacity', None)
1423 alpha
= 1.0 - elem_props_get_color_rgb(fbx_props
, b
'TransparentColor', const_color_black
)[0]
1424 ma_wrap
.alpha
= alpha
1425 ma_wrap
.metallic
= elem_props_get_number(fbx_props
, b
'ReflectionFactor', 0.0)
1426 # We have no metallic (a.k.a. reflection) color...
1427 # elem_props_get_color_rgb(fbx_props, b'ReflectionColor', const_color_white)
1428 ma_wrap
.normalmap_strength
= elem_props_get_number(fbx_props
, b
'BumpFactor', 1.0)
1429 # For emission color we can take into account the factor, but only for default values, not in case of texture.
1430 emission_factor
= elem_props_get_number(fbx_props
, b
'EmissiveFactor', 1.0)
1431 ma_wrap
.emission_color
= [c
* emission_factor
1432 for c
in elem_props_get_color_rgb(fbx_props
, b
'EmissiveColor', const_color_black
)]
1434 nodal_material_wrap_map
[ma
] = ma_wrap
1436 if settings
.use_custom_props
:
1437 blen_read_custom_properties(fbx_obj
, ma
, settings
)
1445 def blen_read_texture_image(fbx_tmpl
, fbx_obj
, basedir
, settings
):
1447 from bpy_extras
import image_utils
1449 def pack_data_from_content(image
, fbx_obj
):
1450 data
= elem_find_first_bytes(fbx_obj
, b
'Content')
1452 data_len
= len(data
)
1454 image
.pack(data
=data
, data_len
=data_len
)
1456 elem_name_utf8
= elem_name_ensure_classes(fbx_obj
, {b
'Texture', b
'Video'})
1458 image_cache
= settings
.image_cache
1460 # Yet another beautiful logic demonstration by Master FBX:
1461 # * RelativeFilename in both Video and Texture nodes.
1462 # * FileName in texture nodes.
1463 # * Filename in video nodes.
1464 # Aaaaaaaarrrrrrrrgggggggggggg!!!!!!!!!!!!!!
1465 filepath
= elem_find_first_string(fbx_obj
, b
'RelativeFilename')
1467 # Make sure we do handle a relative path, and not an absolute one (see D5143).
1468 filepath
= filepath
.lstrip(os
.path
.sep
).lstrip(os
.path
.altsep
)
1469 filepath
= os
.path
.join(basedir
, filepath
)
1471 filepath
= elem_find_first_string(fbx_obj
, b
'FileName')
1473 filepath
= elem_find_first_string(fbx_obj
, b
'Filename')
1475 print("Error, could not find any file path in ", fbx_obj
)
1476 print(" Falling back to: ", elem_name_utf8
)
1477 filepath
= elem_name_utf8
1479 filepath
= filepath
.replace('\\', '/') if (os
.sep
== '/') else filepath
.replace('/', '\\')
1481 image
= image_cache
.get(filepath
)
1482 if image
is not None:
1483 # Data is only embedded once, we may have already created the image but still be missing its data!
1484 if not image
.has_data
:
1485 pack_data_from_content(image
, fbx_obj
)
1488 image
= image_utils
.load_image(
1492 recursive
=settings
.use_image_search
,
1495 # Try to use embedded data, if available!
1496 pack_data_from_content(image
, fbx_obj
)
1498 image_cache
[filepath
] = image
1499 # name can be ../a/b/c
1500 image
.name
= os
.path
.basename(elem_name_utf8
)
1502 if settings
.use_custom_props
:
1503 blen_read_custom_properties(fbx_obj
, image
, settings
)
1508 def blen_read_camera(fbx_tmpl
, fbx_obj
, global_scale
):
1512 elem_name_utf8
= elem_name_ensure_class(fbx_obj
, b
'NodeAttribute')
1514 fbx_props
= (elem_find_first(fbx_obj
, b
'Properties70'),
1515 elem_find_first(fbx_tmpl
, b
'Properties70', fbx_elem_nil
))
1517 camera
= bpy
.data
.cameras
.new(name
=elem_name_utf8
)
1519 camera
.type = 'ORTHO' if elem_props_get_enum(fbx_props
, b
'CameraProjectionType', 0) == 1 else 'PERSP'
1521 camera
.lens
= elem_props_get_number(fbx_props
, b
'FocalLength', 35.0)
1522 camera
.sensor_width
= elem_props_get_number(fbx_props
, b
'FilmWidth', 32.0 * M2I
) / M2I
1523 camera
.sensor_height
= elem_props_get_number(fbx_props
, b
'FilmHeight', 32.0 * M2I
) / M2I
1525 camera
.ortho_scale
= elem_props_get_number(fbx_props
, b
'OrthoZoom', 1.0)
1527 filmaspect
= camera
.sensor_width
/ camera
.sensor_height
1529 camera
.shift_x
= elem_props_get_number(fbx_props
, b
'FilmOffsetX', 0.0) / (M2I
* camera
.sensor_width
)
1530 camera
.shift_y
= elem_props_get_number(fbx_props
, b
'FilmOffsetY', 0.0) / (M2I
* camera
.sensor_height
* filmaspect
)
1532 camera
.clip_start
= elem_props_get_number(fbx_props
, b
'NearPlane', 0.01) * global_scale
1533 camera
.clip_end
= elem_props_get_number(fbx_props
, b
'FarPlane', 100.0) * global_scale
1538 def blen_read_light(fbx_tmpl
, fbx_obj
, global_scale
):
1540 elem_name_utf8
= elem_name_ensure_class(fbx_obj
, b
'NodeAttribute')
1542 fbx_props
= (elem_find_first(fbx_obj
, b
'Properties70'),
1543 elem_find_first(fbx_tmpl
, b
'Properties70', fbx_elem_nil
))
1548 2: 'SPOT'}.get(elem_props_get_enum(fbx_props
, b
'LightType', 0), 'POINT')
1550 lamp
= bpy
.data
.lights
.new(name
=elem_name_utf8
, type=light_type
)
1552 if light_type
== 'SPOT':
1553 spot_size
= elem_props_get_number(fbx_props
, b
'OuterAngle', None)
1554 if spot_size
is None:
1556 spot_size
= elem_props_get_number(fbx_props
, b
'Cone angle', 45.0)
1557 lamp
.spot_size
= math
.radians(spot_size
)
1559 spot_blend
= elem_props_get_number(fbx_props
, b
'InnerAngle', None)
1560 if spot_blend
is None:
1562 spot_blend
= elem_props_get_number(fbx_props
, b
'HotSpot', 45.0)
1563 lamp
.spot_blend
= 1.0 - (spot_blend
/ spot_size
)
1565 # TODO, cycles nodes???
1566 lamp
.color
= elem_props_get_color_rgb(fbx_props
, b
'Color', (1.0, 1.0, 1.0))
1567 lamp
.energy
= elem_props_get_number(fbx_props
, b
'Intensity', 100.0) / 100.0
1568 lamp
.distance
= elem_props_get_number(fbx_props
, b
'DecayStart', 25.0) * global_scale
1569 lamp
.use_shadow
= elem_props_get_bool(fbx_props
, b
'CastShadow', True)
1570 if hasattr(lamp
, "cycles"):
1571 lamp
.cycles
.cast_shadow
= lamp
.use_shadow
1572 # Keeping this for now, but this is not used nor exposed anymore afaik...
1573 lamp
.shadow_color
= elem_props_get_color_rgb(fbx_props
, b
'ShadowColor', (0.0, 0.0, 0.0))
1578 # ### Import Utility class
1579 class FbxImportHelperNode
:
1581 Temporary helper node to store a hierarchy of fbxNode objects before building Objects, Armatures and Bones.
1582 It tries to keep the correction data in one place so it can be applied consistently to the imported data.
1586 '_parent', 'anim_compensation_matrix', 'is_global_animation', 'armature_setup', 'armature', 'bind_matrix',
1587 'bl_bone', 'bl_data', 'bl_obj', 'bone_child_matrix', 'children', 'clusters',
1588 'fbx_elem', 'fbx_name', 'fbx_transform_data', 'fbx_type',
1589 'is_armature', 'has_bone_children', 'is_bone', 'is_root', 'is_leaf',
1590 'matrix', 'matrix_as_parent', 'matrix_geom', 'meshes', 'post_matrix', 'pre_matrix')
1592 def __init__(self
, fbx_elem
, bl_data
, fbx_transform_data
, is_bone
):
1593 self
.fbx_name
= elem_name_ensure_class(fbx_elem
, b
'Model') if fbx_elem
else 'Unknown'
1594 self
.fbx_type
= fbx_elem
.props
[2] if fbx_elem
else None
1595 self
.fbx_elem
= fbx_elem
1597 self
.bl_data
= bl_data
1598 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!)
1599 self
.fbx_transform_data
= fbx_transform_data
1600 self
.is_root
= False
1601 self
.is_bone
= is_bone
1602 self
.is_armature
= False
1603 self
.armature
= None # For bones only, relevant armature node.
1604 self
.has_bone_children
= False # True if the hierarchy below this node contains bones, important to support mixed hierarchies.
1605 self
.is_leaf
= False # True for leaf-bones added to the end of some bone chains to set the lengths.
1606 self
.pre_matrix
= None # correction matrix that needs to be applied before the FBX transform
1607 self
.bind_matrix
= None # for bones this is the matrix used to bind to the skin
1608 if fbx_transform_data
:
1609 self
.matrix
, self
.matrix_as_parent
, self
.matrix_geom
= blen_read_object_transform_do(fbx_transform_data
)
1611 self
.matrix
, self
.matrix_as_parent
, self
.matrix_geom
= (None, None, None)
1612 self
.post_matrix
= None # correction matrix that needs to be applied after the FBX transform
1613 self
.bone_child_matrix
= None # Objects attached to a bone end not the beginning, this matrix corrects for that
1615 # XXX Those two are to handle the fact that rigged meshes are not linked to their armature in FBX, which implies
1616 # that their animation is in global space (afaik...).
1617 # This is actually not really solvable currently, since anim_compensation_matrix is not valid if armature
1618 # itself is animated (we'd have to recompute global-to-local anim_compensation_matrix for each frame,
1619 # and for each armature action... beyond being an insane work).
1620 # Solution for now: do not read rigged meshes animations at all! sic...
1621 self
.anim_compensation_matrix
= None # a mesh moved in the hierarchy may have a different local matrix. This compensates animations for this.
1622 self
.is_global_animation
= False
1624 self
.meshes
= None # List of meshes influenced by this bone.
1625 self
.clusters
= [] # Deformer Cluster nodes
1626 self
.armature_setup
= {} # mesh and armature matrix when the mesh was bound
1636 def parent(self
, value
):
1637 if self
._parent
is not None:
1638 self
._parent
.children
.remove(self
)
1639 self
._parent
= value
1640 if self
._parent
is not None:
1641 self
._parent
.children
.append(self
)
1645 # Separating leaf status from ignore status itself.
1646 # Currently they are equivalent, but this may change in future.
1651 return self
.fbx_elem
.props
[1].decode()
1655 def print_info(self
, indent
=0):
1656 print(" " * indent
+ (self
.fbx_name
if self
.fbx_name
else "(Null)")
1657 + ("[root]" if self
.is_root
else "")
1658 + ("[leaf]" if self
.is_leaf
else "")
1659 + ("[ignore]" if self
.ignore
else "")
1660 + ("[armature]" if self
.is_armature
else "")
1661 + ("[bone]" if self
.is_bone
else "")
1662 + ("[HBC]" if self
.has_bone_children
else "")
1664 for c
in self
.children
:
1665 c
.print_info(indent
+ 1)
1667 def mark_leaf_bones(self
):
1668 if self
.is_bone
and len(self
.children
) == 1:
1669 child
= self
.children
[0]
1670 if child
.is_bone
and len(child
.children
) == 0:
1671 child
.is_leaf
= True
1672 for child
in self
.children
:
1673 child
.mark_leaf_bones()
1675 def do_bake_transform(self
, settings
):
1676 return (settings
.bake_space_transform
and self
.fbx_type
in (b
'Mesh', b
'Null') and
1677 not self
.is_armature
and not self
.is_bone
)
1679 def find_correction_matrix(self
, settings
, parent_correction_inv
=None):
1680 from bpy_extras
.io_utils
import axis_conversion
1682 if self
.parent
and (self
.parent
.is_root
or self
.parent
.do_bake_transform(settings
)):
1683 self
.pre_matrix
= settings
.global_matrix
1685 if parent_correction_inv
:
1686 self
.pre_matrix
= parent_correction_inv
@ (self
.pre_matrix
if self
.pre_matrix
else Matrix())
1688 correction_matrix
= None
1691 if settings
.automatic_bone_orientation
:
1692 # find best orientation to align bone with
1693 bone_children
= tuple(child
for child
in self
.children
if child
.is_bone
)
1694 if len(bone_children
) == 0:
1695 # no children, inherit the correction from parent (if possible)
1696 if self
.parent
and self
.parent
.is_bone
:
1697 correction_matrix
= parent_correction_inv
.inverted() if parent_correction_inv
else None
1699 # else find how best to rotate the bone to align the Y axis with the children
1700 best_axis
= (1, 0, 0)
1701 if len(bone_children
) == 1:
1702 vec
= bone_children
[0].get_bind_matrix().to_translation()
1703 best_axis
= Vector((0, 0, 1 if vec
[2] >= 0 else -1))
1704 if abs(vec
[0]) > abs(vec
[1]):
1705 if abs(vec
[0]) > abs(vec
[2]):
1706 best_axis
= Vector((1 if vec
[0] >= 0 else -1, 0, 0))
1707 elif abs(vec
[1]) > abs(vec
[2]):
1708 best_axis
= Vector((0, 1 if vec
[1] >= 0 else -1, 0))
1710 # get the child directions once because they may be checked several times
1711 child_locs
= (child
.get_bind_matrix().to_translation() for child
in bone_children
)
1712 child_locs
= tuple(loc
.normalized() for loc
in child_locs
if loc
.magnitude
> 0.0)
1714 # I'm not sure which one I like better...
1719 s
= -1 if i
% 2 == 1 else 1
1720 test_axis
= Vector((s
if a
== 0 else 0, s
if a
== 1 else 0, s
if a
== 2 else 0))
1722 # find max angle to children
1724 for loc
in child_locs
:
1725 max_angle
= min(max_angle
, test_axis
.dot(loc
))
1727 # is it better than the last one?
1728 if best_angle
< max_angle
:
1729 best_angle
= max_angle
1730 best_axis
= test_axis
1733 for vec
in child_locs
:
1734 test_axis
= Vector((0, 0, 1 if vec
[2] >= 0 else -1))
1735 if abs(vec
[0]) > abs(vec
[1]):
1736 if abs(vec
[0]) > abs(vec
[2]):
1737 test_axis
= Vector((1 if vec
[0] >= 0 else -1, 0, 0))
1738 elif abs(vec
[1]) > abs(vec
[2]):
1739 test_axis
= Vector((0, 1 if vec
[1] >= 0 else -1, 0))
1741 # find max angle to children
1743 for loc
in child_locs
:
1744 max_angle
= min(max_angle
, test_axis
.dot(loc
))
1746 # is it better than the last one?
1747 if best_angle
< max_angle
:
1748 best_angle
= max_angle
1749 best_axis
= test_axis
1751 # convert best_axis to axis string
1752 to_up
= 'Z' if best_axis
[2] >= 0 else '-Z'
1753 if abs(best_axis
[0]) > abs(best_axis
[1]):
1754 if abs(best_axis
[0]) > abs(best_axis
[2]):
1755 to_up
= 'X' if best_axis
[0] >= 0 else '-X'
1756 elif abs(best_axis
[1]) > abs(best_axis
[2]):
1757 to_up
= 'Y' if best_axis
[1] >= 0 else '-Y'
1758 to_forward
= 'X' if to_up
not in {'X', '-X'} else 'Y'
1760 # Build correction matrix
1761 if (to_up
, to_forward
) != ('Y', 'X'):
1762 correction_matrix
= axis_conversion(from_forward
='X',
1764 to_forward
=to_forward
,
1768 correction_matrix
= settings
.bone_correction_matrix
1770 # camera and light can be hard wired
1771 if self
.fbx_type
== b
'Camera':
1772 correction_matrix
= MAT_CONVERT_CAMERA
1773 elif self
.fbx_type
== b
'Light':
1774 correction_matrix
= MAT_CONVERT_LIGHT
1776 self
.post_matrix
= correction_matrix
1778 if self
.do_bake_transform(settings
):
1779 self
.post_matrix
= settings
.global_matrix_inv
@ (self
.post_matrix
if self
.post_matrix
else Matrix())
1782 correction_matrix_inv
= correction_matrix
.inverted_safe() if correction_matrix
else None
1783 for child
in self
.children
:
1784 child
.find_correction_matrix(settings
, correction_matrix_inv
)
1786 def find_armature_bones(self
, armature
):
1787 for child
in self
.children
:
1789 child
.armature
= armature
1790 child
.find_armature_bones(armature
)
1792 def find_armatures(self
):
1793 needs_armature
= False
1794 for child
in self
.children
:
1796 needs_armature
= True
1799 if self
.fbx_type
in {b
'Null', b
'Root'}:
1800 # if empty then convert into armature
1801 self
.is_armature
= True
1804 # otherwise insert a new node
1805 # XXX Maybe in case self is virtual FBX root node, we should instead add one armature per bone child?
1806 armature
= FbxImportHelperNode(None, None, None, False)
1807 armature
.fbx_name
= "Armature"
1808 armature
.is_armature
= True
1810 for child
in tuple(self
.children
):
1812 child
.parent
= armature
1814 armature
.parent
= self
1816 armature
.find_armature_bones(armature
)
1818 for child
in self
.children
:
1819 if child
.is_armature
or child
.is_bone
:
1821 child
.find_armatures()
1823 def find_bone_children(self
):
1824 has_bone_children
= False
1825 for child
in self
.children
:
1826 has_bone_children |
= child
.find_bone_children()
1827 self
.has_bone_children
= has_bone_children
1828 return self
.is_bone
or has_bone_children
1830 def find_fake_bones(self
, in_armature
=False):
1831 if in_armature
and not self
.is_bone
and self
.has_bone_children
:
1833 # if we are not a null node we need an intermediate node for the data
1834 if self
.fbx_type
not in {b
'Null', b
'Root'}:
1835 node
= FbxImportHelperNode(self
.fbx_elem
, self
.bl_data
, None, False)
1836 self
.fbx_elem
= None
1840 for child
in self
.children
:
1841 if child
.is_bone
or child
.has_bone_children
:
1848 if self
.is_armature
:
1850 for child
in self
.children
:
1851 child
.find_fake_bones(in_armature
)
1853 def get_world_matrix_as_parent(self
):
1854 matrix
= self
.parent
.get_world_matrix_as_parent() if self
.parent
else Matrix()
1855 if self
.matrix_as_parent
:
1856 matrix
= matrix
@ self
.matrix_as_parent
1859 def get_world_matrix(self
):
1860 matrix
= self
.parent
.get_world_matrix_as_parent() if self
.parent
else Matrix()
1862 matrix
= matrix
@ self
.matrix
1865 def get_matrix(self
):
1866 matrix
= self
.matrix
if self
.matrix
else Matrix()
1868 matrix
= self
.pre_matrix
@ matrix
1869 if self
.post_matrix
:
1870 matrix
= matrix
@ self
.post_matrix
1873 def get_bind_matrix(self
):
1874 matrix
= self
.bind_matrix
if self
.bind_matrix
else Matrix()
1876 matrix
= self
.pre_matrix
@ matrix
1877 if self
.post_matrix
:
1878 matrix
= matrix
@ self
.post_matrix
1881 def make_bind_pose_local(self
, parent_matrix
=None):
1882 if parent_matrix
is None:
1883 parent_matrix
= Matrix()
1885 if self
.bind_matrix
:
1886 bind_matrix
= parent_matrix
.inverted_safe() @ self
.bind_matrix
1888 bind_matrix
= self
.matrix
.copy() if self
.matrix
else None
1890 self
.bind_matrix
= bind_matrix
1892 parent_matrix
= parent_matrix
@ bind_matrix
1894 for child
in self
.children
:
1895 child
.make_bind_pose_local(parent_matrix
)
1897 def collect_skeleton_meshes(self
, meshes
):
1898 for _
, m
in self
.clusters
:
1900 for child
in self
.children
:
1901 child
.collect_skeleton_meshes(meshes
)
1903 def collect_armature_meshes(self
):
1904 if self
.is_armature
:
1905 armature_matrix_inv
= self
.get_world_matrix().inverted_safe()
1908 for child
in self
.children
:
1909 # Children meshes may be linked to children armatures, in which case we do not want to link them
1910 # to a parent one. See T70244.
1911 child
.collect_armature_meshes()
1912 if not child
.meshes
:
1913 child
.collect_skeleton_meshes(meshes
)
1915 old_matrix
= m
.matrix
1916 m
.matrix
= armature_matrix_inv
@ m
.get_world_matrix()
1917 m
.anim_compensation_matrix
= old_matrix
.inverted_safe() @ m
.matrix
1918 m
.is_global_animation
= True
1920 self
.meshes
= meshes
1922 for child
in self
.children
:
1923 child
.collect_armature_meshes()
1925 def build_skeleton(self
, arm
, parent_matrix
, parent_bone_size
=1, force_connect_children
=False):
1926 def child_connect(par_bone
, child_bone
, child_head
, connect_ctx
):
1927 # child_bone or child_head may be None.
1928 force_connect_children
, connected
= connect_ctx
1929 if child_bone
is not None:
1930 child_bone
.parent
= par_bone
1931 child_head
= child_bone
.head
1933 if similar_values_iter(par_bone
.tail
, child_head
):
1934 if child_bone
is not None:
1935 child_bone
.use_connect
= True
1936 # Disallow any force-connection at this level from now on, since that child was 'really'
1937 # connected, we do not want to move current bone's tail anymore!
1939 elif force_connect_children
and connected
is not None:
1940 # We only store position where tail of par_bone should be in the end.
1941 # Actual tail moving and force connection of compatible child bones will happen
1942 # once all have been checked.
1943 if connected
is ...:
1944 connected
= ([child_head
.copy(), 1], [child_bone
] if child_bone
is not None else [])
1946 connected
[0][0] += child_head
1947 connected
[0][1] += 1
1948 if child_bone
is not None:
1949 connected
[1].append(child_bone
)
1950 connect_ctx
[1] = connected
1952 def child_connect_finalize(par_bone
, connect_ctx
):
1953 force_connect_children
, connected
= connect_ctx
1954 # Do nothing if force connection is not enabled!
1955 if force_connect_children
and connected
is not None and connected
is not ...:
1956 # Here again we have to be wary about zero-length bones!!!
1957 par_tail
= connected
[0][0] / connected
[0][1]
1958 if (par_tail
- par_bone
.head
).magnitude
< 1e-2:
1959 par_bone_vec
= (par_bone
.tail
- par_bone
.head
).normalized()
1960 par_tail
= par_bone
.head
+ par_bone_vec
* 0.01
1961 par_bone
.tail
= par_tail
1962 for child_bone
in connected
[1]:
1963 if similar_values_iter(par_tail
, child_bone
.head
):
1964 child_bone
.use_connect
= True
1966 # Create the (edit)bone.
1967 bone
= arm
.bl_data
.edit_bones
.new(name
=self
.fbx_name
)
1969 self
.bl_obj
= arm
.bl_obj
1970 self
.bl_data
= arm
.bl_data
1971 self
.bl_bone
= bone
.name
# Could be different from the FBX name!
1973 # get average distance to children
1976 for child
in self
.children
:
1978 bone_size
+= child
.get_bind_matrix().to_translation().magnitude
1981 bone_size
/= bone_count
1983 bone_size
= parent_bone_size
1985 # So that our bone gets its final length, but still Y-aligned in armature space.
1986 # 0-length bones are automatically collapsed into their parent when you leave edit mode,
1987 # so this enforces a minimum length.
1988 bone_tail
= Vector((0.0, 1.0, 0.0)) * max(0.01, bone_size
)
1989 bone
.tail
= bone_tail
1991 # And rotate/move it to its final "rest pose".
1992 bone_matrix
= parent_matrix
@ self
.get_bind_matrix().normalized()
1994 bone
.matrix
= bone_matrix
1996 # Correction for children attached to a bone. FBX expects to attach to the head of a bone,
1997 # while Blender attaches to the tail.
1998 self
.bone_child_matrix
= Matrix
.Translation(-bone_tail
)
2000 connect_ctx
= [force_connect_children
, ...]
2001 for child
in self
.children
:
2002 if child
.is_leaf
and force_connect_children
:
2003 # Arggggggggggggggggg! We do not want to create this bone, but we need its 'virtual head' location
2004 # to orient current one!!!
2005 child_head
= (bone_matrix
@ child
.get_bind_matrix().normalized()).translation
2006 child_connect(bone
, None, child_head
, connect_ctx
)
2007 elif child
.is_bone
and not child
.ignore
:
2008 child_bone
= child
.build_skeleton(arm
, bone_matrix
, bone_size
,
2009 force_connect_children
=force_connect_children
)
2010 # Connection to parent.
2011 child_connect(bone
, child_bone
, None, connect_ctx
)
2013 child_connect_finalize(bone
, connect_ctx
)
2016 def build_node_obj(self
, fbx_tmpl
, settings
):
2020 if self
.is_bone
or not self
.fbx_elem
:
2023 # create when linking since we need object data
2024 elem_name_utf8
= self
.fbx_name
2026 # Object data must be created already
2027 self
.bl_obj
= obj
= bpy
.data
.objects
.new(name
=elem_name_utf8
, object_data
=self
.bl_data
)
2029 fbx_props
= (elem_find_first(self
.fbx_elem
, b
'Properties70'),
2030 elem_find_first(fbx_tmpl
, b
'Properties70', fbx_elem_nil
))
2035 obj
.color
[0:3] = elem_props_get_color_rgb(fbx_props
, b
'Color', (0.8, 0.8, 0.8))
2036 obj
.hide_viewport
= not bool(elem_props_get_visibility(fbx_props
, b
'Visibility', 1.0))
2038 obj
.matrix_basis
= self
.get_matrix()
2040 if settings
.use_custom_props
:
2041 blen_read_custom_properties(self
.fbx_elem
, obj
, settings
)
2045 def build_skeleton_children(self
, fbx_tmpl
, settings
, scene
, view_layer
):
2047 for child
in self
.children
:
2050 child
.build_skeleton_children(fbx_tmpl
, settings
, scene
, view_layer
)
2053 # child is not a bone
2054 obj
= self
.build_node_obj(fbx_tmpl
, settings
)
2059 for child
in self
.children
:
2062 child
.build_skeleton_children(fbx_tmpl
, settings
, scene
, view_layer
)
2065 view_layer
.active_layer_collection
.collection
.objects
.link(obj
)
2066 obj
.select_set(True)
2070 def link_skeleton_children(self
, fbx_tmpl
, settings
, scene
):
2072 for child
in self
.children
:
2075 child_obj
= child
.bl_obj
2076 if child_obj
and child_obj
!= self
.bl_obj
:
2077 child_obj
.parent
= self
.bl_obj
# get the armature the bone belongs to
2078 child_obj
.parent_bone
= self
.bl_bone
2079 child_obj
.parent_type
= 'BONE'
2080 child_obj
.matrix_parent_inverse
= Matrix()
2082 # Blender attaches to the end of a bone, while FBX attaches to the start.
2083 # bone_child_matrix corrects for that.
2084 if child
.pre_matrix
:
2085 child
.pre_matrix
= self
.bone_child_matrix
@ child
.pre_matrix
2087 child
.pre_matrix
= self
.bone_child_matrix
2089 child_obj
.matrix_basis
= child
.get_matrix()
2090 child
.link_skeleton_children(fbx_tmpl
, settings
, scene
)
2095 for child
in self
.children
:
2098 child_obj
= child
.link_skeleton_children(fbx_tmpl
, settings
, scene
)
2100 child_obj
.parent
= obj
2104 def set_pose_matrix(self
, arm
):
2105 pose_bone
= arm
.bl_obj
.pose
.bones
[self
.bl_bone
]
2106 pose_bone
.matrix_basis
= self
.get_bind_matrix().inverted_safe() @ self
.get_matrix()
2108 for child
in self
.children
:
2112 child
.set_pose_matrix(arm
)
2114 def merge_weights(self
, combined_weights
, fbx_cluster
):
2115 indices
= elem_prop_first(elem_find_first(fbx_cluster
, b
'Indexes', default
=None), default
=())
2116 weights
= elem_prop_first(elem_find_first(fbx_cluster
, b
'Weights', default
=None), default
=())
2118 for index
, weight
in zip(indices
, weights
):
2119 w
= combined_weights
.get(index
)
2121 combined_weights
[index
] = [weight
]
2125 def set_bone_weights(self
):
2126 ignored_children
= tuple(child
for child
in self
.children
2127 if child
.is_bone
and child
.ignore
and len(child
.clusters
) > 0)
2129 if len(ignored_children
) > 0:
2130 # If we have an ignored child bone we need to merge their weights into the current bone weights.
2131 # This can happen both intentionally and accidentally when skinning a model. Either way, they
2132 # need to be moved into a parent bone or they cause animation glitches.
2133 for fbx_cluster
, meshes
in self
.clusters
:
2134 combined_weights
= {}
2135 self
.merge_weights(combined_weights
, fbx_cluster
)
2137 for child
in ignored_children
:
2138 for child_cluster
, child_meshes
in child
.clusters
:
2139 if not meshes
.isdisjoint(child_meshes
):
2140 self
.merge_weights(combined_weights
, child_cluster
)
2142 # combine child weights
2145 for i
, w
in combined_weights
.items():
2148 weights
.append(sum(w
) / len(w
))
2150 weights
.append(w
[0])
2152 add_vgroup_to_objects(indices
, weights
, self
.bl_bone
, [node
.bl_obj
for node
in meshes
])
2154 # clusters that drive meshes not included in a parent don't need to be merged
2155 all_meshes
= set().union(*[meshes
for _
, meshes
in self
.clusters
])
2156 for child
in ignored_children
:
2157 for child_cluster
, child_meshes
in child
.clusters
:
2158 if all_meshes
.isdisjoint(child_meshes
):
2159 indices
= elem_prop_first(elem_find_first(child_cluster
, b
'Indexes', default
=None), default
=())
2160 weights
= elem_prop_first(elem_find_first(child_cluster
, b
'Weights', default
=None), default
=())
2161 add_vgroup_to_objects(indices
, weights
, self
.bl_bone
, [node
.bl_obj
for node
in child_meshes
])
2163 # set the vertex weights on meshes
2164 for fbx_cluster
, meshes
in self
.clusters
:
2165 indices
= elem_prop_first(elem_find_first(fbx_cluster
, b
'Indexes', default
=None), default
=())
2166 weights
= elem_prop_first(elem_find_first(fbx_cluster
, b
'Weights', default
=None), default
=())
2167 add_vgroup_to_objects(indices
, weights
, self
.bl_bone
, [node
.bl_obj
for node
in meshes
])
2169 for child
in self
.children
:
2170 if child
.is_bone
and not child
.ignore
:
2171 child
.set_bone_weights()
2173 def build_hierarchy(self
, fbx_tmpl
, settings
, scene
, view_layer
):
2174 if self
.is_armature
:
2175 # create when linking since we need object data
2176 elem_name_utf8
= self
.fbx_name
2178 self
.bl_data
= arm_data
= bpy
.data
.armatures
.new(name
=elem_name_utf8
)
2180 # Object data must be created already
2181 self
.bl_obj
= arm
= bpy
.data
.objects
.new(name
=elem_name_utf8
, object_data
=arm_data
)
2183 arm
.matrix_basis
= self
.get_matrix()
2186 fbx_props
= (elem_find_first(self
.fbx_elem
, b
'Properties70'),
2187 elem_find_first(fbx_tmpl
, b
'Properties70', fbx_elem_nil
))
2189 if settings
.use_custom_props
:
2190 blen_read_custom_properties(self
.fbx_elem
, arm
, settings
)
2193 view_layer
.active_layer_collection
.collection
.objects
.link(arm
)
2194 arm
.select_set(True)
2198 # Switch to Edit mode.
2199 view_layer
.objects
.active
= arm
2200 is_hidden
= arm
.hide_viewport
2201 arm
.hide_viewport
= False # Can't switch to Edit mode hidden objects...
2202 bpy
.ops
.object.mode_set(mode
='EDIT')
2204 for child
in self
.children
:
2208 child
.build_skeleton(self
, Matrix(), force_connect_children
=settings
.force_connect_children
)
2210 bpy
.ops
.object.mode_set(mode
='OBJECT')
2212 arm
.hide_viewport
= is_hidden
2215 for child
in self
.children
:
2219 child
.set_pose_matrix(self
)
2221 # Add bone children:
2222 for child
in self
.children
:
2225 child_obj
= child
.build_skeleton_children(fbx_tmpl
, settings
, scene
, view_layer
)
2228 elif self
.fbx_elem
and not self
.is_bone
:
2229 obj
= self
.build_node_obj(fbx_tmpl
, settings
)
2231 # walk through children
2232 for child
in self
.children
:
2233 child
.build_hierarchy(fbx_tmpl
, settings
, scene
, view_layer
)
2236 view_layer
.active_layer_collection
.collection
.objects
.link(obj
)
2237 obj
.select_set(True)
2241 for child
in self
.children
:
2242 child
.build_hierarchy(fbx_tmpl
, settings
, scene
, view_layer
)
2246 def link_hierarchy(self
, fbx_tmpl
, settings
, scene
):
2247 if self
.is_armature
:
2250 # Link bone children:
2251 for child
in self
.children
:
2254 child_obj
= child
.link_skeleton_children(fbx_tmpl
, settings
, scene
)
2256 child_obj
.parent
= arm
2258 # Add armature modifiers to the meshes
2260 for mesh
in self
.meshes
:
2261 (mmat
, amat
) = mesh
.armature_setup
[self
]
2262 me_obj
= mesh
.bl_obj
2264 # bring global armature & mesh matrices into *Blender* global space.
2265 # Note: Usage of matrix_geom (local 'diff' transform) here is quite brittle.
2266 # Among other things, why in hell isn't it taken into account by bindpose & co???
2267 # Probably because org app (max) handles it completely aside from any parenting stuff,
2268 # which we obviously cannot do in Blender. :/
2270 amat
= self
.bind_matrix
2271 amat
= settings
.global_matrix
@ (Matrix() if amat
is None else amat
)
2272 if self
.matrix_geom
:
2273 amat
= amat
@ self
.matrix_geom
2274 mmat
= settings
.global_matrix
@ mmat
2275 if mesh
.matrix_geom
:
2276 mmat
= mmat
@ mesh
.matrix_geom
2278 # Now that we have armature and mesh in there (global) bind 'state' (matrix),
2279 # we can compute inverse parenting matrix of the mesh.
2280 me_obj
.matrix_parent_inverse
= amat
.inverted_safe() @ mmat
@ me_obj
.matrix_basis
.inverted_safe()
2282 mod
= mesh
.bl_obj
.modifiers
.new(arm
.name
, 'ARMATURE')
2285 # Add bone weights to the deformers
2286 for child
in self
.children
:
2290 child
.set_bone_weights()
2296 # walk through children
2297 for child
in self
.children
:
2298 child_obj
= child
.link_hierarchy(fbx_tmpl
, settings
, scene
)
2300 child_obj
.parent
= obj
2304 for child
in self
.children
:
2305 child
.link_hierarchy(fbx_tmpl
, settings
, scene
)
2310 def load(operator
, context
, filepath
="",
2311 use_manual_orientation
=False,
2315 bake_space_transform
=False,
2316 use_custom_normals
=True,
2317 use_image_search
=False,
2318 use_alpha_decals
=False,
2323 use_custom_props
=True,
2324 use_custom_props_enum_as_string
=True,
2325 ignore_leaf_bones
=False,
2326 force_connect_children
=False,
2327 automatic_bone_orientation
=False,
2328 primary_bone_axis
='Y',
2329 secondary_bone_axis
='X',
2330 use_prepost_rot
=True):
2333 fbx_elem_nil
= FBXElem('', (), (), ())
2337 from bpy_extras
.io_utils
import axis_conversion
2339 from . import parse_fbx
2340 from .fbx_utils
import RIGHT_HAND_AXES
, FBX_FRAMERATES
2342 start_time_proc
= time
.process_time()
2343 start_time_sys
= time
.time()
2347 perfmon
.step("FBX Import: start importing %s" % filepath
)
2350 # Detect ASCII files.
2352 # Typically it's bad practice to fail silently on any error,
2353 # however the file may fail to read for many reasons,
2354 # and this situation is handled later in the code,
2355 # right now we only want to know if the file successfully reads as ascii.
2357 with
open(filepath
, 'r', encoding
="utf-8") as fh
:
2364 operator
.report({'ERROR'}, "ASCII FBX files are not supported %r" % filepath
)
2365 return {'CANCELLED'}
2367 # End ascii detection.
2370 elem_root
, version
= parse_fbx
.parse(filepath
)
2371 except Exception as e
:
2373 traceback
.print_exc()
2375 operator
.report({'ERROR'}, "Couldn't open file %r (%s)" % (filepath
, e
))
2376 return {'CANCELLED'}
2379 operator
.report({'ERROR'}, "Version %r unsupported, must be %r or later" % (version
, 7100))
2380 return {'CANCELLED'}
2382 print("FBX version: %r" % version
)
2384 if bpy
.ops
.object.mode_set
.poll():
2385 bpy
.ops
.object.mode_set(mode
='OBJECT', toggle
=False)
2388 if bpy
.ops
.object.select_all
.poll():
2389 bpy
.ops
.object.select_all(action
='DESELECT')
2391 basedir
= os
.path
.dirname(filepath
)
2393 nodal_material_wrap_map
= {}
2396 # Tables: (FBX_byte_id -> [FBX_data, None or Blender_datablock])
2397 fbx_table_nodes
= {}
2399 if use_alpha_decals
:
2400 material_decals
= set()
2402 material_decals
= None
2404 scene
= context
.scene
2405 view_layer
= context
.view_layer
2407 # #### Get some info from GlobalSettings.
2409 perfmon
.step("FBX import: Prepare...")
2411 fbx_settings
= elem_find_first(elem_root
, b
'GlobalSettings')
2412 fbx_settings_props
= elem_find_first(fbx_settings
, b
'Properties70')
2413 if fbx_settings
is None or fbx_settings_props
is None:
2414 operator
.report({'ERROR'}, "No 'GlobalSettings' found in file %r" % filepath
)
2415 return {'CANCELLED'}
2417 # FBX default base unit seems to be the centimeter, while raw Blender Unit is equivalent to the meter...
2418 unit_scale
= elem_props_get_number(fbx_settings_props
, b
'UnitScaleFactor', 1.0)
2419 unit_scale_org
= elem_props_get_number(fbx_settings_props
, b
'OriginalUnitScaleFactor', 1.0)
2420 global_scale
*= (unit_scale
/ units_blender_to_fbx_factor(context
.scene
))
2421 # Compute global matrix and scale.
2422 if not use_manual_orientation
:
2423 axis_forward
= (elem_props_get_integer(fbx_settings_props
, b
'FrontAxis', 1),
2424 elem_props_get_integer(fbx_settings_props
, b
'FrontAxisSign', 1))
2425 axis_up
= (elem_props_get_integer(fbx_settings_props
, b
'UpAxis', 2),
2426 elem_props_get_integer(fbx_settings_props
, b
'UpAxisSign', 1))
2427 axis_coord
= (elem_props_get_integer(fbx_settings_props
, b
'CoordAxis', 0),
2428 elem_props_get_integer(fbx_settings_props
, b
'CoordAxisSign', 1))
2429 axis_key
= (axis_up
, axis_forward
, axis_coord
)
2430 axis_up
, axis_forward
= {v
: k
for k
, v
in RIGHT_HAND_AXES
.items()}.get(axis_key
, ('Z', 'Y'))
2431 global_matrix
= (Matrix
.Scale(global_scale
, 4) @
2432 axis_conversion(from_forward
=axis_forward
, from_up
=axis_up
).to_4x4())
2434 # To cancel out unwanted rotation/scale on nodes.
2435 global_matrix_inv
= global_matrix
.inverted()
2436 # For transforming mesh normals.
2437 global_matrix_inv_transposed
= global_matrix_inv
.transposed()
2439 # Compute bone correction matrix
2440 bone_correction_matrix
= None # None means no correction/identity
2441 if not automatic_bone_orientation
:
2442 if (primary_bone_axis
, secondary_bone_axis
) != ('Y', 'X'):
2443 bone_correction_matrix
= axis_conversion(from_forward
='X',
2445 to_forward
=secondary_bone_axis
,
2446 to_up
=primary_bone_axis
,
2449 # Compute framerate settings.
2450 custom_fps
= elem_props_get_number(fbx_settings_props
, b
'CustomFrameRate', 25.0)
2451 time_mode
= elem_props_get_enum(fbx_settings_props
, b
'TimeMode')
2452 real_fps
= {eid
: val
for val
, eid
in FBX_FRAMERATES
[1:]}.get(time_mode
, custom_fps
)
2455 scene
.render
.fps
= round(real_fps
)
2456 scene
.render
.fps_base
= scene
.render
.fps
/ real_fps
2458 # store global settings that need to be accessed during conversion
2459 settings
= FBXImportSettings(
2460 operator
.report
, (axis_up
, axis_forward
), global_matrix
, global_scale
,
2461 bake_space_transform
, global_matrix_inv
, global_matrix_inv_transposed
,
2462 use_custom_normals
, use_image_search
,
2463 use_alpha_decals
, decal_offset
,
2464 use_anim
, anim_offset
,
2466 use_custom_props
, use_custom_props_enum_as_string
,
2467 nodal_material_wrap_map
, image_cache
,
2468 ignore_leaf_bones
, force_connect_children
, automatic_bone_orientation
, bone_correction_matrix
,
2472 # #### And now, the "real" data.
2474 perfmon
.step("FBX import: Templates...")
2476 fbx_defs
= elem_find_first(elem_root
, b
'Definitions') # can be None
2477 fbx_nodes
= elem_find_first(elem_root
, b
'Objects')
2478 fbx_connections
= elem_find_first(elem_root
, b
'Connections')
2480 if fbx_nodes
is None:
2481 operator
.report({'ERROR'}, "No 'Objects' found in file %r" % filepath
)
2482 return {'CANCELLED'}
2483 if fbx_connections
is None:
2484 operator
.report({'ERROR'}, "No 'Connections' found in file %r" % filepath
)
2485 return {'CANCELLED'}
2488 # First load property templates
2489 # Load 'PropertyTemplate' values.
2490 # Key is a tuple, (ObjectType, FBXNodeType)
2491 # eg, (b'Texture', b'KFbxFileTexture')
2492 # (b'Geometry', b'KFbxMesh')
2496 if fbx_defs
is not None:
2497 for fbx_def
in fbx_defs
.elems
:
2498 if fbx_def
.id == b
'ObjectType':
2499 for fbx_subdef
in fbx_def
.elems
:
2500 if fbx_subdef
.id == b
'PropertyTemplate':
2501 assert(fbx_def
.props_type
== b
'S')
2502 assert(fbx_subdef
.props_type
== b
'S')
2503 # (b'Texture', b'KFbxFileTexture') - eg.
2504 key
= fbx_def
.props
[0], fbx_subdef
.props
[0]
2505 fbx_templates
[key
] = fbx_subdef
2508 def fbx_template_get(key
):
2509 ret
= fbx_templates
.get(key
, fbx_elem_nil
)
2510 if ret
is fbx_elem_nil
:
2511 # Newest FBX (7.4 and above) use no more 'K' in their type names...
2512 key
= (key
[0], key
[1][1:])
2513 return fbx_templates
.get(key
, fbx_elem_nil
)
2516 perfmon
.step("FBX import: Nodes...")
2519 # Build FBX node-table
2521 for fbx_obj
in fbx_nodes
.elems
:
2522 # TODO, investigate what other items after first 3 may be
2523 assert(fbx_obj
.props_type
[:3] == b
'LSS')
2524 fbx_uuid
= elem_uuid(fbx_obj
)
2525 fbx_table_nodes
[fbx_uuid
] = [fbx_obj
, None]
2530 # http://download.autodesk.com/us/fbx/20112/FBX_SDK_HELP/index.html?url=
2531 # WS73099cc142f487551fea285e1221e4f9ff8-7fda.htm,topicNumber=d0e6388
2533 perfmon
.step("FBX import: Connections...")
2535 fbx_connection_map
= {}
2536 fbx_connection_map_reverse
= {}
2539 for fbx_link
in fbx_connections
.elems
:
2540 c_type
= fbx_link
.props
[0]
2541 if fbx_link
.props_type
[1:3] == b
'LL':
2542 c_src
, c_dst
= fbx_link
.props
[1:3]
2543 fbx_connection_map
.setdefault(c_src
, []).append((c_dst
, fbx_link
))
2544 fbx_connection_map_reverse
.setdefault(c_dst
, []).append((c_src
, fbx_link
))
2547 perfmon
.step("FBX import: Meshes...")
2552 fbx_tmpl
= fbx_template_get((b
'Geometry', b
'KFbxMesh'))
2554 for fbx_uuid
, fbx_item
in fbx_table_nodes
.items():
2555 fbx_obj
, blen_data
= fbx_item
2556 if fbx_obj
.id != b
'Geometry':
2558 if fbx_obj
.props
[-1] == b
'Mesh':
2559 assert(blen_data
is None)
2560 fbx_item
[1] = blen_read_geom(fbx_tmpl
, fbx_obj
, settings
)
2563 perfmon
.step("FBX import: Materials & Textures...")
2566 # Load material data
2568 fbx_tmpl
= fbx_template_get((b
'Material', b
'KFbxSurfacePhong'))
2569 # b'KFbxSurfaceLambert'
2571 for fbx_uuid
, fbx_item
in fbx_table_nodes
.items():
2572 fbx_obj
, blen_data
= fbx_item
2573 if fbx_obj
.id != b
'Material':
2575 assert(blen_data
is None)
2576 fbx_item
[1] = blen_read_material(fbx_tmpl
, fbx_obj
, settings
)
2580 # Load image & textures data
2582 fbx_tmpl_tex
= fbx_template_get((b
'Texture', b
'KFbxFileTexture'))
2583 fbx_tmpl_img
= fbx_template_get((b
'Video', b
'KFbxVideo'))
2585 # Important to run all 'Video' ones first, embedded images are stored in those nodes.
2586 # XXX Note we simplify things here, assuming both matching Video and Texture will use same file path,
2587 # this may be a bit weak, if issue arise we'll fallback to plain connection stuff...
2588 for fbx_uuid
, fbx_item
in fbx_table_nodes
.items():
2589 fbx_obj
, blen_data
= fbx_item
2590 if fbx_obj
.id != b
'Video':
2592 fbx_item
[1] = blen_read_texture_image(fbx_tmpl_img
, fbx_obj
, basedir
, settings
)
2593 for fbx_uuid
, fbx_item
in fbx_table_nodes
.items():
2594 fbx_obj
, blen_data
= fbx_item
2595 if fbx_obj
.id != b
'Texture':
2597 fbx_item
[1] = blen_read_texture_image(fbx_tmpl_tex
, fbx_obj
, basedir
, settings
)
2600 perfmon
.step("FBX import: Cameras & Lamps...")
2605 fbx_tmpl
= fbx_template_get((b
'NodeAttribute', b
'KFbxCamera'))
2607 for fbx_uuid
, fbx_item
in fbx_table_nodes
.items():
2608 fbx_obj
, blen_data
= fbx_item
2609 if fbx_obj
.id != b
'NodeAttribute':
2611 if fbx_obj
.props
[-1] == b
'Camera':
2612 assert(blen_data
is None)
2613 fbx_item
[1] = blen_read_camera(fbx_tmpl
, fbx_obj
, global_scale
)
2619 fbx_tmpl
= fbx_template_get((b
'NodeAttribute', b
'KFbxLight'))
2621 for fbx_uuid
, fbx_item
in fbx_table_nodes
.items():
2622 fbx_obj
, blen_data
= fbx_item
2623 if fbx_obj
.id != b
'NodeAttribute':
2625 if fbx_obj
.props
[-1] == b
'Light':
2626 assert(blen_data
is None)
2627 fbx_item
[1] = blen_read_light(fbx_tmpl
, fbx_obj
, global_scale
)
2632 def connection_filter_ex(fbx_uuid
, fbx_id
, dct
):
2633 return [(c_found
[0], c_found
[1], c_type
)
2634 for (c_uuid
, c_type
) in dct
.get(fbx_uuid
, ())
2635 # 0 is used for the root node, which isnt in fbx_table_nodes
2636 for c_found
in (() if c_uuid
== 0 else (fbx_table_nodes
.get(c_uuid
, (None, None)),))
2637 if (fbx_id
is None) or (c_found
[0] and c_found
[0].id == fbx_id
)]
2639 def connection_filter_forward(fbx_uuid
, fbx_id
):
2640 return connection_filter_ex(fbx_uuid
, fbx_id
, fbx_connection_map
)
2642 def connection_filter_reverse(fbx_uuid
, fbx_id
):
2643 return connection_filter_ex(fbx_uuid
, fbx_id
, fbx_connection_map_reverse
)
2645 perfmon
.step("FBX import: Objects & Armatures...")
2647 # -- temporary helper hierarchy to build armatures and objects from
2648 # lookup from uuid to helper node. Used to build parent-child relations and later to look up animated nodes.
2649 fbx_helper_nodes
= {}
2652 # We build an intermediate hierarchy used to:
2653 # - Calculate and store bone orientation correction matrices. The same matrices will be reused for animation.
2654 # - Find/insert armature nodes.
2655 # - Filter leaf bones.
2658 fbx_helper_nodes
[0] = root_helper
= FbxImportHelperNode(None, None, None, False)
2659 root_helper
.is_root
= True
2662 fbx_tmpl
= fbx_template_get((b
'Model', b
'KFbxNode'))
2663 for a_uuid
, a_item
in fbx_table_nodes
.items():
2664 fbx_obj
, bl_data
= a_item
2665 if fbx_obj
is None or fbx_obj
.id != b
'Model':
2668 fbx_props
= (elem_find_first(fbx_obj
, b
'Properties70'),
2669 elem_find_first(fbx_tmpl
, b
'Properties70', fbx_elem_nil
))
2671 transform_data
= blen_read_object_transform_preprocess(fbx_props
, fbx_obj
, Matrix(), use_prepost_rot
)
2672 # Note: 'Root' "bones" are handled as (armature) objects.
2673 # Note: See T46912 for first FBX file I ever saw with 'Limb' bones - thought those were totally deprecated.
2674 is_bone
= fbx_obj
.props
[2] in {b
'LimbNode', b
'Limb'}
2675 fbx_helper_nodes
[a_uuid
] = FbxImportHelperNode(fbx_obj
, bl_data
, transform_data
, is_bone
)
2677 # add parent-child relations and add blender data to the node
2678 for fbx_link
in fbx_connections
.elems
:
2679 if fbx_link
.props
[0] != b
'OO':
2681 if fbx_link
.props_type
[1:3] == b
'LL':
2682 c_src
, c_dst
= fbx_link
.props
[1:3]
2683 parent
= fbx_helper_nodes
.get(c_dst
)
2687 child
= fbx_helper_nodes
.get(c_src
)
2689 # add blender data (meshes, lights, cameras, etc.) to a helper node
2690 fbx_sdata
, bl_data
= p_item
= fbx_table_nodes
.get(c_src
, (None, None))
2691 if fbx_sdata
is None:
2693 if fbx_sdata
.id not in {b
'Geometry', b
'NodeAttribute'}:
2695 parent
.bl_data
= bl_data
2698 child
.parent
= parent
2700 # find armatures (either an empty below a bone or a new node inserted at the bone
2701 root_helper
.find_armatures()
2703 # mark nodes that have bone children
2704 root_helper
.find_bone_children()
2706 # mark nodes that need a bone to attach child-bones to
2707 root_helper
.find_fake_bones()
2709 # mark leaf nodes that are only required to mark the end of their parent bone
2710 if settings
.ignore_leaf_bones
:
2711 root_helper
.mark_leaf_bones()
2713 # What a mess! Some bones have several BindPoses, some have none, clusters contain a bind pose as well,
2714 # and you can have several clusters per bone!
2715 # Maybe some conversion can be applied to put them all into the same frame of reference?
2717 # get the bind pose from pose elements
2718 for a_uuid
, a_item
in fbx_table_nodes
.items():
2719 fbx_obj
, bl_data
= a_item
2722 if fbx_obj
.id != b
'Pose':
2724 if fbx_obj
.props
[2] != b
'BindPose':
2726 for fbx_pose_node
in fbx_obj
.elems
:
2727 if fbx_pose_node
.id != b
'PoseNode':
2729 node_elem
= elem_find_first(fbx_pose_node
, b
'Node')
2730 node
= elem_uuid(node_elem
)
2731 matrix_elem
= elem_find_first(fbx_pose_node
, b
'Matrix')
2732 matrix
= array_to_matrix4(matrix_elem
.props
[0]) if matrix_elem
else None
2733 bone
= fbx_helper_nodes
.get(node
)
2735 # Store the matrix in the helper node.
2736 # There may be several bind pose matrices for the same node, but in tests they seem to be identical.
2737 bone
.bind_matrix
= matrix
# global space
2739 # get clusters and bind pose
2740 for helper_uuid
, helper_node
in fbx_helper_nodes
.items():
2741 if not helper_node
.is_bone
:
2743 for cluster_uuid
, cluster_link
in fbx_connection_map
.get(helper_uuid
, ()):
2744 if cluster_link
.props
[0] != b
'OO':
2746 fbx_cluster
, _
= fbx_table_nodes
.get(cluster_uuid
, (None, None))
2747 if fbx_cluster
is None or fbx_cluster
.id != b
'Deformer' or fbx_cluster
.props
[2] != b
'Cluster':
2750 # Get the bind pose from the cluster:
2751 tx_mesh_elem
= elem_find_first(fbx_cluster
, b
'Transform', default
=None)
2752 tx_mesh
= array_to_matrix4(tx_mesh_elem
.props
[0]) if tx_mesh_elem
else Matrix()
2754 tx_bone_elem
= elem_find_first(fbx_cluster
, b
'TransformLink', default
=None)
2755 tx_bone
= array_to_matrix4(tx_bone_elem
.props
[0]) if tx_bone_elem
else None
2757 tx_arm_elem
= elem_find_first(fbx_cluster
, b
'TransformAssociateModel', default
=None)
2758 tx_arm
= array_to_matrix4(tx_arm_elem
.props
[0]) if tx_arm_elem
else None
2760 mesh_matrix
= tx_mesh
2761 armature_matrix
= tx_arm
2764 mesh_matrix
= tx_bone
@ mesh_matrix
2765 helper_node
.bind_matrix
= tx_bone
# overwrite the bind matrix
2767 # Get the meshes driven by this cluster: (Shouldn't that be only one?)
2769 for skin_uuid
, skin_link
in fbx_connection_map
.get(cluster_uuid
):
2770 if skin_link
.props
[0] != b
'OO':
2772 fbx_skin
, _
= fbx_table_nodes
.get(skin_uuid
, (None, None))
2773 if fbx_skin
is None or fbx_skin
.id != b
'Deformer' or fbx_skin
.props
[2] != b
'Skin':
2775 for mesh_uuid
, mesh_link
in fbx_connection_map
.get(skin_uuid
):
2776 if mesh_link
.props
[0] != b
'OO':
2778 fbx_mesh
, _
= fbx_table_nodes
.get(mesh_uuid
, (None, None))
2779 if fbx_mesh
is None or fbx_mesh
.id != b
'Geometry' or fbx_mesh
.props
[2] != b
'Mesh':
2781 for object_uuid
, object_link
in fbx_connection_map
.get(mesh_uuid
):
2782 if object_link
.props
[0] != b
'OO':
2784 mesh_node
= fbx_helper_nodes
[object_uuid
]
2787 # If we get a valid mesh matrix (in bone space), store armature and
2788 # mesh global matrices, we need them to compute mesh's matrix_parent_inverse
2789 # when actually binding them via the modifier.
2790 # Note we assume all bones were bound with the same mesh/armature (global) matrix,
2791 # we do not support otherwise in Blender anyway!
2792 mesh_node
.armature_setup
[helper_node
.armature
] = (mesh_matrix
, armature_matrix
)
2793 meshes
.add(mesh_node
)
2795 helper_node
.clusters
.append((fbx_cluster
, meshes
))
2797 # convert bind poses from global space into local space
2798 root_helper
.make_bind_pose_local()
2800 # collect armature meshes
2801 root_helper
.collect_armature_meshes()
2803 # find the correction matrices to align FBX objects with their Blender equivalent
2804 root_helper
.find_correction_matrix(settings
)
2806 # build the Object/Armature/Bone hierarchy
2807 root_helper
.build_hierarchy(fbx_tmpl
, settings
, scene
, view_layer
)
2809 # Link the Object/Armature/Bone hierarchy
2810 root_helper
.link_hierarchy(fbx_tmpl
, settings
, scene
)
2812 # root_helper.print_info(0)
2815 perfmon
.step("FBX import: ShapeKeys...")
2817 # We can handle shapes.
2818 blend_shape_channels
= {} # We do not need Shapes themselves, but keyblocks, for anim.
2821 fbx_tmpl
= fbx_template_get((b
'Geometry', b
'KFbxShape'))
2823 for s_uuid
, s_item
in fbx_table_nodes
.items():
2824 fbx_sdata
, bl_sdata
= s_item
= fbx_table_nodes
.get(s_uuid
, (None, None))
2825 if fbx_sdata
is None or fbx_sdata
.id != b
'Geometry' or fbx_sdata
.props
[2] != b
'Shape':
2828 # shape -> blendshapechannel -> blendshape -> mesh.
2829 for bc_uuid
, bc_ctype
in fbx_connection_map
.get(s_uuid
, ()):
2830 if bc_ctype
.props
[0] != b
'OO':
2832 fbx_bcdata
, _bl_bcdata
= fbx_table_nodes
.get(bc_uuid
, (None, None))
2833 if fbx_bcdata
is None or fbx_bcdata
.id != b
'Deformer' or fbx_bcdata
.props
[2] != b
'BlendShapeChannel':
2837 for bs_uuid
, bs_ctype
in fbx_connection_map
.get(bc_uuid
, ()):
2838 if bs_ctype
.props
[0] != b
'OO':
2840 fbx_bsdata
, _bl_bsdata
= fbx_table_nodes
.get(bs_uuid
, (None, None))
2841 if fbx_bsdata
is None or fbx_bsdata
.id != b
'Deformer' or fbx_bsdata
.props
[2] != b
'BlendShape':
2843 for m_uuid
, m_ctype
in fbx_connection_map
.get(bs_uuid
, ()):
2844 if m_ctype
.props
[0] != b
'OO':
2846 fbx_mdata
, bl_mdata
= fbx_table_nodes
.get(m_uuid
, (None, None))
2847 if fbx_mdata
is None or fbx_mdata
.id != b
'Geometry' or fbx_mdata
.props
[2] != b
'Mesh':
2849 # Blenmeshes are assumed already created at that time!
2850 assert(isinstance(bl_mdata
, bpy
.types
.Mesh
))
2851 # And we have to find all objects using this mesh!
2853 for o_uuid
, o_ctype
in fbx_connection_map
.get(m_uuid
, ()):
2854 if o_ctype
.props
[0] != b
'OO':
2856 node
= fbx_helper_nodes
[o_uuid
]
2858 objects
.append(node
)
2859 meshes
.append((bl_mdata
, objects
))
2860 # BlendShape deformers are only here to connect BlendShapeChannels to meshes, nothing else to do.
2862 # keyblocks is a list of tuples (mesh, keyblock) matching that shape/blendshapechannel, for animation.
2863 keyblocks
= blen_read_shape(fbx_tmpl
, fbx_sdata
, fbx_bcdata
, meshes
, scene
)
2864 blend_shape_channels
[bc_uuid
] = keyblocks
2867 if settings
.use_subsurf
:
2868 perfmon
.step("FBX import: Subdivision surfaces")
2870 # Look through connections for subsurf in meshes and add it to the parent object
2872 for fbx_link
in fbx_connections
.elems
:
2873 if fbx_link
.props
[0] != b
'OO':
2875 if fbx_link
.props_type
[1:3] == b
'LL':
2876 c_src
, c_dst
= fbx_link
.props
[1:3]
2877 parent
= fbx_helper_nodes
.get(c_dst
)
2881 child
= fbx_helper_nodes
.get(c_src
)
2883 fbx_sdata
, bl_data
= fbx_table_nodes
.get(c_src
, (None, None))
2884 if fbx_sdata
.id != b
'Geometry':
2887 preview_levels
= elem_prop_first(elem_find_first(fbx_sdata
, b
'PreviewDivisionLevels'))
2888 render_levels
= elem_prop_first(elem_find_first(fbx_sdata
, b
'RenderDivisionLevels'))
2889 if isinstance(preview_levels
, int) and isinstance(render_levels
, int):
2890 mod
= parent
.bl_obj
.modifiers
.new('subsurf', 'SUBSURF')
2891 mod
.levels
= preview_levels
2892 mod
.render_levels
= render_levels
2897 perfmon
.step("FBX import: Animations...")
2901 fbx_tmpl_astack
= fbx_template_get((b
'AnimationStack', b
'FbxAnimStack'))
2902 fbx_tmpl_alayer
= fbx_template_get((b
'AnimationLayer', b
'FbxAnimLayer'))
2906 for as_uuid
, fbx_asitem
in fbx_table_nodes
.items():
2907 fbx_asdata
, _blen_data
= fbx_asitem
2908 if fbx_asdata
.id != b
'AnimationStack' or fbx_asdata
.props
[2] != b
'':
2910 stacks
[as_uuid
] = (fbx_asitem
, {})
2913 # (mixing is completely ignored for now, each layer results in an independent set of actions).
2914 def get_astacks_from_alayer(al_uuid
):
2915 for as_uuid
, as_ctype
in fbx_connection_map
.get(al_uuid
, ()):
2916 if as_ctype
.props
[0] != b
'OO':
2918 fbx_asdata
, _bl_asdata
= fbx_table_nodes
.get(as_uuid
, (None, None))
2919 if (fbx_asdata
is None or fbx_asdata
.id != b
'AnimationStack' or
2920 fbx_asdata
.props
[2] != b
'' or as_uuid
not in stacks
):
2923 for al_uuid
, fbx_alitem
in fbx_table_nodes
.items():
2924 fbx_aldata
, _blen_data
= fbx_alitem
2925 if fbx_aldata
.id != b
'AnimationLayer' or fbx_aldata
.props
[2] != b
'':
2927 for as_uuid
in get_astacks_from_alayer(al_uuid
):
2928 _fbx_asitem
, alayers
= stacks
[as_uuid
]
2929 alayers
[al_uuid
] = (fbx_alitem
, {})
2931 # AnimationCurveNodes (also the ones linked to actual animated data!).
2933 for acn_uuid
, fbx_acnitem
in fbx_table_nodes
.items():
2934 fbx_acndata
, _blen_data
= fbx_acnitem
2935 if fbx_acndata
.id != b
'AnimationCurveNode' or fbx_acndata
.props
[2] != b
'':
2937 cnode
= curvenodes
[acn_uuid
] = {}
2939 for n_uuid
, n_ctype
in fbx_connection_map
.get(acn_uuid
, ()):
2940 if n_ctype
.props
[0] != b
'OP':
2942 lnk_prop
= n_ctype
.props
[3]
2943 if lnk_prop
in {b
'Lcl Translation', b
'Lcl Rotation', b
'Lcl Scaling'}:
2944 # n_uuid can (????) be linked to root '0' node, instead of a mere object node... See T41712.
2945 ob
= fbx_helper_nodes
.get(n_uuid
, None)
2946 if ob
is None or ob
.is_root
:
2948 items
.append((ob
, lnk_prop
))
2949 elif lnk_prop
== b
'DeformPercent': # Shape keys.
2950 keyblocks
= blend_shape_channels
.get(n_uuid
, None)
2951 if keyblocks
is None:
2953 items
+= [(kb
, lnk_prop
) for kb
in keyblocks
]
2954 elif lnk_prop
== b
'FocalLength': # Camera lens.
2955 from bpy
.types
import Camera
2956 fbx_item
= fbx_table_nodes
.get(n_uuid
, None)
2957 if fbx_item
is None or not isinstance(fbx_item
[1], Camera
):
2960 items
.append((cam
, lnk_prop
))
2961 elif lnk_prop
== b
'DiffuseColor':
2962 from bpy
.types
import Material
2963 fbx_item
= fbx_table_nodes
.get(n_uuid
, None)
2964 if fbx_item
is None or not isinstance(fbx_item
[1], Material
):
2967 items
.append((mat
, lnk_prop
))
2968 print("WARNING! Importing material's animation is not supported for Nodal materials...")
2969 for al_uuid
, al_ctype
in fbx_connection_map
.get(acn_uuid
, ()):
2970 if al_ctype
.props
[0] != b
'OO':
2972 fbx_aldata
, _blen_aldata
= fbx_alitem
= fbx_table_nodes
.get(al_uuid
, (None, None))
2973 if fbx_aldata
is None or fbx_aldata
.id != b
'AnimationLayer' or fbx_aldata
.props
[2] != b
'':
2975 for as_uuid
in get_astacks_from_alayer(al_uuid
):
2976 _fbx_alitem
, anim_items
= stacks
[as_uuid
][1][al_uuid
]
2977 assert(_fbx_alitem
== fbx_alitem
)
2978 for item
, item_prop
in items
:
2979 # No need to keep curvenode FBX data here, contains nothing useful for us.
2980 anim_items
.setdefault(item
, {})[acn_uuid
] = (cnode
, item_prop
)
2982 # AnimationCurves (real animation data).
2983 for ac_uuid
, fbx_acitem
in fbx_table_nodes
.items():
2984 fbx_acdata
, _blen_data
= fbx_acitem
2985 if fbx_acdata
.id != b
'AnimationCurve' or fbx_acdata
.props
[2] != b
'':
2987 for acn_uuid
, acn_ctype
in fbx_connection_map
.get(ac_uuid
, ()):
2988 if acn_ctype
.props
[0] != b
'OP':
2990 fbx_acndata
, _bl_acndata
= fbx_table_nodes
.get(acn_uuid
, (None, None))
2991 if (fbx_acndata
is None or fbx_acndata
.id != b
'AnimationCurveNode' or
2992 fbx_acndata
.props
[2] != b
'' or acn_uuid
not in curvenodes
):
2994 # Note this is an infamous simplification of the compound props stuff,
2995 # seems to be standard naming but we'll probably have to be smarter to handle more exotic files?
2997 b
'd|X': 0, b
'd|Y': 1, b
'd|Z': 2,
2998 b
'd|DeformPercent': 0,
3000 }.get(acn_ctype
.props
[3], None)
3003 curvenodes
[acn_uuid
][ac_uuid
] = (fbx_acitem
, channel
)
3005 # And now that we have sorted all this, apply animations!
3006 blen_read_animations(fbx_tmpl_astack
, fbx_tmpl_alayer
, stacks
, scene
, settings
.anim_offset
)
3010 perfmon
.step("FBX import: Assign materials...")
3013 # link Material's to Geometry (via Model's)
3014 for fbx_uuid
, fbx_item
in fbx_table_nodes
.items():
3015 fbx_obj
, blen_data
= fbx_item
3016 if fbx_obj
.id != b
'Geometry':
3019 mesh
= fbx_table_nodes
.get(fbx_uuid
, (None, None))[1]
3021 # can happen in rare cases
3025 # In Blender, we link materials to data, typically (meshes), while in FBX they are linked to objects...
3026 # So we have to be careful not to re-add endlessly the same material to a mesh!
3027 # This can easily happen with 'baked' dupliobjects, see T44386.
3028 # TODO: add an option to link materials to objects in Blender instead?
3029 done_materials
= set()
3031 for (fbx_lnk
, fbx_lnk_item
, fbx_lnk_type
) in connection_filter_forward(fbx_uuid
, b
'Model'):
3033 fbx_lnk_uuid
= elem_uuid(fbx_lnk
)
3034 for (fbx_lnk_material
, material
, fbx_lnk_material_type
) in connection_filter_reverse(fbx_lnk_uuid
, b
'Material'):
3035 if material
not in done_materials
:
3036 mesh
.materials
.append(material
)
3037 done_materials
.add(material
)
3039 # We have to validate mesh polygons' ma_idx, see T41015!
3040 # Some FBX seem to have an extra 'default' material which is not defined in FBX file.
3041 if mesh
.validate_material_indices():
3042 print("WARNING: mesh '%s' had invalid material indices, those were reset to first material" % mesh
.name
)
3045 perfmon
.step("FBX import: Assign textures...")
3048 material_images
= {}
3050 fbx_tmpl
= fbx_template_get((b
'Material', b
'KFbxSurfacePhong'))
3051 # b'KFbxSurfaceLambert'
3053 def texture_mapping_set(fbx_obj
, node_texture
):
3054 assert(fbx_obj
.id == b
'Texture')
3056 fbx_props
= (elem_find_first(fbx_obj
, b
'Properties70'),
3057 elem_find_first(fbx_tmpl
, b
'Properties70', fbx_elem_nil
))
3058 loc
= elem_props_get_vector_3d(fbx_props
, b
'Translation', (0.0, 0.0, 0.0))
3059 rot
= tuple(-r
for r
in elem_props_get_vector_3d(fbx_props
, b
'Rotation', (0.0, 0.0, 0.0)))
3060 scale
= tuple(((1.0 / s
) if s
!= 0.0 else 1.0)
3061 for s
in elem_props_get_vector_3d(fbx_props
, b
'Scaling', (1.0, 1.0, 1.0)))
3062 clamp
= (bool(elem_props_get_enum(fbx_props
, b
'WrapModeU', 0)) or
3063 bool(elem_props_get_enum(fbx_props
, b
'WrapModeV', 0)))
3065 if (loc
== (0.0, 0.0, 0.0) and
3066 rot
== (0.0, 0.0, 0.0) and
3067 scale
== (1.0, 1.0, 1.0) and
3071 node_texture
.translation
= loc
3072 node_texture
.rotation
= rot
3073 node_texture
.scale
= scale
3075 node_texture
.extension
= 'EXTEND'
3077 for fbx_uuid
, fbx_item
in fbx_table_nodes
.items():
3078 fbx_obj
, blen_data
= fbx_item
3079 if fbx_obj
.id != b
'Material':
3082 material
= fbx_table_nodes
.get(fbx_uuid
, (None, None))[1]
3085 fbx_lnk_type
) in connection_filter_reverse(fbx_uuid
, b
'Texture'):
3087 if fbx_lnk_type
.props
[0] == b
'OP':
3088 lnk_type
= fbx_lnk_type
.props
[3]
3090 ma_wrap
= nodal_material_wrap_map
[material
]
3092 if lnk_type
in {b
'DiffuseColor', b
'3dsMax|maps|texmap_diffuse'}:
3093 ma_wrap
.base_color_texture
.image
= image
3094 texture_mapping_set(fbx_lnk
, ma_wrap
.base_color_texture
)
3095 elif lnk_type
in {b
'SpecularColor', b
'SpecularFactor'}:
3096 # Intensity actually, not color...
3097 ma_wrap
.specular_texture
.image
= image
3098 texture_mapping_set(fbx_lnk
, ma_wrap
.specular_texture
)
3099 elif lnk_type
in {b
'ReflectionColor', b
'ReflectionFactor', b
'3dsMax|maps|texmap_reflection'}:
3100 # Intensity actually, not color...
3101 ma_wrap
.metallic_texture
.image
= image
3102 texture_mapping_set(fbx_lnk
, ma_wrap
.metallic_texture
)
3103 elif lnk_type
in {b
'TransparentColor', b
'TransparentFactor'}:
3104 ma_wrap
.alpha_texture
.image
= image
3105 texture_mapping_set(fbx_lnk
, ma_wrap
.alpha_texture
)
3106 if use_alpha_decals
:
3107 material_decals
.add(material
)
3108 elif lnk_type
== b
'ShininessExponent':
3109 # That is probably reversed compared to expected results? TODO...
3110 ma_wrap
.roughness_texture
.image
= image
3111 texture_mapping_set(fbx_lnk
, ma_wrap
.roughness_texture
)
3112 # XXX, applications abuse bump!
3113 elif lnk_type
in {b
'NormalMap', b
'Bump', b
'3dsMax|maps|texmap_bump'}:
3114 ma_wrap
.normalmap_texture
.image
= image
3115 texture_mapping_set(fbx_lnk
, ma_wrap
.normalmap_texture
)
3117 elif lnk_type == b'Bump':
3118 # TODO displacement...
3120 elif lnk_type
in {b
'EmissiveColor'}:
3121 ma_wrap
.emission_color_texture
.image
= image
3122 texture_mapping_set(fbx_lnk
, ma_wrap
.emission_color_texture
)
3124 print("WARNING: material link %r ignored" % lnk_type
)
3126 material_images
.setdefault(material
, {})[lnk_type
] = image
3128 # Check if the diffuse image has an alpha channel,
3129 # if so, use the alpha channel.
3131 # Note: this could be made optional since images may have alpha but be entirely opaque
3132 for fbx_uuid
, fbx_item
in fbx_table_nodes
.items():
3133 fbx_obj
, blen_data
= fbx_item
3134 if fbx_obj
.id != b
'Material':
3136 material
= fbx_table_nodes
.get(fbx_uuid
, (None, None))[1]
3137 image
= material_images
.get(material
, {}).get(b
'DiffuseColor', None)
3139 if image
and image
.depth
== 32:
3140 if use_alpha_decals
:
3141 material_decals
.add(material
)
3143 ma_wrap
= nodal_material_wrap_map
[material
]
3144 ma_wrap
.alpha_texture
.use_alpha
= True
3145 ma_wrap
.alpha_texture
.copy_from(ma_wrap
.base_color_texture
)
3147 # Propagate mapping from diffuse to all other channels which have none defined.
3148 # XXX Commenting for now, I do not really understand the logic here, why should diffuse mapping
3149 # be applied to all others if not defined for them???
3150 # ~ ma_wrap = nodal_material_wrap_map[material]
3151 # ~ ma_wrap.mapping_set_from_diffuse()
3155 perfmon
.step("FBX import: Cycles z-offset workaround...")
3158 # Annoying workaround for cycles having no z-offset
3159 if material_decals
and use_alpha_decals
:
3160 for fbx_uuid
, fbx_item
in fbx_table_nodes
.items():
3161 fbx_obj
, blen_data
= fbx_item
3162 if fbx_obj
.id != b
'Geometry':
3164 if fbx_obj
.props
[-1] == b
'Mesh':
3167 if decal_offset
!= 0.0:
3168 for material
in mesh
.materials
:
3169 if material
in material_decals
:
3170 for v
in mesh
.vertices
:
3171 v
.co
+= v
.normal
* decal_offset
3174 for obj
in (obj
for obj
in bpy
.data
.objects
if obj
.data
== mesh
):
3175 obj
.cycles_visibility
.shadow
= False
3178 perfmon
.level_down()
3180 perfmon
.level_down("Import finished.")