/* * 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; /** *
* 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. *
** The height of the terrain can be modified at runtime using setHeight() *
** 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. *
* The leaves of the terrain quad tree are Terrain Patches. These have the real geometry mesh. *
* Heightmap coordinates start from the bottom left of the world and work towards the * top right. *
* +x * ^ * | ......N = length of heightmap * | : : * | : : * | 0.....: * +---------> +z * (world coordinates) ** @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: *
* 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. *
* @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(Listsplit
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
* createQuad
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);
}
}
}
/**
* createQuadPatch
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