io_scene_gltf2: quiet error running when context.space is None
[blender-addons.git] / io_scene_fbx / fbx_utils.py
blob97cce3942e614c14ccd7fbd62a7c3d5f2f6b4fa1
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
12 import numpy as np
14 import bpy
15 import bpy_extras
16 from bpy.types import Object, Bone, PoseBone, DepsgraphObjectInstance
17 from mathutils import Vector, Matrix
19 from . import encode_bin, data_types
22 # "Constants"
23 FBX_VERSION = 7400
24 FBX_HEADER_VERSION = 1003
25 FBX_SCENEINFO_VERSION = 100
26 FBX_TEMPLATES_VERSION = 100
28 FBX_MODELS_VERSION = 232
30 FBX_GEOMETRY_VERSION = 124
31 # Revert back normals to 101 (simple 3D values) for now, 102 (4D + weights) seems not well supported by most apps
32 # currently, apart from some AD products.
33 FBX_GEOMETRY_NORMAL_VERSION = 101
34 FBX_GEOMETRY_BINORMAL_VERSION = 101
35 FBX_GEOMETRY_TANGENT_VERSION = 101
36 FBX_GEOMETRY_SMOOTHING_VERSION = 102
37 FBX_GEOMETRY_CREASE_VERSION = 101
38 FBX_GEOMETRY_VCOLOR_VERSION = 101
39 FBX_GEOMETRY_UV_VERSION = 101
40 FBX_GEOMETRY_MATERIAL_VERSION = 101
41 FBX_GEOMETRY_LAYER_VERSION = 100
42 FBX_GEOMETRY_SHAPE_VERSION = 100
43 FBX_DEFORMER_SHAPE_VERSION = 100
44 FBX_DEFORMER_SHAPECHANNEL_VERSION = 100
45 FBX_POSE_BIND_VERSION = 100
46 FBX_DEFORMER_SKIN_VERSION = 101
47 FBX_DEFORMER_CLUSTER_VERSION = 100
48 FBX_MATERIAL_VERSION = 102
49 FBX_TEXTURE_VERSION = 202
50 FBX_ANIM_KEY_VERSION = 4008
52 FBX_NAME_CLASS_SEP = b"\x00\x01"
53 FBX_ANIM_PROPSGROUP_NAME = "d"
55 FBX_KTIME = 46186158000 # This is the number of "ktimes" in one second (yep, precision over the nanosecond...)
58 MAT_CONVERT_LIGHT = Matrix.Rotation(math.pi / 2.0, 4, 'X') # Blender is -Z, FBX is -Y.
59 MAT_CONVERT_CAMERA = Matrix.Rotation(math.pi / 2.0, 4, 'Y') # Blender is -Z, FBX is +X.
60 # XXX I can't get this working :(
61 # MAT_CONVERT_BONE = Matrix.Rotation(math.pi / 2.0, 4, 'Z') # Blender is +Y, FBX is -X.
62 MAT_CONVERT_BONE = Matrix()
65 BLENDER_OTHER_OBJECT_TYPES = {'CURVE', 'SURFACE', 'FONT', 'META'}
66 BLENDER_OBJECT_TYPES_MESHLIKE = {'MESH'} | BLENDER_OTHER_OBJECT_TYPES
69 # Lamps.
70 FBX_LIGHT_TYPES = {
71 'POINT': 0, # Point.
72 'SUN': 1, # Directional.
73 'SPOT': 2, # Spot.
74 'HEMI': 1, # Directional.
75 'AREA': 3, # Area.
77 FBX_LIGHT_DECAY_TYPES = {
78 'CONSTANT': 0, # None.
79 'INVERSE_LINEAR': 1, # Linear.
80 'INVERSE_SQUARE': 2, # Quadratic.
81 'INVERSE_COEFFICIENTS': 2, # Quadratic...
82 'CUSTOM_CURVE': 2, # Quadratic.
83 'LINEAR_QUADRATIC_WEIGHTED': 2, # Quadratic.
87 RIGHT_HAND_AXES = {
88 # Up, Forward -> FBX values (tuples of (axis, sign), Up, Front, Coord).
89 ( 'X', '-Y'): ((0, 1), (1, 1), (2, 1)),
90 ( 'X', 'Y'): ((0, 1), (1, -1), (2, -1)),
91 ( 'X', '-Z'): ((0, 1), (2, 1), (1, -1)),
92 ( 'X', 'Z'): ((0, 1), (2, -1), (1, 1)),
93 ('-X', '-Y'): ((0, -1), (1, 1), (2, -1)),
94 ('-X', 'Y'): ((0, -1), (1, -1), (2, 1)),
95 ('-X', '-Z'): ((0, -1), (2, 1), (1, 1)),
96 ('-X', 'Z'): ((0, -1), (2, -1), (1, -1)),
97 ( 'Y', '-X'): ((1, 1), (0, 1), (2, -1)),
98 ( 'Y', 'X'): ((1, 1), (0, -1), (2, 1)),
99 ( 'Y', '-Z'): ((1, 1), (2, 1), (0, 1)),
100 ( 'Y', 'Z'): ((1, 1), (2, -1), (0, -1)),
101 ('-Y', '-X'): ((1, -1), (0, 1), (2, 1)),
102 ('-Y', 'X'): ((1, -1), (0, -1), (2, -1)),
103 ('-Y', '-Z'): ((1, -1), (2, 1), (0, -1)),
104 ('-Y', 'Z'): ((1, -1), (2, -1), (0, 1)),
105 ( 'Z', '-X'): ((2, 1), (0, 1), (1, 1)),
106 ( 'Z', 'X'): ((2, 1), (0, -1), (1, -1)),
107 ( 'Z', '-Y'): ((2, 1), (1, 1), (0, -1)),
108 ( 'Z', 'Y'): ((2, 1), (1, -1), (0, 1)), # Blender system!
109 ('-Z', '-X'): ((2, -1), (0, 1), (1, -1)),
110 ('-Z', 'X'): ((2, -1), (0, -1), (1, 1)),
111 ('-Z', '-Y'): ((2, -1), (1, 1), (0, 1)),
112 ('-Z', 'Y'): ((2, -1), (1, -1), (0, -1)),
116 # NOTE: Not fully in enum value order, since when exporting the first entry matching the framerate value is used
117 # (e.g. better have NTSC fullframe than NTSC drop frame for 29.97 framerate).
118 FBX_FRAMERATES = (
119 #(-1.0, 0), # Default framerate.
120 (-1.0, 14), # Custom framerate.
121 (120.0, 1),
122 (100.0, 2),
123 (60.0, 3),
124 (50.0, 4),
125 (48.0, 5),
126 (30.0, 6), # BW NTSC, full frame.
127 (30.0, 7), # Drop frame.
128 (30.0 / 1.001, 9), # Color NTSC, full frame.
129 (30.0 / 1.001, 8), # Color NTSC, drop frame.
130 (25.0, 10),
131 (24.0, 11),
132 #(1.0, 12), # 1000 milli/s (use for date time?).
133 (24.0 / 1.001, 13),
134 (96.0, 15),
135 (72.0, 16),
136 (60.0 / 1.001, 17),
137 (120.0 / 1.001, 18),
141 # ##### Misc utilities #####
143 # Enable performance reports (measuring time used to perform various steps of importing or exporting).
144 DO_PERFMON = False
146 if DO_PERFMON:
147 class PerfMon():
148 def __init__(self):
149 self.level = -1
150 self.ref_time = []
152 def level_up(self, message=""):
153 self.level += 1
154 self.ref_time.append(None)
155 if message:
156 print("\t" * self.level, message, sep="")
158 def level_down(self, message=""):
159 if not self.ref_time:
160 if message:
161 print(message)
162 return
163 ref_time = self.ref_time[self.level]
164 print("\t" * self.level,
165 "\tDone (%f sec)\n" % ((time.process_time() - ref_time) if ref_time is not None else 0.0),
166 sep="")
167 if message:
168 print("\t" * self.level, message, sep="")
169 del self.ref_time[self.level]
170 self.level -= 1
172 def step(self, message=""):
173 ref_time = self.ref_time[self.level]
174 curr_time = time.process_time()
175 if ref_time is not None:
176 print("\t" * self.level, "\tDone (%f sec)\n" % (curr_time - ref_time), sep="")
177 self.ref_time[self.level] = curr_time
178 print("\t" * self.level, message, sep="")
179 else:
180 class PerfMon():
181 def __init__(self):
182 pass
184 def level_up(self, message=""):
185 pass
187 def level_down(self, message=""):
188 pass
190 def step(self, message=""):
191 pass
194 # Scale/unit mess. FBX can store the 'reference' unit of a file in its UnitScaleFactor property
195 # (1.0 meaning centimeter, afaik). We use that to reflect user's default unit as set in Blender with scale_length.
196 # However, we always get values in BU (i.e. meters), so we have to reverse-apply that scale in global matrix...
197 # Note that when no default unit is available, we assume 'meters' (and hence scale by 100).
198 def units_blender_to_fbx_factor(scene):
199 return 100.0 if (scene.unit_settings.system == 'NONE') else (100.0 * scene.unit_settings.scale_length)
202 # Note: this could be in a utility (math.units e.g.)...
204 UNITS = {
205 "meter": 1.0, # Ref unit!
206 "kilometer": 0.001,
207 "millimeter": 1000.0,
208 "foot": 1.0 / 0.3048,
209 "inch": 1.0 / 0.0254,
210 "turn": 1.0, # Ref unit!
211 "degree": 360.0,
212 "radian": math.pi * 2.0,
213 "second": 1.0, # Ref unit!
214 "ktime": FBX_KTIME,
218 def units_convertor(u_from, u_to):
219 """Return a convertor between specified units."""
220 conv = UNITS[u_to] / UNITS[u_from]
221 return lambda v: v * conv
224 def units_convertor_iter(u_from, u_to):
225 """Return an iterable convertor between specified units."""
226 conv = units_convertor(u_from, u_to)
228 def convertor(it):
229 for v in it:
230 yield(conv(v))
232 return convertor
235 def matrix4_to_array(mat):
236 """Concatenate matrix's columns into a single, flat tuple"""
237 # blender matrix is row major, fbx is col major so transpose on write
238 return tuple(f for v in mat.transposed() for f in v)
241 def array_to_matrix4(arr):
242 """Convert a single 16-len tuple into a valid 4D Blender matrix"""
243 # Blender matrix is row major, fbx is col major so transpose on read
244 return Matrix(tuple(zip(*[iter(arr)]*4))).transposed()
247 def parray_as_ndarray(arr):
248 """Convert an array.array into an np.ndarray that shares the same memory"""
249 return np.frombuffer(arr, dtype=arr.typecode)
252 def similar_values(v1, v2, e=1e-6):
253 """Return True if v1 and v2 are nearly the same."""
254 if v1 == v2:
255 return True
256 return ((abs(v1 - v2) / max(abs(v1), abs(v2))) <= e)
259 def similar_values_iter(v1, v2, e=1e-6):
260 """Return True if iterables v1 and v2 are nearly the same."""
261 if v1 == v2:
262 return True
263 for v1, v2 in zip(v1, v2):
264 if (v1 != v2) and ((abs(v1 - v2) / max(abs(v1), abs(v2))) > e):
265 return False
266 return True
269 def shape_difference_exclude_similar(sv_cos, ref_cos, e=1e-6):
270 """Return a tuple of:
271 the difference between the vertex cos in sv_cos and ref_cos, excluding any that are nearly the same,
272 and the indices of the vertices that are not nearly the same"""
273 assert(sv_cos.size == ref_cos.size)
275 # Create views of 1 co per row of the arrays, only making copies if needed.
276 sv_cos = sv_cos.reshape(-1, 3)
277 ref_cos = ref_cos.reshape(-1, 3)
279 # Quick check for equality
280 if np.array_equal(sv_cos, ref_cos):
281 # There's no difference between the two arrays.
282 empty_cos = np.empty((0, 3), dtype=sv_cos.dtype)
283 empty_indices = np.empty(0, dtype=np.int32)
284 return empty_cos, empty_indices
286 # Note that unlike math.isclose(a,b), np.isclose(a,b) is not symmetrical and the second argument 'b', is
287 # considered to be the reference value.
288 # Note that atol=0 will mean that if only one co component being compared is zero, they won't be considered close.
289 similar_mask = np.isclose(sv_cos, ref_cos, atol=0, rtol=e)
291 # A co is only similar if every component in it is similar.
292 co_similar_mask = np.all(similar_mask, axis=1)
294 # Get the indices of cos that are not similar.
295 not_similar_verts_idx = np.flatnonzero(~co_similar_mask)
297 # Subtracting first over the entire arrays and then indexing seems faster than indexing both arrays first and then
298 # subtracting, until less than about 3% of the cos are being indexed.
299 difference_cos = (sv_cos - ref_cos)[not_similar_verts_idx]
300 return difference_cos, not_similar_verts_idx
303 def _mat4_vec3_array_multiply(mat4, vec3_array, dtype=None, return_4d=False):
304 """Multiply a 4d matrix by each 3d vector in an array and return as an array of either 3d or 4d vectors.
306 A view of the input array is returned if return_4d=False, the dtype matches the input array and either the matrix is
307 None or, ignoring the last row, is a 3x3 identity matrix with no translation:
308 ┌1, 0, 0, 0┐
309 │0, 1, 0, 0│
310 └0, 0, 1, 0┘
312 When dtype=None, it defaults to the dtype of the input array."""
313 return_dtype = dtype if dtype is not None else vec3_array.dtype
314 vec3_array = vec3_array.reshape(-1, 3)
316 # Multiplying a 4d mathutils.Matrix by a 3d mathutils.Vector implicitly extends the Vector to 4d during the
317 # calculation by appending 1.0 to the Vector and then the 4d result is truncated back to 3d.
318 # Numpy does not do an implicit extension to 4d, so it would have to be done explicitly by extending the entire
319 # vec3_array to 4d.
320 # However, since the w component of the vectors is always 1.0, the last column can be excluded from the
321 # multiplication and then added to every multiplied vector afterwards, which avoids having to make a 4d copy of
322 # vec3_array beforehand.
323 # For a single column vector:
324 # ┌a, b, c, d┐ ┌x┐ ┌ax+by+cz+d┐
325 # │e, f, g, h│ @ │y│ = │ex+fy+gz+h│
326 # │i, j, k, l│ │z│ │ix+jy+kz+l│
327 # └m, n, o, p┘ └1┘ └mx+ny+oz+p┘
328 # ┌a, b, c┐ ┌x┐ ┌d┐ ┌ax+by+cz┐ ┌d┐ ┌ax+by+cz+d┐
329 # │e, f, g│ @ │y│ + │h│ = │ex+fy+gz│ + │h│ = │ex+fy+gz+h│
330 # │i, j, k│ └z┘ │l│ │ix+jy+kz│ │l│ │ix+jy+kz+l│
331 # └m, n, o┘ └p┘ └mx+ny+oz┘ └p┘ └mx+ny+oz+p┘
333 # column_vector_multiplication in mathutils_Vector.c uses double precision math for Matrix @ Vector by casting the
334 # matrix's values to double precision and then casts back to single precision when returning the result, so at least
335 # double precision math is always be used to match standard Blender behaviour.
336 math_precision = np.result_type(np.double, vec3_array)
338 to_multiply = None
339 to_add = None
340 w_to_set = 1.0
341 if mat4 is not None:
342 mat_np = np.array(mat4, dtype=math_precision)
343 # Identity matrix is compared against to check if any matrix multiplication is required.
344 identity = np.identity(4, dtype=math_precision)
345 if not return_4d:
346 # If returning 3d, the entire last row of the matrix can be ignored because it only affects the w component.
347 mat_np = mat_np[:3]
348 identity = identity[:3]
350 # Split mat_np into the columns to multiply and the column to add afterwards.
351 # First 3 columns
352 multiply_columns = mat_np[:, :3]
353 multiply_identity = identity[:, :3]
354 # Last column only
355 add_column = mat_np.T[3]
357 # Analyze the split parts of the matrix to figure out if there is anything to multiply and anything to add.
358 if not np.array_equal(multiply_columns, multiply_identity):
359 to_multiply = multiply_columns
361 if return_4d and to_multiply is None:
362 # When there's nothing to multiply, the w component of add_column can be set directly into the array because
363 # mx+ny+oz+p becomes 0x+0y+0z+p where p is add_column[3].
364 w_to_set = add_column[3]
365 # Replace add_column with a view of only the translation.
366 add_column = add_column[:3]
368 if add_column.any():
369 to_add = add_column
371 if to_multiply is None:
372 # If there's anything to add, ensure it's added using the precision being used for math.
373 array_dtype = math_precision if to_add is not None else return_dtype
374 if return_4d:
375 multiplied_vectors = np.empty((len(vec3_array), 4), dtype=array_dtype)
376 multiplied_vectors[:, :3] = vec3_array
377 multiplied_vectors[:, 3] = w_to_set
378 else:
379 # If there's anything to add, ensure a copy is made so that the input vec3_array isn't modified.
380 multiplied_vectors = vec3_array.astype(array_dtype, copy=to_add is not None)
381 else:
382 # Matrix multiplication has the signature (n,k) @ (k,m) -> (n,m).
383 # Where v is the number of vectors in vec3_array and d is the number of vector dimensions to return:
384 # to_multiply has shape (d,3), vec3_array has shape (v,3) and the result should have shape (v,d).
385 # Either vec3_array or to_multiply must be transposed:
386 # Can transpose vec3_array and then transpose the result:
387 # (v,3).T -> (3,v); (d,3) @ (3,v) -> (d,v); (d,v).T -> (v,d)
388 # Or transpose to_multiply and swap the order of multiplication:
389 # (d,3).T -> (3,d); (v,3) @ (3,d) -> (v,d)
390 # There's no, or negligible, performance difference between the two options, however, the result of the latter
391 # will be C contiguous in memory, making it faster to convert to flattened bytes with .tobytes().
392 multiplied_vectors = vec3_array @ to_multiply.T
394 if to_add is not None:
395 for axis, to_add_to_axis in zip(multiplied_vectors.T, to_add):
396 if to_add_to_axis != 0:
397 axis += to_add_to_axis
399 # Cast to the desired return type before returning.
400 return multiplied_vectors.astype(return_dtype, copy=False)
403 def vcos_transformed(raw_cos, m=None, dtype=None):
404 return _mat4_vec3_array_multiply(m, raw_cos, dtype)
407 def nors_transformed(raw_nors, m=None, dtype=None):
408 # Great, now normals are also expected 4D!
409 # XXX Back to 3D normals for now!
410 # return _mat4_vec3_array_multiply(m, raw_nors, dtype, return_4d=True)
411 return _mat4_vec3_array_multiply(m, raw_nors, dtype)
414 def astype_view_signedness(arr, new_dtype):
415 """Unsafely views arr as new_dtype if the itemsize and byteorder of arr matches but the signedness does not,
416 otherwise calls np.ndarray.astype with copy=False.
418 The benefit of copy=False is that if the array can be safely viewed as the new type, then a view is made, instead of
419 a copy with the new type.
421 Unsigned types can't be viewed safely as signed or vice-versa, meaning that a copy would always be made by
422 .astype(..., copy=False).
424 This is intended for viewing uintc data (a common Blender C type with variable itemsize, though usually 4 bytes, so
425 uint32) as int32 (a common FBX type), when the itemsizes match."""
426 arr_dtype = arr.dtype
428 if not isinstance(new_dtype, np.dtype):
429 # new_dtype could be a type instance or a string, but it needs to be a dtype to compare its itemsize, byteorder
430 # and kind.
431 new_dtype = np.dtype(new_dtype)
433 # For simplicity, only dtypes of the same itemsize and byteorder, but opposite signedness, are handled. Everything
434 # else is left to .astype.
435 arr_kind = arr_dtype.kind
436 new_kind = new_dtype.kind
437 if (
438 # Signed and unsigned int are opposite in terms of signedness. Other types don't have signedness.
439 ((arr_kind == 'i' and new_kind == 'u') or (arr_kind == 'u' and new_kind == 'i'))
440 and arr_dtype.itemsize == new_dtype.itemsize
441 and arr_dtype.byteorder == new_dtype.byteorder
443 # new_dtype has opposite signedness and matching itemsize and byteorder, so return a view of the new type.
444 return arr.view(new_dtype)
445 else:
446 return arr.astype(new_dtype, copy=False)
449 def fast_first_axis_flat(ar):
450 """Get a flat view (or a copy if a view is not possible) of the input array whereby each element is a single element
451 of a dtype that is fast to sort, sorts according to individual bytes and contains the data for an entire row (and
452 any further dimensions) of the input array.
454 Since the dtype of the view could sort in a different order to the dtype of the input array, this isn't typically
455 useful for actual sorting, but it is useful for sorting-based uniqueness, such as np.unique."""
456 # If there are no rows, each element will be viewed as the new dtype.
457 elements_per_row = math.prod(ar.shape[1:])
458 row_itemsize = ar.itemsize * elements_per_row
460 # Get a dtype with itemsize that equals row_itemsize.
461 # Integer types sort the fastest, but are only available for specific itemsizes.
462 uint_dtypes_by_itemsize = {1: np.uint8, 2: np.uint16, 4: np.uint32, 8: np.uint64}
463 # Signed/unsigned makes no noticeable speed difference, but using unsigned will result in ordering according to
464 # individual bytes like the other, non-integer types.
465 if row_itemsize in uint_dtypes_by_itemsize:
466 entire_row_dtype = uint_dtypes_by_itemsize[row_itemsize]
467 else:
468 # When using kind='stable' sorting, numpy only uses radix sort with integer types, but it's still
469 # significantly faster to sort by a single item per row instead of multiple row elements or multiple structured
470 # type fields.
471 # Construct a flexible size dtype with matching itemsize.
472 # Should always be 4 because each character in a unicode string is UCS4.
473 str_itemsize = np.dtype((np.str_, 1)).itemsize
474 if row_itemsize % str_itemsize == 0:
475 # Unicode strings seem to be slightly faster to sort than bytes.
476 entire_row_dtype = np.dtype((np.str_, row_itemsize // str_itemsize))
477 else:
478 # Bytes seem to be slightly faster to sort than raw bytes (np.void).
479 entire_row_dtype = np.dtype((np.bytes_, row_itemsize))
481 # View each element along the first axis as a single element.
482 # View (or copy if a view is not possible) as flat
483 ar = ar.reshape(-1)
484 # To view as a dtype of different size, the last axis (entire array in NumPy 1.22 and earlier) must be C-contiguous.
485 if row_itemsize != ar.itemsize and not ar.flags.c_contiguous:
486 ar = np.ascontiguousarray(ar)
487 return ar.view(entire_row_dtype)
490 def fast_first_axis_unique(ar, return_unique=True, return_index=False, return_inverse=False, return_counts=False):
491 """np.unique with axis=0 but optimised for when the input array has multiple elements per row, and the returned
492 unique array doesn't need to be sorted.
494 Arrays with more than one element per row are more costly to sort in np.unique due to being compared one
495 row-element at a time, like comparing tuples.
497 By viewing each entire row as a single non-structured element, much faster sorting can be achieved. Since the values
498 are viewed as a different type to their original, this means that the returned array of unique values may not be
499 sorted according to their original type.
501 The array of unique values can be excluded from the returned tuple by specifying return_unique=False.
503 Float type caveats:
504 All elements of -0.0 in the input array will be replaced with 0.0 to ensure that both values are collapsed into one.
505 NaN values can have lots of different byte representations (e.g. signalling/quiet and custom payloads). Only the
506 duplicates of each unique byte representation will be collapsed into one."""
507 # At least something should always be returned.
508 assert(return_unique or return_index or return_inverse or return_counts)
509 # Only signed integer, unsigned integer and floating-point kinds of data are allowed. Other kinds of data have not
510 # been tested.
511 assert(ar.dtype.kind in "iuf")
513 # Floating-point types have different byte representations for -0.0 and 0.0. Collapse them together by replacing all
514 # -0.0 in the input array with 0.0.
515 if ar.dtype.kind == 'f':
516 ar[ar == -0.0] = 0.0
518 # It's a bit annoying that the unique array is always calculated even when it might not be needed, but it is
519 # generally insignificant compared to the cost of sorting.
520 result = np.unique(fast_first_axis_flat(ar), return_index=return_index,
521 return_inverse=return_inverse, return_counts=return_counts)
523 if return_unique:
524 unique = result[0] if isinstance(result, tuple) else result
525 # View in the original dtype.
526 unique = unique.view(ar.dtype)
527 # Return the same number of elements per row and any extra dimensions per row as the input array.
528 unique.shape = (-1, *ar.shape[1:])
529 if isinstance(result, tuple):
530 return (unique,) + result[1:]
531 else:
532 return unique
533 else:
534 # Remove the first element, the unique array.
535 result = result[1:]
536 if len(result) == 1:
537 # Unpack single element tuples.
538 return result[0]
539 else:
540 return result
543 # ##### UIDs code. #####
545 # ID class (mere int).
546 class UUID(int):
547 pass
550 # UIDs storage.
551 _keys_to_uuids = {}
552 _uuids_to_keys = {}
555 def _key_to_uuid(uuids, key):
556 # TODO: Check this is robust enough for our needs!
557 # Note: We assume we have already checked the related key wasn't yet in _keys_to_uids!
558 # As int64 is signed in FBX, we keep uids below 2**63...
559 if isinstance(key, int) and 0 <= key < 2**63:
560 # We can use value directly as id!
561 uuid = key
562 else:
563 uuid = hash(key)
564 if uuid < 0:
565 uuid = -uuid
566 if uuid >= 2**63:
567 uuid //= 2
568 # Try to make our uid shorter!
569 if uuid > int(1e9):
570 t_uuid = uuid % int(1e9)
571 if t_uuid not in uuids:
572 uuid = t_uuid
573 # Make sure our uuid *is* unique.
574 if uuid in uuids:
575 inc = 1 if uuid < 2**62 else -1
576 while uuid in uuids:
577 uuid += inc
578 if 0 > uuid >= 2**63:
579 # Note that this is more that unlikely, but does not harm anyway...
580 raise ValueError("Unable to generate an UUID for key {}".format(key))
581 return UUID(uuid)
584 def get_fbx_uuid_from_key(key):
586 Return an UUID for given key, which is assumed to be hashable.
588 uuid = _keys_to_uuids.get(key, None)
589 if uuid is None:
590 uuid = _key_to_uuid(_uuids_to_keys, key)
591 _keys_to_uuids[key] = uuid
592 _uuids_to_keys[uuid] = key
593 return uuid
596 # XXX Not sure we'll actually need this one?
597 def get_key_from_fbx_uuid(uuid):
599 Return the key which generated this uid.
601 assert(uuid.__class__ == UUID)
602 return _uuids_to_keys.get(uuid, None)
605 # Blender-specific key generators
606 def get_bid_name(bid):
607 library = getattr(bid, "library", None)
608 if library is not None:
609 return "%s_L_%s" % (bid.name, library.name)
610 else:
611 return bid.name
614 def get_blenderID_key(bid):
615 if isinstance(bid, Iterable):
616 return "|".join("B" + e.rna_type.name + "#" + get_bid_name(e) for e in bid)
617 else:
618 return "B" + bid.rna_type.name + "#" + get_bid_name(bid)
621 def get_blenderID_name(bid):
622 if isinstance(bid, Iterable):
623 return "|".join(get_bid_name(e) for e in bid)
624 else:
625 return get_bid_name(bid)
628 def get_blender_empty_key(obj):
629 """Return bone's keys (Model and NodeAttribute)."""
630 return "|".join((get_blenderID_key(obj), "Empty"))
633 def get_blender_mesh_shape_key(me):
634 """Return main shape deformer's key."""
635 return "|".join((get_blenderID_key(me), "Shape"))
638 def get_blender_mesh_shape_channel_key(me, shape):
639 """Return shape channel and geometry shape keys."""
640 return ("|".join((get_blenderID_key(me), "Shape", get_blenderID_key(shape))),
641 "|".join((get_blenderID_key(me), "Geometry", get_blenderID_key(shape))))
644 def get_blender_bone_key(armature, bone):
645 """Return bone's keys (Model and NodeAttribute)."""
646 return "|".join((get_blenderID_key((armature, bone)), "Data"))
649 def get_blender_bindpose_key(obj, mesh):
650 """Return object's bindpose key."""
651 return "|".join((get_blenderID_key(obj), get_blenderID_key(mesh), "BindPose"))
654 def get_blender_armature_skin_key(armature, mesh):
655 """Return armature's skin key."""
656 return "|".join((get_blenderID_key(armature), get_blenderID_key(mesh), "DeformerSkin"))
659 def get_blender_bone_cluster_key(armature, mesh, bone):
660 """Return bone's cluster key."""
661 return "|".join((get_blenderID_key(armature), get_blenderID_key(mesh),
662 get_blenderID_key(bone), "SubDeformerCluster"))
665 def get_blender_anim_id_base(scene, ref_id):
666 if ref_id is not None:
667 return get_blenderID_key(scene) + "|" + get_blenderID_key(ref_id)
668 else:
669 return get_blenderID_key(scene)
672 def get_blender_anim_stack_key(scene, ref_id):
673 """Return single anim stack key."""
674 return get_blender_anim_id_base(scene, ref_id) + "|AnimStack"
677 def get_blender_anim_layer_key(scene, ref_id):
678 """Return ID's anim layer key."""
679 return get_blender_anim_id_base(scene, ref_id) + "|AnimLayer"
682 def get_blender_anim_curve_node_key(scene, ref_id, obj_key, fbx_prop_name):
683 """Return (stack/layer, ID, fbxprop) curve node key."""
684 return "|".join((get_blender_anim_id_base(scene, ref_id), obj_key, fbx_prop_name, "AnimCurveNode"))
687 def get_blender_anim_curve_key(scene, ref_id, obj_key, fbx_prop_name, fbx_prop_item_name):
688 """Return (stack/layer, ID, fbxprop, item) curve key."""
689 return "|".join((get_blender_anim_id_base(scene, ref_id), obj_key, fbx_prop_name,
690 fbx_prop_item_name, "AnimCurve"))
693 def get_blender_nodetexture_key(ma, socket_names):
694 return "|".join((get_blenderID_key(ma), *socket_names))
697 # ##### Element generators. #####
699 # Note: elem may be None, in this case the element is not added to any parent.
700 def elem_empty(elem, name):
701 sub_elem = encode_bin.FBXElem(name)
702 if elem is not None:
703 elem.elems.append(sub_elem)
704 return sub_elem
707 def _elem_data_single(elem, name, value, func_name):
708 sub_elem = elem_empty(elem, name)
709 getattr(sub_elem, func_name)(value)
710 return sub_elem
713 def _elem_data_vec(elem, name, value, func_name):
714 sub_elem = elem_empty(elem, name)
715 func = getattr(sub_elem, func_name)
716 for v in value:
717 func(v)
718 return sub_elem
721 def elem_data_single_bool(elem, name, value):
722 return _elem_data_single(elem, name, value, "add_bool")
725 def elem_data_single_int16(elem, name, value):
726 return _elem_data_single(elem, name, value, "add_int16")
729 def elem_data_single_int32(elem, name, value):
730 return _elem_data_single(elem, name, value, "add_int32")
733 def elem_data_single_int64(elem, name, value):
734 return _elem_data_single(elem, name, value, "add_int64")
737 def elem_data_single_float32(elem, name, value):
738 return _elem_data_single(elem, name, value, "add_float32")
741 def elem_data_single_float64(elem, name, value):
742 return _elem_data_single(elem, name, value, "add_float64")
745 def elem_data_single_bytes(elem, name, value):
746 return _elem_data_single(elem, name, value, "add_bytes")
749 def elem_data_single_string(elem, name, value):
750 return _elem_data_single(elem, name, value, "add_string")
753 def elem_data_single_string_unicode(elem, name, value):
754 return _elem_data_single(elem, name, value, "add_string_unicode")
757 def elem_data_single_bool_array(elem, name, value):
758 return _elem_data_single(elem, name, value, "add_bool_array")
761 def elem_data_single_int32_array(elem, name, value):
762 return _elem_data_single(elem, name, value, "add_int32_array")
765 def elem_data_single_int64_array(elem, name, value):
766 return _elem_data_single(elem, name, value, "add_int64_array")
769 def elem_data_single_float32_array(elem, name, value):
770 return _elem_data_single(elem, name, value, "add_float32_array")
773 def elem_data_single_float64_array(elem, name, value):
774 return _elem_data_single(elem, name, value, "add_float64_array")
777 def elem_data_single_byte_array(elem, name, value):
778 return _elem_data_single(elem, name, value, "add_byte_array")
781 def elem_data_vec_float64(elem, name, value):
782 return _elem_data_vec(elem, name, value, "add_float64")
785 # ##### Generators for standard FBXProperties70 properties. #####
787 def elem_properties(elem):
788 return elem_empty(elem, b"Properties70")
791 # Properties definitions, format: (b"type_1", b"label(???)", "name_set_value_1", "name_set_value_2", ...)
792 # XXX Looks like there can be various variations of formats here... Will have to be checked ultimately!
793 # Also, those "custom" types like 'FieldOfView' or 'Lcl Translation' are pure nonsense,
794 # these are just Vector3D ultimately... *sigh* (again).
795 FBX_PROPERTIES_DEFINITIONS = {
796 # Generic types.
797 "p_bool": (b"bool", b"", "add_int32"), # Yes, int32 for a bool (and they do have a core bool type)!!!
798 "p_integer": (b"int", b"Integer", "add_int32"),
799 "p_ulonglong": (b"ULongLong", b"", "add_int64"),
800 "p_double": (b"double", b"Number", "add_float64"), # Non-animatable?
801 "p_number": (b"Number", b"", "add_float64"), # Animatable-only?
802 "p_enum": (b"enum", b"", "add_int32"),
803 "p_vector_3d": (b"Vector3D", b"Vector", "add_float64", "add_float64", "add_float64"), # Non-animatable?
804 "p_vector": (b"Vector", b"", "add_float64", "add_float64", "add_float64"), # Animatable-only?
805 "p_color_rgb": (b"ColorRGB", b"Color", "add_float64", "add_float64", "add_float64"), # Non-animatable?
806 "p_color": (b"Color", b"", "add_float64", "add_float64", "add_float64"), # Animatable-only?
807 "p_string": (b"KString", b"", "add_string_unicode"),
808 "p_string_url": (b"KString", b"Url", "add_string_unicode"),
809 "p_timestamp": (b"KTime", b"Time", "add_int64"),
810 "p_datetime": (b"DateTime", b"", "add_string_unicode"),
811 # Special types.
812 "p_object": (b"object", b""), # XXX Check this! No value for this prop??? Would really like to know how it works!
813 "p_compound": (b"Compound", b""),
814 # Specific types (sic).
815 # ## Objects (Models).
816 "p_lcl_translation": (b"Lcl Translation", b"", "add_float64", "add_float64", "add_float64"),
817 "p_lcl_rotation": (b"Lcl Rotation", b"", "add_float64", "add_float64", "add_float64"),
818 "p_lcl_scaling": (b"Lcl Scaling", b"", "add_float64", "add_float64", "add_float64"),
819 "p_visibility": (b"Visibility", b"", "add_float64"),
820 "p_visibility_inheritance": (b"Visibility Inheritance", b"", "add_int32"),
821 # ## Cameras!!!
822 "p_roll": (b"Roll", b"", "add_float64"),
823 "p_opticalcenterx": (b"OpticalCenterX", b"", "add_float64"),
824 "p_opticalcentery": (b"OpticalCenterY", b"", "add_float64"),
825 "p_fov": (b"FieldOfView", b"", "add_float64"),
826 "p_fov_x": (b"FieldOfViewX", b"", "add_float64"),
827 "p_fov_y": (b"FieldOfViewY", b"", "add_float64"),
831 def _elem_props_set(elem, ptype, name, value, flags):
832 p = elem_data_single_string(elem, b"P", name)
833 for t in ptype[:2]:
834 p.add_string(t)
835 p.add_string(flags)
836 if len(ptype) == 3:
837 getattr(p, ptype[2])(value)
838 elif len(ptype) > 3:
839 # We assume value is iterable, else it's a bug!
840 for callback, val in zip(ptype[2:], value):
841 getattr(p, callback)(val)
844 def _elem_props_flags(animatable, animated, custom):
845 # XXX: There are way more flags, see
846 # http://help.autodesk.com/view/FBX/2015/ENU/?guid=__cpp_ref_class_fbx_property_flags_html
847 # Unfortunately, as usual, no doc at all about their 'translation' in actual FBX file format.
848 # Curse you-know-who.
849 if animatable:
850 if animated:
851 if custom:
852 return b"A+U"
853 return b"A+"
854 if custom:
855 # Seems that customprops always need those 'flags', see T69554. Go figure...
856 return b"A+U"
857 return b"A"
858 if custom:
859 # Seems that customprops always need those 'flags', see T69554. Go figure...
860 return b"A+U"
861 return b""
864 def elem_props_set(elem, ptype, name, value=None, animatable=False, animated=False, custom=False):
865 ptype = FBX_PROPERTIES_DEFINITIONS[ptype]
866 _elem_props_set(elem, ptype, name, value, _elem_props_flags(animatable, animated, custom))
869 def elem_props_compound(elem, cmpd_name, custom=False):
870 def _setter(ptype, name, value, animatable=False, animated=False, custom=False):
871 name = cmpd_name + b"|" + name
872 elem_props_set(elem, ptype, name, value, animatable=animatable, animated=animated, custom=custom)
874 elem_props_set(elem, "p_compound", cmpd_name, custom=custom)
875 return _setter
878 def elem_props_template_init(templates, template_type):
880 Init a writing template of given type, for *one* element's properties.
882 ret = {}
883 tmpl = templates.get(template_type)
884 if tmpl is not None:
885 written = tmpl.written[0]
886 props = tmpl.properties
887 ret = {name: [val, ptype, anim, written] for name, (val, ptype, anim) in props.items()}
888 return ret
891 def elem_props_template_set(template, elem, ptype_name, name, value, animatable=False, animated=False):
893 Only add a prop if the same value is not already defined in given template.
894 Note it is important to not give iterators as value, here!
896 ptype = FBX_PROPERTIES_DEFINITIONS[ptype_name]
897 if len(ptype) > 3:
898 value = tuple(value)
899 tmpl_val, tmpl_ptype, tmpl_animatable, tmpl_written = template.get(name, (None, None, False, False))
900 # Note animatable flag from template takes precedence over given one, if applicable.
901 # However, animated properties are always written, since they cannot match their template!
902 if tmpl_ptype is not None and not animated:
903 if (tmpl_written and
904 ((len(ptype) == 3 and (tmpl_val, tmpl_ptype) == (value, ptype_name)) or
905 (len(ptype) > 3 and (tuple(tmpl_val), tmpl_ptype) == (value, ptype_name)))):
906 return # Already in template and same value.
907 _elem_props_set(elem, ptype, name, value, _elem_props_flags(tmpl_animatable, animated, False))
908 template[name][3] = True
909 else:
910 _elem_props_set(elem, ptype, name, value, _elem_props_flags(animatable, animated, False))
913 def elem_props_template_finalize(template, elem):
915 Finalize one element's template/props.
916 Issue is, some templates might be "needed" by different types (e.g. NodeAttribute is for lights, cameras, etc.),
917 but values for only *one* subtype can be written as template. So we have to be sure we write those for the other
918 subtypes in each and every elements, if they are not overridden by that element.
919 Yes, hairy, FBX that is to say. When they could easily support several subtypes per template... :(
921 for name, (value, ptype_name, animatable, written) in template.items():
922 if written:
923 continue
924 ptype = FBX_PROPERTIES_DEFINITIONS[ptype_name]
925 _elem_props_set(elem, ptype, name, value, _elem_props_flags(animatable, False, False))
928 # ##### Templates #####
929 # TODO: check all those "default" values, they should match Blender's default as much as possible, I guess?
931 FBXTemplate = namedtuple("FBXTemplate", ("type_name", "prop_type_name", "properties", "nbr_users", "written"))
934 def fbx_templates_generate(root, fbx_templates):
935 # We may have to gather different templates in the same node (e.g. NodeAttribute template gathers properties
936 # for Lights, Cameras, LibNodes, etc.).
937 ref_templates = {(tmpl.type_name, tmpl.prop_type_name): tmpl for tmpl in fbx_templates.values()}
939 templates = {}
940 for type_name, prop_type_name, properties, nbr_users, _written in fbx_templates.values():
941 tmpl = templates.setdefault(type_name, [{}, 0])
942 tmpl[0][prop_type_name] = (properties, nbr_users)
943 tmpl[1] += nbr_users
945 for type_name, (subprops, nbr_users) in templates.items():
946 template = elem_data_single_string(root, b"ObjectType", type_name)
947 elem_data_single_int32(template, b"Count", nbr_users)
949 if len(subprops) == 1:
950 prop_type_name, (properties, _nbr_sub_type_users) = next(iter(subprops.items()))
951 subprops = (prop_type_name, properties)
952 ref_templates[(type_name, prop_type_name)].written[0] = True
953 else:
954 # Ack! Even though this could/should work, looks like it is not supported. So we have to chose one. :|
955 max_users = max_props = -1
956 written_prop_type_name = None
957 for prop_type_name, (properties, nbr_sub_type_users) in subprops.items():
958 if nbr_sub_type_users > max_users or (nbr_sub_type_users == max_users and len(properties) > max_props):
959 max_users = nbr_sub_type_users
960 max_props = len(properties)
961 written_prop_type_name = prop_type_name
962 subprops = (written_prop_type_name, properties)
963 ref_templates[(type_name, written_prop_type_name)].written[0] = True
965 prop_type_name, properties = subprops
966 if prop_type_name and properties:
967 elem = elem_data_single_string(template, b"PropertyTemplate", prop_type_name)
968 props = elem_properties(elem)
969 for name, (value, ptype, animatable) in properties.items():
970 try:
971 elem_props_set(props, ptype, name, value, animatable=animatable)
972 except Exception as e:
973 print("Failed to write template prop (%r)" % e)
974 print(props, ptype, name, value, animatable)
977 # ##### FBX animation helpers. #####
980 class AnimationCurveNodeWrapper:
982 This class provides a same common interface for all (FBX-wise) AnimationCurveNode and AnimationCurve elements,
983 and easy API to handle those.
985 __slots__ = (
986 'elem_keys', '_keys', 'default_values', 'fbx_group', 'fbx_gname', 'fbx_props',
987 'force_keying', 'force_startend_keying')
989 kinds = {
990 'LCL_TRANSLATION': ("Lcl Translation", "T", ("X", "Y", "Z")),
991 'LCL_ROTATION': ("Lcl Rotation", "R", ("X", "Y", "Z")),
992 'LCL_SCALING': ("Lcl Scaling", "S", ("X", "Y", "Z")),
993 'SHAPE_KEY': ("DeformPercent", "DeformPercent", ("DeformPercent",)),
994 'CAMERA_FOCAL': ("FocalLength", "FocalLength", ("FocalLength",)),
995 'CAMERA_FOCUS_DISTANCE': ("FocusDistance", "FocusDistance", ("FocusDistance",)),
998 def __init__(self, elem_key, kind, force_keying, force_startend_keying, default_values=...):
999 self.elem_keys = [elem_key]
1000 assert(kind in self.kinds)
1001 self.fbx_group = [self.kinds[kind][0]]
1002 self.fbx_gname = [self.kinds[kind][1]]
1003 self.fbx_props = [self.kinds[kind][2]]
1004 self.force_keying = force_keying
1005 self.force_startend_keying = force_startend_keying
1006 self._keys = [] # (frame, values, write_flags)
1007 if default_values is not ...:
1008 assert(len(default_values) == len(self.fbx_props[0]))
1009 self.default_values = default_values
1010 else:
1011 self.default_values = (0.0) * len(self.fbx_props[0])
1013 def __bool__(self):
1014 # We are 'True' if we do have some validated keyframes...
1015 return bool(self._keys) and (True in ((True in k[2]) for k in self._keys))
1017 def add_group(self, elem_key, fbx_group, fbx_gname, fbx_props):
1019 Add another whole group stuff (curvenode, animated item/prop + curvnode/curve identifiers).
1020 E.g. Shapes animations is written twice, houra!
1022 assert(len(fbx_props) == len(self.fbx_props[0]))
1023 self.elem_keys.append(elem_key)
1024 self.fbx_group.append(fbx_group)
1025 self.fbx_gname.append(fbx_gname)
1026 self.fbx_props.append(fbx_props)
1028 def add_keyframe(self, frame, values):
1030 Add a new keyframe to all curves of the group.
1032 assert(len(values) == len(self.fbx_props[0]))
1033 self._keys.append((frame, values, [True] * len(values))) # write everything by default.
1035 def simplify(self, fac, step, force_keep=False):
1037 Simplifies sampled curves by only enabling samples when:
1038 * their values relatively differ from the previous sample ones.
1040 if not self._keys:
1041 return
1043 if fac == 0.0:
1044 return
1046 # So that, with default factor and step values (1), we get:
1047 min_reldiff_fac = fac * 1.0e-3 # min relative value evolution: 0.1% of current 'order of magnitude'.
1048 min_absdiff_fac = 0.1 # A tenth of reldiff...
1049 keys = self._keys
1051 p_currframe, p_key, p_key_write = keys[0]
1052 p_keyed = list(p_key)
1053 are_keyed = [False] * len(p_key)
1054 for currframe, key, key_write in keys:
1055 for idx, (val, p_val) in enumerate(zip(key, p_key)):
1056 key_write[idx] = False
1057 p_keyedval = p_keyed[idx]
1058 if val == p_val:
1059 # Never write keyframe when value is exactly the same as prev one!
1060 continue
1061 # This is contracted form of relative + absolute-near-zero difference:
1062 # absdiff = abs(a - b)
1063 # if absdiff < min_reldiff_fac * min_absdiff_fac:
1064 # return False
1065 # return (absdiff / ((abs(a) + abs(b)) / 2)) > min_reldiff_fac
1066 # Note that we ignore the '/ 2' part here, since it's not much significant for us.
1067 if abs(val - p_val) > (min_reldiff_fac * max(abs(val) + abs(p_val), min_absdiff_fac)):
1068 # If enough difference from previous sampled value, key this value *and* the previous one!
1069 key_write[idx] = True
1070 p_key_write[idx] = True
1071 p_keyed[idx] = val
1072 are_keyed[idx] = True
1073 elif abs(val - p_keyedval) > (min_reldiff_fac * max((abs(val) + abs(p_keyedval)), min_absdiff_fac)):
1074 # Else, if enough difference from previous keyed value, key this value only!
1075 key_write[idx] = True
1076 p_keyed[idx] = val
1077 are_keyed[idx] = True
1078 p_currframe, p_key, p_key_write = currframe, key, key_write
1080 # If we write nothing (action doing nothing) and are in 'force_keep' mode, we key everything! :P
1081 # See T41766.
1082 # Also, it seems some importers (e.g. UE4) do not handle correctly armatures where some bones
1083 # are not animated, but are children of animated ones, so added an option to systematically force writing
1084 # one key in this case.
1085 # See T41719, T41605, T41254...
1086 if self.force_keying or (force_keep and not self):
1087 are_keyed[:] = [True] * len(are_keyed)
1089 # If we did key something, ensure first and last sampled values are keyed as well.
1090 if self.force_startend_keying:
1091 for idx, is_keyed in enumerate(are_keyed):
1092 if is_keyed:
1093 keys[0][2][idx] = keys[-1][2][idx] = True
1095 def get_final_data(self, scene, ref_id, force_keep=False):
1097 Yield final anim data for this 'curvenode' (for all curvenodes defined).
1098 force_keep is to force to keep a curve even if it only has one valid keyframe.
1100 curves = [[] for k in self._keys[0][1]]
1101 for currframe, key, key_write in self._keys:
1102 for curve, val, wrt in zip(curves, key, key_write):
1103 if wrt:
1104 curve.append((currframe, val))
1106 force_keep = force_keep or self.force_keying
1107 for elem_key, fbx_group, fbx_gname, fbx_props in \
1108 zip(self.elem_keys, self.fbx_group, self.fbx_gname, self.fbx_props):
1109 group_key = get_blender_anim_curve_node_key(scene, ref_id, elem_key, fbx_group)
1110 group = {}
1111 for c, def_val, fbx_item in zip(curves, self.default_values, fbx_props):
1112 fbx_item = FBX_ANIM_PROPSGROUP_NAME + "|" + fbx_item
1113 curve_key = get_blender_anim_curve_key(scene, ref_id, elem_key, fbx_group, fbx_item)
1114 # (curve key, default value, keyframes, write flag).
1115 group[fbx_item] = (curve_key, def_val, c,
1116 True if (len(c) > 1 or (len(c) > 0 and force_keep)) else False)
1117 yield elem_key, group_key, group, fbx_group, fbx_gname
1120 # ##### FBX objects generators. #####
1122 # FBX Model-like data (i.e. Blender objects, depsgraph instances and bones) are wrapped in ObjectWrapper.
1123 # This allows us to have a (nearly) same code FBX-wise for all those types.
1124 # The wrapper tries to stay as small as possible, by mostly using callbacks (property(get...))
1125 # to actual Blender data it contains.
1126 # Note it caches its instances, so that you may call several times ObjectWrapper(your_object)
1127 # with a minimal cost (just re-computing the key).
1129 class MetaObjectWrapper(type):
1130 def __call__(cls, bdata, armature=None):
1131 if bdata is None:
1132 return None
1133 dup_mat = None
1134 if isinstance(bdata, Object):
1135 key = get_blenderID_key(bdata)
1136 elif isinstance(bdata, DepsgraphObjectInstance):
1137 if bdata.is_instance:
1138 key = "|".join((get_blenderID_key((bdata.parent.original, bdata.instance_object.original)),
1139 cls._get_dup_num_id(bdata)))
1140 dup_mat = bdata.matrix_world.copy()
1141 else:
1142 key = get_blenderID_key(bdata.object.original)
1143 else: # isinstance(bdata, (Bone, PoseBone)):
1144 if isinstance(bdata, PoseBone):
1145 bdata = armature.data.bones[bdata.name]
1146 key = get_blenderID_key((armature, bdata))
1148 cache = getattr(cls, "_cache", None)
1149 if cache is None:
1150 cache = cls._cache = {}
1151 instance = cache.get(key)
1152 if instance is not None:
1153 # Duplis hack: since dupli instances are not persistent in Blender (we have to re-create them to get updated
1154 # info like matrix...), we *always* need to reset that matrix when calling ObjectWrapper() (all
1155 # other data is supposed valid during whole cache live span, so we can skip resetting it).
1156 instance._dupli_matrix = dup_mat
1157 return instance
1159 instance = cls.__new__(cls, bdata, armature)
1160 instance.__init__(bdata, armature)
1161 instance.key = key
1162 instance._dupli_matrix = dup_mat
1163 cache[key] = instance
1164 return instance
1167 class ObjectWrapper(metaclass=MetaObjectWrapper):
1169 This class provides a same common interface for all (FBX-wise) object-like elements:
1170 * Blender Object
1171 * Blender Bone and PoseBone
1172 * Blender DepsgraphObjectInstance (for dulis).
1173 Note since a same Blender object might be 'mapped' to several FBX models (esp. with duplis),
1174 we need to use a key to identify each.
1176 __slots__ = (
1177 'name', 'key', 'bdata', 'parented_to_armature', 'override_materials',
1178 '_tag', '_ref', '_dupli_matrix'
1181 @classmethod
1182 def cache_clear(cls):
1183 if hasattr(cls, "_cache"):
1184 del cls._cache
1186 @staticmethod
1187 def _get_dup_num_id(bdata):
1188 INVALID_IDS = {2147483647, 0}
1189 pids = tuple(bdata.persistent_id)
1190 idx_valid = 0
1191 prev_i = ...
1192 for idx, i in enumerate(pids[::-1]):
1193 if i not in INVALID_IDS or (idx == len(pids) and i == 0 and prev_i != 0):
1194 idx_valid = len(pids) - idx
1195 break
1196 prev_i = i
1197 return ".".join(str(i) for i in pids[:idx_valid])
1199 def __init__(self, bdata, armature=None):
1201 bdata might be an Object (deprecated), DepsgraphObjectInstance, Bone or PoseBone.
1202 If Bone or PoseBone, armature Object must be provided.
1204 # Note: DepsgraphObjectInstance are purely runtime data, they become invalid as soon as we step to the next item!
1205 # Hence we have to immediately copy *all* needed data...
1206 if isinstance(bdata, Object): # DEPRECATED
1207 self._tag = 'OB'
1208 self.name = get_blenderID_name(bdata)
1209 self.bdata = bdata
1210 self._ref = None
1211 elif isinstance(bdata, DepsgraphObjectInstance):
1212 if bdata.is_instance:
1213 # Note that dupli instance matrix is set by meta-class initialization.
1214 self._tag = 'DP'
1215 self.name = "|".join((get_blenderID_name((bdata.parent.original, bdata.instance_object.original)),
1216 "Dupli", self._get_dup_num_id(bdata)))
1217 self.bdata = bdata.instance_object.original
1218 self._ref = bdata.parent.original
1219 else:
1220 self._tag = 'OB'
1221 self.name = get_blenderID_name(bdata)
1222 self.bdata = bdata.object.original
1223 self._ref = None
1224 else: # isinstance(bdata, (Bone, PoseBone)):
1225 if isinstance(bdata, PoseBone):
1226 bdata = armature.data.bones[bdata.name]
1227 self._tag = 'BO'
1228 self.name = get_blenderID_name(bdata)
1229 self.bdata = bdata
1230 self._ref = armature
1231 self.parented_to_armature = False
1232 self.override_materials = None
1234 def __eq__(self, other):
1235 return isinstance(other, self.__class__) and self.key == other.key
1237 def __hash__(self):
1238 return hash(self.key)
1240 def __repr__(self):
1241 return self.key
1243 # #### Common to all _tag values.
1244 def get_fbx_uuid(self):
1245 return get_fbx_uuid_from_key(self.key)
1246 fbx_uuid = property(get_fbx_uuid)
1248 # XXX Not sure how much that’s useful now... :/
1249 def get_hide(self):
1250 return self.bdata.hide_viewport if self._tag in {'OB', 'DP'} else self.bdata.hide
1251 hide = property(get_hide)
1253 def get_parent(self):
1254 if self._tag == 'OB':
1255 if (self.bdata.parent and self.bdata.parent.type == 'ARMATURE' and
1256 self.bdata.parent_type == 'BONE' and self.bdata.parent_bone):
1257 # Try to parent to a bone.
1258 bo_par = self.bdata.parent.pose.bones.get(self.bdata.parent_bone, None)
1259 if (bo_par):
1260 return ObjectWrapper(bo_par, self.bdata.parent)
1261 else: # Fallback to mere object parenting.
1262 return ObjectWrapper(self.bdata.parent)
1263 else:
1264 # Mere object parenting.
1265 return ObjectWrapper(self.bdata.parent)
1266 elif self._tag == 'DP':
1267 return ObjectWrapper(self._ref)
1268 else: # self._tag == 'BO'
1269 return ObjectWrapper(self.bdata.parent, self._ref) or ObjectWrapper(self._ref)
1270 parent = property(get_parent)
1272 def get_bdata_pose_bone(self):
1273 if self._tag == 'BO':
1274 return self._ref.pose.bones[self.bdata.name]
1275 return None
1276 bdata_pose_bone = property(get_bdata_pose_bone)
1278 def get_matrix_local(self):
1279 if self._tag == 'OB':
1280 return self.bdata.matrix_local.copy()
1281 elif self._tag == 'DP':
1282 return self._ref.matrix_world.inverted_safe() @ self._dupli_matrix
1283 else: # 'BO', current pose
1284 # PoseBone.matrix is in armature space, bring in back in real local one!
1285 par = self.bdata.parent
1286 par_mat_inv = self._ref.pose.bones[par.name].matrix.inverted_safe() if par else Matrix()
1287 return par_mat_inv @ self._ref.pose.bones[self.bdata.name].matrix
1288 matrix_local = property(get_matrix_local)
1290 def get_matrix_global(self):
1291 if self._tag == 'OB':
1292 return self.bdata.matrix_world.copy()
1293 elif self._tag == 'DP':
1294 return self._dupli_matrix
1295 else: # 'BO', current pose
1296 return self._ref.matrix_world @ self._ref.pose.bones[self.bdata.name].matrix
1297 matrix_global = property(get_matrix_global)
1299 def get_matrix_rest_local(self):
1300 if self._tag == 'BO':
1301 # Bone.matrix_local is in armature space, bring in back in real local one!
1302 par = self.bdata.parent
1303 par_mat_inv = par.matrix_local.inverted_safe() if par else Matrix()
1304 return par_mat_inv @ self.bdata.matrix_local
1305 else:
1306 return self.matrix_local.copy()
1307 matrix_rest_local = property(get_matrix_rest_local)
1309 def get_matrix_rest_global(self):
1310 if self._tag == 'BO':
1311 return self._ref.matrix_world @ self.bdata.matrix_local
1312 else:
1313 return self.matrix_global.copy()
1314 matrix_rest_global = property(get_matrix_rest_global)
1316 # #### Transform and helpers
1317 def has_valid_parent(self, objects):
1318 par = self.parent
1319 if par in objects:
1320 if self._tag == 'OB':
1321 par_type = self.bdata.parent_type
1322 if par_type in {'OBJECT', 'BONE'}:
1323 return True
1324 else:
1325 print("Sorry, “{}” parenting type is not supported".format(par_type))
1326 return False
1327 return True
1328 return False
1330 def use_bake_space_transform(self, scene_data):
1331 # NOTE: Only applies to object types supporting this!!! Currently, only meshes and the like...
1332 # TODO: Check whether this can work for bones too...
1333 return (scene_data.settings.bake_space_transform and self._tag in {'OB', 'DP'} and
1334 self.bdata.type in BLENDER_OBJECT_TYPES_MESHLIKE | {'EMPTY'})
1336 def fbx_object_matrix(self, scene_data, rest=False, local_space=False, global_space=False):
1338 Generate object transform matrix (*always* in matching *FBX* space!).
1339 If local_space is True, returned matrix is *always* in local space.
1340 Else if global_space is True, returned matrix is always in world space.
1341 If both local_space and global_space are False, returned matrix is in parent space if parent is valid,
1342 else in world space.
1343 Note local_space has precedence over global_space.
1344 If rest is True and object is a Bone, returns matching rest pose transform instead of current pose one.
1345 Applies specific rotation to bones, lamps and cameras (conversion Blender -> FBX).
1347 # Objects which are not bones and do not have any parent are *always* in global space
1348 # (unless local_space is True!).
1349 is_global = (not local_space and
1350 (global_space or not (self._tag in {'DP', 'BO'} or self.has_valid_parent(scene_data.objects))))
1352 # Objects (meshes!) parented to armature are not parented to anything in FBX, hence we need them
1353 # in global space, which is their 'virtual' local space...
1354 is_global = is_global or self.parented_to_armature
1356 # Since we have to apply corrections to some types of object, we always need local Blender space here...
1357 matrix = self.matrix_rest_local if rest else self.matrix_local
1358 parent = self.parent
1360 # Bones, lamps and cameras need to be rotated (in local space!).
1361 if self._tag == 'BO':
1362 # If we have a bone parent we need to undo the parent correction.
1363 if not is_global and scene_data.settings.bone_correction_matrix_inv and parent and parent.is_bone:
1364 matrix = scene_data.settings.bone_correction_matrix_inv @ matrix
1365 # Apply the bone correction.
1366 if scene_data.settings.bone_correction_matrix:
1367 matrix = matrix @ scene_data.settings.bone_correction_matrix
1368 elif self.bdata.type == 'LIGHT':
1369 matrix = matrix @ MAT_CONVERT_LIGHT
1370 elif self.bdata.type == 'CAMERA':
1371 matrix = matrix @ MAT_CONVERT_CAMERA
1373 if self._tag in {'DP', 'OB'} and parent:
1374 if parent._tag == 'BO':
1375 # In bone parent case, we get transformation in **bone tip** space (sigh).
1376 # Have to bring it back into bone root, which is FBX expected value.
1377 matrix = Matrix.Translation((0, (parent.bdata.tail - parent.bdata.head).length, 0)) @ matrix
1379 # Our matrix is in local space, time to bring it in its final desired space.
1380 if parent:
1381 if is_global:
1382 # Move matrix to global Blender space.
1383 matrix = (parent.matrix_rest_global if rest else parent.matrix_global) @ matrix
1384 elif parent.use_bake_space_transform(scene_data):
1385 # Blender's and FBX's local space of parent may differ if we use bake_space_transform...
1386 # Apply parent's *Blender* local space...
1387 matrix = (parent.matrix_rest_local if rest else parent.matrix_local) @ matrix
1388 # ...and move it back into parent's *FBX* local space.
1389 par_mat = parent.fbx_object_matrix(scene_data, rest=rest, local_space=True)
1390 matrix = par_mat.inverted_safe() @ matrix
1392 if self.use_bake_space_transform(scene_data):
1393 # If we bake the transforms we need to post-multiply inverse global transform.
1394 # This means that the global transform will not apply to children of this transform.
1395 matrix = matrix @ scene_data.settings.global_matrix_inv
1396 if is_global:
1397 # In any case, pre-multiply the global matrix to get it in FBX global space!
1398 matrix = scene_data.settings.global_matrix @ matrix
1400 return matrix
1402 def fbx_object_tx(self, scene_data, rest=False, rot_euler_compat=None):
1404 Generate object transform data (always in local space when possible).
1406 matrix = self.fbx_object_matrix(scene_data, rest=rest)
1407 loc, rot, scale = matrix.decompose()
1408 matrix_rot = rot.to_matrix()
1409 # quat -> euler, we always use 'XYZ' order, use ref rotation if given.
1410 if rot_euler_compat is not None:
1411 rot = rot.to_euler('XYZ', rot_euler_compat)
1412 else:
1413 rot = rot.to_euler('XYZ')
1414 return loc, rot, scale, matrix, matrix_rot
1416 # #### _tag dependent...
1417 def get_is_object(self):
1418 return self._tag == 'OB'
1419 is_object = property(get_is_object)
1421 def get_is_dupli(self):
1422 return self._tag == 'DP'
1423 is_dupli = property(get_is_dupli)
1425 def get_is_bone(self):
1426 return self._tag == 'BO'
1427 is_bone = property(get_is_bone)
1429 def get_type(self):
1430 if self._tag in {'OB', 'DP'}:
1431 return self.bdata.type
1432 return ...
1433 type = property(get_type)
1435 def get_armature(self):
1436 if self._tag == 'BO':
1437 return ObjectWrapper(self._ref)
1438 return None
1439 armature = property(get_armature)
1441 def get_bones(self):
1442 if self._tag == 'OB' and self.bdata.type == 'ARMATURE':
1443 return (ObjectWrapper(bo, self.bdata) for bo in self.bdata.data.bones)
1444 return ()
1445 bones = property(get_bones)
1447 def get_materials(self):
1448 override_materials = self.override_materials
1449 if override_materials is not None:
1450 return override_materials
1451 if self._tag in {'OB', 'DP'}:
1452 return tuple(slot.material for slot in self.bdata.material_slots)
1453 return ()
1454 materials = property(get_materials)
1456 def is_deformed_by_armature(self, arm_obj):
1457 if not (self.is_object and self.type == 'MESH'):
1458 return False
1459 if self.parent == arm_obj and self.bdata.parent_type == 'ARMATURE':
1460 return True
1461 for mod in self.bdata.modifiers:
1462 if mod.type == 'ARMATURE' and mod.object == arm_obj.bdata:
1463 return True
1465 # #### Duplis...
1466 def dupli_list_gen(self, depsgraph):
1467 if self._tag == 'OB' and self.bdata.is_instancer:
1468 return (ObjectWrapper(dup) for dup in depsgraph.object_instances
1469 if dup.parent and ObjectWrapper(dup.parent.original) == self)
1470 return ()
1473 def fbx_name_class(name, cls):
1474 return FBX_NAME_CLASS_SEP.join((name, cls))
1477 # ##### Top-level FBX data container. #####
1479 # Helper sub-container gathering all exporter settings related to media (texture files).
1480 FBXExportSettingsMedia = namedtuple("FBXExportSettingsMedia", (
1481 "path_mode", "base_src", "base_dst", "subdir",
1482 "embed_textures", "copy_set", "embedded_set",
1485 # Helper container gathering all exporter settings.
1486 FBXExportSettings = namedtuple("FBXExportSettings", (
1487 "report", "to_axes", "global_matrix", "global_scale", "apply_unit_scale", "unit_scale",
1488 "bake_space_transform", "global_matrix_inv", "global_matrix_inv_transposed",
1489 "context_objects", "object_types", "use_mesh_modifiers", "use_mesh_modifiers_render",
1490 "mesh_smooth_type", "use_subsurf", "use_mesh_edges", "use_tspace", "use_triangles",
1491 "armature_nodetype", "use_armature_deform_only", "add_leaf_bones",
1492 "bone_correction_matrix", "bone_correction_matrix_inv",
1493 "bake_anim", "bake_anim_use_all_bones", "bake_anim_use_nla_strips", "bake_anim_use_all_actions",
1494 "bake_anim_step", "bake_anim_simplify_factor", "bake_anim_force_startend_keying",
1495 "use_metadata", "media_settings", "use_custom_props", "colors_type", "prioritize_active_color"
1498 # Helper container gathering some data we need multiple times:
1499 # * templates.
1500 # * settings, scene.
1501 # * objects.
1502 # * object data.
1503 # * skinning data (binding armature/mesh).
1504 # * animations.
1505 FBXExportData = namedtuple("FBXExportData", (
1506 "templates", "templates_users", "connections",
1507 "settings", "scene", "depsgraph", "objects", "animations", "animated", "frame_start", "frame_end",
1508 "data_empties", "data_lights", "data_cameras", "data_meshes", "mesh_material_indices",
1509 "data_bones", "data_leaf_bones", "data_deformers_skin", "data_deformers_shape",
1510 "data_world", "data_materials", "data_textures", "data_videos",
1513 # Helper container gathering all importer settings.
1514 FBXImportSettings = namedtuple("FBXImportSettings", (
1515 "report", "to_axes", "global_matrix", "global_scale",
1516 "bake_space_transform", "global_matrix_inv", "global_matrix_inv_transposed",
1517 "use_custom_normals", "use_image_search",
1518 "use_alpha_decals", "decal_offset",
1519 "use_anim", "anim_offset",
1520 "use_subsurf",
1521 "use_custom_props", "use_custom_props_enum_as_string",
1522 "nodal_material_wrap_map", "image_cache",
1523 "ignore_leaf_bones", "force_connect_children", "automatic_bone_orientation", "bone_correction_matrix",
1524 "use_prepost_rot", "colors_type",