最近粗略地看了一下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类中的绝大部分代码,它的基本属性是这样的:
我们很容易就可以new一个骨骼,然后通过addChild(Bone)方法来给他添加子节点,并通过setBindTransforms()方法来设置骨骼的初始位置。
除此之外,最重要的就是骨骼中的父节点是怎么计算子节点的空间变换了。Bone提供了update()方法来更新根节点以及所有子节点的空间变换。
顺带一提,update中使用的是先序遍历,保证先更新父节点,再更新子节点。
既然主要的工作都在Bone里面做了,那么还要Skeleton干什么呢?Skeleton主要用于管理所有的Bone,除此之外别无它用。
Skeleton中也提供了不少用于计算空间变换的方法,主要都是按调用Bone中的方法,比如下面这个:
(2)动画数据
骨骼数据定义了骨架的形状以及空间变换算法。那么每一帧动画如何变换,就属于动画数据要考虑的问题了。
JME3为动画数据提供了2个接口:Track、Animation。
Track负责保存N个动画帧,Animation则由多个Track来组成。例如:
有一个飞机起飞的动画,这个动画有2个Track:第1个Track保存了飞机收起起落架的动作;第2个Track保存了飞机发动机启动的动画。每个Track都保存了一系列的关键帧数据。
Track只是一个接口,它只定义了3个最基本的方法,连动画关键帧的存储数据结构都没有定义。Track的实现类需要自己去实现。
这3个方法中,最重要的就是setTime方法。它用于计算在指定时刻的关键帧数据,其实现类一般都会使用插值法来进行计算。
顺便说一下,JME3中的动画时间是以“秒”为单位的,getLength()方法要返回动画的实际时长,setTime中的time参数也要是实际时间。比如这个Track的总时长为2.5秒,现在想播放第0.8秒的动画,那么setTime中的time参数就是0.8。
Track最重要的实现类就是BoneTrack和SpatialTrack,当然我们也可以根据自己的需要来实现Track接口。
Animation就单纯了,主要用于管理Track。每个Animation可以有一个名字,比如"Walk"、"Idle"、"Attack"之类的,这样我们要播放动画的时候就很容易控制了。
Animation也有一个setTime方法,其实就是在调用所有Track的setTime。
(3)动画控制
JME3通过AnimControl和AnimChannel来控制动画的播放,加载一个带有骨骼动画数据的模型后,可以通过这俩接口来播放动画。具体的内容在wiki上有介绍,这里就不再赘述了。
http://wiki.jmonkeyengine.org/doku.php/jme3:beginner:hello_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
发表评论
-
jME中文网站
2015-12-10 01:58 1026jME中文论坛:http://bbs.jmecn.net jM ... -
最近的成果
2015-09-09 18:44 743游戏一共有8个State: Intro、Loading、Use ... -
运行时切换全屏/窗口大小
2015-09-08 16:32 1535JME3没有直接提供改变屏幕大小的接口,我们可以通过setti ... -
JME3资源管理之四:心得和小节
2015-08-28 19:04 1340系列目录: JME3资源管 ... -
JME3资源管理之三:资源加载流程
2015-08-28 17:41 817系列目录: JME3资源管理之一:核心组件介绍 JME3资源管 ... -
JME3资源管理之二:AssetLoader和AssetLocator
2015-08-28 17:39 835系列目录: JME3资源管理之一:核心组件介绍 JME3资源管 ... -
JME3资源管理之一:核心组件介绍
2015-08-28 17:01 1223系列目录: JME3资源管 ... -
3维空间曲线
2015-08-13 19:56 997JME3提供了Curve类,用于 ... -
昼夜变化的天空、飘动的云以及动态的阳光
2015-07-25 14:19 1091神说要有光 要分白天和黑夜 要有日月星辰,昼夜交替出现 要有 ... -
昼夜系统-游戏中的时间
2015-07-23 22:11 662我希望在游戏中能够有昼夜变化,四季变化,这样就意味着游戏中将会 ... -
JME3播放背景音乐
2015-07-22 10:52 732JMonkeyEngine3中提供了Audio ...
相关推荐
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,全称jMonkeyEngine3,是一款开源的3D游戏开发引擎,专为Java开发者设计,旨在简化3D游戏和应用程序的开发过程。本文档将详细介绍如何在Netbeans6.x环境下搭建...
5. **渲染与动画**:讲解如何创建和控制3D模型的动画,包括骨骼动画、粒子系统和物理模拟。 6. **输入处理**:解释如何处理键盘、鼠标和游戏手柄输入,实现玩家交互。 7. **音频支持**:JME3提供了音频播放和处理...
4. **动画系统**:理解骨骼动画和关键帧动画的工作原理,创建角色的动态行为。 5. **碰撞检测**:学习如何检测游戏中的物体碰撞,以实现交互效果。 6. **粒子系统**:利用粒子系统创建烟雾、火花、水流等特效。 7...
《JME3 中文教程(ZBP翻译第一版)》是一个专门为Java Media Engine 3 (JMonkeyEngine 3,简称JME3)爱好者和开发者准备的教程资源。JME3是一个开源的3D游戏开发框架,它基于Java编程语言,为开发者提供了高效、便捷...
- 骨骼动画:支持角色动画,实现流畅的动作表现。 - 地形系统:生成和编辑地形。 - 灵活易扩展的逻辑模块:允许开发者根据需要添加或修改游戏逻辑。 - 性能优化:包括八叉树、层次细节、硬件加速等多种优化手段...
《jme3游戏开发:Rise of Mutants》 在IT行业中,游戏开发是一个充满创新和技术挑战的领域,而Java语言并非通常首选的游戏开发工具。然而,随着技术的进步,Java也逐渐进入了游戏开发的舞台,其中JMonkeyEngine...
本文将详细讲解"jme3材质基础知识",主要围绕jMonkeyEngine3(简称jME3)这个强大的开源Java游戏开发引擎。 jMonkeyEngine3是一个基于现代图形技术如OpenGL的3D游戏引擎,它为开发者提供了丰富的工具和库,简化了...
本文档是JME 的javadoc 文档 JME是一个高性能的3D图形API,采用LWJGL作为底层支持。它的后续版本将支持JOGL。JME和Java 3D具有类似的场景结构,开发者必须以树状方式组织自己的场景。JME有一套很好的优化机制,这...
3. **用户界面设计**:学习使用MIDP提供的UI组件创建交互式界面,以及如何自定义Canvas实现复杂图形绘制。 4. **数据存储**:了解如何在受限的设备上持久化数据,如使用Record Management System (RMS)。 5. **网络...
**JMonkeyEngine 3 (JME3) 文件格式详解** JMonkeyEngine 3(简称JME3)是一款开源的游戏开发引擎,专为构建3D游戏和应用而设计。它支持多种文件格式,使得开发者能够方便地导入和管理游戏资源。以下是对JME3支持的...
**JME程序设计实例教程详解** Java Micro Edition(JME),又称为Java 2 Micro Edition,是Java平台的一个子集,主要用于开发和部署在资源有限的设备上的应用程序,如移动电话、PDA、智能家电等嵌入式系统。本教程...
标题中的“联想的JME2207P键盘驱动”是指专门为联想品牌的一款键盘型号为JME2207P的设备设计的驱动程序。在计算机硬件系统中,驱动程序是连接操作系统与硬件设备的关键软件,它使得操作系统能够识别并控制特定硬件,...
Java Media Engine (JME), 也称为jMonkeyEngine 3 (JME3), 是一个开源的游戏开发引擎,专为快速创建3D游戏而设计。它基于Java编程语言,提供了丰富的功能,包括图形渲染、物理模拟、音频处理以及网络通信等。JME3是...
《JME商业游戏进阶二 (地表层的神秘面纱1)...通过深入研究这个"ditu_example"的源代码,开发者不仅能学习到JME的用法,还能掌握游戏开发中的许多核心概念和技术,这对于提升游戏开发技能和理解游戏架构有着极大的帮助。
JME3-JFX 用于 JME 的 JFX Gui 桥接器,具有用于常见用例的有用实用程序。 许可证是新的 BSD 许可证(与 JME3 相同) 二进制版本可在以下位置获得: ( ) ( ) 它也可以用作 maven 存储库(请参阅“设置我”按钮)。 ...
jme8002b蓝牙键盘驱动
JME Molecular Editor结构式在线编辑器
3. **脚本语言支持**:JME允许使用内置的Java语言或外部脚本语言(如JavaScript)编写游戏逻辑,增强了游戏的可定制性和灵活性。 4. **多媒体支持**:JME可以处理各种媒体格式,包括音频和视频,为游戏提供丰富的...