瀏覽代碼

Converted spatial over to use Cloner to do its various
deep and semi-shallow cloning. I'd be very surprised if nothing
is broken as there is only so much testing I can easily do.
Also various fixes for places I forgot to call super.cloneFields().

Paul Speed 9 年之前
父節點
當前提交
ab6fb03171

+ 12 - 17
jme3-core/src/main/java/com/jme3/animation/EffectTrack.java

@@ -118,22 +118,17 @@ public class EffectTrack implements ClonableTrack {
             }
         }
 
-        @Override   
+        @Override
         public Object jmeClone() {
             KillParticleControl c = new KillParticleControl();
             //this control should be removed as it shouldn't have been persisted in the first place
-            //In the quest to find the less hackish solution to achieve this, 
-            //making it remove itself from the spatial in the first update loop when loaded was the less bad. 
+            //In the quest to find the less hackish solution to achieve this,
+            //making it remove itself from the spatial in the first update loop when loaded was the less bad.
             c.remove = true;
             c.spatial = spatial;
             return c;
-        }     
-
-        @Override   
-        public void cloneFields( Cloner cloner, Object original ) { 
-            this.spatial = cloner.clone(spatial);
         }
-         
+
         @Override
         protected void controlRender(RenderManager rm, ViewPort vp) {
         }
@@ -143,8 +138,8 @@ public class EffectTrack implements ClonableTrack {
 
             KillParticleControl c = new KillParticleControl();
             //this control should be removed as it shouldn't have been persisted in the first place
-            //In the quest to find the less hackish solution to achieve this, 
-            //making it remove itself from the spatial in the first update loop when loaded was the less bad. 
+            //In the quest to find the less hackish solution to achieve this,
+            //making it remove itself from the spatial in the first update loop when loaded was the less bad.
             c.remove = true;
             c.setSpatial(spatial);
             return c;
@@ -261,7 +256,7 @@ public class EffectTrack implements ClonableTrack {
     public float[] getKeyFrameTimes() {
         return new float[] { startOffset };
     }
-    
+
     /**
      * Clone this track
      *
@@ -302,21 +297,21 @@ public class EffectTrack implements ClonableTrack {
         return effectTrack;
     }
 
-    @Override   
+    @Override
     public Object jmeClone() {
         try {
             return super.clone();
         } catch( CloneNotSupportedException e ) {
             throw new RuntimeException("Error cloning", e);
         }
-    }     
+    }
 
 
-    @Override   
-    public void cloneFields( Cloner cloner, Object original ) { 
+    @Override
+    public void cloneFields( Cloner cloner, Object original ) {
         this.emitter = cloner.clone(emitter);
     }
-         
+
     /**
      * recursive function responsible for finding the newly cloned Emitter
      *

+ 2 - 0
jme3-core/src/main/java/com/jme3/audio/AudioNode.java

@@ -730,6 +730,8 @@ public class AudioNode extends Node implements AudioSource {
      */
     @Override
     public void cloneFields( Cloner cloner, Object original ) {
+        super.cloneFields(cloner, original);
+
         this.direction = cloner.clone(direction);
         this.velocity = cloner.clone(velocity);
 

+ 9 - 0
jme3-core/src/main/java/com/jme3/effect/ParticleEmitter.java

@@ -174,6 +174,13 @@ public class ParticleEmitter extends Geometry {
 
     @Override
     public ParticleEmitter clone(boolean cloneMaterial) {
+        return (ParticleEmitter)super.clone(cloneMaterial);
+    }
+
+    /**
+     *  The old clone() method that did not use the new Cloner utility.
+     */
+    public ParticleEmitter oldClone(boolean cloneMaterial) {
         ParticleEmitter clone = (ParticleEmitter) super.clone(cloneMaterial);
         clone.shape = shape.deepClone();
 
@@ -216,6 +223,8 @@ public class ParticleEmitter extends Geometry {
      */
     @Override
     public void cloneFields( Cloner cloner, Object original ) {
+        super.cloneFields(cloner, original);
+
         this.shape = cloner.clone(shape);
         this.control = cloner.clone(control);
         this.faceNormal = cloner.clone(faceNormal);

+ 2 - 0
jme3-core/src/main/java/com/jme3/effect/influencers/RadialParticleInfluencer.java

@@ -125,6 +125,8 @@ public class RadialParticleInfluencer extends DefaultParticleInfluencer {
      */
     @Override
     public void cloneFields( Cloner cloner, Object original ) {
+        super.cloneFields(cloner, original);
+
         // Change in behavior: the old origin was not cloned -pspeed
         this.origin = cloner.clone(origin);
     }

+ 2 - 0
jme3-core/src/main/java/com/jme3/font/BitmapText.java

@@ -90,6 +90,8 @@ public class BitmapText extends Node {
      */
     @Override
     public void cloneFields( Cloner cloner, Object original ) {
+        super.cloneFields(cloner, original);
+
         for( int i = 0; i < textPages.length; i++ ) {
             textPages[i] = cloner.clone(textPages[i]);
         }

+ 2 - 0
jme3-core/src/main/java/com/jme3/scene/AssetLinkNode.java

@@ -76,6 +76,8 @@ public class AssetLinkNode extends Node {
      */
     @Override
     public void cloneFields( Cloner cloner, Object original ) {
+        super.cloneFields(cloner, original);
+
         // This is a change in behavior because the old version did not clone
         // this list... changes to one clone would be reflected in all.
         // I think that's probably undesirable. -pspeed

+ 2 - 0
jme3-core/src/main/java/com/jme3/scene/BatchNode.java

@@ -727,6 +727,8 @@ public class BatchNode extends GeometryGroupNode {
      */
     @Override
     public void cloneFields( Cloner cloner, Object original ) {
+        super.cloneFields(cloner, original);
+
         this.batches = cloner.clone(batches);
         this.tmpFloat = cloner.clone(tmpFloat);
         this.tmpFloatN = cloner.clone(tmpFloatN);

+ 2 - 0
jme3-core/src/main/java/com/jme3/scene/CameraNode.java

@@ -100,6 +100,8 @@ public class CameraNode extends Node {
      */
     @Override
     public void cloneFields( Cloner cloner, Object original ) {
+        super.cloneFields(cloner, original);
+
         // A change in behavior... I think previously CameraNode was probably
         // not really cloneable... or at least its camControl would be pointing
         // to the wrong control. -pspeed

+ 47 - 2
jme3-core/src/main/java/com/jme3/scene/Geometry.java

@@ -44,6 +44,7 @@ import com.jme3.math.Matrix4f;
 import com.jme3.renderer.Camera;
 import com.jme3.scene.VertexBuffer.Type;
 import com.jme3.util.clone.Cloner;
+import com.jme3.util.clone.IdentityCloneFunction;
 import com.jme3.util.TempVars;
 import java.io.IOException;
 import java.util.Queue;
@@ -492,6 +493,13 @@ public class Geometry extends Spatial {
      */
     @Override
     public Geometry clone(boolean cloneMaterial) {
+        return (Geometry)super.clone(cloneMaterial);
+    }
+
+    /**
+     *  The old clone() method that did not use the new Cloner utility.
+     */
+    public Geometry oldClone(boolean cloneMaterial) {
         Geometry geomClone = (Geometry) super.clone(cloneMaterial);
 
         // This geometry is managed,
@@ -535,6 +543,10 @@ public class Geometry extends Spatial {
      */
     @Override
     public Spatial deepClone() {
+        return super.deepClone();
+    }
+
+    public Spatial oldDeepClone() {
         Geometry geomClone = clone(true);
         geomClone.mesh = mesh.deepClone();
         return geomClone;
@@ -545,9 +557,42 @@ public class Geometry extends Spatial {
      */
     @Override
     public void cloneFields( Cloner cloner, Object original ) {
-        this.mesh = cloner.clone(mesh);
+        super.cloneFields(cloner, original);
+
+        // If this is a grouped node and if our group node is
+        // also cloned then we'll grab it's reference.
+        if( groupNode != null ) {
+            if( cloner.isCloned(groupNode) ) {
+                // Then resolve the reference
+                this.groupNode = cloner.clone(groupNode);
+            } else {
+                // We are on our own now
+                this.groupNode = null;
+                this.startIndex = -1;
+            }
+
+            // The above is based on the fact that if we were
+            // cloning the hierarchy that contained the parent
+            // group then it would have been shallow cloned before
+            // this child.  Can't really be otherwise.
+        }
+
+        this.cachedWorldMat = cloner.clone(cachedWorldMat);
+
+        // See if we are doing a shallow clone or a deep mesh clone
+        boolean shallowClone = (cloner.getCloneFunction(Mesh.class) instanceof IdentityCloneFunction);
+
+        // See if we clone the mesh using the special animation
+        // semi-deep cloning
+        if( shallowClone && mesh != null && mesh.getBuffer(Type.BindPosePosition) != null ) {
+            // Then we need to clone the mesh a little deeper
+            this.mesh = mesh.cloneForAnim();
+        } else {
+            // Do whatever the cloner wants to do about it
+            this.mesh = cloner.clone(mesh);
+        }
+
         this.material = cloner.clone(material);
-        this.groupNode = cloner.clone(groupNode);
     }
 
     @Override

+ 2 - 0
jme3-core/src/main/java/com/jme3/scene/LightNode.java

@@ -100,6 +100,8 @@ public class LightNode extends Node {
      */
     @Override
     public void cloneFields( Cloner cloner, Object original ) {
+        super.cloneFields(cloner, original);
+
         // A change in behavior... I think previously LightNode was probably
         // not really cloneable... or at least its lightControl would be pointing
         // to the wrong control. -pspeed

+ 3 - 1
jme3-core/src/main/java/com/jme3/scene/Mesh.java

@@ -307,7 +307,9 @@ public class Mesh implements Savable, Cloneable, JmeCloneable {
     @Override
     public Mesh jmeClone() {
         try {
-            return (Mesh)super.clone();
+            Mesh clone = (Mesh)super.clone();
+            clone.vertexArrayID = -1;
+            return clone;
         } catch (CloneNotSupportedException ex) {
             throw new AssertionError();
         }

+ 13 - 1
jme3-core/src/main/java/com/jme3/scene/Node.java

@@ -697,7 +697,17 @@ public class Node extends Spatial {
     }
 
     @Override
-    public Spatial deepClone(){
+    public Spatial deepClone() {
+        Node nodeClone = (Node)super.deepClone();
+
+        // Reset the fields of the clone that should be in a 'new' state.
+        nodeClone.updateList = null;
+        nodeClone.updateListValid = false; // safe because parent is nulled out in super.clone()
+
+        return nodeClone;
+    }
+
+    public Spatial oldDeepClone(){
         Node nodeClone = (Node) super.clone();
         nodeClone.children = new SafeArrayList<Spatial>(Spatial.class);
         for (Spatial child : children){
@@ -713,6 +723,8 @@ public class Node extends Spatial {
      */
     @Override
     public void cloneFields( Cloner cloner, Object original ) {
+        super.cloneFields(cloner, original);
+
         this.children = cloner.clone(children);
 
         // Only the outer cloning thing knows whether this should be nulled

+ 60 - 12
jme3-core/src/main/java/com/jme3/scene/Spatial.java

@@ -48,6 +48,7 @@ import com.jme3.renderer.queue.RenderQueue.Bucket;
 import com.jme3.renderer.queue.RenderQueue.ShadowMode;
 import com.jme3.scene.control.Control;
 import com.jme3.util.clone.Cloner;
+import com.jme3.util.clone.IdentityCloneFunction;
 import com.jme3.util.clone.JmeCloneable;
 import com.jme3.util.SafeArrayList;
 import com.jme3.util.TempVars;
@@ -1263,12 +1264,42 @@ public abstract class Spatial implements Savable, Cloneable, Collidable, Cloneab
      * Note that meshes of geometries are not cloned explicitly, they
      * are shared if static, or specially cloned if animated.
      *
-     * All controls will be cloned using the Control.cloneForSpatial method
-     * on the clone.
-     *
      * @see Mesh#cloneForAnim()
      */
-    public Spatial clone(boolean cloneMaterial) {
+    public Spatial clone( boolean cloneMaterial ) {
+
+        // Setup the cloner for the type of cloning we want to do.
+        Cloner cloner = new Cloner();
+
+        // First, we definitely do not want to clone our own parent
+        cloner.setClonedValue(parent, null);
+
+        // If we aren't cloning materials then we will make sure those
+        // aren't cloned also
+        if( !cloneMaterial ) {
+            cloner.setCloneFunction(Material.class, new IdentityCloneFunction<Material>());
+        }
+
+        // By default the meshes are not cloned.  The geometry
+        // may choose to selectively force them to be cloned but
+        // normally they will be shared
+        cloner.setCloneFunction(Mesh.class, new IdentityCloneFunction<Mesh>());
+
+        // Clone it!
+        Spatial clone = cloner.clone(this);
+
+        // Because we've nulled the parent out we need to make sure
+        // the transforms and stuff get refreshed.
+        clone.setTransformRefresh();
+        clone.setLightListRefresh();
+
+        return clone;
+    }
+
+    /**
+     *  The old clone() method that did not use the new Cloner utility.
+     */
+    public Spatial oldClone(boolean cloneMaterial) {
         try {
             Spatial clone = (Spatial) super.clone();
             if (worldBound != null) {
@@ -1344,7 +1375,22 @@ public abstract class Spatial implements Savable, Cloneable, Collidable, Cloneab
      *
      * @see Spatial#clone()
      */
-    public abstract Spatial deepClone();
+    public Spatial deepClone() {
+        // Setup the cloner for the type of cloning we want to do.
+        Cloner cloner = new Cloner();
+
+        // First, we definitely do not want to clone our own parent
+        cloner.setClonedValue(parent, null);
+
+        Spatial clone = cloner.clone(this);
+
+        // Because we've nulled the parent out we need to make sure
+        // the transforms and stuff get refreshed.
+        clone.setTransformRefresh();
+        clone.setLightListRefresh();
+
+        return clone;
+    }
 
     /**
      *  Called internally by com.jme3.util.clone.Cloner.  Do not call directly.
@@ -1381,13 +1427,15 @@ public abstract class Spatial implements Savable, Cloneable, Collidable, Cloneab
         // to avoid all of the nasty cloneForSpatial() fixup style code that
         // used to inject stuff into the clone's user data.  By using cloner
         // to clone the user data we get this automatically.
-        userData = (HashMap<String, Savable>)userData.clone();
-        for( Map.Entry<String, Savable> e : userData.entrySet() ) {
-            Savable value = e.getValue();
-            if( value instanceof Cloneable ) {
-                // Note: all JmeCloneable objects are also Cloneable so this
-                // catches both cases.
-                e.setValue(cloner.clone(value));
+        if( userData != null ) {
+            userData = (HashMap<String, Savable>)userData.clone();
+            for( Map.Entry<String, Savable> e : userData.entrySet() ) {
+                Savable value = e.getValue();
+                if( value instanceof Cloneable ) {
+                    // Note: all JmeCloneable objects are also Cloneable so this
+                    // catches both cases.
+                    e.setValue(cloner.clone(value));
+                }
             }
         }
     }

+ 2 - 0
jme3-core/src/main/java/com/jme3/scene/instancing/InstancedGeometry.java

@@ -349,6 +349,8 @@ public class InstancedGeometry extends Geometry {
      */
     @Override
     public void cloneFields( Cloner cloner, Object original ) {
+        super.cloneFields(cloner, original);
+
         this.globalInstanceData = cloner.clone(globalInstanceData);
         this.transformInstanceData = cloner.clone(transformInstanceData);
         this.geometries = cloner.clone(geometries);

+ 2 - 0
jme3-core/src/main/java/com/jme3/scene/instancing/InstancedNode.java

@@ -350,6 +350,8 @@ public class InstancedNode extends GeometryGroupNode {
      */
     @Override
     public void cloneFields( Cloner cloner, Object original ) {
+        super.cloneFields(cloner, original);
+
         this.control = cloner.clone(control);
         this.lookUp = cloner.clone(lookUp);
 

+ 1 - 0
jme3-terrain/src/main/java/com/jme3/terrain/geomipmap/NormalRecalcControl.java

@@ -84,6 +84,7 @@ public class NormalRecalcControl extends AbstractControl {
      */
     @Override
     public void cloneFields( Cloner cloner, Object original ) {
+        super.cloneFields(cloner, original);
         this.terrain = cloner.clone(terrain);
     }
 

+ 45 - 43
jme3-terrain/src/main/java/com/jme3/terrain/geomipmap/TerrainLodControl.java

@@ -74,12 +74,12 @@ import java.util.logging.Logger;
  * This control serializes, but it does not save the Camera reference.
  * This camera reference has to be manually added in when you load the
  * terrain to the scene!
- * 
+ *
  * When the control or the terrain are removed from the scene, you should call
  * TerrainLodControl.detachAndCleanUpControl() to remove any threads it created
  * to handle the LOD processing. If you supply your own executor service, then
  * you have to handle its thread termination yourself.
- * 
+ *
  * @author Brent Owens
  */
 public class TerrainLodControl extends AbstractControl {
@@ -92,15 +92,15 @@ public class TerrainLodControl extends AbstractControl {
 
     private HashMap<String,UpdatedTerrainPatch> updatedPatches;
     private final Object updatePatchesLock = new Object();
-    
+
     protected List<Vector3f> lastCameraLocations; // used for LOD calc
     private AtomicBoolean lodCalcRunning = new AtomicBoolean(false);
     private int lodOffCount = 0;
-    
+
     protected ExecutorService executor;
     protected Future<HashMap<String, UpdatedTerrainPatch>> indexer;
     private boolean forceUpdate = true;
-    
+
     public TerrainLodControl() {
     }
 
@@ -111,7 +111,7 @@ public class TerrainLodControl extends AbstractControl {
         this.cameras = cams;
         lodCalculator = new DistanceLodCalculator(65, 2.7f); // a default calculator
     }
-    
+
     /**
      * Only uses the first camera right now.
      * @param terrain to act upon (must be a Spatial)
@@ -134,7 +134,7 @@ public class TerrainLodControl extends AbstractControl {
     public void setExecutor(ExecutorService executor) {
         this.executor = executor;
     }
-    
+
     protected ExecutorService createExecutorService() {
         return Executors.newSingleThreadExecutor(new ThreadFactory() {
             public Thread newThread(Runnable r) {
@@ -145,14 +145,14 @@ public class TerrainLodControl extends AbstractControl {
             }
         });
     }
-    
+
     @Override
     protected void controlUpdate(float tpf) {
         //list of cameras for when terrain supports multiple cameras (ie split screen)
 
         if (lodCalculator == null)
             return;
-        
+
         if (!enabled) {
             if (!hasResetLod) {
                 // this will get run once
@@ -160,7 +160,7 @@ public class TerrainLodControl extends AbstractControl {
                 lodCalculator.turnOffLod();
             }
         }
-        
+
         if (cameras != null) {
             cameraLocations.clear();
             for (Camera c : cameras) // populate them
@@ -170,7 +170,7 @@ public class TerrainLodControl extends AbstractControl {
             updateLOD(cameraLocations, lodCalculator);
         }
     }
-    
+
     /**
      * Call this when you remove the terrain or this control from the scene.
      * It will clear up any threads it had.
@@ -186,7 +186,7 @@ public class TerrainLodControl extends AbstractControl {
         if(getSpatial() == null){
             return;
         }
-        
+
         // update any existing ones that need updating
         updateQuadLODs();
 
@@ -196,9 +196,9 @@ public class TerrainLodControl extends AbstractControl {
                 return;
             else
                 lodOffCount++;
-        } else 
+        } else
             lodOffCount = 0;
-        
+
         if (lastCameraLocations != null) {
             if (!forceUpdate && lastCameraLocationsTheSame(locations) && !lodCalculator.isLodOff())
                 return; // don't update if in same spot
@@ -218,9 +218,9 @@ public class TerrainLodControl extends AbstractControl {
 
         if (executor == null)
             executor = createExecutorService();
-        
+
         prepareTerrain();
-        
+
         UpdateLOD updateLodThread = getLodThread(locations, lodCalculator);
         indexer = executor.submit(updateLodThread);
     }
@@ -232,12 +232,12 @@ public class TerrainLodControl extends AbstractControl {
     public void forceUpdate() {
         this.forceUpdate = true;
     }
-    
+
     protected void prepareTerrain() {
         TerrainQuad terrain = (TerrainQuad)getSpatial();
         terrain.cacheTerrainTransforms();// cache the terrain's world transforms so they can be accessed on the separate thread safely
     }
-    
+
     protected UpdateLOD getLodThread(List<Vector3f> locations, LodCalculator lodCalculator) {
         return new UpdateLOD(locations, lodCalculator);
     }
@@ -249,7 +249,7 @@ public class TerrainLodControl extends AbstractControl {
         if (indexer != null) {
             if (indexer.isDone()) {
                 try {
-                    
+
                     HashMap<String, UpdatedTerrainPatch> updated = indexer.get();
                     if (updated != null) {
                         // do the actual geometry update here
@@ -257,7 +257,7 @@ public class TerrainLodControl extends AbstractControl {
                             utp.updateAll();
                         }
                     }
-                    
+
                 } catch (InterruptedException ex) {
                     Logger.getLogger(TerrainLodControl.class.getName()).log(Level.SEVERE, null, ex);
                 } catch (ExecutionException ex) {
@@ -268,7 +268,7 @@ public class TerrainLodControl extends AbstractControl {
             }
         }
     }
-    
+
     private boolean lastCameraLocationsTheSame(List<Vector3f> locations) {
         boolean theSame = true;
         for (Vector3f l : locations) {
@@ -281,7 +281,7 @@ public class TerrainLodControl extends AbstractControl {
         }
         return theSame;
     }
-    
+
     protected synchronized boolean isLodCalcRunning() {
         return lodCalcRunning.get();
     }
@@ -297,11 +297,11 @@ public class TerrainLodControl extends AbstractControl {
         return cloned;
     }
 
-    
-    
-    
- 
-    @Override   
+
+
+
+
+    @Override
     public Object jmeClone() {
         if (spatial instanceof Terrain) {
             TerrainLodControl cloned = new TerrainLodControl((Terrain) spatial, cameras);
@@ -310,21 +310,23 @@ public class TerrainLodControl extends AbstractControl {
             return cloned;
         }
         return null;
-    }     
+    }
 
-    @Override   
+    @Override
     public void cloneFields( Cloner cloner, Object original ) {
+        super.cloneFields(cloner, original);
+
         this.lodCalculator = cloner.clone(lodCalculator);
- 
-        try {       
+
+        try {
             // Not deep clone of the cameras themselves
             this.cameras = cloner.javaClone(cameras);
         } catch( CloneNotSupportedException e ) {
             throw new RuntimeException("Error cloning", e);
-        } 
-    }     
-    
-    
+        }
+    }
+
+
     @Override
     public Control cloneForSpatial(Spatial spatial) {
         if (spatial instanceof Terrain) {
@@ -346,7 +348,7 @@ public class TerrainLodControl extends AbstractControl {
         cams.add(camera);
         setCameras(cams);
     }
-    
+
     public void setCameras(List<Camera> cameras) {
         this.cameras = cameras;
         cameraLocations.clear();
@@ -374,7 +376,7 @@ public class TerrainLodControl extends AbstractControl {
     public void setLodCalculator(LodCalculator lodCalculator) {
         this.lodCalculator = lodCalculator;
     }
-    
+
     @Override
     public void setEnabled(boolean enabled) {
         this.enabled = enabled;
@@ -386,8 +388,8 @@ public class TerrainLodControl extends AbstractControl {
             lodCalculator.turnOnLod();
         }
     }
-    
-    
+
+
     /**
      * Calculates the LOD of all child terrain patches.
      */
@@ -408,7 +410,7 @@ public class TerrainLodControl extends AbstractControl {
             setLodCalcRunning(true);
 
             TerrainQuad terrainQuad = (TerrainQuad)getSpatial();
-            
+
             // go through each patch and calculate its LOD based on camera distance
             HashMap<String,UpdatedTerrainPatch> updated = new HashMap<String,UpdatedTerrainPatch>();
             boolean lodChanged = terrainQuad.calculateLod(camLocations, updated, lodCalculator); // 'updated' gets populated here
@@ -418,8 +420,8 @@ public class TerrainLodControl extends AbstractControl {
                 setLodCalcRunning(false);
                 return null;
             }
-            
-            
+
+
             // then calculate its neighbour LOD values for seaming in the shader
             terrainQuad.findNeighboursLod(updated);
 
@@ -430,7 +432,7 @@ public class TerrainLodControl extends AbstractControl {
             //setUpdateQuadLODs(updated); // set back to main ogl thread
 
             setLodCalcRunning(false);
-            
+
             return updated;
         }
     }

+ 1 - 0
jme3-terrain/src/main/java/com/jme3/terrain/geomipmap/TerrainPatch.java

@@ -961,6 +961,7 @@ public class TerrainPatch extends Geometry {
      */
     @Override
     public void cloneFields( Cloner cloner, Object original ) {
+        super.cloneFields(cloner, original);
 
         this.stepScale = cloner.clone(stepScale);
         this.offset = cloner.clone(offset);

+ 2 - 0
jme3-terrain/src/main/java/com/jme3/terrain/geomipmap/TerrainQuad.java

@@ -1813,6 +1813,8 @@ public class TerrainQuad extends Node implements Terrain {
      */
     @Override
     public void cloneFields( Cloner cloner, Object original ) {
+        super.cloneFields(cloner, original);
+
         this.stepScale = cloner.clone(stepScale);
         this.offset = cloner.clone(offset);