FBX export: add shape keys support (no anim yet).

This commit is contained in:
Bastien Montagne 2014-07-06 21:08:16 +02:00
parent d994f65cc1
commit 0abd36f1ec
Notes: blender-bot 2023-02-14 20:07:31 +01:00
Referenced by issue #40305, FBX export: Binary FBX does not export shape keys
2 changed files with 192 additions and 55 deletions

View File

@ -51,6 +51,7 @@ from .fbx_utils import (
FBX_GEOMETRY_VERSION, FBX_GEOMETRY_NORMAL_VERSION, FBX_GEOMETRY_BINORMAL_VERSION, FBX_GEOMETRY_TANGENT_VERSION,
FBX_GEOMETRY_SMOOTHING_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,
@ -59,13 +60,14 @@ from .fbx_utils import (
FBX_LIGHT_TYPES, FBX_LIGHT_DECAY_TYPES,
RIGHT_HAND_AXES, FBX_FRAMERATES,
# Miscellaneous utils.
units_convertor, units_convertor_iter, matrix4_to_array, similar_values,
units_convertor, units_convertor_iter, matrix4_to_array, similar_values, similar_values_iter,
# 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_armature_bindpose_key, get_blender_armature_skin_key, get_blender_bone_cluster_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,
# FBX element data.
@ -683,6 +685,105 @@ def fbx_data_camera_elements(root, cam_obj, scene_data):
elem_data_single_float64(cam, b"CameraOrthoZoom", 1.0)
def fbx_data_bindpose_element(root, me_obj, me, scene_data, arm_obj=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)
elem_data_single_int32(fbx_pose, b"NbPoseNodes", 1 + 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))
# 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
# 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)
elem_data_single_float64_array(geom, b"Normals", [0.0] * len(shape_verts_co))
# 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 its 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.
@ -701,7 +802,7 @@ def fbx_data_mesh_elements(root, me_obj, scene_data, done_meshes):
# No gscale/gmat here, all data are supposed to be in object space.
smooth_type = scene_data.settings.mesh_smooth_type
do_bake_space_transform = ObjectWrapper(me_obj).use_bake_space_transform(scene_data)
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.
@ -720,8 +821,6 @@ def fbx_data_mesh_elements(root, me_obj, scene_data, done_meshes):
tmpl = elem_props_template_init(scene_data.templates, b"Geometry")
props = elem_properties(geom)
elem_props_template_finalize(tmpl, props)
# Custom properties.
if scene_data.settings.use_custom_properties:
fbx_data_element_custom_properties(props, me)
@ -1061,6 +1160,10 @@ def fbx_data_mesh_elements(root, me_obj, scene_data, done_meshes):
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)
@ -1272,36 +1375,14 @@ def fbx_data_armature_elements(root, arm_obj, scene_data):
if scene_data.settings.use_custom_properties:
fbx_data_element_custom_properties(props, bo)
# Deformers and BindPoses.
# Skin deformers and BindPoses.
# Note: we might also use Deformers for our "parent to vertex" stuff???
deformer = scene_data.data_deformers.get(arm_obj, None)
deformer = scene_data.data_deformers_skin.get(arm_obj, None)
if deformer is not None:
for me, (skin_key, ob_obj, clusters) in deformer.items():
# BindPose.
# We assume bind pose for our bones are their "Editmode" pose...
# All matrices are expected in global (world) space.
bindpose_key = get_blender_armature_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)
elem_data_single_int32(fbx_pose, b"NbPoseNodes", 1 + len(bones))
# First node is mesh/object.
mat_world_obj = ob_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", ob_obj.fbx_uuid)
elem_data_single_float64_array(fbx_posenode, b"Matrix", matrix4_to_array(mat_world_obj))
# 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))
mat_world_obj, mat_world_bones = fbx_data_bindpose_element(root, ob_obj, me, scene_data, arm_obj, bones)
# Deformer.
fbx_skin = elem_data_single_int64(root, b"Deformer", get_fbx_uuid_from_key(skin_key))
@ -1552,7 +1633,7 @@ def fbx_mat_properties_from_texture(tex):
def fbx_skeleton_from_armature(scene, settings, arm_obj, objects, data_meshes,
data_bones, data_deformers, arm_parents):
data_bones, data_deformers_skin, 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).
@ -1586,9 +1667,8 @@ def fbx_skeleton_from_armature(scene, settings, arm_obj, objects, data_meshes,
continue
# Always handled by an Armature modifier...
ob = ob_obj.bdata
found = False
for mod in ob.modifiers:
for mod in ob_obj.bdata.modifiers:
if mod.type not in {'ARMATURE'}:
continue
# We only support vertex groups binding method, not bone envelopes one!
@ -1602,10 +1682,10 @@ def fbx_skeleton_from_armature(scene, settings, arm_obj, objects, data_meshes,
# 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]
_key, me, _free = data_meshes[ob_obj]
clusters = OrderedDict((bo, get_blender_bone_cluster_key(arm_obj.bdata, me, bo.bdata)) for bo in bones)
data_deformers.setdefault(arm_obj, OrderedDict())[me] = (get_blender_armature_skin_key(arm_obj.bdata, me),
ob_obj, clusters)
data_deformers_skin.setdefault(arm_obj, OrderedDict())[me] = (get_blender_armature_skin_key(arm_obj.bdata, me),
ob_obj, clusters)
# We don't want a regular parent relationship for those in FBX...
arm_parents.add((arm_obj, ob_obj))
@ -1950,22 +2030,46 @@ def fbx_data_from_scene(scene, settings):
use_org_data = False
if not use_org_data:
tmp_me = ob.to_mesh(scene, apply_modifiers=True, settings='RENDER')
data_meshes[ob] = (get_blenderID_key(tmp_me), tmp_me, True)
data_meshes[ob_obj] = (get_blenderID_key(tmp_me), tmp_me, True)
# Re-enable temporary disabled modifiers.
for mod, show_render in tmp_mods:
mod.show_render = show_render
if use_org_data:
data_meshes[ob] = (get_blenderID_key(ob.data), ob.data, False)
data_meshes[ob_obj] = (get_blenderID_key(ob.data), ob.data, False)
# ShapeKeys.
data_deformers_shape = OrderedDict()
for me_key, me, _org in data_meshes.values():
if not (me.shape_keys and me.shape_keys.key_blocks):
continue
shapes_key = get_blender_mesh_shape_key(me)
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 = []
for idx, (sv, v) in enumerate(zip(shape.data, me.vertices)):
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(sv.co - v.co);
shape_verts_idx.append(idx);
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
# Armatures!
data_deformers_skin = OrderedDict()
data_bones = OrderedDict()
data_deformers = OrderedDict()
arm_parents = set()
for ob_obj in tuple(objects):
if not (ob_obj.is_object and ob_obj.type in {'ARMATURE'}):
continue
fbx_skeleton_from_armature(scene, settings, ob_obj, objects, data_meshes,
data_bones, data_deformers, arm_parents)
data_bones, data_deformers_skin, arm_parents)
# Some world settings are embedded in FBX materials...
if scene.world:
@ -2040,7 +2144,7 @@ def fbx_data_from_scene(scene, settings):
None, None, None,
settings, scene, objects, None, 0.0, 0.0,
data_empties, data_lamps, data_cameras, data_meshes, None,
data_bones, data_deformers,
data_bones, data_deformers_skin, data_deformers_shape,
data_world, data_materials, data_textures, data_videos,
)
animations, frame_start, frame_end = fbx_animations_objects(tmp_scdata)
@ -2063,7 +2167,10 @@ def fbx_data_from_scene(scene, settings):
templates[b"Bone"] = fbx_template_def_bone(scene, settings, nbr_users=len(data_bones))
if data_meshes:
templates[b"Geometry"] = fbx_template_def_geometry(scene, settings, nbr_users=len(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))
@ -2072,9 +2179,15 @@ def fbx_data_from_scene(scene, settings):
# 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:
nbr = len(data_deformers)
nbr += sum(len(clusters) for def_me in data_deformers.values() for a, b, clusters in def_me.values())
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...
@ -2151,14 +2264,24 @@ def fbx_data_from_scene(scene, settings):
empty_key = data_empties[ob_obj]
connections.append((b"OO", get_fbx_uuid_from_key(empty_key), ob_obj.fbx_uuid, None))
elif ob_obj.type in BLENDER_OBJECT_TYPES_MESHLIKE:
mesh_key, _me, _free = data_meshes[ob_obj.bdata]
mesh_key, _me, _free = data_meshes[ob_obj]
connections.append((b"OO", get_fbx_uuid_from_key(mesh_key), ob_obj.fbx_uuid, None))
# Deformers (armature-to-geometry, only for meshes currently)...
for arm, deformed_meshes in data_deformers.items():
# '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():
for me, (skin_key, ob_obj, clusters) in deformed_meshes.items():
# skin -> geometry
mesh_key, _me, _free = data_meshes[ob_obj.bdata]
mesh_key, _me, _free = data_meshes[ob_obj]
assert(me == _me)
connections.append((b"OO", get_fbx_uuid_from_key(skin_key), get_fbx_uuid_from_key(mesh_key), None))
for bo_obj, clstr_key in clusters.items():
@ -2180,7 +2303,7 @@ def fbx_data_from_scene(scene, settings):
# 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.bdata]
_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
@ -2228,7 +2351,7 @@ def fbx_data_from_scene(scene, settings):
templates, templates_users, connections,
settings, scene, objects, animations, frame_start, frame_end,
data_empties, data_lamps, data_cameras, data_meshes, mesh_mat_indices,
data_bones, data_deformers,
data_bones, data_deformers_skin, data_deformers_shape,
data_world, data_materials, data_textures, data_videos,
)

