屏幕空間環境光屏蔽(SSAO)探秘


屏幕空間環境光遮蔽(Screen Space Ambient Occlusion,SSAO),是一種在計算機圖形學中實現近似環境光屏蔽效果的渲染技術。離線渲染中,在渲染一個物體A時,如果它的周圍有一些別的物體B、C等,由於它們遮擋了光線,因此最終渲染出的物體A會顯得有一些暗。這種現象在實時渲染就很難模仿。雖然在Phong模型中設置了Ambient一項來代表全局光照,但是這個值對於某個物體往往是統一的,就像下圖這樣:

因此,我們需要一種方法可以模擬物體周圍的遮擋情況,並反映在Ambient項中,這就是SSAO要解決的問題。下面展示一張利用SSAO技術的同樣的模型的渲染效果,可以看到明顯的凹凸層次:

具體來說,SSAO的實現分為以下幾個步驟:
1.渲染當前的場景到一張RT中,該RT中的每個像素存儲了當前渲染像素的法向和深度(均為相機坐標系下)。
2.利用上一步所生成的法向深度圖,采樣生成一個初步的SSAO RT。
3.對上一步生成的SSAO RT進行blur處理,使其更為平滑。
4.將最終生成的SSAO圖作為Ambient項的參數應用到渲染過程中。
下面將對每一個步驟進行講述:

渲染法向深度圖

這一步很簡單,主要的做法是最終寫入像素的時候,給像素的前三位float存入normal,第四位存入depth就可以,在這里直接貼出shader代碼:

VertexOut VS(VertexIn vin)
{
	VertexOut vout;

	vout.PosV = mul(float4(vin.PosL, 1.0f), gWorldView).xyz;
	vout.NormalV = mul(float4(vin.NormalL, 0.0f), gWorldInvTransposeView).xyz;
		
	// Transform to homogeneous clip space.
	vout.PosH = mul(float4(vin.PosL, 1.0f), gWorldViewProj);
	return vout;
}
 

float4 PS(VertexOut pin) : SV_Target
{
	float3 n = normalize(pin.NormalV);
	return float4(n, pin.PosV.z);
}

生成SSAO圖

這一步比較復雜。具體步驟就是,渲染一張和視錐體截面一樣大小的長方形(我一般選取深度為zFar的截面),並記錄下該長方形四個頂點相機坐標系的坐標位置,同樣也是渲到一張RT中。在渲染時,輸入上一步所生成的法向深度圖。然后在這次渲染的PixelShader中,對每一個像素對應頂點的周邊情況進行采樣,確定該像素點的Ambient參數,最終寫入到新的RT中,生成SSAO圖。
具體如下圖所示:

在該圖中,當渲染到某個具體的像素v時,首先應當根據法向深度圖復原出v點所實際對應的場景中的頂點p(p=pz/vz * v)。然后可以得到p的法向n。接着,對p點采樣一個方向得到點q,再次利用r = rz/qz * q得到q實際對應的場景點r(qz在采樣時就可以得到,rz則通過讀取法向深度圖獲取)。最后比較r和p的距離與法向(r-p和n的角度),最終計算得到遮擋系數,存入SSAO圖對應的像素中。
shader代碼如下:

VertexOut SSAOVS(VertexIn vin)
{
	VertexOut vout;
	vout.PosH = float4(vin.PosL.x, vin.PosL.y, 1.0, 1.0);
	vout.PosV = float3(vin.PosL.x * gFarPlaneSize.x, vin.PosL.y * gFarPlaneSize.y, gFarPlaneDepth);
	vout.Tex = vin.Tex;
	return vout;
}

