[MetalKit]30-Raymarching-in-Metal射線行進

本系列文章是對 metalkit.org 上面MetalKit內容的全面翻譯和學習.html

MetalKit系統文章目錄c++


Raymarching射線步進 是一種用在實時圖形的快速渲染方法.幾何體一般不是傳遞到渲染器的,而是在着色器中用Signed Distance Fields (SDF) 函數來建立的,這個函數用來描述場景中一個點到物體的一個面之間的最短距離.當點在物體內部時SDF函數返回一個負數.SDFs很是有用,由於它讓咱們減小了Ray Tracing射線追蹤的採樣數.相似於Ray Tracing射線追蹤,在Raymarching中咱們也有從觀察平面的每一個像素髮出的射線,每條射線被用來肯定是否和某個物體相交.git

這兩種技術的不一樣在於,在射線追蹤中是用嚴格的方程組來肯定相交的,而在Raymarching中相交是估算的.用SDFs咱們能夠沿着射線步進直到咱們離某個物體過近.這種方法相比準確肯定相交來講花費的計算不算多,當場景有不少物體而且光照很複雜時,準確肯定相交代價很大.Raymarching另外一大應用場景是體積渲染(霧,水,雲),這些用Ray Tracing射線追蹤很差作由於肯定和這些的相交很是困難.github

咱們能夠用 Using MetalKit part 10中的playground來繼續下去,下面會解釋這些明顯的改動.讓咱們從兩個基本構建塊開始,這是咱們在內核用到的最小單元:一個射線和一個物體(球體).ide

struct Ray {
    float3 origin;
    float3 direction;
    Ray(float3 o, float3 d) {
        origin = o;
        direction = d;
    }
};

struct Sphere {
    float3 center;
    float radius;
    Sphere(float3 c, float r) {
        center = c;
        radius = r;
    }
};
複製代碼

由於咱們是從第10部分開始寫的,那咱們還要寫一個SDF來計算從一個給定的點到球體的距離.與原有函數不一樣之處在於,咱們如今的點是沿着射線marching步進的,因此咱們用射線位置來代替:函數

float distToSphere(Ray ray, Sphere s) {
    return length(ray.origin - s.center) - s.radius;
}
複製代碼

咱們須要作的是計算從一個給定點到一個圓(不是球體由於咱們尚未3D化)的距離,像這樣:post

float dist(float2 point, float2 center, float radius) {
    return length(point - center) - radius;
}

...
float distToCircle = dist(uv, float2(0.), 0.5);
bool inside = distToCircle < 0.;
output.write(inside ? float4(1.) : float4(0.), gid);
...
複製代碼

咱們如今須要有一個射線,並沿着它步進穿過場景,因此用下面幾行替換內核中的最後三行:學習

Sphere s = Sphere(float3(0.), 1.);
Ray ray = Ray(float3(0., 0., -3.), normalize(float3(uv, 1.0)));
float3 col = float3(0.);
for (int i=0.; i<100.; i++) {
    float dist = distToSphere(ray, s);
    if (dist < 0.001) {
        col = float3(1.);
        break;
    }
    ray.origin += ray.direction * dist;
}
output.write(float4(col, 1.), gid);
複製代碼

讓咱們一行一行來看這些代碼.咱們首先建立了一個球體和一個射線.注意射線的z值接近於0時,球體看起來更大由於射線離場景更近,相反,當它遠離0,球體看上去更小了,緣由很明顯-咱們用射線做爲了隱性攝像機.下面咱們定義顏色來初始化一個純黑色.如今raymarching最精華的地方來了!咱們循環必定次數(步數)來確保咱們行進足夠細膩.咱們在這裏用100,但你能夠嘗試一個更大數值的步數,來觀察渲染圖像的質量的改善,固然也會消耗更多的計算資源.在循環裏,咱們計算當前位置沿射線到場景的距離,同時也檢查咱們是否接觸到了場景中的物體,若是接觸到了就將其着色爲白色並跳出循環,不然就更新射線位置向場景前進一些.動畫

