用Java開發3D游戲之創建場景
本文將用一個Java 3D游戲來介紹Checkers3D以及如何使用它創建一個場景,該場景包含深綠色和藍色間隔的平鋪表面,其軸向是沿著x和z軸,一個藍色的背景和一個浮動的可以從兩個不同的方向照亮的球體。用戶(觀察者)能通過移動鼠標移動該場景。
圖1中左邊的快照顯示出程序起始的視圖;右邊的顯示出用戶移動一點后的場景圖。
Checkers3D游戲展示了Java 3D編程中許多共同之處及一些技巧。例如,3D場景的顯示是使用Java 3D Canvas3D類完成的-這個類必須與Java的Swing組件集成到一起。所有的Java 3D應用程序要求一個場景圖,而Checkers3D展示了怎樣添加基本的形狀、光源(環境光和有向光)和背景。該場景圖用作文檔的一種可視化形式,并且借助于Daniel Selman的Java3dTree包可以容易地生成其信息的一種文本版本。
地板和球利用了Java 3D的QuadArray、Text2D和球體幾何體類。地板是在一個QuadArray中的一系列的四邊形;而標簽是利用Text2D對象沿著地板的主軸放置的。球體的實現將向用戶展示怎樣著色,點亮和放置一個3D形狀。用戶從一種視圖來觀察該3D世界。你將看到如何在初始化過程中確定球體的位置,以及如何在執行期間通過使用Java 3D的OrbitBehavior類來移動該球體的。
一、 Checkers3D的類圖
圖2中的類圖顯示該Checkers3D應用程序的所有的公共和私有數據及方法。
Checkers3D是該應用程序最頂層的JFrame。WrapCheckers3D是一擁有場景圖的JPanel,該場景圖可經由一Canvas3D對象來觀看。 CheckerFloor創建地板子圖(如瓷磚,軸,等等),這里相同顏色的瓷磚是用一個ColoredTiles對象描述的。
二、 集成Java 3D和Swing
Checkers3D是一個JFrame-如果必要的話,可以把GUI控件,例如Swing文本域和按鈕等放置到它上面。在本文的實例中,我創建了一個WrapCheckers3D(一個JPanel)的實例并把它放到一個BorderLayout的中央:
在該場景上的Canvas3D視圖是在WrapCheckers3D中創建的:
當使用Canvas3D時必須小心,因為它是一個輕量級的GUI元素(在一個OS生成的窗口之上的薄層)。重量級的組件無法容易地與輕量級的Swing控件相結合;這些控件大部分由Java生成。如果把Canvas3D對象嵌入到Jpanel中就可以避免這些問題;那么該面板就可以安全地與基于Swing構建的應用程序的其它部分集成到一起。
提示 在j3d.org(http://www.j3d.org/tutorials/quick_fix/swing.html)上有關于把Canvas3D和Swing相結合的詳細討論。
與前面的章節中的應用程序相比,這里沒有更新/繪制動畫循環。這是不必要的,因為Java 3D包含它自己的機制來監視場景變化并且初始化著色。下面是該算法的偽碼形式:
行為是一些場景圖結點。它們包含能夠影響圖中其它部分的代碼,例如移動形狀或改變燈光。它們可以用于監控圖形,從而把細節信息傳遞到應用程序中的非3D部分。
有關細節可能要比這個偽代碼中所建議的更為復雜,例如,Java 3D使用多線程來執行并行遍歷和著色。然而,了解一下這個過程的大致思想將有助于你理解本文后面的代碼。
三、 創建場景圖
這個場景圖是通過WrapCheckers3D的構造器創建的:
該Canvas3D對象被從getPreferredConfiguration()中得到的配置初始化;這個方法查詢有關硬件的著色信息。一些老式的Java 3D程序并不初始化一個GraphicsConfiguration對象,它們使用null作為到Canvas3D構造器的參數。這是一種不好的編程方法。
聚焦于canvas3D會使得鍵盤輸入事件被發送到場景圖中的行為中。行為經常是通過鍵的按下與釋放來激活的,但是它們也可以由定時器、幀變化和由Java 3D內部生成的事件來觸發。在Checkers3D中不存在任何行為,所以沒有必要聚焦。我把這相應的幾行代碼保留下來,因為它們在我們后面將要討論的幾乎每種其它程序中都要使用。
SimpleUniverse對象創建一標準視圖分支圖和場景圖的VirtualUniverse及Locale結點。createSceneGraph()方法設置燈光、天空背景、地板及浮動的球體;initUserPosition()和orbitControls()負責處理觀察者問題。在該方法的最后,結果BranchGroup被添加到該場景圖上:
各種方法把子圖添加到sceneBG上以構建內容分支圖。一旦該圖被終結化并允許Java 3D對它進行優化,sceneBG就被編譯。這種優化包含生成圖、重分組和組合結點。例如,一串包含不同平移的TransformGroup結點可能組合到單個的結點中。另一種可能是把所有的形狀用相同的外觀屬性分組,這樣它們可以更快地著色。
邊界是一個全局的BoundingSphere,用來指定對于燈光、背景和OrbitBehavior對象等環境結點的影響。邊界球體放置在場景的中央并影響BOUNDSIZE個單位半徑內的一切。邊界盒和邊界多面體在Java 3D中都是可用的。
在WrapCheckers3D( )執行最后的場景圖顯示在圖3中。
其中的"Floor Branch"結點是我的發明,用來隱藏一些細節直到最后。圖3中沒有顯示的是場景圖的視圖分支部分。
四、 點亮場景
一個環境燈光和兩個有向燈光被通過lightScene()方法添加到該場景上。一個環境燈光可以達到世界中的每個角落并同等程度地照亮一切。
這里環境光源是沿著邊界創建的并且被添加到場景中。Color3f()函數中使用了紅/綠/藍色,范圍為0.0f~1.0f。
有向燈光模仿了具有一定距離的燈光的效果,從一個特定方向來照亮物體的表面。其與環境光的主要區別在于,它需要一個有關的矢量。
方向是連接點(0,0,0)和點(-1,-1,-1)的矢量;而燈光可以被想象成是方向與該矢量一致的多條平等線。
點光源和斑點光源是其它形式的Java 3D光。點光源位置在空間上,其方向朝各個方向發出。斑點光源是聚集的點光源,它指向一個特定方向。
一個場景的背景可以用一個固定的顏色(如下)、一靜態的圖像或一個紋理貼圖幾何體例如一個球體來指定:
五、 浮動的球體
球體是一個工具類,來自于Java 3D的com.sun.j3d.utils.geometry包,這是一個Primitive類的子類,而Primitive類是一個Group結點-它有一個Shape3D子結點(見圖3)。它的幾何體在一個Java 3D TriangleStripArray中相鄰-它指定球體是一個相連接的三角形的數據。我不必調整這個幾何體,但是該球體的外觀和位置確實需要改變。
Appearance結點是一個包含大量參考信息-包括色彩、線、點、多邊形、著色、透明度和材質屬性-的容器。
ColouringAttributes修正一個形狀的顏色并且不受場景燈光的影響。對于一個要求顏色和光相交互的形狀來說,需要使用Material組件。要使光影響一個形狀的顏色,必須滿足三個條件:
·該形狀的幾何體必須包括法線。
·該形狀的Appearance結點必須有一個Material組件。
·該Material組件必須用setLightingEnable()啟動了燈光效果。
工具球體類能自動地創建法線,因此第一個條件很容易滿足。
六、 給球體加上顏色
Java 3D Material組件控制一個形狀當被不同的燈光點亮時展示什么顏色:
ambientColor參數指定當被環境光點亮時形狀的顏色:這使得對象具有一個統一的顏色。emissiveColor代表形狀產生的顏色;這個參數經常被置為黑色(等于off)。diffuseColor是對象點亮時的顏色,其亮度依賴于光柱與形狀的表面形成的角度的大小。
提示: 散射和環境顏色常被設置為相同色,這與真實世界中的大多數物體被顏色點亮時的方式相匹配。
specularColor參數與形狀與它的發光區的反射程度相關。這個值與亮度參數結合在一起。
提示: 鏡面光的顏色常被設置為白色,這與真實世界中的由大多數物體生成的鏡面光的顏色相匹配。
在Checkers3D中,有兩個有向光源-它們在浮動球體的頂部創建兩個閃亮的光環(見圖1)。地板瓦還沒有點亮,因為它們的顏色是用形狀的幾何體來設置的(見后面)。
在floatingSphere()中管理球體的外觀的代碼如下:
七、 放置球體
放置一個形狀幾乎總是一直通過把它的場景圖結點放到一個TransformGroup(見圖3中的球體Group)的下方來實現的。可以用一個TransformGroup來放置、旋轉和縮放放在它下面的結點,這里變換是用Java 3D Transform3D對象來定義的:
這個set()方法把球體的中心放在(0,4,0)并且重置任何以前的旋轉或縮放。set()可以用來在重置其它變換的同時實現縮放和旋轉。方法setTranslation(),setScale()和setRotation()僅影響給定的變換。
不象其它一些3D繪圖包,Java 3D中的y軸在垂直方向上,而地面是由XZ平面定義的,如圖4所示。
在Checkers3D中球體的位置被設置為(0,4,0),這把它的中心放置到XZ平面上方4個單位的位置。
圖1中左邊的快照顯示出程序起始的視圖;右邊的顯示出用戶移動一點后的場景圖。
圖1.起始和后來的視圖 |
Checkers3D游戲展示了Java 3D編程中許多共同之處及一些技巧。例如,3D場景的顯示是使用Java 3D Canvas3D類完成的-這個類必須與Java的Swing組件集成到一起。所有的Java 3D應用程序要求一個場景圖,而Checkers3D展示了怎樣添加基本的形狀、光源(環境光和有向光)和背景。該場景圖用作文檔的一種可視化形式,并且借助于Daniel Selman的Java3dTree包可以容易地生成其信息的一種文本版本。
地板和球利用了Java 3D的QuadArray、Text2D和球體幾何體類。地板是在一個QuadArray中的一系列的四邊形;而標簽是利用Text2D對象沿著地板的主軸放置的。球體的實現將向用戶展示怎樣著色,點亮和放置一個3D形狀。用戶從一種視圖來觀察該3D世界。你將看到如何在初始化過程中確定球體的位置,以及如何在執行期間通過使用Java 3D的OrbitBehavior類來移動該球體的。
一、 Checkers3D的類圖
圖2中的類圖顯示該Checkers3D應用程序的所有的公共和私有數據及方法。
圖2.Checkers3D的類圖 |
Checkers3D是該應用程序最頂層的JFrame。WrapCheckers3D是一擁有場景圖的JPanel,該場景圖可經由一Canvas3D對象來觀看。 CheckerFloor創建地板子圖(如瓷磚,軸,等等),這里相同顏色的瓷磚是用一個ColoredTiles對象描述的。
二、 集成Java 3D和Swing
Checkers3D是一個JFrame-如果必要的話,可以把GUI控件,例如Swing文本域和按鈕等放置到它上面。在本文的實例中,我創建了一個WrapCheckers3D(一個JPanel)的實例并把它放到一個BorderLayout的中央:
c.setLayout( new BorderLayout( ) ); WrapCheckers3D w3d = new WrapCheckers3D( );//3D畫布的面板 c.add(w3d, BorderLayout.CENTER); |
在該場景上的Canvas3D視圖是在WrapCheckers3D中創建的:
public WrapCheckers3D( ){ setLayout( new BorderLayout( ) ); //另外的初始化代碼 GraphicsConfiguration config =SimpleUniverse.getPreferredConfiguration( ); Canvas3D canvas3D = new Canvas3D(config); add("Center", canvas3D); //另外的初始化代碼 } |
當使用Canvas3D時必須小心,因為它是一個輕量級的GUI元素(在一個OS生成的窗口之上的薄層)。重量級的組件無法容易地與輕量級的Swing控件相結合;這些控件大部分由Java生成。如果把Canvas3D對象嵌入到Jpanel中就可以避免這些問題;那么該面板就可以安全地與基于Swing構建的應用程序的其它部分集成到一起。
提示 在j3d.org(http://www.j3d.org/tutorials/quick_fix/swing.html)上有關于把Canvas3D和Swing相結合的詳細討論。
與前面的章節中的應用程序相比,這里沒有更新/繪制動畫循環。這是不必要的,因為Java 3D包含它自己的機制來監視場景變化并且初始化著色。下面是該算法的偽碼形式:
while(true){ 處理用戶輸入; if (存在請求) break; 執行行為; if (場景圖發生變化) 遍歷場景圖并著色; } |
行為是一些場景圖結點。它們包含能夠影響圖中其它部分的代碼,例如移動形狀或改變燈光。它們可以用于監控圖形,從而把細節信息傳遞到應用程序中的非3D部分。
有關細節可能要比這個偽代碼中所建議的更為復雜,例如,Java 3D使用多線程來執行并行遍歷和著色。然而,了解一下這個過程的大致思想將有助于你理解本文后面的代碼。
三、 創建場景圖
這個場景圖是通過WrapCheckers3D的構造器創建的:
public WrapCheckers3D( ){ //初始化代碼 GraphicsConfiguration config =SimpleUniverse.getPreferredConfiguration( ); Canvas3D canvas3D = new Canvas3D(config); add("Center", canvas3D); canvas3D.setFocusable(true); //聚焦畫布 canvas3D.requestFocus( ); su = new SimpleUniverse(canvas3D); createSceneGraph( ); initUserPosition( ); //設置用戶的觀察點 orbitControls(canvas3D); //控制移動觀察點 su.addBranchGraph( sceneBG ); } |
該Canvas3D對象被從getPreferredConfiguration()中得到的配置初始化;這個方法查詢有關硬件的著色信息。一些老式的Java 3D程序并不初始化一個GraphicsConfiguration對象,它們使用null作為到Canvas3D構造器的參數。這是一種不好的編程方法。
聚焦于canvas3D會使得鍵盤輸入事件被發送到場景圖中的行為中。行為經常是通過鍵的按下與釋放來激活的,但是它們也可以由定時器、幀變化和由Java 3D內部生成的事件來觸發。在Checkers3D中不存在任何行為,所以沒有必要聚焦。我把這相應的幾行代碼保留下來,因為它們在我們后面將要討論的幾乎每種其它程序中都要使用。
SimpleUniverse對象創建一標準視圖分支圖和場景圖的VirtualUniverse及Locale結點。createSceneGraph()方法設置燈光、天空背景、地板及浮動的球體;initUserPosition()和orbitControls()負責處理觀察者問題。在該方法的最后,結果BranchGroup被添加到該場景圖上:
private void createSceneGraph( ){ sceneBG = new BranchGroup(); bounds = new BoundingSphere(new Point3d(0,0,0), BOUNDSIZE); lightScene( ); //添加燈 addBackground( ); //添加天空 sceneBG.addChild( new CheckerFloor( ).getBG( ) );//添加地板 floatingSphere( ); //添加浮動的球體 sceneBG.compile( ); //修改場景 } //createSceneGraph()結束 |
各種方法把子圖添加到sceneBG上以構建內容分支圖。一旦該圖被終結化并允許Java 3D對它進行優化,sceneBG就被編譯。這種優化包含生成圖、重分組和組合結點。例如,一串包含不同平移的TransformGroup結點可能組合到單個的結點中。另一種可能是把所有的形狀用相同的外觀屬性分組,這樣它們可以更快地著色。
邊界是一個全局的BoundingSphere,用來指定對于燈光、背景和OrbitBehavior對象等環境結點的影響。邊界球體放置在場景的中央并影響BOUNDSIZE個單位半徑內的一切。邊界盒和邊界多面體在Java 3D中都是可用的。
在WrapCheckers3D( )執行最后的場景圖顯示在圖3中。
其中的"Floor Branch"結點是我的發明,用來隱藏一些細節直到最后。圖3中沒有顯示的是場景圖的視圖分支部分。
四、 點亮場景
一個環境燈光和兩個有向燈光被通過lightScene()方法添加到該場景上。一個環境燈光可以達到世界中的每個角落并同等程度地照亮一切。
Color3f white = new Color3f(1.0f, 1.0f, 1.0f); //設置環境燈光 AmbientLight ambientLightNode = new AmbientLight(white); ambientLightNode.setInfluencingBounds(bounds); sceneBG.addChild(ambientLightNode); |
這里環境光源是沿著邊界創建的并且被添加到場景中。Color3f()函數中使用了紅/綠/藍色,范圍為0.0f~1.0f。
有向燈光模仿了具有一定距離的燈光的效果,從一個特定方向來照亮物體的表面。其與環境光的主要區別在于,它需要一個有關的矢量。
Vector3f light1Direction=new Vector3f(-1.0f,-1.0f,-1.0f); //左面,下面,后面 DirectionalLight light1 = new DirectionalLight(white, light1Direction); light1.setInfluencingBounds(bounds); sceneBG.addChild(light1); |
圖3.Checkers3D中的部分場景圖 |
方向是連接點(0,0,0)和點(-1,-1,-1)的矢量;而燈光可以被想象成是方向與該矢量一致的多條平等線。
點光源和斑點光源是其它形式的Java 3D光。點光源位置在空間上,其方向朝各個方向發出。斑點光源是聚集的點光源,它指向一個特定方向。
一個場景的背景可以用一個固定的顏色(如下)、一靜態的圖像或一個紋理貼圖幾何體例如一個球體來指定:
Background back = new Background( ); back.setApplicationBounds( bounds ); back.setColor(0.17f, 0.65f, 0.92f); //天空顏色 sceneBG.addChild( back ); |
五、 浮動的球體
球體是一個工具類,來自于Java 3D的com.sun.j3d.utils.geometry包,這是一個Primitive類的子類,而Primitive類是一個Group結點-它有一個Shape3D子結點(見圖3)。它的幾何體在一個Java 3D TriangleStripArray中相鄰-它指定球體是一個相連接的三角形的數據。我不必調整這個幾何體,但是該球體的外觀和位置確實需要改變。
Appearance結點是一個包含大量參考信息-包括色彩、線、點、多邊形、著色、透明度和材質屬性-的容器。
ColouringAttributes修正一個形狀的顏色并且不受場景燈光的影響。對于一個要求顏色和光相交互的形狀來說,需要使用Material組件。要使光影響一個形狀的顏色,必須滿足三個條件:
·該形狀的幾何體必須包括法線。
·該形狀的Appearance結點必須有一個Material組件。
·該Material組件必須用setLightingEnable()啟動了燈光效果。
工具球體類能自動地創建法線,因此第一個條件很容易滿足。
六、 給球體加上顏色
Java 3D Material組件控制一個形狀當被不同的燈光點亮時展示什么顏色:
Material mat = new Material(ambientColor, emissiveColor, diffuseColor, specularColor, shininess); |
ambientColor參數指定當被環境光點亮時形狀的顏色:這使得對象具有一個統一的顏色。emissiveColor代表形狀產生的顏色;這個參數經常被置為黑色(等于off)。diffuseColor是對象點亮時的顏色,其亮度依賴于光柱與形狀的表面形成的角度的大小。
提示: 散射和環境顏色常被設置為相同色,這與真實世界中的大多數物體被顏色點亮時的方式相匹配。
specularColor參數與形狀與它的發光區的反射程度相關。這個值與亮度參數結合在一起。
提示: 鏡面光的顏色常被設置為白色,這與真實世界中的由大多數物體生成的鏡面光的顏色相匹配。
在Checkers3D中,有兩個有向光源-它們在浮動球體的頂部創建兩個閃亮的光環(見圖1)。地板瓦還沒有點亮,因為它們的顏色是用形狀的幾何體來設置的(見后面)。
在floatingSphere()中管理球體的外觀的代碼如下:
Color3f black = new Color3f(0.0f, 0.0f, 0.0f); Color3f blue = new Color3f(0.3f, 0.3f, 0.8f); Color3f specular = new Color3f(0.9f, 0.9f, 0.9f); //近乎白色 Material blueMat= new Material(blue, black, blue, specular, 25.0f); blueMat.setLightingEnable(true); Appearance blueApp = new Appearance( ); blueApp.setMaterial(blueMat); |
七、 放置球體
放置一個形狀幾乎總是一直通過把它的場景圖結點放到一個TransformGroup(見圖3中的球體Group)的下方來實現的。可以用一個TransformGroup來放置、旋轉和縮放放在它下面的結點,這里變換是用Java 3D Transform3D對象來定義的:
Transform3D t3d = new Transform3D(); t3d.set( new Vector3f(0,4,0)); //放在(0,4,0) TransformGroup tg = new TransformGroup(t3d); tg.addChild(new Sphere(2.0f, blueApp)); //設置球體的半徑和外觀 //并缺省地設置其法線 sceneBG.addChild(tg); |
這個set()方法把球體的中心放在(0,4,0)并且重置任何以前的旋轉或縮放。set()可以用來在重置其它變換的同時實現縮放和旋轉。方法setTranslation(),setScale()和setRotation()僅影響給定的變換。
不象其它一些3D繪圖包,Java 3D中的y軸在垂直方向上,而地面是由XZ平面定義的,如圖4所示。
在Checkers3D中球體的位置被設置為(0,4,0),這把它的中心放置到XZ平面上方4個單位的位置。
圖4.在Java 3D中的軸向 |