|
|
针对Java移动设备的3D图形第1部分:M3G 的立即模式
作者:claus 文章来源:developworks 点击数:95 更新时间:2005-11-29
本文是此系列两部分中的第 1 部分,介绍了 Mobile 3D Graphics API (JSR 184) 的有关内容。作者将带领您进入 Java 移动设备的 3D 编程世界,并展示了处理光线、摄像机和材质的方法。
在移动设备上玩游戏是一项有趣的消遣。迄今为止,硬件性能已足以满足经典游戏概念的需求,这些游戏确实令人着迷,但图像非常简单。今天,人们开发出大量二维平面动作游戏,其图像更为丰富,弥补了俄罗斯方块和吃豆游戏的单调感。下一步就是迈进 3D 图像的世界。Sony PlayStation Portable 将移动设备能够实现的图像能力展现在世人面前。虽然普通的移动电话在技术上远不及这种特制的游戏机,但由此可以看出整个市场的发展方向。Mobile 3D Graphics API(简称为 M3G)是在 JSR 184(Java 规范请求,Java Specification Request)中定义的,JSR 184 是一项工业成就,用于为支持 Java 程序设计的移动设备提供标准 3D API。
M3G API 大致可分为两部分:快速模式和保留模式。在快速模式下,您渲染的是单独的 3D 对象;而在保留模式下,您需要定义并显示整个 3D 对象世界,包括其外观信息在内。可以将快速模式视为低级的 3D 功能实现方式,保留模式显示 3D 图像的方式更为抽象,令人感觉也更要舒服一些。本文将对快速模式 API 进行介绍。而本系列的第 2 部分将介绍保留模式的使用方法。
M3G 以外的技术
M3G 不是孤独的。HI Corporation 开发的 Mascot Capsule API 在日本国内非常流行,日本三大运营商均以不同形式选用了这项技术,在其他国家也广受欢迎。例如,Sony Ericsson 为手机增加了 M3G 和 HI Corporation 的特定 API。根据应用程序开发人员在 Sony Ericsson 网站上发布的报告,Mascot Capsule 是一种稳定且快速的 3D环境。
JSR 239 也就是 Java Bindings for OpenGL ES,它面向的设备与 M3G 相同。OpenGL ES 是人们熟知的 OpenGL 3D 库的子集,事实上已成为约束设备上本地 3D 实现的标准。JSR 239 定义了一个几乎与 OpenGL ES 的 C 接口相同的 Java API,使现有 OpenGL 内容的移植更为容易。到 2005 年 9 月为止,JSR 239 还依然处于早期的蓝图设计状态。关于它是否会给手机带来深刻的影响,我只能靠推测。尽管 OpenGL ES 与其 API 不兼容,但却对 M3G 的定义产生了一定影响:JSR 184 专家组确保了 MSG 在 OpenGL ES 之上的有效实现。如果您了解 OpenGL,那么就会在 M3G 中看到许多似曾相识的属性。
尽管还有其他可选技术,但 M3G 获得了所有主要电话制造商和运营商的支持。之前我提到过,游戏是最大的吸引力所在,但 M3G 是一种通用 API,您可以将其用于创建各种 3D 内容。未来的几年中,手机将广泛采用 3D API。
您的第一个 3D 对象
在第一个示例中,我们将创建一个如图 1 所示的立方体。
图 1. 示例立方体: a) 有顶点索引的正面图,b) 切割面的侧面视图(正面,侧面)
这个立方体存在于 M3G 定义的右手坐标系中。举起右手、伸出拇指、食指和中指,保持其中任一手指与其他两指均成直角,那么拇指就表示 x 轴、食指表示 y 轴,中指表示 z 轴。试着将拇指和食指摆成图 1a 中的样子,那么您的中指必然指向自己。我在这里使用了 8 个顶点(立方体的顶点)并使立方体的中心与坐标系的原点相重合。
从图 1 中可以看到,拍摄 3D 场景的摄像机朝向 z 轴的负轴方向,正对立方体。摄像机的位置和属性定义了随后将在屏幕上显示的东西。图 1b 展示了同一场景的侧面视图,这样您就可以更容易地看清摄像机究竟能看到 3D 世界中的哪些地方。限制因素之一就是观察角度,这与使用照相机的情况类似:长焦镜头的视野比广角镜头的观察角度要窄得多。因此观察角度决定了您的视野。与真实世界中的情况不同,3D 计算给我们增加了两个视图边界:近切割面和远切割面。观察角度和切割面共同定义了视域。视域中的一切都是可见的,而超出视域范围的一切均不可见。
在清单 1 中,您可以看到 VerticesSample 类,实现了上面提到的所有内容。
清单 1. 显示立方体的示例,第 1 部分:类成员
package m3gsamples1;
import javax.microedition.lcdui.*;
import javax.microedition.m3g.*;
/**
* Sample displaying a cube defined by eight vertices, which are connected
* by triangles.
*
* @author Claus Hoefele
*/
public class VerticesSample extends Canvas implements Sample
{
/** The cube's vertex positions (x, y, z). */
private static final byte[] VERTEX_POSITIONS = {
-1, -1, 1, 1, -1, 1, -1, 1, 1, 1, 1, 1,
-1, -1, -1, 1, -1, -1, -1, 1, -1, 1, 1, -1
};
/** Indices that define how to connect the vertices to build
* triangles. */
private static int[] TRIANGLE_INDICES = {
0, 1, 2, 3, 7, 1, 5, 4, 7, 6, 2, 4, 0, 1
};
/** The cube's vertex data. */
private VertexBuffer _cubeVertexData;
/** The cube's triangles defined as triangle strips. */
private TriangleStripArray _cubeTriangles;
/** Graphics singleton used for rendering. */
private Graphics3D _graphics3d;
VerticesSample 继承自 Canvas,应该能够直接绘制到屏幕。并且还实现了 Sample,定义它的目的是协助组织本文中的其他源代码示例。VERTEX_POSITIONS 以同样的顺序定义了与图 1a 相同的 8 个顶点。例如,顶点 0 定义为坐标(-1, -1, 1)。由于我将立方体的中心点放在坐标系原点位置处,因此立方体的各边长应为 2 个单位。随后,摄像机的位置和视角可定义一个单位在屏幕上所占的像素数。
仅有顶点位置还不够,您还必须描述出想要建立的几何图形。只能像逐点描图法那样,将顶点用直线连接起来,最终得到所需图形。但 M3G 也带来了一个约束:必须用三角形建立几何图形。任何多边形都可定义为一组三角形的集合,因此三角形在 3D 实现中应用十分广泛。三角形是基本的绘图操作,在此基础上可建立更为抽象的操作。
不幸的是,如果只能使用三角形描述立方体,就需要 6 条边 * 2 个三角形 * 3 个顶点 = 36 个顶点。这么多重复的顶点显然浪费了大量内存。为节约内存,首先应将顶点与其三角形定义分隔开来。TRIANGLE_INDICES 使用 VERTEX_POSITIONS 数组索引定义几何图形,使顶点可重用。然后用三角形带取代三角形,从而减少索引数量。通过使用三角形带,新的三角形可重用最后两个索引。举例来说,三角形带(0,1,2,3)可转换为两个三角形(0,1,2)及(1,2,3)。图 1a 的各角均已标注相应索引数,如果您在图 1a 的 TRIANGLE_INDICES 中遵循这一规则处理,就会发现两个面之间意外地多出了一些三角形。这只是一种用于避免定义某些三角形带的模式。我曾用一个有 14 个立方体索引的三角形带处理过 8 个顶点的情况。
使用其余的类成员即可绘制出立方体。清单 2 展示了其初始化方法。
清单 2. 显示立方体的示例,第 2 部分:初始化
/**
* Called when this sample is displayed.
*/
public void showNotify()
{
init();
}
/**
* Initializes the sample.
*/
protected void init()
{
// Get the singleton for 3D rendering.
_graphics3d = Graphics3D.getInstance();
// Create vertex data.
_cubeVertexData = new VertexBuffer();
VertexArray vertexPositions =
new VertexArray(VERTEX_POSITIONS.length/3, 3, 1);
vertexPositions.set(0, VERTEX_POSITIONS.length/3, VERTEX_POSITIONS);
_cubeVertexData.setPositions(vertexPositions, 1.0f, null);
// Create the triangles that define the cube; the indices point to
// vertices in VERTEX_POSITIONS.
_cubeTriangles = new TriangleStripArray(TRIANGLE_INDICES,
new int[] {TRIANGLE_INDICES.length});
// Create a camera with perspective projection.
Camera camera = new Camera();
float aspect = (float) getWidth() / (float) getHeight();
camera.setPerspective(30.0f, aspect, 1.0f, 1000.0f);
Transform cameraTransform = new Transform();
cameraTransform.postTranslate(0.0f, 0.0f, 10.0f);
_graphics3d.setCamera(camera, cameraTransform);
}
init() 中的第一个步骤就是使用户获取图形上下文(GC),以便绘制 3D 图形。Graphics3D 是一个单元素,_graphics3d 中保存了一个引用,以备将来使用。接下来,创建一个 VertexBuffer 以保存顶点数据。在后文中可以看到,可以为一个顶点指派多种类型的信息,所有顶点都包含于 VertexBuffer 之中,在设置使用 _cubeVertexData.setPositions() 的 VertexArray 中,您惟一需要获取的信息就是顶点位置。VertexArray 构造函数中保存了顶点数量(8 个)、各顶点的组件数(x, y, z)以及各组件的大小(1 字节)。由于这个立方体非常小,1 个字节足以容纳一个坐标。如果需要创建大型的对象,那么还可以创建使用 Short 值(2 个字节)的 VertexArray。但不能使用实数,只能使用整数。接下来,使用 TRIANGLE_INDICES 中的索引对 TriangleStripArray 进行初始化操作。
初始化代码的最后一部分就是摄像机设置。在 setPersective() 中,可设置观察角度、纵横比和剪贴板。注意纵横比和剪贴板的值应为浮点值。M3G 需要 Java 虚拟机(Java Virtual Machine,JVM)提供浮点值支持,这是在 CLDC 1.1 以后的版本中增加的功能。经过观察后,将摄像机从立方体处移开,以查看对象的全视图。可通过平移实现这一操作,转换 部分将就这一主题进行详细讨论。现在,您只要相信,带有第三个正数参数的 postTranslate() 可使摄像机沿 z 轴移动。
初始化后,您就可以将场景渲染到屏幕上。清单 3 实现了此功能。
清单 3. 显示立方体的示例,第 3 部分:绘图
/**
* Renders the sample on the screen.
*
* @param graphics the graphics object to draw on.
*/
protected void paint(Graphics graphics)
{
_graphics3d.bindTarget(graphics);
_graphics3d.clear(null);
_graphics3d.render(_cubeVertexData, _cubeTriangles,
new Appearance(), null);
_graphics3d.releaseTarget();
}
关于示例代码
如果想尝试建立并运行本文中的示例,可以在 “下载” 部分中下载完整的源代码。我使用了 Sun 的 Java Wireless Toolkit 2.2,并将我的项目配置为使用 MIDP 1.0、CLDC 1.1 —— 当然,还有 M3G。我将各部分的示例均作为单独类加以实现。另外还实现了一个简单的界面,您可以在这里选择及执行各个示例。wi-m3gsamples1.zip 压缩包中包含 readme.txt 文件,其中的信息更为详细。
在 paint() 中,bindTarget() 将 Canvas 的图形上下文指派给 Graphics3D。从而开始渲染 3D 对象,到调用 releaseTarget() 时终止渲染。调用 clear() 清除背景后,即可通过 init() 中创建的顶点数据和三角形绘制对象。许多 Graphics3D 的方法都会抛出不可控异常,但绝大多数错误都是不可恢复的,所以我决定不在代码中使用 try/catch 块。可在 VerticesSample.java 处找到本文的全部源代码。
我编写了一个简单的 MIDlet,用于显示示例。可从 下载 中获得 MIDlet 及本文全部源代码。示例运行结果如图 2 所示。
图 2. 示例立方体
很难看出这个屏幕上的矩形就是立方体,这是因为我将摄像机放置在其正对面,这就像站在一堵白墙前面一样。为什么是白色呢?我还没有指派任何颜色,而默认颜色就是白色。下一节将在颜色方面对程序进行完善。
顶点颜色
创建 VertexBuffer 时,我曾提到可以为一个顶点指派多种类型的信息 —— 颜色也是其中之一。图形硬件是以流水线形式处理顶点的,就像工厂以流水线组装汽车一样。它逐个地对各顶点进行一系列的处理,直至各顶点都显示在屏幕上。在这一架构中,来自所有顶点的所有数据都必须同时可用。可以设想,如果组装工人必须每次从不同的地方取螺丝,效率该有多么低。
图 3 以平面布局展示了立方体的前五个顶点,还包括(R,G,B)格式的颜色信息。角上的数字同样是在三角形带中使用的索引。
图 3. 带有索引、顶点颜色和方位的三角形带
为顶点指派颜色对三角形内的像素会有什么影响呢?可能性之一就是为整个三角形使用同样的顶点颜色。另外还有可能在两个顶点之间插入颜色,实现颜色梯度效果。M3G 允许用户在这两个选项之间任选其一。在平面着色渲染模式下,可用三角形的第三个顶点颜色为整个三角形着色。如果您将图 2 所示第 1 个三角形定义为(0,1,2),则其颜色为红色(255,0,0)。在光影渲染模式下,三角形中的各像素都通过插值而获得了自己的颜色。索引 0 和 2 之间的像素初始颜色为绿色(0,255,0),渐变为红色。有些三角形共享索引 2 和索引 3 处的顶点,由于一个顶点只能有一种颜色,所以这也就意味着这些三角形也使用了一种相同的颜色。
图 3 还指出了索引的定义顺序。例如,(0,1,2)按逆时针方向定义第 1 个三角形的顶点,而第二个三角形为(1,2,3)是按照顺时针方向定义的。这就叫做多边形环绕。可以利用它来确定哪个面在前,哪个面在后。从正前方查看立方体时,您总是会认为自己看的仅仅是外部,但如果盒子能打开呢?您一定也想到里边去看看。立方体的每一面都有正反两面。默认地,逆时针方向表示正面。
但这里还有一个小小的问题:如图 3 所示,三角形带中的环绕在每个后续三角形处都会发生变化。按惯例,由三角形带中的第一个三角形定义其环绕。当我将一个三角形带环绕在清单 1 实现的整个立方体上时,首先从一个逆时针方向环绕的三角形(0,1,2)开始。通过这样的方式,也就隐式地将立方体的外部定义为正面,而将内部作为背面。根据具体的需求,您可以要求 M3G 仅渲染正面、仅渲染背面或同时渲染两面。如果立方体有一个半掩的盖子,您同时可看到其正面和背面,此时同时渲染两面的操作非常有用。如果可能,您应该禁用那些看不到的面,这样可以提高渲染速度。将三角形排除在渲染操作之外的方法称为背景拣出。
清单 4 示范了使用顶点颜色的方法。
清单 4. 各顶点都有颜色的立方体,第 1 部分:初始化顶点颜色
/** The cube's vertex colors (R, G, B). */
private static final byte[] VERTEX_COLORS = {
0, (byte) 255, 0, 0, (byte) 255, (byte) 255,
(byte) 255, 0, 0, (byte) 255, 0, (byte) 255,
(byte) 255, (byte) 255, 0, (byte) 255, (byte) 255, (byte) 255,
0, 0, (byte) 128, 0, 0, (byte) 255,
};
/**
* Initializes the sample.
*/
protected void init()
{
// Get the singleton for 3D rendering.
_graphics3d = Graphics3D.getInstance();
// Create vertex data.
_cubeVertexData = new VertexBuffer();
VertexArray vertexPositions =
new VertexArray(VERTEX_POSITIONS.length/3, 3, 1);
vertexPositions.set(0, VERTEX_POSITIONS.length/3, VERTEX_POSITIONS);
_cubeVertexData.setPositions(vertexPositions, 1.0f, null);
VertexArray vertexColors =
new VertexArray(VERTEX_COLORS.length/3, 3, 1);
vertexColors.set(0, VERTEX_COLORS.length/3, VERTEX_COLORS);
_cubeVertexData.setColors(vertexColors);
// Create the triangles that define the cube; the indices point to
// vertices in VERTEX_POSITIONS.
_cubeTriangles = new TriangleStripArray(TRIANGLE_INDICES,
new int[] {TRIANGLE_INDICES.length});
// Define an appearance object and set the polygon mode. The
// default values are: SHADE_SMOOTH, CULL_BACK, and WINDING_CCW.
_cubeAppearance = new Appearance();
_polygonMode = new PolygonMode();
_cubeAppearance.setPolygonMode(_polygonMode);
// Create a camera with perspective projection.
Camera camera = new Camera();
float aspect = (float) getWidth() / (float) getHeight();
camera.setPerspective(30.0f, aspect, 1.0f, 1000.0f);
Transform cameraTransform = new Transform();
cameraTransform.postTranslate(0.0f, 0.0f, 10.0f);
_graphics3d.setCamera(camera, cameraTransform);
}
/**
* Renders the sample on the screen.
*
* @param graphics the graphics object to draw on.
*/
protected void paint(Graphics graphics)
{
_graphics3d.bindTarget(graphics);
_graphics3d.clear(null);
_graphics3d.render(_cubeVertexData, _cubeTriangles,
_cubeAppearance, null);
_graphics3d.releaseTarget();
drawMenu(graphics);
}
在类成员部分的 VERTEX_COLORS 中定义了各顶点颜色。将颜色放在 init()中全新的 VertexArray 内,并通过调用 setColors() 将其指派给 VertexBuffer。在这段代码中还初始化了一个名为 _cubeAppearance 的 Appearance 对象,_graphics3d.render() 使用该对象来更改立方体外观。PolygonMode 是 _cubeAppearance 的一部分,其中包含更改多边形级属性(包括显示哪些面)的方法。为交互地更改这些属性,我还在代码中增加了一个 keyPressed() 方法,如清单 5 所示。
清单 5. 各顶点都有颜色的立方体,第 2 部分:处理按键事件
/**
* Handles key presses.
*
* @param keyCode key code.
*/
protected void keyPressed(int keyCode)
{
switch (getGameAction(keyCode))
{
case FIRE:
init();
break;
case GAME_A:
if (_polygonMode.getShading() == PolygonMode.SHADE_FLAT)
{
_polygonMode.setShading(PolygonMode.SHADE_SMOOTH);
}
else
{
_polygonMode.setShading(PolygonMode.SHADE_FLAT);
}
break;
case GAME_B:
if (_polygonMode.getCulling() == PolygonMode.CULL_BACK)
{
_polygonMode.setCulling(PolygonMode.CULL_FRONT);
}
else
{
_polygonMode.setCulling(PolygonMode.CULL_BACK);
}
break;
case GAME_C:
if (_polygonMode.getWinding() == PolygonMode.WINDING_CCW)
{
_polygonMode.setWinding(PolygonMode.WINDING_CW);
}
else
{
_polygonMode.setWinding(PolygonMode.WINDING_CCW);
}
break;
// no default
}
repaint();
}
键位映射
示例中使用了 MIDP 的动作游戏作为处理按键事件的范例。其控制游戏动作的物理键映射到运行示例的设备上。Sun 的 Java Wireless Toolkit 将 LEFT、RIGHT、UP、DOWN 和 FIRE 映射为游戏操纵杆。GAME_A 映射为 1 键、GAME_B 映射为 3 键、GAME_C 映射为 7 键、GAME_D 映射为 9 键。
按下相应的键更改以下三个属性之一:渲染模式(平面着色渲染模式或光影渲染模式)、背景拣出(看见的是立方体的外面还是里面)、环绕(逆时针三角形表示的是正面还是背面)。图 4 展示了这些选项。VertexColorsSample.java 中包含该示例的完整源代码。
图 4. 经着色的立方体:a) 光影渲染模式;b) 平面着色渲染模式,背面被拣出;c) 正面被拣出,逆时针环绕
回页首
转换
在本文开始处,我曾经使用了一个 Transform 对象将摄像机向后移动,以便查看整个立方体。通过同样的方式可以转换任意 3D 对象。
您可以通过数学方式将转换表示为矩阵操作。一个向量 —— 例如,摄像机位置 —— 乘以恰当的平移矩阵从而得到相应移动的向量。Transform 对象就表示了这样的一个矩阵。对于绝大多数普通转换来说,M3G 提供了 3 种便于使用的接口,隐藏了底层的数学计算:
Transform.postScale(float sx, float sy, float sz):在 x、y、z 方向伸缩 3D 对象。大于 1 的值将按照给定因数扩大对象;0 和 1 之间的值将缩小对象。负值则同时执行伸缩和镜像操作。
Transform.postTranslate(float tx, float ty, float tz):通过为 x、y 和 z 坐标增加指定值移动 3D 对象。负值则表示向负轴方向移动对象。
Transform.postRotate(float angle, float ax, float ay, float az):按给定角度绕穿过(0, 0, 0)和(ax, ay, az)的轴旋转对象。角度为正值,则表示若您顺着正旋转轴方向观察,对象是按顺时针旋转的。例如,postRotate(30, 1, 0, 0) 将绕 x 轴将对象旋转 30 度。
所有操作名都是以 "post" 开头的,表示当前 Transform 对象是从右边与给定转换矩阵相乘的 —— 矩阵操作的顺序是非常重要的。如果您向右旋转 90 度,然后走两步,这时您所处的位置显然与先走两步再转身不同。您可以在各步行指令之后调用两个 post 方法 postRotate() 和 postTranslate(),从而获得上面的步行指令。调用顺序决定了所获得的步行指令。由于使用的是后乘,所以您最后使用的转换会首先应用。
M3G 有一个 Transform 类和一个 Transformable 接口。所有快速模式的 API 均可接受 Transform 对象作为参数,用于修改其关联的 3D 对象。另外,在保留模式下使用 Transformable 接口来转换作为 3D 世界一部分的节点。在本系列的第 2 部分中将就此详细讨论。
清单 6 的示例展示了转换。
清单 6. 转换
/**
* Renders the sample on the screen.
*
* @param graphics the graphics object to draw on.
*/
protected void paint(Graphics graphics)
{
_graphics3d.bindTarget(graphics);
_graphics3d.clear(null);
_graphics3d.render(_cubeVertexData, _cubeTriangles,
new Appearance(), _cubeTransform);
_graphics3d.releaseTarget();
drawMenu(graphics);
}
/**
* Handles key presses.
*
* @param keyCode key code.
*/
protected void keyPressed(int keyCode)
{
switch (getGameAction(keyCode))
{
case UP:
transform(_transformation, TRANSFORMATION_X_AXIS, false);
break;
case DOWN:
transform(_transformation, TRANSFORMATION_X_AXIS, true);
break;
case LEFT:
transform(_transformation, TRANSFORMATION_Y_AXIS, false);
break;
case RIGHT:
transform(_transformation, TRANSFORMATION_Y_AXIS, true);
break;
case GAME_A:
transform(_transformation, TRANSFORMATION_Z_AXIS, false);
break;
case GAME_B:
transform(_transformation, TRANSFORMATION_Z_AXIS, true);
break;
case FIRE:
init();
break;
case GAME_C:
_transformation++;
_transformation %= 3;
break;
// no default
}
repaint();
}
/**
* Transforms the cube with the given parameters.
*
* @param transformation transformation (rotate, translate, scale)
* @param axis axis of translation (x, y, z)
* @param positiveDirection true for increase, false for decreasing
* value.
*/
protected void transform(int transformation, int axis,
boolean positiveDirection)
{
if (transformation == TRANSFORMATION_ROTATE)
{
float amount = 10.0f * (positiveDirection ? 1 : -1);
switch (axis)
{
case TRANSFORMATION_X_AXIS:
_cubeTransform.postRotate(amount, 1.0f, 0.0f, 0.0f);
break;
case TRANSFORMATION_Y_AXIS:
_cubeTransform.postRotate(amount, 0.0f, 1.0f, 0.0f);
break;
case TRANSFORMATION_Z_AXIS:
_cubeTransform.postRotate(amount, 0.0f, 0.0f, 1.0f);
break;
// no default
}
}
else if (transformation == TRANSFORMATION_SCALE)
{
float amount = positiveDirection ? 1.2f : 0.8f;
switch (axis)
{
case TRANSFORMATION_X_AXIS:
_cubeTransform.postScale(amount, 1.0f, 1.0f);
break;
case TRANSFORMATION_Y_AXIS:
_cubeTransform.postScale(1.0f, amount, 1.0f);
break;
case TRANSFORMATION_Z_AXIS:
_cubeTransform.postScale(1.0f, 1.0f, amount);
break;
// no default
}
}
else if (transformation == TRANSFORMATION_TRANSLATE)
{
float amount = 0.2f * (positiveDirection ? 1 : -1);
switch (axis)
{
case TRANSFORMATION_X_AXIS:
_cubeTransform.postTranslate(amount, 0.0f, 0.0f);
break;
case TRANSFORMATION_Y_AXIS:
_cubeTransform.postTranslate(0.0f, amount, 0.0f);
break;
case TRANSFORMATION_Z_AXIS:
_cubeTransform.postTranslate(0.0f, 0.0f, amount);
break;
// no default
}
}
}
paint() 方法现有一个 Transform 对象 _cubeTransform,该对象是 _graphics3d.render() 调用的第 4 个参数。改进的 keyPressed() 方法中包含使用 transform() 交互地更改转换的代码。GAME_C 键在旋转、平移和缩放立方体之间切换。UP/DOWN 键更改当前转换的 x 轴,LEFT/RIGHT 更改 y 轴,GAME_A/GAME_B 更改 z 轴。按 FIRE 可将立方体重新设置为初始位置。您可以在 TransformationsSample.java 中找到完整的源代码。
图 5. 示例立方体:a) 旋转;b) 平移;c) 缩放
深度缓冲和投影
这里我想介绍两个在使用转换时已用到但未说明过的概念:投影,定义了将 3D 对象映射到 2D 屏幕的方法;深度缓冲,是根据对象与摄像机之间的距离正确渲染对象的一种方法。
要从摄像机的观察点观察渲染后的图像,您必须考虑摄像机的位置和方位,将 3D 世界转换为摄像机空间。在前面的示例代码中,我用 Camera 和 Transform 对象调用了 Graphics3D.setCamera()。可将后者视为摄像机转换或告诉 MSG 如何从世界坐标转换为摄像机坐标的指令 —— 两种定义都是正确的。最后,三维对象被显示在二维屏幕上。到这里,Camera.setPerspective() 告诉了 M3G 在将 3D 转换为 2D 空间时实现透视投影。
透视投影与真实世界中的情况比较类似:当您俯视一条又长又直的道路时,道路两边看上去似乎在地平线处交汇了。距离摄像机越远,路旁的对象看起来也就越小。您也可以忽略透视,以相同大小绘制所有对象,不管它们离得多远。这对于某些应用程序,如 CAD 程序来说是很有意义的,因为没有透视可更容易地将精力集中在绘图上。要禁用透视投影,可用 Camera.setParallel() 替换 Camera.setPerspective()。
在摄像机空间中,对象的 z 坐标表示其与摄像机之间的距离。如果渲染一些具有不同 z 坐标的 3D 对象,那么您当然希望距离摄像机较近的对象比远处的对象清晰。通过使用深度缓冲,对象可得到正确的渲染。深度缓冲与屏幕有着相同的宽和高,但用 z 坐标取代颜色值。它存储着绘制在屏幕上的所有像素与摄像机之间的距离。然而,M3G 仅在一个像素比现有同一位置上的像素距离摄像机近时,才将其绘制出来。通过将进入的像素的 z 坐标与深度缓冲中的值相比较,就可以验证这一点。因此,启用深度缓冲可根据对象的 3D 位置渲染对象,而不受 Graphics3D.render() 命令顺序的影响。反之,如果您禁用了深度缓冲,那么必须在绘制 3D 对象的顺序上付出一定精力。在将目标图像绑定到 Graphics3D 时,可启用深度缓冲,也可不启用。在使用接受一个参数的 bindTarget() 重载版本时,默认为启用深度缓冲。在使用带有三个参数的 bindTarget() 时,您可以通过作为第二个参数的布尔值显式切换深度缓冲的开关状态。
您可以更改两个属性:深度缓冲与投影,如清单 7 所示:
清单 7. 深度缓冲与投影
/**
* Initializes the sample.
*/
protected void init()
{
// Get the singleton for 3D rendering.
_graphics3d = Graphics3D.getInstance();
// Create vertex data.
_cubeVertexData = new VertexBuffer();
VertexArray vertexPositions =
new VertexArray(VERTEX_POSITIONS.length/3, 3, 1);
vertexPositions.set(0, VERTEX_POSITIONS.length/3, VERTEX_POSITIONS);
_cubeVertexData.setPositions(vertexPositions, 1.0f, null);
// Create the triangles that define the cube; the indices point to
// vertices in VERTEX_POSITIONS.
_cubeTriangles = new TriangleStripArray(TRIANGLE_INDICES,
new int[] {TRIANGLE_INDICES.length});
// Create parallel and perspective cameras.
_cameraPerspective = new Camera();
float aspect = (float) getWidth() / (float) getHeight();
_cameraPerspective.setPerspective(30.0f, aspect, 1.0f, 1000.0f);
_cameraTransform = new Transform();
_cameraTransform.postTranslate(0.0f, 0.0f, 10.0f);
_cameraParallel = new Camera();
_cameraParallel.setParallel(5.0f, aspect, 1.0f, 1000.0f);
_graphics3d.setCamera(_cameraPerspective, _cameraTransform);
_isPerspective = true;
// Enable depth buffer.
_isDepthBufferEnabled = true;
}
|
|