Skinify: fix shape generation
[blender-addons.git] / io_scene_fbx / fbx_utils.py
blobe52cd9eb141bf5fb78b88fb82b45ce7943bdaf09
1 # SPDX-License-Identifier: GPL-2.0-or-later
3 # Script copyright (C) Campbell Barton, Bastien Montagne
6 import math
7 import time
9 from collections import namedtuple
10 from collections.abc import Iterable
11 from itertools import zip_longest, chain
13 import bpy
14 import bpy_extras
15 from bpy.types import Object, Bone, PoseBone, DepsgraphObjectInstance
16 from mathutils import Vector, Matrix
18 from . import encode_bin, data_types
21 # "Constants"
22 FBX_VERSION = 7400
23 FBX_HEADER_VERSION = 1003
24 FBX_SCENEINFO_VERSION = 100
25 FBX_TEMPLATES_VERSION = 100
27 FBX_MODELS_VERSION = 232
29 FBX_GEOMETRY_VERSION = 124
30 # Revert back normals to 101 (simple 3D values) for now, 102 (4D + weights) seems not well supported by most apps
31 # currently, apart from some AD products.
32 FBX_GEOMETRY_NORMAL_VERSION = 101
33 FBX_GEOMETRY_BINORMAL_VERSION = 101
34 FBX_GEOMETRY_TANGENT_VERSION = 101
35 FBX_GEOMETRY_SMOOTHING_VERSION = 102
36 FBX_GEOMETRY_CREASE_VERSION = 101
37 FBX_GEOMETRY_VCOLOR_VERSION = 101
38 FBX_GEOMETRY_UV_VERSION = 101
39 FBX_GEOMETRY_MATERIAL_VERSION = 101
40 FBX_GEOMETRY_LAYER_VERSION = 100
41 FBX_GEOMETRY_SHAPE_VERSION = 100
42 FBX_DEFORMER_SHAPE_VERSION = 100
43 FBX_DEFORMER_SHAPECHANNEL_VERSION = 100
44 FBX_POSE_BIND_VERSION = 100
45 FBX_DEFORMER_SKIN_VERSION = 101
46 FBX_DEFORMER_CLUSTER_VERSION = 100
47 FBX_MATERIAL_VERSION = 102
48 FBX_TEXTURE_VERSION = 202
49 FBX_ANIM_KEY_VERSION = 4008
51 FBX_NAME_CLASS_SEP = b"\x00\x01"
52 FBX_ANIM_PROPSGROUP_NAME = "d"
54 FBX_KTIME = 46186158000 # This is the number of "ktimes" in one second (yep, precision over the nanosecond...)
57 MAT_CONVERT_LIGHT = Matrix.Rotation(math.pi / 2.0, 4, 'X') # Blender is -Z, FBX is -Y.
58 MAT_CONVERT_CAMERA = Matrix.Rotation(math.pi / 2.0, 4, 'Y') # Blender is -Z, FBX is +X.
59 # XXX I can't get this working :(
60 # MAT_CONVERT_BONE = Matrix.Rotation(math.pi / 2.0, 4, 'Z') # Blender is +Y, FBX is -X.
61 MAT_CONVERT_BONE = Matrix()
64 BLENDER_OTHER_OBJECT_TYPES = {'CURVE', 'SURFACE', 'FONT', 'META'}
65 BLENDER_OBJECT_TYPES_MESHLIKE = {'MESH'} | BLENDER_OTHER_OBJECT_TYPES
68 # Lamps.
69 FBX_LIGHT_TYPES = {
70 'POINT': 0, # Point.
71 'SUN': 1, # Directional.
72 'SPOT': 2, # Spot.
73 'HEMI': 1, # Directional.
74 'AREA': 3, # Area.
76 FBX_LIGHT_DECAY_TYPES = {
77 'CONSTANT': 0, # None.
78 'INVERSE_LINEAR': 1, # Linear.
79 'INVERSE_SQUARE': 2, # Quadratic.
80 'INVERSE_COEFFICIENTS': 2, # Quadratic...
81 'CUSTOM_CURVE': 2, # Quadratic.
82 'LINEAR_QUADRATIC_WEIGHTED': 2, # Quadratic.
86 RIGHT_HAND_AXES = {
87 # Up, Forward -> FBX values (tuples of (axis, sign), Up, Front, Coord).
88 ( 'X', '-Y'): ((0, 1), (1, 1), (2, 1)),
89 ( 'X', 'Y'): ((0, 1), (1, -1), (2, -1)),
90 ( 'X', '-Z'): ((0, 1), (2, 1), (1, -1)),
91 ( 'X', 'Z'): ((0, 1), (2, -1), (1, 1)),
92 ('-X', '-Y'): ((0, -1), (1, 1), (2, -1)),
93 ('-X', 'Y'): ((0, -1), (1, -1), (2, 1)),
94 ('-X', '-Z'): ((0, -1), (2, 1), (1, 1)),
95 ('-X', 'Z'): ((0, -1), (2, -1), (1, -1)),
96 ( 'Y', '-X'): ((1, 1), (0, 1), (2, -1)),
97 ( 'Y', 'X'): ((1, 1), (0, -1), (2, 1)),
98 ( 'Y', '-Z'): ((1, 1), (2, 1), (0, 1)),
99 ( 'Y', 'Z'): ((1, 1), (2, -1), (0, -1)),
100 ('-Y', '-X'): ((1, -1), (0, 1), (2, 1)),
101 ('-Y', 'X'): ((1, -1), (0, -1), (2, -1)),
102 ('-Y', '-Z'): ((1, -1), (2, 1), (0, -1)),
103 ('-Y', 'Z'): ((1, -1), (2, -1), (0, 1)),
104 ( 'Z', '-X'): ((2, 1), (0, 1), (1, 1)),
105 ( 'Z', 'X'): ((2, 1), (0, -1), (1, -1)),
106 ( 'Z', '-Y'): ((2, 1), (1, 1), (0, -1)),
107 ( 'Z', 'Y'): ((2, 1), (1, -1), (0, 1)), # Blender system!
108 ('-Z', '-X'): ((2, -1), (0, 1), (1, -1)),
109 ('-Z', 'X'): ((2, -1), (0, -1), (1, 1)),
110 ('-Z', '-Y'): ((2, -1), (1, 1), (0, 1)),
111 ('-Z', 'Y'): ((2, -1), (1, -1), (0, -1)),
115 # NOTE: Not fully in enum value order, since when exporting the first entry matching the framerate value is used
116 # (e.g. better have NTSC fullframe than NTSC drop frame for 29.97 framerate).
117 FBX_FRAMERATES = (
118 #(-1.0, 0), # Default framerate.
119 (-1.0, 14), # Custom framerate.
120 (120.0, 1),
121 (100.0, 2),
122 (60.0, 3),
123 (50.0, 4),
124 (48.0, 5),
125 (30.0, 6), # BW NTSC, full frame.
126 (30.0, 7), # Drop frame.
127 (30.0 / 1.001, 9), # Color NTSC, full frame.
128 (30.0 / 1.001, 8), # Color NTSC, drop frame.
129 (25.0, 10),
130 (24.0, 11),
131 #(1.0, 12), # 1000 milli/s (use for date time?).
132 (24.0 / 1.001, 13),
133 (96.0, 15),
134 (72.0, 16),
135 (60.0 / 1.001, 17),
136 (120.0 / 1.001, 18),
140 # ##### Misc utilities #####
142 # Enable performance reports (measuring time used to perform various steps of importing or exporting).
143 DO_PERFMON = False
145 if DO_PERFMON:
146 class PerfMon():
147 def __init__(self):
148 self.level = -1
149 self.ref_time = []
151 def level_up(self, message=""):
152 self.level += 1
153 self.ref_time.append(None)
154 if message:
155 print("\t" * self.level, message, sep="")
157 def level_down(self, message=""):
158 if not self.ref_time:
159 if message:
160 print(message)
161 return
162 ref_time = self.ref_time[self.level]
163 print("\t" * self.level,
164 "\tDone (%f sec)\n" % ((time.process_time() - ref_time) if ref_time is not None else 0.0),
165 sep="")
166 if message:
167 print("\t" * self.level, message, sep="")
168 del self.ref_time[self.level]
169 self.level -= 1
171 def step(self, message=""):
172 ref_time = self.ref_time[self.level]
173 curr_time = time.process_time()
174 if ref_time is not None:
175 print("\t" * self.level, "\tDone (%f sec)\n" % (curr_time - ref_time), sep="")
176 self.ref_time[self.level] = curr_time
177 print("\t" * self.level, message, sep="")
178 else:
179 class PerfMon():
180 def __init__(self):
181 pass
183 def level_up(self, message=""):
184 pass
186 def level_down(self, message=""):
187 pass
189 def step(self, message=""):
190 pass
193 # Scale/unit mess. FBX can store the 'reference' unit of a file in its UnitScaleFactor property
194 # (1.0 meaning centimeter, afaik). We use that to reflect user's default unit as set in Blender with scale_length.
195 # However, we always get values in BU (i.e. meters), so we have to reverse-apply that scale in global matrix...
196 # Note that when no default unit is available, we assume 'meters' (and hence scale by 100).
197 def units_blender_to_fbx_factor(scene):
198 return 100.0 if (scene.unit_settings.system == 'NONE') else (100.0 * scene.unit_settings.scale_length)
201 # Note: this could be in a utility (math.units e.g.)...
203 UNITS = {
204 "meter": 1.0, # Ref unit!
205 "kilometer": 0.001,
206 "millimeter": 1000.0,
207 "foot": 1.0 / 0.3048,
208 "inch": 1.0 / 0.0254,
209 "turn": 1.0, # Ref unit!
210 "degree": 360.0,
211 "radian": math.pi * 2.0,
212 "second": 1.0, # Ref unit!
213 "ktime": FBX_KTIME,
217 def units_convertor(u_from, u_to):
218 """Return a convertor between specified units."""
219 conv = UNITS[u_to] / UNITS[u_from]
220 return lambda v: v * conv
223 def units_convertor_iter(u_from, u_to):
224 """Return an iterable convertor between specified units."""
225 conv = units_convertor(u_from, u_to)
227 def convertor(it):
228 for v in it:
229 yield(conv(v))
231 return convertor
234 def matrix4_to_array(mat):
235 """Concatenate matrix's columns into a single, flat tuple"""
236 # blender matrix is row major, fbx is col major so transpose on write
237 return tuple(f for v in mat.transposed() for f in v)
240 def array_to_matrix4(arr):
241 """Convert a single 16-len tuple into a valid 4D Blender matrix"""
242 # Blender matrix is row major, fbx is col major so transpose on read
243 return Matrix(tuple(zip(*[iter(arr)]*4))).transposed()
246 def similar_values(v1, v2, e=1e-6):
247 """Return True if v1 and v2 are nearly the same."""
248 if v1 == v2:
249 return True
250 return ((abs(v1 - v2) / max(abs(v1), abs(v2))) <= e)
253 def similar_values_iter(v1, v2, e=1e-6):
254 """Return True if iterables v1 and v2 are nearly the same."""
255 if v1 == v2:
256 return True
257 for v1, v2 in zip(v1, v2):
258 if (v1 != v2) and ((abs(v1 - v2) / max(abs(v1), abs(v2))) > e):
259 return False
260 return True
262 def vcos_transformed_gen(raw_cos, m=None):
263 # Note: we could most likely get much better performances with numpy, but will leave this as TODO for now.
264 gen = zip(*(iter(raw_cos),) * 3)
265 return gen if m is None else (m @ Vector(v) for v in gen)
267 def nors_transformed_gen(raw_nors, m=None):
268 # Great, now normals are also expected 4D!
269 # XXX Back to 3D normals for now!
270 # gen = zip(*(iter(raw_nors),) * 3 + (_infinite_gen(1.0),))
271 gen = zip(*(iter(raw_nors),) * 3)
272 return gen if m is None else (m @ Vector(v) for v in gen)
275 # ##### UIDs code. #####
277 # ID class (mere int).
278 class UUID(int):
279 pass
282 # UIDs storage.
283 _keys_to_uuids = {}
284 _uuids_to_keys = {}
287 def _key_to_uuid(uuids, key):
288 # TODO: Check this is robust enough for our needs!
289 # Note: We assume we have already checked the related key wasn't yet in _keys_to_uids!
290 # As int64 is signed in FBX, we keep uids below 2**63...
291 if isinstance(key, int) and 0 <= key < 2**63:
292 # We can use value directly as id!
293 uuid = key
294 else:
295 uuid = hash(key)
296 if uuid < 0:
297 uuid = -uuid
298 if uuid >= 2**63:
299 uuid //= 2
300 # Try to make our uid shorter!
301 if uuid > int(1e9):
302 t_uuid = uuid % int(1e9)
303 if t_uuid not in uuids:
304 uuid = t_uuid
305 # Make sure our uuid *is* unique.
306 if uuid in uuids:
307 inc = 1 if uuid < 2**62 else -1
308 while uuid in uuids:
309 uuid += inc
310 if 0 > uuid >= 2**63:
311 # Note that this is more that unlikely, but does not harm anyway...
312 raise ValueError("Unable to generate an UUID for key {}".format(key))
313 return UUID(uuid)
316 def get_fbx_uuid_from_key(key):
318 Return an UUID for given key, which is assumed to be hashable.
320 uuid = _keys_to_uuids.get(key, None)
321 if uuid is None:
322 uuid = _key_to_uuid(_uuids_to_keys, key)
323 _keys_to_uuids[key] = uuid
324 _uuids_to_keys[uuid] = key
325 return uuid
328 # XXX Not sure we'll actually need this one?
329 def get_key_from_fbx_uuid(uuid):
331 Return the key which generated this uid.
333 assert(uuid.__class__ == UUID)
334 return _uuids_to_keys.get(uuid, None)
337 # Blender-specific key generators
338 def get_bid_name(bid):
339 library = getattr(bid, "library", None)
340 if library is not None:
341 return "%s_L_%s" % (bid.name, library.name)
342 else:
343 return bid.name
346 def get_blenderID_key(bid):
347 if isinstance(bid, Iterable):
348 return "|".join("B" + e.rna_type.name + "#" + get_bid_name(e) for e in bid)
349 else:
350 return "B" + bid.rna_type.name + "#" + get_bid_name(bid)
353 def get_blenderID_name(bid):
354 if isinstance(bid, Iterable):
355 return "|".join(get_bid_name(e) for e in bid)
356 else:
357 return get_bid_name(bid)
360 def get_blender_empty_key(obj):
361 """Return bone's keys (Model and NodeAttribute)."""
362 return "|".join((get_blenderID_key(obj), "Empty"))
365 def get_blender_mesh_shape_key(me):
366 """Return main shape deformer's key."""
367 return "|".join((get_blenderID_key(me), "Shape"))
370 def get_blender_mesh_shape_channel_key(me, shape):
371 """Return shape channel and geometry shape keys."""
372 return ("|".join((get_blenderID_key(me), "Shape", get_blenderID_key(shape))),
373 "|".join((get_blenderID_key(me), "Geometry", get_blenderID_key(shape))))
376 def get_blender_bone_key(armature, bone):
377 """Return bone's keys (Model and NodeAttribute)."""
378 return "|".join((get_blenderID_key((armature, bone)), "Data"))
381 def get_blender_bindpose_key(obj, mesh):
382 """Return object's bindpose key."""
383 return "|".join((get_blenderID_key(obj), get_blenderID_key(mesh), "BindPose"))
386 def get_blender_armature_skin_key(armature, mesh):
387 """Return armature's skin key."""
388 return "|".join((get_blenderID_key(armature), get_blenderID_key(mesh), "DeformerSkin"))
391 def get_blender_bone_cluster_key(armature, mesh, bone):
392 """Return bone's cluster key."""
393 return "|".join((get_blenderID_key(armature), get_blenderID_key(mesh),
394 get_blenderID_key(bone), "SubDeformerCluster"))
397 def get_blender_anim_id_base(scene, ref_id):
398 if ref_id is not None:
399 return get_blenderID_key(scene) + "|" + get_blenderID_key(ref_id)
400 else:
401 return get_blenderID_key(scene)
404 def get_blender_anim_stack_key(scene, ref_id):
405 """Return single anim stack key."""
406 return get_blender_anim_id_base(scene, ref_id) + "|AnimStack"
409 def get_blender_anim_layer_key(scene, ref_id):
410 """Return ID's anim layer key."""
411 return get_blender_anim_id_base(scene, ref_id) + "|AnimLayer"
414 def get_blender_anim_curve_node_key(scene, ref_id, obj_key, fbx_prop_name):
415 """Return (stack/layer, ID, fbxprop) curve node key."""
416 return "|".join((get_blender_anim_id_base(scene, ref_id), obj_key, fbx_prop_name, "AnimCurveNode"))
419 def get_blender_anim_curve_key(scene, ref_id, obj_key, fbx_prop_name, fbx_prop_item_name):
420 """Return (stack/layer, ID, fbxprop, item) curve key."""
421 return "|".join((get_blender_anim_id_base(scene, ref_id), obj_key, fbx_prop_name,
422 fbx_prop_item_name, "AnimCurve"))
425 def get_blender_nodetexture_key(ma, socket_names):
426 return "|".join((get_blenderID_key(ma), *socket_names))
429 # ##### Element generators. #####
431 # Note: elem may be None, in this case the element is not added to any parent.
432 def elem_empty(elem, name):
433 sub_elem = encode_bin.FBXElem(name)
434 if elem is not None:
435 elem.elems.append(sub_elem)
436 return sub_elem
439 def _elem_data_single(elem, name, value, func_name):
440 sub_elem = elem_empty(elem, name)
441 getattr(sub_elem, func_name)(value)
442 return sub_elem
445 def _elem_data_vec(elem, name, value, func_name):
446 sub_elem = elem_empty(elem, name)
447 func = getattr(sub_elem, func_name)
448 for v in value:
449 func(v)
450 return sub_elem
453 def elem_data_single_bool(elem, name, value):
454 return _elem_data_single(elem, name, value, "add_bool")
457 def elem_data_single_int16(elem, name, value):
458 return _elem_data_single(elem, name, value, "add_int16")
461 def elem_data_single_int32(elem, name, value):
462 return _elem_data_single(elem, name, value, "add_int32")
465 def elem_data_single_int64(elem, name, value):
466 return _elem_data_single(elem, name, value, "add_int64")
469 def elem_data_single_float32(elem, name, value):
470 return _elem_data_single(elem, name, value, "add_float32")
473 def elem_data_single_float64(elem, name, value):
474 return _elem_data_single(elem, name, value, "add_float64")
477 def elem_data_single_bytes(elem, name, value):
478 return _elem_data_single(elem, name, value, "add_bytes")
481 def elem_data_single_string(elem, name, value):
482 return _elem_data_single(elem, name, value, "add_string")
485 def elem_data_single_string_unicode(elem, name, value):
486 return _elem_data_single(elem, name, value, "add_string_unicode")
489 def elem_data_single_bool_array(elem, name, value):
490 return _elem_data_single(elem, name, value, "add_bool_array")
493 def elem_data_single_int32_array(elem, name, value):
494 return _elem_data_single(elem, name, value, "add_int32_array")
497 def elem_data_single_int64_array(elem, name, value):
498 return _elem_data_single(elem, name, value, "add_int64_array")
501 def elem_data_single_float32_array(elem, name, value):
502 return _elem_data_single(elem, name, value, "add_float32_array")
505 def elem_data_single_float64_array(elem, name, value):
506 return _elem_data_single(elem, name, value, "add_float64_array")
509 def elem_data_single_byte_array(elem, name, value):
510 return _elem_data_single(elem, name, value, "add_byte_array")
513 def elem_data_vec_float64(elem, name, value):
514 return _elem_data_vec(elem, name, value, "add_float64")
517 # ##### Generators for standard FBXProperties70 properties. #####
519 def elem_properties(elem):
520 return elem_empty(elem, b"Properties70")
523 # Properties definitions, format: (b"type_1", b"label(???)", "name_set_value_1", "name_set_value_2", ...)
524 # XXX Looks like there can be various variations of formats here... Will have to be checked ultimately!
525 # Also, those "custom" types like 'FieldOfView' or 'Lcl Translation' are pure nonsense,
526 # these are just Vector3D ultimately... *sigh* (again).
527 FBX_PROPERTIES_DEFINITIONS = {
528 # Generic types.
529 "p_bool": (b"bool", b"", "add_int32"), # Yes, int32 for a bool (and they do have a core bool type)!!!
530 "p_integer": (b"int", b"Integer", "add_int32"),
531 "p_ulonglong": (b"ULongLong", b"", "add_int64"),
532 "p_double": (b"double", b"Number", "add_float64"), # Non-animatable?
533 "p_number": (b"Number", b"", "add_float64"), # Animatable-only?
534 "p_enum": (b"enum", b"", "add_int32"),
535 "p_vector_3d": (b"Vector3D", b"Vector", "add_float64", "add_float64", "add_float64"), # Non-animatable?
536 "p_vector": (b"Vector", b"", "add_float64", "add_float64", "add_float64"), # Animatable-only?
537 "p_color_rgb": (b"ColorRGB", b"Color", "add_float64", "add_float64", "add_float64"), # Non-animatable?
538 "p_color": (b"Color", b"", "add_float64", "add_float64", "add_float64"), # Animatable-only?
539 "p_string": (b"KString", b"", "add_string_unicode"),
540 "p_string_url": (b"KString", b"Url", "add_string_unicode"),
541 "p_timestamp": (b"KTime", b"Time", "add_int64"),
542 "p_datetime": (b"DateTime", b"", "add_string_unicode"),
543 # Special types.
544 "p_object": (b"object", b""), # XXX Check this! No value for this prop??? Would really like to know how it works!
545 "p_compound": (b"Compound", b""),
546 # Specific types (sic).
547 # ## Objects (Models).
548 "p_lcl_translation": (b"Lcl Translation", b"", "add_float64", "add_float64", "add_float64"),
549 "p_lcl_rotation": (b"Lcl Rotation", b"", "add_float64", "add_float64", "add_float64"),
550 "p_lcl_scaling": (b"Lcl Scaling", b"", "add_float64", "add_float64", "add_float64"),
551 "p_visibility": (b"Visibility", b"", "add_float64"),
552 "p_visibility_inheritance": (b"Visibility Inheritance", b"", "add_int32"),
553 # ## Cameras!!!
554 "p_roll": (b"Roll", b"", "add_float64"),
555 "p_opticalcenterx": (b"OpticalCenterX", b"", "add_float64"),
556 "p_opticalcentery": (b"OpticalCenterY", b"", "add_float64"),
557 "p_fov": (b"FieldOfView", b"", "add_float64"),
558 "p_fov_x": (b"FieldOfViewX", b"", "add_float64"),
559 "p_fov_y": (b"FieldOfViewY", b"", "add_float64"),
563 def _elem_props_set(elem, ptype, name, value, flags):
564 p = elem_data_single_string(elem, b"P", name)
565 for t in ptype[:2]:
566 p.add_string(t)
567 p.add_string(flags)
568 if len(ptype) == 3:
569 getattr(p, ptype[2])(value)
570 elif len(ptype) > 3:
571 # We assume value is iterable, else it's a bug!
572 for callback, val in zip(ptype[2:], value):
573 getattr(p, callback)(val)
576 def _elem_props_flags(animatable, animated, custom):
577 # XXX: There are way more flags, see
578 # http://help.autodesk.com/view/FBX/2015/ENU/?guid=__cpp_ref_class_fbx_property_flags_html
579 # Unfortunately, as usual, no doc at all about their 'translation' in actual FBX file format.
580 # Curse you-know-who.
581 if animatable:
582 if animated:
583 if custom:
584 return b"A+U"
585 return b"A+"
586 if custom:
587 # Seems that customprops always need those 'flags', see T69554. Go figure...
588 return b"A+U"
589 return b"A"
590 if custom:
591 # Seems that customprops always need those 'flags', see T69554. Go figure...
592 return b"A+U"
593 return b""
596 def elem_props_set(elem, ptype, name, value=None, animatable=False, animated=False, custom=False):
597 ptype = FBX_PROPERTIES_DEFINITIONS[ptype]
598 _elem_props_set(elem, ptype, name, value, _elem_props_flags(animatable, animated, custom))
601 def elem_props_compound(elem, cmpd_name, custom=False):
602 def _setter(ptype, name, value, animatable=False, animated=False, custom=False):
603 name = cmpd_name + b"|" + name
604 elem_props_set(elem, ptype, name, value, animatable=animatable, animated=animated, custom=custom)
606 elem_props_set(elem, "p_compound", cmpd_name, custom=custom)
607 return _setter
610 def elem_props_template_init(templates, template_type):
612 Init a writing template of given type, for *one* element's properties.
614 ret = {}
615 tmpl = templates.get(template_type)
616 if tmpl is not None:
617 written = tmpl.written[0]
618 props = tmpl.properties
619 ret = {name: [val, ptype, anim, written] for name, (val, ptype, anim) in props.items()}
620 return ret
623 def elem_props_template_set(template, elem, ptype_name, name, value, animatable=False, animated=False):
625 Only add a prop if the same value is not already defined in given template.
626 Note it is important to not give iterators as value, here!
628 ptype = FBX_PROPERTIES_DEFINITIONS[ptype_name]
629 if len(ptype) > 3:
630 value = tuple(value)
631 tmpl_val, tmpl_ptype, tmpl_animatable, tmpl_written = template.get(name, (None, None, False, False))
632 # Note animatable flag from template takes precedence over given one, if applicable.
633 # However, animated properties are always written, since they cannot match their template!
634 if tmpl_ptype is not None and not animated:
635 if (tmpl_written and
636 ((len(ptype) == 3 and (tmpl_val, tmpl_ptype) == (value, ptype_name)) or
637 (len(ptype) > 3 and (tuple(tmpl_val), tmpl_ptype) == (value, ptype_name)))):
638 return # Already in template and same value.
639 _elem_props_set(elem, ptype, name, value, _elem_props_flags(tmpl_animatable, animated, False))
640 template[name][3] = True
641 else:
642 _elem_props_set(elem, ptype, name, value, _elem_props_flags(animatable, animated, False))
645 def elem_props_template_finalize(template, elem):
647 Finalize one element's template/props.
648 Issue is, some templates might be "needed" by different types (e.g. NodeAttribute is for lights, cameras, etc.),
649 but values for only *one* subtype can be written as template. So we have to be sure we write those for the other
650 subtypes in each and every elements, if they are not overridden by that element.
651 Yes, hairy, FBX that is to say. When they could easily support several subtypes per template... :(
653 for name, (value, ptype_name, animatable, written) in template.items():
654 if written:
655 continue
656 ptype = FBX_PROPERTIES_DEFINITIONS[ptype_name]
657 _elem_props_set(elem, ptype, name, value, _elem_props_flags(animatable, False, False))
660 # ##### Templates #####
661 # TODO: check all those "default" values, they should match Blender's default as much as possible, I guess?
663 FBXTemplate = namedtuple("FBXTemplate", ("type_name", "prop_type_name", "properties", "nbr_users", "written"))
666 def fbx_templates_generate(root, fbx_templates):
667 # We may have to gather different templates in the same node (e.g. NodeAttribute template gathers properties
668 # for Lights, Cameras, LibNodes, etc.).
669 ref_templates = {(tmpl.type_name, tmpl.prop_type_name): tmpl for tmpl in fbx_templates.values()}
671 templates = {}
672 for type_name, prop_type_name, properties, nbr_users, _written in fbx_templates.values():
673 tmpl = templates.setdefault(type_name, [{}, 0])
674 tmpl[0][prop_type_name] = (properties, nbr_users)
675 tmpl[1] += nbr_users
677 for type_name, (subprops, nbr_users) in templates.items():
678 template = elem_data_single_string(root, b"ObjectType", type_name)
679 elem_data_single_int32(template, b"Count", nbr_users)
681 if len(subprops) == 1:
682 prop_type_name, (properties, _nbr_sub_type_users) = next(iter(subprops.items()))
683 subprops = (prop_type_name, properties)
684 ref_templates[(type_name, prop_type_name)].written[0] = True
685 else:
686 # Ack! Even though this could/should work, looks like it is not supported. So we have to chose one. :|
687 max_users = max_props = -1
688 written_prop_type_name = None
689 for prop_type_name, (properties, nbr_sub_type_users) in subprops.items():
690 if nbr_sub_type_users > max_users or (nbr_sub_type_users == max_users and len(properties) > max_props):
691 max_users = nbr_sub_type_users
692 max_props = len(properties)
693 written_prop_type_name = prop_type_name
694 subprops = (written_prop_type_name, properties)
695 ref_templates[(type_name, written_prop_type_name)].written[0] = True
697 prop_type_name, properties = subprops
698 if prop_type_name and properties:
699 elem = elem_data_single_string(template, b"PropertyTemplate", prop_type_name)
700 props = elem_properties(elem)
701 for name, (value, ptype, animatable) in properties.items():
702 try:
703 elem_props_set(props, ptype, name, value, animatable=animatable)
704 except Exception as e:
705 print("Failed to write template prop (%r)" % e)
706 print(props, ptype, name, value, animatable)
709 # ##### FBX animation helpers. #####
712 class AnimationCurveNodeWrapper:
714 This class provides a same common interface for all (FBX-wise) AnimationCurveNode and AnimationCurve elements,
715 and easy API to handle those.
717 __slots__ = (
718 'elem_keys', '_keys', 'default_values', 'fbx_group', 'fbx_gname', 'fbx_props',
719 'force_keying', 'force_startend_keying')
721 kinds = {
722 'LCL_TRANSLATION': ("Lcl Translation", "T", ("X", "Y", "Z")),
723 'LCL_ROTATION': ("Lcl Rotation", "R", ("X", "Y", "Z")),
724 'LCL_SCALING': ("Lcl Scaling", "S", ("X", "Y", "Z")),
725 'SHAPE_KEY': ("DeformPercent", "DeformPercent", ("DeformPercent",)),
726 'CAMERA_FOCAL': ("FocalLength", "FocalLength", ("FocalLength",)),
727 'CAMERA_FOCUS_DISTANCE': ("FocusDistance", "FocusDistance", ("FocusDistance",)),
730 def __init__(self, elem_key, kind, force_keying, force_startend_keying, default_values=...):
731 self.elem_keys = [elem_key]
732 assert(kind in self.kinds)
733 self.fbx_group = [self.kinds[kind][0]]
734 self.fbx_gname = [self.kinds[kind][1]]
735 self.fbx_props = [self.kinds[kind][2]]
736 self.force_keying = force_keying
737 self.force_startend_keying = force_startend_keying
738 self._keys = [] # (frame, values, write_flags)
739 if default_values is not ...:
740 assert(len(default_values) == len(self.fbx_props[0]))
741 self.default_values = default_values
742 else:
743 self.default_values = (0.0) * len(self.fbx_props[0])
745 def __bool__(self):
746 # We are 'True' if we do have some validated keyframes...
747 return bool(self._keys) and (True in ((True in k[2]) for k in self._keys))
749 def add_group(self, elem_key, fbx_group, fbx_gname, fbx_props):
751 Add another whole group stuff (curvenode, animated item/prop + curvnode/curve identifiers).
752 E.g. Shapes animations is written twice, houra!
754 assert(len(fbx_props) == len(self.fbx_props[0]))
755 self.elem_keys.append(elem_key)
756 self.fbx_group.append(fbx_group)
757 self.fbx_gname.append(fbx_gname)
758 self.fbx_props.append(fbx_props)
760 def add_keyframe(self, frame, values):
762 Add a new keyframe to all curves of the group.
764 assert(len(values) == len(self.fbx_props[0]))
765 self._keys.append((frame, values, [True] * len(values))) # write everything by default.
767 def simplify(self, fac, step, force_keep=False):
769 Simplifies sampled curves by only enabling samples when:
770 * their values relatively differ from the previous sample ones.
772 if not self._keys:
773 return
775 if fac == 0.0:
776 return
778 # So that, with default factor and step values (1), we get:
779 min_reldiff_fac = fac * 1.0e-3 # min relative value evolution: 0.1% of current 'order of magnitude'.
780 min_absdiff_fac = 0.1 # A tenth of reldiff...
781 keys = self._keys
783 p_currframe, p_key, p_key_write = keys[0]
784 p_keyed = list(p_key)
785 are_keyed = [False] * len(p_key)
786 for currframe, key, key_write in keys:
787 for idx, (val, p_val) in enumerate(zip(key, p_key)):
788 key_write[idx] = False
789 p_keyedval = p_keyed[idx]
790 if val == p_val:
791 # Never write keyframe when value is exactly the same as prev one!
792 continue
793 # This is contracted form of relative + absolute-near-zero difference:
794 # absdiff = abs(a - b)
795 # if absdiff < min_reldiff_fac * min_absdiff_fac:
796 # return False
797 # return (absdiff / ((abs(a) + abs(b)) / 2)) > min_reldiff_fac
798 # Note that we ignore the '/ 2' part here, since it's not much significant for us.
799 if abs(val - p_val) > (min_reldiff_fac * max(abs(val) + abs(p_val), min_absdiff_fac)):
800 # If enough difference from previous sampled value, key this value *and* the previous one!
801 key_write[idx] = True
802 p_key_write[idx] = True
803 p_keyed[idx] = val
804 are_keyed[idx] = True
805 elif abs(val - p_keyedval) > (min_reldiff_fac * max((abs(val) + abs(p_keyedval)), min_absdiff_fac)):
806 # Else, if enough difference from previous keyed value, key this value only!
807 key_write[idx] = True
808 p_keyed[idx] = val
809 are_keyed[idx] = True
810 p_currframe, p_key, p_key_write = currframe, key, key_write
812 # If we write nothing (action doing nothing) and are in 'force_keep' mode, we key everything! :P
813 # See T41766.
814 # Also, it seems some importers (e.g. UE4) do not handle correctly armatures where some bones
815 # are not animated, but are children of animated ones, so added an option to systematically force writing
816 # one key in this case.
817 # See T41719, T41605, T41254...
818 if self.force_keying or (force_keep and not self):
819 are_keyed[:] = [True] * len(are_keyed)
821 # If we did key something, ensure first and last sampled values are keyed as well.
822 if self.force_startend_keying:
823 for idx, is_keyed in enumerate(are_keyed):
824 if is_keyed:
825 keys[0][2][idx] = keys[-1][2][idx] = True
827 def get_final_data(self, scene, ref_id, force_keep=False):
829 Yield final anim data for this 'curvenode' (for all curvenodes defined).
830 force_keep is to force to keep a curve even if it only has one valid keyframe.
832 curves = [[] for k in self._keys[0][1]]
833 for currframe, key, key_write in self._keys:
834 for curve, val, wrt in zip(curves, key, key_write):
835 if wrt:
836 curve.append((currframe, val))
838 force_keep = force_keep or self.force_keying
839 for elem_key, fbx_group, fbx_gname, fbx_props in \
840 zip(self.elem_keys, self.fbx_group, self.fbx_gname, self.fbx_props):
841 group_key = get_blender_anim_curve_node_key(scene, ref_id, elem_key, fbx_group)
842 group = {}
843 for c, def_val, fbx_item in zip(curves, self.default_values, fbx_props):
844 fbx_item = FBX_ANIM_PROPSGROUP_NAME + "|" + fbx_item
845 curve_key = get_blender_anim_curve_key(scene, ref_id, elem_key, fbx_group, fbx_item)
846 # (curve key, default value, keyframes, write flag).
847 group[fbx_item] = (curve_key, def_val, c,
848 True if (len(c) > 1 or (len(c) > 0 and force_keep)) else False)
849 yield elem_key, group_key, group, fbx_group, fbx_gname
852 # ##### FBX objects generators. #####
854 # FBX Model-like data (i.e. Blender objects, depsgraph instances and bones) are wrapped in ObjectWrapper.
855 # This allows us to have a (nearly) same code FBX-wise for all those types.
856 # The wrapper tries to stay as small as possible, by mostly using callbacks (property(get...))
857 # to actual Blender data it contains.
858 # Note it caches its instances, so that you may call several times ObjectWrapper(your_object)
859 # with a minimal cost (just re-computing the key).
861 class MetaObjectWrapper(type):
862 def __call__(cls, bdata, armature=None):
863 if bdata is None:
864 return None
865 dup_mat = None
866 if isinstance(bdata, Object):
867 key = get_blenderID_key(bdata)
868 elif isinstance(bdata, DepsgraphObjectInstance):
869 if bdata.is_instance:
870 key = "|".join((get_blenderID_key((bdata.parent.original, bdata.instance_object.original)),
871 cls._get_dup_num_id(bdata)))
872 dup_mat = bdata.matrix_world.copy()
873 else:
874 key = get_blenderID_key(bdata.object.original)
875 else: # isinstance(bdata, (Bone, PoseBone)):
876 if isinstance(bdata, PoseBone):
877 bdata = armature.data.bones[bdata.name]
878 key = get_blenderID_key((armature, bdata))
880 cache = getattr(cls, "_cache", None)
881 if cache is None:
882 cache = cls._cache = {}
883 instance = cache.get(key)
884 if instance is not None:
885 # Duplis hack: since dupli instances are not persistent in Blender (we have to re-create them to get updated
886 # info like matrix...), we *always* need to reset that matrix when calling ObjectWrapper() (all
887 # other data is supposed valid during whole cache live span, so we can skip resetting it).
888 instance._dupli_matrix = dup_mat
889 return instance
891 instance = cls.__new__(cls, bdata, armature)
892 instance.__init__(bdata, armature)
893 instance.key = key
894 instance._dupli_matrix = dup_mat
895 cache[key] = instance
896 return instance
899 class ObjectWrapper(metaclass=MetaObjectWrapper):
901 This class provides a same common interface for all (FBX-wise) object-like elements:
902 * Blender Object
903 * Blender Bone and PoseBone
904 * Blender DepsgraphObjectInstance (for dulis).
905 Note since a same Blender object might be 'mapped' to several FBX models (esp. with duplis),
906 we need to use a key to identify each.
908 __slots__ = (
909 'name', 'key', 'bdata', 'parented_to_armature',
910 '_tag', '_ref', '_dupli_matrix'
913 @classmethod
914 def cache_clear(cls):
915 if hasattr(cls, "_cache"):
916 del cls._cache
918 @staticmethod
919 def _get_dup_num_id(bdata):
920 INVALID_IDS = {2147483647, 0}
921 pids = tuple(bdata.persistent_id)
922 idx_valid = 0
923 prev_i = ...
924 for idx, i in enumerate(pids[::-1]):
925 if i not in INVALID_IDS or (idx == len(pids) and i == 0 and prev_i != 0):
926 idx_valid = len(pids) - idx
927 break
928 prev_i = i
929 return ".".join(str(i) for i in pids[:idx_valid])
931 def __init__(self, bdata, armature=None):
933 bdata might be an Object (deprecated), DepsgraphObjectInstance, Bone or PoseBone.
934 If Bone or PoseBone, armature Object must be provided.
936 # Note: DepsgraphObjectInstance are purely runtime data, they become invalid as soon as we step to the next item!
937 # Hence we have to immediately copy *all* needed data...
938 if isinstance(bdata, Object): # DEPRECATED
939 self._tag = 'OB'
940 self.name = get_blenderID_name(bdata)
941 self.bdata = bdata
942 self._ref = None
943 elif isinstance(bdata, DepsgraphObjectInstance):
944 if bdata.is_instance:
945 # Note that dupli instance matrix is set by meta-class initialization.
946 self._tag = 'DP'
947 self.name = "|".join((get_blenderID_name((bdata.parent.original, bdata.instance_object.original)),
948 "Dupli", self._get_dup_num_id(bdata)))
949 self.bdata = bdata.instance_object.original
950 self._ref = bdata.parent.original
951 else:
952 self._tag = 'OB'
953 self.name = get_blenderID_name(bdata)
954 self.bdata = bdata.object.original
955 self._ref = None
956 else: # isinstance(bdata, (Bone, PoseBone)):
957 if isinstance(bdata, PoseBone):
958 bdata = armature.data.bones[bdata.name]
959 self._tag = 'BO'
960 self.name = get_blenderID_name(bdata)
961 self.bdata = bdata
962 self._ref = armature
963 self.parented_to_armature = False
965 def __eq__(self, other):
966 return isinstance(other, self.__class__) and self.key == other.key
968 def __hash__(self):
969 return hash(self.key)
971 def __repr__(self):
972 return self.key
974 # #### Common to all _tag values.
975 def get_fbx_uuid(self):
976 return get_fbx_uuid_from_key(self.key)
977 fbx_uuid = property(get_fbx_uuid)
979 # XXX Not sure how much that’s useful now... :/
980 def get_hide(self):
981 return self.bdata.hide_viewport if self._tag in {'OB', 'DP'} else self.bdata.hide
982 hide = property(get_hide)
984 def get_parent(self):
985 if self._tag == 'OB':
986 if (self.bdata.parent and self.bdata.parent.type == 'ARMATURE' and
987 self.bdata.parent_type == 'BONE' and self.bdata.parent_bone):
988 # Try to parent to a bone.
989 bo_par = self.bdata.parent.pose.bones.get(self.bdata.parent_bone, None)
990 if (bo_par):
991 return ObjectWrapper(bo_par, self.bdata.parent)
992 else: # Fallback to mere object parenting.
993 return ObjectWrapper(self.bdata.parent)
994 else:
995 # Mere object parenting.
996 return ObjectWrapper(self.bdata.parent)
997 elif self._tag == 'DP':
998 return ObjectWrapper(self._ref)
999 else: # self._tag == 'BO'
1000 return ObjectWrapper(self.bdata.parent, self._ref) or ObjectWrapper(self._ref)
1001 parent = property(get_parent)
1003 def get_bdata_pose_bone(self):
1004 if self._tag == 'BO':
1005 return self._ref.pose.bones[self.bdata.name]
1006 return None
1007 bdata_pose_bone = property(get_bdata_pose_bone)
1009 def get_matrix_local(self):
1010 if self._tag == 'OB':
1011 return self.bdata.matrix_local.copy()
1012 elif self._tag == 'DP':
1013 return self._ref.matrix_world.inverted_safe() @ self._dupli_matrix
1014 else: # 'BO', current pose
1015 # PoseBone.matrix is in armature space, bring in back in real local one!
1016 par = self.bdata.parent
1017 par_mat_inv = self._ref.pose.bones[par.name].matrix.inverted_safe() if par else Matrix()
1018 return par_mat_inv @ self._ref.pose.bones[self.bdata.name].matrix
1019 matrix_local = property(get_matrix_local)
1021 def get_matrix_global(self):
1022 if self._tag == 'OB':
1023 return self.bdata.matrix_world.copy()
1024 elif self._tag == 'DP':
1025 return self._dupli_matrix
1026 else: # 'BO', current pose
1027 return self._ref.matrix_world @ self._ref.pose.bones[self.bdata.name].matrix
1028 matrix_global = property(get_matrix_global)
1030 def get_matrix_rest_local(self):
1031 if self._tag == 'BO':
1032 # Bone.matrix_local is in armature space, bring in back in real local one!
1033 par = self.bdata.parent
1034 par_mat_inv = par.matrix_local.inverted_safe() if par else Matrix()
1035 return par_mat_inv @ self.bdata.matrix_local
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 == arm_obj.bdata:
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_subsurf", "use_mesh_edges", "use_tspace", "use_triangles",
1219 "armature_nodetype", "use_armature_deform_only", "add_leaf_bones",
1220 "bone_correction_matrix", "bone_correction_matrix_inv",
1221 "bake_anim", "bake_anim_use_all_bones", "bake_anim_use_nla_strips", "bake_anim_use_all_actions",
1222 "bake_anim_step", "bake_anim_simplify_factor", "bake_anim_force_startend_keying",
1223 "use_metadata", "media_settings", "use_custom_props", "colors_type",
1226 # Helper container gathering some data we need multiple times:
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_subsurf",
1249 "use_custom_props", "use_custom_props_enum_as_string",
1250 "nodal_material_wrap_map", "image_cache",
1251 "ignore_leaf_bones", "force_connect_children", "automatic_bone_orientation", "bone_correction_matrix",
1252 "use_prepost_rot", "colors_type",