22-环境光遮蔽

由于性能的限制,实时照明模型通常不考虑间接光(即从场景中的其他物体反射回来的光)。但是,我们在现实世界中看到的许多光是间接的。在第7章中,我们将环境项引入了照明方程:

A=lama A = l a ⊗ m a

颜色 la l a 指定表面从光源接收的间接(环境)光的总量。环境材料颜色 ma m a 指定表面反射和吸收的入射环境光的量。所有的环境光线都会使物体均匀光亮 - 根本没有真正的物理计算。 这个想法是,间接光在场景周围散射和反射很多次,以致它在各个方向均匀地击中物体。 图22.1表明,如果我们只使用环境项绘制一个模型,它将以常量颜色呈现出来。


图22.1 仅使用环境术语渲染的网格显示为纯色

图22.1清楚地表明我们的环境条件可以使用一些改进。 在本章中,我们将讨论常用的环境遮挡技术,以改善环境条件。
目标:
1.了解背景遮挡背后的基本思想以及如何通过光线投射实现环境遮挡。
2.了解如何在称为屏幕空间环境遮挡的屏幕空间中实现环境遮挡的实时近似。

22.1通过射线铸造造成环境污染

环境遮挡的想法是,表面上的点p的间接光的量与半球上入射光的遮挡程度成比例关系,见图22.2。


图22.2 (a)点p完全未被遮挡,且大约半球上的所有入射光达到p。(b)几何体部分遮挡p并阻挡关于p的半球上的入射光线。

估计点p遮挡的一种方法是通过射线投射。 我们随机在半球上投射光线,并检查与网格的交点(图22.3)。 如果我们投射N条射线,并且它们中的h与网格相交,则该点具有遮挡值:

图22.3 通过光线投射估算环境遮挡

occlussion=hN[0,1] o c c l u s s i o n = h N ∈ [ 0 , 1 ]

只有具有与p的距离小于某个阈值d的交点q的射线才有助于遮挡估计; 这是因为远离p的交点q太远而无法将其封闭。

遮挡因子测量该点的遮挡程度(即它没有接收多少光)。 为了计算的目的,我们喜欢使用这个反函数。 也就是说,我们想要知道点接收到多少光 - 这称为可访问性(或者我们称之为环境访问)并且从遮挡导出为:

accessiblity=1occlusion[0,1] a c c e s s i b l i t y = 1 − o c c l u s i o n ∈ [ 0 , 1 ]

“Ambient Occlusion”演示会执行每个三角形的光线投射,然后用共享三角形的顶点对遮挡结果进行平均。 射线的原点是三角形的质心,我们在三角形的半球上产生一个随机的射线方向。

void AmbientOcclusionApp::BuildVertexAmbientOcclusion(
std::vector<Vertex::AmbientOcclusion>& vertices,
const std::vector<UINT>& indices)
{
UINT vcount = vertices.size();
UINT tcount = indices.size()/3;
std::vector<XMFLOAT3> positions(vcount);
for(UINT i = 0; i < vcount; ++i)
positions[i] = vertices[i].Pos;
Octree octree;
octree.Build(positions, indices);
// For each vertex, count how many triangles contain the vertex.
std::vector<int> vertexSharedCount(vcount);
// Cast rays for each triangle, and average triangle occlusion
0// with the vertices that share this triangle.
for(UINT i = 0; i < tcount; ++i)
{
UINT i0 = indices[i*3+0];
UINT i1 = indices[i*3+1];
UINT i2 = indices[i*3+2];
XMVECTOR v0 = XMLoadFloat3(&vertices[i0].Pos);
XMVECTOR v1 = XMLoadFloat3(&vertices[i1].Pos);
XMVECTOR v2 = XMLoadFloat3(&vertices[i2].Pos);
XMVECTOR edge0 = v1 - v0;
XMVECTOR edge1 = v2 - v0;
XMVECTOR normal = XMVector3Normalize(
XMVector3Cross(edge0, edge1));
XMVECTOR centroid = (v0 + v1 + v2)/3.0f;
// Offset to avoid self intersection.
centroid += 0.001f*normal;
const int NumSampleRays = 32;
float numUnoccluded = 0;
for(int j = 0; j < NumSampleRays; ++j)
{
XMVECTOR randomDir = MathHelper::RandHemisphereUnitVec3(normal);
// Test if the random ray intersects the scene mesh.
//
// TODO: Technically we should not count intersections
// that are far away as occluding the triangle, but
// this is OK for demo.
if(!octree.RayOctreeIntersect(centroid, randomDir))
{
numUnoccluded++;
}
}
float ambientAccess = numUnoccluded / NumSampleRays;
// Average with vertices that share this face.
vertices[i0].AmbientAccess += ambientAccess;
vertices[i1].AmbientAccess += ambientAccess;
vertices[i2].AmbientAccess += ambientAccess;
vertexSharedCount[i0]++;
vertexSharedCount[i1]++;
vertexSharedCount[i2]++;
}
// Finish average by dividing by the number of samples we added,
// and store in the vertex attribute.
for(UINT i = 0; i < vcount; ++i)
{
vertices[i].AmbientAccess /= vertexSharedCount[i];
}
}

