File headers: use SPDX license identifiers
[blender-addons.git] / io_scene_fbx / fbx_utils.py
blobe3bb40287807fc74852acaabd6645703b3f35b91
1 # SPDX-License-Identifier: GPL-2.0-or-later
3 # <pep8 compliant>
5 # Script copyright (C) Campbell Barton, Bastien Montagne
8 import math
9 import time
11 from collections import namedtuple
12 from collections.abc import Iterable
13 from itertools import zip_longest, chain
15 import bpy
16 import bpy_extras
17 from bpy.types import Object, Bone, PoseBone, DepsgraphObjectInstance
18 from mathutils import Vector, Matrix
20 from . import encode_bin, data_types
23 # "Constants"
24 FBX_VERSION = 7400
25 FBX_HEADER_VERSION = 1003
26 FBX_SCENEINFO_VERSION = 100
27 FBX_TEMPLATES_VERSION = 100
29 FBX_MODELS_VERSION = 232
31 FBX_GEOMETRY_VERSION = 124
32 # Revert back normals to 101 (simple 3D values) for now, 102 (4D + weights) seems not well supported by most apps
33 # currently, apart from some AD products.
34 FBX_GEOMETRY_NORMAL_VERSION = 101
35 FBX_GEOMETRY_BINORMAL_VERSION = 101
36 FBX_GEOMETRY_TANGENT_VERSION = 101
37 FBX_GEOMETRY_SMOOTHING_VERSION = 102
38 FBX_GEOMETRY_CREASE_VERSION = 101
39 FBX_GEOMETRY_VCOLOR_VERSION = 101
40 FBX_GEOMETRY_UV_VERSION = 101
41 FBX_GEOMETRY_MATERIAL_VERSION = 101
42 FBX_GEOMETRY_LAYER_VERSION = 100
43 FBX_GEOMETRY_SHAPE_VERSION = 100
44 FBX_DEFORMER_SHAPE_VERSION = 100
45 FBX_DEFORMER_SHAPECHANNEL_VERSION = 100
46 FBX_POSE_BIND_VERSION = 100
47 FBX_DEFORMER_SKIN_VERSION = 101
48 FBX_DEFORMER_CLUSTER_VERSION = 100
49 FBX_MATERIAL_VERSION = 102
50 FBX_TEXTURE_VERSION = 202
51 FBX_ANIM_KEY_VERSION = 4008
53 FBX_NAME_CLASS_SEP = b"\x00\x01"
54 FBX_ANIM_PROPSGROUP_NAME = "d"
56 FBX_KTIME = 46186158000 # This is the number of "ktimes" in one second (yep, precision over the nanosecond...)
59 MAT_CONVERT_LIGHT = Matrix.Rotation(math.pi / 2.0, 4, 'X') # Blender is -Z, FBX is -Y.
60 MAT_CONVERT_CAMERA = Matrix.Rotation(math.pi / 2.0, 4, 'Y') # Blender is -Z, FBX is +X.
61 # XXX I can't get this working :(
62 # MAT_CONVERT_BONE = Matrix.Rotation(math.pi / 2.0, 4, 'Z') # Blender is +Y, FBX is -X.
63 MAT_CONVERT_BONE = Matrix()
66 BLENDER_OTHER_OBJECT_TYPES = {'CURVE', 'SURFACE', 'FONT', 'META'}
67 BLENDER_OBJECT_TYPES_MESHLIKE = {'MESH'} | BLENDER_OTHER_OBJECT_TYPES
70 # Lamps.
71 FBX_LIGHT_TYPES = {
72 'POINT': 0, # Point.
73 'SUN': 1, # Directional.
74 'SPOT': 2, # Spot.
75 'HEMI': 1, # Directional.
76 'AREA': 3, # Area.
78 FBX_LIGHT_DECAY_TYPES = {
79 'CONSTANT': 0, # None.
80 'INVERSE_LINEAR': 1, # Linear.
81 'INVERSE_SQUARE': 2, # Quadratic.
82 'INVERSE_COEFFICIENTS': 2, # Quadratic...
83 'CUSTOM_CURVE': 2, # Quadratic.
84 'LINEAR_QUADRATIC_WEIGHTED': 2, # Quadratic.
88 RIGHT_HAND_AXES = {
89 # Up, Forward -> FBX values (tuples of (axis, sign), Up, Front, Coord).
90 ( 'X', '-Y'): ((0, 1), (1, 1), (2, 1)),
91 ( 'X', 'Y'): ((0, 1), (1, -1), (2, -1)),
92 ( 'X', '-Z'): ((0, 1), (2, 1), (1, -1)),
93 ( 'X', 'Z'): ((0, 1), (2, -1), (1, 1)),
94 ('-X', '-Y'): ((0, -1), (1, 1), (2, -1)),
95 ('-X', 'Y'): ((0, -1), (1, -1), (2, 1)),
96 ('-X', '-Z'): ((0, -1), (2, 1), (1, 1)),
97 ('-X', 'Z'): ((0, -1), (2, -1), (1, -1)),
98 ( 'Y', '-X'): ((1, 1), (0, 1), (2, -1)),
99 ( 'Y', 'X'): ((1, 1), (0, -1), (2, 1)),
100 ( 'Y', '-Z'): ((1, 1), (2, 1), (0, 1)),
101 ( 'Y', 'Z'): ((1, 1), (2, -1), (0, -1)),
102 ('-Y', '-X'): ((1, -1), (0, 1), (2, 1)),
103 ('-Y', 'X'): ((1, -1), (0, -1), (2, -1)),
104 ('-Y', '-Z'): ((1, -1), (2, 1), (0, -1)),
105 ('-Y', 'Z'): ((1, -1), (2, -1), (0, 1)),
106 ( 'Z', '-X'): ((2, 1), (0, 1), (1, 1)),
107 ( 'Z', 'X'): ((2, 1), (0, -1), (1, -1)),
108 ( 'Z', '-Y'): ((2, 1), (1, 1), (0, -1)),
109 ( 'Z', 'Y'): ((2, 1), (1, -1), (0, 1)), # Blender system!
110 ('-Z', '-X'): ((2, -1), (0, 1), (1, -1)),
111 ('-Z', 'X'): ((2, -1), (0, -1), (1, 1)),
112 ('-Z', '-Y'): ((2, -1), (1, 1), (0, 1)),
113 ('-Z', 'Y'): ((2, -1), (1, -1), (0, -1)),
117 FBX_FRAMERATES = (
118 (-1.0, 14), # Custom framerate.
119 (120.0, 1),
120 (100.0, 2),
121 (60.0, 3),
122 (50.0, 4),
123 (48.0, 5),
124 (30.0, 6), # BW NTSC.
125 (30.0 / 1.001, 9), # Color NTSC.
126 (25.0, 10),
127 (24.0, 11),
128 (24.0 / 1.001, 13),
129 (96.0, 15),
130 (72.0, 16),
131 (60.0 / 1.001, 17),
135 # ##### Misc utilities #####
137 # Enable performance reports (measuring time used to perform various steps of importing or exporting).
138 DO_PERFMON = False
140 if DO_PERFMON:
141 class PerfMon():
142 def __init__(self):
143 self.level = -1
144 self.ref_time = []
146 def level_up(self, message=""):
147 self.level += 1
148 self.ref_time.append(None)
149 if message:
150 print("\t" * self.level, message, sep="")
152 def level_down(self, message=""):
153 if not self.ref_time:
154 if message:
155 print(message)
156 return
157 ref_time = self.ref_time[self.level]
158 print("\t" * self.level,
159 "\tDone (%f sec)\n" % ((time.process_time() - ref_time) if ref_time is not None else 0.0),
160 sep="")
161 if message:
162 print("\t" * self.level, message, sep="")
163 del self.ref_time[self.level]
164 self.level -= 1
166 def step(self, message=""):
167 ref_time = self.ref_time[self.level]
168 curr_time = time.process_time()
169 if ref_time is not None:
170 print("\t" * self.level, "\tDone (%f sec)\n" % (curr_time - ref_time), sep="")
171 self.ref_time[self.level] = curr_time
172 print("\t" * self.level, message, sep="")
173 else:
174 class PerfMon():
175 def __init__(self):
176 pass
178 def level_up(self, message=""):
179 pass
181 def level_down(self, message=""):
182 pass
184 def step(self, message=""):
185 pass
188 # Scale/unit mess. FBX can store the 'reference' unit of a file in its UnitScaleFactor property
189 # (1.0 meaning centimeter, afaik). We use that to reflect user's default unit as set in Blender with scale_length.
190 # However, we always get values in BU (i.e. meters), so we have to reverse-apply that scale in global matrix...
191 # Note that when no default unit is available, we assume 'meters' (and hence scale by 100).
192 def units_blender_to_fbx_factor(scene):
193 return 100.0 if (scene.unit_settings.system == 'NONE') else (100.0 * scene.unit_settings.scale_length)
196 # Note: this could be in a utility (math.units e.g.)...
198 UNITS = {
199 "meter": 1.0, # Ref unit!
200 "kilometer": 0.001,
201 "millimeter": 1000.0,
202 "foot": 1.0 / 0.3048,
203 "inch": 1.0 / 0.0254,
204 "turn": 1.0, # Ref unit!
205 "degree": 360.0,
206 "radian": math.pi * 2.0,
207 "second": 1.0, # Ref unit!
208 "ktime": FBX_KTIME,
212 def units_convertor(u_from, u_to):
213 """Return a convertor between specified units."""
214 conv = UNITS[u_to] / UNITS[u_from]
215 return lambda v: v * conv
218 def units_convertor_iter(u_from, u_to):
219 """Return an iterable convertor between specified units."""
220 conv = units_convertor(u_from, u_to)
222 def convertor(it):
223 for v in it:
224 yield(conv(v))
226 return convertor
229 def matrix4_to_array(mat):
230 """Concatenate matrix's columns into a single, flat tuple"""
231 # blender matrix is row major, fbx is col major so transpose on write
232 return tuple(f for v in mat.transposed() for f in v)
235 def array_to_matrix4(arr):
236 """Convert a single 16-len tuple into a valid 4D Blender matrix"""
237 # Blender matrix is row major, fbx is col major so transpose on read
238 return Matrix(tuple(zip(*[iter(arr)]*4))).transposed()
241 def similar_values(v1, v2, e=1e-6):
242 """Return True if v1 and v2 are nearly the same."""
243 if v1 == v2:
244 return True
245 return ((abs(v1 - v2) / max(abs(v1), abs(v2))) <= e)
248 def similar_values_iter(v1, v2, e=1e-6):
249 """Return True if iterables v1 and v2 are nearly the same."""
250 if v1 == v2:
251 return True
252 for v1, v2 in zip(v1, v2):
253 if (v1 != v2) and ((abs(v1 - v2) / max(abs(v1), abs(v2))) > e):
254 return False
255 return True
257 def vcos_transformed_gen(raw_cos, m=None):
258 # Note: we could most likely get much better performances with numpy, but will leave this as TODO for now.
259 gen = zip(*(iter(raw_cos),) * 3)
260 return gen if m is None else (m @ Vector(v) for v in gen)
262 def nors_transformed_gen(raw_nors, m=None):
263 # Great, now normals are also expected 4D!
264 # XXX Back to 3D normals for now!
265 # gen = zip(*(iter(raw_nors),) * 3 + (_infinite_gen(1.0),))
266 gen = zip(*(iter(raw_nors),) * 3)
267 return gen if m is None else (m @ Vector(v) for v in gen)
270 # ##### UIDs code. #####
272 # ID class (mere int).
273 class UUID(int):
274 pass
277 # UIDs storage.
278 _keys_to_uuids = {}
279 _uuids_to_keys = {}
282 def _key_to_uuid(uuids, key):
283 # TODO: Check this is robust enough for our needs!
284 # Note: We assume we have already checked the related key wasn't yet in _keys_to_uids!
285 # As int64 is signed in FBX, we keep uids below 2**63...
286 if isinstance(key, int) and 0 <= key < 2**63:
287 # We can use value directly as id!
288 uuid = key
289 else:
290 uuid = hash(key)
291 if uuid < 0:
292 uuid = -uuid
293 if uuid >= 2**63:
294 uuid //= 2
295 # Try to make our uid shorter!
296 if uuid > int(1e9):
297 t_uuid = uuid % int(1e9)
298 if t_uuid not in uuids:
299 uuid = t_uuid
300 # Make sure our uuid *is* unique.
301 if uuid in uuids:
302 inc = 1 if uuid < 2**62 else -1
303 while uuid in uuids:
304 uuid += inc
305 if 0 > uuid >= 2**63:
306 # Note that this is more that unlikely, but does not harm anyway...
307 raise ValueError("Unable to generate an UUID for key {}".format(key))
308 return UUID(uuid)
311 def get_fbx_uuid_from_key(key):
313 Return an UUID for given key, which is assumed to be hashable.
315 uuid = _keys_to_uuids.get(key, None)
316 if uuid is None:
317 uuid = _key_to_uuid(_uuids_to_keys, key)
318 _keys_to_uuids[key] = uuid
319 _uuids_to_keys[uuid] = key
320 return uuid
323 # XXX Not sure we'll actually need this one?
324 def get_key_from_fbx_uuid(uuid):
326 Return the key which generated this uid.
328 assert(uuid.__class__ == UUID)
329 return _uuids_to_keys.get(uuid, None)
332 # Blender-specific key generators
333 def get_bid_name(bid):
334 library = getattr(bid, "library", None)
335 if library is not None:
336 return "%s_L_%s" % (bid.name, library.name)
337 else:
338 return bid.name
341 def get_blenderID_key(bid):
342 if isinstance(bid, Iterable):
343 return "|".join("B" + e.rna_type.name + "#" + get_bid_name(e) for e in bid)
344 else:
345 return "B" + bid.rna_type.name + "#" + get_bid_name(bid)
348 def get_blenderID_name(bid):
349 if isinstance(bid, Iterable):
350 return "|".join(get_bid_name(e) for e in bid)
351 else:
352 return get_bid_name(bid)
355 def get_blender_empty_key(obj):
356 """Return bone's keys (Model and NodeAttribute)."""
357 return "|".join((get_blenderID_key(obj), "Empty"))
360 def get_blender_mesh_shape_key(me):
361 """Return main shape deformer's key."""
362 return "|".join((get_blenderID_key(me), "Shape"))
365 def get_blender_mesh_shape_channel_key(me, shape):
366 """Return shape channel and geometry shape keys."""
367 return ("|".join((get_blenderID_key(me), "Shape", get_blenderID_key(shape))),
368 "|".join((get_blenderID_key(me), "Geometry", get_blenderID_key(shape))))
371 def get_blender_bone_key(armature, bone):
372 """Return bone's keys (Model and NodeAttribute)."""
373 return "|".join((get_blenderID_key((armature, bone)), "Data"))
376 def get_blender_bindpose_key(obj, mesh):
377 """Return object's bindpose key."""
378 return "|".join((get_blenderID_key(obj), get_blenderID_key(mesh), "BindPose"))
381 def get_blender_armature_skin_key(armature, mesh):
382 """Return armature's skin key."""
383 return "|".join((get_blenderID_key(armature), get_blenderID_key(mesh), "DeformerSkin"))
386 def get_blender_bone_cluster_key(armature, mesh, bone):
387 """Return bone's cluster key."""
388 return "|".join((get_blenderID_key(armature), get_blenderID_key(mesh),
389 get_blenderID_key(bone), "SubDeformerCluster"))
392 def get_blender_anim_id_base(scene, ref_id):
393 if ref_id is not None:
394 return get_blenderID_key(scene) + "|" + get_blenderID_key(ref_id)
395 else:
396 return get_blenderID_key(scene)
399 def get_blender_anim_stack_key(scene, ref_id):
400 """Return single anim stack key."""
401 return get_blender_anim_id_base(scene, ref_id) + "|AnimStack"
404 def get_blender_anim_layer_key(scene, ref_id):
405 """Return ID's anim layer key."""
406 return get_blender_anim_id_base(scene, ref_id) + "|AnimLayer"
409 def get_blender_anim_curve_node_key(scene, ref_id, obj_key, fbx_prop_name):
410 """Return (stack/layer, ID, fbxprop) curve node key."""
411 return "|".join((get_blender_anim_id_base(scene, ref_id), obj_key, fbx_prop_name, "AnimCurveNode"))
414 def get_blender_anim_curve_key(scene, ref_id, obj_key, fbx_prop_name, fbx_prop_item_name):
415 """Return (stack/layer, ID, fbxprop, item) curve key."""
416 return "|".join((get_blender_anim_id_base(scene, ref_id), obj_key, fbx_prop_name,
417 fbx_prop_item_name, "AnimCurve"))
420 def get_blender_nodetexture_key(ma, socket_names):
421 return "|".join((get_blenderID_key(ma), *socket_names))
424 # ##### Element generators. #####
426 # Note: elem may be None, in this case the element is not added to any parent.
427 def elem_empty(elem, name):
428 sub_elem = encode_bin.FBXElem(name)
429 if elem is not None:
430 elem.elems.append(sub_elem)
431 return sub_elem
434 def _elem_data_single(elem, name, value, func_name):
435 sub_elem = elem_empty(elem, name)
436 getattr(sub_elem, func_name)(value)
437 return sub_elem
440 def _elem_data_vec(elem, name, value, func_name):
441 sub_elem = elem_empty(elem, name)
442 func = getattr(sub_elem, func_name)
443 for v in value:
444 func(v)
445 return sub_elem
448 def elem_data_single_bool(elem, name, value):
449 return _elem_data_single(elem, name, value, "add_bool")
452 def elem_data_single_int16(elem, name, value):
453 return _elem_data_single(elem, name, value, "add_int16")
456 def elem_data_single_int32(elem, name, value):
457 return _elem_data_single(elem, name, value, "add_int32")
460 def elem_data_single_int64(elem, name, value):
461 return _elem_data_single(elem, name, value, "add_int64")
464 def elem_data_single_float32(elem, name, value):
465 return _elem_data_single(elem, name, value, "add_float32")
468 def elem_data_single_float64(elem, name, value):
469 return _elem_data_single(elem, name, value, "add_float64")
472 def elem_data_single_bytes(elem, name, value):
473 return _elem_data_single(elem, name, value, "add_bytes")
476 def elem_data_single_string(elem, name, value):
477 return _elem_data_single(elem, name, value, "add_string")
480 def elem_data_single_string_unicode(elem, name, value):
481 return _elem_data_single(elem, name, value, "add_string_unicode")
484 def elem_data_single_bool_array(elem, name, value):
485 return _elem_data_single(elem, name, value, "add_bool_array")
488 def elem_data_single_int32_array(elem, name, value):
489 return _elem_data_single(elem, name, value, "add_int32_array")
492 def elem_data_single_int64_array(elem, name, value):
493 return _elem_data_single(elem, name, value, "add_int64_array")
496 def elem_data_single_float32_array(elem, name, value):
497 return _elem_data_single(elem, name, value, "add_float32_array")
500 def elem_data_single_float64_array(elem, name, value):
501 return _elem_data_single(elem, name, value, "add_float64_array")
504 def elem_data_single_byte_array(elem, name, value):
505 return _elem_data_single(elem, name, value, "add_byte_array")
508 def elem_data_vec_float64(elem, name, value):
509 return _elem_data_vec(elem, name, value, "add_float64")
512 # ##### Generators for standard FBXProperties70 properties. #####
514 def elem_properties(elem):
515 return elem_empty(elem, b"Properties70")
518 # Properties definitions, format: (b"type_1", b"label(???)", "name_set_value_1", "name_set_value_2", ...)
519 # XXX Looks like there can be various variations of formats here... Will have to be checked ultimately!
520 # Also, those "custom" types like 'FieldOfView' or 'Lcl Translation' are pure nonsense,
521 # these are just Vector3D ultimately... *sigh* (again).
522 FBX_PROPERTIES_DEFINITIONS = {
523 # Generic types.
524 "p_bool": (b"bool", b"", "add_int32"), # Yes, int32 for a bool (and they do have a core bool type)!!!
525 "p_integer": (b"int", b"Integer", "add_int32"),
526 "p_ulonglong": (b"ULongLong", b"", "add_int64"),
527 "p_double": (b"double", b"Number", "add_float64"), # Non-animatable?
528 "p_number": (b"Number", b"", "add_float64"), # Animatable-only?
529 "p_enum": (b"enum", b"", "add_int32"),
530 "p_vector_3d": (b"Vector3D", b"Vector", "add_float64", "add_float64", "add_float64"), # Non-animatable?
531 "p_vector": (b"Vector", b"", "add_float64", "add_float64", "add_float64"), # Animatable-only?
532 "p_color_rgb": (b"ColorRGB", b"Color", "add_float64", "add_float64", "add_float64"), # Non-animatable?
533 "p_color": (b"Color", b"", "add_float64", "add_float64", "add_float64"), # Animatable-only?
534 "p_string": (b"KString", b"", "add_string_unicode"),
535 "p_string_url": (b"KString", b"Url", "add_string_unicode"),
536 "p_timestamp": (b"KTime", b"Time", "add_int64"),
537 "p_datetime": (b"DateTime", b"", "add_string_unicode"),
538 # Special types.
539 "p_object": (b"object", b""), # XXX Check this! No value for this prop??? Would really like to know how it works!
540 "p_compound": (b"Compound", b""),
541 # Specific types (sic).
542 # ## Objects (Models).
543 "p_lcl_translation": (b"Lcl Translation", b"", "add_float64", "add_float64", "add_float64"),
544 "p_lcl_rotation": (b"Lcl Rotation", b"", "add_float64", "add_float64", "add_float64"),
545 "p_lcl_scaling": (b"Lcl Scaling", b"", "add_float64", "add_float64", "add_float64"),
546 "p_visibility": (b"Visibility", b"", "add_float64"),
547 "p_visibility_inheritance": (b"Visibility Inheritance", b"", "add_int32"),
548 # ## Cameras!!!
549 "p_roll": (b"Roll", b"", "add_float64"),
550 "p_opticalcenterx": (b"OpticalCenterX", b"", "add_float64"),
551 "p_opticalcentery": (b"OpticalCenterY", b"", "add_float64"),
552 "p_fov": (b"FieldOfView", b"", "add_float64"),
553 "p_fov_x": (b"FieldOfViewX", b"", "add_float64"),
554 "p_fov_y": (b"FieldOfViewY", b"", "add_float64"),
558 def _elem_props_set(elem, ptype, name, value, flags):
559 p = elem_data_single_string(elem, b"P", name)
560 for t in ptype[:2]:
561 p.add_string(t)
562 p.add_string(flags)
563 if len(ptype) == 3:
564 getattr(p, ptype[2])(value)
565 elif len(ptype) > 3:
566 # We assume value is iterable, else it's a bug!
567 for callback, val in zip(ptype[2:], value):
568 getattr(p, callback)(val)
571 def _elem_props_flags(animatable, animated, custom):
572 # XXX: There are way more flags, see
573 # http://help.autodesk.com/view/FBX/2015/ENU/?guid=__cpp_ref_class_fbx_property_flags_html
574 # Unfortunately, as usual, no doc at all about their 'translation' in actual FBX file format.
575 # Curse you-know-who.
576 if animatable:
577 if animated:
578 if custom:
579 return b"A+U"
580 return b"A+"
581 if custom:
582 # Seems that customprops always need those 'flags', see T69554. Go figure...
583 return b"A+U"
584 return b"A"
585 if custom:
586 # Seems that customprops always need those 'flags', see T69554. Go figure...
587 return b"A+U"
588 return b""
591 def elem_props_set(elem, ptype, name, value=None, animatable=False, animated=False, custom=False):
592 ptype = FBX_PROPERTIES_DEFINITIONS[ptype]
593 _elem_props_set(elem, ptype, name, value, _elem_props_flags(animatable, animated, custom))
596 def elem_props_compound(elem, cmpd_name, custom=False):
597 def _setter(ptype, name, value, animatable=False, animated=False, custom=False):
598 name = cmpd_name + b"|" + name
599 elem_props_set(elem, ptype, name, value, animatable=animatable, animated=animated, custom=custom)
601 elem_props_set(elem, "p_compound", cmpd_name, custom=custom)
602 return _setter
605 def elem_props_template_init(templates, template_type):
607 Init a writing template of given type, for *one* element's properties.
609 ret = {}
610 tmpl = templates.get(template_type)
611 if tmpl is not None:
612 written = tmpl.written[0]
613 props = tmpl.properties
614 ret = {name: [val, ptype, anim, written] for name, (val, ptype, anim) in props.items()}
615 return ret
618 def elem_props_template_set(template, elem, ptype_name, name, value, animatable=False, animated=False):
620 Only add a prop if the same value is not already defined in given template.
621 Note it is important to not give iterators as value, here!
623 ptype = FBX_PROPERTIES_DEFINITIONS[ptype_name]
624 if len(ptype) > 3:
625 value = tuple(value)
626 tmpl_val, tmpl_ptype, tmpl_animatable, tmpl_written = template.get(name, (None, None, False, False))
627 # Note animatable flag from template takes precedence over given one, if applicable.
628 # However, animated properties are always written, since they cannot match their template!
629 if tmpl_ptype is not None and not animated:
630 if (tmpl_written and
631 ((len(ptype) == 3 and (tmpl_val, tmpl_ptype) == (value, ptype_name)) or
632 (len(ptype) > 3 and (tuple(tmpl_val), tmpl_ptype) == (value, ptype_name)))):
633 return # Already in template and same value.
634 _elem_props_set(elem, ptype, name, value, _elem_props_flags(tmpl_animatable, animated, False))
635 template[name][3] = True
636 else:
637 _elem_props_set(elem, ptype, name, value, _elem_props_flags(animatable, animated, False))
640 def elem_props_template_finalize(template, elem):
642 Finalize one element's template/props.
643 Issue is, some templates might be "needed" by different types (e.g. NodeAttribute is for lights, cameras, etc.),
644 but values for only *one* subtype can be written as template. So we have to be sure we write those for the other
645 subtypes in each and every elements, if they are not overridden by that element.
646 Yes, hairy, FBX that is to say. When they could easily support several subtypes per template... :(
648 for name, (value, ptype_name, animatable, written) in template.items():
649 if written:
650 continue
651 ptype = FBX_PROPERTIES_DEFINITIONS[ptype_name]
652 _elem_props_set(elem, ptype, name, value, _elem_props_flags(animatable, False, False))
655 # ##### Templates #####
656 # TODO: check all those "default" values, they should match Blender's default as much as possible, I guess?
658 FBXTemplate = namedtuple("FBXTemplate", ("type_name", "prop_type_name", "properties", "nbr_users", "written"))
661 def fbx_templates_generate(root, fbx_templates):
662 # We may have to gather different templates in the same node (e.g. NodeAttribute template gathers properties
663 # for Lights, Cameras, LibNodes, etc.).
664 ref_templates = {(tmpl.type_name, tmpl.prop_type_name): tmpl for tmpl in fbx_templates.values()}
666 templates = {}
667 for type_name, prop_type_name, properties, nbr_users, _written in fbx_templates.values():
668 tmpl = templates.setdefault(type_name, [{}, 0])
669 tmpl[0][prop_type_name] = (properties, nbr_users)
670 tmpl[1] += nbr_users
672 for type_name, (subprops, nbr_users) in templates.items():
673 template = elem_data_single_string(root, b"ObjectType", type_name)
674 elem_data_single_int32(template, b"Count", nbr_users)
676 if len(subprops) == 1:
677 prop_type_name, (properties, _nbr_sub_type_users) = next(iter(subprops.items()))
678 subprops = (prop_type_name, properties)
679 ref_templates[(type_name, prop_type_name)].written[0] = True
680 else:
681 # Ack! Even though this could/should work, looks like it is not supported. So we have to chose one. :|
682 max_users = max_props = -1
683 written_prop_type_name = None
684 for prop_type_name, (properties, nbr_sub_type_users) in subprops.items():
685 if nbr_sub_type_users > max_users or (nbr_sub_type_users == max_users and len(properties) > max_props):
686 max_users = nbr_sub_type_users
687 max_props = len(properties)
688 written_prop_type_name = prop_type_name
689 subprops = (written_prop_type_name, properties)
690 ref_templates[(type_name, written_prop_type_name)].written[0] = True
692 prop_type_name, properties = subprops
693 if prop_type_name and properties:
694 elem = elem_data_single_string(template, b"PropertyTemplate", prop_type_name)
695 props = elem_properties(elem)
696 for name, (value, ptype, animatable) in properties.items():
697 try:
698 elem_props_set(props, ptype, name, value, animatable=animatable)
699 except Exception as e:
700 print("Failed to write template prop (%r)" % e)
701 print(props, ptype, name, value, animatable)
704 # ##### FBX animation helpers. #####
707 class AnimationCurveNodeWrapper:
709 This class provides a same common interface for all (FBX-wise) AnimationCurveNode and AnimationCurve elements,
710 and easy API to handle those.
712 __slots__ = (
713 'elem_keys', '_keys', 'default_values', 'fbx_group', 'fbx_gname', 'fbx_props',
714 'force_keying', 'force_startend_keying')
716 kinds = {
717 'LCL_TRANSLATION': ("Lcl Translation", "T", ("X", "Y", "Z")),
718 'LCL_ROTATION': ("Lcl Rotation", "R", ("X", "Y", "Z")),
719 'LCL_SCALING': ("Lcl Scaling", "S", ("X", "Y", "Z")),
720 'SHAPE_KEY': ("DeformPercent", "DeformPercent", ("DeformPercent",)),
721 'CAMERA_FOCAL': ("FocalLength", "FocalLength", ("FocalLength",)),
724 def __init__(self, elem_key, kind, force_keying, force_startend_keying, default_values=...):
725 self.elem_keys = [elem_key]
726 assert(kind in self.kinds)
727 self.fbx_group = [self.kinds[kind][0]]
728 self.fbx_gname = [self.kinds[kind][1]]
729 self.fbx_props = [self.kinds[kind][2]]
730 self.force_keying = force_keying
731 self.force_startend_keying = force_startend_keying
732 self._keys = [] # (frame, values, write_flags)
733 if default_values is not ...:
734 assert(len(default_values) == len(self.fbx_props[0]))
735 self.default_values = default_values
736 else:
737 self.default_values = (0.0) * len(self.fbx_props[0])
739 def __bool__(self):
740 # We are 'True' if we do have some validated keyframes...
741 return bool(self._keys) and (True in ((True in k[2]) for k in self._keys))
743 def add_group(self, elem_key, fbx_group, fbx_gname, fbx_props):
745 Add another whole group stuff (curvenode, animated item/prop + curvnode/curve identifiers).
746 E.g. Shapes animations is written twice, houra!
748 assert(len(fbx_props) == len(self.fbx_props[0]))
749 self.elem_keys.append(elem_key)
750 self.fbx_group.append(fbx_group)
751 self.fbx_gname.append(fbx_gname)
752 self.fbx_props.append(fbx_props)
754 def add_keyframe(self, frame, values):
756 Add a new keyframe to all curves of the group.
758 assert(len(values) == len(self.fbx_props[0]))
759 self._keys.append((frame, values, [True] * len(values))) # write everything by default.
761 def simplify(self, fac, step, force_keep=False):
763 Simplifies sampled curves by only enabling samples when:
764 * their values relatively differ from the previous sample ones.
766 if not self._keys:
767 return
769 if fac == 0.0:
770 return
772 # So that, with default factor and step values (1), we get:
773 min_reldiff_fac = fac * 1.0e-3 # min relative value evolution: 0.1% of current 'order of magnitude'.
774 min_absdiff_fac = 0.1 # A tenth of reldiff...
775 keys = self._keys
777 p_currframe, p_key, p_key_write = keys[0]
778 p_keyed = list(p_key)
779 are_keyed = [False] * len(p_key)
780 for currframe, key, key_write in keys:
781 for idx, (val, p_val) in enumerate(zip(key, p_key)):
782 key_write[idx] = False
783 p_keyedval = p_keyed[idx]
784 if val == p_val:
785 # Never write keyframe when value is exactly the same as prev one!
786 continue
787 # This is contracted form of relative + absolute-near-zero difference:
788 # absdiff = abs(a - b)
789 # if absdiff < min_reldiff_fac * min_absdiff_fac:
790 # return False
791 # return (absdiff / ((abs(a) + abs(b)) / 2)) > min_reldiff_fac
792 # Note that we ignore the '/ 2' part here, since it's not much significant for us.
793 if abs(val - p_val) > (min_reldiff_fac * max(abs(val) + abs(p_val), min_absdiff_fac)):
794 # If enough difference from previous sampled value, key this value *and* the previous one!
795 key_write[idx] = True
796 p_key_write[idx] = True
797 p_keyed[idx] = val
798 are_keyed[idx] = True
799 elif abs(val - p_keyedval) > (min_reldiff_fac * max((abs(val) + abs(p_keyedval)), min_absdiff_fac)):
800 # Else, if enough difference from previous keyed value, key this value only!
801 key_write[idx] = True
802 p_keyed[idx] = val
803 are_keyed[idx] = True
804 p_currframe, p_key, p_key_write = currframe, key, key_write
806 # If we write nothing (action doing nothing) and are in 'force_keep' mode, we key everything! :P
807 # See T41766.
808 # Also, it seems some importers (e.g. UE4) do not handle correctly armatures where some bones
809 # are not animated, but are children of animated ones, so added an option to systematically force writing
810 # one key in this case.
811 # See T41719, T41605, T41254...
812 if self.force_keying or (force_keep and not self):
813 are_keyed[:] = [True] * len(are_keyed)
815 # If we did key something, ensure first and last sampled values are keyed as well.
816 if self.force_startend_keying:
817 for idx, is_keyed in enumerate(are_keyed):
818 if is_keyed:
819 keys[0][2][idx] = keys[-1][2][idx] = True
821 def get_final_data(self, scene, ref_id, force_keep=False):
823 Yield final anim data for this 'curvenode' (for all curvenodes defined).
824 force_keep is to force to keep a curve even if it only has one valid keyframe.
826 curves = [[] for k in self._keys[0][1]]
827 for currframe, key, key_write in self._keys:
828 for curve, val, wrt in zip(curves, key, key_write):
829 if wrt:
830 curve.append((currframe, val))
832 force_keep = force_keep or self.force_keying
833 for elem_key, fbx_group, fbx_gname, fbx_props in \
834 zip(self.elem_keys, self.fbx_group, self.fbx_gname, self.fbx_props):
835 group_key = get_blender_anim_curve_node_key(scene, ref_id, elem_key, fbx_group)
836 group = {}
837 for c, def_val, fbx_item in zip(curves, self.default_values, fbx_props):
838 fbx_item = FBX_ANIM_PROPSGROUP_NAME + "|" + fbx_item
839 curve_key = get_blender_anim_curve_key(scene, ref_id, elem_key, fbx_group, fbx_item)
840 # (curve key, default value, keyframes, write flag).
841 group[fbx_item] = (curve_key, def_val, c,
842 True if (len(c) > 1 or (len(c) > 0 and force_keep)) else False)
843 yield elem_key, group_key, group, fbx_group, fbx_gname
846 # ##### FBX objects generators. #####
848 # FBX Model-like data (i.e. Blender objects, depsgraph instances and bones) are wrapped in ObjectWrapper.
849 # This allows us to have a (nearly) same code FBX-wise for all those types.
850 # The wrapper tries to stay as small as possible, by mostly using callbacks (property(get...))
851 # to actual Blender data it contains.
852 # Note it caches its instances, so that you may call several times ObjectWrapper(your_object)
853 # with a minimal cost (just re-computing the key).
855 class MetaObjectWrapper(type):
856 def __call__(cls, bdata, armature=None):
857 if bdata is None:
858 return None
859 dup_mat = None
860 if isinstance(bdata, Object):
861 key = get_blenderID_key(bdata)
862 elif isinstance(bdata, DepsgraphObjectInstance):
863 if bdata.is_instance:
864 key = "|".join((get_blenderID_key((bdata.parent.original, bdata.instance_object.original)),
865 cls._get_dup_num_id(bdata)))
866 dup_mat = bdata.matrix_world.copy()
867 else:
868 key = get_blenderID_key(bdata.object.original)
869 else: # isinstance(bdata, (Bone, PoseBone)):
870 if isinstance(bdata, PoseBone):
871 bdata = armature.data.bones[bdata.name]
872 key = get_blenderID_key((armature, bdata))
874 cache = getattr(cls, "_cache", None)
875 if cache is None:
876 cache = cls._cache = {}
877 instance = cache.get(key)
878 if instance is not None:
879 # Duplis hack: since dupli instances are not persistent in Blender (we have to re-create them to get updated
880 # info like matrix...), we *always* need to reset that matrix when calling ObjectWrapper() (all
881 # other data is supposed valid during whole cache live span, so we can skip resetting it).
882 instance._dupli_matrix = dup_mat
883 return instance
885 instance = cls.__new__(cls, bdata, armature)
886 instance.__init__(bdata, armature)
887 instance.key = key
888 instance._dupli_matrix = dup_mat
889 cache[key] = instance
890 return instance
893 class ObjectWrapper(metaclass=MetaObjectWrapper):
895 This class provides a same common interface for all (FBX-wise) object-like elements:
896 * Blender Object
897 * Blender Bone and PoseBone
898 * Blender DepsgraphObjectInstance (for dulis).
899 Note since a same Blender object might be 'mapped' to several FBX models (esp. with duplis),
900 we need to use a key to identify each.
902 __slots__ = (
903 'name', 'key', 'bdata', 'parented_to_armature',
904 '_tag', '_ref', '_dupli_matrix'
907 @classmethod
908 def cache_clear(cls):
909 if hasattr(cls, "_cache"):
910 del cls._cache
912 @staticmethod
913 def _get_dup_num_id(bdata):
914 INVALID_IDS = {2147483647, 0}
915 pids = tuple(bdata.persistent_id)
916 idx_valid = 0
917 prev_i = ...
918 for idx, i in enumerate(pids[::-1]):
919 if i not in INVALID_IDS or (idx == len(pids) and i == 0 and prev_i != 0):
920 idx_valid = len(pids) - idx
921 break
922 prev_i = i
923 return ".".join(str(i) for i in pids[:idx_valid])
925 def __init__(self, bdata, armature=None):
927 bdata might be an Object (deprecated), DepsgraphObjectInstance, Bone or PoseBone.
928 If Bone or PoseBone, armature Object must be provided.
930 # Note: DepsgraphObjectInstance are purely runtime data, they become invalid as soon as we step to the next item!
931 # Hence we have to immediately copy *all* needed data...
932 if isinstance(bdata, Object): # DEPRECATED
933 self._tag = 'OB'
934 self.name = get_blenderID_name(bdata)
935 self.bdata = bdata
936 self._ref = None
937 elif isinstance(bdata, DepsgraphObjectInstance):
938 if bdata.is_instance:
939 # Note that dupli instance matrix is set by meta-class initialization.
940 self._tag = 'DP'
941 self.name = "|".join((get_blenderID_name((bdata.parent.original, bdata.instance_object.original)),
942 "Dupli", self._get_dup_num_id(bdata)))
943 self.bdata = bdata.instance_object.original
944 self._ref = bdata.parent.original
945 else:
946 self._tag = 'OB'
947 self.name = get_blenderID_name(bdata)
948 self.bdata = bdata.object.original
949 self._ref = None
950 else: # isinstance(bdata, (Bone, PoseBone)):
951 if isinstance(bdata, PoseBone):
952 bdata = armature.data.bones[bdata.name]
953 self._tag = 'BO'
954 self.name = get_blenderID_name(bdata)
955 self.bdata = bdata
956 self._ref = armature
957 self.parented_to_armature = False
959 def __eq__(self, other):
960 return isinstance(other, self.__class__) and self.key == other.key
962 def __hash__(self):
963 return hash(self.key)
965 def __repr__(self):
966 return self.key
968 # #### Common to all _tag values.
969 def get_fbx_uuid(self):
970 return get_fbx_uuid_from_key(self.key)
971 fbx_uuid = property(get_fbx_uuid)
973 # XXX Not sure how much that’s useful now... :/
974 def get_hide(self):
975 return self.bdata.hide_viewport if self._tag in {'OB', 'DP'} else self.bdata.hide
976 hide = property(get_hide)
978 def get_parent(self):
979 if self._tag == 'OB':
980 if (self.bdata.parent and self.bdata.parent.type == 'ARMATURE' and
981 self.bdata.parent_type == 'BONE' and self.bdata.parent_bone):
982 # Try to parent to a bone.
983 bo_par = self.bdata.parent.pose.bones.get(self.bdata.parent_bone, None)
984 if (bo_par):
985 return ObjectWrapper(bo_par, self.bdata.parent)
986 else: # Fallback to mere object parenting.
987 return ObjectWrapper(self.bdata.parent)
988 else:
989 # Mere object parenting.
990 return ObjectWrapper(self.bdata.parent)
991 elif self._tag == 'DP':
992 return ObjectWrapper(self._ref)
993 else: # self._tag == 'BO'
994 return ObjectWrapper(self.bdata.parent, self._ref) or ObjectWrapper(self._ref)
995 parent = property(get_parent)
997 def get_bdata_pose_bone(self):
998 if self._tag == 'BO':
999 return self._ref.pose.bones[self.bdata.name]
1000 return None
1001 bdata_pose_bone = property(get_bdata_pose_bone)
1003 def get_matrix_local(self):
1004 if self._tag == 'OB':
1005 return self.bdata.matrix_local.copy()
1006 elif self._tag == 'DP':
1007 return self._ref.matrix_world.inverted_safe() @ self._dupli_matrix
1008 else: # 'BO', current pose
1009 # PoseBone.matrix is in armature space, bring in back in real local one!
1010 par = self.bdata.parent
1011 par_mat_inv = self._ref.pose.bones[par.name].matrix.inverted_safe() if par else Matrix()
1012 return par_mat_inv @ self._ref.pose.bones[self.bdata.name].matrix
1013 matrix_local = property(get_matrix_local)
1015 def get_matrix_global(self):
1016 if self._tag == 'OB':
1017 return self.bdata.matrix_world.copy()
1018 elif self._tag == 'DP':
1019 return self._dupli_matrix
1020 else: # 'BO', current pose
1021 return self._ref.matrix_world @ self._ref.pose.bones[self.bdata.name].matrix
1022 matrix_global = property(get_matrix_global)
1024 def get_matrix_rest_local(self):
1025 if self._tag == 'BO':
1026 # Bone.matrix_local is in armature space, bring in back in real local one!
1027 par = self.bdata.parent
1028 par_mat_inv = par.matrix_local.inverted_safe() if par else Matrix()
1029 return par_mat_inv @ self.bdata.matrix_local
1030 else:
1031 return self.matrix_local.copy()
1032 matrix_rest_local = property(get_matrix_rest_local)
1034 def get_matrix_rest_global(self):
1035 if self._tag == 'BO':
1036 return self._ref.matrix_world @ self.bdata.matrix_local
1037 else:
1038 return self.matrix_global.copy()
1039 matrix_rest_global = property(get_matrix_rest_global)
1041 # #### Transform and helpers
1042 def has_valid_parent(self, objects):
1043 par = self.parent
1044 if par in objects:
1045 if self._tag == 'OB':
1046 par_type = self.bdata.parent_type
1047 if par_type in {'OBJECT', 'BONE'}:
1048 return True
1049 else:
1050 print("Sorry, “{}” parenting type is not supported".format(par_type))
1051 return False
1052 return True
1053 return False
1055 def use_bake_space_transform(self, scene_data):
1056 # NOTE: Only applies to object types supporting this!!! Currently, only meshes and the like...
1057 # TODO: Check whether this can work for bones too...
1058 return (scene_data.settings.bake_space_transform and self._tag in {'OB', 'DP'} and
1059 self.bdata.type in BLENDER_OBJECT_TYPES_MESHLIKE | {'EMPTY'})
1061 def fbx_object_matrix(self, scene_data, rest=False, local_space=False, global_space=False):
1063 Generate object transform matrix (*always* in matching *FBX* space!).
1064 If local_space is True, returned matrix is *always* in local space.
1065 Else if global_space is True, returned matrix is always in world space.
1066 If both local_space and global_space are False, returned matrix is in parent space if parent is valid,
1067 else in world space.
1068 Note local_space has precedence over global_space.
1069 If rest is True and object is a Bone, returns matching rest pose transform instead of current pose one.
1070 Applies specific rotation to bones, lamps and cameras (conversion Blender -> FBX).
1072 # Objects which are not bones and do not have any parent are *always* in global space
1073 # (unless local_space is True!).
1074 is_global = (not local_space and
1075 (global_space or not (self._tag in {'DP', 'BO'} or self.has_valid_parent(scene_data.objects))))
1077 # Objects (meshes!) parented to armature are not parented to anything in FBX, hence we need them
1078 # in global space, which is their 'virtual' local space...
1079 is_global = is_global or self.parented_to_armature
1081 # Since we have to apply corrections to some types of object, we always need local Blender space here...
1082 matrix = self.matrix_rest_local if rest else self.matrix_local
1083 parent = self.parent
1085 # Bones, lamps and cameras need to be rotated (in local space!).
1086 if self._tag == 'BO':
1087 # If we have a bone parent we need to undo the parent correction.
1088 if not is_global and scene_data.settings.bone_correction_matrix_inv and parent and parent.is_bone:
1089 matrix = scene_data.settings.bone_correction_matrix_inv @ matrix
1090 # Apply the bone correction.
1091 if scene_data.settings.bone_correction_matrix:
1092 matrix = matrix @ scene_data.settings.bone_correction_matrix
1093 elif self.bdata.type == 'LIGHT':
1094 matrix = matrix @ MAT_CONVERT_LIGHT
1095 elif self.bdata.type == 'CAMERA':
1096 matrix = matrix @ MAT_CONVERT_CAMERA
1098 if self._tag in {'DP', 'OB'} and parent:
1099 if parent._tag == 'BO':
1100 # In bone parent case, we get transformation in **bone tip** space (sigh).
1101 # Have to bring it back into bone root, which is FBX expected value.
1102 matrix = Matrix.Translation((0, (parent.bdata.tail - parent.bdata.head).length, 0)) @ matrix
1104 # Our matrix is in local space, time to bring it in its final desired space.
1105 if parent:
1106 if is_global:
1107 # Move matrix to global Blender space.
1108 matrix = (parent.matrix_rest_global if rest else parent.matrix_global) @ matrix
1109 elif parent.use_bake_space_transform(scene_data):
1110 # Blender's and FBX's local space of parent may differ if we use bake_space_transform...
1111 # Apply parent's *Blender* local space...
1112 matrix = (parent.matrix_rest_local if rest else parent.matrix_local) @ matrix
1113 # ...and move it back into parent's *FBX* local space.
1114 par_mat = parent.fbx_object_matrix(scene_data, rest=rest, local_space=True)
1115 matrix = par_mat.inverted_safe() @ matrix
1117 if self.use_bake_space_transform(scene_data):
1118 # If we bake the transforms we need to post-multiply inverse global transform.
1119 # This means that the global transform will not apply to children of this transform.
1120 matrix = matrix @ scene_data.settings.global_matrix_inv
1121 if is_global:
1122 # In any case, pre-multiply the global matrix to get it in FBX global space!
1123 matrix = scene_data.settings.global_matrix @ matrix
1125 return matrix
1127 def fbx_object_tx(self, scene_data, rest=False, rot_euler_compat=None):
1129 Generate object transform data (always in local space when possible).
1131 matrix = self.fbx_object_matrix(scene_data, rest=rest)
1132 loc, rot, scale = matrix.decompose()
1133 matrix_rot = rot.to_matrix()
1134 # quat -> euler, we always use 'XYZ' order, use ref rotation if given.
1135 if rot_euler_compat is not None:
1136 rot = rot.to_euler('XYZ', rot_euler_compat)
1137 else:
1138 rot = rot.to_euler('XYZ')
1139 return loc, rot, scale, matrix, matrix_rot
1141 # #### _tag dependent...
1142 def get_is_object(self):
1143 return self._tag == 'OB'
1144 is_object = property(get_is_object)
1146 def get_is_dupli(self):
1147 return self._tag == 'DP'
1148 is_dupli = property(get_is_dupli)
1150 def get_is_bone(self):
1151 return self._tag == 'BO'
1152 is_bone = property(get_is_bone)
1154 def get_type(self):
1155 if self._tag in {'OB', 'DP'}:
1156 return self.bdata.type
1157 return ...
1158 type = property(get_type)
1160 def get_armature(self):
1161 if self._tag == 'BO':
1162 return ObjectWrapper(self._ref)
1163 return None
1164 armature = property(get_armature)
1166 def get_bones(self):
1167 if self._tag == 'OB' and self.bdata.type == 'ARMATURE':
1168 return (ObjectWrapper(bo, self.bdata) for bo in self.bdata.data.bones)
1169 return ()
1170 bones = property(get_bones)
1172 def get_material_slots(self):
1173 if self._tag in {'OB', 'DP'}:
1174 return self.bdata.material_slots
1175 return ()
1176 material_slots = property(get_material_slots)
1178 def is_deformed_by_armature(self, arm_obj):
1179 if not (self.is_object and self.type == 'MESH'):
1180 return False
1181 if self.parent == arm_obj and self.bdata.parent_type == 'ARMATURE':
1182 return True
1183 for mod in self.bdata.modifiers:
1184 if mod.type == 'ARMATURE' and mod.object == arm_obj.bdata:
1185 return True
1187 # #### Duplis...
1188 def dupli_list_gen(self, depsgraph):
1189 if self._tag == 'OB' and self.bdata.is_instancer:
1190 return (ObjectWrapper(dup) for dup in depsgraph.object_instances
1191 if dup.parent and ObjectWrapper(dup.parent.original) == self)
1192 return ()
1195 def fbx_name_class(name, cls):
1196 return FBX_NAME_CLASS_SEP.join((name, cls))
1199 # ##### Top-level FBX data container. #####
1201 # Helper sub-container gathering all exporter settings related to media (texture files).
1202 FBXExportSettingsMedia = namedtuple("FBXExportSettingsMedia", (
1203 "path_mode", "base_src", "base_dst", "subdir",
1204 "embed_textures", "copy_set", "embedded_set",
1207 # Helper container gathering all exporter settings.
1208 FBXExportSettings = namedtuple("FBXExportSettings", (
1209 "report", "to_axes", "global_matrix", "global_scale", "apply_unit_scale", "unit_scale",
1210 "bake_space_transform", "global_matrix_inv", "global_matrix_inv_transposed",
1211 "context_objects", "object_types", "use_mesh_modifiers", "use_mesh_modifiers_render",
1212 "mesh_smooth_type", "use_subsurf", "use_mesh_edges", "use_tspace",
1213 "armature_nodetype", "use_armature_deform_only", "add_leaf_bones",
1214 "bone_correction_matrix", "bone_correction_matrix_inv",
1215 "bake_anim", "bake_anim_use_all_bones", "bake_anim_use_nla_strips", "bake_anim_use_all_actions",
1216 "bake_anim_step", "bake_anim_simplify_factor", "bake_anim_force_startend_keying",
1217 "use_metadata", "media_settings", "use_custom_props",
1220 # Helper container gathering some data we need multiple times:
1221 # * templates.
1222 # * settings, scene.
1223 # * objects.
1224 # * object data.
1225 # * skinning data (binding armature/mesh).
1226 # * animations.
1227 FBXExportData = namedtuple("FBXExportData", (
1228 "templates", "templates_users", "connections",
1229 "settings", "scene", "depsgraph", "objects", "animations", "animated", "frame_start", "frame_end",
1230 "data_empties", "data_lights", "data_cameras", "data_meshes", "mesh_material_indices",
1231 "data_bones", "data_leaf_bones", "data_deformers_skin", "data_deformers_shape",
1232 "data_world", "data_materials", "data_textures", "data_videos",
1235 # Helper container gathering all importer settings.
1236 FBXImportSettings = namedtuple("FBXImportSettings", (
1237 "report", "to_axes", "global_matrix", "global_scale",
1238 "bake_space_transform", "global_matrix_inv", "global_matrix_inv_transposed",
1239 "use_custom_normals", "use_image_search",
1240 "use_alpha_decals", "decal_offset",
1241 "use_anim", "anim_offset",
1242 "use_subsurf",
1243 "use_custom_props", "use_custom_props_enum_as_string",
1244 "nodal_material_wrap_map", "image_cache",
1245 "ignore_leaf_bones", "force_connect_children", "automatic_bone_orientation", "bone_correction_matrix",
1246 "use_prepost_rot",