From 59b2e6871c65f58fdad78cd7229c292f6a177578 Mon Sep 17 00:00:00 2001 From: Scott Barta Date: Thu, 1 Mar 2012 12:35:35 -0800 Subject: Adds the jMonkeyEngine library to the build. Adds the jMonkeyEngine open source 3D game engine to the build. This is built as a static library and is only used by the Finsky client. Change-Id: I06a3f054df7b8a67757267d884854f70c5a16ca0 --- .../com/jme3/terrain/geomipmap/TerrainPatch.java | 983 +++++++++++++++++++++ 1 file changed, 983 insertions(+) create mode 100644 engine/src/terrain/com/jme3/terrain/geomipmap/TerrainPatch.java (limited to 'engine/src/terrain/com/jme3/terrain/geomipmap/TerrainPatch.java') diff --git a/engine/src/terrain/com/jme3/terrain/geomipmap/TerrainPatch.java b/engine/src/terrain/com/jme3/terrain/geomipmap/TerrainPatch.java new file mode 100644 index 0000000..40cf190 --- /dev/null +++ b/engine/src/terrain/com/jme3/terrain/geomipmap/TerrainPatch.java @@ -0,0 +1,983 @@ +/* + * 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.BoundingSphere; +import com.jme3.bounding.BoundingVolume; +import com.jme3.collision.Collidable; +import com.jme3.collision.CollisionResults; +import com.jme3.collision.UnsupportedCollisionException; +import com.jme3.export.InputCapsule; +import com.jme3.export.JmeExporter; +import com.jme3.export.JmeImporter; +import com.jme3.export.OutputCapsule; +import com.jme3.math.*; +import com.jme3.scene.Geometry; +import com.jme3.scene.Mesh; +import com.jme3.scene.VertexBuffer; +import com.jme3.scene.VertexBuffer.Type; +import com.jme3.terrain.geomipmap.TerrainQuad.LocationHeight; +import com.jme3.terrain.geomipmap.lodcalc.util.EntropyComputeUtil; +import com.jme3.util.BufferUtils; +import java.io.IOException; +import java.nio.FloatBuffer; +import java.nio.IntBuffer; +import java.util.HashMap; +import java.util.List; + + +/** + * A terrain patch is a leaf in the terrain quad tree. It has a mesh that can change levels of detail (LOD) + * whenever the view point, or camera, changes. The actual terrain mesh is created by the LODGeomap class. + * 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, + * then the LOD changes every 130 units away. + * + * @author Brent Owens + */ +public class TerrainPatch extends Geometry { + + protected LODGeomap geomap; + protected int lod = -1; // this terrain patch's LOD + private int maxLod = -1; + protected int previousLod = -1; + protected int lodLeft, lodTop, lodRight, lodBottom; // it's neighbour's LODs + + protected int size; + + protected int totalSize; + + protected short quadrant = 1; + + // x/z step + protected Vector3f stepScale; + + // center of the patch in relation to (0,0,0) + protected Vector2f offset; + + // amount the patch has been shifted. + protected float offsetAmount; + + //protected LodCalculator lodCalculator; + //protected LodCalculatorFactory lodCalculatorFactory; + + protected TerrainPatch leftNeighbour, topNeighbour, rightNeighbour, bottomNeighbour; + protected boolean searchedForNeighboursAlready = false; + + + protected float[] lodEntropy; + + public TerrainPatch() { + super("TerrainPatch"); + } + + public TerrainPatch(String name) { + super(name); + } + + public TerrainPatch(String name, int size) { + this(name, size, new Vector3f(1,1,1), null, new Vector3f(0,0,0)); + } + + /** + * Constructor instantiates a new TerrainPatch object. The + * parameters and heightmap data are then processed to generate a + * TriMesh object for rendering. + * + * @param name + * the name of the terrain patch. + * @param size + * the size of the heightmap. + * @param stepScale + * the scale for the axes. + * @param heightMap + * the height data. + * @param origin + * the origin offset of the patch. + */ + public TerrainPatch(String name, int size, Vector3f stepScale, + float[] heightMap, Vector3f origin) { + this(name, size, stepScale, heightMap, origin, size, new Vector2f(), 0); + } + + /** + * Constructor instantiates a new TerrainPatch object. The + * parameters and heightmap data are then processed to generate a + * TriMesh object for renderering. + * + * @param name + * the name of the terrain patch. + * @param size + * the size of the patch. + * @param stepScale + * the scale for the axes. + * @param heightMap + * the height data. + * @param origin + * the origin offset of the patch. + * @param totalSize + * the total size of the terrain. (Higher if the patch is part of + * a TerrainQuad tree. + * @param offset + * the offset for texture coordinates. + * @param offsetAmount + * the total offset amount. Used for texture coordinates. + */ + public TerrainPatch(String name, int size, Vector3f stepScale, + float[] heightMap, Vector3f origin, int totalSize, + Vector2f offset, float offsetAmount) { + super(name); + this.size = size; + this.stepScale = stepScale; + this.totalSize = totalSize; + this.offsetAmount = offsetAmount; + this.offset = offset; + + setLocalTranslation(origin); + + geomap = new LODGeomap(size, heightMap); + Mesh m = geomap.createMesh(stepScale, new Vector2f(1,1), offset, offsetAmount, totalSize, false); + setMesh(m); + + } + + /** + * This calculation is slow, so don't use it often. + */ + public void generateLodEntropies() { + float[] entropies = new float[getMaxLod()+1]; + for (int i = 0; i <= getMaxLod(); i++){ + int curLod = (int) Math.pow(2, i); + IntBuffer buf = geomap.writeIndexArrayLodDiff(null, curLod, false, false, false, false); + entropies[i] = EntropyComputeUtil.computeLodEntropy(mesh, buf); + } + + lodEntropy = entropies; + } + + public float[] getLodEntropies(){ + if (lodEntropy == null){ + generateLodEntropies(); + } + return lodEntropy; + } + + @Deprecated + public FloatBuffer getHeightmap() { + return BufferUtils.createFloatBuffer(geomap.getHeightArray()); + } + + public float[] getHeightMap() { + return geomap.getHeightArray(); + } + + /** + * The maximum lod supported by this terrain patch. + * If the patch size is 32 then the returned value would be log2(32)-2 = 3 + * You can then use that value, 3, to see how many times you can divide 32 by 2 + * before the terrain gets too un-detailed (can't stitch it any further). + * @return + */ + 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; + } + + protected void reIndexGeometry(HashMap updated, boolean useVariableLod) { + + UpdatedTerrainPatch utp = updated.get(getName()); + + if (utp != null && (utp.isReIndexNeeded() || utp.isFixEdges()) ) { + int pow = (int) Math.pow(2, utp.getNewLod()); + boolean left = utp.getLeftLod() > utp.getNewLod(); + boolean top = utp.getTopLod() > utp.getNewLod(); + boolean right = utp.getRightLod() > utp.getNewLod(); + boolean bottom = utp.getBottomLod() > utp.getNewLod(); + + IntBuffer ib = null; + if (useVariableLod) + ib = geomap.writeIndexArrayLodVariable(null, 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())); + else + ib = geomap.writeIndexArrayLodDiff(null, pow, right, top, left, bottom); + utp.setNewIndexBuffer(ib); + } + + } + + + public Vector2f getTex(float x, float z, Vector2f store) { + if (x < 0 || z < 0 || x >= size || z >= size) { + store.set(Vector2f.ZERO); + return store; + } + int idx = (int) (z * size + x); + 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 + * @param z local to the terrain patch + * @return the triangle in world coordinates, or null if the point does intersect this patch on the XZ axis + */ + public Triangle getTriangle(float x, float z) { + return geomap.getTriangleAtPoint(x, z, getWorldScale() , getWorldTranslation()); + } + + /** + * Get the triangles at the specified grid point. Probably only 2 triangles + * @param x local to the terrain patch + * @param z local to the terrain patch + * @return the triangles in world coordinates, or null if the point does intersect this patch on the XZ axis + */ + public Triangle[] getGridTriangles(float x, float z) { + return geomap.getGridTrianglesAtPoint(x, z, getWorldScale() , getWorldTranslation()); + } + + protected void setHeight(List locationHeights, boolean overrideHeight) { + + for (LocationHeight lh : locationHeights) { + if (lh.x < 0 || lh.z < 0 || lh.x >= size || lh.z >= size) + continue; + int idx = lh.z * size + lh.x; + if (overrideHeight) { + geomap.getHeightArray()[idx] = lh.h; + } else { + float h = getMesh().getFloatBuffer(Type.Position).get(idx*3+1); + geomap.getHeightArray()[idx] = h+lh.h; + } + + } + + FloatBuffer newVertexBuffer = geomap.writeVertexArray(null, stepScale, false); + getMesh().clearBuffer(Type.Position); + getMesh().setBuffer(Type.Position, 3, newVertexBuffer); + } + + /** + * recalculate all of the normal vectors in this terrain patch + */ + protected void updateNormals() { + FloatBuffer newNormalBuffer = geomap.writeNormalArray(null, getWorldScale()); + getMesh().getBuffer(Type.Normal).updateData(newNormalBuffer); + FloatBuffer newTangentBuffer = null; + FloatBuffer newBinormalBuffer = null; + FloatBuffer[] tb = geomap.writeTangentArray(newTangentBuffer, newBinormalBuffer, (FloatBuffer)getMesh().getBuffer(Type.TexCoord).getData(), getWorldScale()); + newTangentBuffer = tb[0]; + newBinormalBuffer = tb[1]; + getMesh().getBuffer(Type.Tangent).updateData(newTangentBuffer); + getMesh().getBuffer(Type.Binormal).updateData(newBinormalBuffer); + } + + /** + * 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 + * patch, and saves those normals in the neighbour's edges too. + * + * Takes 4 points (if has neighbour on that side) for each + * point on the edge of the patch: + * * + * | + * *---x---* + * | + * * + * 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. + */ + protected void fixNormalEdges(TerrainPatch right, + TerrainPatch bottom, + TerrainPatch top, + TerrainPatch left, + TerrainPatch bottomRight, + TerrainPatch bottomLeft, + TerrainPatch topRight, + TerrainPatch topLeft) + { + Vector3f rootPoint = new Vector3f(); + Vector3f rightPoint = new Vector3f(); + Vector3f leftPoint = new Vector3f(); + Vector3f topPoint = new Vector3f(); + + Vector3f bottomPoint = new Vector3f(); + + Vector3f tangent = new Vector3f(); + 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= 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(); + normal.x = nb.get(index); + normal.y = nb.get(index+1); + normal.z = nb.get(index+2); + return normal; + } + + /** + * Locks the mesh (sets it static) to improve performance. + * But it it not editable then. Set unlock to make it editable. + */ + public void lockMesh() { + getMesh().setStatic(); + } + + /** + * Unlocks the mesh (sets it dynamic) to make it editable. + * It will be editable but performance will be reduced. + * Call lockMesh to improve performance. + */ + public void unlockMesh() { + getMesh().setDynamic(); + } + + /** + * Returns the offset amount this terrain patch uses for textures. + * + * @return The current offset amount. + */ + public float getOffsetAmount() { + return offsetAmount; + } + + /** + * Returns the step scale that stretches the height map. + * + * @return The current step scale. + */ + public Vector3f getStepScale() { + return stepScale; + } + + /** + * Returns the total size of the terrain. + * + * @return The terrain's total size. + */ + public int getTotalSize() { + return totalSize; + } + + /** + * Returns the size of this terrain patch. + * + * @return The current patch size. + */ + public int getSize() { + return size; + } + + /** + * Returns the current offset amount. This is used when building texture + * coordinates. + * + * @return The current offset amount. + */ + public Vector2f getOffset() { + return offset; + } + + /** + * Sets the value for the current offset amount to use when building texture + * coordinates. Note that this does NOT rebuild the terrain at all. + * This is mostly used for outside constructors of terrain patches. + * + * @param offset + * The new texture offset. + */ + public void setOffset(Vector2f offset) { + this.offset = offset; + } + + /** + * Sets the size of this terrain patch. Note that this does NOT + * rebuild the terrain at all. This is mostly used for outside constructors + * of terrain patches. + * + * @param size + * The new size. + */ + public void setSize(int size) { + this.size = size; + + maxLod = -1; // reset it + } + + /** + * Sets the total size of the terrain . Note that this does NOT + * rebuild the terrain at all. This is mostly used for outside constructors + * of terrain patches. + * + * @param totalSize + * The new total size. + */ + public void setTotalSize(int totalSize) { + this.totalSize = totalSize; + } + + /** + * Sets the step scale of this terrain patch's height map. Note that this + * does NOT rebuild the terrain at all. This is mostly used for + * outside constructors of terrain patches. + * + * @param stepScale + * The new step scale. + */ + public void setStepScale(Vector3f stepScale) { + this.stepScale = stepScale; + } + + /** + * Sets the offset of this terrain texture map. Note that this does NOT + * rebuild the terrain at all. This is mostly used for outside + * constructors of terrain patches. + * + * @param offsetAmount + * The new texture offset. + */ + public void setOffsetAmount(float offsetAmount) { + this.offsetAmount = offsetAmount; + } + + /** + * @return Returns the quadrant. + */ + public short getQuadrant() { + return quadrant; + } + + /** + * @param quadrant + * The quadrant to set. + */ + public void setQuadrant(short quadrant) { + this.quadrant = quadrant; + } + + public int getLod() { + return lod; + } + + public void setLod(int lod) { + this.lod = lod; + } + + public int getPreviousLod() { + return previousLod; + } + + public void setPreviousLod(int previousLod) { + this.previousLod = previousLod; + } + + protected int getLodLeft() { + return lodLeft; + } + + protected void setLodLeft(int lodLeft) { + this.lodLeft = lodLeft; + } + + protected int getLodTop() { + return lodTop; + } + + protected void setLodTop(int lodTop) { + this.lodTop = lodTop; + } + + protected int getLodRight() { + return lodRight; + } + + protected void setLodRight(int lodRight) { + this.lodRight = lodRight; + } + + protected int getLodBottom() { + return lodBottom; + } + + protected void setLodBottom(int lodBottom) { + this.lodBottom = lodBottom; + } + + /*public void setLodCalculator(LodCalculatorFactory lodCalculatorFactory) { + this.lodCalculatorFactory = lodCalculatorFactory; + setLodCalculator(lodCalculatorFactory.createCalculator(this)); + }*/ + + @Override + public int collideWith(Collidable other, CollisionResults results) throws UnsupportedCollisionException { + if (refreshFlags != 0) + throw new IllegalStateException("Scene graph must be updated" + + " before checking collision"); + + 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) + return collideWithBoundingVolume((BoundingVolume)other, results); + else { + throw new UnsupportedCollisionException("TerrainPatch cannnot collide with "+other.getClass().getName()); + } + } + + + private int collideWithRay(Ray ray, CollisionResults results) { + // This should be handled in the root terrain quad + return 0; + } + + private int collideWithBoundingVolume(BoundingVolume boundingVolume, CollisionResults results) { + if (boundingVolume instanceof BoundingBox) + return collideWithBoundingBox((BoundingBox)boundingVolume, results); + else if(boundingVolume instanceof BoundingSphere) { + BoundingSphere sphere = (BoundingSphere) boundingVolume; + BoundingBox bbox = new BoundingBox(boundingVolume.getCenter().clone(), sphere.getRadius(), + sphere.getRadius(), + sphere.getRadius()); + return collideWithBoundingBox(bbox, results); + } + return 0; + } + + protected Vector3f worldCoordinateToLocal(Vector3f loc) { + Vector3f translated = new Vector3f(); + translated.x = loc.x/getWorldScale().x - getWorldTranslation().x; + translated.y = loc.y/getWorldScale().y - getWorldTranslation().y; + translated.z = loc.z/getWorldScale().z - getWorldTranslation().z; + return translated; + } + + /** + * 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())); + Vector3f bottomLeft = worldCoordinateToLocal(new Vector3f(bbox.getCenter().x-bbox.getXExtent(), 0, bbox.getCenter().z+bbox.getZExtent())); + Vector3f bottomRight = worldCoordinateToLocal(new Vector3f(bbox.getCenter().x+bbox.getXExtent(), 0, bbox.getCenter().z+bbox.getZExtent())); + + Triangle t = getTriangle(topLeft.x, topLeft.z); + if (t != null && bbox.collideWith(t, results) > 0) + return 1; + t = getTriangle(topRight.x, topRight.z); + if (t != null && bbox.collideWith(t, results) > 0) + return 1; + t = getTriangle(bottomLeft.x, bottomLeft.z); + if (t != null && bbox.collideWith(t, results) > 0) + return 1; + 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= size || z >= size) + continue; + t = getTriangle(x,z); + if (t != null && bbox.collideWith(t, results) > 0) + return 1; + } + } + + return 0; + } + + + @Override + public void write(JmeExporter ex) throws IOException { + // the mesh is removed, and reloaded when read() is called + // 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); + oc.write(totalSize, "totalSize", 16); + oc.write(quadrant, "quadrant", (short)0); + oc.write(stepScale, "stepScale", Vector3f.UNIT_XYZ); + oc.write(offset, "offset", Vector3f.UNIT_XYZ); + oc.write(offsetAmount, "offsetAmount", 0); + //oc.write(lodCalculator, "lodCalculator", null); + //oc.write(lodCalculatorFactory, "lodCalculatorFactory", null); + oc.write(lodEntropy, "lodEntropy", null); + oc.write(geomap, "geomap", null); + + setMesh(temp); + } + + @Override + public void read(JmeImporter im) throws IOException { + super.read(im); + InputCapsule ic = im.getCapsule(this); + size = ic.readInt("size", 16); + totalSize = ic.readInt("totalSize", 16); + quadrant = ic.readShort("quadrant", (short)0); + stepScale = (Vector3f) ic.readSavable("stepScale", Vector3f.UNIT_XYZ); + offset = (Vector2f) ic.readSavable("offset", Vector3f.UNIT_XYZ); + offsetAmount = ic.readFloat("offsetAmount", 0); + //lodCalculator = (LodCalculator) ic.readSavable("lodCalculator", new DistanceLodCalculator()); + //lodCalculator.setTerrainPatch(this); + //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 + ensurePositiveVolumeBBox(); + } + + @Override + public TerrainPatch clone() { + TerrainPatch clone = new TerrainPatch(); + clone.name = name.toString(); + clone.size = size; + clone.totalSize = totalSize; + clone.quadrant = quadrant; + clone.stepScale = stepScale.clone(); + clone.offset = offset.clone(); + clone.offsetAmount = offsetAmount; + //clone.lodCalculator = lodCalculator.clone(); + //clone.lodCalculator.setTerrainPatch(clone); + //clone.setLodCalculator(lodCalculatorFactory.clone()); + clone.geomap = new LODGeomap(size, geomap.getHeightArray()); + clone.setLocalTranslation(getLocalTranslation().clone()); + Mesh m = clone.geomap.createMesh(clone.stepScale, Vector2f.UNIT_XY, clone.offset, clone.offsetAmount, clone.totalSize, false); + clone.setMesh(m); + clone.setMaterial(material.clone()); + return clone; + } + + protected void ensurePositiveVolumeBBox() { + if (getModelBound() instanceof BoundingBox) { + if (((BoundingBox)getModelBound()).getYExtent() < 0.001f) { + // a correction so the box always has a volume + ((BoundingBox)getModelBound()).setYExtent(0.001f); + updateWorldBound(); + } + } + } + + + +} -- cgit v1.2.3