Newer
Older
# ##### BEGIN GPL LICENSE BLOCK #####
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# ##### END GPL LICENSE BLOCK #####
# <pep8 compliant>
# Script copyright (C) Campbell Barton, Bastien Montagne
import array
import datetime
import math
import os
import time
from itertools import zip_longest, chain
if "bpy" in locals():
import importlib
if "encode_bin" in locals():
importlib.reload(encode_bin)
if "data_types" in locals():
importlib.reload(data_types)
if "fbx_utils" in locals():
importlib.reload(fbx_utils)
import bpy
import bpy_extras
from bpy_extras import node_shader_utils
from mathutils import Vector, Matrix
from . import encode_bin, data_types, fbx_utils
Bastien Montagne
committed
from .fbx_utils import (
# Constants.
FBX_VERSION, FBX_HEADER_VERSION, FBX_SCENEINFO_VERSION, FBX_TEMPLATES_VERSION,
FBX_MODELS_VERSION,
FBX_GEOMETRY_VERSION, FBX_GEOMETRY_NORMAL_VERSION, FBX_GEOMETRY_BINORMAL_VERSION, FBX_GEOMETRY_TANGENT_VERSION,
FBX_GEOMETRY_SMOOTHING_VERSION, FBX_GEOMETRY_CREASE_VERSION, FBX_GEOMETRY_VCOLOR_VERSION, FBX_GEOMETRY_UV_VERSION,
FBX_GEOMETRY_MATERIAL_VERSION, FBX_GEOMETRY_LAYER_VERSION,
FBX_GEOMETRY_SHAPE_VERSION, FBX_DEFORMER_SHAPE_VERSION, FBX_DEFORMER_SHAPECHANNEL_VERSION,
FBX_POSE_BIND_VERSION, FBX_DEFORMER_SKIN_VERSION, FBX_DEFORMER_CLUSTER_VERSION,
FBX_MATERIAL_VERSION, FBX_TEXTURE_VERSION,
FBX_ANIM_KEY_VERSION,
FBX_ANIM_PROPSGROUP_NAME,
FBX_KTIME,
BLENDER_OTHER_OBJECT_TYPES, BLENDER_OBJECT_TYPES_MESHLIKE,
FBX_LIGHT_TYPES, FBX_LIGHT_DECAY_TYPES,
RIGHT_HAND_AXES, FBX_FRAMERATES,
# Miscellaneous utils.
PerfMon,
units_blender_to_fbx_factor, units_convertor, units_convertor_iter,
matrix4_to_array, similar_values, similar_values_iter,
Bastien Montagne
committed
# Mesh transform helpers.
vcos_transformed_gen, nors_transformed_gen,
# UUID from key.
get_fbx_uuid_from_key,
# Key generators.
get_blenderID_key, get_blenderID_name,
get_blender_mesh_shape_key, get_blender_mesh_shape_channel_key,
get_blender_empty_key, get_blender_bone_key,
get_blender_bindpose_key, get_blender_armature_skin_key, get_blender_bone_cluster_key,
get_blender_anim_id_base, get_blender_anim_stack_key, get_blender_anim_layer_key,
get_blender_anim_curve_node_key, get_blender_anim_curve_key,
get_blender_nodetexture_key,
# FBX element data.
elem_empty,
elem_data_single_bool, elem_data_single_int16, elem_data_single_int32, elem_data_single_int64,
elem_data_single_float32, elem_data_single_float64,
elem_data_single_bytes, elem_data_single_string, elem_data_single_string_unicode,
elem_data_single_bool_array, elem_data_single_int32_array, elem_data_single_int64_array,
elem_data_single_float32_array, elem_data_single_float64_array, elem_data_vec_float64,
# FBX element properties.
elem_properties, elem_props_set, elem_props_compound,
# FBX element properties handling templates.
elem_props_template_init, elem_props_template_set, elem_props_template_finalize,
# Templates.
FBXTemplate, fbx_templates_generate,
# Animation.
AnimationCurveNodeWrapper,
# Objects.
ObjectWrapper, fbx_name_class,
# Top level.
Jens Ch. Restemeier
committed
FBXExportSettingsMedia, FBXExportSettings, FBXExportData,
Bastien Montagne
committed
)
# Units convertors!
convert_sec_to_ktime = units_convertor("second", "ktime")
convert_sec_to_ktime_iter = units_convertor_iter("second", "ktime")
convert_mm_to_inch = units_convertor("millimeter", "inch")
convert_rad_to_deg = units_convertor("radian", "degree")
convert_rad_to_deg_iter = units_convertor_iter("radian", "degree")
# TODO: check all those "default" values, they should match Blender's default as much as possible, I guess?
def fbx_template_def_globalsettings(scene, settings, override_defaults=None, nbr_users=0):
props = {}
if override_defaults is not None:
props.update(override_defaults)
Bastien Montagne
committed
return FBXTemplate(b"GlobalSettings", b"", props, nbr_users, [False])
def fbx_template_def_model(scene, settings, override_defaults=None, nbr_users=0):
gscale = settings.global_scale
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
props = {
# Name, Value, Type, Animatable
b"QuaternionInterpolate": (0, "p_enum", False), # 0 = no quat interpolation.
b"RotationOffset": ((0.0, 0.0, 0.0), "p_vector_3d", False),
b"RotationPivot": ((0.0, 0.0, 0.0), "p_vector_3d", False),
b"ScalingOffset": ((0.0, 0.0, 0.0), "p_vector_3d", False),
b"ScalingPivot": ((0.0, 0.0, 0.0), "p_vector_3d", False),
b"TranslationActive": (False, "p_bool", False),
b"TranslationMin": ((0.0, 0.0, 0.0), "p_vector_3d", False),
b"TranslationMax": ((0.0, 0.0, 0.0), "p_vector_3d", False),
b"TranslationMinX": (False, "p_bool", False),
b"TranslationMinY": (False, "p_bool", False),
b"TranslationMinZ": (False, "p_bool", False),
b"TranslationMaxX": (False, "p_bool", False),
b"TranslationMaxY": (False, "p_bool", False),
b"TranslationMaxZ": (False, "p_bool", False),
b"RotationOrder": (0, "p_enum", False), # we always use 'XYZ' order.
b"RotationSpaceForLimitOnly": (False, "p_bool", False),
b"RotationStiffnessX": (0.0, "p_double", False),
b"RotationStiffnessY": (0.0, "p_double", False),
b"RotationStiffnessZ": (0.0, "p_double", False),
b"AxisLen": (10.0, "p_double", False),
b"PreRotation": ((0.0, 0.0, 0.0), "p_vector_3d", False),
b"PostRotation": ((0.0, 0.0, 0.0), "p_vector_3d", False),
b"RotationActive": (False, "p_bool", False),
b"RotationMin": ((0.0, 0.0, 0.0), "p_vector_3d", False),
b"RotationMax": ((0.0, 0.0, 0.0), "p_vector_3d", False),
b"RotationMinX": (False, "p_bool", False),
b"RotationMinY": (False, "p_bool", False),
b"RotationMinZ": (False, "p_bool", False),
b"RotationMaxX": (False, "p_bool", False),
b"RotationMaxY": (False, "p_bool", False),
b"RotationMaxZ": (False, "p_bool", False),
b"InheritType": (0, "p_enum", False), # RrSs
b"ScalingActive": (False, "p_bool", False),
b"ScalingMin": ((0.0, 0.0, 0.0), "p_vector_3d", False),
b"ScalingMax": ((1.0, 1.0, 1.0), "p_vector_3d", False),
b"ScalingMinX": (False, "p_bool", False),
b"ScalingMinY": (False, "p_bool", False),
b"ScalingMinZ": (False, "p_bool", False),
b"ScalingMaxX": (False, "p_bool", False),
b"ScalingMaxY": (False, "p_bool", False),
b"ScalingMaxZ": (False, "p_bool", False),
b"GeometricTranslation": ((0.0, 0.0, 0.0), "p_vector_3d", False),
b"GeometricRotation": ((0.0, 0.0, 0.0), "p_vector_3d", False),
b"GeometricScaling": ((1.0, 1.0, 1.0), "p_vector_3d", False),
b"MinDampRangeX": (0.0, "p_double", False),
b"MinDampRangeY": (0.0, "p_double", False),
b"MinDampRangeZ": (0.0, "p_double", False),
b"MaxDampRangeX": (0.0, "p_double", False),
b"MaxDampRangeY": (0.0, "p_double", False),
b"MaxDampRangeZ": (0.0, "p_double", False),
b"MinDampStrengthX": (0.0, "p_double", False),
b"MinDampStrengthY": (0.0, "p_double", False),
b"MinDampStrengthZ": (0.0, "p_double", False),
b"MaxDampStrengthX": (0.0, "p_double", False),
b"MaxDampStrengthY": (0.0, "p_double", False),
b"MaxDampStrengthZ": (0.0, "p_double", False),
b"PreferedAngleX": (0.0, "p_double", False),
b"PreferedAngleY": (0.0, "p_double", False),
b"PreferedAngleZ": (0.0, "p_double", False),
b"LookAtProperty": (None, "p_object", False),
b"UpVectorProperty": (None, "p_object", False),
b"Show": (True, "p_bool", False),
b"NegativePercentShapeSupport": (True, "p_bool", False),
b"DefaultAttributeIndex": (-1, "p_integer", False),
b"Freeze": (False, "p_bool", False),
b"LODBox": (False, "p_bool", False),
b"Lcl Translation": ((0.0, 0.0, 0.0), "p_lcl_translation", True),
b"Lcl Rotation": ((0.0, 0.0, 0.0), "p_lcl_rotation", True),
b"Lcl Scaling": ((1.0, 1.0, 1.0), "p_lcl_scaling", True),
b"Visibility": (1.0, "p_visibility", True),
b"Visibility Inheritance": (1, "p_visibility_inheritance", False),
}
if override_defaults is not None:
props.update(override_defaults)
Bastien Montagne
committed
return FBXTemplate(b"Model", b"FbxNode", props, nbr_users, [False])
def fbx_template_def_null(scene, settings, override_defaults=None, nbr_users=0):
props = {
b"Color": ((0.8, 0.8, 0.8), "p_color_rgb", False),
b"Size": (100.0, "p_double", False),
b"Look": (1, "p_enum", False), # Cross (0 is None, i.e. invisible?).
}
if override_defaults is not None:
props.update(override_defaults)
Bastien Montagne
committed
return FBXTemplate(b"NodeAttribute", b"FbxNull", props, nbr_users, [False])
def fbx_template_def_light(scene, settings, override_defaults=None, nbr_users=0):
gscale = settings.global_scale
props = {
b"LightType": (0, "p_enum", False), # Point light.
b"CastLight": (True, "p_bool", False),
b"Color": ((1.0, 1.0, 1.0), "p_color", True),
b"Intensity": (100.0, "p_number", True), # Times 100 compared to Blender values...
b"DecayType": (2, "p_enum", False), # Quadratic.
b"DecayStart": (30.0 * gscale, "p_double", False),
b"CastShadows": (True, "p_bool", False),
b"ShadowColor": ((0.0, 0.0, 0.0), "p_color", True),
b"AreaLightShape": (0, "p_enum", False), # Rectangle.
}
if override_defaults is not None:
props.update(override_defaults)
Bastien Montagne
committed
return FBXTemplate(b"NodeAttribute", b"FbxLight", props, nbr_users, [False])
def fbx_template_def_camera(scene, settings, override_defaults=None, nbr_users=0):
r = scene.render
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
props = {
b"Color": ((0.8, 0.8, 0.8), "p_color_rgb", False),
b"Position": ((0.0, 0.0, -50.0), "p_vector", True),
b"UpVector": ((0.0, 1.0, 0.0), "p_vector", True),
b"InterestPosition": ((0.0, 0.0, 0.0), "p_vector", True),
b"Roll": (0.0, "p_roll", True),
b"OpticalCenterX": (0.0, "p_opticalcenterx", True),
b"OpticalCenterY": (0.0, "p_opticalcentery", True),
b"BackgroundColor": ((0.63, 0.63, 0.63), "p_color", True),
b"TurnTable": (0.0, "p_number", True),
b"DisplayTurnTableIcon": (False, "p_bool", False),
b"UseMotionBlur": (False, "p_bool", False),
b"UseRealTimeMotionBlur": (True, "p_bool", False),
b"Motion Blur Intensity": (1.0, "p_number", True),
b"AspectRatioMode": (0, "p_enum", False), # WindowSize.
b"AspectWidth": (320.0, "p_double", False),
b"AspectHeight": (200.0, "p_double", False),
b"PixelAspectRatio": (1.0, "p_double", False),
b"FilmOffsetX": (0.0, "p_number", True),
b"FilmOffsetY": (0.0, "p_number", True),
b"FilmWidth": (0.816, "p_double", False),
b"FilmHeight": (0.612, "p_double", False),
b"FilmAspectRatio": (1.3333333333333333, "p_double", False),
b"FilmSqueezeRatio": (1.0, "p_double", False),
b"FilmFormatIndex": (0, "p_enum", False), # Assuming this is ApertureFormat, 0 = custom.
b"PreScale": (1.0, "p_number", True),
b"FilmTranslateX": (0.0, "p_number", True),
b"FilmTranslateY": (0.0, "p_number", True),
b"FilmRollPivotX": (0.0, "p_number", True),
b"FilmRollPivotY": (0.0, "p_number", True),
b"FilmRollValue": (0.0, "p_number", True),
b"FilmRollOrder": (0, "p_enum", False), # 0 = rotate first (default).
b"ApertureMode": (2, "p_enum", False), # 2 = Vertical.
b"GateFit": (0, "p_enum", False), # 0 = no resolution gate fit.
b"FieldOfView": (25.114999771118164, "p_fov", True),
b"FieldOfViewX": (40.0, "p_fov_x", True),
b"FieldOfViewY": (40.0, "p_fov_y", True),
b"FocalLength": (34.89327621672628, "p_number", True),
b"CameraFormat": (0, "p_enum", False), # Custom camera format.
b"UseFrameColor": (False, "p_bool", False),
b"FrameColor": ((0.3, 0.3, 0.3), "p_color_rgb", False),
b"ShowName": (True, "p_bool", False),
b"ShowInfoOnMoving": (True, "p_bool", False),
b"ShowGrid": (True, "p_bool", False),
b"ShowOpticalCenter": (False, "p_bool", False),
b"ShowAzimut": (True, "p_bool", False),
b"ShowTimeCode": (False, "p_bool", False),
b"ShowAudio": (False, "p_bool", False),
b"AudioColor": ((0.0, 1.0, 0.0), "p_vector_3d", False), # Yep, vector3d, not corlorgb… :cry:
b"NearPlane": (10.0, "p_double", False),
b"FarPlane": (4000.0, "p_double", False),
b"AutoComputeClipPanes": (False, "p_bool", False),
b"ViewCameraToLookAt": (True, "p_bool", False),
b"ViewFrustumNearFarPlane": (False, "p_bool", False),
b"ViewFrustumBackPlaneMode": (2, "p_enum", False), # 2 = show back plane if texture added.
b"BackPlaneDistance": (4000.0, "p_number", True),
b"BackPlaneDistanceMode": (1, "p_enum", False), # 1 = relative to camera.
b"ViewFrustumFrontPlaneMode": (2, "p_enum", False), # 2 = show front plane if texture added.
b"FrontPlaneDistance": (10.0, "p_number", True),
b"FrontPlaneDistanceMode": (1, "p_enum", False), # 1 = relative to camera.
b"LockMode": (False, "p_bool", False),
b"LockInterestNavigation": (False, "p_bool", False),
Bastien Montagne
committed
# BackPlate... properties **arggggg!**
b"FitImage": (False, "p_bool", False),
b"Crop": (False, "p_bool", False),
b"Center": (True, "p_bool", False),
b"KeepRatio": (True, "p_bool", False),
Bastien Montagne
committed
# End of BackPlate...
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
b"BackgroundAlphaTreshold": (0.5, "p_double", False),
b"ShowBackplate": (True, "p_bool", False),
b"BackPlaneOffsetX": (0.0, "p_number", True),
b"BackPlaneOffsetY": (0.0, "p_number", True),
b"BackPlaneRotation": (0.0, "p_number", True),
b"BackPlaneScaleX": (1.0, "p_number", True),
b"BackPlaneScaleY": (1.0, "p_number", True),
b"Background Texture": (None, "p_object", False),
b"FrontPlateFitImage": (True, "p_bool", False),
b"FrontPlateCrop": (False, "p_bool", False),
b"FrontPlateCenter": (True, "p_bool", False),
b"FrontPlateKeepRatio": (True, "p_bool", False),
b"Foreground Opacity": (1.0, "p_double", False),
b"ShowFrontplate": (True, "p_bool", False),
b"FrontPlaneOffsetX": (0.0, "p_number", True),
b"FrontPlaneOffsetY": (0.0, "p_number", True),
b"FrontPlaneRotation": (0.0, "p_number", True),
b"FrontPlaneScaleX": (1.0, "p_number", True),
b"FrontPlaneScaleY": (1.0, "p_number", True),
b"Foreground Texture": (None, "p_object", False),
b"DisplaySafeArea": (False, "p_bool", False),
b"DisplaySafeAreaOnRender": (False, "p_bool", False),
b"SafeAreaDisplayStyle": (1, "p_enum", False), # 1 = rounded corners.
b"SafeAreaAspectRatio": (1.3333333333333333, "p_double", False),
b"Use2DMagnifierZoom": (False, "p_bool", False),
b"2D Magnifier Zoom": (100.0, "p_number", True),
b"2D Magnifier X": (50.0, "p_number", True),
b"2D Magnifier Y": (50.0, "p_number", True),
b"CameraProjectionType": (0, "p_enum", False), # 0 = perspective, 1 = orthogonal.
b"OrthoZoom": (1.0, "p_double", False),
b"UseRealTimeDOFAndAA": (False, "p_bool", False),
b"UseDepthOfField": (False, "p_bool", False),
b"FocusSource": (0, "p_enum", False), # 0 = camera interest, 1 = distance from camera interest.
b"FocusAngle": (3.5, "p_double", False), # ???
b"FocusDistance": (200.0, "p_double", False),
b"UseAntialiasing": (False, "p_bool", False),
b"AntialiasingIntensity": (0.77777, "p_double", False),
b"AntialiasingMethod": (0, "p_enum", False), # 0 = oversampling, 1 = hardware.
b"UseAccumulationBuffer": (False, "p_bool", False),
b"FrameSamplingCount": (7, "p_integer", False),
b"FrameSamplingType": (1, "p_enum", False), # 0 = uniform, 1 = stochastic.
}
if override_defaults is not None:
props.update(override_defaults)
Bastien Montagne
committed
return FBXTemplate(b"NodeAttribute", b"FbxCamera", props, nbr_users, [False])
def fbx_template_def_bone(scene, settings, override_defaults=None, nbr_users=0):
props = {}
if override_defaults is not None:
props.update(override_defaults)
Bastien Montagne
committed
return FBXTemplate(b"NodeAttribute", b"LimbNode", props, nbr_users, [False])
def fbx_template_def_geometry(scene, settings, override_defaults=None, nbr_users=0):
props = {
b"Color": ((0.8, 0.8, 0.8), "p_color_rgb", False),
b"BBoxMin": ((0.0, 0.0, 0.0), "p_vector_3d", False),
b"BBoxMax": ((0.0, 0.0, 0.0), "p_vector_3d", False),
b"Primary Visibility": (True, "p_bool", False),
b"Casts Shadows": (True, "p_bool", False),
b"Receive Shadows": (True, "p_bool", False),
}
if override_defaults is not None:
props.update(override_defaults)
Bastien Montagne
committed
return FBXTemplate(b"Geometry", b"FbxMesh", props, nbr_users, [False])
def fbx_template_def_material(scene, settings, override_defaults=None, nbr_users=0):
# WIP...
props = {
b"ShadingModel": ("Phong", "p_string", False),
b"MultiLayer": (False, "p_bool", False),
b"EmissiveColor": ((0.0, 0.0, 0.0), "p_color", True),
b"EmissiveFactor": (1.0, "p_number", True),
b"AmbientColor": ((0.2, 0.2, 0.2), "p_color", True),
b"AmbientFactor": (1.0, "p_number", True),
b"DiffuseColor": ((0.8, 0.8, 0.8), "p_color", True),
b"DiffuseFactor": (1.0, "p_number", True),
b"TransparentColor": ((0.0, 0.0, 0.0), "p_color", True),
b"TransparencyFactor": (0.0, "p_number", True),
b"Opacity": (1.0, "p_number", True),
b"NormalMap": ((0.0, 0.0, 0.0), "p_vector_3d", False),
b"Bump": ((0.0, 0.0, 0.0), "p_vector_3d", False),
b"BumpFactor": (1.0, "p_double", False),
b"DisplacementColor": ((0.0, 0.0, 0.0), "p_color_rgb", False),
b"DisplacementFactor": (1.0, "p_double", False),
b"VectorDisplacementColor": ((0.0, 0.0, 0.0), "p_color_rgb", False),
b"VectorDisplacementFactor": (1.0, "p_double", False),
b"SpecularColor": ((0.2, 0.2, 0.2), "p_color", True),
b"SpecularFactor": (1.0, "p_number", True),
# Not sure about the name, importer uses this (but ShininessExponent for tex prop name!)
# And in fbx exported by sdk, you have one in template, the other in actual material!!! :/
# For now, using both.
b"Shininess": (20.0, "p_number", True),
b"ShininessExponent": (20.0, "p_number", True),
b"ReflectionColor": ((0.0, 0.0, 0.0), "p_color", True),
b"ReflectionFactor": (1.0, "p_number", True),
}
if override_defaults is not None:
props.update(override_defaults)
Bastien Montagne
committed
return FBXTemplate(b"Material", b"FbxSurfacePhong", props, nbr_users, [False])
def fbx_template_def_texture_file(scene, settings, override_defaults=None, nbr_users=0):
# WIP...
# XXX Not sure about all names!
props = {
b"TextureTypeUse": (0, "p_enum", False), # Standard.
b"AlphaSource": (2, "p_enum", False), # Black (i.e. texture's alpha), XXX name guessed!.
b"Texture alpha": (1.0, "p_double", False),
b"PremultiplyAlpha": (True, "p_bool", False),
b"CurrentTextureBlendMode": (1, "p_enum", False), # Additive...
b"CurrentMappingType": (0, "p_enum", False), # UV.
b"UVSet": ("default", "p_string", False), # UVMap name.
b"WrapModeU": (0, "p_enum", False), # Repeat.
b"WrapModeV": (0, "p_enum", False), # Repeat.
b"UVSwap": (False, "p_bool", False),
b"Translation": ((0.0, 0.0, 0.0), "p_vector_3d", False),
b"Rotation": ((0.0, 0.0, 0.0), "p_vector_3d", False),
b"Scaling": ((1.0, 1.0, 1.0), "p_vector_3d", False),
b"TextureRotationPivot": ((0.0, 0.0, 0.0), "p_vector_3d", False),
b"TextureScalingPivot": ((0.0, 0.0, 0.0), "p_vector_3d", False),
Bastien Montagne
committed
# Not sure about those two...
b"UseMaterial": (False, "p_bool", False),
b"UseMipMap": (False, "p_bool", False),
}
if override_defaults is not None:
props.update(override_defaults)
Bastien Montagne
committed
return FBXTemplate(b"Texture", b"FbxFileTexture", props, nbr_users, [False])
def fbx_template_def_video(scene, settings, override_defaults=None, nbr_users=0):
# WIP...
props = {
b"Width": (0, "p_integer", False),
b"Height": (0, "p_integer", False),
b"Path": ("", "p_string_url", False),
b"AccessMode": (0, "p_enum", False), # Disk (0=Disk, 1=Mem, 2=DiskAsync).
b"StartFrame": (0, "p_integer", False),
b"StopFrame": (0, "p_integer", False),
b"Offset": (0, "p_timestamp", False),
b"PlaySpeed": (0.0, "p_double", False),
b"FreeRunning": (False, "p_bool", False),
b"Loop": (False, "p_bool", False),
b"InterlaceMode": (0, "p_enum", False), # None, i.e. progressive.
b"ImageSequence": (False, "p_bool", False),
b"ImageSequenceOffset": (0, "p_integer", False),
b"FrameRate": (0.0, "p_double", False),
b"LastFrame": (0, "p_integer", False),
}
if override_defaults is not None:
props.update(override_defaults)
Bastien Montagne
committed
return FBXTemplate(b"Video", b"FbxVideo", props, nbr_users, [False])
def fbx_template_def_pose(scene, settings, override_defaults=None, nbr_users=0):
props = {}
if override_defaults is not None:
props.update(override_defaults)
Bastien Montagne
committed
return FBXTemplate(b"Pose", b"", props, nbr_users, [False])
def fbx_template_def_deformer(scene, settings, override_defaults=None, nbr_users=0):
props = {}
if override_defaults is not None:
props.update(override_defaults)
Bastien Montagne
committed
return FBXTemplate(b"Deformer", b"", props, nbr_users, [False])
def fbx_template_def_animstack(scene, settings, override_defaults=None, nbr_users=0):
props = {
b"Description": ("", "p_string", False),
b"LocalStart": (0, "p_timestamp", False),
b"LocalStop": (0, "p_timestamp", False),
b"ReferenceStart": (0, "p_timestamp", False),
b"ReferenceStop": (0, "p_timestamp", False),
}
if override_defaults is not None:
props.update(override_defaults)
Bastien Montagne
committed
return FBXTemplate(b"AnimationStack", b"FbxAnimStack", props, nbr_users, [False])
def fbx_template_def_animlayer(scene, settings, override_defaults=None, nbr_users=0):
props = {
b"Weight": (100.0, "p_number", True),
b"Mute": (False, "p_bool", False),
b"Solo": (False, "p_bool", False),
b"Lock": (False, "p_bool", False),
b"Color": ((0.8, 0.8, 0.8), "p_color_rgb", False),
b"BlendMode": (0, "p_enum", False),
b"RotationAccumulationMode": (0, "p_enum", False),
b"ScaleAccumulationMode": (0, "p_enum", False),
b"BlendModeBypass": (0, "p_ulonglong", False),
}
if override_defaults is not None:
props.update(override_defaults)
Bastien Montagne
committed
return FBXTemplate(b"AnimationLayer", b"FbxAnimLayer", props, nbr_users, [False])
def fbx_template_def_animcurvenode(scene, settings, override_defaults=None, nbr_users=0):
props = {
FBX_ANIM_PROPSGROUP_NAME.encode(): (None, "p_compound", False),
}
if override_defaults is not None:
props.update(override_defaults)
Bastien Montagne
committed
return FBXTemplate(b"AnimationCurveNode", b"FbxAnimCurveNode", props, nbr_users, [False])
def fbx_template_def_animcurve(scene, settings, override_defaults=None, nbr_users=0):
props = {}
if override_defaults is not None:
props.update(override_defaults)
Bastien Montagne
committed
return FBXTemplate(b"AnimationCurve", b"", props, nbr_users, [False])
# ##### Generators for connection elements. #####
Bastien Montagne
committed
def elem_connection(elem, c_type, uid_src, uid_dst, prop_dst=None):
e = elem_data_single_string(elem, b"C", c_type)
e.add_int64(uid_src)
e.add_int64(uid_dst)
if prop_dst is not None:
e.add_string(prop_dst)
# ##### FBX objects generators. #####
def fbx_data_element_custom_properties(props, bid):
"""
Store custom properties of blender ID bid (any mapping-like object, in fact) into FBX properties props.
"""
items = bid.items()
if not items:
return
rna_properties = {prop.identifier for prop in bid.bl_rna.properties if prop.is_runtime}
for k, v in items:
if k == '_RNA_UI' or k in rna_properties:
continue
list_val = getattr(v, "to_list", lambda: None)()
elem_props_set(props, "p_string", k.encode(), v, custom=True)
elif isinstance(v, int):
elem_props_set(props, "p_integer", k.encode(), v, custom=True)
elif isinstance(v, float):
Bastien Montagne
committed
elem_props_set(props, "p_double", k.encode(), v, custom=True)
Bastien Montagne
committed
elif list_val:
if len(list_val) == 3:
elem_props_set(props, "p_vector", k.encode(), list_val, custom=True)
else:
elem_props_set(props, "p_string", k.encode(), str(list_val), custom=True)
else:
elem_props_set(props, "p_string", k.encode(), str(v), custom=True)
def fbx_data_empty_elements(root, empty, scene_data):
"""
Bastien Montagne
committed
Write the Empty data block (you can control its FBX datatype with the 'fbx_type' string custom property).
"""
empty_key = scene_data.data_empties[empty]
null = elem_data_single_int64(root, b"NodeAttribute", get_fbx_uuid_from_key(empty_key))
null.add_string(fbx_name_class(empty.name.encode(), b"NodeAttribute"))
Bastien Montagne
committed
val = empty.bdata.get('fbx_type', None)
null.add_string(val.encode() if val and isinstance(val, str) else b"Null")
elem_data_single_string(null, b"TypeFlags", b"Null")
Bastien Montagne
committed
tmpl = elem_props_template_init(scene_data.templates, b"Null")
props = elem_properties(null)
Bastien Montagne
committed
elem_props_template_finalize(tmpl, props)
# No custom properties, already saved with object (Model).
def fbx_data_light_elements(root, lamp, scene_data):
"""
Write the Lamp data block.
"""
gscale = scene_data.settings.global_scale
light_key = scene_data.data_lights[lamp]
do_light = True
decay_type = FBX_LIGHT_DECAY_TYPES['CONSTANT']
do_shadow = False
shadow_color = Vector((0.0, 0.0, 0.0))
if lamp.type not in {'HEMI'}:
if lamp.type not in {'SUN', 'AREA'}:
decay_type = FBX_LIGHT_DECAY_TYPES[lamp.falloff_type]
do_light = True
do_shadow = lamp.use_shadow
shadow_color = lamp.shadow_color
light = elem_data_single_int64(root, b"NodeAttribute", get_fbx_uuid_from_key(light_key))
light.add_string(fbx_name_class(lamp.name.encode(), b"NodeAttribute"))
light.add_string(b"Light")
elem_data_single_int32(light, b"GeometryVersion", FBX_GEOMETRY_VERSION) # Sic...
Bastien Montagne
committed
tmpl = elem_props_template_init(scene_data.templates, b"Light")
props = elem_properties(light)
elem_props_template_set(tmpl, props, "p_enum", b"LightType", FBX_LIGHT_TYPES[lamp.type])
elem_props_template_set(tmpl, props, "p_bool", b"CastLight", do_light)
Bastien Montagne
committed
elem_props_template_set(tmpl, props, "p_color", b"Color", lamp.color)
elem_props_template_set(tmpl, props, "p_number", b"Intensity", lamp.energy * 100.0)
elem_props_template_set(tmpl, props, "p_enum", b"DecayType", decay_type)
Bastien Montagne
committed
elem_props_template_set(tmpl, props, "p_double", b"DecayStart", lamp.distance * gscale)
elem_props_template_set(tmpl, props, "p_bool", b"CastShadows", do_shadow)
Bastien Montagne
committed
elem_props_template_set(tmpl, props, "p_color", b"ShadowColor", shadow_color)
if lamp.type in {'SPOT'}:
Bastien Montagne
committed
elem_props_template_set(tmpl, props, "p_double", b"OuterAngle", math.degrees(lamp.spot_size))
elem_props_template_set(tmpl, props, "p_double", b"InnerAngle",
math.degrees(lamp.spot_size * (1.0 - lamp.spot_blend)))
Bastien Montagne
committed
elem_props_template_finalize(tmpl, props)
# Custom properties.
if scene_data.settings.use_custom_props:
fbx_data_element_custom_properties(props, lamp)
def fbx_data_camera_elements(root, cam_obj, scene_data):
"""
Write the Camera data blocks.
"""
gscale = scene_data.settings.global_scale
Bastien Montagne
committed
cam = cam_obj.bdata
cam_data = cam.data
cam_key = scene_data.data_cameras[cam_obj]
# Real data now, good old camera!
# Object transform info.
Bastien Montagne
committed
loc, rot, scale, matrix, matrix_rot = cam_obj.fbx_object_tx(scene_data)
up = matrix_rot @ Vector((0.0, 1.0, 0.0))
to = matrix_rot @ Vector((0.0, 0.0, -1.0))
# Render settings.
# TODO We could export much more...
render = scene_data.scene.render
width = render.resolution_x
height = render.resolution_y
aspect = width / height
# Film width & height from mm to inches
filmwidth = convert_mm_to_inch(cam_data.sensor_width)
filmheight = convert_mm_to_inch(cam_data.sensor_height)
filmaspect = filmwidth / filmheight
# Film offset
offsetx = filmwidth * cam_data.shift_x
offsety = filmaspect * filmheight * cam_data.shift_y
cam = elem_data_single_int64(root, b"NodeAttribute", get_fbx_uuid_from_key(cam_key))
cam.add_string(fbx_name_class(cam_data.name.encode(), b"NodeAttribute"))
cam.add_string(b"Camera")
Bastien Montagne
committed
tmpl = elem_props_template_init(scene_data.templates, b"Camera")
props = elem_properties(cam)
Bastien Montagne
committed
Bastien Montagne
committed
elem_props_template_set(tmpl, props, "p_vector", b"Position", loc)
elem_props_template_set(tmpl, props, "p_vector", b"UpVector", up)
elem_props_template_set(tmpl, props, "p_vector", b"InterestPosition", loc + to) # Point, not vector!
# Should we use world value?
Bastien Montagne
committed
elem_props_template_set(tmpl, props, "p_color", b"BackgroundColor", (0.0, 0.0, 0.0))
elem_props_template_set(tmpl, props, "p_bool", b"DisplayTurnTableIcon", True)
elem_props_template_set(tmpl, props, "p_enum", b"AspectRatioMode", 2) # FixedResolution
Bastien Montagne
committed
elem_props_template_set(tmpl, props, "p_double", b"AspectWidth", float(render.resolution_x))
elem_props_template_set(tmpl, props, "p_double", b"AspectHeight", float(render.resolution_y))
elem_props_template_set(tmpl, props, "p_double", b"PixelAspectRatio",
float(render.pixel_aspect_x / render.pixel_aspect_y))
Bastien Montagne
committed
Bastien Montagne
committed
elem_props_template_set(tmpl, props, "p_double", b"FilmWidth", filmwidth)
elem_props_template_set(tmpl, props, "p_double", b"FilmHeight", filmheight)
elem_props_template_set(tmpl, props, "p_double", b"FilmAspectRatio", filmaspect)
elem_props_template_set(tmpl, props, "p_double", b"FilmOffsetX", offsetx)
elem_props_template_set(tmpl, props, "p_double", b"FilmOffsetY", offsety)
elem_props_template_set(tmpl, props, "p_enum", b"ApertureMode", 3) # FocalLength.
elem_props_template_set(tmpl, props, "p_enum", b"GateFit", 2) # FitHorizontal.
elem_props_template_set(tmpl, props, "p_fov", b"FieldOfView", math.degrees(cam_data.angle_x))
elem_props_template_set(tmpl, props, "p_fov_x", b"FieldOfViewX", math.degrees(cam_data.angle_x))
elem_props_template_set(tmpl, props, "p_fov_y", b"FieldOfViewY", math.degrees(cam_data.angle_y))
# No need to convert to inches here...
Bastien Montagne
committed
elem_props_template_set(tmpl, props, "p_double", b"FocalLength", cam_data.lens)
elem_props_template_set(tmpl, props, "p_double", b"SafeAreaAspectRatio", aspect)
# Default to perspective camera.
elem_props_template_set(tmpl, props, "p_enum", b"CameraProjectionType", 1 if cam_data.type == 'ORTHO' else 0)
elem_props_template_set(tmpl, props, "p_double", b"OrthoZoom", cam_data.ortho_scale)
Bastien Montagne
committed
elem_props_template_set(tmpl, props, "p_double", b"NearPlane", cam_data.clip_start * gscale)
elem_props_template_set(tmpl, props, "p_double", b"FarPlane", cam_data.clip_end * gscale)
elem_props_template_set(tmpl, props, "p_enum", b"BackPlaneDistanceMode", 1) # RelativeToCamera.
Bastien Montagne
committed
elem_props_template_set(tmpl, props, "p_double", b"BackPlaneDistance", cam_data.clip_end * gscale)
Bastien Montagne
committed
elem_props_template_finalize(tmpl, props)
if scene_data.settings.use_custom_props:
fbx_data_element_custom_properties(props, cam_data)
elem_data_single_string(cam, b"TypeFlags", b"Camera")
elem_data_single_int32(cam, b"GeometryVersion", 124) # Sic...
elem_data_vec_float64(cam, b"Position", loc)
elem_data_vec_float64(cam, b"Up", up)
elem_data_vec_float64(cam, b"LookAt", to)
elem_data_single_int32(cam, b"ShowInfoOnMoving", 1)
elem_data_single_int32(cam, b"ShowAudio", 0)
elem_data_vec_float64(cam, b"AudioColor", (0.0, 1.0, 0.0))
elem_data_single_float64(cam, b"CameraOrthoZoom", 1.0)
def fbx_data_bindpose_element(root, me_obj, me, scene_data, arm_obj=None, mat_world_arm=None, bones=[]):
"""
Helper, since bindpose are used by both meshes shape keys and armature bones...
"""
if arm_obj is None:
arm_obj = me_obj
# We assume bind pose for our bones are their "Editmode" pose...
# All matrices are expected in global (world) space.
bindpose_key = get_blender_bindpose_key(arm_obj.bdata, me)
fbx_pose = elem_data_single_int64(root, b"Pose", get_fbx_uuid_from_key(bindpose_key))
fbx_pose.add_string(fbx_name_class(me.name.encode(), b"Pose"))
fbx_pose.add_string(b"BindPose")
elem_data_single_string(fbx_pose, b"Type", b"BindPose")
elem_data_single_int32(fbx_pose, b"Version", FBX_POSE_BIND_VERSION)
Bastien Montagne
committed
elem_data_single_int32(fbx_pose, b"NbPoseNodes", 1 + (1 if (arm_obj != me_obj) else 0) + len(bones))
# First node is mesh/object.
mat_world_obj = me_obj.fbx_object_matrix(scene_data, global_space=True)
fbx_posenode = elem_empty(fbx_pose, b"PoseNode")
elem_data_single_int64(fbx_posenode, b"Node", me_obj.fbx_uuid)
elem_data_single_float64_array(fbx_posenode, b"Matrix", matrix4_to_array(mat_world_obj))
# Second node is armature object itself.
if arm_obj != me_obj:
fbx_posenode = elem_empty(fbx_pose, b"PoseNode")
elem_data_single_int64(fbx_posenode, b"Node", arm_obj.fbx_uuid)
elem_data_single_float64_array(fbx_posenode, b"Matrix", matrix4_to_array(mat_world_arm))
# And all bones of armature!
mat_world_bones = {}
for bo_obj in bones:
bomat = bo_obj.fbx_object_matrix(scene_data, rest=True, global_space=True)
mat_world_bones[bo_obj] = bomat
fbx_posenode = elem_empty(fbx_pose, b"PoseNode")
elem_data_single_int64(fbx_posenode, b"Node", bo_obj.fbx_uuid)
elem_data_single_float64_array(fbx_posenode, b"Matrix", matrix4_to_array(bomat))
return mat_world_obj, mat_world_bones
def fbx_data_mesh_shapes_elements(root, me_obj, me, scene_data, fbx_me_tmpl, fbx_me_props):
"""
Write shape keys related data.
"""
if me not in scene_data.data_deformers_shape:
return
Bastien Montagne
committed
write_normals = True # scene_data.settings.mesh_smooth_type in {'OFF'}
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
# First, write the geometry data itself (i.e. shapes).
_me_key, shape_key, shapes = scene_data.data_deformers_shape[me]
channels = []
for shape, (channel_key, geom_key, shape_verts_co, shape_verts_idx) in shapes.items():
# Use vgroups as weights, if defined.
if shape.vertex_group and shape.vertex_group in me_obj.bdata.vertex_groups:
shape_verts_weights = [0.0] * (len(shape_verts_co) // 3)
vg_idx = me_obj.bdata.vertex_groups[shape.vertex_group].index
for sk_idx, v_idx in enumerate(shape_verts_idx):
for vg in me.vertices[v_idx].groups:
if vg.group == vg_idx:
shape_verts_weights[sk_idx] = vg.weight * 100.0
else:
shape_verts_weights = [100.0] * (len(shape_verts_co) // 3)
channels.append((channel_key, shape, shape_verts_weights))
geom = elem_data_single_int64(root, b"Geometry", get_fbx_uuid_from_key(geom_key))
geom.add_string(fbx_name_class(shape.name.encode(), b"Geometry"))
geom.add_string(b"Shape")
tmpl = elem_props_template_init(scene_data.templates, b"Geometry")
props = elem_properties(geom)
elem_props_template_finalize(tmpl, props)
elem_data_single_int32(geom, b"Version", FBX_GEOMETRY_SHAPE_VERSION)
elem_data_single_int32_array(geom, b"Indexes", shape_verts_idx)
elem_data_single_float64_array(geom, b"Vertices", shape_verts_co)
Bastien Montagne
committed
if write_normals:
elem_data_single_float64_array(geom, b"Normals", [0.0] * len(shape_verts_co))
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
# Yiha! BindPose for shapekeys too! Dodecasigh...
# XXX Not sure yet whether several bindposes on same mesh are allowed, or not... :/
fbx_data_bindpose_element(root, me_obj, me, scene_data)
# ...and now, the deformers stuff.
fbx_shape = elem_data_single_int64(root, b"Deformer", get_fbx_uuid_from_key(shape_key))
fbx_shape.add_string(fbx_name_class(me.name.encode(), b"Deformer"))
fbx_shape.add_string(b"BlendShape")
elem_data_single_int32(fbx_shape, b"Version", FBX_DEFORMER_SHAPE_VERSION)
for channel_key, shape, shape_verts_weights in channels:
fbx_channel = elem_data_single_int64(root, b"Deformer", get_fbx_uuid_from_key(channel_key))
fbx_channel.add_string(fbx_name_class(shape.name.encode(), b"SubDeformer"))
fbx_channel.add_string(b"BlendShapeChannel")
elem_data_single_int32(fbx_channel, b"Version", FBX_DEFORMER_SHAPECHANNEL_VERSION)
elem_data_single_float64(fbx_channel, b"DeformPercent", shape.value * 100.0) # Percents...
elem_data_single_float64_array(fbx_channel, b"FullWeights", shape_verts_weights)
# *WHY* add this in linked mesh properties too? *cry*
# No idea whether it’s percent here too, or more usual factor (assume percentage for now) :/
elem_props_template_set(fbx_me_tmpl, fbx_me_props, "p_number", shape.name.encode(), shape.value * 100.0,
animatable=True)
def fbx_data_mesh_elements(root, me_obj, scene_data, done_meshes):
"""
Write the Mesh (Geometry) data block.
"""
Bastien Montagne
committed
# Ugly helper... :/
def _infinite_gen(val):
while 1:
yield val
Bastien Montagne
committed
me_key, me, _free = scene_data.data_meshes[me_obj]
# In case of multiple instances of same mesh, only write it once!
if me_key in done_meshes:
return
# No gscale/gmat here, all data are supposed to be in object space.
smooth_type = scene_data.settings.mesh_smooth_type
Bastien Montagne
committed
write_normals = True # smooth_type in {'OFF'}
do_bake_space_transform = me_obj.use_bake_space_transform(scene_data)
# Vertices are in object space, but we are post-multiplying all transforms with the inverse of the
# global matrix, so we need to apply the global matrix to the vertices to get the correct result.
geom_mat_co = scene_data.settings.global_matrix if do_bake_space_transform else None
# We need to apply the inverse transpose of the global matrix when transforming normals.
Bastien Montagne
committed
geom_mat_no = Matrix(scene_data.settings.global_matrix_inv_transposed) if do_bake_space_transform else None
if geom_mat_no is not None:
# Remove translation & scaling!
geom_mat_no.translation = Vector()
geom_mat_no.normalize()
geom = elem_data_single_int64(root, b"Geometry", get_fbx_uuid_from_key(me_key))
geom.add_string(fbx_name_class(me.name.encode(), b"Geometry"))
geom.add_string(b"Mesh")
tmpl = elem_props_template_init(scene_data.templates, b"Geometry")
props = elem_properties(geom)
# Custom properties.
if scene_data.settings.use_custom_props:
fbx_data_element_custom_properties(props, me)
# Subdivision levels. Take them from the first found subsurf modifier from the
# first object that has the mesh. Write crease information if the object has
# and subsurf modifier.
write_crease = False
if scene_data.settings.use_subsurf:
last_subsurf = None
for mod in me_obj.bdata.modifiers:
if not (mod.show_render or mod.show_viewport):
continue
if mod.type == 'SUBSURF' and mod.subdivision_type == 'CATMULL_CLARK':
last_subsurf = mod
if last_subsurf:
elem_data_single_int32(geom, b"Smoothness", 2) # Display control mesh and smoothed
elem_data_single_int32(geom, b"BoundaryRule", 2) # Round edges like Blender
elem_data_single_int32(geom, b"PreviewDivisionLevels", last_subsurf.levels)
elem_data_single_int32(geom, b"RenderDivisionLevels", last_subsurf.render_levels)
elem_data_single_int32(geom, b"PreserveBorders", 0)
elem_data_single_int32(geom, b"PreserveHardEdges", 0)
elem_data_single_int32(geom, b"PropagateEdgeHardness", 0)
write_crease = mod.use_creases
elem_data_single_int32(geom, b"GeometryVersion", FBX_GEOMETRY_VERSION)
# Vertex cos.
t_co = array.array(data_types.ARRAY_FLOAT64, (0.0,)) * len(me.vertices) * 3
me.vertices.foreach_get("co", t_co)
Bastien Montagne
committed
elem_data_single_float64_array(geom, b"Vertices", chain(*vcos_transformed_gen(t_co, geom_mat_co)))
del t_co
# Polygon indices.
#
# We do loose edges as two-vertices faces, if enabled...
#
# Note we have to process Edges in the same time, as they are based on poly's loops...
loop_nbr = len(me.loops)
t_pvi = array.array(data_types.ARRAY_INT32, (0,)) * loop_nbr
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
t_ls = [None] * len(me.polygons)
me.loops.foreach_get("vertex_index", t_pvi)
me.polygons.foreach_get("loop_start", t_ls)
# Add "fake" faces for loose edges.
if scene_data.settings.use_mesh_edges:
t_le = tuple(e.vertices for e in me.edges if e.is_loose)
t_pvi.extend(chain(*t_le))
t_ls.extend(range(loop_nbr, loop_nbr + len(t_le), 2))
del t_le
# Edges...
# Note: Edges are represented as a loop here: each edge uses a single index, which refers to the polygon array.
# The edge is made by the vertex indexed py this polygon's point and the next one on the same polygon.
# Advantage: Only one index per edge.
# Drawback: Only polygon's edges can be represented (that's why we have to add fake two-verts polygons
# for loose edges).
# We also have to store a mapping from real edges to their indices in this array, for edge-mapped data
# (like e.g. crease).
t_eli = array.array(data_types.ARRAY_INT32)
edges_map = {}
edges_nbr = 0
if t_ls and t_pvi:
t_ls = set(t_ls)
todo_edges = [None] * len(me.edges) * 2
Bastien Montagne
committed
# Sigh, cannot access edge.key through foreach_get... :/
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
me.edges.foreach_get("vertices", todo_edges)
todo_edges = set((v1, v2) if v1 < v2 else (v2, v1) for v1, v2 in zip(*(iter(todo_edges),) * 2))
li = 0
vi = vi_start = t_pvi[0]
for li_next, vi_next in enumerate(t_pvi[1:] + t_pvi[:1], start=1):
if li_next in t_ls: # End of a poly's loop.
vi2 = vi_start
vi_start = vi_next
else:
vi2 = vi_next
e_key = (vi, vi2) if vi < vi2 else (vi2, vi)
if e_key in todo_edges:
t_eli.append(li)
todo_edges.remove(e_key)
edges_map[e_key] = edges_nbr
edges_nbr += 1
vi = vi_next
li = li_next
# End of edges!
# We have to ^-1 last index of each loop.
for ls in t_ls:
t_pvi[ls - 1] ^= -1
# And finally we can write data!
elem_data_single_int32_array(geom, b"PolygonVertexIndex", t_pvi)
elem_data_single_int32_array(geom, b"Edges", t_eli)
del t_pvi
del t_ls
del t_eli
# And now, layers!
# Smoothing.
if smooth_type in {'FACE', 'EDGE'}:
t_ps = None
_map = b""
if smooth_type == 'FACE':
t_ps = array.array(data_types.ARRAY_INT32, (0,)) * len(me.polygons)
me.polygons.foreach_get("use_smooth", t_ps)
_map = b"ByPolygon"
else: # EDGE
# Write Edge Smoothing.
Bastien Montagne
committed
# Note edge is sharp also if it's used by more than two faces, or one of its faces is flat.
t_ps = array.array(data_types.ARRAY_INT32, (0,)) * edges_nbr
Bastien Montagne
committed
sharp_edges = set()
temp_sharp_edges = {}
for p in me.polygons:
if not p.use_smooth:
sharp_edges.update(p.edge_keys)
continue
for k in p.edge_keys:
if temp_sharp_edges.setdefault(k, 0) > 1:
sharp_edges.add(k)
else:
temp_sharp_edges[k] += 1
del temp_sharp_edges
for e in me.edges:
if e.key not in edges_map:
continue # Only loose edges, in theory!
Bastien Montagne
committed
t_ps[edges_map[e.key]] = not (e.use_edge_sharp or (e.key in sharp_edges))
_map = b"ByEdge"
lay_smooth = elem_data_single_int32(geom, b"LayerElementSmoothing", 0)
elem_data_single_int32(lay_smooth, b"Version", FBX_GEOMETRY_SMOOTHING_VERSION)
elem_data_single_string(lay_smooth, b"Name", b"")
elem_data_single_string(lay_smooth, b"MappingInformationType", _map)
elem_data_single_string(lay_smooth, b"ReferenceInformationType", b"Direct")
elem_data_single_int32_array(lay_smooth, b"Smoothing", t_ps) # Sight, int32 for bool...
# Edge crease for subdivision
if write_crease:
t_ec = array.array(data_types.ARRAY_FLOAT64, (0.0,)) * edges_nbr
for e in me.edges:
if e.key not in edges_map:
continue # Only loose edges, in theory!
t_ec[edges_map[e.key]] = e.crease
lay_crease = elem_data_single_int32(geom, b"LayerElementEdgeCrease", 0)
elem_data_single_int32(lay_crease, b"Version", FBX_GEOMETRY_CREASE_VERSION)
elem_data_single_string(lay_crease, b"Name", b"")
elem_data_single_string(lay_crease, b"MappingInformationType", b"ByEdge")
elem_data_single_string(lay_crease, b"ReferenceInformationType", b"Direct")
elem_data_single_float64_array(lay_crease, b"EdgeCrease", t_ec)
del t_ec
# And we are done with edges!
del edges_map
# Loop normals.
tspacenumber = 0
Bastien Montagne
committed
if write_normals:
Bastien Montagne
committed
# NOTE: this is not supported by importer currently.
# XXX Official docs says normals should use IndexToDirect,
# but this does not seem well supported by apps currently...
me.calc_normals_split()
Bastien Montagne
committed
t_ln = array.array(data_types.ARRAY_FLOAT64, (0.0,)) * len(me.loops) * 3
me.loops.foreach_get("normal", t_ln)
Bastien Montagne
committed
t_ln = nors_transformed_gen(t_ln, geom_mat_no)
Bastien Montagne
committed
if 0:
t_ln = tuple(t_ln) # No choice... :/
lay_nor = elem_data_single_int32(geom, b"LayerElementNormal", 0)
elem_data_single_int32(lay_nor, b"Version", FBX_GEOMETRY_NORMAL_VERSION)
elem_data_single_string(lay_nor, b"Name", b"")
elem_data_single_string(lay_nor, b"MappingInformationType", b"ByPolygonVertex")
elem_data_single_string(lay_nor, b"ReferenceInformationType", b"IndexToDirect")
ln2idx = tuple(set(t_ln))
elem_data_single_float64_array(lay_nor, b"Normals", chain(*ln2idx))
# Normal weights, no idea what it is.
# t_lnw = array.array(data_types.ARRAY_FLOAT64, (0.0,)) * len(ln2idx)
# elem_data_single_float64_array(lay_nor, b"NormalsW", t_lnw)
ln2idx = {nor: idx for idx, nor in enumerate(ln2idx)}
elem_data_single_int32_array(lay_nor, b"NormalsIndex", (ln2idx[n] for n in t_ln))
del ln2idx
Bastien Montagne
committed
else:
lay_nor = elem_data_single_int32(geom, b"LayerElementNormal", 0)
elem_data_single_int32(lay_nor, b"Version", FBX_GEOMETRY_NORMAL_VERSION)
elem_data_single_string(lay_nor, b"Name", b"")
elem_data_single_string(lay_nor, b"MappingInformationType", b"ByPolygonVertex")
elem_data_single_string(lay_nor, b"ReferenceInformationType", b"Direct")
elem_data_single_float64_array(lay_nor, b"Normals", chain(*t_ln))
# Normal weights, no idea what it is.
# t_ln = array.array(data_types.ARRAY_FLOAT64, (0.0,)) * len(me.loops)
# elem_data_single_float64_array(lay_nor, b"NormalsW", t_ln)
del t_ln
# tspace
if scene_data.settings.use_tspace:
tspacenumber = len(me.uv_layers)
if tspacenumber:
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
# We can only compute tspace on tesellated meshes, need to check that here...
t_lt = [None] * len(me.polygons)
me.polygons.foreach_get("loop_total", t_lt)
if any((lt > 4 for lt in t_lt)):
del t_lt
scene_data.settings.report(
{'WARNING'},
"Mesh '%s' has polygons with more than 4 vertices, "
"cannot compute/export tangent space for it" % me.name)
else:
del t_lt
t_ln = array.array(data_types.ARRAY_FLOAT64, (0.0,)) * len(me.loops) * 3
# t_lnw = array.array(data_types.ARRAY_FLOAT64, (0.0,)) * len(me.loops)
uv_names = [uvlayer.name for uvlayer in me.uv_layers]
for name in uv_names:
me.calc_tangents(uvmap=name)
for idx, uvlayer in enumerate(me.uv_layers):
name = uvlayer.name
# Loop bitangents (aka binormals).
# NOTE: this is not supported by importer currently.
me.loops.foreach_get("bitangent", t_ln)
lay_nor = elem_data_single_int32(geom, b"LayerElementBinormal", idx)
elem_data_single_int32(lay_nor, b"Version", FBX_GEOMETRY_BINORMAL_VERSION)
elem_data_single_string_unicode(lay_nor, b"Name", name)
elem_data_single_string(lay_nor, b"MappingInformationType", b"ByPolygonVertex")
elem_data_single_string(lay_nor, b"ReferenceInformationType", b"Direct")
elem_data_single_float64_array(lay_nor, b"Binormals",
chain(*nors_transformed_gen(t_ln, geom_mat_no)))
# Binormal weights, no idea what it is.
# elem_data_single_float64_array(lay_nor, b"BinormalsW", t_lnw)
# Loop tangents.
# NOTE: this is not supported by importer currently.
me.loops.foreach_get("tangent", t_ln)
lay_nor = elem_data_single_int32(geom, b"LayerElementTangent", idx)
elem_data_single_int32(lay_nor, b"Version", FBX_GEOMETRY_TANGENT_VERSION)
elem_data_single_string_unicode(lay_nor, b"Name", name)
elem_data_single_string(lay_nor, b"MappingInformationType", b"ByPolygonVertex")
elem_data_single_string(lay_nor, b"ReferenceInformationType", b"Direct")
elem_data_single_float64_array(lay_nor, b"Tangents",
chain(*nors_transformed_gen(t_ln, geom_mat_no)))
# Tangent weights, no idea what it is.
# elem_data_single_float64_array(lay_nor, b"TangentsW", t_lnw)
del t_ln
# del t_lnw
me.free_tangents()
Bastien Montagne
committed
me.free_normals_split()
Bastien Montagne
committed
# Write VertexColor Layers.
vcolnumber = len(me.vertex_colors)
if vcolnumber:
def _coltuples_gen(raw_cols):
return zip(*(iter(raw_cols),) * 4)
t_lc = array.array(data_types.ARRAY_FLOAT64, (0.0,)) * len(me.loops) * 4
for colindex, collayer in enumerate(me.vertex_colors):
collayer.data.foreach_get("color", t_lc)
lay_vcol = elem_data_single_int32(geom, b"LayerElementColor", colindex)
elem_data_single_int32(lay_vcol, b"Version", FBX_GEOMETRY_VCOLOR_VERSION)
elem_data_single_string_unicode(lay_vcol, b"Name", collayer.name)
elem_data_single_string(lay_vcol, b"MappingInformationType", b"ByPolygonVertex")
elem_data_single_string(lay_vcol, b"ReferenceInformationType", b"IndexToDirect")
col2idx = tuple(set(_coltuples_gen(t_lc)))
elem_data_single_float64_array(lay_vcol, b"Colors", chain(*col2idx)) # Flatten again...
col2idx = {col: idx for idx, col in enumerate(col2idx)}
elem_data_single_int32_array(lay_vcol, b"ColorIndex", (col2idx[c] for c in _coltuples_gen(t_lc)))
del col2idx
del t_lc
del _coltuples_gen
# Write UV layers.
# Note: LayerElementTexture is deprecated since FBX 2011 - luckily!
# Textures are now only related to materials, in FBX!
uvnumber = len(me.uv_layers)
if uvnumber:
# Looks like this mapping is also expected to convey UV islands (arg..... :((((( ).
# So we need to generate unique triplets (uv, vertex_idx) here, not only just based on UV values.
def _uvtuples_gen(raw_uvs, raw_lvidxs):
return zip(zip(*(iter(raw_uvs),) * 2), raw_lvidxs)
t_luv = array.array(data_types.ARRAY_FLOAT64, (0.0,)) * len(me.loops) * 2
t_lvidx = array.array(data_types.ARRAY_INT32, (0,)) * len(me.loops)
me.loops.foreach_get("vertex_index", t_lvidx)
for uvindex, uvlayer in enumerate(me.uv_layers):
uvlayer.data.foreach_get("uv", t_luv)
lay_uv = elem_data_single_int32(geom, b"LayerElementUV", uvindex)
elem_data_single_int32(lay_uv, b"Version", FBX_GEOMETRY_UV_VERSION)
elem_data_single_string_unicode(lay_uv, b"Name", uvlayer.name)
elem_data_single_string(lay_uv, b"MappingInformationType", b"ByPolygonVertex")
elem_data_single_string(lay_uv, b"ReferenceInformationType", b"IndexToDirect")
uv_ids = tuple(set(_uvtuples_gen(t_luv, t_lvidx)))
elem_data_single_float64_array(lay_uv, b"UV", chain(*(uv for uv, vidx in uv_ids))) # Flatten again...
uv2idx = {uv_id: idx for idx, uv_id in enumerate(uv_ids)}
elem_data_single_int32_array(lay_uv, b"UVIndex", (uv2idx[uv_id] for uv_id in _uvtuples_gen(t_luv, t_lvidx)))
del _uvtuples_gen
# Face's materials.
me_fbxmaterials_idx = scene_data.mesh_material_indices.get(me)
if me_fbxmaterials_idx is not None:
# We cannot use me.materials here, as this array is filled with None in case materials are linked to object...
me_blmaterials = [mat_slot.material for mat_slot in me_obj.material_slots]
if me_fbxmaterials_idx and me_blmaterials:
lay_ma = elem_data_single_int32(geom, b"LayerElementMaterial", 0)
elem_data_single_int32(lay_ma, b"Version", FBX_GEOMETRY_MATERIAL_VERSION)
elem_data_single_string(lay_ma, b"Name", b"")
nbr_mats = len(me_fbxmaterials_idx)
t_pm = array.array(data_types.ARRAY_INT32, (0,)) * len(me.polygons)
me.polygons.foreach_get("material_index", t_pm)
# We have to validate mat indices, and map them to FBX indices.
# Note a mat might not be in me_fbxmats_idx (e.g. node mats are ignored).
blmaterials_to_fbxmaterials_idxs = [me_fbxmaterials_idx[m]
for m in me_blmaterials if m in me_fbxmaterials_idx]
ma_idx_limit = len(blmaterials_to_fbxmaterials_idxs)
def_ma = blmaterials_to_fbxmaterials_idxs[0]
_gen = (blmaterials_to_fbxmaterials_idxs[m] if m < ma_idx_limit else def_ma for m in t_pm)
t_pm = array.array(data_types.ARRAY_INT32, _gen)
elem_data_single_string(lay_ma, b"MappingInformationType", b"ByPolygon")
# XXX Logically, should be "Direct" reference type, since we do not have any index array, and have one
# value per polygon...
# But looks like FBX expects it to be IndexToDirect here (maybe because materials are already
# indices??? *sigh*).
elem_data_single_string(lay_ma, b"ReferenceInformationType", b"IndexToDirect")
elem_data_single_int32_array(lay_ma, b"Materials", t_pm)
elem_data_single_string(lay_ma, b"MappingInformationType", b"AllSame")
elem_data_single_string(lay_ma, b"ReferenceInformationType", b"IndexToDirect")
elem_data_single_int32_array(lay_ma, b"Materials", [0])
# And the "layer TOC"...
layer = elem_data_single_int32(geom, b"Layer", 0)
elem_data_single_int32(layer, b"Version", FBX_GEOMETRY_LAYER_VERSION)
Bastien Montagne
committed
if write_normals:
lay_nor = elem_empty(layer, b"LayerElement")
elem_data_single_string(lay_nor, b"Type", b"LayerElementNormal")
elem_data_single_int32(lay_nor, b"TypedIndex", 0)
if tspacenumber:
lay_binor = elem_empty(layer, b"LayerElement")
elem_data_single_string(lay_binor, b"Type", b"LayerElementBinormal")
elem_data_single_int32(lay_binor, b"TypedIndex", 0)
lay_tan = elem_empty(layer, b"LayerElement")
elem_data_single_string(lay_tan, b"Type", b"LayerElementTangent")
elem_data_single_int32(lay_tan, b"TypedIndex", 0)
if smooth_type in {'FACE', 'EDGE'}:
lay_smooth = elem_empty(layer, b"LayerElement")
elem_data_single_string(lay_smooth, b"Type", b"LayerElementSmoothing")
elem_data_single_int32(lay_smooth, b"TypedIndex", 0)
if write_crease:
lay_smooth = elem_empty(layer, b"LayerElement")
elem_data_single_string(lay_smooth, b"Type", b"LayerElementEdgeCrease")
elem_data_single_int32(lay_smooth, b"TypedIndex", 0)
if vcolnumber:
lay_vcol = elem_empty(layer, b"LayerElement")
elem_data_single_string(lay_vcol, b"Type", b"LayerElementColor")
elem_data_single_int32(lay_vcol, b"TypedIndex", 0)
if uvnumber:
lay_uv = elem_empty(layer, b"LayerElement")
elem_data_single_string(lay_uv, b"Type", b"LayerElementUV")
elem_data_single_int32(lay_uv, b"TypedIndex", 0)
if me_fbxmaterials_idx is not None:
lay_ma = elem_empty(layer, b"LayerElement")
elem_data_single_string(lay_ma, b"Type", b"LayerElementMaterial")
elem_data_single_int32(lay_ma, b"TypedIndex", 0)
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
# Add other uv and/or vcol layers...
for vcolidx, uvidx, tspaceidx in zip_longest(range(1, vcolnumber), range(1, uvnumber), range(1, tspacenumber),
fillvalue=0):
layer = elem_data_single_int32(geom, b"Layer", max(vcolidx, uvidx))
elem_data_single_int32(layer, b"Version", FBX_GEOMETRY_LAYER_VERSION)
if vcolidx:
lay_vcol = elem_empty(layer, b"LayerElement")
elem_data_single_string(lay_vcol, b"Type", b"LayerElementColor")
elem_data_single_int32(lay_vcol, b"TypedIndex", vcolidx)
if uvidx:
lay_uv = elem_empty(layer, b"LayerElement")
elem_data_single_string(lay_uv, b"Type", b"LayerElementUV")
elem_data_single_int32(lay_uv, b"TypedIndex", uvidx)
if tspaceidx:
lay_binor = elem_empty(layer, b"LayerElement")
elem_data_single_string(lay_binor, b"Type", b"LayerElementBinormal")
elem_data_single_int32(lay_binor, b"TypedIndex", tspaceidx)
lay_tan = elem_empty(layer, b"LayerElement")
elem_data_single_string(lay_tan, b"Type", b"LayerElementTangent")
elem_data_single_int32(lay_tan, b"TypedIndex", tspaceidx)
# Shape keys...
fbx_data_mesh_shapes_elements(root, me_obj, me, scene_data, tmpl, props)
elem_props_template_finalize(tmpl, props)
done_meshes.add(me_key)
def fbx_data_material_elements(root, ma, scene_data):
"""
Write the Material data block.
"""
ambient_color = (0.0, 0.0, 0.0)
if scene_data.data_world:
ambient_color = next(iter(scene_data.data_world.keys())).color
ma_wrap = node_shader_utils.PrincipledBSDFWrapper(ma, is_readonly=True)
ma_key, _objs = scene_data.data_materials[ma]
ma_type = b"Phong"
fbx_ma = elem_data_single_int64(root, b"Material", get_fbx_uuid_from_key(ma_key))
fbx_ma.add_string(fbx_name_class(ma.name.encode(), b"Material"))
fbx_ma.add_string(b"")
elem_data_single_int32(fbx_ma, b"Version", FBX_MATERIAL_VERSION)
# those are not yet properties, it seems...
elem_data_single_string(fbx_ma, b"ShadingModel", ma_type)
elem_data_single_int32(fbx_ma, b"MultiLayer", 0) # Should be bool...
Bastien Montagne
committed
tmpl = elem_props_template_init(scene_data.templates, b"Material")
props = elem_properties(fbx_ma)
elem_props_template_set(tmpl, props, "p_string", b"ShadingModel", ma_type.decode())
elem_props_template_set(tmpl, props, "p_color", b"DiffuseColor", ma_wrap.base_color)
# Not in Principled BSDF, so assuming always 1
elem_props_template_set(tmpl, props, "p_number", b"DiffuseFactor", 1.0)
# Not in Principled BSDF, so assuming always 0
elem_props_template_set(tmpl, props, "p_color", b"EmissiveColor", ma_wrap.base_color)
elem_props_template_set(tmpl, props, "p_number", b"EmissiveFactor", 0.0)
# Not in Principled BSDF, so assuming always 0
elem_props_template_set(tmpl, props, "p_color", b"AmbientColor", ambient_color)
elem_props_template_set(tmpl, props, "p_number", b"AmbientFactor", 0.0)
Bastien Montagne
committed
# Sweetness... Looks like we are not the only ones to not know exactly how FBX is supposed to work (see T59850).
# According to one of its developers, Unity uses that formula to extract alpha value:
#
# alpha = 1 - TransparencyFactor
# if (alpha == 1 or alpha == 0):
# alpha = 1 - TransparentColor.r
#
# Until further info, let's assume this is correct way to do, hence the following code for TransparentColor.
if ma_wrap.alpha < 1.0e-5 or ma_wrap.alpha > (1.0 - 1.0e-5):
elem_props_template_set(tmpl, props, "p_color", b"TransparentColor", (1.0 - ma_wrap.alpha,) * 3)
Bastien Montagne
committed
else:
elem_props_template_set(tmpl, props, "p_color", b"TransparentColor", ma_wrap.base_color)
elem_props_template_set(tmpl, props, "p_number", b"TransparencyFactor", 1.0 - ma_wrap.alpha)
elem_props_template_set(tmpl, props, "p_number", b"Opacity", ma_wrap.alpha)
elem_props_template_set(tmpl, props, "p_vector_3d", b"NormalMap", (0.0, 0.0, 0.0))
# Not sure about those...
"""
b"Bump": ((0.0, 0.0, 0.0), "p_vector_3d"),
b"BumpFactor": (1.0, "p_double"),
b"DisplacementColor": ((0.0, 0.0, 0.0), "p_color_rgb"),
b"DisplacementFactor": (0.0, "p_double"),
"""
# TODO: use specular tint?
elem_props_template_set(tmpl, props, "p_color", b"SpecularColor", ma_wrap.base_color)
elem_props_template_set(tmpl, props, "p_number", b"SpecularFactor", ma_wrap.specular / 2.0)
# See Material template about those two!
# XXX Totally empirical conversion, trying to adapt it
# (from 0.0 - 100.0 FBX shininess range to 1.0 - 0.0 Principled BSDF range)...
shininess = (1.0 - ma_wrap.roughness) * 10
shininess *= shininess
elem_props_template_set(tmpl, props, "p_number", b"Shininess", shininess)
elem_props_template_set(tmpl, props, "p_number", b"ShininessExponent", shininess)
elem_props_template_set(tmpl, props, "p_color", b"ReflectionColor", ma_wrap.base_color)
elem_props_template_set(tmpl, props, "p_number", b"ReflectionFactor", ma_wrap.metallic)
Bastien Montagne
committed
elem_props_template_finalize(tmpl, props)
if scene_data.settings.use_custom_props:
fbx_data_element_custom_properties(props, ma)
def _gen_vid_path(img, scene_data):
msetts = scene_data.settings.media_settings
fname_rel = bpy_extras.io_utils.path_reference(img.filepath, msetts.base_src, msetts.base_dst, msetts.path_mode,
msetts.subdir, msetts.copy_set, img.library)
fname_abs = os.path.normpath(os.path.abspath(os.path.join(msetts.base_dst, fname_rel)))
return fname_abs, fname_rel
def fbx_data_texture_file_elements(root, blender_tex_key, scene_data):
"""
Write the (file) Texture data block.
"""
# XXX All this is very fuzzy to me currently...
# Textures do not seem to use properties as much as they could.
# For now assuming most logical and simple stuff.
ma, sock_name = blender_tex_key
ma_wrap = node_shader_utils.PrincipledBSDFWrapper(ma, is_readonly=True)
tex_key, _fbx_prop = scene_data.data_textures[blender_tex_key]
tex = getattr(ma_wrap, sock_name)
img = tex.image
fname_abs, fname_rel = _gen_vid_path(img, scene_data)
fbx_tex = elem_data_single_int64(root, b"Texture", get_fbx_uuid_from_key(tex_key))
fbx_tex.add_string(fbx_name_class(sock_name.encode(), b"Texture"))
fbx_tex.add_string(b"")
elem_data_single_string(fbx_tex, b"Type", b"TextureVideoClip")
elem_data_single_int32(fbx_tex, b"Version", FBX_TEXTURE_VERSION)
elem_data_single_string(fbx_tex, b"TextureName", fbx_name_class(sock_name.encode(), b"Texture"))
elem_data_single_string(fbx_tex, b"Media", fbx_name_class(img.name.encode(), b"Video"))
elem_data_single_string_unicode(fbx_tex, b"FileName", fname_abs)
elem_data_single_string_unicode(fbx_tex, b"RelativeFilename", fname_rel)
alpha_source = 0 # None
if img.alpha_mode != 'NONE':
# ~ if tex.texture.use_calculate_alpha:
# ~ alpha_source = 1 # RGBIntensity as alpha.
# ~ else:
# ~ alpha_source = 2 # Black, i.e. alpha channel.
alpha_source = 2 # Black, i.e. alpha channel.
# BlendMode not useful for now, only affects layered textures afaics.
Bastien Montagne
committed
mapping = 0 # UV.
uvset = None
if tex.texcoords == 'ORCO': # XXX Others?
if tex.projection == 'FLAT':
elif tex.projection == 'CUBE':
elif tex.projection == 'TUBE':
mapping = 3 # Cylindrical
elif tex.projection == 'SPHERE':
mapping = 2 # Spherical
elif tex.texcoords == 'UV':
Bastien Montagne
committed
mapping = 0 # UV
# Yuck, UVs are linked by mere names it seems... :/
# XXX TODO how to get that now???
# uvset = tex.uv_layer
if tex.extension == 'REPEAT':
wrap_mode = 0 # Repeat
Bastien Montagne
committed
tmpl = elem_props_template_init(scene_data.templates, b"TextureFile")
props = elem_properties(fbx_tex)
elem_props_template_set(tmpl, props, "p_enum", b"AlphaSource", alpha_source)
elem_props_template_set(tmpl, props, "p_bool", b"PremultiplyAlpha",
img.alpha_mode in {'STRAIGHT'}) # Or is it PREMUL?
elem_props_template_set(tmpl, props, "p_enum", b"CurrentMappingType", mapping)
Bastien Montagne
committed
if uvset is not None:
elem_props_template_set(tmpl, props, "p_string", b"UVSet", uvset)
elem_props_template_set(tmpl, props, "p_enum", b"WrapModeU", wrap_mode)
elem_props_template_set(tmpl, props, "p_enum", b"WrapModeV", wrap_mode)
elem_props_template_set(tmpl, props, "p_vector_3d", b"Translation", tex.translation)
elem_props_template_set(tmpl, props, "p_vector_3d", b"Rotation", (-r for r in tex.rotation))
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))
Bastien Montagne
committed
# UseMaterial should always be ON imho.
elem_props_template_set(tmpl, props, "p_bool", b"UseMaterial", True)
elem_props_template_set(tmpl, props, "p_bool", b"UseMipMap", False)
Bastien Montagne
committed
elem_props_template_finalize(tmpl, props)
# No custom properties, since that's not a data-block anymore.
def fbx_data_video_elements(root, vid, scene_data):
"""
Write the actual image data block.
"""
msetts = scene_data.settings.media_settings
vid_key, _texs = scene_data.data_videos[vid]
fname_abs, fname_rel = _gen_vid_path(vid, scene_data)
fbx_vid = elem_data_single_int64(root, b"Video", get_fbx_uuid_from_key(vid_key))
fbx_vid.add_string(fbx_name_class(vid.name.encode(), b"Video"))
fbx_vid.add_string(b"Clip")
elem_data_single_string(fbx_vid, b"Type", b"Clip")
# XXX No Version???
tmpl = elem_props_template_init(scene_data.templates, b"Video")
props = elem_properties(fbx_vid)
elem_props_template_set(tmpl, props, "p_string_url", b"Path", fname_abs)
elem_props_template_finalize(tmpl, props)
elem_data_single_int32(fbx_vid, b"UseMipMap", 0)
elem_data_single_string_unicode(fbx_vid, b"Filename", fname_abs)
elem_data_single_string_unicode(fbx_vid, b"RelativeFilename", fname_rel)
if scene_data.settings.media_settings.embed_textures:
if vid.packed_file is not None:
# We only ever embed a given file once!
if fname_abs not in msetts.embedded_set:
elem_data_single_bytes(fbx_vid, b"Content", vid.packed_file.data)
msetts.embedded_set.add(fname_abs)
else:
filepath = bpy.path.abspath(vid.filepath)
# We only ever embed a given file once!
if filepath not in msetts.embedded_set:
try:
with open(filepath, 'br') as f:
elem_data_single_bytes(fbx_vid, b"Content", f.read())
except Exception as e:
print("WARNING: embedding file {} failed ({})".format(filepath, e))
elem_data_single_bytes(fbx_vid, b"Content", b"")
msetts.embedded_set.add(filepath)
Bastien Montagne
committed
# Looks like we'd rather not write any 'Content' element in this case (see T44442).
# Sounds suspect, but let's try it!
#~ else:
#~ elem_data_single_bytes(fbx_vid, b"Content", b"")
Bastien Montagne
committed
def fbx_data_armature_elements(root, arm_obj, scene_data):
"""
Write:
* Bones "data" (NodeAttribute::LimbNode, contains pretty much nothing!).
* Deformers (i.e. Skin), bind between an armature and a mesh.
** SubDeformers (i.e. Cluster), one per bone/vgroup pair.
* BindPose.
Note armature itself has no data, it is a mere "Null" Model...
"""
Bastien Montagne
committed
mat_world_arm = arm_obj.fbx_object_matrix(scene_data, global_space=True)
bones = tuple(bo_obj for bo_obj in arm_obj.bones if bo_obj in scene_data.objects)
bone_radius_scale = 33.0
Bastien Montagne
committed
for bo_obj in bones:
Bastien Montagne
committed
bo = bo_obj.bdata
bo_data_key = scene_data.data_bones[bo_obj]
fbx_bo = elem_data_single_int64(root, b"NodeAttribute", get_fbx_uuid_from_key(bo_data_key))
fbx_bo.add_string(fbx_name_class(bo.name.encode(), b"NodeAttribute"))
fbx_bo.add_string(b"LimbNode")
elem_data_single_string(fbx_bo, b"TypeFlags", b"Skeleton")
Bastien Montagne
committed
tmpl = elem_props_template_init(scene_data.templates, b"Bone")
props = elem_properties(fbx_bo)
Bastien Montagne
committed
elem_props_template_set(tmpl, props, "p_double", b"Size", bo.head_radius * bone_radius_scale)
Bastien Montagne
committed
elem_props_template_finalize(tmpl, props)
# Custom properties.
if scene_data.settings.use_custom_props:
fbx_data_element_custom_properties(props, bo)
Bastien Montagne
committed
# Store Blender bone length - XXX Not much useful actually :/
# (LimbLength can't be used because it is a scale factor 0-1 for the parent-child distance:
# http://docs.autodesk.com/FBX/2014/ENU/FBX-SDK-Documentation/cpp_ref/class_fbx_skeleton.html#a9bbe2a70f4ed82cd162620259e649f0f )
# elem_props_set(props, "p_double", "BlenderBoneLength".encode(), (bo.tail_local - bo.head_local).length, custom=True)
# Skin deformers and BindPoses.
# Note: we might also use Deformers for our "parent to vertex" stuff???
deformer = scene_data.data_deformers_skin.get(arm_obj, None)
if deformer is not None:
Bastien Montagne
committed
for me, (skin_key, ob_obj, clusters) in deformer.items():
mat_world_obj, mat_world_bones = fbx_data_bindpose_element(root, ob_obj, me, scene_data,
arm_obj, mat_world_arm, bones)
fbx_skin = elem_data_single_int64(root, b"Deformer", get_fbx_uuid_from_key(skin_key))
Bastien Montagne
committed
fbx_skin.add_string(fbx_name_class(arm_obj.name.encode(), b"Deformer"))
fbx_skin.add_string(b"Skin")
elem_data_single_int32(fbx_skin, b"Version", FBX_DEFORMER_SKIN_VERSION)
elem_data_single_float64(fbx_skin, b"Link_DeformAcuracy", 50.0) # Only vague idea what it is...
Bastien Montagne
committed
# Pre-process vertex weights (also to check vertices assigned ot more than four bones).
Bastien Montagne
committed
ob = ob_obj.bdata
bo_vg_idx = {bo_obj.bdata.name: ob.vertex_groups[bo_obj.bdata.name].index
for bo_obj in clusters.keys() if bo_obj.bdata.name in ob.vertex_groups}
Bastien Montagne
committed
valid_idxs = set(bo_vg_idx.values())
vgroups = {vg.index: {} for vg in ob.vertex_groups}
Bastien Montagne
committed
verts_vgroups = (sorted(((vg.group, vg.weight) for vg in v.groups if vg.weight and vg.group in valid_idxs),
key=lambda e: e[1], reverse=True)
for v in me.vertices)
for idx, vgs in enumerate(verts_vgroups):
for vg_idx, w in vgs:
vgroups[vg_idx][idx] = w
Bastien Montagne
committed
for bo_obj, clstr_key in clusters.items():
bo = bo_obj.bdata
# Find which vertices are affected by this bone/vgroup pair, and matching weights.
Bastien Montagne
committed
# Note we still write a cluster for bones not affecting the mesh, to get 'rest pose' data
# (the TransformBlah matrices).
vg_idx = bo_vg_idx.get(bo.name, None)
indices, weights = ((), ()) if vg_idx is None or not vgroups[vg_idx] else zip(*vgroups[vg_idx].items())
# Create the cluster.
fbx_clstr = elem_data_single_int64(root, b"Deformer", get_fbx_uuid_from_key(clstr_key))
fbx_clstr.add_string(fbx_name_class(bo.name.encode(), b"SubDeformer"))
fbx_clstr.add_string(b"Cluster")
elem_data_single_int32(fbx_clstr, b"Version", FBX_DEFORMER_CLUSTER_VERSION)
# No idea what that user data might be...
fbx_userdata = elem_data_single_string(fbx_clstr, b"UserData", b"")
fbx_userdata.add_string(b"")
if indices:
elem_data_single_int32_array(fbx_clstr, b"Indexes", indices)
elem_data_single_float64_array(fbx_clstr, b"Weights", weights)
# Transform, TransformLink and TransformAssociateModel matrices...
# They seem to be doublons of BindPose ones??? Have armature (associatemodel) in addition, though.
# WARNING! Even though official FBX API presents Transform in global space,
# **it is stored in bone space in FBX data!** See:
# http://area.autodesk.com/forum/autodesk-fbx/fbx-sdk/why-the-values-return-
# by-fbxcluster-gettransformmatrix-x-not-same-with-the-value-in-ascii-fbx-file/
elem_data_single_float64_array(fbx_clstr, b"Transform",
matrix4_to_array(mat_world_bones[bo_obj].inverted_safe() @ mat_world_obj))
elem_data_single_float64_array(fbx_clstr, b"TransformLink", matrix4_to_array(mat_world_bones[bo_obj]))
elem_data_single_float64_array(fbx_clstr, b"TransformAssociateModel", matrix4_to_array(mat_world_arm))
Bastien Montagne
committed
1586
1587
1588
1589
1590
1591
1592
1593
1594
1595
1596
1597
1598
1599
1600
1601
1602
1603
1604
1605
1606
1607
1608
1609
1610
1611
1612
1613
1614
def fbx_data_leaf_bone_elements(root, scene_data):
# Write a dummy leaf bone that is used by applications to show the length of the last bone in a chain
for (node_name, _par_uuid, node_uuid, attr_uuid, matrix, hide, size) in scene_data.data_leaf_bones:
# Bone 'data'...
fbx_bo = elem_data_single_int64(root, b"NodeAttribute", attr_uuid)
fbx_bo.add_string(fbx_name_class(node_name.encode(), b"NodeAttribute"))
fbx_bo.add_string(b"LimbNode")
elem_data_single_string(fbx_bo, b"TypeFlags", b"Skeleton")
tmpl = elem_props_template_init(scene_data.templates, b"Bone")
props = elem_properties(fbx_bo)
elem_props_template_set(tmpl, props, "p_double", b"Size", size)
elem_props_template_finalize(tmpl, props)
# And bone object.
model = elem_data_single_int64(root, b"Model", node_uuid)
model.add_string(fbx_name_class(node_name.encode(), b"Model"))
model.add_string(b"LimbNode")
elem_data_single_int32(model, b"Version", FBX_MODELS_VERSION)
# Object transform info.
loc, rot, scale = matrix.decompose()
rot = rot.to_euler('XYZ')
rot = tuple(convert_rad_to_deg_iter(rot))
tmpl = elem_props_template_init(scene_data.templates, b"Model")
# For now add only loc/rot/scale...
props = elem_properties(model)
Bastien Montagne
committed
# Generated leaf bones are obviously never animated!
Bastien Montagne
committed
1616
1617
1618
1619
1620
1621
1622
1623
1624
1625
1626
1627
1628
1629
1630
1631
1632
1633
1634
1635
1636
elem_props_template_set(tmpl, props, "p_lcl_translation", b"Lcl Translation", loc)
elem_props_template_set(tmpl, props, "p_lcl_rotation", b"Lcl Rotation", rot)
elem_props_template_set(tmpl, props, "p_lcl_scaling", b"Lcl Scaling", scale)
elem_props_template_set(tmpl, props, "p_visibility", b"Visibility", float(not hide))
# Absolutely no idea what this is, but seems mandatory for validity of the file, and defaults to
# invalid -1 value...
elem_props_template_set(tmpl, props, "p_integer", b"DefaultAttributeIndex", 0)
elem_props_template_set(tmpl, props, "p_enum", b"InheritType", 1) # RSrs
# Those settings would obviously need to be edited in a complete version of the exporter, may depends on
# object type, etc.
elem_data_single_int32(model, b"MultiLayer", 0)
elem_data_single_int32(model, b"MultiTake", 0)
elem_data_single_bool(model, b"Shading", True)
elem_data_single_string(model, b"Culling", b"CullingOff")
elem_props_template_finalize(tmpl, props)
Bastien Montagne
committed
def fbx_data_object_elements(root, ob_obj, scene_data):
"""
Write the Object (Model) data blocks.
Bastien Montagne
committed
Note this "Model" can also be bone or dupli!
"""
obj_type = b"Null" # default, sort of empty...
Bastien Montagne
committed
if ob_obj.is_bone:
obj_type = b"LimbNode"
Bastien Montagne
committed
elif (ob_obj.type == 'ARMATURE'):
Bastien Montagne
committed
if scene_data.settings.armature_nodetype == 'ROOT':
obj_type = b"Root"
elif scene_data.settings.armature_nodetype == 'LIMBNODE':
obj_type = b"LimbNode"
else: # Default, preferred option...
obj_type = b"Null"
Bastien Montagne
committed
elif (ob_obj.type in BLENDER_OBJECT_TYPES_MESHLIKE):
Bastien Montagne
committed
elif (ob_obj.type == 'CAMERA'):
Bastien Montagne
committed
model = elem_data_single_int64(root, b"Model", ob_obj.fbx_uuid)
model.add_string(fbx_name_class(ob_obj.name.encode(), b"Model"))
model.add_string(obj_type)
elem_data_single_int32(model, b"Version", FBX_MODELS_VERSION)
# Object transform info.
Bastien Montagne
committed
loc, rot, scale, matrix, matrix_rot = ob_obj.fbx_object_tx(scene_data)
Bastien Montagne
committed
tmpl = elem_props_template_init(scene_data.templates, b"Model")
# For now add only loc/rot/scale...
props = elem_properties(model)
Bastien Montagne
committed
elem_props_template_set(tmpl, props, "p_lcl_translation", b"Lcl Translation", loc,
animatable=True, animated=((ob_obj.key, "Lcl Translation") in scene_data.animated))
elem_props_template_set(tmpl, props, "p_lcl_rotation", b"Lcl Rotation", rot,
animatable=True, animated=((ob_obj.key, "Lcl Rotation") in scene_data.animated))
elem_props_template_set(tmpl, props, "p_lcl_scaling", b"Lcl Scaling", scale,
animatable=True, animated=((ob_obj.key, "Lcl Scaling") in scene_data.animated))
Bastien Montagne
committed
elem_props_template_set(tmpl, props, "p_visibility", b"Visibility", float(not ob_obj.hide))
Bastien Montagne
committed
# Absolutely no idea what this is, but seems mandatory for validity of the file, and defaults to
# invalid -1 value...
elem_props_template_set(tmpl, props, "p_integer", b"DefaultAttributeIndex", 0)
elem_props_template_set(tmpl, props, "p_enum", b"InheritType", 1) # RSrs
if scene_data.settings.use_custom_props:
Bastien Montagne
committed
fbx_data_element_custom_properties(props, ob_obj.bdata)
# Those settings would obviously need to be edited in a complete version of the exporter, may depends on
# object type, etc.
elem_data_single_int32(model, b"MultiLayer", 0)
elem_data_single_int32(model, b"MultiTake", 0)
elem_data_single_bool(model, b"Shading", True)
elem_data_single_string(model, b"Culling", b"CullingOff")
Bastien Montagne
committed
if obj_type == b"Camera":
# Why, oh why are FBX cameras such a mess???
# And WHY add camera data HERE??? Not even sure this is needed...
render = scene_data.scene.render
width = render.resolution_x * 1.0
height = render.resolution_y * 1.0
elem_props_template_set(tmpl, props, "p_enum", b"ResolutionMode", 0) # Don't know what it means
Bastien Montagne
committed
elem_props_template_set(tmpl, props, "p_double", b"AspectW", width)
elem_props_template_set(tmpl, props, "p_double", b"AspectH", height)
elem_props_template_set(tmpl, props, "p_bool", b"ViewFrustum", True)
elem_props_template_set(tmpl, props, "p_enum", b"BackgroundMode", 0) # Don't know what it means
elem_props_template_set(tmpl, props, "p_bool", b"ForegroundTransparent", True)
Bastien Montagne
committed
elem_props_template_finalize(tmpl, props)
def fbx_data_animation_elements(root, scene_data):
"""
Write animation data.
"""
animations = scene_data.animations
if not animations:
return
scene = scene_data.scene
fps = scene.render.fps / scene.render.fps_base
return (int(v) for v in convert_sec_to_ktime_iter((f / fps for f, _v in keys)))
Bastien Montagne
committed
# Animation stacks.
for astack_key, alayers, alayer_key, name, f_start, f_end in animations:
astack = elem_data_single_int64(root, b"AnimationStack", get_fbx_uuid_from_key(astack_key))
Bastien Montagne
committed
astack.add_string(fbx_name_class(name, b"AnimStack"))
astack.add_string(b"")
astack_tmpl = elem_props_template_init(scene_data.templates, b"AnimationStack")
astack_props = elem_properties(astack)
r = scene_data.scene.render
fps = r.fps / r.fps_base
start = int(convert_sec_to_ktime(f_start / fps))
end = int(convert_sec_to_ktime(f_end / fps))
Bastien Montagne
committed
elem_props_template_set(astack_tmpl, astack_props, "p_timestamp", b"LocalStart", start)
elem_props_template_set(astack_tmpl, astack_props, "p_timestamp", b"LocalStop", end)
elem_props_template_set(astack_tmpl, astack_props, "p_timestamp", b"ReferenceStart", start)
elem_props_template_set(astack_tmpl, astack_props, "p_timestamp", b"ReferenceStop", end)
elem_props_template_finalize(astack_tmpl, astack_props)
# For now, only one layer for all animations.
alayer = elem_data_single_int64(root, b"AnimationLayer", get_fbx_uuid_from_key(alayer_key))
Bastien Montagne
committed
alayer.add_string(fbx_name_class(name, b"AnimLayer"))
alayer.add_string(b"")
Bastien Montagne
committed
for ob_obj, (alayer_key, acurvenodes) in alayers.items():
Bastien Montagne
committed
# Animation layer.
# alayer = elem_data_single_int64(root, b"AnimationLayer", get_fbx_uuid_from_key(alayer_key))
Bastien Montagne
committed
# alayer.add_string(fbx_name_class(ob_obj.name.encode(), b"AnimLayer"))
Bastien Montagne
committed
# alayer.add_string(b"")
Bastien Montagne
committed
for fbx_prop, (acurvenode_key, acurves, acurvenode_name) in acurvenodes.items():
# Animation curve node.
acurvenode = elem_data_single_int64(root, b"AnimationCurveNode", get_fbx_uuid_from_key(acurvenode_key))
Bastien Montagne
committed
acurvenode.add_string(fbx_name_class(acurvenode_name.encode(), b"AnimCurveNode"))
acurvenode.add_string(b"")
acn_tmpl = elem_props_template_init(scene_data.templates, b"AnimationCurveNode")
acn_props = elem_properties(acurvenode)
for fbx_item, (acurve_key, def_value, keys, _acurve_valid) in acurves.items():
Bastien Montagne
committed
elem_props_template_set(acn_tmpl, acn_props, "p_number", fbx_item.encode(),
def_value, animatable=True)
Bastien Montagne
committed
# Only create Animation curve if needed!
if keys:
acurve = elem_data_single_int64(root, b"AnimationCurve", get_fbx_uuid_from_key(acurve_key))
Bastien Montagne
committed
acurve.add_string(fbx_name_class(b"", b"AnimCurve"))
acurve.add_string(b"")
# key attributes...
nbr_keys = len(keys)
# flags...
keyattr_flags = (
1 << 2 | # interpolation mode, 1 = constant, 2 = linear, 3 = cubic.
Bastien Montagne
committed
1 << 8 | # tangent mode, 8 = auto, 9 = TCB, 10 = user, 11 = generic break,
1 << 13 | # tangent mode, 12 = generic clamp, 13 = generic time independent,
1 << 14 | # tangent mode, 13 + 14 = generic clamp progressive.
0,
)
# Maybe values controlling TCB & co???
keyattr_datafloat = (0.0, 0.0, 9.419963346924634e-30, 0.0)
# And now, the *real* data!
elem_data_single_float64(acurve, b"Default", def_value)
elem_data_single_int32(acurve, b"KeyVer", FBX_ANIM_KEY_VERSION)
elem_data_single_int64_array(acurve, b"KeyTime", keys_to_ktimes(keys))
elem_data_single_float32_array(acurve, b"KeyValueFloat", (v for _f, v in keys))
elem_data_single_int32_array(acurve, b"KeyAttrFlags", keyattr_flags)
elem_data_single_float32_array(acurve, b"KeyAttrDataFloat", keyattr_datafloat)
elem_data_single_int32_array(acurve, b"KeyAttrRefCount", (nbr_keys,))
elem_props_template_finalize(acn_tmpl, acn_props)
Bastien Montagne
committed
# ##### Top-level FBX data container. #####
# Mapping Blender -> FBX (principled_socket_name, fbx_name).
PRINCIPLED_TEXTURE_SOCKETS_TO_FBX = (
# ("diffuse", "diffuse", b"DiffuseFactor"),
("base_color_texture", b"DiffuseColor"),
("alpha_texture", b"TransparencyFactor"), # Will be inverted in fact, not much we can do really...
# ("base_color_texture", b"TransparentColor"), # Uses diffuse color in Blender!
# ("emit", "emit", b"EmissiveFactor"),
# ("diffuse", "diffuse", b"EmissiveColor"), # Uses diffuse color in Blender!
# ("ambient", "ambient", b"AmbientFactor"),
# ("", "", b"AmbientColor"), # World stuff in Blender, for now ignore...
("normalmap_texture", b"NormalMap"),
# Note: unsure about those... :/
# ("", "", b"Bump"),
# ("", "", b"BumpFactor"),
# ("", "", b"DisplacementColor"),
# ("", "", b"DisplacementFactor"),
("specular_texture", b"SpecularFactor"),
# ("base_color", b"SpecularColor"), # TODO: use tint?
# See Material template about those two!
("roughness_texture", b"Shininess"),
("roughness_texture", b"ShininessExponent"),
# ("mirror", "mirror", b"ReflectionColor"),
("metallic_texture", b"ReflectionFactor"),
)
Bastien Montagne
committed
def fbx_skeleton_from_armature(scene, settings, arm_obj, objects, data_meshes,
data_bones, data_deformers_skin, data_empties, arm_parents):
"""
Create skeleton from armature/bones (NodeAttribute/LimbNode and Model/LimbNode), and for each deformed mesh,
create Pose/BindPose(with sub PoseNode) and Deformer/Skin(with Deformer/SubDeformer/Cluster).
Also supports "parent to bone" (simple parent to Model/LimbNode).
arm_parents is a set of tuples (armature, object) for all successful armature bindings.
"""
# We need some data for our armature 'object' too!!!
data_empties[arm_obj] = get_blender_empty_key(arm_obj.bdata)
Bastien Montagne
committed
arm_data = arm_obj.bdata.data
bones = {}
Bastien Montagne
committed
for bo in arm_obj.bones:
if settings.use_armature_deform_only:
if bo.bdata.use_deform:
bones[bo] = True
bo_par = bo.parent
while bo_par.is_bone:
bones[bo_par] = True
bo_par = bo_par.parent
elif bo not in bones: # Do not override if already set in the loop above!
bones[bo] = False
else:
bones[bo] = True
bones = {bo: None for bo, use in bones.items() if use}
Bastien Montagne
committed
if not bones:
return
data_bones.update((bo, get_blender_bone_key(arm_obj.bdata, bo.bdata)) for bo in bones)
Bastien Montagne
committed
for ob_obj in objects:
Bastien Montagne
committed
if not ob_obj.is_deformed_by_armature(arm_obj):
continue
# Always handled by an Armature modifier...
found = False
for mod in ob_obj.bdata.modifiers:
if mod.type not in {'ARMATURE'} or not mod.object:
continue
# We only support vertex groups binding method, not bone envelopes one!
if mod.object in {arm_obj.bdata, arm_obj.bdata.proxy} and mod.use_vertex_groups:
found = True
break
if not found:
continue
Bastien Montagne
committed
# Now we have a mesh using this armature.
# Note: bindpose have no relations at all (no connections), so no need for any preprocess for them.
# Create skin & clusters relations (note skins are connected to geometry, *not* model!).
_key, me, _free = data_meshes[ob_obj]
clusters = {bo: get_blender_bone_cluster_key(arm_obj.bdata, me, bo.bdata) for bo in bones}
data_deformers_skin.setdefault(arm_obj, {})[me] = (get_blender_armature_skin_key(arm_obj.bdata, me),
# We don't want a regular parent relationship for those in FBX...
Bastien Montagne
committed
arm_parents.add((arm_obj, ob_obj))
# Needed to handle matrices/spaces (since we do not parent them to 'armature' in FBX :/ ).
ob_obj.parented_to_armature = True
Bastien Montagne
committed
objects.update(bones)
Bastien Montagne
committed
def fbx_generate_leaf_bones(settings, data_bones):
# find which bons have no children
child_count = {bo: 0 for bo in data_bones.keys()}
for bo in data_bones.keys():
Bastien Montagne
committed
if bo.parent and bo.parent.is_bone:
child_count[bo.parent] += 1
bone_radius_scale = settings.global_scale * 33.0
# generate bone data
leaf_parents = [bo for bo, count in child_count.items() if count == 0]
leaf_bones = []
for parent in leaf_parents:
node_name = parent.name + "_end"
parent_uuid = parent.fbx_uuid
parent_key = parent.key
node_uuid = get_fbx_uuid_from_key(parent_key + "_end_node")
attr_uuid = get_fbx_uuid_from_key(parent_key + "_end_nodeattr")
Bastien Montagne
committed
hide = parent.hide
size = parent.bdata.head_radius * bone_radius_scale
bone_length = (parent.bdata.tail_local - parent.bdata.head_local).length
matrix = Matrix.Translation((0, bone_length, 0))
if settings.bone_correction_matrix_inv:
matrix = settings.bone_correction_matrix_inv @ matrix
Bastien Montagne
committed
if settings.bone_correction_matrix:
matrix = matrix @ settings.bone_correction_matrix
Bastien Montagne
committed
leaf_bones.append((node_name, parent_uuid, node_uuid, attr_uuid, matrix, hide, size))
return leaf_bones
def fbx_animations_do(scene_data, ref_id, f_start, f_end, start_zero, objects=None, force_keep=False):
Bastien Montagne
committed
Generate animation data (a single AnimStack) from objects, for a given frame range.
bake_step = scene_data.settings.bake_anim_step
Bastien Montagne
committed
simplify_fac = scene_data.settings.bake_anim_simplify_factor
Bastien Montagne
committed
force_keying = scene_data.settings.bake_anim_use_all_bones
Bastien Montagne
committed
force_sek = scene_data.settings.bake_anim_force_startend_keying
if objects is not None:
Bastien Montagne
committed
# Add bones and duplis!
Bastien Montagne
committed
for ob_obj in tuple(objects):
if not ob_obj.is_object:
Bastien Montagne
committed
continue
Bastien Montagne
committed
if ob_obj.type == 'ARMATURE':
objects |= {bo_obj for bo_obj in ob_obj.bones if bo_obj in scene_data.objects}
for dp_obj in ob_obj.dupli_list_gen(depsgraph):
Bastien Montagne
committed
if dp_obj in scene_data.objects:
objects.add(dp_obj)
Bastien Montagne
committed
objects = scene_data.objects
back_currframe = scene.frame_current
animdata_ob = {}
p_rots = {}
for ob_obj in objects:
if ob_obj.parented_to_armature:
continue
ACNW = AnimationCurveNodeWrapper
loc, rot, scale, _m, _mr = ob_obj.fbx_object_tx(scene_data)
rot_deg = tuple(convert_rad_to_deg_iter(rot))
Bastien Montagne
committed
force_key = (simplify_fac == 0.0) or (ob_obj.is_bone and force_keying)
animdata_ob[ob_obj] = (ACNW(ob_obj.key, 'LCL_TRANSLATION', force_key, force_sek, loc),
ACNW(ob_obj.key, 'LCL_ROTATION', force_key, force_sek, rot_deg),
ACNW(ob_obj.key, 'LCL_SCALING', force_key, force_sek, scale))
p_rots[ob_obj] = rot
Bastien Montagne
committed
force_key = (simplify_fac == 0.0)
animdata_shapes = {}
Bastien Montagne
committed
for me, (me_key, _shapes_key, shapes) in scene_data.data_deformers_shape.items():
# Ignore absolute shape keys for now!
if not me.shape_keys.use_relative:
continue
for shape, (channel_key, geom_key, _shape_verts_co, _shape_verts_idx) in shapes.items():
Bastien Montagne
committed
acnode = AnimationCurveNodeWrapper(channel_key, 'SHAPE_KEY', force_key, force_sek, (0.0,))
# Sooooo happy to have to twist again like a mad snake... Yes, we need to write those curves twice. :/
acnode.add_group(me_key, shape.name, shape.name, (shape.name,))
animdata_shapes[channel_key] = (acnode, me, shape)
animdata_cameras = {}
Bastien Montagne
committed
for cam_obj, cam_key in scene_data.data_cameras.items():
cam = cam_obj.bdata.data
acnode = AnimationCurveNodeWrapper(cam_key, 'CAMERA_FOCAL', force_key, force_sek, (cam.lens,))
animdata_cameras[cam_key] = (acnode, cam)
Bastien Montagne
committed
currframe = f_start
Bastien Montagne
committed
while currframe <= f_end:
real_currframe = currframe - f_start if start_zero else currframe
scene.frame_set(int(currframe), subframe=currframe - int(currframe))
Bastien Montagne
committed
for dp_obj in ob_obj.dupli_list_gen(depsgraph):
pass # Merely updating dupli matrix of ObjectWrapper...
for ob_obj, (anim_loc, anim_rot, anim_scale) in animdata_ob.items():
Bastien Montagne
committed
# We compute baked loc/rot/scale for all objects (rot being euler-compat with previous value!).
Bastien Montagne
committed
p_rot = p_rots.get(ob_obj, None)
loc, rot, scale, _m, _mr = ob_obj.fbx_object_tx(scene_data, rot_euler_compat=p_rot)
p_rots[ob_obj] = rot
anim_loc.add_keyframe(real_currframe, loc)
anim_rot.add_keyframe(real_currframe, tuple(convert_rad_to_deg_iter(rot)))
anim_scale.add_keyframe(real_currframe, scale)
for anim_shape, me, shape in animdata_shapes.values():
anim_shape.add_keyframe(real_currframe, (shape.value * 100.0,))
Bastien Montagne
committed
for anim_camera, camera in animdata_cameras.values():
anim_camera.add_keyframe(real_currframe, (camera.lens,))
scene.frame_set(back_currframe, subframe=0.0)
animations = {}
# And now, produce final data (usable by FBX export code)
# Objects-like loc/rot/scale...
for ob_obj, anims in animdata_ob.items():
for anim in anims:
anim.simplify(simplify_fac, bake_step, force_keep)
Bastien Montagne
committed
if not anim:
continue
for obj_key, group_key, group, fbx_group, fbx_gname in anim.get_final_data(scene, ref_id, force_keep):
anim_data = animations.setdefault(obj_key, ("dummy_unused_key", {}))
Bastien Montagne
committed
anim_data[1][fbx_group] = (group_key, group, fbx_gname)
# And meshes' shape keys.
for channel_key, (anim_shape, me, shape) in animdata_shapes.items():
final_keys = {}
anim_shape.simplify(simplify_fac, bake_step, force_keep)
if not anim_shape:
continue
for elem_key, group_key, group, fbx_group, fbx_gname in anim_shape.get_final_data(scene, ref_id, force_keep):
anim_data = animations.setdefault(elem_key, ("dummy_unused_key", {}))
anim_data[1][fbx_group] = (group_key, group, fbx_gname)
Bastien Montagne
committed
# And cameras' lens keys.
for cam_key, (anim_camera, camera) in animdata_cameras.items():
final_keys = {}
Bastien Montagne
committed
anim_camera.simplify(simplify_fac, bake_step, force_keep)
if not anim_camera:
continue
for elem_key, group_key, group, fbx_group, fbx_gname in anim_camera.get_final_data(scene, ref_id, force_keep):
anim_data = animations.setdefault(elem_key, ("dummy_unused_key", {}))
anim_data[1][fbx_group] = (group_key, group, fbx_gname)
Bastien Montagne
committed
astack_key = get_blender_anim_stack_key(scene, ref_id)
alayer_key = get_blender_anim_layer_key(scene, ref_id)
name = (get_blenderID_name(ref_id) if ref_id else scene.name).encode()
Bastien Montagne
committed
if start_zero:
f_end -= f_start
f_start = 0.0
Bastien Montagne
committed
return (astack_key, animations, alayer_key, name, f_start, f_end) if animations else None
def fbx_animations(scene_data):
Bastien Montagne
committed
"""
Generate global animation data from objects.
"""
scene = scene_data.scene
animations = []
Bastien Montagne
committed
animated = set()
frame_start = 1e100
frame_end = -1e100
Bastien Montagne
committed
Bastien Montagne
committed
def add_anim(animations, animated, anim):
nonlocal frame_start, frame_end
if anim is not None:
animations.append(anim)
f_start, f_end = anim[4:6]
if f_start < frame_start:
frame_start = f_start
if f_end > frame_end:
frame_end = f_end
Bastien Montagne
committed
_astack_key, astack, _alayer_key, _name, _fstart, _fend = anim
for elem_key, (alayer_key, acurvenodes) in astack.items():
for fbx_prop, (acurvenode_key, acurves, acurvenode_name) in acurvenodes.items():
animated.add((elem_key, fbx_prop))
Bastien Montagne
committed
# Per-NLA strip animstacks.
if scene_data.settings.bake_anim_use_nla_strips:
strips = []
Bastien Montagne
committed
ob_actions = []
Bastien Montagne
committed
for ob_obj in scene_data.objects:
Bastien Montagne
committed
# NLA tracks only for objects, not bones!
Bastien Montagne
committed
if not ob_obj.is_object:
Bastien Montagne
committed
continue
Bastien Montagne
committed
ob = ob_obj.bdata # Back to real Blender Object.
if not ob.animation_data:
continue
Bastien Montagne
committed
# We have to remove active action from objects, it overwrites strips actions otherwise...
ob_actions.append((ob, ob.animation_data.action))
ob.animation_data.action = None
Bastien Montagne
committed
for track in ob.animation_data.nla_tracks:
Bastien Montagne
committed
if track.mute:
continue
for strip in track.strips:
if strip.mute:
continue
strips.append(strip)
strip.mute = True
for strip in strips:
strip.mute = False
Bastien Montagne
committed
add_anim(animations, animated,
Bastien Montagne
committed
fbx_animations_do(scene_data, strip, strip.frame_start, strip.frame_end, True, force_keep=True))
Bastien Montagne
committed
strip.mute = True
scene.frame_set(scene.frame_current, subframe=0.0)
Bastien Montagne
committed
for strip in strips:
strip.mute = False
Bastien Montagne
committed
for ob, ob_act in ob_actions:
ob.animation_data.action = ob_act
# All actions.
if scene_data.settings.bake_anim_use_all_actions:
def validate_actions(act, path_resolve):
for fc in act.fcurves:
data_path = fc.data_path
if fc.array_index:
data_path = data_path + "[%d]" % fc.array_index
try:
path_resolve(data_path)
except ValueError:
return False # Invalid.
return True # Valid.
Bastien Montagne
committed
def restore_object(ob_to, ob_from):
# Restore org state of object (ugh :/ ).
props = (
'location', 'rotation_quaternion', 'rotation_axis_angle', 'rotation_euler', 'rotation_mode', 'scale',
'delta_location', 'delta_rotation_euler', 'delta_rotation_quaternion', 'delta_scale',
'lock_location', 'lock_rotation', 'lock_rotation_w', 'lock_rotations_4d', 'lock_scale',
'tag', 'track_axis', 'up_axis', 'active_material', 'active_material_index',
'matrix_parent_inverse', 'empty_display_type', 'empty_display_size', 'empty_image_offset', 'pass_index',
'color', 'hide_viewport', 'hide_select', 'hide_render', 'instance_type',
'use_instance_vertices_rotation', 'use_instance_faces_scale', 'instance_faces_scale',
'display_type', 'show_bounds', 'display_bounds_type', 'show_name', 'show_axis', 'show_texture_space',
'show_wire', 'show_all_edges', 'show_transparent', 'show_in_front',
'show_only_shape_key', 'use_shape_key_edit_mode', 'active_shape_key_index',
)
for p in props:
Bastien Montagne
committed
if not ob_to.is_property_readonly(p):
setattr(ob_to, p, getattr(ob_from, p))
Bastien Montagne
committed
for ob_obj in scene_data.objects:
# Actions only for objects, not bones!
Bastien Montagne
committed
if not ob_obj.is_object:
continue
Bastien Montagne
committed
ob = ob_obj.bdata # Back to real Blender Object.
Bastien Montagne
committed
if not ob.animation_data:
continue # Do not export animations for objects that are absolutely not animated, see T44386.
Bastien Montagne
committed
if ob.animation_data.is_property_readonly('action'):
continue # Cannot re-assign 'active action' to this object (usually related to NLA usage, see T48089).
# We can't play with animdata and actions and get back to org state easily.
# So we have to add a temp copy of the object to the scene, animate it, and remove it... :/
Bastien Montagne
committed
ob_copy = ob.copy()
# Great, have to handle bones as well if needed...
pbones_matrices = [pbo.matrix_basis.copy() for pbo in ob.pose.bones] if ob.type == 'ARMATURE' else ...
Bastien Montagne
committed
org_act = ob.animation_data.action
Bastien Montagne
committed
path_resolve = ob.path_resolve
for act in bpy.data.actions:
# For now, *all* paths in the action must be valid for the object, to validate the action.
# Unless that action was already assigned to the object!
if act != org_act and not validate_actions(act, path_resolve):
continue
Bastien Montagne
committed
ob.animation_data.action = act
frame_start, frame_end = act.frame_range # sic!
Bastien Montagne
committed
add_anim(animations, animated,
Bastien Montagne
committed
fbx_animations_do(scene_data, (ob, act), frame_start, frame_end, True,
objects={ob_obj}, force_keep=True))
if pbones_matrices is not ...:
for pbo, mat in zip(ob.pose.bones, pbones_matrices):
pbo.matrix_basis = mat.copy()
Bastien Montagne
committed
ob.animation_data.action = org_act
Bastien Montagne
committed
restore_object(ob, ob_copy)
scene.frame_set(scene.frame_current, subframe=0.0)
if pbones_matrices is not ...:
for pbo, mat in zip(ob.pose.bones, pbones_matrices):
pbo.matrix_basis = mat.copy()
Bastien Montagne
committed
ob.animation_data.action = org_act
Bastien Montagne
committed
bpy.data.objects.remove(ob_copy)
scene.frame_set(scene.frame_current, subframe=0.0)
# Global (containing everything) animstack, only if not exporting NLA strips and/or all actions.
if not scene_data.settings.bake_anim_use_nla_strips and not scene_data.settings.bake_anim_use_all_actions:
Bastien Montagne
committed
add_anim(animations, animated, fbx_animations_do(scene_data, None, scene.frame_start, scene.frame_end, False))
Bastien Montagne
committed
# Be sure to update all matrices back to org state!
scene.frame_set(scene.frame_current, subframe=0.0)
Bastien Montagne
committed
Bastien Montagne
committed
return animations, animated, frame_start, frame_end
Loading
Loading full blame...