本文是此系列两部分中的第 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 所示的立方体。
这个立方体存在于 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 cubes 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 cubes vertex data. */
private VertexBuffer _cubeVertexData;
/** The cubes 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 所示。
很难看出这个屏幕上的矩形就是立方体,这是因为我将摄像机放置在其正对面,这就像站在一堵白墙前面一样。为什么是白色呢?我还没有指派任何颜色,而默认颜色就是白色。下一节将在颜色方面对程序进行完善。
顶点颜色
创建 VertexBuffer 时,我曾提到可以为一个顶点指派多种类型的信息 ―― 颜色也是其中之一。图形硬件是以流水线形式处理顶点的,就像工厂以流水线组装汽车一样。它逐个地对各顶点进行一系列的处理,直至各顶点都显示在屏幕上。在这一架构中,来自所有顶点的所有数据都必须同时可用。可以设想,如果组装工人必须每次从不同的地方取螺丝,效率该有多么低。
图 3 以平面布局展示了立方体的前五个顶点,还包括(R,G,B)格式的颜色信息。角上的数字同样是在三角形带中使用的索引。
为顶点指派颜色对三角形内的像素会有什么影响呢?可能性之一就是为整个三角形使用同样的顶点颜色。另外还有可能在两个顶点之间插入颜色,实现颜色梯度效果。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 cubes 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 中包含该示例的完整源代码。
转换
在本文开始处,我曾经使用了一个 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 中找到完整的源代码。
深度缓冲和投影
这里我想介绍两个在使用转换时已用到但未说明过的概念:投影,定义了将 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;
}
/**
* Renders the sample on the screen.
*
* @param graphics the graphics object to draw on.
*/
protected void paint(Graphics graphics)
{
// Create transformation objects for the cubes.
Transform origin = new Transform();
Transform behindOrigin = new Transform(origin);
behindOrigin.postTranslate(-1.0f, 0.0f, -1.0f);
Transform inFrontOfOrigin = new Transform(origin);
inFrontOfOrigin.postTranslate(1.0f, 0.0f, 1.0f);
// Disable or enable depth buffering when target is bound.
_graphics3d.bindTarget(graphics, _isDepthBufferEnabled, 0);
_graphics3d.clear(null);
// Draw cubes front to back. If the depth buffer is enabled,
// they will be drawn according to their z coordinate. Otherwise,
// according to the order of rendering.
_cubeVertexData.setDefaultColor(0x00FF0000);
_graphics3d.render(_cubeVertexData, _cubeTriangles,
new Appearance(), inFrontOfOrigin);
_cubeVertexData.setDefaultColor(0x0000FF00);
_graphics3d.render(_cubeVertexData, _cubeTriangles,
new Appearance(), origin);
_cubeVertexData.setDefaultColor(0x000000FF);
_graphics3d.render(_cubeVertexData, _cubeTriangles,
new Appearance(), behindOrigin);
_graphics3d.releaseTarget();
drawMenu(graphics);
}
/**
* Handles key presses.
*
* @param keyCode key code.
*/
protected void keyPressed(int keyCode)
{
switch (getGameAction(keyCode))
{
case GAME_A:
_isPerspective = !_isPerspective;
if (_isPerspective)
{
_graphics3d.setCamera(_cameraPerspective, _cameraTransform);
}
else
{
_graphics3d.setCamera(_cameraParallel, _cameraTransform);
}
break;
case GAME_B:
_isDepthBufferEnabled = !_isDepthBufferEnabled;
break;
case FIRE:
init();
break;
// no default
}
repaint();
}
使用 GAME_A 键可在透视投影与平行投影之间切换。GAME_B 可启用或禁用深度缓冲。完整的源代码包含在 DepthBufferProjectionSample.java 中。图 6 展示了不同设置下的效果。
照明
在一个没有光线的房间中,所有的东西看上去都是黑的。那么前面的示例中没有光线,怎么还能看到东西呢?顶点颜色和后面即将介绍的材质是不需要光线的,它们永远显示为定义好的颜色。但光线会使它们发生一些变化,可增加景深。
光线的方向会根据对象的位置发生反射。如果您用手电筒垂直地照射您面前的镜子,那么光线会反射到您身上。如果镜子是倾斜的,则光线的入射角和反射角是完全相同的。总的来说,您需要一个与照射平面相垂直的方向向量。这一向量就称为法线向量 或简称为法线。M3G 会根据法线、光源位置和摄像机位置计算着色情况。
此外,法线是各顶点都具备的属性,各顶点之间的像素着色既可采用插值法(PolygonMode.SHADE_SMOOTH)也可从三角形的第三个顶点处选取(PolygonMode.SHADE_FLAT)。由于立方体有 8 个顶点,支持法线的方法之一就是指定从立方体中心指向各角的向量,如图 7a 所示。但这样做可能会导致立方体着色不当。有三个面的颜色可能会相同,其中有些边成为不可见状态,使立方体看上去缺乏棱角。这显然更适合球体,不太适合立方体。图 7b 展示了如何为每边使用 4 条法线 ―― 共 24 条,从而创建棱角分明的边线。由于一个顶点只能有一条法线,所以还要复制顶点。
可使用法线计算光线后,还需要告诉 M3G 您需要什么类型的光线。光线来源于不同形式:灯泡、太阳、手电筒等等。在 M3G 中的对应术语分别为全向光、定向光和聚光。
全向光是从一个点发出的,并平均地照射各个方向。没有灯罩的灯泡发出的就是这样的光。
定向光向一个方向发出平行的光线。太阳离我们的距离非常远,所以可以将其光线视为平行的。定向光没有位置,只有方向。
手电筒或剧场中使用的聚光灯发射出的光线就是聚光。其光线呈锥形,与圆锥相交的平面上的对象会被照亮。
在真实世界中,光线还会从对象上反射回来而将周围照亮。如果您打开卧室灯,就会发现即便没有能直接照射到床底下的光线,但床下仍会被照亮。Raytracer 通过追踪从摄像机到光源的路径而清晰真实地展示了图像,但需要很长时间。要获得交互式帧频,必须满足一个简单的模型:环境光。环境光以不变的频率从各方向照亮对象。您可以用环境光模拟前面的卧室场景,将所有对象都照亮到一定程度,从而提供了另外一个全向光源。
清单 8 描述了设置不同光线的方法。
清单 8. 设置光线模式
// Create light.
_light = new Light();
_lightMode = LIGHT_OMNI;
setLightMode(_light, _lightMode);
Transform lightTransform = new Transform();
lightTransform.postTranslate(0.0f, 0.0f, 3.0f);
_graphics3d.resetLights();
_graphics3d.addLight(_light, lightTransform);
/**
* Sets the light mode.
*
* @param light light to be modified.
* @param mode light mode.
*/
protected void setLightMode(Light light, int mode)
{
switch (mode)
{
case LIGHT_AMBIENT:
light.setMode(Light.AMBIENT);
light.setIntensity(2.0f);
break;
case LIGHT_DIRECTIONAL:
light.setMode(Light.DIRECTIONAL);
light.setIntensity(1.0f);
break;
case LIGHT_OMNI:
light.setMode(Light.OMNI);
light.setIntensity(2.0f);
break;
case LIGHT_SPOT:
light.setMode(Light.SPOT);
light.setSpotAngle(20.0f);
light.setIntensity(2.0f);
break;
// no default
}
}
在图 8 中,您可以看到各种光线模式的不同效果。分别以 4 种类型的光照射示例立方体。这里的光线均为白色,就在摄像机前面,朝向立方体的三个面。
全向光在面对光源的顶点处最亮,然后逐渐暗淡下来。另外,聚光在聚光圆锥的边缘处制造了强烈的明暗对比。如果定义了一个足够大的光锥,那么所得到的结果可能与全向光相同。环境光从各个方向照亮立方体,立方体看上去是平的,这是因为缺乏阴影。最后,定向光使每面都具有不同的颜色。每面内的颜色都相同,这是因为光线是平行的。
照明并不精确,否则,聚光照亮的圆锥体范围应该是圆形。这是因为光线计算比较复杂,手机的部件将简化这一计算。可以为立方体的各边添加更多的三角形,从而提高其显示质量。尽管三角形并不能定义一个可见的几何图形,但可使 M3G 拥有更多的控制点(要计算的数量也更多)。
材质
通过光线可实现不同的效果。一个闪闪发光的银色球反射光线的方式与一张纸显然不同。M3G 使用以下属性为这些材质的特征建立模型:
环境反射:由环境光源反射的光线。
漫反射:反射光均匀地分散到各个方向。
放射光:一个像炽热的物体那样发射光线的对象。
镜面反射:光线从有光亮平面的对象反射回来。
您可为各材质属性设置颜色。闪闪发光的银色球的漫反射光线应该是银色的,其镜面反射部分为白色。材质的颜色与光线的颜色相融合,从而得到最终的对象颜色。如果您用蓝光照射银色球,那么球看上去应该略带蓝色。
清单 9 展示了使用材质的方式:
清单 9. 设置材质
// Create appearance and the material.
_cubeAppearance = new Appearance();
_colorTarget = COLOR_DEFAULT;
setMaterial(_cubeAppearance, _colorTarget);
/**
* Sets the material according to the given target.
*
* @param appearance appearance to be modified.
* @param colorTarget target color.
*/
protected void setMaterial(Appearance appearance, int colorTarget)
{
Material material = new Material();
switch (colorTarget)
{
case COLOR_DEFAULT:
break;
case COLOR_AMBIENT:
material.setColor(Material.AMBIENT, 0x00FF0000);
break;
case COLOR_DIFFUSE:
material.setColor(Material.DIFFUSE, 0x00FF0000);
break;
case COLOR_EMISSIVE:
material.setColor(Material.EMISSIVE, 0x00FF0000);
break;
case COLOR_SPECULAR:
material.setColor(Material.SPECULAR, 0x00FF0000);
material.setShininess(2);
break;
// no default
}
appearance.setMaterial(material);
}
setMaterial() 创建了一个新的 Material 对象,通过使用各颜色组件标识符的 setColor() 设置颜色。Material 对象随后被指派给 Appearance 对象,该对象用于调用 Graphics3D.render()。尽管这里没有展示,但您还可以使用 Material.setVertexColorTrackingEnable() 为环境反射和漫反射使用顶点颜色,不必使用 Material.setColor()。LightingMaterialsSample.java 这一示例中实现了光线和材质。按其中的键可以将不同的颜色与材质综合,感受不同的效果。
在图 9 中,用全向光展示了不同的材质特征。各截图都将颜色组件设置为红色,以突出表现其效果。
环境反射仅对环境光起作用,因此,使用全向光是无效的。漫反射材质组件会造成一种不光滑的表面,而放射光组件则制造出一种发光效果。镜面反射颜色组件强调了发亮的效果。此外,您还可以通过使用更多的三角形改进明暗对比的着色质量。
纹理
至此,我已经介绍了更改立方体外观的两种方式:顶点颜色和材质。但经过这两种方式处理后的立方体看起来依然很不真实。在现实世界中,应该还有更多的细节。这就是纹理的效果。纹理是像包在礼物外面的包装纸那样环绕在 3D 对象外的图像。您必须为各种情况选择恰当的包装纸,并且决定如何排列。在 3D 编程中也必须作出相同的决策。
现在,您或许已经猜测到我将引入另外一种每个顶点都具备的属性。对于每个顶点而言,纹理坐标定义了使用纹理的位置。然后 M3G 会映射纹理以适应您的对象。可以这样设想,将一块有弹性的包装纸钉在礼物的各顶点上。这些坐标所引用的纹理像素就叫做 texel。图 10 展示了将 128 x 128 texel 的正方形纹理映射到立方体正面的效果。
将纹理坐标命名为(s,t)是为了与用于表示顶点位置的(x,y)区分开来(从文字角度来讲,(u,v)更为常用)。坐标(s,t)定义为(0,0)的地方就是纹理的左上角,而(1,1)位于右下角。相应地,如果您需要将立方体正面的左下角映射到纹理的左下角,必须将纹理坐标(0,1)指派给顶点 0。
由于您定义了与纹理的角相关的纹理坐标,所以任意大小的图像都有相同的坐标。M3G 为最接近的 texel 插入 0 到 1 之间的值,例如,0.5 表示纹理的中点。如果纹理坐标超过 0~1 的范围,M3G 会提示您确认。坐标既可环绕(例如,1.5 与 0.5 的效果相同),也可采用加强方式,所谓加强,也就意味着任何小于 0 的值都按 0 使用,任何大于 1 的值都按 1 使用。纹理的宽和高可有所不同,但必须是 2 的幂,如图 1 中的 128。部件必须至少支持 256 的纹理大小,这是 M3G 的一个可选属性。
Graphics3D.getProperties() 返回一个 Hashtable,其中填充了特定于部件的属性,如最大纹理维度或支持的最大光源数。getProperties() 的文档包含一个属性及其最低需求的清单。在使用超过这些值的属性之前,应该检查设备的部件是否能提供支持。
清单 10 展示了纹理的使用。
清单 10. 使用纹理,第 1 部分:初始化
/** The cubes vertex positions (x, y, z). */
private static final byte[] VERTEX_POSITIONS = {
-1, -1, 1, 1, -1, 1, -1, 1, 1, 1, 1, 1, // front
1, -1, -1, -1, -1, -1, 1, 1, -1, -1, 1, -1, // back
1, -1, 1, 1, -1, -1, 1, 1, 1, 1, 1, -1, // right
-1, -1, -1, -1, -1, 1, -1, 1, -1, -1, 1, 1, // left
-1, 1, 1, 1, 1, 1, -1, 1, -1, 1, 1, -1, // top
-1, -1, -1, 1, -1, -1, -1, -1, 1, 1, -1, 1 // bottom
};
/** Indices that define how to connect the vertices to build
* triangles. */
private static final int[] TRIANGLE_INDICES = {
0, 1, 2, 3, // front
4, 5, 6, 7, // back
8, 9, 10, 11, // right
12, 13, 14, 15, // left
16, 17, 18, 19, // top
20, 21, 22, 23, // bottom
};
/** Lengths of triangle strips in TRIANGLE_INDICES. */
private static int[] TRIANGLE_LENGTHS = {
4, 4, 4, 4, 4, 4
};
/** File name of the texture. */
private static final String TEXTURE_FILE = "/texture.png";
/** The texture coordinates (s, t) that define how to map the
* texture to the cube. */
private static final byte[] VERTEX_TEXTURE_COORDINATES = {
0, 1, 1, 1, 0, 0, 1, 0, // front
0, 1, 1, 1, 0, 0, 1, 0, // back
0, 1, 1, 1, 0, 0, 1, 0, // right
0, 1, 1, 1, 0, 0, 1, 0, // left
0, 1, 1, 1, 0, 0, 1, 0, // top
0, 1, 1, 1, 0, 0, 1, 0, // bottom
};
/** First color for blending. */
private static final int COLOR_0 = 0x000000FF;
/** Second color for blending. */
private static final int COLOR_1 = 0x0000FF00;
/**
* 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 vertexTextureCoordinates =
new VertexArray(VERTEX_TEXTURE_COORDINATES.length/2, 2, 1);
vertexTextureCoordinates.set(0,
VERTEX_TEXTURE_COORDINATES.length/2, VERTEX_TEXTURE_COORDINATES);
_cubeVertexData.setTexCoords(0, vertexTextureCoordinates, 2.0f, null);
// Set default color for cube.
_cubeVertexData.setDefaultColor(COLOR_0);
// Create the triangles that define the cube; the indices point to
// vertices in VERTEX_POSITIONS.
_cubeTriangles = new TriangleStripArray(TRIANGLE_INDICES,
TRIANGLE_LENGTHS);
// 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);
// Rotate the cube so we can see three sides.
_cubeTransform = new Transform();
_cubeTransform.postRotate(20.0f, 1.0f, 0.0f, 0.0f);
_cubeTransform.postRotate(45.0f, 0.0f, 1.0f, 0.0f);
// Define an appearance object and set the polygon mode.
_cubeAppearance = new Appearance();
_polygonMode = new PolygonMode();
_isPerspectiveCorrectionEnabled = false;
_cubeAppearance.setPolygonMode(_polygonMode);
try
{
// Load image for texture and assign it to the appearance. The
// default values are: WRAP_REPEAT, FILTER_BASE_LEVEL/
// FILTER_NEAREST, and FUNC_MODULATE.
Image2D image2D = (Image2D) Loader.load(TEXTURE_FILE)[0];
_cubeTexture = new Texture2D(image2D);
_cubeTexture.setBlending(Texture2D.FUNC_DECAL);
// Index 0 is used because we have only one texture.
_cubeAppearance.setTexture(0, _cubeTexture);
}
catch (Exception e)
{
System.out.println("Error loading image " + TEXTURE_FILE);
e.printStackTrace();
}
}
在 init() 中,向 VertexBuffer 增加了定义为类的静态成员的纹理坐标。与在照明示例中的情况类似,我为立方体的每个面都使用了 4 个向量,以将各顶点映射到纹理的一个角。注意,我使用了比例 2.0 作为 _cubeVertexData.setTexCoords() 的第三个参数。这也就告诉 M3G 将所有纹理坐标都乘以此值。实际上,纹理仅使用了立方体面的四分之一。这样做的目的是展示 M3G 的加强和环绕特性。如果是加强,那么仅在左上角绘制;如果是环绕,那么纹理图案将填充满整个面。
纹理是用 Loader.load() 载入的,并指派给 Texture2D 对象。您还应使用 MIDP 的 Image.createImage(),但如果您想从 Java Archive(JAR)文件中读取纹理,那么 Loader 类是最快的方式。所得到的 Texture2D 对象随后被设置为立方体外观的纹理。
在进行纹理化处理时,您可能依然希望使用通过照明获得或直接指派给顶点的颜色。出于此方面的考虑,M3G 提供了各种混色功能,可通过调用 _cubeTexture.setBlending() 来设置。在 init() 中,我使用了 Texture2D.FUNC_DECAL,它将根据 α 值将纹理与基本顶点颜色相混合。图 10 中纹理图像的灰色位透明度为 60%。这里没有设置顶点颜色或使用照明,而是使用 _cubeVertexData.setDefaultColor() 为立方体设置了一个默认颜色,这也就意味着立方体中的所有三角形都将使用同样的颜色。通过混色,您也可以在各纹理上使用多重纹理,从而获得更丰富的效果。
我还内置了一个可选的 M3G 特性。如照明部分中所示,渲染的质量取决于您所使用的三角形数量 ―― 顶点之间的距离越小,插值效果就越好。这对于纹理来说也是成立的。高质量对于纹理而言就意味着纹理在不失真的情况下映射。如图 10 所示的纹理是有缺陷的,因为其中包含直线,显然会发生失真的情况。MSG 提供了一种在处理能力方面代价低廉的方法来解决这一问题。可用 PolygonMode.setPerspectiveCorrectionEnable() 设置可选的透视修正标志,如清单 11 所示。
清单 11. 使用纹理,第 2 部分:交互式更改透视修正、环绕模式及混色
/**
* Checks whether perspective correction is supported.
*
* @return true if perspective correction is supported, false otherwise.
*/
protected boolean isPerspectiveCorrectionSupported()
{
Hashtable properties = Graphics3D.getProperties();
Boolean supportPerspectiveCorrection =
(Boolean) properties.get("supportPerspectiveCorrection");
return supportPerspectiveCorrection.booleanValue();
}
/**
* Handles key presses.
*
* @param keyCode key code.
*/
protected void keyPressed(int keyCode)
{
switch (getGameAction(keyCode))
{
case LEFT:
_cubeTransform.postRotate(-10.0f, 0.0f, 1.0f, 0.0f);
break;
case RIGHT:
_cubeTransform.postRotate(10.0f, 0.0f, 1.0f, 0.0f);
break;
case FIRE:
init();
break;
case GAME_A:
if (isPerspectiveCorrectionSupported())
{
_isPerspectiveCorrectionEnabled = !_isPerspectiveCorrectionEnabled;
_polygonMode.setPerspectiveCorrectionEnable(
_isPerspectiveCorrectionEnabled);
}
break;
case GAME_B:
if (_cubeTexture.getWrappingS() == Texture2D.WRAP_CLAMP)
{
_cubeTexture.setWrapping(Texture2D.WRAP_REPEAT,
Texture2D.WRAP_REPEAT);
}
else
{
_cubeTexture.setWrapping(Texture2D.WRAP_CLAMP,
Texture2D.WRAP_CLAMP);
}
break;
case GAME_C:
if (_cubeVertexData.getDefaultColor() == COLOR_0)
{
_cubeVertexData.setDefaultColor(COLOR_1);
}
else
{
_cubeVertexData.setDefaultColor(COLOR_0);
}
break;
// no default
}
repaint();
}
在示例中,isPerspectiveCorrectionSupported() 用于检查部件是否支持透视修正。如果支持,您可在 keyPressed() 中交互地切换标志的开关状态。这里还增加了一个更改纹理映射到立方体的方式(加强或重复)的选项以及一个更改混色的选项。对混色的更改示范了可以容易地将颜色与纹理相混合以获得更丰富的效果。在 TexturesSample.java 中可以看到完整的示例。
图 11 展示了使用不同选项的纹理映射效果。
结束语
本文中介绍了大量基础知识,包括使用顶点数据创建立方体、使用摄像机为立方体照相、在立方体上应用光线和材质、利用纹理创建具有真实感的立方体的方法等详细信息。给出了许多立方体作为示例。
在论证概念时,立方体是一种极好的示例,但它并不是复杂的 3D 设计的里程碑。在介绍过程中,我从游戏的角度强调了 3D 图像。如果像示例那样通过手工组合顶点数据,那么设计一个复杂的游戏世界将成为一项令人望而却步的工作。您需要一种方法,通过建模工具来设计 3D 场景,并将数据导入程序。
导入模型后,必须再寻求一种组织数据的方法。如果使用 VertexBuffer 方法,您必须记住所有的转换以及对象之间的关系。比如说,上臂与下臂相连,而下臂应该与手相连。您必须对应地安置手臂与手。M3G 提供的一种场景图形 API ―― 保留模式 ―― 简化了此类任务,通过保留模式您可以为全部对象及其属性建模。在本系列的第 2 部分中将就此进行详细论述。