From b3f51ab1e9b512f813e9091aad0e4251289e079c Mon Sep 17 00:00:00 2001 From: GoaLitiuM Date: Thu, 5 Aug 2021 23:04:12 +0300 Subject: [PATCH] quickhull copypaste & meshsimplifier --- Source/Game/MeshSimplifier.cs | 600 +++++++++++++++ Source/Game/Q3MapImporter.cs | 1290 ++++++++++++++++++++++++++++++++- 2 files changed, 1887 insertions(+), 3 deletions(-) create mode 100644 Source/Game/MeshSimplifier.cs diff --git a/Source/Game/MeshSimplifier.cs b/Source/Game/MeshSimplifier.cs new file mode 100644 index 0000000..421787c --- /dev/null +++ b/Source/Game/MeshSimplifier.cs @@ -0,0 +1,600 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using FlaxEngine; +using Console = Cabrito.Console; + +namespace Game +{ + [Flags] + public enum TriangleAttributes + { + NONE, + NORMAL = 2, + TEXCOORD = 4, + COLOR = 8 + }; + + public class Triangle + { + public Int3 v; + public Vector3 n; + //public Vector3[3] uvs; + public Vector4 err; + public bool dirty; + public bool deleted; + public TriangleAttributes attr; + public int material; + } + + public class Vertex + { + public Vector3 p; + public int tstart,tcount; + public SymetricMatrix q; + public bool border; + }; + + public class Ref + { + public int tid, tvertex; + + /*public Ref() + { + tid = 0; + tvertex = 0; + }*/ + } + + public class SymetricMatrix + { + // Constructor + + public SymetricMatrix(double c = 0) + { + m = new double[10]; + for (int i=0; i<10; i++) m[i] = c; + } + + public SymetricMatrix( double m11, double m12, double m13, double m14, + double m22, double m23, double m24, + double m33, double m34, + double m44) + { + m = new double[10]; + m[0] = m11; m[1] = m12; m[2] = m13; m[3] = m14; + m[4] = m22; m[5] = m23; m[6] = m24; + m[7] = m33; m[8] = m34; + m[9] = m44; + } + + // Make plane + + public SymetricMatrix(double a,double b,double c,double d) + { + m = new double[10]; + m[0] = a*a; m[1] = a*b; m[2] = a*c; m[3] = a*d; + m[4] = b*b; m[5] = b*c; m[6] = b*d; + m[7 ] =c*c; m[8 ] = c*d; + m[9 ] = d*d; + } + + public double this[int index] + { + get { return m[index]; } + set + { + m[index] = value; + } + } + // Determinant + + public double det( int a11, int a12, int a13, + int a21, int a22, int a23, + int a31, int a32, int a33) + { + double det = m[a11]*m[a22]*m[a33] + m[a13]*m[a21]*m[a32] + m[a12]*m[a23]*m[a31] + - m[a13]*m[a22]*m[a31] - m[a11]*m[a23]*m[a32]- m[a12]*m[a21]*m[a33]; + return det; + } + + + + public static SymetricMatrix operator+(SymetricMatrix o, SymetricMatrix n) + { + return new SymetricMatrix( o.m[0]+n[0], o.m[1]+n[1], o.m[2]+n[2], o.m[3]+n[3], + o.m[4]+n[4], o.m[5]+n[5], o.m[6]+n[6], + o.m[ 7]+n[ 7], o.m[ 8]+n[8 ], + o.m[ 9]+n[9 ]); + } + + + /*public static SymetricMatrix operator+=(SymetricMatrix o, SymetricMatrix n) + { + m[0]+=n[0]; m[1]+=n[1]; m[2]+=n[2]; m[3]+=n[3]; + m[4]+=n[4]; m[5]+=n[5]; m[6]+=n[6]; m[7]+=n[7]; + m[8]+=n[8]; m[9]+=n[9]; + return *this; + }*/ + + double[] m; + }; + + public static class ListExtras + { + // list: List to resize + // size: desired new size + // element: default value to insert + + public static void Resize(this List list, int size, T element = default(T)) where T : new() + { + int count = list.Count; + + if (size < count) + { + list.RemoveRange(size, count - size); + } + else if (size > count) + { + if (size > list.Capacity) // Optimization + list.Capacity = size; + + list.AddRange(Enumerable.Repeat(element, size - count)); + for (int i = count; i < size; i++) + list[i] = new T(); + } + + + + } + } + + // Fast Quadratic Mesh Simplification + public class MeshSimplifier + { + private float ratio; + private float agressiveness; + + private List triangles; + private List vertices; + private List refs = new List(); + + public MeshSimplifier(float ratio = 1.0f, float agressiveness = 7.0f) + { + this.ratio = ratio; + this.agressiveness = agressiveness; + } + + public List Simplify(Vector3[] input) + { + triangles = new List(input.Length/3); + vertices = new List(input.Length); + { + int i = 0; + foreach (var vec in input) + { + vertices.Add(new Vertex() + { + p = vec, + q = new SymetricMatrix(), + }); + i += 3; + } + for (i = 0; i deleted0 = new List(); + List deleted1 = new List(); + int triangle_start_count = triangles.Count; + + int target_count = (int)(ratio * (float)triangle_start_count); + + //int iteration = 0; + //loop(iteration,0,100) + for (int iteration = 0; iteration < 9999; iteration ++) + { + if (ratio < 1.0f && triangle_start_count-deleted_triangles<=target_count) break; + + // update mesh constantly + update_mesh(iteration); + // clear dirty flag + for (int i = 0; i < triangles.Count; i++) + triangles[i].dirty=false; + // + // All triangles with edges below the threshold will be removed + // + // The following numbers works well for most models. + // If it does not, try to adjust the 3 parameters + // + //double threshold = 0.001; //1.0E-3 EPS; + double threshold = 1.0E-3; + if (ratio < 1.0f) + threshold = 0.000000001 * Math.Pow((double)(iteration+3),agressiveness); + //if (verbose) { + // printf("lossless iteration %d\n", iteration); + //} + + // remove vertices & mark deleted triangles + for (int i = 0; i < triangles.Count; i++) + { + Triangle t = triangles[i]; + if(t.err[3]>threshold) continue; + if(t.deleted) continue; + if(t.dirty) continue; + + for (int j = 0; j < 3; j++) + if(t.err[j] deleted) + { + for (int k = 0; k < v0.tcount; k++) + { + Triangle t = triangles[refs[v0.tstart+k].tid]; + if(t.deleted) + continue; + + int s=refs[v0.tstart+k].tvertex; + int id1=t.v[(s+1)%3]; + int id2=t.v[(s+2)%3]; + + if(id1==i1 || id2==i1) // delete ? + { + deleted[k]=true; + continue; + } + Vector3 d1 = vertices[id1].p-p; d1.Normalize(); + Vector3 d2 = vertices[id2].p-p; d2.Normalize(); + if(Mathf.Abs(Vector3.Dot(d1, d2))>0.999) return true; + Vector3 n = Vector3.Cross(d1, d2); + n.Normalize(); + deleted[k]=false; + if(Vector3.Dot(n, t.n)<0.2) return true; + } + return false; + } + + // update_uvs + + private void update_uvs(int i0, ref Vertex v, ref Vector3 p, List deleted) + { + for (int k = 0; k < v.tcount; k++) + { + Ref r=refs[v.tstart+k]; + Triangle t=triangles[r.tid]; + if(t.deleted)continue; + if(deleted[k])continue; + Vector3 p1=vertices[t.v[0]].p; + Vector3 p2=vertices[t.v[1]].p; + Vector3 p3=vertices[t.v[2]].p; + //t.uvs[r.tvertex] = interpolate(p,p1,p2,p3,t.uvs); + } + } + + // Update triangle connections and edge error after a edge is collapsed + + private void update_triangles(int i0,ref Vertex v,List deleted,ref int deleted_triangles) + { + Vector3 p = new Vector3(); + for (int k = 0; k < v.tcount; k++) + { + Ref r=refs[v.tstart+k]; + Triangle t=triangles[r.tid]; + if(t.deleted)continue; + if(deleted[k]) + { + t.deleted=true; + deleted_triangles++; + continue; + } + t.v[r.tvertex]=i0; + t.dirty=true; + t.err[0]=calculate_error(t.v[0],t.v[1],ref p); + t.err[1]=calculate_error(t.v[1],t.v[2],ref p); + t.err[2]=calculate_error(t.v[2],t.v[0],ref p); + t.err[3]=Math.Min(t.err[0],Math.Min(t.err[1],t.err[2])); + triangles[r.tid] = t; + refs.Add(r); + } + } + + /*private Matrix MatrixPlane(float a, float b, float c, float d) + { + Matrix m = new Matrix(); + m[0] = a*a; m[1] = a*b; m[2] = a*c; m[3] = a*d; + m[4] = b*b; m[5] = b*c; m[6] = b*d; + m[7 ] = c*c; m[8 ] = c*d; + m[9 ] = d*d; + return m; + } + + private double MatrixDet(Matrix m, int a11, int a12, int a13, + int a21, int a22, int a23, + int a31, int a32, int a33) + { + double det = m[a11]*m[a22]*m[a33] + m[a13]*m[a21]*m[a32] + m[a12]*m[a23]*m[a31] + - m[a13]*m[a22]*m[a31] - m[a11]*m[a23]*m[a32]- m[a12]*m[a21]*m[a33]; + return det; + }*/ + + // compact triangles, compute edge error and build reference list + + private void update_mesh(int iteration) + { + if(iteration>0) // compact triangles + { + int dst=0; + for (int i = 0; i < triangles.Count; i++) + if(!triangles[i].deleted) + { + triangles[dst++]=triangles[i]; + } + triangles.Resize(dst); + } + // + // Init Quadrics by Plane & Edge Errors + // + // required at the beginning ( iteration == 0 ) + // recomputing during the simplification is not required, + // but mostly improves the result for closed meshes + // + if( iteration == 0 ) + { + for (int i = 0; i < vertices.Count; i++) + vertices[i].q=new SymetricMatrix();//vertices[i].q=Matrix(0.0); + + for (int i = 0; i < triangles.Count; i++) + { + if (i == 18) + Console.Print(i.ToString()); + Triangle t=triangles[i]; + Vector3 n = new Vector3(); + Vector3[] p = new Vector3[3]; + for (int j = 0; j<3; j++) + p[j]=vertices[t.v[j]].p; + n = Vector3.Cross(p[1]-p[0],p[2]-p[0]); + n.Normalize(); + t.n=n; + for (int j = 0; j<3; j++) + vertices[t.v[j]].q = vertices[t.v[j]].q + new SymetricMatrix(n.X,n.Y,n.Z,-Vector3.Dot(n, p[0])); + } + for (int i = 0; i < triangles.Count; i++) + { + // Calc Edge Error + Triangle t=triangles[i]; + Vector3 p = new Vector3(); + for (int j = 0; j<3; j++) t.err[j]=calculate_error(t.v[j],t.v[(j+1)%3], ref p); + t.err[3]=Math.Min(t.err[0],Math.Min(t.err[1],t.err[2])); + } + } + + // Init Reference ID list + for (int i = 0; i < vertices.Count; i++) + { + vertices[i].tstart=0; + vertices[i].tcount=0; + } + for (int i = 0; i < triangles.Count; i++) + { + Triangle t=triangles[i]; + for (int j = 0; j<3; j++) vertices[t.v[j]].tcount++; + } + int tstart=0; + for (int i = 0; i < vertices.Count; i++) + { + Vertex v=vertices[i]; + v.tstart=tstart; + tstart+=v.tcount; + v.tcount=0; + } + + // Write References + refs.Resize(triangles.Count*3); + for (int i = 0; i < triangles.Count; i++) + { + Triangle t=triangles[i]; + for (int j = 0; j<3; j++) + { + Vertex v=vertices[t.v[j]]; + refs[v.tstart+v.tcount].tid=i; + refs[v.tstart+v.tcount].tvertex=j; + v.tcount++; + } + } + + // Identify boundary : vertices[].border=0,1 + if( iteration == 0 ) + { + List vcount = new List(); + List vids = new List(); + + for (int i = 0; i < vertices.Count; i++) + vertices[i].border=false; + + for (int i = 0; i < vertices.Count; i++) + { + Vertex v=vertices[i]; + vcount.Clear(); + vids.Clear(); + for (int j = 0; j < v.tcount; j++) + { + int kk=refs[v.tstart+j].tid; + Triangle t=triangles[kk]; + for (int k = 0; k<3; k++) + { + int ofs=0,id=t.v[k]; + while(ofs try to find best result + Vector3 p1=vertices[id_v1].p; + Vector3 p2=vertices[id_v2].p; + Vector3 p3=(p1+p2)*0.5f; + double error1 = vertex_error(ref q, p1.X,p1.Y,p1.Z); + double error2 = vertex_error(ref q, p2.X,p2.Y,p2.Z); + double error3 = vertex_error(ref q, p3.X,p3.Y,p3.Z); + error = Math.Min(error1, Math.Min(error2, error3)); + if (error1 == error) p_result=p1; + if (error2 == error) p_result=p2; + if (error3 == error) p_result=p3; + } + return (float)error; + } + } +} \ No newline at end of file diff --git a/Source/Game/Q3MapImporter.cs b/Source/Game/Q3MapImporter.cs index 3558e21..a7f0bae 100644 --- a/Source/Game/Q3MapImporter.cs +++ b/Source/Game/Q3MapImporter.cs @@ -3,12 +3,1295 @@ using System.Collections.Generic; using FlaxEngine; using System.IO; using System.Linq; +using System.Runtime.CompilerServices; using FlaxEngine.Assertions; using FlaxEngine.Utilities; using Console = Cabrito.Console; +// TODO: remove coplanar/degenerate faces from the final mesh +// It seems the original algorithm is working but removing degenerate faces does not work + namespace Game { + /// + /// An implementation of the quickhull algorithm for generating 3d convex + /// hulls. + /// + /// The algorithm works like this: you start with an initial "seed" hull, + /// that is just a simple tetrahedron made up of four points in the point + /// cloud. This seed hull is then grown until it all the points in the + /// point cloud is inside of it, at which point it will be the convex hull + /// for the entire set. + /// + /// All of the points in the point cloud is divided into two parts, the + /// "open set" and the "closed set". The open set consists of all the + /// points outside of the tetrahedron, and the closed set is all of the + /// points inside the tetrahedron. After each iteration of the algorithm, + /// the closed set gets bigger and the open set get smaller. When the open + /// set is empty, the algorithm is finished. + /// + /// Each point in the open set is assigned to a face that it lies outside + /// of. To grow the hull, the point in the open set which is farthest from + /// it's face is chosen. All faces which are facing that point (I call + /// them "lit faces" in the code, because if you imagine the point as a + /// point light, it's the set of points which would be lit by that point + /// light) are removed, and a "horizon" of edges is found from where the + /// faces were removed. From this horizon, new faces are constructed in a + /// "cone" like fashion connecting the point to the edges. + /// + /// To keep track of the faces, I use a struct for each face which + /// contains the three vertices of the face in CCW order, as well as the + /// three triangles which share an edge. I was considering doing a + /// half-edge structure to store the mesh, but it's not needed. Using a + /// struct for each face and neighbors simplify the algorithm and makes it + /// easy to export it as a mesh. + /// + /// The most subtle part of the algorithm is finding the horizon. In order + /// to properly construct the cone so that all neighbors are kept + /// consistent, you can do a depth-first search from the first lit face. + /// If the depth-first search always proceeeds in a counter-clockwise + /// fashion, it guarantees that the horizon will be found in a + /// counter-clockwise order, which makes it easy to construct the cone of + /// new faces. + /// + /// A note: the code uses a right-handed coordinate system, where the + /// cross-product uses the right-hand rule and the faces are in CCW order. + /// At the end of the algorithm, the hull is exported in a Unity-friendly + /// fashion, with a left-handed mesh. + /// + public class ConvexHullCalculator { + + /// + /// Constant representing a point that has yet to be assigned to a + /// face. It's only used immediately after constructing the seed hull. + /// + const int UNASSIGNED = -2; + + /// + /// Constant representing a point that is inside the convex hull, and + /// thus is behind all faces. In the openSet array, all points with + /// INSIDE are at the end of the array, with indexes larger + /// openSetTail. + /// + const int INSIDE = -1; + + /// + /// Epsilon value. If the coordinates of the point space are + /// exceptionally close to each other, this value might need to be + /// adjusted. + /// + const float EPSILON = 0.0001f; + + /// + /// Struct representing a single face. + /// + /// Vertex0, Vertex1 and Vertex2 are the vertices in CCW order. They + /// acutal points are stored in the points array, these are just + /// indexes into that array. + /// + /// Opposite0, Opposite1 and Opposite2 are the keys to the faces which + /// share an edge with this face. Opposite0 is the face opposite + /// Vertex0 (so it has an edge with Vertex2 and Vertex1), etc. + /// + /// Normal is (unsurprisingly) the normal of the triangle. + /// + struct Face { + public int Vertex0; + public int Vertex1; + public int Vertex2; + + public int Opposite0; + public int Opposite1; + public int Opposite2; + + public Vector3 Normal; + + public Face(int v0, int v1, int v2, int o0, int o1, int o2, Vector3 normal) { + Vertex0 = v0; + Vertex1 = v1; + Vertex2 = v2; + Opposite0 = o0; + Opposite1 = o1; + Opposite2 = o2; + Normal = normal; + } + + public bool Equals(Face other) { + return (this.Vertex0 == other.Vertex0) + && (this.Vertex1 == other.Vertex1) + && (this.Vertex2 == other.Vertex2) + && (this.Opposite0 == other.Opposite0) + && (this.Opposite1 == other.Opposite1) + && (this.Opposite2 == other.Opposite2) + && (this.Normal == other.Normal); + } + } + + /// + /// Struct representing a mapping between a point and a face. These + /// are used in the openSet array. + /// + /// Point is the index of the point in the points array, Face is the + /// key of the face in the Key dictionary, Distance is the distance + /// from the face to the point. + /// + struct PointFace { + public int Point; + public int Face; + public float Distance; + + public PointFace(int p, int f, float d) { + Point = p; + Face = f; + Distance = d; + } + } + + /// + /// Struct representing a single edge in the horizon. + /// + /// Edge0 and Edge1 are the vertexes of edge in CCW order, Face is the + /// face on the other side of the horizon. + /// + /// TODO Edge1 isn't actually needed, you can just index the next item + /// in the horizon array. + /// + struct HorizonEdge { + public int Face; + public int Edge0; + public int Edge1; + } + + /// + /// A dictionary storing the faces of the currently generated convex + /// hull. The key is the id of the face, used in the Face, PointFace + /// and HorizonEdge struct. + /// + /// This is a Dictionary, because we need both random access to it, + /// the ability to loop through it, and ability to quickly delete + /// faces (in the ConstructCone method), and Dictionary is the obvious + /// candidate that can do all of those things. + /// + /// I'm wondering if using a Dictionary is best idea, though. It might + /// be better to just have them in a List and mark a face as + /// deleted by adding a field to the Face struct. The downside is that + /// we would need an extra field in the Face struct, and when we're + /// looping through the points in openSet, we would have to loop + /// through all the Faces EVER created in the algorithm, and skip the + /// ones that have been marked as deleted. However, looping through a + /// list is fairly fast, and it might be worth it to avoid Dictionary + /// overhead. + /// + /// TODO test converting to a List instead. + /// + Dictionary faces; + + /// + /// The set of points to be processed. "openSet" is a misleading name, + /// because it's both the open set (points which are still outside the + /// convex hull) and the closed set (points that are inside the convex + /// hull). The first part of the array (with indexes <= openSetTail) + /// is the openSet, the last part of the array (with indexes > + /// openSetTail) are the closed set, with Face set to INSIDE. The + /// closed set is largely irrelevant to the algorithm, the open set is + /// what matters. + /// + /// Storing the entire open set in one big list has a downside: when + /// we're reassigning points after ConstructCone, we only need to + /// reassign points that belong to the faces that have been removed, + /// but storing it in one array, we have to loop through the entire + /// list, and checking litFaces to determine which we can skip and + /// which need to be reassigned. + /// + /// The alternative here is to give each face in Face array it's own + /// openSet. I don't like that solution, because then you have to + /// juggle so many more heap-allocated List's, we'd have to use + /// object pools and such. It would do a lot more allocation, and it + /// would have worse locality. I should maybe test that solution, but + /// it probably wont be faster enough (if at all) to justify the extra + /// allocations. + /// + List openSet; + + /// + /// Set of faces which are "lit" by the current point in the set. This + /// is used in the FindHorizon() DFS search to keep track of which + /// faces we've already visited, and in the ReassignPoints() method to + /// know which points need to be reassigned. + /// + HashSet litFaces; + + /// + /// The current horizon. Generated by the FindHorizon() DFS search, + /// and used in ConstructCone to construct new faces. The list of + /// edges are in CCW order. + /// + List horizon; + + /// + /// If SplitVerts is false, this Dictionary is used to keep track of + /// which points we've added to the final mesh. + /// + Dictionary hullVerts; + + /// + /// The "tail" of the openSet, the last index of a vertex that has + /// been assigned to a face. + /// + int openSetTail = -1; + + /// + /// When adding a new face to the faces Dictionary, use this for the + /// key and then increment it. + /// + int faceCount = 0; + + /// + /// Generate a convex hull from points in points array, and store the + /// mesh in Unity-friendly format in verts and tris. If splitVerts is + /// true, the the verts will be split, if false, the same vert will be + /// used for more than one triangle. + /// + public void GenerateHull( + List points, + bool splitVerts, + ref List verts, + ref List tris, + ref List normals) + { + if (points.Count < 4) { + throw new System.ArgumentException("Need at least 4 points to generate a convex hull"); + } + + Initialize(points, splitVerts); + + GenerateInitialHull(points); + + while (openSetTail >= 0) { + GrowHull(points); + } + + ExportMesh(points, splitVerts, ref verts, ref tris, ref normals); + VerifyMesh(points, ref verts, ref tris); + } + + /// + /// Make sure all the buffers and variables needed for the algorithm + /// are initialized. + /// + void Initialize(List points, bool splitVerts) { + faceCount = 0; + openSetTail = -1; + + if (faces == null) { + faces = new Dictionary(); + litFaces = new HashSet(); + horizon = new List(); + openSet = new List(points.Count); + } else { + faces.Clear(); + litFaces.Clear(); + horizon.Clear(); + openSet.Clear(); + + if (openSet.Capacity < points.Count) { + // i wonder if this is a good idea... if you call + // GenerateHull over and over with slightly increasing + // points counts, it's going to reallocate every time. Maybe + // i should just use .Add(), and let the List manage the + // capacity, increasing it geometrically every time we need + // to reallocate. + + // maybe do + // openSet.Capacity = Mathf.NextPowerOfTwo(points.Count) + // instead? + + openSet.Capacity = points.Count; + } + } + + if (!splitVerts) { + if (hullVerts == null) { + hullVerts = new Dictionary(); + } else { + hullVerts.Clear(); + } + } + } + + /// + /// Create initial seed hull. + /// + void GenerateInitialHull(List points) { + // Find points suitable for use as the seed hull. Some varieties of + // this algorithm pick extreme points here, but I'm not convinced + // you gain all that much from that. Currently what it does is just + // find the first four points that are not coplanar. + int b0, b1, b2, b3; + FindInitialHullIndices(points, out b0, out b1, out b2, out b3); + + var v0 = points[b0]; + var v1 = points[b1]; + var v2 = points[b2]; + var v3 = points[b3]; + + var above = Dot(v3 - v1, Cross(v1 - v0, v2 - v0)) > 0.0f; + + // Create the faces of the seed hull. You need to draw a diagram + // here, otherwise it's impossible to know what's going on :) + + // Basically: there are two different possible start-tetrahedrons, + // depending on whether the fourth point is above or below the base + // triangle. If you draw a tetrahedron with these coordinates (in a + // right-handed coordinate-system): + + // b0 = (0,0,0) + // b1 = (1,0,0) + // b2 = (0,1,0) + // b3 = (0,0,1) + + // you can see the first case (set b3 = (0,0,-1) for the second + // case). The faces are added with the proper references to the + // faces opposite each vertex + + faceCount = 0; + if (above) { + faces[faceCount++] = new Face(b0, b2, b1, 3, 1, 2, Normal(points[b0], points[b2], points[b1])); + faces[faceCount++] = new Face(b0, b1, b3, 3, 2, 0, Normal(points[b0], points[b1], points[b3])); + faces[faceCount++] = new Face(b0, b3, b2, 3, 0, 1, Normal(points[b0], points[b3], points[b2])); + faces[faceCount++] = new Face(b1, b2, b3, 2, 1, 0, Normal(points[b1], points[b2], points[b3])); + } else { + faces[faceCount++] = new Face(b0, b1, b2, 3, 2, 1, Normal(points[b0], points[b1], points[b2])); + faces[faceCount++] = new Face(b0, b3, b1, 3, 0, 2, Normal(points[b0], points[b3], points[b1])); + faces[faceCount++] = new Face(b0, b2, b3, 3, 1, 0, Normal(points[b0], points[b2], points[b3])); + faces[faceCount++] = new Face(b1, b3, b2, 2, 0, 1, Normal(points[b1], points[b3], points[b2])); + } + + VerifyFaces(points); + + // Create the openSet. Add all points except the points of the seed + // hull. + for (int i = 0; i < points.Count; i++) { + if (i == b0 || i == b1 || i == b2 || i == b3) continue; + + openSet.Add(new PointFace(i, UNASSIGNED, 0.0f)); + } + + // Add the seed hull verts to the tail of the list. + openSet.Add(new PointFace(b0, INSIDE, float.NaN)); + openSet.Add(new PointFace(b1, INSIDE, float.NaN)); + openSet.Add(new PointFace(b2, INSIDE, float.NaN)); + openSet.Add(new PointFace(b3, INSIDE, float.NaN)); + + // Set the openSetTail value. Last item in the array is + // openSet.Count - 1, but four of the points (the verts of the seed + // hull) are part of the closed set, so move openSetTail to just + // before those. + openSetTail = openSet.Count - 5; + + Assert(openSet.Count == points.Count); + + // Assign all points of the open set. This does basically the same + // thing as ReassignPoints() + for (int i = 0; i <= openSetTail; i++) { + Assert(openSet[i].Face == UNASSIGNED); + Assert(openSet[openSetTail].Face == UNASSIGNED); + Assert(openSet[openSetTail + 1].Face == INSIDE); + + var assigned = false; + var fp = openSet[i]; + + Assert(faces.Count == 4); + Assert(faces.Count == faceCount); + for (int j = 0; j < 4; j++) { + Assert(faces.ContainsKey(j)); + + var face = faces[j]; + + var dist = PointFaceDistance(points[fp.Point], points[face.Vertex0], face); + + if (dist > 0) { + fp.Face = j; + fp.Distance = dist; + openSet[i] = fp; + + assigned = true; + break; + } + } + + if (!assigned) { + // Point is inside + fp.Face = INSIDE; + fp.Distance = float.NaN; + + // Point is inside seed hull: swap point with tail, and move + // openSetTail back. We also have to decrement i, because + // there's a new item at openSet[i], and we need to process + // it next iteration + openSet[i] = openSet[openSetTail]; + openSet[openSetTail] = fp; + + openSetTail -= 1; + i -= 1; + } + } + + VerifyOpenSet(points); + } + + /// + /// Find four points in the point cloud that are not coplanar for the + /// seed hull + /// + void FindInitialHullIndices(List points, out int b0, out int b1, out int b2, out int b3) { + var count = points.Count; + + for (int i0 = 0; i0 < count - 3; i0++) { + for (int i1 = i0 + 1; i1 < count - 2; i1++) { + var p0 = points[i0]; + var p1 = points[i1]; + + if (AreCoincident(p0, p1)) continue; + + for (int i2 = i1 + 1; i2 < count - 1; i2++) { + var p2 = points[i2]; + + if (AreCollinear(p0, p1, p2)) continue; + + for (int i3 = i2 + 1; i3 < count - 0; i3++) { + var p3 = points[i3]; + + if(AreCoplanar(p0, p1, p2, p3)) continue; + + b0 = i0; + b1 = i1; + b2 = i2; + b3 = i3; + return; + } + } + } + } + + throw new System.ArgumentException("Can't generate hull, points are coplanar"); + } + + /// + /// Grow the hull. This method takes the current hull, and expands it + /// to encompass the point in openSet with the point furthest away + /// from its face. + /// + void GrowHull(List points) { + Assert(openSetTail >= 0); + Assert(openSet[0].Face != INSIDE); + + // Find farthest point and first lit face. + var farthestPoint = 0; + var dist = openSet[0].Distance; + + for (int i = 1; i <= openSetTail; i++) { + if (openSet[i].Distance > dist) { + farthestPoint = i; + dist = openSet[i].Distance; + } + } + + // Use lit face to find horizon and the rest of the lit + // faces. + FindHorizon( + points, + points[openSet[farthestPoint].Point], + openSet[farthestPoint].Face, + faces[openSet[farthestPoint].Face]); + + VerifyHorizon(); + + // Construct new cone from horizon + ConstructCone(points, openSet[farthestPoint].Point); + + VerifyFaces(points); + + // Reassign points + ReassignPoints(points); + } + + /// + /// Start the search for the horizon. + /// + /// The search is a DFS search that searches neighboring triangles in + /// a counter-clockwise fashion. When it find a neighbor which is not + /// lit, that edge will be a line on the horizon. If the search always + /// proceeds counter-clockwise, the edges of the horizon will be found + /// in counter-clockwise order. + /// + /// The heart of the search can be found in the recursive + /// SearchHorizon() method, but the the first iteration of the search + /// is special, because it has to visit three neighbors (all the + /// neighbors of the initial triangle), while the rest of the search + /// only has to visit two (because one of them has already been + /// visited, the one you came from). + /// + void FindHorizon(List points, Vector3 point, int fi, Face face) { + // TODO should I use epsilon in the PointFaceDistance comparisons? + + litFaces.Clear(); + horizon.Clear(); + + litFaces.Add(fi); + + Assert(PointFaceDistance(point, points[face.Vertex0], face) > 0.0f); + + // For the rest of the recursive search calls, we first check if the + // triangle has already been visited and is part of litFaces. + // However, in this first call we can skip that because we know it + // can't possibly have been visited yet, since the only thing in + // litFaces is the current triangle. + { + var oppositeFace = faces[face.Opposite0]; + + var dist = PointFaceDistance( + point, + points[oppositeFace.Vertex0], + oppositeFace); + + if (dist <= 0.0f) { + horizon.Add(new HorizonEdge { + Face = face.Opposite0, + Edge0 = face.Vertex1, + Edge1 = face.Vertex2, + }); + } else { + SearchHorizon(points, point, fi, face.Opposite0, oppositeFace); + } + } + + if (!litFaces.Contains(face.Opposite1)) { + var oppositeFace = faces[face.Opposite1]; + + var dist = PointFaceDistance( + point, + points[oppositeFace.Vertex0], + oppositeFace); + + if (dist <= 0.0f) { + horizon.Add(new HorizonEdge { + Face = face.Opposite1, + Edge0 = face.Vertex2, + Edge1 = face.Vertex0, + }); + } else { + SearchHorizon(points, point, fi, face.Opposite1, oppositeFace); + } + } + + if (!litFaces.Contains(face.Opposite2)) { + var oppositeFace = faces[face.Opposite2]; + + var dist = PointFaceDistance( + point, + points[oppositeFace.Vertex0], + oppositeFace); + + if (dist <= 0.0f) { + horizon.Add(new HorizonEdge { + Face = face.Opposite2, + Edge0 = face.Vertex0, + Edge1 = face.Vertex1, + }); + } else { + SearchHorizon(points, point, fi, face.Opposite2, oppositeFace); + } + } + } + + /// + /// Recursively search to find the horizon or lit set. + /// + void SearchHorizon(List points, Vector3 point, int prevFaceIndex, int faceCount, Face face) { + Assert(prevFaceIndex >= 0); + Assert(litFaces.Contains(prevFaceIndex)); + Assert(!litFaces.Contains(faceCount)); + Assert(faces[faceCount].Equals(face)); + + litFaces.Add(faceCount); + + // Use prevFaceIndex to determine what the next face to search will + // be, and what edges we need to cross to get there. It's important + // that the search proceeds in counter-clockwise order from the + // previous face. + int nextFaceIndex0; + int nextFaceIndex1; + int edge0; + int edge1; + int edge2; + + if (prevFaceIndex == face.Opposite0) { + nextFaceIndex0 = face.Opposite1; + nextFaceIndex1 = face.Opposite2; + + edge0 = face.Vertex2; + edge1 = face.Vertex0; + edge2 = face.Vertex1; + } else if (prevFaceIndex == face.Opposite1) { + nextFaceIndex0 = face.Opposite2; + nextFaceIndex1 = face.Opposite0; + + edge0 = face.Vertex0; + edge1 = face.Vertex1; + edge2 = face.Vertex2; + } else { + Assert(prevFaceIndex == face.Opposite2); + + nextFaceIndex0 = face.Opposite0; + nextFaceIndex1 = face.Opposite1; + + edge0 = face.Vertex1; + edge1 = face.Vertex2; + edge2 = face.Vertex0; + } + + if (!litFaces.Contains(nextFaceIndex0)) { + var oppositeFace = faces[nextFaceIndex0]; + + var dist = PointFaceDistance( + point, + points[oppositeFace.Vertex0], + oppositeFace); + + if (dist <= 0.0f) { + horizon.Add(new HorizonEdge { + Face = nextFaceIndex0, + Edge0 = edge0, + Edge1 = edge1, + }); + } else { + SearchHorizon(points, point, faceCount, nextFaceIndex0, oppositeFace); + } + } + + if (!litFaces.Contains(nextFaceIndex1)) { + var oppositeFace = faces[nextFaceIndex1]; + + var dist = PointFaceDistance( + point, + points[oppositeFace.Vertex0], + oppositeFace); + + if (dist <= 0.0f) { + horizon.Add(new HorizonEdge { + Face = nextFaceIndex1, + Edge0 = edge1, + Edge1 = edge2, + }); + } else { + SearchHorizon(points, point, faceCount, nextFaceIndex1, oppositeFace); + } + } + } + + /// + /// Remove all lit faces and construct new faces from the horizon in a + /// "cone-like" fashion. + /// + /// This is a relatively straight-forward procedure, given that the + /// horizon is handed to it in already sorted counter-clockwise. The + /// neighbors of the new faces are easy to find: they're the previous + /// and next faces to be constructed in the cone, as well as the face + /// on the other side of the horizon. We also have to update the face + /// on the other side of the horizon to reflect it's new neighbor from + /// the cone. + /// + void ConstructCone(List points, int farthestPoint) { + foreach (var fi in litFaces) { + Assert(faces.ContainsKey(fi)); + faces.Remove(fi); + } + + var firstNewFace = faceCount; + + // Check for coplanar faces + /*var firstNormal = new Vector3(0, 0, 0); + int sameNormals = 1; + for (int i = 0; i < horizon.Count; i++) + { + var v0 = farthestPoint; + var v1 = horizon[i].Edge0; + var v2 = horizon[i].Edge1; + var norm = Normal(points[v0], points[v1], points[v2]); + if (firstNormal.IsZero) + { + firstNormal = norm; + continue; + } + + const float tol = 0.1f; + if ((firstNormal - norm).Length < tol || (firstNormal - norm).Length > 2.0f-tol) + { + sameNormals++; + } + } + + if (sameNormals == horizon.Count) + sameNormals = sameNormals;*/ + + for (int i = 0; i < horizon.Count; i++) { + // Vertices of the new face, the farthest point as well as the + // edge on the horizon. Horizon edge is CCW, so the triangle + // should be as well. + var v0 = farthestPoint; + var v1 = horizon[i].Edge0; + var v2 = horizon[i].Edge1; + + // Opposite faces of the triangle. First, the edge on the other + // side of the horizon, then the next/prev faces on the new cone + var o0 = horizon[i].Face; + var o1 = (i == horizon.Count - 1) ? firstNewFace : firstNewFace + i + 1; + var o2 = (i == 0) ? (firstNewFace + horizon.Count - 1) : firstNewFace + i - 1; + + var fi = faceCount++; + + faces[fi] = new Face( + v0, v1, v2, + o0, o1, o2, + Normal(points[v0], points[v1], points[v2])); + + var horizonFace = faces[horizon[i].Face]; + + if (horizonFace.Vertex0 == v1) { + Assert(v2 == horizonFace.Vertex2); + horizonFace.Opposite1 = fi; + } else if (horizonFace.Vertex1 == v1) { + Assert(v2 == horizonFace.Vertex0); + horizonFace.Opposite2 = fi; + } else { + Assert(v1 == horizonFace.Vertex2); + Assert(v2 == horizonFace.Vertex1); + horizonFace.Opposite0 = fi; + } + + faces[horizon[i].Face] = horizonFace; + } + } + + /// + /// Reassign points based on the new faces added by ConstructCone(). + /// + /// Only points that were previous assigned to a removed face need to + /// be updated, so check litFaces while looping through the open set. + /// + /// There is a potential optimization here: there's no reason to loop + /// through the entire openSet here. If each face had it's own + /// openSet, we could just loop through the openSets in the removed + /// faces. That would make the loop here shorter. + /// + /// However, to do that, we would have to juggle A LOT more List's, + /// and we would need an object pool to manage them all without + /// generating a whole bunch of garbage. I don't think it's worth + /// doing that to make this loop shorter, a straight for-loop through + /// a list is pretty darn fast. Still, it might be worth trying + /// + void ReassignPoints(List points) { + for (int i = 0; i <= openSetTail; i++) { + var fp = openSet[i]; + + if (litFaces.Contains(fp.Face)) { + var assigned = false; + var point = points[fp.Point]; + + foreach (var kvp in faces) { + var fi = kvp.Key; + var face = kvp.Value; + + var dist = PointFaceDistance( + point, + points[face.Vertex0], + face); + + if (dist > EPSILON) { + assigned = true; + + fp.Face = fi; + fp.Distance = dist; + + openSet[i] = fp; + break; + } + } + + if (!assigned) { + // If point hasn't been assigned, then it's inside the + // convex hull. Swap it with openSetTail, and decrement + // openSetTail. We also have to decrement i, because + // there's now a new thing in openSet[i], so we need i + // to remain the same the next iteration of the loop. + fp.Face = INSIDE; + fp.Distance = float.NaN; + + openSet[i] = openSet[openSetTail]; + openSet[openSetTail] = fp; + + i--; + openSetTail--; + } + } + } + } + + /// + /// Final step in algorithm, export the faces of the convex hull in a + /// mesh-friendly format. + /// + /// TODO normals calculation for non-split vertices. Right now it just + /// leaves the normal array empty. + /// + void ExportMesh( + List points, + bool splitVerts, + ref List verts, + ref List tris, + ref List normals) + { + if (verts == null) { + verts = new List(); + } else { + verts.Clear(); + } + + if (tris == null) { + tris = new List(); + } else { + tris.Clear(); + } + + if (normals == null) { + normals = new List(); + } else { + normals.Clear(); + } + + foreach (var face in faces.Values) { + int vi0, vi1, vi2; + + if (splitVerts) { + vi0 = verts.Count; verts.Add(points[face.Vertex0]); + vi1 = verts.Count; verts.Add(points[face.Vertex1]); + vi2 = verts.Count; verts.Add(points[face.Vertex2]); + + normals.Add(face.Normal); + normals.Add(face.Normal); + normals.Add(face.Normal); + } else { + if (!hullVerts.TryGetValue(face.Vertex0, out vi0)) { + vi0 = verts.Count; + hullVerts[face.Vertex0] = vi0; + verts.Add(points[face.Vertex0]); + } + + if (!hullVerts.TryGetValue(face.Vertex1, out vi1)) { + vi1 = verts.Count; + hullVerts[face.Vertex1] = vi1; + verts.Add(points[face.Vertex1]); + } + + if (!hullVerts.TryGetValue(face.Vertex2, out vi2)) { + vi2 = verts.Count; + hullVerts[face.Vertex2] = vi2; + verts.Add(points[face.Vertex2]); + } + } + + tris.Add(vi0); + tris.Add(vi1); + tris.Add(vi2); + } + } + + /// + /// Signed distance from face to point (a positive number means that + /// the point is above the face) + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + float PointFaceDistance(Vector3 point, Vector3 pointOnFace, Face face) { + return Dot(face.Normal, point - pointOnFace); + } + + /// + /// Calculate normal for triangle + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + Vector3 Normal(Vector3 v0, Vector3 v1, Vector3 v2) { + return Cross(v1 - v0, v2 - v0).Normalized; + } + + /// + /// Dot product, for convenience. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + static float Dot(Vector3 a, Vector3 b) { + return a.X*b.X + a.Y*b.Y + a.Z*b.Z; + } + + /// + /// Vector3.Cross i left-handed, the algorithm is right-handed. Also, + /// i wanna test to see if using aggressive inlining makes any + /// difference here. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + static Vector3 Cross(Vector3 a, Vector3 b) { + return new Vector3( + a.Y*b.Z - a.Z*b.Y, + a.Z*b.X - a.X*b.Z, + a.X*b.Y - a.Y*b.X); + } + + /// + /// Check if two points are coincident + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + bool AreCoincident(Vector3 a, Vector3 b) { + return (a - b).Length <= EPSILON; + } + + /// + /// Check if three points are collinear + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + bool AreCollinear(Vector3 a, Vector3 b, Vector3 c) { + return Cross(c - a, c - b).Length <= EPSILON; + } + + /// + /// Check if four points are coplanar + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + bool AreCoplanar(Vector3 a, Vector3 b, Vector3 c, Vector3 d) { + var n1 = Cross(c - a, c - b); + var n2 = Cross(d - a, d - b); + + var m1 = n1.Length; + var m2 = n2.Length; + + return m1 <= EPSILON + || m2 <= EPSILON + || AreCollinear(Vector3.Zero, + (1.0f / m1) * n1, + (1.0f / m2) * n2); + } + + /// + /// Method used for debugging, verifies that the openSet is in a + /// sensible state. Conditionally compiled if DEBUG_QUICKHULL if + /// defined. + /// + void VerifyOpenSet(List points) { + for (int i = 0; i < openSet.Count; i++) { + if (i > openSetTail) { + Assert(openSet[i].Face == INSIDE); + } else { + Assert(openSet[i].Face != INSIDE); + Assert(openSet[i].Face != UNASSIGNED); + + Assert(PointFaceDistance( + points[openSet[i].Point], + points[faces[openSet[i].Face].Vertex0], + faces[openSet[i].Face]) > 0.0f); + } + } + } + + /// + /// Method used for debugging, verifies that the horizon is in a + /// sensible state. Conditionally compiled if DEBUG_QUICKHULL if + /// defined. + /// + void VerifyHorizon() { + for (int i = 0; i < horizon.Count; i++) { + var prev = i == 0 ? horizon.Count - 1 : i - 1; + + Assert(horizon[prev].Edge1 == horizon[i].Edge0); + Assert(HasEdge(faces[horizon[i].Face], horizon[i].Edge1, horizon[i].Edge0)); + } + } + + /// + /// Method used for debugging, verifies that the faces array is in a + /// sensible state. Conditionally compiled if DEBUG_QUICKHULL if + /// defined. + /// + void VerifyFaces(List points) { + foreach (var kvp in faces) { + var fi = kvp.Key; + var face = kvp.Value; + + Assert(faces.ContainsKey(face.Opposite0)); + Assert(faces.ContainsKey(face.Opposite1)); + Assert(faces.ContainsKey(face.Opposite2)); + + Assert(face.Opposite0 != fi); + Assert(face.Opposite1 != fi); + Assert(face.Opposite2 != fi); + + Assert(face.Vertex0 != face.Vertex1); + Assert(face.Vertex0 != face.Vertex2); + Assert(face.Vertex1 != face.Vertex2); + + Assert(HasEdge(faces[face.Opposite0], face.Vertex2, face.Vertex1)); + Assert(HasEdge(faces[face.Opposite1], face.Vertex0, face.Vertex2)); + Assert(HasEdge(faces[face.Opposite2], face.Vertex1, face.Vertex0)); + + Assert((face.Normal - Normal( + points[face.Vertex0], + points[face.Vertex1], + points[face.Vertex2])).Length < EPSILON); + } + } + + /// + /// Method used for debugging, verifies that the final mesh is + /// actually a convex hull of all the points. Conditionally compiled + /// if DEBUG_QUICKHULL if defined. + /// + void VerifyMesh(List points, ref List verts, ref List tris) { + Assert(tris.Count % 3 == 0); + + for (int i = 0; i < points.Count; i++) { + for (int j = 0; j < tris.Count; j+=3) { + var t0 = verts[tris[j]]; + var t1 = verts[tris[j + 1]]; + var t2 = verts[tris[j + 2]]; + + Assert(Dot(points[i] - t0, Vector3.Cross(t1 - t0, t2 - t0)) <= EPSILON); + } + + } + } + + /// + /// Does face f have a face with vertexes e0 and e1? Used only for + /// debugging. + /// + bool HasEdge(Face f, int e0, int e1) { + return (f.Vertex0 == e0 && f.Vertex1 == e1) + || (f.Vertex1 == e0 && f.Vertex2 == e1) + || (f.Vertex2 == e0 && f.Vertex0 == e1); + } + + /// + /// Assert method, conditionally compiled with DEBUG_QUICKHULL. + /// + /// I could just use Debug.Assert or the Assertions class, but I like + /// the idea of just writing Assert(something), and I also want it to + /// be conditionally compiled out with the same #define as the other + /// debug methods. + /// + static void Assert(bool condition) { + if (!condition) { + FlaxEngine.Assertions.Assert.Fail("Assertion failed"); + //throw new UnityEngine.Assertions.AssertionException("Assertion failed", ""); + } + } + } + + + + public class Q3MapImporter : Script + { + private string mapPath = @"C:\dev\GoakeFlax\Assets\Maps\cube.map"; + //private string mapPath = @"C:\dev\Goake\maps\aerowalk\aerowalk.map"; + + Model model; + public MaterialBase material; + + const float epsilon = 0.00001f; + + Vector3[] QuickHull(Vector3[] points) + { + var verts = new List(); + var tris = new List(); + var normals = new List(); + + var calc = new ConvexHullCalculator(); + calc.GenerateHull(points.ToList(), true, ref verts, ref tris, ref normals); + + var finalPoints = new List(); + + foreach (var tri in tris) + { + finalPoints.Add(verts[tri]); + } + + return MergeCoplanarFaces(finalPoints.ToArray()); + } + + Vector3[] MergeCoplanarFaces(Vector3[] points) + { + // degenerate faces + + return points; + } + + + public override void OnStart() + { + byte[] mapChars = File.ReadAllBytes(mapPath); + var root = MapParser.Parse(mapChars); + + const float cs = 3000f; + + Vector3[] cubePoints = new[] + { + new Vector3(-cs, -cs, -cs), + new Vector3(cs, -cs, -cs), + new Vector3(-cs, cs, -cs), + new Vector3(cs, cs, -cs), + new Vector3(-cs, -cs, cs), + new Vector3(cs, -cs, cs), + new Vector3(-cs, cs, cs), + new Vector3(cs, cs, cs), + }; + Vector3[] cubeVerts = QuickHull(cubePoints).ToArray(); + + List vertices = new List(); + foreach (var brush in root.entities[0].brushes.Take(1)) + { + List brushVertices = new List(cubeVerts); + //foreach (var plane in new [] { brush.planes.First() }) + foreach (var plane in brush.planes.Reverse().Take(1)) + //foreach (var plane in brush.planes) + { + Plane p = new Plane(plane.v1, plane.v2, plane.v3); + Vector3 planeNormal = p.Normal; + List newBrushVertices = new List(); + List faceVertices = new List(); + + if (true) + { + Func isFront = (f) => f > epsilon; + Func isBack = (f) => f < -epsilon; + + for (int i = 0; i < brushVertices.Count; i++) + { + int i2 = ((i + 1) % 3 == 0) ? (i - 2) : (i + 1); + Vector3 start = brushVertices[i]; + Vector3 end = brushVertices[i2]; + + var d1 = (start.X * planeNormal.X) + (start.Y * planeNormal.Y) + (start.Z * planeNormal.Z) + + p.D; + var d2 = (end.X * planeNormal.X) + (end.Y * planeNormal.Y) + (end.Z * planeNormal.Z) + p.D; + + if (isBack(d1)) + { + // include back + faceVertices.Add(start); + } + + if (isBack(d1) && isFront(d2) || isFront(d1) && isBack(d2)) + { + //if (isFront(d2)) + { + // include clip + Ray ray2 = new Ray(start, (end - start).Normalized); + if (p.Intersects(ref ray2, out Vector3 intersect2)) + faceVertices.Add(intersect2); + else + d1 = d1; + } + } + } + + if (true) + { + var newMeshPoints = new List(); + int duplis = 0; + foreach (var v in faceVertices) + { + bool found = false; + foreach (var vo in newMeshPoints) + { + if ((v - vo).Length < epsilon) + { + found = true; + duplis++; + break; + } + } + + //if (!newMeshPoints.Contains(v)) + if (!found) + newMeshPoints.Add(v); + } + + if (duplis > 0) + Console.Print("duplicates: " + duplis); + + if (newMeshPoints.Count > 0) + { + var hullPoints = QuickHull(newMeshPoints.ToArray()); + var ms = new MeshSimplifier(); + var optimizedVerts = ms.Simplify(hullPoints); + newBrushVertices.Clear(); + newBrushVertices.AddRange(hullPoints); + } + else + newBrushVertices.Clear(); + } + } + + Assert.IsTrue(newBrushVertices.Count % 3 == 0, + "invalid amount of vertices: " + newBrushVertices.Count); + //Assert.IsTrue(newBrushVertices.Count > 0, + // "brush was clipped completely, vertices: " + newBrushVertices.Count); + + brushVertices.Clear(); + brushVertices.AddRange(newBrushVertices); + Console.Print("plane verts: " + newBrushVertices.Count); + } + + vertices.AddRange(brushVertices); + } + + + model = Content.CreateVirtualAsset(); + model.SetupLODs(new int[] {1}); + { + var mesh = model.LODs[0].Meshes[0]; + List triangles = new List(vertices.Count); + for (int i = 0; i < vertices.Count; i++) + triangles.Add(i); + Console.Print("verts: " + vertices.Count); + mesh.UpdateMesh(vertices.ToArray(), triangles.ToArray(), vertices.ToArray()); + } + + StaticModel childModel = Actor.AddChild(); + childModel.Name = "MapModel"; + childModel.Model = model; + childModel.SetMaterial(0, material); + } + + public override void OnEnable() + { + // Here you can add code that needs to be called when script is enabled (eg. register for events) + } + + public override void OnDisable() + { + // Here you can add code that needs to be called when script is disabled (eg. unregister from events) + } + + public override void OnUpdate() + { + // Here you can add code that needs to be called every frame + } + + public override void OnDestroy() + { + Destroy(ref model); + base.OnDestroy(); + } + } + +#if false public struct Edge { public Vector3 v1, v2; @@ -616,7 +1899,7 @@ namespace Game } // merge faces with similar normals - /*List discardedFaces = new List(); + List discardedFaces = new List(); for (int i = 0; i < hullFaces.Count; i++) { Face firstFace = hullFaces[i]; @@ -630,7 +1913,7 @@ namespace Game } foreach (var f in discardedFaces) - hullFaces.Remove(f);*/ + hullFaces.Remove(f); List hullPoints = new List(hullFaces.Count * 3); foreach (var face in hullFaces) @@ -1338,7 +2621,7 @@ namespace Game { List brushVertices = new List(cubeVerts); //foreach (var plane in new [] { brush.planes.First() }) - foreach (var plane in brush.planes.Reverse().Take(2)) + foreach (var plane in brush.planes.Reverse().Take(1)) //foreach (var plane in brush.planes) { Plane p = new Plane(plane.v1, plane.v2, plane.v3); @@ -1469,4 +2752,5 @@ namespace Game base.OnDestroy(); } } +#endif } \ No newline at end of file