Merged experimental code in 'stable' addon (mainly Jens Restemeier's summer work).

Will remove experimental one soon. Keeping old 6.1 exporter active for now, though.

Notes:
* Not 100% happy with how complex code is now... Rather sure though refactor would
  take a huge amount of time, not worth it for now.
* New code handles things like armature & animation import much much better than previous one,
  so definitively worth it.
* New code also fixes some real bugs (like Blender-side of T42135).
* Did a few cosmetic changes on the run, nothing serious.

This should make FBX work complete, aside from a few nice TODOs that I'll tackle in comming weeks,
in addition to ongoing bugbusting.

Many thanks to Jens for his work on armature mess! :D
This commit is contained in:
Bastien Montagne 2014-10-08 15:33:56 +02:00
parent 355f86ca08
commit 4a07e805ad
4 changed files with 1104 additions and 529 deletions

View File

@ -19,12 +19,12 @@
# <pep8 compliant>
bl_info = {
"name": "Autodesk FBX format",
"author": "Campbell Barton, Bastien Montagne",
"version": (3, 1, 0),
"blender": (2, 70, 0),
"name": "FBX format",
"author": "Campbell Barton, Bastien Montagne, Jens Restemeier",
"version": (3, 2, 0),
"blender": (2, 72, 0),
"location": "File > Import-Export",
"description": "Export FBX meshes, UV's, vertex colors, materials, "
"description": "FBX IO meshes, UV's, vertex colors, materials, "
"textures, cameras, lamps and actions",
"warning": "",
"wiki_url": "http://wiki.blender.org/index.php/Extensions:2.6/Py/"
@ -59,7 +59,7 @@ from bpy_extras.io_utils import (ImportHelper,
class ImportFBX(bpy.types.Operator, ImportHelper):
"""Load a FBX geometry file"""
"""Load a FBX file"""
bl_idname = "import_scene.fbx"
bl_label = "Import FBX"
bl_options = {'UNDO', 'PRESET'}
@ -101,6 +101,13 @@ class ImportFBX(bpy.types.Operator, ImportHelper):
min=0.001, max=1000.0,
default=1.0,
)
bake_space_transform = BoolProperty(
name="Apply Transform",
description=("Bake space transform into object data, avoids getting unwanted rotations to objects when "
"target space is not aligned with Blender's space "
"(WARNING! experimental option, might give odd/wrong results)"),
default=False,
)
use_image_search = BoolProperty(
name="Image Search",
@ -135,6 +142,41 @@ class ImportFBX(bpy.types.Operator, ImportHelper):
options={'HIDDEN'},
)
ignore_leaf_bones = BoolProperty(
name="Ignore leaf bones",
description="Ignore the last bone at the end of a chain that is used to mark the length of the previous bone",
default=False,
options={'HIDDEN'},
)
automatic_bone_orientation = BoolProperty(
name="Automatic Bone Orientation",
description="Try to align the major bone axis with the bone children",
default=False,
options={'HIDDEN'},
)
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',
)
def draw(self, context):
layout = self.layout
@ -143,7 +185,8 @@ class ImportFBX(bpy.types.Operator, ImportHelper):
sub.enabled = self.use_manual_orientation
sub.prop(self, "axis_forward")
sub.prop(self, "axis_up")
sub.prop(self, "global_scale")
layout.prop(self, "global_scale")
layout.prop(self, "bake_space_transform")
layout.prop(self, "use_image_search")
# layout.prop(self, "use_alpha_decals")
@ -154,6 +197,14 @@ class ImportFBX(bpy.types.Operator, ImportHelper):
sub.enabled = self.use_custom_props
sub.prop(self, "use_custom_props_enum_as_string")
layout.prop(self, "ignore_leaf_bones")
layout.prop(self, "automatic_bone_orientation"),
sub = layout.column()
sub.enabled = not self.automatic_bone_orientation
sub.prop(self, "primary_bone_axis")
sub.prop(self, "secondary_bone_axis")
def execute(self, context):
keywords = self.as_keywords(ignore=("filter_glob", "directory"))
keywords["use_cycles"] = (context.scene.render.engine == 'CYCLES')
@ -163,7 +214,7 @@ class ImportFBX(bpy.types.Operator, ImportHelper):
class ExportFBX(bpy.types.Operator, ExportHelper):
"""Selection to an ASCII Autodesk FBX"""
"""Write a FBX file"""
bl_idname = "export_scene.fbx"
bl_label = "Export FBX"
bl_options = {'UNDO', 'PRESET'}
@ -273,6 +324,35 @@ class ExportFBX(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)",
@ -387,6 +467,9 @@ class ExportFBX(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
@ -442,11 +525,11 @@ class ExportFBX(bpy.types.Operator, ExportHelper):
def menu_func_import(self, context):
self.layout.operator(ImportFBX.bl_idname, text="Autodesk FBX (.fbx)")
self.layout.operator(ImportFBX.bl_idname, text="FBX (.fbx)")
def menu_func_export(self, context):
self.layout.operator(ExportFBX.bl_idname, text="Autodesk FBX (.fbx)")
self.layout.operator(ExportFBX.bl_idname, text="FBX (.fbx)")
def register():

View File

@ -1371,6 +1371,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
@ -1382,13 +1384,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 - XXX Not much useful actually :/
# (LimbLength can't be used because it is a scale factor 0-1 for the parent-child distance:
# http://docs.autodesk.com/FBX/2014/ENU/FBX-SDK-Documentation/cpp_ref/class_fbx_skeleton.html#a9bbe2a70f4ed82cd162620259e649f0f )
# elem_props_set(props, "p_double", "BlenderBoneLength".encode(), (bo.tail_local - bo.head_local).length, custom=True)
# Skin deformers and BindPoses.
# Note: we might also use Deformers for our "parent to vertex" stuff???
deformer = scene_data.data_deformers_skin.get(arm_obj, None)
@ -1450,6 +1457,56 @@ def fbx_data_armature_elements(root, arm_obj, scene_data):
elem_data_single_float64_array(fbx_clstr, b"TransformAssociateModel", matrix4_to_array(mat_world_arm))
def fbx_data_leaf_bone_elements(root, scene_data):
# Write a dummy leaf bone that is used by applications to show the length of the last bone in a chain
for (node_name, _par_uuid, node_uuid, attr_uuid, matrix, hide, size) in scene_data.data_leaf_bones:
# Bone 'data'...
fbx_bo = elem_data_single_int64(root, b"NodeAttribute", attr_uuid)
fbx_bo.add_string(fbx_name_class(node_name.encode(), b"NodeAttribute"))
fbx_bo.add_string(b"LimbNode")
elem_data_single_string(fbx_bo, b"TypeFlags", b"Skeleton")
tmpl = elem_props_template_init(scene_data.templates, b"Bone")
props = elem_properties(fbx_bo)
elem_props_template_set(tmpl, props, "p_double", b"Size", size)
elem_props_template_finalize(tmpl, props)
# And bone object.
model = elem_data_single_int64(root, b"Model", node_uuid)
model.add_string(fbx_name_class(node_name.encode(), b"Model"))
model.add_string(b"LimbNode")
elem_data_single_int32(model, b"Version", FBX_MODELS_VERSION)
# Object transform info.
loc, rot, scale = matrix.decompose()
rot = rot.to_euler('XYZ')
rot = tuple(convert_rad_to_deg_iter(rot))
tmpl = elem_props_template_init(scene_data.templates, b"Model")
# For now add only loc/rot/scale...
props = elem_properties(model)
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)
def fbx_data_object_elements(root, ob_obj, scene_data):
"""
Write the Object (Model) data blocks.
@ -1709,6 +1766,37 @@ 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 = {bo: 0 for bo, _bo_key in data_bones.items()}
for bo, _bo_key in data_bones.items():
if bo.parent and bo.parent.is_bone:
child_count[bo.parent] += 1
bone_radius_scale = settings.global_scale * 33.0
# generate bone data
leaf_parents = [bo for bo, count in child_count.items() if count == 0]
leaf_bones = []
for parent in leaf_parents:
node_name = parent.name + "_end"
parent_uuid = parent.fbx_uuid
node_uuid = get_fbx_uuid_from_key(node_name + "_node")
attr_uuid = get_fbx_uuid_from_key(node_name + "_nodeattr")
hide = parent.hide
size = parent.bdata.head_radius * bone_radius_scale
bone_length = (parent.bdata.tail_local - parent.bdata.head_local).length
matrix = Matrix.Translation((0, bone_length, 0))
if settings.bone_correction_matrix_inv:
matrix = settings.bone_correction_matrix_inv * matrix
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_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.
@ -2050,6 +2138,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)),))
@ -2125,7 +2218,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_skin, data_deformers_shape,
data_bones, data_leaf_bones, data_deformers_skin, data_deformers_shape,
data_world, data_materials, data_textures, data_videos,
)
animations, frame_start, frame_end = fbx_animations(tmp_scdata)
@ -2248,6 +2341,11 @@ 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
for (_node_name, par_uuid, node_uuid, attr_uuid, _matrix, _hide, _size) in data_leaf_bones:
connections.append((b"OO", node_uuid, parent_uuid, None))
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
@ -2332,7 +2430,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_skin, data_deformers_shape,
data_bones, data_leaf_bones, data_deformers_skin, data_deformers_shape,
data_world, data_materials, data_textures, data_videos,
)
@ -2546,6 +2644,9 @@ def fbx_objects_elements(root, scene_data):
continue
fbx_data_armature_elements(objects, ob_obj, scene_data)
if scene_data.data_leaf_bones:
fbx_data_leaf_bone_elements(objects, scene_data)
for mat in scene_data.data_materials:
fbx_data_material_elements(objects, mat, scene_data)
@ -2608,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,
@ -2627,6 +2731,11 @@ def save_single(operator, scene, filepath="",
if 'OTHER' in object_types:
object_types |= BLENDER_OTHER_OBJECT_TYPES
# Scale/unit mess. FBX can store the 'reference' unit of a file in its UnitScaleFactor property
# (1.0 meaning centimeter, afaik). We use that to reflect user's default unit as set in Blender with scale_length.
# However, we always get values in BU (i.e. meters), so we have to reverse-apply that scale in global matrix...
if scene.unit_settings.system != 'NONE':
global_matrix = global_matrix * Matrix.Scale(1.0 / scene.unit_settings.scale_length, 4)
global_scale = global_matrix.median_scale
global_matrix_inv = global_matrix.inverted()
# For transforming mesh normals.
@ -2636,6 +2745,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
@ -2650,7 +2772,8 @@ def save_single(operator, scene, filepath="",
operator.report, (axis_up, axis_forward), global_matrix, global_scale,
bake_space_transform, global_matrix_inv, global_matrix_inv_transposed,
context_objects, object_types, use_mesh_modifiers,
mesh_smooth_type, use_mesh_edges, use_tspace, use_armature_deform_only,
mesh_smooth_type, use_mesh_edges, use_tspace,
use_armature_deform_only, add_leaf_bones, bone_correction_matrix, bone_correction_matrix_inv,
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,
)
@ -2730,6 +2853,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

@ -972,9 +972,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.is_bone:
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':
@ -1100,7 +1103,8 @@ FBXExportSettings = namedtuple("FBXExportSettings", (
"report", "to_axes", "global_matrix", "global_scale",
"bake_space_transform", "global_matrix_inv", "global_matrix_inv_transposed",
"context_objects", "object_types", "use_mesh_modifiers",
"mesh_smooth_type", "use_mesh_edges", "use_tspace", "use_armature_deform_only",
"mesh_smooth_type", "use_mesh_edges", "use_tspace",
"use_armature_deform_only", "add_leaf_bones", "bone_correction_matrix", "bone_correction_matrix_inv",
"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",
))
@ -1116,15 +1120,17 @@ FBXExportData = namedtuple("FBXExportData", (
"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_skin", "data_deformers_shape",
"data_bones", "data_leaf_bones", "data_deformers_skin", "data_deformers_shape",
"data_world", "data_materials", "data_textures", "data_videos",
))
# Helper container gathering all importer settings.
FBXImportSettings = namedtuple("FBXImportSettings", (
"report", "to_axes", "global_matrix", "global_scale",
"bake_space_transform", "global_matrix_inv", "global_matrix_inv_transposed",
"use_cycles", "use_image_search",
"use_alpha_decals", "decal_offset",
"use_custom_props", "use_custom_props_enum_as_string",
"object_tdata_cache", "cycles_material_wrap_map", "image_cache",
"cycles_material_wrap_map", "image_cache",
"ignore_leaf_bones", "automatic_bone_orientation", "bone_correction_matrix"
))

File diff suppressed because it is too large Load Diff