123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377 |
- using System;
- using System.Collections.Generic;
- using UnityEngine;
- using UnityEditor.U2D.Common;
- using UnityEditor.U2D.Animation.ClipperLib;
- using UnityEditor.U2D.Sprites;
- namespace UnityEditor.U2D.Animation
- {
- using Path = List<IntPoint>;
- using Paths = List<List<IntPoint>>;
- internal class OutlineGenerator : IOutlineGenerator
- {
- const double kClipperScale = 1000.0;
- private const float kEpsilon = 1.2e-12f;
- private const float kMinLinearizeDistance = 5f;
- private Texture2D m_CurrentTexture;
- private Rect m_CurrentRect;
- private byte m_CurrentAlphaTolerance;
- public void GenerateOutline(ITextureDataProvider textureDataProvider, Rect rect, float detail, byte alphaTolerance, bool holeDetection, out Vector2[][] paths)
- {
- if (alphaTolerance >= 255)
- throw new ArgumentException("Alpha tolerance should be lower than 255");
- m_CurrentTexture = textureDataProvider.GetReadableTexture2D();
- m_CurrentRect = rect;
- m_CurrentAlphaTolerance = alphaTolerance;
- InternalEditorBridge.GenerateOutline(textureDataProvider.texture, rect, 1f, alphaTolerance, holeDetection, out paths);
- if (paths.Length > 0)
- {
- ClipPaths(ref paths);
- Debug.Assert(paths.Length > 0);
- var rectSizeMagnitude = rect.size.magnitude;
- var minDistance = Mathf.Max(rectSizeMagnitude / 10f, kMinLinearizeDistance);
- var maxDistance = Mathf.Max(rectSizeMagnitude / 100f, kMinLinearizeDistance);
- var distance = Mathf.Lerp(minDistance, maxDistance, detail);
- for (var pathIndex = 0; pathIndex < paths.Length; ++pathIndex)
- {
- var pathLength = CalculatePathLength(paths[pathIndex]);
- if (pathLength > distance)
- {
- var newPath = Linearize(new List<Vector2>(paths[pathIndex]), distance);
- if (newPath.Count > 3)
- paths[pathIndex] = newPath.ToArray();
- SmoothPath(paths[pathIndex], 5, 0.1f, 135f);
- }
- }
- ClipPaths(ref paths);
- }
- }
- private void ClipPaths(ref Vector2[][] paths)
- {
- Debug.Assert(paths.Length > 0);
- var subj = ToClipper(paths);
- var solution = new Paths();
- var clipper = new Clipper(Clipper.ioPreserveCollinear);
- clipper.AddPaths(subj, PolyType.ptSubject, true);
- clipper.Execute(ClipType.ctUnion, solution, PolyFillType.pftPositive, PolyFillType.pftPositive);
- FilterNestedPaths(solution);
- paths = ToVector2(solution);
- }
- private void FilterNestedPaths(Paths paths)
- {
- var filtered = new List<Path>(paths);
- for (var i = 0; i < paths.Count; ++i)
- {
- var path = paths[i];
- if (!filtered.Contains(path))
- continue;
- for (var j = i + 1; j < paths.Count; ++j)
- {
- if (!filtered.Contains(path))
- continue;
- var other = paths[j];
- if (IsPathContainedInOtherPath(path, other))
- {
- filtered.Remove(path);
- break;
- }
- else if (IsPathContainedInOtherPath(other, path))
- filtered.Remove(other);
- }
- }
- paths.Clear();
- paths.AddRange(filtered);
- }
- private bool IsPathContainedInOtherPath(Path path, Path other)
- {
- foreach (var p in path)
- {
- if (Clipper.PointInPolygon(p, other) < 1)
- return false;
- }
- return true;
- }
- private Paths ToClipper(Vector2[][] paths)
- {
- return new Paths(Array.ConvertAll(paths, p => ToClipper(p)));
- }
- private Path ToClipper(Vector2[] path)
- {
- return new Path(Array.ConvertAll(path, p => new IntPoint(p.x * kClipperScale, p.y * kClipperScale)));
- }
- private Vector2[][] ToVector2(Paths paths)
- {
- return paths.ConvertAll(p => ToVector2(p)).ToArray();
- }
- private Vector2[] ToVector2(Path path)
- {
- return path.ConvertAll(p => new Vector2((float)(p.X / kClipperScale), (float)(p.Y / kClipperScale))).ToArray();
- }
- private float CalculatePathLength(Vector2[] path)
- {
- var sum = 0f;
- for (var i = 0; i < path.Length; i++)
- {
- var nextIndex = NextIndex(i, path.Length);
- var p0 = path[i];
- var p1 = path[nextIndex];
- sum += Vector2.Distance(p0, p1);
- }
- return sum;
- }
- //Adapted from https://github.com/burningmime/curves
- private List<Vector2> Linearize(List<Vector2> src, float pointDistance)
- {
- if (src == null) throw new ArgumentNullException("src");
- if (pointDistance <= kEpsilon) throw new InvalidOperationException("pointDistance " + pointDistance + " is less than epislon " + kEpsilon);
- var dst = new List<Vector2>();
- if (src.Count > 0)
- {
- var accDistance = 0f;
- var lastIndex = 0;
- var lastPoint = src[0];
- dst.Add(lastPoint);
- for (var i = 0; i < src.Count; i++)
- {
- var nextIndex = NextIndex(i, src.Count);
- var p0 = src[i];
- var p1 = src[nextIndex];
- var edgeDistance = Vector2.Distance(p0, p1);
- if (accDistance + edgeDistance > pointDistance || nextIndex == 0)
- {
- var partialDistance = pointDistance - accDistance;
- var newPoint = Vector2.Lerp(p0, p1, partialDistance / edgeDistance);
- var remainingDistance = edgeDistance - partialDistance;
- //Roll back until we do not intersect any pixel
- var step = 1f;
- bool finish = false;
- while (!finish && IsLineOverImage(newPoint, lastPoint))
- {
- partialDistance = Vector2.Distance(p0, newPoint) - step;
- while (partialDistance < 0f)
- {
- if (i > lastIndex + 1)
- {
- accDistance -= edgeDistance;
- --i;
- p1 = p0;
- p0 = src[i];
- edgeDistance = Vector2.Distance(p0, p1);
- partialDistance += edgeDistance;
- }
- else
- {
- partialDistance = 0f;
- finish = true;
- }
- remainingDistance = edgeDistance - partialDistance;
- }
- newPoint = Vector2.Lerp(p0, p1, partialDistance / edgeDistance);
- }
- Debug.Assert(lastIndex <= i, "Generate Outline failed");
- nextIndex = NextIndex(i, src.Count);
- if (nextIndex != 0 || !EqualsOrClose(newPoint, p1))
- {
- dst.Add(newPoint);
- lastPoint = newPoint;
- lastIndex = i;
- }
- while (remainingDistance > pointDistance)
- {
- remainingDistance -= pointDistance;
- newPoint = Vector2.Lerp(p0, p1, (edgeDistance - remainingDistance) / edgeDistance);
- if (!EqualsOrClose(newPoint, lastPoint))
- {
- dst.Add(newPoint);
- lastPoint = newPoint;
- }
- }
- accDistance = remainingDistance;
- }
- else
- {
- accDistance += edgeDistance;
- }
- }
- }
- return dst;
- }
- private bool EqualsOrClose(Vector2 v1, Vector2 v2)
- {
- return (v1 - v2).sqrMagnitude < kEpsilon;
- }
- private void SmoothPath(Vector2[] path, int iterations, float velocity, float minAngle)
- {
- Debug.Assert(iterations > 0f);
- Debug.Assert(minAngle >= 0f);
- Debug.Assert(minAngle < 180f);
- var cosTolerance = Mathf.Cos(minAngle * Mathf.Deg2Rad);
- for (int iteration = 0; iteration < iterations; ++iteration)
- for (int i = 0; i < path.Length; ++i)
- {
- var prevPoint = path[PreviousIndex(i, path.Length)];
- var point = path[i];
- var nextPoint = path[NextIndex(i, path.Length)];
- var t1 = prevPoint - point;
- var t2 = nextPoint - point;
- var dot = Vector2.Dot(t1.normalized, t2.normalized);
- if (dot > cosTolerance)
- continue;
- var w1 = 1f / (point - prevPoint).magnitude;
- var w2 = 1f / (point - nextPoint).magnitude;
- var laplacian = (w1 * prevPoint + w2 * nextPoint) / (w1 + w2) - point;
- point += laplacian * velocity;
- if (!IsLineOverImage(point, nextPoint) && !IsLineOverImage(point, prevPoint))
- path[i] = point;
- }
- }
- private Vector2Int ToVector2Int(Vector2 v)
- {
- return new Vector2Int(Mathf.RoundToInt(v.x), Mathf.RoundToInt(v.y));
- }
- private bool IsLineOverImage(Vector2 pointA, Vector2 pointB)
- {
- var pointAInt = ToVector2Int(pointA);
- var pointBInt = ToVector2Int(pointB);
- if (IsPointInRectEdge(pointA) && IsPointInRectEdge(pointB) && (pointAInt.x == pointBInt.x || pointAInt.y == pointBInt.y))
- return false;
- foreach (var point in GetPointsOnLine(pointAInt.x, pointAInt.y, pointBInt.x, pointBInt.y))
- {
- if (IsPointOverImage(point))
- return true;
- }
- return false;
- }
- private bool IsPointOverImage(Vector2 point)
- {
- Debug.Assert(m_CurrentTexture != null);
- point += m_CurrentRect.center;
- return m_CurrentTexture.GetPixel((int)point.x, (int)point.y).a * 255 > m_CurrentAlphaTolerance;
- }
- private bool IsPointInRectEdge(Vector2 point)
- {
- point += m_CurrentRect.center;
- var pointInt = ToVector2Int(point);
- var minInt = ToVector2Int(m_CurrentRect.min);
- var maxInt = ToVector2Int(m_CurrentRect.max);
- return minInt.x >= pointInt.x || maxInt.x <= pointInt.x || minInt.y >= pointInt.y || maxInt.y <= pointInt.y;
- }
- //From http://ericw.ca/notes/bresenhams-line-algorithm-in-csharp.html
- private IEnumerable<Vector2Int> GetPointsOnLine(int x0, int y0, int x1, int y1)
- {
- bool steep = Mathf.Abs(y1 - y0) > Math.Abs(x1 - x0);
- if (steep)
- {
- int t;
- t = x0; // swap x0 and y0
- x0 = y0;
- y0 = t;
- t = x1; // swap x1 and y1
- x1 = y1;
- y1 = t;
- }
- if (x0 > x1)
- {
- int t;
- t = x0; // swap x0 and x1
- x0 = x1;
- x1 = t;
- t = y0; // swap y0 and y1
- y0 = y1;
- y1 = t;
- }
- int dx = x1 - x0;
- int dy = Mathf.Abs(y1 - y0);
- int error = dx / 2;
- int ystep = (y0 < y1) ? 1 : -1;
- int y = y0;
- for (int x = x0; x <= x1; x++)
- {
- yield return new Vector2Int((steep ? y : x), (steep ? x : y));
- error = error - dy;
- if (error < 0)
- {
- y += ystep;
- error += dx;
- }
- }
- yield break;
- }
- private int NextIndex(int index, int pointCount)
- {
- return Mod(index + 1, pointCount);
- }
- private int PreviousIndex(int index, int pointCount)
- {
- return Mod(index - 1, pointCount);
- }
- private int Mod(int x, int m)
- {
- int r = x % m;
- return r < 0 ? r + m : r;
- }
- }
- }
|