我们不打算详细介绍“Ambient Occlusion”演示,因为我们真正需要的环境遮挡技术将在下一节讨论。然而,我们将提到的演示有两点:
1.该演示使用八叉树来加速射线/三角形相交测试。对于包含数千个三角形的网格,使用每个网格三角形测试每个随机射线的速度会很慢。八叉树在空间上对三角形进行排序,因此我们可以快速找到只有与光线相交的三角形;这大大减少了射线/三角形相交测试的次数。八叉树是一种经典的空间数据结构,练习1要求您进一步研究它们。
2.演示使用在§15.2.1中首次提到的xnacollision.h / cpp实用程序库。这些文件位于Microsoft DirectX SDK(2010年6月)\ Samples \ C ++ \ Misc \ Collision。它们提供了快速的XNA数学实现,用于常见的几何图元相交测试,例如光线/三角形交叉,光线/箱子交叉,箱子/箱子交叉,箱子/平面交叉,箱子/平截头体,球体/平截头体等等。
图22.4显示了一个模型的屏幕截图,该模型仅使用前一算法生成的环境遮挡渲染(场景中没有光源)。 环境遮挡在初始化期间作为预计算步骤生成并存储为顶点属性。 正如我们所看到的,它比图22.1有了巨大的改进 - 模型现在看起来实际上是3D。


图22.4 网格仅使用环境遮挡渲染 - 没有场景光。 注意裂缝是如何变黑的; 这是因为当我们投射出光线时,它们更可能与几何相交并有助于遮挡。 另一方面,颅骨帽是白色的(未被遮挡),因为当我们在半球上投射光线以便在颅骨帽上的点时,它们将不会与颅骨的任何几何形状相交。

