这篇文章主要讲在3D空间中,一种简单的体素遍历算法。从一个体素到它临近的体素的计算,只需要去比较两个浮点数,比较后对其中一个添加。当然多条射线在多个物体中交互,在超过一个体素是不能用这种算法的。
在提到这种算法前,首先我们从简单的2D空间的直线生成算法开始。当我们要在屏幕上画一条直线时,由于屏幕由一个个像素(正方形)组成,所以实际上计算机显示的直线是由一些像素点近似组成的,直线生成算法解决的是如何选择最佳的一组像素来显示直线的问题。对于这个问题,最简单的方法是从直线起点开始令x或y每次增加1直到终点,每次根据直线方程计算对应的函数值再四舍五入取整,即可找到一个对应的像素,但这样做每一步都要进行浮点数乘法运算,效率极低,所以出现了DDA和Bresenham两种直线生成算法。
数值微分法(DDA算法)
float slope = (endPoint - startPoint).y / (endPoint - startPoint).x;
float deltaX = slope > 1 ? 1 / slope :1;
float deltaY = slope > 1 ? 1 : slope;
Vector2 vec = new Vector2();
for (int i = 0; i < 10; i++)
{
vec.x += deltaX;
vec.y += deltaY;
posList.Add(new Vector2(Mathf.Floor(vec.x), Mathf.Floor(vec.y)));
}
根据上式可知△x=1时,x每递增1,y就递增k,所以只需要对x和y不断递增就可以得到下一点的函数值,这样避免了对每一个像素都使用直线方程来计算,消除了浮点数乘法运算。但是还是需要加法和取整操作。
Bresenham算法
其中△x起点到终点x轴上距离,△y为y轴上距离,k=△y/△x,c是常量,与像素位置无关。
令ei=△x(d1-d2),则ei的计算仅包括整数运算,符号与d1-d2一致,称为误差量参数,当它小于0时,直线更接近右方像素,大于0时直线更接近右上方像素。
可利用递增整数运算得到后继误差量参数,计算如下:
所以选择右上方像素时(yi+1-yi=1):
初始时,将k=△y/△x代入△x(d1-d2)中可得到起始像素的第一个参数:
代码如下:
float dx, dy, e, x = startPoint.x, y = startPoint.y;
dx = endPoint.x - startPoint.x; dy = endPoint.y - startPoint.y;
e = 2 * dy - dx;
while (x <= endPoint.x)
{
if (e >= 0)
{
e = e + 2 * dy - 2 * dx;
y++;
vec.y = y;
}
else
e = e + 2 * dy;
x++;
vec.x = x;
posList.Add(new Vector2(vec.x, vec.y));
}
这些都是在2D空间下从一个点到另外一个点的算法优化,那么在3D空间中也是有对应的优化算法的。其中一点就是光线追踪算法中的射线遍历。
体素遍历优化算法
从上图可以知道,为了正确地遍历网格,遍历算法必须按顺序访问体素a、b、c、d、e、f、g和h。射线的方程为向量u加上t乘以向量v(t≥0)。新的遍历算法将射线分解为t段间隔,每个间隔一个体素。我们从射线原点开始,以间隔顺序访问每一个体素。
loop {if(tMaxX < tMaxY)
{
tMaxX= tMaxX + tDeltaX;X= X + stepX;
}
else
{
tMaxY= tMaxY + tDeltaY;Y= Y + stepY;}
NextVoxel(X,Y);
}
我们循环比较,直到找到一个具有非空对象列表的体素,否则从当前体素中退出。将算法扩展到三维只需要添加适当的z变量,并在每次迭代中找到tMaxX、tMaxY和tMaxZ的最小值。具体伪代码如下:
list= NIL;
do
{
if(tMaxX < tMaxY)
{
if(tMaxX < tMaxZ)
{
X= X + stepX;
if(X == justOutX)
return(NIL);
else/* outside grid */tMaxX= tMaxX + tDeltaX;}
{
Z= Z + stepZ;if(Z == justOutZ)return(NIL);
tMaxZ= tMaxZ + tDeltaZ;
}
} else
{
if(tMaxY < tMaxZ)
{
Y= Y + stepY;
if(Y == justOutY)
return(NIL);
tMaxY= tMaxY + tDeltaY;
} else
{
Z= Z + stepZ;
if(Z == justOutZ)
return(NIL);
tMaxZ= tMaxZ + tDeltaZ;
}
}
list= ObjectList[X][Y][Z];
}while(list == NIL);
return(list);
上面的需要进行两个浮点纹理数的比较,一个浮点纹理的加法,两个整形比较。每一次迭代一个整形加法。如果射线的起始点在体素里面,需要33个浮点计算,如果是在体素外面则需要40次计算。
首先,检查确保交点在体素内这样需要进行六次浮点数比较。但我们可以将其简化为一次比较。与当前体素中所剩的最大值相比。如果它小于或等于最大允许值,则它位于当前体素中;否则,我们继续遍历,直到与交叉点相交,或者找到一个更近的对象。执行这个比较,最简单的方法是在确定了tMaxX、tMaxY和tMaxZ的最小值之后将其包含到增量遍历代码中。当然,这里我们不想做这个比较。我们使用有两个遍历函数。在找到一个交集之后,我们再次调用递增遍历函数;这次我们需要进行额外的比较。如果交集在当前体素中,则函数返回NULL,然后停止。否则,我们继续遍历,直到找到一个非空的体素,或者找到发生交集的体素。因此,这种方式消耗最低,我们可以确定交集是否在当前体素内。
参考链接:cse.chalmers.se/edu/yea
2D算法链接:https://www.cnblogs.com/LiveForGame/p/11706904.html
声明:发布此文是出于传递更多知识以供交流学习之目的。若有来源标注错误或侵犯了您的合法权益,请作者持权属证明与我们联系,我们将及时更正、删除,谢谢。
作者:南山上
来源:https://zhuanlan.zhihu.com/p/126583341
More:【微信公众号】u3dnotes