目錄
1.是什么
2.原理
3.各部分解釋
2.1.從屏幕空間到視圖空間
2.2.以法線半球為基,獲取隨機向量
2.3.應用偏移,并將其轉換為uv坐標
2.4.獲取深度
2.5.比較并計算貢獻
2.6.最后計算
4.改進
4.1.平滑過渡
4.2.模糊
5.變量和語句解釋
5.1._DepthBias
5.2._RangeCheck
5.3.ssDepth >= 0.9999
6.其他事項
6.1.顏色疊加
6.2.向量的隨機
6.3.墻面噪聲
7.效果演示
1.是什么
? ? ? ? SSAO,全稱Screen Space Ambient Occlusion,即屏幕空間環境光遮蔽。其用處是用來模擬全局光照下,一些細節處的陰影。如墻角,縫隙等。也可以讓畫面整體的立體感增強。
? ? ? ? SSAO是由AO發展而來,AO由于性能問題難以被用于實時渲染,因此出現了SSAO,以相對差的效果換取性能。
2.原理
? ? ? ? 對于一個屏幕上像素點而言,將其通過逆變換和深度紋理轉換為視圖空間的點。然后以其法線半球為基,生成隨機向量,用該向量對該點進行偏移,再將偏移后的點轉到屏幕空間,用此時的點對深度紋理進行采樣,得到一個采樣深度,用該深度與原深度(未偏移的點采樣的深度)比較,如果采樣深度小于原深度,說明被遮擋,計算其貢獻;反之未遮擋,貢獻為0。將一個點的每一次偏移的貢獻相加除以偏移的次數,即為該點最終的顏色。
? ? ? ? 我說的可能沒那么清楚,下面通過畫圖演示一下過程(雖然圖畫的也丑就是了):
3.各部分解釋
2.1.從屏幕空間到視圖空間
? ? ? ? 這里采用的是通過先將點轉換到遠裁剪面,然后通過真實深度獲取真實的視圖空間的坐標。參考了這篇文章(https://zhuanlan.zhihu.com/p/92315967)中的方法一。
????????這里簡單講述其原理:
? ? ? ? 我們知道,一個點從世界坐標轉為屏幕坐標需要進行視圖變換(世界空間->視圖空間),投影變換(視圖空間->裁剪空間),透視除法(裁剪空間->ndc空間),視口映射(ndc空間->屏幕空間)。那么我們只需要反向進行就能夠得到一個屏幕坐標在視圖空間的坐標。
? ? ? ? 首先是將屏幕坐標轉到ndc空間,這里可以直接調用shader中的函數來計算,需要注意的是,這里的screenPos需要除以其w分量才能正常使用(如果有uv坐標,可以直接使用uv坐標):
//screenPos
float4 screenPos = ComputeScreenPos(UnityObjectToClipPos(v.vertex));
float2 ndcUV = (screenPos.xy / screenPos.w) * 2 - 1;//uv
float2 ndcUV = uv * 2 - 1;
? ? ? ? 然后是將ndc轉為裁剪,我們先將該點視為遠裁剪面上的點,則:
float3 clipPos = float3(ndcUV.x, ndcUV.y, 1) * _ProjectionParams.z;
? ? ? ? 然后將裁剪轉為視圖,由于轉為視圖后仍是原裁剪面上的點,所以乘以該點的線性深度值來獲取真實坐標。之所以用clipPos.xyzz進行計算,則是因為裁剪空間下,遠裁剪面的zw相同。
//獲取深度和法線(因為后面要用,所以這里將法線一起獲取了)
float4 depthNormal = tex2D(_CameraDepthNormalsTexture, f.uv);
float3 ssNormal; //ss -> Screen Space
float ssDepth;
DecodeDepthNormal(depthNormal, ssDepth, ssNormal);
float ssDepth01 = Linear01Depth(ssDepth);//變換
float3 viewPos = mul(unity_CameraInvProjection, clipPos.xyzz).xyz * ssDepth01;
? ? ? ? 至此,視圖空間的坐標已得到。
2.2.以法線半球為基,獲取隨機向量
? ? ? ? 我們首先需要知道TBN矩陣,它是由切線,副切線,法線三者構成的一個3*3的矩陣,用途是做切線空間與其他空間轉換的媒介。并且這種轉換不會改變向量的長度(前提是三個都是單位向量)。
? ? ? ? 所以我們先在切線空間中隨機生成一個x在[-1,1],y在[-1,1],z在[0,1]的向量,然后將其轉換到視圖空間下。
float3 viewNormal = normalize(ssNormal);
//隨機生成,是正交基呈現隨機性
float3 viewTangent = normalize(GetRandomVec(f.uv.xy));
float3 viewBitangent = cross(viewTangent, viewNormal);
viewTangent = cross(viewBitangent, viewNormal);
float3x3 TBN = float3x3(viewTangent.x, viewBitangent.x, viewNormal.x,viewTangent.y, viewBitangent.y, viewNormal.y,viewTangent.z, viewBitangent.z, viewNormal.z);//for循環中的語句
float3 randomVec = GetRandomVecHalf(f.uv.yx * i);
//_AORadius是一個由c#腳本傳進的參數,目的是控制陰影的大小
float3 randomVecView = mul(TBN, randomVec) * _AORadius;
2.3.應用偏移,并將其轉換為uv坐標
? ? ? ? 將隨機向量應用到原點,然后通過一系列轉換將其變為uv坐標,以采樣深度法線紋理。
float3 viewOffPos = viewPos + randomVecView;
float4 clipOffPos = mul(unity_CameraProjection, float4(viewOffPos, 1));
float2 sampleUV = clipOffPos.xy / clipOffPos.w;
sampleUV = sampleUV * 0.5 + 0.5;
2.4.獲取深度
? ? ? ? waaaagh!
//獲取深度
float4 sampleDepthNormal = tex2D(_CameraDepthNormalsTexture, sampleUV);
float sampleDepth;
//獲取這個法線的原因是我懶得單獨獲取深度了
float3 sampleNormal;
DecodeDepthNormal(sampleDepthNormal, sampleDepth, sampleNormal);
2.5.比較并計算貢獻
? ? ? ? 需要注意的是,這里比較的是實際的深度值,需要經過LinearEyeDepth轉換。
? ? ? ? 我這里使用大于判斷,是因為最后會將進行1 - ao的運算,相當于黑白取反。
//for外
ssDepth = LinearEyeDepth(ssDepth);//for內
sampleDepth = LinearEyeDepth(sampleDepth);
//_DepthBias:防止自遮擋
float depthDiff = sampleDepth - ssDepth - _DepthBias;
//_RangeCheck:去除不正常的陰影
if (depthDiff > 0 && depthDiff < _RangeCheck)
{ao += 1;
}
2.6.最后計算
? ? ? ? 取平均。
//1-是因為上面進行了相反的運算,所以這里1-再反一次;
//pow和AOStrength都是為了控制強度
ao = 1 - pow(ao / _SampleTime, 1) * _AOStrength;
4.改進
4.1.平滑過渡
? ? ? ? 添加距離衰減,遠距離貢獻小,近距離貢獻大;
? ? ? ? 同樣因為取反操作,所以這里的代碼邏輯為:遠距離貢獻大,近距離貢獻小。
float weight = smoothstep(0, _AORadius, length(randomVec));float depthDiff = sampleDepth - ssDepth - _DepthBias;
if (depthDiff > 0 && depthDiff < _RangeCheck)
{ao += 1 * weight;
}
4.2.模糊
? ? ? ? 因為直接生成的ssao會呈現一種噪聲密布的狀態

?? ? ? ? 所以需要對其進行模糊,模糊的方法很多,我使用的是高斯模糊,原理就不說明了。效果如下:

5.變量和語句解釋
5.1._DepthBias
? ? ? ? 用于防止自遮擋,因為有時會出現由于精度誤差導致的自己遮擋自己的問題。這個變量會將最后的深度差再減小一些,或者理解為將采樣的深度適當減小一些。這樣就不會出現自遮擋的問題了。
5.2._RangeCheck
? ? ? ? 有時我們會發現,明明不該出現陰影的地方出現了陰影,比如兩個僅僅只有前后關系的物體交界處。這個變量就是為了防止這種情況,如果深度差小于指定值,才會計算貢獻,反之舍棄。

5.3.ssDepth >= 0.9999
? ? ? ? 上面沒有寫出來,其位置是在片元著色器中用原點獲取深度之后。用處是為了防止天空盒與物體交界處出現陰影(理論上_RangeCheck就能解決這問題,但不知道什么原因不行):
if (ssDepth >= 0.9999)
{return 1;
}

6.其他事項
6.1.顏色疊加
? ? ? ? 最后ssao求出來后還有的顏色疊加過程,我是圖方便直接疊上去了。
6.2.向量的隨機
? ? ? ? 向量的隨機性會極大程度地影響最后ssao的效果,所以請盡量保證向量的隨機性。
6.3.墻面噪聲
? ? ? ? 我的實現會導致_AORadius增大時,墻面上會出現噪點,可以通過增大_SampleTime來降低其存在感。當然這不是一個好辦法,我解決后會更新該文章。
7.效果演示
?????????代碼在https://github.com/RedShaoWuHuaQu/UnityShader/tree/main/SSAO中。

