2 * Copyright (c) 2003-2009 jMonkeyEngine
5 * Redistribution and use in source and binary forms, with or without
6 * modification, are permitted provided that the following conditions are
9 * * Redistributions of source code must retain the above copyright
10 * notice, this list of conditions and the following disclaimer.
12 * * Redistributions in binary form must reproduce the above copyright
13 * notice, this list of conditions and the following disclaimer in the
14 * documentation and/or other materials provided with the distribution.
16 * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
17 * may be used to endorse or promote products derived from this software
18 * without specific prior written permission.
20 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
21 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
22 * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
23 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
24 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
25 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
26 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
27 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
28 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
29 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
30 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
33 package jmetest.flagrushtut.lesson7;
35 import java.io.BufferedInputStream;
36 import java.io.ByteArrayInputStream;
37 import java.io.ByteArrayOutputStream;
38 import java.io.IOException;
40 import java.util.HashMap;
41 import java.util.logging.Level;
42 import java.util.logging.Logger;
44 import javax.swing.ImageIcon;
46 import jmetest.renderer.TestSkybox;
47 import jmetest.terrain.TestTerrain;
49 import com.jme.app.BaseGame;
50 import com.jme.bounding.BoundingBox;
51 import com.jme.image.Texture;
52 import com.jme.input.ChaseCamera;
53 import com.jme.input.InputHandler;
54 import com.jme.input.KeyBindingManager;
55 import com.jme.input.KeyInput;
56 import com.jme.input.thirdperson.ThirdPersonMouseLook;
57 import com.jme.light.DirectionalLight;
58 import com.jme.math.FastMath;
59 import com.jme.math.Vector3f;
60 import com.jme.renderer.Camera;
61 import com.jme.renderer.ColorRGBA;
62 import com.jme.renderer.Renderer;
63 import com.jme.scene.Node;
64 import com.jme.scene.Skybox;
65 import com.jme.scene.state.CullState;
66 import com.jme.scene.state.LightState;
67 import com.jme.scene.state.TextureState;
68 import com.jme.scene.state.ZBufferState;
69 import com.jme.system.DisplaySystem;
70 import com.jme.system.JmeException;
71 import com.jme.util.TextureManager;
72 import com.jme.util.Timer;
73 import com.jme.util.export.binary.BinaryImporter;
74 import com.jmex.model.converters.MaxToJme;
75 import com.jmex.terrain.TerrainBlock;
76 import com.jmex.terrain.util.MidPointHeightMap;
77 import com.jmex.terrain.util.ProceduralTextureGenerator;
80 * <code>Tutorial 7</code> adds additional graphical pizzazz. The terrain now has a detail
81 * texture, and we are no longer driving a box around, but loading a futuristic bike model
82 * (thanks to 3D Cafe :) ). Additionally, terrain following is improved by taking the terrain
83 * normals and applying it to the bike, so it orients itself based on the slope.
87 public class Lesson7 extends BaseGame {
88 private static final Logger logger = Logger.getLogger(Lesson7.class
91 // the terrain we will drive over.
92 private TerrainBlock tb;
93 // fence that will keep us in.
94 private ForceFieldFence fence;
95 //Sky box (we update it each frame)
96 private Skybox skybox;
97 //the new player object
98 private Vehicle player;
99 //private ChaseCamera chaser;
100 protected InputHandler input;
103 protected Timer timer;
105 // Our camera object for viewing the scene
107 //The chase camera, this will follow our player as he zooms around the level
108 private ChaseCamera chaser;
110 // the root node of the scene graph
113 // display attributes for the window. We will keep these values
114 // to allow the user to change them
115 private int width, height, depth, freq;
116 private boolean fullscreen;
118 //store the normal of the terrain
119 private Vector3f normal = new Vector3f();
122 * Main entry point of the application
124 public static void main(String[] args) {
125 Lesson7 app = new Lesson7();
126 // We will load our own "fantastic" Flag Rush logo. Yes, I'm an artist.
127 app.setConfigShowMode(ConfigShowMode.AlwaysShow, Lesson7.class
128 .getClassLoader().getResource(
129 "jmetest/data/images/FlagRush.png"));
134 * During an update we look for the escape button and update the timer
135 * to get the framerate. Things are now starting to happen, so we will
138 * @see com.jme.app.BaseGame#update(float)
140 protected void update(float interpolation) {
141 // update the time to get the framerate
143 interpolation = timer.getTimePerFrame();
144 //update the keyboard input (move the player around)
145 input.update(interpolation);
146 //update the chase camera to handle the player moving around.
147 chaser.update(interpolation);
149 fence.update(interpolation);
151 //we want to keep the skybox around our eyes, so move it with
153 skybox.setLocalTranslation(cam.getLocation());
155 // if escape was pressed, we exit
156 if (KeyBindingManager.getKeyBindingManager().isValidCommand("exit")) {
160 //We don't want the chase camera to go below the world, so always keep
161 //it 2 units above the level.
162 if(cam.getLocation().y < (tb.getHeight(cam.getLocation())+2)) {
163 cam.getLocation().y = tb.getHeight(cam.getLocation()) + 2;
167 //make sure that if the player left the level we don't crash. When we add collisions,
168 //the fence will do its job and keep the player inside.
169 float characterMinHeight = tb.getHeight(player
170 .getLocalTranslation())+((BoundingBox)player.getWorldBound()).yExtent;
171 if (!Float.isInfinite(characterMinHeight) && !Float.isNaN(characterMinHeight)) {
172 player.getLocalTranslation().y = characterMinHeight;
175 //get the normal of the terrain at our current location. We then apply it to the up vector
177 tb.getSurfaceNormal(player.getLocalTranslation(), normal);
179 player.rotateUpTo(normal);
182 //Because we are changing the scene (moving the skybox and player) we need to update
184 scene.updateGeometricState(interpolation, true);
188 * draws the scene graph
190 * @see com.jme.app.BaseGame#render(float)
192 protected void render(float interpolation) {
194 display.getRenderer().clearBuffers();
195 display.getRenderer().draw(scene);
199 * initializes the display and camera.
201 * @see com.jme.app.BaseGame#initSystem()
203 protected void initSystem() {
204 // store the settings information
205 width = settings.getWidth();
206 height = settings.getHeight();
207 depth = settings.getDepth();
208 freq = settings.getFrequency();
209 fullscreen = settings.isFullscreen();
212 display = DisplaySystem.getDisplaySystem(settings.getRenderer());
213 display.createWindow(width, height, depth, freq, fullscreen);
215 cam = display.getRenderer().createCamera(width, height);
216 } catch (JmeException e) {
217 logger.log(Level.SEVERE, "Could not create displaySystem", e);
221 // set the background to black
222 display.getRenderer().setBackgroundColor(ColorRGBA.black.clone());
224 // initialize the camera
225 cam.setFrustumPerspective(45.0f, (float) width / (float) height, 1,
227 cam.setLocation(new Vector3f(200,1000,200));
229 /** Signal that we've changed our camera's location/frustum. */
232 /** Get a high resolution timer for FPS updates. */
233 timer = Timer.getTimer();
235 display.getRenderer().setCamera(cam);
237 KeyBindingManager.getKeyBindingManager().set("exit",
238 KeyInput.KEY_ESCAPE);
242 * initializes the scene
244 * @see com.jme.app.BaseGame#initGame()
246 protected void initGame() {
247 display.setTitle("Flag Rush");
249 scene = new Node("Scene graph node");
250 /** Create a ZBuffer to display pixels closest to the camera above farther ones. */
251 ZBufferState buf = display.getRenderer().createZBufferState();
252 buf.setEnabled(true);
253 buf.setFunction(ZBufferState.TestFunction.LessThanOrEqualTo);
254 scene.setRenderState(buf);
256 //Time for a little optimization. We don't need to render back face triangles, so lets
257 //not. This will give us a performance boost for very little effort.
258 CullState cs = display.getRenderer().createCullState();
259 cs.setCullFace(CullState.Face.Back);
260 scene.setRenderState(cs);
262 //Add terrain to the scene
266 //add the force field fence
272 //build the chase cam
274 //build the player input
277 // update the scene graph for rendering
278 scene.updateGeometricState(0.0f, true);
279 scene.updateRenderState();
283 * we are going to build the player object here. Now, we will load a .3ds model and convert it
284 * to .jme in realtime. The next lesson will show how to store as .jme so this conversion doesn't
285 * have to take place every time.
287 * We now have a Vehicle object that represents our player. The vehicle object will allow
288 * us to have multiple vehicle types with different capabilities.
291 private void buildPlayer() {
294 MaxToJme C1 = new MaxToJme();
295 ByteArrayOutputStream BO = new ByteArrayOutputStream();
296 URL maxFile = Lesson7.class.getClassLoader().getResource("jmetest/data/model/bike.3ds");
297 C1.convert(new BufferedInputStream(maxFile.openStream()),BO);
298 model = (Node)BinaryImporter.getInstance().load(new ByteArrayInputStream(BO.toByteArray()));
299 //scale it to be MUCH smaller than it is originally
300 model.setLocalScale(.0025f);
301 model.setModelBound(new BoundingBox());
302 model.updateModelBound();
304 //scale it to be MUCH smaller than it is originally
305 model.setLocalScale(.0025f);
306 } catch (IOException e) {
308 .throwing(this.getClass().toString(), "buildPlayer()",
312 //set the vehicles attributes (these numbers can be thought
313 //of as Unit/Second).
314 player = new Vehicle("Player Node", model);
315 player.setAcceleration(15);
316 player.setBraking(15);
317 player.setTurnSpeed(2.5f);
318 player.setWeight(25);
319 player.setMaxSpeed(25);
320 player.setMinSpeed(15);
322 player.setLocalTranslation(new Vector3f(100,0, 100));
323 scene.attachChild(player);
324 scene.updateGeometricState(0, true);
325 player.setRenderQueueMode(Renderer.QUEUE_OPAQUE);
329 * buildEnvironment will create a fence.
331 private void buildEnvironment() {
332 //This is the main node of our fence
333 fence = new ForceFieldFence("fence");
335 //we will do a little 'tweaking' by hand to make it fit in the terrain a bit better.
336 //first we'll scale the entire "model" by a factor of 5
337 fence.setLocalScale(5);
338 //now let's move the fence to to the height of the terrain and in a little bit.
339 fence.setLocalTranslation(new Vector3f(25, tb.getHeight(25,25)+10, 25));
341 scene.attachChild(fence);
345 * creates a light for the terrain.
347 private void buildLighting() {
348 /** Set up a basic, default light. */
349 DirectionalLight light = new DirectionalLight();
350 light.setDiffuse(new ColorRGBA(1.0f, 1.0f, 1.0f, 1.0f));
351 light.setAmbient(new ColorRGBA(0.5f, 0.5f, 0.5f, 1.0f));
352 light.setDirection(new Vector3f(1,-1,0));
353 light.setEnabled(true);
355 /** Attach the light to a lightState and the lightState to rootNode. */
356 LightState lightState = display.getRenderer().createLightState();
357 lightState.setEnabled(true);
358 lightState.attach(light);
359 scene.setRenderState(lightState);
363 * build the height map and terrain block.
365 private void buildTerrain() {
368 MidPointHeightMap heightMap = new MidPointHeightMap(64, 1f);
370 Vector3f terrainScale = new Vector3f(4, 0.0575f, 4);
371 // create a terrainblock
372 tb = new TerrainBlock("Terrain", heightMap.getSize(), terrainScale,
373 heightMap.getHeightMap(), new Vector3f(0, 0, 0));
375 tb.setModelBound(new BoundingBox());
376 tb.updateModelBound();
378 // generate a terrain texture with 2 textures
379 ProceduralTextureGenerator pt = new ProceduralTextureGenerator(
381 pt.addTexture(new ImageIcon(TestTerrain.class.getClassLoader()
382 .getResource("jmetest/data/texture/grassb.png")), -128, 0, 128);
383 pt.addTexture(new ImageIcon(TestTerrain.class.getClassLoader()
384 .getResource("jmetest/data/texture/dirt.jpg")), 0, 128, 255);
385 pt.addTexture(new ImageIcon(TestTerrain.class.getClassLoader()
386 .getResource("jmetest/data/texture/highest.jpg")), 128, 255,
388 pt.createTexture(32);
390 // assign the texture to the terrain
391 TextureState ts = display.getRenderer().createTextureState();
392 Texture t1 = TextureManager.loadTexture(pt.getImageIcon().getImage(),
393 Texture.MinificationFilter.Trilinear, Texture.MagnificationFilter.Bilinear, true);
394 ts.setTexture(t1, 0);
396 //load a detail texture and set the combine modes for the two terrain textures.
397 Texture t2 = TextureManager.loadTexture(
398 TestTerrain.class.getClassLoader().getResource(
399 "jmetest/data/texture/Detail.jpg"),
400 Texture.MinificationFilter.Trilinear,
401 Texture.MagnificationFilter.Bilinear);
403 ts.setTexture(t2, 1);
404 t2.setWrap(Texture.WrapMode.Repeat);
406 t1.setApply(Texture.ApplyMode.Combine);
407 t1.setCombineFuncRGB(Texture.CombinerFunctionRGB.Modulate);
408 t1.setCombineSrc0RGB(Texture.CombinerSource.CurrentTexture);
409 t1.setCombineOp0RGB(Texture.CombinerOperandRGB.SourceColor);
410 t1.setCombineSrc1RGB(Texture.CombinerSource.PrimaryColor);
411 t1.setCombineOp1RGB(Texture.CombinerOperandRGB.SourceColor);
413 t2.setApply(Texture.ApplyMode.Combine);
414 t2.setCombineFuncRGB(Texture.CombinerFunctionRGB.AddSigned);
415 t2.setCombineSrc0RGB(Texture.CombinerSource.CurrentTexture);
416 t2.setCombineOp0RGB(Texture.CombinerOperandRGB.SourceColor);
417 t2.setCombineSrc1RGB(Texture.CombinerSource.Previous);
418 t2.setCombineOp1RGB(Texture.CombinerOperandRGB.SourceColor);
420 tb.setRenderState(ts);
421 //set the detail parameters.
422 tb.setDetailTexture(1, 16);
423 tb.setRenderQueueMode(Renderer.QUEUE_OPAQUE);
424 scene.attachChild(tb);
430 * buildSkyBox creates a new skybox object with all the proper textures. The
431 * textures used are the standard skybox textures from all the tests.
434 private void buildSkyBox() {
435 skybox = new Skybox("skybox", 10, 10, 10);
437 Texture north = TextureManager.loadTexture(
438 TestSkybox.class.getClassLoader().getResource(
439 "jmetest/data/texture/north.jpg"),
440 Texture.MinificationFilter.BilinearNearestMipMap,
441 Texture.MagnificationFilter.Bilinear);
442 Texture south = TextureManager.loadTexture(
443 TestSkybox.class.getClassLoader().getResource(
444 "jmetest/data/texture/south.jpg"),
445 Texture.MinificationFilter.BilinearNearestMipMap,
446 Texture.MagnificationFilter.Bilinear);
447 Texture east = TextureManager.loadTexture(
448 TestSkybox.class.getClassLoader().getResource(
449 "jmetest/data/texture/east.jpg"),
450 Texture.MinificationFilter.BilinearNearestMipMap,
451 Texture.MagnificationFilter.Bilinear);
452 Texture west = TextureManager.loadTexture(
453 TestSkybox.class.getClassLoader().getResource(
454 "jmetest/data/texture/west.jpg"),
455 Texture.MinificationFilter.BilinearNearestMipMap,
456 Texture.MagnificationFilter.Bilinear);
457 Texture up = TextureManager.loadTexture(
458 TestSkybox.class.getClassLoader().getResource(
459 "jmetest/data/texture/top.jpg"),
460 Texture.MinificationFilter.BilinearNearestMipMap,
461 Texture.MagnificationFilter.Bilinear);
462 Texture down = TextureManager.loadTexture(
463 TestSkybox.class.getClassLoader().getResource(
464 "jmetest/data/texture/bottom.jpg"),
465 Texture.MinificationFilter.BilinearNearestMipMap,
466 Texture.MagnificationFilter.Bilinear);
468 skybox.setTexture(Skybox.Face.North, north);
469 skybox.setTexture(Skybox.Face.West, west);
470 skybox.setTexture(Skybox.Face.South, south);
471 skybox.setTexture(Skybox.Face.East, east);
472 skybox.setTexture(Skybox.Face.Up, up);
473 skybox.setTexture(Skybox.Face.Down, down);
474 skybox.preloadTextures();
475 scene.attachChild(skybox);
479 * set the basic parameters of the chase camera. This includes the offset. We want
480 * to be behind the vehicle and a little above it. So we will the offset as 0 for
481 * x and z, but be 1.5 times higher than the node.
483 * We then set the roll out parameters (2 units is the closest the camera can get, and
484 * 5 is the furthest).
487 private void buildChaseCamera() {
488 Vector3f targetOffset = new Vector3f();
489 targetOffset.y = ((BoundingBox) player.getWorldBound()).yExtent * 1.5f;
490 HashMap<String, Object> props = new HashMap<String, Object>();
491 props.put(ThirdPersonMouseLook.PROP_MAXROLLOUT, "6");
492 props.put(ThirdPersonMouseLook.PROP_MINROLLOUT, "3");
493 props.put(ThirdPersonMouseLook.PROP_MAXASCENT, ""+45 * FastMath.DEG_TO_RAD);
494 props.put(ChaseCamera.PROP_INITIALSPHERECOORDS, new Vector3f(5, 0, 30 * FastMath.DEG_TO_RAD));
495 props.put(ChaseCamera.PROP_TARGETOFFSET, targetOffset);
496 props.put(ChaseCamera.PROP_DAMPINGK, "4");
497 props.put(ChaseCamera.PROP_SPRINGK, "9");
498 chaser = new ChaseCamera(cam, player, props);
499 chaser.setMaxDistance(8);
500 chaser.setMinDistance(2);
504 * create our custom input handler.
507 private void buildInput() {
508 input = new FlagRushHandler(player, settings.getRenderer());
512 * will be called if the resolution changes
514 * @see com.jme.app.BaseGame#reinit()
516 protected void reinit() {
517 display.recreateWindow(width, height, depth, freq, fullscreen);
521 * close the window and also exit the program.
523 protected void quit() {
529 * clean up the textures.
531 * @see com.jme.app.BaseGame#cleanup()
533 protected void cleanup() {