预计算环境遮挡适用于静态模型; 甚至还有可生成环境遮挡贴图的工具( http://www.xnormal.net) - 用于存储环境遮挡数据的贴图。 但是,对于动画模型而言,这些静态方法会失效。 如果您加载并运行“Ambient Occlusion”演示,您会注意到仅需一秒钟即可为一个模型预先计算环境遮挡。 因此,在运行时投射射线来实现动态环境遮挡是不可行的。 在下一节中,我们将研究一种使用屏幕空间信息实时计算环境遮挡的流行技术。

22.2屏幕空间环境堵塞

屏幕空间环境遮挡(SSAO)策略是,对于每一帧,将场景视图空间渲染为全屏幕渲染目标,然后仅使用该渲染目标作为输入来估计每个像素处的环境遮挡。

22.2.1渲染法线和深度通过

将场景渲染到屏幕大小的DXGI_FORMAT_R16G16B16A16_FLOAT普通/深度纹理贴图,其中RGB存储视图空间法线,alpha通道存储视图空间深度(z坐标)。 用于此传递的顶点/像素着色器如下所示:

VertexOut VS(VertexIn vin)
{
VertexOut vout;
// Transform to view space.
vout.PosV = mul(float4(vin.PosL, 1.0f), gWorldView).xyz;
vout.NormalV = mul(vin.NormalL, (float3x3)gWorldInvTransposeView);
// Transform to homogeneous clip space.
vout.PosH = mul(float4(vin.PosL, 1.0f), gWorldViewProj);
// Output vertex attributes for interpolation across triangle.
vout.Tex = mul(float4(vin.Tex, 0.0f, 1.0f), gTexTransform).xy;
return vout;
}
float4 PS(VertexOut pin, uniform bool gAlphaClip) : SV_Target
{
// Interpolating normal can unnormalize it, so normalize it.
pin.NormalV = normalize(pin.NormalV);
if(gAlphaClip)
{
float4 texColor = gDiffuseMap.Sample(samLinear, pin.Tex);
clip(texColor.a - 0.1f);
}
return float4(pin.NormalV, pin.PosV.z);
}

顶点着色器只是将法向矢量和位置转换为视图空间。然后,视图空间法线和视图空间z坐标被写入像素着色器中的渲染目标。观察到我们正在写入浮点渲染目标,所以写出任意浮点数据没有问题。

22.2.2环境遮挡通道

在渲染目标的视图空间法线和深度之后,我们禁用深度缓冲区(我们不需要它来生成环境遮挡纹理),并绘制全屏幕四边形以在每个像素处调用SSAO像素着色器。像素着色器然后将使用正常/深度缓冲区来在每个像素处生成环境可访问性值。我们在这个过程中将生成的纹理贴图称为SSAO贴图。尽管我们以全屏幕分辨率(即,我们的后台缓冲区的分辨率)渲染正常/深度图,但出于性能原因,我们渲染到后台缓冲区宽度和高度一半的SSAO地图。一半尺寸的渲染不会影响质量,因为环境遮挡是一种低频效应。在以下小节中参考图22.5。


图22.5 涉及SSAO的要点。点p对应于我们正在处理的当前像素,并且它根据存储在法线/深度图和内插的远 - 远平面矢量v中的深度值重建。点q是p半球中的随机点。 点r对应于沿着从眼睛到q的射线的最近可见点。 如果| pz - rz |,点r有助于p的遮挡 足够小并且r-p和n之间的角度小于90°。 在演示中,我们采用14个随机采样点并平均来自每个采样点的遮挡以估计屏幕空间中的环境遮挡。

22.2.2.1重建视图空间位置

当我们绘制全屏幕四边形以在SSAO映射的每个像素处调用SSAO像素着色器时,我们在四个四边形顶点中的每一个处存储指向远平面平截头体角的索引。 我们在顶点着色器中获取平截头体角矢量,并将其传递给像素着色器。 对这些远平面矢量进行插值并为每个像素提供从眼睛到远平面的矢量v。

void Ssao::BuildFrustumFarCorners(float fovy, float farZ)
{
float aspect = (float)mRenderTargetWidth / (float)mRenderTargetHeight;
float halfHeight = farZ * tanf(0.5f*fovy);
float halfWidth = aspect * halfHeight;
mFrustumFarCorner[0] = XMFLOAT4(-halfWidth, -halfHeight, farZ, 0.0f);
mFrustumFarCorner[1] = XMFLOAT4(-halfWidth, +halfHeight, farZ, 0.0f);
mFrustumFarCorner[2] = XMFLOAT4(+halfWidth, +halfHeight, farZ, 0.0f);
mFrustumFarCorner[3] = XMFLOAT4(+halfWidth, -halfHeight, farZ, 0.0f);
}
cbuffer cbPerFrame
{
float4 gFrustumCorners[4];
...
};
VertexOut VS(VertexIn vin)
{
VertexOut vout;
// Already in NDC space.
vout.PosH = float4(vin.PosL, 1.0f);
// We store the index to the frustum corner in the normal x-coord slot.
vout.ToFarPlane = gFrustumCorners[vin.ToFarPlaneIndex.x].xyz;
// Pass onto pixel shader.
vout.Tex = vin.Tex;
return vout;
}

现在,对于每个像素,我们对正常/深度图进行采样,以便我们将视图空间法线n和最近可见点的z坐标 pz p z 到眼睛。 目标是从采样视图空间z坐标pz和内插的远 - 远平面矢量v重建视图空间位置p =(px,py,pz)。重建过程如下。 因为远 - 远平面矢量v通过p,所以存在使得p = t v的t。特别地,pz = tvz使得t = pz / vz。 从而有 p=pzvzv p = p z v z v

22.2.2.2生成随机样本

这一步类似于在半球上投射的随机光线。我们随机采样N点q关于p也是在p的前面并且在指定的咬合半径内。遮挡半径是一个艺术参数,用于控制距离我们想要采取随机采样点的距离。选择仅在p前采样点类似于在进行射线环境遮挡时仅在半球上投射光线而不是整个球体。

接下来的问题是如何生成随机样本。我们可以生成随机矢量并将它们存储在纹理贴图中,然后在N个不同位置对该纹理贴图进行采样,以获得N个随机矢量。然而,因为它们是随机的,我们不能保证我们采样的矢量将是均匀分布的 - 它们可能都以大致相同的方向聚集在一起。为了克服这一点,我们做了以下诀窍。在我们的实现中,我们使用N = 14个样本,并且我们在C ++代码中生成14个均匀分布的向量:

void Ssao::BuildOffsetVectors()
{
// Start with 14 uniformly distributed vectors. We choose the
// 8 corners of the cube and the 6 center points along each
// cube face. We always alternate the points on opposite sides
// of the cubes. This way we still get the vectors spread out
// even if we choose to use less than 14 samples.
// 8 cube corners
mOffsets[0] = XMFLOAT4(+1.0f, +1.0f, +1.0f, 0.0f);
mOffsets[1] = XMFLOAT4(-1.0f, -1.0f, -1.0f, 0.0f);
mOffsets[2] = XMFLOAT4(-1.0f, +1.0f, +1.0f, 0.0f);
mOffsets[3] = XMFLOAT4(+1.0f, -1.0f, -1.0f, 0.0f);
mOffsets[4] = XMFLOAT4(+1.0f, +1.0f, -1.0f, 0.0f);
mOffsets[5] = XMFLOAT4(-1.0f, -1.0f, +1.0f, 0.0f);
mOffsets[6] = XMFLOAT4(-1.0f, +1.0f, -1.0f, 0.0f);
mOffsets[7] = XMFLOAT4(+1.0f, -1.0f, +1.0f, 0.0f);
// 6 centers of cube faces
mOffsets[8] = XMFLOAT4(-1.0f, 0.0f, 0.0f, 0.0f);
mOffsets[9] = XMFLOAT4(+1.0f, 0.0f, 0.0f, 0.0f);
mOffsets[10] = XMFLOAT4(0.0f, -1.0f, 0.0f, 0.0f);
mOffsets[11] = XMFLOAT4(0.0f, +1.0f, 0.0f, 0.0f);
mOffsets[12] = XMFLOAT4(0.0f, 0.0f, -1.0f, 0.0f);
mOffsets[13] = XMFLOAT4(0.0f, 0.0f, +1.0f, 0.0f);
for(int i = 0; i < 14; ++i)
{
// Create random lengths in [0.25, 1.0].
float s = MathHelper::RandF(0.25f, 1.0f);
XMVECTOR v = s * XMVector4Normalize(XMLoadFloat4(&mOffsets[i]));
XMStoreFloat4(&mOffsets[i], v);
}
}

我们使用4D均匀矢量,所以在设置偏移矢量阵列时,我们不必担心任何对齐问题。

现在,在像素着色器中,我们只对一次随机矢量纹理图进行采样,并用它来反映我们的14个均匀分布的矢量。 这导致14个均匀分布的随机向量。

22.2.2.3生成潜在的遮挡点

我们现在有围绕p的随机采样点q。但是,我们对它们一无所知-它们是否有体积;因此,我们不能用它们来测试它们是否遮挡了p。为了找到潜在的遮挡点,我们需要来自正常/深度图的深度信息。因此,我们所做的是为每个q相对于相机生成投影纹理坐标,并使用它们来对法线/深度图进行采样,以获得沿着从眼睛到q的射线的最近可见像素的深度 rz r z 。在已知z坐标 rz r z 的情况下,我们可以像在§22.2.2.1中那样以类似的方式重建完整的3D视图空间位置r。因为从眼睛到q的矢量经过r,所以存在t使得r=tq。特别是, rz=tqz r z = t q z 所以 t=rz/qz t = r z / q z 。因此, r=rzqzq r = r z q z q 对于每个随机采样点q产生的点r是我们的潜在遮挡点。

22.2.2.4执行阻塞测试

现在我们有了我们的潜在遮挡点r,我们可以执行遮挡测试来估计它们是否遮挡了p。该测试依赖于两个数量:
1.视图空间深度距离 |pzrz| | p z − r z | 。随着距离的增加,我们线性缩小遮挡,因为距离较远的点的遮挡效应较小。 如果距离超过某个指定的最大距离,则不会发生遮挡。 另外,如果距离非常小,那么我们假设p和q在同一平面上,所以q不能遮挡p。
2. n和 r-p 之间的角度由 max(n(rp||rp||),0) m a x ( n · ( r − p | | r − p | | ) , 0 ) 测量。这是为了防止自相交(见图22.6)。


图22.6。 如果r与p位于同一平面上,它可以通过距离| pz - rz |的第一个条件 足够小以至于包含p。 但是,该图显示这是不正确的,因为r不会遮挡p,因为它们位于同一平面上。通过缩放 max(n(rp||rp||),0) m a x ( n · ( r − p | | r − p | | ) , 0 ) 遮挡来防止这种情况。

22.2.2.5 完成计算

在我们总结每个样本的遮挡后,我们通过除以样本计数来计算平均遮挡。 然后我们计算环境访问权限,最后提高环境访问权限以增加对比度。 您也可以通过添加一些数字来增加强度来增加环境贴图的亮度。您可以尝试不同的对比度/亮度值。

occlusionSum /= gSampleCount;
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));
22.2.2.6 实施

