Sfoglia il codice sorgente

- TangentBinormalGenerator now splits vertices with mirrored uvs. This is an optional operation.
It fixes the issue in this thread : http://hub.jmonkeyengine.org/forum/topic/normalmapslighting-issue/
Next step is to split vertices that have triangles with heavily rotated tangent spaces.

git-svn-id: https://jmonkeyengine.googlecode.com/svn/trunk@10698 75d07b2b-3a1a-0410-a2c5-0572b91ccdca

rem..om 12 anni fa
parent
commit
a1533a407b
1 ha cambiato i file con 315 aggiunte e 88 eliminazioni
  1. 315 88
      engine/src/core/com/jme3/util/TangentBinormalGenerator.java

+ 315 - 88
engine/src/core/com/jme3/util/TangentBinormalGenerator.java

@@ -35,16 +35,22 @@ import com.jme3.math.ColorRGBA;
 import com.jme3.math.FastMath;
 import com.jme3.math.Vector2f;
 import com.jme3.math.Vector3f;
-import com.jme3.math.Vector4f;
 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.Buffer;
+import java.nio.ByteBuffer;
+import java.nio.DoubleBuffer;
 import java.nio.FloatBuffer;
 import java.nio.IntBuffer;
+import java.nio.ShortBuffer;
 import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
 import java.util.logging.Level;
 import java.util.logging.Logger;
 
@@ -57,13 +63,13 @@ 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;
+    public static boolean debug = false;
     
     static {
         setToleranceAngle(45);
     }
