My question is similar to How to Make a Point Orbit a Line, 3D but the answer there didn\'t seem to solve my problem. And what I am looking for is a general solution.
What you need is a new coordinate system to place the circle. As any common coordinate system, we'll want the base vectors to be orthogonal to each other, and have length 1 each. I'll name the base vectors v1
, v2
, and v3
, which correspond to x, y, and z in order.
The new base vector that replaces z, which is v3
is given by the normal vector of the circle. If it's not normalized yet, you'll want to normalize it here:
[ v3x ]
v3 = [ v3y ] = normalize(circleNormal)
[ v3z ]
Next, we'll chose v1
. This can be an arbitrary vector that is orthogonal to v3
. Since we want it to take the place of the x-axis, we can choose it to have an y-component of 0:
[ v3z ]
v1 = normalize([ 0 ])
[ -v3x]
Note that the dot product of this vector with v3
is 0, which means that the two vectors are indeed orthogonal. The vector will be degenerate if the normal vector of the circle points exactly in the y-direction. I'll let you figure out how to handle that if it's a concern in your usage.
Now we just need the last vector, which can be calculated as the cross product of the other two:
v2 = v3 x v1
This will already be normalized since v1
and v3
were normalized, and are orthogonal.
With this new basis, points on the circle can now be calculated as:
p = centerPoint + R * (cos(a) * v1 + sin(a) * v2)
Putting the whole thing closer to code form (untested):
// Only needed if normal vector (nx, ny, nz) is not already normalized.
float s = 1.0f / (nx * nx + ny * ny + nz * nz);
float v3x = s * nx;
float v3y = s * ny;
float v3z = s * nz;
// Calculate v1.
s = 1.0f / (v3x * v3x + v3z * v3z);
float v1x = s * v3z;
float v1y = 0.0f;
float v1z = s * -v3x;
// Calculate v2 as cross product of v3 and v1.
// Since v1y is 0, it could be removed from the following calculations. Keeping it for consistency.
float v2x = v3y * v1z - v3z * v1y;
float v2y = v3z * v1x - v3x * v1z;
float v2z = v3x * v1y - v3y * v1x;
// For each circle point.
px = cx + r * (v1x * cos(a) + v2x * sin(a))
py = cy + r * (v1y * cos(a) + v2y * sin(a))
pz = cz + r * (v1z * cos(a) + v2z * sin(a))
So, in working with @Timothy Shields in comments on How to Make a Point Orbit a Line, 3D I got my answer. Here is an excerpt from my resulting Circle class if anyone is interested. The normalized
member on the Vector
class simply divides each of the vector's components by the vector length to return a unit vector. Circle
, Vector
, and Point
are all classes I have created for my app.
public class Circle {
public final Point center;
public final float radius;
public final Vector normal;
....
public Point pointAt(float angle) {
float xv = (float) Math.cos(angle);
float yv = (float) Math.sin(angle);
Vector v = findV();
Vector w = v.crossProduct(normal);
// Return center + r * (V * cos(a) + W * sin(a))
Vector r1 = v.scale(radius*xv);
Vector r2 = w.scale(radius*yv);
return new Point(center.x + r1.x + r2.x,
center.y + r1.y + r2.y,
center.z + r1.z + r2.z);
}
private Vector findV() {
Vector vp = new Vector(0f, 0f, 0f);
if (normal.x != 0 || normal.y != 0) {
vp = new Vector(0f, 0f, 1f);
} else if (normal.x != 0 || normal.z != 0) {
vp = new Vector(0f, 1f, 0f);
} else if (normal.y != 0 || normal.z != 0) {
vp = new Vector(1f, 0f, 0f);
} else {
return null; // will cause an exception later.
}
Vector cp = normal.crossProduct(vp);
return cp.normalized();
}
}
Did an implementation of this using the toxilib library for 3D graphics following tactic in the answer from Reto. Did two versions, getPointOnCircle uses the unforked original toxilib, and getPointOnCircleD which uses a forked toxiLib version employing doubles.
I agree with the comments that question as asked, is incomplete. The starting location of the circle is not specified (what location corresponds to angle==0.0 ?). I would add that the orientation of the circle is also not specified ( Clockwize or CounterClockwise?). To conform to the North American math and physics practice, I multiplied Reto's angles by -1 to get my desired CCW orientation for axies having left hand palm toward +X, fingers toward +Y, thumb toward +Z. In the test cases I document the orientations which result from the from the code.
My use case is to draw cylinders and helices in the https://processing.org/ environment. Toward this end, I included a method which returns both the point on the circle, and a normal thereto.
/* 9 test cases seem necessary.
* 1 from: zero length normal input (Choose to not implement protection from this condition in default method)
* 1 from: normal unaligned with any axis,
* 3 from: normal in any of the three axial planes,
* 3 from: normal along any of the three axies
* 1 from: 1.0 != normal.magnitude() (Choose to not implement protection from this condition in default method)
*/
//VecD3D normal = new VecD3D();
//normal = new VecD3D(1.0,1.0,1.0).normalize(); /* path 0, sets 0==angle near 0.7071,0.0000,0.7071 CCW from -1,-1,-1 */
//normal = new VecD3D(1.0,0.0,0.0); /* path 0, sets 0==angle at -Z CCW from -X */
//normal = new VecD3D(0.0,1.0,0.0); /* path 1, sets 0==angle at +X CCW from -Y */
//normal = new VecD3D(0.0,0.0,1.0); /* path 0, sets 0==angle at +X CCW from -Z */
//normal = new VecD3D(1.0,1.0,0.0).normalize(); /* path 0, sets 0==angle at -Z CCW from -1,-1, 0 */
//normal = new VecD3D(0.0,1.0,1.0).normalize(); /* path 0, sets 0==angle at +X CCW from 0,-1,-1 */
//normal = new VecD3D(1.0,0.0,1.0).normalize(); /* path 0, sets 0==angle at +X CCW from 1, 0, 1 */
//normal = new VecD3D(100.,100.,100.); /* path 0, sets 0==angle near 0.7071,0.0000,0.7071 CCW from -1,-1,-1 */
/* based on https://stackoverflow.com/questions/27714014/3d-point-on-circumference-of-a-circle-with-a-center-radius-and-normal-vector
* This uses the extension of the toxiclibs.org 3D vector class extension fork providing doubles based vectors https://github.com/TPMoyer/toxiclibs
* This method does not check that the normal is normalized, and does not check that the normal is not the zero vector
*/
import toxi.geom.*;
VecD3D getPointOnCircleD(VecD3D v0, VecD3D normal,double angle,double radius){
/* If you are not confident that the input normal will always have
* 1.0==normal.magnitude()
* uncomment the last two lines of this comment block.
*
* Two actions should be taken in order,
* 1'st if the input normal is the zero vector, insert a normal of your choice (I like up, because you should always know which way is up)
* 2'nd normalize the vector
* The need for the ordering is because
* true == new VecD3D().normalize().isZeroVector(); // use .isZeroVector() instead of == compare to VecD3D.ZERO as the later fails
* The expected most likely source for a zero length normal is from an unmodified instance from a VecD3D default constructor
* VecD3D normal = new VecD3D();
*
* if(normal.isZeroVector())normal=new VecD3D(0.,0.,1.);
* normal=normal.normalize();
*/
if(normal.x != 0. || normal.z != 0.){
VecD3D v1 = new VecD3D(normal.z,0.0,-normal.x).normalize();
VecD3D v2 = normal.cross(v1);
//log.info("getPointOnCircleD path 0");
return (v0.add(v1.scale(Math.cos(-angle)).add(v2.scale(Math.sin(-angle))).scale(radius)));
} else {
VecD3D v1 = new VecD3D(normal.y,0.,-normal.x).normalize();
VecD3D v2 = normal.cross(v1);
//log.info("getPointOnCircleD path 1");
return (v0.add(v1.scale(Math.cos(-angle)).add(v2.scale(Math.sin(-angle))).scale(radius)));
}
}
/* based on https://stackoverflow.com/questions/27714014/3d-point-on-circumference-of-a-circle-with-a-center-radius-and-normal-vector
* This uses the extension of the toxiclibs.org 3D vector class extension fork into using doubles https://github.com/TPMoyer/toxiclibs
*/
VecD3D[] getPointAndNormalOnCircleD(VecD3D v0, VecD3D normal,double angle,double radius){
/* If you are not confident that the input normal will always have
* 1.0==normal.magnitude()
* uncomment the last two lines of this comment block.
*
* Two actions should be taken in order,
* 1'st if the input normal is the zero vector, insert a normal of your choice (I like up, because you should always know which way is up)
* 2'nd normalize the vector
* The need for the ordering is because
* true == new VecD3D().normalize().isZeroVector(); // use .isZeroVector() instead of == compare to VecD3D.ZERO as the later fails
* The expected most likely source for a zero length normal is from an unmodified instance from a VecD3D default constructor
* VecD3D normal = new VecD3D();
*
* if(normal.isZeroVector())normal=new VecD3D(0.,0.,1.);
* normal=normal.normalize();
*/
VecD3D[] out = new VecD3D[2];
if(normal.x != 0. || normal.z != 0.){
VecD3D v1 = new VecD3D(normal.z,0.0,-normal.x).normalize();
VecD3D v2 = normal.cross(v1);
out[1]=v1.scale(Math.cos(-angle)).add(v2.scale(Math.sin(-angle)));
out[0]=v0.add(out[1].scale(radius));
} else {
VecD3D v1 = new VecD3D(normal.y,0.,-normal.x).normalize();
VecD3D v2 = normal.cross(v1);
out[1]=v1.scale(Math.cos(-angle)).add(v2.scale(Math.sin(-angle)));
out[0]=v0.add(out[1].scale(radius));
}
return out;
}
/* based on https://stackoverflow.com/questions/27714014/3d-point-on-circumference-of-a-circle-with-a-center-radius-and-normal-vector
* This uses the the toxiclibs.org 3D vector class http://toxiclibs.org/
*/
Vec3D getPointOnCircle(Vec3D v0, Vec3D normal,float angle,float radius){
/* If you are not confident that the input normal will always have
* 1.0==normal.magnitude()
* uncomment the last two lines of this comment block.
*
* Two actions should be taken in order,
* 1'st if the input normal is the zero vector, insert a normal of your choice (I like up, because you should always know which way is up)
* 2'nd normalize the vector
* The need for the ordering is because
* true == new VecD3D().normalize().isZeroVector(); // use .isZeroVector() instead of == compare to VecD3D.ZERO as the later fails
* The expected most likely source for a zero length normal is from an unmodified instance from a VecD3D default constructor
* VecD3D normal = new VecD3D();
*
* if(normal.isZeroVector())normal=new VecD3D(0.,0.,1.);
* normal=normal.normalize();
*/
if(normal.x != 0. || normal.z != 0.){
Vec3D v1 = new Vec3D(normal.z,0.0,-normal.x).normalize();
Vec3D v2 = normal.cross(v1);
return new Vec3D((v0.add(v1.scale((float)Math.cos(-angle)).add(v2.scale((float)Math.sin(-angle))).scale(radius))));
} else {
Vec3D v1 = new Vec3D(normal.y,0.,-normal.x).normalize();
Vec3D v2 = normal.cross(v1);
return (v0.add(v1.scale((float)Math.cos(-angle)).add(v2.scale((float)Math.sin(-angle))).scale(radius)));
}
}