View File

@ -54,6 +54,9 @@ FBX_GEOMETRY_VCOLOR_VERSION = 101
FBX_GEOMETRY_UV_VERSION = 101
FBX_GEOMETRY_MATERIAL_VERSION = 101
FBX_GEOMETRY_LAYER_VERSION = 100
FBX_GEOMETRY_SHAPE_VERSION = 100
FBX_DEFORMER_SHAPE_VERSION = 100
FBX_DEFORMER_SHAPECHANNEL_VERSION = 100
FBX_POSE_BIND_VERSION = 100
FBX_DEFORMER_SKIN_VERSION = 101
FBX_DEFORMER_CLUSTER_VERSION = 100
@ -285,14 +288,25 @@ def get_blender_empty_key(obj):
return "|".join((get_blenderID_key(obj), "Empty"))
def get_blender_mesh_shape_key(me):
"""Return main shape deformer's key."""
return "|".join((get_blenderID_key(me), "Shape"))
def get_blender_mesh_shape_channel_key(me, shape):
"""Return shape channel and geometry shape keys."""
return ("|".join((get_blenderID_key(me), "Shape", get_blenderID_key(shape))),
"|".join((get_blenderID_key(me), "Geometry", get_blenderID_key(shape))))
def get_blender_bone_key(armature, bone):
"""Return bone's keys (Model and NodeAttribute)."""
return "|".join((get_blenderID_key((armature, bone)), "Data"))
def get_blender_armature_bindpose_key(armature, mesh):
"""Return armature's bindpose key."""
return "|".join((get_blenderID_key(armature), get_blenderID_key(mesh), "BindPose"))
def get_blender_bindpose_key(obj, mesh):
"""Return object's bindpose key."""
return "|".join((get_blenderID_key(obj), get_blenderID_key(mesh), "BindPose"))
def get_blender_armature_skin_key(armature, mesh):
@ -934,6 +948,6 @@ FBXData = namedtuple("FBXData", (
"templates", "templates_users", "connections",
"settings", "scene", "objects", "animations", "frame_start", "frame_end",
"data_empties", "data_lamps", "data_cameras", "data_meshes", "mesh_mat_indices",
"data_bones", "data_deformers",
"data_bones", "data_deformers_skin", "data_deformers_shape",
"data_world", "data_materials", "data_textures", "data_videos",
))