[MetalKit]32-Shadows-in-Metal-part-2陰影2

蘋果API搬運工發表於2017-12-14

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

MetalKit系統文章目錄


在本系列的第二部分中,我們將學習soft shadows軟陰影.我們將使用在Raymarching in Metal中的playground,因為它已經建立了3D物體.讓我們建立一個基本場景,包含一個球體,一個平面,一個燈光和一個射線:

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;
    }
};

struct Plane {
    float yCoord;
    Plane(float y) {
        yCoord = y;
    }
};

struct Light {
    float3 position;
    Light(float3 pos) {
        position = pos;
    }
};
複製程式碼

下一步,我們建立幾個distance operation距離運算函式來幫助我們確定元素到場景之間的距離:

float unionOp(float d0, float d1) {
    return min(d0, d1);
}

float differenceOp(float d0, float d1) {//差集
    return max(d0, -d1);
}

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

float distToPlane(Ray ray, Plane plane) {
    return ray.origin.y - plane.yCoord;
}
複製程式碼

下一步,我們建立一個distanceToScene() 函式,它給出場景中到任意物體的最近距離.我們用這些函式來生成一個形狀,它看起來像是一個帶有幾個洞的空心球體:

float distToScene(Ray r) {
    Plane p = Plane(0.0);
    float d2p = distToPlane(r, p);
    Sphere s1 = Sphere(float3(2.0), 1.9);
    Sphere s2 = Sphere(float3(0.0, 4.0, 0.0), 4.0);
    Sphere s3 = Sphere(float3(0.0, 4.0, 0.0), 3.9);
    Ray repeatRay = r;
    repeatRay.origin = fract(r.origin / 4.0) * 4.0;
    float d2s1 = distToSphere(repeatRay, s1);
    float d2s2 = distToSphere(r, s2);
    float d2s3 = distToSphere(r, s3);
    float dist = differenceOp(d2s2, d2s3);
    dist = differenceOp(dist, d2s1);
    dist = unionOp(d2p, dist);
    return dist;
}
複製程式碼

目前我們寫的都是舊程式碼,只是對Raymarching文章中的重構.讓我們談談normals法線及為什麼需要法線.如果我們有一個平板-比如我們的平面-它的法線總是(0,1,0)也就是指向上方.本例中卻很繁瑣.法線在3D空間是一個float3而且我們需要知道它在射線上的位置.假設射線剛好接觸到球體的左側.法線應是(-1,0,0),就是指向左邊並遠離球體.如果射線稍稍移動到該點的右邊,在球體內部(如-0.001).如果射線稍稍移動到該點的左邊,在球體外部(如0.001).如果我們從左邊減去左邊得到-0.001 - 0.001 = -0.002它指向左邊,所以這就是我們法線的x座標.然後對yz重複同樣操作.我們使用一個名為eps2D向量,來讓向量調和vector swizzling更容易操作,每次都使用選定的值0.001作為各個座標值:

float3 getNormal(Ray ray) {
    float2 eps = float2(0.001, 0.0);
    float3 n = float3(distToScene(Ray(ray.origin + eps.xyy, ray.direction)) -
                      distToScene(Ray(ray.origin - eps.xyy, ray.direction)),
                      distToScene(Ray(ray.origin + eps.yxy, ray.direction)) -
                      distToScene(Ray(ray.origin - eps.yxy, ray.direction)),
                      distToScene(Ray(ray.origin + eps.yyx, ray.direction)) -
                      distToScene(Ray(ray.origin - eps.yyx, ray.direction)));
    return normalize(n);
}
複製程式碼

最後,我們已經準備好看到圖形了.我們再次使用Raymarching程式碼,放在已經新增了法線的核心函式的末尾,這樣我們就可以給每個畫素插值出顏色:

kernel void compute(texture2d<float, access::write> output [[texture(0)]],
                    constant float &time [[buffer(0)]],
                    uint2 gid [[thread_position_in_grid]]) {
    int width = output.get_width();
    int height = output.get_height();
    float2 uv = float2(gid) / float2(width, height);
    uv = uv * 2.0 - 1.0;
    uv.y = -uv.y;
    Ray ray = Ray(float3(0., 4., -12), normalize(float3(uv, 1.)));
    float3 col = float3(0.0);
    for (int i=0; i<100; i++) {
        float dist = distToScene(ray);
        if (dist < 0.001) {
            col = float3(1.0);
            break;
        }
        ray.origin += ray.direction * dist;
    }
    float3 n = getNormal(ray);
    output.write(float4(col * n, 1.0), gid);
}
複製程式碼

如果你現在執行playground你將看到類似的影像:

shadows_4.png

現在我們有了法線,我們可以用lighting() 函式來計算場景中每個畫素的光照.首先我們需要知道燈光的方向(lightRay光線),我們用規範化的燈光位置和當前射線來取得燈光方向.對diffuse漫反射光照我們需要知道法線和光線間的角度,也就是兩者的點積.對specular高光光照我們需要在表面進行反射,它們依賴於我們尋找的角度.不同之處在於,本例中,我們首先發射一個射線到場景中,從表面反射回來,再測量反射線和lightRay光線間的角度.然後對這個值進行一個高次乘方運算來讓它更銳利.最後我們返回混合光線:

float lighting(Ray ray, float3 normal, Light light) {
    float3 lightRay = normalize(light.position - ray.origin);
    float diffuse = max(0.0, dot(normal, lightRay));
    float3 reflectedRay = reflect(ray.direction, normal);
    float specular = max(0.0, dot(reflectedRay, lightRay));
    specular = pow(specular, 200.0);
    return diffuse + specular;
}
複製程式碼