上一节概述了生成SSAO地图的关键要素。 以下是完整的效果代码:

//=====================================================================
// Ssao.fx by Frank Luna (C) 2011 All Rights Reserved.
//
// Computes SSAO map.
//=====================================================================
cbuffer cbPerFrame
{
float4x4 gViewToTexSpace; // Proj*Texture
float4 gOffsetVectors[14];
float4 gFrustumCorners[4];
// Coordinates given in view space.
float gOcclusionRadius = 0.5f;
float gOcclusionFadeStart = 0.2f;
float gOcclusionFadeEnd = 2.0f;
float gSurfaceEpsilon = 0.05f;
};
// Nonnumeric values cannot be added to a cbuffer.
Texture2D gNormalDepthMap;
Texture2D gRandomVecMap;
SamplerState samNormalDepth
{
Filter = MIN_MAG_LINEAR_MIP_POINT;
// Set a very far depth value if sampling outside of
// the NormalDepth map so we do not get false occlusions.
AddressU = BORDER;
AddressV = BORDER;
BorderColor = float4(0.0f, 0.0f, 0.0f, 1e5f);
};
SamplerState samRandomVec
{
Filter = MIN_MAG_LINEAR_MIP_POINT;
AddressU = WRAP;
AddressV = WRAP;
};
struct VertexIn
{
float3 PosL : POSITION;
float3 ToFarPlaneIndex : NORMAL;
float2 Tex : TEXCOORD;
};
struct VertexOut
{
float4 PosH : SV_POSITION;
float3 ToFarPlane : TEXCOORD0;
float2 Tex : TEXCOORD1;
};
VertexOut VS(VertexIn vin)
{
VertexOut vout;
// Already in NDC space.
vout.PosH = float4(vin.PosL, 1.0f);
// We store the index to the frustum corner in the normal x-coord slot.
vout.ToFarPlane = gFrustumCorners[vin.ToFarPlaneIndex.x].xyz;
// Pass onto pixel shader.
vout.Tex = vin.Tex;
return vout;
}
// Determines how much the sample point q occludes the point p as a function
// of distZ.
float OcclusionFunction(float distZ)
{/
/
// If depth(q) is "behind" depth(p), then q cannot occlude p.
// Moreover, if depth(q) and depth(p) are sufficiently close,
// then we also assume q cannot occlude p because q needs to
// be in front of p by Epsilon to occlude p.
//
// We use the following function to determine the occlusion.
//
//
//   1.0  -------------\
//        |           | \
//        |           |  \
//        |           |   \
//        |           |    \
//        |           |     \
//        |           |      \
//----|---|-----------|-------|--------|---> zv
//    0  Eps          z0      z1
//
float occlusion = 0.0f;
if(distZ > gSurfaceEpsilon)
{
float fadeLength = gOcclusionFadeEnd - gOcclusionFadeStart;
// Linearly decrease occlusion from 1 to 0 as distZ goes
// from gOcclusionFadeStart to gOcclusionFadeEnd.
occlusion = saturate((gOcclusionFadeEnd-distZ)/fadeLength);
}
return occlusion;
}
float4 PS(VertexOut pin, uniform int gSampleCount) : SV_Target
{
// p -- the point we are computing the ambient occlusion for.
// n -- normal vector at p.
// q -- a random offset from p.
// r -- a potential occluder that might occlude p.
// Get viewspace normal and z-coord of this pixel. The tex-coords for
// the fullscreen quad we drew are already in uv-space.
float4 normalDepth = gNormalDepthMap.SampleLevel(
samNormalDepth, pin.Tex, 0.0f);
float3 n = normalDepth.xyz;
float pz = normalDepth.w;
//
// Reconstruct full view space position (x,y,z).
// Find t such that p = t*pin.ToFarPlane.
// p.z = t*pin.ToFarPlane.z
// t = p.z / pin.ToFarPlane.z
//
float3 p = (pz/pin.ToFarPlane.z)*pin.ToFarPlane;
// 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;
// Sample neighboring points about p in the hemisphere
// oriented by n.
[unroll]
for(int i = 0; i < gSampleCount; ++i)
{
// Are offset vectors are fixed and uniformly
// distributed (so that our offset vectors
// do not clump in the same direction). If we
// reflect them about a random vector then we
// get a random uniform distribution of offset vectors.
float3 offset = reflect(gOffsetVectors[i].xyz, randVec);
// Flip offset vector if it is behind the plane
// defined by (p, n).
float flip = sign(dot(offset, n));
// Sample a point near p within the occlusion radius.
float3 q = p + flip * gOcclusionRadius * offset;
// Project q and generate projective tex-coords.
float4 projQ = mul(float4(q, 1.0f), gViewToTexSpace);
projQ /= projQ.w;
// Find the nearest depth value along the ray from
// the eye to q (this is not the depth of q, as q is
// just an arbitrary point near p and might occupy
// empty space). To find the nearest depth we look
// it up in the depthmap.
float rz = gNormalDepthMap.SampleLevel(
samNormalDepth, projQ.xy, 0.0f).a;
// Reconstruct full view space position r = (rx,ry,rz).
// We know r lies on the ray of q, so there exists a t
// such that r = t*q.
// r.z = t*q.z ==> t = r.z / q.z
float3 r = (rz / q.z) * q;
//
// Test whether r occludes p.
// * The product dot(n, normalize(r - p)) measures how
// much in front of the plane(p,n) the occluder point
// r is. The more in front it is, the more occlusion
// weight we give it. This also prevents self shadowing
// where a point r on an angled plane (p,n) could give a
// false occlusion since they have different depth values
// with respect to the eye.
// * The weight of the occlusion is scaled based on how
// far the occluder is from the point we are computing
// the occlusion of. If the occluder r is far away
// from p, then it does not occlude it.
//
float distZ = p.z - r.z;
float dp = max(dot(n, normalize(r - p)), 0.0f);
float occlusion = dp * OcclusionFunction(distZ);
occlusionSum += occlusion;
}
occlusionSum /= gSampleCount;
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));
}
technique11 Ssao
{
pass P0
{
SetVertexShader(CompileShader(vs_5_0, VS()));
SetGeometryShader(NULL);
SetPixelShader(CompileShader(ps_5_0, PS(14)));
}
}

