#region ================== Copyright (c) 2007 Pascal vd Heiden /* * Copyright (c) 2007 Pascal vd Heiden, www.codeimp.com * This program is released under GNU General Public License * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * */ #endregion #region ================== Namespaces using System; using System.Collections; using System.Collections.Generic; using System.Globalization; using System.Text; using CodeImp.DoomBuilder.Geometry; using CodeImp.DoomBuilder.Rendering; using SlimDX.Direct3D9; using System.Drawing; using CodeImp.DoomBuilder.Map; using System.Collections.ObjectModel; using CodeImp.DoomBuilder.IO; #endregion namespace CodeImp.DoomBuilder.Geometry { /// /// Responsible for creating sector polygons. /// Performs triangulation of sectors by using ear clipping. /// public sealed class Triangulation { #region ================== Delegates #if DEBUG // For debugging purpose only! // These are not called in a release build public delegate void ShowPolygon(LinkedList p); public delegate void ShowEarClip(EarClipVertex[] found, LinkedList remaining); public delegate void ShowRemaining(LinkedList remaining); // For debugging purpose only! // These are not called in a release build public ShowPolygon OnShowPolygon; public ShowEarClip OnShowEarClip; public ShowRemaining OnShowRemaining; #endif #endregion #region ================== Constants #endregion #region ================== Variables // Number of vertices per island private ReadOnlyCollection islandvertices; // Vertices that result from the triangulation, 3 per triangle. private ReadOnlyCollection vertices; // These sidedefs match with the vertices. If a vertex is not the start // along a sidedef, this list contains a null entry for that vertex. private ReadOnlyCollection sidedefs; // Temporary array for the sidedefs deserialization private int[] sidedefindices; #endregion #region ================== Properties public ReadOnlyCollection IslandVertices { get { return islandvertices; } } public ReadOnlyCollection Vertices { get { return vertices; } } public ReadOnlyCollection Sidedefs { get { return sidedefs; } } #endregion #region ================== Constructor / Disposer // Constructor public static Triangulation Create(Sector sector) { Triangulation t = new Triangulation(); t.Triangulate(sector); return t; } // Constructor public Triangulation() { islandvertices = Array.AsReadOnly(new int[0]); vertices = Array.AsReadOnly(new Vector2D[0]); sidedefs = Array.AsReadOnly(new Sidedef[0]); } // This performs the triangulation public void Triangulate(Sector s) { // Initialize List polys; List islandslist = new List(); List verticeslist = new List(); List sidedefslist = new List(); // We have no destructor GC.SuppressFinalize(this); /* * This process is divided into several steps: * * 1) Tracing the sector lines to find clockwise outer polygons * and counter-clockwise inner polygons. These are arranged in a * polygon tree for the next step. * * 2) Cutting the inner polygons to make a flat list of only * outer polygons. * * 3) Ear-clipping the polygons to create triangles. * */ // TRACING polys = DoTrace(s); // CUTTING DoCutting(polys); // EAR-CLIPPING foreach(EarClipPolygon p in polys) islandslist.Add(DoEarClip(p, verticeslist, sidedefslist)); // Make arrays islandvertices = Array.AsReadOnly(islandslist.ToArray()); vertices = Array.AsReadOnly(verticeslist.ToArray()); sidedefs = Array.AsReadOnly(sidedefslist.ToArray()); } #endregion #region ================== Serialization // Serialize / deserialize internal void ReadWrite(IReadWriteStream s) { if(s.IsWriting) { s.wInt(islandvertices.Count); for(int i = 0; i < islandvertices.Count; i++) s.wInt(islandvertices[i]); s.wInt(vertices.Count); for(int i = 0; i < vertices.Count; i++) s.wVector2D(vertices[i]); s.wInt(sidedefs.Count); for(int i = 0; i < sidedefs.Count; i++) { if(sidedefs[i] != null) s.wInt(sidedefs[i].SerializedIndex); else s.wInt(-1); } } else { int c; s.rInt(out c); int[] islandverticeslist = new int[c]; for(int i = 0; i < c; i++) s.rInt(out islandverticeslist[i]); islandvertices = Array.AsReadOnly(islandverticeslist); s.rInt(out c); Vector2D[] verticeslist = new Vector2D[c]; for(int i = 0; i < c; i++) s.rVector2D(out verticeslist[i]); vertices = Array.AsReadOnly(verticeslist); s.rInt(out c); sidedefindices = new int[c]; for(int i = 0; i < c; i++) s.rInt(out sidedefindices[i]); } } // After deserialization we need to find the actual sidedefs back internal void PostDeserialize(MapSet map) { // Find our sidedefs List sides = new List(sidedefindices.Length); for(int i = 0; i < sidedefindices.Length; i++) { if(sidedefindices[i] >= 0) sides.Add(map.SidedefIndices[sidedefindices[i]]); else sides.Add(null); } // We don't need this array any longer sidedefindices = null; // Keep readonly array sidedefs = Array.AsReadOnly(sides.ToArray()); } #endregion #region ================== Tracing // This traces sector lines to create a polygon tree private List DoTrace(Sector s) { Dictionary todosides = new Dictionary(s.Sidedefs.Count); Dictionary ignores = new Dictionary(); List root = new List(); SidedefsTracePath path; EarClipPolygon newpoly; Vertex start; // Fill the dictionary // The bool value is used to indicate lines which has been visited in the trace foreach(Sidedef sd in s.Sidedefs) todosides.Add(sd, false); // First remove all sides that refer to the same sector on both sides of the line RemoveDoubleSidedefReferences(todosides, s.Sidedefs); // Continue until all sidedefs have been processed while(todosides.Count > 0) { // Reset all visited indicators foreach(Sidedef sd in s.Sidedefs) if(todosides.ContainsKey(sd)) todosides[sd] = false; // Find the right-most vertex to start a trace with. // This guarantees that we start out with an outer polygon and we just // have to check if it is inside a previously found polygon. start = FindRightMostVertex(todosides, ignores); // No more possible start vertex found? // Then leave with what we have up till now. if(start == null) break; // Trace to find a polygon path = DoTracePath(new SidedefsTracePath(), start, null, s, todosides); // If tracing is not possible (sector not closed?) // then add the start to the ignore list and try again later if(path == null) { // Ignore vertex as start ignores.Add(start, start); } else { // Remove the sides found in the path foreach(Sidedef sd in path) todosides.Remove(sd); // Create the polygon newpoly = path.MakePolygon(); // Determine where this polygon goes in our tree foreach(EarClipPolygon p in root) { // Insert if it belongs as a child if(p.InsertChild(newpoly)) { // Done newpoly = null; break; } } // Still not inserted in our tree? if(newpoly != null) { // Then add it at root level as outer polygon newpoly.Inner = false; root.Add(newpoly); } } } // Return result return root; } // This recursively traces a path // Returns the resulting TracePath when the search is complete // or returns null when no path found. private SidedefsTracePath DoTracePath(SidedefsTracePath history, Vertex fromhere, Vertex findme, Sector sector, Dictionary sides) { SidedefsTracePath nextpath; SidedefsTracePath result; Vertex nextvertex; List allsides; // Found the vertex we are tracing to? if(fromhere == findme) return history; // On the first run, findme is null (otherwise the trace would end // immeditely when it starts) so set findme here on the first run. if(findme == null) findme = fromhere; // Make a list of sides referring to the same sector allsides = new List(fromhere.Linedefs.Count * 2); foreach(Linedef l in fromhere.Linedefs) { // Should we go along the front or back side? // This is very important for clockwise polygon orientation! if(l.Start == fromhere) { // Front side of line connected to sector? if((l.Front != null) && (l.Front.Sector == sector)) { // Visit here when not visited yet if(sides.ContainsKey(l.Front) && !sides[l.Front]) allsides.Add(l.Front); } } else { // Back side of line connected to sector? if((l.Back != null) && (l.Back.Sector == sector)) { // Visit here when not visited yet if(sides.ContainsKey(l.Back) && !sides[l.Back]) allsides.Add(l.Back); } } } // Previous line available? if(history.Count > 0) { // This is done to ensure the tracing works along vertices that are shared by // more than 2 lines/sides of the same sector. We must continue tracing along // the first next smallest delta angle! This sorts the smallest delta angle to // the top of the list. SidedefAngleSorter sorter = new SidedefAngleSorter(history[history.Count - 1], fromhere); allsides.Sort(sorter); } // Go for all lines connected to this vertex foreach(Sidedef s in allsides) { // Mark sidedef as visited and move to next vertex sides[s] = true; nextpath = new SidedefsTracePath(history, s); if(s.Line.Start == fromhere) nextvertex = s.Line.End; else nextvertex = s.Line.Start; result = DoTracePath(nextpath, nextvertex, findme, sector, sides); if(result != null) return result; } // Nothing found return null; } // This removes all sidedefs which has a sidedefs on the other side // of the same line that refers to the same sector. These are removed // because they are useless and make the triangulation inefficient. private void RemoveDoubleSidedefReferences(Dictionary todosides, ICollection sides) { // Go for all sides foreach(Sidedef sd in sides) { // Double sided? if(sd.Other != null) { // Referring to the same sector on both sides? if(sd.Sector == sd.Other.Sector) { // Remove this one todosides.Remove(sd); } } } } // This finds the right-most vertex to start tracing with private Vertex FindRightMostVertex(Dictionary sides, Dictionary ignores) { Vertex found = null; // Go for all sides to find the right-most side foreach(KeyValuePair sd in sides) { // First found? if((found == null) && !ignores.ContainsKey(sd.Key.Line.Start)) found = sd.Key.Line.Start; if((found == null) && !ignores.ContainsKey(sd.Key.Line.End)) found = sd.Key.Line.End; // Compare? if(found != null) { // Check if more to the right than the previous found if((sd.Key.Line.Start.Position.x > found.Position.x) && !ignores.ContainsKey(sd.Key.Line.Start)) found = sd.Key.Line.Start; if((sd.Key.Line.End.Position.x > found.Position.x) && !ignores.ContainsKey(sd.Key.Line.End)) found = sd.Key.Line.End; } } // Return result return found; } #endregion #region ================== Cutting // This cuts into outer polygons to solve inner polygons and make the polygon tree flat private void DoCutting(List polys) { Queue todo = new Queue(polys); // Begin processing outer polygons while(todo.Count > 0) { // Get outer polygon to process EarClipPolygon p = todo.Dequeue(); // Any inner polygons to work with? if(p.Children.Count > 0) { // Go for all the children foreach(EarClipPolygon c in p.Children) { // The children of the children are outer polygons again, // so move them to the root and add for processing polys.AddRange(c.Children); foreach(EarClipPolygon sc in c.Children) todo.Enqueue(sc); // Remove from inner polygon c.Children.Clear(); } // Now do some cutting on this polygon to merge the inner polygons MergeInnerPolys(p); } } } // This takes an outer polygon and a set of inner polygons to start cutting on private void MergeInnerPolys(EarClipPolygon p) { LinkedList todo = new LinkedList(p.Children); LinkedListNode start; LinkedListNode ip; LinkedListNode found; LinkedListNode foundstart; // Continue until no more inner polygons to process while(todo.Count > 0) { // Find the inner polygon with the highest x vertex found = null; foundstart = null; ip = todo.First; while(ip != null) { start = FindRightMostVertex(ip.Value); if((foundstart == null) || (start.Value.Position.x > foundstart.Value.Position.x)) { // Found a better start found = ip; foundstart = start; } // Next! ip = ip.Next; } // Remove from todo list todo.Remove(found); // Get cut start and end SplitOuterWithInner(foundstart, p, found.Value); } // Remove the children, they should be merged in the polygon by now p.Children.Clear(); } // This finds the right-most vertex in an inner polygon to use for cut startpoint. private LinkedListNode FindRightMostVertex(EarClipPolygon p) { LinkedListNode found = p.First; LinkedListNode v = found.Next; // Go for all vertices to find the on with the biggest x value while(v != null) { if(v.Value.Position.x > found.Value.Position.x) found = v; v = v.Next; } // Return result return found; } // This finds the cut coordinates and splits the other poly with inner vertices private void SplitOuterWithInner(LinkedListNode start, EarClipPolygon p, EarClipPolygon inner) { LinkedListNode v1, v2; LinkedListNode insertbefore = null; float u, ul, bonus, foundu = float.MaxValue; EarClipVertex split; // Create a line from start that goes beyond the right most vertex of p LinkedListNode pr = FindRightMostVertex(p); float startx = start.Value.Position.x; float endx = pr.Value.Position.x + 10.0f; Line2D starttoright = new Line2D(start.Value.Position, new Vector2D(endx, start.Value.Position.y)); // Calculate a small bonus (half mappixel) bonus = starttoright.GetNearestOnLine(new Vector2D(start.Value.Position.x + 0.5f, start.Value.Position.y)); // Go for all lines in the outer polygon v1 = p.Last; v2 = p.First; while(v2 != null) { // Check if the line goes between startx and endx if(((v1.Value.Position.x > startx) || (v2.Value.Position.x > startx)) && ((v1.Value.Position.x < endx) || (v2.Value.Position.x < endx))) { // Find intersection Line2D pl = new Line2D(v1.Value.Position, v2.Value.Position); pl.GetIntersection(starttoright, out u, out ul); if(float.IsNaN(u)) { // We have found a line that is perfectly horizontal // (parallel to the cut scan line) Check if the line // is overlapping the cut scan line. if(v1.Value.Position.y == start.Value.Position.y) { // This is an exceptional situation which causes a bit of a problem, because // this could be a previously made cut, which overlaps another line from the // same cut and we have to determine which of the two we will join with. If we // pick the wrong one, the polygon is no longer valid and triangulation will fail. // Calculate distance of each vertex in units u = starttoright.GetNearestOnLine(v1.Value.Position); ul = starttoright.GetNearestOnLine(v2.Value.Position); // Rule out vertices before the scan line if(u < 0.0f) u = float.MaxValue; if(ul < 0.0f) ul = float.MaxValue; // v2 must be closer, because we must cut in so that it stays a clockwise polygon // We give a small bonus to ensure this choice is preferred over the other lines // that end in the same location if((ul < u) && ((ul - bonus) < foundu)) { insertbefore = v2.Next ?? v2.List.First; foundu = (ul - bonus); } } } // Found a closer match? else if((ul >= 0.0f) && (ul <= 1.0f) && (u > 0.0f) && (u <= foundu)) { // Found a closer intersection insertbefore = v2; foundu = u; } } // Next v1 = v2; v2 = v2.Next; } // Found anything? if(insertbefore != null) { Sidedef sd = (insertbefore.Previous == null) ? insertbefore.List.Last.Value.Sidedef : insertbefore.Previous.Value.Sidedef; // Find the position where we have to split the outer polygon split = new EarClipVertex(starttoright.GetCoordinatesAt(foundu), null); // Insert manual split vertices p.AddBefore(insertbefore, new EarClipVertex(split, sd)); // Start inserting from the start (do I make sense this time?) v1 = start; do { // Insert inner polygon vertex p.AddBefore(insertbefore, new EarClipVertex(v1.Value)); if(v1.Next != null) v1 = v1.Next; else v1 = v1.List.First; } while(v1 != start); // Insert manual split vertices p.AddBefore(insertbefore, new EarClipVertex(start.Value, sd)); if(split.Position != insertbefore.Value.Position) p.AddBefore(insertbefore, new EarClipVertex(split, sd)); } } #endregion #region ================== Ear Clipping // This clips a polygon and returns the triangles // The polygon may not have any holes or islands /// See: http://www.geometrictools.com/Documentation/TriangulationByEarClipping.pdf private int DoEarClip(EarClipPolygon poly, List verticeslist, List sidedefslist) { LinkedList verts = new LinkedList(); List convexes = new List(poly.Count); LinkedList reflexes = new LinkedList(); LinkedList eartips = new LinkedList(); LinkedListNode n1, n2; EarClipVertex v, v1, v2; EarClipVertex[] t, t1, t2; int countvertices = 0; // Go for all vertices to fill list foreach(EarClipVertex vec in poly) vec.SetVertsLink(verts.AddLast(vec)); // Remove any zero-length lines, these will give problems n1 = verts.First; do { // Continue until adjacent zero-length lines are removed n2 = n1.Next ?? verts.First; Vector2D d = n1.Value.Position - n2.Value.Position; while((Math.Abs(d.x) < 0.00001f) && (Math.Abs(d.y) < 0.00001f)) { n2.Value.Remove(); n2 = n1.Next ?? verts.First; if(n2 != null) d = n1.Value.Position - n2.Value.Position; else break; } // Next! n1 = n2; } while(n1 != verts.First); // Optimization: Vertices which have lines with the // same angle are useless. Remove them! n1 = verts.First; while(n1 != null) { // Get the next vertex n2 = n1.Next; // Get triangle for v t = GetTriangle(n1.Value); // Check if both lines have the same angle Line2D a = new Line2D(t[0].Position, t[1].Position); Line2D b = new Line2D(t[1].Position, t[2].Position); if(Math.Abs(Angle2D.Difference(a.GetAngle(), b.GetAngle())) < 0.00001f) { // Same angles, remove vertex n1.Value.Remove(); } // Next! n1 = n2; } // Go for all vertices to determine reflex or convex foreach(EarClipVertex vv in verts) { // Add to reflex or convex list if(IsReflex(GetTriangle(vv))) vv.AddReflex(reflexes); else convexes.Add(vv); } // Go for all convex vertices to see if they are ear tips foreach(EarClipVertex cv in convexes) { // Add when this is a valid ear t = GetTriangle(cv); if(CheckValidEar(t, reflexes)) cv.AddEarTip(eartips); } #if DEBUG if(OnShowPolygon != null) OnShowPolygon(verts); #endif // Process ears until done while((eartips.Count > 0) && (verts.Count > 2)) { // Get next ear v = eartips.First.Value; t = GetTriangle(v); // Only save this triangle when it has an area if(TriangleHasArea(t)) { // Add ear as triangle AddTriangleToList(t, verticeslist, sidedefslist, (verts.Count == 3)); countvertices += 3; } // Remove this ear from all lists v.Remove(); v1 = t[0]; v2 = t[2]; #if DEBUG if(TriangleHasArea(t)) { if(OnShowEarClip != null) OnShowEarClip(t, verts); } #endif // Test first neighbour t1 = GetTriangle(v1); if(IsReflex(t1)) { // List as reflex if not listed yet if(!v1.IsReflex) v1.AddReflex(reflexes); v1.RemoveEarTip(); } else { // Remove from reflexes v1.RemoveReflex(); } // Test second neighbour t2 = GetTriangle(v2); if(IsReflex(t2)) { // List as reflex if not listed yet if(!v2.IsReflex) v2.AddReflex(reflexes); v2.RemoveEarTip(); } else { // Remove from reflexes v2.RemoveReflex(); } // Check if any neightbour have become a valid or invalid ear if(!v1.IsReflex && CheckValidEar(t1, reflexes)) v1.AddEarTip(eartips); else v1.RemoveEarTip(); if(!v2.IsReflex && CheckValidEar(t2, reflexes)) v2.AddEarTip(eartips); else v2.RemoveEarTip(); } #if DEBUG if(OnShowRemaining != null) OnShowRemaining(verts); #endif // Dispose remaining vertices foreach(EarClipVertex ecv in verts) ecv.Dispose(); // Return the number of vertices in the result return countvertices; } // This checks if a given ear is a valid (no intersections from reflex vertices) private bool CheckValidEar(EarClipVertex[] t, LinkedList reflexes) { // Go for all reflex vertices foreach(EarClipVertex rv in reflexes) { // Not one of the triangle corners? if((rv.Position != t[0].Position) && (rv.Position != t[1].Position) && (rv.Position != t[2].Position)) { // Return false on intersection if(PointInsideTriangle(t, rv.MainListNode)) return false; } } // Valid ear! return true; } // This returns the 3-vertex array triangle for an ear private EarClipVertex[] GetTriangle(EarClipVertex v) { EarClipVertex[] t = new EarClipVertex[3]; t[0] = (v.MainListNode.Previous == null) ? v.MainListNode.List.Last.Value : v.MainListNode.Previous.Value; t[1] = v; t[2] = (v.MainListNode.Next == null) ? v.MainListNode.List.First.Value : v.MainListNode.Next.Value; return t; } // This checks if a vertex is reflex (corner > 180 deg) or convex (corner < 180 deg) private bool IsReflex(EarClipVertex[] t) { // Return true when corner is > 180 deg return (Line2D.GetSideOfLine(t[0].Position, t[2].Position, t[1].Position) < 0.0f); } // This checks if a point is inside a triangle // When the point is on an edge of the triangle, it depends on the lines // adjacent to the point if it is considered inside or not // NOTE: vertices in t must be in clockwise order! private bool PointInsideTriangle(EarClipVertex[] t, LinkedListNode p) { // If the triangle has no area, there can never be a point inside if(TriangleHasArea(t)) { float lineside01 = Line2D.GetSideOfLine(t[0].Position, t[1].Position, p.Value.Position); float lineside12 = Line2D.GetSideOfLine(t[1].Position, t[2].Position, p.Value.Position); float lineside20 = Line2D.GetSideOfLine(t[2].Position, t[0].Position, p.Value.Position); // If any of the lineside results are 0 then that means the point lies on that edge and we // need to test if the lines adjacent to the point are in the triangle or not. // If the lines are intersecting the triangle, we also consider the point inside. if((lineside01 == 0.0f) || (lineside12 == 0.0f) || (lineside20 == 0.0f)) { LinkedListNode p1 = p.Previous ?? p.List.Last; LinkedListNode p2 = p.Next ?? p.List.First; if(LineInsideTriangle(t, p.Value.Position, p1.Value.Position)) return true; if(LineInsideTriangle(t, p.Value.Position, p2.Value.Position)) return true; return false; } else { return (lineside01 < 0.0f) && (lineside12 < 0.0f) && (lineside20 < 0.0f); } } else { return false; } } // This checks if a line is inside a triangle (touching the triangle is allowed) // NOTE: Does NOT check if p1 is inside the triangle, because we only use the // method when point-in-triangle is already tested for p1 private bool LineInsideTriangle(EarClipVertex[] t, Vector2D p1, Vector2D p2) { // Test if p2 is inside the triangle if((Line2D.GetSideOfLine(t[0].Position, t[1].Position, p2) < 0.0f) && (Line2D.GetSideOfLine(t[1].Position, t[2].Position, p2) < 0.0f) && (Line2D.GetSideOfLine(t[2].Position, t[0].Position, p2) < 0.0f)) { // Line is inside triangle, because p2 is return true; } else { // Test for line intersections Line2D p = new Line2D(p1, p2); Line2D t01 = new Line2D(t[0].Position, t[1].Position); Line2D t12 = new Line2D(t[1].Position, t[2].Position); Line2D t20 = new Line2D(t[2].Position, t[0].Position); float pu, pt; // Test intersections t01.GetIntersection(p, out pu, out pt); if(!float.IsNaN(pu) && (pu > 0.0f) && (pu < 1.0f) && (pt > 0.0f) && (pt < 1.0f)) return true; t12.GetIntersection(p, out pu, out pt); if(!float.IsNaN(pu) && (pu > 0.0f) && (pu < 1.0f) && (pt > 0.0f) && (pt < 1.0f)) return true; t20.GetIntersection(p, out pu, out pt); return !float.IsNaN(pu) && (pu > 0.0f) && (pu < 1.0f) && (pt > 0.0f) && (pt < 1.0f); } } // This checks if the triangle has an area greater than 0 private bool TriangleHasArea(EarClipVertex[] t) { return ((t[0].Position.x * (t[1].Position.y - t[2].Position.y) + t[1].Position.x * (t[2].Position.y - t[0].Position.y) + t[2].Position.x * (t[0].Position.y - t[1].Position.y)) != 0.0f); } // This adds an array of vertices private void AddTriangleToList(EarClipVertex[] triangle, List verticeslist, List sidedefslist, bool last) { // Create triangle verticeslist.Add(triangle[0].Position); sidedefslist.Add(triangle[0].Sidedef); verticeslist.Add(triangle[1].Position); sidedefslist.Add(triangle[1].Sidedef); verticeslist.Add(triangle[2].Position); if(!last) sidedefslist.Add(null); else sidedefslist.Add(triangle[2].Sidedef); // Modify the first earclipvertex of this triangle, it no longer lies along a sidedef triangle[0].Sidedef = null; } #endregion } }