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