Fix #100973: Node Wrangler: previewing node if hierarchy not active
[blender-addons.git] / io_scene_fbx / export_fbx_bin.py
blobd5a9a1b4834c45845caee513a78f47ca3d931b79
1 # SPDX-FileCopyrightText: 2013 Campbell Barton
2 # SPDX-FileCopyrightText: 2014 Bastien Montagne
4 # SPDX-License-Identifier: GPL-2.0-or-later
6 import datetime
7 import math
8 import numpy as np
9 import os
10 import time
12 from itertools import zip_longest
13 from functools import cache
15 if "bpy" in locals():
16 import importlib
17 if "encode_bin" in locals():
18 importlib.reload(encode_bin)
19 if "data_types" in locals():
20 importlib.reload(data_types)
21 if "fbx_utils" in locals():
22 importlib.reload(fbx_utils)
24 import bpy
25 import bpy_extras
26 from bpy_extras import node_shader_utils
27 from bpy.app.translations import pgettext_tip as tip_
28 from mathutils import Vector, Matrix
30 from . import encode_bin, data_types, fbx_utils
31 from .fbx_utils import (
32 # Constants.
33 FBX_VERSION, FBX_HEADER_VERSION, FBX_SCENEINFO_VERSION, FBX_TEMPLATES_VERSION,
34 FBX_MODELS_VERSION,
35 FBX_GEOMETRY_VERSION, FBX_GEOMETRY_NORMAL_VERSION, FBX_GEOMETRY_BINORMAL_VERSION, FBX_GEOMETRY_TANGENT_VERSION,
36 FBX_GEOMETRY_SMOOTHING_VERSION, FBX_GEOMETRY_CREASE_VERSION, FBX_GEOMETRY_VCOLOR_VERSION, FBX_GEOMETRY_UV_VERSION,
37 FBX_GEOMETRY_MATERIAL_VERSION, FBX_GEOMETRY_LAYER_VERSION,
38 FBX_GEOMETRY_SHAPE_VERSION, FBX_DEFORMER_SHAPE_VERSION, FBX_DEFORMER_SHAPECHANNEL_VERSION,
39 FBX_POSE_BIND_VERSION, FBX_DEFORMER_SKIN_VERSION, FBX_DEFORMER_CLUSTER_VERSION,
40 FBX_MATERIAL_VERSION, FBX_TEXTURE_VERSION,
41 FBX_ANIM_KEY_VERSION,
42 FBX_ANIM_PROPSGROUP_NAME,
43 FBX_KTIME,
44 BLENDER_OTHER_OBJECT_TYPES, BLENDER_OBJECT_TYPES_MESHLIKE,
45 FBX_LIGHT_TYPES, FBX_LIGHT_DECAY_TYPES,
46 RIGHT_HAND_AXES, FBX_FRAMERATES,
47 # Miscellaneous utils.
48 PerfMon,
49 units_blender_to_fbx_factor, units_convertor, units_convertor_iter,
50 matrix4_to_array, similar_values, shape_difference_exclude_similar, astype_view_signedness, fast_first_axis_unique,
51 fast_first_axis_flat,
52 # Attribute helpers.
53 MESH_ATTRIBUTE_CORNER_EDGE, MESH_ATTRIBUTE_SHARP_EDGE, MESH_ATTRIBUTE_EDGE_VERTS, MESH_ATTRIBUTE_CORNER_VERT,
54 MESH_ATTRIBUTE_SHARP_FACE, MESH_ATTRIBUTE_POSITION, MESH_ATTRIBUTE_MATERIAL_INDEX,
55 # Mesh transform helpers.
56 vcos_transformed, nors_transformed,
57 # UUID from key.
58 get_fbx_uuid_from_key,
59 # Key generators.
60 get_blenderID_key, get_blenderID_name,
61 get_blender_mesh_shape_key, get_blender_mesh_shape_channel_key,
62 get_blender_empty_key, get_blender_bone_key,
63 get_blender_bindpose_key, get_blender_armature_skin_key, get_blender_bone_cluster_key,
64 get_blender_anim_id_base, get_blender_anim_stack_key, get_blender_anim_layer_key,
65 get_blender_anim_curve_node_key, get_blender_anim_curve_key,
66 get_blender_nodetexture_key,
67 # FBX element data.
68 elem_empty,
69 elem_data_single_char, elem_data_single_int16, elem_data_single_int32, elem_data_single_int64,
70 elem_data_single_float32, elem_data_single_float64,
71 elem_data_single_bytes, elem_data_single_string, elem_data_single_string_unicode,
72 elem_data_single_bool_array, elem_data_single_int32_array, elem_data_single_int64_array,
73 elem_data_single_float32_array, elem_data_single_float64_array, elem_data_vec_float64,
74 # FBX element properties.
75 elem_properties, elem_props_set, elem_props_compound,
76 # FBX element properties handling templates.
77 elem_props_template_init, elem_props_template_set, elem_props_template_finalize,
78 # Templates.
79 FBXTemplate, fbx_templates_generate,
80 # Animation.
81 AnimationCurveNodeWrapper,
82 # Objects.
83 ObjectWrapper, fbx_name_class, ensure_object_not_in_edit_mode,
84 # Top level.
85 FBXExportSettingsMedia, FBXExportSettings, FBXExportData,
88 # Units converters!
89 convert_sec_to_ktime = units_convertor("second", "ktime")
90 convert_sec_to_ktime_iter = units_convertor_iter("second", "ktime")
92 convert_mm_to_inch = units_convertor("millimeter", "inch")
94 convert_rad_to_deg = units_convertor("radian", "degree")
95 convert_rad_to_deg_iter = units_convertor_iter("radian", "degree")
98 # ##### Templates #####
99 # TODO: check all those "default" values, they should match Blender's default as much as possible, I guess?
101 def fbx_template_def_globalsettings(scene, settings, override_defaults=None, nbr_users=0):
102 props = {}
103 if override_defaults is not None:
104 props.update(override_defaults)
105 return FBXTemplate(b"GlobalSettings", b"", props, nbr_users, [False])
108 def fbx_template_def_model(scene, settings, override_defaults=None, nbr_users=0):
109 gscale = settings.global_scale
110 props = {
111 # Name, Value, Type, Animatable
112 b"QuaternionInterpolate": (0, "p_enum", False), # 0 = no quat interpolation.
113 b"RotationOffset": ((0.0, 0.0, 0.0), "p_vector_3d", False),
114 b"RotationPivot": ((0.0, 0.0, 0.0), "p_vector_3d", False),
115 b"ScalingOffset": ((0.0, 0.0, 0.0), "p_vector_3d", False),
116 b"ScalingPivot": ((0.0, 0.0, 0.0), "p_vector_3d", False),
117 b"TranslationActive": (False, "p_bool", False),
118 b"TranslationMin": ((0.0, 0.0, 0.0), "p_vector_3d", False),
119 b"TranslationMax": ((0.0, 0.0, 0.0), "p_vector_3d", False),
120 b"TranslationMinX": (False, "p_bool", False),
121 b"TranslationMinY": (False, "p_bool", False),
122 b"TranslationMinZ": (False, "p_bool", False),
123 b"TranslationMaxX": (False, "p_bool", False),
124 b"TranslationMaxY": (False, "p_bool", False),
125 b"TranslationMaxZ": (False, "p_bool", False),
126 b"RotationOrder": (0, "p_enum", False), # we always use 'XYZ' order.
127 b"RotationSpaceForLimitOnly": (False, "p_bool", False),
128 b"RotationStiffnessX": (0.0, "p_double", False),
129 b"RotationStiffnessY": (0.0, "p_double", False),
130 b"RotationStiffnessZ": (0.0, "p_double", False),
131 b"AxisLen": (10.0, "p_double", False),
132 b"PreRotation": ((0.0, 0.0, 0.0), "p_vector_3d", False),
133 b"PostRotation": ((0.0, 0.0, 0.0), "p_vector_3d", False),
134 b"RotationActive": (False, "p_bool", False),
135 b"RotationMin": ((0.0, 0.0, 0.0), "p_vector_3d", False),
136 b"RotationMax": ((0.0, 0.0, 0.0), "p_vector_3d", False),
137 b"RotationMinX": (False, "p_bool", False),
138 b"RotationMinY": (False, "p_bool", False),
139 b"RotationMinZ": (False, "p_bool", False),
140 b"RotationMaxX": (False, "p_bool", False),
141 b"RotationMaxY": (False, "p_bool", False),
142 b"RotationMaxZ": (False, "p_bool", False),
143 b"InheritType": (0, "p_enum", False), # RrSs
144 b"ScalingActive": (False, "p_bool", False),
145 b"ScalingMin": ((0.0, 0.0, 0.0), "p_vector_3d", False),
146 b"ScalingMax": ((1.0, 1.0, 1.0), "p_vector_3d", False),
147 b"ScalingMinX": (False, "p_bool", False),
148 b"ScalingMinY": (False, "p_bool", False),
149 b"ScalingMinZ": (False, "p_bool", False),
150 b"ScalingMaxX": (False, "p_bool", False),
151 b"ScalingMaxY": (False, "p_bool", False),
152 b"ScalingMaxZ": (False, "p_bool", False),
153 b"GeometricTranslation": ((0.0, 0.0, 0.0), "p_vector_3d", False),
154 b"GeometricRotation": ((0.0, 0.0, 0.0), "p_vector_3d", False),
155 b"GeometricScaling": ((1.0, 1.0, 1.0), "p_vector_3d", False),
156 b"MinDampRangeX": (0.0, "p_double", False),
157 b"MinDampRangeY": (0.0, "p_double", False),
158 b"MinDampRangeZ": (0.0, "p_double", False),
159 b"MaxDampRangeX": (0.0, "p_double", False),
160 b"MaxDampRangeY": (0.0, "p_double", False),
161 b"MaxDampRangeZ": (0.0, "p_double", False),
162 b"MinDampStrengthX": (0.0, "p_double", False),
163 b"MinDampStrengthY": (0.0, "p_double", False),
164 b"MinDampStrengthZ": (0.0, "p_double", False),
165 b"MaxDampStrengthX": (0.0, "p_double", False),
166 b"MaxDampStrengthY": (0.0, "p_double", False),
167 b"MaxDampStrengthZ": (0.0, "p_double", False),
168 b"PreferedAngleX": (0.0, "p_double", False),
169 b"PreferedAngleY": (0.0, "p_double", False),
170 b"PreferedAngleZ": (0.0, "p_double", False),
171 b"LookAtProperty": (None, "p_object", False),
172 b"UpVectorProperty": (None, "p_object", False),
173 b"Show": (True, "p_bool", False),
174 b"NegativePercentShapeSupport": (True, "p_bool", False),
175 b"DefaultAttributeIndex": (-1, "p_integer", False),
176 b"Freeze": (False, "p_bool", False),
177 b"LODBox": (False, "p_bool", False),
178 b"Lcl Translation": ((0.0, 0.0, 0.0), "p_lcl_translation", True),
179 b"Lcl Rotation": ((0.0, 0.0, 0.0), "p_lcl_rotation", True),
180 b"Lcl Scaling": ((1.0, 1.0, 1.0), "p_lcl_scaling", True),
181 b"Visibility": (1.0, "p_visibility", True),
182 b"Visibility Inheritance": (1, "p_visibility_inheritance", False),
184 if override_defaults is not None:
185 props.update(override_defaults)
186 return FBXTemplate(b"Model", b"FbxNode", props, nbr_users, [False])
189 def fbx_template_def_null(scene, settings, override_defaults=None, nbr_users=0):
190 props = {
191 b"Color": ((0.8, 0.8, 0.8), "p_color_rgb", False),
192 b"Size": (100.0, "p_double", False),
193 b"Look": (1, "p_enum", False), # Cross (0 is None, i.e. invisible?).
195 if override_defaults is not None:
196 props.update(override_defaults)
197 return FBXTemplate(b"NodeAttribute", b"FbxNull", props, nbr_users, [False])
200 def fbx_template_def_light(scene, settings, override_defaults=None, nbr_users=0):
201 gscale = settings.global_scale
202 props = {
203 b"LightType": (0, "p_enum", False), # Point light.
204 b"CastLight": (True, "p_bool", False),
205 b"Color": ((1.0, 1.0, 1.0), "p_color", True),
206 b"Intensity": (100.0, "p_number", True), # Times 100 compared to Blender values...
207 b"DecayType": (2, "p_enum", False), # Quadratic.
208 b"DecayStart": (30.0 * gscale, "p_double", False),
209 b"CastShadows": (True, "p_bool", False),
210 b"ShadowColor": ((0.0, 0.0, 0.0), "p_color", True),
211 b"AreaLightShape": (0, "p_enum", False), # Rectangle.
213 if override_defaults is not None:
214 props.update(override_defaults)
215 return FBXTemplate(b"NodeAttribute", b"FbxLight", props, nbr_users, [False])
218 def fbx_template_def_camera(scene, settings, override_defaults=None, nbr_users=0):
219 r = scene.render
220 props = {
221 b"Color": ((0.8, 0.8, 0.8), "p_color_rgb", False),
222 b"Position": ((0.0, 0.0, -50.0), "p_vector", True),
223 b"UpVector": ((0.0, 1.0, 0.0), "p_vector", True),
224 b"InterestPosition": ((0.0, 0.0, 0.0), "p_vector", True),
225 b"Roll": (0.0, "p_roll", True),
226 b"OpticalCenterX": (0.0, "p_opticalcenterx", True),
227 b"OpticalCenterY": (0.0, "p_opticalcentery", True),
228 b"BackgroundColor": ((0.63, 0.63, 0.63), "p_color", True),
229 b"TurnTable": (0.0, "p_number", True),
230 b"DisplayTurnTableIcon": (False, "p_bool", False),
231 b"UseMotionBlur": (False, "p_bool", False),
232 b"UseRealTimeMotionBlur": (True, "p_bool", False),
233 b"Motion Blur Intensity": (1.0, "p_number", True),
234 b"AspectRatioMode": (0, "p_enum", False), # WindowSize.
235 b"AspectWidth": (320.0, "p_double", False),
236 b"AspectHeight": (200.0, "p_double", False),
237 b"PixelAspectRatio": (1.0, "p_double", False),
238 b"FilmOffsetX": (0.0, "p_number", True),
239 b"FilmOffsetY": (0.0, "p_number", True),
240 b"FilmWidth": (0.816, "p_double", False),
241 b"FilmHeight": (0.612, "p_double", False),
242 b"FilmAspectRatio": (1.3333333333333333, "p_double", False),
243 b"FilmSqueezeRatio": (1.0, "p_double", False),
244 b"FilmFormatIndex": (0, "p_enum", False), # Assuming this is ApertureFormat, 0 = custom.
245 b"PreScale": (1.0, "p_number", True),
246 b"FilmTranslateX": (0.0, "p_number", True),
247 b"FilmTranslateY": (0.0, "p_number", True),
248 b"FilmRollPivotX": (0.0, "p_number", True),
249 b"FilmRollPivotY": (0.0, "p_number", True),
250 b"FilmRollValue": (0.0, "p_number", True),
251 b"FilmRollOrder": (0, "p_enum", False), # 0 = rotate first (default).
252 b"ApertureMode": (2, "p_enum", False), # 2 = Vertical.
253 b"GateFit": (0, "p_enum", False), # 0 = no resolution gate fit.
254 b"FieldOfView": (25.114999771118164, "p_fov", True),
255 b"FieldOfViewX": (40.0, "p_fov_x", True),
256 b"FieldOfViewY": (40.0, "p_fov_y", True),
257 b"FocalLength": (34.89327621672628, "p_number", True),
258 b"CameraFormat": (0, "p_enum", False), # Custom camera format.
259 b"UseFrameColor": (False, "p_bool", False),
260 b"FrameColor": ((0.3, 0.3, 0.3), "p_color_rgb", False),
261 b"ShowName": (True, "p_bool", False),
262 b"ShowInfoOnMoving": (True, "p_bool", False),
263 b"ShowGrid": (True, "p_bool", False),
264 b"ShowOpticalCenter": (False, "p_bool", False),
265 b"ShowAzimut": (True, "p_bool", False),
266 b"ShowTimeCode": (False, "p_bool", False),
267 b"ShowAudio": (False, "p_bool", False),
268 b"AudioColor": ((0.0, 1.0, 0.0), "p_vector_3d", False), # Yep, vector3d, not corlorgb… :cry:
269 b"NearPlane": (10.0, "p_double", False),
270 b"FarPlane": (4000.0, "p_double", False),
271 b"AutoComputeClipPanes": (False, "p_bool", False),
272 b"ViewCameraToLookAt": (True, "p_bool", False),
273 b"ViewFrustumNearFarPlane": (False, "p_bool", False),
274 b"ViewFrustumBackPlaneMode": (2, "p_enum", False), # 2 = show back plane if texture added.
275 b"BackPlaneDistance": (4000.0, "p_number", True),
276 b"BackPlaneDistanceMode": (1, "p_enum", False), # 1 = relative to camera.
277 b"ViewFrustumFrontPlaneMode": (2, "p_enum", False), # 2 = show front plane if texture added.
278 b"FrontPlaneDistance": (10.0, "p_number", True),
279 b"FrontPlaneDistanceMode": (1, "p_enum", False), # 1 = relative to camera.
280 b"LockMode": (False, "p_bool", False),
281 b"LockInterestNavigation": (False, "p_bool", False),
282 # BackPlate... properties **arggggg!**
283 b"FitImage": (False, "p_bool", False),
284 b"Crop": (False, "p_bool", False),
285 b"Center": (True, "p_bool", False),
286 b"KeepRatio": (True, "p_bool", False),
287 # End of BackPlate...
288 b"BackgroundAlphaTreshold": (0.5, "p_double", False),
289 b"ShowBackplate": (True, "p_bool", False),
290 b"BackPlaneOffsetX": (0.0, "p_number", True),
291 b"BackPlaneOffsetY": (0.0, "p_number", True),
292 b"BackPlaneRotation": (0.0, "p_number", True),
293 b"BackPlaneScaleX": (1.0, "p_number", True),
294 b"BackPlaneScaleY": (1.0, "p_number", True),
295 b"Background Texture": (None, "p_object", False),
296 b"FrontPlateFitImage": (True, "p_bool", False),
297 b"FrontPlateCrop": (False, "p_bool", False),
298 b"FrontPlateCenter": (True, "p_bool", False),
299 b"FrontPlateKeepRatio": (True, "p_bool", False),
300 b"Foreground Opacity": (1.0, "p_double", False),
301 b"ShowFrontplate": (True, "p_bool", False),
302 b"FrontPlaneOffsetX": (0.0, "p_number", True),
303 b"FrontPlaneOffsetY": (0.0, "p_number", True),
304 b"FrontPlaneRotation": (0.0, "p_number", True),
305 b"FrontPlaneScaleX": (1.0, "p_number", True),
306 b"FrontPlaneScaleY": (1.0, "p_number", True),
307 b"Foreground Texture": (None, "p_object", False),
308 b"DisplaySafeArea": (False, "p_bool", False),
309 b"DisplaySafeAreaOnRender": (False, "p_bool", False),
310 b"SafeAreaDisplayStyle": (1, "p_enum", False), # 1 = rounded corners.
311 b"SafeAreaAspectRatio": (1.3333333333333333, "p_double", False),
312 b"Use2DMagnifierZoom": (False, "p_bool", False),
313 b"2D Magnifier Zoom": (100.0, "p_number", True),
314 b"2D Magnifier X": (50.0, "p_number", True),
315 b"2D Magnifier Y": (50.0, "p_number", True),
316 b"CameraProjectionType": (0, "p_enum", False), # 0 = perspective, 1 = orthogonal.
317 b"OrthoZoom": (1.0, "p_double", False),
318 b"UseRealTimeDOFAndAA": (False, "p_bool", False),
319 b"UseDepthOfField": (False, "p_bool", False),
320 b"FocusSource": (0, "p_enum", False), # 0 = camera interest, 1 = distance from camera interest.
321 b"FocusAngle": (3.5, "p_double", False), # ???
322 b"FocusDistance": (200.0, "p_double", False),
323 b"UseAntialiasing": (False, "p_bool", False),
324 b"AntialiasingIntensity": (0.77777, "p_double", False),
325 b"AntialiasingMethod": (0, "p_enum", False), # 0 = oversampling, 1 = hardware.
326 b"UseAccumulationBuffer": (False, "p_bool", False),
327 b"FrameSamplingCount": (7, "p_integer", False),
328 b"FrameSamplingType": (1, "p_enum", False), # 0 = uniform, 1 = stochastic.
330 if override_defaults is not None:
331 props.update(override_defaults)
332 return FBXTemplate(b"NodeAttribute", b"FbxCamera", props, nbr_users, [False])
335 def fbx_template_def_bone(scene, settings, override_defaults=None, nbr_users=0):
336 props = {}
337 if override_defaults is not None:
338 props.update(override_defaults)
339 return FBXTemplate(b"NodeAttribute", b"LimbNode", props, nbr_users, [False])
342 def fbx_template_def_geometry(scene, settings, override_defaults=None, nbr_users=0):
343 props = {
344 b"Color": ((0.8, 0.8, 0.8), "p_color_rgb", False),
345 b"BBoxMin": ((0.0, 0.0, 0.0), "p_vector_3d", False),
346 b"BBoxMax": ((0.0, 0.0, 0.0), "p_vector_3d", False),
347 b"Primary Visibility": (True, "p_bool", False),
348 b"Casts Shadows": (True, "p_bool", False),
349 b"Receive Shadows": (True, "p_bool", False),
351 if override_defaults is not None:
352 props.update(override_defaults)
353 return FBXTemplate(b"Geometry", b"FbxMesh", props, nbr_users, [False])
356 def fbx_template_def_material(scene, settings, override_defaults=None, nbr_users=0):
357 # WIP...
358 props = {
359 b"ShadingModel": ("Phong", "p_string", False),
360 b"MultiLayer": (False, "p_bool", False),
361 # Lambert-specific.
362 b"EmissiveColor": ((0.0, 0.0, 0.0), "p_color", True),
363 b"EmissiveFactor": (1.0, "p_number", True),
364 b"AmbientColor": ((0.2, 0.2, 0.2), "p_color", True),
365 b"AmbientFactor": (1.0, "p_number", True),
366 b"DiffuseColor": ((0.8, 0.8, 0.8), "p_color", True),
367 b"DiffuseFactor": (1.0, "p_number", True),
368 b"TransparentColor": ((0.0, 0.0, 0.0), "p_color", True),
369 b"TransparencyFactor": (0.0, "p_number", True),
370 b"Opacity": (1.0, "p_number", True),
371 b"NormalMap": ((0.0, 0.0, 0.0), "p_vector_3d", False),
372 b"Bump": ((0.0, 0.0, 0.0), "p_vector_3d", False),
373 b"BumpFactor": (1.0, "p_double", False),
374 b"DisplacementColor": ((0.0, 0.0, 0.0), "p_color_rgb", False),
375 b"DisplacementFactor": (1.0, "p_double", False),
376 b"VectorDisplacementColor": ((0.0, 0.0, 0.0), "p_color_rgb", False),
377 b"VectorDisplacementFactor": (1.0, "p_double", False),
378 # Phong-specific.
379 b"SpecularColor": ((0.2, 0.2, 0.2), "p_color", True),
380 b"SpecularFactor": (1.0, "p_number", True),
381 # Not sure about the name, importer uses this (but ShininessExponent for tex prop name!)
382 # And in fbx exported by sdk, you have one in template, the other in actual material!!! :/
383 # For now, using both.
384 b"Shininess": (20.0, "p_number", True),
385 b"ShininessExponent": (20.0, "p_number", True),
386 b"ReflectionColor": ((0.0, 0.0, 0.0), "p_color", True),
387 b"ReflectionFactor": (1.0, "p_number", True),
389 if override_defaults is not None:
390 props.update(override_defaults)
391 return FBXTemplate(b"Material", b"FbxSurfacePhong", props, nbr_users, [False])
394 def fbx_template_def_texture_file(scene, settings, override_defaults=None, nbr_users=0):
395 # WIP...
396 # XXX Not sure about all names!
397 props = {
398 b"TextureTypeUse": (0, "p_enum", False), # Standard.
399 b"AlphaSource": (2, "p_enum", False), # Black (i.e. texture's alpha), XXX name guessed!.
400 b"Texture alpha": (1.0, "p_double", False),
401 b"PremultiplyAlpha": (True, "p_bool", False),
402 b"CurrentTextureBlendMode": (1, "p_enum", False), # Additive...
403 b"CurrentMappingType": (0, "p_enum", False), # UV.
404 b"UVSet": ("default", "p_string", False), # UVMap name.
405 b"WrapModeU": (0, "p_enum", False), # Repeat.
406 b"WrapModeV": (0, "p_enum", False), # Repeat.
407 b"UVSwap": (False, "p_bool", False),
408 b"Translation": ((0.0, 0.0, 0.0), "p_vector_3d", False),
409 b"Rotation": ((0.0, 0.0, 0.0), "p_vector_3d", False),
410 b"Scaling": ((1.0, 1.0, 1.0), "p_vector_3d", False),
411 b"TextureRotationPivot": ((0.0, 0.0, 0.0), "p_vector_3d", False),
412 b"TextureScalingPivot": ((0.0, 0.0, 0.0), "p_vector_3d", False),
413 # Not sure about those two...
414 b"UseMaterial": (False, "p_bool", False),
415 b"UseMipMap": (False, "p_bool", False),
417 if override_defaults is not None:
418 props.update(override_defaults)
419 return FBXTemplate(b"Texture", b"FbxFileTexture", props, nbr_users, [False])
422 def fbx_template_def_video(scene, settings, override_defaults=None, nbr_users=0):
423 # WIP...
424 props = {
425 # All pictures.
426 b"Width": (0, "p_integer", False),
427 b"Height": (0, "p_integer", False),
428 b"Path": ("", "p_string_url", False),
429 b"AccessMode": (0, "p_enum", False), # Disk (0=Disk, 1=Mem, 2=DiskAsync).
430 # All videos.
431 b"StartFrame": (0, "p_integer", False),
432 b"StopFrame": (0, "p_integer", False),
433 b"Offset": (0, "p_timestamp", False),
434 b"PlaySpeed": (0.0, "p_double", False),
435 b"FreeRunning": (False, "p_bool", False),
436 b"Loop": (False, "p_bool", False),
437 b"InterlaceMode": (0, "p_enum", False), # None, i.e. progressive.
438 # Image sequences.
439 b"ImageSequence": (False, "p_bool", False),
440 b"ImageSequenceOffset": (0, "p_integer", False),
441 b"FrameRate": (0.0, "p_double", False),
442 b"LastFrame": (0, "p_integer", False),
444 if override_defaults is not None:
445 props.update(override_defaults)
446 return FBXTemplate(b"Video", b"FbxVideo", props, nbr_users, [False])
449 def fbx_template_def_pose(scene, settings, override_defaults=None, nbr_users=0):
450 props = {}
451 if override_defaults is not None:
452 props.update(override_defaults)
453 return FBXTemplate(b"Pose", b"", props, nbr_users, [False])
456 def fbx_template_def_deformer(scene, settings, override_defaults=None, nbr_users=0):
457 props = {}
458 if override_defaults is not None:
459 props.update(override_defaults)
460 return FBXTemplate(b"Deformer", b"", props, nbr_users, [False])
463 def fbx_template_def_animstack(scene, settings, override_defaults=None, nbr_users=0):
464 props = {
465 b"Description": ("", "p_string", False),
466 b"LocalStart": (0, "p_timestamp", False),
467 b"LocalStop": (0, "p_timestamp", False),
468 b"ReferenceStart": (0, "p_timestamp", False),
469 b"ReferenceStop": (0, "p_timestamp", False),
471 if override_defaults is not None:
472 props.update(override_defaults)
473 return FBXTemplate(b"AnimationStack", b"FbxAnimStack", props, nbr_users, [False])
476 def fbx_template_def_animlayer(scene, settings, override_defaults=None, nbr_users=0):
477 props = {
478 b"Weight": (100.0, "p_number", True),
479 b"Mute": (False, "p_bool", False),
480 b"Solo": (False, "p_bool", False),
481 b"Lock": (False, "p_bool", False),
482 b"Color": ((0.8, 0.8, 0.8), "p_color_rgb", False),
483 b"BlendMode": (0, "p_enum", False),
484 b"RotationAccumulationMode": (0, "p_enum", False),
485 b"ScaleAccumulationMode": (0, "p_enum", False),
486 b"BlendModeBypass": (0, "p_ulonglong", False),
488 if override_defaults is not None:
489 props.update(override_defaults)
490 return FBXTemplate(b"AnimationLayer", b"FbxAnimLayer", props, nbr_users, [False])
493 def fbx_template_def_animcurvenode(scene, settings, override_defaults=None, nbr_users=0):
494 props = {
495 FBX_ANIM_PROPSGROUP_NAME.encode(): (None, "p_compound", False),
497 if override_defaults is not None:
498 props.update(override_defaults)
499 return FBXTemplate(b"AnimationCurveNode", b"FbxAnimCurveNode", props, nbr_users, [False])
502 def fbx_template_def_animcurve(scene, settings, override_defaults=None, nbr_users=0):
503 props = {}
504 if override_defaults is not None:
505 props.update(override_defaults)
506 return FBXTemplate(b"AnimationCurve", b"", props, nbr_users, [False])
509 # ##### Generators for connection elements. #####
511 def elem_connection(elem, c_type, uid_src, uid_dst, prop_dst=None):
512 e = elem_data_single_string(elem, b"C", c_type)
513 e.add_int64(uid_src)
514 e.add_int64(uid_dst)
515 if prop_dst is not None:
516 e.add_string(prop_dst)
519 # ##### FBX objects generators. #####
521 def fbx_data_element_custom_properties(props, bid):
523 Store custom properties of blender ID bid (any mapping-like object, in fact) into FBX properties props.
525 items = bid.items()
527 if not items:
528 return
530 rna_properties = {prop.identifier for prop in bid.bl_rna.properties if prop.is_runtime}
532 for k, v in items:
533 if k in rna_properties:
534 continue
536 list_val = getattr(v, "to_list", lambda: None)()
538 if isinstance(v, str):
539 elem_props_set(props, "p_string", k.encode(), v, custom=True)
540 elif isinstance(v, int):
541 elem_props_set(props, "p_integer", k.encode(), v, custom=True)
542 elif isinstance(v, float):
543 elem_props_set(props, "p_double", k.encode(), v, custom=True)
544 elif list_val:
545 if len(list_val) == 3:
546 elem_props_set(props, "p_vector", k.encode(), list_val, custom=True)
547 else:
548 elem_props_set(props, "p_string", k.encode(), str(list_val), custom=True)
549 else:
550 elem_props_set(props, "p_string", k.encode(), str(v), custom=True)
553 def fbx_data_empty_elements(root, empty, scene_data):
555 Write the Empty data block (you can control its FBX datatype with the 'fbx_type' string custom property) or Armature
556 NodeAttribute.
558 empty_key = scene_data.data_empties[empty]
560 null = elem_data_single_int64(root, b"NodeAttribute", get_fbx_uuid_from_key(empty_key))
561 null.add_string(fbx_name_class(empty.name.encode(), b"NodeAttribute"))
562 bdata = empty.bdata
563 if bdata.type == 'EMPTY':
564 val = bdata.get('fbx_type', None)
565 fbx_type = val.encode() if val and isinstance(val, str) else b"Null"
566 else:
567 fbx_type = b"Null"
568 null.add_string(fbx_type)
570 elem_data_single_string(null, b"TypeFlags", b"Null")
572 tmpl = elem_props_template_init(scene_data.templates, b"Null")
573 props = elem_properties(null)
574 elem_props_template_finalize(tmpl, props)
576 # Empty/Armature Object custom properties have already been saved with the Model.
577 # Only Armature data custom properties need to be saved here with the NodeAttribute.
578 if bdata.type == 'ARMATURE':
579 fbx_data_element_custom_properties(props, bdata.data)
582 def fbx_data_light_elements(root, lamp, scene_data):
584 Write the Lamp data block.
586 gscale = scene_data.settings.global_scale
588 light_key = scene_data.data_lights[lamp]
589 do_light = True
590 do_shadow = False
591 shadow_color = Vector((0.0, 0.0, 0.0))
592 if lamp.type not in {'HEMI'}:
593 do_light = True
594 do_shadow = lamp.use_shadow
595 shadow_color = lamp.shadow_color
597 light = elem_data_single_int64(root, b"NodeAttribute", get_fbx_uuid_from_key(light_key))
598 light.add_string(fbx_name_class(lamp.name.encode(), b"NodeAttribute"))
599 light.add_string(b"Light")
601 elem_data_single_int32(light, b"GeometryVersion", FBX_GEOMETRY_VERSION) # Sic...
603 tmpl = elem_props_template_init(scene_data.templates, b"Light")
604 props = elem_properties(light)
605 elem_props_template_set(tmpl, props, "p_enum", b"LightType", FBX_LIGHT_TYPES[lamp.type])
606 elem_props_template_set(tmpl, props, "p_bool", b"CastLight", do_light)
607 elem_props_template_set(tmpl, props, "p_color", b"Color", lamp.color)
608 elem_props_template_set(tmpl, props, "p_number", b"Intensity", lamp.energy * 100.0)
609 elem_props_template_set(tmpl, props, "p_enum", b"DecayType", FBX_LIGHT_DECAY_TYPES['INVERSE_SQUARE'])
610 elem_props_template_set(tmpl, props, "p_double", b"DecayStart", 25.0 * gscale) # 25 is old Blender default
611 elem_props_template_set(tmpl, props, "p_bool", b"CastShadows", do_shadow)
612 elem_props_template_set(tmpl, props, "p_color", b"ShadowColor", shadow_color)
613 if lamp.type in {'SPOT'}:
614 elem_props_template_set(tmpl, props, "p_double", b"OuterAngle", math.degrees(lamp.spot_size))
615 elem_props_template_set(tmpl, props, "p_double", b"InnerAngle",
616 math.degrees(lamp.spot_size * (1.0 - lamp.spot_blend)))
617 elem_props_template_finalize(tmpl, props)
619 # Custom properties.
620 if scene_data.settings.use_custom_props:
621 fbx_data_element_custom_properties(props, lamp)
624 def fbx_data_camera_elements(root, cam_obj, scene_data):
626 Write the Camera data blocks.
628 gscale = scene_data.settings.global_scale
630 cam = cam_obj.bdata
631 cam_data = cam.data
632 cam_key = scene_data.data_cameras[cam_obj]
634 # Real data now, good old camera!
635 # Object transform info.
636 loc, rot, scale, matrix, matrix_rot = cam_obj.fbx_object_tx(scene_data)
637 up = matrix_rot @ Vector((0.0, 1.0, 0.0))
638 to = matrix_rot @ Vector((0.0, 0.0, -1.0))
639 # Render settings.
640 # TODO We could export much more...
641 render = scene_data.scene.render
642 width = render.resolution_x
643 height = render.resolution_y
644 aspect = width / height
645 # Film width & height from mm to inches
646 filmwidth = convert_mm_to_inch(cam_data.sensor_width)
647 filmheight = convert_mm_to_inch(cam_data.sensor_height)
648 filmaspect = filmwidth / filmheight
649 # Film offset
650 offsetx = filmwidth * cam_data.shift_x
651 offsety = filmaspect * filmheight * cam_data.shift_y
653 cam = elem_data_single_int64(root, b"NodeAttribute", get_fbx_uuid_from_key(cam_key))
654 cam.add_string(fbx_name_class(cam_data.name.encode(), b"NodeAttribute"))
655 cam.add_string(b"Camera")
657 tmpl = elem_props_template_init(scene_data.templates, b"Camera")
658 props = elem_properties(cam)
660 elem_props_template_set(tmpl, props, "p_vector", b"Position", loc)
661 elem_props_template_set(tmpl, props, "p_vector", b"UpVector", up)
662 elem_props_template_set(tmpl, props, "p_vector", b"InterestPosition", loc + to) # Point, not vector!
663 # Should we use world value?
664 elem_props_template_set(tmpl, props, "p_color", b"BackgroundColor", (0.0, 0.0, 0.0))
665 elem_props_template_set(tmpl, props, "p_bool", b"DisplayTurnTableIcon", True)
667 elem_props_template_set(tmpl, props, "p_enum", b"AspectRatioMode", 2) # FixedResolution
668 elem_props_template_set(tmpl, props, "p_double", b"AspectWidth", float(render.resolution_x))
669 elem_props_template_set(tmpl, props, "p_double", b"AspectHeight", float(render.resolution_y))
670 elem_props_template_set(tmpl, props, "p_double", b"PixelAspectRatio",
671 float(render.pixel_aspect_x / render.pixel_aspect_y))
673 elem_props_template_set(tmpl, props, "p_double", b"FilmWidth", filmwidth)
674 elem_props_template_set(tmpl, props, "p_double", b"FilmHeight", filmheight)
675 elem_props_template_set(tmpl, props, "p_double", b"FilmAspectRatio", filmaspect)
676 elem_props_template_set(tmpl, props, "p_double", b"FilmOffsetX", offsetx)
677 elem_props_template_set(tmpl, props, "p_double", b"FilmOffsetY", offsety)
679 elem_props_template_set(tmpl, props, "p_enum", b"ApertureMode", 3) # FocalLength.
680 elem_props_template_set(tmpl, props, "p_enum", b"GateFit", 2) # FitHorizontal.
681 elem_props_template_set(tmpl, props, "p_fov", b"FieldOfView", math.degrees(cam_data.angle_x))
682 elem_props_template_set(tmpl, props, "p_fov_x", b"FieldOfViewX", math.degrees(cam_data.angle_x))
683 elem_props_template_set(tmpl, props, "p_fov_y", b"FieldOfViewY", math.degrees(cam_data.angle_y))
684 # No need to convert to inches here...
685 elem_props_template_set(tmpl, props, "p_double", b"FocalLength", cam_data.lens)
686 elem_props_template_set(tmpl, props, "p_double", b"SafeAreaAspectRatio", aspect)
687 # Depth of field and Focus distance.
688 elem_props_template_set(tmpl, props, "p_bool", b"UseDepthOfField", cam_data.dof.use_dof)
689 elem_props_template_set(tmpl, props, "p_double", b"FocusDistance", cam_data.dof.focus_distance * 1000 * gscale)
690 # Default to perspective camera.
691 elem_props_template_set(tmpl, props, "p_enum", b"CameraProjectionType", 1 if cam_data.type == 'ORTHO' else 0)
692 elem_props_template_set(tmpl, props, "p_double", b"OrthoZoom", cam_data.ortho_scale)
694 elem_props_template_set(tmpl, props, "p_double", b"NearPlane", cam_data.clip_start * gscale)
695 elem_props_template_set(tmpl, props, "p_double", b"FarPlane", cam_data.clip_end * gscale)
696 elem_props_template_set(tmpl, props, "p_enum", b"BackPlaneDistanceMode", 1) # RelativeToCamera.
697 elem_props_template_set(tmpl, props, "p_double", b"BackPlaneDistance", cam_data.clip_end * gscale)
699 elem_props_template_finalize(tmpl, props)
701 # Custom properties.
702 if scene_data.settings.use_custom_props:
703 fbx_data_element_custom_properties(props, cam_data)
705 elem_data_single_string(cam, b"TypeFlags", b"Camera")
706 elem_data_single_int32(cam, b"GeometryVersion", 124) # Sic...
707 elem_data_vec_float64(cam, b"Position", loc)
708 elem_data_vec_float64(cam, b"Up", up)
709 elem_data_vec_float64(cam, b"LookAt", to)
710 elem_data_single_int32(cam, b"ShowInfoOnMoving", 1)
711 elem_data_single_int32(cam, b"ShowAudio", 0)
712 elem_data_vec_float64(cam, b"AudioColor", (0.0, 1.0, 0.0))
713 elem_data_single_float64(cam, b"CameraOrthoZoom", 1.0)
716 def fbx_data_bindpose_element(root, me_obj, me, scene_data, arm_obj=None, mat_world_arm=None, bones=[]):
718 Helper, since bindpose are used by both meshes shape keys and armature bones...
720 if arm_obj is None:
721 arm_obj = me_obj
722 # We assume bind pose for our bones are their "Editmode" pose...
723 # All matrices are expected in global (world) space.
724 bindpose_key = get_blender_bindpose_key(arm_obj.bdata, me)
725 fbx_pose = elem_data_single_int64(root, b"Pose", get_fbx_uuid_from_key(bindpose_key))
726 fbx_pose.add_string(fbx_name_class(me.name.encode(), b"Pose"))
727 fbx_pose.add_string(b"BindPose")
729 elem_data_single_string(fbx_pose, b"Type", b"BindPose")
730 elem_data_single_int32(fbx_pose, b"Version", FBX_POSE_BIND_VERSION)
731 elem_data_single_int32(fbx_pose, b"NbPoseNodes", 1 + (1 if (arm_obj != me_obj) else 0) + len(bones))
733 # First node is mesh/object.
734 mat_world_obj = me_obj.fbx_object_matrix(scene_data, global_space=True)
735 fbx_posenode = elem_empty(fbx_pose, b"PoseNode")
736 elem_data_single_int64(fbx_posenode, b"Node", me_obj.fbx_uuid)
737 elem_data_single_float64_array(fbx_posenode, b"Matrix", matrix4_to_array(mat_world_obj))
738 # Second node is armature object itself.
739 if arm_obj != me_obj:
740 fbx_posenode = elem_empty(fbx_pose, b"PoseNode")
741 elem_data_single_int64(fbx_posenode, b"Node", arm_obj.fbx_uuid)
742 elem_data_single_float64_array(fbx_posenode, b"Matrix", matrix4_to_array(mat_world_arm))
743 # And all bones of armature!
744 mat_world_bones = {}
745 for bo_obj in bones:
746 bomat = bo_obj.fbx_object_matrix(scene_data, rest=True, global_space=True)
747 mat_world_bones[bo_obj] = bomat
748 fbx_posenode = elem_empty(fbx_pose, b"PoseNode")
749 elem_data_single_int64(fbx_posenode, b"Node", bo_obj.fbx_uuid)
750 elem_data_single_float64_array(fbx_posenode, b"Matrix", matrix4_to_array(bomat))
752 return mat_world_obj, mat_world_bones
755 def fbx_data_mesh_shapes_elements(root, me_obj, me, scene_data, fbx_me_tmpl, fbx_me_props):
757 Write shape keys related data.
759 if me not in scene_data.data_deformers_shape:
760 return
762 write_normals = True # scene_data.settings.mesh_smooth_type in {'OFF'}
764 # First, write the geometry data itself (i.e. shapes).
765 _me_key, shape_key, shapes = scene_data.data_deformers_shape[me]
767 channels = []
769 vertices = me.vertices
770 for shape, (channel_key, geom_key, shape_verts_co, shape_verts_idx) in shapes.items():
771 # Use vgroups as weights, if defined.
772 if shape.vertex_group and shape.vertex_group in me_obj.bdata.vertex_groups:
773 shape_verts_weights = np.zeros(len(shape_verts_idx), dtype=np.float64)
774 # It's slightly faster to iterate and index the underlying memoryview objects
775 mv_shape_verts_weights = shape_verts_weights.data
776 mv_shape_verts_idx = shape_verts_idx.data
777 vg_idx = me_obj.bdata.vertex_groups[shape.vertex_group].index
778 for sk_idx, v_idx in enumerate(mv_shape_verts_idx):
779 for vg in vertices[v_idx].groups:
780 if vg.group == vg_idx:
781 mv_shape_verts_weights[sk_idx] = vg.weight
782 break
783 shape_verts_weights *= 100.0
784 else:
785 shape_verts_weights = np.full(len(shape_verts_idx), 100.0, dtype=np.float64)
786 channels.append((channel_key, shape, shape_verts_weights))
788 geom = elem_data_single_int64(root, b"Geometry", get_fbx_uuid_from_key(geom_key))
789 geom.add_string(fbx_name_class(shape.name.encode(), b"Geometry"))
790 geom.add_string(b"Shape")
792 tmpl = elem_props_template_init(scene_data.templates, b"Geometry")
793 props = elem_properties(geom)
794 elem_props_template_finalize(tmpl, props)
796 elem_data_single_int32(geom, b"Version", FBX_GEOMETRY_SHAPE_VERSION)
798 elem_data_single_int32_array(geom, b"Indexes", shape_verts_idx)
799 elem_data_single_float64_array(geom, b"Vertices", shape_verts_co)
800 if write_normals:
801 elem_data_single_float64_array(geom, b"Normals", np.zeros(len(shape_verts_idx) * 3, dtype=np.float64))
803 # Yiha! BindPose for shapekeys too! Dodecasigh...
804 # XXX Not sure yet whether several bindposes on same mesh are allowed, or not... :/
805 fbx_data_bindpose_element(root, me_obj, me, scene_data)
807 # ...and now, the deformers stuff.
808 fbx_shape = elem_data_single_int64(root, b"Deformer", get_fbx_uuid_from_key(shape_key))
809 fbx_shape.add_string(fbx_name_class(me.name.encode(), b"Deformer"))
810 fbx_shape.add_string(b"BlendShape")
812 elem_data_single_int32(fbx_shape, b"Version", FBX_DEFORMER_SHAPE_VERSION)
814 for channel_key, shape, shape_verts_weights in channels:
815 fbx_channel = elem_data_single_int64(root, b"Deformer", get_fbx_uuid_from_key(channel_key))
816 fbx_channel.add_string(fbx_name_class(shape.name.encode(), b"SubDeformer"))
817 fbx_channel.add_string(b"BlendShapeChannel")
819 elem_data_single_int32(fbx_channel, b"Version", FBX_DEFORMER_SHAPECHANNEL_VERSION)
820 elem_data_single_float64(fbx_channel, b"DeformPercent", shape.value * 100.0) # Percents...
821 elem_data_single_float64_array(fbx_channel, b"FullWeights", shape_verts_weights)
823 # *WHY* add this in linked mesh properties too? *cry*
824 # No idea whether it’s percent here too, or more usual factor (assume percentage for now) :/
825 elem_props_template_set(fbx_me_tmpl, fbx_me_props, "p_number", shape.name.encode(), shape.value * 100.0,
826 animatable=True)
829 def fbx_data_mesh_elements(root, me_obj, scene_data, done_meshes):
831 Write the Mesh (Geometry) data block.
833 # Ugly helper... :/
834 def _infinite_gen(val):
835 while 1:
836 yield val
838 me_key, me, _free = scene_data.data_meshes[me_obj]
840 # In case of multiple instances of same mesh, only write it once!
841 if me_key in done_meshes:
842 return
844 # No gscale/gmat here, all data are supposed to be in object space.
845 smooth_type = scene_data.settings.mesh_smooth_type
846 write_normals = True # smooth_type in {'OFF'}
848 do_bake_space_transform = me_obj.use_bake_space_transform(scene_data)
850 # Vertices are in object space, but we are post-multiplying all transforms with the inverse of the
851 # global matrix, so we need to apply the global matrix to the vertices to get the correct result.
852 geom_mat_co = scene_data.settings.global_matrix if do_bake_space_transform else None
853 # We need to apply the inverse transpose of the global matrix when transforming normals.
854 geom_mat_no = Matrix(scene_data.settings.global_matrix_inv_transposed) if do_bake_space_transform else None
855 if geom_mat_no is not None:
856 # Remove translation & scaling!
857 geom_mat_no.translation = Vector()
858 geom_mat_no.normalize()
860 geom = elem_data_single_int64(root, b"Geometry", get_fbx_uuid_from_key(me_key))
861 geom.add_string(fbx_name_class(me.name.encode(), b"Geometry"))
862 geom.add_string(b"Mesh")
864 tmpl = elem_props_template_init(scene_data.templates, b"Geometry")
865 props = elem_properties(geom)
867 # Custom properties.
868 if scene_data.settings.use_custom_props:
869 fbx_data_element_custom_properties(props, me)
871 # Subdivision levels. Take them from the first found subsurf modifier from the
872 # first object that has the mesh. Write crease information if the object has
873 # and subsurf modifier.
874 write_crease = False
875 if scene_data.settings.use_subsurf:
876 last_subsurf = None
877 for mod in me_obj.bdata.modifiers:
878 if not (mod.show_render or mod.show_viewport):
879 continue
880 if mod.type == 'SUBSURF' and mod.subdivision_type == 'CATMULL_CLARK':
881 last_subsurf = mod
883 if last_subsurf:
884 elem_data_single_int32(geom, b"Smoothness", 2) # Display control mesh and smoothed
885 if last_subsurf.boundary_smooth == "PRESERVE_CORNERS":
886 elem_data_single_int32(geom, b"BoundaryRule", 1) # CreaseAll
887 else:
888 elem_data_single_int32(geom, b"BoundaryRule", 2) # CreaseEdge
889 elem_data_single_int32(geom, b"PreviewDivisionLevels", last_subsurf.levels)
890 elem_data_single_int32(geom, b"RenderDivisionLevels", last_subsurf.render_levels)
892 elem_data_single_int32(geom, b"PreserveBorders", 0)
893 elem_data_single_int32(geom, b"PreserveHardEdges", 0)
894 elem_data_single_int32(geom, b"PropagateEdgeHardness", 0)
896 write_crease = last_subsurf.use_creases
898 elem_data_single_int32(geom, b"GeometryVersion", FBX_GEOMETRY_VERSION)
900 attributes = me.attributes
902 # Vertex cos.
903 pos_fbx_dtype = np.float64
904 t_pos = MESH_ATTRIBUTE_POSITION.to_ndarray(attributes)
905 elem_data_single_float64_array(geom, b"Vertices", vcos_transformed(t_pos, geom_mat_co, pos_fbx_dtype))
906 del t_pos
908 # Polygon indices.
910 # We do loose edges as two-vertices faces, if enabled...
912 # Note we have to process Edges in the same time, as they are based on poly's loops...
914 # Total number of loops, including any extra added for loose edges.
915 loop_nbr = len(me.loops)
917 # dtypes matching the C data. Matching the C datatype avoids iteration and casting of every element in foreach_get's
918 # C code.
919 bl_loop_index_dtype = np.uintc
921 # Start vertex indices of loops (corners). May contain elements for loops added for the export of loose edges.
922 t_lvi = MESH_ATTRIBUTE_CORNER_VERT.to_ndarray(attributes)
924 # Loop start indices of polygons. May contain elements for the polygons added for the export of loose edges.
925 t_ls = np.empty(len(me.polygons), dtype=bl_loop_index_dtype)
927 # Vertex indices of edges (unsorted, unlike Mesh.edge_keys), flattened into an array twice the length of the number
928 # of edges.
929 t_ev = MESH_ATTRIBUTE_EDGE_VERTS.to_ndarray(attributes)
930 # Each edge has two vertex indices, so it's useful to view the array as 2d where each element on the first axis is a
931 # pair of vertex indices
932 t_ev_pair_view = t_ev.view()
933 t_ev_pair_view.shape = (-1, 2)
935 # Edge indices of loops (corners). May contain elements for loops added for the export of loose edges.
936 t_lei = MESH_ATTRIBUTE_CORNER_EDGE.to_ndarray(attributes)
938 me.polygons.foreach_get("loop_start", t_ls)
940 # Add "fake" faces for loose edges. Each "fake" face consists of two loops creating a new 2-sided polygon.
941 if scene_data.settings.use_mesh_edges:
942 bl_edge_is_loose_dtype = bool
943 # Get the mask of edges that are loose
944 loose_mask = np.empty(len(me.edges), dtype=bl_edge_is_loose_dtype)
945 me.edges.foreach_get('is_loose', loose_mask)
947 indices_of_loose_edges = np.flatnonzero(loose_mask)
948 # Since we add two loops per loose edge, repeat the indices so that there's one for each new loop
949 new_loop_edge_indices = np.repeat(indices_of_loose_edges, 2)
951 # Get the loose edge vertex index pairs
952 t_le = t_ev_pair_view[loose_mask]
954 # append will automatically flatten the pairs in t_le
955 t_lvi = np.append(t_lvi, t_le)
956 t_lei = np.append(t_lei, new_loop_edge_indices)
957 # Two loops are added per loose edge
958 loop_nbr += 2 * len(t_le)
959 t_ls = np.append(t_ls, np.arange(len(me.loops), loop_nbr, 2, dtype=t_ls.dtype))
960 del t_le
961 del loose_mask
962 del indices_of_loose_edges
963 del new_loop_edge_indices
965 # Edges...
966 # Note: Edges are represented as a loop here: each edge uses a single index, which refers to the polygon array.
967 # The edge is made by the vertex indexed py this polygon's point and the next one on the same polygon.
968 # Advantage: Only one index per edge.
969 # Drawback: Only polygon's edges can be represented (that's why we have to add fake two-verts polygons
970 # for loose edges).
971 # We also have to store a mapping from real edges to their indices in this array, for edge-mapped data
972 # (like e.g. crease).
973 eli_fbx_dtype = np.int32
975 # Edge index of each unique edge-key, used to map per-edge data to unique edge-keys (t_pvi).
976 t_pvi_edge_indices = np.empty(0, dtype=t_lei.dtype)
978 pvi_fbx_dtype = np.int32
979 if t_ls.size and t_lvi.size:
980 # Get unsorted edge keys by indexing the edge->vertex-indices array by the loop->edge-index array.
981 t_pvi_edge_keys = t_ev_pair_view[t_lei]
983 # Sort each [edge_start_n, edge_end_n] pair to get edge keys. Heapsort seems to be the fastest for this specific
984 # use case.
985 t_pvi_edge_keys.sort(axis=1, kind='heapsort')
987 # Note that finding unique edge keys means that if there are multiple edges that share the same vertices (which
988 # shouldn't normally happen), only the first edge found in loops will be exported along with its per-edge data.
989 # To export separate edges that share the same vertices, fast_first_axis_unique can be replaced with np.unique
990 # with t_lei as the first argument, finding unique edges rather than unique edge keys.
992 # Since we want the unique values in their original order, the only part we care about is the indices of the
993 # first occurrence of the unique elements in t_pvi_edge_keys, so we can use our fast uniqueness helper function.
994 t_eli = fast_first_axis_unique(t_pvi_edge_keys, return_unique=False, return_index=True)
996 # To get the indices of the elements in t_pvi_edge_keys that produce unique values, but in the original order of
997 # t_pvi_edge_keys, t_eli must be sorted.
998 # Due to loops and their edge keys tending to have a partial ordering within meshes, sorting with kind='stable'
999 # with radix sort tends to be faster than the default of kind='quicksort' with introsort.
1000 t_eli.sort(kind='stable')
1002 # Edge index of each element in unique t_pvi_edge_keys, used to map per-edge data such as sharp and creases.
1003 t_pvi_edge_indices = t_lei[t_eli]
1005 # We have to ^-1 last index of each loop.
1006 # Ensure t_pvi is the correct number of bits before inverting.
1007 # t_lvi may be used again later, so always create a copy to avoid modifying it in the next step.
1008 t_pvi = t_lvi.astype(pvi_fbx_dtype)
1009 # The index of the end of each loop is one before the index of the start of the next loop.
1010 t_pvi[t_ls[1:] - 1] ^= -1
1011 # The index of the end of the last loop will be the very last index.
1012 t_pvi[-1] ^= -1
1013 del t_pvi_edge_keys
1014 else:
1015 # Should be empty, but make sure it's the correct type.
1016 t_pvi = np.empty(0, dtype=pvi_fbx_dtype)
1017 t_eli = np.empty(0, dtype=eli_fbx_dtype)
1019 # And finally we can write data!
1020 t_pvi = astype_view_signedness(t_pvi, pvi_fbx_dtype)
1021 t_eli = astype_view_signedness(t_eli, eli_fbx_dtype)
1022 elem_data_single_int32_array(geom, b"PolygonVertexIndex", t_pvi)
1023 elem_data_single_int32_array(geom, b"Edges", t_eli)
1024 del t_pvi
1025 del t_eli
1026 del t_ev
1027 del t_ev_pair_view
1029 # And now, layers!
1031 # Smoothing.
1032 if smooth_type in {'FACE', 'EDGE'}:
1033 ps_fbx_dtype = np.int32
1034 _map = b""
1035 if smooth_type == 'FACE':
1036 # The FBX integer values are usually interpreted as boolean where 0 is False (sharp) and 1 is True
1037 # (smooth).
1038 # The values may also be used to represent smoothing group bitflags, but this does not seem well-supported.
1039 t_ps = MESH_ATTRIBUTE_SHARP_FACE.get_ndarray(attributes)
1040 if t_ps is not None:
1041 # FBX sharp is False, but Blender sharp is True, so invert.
1042 t_ps = np.logical_not(t_ps)
1043 else:
1044 # The mesh has no "sharp_face" attribute, so every face is smooth.
1045 t_ps = np.ones(len(me.polygons), dtype=ps_fbx_dtype)
1046 _map = b"ByPolygon"
1047 else: # EDGE
1048 _map = b"ByEdge"
1049 if t_pvi_edge_indices.size:
1050 # Write Edge Smoothing.
1051 # Note edge is sharp also if it's used by more than two faces, or one of its faces is flat.
1052 mesh_poly_nbr = len(me.polygons)
1053 mesh_edge_nbr = len(me.edges)
1054 mesh_loop_nbr = len(me.loops)
1055 # t_ls and t_lei may contain extra polygons or loops added for loose edges that are not present in the
1056 # mesh data, so create views that exclude the extra data added for loose edges.
1057 mesh_t_ls_view = t_ls[:mesh_poly_nbr]
1058 mesh_t_lei_view = t_lei[:mesh_loop_nbr]
1060 # - Get sharp edges from edges used by more than two loops (and therefore more than two faces)
1061 e_more_than_two_faces_mask = np.bincount(mesh_t_lei_view, minlength=mesh_edge_nbr) > 2
1063 # - Get sharp edges from the "sharp_edge" attribute. The attribute may not exist, in which case, there
1064 # are no edges marked as sharp.
1065 e_use_sharp_mask = MESH_ATTRIBUTE_SHARP_EDGE.get_ndarray(attributes)
1066 if e_use_sharp_mask is not None:
1067 # - Combine with edges that are sharp because they're in more than two faces
1068 e_use_sharp_mask = np.logical_or(e_use_sharp_mask, e_more_than_two_faces_mask, out=e_use_sharp_mask)
1069 else:
1070 e_use_sharp_mask = e_more_than_two_faces_mask
1072 # - Get sharp edges from flat shaded faces
1073 p_flat_mask = MESH_ATTRIBUTE_SHARP_FACE.get_ndarray(attributes)
1074 if p_flat_mask is not None:
1075 # Convert flat shaded polygons to flat shaded loops by repeating each element by the number of sides
1076 # of that polygon.
1077 # Polygon sides can be calculated from the element-wise difference of loop starts appended by the
1078 # number of loops. Alternatively, polygon sides can be retrieved directly from the 'loop_total'
1079 # attribute of polygons, but since we already have t_ls, it tends to be quicker to calculate from
1080 # t_ls.
1081 polygon_sides = np.diff(mesh_t_ls_view, append=mesh_loop_nbr)
1082 p_flat_loop_mask = np.repeat(p_flat_mask, polygon_sides)
1083 # Convert flat shaded loops to flat shaded (sharp) edge indices.
1084 # Note that if an edge is in multiple loops that are part of flat shaded faces, its edge index will
1085 # end up in sharp_edge_indices_from_polygons multiple times.
1086 sharp_edge_indices_from_polygons = mesh_t_lei_view[p_flat_loop_mask]
1088 # - Combine with edges that are sharp because a polygon they're in has flat shading
1089 e_use_sharp_mask[sharp_edge_indices_from_polygons] = True
1090 del sharp_edge_indices_from_polygons
1091 del p_flat_loop_mask
1092 del polygon_sides
1093 del p_flat_mask
1095 # - Convert sharp edges to sharp edge keys (t_pvi)
1096 ek_use_sharp_mask = e_use_sharp_mask[t_pvi_edge_indices]
1098 # - Sharp edges are indicated in FBX as zero (False), so invert
1099 t_ps = np.invert(ek_use_sharp_mask, out=ek_use_sharp_mask)
1100 del ek_use_sharp_mask
1101 del e_use_sharp_mask
1102 del mesh_t_lei_view
1103 del mesh_t_ls_view
1104 else:
1105 t_ps = np.empty(0, dtype=ps_fbx_dtype)
1106 t_ps = t_ps.astype(ps_fbx_dtype, copy=False)
1107 lay_smooth = elem_data_single_int32(geom, b"LayerElementSmoothing", 0)
1108 elem_data_single_int32(lay_smooth, b"Version", FBX_GEOMETRY_SMOOTHING_VERSION)
1109 elem_data_single_string(lay_smooth, b"Name", b"")
1110 elem_data_single_string(lay_smooth, b"MappingInformationType", _map)
1111 elem_data_single_string(lay_smooth, b"ReferenceInformationType", b"Direct")
1112 elem_data_single_int32_array(lay_smooth, b"Smoothing", t_ps) # Sight, int32 for bool...
1113 del t_ps
1114 del t_ls
1115 del t_lei
1117 # Edge crease for subdivision
1118 if write_crease:
1119 ec_fbx_dtype = np.float64
1120 if t_pvi_edge_indices.size:
1121 ec_bl_dtype = np.single
1122 edge_creases = me.edge_creases
1123 if edge_creases:
1124 t_ec_raw = np.empty(len(me.edges), dtype=ec_bl_dtype)
1125 edge_creases.data.foreach_get("value", t_ec_raw)
1127 # Convert to t_pvi edge-keys.
1128 t_ec_ek_raw = t_ec_raw[t_pvi_edge_indices]
1130 # Blender squares those values before sending them to OpenSubdiv, when other software don't,
1131 # so we need to compensate that to get similar results through FBX...
1132 # Use the precision of the fbx dtype for the calculation since it's usually higher precision.
1133 t_ec_ek_raw = t_ec_ek_raw.astype(ec_fbx_dtype, copy=False)
1134 t_ec = np.square(t_ec_ek_raw, out=t_ec_ek_raw)
1135 del t_ec_ek_raw
1136 del t_ec_raw
1137 else:
1138 # todo: Blender edge creases are optional now, we may be able to avoid writing the array to FBX when
1139 # there are no edge creases.
1140 t_ec = np.zeros(t_pvi_edge_indices.shape, dtype=ec_fbx_dtype)
1141 else:
1142 t_ec = np.empty(0, dtype=ec_fbx_dtype)
1144 lay_crease = elem_data_single_int32(geom, b"LayerElementEdgeCrease", 0)
1145 elem_data_single_int32(lay_crease, b"Version", FBX_GEOMETRY_CREASE_VERSION)
1146 elem_data_single_string(lay_crease, b"Name", b"")
1147 elem_data_single_string(lay_crease, b"MappingInformationType", b"ByEdge")
1148 elem_data_single_string(lay_crease, b"ReferenceInformationType", b"Direct")
1149 elem_data_single_float64_array(lay_crease, b"EdgeCrease", t_ec)
1150 del t_ec
1152 # And we are done with edges!
1153 del t_pvi_edge_indices
1155 # Loop normals.
1156 tspacenumber = 0
1157 if write_normals:
1158 normal_bl_dtype = np.single
1159 normal_fbx_dtype = np.float64
1160 match me.normals_domain:
1161 case 'POINT':
1162 # All faces are smooth shaded, so we can get normals from the vertices.
1163 normal_source = me.vertex_normals
1164 normal_mapping = b"ByVertice"
1165 case 'FACE':
1166 # Either all faces or all edges are sharp, so we can get normals from the faces.
1167 normal_source = me.polygon_normals
1168 normal_mapping = b"ByPolygon"
1169 case 'CORNER':
1170 # We have a mix of sharp/smooth edges/faces or custom split normals, so need to get normals from
1171 # corners.
1172 normal_source = me.corner_normals
1173 normal_mapping = b"ByPolygonVertex"
1174 case _:
1175 # Unreachable
1176 raise AssertionError("Unexpected normals domain '%s'" % me.normals_domain)
1177 # Each normal has 3 components, so the length is multiplied by 3.
1178 t_normal = np.empty(len(normal_source) * 3, dtype=normal_bl_dtype)
1179 normal_source.foreach_get("vector", t_normal)
1180 t_normal = nors_transformed(t_normal, geom_mat_no, normal_fbx_dtype)
1181 normal_idx_fbx_dtype = np.int32
1182 lay_nor = elem_data_single_int32(geom, b"LayerElementNormal", 0)
1183 elem_data_single_int32(lay_nor, b"Version", FBX_GEOMETRY_NORMAL_VERSION)
1184 elem_data_single_string(lay_nor, b"Name", b"")
1185 elem_data_single_string(lay_nor, b"MappingInformationType", normal_mapping)
1186 # FBX SDK documentation says that normals should use IndexToDirect.
1187 elem_data_single_string(lay_nor, b"ReferenceInformationType", b"IndexToDirect")
1189 # Tuple of unique sorted normals and then the index in the unique sorted normals of each normal in t_normal.
1190 # Since we don't care about how the normals are sorted, only that they're unique, we can use the fast unique
1191 # helper function.
1192 t_normal, t_normal_idx = fast_first_axis_unique(t_normal.reshape(-1, 3), return_inverse=True)
1194 # Convert to the type for fbx
1195 t_normal_idx = astype_view_signedness(t_normal_idx, normal_idx_fbx_dtype)
1197 elem_data_single_float64_array(lay_nor, b"Normals", t_normal)
1198 # Normal weights, no idea what it is.
1199 # t_normal_w = np.zeros(len(t_normal), dtype=np.float64)
1200 # elem_data_single_float64_array(lay_nor, b"NormalsW", t_normal_w)
1202 elem_data_single_int32_array(lay_nor, b"NormalsIndex", t_normal_idx)
1204 del t_normal_idx
1205 # del t_normal_w
1206 del t_normal
1208 # tspace
1209 if scene_data.settings.use_tspace:
1210 tspacenumber = len(me.uv_layers)
1211 if tspacenumber:
1212 # We can only compute tspace on tessellated meshes, need to check that here...
1213 lt_bl_dtype = np.uintc
1214 t_lt = np.empty(len(me.polygons), dtype=lt_bl_dtype)
1215 me.polygons.foreach_get("loop_total", t_lt)
1216 if (t_lt > 4).any():
1217 del t_lt
1218 scene_data.settings.report(
1219 {'WARNING'},
1220 tip_("Mesh '%s' has polygons with more than 4 vertices, "
1221 "cannot compute/export tangent space for it") % me.name)
1222 else:
1223 del t_lt
1224 num_loops = len(me.loops)
1225 t_ln = np.empty(num_loops * 3, dtype=normal_bl_dtype)
1226 # t_lnw = np.zeros(len(me.loops), dtype=np.float64)
1227 uv_names = [uvlayer.name for uvlayer in me.uv_layers]
1228 # Annoying, `me.calc_tangent` errors in case there is no geometry...
1229 if num_loops > 0:
1230 for name in uv_names:
1231 me.calc_tangents(uvmap=name)
1232 for idx, uvlayer in enumerate(me.uv_layers):
1233 name = uvlayer.name
1234 # Loop bitangents (aka binormals).
1235 # NOTE: this is not supported by importer currently.
1236 me.loops.foreach_get("bitangent", t_ln)
1237 lay_nor = elem_data_single_int32(geom, b"LayerElementBinormal", idx)
1238 elem_data_single_int32(lay_nor, b"Version", FBX_GEOMETRY_BINORMAL_VERSION)
1239 elem_data_single_string_unicode(lay_nor, b"Name", name)
1240 elem_data_single_string(lay_nor, b"MappingInformationType", b"ByPolygonVertex")
1241 elem_data_single_string(lay_nor, b"ReferenceInformationType", b"Direct")
1242 elem_data_single_float64_array(lay_nor, b"Binormals",
1243 nors_transformed(t_ln, geom_mat_no, normal_fbx_dtype))
1244 # Binormal weights, no idea what it is.
1245 # elem_data_single_float64_array(lay_nor, b"BinormalsW", t_lnw)
1247 # Loop tangents.
1248 # NOTE: this is not supported by importer currently.
1249 me.loops.foreach_get("tangent", t_ln)
1250 lay_nor = elem_data_single_int32(geom, b"LayerElementTangent", idx)
1251 elem_data_single_int32(lay_nor, b"Version", FBX_GEOMETRY_TANGENT_VERSION)
1252 elem_data_single_string_unicode(lay_nor, b"Name", name)
1253 elem_data_single_string(lay_nor, b"MappingInformationType", b"ByPolygonVertex")
1254 elem_data_single_string(lay_nor, b"ReferenceInformationType", b"Direct")
1255 elem_data_single_float64_array(lay_nor, b"Tangents",
1256 nors_transformed(t_ln, geom_mat_no, normal_fbx_dtype))
1257 # Tangent weights, no idea what it is.
1258 # elem_data_single_float64_array(lay_nor, b"TangentsW", t_lnw)
1260 del t_ln
1261 # del t_lnw
1262 me.free_tangents()
1264 # Write VertexColor Layers.
1265 colors_type = scene_data.settings.colors_type
1266 vcolnumber = 0 if colors_type == 'NONE' else len(me.color_attributes)
1267 if vcolnumber:
1268 color_prop_name = "color_srgb" if colors_type == 'SRGB' else "color"
1269 # ByteColorAttribute color also gets returned by the API as single precision float
1270 bl_lc_dtype = np.single
1271 fbx_lc_dtype = np.float64
1272 fbx_lcidx_dtype = np.int32
1274 color_attributes = me.color_attributes
1275 if scene_data.settings.prioritize_active_color:
1276 active_color = me.color_attributes.active_color
1277 color_attributes = sorted(color_attributes, key=lambda x: x == active_color, reverse=True)
1279 for colindex, collayer in enumerate(color_attributes):
1280 is_point = collayer.domain == "POINT"
1281 vcollen = len(me.vertices if is_point else me.loops)
1282 # Each rgba component is flattened in the array
1283 t_lc = np.empty(vcollen * 4, dtype=bl_lc_dtype)
1284 collayer.data.foreach_get(color_prop_name, t_lc)
1285 lay_vcol = elem_data_single_int32(geom, b"LayerElementColor", colindex)
1286 elem_data_single_int32(lay_vcol, b"Version", FBX_GEOMETRY_VCOLOR_VERSION)
1287 elem_data_single_string_unicode(lay_vcol, b"Name", collayer.name)
1288 elem_data_single_string(lay_vcol, b"MappingInformationType", b"ByPolygonVertex")
1289 elem_data_single_string(lay_vcol, b"ReferenceInformationType", b"IndexToDirect")
1291 # Use the fast uniqueness helper function since we don't care about sorting.
1292 t_lc, col_indices = fast_first_axis_unique(t_lc.reshape(-1, 4), return_inverse=True)
1294 if is_point:
1295 # for "point" domain colors, we could directly emit them
1296 # with a "ByVertex" mapping type, but some software does not
1297 # properly understand that. So expand to full "ByPolygonVertex"
1298 # index map.
1299 # Ignore loops added for loose edges.
1300 col_indices = col_indices[t_lvi[:len(me.loops)]]
1302 t_lc = t_lc.astype(fbx_lc_dtype, copy=False)
1303 col_indices = astype_view_signedness(col_indices, fbx_lcidx_dtype)
1305 elem_data_single_float64_array(lay_vcol, b"Colors", t_lc)
1306 elem_data_single_int32_array(lay_vcol, b"ColorIndex", col_indices)
1308 del t_lc
1309 del col_indices
1311 # Write UV layers.
1312 # Note: LayerElementTexture is deprecated since FBX 2011 - luckily!
1313 # Textures are now only related to materials, in FBX!
1314 uvnumber = len(me.uv_layers)
1315 if uvnumber:
1316 luv_bl_dtype = np.single
1317 luv_fbx_dtype = np.float64
1318 lv_idx_fbx_dtype = np.int32
1320 t_luv = np.empty(len(me.loops) * 2, dtype=luv_bl_dtype)
1321 # Fast view for sort-based uniqueness of pairs.
1322 t_luv_fast_pair_view = fast_first_axis_flat(t_luv.reshape(-1, 2))
1323 # It must be a view of t_luv otherwise it won't update when t_luv is updated.
1324 assert(t_luv_fast_pair_view.base is t_luv)
1326 # Looks like this mapping is also expected to convey UV islands (arg..... :((((( ).
1327 # So we need to generate unique triplets (uv, vertex_idx) here, not only just based on UV values.
1328 # Ignore loops added for loose edges.
1329 t_lvidx = t_lvi[:len(me.loops)]
1331 # If we were to create a combined array of (uv, vertex_idx) elements, we could find unique triplets by sorting
1332 # that array by first sorting by the vertex_idx column and then sorting by the uv column using a stable sorting
1333 # algorithm.
1334 # This is exactly what we'll do, but without creating the combined array, because only the uv elements are
1335 # included in the export and the vertex_idx column is the same for every uv layer.
1337 # Because the vertex_idx column is the same for every uv layer, the vertex_idx column can be sorted in advance.
1338 # argsort gets the indices that sort the array, which are needed to be able to sort the array of uv pairs in the
1339 # same way to create the indices that recreate the full uvs from the unique uvs.
1340 # Loops and vertices tend to naturally have a partial ordering, which makes sorting with kind='stable' (radix
1341 # sort) faster than the default of kind='quicksort' (introsort) in most cases.
1342 perm_vidx = t_lvidx.argsort(kind='stable')
1344 # Mask and uv indices arrays will be modified and re-used by each uv layer.
1345 unique_mask = np.empty(len(me.loops), dtype=np.bool_)
1346 unique_mask[:1] = True
1347 uv_indices = np.empty(len(me.loops), dtype=lv_idx_fbx_dtype)
1349 for uvindex, uvlayer in enumerate(me.uv_layers):
1350 lay_uv = elem_data_single_int32(geom, b"LayerElementUV", uvindex)
1351 elem_data_single_int32(lay_uv, b"Version", FBX_GEOMETRY_UV_VERSION)
1352 elem_data_single_string_unicode(lay_uv, b"Name", uvlayer.name)
1353 elem_data_single_string(lay_uv, b"MappingInformationType", b"ByPolygonVertex")
1354 elem_data_single_string(lay_uv, b"ReferenceInformationType", b"IndexToDirect")
1356 uvlayer.uv.foreach_get("vector", t_luv)
1358 # t_luv_fast_pair_view is a view in a dtype that compares elements by individual bytes, but float types have
1359 # separate byte representations of positive and negative zero. For uniqueness, these should be considered
1360 # the same, so replace all -0.0 with 0.0 in advance.
1361 t_luv[t_luv == -0.0] = 0.0
1363 # These steps to create unique_uv_pairs are the same as how np.unique would find unique values by sorting a
1364 # structured array where each element is a triplet of (uv, vertex_idx), except uv and vertex_idx are
1365 # separate arrays here and vertex_idx has already been sorted in advance.
1367 # Sort according to the vertex_idx column, using the precalculated indices that sort it.
1368 sorted_t_luv_fast = t_luv_fast_pair_view[perm_vidx]
1370 # Get the indices that would sort the sorted uv pairs. Stable sorting must be used to maintain the sorting
1371 # of the vertex indices.
1372 perm_uv_pairs = sorted_t_luv_fast.argsort(kind='stable')
1373 # Use the indices to sort both the uv pairs and the vertex_idx columns.
1374 perm_combined = perm_vidx[perm_uv_pairs]
1375 sorted_vidx = t_lvidx[perm_combined]
1376 sorted_t_luv_fast = sorted_t_luv_fast[perm_uv_pairs]
1378 # Create a mask where either the uv pair doesn't equal the previous value in the array, or the vertex index
1379 # doesn't equal the previous value, these will be the unique uv-vidx triplets.
1380 # For an imaginary triplet array:
1381 # ...
1382 # [(0.4, 0.2), 0]
1383 # [(0.4, 0.2), 1] -> Unique because vertex index different from previous
1384 # [(0.4, 0.2), 2] -> Unique because vertex index different from previous
1385 # [(0.7, 0.6), 2] -> Unique because uv different from previous
1386 # [(0.7, 0.6), 2]
1387 # ...
1388 # Output the result into unique_mask.
1389 np.logical_or(sorted_t_luv_fast[1:] != sorted_t_luv_fast[:-1], sorted_vidx[1:] != sorted_vidx[:-1],
1390 out=unique_mask[1:])
1392 # Get each uv pair marked as unique by the unique_mask and then view as the original dtype.
1393 unique_uvs = sorted_t_luv_fast[unique_mask].view(luv_bl_dtype)
1395 # NaN values are considered invalid and indicate a bug somewhere else in Blender or in an addon, we want
1396 # these bugs to be reported instead of hiding them by allowing the export to continue.
1397 if np.isnan(unique_uvs).any():
1398 raise RuntimeError("UV layer %s on %r has invalid UVs containing NaN values" % (uvlayer.name, me))
1400 # Convert to the type needed for fbx
1401 unique_uvs = unique_uvs.astype(luv_fbx_dtype, copy=False)
1403 # Set the indices of pairs in unique_uvs that reconstruct the pairs in t_luv into uv_indices.
1404 # uv_indices will then be the same as an inverse array returned by np.unique with return_inverse=True.
1405 uv_indices[perm_combined] = np.cumsum(unique_mask, dtype=uv_indices.dtype) - 1
1407 elem_data_single_float64_array(lay_uv, b"UV", unique_uvs)
1408 elem_data_single_int32_array(lay_uv, b"UVIndex", uv_indices)
1409 del unique_uvs
1410 del sorted_t_luv_fast
1411 del sorted_vidx
1412 del perm_uv_pairs
1413 del perm_combined
1414 del uv_indices
1415 del unique_mask
1416 del perm_vidx
1417 del t_lvidx
1418 del t_luv
1419 del t_luv_fast_pair_view
1420 del t_lvi
1422 # Face's materials.
1423 me_fbxmaterials_idx = scene_data.mesh_material_indices.get(me)
1424 if me_fbxmaterials_idx is not None:
1425 # We cannot use me.materials here, as this array is filled with None in case materials are linked to object...
1426 me_blmaterials = me_obj.materials
1427 if me_fbxmaterials_idx and me_blmaterials:
1428 lay_ma = elem_data_single_int32(geom, b"LayerElementMaterial", 0)
1429 elem_data_single_int32(lay_ma, b"Version", FBX_GEOMETRY_MATERIAL_VERSION)
1430 elem_data_single_string(lay_ma, b"Name", b"")
1431 nbr_mats = len(me_fbxmaterials_idx)
1432 multiple_fbx_mats = nbr_mats > 1
1433 # If a mesh does not have more than one material its material_index attribute can be ignored.
1434 # If a mesh has multiple materials but all its polygons are assigned to the first material, its
1435 # material_index attribute may not exist.
1436 t_pm = None if not multiple_fbx_mats else MESH_ATTRIBUTE_MATERIAL_INDEX.get_ndarray(attributes)
1437 if t_pm is not None:
1438 fbx_pm_dtype = np.int32
1440 # We have to validate mat indices, and map them to FBX indices.
1441 # Note a mat might not be in me_fbxmaterials_idx (e.g. node mats are ignored).
1443 # The first valid material will be used for materials out of bounds of me_blmaterials or materials not
1444 # in me_fbxmaterials_idx.
1445 def_me_blmaterial_idx, def_ma = next(
1446 (i, me_fbxmaterials_idx[m]) for i, m in enumerate(me_blmaterials) if m in me_fbxmaterials_idx)
1448 # Set material indices that are out of bounds to the default material index
1449 mat_idx_limit = len(me_blmaterials)
1450 # Material indices shouldn't be negative, but they technically could be. Viewing as unsigned before
1451 # checking for indices that are too large means that a single >= check will pick up both negative
1452 # indices and indices that are too large.
1453 t_pm[t_pm.view("u%i" % t_pm.itemsize) >= mat_idx_limit] = def_me_blmaterial_idx
1455 # Map to FBX indices. Materials not in me_fbxmaterials_idx will be set to the default material index.
1456 blmat_fbx_idx = np.fromiter((me_fbxmaterials_idx.get(m, def_ma) for m in me_blmaterials),
1457 dtype=fbx_pm_dtype)
1458 t_pm = blmat_fbx_idx[t_pm]
1460 elem_data_single_string(lay_ma, b"MappingInformationType", b"ByPolygon")
1461 # XXX Logically, should be "Direct" reference type, since we do not have any index array, and have one
1462 # value per polygon...
1463 # But looks like FBX expects it to be IndexToDirect here (maybe because materials are already
1464 # indices??? *sigh*).
1465 elem_data_single_string(lay_ma, b"ReferenceInformationType", b"IndexToDirect")
1466 elem_data_single_int32_array(lay_ma, b"Materials", t_pm)
1467 else:
1468 elem_data_single_string(lay_ma, b"MappingInformationType", b"AllSame")
1469 elem_data_single_string(lay_ma, b"ReferenceInformationType", b"IndexToDirect")
1470 if multiple_fbx_mats:
1471 # There's no material_index attribute, so every material index is effectively zero.
1472 # In the order of the mesh's materials, get the FBX index of the first material that is exported.
1473 all_same_idx = next(me_fbxmaterials_idx[m] for m in me_blmaterials if m in me_fbxmaterials_idx)
1474 else:
1475 # There's only one fbx material, so the index will always be zero.
1476 all_same_idx = 0
1477 elem_data_single_int32_array(lay_ma, b"Materials", [all_same_idx])
1478 del t_pm
1480 # And the "layer TOC"...
1482 layer = elem_data_single_int32(geom, b"Layer", 0)
1483 elem_data_single_int32(layer, b"Version", FBX_GEOMETRY_LAYER_VERSION)
1484 if write_normals:
1485 lay_nor = elem_empty(layer, b"LayerElement")
1486 elem_data_single_string(lay_nor, b"Type", b"LayerElementNormal")
1487 elem_data_single_int32(lay_nor, b"TypedIndex", 0)
1488 if tspacenumber:
1489 lay_binor = elem_empty(layer, b"LayerElement")
1490 elem_data_single_string(lay_binor, b"Type", b"LayerElementBinormal")
1491 elem_data_single_int32(lay_binor, b"TypedIndex", 0)
1492 lay_tan = elem_empty(layer, b"LayerElement")
1493 elem_data_single_string(lay_tan, b"Type", b"LayerElementTangent")
1494 elem_data_single_int32(lay_tan, b"TypedIndex", 0)
1495 if smooth_type in {'FACE', 'EDGE'}:
1496 lay_smooth = elem_empty(layer, b"LayerElement")
1497 elem_data_single_string(lay_smooth, b"Type", b"LayerElementSmoothing")
1498 elem_data_single_int32(lay_smooth, b"TypedIndex", 0)
1499 if write_crease:
1500 lay_smooth = elem_empty(layer, b"LayerElement")
1501 elem_data_single_string(lay_smooth, b"Type", b"LayerElementEdgeCrease")
1502 elem_data_single_int32(lay_smooth, b"TypedIndex", 0)
1503 if vcolnumber:
1504 lay_vcol = elem_empty(layer, b"LayerElement")
1505 elem_data_single_string(lay_vcol, b"Type", b"LayerElementColor")
1506 elem_data_single_int32(lay_vcol, b"TypedIndex", 0)
1507 if uvnumber:
1508 lay_uv = elem_empty(layer, b"LayerElement")
1509 elem_data_single_string(lay_uv, b"Type", b"LayerElementUV")
1510 elem_data_single_int32(lay_uv, b"TypedIndex", 0)
1511 if me_fbxmaterials_idx is not None:
1512 lay_ma = elem_empty(layer, b"LayerElement")
1513 elem_data_single_string(lay_ma, b"Type", b"LayerElementMaterial")
1514 elem_data_single_int32(lay_ma, b"TypedIndex", 0)
1516 # Add other uv and/or vcol layers...
1517 for vcolidx, uvidx, tspaceidx in zip_longest(range(1, vcolnumber), range(1, uvnumber), range(1, tspacenumber),
1518 fillvalue=0):
1519 layer = elem_data_single_int32(geom, b"Layer", max(vcolidx, uvidx))
1520 elem_data_single_int32(layer, b"Version", FBX_GEOMETRY_LAYER_VERSION)
1521 if vcolidx:
1522 lay_vcol = elem_empty(layer, b"LayerElement")
1523 elem_data_single_string(lay_vcol, b"Type", b"LayerElementColor")
1524 elem_data_single_int32(lay_vcol, b"TypedIndex", vcolidx)
1525 if uvidx:
1526 lay_uv = elem_empty(layer, b"LayerElement")
1527 elem_data_single_string(lay_uv, b"Type", b"LayerElementUV")
1528 elem_data_single_int32(lay_uv, b"TypedIndex", uvidx)
1529 if tspaceidx:
1530 lay_binor = elem_empty(layer, b"LayerElement")
1531 elem_data_single_string(lay_binor, b"Type", b"LayerElementBinormal")
1532 elem_data_single_int32(lay_binor, b"TypedIndex", tspaceidx)
1533 lay_tan = elem_empty(layer, b"LayerElement")
1534 elem_data_single_string(lay_tan, b"Type", b"LayerElementTangent")
1535 elem_data_single_int32(lay_tan, b"TypedIndex", tspaceidx)
1537 # Shape keys...
1538 fbx_data_mesh_shapes_elements(root, me_obj, me, scene_data, tmpl, props)
1540 elem_props_template_finalize(tmpl, props)
1541 done_meshes.add(me_key)
1544 def fbx_data_material_elements(root, ma, scene_data):
1546 Write the Material data block.
1549 ambient_color = (0.0, 0.0, 0.0)
1550 if scene_data.data_world:
1551 ambient_color = next(iter(scene_data.data_world.keys())).color
1553 ma_wrap = node_shader_utils.PrincipledBSDFWrapper(ma, is_readonly=True)
1554 ma_key, _objs = scene_data.data_materials[ma]
1555 ma_type = b"Phong"
1557 fbx_ma = elem_data_single_int64(root, b"Material", get_fbx_uuid_from_key(ma_key))
1558 fbx_ma.add_string(fbx_name_class(ma.name.encode(), b"Material"))
1559 fbx_ma.add_string(b"")
1561 elem_data_single_int32(fbx_ma, b"Version", FBX_MATERIAL_VERSION)
1562 # those are not yet properties, it seems...
1563 elem_data_single_string(fbx_ma, b"ShadingModel", ma_type)
1564 elem_data_single_int32(fbx_ma, b"MultiLayer", 0) # Should be bool...
1566 tmpl = elem_props_template_init(scene_data.templates, b"Material")
1567 props = elem_properties(fbx_ma)
1569 elem_props_template_set(tmpl, props, "p_string", b"ShadingModel", ma_type.decode())
1570 elem_props_template_set(tmpl, props, "p_color", b"DiffuseColor", ma_wrap.base_color)
1571 # Not in Principled BSDF, so assuming always 1
1572 elem_props_template_set(tmpl, props, "p_number", b"DiffuseFactor", 1.0)
1573 # Principled BSDF only has an emissive color, so we assume factor to be always 1.0.
1574 elem_props_template_set(tmpl, props, "p_color", b"EmissiveColor", ma_wrap.emission_color)
1575 elem_props_template_set(tmpl, props, "p_number", b"EmissiveFactor", ma_wrap.emission_strength)
1576 # Not in Principled BSDF, so assuming always 0
1577 elem_props_template_set(tmpl, props, "p_color", b"AmbientColor", ambient_color)
1578 elem_props_template_set(tmpl, props, "p_number", b"AmbientFactor", 0.0)
1579 # Sweetness... Looks like we are not the only ones to not know exactly how FBX is supposed to work (see T59850).
1580 # According to one of its developers, Unity uses that formula to extract alpha value:
1582 # alpha = 1 - TransparencyFactor
1583 # if (alpha == 1 or alpha == 0):
1584 # alpha = 1 - TransparentColor.r
1586 # Until further info, let's assume this is correct way to do, hence the following code for TransparentColor.
1587 if ma_wrap.alpha < 1.0e-5 or ma_wrap.alpha > (1.0 - 1.0e-5):
1588 elem_props_template_set(tmpl, props, "p_color", b"TransparentColor", (1.0 - ma_wrap.alpha,) * 3)
1589 else:
1590 elem_props_template_set(tmpl, props, "p_color", b"TransparentColor", ma_wrap.base_color)
1591 elem_props_template_set(tmpl, props, "p_number", b"TransparencyFactor", 1.0 - ma_wrap.alpha)
1592 elem_props_template_set(tmpl, props, "p_number", b"Opacity", ma_wrap.alpha)
1593 elem_props_template_set(tmpl, props, "p_vector_3d", b"NormalMap", (0.0, 0.0, 0.0))
1594 elem_props_template_set(tmpl, props, "p_double", b"BumpFactor", ma_wrap.normalmap_strength)
1595 # Not sure about those...
1597 b"Bump": ((0.0, 0.0, 0.0), "p_vector_3d"),
1598 b"DisplacementColor": ((0.0, 0.0, 0.0), "p_color_rgb"),
1599 b"DisplacementFactor": (0.0, "p_double"),
1601 # TODO: use specular tint?
1602 elem_props_template_set(tmpl, props, "p_color", b"SpecularColor", ma_wrap.base_color)
1603 elem_props_template_set(tmpl, props, "p_number", b"SpecularFactor", ma_wrap.specular / 2.0)
1604 # See Material template about those two!
1605 # XXX Totally empirical conversion, trying to adapt it
1606 # (from 0.0 - 100.0 FBX shininess range to 1.0 - 0.0 Principled BSDF range)...
1607 shininess = (1.0 - ma_wrap.roughness) * 10
1608 shininess *= shininess
1609 elem_props_template_set(tmpl, props, "p_number", b"Shininess", shininess)
1610 elem_props_template_set(tmpl, props, "p_number", b"ShininessExponent", shininess)
1611 elem_props_template_set(tmpl, props, "p_color", b"ReflectionColor", ma_wrap.base_color)
1612 elem_props_template_set(tmpl, props, "p_number", b"ReflectionFactor", ma_wrap.metallic)
1614 elem_props_template_finalize(tmpl, props)
1616 # Custom properties.
1617 if scene_data.settings.use_custom_props:
1618 fbx_data_element_custom_properties(props, ma)
1621 def _gen_vid_path(img, scene_data):
1622 msetts = scene_data.settings.media_settings
1623 fname_rel = bpy_extras.io_utils.path_reference(img.filepath, msetts.base_src, msetts.base_dst, msetts.path_mode,
1624 msetts.subdir, msetts.copy_set, img.library)
1625 fname_abs = os.path.normpath(os.path.abspath(os.path.join(msetts.base_dst, fname_rel)))
1626 return fname_abs, fname_rel
1629 def fbx_data_texture_file_elements(root, blender_tex_key, scene_data):
1631 Write the (file) Texture data block.
1633 # XXX All this is very fuzzy to me currently...
1634 # Textures do not seem to use properties as much as they could.
1635 # For now assuming most logical and simple stuff.
1637 ma, sock_name = blender_tex_key
1638 ma_wrap = node_shader_utils.PrincipledBSDFWrapper(ma, is_readonly=True)
1639 tex_key, _fbx_prop = scene_data.data_textures[blender_tex_key]
1640 tex = getattr(ma_wrap, sock_name)
1641 img = tex.image
1642 fname_abs, fname_rel = _gen_vid_path(img, scene_data)
1644 fbx_tex = elem_data_single_int64(root, b"Texture", get_fbx_uuid_from_key(tex_key))
1645 fbx_tex.add_string(fbx_name_class(sock_name.encode(), b"Texture"))
1646 fbx_tex.add_string(b"")
1648 elem_data_single_string(fbx_tex, b"Type", b"TextureVideoClip")
1649 elem_data_single_int32(fbx_tex, b"Version", FBX_TEXTURE_VERSION)
1650 elem_data_single_string(fbx_tex, b"TextureName", fbx_name_class(sock_name.encode(), b"Texture"))
1651 elem_data_single_string(fbx_tex, b"Media", fbx_name_class(img.name.encode(), b"Video"))
1652 elem_data_single_string_unicode(fbx_tex, b"FileName", fname_abs)
1653 elem_data_single_string_unicode(fbx_tex, b"RelativeFilename", fname_rel)
1655 alpha_source = 0 # None
1656 if img.alpha_mode != 'NONE':
1657 # ~ if tex.texture.use_calculate_alpha:
1658 # ~ alpha_source = 1 # RGBIntensity as alpha.
1659 # ~ else:
1660 # ~ alpha_source = 2 # Black, i.e. alpha channel.
1661 alpha_source = 2 # Black, i.e. alpha channel.
1662 # BlendMode not useful for now, only affects layered textures afaics.
1663 mapping = 0 # UV.
1664 uvset = None
1665 if tex.texcoords == 'ORCO': # XXX Others?
1666 if tex.projection == 'FLAT':
1667 mapping = 1 # Planar
1668 elif tex.projection == 'CUBE':
1669 mapping = 4 # Box
1670 elif tex.projection == 'TUBE':
1671 mapping = 3 # Cylindrical
1672 elif tex.projection == 'SPHERE':
1673 mapping = 2 # Spherical
1674 elif tex.texcoords == 'UV':
1675 mapping = 0 # UV
1676 # Yuck, UVs are linked by mere names it seems... :/
1677 # XXX TODO how to get that now???
1678 # uvset = tex.uv_layer
1679 wrap_mode = 1 # Clamp
1680 if tex.extension == 'REPEAT':
1681 wrap_mode = 0 # Repeat
1683 tmpl = elem_props_template_init(scene_data.templates, b"TextureFile")
1684 props = elem_properties(fbx_tex)
1685 elem_props_template_set(tmpl, props, "p_enum", b"AlphaSource", alpha_source)
1686 elem_props_template_set(tmpl, props, "p_bool", b"PremultiplyAlpha",
1687 img.alpha_mode in {'STRAIGHT'}) # Or is it PREMUL?
1688 elem_props_template_set(tmpl, props, "p_enum", b"CurrentMappingType", mapping)
1689 if uvset is not None:
1690 elem_props_template_set(tmpl, props, "p_string", b"UVSet", uvset)
1691 elem_props_template_set(tmpl, props, "p_enum", b"WrapModeU", wrap_mode)
1692 elem_props_template_set(tmpl, props, "p_enum", b"WrapModeV", wrap_mode)
1693 elem_props_template_set(tmpl, props, "p_vector_3d", b"Translation", tex.translation)
1694 elem_props_template_set(tmpl, props, "p_vector_3d", b"Rotation", (-r for r in tex.rotation))
1695 elem_props_template_set(tmpl, props, "p_vector_3d", b"Scaling", (((1.0 / s) if s != 0.0 else 1.0) for s in tex.scale))
1696 # UseMaterial should always be ON imho.
1697 elem_props_template_set(tmpl, props, "p_bool", b"UseMaterial", True)
1698 elem_props_template_set(tmpl, props, "p_bool", b"UseMipMap", False)
1699 elem_props_template_finalize(tmpl, props)
1701 # No custom properties, since that's not a data-block anymore.
1704 def fbx_data_video_elements(root, vid, scene_data):
1706 Write the actual image data block.
1708 msetts = scene_data.settings.media_settings
1710 vid_key, _texs = scene_data.data_videos[vid]
1711 fname_abs, fname_rel = _gen_vid_path(vid, scene_data)
1713 fbx_vid = elem_data_single_int64(root, b"Video", get_fbx_uuid_from_key(vid_key))
1714 fbx_vid.add_string(fbx_name_class(vid.name.encode(), b"Video"))
1715 fbx_vid.add_string(b"Clip")
1717 elem_data_single_string(fbx_vid, b"Type", b"Clip")
1718 # XXX No Version???
1720 tmpl = elem_props_template_init(scene_data.templates, b"Video")
1721 props = elem_properties(fbx_vid)
1722 elem_props_template_set(tmpl, props, "p_string_url", b"Path", fname_abs)
1723 elem_props_template_finalize(tmpl, props)
1725 elem_data_single_int32(fbx_vid, b"UseMipMap", 0)
1726 elem_data_single_string_unicode(fbx_vid, b"Filename", fname_abs)
1727 elem_data_single_string_unicode(fbx_vid, b"RelativeFilename", fname_rel)
1729 if scene_data.settings.media_settings.embed_textures:
1730 if vid.packed_file is not None:
1731 # We only ever embed a given file once!
1732 if fname_abs not in msetts.embedded_set:
1733 elem_data_single_bytes(fbx_vid, b"Content", vid.packed_file.data)
1734 msetts.embedded_set.add(fname_abs)
1735 else:
1736 filepath = bpy.path.abspath(vid.filepath)
1737 # We only ever embed a given file once!
1738 if filepath not in msetts.embedded_set:
1739 try:
1740 with open(filepath, 'br') as f:
1741 elem_data_single_bytes(fbx_vid, b"Content", f.read())
1742 except Exception as e:
1743 print("WARNING: embedding file {} failed ({})".format(filepath, e))
1744 elem_data_single_bytes(fbx_vid, b"Content", b"")
1745 msetts.embedded_set.add(filepath)
1746 # Looks like we'd rather not write any 'Content' element in this case (see T44442).
1747 # Sounds suspect, but let's try it!
1748 #~ else:
1749 #~ elem_data_single_bytes(fbx_vid, b"Content", b"")
1751 # Blender currently has no UI for editing custom properties on Images, but the importer will import Image custom
1752 # properties from either a Video Node or a Texture Node, preferring a Video node if one exists. We'll propagate
1753 # these custom properties only to Video Nodes because that is most likely where they were imported from, and Texture
1754 # Nodes are more like Blender's Shader Nodes than Images, which is what we're exporting here.
1755 if scene_data.settings.use_custom_props:
1756 fbx_data_element_custom_properties(props, vid)
1760 def fbx_data_armature_elements(root, arm_obj, scene_data):
1762 Write:
1763 * Bones "data" (NodeAttribute::LimbNode, contains pretty much nothing!).
1764 * Deformers (i.e. Skin), bind between an armature and a mesh.
1765 ** SubDeformers (i.e. Cluster), one per bone/vgroup pair.
1766 * BindPose.
1767 Note armature itself has no data, it is a mere "Null" Model...
1769 mat_world_arm = arm_obj.fbx_object_matrix(scene_data, global_space=True)
1770 bones = tuple(bo_obj for bo_obj in arm_obj.bones if bo_obj in scene_data.objects)
1772 bone_radius_scale = 33.0
1774 # Bones "data".
1775 for bo_obj in bones:
1776 bo = bo_obj.bdata
1777 bo_data_key = scene_data.data_bones[bo_obj]
1778 fbx_bo = elem_data_single_int64(root, b"NodeAttribute", get_fbx_uuid_from_key(bo_data_key))
1779 fbx_bo.add_string(fbx_name_class(bo.name.encode(), b"NodeAttribute"))
1780 fbx_bo.add_string(b"LimbNode")
1781 elem_data_single_string(fbx_bo, b"TypeFlags", b"Skeleton")
1783 tmpl = elem_props_template_init(scene_data.templates, b"Bone")
1784 props = elem_properties(fbx_bo)
1785 elem_props_template_set(tmpl, props, "p_double", b"Size", bo.head_radius * bone_radius_scale)
1786 elem_props_template_finalize(tmpl, props)
1788 # Custom properties.
1789 if scene_data.settings.use_custom_props:
1790 fbx_data_element_custom_properties(props, bo)
1792 # Store Blender bone length - XXX Not much useful actually :/
1793 # (LimbLength can't be used because it is a scale factor 0-1 for the parent-child distance:
1794 # http://docs.autodesk.com/FBX/2014/ENU/FBX-SDK-Documentation/cpp_ref/class_fbx_skeleton.html#a9bbe2a70f4ed82cd162620259e649f0f )
1795 # elem_props_set(props, "p_double", "BlenderBoneLength".encode(), (bo.tail_local - bo.head_local).length, custom=True)
1797 # Skin deformers and BindPoses.
1798 # Note: we might also use Deformers for our "parent to vertex" stuff???
1799 deformer = scene_data.data_deformers_skin.get(arm_obj, None)
1800 if deformer is not None:
1801 for me, (skin_key, ob_obj, clusters) in deformer.items():
1802 # BindPose.
1803 mat_world_obj, mat_world_bones = fbx_data_bindpose_element(root, ob_obj, me, scene_data,
1804 arm_obj, mat_world_arm, bones)
1806 # Deformer.
1807 fbx_skin = elem_data_single_int64(root, b"Deformer", get_fbx_uuid_from_key(skin_key))
1808 fbx_skin.add_string(fbx_name_class(arm_obj.name.encode(), b"Deformer"))
1809 fbx_skin.add_string(b"Skin")
1811 elem_data_single_int32(fbx_skin, b"Version", FBX_DEFORMER_SKIN_VERSION)
1812 elem_data_single_float64(fbx_skin, b"Link_DeformAcuracy", 50.0) # Only vague idea what it is...
1814 # Pre-process vertex weights so that the vertices only need to be iterated once.
1815 ob = ob_obj.bdata
1816 bo_vg_idx = {bo_obj.bdata.name: ob.vertex_groups[bo_obj.bdata.name].index
1817 for bo_obj in clusters.keys() if bo_obj.bdata.name in ob.vertex_groups}
1818 valid_idxs = set(bo_vg_idx.values())
1819 vgroups = {vg.index: {} for vg in ob.vertex_groups}
1820 for idx, v in enumerate(me.vertices):
1821 for vg in v.groups:
1822 if (w := vg.weight) and (vg_idx := vg.group) in valid_idxs:
1823 vgroups[vg_idx][idx] = w
1825 for bo_obj, clstr_key in clusters.items():
1826 bo = bo_obj.bdata
1827 # Find which vertices are affected by this bone/vgroup pair, and matching weights.
1828 # Note we still write a cluster for bones not affecting the mesh, to get 'rest pose' data
1829 # (the TransformBlah matrices).
1830 vg_idx = bo_vg_idx.get(bo.name, None)
1831 indices, weights = ((), ()) if vg_idx is None or not vgroups[vg_idx] else zip(*vgroups[vg_idx].items())
1833 # Create the cluster.
1834 fbx_clstr = elem_data_single_int64(root, b"Deformer", get_fbx_uuid_from_key(clstr_key))
1835 fbx_clstr.add_string(fbx_name_class(bo.name.encode(), b"SubDeformer"))
1836 fbx_clstr.add_string(b"Cluster")
1838 elem_data_single_int32(fbx_clstr, b"Version", FBX_DEFORMER_CLUSTER_VERSION)
1839 # No idea what that user data might be...
1840 fbx_userdata = elem_data_single_string(fbx_clstr, b"UserData", b"")
1841 fbx_userdata.add_string(b"")
1842 if indices:
1843 elem_data_single_int32_array(fbx_clstr, b"Indexes", indices)
1844 elem_data_single_float64_array(fbx_clstr, b"Weights", weights)
1845 # Transform, TransformLink and TransformAssociateModel matrices...
1846 # They seem to be doublons of BindPose ones??? Have armature (associatemodel) in addition, though.
1847 # WARNING! Even though official FBX API presents Transform in global space,
1848 # **it is stored in bone space in FBX data!** See:
1849 # http://area.autodesk.com/forum/autodesk-fbx/fbx-sdk/why-the-values-return-
1850 # by-fbxcluster-gettransformmatrix-x-not-same-with-the-value-in-ascii-fbx-file/
1851 elem_data_single_float64_array(fbx_clstr, b"Transform",
1852 matrix4_to_array(mat_world_bones[bo_obj].inverted_safe() @ mat_world_obj))
1853 elem_data_single_float64_array(fbx_clstr, b"TransformLink", matrix4_to_array(mat_world_bones[bo_obj]))
1854 elem_data_single_float64_array(fbx_clstr, b"TransformAssociateModel", matrix4_to_array(mat_world_arm))
1857 def fbx_data_leaf_bone_elements(root, scene_data):
1858 # Write a dummy leaf bone that is used by applications to show the length of the last bone in a chain
1859 for (node_name, _par_uuid, node_uuid, attr_uuid, matrix, hide, size) in scene_data.data_leaf_bones:
1860 # Bone 'data'...
1861 fbx_bo = elem_data_single_int64(root, b"NodeAttribute", attr_uuid)
1862 fbx_bo.add_string(fbx_name_class(node_name.encode(), b"NodeAttribute"))
1863 fbx_bo.add_string(b"LimbNode")
1864 elem_data_single_string(fbx_bo, b"TypeFlags", b"Skeleton")
1866 tmpl = elem_props_template_init(scene_data.templates, b"Bone")
1867 props = elem_properties(fbx_bo)
1868 elem_props_template_set(tmpl, props, "p_double", b"Size", size)
1869 elem_props_template_finalize(tmpl, props)
1871 # And bone object.
1872 model = elem_data_single_int64(root, b"Model", node_uuid)
1873 model.add_string(fbx_name_class(node_name.encode(), b"Model"))
1874 model.add_string(b"LimbNode")
1876 elem_data_single_int32(model, b"Version", FBX_MODELS_VERSION)
1878 # Object transform info.
1879 loc, rot, scale = matrix.decompose()
1880 rot = rot.to_euler('XYZ')
1881 rot = tuple(convert_rad_to_deg_iter(rot))
1883 tmpl = elem_props_template_init(scene_data.templates, b"Model")
1884 # For now add only loc/rot/scale...
1885 props = elem_properties(model)
1886 # Generated leaf bones are obviously never animated!
1887 elem_props_template_set(tmpl, props, "p_lcl_translation", b"Lcl Translation", loc)
1888 elem_props_template_set(tmpl, props, "p_lcl_rotation", b"Lcl Rotation", rot)
1889 elem_props_template_set(tmpl, props, "p_lcl_scaling", b"Lcl Scaling", scale)
1890 elem_props_template_set(tmpl, props, "p_visibility", b"Visibility", float(not hide))
1892 # Absolutely no idea what this is, but seems mandatory for validity of the file, and defaults to
1893 # invalid -1 value...
1894 elem_props_template_set(tmpl, props, "p_integer", b"DefaultAttributeIndex", 0)
1896 elem_props_template_set(tmpl, props, "p_enum", b"InheritType", 1) # RSrs
1898 # Those settings would obviously need to be edited in a complete version of the exporter, may depends on
1899 # object type, etc.
1900 elem_data_single_int32(model, b"MultiLayer", 0)
1901 elem_data_single_int32(model, b"MultiTake", 0)
1902 # Probably the FbxNode.EShadingMode enum. Full description in fbx_data_object_elements.
1903 elem_data_single_char(model, b"Shading", b"\x01")
1904 elem_data_single_string(model, b"Culling", b"CullingOff")
1906 elem_props_template_finalize(tmpl, props)
1909 def fbx_data_object_elements(root, ob_obj, scene_data):
1911 Write the Object (Model) data blocks.
1912 Note this "Model" can also be bone or dupli!
1914 obj_type = b"Null" # default, sort of empty...
1915 if ob_obj.is_bone:
1916 obj_type = b"LimbNode"
1917 elif (ob_obj.type == 'ARMATURE'):
1918 if scene_data.settings.armature_nodetype == 'ROOT':
1919 obj_type = b"Root"
1920 elif scene_data.settings.armature_nodetype == 'LIMBNODE':
1921 obj_type = b"LimbNode"
1922 else: # Default, preferred option...
1923 obj_type = b"Null"
1924 elif (ob_obj.type in BLENDER_OBJECT_TYPES_MESHLIKE):
1925 obj_type = b"Mesh"
1926 elif (ob_obj.type == 'LIGHT'):
1927 obj_type = b"Light"
1928 elif (ob_obj.type == 'CAMERA'):
1929 obj_type = b"Camera"
1930 model = elem_data_single_int64(root, b"Model", ob_obj.fbx_uuid)
1931 model.add_string(fbx_name_class(ob_obj.name.encode(), b"Model"))
1932 model.add_string(obj_type)
1934 elem_data_single_int32(model, b"Version", FBX_MODELS_VERSION)
1936 # Object transform info.
1937 loc, rot, scale, matrix, matrix_rot = ob_obj.fbx_object_tx(scene_data)
1938 rot = tuple(convert_rad_to_deg_iter(rot))
1940 tmpl = elem_props_template_init(scene_data.templates, b"Model")
1941 # For now add only loc/rot/scale...
1942 props = elem_properties(model)
1943 elem_props_template_set(tmpl, props, "p_lcl_translation", b"Lcl Translation", loc,
1944 animatable=True, animated=((ob_obj.key, "Lcl Translation") in scene_data.animated))
1945 elem_props_template_set(tmpl, props, "p_lcl_rotation", b"Lcl Rotation", rot,
1946 animatable=True, animated=((ob_obj.key, "Lcl Rotation") in scene_data.animated))
1947 elem_props_template_set(tmpl, props, "p_lcl_scaling", b"Lcl Scaling", scale,
1948 animatable=True, animated=((ob_obj.key, "Lcl Scaling") in scene_data.animated))
1949 elem_props_template_set(tmpl, props, "p_visibility", b"Visibility", float(not ob_obj.hide))
1951 # Absolutely no idea what this is, but seems mandatory for validity of the file, and defaults to
1952 # invalid -1 value...
1953 elem_props_template_set(tmpl, props, "p_integer", b"DefaultAttributeIndex", 0)
1955 elem_props_template_set(tmpl, props, "p_enum", b"InheritType", 1) # RSrs
1957 # Custom properties.
1958 if scene_data.settings.use_custom_props:
1959 # Here we want customprops from the 'pose' bone, not the 'edit' bone...
1960 bdata = ob_obj.bdata_pose_bone if ob_obj.is_bone else ob_obj.bdata
1961 fbx_data_element_custom_properties(props, bdata)
1963 # Those settings would obviously need to be edited in a complete version of the exporter, may depends on
1964 # object type, etc.
1965 elem_data_single_int32(model, b"MultiLayer", 0)
1966 elem_data_single_int32(model, b"MultiTake", 0)
1967 # This is probably the FbxNode.EShadingMode enum. Not directly used by the FBX SDK, but the SDK guarantees that the
1968 # value will be passed through from an imported file to an exported one. Common values are 'Y' and 'T'. 'U' and 'W'
1969 # have also been seen in older FBX files. It's not clear which enum member each of these values corresponds to or if
1970 # these values are actually application specific. Blender had been exporting this as a `True` bool for a long time
1971 # seemingly without issue. The '\x01' char is the same value as `True` in raw bytes.
1972 elem_data_single_char(model, b"Shading", b"\x01")
1973 elem_data_single_string(model, b"Culling", b"CullingOff")
1975 if obj_type == b"Camera":
1976 # Why, oh why are FBX cameras such a mess???
1977 # And WHY add camera data HERE??? Not even sure this is needed...
1978 render = scene_data.scene.render
1979 width = render.resolution_x * 1.0
1980 height = render.resolution_y * 1.0
1981 elem_props_template_set(tmpl, props, "p_enum", b"ResolutionMode", 0) # Don't know what it means
1982 elem_props_template_set(tmpl, props, "p_double", b"AspectW", width)
1983 elem_props_template_set(tmpl, props, "p_double", b"AspectH", height)
1984 elem_props_template_set(tmpl, props, "p_bool", b"ViewFrustum", True)
1985 elem_props_template_set(tmpl, props, "p_enum", b"BackgroundMode", 0) # Don't know what it means
1986 elem_props_template_set(tmpl, props, "p_bool", b"ForegroundTransparent", True)
1988 elem_props_template_finalize(tmpl, props)
1991 def fbx_data_animation_elements(root, scene_data):
1993 Write animation data.
1995 animations = scene_data.animations
1996 if not animations:
1997 return
1999 # Animation stacks.
2000 for astack_key, alayers, alayer_key, name, f_start, f_end in animations:
2001 astack = elem_data_single_int64(root, b"AnimationStack", get_fbx_uuid_from_key(astack_key))
2002 astack.add_string(fbx_name_class(name, b"AnimStack"))
2003 astack.add_string(b"")
2005 astack_tmpl = elem_props_template_init(scene_data.templates, b"AnimationStack")
2006 astack_props = elem_properties(astack)
2007 r = scene_data.scene.render
2008 fps = r.fps / r.fps_base
2009 start = int(convert_sec_to_ktime(f_start / fps))
2010 end = int(convert_sec_to_ktime(f_end / fps))
2011 elem_props_template_set(astack_tmpl, astack_props, "p_timestamp", b"LocalStart", start)
2012 elem_props_template_set(astack_tmpl, astack_props, "p_timestamp", b"LocalStop", end)
2013 elem_props_template_set(astack_tmpl, astack_props, "p_timestamp", b"ReferenceStart", start)
2014 elem_props_template_set(astack_tmpl, astack_props, "p_timestamp", b"ReferenceStop", end)
2015 elem_props_template_finalize(astack_tmpl, astack_props)
2017 # For now, only one layer for all animations.
2018 alayer = elem_data_single_int64(root, b"AnimationLayer", get_fbx_uuid_from_key(alayer_key))
2019 alayer.add_string(fbx_name_class(name, b"AnimLayer"))
2020 alayer.add_string(b"")
2022 for ob_obj, (alayer_key, acurvenodes) in alayers.items():
2023 # Animation layer.
2024 # alayer = elem_data_single_int64(root, b"AnimationLayer", get_fbx_uuid_from_key(alayer_key))
2025 # alayer.add_string(fbx_name_class(ob_obj.name.encode(), b"AnimLayer"))
2026 # alayer.add_string(b"")
2028 for fbx_prop, (acurvenode_key, acurves, acurvenode_name) in acurvenodes.items():
2029 # Animation curve node.
2030 acurvenode = elem_data_single_int64(root, b"AnimationCurveNode", get_fbx_uuid_from_key(acurvenode_key))
2031 acurvenode.add_string(fbx_name_class(acurvenode_name.encode(), b"AnimCurveNode"))
2032 acurvenode.add_string(b"")
2034 acn_tmpl = elem_props_template_init(scene_data.templates, b"AnimationCurveNode")
2035 acn_props = elem_properties(acurvenode)
2037 for fbx_item, (acurve_key, def_value, (keys, values), _acurve_valid) in acurves.items():
2038 elem_props_template_set(acn_tmpl, acn_props, "p_number", fbx_item.encode(),
2039 def_value, animatable=True)
2041 # Only create Animation curve if needed!
2042 nbr_keys = len(keys)
2043 if nbr_keys:
2044 acurve = elem_data_single_int64(root, b"AnimationCurve", get_fbx_uuid_from_key(acurve_key))
2045 acurve.add_string(fbx_name_class(b"", b"AnimCurve"))
2046 acurve.add_string(b"")
2048 # key attributes...
2049 # flags...
2050 keyattr_flags = (
2051 1 << 2 | # interpolation mode, 1 = constant, 2 = linear, 3 = cubic.
2052 1 << 8 | # tangent mode, 8 = auto, 9 = TCB, 10 = user, 11 = generic break,
2053 1 << 13 | # tangent mode, 12 = generic clamp, 13 = generic time independent,
2054 1 << 14 | # tangent mode, 13 + 14 = generic clamp progressive.
2057 # Maybe values controlling TCB & co???
2058 keyattr_datafloat = (0.0, 0.0, 9.419963346924634e-30, 0.0)
2060 # And now, the *real* data!
2061 elem_data_single_float64(acurve, b"Default", def_value)
2062 elem_data_single_int32(acurve, b"KeyVer", FBX_ANIM_KEY_VERSION)
2063 elem_data_single_int64_array(acurve, b"KeyTime", astype_view_signedness(keys, np.int64))
2064 elem_data_single_float32_array(acurve, b"KeyValueFloat", values.astype(np.float32, copy=False))
2065 elem_data_single_int32_array(acurve, b"KeyAttrFlags", keyattr_flags)
2066 elem_data_single_float32_array(acurve, b"KeyAttrDataFloat", keyattr_datafloat)
2067 elem_data_single_int32_array(acurve, b"KeyAttrRefCount", (nbr_keys,))
2069 elem_props_template_finalize(acn_tmpl, acn_props)
2072 # ##### Top-level FBX data container. #####
2074 # Mapping Blender -> FBX (principled_socket_name, fbx_name).
2075 PRINCIPLED_TEXTURE_SOCKETS_TO_FBX = (
2076 # ("diffuse", "diffuse", b"DiffuseFactor"),
2077 ("base_color_texture", b"DiffuseColor"),
2078 ("alpha_texture", b"TransparencyFactor"), # Will be inverted in fact, not much we can do really...
2079 # ("base_color_texture", b"TransparentColor"), # Uses diffuse color in Blender!
2080 ("emission_strength_texture", b"EmissiveFactor"),
2081 ("emission_color_texture", b"EmissiveColor"),
2082 # ("ambient", "ambient", b"AmbientFactor"),
2083 # ("", "", b"AmbientColor"), # World stuff in Blender, for now ignore...
2084 ("normalmap_texture", b"NormalMap"),
2085 # Note: unsure about those... :/
2086 # ("", "", b"Bump"),
2087 # ("", "", b"BumpFactor"),
2088 # ("", "", b"DisplacementColor"),
2089 # ("", "", b"DisplacementFactor"),
2090 ("specular_texture", b"SpecularFactor"),
2091 # ("base_color", b"SpecularColor"), # TODO: use tint?
2092 # See Material template about those two!
2093 ("roughness_texture", b"Shininess"),
2094 ("roughness_texture", b"ShininessExponent"),
2095 # ("mirror", "mirror", b"ReflectionColor"),
2096 ("metallic_texture", b"ReflectionFactor"),
2100 def fbx_skeleton_from_armature(scene, settings, arm_obj, objects, data_meshes,
2101 data_bones, data_deformers_skin, data_empties, arm_parents):
2103 Create skeleton from armature/bones (NodeAttribute/LimbNode and Model/LimbNode), and for each deformed mesh,
2104 create Pose/BindPose(with sub PoseNode) and Deformer/Skin(with Deformer/SubDeformer/Cluster).
2105 Also supports "parent to bone" (simple parent to Model/LimbNode).
2106 arm_parents is a set of tuples (armature, object) for all successful armature bindings.
2108 # We need some data for our armature 'object' too!!!
2109 data_empties[arm_obj] = get_blender_empty_key(arm_obj.bdata)
2111 arm_data = arm_obj.bdata.data
2112 bones = {}
2113 for bo in arm_obj.bones:
2114 if settings.use_armature_deform_only:
2115 if bo.bdata.use_deform:
2116 bones[bo] = True
2117 bo_par = bo.parent
2118 while bo_par.is_bone:
2119 bones[bo_par] = True
2120 bo_par = bo_par.parent
2121 elif bo not in bones: # Do not override if already set in the loop above!
2122 bones[bo] = False
2123 else:
2124 bones[bo] = True
2126 bones = {bo: None for bo, use in bones.items() if use}
2128 if not bones:
2129 return
2131 data_bones.update((bo, get_blender_bone_key(arm_obj.bdata, bo.bdata)) for bo in bones)
2133 for ob_obj in objects:
2134 if not ob_obj.is_deformed_by_armature(arm_obj):
2135 continue
2137 # Always handled by an Armature modifier...
2138 found = False
2139 for mod in ob_obj.bdata.modifiers:
2140 if mod.type not in {'ARMATURE'} or not mod.object:
2141 continue
2142 # We only support vertex groups binding method, not bone envelopes one!
2143 if mod.object == arm_obj.bdata and mod.use_vertex_groups:
2144 found = True
2145 break
2147 if not found:
2148 continue
2150 # Now we have a mesh using this armature.
2151 # Note: bindpose have no relations at all (no connections), so no need for any preprocess for them.
2152 # Create skin & clusters relations (note skins are connected to geometry, *not* model!).
2153 _key, me, _free = data_meshes[ob_obj]
2154 clusters = {bo: get_blender_bone_cluster_key(arm_obj.bdata, me, bo.bdata) for bo in bones}
2155 data_deformers_skin.setdefault(arm_obj, {})[me] = (get_blender_armature_skin_key(arm_obj.bdata, me),
2156 ob_obj, clusters)
2158 # We don't want a regular parent relationship for those in FBX...
2159 arm_parents.add((arm_obj, ob_obj))
2160 # Needed to handle matrices/spaces (since we do not parent them to 'armature' in FBX :/ ).
2161 ob_obj.parented_to_armature = True
2163 objects.update(bones)
2166 def fbx_generate_leaf_bones(settings, data_bones):
2167 # find which bons have no children
2168 child_count = {bo: 0 for bo in data_bones.keys()}
2169 for bo in data_bones.keys():
2170 if bo.parent and bo.parent.is_bone:
2171 child_count[bo.parent] += 1
2173 bone_radius_scale = settings.global_scale * 33.0
2175 # generate bone data
2176 leaf_parents = [bo for bo, count in child_count.items() if count == 0]
2177 leaf_bones = []
2178 for parent in leaf_parents:
2179 node_name = parent.name + "_end"
2180 parent_uuid = parent.fbx_uuid
2181 parent_key = parent.key
2182 node_uuid = get_fbx_uuid_from_key(parent_key + "_end_node")
2183 attr_uuid = get_fbx_uuid_from_key(parent_key + "_end_nodeattr")
2185 hide = parent.hide
2186 size = parent.bdata.head_radius * bone_radius_scale
2187 bone_length = (parent.bdata.tail_local - parent.bdata.head_local).length
2188 matrix = Matrix.Translation((0, bone_length, 0))
2189 if settings.bone_correction_matrix_inv:
2190 matrix = settings.bone_correction_matrix_inv @ matrix
2191 if settings.bone_correction_matrix:
2192 matrix = matrix @ settings.bone_correction_matrix
2193 leaf_bones.append((node_name, parent_uuid, node_uuid, attr_uuid, matrix, hide, size))
2195 return leaf_bones
2198 def fbx_animations_do(scene_data, ref_id, f_start, f_end, start_zero, objects=None, force_keep=False):
2200 Generate animation data (a single AnimStack) from objects, for a given frame range.
2202 bake_step = scene_data.settings.bake_anim_step
2203 simplify_fac = scene_data.settings.bake_anim_simplify_factor
2204 scene = scene_data.scene
2205 depsgraph = scene_data.depsgraph
2206 force_keying = scene_data.settings.bake_anim_use_all_bones
2207 force_sek = scene_data.settings.bake_anim_force_startend_keying
2208 gscale = scene_data.settings.global_scale
2210 if objects is not None:
2211 # Add bones and duplis!
2212 for ob_obj in tuple(objects):
2213 if not ob_obj.is_object:
2214 continue
2215 if ob_obj.type == 'ARMATURE':
2216 objects |= {bo_obj for bo_obj in ob_obj.bones if bo_obj in scene_data.objects}
2217 for dp_obj in ob_obj.dupli_list_gen(depsgraph):
2218 if dp_obj in scene_data.objects:
2219 objects.add(dp_obj)
2220 else:
2221 objects = scene_data.objects
2223 back_currframe = scene.frame_current
2224 animdata_ob = {}
2225 p_rots = {}
2227 for ob_obj in objects:
2228 if ob_obj.parented_to_armature:
2229 continue
2230 ACNW = AnimationCurveNodeWrapper
2231 loc, rot, scale, _m, _mr = ob_obj.fbx_object_tx(scene_data)
2232 rot_deg = tuple(convert_rad_to_deg_iter(rot))
2233 force_key = (simplify_fac == 0.0) or (ob_obj.is_bone and force_keying)
2234 animdata_ob[ob_obj] = (ACNW(ob_obj.key, 'LCL_TRANSLATION', force_key, force_sek, loc),
2235 ACNW(ob_obj.key, 'LCL_ROTATION', force_key, force_sek, rot_deg),
2236 ACNW(ob_obj.key, 'LCL_SCALING', force_key, force_sek, scale))
2237 p_rots[ob_obj] = rot
2239 force_key = (simplify_fac == 0.0)
2240 animdata_shapes = {}
2242 for me, (me_key, _shapes_key, shapes) in scene_data.data_deformers_shape.items():
2243 # Ignore absolute shape keys for now!
2244 if not me.shape_keys.use_relative:
2245 continue
2246 for shape, (channel_key, geom_key, _shape_verts_co, _shape_verts_idx) in shapes.items():
2247 acnode = AnimationCurveNodeWrapper(channel_key, 'SHAPE_KEY', force_key, force_sek, (0.0,))
2248 # Sooooo happy to have to twist again like a mad snake... Yes, we need to write those curves twice. :/
2249 acnode.add_group(me_key, shape.name, shape.name, (shape.name,))
2250 animdata_shapes[channel_key] = (acnode, me, shape)
2252 animdata_cameras = {}
2253 for cam_obj, cam_key in scene_data.data_cameras.items():
2254 cam = cam_obj.bdata.data
2255 acnode_lens = AnimationCurveNodeWrapper(cam_key, 'CAMERA_FOCAL', force_key, force_sek, (cam.lens,))
2256 acnode_focus_distance = AnimationCurveNodeWrapper(cam_key, 'CAMERA_FOCUS_DISTANCE', force_key,
2257 force_sek, (cam.dof.focus_distance,))
2258 animdata_cameras[cam_key] = (acnode_lens, acnode_focus_distance, cam)
2260 # Get all parent bdata of animated dupli instances, so that we can quickly identify which instances in
2261 # `depsgraph.object_instances` are animated and need their ObjectWrappers' matrices updated each frame.
2262 dupli_parent_bdata = {dup.get_parent().bdata for dup in animdata_ob if dup.is_dupli}
2263 has_animated_duplis = bool(dupli_parent_bdata)
2265 # Initialize keyframe times array. Each AnimationCurveNodeWrapper will share the same instance.
2266 # `np.arange` excludes the `stop` argument like when using `range`, so we use np.nextafter to get the next
2267 # representable value after f_end and use that as the `stop` argument instead.
2268 currframes = np.arange(f_start, np.nextafter(f_end, np.inf), step=bake_step)
2270 # Convert from Blender time to FBX time.
2271 fps = scene.render.fps / scene.render.fps_base
2272 real_currframes = currframes - f_start if start_zero else currframes
2273 real_currframes = (real_currframes / fps * FBX_KTIME).astype(np.int64)
2275 # Generator that yields the animated values of each frame in order.
2276 def frame_values_gen():
2277 # Precalculate integer frames and subframes.
2278 int_currframes = currframes.astype(int)
2279 subframes = currframes - int_currframes
2281 # Create simpler iterables that return only the values we care about.
2282 animdata_shapes_only = [shape for _anim_shape, _me, shape in animdata_shapes.values()]
2283 animdata_cameras_only = [camera for _anim_camera_lens, _anim_camera_focus_distance, camera
2284 in animdata_cameras.values()]
2285 # Previous frame's rotation for each object in animdata_ob, this will be updated each frame.
2286 animdata_ob_p_rots = p_rots.values()
2288 # Iterate through each frame and yield the values for that frame.
2289 # Iterating .data, the memoryview of an array, is faster than iterating the array directly.
2290 for int_currframe, subframe in zip(int_currframes.data, subframes.data):
2291 scene.frame_set(int_currframe, subframe=subframe)
2293 if has_animated_duplis:
2294 # Changing the scene's frame invalidates existing dupli instances. To get the updated matrices of duplis
2295 # for this frame, we must get the duplis from the depsgraph again.
2296 for dup in depsgraph.object_instances:
2297 if (parent := dup.parent) and parent.original in dupli_parent_bdata:
2298 # ObjectWrapper caches its instances. Attempting to create a new instance updates the existing
2299 # ObjectWrapper instance with the current frame's matrix and then returns the existing instance.
2300 ObjectWrapper(dup)
2301 next_p_rots = []
2302 for ob_obj, p_rot in zip(animdata_ob, animdata_ob_p_rots):
2303 # We compute baked loc/rot/scale for all objects (rot being euler-compat with previous value!).
2304 loc, rot, scale, _m, _mr = ob_obj.fbx_object_tx(scene_data, rot_euler_compat=p_rot)
2305 next_p_rots.append(rot)
2306 yield from loc
2307 yield from rot
2308 yield from scale
2309 animdata_ob_p_rots = next_p_rots
2310 for shape in animdata_shapes_only:
2311 yield shape.value
2312 for camera in animdata_cameras_only:
2313 yield camera.lens
2314 yield camera.dof.focus_distance
2316 # Providing `count` to np.fromiter pre-allocates the array, avoiding extra memory allocations while iterating.
2317 num_ob_values = len(animdata_ob) * 9 # Location, rotation and scale, each of which have x, y, and z components
2318 num_shape_values = len(animdata_shapes) # Only 1 value per shape key
2319 num_camera_values = len(animdata_cameras) * 2 # Focal length (`.lens`) and focus distance
2320 num_values_per_frame = num_ob_values + num_shape_values + num_camera_values
2321 num_frames = len(real_currframes)
2322 all_values_flat = np.fromiter(frame_values_gen(), dtype=float, count=num_frames * num_values_per_frame)
2324 # Restore the scene's current frame.
2325 scene.frame_set(back_currframe, subframe=0.0)
2327 # View such that each column is all values for a single frame and each row is all values for a single curve.
2328 all_values = all_values_flat.reshape(num_frames, num_values_per_frame).T
2329 # Split into views of the arrays for each curve type.
2330 split_at = [num_ob_values, num_shape_values, num_camera_values]
2331 # For unequal sized splits, np.split takes indices to split at, which can be acquired through a cumulative sum
2332 # across the list.
2333 # The last value isn't needed, because the last split is assumed to go to the end of the array.
2334 split_at = np.cumsum(split_at[:-1])
2335 all_ob_values, all_shape_key_values, all_camera_values = np.split(all_values, split_at)
2337 all_anims = []
2339 # Set location/rotation/scale curves.
2340 # Split into equal sized views of the arrays for each object.
2341 split_into = len(animdata_ob)
2342 per_ob_values = np.split(all_ob_values, split_into) if split_into > 0 else ()
2343 for anims, ob_values in zip(animdata_ob.values(), per_ob_values):
2344 # Split again into equal sized views of the location, rotation and scaling arrays.
2345 loc_xyz, rot_xyz, sca_xyz = np.split(ob_values, 3)
2346 # In-place convert from Blender rotation to FBX rotation.
2347 np.rad2deg(rot_xyz, out=rot_xyz)
2349 anim_loc, anim_rot, anim_scale = anims
2350 anim_loc.set_keyframes(real_currframes, loc_xyz)
2351 anim_rot.set_keyframes(real_currframes, rot_xyz)
2352 anim_scale.set_keyframes(real_currframes, sca_xyz)
2353 all_anims.extend(anims)
2355 # Set shape key curves.
2356 # There's only one array per shape key, so there's no need to split `all_shape_key_values`.
2357 for (anim_shape, _me, _shape), shape_key_values in zip(animdata_shapes.values(), all_shape_key_values):
2358 # In-place convert from Blender Shape Key Value to FBX Deform Percent.
2359 shape_key_values *= 100.0
2360 anim_shape.set_keyframes(real_currframes, shape_key_values)
2361 all_anims.append(anim_shape)
2363 # Set camera curves.
2364 # Split into equal sized views of the arrays for each camera.
2365 split_into = len(animdata_cameras)
2366 per_camera_values = np.split(all_camera_values, split_into) if split_into > 0 else ()
2367 zipped = zip(animdata_cameras.values(), per_camera_values)
2368 for (anim_camera_lens, anim_camera_focus_distance, _camera), (lens_values, focus_distance_values) in zipped:
2369 # In-place convert from Blender focus distance to FBX.
2370 focus_distance_values *= (1000 * gscale)
2371 anim_camera_lens.set_keyframes(real_currframes, lens_values)
2372 anim_camera_focus_distance.set_keyframes(real_currframes, focus_distance_values)
2373 all_anims.append(anim_camera_lens)
2374 all_anims.append(anim_camera_focus_distance)
2376 animations = {}
2378 # And now, produce final data (usable by FBX export code)
2379 for anim in all_anims:
2380 anim.simplify(simplify_fac, bake_step, force_keep)
2381 if not anim:
2382 continue
2383 for obj_key, group_key, group, fbx_group, fbx_gname in anim.get_final_data(scene, ref_id, force_keep):
2384 anim_data = animations.setdefault(obj_key, ("dummy_unused_key", {}))
2385 anim_data[1][fbx_group] = (group_key, group, fbx_gname)
2387 astack_key = get_blender_anim_stack_key(scene, ref_id)
2388 alayer_key = get_blender_anim_layer_key(scene, ref_id)
2389 name = (get_blenderID_name(ref_id) if ref_id else scene.name).encode()
2391 if start_zero:
2392 f_end -= f_start
2393 f_start = 0.0
2395 return (astack_key, animations, alayer_key, name, f_start, f_end) if animations else None
2398 def fbx_animations(scene_data):
2400 Generate global animation data from objects.
2402 scene = scene_data.scene
2403 animations = []
2404 animated = set()
2405 frame_start = 1e100
2406 frame_end = -1e100
2408 def add_anim(animations, animated, anim):
2409 nonlocal frame_start, frame_end
2410 if anim is not None:
2411 animations.append(anim)
2412 f_start, f_end = anim[4:6]
2413 if f_start < frame_start:
2414 frame_start = f_start
2415 if f_end > frame_end:
2416 frame_end = f_end
2418 _astack_key, astack, _alayer_key, _name, _fstart, _fend = anim
2419 for elem_key, (alayer_key, acurvenodes) in astack.items():
2420 for fbx_prop, (acurvenode_key, acurves, acurvenode_name) in acurvenodes.items():
2421 animated.add((elem_key, fbx_prop))
2423 # Per-NLA strip animstacks.
2424 if scene_data.settings.bake_anim_use_nla_strips:
2425 strips = []
2426 ob_actions = []
2427 for ob_obj in scene_data.objects:
2428 # NLA tracks only for objects, not bones!
2429 if not ob_obj.is_object:
2430 continue
2431 ob = ob_obj.bdata # Back to real Blender Object.
2432 if not ob.animation_data:
2433 continue
2435 # Some actions are read-only, one cause is being in NLA tweakmode
2436 restore_use_tweak_mode = ob.animation_data.use_tweak_mode
2437 if ob.animation_data.is_property_readonly('action'):
2438 ob.animation_data.use_tweak_mode = False
2440 # We have to remove active action from objects, it overwrites strips actions otherwise...
2441 ob_actions.append((ob, ob.animation_data.action, restore_use_tweak_mode))
2442 ob.animation_data.action = None
2443 for track in ob.animation_data.nla_tracks:
2444 if track.mute:
2445 continue
2446 for strip in track.strips:
2447 if strip.mute:
2448 continue
2449 strips.append(strip)
2450 strip.mute = True
2452 for strip in strips:
2453 strip.mute = False
2454 add_anim(animations, animated,
2455 fbx_animations_do(scene_data, strip, strip.frame_start, strip.frame_end, True, force_keep=True))
2456 strip.mute = True
2457 scene.frame_set(scene.frame_current, subframe=0.0)
2459 for strip in strips:
2460 strip.mute = False
2462 for ob, ob_act, restore_use_tweak_mode in ob_actions:
2463 ob.animation_data.action = ob_act
2464 ob.animation_data.use_tweak_mode = restore_use_tweak_mode
2466 # All actions.
2467 if scene_data.settings.bake_anim_use_all_actions:
2468 def validate_actions(act, path_resolve):
2469 for fc in act.fcurves:
2470 data_path = fc.data_path
2471 if fc.array_index:
2472 data_path = data_path + "[%d]" % fc.array_index
2473 try:
2474 path_resolve(data_path)
2475 except ValueError:
2476 return False # Invalid.
2477 return True # Valid.
2479 def restore_object(ob_to, ob_from):
2480 # Restore org state of object (ugh :/ ).
2481 props = (
2482 'location', 'rotation_quaternion', 'rotation_axis_angle', 'rotation_euler', 'rotation_mode', 'scale',
2483 'delta_location', 'delta_rotation_euler', 'delta_rotation_quaternion', 'delta_scale',
2484 'lock_location', 'lock_rotation', 'lock_rotation_w', 'lock_rotations_4d', 'lock_scale',
2485 'tag', 'track_axis', 'up_axis', 'active_material', 'active_material_index',
2486 'matrix_parent_inverse', 'empty_display_type', 'empty_display_size', 'empty_image_offset', 'pass_index',
2487 'color', 'hide_viewport', 'hide_select', 'hide_render', 'instance_type',
2488 'use_instance_vertices_rotation', 'use_instance_faces_scale', 'instance_faces_scale',
2489 'display_type', 'show_bounds', 'display_bounds_type', 'show_name', 'show_axis', 'show_texture_space',
2490 'show_wire', 'show_all_edges', 'show_transparent', 'show_in_front',
2491 'show_only_shape_key', 'use_shape_key_edit_mode', 'active_shape_key_index',
2493 for p in props:
2494 if not ob_to.is_property_readonly(p):
2495 setattr(ob_to, p, getattr(ob_from, p))
2497 for ob_obj in scene_data.objects:
2498 # Actions only for objects, not bones!
2499 if not ob_obj.is_object:
2500 continue
2502 ob = ob_obj.bdata # Back to real Blender Object.
2504 if not ob.animation_data:
2505 continue # Do not export animations for objects that are absolutely not animated, see T44386.
2507 if ob.animation_data.is_property_readonly('action'):
2508 continue # Cannot re-assign 'active action' to this object (usually related to NLA usage, see T48089).
2510 # We can't play with animdata and actions and get back to org state easily.
2511 # So we have to add a temp copy of the object to the scene, animate it, and remove it... :/
2512 ob_copy = ob.copy()
2513 # Great, have to handle bones as well if needed...
2514 pbones_matrices = [pbo.matrix_basis.copy() for pbo in ob.pose.bones] if ob.type == 'ARMATURE' else ...
2516 org_act = ob.animation_data.action
2517 path_resolve = ob.path_resolve
2519 for act in bpy.data.actions:
2520 # For now, *all* paths in the action must be valid for the object, to validate the action.
2521 # Unless that action was already assigned to the object!
2522 if act != org_act and not validate_actions(act, path_resolve):
2523 continue
2524 ob.animation_data.action = act
2525 frame_start, frame_end = act.frame_range # sic!
2526 add_anim(animations, animated,
2527 fbx_animations_do(scene_data, (ob, act), frame_start, frame_end, True,
2528 objects={ob_obj}, force_keep=True))
2529 # Ugly! :/
2530 if pbones_matrices is not ...:
2531 for pbo, mat in zip(ob.pose.bones, pbones_matrices):
2532 pbo.matrix_basis = mat.copy()
2533 ob.animation_data.action = org_act
2534 restore_object(ob, ob_copy)
2535 scene.frame_set(scene.frame_current, subframe=0.0)
2537 if pbones_matrices is not ...:
2538 for pbo, mat in zip(ob.pose.bones, pbones_matrices):
2539 pbo.matrix_basis = mat.copy()
2540 ob.animation_data.action = org_act
2542 bpy.data.objects.remove(ob_copy)
2543 scene.frame_set(scene.frame_current, subframe=0.0)
2545 # Global (containing everything) animstack, only if not exporting NLA strips and/or all actions.
2546 if not scene_data.settings.bake_anim_use_nla_strips and not scene_data.settings.bake_anim_use_all_actions:
2547 add_anim(animations, animated, fbx_animations_do(scene_data, None, scene.frame_start, scene.frame_end, False))
2549 # Be sure to update all matrices back to org state!
2550 scene.frame_set(scene.frame_current, subframe=0.0)
2552 return animations, animated, frame_start, frame_end
2555 def fbx_data_from_scene(scene, depsgraph, settings):
2557 Do some pre-processing over scene's data...
2559 objtypes = settings.object_types
2560 dp_objtypes = objtypes - {'ARMATURE'} # Armatures are not supported as dupli instances currently...
2561 perfmon = PerfMon()
2562 perfmon.level_up()
2564 # ##### Gathering data...
2566 perfmon.step("FBX export prepare: Wrapping Objects...")
2568 # This is rather simple for now, maybe we could end generating templates with most-used values
2569 # instead of default ones?
2570 objects = {} # Because we do not have any ordered set...
2571 for ob in settings.context_objects:
2572 if ob.type not in objtypes:
2573 continue
2574 ob_obj = ObjectWrapper(ob)
2575 objects[ob_obj] = None
2576 # Duplis...
2577 for dp_obj in ob_obj.dupli_list_gen(depsgraph):
2578 if dp_obj.type not in dp_objtypes:
2579 continue
2580 objects[dp_obj] = None
2582 perfmon.step("FBX export prepare: Wrapping Data (lamps, cameras, empties)...")
2584 data_lights = {ob_obj.bdata.data: get_blenderID_key(ob_obj.bdata.data)
2585 for ob_obj in objects if ob_obj.type == 'LIGHT'}
2586 # Unfortunately, FBX camera data contains object-level data (like position, orientation, etc.)...
2587 data_cameras = {ob_obj: get_blenderID_key(ob_obj.bdata.data)
2588 for ob_obj in objects if ob_obj.type == 'CAMERA'}
2589 # Yep! Contains nothing, but needed!
2590 data_empties = {ob_obj: get_blender_empty_key(ob_obj.bdata)
2591 for ob_obj in objects if ob_obj.type == 'EMPTY'}
2593 perfmon.step("FBX export prepare: Wrapping Meshes...")
2595 data_meshes = {}
2596 for ob_obj in objects:
2597 if ob_obj.type not in BLENDER_OBJECT_TYPES_MESHLIKE:
2598 continue
2599 ob = ob_obj.bdata
2600 org_ob_obj = None
2602 # Do not want to systematically recreate a new mesh for dupliobject instances, kind of break purpose of those.
2603 if ob_obj.is_dupli:
2604 org_ob_obj = ObjectWrapper(ob) # We get the "real" object wrapper from that dupli instance.
2605 if org_ob_obj in data_meshes:
2606 data_meshes[ob_obj] = data_meshes[org_ob_obj]
2607 continue
2609 # There are 4 different cases for what we need to do with the original data of each Object:
2610 # 1) The original data can be used without changes.
2611 # 2) A copy of the original data needs to be made.
2612 # - If an export option modifies the data, e.g. Triangulate Faces is enabled.
2613 # - If the Object has Object-linked materials. This is because our current mapping of materials to FBX requires
2614 # that multiple Objects sharing a single mesh must have the same materials.
2615 # 3) The Object needs to be converted to a mesh.
2616 # - All mesh-like Objects that are not meshes need to be converted to a mesh in order to be exported.
2617 # 4) The Object needs to be evaluated and then converted to a mesh.
2618 # - Whenever use_mesh_modifiers is enabled and either there are modifiers to apply or the Object needs to be
2619 # converted to a mesh.
2620 # If multiple cases apply to an Object, then only the last applicable case is relevant.
2621 do_copy = any(ms.link == 'OBJECT' for ms in ob.material_slots) or settings.use_triangles
2622 do_convert = ob.type in BLENDER_OTHER_OBJECT_TYPES
2623 do_evaluate = do_convert and settings.use_mesh_modifiers
2625 # If the Object is a mesh, and we're applying modifiers, check if there are actually any modifiers to apply.
2626 # If there are then the mesh will need to be evaluated, and we may need to make some temporary changes to the
2627 # modifiers or scene before the mesh is evaluated.
2628 backup_pose_positions = []
2629 tmp_mods = []
2630 if ob.type == 'MESH' and settings.use_mesh_modifiers:
2631 # No need to create a new mesh in this case, if no modifier is active!
2632 last_subsurf = None
2633 for mod in ob.modifiers:
2634 # For meshes, when armature export is enabled, disable Armature modifiers here!
2635 # XXX Temp hacks here since currently we only have access to a viewport depsgraph...
2637 # NOTE: We put armature to the rest pose instead of disabling it so we still
2638 # have vertex groups in the evaluated mesh.
2639 if mod.type == 'ARMATURE' and 'ARMATURE' in settings.object_types:
2640 object = mod.object
2641 if object and object.type == 'ARMATURE':
2642 armature = object.data
2643 # If armature is already in REST position, there's nothing to back-up
2644 # This cuts down on export time dramatically, if all armatures are already in REST position
2645 # by not triggering dependency graph update
2646 if armature.pose_position != 'REST':
2647 backup_pose_positions.append((armature, armature.pose_position))
2648 armature.pose_position = 'REST'
2649 elif mod.show_render or mod.show_viewport:
2650 # If exporting with subsurf collect the last Catmull-Clark subsurf modifier
2651 # and disable it. We can use the original data as long as this is the first
2652 # found applicable subsurf modifier.
2653 if settings.use_subsurf and mod.type == 'SUBSURF' and mod.subdivision_type == 'CATMULL_CLARK':
2654 if last_subsurf:
2655 do_evaluate = True
2656 last_subsurf = mod
2657 else:
2658 do_evaluate = True
2659 if settings.use_subsurf and last_subsurf:
2660 # XXX: When exporting with subsurf information temporarily disable
2661 # the last subsurf modifier.
2662 tmp_mods.append((last_subsurf, last_subsurf.show_render, last_subsurf.show_viewport))
2664 if do_evaluate:
2665 # If modifiers has been altered need to update dependency graph.
2666 if backup_pose_positions or tmp_mods:
2667 depsgraph.update()
2668 ob_to_convert = ob.evaluated_get(depsgraph)
2669 # NOTE: The dependency graph might be re-evaluating multiple times, which could
2670 # potentially free the mesh created early on. So we put those meshes to bmain and
2671 # free them afterwards. Not ideal but ensures correct ownership.
2672 tmp_me = bpy.data.meshes.new_from_object(
2673 ob_to_convert, preserve_all_data_layers=True, depsgraph=depsgraph)
2675 # Usually the materials of the evaluated object will be the same, but modifiers, such as Geometry Nodes,
2676 # can change the materials.
2677 orig_mats = tuple(slot.material for slot in ob.material_slots)
2678 eval_mats = tuple(slot.material.original if slot.material else None
2679 for slot in ob_to_convert.material_slots)
2680 if orig_mats != eval_mats:
2681 # Override the default behaviour of getting materials from ob_obj.bdata.material_slots.
2682 ob_obj.override_materials = eval_mats
2683 elif do_convert:
2684 tmp_me = bpy.data.meshes.new_from_object(ob, preserve_all_data_layers=True, depsgraph=depsgraph)
2685 elif do_copy:
2686 # bpy.data.meshes.new_from_object removes shape keys (see #104714), so create a copy of the mesh instead.
2687 tmp_me = ob.data.copy()
2688 else:
2689 tmp_me = None
2691 if tmp_me is None:
2692 # Use the original data of this Object.
2693 data_meshes[ob_obj] = (get_blenderID_key(ob.data), ob.data, False)
2694 else:
2695 # Triangulate the mesh if requested
2696 if settings.use_triangles:
2697 import bmesh
2698 bm = bmesh.new()
2699 bm.from_mesh(tmp_me)
2700 bmesh.ops.triangulate(bm, faces=bm.faces)
2701 bm.to_mesh(tmp_me)
2702 bm.free()
2703 # A temporary mesh was created for this Object, which should be deleted once the export is complete.
2704 data_meshes[ob_obj] = (get_blenderID_key(tmp_me), tmp_me, True)
2706 # Change armatures back.
2707 for armature, pose_position in backup_pose_positions:
2708 print((armature, pose_position))
2709 armature.pose_position = pose_position
2710 # Update now, so we don't leave modified state after last object was exported.
2711 # Re-enable temporary disabled modifiers.
2712 for mod, show_render, show_viewport in tmp_mods:
2713 mod.show_render = show_render
2714 mod.show_viewport = show_viewport
2715 if backup_pose_positions or tmp_mods:
2716 depsgraph.update()
2718 # In case "real" source object of that dupli did not yet still existed in data_meshes, create it now!
2719 if org_ob_obj is not None:
2720 data_meshes[org_ob_obj] = data_meshes[ob_obj]
2722 perfmon.step("FBX export prepare: Wrapping ShapeKeys...")
2724 # ShapeKeys.
2725 data_deformers_shape = {}
2726 geom_mat_co = settings.global_matrix if settings.bake_space_transform else None
2727 co_bl_dtype = np.single
2728 co_fbx_dtype = np.float64
2729 idx_fbx_dtype = np.int32
2731 def empty_verts_fallbacks():
2732 """Create fallback arrays for when there are no verts"""
2733 # FBX does not like empty shapes (makes Unity crash e.g.).
2734 # To prevent this, we add a vertex that does nothing, but it keeps the shape key intact
2735 single_vert_co = np.zeros((1, 3), dtype=co_fbx_dtype)
2736 single_vert_idx = np.zeros(1, dtype=idx_fbx_dtype)
2737 return single_vert_co, single_vert_idx
2739 for me_key, me, _free in data_meshes.values():
2740 if not (me.shape_keys and len(me.shape_keys.key_blocks) > 1): # We do not want basis-only relative skeys...
2741 continue
2742 if me in data_deformers_shape:
2743 continue
2745 shapes_key = get_blender_mesh_shape_key(me)
2747 sk_base = me.shape_keys.key_blocks[0]
2749 # Get and cache only the cos that we need
2750 @cache
2751 def sk_cos(shape_key):
2752 if shape_key == sk_base:
2753 _cos = MESH_ATTRIBUTE_POSITION.to_ndarray(me.attributes)
2754 else:
2755 _cos = np.empty(len(me.vertices) * 3, dtype=co_bl_dtype)
2756 shape_key.data.foreach_get("co", _cos)
2757 return vcos_transformed(_cos, geom_mat_co, co_fbx_dtype)
2759 for shape in me.shape_keys.key_blocks[1:]:
2760 # Only write vertices really different from base coordinates!
2761 relative_key = shape.relative_key
2762 if shape == relative_key:
2763 # Shape is its own relative key, so it does nothing
2764 shape_verts_co, shape_verts_idx = empty_verts_fallbacks()
2765 else:
2766 sv_cos = sk_cos(shape)
2767 ref_cos = sk_cos(shape.relative_key)
2769 # Exclude cos similar to ref_cos and get the indices of the cos that remain
2770 shape_verts_co, shape_verts_idx = shape_difference_exclude_similar(sv_cos, ref_cos)
2772 if not shape_verts_co.size:
2773 shape_verts_co, shape_verts_idx = empty_verts_fallbacks()
2774 else:
2775 # Ensure the indices are of the correct type
2776 shape_verts_idx = astype_view_signedness(shape_verts_idx, idx_fbx_dtype)
2778 channel_key, geom_key = get_blender_mesh_shape_channel_key(me, shape)
2779 data = (channel_key, geom_key, shape_verts_co, shape_verts_idx)
2780 data_deformers_shape.setdefault(me, (me_key, shapes_key, {}))[2][shape] = data
2782 del sk_cos
2784 perfmon.step("FBX export prepare: Wrapping Armatures...")
2786 # Armatures!
2787 data_deformers_skin = {}
2788 data_bones = {}
2789 arm_parents = set()
2790 for ob_obj in tuple(objects):
2791 if not (ob_obj.is_object and ob_obj.type in {'ARMATURE'}):
2792 continue
2793 fbx_skeleton_from_armature(scene, settings, ob_obj, objects, data_meshes,
2794 data_bones, data_deformers_skin, data_empties, arm_parents)
2796 # Generate leaf bones
2797 data_leaf_bones = []
2798 if settings.add_leaf_bones:
2799 data_leaf_bones = fbx_generate_leaf_bones(settings, data_bones)
2801 perfmon.step("FBX export prepare: Wrapping World...")
2803 # Some world settings are embedded in FBX materials...
2804 if scene.world:
2805 data_world = {scene.world: get_blenderID_key(scene.world)}
2806 else:
2807 data_world = {}
2809 perfmon.step("FBX export prepare: Wrapping Materials...")
2811 # TODO: Check all the material stuff works even when they are linked to Objects
2812 # (we can then have the same mesh used with different materials...).
2813 # *Should* work, as FBX always links its materials to Models (i.e. objects).
2814 # XXX However, material indices would probably break...
2815 data_materials = {}
2816 for ob_obj in objects:
2817 # If obj is not a valid object for materials, wrapper will just return an empty tuple...
2818 for ma in ob_obj.materials:
2819 if ma is None:
2820 continue # Empty slots!
2821 # Note theoretically, FBX supports any kind of materials, even GLSL shaders etc.
2822 # However, I doubt anything else than Lambert/Phong is really portable!
2823 # Note we want to keep a 'dummy' empty material even when we can't really support it, see T41396.
2824 ma_data = data_materials.setdefault(ma, (get_blenderID_key(ma), []))
2825 ma_data[1].append(ob_obj)
2827 perfmon.step("FBX export prepare: Wrapping Textures...")
2829 # Note FBX textures also hold their mapping info.
2830 # TODO: Support layers?
2831 data_textures = {}
2832 # FbxVideo also used to store static images...
2833 data_videos = {}
2834 # For now, do not use world textures, don't think they can be linked to anything FBX wise...
2835 for ma in data_materials.keys():
2836 # Note: with nodal shaders, we'll could be generating much more textures, but that's kind of unavoidable,
2837 # given that textures actually do not exist anymore in material context in Blender...
2838 ma_wrap = node_shader_utils.PrincipledBSDFWrapper(ma, is_readonly=True)
2839 for sock_name, fbx_name in PRINCIPLED_TEXTURE_SOCKETS_TO_FBX:
2840 tex = getattr(ma_wrap, sock_name)
2841 if tex is None or tex.image is None:
2842 continue
2843 blender_tex_key = (ma, sock_name)
2844 data_textures[blender_tex_key] = (get_blender_nodetexture_key(*blender_tex_key), fbx_name)
2846 img = tex.image
2847 vid_data = data_videos.setdefault(img, (get_blenderID_key(img), []))
2848 vid_data[1].append(blender_tex_key)
2850 perfmon.step("FBX export prepare: Wrapping Animations...")
2852 # Animation...
2853 animations = ()
2854 animated = set()
2855 frame_start = scene.frame_start
2856 frame_end = scene.frame_end
2857 if settings.bake_anim:
2858 # From objects & bones only for a start.
2859 # Kind of hack, we need a temp scene_data for object's space handling to bake animations...
2860 tmp_scdata = FBXExportData(
2861 None, None, None,
2862 settings, scene, depsgraph, objects, None, None, 0.0, 0.0,
2863 data_empties, data_lights, data_cameras, data_meshes, None,
2864 data_bones, data_leaf_bones, data_deformers_skin, data_deformers_shape,
2865 data_world, data_materials, data_textures, data_videos,
2867 animations, animated, frame_start, frame_end = fbx_animations(tmp_scdata)
2869 # ##### Creation of templates...
2871 perfmon.step("FBX export prepare: Generating templates...")
2873 templates = {}
2874 templates[b"GlobalSettings"] = fbx_template_def_globalsettings(scene, settings, nbr_users=1)
2876 if data_empties:
2877 templates[b"Null"] = fbx_template_def_null(scene, settings, nbr_users=len(data_empties))
2879 if data_lights:
2880 templates[b"Light"] = fbx_template_def_light(scene, settings, nbr_users=len(data_lights))
2882 if data_cameras:
2883 templates[b"Camera"] = fbx_template_def_camera(scene, settings, nbr_users=len(data_cameras))
2885 if data_bones:
2886 templates[b"Bone"] = fbx_template_def_bone(scene, settings, nbr_users=len(data_bones))
2888 if data_meshes:
2889 nbr = len({me_key for me_key, _me, _free in data_meshes.values()})
2890 if data_deformers_shape:
2891 nbr += sum(len(shapes[2]) for shapes in data_deformers_shape.values())
2892 templates[b"Geometry"] = fbx_template_def_geometry(scene, settings, nbr_users=nbr)
2894 if objects:
2895 templates[b"Model"] = fbx_template_def_model(scene, settings, nbr_users=len(objects))
2897 if arm_parents:
2898 # Number of Pose|BindPose elements should be the same as number of meshes-parented-to-armatures
2899 templates[b"BindPose"] = fbx_template_def_pose(scene, settings, nbr_users=len(arm_parents))
2901 if data_deformers_skin or data_deformers_shape:
2902 nbr = 0
2903 if data_deformers_skin:
2904 nbr += len(data_deformers_skin)
2905 nbr += sum(len(clusters) for def_me in data_deformers_skin.values() for a, b, clusters in def_me.values())
2906 if data_deformers_shape:
2907 nbr += len(data_deformers_shape)
2908 nbr += sum(len(shapes[2]) for shapes in data_deformers_shape.values())
2909 assert(nbr != 0)
2910 templates[b"Deformers"] = fbx_template_def_deformer(scene, settings, nbr_users=nbr)
2912 # No world support in FBX...
2914 if data_world:
2915 templates[b"World"] = fbx_template_def_world(scene, settings, nbr_users=len(data_world))
2918 if data_materials:
2919 templates[b"Material"] = fbx_template_def_material(scene, settings, nbr_users=len(data_materials))
2921 if data_textures:
2922 templates[b"TextureFile"] = fbx_template_def_texture_file(scene, settings, nbr_users=len(data_textures))
2924 if data_videos:
2925 templates[b"Video"] = fbx_template_def_video(scene, settings, nbr_users=len(data_videos))
2927 if animations:
2928 nbr_astacks = len(animations)
2929 nbr_acnodes = 0
2930 nbr_acurves = 0
2931 for _astack_key, astack, _al, _n, _fs, _fe in animations:
2932 for _alayer_key, alayer in astack.values():
2933 for _acnode_key, acnode, _acnode_name in alayer.values():
2934 nbr_acnodes += 1
2935 for _acurve_key, _dval, (keys, _values), acurve_valid in acnode.values():
2936 if len(keys):
2937 nbr_acurves += 1
2939 templates[b"AnimationStack"] = fbx_template_def_animstack(scene, settings, nbr_users=nbr_astacks)
2940 # Would be nice to have one layer per animated object, but this seems tricky and not that well supported.
2941 # So for now, only one layer per anim stack.
2942 templates[b"AnimationLayer"] = fbx_template_def_animlayer(scene, settings, nbr_users=nbr_astacks)
2943 templates[b"AnimationCurveNode"] = fbx_template_def_animcurvenode(scene, settings, nbr_users=nbr_acnodes)
2944 templates[b"AnimationCurve"] = fbx_template_def_animcurve(scene, settings, nbr_users=nbr_acurves)
2946 templates_users = sum(tmpl.nbr_users for tmpl in templates.values())
2948 # ##### Creation of connections...
2950 perfmon.step("FBX export prepare: Generating Connections...")
2952 connections = []
2954 # Objects (with classical parenting).
2955 for ob_obj in objects:
2956 # Bones are handled later.
2957 if not ob_obj.is_bone:
2958 par_obj = ob_obj.parent
2959 # Meshes parented to armature are handled separately, yet we want the 'no parent' connection (0).
2960 if par_obj and ob_obj.has_valid_parent(objects) and (par_obj, ob_obj) not in arm_parents:
2961 connections.append((b"OO", ob_obj.fbx_uuid, par_obj.fbx_uuid, None))
2962 else:
2963 connections.append((b"OO", ob_obj.fbx_uuid, 0, None))
2965 # Armature & Bone chains.
2966 for bo_obj in data_bones.keys():
2967 par_obj = bo_obj.parent
2968 if par_obj not in objects:
2969 continue
2970 connections.append((b"OO", bo_obj.fbx_uuid, par_obj.fbx_uuid, None))
2972 # Object data.
2973 for ob_obj in objects:
2974 if ob_obj.is_bone:
2975 bo_data_key = data_bones[ob_obj]
2976 connections.append((b"OO", get_fbx_uuid_from_key(bo_data_key), ob_obj.fbx_uuid, None))
2977 else:
2978 if ob_obj.type == 'LIGHT':
2979 light_key = data_lights[ob_obj.bdata.data]
2980 connections.append((b"OO", get_fbx_uuid_from_key(light_key), ob_obj.fbx_uuid, None))
2981 elif ob_obj.type == 'CAMERA':
2982 cam_key = data_cameras[ob_obj]
2983 connections.append((b"OO", get_fbx_uuid_from_key(cam_key), ob_obj.fbx_uuid, None))
2984 elif ob_obj.type == 'EMPTY' or ob_obj.type == 'ARMATURE':
2985 empty_key = data_empties[ob_obj]
2986 connections.append((b"OO", get_fbx_uuid_from_key(empty_key), ob_obj.fbx_uuid, None))
2987 elif ob_obj.type in BLENDER_OBJECT_TYPES_MESHLIKE:
2988 mesh_key, _me, _free = data_meshes[ob_obj]
2989 connections.append((b"OO", get_fbx_uuid_from_key(mesh_key), ob_obj.fbx_uuid, None))
2991 # Leaf Bones
2992 for (_node_name, par_uuid, node_uuid, attr_uuid, _matrix, _hide, _size) in data_leaf_bones:
2993 connections.append((b"OO", node_uuid, par_uuid, None))
2994 connections.append((b"OO", attr_uuid, node_uuid, None))
2996 # 'Shape' deformers (shape keys, only for meshes currently)...
2997 for me_key, shapes_key, shapes in data_deformers_shape.values():
2998 # shape -> geometry
2999 connections.append((b"OO", get_fbx_uuid_from_key(shapes_key), get_fbx_uuid_from_key(me_key), None))
3000 for channel_key, geom_key, _shape_verts_co, _shape_verts_idx in shapes.values():
3001 # shape channel -> shape
3002 connections.append((b"OO", get_fbx_uuid_from_key(channel_key), get_fbx_uuid_from_key(shapes_key), None))
3003 # geometry (keys) -> shape channel
3004 connections.append((b"OO", get_fbx_uuid_from_key(geom_key), get_fbx_uuid_from_key(channel_key), None))
3006 # 'Skin' deformers (armature-to-geometry, only for meshes currently)...
3007 for arm, deformed_meshes in data_deformers_skin.items():
3008 for me, (skin_key, ob_obj, clusters) in deformed_meshes.items():
3009 # skin -> geometry
3010 mesh_key, _me, _free = data_meshes[ob_obj]
3011 assert(me == _me)
3012 connections.append((b"OO", get_fbx_uuid_from_key(skin_key), get_fbx_uuid_from_key(mesh_key), None))
3013 for bo_obj, clstr_key in clusters.items():
3014 # cluster -> skin
3015 connections.append((b"OO", get_fbx_uuid_from_key(clstr_key), get_fbx_uuid_from_key(skin_key), None))
3016 # bone -> cluster
3017 connections.append((b"OO", bo_obj.fbx_uuid, get_fbx_uuid_from_key(clstr_key), None))
3019 # Materials
3020 mesh_material_indices = {}
3021 _objs_indices = {}
3022 for ma, (ma_key, ob_objs) in data_materials.items():
3023 for ob_obj in ob_objs:
3024 connections.append((b"OO", get_fbx_uuid_from_key(ma_key), ob_obj.fbx_uuid, None))
3025 # Get index of this material for this object (or dupliobject).
3026 # Material indices for mesh faces are determined by their order in 'ma to ob' connections.
3027 # Only materials for meshes currently...
3028 # Note in case of dupliobjects a same me/ma idx will be generated several times...
3029 # Should not be an issue in practice, and it's needed in case we export duplis but not the original!
3030 if ob_obj.type not in BLENDER_OBJECT_TYPES_MESHLIKE:
3031 continue
3032 _mesh_key, me, _free = data_meshes[ob_obj]
3033 idx = _objs_indices[ob_obj] = _objs_indices.get(ob_obj, -1) + 1
3034 # XXX If a mesh has multiple material slots with the same material, they are combined into one slot.
3035 # Even if duplicate materials were exported without combining them into one slot, keeping duplicate
3036 # materials separated does not appear to be common behaviour of external software when importing FBX.
3037 mesh_material_indices.setdefault(me, {})[ma] = idx
3038 del _objs_indices
3040 # Textures
3041 for (ma, sock_name), (tex_key, fbx_prop) in data_textures.items():
3042 ma_key, _ob_objs = data_materials[ma]
3043 # texture -> material properties
3044 connections.append((b"OP", get_fbx_uuid_from_key(tex_key), get_fbx_uuid_from_key(ma_key), fbx_prop))
3046 # Images
3047 for vid, (vid_key, blender_tex_keys) in data_videos.items():
3048 for blender_tex_key in blender_tex_keys:
3049 tex_key, _fbx_prop = data_textures[blender_tex_key]
3050 connections.append((b"OO", get_fbx_uuid_from_key(vid_key), get_fbx_uuid_from_key(tex_key), None))
3052 # Animations
3053 for astack_key, astack, alayer_key, _name, _fstart, _fend in animations:
3054 # Animstack itself is linked nowhere!
3055 astack_id = get_fbx_uuid_from_key(astack_key)
3056 # For now, only one layer!
3057 alayer_id = get_fbx_uuid_from_key(alayer_key)
3058 connections.append((b"OO", alayer_id, astack_id, None))
3059 for elem_key, (alayer_key, acurvenodes) in astack.items():
3060 elem_id = get_fbx_uuid_from_key(elem_key)
3061 # Animlayer -> animstack.
3062 # alayer_id = get_fbx_uuid_from_key(alayer_key)
3063 # connections.append((b"OO", alayer_id, astack_id, None))
3064 for fbx_prop, (acurvenode_key, acurves, acurvenode_name) in acurvenodes.items():
3065 # Animcurvenode -> animalayer.
3066 acurvenode_id = get_fbx_uuid_from_key(acurvenode_key)
3067 connections.append((b"OO", acurvenode_id, alayer_id, None))
3068 # Animcurvenode -> object property.
3069 connections.append((b"OP", acurvenode_id, elem_id, fbx_prop.encode()))
3070 for fbx_item, (acurve_key, default_value, (keys, values), acurve_valid) in acurves.items():
3071 if len(keys):
3072 # Animcurve -> Animcurvenode.
3073 connections.append((b"OP", get_fbx_uuid_from_key(acurve_key), acurvenode_id, fbx_item.encode()))
3075 perfmon.level_down()
3077 # ##### And pack all this!
3079 return FBXExportData(
3080 templates, templates_users, connections,
3081 settings, scene, depsgraph, objects, animations, animated, frame_start, frame_end,
3082 data_empties, data_lights, data_cameras, data_meshes, mesh_material_indices,
3083 data_bones, data_leaf_bones, data_deformers_skin, data_deformers_shape,
3084 data_world, data_materials, data_textures, data_videos,
3088 def fbx_scene_data_cleanup(scene_data):
3090 Some final cleanup...
3092 # Delete temp meshes.
3093 done_meshes = set()
3094 for me_key, me, free in scene_data.data_meshes.values():
3095 if free and me_key not in done_meshes:
3096 bpy.data.meshes.remove(me)
3097 done_meshes.add(me_key)
3100 # ##### Top-level FBX elements generators. #####
3102 def fbx_header_elements(root, scene_data, time=None):
3104 Write boiling code of FBX root.
3105 time is expected to be a datetime.datetime object, or None (using now() in this case).
3107 app_vendor = "Blender Foundation"
3108 app_name = "Blender (stable FBX IO)"
3109 app_ver = bpy.app.version_string
3111 import addon_utils
3112 import sys
3113 addon_ver = addon_utils.module_bl_info(sys.modules[__package__])['version']
3115 # ##### Start of FBXHeaderExtension element.
3116 header_ext = elem_empty(root, b"FBXHeaderExtension")
3118 elem_data_single_int32(header_ext, b"FBXHeaderVersion", FBX_HEADER_VERSION)
3120 elem_data_single_int32(header_ext, b"FBXVersion", FBX_VERSION)
3122 # No encryption!
3123 elem_data_single_int32(header_ext, b"EncryptionType", 0)
3125 if time is None:
3126 time = datetime.datetime.now()
3127 elem = elem_empty(header_ext, b"CreationTimeStamp")
3128 elem_data_single_int32(elem, b"Version", 1000)
3129 elem_data_single_int32(elem, b"Year", time.year)
3130 elem_data_single_int32(elem, b"Month", time.month)
3131 elem_data_single_int32(elem, b"Day", time.day)
3132 elem_data_single_int32(elem, b"Hour", time.hour)
3133 elem_data_single_int32(elem, b"Minute", time.minute)
3134 elem_data_single_int32(elem, b"Second", time.second)
3135 elem_data_single_int32(elem, b"Millisecond", time.microsecond // 1000)
3137 elem_data_single_string_unicode(header_ext, b"Creator", "%s - %s - %d.%d.%d"
3138 % (app_name, app_ver, addon_ver[0], addon_ver[1], addon_ver[2]))
3140 # 'SceneInfo' seems mandatory to get a valid FBX file...
3141 # TODO use real values!
3142 # XXX Should we use scene.name.encode() here?
3143 scene_info = elem_data_single_string(header_ext, b"SceneInfo", fbx_name_class(b"GlobalInfo", b"SceneInfo"))
3144 scene_info.add_string(b"UserData")
3145 elem_data_single_string(scene_info, b"Type", b"UserData")
3146 elem_data_single_int32(scene_info, b"Version", FBX_SCENEINFO_VERSION)
3147 meta_data = elem_empty(scene_info, b"MetaData")
3148 elem_data_single_int32(meta_data, b"Version", FBX_SCENEINFO_VERSION)
3149 elem_data_single_string(meta_data, b"Title", b"")
3150 elem_data_single_string(meta_data, b"Subject", b"")
3151 elem_data_single_string(meta_data, b"Author", b"")
3152 elem_data_single_string(meta_data, b"Keywords", b"")
3153 elem_data_single_string(meta_data, b"Revision", b"")
3154 elem_data_single_string(meta_data, b"Comment", b"")
3156 props = elem_properties(scene_info)
3157 elem_props_set(props, "p_string_url", b"DocumentUrl", "/foobar.fbx")
3158 elem_props_set(props, "p_string_url", b"SrcDocumentUrl", "/foobar.fbx")
3159 original = elem_props_compound(props, b"Original")
3160 original("p_string", b"ApplicationVendor", app_vendor)
3161 original("p_string", b"ApplicationName", app_name)
3162 original("p_string", b"ApplicationVersion", app_ver)
3163 original("p_datetime", b"DateTime_GMT", "01/01/1970 00:00:00.000")
3164 original("p_string", b"FileName", "/foobar.fbx")
3165 lastsaved = elem_props_compound(props, b"LastSaved")
3166 lastsaved("p_string", b"ApplicationVendor", app_vendor)
3167 lastsaved("p_string", b"ApplicationName", app_name)
3168 lastsaved("p_string", b"ApplicationVersion", app_ver)
3169 lastsaved("p_datetime", b"DateTime_GMT", "01/01/1970 00:00:00.000")
3170 original("p_string", b"ApplicationNativeFile", bpy.data.filepath)
3172 # ##### End of FBXHeaderExtension element.
3174 # FileID is replaced by dummy value currently...
3175 elem_data_single_bytes(root, b"FileId", b"FooBar")
3177 # CreationTime is replaced by dummy value currently, but anyway...
3178 elem_data_single_string_unicode(root, b"CreationTime",
3179 "{:04}-{:02}-{:02} {:02}:{:02}:{:02}:{:03}"
3180 "".format(time.year, time.month, time.day, time.hour, time.minute, time.second,
3181 time.microsecond * 1000))
3183 elem_data_single_string_unicode(root, b"Creator", "%s - %s - %d.%d.%d"
3184 % (app_name, app_ver, addon_ver[0], addon_ver[1], addon_ver[2]))
3186 # ##### Start of GlobalSettings element.
3187 global_settings = elem_empty(root, b"GlobalSettings")
3188 scene = scene_data.scene
3190 elem_data_single_int32(global_settings, b"Version", 1000)
3192 props = elem_properties(global_settings)
3193 up_axis, front_axis, coord_axis = RIGHT_HAND_AXES[scene_data.settings.to_axes]
3194 #~ # DO NOT take into account global scale here! That setting is applied to object transformations during export
3195 #~ # (in other words, this is pure blender-exporter feature, and has nothing to do with FBX data).
3196 #~ if scene_data.settings.apply_unit_scale:
3197 #~ # Unit scaling is applied to objects' scale, so our unit is effectively FBX one (centimeter).
3198 #~ scale_factor_org = 1.0
3199 #~ scale_factor = 1.0 / units_blender_to_fbx_factor(scene)
3200 #~ else:
3201 #~ scale_factor_org = units_blender_to_fbx_factor(scene)
3202 #~ scale_factor = scale_factor_org
3203 scale_factor = scale_factor_org = scene_data.settings.unit_scale
3204 elem_props_set(props, "p_integer", b"UpAxis", up_axis[0])
3205 elem_props_set(props, "p_integer", b"UpAxisSign", up_axis[1])
3206 elem_props_set(props, "p_integer", b"FrontAxis", front_axis[0])
3207 elem_props_set(props, "p_integer", b"FrontAxisSign", front_axis[1])
3208 elem_props_set(props, "p_integer", b"CoordAxis", coord_axis[0])
3209 elem_props_set(props, "p_integer", b"CoordAxisSign", coord_axis[1])
3210 elem_props_set(props, "p_integer", b"OriginalUpAxis", -1)
3211 elem_props_set(props, "p_integer", b"OriginalUpAxisSign", 1)
3212 elem_props_set(props, "p_double", b"UnitScaleFactor", scale_factor)
3213 elem_props_set(props, "p_double", b"OriginalUnitScaleFactor", scale_factor_org)
3214 elem_props_set(props, "p_color_rgb", b"AmbientColor", (0.0, 0.0, 0.0))
3215 elem_props_set(props, "p_string", b"DefaultCamera", "Producer Perspective")
3217 # Global timing data.
3218 r = scene.render
3219 _, fbx_fps_mode = FBX_FRAMERATES[0] # Custom framerate.
3220 fbx_fps = fps = r.fps / r.fps_base
3221 for ref_fps, fps_mode in FBX_FRAMERATES:
3222 if similar_values(fps, ref_fps):
3223 fbx_fps = ref_fps
3224 fbx_fps_mode = fps_mode
3225 break
3226 elem_props_set(props, "p_enum", b"TimeMode", fbx_fps_mode)
3227 elem_props_set(props, "p_timestamp", b"TimeSpanStart", 0)
3228 elem_props_set(props, "p_timestamp", b"TimeSpanStop", FBX_KTIME)
3229 elem_props_set(props, "p_double", b"CustomFrameRate", fbx_fps)
3231 # ##### End of GlobalSettings element.
3234 def fbx_documents_elements(root, scene_data):
3236 Write 'Document' part of FBX root.
3237 Seems like FBX support multiple documents, but until I find examples of such, we'll stick to single doc!
3238 time is expected to be a datetime.datetime object, or None (using now() in this case).
3240 name = scene_data.scene.name
3242 # ##### Start of Documents element.
3243 docs = elem_empty(root, b"Documents")
3245 elem_data_single_int32(docs, b"Count", 1)
3247 doc_uid = get_fbx_uuid_from_key("__FBX_Document__" + name)
3248 doc = elem_data_single_int64(docs, b"Document", doc_uid)
3249 doc.add_string_unicode(name)
3250 doc.add_string_unicode(name)
3252 props = elem_properties(doc)
3253 elem_props_set(props, "p_object", b"SourceObject")
3254 elem_props_set(props, "p_string", b"ActiveAnimStackName", "")
3256 # XXX Some kind of ID? Offset?
3257 # Anyway, as long as we have only one doc, probably not an issue.
3258 elem_data_single_int64(doc, b"RootNode", 0)
3261 def fbx_references_elements(root, scene_data):
3263 Have no idea what references are in FBX currently... Just writing empty element.
3265 docs = elem_empty(root, b"References")
3268 def fbx_definitions_elements(root, scene_data):
3270 Templates definitions. Only used by Objects data afaik (apart from dummy GlobalSettings one).
3272 definitions = elem_empty(root, b"Definitions")
3274 elem_data_single_int32(definitions, b"Version", FBX_TEMPLATES_VERSION)
3275 elem_data_single_int32(definitions, b"Count", scene_data.templates_users)
3277 fbx_templates_generate(definitions, scene_data.templates)
3280 def fbx_objects_elements(root, scene_data):
3282 Data (objects, geometry, material, textures, armatures, etc.).
3284 perfmon = PerfMon()
3285 perfmon.level_up()
3286 objects = elem_empty(root, b"Objects")
3288 perfmon.step("FBX export fetch empties (%d)..." % len(scene_data.data_empties))
3290 for empty in scene_data.data_empties:
3291 fbx_data_empty_elements(objects, empty, scene_data)
3293 perfmon.step("FBX export fetch lamps (%d)..." % len(scene_data.data_lights))
3295 for lamp in scene_data.data_lights:
3296 fbx_data_light_elements(objects, lamp, scene_data)
3298 perfmon.step("FBX export fetch cameras (%d)..." % len(scene_data.data_cameras))
3300 for cam in scene_data.data_cameras:
3301 fbx_data_camera_elements(objects, cam, scene_data)
3303 perfmon.step("FBX export fetch meshes (%d)..."
3304 % len({me_key for me_key, _me, _free in scene_data.data_meshes.values()}))
3306 done_meshes = set()
3307 for me_obj in scene_data.data_meshes:
3308 fbx_data_mesh_elements(objects, me_obj, scene_data, done_meshes)
3309 del done_meshes
3311 perfmon.step("FBX export fetch objects (%d)..." % len(scene_data.objects))
3313 for ob_obj in scene_data.objects:
3314 if ob_obj.is_dupli:
3315 continue
3316 fbx_data_object_elements(objects, ob_obj, scene_data)
3317 for dp_obj in ob_obj.dupli_list_gen(scene_data.depsgraph):
3318 if dp_obj not in scene_data.objects:
3319 continue
3320 fbx_data_object_elements(objects, dp_obj, scene_data)
3322 perfmon.step("FBX export fetch remaining...")
3324 for ob_obj in scene_data.objects:
3325 if not (ob_obj.is_object and ob_obj.type == 'ARMATURE'):
3326 continue
3327 fbx_data_armature_elements(objects, ob_obj, scene_data)
3329 if scene_data.data_leaf_bones:
3330 fbx_data_leaf_bone_elements(objects, scene_data)
3332 for ma in scene_data.data_materials:
3333 fbx_data_material_elements(objects, ma, scene_data)
3335 for blender_tex_key in scene_data.data_textures:
3336 fbx_data_texture_file_elements(objects, blender_tex_key, scene_data)
3338 for vid in scene_data.data_videos:
3339 fbx_data_video_elements(objects, vid, scene_data)
3341 perfmon.step("FBX export fetch animations...")
3342 start_time = time.process_time()
3344 fbx_data_animation_elements(objects, scene_data)
3346 perfmon.level_down()
3349 def fbx_connections_elements(root, scene_data):
3351 Relations between Objects (which material uses which texture, and so on).
3353 connections = elem_empty(root, b"Connections")
3355 for c in scene_data.connections:
3356 elem_connection(connections, *c)
3359 def fbx_takes_elements(root, scene_data):
3361 Animations.
3363 # XXX Pretty sure takes are no more needed...
3364 takes = elem_empty(root, b"Takes")
3365 elem_data_single_string(takes, b"Current", b"")
3367 animations = scene_data.animations
3368 for astack_key, animations, alayer_key, name, f_start, f_end in animations:
3369 scene = scene_data.scene
3370 fps = scene.render.fps / scene.render.fps_base
3371 start_ktime = int(convert_sec_to_ktime(f_start / fps))
3372 end_ktime = int(convert_sec_to_ktime(f_end / fps))
3374 take = elem_data_single_string(takes, b"Take", name)
3375 elem_data_single_string(take, b"FileName", name + b".tak")
3376 take_loc_time = elem_data_single_int64(take, b"LocalTime", start_ktime)
3377 take_loc_time.add_int64(end_ktime)
3378 take_ref_time = elem_data_single_int64(take, b"ReferenceTime", start_ktime)
3379 take_ref_time.add_int64(end_ktime)
3382 # ##### "Main" functions. #####
3384 # This func can be called with just the filepath
3385 def save_single(operator, scene, depsgraph, filepath="",
3386 global_matrix=Matrix(),
3387 apply_unit_scale=False,
3388 global_scale=1.0,
3389 apply_scale_options='FBX_SCALE_NONE',
3390 axis_up="Z",
3391 axis_forward="Y",
3392 context_objects=None,
3393 object_types=None,
3394 use_mesh_modifiers=True,
3395 use_mesh_modifiers_render=True,
3396 mesh_smooth_type='FACE',
3397 use_subsurf=False,
3398 use_armature_deform_only=False,
3399 bake_anim=True,
3400 bake_anim_use_all_bones=True,
3401 bake_anim_use_nla_strips=True,
3402 bake_anim_use_all_actions=True,
3403 bake_anim_step=1.0,
3404 bake_anim_simplify_factor=1.0,
3405 bake_anim_force_startend_keying=True,
3406 add_leaf_bones=False,
3407 primary_bone_axis='Y',
3408 secondary_bone_axis='X',
3409 use_metadata=True,
3410 path_mode='AUTO',
3411 use_mesh_edges=True,
3412 use_tspace=True,
3413 use_triangles=False,
3414 embed_textures=False,
3415 use_custom_props=False,
3416 bake_space_transform=False,
3417 armature_nodetype='NULL',
3418 colors_type='SRGB',
3419 prioritize_active_color=False,
3420 **kwargs
3423 # Clear cached ObjectWrappers (just in case...).
3424 ObjectWrapper.cache_clear()
3426 if object_types is None:
3427 object_types = {'EMPTY', 'CAMERA', 'LIGHT', 'ARMATURE', 'MESH', 'OTHER'}
3429 if 'OTHER' in object_types:
3430 object_types |= BLENDER_OTHER_OBJECT_TYPES
3432 # Default Blender unit is equivalent to meter, while FBX one is centimeter...
3433 unit_scale = units_blender_to_fbx_factor(scene) if apply_unit_scale else 100.0
3434 if apply_scale_options == 'FBX_SCALE_NONE':
3435 global_matrix = Matrix.Scale(unit_scale * global_scale, 4) @ global_matrix
3436 unit_scale = 1.0
3437 elif apply_scale_options == 'FBX_SCALE_UNITS':
3438 global_matrix = Matrix.Scale(global_scale, 4) @ global_matrix
3439 elif apply_scale_options == 'FBX_SCALE_CUSTOM':
3440 global_matrix = Matrix.Scale(unit_scale, 4) @ global_matrix
3441 unit_scale = global_scale
3442 else: # if apply_scale_options == 'FBX_SCALE_ALL':
3443 unit_scale = global_scale * unit_scale
3445 global_scale = global_matrix.median_scale
3446 global_matrix_inv = global_matrix.inverted()
3447 # For transforming mesh normals.
3448 global_matrix_inv_transposed = global_matrix_inv.transposed()
3450 # Only embed textures in COPY mode!
3451 if embed_textures and path_mode != 'COPY':
3452 embed_textures = False
3454 # Calculate bone correction matrix
3455 bone_correction_matrix = None # Default is None = no change
3456 bone_correction_matrix_inv = None
3457 if (primary_bone_axis, secondary_bone_axis) != ('Y', 'X'):
3458 from bpy_extras.io_utils import axis_conversion
3459 bone_correction_matrix = axis_conversion(from_forward=secondary_bone_axis,
3460 from_up=primary_bone_axis,
3461 to_forward='X',
3462 to_up='Y',
3463 ).to_4x4()
3464 bone_correction_matrix_inv = bone_correction_matrix.inverted()
3467 media_settings = FBXExportSettingsMedia(
3468 path_mode,
3469 os.path.dirname(bpy.data.filepath), # base_src
3470 os.path.dirname(filepath), # base_dst
3471 # Local dir where to put images (media), using FBX conventions.
3472 os.path.splitext(os.path.basename(filepath))[0] + ".fbm", # subdir
3473 embed_textures,
3474 set(), # copy_set
3475 set(), # embedded_set
3478 settings = FBXExportSettings(
3479 operator.report, (axis_up, axis_forward), global_matrix, global_scale, apply_unit_scale, unit_scale,
3480 bake_space_transform, global_matrix_inv, global_matrix_inv_transposed,
3481 context_objects, object_types, use_mesh_modifiers, use_mesh_modifiers_render,
3482 mesh_smooth_type, use_subsurf, use_mesh_edges, use_tspace, use_triangles,
3483 armature_nodetype, use_armature_deform_only,
3484 add_leaf_bones, bone_correction_matrix, bone_correction_matrix_inv,
3485 bake_anim, bake_anim_use_all_bones, bake_anim_use_nla_strips, bake_anim_use_all_actions,
3486 bake_anim_step, bake_anim_simplify_factor, bake_anim_force_startend_keying,
3487 False, media_settings, use_custom_props, colors_type, prioritize_active_color
3490 import bpy_extras.io_utils
3492 print('\nFBX export starting... %r' % filepath)
3493 start_time = time.process_time()
3495 # Generate some data about exported scene...
3496 scene_data = fbx_data_from_scene(scene, depsgraph, settings)
3498 root = elem_empty(None, b"") # Root element has no id, as it is not saved per se!
3500 # Mostly FBXHeaderExtension and GlobalSettings.
3501 fbx_header_elements(root, scene_data)
3503 # Documents and References are pretty much void currently.
3504 fbx_documents_elements(root, scene_data)
3505 fbx_references_elements(root, scene_data)
3507 # Templates definitions.
3508 fbx_definitions_elements(root, scene_data)
3510 # Actual data.
3511 fbx_objects_elements(root, scene_data)
3513 # How data are inter-connected.
3514 fbx_connections_elements(root, scene_data)
3516 # Animation.
3517 fbx_takes_elements(root, scene_data)
3519 # Cleanup!
3520 fbx_scene_data_cleanup(scene_data)
3522 # And we are down, we can write the whole thing!
3523 encode_bin.write(filepath, root, FBX_VERSION)
3525 # Clear cached ObjectWrappers!
3526 ObjectWrapper.cache_clear()
3528 # copy all collected files, if we did not embed them.
3529 if not media_settings.embed_textures:
3530 bpy_extras.io_utils.path_reference_copy(media_settings.copy_set)
3532 print('export finished in %.4f sec.' % (time.process_time() - start_time))
3533 return {'FINISHED'}
3536 # defaults for applications, currently only unity but could add others.
3537 def defaults_unity3d():
3538 return {
3539 # These options seem to produce the same result as the old Ascii exporter in Unity3D:
3540 "axis_up": 'Y',
3541 "axis_forward": '-Z',
3542 "global_matrix": Matrix.Rotation(-math.pi / 2.0, 4, 'X'),
3543 # Should really be True, but it can cause problems if a model is already in a scene or prefab
3544 # with the old transforms.
3545 "bake_space_transform": False,
3547 "use_selection": False,
3549 "object_types": {'ARMATURE', 'EMPTY', 'MESH', 'OTHER'},
3550 "use_mesh_modifiers": True,
3551 "use_mesh_modifiers_render": True,
3552 "use_mesh_edges": False,
3553 "mesh_smooth_type": 'FACE',
3554 "colors_type": 'SRGB',
3555 "use_subsurf": False,
3556 "use_tspace": False, # XXX Why? Unity is expected to support tspace import...
3557 "use_triangles": False,
3559 "use_armature_deform_only": True,
3561 "use_custom_props": True,
3563 "bake_anim": True,
3564 "bake_anim_simplify_factor": 1.0,
3565 "bake_anim_step": 1.0,
3566 "bake_anim_use_nla_strips": True,
3567 "bake_anim_use_all_actions": True,
3568 "add_leaf_bones": False, # Avoid memory/performance cost for something only useful for modelling
3569 "primary_bone_axis": 'Y', # Doesn't really matter for Unity, so leave unchanged
3570 "secondary_bone_axis": 'X',
3572 "path_mode": 'AUTO',
3573 "embed_textures": False,
3574 "batch_mode": 'OFF',
3578 def save(operator, context,
3579 filepath="",
3580 use_selection=False,
3581 use_visible=False,
3582 use_active_collection=False,
3583 batch_mode='OFF',
3584 use_batch_own_dir=False,
3585 **kwargs
3588 This is a wrapper around save_single, which handles multi-scenes (or collections) cases, when batch-exporting
3589 a whole .blend file.
3592 ret = {'FINISHED'}
3594 active_object = context.view_layer.objects.active
3596 org_mode = None
3597 if active_object and active_object.mode != 'OBJECT' and bpy.ops.object.mode_set.poll():
3598 org_mode = active_object.mode
3599 bpy.ops.object.mode_set(mode='OBJECT')
3601 if batch_mode == 'OFF':
3602 kwargs_mod = kwargs.copy()
3603 if use_active_collection:
3604 if use_selection:
3605 ctx_objects = tuple(obj
3606 for obj in context.view_layer.active_layer_collection.collection.all_objects
3607 if obj.select_get())
3608 else:
3609 ctx_objects = context.view_layer.active_layer_collection.collection.all_objects
3610 else:
3611 if use_selection:
3612 ctx_objects = context.selected_objects
3613 else:
3614 ctx_objects = context.view_layer.objects
3615 if use_visible:
3616 ctx_objects = tuple(obj for obj in ctx_objects if obj.visible_get())
3618 # Ensure no Objects are in Edit mode.
3619 # Copy to a tuple for safety, to avoid the risk of modifying ctx_objects while iterating.
3620 for obj in tuple(ctx_objects):
3621 if not ensure_object_not_in_edit_mode(context, obj):
3622 operator.report({'ERROR'}, "%s could not be set out of Edit Mode, so cannot be exported" % obj.name)
3623 return {'CANCELLED'}
3625 kwargs_mod["context_objects"] = ctx_objects
3627 depsgraph = context.evaluated_depsgraph_get()
3628 ret = save_single(operator, context.scene, depsgraph, filepath, **kwargs_mod)
3629 else:
3630 # XXX We need a way to generate a depsgraph for inactive view_layers first...
3631 # XXX Also, what to do in case of batch-exporting scenes, when there is more than one view layer?
3632 # Scenes have no concept of 'active' view layer, that's on window level...
3633 fbxpath = filepath
3635 prefix = os.path.basename(fbxpath)
3636 if prefix:
3637 fbxpath = os.path.dirname(fbxpath)
3639 if batch_mode == 'COLLECTION':
3640 data_seq = tuple((coll, coll.name, 'objects') for coll in bpy.data.collections if coll.objects)
3641 elif batch_mode in {'SCENE_COLLECTION', 'ACTIVE_SCENE_COLLECTION'}:
3642 scenes = [context.scene] if batch_mode == 'ACTIVE_SCENE_COLLECTION' else bpy.data.scenes
3643 data_seq = []
3644 for scene in scenes:
3645 if not scene.objects:
3646 continue
3647 # Needed to avoid having tens of 'Scene Collection' entries.
3648 todo_collections = [(scene.collection, "_".join((scene.name, scene.collection.name)))]
3649 while todo_collections:
3650 coll, coll_name = todo_collections.pop()
3651 todo_collections.extend(((c, c.name) for c in coll.children if c.all_objects))
3652 data_seq.append((coll, coll_name, 'all_objects'))
3653 else:
3654 data_seq = tuple((scene, scene.name, 'objects') for scene in bpy.data.scenes if scene.objects)
3656 # Ensure no Objects are in Edit mode.
3657 for data, data_name, data_obj_propname in data_seq:
3658 # Copy to a tuple for safety, to avoid the risk of modifying the data prop while iterating it.
3659 for obj in tuple(getattr(data, data_obj_propname)):
3660 if not ensure_object_not_in_edit_mode(context, obj):
3661 operator.report({'ERROR'},
3662 "%s in %s could not be set out of Edit Mode, so cannot be exported"
3663 % (obj.name, data_name))
3664 return {'CANCELLED'}
3666 # call this function within a loop with BATCH_ENABLE == False
3668 new_fbxpath = fbxpath # own dir option modifies, we need to keep an original
3669 for data, data_name, data_obj_propname in data_seq: # scene or collection
3670 newname = "_".join((prefix, bpy.path.clean_name(data_name))) if prefix else bpy.path.clean_name(data_name)
3672 if use_batch_own_dir:
3673 new_fbxpath = os.path.join(fbxpath, newname)
3674 # path may already exist... and be a file.
3675 while os.path.isfile(new_fbxpath):
3676 new_fbxpath = "_".join((new_fbxpath, "dir"))
3677 if not os.path.exists(new_fbxpath):
3678 os.makedirs(new_fbxpath)
3680 filepath = os.path.join(new_fbxpath, newname + '.fbx')
3682 print('\nBatch exporting %s as...\n\t%r' % (data, filepath))
3684 if batch_mode in {'COLLECTION', 'SCENE_COLLECTION', 'ACTIVE_SCENE_COLLECTION'}:
3685 # Collection, so that objects update properly, add a dummy scene.
3686 scene = bpy.data.scenes.new(name="FBX_Temp")
3687 src_scenes = {} # Count how much each 'source' scenes are used.
3688 for obj in getattr(data, data_obj_propname):
3689 for src_sce in obj.users_scene:
3690 src_scenes[src_sce] = src_scenes.setdefault(src_sce, 0) + 1
3691 scene.collection.objects.link(obj)
3693 # Find the 'most used' source scene, and use its unit settings. This is somewhat weak, but should work
3694 # fine in most cases, and avoids stupid issues like T41931.
3695 best_src_scene = None
3696 best_src_scene_users = -1
3697 for sce, nbr_users in src_scenes.items():
3698 if (nbr_users) > best_src_scene_users:
3699 best_src_scene_users = nbr_users
3700 best_src_scene = sce
3701 scene.unit_settings.system = best_src_scene.unit_settings.system
3702 scene.unit_settings.system_rotation = best_src_scene.unit_settings.system_rotation
3703 scene.unit_settings.scale_length = best_src_scene.unit_settings.scale_length
3705 # new scene [only one viewlayer to update]
3706 scene.view_layers[0].update()
3707 # TODO - BUMMER! Armatures not in the group wont animate the mesh
3708 else:
3709 scene = data
3711 kwargs_batch = kwargs.copy()
3712 kwargs_batch["context_objects"] = getattr(data, data_obj_propname)
3714 save_single(operator, scene, scene.view_layers[0].depsgraph, filepath, **kwargs_batch)
3716 if batch_mode in {'COLLECTION', 'SCENE_COLLECTION', 'ACTIVE_SCENE_COLLECTION'}:
3717 # Remove temp collection scene.
3718 bpy.data.scenes.remove(scene)
3720 if active_object and org_mode:
3721 context.view_layer.objects.active = active_object
3722 if bpy.ops.object.mode_set.poll():
3723 bpy.ops.object.mode_set(mode=org_mode)
3725 return ret