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

157 lines
6.1 KiB
HLSL

#if PATH_1 || FILLED_PATH_1
#define MAX_SEGMENTS 1
#elif PATH_2 || FILLED_PATH_2
#define MAX_SEGMENTS 2
#elif PATH_4 || FILLED_PATH_4
#define MAX_SEGMENTS 4
#elif PATH_8 || FILLED_PATH_8
#define MAX_SEGMENTS 8
#elif PATH_16 || FILLED_PATH_16
#define MAX_SEGMENTS 16
#elif PATH_24 || FILLED_PATH_24
#define MAX_SEGMENTS 24
#elif PATH_32 || FILLED_PATH_32
#define MAX_SEGMENTS 32
#endif
#define MAX_POINTS 3 * MAX_SEGMENTS
// each point is (x, y, is-in-loop, unused)
float4 _Points[MAX_POINTS];
int _NumSegments;
float _Thickness;
float3 get_cubic_roots(float4 coefficients) {
// eliminate a3 by dividing it out
coefficients /= coefficients.x;
float a2 = coefficients.y;
float a1 = coefficients.z;
float a0 = coefficients.w;
// follow along at http://mathworld.wolfram.com/CubicFormula.html
// thanks also to https://www.shadertoy.com/view/XdB3Ww
float Q = (3. * a1 - (a2 * a2)) / 9.;
float R = (9. * a2 * a1 - 27. * a0 - 2. * (a2 * a2 * a2)) / 54.;
float D = Q * Q * Q + R * R;
if (D < 0.) {
float theta = acos(R / sqrt(-(Q * Q * Q)));
float3 i1 = float3(0., 2. * PI, 4. * PI) + theta;
float3 i2 = cos(i1 / 3.);
return 2. * sqrt(-Q) * i2 - 1/3. * a2;
} else {
// if sqrt(D) and R are very close to each other, R - sd wigs
// out due to numerical cancellation. so multiply by the
// conjugate to get a slightly different equation that avoids
// that operation (i.e., we end up doing R + sd instead).
float sd = sqrt(D);
// float2 st = float2(sd, -sd) + R;
// if (sign(R) == sign(sd))
// st = float2(R + sd, -pow(Q, 3) / (R + sd));
// else
// st = float2(R - sd, -pow(Q, 3) / (R - sd));
float rsd = (when_neq(sign(R), sign(sd)) * -2 + 1) * sd + R;
float2 st = float2(rsd, -(Q * Q * Q) / rsd);
// preserve the sign of R +- sqrt(D) after taking the cube root
st = sign(st) * pow(abs(st), 1/3.);
float r = -1/3. * a2 + st.x + st.y;
return float3(r, 0, 0);
}
}
float2 distance_to_segment(float2 M, float2 b0, float2 b1, float2 b2) {
// get the coefficients, thanks to
// http://blog.gludion.com/2009/08/distance-to-quadratic-bezier-curve.html
// note that when b1 is too close to the midpoint between b0 and b2, B ~= 0 and we get problems.
// (handling that case in user space before we even get here)
float2 A = b1 - b0;
float2 B = b2 - b1 - A;
float2 Mp = b0 - M;
float a = dot(B, B);
float b = 3. * dot(A, B);
float c = 2. * dot(A, A) + dot(Mp, B);
float d = dot(Mp, A);
float3 roots = clamp(get_cubic_roots(float4(a, b, c, d)), 0, 1);
float2 D = 2. * A;
// thanks yet again to http://alienryderflex.com/polyspline/
float flip = 1;
#if FILLED_PATH_1 || FILLED_PATH_2 || FILLED_PATH_4 || FILLED_PATH_8 || FILLED_PATH_16 || FILLED_PATH_24 || FILLED_PATH_32
// make sure M.y doesn't equal b0.y or b2.y, which could cause F1 or F2 to be exactly 0
// (becomes a problem especially when rotating)
float testY = M.y + when_lt(abs(b0.y - M.y), .0001) * .0002;
testY += when_lt(abs(b2.y - testY), .0001) * .0002;
float bottomPart=2.*(b0.y+b2.y-b1.y-b1.y);
// prevent division-by-zero (also handled in user space)
// if (abs(bottomPart) <= 0.0001) {
// b1.y += 0.0001;
// bottomPart = -0.0004;
// }
float sRoot=D.y;
sRoot*=sRoot;
sRoot-=2.*bottomPart*(b0.y - testY);
if (sRoot >= 0) {
sRoot=sqrt(sRoot);
float topPart=2.*(b0.y-b1.y);
float F1 = (topPart+sRoot)/bottomPart;
float F2 = (topPart-sRoot)/bottomPart;
if (F1>=0. && F1<=1.) {
float xPart=b0.x+F1*(b1.x-b0.x);
if (xPart+F1*(b1.x+F1*(b2.x-b1.x)-xPart)<M.x)
flip *= -1;
}
if (F2>=0. && F2<=1.) {
float xPart=b0.x+F2*(b1.x-b0.x);
if (xPart+F2*(b1.x+F2*(b2.x-b1.x)-xPart)<M.x)
flip *= -1;
}
}
#endif
// find the positions on the curve of each root
// thanks to https://www.shadertoy.com/view/XdB3Ww for this simplification
float2 p1 = roots.x * (D + B * roots.x) + b0;
float2 p2 = roots.y * (D + B * roots.y) + b0;
float2 p3 = roots.z * (D + B * roots.z) + b0;
// figure out which point is closest to M
float dist1 = length(p1 - M);
float dist2 = length(p2 - M);
float dist3 = length(p3 - M);
float dist = min(min(dist1, dist2), dist3);
return float2(dist, flip);
}
float2 distance_to_path(float2 pos) {
float closest_distance = 9999999;
int odd_nodes = -1; // used for testing if the point is inside a filled path
// you can't do variable-length loops in webgl (or es2 technically I think),
// hence the constant...so we have to iterate through MAX_SEGMENTS rather than
// _NumSegments which is the number of vertices actually in our poly
for (int i = 0; i < MAX_SEGMENTS; i++) {
// loop_over is 1 when we're past the number of sides in the poly
float loop_over = when_ge(i, _NumSegments);
float2 b0 = _Points[i * 3 + 0];
float2 b1 = _Points[i * 3 + 1];
float2 b2 = _Points[i * 3 + 2];
float2 result = distance_to_segment(pos, b0, b1, b2) + loop_over * 9999999;
float dist = result.x;
closest_distance = min(dist, closest_distance);
if (_Points[i * 3].z == 1)
odd_nodes *= result.y / (loop_over * (result.y - 1) + 1);
}
return odd_nodes * closest_distance + _Thickness;
}
fixed4 frag(v2f i) : SV_Target {
float2 pos = prepare(i.uv, i.modelPos.z);
float dist = distance_to_path(pos);
float is_outside = when_lt(dist, 0);
fixed4 color = color_from_distance(dist, fill(i.uv), _OutlineColor) * 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 (1 - is_outside) * color + is_outside * fixed4(0, 0, 0, 0);
}