Node Wrangler: change NW menu shortcut to Shift+W
[blender-addons.git] / io_scene_fbx / import_fbx.py
blob26e31363f9bd844d0ba8850f83db52bfb545f9be
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 return name[:55].decode('utf-8', 'replace') + "_" + h[:7]
74 else:
75 # We use 'replace' even though FBX 'specs' say it should always be utf8, see T53841.
76 return name.decode('utf-8', 'replace')
79 def elem_find_first(elem, id_search, default=None):
80 for fbx_item in elem.elems:
81 if fbx_item.id == id_search:
82 return fbx_item
83 return default
86 def elem_find_iter(elem, id_search):
87 for fbx_item in elem.elems:
88 if fbx_item.id == id_search:
89 yield fbx_item
92 def elem_find_first_string(elem, id_search):
93 fbx_item = elem_find_first(elem, id_search)
94 if fbx_item is not None and fbx_item.props: # Do not error on complete empty properties (see T45291).
95 assert(len(fbx_item.props) == 1)
96 assert(fbx_item.props_type[0] == data_types.STRING)
97 return fbx_item.props[0].decode('utf-8', 'replace')
98 return None
101 def elem_find_first_string_as_bytes(elem, id_search):
102 fbx_item = elem_find_first(elem, id_search)
103 if fbx_item is not None and fbx_item.props: # Do not error on complete empty properties (see T45291).
104 assert(len(fbx_item.props) == 1)
105 assert(fbx_item.props_type[0] == data_types.STRING)
106 return fbx_item.props[0] # Keep it as bytes as requested...
107 return None
110 def elem_find_first_bytes(elem, id_search, decode=True):
111 fbx_item = elem_find_first(elem, id_search)
112 if fbx_item is not None and fbx_item.props: # Do not error on complete empty properties (see T45291).
113 assert(len(fbx_item.props) == 1)
114 assert(fbx_item.props_type[0] == data_types.BYTES)
115 return fbx_item.props[0]
116 return None
119 def elem_repr(elem):
120 return "%s: props[%d=%r], elems=(%r)" % (
121 elem.id,
122 len(elem.props),
123 ", ".join([repr(p) for p in elem.props]),
124 # elem.props_type,
125 b", ".join([e.id for e in elem.elems]),
129 def elem_split_name_class(elem):
130 assert(elem.props_type[-2] == data_types.STRING)
131 elem_name, elem_class = elem.props[-2].split(b'\x00\x01')
132 return elem_name, elem_class
135 def elem_name_ensure_class(elem, clss=...):
136 elem_name, elem_class = elem_split_name_class(elem)
137 if clss is not ...:
138 assert(elem_class == clss)
139 return validate_blend_names(elem_name)
142 def elem_name_ensure_classes(elem, clss=...):
143 elem_name, elem_class = elem_split_name_class(elem)
144 if clss is not ...:
145 assert(elem_class in clss)
146 return validate_blend_names(elem_name)
149 def elem_split_name_class_nodeattr(elem):
150 assert(elem.props_type[-2] == data_types.STRING)
151 elem_name, elem_class = elem.props[-2].split(b'\x00\x01')
152 assert(elem_class == b'NodeAttribute')
153 assert(elem.props_type[-1] == data_types.STRING)
154 elem_class = elem.props[-1]
155 return elem_name, elem_class
158 def elem_uuid(elem):
159 assert(elem.props_type[0] == data_types.INT64)
160 return elem.props[0]
163 def elem_prop_first(elem, default=None):
164 return elem.props[0] if (elem is not None) and elem.props else default
167 # ----
168 # Support for
169 # Properties70: { ... P:
170 def elem_props_find_first(elem, elem_prop_id):
171 if elem is None:
172 # When properties are not found... Should never happen, but happens - as usual.
173 return None
174 # support for templates (tuple of elems)
175 if type(elem) is not FBXElem:
176 assert(type(elem) is tuple)
177 for e in elem:
178 result = elem_props_find_first(e, elem_prop_id)
179 if result is not None:
180 return result
181 assert(len(elem) > 0)
182 return None
184 for subelem in elem.elems:
185 assert(subelem.id == b'P')
186 if subelem.props[0] == elem_prop_id:
187 return subelem
188 return None
191 def elem_props_get_color_rgb(elem, elem_prop_id, default=None):
192 elem_prop = elem_props_find_first(elem, elem_prop_id)
193 if elem_prop is not None:
194 assert(elem_prop.props[0] == elem_prop_id)
195 if elem_prop.props[1] == b'Color':
196 # FBX version 7300
197 assert(elem_prop.props[1] == b'Color')
198 assert(elem_prop.props[2] == b'')
199 else:
200 assert(elem_prop.props[1] == b'ColorRGB')
201 assert(elem_prop.props[2] == b'Color')
202 assert(elem_prop.props_type[4:7] == bytes((data_types.FLOAT64,)) * 3)
203 return elem_prop.props[4:7]
204 return default
207 def elem_props_get_vector_3d(elem, elem_prop_id, default=None):
208 elem_prop = elem_props_find_first(elem, elem_prop_id)
209 if elem_prop is not None:
210 assert(elem_prop.props_type[4:7] == bytes((data_types.FLOAT64,)) * 3)
211 return elem_prop.props[4:7]
212 return default
215 def elem_props_get_number(elem, elem_prop_id, default=None):
216 elem_prop = elem_props_find_first(elem, elem_prop_id)
217 if elem_prop is not None:
218 assert(elem_prop.props[0] == elem_prop_id)
219 if elem_prop.props[1] == b'double':
220 assert(elem_prop.props[1] == b'double')
221 assert(elem_prop.props[2] == b'Number')
222 else:
223 assert(elem_prop.props[1] == b'Number')
224 assert(elem_prop.props[2] == b'')
226 # we could allow other number types
227 assert(elem_prop.props_type[4] == data_types.FLOAT64)
229 return elem_prop.props[4]
230 return default
233 def elem_props_get_integer(elem, elem_prop_id, default=None):
234 elem_prop = elem_props_find_first(elem, elem_prop_id)
235 if elem_prop is not None:
236 assert(elem_prop.props[0] == elem_prop_id)
237 if elem_prop.props[1] == b'int':
238 assert(elem_prop.props[1] == b'int')
239 assert(elem_prop.props[2] == b'Integer')
240 elif elem_prop.props[1] == b'ULongLong':
241 assert(elem_prop.props[1] == b'ULongLong')
242 assert(elem_prop.props[2] == b'')
244 # we could allow other number types
245 assert(elem_prop.props_type[4] in {data_types.INT32, data_types.INT64})
247 return elem_prop.props[4]
248 return default
251 def elem_props_get_bool(elem, elem_prop_id, default=None):
252 elem_prop = elem_props_find_first(elem, elem_prop_id)
253 if elem_prop is not None:
254 assert(elem_prop.props[0] == elem_prop_id)
255 # b'Bool' with a capital seems to be used for animated property... go figure...
256 assert(elem_prop.props[1] in {b'bool', b'Bool'})
257 assert(elem_prop.props[2] == b'')
259 # we could allow other number types
260 assert(elem_prop.props_type[4] == data_types.INT32)
261 assert(elem_prop.props[4] in {0, 1})
263 return bool(elem_prop.props[4])
264 return default
267 def elem_props_get_enum(elem, elem_prop_id, default=None):
268 elem_prop = elem_props_find_first(elem, elem_prop_id)
269 if elem_prop is not None:
270 assert(elem_prop.props[0] == elem_prop_id)
271 assert(elem_prop.props[1] == b'enum')
272 assert(elem_prop.props[2] == b'')
273 assert(elem_prop.props[3] == b'')
275 # we could allow other number types
276 assert(elem_prop.props_type[4] == data_types.INT32)
278 return elem_prop.props[4]
279 return default
282 def elem_props_get_visibility(elem, elem_prop_id, default=None):
283 elem_prop = elem_props_find_first(elem, elem_prop_id)
284 if elem_prop is not None:
285 assert(elem_prop.props[0] == elem_prop_id)
286 assert(elem_prop.props[1] == b'Visibility')
287 assert(elem_prop.props[2] == b'')
289 # we could allow other number types
290 assert(elem_prop.props_type[4] == data_types.FLOAT64)
292 return elem_prop.props[4]
293 return default
296 # ----------------------------------------------------------------------------
297 # Blender
299 # ------
300 # Object
301 from collections import namedtuple
304 FBXTransformData = namedtuple("FBXTransformData", (
305 "loc", "geom_loc",
306 "rot", "rot_ofs", "rot_piv", "pre_rot", "pst_rot", "rot_ord", "rot_alt_mat", "geom_rot",
307 "sca", "sca_ofs", "sca_piv", "geom_sca",
311 def blen_read_custom_properties(fbx_obj, blen_obj, settings):
312 # There doesn't seem to be a way to put user properties into templates, so this only get the object properties:
313 fbx_obj_props = elem_find_first(fbx_obj, b'Properties70')
314 if fbx_obj_props:
315 for fbx_prop in fbx_obj_props.elems:
316 assert(fbx_prop.id == b'P')
318 if b'U' in fbx_prop.props[3]:
319 if fbx_prop.props[0] == b'UDP3DSMAX':
320 # Special case for 3DS Max user properties:
321 assert(fbx_prop.props[1] == b'KString')
322 assert(fbx_prop.props_type[4] == data_types.STRING)
323 items = fbx_prop.props[4].decode('utf-8', 'replace')
324 for item in items.split('\r\n'):
325 if item:
326 prop_name, prop_value = item.split('=', 1)
327 prop_name = validate_blend_names(prop_name.strip().encode('utf-8'))
328 blen_obj[prop_name] = prop_value.strip()
329 else:
330 prop_name = validate_blend_names(fbx_prop.props[0])
331 prop_type = fbx_prop.props[1]
332 if prop_type in {b'Vector', b'Vector3D', b'Color', b'ColorRGB'}:
333 assert(fbx_prop.props_type[4:7] == bytes((data_types.FLOAT64,)) * 3)
334 blen_obj[prop_name] = fbx_prop.props[4:7]
335 elif prop_type in {b'Vector4', b'ColorRGBA'}:
336 assert(fbx_prop.props_type[4:8] == bytes((data_types.FLOAT64,)) * 4)
337 blen_obj[prop_name] = fbx_prop.props[4:8]
338 elif prop_type == b'Vector2D':
339 assert(fbx_prop.props_type[4:6] == bytes((data_types.FLOAT64,)) * 2)
340 blen_obj[prop_name] = fbx_prop.props[4:6]
341 elif prop_type in {b'Integer', b'int'}:
342 assert(fbx_prop.props_type[4] == data_types.INT32)
343 blen_obj[prop_name] = fbx_prop.props[4]
344 elif prop_type == b'KString':
345 assert(fbx_prop.props_type[4] == data_types.STRING)
346 blen_obj[prop_name] = fbx_prop.props[4].decode('utf-8', 'replace')
347 elif prop_type in {b'Number', b'double', b'Double'}:
348 assert(fbx_prop.props_type[4] == data_types.FLOAT64)
349 blen_obj[prop_name] = fbx_prop.props[4]
350 elif prop_type in {b'Float', b'float'}:
351 assert(fbx_prop.props_type[4] == data_types.FLOAT32)
352 blen_obj[prop_name] = fbx_prop.props[4]
353 elif prop_type in {b'Bool', b'bool'}:
354 assert(fbx_prop.props_type[4] == data_types.INT32)
355 blen_obj[prop_name] = fbx_prop.props[4] != 0
356 elif prop_type in {b'Enum', b'enum'}:
357 assert(fbx_prop.props_type[4:6] == bytes((data_types.INT32, data_types.STRING)))
358 val = fbx_prop.props[4]
359 if settings.use_custom_props_enum_as_string and fbx_prop.props[5]:
360 enum_items = fbx_prop.props[5].decode('utf-8', 'replace').split('~')
361 assert(val >= 0 and val < len(enum_items))
362 blen_obj[prop_name] = enum_items[val]
363 else:
364 blen_obj[prop_name] = val
365 else:
366 print ("WARNING: User property type '%s' is not supported" % prop_type.decode('utf-8', 'replace'))
369 def blen_read_object_transform_do(transform_data):
370 # This is a nightmare. FBX SDK uses Maya way to compute the transformation matrix of a node - utterly simple:
372 # WorldTransform = ParentWorldTransform @ T @ Roff @ Rp @ Rpre @ R @ Rpost @ Rp-1 @ Soff @ Sp @ S @ Sp-1
374 # Where all those terms are 4 x 4 matrices that contain:
375 # WorldTransform: Transformation matrix of the node in global space.
376 # ParentWorldTransform: Transformation matrix of the parent node in global space.
377 # T: Translation
378 # Roff: Rotation offset
379 # Rp: Rotation pivot
380 # Rpre: Pre-rotation
381 # R: Rotation
382 # Rpost: Post-rotation
383 # Rp-1: Inverse of the rotation pivot
384 # Soff: Scaling offset
385 # Sp: Scaling pivot
386 # S: Scaling
387 # Sp-1: Inverse of the scaling pivot
389 # But it was still too simple, and FBX notion of compatibility is... quite specific. So we also have to
390 # support 3DSMax way:
392 # WorldTransform = ParentWorldTransform @ T @ R @ S @ OT @ OR @ OS
394 # Where all those terms are 4 x 4 matrices that contain:
395 # WorldTransform: Transformation matrix of the node in global space
396 # ParentWorldTransform: Transformation matrix of the parent node in global space
397 # T: Translation
398 # R: Rotation
399 # S: Scaling
400 # OT: Geometric transform translation
401 # OR: Geometric transform rotation
402 # OS: Geometric transform translation
404 # Notes:
405 # Geometric transformations ***are not inherited***: ParentWorldTransform does not contain the OT, OR, OS
406 # of WorldTransform's parent node.
408 # Taken from http://download.autodesk.com/us/fbx/20112/FBX_SDK_HELP/
409 # index.html?url=WS1a9193826455f5ff1f92379812724681e696651.htm,topicNumber=d0e7429
411 # translation
412 lcl_translation = Matrix.Translation(transform_data.loc)
413 geom_loc = Matrix.Translation(transform_data.geom_loc)
415 # rotation
416 to_rot = lambda rot, rot_ord: Euler(convert_deg_to_rad_iter(rot), rot_ord).to_matrix().to_4x4()
417 lcl_rot = to_rot(transform_data.rot, transform_data.rot_ord) @ transform_data.rot_alt_mat
418 pre_rot = to_rot(transform_data.pre_rot, transform_data.rot_ord)
419 pst_rot = to_rot(transform_data.pst_rot, transform_data.rot_ord)
420 geom_rot = to_rot(transform_data.geom_rot, transform_data.rot_ord)
422 rot_ofs = Matrix.Translation(transform_data.rot_ofs)
423 rot_piv = Matrix.Translation(transform_data.rot_piv)
424 sca_ofs = Matrix.Translation(transform_data.sca_ofs)
425 sca_piv = Matrix.Translation(transform_data.sca_piv)
427 # scale
428 lcl_scale = Matrix()
429 lcl_scale[0][0], lcl_scale[1][1], lcl_scale[2][2] = transform_data.sca
430 geom_scale = Matrix();
431 geom_scale[0][0], geom_scale[1][1], geom_scale[2][2] = transform_data.geom_sca
433 base_mat = (
434 lcl_translation @
435 rot_ofs @
436 rot_piv @
437 pre_rot @
438 lcl_rot @
439 pst_rot @
440 rot_piv.inverted_safe() @
441 sca_ofs @
442 sca_piv @
443 lcl_scale @
444 sca_piv.inverted_safe()
446 geom_mat = geom_loc @ geom_rot @ geom_scale
447 # We return mat without 'geometric transforms' too, because it is to be used for children, sigh...
448 return (base_mat @ geom_mat, base_mat, geom_mat)
451 # XXX This might be weak, now that we can add vgroups from both bones and shapes, name collisions become
452 # more likely, will have to make this more robust!!!
453 def add_vgroup_to_objects(vg_indices, vg_weights, vg_name, objects):
454 assert(len(vg_indices) == len(vg_weights))
455 if vg_indices:
456 for obj in objects:
457 # We replace/override here...
458 vg = obj.vertex_groups.get(vg_name)
459 if vg is None:
460 vg = obj.vertex_groups.new(name=vg_name)
461 for i, w in zip(vg_indices, vg_weights):
462 vg.add((i,), w, 'REPLACE')
465 def blen_read_object_transform_preprocess(fbx_props, fbx_obj, rot_alt_mat, use_prepost_rot):
466 # This is quite involved, 'fbxRNode.cpp' from openscenegraph used as a reference
467 const_vector_zero_3d = 0.0, 0.0, 0.0
468 const_vector_one_3d = 1.0, 1.0, 1.0
470 loc = list(elem_props_get_vector_3d(fbx_props, b'Lcl Translation', const_vector_zero_3d))
471 rot = list(elem_props_get_vector_3d(fbx_props, b'Lcl Rotation', const_vector_zero_3d))
472 sca = list(elem_props_get_vector_3d(fbx_props, b'Lcl Scaling', const_vector_one_3d))
474 geom_loc = list(elem_props_get_vector_3d(fbx_props, b'GeometricTranslation', const_vector_zero_3d))
475 geom_rot = list(elem_props_get_vector_3d(fbx_props, b'GeometricRotation', const_vector_zero_3d))
476 geom_sca = list(elem_props_get_vector_3d(fbx_props, b'GeometricScaling', const_vector_one_3d))
478 rot_ofs = elem_props_get_vector_3d(fbx_props, b'RotationOffset', const_vector_zero_3d)
479 rot_piv = elem_props_get_vector_3d(fbx_props, b'RotationPivot', const_vector_zero_3d)
480 sca_ofs = elem_props_get_vector_3d(fbx_props, b'ScalingOffset', const_vector_zero_3d)
481 sca_piv = elem_props_get_vector_3d(fbx_props, b'ScalingPivot', const_vector_zero_3d)
483 is_rot_act = elem_props_get_bool(fbx_props, b'RotationActive', False)
485 if is_rot_act:
486 if use_prepost_rot:
487 pre_rot = elem_props_get_vector_3d(fbx_props, b'PreRotation', const_vector_zero_3d)
488 pst_rot = elem_props_get_vector_3d(fbx_props, b'PostRotation', const_vector_zero_3d)
489 else:
490 pre_rot = const_vector_zero_3d
491 pst_rot = const_vector_zero_3d
492 rot_ord = {
493 0: 'XYZ',
494 1: 'XZY',
495 2: 'YZX',
496 3: 'YXZ',
497 4: 'ZXY',
498 5: 'ZYX',
499 6: 'XYZ', # XXX eSphericXYZ, not really supported...
500 }.get(elem_props_get_enum(fbx_props, b'RotationOrder', 0))
501 else:
502 pre_rot = const_vector_zero_3d
503 pst_rot = const_vector_zero_3d
504 rot_ord = 'XYZ'
506 return FBXTransformData(loc, geom_loc,
507 rot, rot_ofs, rot_piv, pre_rot, pst_rot, rot_ord, rot_alt_mat, geom_rot,
508 sca, sca_ofs, sca_piv, geom_sca)
511 # ---------
512 # Animation
513 def blen_read_animations_curves_iter(fbx_curves, blen_start_offset, fbx_start_offset, fps):
515 Get raw FBX AnimCurve list, and yield values for all curves at each singular curves' keyframes,
516 together with (blender) timing, in frames.
517 blen_start_offset is expected in frames, while fbx_start_offset is expected in FBX ktime.
519 # As a first step, assume linear interpolation between key frames, we'll (try to!) handle more
520 # of FBX curves later.
521 from .fbx_utils import FBX_KTIME
522 timefac = fps / FBX_KTIME
524 curves = tuple([0,
525 elem_prop_first(elem_find_first(c[2], b'KeyTime')),
526 elem_prop_first(elem_find_first(c[2], b'KeyValueFloat')),
528 for c in fbx_curves)
530 allkeys = sorted({item for sublist in curves for item in sublist[1]})
531 for curr_fbxktime in allkeys:
532 curr_values = []
533 for item in curves:
534 idx, times, values, fbx_curve = item
536 if times[idx] < curr_fbxktime:
537 if idx >= 0:
538 idx += 1
539 if idx >= len(times):
540 # We have reached our last element for this curve, stay on it from now on...
541 idx = -1
542 item[0] = idx
544 if times[idx] >= curr_fbxktime:
545 if idx == 0:
546 curr_values.append((values[idx], fbx_curve))
547 else:
548 # Interpolate between this key and the previous one.
549 ifac = (curr_fbxktime - times[idx - 1]) / (times[idx] - times[idx - 1])
550 curr_values.append(((values[idx] - values[idx - 1]) * ifac + values[idx - 1], fbx_curve))
551 curr_blenkframe = (curr_fbxktime - fbx_start_offset) * timefac + blen_start_offset
552 yield (curr_blenkframe, curr_values)
555 def blen_read_animations_action_item(action, item, cnodes, fps, anim_offset):
557 'Bake' loc/rot/scale into the action,
558 taking any pre_ and post_ matrix into account to transform from fbx into blender space.
560 from bpy.types import Object, PoseBone, ShapeKey, Material, Camera
561 from itertools import chain
563 fbx_curves = []
564 for curves, fbxprop in cnodes.values():
565 for (fbx_acdata, _blen_data), channel in curves.values():
566 fbx_curves.append((fbxprop, channel, fbx_acdata))
568 # Leave if no curves are attached (if a blender curve is attached to scale but without keys it defaults to 0).
569 if len(fbx_curves) == 0:
570 return
572 blen_curves = []
573 props = []
575 if isinstance(item, Material):
576 grpname = item.name
577 props = [("diffuse_color", 3, grpname or "Diffuse Color")]
578 elif isinstance(item, ShapeKey):
579 props = [(item.path_from_id("value"), 1, "Key")]
580 elif isinstance(item, Camera):
581 props = [(item.path_from_id("lens"), 1, "Camera")]
582 else: # Object or PoseBone:
583 if item.is_bone:
584 bl_obj = item.bl_obj.pose.bones[item.bl_bone]
585 else:
586 bl_obj = item.bl_obj
588 # We want to create actions for objects, but for bones we 'reuse' armatures' actions!
589 grpname = item.bl_obj.name
591 # Since we might get other channels animated in the end, due to all FBX transform magic,
592 # we need to add curves for whole loc/rot/scale in any case.
593 props = [(bl_obj.path_from_id("location"), 3, grpname or "Location"),
594 None,
595 (bl_obj.path_from_id("scale"), 3, grpname or "Scale")]
596 rot_mode = bl_obj.rotation_mode
597 if rot_mode == 'QUATERNION':
598 props[1] = (bl_obj.path_from_id("rotation_quaternion"), 4, grpname or "Quaternion Rotation")
599 elif rot_mode == 'AXIS_ANGLE':
600 props[1] = (bl_obj.path_from_id("rotation_axis_angle"), 4, grpname or "Axis Angle Rotation")
601 else: # Euler
602 props[1] = (bl_obj.path_from_id("rotation_euler"), 3, grpname or "Euler Rotation")
604 blen_curves = [action.fcurves.new(prop, index=channel, action_group=grpname)
605 for prop, nbr_channels, grpname in props for channel in range(nbr_channels)]
607 if isinstance(item, Material):
608 for frame, values in blen_read_animations_curves_iter(fbx_curves, anim_offset, 0, fps):
609 value = [0,0,0]
610 for v, (fbxprop, channel, _fbx_acdata) in values:
611 assert(fbxprop == b'DiffuseColor')
612 assert(channel in {0, 1, 2})
613 value[channel] = v
615 for fc, v in zip(blen_curves, value):
616 fc.keyframe_points.insert(frame, v, options={'NEEDED', 'FAST'}).interpolation = 'LINEAR'
618 elif isinstance(item, ShapeKey):
619 for frame, values in blen_read_animations_curves_iter(fbx_curves, anim_offset, 0, fps):
620 value = 0.0
621 for v, (fbxprop, channel, _fbx_acdata) in values:
622 assert(fbxprop == b'DeformPercent')
623 assert(channel == 0)
624 value = v / 100.0
626 for fc, v in zip(blen_curves, (value,)):
627 fc.keyframe_points.insert(frame, v, options={'NEEDED', 'FAST'}).interpolation = 'LINEAR'
629 elif isinstance(item, Camera):
630 for frame, values in blen_read_animations_curves_iter(fbx_curves, anim_offset, 0, fps):
631 value = 0.0
632 for v, (fbxprop, channel, _fbx_acdata) in values:
633 assert(fbxprop == b'FocalLength')
634 assert(channel == 0)
635 value = v
637 for fc, v in zip(blen_curves, (value,)):
638 fc.keyframe_points.insert(frame, v, options={'NEEDED', 'FAST'}).interpolation = 'LINEAR'
640 else: # Object or PoseBone:
641 if item.is_bone:
642 bl_obj = item.bl_obj.pose.bones[item.bl_bone]
643 else:
644 bl_obj = item.bl_obj
646 transform_data = item.fbx_transform_data
647 rot_prev = bl_obj.rotation_euler.copy()
649 # Pre-compute inverted local rest matrix of the bone, if relevant.
650 restmat_inv = item.get_bind_matrix().inverted_safe() if item.is_bone else None
652 for frame, values in blen_read_animations_curves_iter(fbx_curves, anim_offset, 0, fps):
653 for v, (fbxprop, channel, _fbx_acdata) in values:
654 if fbxprop == b'Lcl Translation':
655 transform_data.loc[channel] = v
656 elif fbxprop == b'Lcl Rotation':
657 transform_data.rot[channel] = v
658 elif fbxprop == b'Lcl Scaling':
659 transform_data.sca[channel] = v
660 mat, _, _ = blen_read_object_transform_do(transform_data)
662 # compensate for changes in the local matrix during processing
663 if item.anim_compensation_matrix:
664 mat = mat @ item.anim_compensation_matrix
666 # apply pre- and post matrix
667 # post-matrix will contain any correction for lights, camera and bone orientation
668 # pre-matrix will contain any correction for a parent's correction matrix or the global matrix
669 if item.pre_matrix:
670 mat = item.pre_matrix @ mat
671 if item.post_matrix:
672 mat = mat @ item.post_matrix
674 # And now, remove that rest pose matrix from current mat (also in parent space).
675 if restmat_inv:
676 mat = restmat_inv @ mat
678 # Now we have a virtual matrix of transform from AnimCurves, we can insert keyframes!
679 loc, rot, sca = mat.decompose()
680 if rot_mode == 'QUATERNION':
681 pass # nothing to do!
682 elif rot_mode == 'AXIS_ANGLE':
683 vec, ang = rot.to_axis_angle()
684 rot = ang, vec.x, vec.y, vec.z
685 else: # Euler
686 rot = rot.to_euler(rot_mode, rot_prev)
687 rot_prev = rot
688 for fc, value in zip(blen_curves, chain(loc, rot, sca)):
689 fc.keyframe_points.insert(frame, value, options={'NEEDED', 'FAST'}).interpolation = 'LINEAR'
691 # Since we inserted our keyframes in 'FAST' mode, we have to update the fcurves now.
692 for fc in blen_curves:
693 fc.update()
696 def blen_read_animations(fbx_tmpl_astack, fbx_tmpl_alayer, stacks, scene, anim_offset):
698 Recreate an action per stack/layer/object combinations.
699 Only the first found action is linked to objects, more complex setups are not handled,
700 it's up to user to reproduce them!
702 from bpy.types import ShapeKey, Material, Camera
704 actions = {}
705 for as_uuid, ((fbx_asdata, _blen_data), alayers) in stacks.items():
706 stack_name = elem_name_ensure_class(fbx_asdata, b'AnimStack')
707 for al_uuid, ((fbx_aldata, _blen_data), items) in alayers.items():
708 layer_name = elem_name_ensure_class(fbx_aldata, b'AnimLayer')
709 for item, cnodes in items.items():
710 if isinstance(item, Material):
711 id_data = item
712 elif isinstance(item, ShapeKey):
713 id_data = item.id_data
714 elif isinstance(item, Camera):
715 id_data = item
716 else:
717 id_data = item.bl_obj
718 # XXX Ignore rigged mesh animations - those are a nightmare to handle, see note about it in
719 # FbxImportHelperNode class definition.
720 if id_data and id_data.type == 'MESH' and id_data.parent and id_data.parent.type == 'ARMATURE':
721 continue
722 if id_data is None:
723 continue
725 # Create new action if needed (should always be needed!
726 key = (as_uuid, al_uuid, id_data)
727 action = actions.get(key)
728 if action is None:
729 action_name = "|".join((id_data.name, stack_name, layer_name))
730 actions[key] = action = bpy.data.actions.new(action_name)
731 action.use_fake_user = True
732 # If none yet assigned, assign this action to id_data.
733 if not id_data.animation_data:
734 id_data.animation_data_create()
735 if not id_data.animation_data.action:
736 id_data.animation_data.action = action
737 # And actually populate the action!
738 blen_read_animations_action_item(action, item, cnodes, scene.render.fps, anim_offset)
741 # ----
742 # Mesh
744 def blen_read_geom_layerinfo(fbx_layer):
745 return (
746 validate_blend_names(elem_find_first_string_as_bytes(fbx_layer, b'Name')),
747 elem_find_first_string_as_bytes(fbx_layer, b'MappingInformationType'),
748 elem_find_first_string_as_bytes(fbx_layer, b'ReferenceInformationType'),
752 def blen_read_geom_array_setattr(generator, blen_data, blen_attr, fbx_data, stride, item_size, descr, xform):
753 """Generic fbx_layer to blen_data setter, generator is expected to yield tuples (ble_idx, fbx_idx)."""
754 max_idx = len(blen_data) - 1
755 print_error = True
757 def check_skip(blen_idx, fbx_idx):
758 nonlocal print_error
759 if fbx_idx < 0: # Negative values mean 'skip'.
760 return True
761 if blen_idx > max_idx:
762 if print_error:
763 print("ERROR: too much data in this layer, compared to elements in mesh, skipping!")
764 print_error = False
765 return True
766 return False
768 if xform is not None:
769 if isinstance(blen_data, list):
770 if item_size == 1:
771 def _process(blend_data, blen_attr, fbx_data, xform, item_size, blen_idx, fbx_idx):
772 blen_data[blen_idx] = xform(fbx_data[fbx_idx])
773 else:
774 def _process(blend_data, blen_attr, fbx_data, xform, item_size, blen_idx, fbx_idx):
775 blen_data[blen_idx] = xform(fbx_data[fbx_idx:fbx_idx + item_size])
776 else:
777 if item_size == 1:
778 def _process(blend_data, blen_attr, fbx_data, xform, item_size, blen_idx, fbx_idx):
779 setattr(blen_data[blen_idx], blen_attr, xform(fbx_data[fbx_idx]))
780 else:
781 def _process(blend_data, blen_attr, fbx_data, xform, item_size, blen_idx, fbx_idx):
782 setattr(blen_data[blen_idx], blen_attr, xform(fbx_data[fbx_idx:fbx_idx + item_size]))
783 else:
784 if isinstance(blen_data, list):
785 if item_size == 1:
786 def _process(blend_data, blen_attr, fbx_data, xform, item_size, blen_idx, fbx_idx):
787 blen_data[blen_idx] = fbx_data[fbx_idx]
788 else:
789 def _process(blend_data, blen_attr, fbx_data, xform, item_size, blen_idx, fbx_idx):
790 blen_data[blen_idx] = fbx_data[fbx_idx:fbx_idx + item_size]
791 else:
792 if item_size == 1:
793 def _process(blend_data, blen_attr, fbx_data, xform, item_size, blen_idx, fbx_idx):
794 setattr(blen_data[blen_idx], blen_attr, fbx_data[fbx_idx])
795 else:
796 def _process(blend_data, blen_attr, fbx_data, xform, item_size, blen_idx, fbx_idx):
797 setattr(blen_data[blen_idx], blen_attr, fbx_data[fbx_idx:fbx_idx + item_size])
799 for blen_idx, fbx_idx in generator:
800 if check_skip(blen_idx, fbx_idx):
801 continue
802 _process(blen_data, blen_attr, fbx_data, xform, item_size, blen_idx, fbx_idx)
805 # generic generators.
806 def blen_read_geom_array_gen_allsame(data_len):
807 return zip(*(range(data_len), (0,) * data_len))
810 def blen_read_geom_array_gen_direct(fbx_data, stride):
811 fbx_data_len = len(fbx_data)
812 return zip(*(range(fbx_data_len // stride), range(0, fbx_data_len, stride)))
815 def blen_read_geom_array_gen_indextodirect(fbx_layer_index, stride):
816 return ((bi, fi * stride) for bi, fi in enumerate(fbx_layer_index))
819 def blen_read_geom_array_gen_direct_looptovert(mesh, fbx_data, stride):
820 fbx_data_len = len(fbx_data) // stride
821 loops = mesh.loops
822 for p in mesh.polygons:
823 for lidx in p.loop_indices:
824 vidx = loops[lidx].vertex_index
825 if vidx < fbx_data_len:
826 yield lidx, vidx * stride
829 # generic error printers.
830 def blen_read_geom_array_error_mapping(descr, fbx_layer_mapping, quiet=False):
831 if not quiet:
832 print("warning layer %r mapping type unsupported: %r" % (descr, fbx_layer_mapping))
835 def blen_read_geom_array_error_ref(descr, fbx_layer_ref, quiet=False):
836 if not quiet:
837 print("warning layer %r ref type unsupported: %r" % (descr, fbx_layer_ref))
840 def blen_read_geom_array_mapped_vert(
841 mesh, blen_data, blen_attr,
842 fbx_layer_data, fbx_layer_index,
843 fbx_layer_mapping, fbx_layer_ref,
844 stride, item_size, descr,
845 xform=None, quiet=False,
847 if fbx_layer_mapping == b'ByVertice':
848 if fbx_layer_ref == b'Direct':
849 assert(fbx_layer_index is None)
850 blen_read_geom_array_setattr(blen_read_geom_array_gen_direct(fbx_layer_data, stride),
851 blen_data, blen_attr, fbx_layer_data, stride, item_size, descr, xform)
852 return True
853 blen_read_geom_array_error_ref(descr, fbx_layer_ref, quiet)
854 elif fbx_layer_mapping == b'AllSame':
855 if fbx_layer_ref == b'IndexToDirect':
856 assert(fbx_layer_index is None)
857 blen_read_geom_array_setattr(blen_read_geom_array_gen_allsame(len(blen_data)),
858 blen_data, blen_attr, fbx_layer_data, stride, item_size, descr, xform)
859 return True
860 blen_read_geom_array_error_ref(descr, fbx_layer_ref, quiet)
861 else:
862 blen_read_geom_array_error_mapping(descr, fbx_layer_mapping, quiet)
864 return False
867 def blen_read_geom_array_mapped_edge(
868 mesh, blen_data, blen_attr,
869 fbx_layer_data, fbx_layer_index,
870 fbx_layer_mapping, fbx_layer_ref,
871 stride, item_size, descr,
872 xform=None, quiet=False,
874 if fbx_layer_mapping == b'ByEdge':
875 if fbx_layer_ref == b'Direct':
876 blen_read_geom_array_setattr(blen_read_geom_array_gen_direct(fbx_layer_data, stride),
877 blen_data, blen_attr, fbx_layer_data, stride, item_size, descr, xform)
878 return True
879 blen_read_geom_array_error_ref(descr, fbx_layer_ref, quiet)
880 elif fbx_layer_mapping == b'AllSame':
881 if fbx_layer_ref == b'IndexToDirect':
882 assert(fbx_layer_index is None)
883 blen_read_geom_array_setattr(blen_read_geom_array_gen_allsame(len(blen_data)),
884 blen_data, blen_attr, fbx_layer_data, stride, item_size, descr, xform)
885 return True
886 blen_read_geom_array_error_ref(descr, fbx_layer_ref, quiet)
887 else:
888 blen_read_geom_array_error_mapping(descr, fbx_layer_mapping, quiet)
890 return False
893 def blen_read_geom_array_mapped_polygon(
894 mesh, blen_data, blen_attr,
895 fbx_layer_data, fbx_layer_index,
896 fbx_layer_mapping, fbx_layer_ref,
897 stride, item_size, descr,
898 xform=None, quiet=False,
900 if fbx_layer_mapping == b'ByPolygon':
901 if fbx_layer_ref == b'IndexToDirect':
902 # XXX Looks like we often get no fbx_layer_index in this case, shall not happen but happens...
903 # We fallback to 'Direct' mapping in this case.
904 #~ assert(fbx_layer_index is not None)
905 if fbx_layer_index is None:
906 blen_read_geom_array_setattr(blen_read_geom_array_gen_direct(fbx_layer_data, stride),
907 blen_data, blen_attr, fbx_layer_data, stride, item_size, descr, xform)
908 else:
909 blen_read_geom_array_setattr(blen_read_geom_array_gen_indextodirect(fbx_layer_index, stride),
910 blen_data, blen_attr, fbx_layer_data, stride, item_size, descr, xform)
911 return True
912 elif fbx_layer_ref == b'Direct':
913 blen_read_geom_array_setattr(blen_read_geom_array_gen_direct(fbx_layer_data, stride),
914 blen_data, blen_attr, fbx_layer_data, stride, item_size, descr, xform)
915 return True
916 blen_read_geom_array_error_ref(descr, fbx_layer_ref, quiet)
917 elif fbx_layer_mapping == b'AllSame':
918 if fbx_layer_ref == b'IndexToDirect':
919 assert(fbx_layer_index is None)
920 blen_read_geom_array_setattr(blen_read_geom_array_gen_allsame(len(blen_data)),
921 blen_data, blen_attr, fbx_layer_data, stride, item_size, descr, xform)
922 return True
923 blen_read_geom_array_error_ref(descr, fbx_layer_ref, quiet)
924 else:
925 blen_read_geom_array_error_mapping(descr, fbx_layer_mapping, quiet)
927 return False
930 def blen_read_geom_array_mapped_polyloop(
931 mesh, blen_data, blen_attr,
932 fbx_layer_data, fbx_layer_index,
933 fbx_layer_mapping, fbx_layer_ref,
934 stride, item_size, descr,
935 xform=None, quiet=False,
937 if fbx_layer_mapping == b'ByPolygonVertex':
938 if fbx_layer_ref == b'IndexToDirect':
939 # XXX Looks like we often get no fbx_layer_index in this case, shall not happen but happens...
940 # We fallback to 'Direct' mapping in this case.
941 #~ assert(fbx_layer_index is not None)
942 if fbx_layer_index is None:
943 blen_read_geom_array_setattr(blen_read_geom_array_gen_direct(fbx_layer_data, stride),
944 blen_data, blen_attr, fbx_layer_data, stride, item_size, descr, xform)
945 else:
946 blen_read_geom_array_setattr(blen_read_geom_array_gen_indextodirect(fbx_layer_index, stride),
947 blen_data, blen_attr, fbx_layer_data, stride, item_size, descr, xform)
948 return True
949 elif fbx_layer_ref == b'Direct':
950 blen_read_geom_array_setattr(blen_read_geom_array_gen_direct(fbx_layer_data, stride),
951 blen_data, blen_attr, fbx_layer_data, stride, item_size, descr, xform)
952 return True
953 blen_read_geom_array_error_ref(descr, fbx_layer_ref, quiet)
954 elif fbx_layer_mapping == b'ByVertice':
955 if fbx_layer_ref == b'Direct':
956 assert(fbx_layer_index is None)
957 blen_read_geom_array_setattr(blen_read_geom_array_gen_direct_looptovert(mesh, fbx_layer_data, stride),
958 blen_data, blen_attr, fbx_layer_data, stride, item_size, descr, xform)
959 return True
960 blen_read_geom_array_error_ref(descr, fbx_layer_ref, quiet)
961 elif fbx_layer_mapping == b'AllSame':
962 if fbx_layer_ref == b'IndexToDirect':
963 assert(fbx_layer_index is None)
964 blen_read_geom_array_setattr(blen_read_geom_array_gen_allsame(len(blen_data)),
965 blen_data, blen_attr, fbx_layer_data, stride, item_size, descr, xform)
966 return True
967 blen_read_geom_array_error_ref(descr, fbx_layer_ref, quiet)
968 else:
969 blen_read_geom_array_error_mapping(descr, fbx_layer_mapping, quiet)
971 return False
974 def blen_read_geom_layer_material(fbx_obj, mesh):
975 fbx_layer = elem_find_first(fbx_obj, b'LayerElementMaterial')
977 if fbx_layer is None:
978 return
980 (fbx_layer_name,
981 fbx_layer_mapping,
982 fbx_layer_ref,
983 ) = blen_read_geom_layerinfo(fbx_layer)
985 layer_id = b'Materials'
986 fbx_layer_data = elem_prop_first(elem_find_first(fbx_layer, layer_id))
988 blen_data = mesh.polygons
989 blen_read_geom_array_mapped_polygon(
990 mesh, blen_data, "material_index",
991 fbx_layer_data, None,
992 fbx_layer_mapping, fbx_layer_ref,
993 1, 1, layer_id,
997 def blen_read_geom_layer_uv(fbx_obj, mesh):
998 for layer_id in (b'LayerElementUV',):
999 for fbx_layer in elem_find_iter(fbx_obj, layer_id):
1000 # all should be valid
1001 (fbx_layer_name,
1002 fbx_layer_mapping,
1003 fbx_layer_ref,
1004 ) = blen_read_geom_layerinfo(fbx_layer)
1006 fbx_layer_data = elem_prop_first(elem_find_first(fbx_layer, b'UV'))
1007 fbx_layer_index = elem_prop_first(elem_find_first(fbx_layer, b'UVIndex'))
1009 # Always init our new layers with (0, 0) UVs.
1010 uv_lay = mesh.uv_layers.new(name=fbx_layer_name, do_init=False)
1011 if uv_lay is None:
1012 print("Failed to add {%r %r} UVLayer to %r (probably too many of them?)"
1013 "" % (layer_id, fbx_layer_name, mesh.name))
1014 continue
1016 blen_data = uv_lay.data
1018 # some valid files omit this data
1019 if fbx_layer_data is None:
1020 print("%r %r missing data" % (layer_id, fbx_layer_name))
1021 continue
1023 blen_read_geom_array_mapped_polyloop(
1024 mesh, blen_data, "uv",
1025 fbx_layer_data, fbx_layer_index,
1026 fbx_layer_mapping, fbx_layer_ref,
1027 2, 2, layer_id,
1031 def blen_read_geom_layer_color(fbx_obj, mesh):
1032 # almost same as UV's
1033 for layer_id in (b'LayerElementColor',):
1034 for fbx_layer in elem_find_iter(fbx_obj, layer_id):
1035 # all should be valid
1036 (fbx_layer_name,
1037 fbx_layer_mapping,
1038 fbx_layer_ref,
1039 ) = blen_read_geom_layerinfo(fbx_layer)
1041 fbx_layer_data = elem_prop_first(elem_find_first(fbx_layer, b'Colors'))
1042 fbx_layer_index = elem_prop_first(elem_find_first(fbx_layer, b'ColorIndex'))
1044 # Always init our new layers with full white opaque color.
1045 color_lay = mesh.vertex_colors.new(name=fbx_layer_name, do_init=False)
1046 blen_data = color_lay.data
1048 # some valid files omit this data
1049 if fbx_layer_data is None:
1050 print("%r %r missing data" % (layer_id, fbx_layer_name))
1051 continue
1053 blen_read_geom_array_mapped_polyloop(
1054 mesh, blen_data, "color",
1055 fbx_layer_data, fbx_layer_index,
1056 fbx_layer_mapping, fbx_layer_ref,
1057 4, 4, layer_id,
1061 def blen_read_geom_layer_smooth(fbx_obj, mesh):
1062 fbx_layer = elem_find_first(fbx_obj, b'LayerElementSmoothing')
1064 if fbx_layer is None:
1065 return False
1067 # all should be valid
1068 (fbx_layer_name,
1069 fbx_layer_mapping,
1070 fbx_layer_ref,
1071 ) = blen_read_geom_layerinfo(fbx_layer)
1073 layer_id = b'Smoothing'
1074 fbx_layer_data = elem_prop_first(elem_find_first(fbx_layer, layer_id))
1076 # udk has 'Direct' mapped, with no Smoothing, not sure why, but ignore these
1077 if fbx_layer_data is None:
1078 return False
1080 if fbx_layer_mapping == b'ByEdge':
1081 # some models have bad edge data, we cant use this info...
1082 if not mesh.edges:
1083 print("warning skipping sharp edges data, no valid edges...")
1084 return False
1086 blen_data = mesh.edges
1087 blen_read_geom_array_mapped_edge(
1088 mesh, blen_data, "use_edge_sharp",
1089 fbx_layer_data, None,
1090 fbx_layer_mapping, fbx_layer_ref,
1091 1, 1, layer_id,
1092 xform=lambda s: not s,
1094 # We only set sharp edges here, not face smoothing itself...
1095 mesh.use_auto_smooth = True
1096 return False
1097 elif fbx_layer_mapping == b'ByPolygon':
1098 blen_data = mesh.polygons
1099 return blen_read_geom_array_mapped_polygon(
1100 mesh, blen_data, "use_smooth",
1101 fbx_layer_data, None,
1102 fbx_layer_mapping, fbx_layer_ref,
1103 1, 1, layer_id,
1104 xform=lambda s: (s != 0), # smoothgroup bitflags, treat as booleans for now
1106 else:
1107 print("warning layer %r mapping type unsupported: %r" % (fbx_layer.id, fbx_layer_mapping))
1108 return False
1111 def blen_read_geom_layer_normal(fbx_obj, mesh, xform=None):
1112 fbx_layer = elem_find_first(fbx_obj, b'LayerElementNormal')
1114 if fbx_layer is None:
1115 return False
1117 (fbx_layer_name,
1118 fbx_layer_mapping,
1119 fbx_layer_ref,
1120 ) = blen_read_geom_layerinfo(fbx_layer)
1122 layer_id = b'Normals'
1123 fbx_layer_data = elem_prop_first(elem_find_first(fbx_layer, layer_id))
1124 fbx_layer_index = elem_prop_first(elem_find_first(fbx_layer, b'NormalsIndex'))
1126 # try loops, then vertices.
1127 tries = ((mesh.loops, "Loops", False, blen_read_geom_array_mapped_polyloop),
1128 (mesh.polygons, "Polygons", True, blen_read_geom_array_mapped_polygon),
1129 (mesh.vertices, "Vertices", True, blen_read_geom_array_mapped_vert))
1130 for blen_data, blen_data_type, is_fake, func in tries:
1131 bdata = [None] * len(blen_data) if is_fake else blen_data
1132 if func(mesh, bdata, "normal",
1133 fbx_layer_data, fbx_layer_index, fbx_layer_mapping, fbx_layer_ref, 3, 3, layer_id, xform, True):
1134 if blen_data_type == "Polygons":
1135 for pidx, p in enumerate(mesh.polygons):
1136 for lidx in range(p.loop_start, p.loop_start + p.loop_total):
1137 mesh.loops[lidx].normal[:] = bdata[pidx]
1138 elif blen_data_type is "Vertices":
1139 # We have to copy vnors to lnors! Far from elegant, but simple.
1140 for l in mesh.loops:
1141 l.normal[:] = bdata[l.vertex_index]
1142 return True
1144 blen_read_geom_array_error_mapping("normal", fbx_layer_mapping)
1145 blen_read_geom_array_error_ref("normal", fbx_layer_ref)
1146 return False
1149 def blen_read_geom(fbx_tmpl, fbx_obj, settings):
1150 from itertools import chain
1151 import array
1153 # Vertices are in object space, but we are post-multiplying all transforms with the inverse of the
1154 # global matrix, so we need to apply the global matrix to the vertices to get the correct result.
1155 geom_mat_co = settings.global_matrix if settings.bake_space_transform else None
1156 # We need to apply the inverse transpose of the global matrix when transforming normals.
1157 geom_mat_no = Matrix(settings.global_matrix_inv_transposed) if settings.bake_space_transform else None
1158 if geom_mat_no is not None:
1159 # Remove translation & scaling!
1160 geom_mat_no.translation = Vector()
1161 geom_mat_no.normalize()
1163 # TODO, use 'fbx_tmpl'
1164 elem_name_utf8 = elem_name_ensure_class(fbx_obj, b'Geometry')
1166 fbx_verts = elem_prop_first(elem_find_first(fbx_obj, b'Vertices'))
1167 fbx_polys = elem_prop_first(elem_find_first(fbx_obj, b'PolygonVertexIndex'))
1168 fbx_edges = elem_prop_first(elem_find_first(fbx_obj, b'Edges'))
1170 if geom_mat_co is not None:
1171 def _vcos_transformed_gen(raw_cos, m=None):
1172 # Note: we could most likely get much better performances with numpy, but will leave this as TODO for now.
1173 return chain(*(m @ Vector(v) for v in zip(*(iter(raw_cos),) * 3)))
1174 fbx_verts = array.array(fbx_verts.typecode, _vcos_transformed_gen(fbx_verts, geom_mat_co))
1176 if fbx_verts is None:
1177 fbx_verts = ()
1178 if fbx_polys is None:
1179 fbx_polys = ()
1181 mesh = bpy.data.meshes.new(name=elem_name_utf8)
1182 mesh.vertices.add(len(fbx_verts) // 3)
1183 mesh.vertices.foreach_set("co", fbx_verts)
1185 if fbx_polys:
1186 mesh.loops.add(len(fbx_polys))
1187 poly_loop_starts = []
1188 poly_loop_totals = []
1189 poly_loop_prev = 0
1190 for i, l in enumerate(mesh.loops):
1191 index = fbx_polys[i]
1192 if index < 0:
1193 poly_loop_starts.append(poly_loop_prev)
1194 poly_loop_totals.append((i - poly_loop_prev) + 1)
1195 poly_loop_prev = i + 1
1196 index ^= -1
1197 l.vertex_index = index
1199 mesh.polygons.add(len(poly_loop_starts))
1200 mesh.polygons.foreach_set("loop_start", poly_loop_starts)
1201 mesh.polygons.foreach_set("loop_total", poly_loop_totals)
1203 blen_read_geom_layer_material(fbx_obj, mesh)
1204 blen_read_geom_layer_uv(fbx_obj, mesh)
1205 blen_read_geom_layer_color(fbx_obj, mesh)
1207 if fbx_edges:
1208 # edges in fact index the polygons (NOT the vertices)
1209 import array
1210 tot_edges = len(fbx_edges)
1211 edges_conv = array.array('i', [0]) * (tot_edges * 2)
1213 edge_index = 0
1214 for i in fbx_edges:
1215 e_a = fbx_polys[i]
1216 if e_a >= 0:
1217 e_b = fbx_polys[i + 1]
1218 if e_b < 0:
1219 e_b ^= -1
1220 else:
1221 # Last index of polygon, wrap back to the start.
1223 # ideally we wouldn't have to search back,
1224 # but it should only be 2-3 iterations.
1225 j = i - 1
1226 while j >= 0 and fbx_polys[j] >= 0:
1227 j -= 1
1228 e_a ^= -1
1229 e_b = fbx_polys[j + 1]
1231 edges_conv[edge_index] = e_a
1232 edges_conv[edge_index + 1] = e_b
1233 edge_index += 2
1235 mesh.edges.add(tot_edges)
1236 mesh.edges.foreach_set("vertices", edges_conv)
1238 # must be after edge, face loading.
1239 ok_smooth = blen_read_geom_layer_smooth(fbx_obj, mesh)
1241 ok_normals = False
1242 if settings.use_custom_normals:
1243 # Note: we store 'temp' normals in loops, since validate() may alter final mesh,
1244 # we can only set custom lnors *after* calling it.
1245 mesh.create_normals_split()
1246 if geom_mat_no is None:
1247 ok_normals = blen_read_geom_layer_normal(fbx_obj, mesh)
1248 else:
1249 def nortrans(v):
1250 return geom_mat_no @ Vector(v)
1251 ok_normals = blen_read_geom_layer_normal(fbx_obj, mesh, nortrans)
1253 mesh.validate(clean_customdata=False) # *Very* important to not remove lnors here!
1255 if ok_normals:
1256 clnors = array.array('f', [0.0] * (len(mesh.loops) * 3))
1257 mesh.loops.foreach_get("normal", clnors)
1259 if not ok_smooth:
1260 mesh.polygons.foreach_set("use_smooth", [True] * len(mesh.polygons))
1261 ok_smooth = True
1263 mesh.normals_split_custom_set(tuple(zip(*(iter(clnors),) * 3)))
1264 mesh.use_auto_smooth = True
1265 else:
1266 mesh.calc_normals()
1268 if settings.use_custom_normals:
1269 mesh.free_normals_split()
1271 if not ok_smooth:
1272 mesh.polygons.foreach_set("use_smooth", [True] * len(mesh.polygons))
1274 if settings.use_custom_props:
1275 blen_read_custom_properties(fbx_obj, mesh, settings)
1277 return mesh
1280 def blen_read_shape(fbx_tmpl, fbx_sdata, fbx_bcdata, meshes, scene):
1281 elem_name_utf8 = elem_name_ensure_class(fbx_sdata, b'Geometry')
1282 indices = elem_prop_first(elem_find_first(fbx_sdata, b'Indexes'), default=())
1283 dvcos = tuple(co for co in zip(*[iter(elem_prop_first(elem_find_first(fbx_sdata, b'Vertices'), default=()))] * 3))
1284 # We completely ignore normals here!
1285 weight = elem_prop_first(elem_find_first(fbx_bcdata, b'DeformPercent'), default=100.0) / 100.0
1286 vgweights = tuple(vgw / 100.0 for vgw in elem_prop_first(elem_find_first(fbx_bcdata, b'FullWeights'), default=()))
1288 # Special case, in case all weights are the same, FullWeight can have only one element - *sigh!*
1289 nbr_indices = len(indices)
1290 if len(vgweights) == 1 and nbr_indices > 1:
1291 vgweights = (vgweights[0],) * nbr_indices
1293 assert(len(vgweights) == nbr_indices == len(dvcos))
1294 create_vg = bool(set(vgweights) - {1.0})
1296 keyblocks = []
1298 for me, objects in meshes:
1299 vcos = tuple((idx, me.vertices[idx].co + Vector(dvco)) for idx, dvco in zip(indices, dvcos))
1300 objects = list({node.bl_obj for node in objects})
1301 assert(objects)
1303 if me.shape_keys is None:
1304 objects[0].shape_key_add(name="Basis", from_mix=False)
1305 kb = objects[0].shape_key_add(name=elem_name_utf8, from_mix=False)
1306 me.shape_keys.use_relative = True # Should already be set as such.
1308 for idx, co in vcos:
1309 kb.data[idx].co[:] = co
1310 kb.value = weight
1312 # Add vgroup if necessary.
1313 if create_vg:
1314 vgoups = add_vgroup_to_objects(indices, vgweights, kb.name, objects)
1315 kb.vertex_group = kb.name
1317 keyblocks.append(kb)
1319 return keyblocks
1322 # --------
1323 # Material
1325 def blen_read_material(fbx_tmpl, fbx_obj, settings):
1326 from bpy_extras import node_shader_utils
1327 from math import sqrt
1329 elem_name_utf8 = elem_name_ensure_class(fbx_obj, b'Material')
1331 nodal_material_wrap_map = settings.nodal_material_wrap_map
1332 ma = bpy.data.materials.new(name=elem_name_utf8)
1334 const_color_white = 1.0, 1.0, 1.0
1336 fbx_props = (elem_find_first(fbx_obj, b'Properties70'),
1337 elem_find_first(fbx_tmpl, b'Properties70', fbx_elem_nil))
1339 ma_wrap = node_shader_utils.PrincipledBSDFWrapper(ma, is_readonly=False, use_nodes=True)
1340 ma_wrap.base_color = elem_props_get_color_rgb(fbx_props, b'DiffuseColor', const_color_white)
1341 # No specular color in Principled BSDF shader, assumed to be either white or take some tint from diffuse one...
1342 # TODO: add way to handle tint option (guesstimate from spec color + intensity...)?
1343 ma_wrap.specular = elem_props_get_number(fbx_props, b'SpecularFactor', 0.25) * 2.0
1344 # XXX Totally empirical conversion, trying to adapt it
1345 # (from 1.0 - 0.0 Principled BSDF range to 0.0 - 100.0 FBX shininess range)...
1346 fbx_shininess = elem_props_get_number(fbx_props, b'Shininess', 20.0)
1347 ma_wrap.roughness = 1.0 - (sqrt(fbx_shininess) / 10.0)
1348 ma_wrap.transmission = 1.0 - elem_props_get_number(fbx_props, b'Opacity', 1.0)
1349 ma_wrap.metallic = elem_props_get_number(fbx_props, b'ReflectionFactor', 0.0)
1350 # We have no metallic (a.k.a. reflection) color...
1351 # elem_props_get_color_rgb(fbx_props, b'ReflectionColor', const_color_white)
1352 # (x / 7.142) is only a guess, cycles usable range is (0.0 -> 0.5)
1353 ma_wrap.normalmap_strength = elem_props_get_number(fbx_props, b'BumpFactor', 2.5) / 7.142
1355 nodal_material_wrap_map[ma] = ma_wrap
1357 if settings.use_custom_props:
1358 blen_read_custom_properties(fbx_obj, ma, settings)
1360 return ma
1363 # -------
1364 # Image & Texture
1366 def blen_read_texture_image(fbx_tmpl, fbx_obj, basedir, settings):
1367 import os
1368 from bpy_extras import image_utils
1370 def pack_data_from_content(image, fbx_obj):
1371 data = elem_find_first_bytes(fbx_obj, b'Content')
1372 if (data):
1373 data_len = len(data)
1374 if (data_len):
1375 image.pack(data=data, data_len=data_len)
1377 elem_name_utf8 = elem_name_ensure_classes(fbx_obj, {b'Texture', b'Video'})
1379 image_cache = settings.image_cache
1381 # Yet another beautiful logic demonstration by Master FBX:
1382 # * RelativeFilename in both Video and Texture nodes.
1383 # * FileName in texture nodes.
1384 # * Filename in video nodes.
1385 # Aaaaaaaarrrrrrrrgggggggggggg!!!!!!!!!!!!!!
1386 filepath = elem_find_first_string(fbx_obj, b'RelativeFilename')
1387 if filepath:
1388 filepath = os.path.join(basedir, filepath)
1389 else:
1390 filepath = elem_find_first_string(fbx_obj, b'FileName')
1391 if not filepath:
1392 filepath = elem_find_first_string(fbx_obj, b'Filename')
1393 if not filepath:
1394 print("Error, could not find any file path in ", fbx_obj)
1395 print(" Falling back to: ", elem_name_utf8)
1396 filepath = elem_name_utf8
1397 else :
1398 filepath = filepath.replace('\\', '/') if (os.sep == '/') else filepath.replace('/', '\\')
1400 image = image_cache.get(filepath)
1401 if image is not None:
1402 # Data is only embedded once, we may have already created the image but still be missing its data!
1403 if not image.has_data:
1404 pack_data_from_content(image, fbx_obj)
1405 return image
1407 image = image_utils.load_image(
1408 filepath,
1409 dirname=basedir,
1410 place_holder=True,
1411 recursive=settings.use_image_search,
1414 # Try to use embedded data, if available!
1415 pack_data_from_content(image, fbx_obj)
1417 image_cache[filepath] = image
1418 # name can be ../a/b/c
1419 image.name = os.path.basename(elem_name_utf8)
1421 if settings.use_custom_props:
1422 blen_read_custom_properties(fbx_obj, image, settings)
1424 return image
1427 def blen_read_camera(fbx_tmpl, fbx_obj, global_scale):
1428 # meters to inches
1429 M2I = 0.0393700787
1431 elem_name_utf8 = elem_name_ensure_class(fbx_obj, b'NodeAttribute')
1433 fbx_props = (elem_find_first(fbx_obj, b'Properties70'),
1434 elem_find_first(fbx_tmpl, b'Properties70', fbx_elem_nil))
1436 camera = bpy.data.cameras.new(name=elem_name_utf8)
1438 camera.type = 'ORTHO' if elem_props_get_enum(fbx_props, b'CameraProjectionType', 0) == 1 else 'PERSP'
1440 camera.lens = elem_props_get_number(fbx_props, b'FocalLength', 35.0)
1441 camera.sensor_width = elem_props_get_number(fbx_props, b'FilmWidth', 32.0 * M2I) / M2I
1442 camera.sensor_height = elem_props_get_number(fbx_props, b'FilmHeight', 32.0 * M2I) / M2I
1444 camera.ortho_scale = elem_props_get_number(fbx_props, b'OrthoZoom', 1.0)
1446 filmaspect = camera.sensor_width / camera.sensor_height
1447 # film offset
1448 camera.shift_x = elem_props_get_number(fbx_props, b'FilmOffsetX', 0.0) / (M2I * camera.sensor_width)
1449 camera.shift_y = elem_props_get_number(fbx_props, b'FilmOffsetY', 0.0) / (M2I * camera.sensor_height * filmaspect)
1451 camera.clip_start = elem_props_get_number(fbx_props, b'NearPlane', 0.01) * global_scale
1452 camera.clip_end = elem_props_get_number(fbx_props, b'FarPlane', 100.0) * global_scale
1454 return camera
1457 def blen_read_light(fbx_tmpl, fbx_obj, global_scale):
1458 import math
1459 elem_name_utf8 = elem_name_ensure_class(fbx_obj, b'NodeAttribute')
1461 fbx_props = (elem_find_first(fbx_obj, b'Properties70'),
1462 elem_find_first(fbx_tmpl, b'Properties70', fbx_elem_nil))
1464 light_type = {
1465 0: 'POINT',
1466 1: 'SUN',
1467 2: 'SPOT'}.get(elem_props_get_enum(fbx_props, b'LightType', 0), 'POINT')
1469 lamp = bpy.data.lights.new(name=elem_name_utf8, type=light_type)
1471 if light_type == 'SPOT':
1472 spot_size = elem_props_get_number(fbx_props, b'OuterAngle', None)
1473 if spot_size is None:
1474 # Deprecated.
1475 spot_size = elem_props_get_number(fbx_props, b'Cone angle', 45.0)
1476 lamp.spot_size = math.radians(spot_size)
1478 spot_blend = elem_props_get_number(fbx_props, b'InnerAngle', None)
1479 if spot_blend is None:
1480 # Deprecated.
1481 spot_blend = elem_props_get_number(fbx_props, b'HotSpot', 45.0)
1482 lamp.spot_blend = 1.0 - (spot_blend / spot_size)
1484 # TODO, cycles nodes???
1485 lamp.color = elem_props_get_color_rgb(fbx_props, b'Color', (1.0, 1.0, 1.0))
1486 lamp.energy = elem_props_get_number(fbx_props, b'Intensity', 100.0) / 100.0
1487 lamp.distance = elem_props_get_number(fbx_props, b'DecayStart', 25.0) * global_scale
1488 lamp.use_shadow = elem_props_get_bool(fbx_props, b'CastShadow', True)
1489 if hasattr(lamp, "cycles"):
1490 lamp.cycles.cast_shadow = lamp.use_shadow
1491 # Keeping this for now, but this is not used nor exposed anymore afaik...
1492 lamp.shadow_color = elem_props_get_color_rgb(fbx_props, b'ShadowColor', (0.0, 0.0, 0.0))
1494 return lamp
1497 # ### Import Utility class
1498 class FbxImportHelperNode:
1500 Temporary helper node to store a hierarchy of fbxNode objects before building Objects, Armatures and Bones.
1501 It tries to keep the correction data in one place so it can be applied consistently to the imported data.
1504 __slots__ = (
1505 '_parent', 'anim_compensation_matrix', 'is_global_animation', 'armature_setup', 'armature', 'bind_matrix',
1506 'bl_bone', 'bl_data', 'bl_obj', 'bone_child_matrix', 'children', 'clusters',
1507 'fbx_elem', 'fbx_name', 'fbx_transform_data', 'fbx_type',
1508 'is_armature', 'has_bone_children', 'is_bone', 'is_root', 'is_leaf',
1509 'matrix', 'matrix_as_parent', 'matrix_geom', 'meshes', 'post_matrix', 'pre_matrix')
1511 def __init__(self, fbx_elem, bl_data, fbx_transform_data, is_bone):
1512 self.fbx_name = elem_name_ensure_class(fbx_elem, b'Model') if fbx_elem else 'Unknown'
1513 self.fbx_type = fbx_elem.props[2] if fbx_elem else None
1514 self.fbx_elem = fbx_elem
1515 self.bl_obj = None
1516 self.bl_data = bl_data
1517 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!)
1518 self.fbx_transform_data = fbx_transform_data
1519 self.is_root = False
1520 self.is_bone = is_bone
1521 self.is_armature = False
1522 self.armature = None # For bones only, relevant armature node.
1523 self.has_bone_children = False # True if the hierarchy below this node contains bones, important to support mixed hierarchies.
1524 self.is_leaf = False # True for leaf-bones added to the end of some bone chains to set the lengths.
1525 self.pre_matrix = None # correction matrix that needs to be applied before the FBX transform
1526 self.bind_matrix = None # for bones this is the matrix used to bind to the skin
1527 if fbx_transform_data:
1528 self.matrix, self.matrix_as_parent, self.matrix_geom = blen_read_object_transform_do(fbx_transform_data)
1529 else:
1530 self.matrix, self.matrix_as_parent, self.matrix_geom = (None, None, None)
1531 self.post_matrix = None # correction matrix that needs to be applied after the FBX transform
1532 self.bone_child_matrix = None # Objects attached to a bone end not the beginning, this matrix corrects for that
1534 # XXX Those two are to handle the fact that rigged meshes are not linked to their armature in FBX, which implies
1535 # that their animation is in global space (afaik...).
1536 # This is actually not really solvable currently, since anim_compensation_matrix is not valid if armature
1537 # itself is animated (we'd have to recompute global-to-local anim_compensation_matrix for each frame,
1538 # and for each armature action... beyond being an insane work).
1539 # Solution for now: do not read rigged meshes animations at all! sic...
1540 self.anim_compensation_matrix = None # a mesh moved in the hierarchy may have a different local matrix. This compensates animations for this.
1541 self.is_global_animation = False
1543 self.meshes = None # List of meshes influenced by this bone.
1544 self.clusters = [] # Deformer Cluster nodes
1545 self.armature_setup = {} # mesh and armature matrix when the mesh was bound
1547 self._parent = None
1548 self.children = []
1550 @property
1551 def parent(self):
1552 return self._parent
1554 @parent.setter
1555 def parent(self, value):
1556 if self._parent is not None:
1557 self._parent.children.remove(self)
1558 self._parent = value
1559 if self._parent is not None:
1560 self._parent.children.append(self)
1562 @property
1563 def ignore(self):
1564 # Separating leaf status from ignore status itself.
1565 # Currently they are equivalent, but this may change in future.
1566 return self.is_leaf
1568 def __repr__(self):
1569 if self.fbx_elem:
1570 return self.fbx_elem.props[1].decode()
1571 else:
1572 return "None"
1574 def print_info(self, indent=0):
1575 print(" " * indent + (self.fbx_name if self.fbx_name else "(Null)")
1576 + ("[root]" if self.is_root else "")
1577 + ("[leaf]" if self.is_leaf else "")
1578 + ("[ignore]" if self.ignore else "")
1579 + ("[armature]" if self.is_armature else "")
1580 + ("[bone]" if self.is_bone else "")
1581 + ("[HBC]" if self.has_bone_children else "")
1583 for c in self.children:
1584 c.print_info(indent + 1)
1586 def mark_leaf_bones(self):
1587 if self.is_bone and len(self.children) == 1:
1588 child = self.children[0]
1589 if child.is_bone and len(child.children) == 0:
1590 child.is_leaf = True
1591 for child in self.children:
1592 child.mark_leaf_bones()
1594 def do_bake_transform(self, settings):
1595 return (settings.bake_space_transform and self.fbx_type in (b'Mesh', b'Null') and
1596 not self.is_armature and not self.is_bone)
1598 def find_correction_matrix(self, settings, parent_correction_inv=None):
1599 from bpy_extras.io_utils import axis_conversion
1601 if self.parent and (self.parent.is_root or self.parent.do_bake_transform(settings)):
1602 self.pre_matrix = settings.global_matrix
1604 if parent_correction_inv:
1605 self.pre_matrix = parent_correction_inv @ (self.pre_matrix if self.pre_matrix else Matrix())
1607 correction_matrix = None
1609 if self.is_bone:
1610 if settings.automatic_bone_orientation:
1611 # find best orientation to align bone with
1612 bone_children = tuple(child for child in self.children if child.is_bone)
1613 if len(bone_children) == 0:
1614 # no children, inherit the correction from parent (if possible)
1615 if self.parent and self.parent.is_bone:
1616 correction_matrix = parent_correction_inv.inverted() if parent_correction_inv else None
1617 else:
1618 # else find how best to rotate the bone to align the Y axis with the children
1619 best_axis = (1, 0, 0)
1620 if len(bone_children) == 1:
1621 vec = bone_children[0].get_bind_matrix().to_translation()
1622 best_axis = Vector((0, 0, 1 if vec[2] >= 0 else -1))
1623 if abs(vec[0]) > abs(vec[1]):
1624 if abs(vec[0]) > abs(vec[2]):
1625 best_axis = Vector((1 if vec[0] >= 0 else -1, 0, 0))
1626 elif abs(vec[1]) > abs(vec[2]):
1627 best_axis = Vector((0, 1 if vec[1] >= 0 else -1, 0))
1628 else:
1629 # get the child directions once because they may be checked several times
1630 child_locs = (child.get_bind_matrix().to_translation() for child in bone_children)
1631 child_locs = tuple(loc.normalized() for loc in child_locs if loc.magnitude > 0.0)
1633 # I'm not sure which one I like better...
1634 if False:
1635 best_angle = -1.0
1636 for i in range(6):
1637 a = i // 2
1638 s = -1 if i % 2 == 1 else 1
1639 test_axis = Vector((s if a == 0 else 0, s if a == 1 else 0, s if a == 2 else 0))
1641 # find max angle to children
1642 max_angle = 1.0
1643 for loc in child_locs:
1644 max_angle = min(max_angle, test_axis.dot(loc))
1646 # is it better than the last one?
1647 if best_angle < max_angle:
1648 best_angle = max_angle
1649 best_axis = test_axis
1650 else:
1651 best_angle = -1.0
1652 for vec in child_locs:
1653 test_axis = Vector((0, 0, 1 if vec[2] >= 0 else -1))
1654 if abs(vec[0]) > abs(vec[1]):
1655 if abs(vec[0]) > abs(vec[2]):
1656 test_axis = Vector((1 if vec[0] >= 0 else -1, 0, 0))
1657 elif abs(vec[1]) > abs(vec[2]):
1658 test_axis = Vector((0, 1 if vec[1] >= 0 else -1, 0))
1660 # find max angle to children
1661 max_angle = 1.0
1662 for loc in child_locs:
1663 max_angle = min(max_angle, test_axis.dot(loc))
1665 # is it better than the last one?
1666 if best_angle < max_angle:
1667 best_angle = max_angle
1668 best_axis = test_axis
1670 # convert best_axis to axis string
1671 to_up = 'Z' if best_axis[2] >= 0 else '-Z'
1672 if abs(best_axis[0]) > abs(best_axis[1]):
1673 if abs(best_axis[0]) > abs(best_axis[2]):
1674 to_up = 'X' if best_axis[0] >= 0 else '-X'
1675 elif abs(best_axis[1]) > abs(best_axis[2]):
1676 to_up = 'Y' if best_axis[1] >= 0 else '-Y'
1677 to_forward = 'X' if to_up not in {'X', '-X'} else 'Y'
1679 # Build correction matrix
1680 if (to_up, to_forward) != ('Y', 'X'):
1681 correction_matrix = axis_conversion(from_forward='X',
1682 from_up='Y',
1683 to_forward=to_forward,
1684 to_up=to_up,
1685 ).to_4x4()
1686 else:
1687 correction_matrix = settings.bone_correction_matrix
1688 else:
1689 # camera and light can be hard wired
1690 if self.fbx_type == b'Camera':
1691 correction_matrix = MAT_CONVERT_CAMERA
1692 elif self.fbx_type == b'Light':
1693 correction_matrix = MAT_CONVERT_LIGHT
1695 self.post_matrix = correction_matrix
1697 if self.do_bake_transform(settings):
1698 self.post_matrix = settings.global_matrix_inv @ (self.post_matrix if self.post_matrix else Matrix())
1700 # process children
1701 correction_matrix_inv = correction_matrix.inverted_safe() if correction_matrix else None
1702 for child in self.children:
1703 child.find_correction_matrix(settings, correction_matrix_inv)
1705 def find_armature_bones(self, armature):
1706 for child in self.children:
1707 if child.is_bone:
1708 child.armature = armature
1709 child.find_armature_bones(armature)
1711 def find_armatures(self):
1712 needs_armature = False
1713 for child in self.children:
1714 if child.is_bone:
1715 needs_armature = True
1716 break
1717 if needs_armature:
1718 if self.fbx_type in {b'Null', b'Root'}:
1719 # if empty then convert into armature
1720 self.is_armature = True
1721 armature = self
1722 else:
1723 # otherwise insert a new node
1724 # XXX Maybe in case self is virtual FBX root node, we should instead add one armature per bone child?
1725 armature = FbxImportHelperNode(None, None, None, False)
1726 armature.fbx_name = "Armature"
1727 armature.is_armature = True
1729 for child in tuple(self.children):
1730 if child.is_bone:
1731 child.parent = armature
1733 armature.parent = self
1735 armature.find_armature_bones(armature)
1737 for child in self.children:
1738 if child.is_armature or child.is_bone:
1739 continue
1740 child.find_armatures()
1742 def find_bone_children(self):
1743 has_bone_children = False
1744 for child in self.children:
1745 has_bone_children |= child.find_bone_children()
1746 self.has_bone_children = has_bone_children
1747 return self.is_bone or has_bone_children
1749 def find_fake_bones(self, in_armature=False):
1750 if in_armature and not self.is_bone and self.has_bone_children:
1751 self.is_bone = True
1752 # if we are not a null node we need an intermediate node for the data
1753 if self.fbx_type not in {b'Null', b'Root'}:
1754 node = FbxImportHelperNode(self.fbx_elem, self.bl_data, None, False)
1755 self.fbx_elem = None
1756 self.bl_data = None
1758 # transfer children
1759 for child in self.children:
1760 if child.is_bone or child.has_bone_children:
1761 continue
1762 child.parent = node
1764 # attach to parent
1765 node.parent = self
1767 if self.is_armature:
1768 in_armature = True
1769 for child in self.children:
1770 child.find_fake_bones(in_armature)
1772 def get_world_matrix_as_parent(self):
1773 matrix = self.parent.get_world_matrix_as_parent() if self.parent else Matrix()
1774 if self.matrix_as_parent:
1775 matrix = matrix @ self.matrix_as_parent
1776 return matrix
1778 def get_world_matrix(self):
1779 matrix = self.parent.get_world_matrix_as_parent() if self.parent else Matrix()
1780 if self.matrix:
1781 matrix = matrix @ self.matrix
1782 return matrix
1784 def get_matrix(self):
1785 matrix = self.matrix if self.matrix else Matrix()
1786 if self.pre_matrix:
1787 matrix = self.pre_matrix @ matrix
1788 if self.post_matrix:
1789 matrix = matrix @ self.post_matrix
1790 return matrix
1792 def get_bind_matrix(self):
1793 matrix = self.bind_matrix if self.bind_matrix else Matrix()
1794 if self.pre_matrix:
1795 matrix = self.pre_matrix @ matrix
1796 if self.post_matrix:
1797 matrix = matrix @ self.post_matrix
1798 return matrix
1800 def make_bind_pose_local(self, parent_matrix=None):
1801 if parent_matrix is None:
1802 parent_matrix = Matrix()
1804 if self.bind_matrix:
1805 bind_matrix = parent_matrix.inverted_safe() @ self.bind_matrix
1806 else:
1807 bind_matrix = self.matrix.copy() if self.matrix else None
1809 self.bind_matrix = bind_matrix
1810 if bind_matrix:
1811 parent_matrix = parent_matrix @ bind_matrix
1813 for child in self.children:
1814 child.make_bind_pose_local(parent_matrix)
1816 def collect_skeleton_meshes(self, meshes):
1817 for _, m in self.clusters:
1818 meshes.update(m)
1819 for child in self.children:
1820 child.collect_skeleton_meshes(meshes)
1822 def collect_armature_meshes(self):
1823 if self.is_armature:
1824 armature_matrix_inv = self.get_world_matrix().inverted_safe()
1826 meshes = set()
1827 for child in self.children:
1828 child.collect_skeleton_meshes(meshes)
1829 for m in meshes:
1830 old_matrix = m.matrix
1831 m.matrix = armature_matrix_inv @ m.get_world_matrix()
1832 m.anim_compensation_matrix = old_matrix.inverted_safe() @ m.matrix
1833 m.is_global_animation = True
1834 m.parent = self
1835 self.meshes = meshes
1836 else:
1837 for child in self.children:
1838 child.collect_armature_meshes()
1840 def build_skeleton(self, arm, parent_matrix, parent_bone_size=1, force_connect_children=False):
1841 def child_connect(par_bone, child_bone, child_head, connect_ctx):
1842 # child_bone or child_head may be None.
1843 force_connect_children, connected = connect_ctx
1844 if child_bone is not None:
1845 child_bone.parent = par_bone
1846 child_head = child_bone.head
1848 if similar_values_iter(par_bone.tail, child_head):
1849 if child_bone is not None:
1850 child_bone.use_connect = True
1851 # Disallow any force-connection at this level from now on, since that child was 'really'
1852 # connected, we do not want to move current bone's tail anymore!
1853 connected = None
1854 elif force_connect_children and connected is not None:
1855 # We only store position where tail of par_bone should be in the end.
1856 # Actual tail moving and force connection of compatible child bones will happen
1857 # once all have been checked.
1858 if connected is ...:
1859 connected = ([child_head.copy(), 1], [child_bone] if child_bone is not None else [])
1860 else:
1861 connected[0][0] += child_head
1862 connected[0][1] += 1
1863 if child_bone is not None:
1864 connected[1].append(child_bone)
1865 connect_ctx[1] = connected
1867 def child_connect_finalize(par_bone, connect_ctx):
1868 force_connect_children, connected = connect_ctx
1869 # Do nothing if force connection is not enabled!
1870 if force_connect_children and connected is not None and connected is not ...:
1871 # Here again we have to be wary about zero-length bones!!!
1872 par_tail = connected[0][0] / connected[0][1]
1873 if (par_tail - par_bone.head).magnitude < 1e-2:
1874 par_bone_vec = (par_bone.tail - par_bone.head).normalized()
1875 par_tail = par_bone.head + par_bone_vec * 0.01
1876 par_bone.tail = par_tail
1877 for child_bone in connected[1]:
1878 if similar_values_iter(par_tail, child_bone.head):
1879 child_bone.use_connect = True
1881 # Create the (edit)bone.
1882 bone = arm.bl_data.edit_bones.new(name=self.fbx_name)
1883 bone.select = True
1884 self.bl_obj = arm.bl_obj
1885 self.bl_data = arm.bl_data
1886 self.bl_bone = bone.name # Could be different from the FBX name!
1888 # get average distance to children
1889 bone_size = 0.0
1890 bone_count = 0
1891 for child in self.children:
1892 if child.is_bone:
1893 bone_size += child.get_bind_matrix().to_translation().magnitude
1894 bone_count += 1
1895 if bone_count > 0:
1896 bone_size /= bone_count
1897 else:
1898 bone_size = parent_bone_size
1900 # So that our bone gets its final length, but still Y-aligned in armature space.
1901 # 0-length bones are automatically collapsed into their parent when you leave edit mode,
1902 # so this enforces a minimum length.
1903 bone_tail = Vector((0.0, 1.0, 0.0)) * max(0.01, bone_size)
1904 bone.tail = bone_tail
1906 # And rotate/move it to its final "rest pose".
1907 bone_matrix = parent_matrix @ self.get_bind_matrix().normalized()
1909 bone.matrix = bone_matrix
1911 # Correction for children attached to a bone. FBX expects to attach to the head of a bone,
1912 # while Blender attaches to the tail.
1913 self.bone_child_matrix = Matrix.Translation(-bone_tail)
1915 connect_ctx = [force_connect_children, ...]
1916 for child in self.children:
1917 if child.is_leaf and force_connect_children:
1918 # Arggggggggggggggggg! We do not want to create this bone, but we need its 'virtual head' location
1919 # to orient current one!!!
1920 child_head = (bone_matrix @ child.get_bind_matrix().normalized()).translation
1921 child_connect(bone, None, child_head, connect_ctx)
1922 elif child.is_bone and not child.ignore:
1923 child_bone = child.build_skeleton(arm, bone_matrix, bone_size,
1924 force_connect_children=force_connect_children)
1925 # Connection to parent.
1926 child_connect(bone, child_bone, None, connect_ctx)
1928 child_connect_finalize(bone, connect_ctx)
1929 return bone
1931 def build_node_obj(self, fbx_tmpl, settings):
1932 if self.bl_obj:
1933 return self.bl_obj
1935 if self.is_bone or not self.fbx_elem:
1936 return None
1938 # create when linking since we need object data
1939 elem_name_utf8 = self.fbx_name
1941 # Object data must be created already
1942 self.bl_obj = obj = bpy.data.objects.new(name=elem_name_utf8, object_data=self.bl_data)
1944 fbx_props = (elem_find_first(self.fbx_elem, b'Properties70'),
1945 elem_find_first(fbx_tmpl, b'Properties70', fbx_elem_nil))
1947 # ----
1948 # Misc Attributes
1950 obj.color[0:3] = elem_props_get_color_rgb(fbx_props, b'Color', (0.8, 0.8, 0.8))
1951 obj.hide_viewport = not bool(elem_props_get_visibility(fbx_props, b'Visibility', 1.0))
1953 obj.matrix_basis = self.get_matrix()
1955 if settings.use_custom_props:
1956 blen_read_custom_properties(self.fbx_elem, obj, settings)
1958 return obj
1960 def build_skeleton_children(self, fbx_tmpl, settings, scene, view_layer):
1961 if self.is_bone:
1962 for child in self.children:
1963 if child.ignore:
1964 continue
1965 child.build_skeleton_children(fbx_tmpl, settings, scene, view_layer)
1966 return None
1967 else:
1968 # child is not a bone
1969 obj = self.build_node_obj(fbx_tmpl, settings)
1971 if obj is None:
1972 return None
1974 for child in self.children:
1975 if child.ignore:
1976 continue
1977 child.build_skeleton_children(fbx_tmpl, settings, scene, view_layer)
1979 # instance in scene
1980 view_layer.active_layer_collection.collection.objects.link(obj)
1981 obj.select_set(True)
1983 return obj
1985 def link_skeleton_children(self, fbx_tmpl, settings, scene):
1986 if self.is_bone:
1987 for child in self.children:
1988 if child.ignore:
1989 continue
1990 child_obj = child.bl_obj
1991 if child_obj and child_obj != self.bl_obj:
1992 child_obj.parent = self.bl_obj # get the armature the bone belongs to
1993 child_obj.parent_bone = self.bl_bone
1994 child_obj.parent_type = 'BONE'
1995 child_obj.matrix_parent_inverse = Matrix()
1997 # Blender attaches to the end of a bone, while FBX attaches to the start.
1998 # bone_child_matrix corrects for that.
1999 if child.pre_matrix:
2000 child.pre_matrix = self.bone_child_matrix @ child.pre_matrix
2001 else:
2002 child.pre_matrix = self.bone_child_matrix
2004 child_obj.matrix_basis = child.get_matrix()
2005 child.link_skeleton_children(fbx_tmpl, settings, scene)
2006 return None
2007 else:
2008 obj = self.bl_obj
2010 for child in self.children:
2011 if child.ignore:
2012 continue
2013 child_obj = child.link_skeleton_children(fbx_tmpl, settings, scene)
2014 if child_obj:
2015 child_obj.parent = obj
2017 return obj
2019 def set_pose_matrix(self, arm):
2020 pose_bone = arm.bl_obj.pose.bones[self.bl_bone]
2021 pose_bone.matrix_basis = self.get_bind_matrix().inverted_safe() @ self.get_matrix()
2023 for child in self.children:
2024 if child.ignore:
2025 continue
2026 if child.is_bone:
2027 child.set_pose_matrix(arm)
2029 def merge_weights(self, combined_weights, fbx_cluster):
2030 indices = elem_prop_first(elem_find_first(fbx_cluster, b'Indexes', default=None), default=())
2031 weights = elem_prop_first(elem_find_first(fbx_cluster, b'Weights', default=None), default=())
2033 for index, weight in zip(indices, weights):
2034 w = combined_weights.get(index)
2035 if w is None:
2036 combined_weights[index] = [weight]
2037 else:
2038 w.append(weight)
2040 def set_bone_weights(self):
2041 ignored_children = tuple(child for child in self.children
2042 if child.is_bone and child.ignore and len(child.clusters) > 0)
2044 if len(ignored_children) > 0:
2045 # If we have an ignored child bone we need to merge their weights into the current bone weights.
2046 # This can happen both intentionally and accidentally when skinning a model. Either way, they
2047 # need to be moved into a parent bone or they cause animation glitches.
2048 for fbx_cluster, meshes in self.clusters:
2049 combined_weights = {}
2050 self.merge_weights(combined_weights, fbx_cluster)
2052 for child in ignored_children:
2053 for child_cluster, child_meshes in child.clusters:
2054 if not meshes.isdisjoint(child_meshes):
2055 self.merge_weights(combined_weights, child_cluster)
2057 # combine child weights
2058 indices = []
2059 weights = []
2060 for i, w in combined_weights.items():
2061 indices.append(i)
2062 if len(w) > 1:
2063 weights.append(sum(w) / len(w))
2064 else:
2065 weights.append(w[0])
2067 add_vgroup_to_objects(indices, weights, self.bl_bone, [node.bl_obj for node in meshes])
2069 # clusters that drive meshes not included in a parent don't need to be merged
2070 all_meshes = set().union(*[meshes for _, meshes in self.clusters])
2071 for child in ignored_children:
2072 for child_cluster, child_meshes in child.clusters:
2073 if all_meshes.isdisjoint(child_meshes):
2074 indices = elem_prop_first(elem_find_first(child_cluster, b'Indexes', default=None), default=())
2075 weights = elem_prop_first(elem_find_first(child_cluster, b'Weights', default=None), default=())
2076 add_vgroup_to_objects(indices, weights, self.bl_bone, [node.bl_obj for node in child_meshes])
2077 else:
2078 # set the vertex weights on meshes
2079 for fbx_cluster, meshes in self.clusters:
2080 indices = elem_prop_first(elem_find_first(fbx_cluster, b'Indexes', default=None), default=())
2081 weights = elem_prop_first(elem_find_first(fbx_cluster, b'Weights', default=None), default=())
2082 add_vgroup_to_objects(indices, weights, self.bl_bone, [node.bl_obj for node in meshes])
2084 for child in self.children:
2085 if child.is_bone and not child.ignore:
2086 child.set_bone_weights()
2088 def build_hierarchy(self, fbx_tmpl, settings, scene, view_layer):
2089 if self.is_armature:
2090 # create when linking since we need object data
2091 elem_name_utf8 = self.fbx_name
2093 self.bl_data = arm_data = bpy.data.armatures.new(name=elem_name_utf8)
2095 # Object data must be created already
2096 self.bl_obj = arm = bpy.data.objects.new(name=elem_name_utf8, object_data=arm_data)
2098 arm.matrix_basis = self.get_matrix()
2100 if self.fbx_elem:
2101 fbx_props = (elem_find_first(self.fbx_elem, b'Properties70'),
2102 elem_find_first(fbx_tmpl, b'Properties70', fbx_elem_nil))
2104 if settings.use_custom_props:
2105 blen_read_custom_properties(self.fbx_elem, arm, settings)
2107 # instance in scene
2108 view_layer.active_layer_collection.collection.objects.link(arm)
2109 arm.select_set(True)
2111 # Add bones:
2113 # Switch to Edit mode.
2114 view_layer.objects.active = arm
2115 is_hidden = arm.hide_viewport
2116 arm.hide_viewport = False # Can't switch to Edit mode hidden objects...
2117 bpy.ops.object.mode_set(mode='EDIT')
2119 for child in self.children:
2120 if child.ignore:
2121 continue
2122 if child.is_bone:
2123 child.build_skeleton(self, Matrix(), force_connect_children=settings.force_connect_children)
2125 bpy.ops.object.mode_set(mode='OBJECT')
2127 arm.hide_viewport = is_hidden
2129 # Set pose matrix
2130 for child in self.children:
2131 if child.ignore:
2132 continue
2133 if child.is_bone:
2134 child.set_pose_matrix(self)
2136 # Add bone children:
2137 for child in self.children:
2138 if child.ignore:
2139 continue
2140 child_obj = child.build_skeleton_children(fbx_tmpl, settings, scene, view_layer)
2142 return arm
2143 elif self.fbx_elem and not self.is_bone:
2144 obj = self.build_node_obj(fbx_tmpl, settings)
2146 # walk through children
2147 for child in self.children:
2148 child.build_hierarchy(fbx_tmpl, settings, scene, view_layer)
2150 # instance in scene
2151 view_layer.active_layer_collection.collection.objects.link(obj)
2152 obj.select_set(True)
2154 return obj
2155 else:
2156 for child in self.children:
2157 child.build_hierarchy(fbx_tmpl, settings, scene, view_layer)
2159 return None
2161 def link_hierarchy(self, fbx_tmpl, settings, scene):
2162 if self.is_armature:
2163 arm = self.bl_obj
2165 # Link bone children:
2166 for child in self.children:
2167 if child.ignore:
2168 continue
2169 child_obj = child.link_skeleton_children(fbx_tmpl, settings, scene)
2170 if child_obj:
2171 child_obj.parent = arm
2173 # Add armature modifiers to the meshes
2174 if self.meshes:
2175 for mesh in self.meshes:
2176 (mmat, amat) = mesh.armature_setup[self]
2177 me_obj = mesh.bl_obj
2179 # bring global armature & mesh matrices into *Blender* global space.
2180 # Note: Usage of matrix_geom (local 'diff' transform) here is quite brittle.
2181 # Among other things, why in hell isn't it taken into account by bindpose & co???
2182 # Probably because org app (max) handles it completely aside from any parenting stuff,
2183 # which we obviously cannot do in Blender. :/
2184 if amat is None:
2185 amat = self.bind_matrix
2186 amat = settings.global_matrix @ (Matrix() if amat is None else amat)
2187 if self.matrix_geom:
2188 amat = amat @ self.matrix_geom
2189 mmat = settings.global_matrix @ mmat
2190 if mesh.matrix_geom:
2191 mmat = mmat @ mesh.matrix_geom
2193 # Now that we have armature and mesh in there (global) bind 'state' (matrix),
2194 # we can compute inverse parenting matrix of the mesh.
2195 me_obj.matrix_parent_inverse = amat.inverted_safe() @ mmat @ me_obj.matrix_basis.inverted_safe()
2197 mod = mesh.bl_obj.modifiers.new(arm.name, 'ARMATURE')
2198 mod.object = arm
2200 # Add bone weights to the deformers
2201 for child in self.children:
2202 if child.ignore:
2203 continue
2204 if child.is_bone:
2205 child.set_bone_weights()
2207 return arm
2208 elif self.bl_obj:
2209 obj = self.bl_obj
2211 # walk through children
2212 for child in self.children:
2213 child_obj = child.link_hierarchy(fbx_tmpl, settings, scene)
2214 if child_obj:
2215 child_obj.parent = obj
2217 return obj
2218 else:
2219 for child in self.children:
2220 child.link_hierarchy(fbx_tmpl, settings, scene)
2222 return None
2225 def is_ascii(filepath, size):
2226 with open(filepath, 'r', encoding="utf-8") as f:
2227 try:
2228 f.read(size)
2229 return True
2230 except UnicodeDecodeError:
2231 pass
2233 return False
2236 def load(operator, context, filepath="",
2237 use_manual_orientation=False,
2238 axis_forward='-Z',
2239 axis_up='Y',
2240 global_scale=1.0,
2241 bake_space_transform=False,
2242 use_custom_normals=True,
2243 use_image_search=False,
2244 use_alpha_decals=False,
2245 decal_offset=0.0,
2246 use_anim=True,
2247 anim_offset=1.0,
2248 use_custom_props=True,
2249 use_custom_props_enum_as_string=True,
2250 ignore_leaf_bones=False,
2251 force_connect_children=False,
2252 automatic_bone_orientation=False,
2253 primary_bone_axis='Y',
2254 secondary_bone_axis='X',
2255 use_prepost_rot=True):
2257 global fbx_elem_nil
2258 fbx_elem_nil = FBXElem('', (), (), ())
2260 import os
2261 import time
2262 from bpy_extras.io_utils import axis_conversion
2264 from . import parse_fbx
2265 from .fbx_utils import RIGHT_HAND_AXES, FBX_FRAMERATES
2267 start_time_proc = time.process_time()
2268 start_time_sys = time.time()
2270 perfmon = PerfMon()
2271 perfmon.level_up()
2272 perfmon.step("FBX Import: start importing %s" % filepath)
2273 perfmon.level_up()
2275 # detect ascii files
2276 if is_ascii(filepath, 24):
2277 operator.report({'ERROR'}, "ASCII FBX files are not supported %r" % filepath)
2278 return {'CANCELLED'}
2280 try:
2281 elem_root, version = parse_fbx.parse(filepath)
2282 except Exception as e:
2283 import traceback
2284 traceback.print_exc()
2286 operator.report({'ERROR'}, "Couldn't open file %r (%s)" % (filepath, e))
2287 return {'CANCELLED'}
2289 if version < 7100:
2290 operator.report({'ERROR'}, "Version %r unsupported, must be %r or later" % (version, 7100))
2291 return {'CANCELLED'}
2293 print("FBX version: %r" % version)
2295 if bpy.ops.object.mode_set.poll():
2296 bpy.ops.object.mode_set(mode='OBJECT', toggle=False)
2298 # deselect all
2299 if bpy.ops.object.select_all.poll():
2300 bpy.ops.object.select_all(action='DESELECT')
2302 basedir = os.path.dirname(filepath)
2304 nodal_material_wrap_map = {}
2305 image_cache = {}
2307 # Tables: (FBX_byte_id -> [FBX_data, None or Blender_datablock])
2308 fbx_table_nodes = {}
2310 if use_alpha_decals:
2311 material_decals = set()
2312 else:
2313 material_decals = None
2315 scene = context.scene
2316 view_layer = context.view_layer
2318 # #### Get some info from GlobalSettings.
2320 perfmon.step("FBX import: Prepare...")
2322 fbx_settings = elem_find_first(elem_root, b'GlobalSettings')
2323 fbx_settings_props = elem_find_first(fbx_settings, b'Properties70')
2324 if fbx_settings is None or fbx_settings_props is None:
2325 operator.report({'ERROR'}, "No 'GlobalSettings' found in file %r" % filepath)
2326 return {'CANCELLED'}
2328 # FBX default base unit seems to be the centimeter, while raw Blender Unit is equivalent to the meter...
2329 unit_scale = elem_props_get_number(fbx_settings_props, b'UnitScaleFactor', 1.0)
2330 unit_scale_org = elem_props_get_number(fbx_settings_props, b'OriginalUnitScaleFactor', 1.0)
2331 global_scale *= (unit_scale / units_blender_to_fbx_factor(context.scene))
2332 # Compute global matrix and scale.
2333 if not use_manual_orientation:
2334 axis_forward = (elem_props_get_integer(fbx_settings_props, b'FrontAxis', 1),
2335 elem_props_get_integer(fbx_settings_props, b'FrontAxisSign', 1))
2336 axis_up = (elem_props_get_integer(fbx_settings_props, b'UpAxis', 2),
2337 elem_props_get_integer(fbx_settings_props, b'UpAxisSign', 1))
2338 axis_coord = (elem_props_get_integer(fbx_settings_props, b'CoordAxis', 0),
2339 elem_props_get_integer(fbx_settings_props, b'CoordAxisSign', 1))
2340 axis_key = (axis_up, axis_forward, axis_coord)
2341 axis_up, axis_forward = {v: k for k, v in RIGHT_HAND_AXES.items()}.get(axis_key, ('Z', 'Y'))
2342 global_matrix = (Matrix.Scale(global_scale, 4) @
2343 axis_conversion(from_forward=axis_forward, from_up=axis_up).to_4x4())
2345 # To cancel out unwanted rotation/scale on nodes.
2346 global_matrix_inv = global_matrix.inverted()
2347 # For transforming mesh normals.
2348 global_matrix_inv_transposed = global_matrix_inv.transposed()
2350 # Compute bone correction matrix
2351 bone_correction_matrix = None # None means no correction/identity
2352 if not automatic_bone_orientation:
2353 if (primary_bone_axis, secondary_bone_axis) != ('Y', 'X'):
2354 bone_correction_matrix = axis_conversion(from_forward='X',
2355 from_up='Y',
2356 to_forward=secondary_bone_axis,
2357 to_up=primary_bone_axis,
2358 ).to_4x4()
2360 # Compute framerate settings.
2361 custom_fps = elem_props_get_number(fbx_settings_props, b'CustomFrameRate', 25.0)
2362 time_mode = elem_props_get_enum(fbx_settings_props, b'TimeMode')
2363 real_fps = {eid: val for val, eid in FBX_FRAMERATES[1:]}.get(time_mode, custom_fps)
2364 if real_fps <= 0.0:
2365 real_fps = 25.0
2366 scene.render.fps = round(real_fps)
2367 scene.render.fps_base = scene.render.fps / real_fps
2369 # store global settings that need to be accessed during conversion
2370 settings = FBXImportSettings(
2371 operator.report, (axis_up, axis_forward), global_matrix, global_scale,
2372 bake_space_transform, global_matrix_inv, global_matrix_inv_transposed,
2373 use_custom_normals, use_image_search,
2374 use_alpha_decals, decal_offset,
2375 use_anim, anim_offset,
2376 use_custom_props, use_custom_props_enum_as_string,
2377 nodal_material_wrap_map, image_cache,
2378 ignore_leaf_bones, force_connect_children, automatic_bone_orientation, bone_correction_matrix,
2379 use_prepost_rot,
2382 # #### And now, the "real" data.
2384 perfmon.step("FBX import: Templates...")
2386 fbx_defs = elem_find_first(elem_root, b'Definitions') # can be None
2387 fbx_nodes = elem_find_first(elem_root, b'Objects')
2388 fbx_connections = elem_find_first(elem_root, b'Connections')
2390 if fbx_nodes is None:
2391 operator.report({'ERROR'}, "No 'Objects' found in file %r" % filepath)
2392 return {'CANCELLED'}
2393 if fbx_connections is None:
2394 operator.report({'ERROR'}, "No 'Connections' found in file %r" % filepath)
2395 return {'CANCELLED'}
2397 # ----
2398 # First load property templates
2399 # Load 'PropertyTemplate' values.
2400 # Key is a tuple, (ObjectType, FBXNodeType)
2401 # eg, (b'Texture', b'KFbxFileTexture')
2402 # (b'Geometry', b'KFbxMesh')
2403 fbx_templates = {}
2405 def _():
2406 if fbx_defs is not None:
2407 for fbx_def in fbx_defs.elems:
2408 if fbx_def.id == b'ObjectType':
2409 for fbx_subdef in fbx_def.elems:
2410 if fbx_subdef.id == b'PropertyTemplate':
2411 assert(fbx_def.props_type == b'S')
2412 assert(fbx_subdef.props_type == b'S')
2413 # (b'Texture', b'KFbxFileTexture') - eg.
2414 key = fbx_def.props[0], fbx_subdef.props[0]
2415 fbx_templates[key] = fbx_subdef
2416 _(); del _
2418 def fbx_template_get(key):
2419 ret = fbx_templates.get(key, fbx_elem_nil)
2420 if ret is fbx_elem_nil:
2421 # Newest FBX (7.4 and above) use no more 'K' in their type names...
2422 key = (key[0], key[1][1:])
2423 return fbx_templates.get(key, fbx_elem_nil)
2424 return ret
2426 perfmon.step("FBX import: Nodes...")
2428 # ----
2429 # Build FBX node-table
2430 def _():
2431 for fbx_obj in fbx_nodes.elems:
2432 # TODO, investigate what other items after first 3 may be
2433 assert(fbx_obj.props_type[:3] == b'LSS')
2434 fbx_uuid = elem_uuid(fbx_obj)
2435 fbx_table_nodes[fbx_uuid] = [fbx_obj, None]
2436 _(); del _
2438 # ----
2439 # Load in the data
2440 # http://download.autodesk.com/us/fbx/20112/FBX_SDK_HELP/index.html?url=
2441 # WS73099cc142f487551fea285e1221e4f9ff8-7fda.htm,topicNumber=d0e6388
2443 perfmon.step("FBX import: Connections...")
2445 fbx_connection_map = {}
2446 fbx_connection_map_reverse = {}
2448 def _():
2449 for fbx_link in fbx_connections.elems:
2450 c_type = fbx_link.props[0]
2451 if fbx_link.props_type[1:3] == b'LL':
2452 c_src, c_dst = fbx_link.props[1:3]
2453 fbx_connection_map.setdefault(c_src, []).append((c_dst, fbx_link))
2454 fbx_connection_map_reverse.setdefault(c_dst, []).append((c_src, fbx_link))
2455 _(); del _
2457 perfmon.step("FBX import: Meshes...")
2459 # ----
2460 # Load mesh data
2461 def _():
2462 fbx_tmpl = fbx_template_get((b'Geometry', b'KFbxMesh'))
2464 for fbx_uuid, fbx_item in fbx_table_nodes.items():
2465 fbx_obj, blen_data = fbx_item
2466 if fbx_obj.id != b'Geometry':
2467 continue
2468 if fbx_obj.props[-1] == b'Mesh':
2469 assert(blen_data is None)
2470 fbx_item[1] = blen_read_geom(fbx_tmpl, fbx_obj, settings)
2471 _(); del _
2473 perfmon.step("FBX import: Materials & Textures...")
2475 # ----
2476 # Load material data
2477 def _():
2478 fbx_tmpl = fbx_template_get((b'Material', b'KFbxSurfacePhong'))
2479 # b'KFbxSurfaceLambert'
2481 for fbx_uuid, fbx_item in fbx_table_nodes.items():
2482 fbx_obj, blen_data = fbx_item
2483 if fbx_obj.id != b'Material':
2484 continue
2485 assert(blen_data is None)
2486 fbx_item[1] = blen_read_material(fbx_tmpl, fbx_obj, settings)
2487 _(); del _
2489 # ----
2490 # Load image & textures data
2491 def _():
2492 fbx_tmpl_tex = fbx_template_get((b'Texture', b'KFbxFileTexture'))
2493 fbx_tmpl_img = fbx_template_get((b'Video', b'KFbxVideo'))
2495 # Important to run all 'Video' ones first, embedded images are stored in those nodes.
2496 # XXX Note we simplify things here, assuming both matching Video and Texture will use same file path,
2497 # this may be a bit weak, if issue arise we'll fallback to plain connection stuff...
2498 for fbx_uuid, fbx_item in fbx_table_nodes.items():
2499 fbx_obj, blen_data = fbx_item
2500 if fbx_obj.id != b'Video':
2501 continue
2502 fbx_item[1] = blen_read_texture_image(fbx_tmpl_img, fbx_obj, basedir, settings)
2503 for fbx_uuid, fbx_item in fbx_table_nodes.items():
2504 fbx_obj, blen_data = fbx_item
2505 if fbx_obj.id != b'Texture':
2506 continue
2507 fbx_item[1] = blen_read_texture_image(fbx_tmpl_tex, fbx_obj, basedir, settings)
2508 _(); del _
2510 perfmon.step("FBX import: Cameras & Lamps...")
2512 # ----
2513 # Load camera data
2514 def _():
2515 fbx_tmpl = fbx_template_get((b'NodeAttribute', b'KFbxCamera'))
2517 for fbx_uuid, fbx_item in fbx_table_nodes.items():
2518 fbx_obj, blen_data = fbx_item
2519 if fbx_obj.id != b'NodeAttribute':
2520 continue
2521 if fbx_obj.props[-1] == b'Camera':
2522 assert(blen_data is None)
2523 fbx_item[1] = blen_read_camera(fbx_tmpl, fbx_obj, global_scale)
2524 _(); del _
2526 # ----
2527 # Load lamp data
2528 def _():
2529 fbx_tmpl = fbx_template_get((b'NodeAttribute', b'KFbxLight'))
2531 for fbx_uuid, fbx_item in fbx_table_nodes.items():
2532 fbx_obj, blen_data = fbx_item
2533 if fbx_obj.id != b'NodeAttribute':
2534 continue
2535 if fbx_obj.props[-1] == b'Light':
2536 assert(blen_data is None)
2537 fbx_item[1] = blen_read_light(fbx_tmpl, fbx_obj, global_scale)
2538 _(); del _
2540 # ----
2541 # Connections
2542 def connection_filter_ex(fbx_uuid, fbx_id, dct):
2543 return [(c_found[0], c_found[1], c_type)
2544 for (c_uuid, c_type) in dct.get(fbx_uuid, ())
2545 # 0 is used for the root node, which isnt in fbx_table_nodes
2546 for c_found in (() if c_uuid is 0 else (fbx_table_nodes.get(c_uuid, (None, None)),))
2547 if (fbx_id is None) or (c_found[0] and c_found[0].id == fbx_id)]
2549 def connection_filter_forward(fbx_uuid, fbx_id):
2550 return connection_filter_ex(fbx_uuid, fbx_id, fbx_connection_map)
2552 def connection_filter_reverse(fbx_uuid, fbx_id):
2553 return connection_filter_ex(fbx_uuid, fbx_id, fbx_connection_map_reverse)
2555 perfmon.step("FBX import: Objects & Armatures...")
2557 # -- temporary helper hierarchy to build armatures and objects from
2558 # lookup from uuid to helper node. Used to build parent-child relations and later to look up animated nodes.
2559 fbx_helper_nodes = {}
2561 def _():
2562 # We build an intermediate hierarchy used to:
2563 # - Calculate and store bone orientation correction matrices. The same matrices will be reused for animation.
2564 # - Find/insert armature nodes.
2565 # - Filter leaf bones.
2567 # create scene root
2568 fbx_helper_nodes[0] = root_helper = FbxImportHelperNode(None, None, None, False)
2569 root_helper.is_root = True
2571 # add fbx nodes
2572 fbx_tmpl = fbx_template_get((b'Model', b'KFbxNode'))
2573 for a_uuid, a_item in fbx_table_nodes.items():
2574 fbx_obj, bl_data = a_item
2575 if fbx_obj is None or fbx_obj.id != b'Model':
2576 continue
2578 fbx_props = (elem_find_first(fbx_obj, b'Properties70'),
2579 elem_find_first(fbx_tmpl, b'Properties70', fbx_elem_nil))
2581 transform_data = blen_read_object_transform_preprocess(fbx_props, fbx_obj, Matrix(), use_prepost_rot)
2582 # Note: 'Root' "bones" are handled as (armature) objects.
2583 # Note: See T46912 for first FBX file I ever saw with 'Limb' bones - thought those were totally deprecated.
2584 is_bone = fbx_obj.props[2] in {b'LimbNode', b'Limb'}
2585 fbx_helper_nodes[a_uuid] = FbxImportHelperNode(fbx_obj, bl_data, transform_data, is_bone)
2587 # add parent-child relations and add blender data to the node
2588 for fbx_link in fbx_connections.elems:
2589 if fbx_link.props[0] != b'OO':
2590 continue
2591 if fbx_link.props_type[1:3] == b'LL':
2592 c_src, c_dst = fbx_link.props[1:3]
2593 parent = fbx_helper_nodes.get(c_dst)
2594 if parent is None:
2595 continue
2597 child = fbx_helper_nodes.get(c_src)
2598 if child is None:
2599 # add blender data (meshes, lights, cameras, etc.) to a helper node
2600 fbx_sdata, bl_data = p_item = fbx_table_nodes.get(c_src, (None, None))
2601 if fbx_sdata is None:
2602 continue
2603 if fbx_sdata.id not in {b'Geometry', b'NodeAttribute'}:
2604 continue
2605 parent.bl_data = bl_data
2606 else:
2607 # set parent
2608 child.parent = parent
2610 # find armatures (either an empty below a bone or a new node inserted at the bone
2611 root_helper.find_armatures()
2613 # mark nodes that have bone children
2614 root_helper.find_bone_children()
2616 # mark nodes that need a bone to attach child-bones to
2617 root_helper.find_fake_bones()
2619 # mark leaf nodes that are only required to mark the end of their parent bone
2620 if settings.ignore_leaf_bones:
2621 root_helper.mark_leaf_bones()
2623 # What a mess! Some bones have several BindPoses, some have none, clusters contain a bind pose as well,
2624 # and you can have several clusters per bone!
2625 # Maybe some conversion can be applied to put them all into the same frame of reference?
2627 # get the bind pose from pose elements
2628 for a_uuid, a_item in fbx_table_nodes.items():
2629 fbx_obj, bl_data = a_item
2630 if fbx_obj is None:
2631 continue
2632 if fbx_obj.id != b'Pose':
2633 continue
2634 if fbx_obj.props[2] != b'BindPose':
2635 continue
2636 for fbx_pose_node in fbx_obj.elems:
2637 if fbx_pose_node.id != b'PoseNode':
2638 continue
2639 node_elem = elem_find_first(fbx_pose_node, b'Node')
2640 node = elem_uuid(node_elem)
2641 matrix_elem = elem_find_first(fbx_pose_node, b'Matrix')
2642 matrix = array_to_matrix4(matrix_elem.props[0]) if matrix_elem else None
2643 bone = fbx_helper_nodes.get(node)
2644 if bone and matrix:
2645 # Store the matrix in the helper node.
2646 # There may be several bind pose matrices for the same node, but in tests they seem to be identical.
2647 bone.bind_matrix = matrix # global space
2649 # get clusters and bind pose
2650 for helper_uuid, helper_node in fbx_helper_nodes.items():
2651 if not helper_node.is_bone:
2652 continue
2653 for cluster_uuid, cluster_link in fbx_connection_map.get(helper_uuid, ()):
2654 if cluster_link.props[0] != b'OO':
2655 continue
2656 fbx_cluster, _ = fbx_table_nodes.get(cluster_uuid, (None, None))
2657 if fbx_cluster is None or fbx_cluster.id != b'Deformer' or fbx_cluster.props[2] != b'Cluster':
2658 continue
2660 # Get the bind pose from the cluster:
2661 tx_mesh_elem = elem_find_first(fbx_cluster, b'Transform', default=None)
2662 tx_mesh = array_to_matrix4(tx_mesh_elem.props[0]) if tx_mesh_elem else Matrix()
2664 tx_bone_elem = elem_find_first(fbx_cluster, b'TransformLink', default=None)
2665 tx_bone = array_to_matrix4(tx_bone_elem.props[0]) if tx_bone_elem else None
2667 tx_arm_elem = elem_find_first(fbx_cluster, b'TransformAssociateModel', default=None)
2668 tx_arm = array_to_matrix4(tx_arm_elem.props[0]) if tx_arm_elem else None
2670 mesh_matrix = tx_mesh
2671 armature_matrix = tx_arm
2673 if tx_bone:
2674 mesh_matrix = tx_bone @ mesh_matrix
2675 helper_node.bind_matrix = tx_bone # overwrite the bind matrix
2677 # Get the meshes driven by this cluster: (Shouldn't that be only one?)
2678 meshes = set()
2679 for skin_uuid, skin_link in fbx_connection_map.get(cluster_uuid):
2680 if skin_link.props[0] != b'OO':
2681 continue
2682 fbx_skin, _ = fbx_table_nodes.get(skin_uuid, (None, None))
2683 if fbx_skin is None or fbx_skin.id != b'Deformer' or fbx_skin.props[2] != b'Skin':
2684 continue
2685 for mesh_uuid, mesh_link in fbx_connection_map.get(skin_uuid):
2686 if mesh_link.props[0] != b'OO':
2687 continue
2688 fbx_mesh, _ = fbx_table_nodes.get(mesh_uuid, (None, None))
2689 if fbx_mesh is None or fbx_mesh.id != b'Geometry' or fbx_mesh.props[2] != b'Mesh':
2690 continue
2691 for object_uuid, object_link in fbx_connection_map.get(mesh_uuid):
2692 if object_link.props[0] != b'OO':
2693 continue
2694 mesh_node = fbx_helper_nodes[object_uuid]
2695 if mesh_node:
2696 # ----
2697 # If we get a valid mesh matrix (in bone space), store armature and
2698 # mesh global matrices, we need them to compute mesh's matrix_parent_inverse
2699 # when actually binding them via the modifier.
2700 # Note we assume all bones were bound with the same mesh/armature (global) matrix,
2701 # we do not support otherwise in Blender anyway!
2702 mesh_node.armature_setup[helper_node.armature] = (mesh_matrix, armature_matrix)
2703 meshes.add(mesh_node)
2705 helper_node.clusters.append((fbx_cluster, meshes))
2707 # convert bind poses from global space into local space
2708 root_helper.make_bind_pose_local()
2710 # collect armature meshes
2711 root_helper.collect_armature_meshes()
2713 # find the correction matrices to align FBX objects with their Blender equivalent
2714 root_helper.find_correction_matrix(settings)
2716 # build the Object/Armature/Bone hierarchy
2717 root_helper.build_hierarchy(fbx_tmpl, settings, scene, view_layer)
2719 # Link the Object/Armature/Bone hierarchy
2720 root_helper.link_hierarchy(fbx_tmpl, settings, scene)
2722 # root_helper.print_info(0)
2723 _(); del _
2725 perfmon.step("FBX import: ShapeKeys...")
2727 # We can handle shapes.
2728 blend_shape_channels = {} # We do not need Shapes themselves, but keyblocks, for anim.
2730 def _():
2731 fbx_tmpl = fbx_template_get((b'Geometry', b'KFbxShape'))
2733 for s_uuid, s_item in fbx_table_nodes.items():
2734 fbx_sdata, bl_sdata = s_item = fbx_table_nodes.get(s_uuid, (None, None))
2735 if fbx_sdata is None or fbx_sdata.id != b'Geometry' or fbx_sdata.props[2] != b'Shape':
2736 continue
2738 # shape -> blendshapechannel -> blendshape -> mesh.
2739 for bc_uuid, bc_ctype in fbx_connection_map.get(s_uuid, ()):
2740 if bc_ctype.props[0] != b'OO':
2741 continue
2742 fbx_bcdata, _bl_bcdata = fbx_table_nodes.get(bc_uuid, (None, None))
2743 if fbx_bcdata is None or fbx_bcdata.id != b'Deformer' or fbx_bcdata.props[2] != b'BlendShapeChannel':
2744 continue
2745 meshes = []
2746 objects = []
2747 for bs_uuid, bs_ctype in fbx_connection_map.get(bc_uuid, ()):
2748 if bs_ctype.props[0] != b'OO':
2749 continue
2750 fbx_bsdata, _bl_bsdata = fbx_table_nodes.get(bs_uuid, (None, None))
2751 if fbx_bsdata is None or fbx_bsdata.id != b'Deformer' or fbx_bsdata.props[2] != b'BlendShape':
2752 continue
2753 for m_uuid, m_ctype in fbx_connection_map.get(bs_uuid, ()):
2754 if m_ctype.props[0] != b'OO':
2755 continue
2756 fbx_mdata, bl_mdata = fbx_table_nodes.get(m_uuid, (None, None))
2757 if fbx_mdata is None or fbx_mdata.id != b'Geometry' or fbx_mdata.props[2] != b'Mesh':
2758 continue
2759 # Blenmeshes are assumed already created at that time!
2760 assert(isinstance(bl_mdata, bpy.types.Mesh))
2761 # And we have to find all objects using this mesh!
2762 objects = []
2763 for o_uuid, o_ctype in fbx_connection_map.get(m_uuid, ()):
2764 if o_ctype.props[0] != b'OO':
2765 continue
2766 node = fbx_helper_nodes[o_uuid]
2767 if node:
2768 objects.append(node)
2769 meshes.append((bl_mdata, objects))
2770 # BlendShape deformers are only here to connect BlendShapeChannels to meshes, nothing else to do.
2772 # keyblocks is a list of tuples (mesh, keyblock) matching that shape/blendshapechannel, for animation.
2773 keyblocks = blen_read_shape(fbx_tmpl, fbx_sdata, fbx_bcdata, meshes, scene)
2774 blend_shape_channels[bc_uuid] = keyblocks
2775 _(); del _
2777 if use_anim:
2778 perfmon.step("FBX import: Animations...")
2780 # Animation!
2781 def _():
2782 fbx_tmpl_astack = fbx_template_get((b'AnimationStack', b'FbxAnimStack'))
2783 fbx_tmpl_alayer = fbx_template_get((b'AnimationLayer', b'FbxAnimLayer'))
2784 stacks = {}
2786 # AnimationStacks.
2787 for as_uuid, fbx_asitem in fbx_table_nodes.items():
2788 fbx_asdata, _blen_data = fbx_asitem
2789 if fbx_asdata.id != b'AnimationStack' or fbx_asdata.props[2] != b'':
2790 continue
2791 stacks[as_uuid] = (fbx_asitem, {})
2793 # AnimationLayers
2794 # (mixing is completely ignored for now, each layer results in an independent set of actions).
2795 def get_astacks_from_alayer(al_uuid):
2796 for as_uuid, as_ctype in fbx_connection_map.get(al_uuid, ()):
2797 if as_ctype.props[0] != b'OO':
2798 continue
2799 fbx_asdata, _bl_asdata = fbx_table_nodes.get(as_uuid, (None, None))
2800 if (fbx_asdata is None or fbx_asdata.id != b'AnimationStack' or
2801 fbx_asdata.props[2] != b'' or as_uuid not in stacks):
2802 continue
2803 yield as_uuid
2804 for al_uuid, fbx_alitem in fbx_table_nodes.items():
2805 fbx_aldata, _blen_data = fbx_alitem
2806 if fbx_aldata.id != b'AnimationLayer' or fbx_aldata.props[2] != b'':
2807 continue
2808 for as_uuid in get_astacks_from_alayer(al_uuid):
2809 _fbx_asitem, alayers = stacks[as_uuid]
2810 alayers[al_uuid] = (fbx_alitem, {})
2812 # AnimationCurveNodes (also the ones linked to actual animated data!).
2813 curvenodes = {}
2814 for acn_uuid, fbx_acnitem in fbx_table_nodes.items():
2815 fbx_acndata, _blen_data = fbx_acnitem
2816 if fbx_acndata.id != b'AnimationCurveNode' or fbx_acndata.props[2] != b'':
2817 continue
2818 cnode = curvenodes[acn_uuid] = {}
2819 items = []
2820 for n_uuid, n_ctype in fbx_connection_map.get(acn_uuid, ()):
2821 if n_ctype.props[0] != b'OP':
2822 continue
2823 lnk_prop = n_ctype.props[3]
2824 if lnk_prop in {b'Lcl Translation', b'Lcl Rotation', b'Lcl Scaling'}:
2825 # n_uuid can (????) be linked to root '0' node, instead of a mere object node... See T41712.
2826 ob = fbx_helper_nodes.get(n_uuid, None)
2827 if ob is None or ob.is_root:
2828 continue
2829 items.append((ob, lnk_prop))
2830 elif lnk_prop == b'DeformPercent': # Shape keys.
2831 keyblocks = blend_shape_channels.get(n_uuid, None)
2832 if keyblocks is None:
2833 continue
2834 items += [(kb, lnk_prop) for kb in keyblocks]
2835 elif lnk_prop == b'FocalLength': # Camera lens.
2836 from bpy.types import Camera
2837 fbx_item = fbx_table_nodes.get(n_uuid, None)
2838 if fbx_item is None or not isinstance(fbx_item[1], Camera):
2839 continue
2840 cam = fbx_item[1]
2841 items.append((cam, lnk_prop))
2842 elif lnk_prop == b'DiffuseColor':
2843 from bpy.types import Material
2844 fbx_item = fbx_table_nodes.get(n_uuid, None)
2845 if fbx_item is None or not isinstance(fbx_item[1], Material):
2846 continue
2847 mat = fbx_item[1]
2848 items.append((mat, lnk_prop))
2849 print("WARNING! Importing material's animation is not supported for Nodal materials...")
2850 for al_uuid, al_ctype in fbx_connection_map.get(acn_uuid, ()):
2851 if al_ctype.props[0] != b'OO':
2852 continue
2853 fbx_aldata, _blen_aldata = fbx_alitem = fbx_table_nodes.get(al_uuid, (None, None))
2854 if fbx_aldata is None or fbx_aldata.id != b'AnimationLayer' or fbx_aldata.props[2] != b'':
2855 continue
2856 for as_uuid in get_astacks_from_alayer(al_uuid):
2857 _fbx_alitem, anim_items = stacks[as_uuid][1][al_uuid]
2858 assert(_fbx_alitem == fbx_alitem)
2859 for item, item_prop in items:
2860 # No need to keep curvenode FBX data here, contains nothing useful for us.
2861 anim_items.setdefault(item, {})[acn_uuid] = (cnode, item_prop)
2863 # AnimationCurves (real animation data).
2864 for ac_uuid, fbx_acitem in fbx_table_nodes.items():
2865 fbx_acdata, _blen_data = fbx_acitem
2866 if fbx_acdata.id != b'AnimationCurve' or fbx_acdata.props[2] != b'':
2867 continue
2868 for acn_uuid, acn_ctype in fbx_connection_map.get(ac_uuid, ()):
2869 if acn_ctype.props[0] != b'OP':
2870 continue
2871 fbx_acndata, _bl_acndata = fbx_table_nodes.get(acn_uuid, (None, None))
2872 if (fbx_acndata is None or fbx_acndata.id != b'AnimationCurveNode' or
2873 fbx_acndata.props[2] != b'' or acn_uuid not in curvenodes):
2874 continue
2875 # Note this is an infamous simplification of the compound props stuff,
2876 # seems to be standard naming but we'll probably have to be smarter to handle more exotic files?
2877 channel = {
2878 b'd|X': 0, b'd|Y': 1, b'd|Z': 2,
2879 b'd|DeformPercent': 0,
2880 b'd|FocalLength': 0
2881 }.get(acn_ctype.props[3], None)
2882 if channel is None:
2883 continue
2884 curvenodes[acn_uuid][ac_uuid] = (fbx_acitem, channel)
2886 # And now that we have sorted all this, apply animations!
2887 blen_read_animations(fbx_tmpl_astack, fbx_tmpl_alayer, stacks, scene, settings.anim_offset)
2889 _(); del _
2891 perfmon.step("FBX import: Assign materials...")
2893 def _():
2894 # link Material's to Geometry (via Model's)
2895 for fbx_uuid, fbx_item in fbx_table_nodes.items():
2896 fbx_obj, blen_data = fbx_item
2897 if fbx_obj.id != b'Geometry':
2898 continue
2900 mesh = fbx_table_nodes.get(fbx_uuid, (None, None))[1]
2902 # can happen in rare cases
2903 if mesh is None:
2904 continue
2906 # In Blender, we link materials to data, typically (meshes), while in FBX they are linked to objects...
2907 # So we have to be careful not to re-add endlessly the same material to a mesh!
2908 # This can easily happen with 'baked' dupliobjects, see T44386.
2909 # TODO: add an option to link materials to objects in Blender instead?
2910 done_materials = set()
2912 for (fbx_lnk, fbx_lnk_item, fbx_lnk_type) in connection_filter_forward(fbx_uuid, b'Model'):
2913 # link materials
2914 fbx_lnk_uuid = elem_uuid(fbx_lnk)
2915 for (fbx_lnk_material, material, fbx_lnk_material_type) in connection_filter_reverse(fbx_lnk_uuid, b'Material'):
2916 if material not in done_materials:
2917 mesh.materials.append(material)
2918 done_materials.add(material)
2920 # We have to validate mesh polygons' ma_idx, see T41015!
2921 # Some FBX seem to have an extra 'default' material which is not defined in FBX file.
2922 if mesh.validate_material_indices():
2923 print("WARNING: mesh '%s' had invalid material indices, those were reset to first material" % mesh.name)
2924 _(); del _
2926 perfmon.step("FBX import: Assign textures...")
2928 def _():
2929 material_images = {}
2931 fbx_tmpl = fbx_template_get((b'Material', b'KFbxSurfacePhong'))
2932 # b'KFbxSurfaceLambert'
2934 def texture_mapping_set(fbx_obj, node_texture):
2935 assert(fbx_obj.id == b'Texture')
2937 fbx_props = (elem_find_first(fbx_obj, b'Properties70'),
2938 elem_find_first(fbx_tmpl, b'Properties70', fbx_elem_nil))
2939 loc = elem_props_get_vector_3d(fbx_props, b'Translation', (0.0, 0.0, 0.0))
2940 rot = tuple(-r for r in elem_props_get_vector_3d(fbx_props, b'Rotation', (0.0, 0.0, 0.0)))
2941 scale = tuple(((1.0 / s) if s != 0.0 else 1.0)
2942 for s in elem_props_get_vector_3d(fbx_props, b'Scaling', (1.0, 1.0, 1.0)))
2943 clamp_uv = (bool(elem_props_get_enum(fbx_props, b'WrapModeU', 0)),
2944 bool(elem_props_get_enum(fbx_props, b'WrapModeV', 0)))
2946 if (loc == (0.0, 0.0, 0.0) and
2947 rot == (0.0, 0.0, 0.0) and
2948 scale == (1.0, 1.0, 1.0) and
2949 clamp_uv == (False, False)):
2950 return
2952 node_texture.translation = loc
2953 node_texture.rotation = rot
2954 node_texture.scale = scale
2956 # awkward conversion UV clamping to min/max
2957 node_texture.min = (0.0, 0.0, 0.0)
2958 node_texture.max = (1.0, 1.0, 1.0)
2959 node_texture.use_min = node_texture.use_max = clamp_uv[0] or clamp_uv[1]
2960 if clamp_uv[0] != clamp_uv[1]:
2961 # use bool as index
2962 node_texture.min[not clamp[0]] = -1e9
2963 node_texture.max[not clamp[0]] = 1e9
2965 for fbx_uuid, fbx_item in fbx_table_nodes.items():
2966 fbx_obj, blen_data = fbx_item
2967 if fbx_obj.id != b'Material':
2968 continue
2970 material = fbx_table_nodes.get(fbx_uuid, (None, None))[1]
2971 for (fbx_lnk,
2972 image,
2973 fbx_lnk_type) in connection_filter_reverse(fbx_uuid, b'Texture'):
2975 if fbx_lnk_type.props[0] == b'OP':
2976 lnk_type = fbx_lnk_type.props[3]
2978 ma_wrap = nodal_material_wrap_map[material]
2980 if lnk_type in {b'DiffuseColor', b'3dsMax|maps|texmap_diffuse'}:
2981 ma_wrap.base_color_texture.image = image
2982 texture_mapping_set(fbx_lnk, ma_wrap.base_color_texture)
2983 elif lnk_type in {b'SpecularColor', b'SpecularFactor'}:
2984 # Intensity actually, not color...
2985 ma_wrap.specular_texture.image = image
2986 texture_mapping_set(fbx_lnk, ma_wrap.specular_texture)
2987 elif lnk_type in {b'ReflectionColor', b'ReflectionFactor', b'3dsMax|maps|texmap_reflection'}:
2988 # Intensity actually, not color...
2989 ma_wrap.metallic_texture.image = image
2990 texture_mapping_set(fbx_lnk, ma_wrap.metallic_texture)
2991 elif lnk_type in {b'TransparentColor', b'TransparentFactor'}:
2992 # Transparency... sort of...
2993 ma_wrap.transmission_texture.image = image
2994 texture_mapping_set(fbx_lnk, ma_wrap.transmission_texture)
2995 if use_alpha_decals:
2996 material_decals.add(material)
2997 elif lnk_type == b'ShininessExponent':
2998 # That is probably reversed compared to expected results? TODO...
2999 ma_wrap.roughness_texture.image = image
3000 texture_mapping_set(fbx_lnk, ma_wrap.roughness_texture)
3001 # XXX, applications abuse bump!
3002 elif lnk_type in {b'NormalMap', b'Bump', b'3dsMax|maps|texmap_bump'}:
3003 ma_wrap.normalmap_texture.image = image
3004 texture_mapping_set(fbx_lnk, ma_wrap.normalmap_texture)
3006 elif lnk_type == b'Bump':
3007 # TODO displacement...
3009 else:
3010 print("WARNING: material link %r ignored" % lnk_type)
3012 material_images.setdefault(material, {})[lnk_type] = image
3014 # Check if the diffuse image has an alpha channel,
3015 # if so, use the alpha channel.
3017 # Note: this could be made optional since images may have alpha but be entirely opaque
3018 for fbx_uuid, fbx_item in fbx_table_nodes.items():
3019 fbx_obj, blen_data = fbx_item
3020 if fbx_obj.id != b'Material':
3021 continue
3022 material = fbx_table_nodes.get(fbx_uuid, (None, None))[1]
3023 image = material_images.get(material, {}).get(b'DiffuseColor', None)
3024 # do we have alpha?
3025 if image and image.depth == 32:
3026 if use_alpha_decals:
3027 material_decals.add(material)
3029 ma_wrap = nodal_material_wrap_map[material]
3030 ma_wrap.transmission_texture.use_alpha = True
3031 ma_wrap.transmission_texture.copy_from(ma_wrap.base_color_texture)
3033 # Propagate mapping from diffuse to all other channels which have none defined.
3034 # XXX Commenting for now, I do not really understand the logic here, why should diffuse mapping
3035 # be applied to all others if not defined for them???
3036 # ~ ma_wrap = nodal_material_wrap_map[material]
3037 # ~ ma_wrap.mapping_set_from_diffuse()
3039 _(); del _
3041 perfmon.step("FBX import: Cycles z-offset workaround...")
3043 def _():
3044 # Annoying workaround for cycles having no z-offset
3045 if material_decals and use_alpha_decals:
3046 for fbx_uuid, fbx_item in fbx_table_nodes.items():
3047 fbx_obj, blen_data = fbx_item
3048 if fbx_obj.id != b'Geometry':
3049 continue
3050 if fbx_obj.props[-1] == b'Mesh':
3051 mesh = fbx_item[1]
3053 if decal_offset != 0.0:
3054 for material in mesh.materials:
3055 if material in material_decals:
3056 for v in mesh.vertices:
3057 v.co += v.normal * decal_offset
3058 break
3060 for obj in (obj for obj in bpy.data.objects if obj.data == mesh):
3061 obj.cycles_visibility.shadow = False
3062 _(); del _
3064 perfmon.level_down()
3066 perfmon.level_down("Import finished.")
3067 return {'FINISHED'}