[Bf-extensions-cvs] [09c9fa9] master: FBX import: add basic support for animation.
Bastien Montagne
noreply at git.blender.org
Mon Jul 21 22:01:19 CEST 2014
Commit: 09c9fa9a7ba5a156cd4a8e0602aa2ff648b30fbb
Author: Bastien Montagne
Date: Mon Jul 21 21:18:27 2014 +0200
Branches: master
https://developer.blender.org/rBA09c9fa9a7ba5a156cd4a8e0602aa2ff648b30fbb
FBX import: add basic support for animation.
Only objects and bones loc/rot/scale, and shapekeys' value, for now.
*IMPORTANT* note: we currently assume all FBX anim curves are linearly interpolated.
This means unless you exported your animation in 'baked' mode, you should expect
bad results!
Supporting advanced FBX's interpolations (Bézier-like) is on TODO, but no serious ETA yet.
===================================================================
M io_scene_fbx/import_fbx.py
===================================================================
diff --git a/io_scene_fbx/import_fbx.py b/io_scene_fbx/import_fbx.py
index 70af67b..18f31a0 100644
--- a/io_scene_fbx/import_fbx.py
+++ b/io_scene_fbx/import_fbx.py
@@ -581,6 +581,177 @@ def blen_read_armatures(fbx_tmpl, armatures, fbx_bones_to_fake_object, scene, gl
pbo.matrix = mat
+# ---------
+# Animation
+def blen_read_animations_curves_iter(fbx_curves, blen_start_offset, fbx_start_offset, fps):
+ """
+ Get raw FBX AnimCurve list, and yield values for all curves at each singular curves' keyframes,
+ together with (blender) timing, in frames.
+ blen_start_offset is expected in frames, while fbx_start_offset is expected in FBX ktime.
+ """
+ # As a first step, assume linear interpolation between key frames, we'll (try to!) handle more
+ # of FBX curves later.
+ from .fbx_utils import FBX_KTIME
+ timefac = fps / FBX_KTIME
+
+ curves = tuple([0,
+ elem_prop_first(elem_find_first(c[2], b'KeyTime')),
+ elem_prop_first(elem_find_first(c[2], b'KeyValueFloat')),
+ c]
+ for c in fbx_curves)
+
+ while True:
+ tmin = min(curves, key=lambda e: e[1][e[0]])
+ curr_fbxktime = tmin[1][tmin[0]]
+ curr_values = []
+ do_break = True
+ for item in curves:
+ idx, times, values, fbx_curve = item
+ if idx != -1:
+ do_break = False
+ if times[idx] > curr_fbxktime:
+ if idx == 0:
+ curr_values.append((values[idx], fbx_curve))
+ else:
+ # Interpolate between this key and the previous one.
+ ifac = (curr_fbxktime - times[idx - 1]) / (times[idx] - times[idx - 1])
+ curr_values.append(((values[idx] - values[idx - 1]) * ifac + values[idx - 1], fbx_curve))
+ else:
+ curr_values.append((values[idx], fbx_curve))
+ if idx >= 0:
+ idx += 1
+ if idx >= len(times):
+ # We have reached our last element for this curve, stay on it from now on...
+ idx = -1
+ item[0] = idx
+ curr_blenkframe = (curr_fbxktime - fbx_start_offset) * timefac + blen_start_offset
+ yield (curr_blenkframe, curr_values)
+ if do_break:
+ break
+
+
+def blen_read_animations_action_item(action, item, cnodes, global_matrix, force_global, fps):
+ """
+ 'Bake' loc/rot/scale into the action, taking into account global_matrix if no parent is present.
+ """
+ from bpy.types import Object, PoseBone, ShapeKey
+ from mathutils import Euler, Matrix
+ from itertools import chain
+
+ blen_curves = []
+ fbx_curves = []
+ props = []
+
+ if isinstance(item, ShapeKey):
+ props = [(item.path_from_id("value"), 1, "Key")]
+ else: # Object or PoseBone:
+ if item not in object_tdata_cache:
+ print("ERROR! object '%s' has no transform data, while being animated!" % ob.name)
+ return
+
+ # We want to create actions for objects, but for bones we 'reuse' armatures' actions!
+ grpname = None
+ if item.id_data != item:
+ grpname = item.name
+
+ # Since we might get other channels animated in the end, due to all FBX transform magic,
+ # we need to add curves for whole loc/rot/scale in any case.
+ props = [(item.path_from_id("location"), 3, grpname or "Location"),
+ None,
+ (item.path_from_id("scale"), 3, grpname or "Scale")]
+ rot_mode = item.rotation_mode
+ if rot_mode == 'QUATERNION':
+ props[1] = (item.path_from_id("rotation_quaternion"), 4, grpname or "Quaternion Rotation")
+ elif rot_mode == 'AXIS_ANGLE':
+ props[1] = (item.path_from_id("rotation_axis_angle"), 4, grpname or "Axis Angle Rotation")
+ else: # Euler
+ props[1] = (item.path_from_id("rotation_euler"), 3, grpname or "Euler Rotation")
+
+ blen_curves = [action.fcurves.new(prop, channel, grpname)
+ for prop, nbr_channels, grpname in props for channel in range(nbr_channels)]
+
+ for curves, fbxprop in cnodes.values():
+ for (fbx_acdata, _blen_data), channel in curves.values():
+ fbx_curves.append((fbxprop, channel, fbx_acdata))
+
+ if isinstance(item, ShapeKey):
+ # We assume for now blen init point is frame 1.0, while FBX ktime init point is 0.
+ for frame, values in blen_read_animations_curves_iter(fbx_curves, 1.0, 0, fps):
+ value = 0.0
+ for v, (fbxprop, channel, _fbx_acdata) in values:
+ assert(fbxprop == b'DeformPercent')
+ assert(channel == 0)
+ value = v / 100.0
+
+ for fc, v in zip(blen_curves, (value,)):
+ fc.keyframe_points.insert(frame, v, {'NEEDED', 'FAST'}).interpolation = 'LINEAR'
+
+ else: # Object or PoseBone:
+ transform_data = object_tdata_cache[item]
+ rot_prev = item.rotation_euler.copy()
+
+ # We assume for now blen init point is frame 1.0, while FBX ktime init point is 0.
+ for frame, values in blen_read_animations_curves_iter(fbx_curves, 1.0, 0, fps):
+ for v, (fbxprop, channel, _fbx_acdata) in values:
+ if fbxprop == b'Lcl Translation':
+ transform_data.loc[channel] = v
+ elif fbxprop == b'Lcl Rotation':
+ transform_data.rot[channel] = v
+ elif fbxprop == b'Lcl Scaling':
+ transform_data.sca[channel] = v
+ mat = blen_read_object_transform_do(transform_data)
+ # Don't forget global matrix - but never for bones!
+ if isinstance(item, Object):
+ if (not item.parent or force_global) and global_matrix is not None:
+ mat = global_matrix * mat
+ else: # PoseBone, Urg!
+ # First, get local (i.e. parentspace) rest pose matrix
+ restmat = item.bone.matrix_local
+ if item.parent:
+ restmat = item.parent.bone.matrix_local.inverted() * restmat
+ # And now, remove that rest pose matrix from current mat (also in parent space).
+ mat = restmat.inverted() * mat
+
+ # Now we have a virtual matrix of transform from AnimCurves, we can insert keyframes!
+ loc, rot, sca = mat.decompose()
+ if rot_mode == 'QUATERNION':
+ pass # nothing to do!
+ elif rot_mode == 'AXIS_ANGLE':
+ vec, ang = rot.to_axis_angle()
+ rot = ang, vec.x, vec.y, vec.z
+ else: # Euler
+ rot = rot.to_euler(rot_mode, rot_prev)
+ rot_prev = rot
+ for fc, value in zip(blen_curves, chain(loc, rot, sca)):
+ fc.keyframe_points.insert(frame, value, {'NEEDED', 'FAST'}).interpolation = 'LINEAR'
+
+ # Since we inserted our keyframes in 'FAST' mode, we have to update the fcurves now.
+ for fc in blen_curves:
+ fc.update()
+
+
+def blen_read_animations(fbx_tmpl_astack, fbx_tmpl_alayer, stacks, scene, global_matrix, force_global_objects):
+ """
+ Recreate an action per stack/layer/object combinations.
+ Note actions are not linked to objects, this is up to the user!
+ """
+ actions = {}
+ for as_uuid, ((fbx_asdata, _blen_data), alayers) in stacks.items():
+ stack_name = elem_name_ensure_class(fbx_asdata, b'AnimStack')
+ for al_uuid, ((fbx_aldata, _blen_data), items) in alayers.items():
+ layer_name = elem_name_ensure_class(fbx_aldata, b'AnimLayer')
+ for item, cnodes in items.items():
+ id_data = item.id_data
+ key = (as_uuid, al_uuid, id_data)
+ action = actions.get(key)
+ if action is None:
+ action_name = "|".join((id_data.name, stack_name, layer_name))
+ actions[key] = action = bpy.data.actions.new(action_name)
+ action.use_fake_user = True
+ blen_read_animations_action_item(action, item, cnodes, global_matrix,
+ item in force_global_objects, scene.render.fps)
+
+
# ----
# Mesh
@@ -1639,6 +1810,7 @@ def load(operator, context, filepath="",
# II) We can finish armatures processing.
arm_parents = set()
+ force_global_objects = set()
def _():
fbx_tmpl = fbx_template_get((b'Model', b'KFbxNode'))
@@ -1693,6 +1865,97 @@ def load(operator, context, filepath="",
ob_me.matrix_basis = global_matrix * ob_me.matrix_basis
# And reverse-apply armature transform, so that it gets valid parented (local) position!
ob_me.matrix_parent_inverse = ob_arm.matrix_basis.inverted()
+ force_global_objects.add(ob_me)
+ _(); del _
+
+ # Animation!
+ def _():
+ fbx_tmpl_astack = fbx_template_get((b'AnimationStack', b'FbxAnimStack'))
+ fbx_tmpl_alayer = fbx_template_get((b'AnimationLayer', b'FbxAnimLayer'))
+ stacks = {}
+
+ # AnimationStacks.
+ for as_uuid, fbx_asitem in fbx_table_nodes.items():
+ fbx_asdata, _blen_data = fbx_asitem
+ if fbx_asdata.id != b'AnimationStack' or fbx_asdata.props[2] != b'':
+ continue
+ stacks[as_uuid] = (fbx_asitem, {})
+
+ # AnimationLayers (mixing is completely ignored for now, each layer results in an independent set of actions).
+ def get_astacks_from_alayer(al_uuid):
+ for as_uuid, as_ctype in fbx_connection_map.get(al_uuid, ()):
+ if as_ctype.props[0] != b'OO':
+ continue
+ fbx_asdata, _bl_asdata = fbx_table_nodes.get(as_uuid, (None, None))
+ if (fbx_asdata is None or fbx_asdata.id != b'AnimationStack' or
+ fbx_asdata.props[2] != b'' or as_uuid not in stacks):
+ continue
+ yield as_uuid
+ for al_uuid, fbx_alitem in fbx
@@ Diff output truncated at 10240 characters. @@
More information about the Bf-extensions-cvs
mailing list