在核心函式中用下面幾行替換最後一行:

Light light = Light(float3(sin(time) * 10.0, 5.0, cos(time) * 10.0));
float l = lighting(ray, n, light);
output.write(float4(col * l, 1.0), gid);
複製程式碼

如果你現在執行playground你將看到類似的影像:

shadows_5.png

下一步,陰影!我們幾乎從本系列的第一部分就開始使用shadow() 函式到現在,只做過少許修改.我們規範化燈光方向(lightDir),並在步進射線時不斷更新disAlongRay:

float shadow(Ray ray, Light light) {
    float3 lightDir = light.position - ray.origin;
    float lightDist = length(lightDir);
    lightDir = normalize(lightDir);
    float distAlongRay = 0.01;
    for (int i=0; i<100; i++) {
        Ray lightRay = Ray(ray.origin + lightDir * distAlongRay, lightDir);
        float dist = distToScene(lightRay);
        if (dist < 0.001) {
            return 0.0;
            break;
        }
        distAlongRay += dist;
        if (distAlongRay > lightDist) { break; }
    }
    return 1.0;
}
複製程式碼

用下面幾行替換核心函式中的最後一行:

float s = shadow(ray, light);
output.write(float4(col * l * s, 1.0), gid);
複製程式碼

如果你現在執行playground你將看到類似的影像:

shadows_6.png

讓我們給場景新增點soft shadows軟陰影.在現實生活中,離物體越遠陰影散佈越大.例如,如果地板上有個立方體,在立方體的頂點我們得到清晰的陰影,但離立方體遠的地方看起來像一個模糊的陰影.換句話說,我們從地板上的某點出發,向著燈光前進,要麼撞到要麼錯過.硬陰影很簡單:我們撞到了什麼東西,這個點主在陰影中.軟陰影則處於兩者之間.用下面幾行更新shadow() 函式:

float shadow(Ray ray, float k, Light l) {
    float3 lightDir = l.position - ray.origin;
    float lightDist = length(lightDir);
    lightDir = normalize(lightDir);
    float eps = 0.1;
    float distAlongRay = eps * 2.0;
    float light = 1.0;
    for (int i=0; i<100; i++) {
        Ray lightRay = Ray(ray.origin + lightDir * distAlongRay, lightDir);
        float dist = distToScene(lightRay);
        light = min(light, 1.0 - (eps - dist) / eps);
        distAlongRay += dist * 0.5;
        eps += dist * k;
        if (distAlongRay > lightDist) { break; }
    }
    return max(light, 0.0);
}
複製程式碼

你會注意到,我們這次從白色(1.0)燈光開始,通過使用一個衰減器(k)來得到不同的(中間的)燈光值.eps變數告訴我們當光線進入場景中時beam波束有多寬.窄波束意味著銳利的陰影,而寬波束意味著軟陰影.我們從小distAlongRay到大開始,不然的話該點所在的曲面會投射陰影到自己身上.然後我們像硬陰影中那樣沿射線前進,並得到離場景的距離,之後我們從eps(beam width波束寬度)中減掉dist併除以eps.這樣給出了波束覆蓋的百分比.如果我們顛倒它(1 - beam width)就得到了處於燈光中的百分比.當我們沿著射線前進時,我們取這個新的值和light值中的最小值,來讓陰影保持最黑.然後再沿射線前進,並根據行進距離均勻地增加beam width波束寬度,並縮放k倍.如果超過了燈光,就跳出迴圈.最後,我們想要避免給燈光一個負值,所以我們返回0.0和燈光值之間的最大值.現在讓我們用新的shadow()函式來改寫核心函式:

float3 col = float3(1.0);
bool hit = false;
for (int i=0; i<200; i++) {
    float dist = distToScene(ray);
    if (dist < 0.001) {
        hit = true;
        break;
    }
    ray.origin += ray.direction * dist;
}
if (!hit) {
    col = float3(0.5);
} else {
    float3 n = getNormal(ray);
    Light light = Light(float3(sin(time) * 10.0, 5.0, cos(time) * 10.0));
    float l = lighting(ray, n, light);
    float s = shadow(ray, 0.3, light);
    col = col * l * s;
}
Light light2 = Light(float3(0.0, 5.0, -15.0));
float3 lightRay = normalize(light2.position - ray.origin);
float fl = max(0.0, dot(getNormal(ray), lightRay) / 2.0);
col = col + fl;
output.write(float4(col, 1.0), gid);
複製程式碼

注意我們切換到了預設的白色.然後我們新增一個布林值叫hit,它來告訴我們碰撞到物體沒有.我們限定當我們到場景的距離在0.001之內就是碰撞了,如果我們沒有碰到任何東西,則用灰色著色,否則確定陰影的數值.在最後我們只需要在場景前面新增另一個(固定的)光源,就能看到陰影的更多細節.如果你現在執行playground你將看到類似的影像:

shadows_7.png

要看這份程式碼的動畫效果,我在下面使用一個Shadertoy嵌入式播放器.只要把滑鼠懸浮在上面,並單擊播放按鈕就能看到動畫:<譯者注:這裡不支援嵌入播放器,我用gif代替https://www.shadertoy.com/embed/XltSWf>

shadow2.mov.gif
原始碼source code已釋出在Github上.

下次見!

相關文章