屏幕空间环境光屏蔽(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后局部的阴影更明显了。

原文地址:https://www.cnblogs.com/wickedpriest/p/13219981.html