XNA CatmullRom Curves - c#

I'm in need of some clarification over a technique I'm trying. I'm trying to move an entity from point A to point B, but I don't want the entity to travel in a straight line.
For example if the entity is positioned at x: 0, y:0 and I want to get to point x:50, y: 0, I want the entity to travel in a curve to the target, I would imagine the maximum distance it would be away is x:25 y: 25 so it's travelled on the X towards the target but has moved away from the target on the y.
I've investigated a couple of options including splines, curves but what I thought would do the job is the CatmullRom curve. I'm a bit confused how to use it? I want to know where to move my entity each frame rather than what the function returns which is the interpolation. I would appreciate some gudiance as to how to use it.
If there's any alternative methods which might be easier that I've missed, I'd appreciate hearing them as well.
Edit:
To show how I'm getting a curve:
Vector2 blah = Vector2.CatmullRom(
StartPosition,
new Vector2(StartPosition.X + 5, StartPosition.Y + 5),
new Vector2(StartPosition.X + 10, StartPosition.Y + 5),
/*This is the end position*/
new Vector2(StartPosition.X + 15, StartPosition.Y), 0.25f);
The idea eventually is I generate these points on the fly but I'm just trying to work it out at the moment.

As you've noticed, splines produce line segments of different lengths. The tighter the curve, the shorter the segments. This is fine for display purposes, not so useful for path generation for mobiles.
To get a reasonable approximation of a constant-speed traversal of a spline path, you need to do some interpolation along the segments of the curve. Since you already have a set of line segments (between pairs of points returned by Vector2.CatmullRom()) you need an method of walking those segments in constant speed.
Given a set of points and a total distance to move along the path defined as lines between those points, the following (more-or-less pseudo-)code will find a point that lies a specific distance along the path:
Point2D WalkPath(Point2D[] path, double distance)
{
Point curr = path[0];
for (int i = 1; i < path.Length; ++i)
{
double dist = Distance(curr, path[i]);
if (dist < distance)
return Interpolate(curr, path[i], distance / dist;
distance -= dist;
curr = path[i];
}
return curr;
}
There are various optimizations you can do to speed this up, such as storing the path distance with each point in the path to make it easier to lookup during a walk operation. This becomes more important as your paths get more complex, but is probable overkill for a path with only a few segments.
Edit: Here's an example that I did with this method in JavaScript a while back. It's a proof-of-concept, so don't look too critically at the code :P
Edit: more information on spline generation
Given a set of 'knot' points - being points that a curve must pass through in sequence - the most obvious fit for a curve algorithm is Catmull-Rom. The downside is that C-R needs two additional control points that can be awkward to generate automatically.
A while back I found a fairly useful article online (which I can't locate anymore to give correct attribution) that calculated a set of control points based on the locations of sets of points within your path. Here's my C# code for the method that calculates the control points:
// Calculate control points for Point 'p1' using neighbour points
public static Point2D[] GetControlsPoints(Point2D p0, Point2D p1, Point2D p2, double tension = 0.5)
{
// get length of lines [p0-p1] and [p1-p2]
double d01 = Distance(p0, p1);
double d12 = Distance(p1, p2);
// calculate scaling factors as fractions of total
double sa = tension * d01 / (d01 + d12);
double sb = tension * d12 / (d01 + d12);
// left control point
double c1x = p1.X - sa * (p2.X - p0.X);
double c1y = p1.Y - sa * (p2.Y - p0.Y);
// right control point
double c2x = p1.X + sb * (p2.X - p0.X);
double c2y = p1.Y + sb * (p2.Y - p0.Y);
// return control points
return new Point2D[] { new Point2D(c1x, c1y), new Point2D(c2x, c2y) };
}
The tension parameter adjusts the control point generation to change the tightness of the curve. Higher values result in broader curves, lower values in tighter curves. Play with it and see what value works best for you.
Given a set of 'n' knots (points on the curve), we can generate a set of control points that will be used to generate the curves between pairs of knots:
// Generate all control points for a set of knots
public static List<Point2D> GenerateControlPoints(List<Point2D> knots)
{
if (knots == null || knots.Count < 3)
return null;
List<Point2D> res = new List<Point2D>();
// First control point is same as first knot
res.Add(knots.First());
// generate control point pairs for each non-end knot
for (int i = 1; i < knots.Count - 1; ++i)
{
Point2D[] cps = GetControlsPoints(knots[i - 1], knots[i], knots[i+1]);
res.AddRange(cps);
}
// Last control points is same as last knot
res.Add(knots.Last());
return res;
}
So now you have an array of 2*(n-1) control points, which you can then use to generate the actual curve segments between the knot points.
public static Point2D LinearInterp(Point2D p0, Point2D p1, double fraction)
{
double ix = p0.X + (p1.X - p0.X) * fraction;
double iy = p0.Y + (p1.Y - p0.Y) * fraction;
return new Point2D(ix, iy);
}
public static Point2D BezierInterp(Point2D p0, Point2D p1, Point2D c0, Point2D c1, double fraction)
{
// calculate first-derivative, lines containing end-points for 2nd derivative
var t00 = LinearInterp(p0, c0, fraction);
var t01 = LinearInterp(c0, c1, fraction);
var t02 = LinearInterp(c1, p1, fraction);
// calculate second-derivate, line tangent to curve
var t10 = LinearInterp(t00, t01, fraction);
var t11 = LinearInterp(t01, t02, fraction);
// return third-derivate, point on curve
return LinearInterp(t10, t11, fraction);
}
// generate multiple points per curve segment for entire path
public static List<Point2D> GenerateCurvePoints(List<Point2D> knots, List<Point2D> controls)
{
List<Point2D> res = new List<Point2D>();
// start curve at first knot
res.Add(knots[0]);
// process each curve segment
for (int i = 0; i < knots.Count - 1; ++i)
{
// get knot points for this curve segment
Point2D p0 = knots[i];
Point2D p1 = knots[i + 1];
// get control points for this curve segment
Point2D c0 = controls[i * 2];
Point2D c1 = controls[i * 2 + 1];
// calculate 20 points along curve segment
int steps = 20;
for (int s = 1; s < steps; ++s)
{
double fraction = (double)s / steps;
res.Add(BezierInterp(p0, p1, c0, c1, fraction));
}
}
return res;
}
Once you have run this over your knots you now have a set of interpolated points that are a variable distance apart, distance depending on the curvature of the line. From this you run the original WalkPath method iteratively to generate a set of points that are a constant distance apart, which define your mobile's progression along the curve at constant speed.
The heading of your mobile at any point in the path is (roughly) the angle between the points on either side. For any point n in the path, the angle between p[n-1] and p[n+1] is the heading angle.
// get angle (in Radians) from p0 to p1
public static double AngleBetween(Point2D p0, Point2D p1)
{
return Math.Atan2(p1.X - p0.X, p1.Y - p0.Y);
}
I've adapted the above from my code, since I use a Point2D class I wrote ages ago that has a lot of the functionality - point arithmetic, interpolation, etc - built in. I might have added some bugs during translation, but hopefully they'll be easy to spot when you're playing with it.
Let me know how it goes. If you run into any particular difficulties I'll see what I can do to help.

Related

How to create bezier curves for an arc with different start and end tangent slopes

I've been stuck on this for a week now i can't seem to solve it.
I have an arc which i can convert to a series of bezier curves quite easily when the arc is flat:
But i am struggling to work out how to find the bezier curves when the arc is a helix and the end tangents have different slopes.
This is as far as i have gotten so far:
As you can see each bezier curve has control points that are not on the right plane, and the start and end tangent (the red vectors in the second image) of the full arc is not factored in as i couldn't work out how to do it.
To find the flat version of the bezier slices from arcs i have this piece of code which certainly works fine for a flat arc:
// from https://pomax.github.io/bezierinfo/#circles_cubic
public CubicBezier ConvertArc(Vector3 origin, float radius, Vector3 from, Vector3 to, float angle)
{
var c = Math.Tan(angle * Mathf.Deg2Rad / 4f) * 4 / 3f * radius;
var c1 = from + (from - origin).Perp().normalized * c;
var c2 = to - (to - origin).Perp().normalized * c;
return new CubicBezier(from, c1, c2, to);
}
This is my current code to create each bezier cut:
//cut the arc in to bezier curves up to 90 degrees max
float cuts = _arc.totalAngle / 90f;
for (int i = 0; i < cuts; i++)
{
float t = i / cuts;
float t2 = (i + 1) / cuts;
Arc slice = new Arc(_arc,_arc.Point(t),_arc.Point(t2));
//this function below is the issue, it needs start and end tangent for the slice,
//but i also don't know how to find the tangents at each slice for the whole arc
//relating the start and end tangents of the entire arc
//see above snippet for function code
var cb = ConvertArc(slice.origin, slice.radius, slice.a, slice.b, slice.totalAngle);
cb.DebugDraw(Color.yellow);
}
Hope some one can help explain the logic to solve how to find the control points correctly to match the tangents, wasted a week already with little progress.
This is written in C# but i don't think the language matters, math is math no matter the language.
A visual (albeit poor drawing) of how i want the result to respect the end tangent slopes:
The problem is that Bezier control points are not as intuitive as interpolation cubics. So we can use those instead and convert their control points into bezier later to make thing easier.
Simply create list of points along your path
all of these are directly on the path and the continuity of the curve is guaranteed by the interpolation cubic equation itself so no tweaking needed...
be sure you have enough points ... for example for full circle at least 8 points are needed nut 16 are better ...
Convert path points into Bezier cubic control points
so simply pick 4 consequent points on path and convert them into bezier control points using this:
Interpolation cubic vs. Bezier cubic
to ensure continuity the next bezier should be done from next point ... So if we have points p0,p1,p2,p3,p4,p5... then we create beziers from (p0,p1,p2,p3) , (p1,p2,p3,p4) , ... and so on. The first point p0 determines starting direction and the last the ending one. If you want your path to start / end on those simply duplicate them ...
Here a small unoptimized and crude example of this in C++:
//---------------------------------------------------------------------------
List<double> it4; // interpolation cubic control points
List<double> bz4; // bezier cubic control points
//---------------------------------------------------------------------------
void generate()
{
int i,j,n;
double x,y,z,a,a0,a1,z0,z1,da,dz,r;
const double deg=M_PI/180.0;
const double rad=180.0/M_PI;
// generate some helix path points
n=32; // number of points along path
r=0.75; // radius
z0=0.0; z1=0.5; // height range
a0=-25.0*deg; a1=+720.0*deg; // angle range
da=(a1-a0)/double(n);
dz=(z1-z0)/double(n);
it4.num=0; // clear list of points
for (z=z0,a=a0,i=0;i<n;i++,a+=da,z+=dz)
{
// 3D point on helix
x=r*cos(a);
y=r*sin(a);
// add it to the list
it4.add(x);
it4.add(y);
it4.add(z);
}
// convert it4 into bz4 control points
bz4.num=0; // clear list of points
for (i=0;i<=it4.num-12;i+=3)
{
const double m=1.0/6.0;
double x0,y0,z0,x1,y1,z1,x2,y2,z2,x3,y3,z3;
double X0,Y0,Z0,X1,Y1,Z1,X2,Y2,Z2,X3,Y3,Z3;
j=i;
X0=it4[j]; j++; Y0=it4[j]; j++; Z0=it4[j]; j++;
X1=it4[j]; j++; Y1=it4[j]; j++; Z1=it4[j]; j++;
X2=it4[j]; j++; Y2=it4[j]; j++; Z2=it4[j]; j++;
X3=it4[j]; j++; Y3=it4[j]; j++; Z3=it4[j]; j++;
x0 = X1; y0 = Y1; z0 = Z1;
x1 = X1-(X0-X2)*m; y1 = Y1-(Y0-Y2)*m; z1 = Z1-(Z0-Z2)*m;
x2 = X2+(X1-X3)*m; y2 = Y2+(Y1-Y3)*m; z2 = Z2+(Z1-Z3)*m;
x3 = X2; y3 = Y2; z3 = Z2;
bz4.add(x0); bz4.add(y0); bz4.add(z0);
bz4.add(x1); bz4.add(y1); bz4.add(z1);
bz4.add(x2); bz4.add(y2); bz4.add(z2);
bz4.add(x3); bz4.add(y3); bz4.add(z3);
}
}
//---------------------------------------------------------------------------
And simple render in VCL/GL/C++
//---------------------------------------------------------------------------
void gl_draw()
{
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
float aspect=float(xs)/float(ys);
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
gluPerspective(60.0/aspect,aspect,0.1,100.0);
glMatrixMode(GL_TEXTURE);
glLoadIdentity();
glMatrixMode(GL_MODELVIEW);
glLoadIdentity();
glTranslatef(0.0,0.0,-2.5);
glRotatef(-70.0,1.0,0.0,0.0);
glRotatef(-130.0,0.0,0.0,1.0);
glEnable(GL_DEPTH_TEST);
glDisable(GL_TEXTURE_2D);
int i,j;
// render axises
glBegin(GL_LINES);
glColor3f(1.0,0.0,0.0); glVertex3d(1.0,0.0,0.0); glVertex3d(0.0,0.0,0.0);
glColor3f(0.0,1.0,0.0); glVertex3d(0.0,1.0,0.0); glVertex3d(0.0,0.0,0.0);
glColor3f(0.0,0.0,1.0); glVertex3d(0.0,0.0,1.0); glVertex3d(0.0,0.0,0.0);
glEnd();
// render it4 control points (aqua)
glColor3f(0.0,1.0,1.0);
glPointSize(8);
glBegin(GL_POINTS);
for (i=0;i<it4.num;i+=3) glVertex3dv(it4.dat+i);
glEnd();
glPointSize(1);
// render bz4 control points (magenta)
glColor3f(1.0,0.0,1.0);
glPointSize(4);
glBegin(GL_POINTS);
for (i=0;i<bz4.num;i+=3) glVertex3dv(bz4.dat+i);
glEnd();
glPointSize(1);
// render bz4 path (yellow)
double t,tt,ttt,cx[4],cy[4],cz[4],x,y,z;
double x0,y0,z0,x1,y1,z1,x2,y2,z2,x3,y3,z3;
glColor3f(1.0,1.0,0.0);
glLineWidth(2);
for (i=0;i<=bz4.num-12;i+=12)
{
j=i;
x0=bz4[j]; j++; y0=bz4[j]; j++; z0=bz4[j]; j++;
x1=bz4[j]; j++; y1=bz4[j]; j++; z1=bz4[j]; j++;
x2=bz4[j]; j++; y2=bz4[j]; j++; z2=bz4[j]; j++;
x3=bz4[j]; j++; y3=bz4[j]; j++; z3=bz4[j]; j++;
cx[0]= ( x0);
cx[1]= (3.0*x1)-(3.0*x0);
cx[2]= (3.0*x2)-(6.0*x1)+(3.0*x0);
cx[3]= ( x3)-(3.0*x2)+(3.0*x1)-( x0);
cy[0]= ( y0);
cy[1]= (3.0*y1)-(3.0*y0);
cy[2]= (3.0*y2)-(6.0*y1)+(3.0*y0);
cy[3]= ( y3)-(3.0*y2)+(3.0*y1)-( y0);
cz[0]= ( z0);
cz[1]= (3.0*z1)-(3.0*z0);
cz[2]= (3.0*z2)-(6.0*z1)+(3.0*z0);
cz[3]= ( z3)-(3.0*z2)+(3.0*z1)-( z0);
glBegin(GL_LINE_STRIP);
for (t=0.0,j=0;j<20;j++,t+=0.05)
{
tt=t*t; ttt=tt*t;
x=cx[0]+cx[1]*t+cx[2]*tt+cx[3]*ttt;
y=cy[0]+cy[1]*t+cy[2]*tt+cy[3]*ttt;
z=cz[0]+cz[1]*t+cz[2]*tt+cz[3]*ttt;
glVertex3d(x,y,z);
}
glEnd();
}
glLineWidth(1);
glFlush();
SwapBuffers(hdc);
}
//---------------------------------------------------------------------------
I also used mine dynamic list template so:
List<double> xxx; is the same as double xxx[];
xxx.add(5); adds 5 to end of the list
xxx[7] access array element (safe)
xxx.dat[7] access array element (unsafe but fast direct access)
xxx.num is the actual used size of the array
xxx.reset() clears the array and set xxx.num=0
xxx.allocate(100) preallocate space for 100 items
just to be sure the code is comprehensable.
And preview:
When you want to edit your path its better to control the interpolation cubic control points instead of the bezier as you learned the hard way those are not as intuitive and easy to manipulate to achieve wanted output.
[Edit1] input points better matching your shape
As you finally provided image of shape you want ... you simply sample some points along the path and convert that into bezier. So the only stuff that changes are the input points:
void generate()
{
int i,j,n;
double x,y,z,a,a0,a1,b,b0,b1,z0,dz,r,t;
const double deg=M_PI/180.0;
const double rad=180.0/M_PI;
// generate some helix path points
n=32; // number of points along path
r=0.75; // curve radius
z0=0.0; // mid height
dz=0.1; // height amplitude
a0=180.0*deg; a1= 0.0*deg; // angle range
b0= 30.0*deg; b1=+330.0*deg; // angle range
it4.num=0; // clear list of points
for (i=0;i<n;i++)
{
// parameters
t=double(i)/double(n-1);
a=a0+(a1-a0)*t;
b=b0+(b1-b0)*t;
// curve
x=r*cos(a);
y=r*sin(a);
// height
z=z0+dz*sin(b);
// add it to the list
it4.add(x);
it4.add(y);
it4.add(z);
}
// convert it4 into bz4 control points
bz4.num=0; // clear list of points
for (i=0;i<=it4.num-12;i+=3)
{
const double m=1.0/6.0;
double x0,y0,z0,x1,y1,z1,x2,y2,z2,x3,y3,z3;
double X0,Y0,Z0,X1,Y1,Z1,X2,Y2,Z2,X3,Y3,Z3;
j=i;
X0=it4[j]; j++; Y0=it4[j]; j++; Z0=it4[j]; j++;
X1=it4[j]; j++; Y1=it4[j]; j++; Z1=it4[j]; j++;
X2=it4[j]; j++; Y2=it4[j]; j++; Z2=it4[j]; j++;
X3=it4[j]; j++; Y3=it4[j]; j++; Z3=it4[j]; j++;
x0 = X1; y0 = Y1; z0 = Z1;
x1 = X1-(X0-X2)*m; y1 = Y1-(Y0-Y2)*m; z1 = Z1-(Z0-Z2)*m;
x2 = X2+(X1-X3)*m; y2 = Y2+(Y1-Y3)*m; z2 = Z2+(Z1-Z3)*m;
x3 = X2; y3 = Y2; z3 = Z2;
bz4.add(x0); bz4.add(y0); bz4.add(z0);
bz4.add(x1); bz4.add(y1); bz4.add(z1);
bz4.add(x2); bz4.add(y2); bz4.add(z2);
bz4.add(x3); bz4.add(y3); bz4.add(z3);
}
}
Here preview:
And preview with N=8 points:
I simply separated curve and height into circular path with parameter a and sinusoid with parameter b. As you can see the conversion code is the same no matter the change of input points ...
You have some segment of 3d curve with known tangents at endpoints and want to build Bezier approximation.
Inner control points of Bezier curve will lie on vectors collinear with tangent vectors. But you need to know their length.
Approximation approach for circle arc chooses such length of these vectors to provide middle Bezier point coinciding with middle point of arc. You can apply the same method here. Write
P1 = P0 + T0 * L
P2 = P3 - T3 * L
substitute in Bezier equation with t=1/2, P = middle of the curve and find unknown L. Make this for all three components and get some average providing rather good error (perhaps some optimization is possible).
If curve is highly unsymmetric - someone may try to use different lengths for both tangents.

Making a Pen's LineCap go more into the drawn Line/Curve

Currently I have the following code:
AdjustableArrowCap arrow = new AdjustableArrowCap(10, 15, false);
penStateOutline.CustomEndCap = arrow;
And it draws this:
I have tried all day to make the arrow point to the ellipse itself rather than the center of it..
Update (I was wrong about the cap extending the line; it doesn't!)
To let the line and its cap end at the cirlce's outside you need to make the line shorter by the radius of the circle.
There are two approaches:
You can find a new endpoint that sits on the circle by calculating it, either with Pythagoras or by trigonometry. Then replace the endpoint, i.e. the circle's center, when drawing the line or curve by that new point.
Or put the other way round: You need to calculate a point on the circle as the new endpoint of the line.
It requires a little math, unless the line is horizontal or vertical...
This will work well for straight lines but for curves it may cause considerable changes in the shape, depending on how close the points get and how curved the shape is.
This may or may not be a problem.
To avoid it you can replace the curve points by a series of line points that are close enough to look like a curve when drawn. From the list of points we subtract all those that do not lie inside the circle.
This sounds more complicated than it is as there is a nice class called GraphicsPath that will allow you to add a curve and then flatten it. The result is a more or less large number of points. The same class also allows you to determine whether a point lies inside a shape, i.e. our case inside the circle.
To implement the latter approach, here is a routine that transforms a list of curve points to a list of line points that will end close to the circle..:
void FlattenCurveOutside(List<Point> points, float radius)//, int count)
{
using (GraphicsPath gp = new GraphicsPath())
using (GraphicsPath gpc = new GraphicsPath())
{
// firt create a path that looks like our circle:
PointF l = points.Last();
gpc.AddEllipse(l.X - radius, l.Y - radius, radius * 2, radius* 2);
// next one that holds the curve:
gp.AddCurve(points.ToArray());
// now we flatten it to a not too large number of line segments:
Matrix m = new Matrix(); // dummy matrix
gp.Flatten(m, 0.75f); // <== play with this param!!
// now we test the pathpoints from bach to front until we have left the circle:
int k = -1;
for (int i = gp.PathPoints.Length - 1; i >= 0; i--)
{
if ( !gpc.IsVisible(gp.PathPoints[i])) k = i;
if (k > 0) break;
}
// now we know how many pathpoints we want to retain:
points.Clear();
for (int i = 1; i <= k; i++)
points.Add(Point.Round(gp.PathPoints[i]));
}
}
Note that when the last part of the curve is too straight the result may look a little jagged..
Update 2
To implement the former approach here is a function that returns a PointF on a circle of radius r and a line connecting a Point b with the circle center c:
PointF Intersect(Point c, Point a, int rad)
{
float dx = c.X - a.X;
float dy = c.Y - a.Y;
var radians = Math.Atan2(dy, dx);
var angle = 180 - radians * (180 / Math.PI);
float alpha = (float)(angle * Math.PI / 180f);
float ry = (float)(Math.Sin(alpha) * rad );
float rx = (float)(Math.Cos(alpha) * rad );
return new PointF(c.X + rx, c.Y - ry);
}
The result thankfully looks rather similar:
The math can be simplified a little..
To apply it you can get the new endpoint and replace the old one:
PointF I = Intersect(c, b, r);
points2[points2.Count - 1] = Point.Round(I);
Thank you so much to #TaW for helping.
Unfortunately the answer he provided did not match my stupid OCD..
You made me think about the problem from a different perspective and I now have found a solution that I'll share if anyone else needs, note that the solution is not perfect.
public static Point ClosestPointOnCircle(int rad, PointF circle, PointF other)
{
if (other.X == circle.X && other.Y == circle.Y) // dealing with division by 0
other.Y--;
return new Point((int)Math.Floor(circle.X + rad * ((other.X - circle.X) / (Math.Sqrt(Math.Pow(other.X - circle.X, 2) + Math.Pow(other.Y - circle.Y, 2))))), (int)Math.Floor(circle.Y + rad * ((other.Y - circle.Y) / (Math.Sqrt(Math.Pow(other.X - circle.X, 2) + Math.Pow(other.Y - circle.Y, 2))))));
}
What the code does is return a point on the circle that is the closest to the another point, then, I used the curve middle point as the other point to change the end point of the curve.
Then I used the arrow cap as normal and got this:image
Which is good enough for my project.

How to find the interval of a constant moviment in a bezier path

I get the bezier value from a path like this (note: If there is another/better way to do that let me know, please):
public static Vector3 PathCubic (float t, Vector3[] path)
{
if (t >= 1f)
return path[path.Length - 1];
// projects t in the path
var projT = path.Length / 3 * t;
// what interval t is (between P1 and P2 or between P2 and P3, etc)
var range = (int)projT;
// get the interval index
var i = range * 3;
var p0 = path[i + 0]; // first point
var c0 = path[i + 1]; // control 1
var c1 = path[i + 2]; // control 2
var p1 = path[i + 3]; // second point
// calculate bezier in the current interval
return Cubic(projT - range, p0, c0, c1, p1);
}
So, supposing points P1, P2, P3, P4, t = 0.0 is the first point P1 and t = 1.0 is the last point P4.
But, that doesn't give me a constant moviment over the bezier. Image bellow depicts what I mean. A t = 0.5 give me different positions depending on points location.
I found I have to calculate the length of the path in order to achieve that. And here comes the problem. Once calculated this length, how can I calculate the interval index, as I did before? Or I will need to walk through each interval (for instruction) in order to find that?
// get the ***interval index***
var i = range * 3;
var p0 = path[i + 0]; // first point
var c0 = path[i + 1]; // control 1
var c1 = path[i + 2]; // control 2
var p1 = path[i + 3]; // second point
There is no true solution but here is a usefull approximation:
First get the length of each segment. Let's say it's:
// |a b| is the operator of length between two points a and b
A=|P1 P2|=3.0
B=|P2 P3|=1.2
C=|P3 P4|=0.8
L=A+B+C=5.0 // length of the whole spline
Your t=0.5 and is between points P1 and P2 - we know that because t*L = 2.5 and length A=3 (and A is first, so the immaginary length before was 0).
We also know that the value of t that we want should be on 2.5/3.0 =~ 0.833 of the length between points P1 and P2.
Note that P1was calculated with t = 0 and P2 with t =~ 0.333.
We can now find the fixed value of t: Tfix=Mathf.Lerp(0, 0.333, 0.833) =~ 0.277, where:
0 - t of point P1
0.333 - t of point P2
0.833 - the approximated position of searched t relative to closest approximation points.
Finally to find the point you were searching for substitute Tfix for t: PathCubic(Tfix)
Also, think about using more approximation points. For more demanding uses I needed dividing the segment in 16 pieces.
Mathematical answer: in general you can't. There is no symbolic way to determine the length of your cubic curve (it'd require solving a 6th order polynomial, which cannot be with regular formula manipulations), so there's no way to straight-up compute which t value you need for distance d along your curve.
The fastest solution is typically to index your curve: at fixed t intervals, build a lookup table with the length of the curve up to that point. Then use that LUT to do estimation. If your curve is length 42, and your LUT is distance = [0,5,12,25,37,42] for corresponding t = [0,1/5,2/5,3/5,4/5,1] then now you can use that data at moment to quickly estimate which t values you need to highlight which fixed distances, using simple linear interpolation.

Get the distance from a Point to a Geometry

I have System.Windows.Media.Geometry g and System.Windows.Point p.
I want to know the shortest distance between the point and the outline of the geometry. How should I do it?
Here is my effort:
Geometry.GetOutlinedPathGeometry() returns PathGeometry.
PathGeometry.Figures returns PathFigure.
PathFigure.Segments returns PathSegment.
No useful method on PathSegment...
Basically, what you need to do is go through every point on that path that you got from your geometry and measure distance between one of those points and the segregated point.
There's a post on SO that finds the closest point to the segregated point:
https://stackoverflow.com/a/19031758/2006048
And there's a C++ algorithm that does the distance measuring. You'll just have to convert it to C#:
https://stackoverflow.com/a/1501725/2006048
You can probably also use Point.Subtract() method to get and compare Vectors between each of those points.
If you can get an array or list of points from that shape, then you can probably do something like this (Note that this is not as elaborate as the links I've provided. This will give you the shortest distance to one of the available points and not the segment itself):
public static double GetShortestDistance(Point target, Point[] points)
{
var dist = 0;
foreach (var point in points)
{
var xDist = target.X - point.X;
var yDist = target.Y - point.Y;
var nDist = Math.Sqrt(xDist * xDist + yDist * yDist);
if (nDist < dist)
{
dist = nDist;
}
}
return dist;
}
I recommend using the C++ algorithm in the second link.

Approximating an ellipse with a polygon

I am working with geographic information, and recently I needed to draw an ellipse. For compatibility with the OGC convention, I cannot use the ellipse as it is; instead, I use an approximation of the ellipse using a polygon, by taking a polygon which is contained by the ellipse and using arbitrarily many points.
The process I used to generate the ellipse for a given number of point N is the following (using C# and a fictional Polygon class):
Polygon CreateEllipsePolygon(Coordinate center, double radiusX, double radiusY, int numberOfPoints)
{
Polygon result = new Polygon();
for (int i=0;i<numberOfPoints;i++)
{
double percentDone = ((double)i)/((double)numberOfPoints);
double currentEllipseAngle = percentDone * 2 * Math.PI;
Point newPoint = CalculatePointOnEllipseForAngle(currentEllipseAngle, center, radiusX, radiusY);
result.Add(newPoint);
}
return result;
}
This has served me quite while so far, but I've noticed a problem with it: if my ellipse is 'stocky', that is, radiusX is much larger than radiusY, the number of points on the top part of the ellipse is the same as the number of points on the left part of the ellipse.
That is a wasteful use of points! Adding a point on the upper part of the ellipse would hardly affect the precision of my polygon approximation, but adding a point to the left part of the ellipse can have a major effect.
What I'd really like, is a better algorithm to approximate the ellipse with a polygon. What I need from this algorithm:
It must accept the number of points as a parameter; it's OK to accept the number of points in every quadrant (I could iteratively add points in the 'problematic' places, but I need good control on how many points I'm using)
It must be bounded by the ellipse
It must contain the points straight above, straight below, straight to the left and straight to the right of the ellipse's center
Its area should be as close as possible to the area of the ellipse, with preference to optimal for the given number of points of course (See Jaan's answer - appearantly this solution is already optimal)
The minimal internal angle in the polygon is maximal
What I've had in mind is finding a polygon in which the angle between every two lines is always the same - but not only I couldn't find out how to produce such a polygon, I'm not even sure one exists, even if I remove the restrictions!
Does anybody have an idea about how I can find such a polygon?
finding a polygon in which the angle between every two lines is
always the same
Yes, it is possible. We want to find such points of (the first) ellipse quadrant, that angles of tangents in these points form equidistant (the same angle difference) sequence. It is not hard to find that tangent in point
x=a*Cos(fi)
y=b*Sin(Fi)
derivatives
dx=-a*Sin(Fi), dy=b*Cos(Fi)
y'=dy/dx=-b/a*Cos(Fi)/Sin(Fi)=-b/a*Ctg(Fi)
Derivative y' describes tangent, this tangent has angular coefficient
k=b/a*Cotangent(Fi)=Tg(Theta)
Fi = ArcCotangent(a/b*Tg(Theta)) = Pi/2-ArcTan(a/b*Tg(Theta))
due to relation for complementary angles
where Fi varies from 0 to Pi/2, and Theta - from Pi/2 to 0.
So code for finding N + 1 points (including extremal ones) per quadrant may look like (this is Delphi code producing attached picture)
for i := 0 to N - 1 do begin
Theta := Pi/2 * i / N;
Fi := Pi/2 - ArcTan(Tan(Theta) * a/b);
x := CenterX + Round(a * Cos(Fi));
y := CenterY + Round(b * Sin(Fi));
end;
// I've removed Nth point calculation, that involves indefinite Tan(Pi/2)
// It would better to assign known value 0 to Fi in this point
Sketch for perfect-angle polygon:
One way to achieve adaptive discretisations for closed contours (like ellipses) is to run the Ramer–Douglas–Peucker algorithm in reverse:
1. Start with a coarse description of the contour C, in this case 4
points located at the left, right, top and bottom of the ellipse.
2. Push the initial 4 edges onto a queue Q.
while (N < Nmax && Q not empty)
3. Pop an edge [pi,pj] <- Q, where pi,pj are the endpoints.
4. Project a midpoint pk onto the contour C. (I expect that
simply bisecting the theta endpoint values will suffice
for an ellipse).
5. Calculate distance D between point pk and edge [pi,pj].
if (D > TOL)
6. Replace edge [pi,pj] with sub-edges [pi,pk], [pk,pj].
7. Push new edges onto Q.
8. N = N+1
endif
endwhile
This algorithm iteratively refines an initial discretisation of the contour C, clustering points in areas of high curvature. It terminates when, either (i) a user defined error tolerance TOL is satisfied, or (ii) the maximum allowable number of points Nmax is used.
I'm sure that it's possible to find an alternative that's optimised specifically for the case of an ellipse, but the generality of this method is, I think, pretty handy.
I assume that in the OP's question, CalculatePointOnEllipseForAngle returns a point whose coordinates are as follows.
newPoint.x = radiusX*cos(currentEllipseAngle) + center.x
newPoint.y = radiusY*sin(currentEllipseAngle) + center.y
Then, if the goal is to minimize the difference of the areas of the ellipse and the inscribed polygon (i.e., to find an inscribed polygon with maximal area), the OP's original solution is already an optimal one. See Ivan Niven, "Maxima and Minima Without Calculus", Theorem 7.3b. (There are infinitely many optimal solutions: one can get another polygon with the same area by adding an arbitrary constant to currentEllipseAngle in the formulae above; these are the only optimal solutions. The proof idea is quite simple: first one proves that these are the optimal solutions in case of a circle, i.e. if radiusX=radiusY; secondly one observes that under a linear transformation that transforms a circle into our ellipse, e.g. a transformation of multiplying the x-coordinate by some constant, all areas are multiplied by a constant and therefore a maximal-area inscribed polygon of the circle is transformed into a maximal-area inscribed polygon of the ellipse.)
One may also regard other goals, as suggested in the other posts: e.g. maximizing the minimal angle of the polygon or minimizing the Hausdorff distance between the boundaries of the polygon and ellipse. (E.g. the Ramer-Douglas-Peucker algorithm is a heuristic to approximately solve the latter problem. Instead of approximating a polygonal curve, as in the usual Ramer-Douglas-Peucker implementation, we approximate an ellipse, but it is possible to devise a formula for finding on an ellipse arc the farthest point from a line segment.) With respect to these goals, the OP's solution would usually not be optimal and I don't know if finding an exact solution formula is feasible at all. But the OP's solution is not as bad as the OP's picture shows: it seems that the OP's picture has not been produced using this algorithm, as it has less points in the more sharply curved parts of the ellipse than this algorithm produces.
I suggest you switch to polar coordinates:
Ellipse in polar coord is:
x(t) = XRadius * cos(t)
y(t) = YRadius * sin(t)
for 0 <= t <= 2*pi
The problems arise when Xradius >> YRadius (or Yradius >> Yradius)
Instead of using numberOfPoints you can use an array of angles obviously not all identical.
I.e. with 36 points and dividing equally you get angle = 2*pi*n / 36 radiants for each sector.
When you get around n = 0 (or 36) or n = 18 in a "neighborhood" of these 2 values the approx method doesn't works well cause the ellipse sector is significantly different from the triangle used to approximate it. You can decrease the sector size around this points thus increasing precision. Instead of just increasing the number of points that would also increase segments in other unneeded areas. The sequence of angles should become something like (in degrees ):
angles_array = [5,10,10,10,10.....,5,5,....10,10,...5]
The first 5 deg. sequence is for t = 0 the second for t = pi, and again the last is around 2*pi.
Here is an iterative algorithm I've used.
I didn't look for theoretically-optimal solution, but it works quit well for me.
Notice that this algorithm gets as an input the maximal error of the prime of the polygon agains the ellipse, and not the number of points as you wish.
public static class EllipsePolygonCreator
{
#region Public static methods
public static IEnumerable<Coordinate> CreateEllipsePoints(
double maxAngleErrorRadians,
double width,
double height)
{
IEnumerable<double> thetas = CreateEllipseThetas(maxAngleErrorRadians, width, height);
return thetas.Select(theta => GetPointOnEllipse(theta, width, height));
}
#endregion
#region Private methods
private static IEnumerable<double> CreateEllipseThetas(
double maxAngleErrorRadians,
double width,
double height)
{
double firstQuarterStart = 0;
double firstQuarterEnd = Math.PI / 2;
double startPrimeAngle = Math.PI / 2;
double endPrimeAngle = 0;
double[] thetasFirstQuarter = RecursiveCreateEllipsePoints(
firstQuarterStart,
firstQuarterEnd,
maxAngleErrorRadians,
width / height,
startPrimeAngle,
endPrimeAngle).ToArray();
double[] thetasSecondQuarter = new double[thetasFirstQuarter.Length];
for (int i = 0; i < thetasFirstQuarter.Length; ++i)
{
thetasSecondQuarter[i] = Math.PI - thetasFirstQuarter[thetasFirstQuarter.Length - i - 1];
}
IEnumerable<double> thetasFirstHalf = thetasFirstQuarter.Concat(thetasSecondQuarter);
IEnumerable<double> thetasSecondHalf = thetasFirstHalf.Select(theta => theta + Math.PI);
IEnumerable<double> thetas = thetasFirstHalf.Concat(thetasSecondHalf);
return thetas;
}
private static IEnumerable<double> RecursiveCreateEllipsePoints(
double startTheta,
double endTheta,
double maxAngleError,
double widthHeightRatio,
double startPrimeAngle,
double endPrimeAngle)
{
double yDelta = Math.Sin(endTheta) - Math.Sin(startTheta);
double xDelta = Math.Cos(startTheta) - Math.Cos(endTheta);
double averageAngle = Math.Atan2(yDelta, xDelta * widthHeightRatio);
if (Math.Abs(averageAngle - startPrimeAngle) < maxAngleError &&
Math.Abs(averageAngle - endPrimeAngle) < maxAngleError)
{
return new double[] { endTheta };
}
double middleTheta = (startTheta + endTheta) / 2;
double middlePrimeAngle = GetPrimeAngle(middleTheta, widthHeightRatio);
IEnumerable<double> firstPoints = RecursiveCreateEllipsePoints(
startTheta,
middleTheta,
maxAngleError,
widthHeightRatio,
startPrimeAngle,
middlePrimeAngle);
IEnumerable<double> lastPoints = RecursiveCreateEllipsePoints(
middleTheta,
endTheta,
maxAngleError,
widthHeightRatio,
middlePrimeAngle,
endPrimeAngle);
return firstPoints.Concat(lastPoints);
}
private static double GetPrimeAngle(double theta, double widthHeightRatio)
{
return Math.Atan(1 / (Math.Tan(theta) * widthHeightRatio)); // Prime of an ellipse
}
private static Coordinate GetPointOnEllipse(double theta, double width, double height)
{
double x = width * Math.Cos(theta);
double y = height * Math.Sin(theta);
return new Coordinate(x, y);
}
#endregion
}

Categories

Resources