22.2.3 模糊通行证

图22.7显示了我们的环境遮挡贴图当前的样子。 噪音是由于我们只采取了一些随机样本。 采取足够的样本来隐藏噪声对于实时是不切实际的。 常见的解决方案是将边缘保留模糊(即,双边模糊)应用于SSAO映射以使其平滑。 如果我们使用非边缘保留模糊,那么我们会失去场景中的清晰度,因为尖锐的不连续性会变得平滑。 边缘保留模糊与我们在第12章中实现的模糊相似,不同的是我们添加了条件语句,以便我们不会在边缘模糊(从正常/深度图中检测边缘):


图22.7 由于我们只采取了一些随机样本,SSAO显得很吵。

//=====================================================================
// SsaoBlur.fx by Frank Luna (C) 2011 All Rights Reserved.
//
// Performs a bilateral edge preserving blur of the ambient map. We use
// a pixel shader instead of compute shader to avoid the switch from
// compute mode to rendering mode. The texture cache makes up for some
// of the loss of not having shared memory. The ambient map uses
// 16-bit texture format, which is small, so we should be able to fit
// a lot of texels in the cache.
//=====================================================================
cbuffer cbPerFrame
{
float gTexelWidth;
float gTexelHeight;
};
cbuffer cbSettings
{
float gWeights[11] =
{
0.05f, 0.05f, 0.1f, 0.1f, 0.1f, 0.2f, 0.1f, 0.1f, 0.1f, 0.05f, 0.05f
};
};
cbuffer cbFixed
{
static const int gBlurRadius = 5;
};
// Nonnumeric values cannot be added to a cbuffer.
Texture2D gNormalDepthMap;
Texture2D gInputImage;
SamplerState samNormalDepth
{
Filter = MIN_MAG_LINEAR_MIP_POINT;
AddressU = CLAMP;
AddressV = CLAMP;
};
SamplerState samInputImage
{
Filter = MIN_MAG_LINEAR_MIP_POINT;
AddressU = CLAMP;
AddressV = CLAMP;
};
struct VertexIn
{
float3 PosL : POSITION;
float3 NormalL : NORMAL;
float2 Tex : TEXCOORD;
};
struct VertexOut
{
float4 PosH : SV_POSITION;
float2 Tex : TEXCOORD;
};
VertexOut VS(VertexIn vin)
{
VertexOut vout;
// Already in NDC space.
vout.PosH = float4(vin.PosL, 1.0f);
// Pass onto pixel shader.
vout.Tex = vin.Tex;
return vout;
}f
loat4 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;
}
technique11 HorzBlur
{
pass P0
{
SetVertexShader(CompileShader(vs_5_0, VS()));
SetGeometryShader(NULL);
SetPixelShader(CompileShader(ps_5_0, PS(true)));
}
}
technique11 VertBlur
{
pass P0
{
SetVertexShader(CompileShader(vs_5_0, VS()));
SetGeometryShader(NULL);
SetPixelShader(CompileShader(ps_5_0, PS(false)));
}
}

