1 /* 2 * Copyright (c) 2009-2010 jMonkeyEngine 3 * All rights reserved. 4 * 5 * Redistribution and use in source and binary forms, with or without 6 * modification, are permitted provided that the following conditions are 7 * met: 8 * 9 * * Redistributions of source code must retain the above copyright 10 * notice, this list of conditions and the following disclaimer. 11 * 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. 15 * 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. 19 * 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. 31 */ 32 package jme3test.terrain; 33 34 import com.jme3.app.SimpleApplication; 35 import com.jme3.export.Savable; 36 import com.jme3.export.binary.BinaryExporter; 37 import com.jme3.export.binary.BinaryImporter; 38 import com.jme3.font.BitmapText; 39 import com.jme3.input.KeyInput; 40 import com.jme3.input.controls.ActionListener; 41 import com.jme3.input.controls.KeyTrigger; 42 import com.jme3.light.DirectionalLight; 43 import com.jme3.material.Material; 44 import com.jme3.math.ColorRGBA; 45 import com.jme3.math.Vector3f; 46 import com.jme3.scene.Node; 47 import com.jme3.terrain.Terrain; 48 import com.jme3.terrain.geomipmap.TerrainLodControl; 49 import com.jme3.terrain.geomipmap.TerrainQuad; 50 import com.jme3.terrain.geomipmap.lodcalc.DistanceLodCalculator; 51 import com.jme3.terrain.heightmap.AbstractHeightMap; 52 import com.jme3.terrain.heightmap.ImageBasedHeightMap; 53 import com.jme3.texture.Texture; 54 import com.jme3.texture.Texture.WrapMode; 55 import java.io.*; 56 import java.util.logging.Level; 57 import java.util.logging.Logger; 58 59 /** 60 * Saves and loads terrain. 61 * 62 * @author Brent Owens 63 */ 64 public class TerrainTestReadWrite extends SimpleApplication { 65 66 private Terrain terrain; 67 protected BitmapText hintText; 68 private float grassScale = 64; 69 private float dirtScale = 16; 70 private float rockScale = 128; 71 private Material matTerrain; 72 private Material matWire; 73 74 public static void main(String[] args) { 75 TerrainTestReadWrite app = new TerrainTestReadWrite(); 76 app.start(); 77 //testHeightmapBuilding(); 78 } 79 80 @Override 81 public void initialize() { 82 super.initialize(); 83 84 loadHintText(); 85 } 86 87 @Override 88 public void simpleInitApp() { 89 90 91 createControls(); 92 createMap(); 93 } 94 95 private void createMap() { 96 matTerrain = new Material(assetManager, "Common/MatDefs/Terrain/TerrainLighting.j3md"); 97 matTerrain.setBoolean("useTriPlanarMapping", false); 98 matTerrain.setBoolean("WardIso", true); 99 100 // ALPHA map (for splat textures) 101 matTerrain.setTexture("AlphaMap", assetManager.loadTexture("Textures/Terrain/splat/alphamap.png")); 102 103 // HEIGHTMAP image (for the terrain heightmap) 104 Texture heightMapImage = assetManager.loadTexture("Textures/Terrain/splat/mountains512.png"); 105 106 // GRASS texture 107 Texture grass = assetManager.loadTexture("Textures/Terrain/splat/grass.jpg"); 108 grass.setWrap(WrapMode.Repeat); 109 matTerrain.setTexture("DiffuseMap", grass); 110 matTerrain.setFloat("DiffuseMap_0_scale", grassScale); 111 112 113 // DIRT texture 114 Texture dirt = assetManager.loadTexture("Textures/Terrain/splat/dirt.jpg"); 115 dirt.setWrap(WrapMode.Repeat); 116 matTerrain.setTexture("DiffuseMap_1", dirt); 117 matTerrain.setFloat("DiffuseMap_1_scale", dirtScale); 118 119 // ROCK texture 120 Texture rock = assetManager.loadTexture("Textures/Terrain/splat/road.jpg"); 121 rock.setWrap(WrapMode.Repeat); 122 matTerrain.setTexture("DiffuseMap_2", rock); 123 matTerrain.setFloat("DiffuseMap_2_scale", rockScale); 124 125 126 Texture normalMap0 = assetManager.loadTexture("Textures/Terrain/splat/grass_normal.jpg"); 127 normalMap0.setWrap(WrapMode.Repeat); 128 Texture normalMap1 = assetManager.loadTexture("Textures/Terrain/splat/dirt_normal.png"); 129 normalMap1.setWrap(WrapMode.Repeat); 130 Texture normalMap2 = assetManager.loadTexture("Textures/Terrain/splat/road_normal.png"); 131 normalMap2.setWrap(WrapMode.Repeat); 132 matTerrain.setTexture("NormalMap", normalMap0); 133 matTerrain.setTexture("NormalMap_1", normalMap2); 134 matTerrain.setTexture("NormalMap_2", normalMap2); 135 136 matWire = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md"); 137 matWire.getAdditionalRenderState().setWireframe(true); 138 matWire.setColor("Color", ColorRGBA.Green); 139 140 141 // CREATE HEIGHTMAP 142 AbstractHeightMap heightmap = null; 143 try { 144 heightmap = new ImageBasedHeightMap(heightMapImage.getImage(), 1f); 145 heightmap.load(); 146 147 } catch (Exception e) { 148 e.printStackTrace(); 149 } 150 151 if (new File("terrainsave.jme").exists()) { 152 loadTerrain(); 153 } else { 154 // create the terrain as normal, and give it a control for LOD management 155 TerrainQuad terrainQuad = new TerrainQuad("terrain", 65, 129, heightmap.getHeightMap());//, new LodPerspectiveCalculatorFactory(getCamera(), 4)); // add this in to see it use entropy for LOD calculations 156 TerrainLodControl control = new TerrainLodControl(terrainQuad, getCamera()); 157 control.setLodCalculator( new DistanceLodCalculator(65, 2.7f) ); // patch size, and a multiplier 158 terrainQuad.addControl(control); 159 terrainQuad.setMaterial(matTerrain); 160 terrainQuad.setLocalTranslation(0, -100, 0); 161 terrainQuad.setLocalScale(4f, 0.25f, 4f); 162 rootNode.attachChild(terrainQuad); 163 164 this.terrain = terrainQuad; 165 } 166 167 DirectionalLight light = new DirectionalLight(); 168 light.setDirection((new Vector3f(-0.5f, -1f, -0.5f)).normalize()); 169 rootNode.addLight(light); 170 } 171 172 /** 173 * Create the save and load actions and add them to the input listener 174 */ 175 private void createControls() { 176 flyCam.setMoveSpeed(50); 177 cam.setLocation(new Vector3f(0, 100, 0)); 178 179 inputManager.addMapping("save", new KeyTrigger(KeyInput.KEY_T)); 180 inputManager.addListener(saveActionListener, "save"); 181 182 inputManager.addMapping("load", new KeyTrigger(KeyInput.KEY_Y)); 183 inputManager.addListener(loadActionListener, "load"); 184 185 inputManager.addMapping("clone", new KeyTrigger(KeyInput.KEY_C)); 186 inputManager.addListener(cloneActionListener, "clone"); 187 } 188 189 public void loadHintText() { 190 hintText = new BitmapText(guiFont, false); 191 hintText.setSize(guiFont.getCharSet().getRenderedSize()); 192 hintText.setLocalTranslation(0, getCamera().getHeight(), 0); 193 hintText.setText("Hit T to save, and Y to load"); 194 guiNode.attachChild(hintText); 195 } 196 private ActionListener saveActionListener = new ActionListener() { 197 198 public void onAction(String name, boolean pressed, float tpf) { 199 if (name.equals("save") && !pressed) { 200 201 FileOutputStream fos = null; 202 try { 203 long start = System.currentTimeMillis(); 204 fos = new FileOutputStream(new File("terrainsave.jme")); 205 206 // we just use the exporter and pass in the terrain 207 BinaryExporter.getInstance().save((Savable)terrain, new BufferedOutputStream(fos)); 208 209 fos.flush(); 210 float duration = (System.currentTimeMillis() - start) / 1000.0f; 211 System.out.println("Save took " + duration + " seconds"); 212 } catch (IOException ex) { 213 Logger.getLogger(TerrainTestReadWrite.class.getName()).log(Level.SEVERE, null, ex); 214 } finally { 215 try { 216 if (fos != null) { 217 fos.close(); 218 } 219 } catch (IOException e) { 220 Logger.getLogger(TerrainTestReadWrite.class.getName()).log(Level.SEVERE, null, e); 221 } 222 } 223 } 224 } 225 }; 226 227 private void loadTerrain() { 228 FileInputStream fis = null; 229 try { 230 long start = System.currentTimeMillis(); 231 // remove the existing terrain and detach it from the root node. 232 if (terrain != null) { 233 Node existingTerrain = (Node)terrain; 234 existingTerrain.removeFromParent(); 235 existingTerrain.removeControl(TerrainLodControl.class); 236 existingTerrain.detachAllChildren(); 237 terrain = null; 238 } 239 240 // import the saved terrain, and attach it back to the root node 241 File f = new File("terrainsave.jme"); 242 fis = new FileInputStream(f); 243 BinaryImporter imp = BinaryImporter.getInstance(); 244 imp.setAssetManager(assetManager); 245 terrain = (TerrainQuad) imp.load(new BufferedInputStream(fis)); 246 rootNode.attachChild((Node)terrain); 247 248 float duration = (System.currentTimeMillis() - start) / 1000.0f; 249 System.out.println("Load took " + duration + " seconds"); 250 251 // now we have to add back the camera to the LOD control 252 TerrainLodControl lodControl = ((Node)terrain).getControl(TerrainLodControl.class); 253 if (lodControl != null) 254 lodControl.setCamera(getCamera()); 255 256 } catch (IOException ex) { 257 Logger.getLogger(TerrainTestReadWrite.class.getName()).log(Level.SEVERE, null, ex); 258 } finally { 259 try { 260 if (fis != null) { 261 fis.close(); 262 } 263 } catch (IOException ex) { 264 Logger.getLogger(TerrainTestReadWrite.class.getName()).log(Level.SEVERE, null, ex); 265 } 266 } 267 } 268 private ActionListener loadActionListener = new ActionListener() { 269 270 public void onAction(String name, boolean pressed, float tpf) { 271 if (name.equals("load") && !pressed) { 272 loadTerrain(); 273 } 274 } 275 }; 276 private ActionListener cloneActionListener = new ActionListener() { 277 278 public void onAction(String name, boolean pressed, float tpf) { 279 if (name.equals("clone") && !pressed) { 280 281 Terrain clone = (Terrain) ((Node)terrain).clone(); 282 ((Node)terrain).removeFromParent(); 283 terrain = clone; 284 getRootNode().attachChild((Node)terrain); 285 } 286 } 287 }; 288 289 // no junit tests, so this has to be hand-tested: 290 private static void testHeightmapBuilding() { 291 int s = 9; 292 int b = 3; 293 float[] hm = new float[s * s]; 294 for (int i = 0; i < s; i++) { 295 for (int j = 0; j < s; j++) { 296 hm[(i * s) + j] = i * j; 297 } 298 } 299 300 for (int i = 0; i < s; i++) { 301 for (int j = 0; j < s; j++) { 302 System.out.print(hm[i * s + j] + " "); 303 } 304 System.out.println(""); 305 } 306 307 TerrainQuad terrain = new TerrainQuad("terrain", b, s, hm); 308 float[] hm2 = terrain.getHeightMap(); 309 boolean failed = false; 310 for (int i = 0; i < s * s; i++) { 311 if (hm[i] != hm2[i]) { 312 failed = true; 313 } 314 } 315 316 System.out.println(""); 317 if (failed) { 318 System.out.println("Terrain heightmap building FAILED!!!"); 319 for (int i = 0; i < s; i++) { 320 for (int j = 0; j < s; j++) { 321 System.out.print(hm2[i * s + j] + " "); 322 } 323 System.out.println(""); 324 } 325 } else { 326 System.out.println("Terrain heightmap building PASSED"); 327 } 328 } 329 } 330