M3G教程:进阶篇(六)动画
M3G中动画的数据结构如下:
【载入World节点】
通过M3G Viewer查看M3G Converter转换的m3g文件,我们会发现m3g文件中camera并没有在world树下,而是和world树是平级的。因为JSR184要求渲染时所有模型信息必须放到world树下,所以我们要先获取World节点,方法有两种:
①遍历该Object3D数组,并比较每个元素的userid如果正是World节点的useid将该元素取出;
②遍历该Object3D数组,并比较每个元素是不是World类的实例,那么既然World节点是场景的根节点,那么在该Object3D数组中也应该只有一个World类的实例对象。
第一种必需通过查看文件中world节点的userId来获取,第二种比较容易操作,代码如下:
/** Loads our world */
private void loadWorld() {
try {
// Loading the world is very simple. Note that I like to use a
// res-folder that I keep all files in. If you normally just put
// your
// resources in the project root, then load it from the root.
Object3D[] buffer = Loader.load("/axe.m3g");
// Find the world node, best to do it the "safe" way
for (int i = 0; i < buffer.length; i++) {
if (buffer[i] instanceof World) {
world = (World) buffer[i];
break;
}
}
// Clean objects
buffer = null;
} catch (Exception e) {
// ERROR!
System.out.println("Loading error!");
reportException(e);
}
}
在JSR184中,使用AnimationTrack、AnimationController和KeyframeSquence三个类来实现关键帧动画:
AnimationTrack:是一个集合类(Composition),用来将AnimationController、KeyframeSquence和一个特定属性(Target Property)捆绑起来,这种结构也使得同一个AnimationController和KeyframeSquence可以被不同的关键帧动画复用。在每一个Object3D对象中,通过addAnimationTrack和removeAnimationTrack方法,管理一组AnimationTrack序列;当使用animate方法,计算该Object3D对象关键帧动画时,所有在序列中的AnimationTrack都会被依次计算,此外若该Object3D对象有到其他Object3D的引用,则所有引用的Object3D的animate方法也会被依次执行。
AnimationController:通过指定采样点和采样速度,实现从WorldTime到SquenceTime的映射,从而控制动画的播放效果,如:暂停,继续,快进、倒退等。
AnimationController有一个**时间段属性(通过setActiveInterval(int,int)实现),通过指定最小及最大的world time值来控制这个AnimationController在这个时间段是**状态的。如果AnimationController是在某时间是非**状态,则系统在动画计算时直接将其忽略。
另外,AnimationController有一个weight属性,因为系统对对象某属性进行计算时会将与此属性相关的所有动画计算的结果进行合并,而weight则表示这个AnimationController在这个计算中所占的比重,确切的说,对于一个标量属性P的计算是这样的:P = sum [ wi Pi ] 。
AnimationController将传入Object3D.animate()方法的world time值与sequence time值进行映射,sequence time主要用于对关键帧动画数据进行抽样。sequence time会在每次调用对象的animate()方法时计算得出,而不是store it internally,这是为了避免误差的累积及由此产生的一系列状况。这种设计简化了动画系统,使动画系统能够无状态化(相对于状态机),并且效率更高。
从world time到sequence time的映射由AnimationController中的三个常量及传入animate()方法的world time进行计算得到的。公式如下:
其中的参考值t[sref]及t[wref]是通过setPosition()方法指定,而速度speed则是由setSpeed()方法指定(注意改变速度会影响到t[sref]及t[wref]值)。
注意:虽然API及文档中并没有明确的说明这里的时间单位使用毫秒,但我们强烈建议你使用这个默认的时间单位。当然,如果确实需要使用特有有时间单位,动画系统还是支持的。
KeyframeSquence:用来保存所有的关键帧的值,同时指定关键帧循环的方式,以及计算时的插值方法。
无论在立即模式还是保留模式下,都是通过调用Object3D对象的animate()方法实现将对象动画数据的计算。根据使用情形,我们既可以调用World.animate(),也可以调用相应Object3D对象的animate()方法。
【示例】
以下代码实现一个灯光在红色与绿色之间转换,并沿着一条曲线路径进行移动:
初始化时:
Light light = new Light(); // Create a light node
// Load a motion path from a stream, assuming it's the first object there
Object3D[] objects = Loader.load("http://www.ex.com/ex.m3g");
KeyframeSequence motion = (KeyframeSequence) objects[0];
// Create a color keyframe sequence, with keyframes at 0 ms
// and 500 ms, and a total duration of 1000 ms. The animate
// method will throw an exception if it encounters a
// KeyframeSequence whose duration has not been set or whose
// keyframes are out of order. Note that the Loader
// automatically validates any sequences that are loaded from
// a file.
KeyframeSequence blinking = new KeyframeSequence(2, 3, KeyframeSequence.LINEAR);
blinking.setKeyframe(0, 0, new float[] { 1.0f, 0.0f, 0.0f });
blinking.setKeyframe(1, 500, new float[] { 0.0f, 1.0f, 0.0f });
blinking.setDuration(1000);
AnimationTrack blink = new AnimationTrack(blinking, AnimationTrack.COLOR);
AnimationTrack move = new AnimationTrack(motion, AnimationTrack.TRANSLATION);
light.addAnimationTrack(blink);
light.addAnimationTrack(move);
// Create an AnimationController and make it control both the
// blinking and the movement of our light
AnimationController lightAnim = new AnimationController();
blink.setController(lightAnim);
move.setController(lightAnim);
// Start the animation when world time reaches 2 seconds, stop
// at 5 s. There is only one reference point for this
// animation: sequence time must be zero at world time 2000
// ms. The animation will be running at normal speed (1.0, the
// default).
lightAnim.setActiveInterval(2000, 5000);
lightAnim.setPosition(0, 2000);
渲染时:
appTime += 30; // advance time by 30 ms each frame
light.animate(appTime);
// Assume 'myGraphics3D' is the Graphics3D object we draw into.
// In immediate mode, node transforms are ignored, so we get
// our animated transformation into a local Transform object,
// "lightToWorld". As its name implies, the transformation is
// from the Light node's local coordinates to world space.
light.getTransform(lightToWorld);
myGraphics3D.resetLights();
myGraphics3D.addLight(light, lightToWorld);
【示例:播放袋鼠跳动动画】
package study.pogoroo;
import javax.microedition.lcdui.Graphics;
import javax.microedition.lcdui.game.GameCanvas;
import javax.microedition.m3g.AnimationController;
import javax.microedition.m3g.AnimationTrack;
import javax.microedition.m3g.Camera;
import javax.microedition.m3g.Graphics3D;
import javax.microedition.m3g.Group;
import javax.microedition.m3g.KeyframeSequence;
import javax.microedition.m3g.Loader;
import javax.microedition.m3g.Object3D;
import javax.microedition.m3g.World;
public class M3GCanvas extends GameCanvas implements Runnable {
public static final int FPS = 50; //每秒绘制的帧数
// Thread-control
boolean running = false;
boolean done = true;
//UserIDs for objects we use in the scene.
static final int POGOROO_TRANSFORM_ID = 347178853;
static final int ROO_BOUNCE_ID = 418071423;
// Control objects for game play
// control for 'roo - group transform and cameras
private AnimationController animRoo = null;
private Group acRoo = null;
private int animTime = 0;
private int animLength = 0;
private int animLastTime = 0;
int viewport_x;
int viewport_y;
int viewport_width;
int viewport_height;
// Key array
private boolean[] key = new boolean[5];
// Key constants
public static final int FIRE = 0;
public static final int UP = 1;
public static final int DOWN = 2;
public static final int LEFT = 3;
public static final int RIGHT = 4;
// Camera rotation
private float camRot = 0.0f;
private Graphics3D g3d;
private World world;
private boolean runnable=true;
private Thread thread;
private Camera camera;
protected M3GCanvas() {
super(false);
setFullScreenMode(true);
g3d = Graphics3D.getInstance();
//Load our world
loadWorld();
// Load our camera
loadCamera();
getObjects();
setupAspectRatio();
}
/** Loads our camera */
private void loadCamera() {
// BAD!
if (world == null)
return;
// Get the active camera from the world
camera = world.getActiveCamera();
}
/** Loads our world */
private void loadWorld() {
try {
// Loading the world is very simple. Note that I like to use a
// res-folder that I keep all files in. If you normally just put
// your
// resources in the project root, then load it from the root.
Object3D[] buffer = Loader.load("/pogoroo.m3g");
// Find the world node, best to do it the "safe" way
for (int i = 0; i < buffer.length; i++) {
if (buffer[i] instanceof World) {
world = (World) buffer[i];
break;
}
}
// Clean objects
buffer = null;
} catch (Exception e) {
// ERROR!
System.out.println("Loading error!");
}
}
/**
* Make sure that the content is rendered with the correct aspect ratio.
*/
void setupAspectRatio() {
viewport_x = 0;
viewport_y = 0;
viewport_width = getWidth();
viewport_height = getHeight();
float[] params = new float[4];
int type = camera.getProjection(params);
if (type != Camera.GENERIC) {
//calculate window aspect ratio
float waspect = viewport_width / viewport_height;
if (waspect < params[1]) {
float height = viewport_width / params[1];
viewport_height = (int)height;
viewport_y = (getHeight() - viewport_height) / 2;
} else {
float width = viewport_height * params[1];
viewport_width = (int)width;
viewport_x = (getWidth() - viewport_width) / 2;
}
}
}
/**
* getObjects()
* get objects from the scene tree for use in the game AI
*/
public void getObjects() {
try {
acRoo = (Group)world.find(POGOROO_TRANSFORM_ID);
animRoo = (AnimationController)world.find(ROO_BOUNCE_ID);
// get length of animation
AnimationTrack track = acRoo.getAnimationTrack(0);
animLength = 1000; // default length, 1 second
if (track != null) {
KeyframeSequence ks = track.getKeyframeSequence();
if (ks != null) {
animLength = ks.getDuration();
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* animateRoo()
* Makes sure that the hopping animation loops correctly.
*/
private void animateRoo(int worldTime) {
// control the kangaroo animation sequence
if (animLastTime == 0) {
animLastTime = worldTime;
}
animTime += (worldTime - animLastTime);
// initialise animation at end of sequence
if (animTime > animLength) // sequence is ~1000ms
{
animRoo.setActiveInterval(worldTime, worldTime+2000);
//setPosition(float sequenceTime, int worldTime)
//Sets a new playback position, relative to world time, for this animation controller.
animRoo.setPosition(0, worldTime);
animTime = 0;
}
// update storage of last position and time
animLastTime = worldTime;
}
private void moveCamera() {
// Check controls
if (key[LEFT]) {
camRot += 5.0f;
} else if (key[RIGHT]) {
camRot -= 5.0f;
}
// Set the orientation
camera.setOrientation(camRot, 0.0f, 1.0f, 0.0f);
// If the user presses the FIRE key, let's quit
if (key[FIRE])
System.out.println("Fire");
}
protected void process() {
int keys = getKeyStates();
if ((keys & GameCanvas.FIRE_PRESSED) != 0)
key[FIRE] = true;
else
key[FIRE] = false;
if ((keys & GameCanvas.UP_PRESSED) != 0)
key[UP] = true;
else
key[UP] = false;
if ((keys & GameCanvas.DOWN_PRESSED) != 0)
key[DOWN] = true;
else
key[DOWN] = false;
if ((keys & GameCanvas.LEFT_PRESSED) != 0)
key[LEFT] = true;
else
key[LEFT] = false;
if ((keys & GameCanvas.RIGHT_PRESSED) != 0)
key[RIGHT] = true;
else
key[RIGHT] = false;
}
public void run() {
Graphics g = getGraphics();
while (runnable) {
long startTime = System.currentTimeMillis();
//Call the process method (computes keys)
process();
//Move the camera around
moveCamera();
try {
//First bind the graphics object. We use our pre-defined rendering
// hints.
g3d.bindTarget(g);
int st=(int)startTime;
// update the control and game AI
animateRoo(st);
// Update the world to the current time.
world.animate(st);
g3d.setViewport(viewport_x, viewport_y, viewport_width, viewport_height);
//Now, just render the world. Simple as pie!
g3d.render(world);
} finally {
g3d.releaseTarget();
}
flushGraphics();
long endTime = System.currentTimeMillis();
long costTime = endTime - startTime;
if(costTime<1000/FPS)
{
try{
Thread.sleep(1000/FPS-costTime);
}
catch(Exception e){
e.printStackTrace();
}
}
}
System.out.println("Canvas stopped");
}
public void start()
{
thread=new Thread(this);
thread.start();
}
public void stop()
{
this.runnable=false;
try {
thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
其中pogoroo.m3g的节点树关系如下:
效果如下: