Ray-AABB問題:判斷線段是否相交於軸對齊邊界框(Axially Aligned Bounding Box, AABB)

摘要

Ray-AABB問題:判斷線段是否相交於軸對齊邊界框(Axially Aligned Bounding Box, AABB)
本文介紹了slab算法的實現,從一個簡單實現開始,逐步優化slab算法,算法加速和並進行邊界情況處理。


本文轉載並翻譯自:

第一部分:簡單實現

軸對齊包圍盒(AABB)通常用於限制光線跟蹤中的有限對象。 射線/ AABB交點通常比精確的射線/對象交點計算得更快,並允許構造邊界體積層次結構(BVH),減少了每條射線需要考慮的對象數量。 (這將在以後的文章中詳細介紹BVH。)這意味着光線跟蹤器會花費大量時間來計算光線/ AABB交點,因此應高度優化此代碼。

進行ray / AABB相交的最快方法是slab方法。 想法是將盒子視爲三對平行平面內的空間。 射線被每對平行平面夾住,並且如果射線的任何部分保留下來,它將與盒子相交。
在這裏插入圖片描述

v1.0 簡單實現版本

此算法的簡單實現可能看起來像這樣(爲簡潔起見,在兩個維度中):

bool intersection(box b, ray r) {
    double tmin = -INFINITY, tmax = INFINITY;
 
    if (ray.n.x != 0.0) {
        double tx1 = (b.min.x - r.x0.x)/r.n.x;
        double tx2 = (b.max.x - r.x0.x)/r.n.x;
 
        tmin = max(tmin, min(tx1, tx2));
        tmax = min(tmax, max(tx1, tx2));
    }
 
    if (ray.n.y != 0.0) {
        double ty1 = (b.min.y - r.x0.y)/r.n.y;
        double ty2 = (b.max.y - r.x0.y)/r.n.y;
 
        tmin = max(tmin, min(ty1, ty2));
        tmax = min(tmax, max(ty1, ty2));
    }
 
    return tmax >= tmin;
}

但是,這些部門要花很多時間。 由於在進行射線追蹤時,同一束射線是針對許多ABB進行測試的,因此預先計算射線方向分量的逆數是有意義的。 如果我們可以依靠IEEE 754浮點屬性,則這也可以隱式處理方向分量爲零的邊緣情況-例如,如果射線在射線的範圍內,則tx1和tx2值將是相反符號的無窮大。 平板,因此tmin和tmax保持不變。 如果光線在平板之外,則tx1和tx2將是具有相同符號的無限大,從而使tmin == + inftmax == -inf,從而導致測試失敗。

v1.1 版本

最終的實現如下所示:

bool intersection(box b, ray r) {
    double tx1 = (b.min.x - r.x0.x)*r.n_inv.x;
    double tx2 = (b.max.x - r.x0.x)*r.n_inv.x;
 
    double tmin = min(tx1, tx2);
    double tmax = max(tx1, tx2);
 
    double ty1 = (b.min.y - r.x0.y)*r.n_inv.y;
    double ty2 = (b.max.y - r.x0.y)*r.n_inv.y;
 
    tmin = max(tmin, min(ty1, ty2));
    tmax = min(tmax, max(ty1, ty2));
 
    return tmax >= tmin;
}

由於現代浮點指令集可以在沒有分支的情況下計算最小值和最大值,因此可以進行無分支或除法的ray / AABB交集測試。

在我寫的光線追蹤器 Dimension 中對此的實現可以在這裏看到。

第二部分:考慮線段與平板重合的情況

在第1部分中,我概述了一種用於計算光線和與軸對齊的邊界框之間的交點的算法。 依靠IEEE 754浮點屬性消除分支的想法可以追溯到[1]中的Brian Smits,該實現被Amy Williams充實了。 等。 在[2]中。

v2.0

爲了快速回顧一下,該想法是替換樸素的平板方法:

bool intersection(box b, ray r) {
    double tmin = -INFINITY, tmax = INFINITY;
 
    for (int i = 0; i < 3; ++i) {
        if (ray.dir[i] != 0.0) {
            double t1 = (b.min[i] - r.origin[i])/r.dir[i];
            double t2 = (b.max[i] - r.origin[i])/r.dir[i];
 
            tmin = max(tmin, min(t1, t2));
            tmax = min(tmax, max(t1, t2));
        } else if (ray.origin[i] <= b.min[i] || ray.origin[i] >= b.max[i]) {
            return false;
        }
    }
 
    return tmax > tmin && tmax > 0.0;
}

