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