blob: 502702effcdec13e096645d1b19edb4e32664d09 [file] [log] [blame]
/*
* Copyright (c) 2009-2010 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 com.jme3.terrain.geomipmap;
import com.jme3.bounding.BoundingBox;
import com.jme3.export.InputCapsule;
import com.jme3.export.JmeExporter;
import com.jme3.export.JmeImporter;
import com.jme3.export.OutputCapsule;
import com.jme3.material.Material;
import com.jme3.math.FastMath;
import com.jme3.math.Vector2f;
import com.jme3.math.Vector3f;
import com.jme3.scene.Spatial;
import com.jme3.scene.control.UpdateControl;
import com.jme3.terrain.Terrain;
import com.jme3.terrain.geomipmap.lodcalc.LodCalculator;
import com.jme3.terrain.heightmap.HeightMap;
import com.jme3.terrain.heightmap.HeightMapGrid;
import java.io.IOException;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* TerrainGrid itself is an actual TerrainQuad. Its four children are the visible four tiles.
*
* The grid is indexed by cells. Each cell has an integer XZ coordinate originating at 0,0.
* TerrainGrid will piggyback on the TerrainLodControl so it can use the camera for its
* updates as well. It does this in the overwritten update() method.
*
* It uses an LRU (Least Recently Used) cache of 16 terrain tiles (full TerrainQuadTrees). The
* center 4 are the ones that are visible. As the camera moves, it checks what camera cell it is in
* and will attach the now visible tiles.
*
* The 'quadIndex' variable is a 4x4 array that represents the tiles. The center
* four (index numbers: 5, 6, 9, 10) are what is visible. Each quadIndex value is an
* offset vector. The vector contains whole numbers and represents how many tiles in offset
* this location is from the center of the map. So for example the index 11 [Vector3f(2, 0, 1)]
* is located 2*terrainSize in X axis and 1*terrainSize in Z axis.
*
* As the camera moves, it tests what cameraCell it is in. Each camera cell covers four quad tiles
* and is half way inside each one.
*
* +-------+-------+
* | 1 | 4 | Four terrainQuads that make up the grid
* | *..|..* | with the cameraCell in the middle, covering
* |----|--|--|----| all four quads.
* | *..|..* |
* | 2 | 3 |
* +-------+-------+
*
* This results in the effect of when the camera gets half way across one of the sides of a quad to
* an empty (non-loaded) area, it will trigger the system to load in the next tiles.
*
* The tile loading is done on a background thread, and once the tile is loaded, then it is
* attached to the qrid quad tree, back on the OGL thread. It will grab the terrain quad from
* the LRU cache if it exists. If it does not exist, it will load in the new TerrainQuad tile.
*
* The loading of new tiles triggers events for any TerrainGridListeners. The events are:
* -tile Attached
* -tile Detached
* -grid moved.
*
* These allow physics to update, and other operation (often needed for loading the terrain) to occur
* at the right time.
*
* @author Anthyon
*/
public class TerrainGrid extends TerrainQuad {
protected static final Logger log = Logger.getLogger(TerrainGrid.class.getCanonicalName());
protected Vector3f currentCamCell = Vector3f.ZERO;
protected int quarterSize; // half of quadSize
protected int quadSize;
protected HeightMapGrid heightMapGrid;
private TerrainGridTileLoader gridTileLoader;
protected Vector3f[] quadIndex;
protected Set<TerrainGridListener> listeners = new HashSet<TerrainGridListener>();
protected Material material;
protected LRUCache<Vector3f, TerrainQuad> cache = new LRUCache<Vector3f, TerrainQuad>(16);
private int cellsLoaded = 0;
private int[] gridOffset;
private boolean runOnce = false;
protected class UpdateQuadCache implements Runnable {
protected final Vector3f location;
public UpdateQuadCache(Vector3f location) {
this.location = location;
}
/**
* This is executed if the camera has moved into a new CameraCell and will load in
* the new TerrainQuad tiles to be children of this TerrainGrid parent.
* It will first check the LRU cache to see if the terrain tile is already there,
* if it is not there, it will load it in and then cache that tile.
* The terrain tiles get added to the quad tree back on the OGL thread using the
* attachQuadAt() method. It also resets any cached values in TerrainQuad (such as
* neighbours).
*/
public void run() {
for (int i = 0; i < 4; i++) {
for (int j = 0; j < 4; j++) {
int quadIdx = i * 4 + j;
final Vector3f quadCell = location.add(quadIndex[quadIdx]);
TerrainQuad q = cache.get(quadCell);
if (q == null) {
if (heightMapGrid != null) {
// create the new Quad since it doesn't exist
HeightMap heightMapAt = heightMapGrid.getHeightMapAt(quadCell);
q = new TerrainQuad(getName() + "Quad" + quadCell, patchSize, quadSize, heightMapAt == null ? null : heightMapAt.getHeightMap());
q.setMaterial(material.clone());
log.log(Level.FINE, "Loaded TerrainQuad {0} from HeightMapGrid", q.getName());
} else if (gridTileLoader != null) {
q = gridTileLoader.getTerrainQuadAt(quadCell);
// only clone the material to the quad if it doesn't have a material of its own
if(q.getMaterial()==null) q.setMaterial(material.clone());
log.log(Level.FINE, "Loaded TerrainQuad {0} from TerrainQuadGrid", q.getName());
}
}
cache.put(quadCell, q);
if (isCenter(quadIdx)) {
// if it should be attached as a child right now, attach it
final int quadrant = getQuadrant(quadIdx);
final TerrainQuad newQuad = q;
// back on the OpenGL thread:
getControl(UpdateControl.class).enqueue(new Callable() {
public Object call() throws Exception {
attachQuadAt(newQuad, quadrant, quadCell);
//newQuad.resetCachedNeighbours();
return null;
}
});
}
}
}
}
}
protected boolean isCenter(int quadIndex) {
return quadIndex == 9 || quadIndex == 5 || quadIndex == 10 || quadIndex == 6;
}
protected int getQuadrant(int quadIndex) {
if (quadIndex == 5) {
return 1;
} else if (quadIndex == 9) {
return 2;
} else if (quadIndex == 6) {
return 3;
} else if (quadIndex == 10) {
return 4;
}
return 0; // error
}
public TerrainGrid(String name, int patchSize, int maxVisibleSize, Vector3f scale, TerrainGridTileLoader terrainQuadGrid,
Vector2f offset, float offsetAmount) {
this.name = name;
this.patchSize = patchSize;
this.size = maxVisibleSize;
this.stepScale = scale;
this.offset = offset;
this.offsetAmount = offsetAmount;
initData();
this.gridTileLoader = terrainQuadGrid;
terrainQuadGrid.setPatchSize(this.patchSize);
terrainQuadGrid.setQuadSize(this.quadSize);
addControl(new UpdateControl());
}
public TerrainGrid(String name, int patchSize, int maxVisibleSize, Vector3f scale, TerrainGridTileLoader terrainQuadGrid) {
this(name, patchSize, maxVisibleSize, scale, terrainQuadGrid, new Vector2f(), 0);
}
public TerrainGrid(String name, int patchSize, int maxVisibleSize, TerrainGridTileLoader terrainQuadGrid) {
this(name, patchSize, maxVisibleSize, Vector3f.UNIT_XYZ, terrainQuadGrid);
}
@Deprecated
public TerrainGrid(String name, int patchSize, int maxVisibleSize, Vector3f scale, HeightMapGrid heightMapGrid,
Vector2f offset, float offsetAmount) {
this.name = name;
this.patchSize = patchSize;
this.size = maxVisibleSize;
this.stepScale = scale;
this.offset = offset;
this.offsetAmount = offsetAmount;
initData();
this.heightMapGrid = heightMapGrid;
heightMapGrid.setSize(this.quadSize);
addControl(new UpdateControl());
}
@Deprecated
public TerrainGrid(String name, int patchSize, int maxVisibleSize, Vector3f scale, HeightMapGrid heightMapGrid) {
this(name, patchSize, maxVisibleSize, scale, heightMapGrid, new Vector2f(), 0);
}
@Deprecated
public TerrainGrid(String name, int patchSize, int maxVisibleSize, HeightMapGrid heightMapGrid) {
this(name, patchSize, maxVisibleSize, Vector3f.UNIT_XYZ, heightMapGrid);
}
public TerrainGrid() {
}
private void initData() {
int maxVisibleSize = size;
this.quarterSize = maxVisibleSize >> 2;
this.quadSize = (maxVisibleSize + 1) >> 1;
this.totalSize = maxVisibleSize;
this.gridOffset = new int[]{0, 0};
/*
* -z
* |
* 1|3
* -x ----+---- x
* 2|4
* |
* z
*/
this.quadIndex = new Vector3f[]{
new Vector3f(-1, 0, -1), new Vector3f(0, 0, -1), new Vector3f(1, 0, -1), new Vector3f(2, 0, -1),
new Vector3f(-1, 0, 0), new Vector3f(0, 0, 0), new Vector3f(1, 0, 0), new Vector3f(2, 0, 0),
new Vector3f(-1, 0, 1), new Vector3f(0, 0, 1), new Vector3f(1, 0, 1), new Vector3f(2, 0, 1),
new Vector3f(-1, 0, 2), new Vector3f(0, 0, 2), new Vector3f(1, 0, 2), new Vector3f(2, 0, 2)};
}
/**
* @deprecated not needed to be called any more, handled automatically
*/
public void initialize(Vector3f location) {
if (this.material == null) {
throw new RuntimeException("Material must be set prior to call of initialize");
}
Vector3f camCell = this.getCamCell(location);
this.updateChildren(camCell);
for (TerrainGridListener l : this.listeners) {
l.gridMoved(camCell);
}
}
@Override
public void update(List<Vector3f> locations, LodCalculator lodCalculator) {
// for now, only the first camera is handled.
// to accept more, there are two ways:
// 1: every camera has an associated grid, then the location is not enough to identify which camera location has changed
// 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
Vector3f cam = locations.isEmpty() ? Vector3f.ZERO.clone() : locations.get(0);
Vector3f camCell = this.getCamCell(cam); // get the grid index value of where the camera is (ie. 2,1)
if (cellsLoaded > 1) { // Check if cells are updated before updating gridoffset.
gridOffset[0] = Math.round(camCell.x * (size / 2));
gridOffset[1] = Math.round(camCell.z * (size / 2));
cellsLoaded = 0;
}
if (camCell.x != this.currentCamCell.x || camCell.z != currentCamCell.z || !runOnce) {
// if the camera has moved into a new cell, load new terrain into the visible 4 center quads
this.updateChildren(camCell);
for (TerrainGridListener l : this.listeners) {
l.gridMoved(camCell);
}
}
runOnce = true;
super.update(locations, lodCalculator);
}
public Vector3f getCamCell(Vector3f location) {
Vector3f tile = getTileCell(location);
Vector3f offsetHalf = new Vector3f(-0.5f, 0, -0.5f);
Vector3f shifted = tile.subtract(offsetHalf);
return new Vector3f(FastMath.floor(shifted.x), 0, FastMath.floor(shifted.z));
}
/**
* Centered at 0,0.
* Get the tile index location in integer form:
* @param location world coordinate
*/
public Vector3f getTileCell(Vector3f location) {
Vector3f tileLoc = location.divide(this.getWorldScale().mult(this.quadSize));
return tileLoc;
}
public TerrainGridTileLoader getGridTileLoader() {
return gridTileLoader;
}
protected void removeQuad(int idx) {
if (this.getQuad(idx) != null) {
for (TerrainGridListener l : listeners) {
l.tileDetached(getTileCell(this.getQuad(idx).getWorldTranslation()), this.getQuad(idx));
}
this.detachChild(this.getQuad(idx));
cellsLoaded++; // For gridoffset calc., maybe the run() method is a better location for this.
}
}
/**
* Runs on the rendering thread
*/
protected void attachQuadAt(TerrainQuad q, int quadrant, Vector3f quadCell) {
this.removeQuad(quadrant);
q.setQuadrant((short) quadrant);
this.attachChild(q);
Vector3f loc = quadCell.mult(this.quadSize - 1).subtract(quarterSize, 0, quarterSize);// quadrant location handled TerrainQuad automatically now
q.setLocalTranslation(loc);
for (TerrainGridListener l : listeners) {
l.tileAttached(quadCell, q);
}
updateModelBound();
for (Spatial s : getChildren()) {
if (s instanceof TerrainQuad) {
TerrainQuad tq = (TerrainQuad)s;
tq.resetCachedNeighbours();
tq.fixNormalEdges(new BoundingBox(tq.getWorldTranslation(), totalSize*2, Float.MAX_VALUE, totalSize*2));
}
}
}
@Deprecated
/**
* @Deprecated, use updateChildren
*/
protected void updateChildrens(Vector3f camCell) {
updateChildren(camCell);
}
/**
* Called when the camera has moved into a new cell. We need to
* update what quads are in the scene now.
*
* Step 1: touch cache
* LRU cache is used, so elements that need to remain
* should be touched.
*
* Step 2: load new quads in background thread
* if the camera has moved into a new cell, we load in new quads
* @param camCell the cell the camera is in
*/
protected void updateChildren(Vector3f camCell) {
int dx = 0;
int dy = 0;
if (currentCamCell != null) {
dx = (int) (camCell.x - currentCamCell.x);
dy = (int) (camCell.z - currentCamCell.z);
}
int xMin = 0;
int xMax = 4;
int yMin = 0;
int yMax = 4;
if (dx == -1) { // camera moved to -X direction
xMax = 3;
} else if (dx == 1) { // camera moved to +X direction
xMin = 1;
}
if (dy == -1) { // camera moved to -Y direction
yMax = 3;
} else if (dy == 1) { // camera moved to +Y direction
yMin = 1;
}
// Touch the items in the cache that we are and will be interested in.
// We activate cells in the direction we are moving. If we didn't move
// either way in one of the axes (say X or Y axis) then they are all touched.
for (int i = yMin; i < yMax; i++) {
for (int j = xMin; j < xMax; j++) {
cache.get(camCell.add(quadIndex[i * 4 + j]));
}
}
// ---------------------------------------------------
// ---------------------------------------------------
if (executor == null) {
// use the same executor as the LODControl
executor = createExecutorService();
}
executor.submit(new UpdateQuadCache(camCell));
this.currentCamCell = camCell;
}
public void addListener(TerrainGridListener listener) {
this.listeners.add(listener);
}
public Vector3f getCurrentCell() {
return this.currentCamCell;
}
public void removeListener(TerrainGridListener listener) {
this.listeners.remove(listener);
}
@Override
public void setMaterial(Material mat) {
this.material = mat;
super.setMaterial(mat);
}
public void setQuadSize(int quadSize) {
this.quadSize = quadSize;
}
@Override
public void adjustHeight(List<Vector2f> xz, List<Float> height) {
Vector3f currentGridLocation = getCurrentCell().mult(getLocalScale()).multLocal(quadSize - 1);
for (Vector2f vect : xz) {
vect.x -= currentGridLocation.x;
vect.y -= currentGridLocation.z;
}
super.adjustHeight(xz, height);
}
@Override
protected float getHeightmapHeight(int x, int z) {
return super.getHeightmapHeight(x - gridOffset[0], z - gridOffset[1]);
}
@Override
public int getNumMajorSubdivisions() {
return 2;
}
@Override
public Material getMaterial(Vector3f worldLocation) {
if (worldLocation == null)
return null;
Vector3f tileCell = getTileCell(worldLocation);
Terrain terrain = cache.get(tileCell);
if (terrain == null)
return null; // terrain not loaded for that cell yet!
return terrain.getMaterial(worldLocation);
}
@Override
public void read(JmeImporter im) throws IOException {
super.read(im);
InputCapsule c = im.getCapsule(this);
name = c.readString("name", null);
size = c.readInt("size", 0);
patchSize = c.readInt("patchSize", 0);
stepScale = (Vector3f) c.readSavable("stepScale", null);
offset = (Vector2f) c.readSavable("offset", null);
offsetAmount = c.readFloat("offsetAmount", 0);
gridTileLoader = (TerrainGridTileLoader) c.readSavable("terrainQuadGrid", null);
material = (Material) c.readSavable("material", null);
initData();
if (gridTileLoader != null) {
gridTileLoader.setPatchSize(this.patchSize);
gridTileLoader.setQuadSize(this.quadSize);
}
}
@Override
public void write(JmeExporter ex) throws IOException {
super.write(ex);
OutputCapsule c = ex.getCapsule(this);
c.write(gridTileLoader, "terrainQuadGrid", null);
c.write(size, "size", 0);
c.write(patchSize, "patchSize", 0);
c.write(stepScale, "stepScale", null);
c.write(offset, "offset", null);
c.write(offsetAmount, "offsetAmount", 0);
c.write(material, "material", null);
}
}