`
windybell
  • 浏览: 14858 次
  • 性别: Icon_minigender_1
  • 来自: 武汉
社区版块
存档分类
最新评论

JME3骨骼动画研究

 
阅读更多
最近粗略地看了一下com.jme3.animation包下的源码,有一点点理解,不过也不一定对。反正以后也还要接着继续研究,先总结在这里做个备忘录。

一、JME3支持的动画类型

JME3目前只支持骨骼动画和节点动画。虽然它貌似也曾经实现过关键帧动画(有一个PoseTrack类),但是现在废弃了。JME3最重视的还是骨骼动画,看看animation包下面专门定义了Skeleton、Bone、BoneTrack等那么多类就可想而知。

二、骨骼动画相关知识

由于我以前从来没有学习过3D动画知识,所以有一些基本的数学原理不太懂,最近也差了一些资料。
下面几个帖子非常有学习价值:
骨骼蒙皮动画(SkinnedMesh)的原理解析(一)
骨骼蒙皮动画(Skinned Mesh)的原理解析(二)
Skinned Mesh原理解析和一个最简单的实现示例

三、JME3动画接口

JME3把动画的数据、控制进行了抽象,希望能够通过统一的方式来管理各种不同类型的动画。他的动画接口包含这么几个方面:

(1)骨骼数据
骨骼的数据主要包括每块Bone的定义以及Bone之间的继承关系,按树形结构组织,最终形成一个完整的Skeleton。
JME3中的Bone其实应该叫做Joint(关节)。按我的理解,关节之间那根线才叫骨骼,骨骼相连的地方不是关节又是什么呢?不过骨骼动画中最精髓的部分就是关节点的移动,那么重点关注这个节点也对。
每个Bone都有它自己的名称、唯一的父节点以及0~N个子节点。由于Bone其实是关节点,因此它还有自己的Position、Rotation以及Scale。忽略掉Bone类中的绝大部分代码,它的基本属性是这样的:
/**
 * <code>Bone</code> describes a bone in the bone-weight skeletal animation
 * system. A bone contains a name and an index, as well as relevant
 * transformation data.
 *
 * @author Kirill Vainer
 */
public final class Bone implements Savable {

    private String name;
    private Bone parent;
    private final ArrayList<Bone> children = new ArrayList<Bone>();

    /**
     * Initial transform is the local bind transform of this bone.
     * PARENT SPACE -> BONE SPACE
     */
    private Vector3f initialPos;
    private Quaternion initialRot;
    private Vector3f initialScale;
    /**
     * The inverse world bind transform.
     * BONE SPACE -> MODEL SPACE
     */
    private Vector3f worldBindInversePos;
    private Quaternion worldBindInverseRot;
    private Vector3f worldBindInverseScale;
    /**
     * The local animated transform combined with the local bind transform and parent world transform
     */
    private Vector3f localPos = new Vector3f();
    private Quaternion localRot = new Quaternion();
    private Vector3f localScale = new Vector3f(1.0f, 1.0f, 1.0f);
    /**
     * MODEL SPACE -> BONE SPACE (in animated state)
     */
    private Vector3f worldPos = new Vector3f();
    private Quaternion worldRot = new Quaternion();
    private Vector3f worldScale = new Vector3f();
    
    // Used for getCombinedTransform
    private Transform tmpTransform = new Transform();

    /**
     * Creates a new bone with the given name.
     * 
     * @param name Name to give to this bone
     */
    public Bone(String name) {
        if (name == null)
            throw new IllegalArgumentException("Name cannot be null");
        
        this.name = name;

        initialPos = new Vector3f();
        initialRot = new Quaternion();
        initialScale = new Vector3f(1, 1, 1);

        worldBindInversePos = new Vector3f();
        worldBindInverseRot = new Quaternion();
        worldBindInverseScale = new Vector3f();
    }

    /**
     * Returns parent bone of this bone, or null if it is a root bone.
     * @return The parent bone of this bone, or null if it is a root bone.
     */
    public Bone getParent() {
        return parent;
    }

    /**
     * Add a new child to this bone. Shouldn't be used by user code.
     * Can corrupt skeleton.
     * 
     * @param bone The bone to add
     */
    public void addChild(Bone bone) {
        children.add(bone);
        bone.parent = this;
    }

    /**
     * Sets local bind transform for bone.
     * Call setBindingPose() after all of the skeleton bones' bind transforms are set to save them.
     */
    public void setBindTransforms(Vector3f translation, Quaternion rotation, Vector3f scale) {
        initialPos.set(translation);
        initialRot.set(rotation);
        //ogre.xml can have null scale values breaking this if the check is removed
        if (scale != null) {
            initialScale.set(scale);
        }

        localPos.set(translation);
        localRot.set(rotation);
        if (scale != null) {
            localScale.set(scale);
        }
    }
}

我们很容易就可以new一个骨骼,然后通过addChild(Bone)方法来给他添加子节点,并通过setBindTransforms()方法来设置骨骼的初始位置。
除此之外,最重要的就是骨骼中的父节点是怎么计算子节点的空间变换了。Bone提供了update()方法来更新根节点以及所有子节点的空间变换。
顺带一提,update中使用的是先序遍历,保证先更新父节点,再更新子节点。
    /**
     * Updates the world transforms for this bone, and, possibly the attach node
     * if not null.
     * <p>
     * The world transform of this bone is computed by combining the parent's
     * world transform with this bones' local transform.
     */
    public final void updateWorldVectors() {
        if (currentWeightSum == 1f) {
            currentWeightSum = -1;
        } else if (currentWeightSum != -1f) {
            // Apply the weight to the local transform
            if (currentWeightSum == 0) {
                localRot.set(initialRot);
                localPos.set(initialPos);
                localScale.set(initialScale);
            } else {
                float invWeightSum = 1f - currentWeightSum;
                localRot.nlerp(initialRot, invWeightSum);
                localPos.interpolate(initialPos, invWeightSum);
                localScale.interpolate(initialScale, invWeightSum);
            }
            
            // Future invocations of transform blend will start over.
            currentWeightSum = -1;
        }
        
        if (parent != null) {
            //rotation
            parent.worldRot.mult(localRot, worldRot);

            //scale
            //For scale parent scale is not taken into account!
            // worldScale.set(localScale);
            parent.worldScale.mult(localScale, worldScale);

            //translation
            //scale and rotation of parent affect bone position            
            parent.worldRot.mult(localPos, worldPos);
            worldPos.multLocal(parent.worldScale);
            worldPos.addLocal(parent.worldPos);
        } else {
            worldRot.set(localRot);
            worldPos.set(localPos);
            worldScale.set(localScale);
        }

        if (attachNode != null) {
            attachNode.setLocalTranslation(worldPos);
            attachNode.setLocalRotation(worldRot);
            attachNode.setLocalScale(worldScale);
        }
    }

    /**
     * Updates world transforms for this bone and it's children.
     */
    final void update() {
        this.updateWorldVectors();

        for (int i = children.size() - 1; i >= 0; i--) {
            children.get(i).update();
        }
    }

既然主要的工作都在Bone里面做了,那么还要Skeleton干什么呢?Skeleton主要用于管理所有的Bone,除此之外别无它用。
/**
 * <code>Skeleton</code> is a convenience class for managing a bone hierarchy.
 * Skeleton updates the world transforms to reflect the current local
 * animated matrixes.
 * 
 * @author Kirill Vainer
 */
public final class Skeleton implements Savable {

    private Bone[] rootBones;
    private Bone[] boneList;
    
    /**
     * Contains the skinning matrices, multiplying it by a vertex effected by a bone
     * will cause it to go to the animated position.
     */
    private transient Matrix4f[] skinningMatrixes;

    /**
     * Creates a skeleton from a bone list. 
     * The root bones are found automatically.
     * <p>
     * Note that using this constructor will cause the bones in the list
     * to have their bind pose recomputed based on their local transforms.
     * 
     * @param boneList The list of bones to manage by this Skeleton
     */
    public Skeleton(Bone[] boneList) {
        this.boneList = boneList;

        List<Bone> rootBoneList = new ArrayList<Bone>();
        for (int i = boneList.length - 1; i >= 0; i--) {
            Bone b = boneList[i];
            if (b.getParent() == null) {
                rootBoneList.add(b);
            }
        }
        rootBones = rootBoneList.toArray(new Bone[rootBoneList.size()]);

        createSkinningMatrices();

        for (int i = rootBones.length - 1; i >= 0; i--) {
            Bone rootBone = rootBones[i];
            rootBone.update();
            rootBone.setBindingPose();
        }
    }

Skeleton中也提供了不少用于计算空间变换的方法,主要都是按调用Bone中的方法,比如下面这个:
    /**
     * Updates world transforms for all bones in this skeleton.
     * Typically called after setting local animation transforms.
     */
    public void updateWorldVectors() {
        for (int i = rootBones.length - 1; i >= 0; i--) {
            rootBones[i].update();
        }
    }


(2)动画数据
骨骼数据定义了骨架的形状以及空间变换算法。那么每一帧动画如何变换,就属于动画数据要考虑的问题了。

JME3为动画数据提供了2个接口:Track、Animation。
Track负责保存N个动画帧,Animation则由多个Track来组成。例如:
有一个飞机起飞的动画,这个动画有2个Track:第1个Track保存了飞机收起起落架的动作;第2个Track保存了飞机发动机启动的动画。每个Track都保存了一系列的关键帧数据。

Track只是一个接口,它只定义了3个最基本的方法,连动画关键帧的存储数据结构都没有定义。Track的实现类需要自己去实现。
public interface Track extends Savable, Cloneable {

    /**
     * Sets the time of the animation.
     * 
     * Internally, the track will retrieve objects from the control
     * and modify them according to the properties of the channel and the
     * given parameters.
     * 
     * @param time The time in the animation
     * @param weight The weight from 0 to 1 on how much to apply the track 
     * @param control The control which the track should effect
     * @param channel The channel which the track should effect
     */
    public void setTime(float time, float weight, AnimControl control, AnimChannel channel, TempVars vars);

    /**
     * @return the length of the track
     */
    public float getLength();

    /**
     * This method creates a clone of the current object.
     * @return a clone of the current object
     */
    public Track clone();
}

这3个方法中,最重要的就是setTime方法。它用于计算在指定时刻的关键帧数据,其实现类一般都会使用插值法来进行计算。
顺便说一下,JME3中的动画时间是以“秒”为单位的,getLength()方法要返回动画的实际时长,setTime中的time参数也要是实际时间。比如这个Track的总时长为2.5秒,现在想播放第0.8秒的动画,那么setTime中的time参数就是0.8。

Track最重要的实现类就是BoneTrack和SpatialTrack,当然我们也可以根据自己的需要来实现Track接口。
/**
 * Contains a list of transforms and times for each keyframe.
 * 
 * @author Kirill Vainer
 */
public final class BoneTrack implements Track {

    /**
     * Bone index in the skeleton which this track effects.
     */
    private int targetBoneIndex;
    
    /**
     * Transforms and times for track.
     */
    private CompactVector3Array translations;
    private CompactQuaternionArray rotations;
    private CompactVector3Array scales;
    private float[] times;
    
    /**
     * Creates a bone track for the given bone index
     * @param targetBoneIndex the bone index
     * @param times a float array with the time of each frame
     * @param translations the translation of the bone for each frame
     * @param rotations the rotation of the bone for each frame
     */
    public BoneTrack(int targetBoneIndex, float[] times, Vector3f[] translations, Quaternion[] rotations) {
        this.targetBoneIndex = targetBoneIndex;
        this.setKeyframes(times, translations, rotations);
    }

    /**
     * Creates a bone track for the given bone index
     * @param targetBoneIndex the bone index
     * @param times a float array with the time of each frame
     * @param translations the translation of the bone for each frame
     * @param rotations the rotation of the bone for each frame
     * @param scales the scale of the bone for each frame
     */
    public BoneTrack(int targetBoneIndex, float[] times, Vector3f[] translations, Quaternion[] rotations, Vector3f[] scales) {
    	this.targetBoneIndex = targetBoneIndex;
        this.setKeyframes(times, translations, rotations, scales);
    }

    /**
     * Set the translations and rotations for this bone track
     * @param times a float array with the time of each frame
     * @param translations the translation of the bone for each frame
     * @param rotations the rotation of the bone for each frame
     */
    public void setKeyframes(float[] times, Vector3f[] translations, Quaternion[] rotations) {
        if (times.length == 0) {
            throw new RuntimeException("BoneTrack with no keyframes!");
        }

        assert times.length == translations.length && times.length == rotations.length;

        this.times = times;
        this.translations = new CompactVector3Array();
        this.translations.add(translations);
        this.translations.freeze();
        this.rotations = new CompactQuaternionArray();
        this.rotations.add(rotations);
        this.rotations.freeze();
    }

    /**
     * Set the translations, rotations and scales for this bone track
     * @param times a float array with the time of each frame
     * @param translations the translation of the bone for each frame
     * @param rotations the rotation of the bone for each frame
     * @param scales the scale of the bone for each frame
     */
    public void setKeyframes(float[] times, Vector3f[] translations, Quaternion[] rotations, Vector3f[] scales) {
        this.setKeyframes(times, translations, rotations);
        assert times.length == scales.length;
        if (scales != null) {
            this.scales = new CompactVector3Array();
            this.scales.add(scales);
            this.scales.freeze();
        }
    }

    /**
     * 
     * Modify the bone which this track modifies in the skeleton to contain
     * the correct animation transforms for a given time.
     * The transforms can be interpolated in some method from the keyframes.
     *
     * @param time the current time of the animation
     * @param weight the weight of the animation
     * @param control
     * @param channel
     * @param vars
     */
    public void setTime(float time, float weight, AnimControl control, AnimChannel channel, TempVars vars) {
        BitSet affectedBones = channel.getAffectedBones();
        if (affectedBones != null && !affectedBones.get(targetBoneIndex)) {
            return;
        }
        
        Bone target = control.getSkeleton().getBone(targetBoneIndex);

        Vector3f tempV = vars.vect1;
        Vector3f tempS = vars.vect2;
        Quaternion tempQ = vars.quat1;
        Vector3f tempV2 = vars.vect3;
        Vector3f tempS2 = vars.vect4;
        Quaternion tempQ2 = vars.quat2;
        
        int lastFrame = times.length - 1;
        if (time < 0 || lastFrame == 0) {
            rotations.get(0, tempQ);
            translations.get(0, tempV);
            if (scales != null) {
                scales.get(0, tempS);
            }
        } else if (time >= times[lastFrame]) {
            rotations.get(lastFrame, tempQ);
            translations.get(lastFrame, tempV);
            if (scales != null) {
                scales.get(lastFrame, tempS);
            }
        } else {
            int startFrame = 0;
            int endFrame = 1;
            // use lastFrame so we never overflow the array
            int i;
            for (i = 0; i < lastFrame && times[i] < time; i++) {
                startFrame = i;
                endFrame = i + 1;
            }

            float blend = (time - times[startFrame])
                    / (times[endFrame] - times[startFrame]);

            rotations.get(startFrame, tempQ);
            translations.get(startFrame, tempV);
            if (scales != null) {
                scales.get(startFrame, tempS);
            }
            rotations.get(endFrame, tempQ2);
            translations.get(endFrame, tempV2);
            if (scales != null) {
                scales.get(endFrame, tempS2);
            }
            tempQ.nlerp(tempQ2, blend);
            tempV.interpolate(tempV2, blend);
            tempS.interpolate(tempS2, blend);
        }

        target.blendAnimTransforms(tempV, tempQ, scales != null ? tempS : 
    }
    
    /**
     * @return the length of the track
     */
    public float getLength() {
        return times == null ? 0 : times[times.length - 1] - times[0];
    }


Animation就单纯了,主要用于管理Track。每个Animation可以有一个名字,比如"Walk"、"Idle"、"Attack"之类的,这样我们要播放动画的时候就很容易控制了。
Animation也有一个setTime方法,其实就是在调用所有Track的setTime。
    /**
     * This method sets the current time of the animation.
     * This method behaves differently for every known track type.
     * Override this method if you have your own type of track.
     * 
     * @param time the time of the animation
     * @param blendAmount the blend amount factor
     * @param control the animation control
     * @param channel the animation channel
     */
    void setTime(float time, float blendAmount, AnimControl control, AnimChannel channel, TempVars vars) {
        if (tracks == null) {
            return;
        }

        for (Track track : tracks) {
            track.setTime(time, blendAmount, control, channel, vars);
        }
    }


(3)动画控制
JME3通过AnimControl和AnimChannel来控制动画的播放,加载一个带有骨骼动画数据的模型后,可以通过这俩接口来播放动画。具体的内容在wiki上有介绍,这里就不再赘述了。
http://wiki.jmonkeyengine.org/doku.php/jme3:beginner:hello_animation
分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics