本系列文章是對(duì) http://metalkit.org 上面MetalKit內(nèi)容的全面翻譯和學(xué)習(xí).
Raymarching射線步進(jìn) 是一種用在實(shí)時(shí)圖形的快速渲染方法.幾何體通常不是傳遞到渲染器的,而是在著色器中用Signed Distance Fields (SDF)函數(shù)來創(chuàng)建的,這個(gè)函數(shù)用來描述場(chǎng)景中一個(gè)點(diǎn)到物體的一個(gè)面之間的最短距離.當(dāng)點(diǎn)在物體內(nèi)部時(shí)SDF
函數(shù)返回一個(gè)負(fù)數(shù).SDFs
非常有用,因?yàn)樗屛覀儨p少了Ray Tracing射線追蹤
的采樣數(shù).類似于Ray Tracing射線追蹤
,在Raymarching
中我們也有從觀察平面的每個(gè)像素發(fā)出的射線,每條射線被用來確定是否和某個(gè)物體相交.
這兩種技術(shù)的不同在于,在射線追蹤中是用嚴(yán)格的方程組來確定相交的,而在Raymarching
中相交是估算的.用SDFs
我們可以沿著射線步進(jìn)
直到我們離某個(gè)物體過近.這種方法相比準(zhǔn)確確定相交來說花費(fèi)的計(jì)算不算多,當(dāng)場(chǎng)景有很多物體并且光照很復(fù)雜時(shí),準(zhǔn)確確定相交代價(jià)很大.Raymarching
另一大應(yīng)用場(chǎng)景是體積渲染(霧,水,云),這些用Ray Tracing射線追蹤
不好做因?yàn)榇_定和這些的相交非常困難.
我們可以用 Using MetalKit part 10
中的playground來繼續(xù)下去,下面會(huì)解釋這些明顯的改動(dòng).讓我們從兩個(gè)基本構(gòu)建塊開始,這是我們?cè)趦?nèi)核用到的最小單元:一個(gè)射線和一個(gè)物體(球體).
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;
}
};
因?yàn)槲覀兪菑?code>第10部分開始寫的,那我們還要寫一個(gè)SDF
來計(jì)算從一個(gè)給定的點(diǎn)到球體的距離.與原有函數(shù)不同之處在于,我們現(xiàn)在的點(diǎn)是沿著射線marching步進(jìn)
的,所以我們用射線位置來代替:
float distToSphere(Ray ray, Sphere s) {
return length(ray.origin - s.center) - s.radius;
}
我們需要做的是計(jì)算從一個(gè)給定點(diǎn)到一個(gè)圓(不是球體因?yàn)槲覀冞€沒有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)在需要有一個(gè)射線,并沿著它步進(jìn)穿過場(chǎng)景,所以用下面幾行替換內(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)建了一個(gè)球體和一個(gè)射線.注意射線的z
值接近于0
時(shí),球體看起來更大因?yàn)樯渚€離場(chǎng)景更近,相反,當(dāng)它遠(yuǎn)離0
,球體看上去更小了,原因很明顯-我們用射線作為了隱性攝像機(jī)
.下面我們定義顏色來初始化一個(gè)純黑色.現(xiàn)在raymarching
最精華的地方來了!我們循環(huán)一定次數(shù)(步數(shù))來確保我們行進(jìn)足夠細(xì)膩.我們?cè)谶@里用100
,但你可以嘗試一個(gè)更大數(shù)值的步數(shù),來觀察渲染圖像的質(zhì)量的改善,當(dāng)然也會(huì)消耗更多的計(jì)算資源.在循環(huán)里,我們計(jì)算當(dāng)前位置沿射線到場(chǎng)景的距離,同時(shí)也檢查我們是否接觸到了場(chǎng)景中的物體,如果接觸到了就將其著色為白色并跳出循環(huán),否則就更新射線位置向場(chǎng)景前進(jìn)一些.
注意我們規(guī)范化了射線方向來覆蓋邊緣情況,例如向量(1,1,1)
(屏幕邊角)的長(zhǎng)度會(huì)是sqrt(1 * 1 + 1 * 1 + 1 * 1)
即大約1.732
.這意味著我們需要向前移動(dòng)射線位置大約1.73*dist
,也就是大約我們需要前進(jìn)距離的兩倍,這可能會(huì)讓我們因?yàn)槌^射線交點(diǎn)而錯(cuò)過/穿過物體.為此,我們規(guī)范化了方向,來確保它的長(zhǎng)度始終是1
.最后,我們將顏色寫入到輸出紋理中.如果你現(xiàn)在運(yùn)行playground,你應(yīng)該會(huì)看到類似的圖像:
現(xiàn)在我們創(chuàng)建一個(gè)函數(shù)命名為distToScene,它接收一個(gè)射線作為參數(shù),因?yàn)槲覀儸F(xiàn)在卷尺的是找到包含多個(gè)物體的復(fù)雜場(chǎng)景中的最短距離.下一步,我們移動(dòng)球體相關(guān)的代碼到新函數(shù)內(nèi),只返回到球體的距離(暫時(shí)).然后,我們改變球體位置到(1,1,1)
,半徑0.5
,這意味著球體現(xiàn)在在0.5 ... 1.5
范圍內(nèi).這里有個(gè)巧妙的花招來做例子:如果我們?cè)?code>0.0 ... 2.0內(nèi)重復(fù)空間,則球體總是處于內(nèi)部.下一步,我們做個(gè)射線的本地副本,并對(duì)原始值取模.然后我們用重復(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ù)空間填滿整個(gè)屏幕,實(shí)際上創(chuàng)建了一個(gè)無限數(shù)量的球體,每一個(gè)都帶著自己的(重復(fù)的)射線.當(dāng)然,我們將只看被屏幕的x
和y
坐標(biāo)之內(nèi)的那些,然而,z
坐標(biāo)將讓我們看到球體是如何進(jìn)到無限深度的.在內(nèi)核中,移除球體代碼,將射線移到很遠(yuǎn)的位置,修改dist
來給我們留出到場(chǎng)景的距離,最后修改最后一行來顯示更好看的顏色:
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
因?yàn)閳?chǎng)景相當(dāng)大,射線位置在大部分地方會(huì)大于1.0
,這會(huì)讓我們看到純白色.我們用abs()
因?yàn)槠聊蛔筮叺?code>x小于0
,它會(huì)讓我們看到純黑色,所以我們只需鏡像上/下和左/右的顏色.最后,我們偏移射線位置100
,以匹配射線起點(diǎn)(攝像機(jī)).如果你現(xiàn)在運(yùn)行playground,你應(yīng)該會(huì)看到類似的圖像:
下一步,我們讓場(chǎng)景動(dòng)起來!我們?cè)?a target="_blank" rel="nofollow">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
到所有三個(gè)坐標(biāo),但我們只讓x
和y
起伏變化而z
保持直線.1.
部分只是為了阻止攝像機(jī)撞到最近的球體上.要看這份代碼的動(dòng)畫效果,我在下面使用一個(gè)Shadertoy
嵌入式播放器.只要把鼠標(biāo)懸浮在上面,并單擊播放按鈕就能看到動(dòng)畫:<譯者注:簡(jiǎn)書不支持嵌入播放器,我用gif代替https://www.shadertoy.com/embed/XtcSDf>
感謝 Chris的協(xié)助.
源代碼source code已發(fā)布在Github上.
下次見!