| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283 |
- float4 _ArcAngles; // min, max, use_arc, invert_arc in radians from 0 - PI2
- float4 _InnerRadii; // just x & y
- // gets distances to the edge of the shape, and then distances to outer_blur, outline and inner_blur
- // from the edge of the shape
- float4 ellipse_distances(float theta, float2 radii) {
- float2 rad_outer_blur = radii;
- float2 rad_outer = rad_outer_blur - _OuterBlur;
- float2 rad_inner = rad_outer - _OutlineSize;
- float2 rad_inner_blur = rad_inner - _InnerBlur;
- // get distances from the edge of the shape
- float sqrSinTheta = pow(sin(theta), 2);
- float sqrCosTheta = pow(cos(theta), 2);
- float edge = (rad_outer_blur[0] * rad_outer_blur[1]) / sqrt(pow(rad_outer_blur[0], 2)
- * sqrSinTheta + pow(rad_outer_blur[1], 2) * sqrCosTheta);
- float outer_blur = edge - (rad_outer[0] * rad_outer[1]) / sqrt(pow(rad_outer[0], 2)
- * sqrSinTheta + pow(rad_outer[1], 2) * sqrCosTheta);
- float outline = edge - (rad_inner[0] * rad_inner[1]) / sqrt(pow(rad_inner[0], 2)
- * sqrSinTheta + pow(rad_inner[1], 2) * sqrCosTheta);
- float inner_blur = edge - (rad_inner_blur[0] * rad_inner_blur[1]) / sqrt(pow(rad_inner_blur[0], 2)
- * sqrSinTheta + pow(rad_inner_blur[1], 2) * sqrCosTheta);
- return float4(edge, outer_blur, outline, inner_blur);
- }
- // gets distances to the edge of the shape, and then distances to outer_blur, outline and inner_blur
- // from the edge of the shape - but going from the inside out, for an inner "donut hole" ellipse
- float4 ellipse_distances_inverse(float theta, float2 radii) {
- float2 rad_outer_blur = radii;
- float2 rad_outer = rad_outer_blur + _OuterBlur;
- float2 rad_inner = rad_outer + _OutlineSize;
- float2 rad_inner_blur = rad_inner + _InnerBlur;
- // get distances from the edge of the shape
- float sqrSinTheta = pow(sin(theta), 2);
- float sqrCosTheta = pow(cos(theta), 2);
- float edge = (rad_outer_blur[0] * rad_outer_blur[1]) / sqrt(pow(rad_outer_blur[0], 2)
- * sqrSinTheta + pow(rad_outer_blur[1], 2) * sqrCosTheta);
- float outer_blur = (rad_outer[0] * rad_outer[1]) / sqrt(pow(rad_outer[0], 2)
- * sqrSinTheta + pow(rad_outer[1], 2) * sqrCosTheta) - edge;
- float outline = (rad_inner[0] * rad_inner[1]) / sqrt(pow(rad_inner[0], 2)
- * sqrSinTheta + pow(rad_inner[1], 2) * sqrCosTheta) - edge;
- float inner_blur = (rad_inner_blur[0] * rad_inner_blur[1]) / sqrt(pow(rad_inner_blur[0], 2)
- * sqrSinTheta + pow(rad_inner_blur[1], 2) * sqrCosTheta) - edge;
- return float4(edge, outer_blur, outline, inner_blur);
- }
- // this method is pretty fast, and has accurate distances on the minor and
- // major dimensions but only at 0 and 90 degrees - outlines get stretched when
- // the ratio of major to minor gets too far away from 1.
- fixed4 nested_ellipse_method(float2 pos, fixed4 fill_color, fixed4 outline_color) {
- float dist_to_pos = length(pos);
- // angle of pos from the center
- float theta = atan2(pos.y, pos.x + when_eq(pos.x, 0) * 0.00001);
- // useful info about the outer ellipse
- float2 radii = float2(_XScale, _YScale) / 2;
- float4 distances = ellipse_distances(theta, radii);
-
- // distance from the current point to the edge of the outer ellipse
- float dist = distances.x - dist_to_pos;
- if (_ArcAngles.z > 0) {
- // normalize theta to something more useful. theta as returned from atan() is
- // 0 to PI or 0 to -PI depending on which side of the x axis you are, so this turns
- // it into 0 to 2PI so we can do simple comparisons between angles.
- float negative_theta = when_lt(theta, 0);
- float norm_theta = negative_theta * (PI2 + theta) + (1 - negative_theta) * theta;
- // see if we're closer to one of the arc lines than to the ellipse curve. doesn't matter
- // if our line segment extends past the radius (in fact it helps if it does).
- float max_dim = max(_XScale, _YScale);
- float2 p1 = float2(cos(_ArcAngles.x) * max_dim, sin(_ArcAngles.x) * max_dim);
- float2 p2 = float2(cos(_ArcAngles.y) * max_dim, sin(_ArcAngles.y) * max_dim);
- float d1 = distance_to_line_segment(pos, float2(0, 0), p1);
- float d2 = distance_to_line_segment(pos, float2(0, 0), p2);
- float line_dist = min(d1, d2);
- float4 line_distances = float4(0, _OuterBlur, _OuterBlur + _OutlineSize, _OuterBlur + _OutlineSize + _InnerBlur);
- // ellipse distances are weird, so don't just use min(line_dist, dist) - check if we're
- // closer relative to the total outline distance
- float use_line = when_lt(line_dist / line_distances.w, dist / distances.w) * _ArcAngles.z;
- dist = use_line * line_dist + (1 - use_line) * dist;
- distances = use_line * line_distances + (1 - use_line) * distances;
-
- // see if we're outside the visible part given the arc constraints
- float less_than_min = when_lt(norm_theta, _ArcAngles.x);
- float greater_than_max = when_gt(norm_theta, _ArcAngles.y);
- float outside_arc = or(less_than_min, greater_than_max);
- outside_arc = _ArcAngles.w * outside_arc + (1 - _ArcAngles.w) * (1 - outside_arc);
- dist *= outside_arc;
- }
-
- if (_InnerRadii.x > 0 || _InnerRadii.y > 0) {
- // are we closer to the inner ellipse?
- // useful info about the "donut hole" inner ellipse
- float2 inner_radii = _InnerRadii.xy;
- float4 inner_distances = ellipse_distances_inverse(theta, inner_radii);
- // distance from the current point to the edge of the inner ellipse, on the inside
- float inner_dist = dist_to_pos - inner_distances.x;
- // ellipse distances are weird, so don't just use min(inner_dist, dist) - check if we're
- // closer relative to the total outline distance
- float use_inner = when_lt(inner_dist / inner_distances.w, dist / distances.w);
- dist = use_inner * inner_dist + (1 - use_inner) * dist;
- distances = use_inner * inner_distances + (1 - use_inner) * distances;
- }
- if (_OutlineSize == 0) {
- return fill_blend(dist, fill_color, distances.y);
- } else {
- return outline_fill_blend(dist, fill_color, outline_color,
- distances.y, distances.z, distances.w);
- }
- }
- // in standard form so b3 == 1. this just returns a real root.
- float get_cubic_root(float b2, float b0) {
- // in standard form - seems to have fewer glitches
- // http://mathworld.wolfram.com/CubicFormula.html
- // note that b1 has been removed cause it's always 0
- float p = (-b2 * b2) / 3.;
- float q = (-27 * b0 - 2 * b2 * b2 * b2) / 27.;
- float Q = p / 3.;
- float R = q / 2.;
- float D = Q * Q * Q + R * R;
- if (D < 0) {
- float phi = acos(clamp(R / sqrt(max(0, -Q * -Q * -Q)), -1, 1));
- return 2 * sqrt(max(0, -Q)) * cos(phi / 3.) - 1/3. * b2;
- } else {
- float S = pow(max(0, R + sqrt(max(0, D))), 1/3.);
- float n = R - sqrt(max(0, D));
- // if R is very close to sqrt(D) then we get noise because n may
- // end up negative (I assume due to floating point inaccuracies?)
- // and the pow(n, 1/3.) is undefined.
- float T = pow(max(0, n), 1/3.);
- return -1/3. * b2 + (S + T);
- }
-
- // // coefficients of the resolvent cubic in general form
- // float b2 = -a2;
- // float b1 = a1 * a3 - 4. * a0;
- // float b0 = 4. * a0 * a2 - a1 * a1 - a0 * a3 * a3;
- // float Q = (3 * b1 - b2 * b2) / 9.;
- // float R = (9 * b2 * b1 - 27 * b0 - 2 * b2 * b2 * b2) / 54.;
- // float D = Q * Q * Q + R * R;
- // if (D < 0) {
- // float phi = acos(R / sqrt(-Q * -Q * -Q));
- // return 2 * sqrt(-Q) * cos(phi / 3.) - 1/3. * b2;
- // } else {
- // float S = pow(R + sqrt(D), 1/3.);
- // float T = pow(R - sqrt(D), 1/3.);
- // // a root of the resolvent cubic
- // return -1/3. * b2 + (S + T);
- // }
- }
- // in standard form so a4 == 1. we return a different root depending on if the
- // ellipse has its major axis as y (i.e. is vertically stretched rather than
- // horizontally).
- float get_quartic_root(float a3, float a2, float a1, float a0, bool vertical) {
- // coefficients of the resolvent cubic equation
- // http://mathworld.wolfram.com/ResolventCubic.html
- float b2 = -a2;
- // b1 always ends up being 0
- // float b1 = a1 * a3 - 4. * a0;
- float b0 = 4. * a0 * a2 - a1 * a1 - a0 * a3 * a3;
- float y1 = get_cubic_root(b2, b0);
- // take the cubic root back to the quartic and find its root
- // http://mathworld.wolfram.com/QuarticEquation.html
- float R = sqrt(max(1/4. * a3 * a3 - a2 + y1, 0));
- float e1 = 3/4. * a3 * a3 - R * R - 2. * a2;
- float e2 = 1/4. * (4 * a3 * a2 - 8 * a1 - a3 * a3 * a3);
- float e3 = 0;
- if (R != 0)
- e3 = pow(R, -1);
- // thanks to shadertoy user kusma for the hint about -1 <= acos <= 1
- // https://www.shadertoy.com/view/4sS3zz
- if (vertical) {
- float v = e1 + e2 * e3;
- // with very small values of v, sqrt(v) causes glitchiness. this isn't a real
- // fix but it at least draws something close to the right thing.
- float D = sqrt(max(0, v));
- float z1 = acos(clamp(-1/4. * a3 + 1/2. * R + 1/2. * D, -1, 1));
- // float z2 = acos(min(- 1/4. * a3 + 1/2. * R - 1/2. * D, 1));
- return z1;
- } else {
- float v = e1 - e2 * e3;
- // with very small values of v, sqrt(v) causes glitchiness. this isn't a real
- // fix but it at least draws something close to the right thing.
- float E = sqrt(max(0, v));
- float z3 = acos(clamp(-1/4. * a3 - 1/2. * R + 1/2. * E, -1, 1));
- // float z4 = acos(min(- 1/4. * a3 - 1/2. * R - 1/2. * E, 1));
- return z3;
- }
- }
- // note - this method is "correct" but it has glitches, meaning noise appears sometimes.
- // I haven't been able to figure out why - it doesn't seem like there are any possible
- // math domain errors, but maybe there is some floating point inaccuracy somewhere.
- // we try to mitigate it by drawing an "incorrect" but less glitchy method for some
- // pixels/ellipses, but it's not perfect.
- fixed4 quartic_method(float2 pos, fixed4 fill_color, fixed4 outline_color) {
- // if outside the ellipse, return clear
- if (ellipse(pos, float2(_XScale, _YScale) / 2) > 1)
- return fixed4(0, 0, 0, 0);
- float a = _XScale / 2;
- float b = _YScale / 2;
- float x = abs(pos.x);
- float y = abs(pos.y);
-
- // this method glitches out a lot for near-circles so render those using
- // a simpler method. it's pretty hard to tell the difference a lot of the time.
- // also, with very small values of x and y, we get glitches. this isn't a real
- // fix but it at least draws something close to the right thing.
- if (1 - min(a, b) / max(a, b) < 0.25f
- || x / a < 0.005f || y / b < 0.005f)
- return nested_ellipse_method(pos, fill_color, outline_color);
-
- // coefficients of the quartic equation
- // many thanks to http://iquilezles.org/www/articles/ellipsedist/ellipsedist.htm
- // for help with the math
- float m = x * (a / (b * b - a * a));
- float n = y * (b / (b * b - a * a));
- float a4 = 1.;
- float a3 = 2. * m;
- float a2 = m * m + n * n - 1.;
- float a1 = -2. * m;
- float a0 = -m * m;
-
- // theta, a root of the quartic equation, is the angle on the ellipse corresponding
- // to the nearest point on the ellipse to our current point. given the equation:
- // f(theta) = get the distance from the current point (inside the ellipse) to the
- // point at theta (on the ellipse),
- // then the derivative of f represents how the distance changes as you change theta.
- // the values of theta for which the derivative returns 0 (i.e. the roots) represent
- // the points where the distance is nearest or farthest from the current point.
- // this derivative turns out to be a quartic eqation (see the above iquilezles
- // article), so finding the correct root gives us the theta we need.
- float theta = get_quartic_root(a3, a2, a1, a0, _XScale < _YScale);
- float2 radii = float2(_XScale, _YScale) / 2;
- // the nearest point on the ellipse to our current point. the line going between our
- // current point and the nearest point is the normal, and measuring the distance from
- // the current point to the nearest point tells us whether we're on the outline or not.
- float2 nearest = float2(cos(theta) * radii[0], sin(theta) * radii[1]);
- nearest.x *= sign(pos.x);
- nearest.y *= sign(pos.y);
- float dist = distance(nearest, pos);
- return color_from_distance(dist, fill_color, outline_color);
- }
- fixed4 frag(v2f i) : SV_Target {
- float2 pos = prepare(i.uv, i.modelPos.z);
- fixed4 outline_color = _OutlineColor;
- fixed4 fill_color = fill(i.uv);
- #if ELLIPSE
- fixed4 color = quartic_method(pos, fill_color, outline_color);
- #else // SIMPLE_ELLIPSE
- fixed4 color = nested_ellipse_method(pos, fill_color, outline_color);
- #endif
- color *= i.color;
- if (_PreMultiplyAlpha == 1)
- color.rgb *= color.a;
- if (_UseClipRect == 1)
- color.a *= UnityGet2DClipping(i.modelPos.xy, _ClipRect);
- clip(color.a - 0.001);
-
- return color;
- }
|