UltimateZoneBuilder/Source/Geometry/EarClipTriangulator.cs

654 lines
19 KiB
C#

#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.Direct3D;
using System.Drawing;
using CodeImp.DoomBuilder.Map;
#endregion
namespace CodeImp.DoomBuilder.Geometry
{
/// <summary>
/// Responsible for creating and caching sector polygons.
/// Performs triangulation of sectors by using ear clipping.
/// </summary>
/// See: http://www.geometrictools.com/Documentation/TriangulationByEarClipping.pdf
public sealed class EarClipTriangulator : Triangulator
{
#region ================== Delegates
// For debugging purpose only!
// These are not called in a release build
public delegate void ShowLine(Vector2D v1, Vector2D v2, PixelColor c);
public delegate void ShowPolygon(Polygon p, PixelColor c);
public delegate void ShowPoint(Vector2D v, int c);
public delegate void ShowEarClip(EarClipVertex[] found, LinkedList<EarClipVertex> remaining);
// For debugging purpose only!
// These are not called in a release build
public ShowLine OnShowLine;
public ShowPolygon OnShowPolygon;
public ShowPoint OnShowPoint;
public ShowEarClip OnShowEarClip;
#endregion
#region ================== Constants
#endregion
#region ================== Variables
#endregion
#region ================== Properties
#endregion
#region ================== Constructor / Disposer
// Constructor
public EarClipTriangulator()
{
// Initialize
// We have no destructor
GC.SuppressFinalize(this);
}
// Diposer
public override void Dispose()
{
// Not already disposed?
if(!isdisposed)
{
// Clean up
// Done
base.Dispose();
}
}
#endregion
#region ================== Methods
// This triangulates a sector and stores it
protected override void PerformTriangulation(Sector sector)
{
TriangleList triangles = new TriangleList();
List<Polygon> polys;
/*
* 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(sector);
// CUTTING
DoCutting(polys);
// EAR-CLIPPING
foreach(Polygon p in polys) triangles.AddRange(DoEarClip(p));
// STORE
base.StoreTriangles(sector, triangles);
}
#endregion
#region ================== Tracing
// This traces sector lines to create a polygon tree
private List<Polygon> DoTrace(Sector s)
{
Dictionary<Sidedef, bool> todosides = new Dictionary<Sidedef, bool>(s.Sidedefs.Count);
List<Polygon> root = new List<Polygon>();
TracePath path;
Polygon 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);
// Trace to find a polygon
path = DoTracePath(new TracePath(), start, null, s, todosides);
// If tracing is not possible (sector not closed?)
// then leave with what we have up till now
if(path == null) break;
// 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(Polygon 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 TracePath DoTracePath(TracePath history, Vertex fromhere, Vertex findme, Sector sector, Dictionary<Sidedef, bool> sides)
{
TracePath nextpath;
TracePath result;
Vertex nextvertex;
List<Sidedef> allsides;
SidedefAngleSorter sorter;
// 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<Sidedef>(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.
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 TracePath(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<Sidedef, bool> todosides, ICollection<Sidedef> 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<Sidedef, bool> sides)
{
Vertex found = General.GetByIndex<Sidedef>(sides.Keys, 0).Line.Start;
// Go for all sides to find the right-most side
foreach(KeyValuePair<Sidedef, bool> sd in sides)
{
// Check if more to the right than the last found
if(sd.Key.Line.Start.X > found.X) found = sd.Key.Line.Start;
if(sd.Key.Line.End.X > found.X) 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<Polygon> polys)
{
Queue<Polygon> todo = new Queue<Polygon>(polys);
// Begin processing outer polygons
while(todo.Count > 0)
{
// Get outer polygon to process
Polygon p = todo.Dequeue();
// Any inner polygons to work with?
if(p.Children.Count > 0)
{
// Go for all the children
foreach(Polygon 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(Polygon 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(Polygon p)
{
LinkedList<Polygon> todo = new LinkedList<Polygon>(p.Children);
LinkedListNode<EarClipVertex> start;
LinkedListNode<Polygon> ip;
LinkedListNode<Polygon> found;
LinkedListNode<EarClipVertex> 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<EarClipVertex> FindRightMostVertex(Polygon p)
{
LinkedListNode<EarClipVertex> found = p.First;
LinkedListNode<EarClipVertex> 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<EarClipVertex> start, Polygon p, Polygon inner)
{
Line2D starttoright = new Line2D(start.Value.Position, start.Value.Position + new Vector2D(1000.0f, 0.0f));
LinkedListNode<EarClipVertex> v1, v2;
LinkedListNode<EarClipVertex> insertbefore = null;
float u, ul, foundu = float.MaxValue;
EarClipVertex split;
// Go for all lines in the outer polygon
v1 = p.Last;
v2 = p.First;
while(v2 != null)
{
// Check if the line is to the right of start
if((v1.Value.Position.x > start.Value.Position.x) ||
(v2.Value.Position.x > start.Value.Position.x))
{
// 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)
{
// 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) u = float.MaxValue;
if(ul < 0) ul = float.MaxValue;
// Choose closest of both vertices
if((u < ul) && (u < foundu))
{
insertbefore = v2;
foundu = u;
}
else if((u > ul) && (ul < foundu))
{
insertbefore = v2;
foundu = ul;
}
}
}
// Found a closer match?
else if((ul >= 0) && (ul <= 1) && (u > 0) && (u < foundu))
{
// Found a closer intersection
insertbefore = v2;
foundu = u;
}
}
// Next
v1 = v2;
v2 = v2.Next;
}
// Found anything?
if(insertbefore != null)
{
// Find the position where we have to split the outer polygon
split = new EarClipVertex(starttoright.GetCoordinatesAt(foundu));
// Insert manual split vertices
p.AddBefore(insertbefore, new EarClipVertex(split));
// 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));
p.AddBefore(insertbefore, new EarClipVertex(split));
}
else
{
throw new Exception("Can't find an edge for polygon cutting!");
}
}
#endregion
#region ================== Ear Clipping
// This clips a polygon and returns the triangles
// The polygon may not have any holes or islands
private TriangleList DoEarClip(Polygon poly)
{
LinkedList<EarClipVertex> verts = new LinkedList<EarClipVertex>();
List<EarClipVertex> convexes = new List<EarClipVertex>(poly.Count);
LinkedList<EarClipVertex> reflexes = new LinkedList<EarClipVertex>();
LinkedList<EarClipVertex> eartips = new LinkedList<EarClipVertex>();
TriangleList result = new TriangleList();
EarClipVertex v, v1, v2;
EarClipVertex[] t, t1, t2;
// Go for all vertices to fill list
foreach(EarClipVertex vec in poly)
vec.SetVertsLink(verts.AddLast(vec));
// Optimization: Vertices which have lines with the
// same angle are useless. Remove them!
v = verts.First.Value;
while(v != null)
{
// Get the next vertex
if(v.MainListNode.Next != null) v1 = v.MainListNode.Next.Value; else v1 = null;
// Get triangle for v
t = GetTriangle(v);
// 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
v.Remove();
}
// Next!
v = v1;
}
// 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 a valid ear
t = GetTriangle(cv);
if(CheckValidEar(t, reflexes)) cv.AddEarTip(eartips);
}
// Process ears until done
while((eartips.Count > 0) && (verts.Count > 2))
{
// Get next ear
v = eartips.First.Value;
t = GetTriangle(v);
// Add ear as triangle
result.Add(t);
// Remove this ear from all lists
v.Remove();
v1 = t[0];
v2 = t[2];
#if DEBUG
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();
}
// Return result
return result;
}
// This checks if a given ear is a valid (no intersections from reflex vertices)
private bool CheckValidEar(EarClipVertex[] t, LinkedList<EarClipVertex> reflexes)
{
// Go for all reflex vertices
foreach(EarClipVertex rv in reflexes)
{
// Return false on intersection
if(PointInsideTriangle(t, rv.Position) &&
(rv != t[0]) && (rv != t[1]) && (rv != t[2])) 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];
if(v.MainListNode.Previous == null) t[0] = v.MainListNode.List.Last.Value; else t[0] = v.MainListNode.Previous.Value;
t[1] = v;
if(v.MainListNode.Next == null) t[2] = v.MainListNode.List.First.Value; else t[2] = 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.00001f);
return (Line2D.GetSideOfLine(t[0].Position, t[2].Position, t[1].Position) < 0);
}
// This checks if a point is inside a triangle
// NOTE: vertices in t must be in clockwise order!
private bool PointInsideTriangle(EarClipVertex[] t, Vector2D p)
{
return (Line2D.GetSideOfLine(t[0].Position, t[1].Position, p) < 0.00001f) &&
(Line2D.GetSideOfLine(t[1].Position, t[2].Position, p) < 0.00001f) &&
(Line2D.GetSideOfLine(t[2].Position, t[0].Position, p) < 0.00001f);
}
#endregion
}
}