using System; using System.Collections.Generic; using UnityEngine; namespace ProceduralToolkit.Skeleton { /// /// A straight skeleton generator, computes a straight skeleton for the input polygon, reusable. /// The generation algorithm is loosely based on the work of Tom Kelly /// (2014) "Unwritten procedural modeling with the straight skeleton" /// http://www.twak.co.uk/2014/02/unwritten-procedural-modeling-with.html /// public class StraightSkeletonGenerator { private readonly List activePlans = new List(); private readonly List newPlans = new List(); private StraightSkeleton skeleton; public StraightSkeleton Generate(IReadOnlyList polygon) { activePlans.Clear(); newPlans.Clear(); var activePlan = new Plan(polygon); activePlans.Add(activePlan); skeleton = new StraightSkeleton(activePlan); foreach (var p in activePlans) { foreach (var vertex in p) { CalculateBisector(vertex); } } int count = 0; while (activePlans.Count > 0) { if (count > polygon.Count) { Debug.LogError("Too many iterations"); break; } bool success = OffsetAndDetectEvents(); if (!success) { return skeleton; } count++; } return skeleton; } public List GetActivePlans() { return activePlans; } private bool OffsetAndDetectEvents() { ResetInEventFlags(); foreach (var plan in activePlans) { float offset = FindSmallestOffset(plan); if (float.IsPositiveInfinity(offset) || offset <= 0) { Debug.LogError("Unable to find offset"); return false; } plan.Offset(-offset); var intersectionEvents = FindIntersectionEvents(plan); foreach (var intersectionEvent in intersectionEvents) { ProcessIntersectionEvent(plan, intersectionEvent); } newPlans.AddRange(plan.Split()); } activePlans.AddRange(newPlans); newPlans.Clear(); CleanupLoops(); activePlans.RemoveAll(p => p.Count == 0); skeleton.ValidatePolygons(); return true; } private void ResetInEventFlags() { foreach (var plan in activePlans) { foreach (var vertex in plan) { vertex.inEvent = false; } } } private float FindSmallestOffset(Plan plan) { float smallestOffset = float.PositiveInfinity; foreach (var vertex in plan) { if (BisectorBisector(vertex, vertex.next, out float offset)) { smallestOffset = Mathf.Min(smallestOffset, offset); } if (vertex.reflect) { foreach (var segmentA in plan) { var segmentB = segmentA.next; if (segmentA == vertex || segmentB == vertex) { continue; } if (BisectorSegment(vertex, segmentA.position, segmentB.position, out offset)) { smallestOffset = Mathf.Min(smallestOffset, offset); } } } } return smallestOffset; } private List FindIntersectionEvents(Plan plan) { var intersectionEvents = new List(); var vertex = plan.First; do { CreateSplitVertices(plan, vertex); var intersectionEvent = FindIntersectionEvent(vertex); if (intersectionEvent != null) { intersectionEvents.Add(intersectionEvent); if (intersectionEvent.chains[0].Count == plan.Count) { return intersectionEvents; } } vertex = vertex.next; } while (vertex != plan.First); CompressEvents(intersectionEvents); return intersectionEvents; } private void CreateSplitVertices(Plan plan, Plan.Vertex searchVertex) { var vertex = searchVertex; do { if (vertex != searchVertex && vertex.next != searchVertex && PointSegment(searchVertex.position, vertex.position, vertex.next.position)) { var newVertex = CreateSplitVertex(plan, searchVertex.position, vertex, vertex.next); vertex = newVertex.next; } else { vertex = vertex.next; } } while (vertex != searchVertex); } private Plan.Vertex CreateSplitVertex(Plan plan, Vector2 position, Plan.Vertex segmentA, Plan.Vertex segmentB) { var newVertex = new Plan.Vertex(position) { previousPolygonIndex = segmentA.nextPolygonIndex, nextPolygonIndex = segmentB.previousPolygonIndex }; plan.Insert(newVertex, segmentA, segmentB); CalculateBisector(newVertex); return newVertex; } private IntersectionEvent FindIntersectionEvent(Plan.Vertex searchVertex) { var vertex = searchVertex; do { if (!vertex.inEvent && vertex != searchVertex && IncidentalVertices(searchVertex, vertex)) { var intersectionEvent = new IntersectionEvent(searchVertex.position); ExpandEvent(intersectionEvent, searchVertex); return intersectionEvent; } vertex = vertex.next; } while (vertex != searchVertex); return null; } private void ExpandEvent(IntersectionEvent intersectionEvent, Plan.Vertex startVertex) { var vertex = startVertex; do { if (!vertex.inEvent && IncidentalVertices(intersectionEvent.position, vertex.position)) { var chain = FindCollapsedChain(vertex); intersectionEvent.chains.Add(chain); vertex = chain[chain.Count - 1].next; } else { vertex = vertex.next; } } while (vertex != startVertex); } private List FindCollapsedChain(Plan.Vertex startVertex) { startVertex.inEvent = true; var chain = new List {startVertex}; var vertex = startVertex; do { if (!vertex.next.inEvent && IncidentalVertices(vertex, vertex.next)) { vertex.next.inEvent = true; chain.Add(vertex.next); vertex = vertex.next; } else { break; } } while (vertex != startVertex); return chain; } private void CompressEvents(List intersectionEvents) { foreach (var intersectionEvent in intersectionEvents) { var firstChain = intersectionEvent.chains[0]; var lastChain = intersectionEvent.chains[intersectionEvent.chains.Count - 1]; var lastVertex = lastChain[lastChain.Count - 1]; if (lastVertex.next == firstChain[0]) { lastChain.AddRange(firstChain); intersectionEvent.chains[0] = lastChain; firstChain.Clear(); intersectionEvent.chains.RemoveAt(intersectionEvent.chains.Count - 1); } } } private void ProcessIntersectionEvent(Plan plan, IntersectionEvent intersectionEvent) { foreach (var chain in intersectionEvent.chains) { SimplifyChain(plan, chain); } intersectionEvent.chains.RemoveAll(c => c.Count == 0); if (intersectionEvent.chains.Count >= 2) { for (var i = 0; i < intersectionEvent.chains.Count; i++) { var chain = intersectionEvent.chains[i]; var previousChain = intersectionEvent.chains.GetLooped(i - 1); var vertex = chain[0]; plan.Remove(vertex); skeleton.AddVertex(vertex); var newVertex = new Plan.Vertex(intersectionEvent.position) { previousPolygonIndex = vertex.previousPolygonIndex, nextPolygonIndex = previousChain[0].nextPolygonIndex }; plan.Insert(newVertex, vertex.previous, previousChain[0].next); CalculateBisector(newVertex); } } } private void SimplifyChain(Plan plan, List chain) { if (chain.Count == 1) return; var first = chain[0]; var previous = first.previous; var last = chain[chain.Count - 1]; var next = last.next; foreach (var vertex in chain) { plan.Remove(vertex); skeleton.AddVertex(vertex); } chain.Clear(); if (next != first) { var newVertex = new Plan.Vertex(first.position) { previousPolygonIndex = first.previousPolygonIndex, nextPolygonIndex = last.nextPolygonIndex, }; plan.Insert(newVertex, previous, next); chain.Add(newVertex); CalculateBisector(newVertex); } } private void CleanupLoops() { foreach (var plan in activePlans) { if (plan.Count == 1) { Debug.LogError("Invalid plan: " + plan.First); } else if (plan.Count == 2) { var vertex = plan.First; plan.Remove(vertex); plan.Remove(vertex.next); } } } private static void CalculateBisector(Plan.Vertex vertex) { Plan.Vertex previous = vertex.previous; Plan.Vertex next = vertex.next; Vector2 direction = Geometry.GetAngleBisector(previous.position, vertex.position, next.position, out float angle); vertex.angle = angle; vertex.bisector = direction; } private static bool PointSegment(Vector2 point, Vector2 segmentA, Vector2 segmentB) { if (!IncidentalVertices(point, segmentA) && !IncidentalVertices(point, segmentB)) { return Intersect.PointSegment(point, segmentA, segmentB); } return false; } private static bool BisectorBisector(Plan.Vertex vertexA, Plan.Vertex vertexB, out float offset) { if (Intersect.RayRay(vertexA.position, vertexA.bisector, vertexB.position, vertexB.bisector, out var intersection)) { if (intersection.type == IntersectionType.Point) { float offsetA = GetBisectorBisectorOffset(vertexA, intersection.pointA); float offsetB = GetBisectorBisectorOffset(vertexB, intersection.pointA); offset = Mathf.Min(offsetA, offsetB); return true; } if (intersection.type == IntersectionType.Segment) { float toIntersection = Vector2.Distance(vertexA.position, vertexB.position); float offsetA = GetBisectorBisectorOffset(vertexA, toIntersection)/2; float offsetB = GetBisectorBisectorOffset(vertexB, toIntersection)/2; offset = Mathf.Min(offsetA, offsetB); return true; } Debug.LogErrorFormat("Invalid bisector intersection\ntype: {0}\npointA: {1} pointB: {2}\nbisectorA: {3} bisectorB:{4}", intersection.type, intersection.pointA, intersection.pointB, vertexA.bisector, vertexB.bisector); offset = 0; return false; } offset = 0; return false; } private static float GetBisectorBisectorOffset(Plan.Vertex vertex, Vector2 intersection) { float toIntersection = Vector2.Distance(vertex.position, intersection); return GetBisectorBisectorOffset(vertex, toIntersection); } private static float GetBisectorBisectorOffset(Plan.Vertex vertex, float toIntersection) { return toIntersection*Geometry.GetAngleBisectorSin(vertex.angle); } private static bool BisectorSegment(Plan.Vertex vertex, Vector2 segmentA, Vector2 segmentB, out float offset) { if (Intersect.RaySegment(vertex.position, vertex.bisector, segmentA, segmentB, out var intersection)) { if (intersection.type == IntersectionType.Point) { Vector2 segmentDirection = (segmentB - segmentA).normalized; float toIntersection = Vector2.Distance(vertex.position, intersection.pointA); float intersectionAngle = Vector2.Angle(vertex.bisector, segmentDirection); float intersectionSin = Mathf.Sin(intersectionAngle*Mathf.Deg2Rad); float bisectorSin = Geometry.GetAngleBisectorSin(vertex.angle); offset = toIntersection/(1/intersectionSin + 1/bisectorSin); return true; } Debug.LogErrorFormat("Invalid bisector intersection\ntype: {0}\npointA: {1} pointB: {2}\nray: {3} segmentA:{4} segmentB:{5}", intersection.type, intersection.pointA, intersection.pointB, vertex.bisector, segmentA, segmentB); offset = 0; return false; } offset = 0; return false; } private static bool IncidentalVertices(Plan.Vertex a, Plan.Vertex b) { return IncidentalVertices(a.position, b.position); } private static bool IncidentalVertices(Vector2 a, Vector2 b) { return (a - b).sqrMagnitude < Geometry.Epsilon; } private class IntersectionEvent { public readonly Vector2 position; public readonly List> chains = new List>(); public IntersectionEvent(Vector2 position) { this.position = position; } public override string ToString() { return string.Format("{0} Count: {1}", position, chains.Count); } } } }