Direct X 12 – Ambient Occulusion – 环境光遮蔽

21.2 屏幕空间环境光遮蔽

屏幕空间环境光遮蔽(screen space ambient occlusion,SSAO)的原理是,将场景的法线向量绘制到一个与屏幕尺寸相同的render target中,场景的深度值仍然绘制到depth/stencil buffer中,之后使用我们记录的场景法线向量与depth buffer计算每一个像素的环境光遮蔽率。当我们得到了存储着屏幕中每一个像素的环境光遮蔽率之后,照常将场景绘制到back buffer,并且使用SSAO信息作为环境光计算模型中的因子。

21.2.1 用于绘制法线向量与深度的渲染通道

首先我们将camear空间下场景物体的法线向量绘制到一张格式为DXGI_FORMAT_R16G16B16A16_FLOAT且尺寸与视窗尺寸相同的贴图中,同时我们需要照常将depth/stencil buffer绑定至渲染管线用以记录场景的深度值。该渲染通道所使用的顶点着色器与像素着色器如下:

// HLSL
// 常用HLSL代码
#include "Common.hlsl"

struct VertexIn
{
    float3 PosL : POSITION;
    float3 NormalL : NORMAL;
    float2 TexC : TEXCOORD;
    float3 TangentU : TANGENT;
};

struct VertexOut
{
    float4 PosH : SV_POSITION;
    float3 NormalW : NORMAL;
    float3 TangentW : TANGENT;
    float2 TexC : TEXCOORD;
};

VertexOut VS(VertexIn vin)
{
    VertexOut = (VertexOut)0.f;

    // 读取材质数据
    MaterialData matData = gMaterialData[gMaterialIndex];

    // 假设并没有不对称缩放;
    // 否则需要使用世界矩阵的逆转置矩阵
    vout.NormalW = mul(vin.NormalL, (float3x3)gWorld);
    vout.TangentW = mul(vin.TangentU, (float3x3)gWorld);

    // 转换至齐次剪裁空间
    float4 posW = mul(float4(vin.PosL, 1.f), gWorld);
    vout.PosH = mul(posW, gViewProj);

    float4 texC = mul(float4(vin.TexC, 0.f, 1.f), gTexTransform);
    vout.TexC = mul(texC, matData.MatTransform).xy;

    return vout;
}

float4 PS(VertexOut pin) : SV_Target
{
    // 读取材质数据
    MaterialData matData = gMaterialData[gMaterialIndex];

    float4 diffuseAlbedo = matData.DiffuseAlbedo;
    uint diffuseMapIndex = matData.DiffuseMapIndex;
    uint normalMapIndex = matData.NormalMapIndex;

    // 动态读取贴图数组中的贴图
    diffuseAlbedo *= gTextureMaps[diffuseMapIndex].Sample(gsamAnisotropicWrap, pin.TexC);

#ifdef ALPHA_TEST
    // 如果α<0.1,那么尽快舍弃该像素点
    clip(diffuseAlbedo.a - 0.1f);
#endif

    // 插值后的法线向量可能不再是单位向量,需要进行标准化
    pin.NormalW = normalize(pin.NormalW);

    // 注意:本demo中,我们使用顶点插值后的法线向量而不是法线映射贴图中的法线向量
    // 将法线向量转换至camera坐标系
    float3 normalV = mul(pin.NormalW, (float3x3)gView);

    return float4(normalV, 0.f);
}

正如上述HLSL代码所示,像素着色器将输出位于camera坐标系的法线向量。由于我们的render target的格式为浮点数,所以我们可以写入任何浮点数数据。

21.2.2 环境光遮蔽的渲染通道

