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