/* | |
* 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 <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); | |
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<String,UpdatedTerrainPatch> 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<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.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(newNormalBuffer, 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); | |
} | |
private void setInBuffer(Mesh mesh, int index, Vector3f normal, Vector3f tangent, Vector3f binormal) { | |
VertexBuffer NB = mesh.getBuffer(Type.Normal); | |
VertexBuffer TB = mesh.getBuffer(Type.Tangent); | |
VertexBuffer BB = mesh.getBuffer(Type.Binormal); | |
BufferUtils.setInBuffer(normal, (FloatBuffer)NB.getData(), index); | |
BufferUtils.setInBuffer(tangent, (FloatBuffer)TB.getData(), index); | |
BufferUtils.setInBuffer(binormal, (FloatBuffer)BB.getData(), index); | |
NB.setUpdateNeeded(); | |
TB.setUpdateNeeded(); | |
BB.setUpdateNeeded(); | |
} | |
/** | |
* Matches the normals along the edge of the patch with the neighbours. | |
* Computes the normals for the right, bottom, left, and top edges of the | |
* 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<s+1; i++) { | |
rootPoint.set(0, this.getHeightmapHeight(s,i), 0); | |
leftPoint.set(-1, this.getHeightmapHeight(s-1,i), 0); | |
rightPoint.set(1, right.getHeightmapHeight(1,i), 0); | |
if (i == 0) { // top point | |
bottomPoint.set(0, this.getHeightmapHeight(s,i+1), 1); | |
if (top == null) { | |
averageNormalsTangents(null, rootPoint, leftPoint, bottomPoint, rightPoint, normal, tangent, binormal); | |
setInBuffer(this.getMesh(), s, normal, tangent, binormal); | |
setInBuffer(right.getMesh(), 0, normal, tangent, binormal); | |
} else { | |
topPoint.set(0, top.getHeightmapHeight(s,s-1), -1); | |
averageNormalsTangents(topPoint, rootPoint, leftPoint, bottomPoint, rightPoint,normal, tangent, binormal); | |
setInBuffer(this.getMesh(), s, normal, tangent, binormal); | |
setInBuffer(right.getMesh(), 0, normal, tangent, binormal); | |
setInBuffer(top.getMesh(), (s+1)*(s+1)-1, normal, tangent, binormal); | |
if (topRight != null) { | |
// setInBuffer(topRight.getMesh(), (s+1)*s, normal, tangent, binormal); | |
} | |
} | |
} else if (i == s) { // bottom point | |
topPoint.set(0, this.getHeightmapHeight(s,s-1), -1); | |
if (bottom == null) { | |
averageNormalsTangents(topPoint, rootPoint, leftPoint, null, rightPoint, normal, tangent, binormal); | |
setInBuffer(this.getMesh(), (s+1)*(s+1)-1, normal, tangent, binormal); | |
setInBuffer(right.getMesh(), (s+1)*(s), normal, tangent, binormal); | |
} else { | |
bottomPoint.set(0, bottom.getHeightmapHeight(s,1), 1); | |
averageNormalsTangents(topPoint, rootPoint, leftPoint, bottomPoint, rightPoint, normal, tangent, binormal); | |
setInBuffer(this.getMesh(), (s+1)*(s+1)-1, normal, tangent, binormal); | |
setInBuffer(right.getMesh(), (s+1)*s, normal, tangent, binormal); | |
setInBuffer(bottom.getMesh(), s, normal, tangent, binormal); | |
if (bottomRight != null) { | |
// setInBuffer(bottomRight.getMesh(), 0, normal, tangent, binormal); | |
} | |
} | |
} else { // all in the middle | |
topPoint.set(0, this.getHeightmapHeight(s,i-1), -1); | |
bottomPoint.set(0, this.getHeightmapHeight(s,i+1), 1); | |
averageNormalsTangents(topPoint, rootPoint, leftPoint, bottomPoint, rightPoint, normal, tangent, binormal); | |
setInBuffer(this.getMesh(), (s+1)*(i+1)-1, normal, tangent, binormal); | |
setInBuffer(right.getMesh(), (s+1)*(i), normal, tangent, binormal); | |
} | |
} | |
} | |
if (left != null) { // left side, works its way down | |
for (int i=0; i<s+1; i++) { | |
rootPoint.set(0, this.getHeightmapHeight(0,i), 0); | |
leftPoint.set(-1, left.getHeightmapHeight(s-1,i), 0); | |
rightPoint.set(1, this.getHeightmapHeight(1,i), 0); | |
if (i == 0) { // top point | |
bottomPoint.set(0, this.getHeightmapHeight(0,i+1), 1); | |
if (top == null) { | |
averageNormalsTangents(null, rootPoint, leftPoint, bottomPoint, rightPoint, normal, tangent, binormal); | |
setInBuffer(this.getMesh(), 0, normal, tangent, binormal); | |
setInBuffer(left.getMesh(), s, normal, tangent, binormal); | |
} else { | |
topPoint.set(0, top.getHeightmapHeight(0,s-1), -1); | |
averageNormalsTangents(topPoint, rootPoint, leftPoint, bottomPoint, rightPoint, normal, tangent, binormal); | |
setInBuffer(this.getMesh(), 0, normal, tangent, binormal); | |
setInBuffer(left.getMesh(), s, normal, tangent, binormal); | |
setInBuffer(top.getMesh(), (s+1)*s, normal, tangent, binormal); | |
if (topLeft != null) { | |
// setInBuffer(topLeft.getMesh(), (s+1)*(s+1)-1, normal, tangent, binormal); | |
} | |
} | |
} else if (i == s) { // bottom point | |
topPoint.set(0, this.getHeightmapHeight(0,i-1), -1); | |
if (bottom == null) { | |
averageNormalsTangents(topPoint, rootPoint, leftPoint, null, rightPoint, normal, tangent, binormal); | |
setInBuffer(this.getMesh(), (s+1)*(s), normal, tangent, binormal); | |
setInBuffer(left.getMesh(), (s+1)*(s+1)-1, normal, tangent, binormal); | |
} else { | |
bottomPoint.set(0, bottom.getHeightmapHeight(0,1), 1); | |
averageNormalsTangents(topPoint, rootPoint, leftPoint, bottomPoint, rightPoint, normal, tangent, binormal); | |
setInBuffer(this.getMesh(), (s+1)*(s), normal, tangent, binormal); | |
setInBuffer(left.getMesh(), (s+1)*(s+1)-1, normal, tangent, binormal); | |
setInBuffer(bottom.getMesh(), 0, normal, tangent, binormal); | |
if (bottomLeft != null) { | |
// setInBuffer(bottomLeft.getMesh(), s, normal, tangent, binormal); | |
} | |
} | |
} else { // all in the middle | |
topPoint.set(0, this.getHeightmapHeight(0,i-1), -1); | |
bottomPoint.set(0, this.getHeightmapHeight(0,i+1), 1); | |
averageNormalsTangents(topPoint, rootPoint, leftPoint, bottomPoint, rightPoint, normal, tangent, binormal); | |
setInBuffer(this.getMesh(), (s+1)*(i), normal, tangent, binormal); | |
setInBuffer(left.getMesh(), (s+1)*(i+1)-1, normal, tangent, binormal); | |
} | |
} | |
} | |
if (top != null) { // top side, works its way right | |
for (int i=0; i<s+1; i++) { | |
rootPoint.set(0, this.getHeightmapHeight(i,0), 0); | |
topPoint.set(0, top.getHeightmapHeight(i,s-1), -1); | |
bottomPoint.set(0, this.getHeightmapHeight(i,1), 1); | |
if (i == 0) { // left corner | |
// handled by left side pass | |
} else if (i == s) { // right corner | |
// handled by this patch when it does its right side | |
} else { // all in the middle | |
leftPoint.set(-1, this.getHeightmapHeight(i-1,0), 0); | |
rightPoint.set(1, this.getHeightmapHeight(i+1,0), 0); | |
averageNormalsTangents(topPoint, rootPoint, leftPoint, bottomPoint, rightPoint, normal, tangent, binormal); | |
setInBuffer(this.getMesh(), i, normal, tangent, binormal); | |
setInBuffer(top.getMesh(), (s+1)*(s)+i, normal, tangent, binormal); | |
} | |
} | |
} | |
if (bottom != null) { // bottom side, works its way right | |
for (int i=0; i<s+1; i++) { | |
rootPoint.set(0, this.getHeightmapHeight(i,s), 0); | |
topPoint.set(0, this.getHeightmapHeight(i,s-1), -1); | |
bottomPoint.set(0, bottom.getHeightmapHeight(i,1), 1); | |
if (i == 0) { // left | |
// handled by the left side pass | |
} else if (i == s) { // right | |
// handled by the right side pass | |
} else { // all in the middle | |
leftPoint.set(-1, this.getHeightmapHeight(i-1,s), 0); | |
rightPoint.set(1, this.getHeightmapHeight(i+1,s), 0); | |
averageNormalsTangents(topPoint, rootPoint, leftPoint, bottomPoint, rightPoint, normal, tangent, binormal); | |
setInBuffer(this.getMesh(), (s+1)*(s)+i, normal, tangent, binormal); | |
setInBuffer(bottom.getMesh(), i, normal, tangent, binormal); | |
} | |
} | |
} | |
} | |
protected void averageNormalsTangents( | |
Vector3f topPoint, | |
Vector3f rootPoint, | |
Vector3f leftPoint, | |
Vector3f bottomPoint, | |
Vector3f rightPoint, | |
Vector3f normal, | |
Vector3f tangent, | |
Vector3f binormal) | |
{ | |
Vector3f scale = getWorldScale(); | |
Vector3f n1 = new Vector3f(0,0,0); | |
if (topPoint != null && leftPoint != null) { | |
n1.set(calculateNormal(topPoint.mult(scale), rootPoint.mult(scale), leftPoint.mult(scale))); | |
} | |
Vector3f n2 = new Vector3f(0,0,0); | |
if (leftPoint != null && bottomPoint != null) { | |
n2.set(calculateNormal(leftPoint.mult(scale), rootPoint.mult(scale), bottomPoint.mult(scale))); | |
} | |
Vector3f n3 = new Vector3f(0,0,0); | |
if (rightPoint != null && bottomPoint != null) { | |
n3.set(calculateNormal(bottomPoint.mult(scale), rootPoint.mult(scale), rightPoint.mult(scale))); | |
} | |
Vector3f n4 = new Vector3f(0,0,0); | |
if (rightPoint != null && topPoint != null) { | |
n4.set(calculateNormal(rightPoint.mult(scale), rootPoint.mult(scale), topPoint.mult(scale))); | |
} | |
//if (bottomPoint != null && rightPoint != null && rootTex != null && rightTex != null && bottomTex != null) | |
// LODGeomap.calculateTangent(new Vector3f[]{rootPoint.mult(scale),rightPoint.mult(scale),bottomPoint.mult(scale)}, new Vector2f[]{rootTex,rightTex,bottomTex}, tangent, binormal); | |
normal.set(n1.add(n2).add(n3).add(n4).normalize()); | |
tangent.set(normal.cross(new Vector3f(0,0,1)).normalize()); | |
binormal.set(new Vector3f(1,0,0).cross(normal).normalize()); | |
} | |
private Vector3f calculateNormal(Vector3f firstPoint, Vector3f rootPoint, Vector3f secondPoint) { | |
Vector3f normal = new Vector3f(); | |
normal.set(firstPoint).subtractLocal(rootPoint) | |
.crossLocal(secondPoint.subtract(rootPoint)).normalizeLocal(); | |
return normal; | |
} | |
protected Vector3f getMeshNormal(int x, int z) { | |
if (x >= size || z >= size) | |
return null; // out of range | |
int index = (z*size+x)*3; | |
FloatBuffer nb = (FloatBuffer)this.getMesh().getBuffer(Type.Normal).getData(); | |
Vector3f normal = new Vector3f(); | |
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 <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 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.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(); | |
} | |
} | |
} | |
} |