Newer
Older
Bastien Montagne
committed
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
for strip in strips:
strip.mute = False
# 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', 'layers', 'select', 'track_axis', 'up_axis', 'active_material', 'active_material_index',
'matrix_parent_inverse', 'empty_draw_type', 'empty_draw_size', 'empty_image_offset', 'pass_index',
'color', 'hide', 'hide_select', 'hide_render', 'use_slow_parent', 'slow_parent_offset',
'use_extra_recalc_object', 'use_extra_recalc_data', 'dupli_type', 'use_dupli_frames_speed',
'use_dupli_vertices_rotation', 'use_dupli_faces_scale', 'dupli_faces_scale', 'dupli_group',
'dupli_frames_start', 'dupli_frames_end', 'dupli_frames_on', 'dupli_frames_off',
'draw_type', 'show_bounds', 'draw_bounds_type', 'show_name', 'show_axis', 'show_texture_space',
'show_wire', 'show_all_edges', 'show_transparent', 'show_x_ray',
'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.
# 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)
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)
# 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, 0.0)
Bastien Montagne
committed
return animations, animated, frame_start, frame_end
def fbx_data_from_scene(scene, settings):
"""
Do some pre-processing over scene's data...
"""
objtypes = settings.object_types
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?
Bastien Montagne
committed
objects = OrderedDict() # Because we do not have any ordered set...
for ob in settings.context_objects:
if ob.type not in objtypes:
continue
ob_obj = ObjectWrapper(ob)
objects[ob_obj] = None
# Duplis...
ob_obj.dupli_list_create(scene, 'RENDER')
for dp_obj in ob_obj.dupli_list:
objects[dp_obj] = None
ob_obj.dupli_list_clear()
Bastien Montagne
committed
perfmon.step("FBX export prepare: Wrapping Data (lamps, cameras, empties)...")
Bastien Montagne
committed
data_lamps = OrderedDict((ob_obj.bdata.data, get_blenderID_key(ob_obj.bdata.data))
for ob_obj in objects if ob_obj.type == 'LAMP')
# Unfortunately, FBX camera data contains object-level data (like position, orientation, etc.)...
Bastien Montagne
committed
data_cameras = OrderedDict((ob_obj, get_blenderID_key(ob_obj.bdata.data))
for ob_obj in objects if ob_obj.type == 'CAMERA')
# Yep! Contains nothing, but needed!
Bastien Montagne
committed
data_empties = OrderedDict((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...")
Bastien Montagne
committed
data_meshes = OrderedDict()
Bastien Montagne
committed
for ob_obj in objects:
if ob_obj.type not in BLENDER_OBJECT_TYPES_MESHLIKE:
continue
ob = ob_obj.bdata
if ob in data_meshes: # Happens with dupli instances.
Bastien Montagne
committed
continue
Bastien Montagne
committed
if settings.use_mesh_modifiers or ob.type in BLENDER_OTHER_OBJECT_TYPES:
Bastien Montagne
committed
tmp_mods = []
Bastien Montagne
committed
if ob.type == 'MESH':
# No need to create a new mesh in this case, if no modifier is active!
use_org_data = True
Bastien Montagne
committed
for mod in ob.modifiers:
Bastien Montagne
committed
# For meshes, when armature export is enabled, disable Armature modifiers here!
if mod.type == 'ARMATURE' and 'ARMATURE' in settings.object_types:
Bastien Montagne
committed
tmp_mods.append((mod, mod.show_render))
mod.show_render = False
if mod.show_render:
use_org_data = False
if not use_org_data:
Bastien Montagne
committed
tmp_me = ob.to_mesh(scene, apply_modifiers=True, settings='RENDER')
data_meshes[ob_obj] = (get_blenderID_key(tmp_me), tmp_me, True)
Bastien Montagne
committed
# Re-enable temporary disabled modifiers.
for mod, show_render in tmp_mods:
mod.show_render = show_render
data_meshes[ob_obj] = (get_blenderID_key(ob.data), ob.data, False)
Bastien Montagne
committed
perfmon.step("FBX export prepare: Wrapping ShapeKeys...")
# ShapeKeys.
data_deformers_shape = OrderedDict()
Bastien Montagne
committed
geom_mat_co = settings.global_matrix if settings.bake_space_transform else None
for me_obj, (me_key, me, _org) in data_meshes.items():
if not (me.shape_keys and me.shape_keys.key_blocks):
continue
Bastien Montagne
committed
shapes_key = get_blender_mesh_shape_key(me)
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))
for shape in me.shape_keys.key_blocks:
# Only write vertices really different from org coordinates!
# XXX FBX does not like empty shapes (makes Unity crash e.g.), so we have to do this here... :/
shape_verts_co = []
shape_verts_idx = []
Bastien Montagne
committed
shape.data.foreach_get("co", _cos)
sv_cos = tuple(vcos_transformed_gen(_cos, geom_mat_co))
for idx, (sv_co, v_co) in enumerate(zip(sv_cos, v_cos)):
if similar_values_iter(sv_co, v_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(v_co))
if not shape_verts_co:
continue
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, OrderedDict()))[2][shape] = data
Bastien Montagne
committed
Bastien Montagne
committed
perfmon.step("FBX export prepare: Wrapping Armatures...")
data_deformers_skin = OrderedDict()
Bastien Montagne
committed
data_bones = OrderedDict()
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:
Bastien Montagne
committed
data_world = OrderedDict(((scene.world, get_blenderID_key(scene.world)),))
Bastien Montagne
committed
data_world = OrderedDict()
Bastien Montagne
committed
perfmon.step("FBX export prepare: Wrapping Materials...")
# TODO: Check all the mat stuff works even when mats 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...
Bastien Montagne
committed
data_materials = OrderedDict()
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 mat_s in ob_obj.material_slots:
if mat 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!
# We support any kind of 'surface' shader though, better to have some kind of default Lambert than nothing.
Bastien Montagne
committed
# Note we want to keep a 'dummy' empty mat even when we can't really support it, see T41396.
mat_data = data_materials.get(mat)
if mat_data is not None:
mat_data[1].append(ob_obj)
else:
data_materials[mat] = (get_blenderID_key(mat), [ob_obj])
Bastien Montagne
committed
perfmon.step("FBX export prepare: Wrapping Textures...")
# Note FBX textures also hold their mapping info.
# TODO: Support layers?
Bastien Montagne
committed
data_textures = OrderedDict()
# FbxVideo also used to store static images...
Bastien Montagne
committed
data_videos = OrderedDict()
# For now, do not use world textures, don't think they can be linked to anything FBX wise...
for mat in data_materials.keys():
Bastien Montagne
committed
if check_skip_material(mat):
continue
for tex, use_tex in zip(mat.texture_slots, mat.use_textures):
Bastien Montagne
committed
if tex is None or tex.texture is None or not use_tex:
continue
# For now, only consider image textures.
# Note FBX does has support for procedural, but this is not portable at all (opaque blob),
# so not useful for us.
# TODO I think ENVIRONMENT_MAP should be usable in FBX as well, but for now let it aside.
# if tex.texture.type not in {'IMAGE', 'ENVIRONMENT_MAP'}:
if tex.texture.type not in {'IMAGE'}:
continue
img = tex.texture.image
if img is None:
continue
# Find out whether we can actually use this texture for this material, in FBX context.
tex_fbx_props = fbx_mat_properties_from_texture(tex)
if not tex_fbx_props:
continue
tex_data = data_textures.get(tex)
if tex_data is not None:
tex_data[1][mat] = tex_fbx_props
Bastien Montagne
committed
data_textures[tex] = (get_blenderID_key(tex), OrderedDict(((mat, tex_fbx_props),)))
vid_data = data_videos.get(img)
if vid_data is not None:
vid_data[1].append(tex)
else:
data_videos[img] = (get_blenderID_key(img), [tex])
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,
Bastien Montagne
committed
settings, scene, objects, None, None, 0.0, 0.0,
Bastien Montagne
committed
data_empties, data_lamps, 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 = OrderedDict()
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_lamps:
templates[b"Light"] = fbx_template_def_light(scene, settings, nbr_users=len(data_lamps))
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(data_meshes)
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:
Bastien Montagne
committed
if ob_obj.type == 'LAMP':
lamp_key = data_lamps[ob_obj.bdata.data]
connections.append((b"OO", get_fbx_uuid_from_key(lamp_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))
Bastien Montagne
committed
mesh_mat_indices = OrderedDict()
Bastien Montagne
committed
for mat, (mat_key, ob_objs) in data_materials.items():
for ob_obj in ob_objs:
connections.append((b"OO", get_fbx_uuid_from_key(mat_key), ob_obj.fbx_uuid, None))
# Get index of this mat for this object (or dupliobject).
# Mat indices for mesh faces are determined by their order in 'mat to ob' connections.
# Only mats for meshes currently...
# Note in case of dupliobjects a same me/mat 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_mat_indices.setdefault(me, OrderedDict())[mat] = idx
del _objs_indices
# Textures
for tex, (tex_key, mats) in data_textures.items():
for mat, fbx_mat_props in mats.items():
Bastien Montagne
committed
mat_key, _ob_objs = data_materials[mat]
for fbx_prop in fbx_mat_props:
# texture -> material properties
connections.append((b"OP", get_fbx_uuid_from_key(tex_key), get_fbx_uuid_from_key(mat_key), fbx_prop))
# Images
for vid, (vid_key, texs) in data_videos.items():
for tex in texs:
tex_key, _texs = data_textures[tex]
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,
Bastien Montagne
committed
settings, scene, objects, animations, animated, frame_start, frame_end,
data_empties, data_lamps, data_cameras, data_meshes, mesh_mat_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.
for _key, me, free in scene_data.data_meshes.values():
if free:
bpy.data.meshes.remove(me)
# ##### 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
# ##### Start of FBXHeaderExtension element.
2543
2544
2545
2546
2547
2548
2549
2550
2551
2552
2553
2554
2555
2556
2557
2558
2559
2560
2561
2562
2563
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)
Bastien Montagne
committed
elem_data_single_string_unicode(header_ext, b"Creator", "%s - %s" % (app_name, app_ver))
2565
2566
2567
2568
2569
2570
2571
2572
2573
2574
2575
2576
2577
2578
2579
2580
2581
2582
2583
2584
2585
# '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))
Bastien Montagne
committed
elem_data_single_string_unicode(root, b"Creator", "%s - %s" % (app_name, app_ver))
# ##### 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]
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 = scene_data.settings.global_scale / units_blender_to_fbx_factor(scene)
else:
scale_factor_org = units_blender_to_fbx_factor(scene)
scale_factor = scene_data.settings.global_scale * units_blender_to_fbx_factor(scene)
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)
2670
2671
2672
2673
2674
2675
2676
2677
2678
2679
2680
2681
2682
2683
2684
2685
2686
2687
2688
2689
2690
2691
2692
2693
2694
2695
2696
2697
2698
2699
2700
2701
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)
Bastien Montagne
committed
perfmon.step("FBX export fetch lamps (%d)..." % len(scene_data.data_lamps))
Bastien Montagne
committed
for lamp in scene_data.data_lamps:
fbx_data_lamp_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)
Bastien Montagne
committed
perfmon.step("FBX export fetch meshes (%d)..." % len(scene_data.data_meshes))
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)
ob_obj.dupli_list_create(scene_data.scene, 'RENDER')
for dp_obj in ob_obj.dupli_list:
if dp_obj not in scene_data.objects:
continue
fbx_data_object_elements(objects, dp_obj, scene_data)
ob_obj.dupli_list_clear()
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)
Bastien Montagne
committed
for mat in scene_data.data_materials:
fbx_data_material_elements(objects, mat, scene_data)
Bastien Montagne
committed
for tex in scene_data.data_textures:
fbx_data_texture_file_elements(objects, tex, 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, filepath="",
global_matrix=Matrix(),
axis_up="Z",
axis_forward="Y",
context_objects=None,
object_types=None,
use_mesh_modifiers=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
# Clear cached ObjectWrappers (just in case...).
ObjectWrapper.cache_clear()
if object_types is None:
Bastien Montagne
committed
object_types = {'EMPTY', 'CAMERA', 'LAMP', 'ARMATURE', 'MESH', 'OTHER'}
if 'OTHER' in object_types:
object_types |= BLENDER_OTHER_OBJECT_TYPES
if apply_unit_scale:
global_matrix = global_matrix * Matrix.Scale(units_blender_to_fbx_factor(scene), 4)
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
Bastien Montagne
committed
# Calcuate bone correction matrix
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,
Bastien Montagne
committed
bake_space_transform, global_matrix_inv, global_matrix_inv_transposed,
context_objects, object_types, use_mesh_modifiers,
Bastien Montagne
committed
mesh_smooth_type, use_mesh_edges, use_tspace,
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,
2889
2890
2891
2892
2893
2894
2895
2896
2897
2898
2899
2900
2901
2902
2903
2904
2905
2906
2907
2908
2909
2910
2911
2912
2913
2914
2915
2916
2917
2918
2919
)
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, 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.
fbx_objects_elements(root, scene_data)
# How data are inter-connected.
fbx_connections_elements(root, scene_data)
# Animation.
fbx_takes_elements(root, scene_data)
Bastien Montagne
committed
# Cleanup!
fbx_scene_data_cleanup(scene_data)
# And we are down, we can write the whole thing!
encode_bin.write(filepath, root, FBX_VERSION)
Bastien Montagne
committed
# Clear cached ObjectWrappers!
ObjectWrapper.cache_clear()
# copy all collected files, if we did not embed them.
if not media_settings.embed_textures:
bpy_extras.io_utils.path_reference_copy(media_settings.copy_set)
print('export finished in %.4f sec.' % (time.process_time() - start_time))
return {'FINISHED'}
# defaults for applications, currently only unity but could add others.
def defaults_unity3d():
return {
# These options seem to produce the same result as the old Ascii exporter in Unity3D:
"version": 'BIN7400',
"axis_up": 'Y',
"axis_forward": '-Z',
"global_matrix": Matrix.Rotation(-math.pi / 2.0, 4, 'X'),
# Should really be True, but it can cause problems if a model is already in a scene or prefab
# with the old transforms.
"bake_space_transform": False,
"use_selection": False,
"object_types": {'ARMATURE', 'EMPTY', 'MESH', 'OTHER'},
"use_mesh_modifiers": True,
"use_mesh_edges": False,
"mesh_smooth_type": 'FACE',
"use_tspace": False, # XXX Why? Unity is expected to support tspace import...
"use_armature_deform_only": True,
"use_custom_props": True,
"bake_anim_simplify_factor": 1.0,
"bake_anim_step": 1.0,
"bake_anim_use_nla_strips": True,
"bake_anim_use_all_actions": True,
Bastien Montagne
committed
"add_leaf_bones": False, # Avoid memory/performance cost for something only useful for modelling
"primary_bone_axis": 'Y', # Doesn't really matter for Unity, so leave unchanged
"secondary_bone_axis": 'X',
"path_mode": 'AUTO',
"embed_textures": False,
2972
2973
2974
2975
2976
2977
2978
2979
2980
2981
2982
2983
2984
2985
2986
2987
2988
2989
2990
2991
2992
2993
2994
2995
2996
2997
2998
2999
3000
"batch_mode": 'OFF',
}
def save(operator, context,
filepath="",
use_selection=False,
batch_mode='OFF',
use_batch_own_dir=False,
**kwargs
):
"""
This is a wrapper around save_single, which handles multi-scenes (or groups) cases, when batch-exporting a whole
.blend file.
"""
ret = None
org_mode = None
if context.active_object and context.active_object.mode != 'OBJECT' and bpy.ops.object.mode_set.poll():
org_mode = context.active_object.mode
bpy.ops.object.mode_set(mode='OBJECT')
if batch_mode == 'OFF':
kwargs_mod = kwargs.copy()
if use_selection:
kwargs_mod["context_objects"] = context.selected_objects
else:
kwargs_mod["context_objects"] = context.scene.objects