You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1919 lines
72 KiB
1919 lines
72 KiB
/*
|
|
* Copyright (c) 2009-2012 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.bounding.BoundingVolume;
|
|
import com.jme3.collision.Collidable;
|
|
import com.jme3.collision.CollisionResults;
|
|
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.Ray;
|
|
import com.jme3.math.Vector2f;
|
|
import com.jme3.math.Vector3f;
|
|
import com.jme3.scene.Geometry;
|
|
import com.jme3.scene.Node;
|
|
import com.jme3.scene.Spatial;
|
|
import com.jme3.scene.debug.WireBox;
|
|
import com.jme3.terrain.ProgressMonitor;
|
|
import com.jme3.terrain.Terrain;
|
|
import com.jme3.terrain.geomipmap.lodcalc.LodCalculator;
|
|
import com.jme3.terrain.geomipmap.picking.BresenhamTerrainPicker;
|
|
import com.jme3.terrain.geomipmap.picking.TerrainPickData;
|
|
import com.jme3.terrain.geomipmap.picking.TerrainPicker;
|
|
import com.jme3.util.TangentBinormalGenerator;
|
|
import com.jme3.util.clone.Cloner;
|
|
import java.io.IOException;
|
|
import java.util.ArrayList;
|
|
import java.util.HashMap;
|
|
import java.util.List;
|
|
import java.util.Map;
|
|
import java.util.logging.Level;
|
|
import java.util.logging.Logger;
|
|
|
|
/**
|
|
* <p>
|
|
* TerrainQuad is a heightfield-based terrain system. Heightfield terrain is fast and can
|
|
* render large areas, and allows for easy Level of Detail control. However it does not
|
|
* permit caves easily.
|
|
* TerrainQuad is a quad tree, meaning that the root quad has four children, and each of
|
|
* those children have four children. All the way down until you reach the bottom, the actual
|
|
* geometry, the TerrainPatches.
|
|
* If you look at a TerrainQuad in wireframe mode with the TerrainLODControl attached, you will
|
|
* see blocks that change their LOD level together; these are the TerrainPatches. The TerrainQuad
|
|
* is just an organizational structure for the TerrainPatches so patches that are not in the
|
|
* view frustum get culled quickly.
|
|
* TerrainQuads size are a power of 2, plus 1. So 513x513, or 1025x1025 etc.
|
|
* Each point in the terrain is one unit apart from its neighbour. So a 513x513 terrain
|
|
* will be 513 units wide and 513 units long.
|
|
* Patch size can be specified on the terrain. This sets how large each geometry (TerrainPatch)
|
|
* is. It also must be a power of 2 plus 1 so the terrain can be subdivided equally.
|
|
* </p>
|
|
* <p>
|
|
* The height of the terrain can be modified at runtime using setHeight()
|
|
* </p>
|
|
* <p>
|
|
* A terrain quad is a node in the quad tree of the terrain system.
|
|
* The root terrain quad will be the only one that receives the update() call every frame
|
|
* and it will determine if there has been any LOD change.
|
|
* </p><p>
|
|
* The leaves of the terrain quad tree are Terrain Patches. These have the real geometry mesh.
|
|
* </p><p>
|
|
* Heightmap coordinates start from the bottom left of the world and work towards the
|
|
* top right.
|
|
* </p><pre>
|
|
* +x
|
|
* ^
|
|
* | ......N = length of heightmap
|
|
* | : :
|
|
* | : :
|
|
* | 0.....:
|
|
* +---------> +z
|
|
* (world coordinates)
|
|
* </pre>
|
|
* @author Brent Owens
|
|
*/
|
|
public class TerrainQuad extends Node implements Terrain {
|
|
protected Vector2f offset;
|
|
|
|
protected int totalSize; // the size of this entire terrain tree (on one side)
|
|
|
|
protected int size; // size of this quad, can be between totalSize and patchSize
|
|
|
|
protected int patchSize; // size of the individual patches
|
|
|
|
protected Vector3f stepScale;
|
|
|
|
protected float offsetAmount;
|
|
|
|
protected int quadrant = 0; // 1=upper left, 2=lower left, 3=upper right, 4=lower right
|
|
private int maxLod = -1;
|
|
private BoundingBox affectedAreaBBox; // only set in the root quad
|
|
|
|
private TerrainPicker picker;
|
|
private Vector3f lastScale = Vector3f.UNIT_XYZ;
|
|
|
|
protected NeighbourFinder neighbourFinder;
|
|
|
|
public TerrainQuad() {
|
|
super("Terrain");
|
|
}
|
|
|
|
/**
|
|
* Creates a terrain with:
|
|
* <ul>
|
|
* <li>the total, real-world, size of the terrain</li>
|
|
* <li>the patchSize, or the size of each geometry tile of the terrain</li>
|
|
* <li>the heightmap that defines the height of the terrain</li>
|
|
* </ul>
|
|
* <p>
|
|
* A TerrainQuad of totalSize 513x513 will be 513 units wide and 513 units long.
|
|
* PatchSize is just used to subdivide the terrain into tiles that can be culled.
|
|
* </p>
|
|
* @param name the name of the scene element. This is required for
|
|
* identification and comparison purposes.
|
|
* @param patchSize size of the individual patches (geometry). Power of 2 plus 1,
|
|
* must be smaller than totalSize. (eg. 33, 65...)
|
|
* @param totalSize the size of this entire terrain (on one side). Power of 2 plus 1
|
|
* (eg. 513, 1025, 2049...)
|
|
* @param heightMap The height map to generate the terrain from (a flat
|
|
* height map will be generated if this is null). The size of one side of the heightmap
|
|
* must match the totalSize. So a 513x513 heightmap is needed for a terrain with totalSize of 513.
|
|
*/
|
|
public TerrainQuad(String name, int patchSize, int totalSize, float[] heightMap) {
|
|
this(name, patchSize, totalSize, Vector3f.UNIT_XYZ, heightMap);
|
|
|
|
affectedAreaBBox = new BoundingBox(new Vector3f(0,0,0), size*2, Float.MAX_VALUE, size*2);
|
|
fixNormalEdges(affectedAreaBBox);
|
|
addControl(new NormalRecalcControl(this));
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param name the name of the scene element. This is required for
|
|
* identification and comparison purposes.
|
|
* @param patchSize size of the individual patches
|
|
* @param size size of this quad, can be between totalSize and patchSize
|
|
* @param scale
|
|
* @param heightMap The height map to generate the terrain from (a flat
|
|
* height map will be generated if this is null)
|
|
*/
|
|
@Deprecated
|
|
public TerrainQuad(String name, int patchSize, int size, Vector3f scale, float[] heightMap) {
|
|
this(name, patchSize, size, scale, heightMap, size, new Vector2f(), 0);
|
|
//affectedAreaBBox = new BoundingBox(new Vector3f(0,0,0), size*2, Float.MAX_VALUE, size*2);
|
|
//fixNormalEdges(affectedAreaBBox);
|
|
//addControl(new NormalRecalcControl(this));
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param name the name of the scene element. This is required for
|
|
* identification and comparison purposes.
|
|
* @param patchSize size of the individual patches
|
|
* @param totalSize the size of this entire terrain tree (on one side)
|
|
* @param quadSize
|
|
* @param scale
|
|
* @param heightMap The height map to generate the terrain from (a flat
|
|
* height map will be generated if this is null)
|
|
*/
|
|
@Deprecated
|
|
public TerrainQuad(String name, int patchSize, int totalSize, int quadSize, Vector3f scale, float[] heightMap) {
|
|
this(name, patchSize, quadSize, scale, heightMap, totalSize, new Vector2f(), 0);
|
|
//affectedAreaBBox = new BoundingBox(new Vector3f(0,0,0), totalSize*2, Float.MAX_VALUE, totalSize*2);
|
|
//fixNormalEdges(affectedAreaBBox);
|
|
//addControl(new NormalRecalcControl(this));
|
|
}
|
|
|
|
protected TerrainQuad(String name, int patchSize, int quadSize,
|
|
Vector3f scale, float[] heightMap, int totalSize,
|
|
Vector2f offset, float offsetAmount)
|
|
{
|
|
super(name);
|
|
|
|
if (heightMap == null)
|
|
heightMap = generateDefaultHeightMap(quadSize);
|
|
|
|
if (!FastMath.isPowerOfTwo(quadSize - 1)) {
|
|
throw new RuntimeException("size given: " + quadSize + " Terrain quad sizes may only be (2^N + 1)");
|
|
}
|
|
if (FastMath.sqrt(heightMap.length) > quadSize) {
|
|
Logger.getLogger(this.getClass().getName()).log(Level.WARNING, "Heightmap size is larger than the terrain size. Make sure your heightmap image is the same size as the terrain!");
|
|
}
|
|
|
|
this.offset = offset;
|
|
this.offsetAmount = offsetAmount;
|
|
this.totalSize = totalSize;
|
|
this.size = quadSize;
|
|
this.patchSize = patchSize;
|
|
this.stepScale = scale;
|
|
split(patchSize, heightMap);
|
|
}
|
|
|
|
public void setNeighbourFinder(NeighbourFinder neighbourFinder) {
|
|
this.neighbourFinder = neighbourFinder;
|
|
resetCachedNeighbours();
|
|
}
|
|
|
|
/**
|
|
* Forces the recalculation of all normals on the terrain.
|
|
*/
|
|
public void recalculateAllNormals() {
|
|
affectedAreaBBox = new BoundingBox(new Vector3f(0,0,0), totalSize*2, Float.MAX_VALUE, totalSize*2);
|
|
}
|
|
|
|
/**
|
|
* Create just a flat heightmap
|
|
*/
|
|
private float[] generateDefaultHeightMap(int size) {
|
|
float[] heightMap = new float[size*size];
|
|
return heightMap;
|
|
}
|
|
|
|
/**
|
|
* update the normals if there were any height changes recently.
|
|
* Should only be called on the root quad
|
|
*/
|
|
protected void updateNormals() {
|
|
|
|
if (needToRecalculateNormals()) {
|
|
//TODO background-thread this if it ends up being expensive
|
|
fixNormals(affectedAreaBBox); // the affected patches
|
|
fixNormalEdges(affectedAreaBBox); // the edges between the patches
|
|
|
|
setNormalRecalcNeeded(null); // set to false
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Caches the transforms (except rotation) so the LOD calculator,
|
|
* which runs on a separate thread, can access them safely.
|
|
*/
|
|
protected void cacheTerrainTransforms() {
|
|
for (int i = children.size(); --i >= 0;) {
|
|
Spatial child = children.get(i);
|
|
if (child instanceof TerrainQuad) {
|
|
((TerrainQuad) child).cacheTerrainTransforms();
|
|
} else if (child instanceof TerrainPatch) {
|
|
((TerrainPatch) child).cacheTerrainTransforms();
|
|
}
|
|
}
|
|
}
|
|
|
|
private int collideWithRay(Ray ray, CollisionResults results) {
|
|
if (picker == null)
|
|
picker = new BresenhamTerrainPicker(this);
|
|
|
|
Vector3f intersection = picker.getTerrainIntersection(ray, results);
|
|
if (intersection != null) {
|
|
if (ray.getLimit() < Float.POSITIVE_INFINITY) {
|
|
if (results.getClosestCollision().getDistance() <= ray.getLimit())
|
|
return 1; // in range
|
|
else
|
|
return 0; // out of range
|
|
} else
|
|
return 1;
|
|
} else
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* Generate the entropy values for the terrain for the "perspective" LOD
|
|
* calculator. This routine can take a long time to run!
|
|
* @param progressMonitor optional
|
|
*/
|
|
public void generateEntropy(ProgressMonitor progressMonitor) {
|
|
// only check this on the root quad
|
|
if (isRootQuad())
|
|
if (progressMonitor != null) {
|
|
int numCalc = (totalSize-1)/(patchSize-1); // make it an even number
|
|
progressMonitor.setMonitorMax(numCalc*numCalc);
|
|
}
|
|
|
|
if (children != null) {
|
|
for (int i = children.size(); --i >= 0;) {
|
|
Spatial child = children.get(i);
|
|
if (child instanceof TerrainQuad) {
|
|
((TerrainQuad) child).generateEntropy(progressMonitor);
|
|
} else if (child instanceof TerrainPatch) {
|
|
((TerrainPatch) child).generateLodEntropies();
|
|
if (progressMonitor != null)
|
|
progressMonitor.incrementProgress(1);
|
|
}
|
|
}
|
|
}
|
|
|
|
// only do this on the root quad
|
|
if (isRootQuad())
|
|
if (progressMonitor != null)
|
|
progressMonitor.progressComplete();
|
|
}
|
|
|
|
protected boolean isRootQuad() {
|
|
return (getParent() != null && !(getParent() instanceof TerrainQuad) );
|
|
}
|
|
|
|
public Material getMaterial() {
|
|
return getMaterial(null);
|
|
}
|
|
|
|
public Material getMaterial(Vector3f worldLocation) {
|
|
// get the material from one of the children. They all share the same material
|
|
if (children != null) {
|
|
for (int i = children.size(); --i >= 0;) {
|
|
Spatial child = children.get(i);
|
|
if (child instanceof TerrainQuad) {
|
|
return ((TerrainQuad)child).getMaterial(worldLocation);
|
|
} else if (child instanceof TerrainPatch) {
|
|
return ((TerrainPatch)child).getMaterial();
|
|
}
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
public int getNumMajorSubdivisions() {
|
|
return 1;
|
|
}
|
|
|
|
|
|
protected boolean calculateLod(List<Vector3f> location, HashMap<String,UpdatedTerrainPatch> updates, LodCalculator lodCalculator) {
|
|
|
|
boolean lodChanged = false;
|
|
|
|
if (children != null) {
|
|
for (int i = children.size(); --i >= 0;) {
|
|
Spatial child = children.get(i);
|
|
if (child instanceof TerrainQuad) {
|
|
boolean b = ((TerrainQuad) child).calculateLod(location, updates, lodCalculator);
|
|
if (b)
|
|
lodChanged = true;
|
|
} else if (child instanceof TerrainPatch) {
|
|
boolean b = lodCalculator.calculateLod((TerrainPatch) child, location, updates);
|
|
if (b)
|
|
lodChanged = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
return lodChanged;
|
|
}
|
|
|
|
protected synchronized void findNeighboursLod(HashMap<String,UpdatedTerrainPatch> updated) {
|
|
if (children != null) {
|
|
for (int x = children.size(); --x >= 0;) {
|
|
Spatial child = children.get(x);
|
|
if (child instanceof TerrainQuad) {
|
|
((TerrainQuad) child).findNeighboursLod(updated);
|
|
} else if (child instanceof TerrainPatch) {
|
|
|
|
TerrainPatch patch = (TerrainPatch) child;
|
|
if (!patch.searchedForNeighboursAlready) {
|
|
// set the references to the neighbours
|
|
patch.rightNeighbour = findRightPatch(patch);
|
|
patch.bottomNeighbour = findDownPatch(patch);
|
|
patch.leftNeighbour = findLeftPatch(patch);
|
|
patch.topNeighbour = findTopPatch(patch);
|
|
patch.searchedForNeighboursAlready = true;
|
|
}
|
|
TerrainPatch right = patch.rightNeighbour;
|
|
TerrainPatch down = patch.bottomNeighbour;
|
|
TerrainPatch left = patch.leftNeighbour;
|
|
TerrainPatch top = patch.topNeighbour;
|
|
|
|
UpdatedTerrainPatch utp = updated.get(patch.getName());
|
|
if (utp == null) {
|
|
utp = new UpdatedTerrainPatch(patch, patch.lod);
|
|
updated.put(utp.getName(), utp);
|
|
}
|
|
|
|
if (right != null) {
|
|
UpdatedTerrainPatch utpR = updated.get(right.getName());
|
|
if (utpR == null) {
|
|
utpR = new UpdatedTerrainPatch(right);
|
|
updated.put(utpR.getName(), utpR);
|
|
utpR.setNewLod(right.lod);
|
|
}
|
|
utp.setRightLod(utpR.getNewLod());
|
|
utpR.setLeftLod(utp.getNewLod());
|
|
}
|
|
if (down != null) {
|
|
UpdatedTerrainPatch utpD = updated.get(down.getName());
|
|
if (utpD == null) {
|
|
utpD = new UpdatedTerrainPatch(down);
|
|
updated.put(utpD.getName(), utpD);
|
|
utpD.setNewLod(down.lod);
|
|
}
|
|
utp.setBottomLod(utpD.getNewLod());
|
|
utpD.setTopLod(utp.getNewLod());
|
|
}
|
|
|
|
if (left != null) {
|
|
UpdatedTerrainPatch utpL = updated.get(left.getName());
|
|
if (utpL == null) {
|
|
utpL = new UpdatedTerrainPatch(left);
|
|
updated.put(utpL.getName(), utpL);
|
|
utpL.setNewLod(left.lod);
|
|
}
|
|
utp.setLeftLod(utpL.getNewLod());
|
|
utpL.setRightLod(utp.getNewLod());
|
|
}
|
|
if (top != null) {
|
|
UpdatedTerrainPatch utpT = updated.get(top.getName());
|
|
if (utpT == null) {
|
|
utpT = new UpdatedTerrainPatch(top);
|
|
updated.put(utpT.getName(), utpT);
|
|
utpT.setNewLod(top.lod);
|
|
}
|
|
utp.setTopLod(utpT.getNewLod());
|
|
utpT.setBottomLod(utp.getNewLod());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Reset the cached references of neighbours.
|
|
* TerrainQuad caches neighbours for faster LOD checks.
|
|
* Sometimes you might want to reset this cache (for instance in TerrainGrid)
|
|
*/
|
|
public void resetCachedNeighbours() {
|
|
if (children != null) {
|
|
for (int x = children.size(); --x >= 0;) {
|
|
Spatial child = children.get(x);
|
|
if (child instanceof TerrainQuad) {
|
|
((TerrainQuad) child).resetCachedNeighbours();
|
|
} else if (child instanceof TerrainPatch) {
|
|
TerrainPatch patch = (TerrainPatch) child;
|
|
patch.searchedForNeighboursAlready = false;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Find any neighbours that should have their edges seamed because another neighbour
|
|
* changed its LOD to a greater value (less detailed)
|
|
*/
|
|
protected synchronized void fixEdges(HashMap<String,UpdatedTerrainPatch> updated) {
|
|
if (children != null) {
|
|
for (int x = children.size(); --x >= 0;) {
|
|
Spatial child = children.get(x);
|
|
if (child instanceof TerrainQuad) {
|
|
((TerrainQuad) child).fixEdges(updated);
|
|
} else if (child instanceof TerrainPatch) {
|
|
TerrainPatch patch = (TerrainPatch) child;
|
|
UpdatedTerrainPatch utp = updated.get(patch.getName());
|
|
|
|
if(utp != null && utp.lodChanged()) {
|
|
if (!patch.searchedForNeighboursAlready) {
|
|
// set the references to the neighbours
|
|
patch.rightNeighbour = findRightPatch(patch);
|
|
patch.bottomNeighbour = findDownPatch(patch);
|
|
patch.leftNeighbour = findLeftPatch(patch);
|
|
patch.topNeighbour = findTopPatch(patch);
|
|
patch.searchedForNeighboursAlready = true;
|
|
}
|
|
TerrainPatch right = patch.rightNeighbour;
|
|
TerrainPatch down = patch.bottomNeighbour;
|
|
TerrainPatch top = patch.topNeighbour;
|
|
TerrainPatch left = patch.leftNeighbour;
|
|
if (right != null) {
|
|
UpdatedTerrainPatch utpR = updated.get(right.getName());
|
|
if (utpR == null) {
|
|
utpR = new UpdatedTerrainPatch(right);
|
|
updated.put(utpR.getName(), utpR);
|
|
utpR.setNewLod(right.lod);
|
|
}
|
|
utpR.setLeftLod(utp.getNewLod());
|
|
utpR.setFixEdges(true);
|
|
}
|
|
if (down != null) {
|
|
UpdatedTerrainPatch utpD = updated.get(down.getName());
|
|
if (utpD == null) {
|
|
utpD = new UpdatedTerrainPatch(down);
|
|
updated.put(utpD.getName(), utpD);
|
|
utpD.setNewLod(down.lod);
|
|
}
|
|
utpD.setTopLod(utp.getNewLod());
|
|
utpD.setFixEdges(true);
|
|
}
|
|
if (top != null){
|
|
UpdatedTerrainPatch utpT = updated.get(top.getName());
|
|
if (utpT == null) {
|
|
utpT = new UpdatedTerrainPatch(top);
|
|
updated.put(utpT.getName(), utpT);
|
|
utpT.setNewLod(top.lod);
|
|
}
|
|
utpT.setBottomLod(utp.getNewLod());
|
|
utpT.setFixEdges(true);
|
|
}
|
|
if (left != null){
|
|
UpdatedTerrainPatch utpL = updated.get(left.getName());
|
|
if (utpL == null) {
|
|
utpL = new UpdatedTerrainPatch(left);
|
|
updated.put(utpL.getName(), utpL);
|
|
utpL.setNewLod(left.lod);
|
|
}
|
|
utpL.setRightLod(utp.getNewLod());
|
|
utpL.setFixEdges(true);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
protected synchronized void reIndexPages(HashMap<String,UpdatedTerrainPatch> updated, boolean usesVariableLod) {
|
|
if (children != null) {
|
|
for (int i = children.size(); --i >= 0;) {
|
|
Spatial child = children.get(i);
|
|
if (child instanceof TerrainQuad) {
|
|
((TerrainQuad) child).reIndexPages(updated, usesVariableLod);
|
|
} else if (child instanceof TerrainPatch) {
|
|
((TerrainPatch) child).reIndexGeometry(updated, usesVariableLod);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* <code>split</code> divides the heightmap data for four children. The
|
|
* children are either quads or patches. This is dependent on the size of the
|
|
* children. If the child's size is less than or equal to the set block
|
|
* size, then patches are created, otherwise, quads are created.
|
|
*
|
|
* @param blockSize
|
|
* the blocks size to test against.
|
|
* @param heightMap
|
|
* the height data.
|
|
*/
|
|
protected void split(int blockSize, float[] heightMap) {
|
|
if ((size >> 1) + 1 <= blockSize) {
|
|
createQuadPatch(heightMap);
|
|
} else {
|
|
createQuad(blockSize, heightMap);
|
|
}
|
|
|
|
}
|
|
|
|
/**
|
|
* Quadrants, world coordinates, and heightmap coordinates (Y-up):
|
|
*
|
|
* -z
|
|
* -u |
|
|
* -v 1|3
|
|
* -x ----+---- x
|
|
* 2|4 u
|
|
* | v
|
|
* z
|
|
* <code>createQuad</code> generates four new quads from this quad.
|
|
* The heightmap's top left (0,0) coordinate is at the bottom, -x,-z
|
|
* coordinate of the terrain, so it grows in the positive x.z direction.
|
|
*/
|
|
protected void createQuad(int blockSize, float[] heightMap) {
|
|
// create 4 terrain quads
|
|
int quarterSize = size >> 2;
|
|
|
|
int split = (size + 1) >> 1;
|
|
|
|
Vector2f tempOffset = new Vector2f();
|
|
offsetAmount += quarterSize;
|
|
|
|
//if (lodCalculator == null)
|
|
// lodCalculator = createDefaultLodCalculator(); // set a default one
|
|
|
|
// 1 upper left of heightmap, upper left quad
|
|
float[] heightBlock1 = createHeightSubBlock(heightMap, 0, 0, split);
|
|
|
|
Vector3f origin1 = new Vector3f(-quarterSize * stepScale.x, 0,
|
|
-quarterSize * stepScale.z);
|
|
|
|
tempOffset.x = offset.x;
|
|
tempOffset.y = offset.y;
|
|
tempOffset.x += origin1.x;
|
|
tempOffset.y += origin1.z;
|
|
|
|
TerrainQuad quad1 = new TerrainQuad(getName() + "Quad1", blockSize,
|
|
split, stepScale, heightBlock1, totalSize, tempOffset,
|
|
offsetAmount);
|
|
quad1.setLocalTranslation(origin1);
|
|
quad1.quadrant = 1;
|
|
this.attachChild(quad1);
|
|
|
|
// 2 lower left of heightmap, lower left quad
|
|
float[] heightBlock2 = createHeightSubBlock(heightMap, 0, split - 1,
|
|
split);
|
|
|
|
Vector3f origin2 = new Vector3f(-quarterSize * stepScale.x, 0,
|
|
quarterSize * stepScale.z);
|
|
|
|
tempOffset = new Vector2f();
|
|
tempOffset.x = offset.x;
|
|
tempOffset.y = offset.y;
|
|
tempOffset.x += origin2.x;
|
|
tempOffset.y += origin2.z;
|
|
|
|
TerrainQuad quad2 = new TerrainQuad(getName() + "Quad2", blockSize,
|
|
split, stepScale, heightBlock2, totalSize, tempOffset,
|
|
offsetAmount);
|
|
quad2.setLocalTranslation(origin2);
|
|
quad2.quadrant = 2;
|
|
this.attachChild(quad2);
|
|
|
|
// 3 upper right of heightmap, upper right quad
|
|
float[] heightBlock3 = createHeightSubBlock(heightMap, split - 1, 0,
|
|
split);
|
|
|
|
Vector3f origin3 = new Vector3f(quarterSize * stepScale.x, 0,
|
|
-quarterSize * stepScale.z);
|
|
|
|
tempOffset = new Vector2f();
|
|
tempOffset.x = offset.x;
|
|
tempOffset.y = offset.y;
|
|
tempOffset.x += origin3.x;
|
|
tempOffset.y += origin3.z;
|
|
|
|
TerrainQuad quad3 = new TerrainQuad(getName() + "Quad3", blockSize,
|
|
split, stepScale, heightBlock3, totalSize, tempOffset,
|
|
offsetAmount);
|
|
quad3.setLocalTranslation(origin3);
|
|
quad3.quadrant = 3;
|
|
this.attachChild(quad3);
|
|
|
|
// 4 lower right of heightmap, lower right quad
|
|
float[] heightBlock4 = createHeightSubBlock(heightMap, split - 1,
|
|
split - 1, split);
|
|
|
|
Vector3f origin4 = new Vector3f(quarterSize * stepScale.x, 0,
|
|
quarterSize * stepScale.z);
|
|
|
|
tempOffset = new Vector2f();
|
|
tempOffset.x = offset.x;
|
|
tempOffset.y = offset.y;
|
|
tempOffset.x += origin4.x;
|
|
tempOffset.y += origin4.z;
|
|
|
|
TerrainQuad quad4 = new TerrainQuad(getName() + "Quad4", blockSize,
|
|
split, stepScale, heightBlock4, totalSize, tempOffset,
|
|
offsetAmount);
|
|
quad4.setLocalTranslation(origin4);
|
|
quad4.quadrant = 4;
|
|
this.attachChild(quad4);
|
|
|
|
}
|
|
|
|
public void generateDebugTangents(Material mat) {
|
|
for (int x = children.size(); --x >= 0;) {
|
|
Spatial child = children.get(x);
|
|
if (child instanceof TerrainQuad) {
|
|
((TerrainQuad)child).generateDebugTangents(mat);
|
|
} else if (child instanceof TerrainPatch) {
|
|
Geometry debug = new Geometry( "Debug " + name,
|
|
TangentBinormalGenerator.genTbnLines( ((TerrainPatch)child).getMesh(), 0.8f));
|
|
attachChild(debug);
|
|
debug.setLocalTranslation(child.getLocalTranslation());
|
|
debug.setCullHint(CullHint.Never);
|
|
debug.setMaterial(mat);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* <code>createQuadPatch</code> creates four child patches from this quad.
|
|
*/
|
|
protected void createQuadPatch(float[] heightMap) {
|
|
// create 4 terrain patches
|
|
int quarterSize = size >> 2;
|
|
int halfSize = size >> 1;
|
|
int split = (size + 1) >> 1;
|
|
|
|
//if (lodCalculator == null)
|
|
// lodCalculator = createDefaultLodCalculator(); // set a default one
|
|
|
|
offsetAmount += quarterSize;
|
|
|
|
// 1 lower left
|
|
float[] heightBlock1 = createHeightSubBlock(heightMap, 0, 0, split);
|
|
|
|
Vector3f origin1 = new Vector3f(-halfSize * stepScale.x, 0, -halfSize
|
|
* stepScale.z);
|
|
|
|
Vector2f tempOffset1 = new Vector2f();
|
|
tempOffset1.x = offset.x;
|
|
tempOffset1.y = offset.y;
|
|
tempOffset1.x += origin1.x / 2;
|
|
tempOffset1.y += origin1.z / 2;
|
|
|
|
TerrainPatch patch1 = new TerrainPatch(getName() + "Patch1", split,
|
|
stepScale, heightBlock1, origin1, totalSize, tempOffset1,
|
|
offsetAmount);
|
|
patch1.setQuadrant((short) 1);
|
|
this.attachChild(patch1);
|
|
patch1.setModelBound(new BoundingBox());
|
|
patch1.updateModelBound();
|
|
//patch1.setLodCalculator(lodCalculator);
|
|
//TangentBinormalGenerator.generate(patch1);
|
|
|
|
// 2 upper left
|
|
float[] heightBlock2 = createHeightSubBlock(heightMap, 0, split - 1,
|
|
split);
|
|
|
|
Vector3f origin2 = new Vector3f(-halfSize * stepScale.x, 0, 0);
|
|
|
|
Vector2f tempOffset2 = new Vector2f();
|
|
tempOffset2.x = offset.x;
|
|
tempOffset2.y = offset.y;
|
|
tempOffset2.x += origin1.x / 2;
|
|
tempOffset2.y += quarterSize * stepScale.z;
|
|
|
|
TerrainPatch patch2 = new TerrainPatch(getName() + "Patch2", split,
|
|
stepScale, heightBlock2, origin2, totalSize, tempOffset2,
|
|
offsetAmount);
|
|
patch2.setQuadrant((short) 2);
|
|
this.attachChild(patch2);
|
|
patch2.setModelBound(new BoundingBox());
|
|
patch2.updateModelBound();
|
|
//patch2.setLodCalculator(lodCalculator);
|
|
//TangentBinormalGenerator.generate(patch2);
|
|
|
|
// 3 lower right
|
|
float[] heightBlock3 = createHeightSubBlock(heightMap, split - 1, 0,
|
|
split);
|
|
|
|
Vector3f origin3 = new Vector3f(0, 0, -halfSize * stepScale.z);
|
|
|
|
Vector2f tempOffset3 = new Vector2f();
|
|
tempOffset3.x = offset.x;
|
|
tempOffset3.y = offset.y;
|
|
tempOffset3.x += quarterSize * stepScale.x;
|
|
tempOffset3.y += origin3.z / 2;
|
|
|
|
TerrainPatch patch3 = new TerrainPatch(getName() + "Patch3", split,
|
|
stepScale, heightBlock3, origin3, totalSize, tempOffset3,
|
|
offsetAmount);
|
|
patch3.setQuadrant((short) 3);
|
|
this.attachChild(patch3);
|
|
patch3.setModelBound(new BoundingBox());
|
|
patch3.updateModelBound();
|
|
//patch3.setLodCalculator(lodCalculator);
|
|
//TangentBinormalGenerator.generate(patch3);
|
|
|
|
// 4 upper right
|
|
float[] heightBlock4 = createHeightSubBlock(heightMap, split - 1,
|
|
split - 1, split);
|
|
|
|
Vector3f origin4 = new Vector3f(0, 0, 0);
|
|
|
|
Vector2f tempOffset4 = new Vector2f();
|
|
tempOffset4.x = offset.x;
|
|
tempOffset4.y = offset.y;
|
|
tempOffset4.x += quarterSize * stepScale.x;
|
|
tempOffset4.y += quarterSize * stepScale.z;
|
|
|
|
TerrainPatch patch4 = new TerrainPatch(getName() + "Patch4", split,
|
|
stepScale, heightBlock4, origin4, totalSize, tempOffset4,
|
|
offsetAmount);
|
|
patch4.setQuadrant((short) 4);
|
|
this.attachChild(patch4);
|
|
patch4.setModelBound(new BoundingBox());
|
|
patch4.updateModelBound();
|
|
//patch4.setLodCalculator(lodCalculator);
|
|
//TangentBinormalGenerator.generate(patch4);
|
|
}
|
|
|
|
public float[] createHeightSubBlock(float[] heightMap, int x,
|
|
int y, int side) {
|
|
float[] rVal = new float[side * side];
|
|
int bsize = (int) FastMath.sqrt(heightMap.length);
|
|
int count = 0;
|
|
for (int i = y; i < side + y; i++) {
|
|
for (int j = x; j < side + x; j++) {
|
|
if (j < bsize && i < bsize)
|
|
rVal[count] = heightMap[j + (i * bsize)];
|
|
count++;
|
|
}
|
|
}
|
|
return rVal;
|
|
}
|
|
|
|
/**
|
|
* A handy method that will attach all bounding boxes of this terrain
|
|
* to the node you supply.
|
|
* Useful to visualize the bounding boxes when debugging.
|
|
*
|
|
* @param parent that will get the bounding box shapes of the terrain attached to
|
|
*/
|
|
public void attachBoundChildren(Node parent) {
|
|
for (int i = 0; i < this.getQuantity(); i++) {
|
|
if (this.getChild(i) instanceof TerrainQuad) {
|
|
((TerrainQuad) getChild(i)).attachBoundChildren(parent);
|
|
} else if (this.getChild(i) instanceof TerrainPatch) {
|
|
BoundingVolume bv = getChild(i).getWorldBound();
|
|
if (bv instanceof BoundingBox) {
|
|
attachBoundingBox((BoundingBox)bv, parent);
|
|
}
|
|
}
|
|
}
|
|
BoundingVolume bv = getWorldBound();
|
|
if (bv instanceof BoundingBox) {
|
|
attachBoundingBox((BoundingBox)bv, parent);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* used by attachBoundChildren()
|
|
*/
|
|
private void attachBoundingBox(BoundingBox bb, Node parent) {
|
|
WireBox wb = new WireBox(bb.getXExtent(), bb.getYExtent(), bb.getZExtent());
|
|
Geometry g = new Geometry();
|
|
g.setMesh(wb);
|
|
g.setLocalTranslation(bb.getCenter());
|
|
parent.attachChild(g);
|
|
}
|
|
|
|
/**
|
|
* Signal if the normal vectors for the terrain need to be recalculated.
|
|
* Does this by looking at the affectedAreaBBox bounding box. If the bbox
|
|
* exists already, then it will grow the box to fit the new changedPoint.
|
|
* If the affectedAreaBBox is null, then it will create one of unit size.
|
|
*
|
|
* @param needToRecalculateNormals if null, will cause needToRecalculateNormals() to return false
|
|
*/
|
|
protected void setNormalRecalcNeeded(Vector2f changedPoint) {
|
|
if (changedPoint == null) { // set needToRecalculateNormals() to false
|
|
affectedAreaBBox = null;
|
|
return;
|
|
}
|
|
|
|
if (affectedAreaBBox == null) {
|
|
affectedAreaBBox = new BoundingBox(new Vector3f(changedPoint.x, 0, changedPoint.y), 1f, Float.MAX_VALUE, 1f); // unit length
|
|
} else {
|
|
// adjust size of box to be larger
|
|
affectedAreaBBox.mergeLocal(new BoundingBox(new Vector3f(changedPoint.x, 0, changedPoint.y), 1f, Float.MAX_VALUE, 1f));
|
|
}
|
|
}
|
|
|
|
protected boolean needToRecalculateNormals() {
|
|
if (affectedAreaBBox != null)
|
|
return true;
|
|
if (!lastScale.equals(getWorldScale())) {
|
|
affectedAreaBBox = new BoundingBox(getWorldTranslation(), Float.MAX_VALUE, Float.MAX_VALUE, Float.MAX_VALUE);
|
|
lastScale = getWorldScale();
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* This will cause all normals for this terrain quad to be recalculated
|
|
*/
|
|
protected void setNeedToRecalculateNormals() {
|
|
affectedAreaBBox = new BoundingBox(getWorldTranslation(), Float.MAX_VALUE, Float.MAX_VALUE, Float.MAX_VALUE);
|
|
}
|
|
|
|
public float getHeightmapHeight(Vector2f xz) {
|
|
// offset
|
|
int halfSize = totalSize / 2;
|
|
int x = Math.round((xz.x / getWorldScale().x) + halfSize);
|
|
int z = Math.round((xz.y / getWorldScale().z) + halfSize);
|
|
|
|
if (!isInside(x, z))
|
|
return Float.NaN;
|
|
return getHeightmapHeight(x, z);
|
|
}
|
|
|
|
/**
|
|
* This will just get the heightmap value at the supplied point,
|
|
* not an interpolated (actual) height value.
|
|
*/
|
|
protected float getHeightmapHeight(int x, int z) {
|
|
int quad = findQuadrant(x, z);
|
|
int split = (size + 1) >> 1;
|
|
if (children != null) {
|
|
for (int i = children.size(); --i >= 0;) {
|
|
Spatial spat = children.get(i);
|
|
int col = x;
|
|
int row = z;
|
|
boolean match = false;
|
|
|
|
// get the childs quadrant
|
|
int childQuadrant = 0;
|
|
if (spat instanceof TerrainQuad) {
|
|
childQuadrant = ((TerrainQuad) spat).getQuadrant();
|
|
} else if (spat instanceof TerrainPatch) {
|
|
childQuadrant = ((TerrainPatch) spat).getQuadrant();
|
|
}
|
|
|
|
if (childQuadrant == 1 && (quad & 1) != 0) {
|
|
match = true;
|
|
} else if (childQuadrant == 2 && (quad & 2) != 0) {
|
|
row = z - split + 1;
|
|
match = true;
|
|
} else if (childQuadrant == 3 && (quad & 4) != 0) {
|
|
col = x - split + 1;
|
|
match = true;
|
|
} else if (childQuadrant == 4 && (quad & 8) != 0) {
|
|
col = x - split + 1;
|
|
row = z - split + 1;
|
|
match = true;
|
|
}
|
|
|
|
if (match) {
|
|
if (spat instanceof TerrainQuad) {
|
|
return ((TerrainQuad) spat).getHeightmapHeight(col, row);
|
|
} else if (spat instanceof TerrainPatch) {
|
|
return ((TerrainPatch) spat).getHeightmapHeight(col, row);
|
|
}
|
|
}
|
|
|
|
}
|
|
}
|
|
return Float.NaN;
|
|
}
|
|
|
|
protected Vector3f getMeshNormal(int x, int z) {
|
|
int quad = findQuadrant(x, z);
|
|
int split = (size + 1) >> 1;
|
|
if (children != null) {
|
|
for (int i = children.size(); --i >= 0;) {
|
|
Spatial spat = children.get(i);
|
|
int col = x;
|
|
int row = z;
|
|
boolean match = false;
|
|
|
|
// get the childs quadrant
|
|
int childQuadrant = 0;
|
|
if (spat instanceof TerrainQuad) {
|
|
childQuadrant = ((TerrainQuad) spat).getQuadrant();
|
|
} else if (spat instanceof TerrainPatch) {
|
|
childQuadrant = ((TerrainPatch) spat).getQuadrant();
|
|
}
|
|
|
|
if (childQuadrant == 1 && (quad & 1) != 0) {
|
|
match = true;
|
|
} else if (childQuadrant == 2 && (quad & 2) != 0) {
|
|
row = z - split + 1;
|
|
match = true;
|
|
} else if (childQuadrant == 3 && (quad & 4) != 0) {
|
|
col = x - split + 1;
|
|
match = true;
|
|
} else if (childQuadrant == 4 && (quad & 8) != 0) {
|
|
col = x - split + 1;
|
|
row = z - split + 1;
|
|
match = true;
|
|
}
|
|
|
|
if (match) {
|
|
if (spat instanceof TerrainQuad) {
|
|
return ((TerrainQuad) spat).getMeshNormal(col, row);
|
|
} else if (spat instanceof TerrainPatch) {
|
|
return ((TerrainPatch) spat).getMeshNormal(col, row);
|
|
}
|
|
}
|
|
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* is the 2d point inside the terrain?
|
|
* @param x local coordinate
|
|
* @param z local coordinate
|
|
*/
|
|
private boolean isInside(int x, int z) {
|
|
if (x < 0 || z < 0 || x > totalSize || z > totalSize)
|
|
return false;
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Used for searching for a child and keeping
|
|
* track of its quadrant
|
|
*/
|
|
private class QuadrantChild {
|
|
int col;
|
|
int row;
|
|
Spatial child;
|
|
|
|
QuadrantChild(int col, int row, Spatial child) {
|
|
this.col = col;
|
|
this.row = row;
|
|
this.child = child;
|
|
}
|
|
}
|
|
|
|
private QuadrantChild findMatchingChild(int x, int z) {
|
|
int quad = findQuadrant(x, z);
|
|
int split = (size + 1) >> 1;
|
|
if (children != null) {
|
|
for (int i = children.size(); --i >= 0;) {
|
|
Spatial spat = children.get(i);
|
|
int col = x;
|
|
int row = z;
|
|
boolean match = false;
|
|
|
|
// get the childs quadrant
|
|
int childQuadrant = 0;
|
|
if (spat instanceof TerrainQuad) {
|
|
childQuadrant = ((TerrainQuad) spat).getQuadrant();
|
|
} else if (spat instanceof TerrainPatch) {
|
|
childQuadrant = ((TerrainPatch) spat).getQuadrant();
|
|
}
|
|
|
|
if (childQuadrant == 1 && (quad & 1) != 0) {
|
|
match = true;
|
|
} else if (childQuadrant == 2 && (quad & 2) != 0) {
|
|
row = z - split + 1;
|
|
match = true;
|
|
} else if (childQuadrant == 3 && (quad & 4) != 0) {
|
|
col = x - split + 1;
|
|
match = true;
|
|
} else if (childQuadrant == 4 && (quad & 8) != 0) {
|
|
col = x - split + 1;
|
|
row = z - split + 1;
|
|
match = true;
|
|
}
|
|
if (match)
|
|
return new QuadrantChild(col, row, spat);
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Get the interpolated height of the terrain at the specified point.
|
|
* @param xz the location to get the height for
|
|
* @return Float.NAN if the value does not exist, or the coordinates are outside of the terrain
|
|
*/
|
|
public float getHeight(Vector2f xz) {
|
|
// offset
|
|
float x = (float)(((xz.x - getWorldTranslation().x) / getWorldScale().x) + (float)(totalSize-1) / 2f);
|
|
float z = (float)(((xz.y - getWorldTranslation().z) / getWorldScale().z) + (float)(totalSize-1) / 2f);
|
|
if (!isInside((int)x, (int)z))
|
|
return Float.NaN;
|
|
float height = getHeight((int)x, (int)z, (x%1f), (z%1f));
|
|
height *= getWorldScale().y;
|
|
return height;
|
|
}
|
|
|
|
/*
|
|
* gets an interpolated value at the specified point
|
|
*/
|
|
protected float getHeight(int x, int z, float xm, float zm) {
|
|
|
|
QuadrantChild match = findMatchingChild(x,z);
|
|
if (match != null) {
|
|
if (match.child instanceof TerrainQuad) {
|
|
return ((TerrainQuad) match.child).getHeight(match.col, match.row, xm, zm);
|
|
} else if (match.child instanceof TerrainPatch) {
|
|
return ((TerrainPatch) match.child).getHeight(match.col, match.row, xm, zm);
|
|
}
|
|
}
|
|
return Float.NaN;
|
|
}
|
|
|
|
public Vector3f getNormal(Vector2f xz) {
|
|
// offset
|
|
float x = (float)(((xz.x - getWorldTranslation().x) / getWorldScale().x) + (float)(totalSize-1) / 2f);
|
|
float z = (float)(((xz.y - getWorldTranslation().z) / getWorldScale().z) + (float)(totalSize-1) / 2f);
|
|
Vector3f normal = getNormal(x, z, xz);
|
|
|
|
return normal;
|
|
}
|
|
|
|
protected Vector3f getNormal(float x, float z, Vector2f xz) {
|
|
x-=0.5f;
|
|
z-=0.5f;
|
|
float col = FastMath.floor(x);
|
|
float row = FastMath.floor(z);
|
|
boolean onX = false;
|
|
if(1 - (x - col)-(z - row) < 0) // what triangle to interpolate on
|
|
onX = true;
|
|
// v1--v2 ^
|
|
// | / | |
|
|
// | / | |
|
|
// v3--v4 | Z
|
|
// |
|
|
// <-------Y
|
|
// X
|
|
Vector3f n1 = getMeshNormal((int) FastMath.ceil(x), (int) FastMath.ceil(z));
|
|
Vector3f n2 = getMeshNormal((int) FastMath.floor(x), (int) FastMath.ceil(z));
|
|
Vector3f n3 = getMeshNormal((int) FastMath.ceil(x), (int) FastMath.floor(z));
|
|
Vector3f n4 = getMeshNormal((int) FastMath.floor(x), (int) FastMath.floor(z));
|
|
|
|
return n1.add(n2).add(n3).add(n4).normalize();
|
|
}
|
|
|
|
public void setHeight(Vector2f xz, float height) {
|
|
List<Vector2f> coord = new ArrayList<Vector2f>();
|
|
coord.add(xz);
|
|
List<Float> h = new ArrayList<Float>();
|
|
h.add(height);
|
|
|
|
setHeight(coord, h);
|
|
}
|
|
|
|
public void adjustHeight(Vector2f xz, float delta) {
|
|
List<Vector2f> coord = new ArrayList<Vector2f>();
|
|
coord.add(xz);
|
|
List<Float> h = new ArrayList<Float>();
|
|
h.add(delta);
|
|
|
|
adjustHeight(coord, h);
|
|
}
|
|
|
|
public void setHeight(List<Vector2f> xz, List<Float> height) {
|
|
setHeight(xz, height, true);
|
|
}
|
|
|
|
public void adjustHeight(List<Vector2f> xz, List<Float> height) {
|
|
setHeight(xz, height, false);
|
|
}
|
|
|
|
protected void setHeight(List<Vector2f> xz, List<Float> height, boolean overrideHeight) {
|
|
if (xz.size() != height.size())
|
|
throw new IllegalArgumentException("Both lists must be the same length!");
|
|
|
|
int halfSize = totalSize / 2;
|
|
|
|
List<LocationHeight> locations = new ArrayList<LocationHeight>();
|
|
|
|
// offset
|
|
for (int i=0; i<xz.size(); i++) {
|
|
int x = Math.round((xz.get(i).x / getWorldScale().x) + halfSize);
|
|
int z = Math.round((xz.get(i).y / getWorldScale().z) + halfSize);
|
|
if (!isInside(x, z))
|
|
continue;
|
|
locations.add(new LocationHeight(x,z,height.get(i)));
|
|
}
|
|
|
|
setHeight(locations, overrideHeight); // adjust height of the actual mesh
|
|
|
|
// signal that the normals need updating
|
|
for (int i=0; i<xz.size(); i++)
|
|
setNormalRecalcNeeded(xz.get(i) );
|
|
}
|
|
|
|
protected class LocationHeight {
|
|
int x;
|
|
int z;
|
|
float h;
|
|
|
|
LocationHeight(){}
|
|
|
|
LocationHeight(int x, int z, float h){
|
|
this.x = x;
|
|
this.z = z;
|
|
this.h = h;
|
|
}
|
|
}
|
|
|
|
protected void setHeight(List<LocationHeight> locations, boolean overrideHeight) {
|
|
if (children == null)
|
|
return;
|
|
|
|
List<LocationHeight> quadLH1 = new ArrayList<LocationHeight>();
|
|
List<LocationHeight> quadLH2 = new ArrayList<LocationHeight>();
|
|
List<LocationHeight> quadLH3 = new ArrayList<LocationHeight>();
|
|
List<LocationHeight> quadLH4 = new ArrayList<LocationHeight>();
|
|
Spatial quad1 = null;
|
|
Spatial quad2 = null;
|
|
Spatial quad3 = null;
|
|
Spatial quad4 = null;
|
|
|
|
// get the child quadrants
|
|
for (int i = children.size(); --i >= 0;) {
|
|
Spatial spat = children.get(i);
|
|
int childQuadrant = 0;
|
|
if (spat instanceof TerrainQuad) {
|
|
childQuadrant = ((TerrainQuad) spat).getQuadrant();
|
|
} else if (spat instanceof TerrainPatch) {
|
|
childQuadrant = ((TerrainPatch) spat).getQuadrant();
|
|
}
|
|
|
|
if (childQuadrant == 1)
|
|
quad1 = spat;
|
|
else if (childQuadrant == 2)
|
|
quad2 = spat;
|
|
else if (childQuadrant == 3)
|
|
quad3 = spat;
|
|
else if (childQuadrant == 4)
|
|
quad4 = spat;
|
|
}
|
|
|
|
int split = (size + 1) >> 1;
|
|
|
|
// distribute each locationHeight into the quadrant it intersects
|
|
for (LocationHeight lh : locations) {
|
|
int quad = findQuadrant(lh.x, lh.z);
|
|
int col = lh.x;
|
|
int row = lh.z;
|
|
|
|
if ((quad & 1) != 0) {
|
|
quadLH1.add(lh);
|
|
}
|
|
if ((quad & 2) != 0) {
|
|
row = lh.z - split + 1;
|
|
quadLH2.add(new LocationHeight(lh.x, row, lh.h));
|
|
}
|
|
if ((quad & 4) != 0) {
|
|
col = lh.x - split + 1;
|
|
quadLH3.add(new LocationHeight(col, lh.z, lh.h));
|
|
}
|
|
if ((quad & 8) != 0) {
|
|
col = lh.x - split + 1;
|
|
row = lh.z - split + 1;
|
|
quadLH4.add(new LocationHeight(col, row, lh.h));
|
|
}
|
|
}
|
|
|
|
// send the locations to the children
|
|
if (!quadLH1.isEmpty()) {
|
|
if (quad1 instanceof TerrainQuad)
|
|
((TerrainQuad)quad1).setHeight(quadLH1, overrideHeight);
|
|
else if(quad1 instanceof TerrainPatch)
|
|
((TerrainPatch)quad1).setHeight(quadLH1, overrideHeight);
|
|
}
|
|
|
|
if (!quadLH2.isEmpty()) {
|
|
if (quad2 instanceof TerrainQuad)
|
|
((TerrainQuad)quad2).setHeight(quadLH2, overrideHeight);
|
|
else if(quad2 instanceof TerrainPatch)
|
|
((TerrainPatch)quad2).setHeight(quadLH2, overrideHeight);
|
|
}
|
|
|
|
if (!quadLH3.isEmpty()) {
|
|
if (quad3 instanceof TerrainQuad)
|
|
((TerrainQuad)quad3).setHeight(quadLH3, overrideHeight);
|
|
else if(quad3 instanceof TerrainPatch)
|
|
((TerrainPatch)quad3).setHeight(quadLH3, overrideHeight);
|
|
}
|
|
|
|
if (!quadLH4.isEmpty()) {
|
|
if (quad4 instanceof TerrainQuad)
|
|
((TerrainQuad)quad4).setHeight(quadLH4, overrideHeight);
|
|
else if(quad4 instanceof TerrainPatch)
|
|
((TerrainPatch)quad4).setHeight(quadLH4, overrideHeight);
|
|
}
|
|
}
|
|
|
|
protected boolean isPointOnTerrain(int x, int z) {
|
|
return (x >= 0 && x <= totalSize && z >= 0 && z <= totalSize);
|
|
}
|
|
|
|
|
|
public int getTerrainSize() {
|
|
return totalSize;
|
|
}
|
|
|
|
|
|
// a position can be in multiple quadrants, so use a bit anded value.
|
|
private int findQuadrant(int x, int y) {
|
|
int split = (size + 1) >> 1;
|
|
int quads = 0;
|
|
if (x < split && y < split)
|
|
quads |= 1;
|
|
if (x < split && y >= split - 1)
|
|
quads |= 2;
|
|
if (x >= split - 1 && y < split)
|
|
quads |= 4;
|
|
if (x >= split - 1 && y >= split - 1)
|
|
quads |= 8;
|
|
return quads;
|
|
}
|
|
|
|
/**
|
|
* lock or unlock the meshes of this terrain.
|
|
* Locked meshes are uneditable but have better performance.
|
|
* @param locked or unlocked
|
|
*/
|
|
public void setLocked(boolean locked) {
|
|
for (int i = 0; i < this.getQuantity(); i++) {
|
|
if (this.getChild(i) instanceof TerrainQuad) {
|
|
((TerrainQuad) getChild(i)).setLocked(locked);
|
|
} else if (this.getChild(i) instanceof TerrainPatch) {
|
|
if (locked)
|
|
((TerrainPatch) getChild(i)).lockMesh();
|
|
else
|
|
((TerrainPatch) getChild(i)).unlockMesh();
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
public int getQuadrant() {
|
|
return quadrant;
|
|
}
|
|
|
|
public void setQuadrant(short quadrant) {
|
|
this.quadrant = quadrant;
|
|
}
|
|
|
|
|
|
protected TerrainPatch getPatch(int quad) {
|
|
if (children != null)
|
|
for (int x = children.size(); --x >= 0;) {
|
|
Spatial child = children.get(x);
|
|
if (child instanceof TerrainPatch) {
|
|
TerrainPatch tb = (TerrainPatch) child;
|
|
if (tb.getQuadrant() == quad)
|
|
return tb;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
protected TerrainQuad getQuad(int quad) {
|
|
if (quad == 0)
|
|
return this;
|
|
if (children != null)
|
|
for (int x = children.size(); --x >= 0;) {
|
|
Spatial child = children.get(x);
|
|
if (child instanceof TerrainQuad) {
|
|
TerrainQuad tq = (TerrainQuad) child;
|
|
if (tq.getQuadrant() == quad)
|
|
return tq;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
protected TerrainPatch findRightPatch(TerrainPatch tp) {
|
|
if (tp.getQuadrant() == 1)
|
|
return getPatch(3);
|
|
else if (tp.getQuadrant() == 2)
|
|
return getPatch(4);
|
|
else if (tp.getQuadrant() == 3) {
|
|
// find the patch to the right and ask it for child 1.
|
|
TerrainQuad quad = findRightQuad();
|
|
if (quad != null)
|
|
return quad.getPatch(1);
|
|
} else if (tp.getQuadrant() == 4) {
|
|
// find the patch to the right and ask it for child 2.
|
|
TerrainQuad quad = findRightQuad();
|
|
if (quad != null)
|
|
return quad.getPatch(2);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
protected TerrainPatch findDownPatch(TerrainPatch tp) {
|
|
if (tp.getQuadrant() == 1)
|
|
return getPatch(2);
|
|
else if (tp.getQuadrant() == 3)
|
|
return getPatch(4);
|
|
else if (tp.getQuadrant() == 2) {
|
|
// find the patch below and ask it for child 1.
|
|
TerrainQuad quad = findDownQuad();
|
|
if (quad != null)
|
|
return quad.getPatch(1);
|
|
} else if (tp.getQuadrant() == 4) {
|
|
TerrainQuad quad = findDownQuad();
|
|
if (quad != null)
|
|
return quad.getPatch(3);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
|
|
protected TerrainPatch findTopPatch(TerrainPatch tp) {
|
|
if (tp.getQuadrant() == 2)
|
|
return getPatch(1);
|
|
else if (tp.getQuadrant() == 4)
|
|
return getPatch(3);
|
|
else if (tp.getQuadrant() == 1) {
|
|
// find the patch above and ask it for child 2.
|
|
TerrainQuad quad = findTopQuad();
|
|
if (quad != null)
|
|
return quad.getPatch(2);
|
|
} else if (tp.getQuadrant() == 3) {
|
|
TerrainQuad quad = findTopQuad();
|
|
if (quad != null)
|
|
return quad.getPatch(4);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
protected TerrainPatch findLeftPatch(TerrainPatch tp) {
|
|
if (tp.getQuadrant() == 3)
|
|
return getPatch(1);
|
|
else if (tp.getQuadrant() == 4)
|
|
return getPatch(2);
|
|
else if (tp.getQuadrant() == 1) {
|
|
// find the patch above and ask it for child 3.
|
|
TerrainQuad quad = findLeftQuad();
|
|
if (quad != null)
|
|
return quad.getPatch(3);
|
|
} else if (tp.getQuadrant() == 2) {
|
|
TerrainQuad quad = findLeftQuad();
|
|
if (quad != null)
|
|
return quad.getPatch(4);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
protected TerrainQuad findRightQuad() {
|
|
boolean useFinder = false;
|
|
if (getParent() == null || !(getParent() instanceof TerrainQuad)) {
|
|
if (neighbourFinder == null)
|
|
return null;
|
|
else
|
|
useFinder = true;
|
|
}
|
|
|
|
TerrainQuad pQuad = null;
|
|
if (!useFinder)
|
|
pQuad = (TerrainQuad) getParent();
|
|
|
|
if (quadrant == 1)
|
|
return pQuad.getQuad(3);
|
|
else if (quadrant == 2)
|
|
return pQuad.getQuad(4);
|
|
else if (quadrant == 3) {
|
|
TerrainQuad quad = pQuad.findRightQuad();
|
|
if (quad != null)
|
|
return quad.getQuad(1);
|
|
} else if (quadrant == 4) {
|
|
TerrainQuad quad = pQuad.findRightQuad();
|
|
if (quad != null)
|
|
return quad.getQuad(2);
|
|
} else if (quadrant == 0) {
|
|
// at the top quad
|
|
if (useFinder) {
|
|
TerrainQuad quad = neighbourFinder.getRightQuad(this);
|
|
return quad;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
protected TerrainQuad findDownQuad() {
|
|
boolean useFinder = false;
|
|
if (getParent() == null || !(getParent() instanceof TerrainQuad)) {
|
|
if (neighbourFinder == null)
|
|
return null;
|
|
else
|
|
useFinder = true;
|
|
}
|
|
|
|
TerrainQuad pQuad = null;
|
|
if (!useFinder)
|
|
pQuad = (TerrainQuad) getParent();
|
|
|
|
if (quadrant == 1)
|
|
return pQuad.getQuad(2);
|
|
else if (quadrant == 3)
|
|
return pQuad.getQuad(4);
|
|
else if (quadrant == 2) {
|
|
TerrainQuad quad = pQuad.findDownQuad();
|
|
if (quad != null)
|
|
return quad.getQuad(1);
|
|
} else if (quadrant == 4) {
|
|
TerrainQuad quad = pQuad.findDownQuad();
|
|
if (quad != null)
|
|
return quad.getQuad(3);
|
|
} else if (quadrant == 0) {
|
|
// at the top quad
|
|
if (useFinder) {
|
|
TerrainQuad quad = neighbourFinder.getDownQuad(this);
|
|
return quad;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
protected TerrainQuad findTopQuad() {
|
|
boolean useFinder = false;
|
|
if (getParent() == null || !(getParent() instanceof TerrainQuad)) {
|
|
if (neighbourFinder == null)
|
|
return null;
|
|
else
|
|
useFinder = true;
|
|
}
|
|
|
|
TerrainQuad pQuad = null;
|
|
if (!useFinder)
|
|
pQuad = (TerrainQuad) getParent();
|
|
|
|
if (quadrant == 2)
|
|
return pQuad.getQuad(1);
|
|
else if (quadrant == 4)
|
|
return pQuad.getQuad(3);
|
|
else if (quadrant == 1) {
|
|
TerrainQuad quad = pQuad.findTopQuad();
|
|
if (quad != null)
|
|
return quad.getQuad(2);
|
|
} else if (quadrant == 3) {
|
|
TerrainQuad quad = pQuad.findTopQuad();
|
|
if (quad != null)
|
|
return quad.getQuad(4);
|
|
} else if (quadrant == 0) {
|
|
// at the top quad
|
|
if (useFinder) {
|
|
TerrainQuad quad = neighbourFinder.getTopQuad(this);
|
|
return quad;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
protected TerrainQuad findLeftQuad() {
|
|
boolean useFinder = false;
|
|
if (getParent() == null || !(getParent() instanceof TerrainQuad)) {
|
|
if (neighbourFinder == null)
|
|
return null;
|
|
else
|
|
useFinder = true;
|
|
}
|
|
|
|
TerrainQuad pQuad = null;
|
|
if (!useFinder)
|
|
pQuad = (TerrainQuad) getParent();
|
|
|
|
if (quadrant == 3)
|
|
return pQuad.getQuad(1);
|
|
else if (quadrant == 4)
|
|
return pQuad.getQuad(2);
|
|
else if (quadrant == 1) {
|
|
TerrainQuad quad = pQuad.findLeftQuad();
|
|
if (quad != null)
|
|
return quad.getQuad(3);
|
|
} else if (quadrant == 2) {
|
|
TerrainQuad quad = pQuad.findLeftQuad();
|
|
if (quad != null)
|
|
return quad.getQuad(4);
|
|
} else if (quadrant == 0) {
|
|
// at the top quad
|
|
if (useFinder) {
|
|
TerrainQuad quad = neighbourFinder.getLeftQuad(this);
|
|
return quad;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Find what terrain patches need normal recalculations and update
|
|
* their normals;
|
|
*/
|
|
protected void fixNormals(BoundingBox affectedArea) {
|
|
if (children == null)
|
|
return;
|
|
|
|
// go through the children and see if they collide with the affectedAreaBBox
|
|
// if they do, then update their normals
|
|
for (int x = children.size(); --x >= 0;) {
|
|
Spatial child = children.get(x);
|
|
if (child instanceof TerrainQuad) {
|
|
if (affectedArea != null && affectedArea.intersects(((TerrainQuad) child).getWorldBound()) )
|
|
((TerrainQuad) child).fixNormals(affectedArea);
|
|
} else if (child instanceof TerrainPatch) {
|
|
if (affectedArea != null && affectedArea.intersects(((TerrainPatch) child).getWorldBound()) )
|
|
((TerrainPatch) child).updateNormals(); // recalculate the patch's normals
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* fix the normals on the edge of the terrain patches.
|
|
*/
|
|
protected void fixNormalEdges(BoundingBox affectedArea) {
|
|
if (children == null)
|
|
return;
|
|
|
|
for (int x = children.size(); --x >= 0;) {
|
|
Spatial child = children.get(x);
|
|
if (child instanceof TerrainQuad) {
|
|
if (affectedArea != null && affectedArea.intersects(((TerrainQuad) child).getWorldBound()) )
|
|
((TerrainQuad) child).fixNormalEdges(affectedArea);
|
|
} else if (child instanceof TerrainPatch) {
|
|
if (affectedArea != null && !affectedArea.intersects(((TerrainPatch) child).getWorldBound()) ) // if doesn't intersect, continue
|
|
continue;
|
|
|
|
TerrainPatch tp = (TerrainPatch) child;
|
|
TerrainPatch right = findRightPatch(tp);
|
|
TerrainPatch bottom = findDownPatch(tp);
|
|
TerrainPatch top = findTopPatch(tp);
|
|
TerrainPatch left = findLeftPatch(tp);
|
|
TerrainPatch topLeft = null;
|
|
if (top != null)
|
|
topLeft = findLeftPatch(top);
|
|
TerrainPatch bottomRight = null;
|
|
if (right != null)
|
|
bottomRight = findDownPatch(right);
|
|
TerrainPatch topRight = null;
|
|
if (top != null)
|
|
topRight = findRightPatch(top);
|
|
TerrainPatch bottomLeft = null;
|
|
if (left != null)
|
|
bottomLeft = findDownPatch(left);
|
|
|
|
tp.fixNormalEdges(right, bottom, top, left, bottomRight, bottomLeft, topRight, topLeft);
|
|
|
|
}
|
|
} // for each child
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
public int collideWith(Collidable other, CollisionResults results){
|
|
int total = 0;
|
|
|
|
if (other instanceof Ray)
|
|
return collideWithRay((Ray)other, results);
|
|
|
|
// if it didn't collide with this bbox, return
|
|
if (other instanceof BoundingVolume)
|
|
if (!this.getWorldBound().intersects((BoundingVolume)other))
|
|
return total;
|
|
|
|
for (Spatial child : children){
|
|
total += child.collideWith(other, results);
|
|
}
|
|
return total;
|
|
}
|
|
|
|
/**
|
|
* Gather the terrain patches that intersect the given ray (toTest).
|
|
* This only tests the bounding boxes
|
|
* @param toTest
|
|
* @param results
|
|
*/
|
|
public void findPick(Ray toTest, List<TerrainPickData> results) {
|
|
|
|
if (getWorldBound() != null) {
|
|
if (getWorldBound().intersects(toTest)) {
|
|
// further checking needed.
|
|
for (int i = 0; i < getQuantity(); i++) {
|
|
if (children.get(i) instanceof TerrainPatch) {
|
|
TerrainPatch tp = (TerrainPatch) children.get(i);
|
|
tp.ensurePositiveVolumeBBox();
|
|
if (tp.getWorldBound().intersects(toTest)) {
|
|
CollisionResults cr = new CollisionResults();
|
|
toTest.collideWith(tp.getWorldBound(), cr);
|
|
if (cr != null && cr.getClosestCollision() != null) {
|
|
cr.getClosestCollision().getDistance();
|
|
results.add(new TerrainPickData(tp, cr.getClosestCollision()));
|
|
}
|
|
}
|
|
}
|
|
else if (children.get(i) instanceof TerrainQuad) {
|
|
((TerrainQuad) children.get(i)).findPick(toTest, results);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Retrieve all Terrain Patches from all children and store them
|
|
* in the 'holder' list
|
|
* @param holder must not be null, will be populated when returns
|
|
*/
|
|
public void getAllTerrainPatches(List<TerrainPatch> holder) {
|
|
if (children != null) {
|
|
for (int i = children.size(); --i >= 0;) {
|
|
Spatial child = children.get(i);
|
|
if (child instanceof TerrainQuad) {
|
|
((TerrainQuad) child).getAllTerrainPatches(holder);
|
|
} else if (child instanceof TerrainPatch) {
|
|
holder.add((TerrainPatch)child);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public void getAllTerrainPatchesWithTranslation(Map<TerrainPatch,Vector3f> holder, Vector3f translation) {
|
|
if (children != null) {
|
|
for (int i = children.size(); --i >= 0;) {
|
|
Spatial child = children.get(i);
|
|
if (child instanceof TerrainQuad) {
|
|
((TerrainQuad) child).getAllTerrainPatchesWithTranslation(holder, translation.clone().add(child.getLocalTranslation()));
|
|
} else if (child instanceof TerrainPatch) {
|
|
//if (holder.size() < 4)
|
|
holder.put((TerrainPatch)child, translation.clone().add(child.getLocalTranslation()));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void read(JmeImporter e) throws IOException {
|
|
super.read(e);
|
|
InputCapsule c = e.getCapsule(this);
|
|
size = c.readInt("size", 0);
|
|
stepScale = (Vector3f) c.readSavable("stepScale", null);
|
|
offset = (Vector2f) c.readSavable("offset", new Vector2f(0,0));
|
|
offsetAmount = c.readFloat("offsetAmount", 0);
|
|
quadrant = c.readInt("quadrant", 0);
|
|
totalSize = c.readInt("totalSize", 0);
|
|
//lodCalculator = (LodCalculator) c.readSavable("lodCalculator", createDefaultLodCalculator());
|
|
//lodCalculatorFactory = (LodCalculatorFactory) c.readSavable("lodCalculatorFactory", null);
|
|
|
|
if ( !(getParent() instanceof TerrainQuad) ) {
|
|
BoundingBox all = new BoundingBox(getWorldTranslation(), totalSize, totalSize, totalSize);
|
|
affectedAreaBBox = all;
|
|
updateNormals();
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void write(JmeExporter e) throws IOException {
|
|
super.write(e);
|
|
OutputCapsule c = e.getCapsule(this);
|
|
c.write(size, "size", 0);
|
|
c.write(totalSize, "totalSize", 0);
|
|
c.write(stepScale, "stepScale", null);
|
|
c.write(offset, "offset", new Vector2f(0,0));
|
|
c.write(offsetAmount, "offsetAmount", 0);
|
|
c.write(quadrant, "quadrant", 0);
|
|
//c.write(lodCalculatorFactory, "lodCalculatorFactory", null);
|
|
//c.write(lodCalculator, "lodCalculator", null);
|
|
}
|
|
|
|
@Override
|
|
public TerrainQuad clone() {
|
|
return this.clone(true);
|
|
}
|
|
|
|
@Override
|
|
public TerrainQuad clone(boolean cloneMaterials) {
|
|
TerrainQuad quadClone = (TerrainQuad) super.clone(cloneMaterials);
|
|
quadClone.name = name.toString();
|
|
quadClone.size = size;
|
|
quadClone.totalSize = totalSize;
|
|
if (stepScale != null) {
|
|
quadClone.stepScale = stepScale.clone();
|
|
}
|
|
if (offset != null) {
|
|
quadClone.offset = offset.clone();
|
|
}
|
|
quadClone.offsetAmount = offsetAmount;
|
|
quadClone.quadrant = quadrant;
|
|
//quadClone.lodCalculatorFactory = lodCalculatorFactory.clone();
|
|
//quadClone.lodCalculator = lodCalculator.clone();
|
|
|
|
TerrainLodControl lodControlCloned = this.getControl(TerrainLodControl.class);
|
|
TerrainLodControl lodControl = quadClone.getControl(TerrainLodControl.class);
|
|
|
|
if (lodControlCloned != null && !(getParent() instanceof TerrainQuad)) {
|
|
//lodControlCloned.setLodCalculator(lodControl.getLodCalculator().clone());
|
|
}
|
|
NormalRecalcControl normalControl = getControl(NormalRecalcControl.class);
|
|
if (normalControl != null)
|
|
normalControl.setTerrain(this);
|
|
|
|
return quadClone;
|
|
}
|
|
|
|
/**
|
|
* Called internally by com.jme3.util.clone.Cloner. Do not call directly.
|
|
*/
|
|
@Override
|
|
public void cloneFields( Cloner cloner, Object original ) {
|
|
super.cloneFields(cloner, original);
|
|
|
|
this.stepScale = cloner.clone(stepScale);
|
|
this.offset = cloner.clone(offset);
|
|
|
|
// This was not cloned before... I think that's a mistake.
|
|
this.affectedAreaBBox = cloner.clone(affectedAreaBBox);
|
|
|
|
// picker is not cloneable and not cloned. This also seems like
|
|
// a mistake if you ever load the same terrain twice.
|
|
// this.picker = cloner.clone(picker);
|
|
|
|
// neighbourFinder is also not cloned. Maybe that's ok.
|
|
}
|
|
|
|
@Override
|
|
protected void setParent(Node parent) {
|
|
super.setParent(parent);
|
|
if (parent == null) {
|
|
// if the terrain is being detached
|
|
clearCaches();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Removes any cached references this terrain is holding, in particular
|
|
* the TerrainPatch's neighbour references.
|
|
* This is called automatically when the root terrainQuad is detached from
|
|
* its parent or if setParent(null) is called.
|
|
*/
|
|
public void clearCaches() {
|
|
if (children != null) {
|
|
for (int i = children.size(); --i >= 0;) {
|
|
Spatial child = children.get(i);
|
|
if (child instanceof TerrainQuad) {
|
|
((TerrainQuad) child).clearCaches();
|
|
} else if (child instanceof TerrainPatch) {
|
|
((TerrainPatch) child).clearCaches();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public int getMaxLod() {
|
|
if (maxLod < 0)
|
|
maxLod = Math.max(1, (int) (FastMath.log(size-1)/FastMath.log(2)) -1); // -1 forces our minimum of 4 triangles wide
|
|
|
|
return maxLod;
|
|
}
|
|
|
|
public int getPatchSize() {
|
|
return patchSize;
|
|
}
|
|
|
|
public int getTotalSize() {
|
|
return totalSize;
|
|
}
|
|
|
|
public float[] getHeightMap() {
|
|
|
|
float[] hm = null;
|
|
int length = ((size-1)/2)+1;
|
|
int area = size*size;
|
|
hm = new float[area];
|
|
|
|
if (getChildren() != null && !getChildren().isEmpty()) {
|
|
float[] ul=null, ur=null, bl=null, br=null;
|
|
// get the child heightmaps
|
|
if (getChild(0) instanceof TerrainPatch) {
|
|
for (Spatial s : getChildren()) {
|
|
if ( ((TerrainPatch)s).getQuadrant() == 1)
|
|
ul = ((TerrainPatch)s).getHeightMap();
|
|
else if(((TerrainPatch) s).getQuadrant() == 2)
|
|
bl = ((TerrainPatch)s).getHeightMap();
|
|
else if(((TerrainPatch) s).getQuadrant() == 3)
|
|
ur = ((TerrainPatch)s).getHeightMap();
|
|
else if(((TerrainPatch) s).getQuadrant() == 4)
|
|
br = ((TerrainPatch)s).getHeightMap();
|
|
}
|
|
}
|
|
else {
|
|
ul = getQuad(1).getHeightMap();
|
|
bl = getQuad(2).getHeightMap();
|
|
ur = getQuad(3).getHeightMap();
|
|
br = getQuad(4).getHeightMap();
|
|
}
|
|
|
|
// combine them into a single heightmap
|
|
|
|
|
|
// first upper blocks
|
|
for (int y=0; y<length; y++) { // rows
|
|
for (int x1=0; x1<length; x1++) {
|
|
int row = y*size;
|
|
hm[row+x1] = ul[y*length+x1];
|
|
}
|
|
for (int x2=1; x2<length; x2++) {
|
|
int row = y*size + length;
|
|
hm[row+x2-1] = ur[y*length + x2];
|
|
}
|
|
}
|
|
// second lower blocks
|
|
int rowOffset = size*length;
|
|
for (int y=1; y<length; y++) { // rows
|
|
for (int x1=0; x1<length; x1++) {
|
|
int row = (y-1)*size;
|
|
hm[rowOffset+row+x1] = bl[y*length+x1];
|
|
}
|
|
for (int x2=1; x2<length; x2++) {
|
|
int row = (y-1)*size + length;
|
|
hm[rowOffset+row+x2-1] = br[y*length + x2];
|
|
}
|
|
}
|
|
}
|
|
|
|
return hm;
|
|
}
|
|
}
|
|
|
|
|