本系列文章是對 metalkit.org 上面MetalKit內容的全面翻譯和學習.c++
在本系列的第二部分中,咱們將學習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
座標.而後對y
和z
重複一樣操做.咱們使用一個名爲eps的2D
向量,來讓向量調和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
如今咱們有了法線,咱們能夠用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你將看到相似的圖像:
下一步,陰影!咱們幾乎從本系列的第一部分就開始使用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你將看到相似的圖像:
讓咱們給場景添加點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你將看到相似的圖像:
要看這份代碼的動畫效果,我在下面使用一個Shadertoy
嵌入式播放器.只要把鼠標懸浮在上面,並單擊播放按鈕就能看到動畫:<譯者注:這裏不支持嵌入播放器,我用gif代替https://www.shadertoy.com/embed/XltSWf>
下次見!