最近粗略地看了一下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