Browse Source

Added 3D polygon ear clipping (WIP)

Vicente Penades 6 years ago
parent
commit
e778880977

+ 2 - 2
src/SharpGLTF.Toolkit/Geometry/MeshBuilder.cs

@@ -69,7 +69,7 @@ namespace SharpGLTF.Geometry
         private readonly Dictionary<(TMaterial, int), PrimitiveBuilder<TMaterial, TvG, TvM, TvS>> _Primitives = new Dictionary<(TMaterial, int), PrimitiveBuilder<TMaterial, TvG, TvM, TvS>>();
 
         #pragma warning disable SA1401 // Fields should be private
-        internal IPolygonTriangulator _Triangulator = NaivePolygonTriangulation.Default;
+        internal IPolygonTriangulator _Triangulator = BasicEarClippingTriangulation.Default;
         #pragma warning restore SA1401 // Fields should be private
 
         #endregion
@@ -81,7 +81,7 @@ namespace SharpGLTF.Geometry
         public IPolygonTriangulator Triangulator
         {
             get => _Triangulator;
-            set => _Triangulator = value == null ? NaivePolygonTriangulation.Default : _Triangulator;
+            set => _Triangulator = value == null ? BasicEarClippingTriangulation.Default : value;
         }
 
         public string Name { get; set; }

+ 11 - 4
src/SharpGLTF.Toolkit/Geometry/PrimitiveBuilder.cs

@@ -224,6 +224,17 @@ namespace SharpGLTF.Geometry
         /// </remarks>
         public void AddPolygon(params VertexBuilder<TvG, TvM, TvS>[] points)
         {
+            Guard.IsTrue(_PrimitiveVertexCount == 3, nameof(VerticesPerPrimitive), "Triangles are not supported for this primitive");
+
+            Guard.NotNull(points, nameof(points));
+            Guard.MustBeGreaterThanOrEqualTo(points.Length, 3, nameof(points));
+
+            if (points.Length == 3)
+            {
+                AddTriangle(points[0], points[1], points[2]);
+                return;
+            }
+
             Span<Vector3> vertices = stackalloc Vector3[points.Length];
 
             for (int i = 0; i < vertices.Length; ++i)
@@ -231,10 +242,6 @@ namespace SharpGLTF.Geometry
                 vertices[i] = points[i].Position;
             }
 
-            // TODO: remove duplicate vertices
-
-            if (vertices.Length < 3) return;
-
             Span<int> indices = stackalloc int[(vertices.Length - 2) * 3];
 
             _Mesh._Triangulator.Triangulate(indices, vertices);

+ 85 - 0
src/SharpGLTF.Toolkit/Geometry/Triangulation.cs

@@ -35,4 +35,89 @@ namespace SharpGLTF.Geometry
             }
         }
     }