v2.1

v2.0的等價寫法,但是比v2.0要更快:

bool intersection(box b, ray r) {
    double tmin = -INFINITY, tmax = INFINITY;
 
    for (int i = 0; i < 3; ++i) {
        double t1 = (b.min[i] - r.origin[i])*r.dir_inv[i];
        double t2 = (b.max[i] - r.origin[i])*r.dir_inv[i];
 
        tmin = max(tmin, min(t1, t2));
        tmax = min(tmax, max(t1, t2));
    }
 
    return tmax > max(tmin, 0.0);
}

這兩種算法真的等效嗎? 我們已經依靠IEEE 754浮點行爲消除了 ray.diri0ray.dir_i ≠ 0 檢查。 當 ray.diri=±0ray.dir_i = ±0時,ray.dir_invi=±ray.dir\_inv_i =±∞ 。如果射線原點的ii座標在框內,則意味着 b.mini<r.origini<b.maxib.min_i <r.origin_i <b.max_i ,我們將得到 t1=t2=±t1 = −t2 =±∞ 。 由於對於所有 nnmaxn=minn+=nmax(n,-∞)= min(n,+∞)= n,因此tmin和tmax將保持不變。

另一方面,如果 $i 座標在框外(r.origin_i <b.min_i$ 或 r.origini>b.maxir.origin_i> b.max_i),則 t1=t2=±t1 = t2 =±∞,因此 tmin=+tmax=tmin = +∞或 tmax =-∞ 。 這些值之一將貫穿算法的其餘部分,從而導致我們返回false。

不幸的是,上述分析有一個警告:如果射線正好位於平板上(r.origini=b.minir.origini=b.maxir.origin_i = b.min_i或r.origin_i = b.max_i),我們將擁有(說)
t1=(b.minir.origini)r.dirinvi=0=NaN \begin{aligned} t1 =& (b.min_i−r.origin_i)⋅r.dirinv_i \\ =& 0⋅∞ \\ =& NaN \end{aligned}
這表現得比無窮大得多。 正確處理此邊緣(字面意義!)的情況取決於min()和max()的確切行爲。

關於min()和max()

min()和max()的最常見實現可能是

#define min(x, y) ((x) < (y) ? (x) : (y))
#define max(x, y) ((x) > (y) ? (x) : (y))

這種形式是如此普遍,以至於被稱爲SSE / SSE2指令集中的最小/最大指令的行爲。 使用這些指令是從算法中獲得良好性能的關鍵。 話雖如此,這種形式具有涉及NaN的某些奇怪行爲。 由於所有與NaN的比較都是錯誤的,
min(x,NaN)=max(x,NaN)=NaNmin(NaN,x)=max(NaN,x)=x \begin{aligned} min(x,NaN) =& max(x,NaN) =& NaN \\ min(NaN,x) =& max(NaN,x) =& x \end{aligned}
這些操作既不傳播也不抑制NaN。 相反,當其中一個參數爲NaN時,總是返回第二個參數。 (關於有符號零,也有類似的奇數行爲,但這並不影響此算法。)

相反,IEEE 754指定的最小/最大操作(稱爲“ minNum”和“ maxNum”)抑制NaN,如果可能的話總是返回一個數字。 這也是C99的fmin()和fmax()函數的行爲。 另一方面,Java的Math.min()和Math.max()函數傳播NaN,與大多數其他對浮點值的二進制運算保持一致。 [3]和[4]對野外的各種最小/最大實現進行了更多討論。

存在問題

min()和max()的IEEE和Java版本提供一致的行爲:恰好位於板上的所有光線均被視爲不與盒子相交。 很容易理解爲什麼要使用Java版本,因爲NaN最終會污染所有計算,並使我們返回false。 對於IEEE版本,min(t1,t2)=max(t1,t2)=±min(t1,t2)=max(t1,t2)=±∞,與射線完全在盒子外面時相同。

(由於這是一個極端的情況,您可能想知道爲什麼我們不選擇對邊界上的光線返回true而不是false。事實證明,使用高效代碼來實現此行爲要困難得多。)

