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 com.jme3.terrain.geomipmap; 33 34 import com.jme3.bounding.BoundingBox; 35 import com.jme3.export.InputCapsule; 36 import com.jme3.export.JmeExporter; 37 import com.jme3.export.JmeImporter; 38 import com.jme3.export.OutputCapsule; 39 import com.jme3.material.Material; 40 import com.jme3.math.FastMath; 41 import com.jme3.math.Vector2f; 42 import com.jme3.math.Vector3f; 43 import com.jme3.scene.Spatial; 44 import com.jme3.scene.control.UpdateControl; 45 import com.jme3.terrain.Terrain; 46 import com.jme3.terrain.geomipmap.lodcalc.LodCalculator; 47 import com.jme3.terrain.heightmap.HeightMap; 48 import com.jme3.terrain.heightmap.HeightMapGrid; 49 import java.io.IOException; 50 import java.util.HashSet; 51 import java.util.List; 52 import java.util.Set; 53 import java.util.concurrent.Callable; 54 import java.util.logging.Level; 55 import java.util.logging.Logger; 56 57 /** 58 * TerrainGrid itself is an actual TerrainQuad. Its four children are the visible four tiles. 59 * 60 * The grid is indexed by cells. Each cell has an integer XZ coordinate originating at 0,0. 61 * TerrainGrid will piggyback on the TerrainLodControl so it can use the camera for its 62 * updates as well. It does this in the overwritten update() method. 63 * 64 * It uses an LRU (Least Recently Used) cache of 16 terrain tiles (full TerrainQuadTrees). The 65 * center 4 are the ones that are visible. As the camera moves, it checks what camera cell it is in 66 * and will attach the now visible tiles. 67 * 68 * The 'quadIndex' variable is a 4x4 array that represents the tiles. The center 69 * four (index numbers: 5, 6, 9, 10) are what is visible. Each quadIndex value is an 70 * offset vector. The vector contains whole numbers and represents how many tiles in offset 71 * this location is from the center of the map. So for example the index 11 [Vector3f(2, 0, 1)] 72 * is located 2*terrainSize in X axis and 1*terrainSize in Z axis. 73 * 74 * As the camera moves, it tests what cameraCell it is in. Each camera cell covers four quad tiles 75 * and is half way inside each one. 76 * 77 * +-------+-------+ 78 * | 1 | 4 | Four terrainQuads that make up the grid 79 * | *..|..* | with the cameraCell in the middle, covering 80 * |----|--|--|----| all four quads. 81 * | *..|..* | 82 * | 2 | 3 | 83 * +-------+-------+ 84 * 85 * This results in the effect of when the camera gets half way across one of the sides of a quad to 86 * an empty (non-loaded) area, it will trigger the system to load in the next tiles. 87 * 88 * The tile loading is done on a background thread, and once the tile is loaded, then it is 89 * attached to the qrid quad tree, back on the OGL thread. It will grab the terrain quad from 90 * the LRU cache if it exists. If it does not exist, it will load in the new TerrainQuad tile. 91 * 92 * The loading of new tiles triggers events for any TerrainGridListeners. The events are: 93 * -tile Attached 94 * -tile Detached 95 * -grid moved. 96 * 97 * These allow physics to update, and other operation (often needed for loading the terrain) to occur 98 * at the right time. 99 * 100 * @author Anthyon 101 */ 102 public class TerrainGrid extends TerrainQuad { 103 104 protected static final Logger log = Logger.getLogger(TerrainGrid.class.getCanonicalName()); 105 protected Vector3f currentCamCell = Vector3f.ZERO; 106 protected int quarterSize; // half of quadSize 107 protected int quadSize; 108 protected HeightMapGrid heightMapGrid; 109 private TerrainGridTileLoader gridTileLoader; 110 protected Vector3f[] quadIndex; 111 protected Set<TerrainGridListener> listeners = new HashSet<TerrainGridListener>(); 112 protected Material material; 113 protected LRUCache<Vector3f, TerrainQuad> cache = new LRUCache<Vector3f, TerrainQuad>(16); 114 private int cellsLoaded = 0; 115 private int[] gridOffset; 116 private boolean runOnce = false; 117 118 protected class UpdateQuadCache implements Runnable { 119 120 protected final Vector3f location; 121 UpdateQuadCache(Vector3f location)122 public UpdateQuadCache(Vector3f location) { 123 this.location = location; 124 } 125 126 /** 127 * This is executed if the camera has moved into a new CameraCell and will load in 128 * the new TerrainQuad tiles to be children of this TerrainGrid parent. 129 * It will first check the LRU cache to see if the terrain tile is already there, 130 * if it is not there, it will load it in and then cache that tile. 131 * The terrain tiles get added to the quad tree back on the OGL thread using the 132 * attachQuadAt() method. It also resets any cached values in TerrainQuad (such as 133 * neighbours). 134 */ run()135 public void run() { 136 for (int i = 0; i < 4; i++) { 137 for (int j = 0; j < 4; j++) { 138 int quadIdx = i * 4 + j; 139 final Vector3f quadCell = location.add(quadIndex[quadIdx]); 140 TerrainQuad q = cache.get(quadCell); 141 if (q == null) { 142 if (heightMapGrid != null) { 143 // create the new Quad since it doesn't exist 144 HeightMap heightMapAt = heightMapGrid.getHeightMapAt(quadCell); 145 q = new TerrainQuad(getName() + "Quad" + quadCell, patchSize, quadSize, heightMapAt == null ? null : heightMapAt.getHeightMap()); 146 q.setMaterial(material.clone()); 147 log.log(Level.FINE, "Loaded TerrainQuad {0} from HeightMapGrid", q.getName()); 148 } else if (gridTileLoader != null) { 149 q = gridTileLoader.getTerrainQuadAt(quadCell); 150 // only clone the material to the quad if it doesn't have a material of its own 151 if(q.getMaterial()==null) q.setMaterial(material.clone()); 152 log.log(Level.FINE, "Loaded TerrainQuad {0} from TerrainQuadGrid", q.getName()); 153 } 154 } 155 cache.put(quadCell, q); 156 157 if (isCenter(quadIdx)) { 158 // if it should be attached as a child right now, attach it 159 final int quadrant = getQuadrant(quadIdx); 160 final TerrainQuad newQuad = q; 161 // back on the OpenGL thread: 162 getControl(UpdateControl.class).enqueue(new Callable() { 163 164 public Object call() throws Exception { 165 attachQuadAt(newQuad, quadrant, quadCell); 166 //newQuad.resetCachedNeighbours(); 167 return null; 168 } 169 }); 170 } 171 } 172 } 173 174 } 175 } 176 isCenter(int quadIndex)177 protected boolean isCenter(int quadIndex) { 178 return quadIndex == 9 || quadIndex == 5 || quadIndex == 10 || quadIndex == 6; 179 } 180 getQuadrant(int quadIndex)181 protected int getQuadrant(int quadIndex) { 182 if (quadIndex == 5) { 183 return 1; 184 } else if (quadIndex == 9) { 185 return 2; 186 } else if (quadIndex == 6) { 187 return 3; 188 } else if (quadIndex == 10) { 189 return 4; 190 } 191 return 0; // error 192 } 193 TerrainGrid(String name, int patchSize, int maxVisibleSize, Vector3f scale, TerrainGridTileLoader terrainQuadGrid, Vector2f offset, float offsetAmount)194 public TerrainGrid(String name, int patchSize, int maxVisibleSize, Vector3f scale, TerrainGridTileLoader terrainQuadGrid, 195 Vector2f offset, float offsetAmount) { 196 this.name = name; 197 this.patchSize = patchSize; 198 this.size = maxVisibleSize; 199 this.stepScale = scale; 200 this.offset = offset; 201 this.offsetAmount = offsetAmount; 202 initData(); 203 this.gridTileLoader = terrainQuadGrid; 204 terrainQuadGrid.setPatchSize(this.patchSize); 205 terrainQuadGrid.setQuadSize(this.quadSize); 206 addControl(new UpdateControl()); 207 } 208 TerrainGrid(String name, int patchSize, int maxVisibleSize, Vector3f scale, TerrainGridTileLoader terrainQuadGrid)209 public TerrainGrid(String name, int patchSize, int maxVisibleSize, Vector3f scale, TerrainGridTileLoader terrainQuadGrid) { 210 this(name, patchSize, maxVisibleSize, scale, terrainQuadGrid, new Vector2f(), 0); 211 } 212 TerrainGrid(String name, int patchSize, int maxVisibleSize, TerrainGridTileLoader terrainQuadGrid)213 public TerrainGrid(String name, int patchSize, int maxVisibleSize, TerrainGridTileLoader terrainQuadGrid) { 214 this(name, patchSize, maxVisibleSize, Vector3f.UNIT_XYZ, terrainQuadGrid); 215 } 216 217 @Deprecated TerrainGrid(String name, int patchSize, int maxVisibleSize, Vector3f scale, HeightMapGrid heightMapGrid, Vector2f offset, float offsetAmount)218 public TerrainGrid(String name, int patchSize, int maxVisibleSize, Vector3f scale, HeightMapGrid heightMapGrid, 219 Vector2f offset, float offsetAmount) { 220 this.name = name; 221 this.patchSize = patchSize; 222 this.size = maxVisibleSize; 223 this.stepScale = scale; 224 this.offset = offset; 225 this.offsetAmount = offsetAmount; 226 initData(); 227 this.heightMapGrid = heightMapGrid; 228 heightMapGrid.setSize(this.quadSize); 229 addControl(new UpdateControl()); 230 } 231 232 @Deprecated TerrainGrid(String name, int patchSize, int maxVisibleSize, Vector3f scale, HeightMapGrid heightMapGrid)233 public TerrainGrid(String name, int patchSize, int maxVisibleSize, Vector3f scale, HeightMapGrid heightMapGrid) { 234 this(name, patchSize, maxVisibleSize, scale, heightMapGrid, new Vector2f(), 0); 235 } 236 237 @Deprecated TerrainGrid(String name, int patchSize, int maxVisibleSize, HeightMapGrid heightMapGrid)238 public TerrainGrid(String name, int patchSize, int maxVisibleSize, HeightMapGrid heightMapGrid) { 239 this(name, patchSize, maxVisibleSize, Vector3f.UNIT_XYZ, heightMapGrid); 240 } 241 TerrainGrid()242 public TerrainGrid() { 243 } 244 initData()245 private void initData() { 246 int maxVisibleSize = size; 247 this.quarterSize = maxVisibleSize >> 2; 248 this.quadSize = (maxVisibleSize + 1) >> 1; 249 this.totalSize = maxVisibleSize; 250 this.gridOffset = new int[]{0, 0}; 251 252 /* 253 * -z 254 * | 255 * 1|3 256 * -x ----+---- x 257 * 2|4 258 * | 259 * z 260 */ 261 this.quadIndex = new Vector3f[]{ 262 new Vector3f(-1, 0, -1), new Vector3f(0, 0, -1), new Vector3f(1, 0, -1), new Vector3f(2, 0, -1), 263 new Vector3f(-1, 0, 0), new Vector3f(0, 0, 0), new Vector3f(1, 0, 0), new Vector3f(2, 0, 0), 264 new Vector3f(-1, 0, 1), new Vector3f(0, 0, 1), new Vector3f(1, 0, 1), new Vector3f(2, 0, 1), 265 new Vector3f(-1, 0, 2), new Vector3f(0, 0, 2), new Vector3f(1, 0, 2), new Vector3f(2, 0, 2)}; 266 267 } 268 269 /** 270 * @deprecated not needed to be called any more, handled automatically 271 */ initialize(Vector3f location)272 public void initialize(Vector3f location) { 273 if (this.material == null) { 274 throw new RuntimeException("Material must be set prior to call of initialize"); 275 } 276 Vector3f camCell = this.getCamCell(location); 277 this.updateChildren(camCell); 278 for (TerrainGridListener l : this.listeners) { 279 l.gridMoved(camCell); 280 } 281 } 282 283 @Override update(List<Vector3f> locations, LodCalculator lodCalculator)284 public void update(List<Vector3f> locations, LodCalculator lodCalculator) { 285 // for now, only the first camera is handled. 286 // to accept more, there are two ways: 287 // 1: every camera has an associated grid, then the location is not enough to identify which camera location has changed 288 // 2: grids are associated with locations, and no incremental update is done, we load new grids for new locations, and unload those that are not needed anymore 289 Vector3f cam = locations.isEmpty() ? Vector3f.ZERO.clone() : locations.get(0); 290 Vector3f camCell = this.getCamCell(cam); // get the grid index value of where the camera is (ie. 2,1) 291 if (cellsLoaded > 1) { // Check if cells are updated before updating gridoffset. 292 gridOffset[0] = Math.round(camCell.x * (size / 2)); 293 gridOffset[1] = Math.round(camCell.z * (size / 2)); 294 cellsLoaded = 0; 295 } 296 if (camCell.x != this.currentCamCell.x || camCell.z != currentCamCell.z || !runOnce) { 297 // if the camera has moved into a new cell, load new terrain into the visible 4 center quads 298 this.updateChildren(camCell); 299 for (TerrainGridListener l : this.listeners) { 300 l.gridMoved(camCell); 301 } 302 } 303 runOnce = true; 304 super.update(locations, lodCalculator); 305 } 306 getCamCell(Vector3f location)307 public Vector3f getCamCell(Vector3f location) { 308 Vector3f tile = getTileCell(location); 309 Vector3f offsetHalf = new Vector3f(-0.5f, 0, -0.5f); 310 Vector3f shifted = tile.subtract(offsetHalf); 311 return new Vector3f(FastMath.floor(shifted.x), 0, FastMath.floor(shifted.z)); 312 } 313 314 /** 315 * Centered at 0,0. 316 * Get the tile index location in integer form: 317 * @param location world coordinate 318 */ getTileCell(Vector3f location)319 public Vector3f getTileCell(Vector3f location) { 320 Vector3f tileLoc = location.divide(this.getWorldScale().mult(this.quadSize)); 321 return tileLoc; 322 } 323 getGridTileLoader()324 public TerrainGridTileLoader getGridTileLoader() { 325 return gridTileLoader; 326 } 327 removeQuad(int idx)328 protected void removeQuad(int idx) { 329 if (this.getQuad(idx) != null) { 330 for (TerrainGridListener l : listeners) { 331 l.tileDetached(getTileCell(this.getQuad(idx).getWorldTranslation()), this.getQuad(idx)); 332 } 333 this.detachChild(this.getQuad(idx)); 334 cellsLoaded++; // For gridoffset calc., maybe the run() method is a better location for this. 335 } 336 } 337 338 /** 339 * Runs on the rendering thread 340 */ attachQuadAt(TerrainQuad q, int quadrant, Vector3f quadCell)341 protected void attachQuadAt(TerrainQuad q, int quadrant, Vector3f quadCell) { 342 this.removeQuad(quadrant); 343 344 q.setQuadrant((short) quadrant); 345 this.attachChild(q); 346 347 Vector3f loc = quadCell.mult(this.quadSize - 1).subtract(quarterSize, 0, quarterSize);// quadrant location handled TerrainQuad automatically now 348 q.setLocalTranslation(loc); 349 350 for (TerrainGridListener l : listeners) { 351 l.tileAttached(quadCell, q); 352 } 353 updateModelBound(); 354 355 for (Spatial s : getChildren()) { 356 if (s instanceof TerrainQuad) { 357 TerrainQuad tq = (TerrainQuad)s; 358 tq.resetCachedNeighbours(); 359 tq.fixNormalEdges(new BoundingBox(tq.getWorldTranslation(), totalSize*2, Float.MAX_VALUE, totalSize*2)); 360 } 361 } 362 } 363 364 @Deprecated 365 /** 366 * @Deprecated, use updateChildren 367 */ updateChildrens(Vector3f camCell)368 protected void updateChildrens(Vector3f camCell) { 369 updateChildren(camCell); 370 } 371 372 /** 373 * Called when the camera has moved into a new cell. We need to 374 * update what quads are in the scene now. 375 * 376 * Step 1: touch cache 377 * LRU cache is used, so elements that need to remain 378 * should be touched. 379 * 380 * Step 2: load new quads in background thread 381 * if the camera has moved into a new cell, we load in new quads 382 * @param camCell the cell the camera is in 383 */ updateChildren(Vector3f camCell)384 protected void updateChildren(Vector3f camCell) { 385 386 int dx = 0; 387 int dy = 0; 388 if (currentCamCell != null) { 389 dx = (int) (camCell.x - currentCamCell.x); 390 dy = (int) (camCell.z - currentCamCell.z); 391 } 392 393 int xMin = 0; 394 int xMax = 4; 395 int yMin = 0; 396 int yMax = 4; 397 if (dx == -1) { // camera moved to -X direction 398 xMax = 3; 399 } else if (dx == 1) { // camera moved to +X direction 400 xMin = 1; 401 } 402 403 if (dy == -1) { // camera moved to -Y direction 404 yMax = 3; 405 } else if (dy == 1) { // camera moved to +Y direction 406 yMin = 1; 407 } 408 409 // Touch the items in the cache that we are and will be interested in. 410 // We activate cells in the direction we are moving. If we didn't move 411 // either way in one of the axes (say X or Y axis) then they are all touched. 412 for (int i = yMin; i < yMax; i++) { 413 for (int j = xMin; j < xMax; j++) { 414 cache.get(camCell.add(quadIndex[i * 4 + j])); 415 } 416 } 417 // --------------------------------------------------- 418 // --------------------------------------------------- 419 420 if (executor == null) { 421 // use the same executor as the LODControl 422 executor = createExecutorService(); 423 } 424 425 executor.submit(new UpdateQuadCache(camCell)); 426 427 this.currentCamCell = camCell; 428 } 429 addListener(TerrainGridListener listener)430 public void addListener(TerrainGridListener listener) { 431 this.listeners.add(listener); 432 } 433 getCurrentCell()434 public Vector3f getCurrentCell() { 435 return this.currentCamCell; 436 } 437 removeListener(TerrainGridListener listener)438 public void removeListener(TerrainGridListener listener) { 439 this.listeners.remove(listener); 440 } 441 442 @Override setMaterial(Material mat)443 public void setMaterial(Material mat) { 444 this.material = mat; 445 super.setMaterial(mat); 446 } 447 setQuadSize(int quadSize)448 public void setQuadSize(int quadSize) { 449 this.quadSize = quadSize; 450 } 451 452 @Override adjustHeight(List<Vector2f> xz, List<Float> height)453 public void adjustHeight(List<Vector2f> xz, List<Float> height) { 454 Vector3f currentGridLocation = getCurrentCell().mult(getLocalScale()).multLocal(quadSize - 1); 455 for (Vector2f vect : xz) { 456 vect.x -= currentGridLocation.x; 457 vect.y -= currentGridLocation.z; 458 } 459 super.adjustHeight(xz, height); 460 } 461 462 @Override getHeightmapHeight(int x, int z)463 protected float getHeightmapHeight(int x, int z) { 464 return super.getHeightmapHeight(x - gridOffset[0], z - gridOffset[1]); 465 } 466 467 @Override getNumMajorSubdivisions()468 public int getNumMajorSubdivisions() { 469 return 2; 470 } 471 472 @Override getMaterial(Vector3f worldLocation)473 public Material getMaterial(Vector3f worldLocation) { 474 if (worldLocation == null) 475 return null; 476 Vector3f tileCell = getTileCell(worldLocation); 477 Terrain terrain = cache.get(tileCell); 478 if (terrain == null) 479 return null; // terrain not loaded for that cell yet! 480 return terrain.getMaterial(worldLocation); 481 } 482 483 @Override read(JmeImporter im)484 public void read(JmeImporter im) throws IOException { 485 super.read(im); 486 InputCapsule c = im.getCapsule(this); 487 name = c.readString("name", null); 488 size = c.readInt("size", 0); 489 patchSize = c.readInt("patchSize", 0); 490 stepScale = (Vector3f) c.readSavable("stepScale", null); 491 offset = (Vector2f) c.readSavable("offset", null); 492 offsetAmount = c.readFloat("offsetAmount", 0); 493 gridTileLoader = (TerrainGridTileLoader) c.readSavable("terrainQuadGrid", null); 494 material = (Material) c.readSavable("material", null); 495 initData(); 496 if (gridTileLoader != null) { 497 gridTileLoader.setPatchSize(this.patchSize); 498 gridTileLoader.setQuadSize(this.quadSize); 499 } 500 } 501 502 @Override write(JmeExporter ex)503 public void write(JmeExporter ex) throws IOException { 504 super.write(ex); 505 OutputCapsule c = ex.getCapsule(this); 506 c.write(gridTileLoader, "terrainQuadGrid", null); 507 c.write(size, "size", 0); 508 c.write(patchSize, "patchSize", 0); 509 c.write(stepScale, "stepScale", null); 510 c.write(offset, "offset", null); 511 c.write(offsetAmount, "offsetAmount", 0); 512 c.write(material, "material", null); 513 } 514 } 515