1 # SPDX-License-Identifier: GPL-2.0-or-later
3 # Script copyright (C) Campbell Barton, Bastien Montagne
9 from collections
import namedtuple
10 from collections
.abc
import Iterable
11 from itertools
import zip_longest
, chain
15 from bpy
.types
import Object
, Bone
, PoseBone
, DepsgraphObjectInstance
16 from mathutils
import Vector
, Matrix
18 from . import encode_bin
, data_types
23 FBX_HEADER_VERSION
= 1003
24 FBX_SCENEINFO_VERSION
= 100
25 FBX_TEMPLATES_VERSION
= 100
27 FBX_MODELS_VERSION
= 232
29 FBX_GEOMETRY_VERSION
= 124
30 # Revert back normals to 101 (simple 3D values) for now, 102 (4D + weights) seems not well supported by most apps
31 # currently, apart from some AD products.
32 FBX_GEOMETRY_NORMAL_VERSION
= 101
33 FBX_GEOMETRY_BINORMAL_VERSION
= 101
34 FBX_GEOMETRY_TANGENT_VERSION
= 101
35 FBX_GEOMETRY_SMOOTHING_VERSION
= 102
36 FBX_GEOMETRY_CREASE_VERSION
= 101
37 FBX_GEOMETRY_VCOLOR_VERSION
= 101
38 FBX_GEOMETRY_UV_VERSION
= 101
39 FBX_GEOMETRY_MATERIAL_VERSION
= 101
40 FBX_GEOMETRY_LAYER_VERSION
= 100
41 FBX_GEOMETRY_SHAPE_VERSION
= 100
42 FBX_DEFORMER_SHAPE_VERSION
= 100
43 FBX_DEFORMER_SHAPECHANNEL_VERSION
= 100
44 FBX_POSE_BIND_VERSION
= 100
45 FBX_DEFORMER_SKIN_VERSION
= 101
46 FBX_DEFORMER_CLUSTER_VERSION
= 100
47 FBX_MATERIAL_VERSION
= 102
48 FBX_TEXTURE_VERSION
= 202
49 FBX_ANIM_KEY_VERSION
= 4008
51 FBX_NAME_CLASS_SEP
= b
"\x00\x01"
52 FBX_ANIM_PROPSGROUP_NAME
= "d"
54 FBX_KTIME
= 46186158000 # This is the number of "ktimes" in one second (yep, precision over the nanosecond...)
57 MAT_CONVERT_LIGHT
= Matrix
.Rotation(math
.pi
/ 2.0, 4, 'X') # Blender is -Z, FBX is -Y.
58 MAT_CONVERT_CAMERA
= Matrix
.Rotation(math
.pi
/ 2.0, 4, 'Y') # Blender is -Z, FBX is +X.
59 # XXX I can't get this working :(
60 # MAT_CONVERT_BONE = Matrix.Rotation(math.pi / 2.0, 4, 'Z') # Blender is +Y, FBX is -X.
61 MAT_CONVERT_BONE
= Matrix()
64 BLENDER_OTHER_OBJECT_TYPES
= {'CURVE', 'SURFACE', 'FONT', 'META'}
65 BLENDER_OBJECT_TYPES_MESHLIKE
= {'MESH'} | BLENDER_OTHER_OBJECT_TYPES
71 'SUN': 1, # Directional.
73 'HEMI': 1, # Directional.
76 FBX_LIGHT_DECAY_TYPES
= {
77 'CONSTANT': 0, # None.
78 'INVERSE_LINEAR': 1, # Linear.
79 'INVERSE_SQUARE': 2, # Quadratic.
80 'INVERSE_COEFFICIENTS': 2, # Quadratic...
81 'CUSTOM_CURVE': 2, # Quadratic.
82 'LINEAR_QUADRATIC_WEIGHTED': 2, # Quadratic.
87 # Up, Forward -> FBX values (tuples of (axis, sign), Up, Front, Coord).
88 ( 'X', '-Y'): ((0, 1), (1, 1), (2, 1)),
89 ( 'X', 'Y'): ((0, 1), (1, -1), (2, -1)),
90 ( 'X', '-Z'): ((0, 1), (2, 1), (1, -1)),
91 ( 'X', 'Z'): ((0, 1), (2, -1), (1, 1)),
92 ('-X', '-Y'): ((0, -1), (1, 1), (2, -1)),
93 ('-X', 'Y'): ((0, -1), (1, -1), (2, 1)),
94 ('-X', '-Z'): ((0, -1), (2, 1), (1, 1)),
95 ('-X', 'Z'): ((0, -1), (2, -1), (1, -1)),
96 ( 'Y', '-X'): ((1, 1), (0, 1), (2, -1)),
97 ( 'Y', 'X'): ((1, 1), (0, -1), (2, 1)),
98 ( 'Y', '-Z'): ((1, 1), (2, 1), (0, 1)),
99 ( 'Y', 'Z'): ((1, 1), (2, -1), (0, -1)),
100 ('-Y', '-X'): ((1, -1), (0, 1), (2, 1)),
101 ('-Y', 'X'): ((1, -1), (0, -1), (2, -1)),
102 ('-Y', '-Z'): ((1, -1), (2, 1), (0, -1)),
103 ('-Y', 'Z'): ((1, -1), (2, -1), (0, 1)),
104 ( 'Z', '-X'): ((2, 1), (0, 1), (1, 1)),
105 ( 'Z', 'X'): ((2, 1), (0, -1), (1, -1)),
106 ( 'Z', '-Y'): ((2, 1), (1, 1), (0, -1)),
107 ( 'Z', 'Y'): ((2, 1), (1, -1), (0, 1)), # Blender system!
108 ('-Z', '-X'): ((2, -1), (0, 1), (1, -1)),
109 ('-Z', 'X'): ((2, -1), (0, -1), (1, 1)),
110 ('-Z', '-Y'): ((2, -1), (1, 1), (0, 1)),
111 ('-Z', 'Y'): ((2, -1), (1, -1), (0, -1)),
115 # NOTE: Not fully in enum value order, since when exporting the first entry matching the framerate value is used
116 # (e.g. better have NTSC fullframe than NTSC drop frame for 29.97 framerate).
118 #(-1.0, 0), # Default framerate.
119 (-1.0, 14), # Custom framerate.
125 (30.0, 6), # BW NTSC, full frame.
126 (30.0, 7), # Drop frame.
127 (30.0 / 1.001, 9), # Color NTSC, full frame.
128 (30.0 / 1.001, 8), # Color NTSC, drop frame.
131 #(1.0, 12), # 1000 milli/s (use for date time?).
140 # ##### Misc utilities #####
142 # Enable performance reports (measuring time used to perform various steps of importing or exporting).
151 def level_up(self
, message
=""):
153 self
.ref_time
.append(None)
155 print("\t" * self
.level
, message
, sep
="")
157 def level_down(self
, message
=""):
158 if not self
.ref_time
:
162 ref_time
= self
.ref_time
[self
.level
]
163 print("\t" * self
.level
,
164 "\tDone (%f sec)\n" % ((time
.process_time() - ref_time
) if ref_time
is not None else 0.0),
167 print("\t" * self
.level
, message
, sep
="")
168 del self
.ref_time
[self
.level
]
171 def step(self
, message
=""):
172 ref_time
= self
.ref_time
[self
.level
]
173 curr_time
= time
.process_time()
174 if ref_time
is not None:
175 print("\t" * self
.level
, "\tDone (%f sec)\n" % (curr_time
- ref_time
), sep
="")
176 self
.ref_time
[self
.level
] = curr_time
177 print("\t" * self
.level
, message
, sep
="")
183 def level_up(self
, message
=""):
186 def level_down(self
, message
=""):
189 def step(self
, message
=""):
193 # Scale/unit mess. FBX can store the 'reference' unit of a file in its UnitScaleFactor property
194 # (1.0 meaning centimeter, afaik). We use that to reflect user's default unit as set in Blender with scale_length.
195 # However, we always get values in BU (i.e. meters), so we have to reverse-apply that scale in global matrix...
196 # Note that when no default unit is available, we assume 'meters' (and hence scale by 100).
197 def units_blender_to_fbx_factor(scene
):
198 return 100.0 if (scene
.unit_settings
.system
== 'NONE') else (100.0 * scene
.unit_settings
.scale_length
)
201 # Note: this could be in a utility (math.units e.g.)...
204 "meter": 1.0, # Ref unit!
206 "millimeter": 1000.0,
207 "foot": 1.0 / 0.3048,
208 "inch": 1.0 / 0.0254,
209 "turn": 1.0, # Ref unit!
211 "radian": math
.pi
* 2.0,
212 "second": 1.0, # Ref unit!
217 def units_convertor(u_from
, u_to
):
218 """Return a convertor between specified units."""
219 conv
= UNITS
[u_to
] / UNITS
[u_from
]
220 return lambda v
: v
* conv
223 def units_convertor_iter(u_from
, u_to
):
224 """Return an iterable convertor between specified units."""
225 conv
= units_convertor(u_from
, u_to
)
234 def matrix4_to_array(mat
):
235 """Concatenate matrix's columns into a single, flat tuple"""
236 # blender matrix is row major, fbx is col major so transpose on write
237 return tuple(f
for v
in mat
.transposed() for f
in v
)
240 def array_to_matrix4(arr
):
241 """Convert a single 16-len tuple into a valid 4D Blender matrix"""
242 # Blender matrix is row major, fbx is col major so transpose on read
243 return Matrix(tuple(zip(*[iter(arr
)]*4))).transposed()
246 def similar_values(v1
, v2
, e
=1e-6):
247 """Return True if v1 and v2 are nearly the same."""
250 return ((abs(v1
- v2
) / max(abs(v1
), abs(v2
))) <= e
)
253 def similar_values_iter(v1
, v2
, e
=1e-6):
254 """Return True if iterables v1 and v2 are nearly the same."""
257 for v1
, v2
in zip(v1
, v2
):
258 if (v1
!= v2
) and ((abs(v1
- v2
) / max(abs(v1
), abs(v2
))) > e
):
262 def vcos_transformed_gen(raw_cos
, m
=None):
263 # Note: we could most likely get much better performances with numpy, but will leave this as TODO for now.
264 gen
= zip(*(iter(raw_cos
),) * 3)
265 return gen
if m
is None else (m
@ Vector(v
) for v
in gen
)
267 def nors_transformed_gen(raw_nors
, m
=None):
268 # Great, now normals are also expected 4D!
269 # XXX Back to 3D normals for now!
270 # gen = zip(*(iter(raw_nors),) * 3 + (_infinite_gen(1.0),))
271 gen
= zip(*(iter(raw_nors
),) * 3)
272 return gen
if m
is None else (m
@ Vector(v
) for v
in gen
)
275 # ##### UIDs code. #####
277 # ID class (mere int).
287 def _key_to_uuid(uuids
, key
):
288 # TODO: Check this is robust enough for our needs!
289 # Note: We assume we have already checked the related key wasn't yet in _keys_to_uids!
290 # As int64 is signed in FBX, we keep uids below 2**63...
291 if isinstance(key
, int) and 0 <= key
< 2**63:
292 # We can use value directly as id!
300 # Try to make our uid shorter!
302 t_uuid
= uuid
% int(1e9
)
303 if t_uuid
not in uuids
:
305 # Make sure our uuid *is* unique.
307 inc
= 1 if uuid
< 2**62 else -1
310 if 0 > uuid
>= 2**63:
311 # Note that this is more that unlikely, but does not harm anyway...
312 raise ValueError("Unable to generate an UUID for key {}".format(key
))
316 def get_fbx_uuid_from_key(key
):
318 Return an UUID for given key, which is assumed to be hashable.
320 uuid
= _keys_to_uuids
.get(key
, None)
322 uuid
= _key_to_uuid(_uuids_to_keys
, key
)
323 _keys_to_uuids
[key
] = uuid
324 _uuids_to_keys
[uuid
] = key
328 # XXX Not sure we'll actually need this one?
329 def get_key_from_fbx_uuid(uuid
):
331 Return the key which generated this uid.
333 assert(uuid
.__class
__ == UUID
)
334 return _uuids_to_keys
.get(uuid
, None)
337 # Blender-specific key generators
338 def get_bid_name(bid
):
339 library
= getattr(bid
, "library", None)
340 if library
is not None:
341 return "%s_L_%s" % (bid
.name
, library
.name
)
346 def get_blenderID_key(bid
):
347 if isinstance(bid
, Iterable
):
348 return "|".join("B" + e
.rna_type
.name
+ "#" + get_bid_name(e
) for e
in bid
)
350 return "B" + bid
.rna_type
.name
+ "#" + get_bid_name(bid
)
353 def get_blenderID_name(bid
):
354 if isinstance(bid
, Iterable
):
355 return "|".join(get_bid_name(e
) for e
in bid
)
357 return get_bid_name(bid
)
360 def get_blender_empty_key(obj
):
361 """Return bone's keys (Model and NodeAttribute)."""
362 return "|".join((get_blenderID_key(obj
), "Empty"))
365 def get_blender_mesh_shape_key(me
):
366 """Return main shape deformer's key."""
367 return "|".join((get_blenderID_key(me
), "Shape"))
370 def get_blender_mesh_shape_channel_key(me
, shape
):
371 """Return shape channel and geometry shape keys."""
372 return ("|".join((get_blenderID_key(me
), "Shape", get_blenderID_key(shape
))),
373 "|".join((get_blenderID_key(me
), "Geometry", get_blenderID_key(shape
))))
376 def get_blender_bone_key(armature
, bone
):
377 """Return bone's keys (Model and NodeAttribute)."""
378 return "|".join((get_blenderID_key((armature
, bone
)), "Data"))
381 def get_blender_bindpose_key(obj
, mesh
):
382 """Return object's bindpose key."""
383 return "|".join((get_blenderID_key(obj
), get_blenderID_key(mesh
), "BindPose"))
386 def get_blender_armature_skin_key(armature
, mesh
):
387 """Return armature's skin key."""
388 return "|".join((get_blenderID_key(armature
), get_blenderID_key(mesh
), "DeformerSkin"))
391 def get_blender_bone_cluster_key(armature
, mesh
, bone
):
392 """Return bone's cluster key."""
393 return "|".join((get_blenderID_key(armature
), get_blenderID_key(mesh
),
394 get_blenderID_key(bone
), "SubDeformerCluster"))
397 def get_blender_anim_id_base(scene
, ref_id
):
398 if ref_id
is not None:
399 return get_blenderID_key(scene
) + "|" + get_blenderID_key(ref_id
)
401 return get_blenderID_key(scene
)
404 def get_blender_anim_stack_key(scene
, ref_id
):
405 """Return single anim stack key."""
406 return get_blender_anim_id_base(scene
, ref_id
) + "|AnimStack"
409 def get_blender_anim_layer_key(scene
, ref_id
):
410 """Return ID's anim layer key."""
411 return get_blender_anim_id_base(scene
, ref_id
) + "|AnimLayer"
414 def get_blender_anim_curve_node_key(scene
, ref_id
, obj_key
, fbx_prop_name
):
415 """Return (stack/layer, ID, fbxprop) curve node key."""
416 return "|".join((get_blender_anim_id_base(scene
, ref_id
), obj_key
, fbx_prop_name
, "AnimCurveNode"))
419 def get_blender_anim_curve_key(scene
, ref_id
, obj_key
, fbx_prop_name
, fbx_prop_item_name
):
420 """Return (stack/layer, ID, fbxprop, item) curve key."""
421 return "|".join((get_blender_anim_id_base(scene
, ref_id
), obj_key
, fbx_prop_name
,
422 fbx_prop_item_name
, "AnimCurve"))
425 def get_blender_nodetexture_key(ma
, socket_names
):
426 return "|".join((get_blenderID_key(ma
), *socket_names
))
429 # ##### Element generators. #####
431 # Note: elem may be None, in this case the element is not added to any parent.
432 def elem_empty(elem
, name
):
433 sub_elem
= encode_bin
.FBXElem(name
)
435 elem
.elems
.append(sub_elem
)
439 def _elem_data_single(elem
, name
, value
, func_name
):
440 sub_elem
= elem_empty(elem
, name
)
441 getattr(sub_elem
, func_name
)(value
)
445 def _elem_data_vec(elem
, name
, value
, func_name
):
446 sub_elem
= elem_empty(elem
, name
)
447 func
= getattr(sub_elem
, func_name
)
453 def elem_data_single_bool(elem
, name
, value
):
454 return _elem_data_single(elem
, name
, value
, "add_bool")
457 def elem_data_single_int16(elem
, name
, value
):
458 return _elem_data_single(elem
, name
, value
, "add_int16")
461 def elem_data_single_int32(elem
, name
, value
):
462 return _elem_data_single(elem
, name
, value
, "add_int32")
465 def elem_data_single_int64(elem
, name
, value
):
466 return _elem_data_single(elem
, name
, value
, "add_int64")
469 def elem_data_single_float32(elem
, name
, value
):
470 return _elem_data_single(elem
, name
, value
, "add_float32")
473 def elem_data_single_float64(elem
, name
, value
):
474 return _elem_data_single(elem
, name
, value
, "add_float64")
477 def elem_data_single_bytes(elem
, name
, value
):
478 return _elem_data_single(elem
, name
, value
, "add_bytes")
481 def elem_data_single_string(elem
, name
, value
):
482 return _elem_data_single(elem
, name
, value
, "add_string")
485 def elem_data_single_string_unicode(elem
, name
, value
):
486 return _elem_data_single(elem
, name
, value
, "add_string_unicode")
489 def elem_data_single_bool_array(elem
, name
, value
):
490 return _elem_data_single(elem
, name
, value
, "add_bool_array")
493 def elem_data_single_int32_array(elem
, name
, value
):
494 return _elem_data_single(elem
, name
, value
, "add_int32_array")
497 def elem_data_single_int64_array(elem
, name
, value
):
498 return _elem_data_single(elem
, name
, value
, "add_int64_array")
501 def elem_data_single_float32_array(elem
, name
, value
):
502 return _elem_data_single(elem
, name
, value
, "add_float32_array")
505 def elem_data_single_float64_array(elem
, name
, value
):
506 return _elem_data_single(elem
, name
, value
, "add_float64_array")
509 def elem_data_single_byte_array(elem
, name
, value
):
510 return _elem_data_single(elem
, name
, value
, "add_byte_array")
513 def elem_data_vec_float64(elem
, name
, value
):
514 return _elem_data_vec(elem
, name
, value
, "add_float64")
517 # ##### Generators for standard FBXProperties70 properties. #####
519 def elem_properties(elem
):
520 return elem_empty(elem
, b
"Properties70")
523 # Properties definitions, format: (b"type_1", b"label(???)", "name_set_value_1", "name_set_value_2", ...)
524 # XXX Looks like there can be various variations of formats here... Will have to be checked ultimately!
525 # Also, those "custom" types like 'FieldOfView' or 'Lcl Translation' are pure nonsense,
526 # these are just Vector3D ultimately... *sigh* (again).
527 FBX_PROPERTIES_DEFINITIONS
= {
529 "p_bool": (b
"bool", b
"", "add_int32"), # Yes, int32 for a bool (and they do have a core bool type)!!!
530 "p_integer": (b
"int", b
"Integer", "add_int32"),
531 "p_ulonglong": (b
"ULongLong", b
"", "add_int64"),
532 "p_double": (b
"double", b
"Number", "add_float64"), # Non-animatable?
533 "p_number": (b
"Number", b
"", "add_float64"), # Animatable-only?
534 "p_enum": (b
"enum", b
"", "add_int32"),
535 "p_vector_3d": (b
"Vector3D", b
"Vector", "add_float64", "add_float64", "add_float64"), # Non-animatable?
536 "p_vector": (b
"Vector", b
"", "add_float64", "add_float64", "add_float64"), # Animatable-only?
537 "p_color_rgb": (b
"ColorRGB", b
"Color", "add_float64", "add_float64", "add_float64"), # Non-animatable?
538 "p_color": (b
"Color", b
"", "add_float64", "add_float64", "add_float64"), # Animatable-only?
539 "p_string": (b
"KString", b
"", "add_string_unicode"),
540 "p_string_url": (b
"KString", b
"Url", "add_string_unicode"),
541 "p_timestamp": (b
"KTime", b
"Time", "add_int64"),
542 "p_datetime": (b
"DateTime", b
"", "add_string_unicode"),
544 "p_object": (b
"object", b
""), # XXX Check this! No value for this prop??? Would really like to know how it works!
545 "p_compound": (b
"Compound", b
""),
546 # Specific types (sic).
547 # ## Objects (Models).
548 "p_lcl_translation": (b
"Lcl Translation", b
"", "add_float64", "add_float64", "add_float64"),
549 "p_lcl_rotation": (b
"Lcl Rotation", b
"", "add_float64", "add_float64", "add_float64"),
550 "p_lcl_scaling": (b
"Lcl Scaling", b
"", "add_float64", "add_float64", "add_float64"),
551 "p_visibility": (b
"Visibility", b
"", "add_float64"),
552 "p_visibility_inheritance": (b
"Visibility Inheritance", b
"", "add_int32"),
554 "p_roll": (b
"Roll", b
"", "add_float64"),
555 "p_opticalcenterx": (b
"OpticalCenterX", b
"", "add_float64"),
556 "p_opticalcentery": (b
"OpticalCenterY", b
"", "add_float64"),
557 "p_fov": (b
"FieldOfView", b
"", "add_float64"),
558 "p_fov_x": (b
"FieldOfViewX", b
"", "add_float64"),
559 "p_fov_y": (b
"FieldOfViewY", b
"", "add_float64"),
563 def _elem_props_set(elem
, ptype
, name
, value
, flags
):
564 p
= elem_data_single_string(elem
, b
"P", name
)
569 getattr(p
, ptype
[2])(value
)
571 # We assume value is iterable, else it's a bug!
572 for callback
, val
in zip(ptype
[2:], value
):
573 getattr(p
, callback
)(val
)
576 def _elem_props_flags(animatable
, animated
, custom
):
577 # XXX: There are way more flags, see
578 # http://help.autodesk.com/view/FBX/2015/ENU/?guid=__cpp_ref_class_fbx_property_flags_html
579 # Unfortunately, as usual, no doc at all about their 'translation' in actual FBX file format.
580 # Curse you-know-who.
587 # Seems that customprops always need those 'flags', see T69554. Go figure...
591 # Seems that customprops always need those 'flags', see T69554. Go figure...
596 def elem_props_set(elem
, ptype
, name
, value
=None, animatable
=False, animated
=False, custom
=False):
597 ptype
= FBX_PROPERTIES_DEFINITIONS
[ptype
]
598 _elem_props_set(elem
, ptype
, name
, value
, _elem_props_flags(animatable
, animated
, custom
))
601 def elem_props_compound(elem
, cmpd_name
, custom
=False):
602 def _setter(ptype
, name
, value
, animatable
=False, animated
=False, custom
=False):
603 name
= cmpd_name
+ b
"|" + name
604 elem_props_set(elem
, ptype
, name
, value
, animatable
=animatable
, animated
=animated
, custom
=custom
)
606 elem_props_set(elem
, "p_compound", cmpd_name
, custom
=custom
)
610 def elem_props_template_init(templates
, template_type
):
612 Init a writing template of given type, for *one* element's properties.
615 tmpl
= templates
.get(template_type
)
617 written
= tmpl
.written
[0]
618 props
= tmpl
.properties
619 ret
= {name
: [val
, ptype
, anim
, written
] for name
, (val
, ptype
, anim
) in props
.items()}
623 def elem_props_template_set(template
, elem
, ptype_name
, name
, value
, animatable
=False, animated
=False):
625 Only add a prop if the same value is not already defined in given template.
626 Note it is important to not give iterators as value, here!
628 ptype
= FBX_PROPERTIES_DEFINITIONS
[ptype_name
]
631 tmpl_val
, tmpl_ptype
, tmpl_animatable
, tmpl_written
= template
.get(name
, (None, None, False, False))
632 # Note animatable flag from template takes precedence over given one, if applicable.
633 # However, animated properties are always written, since they cannot match their template!
634 if tmpl_ptype
is not None and not animated
:
636 ((len(ptype
) == 3 and (tmpl_val
, tmpl_ptype
) == (value
, ptype_name
)) or
637 (len(ptype
) > 3 and (tuple(tmpl_val
), tmpl_ptype
) == (value
, ptype_name
)))):
638 return # Already in template and same value.
639 _elem_props_set(elem
, ptype
, name
, value
, _elem_props_flags(tmpl_animatable
, animated
, False))
640 template
[name
][3] = True
642 _elem_props_set(elem
, ptype
, name
, value
, _elem_props_flags(animatable
, animated
, False))
645 def elem_props_template_finalize(template
, elem
):
647 Finalize one element's template/props.
648 Issue is, some templates might be "needed" by different types (e.g. NodeAttribute is for lights, cameras, etc.),
649 but values for only *one* subtype can be written as template. So we have to be sure we write those for the other
650 subtypes in each and every elements, if they are not overridden by that element.
651 Yes, hairy, FBX that is to say. When they could easily support several subtypes per template... :(
653 for name
, (value
, ptype_name
, animatable
, written
) in template
.items():
656 ptype
= FBX_PROPERTIES_DEFINITIONS
[ptype_name
]
657 _elem_props_set(elem
, ptype
, name
, value
, _elem_props_flags(animatable
, False, False))
660 # ##### Templates #####
661 # TODO: check all those "default" values, they should match Blender's default as much as possible, I guess?
663 FBXTemplate
= namedtuple("FBXTemplate", ("type_name", "prop_type_name", "properties", "nbr_users", "written"))
666 def fbx_templates_generate(root
, fbx_templates
):
667 # We may have to gather different templates in the same node (e.g. NodeAttribute template gathers properties
668 # for Lights, Cameras, LibNodes, etc.).
669 ref_templates
= {(tmpl
.type_name
, tmpl
.prop_type_name
): tmpl
for tmpl
in fbx_templates
.values()}
672 for type_name
, prop_type_name
, properties
, nbr_users
, _written
in fbx_templates
.values():
673 tmpl
= templates
.setdefault(type_name
, [{}, 0])
674 tmpl
[0][prop_type_name
] = (properties
, nbr_users
)
677 for type_name
, (subprops
, nbr_users
) in templates
.items():
678 template
= elem_data_single_string(root
, b
"ObjectType", type_name
)
679 elem_data_single_int32(template
, b
"Count", nbr_users
)
681 if len(subprops
) == 1:
682 prop_type_name
, (properties
, _nbr_sub_type_users
) = next(iter(subprops
.items()))
683 subprops
= (prop_type_name
, properties
)
684 ref_templates
[(type_name
, prop_type_name
)].written
[0] = True
686 # Ack! Even though this could/should work, looks like it is not supported. So we have to chose one. :|
687 max_users
= max_props
= -1
688 written_prop_type_name
= None
689 for prop_type_name
, (properties
, nbr_sub_type_users
) in subprops
.items():
690 if nbr_sub_type_users
> max_users
or (nbr_sub_type_users
== max_users
and len(properties
) > max_props
):
691 max_users
= nbr_sub_type_users
692 max_props
= len(properties
)
693 written_prop_type_name
= prop_type_name
694 subprops
= (written_prop_type_name
, properties
)
695 ref_templates
[(type_name
, written_prop_type_name
)].written
[0] = True
697 prop_type_name
, properties
= subprops
698 if prop_type_name
and properties
:
699 elem
= elem_data_single_string(template
, b
"PropertyTemplate", prop_type_name
)
700 props
= elem_properties(elem
)
701 for name
, (value
, ptype
, animatable
) in properties
.items():
703 elem_props_set(props
, ptype
, name
, value
, animatable
=animatable
)
704 except Exception as e
:
705 print("Failed to write template prop (%r)" % e
)
706 print(props
, ptype
, name
, value
, animatable
)
709 # ##### FBX animation helpers. #####
712 class AnimationCurveNodeWrapper
:
714 This class provides a same common interface for all (FBX-wise) AnimationCurveNode and AnimationCurve elements,
715 and easy API to handle those.
718 'elem_keys', '_keys', 'default_values', 'fbx_group', 'fbx_gname', 'fbx_props',
719 'force_keying', 'force_startend_keying')
722 'LCL_TRANSLATION': ("Lcl Translation", "T", ("X", "Y", "Z")),
723 'LCL_ROTATION': ("Lcl Rotation", "R", ("X", "Y", "Z")),
724 'LCL_SCALING': ("Lcl Scaling", "S", ("X", "Y", "Z")),
725 'SHAPE_KEY': ("DeformPercent", "DeformPercent", ("DeformPercent",)),
726 'CAMERA_FOCAL': ("FocalLength", "FocalLength", ("FocalLength",)),
727 'CAMERA_FOCUS_DISTANCE': ("FocusDistance", "FocusDistance", ("FocusDistance",)),
730 def __init__(self
, elem_key
, kind
, force_keying
, force_startend_keying
, default_values
=...):
731 self
.elem_keys
= [elem_key
]
732 assert(kind
in self
.kinds
)
733 self
.fbx_group
= [self
.kinds
[kind
][0]]
734 self
.fbx_gname
= [self
.kinds
[kind
][1]]
735 self
.fbx_props
= [self
.kinds
[kind
][2]]
736 self
.force_keying
= force_keying
737 self
.force_startend_keying
= force_startend_keying
738 self
._keys
= [] # (frame, values, write_flags)
739 if default_values
is not ...:
740 assert(len(default_values
) == len(self
.fbx_props
[0]))
741 self
.default_values
= default_values
743 self
.default_values
= (0.0) * len(self
.fbx_props
[0])
746 # We are 'True' if we do have some validated keyframes...
747 return bool(self
._keys
) and (True in ((True in k
[2]) for k
in self
._keys
))
749 def add_group(self
, elem_key
, fbx_group
, fbx_gname
, fbx_props
):
751 Add another whole group stuff (curvenode, animated item/prop + curvnode/curve identifiers).
752 E.g. Shapes animations is written twice, houra!
754 assert(len(fbx_props
) == len(self
.fbx_props
[0]))
755 self
.elem_keys
.append(elem_key
)
756 self
.fbx_group
.append(fbx_group
)
757 self
.fbx_gname
.append(fbx_gname
)
758 self
.fbx_props
.append(fbx_props
)
760 def add_keyframe(self
, frame
, values
):
762 Add a new keyframe to all curves of the group.
764 assert(len(values
) == len(self
.fbx_props
[0]))
765 self
._keys
.append((frame
, values
, [True] * len(values
))) # write everything by default.
767 def simplify(self
, fac
, step
, force_keep
=False):
769 Simplifies sampled curves by only enabling samples when:
770 * their values relatively differ from the previous sample ones.
778 # So that, with default factor and step values (1), we get:
779 min_reldiff_fac
= fac
* 1.0e-3 # min relative value evolution: 0.1% of current 'order of magnitude'.
780 min_absdiff_fac
= 0.1 # A tenth of reldiff...
783 p_currframe
, p_key
, p_key_write
= keys
[0]
784 p_keyed
= list(p_key
)
785 are_keyed
= [False] * len(p_key
)
786 for currframe
, key
, key_write
in keys
:
787 for idx
, (val
, p_val
) in enumerate(zip(key
, p_key
)):
788 key_write
[idx
] = False
789 p_keyedval
= p_keyed
[idx
]
791 # Never write keyframe when value is exactly the same as prev one!
793 # This is contracted form of relative + absolute-near-zero difference:
794 # absdiff = abs(a - b)
795 # if absdiff < min_reldiff_fac * min_absdiff_fac:
797 # return (absdiff / ((abs(a) + abs(b)) / 2)) > min_reldiff_fac
798 # Note that we ignore the '/ 2' part here, since it's not much significant for us.
799 if abs(val
- p_val
) > (min_reldiff_fac
* max(abs(val
) + abs(p_val
), min_absdiff_fac
)):
800 # If enough difference from previous sampled value, key this value *and* the previous one!
801 key_write
[idx
] = True
802 p_key_write
[idx
] = True
804 are_keyed
[idx
] = True
805 elif abs(val
- p_keyedval
) > (min_reldiff_fac
* max((abs(val
) + abs(p_keyedval
)), min_absdiff_fac
)):
806 # Else, if enough difference from previous keyed value, key this value only!
807 key_write
[idx
] = True
809 are_keyed
[idx
] = True
810 p_currframe
, p_key
, p_key_write
= currframe
, key
, key_write
812 # If we write nothing (action doing nothing) and are in 'force_keep' mode, we key everything! :P
814 # Also, it seems some importers (e.g. UE4) do not handle correctly armatures where some bones
815 # are not animated, but are children of animated ones, so added an option to systematically force writing
816 # one key in this case.
817 # See T41719, T41605, T41254...
818 if self
.force_keying
or (force_keep
and not self
):
819 are_keyed
[:] = [True] * len(are_keyed
)
821 # If we did key something, ensure first and last sampled values are keyed as well.
822 if self
.force_startend_keying
:
823 for idx
, is_keyed
in enumerate(are_keyed
):
825 keys
[0][2][idx
] = keys
[-1][2][idx
] = True
827 def get_final_data(self
, scene
, ref_id
, force_keep
=False):
829 Yield final anim data for this 'curvenode' (for all curvenodes defined).
830 force_keep is to force to keep a curve even if it only has one valid keyframe.
832 curves
= [[] for k
in self
._keys
[0][1]]
833 for currframe
, key
, key_write
in self
._keys
:
834 for curve
, val
, wrt
in zip(curves
, key
, key_write
):
836 curve
.append((currframe
, val
))
838 force_keep
= force_keep
or self
.force_keying
839 for elem_key
, fbx_group
, fbx_gname
, fbx_props
in \
840 zip(self
.elem_keys
, self
.fbx_group
, self
.fbx_gname
, self
.fbx_props
):
841 group_key
= get_blender_anim_curve_node_key(scene
, ref_id
, elem_key
, fbx_group
)
843 for c
, def_val
, fbx_item
in zip(curves
, self
.default_values
, fbx_props
):
844 fbx_item
= FBX_ANIM_PROPSGROUP_NAME
+ "|" + fbx_item
845 curve_key
= get_blender_anim_curve_key(scene
, ref_id
, elem_key
, fbx_group
, fbx_item
)
846 # (curve key, default value, keyframes, write flag).
847 group
[fbx_item
] = (curve_key
, def_val
, c
,
848 True if (len(c
) > 1 or (len(c
) > 0 and force_keep
)) else False)
849 yield elem_key
, group_key
, group
, fbx_group
, fbx_gname
852 # ##### FBX objects generators. #####
854 # FBX Model-like data (i.e. Blender objects, depsgraph instances and bones) are wrapped in ObjectWrapper.
855 # This allows us to have a (nearly) same code FBX-wise for all those types.
856 # The wrapper tries to stay as small as possible, by mostly using callbacks (property(get...))
857 # to actual Blender data it contains.
858 # Note it caches its instances, so that you may call several times ObjectWrapper(your_object)
859 # with a minimal cost (just re-computing the key).
861 class MetaObjectWrapper(type):
862 def __call__(cls
, bdata
, armature
=None):
866 if isinstance(bdata
, Object
):
867 key
= get_blenderID_key(bdata
)
868 elif isinstance(bdata
, DepsgraphObjectInstance
):
869 if bdata
.is_instance
:
870 key
= "|".join((get_blenderID_key((bdata
.parent
.original
, bdata
.instance_object
.original
)),
871 cls
._get
_dup
_num
_id
(bdata
)))
872 dup_mat
= bdata
.matrix_world
.copy()
874 key
= get_blenderID_key(bdata
.object.original
)
875 else: # isinstance(bdata, (Bone, PoseBone)):
876 if isinstance(bdata
, PoseBone
):
877 bdata
= armature
.data
.bones
[bdata
.name
]
878 key
= get_blenderID_key((armature
, bdata
))
880 cache
= getattr(cls
, "_cache", None)
882 cache
= cls
._cache
= {}
883 instance
= cache
.get(key
)
884 if instance
is not None:
885 # Duplis hack: since dupli instances are not persistent in Blender (we have to re-create them to get updated
886 # info like matrix...), we *always* need to reset that matrix when calling ObjectWrapper() (all
887 # other data is supposed valid during whole cache live span, so we can skip resetting it).
888 instance
._dupli
_matrix
= dup_mat
891 instance
= cls
.__new
__(cls
, bdata
, armature
)
892 instance
.__init
__(bdata
, armature
)
894 instance
._dupli
_matrix
= dup_mat
895 cache
[key
] = instance
899 class ObjectWrapper(metaclass
=MetaObjectWrapper
):
901 This class provides a same common interface for all (FBX-wise) object-like elements:
903 * Blender Bone and PoseBone
904 * Blender DepsgraphObjectInstance (for dulis).
905 Note since a same Blender object might be 'mapped' to several FBX models (esp. with duplis),
906 we need to use a key to identify each.
909 'name', 'key', 'bdata', 'parented_to_armature',
910 '_tag', '_ref', '_dupli_matrix'
914 def cache_clear(cls
):
915 if hasattr(cls
, "_cache"):
919 def _get_dup_num_id(bdata
):
920 INVALID_IDS
= {2147483647, 0}
921 pids
= tuple(bdata
.persistent_id
)
924 for idx
, i
in enumerate(pids
[::-1]):
925 if i
not in INVALID_IDS
or (idx
== len(pids
) and i
== 0 and prev_i
!= 0):
926 idx_valid
= len(pids
) - idx
929 return ".".join(str(i
) for i
in pids
[:idx_valid
])
931 def __init__(self
, bdata
, armature
=None):
933 bdata might be an Object (deprecated), DepsgraphObjectInstance, Bone or PoseBone.
934 If Bone or PoseBone, armature Object must be provided.
936 # Note: DepsgraphObjectInstance are purely runtime data, they become invalid as soon as we step to the next item!
937 # Hence we have to immediately copy *all* needed data...
938 if isinstance(bdata
, Object
): # DEPRECATED
940 self
.name
= get_blenderID_name(bdata
)
943 elif isinstance(bdata
, DepsgraphObjectInstance
):
944 if bdata
.is_instance
:
945 # Note that dupli instance matrix is set by meta-class initialization.
947 self
.name
= "|".join((get_blenderID_name((bdata
.parent
.original
, bdata
.instance_object
.original
)),
948 "Dupli", self
._get
_dup
_num
_id
(bdata
)))
949 self
.bdata
= bdata
.instance_object
.original
950 self
._ref
= bdata
.parent
.original
953 self
.name
= get_blenderID_name(bdata
)
954 self
.bdata
= bdata
.object.original
956 else: # isinstance(bdata, (Bone, PoseBone)):
957 if isinstance(bdata
, PoseBone
):
958 bdata
= armature
.data
.bones
[bdata
.name
]
960 self
.name
= get_blenderID_name(bdata
)
963 self
.parented_to_armature
= False
965 def __eq__(self
, other
):
966 return isinstance(other
, self
.__class
__) and self
.key
== other
.key
969 return hash(self
.key
)
974 # #### Common to all _tag values.
975 def get_fbx_uuid(self
):
976 return get_fbx_uuid_from_key(self
.key
)
977 fbx_uuid
= property(get_fbx_uuid
)
979 # XXX Not sure how much that’s useful now... :/
981 return self
.bdata
.hide_viewport
if self
._tag
in {'OB', 'DP'} else self
.bdata
.hide
982 hide
= property(get_hide
)
984 def get_parent(self
):
985 if self
._tag
== 'OB':
986 if (self
.bdata
.parent
and self
.bdata
.parent
.type == 'ARMATURE' and
987 self
.bdata
.parent_type
== 'BONE' and self
.bdata
.parent_bone
):
988 # Try to parent to a bone.
989 bo_par
= self
.bdata
.parent
.pose
.bones
.get(self
.bdata
.parent_bone
, None)
991 return ObjectWrapper(bo_par
, self
.bdata
.parent
)
992 else: # Fallback to mere object parenting.
993 return ObjectWrapper(self
.bdata
.parent
)
995 # Mere object parenting.
996 return ObjectWrapper(self
.bdata
.parent
)
997 elif self
._tag
== 'DP':
998 return ObjectWrapper(self
._ref
)
999 else: # self._tag == 'BO'
1000 return ObjectWrapper(self
.bdata
.parent
, self
._ref
) or ObjectWrapper(self
._ref
)
1001 parent
= property(get_parent
)
1003 def get_bdata_pose_bone(self
):
1004 if self
._tag
== 'BO':
1005 return self
._ref
.pose
.bones
[self
.bdata
.name
]
1007 bdata_pose_bone
= property(get_bdata_pose_bone
)
1009 def get_matrix_local(self
):
1010 if self
._tag
== 'OB':
1011 return self
.bdata
.matrix_local
.copy()
1012 elif self
._tag
== 'DP':
1013 return self
._ref
.matrix_world
.inverted_safe() @ self
._dupli
_matrix
1014 else: # 'BO', current pose
1015 # PoseBone.matrix is in armature space, bring in back in real local one!
1016 par
= self
.bdata
.parent
1017 par_mat_inv
= self
._ref
.pose
.bones
[par
.name
].matrix
.inverted_safe() if par
else Matrix()
1018 return par_mat_inv
@ self
._ref
.pose
.bones
[self
.bdata
.name
].matrix
1019 matrix_local
= property(get_matrix_local
)
1021 def get_matrix_global(self
):
1022 if self
._tag
== 'OB':
1023 return self
.bdata
.matrix_world
.copy()
1024 elif self
._tag
== 'DP':
1025 return self
._dupli
_matrix
1026 else: # 'BO', current pose
1027 return self
._ref
.matrix_world
@ self
._ref
.pose
.bones
[self
.bdata
.name
].matrix
1028 matrix_global
= property(get_matrix_global
)
1030 def get_matrix_rest_local(self
):
1031 if self
._tag
== 'BO':
1032 # Bone.matrix_local is in armature space, bring in back in real local one!
1033 par
= self
.bdata
.parent
1034 par_mat_inv
= par
.matrix_local
.inverted_safe() if par
else Matrix()
1035 return par_mat_inv
@ self
.bdata
.matrix_local
1037 return self
.matrix_local
.copy()
1038 matrix_rest_local
= property(get_matrix_rest_local
)
1040 def get_matrix_rest_global(self
):
1041 if self
._tag
== 'BO':
1042 return self
._ref
.matrix_world
@ self
.bdata
.matrix_local
1044 return self
.matrix_global
.copy()
1045 matrix_rest_global
= property(get_matrix_rest_global
)
1047 # #### Transform and helpers
1048 def has_valid_parent(self
, objects
):
1051 if self
._tag
== 'OB':
1052 par_type
= self
.bdata
.parent_type
1053 if par_type
in {'OBJECT', 'BONE'}:
1056 print("Sorry, “{}” parenting type is not supported".format(par_type
))
1061 def use_bake_space_transform(self
, scene_data
):
1062 # NOTE: Only applies to object types supporting this!!! Currently, only meshes and the like...
1063 # TODO: Check whether this can work for bones too...
1064 return (scene_data
.settings
.bake_space_transform
and self
._tag
in {'OB', 'DP'} and
1065 self
.bdata
.type in BLENDER_OBJECT_TYPES_MESHLIKE |
{'EMPTY'})
1067 def fbx_object_matrix(self
, scene_data
, rest
=False, local_space
=False, global_space
=False):
1069 Generate object transform matrix (*always* in matching *FBX* space!).
1070 If local_space is True, returned matrix is *always* in local space.
1071 Else if global_space is True, returned matrix is always in world space.
1072 If both local_space and global_space are False, returned matrix is in parent space if parent is valid,
1073 else in world space.
1074 Note local_space has precedence over global_space.
1075 If rest is True and object is a Bone, returns matching rest pose transform instead of current pose one.
1076 Applies specific rotation to bones, lamps and cameras (conversion Blender -> FBX).
1078 # Objects which are not bones and do not have any parent are *always* in global space
1079 # (unless local_space is True!).
1080 is_global
= (not local_space
and
1081 (global_space
or not (self
._tag
in {'DP', 'BO'} or self
.has_valid_parent(scene_data
.objects
))))
1083 # Objects (meshes!) parented to armature are not parented to anything in FBX, hence we need them
1084 # in global space, which is their 'virtual' local space...
1085 is_global
= is_global
or self
.parented_to_armature
1087 # Since we have to apply corrections to some types of object, we always need local Blender space here...
1088 matrix
= self
.matrix_rest_local
if rest
else self
.matrix_local
1089 parent
= self
.parent
1091 # Bones, lamps and cameras need to be rotated (in local space!).
1092 if self
._tag
== 'BO':
1093 # If we have a bone parent we need to undo the parent correction.
1094 if not is_global
and scene_data
.settings
.bone_correction_matrix_inv
and parent
and parent
.is_bone
:
1095 matrix
= scene_data
.settings
.bone_correction_matrix_inv
@ matrix
1096 # Apply the bone correction.
1097 if scene_data
.settings
.bone_correction_matrix
:
1098 matrix
= matrix
@ scene_data
.settings
.bone_correction_matrix
1099 elif self
.bdata
.type == 'LIGHT':
1100 matrix
= matrix
@ MAT_CONVERT_LIGHT
1101 elif self
.bdata
.type == 'CAMERA':
1102 matrix
= matrix
@ MAT_CONVERT_CAMERA
1104 if self
._tag
in {'DP', 'OB'} and parent
:
1105 if parent
._tag
== 'BO':
1106 # In bone parent case, we get transformation in **bone tip** space (sigh).
1107 # Have to bring it back into bone root, which is FBX expected value.
1108 matrix
= Matrix
.Translation((0, (parent
.bdata
.tail
- parent
.bdata
.head
).length
, 0)) @ matrix
1110 # Our matrix is in local space, time to bring it in its final desired space.
1113 # Move matrix to global Blender space.
1114 matrix
= (parent
.matrix_rest_global
if rest
else parent
.matrix_global
) @ matrix
1115 elif parent
.use_bake_space_transform(scene_data
):
1116 # Blender's and FBX's local space of parent may differ if we use bake_space_transform...
1117 # Apply parent's *Blender* local space...
1118 matrix
= (parent
.matrix_rest_local
if rest
else parent
.matrix_local
) @ matrix
1119 # ...and move it back into parent's *FBX* local space.
1120 par_mat
= parent
.fbx_object_matrix(scene_data
, rest
=rest
, local_space
=True)
1121 matrix
= par_mat
.inverted_safe() @ matrix
1123 if self
.use_bake_space_transform(scene_data
):
1124 # If we bake the transforms we need to post-multiply inverse global transform.
1125 # This means that the global transform will not apply to children of this transform.
1126 matrix
= matrix
@ scene_data
.settings
.global_matrix_inv
1128 # In any case, pre-multiply the global matrix to get it in FBX global space!
1129 matrix
= scene_data
.settings
.global_matrix
@ matrix
1133 def fbx_object_tx(self
, scene_data
, rest
=False, rot_euler_compat
=None):
1135 Generate object transform data (always in local space when possible).
1137 matrix
= self
.fbx_object_matrix(scene_data
, rest
=rest
)
1138 loc
, rot
, scale
= matrix
.decompose()
1139 matrix_rot
= rot
.to_matrix()
1140 # quat -> euler, we always use 'XYZ' order, use ref rotation if given.
1141 if rot_euler_compat
is not None:
1142 rot
= rot
.to_euler('XYZ', rot_euler_compat
)
1144 rot
= rot
.to_euler('XYZ')
1145 return loc
, rot
, scale
, matrix
, matrix_rot
1147 # #### _tag dependent...
1148 def get_is_object(self
):
1149 return self
._tag
== 'OB'
1150 is_object
= property(get_is_object
)
1152 def get_is_dupli(self
):
1153 return self
._tag
== 'DP'
1154 is_dupli
= property(get_is_dupli
)
1156 def get_is_bone(self
):
1157 return self
._tag
== 'BO'
1158 is_bone
= property(get_is_bone
)
1161 if self
._tag
in {'OB', 'DP'}:
1162 return self
.bdata
.type
1164 type = property(get_type
)
1166 def get_armature(self
):
1167 if self
._tag
== 'BO':
1168 return ObjectWrapper(self
._ref
)
1170 armature
= property(get_armature
)
1172 def get_bones(self
):
1173 if self
._tag
== 'OB' and self
.bdata
.type == 'ARMATURE':
1174 return (ObjectWrapper(bo
, self
.bdata
) for bo
in self
.bdata
.data
.bones
)
1176 bones
= property(get_bones
)
1178 def get_material_slots(self
):
1179 if self
._tag
in {'OB', 'DP'}:
1180 return self
.bdata
.material_slots
1182 material_slots
= property(get_material_slots
)
1184 def is_deformed_by_armature(self
, arm_obj
):
1185 if not (self
.is_object
and self
.type == 'MESH'):
1187 if self
.parent
== arm_obj
and self
.bdata
.parent_type
== 'ARMATURE':
1189 for mod
in self
.bdata
.modifiers
:
1190 if mod
.type == 'ARMATURE' and mod
.object == arm_obj
.bdata
:
1194 def dupli_list_gen(self
, depsgraph
):
1195 if self
._tag
== 'OB' and self
.bdata
.is_instancer
:
1196 return (ObjectWrapper(dup
) for dup
in depsgraph
.object_instances
1197 if dup
.parent
and ObjectWrapper(dup
.parent
.original
) == self
)
1201 def fbx_name_class(name
, cls
):
1202 return FBX_NAME_CLASS_SEP
.join((name
, cls
))
1205 # ##### Top-level FBX data container. #####
1207 # Helper sub-container gathering all exporter settings related to media (texture files).
1208 FBXExportSettingsMedia
= namedtuple("FBXExportSettingsMedia", (
1209 "path_mode", "base_src", "base_dst", "subdir",
1210 "embed_textures", "copy_set", "embedded_set",
1213 # Helper container gathering all exporter settings.
1214 FBXExportSettings
= namedtuple("FBXExportSettings", (
1215 "report", "to_axes", "global_matrix", "global_scale", "apply_unit_scale", "unit_scale",
1216 "bake_space_transform", "global_matrix_inv", "global_matrix_inv_transposed",
1217 "context_objects", "object_types", "use_mesh_modifiers", "use_mesh_modifiers_render",
1218 "mesh_smooth_type", "use_subsurf", "use_mesh_edges", "use_tspace", "use_triangles",
1219 "armature_nodetype", "use_armature_deform_only", "add_leaf_bones",
1220 "bone_correction_matrix", "bone_correction_matrix_inv",
1221 "bake_anim", "bake_anim_use_all_bones", "bake_anim_use_nla_strips", "bake_anim_use_all_actions",
1222 "bake_anim_step", "bake_anim_simplify_factor", "bake_anim_force_startend_keying",
1223 "use_metadata", "media_settings", "use_custom_props", "colors_type",
1226 # Helper container gathering some data we need multiple times:
1228 # * settings, scene.
1231 # * skinning data (binding armature/mesh).
1233 FBXExportData
= namedtuple("FBXExportData", (
1234 "templates", "templates_users", "connections",
1235 "settings", "scene", "depsgraph", "objects", "animations", "animated", "frame_start", "frame_end",
1236 "data_empties", "data_lights", "data_cameras", "data_meshes", "mesh_material_indices",
1237 "data_bones", "data_leaf_bones", "data_deformers_skin", "data_deformers_shape",
1238 "data_world", "data_materials", "data_textures", "data_videos",
1241 # Helper container gathering all importer settings.
1242 FBXImportSettings
= namedtuple("FBXImportSettings", (
1243 "report", "to_axes", "global_matrix", "global_scale",
1244 "bake_space_transform", "global_matrix_inv", "global_matrix_inv_transposed",
1245 "use_custom_normals", "use_image_search",
1246 "use_alpha_decals", "decal_offset",
1247 "use_anim", "anim_offset",
1249 "use_custom_props", "use_custom_props_enum_as_string",
1250 "nodal_material_wrap_map", "image_cache",
1251 "ignore_leaf_bones", "force_connect_children", "automatic_bone_orientation", "bone_correction_matrix",
1252 "use_prepost_rot", "colors_type",