1. 程式人生 > >光線跟蹤(RayTracing)原理及c++實現

光線跟蹤(RayTracing)原理及c++實現

提到Computer Graphics,眾所周知的是如OpenGL、Direct3D這樣非常流行的光柵化渲染器。事實上,這些大部分應用於遊戲製作的API主要為實時渲染(Real-time Rendering)而設定,而它們所採用的光柵化(Rasterization)的渲染方式,通過渲染大量的三角形(或者其他的幾何圖元種類(Primitive types)),是與本文介紹的光線跟蹤相對的一種渲染方式。這種基於光柵化的渲染系統,往往只支援區域性照明(Local Illumination)。區域性照明在渲染幾何圖形的一個畫素時,光照計算只能取得該畫素的資訊,而不能訪問其他幾何圖形的資訊。


圖1.jpg

該圖片出自《孤島驚魂》,儘管看似水面顯示出了遠處山峰的倒影,卻不能渲染植被、船骸等細節。

理論上,陰影(Shadow)、反射(Reflection)、折射(Refraction)均為全域性照明(Global Illumination)效果,所以在實際應用中,柵格化渲染系統可以使用預處理(如陰影貼圖(shadow mapping)、環境貼圖(environment mapping))去模擬這些效果。

柵格化的最大優勢是計算量比較小,適合實時渲染。相反,全域性光照計算量大,一般也沒有特殊硬體加速(通常只使用CPU而非GPU),所以只適合離線渲染(offline rendering),例如3D Studio Max、Maya等工具。其中一個支援全域性光照的方法,稱為光線追蹤(ray tracing)。光線追蹤能簡單直接地支援陰影、反射、折射,實現起來亦非常容易。

Chapt2. Principles of RayTracing

由光源發出的光到達物體表面後,產生反射和折射,簡單光照明模型和光透射模型模擬了這兩種現象。在簡單光照明模型中,反射被分為理想漫反射和鏡面反射光,把透射光模型分為理想漫透射光和規則透射光。由廣元發出的光成為直接光,物體對直接光的反射或折射成為直接反射和直接折射,相對的,把物體表面間對廣德反射和折射成為間接光、間接反射、間接折射。光線在物體之間的傳播方式是光線跟蹤演算法的基礎。

最基本的光線跟蹤演算法是跟蹤鏡面反射和折射。從光源發出的光遇到物體的表面,發生反射和折射,光就改變方向,沿著反射方向和折射方向繼續前進,知道遇到新的物體。但是光源發出光線,經過反射與折射,只有很少部分可以進入人的眼睛。因此實際光線跟蹤演算法的跟蹤方向與光傳播的方向是相反的(反向光線跟蹤),稱之為視線跟蹤。由視點與畫素(x,y)

發出一根射線,與第一個物體相交後,在其反射與折射方向上進行跟蹤,如圖2所示


圖2.gif

在光線跟蹤演算法中,我們有如下的四種光線:

  • 視線是由視點與畫素(x,y)發出的射線;
  • 陰影測試線是物體表面上點與光源的連線;
  • 反射光線;
  • 折射光線

當光線 V與物體表面交於點P時,點P分為三部分,把這三部分光強相加,就是該條光線V在P點處的總的光強:

  • a) 由光源產生的直接的光線照射光強,是交點處的區域性光強,可以由下式計算:

    式1.gif
  • b) 反射方向上由其它物體引起的間接光照光強,由IsKs'計算,Is通過對反射光線的遞迴跟蹤得到
  • c) 折射方向上由其它物體引起的間接光照光強,由ItKt'計算,It通過對摺射光線的遞迴跟蹤得到

現在我們來討論光線跟蹤演算法本身。我們將對一個由兩個透明球和一個非透明物體組成的場景進行光線跟蹤(圖3)通過這個例子,可以把光線跟蹤的基本過程解釋清楚。


圖3.gif

在我們的場景中,有一個點光源L,兩個透明的球體O1與O2,一個不透明的物體O3。首先,從視點出發經過視屏一個畫素點的視線E傳播到達球體O1,與其交點為P1。從P1向光源L作一條陰影測試線S1,我們發現其間沒有遮擋的物體,那麼我們就用區域性光照明模型計算光源對P1在其視線E的方向上的光強,作為該點的區域性光強。同時我們還要跟蹤該點處反射光線R1和折射光線T1,它們也對P1點的光強有貢獻。在反射光線R1方向上,沒有再與其他物體相交,那麼就設該方向的光強為零,並結束這光線方向的跟蹤。然後我們來對摺射光線T1方向進行跟蹤,來計算該光線的光強貢獻。折射光線T1在物體O1內部傳播,與O1相交於點P2,由於該點在物體內部,我們假設它的區域性光強為零,同時,產生了反射光線R2和折射光線T2,在反射光線R2方向,我們可以繼續遞迴跟蹤下去計算它的光強,在這裡就不再繼續下去了。我們將繼續對摺射光線T2進行跟蹤。T2與物體O3交於點P3,作P3與光源L的陰影測試線S3,沒有物體遮擋,那麼計算該處的區域性光強,由於該物體是非透明的,那麼我們可以繼續跟蹤反射光線R3方向的光強,結合區域性光強,來得到P3處的光強。反射光線R3的跟蹤與前面的過程類似,演算法可以遞迴的進行下去。重複上面的過程,直到光線滿足跟蹤終止條件。這樣我們就可以得到視屏上的一個象素點的光強,也就是它相應的顏色值。

