FBX export: rework animation handling, and add shapes animation support.

This commit is contained in:
Bastien Montagne 2014-07-06 21:49:06 +02:00
parent 0abd36f1ec
commit 59ff66fa7a
2 changed files with 190 additions and 111 deletions

View File

@ -55,6 +55,7 @@ from .fbx_utils import (
FBX_POSE_BIND_VERSION, FBX_DEFORMER_SKIN_VERSION, FBX_DEFORMER_CLUSTER_VERSION,
FBX_MATERIAL_VERSION, FBX_TEXTURE_VERSION,
FBX_ANIM_KEY_VERSION,
FBX_ANIM_PROPSGROUP_NAME,
FBX_KTIME,
BLENDER_OTHER_OBJECT_TYPES, BLENDER_OBJECT_TYPES_MESHLIKE,
FBX_LIGHT_TYPES, FBX_LIGHT_DECAY_TYPES,
@ -83,6 +84,8 @@ from .fbx_utils import (
elem_props_template_init, elem_props_template_set, elem_props_template_finalize,
# Templates.
FBXTemplate, fbx_templates_generate,
# Animation.
AnimationCurveNodeWrapper,
# Objects.
ObjectWrapper, fbx_name_class,
# Top level.
@ -496,7 +499,7 @@ def fbx_template_def_animlayer(scene, settings, override_defaults=None, nbr_user
def fbx_template_def_animcurvenode(scene, settings, override_defaults=None, nbr_users=0):
props = OrderedDict((
(b"d", (None, "p_compound", False)),
(FBX_ANIM_PROPSGROUP_NAME.encode(), (None, "p_compound", False)),
))
if override_defaults is not None:
props.update(override_defaults)
@ -1693,61 +1696,13 @@ def fbx_skeleton_from_armature(scene, settings, arm_obj, objects, data_meshes,
objects.update(bones)
def fbx_animations_simplify(scene_data, animdata):
"""
Simplifies FCurves!
"""
fac = scene_data.settings.bake_anim_simplify_factor
step = scene_data.settings.bake_anim_step
# So that, with default factor and step values (1), we get:
max_frame_diff = step * fac * 10 # max step of 10 frames.
value_diff_fac = fac / 1000 # min value evolution: 0.1% of whole range.
min_significant_diff = 1.0e-6
for keys in animdata.values():
if not keys:
continue
extremums = [(min(values), max(values)) for values in zip(*(k[1] for k in keys))]
min_diffs = [max((mx - mn) * value_diff_fac, min_significant_diff) for mn, mx in extremums]
p_currframe, p_key, p_key_write = keys[0]
p_keyed = [(p_currframe - max_frame_diff, val) for val in p_key]
are_keyed = [False] * len(p_key)
for currframe, key, key_write in keys:
for idx, (val, p_val) in enumerate(zip(key, p_key)):
p_keyedframe, p_keyedval = p_keyed[idx]
if val == p_val:
# Never write keyframe when value is exactly the same as prev one!
continue
if abs(val - p_val) >= min_diffs[idx]:
# If enough difference from previous sampled value, key this value *and* the previous one!
key_write[idx] = True
p_key_write[idx] = True
p_keyed[idx] = (currframe, val)
are_keyed[idx] = True
else:
frame_diff = currframe - p_keyedframe
val_diff = abs(val - p_keyedval)
if ((val_diff >= min_diffs[idx]) or
((val_diff >= min_significant_diff) and (frame_diff >= max_frame_diff))):
# Else, if enough difference from previous keyed value
# (or any significant difference and max gap between keys is reached),
# key this value only!
key_write[idx] = True
p_keyed[idx] = (currframe, val)
are_keyed[idx] = True
p_currframe, p_key, p_key_write = currframe, key, key_write
# If we did key something, ensure first and last sampled values are keyed as well.
for idx, is_keyed in enumerate(are_keyed):
if is_keyed:
keys[0][2][idx] = keys[-1][2][idx] = True
def fbx_animations_objects_do(scene_data, ref_id, f_start, f_end, start_zero, objects=None, force_keep=False):
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.
"""
bake_step = scene_data.settings.bake_anim_step
scene = scene_data.scene
meshes = scene_data.data_meshes
if objects is not None:
# Add bones and duplis!
@ -1764,15 +1719,22 @@ def fbx_animations_objects_do(scene_data, ref_id, f_start, f_end, start_zero, ob
else:
objects = scene_data.objects
# FBX mapping info: Property affected, and name of the "sub" property (to distinguish e.g. vector's channels).
fbx_names = (
("Lcl Translation", "T", "d|X"), ("Lcl Translation", "T", "d|Y"), ("Lcl Translation", "T", "d|Z"),
("Lcl Rotation", "R", "d|X"), ("Lcl Rotation", "R", "d|Y"), ("Lcl Rotation", "R", "d|Z"),
("Lcl Scaling", "S", "d|X"), ("Lcl Scaling", "S", "d|Y"), ("Lcl Scaling", "S", "d|Z"),
)
back_currframe = scene.frame_current
animdata = OrderedDict((obj, []) for obj in objects)
animdata_ob = OrderedDict((ob_obj, (AnimationCurveNodeWrapper(ob_obj.key, 'LCL_TRANSLATION', (0.0, 0.0, 0.0)),
AnimationCurveNodeWrapper(ob_obj.key, 'LCL_ROTATION', (0.0, 0.0, 0.0)),
AnimationCurveNodeWrapper(ob_obj.key, 'LCL_SCALING', (1.0, 1.0, 1.0))))
for ob_obj in objects)
animdata_shapes = OrderedDict()
for me, (me_key, _shapes_key, shapes) in scene_data.data_deformers_shape.items():
# Ignore absolute shape keys for now!
if not me.shape_keys.use_relative:
continue
for shape, (channel_key, geom_key, _shape_verts_co, _shape_verts_idx) in shapes.items():
acnode = AnimationCurveNodeWrapper(channel_key, 'SHAPE_KEY', (0.0,))
# Sooooo happy to have to twist again like a mad snake... Yes, we need to write those curves twice. :/
acnode.add_group(me_key, shape.name, shape.name, (shape.name,))
animdata_shapes[channel_key] = (acnode, me, shape)
p_rots = {}
@ -1781,63 +1743,49 @@ def fbx_animations_objects_do(scene_data, ref_id, f_start, f_end, start_zero, ob
real_currframe = currframe - f_start if start_zero else currframe
scene.frame_set(int(currframe), currframe - int(currframe))
for ob_obj in objects:
for ob_obj in animdata_ob:
ob_obj.dupli_list_create(scene, 'RENDER')
for ob_obj in objects:
for ob_obj, (anim_loc, anim_rot, anim_scale) in animdata_ob.items():
# We compute baked loc/rot/scale for all objects (rot being euler-compat with previous value!).
p_rot = p_rots.get(ob_obj, None)
loc, rot, scale, _m, _mr = ob_obj.fbx_object_tx(scene_data, rot_euler_compat=p_rot)
p_rots[ob_obj] = rot
tx = tuple(loc) + tuple(convert_rad_to_deg_iter(rot)) + tuple(scale)
animdata[ob_obj].append((real_currframe, tx, [False] * len(tx)))
anim_loc.add_keyframe(real_currframe, loc)
anim_rot.add_keyframe(real_currframe, tuple(convert_rad_to_deg_iter(rot)))
anim_scale.add_keyframe(real_currframe, scale)
for ob_obj in objects:
ob_obj.dupli_list_clear()
for anim_shape, me, shape in animdata_shapes.values():
anim_shape.add_keyframe(real_currframe, (shape.value * 100.0,))
currframe += bake_step
scene.frame_set(back_currframe, 0.0)
fbx_animations_simplify(scene_data, animdata)
animations = OrderedDict()
simplify_fac = scene_data.settings.bake_anim_simplify_factor
# And now, produce final data (usable by FBX export code)...
for ob_obj, keys in animdata.items():
if not keys:
continue
curves = [[] for k in keys[0][1]]
for currframe, key, key_write in keys:
for idx, (val, wrt) in enumerate(zip(key, key_write)):
if wrt:
curves[idx].append((currframe, val))
# And now, produce final data (usable by FBX export code)
# Objects-like loc/rot/scale...
for ob_obj, anims in animdata_ob.items():
for anim in anims:
anim.simplfy(simplify_fac, bake_step)
if anim:
for obj_key, group_key, group, fbx_group, fbx_gname in anim.get_final_data(scene, ref_id, force_keep):
anim_data = animations.get(obj_key)
if anim_data is None:
anim_data = animations[obj_key] = ("dummy_unused_key", OrderedDict())
anim_data[1][fbx_group] = (group_key, group, fbx_gname)
obj_key = ob_obj.key
# Get PoseBone from bone...
#tobj = bone_map[obj] if isinstance(obj, Bone) else obj
#loc, rot, scale, _m, _mr = fbx_object_tx(scene_data, tobj)
#tx = tuple(loc) + tuple(convert_rad_to_deg_iter(rot)) + tuple(scale)
dtx = (0.0, 0.0, 0.0) + (0.0, 0.0, 0.0) + (1.0, 1.0, 1.0)
# If animation for a channel, (True, keyframes), else (False, current value).
# And meshes' shape keys.
for channel_key, (anim_shape, me, shape) in animdata_shapes.items():
final_keys = OrderedDict()
for idx, c in enumerate(curves):
fbx_group, fbx_gname, fbx_item = fbx_names[idx]
fbx_item_key = get_blender_anim_curve_key(scene, ref_id, obj_key, fbx_group, fbx_item)
if fbx_group not in final_keys:
fbx_group_key = get_blender_anim_curve_node_key(scene, ref_id, obj_key, fbx_group)
final_keys[fbx_group] = (fbx_group_key, OrderedDict(), fbx_gname)
final_keys[fbx_group][1][fbx_item] = (fbx_item_key, dtx[idx], c,
True if (len(c) > 1 or (len(c) > 0 and force_keep)) else False)
# And now, remove anim groups (i.e. groups of curves affecting a single FBX property) with no curve at all!
del_groups = []
for grp, (_k, data, _n) in final_keys.items():
if True in (d[3] for d in data.values()):
continue
del_groups.append(grp)
for grp in del_groups:
del final_keys[grp]
if final_keys:
#animations[obj] = (get_blender_anim_layer_key(scene, obj.bdata), final_keys)
animations[ob_obj] = ("dummy_unused_key", final_keys)
anim_shape.simplfy(simplify_fac, bake_step)
if anim_shape:
for elem_key, group_key, group, fbx_group, fbx_gname in anim_shape.get_final_data(scene, ref_id, force_keep):
anim_data = animations.get(elem_key)
if anim_data is None:
anim_data = animations[elem_key] = ("dummy_unused_key", OrderedDict())
anim_data[1][fbx_group] = (group_key, group, fbx_gname)
astack_key = get_blender_anim_stack_key(scene, ref_id)
alayer_key = get_blender_anim_layer_key(scene, ref_id)
@ -1850,7 +1798,7 @@ def fbx_animations_objects_do(scene_data, ref_id, f_start, f_end, start_zero, ob
return (astack_key, animations, alayer_key, name, f_start, f_end) if animations else None
def fbx_animations_objects(scene_data):
def fbx_animations(scene_data):
"""
Generate global animation data from objects.
"""
@ -1890,7 +1838,7 @@ def fbx_animations_objects(scene_data):
for strip in strips:
strip.mute = False
add_anim(animations, fbx_animations_objects_do(scene_data, strip, strip.frame_start, strip.frame_end, True))
add_anim(animations, fbx_animations_do(scene_data, strip, strip.frame_start, strip.frame_end, True))
strip.mute = True
for strip in strips:
@ -1954,7 +1902,7 @@ def fbx_animations_objects(scene_data):
ob.animation_data.action = act
frame_start, frame_end = act.frame_range # sic!
add_anim(animations,
fbx_animations_objects_do(scene_data, (ob, act), frame_start, frame_end, True, {ob_obj}, True))
fbx_animations_do(scene_data, (ob, act), frame_start, frame_end, True, {ob_obj}, True))
# Ugly! :/
ob.animation_data.action = None if org_act is ... else org_act
restore_object(ob, ob_copy)
@ -1968,7 +1916,7 @@ def fbx_animations_objects(scene_data):
# Global (containing everything) animstack.
if not scene_data.settings.bake_anim_use_nla_strips or not animations:
add_anim(animations, fbx_animations_objects_do(scene_data, None, scene.frame_start, scene.frame_end, False))
add_anim(animations, fbx_animations_do(scene_data, None, scene.frame_start, scene.frame_end, False))
# Be sure to update all matrices back to org state!
scene.frame_set(scene.frame_current, 0.0)
@ -2147,7 +2095,7 @@ def fbx_data_from_scene(scene, settings):
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)
animations, frame_start, frame_end = fbx_animations(tmp_scdata)
##### Creation of templates...
@ -2329,8 +2277,8 @@ def fbx_data_from_scene(scene, settings):
# For now, only one layer!
alayer_id = get_fbx_uuid_from_key(alayer_key)
connections.append((b"OO", alayer_id, astack_id, None))
for ob_obj, (alayer_key, acurvenodes) in astack.items():
ob_id = ob_obj.fbx_uuid
for elem_key, (alayer_key, acurvenodes) in astack.items():
elem_id = get_fbx_uuid_from_key(elem_key)
# Animlayer -> animstack.
# alayer_id = get_fbx_uuid_from_key(alayer_key)
# connections.append((b"OO", alayer_id, astack_id, None))
@ -2339,7 +2287,7 @@ def fbx_data_from_scene(scene, settings):
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, ob_id, fbx_prop.encode()))
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.
@ -2542,8 +2490,8 @@ def fbx_objects_elements(root, scene_data):
fbx_data_camera_elements(objects, cam, scene_data)
done_meshes = set()
for me in scene_data.data_meshes:
fbx_data_mesh_elements(objects, me, scene_data, done_meshes)
for me_obj in scene_data.data_meshes:
fbx_data_mesh_elements(objects, me_obj, scene_data, done_meshes)
del done_meshes
for ob_obj in scene_data.objects:

View File

@ -65,6 +65,7 @@ FBX_TEXTURE_VERSION = 202
FBX_ANIM_KEY_VERSION = 4008
FBX_NAME_CLASS_SEP = b"\x00\x01"
FBX_ANIM_PROPSGROUP_NAME = "d"
FBX_KTIME = 46186158000 # This is the number of "ktimes" in one second (yep, precision over the nanosecond...)
@ -619,6 +620,136 @@ def fbx_templates_generate(root, fbx_templates):
print(props, ptype, name, value, animatable)
##### FBX animation helpers. #####
class AnimationCurveNodeWrapper:
"""
This class provides a same common interface for all (FBX-wise) AnimationCurveNode and AnimationCurve elements,
and easy API to handle those.
"""
__slots__ = ('elem_keys', '_keys', 'default_values', 'fbx_group', 'fbx_gname', 'fbx_props')
kinds = {
'LCL_TRANSLATION': ("Lcl Translation", "T", ("X", "Y", "Z")),
'LCL_ROTATION': ("Lcl Rotation", "R", ("X", "Y", "Z")),
'LCL_SCALING': ("Lcl Scaling", "S", ("X", "Y", "Z")),
'SHAPE_KEY': ("DeformPercent", "DeformPercent", ("DeformPercent",)),
}
def __init__(self, elem_key, kind, default_values=...):
"""
bdata might be an Object, DupliObject, Bone or PoseBone.
If Bone or PoseBone, armature Object must be provided.
"""
self.elem_keys = [elem_key]
assert(kind in self.kinds)
self.fbx_group = [self.kinds[kind][0]]
self.fbx_gname = [self.kinds[kind][1]]
self.fbx_props = [self.kinds[kind][2]]
self._keys = [] # (frame, values, write_flags)
if default_values is not ...:
assert(len(default_values) == len(self.fbx_props[0]))
self.default_values = default_values
else:
self.default_values = (0.0) * len(self.fbx_props[0])
def __bool__(self):
# We are 'True' if we do have some validated keyframes...
return self._keys and True in ((True in k[2]) for k in self._keys)
def add_group(self, elem_key, fbx_group, fbx_gname, fbx_props):
"""
Add another whole group stuff (curvenode, animated item/prop + curvnode/curve identifiers).
E.g. Shapes animations is written twice, houra!
"""
assert(len(fbx_props) == len(self.fbx_props[0]))
self.elem_keys.append(elem_key)
self.fbx_group.append(fbx_group)
self.fbx_gname.append(fbx_gname)
self.fbx_props.append(fbx_props)
def add_keyframe(self, frame, values):
"""
Add a new keyframe to all curves of the group.
"""
assert(len(values) == len(self.fbx_props[0]))
self._keys.append((frame, values, [True] * len(values))) # write everything by default.
def simplfy(self, fac, step):
"""
Simplifies sampled curves by only enabling samples when:
* their values differ significantly from the previous sample ones, or
* their values differ significantly from the previous validated sample ones, or
* the previous validated samples are far enough from current ones in time.
"""
if not self._keys:
return
# So that, with default factor and step values (1), we get:
max_frame_diff = step * fac * 10 # max step of 10 frames.
value_diff_fac = fac / 1000 # min value evolution: 0.1% of whole range.
min_significant_diff = 1.0e-6
keys = self._keys
extremums = tuple((min(values), max(values)) for values in zip(*(k[1] for k in keys)))
min_diffs = tuple(max((mx - mn) * value_diff_fac, min_significant_diff) for mn, mx in extremums)
p_currframe, p_key, p_key_write = keys[0]
p_keyed = [(p_currframe - max_frame_diff, val) for val in p_key]
are_keyed = [False] * len(p_key)
for currframe, key, key_write in keys:
for idx, (val, p_val) in enumerate(zip(key, p_key)):
key_write[idx] = False
p_keyedframe, p_keyedval = p_keyed[idx]
if val == p_val:
# Never write keyframe when value is exactly the same as prev one!
continue
if abs(val - p_val) >= min_diffs[idx]:
# If enough difference from previous sampled value, key this value *and* the previous one!
key_write[idx] = True
p_key_write[idx] = True
p_keyed[idx] = (currframe, val)
are_keyed[idx] = True
else:
frame_diff = currframe - p_keyedframe
val_diff = abs(val - p_keyedval)
if ((val_diff >= min_diffs[idx]) or
((val_diff >= min_significant_diff) and (frame_diff >= max_frame_diff))):
# Else, if enough difference from previous keyed value
# (or any significant difference and max gap between keys is reached),
# key this value only!
key_write[idx] = True
p_keyed[idx] = (currframe, val)
are_keyed[idx] = True
p_currframe, p_key, p_key_write = currframe, key, key_write
# If we did key something, ensure first and last sampled values are keyed as well.
for idx, is_keyed in enumerate(are_keyed):
if is_keyed:
keys[0][2][idx] = keys[-1][2][idx] = True
def get_final_data(self, scene, ref_id, force_keep=False):
"""
Yield final anim data for this 'curvenode' (for all curvenodes defined).
force_keep is to force to keep a curve even if it only has one valid keyframe.
"""
curves = [[] for k in self._keys[0][1]]
for currframe, key, key_write in self._keys:
for curve, val, wrt in zip(curves, key, key_write):
if wrt:
curve.append((currframe, val))
for elem_key, fbx_group, fbx_gname, fbx_props in zip(self.elem_keys, self.fbx_group, self.fbx_gname, self.fbx_props):
group_key = get_blender_anim_curve_node_key(scene, ref_id, elem_key, fbx_group)
group = OrderedDict()
for c, def_val, fbx_item in zip(curves, self.default_values, fbx_props):
fbx_item = FBX_ANIM_PROPSGROUP_NAME + "|" + fbx_item
curve_key = get_blender_anim_curve_key(scene, ref_id, elem_key, fbx_group, fbx_item)
# (curve key, default value, keyframes, write flag).
group[fbx_item] = (curve_key, def_val, c, True if (len(c) > 1 or (len(c) > 0 and force_keep)) else False)
yield elem_key, group_key, group, fbx_group, fbx_gname
##### FBX objects generators. #####
# FBX Model-like data (i.e. Blender objects, dupliobjects and bones) are wrapped in ObjectWrapper.