本文是此系列兩部分中的第 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(); } }