PLY: cleanup style
[blender-addons.git] / io_scene_fbx / fbx_utils.py
blob19f3280029ce1d1bcac5af25d03293abd79e79d3
1 # ##### BEGIN GPL LICENSE BLOCK #####
3 # This program is free software; you can redistribute it and/or
4 # modify it under the terms of the GNU General Public License
5 # as published by the Free Software Foundation; either version 2
6 # of the License, or (at your option) any later version.
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
13 # You should have received a copy of the GNU General Public License
14 # along with this program; if not, write to the Free Software Foundation,
15 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 # ##### END GPL LICENSE BLOCK #####
19 # <pep8 compliant>
21 # Script copyright (C) Campbell Barton, Bastien Montagne
24 import math
25 import time
27 from collections import namedtuple
28 from collections.abc import Iterable
29 from itertools import zip_longest, chain
31 import bpy
32 import bpy_extras
33 from bpy.types import Object, Bone, PoseBone, DepsgraphObjectInstance
34 from mathutils import Vector, Matrix
36 from . import encode_bin, data_types
39 # "Constants"
40 FBX_VERSION = 7400
41 FBX_HEADER_VERSION = 1003
42 FBX_SCENEINFO_VERSION = 100
43 FBX_TEMPLATES_VERSION = 100
45 FBX_MODELS_VERSION = 232
47 FBX_GEOMETRY_VERSION = 124
48 # Revert back normals to 101 (simple 3D values) for now, 102 (4D + weights) seems not well supported by most apps
49 # currently, apart from some AD products.
50 FBX_GEOMETRY_NORMAL_VERSION = 101
51 FBX_GEOMETRY_BINORMAL_VERSION = 101
52 FBX_GEOMETRY_TANGENT_VERSION = 101
53 FBX_GEOMETRY_SMOOTHING_VERSION = 102
54 FBX_GEOMETRY_VCOLOR_VERSION = 101
55 FBX_GEOMETRY_UV_VERSION = 101
56 FBX_GEOMETRY_MATERIAL_VERSION = 101
57 FBX_GEOMETRY_LAYER_VERSION = 100
58 FBX_GEOMETRY_SHAPE_VERSION = 100
59 FBX_DEFORMER_SHAPE_VERSION = 100
60 FBX_DEFORMER_SHAPECHANNEL_VERSION = 100
61 FBX_POSE_BIND_VERSION = 100
62 FBX_DEFORMER_SKIN_VERSION = 101
63 FBX_DEFORMER_CLUSTER_VERSION = 100
64 FBX_MATERIAL_VERSION = 102
65 FBX_TEXTURE_VERSION = 202
66 FBX_ANIM_KEY_VERSION = 4008
68 FBX_NAME_CLASS_SEP = b"\x00\x01"
69 FBX_ANIM_PROPSGROUP_NAME = "d"
71 FBX_KTIME = 46186158000 # This is the number of "ktimes" in one second (yep, precision over the nanosecond...)
74 MAT_CONVERT_LIGHT = Matrix.Rotation(math.pi / 2.0, 4, 'X') # Blender is -Z, FBX is -Y.
75 MAT_CONVERT_CAMERA = Matrix.Rotation(math.pi / 2.0, 4, 'Y') # Blender is -Z, FBX is +X.
76 # XXX I can't get this working :(
77 # MAT_CONVERT_BONE = Matrix.Rotation(math.pi / 2.0, 4, 'Z') # Blender is +Y, FBX is -X.
78 MAT_CONVERT_BONE = Matrix()
81 BLENDER_OTHER_OBJECT_TYPES = {'CURVE', 'SURFACE', 'FONT', 'META'}
82 BLENDER_OBJECT_TYPES_MESHLIKE = {'MESH'} | BLENDER_OTHER_OBJECT_TYPES
85 # Lamps.
86 FBX_LIGHT_TYPES = {
87 'POINT': 0, # Point.
88 'SUN': 1, # Directional.
89 'SPOT': 2, # Spot.
90 'HEMI': 1, # Directional.
91 'AREA': 3, # Area.
93 FBX_LIGHT_DECAY_TYPES = {
94 'CONSTANT': 0, # None.
95 'INVERSE_LINEAR': 1, # Linear.
96 'INVERSE_SQUARE': 2, # Quadratic.
97 'INVERSE_COEFFICIENTS': 2, # Quadratic...
98 'CUSTOM_CURVE': 2, # Quadratic.
99 'LINEAR_QUADRATIC_WEIGHTED': 2, # Quadratic.
103 RIGHT_HAND_AXES = {
104 # Up, Forward -> FBX values (tuples of (axis, sign), Up, Front, Coord).
105 ( 'X', '-Y'): ((0, 1), (1, 1), (2, 1)),
106 ( 'X', 'Y'): ((0, 1), (1, -1), (2, -1)),
107 ( 'X', '-Z'): ((0, 1), (2, 1), (1, -1)),
108 ( 'X', 'Z'): ((0, 1), (2, -1), (1, 1)),
109 ('-X', '-Y'): ((0, -1), (1, 1), (2, -1)),
110 ('-X', 'Y'): ((0, -1), (1, -1), (2, 1)),
111 ('-X', '-Z'): ((0, -1), (2, 1), (1, 1)),
112 ('-X', 'Z'): ((0, -1), (2, -1), (1, -1)),
113 ( 'Y', '-X'): ((1, 1), (0, 1), (2, -1)),
114 ( 'Y', 'X'): ((1, 1), (0, -1), (2, 1)),
115 ( 'Y', '-Z'): ((1, 1), (2, 1), (0, 1)),
116 ( 'Y', 'Z'): ((1, 1), (2, -1), (0, -1)),
117 ('-Y', '-X'): ((1, -1), (0, 1), (2, 1)),
118 ('-Y', 'X'): ((1, -1), (0, -1), (2, -1)),
119 ('-Y', '-Z'): ((1, -1), (2, 1), (0, -1)),
120 ('-Y', 'Z'): ((1, -1), (2, -1), (0, 1)),
121 ( 'Z', '-X'): ((2, 1), (0, 1), (1, 1)),
122 ( 'Z', 'X'): ((2, 1), (0, -1), (1, -1)),
123 ( 'Z', '-Y'): ((2, 1), (1, 1), (0, -1)),
124 ( 'Z', 'Y'): ((2, 1), (1, -1), (0, 1)), # Blender system!
125 ('-Z', '-X'): ((2, -1), (0, 1), (1, -1)),
126 ('-Z', 'X'): ((2, -1), (0, -1), (1, 1)),
127 ('-Z', '-Y'): ((2, -1), (1, 1), (0, 1)),
128 ('-Z', 'Y'): ((2, -1), (1, -1), (0, -1)),
132 FBX_FRAMERATES = (
133 (-1.0, 14), # Custom framerate.
134 (120.0, 1),
135 (100.0, 2),
136 (60.0, 3),
137 (50.0, 4),
138 (48.0, 5),
139 (30.0, 6), # BW NTSC.
140 (30.0 / 1.001, 9), # Color NTSC.
141 (25.0, 10),
142 (24.0, 11),
143 (24.0 / 1.001, 13),
144 (96.0, 15),
145 (72.0, 16),
146 (60.0 / 1.001, 17),
150 # ##### Misc utilities #####
152 DO_PERFMON = True
154 if DO_PERFMON:
155 class PerfMon():
156 def __init__(self):
157 self.level = -1
158 self.ref_time = []
160 def level_up(self, message=""):
161 self.level += 1
162 self.ref_time.append(None)
163 if message:
164 print("\t" * self.level, message, sep="")
166 def level_down(self, message=""):
167 if not self.ref_time:
168 if message:
169 print(message)
170 return
171 ref_time = self.ref_time[self.level]
172 print("\t" * self.level,
173 "\tDone (%f sec)\n" % ((time.process_time() - ref_time) if ref_time is not None else 0.0),
174 sep="")
175 if message:
176 print("\t" * self.level, message, sep="")
177 del self.ref_time[self.level]
178 self.level -= 1
180 def step(self, message=""):
181 ref_time = self.ref_time[self.level]
182 curr_time = time.process_time()
183 if ref_time is not None:
184 print("\t" * self.level, "\tDone (%f sec)\n" % (curr_time - ref_time), sep="")
185 self.ref_time[self.level] = curr_time
186 print("\t" * self.level, message, sep="")
187 else:
188 class PerfMon():
189 def __init__(self):
190 pass
192 def level_up(self, message=""):
193 pass
195 def level_down(self, message=""):
196 pass
198 def step(self, message=""):
199 pass
202 # Scale/unit mess. FBX can store the 'reference' unit of a file in its UnitScaleFactor property
203 # (1.0 meaning centimeter, afaik). We use that to reflect user's default unit as set in Blender with scale_length.
204 # However, we always get values in BU (i.e. meters), so we have to reverse-apply that scale in global matrix...
205 # Note that when no default unit is available, we assume 'meters' (and hence scale by 100).
206 def units_blender_to_fbx_factor(scene):
207 return 100.0 if (scene.unit_settings.system == 'NONE') else (100.0 * scene.unit_settings.scale_length)
210 # Note: this could be in a utility (math.units e.g.)...
212 UNITS = {
213 "meter": 1.0, # Ref unit!
214 "kilometer": 0.001,
215 "millimeter": 1000.0,
216 "foot": 1.0 / 0.3048,
217 "inch": 1.0 / 0.0254,
218 "turn": 1.0, # Ref unit!
219 "degree": 360.0,
220 "radian": math.pi * 2.0,
221 "second": 1.0, # Ref unit!
222 "ktime": FBX_KTIME,
226 def units_convertor(u_from, u_to):
227 """Return a convertor between specified units."""
228 conv = UNITS[u_to] / UNITS[u_from]
229 return lambda v: v * conv
232 def units_convertor_iter(u_from, u_to):
233 """Return an iterable convertor between specified units."""
234 conv = units_convertor(u_from, u_to)
236 def convertor(it):
237 for v in it:
238 yield(conv(v))
240 return convertor
243 def matrix4_to_array(mat):
244 """Concatenate matrix's columns into a single, flat tuple"""
245 # blender matrix is row major, fbx is col major so transpose on write
246 return tuple(f for v in mat.transposed() for f in v)
249 def array_to_matrix4(arr):
250 """Convert a single 16-len tuple into a valid 4D Blender matrix"""
251 # Blender matrix is row major, fbx is col major so transpose on read
252 return Matrix(tuple(zip(*[iter(arr)]*4))).transposed()
255 def similar_values(v1, v2, e=1e-6):
256 """Return True if v1 and v2 are nearly the same."""
257 if v1 == v2:
258 return True
259 return ((abs(v1 - v2) / max(abs(v1), abs(v2))) <= e)
262 def similar_values_iter(v1, v2, e=1e-6):
263 """Return True if iterables v1 and v2 are nearly the same."""
264 if v1 == v2:
265 return True
266 for v1, v2 in zip(v1, v2):
267 if (v1 != v2) and ((abs(v1 - v2) / max(abs(v1), abs(v2))) > e):
268 return False
269 return True
271 def vcos_transformed_gen(raw_cos, m=None):
272 # Note: we could most likely get much better performances with numpy, but will leave this as TODO for now.
273 gen = zip(*(iter(raw_cos),) * 3)
274 return gen if m is None else (m @ Vector(v) for v in gen)
276 def nors_transformed_gen(raw_nors, m=None):
277 # Great, now normals are also expected 4D!
278 # XXX Back to 3D normals for now!
279 # gen = zip(*(iter(raw_nors),) * 3 + (_infinite_gen(1.0),))
280 gen = zip(*(iter(raw_nors),) * 3)
281 return gen if m is None else (m @ Vector(v) for v in gen)
284 # ##### UIDs code. #####
286 # ID class (mere int).
287 class UUID(int):
288 pass
291 # UIDs storage.
292 _keys_to_uuids = {}
293 _uuids_to_keys = {}
296 def _key_to_uuid(uuids, key):
297 # TODO: Check this is robust enough for our needs!
298 # Note: We assume we have already checked the related key wasn't yet in _keys_to_uids!
299 # As int64 is signed in FBX, we keep uids below 2**63...
300 if isinstance(key, int) and 0 <= key < 2**63:
301 # We can use value directly as id!
302 uuid = key
303 else:
304 uuid = hash(key)
305 if uuid < 0:
306 uuid = -uuid
307 if uuid >= 2**63:
308 uuid //= 2
309 # Try to make our uid shorter!
310 if uuid > int(1e9):
311 t_uuid = uuid % int(1e9)
312 if t_uuid not in uuids:
313 uuid = t_uuid
314 # Make sure our uuid *is* unique.
315 if uuid in uuids:
316 inc = 1 if uuid < 2**62 else -1
317 while uuid in uuids:
318 uuid += inc
319 if 0 > uuid >= 2**63:
320 # Note that this is more that unlikely, but does not harm anyway...
321 raise ValueError("Unable to generate an UUID for key {}".format(key))
322 return UUID(uuid)
325 def get_fbx_uuid_from_key(key):
327 Return an UUID for given key, which is assumed to be hashable.
329 uuid = _keys_to_uuids.get(key, None)
330 if uuid is None:
331 uuid = _key_to_uuid(_uuids_to_keys, key)
332 _keys_to_uuids[key] = uuid
333 _uuids_to_keys[uuid] = key
334 return uuid
337 # XXX Not sure we'll actually need this one?
338 def get_key_from_fbx_uuid(uuid):
340 Return the key which generated this uid.
342 assert(uuid.__class__ == UUID)
343 return _uuids_to_keys.get(uuid, None)
346 # Blender-specific key generators
347 def get_bid_name(bid):
348 library = getattr(bid, "library", None)
349 if library is not None:
350 return "%s_L_%s" % (bid.name, library.name)
351 else:
352 return bid.name
355 def get_blenderID_key(bid):
356 if isinstance(bid, Iterable):
357 return "|".join("B" + e.rna_type.name + "#" + get_bid_name(e) for e in bid)
358 else:
359 return "B" + bid.rna_type.name + "#" + get_bid_name(bid)
362 def get_blenderID_name(bid):
363 if isinstance(bid, Iterable):
364 return "|".join(get_bid_name(e) for e in bid)
365 else:
366 return get_bid_name(bid)
369 def get_blender_empty_key(obj):
370 """Return bone's keys (Model and NodeAttribute)."""
371 return "|".join((get_blenderID_key(obj), "Empty"))
374 def get_blender_mesh_shape_key(me):
375 """Return main shape deformer's key."""
376 return "|".join((get_blenderID_key(me), "Shape"))
379 def get_blender_mesh_shape_channel_key(me, shape):
380 """Return shape channel and geometry shape keys."""
381 return ("|".join((get_blenderID_key(me), "Shape", get_blenderID_key(shape))),
382 "|".join((get_blenderID_key(me), "Geometry", get_blenderID_key(shape))))
385 def get_blender_bone_key(armature, bone):
386 """Return bone's keys (Model and NodeAttribute)."""
387 return "|".join((get_blenderID_key((armature, bone)), "Data"))
390 def get_blender_bindpose_key(obj, mesh):
391 """Return object's bindpose key."""
392 return "|".join((get_blenderID_key(obj), get_blenderID_key(mesh), "BindPose"))
395 def get_blender_armature_skin_key(armature, mesh):
396 """Return armature's skin key."""
397 return "|".join((get_blenderID_key(armature), get_blenderID_key(mesh), "DeformerSkin"))
400 def get_blender_bone_cluster_key(armature, mesh, bone):
401 """Return bone's cluster key."""
402 return "|".join((get_blenderID_key(armature), get_blenderID_key(mesh),
403 get_blenderID_key(bone), "SubDeformerCluster"))
406 def get_blender_anim_id_base(scene, ref_id):
407 if ref_id is not None:
408 return get_blenderID_key(scene) + "|" + get_blenderID_key(ref_id)
409 else:
410 return get_blenderID_key(scene)
413 def get_blender_anim_stack_key(scene, ref_id):
414 """Return single anim stack key."""
415 return get_blender_anim_id_base(scene, ref_id) + "|AnimStack"
418 def get_blender_anim_layer_key(scene, ref_id):
419 """Return ID's anim layer key."""
420 return get_blender_anim_id_base(scene, ref_id) + "|AnimLayer"
423 def get_blender_anim_curve_node_key(scene, ref_id, obj_key, fbx_prop_name):
424 """Return (stack/layer, ID, fbxprop) curve node key."""
425 return "|".join((get_blender_anim_id_base(scene, ref_id), obj_key, fbx_prop_name, "AnimCurveNode"))
428 def get_blender_anim_curve_key(scene, ref_id, obj_key, fbx_prop_name, fbx_prop_item_name):
429 """Return (stack/layer, ID, fbxprop, item) curve key."""
430 return "|".join((get_blender_anim_id_base(scene, ref_id), obj_key, fbx_prop_name,
431 fbx_prop_item_name, "AnimCurve"))
434 def get_blender_nodetexture_key(ma, socket_names):
435 return "|".join((get_blenderID_key(ma), *socket_names))
438 # ##### Element generators. #####
440 # Note: elem may be None, in this case the element is not added to any parent.
441 def elem_empty(elem, name):
442 sub_elem = encode_bin.FBXElem(name)
443 if elem is not None:
444 elem.elems.append(sub_elem)
445 return sub_elem
448 def _elem_data_single(elem, name, value, func_name):
449 sub_elem = elem_empty(elem, name)
450 getattr(sub_elem, func_name)(value)
451 return sub_elem
454 def _elem_data_vec(elem, name, value, func_name):
455 sub_elem = elem_empty(elem, name)
456 func = getattr(sub_elem, func_name)
457 for v in value:
458 func(v)
459 return sub_elem
462 def elem_data_single_bool(elem, name, value):
463 return _elem_data_single(elem, name, value, "add_bool")
466 def elem_data_single_int16(elem, name, value):
467 return _elem_data_single(elem, name, value, "add_int16")
470 def elem_data_single_int32(elem, name, value):
471 return _elem_data_single(elem, name, value, "add_int32")
474 def elem_data_single_int64(elem, name, value):
475 return _elem_data_single(elem, name, value, "add_int64")
478 def elem_data_single_float32(elem, name, value):
479 return _elem_data_single(elem, name, value, "add_float32")
482 def elem_data_single_float64(elem, name, value):
483 return _elem_data_single(elem, name, value, "add_float64")
486 def elem_data_single_bytes(elem, name, value):
487 return _elem_data_single(elem, name, value, "add_bytes")
490 def elem_data_single_string(elem, name, value):
491 return _elem_data_single(elem, name, value, "add_string")
494 def elem_data_single_string_unicode(elem, name, value):
495 return _elem_data_single(elem, name, value, "add_string_unicode")
498 def elem_data_single_bool_array(elem, name, value):
499 return _elem_data_single(elem, name, value, "add_bool_array")
502 def elem_data_single_int32_array(elem, name, value):
503 return _elem_data_single(elem, name, value, "add_int32_array")
506 def elem_data_single_int64_array(elem, name, value):
507 return _elem_data_single(elem, name, value, "add_int64_array")
510 def elem_data_single_float32_array(elem, name, value):
511 return _elem_data_single(elem, name, value, "add_float32_array")
514 def elem_data_single_float64_array(elem, name, value):
515 return _elem_data_single(elem, name, value, "add_float64_array")
518 def elem_data_single_byte_array(elem, name, value):
519 return _elem_data_single(elem, name, value, "add_byte_array")
522 def elem_data_vec_float64(elem, name, value):
523 return _elem_data_vec(elem, name, value, "add_float64")
526 # ##### Generators for standard FBXProperties70 properties. #####
528 def elem_properties(elem):
529 return elem_empty(elem, b"Properties70")
532 # Properties definitions, format: (b"type_1", b"label(???)", "name_set_value_1", "name_set_value_2", ...)
533 # XXX Looks like there can be various variations of formats here... Will have to be checked ultimately!
534 # Also, those "custom" types like 'FieldOfView' or 'Lcl Translation' are pure nonsense,
535 # these are just Vector3D ultimately... *sigh* (again).
536 FBX_PROPERTIES_DEFINITIONS = {
537 # Generic types.
538 "p_bool": (b"bool", b"", "add_int32"), # Yes, int32 for a bool (and they do have a core bool type)!!!
539 "p_integer": (b"int", b"Integer", "add_int32"),
540 "p_ulonglong": (b"ULongLong", b"", "add_int64"),
541 "p_double": (b"double", b"Number", "add_float64"), # Non-animatable?
542 "p_number": (b"Number", b"", "add_float64"), # Animatable-only?
543 "p_enum": (b"enum", b"", "add_int32"),
544 "p_vector_3d": (b"Vector3D", b"Vector", "add_float64", "add_float64", "add_float64"), # Non-animatable?
545 "p_vector": (b"Vector", b"", "add_float64", "add_float64", "add_float64"), # Animatable-only?
546 "p_color_rgb": (b"ColorRGB", b"Color", "add_float64", "add_float64", "add_float64"), # Non-animatable?
547 "p_color": (b"Color", b"", "add_float64", "add_float64", "add_float64"), # Animatable-only?
548 "p_string": (b"KString", b"", "add_string_unicode"),
549 "p_string_url": (b"KString", b"Url", "add_string_unicode"),
550 "p_timestamp": (b"KTime", b"Time", "add_int64"),
551 "p_datetime": (b"DateTime", b"", "add_string_unicode"),
552 # Special types.
553 "p_object": (b"object", b""), # XXX Check this! No value for this prop??? Would really like to know how it works!
554 "p_compound": (b"Compound", b""),
555 # Specific types (sic).
556 # ## Objects (Models).
557 "p_lcl_translation": (b"Lcl Translation", b"", "add_float64", "add_float64", "add_float64"),
558 "p_lcl_rotation": (b"Lcl Rotation", b"", "add_float64", "add_float64", "add_float64"),
559 "p_lcl_scaling": (b"Lcl Scaling", b"", "add_float64", "add_float64", "add_float64"),
560 "p_visibility": (b"Visibility", b"", "add_float64"),
561 "p_visibility_inheritance": (b"Visibility Inheritance", b"", "add_int32"),
562 # ## Cameras!!!
563 "p_roll": (b"Roll", b"", "add_float64"),
564 "p_opticalcenterx": (b"OpticalCenterX", b"", "add_float64"),
565 "p_opticalcentery": (b"OpticalCenterY", b"", "add_float64"),
566 "p_fov": (b"FieldOfView", b"", "add_float64"),
567 "p_fov_x": (b"FieldOfViewX", b"", "add_float64"),
568 "p_fov_y": (b"FieldOfViewY", b"", "add_float64"),
572 def _elem_props_set(elem, ptype, name, value, flags):
573 p = elem_data_single_string(elem, b"P", name)
574 for t in ptype[:2]:
575 p.add_string(t)
576 p.add_string(flags)
577 if len(ptype) == 3:
578 getattr(p, ptype[2])(value)
579 elif len(ptype) > 3:
580 # We assume value is iterable, else it's a bug!
581 for callback, val in zip(ptype[2:], value):
582 getattr(p, callback)(val)
585 def _elem_props_flags(animatable, animated, custom):
586 # XXX: There are way more flags, see
587 # http://help.autodesk.com/view/FBX/2015/ENU/?guid=__cpp_ref_class_fbx_property_flags_html
588 # Unfortunately, as usual, no doc at all about their 'translation' in actual FBX file format.
589 # Curse you-know-who.
590 if animatable:
591 if animated:
592 if custom:
593 return b"A+U"
594 return b"A+"
595 if custom:
596 return b"AU"
597 return b"A"
598 if custom:
599 return b"U"
600 return b""
603 def elem_props_set(elem, ptype, name, value=None, animatable=False, animated=False, custom=False):
604 ptype = FBX_PROPERTIES_DEFINITIONS[ptype]
605 _elem_props_set(elem, ptype, name, value, _elem_props_flags(animatable, animated, custom))
608 def elem_props_compound(elem, cmpd_name, custom=False):
609 def _setter(ptype, name, value, animatable=False, animated=False, custom=False):
610 name = cmpd_name + b"|" + name
611 elem_props_set(elem, ptype, name, value, animatable=animatable, animated=animated, custom=custom)
613 elem_props_set(elem, "p_compound", cmpd_name, custom=custom)
614 return _setter
617 def elem_props_template_init(templates, template_type):
619 Init a writing template of given type, for *one* element's properties.
621 ret = {}
622 tmpl = templates.get(template_type)
623 if tmpl is not None:
624 written = tmpl.written[0]
625 props = tmpl.properties
626 ret = {name: [val, ptype, anim, written] for name, (val, ptype, anim) in props.items()}
627 return ret
630 def elem_props_template_set(template, elem, ptype_name, name, value, animatable=False, animated=False):
632 Only add a prop if the same value is not already defined in given template.
633 Note it is important to not give iterators as value, here!
635 ptype = FBX_PROPERTIES_DEFINITIONS[ptype_name]
636 if len(ptype) > 3:
637 value = tuple(value)
638 tmpl_val, tmpl_ptype, tmpl_animatable, tmpl_written = template.get(name, (None, None, False, False))
639 # Note animatable flag from template takes precedence over given one, if applicable.
640 # However, animated properties are always written, since they cannot match their template!
641 if tmpl_ptype is not None and not animated:
642 if (tmpl_written and
643 ((len(ptype) == 3 and (tmpl_val, tmpl_ptype) == (value, ptype_name)) or
644 (len(ptype) > 3 and (tuple(tmpl_val), tmpl_ptype) == (value, ptype_name)))):
645 return # Already in template and same value.
646 _elem_props_set(elem, ptype, name, value, _elem_props_flags(tmpl_animatable, animated, False))
647 template[name][3] = True
648 else:
649 _elem_props_set(elem, ptype, name, value, _elem_props_flags(animatable, animated, False))
652 def elem_props_template_finalize(template, elem):
654 Finalize one element's template/props.
655 Issue is, some templates might be "needed" by different types (e.g. NodeAttribute is for lights, cameras, etc.),
656 but values for only *one* subtype can be written as template. So we have to be sure we write those for the other
657 subtypes in each and every elements, if they are not overridden by that element.
658 Yes, hairy, FBX that is to say. When they could easily support several subtypes per template... :(
660 for name, (value, ptype_name, animatable, written) in template.items():
661 if written:
662 continue
663 ptype = FBX_PROPERTIES_DEFINITIONS[ptype_name]
664 _elem_props_set(elem, ptype, name, value, _elem_props_flags(animatable, False, False))
667 # ##### Templates #####
668 # TODO: check all those "default" values, they should match Blender's default as much as possible, I guess?
670 FBXTemplate = namedtuple("FBXTemplate", ("type_name", "prop_type_name", "properties", "nbr_users", "written"))
673 def fbx_templates_generate(root, fbx_templates):
674 # We may have to gather different templates in the same node (e.g. NodeAttribute template gathers properties
675 # for Lights, Cameras, LibNodes, etc.).
676 ref_templates = {(tmpl.type_name, tmpl.prop_type_name): tmpl for tmpl in fbx_templates.values()}
678 templates = {}
679 for type_name, prop_type_name, properties, nbr_users, _written in fbx_templates.values():
680 tmpl = templates.setdefault(type_name, [{}, 0])
681 tmpl[0][prop_type_name] = (properties, nbr_users)
682 tmpl[1] += nbr_users
684 for type_name, (subprops, nbr_users) in templates.items():
685 template = elem_data_single_string(root, b"ObjectType", type_name)
686 elem_data_single_int32(template, b"Count", nbr_users)
688 if len(subprops) == 1:
689 prop_type_name, (properties, _nbr_sub_type_users) = next(iter(subprops.items()))
690 subprops = (prop_type_name, properties)
691 ref_templates[(type_name, prop_type_name)].written[0] = True
692 else:
693 # Ack! Even though this could/should work, looks like it is not supported. So we have to chose one. :|
694 max_users = max_props = -1
695 written_prop_type_name = None
696 for prop_type_name, (properties, nbr_sub_type_users) in subprops.items():
697 if nbr_sub_type_users > max_users or (nbr_sub_type_users == max_users and len(properties) > max_props):
698 max_users = nbr_sub_type_users
699 max_props = len(properties)
700 written_prop_type_name = prop_type_name
701 subprops = (written_prop_type_name, properties)
702 ref_templates[(type_name, written_prop_type_name)].written[0] = True
704 prop_type_name, properties = subprops
705 if prop_type_name and properties:
706 elem = elem_data_single_string(template, b"PropertyTemplate", prop_type_name)
707 props = elem_properties(elem)
708 for name, (value, ptype, animatable) in properties.items():
709 try:
710 elem_props_set(props, ptype, name, value, animatable=animatable)
711 except Exception as e:
712 print("Failed to write template prop (%r)" % e)
713 print(props, ptype, name, value, animatable)
716 # ##### FBX animation helpers. #####
719 class AnimationCurveNodeWrapper:
721 This class provides a same common interface for all (FBX-wise) AnimationCurveNode and AnimationCurve elements,
722 and easy API to handle those.
724 __slots__ = (
725 'elem_keys', '_keys', 'default_values', 'fbx_group', 'fbx_gname', 'fbx_props',
726 'force_keying', 'force_startend_keying')
728 kinds = {
729 'LCL_TRANSLATION': ("Lcl Translation", "T", ("X", "Y", "Z")),
730 'LCL_ROTATION': ("Lcl Rotation", "R", ("X", "Y", "Z")),
731 'LCL_SCALING': ("Lcl Scaling", "S", ("X", "Y", "Z")),
732 'SHAPE_KEY': ("DeformPercent", "DeformPercent", ("DeformPercent",)),
733 'CAMERA_FOCAL': ("FocalLength", "FocalLength", ("FocalLength",)),
736 def __init__(self, elem_key, kind, force_keying, force_startend_keying, default_values=...):
737 self.elem_keys = [elem_key]
738 assert(kind in self.kinds)
739 self.fbx_group = [self.kinds[kind][0]]
740 self.fbx_gname = [self.kinds[kind][1]]
741 self.fbx_props = [self.kinds[kind][2]]
742 self.force_keying = force_keying
743 self.force_startend_keying = force_startend_keying
744 self._keys = [] # (frame, values, write_flags)
745 if default_values is not ...:
746 assert(len(default_values) == len(self.fbx_props[0]))
747 self.default_values = default_values
748 else:
749 self.default_values = (0.0) * len(self.fbx_props[0])
751 def __bool__(self):
752 # We are 'True' if we do have some validated keyframes...
753 return bool(self._keys) and (True in ((True in k[2]) for k in self._keys))
755 def add_group(self, elem_key, fbx_group, fbx_gname, fbx_props):
757 Add another whole group stuff (curvenode, animated item/prop + curvnode/curve identifiers).
758 E.g. Shapes animations is written twice, houra!
760 assert(len(fbx_props) == len(self.fbx_props[0]))
761 self.elem_keys.append(elem_key)
762 self.fbx_group.append(fbx_group)
763 self.fbx_gname.append(fbx_gname)
764 self.fbx_props.append(fbx_props)
766 def add_keyframe(self, frame, values):
768 Add a new keyframe to all curves of the group.
770 assert(len(values) == len(self.fbx_props[0]))
771 self._keys.append((frame, values, [True] * len(values))) # write everything by default.
773 def simplify(self, fac, step, force_keep=False):
775 Simplifies sampled curves by only enabling samples when:
776 * their values relatively differ from the previous sample ones.
778 if not self._keys:
779 return
781 if fac == 0.0:
782 return
784 # So that, with default factor and step values (1), we get:
785 min_reldiff_fac = fac * 1.0e-3 # min relative value evolution: 0.1% of current 'order of magnitude'.
786 min_absdiff_fac = 0.1 # A tenth of reldiff...
787 keys = self._keys
789 p_currframe, p_key, p_key_write = keys[0]
790 p_keyed = list(p_key)
791 are_keyed = [False] * len(p_key)
792 for currframe, key, key_write in keys:
793 for idx, (val, p_val) in enumerate(zip(key, p_key)):
794 key_write[idx] = False
795 p_keyedval = p_keyed[idx]
796 if val == p_val:
797 # Never write keyframe when value is exactly the same as prev one!
798 continue
799 # This is contracted form of relative + absolute-near-zero difference:
800 # absdiff = abs(a - b)
801 # if absdiff < min_reldiff_fac * min_absdiff_fac:
802 # return False
803 # return (absdiff / ((abs(a) + abs(b)) / 2)) > min_reldiff_fac
804 # Note that we ignore the '/ 2' part here, since it's not much significant for us.
805 if abs(val - p_val) > (min_reldiff_fac * max(abs(val) + abs(p_val), min_absdiff_fac)):
806 # If enough difference from previous sampled value, key this value *and* the previous one!
807 key_write[idx] = True
808 p_key_write[idx] = True
809 p_keyed[idx] = val
810 are_keyed[idx] = True
811 elif abs(val - p_keyedval) > (min_reldiff_fac * max((abs(val) + abs(p_keyedval)), min_absdiff_fac)):
812 # Else, if enough difference from previous keyed value, key this value only!
813 key_write[idx] = True
814 p_keyed[idx] = val
815 are_keyed[idx] = True
816 p_currframe, p_key, p_key_write = currframe, key, key_write
818 # If we write nothing (action doing nothing) and are in 'force_keep' mode, we key everything! :P
819 # See T41766.
820 # Also, it seems some importers (e.g. UE4) do not handle correctly armatures where some bones
821 # are not animated, but are children of animated ones, so added an option to systematically force writing
822 # one key in this case.
823 # See T41719, T41605, T41254...
824 if self.force_keying or (force_keep and not self):
825 are_keyed[:] = [True] * len(are_keyed)
827 # If we did key something, ensure first and last sampled values are keyed as well.
828 if self.force_startend_keying:
829 for idx, is_keyed in enumerate(are_keyed):
830 if is_keyed:
831 keys[0][2][idx] = keys[-1][2][idx] = True
833 def get_final_data(self, scene, ref_id, force_keep=False):
835 Yield final anim data for this 'curvenode' (for all curvenodes defined).
836 force_keep is to force to keep a curve even if it only has one valid keyframe.
838 curves = [[] for k in self._keys[0][1]]
839 for currframe, key, key_write in self._keys:
840 for curve, val, wrt in zip(curves, key, key_write):
841 if wrt:
842 curve.append((currframe, val))
844 force_keep = force_keep or self.force_keying
845 for elem_key, fbx_group, fbx_gname, fbx_props in \
846 zip(self.elem_keys, self.fbx_group, self.fbx_gname, self.fbx_props):
847 group_key = get_blender_anim_curve_node_key(scene, ref_id, elem_key, fbx_group)
848 group = {}
849 for c, def_val, fbx_item in zip(curves, self.default_values, fbx_props):
850 fbx_item = FBX_ANIM_PROPSGROUP_NAME + "|" + fbx_item
851 curve_key = get_blender_anim_curve_key(scene, ref_id, elem_key, fbx_group, fbx_item)
852 # (curve key, default value, keyframes, write flag).
853 group[fbx_item] = (curve_key, def_val, c,
854 True if (len(c) > 1 or (len(c) > 0 and force_keep)) else False)
855 yield elem_key, group_key, group, fbx_group, fbx_gname
858 # ##### FBX objects generators. #####
860 # FBX Model-like data (i.e. Blender objects, depsgraph instances and bones) are wrapped in ObjectWrapper.
861 # This allows us to have a (nearly) same code FBX-wise for all those types.
862 # The wrapper tries to stay as small as possible, by mostly using callbacks (property(get...))
863 # to actual Blender data it contains.
864 # Note it caches its instances, so that you may call several times ObjectWrapper(your_object)
865 # with a minimal cost (just re-computing the key).
867 class MetaObjectWrapper(type):
868 def __call__(cls, bdata, armature=None):
869 if bdata is None:
870 return None
871 dup_mat = None
872 if isinstance(bdata, Object):
873 key = get_blenderID_key(bdata)
874 elif isinstance(bdata, DepsgraphObjectInstance):
875 if bdata.is_instance:
876 key = "|".join((get_blenderID_key((bdata.parent.original, bdata.instance_object.original)),
877 cls._get_dup_num_id(bdata)))
878 dup_mat = bdata.matrix_world.copy()
879 else:
880 key = get_blenderID_key(bdata.object.original)
881 else: # isinstance(bdata, (Bone, PoseBone)):
882 if isinstance(bdata, PoseBone):
883 bdata = armature.data.bones[bdata.name]
884 key = get_blenderID_key((armature, bdata))
886 cache = getattr(cls, "_cache", None)
887 if cache is None:
888 cache = cls._cache = {}
889 instance = cache.get(key)
890 if instance is not None:
891 # Duplis hack: since dupli instances are not persistent in Blender (we have to re-create them to get updated
892 # info like matrix...), we *always* need to reset that matrix when calling ObjectWrapper() (all
893 # other data is supposed valid during whole cache live span, so we can skip resetting it).
894 instance._dupli_matrix = dup_mat
895 return instance
897 instance = cls.__new__(cls, bdata, armature)
898 instance.__init__(bdata, armature)
899 instance.key = key
900 instance._dupli_matrix = dup_mat
901 cache[key] = instance
902 return instance
905 class ObjectWrapper(metaclass=MetaObjectWrapper):
907 This class provides a same common interface for all (FBX-wise) object-like elements:
908 * Blender Object
909 * Blender Bone and PoseBone
910 * Blender DepsgraphObjectInstance (for dulis).
911 Note since a same Blender object might be 'mapped' to several FBX models (esp. with duplis),
912 we need to use a key to identify each.
914 __slots__ = (
915 'name', 'key', 'bdata', 'parented_to_armature',
916 '_tag', '_ref', '_dupli_matrix'
919 @classmethod
920 def cache_clear(cls):
921 if hasattr(cls, "_cache"):
922 del cls._cache
924 @staticmethod
925 def _get_dup_num_id(bdata):
926 INVALID_IDS = {2147483647, 0}
927 pids = tuple(bdata.persistent_id)
928 idx_valid = 0
929 prev_i = ...
930 for idx, i in enumerate(pids[::-1]):
931 if i not in INVALID_IDS or (idx == len(pids) and i == 0 and prev_i != 0):
932 idx_valid = len(pids) - idx
933 break
934 prev_i = i
935 return ".".join(str(i) for i in pids[:idx_valid])
937 def __init__(self, bdata, armature=None):
939 bdata might be an Object (deprecated), DepsgraphObjectInstance, Bone or PoseBone.
940 If Bone or PoseBone, armature Object must be provided.
942 # Note: DepsgraphObjectInstance are purely runtime data, they become invalid as soon as we step to the next item!
943 # Hence we have to immediately copy *all* needed data...
944 if isinstance(bdata, Object): # DEPRECATED
945 self._tag = 'OB'
946 self.name = get_blenderID_name(bdata)
947 self.bdata = bdata
948 self._ref = None
949 elif isinstance(bdata, DepsgraphObjectInstance):
950 if bdata.is_instance:
951 # Note that dupli instance matrix is set by meta-class initialization.
952 self._tag = 'DP'
953 self.name = "|".join((get_blenderID_name((bdata.parent.original, bdata.instance_object.original)),
954 "Dupli", self._get_dup_num_id(bdata)))
955 self.bdata = bdata.instance_object.original
956 self._ref = bdata.parent.original
957 else:
958 self._tag = 'OB'
959 self.name = get_blenderID_name(bdata)
960 self.bdata = bdata.object.original
961 self._ref = None
962 else: # isinstance(bdata, (Bone, PoseBone)):
963 if isinstance(bdata, PoseBone):
964 bdata = armature.data.bones[bdata.name]
965 self._tag = 'BO'
966 self.name = get_blenderID_name(bdata)
967 self.bdata = bdata
968 self._ref = armature
969 self.parented_to_armature = False
971 def __eq__(self, other):
972 return isinstance(other, self.__class__) and self.key == other.key
974 def __hash__(self):
975 return hash(self.key)
977 def __repr__(self):
978 return self.key
980 # #### Common to all _tag values.
981 def get_fbx_uuid(self):
982 return get_fbx_uuid_from_key(self.key)
983 fbx_uuid = property(get_fbx_uuid)
985 # XXX Not sure how much that’s useful now... :/
986 def get_hide(self):
987 return self.bdata.hide_viewport if self._tag in {'OB', 'DP'} else self.bdata.hide
988 hide = property(get_hide)
990 def get_parent(self):
991 if self._tag == 'OB':
992 if (self.bdata.parent and self.bdata.parent.type == 'ARMATURE' and
993 self.bdata.parent_type == 'BONE' and self.bdata.parent_bone):
994 # Try to parent to a bone.
995 bo_par = self.bdata.parent.pose.bones.get(self.bdata.parent_bone, None)
996 if (bo_par):
997 return ObjectWrapper(bo_par, self.bdata.parent)
998 else: # Fallback to mere object parenting.
999 return ObjectWrapper(self.bdata.parent)
1000 else:
1001 # Mere object parenting.
1002 return ObjectWrapper(self.bdata.parent)
1003 elif self._tag == 'DP':
1004 return ObjectWrapper(self._ref)
1005 else: # self._tag == 'BO'
1006 return ObjectWrapper(self.bdata.parent, self._ref) or ObjectWrapper(self._ref)
1007 parent = property(get_parent)
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
1036 else:
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
1043 else:
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):
1049 par = self.parent
1050 if par in objects:
1051 if self._tag == 'OB':
1052 par_type = self.bdata.parent_type
1053 if par_type in {'OBJECT', 'BONE'}:
1054 return True
1055 else:
1056 print("Sorry, “{}” parenting type is not supported".format(par_type))
1057 return False
1058 return True
1059 return False
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.
1111 if parent:
1112 if is_global:
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
1127 if is_global:
1128 # In any case, pre-multiply the global matrix to get it in FBX global space!
1129 matrix = scene_data.settings.global_matrix @ matrix
1131 return 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)
1143 else:
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)
1160 def get_type(self):
1161 if self._tag in {'OB', 'DP'}:
1162 return self.bdata.type
1163 return ...
1164 type = property(get_type)
1166 def get_armature(self):
1167 if self._tag == 'BO':
1168 return ObjectWrapper(self._ref)
1169 return None
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)
1175 return ()
1176 bones = property(get_bones)
1178 def get_material_slots(self):
1179 if self._tag in {'OB', 'DP'}:
1180 return self.bdata.material_slots
1181 return ()
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'):
1186 return False
1187 if self.parent == arm_obj and self.bdata.parent_type == 'ARMATURE':
1188 return True
1189 for mod in self.bdata.modifiers:
1190 if mod.type == 'ARMATURE' and mod.object in {arm_obj.bdata, arm_obj.bdata.proxy}:
1191 return True
1193 # #### Duplis...
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)
1198 return ()
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_mesh_edges", "use_tspace",
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",
1226 # Helper container gathering some data we need multiple times:
1227 # * templates.
1228 # * settings, scene.
1229 # * objects.
1230 # * object data.
1231 # * skinning data (binding armature/mesh).
1232 # * animations.
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",
1248 "use_custom_props", "use_custom_props_enum_as_string",
1249 "nodal_material_wrap_map", "image_cache",
1250 "ignore_leaf_bones", "force_connect_children", "automatic_bone_orientation", "bone_correction_matrix",
1251 "use_prepost_rot",