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