Files
XRLib/Assets/Shapes2D/Shaders/Ellipse.cginc
2025-08-12 12:54:51 +09:00

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;
}