图22.8显示了边缘保留模糊后的环境图。


图22.8 边缘保留模糊平滑噪音。在我们的演示中,我们将图像模糊了4次。

22.2.4 使用Ambient Occlusion Map

迄今为止,我们已经构建了一个很好的环境遮挡贴图。 最后一步是将其应用到现场。 有人可能会认为使用alpha混合并使用后台缓冲区调制环境地图。 但是,如果我们这样做,那么环境贴图不仅会修改环境项,还会修改光照方程的漫反射和镜面反射项,这是不正确的。 相反,当我们将场景渲染到后台缓冲区时,我们将环境贴图绑定为着色器输入。 然后,我们生成投影纹理坐标(相对于相机),对SSAO图进行采样,并将其仅应用于照明方程的环境项:

// In Vertex shader, generate projective tex-coords
// to project SSAO map onto scene.
vout.SsaoPosH = mul(float4(vin.PosL, 1.0f), gWorldViewProjTex);
// In pixel shader, finish texture projection and sample SSAO map.
pin.SsaoPosH /= pin.SsaoPosH.w;
float ambientAccess = gSsaoMap.Sample(samLinear, pin.SsaoPosH.xy, 0.0f).r;
// Scale ambient term of lighting equation.
ambient += ambientAccess*A;
diffuse += shadow[i]*D;
spec += shadow[i]*S;

