本系列文章是對 metalkit.org 上面MetalKit內容的全面翻譯和學習.html
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
如今咱們建立一個函數命名爲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
函數,咱們重複空間填滿整個屏幕,實際上建立了一個無限數量的球體,每個都帶着本身的(重複的)射線.固然,咱們將只看被屏幕的x
和y
座標以內的那些,然而,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,你應該會看到相似的圖像:
下一步,咱們讓場景動起來!咱們在part 12中已經看到如何發送uniforms變量好比time
到GPU
,因此咱們就再也不重複了.
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
到全部三個座標,但咱們只讓x
和y
起伏變化而z
保持直線.1.
部分只是爲了阻止攝像機撞到最近的球體上.要看這份代碼的動畫效果,我在下面使用一個Shadertoy
嵌入式播放器.只要把鼠標懸浮在上面,並單擊播放按鈕就能看到動畫:<譯者注:這裏不支持嵌入播放器,我用gif代替https://www.shadertoy.com/embed/XtcSDf>
感謝 Chris的協助. 源代碼source code已發佈在Github上.
下次見!