Now that we have the terrain (or the actual level that we will be interacting with), we need other “fluff” objects to make it feel like the world is large around us. Because the terrain is finite, we need a way to keeping the players contained within our little area and do so in a reasonable way. So, this tutorial, we will be building a force-field fence, that defines the play field. Perhaps our players are prisoners force to play for the amusement of a corrupt warden? This will create the feeling that there is a reason we can't keep going on forever over the terrain. Secondly, we want to make it feel like there is a world outside of this fence. We are going to do this with a Skybox. The skybox will create the illusion of a large world. Let's get started.
There are a couple changes to the code that are not related to the environment that I would like to mention. Since, I am writing a game from scratch and writing the tutorials as I do it, there are bound to be little things that need adjusting as time goes on. Rest assured, I will always cover any changes here.
First, I changed the camera settings for this test (future tests will have inputs, so this should be the last time). However, because I'm adding a fairly large piece of Geometry I wanted to be able to view it clearly. To do this, I move the camera back, up and looking down at an angle. You'll notice these changes as:
Vector3f loc = new Vector3f(250.0f, 100.0f, 250.0f); Vector3f left = new Vector3f(-0.5f, 0.0f, 0.5f); Vector3f up = new Vector3f(0.0f, 1.0f, 0.0f); Vector3f dir = new Vector3f(-0.5f, 0.0f, -0.5f);
Secondly, our window has been missing the title. We certainly want to put our title on the screen! To do this, we tell the DisplaySystem what our title is going to be. This is added to the top of initGame.
display.setTitle("Flag Rush");
Another slight clean up is I removed the terrain attaching code from initGame and put it into buildTerrain.
Last, but not least, I adjusted the scale of the terrain. Since I'm doing this game in a non-traditional format (tutorial) I need to be able to do things slightly unconventionally (so that I can show the underlying details). As such, this requires me to make little adjustments to objects so that they “fit” together better. In this case, the size of my fence just wasn't working out, so I needed to adjust the terrain to its level. The new terrain scale line is:
Vector3f terrainScale = new Vector3f(4, 0.0575f, 4);
Yes, to come up with this number was purely trial and error. I reduced the height variation because it was just too bumpy. Now that those changes are made you should see something like the following:
Now, we are ready to get started!
We are now going to be adding multiple objects to the scene. If we aren't careful, objects will draw “over” other objects that shouldn't. What we want is objects that are in front of another object should paint over it. There are two things we are going to do to handle proper rendering order, RenderQueue and ZBufferState. I will be talking about ZBufferState (or depth buffer) first, and save the RenderQueue when we handle transparencies.
The depth buffer allows OpenGL to maintain depth information for each pixel. That is, a color value is assigned to a pixel, OpenGL can know how far away from the camera that pixel is supposed to be and decide whether or not to overwrite it with another object's pixel. In our case, we want pixels to be overwritten if the incoming pixel is less or equal distance away as the current pixel. We will do this by creating a ZBufferState and assigning it to the root of the scene graph.
We will add it to the initGame method.
ZBufferState buf = display.getRenderer().createZBufferState(); buf.setEnabled(true); buf.setFunction(ZBufferState.TestFunction.LessThanOrEqualTo); scene.setRenderState(buf);
That it. Now, anything added to the scene will have to have a distance less than or equal to the current pixel before it is allowed to render to the buffer. This will keep our fence from being behind the terrain.
This is the big section, we are going to build the fence and do so by hand in code. Normally, you'd simply load a model that represented the fence and place it on the terrain. However, for the sake of the tutorial, I want to show how to load various Shapes and arrange them into a single Node.
The fence is going to have the following parameters:
All this code will be in a new method called buildEnvironment and called from the initGame method.
I've decided that I am going to create cylindrical towers at each corner of the fence. This means four towers. How should I create a cylindrical object?? Well, Cylinder certainly sounds good. However, instead of creating four cylinders and creating four reprensentations of the same data, I'm only going to create one. I'll then rotate this Cylinder to be facing vertical (by default the Cylinder is laying on its side). After this, I will make four copies of it using SharedMesh. SharedMesh allows me to use the same Geometry data but place them in seperate areas of the scene. This saves on both memory (only one set of data) and rendering time (only sending the data once).
Because I am using SharedMesh I should not add the original Cylinder Geometry to the scene. This is because the SharedMesh manipulates the original's local data during a rendering phase.
So, I decided that the fence should be 32 units long on a side. Why did I choose 32? No particular reason, it just seemed reasonable at the time. I then made the Cylinder 10 units high. Again, no reason, just trial and error. So, to place each tower 32 units apart from each other, I set their localTranslation to the proper spot.
/* Edited by Nader to suit JME 2.1+*/ //This cylinder will act as the four main posts at each corner Cylinder postGeometry = new Cylinder("post", 10, 10, 1, 10); //Declare a quaternion so that we could use it for rotations with respect to axis. Quaternion q = new Quaternion(); //rotate the cylinder to be vertical , that is why we have to rotate it with 90 deg(PI/2) around the x-axis q.fromAngleAxis(FastMath.PI/2, new Vector3f(1,0,0)); postGeometry.setModelBound(new BoundingBox()); postGeometry.updateModelBound(); //NOTE: do not set the local properties now (translation, rotation, etc) , because for some reason, JME 2.1 //maybe had something changed with the SharedMesh object and seems that it is not basing the targeted objects properties off the //primary's //We will share the post 4 times (one for each post) //It is *not* a good idea to add the original geometry //as the sharedmeshes will alter its local values. //We then translate the posts into position and rotate them accordingly to the quaternion. //Magic numbers are bad, but help illustrate the point.:) SharedMesh post1 = new SharedMesh("post1", postGeometry); post1.setLocalRotation(q); //sets the rotation for post1 to be rotated 90 Deg around the x-axis. post1.setLocalTranslation(new Vector3f(0,0.5f,0)); SharedMesh post2 = new SharedMesh("post2", postGeometry); post2.setLocalRotation(q); post2.setLocalTranslation(new Vector3f(32,0.5f,0)); SharedMesh post3 = new SharedMesh("post3", postGeometry); post3.setLocalRotation(q); post3.setLocalTranslation(new Vector3f(0,0.5f,32)); SharedMesh post4 = new SharedMesh("post4", postGeometry); post4.setLocalRotation(q); post4.setLocalTranslation(new Vector3f(32,0.5f,32));
We now have four towers and they are in the correct location. Now, I want to organize these towers a bit more. They will be using the same texture, and therefore, I want to add them to a single grouping and apply the TextureState to the parent of this grouping.
//put all the posts into a tower node Node towerNode = new Node("tower"); towerNode.attachChild(post1); towerNode.attachChild(post2); towerNode.attachChild(post3); towerNode.attachChild(post4); //load a texture for the towers TextureState ts2 = display.getRenderer().createTextureState(); Texture t2 = TextureManager.loadTexture(Lesson4.class.getClassLoader() .getResource("jmetest/data/texture/post.jpg"), Texture.MinificationFilter.BilinearNearestMipMap, Texture.MagnificationFilter.Bilinear); ts2.setTexture(t2); towerNode.setRenderState(ts2);
I can now manipulate all four towers at once (move them, scale them, etc) by controlling the tower node.
Now, we are ready to build the remaining parts of the fence. We will build a “strut” like system using Cylinders as well. One interesting note about these struts are two will need to be turned 90 degrees. To do this we are going to use a Quaternion. I won't go into detail about what a Quaternion is (you can read about it in the User's Guide), but we make use of its fromAngleAxis method to rotate the two struts. Again, the four struts are actually SharedMesh objects. And just like the previous towers, they are attached to a Node and have their own texture assigned.
I adjust the translation of the struts to place them high on the towers. Again, nothing special here, just trial and error and finding out what looks good. These kind of arbitrary numbers is what you get when creating objects by hand rather than using a modeler.
//This cylinder will be the horizontal struts that hold //the field in place. Cylinder strutGeometry = new Cylinder("strut", 10,10, 0.125f, 32); strutGeometry.setModelBound(new BoundingBox()); strutGeometry.updateModelBound(); //again, we'll share this mesh. //Some we need to rotate to connect various posts. SharedMesh strut1 = new SharedMesh("strut1", strutGeometry); Quaternion rotate90 = new Quaternion(); rotate90.fromAngleAxis(FastMath.PI/2, new Vector3f(0,1,0)); strut1.setLocalRotation(rotate90); strut1.setLocalTranslation(new Vector3f(16,3f,0)); SharedMesh strut2 = new SharedMesh("strut2", strutGeometry); strut2.setLocalTranslation(new Vector3f(0,3f,16)); SharedMesh strut3 = new SharedMesh("strut3", strutGeometry); strut3.setLocalTranslation(new Vector3f(32,3f,16)); SharedMesh strut4 = new SharedMesh("strut4", strutGeometry); strut4.setLocalRotation(rotate90); strut4.setLocalTranslation(new Vector3f(16,3f,32)); //put all the struts into a single node. Node strutNode = new Node("strutNode"); strutNode.attachChild(strut1); strutNode.attachChild(strut2); strutNode.attachChild(strut3); strutNode.attachChild(strut4); //load a texture for the struts TextureState ts3 = display.getRenderer().createTextureState(); Texture t3 = TextureManager.loadTexture(Lesson4.class.getClassLoader() .getResource("jmetest/data/texture/rust.jpg"), Texture.MinificationFilter.BilinearNearestMipMap, Texture.MagnificationFilter.Bilinear); ts3.setTexture(t3); strutNode.setRenderState(ts3);
Now, with the struts and towers taken care of, we have the metal structure of the fence taken care of. Now this fence is powering a high tech force field that allows nothing past it. This force field will be a flat object that seems to hang from the struts. We will use a Box for the forcefield with a futuristic texture. I won't go into extreme detail about this section as again it makes use of SharedMesh and it's attached to its own Node. One interesting item is I didn't rotate two of the boxes like I did the struts. Instead, I made use of Box's constructor to build two differently shaped boxes that fit my needs. There is no real reason for this other than to show you how the Box shape can differ.
//Create the actual forcefield //The first box handles the X-axis, the second handles the z-axis. //We don't rotate the box as a demonstration on how boxes can be //created differently. Box forceFieldX = new Box("forceFieldX", new Vector3f(-16, -3f, -0.1f), new Vector3f(16f, 3f, 0.1f)); forceFieldX.setModelBound(new BoundingBox()); forceFieldX.updateModelBound(); //We are going to share these boxes as well SharedMesh forceFieldX1 = new SharedMesh("forceFieldX1",forceFieldX); forceFieldX1.setLocalTranslation(new Vector3f(16,0,0)); SharedMesh forceFieldX2 = new SharedMesh("forceFieldX2",forceFieldX); forceFieldX2.setLocalTranslation(new Vector3f(16,0,32)); //The other box for the Z axis Box forceFieldZ = new Box("forceFieldZ", new Vector3f(-0.1f, -3f, -16), new Vector3f(0.1f, 3f, 16)); forceFieldZ.setModelBound(new BoundingBox()); forceFieldZ.updateModelBound(); //and again we will share it SharedMesh forceFieldZ1 = new SharedMesh("forceFieldZ1",forceFieldZ); forceFieldZ1.setLocalTranslation(new Vector3f(0,0,16)); SharedMesh forceFieldZ2 = new SharedMesh("forceFieldZ2",forceFieldZ); forceFieldZ2.setLocalTranslation(new Vector3f(32,0,16)); //add all the force fields to a single node Node forceFieldNode = new Node("forceFieldNode"); forceFieldNode.attachChild(forceFieldX1); forceFieldNode.attachChild(forceFieldX2); forceFieldNode.attachChild(forceFieldZ1); forceFieldNode.attachChild(forceFieldZ2); //load a texture for the force field elements TextureState ts = display.getRenderer().createTextureState(); Texture t = TextureManager.loadTexture(Lesson4.class.getClassLoader() .getResource("jmetest/data/texture/reflector.jpg"), Texture.MinificationFilter.BilinearNearestMipMap, Texture.MagnificationFilter.Bilinear); t.setWrap(Texture.WrapMode.Repeat); t.setTranslation(new Vector3f()); ts.setTexture(t); forceFieldNode.setRenderState(ts);
We now have three nodes that contain all the Geometry of the fence. To future make things easier, we will add these three Nodes to a single Node. We can then attach this Node to the scene and we have our fence in place.
Node forceFieldFence = new Node("fence"); //Attach all the pieces to the main fence node forceFieldFence.attachChild(forceFieldNode); forceFieldFence.attachChild(towerNode); forceFieldFence.attachChild(strutNode); scene.attachChild(forceFieldFence);
Everything is great, but the force field sure doesn't seem like much of a force field. There are two things I want to do to improve that: Transparency and animation.
I want to make the force field transparent. That is, I want to be able to see through to the other side, but still make out the particles. To do this we set an AlphaState to the node containing the boxes that make up the forcefield. We are going to attach the AlphaState as:
//Add the alpha values for the transparent node BlendState as1 = DisplaySystem.getDisplaySystem().getRenderer().createBlendState(); as1.setBlendEnabled(true); as1.setSourceFunction(BlendState.SourceFunction.SourceAlpha); as1.setDestinationFunction(BlendState.DestinationFunction.One); as1.setTestEnabled(true); as1.setTestFunction(BlendState.TestFunction.GreaterThan); as1.setEnabled(true); forceFieldNode.setRenderState(as1);
This allows the color of the texture to blend with the color behind it. Because our force-field texture is made up of a lot of black that will be 100% transparent. So, we now have hovering dots.
We still have a problem though. If we were able to move around the map with the current set up, we would be able to see objects behind the force-field sometimes, but not all the time. That is because transparent objects depend on the other objects that you can see behind it to already have been drawn. But our current rendering order is not guaranteed to give us this. This is where RenderQueue comes to the rescue. We can put all our scene elements into this queue and it will order them properly for us. There are three major queues (there are others, but not important for our purposes): Transparent queue is where we will put the force field. Opaque is where we will place non-transparent objects, and Ortho is where GUI elements (in future lessons) will go. The RenderQueue optimizes the order in the following way:
So, we are going to place all our Geometry into these queues:
forceFieldNode.setRenderQueueMode(Renderer.QUEUE_TRANSPARENT); towerNode.setRenderQueueMode(Renderer.QUEUE_OPAQUE); strutNode.setRenderQueueMode(Renderer.QUEUE_OPAQUE);
and in buildTerrain:
tb.setRenderQueueMode(Renderer.QUEUE_OPAQUE);
We can set the parent of the actual Geometry because by default they will inherit the mode. Now, when we render, the transparent forcefield will look correct from all sides.
The force-field still doesn't look perfect. I want the dots to appears to be falling from the struts, like they are constantly being generated. Let's animate the texture.
Animating the texture is actually quite easy. First, we are going to need access to the Texture object that contains the force-field texture. Move that reference to the class level.
We can now access the Texture in the update method.
Every texture has its own matrix. This texture matrix defines how the texture coordinates should be applied. You can rotate textures, scale them, and translate them. In our case, we want to translate along the Y-axis. It's really as easy as it sounds:
//We will use the interpolation value to keep the speed //of the forcefield consistent between computers. //we update the Y have of the texture matrix to give //the appearance the forcefield is moving. t.getTranslation().y += 0.3f * interpolation; //if the translation is over 1, it's wrapped, so go ahead //and check for this (to keep the vector's y value from getting //too large.) if(t.getTranslation().y > 1) { t.getTranslation().y = 0; }
Once this is done, we have something that truley looks like a powerful fence.
Now, we want to place this fence on the terrain. I want to make sure of a couple of things. First, I want it to be in the terrain a bit, that way when a player reaches the fence they don't see “off” the terrain, and second it needs to be high enough and large enough to be above the terrain.
This is where I just had to play around with things until I got the result I was looking for. I found that scaling the fence by a factor of 5 gave me the size I wanted. I then moved it in by 25 units on both the X and Z axis. To raise it to the appropriate height, I obtained the height at 25,25 from the TerrainBlock, and added a little error buffer.
//we will do a little 'tweaking' by hand to make it fit in the terrain a bit better. //first we'll scale the entire "model" by a factor of 5 forceFieldFence.setLocalScale(5); //now let's move the fence to to the height of the terrain and in a little bit. forceFieldFence.setLocalTranslation(new Vector3f(25, tb.getHeight(25,25) + 15, 25));
Ok, we now have a wonderful fence keeping our players from leaping off the world and falling to their deaths. We still see blackness whereever the terrain isn't, however. This really ruins the perception that we are on a large world. What we can do is give the illusion of the size of the world with a SkyBox. This will wrap the camera with a box that has images representing the world around us. This box will move with the camera as you should never actually get closer to the horizon.
Create a new method called createSkybox and create a class variable skybox. We want this to be a class instance because we are going to have to update it as the camera moves.
Skybox handles all its own internal state. This makes it quite a bit different than most other scene elements. So, in this case all we do is create a new Skybox object and set its textures. We will use the textures that is contained in the jmetest area, and used for the other tests. I could have found my own and made Flag Rush original, but I'm a bit lazy. Maybe later.
private void buildSkyBox() { skybox = new Skybox("skybox", 10, 10, 10); Texture north = TextureManager.loadTexture( TestSkybox.class.getClassLoader().getResource( "jmetest/data/texture/north.jpg"), Texture.MinificationFilter.BilinearNearestMipMap, Texture.MagnificationFilter.Bilinear); Texture south = TextureManager.loadTexture( TestSkybox.class.getClassLoader().getResource( "jmetest/data/texture/south.jpg"), Texture.MinificationFilter.BilinearNearestMipMap, Texture.MagnificationFilter.Bilinear); Texture east = TextureManager.loadTexture( TestSkybox.class.getClassLoader().getResource( "jmetest/data/texture/east.jpg"), Texture.MinificationFilter.BilinearNearestMipMap, Texture.MagnificationFilter.Bilinear); Texture west = TextureManager.loadTexture( TestSkybox.class.getClassLoader().getResource( "jmetest/data/texture/west.jpg"), Texture.MinificationFilter.BilinearNearestMipMap, Texture.MagnificationFilter.Bilinear); Texture up = TextureManager.loadTexture( TestSkybox.class.getClassLoader().getResource( "jmetest/data/texture/top.jpg"), Texture.MinificationFilter.BilinearNearestMipMap, Texture.MagnificationFilter.Bilinear); Texture down = TextureManager.loadTexture( TestSkybox.class.getClassLoader().getResource( "jmetest/data/texture/bottom.jpg"), Texture.MinificationFilter.BilinearNearestMipMap, Texture.MagnificationFilter.Bilinear); skybox.setTexture(Skybox.Face.North, north); skybox.setTexture(Skybox.Face.West, west); skybox.setTexture(Skybox.Face.South, south); skybox.setTexture(Skybox.Face.East, east); skybox.setTexture(Skybox.Face.Up, up); skybox.setTexture(Skybox.Face.Down, down); skybox.preloadTextures(); skybox.updateRenderState(); // scene.attachChild(skybox); }
We then tell Skybox to initialize the textures by calling preloadTextures. Last, add it to the scene as a child.
We are almost done, next we need to tell it to set its localTranslation to the position of our cam object. Go to the update method and add:
//we want to keep the skybox around our eyes, so move it with //the camera skybox.setLocalTranslation(cam.getLocation()); //Because we are changing the scene (moving the skybox) we need to update //the graph. scene.updateGeometricState(interpolation, true);
This will move the Skybox with the camera as it moves. That's it. It was that simple to add the world! We now have a complete level that looks like:
We now have a complete level, with terrain, a fence and the sky. This gives us a viable place to place our game objects and start racing around.
Next lesson I will discuss how to make use of a Third Person Handler to control a vehicle. We are that much closer to racing around.
/* * Copyright (c) 2003-2008 jMonkeyEngine * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * * Neither the name of 'jMonkeyEngine' nor the names of its contributors * may be used to endorse or promote products derived from this software * without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ package jmetest.flagrushtut; import java.util.logging.Level; import java.util.logging.Logger; import javax.swing.ImageIcon; import jmetest.renderer.TestSkybox; import jmetest.terrain.TestTerrain; import com.jme.app.BaseGame; import com.jme.bounding.BoundingBox; import com.jme.image.Texture; import com.jme.input.KeyBindingManager; import com.jme.input.KeyInput; import com.jme.light.DirectionalLight; import com.jme.math.FastMath; import com.jme.math.Quaternion; import com.jme.math.Vector3f; import com.jme.renderer.Camera; import com.jme.renderer.ColorRGBA; import com.jme.renderer.Renderer; import com.jme.scene.Node; import com.jme.scene.SharedMesh; import com.jme.scene.Skybox; import com.jme.scene.shape.Box; import com.jme.scene.shape.Cylinder; import com.jme.scene.state.BlendState; import com.jme.scene.state.LightState; import com.jme.scene.state.TextureState; import com.jme.scene.state.ZBufferState; import com.jme.system.DisplaySystem; import com.jme.system.JmeException; import com.jme.util.TextureManager; import com.jme.util.Timer; import com.jme.util.geom.Debugger; import com.jmex.terrain.TerrainBlock; import com.jmex.terrain.util.MidPointHeightMap; import com.jmex.terrain.util.ProceduralTextureGenerator; public class Lesson4 extends BaseGame { private static final Logger logger = Logger.getLogger(Lesson4.class .getName()); // the terrain we will drive over. private TerrainBlock tb; // The texture that makes up the "force field", we will keep a reference to it // here to allow us to animate it. private Texture t; //Sky box (we update it each frame) private Skybox skybox; //the timer protected Timer timer; // Our camera object for viewing the scene private Camera cam; // the root node of the scene graph private Node scene; // display attributes for the window. We will keep these values // to allow the user to change them private int width, height, depth, freq; private boolean fullscreen; /** * Main entry point of the application */ public static void main(String[] args) { Lesson4 app = new Lesson4(); // We will load our own "fantastic" Flag Rush logo. Yes, I'm an artist. app.setConfigShowMode(ConfigShowMode.AlwaysShow, Lesson4.class .getClassLoader().getResource( "jmetest/data/images/FlagRush.png")); app.start(); } /** * During an update we look for the escape button and update the timer * to get the framerate. Things are now starting to happen, so we will * update * * @see com.jme.app.BaseGame#update(float) */ protected void update(float interpolation) { // update the time to get the framerate timer.update(); interpolation = timer.getTimePerFrame(); //We will use the interpolation value to keep the speed //of the forcefield consistent between computers. //we update the Y have of the texture matrix to give //the appearance the forcefield is moving. t.getTranslation().y += 0.3f * interpolation; //if the translation is over 1, it's wrapped, so go ahead //and check for this (to keep the vector's y value from getting //too large.) if(t.getTranslation().y > 1) { t.getTranslation().y = 0; } //we want to keep the skybox around our eyes, so move it with //the camera skybox.setLocalTranslation(cam.getLocation()); // if escape was pressed, we exit if (KeyBindingManager.getKeyBindingManager().isValidCommand("exit")) { finished = true; } //Because we are changing the scene (moving the skybox) we need to update //the graph. scene.updateGeometricState(interpolation, true); } /** * draws the scene graph * * @see com.jme.app.BaseGame#render(float) */ protected void render(float interpolation) { // Clear the screen display.getRenderer().clearBuffers(); display.getRenderer().draw(scene); Debugger.drawBounds(scene, display.getRenderer()); } /** * initializes the display and camera. * * @see com.jme.app.BaseGame#initSystem() */ protected void initSystem() { // store the settings information width = settings.getWidth(); height = settings.getHeight(); depth = settings.getDepth(); freq = settings.getFrequency(); fullscreen = settings.isFullscreen(); try { display = DisplaySystem.getDisplaySystem(settings.getRenderer()); display.createWindow(width, height, depth, freq, fullscreen); cam = display.getRenderer().createCamera(width, height); } catch (JmeException e) { logger.log(Level.SEVERE, "Could not create displaySystem", e); System.exit(1); } // set the background to black display.getRenderer().setBackgroundColor(ColorRGBA.black.clone()); // initialize the camera cam.setFrustumPerspective(45.0f, (float) width / (float) height, 1, 5000); Vector3f loc = new Vector3f(250.0f, 100.0f, 250.0f); Vector3f left = new Vector3f(-0.5f, 0.0f, 0.5f); Vector3f up = new Vector3f(0.0f, 1.0f, 0.0f); Vector3f dir = new Vector3f(-0.5f, 0.0f, -0.5f); // Move our camera to a correct place and orientation. cam.setFrame(loc, left, up, dir); /** Signal that we've changed our camera's location/frustum. */ cam.update(); /** Get a high resolution timer for FPS updates. */ timer = Timer.getTimer(); display.getRenderer().setCamera(cam); KeyBindingManager.getKeyBindingManager().set("exit", KeyInput.KEY_ESCAPE); } /** * initializes the scene * * @see com.jme.app.BaseGame#initGame() */ protected void initGame() { display.setTitle("Flag Rush"); scene = new Node("Scene graph node"); /** Create a ZBuffer to display pixels closest to the camera above farther ones. */ ZBufferState buf = display.getRenderer().createZBufferState(); buf.setEnabled(true); buf.setFunction(ZBufferState.TestFunction.LessThanOrEqualTo); scene.setRenderState(buf); //Add terrain to the scene buildTerrain(); //Light the world buildLighting(); //add the force field fence buildEnvironment(); //Add the skybox buildSkyBox(); // update the scene graph for rendering scene.updateGeometricState(0.0f, true); scene.updateRenderState(); } /** * buildEnvironment will create a fence. This is done by hand * to show how to create geometry and shared this geometry. * Normally, you wouldn't build your models by hand as it is * too much of a trial and error process. */ private void buildEnvironment() { //This is the main node of our fence Node forceFieldFence = new Node("fence"); //This cylinder will act as the four main posts at each corner Cylinder postGeometry = new Cylinder("post", 10, 10, 1, 10); Quaternion q = new Quaternion(); //rotate the cylinder to be vertical q.fromAngleAxis(FastMath.PI/2, new Vector3f(1,0,0)); // postGeometry.setLocalRotation(q); (obsolete) postGeometry.setModelBound(new BoundingBox()); postGeometry.updateModelBound(); //We will share the post 4 times (one for each post) //It is *not* a good idea to add the original geometry //as the sharedmeshes will alter its local values. //We then translate the posts into position. //Magic numbers are bad, but help illustrate the point.:) SharedMesh post1 = new SharedMesh("post1", postGeometry); post1.setLocalRotation(q); //sets the rotation for post1 to be rotated 90 Deg around the x-axis. post1.setLocalTranslation(new Vector3f(0,0.5f,0)); SharedMesh post2 = new SharedMesh("post2", postGeometry); post2.setLocalRotation(q); post2.setLocalTranslation(new Vector3f(32,0.5f,0)); SharedMesh post3 = new SharedMesh("post3", postGeometry); post3.setLocalRotation(q); post3.setLocalTranslation(new Vector3f(0,0.5f,32)); SharedMesh post4 = new SharedMesh("post4", postGeometry); post4.setLocalRotation(q); post4.setLocalTranslation(new Vector3f(32,0.5f,32)); //This cylinder will be the horizontal struts that hold //the field in place. Cylinder strutGeometry = new Cylinder("strut", 10,10, 0.125f, 32); strutGeometry.setModelBound(new BoundingBox()); strutGeometry.updateModelBound(); //again, we'll share this mesh. //Some we need to rotate to connect various posts. SharedMesh strut1 = new SharedMesh("strut1", strutGeometry); Quaternion rotate90 = new Quaternion(); rotate90.fromAngleAxis(FastMath.PI/2, new Vector3f(0,1,0)); strut1.setLocalRotation(rotate90); strut1.setLocalTranslation(new Vector3f(16,3f,0)); SharedMesh strut2 = new SharedMesh("strut2", strutGeometry); strut2.setLocalTranslation(new Vector3f(0,3f,16)); SharedMesh strut3 = new SharedMesh("strut3", strutGeometry); strut3.setLocalTranslation(new Vector3f(32,3f,16)); SharedMesh strut4 = new SharedMesh("strut4", strutGeometry); strut4.setLocalRotation(rotate90); strut4.setLocalTranslation(new Vector3f(16,3f,32)); //Create the actual forcefield //The first box handles the X-axis, the second handles the z-axis. //We don't rotate the box as a demonstration on how boxes can be //created differently. Box forceFieldX = new Box("forceFieldX", new Vector3f(-16, -3f, -0.1f), new Vector3f(16f, 3f, 0.1f)); forceFieldX.setModelBound(new BoundingBox()); forceFieldX.updateModelBound(); //We are going to share these boxes as well SharedMesh forceFieldX1 = new SharedMesh("forceFieldX1",forceFieldX); forceFieldX1.setLocalTranslation(new Vector3f(16,0,0)); SharedMesh forceFieldX2 = new SharedMesh("forceFieldX2",forceFieldX); forceFieldX2.setLocalTranslation(new Vector3f(16,0,32)); //The other box for the Z axis Box forceFieldZ = new Box("forceFieldZ", new Vector3f(-0.1f, -3f, -16), new Vector3f(0.1f, 3f, 16)); forceFieldZ.setModelBound(new BoundingBox()); forceFieldZ.updateModelBound(); //and again we will share it SharedMesh forceFieldZ1 = new SharedMesh("forceFieldZ1",forceFieldZ); forceFieldZ1.setLocalTranslation(new Vector3f(0,0,16)); SharedMesh forceFieldZ2 = new SharedMesh("forceFieldZ2",forceFieldZ); forceFieldZ2.setLocalTranslation(new Vector3f(32,0,16)); //add all the force fields to a single node and make this node part of //the transparent queue. Node forceFieldNode = new Node("forceFieldNode"); forceFieldNode.setRenderQueueMode(Renderer.QUEUE_TRANSPARENT); forceFieldNode.attachChild(forceFieldX1); forceFieldNode.attachChild(forceFieldX2); forceFieldNode.attachChild(forceFieldZ1); forceFieldNode.attachChild(forceFieldZ2); //Add the alpha values for the transparent node BlendState as1 = display.getRenderer().createBlendState(); as1.setBlendEnabled(true); as1.setSourceFunction(BlendState.SourceFunction.SourceAlpha); as1.setDestinationFunction(BlendState.DestinationFunction.One); as1.setTestEnabled(true); as1.setTestFunction(BlendState.TestFunction.GreaterThan); as1.setEnabled(true); forceFieldNode.setRenderState(as1); //load a texture for the force field elements TextureState ts = display.getRenderer().createTextureState(); t = TextureManager.loadTexture(Lesson2.class.getClassLoader() .getResource("jmetest/data/texture/reflector.jpg"), Texture.MinificationFilter.Trilinear, Texture.MagnificationFilter.Bilinear); t.setWrap(Texture.WrapMode.Repeat); t.setTranslation(new Vector3f()); ts.setTexture(t); forceFieldNode.setRenderState(ts); //put all the posts into a tower node Node towerNode = new Node("tower"); towerNode.attachChild(post1); towerNode.attachChild(post2); towerNode.attachChild(post3); towerNode.attachChild(post4); //add the tower to the opaque queue (we don't want to be able to see through them) //and we do want to see them through the forcefield. towerNode.setRenderQueueMode(Renderer.QUEUE_OPAQUE); //load a texture for the towers TextureState ts2 = display.getRenderer().createTextureState(); Texture t2 = TextureManager.loadTexture(Lesson2.class.getClassLoader() .getResource("jmetest/data/texture/post.jpg"), Texture.MinificationFilter.Trilinear, Texture.MagnificationFilter.Bilinear); ts2.setTexture(t2); towerNode.setRenderState(ts2); //put all the struts into a single node. Node strutNode = new Node("strutNode"); strutNode.attachChild(strut1); strutNode.attachChild(strut2); strutNode.attachChild(strut3); strutNode.attachChild(strut4); //this too is in the opaque queue. strutNode.setRenderQueueMode(Renderer.QUEUE_OPAQUE); //load a texture for the struts TextureState ts3 = display.getRenderer().createTextureState(); Texture t3 = TextureManager.loadTexture(Lesson2.class.getClassLoader() .getResource("jmetest/data/texture/rust.jpg"), Texture.MinificationFilter.Trilinear, Texture.MagnificationFilter.Bilinear); ts3.setTexture(t3); strutNode.setRenderState(ts3); //we will do a little 'tweaking' by hand to make it fit in the terrain a bit better. //first we'll scale the entire "model" by a factor of 5 forceFieldFence.setLocalScale(new Vector3f(5,4,4)); //now let's move the fence to to the height of the terrain and in a little bit. forceFieldFence.setLocalTranslation(new Vector3f(25, tb.getHeight(25,25) + 15, 25)); //Attach all the pieces to the main fence node forceFieldFence.attachChild(forceFieldNode); forceFieldFence.attachChild(towerNode); forceFieldFence.attachChild(strutNode); scene.attachChild(forceFieldFence); } /** * creates a light for the terrain. */ private void buildLighting() { /** Set up a basic, default light. */ DirectionalLight light = new DirectionalLight(); light.setDiffuse(new ColorRGBA(1.0f, 1.0f, 1.0f, 1.0f)); light.setAmbient(new ColorRGBA(0.5f, 0.5f, 0.5f, 1.0f)); light.setDirection(new Vector3f(1,-1,0)); light.setEnabled(true); /** Attach the light to a lightState and the lightState to rootNode. */ LightState lightState = display.getRenderer().createLightState(); lightState.setEnabled(true); lightState.attach(light); scene.setRenderState(lightState); } /** * build the height map and terrain block. */ private void buildTerrain() { MidPointHeightMap heightMap = new MidPointHeightMap(64, 1f); // Scale the data Vector3f terrainScale = new Vector3f(4, 0.0575f, 4); // create a terrainblock tb = new TerrainBlock("Terrain", heightMap.getSize(), terrainScale, heightMap.getHeightMap(), new Vector3f(0, 0, 0)); tb.setModelBound(new BoundingBox()); tb.updateModelBound(); // generate a terrain texture with 2 textures ProceduralTextureGenerator pt = new ProceduralTextureGenerator( heightMap); pt.addTexture(new ImageIcon(TestTerrain.class.getClassLoader() .getResource("jmetest/data/texture/grassb.png")), -128, 0, 128); pt.addTexture(new ImageIcon(TestTerrain.class.getClassLoader() .getResource("jmetest/data/texture/dirt.jpg")), 0, 128, 255); pt.addTexture(new ImageIcon(TestTerrain.class.getClassLoader() .getResource("jmetest/data/texture/highest.jpg")), 128, 255, 384); pt.createTexture(32); // assign the texture to the terrain TextureState ts = display.getRenderer().createTextureState(); Texture t1 = TextureManager.loadTexture(pt.getImageIcon().getImage(), Texture.MinificationFilter.Trilinear, Texture.MagnificationFilter.Bilinear, true); ts.setTexture(t1, 0); tb.setRenderState(ts); tb.setRenderQueueMode(Renderer.QUEUE_OPAQUE); scene.attachChild(tb); } /** * buildSkyBox creates a new skybox object with all the proper textures. The * textures used are the standard skybox textures from all the tests. * */ private void buildSkyBox() { skybox = new Skybox("skybox", 10, 10, 10); Texture north = TextureManager.loadTexture( TestSkybox.class.getClassLoader().getResource( "jmetest/data/texture/north.jpg"), Texture.MinificationFilter.BilinearNearestMipMap, Texture.MagnificationFilter.Bilinear); Texture south = TextureManager.loadTexture( TestSkybox.class.getClassLoader().getResource( "jmetest/data/texture/south.jpg"), Texture.MinificationFilter.BilinearNearestMipMap, Texture.MagnificationFilter.Bilinear); Texture east = TextureManager.loadTexture( TestSkybox.class.getClassLoader().getResource( "jmetest/data/texture/east.jpg"), Texture.MinificationFilter.BilinearNearestMipMap, Texture.MagnificationFilter.Bilinear); Texture west = TextureManager.loadTexture( TestSkybox.class.getClassLoader().getResource( "jmetest/data/texture/west.jpg"), Texture.MinificationFilter.BilinearNearestMipMap, Texture.MagnificationFilter.Bilinear); Texture up = TextureManager.loadTexture( TestSkybox.class.getClassLoader().getResource( "jmetest/data/texture/top.jpg"), Texture.MinificationFilter.BilinearNearestMipMap, Texture.MagnificationFilter.Bilinear); Texture down = TextureManager.loadTexture( TestSkybox.class.getClassLoader().getResource( "jmetest/data/texture/bottom.jpg"), Texture.MinificationFilter.BilinearNearestMipMap, Texture.MagnificationFilter.Bilinear); skybox.setTexture(Skybox.Face.North, north); skybox.setTexture(Skybox.Face.West, west); skybox.setTexture(Skybox.Face.South, south); skybox.setTexture(Skybox.Face.East, east); skybox.setTexture(Skybox.Face.Up, up); skybox.setTexture(Skybox.Face.Down, down); skybox.preloadTextures(); scene.attachChild(skybox); } /** * will be called if the resolution changes * * @see com.jme.app.BaseGame#reinit() */ protected void reinit() { display.recreateWindow(width, height, depth, freq, fullscreen); } /** * close the window and also exit the program. */ protected void quit() { super.quit(); System.exit(0); } /** * clean up the textures. * * @see com.jme.app.BaseGame#cleanup() */ protected void cleanup() { } }