float4 SSAOPS(VertexOut pin) : SV_Target
{
	float4 normalDepth = gNormalDepthMap.SampleLevel(samNormalDepth, pin.Tex, 0.0f);
	float3 n = normalDepth.xyz;
	float pz = normalDepth.w;
	
	float3 p = pz / pin.PosV.z * pin.PosV.xyz;
	
	// Extract random vector and map from [0,1] --> [-1, +1].
	float3 randVec = 2.0f * gRandomVecMap.SampleLevel(samRandomVec, 4.0f*pin.Tex, 0.0f).rgb - 1.0f;
	float occlusionSum = 0.0f;

	int sampleCount = 14;
	[unroll]
	for (int i = 0; i < sampleCount; ++i)
	{
		float3 offset = reflect(gOffsetVectors[i].xyz, randVec);
		float flip = sign(dot(offset, n));
		float3 q = p + flip * gOcclusionRadius * offset;
		float4 projQ = mul(float4(q, 1.0f), gViewToTexSpace);
		projQ /= projQ.w;
		float rz = gNormalDepthMap.SampleLevel(samNormalDepth, projQ.xy, 0.0f).a;
		float3 r = (rz / q.z) * q;
		float dp = max(dot(n, normalize(r - p)), 0.0f);
		float occlusion = dp * OcclusionFunction(p.z - r.z);

		occlusionSum += occlusion;
	}

	occlusionSum /= sampleCount;

	float access = 1.0f - occlusionSum;

	// Sharpen the contrast of the SSAO map to make the SSAO affect more dramatic.
	return saturate(pow(access, 4.0f));
}

關於如何對p進行采樣生成q,因為一般shader中沒有隨機函數,因此常用的方法是通過一張隨機生成的貼圖信息輸入shader中來引入隨機性。gOffsetVectors是14個方向均勻分布的向量,通過對一個任意的向量執行reflect操作來引入隨機性,最終生成了14個在p的normal半球面隨機分布的q。

平滑SSAO圖

上一步所生成的SSAO圖因為采樣數太少,因此會顯得噪點過多,如下圖所示:

為了解決這個問題,我們可以blur一下SSAO圖,使其更為平滑。Blur過程分為兩步,第一步是先blur豎直方向,第二部再平滑水平方向,如此反復操作。
下面是執行blur的shader代碼。注意,在blur時,如果周圍的像素點的normal和距離與中心點差距太大,那么則不參與該中心點的blur過程:

float4 PS(VertexOut pin, uniform bool gHorizontalBlur) : SV_Target
{
	float2 texOffset;
	{
	if (gHorizontalBlur)
	{
		texOffset = float2(gTexelWidth, 0.0f);
	}
	else
		texOffset = float2(0.0f, gTexelHeight);
	}

	// The center value always contributes to the sum.
	float4 color = gWeights[5] * gInputImage.SampleLevel(samInputImage, pin.Tex, 0.0);
	float totalWeight = gWeights[5];

	float4 centerNormalDepth = gNormalDepthMap.SampleLevel(samNormalDepth, pin.Tex, 0.0f);

	for (float i = -gBlurRadius; i <= gBlurRadius; ++i)
	{
		// We already added in the center weight.
		if (i == 0)
			continue;

		float2 tex = pin.Tex + i * texOffset;

		float4 neighborNormalDepth = gNormalDepthMap.SampleLevel(
			samNormalDepth, tex, 0.0f);

		//
		// If the center value and neighbor values differ too much (either in 
		// normal or depth), then we assume we are sampling across a discontinuity.
		// We discard such samples from the blur.
		//

		if (dot(neighborNormalDepth.xyz, centerNormalDepth.xyz) >= 0.8f &&
			abs(neighborNormalDepth.a - centerNormalDepth.a) <= 0.2f)
		{
			float weight = gWeights[i + gBlurRadius];

			// Add neighbor pixel to blur.
			color += weight * gInputImage.SampleLevel(
				samInputImage, tex, 0.0);

			totalWeight += weight;
		}
	}

	// Compensate for discarded samples by making total weights sum to 1.
	return color / totalWeight;
}

blur之后可以看到SSAO圖比之前平滑了很多

應用SSAO

在生成了SSAO圖后,接下來執行正常的渲染流程。此時輸入SSAO圖,並在PixelShader中對它進行采樣,獲取對應的Ambient值,最后在計算的時候將它乘到Ambient項中即可。

float ambient_weight = 1.0; 
if (gUseSSAO)
{
      pin.SSAOPosH /= pin.SSAOPosH.w;
      ambient_weight = gSSAOMap.Sample(samLinear, pin.SSAOPosH.xy, 0.0f).r;
}
...

litColor = texColor * (ambient * ambient_weight + diffuse) + spec;

最后展示一下我所實現的SSAO效果,可以看到加了SSAO后局部的陰影更明顯了。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM