Quellcode durchsuchen

JmeCloneable related changes to TerrainQuad and TerrainPatch. Fixed
something I missed in NormalRecalcControl.

Paul Speed vor 9 Jahren
Ursprung
Commit
7b29c58fe0

+ 15 - 4
jme3-terrain/src/main/java/com/jme3/terrain/geomipmap/NormalRecalcControl.java

@@ -69,12 +69,23 @@ public class NormalRecalcControl extends AbstractControl {
 
     }
 
-    @Override   
+    /**
+     *  Called internally by com.jme3.util.clone.Cloner.  Do not call directly.
+     */
+    @Override
     public Object jmeClone() {
         NormalRecalcControl control = (NormalRecalcControl)super.jmeClone();
         control.setEnabled(true);
-        return control; 
-    }     
+        return control;
+    }
+
+    /**
+     *  Called internally by com.jme3.util.clone.Cloner.  Do not call directly.
+     */
+    @Override
+    public void cloneFields( Cloner cloner, Object original ) {
+        this.terrain = cloner.clone(terrain);
+    }
 
     @Override
     public Control cloneForSpatial(Spatial spatial) {
@@ -83,7 +94,7 @@ public class NormalRecalcControl extends AbstractControl {
         control.setEnabled(true);
         return control;
     }
-    
+
     @Override
     public void setSpatial(Spatial spatial) {
         super.setSpatial(spatial);

+ 85 - 57
jme3-terrain/src/main/java/com/jme3/terrain/geomipmap/TerrainPatch.java

@@ -50,6 +50,7 @@ import com.jme3.scene.mesh.IndexBuffer;
 import com.jme3.terrain.geomipmap.TerrainQuad.LocationHeight;
 import com.jme3.terrain.geomipmap.lodcalc.util.EntropyComputeUtil;
 import com.jme3.util.BufferUtils;
+import com.jme3.util.clone.Cloner;
 import java.io.IOException;
 import java.nio.Buffer;
 import java.nio.FloatBuffer;
@@ -65,18 +66,18 @@ import java.util.List;
  * That uses a geo-mipmapping algorithm to change the index buffer of the mesh.
  * The mesh is a triangle strip. In wireframe mode you might notice some strange lines, these are degenerate
  * triangles generated by the geoMipMap algorithm and can be ignored. The video card removes them at almost no cost.
- * 
+ *
  * Each patch needs to know its neighbour's LOD so it can seam its edges with them, in case the neighbour has a different
  * LOD. If this doesn't happen, you will see gaps.
- * 
+ *
  * The LOD value is most detailed at zero. It gets less detailed the higher the LOD value until you reach maxLod, which
  * is a mathematical limit on the number of times the 'size' of the patch can be divided by two. However there is a -1 to that
  * for now until I add in a custom index buffer calculation for that max level, the current algorithm does not go that far.
- * 
- * You can supply a LodThresholdCalculator for use in determining when the LOD should change. It's API will no doubt change 
- * in the near future. Right now it defaults to just changing LOD every two patch sizes. So if a patch has a size of 65, 
+ *
+ * You can supply a LodThresholdCalculator for use in determining when the LOD should change. It's API will no doubt change
+ * in the near future. Right now it defaults to just changing LOD every two patch sizes. So if a patch has a size of 65,
  * then the LOD changes every 130 units away.
- * 
+ *
  * @author Brent Owens
  */
 public class TerrainPatch extends Geometry {
@@ -118,7 +119,7 @@ public class TerrainPatch extends Geometry {
         super("TerrainPatch");
         setBatchHint(BatchHint.Never);
     }
-    
+
     public TerrainPatch(String name) {
         super(name);
         setBatchHint(BatchHint.Never);
@@ -221,7 +222,7 @@ public class TerrainPatch extends Geometry {
     public FloatBuffer getHeightmap() {
         return BufferUtils.createFloatBuffer(geomap.getHeightArray());
     }
-    
+
     public float[] getHeightMap() {
         return geomap.getHeightArray();
     }
@@ -256,7 +257,7 @@ public class TerrainPatch extends Geometry {
                 idxB = geomap.writeIndexArrayLodVariable(pow, (int) Math.pow(2, utp.getRightLod()), (int) Math.pow(2, utp.getTopLod()), (int) Math.pow(2, utp.getLeftLod()), (int) Math.pow(2, utp.getBottomLod()), totalSize);
             else
                 idxB = geomap.writeIndexArrayLodDiff(pow, right, top, left, bottom, totalSize);
-            
+
             Buffer b;
             if (idxB.getBuffer() instanceof IntBuffer)
                 b = (IntBuffer)idxB.getBuffer();
@@ -277,14 +278,14 @@ public class TerrainPatch extends Geometry {
         return store.set(getMesh().getFloatBuffer(Type.TexCoord).get(idx*2),
                          getMesh().getFloatBuffer(Type.TexCoord).get(idx*2+1) );
     }
-    
+
     public float getHeightmapHeight(float x, float z) {
         if (x < 0 || z < 0 || x >= size || z >= size)
             return 0;
         int idx = (int) (z * size + x);
         return getMesh().getFloatBuffer(Type.Position).get(idx*3+1); // 3 floats per entry (x,y,z), the +1 is to get the Y
     }
-    
+
     /**
      * Get the triangle of this geometry at the specified local coordinate.
      * @param x local to the terrain patch
@@ -306,7 +307,7 @@ public class TerrainPatch extends Geometry {
     }
 
     protected void setHeight(List<LocationHeight> locationHeights, boolean overrideHeight) {
-        
+
         for (LocationHeight lh : locationHeights) {
             if (lh.x < 0 || lh.z < 0 || lh.x >= size || lh.z >= size)
                 continue;
@@ -317,7 +318,7 @@ public class TerrainPatch extends Geometry {
                 float h = getMesh().getFloatBuffer(Type.Position).get(idx*3+1);
                 geomap.getHeightArray()[idx] = h+lh.h;
             }
-            
+
         }
 
         FloatBuffer newVertexBuffer = geomap.writeVertexArray(null, stepScale, false);
@@ -351,7 +352,7 @@ public class TerrainPatch extends Geometry {
         TB.setUpdateNeeded();
         BB.setUpdateNeeded();
     }
-    
+
     /**
      * Matches the normals along the edge of the patch with the neighbours.
      * Computes the normals for the right, bottom, left, and top edges of the
@@ -364,7 +365,7 @@ public class TerrainPatch extends Geometry {
      *          *---x---*
      *              |
      *              *
-     * It works across the right side of the patch, from the top down to 
+     * It works across the right side of the patch, from the top down to
      * the bottom. Then it works on the bottom side of the patch, from the
      * left to the right.
      */
@@ -388,9 +389,9 @@ public class TerrainPatch extends Geometry {
         Vector3f binormal = new Vector3f();
         Vector3f normal = new Vector3f();
 
-        
+
         int s = this.getSize()-1;
-        
+
         if (right != null) { // right side,    works its way down
             for (int i=0; i<s+1; i++) {
                 rootPoint.set(0, this.getHeightmapHeight(s,i), 0);
@@ -399,26 +400,26 @@ public class TerrainPatch extends Geometry {
 
                 if (i == 0) { // top point
                     bottomPoint.set(0, this.getHeightmapHeight(s,i+1), 1);
-                    
+
                     if (top == null) {
                         averageNormalsTangents(null, rootPoint, leftPoint, bottomPoint, rightPoint,  normal, tangent, binormal);
                         setInBuffer(this.getMesh(), s, normal, tangent, binormal);
                         setInBuffer(right.getMesh(), 0, normal, tangent, binormal);
                     } else {
                         topPoint.set(0, top.getHeightmapHeight(s,s-1), -1);
-                        
+
                         averageNormalsTangents(topPoint, rootPoint, leftPoint, bottomPoint, rightPoint,normal, tangent, binormal);
                         setInBuffer(this.getMesh(), s, normal, tangent, binormal);
                         setInBuffer(right.getMesh(), 0, normal, tangent, binormal);
                         setInBuffer(top.getMesh(), (s+1)*(s+1)-1, normal, tangent, binormal);
-                        
+
                         if (topRight != null) {
                     //        setInBuffer(topRight.getMesh(), (s+1)*s, normal, tangent, binormal);
                         }
                     }
                 } else if (i == s) { // bottom point
                     topPoint.set(0, this.getHeightmapHeight(s,s-1), -1);
-                    
+
                     if (bottom == null) {
                         averageNormalsTangents(topPoint, rootPoint, leftPoint, null, rightPoint, normal, tangent, binormal);
                         setInBuffer(this.getMesh(), (s+1)*(s+1)-1, normal, tangent, binormal);
@@ -429,7 +430,7 @@ public class TerrainPatch extends Geometry {
                         setInBuffer(this.getMesh(), (s+1)*(s+1)-1, normal, tangent, binormal);
                         setInBuffer(right.getMesh(), (s+1)*s, normal, tangent, binormal);
                         setInBuffer(bottom.getMesh(), s, normal, tangent, binormal);
-                        
+
                         if (bottomRight != null) {
                    //         setInBuffer(bottomRight.getMesh(), 0, normal, tangent, binormal);
                         }
@@ -449,41 +450,41 @@ public class TerrainPatch extends Geometry {
                 rootPoint.set(0, this.getHeightmapHeight(0,i), 0);
                 leftPoint.set(-1, left.getHeightmapHeight(s-1,i), 0);
                 rightPoint.set(1, this.getHeightmapHeight(1,i), 0);
-                
+
                 if (i == 0) { // top point
                     bottomPoint.set(0, this.getHeightmapHeight(0,i+1), 1);
-                    
+
                     if (top == null) {
                         averageNormalsTangents(null, rootPoint, leftPoint, bottomPoint, rightPoint, normal, tangent, binormal);
                         setInBuffer(this.getMesh(), 0, normal, tangent, binormal);
                         setInBuffer(left.getMesh(), s, normal, tangent, binormal);
                     } else {
                         topPoint.set(0, top.getHeightmapHeight(0,s-1), -1);
-                        
+
                         averageNormalsTangents(topPoint, rootPoint, leftPoint, bottomPoint, rightPoint, normal, tangent, binormal);
                         setInBuffer(this.getMesh(), 0, normal, tangent, binormal);
                         setInBuffer(left.getMesh(), s, normal, tangent, binormal);
                         setInBuffer(top.getMesh(), (s+1)*s, normal, tangent, binormal);
-                        
+
                         if (topLeft != null) {
                      //       setInBuffer(topLeft.getMesh(), (s+1)*(s+1)-1, normal, tangent, binormal);
                         }
                     }
                 } else if (i == s) { // bottom point
                     topPoint.set(0, this.getHeightmapHeight(0,i-1), -1);
-                    
+
                     if (bottom == null) {
                         averageNormalsTangents(topPoint, rootPoint, leftPoint, null, rightPoint, normal, tangent, binormal);
                         setInBuffer(this.getMesh(), (s+1)*(s), normal, tangent, binormal);
                         setInBuffer(left.getMesh(), (s+1)*(s+1)-1, normal, tangent, binormal);
                     } else {
                         bottomPoint.set(0, bottom.getHeightmapHeight(0,1), 1);
-                        
+
                         averageNormalsTangents(topPoint, rootPoint, leftPoint, bottomPoint, rightPoint, normal, tangent, binormal);
                         setInBuffer(this.getMesh(), (s+1)*(s), normal, tangent, binormal);
                         setInBuffer(left.getMesh(), (s+1)*(s+1)-1, normal, tangent, binormal);
                         setInBuffer(bottom.getMesh(), 0, normal, tangent, binormal);
-                        
+
                         if (bottomLeft != null) {
                      //       setInBuffer(bottomLeft.getMesh(), s, normal, tangent, binormal);
                         }
@@ -491,7 +492,7 @@ public class TerrainPatch extends Geometry {
                 } else { // all in the middle
                     topPoint.set(0, this.getHeightmapHeight(0,i-1), -1);
                     bottomPoint.set(0, this.getHeightmapHeight(0,i+1), 1);
-                    
+
                     averageNormalsTangents(topPoint, rootPoint, leftPoint, bottomPoint, rightPoint, normal, tangent, binormal);
                     setInBuffer(this.getMesh(), (s+1)*(i), normal, tangent, binormal);
                     setInBuffer(left.getMesh(), (s+1)*(i+1)-1, normal, tangent, binormal);
@@ -504,14 +505,14 @@ public class TerrainPatch extends Geometry {
                 rootPoint.set(0, this.getHeightmapHeight(i,0), 0);
                 topPoint.set(0, top.getHeightmapHeight(i,s-1), -1);
                 bottomPoint.set(0, this.getHeightmapHeight(i,1), 1);
-                
+
                 if (i == 0) { // left corner
                     // handled by left side pass
-                    
+
                 } else if (i == s) { // right corner
-                    
+
                     // handled by this patch when it does its right side
-                    
+
                 } else { // all in the middle
                     leftPoint.set(-1, this.getHeightmapHeight(i-1,0), 0);
                     rightPoint.set(1, this.getHeightmapHeight(i+1,0), 0);
@@ -520,9 +521,9 @@ public class TerrainPatch extends Geometry {
                     setInBuffer(top.getMesh(), (s+1)*(s)+i, normal, tangent, binormal);
                 }
             }
-            
+
         }
-        
+
         if (bottom != null) { // bottom side,    works its way right
             for (int i=0; i<s+1; i++) {
                 rootPoint.set(0, this.getHeightmapHeight(i,s), 0);
@@ -531,11 +532,11 @@ public class TerrainPatch extends Geometry {
 
                 if (i == 0) { // left
                     // handled by the left side pass
-                    
+
                 } else if (i == s) { // right
-                    
+
                     // handled by the right side pass
-                    
+
                 } else { // all in the middle
                     leftPoint.set(-1, this.getHeightmapHeight(i-1,s), 0);
                     rightPoint.set(1, this.getHeightmapHeight(i+1,s), 0);
@@ -544,22 +545,22 @@ public class TerrainPatch extends Geometry {
                     setInBuffer(bottom.getMesh(), i, normal, tangent, binormal);
                 }
             }
-            
+
         }
     }
 
     protected void averageNormalsTangents(
             Vector3f topPoint,
             Vector3f rootPoint,
-            Vector3f leftPoint, 
-            Vector3f bottomPoint, 
+            Vector3f leftPoint,
+            Vector3f bottomPoint,
             Vector3f rightPoint,
             Vector3f normal,
             Vector3f tangent,
             Vector3f binormal)
     {
         Vector3f scale = getWorldScale();
-        
+
         Vector3f n1 = new Vector3f(0,0,0);
         if (topPoint != null && leftPoint != null) {
             n1.set(calculateNormal(topPoint.mult(scale), rootPoint.mult(scale), leftPoint.mult(scale)));
@@ -576,12 +577,12 @@ public class TerrainPatch extends Geometry {
         if (rightPoint != null && topPoint != null) {
             n4.set(calculateNormal(rightPoint.mult(scale), rootPoint.mult(scale), topPoint.mult(scale)));
         }
-        
+
         //if (bottomPoint != null && rightPoint != null && rootTex != null && rightTex != null && bottomTex != null)
         //    LODGeomap.calculateTangent(new Vector3f[]{rootPoint.mult(scale),rightPoint.mult(scale),bottomPoint.mult(scale)}, new Vector2f[]{rootTex,rightTex,bottomTex}, tangent, binormal);
 
         normal.set(n1.add(n2).add(n3).add(n4).normalize());
-        
+
         tangent.set(normal.cross(new Vector3f(0,0,1)).normalize());
         binormal.set(new Vector3f(1,0,0).cross(normal).normalize());
     }
@@ -592,11 +593,11 @@ public class TerrainPatch extends Geometry {
                   .crossLocal(secondPoint.subtract(rootPoint)).normalizeLocal();
         return normal;
     }
-    
+
     protected Vector3f getMeshNormal(int x, int z) {
         if (x >= size || z >= size)
             return null; // out of range
-        
+
         int index = (z*size+x)*3;
         FloatBuffer nb = (FloatBuffer)this.getMesh().getBuffer(Type.Normal).getData();
         Vector3f normal = new Vector3f();
@@ -609,7 +610,7 @@ public class TerrainPatch extends Geometry {
     protected float getHeight(int x, int z, float xm, float zm) {
         return geomap.getHeight(x,z,xm,zm);
     }
-    
+
     /**
      * Locks the mesh (sets it static) to improve performance.
      * But it it not editable then. Set unlock to make it editable.
@@ -626,7 +627,7 @@ public class TerrainPatch extends Geometry {
     public void unlockMesh() {
         getMesh().setDynamic();
     }
-	
+
     /**
      * Returns the offset amount this terrain patch uses for textures.
      *
@@ -797,7 +798,7 @@ public class TerrainPatch extends Geometry {
     protected void setLodBottom(int lodBottom) {
         this.lodBottom = lodBottom;
     }
-    
+
     /*public void setLodCalculator(LodCalculatorFactory lodCalculatorFactory) {
         this.lodCalculatorFactory = lodCalculatorFactory;
         setLodCalculator(lodCalculatorFactory.createCalculator(this));
@@ -812,7 +813,7 @@ public class TerrainPatch extends Geometry {
         if (other instanceof BoundingVolume)
             if (!getWorldBound().intersects((BoundingVolume)other))
                 return 0;
-        
+
         if(other instanceof Ray)
             return collideWithRay((Ray)other, results);
         else if (other instanceof BoundingVolume)
@@ -853,7 +854,7 @@ public class TerrainPatch extends Geometry {
      * This most definitely is not optimized.
      */
     private int collideWithBoundingBox(BoundingBox bbox, CollisionResults results) {
-        
+
         // test the four corners, for cases where the bbox dimensions are less than the terrain grid size, which is probably most of the time
         Vector3f topLeft = worldCoordinateToLocal(new Vector3f(bbox.getCenter().x-bbox.getXExtent(), 0, bbox.getCenter().z-bbox.getZExtent()));
         Vector3f topRight = worldCoordinateToLocal(new Vector3f(bbox.getCenter().x+bbox.getXExtent(), 0, bbox.getCenter().z-bbox.getZExtent()));
@@ -872,11 +873,11 @@ public class TerrainPatch extends Geometry {
         t = getTriangle(bottomRight.x, bottomRight.z);
         if (t != null && bbox.collideWith(t, results) > 0)
             return 1;
-        
+
         // box is larger than the points on the terrain, so test against the points
         for (float z=topLeft.z; z<bottomLeft.z; z+=1) {
             for (float x=topLeft.x; x<topRight.x; x+=1) {
-                
+
                 if (x < 0 || z < 0 || x >= size || z >= size)
                     continue;
                 t = getTriangle(x,z);
@@ -895,7 +896,7 @@ public class TerrainPatch extends Geometry {
         // this reduces the save size to 10% by not saving the mesh
         Mesh temp = getMesh();
         mesh = null;
-        
+
         super.write(ex);
         OutputCapsule oc = ex.getCapsule(this);
         oc.write(size, "size", 16);
@@ -908,7 +909,7 @@ public class TerrainPatch extends Geometry {
         //oc.write(lodCalculatorFactory, "lodCalculatorFactory", null);
         oc.write(lodEntropy, "lodEntropy", null);
         oc.write(geomap, "geomap", null);
-        
+
         setMesh(temp);
     }
 
@@ -927,7 +928,7 @@ public class TerrainPatch extends Geometry {
         //lodCalculatorFactory = (LodCalculatorFactory) ic.readSavable("lodCalculatorFactory", null);
         lodEntropy = ic.readFloatArray("lodEntropy", null);
         geomap = (LODGeomap) ic.readSavable("geomap", null);
-        
+
         Mesh regen = geomap.createMesh(stepScale, new Vector2f(1,1), offset, offsetAmount, totalSize, false);
         setMesh(regen);
         //TangentBinormalGenerator.generate(this); // note that this will be removed
@@ -955,6 +956,33 @@ public class TerrainPatch extends Geometry {
         return clone;
     }
 
+    /**
+     *  Called internally by com.jme3.util.clone.Cloner.  Do not call directly.
+     */
+    @Override
+    public void cloneFields( Cloner cloner, Object original ) {
+
+        this.stepScale = cloner.clone(stepScale);
+        this.offset = cloner.clone(offset);
+
+        this.leftNeighbour = null;
+        this.topNeighbour = null;
+        this.rightNeighbour = null;
+        this.bottomNeighbour = null;
+
+        // Don't feel like making geomap cloneable tonight
+        // so I'll copy the old logic.
+        this.geomap = new LODGeomap(size, geomap.getHeightArray());
+        Mesh m = geomap.createMesh(stepScale, Vector2f.UNIT_XY, offset, offsetAmount, totalSize, false);
+        this.setMesh(m);
+
+        // In this case, we always clone material even if the cloner is setup
+        // not to clone it.  Terrain uses mutable textures and stuff so it's important
+        // to clone it.  (At least that's my understanding and is evidenced by the old
+        // clone code specifically cloning material.)  -pspeed
+        this.material = material.clone();
+    }
+
     protected void ensurePositiveVolumeBBox() {
         if (getModelBound() instanceof BoundingBox) {
             if (((BoundingBox)getModelBound()).getYExtent() < 0.001f) {

+ 60 - 41
jme3-terrain/src/main/java/com/jme3/terrain/geomipmap/TerrainQuad.java

@@ -55,6 +55,7 @@ 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;
@@ -126,7 +127,7 @@ public class TerrainQuad extends Node implements Terrain {
     private Vector3f lastScale = Vector3f.UNIT_XYZ;
 
     protected NeighbourFinder neighbourFinder;
-    
+
     public TerrainQuad() {
         super("Terrain");
     }
@@ -144,24 +145,24 @@ public class TerrainQuad extends Node implements Terrain {
      * </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, 
+     * @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 
+     * @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 
+     * 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
@@ -176,7 +177,7 @@ public class TerrainQuad extends Node implements Terrain {
     }
 
     /**
-     * 
+     *
      * @param name the name of the scene element. This is required for
      * identification and comparison purposes.
      * @param patchSize size of the individual patches
@@ -192,9 +193,9 @@ public class TerrainQuad extends Node implements Terrain {
         //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
@@ -217,17 +218,17 @@ public class TerrainQuad extends Node implements Terrain {
                             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;
@@ -248,7 +249,7 @@ public class TerrainQuad extends Node implements Terrain {
     public void recalculateAllNormals() {
         affectedAreaBBox = new BoundingBox(new Vector3f(0,0,0), totalSize*2, Float.MAX_VALUE, totalSize*2);
     }
-    
+
     /**
      * Create just a flat heightmap
      */
@@ -267,11 +268,11 @@ public class TerrainQuad extends Node implements Terrain {
             //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.
@@ -343,7 +344,7 @@ public class TerrainQuad extends Node implements Terrain {
     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) {
@@ -362,7 +363,7 @@ public class TerrainQuad extends Node implements Terrain {
     public int getNumMajorSubdivisions() {
         return 1;
     }
-    
+
 
     protected boolean calculateLod(List<Vector3f> location, HashMap<String,UpdatedTerrainPatch> updates, LodCalculator lodCalculator) {
 
@@ -434,7 +435,7 @@ public class TerrainQuad extends Node implements Terrain {
                         utp.setBottomLod(utpD.getNewLod());
                         utpD.setTopLod(utp.getNewLod());
                     }
-                    
+
                     if (left != null) {
                         UpdatedTerrainPatch utpL = updated.get(left.getName());
                         if (utpL == null) {
@@ -478,7 +479,7 @@ public class TerrainQuad extends Node implements Terrain {
             }
         }
     }
-    
+
     /**
      * Find any neighbours that should have their edges seamed because another neighbour
      * changed its LOD to a greater value (less detailed)
@@ -587,10 +588,10 @@ public class TerrainQuad extends Node implements Terrain {
 
     /**
      * Quadrants, world coordinates, and heightmap coordinates (Y-up):
-     * 
+     *
      *         -z
-     *      -u | 
-     *    -v  1|3 
+     *      -u |
+     *    -v  1|3
      *  -x ----+---- x
      *        2|4 u
      *         | v
@@ -668,7 +669,7 @@ public class TerrainQuad extends Node implements Terrain {
         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);
@@ -892,7 +893,7 @@ public class TerrainQuad extends Node implements Terrain {
         }
         return false;
     }
-    
+
     /**
      * This will cause all normals for this terrain quad to be recalculated
      */
@@ -1024,14 +1025,14 @@ public class TerrainQuad extends Node implements Terrain {
         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;
@@ -1069,7 +1070,7 @@ public class TerrainQuad extends Node implements Terrain {
         }
         return null;
     }
-    
+
     /**
      * Get the interpolated height of the terrain at the specified point.
      * @param xz the location to get the height for
@@ -1090,7 +1091,7 @@ public class TerrainQuad extends Node implements Terrain {
      * 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) {
@@ -1107,10 +1108,10 @@ public class TerrainQuad extends Node implements Terrain {
         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;
@@ -1125,15 +1126,15 @@ public class TerrainQuad extends Node implements Terrain {
         // v3--v4  | Z
         //         |
         // <-------Y
-        //     X 
+        //     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);
@@ -1291,7 +1292,7 @@ public class TerrainQuad extends Node implements Terrain {
         return (x >= 0 && x <= totalSize && z >= 0 && z <= totalSize);
     }
 
-    
+
     public int getTerrainSize() {
         return totalSize;
     }
@@ -1750,7 +1751,7 @@ public class TerrainQuad extends Node implements Terrain {
         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;
@@ -1793,10 +1794,10 @@ public class TerrainQuad extends Node implements Terrain {
         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());
         }
@@ -1806,7 +1807,25 @@ public class TerrainQuad extends Node implements Terrain {
 
         return quadClone;
     }
-        
+
+    /**
+     *  Called internally by com.jme3.util.clone.Cloner.  Do not call directly.
+     */
+    @Override
+    public void cloneFields( Cloner cloner, Object 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);
@@ -1815,7 +1834,7 @@ public class TerrainQuad extends Node implements Terrain {
             clearCaches();
         }
     }
-    
+
     /**
      * Removes any cached references this terrain is holding, in particular
      * the TerrainPatch's neighbour references.
@@ -1834,7 +1853,7 @@ public class TerrainQuad extends Node implements Terrain {
             }
         }
     }
-    
+
     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