Metal入門資料021-陰影效果實現(xiàn)(上)

寫在前面:

對Metal技術(shù)感興趣的同學(xué),可以關(guān)注我的專題:Metal專輯
也可以關(guān)注我個人的簡書賬號:張芳濤
所有的代碼存儲的Github地址是:Metal

正文

Computer Graphics一個重要的話題是燈光和陰影效果。本文將講解Metal陰影效果的一些基本用法。首先,我們設(shè)置一個場景:

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

float distanceToRect( float2 point, float2 center, float2 size ) {
point -= center;
point = abs(point);
point -= size / 2.;
return max(point.x, point.y);
}

float distanceToScene( float2 point ) {
float d2r1 = distanceToRect( point, float2(0.), float2(0.45, 0.85) 
);
float2 mod = point - 0.1 * floor(point / 0.1);
float d2r2 = distanceToRect( mod, float2( 0.05 ), float2(0.02, 0.04) );
float diff = differenceOp(d2r1, d2r2);
return diff;
}

上面的代碼中,differenceOp()函數(shù)用于獲取兩個距離(signed distances)之間的差異。當(dāng)我們需要繪制出來其他形狀的時候,我們需要用到這個函數(shù)。distanceToRect()函數(shù)用于確定給定的點是在內(nèi)部還是外部。point -= center用于抵消給定當(dāng)前坐標(biāo)的中心位置。point = abs(point)我們得到給定點的對稱坐標(biāo)。point -= size / 2.得到距離任何邊緣的距離。distanceToScene()用來獲取與場景中任意對象之間的最近的距離。

kernel void compute(texture2d<float, access::write> output [[texture(0)]],
                constant float &timer [[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;
float d2scene = distanceToScene(uv);
bool i = d2scene < 0.0;
float4 color = i ? float4( .1, .5, .5, 1. ) : float4( .7, .8, .8, 1. );
output.write(color, gid);
}

效果圖:

效果圖

接下來,需要顯示出來陰影效果。第一步:我們需要獲得和光線之間的距離。其次:獲得光線的方向。第三步:沿著光線的方向到達光源或者到達物體。我們需要在lightPos位置創(chuàng)建一個燈光,為了更好地顯示,我們需要為這個燈光添加一個動畫效果,然后我們得到任意一個給頂點的距離lightPos的距離,然后根據(jù)和光線之間的距離進行著色。距離物體越近,顏色越淡,距離越遠,光線越亮。為了避免燈光的亮度出現(xiàn)負數(shù),我們使用max()函數(shù):

float2 lightPos = float2(1.3 * sin(timer), 1.3 * cos(timer));
float dist2light = length(lightPos - uv);
color *= max(0.0, 2. - dist2light );
output.write(color, gid);
效果圖

現(xiàn)在這個物體像是透明的,并沒有實際的陰影效果,我們繼續(xù)優(yōu)化:

float getShadow(float2 point, float2 lightPos) {
float2 lightDir = lightPos - point;
float dist2light = length(lightDir);
for (float i=0.; i < 300.; i++) {
    float distAlongRay = dist2light * (i / 300.);
    float2 currentPoint = point + lightDir * distAlongRay;
    float d2scene = distanceToScene(currentPoint);
    if (d2scene <= 0.) { return 0.; }
}
return 1.;
} 

上面這個函數(shù)的意思是:float2 lightDir = lightPos - point;用來獲取從點到光源的方向。float dist2light = length(lightDir);獲取點到光源之間的距離。后面就是一個for循環(huán)把光線分成若干個比較小的步驟。如果不使用這個循環(huán),可能就會在陰影后面留下黑色的洞。接下來,我們計算我們當(dāng)前光線的距離,然后沿著光線移動這個距離,知道找到我們正在采樣的空間點。然后,我們看到我們距離表面有多遠,然后測試我們是否在一個物體內(nèi)。如果是,因為處在陰影中,就返回0。否則就說明光線沒有投射到物體上,返回1

float shadow = getShadow(uv, lightPos);
shadow = shadow * 0.5 + 0.5;
color *= shadow;
output.write(color, gid);

就像代碼中設(shè)置的,我們使用0.5來減弱陰影效果,也可以使用其他的數(shù)值。

效果圖

到目前為止,大概的效果圖出來了,只不過,現(xiàn)在的循環(huán)是以一個像素的步長進行的,這樣性能不太友好,我們可以通過增加射線的"速度"來改善這一點。也就是說,我們不需要把步長設(shè)置這么小,我們可以安全地想任意一個方向移動人以距離,而不是固定的步長。這樣我們就可以快速地跳過空白區(qū)域,當(dāng)找到距離最近曲面的距離時,我們不知道曲面的方向,所以實際上我們有一個圓的半徑與場景的最近部分相交。我們可以沿著光線追蹤,總是踩到圓的邊緣,直到圓半徑變?yōu)?code>0,這意味著它與一個表面相交。這就是我們上次學(xué)到的射線技術(shù)!只需使用以下行替換getShadow()函數(shù)的內(nèi)容:

float2 lightDir = normalize(lightPos - point);
float dist2light = length(lightDir);
float distAlongRay = 0.0;
for (float i=0.0; i < 80.; i++) {
float2 currentPoint = point + lightDir * distAlongRay;
float d2scene = distanceToScene(currentPoint);
if (d2scene <= 0.001) { return 0.0; }
distAlongRay += d2scene;
if (distAlongRay > dist2light) { break; }
}
return 1.;

raymarching里面,該步驟的大小取決于從表面的距離。在空曠的地方,它會跳得很遠。但如果它與物體平行并靠近物體,則距離總是很小,因此跳躍尺寸也很小。這意味著光線傳播速度非常慢。通過固定的步數(shù),它不會走得太遠。通過80步或更多步驟,我們應(yīng)該避免在陰影中獲得“漏洞”。

效果圖

代碼點擊這里

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容