在我们将camera坐标系内的法线向量绘制到贴图并将场景深度记录到depth buffer之后,在我们暂时不会将depth buffer作为“depth buffer”(之后depth buffer用作着色器输入资源)。接着,绘制一张覆盖整个屏幕的面片以激活我们所绘制的法线贴图中的每个像素的像素着色器。之后像素着色器将使用法线贴图与之前的depth buffer生成每个像素点的ambient accessibility(与环境光遮蔽率相对)并绘制到贴图中,而我们将此贴图称为SSAO映射贴图。需要注意的是,即使我们绘制的法线/深度贴图的尺寸与屏幕尺寸相同,但是出于性能考虑,我们绘制的SSAO映射贴图的分辨率只是视窗尺寸的四分之一。请参考下图。

上图展示了SSAO中所使用的几个点。坐标点p,表示我们当前正在处理的像素,基于之前所记录的depth buffer被重新投影至camera坐标系内,向量v穿过camera原点与坐标点p。坐标点q是坐标点p法线方向上的半球上的随机一点。坐标点r是沿着坐标点q至原点方向上距离camera最近的一点。如果|p(z)-r(z)|的值足够小切向量r-p与法线n的夹角小于90°,那么我们可以认为r点将遮蔽p点。在demo中,我们将随机取14个采样点并且将这些点的环境光遮蔽率进行平均,以求出屏幕空间内每个点的环境光遮蔽率。

21.2.2.1 重新构建camera空间的坐标位置

当我们绘制一张覆盖整个视窗的面片以激活SSAO映射贴图中每个像素的像素着色器时,我们可以使用投影矩阵的逆矩阵将面片上的点从NDC空间转换至投影矩阵中近平面上的点:

// HLSL
static const float2 gTexCoords[6] = 
{
    float2(0.f, 1.f),
    float2(0.f, 0.f),
    float2(1.f, 0.f),
    float2(0.f, 1.f),
    float2(1.f, 0.f),
    float2(1.f, 1.f)
};

// 绘制6个顶点
VertexOut VS(uint vid : SV_VertexID)
{
    VertexOut vout;
    vout.TexC = gTexCoords[vid];

    // NDC空间内覆盖屏幕的面片
    vout.PosH = float4(2.f * vout.TexC.x - 1.f, 1.f - 2.f * vout.TexC.y, 0.f, 1.f);

    // 将顶点转换至camera空间内的投影近平面
    float4 ph = mul(vout.PosH, gInvProj);
    vout.PosV = ph.xyz / ph.w;

    return vout;
}

这六个顶点将构成近平面(需要6个顶点是因为长方形又两个三角形构成,所以需要6个顶点),经过插值之后的像素属性PosV就代表了上图中的向量v。现在,对于每一个像素,我们都需要对depth buffer进行采样以获取在NDC空间内距离camera最近的那个点的z轴坐标值(也就是深度值)。而我们要做的就是重新构建位于camera坐标系内的坐标点p=(p(x),p(y),p(z))。代码实现如下所示。由于向量v将穿过点p,因此存在t使p=tv。此外,p(z)=t·v(z)t=p(z)/v(z)。因此,p=p(z)/v(z)·v

// HLSL
float NdcDepthToViewDepth(float z_ndc)
{
    // 我们可以将NDC空间内的z轴坐标转换为camera空间内的z轴坐标
    // z_ndc = A + B/viewZ,其中gProj[2,2]=A,gProj[3,2]=B
    float viewZ = gProj[3][2] / (z_ndc - gProj[2][2]);

    return viewZ;
}

float4 PS(VertexOut pin) : SV_Target
{
    // 从depth buffer中读取像素位于NDC空间内的深度值也就是z轴坐标
    float pz = gDepthMap.SmapleLevel(gsamDepthMap, pin.TexC, 0.f).r;

    // 将深度值转换至camera空间内的z轴坐标
    pz = NdcDepthToViewDepth(pz);

    // 基于近平面坐标构建其位于camera空间内的坐标位置
    float3 p = (pz / pin.PosV.z) * pin.PosV;

    ...
}

21.2.2.2 生成随机采样

