本系列文章是對 metalkit.org 上面MetalKit內容的全面翻譯和學習.
今天我們將學習ambient occlusion環境光遮蔽.我們將使用Shadows in Metal part 2
的playground程式碼.首先,讓我們新增一個新的物件型別-矩形盒子:
struct Box {
float3 center;
float size;
Box(float3 c, float s) {
center = c;
size = s;
}
};
複製程式碼
下一步,讓我為新的結構體再新增一個新的距離函式:
float distToBox(Ray r, Box b) {
float3 d = abs(r.origin - b.center) - float3(b.size);
return min(max(d.x, max(d.y, d.z)), 0.0) + length(max(d, 0.0));
}
複製程式碼
然後,更新我們的場景:
float distToScene(Ray r) {
Plane p = Plane(0.0);
float d2p = distToPlane(r, p);
Sphere s1 = Sphere(float3(0.0, 0.5, 0.0), 8.0);
Sphere s2 = Sphere(float3(0.0, 0.5, 0.0), 6.0);
Sphere s3 = Sphere(float3(10., -5., -10.), 15.0);
Box b = Box(float3(1., 1., -4.), 1.);
float dtb = distToBox(r, b);
float d2s1 = distToSphere(r, s1);
float d2s2 = distToSphere(r, s2);
float d2s3 = distToSphere(r, s3);
float dist = differenceOp(d2s1, d2s2);
dist = differenceOp(dist, d2s3);
dist = unionOp(dist, dtb);
dist = unionOp(d2p, dist);
return dist;
}
複製程式碼
我們剛才做的是首先繪製一個半徑為8
的球體,一個半徑為6
的球體,並求出它們的差集.因為它們中心相同,所以小的那個看不到,除非我們做個橫截面.這就是為什麼我們用到了第三個球體,大很多而且中心也不同.我們再取一次差集,就能看到第一個差集的結果.最後,我們新增一個盒子,來讓它更好看更多樣.如果你現在執行playground你將看到類似的影像:
下一步,讓我們刪除lighting() 和shadow() 函式,因為我們不再需要他們了.還有,刪除Light結構體和核心中的兩個例項.現在讓我們建立一個ambient occlusion環境光遮蔽
的替代函式:
float ao(float3 pos, float3 n) {
return n.y * 0.5 + 0.5;
}
複製程式碼
我們在燈光中只用到了法線的y
分量,就像有一個正上方的燈光一樣.在核心中,建立法線之後(在else
括號中),呼叫ao()
函式:
float o = ao(ray.origin, n);
col = col * o;
複製程式碼
只有一個基本(正上方)燈光時,沒有陰影了.如果你現在執行playground你將看到類似的影像:
是時候來點真正的ambient occlusion環境光遮蔽了. Ambient環境光意味著燈光不是來自一個定義好的光源,而是意味著一般的背景光照. * Occlusion遮蔽*意思是多少環境光被阻擋了.我們在曲面上取一個射線碰撞的點,觀察它的周圍.如果周圍有一個物體,那顏色值阻擋場景中的大部分光源,所以這是一個暗區.如果周圍沒有東西,那就是亮區.對於處於中間狀態的情況,我們需要精確計算出多少光被阻塞了.介紹一下cone tracing圓錐追蹤概念.
cone tracing圓錐追蹤
的想法就是在場景中使用一個圓錐體代替射線.如果圓錐與物體相交,我們不僅僅能得到一個簡單的true/false
的結果.我們可以得到物體在該點處覆蓋了多少圓錐體.但是我們如何追蹤一個圓錐呢?我們可以使用許多球體來做一個圓錐.試著想一下許多球體排成一行,一頭小一頭大.這就是我們目前能近似得到的圓錐體.下面是我們需要步驟:
- 從曲面上的一個點開始
- 沿法線方向走出曲面
- 每次迭代,用距離函式確定球體的多少被場景填充了
- 每次迭代,離曲面的距離翻倍,同時球體尺寸翻倍
因為我們每步都把球體尺寸翻倍,這就意味著我們只需要幾步迭代就可以很快從曲面表面出來.這也給了我們一個很棒的寬的圓錐.下面是完整的ao()
函式:
float ao(float3 pos, float3 n) {
float eps = 0.01;
pos += n * eps * 2.0;
float occlusion = 0.0;
for (float i=1.0; i<10.0; i++) {
float d = distToScene(Ray(pos, float3(0)));
float coneWidth = 2.0 * eps;
float occlusionAmount = max(coneWidth - d, 0.);
float occlusionFactor = occlusionAmount / coneWidth;
occlusionFactor *= 1.0 - (i / 10.0);
occlusion = max(occlusion, occlusionFactor);
eps *= 2.0;
pos += n * eps;
}
return max(0.0, 1.0 - occlusion);
}
複製程式碼
讓我們一行一行看看這些程式碼.首先,我們定義了eps變數,它包含了圓錐半徑和距離曲面的距離.然後,我們移出去一點來避免我們碰撞到我們離開的表面.下一步,我們定義occlusion遮蔽變數,初始化為nil(場景是完全被照亮的).然後,我們進入迴圈,每次迭代我們拿到場景距離,將半徑加倍以便知道圓錐的多少被遮蔽了,確保排隊了燈光的負值,拿到遮蔽數量(比率)乘以圓錐寬度,給遠處的遮蔽(可以從迭代次數獲取遠近)設定一個低的影響因子,儲存當前最高的遮蔽值,將eps加倍並沿法線移動同樣距離.然後返回一個值,它代表有多少光線到達了這個點.
現在讓我們建立個camera結構體.它需要一個位置.我們只需儲存一個射線來代替攝像機方向.最後rayDivergence給我們一個因子,代表射線擴散了多少.
struct Camera {
float3 position;
Ray ray = Ray(float3(0), float3(0));
float rayDivergence;
Camera(float3 pos, Ray r, float div) {
position = pos;
ray = r;
rayDivergence = div;
}
};
複製程式碼
下一步,設定攝像機.需要一個攝像機位置,觀察目標/朝向,視場和檢視座標:
Camera setupCam(float3 pos, float3 target, float fov, float2 uv, int x) {
uv *= fov;
float3 cw = normalize(target - pos );
float3 cp = float3(0.0, 1.0, 0.0);
float3 cu = normalize(cross(cw, cp));
float3 cv = normalize(cross(cu, cw));
Ray ray = Ray(pos, normalize(uv.x * cu + uv.y * cv + 0.5 * cw));
Camera cam = Camera(pos, ray, fov / float(x));
return cam;
}
複製程式碼
現在我們只需要初始化攝像機.我們讓它環繞場景,朝向中心**(0,0,0)**.新增到核心,放在uv
變數建立後:
float3 camPos = float3(sin(time) * 10., 3., cos(time) * 10.);
Camera cam = setupCam(camPos, float3(0), 1.25, uv, width);
複製程式碼
然後刪除ray變數,用cam.ray替換核心中用到它的地方.如果你現在執行playground你將看到類似的影像:
要看這份程式碼的動畫效果,我在下面使用一個Shadertoy
嵌入式播放器.只要把滑鼠懸浮在上面,並單擊播放按鈕就能看到動畫:<譯者注:這裡不支援嵌入播放器,我用gif代替https://www.shadertoy.com/embed/4ltSWf>
原始碼source code已釋出在Github上.
下次見!