1 # SPDX-License-Identifier: GPL-2.0-or-later
3 # Script copyright (C) Blender Foundation
5 # FBX 7.1.0 -> 7.4.0 loader for Blender
7 # Not totally pep8 compliant.
8 # pep8 import_fbx.py --ignore=E501,E123,E702,E125
12 if "parse_fbx" in locals():
13 importlib
.reload(parse_fbx
)
14 if "fbx_utils" in locals():
15 importlib
.reload(fbx_utils
)
18 from bpy
.app
.translations
import pgettext_tip
as tip_
19 from mathutils
import Matrix
, Euler
, Vector
21 # Also imported in .fbx_utils, so importing here is unlikely to further affect Blender startup time.
26 from . import parse_fbx
, fbx_utils
28 from .parse_fbx
import (
32 from .fbx_utils
import (
34 units_blender_to_fbx_factor
,
43 astype_view_signedness
,
46 # global singleton, assign on execution
50 convert_deg_to_rad_iter
= units_convertor_iter("degree", "radian")
52 MAT_CONVERT_BONE
= fbx_utils
.MAT_CONVERT_BONE
.inverted()
53 MAT_CONVERT_LIGHT
= fbx_utils
.MAT_CONVERT_LIGHT
.inverted()
54 MAT_CONVERT_CAMERA
= fbx_utils
.MAT_CONVERT_CAMERA
.inverted()
57 def validate_blend_names(name
):
58 assert(type(name
) == bytes
)
59 # Blender typically does not accept names over 63 bytes...
62 h
= hashlib
.sha1(name
).hexdigest()
64 name_utf8
= name
[:n
].decode('utf-8', 'replace') + "_" + h
[:7]
65 while len(name_utf8
.encode()) > 63:
67 name_utf8
= name
[:n
].decode('utf-8', 'replace') + "_" + h
[:7]
70 # We use 'replace' even though FBX 'specs' say it should always be utf8, see T53841.
71 return name
.decode('utf-8', 'replace')
74 def elem_find_first(elem
, id_search
, default
=None):
75 for fbx_item
in elem
.elems
:
76 if fbx_item
.id == id_search
:
81 def elem_find_iter(elem
, id_search
):
82 for fbx_item
in elem
.elems
:
83 if fbx_item
.id == id_search
:
87 def elem_find_first_string(elem
, id_search
):
88 fbx_item
= elem_find_first(elem
, id_search
)
89 if fbx_item
is not None and fbx_item
.props
: # Do not error on complete empty properties (see T45291).
90 assert(len(fbx_item
.props
) == 1)
91 assert(fbx_item
.props_type
[0] == data_types
.STRING
)
92 return fbx_item
.props
[0].decode('utf-8', 'replace')
96 def elem_find_first_string_as_bytes(elem
, id_search
):
97 fbx_item
= elem_find_first(elem
, id_search
)
98 if fbx_item
is not None and fbx_item
.props
: # Do not error on complete empty properties (see T45291).
99 assert(len(fbx_item
.props
) == 1)
100 assert(fbx_item
.props_type
[0] == data_types
.STRING
)
101 return fbx_item
.props
[0] # Keep it as bytes as requested...
105 def elem_find_first_bytes(elem
, id_search
, decode
=True):
106 fbx_item
= elem_find_first(elem
, id_search
)
107 if fbx_item
is not None and fbx_item
.props
: # Do not error on complete empty properties (see T45291).
108 assert(len(fbx_item
.props
) == 1)
109 assert(fbx_item
.props_type
[0] == data_types
.BYTES
)
110 return fbx_item
.props
[0]
115 return "%s: props[%d=%r], elems=(%r)" % (
118 ", ".join([repr(p
) for p
in elem
.props
]),
120 b
", ".join([e
.id for e
in elem
.elems
]),
124 def elem_split_name_class(elem
):
125 assert(elem
.props_type
[-2] == data_types
.STRING
)
126 elem_name
, elem_class
= elem
.props
[-2].split(b
'\x00\x01')
127 return elem_name
, elem_class
130 def elem_name_ensure_class(elem
, clss
=...):
131 elem_name
, elem_class
= elem_split_name_class(elem
)
133 assert(elem_class
== clss
)
134 return validate_blend_names(elem_name
)
137 def elem_name_ensure_classes(elem
, clss
=...):
138 elem_name
, elem_class
= elem_split_name_class(elem
)
140 assert(elem_class
in clss
)
141 return validate_blend_names(elem_name
)
144 def elem_split_name_class_nodeattr(elem
):
145 assert(elem
.props_type
[-2] == data_types
.STRING
)
146 elem_name
, elem_class
= elem
.props
[-2].split(b
'\x00\x01')
147 assert(elem_class
== b
'NodeAttribute')
148 assert(elem
.props_type
[-1] == data_types
.STRING
)
149 elem_class
= elem
.props
[-1]
150 return elem_name
, elem_class
154 assert(elem
.props_type
[0] == data_types
.INT64
)
158 def elem_prop_first(elem
, default
=None):
159 return elem
.props
[0] if (elem
is not None) and elem
.props
else default
164 # Properties70: { ... P:
165 def elem_props_find_first(elem
, elem_prop_id
):
167 # When properties are not found... Should never happen, but happens - as usual.
169 # support for templates (tuple of elems)
170 if type(elem
) is not FBXElem
:
171 assert(type(elem
) is tuple)
173 result
= elem_props_find_first(e
, elem_prop_id
)
174 if result
is not None:
176 assert(len(elem
) > 0)
179 for subelem
in elem
.elems
:
180 assert(subelem
.id == b
'P')
181 if subelem
.props
[0] == elem_prop_id
:
186 def elem_props_get_color_rgb(elem
, elem_prop_id
, default
=None):
187 elem_prop
= elem_props_find_first(elem
, elem_prop_id
)
188 if elem_prop
is not None:
189 assert(elem_prop
.props
[0] == elem_prop_id
)
190 if elem_prop
.props
[1] == b
'Color':
192 assert(elem_prop
.props
[1] == b
'Color')
193 assert(elem_prop
.props
[2] == b
'')
195 assert(elem_prop
.props
[1] == b
'ColorRGB')
196 assert(elem_prop
.props
[2] == b
'Color')
197 assert(elem_prop
.props_type
[4:7] == bytes((data_types
.FLOAT64
,)) * 3)
198 return elem_prop
.props
[4:7]
202 def elem_props_get_vector_3d(elem
, elem_prop_id
, default
=None):
203 elem_prop
= elem_props_find_first(elem
, elem_prop_id
)
204 if elem_prop
is not None:
205 assert(elem_prop
.props_type
[4:7] == bytes((data_types
.FLOAT64
,)) * 3)
206 return elem_prop
.props
[4:7]
210 def elem_props_get_number(elem
, elem_prop_id
, default
=None):
211 elem_prop
= elem_props_find_first(elem
, elem_prop_id
)
212 if elem_prop
is not None:
213 assert(elem_prop
.props
[0] == elem_prop_id
)
214 if elem_prop
.props
[1] == b
'double':
215 assert(elem_prop
.props
[1] == b
'double')
216 assert(elem_prop
.props
[2] == b
'Number')
218 assert(elem_prop
.props
[1] == b
'Number')
219 assert(elem_prop
.props
[2] == b
'')
221 # we could allow other number types
222 assert(elem_prop
.props_type
[4] == data_types
.FLOAT64
)
224 return elem_prop
.props
[4]
228 def elem_props_get_integer(elem
, elem_prop_id
, default
=None):
229 elem_prop
= elem_props_find_first(elem
, elem_prop_id
)
230 if elem_prop
is not None:
231 assert(elem_prop
.props
[0] == elem_prop_id
)
232 if elem_prop
.props
[1] == b
'int':
233 assert(elem_prop
.props
[1] == b
'int')
234 assert(elem_prop
.props
[2] == b
'Integer')
235 elif elem_prop
.props
[1] == b
'ULongLong':
236 assert(elem_prop
.props
[1] == b
'ULongLong')
237 assert(elem_prop
.props
[2] == b
'')
239 # we could allow other number types
240 assert(elem_prop
.props_type
[4] in {data_types
.INT32
, data_types
.INT64
})
242 return elem_prop
.props
[4]
246 def elem_props_get_bool(elem
, elem_prop_id
, default
=None):
247 elem_prop
= elem_props_find_first(elem
, elem_prop_id
)
248 if elem_prop
is not None:
249 assert(elem_prop
.props
[0] == elem_prop_id
)
250 # b'Bool' with a capital seems to be used for animated property... go figure...
251 assert(elem_prop
.props
[1] in {b
'bool', b
'Bool'})
252 assert(elem_prop
.props
[2] == b
'')
254 # we could allow other number types
255 assert(elem_prop
.props_type
[4] == data_types
.INT32
)
256 assert(elem_prop
.props
[4] in {0, 1})
258 return bool(elem_prop
.props
[4])
262 def elem_props_get_enum(elem
, elem_prop_id
, default
=None):
263 elem_prop
= elem_props_find_first(elem
, elem_prop_id
)
264 if elem_prop
is not None:
265 assert(elem_prop
.props
[0] == elem_prop_id
)
266 assert(elem_prop
.props
[1] == b
'enum')
267 assert(elem_prop
.props
[2] == b
'')
268 assert(elem_prop
.props
[3] == b
'')
270 # we could allow other number types
271 assert(elem_prop
.props_type
[4] == data_types
.INT32
)
273 return elem_prop
.props
[4]
277 def elem_props_get_visibility(elem
, elem_prop_id
, default
=None):
278 elem_prop
= elem_props_find_first(elem
, elem_prop_id
)
279 if elem_prop
is not None:
280 assert(elem_prop
.props
[0] == elem_prop_id
)
281 assert(elem_prop
.props
[1] == b
'Visibility')
282 assert(elem_prop
.props
[2] == b
'')
284 # we could allow other number types
285 assert(elem_prop
.props_type
[4] == data_types
.FLOAT64
)
287 return elem_prop
.props
[4]
291 # ----------------------------------------------------------------------------
296 from collections
import namedtuple
299 FBXTransformData
= namedtuple("FBXTransformData", (
301 "rot", "rot_ofs", "rot_piv", "pre_rot", "pst_rot", "rot_ord", "rot_alt_mat", "geom_rot",
302 "sca", "sca_ofs", "sca_piv", "geom_sca",
306 def blen_read_custom_properties(fbx_obj
, blen_obj
, settings
):
307 # There doesn't seem to be a way to put user properties into templates, so this only get the object properties:
308 fbx_obj_props
= elem_find_first(fbx_obj
, b
'Properties70')
310 for fbx_prop
in fbx_obj_props
.elems
:
311 assert(fbx_prop
.id == b
'P')
313 if b
'U' in fbx_prop
.props
[3]:
314 if fbx_prop
.props
[0] == b
'UDP3DSMAX':
315 # Special case for 3DS Max user properties:
316 assert(fbx_prop
.props
[1] == b
'KString')
317 assert(fbx_prop
.props_type
[4] == data_types
.STRING
)
318 items
= fbx_prop
.props
[4].decode('utf-8', 'replace')
319 for item
in items
.split('\r\n'):
321 split_item
= item
.split('=', 1)
322 if len(split_item
) != 2:
323 split_item
= item
.split(':', 1)
324 if len(split_item
) != 2:
325 print("cannot parse UDP3DSMAX custom property '%s', ignoring..." % item
)
327 prop_name
, prop_value
= split_item
328 prop_name
= validate_blend_names(prop_name
.strip().encode('utf-8'))
329 blen_obj
[prop_name
] = prop_value
.strip()
331 prop_name
= validate_blend_names(fbx_prop
.props
[0])
332 prop_type
= fbx_prop
.props
[1]
333 if prop_type
in {b
'Vector', b
'Vector3D', b
'Color', b
'ColorRGB'}:
334 assert(fbx_prop
.props_type
[4:7] == bytes((data_types
.FLOAT64
,)) * 3)
335 blen_obj
[prop_name
] = fbx_prop
.props
[4:7]
336 elif prop_type
in {b
'Vector4', b
'ColorRGBA'}:
337 assert(fbx_prop
.props_type
[4:8] == bytes((data_types
.FLOAT64
,)) * 4)
338 blen_obj
[prop_name
] = fbx_prop
.props
[4:8]
339 elif prop_type
== b
'Vector2D':
340 assert(fbx_prop
.props_type
[4:6] == bytes((data_types
.FLOAT64
,)) * 2)
341 blen_obj
[prop_name
] = fbx_prop
.props
[4:6]
342 elif prop_type
in {b
'Integer', b
'int'}:
343 assert(fbx_prop
.props_type
[4] == data_types
.INT32
)
344 blen_obj
[prop_name
] = fbx_prop
.props
[4]
345 elif prop_type
== b
'KString':
346 assert(fbx_prop
.props_type
[4] == data_types
.STRING
)
347 blen_obj
[prop_name
] = fbx_prop
.props
[4].decode('utf-8', 'replace')
348 elif prop_type
in {b
'Number', b
'double', b
'Double'}:
349 assert(fbx_prop
.props_type
[4] == data_types
.FLOAT64
)
350 blen_obj
[prop_name
] = fbx_prop
.props
[4]
351 elif prop_type
in {b
'Float', b
'float'}:
352 assert(fbx_prop
.props_type
[4] == data_types
.FLOAT32
)
353 blen_obj
[prop_name
] = fbx_prop
.props
[4]
354 elif prop_type
in {b
'Bool', b
'bool'}:
355 assert(fbx_prop
.props_type
[4] == data_types
.INT32
)
356 blen_obj
[prop_name
] = fbx_prop
.props
[4] != 0
357 elif prop_type
in {b
'Enum', b
'enum'}:
358 assert(fbx_prop
.props_type
[4:6] == bytes((data_types
.INT32
, data_types
.STRING
)))
359 val
= fbx_prop
.props
[4]
360 if settings
.use_custom_props_enum_as_string
and fbx_prop
.props
[5]:
361 enum_items
= fbx_prop
.props
[5].decode('utf-8', 'replace').split('~')
362 if val
>= 0 and val
< len(enum_items
):
363 blen_obj
[prop_name
] = enum_items
[val
]
365 print ("WARNING: User property '%s' has wrong enum value, skipped" % prop_name
)
367 blen_obj
[prop_name
] = val
369 print ("WARNING: User property type '%s' is not supported" % prop_type
.decode('utf-8', 'replace'))
372 def blen_read_object_transform_do(transform_data
):
373 # This is a nightmare. FBX SDK uses Maya way to compute the transformation matrix of a node - utterly simple:
375 # WorldTransform = ParentWorldTransform @ T @ Roff @ Rp @ Rpre @ R @ Rpost-1 @ Rp-1 @ Soff @ Sp @ S @ Sp-1
377 # Where all those terms are 4 x 4 matrices that contain:
378 # WorldTransform: Transformation matrix of the node in global space.
379 # ParentWorldTransform: Transformation matrix of the parent node in global space.
381 # Roff: Rotation offset
385 # Rpost-1: Inverse of the post-rotation (FBX 2011 documentation incorrectly specifies this without inversion)
386 # Rp-1: Inverse of the rotation pivot
387 # Soff: Scaling offset
390 # Sp-1: Inverse of the scaling pivot
392 # But it was still too simple, and FBX notion of compatibility is... quite specific. So we also have to
393 # support 3DSMax way:
395 # WorldTransform = ParentWorldTransform @ T @ R @ S @ OT @ OR @ OS
397 # Where all those terms are 4 x 4 matrices that contain:
398 # WorldTransform: Transformation matrix of the node in global space
399 # ParentWorldTransform: Transformation matrix of the parent node in global space
403 # OT: Geometric transform translation
404 # OR: Geometric transform rotation
405 # OS: Geometric transform scale
408 # Geometric transformations ***are not inherited***: ParentWorldTransform does not contain the OT, OR, OS
409 # of WorldTransform's parent node.
410 # The R matrix takes into account the rotation order. Other rotation matrices are always 'XYZ' order.
412 # Taken from https://help.autodesk.com/view/FBX/2020/ENU/
413 # ?guid=FBX_Developer_Help_nodes_and_scene_graph_fbx_nodes_computing_transformation_matrix_html
416 lcl_translation
= Matrix
.Translation(transform_data
.loc
)
417 geom_loc
= Matrix
.Translation(transform_data
.geom_loc
)
420 to_rot
= lambda rot
, rot_ord
: Euler(convert_deg_to_rad_iter(rot
), rot_ord
).to_matrix().to_4x4()
421 lcl_rot
= to_rot(transform_data
.rot
, transform_data
.rot_ord
) @ transform_data
.rot_alt_mat
422 pre_rot
= to_rot(transform_data
.pre_rot
, 'XYZ')
423 pst_rot
= to_rot(transform_data
.pst_rot
, 'XYZ')
424 geom_rot
= to_rot(transform_data
.geom_rot
, 'XYZ')
426 rot_ofs
= Matrix
.Translation(transform_data
.rot_ofs
)
427 rot_piv
= Matrix
.Translation(transform_data
.rot_piv
)
428 sca_ofs
= Matrix
.Translation(transform_data
.sca_ofs
)
429 sca_piv
= Matrix
.Translation(transform_data
.sca_piv
)
433 lcl_scale
[0][0], lcl_scale
[1][1], lcl_scale
[2][2] = transform_data
.sca
434 geom_scale
= Matrix();
435 geom_scale
[0][0], geom_scale
[1][1], geom_scale
[2][2] = transform_data
.geom_sca
443 pst_rot
.inverted_safe() @
444 rot_piv
.inverted_safe() @
448 sca_piv
.inverted_safe()
450 geom_mat
= geom_loc
@ geom_rot
@ geom_scale
451 # We return mat without 'geometric transforms' too, because it is to be used for children, sigh...
452 return (base_mat
@ geom_mat
, base_mat
, geom_mat
)
455 # XXX This might be weak, now that we can add vgroups from both bones and shapes, name collisions become
456 # more likely, will have to make this more robust!!!
457 def add_vgroup_to_objects(vg_indices
, vg_weights
, vg_name
, objects
):
458 assert(len(vg_indices
) == len(vg_weights
))
461 # We replace/override here...
462 vg
= obj
.vertex_groups
.get(vg_name
)
464 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
, global_scale
):
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:
581 # Add each keyframe to the keyframe dict
582 def store_keyframe(fc
, frame
, value
):
583 fc_key
= (fc
.data_path
, fc
.array_index
)
584 if not keyframes
.get(fc_key
):
585 keyframes
[fc_key
] = []
586 keyframes
[fc_key
].extend((frame
, value
))
588 if isinstance(item
, Material
):
590 props
= [("diffuse_color", 3, grpname
or "Diffuse Color")]
591 elif isinstance(item
, ShapeKey
):
592 props
= [(item
.path_from_id("value"), 1, "Key")]
593 elif isinstance(item
, Camera
):
594 props
= [(item
.path_from_id("lens"), 1, "Camera"), (item
.dof
.path_from_id("focus_distance"), 1, "Camera")]
595 else: # Object or PoseBone:
597 bl_obj
= item
.bl_obj
.pose
.bones
[item
.bl_bone
]
601 # We want to create actions for objects, but for bones we 'reuse' armatures' actions!
602 grpname
= bl_obj
.name
604 # Since we might get other channels animated in the end, due to all FBX transform magic,
605 # we need to add curves for whole loc/rot/scale in any case.
606 props
= [(bl_obj
.path_from_id("location"), 3, grpname
or "Location"),
608 (bl_obj
.path_from_id("scale"), 3, grpname
or "Scale")]
609 rot_mode
= bl_obj
.rotation_mode
610 if rot_mode
== 'QUATERNION':
611 props
[1] = (bl_obj
.path_from_id("rotation_quaternion"), 4, grpname
or "Quaternion Rotation")
612 elif rot_mode
== 'AXIS_ANGLE':
613 props
[1] = (bl_obj
.path_from_id("rotation_axis_angle"), 4, grpname
or "Axis Angle Rotation")
615 props
[1] = (bl_obj
.path_from_id("rotation_euler"), 3, grpname
or "Euler Rotation")
617 blen_curves
= [action
.fcurves
.new(prop
, index
=channel
, action_group
=grpname
)
618 for prop
, nbr_channels
, grpname
in props
for channel
in range(nbr_channels
)]
620 if isinstance(item
, Material
):
621 for frame
, values
in blen_read_animations_curves_iter(fbx_curves
, anim_offset
, 0, fps
):
623 for v
, (fbxprop
, channel
, _fbx_acdata
) in values
:
624 assert(fbxprop
== b
'DiffuseColor')
625 assert(channel
in {0, 1, 2})
628 for fc
, v
in zip(blen_curves
, value
):
629 store_keyframe(fc
, frame
, v
)
631 elif isinstance(item
, ShapeKey
):
632 for frame
, values
in blen_read_animations_curves_iter(fbx_curves
, anim_offset
, 0, fps
):
634 for v
, (fbxprop
, channel
, _fbx_acdata
) in values
:
635 assert(fbxprop
== b
'DeformPercent')
639 for fc
, v
in zip(blen_curves
, (value
,)):
640 store_keyframe(fc
, frame
, v
)
642 elif isinstance(item
, Camera
):
643 for frame
, values
in blen_read_animations_curves_iter(fbx_curves
, anim_offset
, 0, fps
):
646 for v
, (fbxprop
, channel
, _fbx_acdata
) in values
:
647 assert(fbxprop
== b
'FocalLength' or fbxprop
== b
'FocusDistance' )
649 if (fbxprop
== b
'FocalLength' ):
651 elif(fbxprop
== b
'FocusDistance'):
652 focus_distance
= v
/ 1000 * global_scale
654 for fc
, v
in zip(blen_curves
, (focal_length
, focus_distance
)):
655 store_keyframe(fc
, frame
, v
)
657 else: # Object or PoseBone:
659 bl_obj
= item
.bl_obj
.pose
.bones
[item
.bl_bone
]
663 transform_data
= item
.fbx_transform_data
664 rot_eul_prev
= bl_obj
.rotation_euler
.copy()
665 rot_quat_prev
= bl_obj
.rotation_quaternion
.copy()
667 # Pre-compute inverted local rest matrix of the bone, if relevant.
668 restmat_inv
= item
.get_bind_matrix().inverted_safe() if item
.is_bone
else None
670 for frame
, values
in blen_read_animations_curves_iter(fbx_curves
, anim_offset
, 0, fps
):
671 for v
, (fbxprop
, channel
, _fbx_acdata
) in values
:
672 if fbxprop
== b
'Lcl Translation':
673 transform_data
.loc
[channel
] = v
674 elif fbxprop
== b
'Lcl Rotation':
675 transform_data
.rot
[channel
] = v
676 elif fbxprop
== b
'Lcl Scaling':
677 transform_data
.sca
[channel
] = v
678 mat
, _
, _
= blen_read_object_transform_do(transform_data
)
680 # compensate for changes in the local matrix during processing
681 if item
.anim_compensation_matrix
:
682 mat
= mat
@ item
.anim_compensation_matrix
684 # apply pre- and post matrix
685 # post-matrix will contain any correction for lights, camera and bone orientation
686 # pre-matrix will contain any correction for a parent's correction matrix or the global matrix
688 mat
= item
.pre_matrix
@ mat
690 mat
= mat
@ item
.post_matrix
692 # And now, remove that rest pose matrix from current mat (also in parent space).
694 mat
= restmat_inv
@ mat
696 # Now we have a virtual matrix of transform from AnimCurves, we can insert keyframes!
697 loc
, rot
, sca
= mat
.decompose()
698 if rot_mode
== 'QUATERNION':
699 if rot_quat_prev
.dot(rot
) < 0.0:
702 elif rot_mode
== 'AXIS_ANGLE':
703 vec
, ang
= rot
.to_axis_angle()
704 rot
= ang
, vec
.x
, vec
.y
, vec
.z
706 rot
= rot
.to_euler(rot_mode
, rot_eul_prev
)
709 # Add each keyframe and its value to the keyframe dict
710 for fc
, value
in zip(blen_curves
, chain(loc
, rot
, sca
)):
711 store_keyframe(fc
, frame
, value
)
713 # Add all keyframe points to the fcurves at once and modify them after
714 for fc_key
, key_values
in keyframes
.items():
715 data_path
, index
= fc_key
717 # Add all keyframe points at once
718 fcurve
= action
.fcurves
.find(data_path
=data_path
, index
=index
)
719 num_keys
= len(key_values
) // 2
720 fcurve
.keyframe_points
.add(num_keys
)
721 fcurve
.keyframe_points
.foreach_set('co', key_values
)
722 linear_enum_value
= bpy
.types
.Keyframe
.bl_rna
.properties
['interpolation'].enum_items
['LINEAR'].value
723 fcurve
.keyframe_points
.foreach_set('interpolation', (linear_enum_value
,) * num_keys
)
725 # Since we inserted our keyframes in 'ultra-fast' mode, we have to update the fcurves now.
726 for fc
in blen_curves
:
730 def blen_read_animations(fbx_tmpl_astack
, fbx_tmpl_alayer
, stacks
, scene
, anim_offset
, global_scale
):
732 Recreate an action per stack/layer/object combinations.
733 Only the first found action is linked to objects, more complex setups are not handled,
734 it's up to user to reproduce them!
736 from bpy
.types
import ShapeKey
, Material
, Camera
739 for as_uuid
, ((fbx_asdata
, _blen_data
), alayers
) in stacks
.items():
740 stack_name
= elem_name_ensure_class(fbx_asdata
, b
'AnimStack')
741 for al_uuid
, ((fbx_aldata
, _blen_data
), items
) in alayers
.items():
742 layer_name
= elem_name_ensure_class(fbx_aldata
, b
'AnimLayer')
743 for item
, cnodes
in items
.items():
744 if isinstance(item
, Material
):
746 elif isinstance(item
, ShapeKey
):
747 id_data
= item
.id_data
748 elif isinstance(item
, Camera
):
751 id_data
= item
.bl_obj
752 # XXX Ignore rigged mesh animations - those are a nightmare to handle, see note about it in
753 # FbxImportHelperNode class definition.
754 if id_data
and id_data
.type == 'MESH' and id_data
.parent
and id_data
.parent
.type == 'ARMATURE':
759 # Create new action if needed (should always be needed, except for keyblocks from shapekeys cases).
760 key
= (as_uuid
, al_uuid
, id_data
)
761 action
= actions
.get(key
)
763 if stack_name
== layer_name
:
764 action_name
= "|".join((id_data
.name
, stack_name
))
766 action_name
= "|".join((id_data
.name
, stack_name
, layer_name
))
767 actions
[key
] = action
= bpy
.data
.actions
.new(action_name
)
768 action
.use_fake_user
= True
769 # If none yet assigned, assign this action to id_data.
770 if not id_data
.animation_data
:
771 id_data
.animation_data_create()
772 if not id_data
.animation_data
.action
:
773 id_data
.animation_data
.action
= action
774 # And actually populate the action!
775 blen_read_animations_action_item(action
, item
, cnodes
, scene
.render
.fps
, anim_offset
, global_scale
)
781 def blen_read_geom_layerinfo(fbx_layer
):
783 validate_blend_names(elem_find_first_string_as_bytes(fbx_layer
, b
'Name')),
784 elem_find_first_string_as_bytes(fbx_layer
, b
'MappingInformationType'),
785 elem_find_first_string_as_bytes(fbx_layer
, b
'ReferenceInformationType'),
789 def blen_read_geom_validate_blen_data(blen_data
, blen_dtype
, item_size
):
790 """Validate blen_data when it's not a bpy_prop_collection.
791 Returns whether blen_data is a bpy_prop_collection"""
792 blen_data_is_collection
= isinstance(blen_data
, bpy
.types
.bpy_prop_collection
)
793 if not blen_data_is_collection
:
795 assert(len(blen_data
.shape
) == 2)
796 assert(blen_data
.shape
[1] == item_size
)
797 assert(blen_data
.dtype
== blen_dtype
)
798 return blen_data_is_collection
801 def blen_read_geom_parse_fbx_data(fbx_data
, stride
, item_size
):
802 """Parse fbx_data as an array.array into a 2d np.ndarray that shares the same memory, where each row is a single
804 # Technically stride < item_size could be supported, but there's probably not a use case for it since it would
805 # result in a view of the data with self-overlapping memory.
806 assert(stride
>= item_size
)
807 # View the array.array as an np.ndarray.
808 fbx_data_np
= parray_as_ndarray(fbx_data
)
810 if stride
== item_size
:
812 # Need to make sure fbx_data_np has a whole number of items to be able to view item_size elements per row.
813 items_remainder
= len(fbx_data_np
) % item_size
815 print("ERROR: not a whole number of items in this FBX layer, skipping the partial item!")
816 fbx_data_np
= fbx_data_np
[:-items_remainder
]
817 fbx_data_np
= fbx_data_np
.reshape(-1, item_size
)
819 # Create a view of fbx_data_np that is only the first item_size elements of each stride. Note that the view will
820 # not be C-contiguous.
821 stride_remainder
= len(fbx_data_np
) % stride
823 if stride_remainder
< item_size
:
824 print("ERROR: not a whole number of items in this FBX layer, skipping the partial item!")
825 # Not enough in the remainder for a full item, so cut off the partial stride
826 fbx_data_np
= fbx_data_np
[:-stride_remainder
]
827 # Reshape to one stride per row and then create a view that includes only the first item_size elements
829 fbx_data_np
= fbx_data_np
.reshape(-1, stride
)[:, :item_size
]
831 print("ERROR: not a whole number of strides in this FBX layer! There are a whole number of items, but"
832 " this could indicate an error!")
833 # There is not a whole number of strides, but there is a whole number of items.
834 # This is a pain to deal with because fbx_data_np.reshape(-1, stride) is not possible.
835 # A view of just the items can be created using stride_tricks.as_strided by specifying the shape and
836 # strides of the view manually.
837 # Extreme care must be taken when using stride_tricks.as_strided because improper usage can result in
838 # a view that gives access to memory outside the array.
839 from numpy
.lib
import stride_tricks
841 # fbx_data_np should always start off as flat and C-contiguous.
842 assert(fbx_data_np
.strides
== (fbx_data_np
.itemsize
,))
844 num_whole_strides
= len(fbx_data_np
) // stride
845 # Plus the one partial stride that is enough elements for a complete item.
846 num_items
= num_whole_strides
+ 1
847 shape
= (num_items
, item_size
)
849 # strides are the number of bytes to step to get to the next element, for each axis.
850 step_per_item
= fbx_data_np
.itemsize
* stride
851 step_per_item_element
= fbx_data_np
.itemsize
852 strides
= (step_per_item
, step_per_item_element
)
854 fbx_data_np
= stride_tricks
.as_strided(fbx_data_np
, shape
, strides
)
856 # There's a whole number of strides, so first reshape to one stride per row and then create a view that
857 # includes only the first item_size elements of each stride.
858 fbx_data_np
= fbx_data_np
.reshape(-1, stride
)[:, :item_size
]
863 def blen_read_geom_check_fbx_data_length(blen_data
, fbx_data_np
, is_indices
=False):
864 """Check that there are the same number of items in blen_data and fbx_data_np.
866 Returns a tuple of two elements:
867 0: fbx_data_np or, if fbx_data_np contains more items than blen_data, a view of fbx_data_np with the excess
869 1: Whether the returned fbx_data_np contains enough items to completely fill blen_data"""
870 bl_num_items
= len(blen_data
)
871 fbx_num_items
= len(fbx_data_np
)
872 enough_data
= fbx_num_items
>= bl_num_items
875 print("ERROR: not enough indices in this FBX layer, missing data will be left as default!")
877 print("ERROR: not enough data in this FBX layer, missing data will be left as default!")
878 elif fbx_num_items
> bl_num_items
:
880 print("ERROR: too many indices in this FBX layer, skipping excess!")
882 print("ERROR: too much data in this FBX layer, skipping excess!")
883 fbx_data_np
= fbx_data_np
[:bl_num_items
]
885 return fbx_data_np
, enough_data
888 def blen_read_geom_xform(fbx_data_np
, xform
):
889 """xform is either None, or a function that takes fbx_data_np as its only positional argument and returns an
890 np.ndarray with the same total number of elements as fbx_data_np.
891 It is acceptable for xform to return an array with a different dtype to fbx_data_np.
893 Returns xform(fbx_data_np) when xform is not None and ensures the result of xform(fbx_data_np) has the same shape as
894 fbx_data_np before returning it.
895 When xform is None, fbx_data_np is returned as is."""
896 if xform
is not None:
897 item_size
= fbx_data_np
.shape
[1]
898 fbx_total_data
= fbx_data_np
.size
899 fbx_data_np
= xform(fbx_data_np
)
900 # The amount of data should not be changed by xform
901 assert(fbx_data_np
.size
== fbx_total_data
)
902 # Ensure fbx_data_np is still item_size elements per row
903 if len(fbx_data_np
.shape
) != 2 or fbx_data_np
.shape
[1] != item_size
:
904 fbx_data_np
= fbx_data_np
.reshape(-1, item_size
)
908 def blen_read_geom_array_foreach_set_direct(blen_data
, blen_attr
, blen_dtype
, fbx_data
, stride
, item_size
, descr
,
910 """Generic fbx_layer to blen_data foreach setter for Direct layers.
911 blen_data must be a bpy_prop_collection or 2d np.ndarray whose second axis length is item_size.
912 fbx_data must be an array.array."""
913 fbx_data_np
= blen_read_geom_parse_fbx_data(fbx_data
, stride
, item_size
)
914 fbx_data_np
, enough_data
= blen_read_geom_check_fbx_data_length(blen_data
, fbx_data_np
)
915 fbx_data_np
= blen_read_geom_xform(fbx_data_np
, xform
)
917 blen_data_is_collection
= blen_read_geom_validate_blen_data(blen_data
, blen_dtype
, item_size
)
919 if blen_data_is_collection
:
921 blen_total_data
= len(blen_data
) * item_size
922 buffer = np
.empty(blen_total_data
, dtype
=blen_dtype
)
923 # It's not clear what values should be used for the missing data, so read the current values into a buffer.
924 blen_data
.foreach_get(blen_attr
, buffer)
926 # Change the buffer shape to one item per row
927 buffer.shape
= (-1, item_size
)
929 # Copy the fbx data into the start of the buffer
930 buffer[:len(fbx_data_np
)] = fbx_data_np
932 # Convert the buffer to the Blender C type of blen_attr
933 buffer = astype_view_signedness(fbx_data_np
, blen_dtype
)
935 # Set blen_attr of blen_data. The buffer must be flat and C-contiguous, which ravel() ensures
936 blen_data
.foreach_set(blen_attr
, buffer.ravel())
938 assert(blen_data
.size
% item_size
== 0)
939 blen_data
= blen_data
.view()
940 blen_data
.shape
= (-1, item_size
)
941 blen_data
[:len(fbx_data_np
)] = fbx_data_np
944 def blen_read_geom_array_foreach_set_indexed(blen_data
, blen_attr
, blen_dtype
, fbx_data
, fbx_layer_index
, stride
,
945 item_size
, descr
, xform
):
946 """Generic fbx_layer to blen_data foreach setter for IndexToDirect layers.
947 blen_data must be a bpy_prop_collection or 2d np.ndarray whose second axis length is item_size.
948 fbx_data must be an array.array or a 1d np.ndarray."""
949 fbx_data_np
= blen_read_geom_parse_fbx_data(fbx_data
, stride
, item_size
)
950 fbx_data_np
= blen_read_geom_xform(fbx_data_np
, xform
)
952 # fbx_layer_index is allowed to be a 1d np.ndarray for use with blen_read_geom_array_foreach_set_looptovert.
953 if not isinstance(fbx_layer_index
, np
.ndarray
):
954 fbx_layer_index
= parray_as_ndarray(fbx_layer_index
)
956 fbx_layer_index
, enough_indices
= blen_read_geom_check_fbx_data_length(blen_data
, fbx_layer_index
, is_indices
=True)
958 blen_data_is_collection
= blen_read_geom_validate_blen_data(blen_data
, blen_dtype
, item_size
)
960 blen_data_items_len
= len(blen_data
)
961 blen_data_len
= blen_data_items_len
* item_size
962 fbx_num_items
= len(fbx_data_np
)
964 # Find all indices that are out of bounds of fbx_data_np.
965 min_index_inclusive
= -fbx_num_items
966 max_index_inclusive
= fbx_num_items
- 1
967 valid_index_mask
= np
.equal(fbx_layer_index
, fbx_layer_index
.clip(min_index_inclusive
, max_index_inclusive
))
968 indices_invalid
= not valid_index_mask
.all()
970 fbx_data_items
= fbx_data_np
.reshape(-1, item_size
)
972 if indices_invalid
or not enough_indices
:
973 if blen_data_is_collection
:
974 buffer = np
.empty(blen_data_len
, dtype
=blen_dtype
)
975 buffer_item_view
= buffer.view()
976 buffer_item_view
.shape
= (-1, item_size
)
977 # Since we don't know what the default values should be for the missing data, read the current values into a
979 blen_data
.foreach_get(blen_attr
, buffer)
981 buffer_item_view
= blen_data
983 if not enough_indices
:
984 # Reduce the length of the view to the same length as the number of indices.
985 buffer_item_view
= buffer_item_view
[:len(fbx_layer_index
)]
987 # Copy the result of indexing fbx_data_items by each element in fbx_layer_index into the buffer.
989 print("ERROR: indices in this FBX layer out of bounds of the FBX data, skipping invalid indices!")
990 buffer_item_view
[valid_index_mask
] = fbx_data_items
[fbx_layer_index
[valid_index_mask
]]
992 buffer_item_view
[:] = fbx_data_items
[fbx_layer_index
]
994 if blen_data_is_collection
:
995 blen_data
.foreach_set(blen_attr
, buffer.ravel())
997 if blen_data_is_collection
:
998 # Cast the buffer to the Blender C type of blen_attr
999 fbx_data_items
= astype_view_signedness(fbx_data_items
, blen_dtype
)
1000 buffer_items
= fbx_data_items
[fbx_layer_index
]
1001 blen_data
.foreach_set(blen_attr
, buffer_items
.ravel())
1003 blen_data
[:] = fbx_data_items
[fbx_layer_index
]
1006 def blen_read_geom_array_foreach_set_allsame(blen_data
, blen_attr
, blen_dtype
, fbx_data
, stride
, item_size
, descr
,
1008 """Generic fbx_layer to blen_data foreach setter for AllSame layers.
1009 blen_data must be a bpy_prop_collection or 2d np.ndarray whose second axis length is item_size.
1010 fbx_data must be an array.array."""
1011 fbx_data_np
= blen_read_geom_parse_fbx_data(fbx_data
, stride
, item_size
)
1012 fbx_data_np
= blen_read_geom_xform(fbx_data_np
, xform
)
1013 blen_data_is_collection
= blen_read_geom_validate_blen_data(blen_data
, blen_dtype
, item_size
)
1014 fbx_items_len
= len(fbx_data_np
)
1015 blen_items_len
= len(blen_data
)
1017 if fbx_items_len
< 1:
1018 print("ERROR: not enough data in this FBX layer, skipping!")
1021 if blen_data_is_collection
:
1022 # Create an array filled with the value from fbx_data_np
1023 buffer = np
.full((blen_items_len
, item_size
), fbx_data_np
[0], dtype
=blen_dtype
)
1025 blen_data
.foreach_set(blen_attr
, buffer.ravel())
1027 blen_data
[:] = fbx_data_np
[0]
1030 def blen_read_geom_array_foreach_set_looptovert(mesh
, blen_data
, blen_attr
, blen_dtype
, fbx_data
, stride
, item_size
,
1032 """Generic fbx_layer to blen_data foreach setter for polyloop ByVertice layers.
1033 blen_data must be a bpy_prop_collection or 2d np.ndarray whose second axis length is item_size.
1034 fbx_data must be an array.array"""
1035 # The fbx_data is mapped to vertices. To expand fbx_data to polygon loops, get an array of the vertex index of each
1036 # polygon loop that will then be used to index fbx_data
1037 loop_vertex_indices
= np
.empty(len(mesh
.loops
), dtype
=np
.uintc
)
1038 mesh
.loops
.foreach_get("vertex_index", loop_vertex_indices
)
1039 blen_read_geom_array_foreach_set_indexed(blen_data
, blen_attr
, blen_dtype
, fbx_data
, loop_vertex_indices
, stride
,
1040 item_size
, descr
, xform
)
1043 # generic error printers.
1044 def blen_read_geom_array_error_mapping(descr
, fbx_layer_mapping
, quiet
=False):
1046 print("warning layer %r mapping type unsupported: %r" % (descr
, fbx_layer_mapping
))
1049 def blen_read_geom_array_error_ref(descr
, fbx_layer_ref
, quiet
=False):
1051 print("warning layer %r ref type unsupported: %r" % (descr
, fbx_layer_ref
))
1054 def blen_read_geom_array_mapped_vert(
1055 mesh
, blen_data
, blen_attr
, blen_dtype
,
1056 fbx_layer_data
, fbx_layer_index
,
1057 fbx_layer_mapping
, fbx_layer_ref
,
1058 stride
, item_size
, descr
,
1059 xform
=None, quiet
=False,
1061 if fbx_layer_mapping
== b
'ByVertice':
1062 if fbx_layer_ref
== b
'Direct':
1063 assert(fbx_layer_index
is None)
1064 blen_read_geom_array_foreach_set_direct(blen_data
, blen_attr
, blen_dtype
, fbx_layer_data
, stride
, item_size
,
1067 blen_read_geom_array_error_ref(descr
, fbx_layer_ref
, quiet
)
1068 elif fbx_layer_mapping
== b
'AllSame':
1069 if fbx_layer_ref
== b
'IndexToDirect':
1070 assert(fbx_layer_index
is None)
1071 blen_read_geom_array_foreach_set_allsame(blen_data
, blen_attr
, blen_dtype
, fbx_layer_data
, stride
,
1072 item_size
, descr
, xform
)
1074 blen_read_geom_array_error_ref(descr
, fbx_layer_ref
, quiet
)
1076 blen_read_geom_array_error_mapping(descr
, fbx_layer_mapping
, quiet
)
1081 def blen_read_geom_array_mapped_edge(
1082 mesh
, blen_data
, blen_attr
, blen_dtype
,
1083 fbx_layer_data
, fbx_layer_index
,
1084 fbx_layer_mapping
, fbx_layer_ref
,
1085 stride
, item_size
, descr
,
1086 xform
=None, quiet
=False,
1088 if fbx_layer_mapping
== b
'ByEdge':
1089 if fbx_layer_ref
== b
'Direct':
1090 blen_read_geom_array_foreach_set_direct(blen_data
, blen_attr
, blen_dtype
, fbx_layer_data
, stride
, item_size
,
1093 blen_read_geom_array_error_ref(descr
, fbx_layer_ref
, quiet
)
1094 elif fbx_layer_mapping
== b
'AllSame':
1095 if fbx_layer_ref
== b
'IndexToDirect':
1096 assert(fbx_layer_index
is None)
1097 blen_read_geom_array_foreach_set_allsame(blen_data
, blen_attr
, blen_dtype
, fbx_layer_data
, stride
,
1098 item_size
, descr
, xform
)
1100 blen_read_geom_array_error_ref(descr
, fbx_layer_ref
, quiet
)
1102 blen_read_geom_array_error_mapping(descr
, fbx_layer_mapping
, quiet
)
1107 def blen_read_geom_array_mapped_polygon(
1108 mesh
, blen_data
, blen_attr
, blen_dtype
,
1109 fbx_layer_data
, fbx_layer_index
,
1110 fbx_layer_mapping
, fbx_layer_ref
,
1111 stride
, item_size
, descr
,
1112 xform
=None, quiet
=False,
1114 if fbx_layer_mapping
== b
'ByPolygon':
1115 if fbx_layer_ref
== b
'IndexToDirect':
1116 # XXX Looks like we often get no fbx_layer_index in this case, shall not happen but happens...
1117 # We fallback to 'Direct' mapping in this case.
1118 #~ assert(fbx_layer_index is not None)
1119 if fbx_layer_index
is None:
1120 blen_read_geom_array_foreach_set_direct(blen_data
, blen_attr
, blen_dtype
, fbx_layer_data
, stride
,
1121 item_size
, descr
, xform
)
1123 blen_read_geom_array_foreach_set_indexed(blen_data
, blen_attr
, blen_dtype
, fbx_layer_data
,
1124 fbx_layer_index
, stride
, item_size
, descr
, xform
)
1126 elif fbx_layer_ref
== b
'Direct':
1127 blen_read_geom_array_foreach_set_direct(blen_data
, blen_attr
, blen_dtype
, fbx_layer_data
, stride
, item_size
,
1130 blen_read_geom_array_error_ref(descr
, fbx_layer_ref
, quiet
)
1131 elif fbx_layer_mapping
== b
'AllSame':
1132 if fbx_layer_ref
== b
'IndexToDirect':
1133 assert(fbx_layer_index
is None)
1134 blen_read_geom_array_foreach_set_allsame(blen_data
, blen_attr
, blen_dtype
, fbx_layer_data
, stride
,
1135 item_size
, descr
, xform
)
1137 blen_read_geom_array_error_ref(descr
, fbx_layer_ref
, quiet
)
1139 blen_read_geom_array_error_mapping(descr
, fbx_layer_mapping
, quiet
)
1144 def blen_read_geom_array_mapped_polyloop(
1145 mesh
, blen_data
, blen_attr
, blen_dtype
,
1146 fbx_layer_data
, fbx_layer_index
,
1147 fbx_layer_mapping
, fbx_layer_ref
,
1148 stride
, item_size
, descr
,
1149 xform
=None, quiet
=False,
1151 if fbx_layer_mapping
== b
'ByPolygonVertex':
1152 if fbx_layer_ref
== b
'IndexToDirect':
1153 # XXX Looks like we often get no fbx_layer_index in this case, shall not happen but happens...
1154 # We fallback to 'Direct' mapping in this case.
1155 #~ assert(fbx_layer_index is not None)
1156 if fbx_layer_index
is None:
1157 blen_read_geom_array_foreach_set_direct(blen_data
, blen_attr
, blen_dtype
, fbx_layer_data
, stride
,
1158 item_size
, descr
, xform
)
1160 blen_read_geom_array_foreach_set_indexed(blen_data
, blen_attr
, blen_dtype
, fbx_layer_data
,
1161 fbx_layer_index
, stride
, item_size
, descr
, xform
)
1163 elif fbx_layer_ref
== b
'Direct':
1164 blen_read_geom_array_foreach_set_direct(blen_data
, blen_attr
, blen_dtype
, fbx_layer_data
, stride
, item_size
,
1167 blen_read_geom_array_error_ref(descr
, fbx_layer_ref
, quiet
)
1168 elif fbx_layer_mapping
== b
'ByVertice':
1169 if fbx_layer_ref
== b
'Direct':
1170 assert(fbx_layer_index
is None)
1171 blen_read_geom_array_foreach_set_looptovert(mesh
, blen_data
, blen_attr
, blen_dtype
, fbx_layer_data
, stride
,
1172 item_size
, descr
, xform
)
1174 blen_read_geom_array_error_ref(descr
, fbx_layer_ref
, quiet
)
1175 elif fbx_layer_mapping
== b
'AllSame':
1176 if fbx_layer_ref
== b
'IndexToDirect':
1177 assert(fbx_layer_index
is None)
1178 blen_read_geom_array_foreach_set_allsame(blen_data
, blen_attr
, blen_dtype
, fbx_layer_data
, stride
,
1179 item_size
, descr
, xform
)
1181 blen_read_geom_array_error_ref(descr
, fbx_layer_ref
, quiet
)
1183 blen_read_geom_array_error_mapping(descr
, fbx_layer_mapping
, quiet
)
1188 def blen_read_geom_layer_material(fbx_obj
, mesh
):
1189 fbx_layer
= elem_find_first(fbx_obj
, b
'LayerElementMaterial')
1191 if fbx_layer
is None:
1197 ) = blen_read_geom_layerinfo(fbx_layer
)
1199 layer_id
= b
'Materials'
1200 fbx_layer_data
= elem_prop_first(elem_find_first(fbx_layer
, layer_id
))
1202 blen_data
= mesh
.polygons
1203 blen_read_geom_array_mapped_polygon(
1204 mesh
, blen_data
, "material_index", np
.uintc
,
1205 fbx_layer_data
, None,
1206 fbx_layer_mapping
, fbx_layer_ref
,
1211 def blen_read_geom_layer_uv(fbx_obj
, mesh
):
1212 for layer_id
in (b
'LayerElementUV',):
1213 for fbx_layer
in elem_find_iter(fbx_obj
, layer_id
):
1214 # all should be valid
1218 ) = blen_read_geom_layerinfo(fbx_layer
)
1220 fbx_layer_data
= elem_prop_first(elem_find_first(fbx_layer
, b
'UV'))
1221 fbx_layer_index
= elem_prop_first(elem_find_first(fbx_layer
, b
'UVIndex'))
1223 # Always init our new layers with (0, 0) UVs.
1224 uv_lay
= mesh
.uv_layers
.new(name
=fbx_layer_name
, do_init
=False)
1226 print("Failed to add {%r %r} UVLayer to %r (probably too many of them?)"
1227 "" % (layer_id
, fbx_layer_name
, mesh
.name
))
1230 blen_data
= uv_lay
.data
1232 # some valid files omit this data
1233 if fbx_layer_data
is None:
1234 print("%r %r missing data" % (layer_id
, fbx_layer_name
))
1237 blen_read_geom_array_mapped_polyloop(
1238 mesh
, blen_data
, "uv", np
.single
,
1239 fbx_layer_data
, fbx_layer_index
,
1240 fbx_layer_mapping
, fbx_layer_ref
,
1245 def blen_read_geom_layer_color(fbx_obj
, mesh
, colors_type
):
1246 if colors_type
== 'NONE':
1248 use_srgb
= colors_type
== 'SRGB'
1249 layer_type
= 'BYTE_COLOR' if use_srgb
else 'FLOAT_COLOR'
1250 color_prop_name
= "color_srgb" if use_srgb
else "color"
1251 # almost same as UVs
1252 for layer_id
in (b
'LayerElementColor',):
1253 for fbx_layer
in elem_find_iter(fbx_obj
, layer_id
):
1254 # all should be valid
1258 ) = blen_read_geom_layerinfo(fbx_layer
)
1260 fbx_layer_data
= elem_prop_first(elem_find_first(fbx_layer
, b
'Colors'))
1261 fbx_layer_index
= elem_prop_first(elem_find_first(fbx_layer
, b
'ColorIndex'))
1263 color_lay
= mesh
.color_attributes
.new(name
=fbx_layer_name
, type=layer_type
, domain
='CORNER')
1265 if color_lay
is None:
1266 print("Failed to add {%r %r} vertex color layer to %r (probably too many of them?)"
1267 "" % (layer_id
, fbx_layer_name
, mesh
.name
))
1270 blen_data
= color_lay
.data
1272 # some valid files omit this data
1273 if fbx_layer_data
is None:
1274 print("%r %r missing data" % (layer_id
, fbx_layer_name
))
1277 blen_read_geom_array_mapped_polyloop(
1278 mesh
, blen_data
, color_prop_name
, np
.single
,
1279 fbx_layer_data
, fbx_layer_index
,
1280 fbx_layer_mapping
, fbx_layer_ref
,
1285 def blen_read_geom_layer_smooth(fbx_obj
, mesh
):
1286 fbx_layer
= elem_find_first(fbx_obj
, b
'LayerElementSmoothing')
1288 if fbx_layer
is None:
1291 # all should be valid
1295 ) = blen_read_geom_layerinfo(fbx_layer
)
1297 layer_id
= b
'Smoothing'
1298 fbx_layer_data
= elem_prop_first(elem_find_first(fbx_layer
, layer_id
))
1300 # udk has 'Direct' mapped, with no Smoothing, not sure why, but ignore these
1301 if fbx_layer_data
is None:
1304 if fbx_layer_mapping
== b
'ByEdge':
1305 # some models have bad edge data, we can't use this info...
1307 print("warning skipping sharp edges data, no valid edges...")
1310 blen_data
= mesh
.edges
1311 blen_read_geom_array_mapped_edge(
1312 mesh
, blen_data
, "use_edge_sharp", bool,
1313 fbx_layer_data
, None,
1314 fbx_layer_mapping
, fbx_layer_ref
,
1316 xform
=np
.logical_not
,
1318 # We only set sharp edges here, not face smoothing itself...
1319 mesh
.use_auto_smooth
= True
1321 elif fbx_layer_mapping
== b
'ByPolygon':
1322 blen_data
= mesh
.polygons
1323 return blen_read_geom_array_mapped_polygon(
1324 mesh
, blen_data
, "use_smooth", bool,
1325 fbx_layer_data
, None,
1326 fbx_layer_mapping
, fbx_layer_ref
,
1328 xform
=lambda s
: (s
!= 0), # smoothgroup bitflags, treat as booleans for now
1331 print("warning layer %r mapping type unsupported: %r" % (fbx_layer
.id, fbx_layer_mapping
))
1334 def blen_read_geom_layer_edge_crease(fbx_obj
, mesh
):
1335 fbx_layer
= elem_find_first(fbx_obj
, b
'LayerElementEdgeCrease')
1337 if fbx_layer
is None:
1340 # all should be valid
1344 ) = blen_read_geom_layerinfo(fbx_layer
)
1346 if fbx_layer_mapping
!= b
'ByEdge':
1349 layer_id
= b
'EdgeCrease'
1350 fbx_layer_data
= elem_prop_first(elem_find_first(fbx_layer
, layer_id
))
1352 # some models have bad edge data, we can't use this info...
1354 print("warning skipping edge crease data, no valid edges...")
1357 if fbx_layer_mapping
== b
'ByEdge':
1358 # some models have bad edge data, we can't use this info...
1360 print("warning skipping edge crease data, no valid edges...")
1363 blen_data
= mesh
.edges
1364 return blen_read_geom_array_mapped_edge(
1365 mesh
, blen_data
, "crease", np
.single
,
1366 fbx_layer_data
, None,
1367 fbx_layer_mapping
, fbx_layer_ref
,
1369 # Blender squares those values before sending them to OpenSubdiv, when other software don't,
1370 # so we need to compensate that to get similar results through FBX...
1374 print("warning layer %r mapping type unsupported: %r" % (fbx_layer
.id, fbx_layer_mapping
))
1377 def blen_read_geom_layer_normal(fbx_obj
, mesh
, xform
=None):
1378 fbx_layer
= elem_find_first(fbx_obj
, b
'LayerElementNormal')
1380 if fbx_layer
is None:
1386 ) = blen_read_geom_layerinfo(fbx_layer
)
1388 layer_id
= b
'Normals'
1389 fbx_layer_data
= elem_prop_first(elem_find_first(fbx_layer
, layer_id
))
1390 fbx_layer_index
= elem_prop_first(elem_find_first(fbx_layer
, b
'NormalsIndex'))
1392 if fbx_layer_data
is None:
1393 print("warning %r %r missing data" % (layer_id
, fbx_layer_name
))
1396 # Normals are temporarily set here so that they can be retrieved again after a call to Mesh.validate().
1397 bl_norm_dtype
= np
.single
1399 # try loops, then polygons, then vertices.
1400 tries
= ((mesh
.loops
, "Loops", False, blen_read_geom_array_mapped_polyloop
),
1401 (mesh
.polygons
, "Polygons", True, blen_read_geom_array_mapped_polygon
),
1402 (mesh
.vertices
, "Vertices", True, blen_read_geom_array_mapped_vert
))
1403 for blen_data
, blen_data_type
, is_fake
, func
in tries
:
1404 bdata
= np
.zeros((len(blen_data
), item_size
), dtype
=bl_norm_dtype
) if is_fake
else blen_data
1405 if func(mesh
, bdata
, "normal", bl_norm_dtype
,
1406 fbx_layer_data
, fbx_layer_index
, fbx_layer_mapping
, fbx_layer_ref
, 3, item_size
, layer_id
, xform
, True):
1407 if blen_data_type
== "Polygons":
1408 # To expand to per-loop normals, repeat each per-polygon normal by the number of loops of each polygon.
1409 poly_loop_totals
= np
.empty(len(mesh
.polygons
), dtype
=np
.uintc
)
1410 mesh
.polygons
.foreach_get("loop_total", poly_loop_totals
)
1411 loop_normals
= np
.repeat(bdata
, poly_loop_totals
, axis
=0)
1412 mesh
.loops
.foreach_set("normal", loop_normals
.ravel())
1413 elif blen_data_type
== "Vertices":
1414 # We have to copy vnors to lnors! Far from elegant, but simple.
1415 loop_vertex_indices
= np
.empty(len(mesh
.loops
), dtype
=np
.uintc
)
1416 mesh
.loops
.foreach_get("vertex_index", loop_vertex_indices
)
1417 mesh
.loops
.foreach_set("normal", bdata
[loop_vertex_indices
].ravel())
1420 blen_read_geom_array_error_mapping("normal", fbx_layer_mapping
)
1421 blen_read_geom_array_error_ref("normal", fbx_layer_ref
)
1425 def blen_read_geom(fbx_tmpl
, fbx_obj
, settings
):
1426 # Vertices are in object space, but we are post-multiplying all transforms with the inverse of the
1427 # global matrix, so we need to apply the global matrix to the vertices to get the correct result.
1428 geom_mat_co
= settings
.global_matrix
if settings
.bake_space_transform
else None
1429 # We need to apply the inverse transpose of the global matrix when transforming normals.
1430 geom_mat_no
= Matrix(settings
.global_matrix_inv_transposed
) if settings
.bake_space_transform
else None
1431 if geom_mat_no
is not None:
1432 # Remove translation & scaling!
1433 geom_mat_no
.translation
= Vector()
1434 geom_mat_no
.normalize()
1436 # TODO, use 'fbx_tmpl'
1437 elem_name_utf8
= elem_name_ensure_class(fbx_obj
, b
'Geometry')
1439 fbx_verts
= elem_prop_first(elem_find_first(fbx_obj
, b
'Vertices'))
1440 fbx_polys
= elem_prop_first(elem_find_first(fbx_obj
, b
'PolygonVertexIndex'))
1441 fbx_edges
= elem_prop_first(elem_find_first(fbx_obj
, b
'Edges'))
1443 bl_vcos_dtype
= np
.single
1445 # The dtypes when empty don't matter, but are set to what the fbx arrays are expected to be.
1446 fbx_verts
= parray_as_ndarray(fbx_verts
) if fbx_verts
else np
.empty(0, dtype
=data_types
.ARRAY_FLOAT64
)
1447 fbx_polys
= parray_as_ndarray(fbx_polys
) if fbx_polys
else np
.empty(0, dtype
=data_types
.ARRAY_INT32
)
1448 fbx_edges
= parray_as_ndarray(fbx_edges
) if fbx_edges
else np
.empty(0, dtype
=data_types
.ARRAY_INT32
)
1450 # Each vert is a 3d vector so is made of 3 components.
1451 tot_verts
= len(fbx_verts
) // 3
1452 if tot_verts
* 3 != len(fbx_verts
):
1453 print("ERROR: Not a whole number of vertices. Ignoring the partial vertex!")
1454 # Remove any remainder.
1455 fbx_verts
= fbx_verts
[:tot_verts
* 3]
1457 tot_loops
= len(fbx_polys
)
1458 tot_edges
= len(fbx_edges
)
1460 mesh
= bpy
.data
.meshes
.new(name
=elem_name_utf8
)
1463 if geom_mat_co
is not None:
1464 fbx_verts
= vcos_transformed(fbx_verts
, geom_mat_co
, bl_vcos_dtype
)
1466 fbx_verts
= fbx_verts
.astype(bl_vcos_dtype
, copy
=False)
1468 mesh
.vertices
.add(tot_verts
)
1469 mesh
.vertices
.foreach_set("co", fbx_verts
.ravel())
1472 bl_loop_start_dtype
= bl_loop_vertex_index_dtype
= np
.uintc
1474 mesh
.loops
.add(tot_loops
)
1475 # The end of each polygon is specified by an inverted index.
1476 fbx_loop_end_idx
= np
.flatnonzero(fbx_polys
< 0)
1478 tot_polys
= len(fbx_loop_end_idx
)
1480 # Un-invert the loop ends.
1481 fbx_polys
[fbx_loop_end_idx
] ^
= -1
1482 # Set loop vertex indices, casting to the Blender C type first for performance.
1483 mesh
.loops
.foreach_set("vertex_index", astype_view_signedness(fbx_polys
, bl_loop_vertex_index_dtype
))
1485 poly_loop_starts
= np
.empty(tot_polys
, dtype
=bl_loop_start_dtype
)
1486 # The first loop is always a loop start.
1487 poly_loop_starts
[0] = 0
1488 # Ignoring the last loop end, the indices after every loop end are the remaining loop starts.
1489 poly_loop_starts
[1:] = fbx_loop_end_idx
[:-1] + 1
1491 mesh
.polygons
.add(tot_polys
)
1492 mesh
.polygons
.foreach_set("loop_start", poly_loop_starts
)
1494 blen_read_geom_layer_material(fbx_obj
, mesh
)
1495 blen_read_geom_layer_uv(fbx_obj
, mesh
)
1496 blen_read_geom_layer_color(fbx_obj
, mesh
, settings
.colors_type
)
1499 # edges in fact index the polygons (NOT the vertices)
1500 bl_edge_vertex_indices_dtype
= np
.uintc
1502 # The first vertex index of each edge is the vertex index of the corresponding loop in fbx_polys.
1503 edges_a
= fbx_polys
[fbx_edges
]
1505 # The second vertex index of each edge is the vertex index of the next loop in the same polygon. The
1506 # complexity here is that if the first vertex index was the last loop of that polygon in fbx_polys, the next
1507 # loop in the polygon is the first loop of that polygon, which is not the next loop in fbx_polys.
1509 # Copy fbx_polys, but rolled backwards by 1 so that indexing the result by [fbx_edges] will get the next
1510 # loop of the same polygon unless the first vertex index was the last loop of the polygon.
1511 fbx_polys_next
= np
.roll(fbx_polys
, -1)
1512 # Get the first loop of each polygon and set them into fbx_polys_next at the same indices as the last loop
1513 # of each polygon in fbx_polys.
1514 fbx_polys_next
[fbx_loop_end_idx
] = fbx_polys
[poly_loop_starts
]
1516 # Indexing fbx_polys_next by fbx_edges now gets the vertex index of the next loop in fbx_polys.
1517 edges_b
= fbx_polys_next
[fbx_edges
]
1519 # edges_a and edges_b need to be combined so that the first vertex index of each edge is immediately
1520 # followed by the second vertex index of that same edge.
1521 # Stack edges_a and edges_b as individual columns like np.column_stack((edges_a, edges_b)).
1522 # np.concatenate is used because np.column_stack doesn't allow specifying the dtype of the returned array.
1523 edges_conv
= np
.concatenate((edges_a
.reshape(-1, 1), edges_b
.reshape(-1, 1)),
1524 axis
=1, dtype
=bl_edge_vertex_indices_dtype
, casting
='unsafe')
1526 # Add the edges and set their vertex indices.
1527 mesh
.edges
.add(len(edges_conv
))
1528 # ravel() because edges_conv must be flat and C-contiguous when passed to foreach_set.
1529 mesh
.edges
.foreach_set("vertices", edges_conv
.ravel())
1531 print("ERROR: No polygons, but edges exist. Ignoring the edges!")
1533 # must be after edge, face loading.
1534 ok_smooth
= blen_read_geom_layer_smooth(fbx_obj
, mesh
)
1536 blen_read_geom_layer_edge_crease(fbx_obj
, mesh
)
1539 if settings
.use_custom_normals
:
1540 # Note: we store 'temp' normals in loops, since validate() may alter final mesh,
1541 # we can only set custom lnors *after* calling it.
1542 mesh
.create_normals_split()
1543 if geom_mat_no
is None:
1544 ok_normals
= blen_read_geom_layer_normal(fbx_obj
, mesh
)
1546 ok_normals
= blen_read_geom_layer_normal(fbx_obj
, mesh
,
1547 lambda v_array
: nors_transformed(v_array
, geom_mat_no
))
1549 mesh
.validate(clean_customdata
=False) # *Very* important to not remove lnors here!
1552 bl_nors_dtype
= np
.single
1553 clnors
= np
.empty(len(mesh
.loops
) * 3, dtype
=bl_nors_dtype
)
1554 mesh
.loops
.foreach_get("normal", clnors
)
1557 mesh
.polygons
.foreach_set("use_smooth", np
.full(len(mesh
.polygons
), True, dtype
=bool))
1560 # Iterating clnors into a nested tuple first is faster than passing clnors.reshape(-1, 3) directly into
1561 # normals_split_custom_set. We use clnors.data since it is a memoryview, which is faster to iterate than clnors.
1562 mesh
.normals_split_custom_set(tuple(zip(*(iter(clnors
.data
),) * 3)))
1563 mesh
.use_auto_smooth
= True
1567 if settings
.use_custom_normals
:
1568 mesh
.free_normals_split()
1571 mesh
.polygons
.foreach_set("use_smooth", np
.full(len(mesh
.polygons
), True, dtype
=bool))
1573 if settings
.use_custom_props
:
1574 blen_read_custom_properties(fbx_obj
, mesh
, settings
)
1579 def blen_read_shapes(fbx_tmpl
, fbx_data
, objects
, me
, scene
):
1581 # No shape key data. Nothing to do.
1584 bl_vcos_dtype
= np
.single
1585 me_vcos
= np
.empty(len(me
.vertices
) * 3, dtype
=bl_vcos_dtype
)
1586 me
.vertices
.foreach_get("co", me_vcos
)
1587 me_vcos_vector_view
= me_vcos
.reshape(-1, 3)
1589 objects
= list({node
.bl_obj
for node
in objects
})
1592 bc_uuid_to_keyblocks
= {}
1593 for bc_uuid
, fbx_sdata
, fbx_bcdata
in fbx_data
:
1594 elem_name_utf8
= elem_name_ensure_class(fbx_sdata
, b
'Geometry')
1595 indices
= elem_prop_first(elem_find_first(fbx_sdata
, b
'Indexes'))
1596 dvcos
= elem_prop_first(elem_find_first(fbx_sdata
, b
'Vertices'))
1598 indices
= parray_as_ndarray(indices
) if indices
else np
.empty(0, dtype
=data_types
.ARRAY_INT32
)
1599 dvcos
= parray_as_ndarray(dvcos
) if dvcos
else np
.empty(0, dtype
=data_types
.ARRAY_FLOAT64
)
1601 # If there's not a whole number of vectors, trim off the remainder.
1602 # 3 components per vector.
1603 remainder
= len(dvcos
) % 3
1605 dvcos
= dvcos
[:-remainder
]
1606 dvcos
= dvcos
.reshape(-1, 3)
1608 # We completely ignore normals here!
1609 weight
= elem_prop_first(elem_find_first(fbx_bcdata
, b
'DeformPercent'), default
=100.0) / 100.0
1611 vgweights
= elem_prop_first(elem_find_first(fbx_bcdata
, b
'FullWeights'))
1612 vgweights
= parray_as_ndarray(vgweights
) if vgweights
else np
.empty(0, dtype
=data_types
.ARRAY_FLOAT64
)
1613 # Not doing the division in-place in-case it's possible for FBX shape keys to be used by more than one mesh.
1614 vgweights
= vgweights
/ 100.0
1616 create_vg
= (vgweights
!= 1.0).any()
1618 # Special case, in case all weights are the same, FullWeight can have only one element - *sigh!*
1619 nbr_indices
= len(indices
)
1620 if len(vgweights
) == 1 and nbr_indices
> 1:
1621 vgweights
= np
.full_like(indices
, vgweights
[0], dtype
=vgweights
.dtype
)
1623 assert(len(vgweights
) == nbr_indices
== len(dvcos
))
1625 # To add shape keys to the mesh, an Object using the mesh is needed.
1626 if me
.shape_keys
is None:
1627 objects
[0].shape_key_add(name
="Basis", from_mix
=False)
1628 kb
= objects
[0].shape_key_add(name
=elem_name_utf8
, from_mix
=False)
1629 me
.shape_keys
.use_relative
= True # Should already be set as such.
1631 # Only need to set the shape key co if there are any non-zero dvcos.
1633 shape_cos
= me_vcos_vector_view
.copy()
1634 shape_cos
[indices
] += dvcos
1635 kb
.data
.foreach_set("co", shape_cos
.ravel())
1639 # Add vgroup if necessary.
1641 # VertexGroup.add only allows sequences of int indices, but iterating the indices array directly would
1642 # produce numpy scalars of types such as np.int32. The underlying memoryview of the indices array, however,
1643 # does produce standard Python ints when iterated, so pass indices.data to add_vgroup_to_objects instead of
1645 # memoryviews tend to be faster to iterate than numpy arrays anyway, so vgweights.data is passed too.
1646 add_vgroup_to_objects(indices
.data
, vgweights
.data
, kb
.name
, objects
)
1647 kb
.vertex_group
= kb
.name
1649 bc_uuid_to_keyblocks
.setdefault(bc_uuid
, []).append(kb
)
1650 return bc_uuid_to_keyblocks
1656 def blen_read_material(fbx_tmpl
, fbx_obj
, settings
):
1657 from bpy_extras
import node_shader_utils
1658 from math
import sqrt
1660 elem_name_utf8
= elem_name_ensure_class(fbx_obj
, b
'Material')
1662 nodal_material_wrap_map
= settings
.nodal_material_wrap_map
1663 ma
= bpy
.data
.materials
.new(name
=elem_name_utf8
)
1665 const_color_white
= 1.0, 1.0, 1.0
1666 const_color_black
= 0.0, 0.0, 0.0
1668 fbx_props
= (elem_find_first(fbx_obj
, b
'Properties70'),
1669 elem_find_first(fbx_tmpl
, b
'Properties70', fbx_elem_nil
))
1670 fbx_props_no_template
= (fbx_props
[0], fbx_elem_nil
)
1672 ma_wrap
= node_shader_utils
.PrincipledBSDFWrapper(ma
, is_readonly
=False, use_nodes
=True)
1673 ma_wrap
.base_color
= elem_props_get_color_rgb(fbx_props
, b
'DiffuseColor', const_color_white
)
1674 # No specular color in Principled BSDF shader, assumed to be either white or take some tint from diffuse one...
1675 # TODO: add way to handle tint option (guesstimate from spec color + intensity...)?
1676 ma_wrap
.specular
= elem_props_get_number(fbx_props
, b
'SpecularFactor', 0.25) * 2.0
1677 # XXX Totally empirical conversion, trying to adapt it (and protect against invalid negative values, see T96076):
1678 # From [1.0 - 0.0] Principled BSDF range to [0.0 - 100.0] FBX shininess range)...
1679 fbx_shininess
= max(elem_props_get_number(fbx_props
, b
'Shininess', 20.0), 0.0)
1680 ma_wrap
.roughness
= 1.0 - (sqrt(fbx_shininess
) / 10.0)
1681 # Sweetness... Looks like we are not the only ones to not know exactly how FBX is supposed to work (see T59850).
1682 # According to one of its developers, Unity uses that formula to extract alpha value:
1684 # alpha = 1 - TransparencyFactor
1685 # if (alpha == 1 or alpha == 0):
1686 # alpha = 1 - TransparentColor.r
1688 # Until further info, let's assume this is correct way to do, hence the following code for TransparentColor.
1689 # However, there are some cases (from 3DSMax, see T65065), where we do have TransparencyFactor only defined
1690 # in the template to 0.0, and then materials defining TransparentColor to pure white (1.0, 1.0, 1.0),
1691 # and setting alpha value in Opacity... try to cope with that too. :((((
1692 alpha
= 1.0 - elem_props_get_number(fbx_props
, b
'TransparencyFactor', 0.0)
1693 if (alpha
== 1.0 or alpha
== 0.0):
1694 alpha
= elem_props_get_number(fbx_props_no_template
, b
'Opacity', None)
1696 alpha
= 1.0 - elem_props_get_color_rgb(fbx_props
, b
'TransparentColor', const_color_black
)[0]
1697 ma_wrap
.alpha
= alpha
1698 ma_wrap
.metallic
= elem_props_get_number(fbx_props
, b
'ReflectionFactor', 0.0)
1699 # We have no metallic (a.k.a. reflection) color...
1700 # elem_props_get_color_rgb(fbx_props, b'ReflectionColor', const_color_white)
1701 ma_wrap
.normalmap_strength
= elem_props_get_number(fbx_props
, b
'BumpFactor', 1.0)
1702 # Emission strength and color
1703 ma_wrap
.emission_strength
= elem_props_get_number(fbx_props
, b
'EmissiveFactor', 1.0)
1704 ma_wrap
.emission_color
= elem_props_get_color_rgb(fbx_props
, b
'EmissiveColor', const_color_black
)
1706 nodal_material_wrap_map
[ma
] = ma_wrap
1708 if settings
.use_custom_props
:
1709 blen_read_custom_properties(fbx_obj
, ma
, settings
)
1717 def blen_read_texture_image(fbx_tmpl
, fbx_obj
, basedir
, settings
):
1719 from bpy_extras
import image_utils
1721 def pack_data_from_content(image
, fbx_obj
):
1722 data
= elem_find_first_bytes(fbx_obj
, b
'Content')
1724 data_len
= len(data
)
1726 image
.pack(data
=data
, data_len
=data_len
)
1728 elem_name_utf8
= elem_name_ensure_classes(fbx_obj
, {b
'Texture', b
'Video'})
1730 image_cache
= settings
.image_cache
1732 # Yet another beautiful logic demonstration by Master FBX:
1733 # * RelativeFilename in both Video and Texture nodes.
1734 # * FileName in texture nodes.
1735 # * Filename in video nodes.
1736 # Aaaaaaaarrrrrrrrgggggggggggg!!!!!!!!!!!!!!
1737 filepath
= elem_find_first_string(fbx_obj
, b
'RelativeFilename')
1739 # Make sure we do handle a relative path, and not an absolute one (see D5143).
1740 filepath
= filepath
.lstrip(os
.path
.sep
).lstrip(os
.path
.altsep
)
1741 filepath
= os
.path
.join(basedir
, filepath
)
1743 filepath
= elem_find_first_string(fbx_obj
, b
'FileName')
1745 filepath
= elem_find_first_string(fbx_obj
, b
'Filename')
1747 print("Error, could not find any file path in ", fbx_obj
)
1748 print(" Falling back to: ", elem_name_utf8
)
1749 filepath
= elem_name_utf8
1751 filepath
= filepath
.replace('\\', '/') if (os
.sep
== '/') else filepath
.replace('/', '\\')
1753 image
= image_cache
.get(filepath
)
1754 if image
is not None:
1755 # Data is only embedded once, we may have already created the image but still be missing its data!
1756 if not image
.has_data
:
1757 pack_data_from_content(image
, fbx_obj
)
1760 image
= image_utils
.load_image(
1764 recursive
=settings
.use_image_search
,
1767 # Try to use embedded data, if available!
1768 pack_data_from_content(image
, fbx_obj
)
1770 image_cache
[filepath
] = image
1771 # name can be ../a/b/c
1772 image
.name
= os
.path
.basename(elem_name_utf8
)
1774 if settings
.use_custom_props
:
1775 blen_read_custom_properties(fbx_obj
, image
, settings
)
1780 def blen_read_camera(fbx_tmpl
, fbx_obj
, global_scale
):
1784 elem_name_utf8
= elem_name_ensure_class(fbx_obj
, b
'NodeAttribute')
1786 fbx_props
= (elem_find_first(fbx_obj
, b
'Properties70'),
1787 elem_find_first(fbx_tmpl
, b
'Properties70', fbx_elem_nil
))
1789 camera
= bpy
.data
.cameras
.new(name
=elem_name_utf8
)
1791 camera
.type = 'ORTHO' if elem_props_get_enum(fbx_props
, b
'CameraProjectionType', 0) == 1 else 'PERSP'
1793 camera
.dof
.focus_distance
= elem_props_get_number(fbx_props
, b
'FocusDistance', 10 * 1000) / 1000 * global_scale
1794 if (elem_props_get_bool(fbx_props
, b
'UseDepthOfField', False)):
1795 camera
.dof
.use_dof
= True
1797 camera
.lens
= elem_props_get_number(fbx_props
, b
'FocalLength', 35.0)
1798 camera
.sensor_width
= elem_props_get_number(fbx_props
, b
'FilmWidth', 32.0 * M2I
) / M2I
1799 camera
.sensor_height
= elem_props_get_number(fbx_props
, b
'FilmHeight', 32.0 * M2I
) / M2I
1801 camera
.ortho_scale
= elem_props_get_number(fbx_props
, b
'OrthoZoom', 1.0)
1803 filmaspect
= camera
.sensor_width
/ camera
.sensor_height
1805 camera
.shift_x
= elem_props_get_number(fbx_props
, b
'FilmOffsetX', 0.0) / (M2I
* camera
.sensor_width
)
1806 camera
.shift_y
= elem_props_get_number(fbx_props
, b
'FilmOffsetY', 0.0) / (M2I
* camera
.sensor_height
* filmaspect
)
1808 camera
.clip_start
= elem_props_get_number(fbx_props
, b
'NearPlane', 0.01) * global_scale
1809 camera
.clip_end
= elem_props_get_number(fbx_props
, b
'FarPlane', 100.0) * global_scale
1814 def blen_read_light(fbx_tmpl
, fbx_obj
, global_scale
):
1816 elem_name_utf8
= elem_name_ensure_class(fbx_obj
, b
'NodeAttribute')
1818 fbx_props
= (elem_find_first(fbx_obj
, b
'Properties70'),
1819 elem_find_first(fbx_tmpl
, b
'Properties70', fbx_elem_nil
))
1824 2: 'SPOT'}.get(elem_props_get_enum(fbx_props
, b
'LightType', 0), 'POINT')
1826 lamp
= bpy
.data
.lights
.new(name
=elem_name_utf8
, type=light_type
)
1828 if light_type
== 'SPOT':
1829 spot_size
= elem_props_get_number(fbx_props
, b
'OuterAngle', None)
1830 if spot_size
is None:
1832 spot_size
= elem_props_get_number(fbx_props
, b
'Cone angle', 45.0)
1833 lamp
.spot_size
= math
.radians(spot_size
)
1835 spot_blend
= elem_props_get_number(fbx_props
, b
'InnerAngle', None)
1836 if spot_blend
is None:
1838 spot_blend
= elem_props_get_number(fbx_props
, b
'HotSpot', 45.0)
1839 lamp
.spot_blend
= 1.0 - (spot_blend
/ spot_size
)
1841 # TODO, cycles nodes???
1842 lamp
.color
= elem_props_get_color_rgb(fbx_props
, b
'Color', (1.0, 1.0, 1.0))
1843 lamp
.energy
= elem_props_get_number(fbx_props
, b
'Intensity', 100.0) / 100.0
1844 lamp
.distance
= elem_props_get_number(fbx_props
, b
'DecayStart', 25.0) * global_scale
1845 lamp
.use_shadow
= elem_props_get_bool(fbx_props
, b
'CastShadow', True)
1846 if hasattr(lamp
, "cycles"):
1847 lamp
.cycles
.cast_shadow
= lamp
.use_shadow
1848 # Keeping this for now, but this is not used nor exposed anymore afaik...
1849 lamp
.shadow_color
= elem_props_get_color_rgb(fbx_props
, b
'ShadowColor', (0.0, 0.0, 0.0))
1854 # ### Import Utility class
1855 class FbxImportHelperNode
:
1857 Temporary helper node to store a hierarchy of fbxNode objects before building Objects, Armatures and Bones.
1858 It tries to keep the correction data in one place so it can be applied consistently to the imported data.
1862 '_parent', 'anim_compensation_matrix', 'is_global_animation', 'armature_setup', 'armature', 'bind_matrix',
1863 'bl_bone', 'bl_data', 'bl_obj', 'bone_child_matrix', 'children', 'clusters',
1864 'fbx_elem', 'fbx_name', 'fbx_transform_data', 'fbx_type',
1865 'is_armature', 'has_bone_children', 'is_bone', 'is_root', 'is_leaf',
1866 'matrix', 'matrix_as_parent', 'matrix_geom', 'meshes', 'post_matrix', 'pre_matrix')
1868 def __init__(self
, fbx_elem
, bl_data
, fbx_transform_data
, is_bone
):
1869 self
.fbx_name
= elem_name_ensure_class(fbx_elem
, b
'Model') if fbx_elem
else 'Unknown'
1870 self
.fbx_type
= fbx_elem
.props
[2] if fbx_elem
else None
1871 self
.fbx_elem
= fbx_elem
1873 self
.bl_data
= bl_data
1874 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!)
1875 self
.fbx_transform_data
= fbx_transform_data
1876 self
.is_root
= False
1877 self
.is_bone
= is_bone
1878 self
.is_armature
= False
1879 self
.armature
= None # For bones only, relevant armature node.
1880 self
.has_bone_children
= False # True if the hierarchy below this node contains bones, important to support mixed hierarchies.
1881 self
.is_leaf
= False # True for leaf-bones added to the end of some bone chains to set the lengths.
1882 self
.pre_matrix
= None # correction matrix that needs to be applied before the FBX transform
1883 self
.bind_matrix
= None # for bones this is the matrix used to bind to the skin
1884 if fbx_transform_data
:
1885 self
.matrix
, self
.matrix_as_parent
, self
.matrix_geom
= blen_read_object_transform_do(fbx_transform_data
)
1887 self
.matrix
, self
.matrix_as_parent
, self
.matrix_geom
= (None, None, None)
1888 self
.post_matrix
= None # correction matrix that needs to be applied after the FBX transform
1889 self
.bone_child_matrix
= None # Objects attached to a bone end not the beginning, this matrix corrects for that
1891 # XXX Those two are to handle the fact that rigged meshes are not linked to their armature in FBX, which implies
1892 # that their animation is in global space (afaik...).
1893 # This is actually not really solvable currently, since anim_compensation_matrix is not valid if armature
1894 # itself is animated (we'd have to recompute global-to-local anim_compensation_matrix for each frame,
1895 # and for each armature action... beyond being an insane work).
1896 # Solution for now: do not read rigged meshes animations at all! sic...
1897 self
.anim_compensation_matrix
= None # a mesh moved in the hierarchy may have a different local matrix. This compensates animations for this.
1898 self
.is_global_animation
= False
1900 self
.meshes
= None # List of meshes influenced by this bone.
1901 self
.clusters
= [] # Deformer Cluster nodes
1902 self
.armature_setup
= {} # mesh and armature matrix when the mesh was bound
1912 def parent(self
, value
):
1913 if self
._parent
is not None:
1914 self
._parent
.children
.remove(self
)
1915 self
._parent
= value
1916 if self
._parent
is not None:
1917 self
._parent
.children
.append(self
)
1921 # Separating leaf status from ignore status itself.
1922 # Currently they are equivalent, but this may change in future.
1927 return self
.fbx_elem
.props
[1].decode()
1931 def print_info(self
, indent
=0):
1932 print(" " * indent
+ (self
.fbx_name
if self
.fbx_name
else "(Null)")
1933 + ("[root]" if self
.is_root
else "")
1934 + ("[leaf]" if self
.is_leaf
else "")
1935 + ("[ignore]" if self
.ignore
else "")
1936 + ("[armature]" if self
.is_armature
else "")
1937 + ("[bone]" if self
.is_bone
else "")
1938 + ("[HBC]" if self
.has_bone_children
else "")
1940 for c
in self
.children
:
1941 c
.print_info(indent
+ 1)
1943 def mark_leaf_bones(self
):
1944 if self
.is_bone
and len(self
.children
) == 1:
1945 child
= self
.children
[0]
1946 if child
.is_bone
and len(child
.children
) == 0:
1947 child
.is_leaf
= True
1948 for child
in self
.children
:
1949 child
.mark_leaf_bones()
1951 def do_bake_transform(self
, settings
):
1952 return (settings
.bake_space_transform
and self
.fbx_type
in (b
'Mesh', b
'Null') and
1953 not self
.is_armature
and not self
.is_bone
)
1955 def find_correction_matrix(self
, settings
, parent_correction_inv
=None):
1956 from bpy_extras
.io_utils
import axis_conversion
1958 if self
.parent
and (self
.parent
.is_root
or self
.parent
.do_bake_transform(settings
)):
1959 self
.pre_matrix
= settings
.global_matrix
1961 if parent_correction_inv
:
1962 self
.pre_matrix
= parent_correction_inv
@ (self
.pre_matrix
if self
.pre_matrix
else Matrix())
1964 correction_matrix
= None
1967 if settings
.automatic_bone_orientation
:
1968 # find best orientation to align bone with
1969 bone_children
= tuple(child
for child
in self
.children
if child
.is_bone
)
1970 if len(bone_children
) == 0:
1971 # no children, inherit the correction from parent (if possible)
1972 if self
.parent
and self
.parent
.is_bone
:
1973 correction_matrix
= parent_correction_inv
.inverted() if parent_correction_inv
else None
1975 # else find how best to rotate the bone to align the Y axis with the children
1976 best_axis
= (1, 0, 0)
1977 if len(bone_children
) == 1:
1978 vec
= bone_children
[0].get_bind_matrix().to_translation()
1979 best_axis
= Vector((0, 0, 1 if vec
[2] >= 0 else -1))
1980 if abs(vec
[0]) > abs(vec
[1]):
1981 if abs(vec
[0]) > abs(vec
[2]):
1982 best_axis
= Vector((1 if vec
[0] >= 0 else -1, 0, 0))
1983 elif abs(vec
[1]) > abs(vec
[2]):
1984 best_axis
= Vector((0, 1 if vec
[1] >= 0 else -1, 0))
1986 # get the child directions once because they may be checked several times
1987 child_locs
= (child
.get_bind_matrix().to_translation() for child
in bone_children
)
1988 child_locs
= tuple(loc
.normalized() for loc
in child_locs
if loc
.magnitude
> 0.0)
1990 # I'm not sure which one I like better...
1995 s
= -1 if i
% 2 == 1 else 1
1996 test_axis
= Vector((s
if a
== 0 else 0, s
if a
== 1 else 0, s
if a
== 2 else 0))
1998 # find max angle to children
2000 for loc
in child_locs
:
2001 max_angle
= min(max_angle
, test_axis
.dot(loc
))
2003 # is it better than the last one?
2004 if best_angle
< max_angle
:
2005 best_angle
= max_angle
2006 best_axis
= test_axis
2009 for vec
in child_locs
:
2010 test_axis
= Vector((0, 0, 1 if vec
[2] >= 0 else -1))
2011 if abs(vec
[0]) > abs(vec
[1]):
2012 if abs(vec
[0]) > abs(vec
[2]):
2013 test_axis
= Vector((1 if vec
[0] >= 0 else -1, 0, 0))
2014 elif abs(vec
[1]) > abs(vec
[2]):
2015 test_axis
= Vector((0, 1 if vec
[1] >= 0 else -1, 0))
2017 # find max angle to children
2019 for loc
in child_locs
:
2020 max_angle
= min(max_angle
, test_axis
.dot(loc
))
2022 # is it better than the last one?
2023 if best_angle
< max_angle
:
2024 best_angle
= max_angle
2025 best_axis
= test_axis
2027 # convert best_axis to axis string
2028 to_up
= 'Z' if best_axis
[2] >= 0 else '-Z'
2029 if abs(best_axis
[0]) > abs(best_axis
[1]):
2030 if abs(best_axis
[0]) > abs(best_axis
[2]):
2031 to_up
= 'X' if best_axis
[0] >= 0 else '-X'
2032 elif abs(best_axis
[1]) > abs(best_axis
[2]):
2033 to_up
= 'Y' if best_axis
[1] >= 0 else '-Y'
2034 to_forward
= 'X' if to_up
not in {'X', '-X'} else 'Y'
2036 # Build correction matrix
2037 if (to_up
, to_forward
) != ('Y', 'X'):
2038 correction_matrix
= axis_conversion(from_forward
='X',
2040 to_forward
=to_forward
,
2044 correction_matrix
= settings
.bone_correction_matrix
2046 # camera and light can be hard wired
2047 if self
.fbx_type
== b
'Camera':
2048 correction_matrix
= MAT_CONVERT_CAMERA
2049 elif self
.fbx_type
== b
'Light':
2050 correction_matrix
= MAT_CONVERT_LIGHT
2052 self
.post_matrix
= correction_matrix
2054 if self
.do_bake_transform(settings
):
2055 self
.post_matrix
= settings
.global_matrix_inv
@ (self
.post_matrix
if self
.post_matrix
else Matrix())
2058 correction_matrix_inv
= correction_matrix
.inverted_safe() if correction_matrix
else None
2059 for child
in self
.children
:
2060 child
.find_correction_matrix(settings
, correction_matrix_inv
)
2062 def find_armature_bones(self
, armature
):
2063 for child
in self
.children
:
2065 child
.armature
= armature
2066 child
.find_armature_bones(armature
)
2068 def find_armatures(self
):
2069 needs_armature
= False
2070 for child
in self
.children
:
2072 needs_armature
= True
2075 if self
.fbx_type
in {b
'Null', b
'Root'}:
2076 # if empty then convert into armature
2077 self
.is_armature
= True
2080 # otherwise insert a new node
2081 # XXX Maybe in case self is virtual FBX root node, we should instead add one armature per bone child?
2082 armature
= FbxImportHelperNode(None, None, None, False)
2083 armature
.fbx_name
= "Armature"
2084 armature
.is_armature
= True
2086 for child
in tuple(self
.children
):
2088 child
.parent
= armature
2090 armature
.parent
= self
2092 armature
.find_armature_bones(armature
)
2094 for child
in self
.children
:
2095 if child
.is_armature
or child
.is_bone
:
2097 child
.find_armatures()
2099 def find_bone_children(self
):
2100 has_bone_children
= False
2101 for child
in self
.children
:
2102 has_bone_children |
= child
.find_bone_children()
2103 self
.has_bone_children
= has_bone_children
2104 return self
.is_bone
or has_bone_children
2106 def find_fake_bones(self
, in_armature
=False):
2107 if in_armature
and not self
.is_bone
and self
.has_bone_children
:
2109 # if we are not a null node we need an intermediate node for the data
2110 if self
.fbx_type
not in {b
'Null', b
'Root'}:
2111 node
= FbxImportHelperNode(self
.fbx_elem
, self
.bl_data
, None, False)
2112 self
.fbx_elem
= None
2116 for child
in self
.children
:
2117 if child
.is_bone
or child
.has_bone_children
:
2124 if self
.is_armature
:
2126 for child
in self
.children
:
2127 child
.find_fake_bones(in_armature
)
2129 def get_world_matrix_as_parent(self
):
2130 matrix
= self
.parent
.get_world_matrix_as_parent() if self
.parent
else Matrix()
2131 if self
.matrix_as_parent
:
2132 matrix
= matrix
@ self
.matrix_as_parent
2135 def get_world_matrix(self
):
2136 matrix
= self
.parent
.get_world_matrix_as_parent() if self
.parent
else Matrix()
2138 matrix
= matrix
@ self
.matrix
2141 def get_matrix(self
):
2142 matrix
= self
.matrix
if self
.matrix
else Matrix()
2144 matrix
= self
.pre_matrix
@ matrix
2145 if self
.post_matrix
:
2146 matrix
= matrix
@ self
.post_matrix
2149 def get_bind_matrix(self
):
2150 matrix
= self
.bind_matrix
if self
.bind_matrix
else Matrix()
2152 matrix
= self
.pre_matrix
@ matrix
2153 if self
.post_matrix
:
2154 matrix
= matrix
@ self
.post_matrix
2157 def make_bind_pose_local(self
, parent_matrix
=None):
2158 if parent_matrix
is None:
2159 parent_matrix
= Matrix()
2161 if self
.bind_matrix
:
2162 bind_matrix
= parent_matrix
.inverted_safe() @ self
.bind_matrix
2164 bind_matrix
= self
.matrix
.copy() if self
.matrix
else None
2166 self
.bind_matrix
= bind_matrix
2168 parent_matrix
= parent_matrix
@ bind_matrix
2170 for child
in self
.children
:
2171 child
.make_bind_pose_local(parent_matrix
)
2173 def collect_skeleton_meshes(self
, meshes
):
2174 for _
, m
in self
.clusters
:
2176 for child
in self
.children
:
2177 if not child
.meshes
:
2178 child
.collect_skeleton_meshes(meshes
)
2180 def collect_armature_meshes(self
):
2181 if self
.is_armature
:
2182 armature_matrix_inv
= self
.get_world_matrix().inverted_safe()
2185 for child
in self
.children
:
2186 # Children meshes may be linked to children armatures, in which case we do not want to link them
2187 # to a parent one. See T70244.
2188 child
.collect_armature_meshes()
2189 if not child
.meshes
:
2190 child
.collect_skeleton_meshes(meshes
)
2192 old_matrix
= m
.matrix
2193 m
.matrix
= armature_matrix_inv
@ m
.get_world_matrix()
2194 m
.anim_compensation_matrix
= old_matrix
.inverted_safe() @ m
.matrix
2195 m
.is_global_animation
= True
2197 self
.meshes
= meshes
2199 for child
in self
.children
:
2200 child
.collect_armature_meshes()
2202 def build_skeleton(self
, arm
, parent_matrix
, parent_bone_size
=1, force_connect_children
=False):
2203 def child_connect(par_bone
, child_bone
, child_head
, connect_ctx
):
2204 # child_bone or child_head may be None.
2205 force_connect_children
, connected
= connect_ctx
2206 if child_bone
is not None:
2207 child_bone
.parent
= par_bone
2208 child_head
= child_bone
.head
2210 if similar_values_iter(par_bone
.tail
, child_head
):
2211 if child_bone
is not None:
2212 child_bone
.use_connect
= True
2213 # Disallow any force-connection at this level from now on, since that child was 'really'
2214 # connected, we do not want to move current bone's tail anymore!
2216 elif force_connect_children
and connected
is not None:
2217 # We only store position where tail of par_bone should be in the end.
2218 # Actual tail moving and force connection of compatible child bones will happen
2219 # once all have been checked.
2220 if connected
is ...:
2221 connected
= ([child_head
.copy(), 1], [child_bone
] if child_bone
is not None else [])
2223 connected
[0][0] += child_head
2224 connected
[0][1] += 1
2225 if child_bone
is not None:
2226 connected
[1].append(child_bone
)
2227 connect_ctx
[1] = connected
2229 def child_connect_finalize(par_bone
, connect_ctx
):
2230 force_connect_children
, connected
= connect_ctx
2231 # Do nothing if force connection is not enabled!
2232 if force_connect_children
and connected
is not None and connected
is not ...:
2233 # Here again we have to be wary about zero-length bones!!!
2234 par_tail
= connected
[0][0] / connected
[0][1]
2235 if (par_tail
- par_bone
.head
).magnitude
< 1e-2:
2236 par_bone_vec
= (par_bone
.tail
- par_bone
.head
).normalized()
2237 par_tail
= par_bone
.head
+ par_bone_vec
* 0.01
2238 par_bone
.tail
= par_tail
2239 for child_bone
in connected
[1]:
2240 if similar_values_iter(par_tail
, child_bone
.head
):
2241 child_bone
.use_connect
= True
2243 # Create the (edit)bone.
2244 bone
= arm
.bl_data
.edit_bones
.new(name
=self
.fbx_name
)
2246 self
.bl_obj
= arm
.bl_obj
2247 self
.bl_data
= arm
.bl_data
2248 self
.bl_bone
= bone
.name
# Could be different from the FBX name!
2250 # get average distance to children
2253 for child
in self
.children
:
2255 bone_size
+= child
.get_bind_matrix().to_translation().magnitude
2258 bone_size
/= bone_count
2260 bone_size
= parent_bone_size
2262 # So that our bone gets its final length, but still Y-aligned in armature space.
2263 # 0-length bones are automatically collapsed into their parent when you leave edit mode,
2264 # so this enforces a minimum length.
2265 bone_tail
= Vector((0.0, 1.0, 0.0)) * max(0.01, bone_size
)
2266 bone
.tail
= bone_tail
2268 # And rotate/move it to its final "rest pose".
2269 bone_matrix
= parent_matrix
@ self
.get_bind_matrix().normalized()
2271 bone
.matrix
= bone_matrix
2273 # Correction for children attached to a bone. FBX expects to attach to the head of a bone,
2274 # while Blender attaches to the tail.
2275 self
.bone_child_matrix
= Matrix
.Translation(-bone_tail
)
2277 connect_ctx
= [force_connect_children
, ...]
2278 for child
in self
.children
:
2279 if child
.is_leaf
and force_connect_children
:
2280 # Arggggggggggggggggg! We do not want to create this bone, but we need its 'virtual head' location
2281 # to orient current one!!!
2282 child_head
= (bone_matrix
@ child
.get_bind_matrix().normalized()).translation
2283 child_connect(bone
, None, child_head
, connect_ctx
)
2284 elif child
.is_bone
and not child
.ignore
:
2285 child_bone
= child
.build_skeleton(arm
, bone_matrix
, bone_size
,
2286 force_connect_children
=force_connect_children
)
2287 # Connection to parent.
2288 child_connect(bone
, child_bone
, None, connect_ctx
)
2290 child_connect_finalize(bone
, connect_ctx
)
2293 def build_node_obj(self
, fbx_tmpl
, settings
):
2297 if self
.is_bone
or not self
.fbx_elem
:
2300 # create when linking since we need object data
2301 elem_name_utf8
= self
.fbx_name
2303 # Object data must be created already
2304 self
.bl_obj
= obj
= bpy
.data
.objects
.new(name
=elem_name_utf8
, object_data
=self
.bl_data
)
2306 fbx_props
= (elem_find_first(self
.fbx_elem
, b
'Properties70'),
2307 elem_find_first(fbx_tmpl
, b
'Properties70', fbx_elem_nil
))
2312 obj
.color
[0:3] = elem_props_get_color_rgb(fbx_props
, b
'Color', (0.8, 0.8, 0.8))
2313 obj
.hide_viewport
= not bool(elem_props_get_visibility(fbx_props
, b
'Visibility', 1.0))
2315 obj
.matrix_basis
= self
.get_matrix()
2317 if settings
.use_custom_props
:
2318 blen_read_custom_properties(self
.fbx_elem
, obj
, settings
)
2322 def build_skeleton_children(self
, fbx_tmpl
, settings
, scene
, view_layer
):
2324 for child
in self
.children
:
2327 child
.build_skeleton_children(fbx_tmpl
, settings
, scene
, view_layer
)
2330 # child is not a bone
2331 obj
= self
.build_node_obj(fbx_tmpl
, settings
)
2336 for child
in self
.children
:
2339 child
.build_skeleton_children(fbx_tmpl
, settings
, scene
, view_layer
)
2342 view_layer
.active_layer_collection
.collection
.objects
.link(obj
)
2343 obj
.select_set(True)
2347 def link_skeleton_children(self
, fbx_tmpl
, settings
, scene
):
2349 for child
in self
.children
:
2352 child_obj
= child
.bl_obj
2353 if child_obj
and child_obj
!= self
.bl_obj
:
2354 child_obj
.parent
= self
.bl_obj
# get the armature the bone belongs to
2355 child_obj
.parent_bone
= self
.bl_bone
2356 child_obj
.parent_type
= 'BONE'
2357 child_obj
.matrix_parent_inverse
= Matrix()
2359 # Blender attaches to the end of a bone, while FBX attaches to the start.
2360 # bone_child_matrix corrects for that.
2361 if child
.pre_matrix
:
2362 child
.pre_matrix
= self
.bone_child_matrix
@ child
.pre_matrix
2364 child
.pre_matrix
= self
.bone_child_matrix
2366 child_obj
.matrix_basis
= child
.get_matrix()
2367 child
.link_skeleton_children(fbx_tmpl
, settings
, scene
)
2372 for child
in self
.children
:
2375 child_obj
= child
.link_skeleton_children(fbx_tmpl
, settings
, scene
)
2377 child_obj
.parent
= obj
2381 def set_pose_matrix(self
, arm
):
2382 pose_bone
= arm
.bl_obj
.pose
.bones
[self
.bl_bone
]
2383 pose_bone
.matrix_basis
= self
.get_bind_matrix().inverted_safe() @ self
.get_matrix()
2385 for child
in self
.children
:
2389 child
.set_pose_matrix(arm
)
2391 def merge_weights(self
, combined_weights
, fbx_cluster
):
2392 indices
= elem_prop_first(elem_find_first(fbx_cluster
, b
'Indexes', default
=None), default
=())
2393 weights
= elem_prop_first(elem_find_first(fbx_cluster
, b
'Weights', default
=None), default
=())
2395 for index
, weight
in zip(indices
, weights
):
2396 w
= combined_weights
.get(index
)
2398 combined_weights
[index
] = [weight
]
2402 def set_bone_weights(self
):
2403 ignored_children
= tuple(child
for child
in self
.children
2404 if child
.is_bone
and child
.ignore
and len(child
.clusters
) > 0)
2406 if len(ignored_children
) > 0:
2407 # If we have an ignored child bone we need to merge their weights into the current bone weights.
2408 # This can happen both intentionally and accidentally when skinning a model. Either way, they
2409 # need to be moved into a parent bone or they cause animation glitches.
2410 for fbx_cluster
, meshes
in self
.clusters
:
2411 combined_weights
= {}
2412 self
.merge_weights(combined_weights
, fbx_cluster
)
2414 for child
in ignored_children
:
2415 for child_cluster
, child_meshes
in child
.clusters
:
2416 if not meshes
.isdisjoint(child_meshes
):
2417 self
.merge_weights(combined_weights
, child_cluster
)
2419 # combine child weights
2422 for i
, w
in combined_weights
.items():
2425 weights
.append(sum(w
) / len(w
))
2427 weights
.append(w
[0])
2429 add_vgroup_to_objects(indices
, weights
, self
.bl_bone
, [node
.bl_obj
for node
in meshes
])
2431 # clusters that drive meshes not included in a parent don't need to be merged
2432 all_meshes
= set().union(*[meshes
for _
, meshes
in self
.clusters
])
2433 for child
in ignored_children
:
2434 for child_cluster
, child_meshes
in child
.clusters
:
2435 if all_meshes
.isdisjoint(child_meshes
):
2436 indices
= elem_prop_first(elem_find_first(child_cluster
, b
'Indexes', default
=None), default
=())
2437 weights
= elem_prop_first(elem_find_first(child_cluster
, b
'Weights', default
=None), default
=())
2438 add_vgroup_to_objects(indices
, weights
, self
.bl_bone
, [node
.bl_obj
for node
in child_meshes
])
2440 # set the vertex weights on meshes
2441 for fbx_cluster
, meshes
in self
.clusters
:
2442 indices
= elem_prop_first(elem_find_first(fbx_cluster
, b
'Indexes', default
=None), default
=())
2443 weights
= elem_prop_first(elem_find_first(fbx_cluster
, b
'Weights', default
=None), default
=())
2444 add_vgroup_to_objects(indices
, weights
, self
.bl_bone
, [node
.bl_obj
for node
in meshes
])
2446 for child
in self
.children
:
2447 if child
.is_bone
and not child
.ignore
:
2448 child
.set_bone_weights()
2450 def build_hierarchy(self
, fbx_tmpl
, settings
, scene
, view_layer
):
2451 if self
.is_armature
:
2452 # create when linking since we need object data
2453 elem_name_utf8
= self
.fbx_name
2455 self
.bl_data
= arm_data
= bpy
.data
.armatures
.new(name
=elem_name_utf8
)
2457 # Object data must be created already
2458 self
.bl_obj
= arm
= bpy
.data
.objects
.new(name
=elem_name_utf8
, object_data
=arm_data
)
2460 arm
.matrix_basis
= self
.get_matrix()
2463 fbx_props
= (elem_find_first(self
.fbx_elem
, b
'Properties70'),
2464 elem_find_first(fbx_tmpl
, b
'Properties70', fbx_elem_nil
))
2466 if settings
.use_custom_props
:
2467 blen_read_custom_properties(self
.fbx_elem
, arm
, settings
)
2470 view_layer
.active_layer_collection
.collection
.objects
.link(arm
)
2471 arm
.select_set(True)
2475 # Switch to Edit mode.
2476 view_layer
.objects
.active
= arm
2477 is_hidden
= arm
.hide_viewport
2478 arm
.hide_viewport
= False # Can't switch to Edit mode hidden objects...
2479 bpy
.ops
.object.mode_set(mode
='EDIT')
2481 for child
in self
.children
:
2485 child
.build_skeleton(self
, Matrix(), force_connect_children
=settings
.force_connect_children
)
2487 bpy
.ops
.object.mode_set(mode
='OBJECT')
2489 arm
.hide_viewport
= is_hidden
2492 for child
in self
.children
:
2496 child
.set_pose_matrix(self
)
2498 # Add bone children:
2499 for child
in self
.children
:
2502 child_obj
= child
.build_skeleton_children(fbx_tmpl
, settings
, scene
, view_layer
)
2505 elif self
.fbx_elem
and not self
.is_bone
:
2506 obj
= self
.build_node_obj(fbx_tmpl
, settings
)
2508 # walk through children
2509 for child
in self
.children
:
2510 child
.build_hierarchy(fbx_tmpl
, settings
, scene
, view_layer
)
2513 view_layer
.active_layer_collection
.collection
.objects
.link(obj
)
2514 obj
.select_set(True)
2518 for child
in self
.children
:
2519 child
.build_hierarchy(fbx_tmpl
, settings
, scene
, view_layer
)
2523 def link_hierarchy(self
, fbx_tmpl
, settings
, scene
):
2524 if self
.is_armature
:
2527 # Link bone children:
2528 for child
in self
.children
:
2531 child_obj
= child
.link_skeleton_children(fbx_tmpl
, settings
, scene
)
2533 child_obj
.parent
= arm
2535 # Add armature modifiers to the meshes
2537 for mesh
in self
.meshes
:
2538 (mmat
, amat
) = mesh
.armature_setup
[self
]
2539 me_obj
= mesh
.bl_obj
2541 # bring global armature & mesh matrices into *Blender* global space.
2542 # Note: Usage of matrix_geom (local 'diff' transform) here is quite brittle.
2543 # Among other things, why in hell isn't it taken into account by bindpose & co???
2544 # Probably because org app (max) handles it completely aside from any parenting stuff,
2545 # which we obviously cannot do in Blender. :/
2547 amat
= self
.bind_matrix
2548 amat
= settings
.global_matrix
@ (Matrix() if amat
is None else amat
)
2549 if self
.matrix_geom
:
2550 amat
= amat
@ self
.matrix_geom
2551 mmat
= settings
.global_matrix
@ mmat
2552 if mesh
.matrix_geom
:
2553 mmat
= mmat
@ mesh
.matrix_geom
2555 # Now that we have armature and mesh in there (global) bind 'state' (matrix),
2556 # we can compute inverse parenting matrix of the mesh.
2557 me_obj
.matrix_parent_inverse
= amat
.inverted_safe() @ mmat
@ me_obj
.matrix_basis
.inverted_safe()
2559 mod
= mesh
.bl_obj
.modifiers
.new(arm
.name
, 'ARMATURE')
2562 # Add bone weights to the deformers
2563 for child
in self
.children
:
2567 child
.set_bone_weights()
2573 # walk through children
2574 for child
in self
.children
:
2575 child_obj
= child
.link_hierarchy(fbx_tmpl
, settings
, scene
)
2577 child_obj
.parent
= obj
2581 for child
in self
.children
:
2582 child
.link_hierarchy(fbx_tmpl
, settings
, scene
)
2587 def load(operator
, context
, filepath
="",
2588 use_manual_orientation
=False,
2592 bake_space_transform
=False,
2593 use_custom_normals
=True,
2594 use_image_search
=False,
2595 use_alpha_decals
=False,
2600 use_custom_props
=True,
2601 use_custom_props_enum_as_string
=True,
2602 ignore_leaf_bones
=False,
2603 force_connect_children
=False,
2604 automatic_bone_orientation
=False,
2605 primary_bone_axis
='Y',
2606 secondary_bone_axis
='X',
2607 use_prepost_rot
=True,
2608 colors_type
='SRGB'):
2611 fbx_elem_nil
= FBXElem('', (), (), ())
2615 from bpy_extras
.io_utils
import axis_conversion
2617 from . import parse_fbx
2618 from .fbx_utils
import RIGHT_HAND_AXES
, FBX_FRAMERATES
2620 start_time_proc
= time
.process_time()
2621 start_time_sys
= time
.time()
2625 perfmon
.step("FBX Import: start importing %s" % filepath
)
2628 # Detect ASCII files.
2630 # Typically it's bad practice to fail silently on any error,
2631 # however the file may fail to read for many reasons,
2632 # and this situation is handled later in the code,
2633 # right now we only want to know if the file successfully reads as ascii.
2635 with
open(filepath
, 'r', encoding
="utf-8") as fh
:
2642 operator
.report({'ERROR'}, tip_("ASCII FBX files are not supported %r") % filepath
)
2643 return {'CANCELLED'}
2645 # End ascii detection.
2648 elem_root
, version
= parse_fbx
.parse(filepath
)
2649 except Exception as e
:
2651 traceback
.print_exc()
2653 operator
.report({'ERROR'}, tip_("Couldn't open file %r (%s)") % (filepath
, e
))
2654 return {'CANCELLED'}
2657 operator
.report({'ERROR'}, tip_("Version %r unsupported, must be %r or later") % (version
, 7100))
2658 return {'CANCELLED'}
2660 print("FBX version: %r" % version
)
2662 if bpy
.ops
.object.mode_set
.poll():
2663 bpy
.ops
.object.mode_set(mode
='OBJECT', toggle
=False)
2666 if bpy
.ops
.object.select_all
.poll():
2667 bpy
.ops
.object.select_all(action
='DESELECT')
2669 basedir
= os
.path
.dirname(filepath
)
2671 nodal_material_wrap_map
= {}
2674 # Tables: (FBX_byte_id -> [FBX_data, None or Blender_datablock])
2675 fbx_table_nodes
= {}
2677 if use_alpha_decals
:
2678 material_decals
= set()
2680 material_decals
= None
2682 scene
= context
.scene
2683 view_layer
= context
.view_layer
2685 # #### Get some info from GlobalSettings.
2687 perfmon
.step("FBX import: Prepare...")
2689 fbx_settings
= elem_find_first(elem_root
, b
'GlobalSettings')
2690 fbx_settings_props
= elem_find_first(fbx_settings
, b
'Properties70')
2691 if fbx_settings
is None or fbx_settings_props
is None:
2692 operator
.report({'ERROR'}, tip_("No 'GlobalSettings' found in file %r") % filepath
)
2693 return {'CANCELLED'}
2695 # FBX default base unit seems to be the centimeter, while raw Blender Unit is equivalent to the meter...
2696 unit_scale
= elem_props_get_number(fbx_settings_props
, b
'UnitScaleFactor', 1.0)
2697 unit_scale_org
= elem_props_get_number(fbx_settings_props
, b
'OriginalUnitScaleFactor', 1.0)
2698 global_scale
*= (unit_scale
/ units_blender_to_fbx_factor(context
.scene
))
2699 # Compute global matrix and scale.
2700 if not use_manual_orientation
:
2701 axis_forward
= (elem_props_get_integer(fbx_settings_props
, b
'FrontAxis', 1),
2702 elem_props_get_integer(fbx_settings_props
, b
'FrontAxisSign', 1))
2703 axis_up
= (elem_props_get_integer(fbx_settings_props
, b
'UpAxis', 2),
2704 elem_props_get_integer(fbx_settings_props
, b
'UpAxisSign', 1))
2705 axis_coord
= (elem_props_get_integer(fbx_settings_props
, b
'CoordAxis', 0),
2706 elem_props_get_integer(fbx_settings_props
, b
'CoordAxisSign', 1))
2707 axis_key
= (axis_up
, axis_forward
, axis_coord
)
2708 axis_up
, axis_forward
= {v
: k
for k
, v
in RIGHT_HAND_AXES
.items()}.get(axis_key
, ('Z', 'Y'))
2709 global_matrix
= (Matrix
.Scale(global_scale
, 4) @
2710 axis_conversion(from_forward
=axis_forward
, from_up
=axis_up
).to_4x4())
2712 # To cancel out unwanted rotation/scale on nodes.
2713 global_matrix_inv
= global_matrix
.inverted()
2714 # For transforming mesh normals.
2715 global_matrix_inv_transposed
= global_matrix_inv
.transposed()
2717 # Compute bone correction matrix
2718 bone_correction_matrix
= None # None means no correction/identity
2719 if not automatic_bone_orientation
:
2720 if (primary_bone_axis
, secondary_bone_axis
) != ('Y', 'X'):
2721 bone_correction_matrix
= axis_conversion(from_forward
='X',
2723 to_forward
=secondary_bone_axis
,
2724 to_up
=primary_bone_axis
,
2727 # Compute framerate settings.
2728 custom_fps
= elem_props_get_number(fbx_settings_props
, b
'CustomFrameRate', 25.0)
2729 time_mode
= elem_props_get_enum(fbx_settings_props
, b
'TimeMode')
2730 real_fps
= {eid
: val
for val
, eid
in FBX_FRAMERATES
[1:]}.get(time_mode
, custom_fps
)
2733 scene
.render
.fps
= round(real_fps
)
2734 scene
.render
.fps_base
= scene
.render
.fps
/ real_fps
2736 # store global settings that need to be accessed during conversion
2737 settings
= FBXImportSettings(
2738 operator
.report
, (axis_up
, axis_forward
), global_matrix
, global_scale
,
2739 bake_space_transform
, global_matrix_inv
, global_matrix_inv_transposed
,
2740 use_custom_normals
, use_image_search
,
2741 use_alpha_decals
, decal_offset
,
2742 use_anim
, anim_offset
,
2744 use_custom_props
, use_custom_props_enum_as_string
,
2745 nodal_material_wrap_map
, image_cache
,
2746 ignore_leaf_bones
, force_connect_children
, automatic_bone_orientation
, bone_correction_matrix
,
2747 use_prepost_rot
, colors_type
,
2750 # #### And now, the "real" data.
2752 perfmon
.step("FBX import: Templates...")
2754 fbx_defs
= elem_find_first(elem_root
, b
'Definitions') # can be None
2755 fbx_nodes
= elem_find_first(elem_root
, b
'Objects')
2756 fbx_connections
= elem_find_first(elem_root
, b
'Connections')
2758 if fbx_nodes
is None:
2759 operator
.report({'ERROR'}, tip_("No 'Objects' found in file %r") % filepath
)
2760 return {'CANCELLED'}
2761 if fbx_connections
is None:
2762 operator
.report({'ERROR'}, tip_("No 'Connections' found in file %r") % filepath
)
2763 return {'CANCELLED'}
2766 # First load property templates
2767 # Load 'PropertyTemplate' values.
2768 # Key is a tuple, (ObjectType, FBXNodeType)
2769 # eg, (b'Texture', b'KFbxFileTexture')
2770 # (b'Geometry', b'KFbxMesh')
2774 if fbx_defs
is not None:
2775 for fbx_def
in fbx_defs
.elems
:
2776 if fbx_def
.id == b
'ObjectType':
2777 for fbx_subdef
in fbx_def
.elems
:
2778 if fbx_subdef
.id == b
'PropertyTemplate':
2779 assert(fbx_def
.props_type
== b
'S')
2780 assert(fbx_subdef
.props_type
== b
'S')
2781 # (b'Texture', b'KFbxFileTexture') - eg.
2782 key
= fbx_def
.props
[0], fbx_subdef
.props
[0]
2783 fbx_templates
[key
] = fbx_subdef
2786 def fbx_template_get(key
):
2787 ret
= fbx_templates
.get(key
, fbx_elem_nil
)
2788 if ret
is fbx_elem_nil
:
2789 # Newest FBX (7.4 and above) use no more 'K' in their type names...
2790 key
= (key
[0], key
[1][1:])
2791 return fbx_templates
.get(key
, fbx_elem_nil
)
2794 perfmon
.step("FBX import: Nodes...")
2797 # Build FBX node-table
2799 for fbx_obj
in fbx_nodes
.elems
:
2800 # TODO, investigate what other items after first 3 may be
2801 assert(fbx_obj
.props_type
[:3] == b
'LSS')
2802 fbx_uuid
= elem_uuid(fbx_obj
)
2803 fbx_table_nodes
[fbx_uuid
] = [fbx_obj
, None]
2808 # http://download.autodesk.com/us/fbx/20112/FBX_SDK_HELP/index.html?url=
2809 # WS73099cc142f487551fea285e1221e4f9ff8-7fda.htm,topicNumber=d0e6388
2811 perfmon
.step("FBX import: Connections...")
2813 fbx_connection_map
= {}
2814 fbx_connection_map_reverse
= {}
2817 for fbx_link
in fbx_connections
.elems
:
2818 c_type
= fbx_link
.props
[0]
2819 if fbx_link
.props_type
[1:3] == b
'LL':
2820 c_src
, c_dst
= fbx_link
.props
[1:3]
2821 fbx_connection_map
.setdefault(c_src
, []).append((c_dst
, fbx_link
))
2822 fbx_connection_map_reverse
.setdefault(c_dst
, []).append((c_src
, fbx_link
))
2825 perfmon
.step("FBX import: Meshes...")
2830 fbx_tmpl
= fbx_template_get((b
'Geometry', b
'KFbxMesh'))
2832 for fbx_uuid
, fbx_item
in fbx_table_nodes
.items():
2833 fbx_obj
, blen_data
= fbx_item
2834 if fbx_obj
.id != b
'Geometry':
2836 if fbx_obj
.props
[-1] == b
'Mesh':
2837 assert(blen_data
is None)
2838 fbx_item
[1] = blen_read_geom(fbx_tmpl
, fbx_obj
, settings
)
2841 perfmon
.step("FBX import: Materials & Textures...")
2844 # Load material data
2846 fbx_tmpl
= fbx_template_get((b
'Material', b
'KFbxSurfacePhong'))
2847 # b'KFbxSurfaceLambert'
2849 for fbx_uuid
, fbx_item
in fbx_table_nodes
.items():
2850 fbx_obj
, blen_data
= fbx_item
2851 if fbx_obj
.id != b
'Material':
2853 assert(blen_data
is None)
2854 fbx_item
[1] = blen_read_material(fbx_tmpl
, fbx_obj
, settings
)
2858 # Load image & textures data
2860 fbx_tmpl_tex
= fbx_template_get((b
'Texture', b
'KFbxFileTexture'))
2861 fbx_tmpl_img
= fbx_template_get((b
'Video', b
'KFbxVideo'))
2863 # Important to run all 'Video' ones first, embedded images are stored in those nodes.
2864 # XXX Note we simplify things here, assuming both matching Video and Texture will use same file path,
2865 # this may be a bit weak, if issue arise we'll fallback to plain connection stuff...
2866 for fbx_uuid
, fbx_item
in fbx_table_nodes
.items():
2867 fbx_obj
, blen_data
= fbx_item
2868 if fbx_obj
.id != b
'Video':
2870 fbx_item
[1] = blen_read_texture_image(fbx_tmpl_img
, fbx_obj
, basedir
, settings
)
2871 for fbx_uuid
, fbx_item
in fbx_table_nodes
.items():
2872 fbx_obj
, blen_data
= fbx_item
2873 if fbx_obj
.id != b
'Texture':
2875 fbx_item
[1] = blen_read_texture_image(fbx_tmpl_tex
, fbx_obj
, basedir
, settings
)
2878 perfmon
.step("FBX import: Cameras & Lamps...")
2883 fbx_tmpl
= fbx_template_get((b
'NodeAttribute', b
'KFbxCamera'))
2885 for fbx_uuid
, fbx_item
in fbx_table_nodes
.items():
2886 fbx_obj
, blen_data
= fbx_item
2887 if fbx_obj
.id != b
'NodeAttribute':
2889 if fbx_obj
.props
[-1] == b
'Camera':
2890 assert(blen_data
is None)
2891 fbx_item
[1] = blen_read_camera(fbx_tmpl
, fbx_obj
, global_scale
)
2897 fbx_tmpl
= fbx_template_get((b
'NodeAttribute', b
'KFbxLight'))
2899 for fbx_uuid
, fbx_item
in fbx_table_nodes
.items():
2900 fbx_obj
, blen_data
= fbx_item
2901 if fbx_obj
.id != b
'NodeAttribute':
2903 if fbx_obj
.props
[-1] == b
'Light':
2904 assert(blen_data
is None)
2905 fbx_item
[1] = blen_read_light(fbx_tmpl
, fbx_obj
, global_scale
)
2910 def connection_filter_ex(fbx_uuid
, fbx_id
, dct
):
2911 return [(c_found
[0], c_found
[1], c_type
)
2912 for (c_uuid
, c_type
) in dct
.get(fbx_uuid
, ())
2913 # 0 is used for the root node, which isn't in fbx_table_nodes
2914 for c_found
in (() if c_uuid
== 0 else (fbx_table_nodes
.get(c_uuid
, (None, None)),))
2915 if (fbx_id
is None) or (c_found
[0] and c_found
[0].id == fbx_id
)]
2917 def connection_filter_forward(fbx_uuid
, fbx_id
):
2918 return connection_filter_ex(fbx_uuid
, fbx_id
, fbx_connection_map
)
2920 def connection_filter_reverse(fbx_uuid
, fbx_id
):
2921 return connection_filter_ex(fbx_uuid
, fbx_id
, fbx_connection_map_reverse
)
2923 perfmon
.step("FBX import: Objects & Armatures...")
2925 # -- temporary helper hierarchy to build armatures and objects from
2926 # lookup from uuid to helper node. Used to build parent-child relations and later to look up animated nodes.
2927 fbx_helper_nodes
= {}
2930 # We build an intermediate hierarchy used to:
2931 # - Calculate and store bone orientation correction matrices. The same matrices will be reused for animation.
2932 # - Find/insert armature nodes.
2933 # - Filter leaf bones.
2936 fbx_helper_nodes
[0] = root_helper
= FbxImportHelperNode(None, None, None, False)
2937 root_helper
.is_root
= True
2940 fbx_tmpl
= fbx_template_get((b
'Model', b
'KFbxNode'))
2941 for a_uuid
, a_item
in fbx_table_nodes
.items():
2942 fbx_obj
, bl_data
= a_item
2943 if fbx_obj
is None or fbx_obj
.id != b
'Model':
2946 fbx_props
= (elem_find_first(fbx_obj
, b
'Properties70'),
2947 elem_find_first(fbx_tmpl
, b
'Properties70', fbx_elem_nil
))
2949 transform_data
= blen_read_object_transform_preprocess(fbx_props
, fbx_obj
, Matrix(), use_prepost_rot
)
2950 # Note: 'Root' "bones" are handled as (armature) objects.
2951 # Note: See T46912 for first FBX file I ever saw with 'Limb' bones - thought those were totally deprecated.
2952 is_bone
= fbx_obj
.props
[2] in {b
'LimbNode', b
'Limb'}
2953 fbx_helper_nodes
[a_uuid
] = FbxImportHelperNode(fbx_obj
, bl_data
, transform_data
, is_bone
)
2955 # add parent-child relations and add blender data to the node
2956 for fbx_link
in fbx_connections
.elems
:
2957 if fbx_link
.props
[0] != b
'OO':
2959 if fbx_link
.props_type
[1:3] == b
'LL':
2960 c_src
, c_dst
= fbx_link
.props
[1:3]
2961 parent
= fbx_helper_nodes
.get(c_dst
)
2965 child
= fbx_helper_nodes
.get(c_src
)
2967 # add blender data (meshes, lights, cameras, etc.) to a helper node
2968 fbx_sdata
, bl_data
= p_item
= fbx_table_nodes
.get(c_src
, (None, None))
2969 if fbx_sdata
is None:
2971 if fbx_sdata
.id not in {b
'Geometry', b
'NodeAttribute'}:
2973 parent
.bl_data
= bl_data
2976 child
.parent
= parent
2978 # find armatures (either an empty below a bone or a new node inserted at the bone
2979 root_helper
.find_armatures()
2981 # mark nodes that have bone children
2982 root_helper
.find_bone_children()
2984 # mark nodes that need a bone to attach child-bones to
2985 root_helper
.find_fake_bones()
2987 # mark leaf nodes that are only required to mark the end of their parent bone
2988 if settings
.ignore_leaf_bones
:
2989 root_helper
.mark_leaf_bones()
2991 # What a mess! Some bones have several BindPoses, some have none, clusters contain a bind pose as well,
2992 # and you can have several clusters per bone!
2993 # Maybe some conversion can be applied to put them all into the same frame of reference?
2995 # get the bind pose from pose elements
2996 for a_uuid
, a_item
in fbx_table_nodes
.items():
2997 fbx_obj
, bl_data
= a_item
3000 if fbx_obj
.id != b
'Pose':
3002 if fbx_obj
.props
[2] != b
'BindPose':
3004 for fbx_pose_node
in fbx_obj
.elems
:
3005 if fbx_pose_node
.id != b
'PoseNode':
3007 node_elem
= elem_find_first(fbx_pose_node
, b
'Node')
3008 node
= elem_uuid(node_elem
)
3009 matrix_elem
= elem_find_first(fbx_pose_node
, b
'Matrix')
3010 matrix
= array_to_matrix4(matrix_elem
.props
[0]) if matrix_elem
else None
3011 bone
= fbx_helper_nodes
.get(node
)
3013 # Store the matrix in the helper node.
3014 # There may be several bind pose matrices for the same node, but in tests they seem to be identical.
3015 bone
.bind_matrix
= matrix
# global space
3017 # get clusters and bind pose
3018 for helper_uuid
, helper_node
in fbx_helper_nodes
.items():
3019 if not helper_node
.is_bone
:
3021 for cluster_uuid
, cluster_link
in fbx_connection_map
.get(helper_uuid
, ()):
3022 if cluster_link
.props
[0] != b
'OO':
3024 fbx_cluster
, _
= fbx_table_nodes
.get(cluster_uuid
, (None, None))
3025 if fbx_cluster
is None or fbx_cluster
.id != b
'Deformer' or fbx_cluster
.props
[2] != b
'Cluster':
3028 # Get the bind pose from the cluster:
3029 tx_mesh_elem
= elem_find_first(fbx_cluster
, b
'Transform', default
=None)
3030 tx_mesh
= array_to_matrix4(tx_mesh_elem
.props
[0]) if tx_mesh_elem
else Matrix()
3032 tx_bone_elem
= elem_find_first(fbx_cluster
, b
'TransformLink', default
=None)
3033 tx_bone
= array_to_matrix4(tx_bone_elem
.props
[0]) if tx_bone_elem
else None
3035 tx_arm_elem
= elem_find_first(fbx_cluster
, b
'TransformAssociateModel', default
=None)
3036 tx_arm
= array_to_matrix4(tx_arm_elem
.props
[0]) if tx_arm_elem
else None
3038 mesh_matrix
= tx_mesh
3039 armature_matrix
= tx_arm
3042 mesh_matrix
= tx_bone
@ mesh_matrix
3043 helper_node
.bind_matrix
= tx_bone
# overwrite the bind matrix
3045 # Get the meshes driven by this cluster: (Shouldn't that be only one?)
3047 for skin_uuid
, skin_link
in fbx_connection_map
.get(cluster_uuid
):
3048 if skin_link
.props
[0] != b
'OO':
3050 fbx_skin
, _
= fbx_table_nodes
.get(skin_uuid
, (None, None))
3051 if fbx_skin
is None or fbx_skin
.id != b
'Deformer' or fbx_skin
.props
[2] != b
'Skin':
3053 for mesh_uuid
, mesh_link
in fbx_connection_map
.get(skin_uuid
):
3054 if mesh_link
.props
[0] != b
'OO':
3056 fbx_mesh
, _
= fbx_table_nodes
.get(mesh_uuid
, (None, None))
3057 if fbx_mesh
is None or fbx_mesh
.id != b
'Geometry' or fbx_mesh
.props
[2] != b
'Mesh':
3059 for object_uuid
, object_link
in fbx_connection_map
.get(mesh_uuid
):
3060 if object_link
.props
[0] != b
'OO':
3062 mesh_node
= fbx_helper_nodes
[object_uuid
]
3065 # If we get a valid mesh matrix (in bone space), store armature and
3066 # mesh global matrices, we need them to compute mesh's matrix_parent_inverse
3067 # when actually binding them via the modifier.
3068 # Note we assume all bones were bound with the same mesh/armature (global) matrix,
3069 # we do not support otherwise in Blender anyway!
3070 mesh_node
.armature_setup
[helper_node
.armature
] = (mesh_matrix
, armature_matrix
)
3071 meshes
.add(mesh_node
)
3073 helper_node
.clusters
.append((fbx_cluster
, meshes
))
3075 # convert bind poses from global space into local space
3076 root_helper
.make_bind_pose_local()
3078 # collect armature meshes
3079 root_helper
.collect_armature_meshes()
3081 # find the correction matrices to align FBX objects with their Blender equivalent
3082 root_helper
.find_correction_matrix(settings
)
3084 # build the Object/Armature/Bone hierarchy
3085 root_helper
.build_hierarchy(fbx_tmpl
, settings
, scene
, view_layer
)
3087 # Link the Object/Armature/Bone hierarchy
3088 root_helper
.link_hierarchy(fbx_tmpl
, settings
, scene
)
3090 # root_helper.print_info(0)
3093 perfmon
.step("FBX import: ShapeKeys...")
3095 # We can handle shapes.
3096 blend_shape_channels
= {} # We do not need Shapes themselves, but keyblocks, for anim.
3099 fbx_tmpl
= fbx_template_get((b
'Geometry', b
'KFbxShape'))
3102 for s_uuid
, s_item
in fbx_table_nodes
.items():
3103 fbx_sdata
, bl_sdata
= s_item
= fbx_table_nodes
.get(s_uuid
, (None, None))
3104 if fbx_sdata
is None or fbx_sdata
.id != b
'Geometry' or fbx_sdata
.props
[2] != b
'Shape':
3107 # shape -> blendshapechannel -> blendshape -> mesh.
3108 for bc_uuid
, bc_ctype
in fbx_connection_map
.get(s_uuid
, ()):
3109 if bc_ctype
.props
[0] != b
'OO':
3111 fbx_bcdata
, _bl_bcdata
= fbx_table_nodes
.get(bc_uuid
, (None, None))
3112 if fbx_bcdata
is None or fbx_bcdata
.id != b
'Deformer' or fbx_bcdata
.props
[2] != b
'BlendShapeChannel':
3114 for bs_uuid
, bs_ctype
in fbx_connection_map
.get(bc_uuid
, ()):
3115 if bs_ctype
.props
[0] != b
'OO':
3117 fbx_bsdata
, _bl_bsdata
= fbx_table_nodes
.get(bs_uuid
, (None, None))
3118 if fbx_bsdata
is None or fbx_bsdata
.id != b
'Deformer' or fbx_bsdata
.props
[2] != b
'BlendShape':
3120 for m_uuid
, m_ctype
in fbx_connection_map
.get(bs_uuid
, ()):
3121 if m_ctype
.props
[0] != b
'OO':
3123 fbx_mdata
, bl_mdata
= fbx_table_nodes
.get(m_uuid
, (None, None))
3124 if fbx_mdata
is None or fbx_mdata
.id != b
'Geometry' or fbx_mdata
.props
[2] != b
'Mesh':
3126 # Blenmeshes are assumed already created at that time!
3127 assert(isinstance(bl_mdata
, bpy
.types
.Mesh
))
3128 # Group shapes by mesh so that each mesh only needs to be processed once for all of its shape
3130 if bl_mdata
not in mesh_to_shapes
:
3131 # And we have to find all objects using this mesh!
3133 for o_uuid
, o_ctype
in fbx_connection_map
.get(m_uuid
, ()):
3134 if o_ctype
.props
[0] != b
'OO':
3136 node
= fbx_helper_nodes
[o_uuid
]
3138 objects
.append(node
)
3140 mesh_to_shapes
[bl_mdata
] = (objects
, shapes_list
)
3142 shapes_list
= mesh_to_shapes
[bl_mdata
][1]
3143 shapes_list
.append((bc_uuid
, fbx_sdata
, fbx_bcdata
))
3144 # BlendShape deformers are only here to connect BlendShapeChannels to meshes, nothing else to do.
3146 # Iterate through each mesh and create its shape keys
3147 for bl_mdata
, (objects
, shapes
) in mesh_to_shapes
.items():
3148 for bc_uuid
, keyblocks
in blen_read_shapes(fbx_tmpl
, shapes
, objects
, bl_mdata
, scene
).items():
3149 # keyblocks is a list of tuples (mesh, keyblock) matching that shape/blendshapechannel, for animation.
3150 blend_shape_channels
.setdefault(bc_uuid
, []).extend(keyblocks
)
3153 if settings
.use_subsurf
:
3154 perfmon
.step("FBX import: Subdivision surfaces")
3156 # Look through connections for subsurf in meshes and add it to the parent object
3158 for fbx_link
in fbx_connections
.elems
:
3159 if fbx_link
.props
[0] != b
'OO':
3161 if fbx_link
.props_type
[1:3] == b
'LL':
3162 c_src
, c_dst
= fbx_link
.props
[1:3]
3163 parent
= fbx_helper_nodes
.get(c_dst
)
3167 child
= fbx_helper_nodes
.get(c_src
)
3169 fbx_sdata
, bl_data
= fbx_table_nodes
.get(c_src
, (None, None))
3170 if fbx_sdata
.id != b
'Geometry':
3173 preview_levels
= elem_prop_first(elem_find_first(fbx_sdata
, b
'PreviewDivisionLevels'))
3174 render_levels
= elem_prop_first(elem_find_first(fbx_sdata
, b
'RenderDivisionLevels'))
3175 if isinstance(preview_levels
, int) and isinstance(render_levels
, int):
3176 mod
= parent
.bl_obj
.modifiers
.new('subsurf', 'SUBSURF')
3177 mod
.levels
= preview_levels
3178 mod
.render_levels
= render_levels
3179 boundary_rule
= elem_prop_first(elem_find_first(fbx_sdata
, b
'BoundaryRule'), default
=1)
3180 if boundary_rule
== 1:
3181 mod
.boundary_smooth
= "PRESERVE_CORNERS"
3183 mod
.boundary_smooth
= "ALL"
3188 perfmon
.step("FBX import: Animations...")
3192 fbx_tmpl_astack
= fbx_template_get((b
'AnimationStack', b
'FbxAnimStack'))
3193 fbx_tmpl_alayer
= fbx_template_get((b
'AnimationLayer', b
'FbxAnimLayer'))
3197 for as_uuid
, fbx_asitem
in fbx_table_nodes
.items():
3198 fbx_asdata
, _blen_data
= fbx_asitem
3199 if fbx_asdata
.id != b
'AnimationStack' or fbx_asdata
.props
[2] != b
'':
3201 stacks
[as_uuid
] = (fbx_asitem
, {})
3204 # (mixing is completely ignored for now, each layer results in an independent set of actions).
3205 def get_astacks_from_alayer(al_uuid
):
3206 for as_uuid
, as_ctype
in fbx_connection_map
.get(al_uuid
, ()):
3207 if as_ctype
.props
[0] != b
'OO':
3209 fbx_asdata
, _bl_asdata
= fbx_table_nodes
.get(as_uuid
, (None, None))
3210 if (fbx_asdata
is None or fbx_asdata
.id != b
'AnimationStack' or
3211 fbx_asdata
.props
[2] != b
'' or as_uuid
not in stacks
):
3214 for al_uuid
, fbx_alitem
in fbx_table_nodes
.items():
3215 fbx_aldata
, _blen_data
= fbx_alitem
3216 if fbx_aldata
.id != b
'AnimationLayer' or fbx_aldata
.props
[2] != b
'':
3218 for as_uuid
in get_astacks_from_alayer(al_uuid
):
3219 _fbx_asitem
, alayers
= stacks
[as_uuid
]
3220 alayers
[al_uuid
] = (fbx_alitem
, {})
3222 # AnimationCurveNodes (also the ones linked to actual animated data!).
3224 for acn_uuid
, fbx_acnitem
in fbx_table_nodes
.items():
3225 fbx_acndata
, _blen_data
= fbx_acnitem
3226 if fbx_acndata
.id != b
'AnimationCurveNode' or fbx_acndata
.props
[2] != b
'':
3228 cnode
= curvenodes
[acn_uuid
] = {}
3230 for n_uuid
, n_ctype
in fbx_connection_map
.get(acn_uuid
, ()):
3231 if n_ctype
.props
[0] != b
'OP':
3233 lnk_prop
= n_ctype
.props
[3]
3234 if lnk_prop
in {b
'Lcl Translation', b
'Lcl Rotation', b
'Lcl Scaling'}:
3235 # n_uuid can (????) be linked to root '0' node, instead of a mere object node... See T41712.
3236 ob
= fbx_helper_nodes
.get(n_uuid
, None)
3237 if ob
is None or ob
.is_root
:
3239 items
.append((ob
, lnk_prop
))
3240 elif lnk_prop
== b
'DeformPercent': # Shape keys.
3241 keyblocks
= blend_shape_channels
.get(n_uuid
, None)
3242 if keyblocks
is None:
3244 items
+= [(kb
, lnk_prop
) for kb
in keyblocks
]
3245 elif lnk_prop
== b
'FocalLength': # Camera lens.
3246 from bpy
.types
import Camera
3247 fbx_item
= fbx_table_nodes
.get(n_uuid
, None)
3248 if fbx_item
is None or not isinstance(fbx_item
[1], Camera
):
3251 items
.append((cam
, lnk_prop
))
3252 elif lnk_prop
== b
'FocusDistance': # Camera focus.
3253 from bpy
.types
import Camera
3254 fbx_item
= fbx_table_nodes
.get(n_uuid
, None)
3255 if fbx_item
is None or not isinstance(fbx_item
[1], Camera
):
3258 items
.append((cam
, lnk_prop
))
3259 elif lnk_prop
== b
'DiffuseColor':
3260 from bpy
.types
import Material
3261 fbx_item
= fbx_table_nodes
.get(n_uuid
, None)
3262 if fbx_item
is None or not isinstance(fbx_item
[1], Material
):
3265 items
.append((mat
, lnk_prop
))
3266 print("WARNING! Importing material's animation is not supported for Nodal materials...")
3267 for al_uuid
, al_ctype
in fbx_connection_map
.get(acn_uuid
, ()):
3268 if al_ctype
.props
[0] != b
'OO':
3270 fbx_aldata
, _blen_aldata
= fbx_alitem
= fbx_table_nodes
.get(al_uuid
, (None, None))
3271 if fbx_aldata
is None or fbx_aldata
.id != b
'AnimationLayer' or fbx_aldata
.props
[2] != b
'':
3273 for as_uuid
in get_astacks_from_alayer(al_uuid
):
3274 _fbx_alitem
, anim_items
= stacks
[as_uuid
][1][al_uuid
]
3275 assert(_fbx_alitem
== fbx_alitem
)
3276 for item
, item_prop
in items
:
3277 # No need to keep curvenode FBX data here, contains nothing useful for us.
3278 anim_items
.setdefault(item
, {})[acn_uuid
] = (cnode
, item_prop
)
3280 # AnimationCurves (real animation data).
3281 for ac_uuid
, fbx_acitem
in fbx_table_nodes
.items():
3282 fbx_acdata
, _blen_data
= fbx_acitem
3283 if fbx_acdata
.id != b
'AnimationCurve' or fbx_acdata
.props
[2] != b
'':
3285 for acn_uuid
, acn_ctype
in fbx_connection_map
.get(ac_uuid
, ()):
3286 if acn_ctype
.props
[0] != b
'OP':
3288 fbx_acndata
, _bl_acndata
= fbx_table_nodes
.get(acn_uuid
, (None, None))
3289 if (fbx_acndata
is None or fbx_acndata
.id != b
'AnimationCurveNode' or
3290 fbx_acndata
.props
[2] != b
'' or acn_uuid
not in curvenodes
):
3292 # Note this is an infamous simplification of the compound props stuff,
3293 # seems to be standard naming but we'll probably have to be smarter to handle more exotic files?
3295 b
'd|X': 0, b
'd|Y': 1, b
'd|Z': 2,
3296 b
'd|DeformPercent': 0,
3297 b
'd|FocalLength': 0,
3298 b
'd|FocusDistance': 0
3299 }.get(acn_ctype
.props
[3], None)
3302 curvenodes
[acn_uuid
][ac_uuid
] = (fbx_acitem
, channel
)
3304 # And now that we have sorted all this, apply animations!
3305 blen_read_animations(fbx_tmpl_astack
, fbx_tmpl_alayer
, stacks
, scene
, settings
.anim_offset
, global_scale
)
3309 perfmon
.step("FBX import: Assign materials...")
3312 # link Material's to Geometry (via Model's)
3313 for fbx_uuid
, fbx_item
in fbx_table_nodes
.items():
3314 fbx_obj
, blen_data
= fbx_item
3315 if fbx_obj
.id != b
'Geometry':
3318 mesh
= fbx_table_nodes
.get(fbx_uuid
, (None, None))[1]
3320 # can happen in rare cases
3324 # In Blender, we link materials to data, typically (meshes), while in FBX they are linked to objects...
3325 # So we have to be careful not to re-add endlessly the same material to a mesh!
3326 # This can easily happen with 'baked' dupliobjects, see T44386.
3327 # TODO: add an option to link materials to objects in Blender instead?
3328 done_materials
= set()
3330 for (fbx_lnk
, fbx_lnk_item
, fbx_lnk_type
) in connection_filter_forward(fbx_uuid
, b
'Model'):
3332 fbx_lnk_uuid
= elem_uuid(fbx_lnk
)
3333 for (fbx_lnk_material
, material
, fbx_lnk_material_type
) in connection_filter_reverse(fbx_lnk_uuid
, b
'Material'):
3334 if material
not in done_materials
:
3335 mesh
.materials
.append(material
)
3336 done_materials
.add(material
)
3338 # We have to validate mesh polygons' ma_idx, see T41015!
3339 # Some FBX seem to have an extra 'default' material which is not defined in FBX file.
3340 if mesh
.validate_material_indices():
3341 print("WARNING: mesh '%s' had invalid material indices, those were reset to first material" % mesh
.name
)
3344 perfmon
.step("FBX import: Assign textures...")
3347 material_images
= {}
3349 fbx_tmpl
= fbx_template_get((b
'Material', b
'KFbxSurfacePhong'))
3350 # b'KFbxSurfaceLambert'
3352 def texture_mapping_set(fbx_obj
, node_texture
):
3353 assert(fbx_obj
.id == b
'Texture')
3355 fbx_props
= (elem_find_first(fbx_obj
, b
'Properties70'),
3356 elem_find_first(fbx_tmpl
, b
'Properties70', fbx_elem_nil
))
3357 loc
= elem_props_get_vector_3d(fbx_props
, b
'Translation', (0.0, 0.0, 0.0))
3358 rot
= tuple(-r
for r
in elem_props_get_vector_3d(fbx_props
, b
'Rotation', (0.0, 0.0, 0.0)))
3359 scale
= tuple(((1.0 / s
) if s
!= 0.0 else 1.0)
3360 for s
in elem_props_get_vector_3d(fbx_props
, b
'Scaling', (1.0, 1.0, 1.0)))
3361 clamp
= (bool(elem_props_get_enum(fbx_props
, b
'WrapModeU', 0)) or
3362 bool(elem_props_get_enum(fbx_props
, b
'WrapModeV', 0)))
3364 if (loc
== (0.0, 0.0, 0.0) and
3365 rot
== (0.0, 0.0, 0.0) and
3366 scale
== (1.0, 1.0, 1.0) and
3370 node_texture
.translation
= loc
3371 node_texture
.rotation
= rot
3372 node_texture
.scale
= scale
3374 node_texture
.extension
= 'EXTEND'
3376 for fbx_uuid
, fbx_item
in fbx_table_nodes
.items():
3377 fbx_obj
, blen_data
= fbx_item
3378 if fbx_obj
.id != b
'Material':
3381 material
= fbx_table_nodes
.get(fbx_uuid
, (None, None))[1]
3384 fbx_lnk_type
) in connection_filter_reverse(fbx_uuid
, b
'Texture'):
3386 if fbx_lnk_type
.props
[0] == b
'OP':
3387 lnk_type
= fbx_lnk_type
.props
[3]
3389 ma_wrap
= nodal_material_wrap_map
[material
]
3391 if lnk_type
in {b
'DiffuseColor', b
'3dsMax|maps|texmap_diffuse'}:
3392 ma_wrap
.base_color_texture
.image
= image
3393 texture_mapping_set(fbx_lnk
, ma_wrap
.base_color_texture
)
3394 elif lnk_type
in {b
'SpecularColor', b
'SpecularFactor'}:
3395 # Intensity actually, not color...
3396 ma_wrap
.specular_texture
.image
= image
3397 texture_mapping_set(fbx_lnk
, ma_wrap
.specular_texture
)
3398 elif lnk_type
in {b
'ReflectionColor', b
'ReflectionFactor', b
'3dsMax|maps|texmap_reflection'}:
3399 # Intensity actually, not color...
3400 ma_wrap
.metallic_texture
.image
= image
3401 texture_mapping_set(fbx_lnk
, ma_wrap
.metallic_texture
)
3402 elif lnk_type
in {b
'TransparentColor', b
'TransparencyFactor'}:
3403 ma_wrap
.alpha_texture
.image
= image
3404 texture_mapping_set(fbx_lnk
, ma_wrap
.alpha_texture
)
3405 if use_alpha_decals
:
3406 material_decals
.add(material
)
3407 elif lnk_type
== b
'ShininessExponent':
3408 # That is probably reversed compared to expected results? TODO...
3409 ma_wrap
.roughness_texture
.image
= image
3410 texture_mapping_set(fbx_lnk
, ma_wrap
.roughness_texture
)
3411 # XXX, applications abuse bump!
3412 elif lnk_type
in {b
'NormalMap', b
'Bump', b
'3dsMax|maps|texmap_bump'}:
3413 ma_wrap
.normalmap_texture
.image
= image
3414 texture_mapping_set(fbx_lnk
, ma_wrap
.normalmap_texture
)
3416 elif lnk_type == b'Bump':
3417 # TODO displacement...
3419 elif lnk_type
in {b
'EmissiveColor'}:
3420 ma_wrap
.emission_color_texture
.image
= image
3421 texture_mapping_set(fbx_lnk
, ma_wrap
.emission_color_texture
)
3422 elif lnk_type
in {b
'EmissiveFactor'}:
3423 ma_wrap
.emission_strength_texture
.image
= image
3424 texture_mapping_set(fbx_lnk
, ma_wrap
.emission_strength_texture
)
3426 print("WARNING: material link %r ignored" % lnk_type
)
3428 material_images
.setdefault(material
, {})[lnk_type
] = image
3430 # Check if the diffuse image has an alpha channel,
3431 # if so, use the alpha channel.
3433 # Note: this could be made optional since images may have alpha but be entirely opaque
3434 for fbx_uuid
, fbx_item
in fbx_table_nodes
.items():
3435 fbx_obj
, blen_data
= fbx_item
3436 if fbx_obj
.id != b
'Material':
3438 material
= fbx_table_nodes
.get(fbx_uuid
, (None, None))[1]
3439 image
= material_images
.get(material
, {}).get(b
'DiffuseColor', None)
3441 if image
and image
.depth
== 32:
3442 if use_alpha_decals
:
3443 material_decals
.add(material
)
3445 ma_wrap
= nodal_material_wrap_map
[material
]
3446 ma_wrap
.alpha_texture
.use_alpha
= True
3447 ma_wrap
.alpha_texture
.copy_from(ma_wrap
.base_color_texture
)
3449 # Propagate mapping from diffuse to all other channels which have none defined.
3450 # XXX Commenting for now, I do not really understand the logic here, why should diffuse mapping
3451 # be applied to all others if not defined for them???
3452 # ~ ma_wrap = nodal_material_wrap_map[material]
3453 # ~ ma_wrap.mapping_set_from_diffuse()
3457 perfmon
.step("FBX import: Cycles z-offset workaround...")
3460 # Annoying workaround for cycles having no z-offset
3461 if material_decals
and use_alpha_decals
:
3462 for fbx_uuid
, fbx_item
in fbx_table_nodes
.items():
3463 fbx_obj
, blen_data
= fbx_item
3464 if fbx_obj
.id != b
'Geometry':
3466 if fbx_obj
.props
[-1] == b
'Mesh':
3469 if decal_offset
!= 0.0:
3470 for material
in mesh
.materials
:
3471 if material
in material_decals
:
3472 num_verts
= len(mesh
.vertices
)
3473 blen_cos_dtype
= blen_norm_dtype
= np
.single
3474 vcos
= np
.empty(num_verts
* 3, dtype
=blen_cos_dtype
)
3475 vnorm
= np
.empty(num_verts
* 3, dtype
=blen_norm_dtype
)
3476 mesh
.vertices
.foreach_get("co", vcos
)
3477 mesh
.vertices
.foreach_get("normal", vnorm
)
3479 vcos
+= vnorm
* decal_offset
3481 mesh
.vertices
.foreach_set("co", vcos
)
3484 for obj
in (obj
for obj
in bpy
.data
.objects
if obj
.data
== mesh
):
3485 obj
.visible_shadow
= False
3488 perfmon
.level_down()
3490 perfmon
.level_down("Import finished.")