文章目錄
- Intro - 介紹
- The trick - 技巧
- An improvement - 改進
- GGB
原文:penumbra shadows in raymarched SDFs
Intro - 介紹
許多步進用的距離場(distance fields)函數,他們都是本身提供了全局資訊。這意味着,當要着色着色某一點時,它可以使用距離場函數(distance function)很簡單就探索出周圍幾何體。這不像傳統的光栅器,它得使用一個預先烘焙好的全局全局光照資料給後續的使用(如shadowmap陰影圖,depthmap深度圖,pointcloud點雲圖),或是在一個光線追蹤要查找全局資訊就必須通過光線投射來采樣幾何體資訊,而在distance field距離場中是着色期間就可以取到的,這簡直就是免費的(“免費”是加了雙引号)。這意味着許多逼真的着色器和光照技術使用距離場都是很簡單就可以實作的。并且raymarcher射線步進器在采樣/渲染時會更真實。此文将運用光線步進的這些良好的特性來免費渲染半影的軟陰影。
免費的計算軟陰影與半影
經典的光線投射陰影
The trick - 技巧
那麼,假設你有一個距離場編碼函數
float map(vec3 p)
。你可以在這裡
檢視建構一些基礎的距離函數。為了簡便起見,假設
map()
函數包含了場景中所有需要渲染的對象,并且所有對象都允許投射陰影給其他對象。那麼,在着色一個點的陰影計算是很簡單的,沿着光方向的向量來射線步進,直到從光源到着色點的距離有碰撞到東西就好了。你可能需要處理類似這樣的代碼:
float shadow( in vec3 ro, in vec3 rd, float mint, float maxt )
{
for( float t=mint; t<maxt; )
{
float h = map(ro + rd*t);
if( h<0.001 )
return 0.0;
t += h;
}
return 1.0;
}
這代碼運作得真的優美,并且生成良好且精确的銳利陰影,如上面的 經典的光線投射陰影 圖一樣。現在我們可以僅僅隻添加一句代碼就可以讓結果看起來更好!
這個技巧就是,處理那些陰影射線沒有碰撞到任何對象的,但射線在步進過程中又非常靠近碰撞對象的處理。那麼,這些着色點就很可能是處于penumbra半影的點。那麼就很有可能越是近靠近的已碰撞對象的點,就是越黑的點。同樣的,你着色的點最靠近碰撞對象但沒有碰撞的點,也會越黑。那麼,這些都是發生在我們的陰影射線步進處理的,這些距離對于我們來說都是有效的陰影距離!上面的代碼中第一個距離是
h
,接着第二個是
t
。是以,我們可以簡單的為每一步的步進點計算半影因子,并取半影中最暗的。在2019年,一些Shadertoy使用者注意到,可以通過一個内半影來偏移陰影計算。最終代碼看起來是這樣的:
float softshadow( in vec3 ro, in vec3 rd, float mint, float maxt, float w )
{
float s = 1.0;
for( float t=mint; t<maxt; )
{
float h = map(ro + rd*t);
s = min( s, 0.5+0.5*h/(w*t) );
if( s<0.0 ) break;
t += h;
}
s = max(s,0.0);
return s*s*(3.0-2.0*s); // smoothstep
}
譯者jave.lin:千萬别在Unity的shaderLab跑這段softShadow,我跑了3次,Unity都無響應,我還以為其他地方有死循環了,後來發現是此段程式有問題。
你可以通過将0到1的值偏移到-1到1,來優化0.5乘數與加數(這裡有點沒看懂)。你可以這找到參考的實作:https://www.shadertoy.com/view/WdyXRD。
w=1/2
w = 1/8
w = 1/32
w = 1/128
這一簡單的修改就足以生成本頁面開頭的那張更好效果的軟陰影圖。如你所見,提升是巨大的:不單隻得到了軟陰影,還讓陰影更真實了,當陰影阻擋對象與被阻擋對象連接配接處(注意橋與地闆接觸的地方)時陰影是銳利的,而當陰影阻擋對象遠離被阻擋變越遠時,半影則更加軟。這裡,消耗點在于每一次步進點的一次除法,零消耗是相對
map()
的處理内容來衡量的。
w
參數是光源的大小,用于控制硬/軟陰影的。可以看到不同的
w
值對應的渲染陰影的效果。
是以,基本上,如果你會處理典型的光線步進陰影,你也可以免費的實作半影的軟陰影。
下圖展示了一個使用光線步進的距離場的一個示例:
半影的軟陰影示例
而你可以檢視更多的 使用距離場來光線步進的文章示例。
An improvement - 改進
在這項技術公布7年後,Sebastian Aaltonen 在他的GDC上釋出了一項改進,這對于在使用這項技術時出現帶條瑕疵是很有幫助的,尤其是銳利的角投射出來的陰影。
為了這個算法的穩定性,我們應該精準地沿着射線找到半影位置。然而,由于我們的步進中,可能會錯過沿着射線生成最暗的半影點。那可以說明這種步進的步長方式有漏光。特别是銳利的角投射陰影時通常會錯過最暗半影的。Sebastian的技巧有助于在
h/t
計算半影時,不僅僅隻采樣步進點的情況,他也考量了每次疊代步進射線點到表面最近的點。或是說,使用目前的與前一個的采樣點,他的技巧是使用了三角化資訊來計算考量最近距離的。下圖展示了這種幾何情況:
白色箭頭是我們步進用的射線。綠點是目前沿着射線步進到的點,而紅點是之前一個步進的點。綠圈與紅圈代表目前與之前
SDF
的場景最近距離。其一可表示最近表面将落在兩個圓形相交的位置(黃線那兩個點)。最近點将會是沿着射線的,與黃線有交點的的距離。
y
就是目前點(綠點)到沿着射線點最近點(黃點)之間的距離,而
d
就是最近的距離(黃色線段中一半的那個點)。那麼,用代碼來計算這兩個量是很簡單的:
譯者jave.lin:上面的描述有可能不太好了解,下面我再話一個更詳細的圖,從圖中可知:
y=|BE|, d = |CE|
float y = r2*r2/(2.0*r1);
float d = sqrt(r2*r2-y*y);
r1
和
r2
分别是紅圈和綠圈的半徑,或這說是,SDFs 估算出前一個步進點與目前步進點的距離。根據這兩個量,我們可以改進半影的估算:
float softshadow( in vec3 ro, in vec3 rd, float mint, float maxt, float k )
{
float res = 1.0;
float ph = 1e20;
for( float t=mint; t<maxt; )
{
float h = map(ro + rd*t);
if( h<0.001 )
return 0.0;
float y = h*h/(2.0*ph);
float d = sqrt(h*h-y*y)
res = min( res, k*d/max(0.0,t-y) );
ph = h;
t += h;
}
return res;
}
這将會在複雜情況下生成更好的陰影,可以參考下圖對比:
原來的方法
改進後的方法
你可以檢視這裡已改進過的實作:https://www.shadertoy.com/view/lsKcDD
GGB
- y & b.gbb