这个步骤与随机射线检测较为相似。我们将在p点“前方”且遮蔽半径之内随机采样N个点q。遮蔽半径是一个由美术进行调整的参数,其决定我们的随机采样点距点p的距离。

接下去我们就要解决如何生成随机采样点的问题。我们可以生成随机向量并将其存储在贴图中,之后采样这张贴图中的N个不同texel,以得到N个随机向量。由于这些向量都是随机的,我们并不能保证这些向量是平均分布的——也就是说,它们可能簇拥在一起(得到的遮蔽率可能是错误的)。为了克服这一点,我们在c++代码中生成了平均分布的14个向量:

void Ssao::BuildOffsetVectors()
{
    // 立方体的8个顶点
    mOffsets[0] = XMFLOAT4(1.f, 1.f, 1.f, 0.f);
    mOffsets[1] = XMFLOAT4(-1.f, -1.f, -1.f, 0.f);
    mOffsets[2] = XMFLOAT4(-1.f, 1.f, 1.f, 0.f);
    mOffsets[3] = XMFLOAT4(1.f, -1.f, -1.f, 0.f);
    mOffsets[4] = XMFLOAT4(1.f, 1.f, -1.f, 0.f);
    mOffsets[5] = XMFLOAT4(-1.f, -1.f, 1.f, 0.f);
    mOffsets[6] = XMFLOAT4(-1.f, 1.f, -1.f, 0.f);
    mOffsets[7] = XMFLOAT4(1.f, -1.f, 1.f, 0.f);

    // 立方体6个面的中心点
    mOffsets[8] = XMFLOAT4(-1.f, 0.f, 0.f, 0.f);
    mOffsets[9] = XMFLOAT4(1.f, 0.f, 0.f, 0.f);
    mOffsets[10] = XMFLOAT4(0.f, -1.f, 0.f, 0.f);
    mOffsets[11] = XMFLOAT4(0.f, 1.f, 0.f, 0.f);
    mOffsets[12] = XMFLOAT4(0.f, 0.f, -1.f, 0.f);
    mOffsets[13] = XMFLOAT4(0.f, 0.f, 1.f, 0.f);

    for(int i = 0; i < 14; ++i)
    {
        // 创建随机长度范围[0.25, 1]
        float s = MathHelper::RandF(0.25f, 1.f);

        XMVECTOR v = s * XMVector4Normalize(XMLoadFloat4(&mOffsets[i]));
        XMStoreFloat4(&mOffsets[i], v);
    }
}

我们使用4D齐次向量,这样就不需要担心向量对对齐问题了。

现在,在像素着色器中,我们只需要对随机向量贴图采样一次,拿到14个均匀分布的随机向量即可。

21.2.2.3 生成潜在的遮蔽点

现在我们已经随机采样了围绕着坐标点p的点。但是,我们还对这些点的具体信息一无所知——它们究竟是空白的空间还是场景中不透明的物体;因此,我们现在还不能判断这些点是否遮盖了坐标点p。为了找到潜在的遮蔽点,我们需要从depth buffer中获取深度信息。所以我们需要为每一个随机采样点生成其基于camera的投影贴图坐标,并使用该坐标对depth buffer进行采样以获取这些点在NDC空间的深度值,将深度值转换为camera空间内的z轴坐标r(z)后,我们就知道了由camera原点指向采样点的射线上距离camera最近的点的z轴坐标。有了z轴坐标r(z)之后,我们可以按照21.2.2.1小节中的方法,重新构建位于camera空间的坐标点r。因为由camera原点指向坐标q的射线穿过坐标点r,所以存在t,使r=tq。此外,r(z)=t·q(z)。因此,r=r(z)/q(z)·q。以此求出的坐标点r就是基于随机采样点q的潜在遮蔽点。

21.2.2.4 进行遮蔽测试

现在我们拥有了坐标点p的潜在遮蔽点r,因此可以检测该坐标点是否遮蔽点p。检测则基于以下两个变量:

1. camera空间内的深度距离|p(z)r(z)|。如果坐标点距离越远,那么遮蔽的效果越小,所以随着深度距离增加,我们将线性降低遮蔽程度。如果距离超过一定限度,那么就不会发生遮蔽。同时,如果距离非常小,那么意味着点p与点q位于同一平面,那么q也不能遮蔽p

2. 向量n与向量rp的夹角为max(n·normalize(rp),0)。这是为了防止坐标点qr的重合。

上图中,如果坐标点r与q位于同一平面,那么其仍然能通过第一个检测(两个点的深度值仍然是有距离的)。然而,上图显示,坐标点r与p位于同一平面,所以r是无法遮盖p的。而max(n·normalize(rp),0)避免了上述情况。

21.2.2.5 完成计算

当我们得到每个采样点的遮蔽率之后,我们将总和除以采样点的数量得到一个平均值。之后,我再求出ambient-access并且对其开方以增加环境光的对比度。而我们也可以增加环境光贴图的强度以增加环境光的亮度。

// HLSL
occlusionSum /= gSampleCount;
float access = 1.f - occlusionSum;

// 增强SSAO贴图的对比度让SSAO效果更明显
return saturate(pow(access, 4.f));

21.2.2.6 实现

之前的几个小节列举出了生成SSAO贴图的几个关键部分。以下为完整HLSL代码:

// HLSL
cbuffer cbSsao : register(b0)
{
    float4x4 gProj;
    float4x4 gInvProj;
    float4x4 gProjTex;
    float4 gOffsetVectors[14];

    // 用于SsaoBlur.hlsl
    float4 gBlurWeights[3];
    float2 gInvRenderTargetSize;

    float gOcclusionRadius;
    float gOcclusionFadeStart;
    float gOcclusionFadeEnd;
    float gSurfaceEpsilon;
}

cbuffer cbRootConstants : register(b1)
{
    bool gHorizontalBlur;
};

Texture2D gNormalMap : register(t0);
Texture2D gDepthMap : register(t1);
Texture2D gRandomVecMap : register(t2);

SamplerState gsamPointClamp : register(s0);
SamplerState gsamLinearClamp : register(s1);
SamplerState gsamDepthMap : register(s2);
SamplerState gsamLinearWrap : register(s3);

static const int gSampleCount = 14;
static const float2 gTexCoords[6] = 
{
    float2(0.f, 1.f);
    float2(0.f, 0.f);
    float2(1.f, 0.f);
    float2(0.f, 1.f);
    float2(1.f, 0.f);
    float2(1.f, 0.f);
    float2(1.f, 1.f);
};

struct VertexOut
{
    float4 PosH : SV_POSITION;
    float3 PosV : POSITION;
    float2 TexC : TEXCOORD0;
};

VertexOut VS(uint vid : SV_VertexID)
{
    VertexOut vout;
    vout.TexC = gTexCoords[vid];

    // 转换为在NDC空间内覆盖屏幕的点
    vout.PosH = float4(2.f * vout.TexC.x - 1.f, 1.f - 2.f * vout.TexC.y, 0.f, 1.f);

    // 将顶点转换至camear空间视锥体的近平面
    float4 ph = mul(vout.PosH, gInvProj);
    vout.PosV = ph.xyz / ph.w;

    return vout;
}

// 基于z轴坐标,计算点q对于点p的遮蔽率
float OcclusionFunction(float distZ)
{
    // 如果depth(q)>depth(p),那么说明点q位于点p“后方”。
    // 那么点q无法遮住点p。
    // 此外,如果depth(q)≈depth(p),那么我们也认为点q无法遮住点p。
    
    float occlusion = 0.f;
    if(distZ > gSurfaceEpsilon)
    {
        float fadeLength = gOcclusionFadeEnd - gOcclusionFadeStart;
        occlusion = saturate((gOcclusionFadeEnd - distZ) / fadeLength)        ;
    }

    return occlusion;
}

