Ellipse.cginc 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283
  1. float4 _ArcAngles; // min, max, use_arc, invert_arc in radians from 0 - PI2
  2. float4 _InnerRadii; // just x & y
  3. // gets distances to the edge of the shape, and then distances to outer_blur, outline and inner_blur
  4. // from the edge of the shape
  5. float4 ellipse_distances(float theta, float2 radii) {
  6. float2 rad_outer_blur = radii;
  7. float2 rad_outer = rad_outer_blur - _OuterBlur;
  8. float2 rad_inner = rad_outer - _OutlineSize;
  9. float2 rad_inner_blur = rad_inner - _InnerBlur;
  10. // get distances from the edge of the shape
  11. float sqrSinTheta = pow(sin(theta), 2);
  12. float sqrCosTheta = pow(cos(theta), 2);
  13. float edge = (rad_outer_blur[0] * rad_outer_blur[1]) / sqrt(pow(rad_outer_blur[0], 2)
  14. * sqrSinTheta + pow(rad_outer_blur[1], 2) * sqrCosTheta);
  15. float outer_blur = edge - (rad_outer[0] * rad_outer[1]) / sqrt(pow(rad_outer[0], 2)
  16. * sqrSinTheta + pow(rad_outer[1], 2) * sqrCosTheta);
  17. float outline = edge - (rad_inner[0] * rad_inner[1]) / sqrt(pow(rad_inner[0], 2)
  18. * sqrSinTheta + pow(rad_inner[1], 2) * sqrCosTheta);
  19. float inner_blur = edge - (rad_inner_blur[0] * rad_inner_blur[1]) / sqrt(pow(rad_inner_blur[0], 2)
  20. * sqrSinTheta + pow(rad_inner_blur[1], 2) * sqrCosTheta);
  21. return float4(edge, outer_blur, outline, inner_blur);
  22. }
  23. // gets distances to the edge of the shape, and then distances to outer_blur, outline and inner_blur
  24. // from the edge of the shape - but going from the inside out, for an inner "donut hole" ellipse
  25. float4 ellipse_distances_inverse(float theta, float2 radii) {
  26. float2 rad_outer_blur = radii;
  27. float2 rad_outer = rad_outer_blur + _OuterBlur;
  28. float2 rad_inner = rad_outer + _OutlineSize;
  29. float2 rad_inner_blur = rad_inner + _InnerBlur;
  30. // get distances from the edge of the shape
  31. float sqrSinTheta = pow(sin(theta), 2);
  32. float sqrCosTheta = pow(cos(theta), 2);
  33. float edge = (rad_outer_blur[0] * rad_outer_blur[1]) / sqrt(pow(rad_outer_blur[0], 2)
  34. * sqrSinTheta + pow(rad_outer_blur[1], 2) * sqrCosTheta);
  35. float outer_blur = (rad_outer[0] * rad_outer[1]) / sqrt(pow(rad_outer[0], 2)
  36. * sqrSinTheta + pow(rad_outer[1], 2) * sqrCosTheta) - edge;
  37. float outline = (rad_inner[0] * rad_inner[1]) / sqrt(pow(rad_inner[0], 2)
  38. * sqrSinTheta + pow(rad_inner[1], 2) * sqrCosTheta) - edge;
  39. float inner_blur = (rad_inner_blur[0] * rad_inner_blur[1]) / sqrt(pow(rad_inner_blur[0], 2)
  40. * sqrSinTheta + pow(rad_inner_blur[1], 2) * sqrCosTheta) - edge;
  41. return float4(edge, outer_blur, outline, inner_blur);
  42. }
  43. // this method is pretty fast, and has accurate distances on the minor and
  44. // major dimensions but only at 0 and 90 degrees - outlines get stretched when
  45. // the ratio of major to minor gets too far away from 1.
  46. fixed4 nested_ellipse_method(float2 pos, fixed4 fill_color, fixed4 outline_color) {
  47. float dist_to_pos = length(pos);
  48. // angle of pos from the center
  49. float theta = atan2(pos.y, pos.x + when_eq(pos.x, 0) * 0.00001);
  50. // useful info about the outer ellipse
  51. float2 radii = float2(_XScale, _YScale) / 2;
  52. float4 distances = ellipse_distances(theta, radii);
  53. // distance from the current point to the edge of the outer ellipse
  54. float dist = distances.x - dist_to_pos;
  55. if (_ArcAngles.z > 0) {
  56. // normalize theta to something more useful. theta as returned from atan() is
  57. // 0 to PI or 0 to -PI depending on which side of the x axis you are, so this turns
  58. // it into 0 to 2PI so we can do simple comparisons between angles.
  59. float negative_theta = when_lt(theta, 0);
  60. float norm_theta = negative_theta * (PI2 + theta) + (1 - negative_theta) * theta;
  61. // see if we're closer to one of the arc lines than to the ellipse curve. doesn't matter
  62. // if our line segment extends past the radius (in fact it helps if it does).
  63. float max_dim = max(_XScale, _YScale);
  64. float2 p1 = float2(cos(_ArcAngles.x) * max_dim, sin(_ArcAngles.x) * max_dim);
  65. float2 p2 = float2(cos(_ArcAngles.y) * max_dim, sin(_ArcAngles.y) * max_dim);
  66. float d1 = distance_to_line_segment(pos, float2(0, 0), p1);
  67. float d2 = distance_to_line_segment(pos, float2(0, 0), p2);
  68. float line_dist = min(d1, d2);
  69. float4 line_distances = float4(0, _OuterBlur, _OuterBlur + _OutlineSize, _OuterBlur + _OutlineSize + _InnerBlur);
  70. // ellipse distances are weird, so don't just use min(line_dist, dist) - check if we're
  71. // closer relative to the total outline distance
  72. float use_line = when_lt(line_dist / line_distances.w, dist / distances.w) * _ArcAngles.z;
  73. dist = use_line * line_dist + (1 - use_line) * dist;
  74. distances = use_line * line_distances + (1 - use_line) * distances;
  75. // see if we're outside the visible part given the arc constraints
  76. float less_than_min = when_lt(norm_theta, _ArcAngles.x);
  77. float greater_than_max = when_gt(norm_theta, _ArcAngles.y);
  78. float outside_arc = or(less_than_min, greater_than_max);
  79. outside_arc = _ArcAngles.w * outside_arc + (1 - _ArcAngles.w) * (1 - outside_arc);
  80. dist *= outside_arc;
  81. }
  82. if (_InnerRadii.x > 0 || _InnerRadii.y > 0) {
  83. // are we closer to the inner ellipse?
  84. // useful info about the "donut hole" inner ellipse
  85. float2 inner_radii = _InnerRadii.xy;
  86. float4 inner_distances = ellipse_distances_inverse(theta, inner_radii);
  87. // distance from the current point to the edge of the inner ellipse, on the inside
  88. float inner_dist = dist_to_pos - inner_distances.x;
  89. // ellipse distances are weird, so don't just use min(inner_dist, dist) - check if we're
  90. // closer relative to the total outline distance
  91. float use_inner = when_lt(inner_dist / inner_distances.w, dist / distances.w);
  92. dist = use_inner * inner_dist + (1 - use_inner) * dist;
  93. distances = use_inner * inner_distances + (1 - use_inner) * distances;
  94. }
  95. if (_OutlineSize == 0) {
  96. return fill_blend(dist, fill_color, distances.y);
  97. } else {
  98. return outline_fill_blend(dist, fill_color, outline_color,
  99. distances.y, distances.z, distances.w);
  100. }
  101. }
  102. // in standard form so b3 == 1. this just returns a real root.
  103. float get_cubic_root(float b2, float b0) {
  104. // in standard form - seems to have fewer glitches
  105. // http://mathworld.wolfram.com/CubicFormula.html
  106. // note that b1 has been removed cause it's always 0
  107. float p = (-b2 * b2) / 3.;
  108. float q = (-27 * b0 - 2 * b2 * b2 * b2) / 27.;
  109. float Q = p / 3.;
  110. float R = q / 2.;
  111. float D = Q * Q * Q + R * R;
  112. if (D < 0) {
  113. float phi = acos(clamp(R / sqrt(max(0, -Q * -Q * -Q)), -1, 1));
  114. return 2 * sqrt(max(0, -Q)) * cos(phi / 3.) - 1/3. * b2;
  115. } else {
  116. float S = pow(max(0, R + sqrt(max(0, D))), 1/3.);
  117. float n = R - sqrt(max(0, D));
  118. // if R is very close to sqrt(D) then we get noise because n may
  119. // end up negative (I assume due to floating point inaccuracies?)
  120. // and the pow(n, 1/3.) is undefined.
  121. float T = pow(max(0, n), 1/3.);
  122. return -1/3. * b2 + (S + T);
  123. }
  124. // // coefficients of the resolvent cubic in general form
  125. // float b2 = -a2;
  126. // float b1 = a1 * a3 - 4. * a0;
  127. // float b0 = 4. * a0 * a2 - a1 * a1 - a0 * a3 * a3;
  128. // float Q = (3 * b1 - b2 * b2) / 9.;
  129. // float R = (9 * b2 * b1 - 27 * b0 - 2 * b2 * b2 * b2) / 54.;
  130. // float D = Q * Q * Q + R * R;
  131. // if (D < 0) {
  132. // float phi = acos(R / sqrt(-Q * -Q * -Q));
  133. // return 2 * sqrt(-Q) * cos(phi / 3.) - 1/3. * b2;
  134. // } else {
  135. // float S = pow(R + sqrt(D), 1/3.);
  136. // float T = pow(R - sqrt(D), 1/3.);
  137. // // a root of the resolvent cubic
  138. // return -1/3. * b2 + (S + T);
  139. // }
  140. }
  141. // in standard form so a4 == 1. we return a different root depending on if the
  142. // ellipse has its major axis as y (i.e. is vertically stretched rather than
  143. // horizontally).
  144. float get_quartic_root(float a3, float a2, float a1, float a0, bool vertical) {
  145. // coefficients of the resolvent cubic equation
  146. // http://mathworld.wolfram.com/ResolventCubic.html
  147. float b2 = -a2;
  148. // b1 always ends up being 0
  149. // float b1 = a1 * a3 - 4. * a0;
  150. float b0 = 4. * a0 * a2 - a1 * a1 - a0 * a3 * a3;
  151. float y1 = get_cubic_root(b2, b0);
  152. // take the cubic root back to the quartic and find its root
  153. // http://mathworld.wolfram.com/QuarticEquation.html
  154. float R = sqrt(max(1/4. * a3 * a3 - a2 + y1, 0));
  155. float e1 = 3/4. * a3 * a3 - R * R - 2. * a2;
  156. float e2 = 1/4. * (4 * a3 * a2 - 8 * a1 - a3 * a3 * a3);
  157. float e3 = 0;
  158. if (R != 0)
  159. e3 = pow(R, -1);
  160. // thanks to shadertoy user kusma for the hint about -1 <= acos <= 1
  161. // https://www.shadertoy.com/view/4sS3zz
  162. if (vertical) {
  163. float v = e1 + e2 * e3;
  164. // with very small values of v, sqrt(v) causes glitchiness. this isn't a real
  165. // fix but it at least draws something close to the right thing.
  166. float D = sqrt(max(0, v));
  167. float z1 = acos(clamp(-1/4. * a3 + 1/2. * R + 1/2. * D, -1, 1));
  168. // float z2 = acos(min(- 1/4. * a3 + 1/2. * R - 1/2. * D, 1));
  169. return z1;
  170. } else {
  171. float v = e1 - e2 * e3;
  172. // with very small values of v, sqrt(v) causes glitchiness. this isn't a real
  173. // fix but it at least draws something close to the right thing.
  174. float E = sqrt(max(0, v));
  175. float z3 = acos(clamp(-1/4. * a3 - 1/2. * R + 1/2. * E, -1, 1));
  176. // float z4 = acos(min(- 1/4. * a3 - 1/2. * R - 1/2. * E, 1));
  177. return z3;
  178. }
  179. }
  180. // note - this method is "correct" but it has glitches, meaning noise appears sometimes.
  181. // I haven't been able to figure out why - it doesn't seem like there are any possible
  182. // math domain errors, but maybe there is some floating point inaccuracy somewhere.
  183. // we try to mitigate it by drawing an "incorrect" but less glitchy method for some
  184. // pixels/ellipses, but it's not perfect.
  185. fixed4 quartic_method(float2 pos, fixed4 fill_color, fixed4 outline_color) {
  186. // if outside the ellipse, return clear
  187. if (ellipse(pos, float2(_XScale, _YScale) / 2) > 1)
  188. return fixed4(0, 0, 0, 0);
  189. float a = _XScale / 2;
  190. float b = _YScale / 2;
  191. float x = abs(pos.x);
  192. float y = abs(pos.y);
  193. // this method glitches out a lot for near-circles so render those using
  194. // a simpler method. it's pretty hard to tell the difference a lot of the time.
  195. // also, with very small values of x and y, we get glitches. this isn't a real
  196. // fix but it at least draws something close to the right thing.
  197. if (1 - min(a, b) / max(a, b) < 0.25f
  198. || x / a < 0.005f || y / b < 0.005f)
  199. return nested_ellipse_method(pos, fill_color, outline_color);
  200. // coefficients of the quartic equation
  201. // many thanks to http://iquilezles.org/www/articles/ellipsedist/ellipsedist.htm
  202. // for help with the math
  203. float m = x * (a / (b * b - a * a));
  204. float n = y * (b / (b * b - a * a));
  205. float a4 = 1.;
  206. float a3 = 2. * m;
  207. float a2 = m * m + n * n - 1.;
  208. float a1 = -2. * m;
  209. float a0 = -m * m;
  210. // theta, a root of the quartic equation, is the angle on the ellipse corresponding
  211. // to the nearest point on the ellipse to our current point. given the equation:
  212. // f(theta) = get the distance from the current point (inside the ellipse) to the
  213. // point at theta (on the ellipse),
  214. // then the derivative of f represents how the distance changes as you change theta.
  215. // the values of theta for which the derivative returns 0 (i.e. the roots) represent
  216. // the points where the distance is nearest or farthest from the current point.
  217. // this derivative turns out to be a quartic eqation (see the above iquilezles
  218. // article), so finding the correct root gives us the theta we need.
  219. float theta = get_quartic_root(a3, a2, a1, a0, _XScale < _YScale);
  220. float2 radii = float2(_XScale, _YScale) / 2;
  221. // the nearest point on the ellipse to our current point. the line going between our
  222. // current point and the nearest point is the normal, and measuring the distance from
  223. // the current point to the nearest point tells us whether we're on the outline or not.
  224. float2 nearest = float2(cos(theta) * radii[0], sin(theta) * radii[1]);
  225. nearest.x *= sign(pos.x);
  226. nearest.y *= sign(pos.y);
  227. float dist = distance(nearest, pos);
  228. return color_from_distance(dist, fill_color, outline_color);
  229. }
  230. fixed4 frag(v2f i) : SV_Target {
  231. float2 pos = prepare(i.uv, i.modelPos.z);
  232. fixed4 outline_color = _OutlineColor;
  233. fixed4 fill_color = fill(i.uv);
  234. #if ELLIPSE
  235. fixed4 color = quartic_method(pos, fill_color, outline_color);
  236. #else // SIMPLE_ELLIPSE
  237. fixed4 color = nested_ellipse_method(pos, fill_color, outline_color);
  238. #endif
  239. color *= i.color;
  240. if (_PreMultiplyAlpha == 1)
  241. color.rgb *= color.a;
  242. if (_UseClipRect == 1)
  243. color.a *= UnityGet2DClipping(i.modelPos.xy, _ClipRect);
  244. clip(color.a - 0.001);
  245. return color;
  246. }