上面的例子就是光線跟蹤演算法的基本過程,我們可以看出,光線跟蹤演算法實際上是光照明物理過程的近似逆過程,這一過程可以跟蹤物體間的鏡面反射光線和規則透射,模擬了理想表面的光的傳播。

雖然在理想情況下,光線可以在物體之間進行無限的反射和折射,但是在實際的演算法進行過程中,我們不可能進行無窮的光線跟蹤,因而需要給出一些跟蹤的終止條件。在演算法應用的意義上,可以有以下的幾種終止條件:

  • 該光線未碰到任何物體。
  • 該光線碰到了背景
  • 光線在經過許多次反射和折射以後,就會產生衰減,光線對於視點的光強貢獻很小(小於某個設定值)
  • 光線反射或折射次數即跟蹤深度大於一定值

Chapt3. Rasterization & RayTracing

瞭解了光線跟蹤的原理之後,再來看一下在計算機上的實現。

光柵化渲染,簡單地說,就是把大量三角形畫到螢幕上。當中會採用深度緩衝(depth buffer, z-buffer),來解決多個三角形重疊時的前後問題。三角形數目影響效能,但三角形在螢幕上的總面積才是主要瓶頸。

光線追蹤,簡單地說,就是從攝影機的位置,通過影像平面上的畫素位置(比較正確的說法是取樣(sampling)位置),發射一束光線到場景,求光線和幾何圖形間最近的交點,再求該交點的著色。如果該交點的材質是反射性的,可以在該交點向反射方向繼續追蹤。光線追蹤除了容易支援一些全域性光照效果外,亦不侷限於三角形作為幾何圖形的單位。任何幾何圖形,能與一束光線計算交點(intersection point),就能支援。


圖4.png

上圖顯示了光線追蹤的基本方式。要計算一點是否在陰影之內,也只須發射一束光線到光源,檢測中間是否存在障礙物。

Chapt4. Source Code

1. Base Class

本例程式碼嘗試使用基於物件(object-based)的方式編寫

3D Vector
struct Vector {
    float x, y, z;
    Vector(float x_, float y_, float z_) : x(x_), y(y_), z(z_) {}
    Vector(const Vector& r) : x(r.x), y(r.y), z(r.z) {}
    float sqrLength() const {
        return x  x + y  y + z  z;
    }
    float length() const {
        return sqrt(sqrLength());
    }
    Vector operator+(const Vector& r) const {
        return Vector(x + r.x, y + r.y, z + r.z);
    }
    Vector operator-(const Vector& r) const {
        return Vector(x - r.x, y - r.y, z - r.z);
    }
    Vector operator(float v) const {
        return Vector(v  x, v  y, v  z);
    }
    Vector operator/(float v) const {
        float inv = 1 / v;
        return this  inv;
    }
    Vector normalize() const {
        float invlen = 1 / length();
        return this  invlen;
    }
    float dot(const Vector& r) const {
        return x  r.x + y  r.y + z  r.z;
    }
    Vector cross(const Vector& r) const {
        return Vector(-z  r.y + y  r.z,
                      z  r.x - x  r.z,
                      -y  r.x + x  r.y);
    }
    static Vector zero() {
        return Vector(0, 0, 0);
    }
};
inline Vector operator(float l, const Vector& r) {return r  l;}

這些類方法(如normalize、dot、cross等),如果傳回Vector物件,都會傳回一個新建構的Vector。這些三維向量的功能很簡單,不在此詳述。

Vector zero()用作常量,避免每次重新構建。值得一提,這些常量必需在prototype設定之後才能定義。

Ray

即為光線類,所謂光線(ray),從一點向某方向發射也。數學上可用引數函式(parametric function)表示:


式2.png

當中,o即發謝起點(origin),d為方向。在本文的例子裡,都假設d為單位向量(unit vector),因此t為距離。實現如下

struct Ray {
    Vector origin, direction;
    Ray(const Vector& o, const Vector& d) : origin(o), direction(d) {}
    Vector getPoint(float t) const {
        return origin + t * direction;
    }
};
Sphere

球體(sphere)是其中一個最簡單的立體幾何圖形。這裡只考慮球體的表面(surface),中心點為c、半徑為r的球體表面可用等式(equation)表示:


式3.png

如前文所述,需要計算光線和球體的最近交點。只要把光線x = r(t)代入球體等式,把該等式求解就是交點。為簡化方程,設v=o - c,則:


式4.png

因為d為單位向量,所以二次方的係數可以消去。 t的二次方程式的解為


式5.png
struct Sphere : public Geometry {
    Vector center;
    float radius, sqrRadius;
    Sphere(const Vector& c, float r, Material m = NULL) :
            Geometry(m), center(c), radius(r), sqrRadius(r  r) {}
    IntersectResult intersect(const Ray& ray) const {
        Vector v = ray.origin - center;
        float a0 = v.sqrLength() - sqrRadius;
        float DdotV = ray.direction.dot(v);
        if (DdotV <= 0.0) {
            float discr = DdotV * DdotV - a0;
            if (discr >= 0.0) {
                float d = -DdotV - sqrt(discr);
                Vector p = ray.getPoint(d);
                Vector n = (p - center).normalize();
                return IntersectResult(this, d, p, n);
            }
        }
        return IntersectResult();
    }
};