float NdcDepthToViewDepth(float z_ndc)
{
    // z_ndc = A + B/viewZ,其中gProj[2,2]=A,gProj[3,2]=B
    float viewZ = gProj[3][2] / (z_ndc - gProj[2][2]);
    return viewZ;
}

float4 PS(VertexOut pin) : SV_Target
{
    // p -- 我们正在处理的像素点
    // n -- p的法线向量
    // q -- 随机采样的点
    // r -- 潜在的可能遮蔽p的点

    // 得到该点位于camera空间的法线向量,以及该点的z轴坐标
    float3 n = gNormalMap.SampleLevel(gsamPointClamp, pin.TexC, 0.f).xyz;
    float pz = gDepthMap.SampleLevel(gsamDepthMap, pin.TexC, 0.f).r;
    pz = NdcDepthToViewDepth(pz);

    // 重新构建位于camera坐标系的点p(x,y,z)
    // 求出t,使得p=t*pin.PosV
    // p.z = t*pin.PosV.z
    // t = p.z / pin.PosV.z
    float3 p = (pz / pin.PosV.z) * pin.PosV;

    // 读取随机向量并将范围[0,1]映射至[-1,1]
    float3 randVec = 2.f * gRandomVecMap.SampleLevel(
        gsamLinearWrap, 4.f * pin.TexC, 0.f).rgb - 1.f;

    float occlusionSum = 0.f;

    // 对p周围,且法线方向周边的点进行随机采样
    for(int i = 0; i < gSampleCount; ++i) 
    { 
        // 所有的平移向量的方向都是平均分布的。 
        float3 offset = reflect(gOffsetVectors[i].xyz, randVec); 

        // 如果随机后的平移向量指向法线的反向,那么将其翻转 
        float flip = sign(dot(offset, n)); 

        // 在p点周围,遮蔽半径之内采样一个点 
        float3 q = p + flip * gOcclusionRadius * offset; 

        // 将q投影至贴图空间并求出投影贴图坐标 
        float4 projQ = mul(float4(q, 1.f), gProjTex); 
        projQ /= projQ.w; 

        // 找出由camera指向q的射线上,距离camera最近的点的深度值。 
        float rz = gDepthMap.SampleLevel(gsamDepthMap, projQ.xy, 0.f).r; 
        rz = NdcDepthToViewDepth(rz); 

        // 重新构建位于camera坐标系的点r(x,y,z) 
        // 求出t,使得r=t*q 
        // r.z=t*q.z ==> t=r.z/q.z
        float3 r = (rz / q.z) * q;

        // 检测r是否遮住p
        // 1. dot(n,normalize(r-p))将计算出r点是否位于p所在平面的正前方。
        // r越是正前方那么其遮蔽的权重越大。
        // 同时这也防止了r与p位于同一平面但还是遮住了p,这一错误。
        // 2. 遮蔽率的权重也会基于r到p的距离。
        // 如果r距离p非常远,那么其不会遮住p。
        float distZ = p.z - r.z;
        float dp = max(dot(n, normalize(r - p)), 0.f);
        float occlusion = dp * OcclusionFunction(distZ);
        
        occlusionSum += occlusion;
    }

    occlusionSum /= gSampleCount;
    float access = 1.f - occlusionSum;

    // 增加对比度,让SSAO的效果更为明显
    return saturate(pow(access, 2.f));
}

对于那些视距非常远的场景,由于depth buffer的精度有限,使用上述方法生成环境光遮蔽的效果可能会导致渲染错误。一个简单的解决办法是基于到camera的距离,对SSAO的效应进行弱化处理。

21.2.3 模糊处理的渲染通道

