283 lines
13 KiB
HLSL
283 lines
13 KiB
HLSL
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;
|
|
} |