FBX import: Add static armature support (no animation yet).

Please note following limitations:
* FBX 'Bones' orientation status is currently unclear - they *seem* to be -X oriented,
  wich means they would need to be corrected (in export as well). Could not get this
  workling yet, though, and it does not seems to bother much apps like Unity?
  This is still being investigated.
* We only support 'Deformer' based skinning, not 'BindPose' one. Why FBX keeps two different
  systems here, sometimes mixing them happily? And why BindPose has no weighting system?
This commit is contained in:
Bastien Montagne 2014-07-13 18:29:28 +02:00
parent dbfb5209c0
commit 4555427506
1 changed files with 332 additions and 5 deletions

View File

@ -313,6 +313,20 @@ def blen_read_object_transform_do(transform_data):
)
# XXX This might be weak, now that we can add vgroups from both bones and shapes, name collisions become
# more likely, will have to make this more robust!!!
def add_vgroup_to_objects(vg_indices, vg_weights, vg_name, objects):
assert(len(vg_indices) == len(vg_weights))
if vg_indices:
for obj in objects:
# We replace/override here...
vg = obj.vertex_groups.get(vg_name)
if vg is None:
vg = obj.vertex_groups.new(vg_name)
for i, w in zip(vg_indices, vg_weights):
vg.add((i,), w, 'REPLACE')
def blen_read_object_transform_preprocess(fbx_props, fbx_obj, rot_alt_mat):
# This is quite involved, 'fbxRNode.cpp' from openscenegraph used as a reference
const_vector_zero_3d = 0.0, 0.0, 0.0
@ -390,6 +404,180 @@ def blen_read_object(fbx_tmpl, fbx_obj, object_data):
return obj
# --------
# Armature
def blen_read_armatures_add_bone(bl_obj, bl_arm, bones, b_uuid, matrices, fbx_tmpl_model):
from mathutils import Matrix, Vector
b_item, bsize, p_uuid, clusters = bones[b_uuid]
fbx_bdata, bl_bname = b_item
if bl_bname is not None:
return bl_arm.edit_bones[bl_bname] # Have already been created...
p_ebo = None
if p_uuid is not None:
# Recurse over parents!
p_ebo = blen_read_armatures_add_bone(bl_obj, bl_arm, bones, p_uuid, matrices, fbx_tmpl_model)
if clusters:
# Note in some cases, one bone can have several clusters (kind of LoD?), in Blender we'll always
# use only the first, for now.
fbx_cdata, meshes, objects = clusters[0]
objects = {blen_o for fbx_o, blen_o in objects}
# We assume matrices in cluster are rest pose of bones (they are in Global space!).
# TransformLink is matrix of bone, in global space.
# TransformAssociateModel is matrix of armature, in global space (at bind time).
elm = elem_find_first(fbx_cdata, b'Transform', default=None)
mmat_bone = array_to_matrix4(elm.props[0]) if elm is not None else None
elm = elem_find_first(fbx_cdata, b'TransformLink', default=None)
bmat_glob = array_to_matrix4(elm.props[0]) if elm is not None else Matrix()
elm = elem_find_first(fbx_cdata, b'TransformAssociateModel', default=None)
amat_glob = array_to_matrix4(elm.props[0]) if elm is not None else Matrix()
mmat_glob = bmat_glob * mmat_bone
# We seek for matrix of bone in armature space...
bmat_arm = amat_glob.inverted() * bmat_glob
# Bone correction, works here...
bmat_loc = (p_ebo.matrix.inverted() * bmat_arm) if p_ebo else bmat_arm
bmat_loc = bmat_loc * MAT_CONVERT_BONE
bmat_arm = (p_ebo.matrix * bmat_loc) if p_ebo else bmat_loc
else:
# Armature bound to no mesh...
fbx_cdata, meshes, objects = (None, (), ())
mmat_bone = None
amat_glob = bl_obj.matrix_world
fbx_props = (elem_find_first(fbx_bdata, b'Properties70'),
elem_find_first(fbx_tmpl_model, b'Properties70', fbx_elem_nil))
assert(fbx_props[0] is not None)
# Bone correction, works here...
transform_data = blen_read_object_transform_preprocess(fbx_props, fbx_bdata, MAT_CONVERT_BONE)
bmat_loc = blen_read_object_transform_do(transform_data)
# Bring back matrix in armature space.
bmat_arm = (p_ebo.matrix * bmat_loc) if p_ebo else bmat_loc
# ----
# Now, create the (edit)bone.
bone_name = elem_name_ensure_class(fbx_bdata, b'Model')
ebo = bl_arm.edit_bones.new(name=bone_name)
bone_name = ebo.name # Might differ from FBX bone name!
b_item[1] = bone_name # since ebo is only valid in Edit mode... :/
# So that our bone gets its final length, but still Y-aligned in armature space.
ebo.tail = Vector((0.0, 1.0, 0.0)) * bsize
# And rotate/move it to its final "rest pose".
ebo.matrix = bmat_arm.normalized()
# Connection to parent.
if p_ebo is not None:
ebo.parent = p_ebo
if similar_values_iter(p_ebo.tail, ebo.head):
ebo.use_connect = True
if fbx_cdata is not None:
# ----
# Add a new vgroup to the meshes (their objects, actually!).
# Quite obviously, only one mesh is expected...
indices = elem_prop_first(elem_find_first(fbx_cdata, b'Indexes', default=None), default=())
weights = elem_prop_first(elem_find_first(fbx_cdata, b'Weights', default=None), default=())
add_vgroup_to_objects(indices, weights, bone_name, objects)
# ----
# If we get a valid mesh matrix (in bone space), store armature and mesh global matrices, we need to set temporarily
# both objects to those matrices when actually binding them via the modifier.
# Note we assume all bones were bound with the same mesh/armature (global) matrix, we do not support otherwise
# in Blender anyway!
if mmat_bone is not None:
for obj in objects:
if obj in matrices:
continue
matrices[obj] = (amat_glob, mmat_glob)
return ebo
def blen_read_armatures(fbx_tmpl, armatures, fbx_bones_to_fake_object, scene, global_matrix):
from mathutils import Matrix
if global_matrix is None:
global_matrix = Matrix()
for a_item, bones in armatures:
fbx_adata, bl_adata = a_item
matrices = {}
# ----
# Armature data.
elem_name_utf8 = elem_name_ensure_class(fbx_adata, b'Model')
bl_arm = bpy.data.armatures.new(name=elem_name_utf8)
# Need to create the object right now, since we can only add bones in Edit mode... :/
assert(a_item[1] is None)
if fbx_adata.props[2] in {b'LimbNode', b'Root'}:
# rootbone-as-armature case...
fbx_bones_to_fake_object[fbx_adata.props[0]] = bl_adata = blen_read_object(fbx_tmpl, fbx_adata, bl_arm)
# reset transform.
bl_adata.matrix_basis = Matrix()
else:
bl_adata = a_item[1] = blen_read_object(fbx_tmpl, fbx_adata, bl_arm)
# Instantiate in scene.
obj_base = scene.objects.link(bl_adata)
obj_base.select = True
# Switch to Edit mode.
scene.objects.active = bl_adata
bpy.ops.object.mode_set(mode='EDIT')
for b_uuid in bones:
blen_read_armatures_add_bone(bl_adata, bl_arm, bones, b_uuid, matrices, fbx_tmpl)
bpy.ops.object.mode_set(mode='OBJECT')
# Bind armature to objects.
arm_mat_back = bl_adata.matrix_basis.copy()
for ob_me, (amat, mmat) in matrices.items():
# bring global armature & mesh matrices into *Blender* global space.
amat = global_matrix * amat
mmat = global_matrix * mmat
bl_adata.matrix_basis = amat
me_mat_back = ob_me.matrix_basis.copy()
ob_me.matrix_basis = mmat
mod = ob_me.modifiers.new(elem_name_utf8, 'ARMATURE')
mod.object = bl_adata
ob_me.parent = bl_adata
ob_me.matrix_basis = me_mat_back
bl_adata.matrix_basis = arm_mat_back
# Set Pose transformations...
for b_item, _b_size, _p_uuid, _clusters in bones.values():
fbx_bdata, bl_bname = b_item
fbx_props = (elem_find_first(fbx_bdata, b'Properties70'),
elem_find_first(fbx_tmpl, b'Properties70', fbx_elem_nil))
assert(fbx_props[0] is not None)
pbo = b_item[1] = bl_adata.pose.bones[bl_bname]
transform_data = object_tdata_cache.get(pbo)
if transform_data is None:
# Bone correction, gives a mess as result. :(
transform_data = blen_read_object_transform_preprocess(fbx_props, fbx_bdata, MAT_CONVERT_BONE)
object_tdata_cache[pbo] = transform_data
mat = blen_read_object_transform_do(transform_data)
if pbo.parent:
# Bring back matrix in armature space.
mat = pbo.parent.matrix * mat
pbo.matrix = mat
# ----
# Mesh
@ -1232,14 +1420,133 @@ def load(operator, context, filepath="",
def connection_filter_reverse(fbx_uuid, fbx_id):
return connection_filter_ex(fbx_uuid, fbx_id, fbx_connection_map_reverse)
# Armatures pre-processing!
fbx_objects_ignore = set()
fbx_objects_parent_ignore = set()
# Arg! In some case, root bone is used as armature as well, in Blender we have to 'insert'
# an armature object between them, so to handle possible parents of root bones we need a mapping
# from root bone uuid to Blender's object...
fbx_bones_to_fake_object = dict()
armatures = []
def _():
nonlocal fbx_objects_ignore, fbx_objects_parent_ignore
for a_uuid, a_item in fbx_table_nodes.items():
root_bone = False
fbx_adata, bl_adata = a_item = fbx_table_nodes.get(a_uuid, (None, None))
if fbx_adata is None or fbx_adata.id != b'Model':
continue
elif fbx_adata.props[2] != b'Null':
if fbx_adata.props[2] not in {b'LimbNode', b'Root'}:
continue
# In some cases, armatures have no root 'Null' object, we have to consider all root bones
# as armatures in this case. :/
root_bone = True
for p_uuid, p_ctype in fbx_connection_map.get(a_uuid, ()):
if p_ctype.props[0] != b'OO':
continue
fbx_pdata, bl_pdata = p_item = fbx_table_nodes.get(p_uuid, (None, None))
if (fbx_pdata and fbx_pdata.id == b'Model' and fbx_pdata.props[2] in {b'LimbNode', b'Root', b'Null'}):
# Not a root bone...
root_bone = False
if not root_bone:
continue
fbx_bones_to_fake_object[a_uuid] = None
bones = {}
todo_uuids = set() if root_bone else {a_uuid}
init_uuids = {a_uuid} if root_bone else set()
done_uuids = set()
while todo_uuids or init_uuids:
if init_uuids:
p_uuid = None
uuids = [(uuid, None) for uuid in init_uuids]
init_uuids = None
else:
p_uuid = todo_uuids.pop()
uuids = fbx_connection_map_reverse.get(p_uuid, ())
# bone -> cluster -> skin -> mesh.
# XXX Note: only LimbNode for now (there are also Limb's :/ ).
for b_uuid, b_ctype in uuids:
if b_ctype and b_ctype.props[0] != b'OO':
continue
fbx_bdata, bl_bdata = b_item = fbx_table_nodes.get(b_uuid, (None, None))
if (fbx_bdata is None or fbx_bdata.id != b'Model' or
fbx_bdata.props[2] not in {b'LimbNode', b'Root'}):
continue
# Find bone's size.
size = 1.0
for t_uuid, t_ctype in fbx_connection_map_reverse.get(b_uuid, ()):
if t_ctype.props[0] != b'OO':
continue
fbx_tdata, _bl_tdata = fbx_table_nodes.get(t_uuid, (None, None))
if fbx_tdata is None or fbx_tdata.id != b'NodeAttribute' or fbx_tdata.props[2] != b'LimbNode':
continue
fbx_props = (elem_find_first(fbx_tdata, b'Properties70'),)
size = elem_props_get_number(fbx_props, b'Size', default=size)
break # Only one bone data per bone!
clusters = []
for c_uuid, c_ctype in fbx_connection_map.get(b_uuid, ()):
if c_ctype.props[0] != b'OO':
continue
fbx_cdata, _bl_cdata = fbx_table_nodes.get(c_uuid, (None, None))
if fbx_cdata is None or fbx_cdata.id != b'Deformer' or fbx_cdata.props[2] != b'Cluster':
continue
meshes = set()
objects = []
for s_uuid, s_ctype in fbx_connection_map.get(c_uuid, ()):
if s_ctype.props[0] != b'OO':
continue
fbx_sdata, _bl_sdata = fbx_table_nodes.get(s_uuid, (None, None))
if fbx_sdata is None or fbx_sdata.id != b'Deformer' or fbx_sdata.props[2] != b'Skin':
continue
for m_uuid, m_ctype in fbx_connection_map.get(s_uuid, ()):
if m_ctype.props[0] != b'OO':
continue
fbx_mdata, bl_mdata = fbx_table_nodes.get(m_uuid, (None, None))
if fbx_mdata is None or fbx_mdata.id != b'Geometry' or fbx_mdata.props[2] != b'Mesh':
continue
# Blenmeshes are assumed already created at that time!
assert(isinstance(bl_mdata, bpy.types.Mesh))
# And we have to find all objects using this mesh!
for o_uuid, o_ctype in fbx_connection_map.get(m_uuid, ()):
if o_ctype.props[0] != b'OO':
continue
fbx_odata, bl_odata = o_item = fbx_table_nodes.get(o_uuid, (None, None))
if fbx_odata is None or fbx_odata.id != b'Model' or fbx_odata.props[2] != b'Mesh':
continue
# bl_odata is still None, objects have not yet been created...
objects.append(o_item)
meshes.add(bl_mdata)
# Skin deformers are only here to connect clusters to meshes, for us, nothing else to do.
clusters.append((fbx_cdata, meshes, objects))
# For now, we assume there is only one cluster & skin per bone (at least for a given armature)!
# XXX This is not true, some apps export several clusters (kind of LoD), we only use first one!
# assert(len(clusters) <= 1)
bones[b_uuid] = (b_item, size, p_uuid if (p_uuid != a_uuid or root_bone) else None, clusters)
fbx_objects_parent_ignore.add(b_uuid)
done_uuids.add(p_uuid)
todo_uuids.add(b_uuid)
if bones:
# in case we have no Null parent, rootbone will be a_item too...
armatures.append((a_item, bones))
fbx_objects_ignore.add(a_uuid)
fbx_objects_ignore |= fbx_objects_parent_ignore
# We need to handle parenting at object-level for rootbones-as-armature case :/
fbx_objects_parent_ignore -= set(fbx_bones_to_fake_object.keys())
_(); del _
def _():
fbx_tmpl = fbx_template_get((b'Model', b'KFbxNode'))
# Link objects, keep first, this also creates objects
objects = []
for fbx_uuid, fbx_item in fbx_table_nodes.items():
if fbx_uuid in fbx_objects_ignore:
# armatures and bones, handled separately.
continue
fbx_obj, blen_data = fbx_item
if fbx_obj.id != b'Model':
if fbx_obj.id != b'Model' or fbx_obj.props[2] in {b'Root', b'LimbNode'}:
continue
# Create empty object or search for object data
@ -1273,18 +1580,31 @@ def load(operator, context, filepath="",
# instance in scene
obj_base = scene.objects.link(obj)
obj_base.select = True
_(); del _
objects.append(obj)
# Now that we have objects...
# II) We can finish armatures processing.
def _():
fbx_tmpl = fbx_template_get((b'Model', b'KFbxNode'))
blen_read_armatures(fbx_tmpl, armatures, fbx_bones_to_fake_object, scene, global_matrix)
_(); del _
def _():
# Parent objects, after we created them...
for fbx_uuid, fbx_item in fbx_table_nodes.items():
if fbx_uuid in fbx_objects_parent_ignore:
# Ignore bones, but not armatures here!
continue
fbx_obj, blen_data = fbx_item
if fbx_obj.id != b'Model':
continue
if blen_data is None:
# Handle rootbone-as-armature case :/
t_data = fbx_bones_to_fake_object.get(fbx_uuid)
if t_data is not None:
blen_data = t_data
elif blen_data is None:
continue # no object loaded.. ignore
for (fbx_lnk,
@ -1298,10 +1618,17 @@ def load(operator, context, filepath="",
if global_matrix is not None:
# Apply global matrix last (after parenting)
for fbx_uuid, fbx_item in fbx_table_nodes.items():
if fbx_uuid in fbx_objects_parent_ignore:
# Ignore bones, but not armatures here!
continue
fbx_obj, blen_data = fbx_item
if fbx_obj.id != b'Model':
continue
if blen_data is None:
# Handle rootbone-as-armature case :/
t_data = fbx_bones_to_fake_object.get(fbx_uuid)
if t_data is not None:
blen_data = t_data
elif blen_data is None:
continue # no object loaded.. ignore
if blen_data.parent is None: