Cleanup: remove unused operator args
[blender-addons.git] / io_scene_fbx / import_fbx.py
blobb8429071aad9942ec0c1f5d763c776fb3497a6e7
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_LAMP = fbx_utils.MAT_CONVERT_LAMP.inverted()
64 MAT_CONVERT_CAMERA = fbx_utils.MAT_CONVERT_CAMERA.inverted()
67 def elem_find_first(elem, id_search, default=None):
68 for fbx_item in elem.elems:
69 if fbx_item.id == id_search:
70 return fbx_item
71 return default
74 def elem_find_iter(elem, id_search):
75 for fbx_item in elem.elems:
76 if fbx_item.id == id_search:
77 yield fbx_item
80 def elem_find_first_string(elem, id_search):
81 fbx_item = elem_find_first(elem, id_search)
82 if fbx_item is not None and fbx_item.props: # Do not error on complete empty properties (see T45291).
83 assert(len(fbx_item.props) == 1)
84 assert(fbx_item.props_type[0] == data_types.STRING)
85 return fbx_item.props[0].decode('utf-8')
86 return None
89 def elem_find_first_string_as_bytes(elem, id_search):
90 fbx_item = elem_find_first(elem, id_search)
91 if fbx_item is not None and fbx_item.props: # Do not error on complete empty properties (see T45291).
92 assert(len(fbx_item.props) == 1)
93 assert(fbx_item.props_type[0] == data_types.STRING)
94 return fbx_item.props[0] # Keep it as bytes as requested...
95 return None
98 def elem_find_first_bytes(elem, id_search, decode=True):
99 fbx_item = elem_find_first(elem, id_search)
100 if fbx_item is not None and fbx_item.props: # Do not error on complete empty properties (see T45291).
101 assert(len(fbx_item.props) == 1)
102 assert(fbx_item.props_type[0] == data_types.BYTES)
103 return fbx_item.props[0]
104 return None
107 def elem_repr(elem):
108 return "%s: props[%d=%r], elems=(%r)" % (
109 elem.id,
110 len(elem.props),
111 ", ".join([repr(p) for p in elem.props]),
112 # elem.props_type,
113 b", ".join([e.id for e in elem.elems]),
117 def elem_split_name_class(elem):
118 assert(elem.props_type[-2] == data_types.STRING)
119 elem_name, elem_class = elem.props[-2].split(b'\x00\x01')
120 return elem_name, elem_class
123 def elem_name_ensure_class(elem, clss=...):
124 elem_name, elem_class = elem_split_name_class(elem)
125 if clss is not ...:
126 assert(elem_class == clss)
127 return elem_name.decode('utf-8')
130 def elem_name_ensure_classes(elem, clss=...):
131 elem_name, elem_class = elem_split_name_class(elem)
132 if clss is not ...:
133 assert(elem_class in clss)
134 return elem_name.decode('utf-8')
137 def elem_split_name_class_nodeattr(elem):
138 assert(elem.props_type[-2] == data_types.STRING)
139 elem_name, elem_class = elem.props[-2].split(b'\x00\x01')
140 assert(elem_class == b'NodeAttribute')
141 assert(elem.props_type[-1] == data_types.STRING)
142 elem_class = elem.props[-1]
143 return elem_name, elem_class
146 def elem_uuid(elem):
147 assert(elem.props_type[0] == data_types.INT64)
148 return elem.props[0]
151 def elem_prop_first(elem, default=None):
152 return elem.props[0] if (elem is not None) and elem.props else default
155 # ----
156 # Support for
157 # Properties70: { ... P:
158 def elem_props_find_first(elem, elem_prop_id):
159 if elem is None:
160 # When properties are not found... Should never happen, but happens - as usual.
161 return None
162 # support for templates (tuple of elems)
163 if type(elem) is not FBXElem:
164 assert(type(elem) is tuple)
165 for e in elem:
166 result = elem_props_find_first(e, elem_prop_id)
167 if result is not None:
168 return result
169 assert(len(elem) > 0)
170 return None
172 for subelem in elem.elems:
173 assert(subelem.id == b'P')
174 if subelem.props[0] == elem_prop_id:
175 return subelem
176 return None
179 def elem_props_get_color_rgb(elem, elem_prop_id, default=None):
180 elem_prop = elem_props_find_first(elem, elem_prop_id)
181 if elem_prop is not None:
182 assert(elem_prop.props[0] == elem_prop_id)
183 if elem_prop.props[1] == b'Color':
184 # FBX version 7300
185 assert(elem_prop.props[1] == b'Color')
186 assert(elem_prop.props[2] == b'')
187 else:
188 assert(elem_prop.props[1] == b'ColorRGB')
189 assert(elem_prop.props[2] == b'Color')
190 assert(elem_prop.props_type[4:7] == bytes((data_types.FLOAT64,)) * 3)
191 return elem_prop.props[4:7]
192 return default
195 def elem_props_get_vector_3d(elem, elem_prop_id, default=None):
196 elem_prop = elem_props_find_first(elem, elem_prop_id)
197 if elem_prop is not None:
198 assert(elem_prop.props_type[4:7] == bytes((data_types.FLOAT64,)) * 3)
199 return elem_prop.props[4:7]
200 return default
203 def elem_props_get_number(elem, elem_prop_id, default=None):
204 elem_prop = elem_props_find_first(elem, elem_prop_id)
205 if elem_prop is not None:
206 assert(elem_prop.props[0] == elem_prop_id)
207 if elem_prop.props[1] == b'double':
208 assert(elem_prop.props[1] == b'double')
209 assert(elem_prop.props[2] == b'Number')
210 else:
211 assert(elem_prop.props[1] == b'Number')
212 assert(elem_prop.props[2] == b'')
214 # we could allow other number types
215 assert(elem_prop.props_type[4] == data_types.FLOAT64)
217 return elem_prop.props[4]
218 return default
221 def elem_props_get_integer(elem, elem_prop_id, default=None):
222 elem_prop = elem_props_find_first(elem, elem_prop_id)
223 if elem_prop is not None:
224 assert(elem_prop.props[0] == elem_prop_id)
225 if elem_prop.props[1] == b'int':
226 assert(elem_prop.props[1] == b'int')
227 assert(elem_prop.props[2] == b'Integer')
228 elif elem_prop.props[1] == b'ULongLong':
229 assert(elem_prop.props[1] == b'ULongLong')
230 assert(elem_prop.props[2] == b'')
232 # we could allow other number types
233 assert(elem_prop.props_type[4] in {data_types.INT32, data_types.INT64})
235 return elem_prop.props[4]
236 return default
239 def elem_props_get_bool(elem, elem_prop_id, default=None):
240 elem_prop = elem_props_find_first(elem, elem_prop_id)
241 if elem_prop is not None:
242 assert(elem_prop.props[0] == elem_prop_id)
243 assert(elem_prop.props[1] == b'bool')
244 assert(elem_prop.props[2] == b'')
245 assert(elem_prop.props[3] == b'')
247 # we could allow other number types
248 assert(elem_prop.props_type[4] == data_types.INT32)
249 assert(elem_prop.props[4] in {0, 1})
251 return bool(elem_prop.props[4])
252 return default
255 def elem_props_get_enum(elem, elem_prop_id, default=None):
256 elem_prop = elem_props_find_first(elem, elem_prop_id)
257 if elem_prop is not None:
258 assert(elem_prop.props[0] == elem_prop_id)
259 assert(elem_prop.props[1] == b'enum')
260 assert(elem_prop.props[2] == b'')
261 assert(elem_prop.props[3] == b'')
263 # we could allow other number types
264 assert(elem_prop.props_type[4] == data_types.INT32)
266 return elem_prop.props[4]
267 return default
270 def elem_props_get_visibility(elem, elem_prop_id, default=None):
271 elem_prop = elem_props_find_first(elem, elem_prop_id)
272 if elem_prop is not None:
273 assert(elem_prop.props[0] == elem_prop_id)
274 assert(elem_prop.props[1] == b'Visibility')
275 assert(elem_prop.props[2] == b'')
277 # we could allow other number types
278 assert(elem_prop.props_type[4] == data_types.FLOAT64)
280 return elem_prop.props[4]
281 return default
284 # ----------------------------------------------------------------------------
285 # Blender
287 # ------
288 # Object
289 from collections import namedtuple
292 FBXTransformData = namedtuple("FBXTransformData", (
293 "loc", "geom_loc",
294 "rot", "rot_ofs", "rot_piv", "pre_rot", "pst_rot", "rot_ord", "rot_alt_mat", "geom_rot",
295 "sca", "sca_ofs", "sca_piv", "geom_sca",
299 def blen_read_custom_properties(fbx_obj, blen_obj, settings):
300 # There doesn't seem to be a way to put user properties into templates, so this only get the object properties:
301 fbx_obj_props = elem_find_first(fbx_obj, b'Properties70')
302 if fbx_obj_props:
303 for fbx_prop in fbx_obj_props.elems:
304 assert(fbx_prop.id == b'P')
306 if b'U' in fbx_prop.props[3]:
307 if fbx_prop.props[0] == b'UDP3DSMAX':
308 # Special case for 3DS Max user properties:
309 assert(fbx_prop.props[1] == b'KString')
310 assert(fbx_prop.props_type[4] == data_types.STRING)
311 items = fbx_prop.props[4].decode('utf-8')
312 for item in items.split('\r\n'):
313 if item:
314 prop_name, prop_value = item.split('=', 1)
315 blen_obj[prop_name.strip()] = prop_value.strip()
316 else:
317 prop_name = fbx_prop.props[0].decode('utf-8')
318 prop_type = fbx_prop.props[1]
319 if prop_type in {b'Vector', b'Vector3D', b'Color', b'ColorRGB'}:
320 assert(fbx_prop.props_type[4:7] == bytes((data_types.FLOAT64,)) * 3)
321 blen_obj[prop_name] = fbx_prop.props[4:7]
322 elif prop_type in {b'Vector4', b'ColorRGBA'}:
323 assert(fbx_prop.props_type[4:8] == bytes((data_types.FLOAT64,)) * 4)
324 blen_obj[prop_name] = fbx_prop.props[4:8]
325 elif prop_type == b'Vector2D':
326 assert(fbx_prop.props_type[4:6] == bytes((data_types.FLOAT64,)) * 2)
327 blen_obj[prop_name] = fbx_prop.props[4:6]
328 elif prop_type in {b'Integer', b'int'}:
329 assert(fbx_prop.props_type[4] == data_types.INT32)
330 blen_obj[prop_name] = fbx_prop.props[4]
331 elif prop_type == b'KString':
332 assert(fbx_prop.props_type[4] == data_types.STRING)
333 blen_obj[prop_name] = fbx_prop.props[4].decode('utf-8')
334 elif prop_type in {b'Number', b'double', b'Double'}:
335 assert(fbx_prop.props_type[4] == data_types.FLOAT64)
336 blen_obj[prop_name] = fbx_prop.props[4]
337 elif prop_type in {b'Float', b'float'}:
338 assert(fbx_prop.props_type[4] == data_types.FLOAT32)
339 blen_obj[prop_name] = fbx_prop.props[4]
340 elif prop_type in {b'Bool', b'bool'}:
341 assert(fbx_prop.props_type[4] == data_types.INT32)
342 blen_obj[prop_name] = fbx_prop.props[4] != 0
343 elif prop_type in {b'Enum', b'enum'}:
344 assert(fbx_prop.props_type[4:6] == bytes((data_types.INT32, data_types.STRING)))
345 val = fbx_prop.props[4]
346 if settings.use_custom_props_enum_as_string:
347 enum_items = fbx_prop.props[5].decode('utf-8').split('~')
348 assert(val >= 0 and val < len(enum_items))
349 blen_obj[prop_name] = enum_items[val]
350 else:
351 blen_obj[prop_name] = val
352 else:
353 print ("WARNING: User property type '%s' is not supported" % prop_type.decode('utf-8'))
356 def blen_read_object_transform_do(transform_data):
357 # This is a nightmare. FBX SDK uses Maya way to compute the transformation matrix of a node - utterly simple:
359 # WorldTransform = ParentWorldTransform * T * Roff * Rp * Rpre * R * Rpost * Rp-1 * Soff * Sp * S * Sp-1
361 # Where all those terms are 4 x 4 matrices that contain:
362 # WorldTransform: Transformation matrix of the node in global space.
363 # ParentWorldTransform: Transformation matrix of the parent node in global space.
364 # T: Translation
365 # Roff: Rotation offset
366 # Rp: Rotation pivot
367 # Rpre: Pre-rotation
368 # R: Rotation
369 # Rpost: Post-rotation
370 # Rp-1: Inverse of the rotation pivot
371 # Soff: Scaling offset
372 # Sp: Scaling pivot
373 # S: Scaling
374 # Sp-1: Inverse of the scaling pivot
376 # But it was still too simple, and FBX notion of compatibility is... quite specific. So we also have to
377 # support 3DSMax way:
379 # WorldTransform = ParentWorldTransform * T * R * S * OT * OR * OS
381 # Where all those terms are 4 x 4 matrices that contain:
382 # WorldTransform: Transformation matrix of the node in global space
383 # ParentWorldTransform: Transformation matrix of the parent node in global space
384 # T: Translation
385 # R: Rotation
386 # S: Scaling
387 # OT: Geometric transform translation
388 # OR: Geometric transform rotation
389 # OS: Geometric transform translation
391 # Notes:
392 # Geometric transformations ***are not inherited***: ParentWorldTransform does not contain the OT, OR, OS
393 # of WorldTransform's parent node.
395 # Taken from http://download.autodesk.com/us/fbx/20112/FBX_SDK_HELP/
396 # index.html?url=WS1a9193826455f5ff1f92379812724681e696651.htm,topicNumber=d0e7429
398 # translation
399 lcl_translation = Matrix.Translation(transform_data.loc)
400 geom_loc = Matrix.Translation(transform_data.geom_loc)
402 # rotation
403 to_rot = lambda rot, rot_ord: Euler(convert_deg_to_rad_iter(rot), rot_ord).to_matrix().to_4x4()
404 lcl_rot = to_rot(transform_data.rot, transform_data.rot_ord) * transform_data.rot_alt_mat
405 pre_rot = to_rot(transform_data.pre_rot, transform_data.rot_ord)
406 pst_rot = to_rot(transform_data.pst_rot, transform_data.rot_ord)
407 geom_rot = to_rot(transform_data.geom_rot, transform_data.rot_ord)
409 rot_ofs = Matrix.Translation(transform_data.rot_ofs)
410 rot_piv = Matrix.Translation(transform_data.rot_piv)
411 sca_ofs = Matrix.Translation(transform_data.sca_ofs)
412 sca_piv = Matrix.Translation(transform_data.sca_piv)
414 # scale
415 lcl_scale = Matrix()
416 lcl_scale[0][0], lcl_scale[1][1], lcl_scale[2][2] = transform_data.sca
417 geom_scale = Matrix();
418 geom_scale[0][0], geom_scale[1][1], geom_scale[2][2] = transform_data.geom_sca
420 base_mat = (
421 lcl_translation *
422 rot_ofs *
423 rot_piv *
424 pre_rot *
425 lcl_rot *
426 pst_rot *
427 rot_piv.inverted_safe() *
428 sca_ofs *
429 sca_piv *
430 lcl_scale *
431 sca_piv.inverted_safe()
433 geom_mat = geom_loc * geom_rot * geom_scale
434 # We return mat without 'geometric transforms' too, because it is to be used for children, sigh...
435 return (base_mat * geom_mat, base_mat, geom_mat)
438 # XXX This might be weak, now that we can add vgroups from both bones and shapes, name collisions become
439 # more likely, will have to make this more robust!!!
440 def add_vgroup_to_objects(vg_indices, vg_weights, vg_name, objects):
441 assert(len(vg_indices) == len(vg_weights))
442 if vg_indices:
443 for obj in objects:
444 # We replace/override here...
445 vg = obj.vertex_groups.get(vg_name)
446 if vg is None:
447 vg = obj.vertex_groups.new(vg_name)
448 for i, w in zip(vg_indices, vg_weights):
449 vg.add((i,), w, 'REPLACE')
452 def blen_read_object_transform_preprocess(fbx_props, fbx_obj, rot_alt_mat, use_prepost_rot):
453 # This is quite involved, 'fbxRNode.cpp' from openscenegraph used as a reference
454 const_vector_zero_3d = 0.0, 0.0, 0.0
455 const_vector_one_3d = 1.0, 1.0, 1.0
457 loc = list(elem_props_get_vector_3d(fbx_props, b'Lcl Translation', const_vector_zero_3d))
458 rot = list(elem_props_get_vector_3d(fbx_props, b'Lcl Rotation', const_vector_zero_3d))
459 sca = list(elem_props_get_vector_3d(fbx_props, b'Lcl Scaling', const_vector_one_3d))
461 geom_loc = list(elem_props_get_vector_3d(fbx_props, b'GeometricTranslation', const_vector_zero_3d))
462 geom_rot = list(elem_props_get_vector_3d(fbx_props, b'GeometricRotation', const_vector_zero_3d))
463 geom_sca = list(elem_props_get_vector_3d(fbx_props, b'GeometricScaling', const_vector_one_3d))
465 rot_ofs = elem_props_get_vector_3d(fbx_props, b'RotationOffset', const_vector_zero_3d)
466 rot_piv = elem_props_get_vector_3d(fbx_props, b'RotationPivot', const_vector_zero_3d)
467 sca_ofs = elem_props_get_vector_3d(fbx_props, b'ScalingOffset', const_vector_zero_3d)
468 sca_piv = elem_props_get_vector_3d(fbx_props, b'ScalingPivot', const_vector_zero_3d)
470 is_rot_act = elem_props_get_bool(fbx_props, b'RotationActive', False)
472 if is_rot_act:
473 if use_prepost_rot:
474 pre_rot = elem_props_get_vector_3d(fbx_props, b'PreRotation', const_vector_zero_3d)
475 pst_rot = elem_props_get_vector_3d(fbx_props, b'PostRotation', const_vector_zero_3d)
476 else:
477 pre_rot = const_vector_zero_3d
478 pst_rot = const_vector_zero_3d
479 rot_ord = {
480 0: 'XYZ',
481 1: 'XZY',
482 2: 'YZX',
483 3: 'YXZ',
484 4: 'ZXY',
485 5: 'ZYX',
486 6: 'XYZ', # XXX eSphericXYZ, not really supported...
487 }.get(elem_props_get_enum(fbx_props, b'RotationOrder', 0))
488 else:
489 pre_rot = const_vector_zero_3d
490 pst_rot = const_vector_zero_3d
491 rot_ord = 'XYZ'
493 return FBXTransformData(loc, geom_loc,
494 rot, rot_ofs, rot_piv, pre_rot, pst_rot, rot_ord, rot_alt_mat, geom_rot,
495 sca, sca_ofs, sca_piv, geom_sca)
498 # ---------
499 # Animation
500 def blen_read_animations_curves_iter(fbx_curves, blen_start_offset, fbx_start_offset, fps):
502 Get raw FBX AnimCurve list, and yield values for all curves at each singular curves' keyframes,
503 together with (blender) timing, in frames.
504 blen_start_offset is expected in frames, while fbx_start_offset is expected in FBX ktime.
506 # As a first step, assume linear interpolation between key frames, we'll (try to!) handle more
507 # of FBX curves later.
508 from .fbx_utils import FBX_KTIME
509 timefac = fps / FBX_KTIME
511 curves = tuple([0,
512 elem_prop_first(elem_find_first(c[2], b'KeyTime')),
513 elem_prop_first(elem_find_first(c[2], b'KeyValueFloat')),
515 for c in fbx_curves)
517 allkeys = sorted({item for sublist in curves for item in sublist[1]})
518 for curr_fbxktime in allkeys:
519 curr_values = []
520 for item in curves:
521 idx, times, values, fbx_curve = item
523 if times[idx] < curr_fbxktime:
524 if idx >= 0:
525 idx += 1
526 if idx >= len(times):
527 # We have reached our last element for this curve, stay on it from now on...
528 idx = -1
529 item[0] = idx
531 if times[idx] >= curr_fbxktime:
532 if idx == 0:
533 curr_values.append((values[idx], fbx_curve))
534 else:
535 # Interpolate between this key and the previous one.
536 ifac = (curr_fbxktime - times[idx - 1]) / (times[idx] - times[idx - 1])
537 curr_values.append(((values[idx] - values[idx - 1]) * ifac + values[idx - 1], fbx_curve))
538 curr_blenkframe = (curr_fbxktime - fbx_start_offset) * timefac + blen_start_offset
539 yield (curr_blenkframe, curr_values)
542 def blen_read_animations_action_item(action, item, cnodes, fps, anim_offset):
544 'Bake' loc/rot/scale into the action,
545 taking any pre_ and post_ matrix into account to transform from fbx into blender space.
547 from bpy.types import Object, PoseBone, ShapeKey
548 from itertools import chain
550 fbx_curves = []
551 for curves, fbxprop in cnodes.values():
552 for (fbx_acdata, _blen_data), channel in curves.values():
553 fbx_curves.append((fbxprop, channel, fbx_acdata))
555 # Leave if no curves are attached (if a blender curve is attached to scale but without keys it defaults to 0).
556 if len(fbx_curves) == 0:
557 return
559 blen_curves = []
560 props = []
562 if isinstance(item, ShapeKey):
563 props = [(item.path_from_id("value"), 1, "Key")]
564 else: # Object or PoseBone:
565 if item.is_bone:
566 bl_obj = item.bl_obj.pose.bones[item.bl_bone]
567 else:
568 bl_obj = item.bl_obj
570 # We want to create actions for objects, but for bones we 'reuse' armatures' actions!
571 grpname = item.bl_obj.name
573 # Since we might get other channels animated in the end, due to all FBX transform magic,
574 # we need to add curves for whole loc/rot/scale in any case.
575 props = [(bl_obj.path_from_id("location"), 3, grpname or "Location"),
576 None,
577 (bl_obj.path_from_id("scale"), 3, grpname or "Scale")]
578 rot_mode = bl_obj.rotation_mode
579 if rot_mode == 'QUATERNION':
580 props[1] = (bl_obj.path_from_id("rotation_quaternion"), 4, grpname or "Quaternion Rotation")
581 elif rot_mode == 'AXIS_ANGLE':
582 props[1] = (bl_obj.path_from_id("rotation_axis_angle"), 4, grpname or "Axis Angle Rotation")
583 else: # Euler
584 props[1] = (bl_obj.path_from_id("rotation_euler"), 3, grpname or "Euler Rotation")
586 blen_curves = [action.fcurves.new(prop, channel, grpname)
587 for prop, nbr_channels, grpname in props for channel in range(nbr_channels)]
589 if isinstance(item, ShapeKey):
590 for frame, values in blen_read_animations_curves_iter(fbx_curves, anim_offset, 0, fps):
591 value = 0.0
592 for v, (fbxprop, channel, _fbx_acdata) in values:
593 assert(fbxprop == b'DeformPercent')
594 assert(channel == 0)
595 value = v / 100.0
597 for fc, v in zip(blen_curves, (value,)):
598 fc.keyframe_points.insert(frame, v, {'NEEDED', 'FAST'}).interpolation = 'LINEAR'
600 else: # Object or PoseBone:
601 if item.is_bone:
602 bl_obj = item.bl_obj.pose.bones[item.bl_bone]
603 else:
604 bl_obj = item.bl_obj
606 transform_data = item.fbx_transform_data
607 rot_prev = bl_obj.rotation_euler.copy()
609 # Pre-compute inverted local rest matrix of the bone, if relevant.
610 restmat_inv = item.get_bind_matrix().inverted_safe() if item.is_bone else None
612 for frame, values in blen_read_animations_curves_iter(fbx_curves, anim_offset, 0, fps):
613 for v, (fbxprop, channel, _fbx_acdata) in values:
614 if fbxprop == b'Lcl Translation':
615 transform_data.loc[channel] = v
616 elif fbxprop == b'Lcl Rotation':
617 transform_data.rot[channel] = v
618 elif fbxprop == b'Lcl Scaling':
619 transform_data.sca[channel] = v
620 mat, _, _ = blen_read_object_transform_do(transform_data)
622 # compensate for changes in the local matrix during processing
623 if item.anim_compensation_matrix:
624 mat = mat * item.anim_compensation_matrix
626 # apply pre- and post matrix
627 # post-matrix will contain any correction for lights, camera and bone orientation
628 # pre-matrix will contain any correction for a parent's correction matrix or the global matrix
629 if item.pre_matrix:
630 mat = item.pre_matrix * mat
631 if item.post_matrix:
632 mat = mat * item.post_matrix
634 # And now, remove that rest pose matrix from current mat (also in parent space).
635 if restmat_inv:
636 mat = restmat_inv * mat
638 # Now we have a virtual matrix of transform from AnimCurves, we can insert keyframes!
639 loc, rot, sca = mat.decompose()
640 if rot_mode == 'QUATERNION':
641 pass # nothing to do!
642 elif rot_mode == 'AXIS_ANGLE':
643 vec, ang = rot.to_axis_angle()
644 rot = ang, vec.x, vec.y, vec.z
645 else: # Euler
646 rot = rot.to_euler(rot_mode, rot_prev)
647 rot_prev = rot
648 for fc, value in zip(blen_curves, chain(loc, rot, sca)):
649 fc.keyframe_points.insert(frame, value, {'NEEDED', 'FAST'}).interpolation = 'LINEAR'
651 # Since we inserted our keyframes in 'FAST' mode, we have to update the fcurves now.
652 for fc in blen_curves:
653 fc.update()
656 def blen_read_animations(fbx_tmpl_astack, fbx_tmpl_alayer, stacks, scene, anim_offset):
658 Recreate an action per stack/layer/object combinations.
659 Only the first found action is linked to objects, more complex setups are not handled,
660 it's up to user to reproduce them!
662 from bpy.types import ShapeKey
664 actions = {}
665 for as_uuid, ((fbx_asdata, _blen_data), alayers) in stacks.items():
666 stack_name = elem_name_ensure_class(fbx_asdata, b'AnimStack')
667 for al_uuid, ((fbx_aldata, _blen_data), items) in alayers.items():
668 layer_name = elem_name_ensure_class(fbx_aldata, b'AnimLayer')
669 for item, cnodes in items.items():
670 if isinstance(item, ShapeKey):
671 id_data = item.id_data
672 else:
673 id_data = item.bl_obj
674 if id_data is None:
675 continue
676 # Create new action if needed (should always be needed!
677 key = (as_uuid, al_uuid, id_data)
678 action = actions.get(key)
679 if action is None:
680 action_name = "|".join((id_data.name, stack_name, layer_name))
681 actions[key] = action = bpy.data.actions.new(action_name)
682 action.use_fake_user = True
683 # If none yet assigned, assign this action to id_data.
684 if not id_data.animation_data:
685 id_data.animation_data_create()
686 if not id_data.animation_data.action:
687 id_data.animation_data.action = action
688 # And actually populate the action!
689 blen_read_animations_action_item(action, item, cnodes, scene.render.fps, anim_offset)
692 # ----
693 # Mesh
695 def blen_read_geom_layerinfo(fbx_layer):
696 return (
697 elem_find_first_string(fbx_layer, b'Name'),
698 elem_find_first_string_as_bytes(fbx_layer, b'MappingInformationType'),
699 elem_find_first_string_as_bytes(fbx_layer, b'ReferenceInformationType'),
703 def blen_read_geom_array_setattr(generator, blen_data, blen_attr, fbx_data, stride, item_size, descr, xform):
704 """Generic fbx_layer to blen_data setter, generator is expected to yield tuples (ble_idx, fbx_idx)."""
705 max_idx = len(blen_data) - 1
706 print_error = True
708 def check_skip(blen_idx, fbx_idx):
709 nonlocal print_error
710 if fbx_idx < 0: # Negative values mean 'skip'.
711 return True
712 if blen_idx > max_idx:
713 if print_error:
714 print("ERROR: too much data in this layer, compared to elements in mesh, skipping!")
715 print_error = False
716 return True
717 return False
719 if xform is not None:
720 if isinstance(blen_data, list):
721 if item_size == 1:
722 def _process(blend_data, blen_attr, fbx_data, xform, item_size, blen_idx, fbx_idx):
723 blen_data[blen_idx] = xform(fbx_data[fbx_idx])
724 else:
725 def _process(blend_data, blen_attr, fbx_data, xform, item_size, blen_idx, fbx_idx):
726 blen_data[blen_idx] = xform(fbx_data[fbx_idx:fbx_idx + item_size])
727 else:
728 if item_size == 1:
729 def _process(blend_data, blen_attr, fbx_data, xform, item_size, blen_idx, fbx_idx):
730 setattr(blen_data[blen_idx], blen_attr, xform(fbx_data[fbx_idx]))
731 else:
732 def _process(blend_data, blen_attr, fbx_data, xform, item_size, blen_idx, fbx_idx):
733 setattr(blen_data[blen_idx], blen_attr, xform(fbx_data[fbx_idx:fbx_idx + item_size]))
734 else:
735 if isinstance(blen_data, list):
736 if item_size == 1:
737 def _process(blend_data, blen_attr, fbx_data, xform, item_size, blen_idx, fbx_idx):
738 blen_data[blen_idx] = fbx_data[fbx_idx]
739 else:
740 def _process(blend_data, blen_attr, fbx_data, xform, item_size, blen_idx, fbx_idx):
741 blen_data[blen_idx] = fbx_data[fbx_idx:fbx_idx + item_size]
742 else:
743 if item_size == 1:
744 def _process(blend_data, blen_attr, fbx_data, xform, item_size, blen_idx, fbx_idx):
745 setattr(blen_data[blen_idx], blen_attr, fbx_data[fbx_idx])
746 else:
747 def _process(blend_data, blen_attr, fbx_data, xform, item_size, blen_idx, fbx_idx):
748 setattr(blen_data[blen_idx], blen_attr, fbx_data[fbx_idx:fbx_idx + item_size])
750 for blen_idx, fbx_idx in generator:
751 if check_skip(blen_idx, fbx_idx):
752 continue
753 _process(blen_data, blen_attr, fbx_data, xform, item_size, blen_idx, fbx_idx)
756 # generic generators.
757 def blen_read_geom_array_gen_allsame(data_len):
758 return zip(*(range(data_len), (0,) * data_len))
761 def blen_read_geom_array_gen_direct(fbx_data, stride):
762 fbx_data_len = len(fbx_data)
763 return zip(*(range(fbx_data_len // stride), range(0, fbx_data_len, stride)))
766 def blen_read_geom_array_gen_indextodirect(fbx_layer_index, stride):
767 return ((bi, fi * stride) for bi, fi in enumerate(fbx_layer_index))
770 def blen_read_geom_array_gen_direct_looptovert(mesh, fbx_data, stride):
771 fbx_data_len = len(fbx_data) // stride
772 loops = mesh.loops
773 for p in mesh.polygons:
774 for lidx in p.loop_indices:
775 vidx = loops[lidx].vertex_index
776 if vidx < fbx_data_len:
777 yield lidx, vidx * stride
780 # generic error printers.
781 def blen_read_geom_array_error_mapping(descr, fbx_layer_mapping):
782 print("warning layer %r mapping type unsupported: %r" % (descr, fbx_layer_mapping))
785 def blen_read_geom_array_error_ref(descr, fbx_layer_ref):
786 print("warning layer %r ref type unsupported: %r" % (descr, fbx_layer_ref))
789 def blen_read_geom_array_mapped_vert(
790 mesh, blen_data, blen_attr,
791 fbx_layer_data, fbx_layer_index,
792 fbx_layer_mapping, fbx_layer_ref,
793 stride, item_size, descr,
794 xform=None,
796 if fbx_layer_mapping == b'ByVertice':
797 if fbx_layer_ref == b'Direct':
798 assert(fbx_layer_index is None)
799 blen_read_geom_array_setattr(blen_read_geom_array_gen_direct(fbx_layer_data, stride),
800 blen_data, blen_attr, fbx_layer_data, stride, item_size, descr, xform)
801 return True
802 blen_read_geom_array_error_ref(descr, fbx_layer_ref)
803 elif fbx_layer_mapping == b'AllSame':
804 if fbx_layer_ref == b'IndexToDirect':
805 assert(fbx_layer_index is None)
806 blen_read_geom_array_setattr(blen_read_geom_array_gen_allsame(len(blen_data)),
807 blen_data, blen_attr, fbx_layer_data, stride, item_size, descr, xform)
808 return True
809 blen_read_geom_array_error_ref(descr, fbx_layer_ref)
810 else:
811 blen_read_geom_array_error_mapping(descr, fbx_layer_mapping)
813 return False
816 def blen_read_geom_array_mapped_edge(
817 mesh, blen_data, blen_attr,
818 fbx_layer_data, fbx_layer_index,
819 fbx_layer_mapping, fbx_layer_ref,
820 stride, item_size, descr,
821 xform=None,
823 if fbx_layer_mapping == b'ByEdge':
824 if fbx_layer_ref == b'Direct':
825 blen_read_geom_array_setattr(blen_read_geom_array_gen_direct(fbx_layer_data, stride),
826 blen_data, blen_attr, fbx_layer_data, stride, item_size, descr, xform)
827 return True
828 blen_read_geom_array_error_ref(descr, fbx_layer_ref)
829 elif fbx_layer_mapping == b'AllSame':
830 if fbx_layer_ref == b'IndexToDirect':
831 assert(fbx_layer_index is None)
832 blen_read_geom_array_setattr(blen_read_geom_array_gen_allsame(len(blen_data)),
833 blen_data, blen_attr, fbx_layer_data, stride, item_size, descr, xform)
834 return True
835 blen_read_geom_array_error_ref(descr, fbx_layer_ref)
836 else:
837 blen_read_geom_array_error_mapping(descr, fbx_layer_mapping)
839 return False
842 def blen_read_geom_array_mapped_polygon(
843 mesh, blen_data, blen_attr,
844 fbx_layer_data, fbx_layer_index,
845 fbx_layer_mapping, fbx_layer_ref,
846 stride, item_size, descr,
847 xform=None,
849 if fbx_layer_mapping == b'ByPolygon':
850 if fbx_layer_ref == b'IndexToDirect':
851 # XXX Looks like we often get no fbx_layer_index in this case, shall not happen but happens...
852 # We fallback to 'Direct' mapping in this case.
853 #~ assert(fbx_layer_index is not None)
854 if fbx_layer_index is None:
855 blen_read_geom_array_setattr(blen_read_geom_array_gen_direct(fbx_layer_data, stride),
856 blen_data, blen_attr, fbx_layer_data, stride, item_size, descr, xform)
857 else:
858 blen_read_geom_array_setattr(blen_read_geom_array_gen_indextodirect(fbx_layer_index, stride),
859 blen_data, blen_attr, fbx_layer_data, stride, item_size, descr, xform)
860 return True
861 elif fbx_layer_ref == b'Direct':
862 blen_read_geom_array_setattr(blen_read_geom_array_gen_direct(fbx_layer_data, stride),
863 blen_data, blen_attr, fbx_layer_data, stride, item_size, descr, xform)
864 return True
865 blen_read_geom_array_error_ref(descr, fbx_layer_ref)
866 elif fbx_layer_mapping == b'AllSame':
867 if fbx_layer_ref == b'IndexToDirect':
868 assert(fbx_layer_index is None)
869 blen_read_geom_array_setattr(blen_read_geom_array_gen_allsame(len(blen_data)),
870 blen_data, blen_attr, fbx_layer_data, stride, item_size, descr, xform)
871 return True
872 blen_read_geom_array_error_ref(descr, fbx_layer_ref)
873 else:
874 blen_read_geom_array_error_mapping(descr, fbx_layer_mapping)
876 return False
879 def blen_read_geom_array_mapped_polyloop(
880 mesh, blen_data, blen_attr,
881 fbx_layer_data, fbx_layer_index,
882 fbx_layer_mapping, fbx_layer_ref,
883 stride, item_size, descr,
884 xform=None,
886 if fbx_layer_mapping == b'ByPolygonVertex':
887 if fbx_layer_ref == b'IndexToDirect':
888 # XXX Looks like we often get no fbx_layer_index in this case, shall not happen but happens...
889 # We fallback to 'Direct' mapping in this case.
890 #~ assert(fbx_layer_index is not None)
891 if fbx_layer_index is None:
892 blen_read_geom_array_setattr(blen_read_geom_array_gen_direct(fbx_layer_data, stride),
893 blen_data, blen_attr, fbx_layer_data, stride, item_size, descr, xform)
894 else:
895 blen_read_geom_array_setattr(blen_read_geom_array_gen_indextodirect(fbx_layer_index, stride),
896 blen_data, blen_attr, fbx_layer_data, stride, item_size, descr, xform)
897 return True
898 elif fbx_layer_ref == b'Direct':
899 blen_read_geom_array_setattr(blen_read_geom_array_gen_direct(fbx_layer_data, stride),
900 blen_data, blen_attr, fbx_layer_data, stride, item_size, descr, xform)
901 return True
902 blen_read_geom_array_error_ref(descr, fbx_layer_ref)
903 elif fbx_layer_mapping == b'ByVertice':
904 if fbx_layer_ref == b'Direct':
905 assert(fbx_layer_index is None)
906 blen_read_geom_array_setattr(blen_read_geom_array_gen_direct_looptovert(mesh, fbx_layer_data, stride),
907 blen_data, blen_attr, fbx_layer_data, stride, item_size, descr, xform)
908 return True
909 blen_read_geom_array_error_ref(descr, fbx_layer_ref)
910 elif fbx_layer_mapping == b'AllSame':
911 if fbx_layer_ref == b'IndexToDirect':
912 assert(fbx_layer_index is None)
913 blen_read_geom_array_setattr(blen_read_geom_array_gen_allsame(len(blen_data)),
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)
917 else:
918 blen_read_geom_array_error_mapping(descr, fbx_layer_mapping)
920 return False
923 def blen_read_geom_layer_material(fbx_obj, mesh):
924 fbx_layer = elem_find_first(fbx_obj, b'LayerElementMaterial')
926 if fbx_layer is None:
927 return
929 (fbx_layer_name,
930 fbx_layer_mapping,
931 fbx_layer_ref,
932 ) = blen_read_geom_layerinfo(fbx_layer)
934 layer_id = b'Materials'
935 fbx_layer_data = elem_prop_first(elem_find_first(fbx_layer, layer_id))
937 blen_data = mesh.polygons
938 blen_read_geom_array_mapped_polygon(
939 mesh, blen_data, "material_index",
940 fbx_layer_data, None,
941 fbx_layer_mapping, fbx_layer_ref,
942 1, 1, layer_id,
946 def blen_read_geom_layer_uv(fbx_obj, mesh):
947 for layer_id in (b'LayerElementUV',):
948 for fbx_layer in elem_find_iter(fbx_obj, layer_id):
949 # all should be valid
950 (fbx_layer_name,
951 fbx_layer_mapping,
952 fbx_layer_ref,
953 ) = blen_read_geom_layerinfo(fbx_layer)
955 fbx_layer_data = elem_prop_first(elem_find_first(fbx_layer, b'UV'))
956 fbx_layer_index = elem_prop_first(elem_find_first(fbx_layer, b'UVIndex'))
958 uv_tex = mesh.uv_textures.new(name=fbx_layer_name)
959 uv_lay = mesh.uv_layers[-1]
960 blen_data = uv_lay.data
962 # some valid files omit this data
963 if fbx_layer_data is None:
964 print("%r %r missing data" % (layer_id, fbx_layer_name))
965 continue
967 blen_read_geom_array_mapped_polyloop(
968 mesh, blen_data, "uv",
969 fbx_layer_data, fbx_layer_index,
970 fbx_layer_mapping, fbx_layer_ref,
971 2, 2, layer_id,
975 def blen_read_geom_layer_color(fbx_obj, mesh):
976 # almost same as UV's
977 for layer_id in (b'LayerElementColor',):
978 for fbx_layer in elem_find_iter(fbx_obj, layer_id):
979 # all should be valid
980 (fbx_layer_name,
981 fbx_layer_mapping,
982 fbx_layer_ref,
983 ) = blen_read_geom_layerinfo(fbx_layer)
985 fbx_layer_data = elem_prop_first(elem_find_first(fbx_layer, b'Colors'))
986 fbx_layer_index = elem_prop_first(elem_find_first(fbx_layer, b'ColorIndex'))
988 color_lay = mesh.vertex_colors.new(name=fbx_layer_name)
989 blen_data = color_lay.data
991 # some valid files omit this data
992 if fbx_layer_data is None:
993 print("%r %r missing data" % (layer_id, fbx_layer_name))
994 continue
996 # ignore alpha layer (read 4 items into 3)
997 blen_read_geom_array_mapped_polyloop(
998 mesh, blen_data, "color",
999 fbx_layer_data, fbx_layer_index,
1000 fbx_layer_mapping, fbx_layer_ref,
1001 4, 3, layer_id,
1005 def blen_read_geom_layer_smooth(fbx_obj, mesh):
1006 fbx_layer = elem_find_first(fbx_obj, b'LayerElementSmoothing')
1008 if fbx_layer is None:
1009 return False
1011 # all should be valid
1012 (fbx_layer_name,
1013 fbx_layer_mapping,
1014 fbx_layer_ref,
1015 ) = blen_read_geom_layerinfo(fbx_layer)
1017 layer_id = b'Smoothing'
1018 fbx_layer_data = elem_prop_first(elem_find_first(fbx_layer, layer_id))
1020 # udk has 'Direct' mapped, with no Smoothing, not sure why, but ignore these
1021 if fbx_layer_data is None:
1022 return False
1024 if fbx_layer_mapping == b'ByEdge':
1025 # some models have bad edge data, we cant use this info...
1026 if not mesh.edges:
1027 print("warning skipping sharp edges data, no valid edges...")
1028 return False
1030 blen_data = mesh.edges
1031 blen_read_geom_array_mapped_edge(
1032 mesh, blen_data, "use_edge_sharp",
1033 fbx_layer_data, None,
1034 fbx_layer_mapping, fbx_layer_ref,
1035 1, 1, layer_id,
1036 xform=lambda s: not s,
1038 # We only set sharp edges here, not face smoothing itself...
1039 mesh.use_auto_smooth = True
1040 mesh.show_edge_sharp = True
1041 return False
1042 elif fbx_layer_mapping == b'ByPolygon':
1043 blen_data = mesh.polygons
1044 return blen_read_geom_array_mapped_polygon(
1045 mesh, blen_data, "use_smooth",
1046 fbx_layer_data, None,
1047 fbx_layer_mapping, fbx_layer_ref,
1048 1, 1, layer_id,
1049 xform=lambda s: (s != 0), # smoothgroup bitflags, treat as booleans for now
1051 else:
1052 print("warning layer %r mapping type unsupported: %r" % (fbx_layer.id, fbx_layer_mapping))
1053 return False
1056 def blen_read_geom_layer_normal(fbx_obj, mesh, xform=None):
1057 fbx_layer = elem_find_first(fbx_obj, b'LayerElementNormal')
1059 if fbx_layer is None:
1060 return False
1062 (fbx_layer_name,
1063 fbx_layer_mapping,
1064 fbx_layer_ref,
1065 ) = blen_read_geom_layerinfo(fbx_layer)
1067 layer_id = b'Normals'
1068 fbx_layer_data = elem_prop_first(elem_find_first(fbx_layer, layer_id))
1069 fbx_layer_index = elem_prop_first(elem_find_first(fbx_layer, b'NormalsIndex'))
1071 # try loops, then vertices.
1072 tries = ((mesh.loops, False, blen_read_geom_array_mapped_polyloop),
1073 (mesh.polygons, True, blen_read_geom_array_mapped_polygon),
1074 (mesh.vertices, True, blen_read_geom_array_mapped_vert))
1075 for blen_data, is_fake, func in tries:
1076 bdata = [None] * len(blen_data) if is_fake else blen_data
1077 if func(mesh, bdata, "normal",
1078 fbx_layer_data, fbx_layer_index, fbx_layer_mapping, fbx_layer_ref, 3, 3, layer_id, xform):
1079 if blen_data is mesh.polygons:
1080 for pidx, p in enumerate(mesh.polygons):
1081 for lidx in range(p.loop_start, p.loop_start + p.loop_total):
1082 mesh.loops[lidx].normal[:] = bdata[pidx]
1083 elif blen_data is mesh.vertices:
1084 # We have to copy vnors to lnors! Far from elegant, but simple.
1085 for l in mesh.loops:
1086 l.normal[:] = bdata[l.vertex_index]
1087 return True
1088 return False
1091 def blen_read_geom(fbx_tmpl, fbx_obj, settings):
1092 from itertools import chain
1093 import array
1095 # Vertices are in object space, but we are post-multiplying all transforms with the inverse of the
1096 # global matrix, so we need to apply the global matrix to the vertices to get the correct result.
1097 geom_mat_co = settings.global_matrix if settings.bake_space_transform else None
1098 # We need to apply the inverse transpose of the global matrix when transforming normals.
1099 geom_mat_no = Matrix(settings.global_matrix_inv_transposed) if settings.bake_space_transform else None
1100 if geom_mat_no is not None:
1101 # Remove translation & scaling!
1102 geom_mat_no.translation = Vector()
1103 geom_mat_no.normalize()
1105 # TODO, use 'fbx_tmpl'
1106 elem_name_utf8 = elem_name_ensure_class(fbx_obj, b'Geometry')
1108 fbx_verts = elem_prop_first(elem_find_first(fbx_obj, b'Vertices'))
1109 fbx_polys = elem_prop_first(elem_find_first(fbx_obj, b'PolygonVertexIndex'))
1110 fbx_edges = elem_prop_first(elem_find_first(fbx_obj, b'Edges'))
1112 if geom_mat_co is not None:
1113 def _vcos_transformed_gen(raw_cos, m=None):
1114 # Note: we could most likely get much better performances with numpy, but will leave this as TODO for now.
1115 return chain(*(m * Vector(v) for v in zip(*(iter(raw_cos),) * 3)))
1116 fbx_verts = array.array(fbx_verts.typecode, _vcos_transformed_gen(fbx_verts, geom_mat_co))
1118 if fbx_verts is None:
1119 fbx_verts = ()
1120 if fbx_polys is None:
1121 fbx_polys = ()
1123 mesh = bpy.data.meshes.new(name=elem_name_utf8)
1124 mesh.vertices.add(len(fbx_verts) // 3)
1125 mesh.vertices.foreach_set("co", fbx_verts)
1127 if fbx_polys:
1128 mesh.loops.add(len(fbx_polys))
1129 poly_loop_starts = []
1130 poly_loop_totals = []
1131 poly_loop_prev = 0
1132 for i, l in enumerate(mesh.loops):
1133 index = fbx_polys[i]
1134 if index < 0:
1135 poly_loop_starts.append(poly_loop_prev)
1136 poly_loop_totals.append((i - poly_loop_prev) + 1)
1137 poly_loop_prev = i + 1
1138 index ^= -1
1139 l.vertex_index = index
1141 mesh.polygons.add(len(poly_loop_starts))
1142 mesh.polygons.foreach_set("loop_start", poly_loop_starts)
1143 mesh.polygons.foreach_set("loop_total", poly_loop_totals)
1145 blen_read_geom_layer_material(fbx_obj, mesh)
1146 blen_read_geom_layer_uv(fbx_obj, mesh)
1147 blen_read_geom_layer_color(fbx_obj, mesh)
1149 if fbx_edges:
1150 # edges in fact index the polygons (NOT the vertices)
1151 import array
1152 tot_edges = len(fbx_edges)
1153 edges_conv = array.array('i', [0]) * (tot_edges * 2)
1155 edge_index = 0
1156 for i in fbx_edges:
1157 e_a = fbx_polys[i]
1158 if e_a >= 0:
1159 e_b = fbx_polys[i + 1]
1160 if e_b < 0:
1161 e_b ^= -1
1162 else:
1163 # Last index of polygon, wrap back to the start.
1165 # ideally we wouldn't have to search back,
1166 # but it should only be 2-3 iterations.
1167 j = i - 1
1168 while j >= 0 and fbx_polys[j] >= 0:
1169 j -= 1
1170 e_a ^= -1
1171 e_b = fbx_polys[j + 1]
1173 edges_conv[edge_index] = e_a
1174 edges_conv[edge_index + 1] = e_b
1175 edge_index += 2
1177 mesh.edges.add(tot_edges)
1178 mesh.edges.foreach_set("vertices", edges_conv)
1180 # must be after edge, face loading.
1181 ok_smooth = blen_read_geom_layer_smooth(fbx_obj, mesh)
1183 ok_normals = False
1184 if settings.use_custom_normals:
1185 # Note: we store 'temp' normals in loops, since validate() may alter final mesh,
1186 # we can only set custom lnors *after* calling it.
1187 mesh.create_normals_split()
1188 if geom_mat_no is None:
1189 ok_normals = blen_read_geom_layer_normal(fbx_obj, mesh)
1190 else:
1191 def nortrans(v):
1192 return geom_mat_no * Vector(v)
1193 ok_normals = blen_read_geom_layer_normal(fbx_obj, mesh, nortrans)
1195 mesh.validate(clean_customdata=False) # *Very* important to not remove lnors here!
1197 if ok_normals:
1198 clnors = array.array('f', [0.0] * (len(mesh.loops) * 3))
1199 mesh.loops.foreach_get("normal", clnors)
1201 if not ok_smooth:
1202 mesh.polygons.foreach_set("use_smooth", [True] * len(mesh.polygons))
1203 ok_smooth = True
1205 mesh.normals_split_custom_set(tuple(zip(*(iter(clnors),) * 3)))
1206 mesh.use_auto_smooth = True
1207 mesh.show_edge_sharp = True
1208 else:
1209 mesh.calc_normals()
1211 if settings.use_custom_normals:
1212 mesh.free_normals_split()
1214 if not ok_smooth:
1215 mesh.polygons.foreach_set("use_smooth", [True] * len(mesh.polygons))
1217 if settings.use_custom_props:
1218 blen_read_custom_properties(fbx_obj, mesh, settings)
1220 return mesh
1223 def blen_read_shape(fbx_tmpl, fbx_sdata, fbx_bcdata, meshes, scene):
1224 elem_name_utf8 = elem_name_ensure_class(fbx_sdata, b'Geometry')
1225 indices = elem_prop_first(elem_find_first(fbx_sdata, b'Indexes'), default=())
1226 dvcos = tuple(co for co in zip(*[iter(elem_prop_first(elem_find_first(fbx_sdata, b'Vertices'), default=()))] * 3))
1227 # We completely ignore normals here!
1228 weight = elem_prop_first(elem_find_first(fbx_bcdata, b'DeformPercent'), default=100.0) / 100.0
1229 vgweights = tuple(vgw / 100.0 for vgw in elem_prop_first(elem_find_first(fbx_bcdata, b'FullWeights'), default=()))
1231 # Special case, in case all weights are the same, FullWeight can have only one element - *sigh!*
1232 nbr_indices = len(indices)
1233 if len(vgweights) == 1 and nbr_indices > 1:
1234 vgweights = (vgweights[0],) * nbr_indices
1236 assert(len(vgweights) == nbr_indices == len(dvcos))
1237 create_vg = bool(set(vgweights) - {1.0})
1239 keyblocks = []
1241 for me, objects in meshes:
1242 vcos = tuple((idx, me.vertices[idx].co + Vector(dvco)) for idx, dvco in zip(indices, dvcos))
1243 objects = list({node.bl_obj for node in objects})
1244 assert(objects)
1246 if me.shape_keys is None:
1247 objects[0].shape_key_add(name="Basis", from_mix=False)
1248 objects[0].shape_key_add(name=elem_name_utf8, from_mix=False)
1249 me.shape_keys.use_relative = True # Should already be set as such.
1251 kb = me.shape_keys.key_blocks[elem_name_utf8]
1252 for idx, co in vcos:
1253 kb.data[idx].co[:] = co
1254 kb.value = weight
1256 # Add vgroup if necessary.
1257 if create_vg:
1258 add_vgroup_to_objects(indices, vgweights, elem_name_utf8, objects)
1259 kb.vertex_group = elem_name_utf8
1261 keyblocks.append(kb)
1263 return keyblocks
1266 # --------
1267 # Material
1269 def blen_read_material(fbx_tmpl, fbx_obj, settings):
1270 elem_name_utf8 = elem_name_ensure_class(fbx_obj, b'Material')
1272 cycles_material_wrap_map = settings.cycles_material_wrap_map
1273 ma = bpy.data.materials.new(name=elem_name_utf8)
1275 const_color_white = 1.0, 1.0, 1.0
1277 fbx_props = (elem_find_first(fbx_obj, b'Properties70'),
1278 elem_find_first(fbx_tmpl, b'Properties70', fbx_elem_nil))
1279 assert(fbx_props[0] is not None)
1281 ma_diff = elem_props_get_color_rgb(fbx_props, b'DiffuseColor', const_color_white)
1282 ma_spec = elem_props_get_color_rgb(fbx_props, b'SpecularColor', const_color_white)
1283 ma_alpha = elem_props_get_number(fbx_props, b'Opacity', 1.0)
1284 ma_spec_intensity = ma.specular_intensity = elem_props_get_number(fbx_props, b'SpecularFactor', 0.25) * 2.0
1285 ma_spec_hardness = elem_props_get_number(fbx_props, b'Shininess', 9.6)
1286 ma_refl_factor = elem_props_get_number(fbx_props, b'ReflectionFactor', 0.0)
1287 ma_refl_color = elem_props_get_color_rgb(fbx_props, b'ReflectionColor', const_color_white)
1289 if settings.use_cycles:
1290 from . import cycles_shader_compat
1291 # viewport color
1292 ma.diffuse_color = ma_diff
1294 ma_wrap = cycles_shader_compat.CyclesShaderWrapper(ma)
1295 ma_wrap.diffuse_color_set(ma_diff)
1296 ma_wrap.specular_color_set([c * ma_spec_intensity for c in ma_spec])
1297 ma_wrap.hardness_value_set(((ma_spec_hardness + 3.0) / 5.0) - 0.65)
1298 ma_wrap.alpha_value_set(ma_alpha)
1299 ma_wrap.reflect_factor_set(ma_refl_factor)
1300 ma_wrap.reflect_color_set(ma_refl_color)
1302 cycles_material_wrap_map[ma] = ma_wrap
1303 else:
1304 # TODO, number BumpFactor isnt used yet
1305 ma.diffuse_color = ma_diff
1306 ma.specular_color = ma_spec
1307 ma.alpha = ma_alpha
1308 ma.specular_intensity = ma_spec_intensity
1309 ma.specular_hardness = ma_spec_hardness * 5.10 + 1.0
1311 if ma_refl_factor != 0.0:
1312 ma.raytrace_mirror.use = True
1313 ma.raytrace_mirror.reflect_factor = ma_refl_factor
1314 ma.mirror_color = ma_refl_color
1316 if settings.use_custom_props:
1317 blen_read_custom_properties(fbx_obj, ma, settings)
1319 return ma
1322 # -------
1323 # Image & Texture
1325 def blen_read_texture_image(fbx_tmpl, fbx_obj, basedir, settings):
1326 import os
1327 from bpy_extras import image_utils
1329 def pack_data_from_content(image, fbx_obj):
1330 data = elem_find_first_bytes(fbx_obj, b'Content')
1331 if (data):
1332 data_len = len(data)
1333 if (data_len):
1334 image.pack(data=data, data_len=data_len)
1336 elem_name_utf8 = elem_name_ensure_classes(fbx_obj, {b'Texture', b'Video'})
1338 image_cache = settings.image_cache
1340 # Yet another beautiful logic demonstration by Master FBX:
1341 # * RelativeFilename in both Video and Texture nodes.
1342 # * FileName in texture nodes.
1343 # * Filename in video nodes.
1344 # Aaaaaaaarrrrrrrrgggggggggggg!!!!!!!!!!!!!!
1345 filepath = elem_find_first_string(fbx_obj, b'RelativeFilename')
1346 if filepath:
1347 filepath = os.path.join(basedir, filepath)
1348 else:
1349 filepath = elem_find_first_string(fbx_obj, b'FileName')
1350 if not filepath:
1351 filepath = elem_find_first_string(fbx_obj, b'Filename')
1352 if not filepath:
1353 print("Error, could not find any file path in ", fbx_obj)
1354 print(" Falling back to: ", elem_name_utf8)
1355 filepath = elem_name_utf8
1356 else :
1357 filepath = filepath.replace('\\', '/') if (os.sep == '/') else filepath.replace('/', '\\')
1359 image = image_cache.get(filepath)
1360 if image is not None:
1361 # Data is only embedded once, we may have already created the image but still be missing its data!
1362 if not image.has_data:
1363 pack_data_from_content(image, fbx_obj)
1364 return image
1366 image = image_utils.load_image(
1367 filepath,
1368 dirname=basedir,
1369 place_holder=True,
1370 recursive=settings.use_image_search,
1373 # Try to use embedded data, if available!
1374 pack_data_from_content(image, fbx_obj)
1376 image_cache[filepath] = image
1377 # name can be ../a/b/c
1378 image.name = os.path.basename(elem_name_utf8)
1380 if settings.use_custom_props:
1381 blen_read_custom_properties(fbx_obj, image, settings)
1383 return image
1386 def blen_read_camera(fbx_tmpl, fbx_obj, global_scale):
1387 # meters to inches
1388 M2I = 0.0393700787
1390 elem_name_utf8 = elem_name_ensure_class(fbx_obj, b'NodeAttribute')
1392 fbx_props = (elem_find_first(fbx_obj, b'Properties70'),
1393 elem_find_first(fbx_tmpl, b'Properties70', fbx_elem_nil))
1394 assert(fbx_props[0] is not None)
1396 camera = bpy.data.cameras.new(name=elem_name_utf8)
1398 camera.type = 'ORTHO' if elem_props_get_enum(fbx_props, b'CameraProjectionType', 0) == 1 else 'PERSP'
1400 camera.lens = elem_props_get_number(fbx_props, b'FocalLength', 35.0)
1401 camera.sensor_width = elem_props_get_number(fbx_props, b'FilmWidth', 32.0 * M2I) / M2I
1402 camera.sensor_height = elem_props_get_number(fbx_props, b'FilmHeight', 32.0 * M2I) / M2I
1404 camera.ortho_scale = elem_props_get_number(fbx_props, b'OrthoZoom', 1.0)
1406 filmaspect = camera.sensor_width / camera.sensor_height
1407 # film offset
1408 camera.shift_x = elem_props_get_number(fbx_props, b'FilmOffsetX', 0.0) / (M2I * camera.sensor_width)
1409 camera.shift_y = elem_props_get_number(fbx_props, b'FilmOffsetY', 0.0) / (M2I * camera.sensor_height * filmaspect)
1411 camera.clip_start = elem_props_get_number(fbx_props, b'NearPlane', 0.01) * global_scale
1412 camera.clip_end = elem_props_get_number(fbx_props, b'FarPlane', 100.0) * global_scale
1414 return camera
1417 def blen_read_light(fbx_tmpl, fbx_obj, global_scale):
1418 import math
1419 elem_name_utf8 = elem_name_ensure_class(fbx_obj, b'NodeAttribute')
1421 fbx_props = (elem_find_first(fbx_obj, b'Properties70'),
1422 elem_find_first(fbx_tmpl, b'Properties70', fbx_elem_nil))
1423 # rare
1424 if fbx_props[0] is None:
1425 lamp = bpy.data.lamps.new(name=elem_name_utf8, type='POINT')
1426 return lamp
1428 light_type = {
1429 0: 'POINT',
1430 1: 'SUN',
1431 2: 'SPOT'}.get(elem_props_get_enum(fbx_props, b'LightType', 0), 'POINT')
1433 lamp = bpy.data.lamps.new(name=elem_name_utf8, type=light_type)
1435 if light_type == 'SPOT':
1436 spot_size = elem_props_get_number(fbx_props, b'OuterAngle', None)
1437 if spot_size is None:
1438 # Deprecated.
1439 spot_size = elem_props_get_number(fbx_props, b'Cone angle', 45.0)
1440 lamp.spot_size = math.radians(spot_size)
1442 spot_blend = elem_props_get_number(fbx_props, b'InnerAngle', None)
1443 if spot_blend is None:
1444 # Deprecated.
1445 spot_blend = elem_props_get_number(fbx_props, b'HotSpot', 45.0)
1446 lamp.spot_blend = 1.0 - (spot_blend / spot_size)
1448 # TODO, cycles
1449 lamp.color = elem_props_get_color_rgb(fbx_props, b'Color', (1.0, 1.0, 1.0))
1450 lamp.energy = elem_props_get_number(fbx_props, b'Intensity', 100.0) / 100.0
1451 lamp.distance = elem_props_get_number(fbx_props, b'DecayStart', 25.0) * global_scale
1452 lamp.shadow_method = ('RAY_SHADOW' if elem_props_get_bool(fbx_props, b'CastShadow', True) else 'NOSHADOW')
1453 lamp.shadow_color = elem_props_get_color_rgb(fbx_props, b'ShadowColor', (0.0, 0.0, 0.0))
1455 return lamp
1458 # ### Import Utility class
1459 class FbxImportHelperNode:
1461 Temporary helper node to store a hierarchy of fbxNode objects before building Objects, Armatures and Bones.
1462 It tries to keep the correction data in one place so it can be applied consistently to the imported data.
1465 __slots__ = (
1466 '_parent', 'anim_compensation_matrix', 'armature_setup', 'armature', 'bind_matrix',
1467 'bl_bone', 'bl_data', 'bl_obj', 'bone_child_matrix', 'children', 'clusters',
1468 'fbx_elem', 'fbx_name', 'fbx_transform_data', 'fbx_type',
1469 'is_armature', 'has_bone_children', 'is_bone', 'is_root', 'is_leaf',
1470 'matrix', 'matrix_as_parent', 'matrix_geom', 'meshes', 'post_matrix', 'pre_matrix')
1472 def __init__(self, fbx_elem, bl_data, fbx_transform_data, is_bone):
1473 self.fbx_name = elem_name_ensure_class(fbx_elem, b'Model') if fbx_elem else 'Unknown'
1474 self.fbx_type = fbx_elem.props[2] if fbx_elem else None
1475 self.fbx_elem = fbx_elem
1476 self.bl_obj = None
1477 self.bl_data = bl_data
1478 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!)
1479 self.fbx_transform_data = fbx_transform_data
1480 self.is_root = False
1481 self.is_bone = is_bone
1482 self.is_armature = False
1483 self.armature = None # For bones only, relevant armature node.
1484 self.has_bone_children = False # True if the hierarchy below this node contains bones, important to support mixed hierarchies.
1485 self.is_leaf = False # True for leaf-bones added to the end of some bone chains to set the lengths.
1486 self.pre_matrix = None # correction matrix that needs to be applied before the FBX transform
1487 self.bind_matrix = None # for bones this is the matrix used to bind to the skin
1488 if fbx_transform_data:
1489 self.matrix, self.matrix_as_parent, self.matrix_geom = blen_read_object_transform_do(fbx_transform_data)
1490 else:
1491 self.matrix, self.matrix_as_parent, self.matrix_geom = (None, None, None)
1492 self.post_matrix = None # correction matrix that needs to be applied after the FBX transform
1493 self.bone_child_matrix = None # Objects attached to a bone end not the beginning, this matrix corrects for that
1494 self.anim_compensation_matrix = None # a mesh moved in the hierarchy may have a different local matrix. This compensates animations for this.
1496 self.meshes = None # List of meshes influenced by this bone.
1497 self.clusters = [] # Deformer Cluster nodes
1498 self.armature_setup = {} # mesh and armature matrix when the mesh was bound
1500 self._parent = None
1501 self.children = []
1503 @property
1504 def parent(self):
1505 return self._parent
1507 @parent.setter
1508 def parent(self, value):
1509 if self._parent is not None:
1510 self._parent.children.remove(self)
1511 self._parent = value
1512 if self._parent is not None:
1513 self._parent.children.append(self)
1515 @property
1516 def ignore(self):
1517 # Separating leaf status from ignore status itself.
1518 # Currently they are equivalent, but this may change in future.
1519 return self.is_leaf
1521 def __repr__(self):
1522 if self.fbx_elem:
1523 return self.fbx_elem.props[1].decode()
1524 else:
1525 return "None"
1527 def print_info(self, indent=0):
1528 print(" " * indent + (self.fbx_name if self.fbx_name else "(Null)")
1529 + ("[root]" if self.is_root else "")
1530 + ("[leaf]" if self.is_leaf else "")
1531 + ("[ignore]" if self.ignore else "")
1532 + ("[armature]" if self.is_armature else "")
1533 + ("[bone]" if self.is_bone else "")
1534 + ("[HBC]" if self.has_bone_children else "")
1536 for c in self.children:
1537 c.print_info(indent + 1)
1539 def mark_leaf_bones(self):
1540 if self.is_bone and len(self.children) == 1:
1541 child = self.children[0]
1542 if child.is_bone and len(child.children) == 0:
1543 child.is_leaf = True
1544 for child in self.children:
1545 child.mark_leaf_bones()
1547 def do_bake_transform(self, settings):
1548 return (settings.bake_space_transform and self.fbx_type in (b'Mesh', b'Null') and
1549 not self.is_armature and not self.is_bone)
1551 def find_correction_matrix(self, settings, parent_correction_inv=None):
1552 from bpy_extras.io_utils import axis_conversion
1554 if self.parent and (self.parent.is_root or self.parent.do_bake_transform(settings)):
1555 self.pre_matrix = settings.global_matrix
1557 if parent_correction_inv:
1558 self.pre_matrix = parent_correction_inv * (self.pre_matrix if self.pre_matrix else Matrix())
1560 correction_matrix = None
1562 if self.is_bone:
1563 if settings.automatic_bone_orientation:
1564 # find best orientation to align bone with
1565 bone_children = tuple(child for child in self.children if child.is_bone)
1566 if len(bone_children) == 0:
1567 # no children, inherit the correction from parent (if possible)
1568 if self.parent and self.parent.is_bone:
1569 correction_matrix = parent_correction_inv.inverted() if parent_correction_inv else None
1570 else:
1571 # else find how best to rotate the bone to align the Y axis with the children
1572 best_axis = (1, 0, 0)
1573 if len(bone_children) == 1:
1574 vec = bone_children[0].get_bind_matrix().to_translation()
1575 best_axis = Vector((0, 0, 1 if vec[2] >= 0 else -1))
1576 if abs(vec[0]) > abs(vec[1]):
1577 if abs(vec[0]) > abs(vec[2]):
1578 best_axis = Vector((1 if vec[0] >= 0 else -1, 0, 0))
1579 elif abs(vec[1]) > abs(vec[2]):
1580 best_axis = Vector((0, 1 if vec[1] >= 0 else -1, 0))
1581 else:
1582 # get the child directions once because they may be checked several times
1583 child_locs = (child.get_bind_matrix().to_translation() for child in bone_children)
1584 child_locs = tuple(loc.normalized() for loc in child_locs if loc.magnitude > 0.0)
1586 # I'm not sure which one I like better...
1587 if False:
1588 best_angle = -1.0
1589 for i in range(6):
1590 a = i // 2
1591 s = -1 if i % 2 == 1 else 1
1592 test_axis = Vector((s if a == 0 else 0, s if a == 1 else 0, s if a == 2 else 0))
1594 # find max angle to children
1595 max_angle = 1.0
1596 for loc in child_locs:
1597 max_angle = min(max_angle, test_axis.dot(loc))
1599 # is it better than the last one?
1600 if best_angle < max_angle:
1601 best_angle = max_angle
1602 best_axis = test_axis
1603 else:
1604 best_angle = -1.0
1605 for vec in child_locs:
1606 test_axis = Vector((0, 0, 1 if vec[2] >= 0 else -1))
1607 if abs(vec[0]) > abs(vec[1]):
1608 if abs(vec[0]) > abs(vec[2]):
1609 test_axis = Vector((1 if vec[0] >= 0 else -1, 0, 0))
1610 elif abs(vec[1]) > abs(vec[2]):
1611 test_axis = Vector((0, 1 if vec[1] >= 0 else -1, 0))
1613 # find max angle to children
1614 max_angle = 1.0
1615 for loc in child_locs:
1616 max_angle = min(max_angle, test_axis.dot(loc))
1618 # is it better than the last one?
1619 if best_angle < max_angle:
1620 best_angle = max_angle
1621 best_axis = test_axis
1623 # convert best_axis to axis string
1624 to_up = 'Z' if best_axis[2] >= 0 else '-Z'
1625 if abs(best_axis[0]) > abs(best_axis[1]):
1626 if abs(best_axis[0]) > abs(best_axis[2]):
1627 to_up = 'X' if best_axis[0] >= 0 else '-X'
1628 elif abs(best_axis[1]) > abs(best_axis[2]):
1629 to_up = 'Y' if best_axis[1] >= 0 else '-Y'
1630 to_forward = 'X' if to_up not in {'X', '-X'} else 'Y'
1632 # Build correction matrix
1633 if (to_up, to_forward) != ('Y', 'X'):
1634 correction_matrix = axis_conversion(from_forward='X',
1635 from_up='Y',
1636 to_forward=to_forward,
1637 to_up=to_up,
1638 ).to_4x4()
1639 else:
1640 correction_matrix = settings.bone_correction_matrix
1641 else:
1642 # camera and light can be hard wired
1643 if self.fbx_type == b'Camera':
1644 correction_matrix = MAT_CONVERT_CAMERA
1645 elif self.fbx_type == b'Light':
1646 correction_matrix = MAT_CONVERT_LAMP
1648 self.post_matrix = correction_matrix
1650 if self.do_bake_transform(settings):
1651 self.post_matrix = settings.global_matrix_inv * (self.post_matrix if self.post_matrix else Matrix())
1653 # process children
1654 correction_matrix_inv = correction_matrix.inverted_safe() if correction_matrix else None
1655 for child in self.children:
1656 child.find_correction_matrix(settings, correction_matrix_inv)
1658 def find_armature_bones(self, armature):
1659 for child in self.children:
1660 if child.is_bone:
1661 child.armature = armature
1662 child.find_armature_bones(armature)
1664 def find_armatures(self):
1665 needs_armature = False
1666 for child in self.children:
1667 if child.is_bone:
1668 needs_armature = True
1669 break
1670 if needs_armature:
1671 if self.fbx_type in {b'Null', b'Root'}:
1672 # if empty then convert into armature
1673 self.is_armature = True
1674 armature = self
1675 else:
1676 # otherwise insert a new node
1677 armature = FbxImportHelperNode(None, None, None, False)
1678 armature.fbx_name = "Armature"
1679 armature.is_armature = True
1681 for child in self.children:
1682 if child.is_bone:
1683 child.parent = armature
1685 armature.parent = self
1687 armature.find_armature_bones(armature)
1689 for child in self.children:
1690 if child.is_armature or child.is_bone:
1691 continue
1692 child.find_armatures()
1694 def find_bone_children(self):
1695 has_bone_children = False
1696 for child in self.children:
1697 has_bone_children |= child.find_bone_children()
1698 self.has_bone_children = has_bone_children
1699 return self.is_bone or has_bone_children
1701 def find_fake_bones(self, in_armature=False):
1702 if in_armature and not self.is_bone and self.has_bone_children:
1703 self.is_bone = True
1704 # if we are not a null node we need an intermediate node for the data
1705 if self.fbx_type not in {b'Null', b'Root'}:
1706 node = FbxImportHelperNode(self.fbx_elem, self.bl_data, None, False)
1707 self.fbx_elem = None
1708 self.bl_data = None
1710 # transfer children
1711 for child in self.children:
1712 if child.is_bone or child.has_bone_children:
1713 continue
1714 child.parent = node
1716 # attach to parent
1717 node.parent = self
1719 if self.is_armature:
1720 in_armature = True
1721 for child in self.children:
1722 child.find_fake_bones(in_armature)
1724 def get_world_matrix_as_parent(self):
1725 matrix = self.parent.get_world_matrix_as_parent() if self.parent else Matrix()
1726 if self.matrix_as_parent:
1727 matrix = matrix * self.matrix_as_parent
1728 return matrix
1730 def get_world_matrix(self):
1731 matrix = self.parent.get_world_matrix_as_parent() if self.parent else Matrix()
1732 if self.matrix:
1733 matrix = matrix * self.matrix
1734 return matrix
1736 def get_matrix(self):
1737 matrix = self.matrix if self.matrix else Matrix()
1738 if self.pre_matrix:
1739 matrix = self.pre_matrix * matrix
1740 if self.post_matrix:
1741 matrix = matrix * self.post_matrix
1742 return matrix
1744 def get_bind_matrix(self):
1745 matrix = self.bind_matrix if self.bind_matrix else Matrix()
1746 if self.pre_matrix:
1747 matrix = self.pre_matrix * matrix
1748 if self.post_matrix:
1749 matrix = matrix * self.post_matrix
1750 return matrix
1752 def make_bind_pose_local(self, parent_matrix=None):
1753 if parent_matrix is None:
1754 parent_matrix = Matrix()
1756 if self.bind_matrix:
1757 bind_matrix = parent_matrix.inverted_safe() * self.bind_matrix
1758 else:
1759 bind_matrix = self.matrix.copy() if self.matrix else None
1761 self.bind_matrix = bind_matrix
1762 if bind_matrix:
1763 parent_matrix = parent_matrix * bind_matrix
1765 for child in self.children:
1766 child.make_bind_pose_local(parent_matrix)
1768 def collect_skeleton_meshes(self, meshes):
1769 for _, m in self.clusters:
1770 meshes.update(m)
1771 for child in self.children:
1772 child.collect_skeleton_meshes(meshes)
1774 def collect_armature_meshes(self):
1775 if self.is_armature:
1776 armature_matrix_inv = self.get_world_matrix().inverted_safe()
1778 meshes = set()
1779 for child in self.children:
1780 child.collect_skeleton_meshes(meshes)
1781 for m in meshes:
1782 old_matrix = m.matrix
1783 m.matrix = armature_matrix_inv * m.get_world_matrix()
1784 m.anim_compensation_matrix = old_matrix.inverted_safe() * m.matrix
1785 m.parent = self
1786 self.meshes = meshes
1787 else:
1788 for child in self.children:
1789 child.collect_armature_meshes()
1791 def build_skeleton(self, arm, parent_matrix, parent_bone_size=1, force_connect_children=False):
1792 def child_connect(par_bone, child_bone, child_head, connect_ctx):
1793 # child_bone or child_head may be None.
1794 force_connect_children, connected = connect_ctx
1795 if child_bone is not None:
1796 child_bone.parent = par_bone
1797 child_head = child_bone.head
1799 if similar_values_iter(par_bone.tail, child_head):
1800 if child_bone is not None:
1801 child_bone.use_connect = True
1802 # Disallow any force-connection at this level from now on, since that child was 'really'
1803 # connected, we do not want to move current bone's tail anymore!
1804 connected = None
1805 elif force_connect_children and connected is not None:
1806 # We only store position where tail of par_bone should be in the end.
1807 # Actual tail moving and force connection of compatible child bones will happen
1808 # once all have been checked.
1809 if connected is ...:
1810 connected = ([child_head.copy(), 1], [child_bone] if child_bone is not None else [])
1811 else:
1812 connected[0][0] += child_head
1813 connected[0][1] += 1
1814 if child_bone is not None:
1815 connected[1].append(child_bone)
1816 connect_ctx[1] = connected
1818 def child_connect_finalize(par_bone, connect_ctx):
1819 force_connect_children, connected = connect_ctx
1820 # Do nothing if force connection is not enabled!
1821 if force_connect_children and connected is not None and connected is not ...:
1822 # Here again we have to be wary about zero-length bones!!!
1823 par_tail = connected[0][0] / connected[0][1]
1824 if (par_tail - par_bone.head).magnitude < 1e-2:
1825 par_bone_vec = (par_bone.tail - par_bone.head).normalized()
1826 par_tail = par_bone.head + par_bone_vec * 0.01
1827 par_bone.tail = par_tail
1828 for child_bone in connected[1]:
1829 if similar_values_iter(par_tail, child_bone.head):
1830 child_bone.use_connect = True
1832 # Create the (edit)bone.
1833 bone = arm.bl_data.edit_bones.new(name=self.fbx_name)
1834 bone.select = True
1835 self.bl_obj = arm.bl_obj
1836 self.bl_data = arm.bl_data
1837 self.bl_bone = bone.name # Could be different from the FBX name!
1839 # get average distance to children
1840 bone_size = 0.0
1841 bone_count = 0
1842 for child in self.children:
1843 if child.is_bone:
1844 bone_size += child.get_bind_matrix().to_translation().magnitude
1845 bone_count += 1
1846 if bone_count > 0:
1847 bone_size /= bone_count
1848 else:
1849 bone_size = parent_bone_size
1851 # So that our bone gets its final length, but still Y-aligned in armature space.
1852 # 0-length bones are automatically collapsed into their parent when you leave edit mode,
1853 # so this enforces a minimum length.
1854 bone_tail = Vector((0.0, 1.0, 0.0)) * max(0.01, bone_size)
1855 bone.tail = bone_tail
1857 # And rotate/move it to its final "rest pose".
1858 bone_matrix = parent_matrix * self.get_bind_matrix().normalized()
1860 bone.matrix = bone_matrix
1862 # Correction for children attached to a bone. FBX expects to attach to the head of a bone,
1863 # while Blender attaches to the tail.
1864 self.bone_child_matrix = Matrix.Translation(-bone_tail)
1866 connect_ctx = [force_connect_children, ...]
1867 for child in self.children:
1868 if child.is_leaf and force_connect_children:
1869 # Arggggggggggggggggg! We do not want to create this bone, but we need its 'virtual head' location
1870 # to orient current one!!!
1871 child_head = (bone_matrix * child.get_bind_matrix().normalized()).translation
1872 child_connect(bone, None, child_head, connect_ctx)
1873 elif child.is_bone and not child.ignore:
1874 child_bone = child.build_skeleton(arm, bone_matrix, bone_size,
1875 force_connect_children=force_connect_children)
1876 # Connection to parent.
1877 child_connect(bone, child_bone, None, connect_ctx)
1879 child_connect_finalize(bone, connect_ctx)
1880 return bone
1882 def build_node_obj(self, fbx_tmpl, settings):
1883 if self.bl_obj:
1884 return self.bl_obj
1886 if self.is_bone or not self.fbx_elem:
1887 return None
1889 # create when linking since we need object data
1890 elem_name_utf8 = self.fbx_name
1892 # Object data must be created already
1893 self.bl_obj = obj = bpy.data.objects.new(name=elem_name_utf8, object_data=self.bl_data)
1895 fbx_props = (elem_find_first(self.fbx_elem, b'Properties70'),
1896 elem_find_first(fbx_tmpl, b'Properties70', fbx_elem_nil))
1897 assert(fbx_props[0] is not None)
1899 # ----
1900 # Misc Attributes
1902 obj.color[0:3] = elem_props_get_color_rgb(fbx_props, b'Color', (0.8, 0.8, 0.8))
1903 obj.hide = not bool(elem_props_get_visibility(fbx_props, b'Visibility', 1.0))
1905 obj.matrix_basis = self.get_matrix()
1907 if settings.use_custom_props:
1908 blen_read_custom_properties(fbx_props[0], obj, settings)
1910 return obj
1912 def build_skeleton_children(self, fbx_tmpl, settings, scene):
1913 if self.is_bone:
1914 for child in self.children:
1915 if child.ignore:
1916 continue
1917 child.build_skeleton_children(fbx_tmpl, settings, scene)
1918 return None
1919 else:
1920 # child is not a bone
1921 obj = self.build_node_obj(fbx_tmpl, settings)
1923 if obj is None:
1924 return None
1926 for child in self.children:
1927 if child.ignore:
1928 continue
1929 child.build_skeleton_children(fbx_tmpl, settings, scene)
1931 # instance in scene
1932 obj_base = scene.objects.link(obj)
1933 obj_base.select = True
1935 return obj
1937 def link_skeleton_children(self, fbx_tmpl, settings, scene):
1938 if self.is_bone:
1939 for child in self.children:
1940 if child.ignore:
1941 continue
1942 child_obj = child.bl_obj
1943 if child_obj and child_obj != self.bl_obj:
1944 child_obj.parent = self.bl_obj # get the armature the bone belongs to
1945 child_obj.parent_bone = self.bl_bone
1946 child_obj.parent_type = 'BONE'
1947 child_obj.matrix_parent_inverse = Matrix()
1949 # Blender attaches to the end of a bone, while FBX attaches to the start.
1950 # bone_child_matrix corrects for that.
1951 if child.pre_matrix:
1952 child.pre_matrix = self.bone_child_matrix * child.pre_matrix
1953 else:
1954 child.pre_matrix = self.bone_child_matrix
1956 child_obj.matrix_basis = child.get_matrix()
1957 return None
1958 else:
1959 obj = self.bl_obj
1961 for child in self.children:
1962 if child.ignore:
1963 continue
1964 child_obj = child.link_skeleton_children(fbx_tmpl, settings, scene)
1965 if child_obj:
1966 child_obj.parent = obj
1968 return obj
1970 def set_pose_matrix(self, arm):
1971 pose_bone = arm.bl_obj.pose.bones[self.bl_bone]
1972 pose_bone.matrix_basis = self.get_bind_matrix().inverted_safe() * self.get_matrix()
1974 for child in self.children:
1975 if child.ignore:
1976 continue
1977 if child.is_bone:
1978 child.set_pose_matrix(arm)
1980 def merge_weights(self, combined_weights, fbx_cluster):
1981 indices = elem_prop_first(elem_find_first(fbx_cluster, b'Indexes', default=None), default=())
1982 weights = elem_prop_first(elem_find_first(fbx_cluster, b'Weights', default=None), default=())
1984 for index, weight in zip(indices, weights):
1985 w = combined_weights.get(index)
1986 if w is None:
1987 combined_weights[index] = [weight]
1988 else:
1989 w.append(weight)
1991 def set_bone_weights(self):
1992 ignored_children = tuple(child for child in self.children
1993 if child.is_bone and child.ignore and len(child.clusters) > 0)
1995 if len(ignored_children) > 0:
1996 # If we have an ignored child bone we need to merge their weights into the current bone weights.
1997 # This can happen both intentionally and accidentally when skinning a model. Either way, they
1998 # need to be moved into a parent bone or they cause animation glitches.
1999 for fbx_cluster, meshes in self.clusters:
2000 combined_weights = {}
2001 self.merge_weights(combined_weights, fbx_cluster)
2003 for child in ignored_children:
2004 for child_cluster, child_meshes in child.clusters:
2005 if not meshes.isdisjoint(child_meshes):
2006 self.merge_weights(combined_weights, child_cluster)
2008 # combine child weights
2009 indices = []
2010 weights = []
2011 for i, w in combined_weights.items():
2012 indices.append(i)
2013 if len(w) > 1:
2014 weights.append(sum(w) / len(w))
2015 else:
2016 weights.append(w[0])
2018 add_vgroup_to_objects(indices, weights, self.bl_bone, [node.bl_obj for node in meshes])
2020 # clusters that drive meshes not included in a parent don't need to be merged
2021 all_meshes = set().union(*[meshes for _, meshes in self.clusters])
2022 for child in ignored_children:
2023 for child_cluster, child_meshes in child.clusters:
2024 if all_meshes.isdisjoint(child_meshes):
2025 indices = elem_prop_first(elem_find_first(child_cluster, b'Indexes', default=None), default=())
2026 weights = elem_prop_first(elem_find_first(child_cluster, b'Weights', default=None), default=())
2027 add_vgroup_to_objects(indices, weights, self.bl_bone, [node.bl_obj for node in child_meshes])
2028 else:
2029 # set the vertex weights on meshes
2030 for fbx_cluster, meshes in self.clusters:
2031 indices = elem_prop_first(elem_find_first(fbx_cluster, b'Indexes', default=None), default=())
2032 weights = elem_prop_first(elem_find_first(fbx_cluster, b'Weights', default=None), default=())
2033 add_vgroup_to_objects(indices, weights, self.bl_bone, [node.bl_obj for node in meshes])
2035 for child in self.children:
2036 if child.is_bone and not child.ignore:
2037 child.set_bone_weights()
2039 def build_hierarchy(self, fbx_tmpl, settings, scene):
2040 if self.is_armature:
2041 # create when linking since we need object data
2042 elem_name_utf8 = self.fbx_name
2044 self.bl_data = arm_data = bpy.data.armatures.new(name=elem_name_utf8)
2046 # Object data must be created already
2047 self.bl_obj = arm = bpy.data.objects.new(name=elem_name_utf8, object_data=arm_data)
2049 arm.matrix_basis = self.get_matrix()
2051 if self.fbx_elem:
2052 fbx_props = (elem_find_first(self.fbx_elem, b'Properties70'),
2053 elem_find_first(fbx_tmpl, b'Properties70', fbx_elem_nil))
2054 assert(fbx_props[0] is not None)
2056 if settings.use_custom_props:
2057 blen_read_custom_properties(fbx_props[0], arm, settings)
2059 # instance in scene
2060 obj_base = scene.objects.link(arm)
2061 obj_base.select = True
2063 # Add bones:
2065 # Switch to Edit mode.
2066 scene.objects.active = arm
2067 is_hidden = arm.hide
2068 arm.hide = False # Can't switch to Edit mode hidden objects...
2069 bpy.ops.object.mode_set(mode='EDIT')
2071 for child in self.children:
2072 if child.ignore:
2073 continue
2074 if child.is_bone:
2075 child.build_skeleton(self, Matrix(), force_connect_children=settings.force_connect_children)
2077 bpy.ops.object.mode_set(mode='OBJECT')
2079 arm.hide = is_hidden
2081 # Set pose matrix
2082 for child in self.children:
2083 if child.ignore:
2084 continue
2085 if child.is_bone:
2086 child.set_pose_matrix(self)
2088 # Add bone children:
2089 for child in self.children:
2090 if child.ignore:
2091 continue
2092 child_obj = child.build_skeleton_children(fbx_tmpl, settings, scene)
2094 return arm
2095 elif self.fbx_elem and not self.is_bone:
2096 obj = self.build_node_obj(fbx_tmpl, settings)
2098 # walk through children
2099 for child in self.children:
2100 child.build_hierarchy(fbx_tmpl, settings, scene)
2102 # instance in scene
2103 obj_base = scene.objects.link(obj)
2104 obj_base.select = True
2106 return obj
2107 else:
2108 for child in self.children:
2109 child.build_hierarchy(fbx_tmpl, settings, scene)
2111 return None
2113 def link_hierarchy(self, fbx_tmpl, settings, scene):
2114 if self.is_armature:
2115 arm = self.bl_obj
2117 # Link bone children:
2118 for child in self.children:
2119 if child.ignore:
2120 continue
2121 child_obj = child.link_skeleton_children(fbx_tmpl, settings, scene)
2122 if child_obj:
2123 child_obj.parent = arm
2125 # Add armature modifiers to the meshes
2126 if self.meshes:
2127 for mesh in self.meshes:
2128 (mmat, amat) = mesh.armature_setup[self]
2129 me_obj = mesh.bl_obj
2131 # bring global armature & mesh matrices into *Blender* global space.
2132 # Note: Usage of matrix_geom (local 'diff' transform) here is quite brittle.
2133 # Among other things, why in hell isn't it taken into account by bindpose & co???
2134 # Probably because org app (max) handles it completely aside from any parenting stuff,
2135 # which we obviously cannot do in Blender. :/
2136 if amat is None:
2137 amat = self.bind_matrix
2138 amat = settings.global_matrix * (Matrix() if amat is None else amat)
2139 if self.matrix_geom:
2140 amat = amat * self.matrix_geom
2141 mmat = settings.global_matrix * mmat
2142 if mesh.matrix_geom:
2143 mmat = mmat * mesh.matrix_geom
2145 # Now that we have armature and mesh in there (global) bind 'state' (matrix),
2146 # we can compute inverse parenting matrix of the mesh.
2147 me_obj.matrix_parent_inverse = amat.inverted_safe() * mmat * me_obj.matrix_basis.inverted_safe()
2149 mod = mesh.bl_obj.modifiers.new(arm.name, 'ARMATURE')
2150 mod.object = arm
2152 # Add bone weights to the deformers
2153 for child in self.children:
2154 if child.ignore:
2155 continue
2156 if child.is_bone:
2157 child.set_bone_weights()
2159 return arm
2160 elif self.bl_obj:
2161 obj = self.bl_obj
2163 # walk through children
2164 for child in self.children:
2165 child_obj = child.link_hierarchy(fbx_tmpl, settings, scene)
2166 if child_obj:
2167 child_obj.parent = obj
2169 return obj
2170 else:
2171 for child in self.children:
2172 child.link_hierarchy(fbx_tmpl, settings, scene)
2174 return None
2177 def is_ascii(filepath, size):
2178 with open(filepath, 'r', encoding="utf-8") as f:
2179 try:
2180 f.read(size)
2181 return True
2182 except UnicodeDecodeError:
2183 pass
2185 return False
2188 def load(operator, context, filepath="",
2189 use_manual_orientation=False,
2190 axis_forward='-Z',
2191 axis_up='Y',
2192 global_scale=1.0,
2193 bake_space_transform=False,
2194 use_custom_normals=True,
2195 use_cycles=True,
2196 use_image_search=False,
2197 use_alpha_decals=False,
2198 decal_offset=0.0,
2199 use_anim=True,
2200 anim_offset=1.0,
2201 use_custom_props=True,
2202 use_custom_props_enum_as_string=True,
2203 ignore_leaf_bones=False,
2204 force_connect_children=False,
2205 automatic_bone_orientation=False,
2206 primary_bone_axis='Y',
2207 secondary_bone_axis='X',
2208 use_prepost_rot=True):
2210 global fbx_elem_nil
2211 fbx_elem_nil = FBXElem('', (), (), ())
2213 import os
2214 import time
2215 from bpy_extras.io_utils import axis_conversion
2217 from . import parse_fbx
2218 from .fbx_utils import RIGHT_HAND_AXES, FBX_FRAMERATES
2220 start_time_proc = time.process_time()
2221 start_time_sys = time.time()
2223 perfmon = PerfMon()
2224 perfmon.level_up()
2225 perfmon.step("FBX Import: start importing %s" % filepath)
2226 perfmon.level_up()
2228 # detect ascii files
2229 if is_ascii(filepath, 24):
2230 operator.report({'ERROR'}, "ASCII FBX files are not supported %r" % filepath)
2231 return {'CANCELLED'}
2233 try:
2234 elem_root, version = parse_fbx.parse(filepath)
2235 except:
2236 import traceback
2237 traceback.print_exc()
2239 operator.report({'ERROR'}, "Couldn't open file %r" % filepath)
2240 return {'CANCELLED'}
2242 if version < 7100:
2243 operator.report({'ERROR'}, "Version %r unsupported, must be %r or later" % (version, 7100))
2244 return {'CANCELLED'}
2246 print("FBX version: %r" % version)
2248 if bpy.ops.object.mode_set.poll():
2249 bpy.ops.object.mode_set(mode='OBJECT', toggle=False)
2251 # deselect all
2252 if bpy.ops.object.select_all.poll():
2253 bpy.ops.object.select_all(action='DESELECT')
2255 basedir = os.path.dirname(filepath)
2257 cycles_material_wrap_map = {}
2258 image_cache = {}
2259 if not use_cycles:
2260 texture_cache = {}
2262 # Tables: (FBX_byte_id -> [FBX_data, None or Blender_datablock])
2263 fbx_table_nodes = {}
2265 if use_alpha_decals:
2266 material_decals = set()
2267 else:
2268 material_decals = None
2270 scene = context.scene
2272 # #### Get some info from GlobalSettings.
2274 perfmon.step("FBX import: Prepare...")
2276 fbx_settings = elem_find_first(elem_root, b'GlobalSettings')
2277 fbx_settings_props = elem_find_first(fbx_settings, b'Properties70')
2278 if fbx_settings is None or fbx_settings_props is None:
2279 operator.report({'ERROR'}, "No 'GlobalSettings' found in file %r" % filepath)
2280 return {'CANCELLED'}
2282 # FBX default base unit seems to be the centimeter, while raw Blender Unit is equivalent to the meter...
2283 unit_scale = elem_props_get_number(fbx_settings_props, b'UnitScaleFactor', 1.0)
2284 unit_scale_org = elem_props_get_number(fbx_settings_props, b'OriginalUnitScaleFactor', 1.0)
2285 global_scale *= (unit_scale / units_blender_to_fbx_factor(context.scene))
2286 # Compute global matrix and scale.
2287 if not use_manual_orientation:
2288 axis_forward = (elem_props_get_integer(fbx_settings_props, b'FrontAxis', 1),
2289 elem_props_get_integer(fbx_settings_props, b'FrontAxisSign', 1))
2290 axis_up = (elem_props_get_integer(fbx_settings_props, b'UpAxis', 2),
2291 elem_props_get_integer(fbx_settings_props, b'UpAxisSign', 1))
2292 axis_coord = (elem_props_get_integer(fbx_settings_props, b'CoordAxis', 0),
2293 elem_props_get_integer(fbx_settings_props, b'CoordAxisSign', 1))
2294 axis_key = (axis_up, axis_forward, axis_coord)
2295 axis_up, axis_forward = {v: k for k, v in RIGHT_HAND_AXES.items()}.get(axis_key, ('Z', 'Y'))
2296 global_matrix = (Matrix.Scale(global_scale, 4) *
2297 axis_conversion(from_forward=axis_forward, from_up=axis_up).to_4x4())
2299 # To cancel out unwanted rotation/scale on nodes.
2300 global_matrix_inv = global_matrix.inverted()
2301 # For transforming mesh normals.
2302 global_matrix_inv_transposed = global_matrix_inv.transposed()
2304 # Compute bone correction matrix
2305 bone_correction_matrix = None # None means no correction/identity
2306 if not automatic_bone_orientation:
2307 if (primary_bone_axis, secondary_bone_axis) != ('Y', 'X'):
2308 bone_correction_matrix = axis_conversion(from_forward='X',
2309 from_up='Y',
2310 to_forward=secondary_bone_axis,
2311 to_up=primary_bone_axis,
2312 ).to_4x4()
2314 # Compute framerate settings.
2315 custom_fps = elem_props_get_number(fbx_settings_props, b'CustomFrameRate', 25.0)
2316 time_mode = elem_props_get_enum(fbx_settings_props, b'TimeMode')
2317 real_fps = {eid: val for val, eid in FBX_FRAMERATES[1:]}.get(time_mode, custom_fps)
2318 if real_fps <= 0.0:
2319 real_fps = 25.0
2320 scene.render.fps = round(real_fps)
2321 scene.render.fps_base = scene.render.fps / real_fps
2323 # store global settings that need to be accessed during conversion
2324 settings = FBXImportSettings(
2325 operator.report, (axis_up, axis_forward), global_matrix, global_scale,
2326 bake_space_transform, global_matrix_inv, global_matrix_inv_transposed,
2327 use_custom_normals, use_cycles, use_image_search,
2328 use_alpha_decals, decal_offset,
2329 use_anim, anim_offset,
2330 use_custom_props, use_custom_props_enum_as_string,
2331 cycles_material_wrap_map, image_cache,
2332 ignore_leaf_bones, force_connect_children, automatic_bone_orientation, bone_correction_matrix,
2333 use_prepost_rot,
2336 # #### And now, the "real" data.
2338 perfmon.step("FBX import: Templates...")
2340 fbx_defs = elem_find_first(elem_root, b'Definitions') # can be None
2341 fbx_nodes = elem_find_first(elem_root, b'Objects')
2342 fbx_connections = elem_find_first(elem_root, b'Connections')
2344 if fbx_nodes is None:
2345 operator.report({'ERROR'}, "No 'Objects' found in file %r" % filepath)
2346 return {'CANCELLED'}
2347 if fbx_connections is None:
2348 operator.report({'ERROR'}, "No 'Connections' found in file %r" % filepath)
2349 return {'CANCELLED'}
2351 # ----
2352 # First load property templates
2353 # Load 'PropertyTemplate' values.
2354 # Key is a tuple, (ObjectType, FBXNodeType)
2355 # eg, (b'Texture', b'KFbxFileTexture')
2356 # (b'Geometry', b'KFbxMesh')
2357 fbx_templates = {}
2359 def _():
2360 if fbx_defs is not None:
2361 for fbx_def in fbx_defs.elems:
2362 if fbx_def.id == b'ObjectType':
2363 for fbx_subdef in fbx_def.elems:
2364 if fbx_subdef.id == b'PropertyTemplate':
2365 assert(fbx_def.props_type == b'S')
2366 assert(fbx_subdef.props_type == b'S')
2367 # (b'Texture', b'KFbxFileTexture') - eg.
2368 key = fbx_def.props[0], fbx_subdef.props[0]
2369 fbx_templates[key] = fbx_subdef
2370 _(); del _
2372 def fbx_template_get(key):
2373 ret = fbx_templates.get(key, fbx_elem_nil)
2374 if ret is None:
2375 # Newest FBX (7.4 and above) use no more 'K' in their type names...
2376 key = (key[0], key[1][1:])
2377 return fbx_templates.get(key, fbx_elem_nil)
2378 return ret
2380 perfmon.step("FBX import: Nodes...")
2382 # ----
2383 # Build FBX node-table
2384 def _():
2385 for fbx_obj in fbx_nodes.elems:
2386 # TODO, investigate what other items after first 3 may be
2387 assert(fbx_obj.props_type[:3] == b'LSS')
2388 fbx_uuid = elem_uuid(fbx_obj)
2389 fbx_table_nodes[fbx_uuid] = [fbx_obj, None]
2390 _(); del _
2392 # ----
2393 # Load in the data
2394 # http://download.autodesk.com/us/fbx/20112/FBX_SDK_HELP/index.html?url=
2395 # WS73099cc142f487551fea285e1221e4f9ff8-7fda.htm,topicNumber=d0e6388
2397 perfmon.step("FBX import: Connections...")
2399 fbx_connection_map = {}
2400 fbx_connection_map_reverse = {}
2402 def _():
2403 for fbx_link in fbx_connections.elems:
2404 c_type = fbx_link.props[0]
2405 if fbx_link.props_type[1:3] == b'LL':
2406 c_src, c_dst = fbx_link.props[1:3]
2407 fbx_connection_map.setdefault(c_src, []).append((c_dst, fbx_link))
2408 fbx_connection_map_reverse.setdefault(c_dst, []).append((c_src, fbx_link))
2409 _(); del _
2411 perfmon.step("FBX import: Meshes...")
2413 # ----
2414 # Load mesh data
2415 def _():
2416 fbx_tmpl = fbx_template_get((b'Geometry', b'KFbxMesh'))
2418 for fbx_uuid, fbx_item in fbx_table_nodes.items():
2419 fbx_obj, blen_data = fbx_item
2420 if fbx_obj.id != b'Geometry':
2421 continue
2422 if fbx_obj.props[-1] == b'Mesh':
2423 assert(blen_data is None)
2424 fbx_item[1] = blen_read_geom(fbx_tmpl, fbx_obj, settings)
2425 _(); del _
2427 perfmon.step("FBX import: Materials & Textures...")
2429 # ----
2430 # Load material data
2431 def _():
2432 fbx_tmpl = fbx_template_get((b'Material', b'KFbxSurfacePhong'))
2433 # b'KFbxSurfaceLambert'
2435 for fbx_uuid, fbx_item in fbx_table_nodes.items():
2436 fbx_obj, blen_data = fbx_item
2437 if fbx_obj.id != b'Material':
2438 continue
2439 assert(blen_data is None)
2440 fbx_item[1] = blen_read_material(fbx_tmpl, fbx_obj, settings)
2441 _(); del _
2443 # ----
2444 # Load image & textures data
2445 def _():
2446 fbx_tmpl_tex = fbx_template_get((b'Texture', b'KFbxFileTexture'))
2447 fbx_tmpl_img = fbx_template_get((b'Video', b'KFbxVideo'))
2449 # Important to run all 'Video' ones first, embedded images are stored in those nodes.
2450 # XXX Note we simplify things here, assuming both matching Video and Texture will use same file path,
2451 # this may be a bit weak, if issue arise we'll fallback to plain connection stuff...
2452 for fbx_uuid, fbx_item in fbx_table_nodes.items():
2453 fbx_obj, blen_data = fbx_item
2454 if fbx_obj.id != b'Video':
2455 continue
2456 fbx_item[1] = blen_read_texture_image(fbx_tmpl_img, fbx_obj, basedir, settings)
2457 for fbx_uuid, fbx_item in fbx_table_nodes.items():
2458 fbx_obj, blen_data = fbx_item
2459 if fbx_obj.id != b'Texture':
2460 continue
2461 fbx_item[1] = blen_read_texture_image(fbx_tmpl_tex, fbx_obj, basedir, settings)
2462 _(); del _
2464 perfmon.step("FBX import: Cameras & Lamps...")
2466 # ----
2467 # Load camera data
2468 def _():
2469 fbx_tmpl = fbx_template_get((b'NodeAttribute', b'KFbxCamera'))
2471 for fbx_uuid, fbx_item in fbx_table_nodes.items():
2472 fbx_obj, blen_data = fbx_item
2473 if fbx_obj.id != b'NodeAttribute':
2474 continue
2475 if fbx_obj.props[-1] == b'Camera':
2476 assert(blen_data is None)
2477 fbx_item[1] = blen_read_camera(fbx_tmpl, fbx_obj, global_scale)
2478 _(); del _
2480 # ----
2481 # Load lamp data
2482 def _():
2483 fbx_tmpl = fbx_template_get((b'NodeAttribute', b'KFbxLight'))
2485 for fbx_uuid, fbx_item in fbx_table_nodes.items():
2486 fbx_obj, blen_data = fbx_item
2487 if fbx_obj.id != b'NodeAttribute':
2488 continue
2489 if fbx_obj.props[-1] == b'Light':
2490 assert(blen_data is None)
2491 fbx_item[1] = blen_read_light(fbx_tmpl, fbx_obj, global_scale)
2492 _(); del _
2494 # ----
2495 # Connections
2496 def connection_filter_ex(fbx_uuid, fbx_id, dct):
2497 return [(c_found[0], c_found[1], c_type)
2498 for (c_uuid, c_type) in dct.get(fbx_uuid, ())
2499 # 0 is used for the root node, which isnt in fbx_table_nodes
2500 for c_found in (() if c_uuid is 0 else (fbx_table_nodes.get(c_uuid, (None, None)),))
2501 if (fbx_id is None) or (c_found[0] and c_found[0].id == fbx_id)]
2503 def connection_filter_forward(fbx_uuid, fbx_id):
2504 return connection_filter_ex(fbx_uuid, fbx_id, fbx_connection_map)
2506 def connection_filter_reverse(fbx_uuid, fbx_id):
2507 return connection_filter_ex(fbx_uuid, fbx_id, fbx_connection_map_reverse)
2509 perfmon.step("FBX import: Objects & Armatures...")
2511 # -- temporary helper hierarchy to build armatures and objects from
2512 # lookup from uuid to helper node. Used to build parent-child relations and later to look up animated nodes.
2513 fbx_helper_nodes = {}
2515 def _():
2516 # We build an intermediate hierarchy used to:
2517 # - Calculate and store bone orientation correction matrices. The same matrices will be reused for animation.
2518 # - Find/insert armature nodes.
2519 # - Filter leaf bones.
2521 # create scene root
2522 fbx_helper_nodes[0] = root_helper = FbxImportHelperNode(None, None, None, False)
2523 root_helper.is_root = True
2525 # add fbx nodes
2526 fbx_tmpl = fbx_template_get((b'Model', b'KFbxNode'))
2527 for a_uuid, a_item in fbx_table_nodes.items():
2528 fbx_obj, bl_data = a_item
2529 if fbx_obj is None or fbx_obj.id != b'Model':
2530 continue
2532 fbx_props = (elem_find_first(fbx_obj, b'Properties70'),
2533 elem_find_first(fbx_tmpl, b'Properties70', fbx_elem_nil))
2534 assert(fbx_props[0] is not None)
2536 transform_data = blen_read_object_transform_preprocess(fbx_props, fbx_obj, Matrix(), use_prepost_rot)
2537 # Note: 'Root' "bones" are handled as (armature) objects.
2538 # Note: See T46912 for first FBX file I ever saw with 'Limb' bones - thought those were totally deprecated.
2539 is_bone = fbx_obj.props[2] in {b'LimbNode', b'Limb'}
2540 fbx_helper_nodes[a_uuid] = FbxImportHelperNode(fbx_obj, bl_data, transform_data, is_bone)
2542 # add parent-child relations and add blender data to the node
2543 for fbx_link in fbx_connections.elems:
2544 if fbx_link.props[0] != b'OO':
2545 continue
2546 if fbx_link.props_type[1:3] == b'LL':
2547 c_src, c_dst = fbx_link.props[1:3]
2548 parent = fbx_helper_nodes.get(c_dst)
2549 if parent is None:
2550 continue
2552 child = fbx_helper_nodes.get(c_src)
2553 if child is None:
2554 # add blender data (meshes, lights, cameras, etc.) to a helper node
2555 fbx_sdata, bl_data = p_item = fbx_table_nodes.get(c_src, (None, None))
2556 if fbx_sdata is None:
2557 continue
2558 if fbx_sdata.id not in {b'Geometry', b'NodeAttribute'}:
2559 continue
2560 parent.bl_data = bl_data
2561 else:
2562 # set parent
2563 child.parent = parent
2565 # find armatures (either an empty below a bone or a new node inserted at the bone
2566 root_helper.find_armatures()
2568 # mark nodes that have bone children
2569 root_helper.find_bone_children()
2571 # mark nodes that need a bone to attach child-bones to
2572 root_helper.find_fake_bones()
2574 # mark leaf nodes that are only required to mark the end of their parent bone
2575 if settings.ignore_leaf_bones:
2576 root_helper.mark_leaf_bones()
2578 # What a mess! Some bones have several BindPoses, some have none, clusters contain a bind pose as well,
2579 # and you can have several clusters per bone!
2580 # Maybe some conversion can be applied to put them all into the same frame of reference?
2582 # get the bind pose from pose elements
2583 for a_uuid, a_item in fbx_table_nodes.items():
2584 fbx_obj, bl_data = a_item
2585 if fbx_obj is None:
2586 continue
2587 if fbx_obj.id != b'Pose':
2588 continue
2589 if fbx_obj.props[2] != b'BindPose':
2590 continue
2591 for fbx_pose_node in fbx_obj.elems:
2592 if fbx_pose_node.id != b'PoseNode':
2593 continue
2594 node_elem = elem_find_first(fbx_pose_node, b'Node')
2595 node = elem_uuid(node_elem)
2596 matrix_elem = elem_find_first(fbx_pose_node, b'Matrix')
2597 matrix = array_to_matrix4(matrix_elem.props[0]) if matrix_elem else None
2598 bone = fbx_helper_nodes.get(node)
2599 if bone and matrix:
2600 # Store the matrix in the helper node.
2601 # There may be several bind pose matrices for the same node, but in tests they seem to be identical.
2602 bone.bind_matrix = matrix # global space
2604 # get clusters and bind pose
2605 for helper_uuid, helper_node in fbx_helper_nodes.items():
2606 if not helper_node.is_bone:
2607 continue
2608 for cluster_uuid, cluster_link in fbx_connection_map.get(helper_uuid, ()):
2609 if cluster_link.props[0] != b'OO':
2610 continue
2611 fbx_cluster, _ = fbx_table_nodes.get(cluster_uuid, (None, None))
2612 if fbx_cluster is None or fbx_cluster.id != b'Deformer' or fbx_cluster.props[2] != b'Cluster':
2613 continue
2615 # Get the bind pose from the cluster:
2616 tx_mesh_elem = elem_find_first(fbx_cluster, b'Transform', default=None)
2617 tx_mesh = array_to_matrix4(tx_mesh_elem.props[0]) if tx_mesh_elem else Matrix()
2619 tx_bone_elem = elem_find_first(fbx_cluster, b'TransformLink', default=None)
2620 tx_bone = array_to_matrix4(tx_bone_elem.props[0]) if tx_bone_elem else None
2622 tx_arm_elem = elem_find_first(fbx_cluster, b'TransformAssociateModel', default=None)
2623 tx_arm = array_to_matrix4(tx_arm_elem.props[0]) if tx_arm_elem else None
2625 mesh_matrix = tx_mesh
2626 armature_matrix = tx_arm
2628 if tx_bone:
2629 mesh_matrix = tx_bone * mesh_matrix
2630 helper_node.bind_matrix = tx_bone # overwrite the bind matrix
2632 # Get the meshes driven by this cluster: (Shouldn't that be only one?)
2633 meshes = set()
2634 for skin_uuid, skin_link in fbx_connection_map.get(cluster_uuid):
2635 if skin_link.props[0] != b'OO':
2636 continue
2637 fbx_skin, _ = fbx_table_nodes.get(skin_uuid, (None, None))
2638 if fbx_skin is None or fbx_skin.id != b'Deformer' or fbx_skin.props[2] != b'Skin':
2639 continue
2640 for mesh_uuid, mesh_link in fbx_connection_map.get(skin_uuid):
2641 if mesh_link.props[0] != b'OO':
2642 continue
2643 fbx_mesh, _ = fbx_table_nodes.get(mesh_uuid, (None, None))
2644 if fbx_mesh is None or fbx_mesh.id != b'Geometry' or fbx_mesh.props[2] != b'Mesh':
2645 continue
2646 for object_uuid, object_link in fbx_connection_map.get(mesh_uuid):
2647 if object_link.props[0] != b'OO':
2648 continue
2649 mesh_node = fbx_helper_nodes[object_uuid]
2650 if mesh_node:
2651 # ----
2652 # If we get a valid mesh matrix (in bone space), store armature and
2653 # mesh global matrices, we need them to compute mesh's matrix_parent_inverse
2654 # when actually binding them via the modifier.
2655 # Note we assume all bones were bound with the same mesh/armature (global) matrix,
2656 # we do not support otherwise in Blender anyway!
2657 mesh_node.armature_setup[helper_node.armature] = (mesh_matrix, armature_matrix)
2658 meshes.add(mesh_node)
2660 helper_node.clusters.append((fbx_cluster, meshes))
2662 # convert bind poses from global space into local space
2663 root_helper.make_bind_pose_local()
2665 # collect armature meshes
2666 root_helper.collect_armature_meshes()
2668 # find the correction matrices to align FBX objects with their Blender equivalent
2669 root_helper.find_correction_matrix(settings)
2671 # build the Object/Armature/Bone hierarchy
2672 root_helper.build_hierarchy(fbx_tmpl, settings, scene)
2674 # Link the Object/Armature/Bone hierarchy
2675 root_helper.link_hierarchy(fbx_tmpl, settings, scene)
2677 # root_helper.print_info(0)
2678 _(); del _
2680 perfmon.step("FBX import: ShapeKeys...")
2682 # We can handle shapes.
2683 blend_shape_channels = {} # We do not need Shapes themselves, but keyblocks, for anim.
2685 def _():
2686 fbx_tmpl = fbx_template_get((b'Geometry', b'KFbxShape'))
2688 for s_uuid, s_item in fbx_table_nodes.items():
2689 fbx_sdata, bl_sdata = s_item = fbx_table_nodes.get(s_uuid, (None, None))
2690 if fbx_sdata is None or fbx_sdata.id != b'Geometry' or fbx_sdata.props[2] != b'Shape':
2691 continue
2693 # shape -> blendshapechannel -> blendshape -> mesh.
2694 for bc_uuid, bc_ctype in fbx_connection_map.get(s_uuid, ()):
2695 if bc_ctype.props[0] != b'OO':
2696 continue
2697 fbx_bcdata, _bl_bcdata = fbx_table_nodes.get(bc_uuid, (None, None))
2698 if fbx_bcdata is None or fbx_bcdata.id != b'Deformer' or fbx_bcdata.props[2] != b'BlendShapeChannel':
2699 continue
2700 meshes = []
2701 objects = []
2702 for bs_uuid, bs_ctype in fbx_connection_map.get(bc_uuid, ()):
2703 if bs_ctype.props[0] != b'OO':
2704 continue
2705 fbx_bsdata, _bl_bsdata = fbx_table_nodes.get(bs_uuid, (None, None))
2706 if fbx_bsdata is None or fbx_bsdata.id != b'Deformer' or fbx_bsdata.props[2] != b'BlendShape':
2707 continue
2708 for m_uuid, m_ctype in fbx_connection_map.get(bs_uuid, ()):
2709 if m_ctype.props[0] != b'OO':
2710 continue
2711 fbx_mdata, bl_mdata = fbx_table_nodes.get(m_uuid, (None, None))
2712 if fbx_mdata is None or fbx_mdata.id != b'Geometry' or fbx_mdata.props[2] != b'Mesh':
2713 continue
2714 # Blenmeshes are assumed already created at that time!
2715 assert(isinstance(bl_mdata, bpy.types.Mesh))
2716 # And we have to find all objects using this mesh!
2717 objects = []
2718 for o_uuid, o_ctype in fbx_connection_map.get(m_uuid, ()):
2719 if o_ctype.props[0] != b'OO':
2720 continue
2721 node = fbx_helper_nodes[o_uuid]
2722 if node:
2723 objects.append(node)
2724 meshes.append((bl_mdata, objects))
2725 # BlendShape deformers are only here to connect BlendShapeChannels to meshes, nothing else to do.
2727 # keyblocks is a list of tuples (mesh, keyblock) matching that shape/blendshapechannel, for animation.
2728 keyblocks = blen_read_shape(fbx_tmpl, fbx_sdata, fbx_bcdata, meshes, scene)
2729 blend_shape_channels[bc_uuid] = keyblocks
2730 _(); del _
2732 if use_anim:
2733 perfmon.step("FBX import: Animations...")
2735 # Animation!
2736 def _():
2737 fbx_tmpl_astack = fbx_template_get((b'AnimationStack', b'FbxAnimStack'))
2738 fbx_tmpl_alayer = fbx_template_get((b'AnimationLayer', b'FbxAnimLayer'))
2739 stacks = {}
2741 # AnimationStacks.
2742 for as_uuid, fbx_asitem in fbx_table_nodes.items():
2743 fbx_asdata, _blen_data = fbx_asitem
2744 if fbx_asdata.id != b'AnimationStack' or fbx_asdata.props[2] != b'':
2745 continue
2746 stacks[as_uuid] = (fbx_asitem, {})
2748 # AnimationLayers
2749 # (mixing is completely ignored for now, each layer results in an independent set of actions).
2750 def get_astacks_from_alayer(al_uuid):
2751 for as_uuid, as_ctype in fbx_connection_map.get(al_uuid, ()):
2752 if as_ctype.props[0] != b'OO':
2753 continue
2754 fbx_asdata, _bl_asdata = fbx_table_nodes.get(as_uuid, (None, None))
2755 if (fbx_asdata is None or fbx_asdata.id != b'AnimationStack' or
2756 fbx_asdata.props[2] != b'' or as_uuid not in stacks):
2757 continue
2758 yield as_uuid
2759 for al_uuid, fbx_alitem in fbx_table_nodes.items():
2760 fbx_aldata, _blen_data = fbx_alitem
2761 if fbx_aldata.id != b'AnimationLayer' or fbx_aldata.props[2] != b'':
2762 continue
2763 for as_uuid in get_astacks_from_alayer(al_uuid):
2764 _fbx_asitem, alayers = stacks[as_uuid]
2765 alayers[al_uuid] = (fbx_alitem, {})
2767 # AnimationCurveNodes (also the ones linked to actual animated data!).
2768 curvenodes = {}
2769 for acn_uuid, fbx_acnitem in fbx_table_nodes.items():
2770 fbx_acndata, _blen_data = fbx_acnitem
2771 if fbx_acndata.id != b'AnimationCurveNode' or fbx_acndata.props[2] != b'':
2772 continue
2773 cnode = curvenodes[acn_uuid] = {}
2774 items = []
2775 for n_uuid, n_ctype in fbx_connection_map.get(acn_uuid, ()):
2776 if n_ctype.props[0] != b'OP':
2777 continue
2778 lnk_prop = n_ctype.props[3]
2779 if lnk_prop in {b'Lcl Translation', b'Lcl Rotation', b'Lcl Scaling'}:
2780 # n_uuid can (????) be linked to root '0' node, instead of a mere object node... See T41712.
2781 ob = fbx_helper_nodes.get(n_uuid, None)
2782 if ob is None:
2783 continue
2784 items.append((ob, lnk_prop))
2785 elif lnk_prop == b'DeformPercent': # Shape keys.
2786 keyblocks = blend_shape_channels.get(n_uuid)
2787 if keyblocks is None:
2788 continue
2789 items += [(kb, lnk_prop) for kb in keyblocks]
2790 for al_uuid, al_ctype in fbx_connection_map.get(acn_uuid, ()):
2791 if al_ctype.props[0] != b'OO':
2792 continue
2793 fbx_aldata, _blen_aldata = fbx_alitem = fbx_table_nodes.get(al_uuid, (None, None))
2794 if fbx_aldata is None or fbx_aldata.id != b'AnimationLayer' or fbx_aldata.props[2] != b'':
2795 continue
2796 for as_uuid in get_astacks_from_alayer(al_uuid):
2797 _fbx_alitem, anim_items = stacks[as_uuid][1][al_uuid]
2798 assert(_fbx_alitem == fbx_alitem)
2799 for item, item_prop in items:
2800 # No need to keep curvenode FBX data here, contains nothing useful for us.
2801 anim_items.setdefault(item, {})[acn_uuid] = (cnode, item_prop)
2803 # AnimationCurves (real animation data).
2804 for ac_uuid, fbx_acitem in fbx_table_nodes.items():
2805 fbx_acdata, _blen_data = fbx_acitem
2806 if fbx_acdata.id != b'AnimationCurve' or fbx_acdata.props[2] != b'':
2807 continue
2808 for acn_uuid, acn_ctype in fbx_connection_map.get(ac_uuid, ()):
2809 if acn_ctype.props[0] != b'OP':
2810 continue
2811 fbx_acndata, _bl_acndata = fbx_table_nodes.get(acn_uuid, (None, None))
2812 if (fbx_acndata is None or fbx_acndata.id != b'AnimationCurveNode' or
2813 fbx_acndata.props[2] != b'' or acn_uuid not in curvenodes):
2814 continue
2815 # Note this is an infamous simplification of the compound props stuff,
2816 # seems to be standard naming but we'll probably have to be smarter to handle more exotic files?
2817 channel = {b'd|X': 0, b'd|Y': 1, b'd|Z': 2, b'd|DeformPercent': 0}.get(acn_ctype.props[3], None)
2818 if channel is None:
2819 continue
2820 curvenodes[acn_uuid][ac_uuid] = (fbx_acitem, channel)
2822 # And now that we have sorted all this, apply animations!
2823 blen_read_animations(fbx_tmpl_astack, fbx_tmpl_alayer, stacks, scene, settings.anim_offset)
2825 _(); del _
2827 perfmon.step("FBX import: Assign materials...")
2829 def _():
2830 # link Material's to Geometry (via Model's)
2831 for fbx_uuid, fbx_item in fbx_table_nodes.items():
2832 fbx_obj, blen_data = fbx_item
2833 if fbx_obj.id != b'Geometry':
2834 continue
2836 mesh = fbx_table_nodes.get(fbx_uuid, (None, None))[1]
2838 # can happen in rare cases
2839 if mesh is None:
2840 continue
2842 # In Blender, we link materials to data, typically (meshes), while in FBX they are linked to objects...
2843 # So we have to be careful not to re-add endlessly the same material to a mesh!
2844 # This can easily happen with 'baked' dupliobjects, see T44386.
2845 # TODO: add an option to link materials to objects in Blender instead?
2846 done_mats = set()
2848 for (fbx_lnk, fbx_lnk_item, fbx_lnk_type) in connection_filter_forward(fbx_uuid, b'Model'):
2849 # link materials
2850 fbx_lnk_uuid = elem_uuid(fbx_lnk)
2851 for (fbx_lnk_material, material, fbx_lnk_material_type) in connection_filter_reverse(fbx_lnk_uuid, b'Material'):
2852 if material not in done_mats:
2853 mesh.materials.append(material)
2854 done_mats.add(material)
2856 # We have to validate mesh polygons' mat_idx, see T41015!
2857 # Some FBX seem to have an extra 'default' material which is not defined in FBX file.
2858 if mesh.validate_material_indices():
2859 print("WARNING: mesh '%s' had invalid material indices, those were reset to first material" % mesh.name)
2860 _(); del _
2862 perfmon.step("FBX import: Assign textures...")
2864 def _():
2865 material_images = {}
2867 fbx_tmpl = fbx_template_get((b'Material', b'KFbxSurfacePhong'))
2868 # b'KFbxSurfaceLambert'
2870 # textures that use this material
2871 def texture_bumpfac_get(fbx_obj):
2872 assert(fbx_obj.id == b'Material')
2873 fbx_props = (elem_find_first(fbx_obj, b'Properties70'),
2874 elem_find_first(fbx_tmpl, b'Properties70', fbx_elem_nil))
2875 # Do not assert, it can be None actually, sigh...
2876 #~ assert(fbx_props[0] is not None)
2877 # (x / 7.142) is only a guess, cycles usable range is (0.0 -> 0.5)
2878 return elem_props_get_number(fbx_props, b'BumpFactor', 2.5) / 7.142
2880 def texture_mapping_get(fbx_obj):
2881 assert(fbx_obj.id == b'Texture')
2883 fbx_props = (elem_find_first(fbx_obj, b'Properties70'),
2884 elem_find_first(fbx_tmpl, b'Properties70', fbx_elem_nil))
2885 # Do not assert, it can be None actually, sigh...
2886 #~ assert(fbx_props[0] is not None)
2887 return (elem_props_get_vector_3d(fbx_props, b'Translation', (0.0, 0.0, 0.0)),
2888 elem_props_get_vector_3d(fbx_props, b'Rotation', (0.0, 0.0, 0.0)),
2889 elem_props_get_vector_3d(fbx_props, b'Scaling', (1.0, 1.0, 1.0)),
2890 (bool(elem_props_get_enum(fbx_props, b'WrapModeU', 0)),
2891 bool(elem_props_get_enum(fbx_props, b'WrapModeV', 0))))
2893 if not use_cycles:
2894 # Simple function to make a new mtex and set defaults
2895 def material_mtex_new(material, image, tex_map):
2896 tex = texture_cache.get(image)
2897 if tex is None:
2898 tex = bpy.data.textures.new(name=image.name, type='IMAGE')
2899 tex.image = image
2900 texture_cache[image] = tex
2902 # copy custom properties from image object to texture
2903 for key, value in image.items():
2904 tex[key] = value
2906 # delete custom properties on the image object
2907 for key in image.keys():
2908 del image[key]
2910 mtex = material.texture_slots.add()
2911 mtex.texture = tex
2912 mtex.texture_coords = 'UV'
2913 mtex.use_map_color_diffuse = False
2915 # No rotation here...
2916 mtex.offset[:] = tex_map[0]
2917 mtex.scale[:] = tex_map[2]
2918 return mtex
2920 for fbx_uuid, fbx_item in fbx_table_nodes.items():
2921 fbx_obj, blen_data = fbx_item
2922 if fbx_obj.id != b'Material':
2923 continue
2925 material = fbx_table_nodes.get(fbx_uuid, (None, None))[1]
2926 for (fbx_lnk,
2927 image,
2928 fbx_lnk_type) in connection_filter_reverse(fbx_uuid, b'Texture'):
2930 if use_cycles:
2931 if fbx_lnk_type.props[0] == b'OP':
2932 lnk_type = fbx_lnk_type.props[3]
2934 ma_wrap = cycles_material_wrap_map[material]
2936 # tx/rot/scale
2937 tex_map = texture_mapping_get(fbx_lnk)
2938 if (tex_map[0] == (0.0, 0.0, 0.0) and
2939 tex_map[1] == (0.0, 0.0, 0.0) and
2940 tex_map[2] == (1.0, 1.0, 1.0) and
2941 tex_map[3] == (False, False)):
2942 use_mapping = False
2943 else:
2944 use_mapping = True
2945 tex_map_kw = {
2946 "translation": tex_map[0],
2947 "rotation": [-i for i in tex_map[1]],
2948 "scale": [((1.0 / i) if i != 0.0 else 1.0) for i in tex_map[2]],
2949 "clamp": tex_map[3],
2952 if lnk_type in {b'DiffuseColor', b'3dsMax|maps|texmap_diffuse'}:
2953 ma_wrap.diffuse_image_set(image)
2954 if use_mapping:
2955 ma_wrap.diffuse_mapping_set(**tex_map_kw)
2956 elif lnk_type == b'SpecularColor':
2957 ma_wrap.specular_image_set(image)
2958 if use_mapping:
2959 ma_wrap.specular_mapping_set(**tex_map_kw)
2960 elif lnk_type in {b'ReflectionColor', b'3dsMax|maps|texmap_reflection'}:
2961 ma_wrap.reflect_image_set(image)
2962 if use_mapping:
2963 ma_wrap.reflect_mapping_set(**tex_map_kw)
2964 elif lnk_type == b'TransparentColor': # alpha
2965 ma_wrap.alpha_image_set(image)
2966 if use_mapping:
2967 ma_wrap.alpha_mapping_set(**tex_map_kw)
2968 if use_alpha_decals:
2969 material_decals.add(material)
2970 elif lnk_type == b'DiffuseFactor':
2971 pass # TODO
2972 elif lnk_type == b'ShininessExponent':
2973 ma_wrap.hardness_image_set(image)
2974 if use_mapping:
2975 ma_wrap.hardness_mapping_set(**tex_map_kw)
2976 # XXX, applications abuse bump!
2977 elif lnk_type in {b'NormalMap', b'Bump', b'3dsMax|maps|texmap_bump'}:
2978 ma_wrap.normal_image_set(image)
2979 ma_wrap.normal_factor_set(texture_bumpfac_get(fbx_obj))
2980 if use_mapping:
2981 ma_wrap.normal_mapping_set(**tex_map_kw)
2983 elif lnk_type == b'Bump':
2984 ma_wrap.bump_image_set(image)
2985 ma_wrap.bump_factor_set(texture_bumpfac_get(fbx_obj))
2986 if use_mapping:
2987 ma_wrap.bump_mapping_set(**tex_map_kw)
2989 else:
2990 print("WARNING: material link %r ignored" % lnk_type)
2992 material_images.setdefault(material, {})[lnk_type] = (image, tex_map)
2993 else:
2994 if fbx_lnk_type.props[0] == b'OP':
2995 lnk_type = fbx_lnk_type.props[3]
2997 # tx/rot/scale (rot is ignored here!).
2998 tex_map = texture_mapping_get(fbx_lnk)
3000 mtex = material_mtex_new(material, image, tex_map)
3002 if lnk_type in {b'DiffuseColor', b'3dsMax|maps|texmap_diffuse'}:
3003 mtex.use_map_color_diffuse = True
3004 mtex.blend_type = 'MULTIPLY'
3005 elif lnk_type == b'SpecularColor':
3006 mtex.use_map_color_spec = True
3007 mtex.blend_type = 'MULTIPLY'
3008 elif lnk_type in {b'ReflectionColor', b'3dsMax|maps|texmap_reflection'}:
3009 mtex.use_map_raymir = True
3010 elif lnk_type == b'TransparentColor': # alpha
3011 material.use_transparency = True
3012 material.transparency_method = 'RAYTRACE'
3013 material.alpha = 0.0
3014 mtex.use_map_alpha = True
3015 mtex.alpha_factor = 1.0
3016 if use_alpha_decals:
3017 material_decals.add(material)
3018 elif lnk_type == b'DiffuseFactor':
3019 mtex.use_map_diffuse = True
3020 elif lnk_type == b'ShininessExponent':
3021 mtex.use_map_hardness = True
3022 # XXX, applications abuse bump!
3023 elif lnk_type in {b'NormalMap', b'Bump', b'3dsMax|maps|texmap_bump'}:
3024 mtex.texture.use_normal_map = True # not ideal!
3025 mtex.use_map_normal = True
3026 mtex.normal_factor = texture_bumpfac_get(fbx_obj)
3028 elif lnk_type == b'Bump':
3029 mtex.use_map_normal = True
3030 mtex.normal_factor = texture_bumpfac_get(fbx_obj)
3032 else:
3033 print("WARNING: material link %r ignored" % lnk_type)
3035 material_images.setdefault(material, {})[lnk_type] = (image, tex_map)
3037 # Check if the diffuse image has an alpha channel,
3038 # if so, use the alpha channel.
3040 # Note: this could be made optional since images may have alpha but be entirely opaque
3041 for fbx_uuid, fbx_item in fbx_table_nodes.items():
3042 fbx_obj, blen_data = fbx_item
3043 if fbx_obj.id != b'Material':
3044 continue
3045 material = fbx_table_nodes.get(fbx_uuid, (None, None))[1]
3046 image, tex_map = material_images.get(material, {}).get(b'DiffuseColor', (None, None))
3047 # do we have alpha?
3048 if image and image.depth == 32:
3049 if use_alpha_decals:
3050 material_decals.add(material)
3052 if use_cycles:
3053 ma_wrap = cycles_material_wrap_map[material]
3054 if ma_wrap.node_bsdf_alpha.mute:
3055 ma_wrap.alpha_image_set_from_diffuse()
3056 else:
3057 if not any((True for mtex in material.texture_slots if mtex and mtex.use_map_alpha)):
3058 mtex = material_mtex_new(material, image, tex_map)
3060 material.use_transparency = True
3061 material.transparency_method = 'RAYTRACE'
3062 material.alpha = 0.0
3063 mtex.use_map_alpha = True
3064 mtex.alpha_factor = 1.0
3066 # propagate mapping from diffuse to all other channels which have none defined.
3067 if use_cycles:
3068 ma_wrap = cycles_material_wrap_map[material]
3069 ma_wrap.mapping_set_from_diffuse()
3071 _(); del _
3073 perfmon.step("FBX import: Cycles z-offset workaround...")
3075 def _():
3076 # Annoying workaround for cycles having no z-offset
3077 if material_decals and use_alpha_decals:
3078 for fbx_uuid, fbx_item in fbx_table_nodes.items():
3079 fbx_obj, blen_data = fbx_item
3080 if fbx_obj.id != b'Geometry':
3081 continue
3082 if fbx_obj.props[-1] == b'Mesh':
3083 mesh = fbx_item[1]
3085 if decal_offset != 0.0:
3086 for material in mesh.materials:
3087 if material in material_decals:
3088 for v in mesh.vertices:
3089 v.co += v.normal * decal_offset
3090 break
3092 if use_cycles:
3093 for obj in (obj for obj in bpy.data.objects if obj.data == mesh):
3094 obj.cycles_visibility.shadow = False
3095 else:
3096 for material in mesh.materials:
3097 if material in material_decals:
3098 # recieve but dont cast shadows
3099 material.use_raytrace = False
3100 _(); del _
3102 perfmon.level_down()
3104 perfmon.level_down("Import finished.")
3105 return {'FINISHED'}