使用對SSE友好的最小/最大實現,行爲是不一致的。 平板上的某些光線會相交,即使它們完全位於盒子的另一個維度之外:
在這裏插入圖片描述
在上面的圖像中,相機位於立方體的頂面的平面中,並且使用上述算法計算了交點。 由於不正確的NaN處理,頂面超出了側面。

v2.2 解決方案

當最多一個參數是NaN時,我們可以使用
minNum(x,y)=min(x,min(y,))maxNum(x,y)=max(x,max(y,)) \begin{aligned} minNum(x,y) =& min(x,min(y,∞)) \\ maxNum(x,y) =& max(x,max(y,−∞)) \end{aligned}
Thierry Berger-Perrin在[5]中採用了類似的策略,可以有效地進行計算

tmin = max(tmin, min(min(t1, t2), INFINITY));
tmax = min(tmax, max(max(t1, t2), -INFINITY));

在循環中。 這樣做也很好。由於CPU處理浮點特殊情況(無窮大,NaN,次正規)的速度較慢,因此改成下面這樣速度要比上面快30%左右。

tmin = max(tmin, min(min(t1, t2), tmax));
tmax = min(tmax, max(max(t1, t2), tmin));

出於同樣的原因,最好像這樣展開循環,以避免處理不必要的更多無窮大:

bool intersection(box b, ray r) {
    double t1 = (b.min[0] - r.origin[0])*r.dir_inv[0];
    double t2 = (b.max[0] - r.origin[0])*r.dir_inv[0];
 
    double tmin = min(t1, t2);
    double tmax = max(t1, t2);
 
    for (int i = 1; i < 3; ++i) {
        t1 = (b.min[i] - r.origin[i])*r.dir_inv[i];
        t2 = (b.max[i] - r.origin[i])*r.dir_inv[i];
 
        tmin = max(tmin, min(min(t1, t2), tmax));
        tmax = min(tmax, max(max(t1, t2), tmin));
    }
 
    return tmax > max(tmin, 0.0);
}

很難理解爲什麼這個版本是正確的:x座標的任何NaN都會傳播到末端,而其他座標的NaN將導致tmin≥tmax。 在兩種情況下,都返回false。

使用-O3的GCC 4.9.2,該實現每秒處理超過9300萬條射線,這意味着即使沒有向量化,運行時間也大約是30個時鐘週期!

v2.3 更好的解決方案

可悲的是,這仍然比沒有顯式NaN處理的版本慢15%。 而且由於遍歷有界體積層次結構時通常使用此算法,因此最糟糕的事情是,在退化情況下遍歷過多的節點。 對於許多應用程序而言,這是非常值得的,並且如果正確實現射線/物體相交功能,則在實踐中絕不應導致任何視覺僞影。 爲了完整起見,這是一個快速實施(1.08億射線/秒),它不會嘗試一致地處理NaN:

bool intersection(box b, ray r) {
    double t1 = (b.min[0] - r.origin[0])*r.dir_inv[0];
    double t2 = (b.max[0] - r.origin[0])*r.dir_inv[0];
 
    double tmin = min(t1, t2);
    double tmax = max(t1, t2);
 
    for (int i = 1; i < 3; ++i) {
        t1 = (b.min[i] - r.origin[i])*r.dir_inv[i];
        t2 = (b.max[i] - r.origin[i])*r.dir_inv[i];
 
        tmin = max(tmin, min(t1, t2));
        tmax = min(tmax, max(t1, t2));
    }
 
    return tmax > max(tmin, 0.0);
}

在[6]中給出了我用來測試各種cross()實現的程序。 在有關該主題的下一篇文章中,我將討論低級實現的細節,包括矢量化,以從該算法中獲得最大的性能。


[1]: Brian Smits: Efficiency Issues for Ray Tracing. Journal of Graphics Tools (1998).
[2]: Amy Williams. et al.: An Efficient and Robust Ray-Box Intersection Algorithm. Journal of Graphics Tools (2005).
[3]: https://groups.google.com/forum/#!topic/llvm-dev/-SKl0nOJW_w
[4]: https://ghc.haskell.org/trac/ghc/ticket/9251
[5]: http://www.flipcode.com/archives/SSE_RayBox_Intersection_Test.shtml
[6]: https://gist.github.com/tavianator/132d081ed4d410c755fd

相關/參考鏈接

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章