/*
 * Copyright (c) 2009-2010 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 java.nio.FloatBuffer;
import java.nio.IntBuffer;
import java.util.HashMap;

import com.jme3.math.FastMath;
import com.jme3.math.Ray;
import com.jme3.math.Triangle;
import com.jme3.math.Vector2f;
import com.jme3.math.Vector3f;
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.DistanceLodCalculator;
import com.jme3.terrain.geomipmap.lodcalc.LodCalculator;
import com.jme3.terrain.geomipmap.lodcalc.LodCalculatorFactory;
import com.jme3.terrain.geomipmap.lodcalc.util.EntropyComputeUtil;
import com.jme3.util.BufferUtils;
import com.jme3.util.TangentBinormalGenerator;
import java.io.IOException;
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 <code>TerrainPatch</code> object. The
     * parameters and heightmap data are then processed to generate a
     * <code>TriMesh</code> 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 <code>TerrainPatch</code> object. The
     * parameters and heightmap data are then processed to generate a
     * <code>TriMesh</code> 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 <code>TerrainQuad</code> 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);

        FloatBuffer heightBuffer = BufferUtils.createFloatBuffer(size*size);
        heightBuffer.put(heightMap);

        geomap = new LODGeomap(size, heightBuffer);
        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;
    }

    public FloatBuffer getHeightmap() {
        return geomap.getHeightData();
    }

    /**
     * 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;
    }

    
	

    /**
     * Delegates to the lodCalculator that was passed in.
     * @param locations all possible camera locations
     * @param updates update objects that may or may not contain this terrain patch
     * @return true if the geometry needs re-indexing
     */
    protected boolean calculateLod(List<Vector3f> locations, HashMap<String,UpdatedTerrainPatch> updates) {
        return lodCalculator.calculateLod(locations, updates);
    }

    protected void reIndexGeometry(HashMap<String,UpdatedTerrainPatch> updated) {

        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 (lodCalculator.usesVariableLod())
                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 gridX local to the terrain patch
     * @param gridY 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 gridX local to the terrain patch
     * @param gridY 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<LocationHeight> 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.getHeightData().put(idx, lh.h);
            } else {
                float h = getMesh().getFloatBuffer(Type.Position).get(idx*3+1);
                geomap.getHeightData().put(idx, h+lh.h);
            }
            
        }

        FloatBuffer newVertexBuffer = geomap.writeVertexArray(null, stepScale, false);
        getMesh().clearBuffer(Type.Position);
        getMesh().setBuffer(Type.Position, 3, newVertexBuffer);
    }

    /**
     * recalculate all of this 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---*
     *              |
     *              *
     */
    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();
        Vector2f rootTex = new Vector2f();
        Vector2f rightTex = new Vector2f();
        Vector2f leftTex = new Vector2f();
        Vector2f topTex = new Vector2f();
        Vector2f bottomTex = new Vector2f();
        Vector3f normal = new Vector3f();
        Vector3f tangent = new Vector3f();

        int s = this.getSize()-1;
        
        if (right != null) { // right side
            for (int i=0; i<s+1; i++) {
                rootPoint.set(s, this.getHeightmapHeight(s,i), i);
                leftPoint.set(s-1, this.getHeightmapHeight(s-1,i), i);
                rightPoint.set(s+1, right.getHeightmapHeight(1,i), i);
                this.getTex(s,i, rootTex);
                this.getTex(s-1,i, leftTex);
                right.getTex(1,i, rightTex);
                if (i == 0) { // top
                    if (top == null) {
                        bottomPoint.set(s, this.getHeightmapHeight(s,i+1), i+1);
                        this.getTex(s,i+1, bottomTex);
                        averageNormalsTangents(null, rootPoint, leftPoint, bottomPoint, rightPoint, null, rootTex, leftTex, bottomTex, rightTex, normal, tangent);
                        VertexBuffer tpNB = this.getMesh().getBuffer(Type.Normal);
                        VertexBuffer tpTB = this.getMesh().getBuffer(Type.Tangent);
                        VertexBuffer rightNB = right.getMesh().getBuffer(Type.Normal);
                        VertexBuffer rightTB = right.getMesh().getBuffer(Type.Tangent);
                        BufferUtils.setInBuffer(normal, (FloatBuffer)tpNB.getData(), s);
                        BufferUtils.setInBuffer(tangent, (FloatBuffer)tpTB.getData(), s);
                        BufferUtils.setInBuffer(normal, (FloatBuffer)rightNB.getData(), 0);
                        BufferUtils.setInBuffer(tangent, (FloatBuffer)rightTB.getData(), 0);
                    } else {
                        topPoint.set(s, top.getHeightmapHeight(s,s-1), i-1);
                        bottomPoint.set(s, this.getHeightmapHeight(s,i+1), i+1);
                        top.getTex(s,s-1, topTex);
                        this.getTex(s,i+1, bottomTex);
                        averageNormalsTangents(topPoint, rootPoint, leftPoint, bottomPoint, rightPoint, topTex, rootTex, leftTex, bottomTex, rightTex, normal, tangent);
                        VertexBuffer tpNB = this.getMesh().getBuffer(Type.Normal);
                        VertexBuffer tpTB = this.getMesh().getBuffer(Type.Tangent);
                        VertexBuffer rightNB = right.getMesh().getBuffer(Type.Normal);
                        VertexBuffer rightTB = right.getMesh().getBuffer(Type.Tangent);
                        VertexBuffer topNB = top.getMesh().getBuffer(Type.Normal);
                        VertexBuffer topTB = top.getMesh().getBuffer(Type.Tangent);
                        BufferUtils.setInBuffer(normal, (FloatBuffer)tpNB.getData(), s);
                        BufferUtils.setInBuffer(tangent, (FloatBuffer)tpTB.getData(), s);
                        BufferUtils.setInBuffer(normal, (FloatBuffer)rightNB.getData(), 0);
                        BufferUtils.setInBuffer(tangent, (FloatBuffer)rightTB.getData(), 0);
                        BufferUtils.setInBuffer(normal, (FloatBuffer)topNB.getData(), (s+1)*(s+1)-1);
                        BufferUtils.setInBuffer(tangent, (FloatBuffer)topTB.getData(), (s+1)*(s+1)-1);
                        // update top right corner
                        /*if (topRight != null) {
                            VertexBuffer topRightNB = topRight.getMesh().getBuffer(Type.Normal);
                            VertexBuffer topRightTB = topRight.getMesh().getBuffer(Type.Tangent);
                            BufferUtils.setInBuffer(normal, (FloatBuffer)topRightNB.getData(), (s+1)*s);
                            BufferUtils.setInBuffer(tangent, (FloatBuffer)topRightTB.getData(), (s+1)*s);
                            topRightNB.setUpdateNeeded();
                            topRightTB.setUpdateNeeded();
                        }*/
                    }
                } else if (i == s) { // bottom
                    if (bottom == null) {
                        topPoint.set(s, this.getHeightmapHeight(s,i-1), i-1);
                        this.getTex(s,i-1, topTex);
                        averageNormalsTangents(topPoint, rootPoint, leftPoint, null, rightPoint, topTex, rootTex, leftTex, null, rightTex, normal, tangent);
                        VertexBuffer tpNB = this.getMesh().getBuffer(Type.Normal);
                        VertexBuffer tpTB = this.getMesh().getBuffer(Type.Tangent);
                        VertexBuffer rightNB = right.getMesh().getBuffer(Type.Normal);
                        VertexBuffer rightTB = right.getMesh().getBuffer(Type.Tangent);
                        BufferUtils.setInBuffer(normal, (FloatBuffer)tpNB.getData(), (s+1)*(i+1)-1);
                        BufferUtils.setInBuffer(tangent, (FloatBuffer)tpTB.getData(), (s+1)*(i+1)-1);
                        BufferUtils.setInBuffer(normal, (FloatBuffer)rightNB.getData(), (s+1)*(i));
                        BufferUtils.setInBuffer(tangent, (FloatBuffer)rightTB.getData(), (s+1)*(i));
                    } else {
                        topPoint.set(s, this.getHeightmapHeight(s,i-1), i-1);
                        bottomPoint.set(s, bottom.getHeightmapHeight(s,1), i+1);
                        this.getTex(s,i-1, topTex);
                        bottom.getTex(s,1, bottomTex);
                        averageNormalsTangents(topPoint, rootPoint, leftPoint, bottomPoint, rightPoint, topTex, rootTex, leftTex, bottomTex, rightTex, normal, tangent);
                        VertexBuffer tpNB = this.getMesh().getBuffer(Type.Normal);
                        VertexBuffer tpTB = this.getMesh().getBuffer(Type.Tangent);
                        VertexBuffer rightNB = right.getMesh().getBuffer(Type.Normal);
                        VertexBuffer rightTB = right.getMesh().getBuffer(Type.Tangent);
                        VertexBuffer downNB = bottom.getMesh().getBuffer(Type.Normal);
                        VertexBuffer downTB = bottom.getMesh().getBuffer(Type.Tangent);
                        BufferUtils.setInBuffer(normal, (FloatBuffer)tpNB.getData(), (s+1)*(s+1)-1);
                        BufferUtils.setInBuffer(tangent, (FloatBuffer)tpTB.getData(), (s+1)*(s+1)-1);
                        BufferUtils.setInBuffer(normal, (FloatBuffer)rightNB.getData(), (s+1)*s);
                        BufferUtils.setInBuffer(tangent, (FloatBuffer)rightTB.getData(), (s+1)*s);
                        BufferUtils.setInBuffer(normal, (FloatBuffer)downNB.getData(), s);
                        BufferUtils.setInBuffer(tangent, (FloatBuffer)downTB.getData(), s);
                        /*if (bottomRight != null) {
                            VertexBuffer bottomRightNB = bottomRight.getMesh().getBuffer(Type.Normal);
                            VertexBuffer bottomRightTB = bottomRight.getMesh().getBuffer(Type.Tangent);
                            BufferUtils.setInBuffer(normal, (FloatBuffer)bottomRightNB.getData(), 0);
                            BufferUtils.setInBuffer(tangent, (FloatBuffer)bottomRightTB.getData(), 0);
                            bottomRightNB.setUpdateNeeded();
                            bottomRightTB.setUpdateNeeded();
                        }*/
                        downNB.setUpdateNeeded();
                        downTB.setUpdateNeeded();
                    }
                } else { // all in the middle
                    topPoint.set(s, this.getHeightmapHeight(s,i-1), i-1);
                    bottomPoint.set(s, this.getHeightmapHeight(s,i+1), i+1);
                    this.getTex(s,i-1, topTex);
                    this.getTex(s,i+1, bottomTex);
                    averageNormalsTangents(topPoint, rootPoint, leftPoint, bottomPoint, rightPoint, topTex, rootTex, leftTex, bottomTex, rightTex, normal, tangent);
                    VertexBuffer tpNB = this.getMesh().getBuffer(Type.Normal);
                    VertexBuffer tpTB = this.getMesh().getBuffer(Type.Tangent);
                    VertexBuffer rightNB = right.getMesh().getBuffer(Type.Normal);
                    VertexBuffer rightTB = right.getMesh().getBuffer(Type.Tangent);
                    BufferUtils.setInBuffer(normal, (FloatBuffer)tpNB.getData(), (s+1)*(i+1)-1);
                    BufferUtils.setInBuffer(tangent, (FloatBuffer)tpTB.getData(), (s+1)*(i+1)-1);
                    BufferUtils.setInBuffer(normal, (FloatBuffer)rightNB.getData(), (s+1)*(i));
                    BufferUtils.setInBuffer(tangent, (FloatBuffer)rightTB.getData(), (s+1)*(i));
                }
            }
            right.getMesh().getBuffer(Type.Normal).setUpdateNeeded();
            right.getMesh().getBuffer(Type.Tangent).setUpdateNeeded();
        }

        if (bottom != null) {
            for (int i=0; i<s+1; i++) {
                rootPoint.set(i, this.getHeightmapHeight(i,s), s);
                topPoint.set(i, this.getHeightmapHeight(i,s-1), s-1);
                bottomPoint.set(i, bottom.getHeightmapHeight(i,1), s+1);
                this.getTex(i,s, rootTex);
                this.getTex(i,s-1, topTex);
                bottom.getTex(i,1, bottomTex);
                if (i == 0) { // left
                    /*if (left == null) {
                        rightPoint.set(i+1, this.getHeight(i+1,s), s);
                        averageNormalsTangents(v, t, indexes, topPoint, rootPoint, null, bottomPoint, rightPoint, topTex, rootTex, null, bottomTex, rightTex, normal, tangent);
                        VertexBuffer tpNB = this.getMesh().getBuffer(Type.Normal);
                        VertexBuffer tpTB = this.getMesh().getBuffer(Type.Tangent);
                        VertexBuffer downNB = bottom.getMesh().getBuffer(Type.Normal);
                        VertexBuffer downTB = bottom.getMesh().getBuffer(Type.Tangent);
                        BufferUtils.setInBuffer(normal, (FloatBuffer)tpNB.getData(), (s+1)*s);
                        BufferUtils.setInBuffer(tangent, (FloatBuffer)tpTB.getData(), (s+1)*s);
                        BufferUtils.setInBuffer(normal, (FloatBuffer)downNB.getData(), 0);
                        BufferUtils.setInBuffer(tangent, (FloatBuffer)downTB.getData(), 0);
                    } else {
                        leftPoint.set(i-1, left.getHeight(s-1,s), s);
                        rightPoint.set(i+1, this.getHeight(i+1,s), s);
                        left.getTex(i-1,s, leftTex);
                        this.getTex(i+1,s, rightTex);
                        averageNormalsTangents(v, t, indexes, topPoint, rootPoint, leftPoint, bottomPoint, rightPoint, topTex, rootTex, leftTex, bottomTex, rightTex, normal, tangent);
                        VertexBuffer tpNB = this.getMesh().getBuffer(Type.Normal);
                        VertexBuffer tpTB = this.getMesh().getBuffer(Type.Tangent);
                        VertexBuffer leftNB = left.getMesh().getBuffer(Type.Normal);
                        VertexBuffer leftTB = left.getMesh().getBuffer(Type.Tangent);
                        VertexBuffer downNB = bottom.getMesh().getBuffer(Type.Normal);
                        VertexBuffer downTB = bottom.getMesh().getBuffer(Type.Tangent);
                        BufferUtils.setInBuffer(normal, (FloatBuffer)tpNB.getData(), (s+1)*s);
                        BufferUtils.setInBuffer(tangent, (FloatBuffer)tpTB.getData(), (s+1)*s);
                        BufferUtils.setInBuffer(normal, (FloatBuffer)leftNB.getData(), (s+1)*(s+1)-1);
                        BufferUtils.setInBuffer(tangent, (FloatBuffer)leftTB.getData(), (s+1)*(s+1)-1);
                        BufferUtils.setInBuffer(normal, (FloatBuffer)downNB.getData(), 0);
                        BufferUtils.setInBuffer(tangent, (FloatBuffer)downTB.getData(), 0);
                    }*/
                } else if (i == s) { // right
                    
                    // handled by right side
                    
                } else { // all in the middle
                    leftPoint.set(i-1, this.getHeightmapHeight(i-1,s), s);
                    rightPoint.set(i+1, this.getHeightmapHeight(i+1,s), s);
                    this.getTex(i-1,s, leftTex);
                    this.getTex(i+1,s, rightTex);
                    averageNormalsTangents(topPoint, rootPoint, leftPoint, bottomPoint, rightPoint, topTex, rootTex, leftTex, bottomTex, rightTex, normal, tangent);
                    VertexBuffer tpNB = this.getMesh().getBuffer(Type.Normal);
                    VertexBuffer tpTB = this.getMesh().getBuffer(Type.Tangent);
                    VertexBuffer downNB = bottom.getMesh().getBuffer(Type.Normal);
                    VertexBuffer downTB = bottom.getMesh().getBuffer(Type.Tangent);
                    BufferUtils.setInBuffer(normal, (FloatBuffer)tpNB.getData(), (s+1)*(s)+i);
                    BufferUtils.setInBuffer(tangent, (FloatBuffer)tpTB.getData(), (s+1)*(s)+i);
                    BufferUtils.setInBuffer(normal, (FloatBuffer)downNB.getData(), i);
                    BufferUtils.setInBuffer(tangent, (FloatBuffer)downTB.getData(), i);
                }
            }
            bottom.getMesh().getBuffer(Type.Normal).setUpdateNeeded();
            bottom.getMesh().getBuffer(Type.Tangent).setUpdateNeeded();
        }

        if (left != null) { // left side
            for (int i=0; i<s+1; i++) {
                rootPoint.set(0, this.getHeightmapHeight(0,i), i);
                leftPoint.set(-1, left.getHeightmapHeight(s-1,i), i);
                rightPoint.set(1, this.getHeightmapHeight(1,i), i);
                this.getTex(0,i, rootTex);
                left.getTex(s-1,i, leftTex);
                this.getTex(1,i, rightTex);
                if (i == 0) { // top
                    if (top == null) {
                        bottomPoint.set(0, this.getHeightmapHeight(0,i+1), i+1);
                        this.getTex(0,i+1, bottomTex);
                        averageNormalsTangents(null, rootPoint, leftPoint, bottomPoint, rightPoint, null, rootTex, leftTex, bottomTex, rightTex, normal, tangent);
                        VertexBuffer tpNB = this.getMesh().getBuffer(Type.Normal);
                        VertexBuffer tpTB = this.getMesh().getBuffer(Type.Tangent);
                        VertexBuffer leftNB = left.getMesh().getBuffer(Type.Normal);
                        VertexBuffer leftTB = left.getMesh().getBuffer(Type.Tangent);
                        BufferUtils.setInBuffer(normal, (FloatBuffer)tpNB.getData(), 0);
                        BufferUtils.setInBuffer(tangent, (FloatBuffer)tpTB.getData(), 0);
                        BufferUtils.setInBuffer(normal, (FloatBuffer)leftNB.getData(), s+1);
                        BufferUtils.setInBuffer(tangent, (FloatBuffer)leftTB.getData(), s+1);
                    } else {
                        topPoint.set(0, top.getHeightmapHeight(0,s-1), i-1);
                        bottomPoint.set(0, this.getHeightmapHeight(0,i+1), i+1);
                        top.getTex(0,i-1, topTex);
                        this.getTex(0,i+1, bottomTex);
                        averageNormalsTangents(topPoint, rootPoint, leftPoint, bottomPoint, rightPoint, topTex, rootTex, leftTex, bottomTex, rightTex, normal, tangent);
                        VertexBuffer tpNB = this.getMesh().getBuffer(Type.Normal);
                        VertexBuffer tpTB = this.getMesh().getBuffer(Type.Tangent);
                        VertexBuffer leftNB = left.getMesh().getBuffer(Type.Normal);
                        VertexBuffer leftTB = left.getMesh().getBuffer(Type.Tangent);
                        VertexBuffer topNB = top.getMesh().getBuffer(Type.Normal);
                        VertexBuffer topTB = top.getMesh().getBuffer(Type.Tangent);
                        BufferUtils.setInBuffer(normal, (FloatBuffer)tpNB.getData(), 0);
                        BufferUtils.setInBuffer(tangent, (FloatBuffer)tpTB.getData(), 0);
                        BufferUtils.setInBuffer(normal, (FloatBuffer)leftNB.getData(), s);
                        BufferUtils.setInBuffer(tangent, (FloatBuffer)leftTB.getData(), s);
                        BufferUtils.setInBuffer(normal, (FloatBuffer)topNB.getData(), (s+1)*s);
                        BufferUtils.setInBuffer(tangent, (FloatBuffer)topTB.getData(), (s+1)*s);
                        /*if (topLeft != null) {
                            VertexBuffer topLeftNB = topLeft.getMesh().getBuffer(Type.Normal);
                            VertexBuffer topLeftTB = topLeft.getMesh().getBuffer(Type.Tangent);
                            BufferUtils.setInBuffer(normal, (FloatBuffer)topLeftNB.getData(), (s+1)*(s+1)-1);
                            BufferUtils.setInBuffer(tangent, (FloatBuffer)topLeftTB.getData(), (s+1)*(s+1)-1);
                            topLeftNB.setUpdateNeeded();
                            topLeftTB.setUpdateNeeded();
                        }*/
                        topNB.setUpdateNeeded();
                    }
                } else if (i == s) { // bottom

                    // handled by bottom

                    /*if (bottomLeft != null) {
                        VertexBuffer bottomLeftNB = bottomLeft.getMesh().getBuffer(Type.Normal);
                        VertexBuffer bottomLeftTB = bottomLeft.getMesh().getBuffer(Type.Tangent);
                        BufferUtils.setInBuffer(normal, (FloatBuffer)bottomLeftNB.getData(), s);
                        BufferUtils.setInBuffer(tangent, (FloatBuffer)bottomLeftTB.getData(), s);
                        bottomLeftNB.setUpdateNeeded();
                        bottomLeftTB.setUpdateNeeded();
                    }*/

                } else { // all in the middle
                    topPoint.set(0, this.getHeightmapHeight(0,i-1), i-1);
                    bottomPoint.set(0, this.getHeightmapHeight(0,i+1), i+1);
                    this.getTex(0,i-1, topTex);
                    this.getTex(0,i+1, bottomTex);
                    averageNormalsTangents(topPoint, rootPoint, leftPoint, bottomPoint, rightPoint, topTex, rootTex, leftTex, bottomTex, rightTex, normal, tangent);
                    VertexBuffer tpNB = this.getMesh().getBuffer(Type.Normal);
                    VertexBuffer tpTB = this.getMesh().getBuffer(Type.Tangent);
                    VertexBuffer leftNB = left.getMesh().getBuffer(Type.Normal);
                    VertexBuffer leftTB = left.getMesh().getBuffer(Type.Tangent);
                    BufferUtils.setInBuffer(normal, (FloatBuffer)tpNB.getData(), (s+1)*(i));
                    BufferUtils.setInBuffer(tangent, (FloatBuffer)tpTB.getData(), (s+1)*(i));
                    BufferUtils.setInBuffer(normal, (FloatBuffer)leftNB.getData(), (s+1)*(i+1)-1);
                    BufferUtils.setInBuffer(tangent, (FloatBuffer)leftTB.getData(), (s+1)*(i+1)-1);
                }
            }
            left.getMesh().getBuffer(Type.Normal).setUpdateNeeded();
            left.getMesh().getBuffer(Type.Tangent).setUpdateNeeded();
        }

        if (top != null) { // top side
            for (int i=0; i<s+1; i++) {
                rootPoint.set(i, this.getHeightmapHeight(i,0), 0);
                topPoint.set(i, top.getHeightmapHeight(i,s-1), -1);
                bottomPoint.set(i, this.getHeightmapHeight(i,1), 1);
                this.getTex(i,s, rootTex);
                top.getTex(i,s-1, topTex);
                this.getTex(i,1, bottomTex);
                if (i == 0) { // left

                    // handled by left side

                } else if (i == s) { // right

                    // handled by right side

                } else { // all in the middle
                    leftPoint.set(i-1, this.getHeightmapHeight(i-1,0), 0);
                    rightPoint.set(i+1, this.getHeightmapHeight(i+1,0), 0);
                    this.getTex(i-1,0, leftTex);
                    this.getTex(i+1,0, rightTex);
                    averageNormalsTangents(topPoint, rootPoint, leftPoint, bottomPoint, rightPoint, topTex, rootTex, leftTex, bottomTex, rightTex, normal, tangent);
                    VertexBuffer tpNB = this.getMesh().getBuffer(Type.Normal);
                    VertexBuffer tpTB = this.getMesh().getBuffer(Type.Tangent);
                    VertexBuffer topNB = top.getMesh().getBuffer(Type.Normal);
                    VertexBuffer topTB = top.getMesh().getBuffer(Type.Tangent);
                    BufferUtils.setInBuffer(normal, (FloatBuffer)tpNB.getData(), i);
                    BufferUtils.setInBuffer(tangent, (FloatBuffer)tpTB.getData(), i);
                    BufferUtils.setInBuffer(normal, (FloatBuffer)topNB.getData(), (s+1)*(s)+i);
                    BufferUtils.setInBuffer(tangent, (FloatBuffer)topTB.getData(), (s+1)*(s)+i);
                }
            }
            top.getMesh().getBuffer(Type.Normal).setUpdateNeeded();
        }

        this.getMesh().getBuffer(Type.Normal).setUpdateNeeded();
        this.getMesh().getBuffer(Type.Tangent).setUpdateNeeded();
    }

    protected void averageNormalsTangents(
            Vector3f topPoint,
            Vector3f rootPoint,
            Vector3f leftPoint, 
            Vector3f bottomPoint, 
            Vector3f rightPoint,
            Vector2f topTex,
            Vector2f rootTex,
            Vector2f leftTex,
            Vector2f bottomTex,
            Vector2f rightTex,
            Vector3f normal,
            Vector3f tangent)
    {
        Vector3f scale = getWorldScale();
        
        Vector3f n1 = Vector3f.ZERO;
        if (topPoint != null && leftPoint != null) {
            n1 = getNormal(topPoint.mult(scale), rootPoint.mult(scale), leftPoint.mult(scale));
        }
        Vector3f n2 = Vector3f.ZERO;
        if (leftPoint != null && bottomPoint != null) {
            n2 = getNormal(leftPoint.mult(scale), rootPoint.mult(scale), bottomPoint.mult(scale));
        }
        Vector3f n3 = Vector3f.ZERO;
        if (rightPoint != null && bottomPoint != null) {
            n3 = getNormal(bottomPoint.mult(scale), rootPoint.mult(scale), rightPoint.mult(scale));
        }
        Vector3f n4 = Vector3f.ZERO;
        if (rightPoint != null && topPoint != null) {
            n4 = getNormal(rightPoint.mult(scale), rootPoint.mult(scale), topPoint.mult(scale));
        }
        
        Vector3f binormal = new Vector3f();
        if (topPoint != null && rightPoint != null)
            LODGeomap.calculateTangent(new Vector3f[]{rootPoint.mult(scale),rightPoint.mult(scale),topPoint.mult(scale)}, new Vector2f[]{rootTex,rightTex,topTex}, tangent, binormal);

        normal.set(n1.add(n2).add(n3).add(n4).normalizeLocal());
    }

    private Vector3f getNormal(Vector3f firstPoint, Vector3f rootPoint, Vector3f secondPoint) {
        Vector3f normal = new Vector3f();
        normal.set(firstPoint).subtractLocal(rootPoint)
                  .crossLocal(secondPoint.subtract(rootPoint)).normalizeLocal();
        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 <b>NOT </b> 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 <b>NOT </b>
     * 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 <b>NOT </b>
     * 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 <b>NOT </b> 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 <b>NOT
     * </b> 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 LodCalculator getLodCalculator() {
        return lodCalculator;
    }

    public void setLodCalculator(LodCalculator lodCalculator) {
        this.lodCalculator = lodCalculator;
    }

    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<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);
                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.getHeightData());
        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();
            }
        }
    }



}
