From the image below, I have represented a circle by a closed bezier curve made of two cubic bezier segments. I am aware that a bezier curve cannot represent a perfect circle. How can I calculate the distance from the anchor points to the control points based on the distance from both anchor points to give a close approximation of a circle?
Also how can I use the calculated distance to find the positions for the control points?
Answer
Please don't forget to search for existing information on your problem area before posting new questions - especially when you're asking about something fundamental that's surely been answered before.
That said, I'd like to take a different approach than the answers I linked above.
These past answers seem to choose their control points so that the midpoint of the Bézier curve lies at the midpoint of the circular arc. That means that the half-curve on either side of the midpoint bulge out beyond the circle.
That's not bad, but we can do better, in terms of minimizing the maximum error. If we undershoot a little at the midpoint, we reduce the overshoot on either side, keeping our Bézier curve closer to the circle overall.
To do this, instead of kissing the arc at \$t = \frac 1 2\$, we'll cross it at \$t = \frac 1 3\$ and \$t = \frac 2 3\$. Our curve will weave back-and-forth across the circle we want to follow instead of staying strictly outside it.
I'll spare you the full derivation, but to do that, we want our control handles to have the following length:
$$l = \frac {3\sqrt{\sin^2\theta - 70 \cos \theta + 70} - 17 \sin \theta}{15 - 12 \cos \theta} \cdot r $$
Where \$\theta\$ is the angle of the circular arc (180° or \$\pi\$ radians if you use just two anchor points at opposite ends of the diameter), and \$r\$ is the radius of the circle (half the distance between the anchor points if they're at opposite ends of a diameter)
For the 180° case that translates to \$l = \frac {2 \sqrt {35}} 9 \cdot r \approx 1.314684396 \cdot r \$, with a maximum error of about 1.40%
(For comparison, the answers linked above use \$l = \frac 4 3 \cdot r \approx 1.333333 \cdot r\$ which suffers an error of about 1.83%)
If you care about fidelity, I'd strongly recommend using more than two segments to represent the circle. Even just 3 segments brings the error down to 0.12%, 4 segments brings it to 0.02%
If your Bézier anchor point is at \$A_i = (c_x + r \cdot \cos {\alpha}, c_y + r \cdot \sin \alpha)\$, with \$(c_x, c_y)\$ the center of the circle, and \$\alpha\$ the angle of the point counter-clockwise from the positive x-axis, then its corresponding anchor point is at \$C_i = A_i \pm l \cdot (-\sin \alpha, \cos \alpha)\$, with the sign of the \$\pm\$ being + for the start point, and - for the end.
Here's an animation of this method in action:
And here's the code I used:
// OnValidate is called by the editor automatically when the user modifies
// the inspector parameters like center, radius, and circleCoverageDegrees.
// Here it's used to update our array of Bézier control points.
private void OnValidate() {
BezierCircle(center, radius, circleCoverageDegrees, 0, bezierPoints, 0);
BezierCircle(center, radius, 0, -circleCoverageDegrees, bezierPoints, 3);
}
// This method populates 4 cubic Bézier control points into an array,
// so that the resulting curve approximates a circular arc.
static void BezierCircle(
Vector2 center, float radius,
float startAngleDegrees, float endAngleDegrees,
Vector2[] points, int startIndex
) {
// First, compute how long our tangents should be to best fit this arc.
float angle = Mathf.Abs(endAngleDegrees - startAngleDegrees) * Mathf.Deg2Rad;
float sine = Mathf.Sin(angle);
float cosine = Mathf.Cos(angle);
float tangentScale = radius * (3 * Mathf.Sqrt(sine * sine - 70f * cosine + 70) - 17 * sine) / (15 - 12 * cosine);
tangentScale *= Mathf.Sign(endAngleDegrees - startAngleDegrees);
// Place the start anchor point and its control point relative to it.
angle = Mathf.Deg2Rad * startAngleDegrees;
sine = Mathf.Sin(angle);
cosine = Mathf.Cos(angle);
points[startIndex] = center + radius * new Vector2(cosine, sine);
points[startIndex + 1] = points[startIndex] + tangentScale * new Vector2(-sine, cosine);
// Place the end anchor point and its control point relative to it.
angle = Mathf.Deg2Rad * endAngleDegrees;
sine = Mathf.Sin(angle);
cosine = Mathf.Cos(angle);
points[startIndex + 3] = center + radius * new Vector2(cosine, sine);
points[startIndex + 2] = points[startIndex + 3] - tangentScale * new Vector2(-sine, cosine);
}
No comments:
Post a Comment