[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