屏幕空间环境光屏蔽(SSAO)探秘

屏幕空间环境光遮蔽(Screen Space Ambient Occlusion,SSAO),是一种在计算机图形学中实现近似环境光屏蔽效果的渲染技术。离线渲染中,在渲染一个物体A时,如果它的周围有一些别的物体B、C等,由于它们遮挡了光线,因此最终渲染出的物体A会显得有一些暗。这种现象在实时渲染就很难模仿。虽然在Phong模型中设置了Ambient一项来代表全局光照,但是这个值对于某个物体往往是统一的,就像下图这样:
屏幕空间环境光屏蔽(SSAO)探秘
因此,我们需要一种方法可以模拟物体周围的遮挡情况,并反映在Ambient项中,这就是SSAO要解决的问题。下面展示一张利用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图。
具体如下图所示:
屏幕空间环境光屏蔽(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图因为采样数太少,因此会显得噪点过多,如下图所示:
屏幕空间环境光屏蔽(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图后,接下来执行正常的渲染流程。此时输入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后局部的阴影更明显了。

屏幕空间环境光屏蔽(SSAO)探秘

原文链接: https://www.cnblogs.com/wickedpriest/p/13219981.html

欢迎关注

微信关注下方公众号,第一时间获取干货硬货;公众号内回复【pdf】免费获取数百本计算机经典书籍;

也有高质量的技术群,里面有嵌入式、搜广推等BAT大佬

    屏幕空间环境光屏蔽(SSAO)探秘

原创文章受到原创版权保护。转载请注明出处:https://www.ccppcoding.com/archives/361176

非原创文章文中已经注明原地址,如有侵权,联系删除

关注公众号【高性能架构探索】,第一时间获取最新文章

转载文章受原作者版权保护。转载请注明原作者出处!

(0)
上一篇 2023年3月2日 下午1:41
下一篇 2023年3月2日 下午1:41

相关推荐