用Java開發3D游戲之創建浮動的球體
在本系列的第一篇中,我們討論了如何在開發Java 3D圖形程序中創建形狀、燈光和背景。在隨后的本篇中,我將向你展示如何為Checkers3D程序創建浮動的球體。
一、 地板
地板是由瓷磚(用我的ColouredTiles類創建)和軸標簽(用Java 3D中的Text2D工具類構建)組成的。圖5顯示了地板分支子圖,以前隱藏在圖3中的一個"Floor Branch"方框中。
這個地板子圖是用我的CheckerFloor類的一個實例構建的,它可以通過調用getBG()方法來使用:
這個CheckerFloor()構造器使用嵌套的循環來初始化兩個ArrayLists。blueCoords列表包含藍色瓷磚相關的所有坐標,而greenCoords包含綠色瓷磚相關的坐標。一旦填充完ArrayLists,連同用于生成瓷磚的顏色被一起傳遞到ColouredTiles對象中。ColouredTiles對象是一個Shape3D的子類,因此可以被直接添加到地板圖中:
原點處的紅色方格(見于圖1)是用相似的方式構建的:
該方格的中心在XZ平面的(0,0)處并且在y軸稍微向上(+0.01單位)的地方,這樣就可以在瓷磚上看到它。
該方格的每一邊的長度為0.5單位。在ArrayList中的四個Point3f點以逆時針方向存儲。對于在blueCoords和greenCoords中的每一組的四個點都是如此。圖6顯示出方格中的點的順序。
二、 著色的瓷磚
我的ColouredTiles類擴展了Shape3D并用相同的顏色定義瓷磚的幾何體和外觀。該幾何體使用一個Java 3D QuadArray來描述瓷磚為一系列的四邊形。其構造器是:
vertexFormat是一個靜態整數集合-它指定稍后被初始化的該四邊形的各種信息如坐標、顏色和法線。在ColouredTiles中,QuadArray平面是用下面一行代碼創建的:
size()方法返回在ArrayList中的坐標數目。坐標和顏色數據是在createGeometry()中提供的:
一個四邊形的坐標順序的正確指定是極為重要的。一個多邊形的前面是指頂點形成一個逆時針方向環時的那一面。區別開前面與后面對于光線和隱藏面的選擇是非常重要的;并且默認情況下,在一個場景中只有多邊形的前面是可見的。在這個應用程序中,瓷磚是有向的-它們的前面朝向y軸。
必須確保一個凸的平面多邊形的每個四邊形中的頂點可以是折衷的(compromised)。然而,在坐標數組中的每個四邊形不需要連接到鄰近其它的四邊形-正好適于表達瓷磚。既然一個四邊形的幾何體不包括法線信息,那么一個Material結點組件不可能被用來指定該四邊形的在照亮時的顏色。我可以使用一個ColoringAttributes,但是第三種選擇是設置幾何體中的顏色,例如使用語句"plane.setColors(0,cols);"。這個顏色將固定不變-不受場景中燈光的影響。
一旦完成,用下列語句設置Shape3D的幾何體:
形狀的外觀是由createAppearance()管理的,這個方法使用一個Java 3D PolygonAttribute組件來隱藏后面的顯示。PolygonAttribute可能被用于以點或線形式生成多邊形(也就是以線框架形式),并且用于翻轉后面形狀的法線:
一旦外觀全部指定,它就被用下列語句固定在形狀中:
三、 地板的軸標簽
地板的軸標簽是在CheckerFloor()中用labelAxes()和makeText()方法生成的。labelAxes()使用兩個循環來沿著x和z軸創建標簽。每個標簽是由makeText()構建的,然后被添加到地板的BranchGroup(見圖5):
makeText()使用Text2D工具類創建一個2D串來指定顏色、字體、點大小及字體風格:
Text2D對象是一個具有一個四邊形幾何體(一矩形)的Shape3D對象,并且外觀是由一個字符串表達的材質貼圖(圖像)來確定的-貼圖被放在前面。默認地,后面是隱藏的;如果用戶移動到軸標簽的后面,那么對象成為不可見的。
點大小被轉換成虛擬世界單位-方法把其是與256相除。通常,在Text2D()構造器使用太大的點是一種糟糕的主意,因為這有可能導致文字的不正確著色。我建議把一個TransformGroup放置在形狀上方并且把它縮放到必要的大小。
每個標簽的放置是由在形狀上方的一個TransformGroup來實現的:
setTranslation()僅僅影響該形狀的位置。TransformGroup tg被添加到地板場景圖。
四、 觀察者位置
圖3中的場景圖并不包括視圖分支圖;該分支圖顯示在圖7中。
這個分支是通過在WrapCheckers3D()構造器中調用SimpleUniverse構造器構建的:
SimpleUniverse提供到視圖分支圖的簡化存取-這是經由ViewingPlatform和Viewer類實現的,而且這兩個類被映射到圖上(在圖7中顯示為用點畫線繪制的矩形)。
ViewingPlatform被用在initUserPosition()中以存取ViewPlatform結點上方的TransformGroup:
steerTG相應于圖7中的TG結點。它的Transform3D組件分別用lookAt()和invert()方法加以提取和改變:
lookAt()是在虛擬的世界中設置觀察者位置的一種方便的方法。這個方法需要觀察者的所在位置、他的觀察點和一個指定向上方向的矢量。在這個應用程序中,觀察者的位置是USERPOSN((0,5,20)坐標);他向著原點(0,0,0)觀看,并且沿著正向y軸向上看。這可以由圖8展示。
既然位置是相對于觀察者而不是相對于場景中的一個對象,所以必須調用invert()。
五、 觀察者移動
用戶能夠通過場景移動-利用視圖圖中的Java 3D OrbitBehavior工具類實現。觀察者的位置是通過控制鍵和鼠標按鈕結合方式完成移動和旋轉的。
行為是在WrapCheckers3D中的orbitControls()方法中建立的:
REVERSE_ALL標志保證觀察點的移動沿著與鼠標一樣的方向。
提示: 還有其它許多標志和方法影響旋轉、平移和縮放特性,詳見OrbitBehavior類文檔的有關解釋。
MouseRotate、MouseTranslate和MouseZoom是一些經常出現在許多Java 3D例子中的相類似的行為類;它們與OrbitBehavior的主要的差別是它們影響場景中的對象而不是影響觀察者。
提示: 大多數游戲,例如第一人稱射手(FPS),要求較強地控制觀察者的移動,甚至超出這些工具行為能提供的功能;因此我將在后面實現我自己的行為。
六、 觀看場景圖
本文中已經使用場景圖來展示所討論的編碼技術,而場景圖是一個理解(和檢查)代碼的相當有用的方法。
我使用Daniel Selman的Java3dTree包來幫助我實現繪圖。它創建一個Jframe-它用一個文本樹來描述場景圖(圖9)。
該樹(一個JTree對象)在開始時是最小化的,并且可以通過點擊子文件夾圖標來檢查分支。當前選擇結點信息出現在底部窗口中。該包包含在j3dtree.jar中,它是從http://www.manning.com/selman/下載的源代碼(Selman的"Java 3D編程文本")的一部分。
擴充代碼來生成JTree是簡單的。為了顯示JFrame樹,WrapCheckers3D必須導入j3dtree包并且聲明一個全局變量:
由WrapCheckers3D()構造器創建j3dTree對象:
在完成場景圖之后(也就是,在構造器的最后),該樹的顯示是用一行代碼實現的:
然而,在這之前,必須調整場景圖結點的能力:
應該在完成內容分支組(sceneBG)之后而編譯或使其成為現場的之前執行這個操作。在我的代碼中,這意味著在createSceneGraph()中添加一行代碼:
不幸的是,你不能僅調用:
在此,沒有產生錯誤-因為SimpleUniverse()構造器已經使得ViewingPlatform成為現場的,它可以防止進一步改變它的能力。
既然只有內容分支的能力得到調整,那么當遇到在Locale結點下的視圖分支時,對updateNodes()的調用將生成一些警告消息。
注意 在編譯和執行中必須把j3dtree.jar包括在classpath中。我比較喜歡的方式是用命令行參數來實現:
提示: 如果重復地輸入classpath不適合你,象上面這樣的命令行可以被隱藏在批文件或外殼腳本內部。
Java3dTree對象是一個文本方式的場景描述,這意味著我們必須自己繪制場景圖。但其優點是樹的生成對于程序的其它部分所產生的影響可以忽略。
另一種方法是使用Java 3D場景圖編輯器(http://java3d.netbeans.org/j3deditor_intro.html)。這里顯示場景圖的一個圖形化的版本但是也有其負面-其安裝和用法是復雜的并且內存要求對于一些機器來說可能很苛刻。
一、 地板
地板是由瓷磚(用我的ColouredTiles類創建)和軸標簽(用Java 3D中的Text2D工具類構建)組成的。圖5顯示了地板分支子圖,以前隱藏在圖3中的一個"Floor Branch"方框中。
圖5.場景圖的地板分支子圖 |
這個地板子圖是用我的CheckerFloor類的一個實例構建的,它可以通過調用getBG()方法來使用:
sceneBG.addChild(new CheckerFloor().getBG());//添加地板 |
這個CheckerFloor()構造器使用嵌套的循環來初始化兩個ArrayLists。blueCoords列表包含藍色瓷磚相關的所有坐標,而greenCoords包含綠色瓷磚相關的坐標。一旦填充完ArrayLists,連同用于生成瓷磚的顏色被一起傳遞到ColouredTiles對象中。ColouredTiles對象是一個Shape3D的子類,因此可以被直接添加到地板圖中:
floorBG.addChild( new ColouredTiles(blueCoords, blue) ); floorBG.addChild( new ColouredTiles(greenCoords, green) ); |
原點處的紅色方格(見于圖1)是用相似的方式構建的:
Point3f p1 = new Point3f(-0.25f, 0.01f, 0.25f); Point3f p2 = new Point3f(0.25f, 0.01f, 0.25f); Point3f p3 = new Point3f(0.25f, 0.01f, -0.25f); Point3f p4 = new Point3f(-0.25f, 0.01f, -0.25f); ArrayList oCoords = new ArrayList( ); oCoords.add(p1); oCoords.add(p2); oCoords.add(p3); oCoords.add(p4); floorBG.addChild( new ColouredTiles(oCoords, medRed) ); |
該方格的中心在XZ平面的(0,0)處并且在y軸稍微向上(+0.01單位)的地方,這樣就可以在瓷磚上看到它。
該方格的每一邊的長度為0.5單位。在ArrayList中的四個Point3f點以逆時針方向存儲。對于在blueCoords和greenCoords中的每一組的四個點都是如此。圖6顯示出方格中的點的順序。
圖6.從上方看上去的OrigMarker |
二、 著色的瓷磚
我的ColouredTiles類擴展了Shape3D并用相同的顏色定義瓷磚的幾何體和外觀。該幾何體使用一個Java 3D QuadArray來描述瓷磚為一系列的四邊形。其構造器是:
QuadArray(int vertexCount, int vertexFormat); |
vertexFormat是一個靜態整數集合-它指定稍后被初始化的該四邊形的各種信息如坐標、顏色和法線。在ColouredTiles中,QuadArray平面是用下面一行代碼創建的:
plane=new QuadArray(coords.size(),GeometryArray.COORDINATES|GeometryArray.COLOR_3); |
size()方法返回在ArrayList中的坐標數目。坐標和顏色數據是在createGeometry()中提供的:
int numPoints=coords.size(); Point3f[] points=new Point3f[numPoints]; coords.toArray(points);//ArrayList->數組 plane.setCoordinates(0,points); Color3f cols[]=new Color3f[numPoints]; for(int i=0;i<numPoints;i++) cols[i]=col; plane.setColors(0,cols); |
一個四邊形的坐標順序的正確指定是極為重要的。一個多邊形的前面是指頂點形成一個逆時針方向環時的那一面。區別開前面與后面對于光線和隱藏面的選擇是非常重要的;并且默認情況下,在一個場景中只有多邊形的前面是可見的。在這個應用程序中,瓷磚是有向的-它們的前面朝向y軸。
必須確保一個凸的平面多邊形的每個四邊形中的頂點可以是折衷的(compromised)。然而,在坐標數組中的每個四邊形不需要連接到鄰近其它的四邊形-正好適于表達瓷磚。既然一個四邊形的幾何體不包括法線信息,那么一個Material結點組件不可能被用來指定該四邊形的在照亮時的顏色。我可以使用一個ColoringAttributes,但是第三種選擇是設置幾何體中的顏色,例如使用語句"plane.setColors(0,cols);"。這個顏色將固定不變-不受場景中燈光的影響。
一旦完成,用下列語句設置Shape3D的幾何體:
setGeometry(plane); |
形狀的外觀是由createAppearance()管理的,這個方法使用一個Java 3D PolygonAttribute組件來隱藏后面的顯示。PolygonAttribute可能被用于以點或線形式生成多邊形(也就是以線框架形式),并且用于翻轉后面形狀的法線:
Appearance app=new Appearance(); PolygonAttributes pa=new PolygonAttributes(); pa.setCullFace(PolygonAttributes.CULL_NONE); app.setPolygonAttributes(pa); |
一旦外觀全部指定,它就被用下列語句固定在形狀中:
setAppearance(app); |
三、 地板的軸標簽
地板的軸標簽是在CheckerFloor()中用labelAxes()和makeText()方法生成的。labelAxes()使用兩個循環來沿著x和z軸創建標簽。每個標簽是由makeText()構建的,然后被添加到地板的BranchGroup(見圖5):
floorBG.addChild(makeText(pt,""+i)); |
makeText()使用Text2D工具類創建一個2D串來指定顏色、字體、點大小及字體風格:
Text2D message=new Text2D(text,white,"SansSerif",36,Font.BOLD); //36點粗體的Sans Serif |
Text2D對象是一個具有一個四邊形幾何體(一矩形)的Shape3D對象,并且外觀是由一個字符串表達的材質貼圖(圖像)來確定的-貼圖被放在前面。默認地,后面是隱藏的;如果用戶移動到軸標簽的后面,那么對象成為不可見的。
點大小被轉換成虛擬世界單位-方法把其是與256相除。通常,在Text2D()構造器使用太大的點是一種糟糕的主意,因為這有可能導致文字的不正確著色。我建議把一個TransformGroup放置在形狀上方并且把它縮放到必要的大小。
每個標簽的放置是由在形狀上方的一個TransformGroup來實現的:
TransformGroup tg=new TransformGroup( ); Transform3D t3d=new Transform3D(); t3d.setTranslation(vertex);//標簽的位置 tg.setTransform(t3d); tg.addChild(message); |
setTranslation()僅僅影響該形狀的位置。TransformGroup tg被添加到地板場景圖。
四、 觀察者位置
圖3中的場景圖并不包括視圖分支圖;該分支圖顯示在圖7中。
圖7.視圖分支圖 |
這個分支是通過在WrapCheckers3D()構造器中調用SimpleUniverse構造器構建的:
su=new SimpleUniverse(canvas3D); |
SimpleUniverse提供到視圖分支圖的簡化存取-這是經由ViewingPlatform和Viewer類實現的,而且這兩個類被映射到圖上(在圖7中顯示為用點畫線繪制的矩形)。
ViewingPlatform被用在initUserPosition()中以存取ViewPlatform結點上方的TransformGroup:
ViewingPlatform vp=su.getViewingPlatform(); TransformGroup steerTG=vp.getViewPlatformTransform(); |
steerTG相應于圖7中的TG結點。它的Transform3D組件分別用lookAt()和invert()方法加以提取和改變:
Transform3D t3d=new Transform3D(); steerTG.getTransform(t3d); t3d.lookAt( USERPOSN,new Point3d(0,0,0),new Vector3d(0,1,0)); t3d.invert(); steerTG.setTransform(t3d); |
lookAt()是在虛擬的世界中設置觀察者位置的一種方便的方法。這個方法需要觀察者的所在位置、他的觀察點和一個指定向上方向的矢量。在這個應用程序中,觀察者的位置是USERPOSN((0,5,20)坐標);他向著原點(0,0,0)觀看,并且沿著正向y軸向上看。這可以由圖8展示。
圖8.lookAt()的圖形描述 |
既然位置是相對于觀察者而不是相對于場景中的一個對象,所以必須調用invert()。
五、 觀察者移動
用戶能夠通過場景移動-利用視圖圖中的Java 3D OrbitBehavior工具類實現。觀察者的位置是通過控制鍵和鼠標按鈕結合方式完成移動和旋轉的。
行為是在WrapCheckers3D中的orbitControls()方法中建立的:
OrbitBehavior orbit = new OrbitBehavior(c, OrbitBehavior.REVERSE_ALL); orbit.setSchedulingBounds(bounds); ViewingPlatform vp = su.getViewingPlatform( ); vp.setViewPlatformBehavior(orbit); |
REVERSE_ALL標志保證觀察點的移動沿著與鼠標一樣的方向。
提示: 還有其它許多標志和方法影響旋轉、平移和縮放特性,詳見OrbitBehavior類文檔的有關解釋。
MouseRotate、MouseTranslate和MouseZoom是一些經常出現在許多Java 3D例子中的相類似的行為類;它們與OrbitBehavior的主要的差別是它們影響場景中的對象而不是影響觀察者。
提示: 大多數游戲,例如第一人稱射手(FPS),要求較強地控制觀察者的移動,甚至超出這些工具行為能提供的功能;因此我將在后面實現我自己的行為。
六、 觀看場景圖
本文中已經使用場景圖來展示所討論的編碼技術,而場景圖是一個理解(和檢查)代碼的相當有用的方法。
我使用Daniel Selman的Java3dTree包來幫助我實現繪圖。它創建一個Jframe-它用一個文本樹來描述場景圖(圖9)。
圖9.Checkers3D場景圖的Java3dTree描述 |
該樹(一個JTree對象)在開始時是最小化的,并且可以通過點擊子文件夾圖標來檢查分支。當前選擇結點信息出現在底部窗口中。該包包含在j3dtree.jar中,它是從http://www.manning.com/selman/下載的源代碼(Selman的"Java 3D編程文本")的一部分。
擴充代碼來生成JTree是簡單的。為了顯示JFrame樹,WrapCheckers3D必須導入j3dtree包并且聲明一個全局變量:
import com.sun.j3d.utils.behaviors.vp.*; private Java3dTree j3dTree; |
由WrapCheckers3D()構造器創建j3dTree對象:
public WrapCheckers3D(){ //另外的代碼 su = new SimpleUniverse(canvas3D); j3dTree = new Java3dTree( );//為SG創建一個顯示樹 createSceneGraph( ); initUserPosition( ); orbitControls(canvas3D); su.addBranchGraph( sceneBG ); j3dTree.updateNodes( su );//構建樹顯示窗口 } |
在完成場景圖之后(也就是,在構造器的最后),該樹的顯示是用一行代碼實現的:
j3dTree.updateNodes( su); |
然而,在這之前,必須調整場景圖結點的能力:
j3dTree.recursiveApplyCapability(sceneBG); |
應該在完成內容分支組(sceneBG)之后而編譯或使其成為現場的之前執行這個操作。在我的代碼中,這意味著在createSceneGraph()中添加一行代碼:
private void ceateSceneGraph(){ sceneBG = new BranchGroup(); //創建場景的其它代碼 j3dTree.recursiveApplyCapability( sceneBG ); sceneBG.compile( ); } |
不幸的是,你不能僅調用:
j3dTree.recursiveApplyCapability(su); |
在此,沒有產生錯誤-因為SimpleUniverse()構造器已經使得ViewingPlatform成為現場的,它可以防止進一步改變它的能力。
既然只有內容分支的能力得到調整,那么當遇到在Locale結點下的視圖分支時,對updateNodes()的調用將生成一些警告消息。
注意 在編譯和執行中必須把j3dtree.jar包括在classpath中。我比較喜歡的方式是用命令行參數來實現:
javac -classpath "%CLASSPATH%;j3dtree.jar" *.java java -cp "%CLASSPATH%;j3dtree.jar" Checkers3D |
提示: 如果重復地輸入classpath不適合你,象上面這樣的命令行可以被隱藏在批文件或外殼腳本內部。
Java3dTree對象是一個文本方式的場景描述,這意味著我們必須自己繪制場景圖。但其優點是樹的生成對于程序的其它部分所產生的影響可以忽略。
另一種方法是使用Java 3D場景圖編輯器(http://java3d.netbeans.org/j3deditor_intro.html)。這里顯示場景圖的一個圖形化的版本但是也有其負面-其安裝和用法是復雜的并且內存要求對于一些機器來說可能很苛刻。