【IT168 技术文章】在二维(2D)动画中,通常需要按预定义的模式(有时称为 控制路径)在一个 2D 区域内移动对象。这种动画需要解决两个问题:
如何指定对象要遵循的控制路径。
如何沿着所选的路径移动对象。
在本文中我们将为您展示如何用无损图像、Swing 技术和基于 Java 的动画引擎解决这些问题。我们将首先绘制所需要的动画对象轨道,然后用动画引擎驱动对象沿着定义的控制路径运动。
可以容易创建和处理无损图像,而且可以根据需要对使用它们的技术进行细致的调节。我们将利用一个示例动画序列,介绍如何用不同的颜色集创建复杂的运动序列。我们还将介绍如何处理图像以提取出所需要的控制路径、将控制路径与背景图像分层、为动画序列创建对象(Swing GUI 组件),并驱动这些对象沿着定义的控制路径运动以完成动画过程。
什么是无损?
无损图像(lossless image)是永久保留了所有图像像素的图像。这种图像必须能够存储为或者恢复成与原件完全一样的复制品。
可以使用不同的应用程序创始无损图像,包括 Microsoft Paint、Jasc Paint Shop Pro 和一些定制的应用程序。可以将这些图像存储到文件中,也可以只在内存创建它们。图像必须是无压缩的,或者是使用无损压缩算法如 zip 压缩进行压缩的。典型的无损图像格式包括 Microsoft 的 Bitmap (BMP) 和 Portable Network Graphics (PNG) 格式。有损压缩算法,比如通常用于 GIF(Graphics Interchange Format)和 JPEG(Joint Photographic Experts Group) 文件的压缩算法,不适用于本文所描述的动画技术。
完全是控制问题
控制路径 的最一般化的定义是通过任意 n 维空间时,在特定位置和时间所要采取的行为。我们将控制路径定义为一个或者多个对象穿过一个 2D 空间时所采取的路径。通过将对象的位置映射到该位置的行为来表示控制路径。然后程序遍历所定义的对象、在映射中查找对象在该位置上的行为、并让对象执行所指定的行动。对所有控制路径—— 除去最简单的—— 在代码中建立这样一个映射都是耗费时间和容易出错的,因此使用一个绘图程序更合适。
控制路径可以是 不随时间变化的(time invariant),在这种情况下是静态的,也可以是 随时间变化的(time variable),在这种情况下是动态的。如果无损图像包含在一个图像文件中,那么它就是不随时间变化的,或者说是静态的。如果无损图像是包含在 RAM 中并直接使用的,那么它就是随时间变化的,或者说是动态的。在本文中我们讨论的是静态控制路径。使用正确的编辑程序,可以更容易地生成静态图像,尽管所定义的行为类型也会在某种程度上影响这个过程。
让我们度过一个狂热的夜晚!
学习动画的一个好方法是自己动手实践。我们将在本文其余部分使用一个动画的例子来阐明所讨论的概念。我们的例子是一个动画的火灾逃生序列,我们将生成控制路径以表示几个人物的逃生路径。我们将使用图 1 中的部分平面图作为背景图像。可以在 图 6中看到完整的背景图像。
图 1. 背景图像的一部分

我们可以用一个数值数组生成控制映射。用一个图像代替数组(如图 2 所示)使我们可以用颜色值来表示每一个位置的行为。每一种颜色值的大小(颜色位数)取决于图像格式。图 2 展示了火灾逃生序列的控制路径。
图 2. 部分控制路径

为了看到控制路径图像与动画背景的对应关系,我们可以将控制图像覆盖到背景图像上,如图 3 所示。
图 3. 使用分层透明度结合起来的图像

让世界充满色彩
生成一个图像后,就可容易地将它转换为所需要的映射。我们只要遍历图像的颜色并为每种颜色值指定一种行为。比如我们可以使用白色 ——它通常是全为 1 的值——表示无映射或者默认行为。黑色——通常是零值——可以用于表示自定义的行为。如果是根据我们的图像映射的,当对象遇到一个有同样行为(即同一种颜色,如黑色)的位置时,它就会继续沿着由这个位置定义的方向运行。如果这个位置不是同样的行为,那么它就会找到与它有同样行为的相邻位置,但是不会走回头路。
我们可以用不同的颜色表示其他行为。没有定义的颜色值将会被忽略。因此,背景中的像素(如 图 3中的浅灰色)会被忽略。
清单 1 显示如何完成映射。先针对特定颜色值扫描该图像,然后用每一个颜色像素的位置定义该位置在控制状态映射中的控制状态。在逃生的例子中,用不同的 STATE_xxx 常量定义了六种行为。
清单 1. 处理控制路径图像
2 :
3 public final static int STATE_UNKNOWN = -1;
4 public final static int STATE_NONE = 0;
5 public final static int STATE_HALLWAY = 1;
6 public final static int STATE_INTERSECTION = 2;
7 public final static int STATE_HINT = 3;
8 public final static int STATE_START = 4;
9 public final static int STATE_EXIT = 5;
10 :
11 /** Process the control image */
12 void processControl(Image img, int x, int y, int w, int h)
13 {
14 int pmap[] = new int[w * h];
15 PixelGrabber pg = new PixelGrabber(img, x, y, w, h, pmap, 0, w);
16 try {
17 pg.grabPixels();
18 if ((pg.getStatus() & ImageObserver.ABORT) != 0) {
19 System.err.println("image fetch error");
20 }
21 else {
22 Integer none = new Integer(STATE_NONE);
23 Integer hall = new Integer(STATE_HALLWAY);
24 Integer start = new Integer(STATE_START);
25 Integer exit = new Integer(STATE_EXIT);
26 Integer hint = new Integer(STATE_HINT);
27 Integer inter = new Integer(STATE_INTERSECTION);
28 // for each position
29 for (int i = 0; i < pmap.length; i++) {
30 int red = (pmap[i] >> 16) & 0xff;
31 int green = (pmap[i] >> 8) & 0xff;
32 int blue = (pmap[i] ) & 0xff;
33 if (red == 255 && green == 255 && blue == 255)
34 ; // don't bother to add NONE to map
35 else if (red == 0 && green == 0 && blue == 0)
36 map.put(new Integer(i), hall);
37 else if (red == 0 && green == 255 && blue == 0)
38 map.put(new Integer(i), start);
39 else if (red == 200 && green == 0 && blue == 0)
40 map.put(new Integer(i), exit);
41 else if (red == 255 && green == 0 && blue == 0)
42 map.put(new Integer(i), hint);
43 else if (red == 0 && green == 0 && blue == 255)
44 map.put(new Integer(i), inter);
45 }
46 }
47 }
48 catch (InterruptedException e) {
49 System.err.println("image processing interrupted");
50 }
51 }
52