本文是此系列两部分中的第 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 计算给我们增加了两个视图边界:近切割面和远切割面。观察角度和切割面共同定义了视域。视域中的一切都是可见的,而超出视域范围的一切均不可见。
仅有顶点位置还不够,您还必须描述出想要建立的几何图形。只能像逐点描图法那样,将顶点用直线连接起来,最终得到所需图形。但 M3G 也带来了一个约束:必须用三角形建立几何图形。任何多边形都可定义为一组三角形的集合,因此三角形在 3D 实现中应用十分广泛。三角形是基本的绘图操作,在此基础上可建立更为抽象的操作。
// 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 进行初始化操作。
/** * 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(); }
// 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();
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();
在摄像机空间中,对象的 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();
// 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();
/** * 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);
// 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;
至此,我已经介绍了更改立方体外观的两种方式:顶点颜色和材质。但经过这两种方式处理后的立方体看起来依然很不真实。在现实世界中,应该还有更多的细节。这就是纹理的效果。纹理是像包在礼物外面的包装纸那样环绕在 3D 对象外的图像。您必须为各种情况选择恰当的包装纸,并且决定如何排列。在 3D 编程中也必须作出相同的决策。
/** 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 };
// 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(); } }