我的问题类似于 如何使一个点绕着一条线运行,3D 但那里的答案似乎并不能解决我的问题。 而我正在寻找的是一个通用的解决方案。
我试图用OpenGL ES(JavaAndroid)解决一个问题。
我有一个圆,它的中心是一个3D点,半径和一个3D向量,指定圆所在平面的法线。
我需要找到代表圆周上与 "旋转 "X轴(根据法线矢量旋转)成一定角度的3D点。
我已经有了一个在X轴上给定角度的3D点的实现。Circle
成员函数的类。pointAt
,这在有限的情况下是可行的。 具体来说,在我目前的实现中,我假设圆位于X-Y平面上,并相应地返回一个点,然后,由于我知道圆实际上位于X-Z平面上,我只需将返回点中的Y和Z值互换一下就可以了。 然而,这并不是一个通用的解决方案,这也是我所要的。
当我尝试了在 如何使一个点绕着一条线运行,3D我得到的点离它们应该在的地方很远。
那么,如何,才能计算出这样一个圆周上的点呢?
编辑]我想我的解释还不够充分。 我的假设是,圆在X-Y平面上是 "正常 "的,Z方向的法向量为(0,0,1)-1。 如果在圆周上需要一个点,该点的定义是:。
x = R*cos(a) + Cx
y = R*sin(a) + Cy
其中 R
是半径。Cx
和 Cy
是 X
和 Y
圆心的坐标,以及 a
是通过圆的中心点并与X轴平行的向量的角度。
现在,如果圆没有一个法向量指向Z轴,而是一些任意的(x,y,z)向量,我如何找到同一个点?
你需要的是一个新的坐标系来放置圆。就像任何普通的坐标系一样,我们希望基向量相互正交,并且每个基向量的长度为1。我会把基向量命名为 v1
, v2
和 v3
,它们依次对应x、y、z。
取代z的新的基向量,就是 v3
是由圆的法向量给出的。如果还没有归一化,你要在这里将其归一化。
[ v3x ]
v3 = [ v3y ] = normalize(circleNormal)
[ v3z ]
接下来,我们将选择 v1
. 这可以是一个任意的向量,它与 v3
. 由于我们希望它取代x轴,我们可以选择它的y分量为0。
[ v3z ]
v1 = normalize([ 0 ])
[ -v3x]
请注意,这个向量的点积与... v3
为0,这意味着两个向量确实是正交的。如果圆的法向量正好指向y方向,那么这个向量就会退化。如果你在使用中担心这个问题,我会让你想办法处理。
现在我们只需要最后一个向量,它可以作为其他两个向量的交叉乘积来计算。
v2 = v3 x v1
这个已经被归一化了,因为 v1
和 v3
被归一化,并且是正交的。
有了这个新的基础,现在圆上的点可以计算为。
p = centerPoint + R * (cos(a) * v1 + sin(a) * v2)
让整个过程更接近代码形式(未经测试)。
// 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))
所以,在与@Timothy Shields合作的过程中,他在评论中说: 如何使一个点绕着一条线运行,3D 我得到了我的答案。 下面是我的成果圈课的节选,如果有人感兴趣的话。 该 normalized
在座 Vector
类简单地将向量的每一个分量除以向量长度,返回一个单位向量。 Circle
, Vector
和 Point
都是我为我的应用程序创建的类。
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();
}
}
按照Reto的回答中的策略,使用3D图形的 toxilib库做了一个实现。 做了两个版本,getPointOnCircle使用的是未分叉的原始 toxilib,getPointOnCircleD使用的是分叉的 toxiLib版本,采用了双倍函数。
我同意评论中说的问题,问的不全面。 圆的起始位置没有指定(什么位置对应角度==0.0 ? 我想补充的是,圆的方向也没有指定(Clockwize还是CounterClockwise?)。为了符合北美数学和物理学的惯例,我将Reto的角度乘以-1,得到我想要的CCW方向的轴心,左手掌心朝向+X,手指朝向+Y,拇指朝向+Z。 在测试用例中,我记录了从代码中得到的方向。
我的用例是在代码中绘制圆柱体和螺旋体。https:/processing.org 的环境。 为此,我加入了一个方法,它既可以返回圆上的点,也可以返回其法线。
/* 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)));
}
}