上图展示了我们的环境光遮蔽贴图。由于随机采样点的数量有限,上图中有明显的噪点。但是对于实时渲染的应用来说,使用过多的采样点是不现实的。常见的解决办法是使用保持边缘的模糊处理(例如,双向模糊处理),这可以使SSAO贴图更为平滑。如果我们使用一个不保持边缘的模糊处理,那么我们会丢失SSAO贴图中的对比度。保持边缘的模糊处理类似于我们在第十三章中的demo,但是我们会加入一个额外的条件判断,这样就能避免在三角形的边缘处进行模糊处理(我们将检测法线贴图与深度贴图以判断该像素是否处于边缘):

// HLSL
// 对环境光遮蔽贴图进行双边保持边缘的模糊处理。
// 我们使用像素着色器而不是compute shader,
// 这样能够避免在计算模式和渲染模式间的切换。
// 由于没有compute shader的共享内存,所以在缓存贴图时会有性能损耗。
// 环境光遮蔽贴图是一张16位格式的贴图,其不会占用太多空间,
// 因此我们缓存较多texel。
cbuffer cbSsao : register(b0)
{
    float4x4 gProj;
    float4x4 gInvProj;
    float4x4 gProjTex;
    float4 gOffsetVectors[14];

    // 用于SsaoBlur.hlsl
    float4 gBlurWeights[3];
    float2 gInvRenderTargetSize;

    float gOcclusionRadius;
    float gOcclusionFadeStart;
    float gOcclusionFadeEnd;
    float gSurfaceEpsilon;
};

cbuffer cbRootConstants : register(b1)
{
    bool gHorizontalBlur;
};

Texture2D gNormalMap : register(t0);
Texture2D gDepthMap : register(t1);
Texture2D gInputMap : register(t2);

SamplerState gsamPointClamp : register(s0);
SamplerState gsamLinearClamp : register(s1);
SamplerState gsamDepthMap : register(s2);
SamplerState gsamLinearWrap : register(s3);

static const int gBlurRadius = 5;
static const float2 gTexCoords[6] =
{
    float2(0.f, 1.f),
    float2(0.f, 0.f),
    float2(1.f, 0.f),
    float2(0.f, 1.f),
    float2(1.f, 0.f),
    float2(1.f, 1.f)
};

struct VertexOut
{
    float4 PosH : SV_POSITION;
    float2 TexC : TEXCOORD;
};

VertexOut VS(uint vid : SV_VertexID)
{
    VertexOut vout;
    vout.TexC = gTexCoords[vid];

    // 在NDC空间中覆盖屏幕的面片
    vout.PosH = float4(2.f * vout.TexC.x - 1.f, 1.f - 2.f * vout.TexC.y, 0.f, 1.f);

    return vout;
}

float2 NdcDepthToViewDepth(float z_ndc)
{
    float viewZ = gProj[3][2] / (z_ndc - gProj[2][2]);
    return viewZ;
}

