Building the LandscapeThe Landscape object is created by WrapTerra3D, and is passed a reference to the world's scene (sceneBG) and the filename supplied on the command line (e.g., test1). Landscape's primary purpose is to display a terrain composed from a mesh and a texture. Landscape looks in the models/ subdirectory for a OBJ file containing the mesh (e.g., test1.obj) and for a JPG (e.g., test1.jpg) to act as the texture. The OBJ file is loaded, becoming the landBG BranchGroup linked to sceneBG. The texture is laid over the geometry stored within landBG. Landscape can add two kinds of scenery to the terrain:
The terrain is surrounded by walls covered in a mountain range image. Loading the MeshThe Landscape( ) constructor loads the mesh, checks that the resulting Java 3D subgraph has the right characteristics, and extracts various mesh dimensions. At the end of the constructor, the land is added to the world and the texture laid over it: // globals private BranchGroup sceneBG; private BranchGroup landBG = null; private Shape3D landShape3D = null; // various mesh dimensions, set in getLandDimensions( ) private double landLength, minHeight, maxHeight; private double scaleLen; public Landscape(BranchGroup sceneBG, String fname) { loadMesh(fname); // initialize landBG getLandShape(landBG); // initialize landShape3D // set the picking capabilities so that intersection // coords can be extracted after the shape is picked PickTool.setCapabilities(landShape3D, PickTool.INTERSECT_COORD); getLandDimensions(landShape3D); // extracts sizes from landShape3D makeScenery(landBG, fname); // add any scenery addWalls( ); // walls around the landscape GroundCover gc = new GroundCover(fname); landBG.addChild( gc.getCoverBG( ) ); // add any ground cover addLandtoScene(landBG); addLandTexture(landShape3D, fname); } loadMesh( ) uses Java 3D's utility class, ObjectFile, to load the OBJ file. If the load is successful, the geometry will be stored in a TRiangleStripArray below a Shape3D node and BranchGroup. loadMesh( ) assigns this BranchGroup to the global landBG: private void loadMesh(String fname) { FileWriter ofw = null; String fn = new String("models/" + fname + ".obj"); System.out.println( "Loading terrain mesh from: " + fn +" ..." ); try { ObjectFile f = new ObjectFile( ); Scene loadedScene = f.load(fn); if(loadedScene == null) { System.out.println("Scene not found in: " + fn); System.exit(0); } landBG = loadedScene.getSceneGroup( ); // the land's BG if(landBG == null ) { System.out.println("No land branch group found"); System.exit(0); } } catch(IOException ioe) { System.err.println("Terrain mesh load error: " + fn); System.exit(0); } } getLandShape( ) checks that the subgraph below landBG has a Shape3D node and the Shape3D is holding a single GeometryArray. The Shape3D node is assigned to the landShape3D global: private void getLandShape(BranchGroup landBG) { if (landBG.numChildren( ) > 1) System.out.println("More than one child in land branch group"); Node node = landBG.getChild(0); if (!(node instanceof Shape3D)) { System.out.println("No Shape3D found in land branch group"); System.exit(0); } landShape3D = (Shape3D) node; if (landShape3D == null) { System.out.println("Land Shape3D has no value"); System.exit(0); } if (landShape3D.numGeometries( ) > 1) System.out.println("More than 1 geometry in land BG"); Geometry g = landShape3D.getGeometry( ); if (!(g instanceof GeometryArray)) { System.out.println("No Geometry Array found in land Shape3D"); System.exit(0); } } getLandDimensions( ) is called from Landscape's constructor to initialize four globals related to the size of the mesh:
The underlying assumptions are that the floor runs across the XY plane, is square, with its lower lefthand corner at (0,0), and the positive z-axis holds the height values: private void getLandDimensions(Shape3D landShape3D) { // get the bounds of the shape BoundingBox boundBox = new BoundingBox(landShape3D.getBounds( )); Point3d lower = new Point3d( ); Point3d upper = new Point3d( ); boundBox.getLower(lower); boundBox.getUpper(upper); System.out.println("lower: " + lower + "\nupper: " + upper ); if ((lower.y == 0) && (upper.x == upper.y)) { // System.out.println("XY being used as the floor"); } else if ((lower.z == 0) && (upper.x == upper.z)) { System.out.println("Error: XZ set as the floor; change to XY in Terragen"); System.exit(0); } else { System.out.println("Cannot determine floor axes"); System.out.println("Y range should == X range, and start at 0"); System.exit(0); } landLength = upper.x; scaleLen = LAND_LEN/landLength; System.out.println("scaleLen: " + scaleLen); minHeight = lower.z; maxHeight = upper.z; } // end of getLandDimensions( ) The lower and upper corners of the mesh can be obtained easily by extracting the BoundingBox for the shape. However, this approach only works correctly if the shape contains a single geometry, which is checked by getLandShape( ) before getLandDimensions( ) is called. Placing the Terrain in the WorldThe floor of the landscape runs across the XY plane, starting at (0, 0), with sides of landLength units and heights in the z-direction. The world's floor is the XZ plane, with sides of LAND_LEN units and the y-axis corresponding to up and down. Consequently, the landscape (stored in landBG) must be rotated to lie on the XZ plane and must be scaled to have floor sides of length LAND_LEN. The scaling is a matter of applying the scaleLen global, which equals LAND_LEN/landLength. In addition, the terrain is translated so the center of its floor is at (0, 0) in the world's XZ plane. These changes are illustrated by Figure 27-11. Figure 27-11. Placing the terrainHere's the relevant code: private void addLandtoScene(BranchGroup landBG) { Transform3D t3d = new Transform3D( ); t3d.rotX( -Math.PI/2.0 ); // so land's XY resting on XZ plane t3d.setScale( new Vector3d(scaleLen, scaleLen, scaleLen) ); TransformGroup sTG = new TransformGroup(t3d); sTG.addChild(landBG); // center the land, which starts at (0,0) on the XZ plane, // so move it left and forward Transform3D t3d1 = new Transform3D( ); t3d1.set( new Vector3d(-LAND_LEN/2, 0, LAND_LEN/2)); TransformGroup posTG = new TransformGroup(t3d1); posTG.addChild( sTG ); sceneBG.addChild(posTG); // add to the world } The subgraph added to sceneBG is shown in Figure 27-12. Figure 27-12. Subgraph for the landscapeAn essential point is that any nodes added to landBG will be affected by the translation, rotation, and scaling applied to the landscape. This includes the scenery nodes (i.e., the 3D models and ground cover), but the landscape walls are connected to sceneBG and aren't transformed. The principal reason for connecting nodes to landBG is so their positioning in space can utilize the local coordinate system in landBG. These are the coordinates specified in Terragen: the floor in the XY plane and heights along the z-axis. Adding Texture to the TerrainThe texture is stretched to fit the terrain stored in landShape3D below landBG. The texture coordinates (s, t), which define a unit square, must be mapped to the (x, y) coordinates of the terrain whose lower lefthand corner is at (0, 0), and the top righthand corner at landLength, landLength. The intended mapping is captured by Figure 27-13. The simplest way of doing this is define generation planes to translate (x, y) coordinates to (s, t) values. Figure 27-13. Mapping the terrain to the texture
addLandTexture( ) sets up the generation planes via a call to stampTexCoords( ). It creates an Appearance node for landShape3Dand loads the texture: private void addLandTexture(Shape3D shape, String fname) { Appearance app = shape.getAppearance( ); // generate texture coords app.setTexCoordGeneration( stampTexCoords(shape) ); // combine texture with colour and lighting of underlying surface TextureAttributes ta = new TextureAttributes( ); ta.setTextureMode( TextureAttributes.MODULATE ); app.setTextureAttributes( ta ); // apply texture to shape Texture2D tex = loadLandTexture(fname); if (tex != null) { app.setTexture(tex); shape.setAppearance(app); } } The generation planes are specified using the following equations: s = x/landLength t = y/landLength Here's the code that puts this into action: private TexCoordGeneration stampTexCoords(Shape3D shape) { Vector4f planeS = new Vector4f( (float)(1.0/landLength), 0.0f, 0.0f, 0.0f); Vector4f planeT = new Vector4f( 0.0f, (float)(1.0/landLength), 0.0f, 0.0f); // generate new texture coordinates for GeometryArray TexCoordGeneration texGen = new TexCoordGeneration( ); texGen.setPlaneS(planeS); texGen.setPlaneT(planeT); return texGen; } |