diff --git a/engine/src/terrain/com/jme3/terrain/Terrain.java b/engine/src/terrain/com/jme3/terrain/Terrain.java index 9c6cd76f8..78a20ab83 100644 --- a/engine/src/terrain/com/jme3/terrain/Terrain.java +++ b/engine/src/terrain/com/jme3/terrain/Terrain.java @@ -54,6 +54,15 @@ public interface Terrain { * @return the height at the given point */ public float getHeight(Vector2f xz); + + /** + * Get the normal vector for the surface of the terrain at the specified + * X-Z coordinate. This normal vector can be a close approximation. It does not + * take into account any normal maps on the material. + * @param xz the X-Z world coordinate + * @return the normal vector at the given point + */ + public Vector3f getNormal(Vector2f xz); /** * Get the heightmap height at the specified X-Z coordinate. This does not diff --git a/engine/src/terrain/com/jme3/terrain/geomipmap/TerrainPatch.java b/engine/src/terrain/com/jme3/terrain/geomipmap/TerrainPatch.java index 6024111c5..fab2efa2a 100644 --- a/engine/src/terrain/com/jme3/terrain/geomipmap/TerrainPatch.java +++ b/engine/src/terrain/com/jme3/terrain/geomipmap/TerrainPatch.java @@ -691,19 +691,19 @@ public class TerrainPatch extends Geometry { Vector3f n1 = Vector3f.ZERO; if (topPoint != null && leftPoint != null) { - n1 = getNormal(topPoint.mult(scale), rootPoint.mult(scale), leftPoint.mult(scale)); + n1 = calculateNormal(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)); + n2 = calculateNormal(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)); + n3 = calculateNormal(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)); + n4 = calculateNormal(rightPoint.mult(scale), rootPoint.mult(scale), topPoint.mult(scale)); } Vector3f binormal = new Vector3f(); @@ -713,12 +713,25 @@ public class TerrainPatch extends Geometry { normal.set(n1.add(n2).add(n3).add(n4).normalizeLocal()); } - private Vector3f getNormal(Vector3f firstPoint, Vector3f rootPoint, Vector3f secondPoint) { + 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. diff --git a/engine/src/terrain/com/jme3/terrain/geomipmap/TerrainQuad.java b/engine/src/terrain/com/jme3/terrain/geomipmap/TerrainQuad.java index f084e0ee1..5ca0b39fa 100644 --- a/engine/src/terrain/com/jme3/terrain/geomipmap/TerrainQuad.java +++ b/engine/src/terrain/com/jme3/terrain/geomipmap/TerrainQuad.java @@ -959,12 +959,56 @@ public class TerrainQuad extends Node implements Terrain { return Float.NaN; } + protected Vector3f getMeshNormal(int x, int z) { + int quad = findQuadrant(x, z); + int split = (size + 1) >> 1; + if (children != null) { + for (int i = children.size(); --i >= 0;) { + Spatial spat = children.get(i); + int col = x; + int row = z; + boolean match = false; + + // get the childs quadrant + int childQuadrant = 0; + if (spat instanceof TerrainQuad) { + childQuadrant = ((TerrainQuad) spat).getQuadrant(); + } else if (spat instanceof TerrainPatch) { + childQuadrant = ((TerrainPatch) spat).getQuadrant(); + } + + if (childQuadrant == 1 && (quad & 1) != 0) { + match = true; + } else if (childQuadrant == 2 && (quad & 2) != 0) { + row = z - split + 1; + match = true; + } else if (childQuadrant == 3 && (quad & 4) != 0) { + col = x - split + 1; + match = true; + } else if (childQuadrant == 4 && (quad & 8) != 0) { + col = x - split + 1; + row = z - split + 1; + match = true; + } + + if (match) { + if (spat instanceof TerrainQuad) { + return ((TerrainQuad) spat).getMeshNormal(col, row); + } else if (spat instanceof TerrainPatch) { + return ((TerrainPatch) spat).getMeshNormal(col, row); + } + } + + } + } + return null; + } public float getHeight(Vector2f xz) { // offset float x = (float)(((xz.x - getLocalTranslation().x) / getLocalScale().x) + (float)totalSize / 2f); float z = (float)(((xz.y - getLocalTranslation().z) / getLocalScale().z) + (float)totalSize / 2f); - float height = getHeight(x, z, xz); + float height = getHeight(x, z); height *= getLocalScale().y; return height; } @@ -974,7 +1018,7 @@ public class TerrainQuad extends Node implements Terrain { * @param x coordinate translated into actual (positive) terrain grid coordinates * @param y coordinate translated into actual (positive) terrain grid coordinates */ - protected float getHeight(float x, float z, Vector2f xz) { + protected float getHeight(float x, float z) { x-=0.5f; z-=0.5f; float col = FastMath.floor(x); @@ -1000,6 +1044,38 @@ public class TerrainQuad extends Node implements Terrain { } } + public Vector3f getNormal(Vector2f xz) { + // offset + float x = (float)(((xz.x - getLocalTranslation().x) / getLocalScale().x) + (float)totalSize / 2f); + float z = (float)(((xz.y - getLocalTranslation().z) / getLocalScale().z) + (float)totalSize / 2f); + Vector3f normal = getNormal(x, z, xz); + + return normal; + } + + protected Vector3f getNormal(float x, float z, Vector2f xz) { + x-=0.5f; + z-=0.5f; + float col = FastMath.floor(x); + float row = FastMath.floor(z); + boolean onX = false; + if(1 - (x - col)-(z - row) < 0) // what triangle to interpolate on + onX = true; + // v1--v2 ^ + // | / | | + // | / | | + // v3--v4 | Z + // | + // <-------Y + // X + Vector3f n1 = getMeshNormal((int) FastMath.ceil(x), (int) FastMath.ceil(z)); + Vector3f n2 = getMeshNormal((int) FastMath.floor(x), (int) FastMath.ceil(z)); + Vector3f n3 = getMeshNormal((int) FastMath.ceil(x), (int) FastMath.floor(z)); + Vector3f n4 = getMeshNormal((int) FastMath.floor(x), (int) FastMath.floor(z)); + + return n1.add(n2).add(n3).add(n4).normalize(); + } + public void setHeight(Vector2f xz, float height) { List coord = new ArrayList(); coord.add(xz); diff --git a/engine/src/test/jme3test/terrain/TerrainTestModifyHeight.java b/engine/src/test/jme3test/terrain/TerrainTestModifyHeight.java index d51a3ceb3..1fe72532b 100644 --- a/engine/src/test/jme3test/terrain/TerrainTestModifyHeight.java +++ b/engine/src/test/jme3test/terrain/TerrainTestModifyHeight.java @@ -32,12 +32,8 @@ package jme3test.terrain; import com.jme3.app.SimpleApplication; -import com.jme3.bounding.BoundingBox; import com.jme3.collision.CollisionResult; import com.jme3.collision.CollisionResults; -import com.jme3.export.Savable; -import com.jme3.export.binary.BinaryExporter; -import com.jme3.export.binary.BinaryImporter; import com.jme3.font.BitmapText; import com.jme3.input.KeyInput; import com.jme3.input.MouseInput; @@ -52,9 +48,8 @@ import com.jme3.math.ColorRGBA; import com.jme3.math.Ray; import com.jme3.math.Vector2f; import com.jme3.math.Vector3f; -import com.jme3.renderer.queue.RenderQueue.Bucket; import com.jme3.scene.Geometry; -import com.jme3.scene.Node; +import com.jme3.scene.debug.Arrow; import com.jme3.scene.shape.Sphere; import com.jme3.terrain.geomipmap.TerrainGrid; import com.jme3.terrain.geomipmap.TerrainLodControl; @@ -64,16 +59,8 @@ import com.jme3.terrain.heightmap.FractalHeightMapGrid; import com.jme3.terrain.heightmap.ImageBasedHeightMap; import com.jme3.texture.Texture; import com.jme3.texture.Texture.WrapMode; -import java.io.BufferedInputStream; -import java.io.BufferedOutputStream; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.IOException; import java.util.ArrayList; import java.util.List; -import java.util.logging.Level; -import java.util.logging.Logger; import jme3tools.converters.ImageToAwt; import org.novyon.noise.ShaderUtils; import org.novyon.noise.basis.FilteredBasis; @@ -106,6 +93,7 @@ public class TerrainTestModifyHeight extends SimpleApplication { private boolean lowerTerrain = false; private Geometry marker; + private Geometry markerNormal; public static void main(String[] args) { TerrainTestModifyHeight app = new TerrainTestModifyHeight(); @@ -132,6 +120,10 @@ public class TerrainTestModifyHeight extends SimpleApplication { float h = terrain.getHeight(new Vector2f(intersection.x, intersection.z)); Vector3f tl = terrain.getWorldTranslation(); marker.setLocalTranslation(tl.add(new Vector3f(intersection.x, h, intersection.z)) ); + markerNormal.setLocalTranslation(tl.add(new Vector3f(intersection.x, h, intersection.z)) ); + + Vector3f normal = terrain.getNormal(new Vector2f(intersection.x, intersection.z)); + ((Arrow)markerNormal.getMesh()).setArrowExtent(normal); } } @@ -148,8 +140,8 @@ public class TerrainTestModifyHeight extends SimpleApplication { matWire.getAdditionalRenderState().setWireframe(true); matWire.setColor("Color", ColorRGBA.Green); - //createTerrain(); - createTerrainGrid(); + createTerrain(); + //createTerrainGrid(); DirectionalLight light = new DirectionalLight(); light.setDirection((new Vector3f(-0.5f, -1f, -0.5f)).normalize()); @@ -422,6 +414,7 @@ public class TerrainTestModifyHeight extends SimpleApplication { } private void createMarker() { + // collision marker Sphere sphere = new Sphere(8, 8, 0.5f); marker = new Geometry("Marker"); marker.setMesh(sphere); @@ -433,5 +426,12 @@ public class TerrainTestModifyHeight extends SimpleApplication { marker.setMaterial(mat); rootNode.attachChild(marker); + + // surface normal marker + Arrow arrow = new Arrow(new Vector3f(0,1,0)); + markerNormal = new Geometry("MarkerNormal"); + markerNormal.setMesh(arrow); + markerNormal.setMaterial(mat); + rootNode.attachChild(markerNormal); } }