float4 PS(VertexOut pin) : SV_Target
{
    // 组成权重数组
    float blurWeights[12] =
    {
        gBlurWeights[0].x, gBlurWeights[0].y, gBlurWeights[0].z, gBlurWeights[0].w,
        gBlurWeights[1].x, gBlurWeights[1].y, gBlurWeights[1].z, gBlurWeights[1].w,
        gBlurWeights[2].x, gBlurWeights[2].y, gBlurWeights[2].z, gBlurWeights[2].w
    };

    float2 texOffset;
    if(gHorizontalBlur)
    {
        texOffset = float2(gInvRenderTargetSize.x, 0.f);
    }
    else
    {
        texOffset = float2(0.f, gInvRenderTargetSize.y);
    }

    // 永远记录位于中间像素的颜色值
    float4 color = blurWeights[gBlurRadius] * gInputMap.SampleLevel(
        gsamPointClamp, pin.TexC, 0.f);
    float totalWeight = blurWeights[gBlurRadius];

    float3 centerNormal = gNormalMap.SampleLevel(gsamPointClamp, pin.TexC, 0.f).xyz;
    float centerDepth = NdcDepthToViewDepth(
        gDepthMap.SampleLevel(gsamDepthMap, pin.TexC, 0.f).r);

    for(float i = -gBlurRadius; i <= gBlurRadius; ++i) 
    { 
        // 位于中间的值已经被记录 
        if(i == 0) 
            continue; 
        
        float2 tex = pin.TexC + i * texOffset; 
        float3 neighborNormal = gNormalMap.SampleLevel(gsamPointClamp, tex, 0.f).xyz; 
        float neighborDepth = NdcDepthToViewDepth( 
            gDepthMap.SampleLevel(gsamDepthMap, tex, -.f).r); 

        // 如果原始像素与周围的像素差异过大(法线与深度值), 
        // 那么我们认为两个像素之间是不连接的(例如,一个像素位于物体A,另一个位于物体B)。 
        // 同时将该像素从blur操作中舍弃 
        if(dot(neighborNormal, centerNormal) >= 0.8 &&
            abs(neighborDepth - centerDepth) <= 2.f)
        {
            float weight = blurWeights[i + gBlurRadius];

            // 将该像素加入blur操作
            color += weight * gInputMap.SampleLevel(
                gsamPointClamp, tex, 0.0);
            totalWeight += weight;
        }
    }

    // 保证我们进行的是加权平均
    return color / totalWeight;
}

下图展示了结果blur之后的环境光遮蔽贴图。

21.2.4 使用环境光遮蔽贴图

到现在为止,我们已经成功构建了环境光遮蔽贴图。最后需要做的就是将其运用于场景。大家可能认为我们应该使用α融合,基于环境光遮蔽贴图来修正back buffer。但是,如果我们这么做了,环境光遮蔽贴图不但改变了环境光计算模型还改变了漫反射与镜面反射的计算模型,这当然是不对的。相反,当我们将场景绘制到back buffer时,我们将环境光遮蔽贴图绑定为着色器的输入资源。之后,我们创建基于camera的投影贴图坐标,对SSAO贴图进行采样,只将环境光遮蔽贴图应用于环境光的计算模型:

// HLSL
// 在顶点着色器中,生成投影贴图坐标
vout.SsaoPosH = mul(posW, gViewProjTex);

// 在像素着色器中,完成贴图投影并且采样SSAO贴图
pin.SsaoPosH /= pin.SsaoPosH.w;
float ambientAccess = gSsaoMap.Sample(gsamLinearClamp, pin.SsaoPosH.xy, 0.f).r;

// 改变环境光
float4 ambient = ambientAccess * gAmbientLight * diffuseAlbedo;

下图展示本章节demo的截图。我们可以让SSAO的精度更高,但是我们的场景中的物体也需要反射更多的环境光,这样ambient-access才能让玩家注意到环境光在某些位置的不同。SSAO的效果在物体处于阴影中时才最明显。因为在阴影中时,物体不会有漫反射与镜面反射光线,只有环境光。如果没有SSAO,那么位于阴影中的物体,其每个部位的光照效果都是相同的,但是使用SSAO之后,我们能看到物体的外形与周边环境对光照效果的影响。

当我们绘制场景中的物体位于camera空间的法线向量时,我们也构建了场景的depth buffer。因此,当我们第二次使用SSAO作为着色器输入资源绘制场景时,我们将深度值的比较方法改成了“EQUAL”。这防止我们在第二次渲染通道中重复绘制像素,此外,在第二次渲染通道中,我们不需要再一次写入depth buffer,因为我们已经在绘制场景的法线向量时已经记录了depth buffer。

opaquePsoDesc.DepthStencilState.DepthFunc = D3D12_COMPARISON_FUNC_EQUAL;
opaquePsoDesc.DepthStencilState.DepthWriteMask = D3D12_DEPTH_WRITE_MASK_ZERO;
ThrowIfFailed(md3dDevice->CreateGraphicsPipelineState(
    &opaquePsoDesc, IID_PPV_ARGS(&mPSOs["opaque"])));

留下评论

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据