+
+    class BasicEarClippingTriangulation : IPolygonTriangulator
+    {
+        static BasicEarClippingTriangulation() { }
+
+        private BasicEarClippingTriangulation() { }
+
+        private static readonly BasicEarClippingTriangulation _Instance = new BasicEarClippingTriangulation();
+
+        public static IPolygonTriangulator Default => _Instance;
+
+        public void Triangulate(Span<int> outIndices, ReadOnlySpan<Vector3> inVertices)
+        {
+            System.Diagnostics.Debug.Assert(outIndices.Length == (inVertices.Length - 2) * 3, $"{nameof(outIndices)} has invalid length");
+
+            // setup source Indices
+            Span<int> inIndices = stackalloc int[inVertices.Length];
+            for (int i = 0; i < inIndices.Length; ++i) inIndices[i] = i;
+
+            // define master direction
+            var masterDirection = Vector3.Zero;
+            for (int i = 2; i < inVertices.Length; ++i)
+            {
+                var ab = inVertices[i - 1] - inVertices[0];
+                var ac = inVertices[i - 0] - inVertices[0];
+                masterDirection += Vector3.Cross(ab, ac);
+            }
+
+            int triIdx = 0;
+
+            // begin clipping ears
+            while (inIndices.Length > 3)
+            {
+                var tri = _FindEarTriangle(inIndices, inVertices, masterDirection);
+                if (tri.Item1 < 0) throw new ArgumentException("failed to triangulate", nameof(inVertices));
+
+                outIndices[triIdx++] = inIndices[tri.Item1];
+                outIndices[triIdx++] = inIndices[tri.Item2];
+                outIndices[triIdx++] = inIndices[tri.Item3];
+
+                // tri.Item2 is the index of the ear's triangle.
+                // remove the ear from the array:
+                for (int i = tri.Item2 + 1; i < inIndices.Length; ++i)
+                {
+                    inIndices[i - 1] = inIndices[i];
+                }
+
+                inIndices = inIndices.Slice(0, inIndices.Length - 1);
+            }
+
+            // add last triangle.
+            outIndices[triIdx++] = inIndices[0];
+            outIndices[triIdx++] = inIndices[1];
+            outIndices[triIdx++] = inIndices[2];
+
+            System.Diagnostics.Debug.Assert(outIndices.Length == triIdx, $"{nameof(outIndices)} has invalid length");
+        }
+
+        private static (int, int, int) _FindEarTriangle(ReadOnlySpan<int> inIndices, ReadOnlySpan<Vector3> inVertices, Vector3 masterDirection)
+        {
+            for (int i = 0; i < inIndices.Length; ++i)
+            {
+                // define indices of the ear.
+                var a = i + 0;
+                var b = i + 1; if (b >= inIndices.Length) b -= inIndices.Length;
+                var c = i + 2; if (c >= inIndices.Length) c -= inIndices.Length;
+
+                // map to vertex indices
+                var aa = inIndices[a];
+                var bb = inIndices[b];
+                var cc = inIndices[c];
+
+                var ab = inVertices[bb] - inVertices[aa];
+                var ac = inVertices[cc] - inVertices[aa];
+                var dir = Vector3.Cross(ab, ac);
+
+                // determine the winding of the ear, and skip it if it's reversed.
+                if (Vector3.Dot(masterDirection, dir) <= 0) continue;
+
+                return (a, b, c);
+            }
+
+            return (-1, -1, -1);
+        }
+    }
 }

+ 4 - 4
src/SharpGLTF.Toolkit/Geometry/VertexTypes/VertexGeometry.cs

@@ -17,7 +17,7 @@ namespace SharpGLTF.Geometry.VertexTypes
         void SetNormal(Vector3 normal);
         void SetTangent(Vector4 tangent);
 
-        void Transform(Matrix4x4 xform);
+        void ApplyTransform(Matrix4x4 xform);
     }
 
     /// <summary>
@@ -71,7 +71,7 @@ namespace SharpGLTF.Geometry.VertexTypes
 
         public bool TryGetTangent(out Vector4 tangent) { tangent = default; return false; }
 
-        public void Transform(Matrix4x4 xform)
+        public void ApplyTransform(Matrix4x4 xform)
         {
             Position = Vector3.Transform(Position, xform);
         }
@@ -136,7 +136,7 @@ namespace SharpGLTF.Geometry.VertexTypes
 
         public bool TryGetTangent(out Vector4 tangent) { tangent = default; return false; }
 
