aboutsummaryrefslogtreecommitdiff
path: root/engine/src/terrain/com/jme3/terrain/geomipmap/TerrainQuad.java
diff options
context:
space:
mode:
Diffstat (limited to 'engine/src/terrain/com/jme3/terrain/geomipmap/TerrainQuad.java')
-rw-r--r--engine/src/terrain/com/jme3/terrain/geomipmap/TerrainQuad.java1862
1 files changed, 1862 insertions, 0 deletions
diff --git a/engine/src/terrain/com/jme3/terrain/geomipmap/TerrainQuad.java b/engine/src/terrain/com/jme3/terrain/geomipmap/TerrainQuad.java
new file mode 100644
index 0000000..0b9b218
--- /dev/null
+++ b/engine/src/terrain/com/jme3/terrain/geomipmap/TerrainQuad.java
@@ -0,0 +1,1862 @@
+/*
+ * 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 java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ThreadFactory;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * 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
+
+ //protected LodCalculatorFactory lodCalculatorFactory;
+ //protected LodCalculator lodCalculator;
+
+ protected List<Vector3f> lastCameraLocations; // used for LOD calc
+ private boolean lodCalcRunning = false;
+ private int lodOffCount = 0;
+ private int maxLod = -1;
+ private HashMap<String,UpdatedTerrainPatch> updatedPatches;
+ private final Object updatePatchesLock = new Object();
+ private BoundingBox affectedAreaBBox; // only set in the root quad
+
+ private TerrainPicker picker;
+ private Vector3f lastScale = Vector3f.UNIT_XYZ;
+
+ protected ExecutorService executor;
+
+ protected ExecutorService createExecutorService() {
+ return Executors.newSingleThreadExecutor(new ThreadFactory() {
+ public Thread newThread(Runnable r) {
+ Thread th = new Thread(r);
+ th.setName("jME Terrain Thread");
+ th.setDaemon(true);
+ return th;
+ }
+ });
+ }
+
+ public TerrainQuad() {
+ super("Terrain");
+ }
+
+ /**
+ *
+ * @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 heightMap The height map to generate the terrain from (a flat
+ * height map will be generated if this is null)
+ */
+ public TerrainQuad(String name, int patchSize, int totalSize, float[] heightMap) {
+ this(name, patchSize, totalSize, Vector3f.UNIT_XYZ, heightMap);
+ }
+
+ /**
+ *
+ * @param name the name of the scene element. This is required for
+ * identification and comparison purposes.
+ * @param patchSize size of the individual patches
+ * @param quadSize
+ * @param totalSize the size of this entire terrain tree (on one side)
+ * @param heightMap The height map to generate the terrain from (a flat
+ * height map will be generated if this is null)
+ */
+ public TerrainQuad(String name, int patchSize, int quadSize, int totalSize, float[] heightMap) {
+ this(name, patchSize, totalSize, quadSize, Vector3f.UNIT_XYZ, heightMap);
+ }
+
+ /**
+ *
+ * @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)
+ */
+ 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)
+ */
+ 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;
+ //this.lodCalculatorFactory = lodCalculatorFactory;
+ //this.lodCalculator = lodCalculator;
+ split(patchSize, heightMap);
+ }
+
+ /*public void setLodCalculatorFactory(LodCalculatorFactory lodCalculatorFactory) {
+ if (children != null) {
+ for (int i = children.size(); --i >= 0;) {
+ Spatial child = children.get(i);
+ if (child instanceof TerrainQuad) {
+ ((TerrainQuad) child).setLodCalculatorFactory(lodCalculatorFactory);
+ } else if (child instanceof TerrainPatch) {
+ ((TerrainPatch) child).setLodCalculator(lodCalculatorFactory.createCalculator((TerrainPatch) child));
+ }
+ }
+ }
+ }*/
+
+
+ /**
+ * Create just a flat heightmap
+ */
+ private float[] generateDefaultHeightMap(int size) {
+ float[] heightMap = new float[size*size];
+ return heightMap;
+ }
+
+ /**
+ * Call from the update() method of a terrain controller to update
+ * the LOD values of each patch.
+ * This will perform the geometry calculation in a background thread and
+ * do the actual update on the opengl thread.
+ */
+ public void update(List<Vector3f> locations, LodCalculator lodCalculator) {
+ updateLOD(locations, lodCalculator);
+ }
+
+ /**
+ * 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
+ }
+ }
+
+ // do all of the LOD calculations
+ protected void updateLOD(List<Vector3f> locations, LodCalculator lodCalculator) {
+ // update any existing ones that need updating
+ updateQuadLODs();
+
+ if (lodCalculator.isLodOff()) {
+ // we want to calculate the base lod at least once
+ if (lodOffCount == 1)
+ return;
+ else
+ lodOffCount++;
+ } else
+ lodOffCount = 0;
+
+ if (lastCameraLocations != null) {
+ if (lastCameraLocationsTheSame(locations) && !lodCalculator.isLodOff())
+ return; // don't update if in same spot
+ else
+ lastCameraLocations = cloneVectorList(locations);
+ }
+ else {
+ lastCameraLocations = cloneVectorList(locations);
+ return;
+ }
+
+ if (isLodCalcRunning()) {
+ return;
+ }
+
+ if (getParent() instanceof TerrainQuad) {
+ return; // we just want the root quad to perform this.
+ }
+
+ if (executor == null)
+ executor = createExecutorService();
+
+ UpdateLOD updateLodThread = new UpdateLOD(locations, lodCalculator);
+ executor.execute(updateLodThread);
+ }
+
+ private synchronized boolean isLodCalcRunning() {
+ return lodCalcRunning;
+ }
+
+ private synchronized void setLodCalcRunning(boolean running) {
+ lodCalcRunning = running;
+ }
+
+ private List<Vector3f> cloneVectorList(List<Vector3f> locations) {
+ List<Vector3f> cloned = new ArrayList<Vector3f>();
+ for(Vector3f l : locations)
+ cloned.add(l.clone());
+ return cloned;
+ }
+
+ private boolean lastCameraLocationsTheSame(List<Vector3f> locations) {
+ boolean theSame = true;
+ for (Vector3f l : locations) {
+ for (Vector3f v : lastCameraLocations) {
+ if (!v.equals(l) ) {
+ theSame = false;
+ return false;
+ }
+ }
+ }
+ return theSame;
+ }
+
+ private int collideWithRay(Ray ray, CollisionResults results) {
+ if (picker == null)
+ picker = new BresenhamTerrainPicker(this);
+
+ Vector3f intersection = picker.getTerrainIntersection(ray, results);
+ if (intersection != null)
+ 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 float getTextureCoordinateScale() {
+ // return 1f/(float)totalSize;
+ //}
+ public int getNumMajorSubdivisions() {
+ return 1;
+ }
+
+ /**
+ * Calculates the LOD of all child terrain patches.
+ */
+ private class UpdateLOD implements Runnable {
+ private List<Vector3f> camLocations;
+ private LodCalculator lodCalculator;
+
+ UpdateLOD(List<Vector3f> camLocations, LodCalculator lodCalculator) {
+ this.camLocations = camLocations;
+ this.lodCalculator = lodCalculator;
+ }
+
+ public void run() {
+ long start = System.currentTimeMillis();
+ if (isLodCalcRunning()) {
+ //System.out.println("thread already running");
+ return;
+ }
+ //System.out.println("spawned thread "+toString());
+ setLodCalcRunning(true);
+
+ // go through each patch and calculate its LOD based on camera distance
+ HashMap<String,UpdatedTerrainPatch> updated = new HashMap<String,UpdatedTerrainPatch>();
+ boolean lodChanged = calculateLod(camLocations, updated, lodCalculator); // 'updated' gets populated here
+
+ if (!lodChanged) {
+ // not worth updating anything else since no one's LOD changed
+ setLodCalcRunning(false);
+ return;
+ }
+ // then calculate its neighbour LOD values for seaming in the shader
+ findNeighboursLod(updated);
+
+ fixEdges(updated); // 'updated' can get added to here
+
+ reIndexPages(updated, lodCalculator.usesVariableLod());
+
+ setUpdateQuadLODs(updated); // set back to main ogl thread
+
+ setLodCalcRunning(false);
+ //double duration = (System.currentTimeMillis()-start);
+ //System.out.println("terminated in "+duration);
+ }
+ }
+
+ private void setUpdateQuadLODs(HashMap<String,UpdatedTerrainPatch> updated) {
+ synchronized (updatePatchesLock) {
+ updatedPatches = updated;
+ }
+ }
+
+ /**
+ * Back on the ogl thread: update the terrain patch geometries
+ * @param updatedPatches to be updated
+ */
+ private void updateQuadLODs() {
+ synchronized (updatePatchesLock) {
+
+ if (updatedPatches == null || updatedPatches.isEmpty())
+ return;
+
+ // do the actual geometry update here
+ for (UpdatedTerrainPatch utp : updatedPatches.values()) {
+ utp.updateAll();
+ }
+
+ updatedPatches.clear();
+ }
+ }
+
+ public boolean hasPatchesToUpdate() {
+ return updatedPatches != null && !updatedPatches.isEmpty();
+ }
+
+ 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;
+
+ 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, right.lod);
+ updated.put(utpR.getName(), utpR);
+ }
+
+ utp.setRightLod(utpR.getNewLod());
+ utpR.setLeftLod(utp.getNewLod());
+ }
+ if (down != null) {
+ UpdatedTerrainPatch utpD = updated.get(down.getName());
+ if (utpD == null) {
+ utpD = new UpdatedTerrainPatch(down, down.lod);
+ updated.put(utpD.getName(), utpD);
+ }
+
+ utp.setBottomLod(utpD.getNewLod());
+ utpD.setTopLod(utp.getNewLod());
+ }
+
+ }
+ }
+ }
+ }
+
+ /**
+ * TerrainQuad caches neighbours for faster LOD checks.
+ * Sometimes you might want to reset this cache (for instance in TerrainGrid)
+ */
+ protected 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, right.lod);
+ updated.put(utpR.getName(), utpR);
+ }
+ utpR.setFixEdges(true);
+ }
+ if (down != null) {
+ UpdatedTerrainPatch utpD = updated.get(down.getName());
+ if (utpD == null) {
+ utpD = new UpdatedTerrainPatch(down, down.lod);
+ updated.put(utpD.getName(), utpD);
+ }
+ utpD.setFixEdges(true);
+ }
+ if (top != null){
+ UpdatedTerrainPatch utpT = updated.get(top.getName());
+ if (utpT == null) {
+ utpT = new UpdatedTerrainPatch(top, top.lod);
+ updated.put(utpT.getName(), utpT);
+ }
+ utpT.setFixEdges(true);
+ }
+ if (left != null){
+ UpdatedTerrainPatch utpL = updated.get(left.getName());
+ if (utpL == null) {
+ utpL = new UpdatedTerrainPatch(left, left.lod);
+ updated.put(utpL.getName(), utpL);
+ }
+ 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(new Vector3f(0,0,0), size, Float.MAX_VALUE, size);
+ lastScale = getWorldScale();
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * This will cause all normals for this terrain quad to be recalculated
+ */
+ protected void setNeedToRecalculateNormals() {
+ affectedAreaBBox = new BoundingBox(new Vector3f(0,0,0), size*2, Float.MAX_VALUE, size*2);
+ }
+
+ 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);
+
+ 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;
+ }
+
+ public float getHeight(Vector2f xz) {
+ // offset
+ float x = (float)(((xz.x - getWorldTranslation().x) / getWorldScale().x) + (float)totalSize / 2f);
+ float z = (float)(((xz.y - getWorldTranslation().z) / getWorldScale().z) + (float)totalSize / 2f);
+ float height = getHeight(x, z);
+ height *= getWorldScale().y;
+ return height;
+ }
+
+ /*
+ * gets an interpolated value at the specified point
+ * @param x coordinate translated into actual (positive) terrain grid coordinates
+ * @param y coordinate translated into actual (positive) terrain grid coordinates
+ */
+ protected float getHeight(float x, float z) {
+ 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
+ float v1 = getHeightmapHeight((int) FastMath.ceil(x), (int) FastMath.ceil(z));
+ float v2 = getHeightmapHeight((int) FastMath.floor(x), (int) FastMath.ceil(z));
+ float v3 = getHeightmapHeight((int) FastMath.ceil(x), (int) FastMath.floor(z));
+ float v4 = getHeightmapHeight((int) FastMath.floor(x), (int) FastMath.floor(z));
+ if (onX) {
+ return ((x - col) + (z - row) - 1f)*v1 + (1f - (x - col))*v2 + (1f - (z - row))*v3;
+ } else {
+ return (1f - (x - col) - (z - row))*v4 + (z - row)*v2 + (x - col)*v3;
+ }
+ }
+
+ public Vector3f getNormal(Vector2f xz) {
+ // offset
+ float x = (float)(((xz.x - getWorldTranslation().x) / getWorldScale().x) + (float)totalSize / 2f);
+ float z = (float)(((xz.y - getWorldTranslation().z) / getWorldScale().z) + (float)totalSize / 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);
+ 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 (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 2.
+ 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() {
+ if (getParent() == null || !(getParent() instanceof TerrainQuad))
+ return null;
+
+ TerrainQuad 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);
+ }
+
+ return null;
+ }
+
+ protected TerrainQuad findDownQuad() {
+ if (getParent() == null || !(getParent() instanceof TerrainQuad))
+ return null;
+
+ TerrainQuad 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);
+ }
+
+ return null;
+ }
+
+ protected TerrainQuad findTopQuad() {
+ if (getParent() == null || !(getParent() instanceof TerrainQuad))
+ return null;
+
+ TerrainQuad 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);
+ }
+
+ return null;
+ }
+
+ protected TerrainQuad findLeftQuad() {
+ if (getParent() == null || !(getParent() instanceof TerrainQuad))
+ return null;
+
+ TerrainQuad 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);
+ }
+
+ 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
+ ((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;
+ }
+
+ 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;
+ }
+}
+