Newer
Older
Bastien Montagne
committed
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
def fbx_data_from_scene(scene, depsgraph, settings):
"""
Do some pre-processing over scene's data...
"""
objtypes = settings.object_types
Bastien Montagne
committed
dp_objtypes = objtypes - {'ARMATURE'} # Armatures are not supported as dupli instances currently...
Bastien Montagne
committed
perfmon = PerfMon()
perfmon.level_up()
Bastien Montagne
committed
perfmon.step("FBX export prepare: Wrapping Objects...")
# This is rather simple for now, maybe we could end generating templates with most-used values
# instead of default ones?
objects = {} # Because we do not have any ordered set...
Bastien Montagne
committed
for ob in settings.context_objects:
if ob.type not in objtypes:
continue
ob_obj = ObjectWrapper(ob)
objects[ob_obj] = None
# Duplis...
for dp_obj in ob_obj.dupli_list_gen(depsgraph):
Bastien Montagne
committed
if dp_obj.type not in dp_objtypes:
continue
Bastien Montagne
committed
objects[dp_obj] = None
Bastien Montagne
committed
perfmon.step("FBX export prepare: Wrapping Data (lamps, cameras, empties)...")
data_lights = {ob_obj.bdata.data: get_blenderID_key(ob_obj.bdata.data)
for ob_obj in objects if ob_obj.type == 'LIGHT'}
# Unfortunately, FBX camera data contains object-level data (like position, orientation, etc.)...
data_cameras = {ob_obj: get_blenderID_key(ob_obj.bdata.data)
for ob_obj in objects if ob_obj.type == 'CAMERA'}
# Yep! Contains nothing, but needed!
data_empties = {ob_obj: get_blender_empty_key(ob_obj.bdata)
for ob_obj in objects if ob_obj.type == 'EMPTY'}
Bastien Montagne
committed
perfmon.step("FBX export prepare: Wrapping Meshes...")
data_meshes = {}
Bastien Montagne
committed
for ob_obj in objects:
if ob_obj.type not in BLENDER_OBJECT_TYPES_MESHLIKE:
continue
ob = ob_obj.bdata
org_ob_obj = None
# Do not want to systematically recreate a new mesh for dupliobject instances, kind of break purpose of those.
if ob_obj.is_dupli:
org_ob_obj = ObjectWrapper(ob) # We get the "real" object wrapper from that dupli instance.
if org_ob_obj in data_meshes:
data_meshes[ob_obj] = data_meshes[org_ob_obj]
continue
Bastien Montagne
committed
is_ob_material = any(ms.link == 'OBJECT' for ms in ob.material_slots)
if settings.use_mesh_modifiers or ob.type in BLENDER_OTHER_OBJECT_TYPES or is_ob_material:
# We cannot use default mesh in that case, or material would not be the right ones...
use_org_data = not (is_ob_material or ob.type in BLENDER_OTHER_OBJECT_TYPES)
Bastien Montagne
committed
tmp_mods = []
Bastien Montagne
committed
if use_org_data and ob.type == 'MESH':
# No need to create a new mesh in this case, if no modifier is active!
Bastien Montagne
committed
for mod in ob.modifiers:
Bastien Montagne
committed
# For meshes, when armature export is enabled, disable Armature modifiers here!
# XXX Temp hacks here since currently we only have access to a viewport depsgraph...
Bastien Montagne
committed
if mod.type == 'ARMATURE' and 'ARMATURE' in settings.object_types:
tmp_mods.append((mod, mod.show_render, mod.show_viewport))
Bastien Montagne
committed
mod.show_render = False
mod.show_viewport = False
if mod.show_render or mod.show_viewport:
use_org_data = False
if not use_org_data:
depsgraph,
apply_modifiers=settings.use_mesh_modifiers)
data_meshes[ob_obj] = (get_blenderID_key(tmp_me), tmp_me, True)
Bastien Montagne
committed
# Re-enable temporary disabled modifiers.
for mod, show_render, show_viewport in tmp_mods:
Bastien Montagne
committed
mod.show_render = show_render
mod.show_viewport = show_viewport
data_meshes[ob_obj] = (get_blenderID_key(ob.data), ob.data, False)
# In case "real" source object of that dupli did not yet still existed in data_meshes, create it now!
if org_ob_obj is not None:
data_meshes[org_ob_obj] = data_meshes[ob_obj]
Bastien Montagne
committed
perfmon.step("FBX export prepare: Wrapping ShapeKeys...")
data_deformers_shape = {}
Bastien Montagne
committed
geom_mat_co = settings.global_matrix if settings.bake_space_transform else None
for me_key, me, _free in data_meshes.values():
if not (me.shape_keys and len(me.shape_keys.key_blocks) > 1): # We do not want basis-only relative skeys...
if me in data_deformers_shape:
continue
Bastien Montagne
committed
shapes_key = get_blender_mesh_shape_key(me)
# We gather all vcos first, since some skeys may be based on others...
Bastien Montagne
committed
_cos = array.array(data_types.ARRAY_FLOAT64, (0.0,)) * len(me.vertices) * 3
me.vertices.foreach_get("co", _cos)
v_cos = tuple(vcos_transformed_gen(_cos, geom_mat_co))
sk_cos = {}
for shape in me.shape_keys.key_blocks[1:]:
shape.data.foreach_get("co", _cos)
sk_cos[shape] = tuple(vcos_transformed_gen(_cos, geom_mat_co))
sk_base = me.shape_keys.key_blocks[0]
for shape in me.shape_keys.key_blocks[1:]:
# Only write vertices really different from org coordinates!
shape_verts_co = []
shape_verts_idx = []
Bastien Montagne
committed
sv_cos = sk_cos[shape]
ref_cos = v_cos if shape.relative_key == sk_base else sk_cos[shape.relative_key]
for idx, (sv_co, ref_co) in enumerate(zip(sv_cos, ref_cos)):
if similar_values_iter(sv_co, ref_co):
# Note: Maybe this is a bit too simplistic, should we use real shape base here? Though FBX does not
# have this at all... Anyway, this should cover most common cases imho.
continue
shape_verts_co.extend(Vector(sv_co) - Vector(ref_co))
# FBX does not like empty shapes (makes Unity crash e.g.).
# To prevent this, we add a vertex that does nothing, but it keeps the shape key intact
if not shape_verts_co:
shape_verts_co.extend((0, 0, 0))
shape_verts_idx.append(0)
channel_key, geom_key = get_blender_mesh_shape_channel_key(me, shape)
data = (channel_key, geom_key, shape_verts_co, shape_verts_idx)
data_deformers_shape.setdefault(me, (me_key, shapes_key, {}))[2][shape] = data
Bastien Montagne
committed
Bastien Montagne
committed
perfmon.step("FBX export prepare: Wrapping Armatures...")
data_deformers_skin = {}
data_bones = {}
Bastien Montagne
committed
for ob_obj in tuple(objects):
if not (ob_obj.is_object and ob_obj.type in {'ARMATURE'}):
Bastien Montagne
committed
fbx_skeleton_from_armature(scene, settings, ob_obj, objects, data_meshes,
data_bones, data_deformers_skin, data_empties, arm_parents)
Bastien Montagne
committed
# Generate leaf bones
Bastien Montagne
committed
data_leaf_bones = []
Bastien Montagne
committed
if settings.add_leaf_bones:
data_leaf_bones = fbx_generate_leaf_bones(settings, data_bones)
Bastien Montagne
committed
perfmon.step("FBX export prepare: Wrapping World...")
# Some world settings are embedded in FBX materials...
if scene.world:
data_world = {scene.world: get_blenderID_key(scene.world)}
data_world = {}
Bastien Montagne
committed
perfmon.step("FBX export prepare: Wrapping Materials...")
# TODO: Check all the material stuff works even when they are linked to Objects
# (we can then have the same mesh used with different materials...).
# *Should* work, as FBX always links its materials to Models (i.e. objects).
# XXX However, material indices would probably break...
data_materials = {}
Bastien Montagne
committed
for ob_obj in objects:
# If obj is not a valid object for materials, wrapper will just return an empty tuple...
for ma_s in ob_obj.material_slots:
ma = ma_s.material
if ma is None:
continue # Empty slots!
# Note theoretically, FBX supports any kind of materials, even GLSL shaders etc.
# However, I doubt anything else than Lambert/Phong is really portable!
# Note we want to keep a 'dummy' empty material even when we can't really support it, see T41396.
ma_data = data_materials.setdefault(ma, (get_blenderID_key(ma), []))
ma_data[1].append(ob_obj)
Bastien Montagne
committed
perfmon.step("FBX export prepare: Wrapping Textures...")
# Note FBX textures also hold their mapping info.
# TODO: Support layers?
data_textures = {}
# FbxVideo also used to store static images...
data_videos = {}
# For now, do not use world textures, don't think they can be linked to anything FBX wise...
for ma in data_materials.keys():
# Note: with nodal shaders, we'll could be generating much more textures, but that's kind of unavoidable,
# given that textures actually do not exist anymore in material context in Blender...
ma_wrap = node_shader_utils.PrincipledBSDFWrapper(ma, is_readonly=True)
for sock_name, fbx_name in PRINCIPLED_TEXTURE_SOCKETS_TO_FBX:
tex = getattr(ma_wrap, sock_name)
if tex is None or tex.image is None:
blender_tex_key = (ma, sock_name)
data_textures[blender_tex_key] = (get_blender_nodetexture_key(*blender_tex_key), fbx_name)
img = tex.image
vid_data = data_videos.setdefault(img, (get_blenderID_key(img), []))
vid_data[1].append(blender_tex_key)
Bastien Montagne
committed
perfmon.step("FBX export prepare: Wrapping Animations...")
Bastien Montagne
committed
animations = ()
Bastien Montagne
committed
animated = set()
frame_start = scene.frame_start
frame_end = scene.frame_end
Bastien Montagne
committed
if settings.bake_anim:
# From objects & bones only for a start.
# Kind of hack, we need a temp scene_data for object's space handling to bake animations...
tmp_scdata = FBXExportData(
Bastien Montagne
committed
None, None, None,
settings, scene, depsgraph, objects, None, None, 0.0, 0.0,
data_empties, data_lights, data_cameras, data_meshes, None,
Bastien Montagne
committed
data_bones, data_leaf_bones, data_deformers_skin, data_deformers_shape,
Bastien Montagne
committed
data_world, data_materials, data_textures, data_videos,
)
Bastien Montagne
committed
animations, animated, frame_start, frame_end = fbx_animations(tmp_scdata)
Bastien Montagne
committed
perfmon.step("FBX export prepare: Generating templates...")
templates = {}
templates[b"GlobalSettings"] = fbx_template_def_globalsettings(scene, settings, nbr_users=1)
if data_empties:
templates[b"Null"] = fbx_template_def_null(scene, settings, nbr_users=len(data_empties))
if data_lights:
templates[b"Light"] = fbx_template_def_light(scene, settings, nbr_users=len(data_lights))
if data_cameras:
templates[b"Camera"] = fbx_template_def_camera(scene, settings, nbr_users=len(data_cameras))
if data_bones:
templates[b"Bone"] = fbx_template_def_bone(scene, settings, nbr_users=len(data_bones))
if data_meshes:
nbr = len({me_key for me_key, _me, _free in data_meshes.values()})
if data_deformers_shape:
nbr += sum(len(shapes[2]) for shapes in data_deformers_shape.values())
templates[b"Geometry"] = fbx_template_def_geometry(scene, settings, nbr_users=nbr)
if objects:
templates[b"Model"] = fbx_template_def_model(scene, settings, nbr_users=len(objects))
if arm_parents:
# Number of Pose|BindPose elements should be the same as number of meshes-parented-to-armatures
templates[b"BindPose"] = fbx_template_def_pose(scene, settings, nbr_users=len(arm_parents))
if data_deformers_skin or data_deformers_shape:
nbr = 0
if data_deformers_skin:
nbr += len(data_deformers_skin)
nbr += sum(len(clusters) for def_me in data_deformers_skin.values() for a, b, clusters in def_me.values())
if data_deformers_shape:
nbr += len(data_deformers_shape)
nbr += sum(len(shapes[2]) for shapes in data_deformers_shape.values())
assert(nbr != 0)
templates[b"Deformers"] = fbx_template_def_deformer(scene, settings, nbr_users=nbr)
# No world support in FBX...
"""
if data_world:
templates[b"World"] = fbx_template_def_world(scene, settings, nbr_users=len(data_world))
"""
if data_materials:
templates[b"Material"] = fbx_template_def_material(scene, settings, nbr_users=len(data_materials))
if data_textures:
templates[b"TextureFile"] = fbx_template_def_texture_file(scene, settings, nbr_users=len(data_textures))
if data_videos:
templates[b"Video"] = fbx_template_def_video(scene, settings, nbr_users=len(data_videos))
Bastien Montagne
committed
nbr_astacks = len(animations)
nbr_acnodes = 0
nbr_acurves = 0
for _astack_key, astack, _al, _n, _fs, _fe in animations:
for _alayer_key, alayer in astack.values():
for _acnode_key, acnode, _acnode_name in alayer.values():
nbr_acnodes += 1
for _acurve_key, _dval, acurve, acurve_valid in acnode.values():
if acurve:
nbr_acurves += 1
templates[b"AnimationStack"] = fbx_template_def_animstack(scene, settings, nbr_users=nbr_astacks)
Bastien Montagne
committed
# Would be nice to have one layer per animated object, but this seems tricky and not that well supported.
Bastien Montagne
committed
# So for now, only one layer per anim stack.
templates[b"AnimationLayer"] = fbx_template_def_animlayer(scene, settings, nbr_users=nbr_astacks)
templates[b"AnimationCurveNode"] = fbx_template_def_animcurvenode(scene, settings, nbr_users=nbr_acnodes)
templates[b"AnimationCurve"] = fbx_template_def_animcurve(scene, settings, nbr_users=nbr_acurves)
templates_users = sum(tmpl.nbr_users for tmpl in templates.values())
# ##### Creation of connections...
Bastien Montagne
committed
perfmon.step("FBX export prepare: Generating Connections...")
connections = []
# Objects (with classical parenting).
Bastien Montagne
committed
for ob_obj in objects:
# Bones are handled later.
Bastien Montagne
committed
if not ob_obj.is_bone:
par_obj = ob_obj.parent
# Meshes parented to armature are handled separately, yet we want the 'no parent' connection (0).
if par_obj and ob_obj.has_valid_parent(objects) and (par_obj, ob_obj) not in arm_parents:
connections.append((b"OO", ob_obj.fbx_uuid, par_obj.fbx_uuid, None))
else:
connections.append((b"OO", ob_obj.fbx_uuid, 0, None))
# Armature & Bone chains.
Bastien Montagne
committed
for bo_obj in data_bones.keys():
par_obj = bo_obj.parent
if par_obj not in objects:
Bastien Montagne
committed
connections.append((b"OO", bo_obj.fbx_uuid, par_obj.fbx_uuid, None))
Bastien Montagne
committed
for ob_obj in objects:
if ob_obj.is_bone:
bo_data_key = data_bones[ob_obj]
connections.append((b"OO", get_fbx_uuid_from_key(bo_data_key), ob_obj.fbx_uuid, None))
Bastien Montagne
committed
else:
if ob_obj.type == 'LIGHT':
light_key = data_lights[ob_obj.bdata.data]
connections.append((b"OO", get_fbx_uuid_from_key(light_key), ob_obj.fbx_uuid, None))
Bastien Montagne
committed
elif ob_obj.type == 'CAMERA':
cam_key = data_cameras[ob_obj]
connections.append((b"OO", get_fbx_uuid_from_key(cam_key), ob_obj.fbx_uuid, None))
elif ob_obj.type == 'EMPTY' or ob_obj.type == 'ARMATURE':
Bastien Montagne
committed
empty_key = data_empties[ob_obj]
connections.append((b"OO", get_fbx_uuid_from_key(empty_key), ob_obj.fbx_uuid, None))
Bastien Montagne
committed
elif ob_obj.type in BLENDER_OBJECT_TYPES_MESHLIKE:
mesh_key, _me, _free = data_meshes[ob_obj]
connections.append((b"OO", get_fbx_uuid_from_key(mesh_key), ob_obj.fbx_uuid, None))
Bastien Montagne
committed
# Leaf Bones
for (_node_name, par_uuid, node_uuid, attr_uuid, _matrix, _hide, _size) in data_leaf_bones:
connections.append((b"OO", node_uuid, par_uuid, None))
Bastien Montagne
committed
connections.append((b"OO", attr_uuid, node_uuid, None))
# 'Shape' deformers (shape keys, only for meshes currently)...
for me_key, shapes_key, shapes in data_deformers_shape.values():
# shape -> geometry
connections.append((b"OO", get_fbx_uuid_from_key(shapes_key), get_fbx_uuid_from_key(me_key), None))
for channel_key, geom_key, _shape_verts_co, _shape_verts_idx in shapes.values():
# shape channel -> shape
connections.append((b"OO", get_fbx_uuid_from_key(channel_key), get_fbx_uuid_from_key(shapes_key), None))
# geometry (keys) -> shape channel
connections.append((b"OO", get_fbx_uuid_from_key(geom_key), get_fbx_uuid_from_key(channel_key), None))
# 'Skin' deformers (armature-to-geometry, only for meshes currently)...
for arm, deformed_meshes in data_deformers_skin.items():
Bastien Montagne
committed
for me, (skin_key, ob_obj, clusters) in deformed_meshes.items():
mesh_key, _me, _free = data_meshes[ob_obj]
Bastien Montagne
committed
assert(me == _me)
connections.append((b"OO", get_fbx_uuid_from_key(skin_key), get_fbx_uuid_from_key(mesh_key), None))
Bastien Montagne
committed
for bo_obj, clstr_key in clusters.items():
connections.append((b"OO", get_fbx_uuid_from_key(clstr_key), get_fbx_uuid_from_key(skin_key), None))
connections.append((b"OO", bo_obj.fbx_uuid, get_fbx_uuid_from_key(clstr_key), None))
mesh_material_indices = {}
for ma, (ma_key, ob_objs) in data_materials.items():
Bastien Montagne
committed
for ob_obj in ob_objs:
connections.append((b"OO", get_fbx_uuid_from_key(ma_key), ob_obj.fbx_uuid, None))
# Get index of this material for this object (or dupliobject).
# Material indices for mesh faces are determined by their order in 'ma to ob' connections.
# Only materials for meshes currently...
# Note in case of dupliobjects a same me/ma idx will be generated several times...
# Should not be an issue in practice, and it's needed in case we export duplis but not the original!
if ob_obj.type not in BLENDER_OBJECT_TYPES_MESHLIKE:
continue
_mesh_key, me, _free = data_meshes[ob_obj]
idx = _objs_indices[ob_obj] = _objs_indices.get(ob_obj, -1) + 1
mesh_material_indices.setdefault(me, {})[ma] = idx
del _objs_indices
# Textures
for (ma, sock_name), (tex_key, fbx_prop) in data_textures.items():
ma_key, _ob_objs = data_materials[ma]
# texture -> material properties
connections.append((b"OP", get_fbx_uuid_from_key(tex_key), get_fbx_uuid_from_key(ma_key), fbx_prop))
for vid, (vid_key, blender_tex_keys) in data_videos.items():
for blender_tex_key in blender_tex_keys:
tex_key, _fbx_prop = data_textures[blender_tex_key]
connections.append((b"OO", get_fbx_uuid_from_key(vid_key), get_fbx_uuid_from_key(tex_key), None))
Bastien Montagne
committed
for astack_key, astack, alayer_key, _name, _fstart, _fend in animations:
# Animstack itself is linked nowhere!
astack_id = get_fbx_uuid_from_key(astack_key)
Bastien Montagne
committed
# For now, only one layer!
alayer_id = get_fbx_uuid_from_key(alayer_key)
Bastien Montagne
committed
connections.append((b"OO", alayer_id, astack_id, None))
for elem_key, (alayer_key, acurvenodes) in astack.items():
elem_id = get_fbx_uuid_from_key(elem_key)
# alayer_id = get_fbx_uuid_from_key(alayer_key)
Bastien Montagne
committed
# connections.append((b"OO", alayer_id, astack_id, None))
Bastien Montagne
committed
for fbx_prop, (acurvenode_key, acurves, acurvenode_name) in acurvenodes.items():
# Animcurvenode -> animalayer.
acurvenode_id = get_fbx_uuid_from_key(acurvenode_key)
connections.append((b"OO", acurvenode_id, alayer_id, None))
# Animcurvenode -> object property.
connections.append((b"OP", acurvenode_id, elem_id, fbx_prop.encode()))
for fbx_item, (acurve_key, default_value, acurve, acurve_valid) in acurves.items():
if acurve:
# Animcurve -> Animcurvenode.
connections.append((b"OP", get_fbx_uuid_from_key(acurve_key), acurvenode_id, fbx_item.encode()))
Bastien Montagne
committed
perfmon.level_down()
Jens Ch. Restemeier
committed
return FBXExportData(
templates, templates_users, connections,
settings, scene, depsgraph, objects, animations, animated, frame_start, frame_end,
data_empties, data_lights, data_cameras, data_meshes, mesh_material_indices,
Bastien Montagne
committed
data_bones, data_leaf_bones, data_deformers_skin, data_deformers_shape,
data_world, data_materials, data_textures, data_videos,
)
Bastien Montagne
committed
def fbx_scene_data_cleanup(scene_data):
"""
Some final cleanup...
"""
# Delete temp meshes.
done_meshes = set()
for me_key, me, free in scene_data.data_meshes.values():
if free and me_key not in done_meshes:
Bastien Montagne
committed
bpy.data.meshes.remove(me)
done_meshes.add(me_key)
Bastien Montagne
committed
# ##### Top-level FBX elements generators. #####
def fbx_header_elements(root, scene_data, time=None):
"""
Write boiling code of FBX root.
time is expected to be a datetime.datetime object, or None (using now() in this case).
"""
Bastien Montagne
committed
app_vendor = "Blender Foundation"
app_name = "Blender (stable FBX IO)"
app_ver = bpy.app.version_string
import addon_utils
import sys
addon_ver = addon_utils.module_bl_info(sys.modules[__package__])['version']
# ##### Start of FBXHeaderExtension element.
2612
2613
2614
2615
2616
2617
2618
2619
2620
2621
2622
2623
2624
2625
2626
2627
2628
2629
2630
2631
2632
header_ext = elem_empty(root, b"FBXHeaderExtension")
elem_data_single_int32(header_ext, b"FBXHeaderVersion", FBX_HEADER_VERSION)
elem_data_single_int32(header_ext, b"FBXVersion", FBX_VERSION)
# No encryption!
elem_data_single_int32(header_ext, b"EncryptionType", 0)
if time is None:
time = datetime.datetime.now()
elem = elem_empty(header_ext, b"CreationTimeStamp")
elem_data_single_int32(elem, b"Version", 1000)
elem_data_single_int32(elem, b"Year", time.year)
elem_data_single_int32(elem, b"Month", time.month)
elem_data_single_int32(elem, b"Day", time.day)
elem_data_single_int32(elem, b"Hour", time.hour)
elem_data_single_int32(elem, b"Minute", time.minute)
elem_data_single_int32(elem, b"Second", time.second)
elem_data_single_int32(elem, b"Millisecond", time.microsecond // 1000)
elem_data_single_string_unicode(header_ext, b"Creator", "%s - %s - %d.%d.%d"
% (app_name, app_ver, addon_ver[0], addon_ver[1], addon_ver[2]))
2635
2636
2637
2638
2639
2640
2641
2642
2643
2644
2645
2646
2647
2648
2649
2650
2651
2652
2653
2654
2655
# 'SceneInfo' seems mandatory to get a valid FBX file...
# TODO use real values!
# XXX Should we use scene.name.encode() here?
scene_info = elem_data_single_string(header_ext, b"SceneInfo", fbx_name_class(b"GlobalInfo", b"SceneInfo"))
scene_info.add_string(b"UserData")
elem_data_single_string(scene_info, b"Type", b"UserData")
elem_data_single_int32(scene_info, b"Version", FBX_SCENEINFO_VERSION)
meta_data = elem_empty(scene_info, b"MetaData")
elem_data_single_int32(meta_data, b"Version", FBX_SCENEINFO_VERSION)
elem_data_single_string(meta_data, b"Title", b"")
elem_data_single_string(meta_data, b"Subject", b"")
elem_data_single_string(meta_data, b"Author", b"")
elem_data_single_string(meta_data, b"Keywords", b"")
elem_data_single_string(meta_data, b"Revision", b"")
elem_data_single_string(meta_data, b"Comment", b"")
props = elem_properties(scene_info)
elem_props_set(props, "p_string_url", b"DocumentUrl", "/foobar.fbx")
elem_props_set(props, "p_string_url", b"SrcDocumentUrl", "/foobar.fbx")
original = elem_props_compound(props, b"Original")
Bastien Montagne
committed
original("p_string", b"ApplicationVendor", app_vendor)
original("p_string", b"ApplicationName", app_name)
original("p_string", b"ApplicationVersion", app_ver)
original("p_datetime", b"DateTime_GMT", "01/01/1970 00:00:00.000")
original("p_string", b"FileName", "/foobar.fbx")
lastsaved = elem_props_compound(props, b"LastSaved")
Bastien Montagne
committed
lastsaved("p_string", b"ApplicationVendor", app_vendor)
lastsaved("p_string", b"ApplicationName", app_name)
lastsaved("p_string", b"ApplicationVersion", app_ver)
lastsaved("p_datetime", b"DateTime_GMT", "01/01/1970 00:00:00.000")
# ##### End of FBXHeaderExtension element.
# FileID is replaced by dummy value currently...
elem_data_single_bytes(root, b"FileId", b"FooBar")
# CreationTime is replaced by dummy value currently, but anyway...
elem_data_single_string_unicode(root, b"CreationTime",
"{:04}-{:02}-{:02} {:02}:{:02}:{:02}:{:03}"
"".format(time.year, time.month, time.day, time.hour, time.minute, time.second,
time.microsecond * 1000))
elem_data_single_string_unicode(root, b"Creator", "%s - %s - %d.%d.%d"
% (app_name, app_ver, addon_ver[0], addon_ver[1], addon_ver[2]))
# ##### Start of GlobalSettings element.
global_settings = elem_empty(root, b"GlobalSettings")
scene = scene_data.scene
elem_data_single_int32(global_settings, b"Version", 1000)
props = elem_properties(global_settings)
up_axis, front_axis, coord_axis = RIGHT_HAND_AXES[scene_data.settings.to_axes]
#~ # DO NOT take into account global scale here! That setting is applied to object transformations during export
#~ # (in other words, this is pure blender-exporter feature, and has nothing to do with FBX data).
#~ if scene_data.settings.apply_unit_scale:
#~ # Unit scaling is applied to objects' scale, so our unit is effectively FBX one (centimeter).
#~ scale_factor_org = 1.0
#~ scale_factor = 1.0 / units_blender_to_fbx_factor(scene)
#~ else:
#~ scale_factor_org = units_blender_to_fbx_factor(scene)
#~ scale_factor = scale_factor_org
scale_factor = scale_factor_org = scene_data.settings.unit_scale
elem_props_set(props, "p_integer", b"UpAxis", up_axis[0])
elem_props_set(props, "p_integer", b"UpAxisSign", up_axis[1])
elem_props_set(props, "p_integer", b"FrontAxis", front_axis[0])
elem_props_set(props, "p_integer", b"FrontAxisSign", front_axis[1])
elem_props_set(props, "p_integer", b"CoordAxis", coord_axis[0])
elem_props_set(props, "p_integer", b"CoordAxisSign", coord_axis[1])
elem_props_set(props, "p_integer", b"OriginalUpAxis", -1)
elem_props_set(props, "p_integer", b"OriginalUpAxisSign", 1)
elem_props_set(props, "p_double", b"UnitScaleFactor", scale_factor)
elem_props_set(props, "p_double", b"OriginalUnitScaleFactor", scale_factor_org)
elem_props_set(props, "p_color_rgb", b"AmbientColor", (0.0, 0.0, 0.0))
elem_props_set(props, "p_string", b"DefaultCamera", "Producer Perspective")
# Global timing data.
r = scene.render
_, fbx_fps_mode = FBX_FRAMERATES[0] # Custom framerate.
fbx_fps = fps = r.fps / r.fps_base
Bastien Montagne
committed
for ref_fps, fps_mode in FBX_FRAMERATES:
if similar_values(fps, ref_fps):
fbx_fps = ref_fps
fbx_fps_mode = fps_mode
elem_props_set(props, "p_enum", b"TimeMode", fbx_fps_mode)
Bastien Montagne
committed
elem_props_set(props, "p_timestamp", b"TimeSpanStart", 0)
elem_props_set(props, "p_timestamp", b"TimeSpanStop", FBX_KTIME)
Bastien Montagne
committed
elem_props_set(props, "p_double", b"CustomFrameRate", fbx_fps)
# ##### End of GlobalSettings element.
def fbx_documents_elements(root, scene_data):
"""
Write 'Document' part of FBX root.
Seems like FBX support multiple documents, but until I find examples of such, we'll stick to single doc!
time is expected to be a datetime.datetime object, or None (using now() in this case).
"""
name = scene_data.scene.name
# ##### Start of Documents element.
docs = elem_empty(root, b"Documents")
elem_data_single_int32(docs, b"Count", 1)
doc_uid = get_fbx_uuid_from_key("__FBX_Document__" + name)
doc = elem_data_single_int64(docs, b"Document", doc_uid)
doc.add_string_unicode(name)
2744
2745
2746
2747
2748
2749
2750
2751
2752
2753
2754
2755
2756
2757
2758
2759
2760
2761
2762
2763
2764
2765
2766
2767
2768
2769
2770
2771
2772
2773
2774
2775
doc.add_string_unicode(name)
props = elem_properties(doc)
elem_props_set(props, "p_object", b"SourceObject")
elem_props_set(props, "p_string", b"ActiveAnimStackName", "")
# XXX Some kind of ID? Offset?
# Anyway, as long as we have only one doc, probably not an issue.
elem_data_single_int64(doc, b"RootNode", 0)
def fbx_references_elements(root, scene_data):
"""
Have no idea what references are in FBX currently... Just writing empty element.
"""
docs = elem_empty(root, b"References")
def fbx_definitions_elements(root, scene_data):
"""
Templates definitions. Only used by Objects data afaik (apart from dummy GlobalSettings one).
"""
definitions = elem_empty(root, b"Definitions")
elem_data_single_int32(definitions, b"Version", FBX_TEMPLATES_VERSION)
elem_data_single_int32(definitions, b"Count", scene_data.templates_users)
fbx_templates_generate(definitions, scene_data.templates)
def fbx_objects_elements(root, scene_data):
"""
Bastien Montagne
committed
Data (objects, geometry, material, textures, armatures, etc.).
Bastien Montagne
committed
perfmon = PerfMon()
perfmon.level_up()
objects = elem_empty(root, b"Objects")
Bastien Montagne
committed
perfmon.step("FBX export fetch empties (%d)..." % len(scene_data.data_empties))
Bastien Montagne
committed
for empty in scene_data.data_empties:
fbx_data_empty_elements(objects, empty, scene_data)
perfmon.step("FBX export fetch lamps (%d)..." % len(scene_data.data_lights))
Bastien Montagne
committed
for lamp in scene_data.data_lights:
fbx_data_light_elements(objects, lamp, scene_data)
Bastien Montagne
committed
perfmon.step("FBX export fetch cameras (%d)..." % len(scene_data.data_cameras))
Bastien Montagne
committed
for cam in scene_data.data_cameras:
fbx_data_camera_elements(objects, cam, scene_data)
perfmon.step("FBX export fetch meshes (%d)..."
% len({me_key for me_key, _me, _free in scene_data.data_meshes.values()}))
Bastien Montagne
committed
for me_obj in scene_data.data_meshes:
fbx_data_mesh_elements(objects, me_obj, scene_data, done_meshes)
Bastien Montagne
committed
perfmon.step("FBX export fetch objects (%d)..." % len(scene_data.objects))
Bastien Montagne
committed
for ob_obj in scene_data.objects:
if ob_obj.is_dupli:
Bastien Montagne
committed
continue
Bastien Montagne
committed
fbx_data_object_elements(objects, ob_obj, scene_data)
for dp_obj in ob_obj.dupli_list_gen(scene_data.depsgraph):
Bastien Montagne
committed
if dp_obj not in scene_data.objects:
continue
fbx_data_object_elements(objects, dp_obj, scene_data)
Bastien Montagne
committed
perfmon.step("FBX export fetch remaining...")
Bastien Montagne
committed
for ob_obj in scene_data.objects:
if not (ob_obj.is_object and ob_obj.type == 'ARMATURE'):
Bastien Montagne
committed
fbx_data_armature_elements(objects, ob_obj, scene_data)
Bastien Montagne
committed
if scene_data.data_leaf_bones:
fbx_data_leaf_bone_elements(objects, scene_data)
for ma in scene_data.data_materials:
fbx_data_material_elements(objects, ma, scene_data)
for blender_tex_key in scene_data.data_textures:
fbx_data_texture_file_elements(objects, blender_tex_key, scene_data)
Bastien Montagne
committed
for vid in scene_data.data_videos:
fbx_data_video_elements(objects, vid, scene_data)
Bastien Montagne
committed
perfmon.step("FBX export fetch animations...")
start_time = time.process_time()
fbx_data_animation_elements(objects, scene_data)
Bastien Montagne
committed
perfmon.level_down()
def fbx_connections_elements(root, scene_data):
"""
Relations between Objects (which material uses which texture, and so on).
"""
connections = elem_empty(root, b"Connections")
for c in scene_data.connections:
elem_connection(connections, *c)
def fbx_takes_elements(root, scene_data):
"""
Bastien Montagne
committed
Animations.
# XXX Pretty sure takes are no more needed...
takes = elem_empty(root, b"Takes")
elem_data_single_string(takes, b"Current", b"")
animations = scene_data.animations
for astack_key, animations, alayer_key, name, f_start, f_end in animations:
scene = scene_data.scene
fps = scene.render.fps / scene.render.fps_base
start_ktime = int(convert_sec_to_ktime(f_start / fps))
end_ktime = int(convert_sec_to_ktime(f_end / fps))
take = elem_data_single_string(takes, b"Take", name)
elem_data_single_string(take, b"FileName", name + b".tak")
take_loc_time = elem_data_single_int64(take, b"LocalTime", start_ktime)
take_loc_time.add_int64(end_ktime)
take_ref_time = elem_data_single_int64(take, b"ReferenceTime", start_ktime)
take_ref_time.add_int64(end_ktime)
# This func can be called with just the filepath
def save_single(operator, scene, depsgraph, filepath="",
global_matrix=Matrix(),
global_scale=1.0,
apply_scale_options='FBX_SCALE_NONE',
axis_up="Z",
axis_forward="Y",
context_objects=None,
object_types=None,
use_mesh_modifiers=True,
Bastien Montagne
committed
use_mesh_modifiers_render=True,
mesh_smooth_type='FACE',
use_armature_deform_only=False,
Bastien Montagne
committed
bake_anim_use_all_bones=True,
Bastien Montagne
committed
bake_anim_use_nla_strips=True,
bake_anim_use_all_actions=True,
bake_anim_step=1.0,
bake_anim_simplify_factor=1.0,
Bastien Montagne
committed
bake_anim_force_startend_keying=True,
Bastien Montagne
committed
add_leaf_bones=False,
primary_bone_axis='Y',
secondary_bone_axis='X',
use_metadata=True,
path_mode='AUTO',
use_mesh_edges=True,
use_tspace=True,
embed_textures=False,
use_custom_props=False,
bake_space_transform=False,
Bastien Montagne
committed
armature_nodetype='NULL',
Bastien Montagne
committed
# Clear cached ObjectWrappers (just in case...).
ObjectWrapper.cache_clear()
if object_types is None:
object_types = {'EMPTY', 'CAMERA', 'LIGHT', 'ARMATURE', 'MESH', 'OTHER'}
Bastien Montagne
committed
if 'OTHER' in object_types:
object_types |= BLENDER_OTHER_OBJECT_TYPES
# Default Blender unit is equivalent to meter, while FBX one is centimeter...
unit_scale = units_blender_to_fbx_factor(scene) if apply_unit_scale else 100.0
if apply_scale_options == 'FBX_SCALE_NONE':
global_matrix = Matrix.Scale(unit_scale * global_scale, 4) @ global_matrix
unit_scale = 1.0
elif apply_scale_options == 'FBX_SCALE_UNITS':
global_matrix = Matrix.Scale(global_scale, 4) @ global_matrix
elif apply_scale_options == 'FBX_SCALE_CUSTOM':
global_matrix = Matrix.Scale(unit_scale, 4) @ global_matrix
unit_scale = global_scale
else: # if apply_scale_options == 'FBX_SCALE_ALL':
unit_scale = global_scale * unit_scale
global_scale = global_matrix.median_scale
global_matrix_inv = global_matrix.inverted()
Bastien Montagne
committed
# For transforming mesh normals.
global_matrix_inv_transposed = global_matrix_inv.transposed()
# Only embed textures in COPY mode!
if embed_textures and path_mode != 'COPY':
embed_textures = False
# Calculate bone correction matrix
Bastien Montagne
committed
bone_correction_matrix = None # Default is None = no change
bone_correction_matrix_inv = None
if (primary_bone_axis, secondary_bone_axis) != ('Y', 'X'):
from bpy_extras.io_utils import axis_conversion
bone_correction_matrix = axis_conversion(from_forward=secondary_bone_axis,
from_up=primary_bone_axis,
to_forward='X',
to_up='Y',
).to_4x4()
bone_correction_matrix_inv = bone_correction_matrix.inverted()
Jens Ch. Restemeier
committed
media_settings = FBXExportSettingsMedia(
path_mode,
os.path.dirname(bpy.data.filepath), # base_src
os.path.dirname(filepath), # base_dst
# Local dir where to put images (medias), using FBX conventions.
os.path.splitext(os.path.basename(filepath))[0] + ".fbm", # subdir
embed_textures,
set(), # copy_set
set(), # embedded_set
Jens Ch. Restemeier
committed
settings = FBXExportSettings(
operator.report, (axis_up, axis_forward), global_matrix, global_scale, apply_unit_scale, unit_scale,
Bastien Montagne
committed
bake_space_transform, global_matrix_inv, global_matrix_inv_transposed,
Bastien Montagne
committed
context_objects, object_types, use_mesh_modifiers, use_mesh_modifiers_render,
Bastien Montagne
committed
mesh_smooth_type, use_mesh_edges, use_tspace,
Bastien Montagne
committed
armature_nodetype, use_armature_deform_only,
add_leaf_bones, bone_correction_matrix, bone_correction_matrix_inv,
Bastien Montagne
committed
bake_anim, bake_anim_use_all_bones, bake_anim_use_nla_strips, bake_anim_use_all_actions,
Bastien Montagne
committed
bake_anim_step, bake_anim_simplify_factor, bake_anim_force_startend_keying,
False, media_settings, use_custom_props,
)
import bpy_extras.io_utils
print('\nFBX export starting... %r' % filepath)
start_time = time.process_time()
# Generate some data about exported scene...
scene_data = fbx_data_from_scene(scene, depsgraph, settings)
root = elem_empty(None, b"") # Root element has no id, as it is not saved per se!
# Mostly FBXHeaderExtension and GlobalSettings.
fbx_header_elements(root, scene_data)
# Documents and References are pretty much void currently.
fbx_documents_elements(root, scene_data)
fbx_references_elements(root, scene_data)
# Templates definitions.
fbx_definitions_elements(root, scene_data)
# Actual data.