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