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 */ 32package com.jme3.terrain.geomipmap; 33 34import com.jme3.bounding.BoundingBox; 35import com.jme3.export.InputCapsule; 36import com.jme3.export.JmeExporter; 37import com.jme3.export.JmeImporter; 38import com.jme3.export.OutputCapsule; 39import com.jme3.material.Material; 40import com.jme3.math.FastMath; 41import com.jme3.math.Vector2f; 42import com.jme3.math.Vector3f; 43import com.jme3.scene.Spatial; 44import com.jme3.scene.control.UpdateControl; 45import com.jme3.terrain.Terrain; 46import com.jme3.terrain.geomipmap.lodcalc.LodCalculator; 47import com.jme3.terrain.heightmap.HeightMap; 48import com.jme3.terrain.heightmap.HeightMapGrid; 49import java.io.IOException; 50import java.util.HashSet; 51import java.util.List; 52import java.util.Set; 53import java.util.concurrent.Callable; 54import java.util.logging.Level; 55import 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 */ 102public 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 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 */ 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 177 protected boolean isCenter(int quadIndex) { 178 return quadIndex == 9 || quadIndex == 5 || quadIndex == 10 || quadIndex == 6; 179 } 180 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 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 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 213 public TerrainGrid(String name, int patchSize, int maxVisibleSize, TerrainGridTileLoader terrainQuadGrid) { 214 this(name, patchSize, maxVisibleSize, Vector3f.UNIT_XYZ, terrainQuadGrid); 215 } 216 217 @Deprecated 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 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 238 public TerrainGrid(String name, int patchSize, int maxVisibleSize, HeightMapGrid heightMapGrid) { 239 this(name, patchSize, maxVisibleSize, Vector3f.UNIT_XYZ, heightMapGrid); 240 } 241 242 public TerrainGrid() { 243 } 244 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 */ 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 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 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 */ 319 public Vector3f getTileCell(Vector3f location) { 320 Vector3f tileLoc = location.divide(this.getWorldScale().mult(this.quadSize)); 321 return tileLoc; 322 } 323 324 public TerrainGridTileLoader getGridTileLoader() { 325 return gridTileLoader; 326 } 327 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 */ 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 */ 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 */ 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 430 public void addListener(TerrainGridListener listener) { 431 this.listeners.add(listener); 432 } 433 434 public Vector3f getCurrentCell() { 435 return this.currentCamCell; 436 } 437 438 public void removeListener(TerrainGridListener listener) { 439 this.listeners.remove(listener); 440 } 441 442 @Override 443 public void setMaterial(Material mat) { 444 this.material = mat; 445 super.setMaterial(mat); 446 } 447 448 public void setQuadSize(int quadSize) { 449 this.quadSize = quadSize; 450 } 451 452 @Override 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 463 protected float getHeightmapHeight(int x, int z) { 464 return super.getHeightmapHeight(x - gridOffset[0], z - gridOffset[1]); 465 } 466 467 @Override 468 public int getNumMajorSubdivisions() { 469 return 2; 470 } 471 472 @Override 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 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 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