OutlineGenerator.cs 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377
  1. using System;
  2. using System.Collections.Generic;
  3. using UnityEngine;
  4. using UnityEditor.U2D.Common;
  5. using UnityEditor.U2D.Animation.ClipperLib;
  6. using UnityEditor.U2D.Sprites;
  7. namespace UnityEditor.U2D.Animation
  8. {
  9. using Path = List<IntPoint>;
  10. using Paths = List<List<IntPoint>>;
  11. internal class OutlineGenerator : IOutlineGenerator
  12. {
  13. const double kClipperScale = 1000.0;
  14. private const float kEpsilon = 1.2e-12f;
  15. private const float kMinLinearizeDistance = 5f;
  16. private Texture2D m_CurrentTexture;
  17. private Rect m_CurrentRect;
  18. private byte m_CurrentAlphaTolerance;
  19. public void GenerateOutline(ITextureDataProvider textureDataProvider, Rect rect, float detail, byte alphaTolerance, bool holeDetection, out Vector2[][] paths)
  20. {
  21. if (alphaTolerance >= 255)
  22. throw new ArgumentException("Alpha tolerance should be lower than 255");
  23. m_CurrentTexture = textureDataProvider.GetReadableTexture2D();
  24. m_CurrentRect = rect;
  25. m_CurrentAlphaTolerance = alphaTolerance;
  26. InternalEditorBridge.GenerateOutline(textureDataProvider.texture, rect, 1f, alphaTolerance, holeDetection, out paths);
  27. if (paths.Length > 0)
  28. {
  29. ClipPaths(ref paths);
  30. Debug.Assert(paths.Length > 0);
  31. var rectSizeMagnitude = rect.size.magnitude;
  32. var minDistance = Mathf.Max(rectSizeMagnitude / 10f, kMinLinearizeDistance);
  33. var maxDistance = Mathf.Max(rectSizeMagnitude / 100f, kMinLinearizeDistance);
  34. var distance = Mathf.Lerp(minDistance, maxDistance, detail);
  35. for (var pathIndex = 0; pathIndex < paths.Length; ++pathIndex)
  36. {
  37. var pathLength = CalculatePathLength(paths[pathIndex]);
  38. if (pathLength > distance)
  39. {
  40. var newPath = Linearize(new List<Vector2>(paths[pathIndex]), distance);
  41. if (newPath.Count > 3)
  42. paths[pathIndex] = newPath.ToArray();
  43. SmoothPath(paths[pathIndex], 5, 0.1f, 135f);
  44. }
  45. }
  46. ClipPaths(ref paths);
  47. }
  48. }
  49. private void ClipPaths(ref Vector2[][] paths)
  50. {
  51. Debug.Assert(paths.Length > 0);
  52. var subj = ToClipper(paths);
  53. var solution = new Paths();
  54. var clipper = new Clipper(Clipper.ioPreserveCollinear);
  55. clipper.AddPaths(subj, PolyType.ptSubject, true);
  56. clipper.Execute(ClipType.ctUnion, solution, PolyFillType.pftPositive, PolyFillType.pftPositive);
  57. FilterNestedPaths(solution);
  58. paths = ToVector2(solution);
  59. }
  60. private void FilterNestedPaths(Paths paths)
  61. {
  62. var filtered = new List<Path>(paths);
  63. for (var i = 0; i < paths.Count; ++i)
  64. {
  65. var path = paths[i];
  66. if (!filtered.Contains(path))
  67. continue;
  68. for (var j = i + 1; j < paths.Count; ++j)
  69. {
  70. if (!filtered.Contains(path))
  71. continue;
  72. var other = paths[j];
  73. if (IsPathContainedInOtherPath(path, other))
  74. {
  75. filtered.Remove(path);
  76. break;
  77. }
  78. else if (IsPathContainedInOtherPath(other, path))
  79. filtered.Remove(other);
  80. }
  81. }
  82. paths.Clear();
  83. paths.AddRange(filtered);
  84. }
  85. private bool IsPathContainedInOtherPath(Path path, Path other)
  86. {
  87. foreach (var p in path)
  88. {
  89. if (Clipper.PointInPolygon(p, other) < 1)
  90. return false;
  91. }
  92. return true;
  93. }
  94. private Paths ToClipper(Vector2[][] paths)
  95. {
  96. return new Paths(Array.ConvertAll(paths, p => ToClipper(p)));
  97. }
  98. private Path ToClipper(Vector2[] path)
  99. {
  100. return new Path(Array.ConvertAll(path, p => new IntPoint(p.x * kClipperScale, p.y * kClipperScale)));
  101. }
  102. private Vector2[][] ToVector2(Paths paths)
  103. {
  104. return paths.ConvertAll(p => ToVector2(p)).ToArray();
  105. }
  106. private Vector2[] ToVector2(Path path)
  107. {
  108. return path.ConvertAll(p => new Vector2((float)(p.X / kClipperScale), (float)(p.Y / kClipperScale))).ToArray();
  109. }
  110. private float CalculatePathLength(Vector2[] path)
  111. {
  112. var sum = 0f;
  113. for (var i = 0; i < path.Length; i++)
  114. {
  115. var nextIndex = NextIndex(i, path.Length);
  116. var p0 = path[i];
  117. var p1 = path[nextIndex];
  118. sum += Vector2.Distance(p0, p1);
  119. }
  120. return sum;
  121. }
  122. //Adapted from https://github.com/burningmime/curves
  123. private List<Vector2> Linearize(List<Vector2> src, float pointDistance)
  124. {
  125. if (src == null) throw new ArgumentNullException("src");
  126. if (pointDistance <= kEpsilon) throw new InvalidOperationException("pointDistance " + pointDistance + " is less than epislon " + kEpsilon);
  127. var dst = new List<Vector2>();
  128. if (src.Count > 0)
  129. {
  130. var accDistance = 0f;
  131. var lastIndex = 0;
  132. var lastPoint = src[0];
  133. dst.Add(lastPoint);
  134. for (var i = 0; i < src.Count; i++)
  135. {
  136. var nextIndex = NextIndex(i, src.Count);
  137. var p0 = src[i];
  138. var p1 = src[nextIndex];
  139. var edgeDistance = Vector2.Distance(p0, p1);
  140. if (accDistance + edgeDistance > pointDistance || nextIndex == 0)
  141. {
  142. var partialDistance = pointDistance - accDistance;
  143. var newPoint = Vector2.Lerp(p0, p1, partialDistance / edgeDistance);
  144. var remainingDistance = edgeDistance - partialDistance;
  145. //Roll back until we do not intersect any pixel
  146. var step = 1f;
  147. bool finish = false;
  148. while (!finish && IsLineOverImage(newPoint, lastPoint))
  149. {
  150. partialDistance = Vector2.Distance(p0, newPoint) - step;
  151. while (partialDistance < 0f)
  152. {
  153. if (i > lastIndex + 1)
  154. {
  155. accDistance -= edgeDistance;
  156. --i;
  157. p1 = p0;
  158. p0 = src[i];
  159. edgeDistance = Vector2.Distance(p0, p1);
  160. partialDistance += edgeDistance;
  161. }
  162. else
  163. {
  164. partialDistance = 0f;
  165. finish = true;
  166. }
  167. remainingDistance = edgeDistance - partialDistance;
  168. }
  169. newPoint = Vector2.Lerp(p0, p1, partialDistance / edgeDistance);
  170. }
  171. Debug.Assert(lastIndex <= i, "Generate Outline failed");
  172. nextIndex = NextIndex(i, src.Count);
  173. if (nextIndex != 0 || !EqualsOrClose(newPoint, p1))
  174. {
  175. dst.Add(newPoint);
  176. lastPoint = newPoint;
  177. lastIndex = i;
  178. }
  179. while (remainingDistance > pointDistance)
  180. {
  181. remainingDistance -= pointDistance;
  182. newPoint = Vector2.Lerp(p0, p1, (edgeDistance - remainingDistance) / edgeDistance);
  183. if (!EqualsOrClose(newPoint, lastPoint))
  184. {
  185. dst.Add(newPoint);
  186. lastPoint = newPoint;
  187. }
  188. }
  189. accDistance = remainingDistance;
  190. }
  191. else
  192. {
  193. accDistance += edgeDistance;
  194. }
  195. }
  196. }
  197. return dst;
  198. }
  199. private bool EqualsOrClose(Vector2 v1, Vector2 v2)
  200. {
  201. return (v1 - v2).sqrMagnitude < kEpsilon;
  202. }
  203. private void SmoothPath(Vector2[] path, int iterations, float velocity, float minAngle)
  204. {
  205. Debug.Assert(iterations > 0f);
  206. Debug.Assert(minAngle >= 0f);
  207. Debug.Assert(minAngle < 180f);
  208. var cosTolerance = Mathf.Cos(minAngle * Mathf.Deg2Rad);
  209. for (int iteration = 0; iteration < iterations; ++iteration)
  210. for (int i = 0; i < path.Length; ++i)
  211. {
  212. var prevPoint = path[PreviousIndex(i, path.Length)];
  213. var point = path[i];
  214. var nextPoint = path[NextIndex(i, path.Length)];
  215. var t1 = prevPoint - point;
  216. var t2 = nextPoint - point;
  217. var dot = Vector2.Dot(t1.normalized, t2.normalized);
  218. if (dot > cosTolerance)
  219. continue;
  220. var w1 = 1f / (point - prevPoint).magnitude;
  221. var w2 = 1f / (point - nextPoint).magnitude;
  222. var laplacian = (w1 * prevPoint + w2 * nextPoint) / (w1 + w2) - point;
  223. point += laplacian * velocity;
  224. if (!IsLineOverImage(point, nextPoint) && !IsLineOverImage(point, prevPoint))
  225. path[i] = point;
  226. }
  227. }
  228. private Vector2Int ToVector2Int(Vector2 v)
  229. {
  230. return new Vector2Int(Mathf.RoundToInt(v.x), Mathf.RoundToInt(v.y));
  231. }
  232. private bool IsLineOverImage(Vector2 pointA, Vector2 pointB)
  233. {
  234. var pointAInt = ToVector2Int(pointA);
  235. var pointBInt = ToVector2Int(pointB);
  236. if (IsPointInRectEdge(pointA) && IsPointInRectEdge(pointB) && (pointAInt.x == pointBInt.x || pointAInt.y == pointBInt.y))
  237. return false;
  238. foreach (var point in GetPointsOnLine(pointAInt.x, pointAInt.y, pointBInt.x, pointBInt.y))
  239. {
  240. if (IsPointOverImage(point))
  241. return true;
  242. }
  243. return false;
  244. }
  245. private bool IsPointOverImage(Vector2 point)
  246. {
  247. Debug.Assert(m_CurrentTexture != null);
  248. point += m_CurrentRect.center;
  249. return m_CurrentTexture.GetPixel((int)point.x, (int)point.y).a * 255 > m_CurrentAlphaTolerance;
  250. }
  251. private bool IsPointInRectEdge(Vector2 point)
  252. {
  253. point += m_CurrentRect.center;
  254. var pointInt = ToVector2Int(point);
  255. var minInt = ToVector2Int(m_CurrentRect.min);
  256. var maxInt = ToVector2Int(m_CurrentRect.max);
  257. return minInt.x >= pointInt.x || maxInt.x <= pointInt.x || minInt.y >= pointInt.y || maxInt.y <= pointInt.y;
  258. }
  259. //From http://ericw.ca/notes/bresenhams-line-algorithm-in-csharp.html
  260. private IEnumerable<Vector2Int> GetPointsOnLine(int x0, int y0, int x1, int y1)
  261. {
  262. bool steep = Mathf.Abs(y1 - y0) > Math.Abs(x1 - x0);
  263. if (steep)
  264. {
  265. int t;
  266. t = x0; // swap x0 and y0
  267. x0 = y0;
  268. y0 = t;
  269. t = x1; // swap x1 and y1
  270. x1 = y1;
  271. y1 = t;
  272. }
  273. if (x0 > x1)
  274. {
  275. int t;
  276. t = x0; // swap x0 and x1
  277. x0 = x1;
  278. x1 = t;
  279. t = y0; // swap y0 and y1
  280. y0 = y1;
  281. y1 = t;
  282. }
  283. int dx = x1 - x0;
  284. int dy = Mathf.Abs(y1 - y0);
  285. int error = dx / 2;
  286. int ystep = (y0 < y1) ? 1 : -1;
  287. int y = y0;
  288. for (int x = x0; x <= x1; x++)
  289. {
  290. yield return new Vector2Int((steep ? y : x), (steep ? x : y));
  291. error = error - dy;
  292. if (error < 0)
  293. {
  294. y += ystep;
  295. error += dx;
  296. }
  297. }
  298. yield break;
  299. }
  300. private int NextIndex(int index, int pointCount)
  301. {
  302. return Mod(index + 1, pointCount);
  303. }
  304. private int PreviousIndex(int index, int pointCount)
  305. {
  306. return Mod(index - 1, pointCount);
  307. }
  308. private int Mod(int x, int m)
  309. {
  310. int r = x % m;
  311. return r < 0 ? r + m : r;
  312. }
  313. }
  314. }