-    
+
     
     private static class VertexInfo {
         public final Vector3f position;
@@ -91,32 +97,39 @@ public class TangentBinormalGenerator {
     public static class TriangleData {
         public final Vector3f tangent;
         public final Vector3f binormal;
-        public final Vector3f normal;
+        public final Vector3f normal;        
+        public int[] index = new int[3];
+        public int triangleOffset;
         
         public TriangleData(Vector3f tangent, Vector3f binormal, Vector3f normal) {
             this.tangent = tangent;
             this.binormal = binormal;
             this.normal = normal;
         }
+        public void setIndex(int[] index) {
+            for (int i = 0; i < index.length; i++) {
+                this.index[i] = index[i];
+            }
+        }
     }
     
-    private static VertexData[] initVertexData(int size) {
-        VertexData[] vertices = new VertexData[size];
+    private static List<VertexData> initVertexData(int size) {
+        List<VertexData> vertices = new ArrayList<VertexData>(size);        
         for (int i = 0; i < size; i++) {
-            vertices[i] = new VertexData();
+            vertices.add(new VertexData());
         }
         return vertices;
     }
     
     public static void generate(Mesh mesh) {
-        generate(mesh, true);
+        generate(mesh, true, false);
     }
     
-    public static void generate(Spatial scene) {
+    public static void generate(Spatial scene, boolean splitMirrored) {
         if (scene instanceof Node) {
             Node node = (Node) scene;
             for (Spatial child : node.getChildren()) {
-                generate(child);
+                generate(child, splitMirrored);
             }
         } else {
             Geometry geom = (Geometry) scene;
@@ -125,12 +138,16 @@ public class TangentBinormalGenerator {
             // Check to ensure mesh has texcoords and normals before generating
             if (mesh.getBuffer(Type.TexCoord) != null 
              && mesh.getBuffer(Type.Normal) != null){
-                generate(geom.getMesh());
+                generate(geom.getMesh(),true, splitMirrored);
             }
         }
     }
     
-    public static void generate(Mesh mesh, boolean approxTangents) {
+    public static void generate(Spatial scene) {
+        generate(scene, false);
+    }
+    
+    public static void generate(Mesh mesh, boolean approxTangents, boolean splitMirrored) {        
         int[] index = new int[3];
         Vector3f[] v = new Vector3f[3];
         Vector2f[] t = new Vector2f[3];
@@ -143,10 +160,13 @@ public class TangentBinormalGenerator {
             throw new IllegalArgumentException("The given mesh has no normal data!");
         }
         
-        VertexData[] vertices;
+         List<VertexData> vertices;
         switch (mesh.getMode()) {
             case Triangles:
-                vertices = processTriangles(mesh, index, v, t);
+                vertices = processTriangles(mesh, index, v, t, splitMirrored);
+                if(splitMirrored){
+                    splitVertices(mesh, vertices, splitMirrored);
+                }
                 break;
             case TriangleStrip:
                 vertices = processTriangleStrip(mesh, index, v, t);
@@ -181,8 +201,8 @@ public class TangentBinormalGenerator {
         }
     }
     
-    private static VertexData[] processTriangles(Mesh mesh,
-            int[] index, Vector3f[] v, Vector2f[] t) {
+    private static  List<VertexData> processTriangles(Mesh mesh,
+            int[] index, Vector3f[] v, Vector2f[] t, boolean splitMirrored) {
         IndexBuffer indexBuffer = mesh.getIndexBuffer();
         FloatBuffer vertexBuffer = (FloatBuffer) mesh.getBuffer(Type.Position).getData();
         if (mesh.getBuffer(Type.TexCoord) == null) {
@@ -192,7 +212,7 @@ public class TangentBinormalGenerator {
         
         FloatBuffer textureBuffer = (FloatBuffer) mesh.getBuffer(Type.TexCoord).getData();
         
-        VertexData[] vertices = initVertexData(vertexBuffer.limit() / 3);
+        List<VertexData> vertices = initVertexData(vertexBuffer.limit() / 3);
         
         for (int i = 0; i < indexBuffer.size() / 3; i++) {
             for (int j = 0; j < 3; j++) {
@@ -202,23 +222,201 @@ public class TangentBinormalGenerator {
             }
             
             TriangleData triData = processTriangle(index, v, t);
+            if(splitMirrored){
+                triData.setIndex(index);
+                triData.triangleOffset = i * 3 ;
+            }
             if (triData != null) {
-                vertices[index[0]].triangles.add(triData);
-                vertices[index[1]].triangles.add(triData);
-                vertices[index[2]].triangles.add(triData);
+                vertices.get(index[0]).triangles.add(triData);
+                vertices.get(index[1]).triangles.add(triData);
+                vertices.get(index[2]).triangles.add(triData);
             }
         }
         
         return vertices;
     }
     
-    private static VertexData[] processTriangleStrip(Mesh mesh,
+    //Don't remove splitmirorred boolean,It's not used right now, but i intend to
+    //make this method also split vertice with rotated tangent space and I'll
+    //add another splitRotated boolean 
+    private static List<VertexData> splitVertices(Mesh mesh, List<VertexData> vertexData, boolean splitMirorred) {
+        int nbVertices = mesh.getBuffer(Type.Position).getNumElements();
+        List<VertexData> newVertices = new ArrayList<VertexData>();
+        Map<Integer, Integer> indiceMap = new HashMap<Integer, Integer>();
+        FloatBuffer normalBuffer = mesh.getFloatBuffer(Type.Normal);
+
+        for (int i = 0; i < vertexData.size(); i++) {
+            ArrayList<TriangleData> triangles = vertexData.get(i).triangles;
+            Vector3f givenNormal = new Vector3f();
+            populateFromBuffer(givenNormal, normalBuffer, i);
+          
+            ArrayList<TriangleData> trianglesUp = new ArrayList<TriangleData>();
+            ArrayList<TriangleData> trianglesDown = new ArrayList<TriangleData>();  
+            for (int j = 0; j < triangles.size(); j++) {
+                TriangleData triangleData = triangles.get(j);
+                if(parity(givenNormal, triangleData.normal) > 0){
+                    trianglesUp.add(triangleData);
+                }else{
+                    trianglesDown.add(triangleData);
+                }
+            }
+            
+            //if the vertex has triangles with opposite parity it has to be split
+            if(!trianglesUp.isEmpty() && !trianglesDown.isEmpty()){
+                log.log(Level.FINE, "Splitting vertex {0}", i);
+                //assigning triangle with the same parity to the original vertex
+                vertexData.get(i).triangles.clear();
+                vertexData.get(i).triangles.addAll(trianglesUp);
+                
+                //creating a new vertex
+                VertexData newVert = new VertexData();
+                //assigning triangles with opposite parity to it
+                newVert.triangles.addAll(trianglesDown);
+                
+                newVertices.add(newVert);
+                //keep vertex index to fix the index buffers later
+                indiceMap.put(nbVertices, i);
+                for (TriangleData tri : newVert.triangles) {
+                    for (int j = 0; j < tri.index.length; j++) {
+                        if(tri.index[j] == i){
+                            tri.index[j] = nbVertices;                            
+                        }
+                    }
+                }
+                nbVertices++;
+                
+            }            
+
+
+        }
+
+        if(!newVertices.isEmpty()){
+            
+            //we have new vertices, we need to update the mesh's buffers.
+            for (Type type : VertexBuffer.Type.values()) {
+                //skip tangent buffer as we're gonna overwrite it later
+                if(type == Type.Tangent || type == Type.BindPoseTangent) continue;
+                VertexBuffer vb = mesh.getBuffer(type);
+                //Some buffer (hardware skinning ones) can be there but not 
+                //initialized, they must be skipped. 
+                //They'll be initialized when Hardware Skinning is engaged
+                if(vb==null || vb.getNumComponents() == 0) continue;
+                
+                Buffer buffer = vb.getData();   
+                //IndexBuffer has special treatement, only swapping the vertex indices is needed                
+                if(type == Type.Index){
+                    boolean isShortBuffer = vb.getFormat() == VertexBuffer.Format.UnsignedShort;                     
+                    for (VertexData vertex : newVertices) {
+                        for (TriangleData tri : vertex.triangles) {
+                            for (int i = 0; i < tri.index.length; i++) {
+                                if (isShortBuffer) {
+                                    ((ShortBuffer) buffer).put(tri.triangleOffset + i, (short) tri.index[i]);
+                                } else {
+                                    ((IntBuffer) buffer).put(tri.triangleOffset + i, tri.index[i]);
+                                }
+                            }
+                        }
+                    }
+                    vb.setUpdateNeeded();
+                }else{
+                    //copy the buffer in a bigger one and append nex vertices to the end
+                    Buffer newVerts = VertexBuffer.createBuffer(vb.getFormat(), vb.getNumComponents(), nbVertices);                   
+                    if (buffer != null) {
+                        buffer.rewind();
+                        bulkPut(vb.getFormat(), newVerts,buffer);  
+                        
+                        int index = vertexData.size();                      
+                        newVerts.position(vertexData.size() * vb.getNumComponents());
+                        for (int j = 0; j < newVertices.size(); j++) {
+                            int oldInd = indiceMap.get(index) ;
+                            for (int i = 0; i < vb.getNumComponents(); i++) {                                
+                                    putValue(vb.getFormat(), newVerts, buffer, oldInd* vb.getNumComponents() + i);
+                            }                            
+                            index++;
+                        }                        
+                        vb.updateData(newVerts);    
+                        //destroy previous buffer as it's no longer needed
+                        destroyDirectBuffer(buffer);
+                    }             
+                }                
+            }
+            vertexData.addAll(newVertices);
+            
+            mesh.updateCounts();
+        }
+
+        return vertexData;
+    }
+    
+    private static void bulkPut(VertexBuffer.Format format, Buffer buf1, Buffer buf2) {
+        switch (format) {
+            case Byte:
+            case Half:
+            case UnsignedByte:
+                ((ByteBuffer) buf1).put((ByteBuffer) buf2);
+                break;
+            case Short:
+            case UnsignedShort:
+
+                ((ShortBuffer) buf1).put((ShortBuffer) buf2);
+                break;
+
+            case Int:
+            case UnsignedInt:
+                ((IntBuffer) buf1).put((IntBuffer) buf2);
+                break;
+            case Float:
+
+                ((FloatBuffer) buf1).put((FloatBuffer) buf2);
+                break;
+            case Double:
+                ((DoubleBuffer) buf1).put((DoubleBuffer) buf2);
+                break;
+
+            default:
+                throw new UnsupportedOperationException("Unrecoginized buffer format: " + format);
+        }
+    }
+    
+     private static void putValue(VertexBuffer.Format format, Buffer buf1, Buffer buf2,int index) {
+        switch (format) {
+            case Byte:
+            case Half:
+            case UnsignedByte:
+                byte b = ((ByteBuffer) buf2).get(index);
+                ((ByteBuffer) buf1).put(b);
+                break;
+            case Short:
+            case UnsignedShort:
+                short s = ((ShortBuffer) buf2).get(index);
+                ((ShortBuffer) buf1).put(s);              
+                break;
+
+            case Int:
+            case UnsignedInt:
+                int i = ((IntBuffer) buf2).get(index);
+                ((IntBuffer) buf1).put(i);              
+                break;                
+            case Float:
+                float f = ((FloatBuffer) buf2).get(index);
+                ((FloatBuffer) buf1).put(f);              
+                break;                   
+            case Double:
+                double d = ((DoubleBuffer) buf2).get(index);
+                ((DoubleBuffer) buf1).put(d);              
+                break;                 
+            default:
+                throw new UnsupportedOperationException("Unrecoginized buffer format: " + format);
+        }
+    }
+    
+    private static List<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.limit() / 3);
+        List<VertexData> vertices = initVertexData(vertexBuffer.limit() / 3);
         
         index[0] = indexBuffer.get(0);
         index[1] = indexBuffer.get(1);
@@ -238,9 +436,9 @@ public class TangentBinormalGenerator {
             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);
+                vertices.get(index[0]).triangles.add(triData);
+                vertices.get(index[1]).triangles.add(triData);
+                vertices.get(index[2]).triangles.add(triData);
             }
             
             Vector3f vTemp = v[0];
@@ -260,13 +458,13 @@ public class TangentBinormalGenerator {
         return vertices;
     }
     
-    private static VertexData[] processTriangleFan(Mesh mesh,
+    private static List<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.limit() / 3);
+        List<VertexData> vertices = initVertexData(vertexBuffer.limit() / 3);
         
         index[0] = indexBuffer.get(0);
         index[1] = indexBuffer.get(1);
@@ -284,9 +482,9 @@ public class TangentBinormalGenerator {
             
             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);
+                vertices.get(index[0]).triangles.add(triData);
+                vertices.get(index[1]).triangles.add(triData);
+                vertices.get(index[2]).triangles.add(triData);
             }
             
             Vector3f vTemp = v[1];
@@ -379,7 +577,6 @@ public class TangentBinormalGenerator {
                     "The angle must be between 0 and 179 degrees.");
         }
         toleranceDot = FastMath.cos(angle * FastMath.DEG_TO_RAD);
-        toleranceAngle = angle;
     }
     
     
@@ -415,18 +612,20 @@ public class TangentBinormalGenerator {
             populateFromBuffer(texCoord, texcoordBuffer, 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) &&
-                    approxEqual(vertexInfo.texCoord, texCoord))
-                {
-                    vertexInfo.indices.add(i);
-                    found = true;
-                    break;  
-                }
-            }
+            //Nehon 07/07/2013
+            //Removed this part, joining splitted vertice to compute tangent space makes no sense to me
+            //separate vertice should have separate tangent space            
+//            for (int j = 0; j < vertexMap.size(); j++) {
+//                VertexInfo vertexInfo = vertexMap.get(j);
+//                if (approxEqual(vertexInfo.position, position) &&
+//                    approxEqual(vertexInfo.normal, normal) &&
+//                    approxEqual(vertexInfo.texCoord, texCoord))
+//                {
+//                    vertexInfo.indices.add(i);
+//                    found = true;
+//                    break;  
+//                }
+//            }
             
             if (!found) {
                 VertexInfo vertexInfo = new VertexInfo(position.clone(), normal.clone(), texCoord.clone());
@@ -438,43 +637,44 @@ public class TangentBinormalGenerator {
         return vertexMap;
     }
     
-    private static void processTriangleData(Mesh mesh, VertexData[] vertices,
-            boolean approxTangent)
-    {
+    private static void processTriangleData(Mesh mesh, List<VertexData> vertices,
+            boolean approxTangent) {
         ArrayList<VertexInfo> 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);
+
+        FloatBuffer tangents = BufferUtils.createFloatBuffer(vertices.size() * 4);
+
+        ColorRGBA[] cols = null;
+        if (debug) {
+            cols = new ColorRGBA[vertices.size()];
+        }
 
         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);
+
+            TriangleData firstTriangle = vertices.get(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<TriangleData> triangles = vertices[i].triangles;
-                
+                ArrayList<TriangleData> triangles = vertices.get(i).triangles;
+
                 for (int j = 0; j < triangles.size(); j++) {
                     TriangleData triangleData = triangles.get(j);
 
@@ -499,40 +699,31 @@ public class TangentBinormalGenerator {
                     }
                 }
             }
-            
-            
+
+
             // find average tangent
             tangent.set(0, 0, 0);
             binormal.set(0, 0, 0);
-            
+
             int triangleCount = 0;
             for (int i : vertexInfo.indices) {
-                ArrayList<TriangleData> triangles = vertices[i].triangles;
+                ArrayList<TriangleData> triangles = vertices.get(i).triangles;
                 triangleCount += triangles.size();
-                
-          //      boolean flippedNormal = false;
+                if (debug) {
+                    cols[i] = ColorRGBA.White;
+                }
+
                 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);
@@ -587,21 +778,21 @@ public class TangentBinormalGenerator {
                             "Tangent and binormal are parallel for vertex {0}.", blameVertex);
                 }
             }
-            
+
             Vector3f finalTangent = new Vector3f();
             Vector3f tmp = new Vector3f();
             for (int i : vertexInfo.indices) {
                 if (approxTangent) {
                     // Gram-Schmidt orthogonalize
-                    finalTangent.set(tangent).subtractLocal(tmp.set(givenNormal).multLocal(givenNormal.dot(tangent)));                    
+                    finalTangent.set(tangent).subtractLocal(tmp.set(givenNormal).multLocal(givenNormal.dot(tangent)));
                     finalTangent.normalizeLocal();
-                    
-                    wCoord = tmp.set(givenNormal).crossLocal(tangent).dot(binormal) <0f? -1f:1f;
+
+                    wCoord = tmp.set(givenNormal).crossLocal(tangent).dot(binormal) < 0f ? -1f : 1f;
 
                     tangents.put((i * 4), finalTangent.x);
                     tangents.put((i * 4) + 1, finalTangent.y);
                     tangents.put((i * 4) + 2, finalTangent.z);
-                    tangents.put((i * 4) + 3, wCoord);  
+                    tangents.put((i * 4) + 3, wCoord);
                 } else {
                     tangents.put((i * 4), tangent.x);
                     tangents.put((i * 4) + 1, tangent.y);
@@ -612,13 +803,49 @@ public class TangentBinormalGenerator {
                 }
             }
         }
-        
+        tangents.limit(tangents.capacity());
         // If the model already had a tangent buffer, replace it with the regenerated one
-        mesh.clearBuffer(Type.Tangent); 
+        mesh.clearBuffer(Type.Tangent);
         mesh.setBuffer(Type.Tangent, 4, tangents);
-//        if (!approxTangent) mesh.setBuffer(Type.Binormal, 3, binormals);
-    }
+        
+        
+        
+        if(mesh.isAnimated()){
+            mesh.clearBuffer(Type.BindPoseNormal);
+            mesh.clearBuffer(Type.BindPosePosition);
+            mesh.clearBuffer(Type.BindPoseTangent);
+            mesh.generateBindPose(true);
+        }
+
+        if (debug) {
+            writeColorBuffer( vertices, cols, mesh);
+        }
+        mesh.updateBound();
+        mesh.updateCounts();
+    }    
     
+    private static void writeColorBuffer(List<VertexData> vertices, ColorRGBA[] cols, Mesh mesh) {
+        FloatBuffer colors = BufferUtils.createFloatBuffer(vertices.size() * 4);
+        colors.rewind();
+        for (ColorRGBA color : cols) {
+            colors.put(color.r);
+            colors.put(color.g);
+            colors.put(color.b);
+            colors.put(color.a);
+        }
+        mesh.clearBuffer(Type.Color);
+        mesh.setBuffer(Type.Color, 4, colors);
+    }
+
+    private static int parity(Vector3f n1, Vector3f n) {
+        if (n1.dot(n) < 0) {
+            return -1;
+        } else {
+            return 1;
+        }
+
+    }
+
     public static Mesh genTbnLines(Mesh mesh, float scale) {
         if (mesh.getBuffer(Type.Tangent) == null) {
             return genNormalLines(mesh, scale);