`
windybell
  • 浏览: 15515 次
  • 性别: 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
分享到:
评论

相关推荐

    jme3 api(精华chm)

    com.jme3.animation com.jme3.app com.jme3.app.state com.jme3.asset com.jme3.asset.pack com.jme3.asset.plugins com.jme3.audio com.jme3.audio.joal ...jme3tools.preview

    JME3学习文档

    ### JME3游戏开发引擎中文学习指南 #### 引言 JME3,全称jMonkeyEngine3,是一款开源的3D游戏开发引擎,专为Java开发者设计,旨在简化3D游戏和应用程序的开发过程。本文档将详细介绍如何在Netbeans6.x环境下搭建...

    2015jme3指南

    5. **渲染与动画**:讲解如何创建和控制3D模型的动画,包括骨骼动画、粒子系统和物理模拟。 6. **输入处理**:解释如何处理键盘、鼠标和游戏手柄输入,实现玩家交互。 7. **音频支持**:JME3提供了音频播放和处理...

    JME3中文教程(ZBP第一版)

    4. **动画系统**:理解骨骼动画和关键帧动画的工作原理,创建角色的动态行为。 5. **碰撞检测**:学习如何检测游戏中的物体碰撞,以实现交互效果。 6. **粒子系统**:利用粒子系统创建烟雾、火花、水流等特效。 7...

    JME3 中文教程(ZBP翻译第一版)

    《JME3 中文教程(ZBP翻译第一版)》是一个专门为Java Media Engine 3 (JMonkeyEngine 3,简称JME3)爱好者和开发者准备的教程资源。JME3是一个开源的3D游戏开发框架,它基于Java编程语言,为开发者提供了高效、便捷...

    JME中文教程.pdf

    - 骨骼动画:支持角色动画,实现流畅的动作表现。 - 地形系统:生成和编辑地形。 - 灵活易扩展的逻辑模块:允许开发者根据需要添加或修改游戏逻辑。 - 性能优化:包括八叉树、层次细节、硬件加速等多种优化手段...

    jme3游戏demo rise-of-mutants

    《jme3游戏开发:Rise of Mutants》 在IT行业中,游戏开发是一个充满创新和技术挑战的领域,而Java语言并非通常首选的游戏开发工具。然而,随着技术的进步,Java也逐渐进入了游戏开发的舞台,其中JMonkeyEngine...

    jme3材质基础知识

    本文将详细讲解"jme3材质基础知识",主要围绕jMonkeyEngine3(简称jME3)这个强大的开源Java游戏开发引擎。 jMonkeyEngine3是一个基于现代图形技术如OpenGL的3D游戏引擎,它为开发者提供了丰富的工具和库,简化了...

    JME3 JAVADOC

    本文档是JME 的javadoc 文档 JME是一个高性能的3D图形API,采用LWJGL作为底层支持。它的后续版本将支持JOGL。JME和Java 3D具有类似的场景结构,开发者必须以树状方式组织自己的场景。JME有一套很好的优化机制,这...

    JME教程.rar

    3. **用户界面设计**:学习使用MIDP提供的UI组件创建交互式界面,以及如何自定义Canvas实现复杂图形绘制。 4. **数据存储**:了解如何在受限的设备上持久化数据,如使用Record Management System (RMS)。 5. **网络...

    JME的文件格式及支持的文件格式

    **JMonkeyEngine 3 (JME3) 文件格式详解** JMonkeyEngine 3(简称JME3)是一款开源的游戏开发引擎,专为构建3D游戏和应用而设计。它支持多种文件格式,使得开发者能够方便地导入和管理游戏资源。以下是对JME3支持的...

    JME程序设计实例教程

    **JME程序设计实例教程详解** Java Micro Edition(JME),又称为Java 2 Micro Edition,是Java平台的一个子集,主要用于开发和部署在资源有限的设备上的应用程序,如移动电话、PDA、智能家电等嵌入式系统。本教程...

    联想的JME2207P键盘驱动

    标题中的“联想的JME2207P键盘驱动”是指专门为联想品牌的一款键盘型号为JME2207P的设备设计的驱动程序。在计算机硬件系统中,驱动程序是连接操作系统与硬件设备的关键软件,它使得操作系统能够识别并控制特定硬件,...

    JME初级教程(持续跟新)

    Java Media Engine (JME), 也称为jMonkeyEngine 3 (JME3), 是一个开源的游戏开发引擎,专为快速创建3D游戏而设计。它基于Java编程语言,提供了丰富的功能,包括图形渲染、物理模拟、音频处理以及网络通信等。JME3是...

    JME商业游戏进阶二 (地表层的神秘面纱1)源代码

    《JME商业游戏进阶二 (地表层的神秘面纱1)...通过深入研究这个"ditu_example"的源代码,开发者不仅能学习到JME的用法,还能掌握游戏开发中的许多核心概念和技术,这对于提升游戏开发技能和理解游戏架构有着极大的帮助。

    java8看不到源码-JME3-JFX:用于JME的JFXGui桥接器,具有用于常见用例的有用实用程序

    JME3-JFX 用于 JME 的 JFX Gui 桥接器,具有用于常见用例的有用实用程序。 许可证是新的 BSD 许可证(与 JME3 相同) 二进制版本可在以下位置获得: ( ) ( ) 它也可以用作 maven 存储库(请参阅“设置我”按钮)。 ...

    jme8002b蓝牙键盘驱动

    jme8002b蓝牙键盘驱动

    JME Molecular Editor结构式在线编辑器

    JME Molecular Editor结构式在线编辑器

    相关技术\游戏引擎,JME

    3. **脚本语言支持**:JME允许使用内置的Java语言或外部脚本语言(如JavaScript)编写游戏逻辑,增强了游戏的可定制性和灵活性。 4. **多媒体支持**:JME可以处理各种媒体格式,包括音频和视频,为游戏提供丰富的...

Global site tag (gtag.js) - Google Analytics