小小處理上一個Work的光追陰影
圖1. 上為效能截圖、左下hard shadow、右下soft shadow
使用光追陰影之後,陰影會異常清晰,原因應該不用解釋了;作為老二,hard是好事,但是作為陰影,還是需要有soft一點(半影區域)的效果的。
(Ray tracing shadow is too clear and sharp, we need to soften them.)
PCSS
實際上包含著PCF的運算,但是引入了Blocker Search的概念,原文可以參考NVIDIA的文章。
圖2. Blocker Search公式
一般來說PCF算下去,就是整體陰影都一起柔掉了~但透過這個Blocker Search,讓陰影隨著遮擋物與接收點的距離產生變化,圖1也可以看到,接收點距離遮擋物越遠,半影區域越多,越靠近遮擋物的接收點,則越來越銳利,這就是PCSS的特色了,也很符合現實世界的樣子。
(PCSS is Blocker Search + PCF, received shadow is softer/harder when blocker is far/near.)
從圖2的公式看,我們要取得接收點到燈光的距離&遮擋點到燈光的距離&燈光尺寸,這些資訊在DXR光追中太容易取得了。
payload.distBlockToLight = RayTCurrent(); // get TMax of ray at blocker point
// output shadow
float currAtten = _OutputShadow[DispatchRaysIndex().xy].r;
_OutputShadow[DispatchRaysIndex().xy].r = payload.atten;
float receiverDistToLight = length(lightPos - wpos); // receiver dist to light
_OutputShadow[DispatchRaysIndex().xy].g = receiverDistToLight - payload.distBlockToLight; // blocker distance to light
_OutputShadow[DispatchRaysIndex().xy].b = receiverDistToLight;
_OutputShadow[DispatchRaysIndex().xy].a = _light.shadowSize; // use as light size
在closest shader這個流程,就會更新ray打到的位置,代表blocker與receiver的距離,那只要把燈光到接收點的距離減掉它,就是遮擋物到燈光的距離了,另外這種數值一定是超過[0,1]範圍的,記得至少要用Float16來儲存資料。
(It’s easy to get distance from blocker/receiver to light in DXR Shader. DXR Shader provides RayTCurrent() which indicates the distance between blocker and receiver.)
float2 texelSize = 1.0f / d;
for (int i = -innerLoop; i <= innerLoop; i++)
{
for (int j = -innerLoop; j <= innerLoop; j++)
{
// x for blocked, y for dist to blocker
float4 shadowData = _TexTable[_RayShadowIndex].Sample(_SamplerTable[_CollectShadowSampler], uv + texelSize * float2(i, j)).rgba;
[branch]
if (shadowData.x < 1)
{
avgBlockDepth += shadowData.y; // blocker to light distance
avgReceiverDepth += shadowData.z; // receiver to light distance
lightSize += shadowData.w; // light size
blockCount++;
}
}
}
[branch]
if (blockCount > 0)
{
avgBlockDepth /= blockCount;
avgReceiverDepth /= blockCount;
lightSize /= blockCount;
// penumbra formula: (d_receiver - d_blocker) * light_size / d_blocker
float penumbra = (avgReceiverDepth - avgBlockDepth) * lightSize / avgBlockDepth;
penumbra = pow(penumbra, 0.5f);
return penumbra;
}
Blocker Search的片段,取樣剛才ray tracing後的結果,平均每個點的blocker/receiver到燈光的距離,再套入公式,然後我故意給了平方衰退讓它的衰退變化再快一點。
(Calculate penumbra and give it a square root attenuation.)
最後就是Blur操作了,之所以把標題取為半PCSS是因為這邊根本沒有PCF,PCF需要shadow map的深度值、用shadow matrix換算目前螢幕深度的值,然後呼叫SampleCmp來比較&blend,但是光追陰影是不需要這兩個東西、所以當然也沒有這兩個東西。現在做的事情是Screen-Space Shadow,沒有什麼深度值給你比,所以這邊套上模糊就行了。
(I call this “Semi-PCSS". Because I don’t have shadow map and I can’t use SampleCmp for PCF. There is no PCF here. I use custom box blur to shadow.)
[loop]
for (int i = -innerLoop; i <= innerLoop; i++)
{
[loop]
for (int j = -innerLoop; j <= innerLoop; j++)
{
float w = (innerLoop - abs(i) + 1) * (innerLoop - abs(j) + 1);
atten += _TexTable[_RayShadowIndex].Sample(_SamplerTable[_CollectShadowSampler], uv + float2(i, j) * texelSize * penumbra).r * w;
count += w;
}
}
w的權重簡單計算,如果kernel5x5就是12321 24642 36963 24642 12321,想要正式一點的高斯權重也是行,但是就建議算個查找表了因為高斯有指數操作,在取樣陰影的時候,乘上半影區域的衰退,完成柔和。
(Simply calculate blur weight and consider penumbra when sampling screen shadow.)
最後的最後,由於這是screen-space shadow soft,跟以前轉換到shadow map的空間不同,這裡會發生遠處陰影會糊掉的現象,越來越糊。這是很理所當然的因為遠處的資訊會越來越少,但我們還是用一樣的kernel去取樣,所以最後再加一小段。
float depth = _TexTable[_TransDepthIndex].Sample(_SamplerTable[_CollectShadowSampler], i.uv).r;
float3 wpos = DepthToWorldPos(depth, i.screenPos);
float distRatio = 1 - saturate(abs(wpos.z - _CameraPos.z) * 0.05f);
// reduce penumbra according to distance
// prevent far object blurring too much
float penumbra = PenumbraFilter(i.uv, _PCFIndex);
return BlurFilter(i.uv, _PCFIndex, penumbra * distRatio);
就這樣,用距離去衰退kernel,近處的可能是5×5,遠處的模糊大概就3×3甚至沒有。
(Due to the screen space blur, far object shadow is blurred too much. Calculate distance to camera and reduce blur size on small pixels.)
圖3. 左邊太糊了,右邊隨著距離調整模糊強度
大概就這樣了,下次要開始練習處理point light/spot light。
Ray Tracing Shadow + Semi-PCSS 有 “ 3 則迴響 ”