FBX Exporter: (Optionally) add leaf bones and change bone orientation

Revision: D735

Patch by jrestemeier (Jens Restemeier).
This commit is contained in:
Bastien Montagne 2014-09-08 14:47:30 +02:00
parent a6916776e7
commit 866397a8ff
3 changed files with 166 additions and 4 deletions

View File

@ -317,6 +317,35 @@ class ExportFBX_experimental(bpy.types.Operator, ExportHelper):
description="Export custom properties",
default=False,
)
add_leaf_bones = BoolProperty(
name="Add leaf bones",
description=("Append a last bone to the end of each chain to specify bone length. It is useful to, "
"enable this when exporting into another modelling application and to disable this when"
"exporting into a game engine or real-time viewer."),
default=True # False for commit!
)
primary_bone_axis = EnumProperty(
name="Primary Bone Axis",
items=(('X', "X Axis", ""),
('Y', "Y Axis", ""),
('Z', "Z Axis", ""),
('-X', "-X Axis", ""),
('-Y', "-Y Axis", ""),
('-Z', "-Z Axis", ""),
),
default='Y',
)
secondary_bone_axis = EnumProperty(
name="Secondary Bone Axis",
items=(('X', "X Axis", ""),
('Y', "Y Axis", ""),
('Z', "Z Axis", ""),
('-X', "-X Axis", ""),
('-Y', "-Y Axis", ""),
('-Z', "-Z Axis", ""),
),
default='X',
)
use_armature_deform_only = BoolProperty(
name="Only Deform Bones",
description="Only write deforming bones (and non-deforming ones when they have deforming children)",
@ -431,6 +460,9 @@ class ExportFBX_experimental(bpy.types.Operator, ExportHelper):
layout.prop(self, "use_armature_deform_only")
if is_74bin:
layout.prop(self, "use_custom_props")
layout.prop(self, "add_leaf_bones")
layout.prop(self, "primary_bone_axis")
layout.prop(self, "secondary_bone_axis")
layout.prop(self, "bake_anim")
col = layout.column()
col.enabled = self.bake_anim

View File

@ -1379,6 +1379,8 @@ def fbx_data_armature_elements(root, arm_obj, scene_data):
mat_world_arm = arm_obj.fbx_object_matrix(scene_data, global_space=True)
bones = tuple(bo_obj for bo_obj in arm_obj.bones if bo_obj in scene_data.objects)
bone_radius_scale = scene_data.settings.global_scale * 33.0
# Bones "data".
for bo_obj in bones:
bo = bo_obj.bdata
@ -1390,13 +1392,18 @@ def fbx_data_armature_elements(root, arm_obj, scene_data):
tmpl = elem_props_template_init(scene_data.templates, b"Bone")
props = elem_properties(fbx_bo)
elem_props_template_set(tmpl, props, "p_double", b"Size", (bo.tail_local - bo.head_local).length)
elem_props_template_set(tmpl, props, "p_double", b"Size", bo.head_radius * bone_radius_scale)
elem_props_template_finalize(tmpl, props)
# Custom properties.
if scene_data.settings.use_custom_props:
fbx_data_element_custom_properties(props, bo)
# Store Blender bone length
# (LimbLength can't be used because it is a scale factor 0-1 for the parent-child distance:
# http://docs.autodesk.com/FBX/2014/ENU/FBX-SDK-Documentation/cpp_ref/class_fbx_skeleton.html#a9bbe2a70f4ed82cd162620259e649f0f )
elem_props_set(props, "p_double", "BlenderBoneLength".encode(), (bo.tail_local - bo.head_local).length, custom=True)
# Skin deformers and BindPoses.
# Note: we might also use Deformers for our "parent to vertex" stuff???
deformer = scene_data.data_deformers_skin.get(arm_obj, None)
@ -1717,6 +1724,90 @@ def fbx_skeleton_from_armature(scene, settings, arm_obj, objects, data_meshes,
objects.update(bones)
def fbx_generate_leaf_bones(settings, data_bones):
# find which bons have no children
child_count = {bone: 0 for bone,_ in data_bones.items()}
for bone,_ in data_bones.items():
if bone.parent and bone.parent._tag == 'BO':
child_count[bone.parent] += 1
bone_radius_scale = settings.global_scale * 33.0
# generate bone data
leaf_parents = [bone for bone,count in child_count.items() if count == 0]
leaf_bones = []
for parent in leaf_parents:
node_name = parent.name + "_end"
parent_uuid = parent.fbx_uuid
node_uuid = get_fbx_uuid_from_key(node_name + "_node")
attr_uuid = get_fbx_uuid_from_key(node_name + "_nodeattr")
hide = parent.bdata.hide
size = parent.bdata.head_radius * bone_radius_scale
bone_length = (parent.bdata.tail_local - parent.bdata.head_local).length
matrix = Matrix.Translation((0, bone_length, 0))
if settings.bone_correction_matrix_inv:
matrix = settings.bone_correction_matrix_inv * matrix
if settings.bone_correction_matrix:
matrix = matrix * settings.bone_correction_matrix
leaf_bones.append((node_name, parent_uuid, node_uuid, attr_uuid, matrix, hide, size))
return leaf_bones
def fbx_write_leaf_bone_data(root, scene_data):
# Write a dummy leaf bone that is used by applications to show the length of the last bone in a chain
for (node_name, _, node_uuid, _, matrix, hide, _) in scene_data.data_leaf_bones:
model = elem_data_single_int64(root, b"Model", node_uuid)
model.add_string(fbx_name_class(node_name.encode(), b"Model"))
model.add_string(b"LimbNode")
elem_data_single_int32(model, b"Version", FBX_MODELS_VERSION)
# Object transform info.
loc, rot, scale = matrix.decompose()
rot = rot.to_euler('XYZ')
rot = tuple(convert_rad_to_deg_iter(rot))
tmpl = elem_props_template_init(scene_data.templates, b"Model")
# For now add only loc/rot/scale...
props = elem_properties(model)
elem_props_template_set(tmpl, props, "p_lcl_translation", b"Lcl Translation", loc)
elem_props_template_set(tmpl, props, "p_lcl_rotation", b"Lcl Rotation", rot)
elem_props_template_set(tmpl, props, "p_lcl_scaling", b"Lcl Scaling", scale)
elem_props_template_set(tmpl, props, "p_visibility", b"Visibility", float(not hide))
# Absolutely no idea what this is, but seems mandatory for validity of the file, and defaults to
# invalid -1 value...
elem_props_template_set(tmpl, props, "p_integer", b"DefaultAttributeIndex", 0)
elem_props_template_set(tmpl, props, "p_enum", b"InheritType", 1) # RSrs
# Those settings would obviously need to be edited in a complete version of the exporter, may depends on
# object type, etc.
elem_data_single_int32(model, b"MultiLayer", 0)
elem_data_single_int32(model, b"MultiTake", 0)
elem_data_single_bool(model, b"Shading", True)
elem_data_single_string(model, b"Culling", b"CullingOff")
elem_props_template_finalize(tmpl, props)
for (node_name, _, _, attr_uuid, _, _, size) in scene_data.data_leaf_bones:
fbx_bo = elem_data_single_int64(root, b"NodeAttribute", attr_uuid)
fbx_bo.add_string(fbx_name_class(node_name.encode(), b"NodeAttribute"))
fbx_bo.add_string(b"LimbNode")
elem_data_single_string(fbx_bo, b"TypeFlags", b"Skeleton")
tmpl = elem_props_template_init(scene_data.templates, b"Bone")
props = elem_properties(fbx_bo)
elem_props_template_set(tmpl, props, "p_double", b"Size", size)
elem_props_template_finalize(tmpl, props)
def fbx_write_leaf_bone_connections(connections, leaf_bones):
# attach nodes to parents and attributes to nodes
for (_, parent_uuid, node_uuid, attr_uuid, _, _, _) in leaf_bones:
connections.append((b"OO", node_uuid, parent_uuid, None))
connections.append((b"OO", attr_uuid, node_uuid, None))
def fbx_animations_do(scene_data, ref_id, f_start, f_end, start_zero, objects=None, force_keep=False):
"""
Generate animation data (a single AnimStack) from objects, for a given frame range.
@ -2049,6 +2140,11 @@ def fbx_data_from_scene(scene, settings):
fbx_skeleton_from_armature(scene, settings, ob_obj, objects, data_meshes,
data_bones, data_deformers_skin, arm_parents)
# Generate leaf bones
data_leaf_bones = None
if settings.add_leaf_bones:
data_leaf_bones = fbx_generate_leaf_bones(settings, data_bones)
# Some world settings are embedded in FBX materials...
if scene.world:
data_world = OrderedDict(((scene.world, get_blenderID_key(scene.world)),))
@ -2126,6 +2222,7 @@ def fbx_data_from_scene(scene, settings):
data_empties, data_lamps, data_cameras, data_meshes, None,
data_bones, data_deformers_skin, data_deformers_shape,
data_world, data_materials, data_textures, data_videos,
data_leaf_bones
)
animations, frame_start, frame_end = fbx_animations(tmp_scdata)
@ -2247,6 +2344,10 @@ def fbx_data_from_scene(scene, settings):
mesh_key, _me, _free = data_meshes[ob_obj]
connections.append((b"OO", get_fbx_uuid_from_key(mesh_key), ob_obj.fbx_uuid, None))
# Leaf Bones
if data_leaf_bones:
fbx_write_leaf_bone_connections(connections, data_leaf_bones)
# 'Shape' deformers (shape keys, only for meshes currently)...
for me_key, shapes_key, shapes in data_deformers_shape.values():
# shape -> geometry
@ -2333,6 +2434,7 @@ def fbx_data_from_scene(scene, settings):
data_empties, data_lamps, data_cameras, data_meshes, mesh_mat_indices,
data_bones, data_deformers_skin, data_deformers_shape,
data_world, data_materials, data_textures, data_videos,
data_leaf_bones
)
@ -2521,6 +2623,9 @@ def fbx_objects_elements(root, scene_data):
for cam in scene_data.data_cameras:
fbx_data_camera_elements(objects, cam, scene_data)
if scene_data.data_leaf_bones:
fbx_write_leaf_bone_data(objects, scene_data)
done_meshes = set()
for me_obj in scene_data.data_meshes:
fbx_data_mesh_elements(objects, me_obj, scene_data, done_meshes)
@ -2604,6 +2709,9 @@ def save_single(operator, scene, filepath="",
bake_anim_use_all_actions=True,
bake_anim_step=1.0,
bake_anim_simplify_factor=1.0,
add_leaf_bones=False,
primary_bone_axis='Y',
secondary_bone_axis='X',
use_metadata=True,
path_mode='AUTO',
use_mesh_edges=True,
@ -2632,6 +2740,19 @@ def save_single(operator, scene, filepath="",
if embed_textures and path_mode != 'COPY':
embed_textures = False
# 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()
media_settings = FBXExportSettingsMedia(
path_mode,
os.path.dirname(bpy.data.filepath), # base_src
@ -2649,6 +2770,7 @@ def save_single(operator, scene, filepath="",
mesh_smooth_type, use_mesh_edges, use_tspace, use_armature_deform_only,
bake_anim, bake_anim_use_nla_strips, bake_anim_use_all_actions, bake_anim_step, bake_anim_simplify_factor,
False, media_settings, use_custom_props,
add_leaf_bones, bone_correction_matrix, bone_correction_matrix_inv,
)
import bpy_extras.io_utils
@ -2726,6 +2848,9 @@ def defaults_unity3d():
"bake_anim_step": 1.0,
"bake_anim_use_nla_strips": True,
"bake_anim_use_all_actions": True,
"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,

View File

@ -955,9 +955,12 @@ class ObjectWrapper(metaclass=MetaObjectWrapper):
# Bones, lamps and cameras need to be rotated (in local space!).
if self._tag == 'BO':
# XXX This should work smoothly, but actually is only OK for 'rest' pose, actual pose/animations
# give insane results... :(
matrix = matrix * MAT_CONVERT_BONE
# if we have a bone parent we need to undo the parent correction
if not is_global and scene_data.settings.bone_correction_matrix_inv and parent and parent._tag == 'BO':
matrix = scene_data.settings.bone_correction_matrix_inv * matrix
# apply the bone correction
if scene_data.settings.bone_correction_matrix:
matrix = matrix * scene_data.settings.bone_correction_matrix
elif self.bdata.type == 'LAMP':
matrix = matrix * MAT_CONVERT_LAMP
elif self.bdata.type == 'CAMERA':
@ -1077,6 +1080,7 @@ FBXExportSettings = namedtuple("FBXExportSettings", (
"mesh_smooth_type", "use_mesh_edges", "use_tspace", "use_armature_deform_only",
"bake_anim", "bake_anim_use_nla_strips", "bake_anim_use_all_actions", "bake_anim_step", "bake_anim_simplify_factor",
"use_metadata", "media_settings", "use_custom_props",
"add_leaf_bones", "bone_correction_matrix", "bone_correction_matrix_inv",
))
# Helper container gathering some data we need multiple times:
@ -1092,6 +1096,7 @@ FBXExportData = namedtuple("FBXExportData", (
"data_empties", "data_lamps", "data_cameras", "data_meshes", "mesh_mat_indices",
"data_bones", "data_deformers_skin", "data_deformers_shape",
"data_world", "data_materials", "data_textures", "data_videos",
"data_leaf_bones",
))
# Helper container gathering all importer settings.