/* * 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.util; import com.jme3.math.ColorRGBA; import com.jme3.math.FastMath; import com.jme3.math.Vector2f; import com.jme3.math.Vector3f; import com.jme3.scene.*; import com.jme3.scene.VertexBuffer.Format; import com.jme3.scene.VertexBuffer.Type; import com.jme3.scene.VertexBuffer.Usage; import com.jme3.scene.mesh.IndexBuffer; import static com.jme3.util.BufferUtils.*; import java.nio.FloatBuffer; import java.nio.IntBuffer; import java.util.ArrayList; import java.util.logging.Level; import java.util.logging.Logger; /** * * @author Lex (Aleksey Nikiforov) */ public class TangentBinormalGenerator { private static final float ZERO_TOLERANCE = 0.0000001f; private static final Logger log = Logger.getLogger( TangentBinormalGenerator.class.getName()); private static float toleranceAngle; private static float toleranceDot; static { setToleranceAngle(45); } private static class VertexInfo { public final Vector3f position; public final Vector3f normal; public final ArrayList indices = new ArrayList(); public VertexInfo(Vector3f position, Vector3f normal) { this.position = position; this.normal = normal; } } /** Collects all the triangle data for one vertex. */ private static class VertexData { public final ArrayList triangles = new ArrayList(); public VertexData() { } } /** Keeps track of tangent, binormal, and normal for one triangle. */ public static class TriangleData { public final Vector3f tangent; public final Vector3f binormal; public final Vector3f normal; public TriangleData(Vector3f tangent, Vector3f binormal, Vector3f normal) { this.tangent = tangent; this.binormal = binormal; this.normal = normal; } } private static VertexData[] initVertexData(int size) { VertexData[] vertices = new VertexData[size]; for (int i = 0; i < size; i++) { vertices[i] = new VertexData(); } return vertices; } public static void generate(Mesh mesh) { generate(mesh, true); } public static void generate(Spatial scene) { if (scene instanceof Node) { Node node = (Node) scene; for (Spatial child : node.getChildren()) { generate(child); } } else { Geometry geom = (Geometry) scene; Mesh mesh = geom.getMesh(); // Check to ensure mesh has texcoords and normals before generating if (mesh.getBuffer(Type.TexCoord) != null && mesh.getBuffer(Type.Normal) != null){ generate(geom.getMesh()); } } } public static void generate(Mesh mesh, boolean approxTangents) { int[] index = new int[3]; Vector3f[] v = new Vector3f[3]; Vector2f[] t = new Vector2f[3]; for (int i = 0; i < 3; i++) { v[i] = new Vector3f(); t[i] = new Vector2f(); } if (mesh.getBuffer(Type.Normal) == null) { throw new IllegalArgumentException("The given mesh has no normal data!"); } VertexData[] vertices; switch (mesh.getMode()) { case Triangles: vertices = processTriangles(mesh, index, v, t); break; case TriangleStrip: vertices = processTriangleStrip(mesh, index, v, t); break; case TriangleFan: vertices = processTriangleFan(mesh, index, v, t); break; default: throw new UnsupportedOperationException( mesh.getMode() + " is not supported."); } processTriangleData(mesh, vertices, approxTangents); //if the mesh has a bind pose, we need to generate the bind pose for the tangent buffer if (mesh.getBuffer(Type.BindPosePosition) != null) { VertexBuffer tangents = mesh.getBuffer(Type.Tangent); if (tangents != null) { VertexBuffer bindTangents = new VertexBuffer(Type.BindPoseTangent); bindTangents.setupData(Usage.CpuOnly, 4, Format.Float, BufferUtils.clone(tangents.getData())); if (mesh.getBuffer(Type.BindPoseTangent) != null) { mesh.clearBuffer(Type.BindPoseTangent); } mesh.setBuffer(bindTangents); tangents.setUsage(Usage.Stream); } } } private static VertexData[] processTriangles(Mesh mesh, int[] index, Vector3f[] v, Vector2f[] t) { IndexBuffer indexBuffer = mesh.getIndexBuffer(); FloatBuffer vertexBuffer = (FloatBuffer) mesh.getBuffer(Type.Position).getData(); if (mesh.getBuffer(Type.TexCoord) == null) { throw new IllegalArgumentException("Can only generate tangents for " + "meshes with texture coordinates"); } FloatBuffer textureBuffer = (FloatBuffer) mesh.getBuffer(Type.TexCoord).getData(); VertexData[] vertices = initVertexData(vertexBuffer.capacity() / 3); for (int i = 0; i < indexBuffer.size() / 3; i++) { for (int j = 0; j < 3; j++) { index[j] = indexBuffer.get(i * 3 + j); populateFromBuffer(v[j], vertexBuffer, index[j]); populateFromBuffer(t[j], textureBuffer, index[j]); } TriangleData triData = processTriangle(index, v, t); if (triData != null) { vertices[index[0]].triangles.add(triData); vertices[index[1]].triangles.add(triData); vertices[index[2]].triangles.add(triData); } } return vertices; } private static VertexData[] processTriangleStrip(Mesh mesh, int[] index, Vector3f[] v, Vector2f[] t) { IndexBuffer indexBuffer = mesh.getIndexBuffer(); FloatBuffer vertexBuffer = (FloatBuffer) mesh.getBuffer(Type.Position).getData(); FloatBuffer textureBuffer = (FloatBuffer) mesh.getBuffer(Type.TexCoord).getData(); VertexData[] vertices = initVertexData(vertexBuffer.capacity() / 3); index[0] = indexBuffer.get(0); index[1] = indexBuffer.get(1); populateFromBuffer(v[0], vertexBuffer, index[0]); populateFromBuffer(v[1], vertexBuffer, index[1]); populateFromBuffer(t[0], textureBuffer, index[0]); populateFromBuffer(t[1], textureBuffer, index[1]); for (int i = 2; i < indexBuffer.size(); i++) { index[2] = indexBuffer.get(i); BufferUtils.populateFromBuffer(v[2], vertexBuffer, index[2]); BufferUtils.populateFromBuffer(t[2], textureBuffer, index[2]); boolean isDegenerate = isDegenerateTriangle(v[0], v[1], v[2]); TriangleData triData = processTriangle(index, v, t); if (triData != null && !isDegenerate) { vertices[index[0]].triangles.add(triData); vertices[index[1]].triangles.add(triData); vertices[index[2]].triangles.add(triData); } Vector3f vTemp = v[0]; v[0] = v[1]; v[1] = v[2]; v[2] = vTemp; Vector2f tTemp = t[0]; t[0] = t[1]; t[1] = t[2]; t[2] = tTemp; index[0] = index[1]; index[1] = index[2]; } return vertices; } private static VertexData[] processTriangleFan(Mesh mesh, int[] index, Vector3f[] v, Vector2f[] t) { IndexBuffer indexBuffer = mesh.getIndexBuffer(); FloatBuffer vertexBuffer = (FloatBuffer) mesh.getBuffer(Type.Position).getData(); FloatBuffer textureBuffer = (FloatBuffer) mesh.getBuffer(Type.TexCoord).getData(); VertexData[] vertices = initVertexData(vertexBuffer.capacity() / 3); index[0] = indexBuffer.get(0); index[1] = indexBuffer.get(1); populateFromBuffer(v[0], vertexBuffer, index[0]); populateFromBuffer(v[1], vertexBuffer, index[1]); populateFromBuffer(t[0], textureBuffer, index[0]); populateFromBuffer(t[1], textureBuffer, index[1]); for (int i = 2; i < vertexBuffer.capacity() / 3; i++) { index[2] = indexBuffer.get(i); populateFromBuffer(v[2], vertexBuffer, index[2]); populateFromBuffer(t[2], textureBuffer, index[2]); TriangleData triData = processTriangle(index, v, t); if (triData != null) { vertices[index[0]].triangles.add(triData); vertices[index[1]].triangles.add(triData); vertices[index[2]].triangles.add(triData); } Vector3f vTemp = v[1]; v[1] = v[2]; v[2] = vTemp; Vector2f tTemp = t[1]; t[1] = t[2]; t[2] = tTemp; index[1] = index[2]; } return vertices; } // check if the area is greater than zero private static boolean isDegenerateTriangle(Vector3f a, Vector3f b, Vector3f c) { return (a.subtract(b).cross(c.subtract(b))).lengthSquared() == 0; } public static TriangleData processTriangle(int[] index, Vector3f[] v, Vector2f[] t) { Vector3f edge1 = new Vector3f(); Vector3f edge2 = new Vector3f(); Vector2f edge1uv = new Vector2f(); Vector2f edge2uv = new Vector2f(); Vector3f tangent = new Vector3f(); Vector3f binormal = new Vector3f(); Vector3f normal = new Vector3f(); t[1].subtract(t[0], edge1uv); t[2].subtract(t[0], edge2uv); float det = edge1uv.x * edge2uv.y - edge1uv.y * edge2uv.x; boolean normalize = false; if (Math.abs(det) < ZERO_TOLERANCE) { log.log(Level.WARNING, "Colinear uv coordinates for triangle " + "[{0}, {1}, {2}]; tex0 = [{3}, {4}], " + "tex1 = [{5}, {6}], tex2 = [{7}, {8}]", new Object[]{index[0], index[1], index[2], t[0].x, t[0].y, t[1].x, t[1].y, t[2].x, t[2].y}); det = 1; normalize = true; } v[1].subtract(v[0], edge1); v[2].subtract(v[0], edge2); tangent.set(edge1); tangent.normalizeLocal(); binormal.set(edge2); binormal.normalizeLocal(); if (Math.abs(Math.abs(tangent.dot(binormal)) - 1) < ZERO_TOLERANCE) { log.log(Level.WARNING, "Vertices are on the same line " + "for triangle [{0}, {1}, {2}].", new Object[]{index[0], index[1], index[2]}); } float factor = 1 / det; tangent.x = (edge2uv.y * edge1.x - edge1uv.y * edge2.x) * factor; tangent.y = (edge2uv.y * edge1.y - edge1uv.y * edge2.y) * factor; tangent.z = (edge2uv.y * edge1.z - edge1uv.y * edge2.z) * factor; if (normalize) { tangent.normalizeLocal(); } binormal.x = (edge1uv.x * edge2.x - edge2uv.x * edge1.x) * factor; binormal.y = (edge1uv.x * edge2.y - edge2uv.x * edge1.y) * factor; binormal.z = (edge1uv.x * edge2.z - edge2uv.x * edge1.z) * factor; if (normalize) { binormal.normalizeLocal(); } tangent.cross(binormal, normal); normal.normalizeLocal(); return new TriangleData( tangent, binormal, normal); } public static void setToleranceAngle(float angle) { if (angle < 0 || angle > 179) { throw new IllegalArgumentException( "The angle must be between 0 and 179 degrees."); } toleranceDot = FastMath.cos(angle * FastMath.DEG_TO_RAD); toleranceAngle = angle; } private static boolean approxEqual(Vector3f u, Vector3f v) { float tolerance = 1E-4f; return (FastMath.abs(u.x - v.x) < tolerance) && (FastMath.abs(u.y - v.y) < tolerance) && (FastMath.abs(u.z - v.z) < tolerance); } private static ArrayList linkVertices(Mesh mesh) { ArrayList vertexMap = new ArrayList(); FloatBuffer vertexBuffer = (FloatBuffer) mesh.getBuffer(Type.Position).getData(); FloatBuffer normalBuffer = (FloatBuffer) mesh.getBuffer(Type.Normal).getData(); Vector3f position = new Vector3f(); Vector3f normal = new Vector3f(); final int size = vertexBuffer.capacity() / 3; for (int i = 0; i < size; i++) { populateFromBuffer(position, vertexBuffer, i); populateFromBuffer(normal, normalBuffer, i); boolean found = false; for (int j = 0; j < vertexMap.size(); j++) { VertexInfo vertexInfo = vertexMap.get(j); if (approxEqual(vertexInfo.position, position) && approxEqual(vertexInfo.normal, normal)) { vertexInfo.indices.add(i); found = true; break; } } if (!found) { VertexInfo vertexInfo = new VertexInfo(position.clone(), normal.clone()); vertexInfo.indices.add(i); vertexMap.add(vertexInfo); } } return vertexMap; } private static void processTriangleData(Mesh mesh, VertexData[] vertices, boolean approxTangent) { ArrayList vertexMap = linkVertices(mesh); FloatBuffer normalBuffer = (FloatBuffer) mesh.getBuffer(Type.Normal).getData(); FloatBuffer tangents = BufferUtils.createFloatBuffer(vertices.length * 4); // FloatBuffer binormals = BufferUtils.createFloatBuffer(vertices.length * 3); Vector3f tangent = new Vector3f(); Vector3f binormal = new Vector3f(); Vector3f normal = new Vector3f(); Vector3f givenNormal = new Vector3f(); Vector3f tangentUnit = new Vector3f(); Vector3f binormalUnit = new Vector3f(); for (int k = 0; k < vertexMap.size(); k++) { float wCoord = -1; VertexInfo vertexInfo = vertexMap.get(k); givenNormal.set(vertexInfo.normal); givenNormal.normalizeLocal(); TriangleData firstTriangle = vertices[vertexInfo.indices.get(0)].triangles.get(0); // check tangent and binormal consistency tangent.set(firstTriangle.tangent); tangent.normalizeLocal(); binormal.set(firstTriangle.binormal); binormal.normalizeLocal(); for (int i : vertexInfo.indices) { ArrayList triangles = vertices[i].triangles; for (int j = 0; j < triangles.size(); j++) { TriangleData triangleData = triangles.get(j); tangentUnit.set(triangleData.tangent); tangentUnit.normalizeLocal(); if (tangent.dot(tangentUnit) < toleranceDot) { log.log(Level.WARNING, "Angle between tangents exceeds tolerance " + "for vertex {0}.", i); break; } if (!approxTangent) { binormalUnit.set(triangleData.binormal); binormalUnit.normalizeLocal(); if (binormal.dot(binormalUnit) < toleranceDot) { log.log(Level.WARNING, "Angle between binormals exceeds tolerance " + "for vertex {0}.", i); break; } } } } // find average tangent tangent.set(0, 0, 0); binormal.set(0, 0, 0); int triangleCount = 0; for (int i : vertexInfo.indices) { ArrayList triangles = vertices[i].triangles; triangleCount += triangles.size(); boolean flippedNormal = false; for (int j = 0; j < triangles.size(); j++) { TriangleData triangleData = triangles.get(j); tangent.addLocal(triangleData.tangent); binormal.addLocal(triangleData.binormal); if (givenNormal.dot(triangleData.normal) < 0) { flippedNormal = true; } } if (flippedNormal /*&& approxTangent*/) { // Generated normal is flipped for this vertex, // so binormal = normal.cross(tangent) will be flipped in the shader // log.log(Level.WARNING, // "Binormal is flipped for vertex {0}.", i); wCoord = 1; } } int blameVertex = vertexInfo.indices.get(0); if (tangent.length() < ZERO_TOLERANCE) { log.log(Level.WARNING, "Shared tangent is zero for vertex {0}.", blameVertex); // attempt to fix from binormal if (binormal.length() >= ZERO_TOLERANCE) { binormal.cross(givenNormal, tangent); tangent.normalizeLocal(); } // if all fails use the tangent from the first triangle else { tangent.set(firstTriangle.tangent); } } else { tangent.divideLocal(triangleCount); } tangentUnit.set(tangent); tangentUnit.normalizeLocal(); if (Math.abs(Math.abs(tangentUnit.dot(givenNormal)) - 1) < ZERO_TOLERANCE) { log.log(Level.WARNING, "Normal and tangent are parallel for vertex {0}.", blameVertex); } if (!approxTangent) { if (binormal.length() < ZERO_TOLERANCE) { log.log(Level.WARNING, "Shared binormal is zero for vertex {0}.", blameVertex); // attempt to fix from tangent if (tangent.length() >= ZERO_TOLERANCE) { givenNormal.cross(tangent, binormal); binormal.normalizeLocal(); } // if all fails use the binormal from the first triangle else { binormal.set(firstTriangle.binormal); } } else { binormal.divideLocal(triangleCount); } binormalUnit.set(binormal); binormalUnit.normalizeLocal(); if (Math.abs(Math.abs(binormalUnit.dot(givenNormal)) - 1) < ZERO_TOLERANCE) { log.log(Level.WARNING, "Normal and binormal are parallel for vertex {0}.", blameVertex); } if (Math.abs(Math.abs(binormalUnit.dot(tangentUnit)) - 1) < ZERO_TOLERANCE) { log.log(Level.WARNING, "Tangent and binormal are parallel for vertex {0}.", blameVertex); } } for (int i : vertexInfo.indices) { if (approxTangent) { // This calculation ensures that normal and tagent have a 90 degree angle. // Removing this will lead to visual artifacts. givenNormal.cross(tangent, binormal); binormal.cross(givenNormal, tangent); tangent.normalizeLocal(); tangents.put((i * 4), tangent.x); tangents.put((i * 4) + 1, tangent.y); tangents.put((i * 4) + 2, tangent.z); tangents.put((i * 4) + 3, wCoord); } else { tangents.put((i * 4), tangent.x); tangents.put((i * 4) + 1, tangent.y); tangents.put((i * 4) + 2, tangent.z); tangents.put((i * 4) + 3, wCoord); //setInBuffer(binormal, binormals, i); } } } mesh.setBuffer(Type.Tangent, 4, tangents); // if (!approxTangent) mesh.setBuffer(Type.Binormal, 3, binormals); } public static Mesh genTbnLines(Mesh mesh, float scale) { if (mesh.getBuffer(Type.Tangent) == null) { return genNormalLines(mesh, scale); } else { return genTangentLines(mesh, scale); } } public static Mesh genNormalLines(Mesh mesh, float scale) { FloatBuffer vertexBuffer = (FloatBuffer) mesh.getBuffer(Type.Position).getData(); FloatBuffer normalBuffer = (FloatBuffer) mesh.getBuffer(Type.Normal).getData(); ColorRGBA originColor = ColorRGBA.White; ColorRGBA normalColor = ColorRGBA.Blue; Mesh lineMesh = new Mesh(); lineMesh.setMode(Mesh.Mode.Lines); Vector3f origin = new Vector3f(); Vector3f point = new Vector3f(); FloatBuffer lineVertex = BufferUtils.createFloatBuffer(vertexBuffer.capacity() * 2); FloatBuffer lineColor = BufferUtils.createFloatBuffer(vertexBuffer.capacity() / 3 * 4 * 2); for (int i = 0; i < vertexBuffer.capacity() / 3; i++) { populateFromBuffer(origin, vertexBuffer, i); populateFromBuffer(point, normalBuffer, i); int index = i * 2; setInBuffer(origin, lineVertex, index); setInBuffer(originColor, lineColor, index); point.multLocal(scale); point.addLocal(origin); setInBuffer(point, lineVertex, index + 1); setInBuffer(normalColor, lineColor, index + 1); } lineMesh.setBuffer(Type.Position, 3, lineVertex); lineMesh.setBuffer(Type.Color, 4, lineColor); lineMesh.setStatic(); //lineMesh.setInterleaved(); return lineMesh; } private static Mesh genTangentLines(Mesh mesh, float scale) { FloatBuffer vertexBuffer = (FloatBuffer) mesh.getBuffer(Type.Position).getData(); FloatBuffer normalBuffer = (FloatBuffer) mesh.getBuffer(Type.Normal).getData(); FloatBuffer tangentBuffer = (FloatBuffer) mesh.getBuffer(Type.Tangent).getData(); FloatBuffer binormalBuffer = null; if (mesh.getBuffer(Type.Binormal) != null) { binormalBuffer = (FloatBuffer) mesh.getBuffer(Type.Binormal).getData(); } ColorRGBA originColor = ColorRGBA.White; ColorRGBA tangentColor = ColorRGBA.Red; ColorRGBA binormalColor = ColorRGBA.Green; ColorRGBA normalColor = ColorRGBA.Blue; Mesh lineMesh = new Mesh(); lineMesh.setMode(Mesh.Mode.Lines); Vector3f origin = new Vector3f(); Vector3f point = new Vector3f(); Vector3f tangent = new Vector3f(); Vector3f normal = new Vector3f(); IntBuffer lineIndex = BufferUtils.createIntBuffer(vertexBuffer.capacity() / 3 * 6); FloatBuffer lineVertex = BufferUtils.createFloatBuffer(vertexBuffer.capacity() * 4); FloatBuffer lineColor = BufferUtils.createFloatBuffer(vertexBuffer.capacity() / 3 * 4 * 4); boolean hasParity = mesh.getBuffer(Type.Tangent).getNumComponents() == 4; float tangentW = 1; for (int i = 0; i < vertexBuffer.capacity() / 3; i++) { populateFromBuffer(origin, vertexBuffer, i); populateFromBuffer(normal, normalBuffer, i); if (hasParity) { tangent.x = tangentBuffer.get(i * 4); tangent.y = tangentBuffer.get(i * 4 + 1); tangent.z = tangentBuffer.get(i * 4 + 2); tangentW = tangentBuffer.get(i * 4 + 3); } else { populateFromBuffer(tangent, tangentBuffer, i); } int index = i * 4; int id = i * 6; lineIndex.put(id, index); lineIndex.put(id + 1, index + 1); lineIndex.put(id + 2, index); lineIndex.put(id + 3, index + 2); lineIndex.put(id + 4, index); lineIndex.put(id + 5, index + 3); setInBuffer(origin, lineVertex, index); setInBuffer(originColor, lineColor, index); point.set(tangent); point.multLocal(scale); point.addLocal(origin); setInBuffer(point, lineVertex, index + 1); setInBuffer(tangentColor, lineColor, index + 1); // wvBinormal = cross(wvNormal, wvTangent) * -inTangent.w if (binormalBuffer == null) { normal.cross(tangent, point); point.multLocal(-tangentW); point.normalizeLocal(); } else { populateFromBuffer(point, binormalBuffer, i); } point.multLocal(scale); point.addLocal(origin); setInBuffer(point, lineVertex, index + 2); setInBuffer(binormalColor, lineColor, index + 2); point.set(normal); point.multLocal(scale); point.addLocal(origin); setInBuffer(point, lineVertex, index + 3); setInBuffer(normalColor, lineColor, index + 3); } lineMesh.setBuffer(Type.Index, 1, lineIndex); lineMesh.setBuffer(Type.Position, 3, lineVertex); lineMesh.setBuffer(Type.Color, 4, lineColor); lineMesh.setStatic(); //lineMesh.setInterleaved(); return lineMesh; } }