射线箱相交理论
Posted
技术标签:
【中文标题】射线箱相交理论【英文标题】:Ray-box Intersection Theory 【发布时间】:2011-02-03 13:33:58 【问题描述】:我希望确定射线和盒子之间的交点。盒子由它的最小 3D 坐标和最大 3D 坐标定义,射线由它的原点和它所指向的方向定义。
目前,我正在为盒子的每个面形成一个平面,并将射线与平面相交。如果射线与平面相交,那么我检查交点是否实际上在盒子的表面上。如果是,我检查它是否是这条射线最近的交点并返回最近的交点。
我检查平面交点是否在盒子表面上的方法是通过一个函数
bool PointOnBoxFace(R3Point point, R3Point corner1, R3Point corner2)
double min_x = min(corner1.X(), corner2.X());
double max_x = max(corner1.X(), corner2.X());
double min_y = min(corner1.Y(), corner2.Y());
double max_y = max(corner1.Y(), corner2.Y());
double min_z = min(corner1.Z(), corner2.Z());
double max_z = max(corner1.Z(), corner2.Z());
if(point.X() >= min_x && point.X() <= max_x &&
point.Y() >= min_y && point.Y() <= max_y &&
point.Z() >= min_z && point.Z() <= max_z)
return true;
return false;
其中corner1
是矩形面的一个角,corner2
是对角。我的实现大部分时间都有效,但有时它给了我错误的交集。请看图片:
图像显示了来自相机眼睛并撞击盒子表面的光线。其他光线是盒子表面的法线。可以看出,特别是一条光线(实际上是看到的法线)从盒子的“背面”出来,而法线应该从盒子的顶部出来。这似乎很奇怪,因为还有多条其他光线正确地击中了盒子的顶部。
我想知道我检查交点是否在盒子上的方式是否正确,或者我是否应该使用其他算法。
谢谢。
【问题讨论】:
很棒的图表。这确实有助于说明问题。 【参考方案1】:代码看起来不错。尝试找到这个特定的射线并对其进行调试。
【讨论】:
是否有其他方法可以检查特定点是否在盒子的表面上?也许使用射线和 3D 点而不是明确检查 x,y,z 坐标? 你可以而且你应该。将射线视为向量并通过方程找到交点的x,y,z 我通过方程找到了交集的 x,y,z。现在的目标是找出这个 x,y,z 是否在盒子的某个面上,我想知道是否有一个方程 代码看起来不太好。它测试了 Myx 已经假设的东西,即一张脸被击中。 再次测试是多余的。【参考方案2】:编辑:忽略这个答案(参见下面的 cmets,我非常有说服力地展示了我的方式的错误)。
您正在测试该点是否在体积内,但该点位于该体积的外围,因此您可能会发现它是体积外的“无穷小”距离。尝试将盒子增加一些小的 epsilon,例如:
double epsilon = 1e-10; // Depends the scale of things in your code.
double min_x = min(corner1.X(), corner2.X()) - epsilon;
double max_x = max(corner1.X(), corner2.X()) + epsilon;
double min_y = min(corner1.Y(), corner2.Y()) - epsilon;
...
从技术上讲,比较几乎相等的数字的正确方法是将它们的位表示转换为整数,然后比较一些小偏移量的整数,例如,在 C 中:
#define EPSILON 10 /* some small int; tune to suit */
int almostequal(double a, double b)
return llabs(*(long long*)&a - *(long long*)&b) < EPSILON;
当然,这在 C# 中并不容易,但也许不安全的语义可以达到同样的效果。 (感谢@Novox 的cmets,这让我找到了一个很好的page,详细解释了这项技术。)
【讨论】:
似乎这对我上面显示的示例进行了修复。谢谢!但是,我觉得做出这样的改变有点不舒服。你会不会有另一个可能更几何相关的修复? 请注意不要选择太小的ε;否则你的 FPU 最终会弄乱 denorms(即非标准化浮点数),这会严重损害性能。 这对于某些问题来说是一个很好的答案,但对于 Myx 的问题却不是这样。现在你击中了你不应该错过但几乎没有错过的东西——你这样做是不必要的。 @Myx:浮点数使用二进制尾数 mmmm 和指数 eee 表示,它们一起表示数字 1.mmmm*2^eee。当数字太接近零时,eee 达到其负极限,并且该数字现在表示为 0.01mm*2^eee。这是一个非规范化的浮点数。这些数字的问题在于精度损失和 FPU 的更多工作。 @Marcelo:epsilon 比较对于某些问题非常有用,但不是这个问题。它是一个长方体,无论如何,它是一个体积碰撞测试,当你碰到平面时,你永远不需要进行ε(除非你的体积小于ε厚度)。这是多面体表面反射的好方法。【参考方案3】:会不会是那条光线最终正好穿过了盒子的边缘?浮点舍入错误可能会导致右侧和背面都错过它。
【讨论】:
从视觉反馈来看,似乎并非如此。但这也是我可能也应该处理的事情(直到以后我才会担心)。我该如何解决这个问题? (即,如果光线正好击中边缘或角落)【参考方案4】:通过 epsilon 增加事物实际上并不是一个很好的方法,因为您现在在盒子的边缘有一个大小为 epsilon 的边框,光线可以通过它。所以你会摆脱这组(相对常见的)奇怪的错误,并最终得到另一组(罕见的)奇怪的错误。
我假设您已经在设想您的光线以某种速度沿其矢量行进,并找到与每个平面相交的时间。因此,例如,如果您在x=x0
与平面相交,并且您的光线从(0,0,0)
向(rx,ry,rz)
方向前进,那么相交时间为t = x0/rx
。如果t
是否定的,请忽略它——你会走另一条路。如果t
为零,您必须决定如何处理这种特殊情况——如果您已经在飞机上,您是弹开它,还是穿过它?您可能还希望将rx==0
处理为特殊情况(这样您就可以碰到框的边缘)。
不管怎样,现在你有了你击中那架飞机的确切坐标:它们是(t*rx , t*ry , t*rz)
。现在您可以读取t*ry
和t*rz
是否在它们需要位于的矩形内(即在这些轴上的立方体的最小值和最大值之间)。 你不测试 x 坐标,因为你已经知道你击中了它再次,你必须决定是否/如何处理击中角落作为一种特殊情况。此外,现在您可以按时间排序与各种表面的碰撞,并选择第一个作为碰撞点。
这允许您在不使用任意 epsilon 因子的情况下计算您的光线是否与您的立方体相交以及在何处与您的立方体相交,以达到浮点运算可能的精度。
所以你只需要三个函数,就像你已经拥有的那个:一个用于测试你是否在yz
范围内点击,假设你点击了x
,以及xz
和xy
的相应函数,假设你分别点击y
和z
。
编辑:添加的代码(详细地)显示如何对每个轴进行不同的测试:
#define X_FACE 0
#define Y_FACE 1
#define Z_FACE 2
#define MAX_FACE 4
// true if we hit a box face, false otherwise
bool hit_face(double uhit,double vhit,
double umin,double umax,double vmin,double vmax)
return (umin <= uhit && uhit <= umax && vmin <= vhit && vhit <= vmax);
// 0.0 if we missed, the time of impact otherwise
double hit_box(double rx,double ry, double rz,
double min_x,double min_y,double min_z,
double max_x,double max_y,double max_z)
double times[6];
bool hits[6];
int faces[6];
double t;
if (rx==0) times[0] = times[1] = 0.0;
else
t = min_x/rx;
times[0] = t; faces[0] = X_FACE;
hits[0] = hit_box(t*ry , t*rz , min_y , max_y , min_z , max_z);
t = max_x/rx;
times[1] = t; faces[1] = X_FACE + MAX_FACE;
hits[1] = hit_box(t*ry , t*rz , min_y , max_y , min_z , max_z);
if (ry==0) times[2] = times[3] = 0.0;
else
t = min_y/ry;
times[2] = t; faces[2] = Y_FACE;
hits[2] = hit_box(t*rx , t*rz , min_x , max_x , min_z , max_z);
t = max_y/ry;
times[3] = t; faces[3] = Y_FACE + MAX_FACE;
hits[3] = hit_box(t*rx , t*rz , min_x , max_x , min_z , max_z);
if (rz==0) times[4] = times[5] = 0.0;
else
t = min_z/rz;
times[4] = t; faces[4] = Z_FACE;
hits[4] = hit_box(t*rx , t*ry , min_x , max_x , min_y , max_y);
t = max_z/rz;
times[5] = t; faces[5] = Z_FACE + MAX_FACE;
hits[5] = hit_box(t*rx , t*ry , min_x , max_x , min_y , max_y);
int first = 6;
t = 0.0;
for (int i=0 ; i<6 ; i++)
if (times[i] > 0.0 && (times[i]<t || t==0.0))
first = i;
t = times[i];
if (first>5) return 0.0; // Found nothing
else return times[first]; // Probably want hits[first] and faces[first] also....
(我只是输入了这个,没有编译它,所以要小心错误。)
(编辑:刚刚更正了i
-> first
。)
不管怎样,关键是你分别对待三个方向,并测试看看是否在(u,v)坐标的右框内发生了影响,其中(u,v)是(x,y) , (x,z) 或 (y,z) 取决于您击中的飞机。
【讨论】:
感谢您的详细回复。我也更喜欢使用不依赖于 epsilon 增量的方法。但是,用你的方法,我必须知道面平面不包括哪个轴,不是吗?如果是这样,我将如何准确检查我击中了哪个轴(即,你能在最后一句话中解释你的措辞吗?) 我已经添加了代码,希望能清楚地说明这一点。需要注意的关键是,您根据要测试的面进行不同的hit_box
测试。 (例如,第一个测试的是min_x
,我们看看影响点是否在yz
矩形内。)
感谢您的代码。这让我更清楚了。在您的代码中,您进行if(rx==0)...if(ry==0)...if(rz==0)
之类的检查。 “轴对齐的盒子”是否如此?我只是担心盒子的方向可能会导致其平面不一定位于 x、y 或 z 轴上。
这是为了处理光线平行于其中一个面的情况——否则我会除以零。在那种情况下,我说它总是使用上面的逻辑错过脸部。如果您的框未沿轴对齐,请旋转 vector(和原点)的坐标以使其对齐,然后进行比较 - 这样您就可以使用此处显示的确切测试。
感谢您的帮助和解释。非常感谢! =)【参考方案5】:
PointOnBoxFace
应该是二维检查而不是三维检查。例如,如果您针对z = z_min
平面进行测试,那么您只需要将x
和y
与它们各自的边界进行比较。您已经发现z
坐标是正确的。当您“重新检查”第三个坐标时,浮点精度可能会让您感到困惑。
例如,如果z_min
是 1.0,您首先针对该平面进行测试。您会找到 (x
, y
, 0.999999999) 的交点。现在,即使 x
和 y
在范围内,z
也不太正确。
【讨论】:
以上是关于射线箱相交理论的主要内容,如果未能解决你的问题,请参考以下文章