-        public void Transform(Matrix4x4 xform)
+        public void ApplyTransform(Matrix4x4 xform)
         {
             Position = Vector3.Transform(Position, xform);
             Normal = Vector3.Normalize(Vector3.TransformNormal(Normal, xform));
@@ -202,7 +202,7 @@ namespace SharpGLTF.Geometry.VertexTypes
 
         public bool TryGetTangent(out Vector4 tangent) { tangent = this.Tangent; return true; }
 
-        public void Transform(Matrix4x4 xform)
+        public void ApplyTransform(Matrix4x4 xform)
         {
             Position = Vector3.Transform(Position, xform);
             Normal = Vector3.Normalize(Vector3.TransformNormal(Normal, xform));

+ 75 - 0
tests/SharpGLTF.Tests/Geometry/TriangulationTests.cs

@@ -0,0 +1,75 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Numerics;
+using System.Text;
+
+using NUnit.Framework;
+
+namespace SharpGLTF.Geometry
+{
+    using VERTEX = VertexBuilder<VertexTypes.VertexPosition, VertexTypes.VertexEmpty, VertexTypes.VertexEmpty>;
+
+    [Category("Model Authoring")]
+    public class TriangulationTests
+    {
+        private static readonly Vector3[] _BowTiePolygon = new[]
+        {
+            new Vector3(-10, -10, 0),
+            new Vector3(0, -5, 0),
+            new Vector3(10, -10, 0),
+            new Vector3(10, 10, 0),
+            new Vector3(0, 5, 0),
+            new Vector3(-10, 10, 0)
+        };
+
+        private static readonly Vector3[] _BridgePolygon = new[]
+        {            
+            new Vector3(-10, 2, 0),
+            new Vector3(-10, -10, 0),
+            new Vector3(-8, -10, 0),
+            new Vector3(-8, -5, 0),
+            new Vector3(-4, -1, 0),
+            new Vector3(0, 0, 0),
+            new Vector3(+4, -1, 0),
+            new Vector3(+8, -5, 0),
+            new Vector3(+8, -10, 0),
+            new Vector3(+10, -10, 0),
+            new Vector3(+10, 2, 0),
+        };
+
+        private static readonly Vector3[] _CoLinearBaseTriangle = new[]
+        {
+            new Vector3(0,10,0),
+            new Vector3(-10,-10,0),
+            new Vector3(-5,-10,0),
+            new Vector3(0,-10,0),
+            new Vector3(5,-10,0),
+            new Vector3(10,-10,0),            
+        };
+
+        [Test]
+        public void CreateEarClippingPolygons()
+        {
+            var material = new Materials.MaterialBuilder()
+                .WithUnlitShader();
+
+            var mesh = VERTEX.CreateCompatibleMesh();
+            mesh.Triangulator = BasicEarClippingTriangulation.Default;
+
+            for(int i=0; i < 10; ++i)
+            {
+                var xform = Matrix4x4.CreateFromYawPitchRoll(i, i *2, i);
+                xform = Matrix4x4.CreateTranslation(0, 0, 20) * xform;
+
+                var vertices = _BridgePolygon
+                    .Select(item => new VERTEX(Vector3.Transform(item,xform)) )
+                    .ToArray();
+
+                mesh.UsePrimitive(material).AddPolygon(vertices);
+            }            
+
+            mesh.AttachToCurrentTest("BasicEarClippingTriangulation.glb");
+        }
+    }
+}

+ 15 - 0
tests/SharpGLTF.Tests/Utils.cs

@@ -79,6 +79,21 @@ namespace SharpGLTF
             TestContext.AddTestAttachment(fileName);
         }
 
+        public static void AttachToCurrentTest<TvG, TvM, TvS>(this Geometry.MeshBuilder<TvG, TvM, TvS> mesh, string fileName)
+            where TvG : struct, Geometry.VertexTypes.IVertexGeometry
+            where TvM : struct, Geometry.VertexTypes.IVertexMaterial
+            where TvS : struct, Geometry.VertexTypes.IVertexSkinning
+        {
+            var gl2model = Schema2.ModelRoot.CreateModel();
+
+            var gl2mesh = Schema2.Schema2Toolkit.CreateMeshes(gl2model, mesh).First();
+
+            var node = gl2model.UseScene(0).CreateNode();
+            node.Mesh = gl2mesh;
+
+            gl2model.AttachToCurrentTest(fileName);
+        }
+
         public static void AttachText(this TestContext context, string fileName, string[] lines)
         {
             fileName = context.GetAttachmentPath(fileName, true);