本系列文章是對 http://metalkit.org 上面MetalKit內(nèi)容的全面翻譯和學(xué)習(xí).
Raymarching射線步進(jìn) 是一種用在實時圖形的快速渲染方法.幾何體通常不是傳遞到渲染器的,而是在著色器中用Signed Distance Fields (SDF)函數(shù)來創(chuàng)建的,這個函數(shù)用來描述場景中一個點到物體的一個面之間的最短距離.當(dāng)點在物體內(nèi)部時SDF函數(shù)返回一個負(fù)數(shù).SDFs非常有用,因為它讓我們減少了Ray Tracing射線追蹤的采樣數(shù).類似于Ray Tracing射線追蹤,在Raymarching中我們也有從觀察平面的每個像素發(fā)出的射線,每條射線被用來確定是否和某個物體相交.
這兩種技術(shù)的不同在于,在射線追蹤中是用嚴(yán)格的方程組來確定相交的,而在Raymarching中相交是估算的.用SDFs我們可以沿著射線步進(jìn)直到我們離某個物體過近.這種方法相比準(zhǔn)確確定相交來說花費的計算不算多,當(dāng)場景有很多物體并且光照很復(fù)雜時,準(zhǔn)確確定相交代價很大.Raymarching另一大應(yīng)用場景是體積渲染(霧,水,云),這些用Ray Tracing射線追蹤不好做因為確定和這些的相交非常困難.
我們可以用 Using MetalKit part 10中的playground來繼續(xù)下去,下面會解釋這些明顯的改動.讓我們從兩個基本構(gòu)建塊開始,這是我們在內(nèi)核用到的最小單元:一個射線和一個物體(球體).
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來計算從一個給定的點到球體的距離.與原有函數(shù)不同之處在于,我們現(xiàn)在的點是沿著射線marching步進(jìn)的,所以我們用射線位置來代替:
float distToSphere(Ray ray, Sphere s) {
return length(ray.origin - s.center) - s.radius;
}
我們需要做的是計算從一個給定點到一個圓(不是球體因為我們還沒有3D化)的距離,像這樣:
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);
...
我們現(xiàn)在需要有一個射線,并沿著它步進(jìn)穿過場景,所以用下面幾行替換內(nèi)核中的最后三行:
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);
讓我們一行一行來看這些代碼.我們首先創(chuàng)建了一個球體和一個射線.注意射線的z值接近于0時,球體看起來更大因為射線離場景更近,相反,當(dāng)它遠(yuǎn)離0,球體看上去更小了,原因很明顯-我們用射線作為了隱性攝像機(jī).下面我們定義顏色來初始化一個純黑色.現(xiàn)在raymarching最精華的地方來了!我們循環(huán)一定次數(shù)(步數(shù))來確保我們行進(jìn)足夠細(xì)膩.我們在這里用100,但你可以嘗試一個更大數(shù)值的步數(shù),來觀察渲染圖像的質(zhì)量的改善,當(dāng)然也會消耗更多的計算資源.在循環(huán)里,我們計算當(dāng)前位置沿射線到場景的距離,同時也檢查我們是否接觸到了場景中的物體,如果接觸到了就將其著色為白色并跳出循環(huán),否則就更新射線位置向場景前進(jìn)一些.
注意我們規(guī)范化了射線方向來覆蓋邊緣情況,例如向量(1,1,1)(屏幕邊角)的長度會是sqrt(1 * 1 + 1 * 1 + 1 * 1)即大約1.732.這意味著我們需要向前移動射線位置大約1.73*dist,也就是大約我們需要前進(jìn)距離的兩倍,這可能會讓我們因為超過射線交點而錯過/穿過物體.為此,我們規(guī)范化了方向,來確保它的長度始終是1.最后,我們將顏色寫入到輸出紋理中.如果你現(xiàn)在運行playground,你應(yīng)該會看到類似的圖像:

現(xiàn)在我們創(chuàng)建一個函數(shù)命名為distToScene,它接收一個射線作為參數(shù),因為我們現(xiàn)在卷尺的是找到包含多個物體的復(fù)雜場景中的最短距離.下一步,我們移動球體相關(guān)的代碼到新函數(shù)內(nèi),只返回到球體的距離(暫時).然后,我們改變球體位置到(1,1,1),半徑0.5,這意味著球體現(xiàn)在在0.5 ... 1.5范圍內(nèi).這里有個巧妙的花招來做例子:如果我們在0.0 ... 2.0內(nèi)重復(fù)空間,則球體總是處于內(nèi)部.下一步,我們做個射線的本地副本,并對原始值取模.然后我們用重復(fù)的射線代入distToSphere()函數(shù).
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函數(shù),我們重復(fù)空間填滿整個屏幕,實際上創(chuàng)建了一個無限數(shù)量的球體,每一個都帶著自己的(重復(fù)的)射線.當(dāng)然,我們將只看被屏幕的x和y坐標(biāo)之內(nèi)的那些,然而,z坐標(biāo)將讓我們看到球體是如何進(jìn)到無限深度的.在內(nèi)核中,移除球體代碼,將射線移到很遠(yuǎn)的位置,修改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因為場景相當(dāng)大,射線位置在大部分地方會大于1.0,這會讓我們看到純白色.我們用abs()因為屏幕左邊的x小于0,它會讓我們看到純黑色,所以我們只需鏡像上/下和左/右的顏色.最后,我們偏移射線位置100,以匹配射線起點(攝像機(jī)).如果你現(xiàn)在運行playground,你應(yīng)該會看到類似的圖像:

下一步,我們讓場景動起來!我們在part 12中已經(jīng)看到如何發(fā)送uniforms變量比如time到GPU,所以我們就不再重復(fù)了.
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到所有三個坐標(biāo),但我們只讓x和y起伏變化而z保持直線.1.部分只是為了阻止攝像機(jī)撞到最近的球體上.要看這份代碼的動畫效果,我在下面使用一個Shadertoy嵌入式播放器.只要把鼠標(biāo)懸浮在上面,并單擊播放按鈕就能看到動畫:<譯者注:簡書不支持嵌入播放器,我用gif代替https://www.shadertoy.com/embed/XtcSDf>

感謝 Chris的協(xié)助.
源代碼source code已發(fā)布在Github上.
下次見!