注意咱們規範化了射線方向來覆蓋邊緣狀況,例如向量(1,1,1)(屏幕邊角)的長度會是sqrt(1 * 1 + 1 * 1 + 1 * 1)即大約1.732.這意味着咱們須要向前移動射線位置大約1.73*dist,也就是大約咱們須要前進距離的兩倍,這可能會讓咱們由於超過射線交點而錯過/穿過物體.爲此,咱們規範化了方向,來確保它的長度始終是1.最後,咱們將顏色寫入到輸出紋理中.若是你如今運行playground,你應該會看到相似的圖像:ui

raymarching1.png

如今咱們建立一個函數命名爲distToScene,它接收一個射線做爲參數,由於咱們如今捲尺的是找到包含多個物體的複雜場景中的最短距離.下一步,咱們移動球體相關的代碼到新函數內,只返回到球體的距離(暫時).而後,咱們改變球體位置到(1,1,1),半徑0.5,這意味着球體如今在0.5 ... 1.5範圍內.這裏有個巧妙的花招來作例子:若是咱們在0.0 ... 2.0內重複空間,則球體老是處於內部.下一步,咱們作個射線的本地副本,並對原始值取模.而後咱們用重複的射線代入distToSphere()函數.

float distToScene(Ray r) {
    Sphere s = Sphere(float3(1.), 0.5);
    Ray repeatRay = r;
    repeatRay.origin = fmod(r.origin, 2.0);
    return distToSphere(repeatRay, s);
}
複製代碼

經過使用fmod函數,咱們重複空間填滿整個屏幕,實際上建立了一個無限數量的球體,每個都帶着本身的(重複的)射線.固然,咱們將只看被屏幕的xy座標以內的那些,然而,z座標將讓咱們看到球體是如何進到無限深度的.在內核中,移除球體代碼,將射線移到很遠的位置,修改dist來給咱們留出到場景的距離,最後修改最後一行來顯示更好看的顏色:

Ray ray = Ray(float3(1000.), normalize(float3(uv, 1.0)));
...
float dist = distToScene(ray);
...
output.write(float4(col * abs((ray.origin - 1000.) / 10.0), 1.), gid);
複製代碼

咱們將顏色與射線位置相乘.除以10.0由於場景至關大,射線位置在大部分地方會大於1.0,這會讓咱們看到純白色.咱們用abs()由於屏幕左邊的x小於0,它會讓咱們看到純黑色,因此咱們只需鏡像上/下和左/右的顏色.最後,咱們偏移射線位置100,以匹配射線起點(攝像機).若是你如今運行playground,你應該會看到相似的圖像:

raymarching2.png

下一步,咱們讓場景動起來!咱們在part 12中已經看到如何發送uniforms變量好比timeGPU,因此咱們就再也不重複了.

float3 camPos = float3(1000. + sin(time) + 1., 1000. + cos(time) + 1., time);
Ray ray = Ray(camPos, normalize(float3(uv, 1.)));
...
float3 posRelativeToCamera = ray.origin - camPos;
output.write(float4(col * abs((posRelativeToCamera) / 10.0), 1.), gid);
複製代碼

咱們添加time到全部三個座標,但咱們只讓xy起伏變化而z保持直線.1.部分只是爲了阻止攝像機撞到最近的球體上.要看這份代碼的動畫效果,我在下面使用一個Shadertoy嵌入式播放器.只要把鼠標懸浮在上面,並單擊播放按鈕就能看到動畫:<譯者注:這裏不支持嵌入播放器,我用gif代替https://www.shadertoy.com/embed/XtcSDf>

raymarching.mov.gif

感謝 Chris的協助. 源代碼source code已發佈在Github上.

下次見!

相關文章
相關標籤/搜索