[MetalKit]Shadows in Metal part 2陰影2

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

MetalKit系統(tǒng)文章目錄


在本系列的第二部分中,我們將學習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;
    }
};

下一步,我們創(chuàng)建幾個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;
}

下一步,我們創(chuàng)建一個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);
}

如果你現(xiàn)在運行playground你將看到類似的圖像:

shadows_4.png

現(xiàn)在我們有了法線,我們可以用lighting()函數來計算場景中每個像素的光照.首先我們需要知道燈光的方向(lightRay光線),我們用規(guī)范化的燈光位置和當前射線來取得燈光方向.對diffuse漫反射光照我們需要知道法線和光線間的角度,也就是兩者的點積.對specular高光光照我們需要在表面進行反射,它們依賴于我們尋找的角度.不同之處在于,本例中,我們首先發(fā)射一個射線到場景中,從表面反射回來,再測量反射線和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);

如果你現(xiàn)在運行playground你將看到類似的圖像:

shadows_5.png

下一步,陰影!我們幾乎從本系列的第一部分就開始使用shadow()函數到現(xiàn)在,只做過少許修改.我們規(guī)范化燈光方向(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);

如果你現(xiàn)在運行playground你將看到類似的圖像:

shadows_6.png

讓我們給場景添加點soft shadows軟陰影.在現(xiàn)實生活中,離物體越遠陰影散布越大.例如,如果地板上有個立方體,在立方體的頂點我們得到清晰的陰影,但離立方體遠的地方看起來像一個模糊的陰影.換句話說,我們從地板上的某點出發(fā),向著燈光前進,要么撞到要么錯過.硬陰影很簡單:我們撞到了什么東西,這個點主在陰影中.軟陰影則處于兩者之間.用下面幾行更新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倍.如果超過了燈光,就跳出循環(huán).最后,我們想要避免給燈光一個負值,所以我們返回0.0和燈光值之間的最大值.現(xiàn)在讓我們用新的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之內就是碰撞了,如果我們沒有碰到任何東西,則用灰色著色,否則確定陰影的數值.在最后我們只需要在場景前面添加另一個(固定的)光源,就能看到陰影的更多細節(jié).如果你現(xiàn)在運行playground你將看到類似的圖像:

shadows_7.png

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

shadow2.mov.gif

源代碼source code已發(fā)布在Github上.
下次見!

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末唇敞,一起剝皮案震驚了整個濱河市丐枉,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌匾灶,老刑警劉巖,帶你破解...
    沈念sama閱讀 212,542評論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件浪腐,死亡現(xiàn)場離奇詭異燥滑,居然都是意外死亡雹洗,警方通過查閱死者的電腦和手機香罐,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,596評論 3 385
  • 文/潘曉璐 我一進店門卧波,熙熙樓的掌柜王于貴愁眉苦臉地迎上來时肿,“玉大人,你說我怎么就攤上這事港粱◇Τ桑” “怎么了?”我有些...
    開封第一講書人閱讀 158,021評論 0 348
  • 文/不壞的土叔 我叫張陵查坪,是天一觀的道長寸宏。 經常有香客問我,道長偿曙,這世上最難降的妖魔是什么氮凝? 我笑而不...
    開封第一講書人閱讀 56,682評論 1 284
  • 正文 為了忘掉前任,我火速辦了婚禮望忆,結果婚禮上罩阵,老公的妹妹穿的比我還像新娘。我一直安慰自己启摄,他們只是感情好稿壁,可當我...
    茶點故事閱讀 65,792評論 6 386
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著歉备,像睡著了一般傅是。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上蕾羊,一...
    開封第一講書人閱讀 49,985評論 1 291
  • 那天喧笔,我揣著相機與錄音,去河邊找鬼龟再。 笑死书闸,一個胖子當著我的面吹牛,可吹牛的內容都是我干的吸申。 我是一名探鬼主播梗劫,決...
    沈念sama閱讀 39,107評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼享甸,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了梳侨?” 一聲冷哼從身側響起蛉威,我...
    開封第一講書人閱讀 37,845評論 0 268
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎走哺,沒想到半個月后蚯嫌,有當地人在樹林里發(fā)現(xiàn)了一具尸體,經...
    沈念sama閱讀 44,299評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡丙躏,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 36,612評論 2 327
  • 正文 我和宋清朗相戀三年择示,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片晒旅。...
    茶點故事閱讀 38,747評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡栅盲,死狀恐怖,靈堂內的尸體忽然破棺而出废恋,到底是詐尸還是另有隱情谈秫,我是刑警寧澤,帶...
    沈念sama閱讀 34,441評論 4 333
  • 正文 年R本政府宣布鱼鼓,位于F島的核電站拟烫,受9級特大地震影響,放射性物質發(fā)生泄漏迄本。R本人自食惡果不足惜硕淑,卻給世界環(huán)境...
    茶點故事閱讀 40,072評論 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望嘉赎。 院中可真熱鬧置媳,春花似錦、人聲如沸曹阔。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,828評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽赃份。三九已至寂拆,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間抓韩,已是汗流浹背纠永。 一陣腳步聲響...
    開封第一講書人閱讀 32,069評論 1 267
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留谒拴,地道東北人尝江。 一個月前我還...
    沈念sama閱讀 46,545評論 2 362
  • 正文 我出身青樓,卻偏偏與公主長得像英上,于是被迫代替她去往敵國和親炭序。 傳聞我的和親對象是個殘疾皇子啤覆,可洞房花燭夜當晚...
    茶點故事閱讀 43,658評論 2 350

推薦閱讀更多精彩內容