图22.9显示了应用了SSAO映射的场景。 SSAO可能很微妙,而且您的场景必须反射足够的环境光线,以便通过环境访问进行缩放会产生足够明显的差异。 如果你愿意,你可以考虑将漫射分量乘以环境访问,以使效果更加戏剧化,尽管这样做不太准确。


图22.9 演示的屏幕截图。效果很微妙,因为它们只影响环境条件,但是您可以在列和框的底部,球体下方以及头骨周围看到变暗。

当物体处于阴影中时,SSAO的优势最为明显。 当物体处于阴影中时,漫反射和镜面反射项被杀死; 因此只有环境期限出现。 如果没有SSAO,阴影中的物体将会以恒定的环境条件平稳亮起,但对于SSAO,它们将保持其3D定义。

当我们将场景渲染到正常/深度渲染目标时,我们还为场景构建深度缓冲区。 因此,当我们第二次使用SSAO贴图渲染场景时,我们将深度比较测试修改为“EQUALS”。这样可以防止第二次渲染过程中出现任何透支,因为只有最近的可见像素才会通过此深度比较测试。 此外,第二次渲染过程不需要写入深度缓冲区,因为我们已经将场景写入正常/深度渲染目标阶段中的深度缓冲区。

D3D11_DEPTH_STENCIL_DESC equalsDesc;
ZeroMemory(&equalsDesc, sizeof(D3D11_DEPTH_STENCIL_DESC));
equalsDesc.DepthEnable = true;
equalsDesc.DepthWriteMask = D3D11_DEPTH_WRITE_MASK_ZERO;
equalsDesc.DepthFunc = D3D11_COMPARISON_EQUAL;
HR(device->CreateDepthStencilState(&equalsDesc, &EqualsDSS));
...
//
// Render the view space normals and depths. This render target has the
// same dimensions as the back buffer, so we can use the screen
viewport.
// This render pass is needed to compute the ambient occlusion.
// Notice that we use the main depth/stencil buffer in this pass.
//
md3dImmediateContext->ClearDepthStencilView(mDepthStencilView,
D3D11_CLEAR_DEPTH|D3D11_CLEAR_STENCIL, 1.0f, 0);
md3dImmediateContext->RSSetViewports(1, &mScreenViewport);
mSsao->SetNormalDepthRenderTarget(mDepthStencilView);
DrawSceneToSsaoNormalDepthMap();
//
// Now compute the ambient occlusion.
//
mSsao->ComputeSsao(mCam);
mSsao->BlurAmbientMap(6);
//
// Restore the back and depth buffer and viewport to the OM stage.
//
ID3D11RenderTargetView* renderTargets[1] = {mRenderTargetView};
md3dImmediateContext->OMSetRenderTargets(1, renderTargets, mDepthStencilView);
md3dImmediateContext->RSSetViewports(1, &mScreenViewport);
md3dImmediateContext->ClearRenderTargetView(mRenderTargetView,
reinterpret_cast<const float*>(&Colors::Silver));
// We already laid down scene depth to the depth buffer in the
// Normal/Depth map pass, so we can set the depth comparison
// test to "EQUALS." This prevents any overdraw in this rendering
// pass, as only the nearest visible pixels will pass this depth
// comparison test.
md3dImmediateContext->OMSetDepthStencilState(RenderStates::EqualsDSS, 0);
// ...draw scene to back buffer

在这个演示中,我们从非细分几何中计算SSAO。 这种简化可能取决于场景中的位移量。 如果要使用曲面细分几何来计算SSAO贴图,那么在渲染到正常/深度渲染目标时,需要镶嵌几何图形。

22.3总结

1.照明方程的环境项模拟间接光。在我们的照明模型中,环境项仅仅是一个常数值。因此,当物体处于阴影中并且只有环境光应用于表面时,模型显得非常平坦,没有固定的定义。环境遮挡的目标是找到环境项的更好估计值,以便即使仅应用环境项,对象仍然看起来是3D。
2.环境遮挡的想法是,表面上的点p的间接光线的量与半球上入射的光线对p的遮挡程度成比例。估计点p遮挡的一种方法是通过射线投射。我们随机在半球上投射光线,并检查与网格的交点。如果光线不与任何几何相交,则该点完全不受遮挡;然而,交叉点越多,p必须越多。
3.对于动态物体,射线环境光遮挡对于实时进行太昂贵。屏幕空间环境遮挡(SSAO)是一种基于视图空间正常/深度值的实时近似。你一定可以找到它给出错误结果的缺陷和情况,但在实践中结果非常好,因为它只能处理有限的信息。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值