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

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

MetalKit系統文章目錄git


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

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() 函數,它給出場景中到任意物體的最近距離.咱們用這些函數來生成一個形狀,它看起來像是一個帶有幾個洞的空心球體:post

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你將看到相似的圖像:ui

shadows_4.png

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

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上.

下次見!

相關文章
相關標籤/搜索