21-阴影映射

阴影向观察者指示光线来源,并帮助传达场景中物体的相对位置。本章介绍了基本的阴影映射算法,这是一种在游戏和3D应用中建模动态阴影的流行方法。对于一本介绍性书籍,我们只关注基本阴影映射算法;通过扩展基本阴影映射算法来构建更高级的阴影技术,例如级联阴影贴图[Engel06],它可提供更好的质量结果。
目标:
1.发现基本阴影映射算法。
2.了解投影纹理如何工作。
3.了解关于正射投影。
4.了解阴影贴图锯齿问题和修复它们的常用策略。

21.1渲染场景深度

阴影映射算法依赖于从光源视角渲染场景深度 - 这实质上是渲染到纹理的变体,这在第12.7.2节中首次描述。 “渲染场景深度”是指从光源的角度来构建深度缓冲区。因此,在我们从光源的角度渲染场景后,我们会知道离光源最近的像素碎片 - 这些碎片不能在阴影中。在本节中,我们将回顾一个名为ShadowMap的实用工具类,帮助我们从光源角度存储场景深度。它只是封装深度/模板缓冲区,必要的视图和视口。用于阴影映射的深度/模板缓冲区称为阴影图。

class ShadowMap
{
public:S hadowMap(ID3D11Device* device, UINT width, UINT height);
~ShadowMap();
ID3D11ShaderResourceView* DepthMapSRV();
void BindDsvAndSetNullRenderTarget(ID3D11DeviceContext* dc);
private:
ShadowMap(const ShadowMap& rhs);
ShadowMap& operator=(const ShadowMap& rhs);
private:
UINT mWidth;
UINT mHeight;
ID3D11ShaderResourceView* mDepthMapSRV;
ID3D11DepthStencilView* mDepthMapDSV;
D3D11_VIEWPORT mViewport;
};

构造函数创建指定尺寸,视图和视口的纹理。 阴影贴图的分辨率会影响阴影的质量,但同时,高分辨率阴影贴图的渲染成本更高,需要更多的内存。

ShadowMap::ShadowMap(ID3D11Device* device, UINT width, UINT height)
: mWidth(width), mHeight(height), mDepthMapSRV(0), mDepthMapDSV(0)
{
// Viewport that matches the shadow map dimensions.
mViewport.TopLeftX = 0.0f;
mViewport.TopLeftY = 0.0f;
mViewport.Width = static_cast<float>(width);
mViewport.Height = static_cast<float>(height);
mViewport.MinDepth = 0.0f;
mViewport.MaxDepth = 1.0f;
// Use typeless format because the DSV is going to interpret
// the bits as DXGI_FORMAT_D24_UNORM_S8_UINT, whereas the SRV is going
// to interpret the bits as DXGI_FORMAT_R24_UNORM_X8_TYPELESS.
D3D11_TEXTURE2D_DESC texDesc;
texDesc.Width = mWidth;
texDesc.Height = mHeight;
texDesc.MipLevels = 1;
texDesc.ArraySize = 1;
texDesc.Format = DXGI_FORMAT_R24G8_TYPELESS;
texDesc.SampleDesc.Count = 1;
texDesc.SampleDesc.Quality = 0;
texDesc.Usage = D3D11_USAGE_DEFAULT;
texDesc.BindFlags = D3D11_BIND_DEPTH_STENCIL |
D3D11_BIND_SHADER_RESOURCE;
texDesc.CPUAccessFlags = 0;
texDesc.MiscFlags = 0;
ID3D11Texture2D* depthMap = 0;
HR(device->CreateTexture2D(&texDesc, 0, &depthMap));
D3D11_DEPTH_STENCIL_VIEW_DESC dsvDesc;
dsvDesc.Flags = 0;
dsvDesc.Format = DXGI_FORMAT_D24_UNORM_S8_UINT;
dsvDesc.ViewDimension = D3D11_DSV_DIMENSION_TEXTURE2D;
dsvDesc.Texture2D.MipSlice = 0;
HR(device->CreateDepthStencilView(depthMap, &dsvDesc, &mDepthMapDSV));
D3D11_SHADER_RESOURCE_VIEW_DESC srvDesc;
srvDesc.Format = DXGI_FORMAT_R24_UNORM_X8_TYPELESS;
srvDesc.ViewDimension = D3D11_SRV_DIMENSION_TEXTURE2D;
srvDesc.Texture2D.MipLevels = texDesc.MipLevels;
srvDesc.Texture2D.MostDetailedMip = 0;
HR(device->CreateShaderResourceView(depthMap, &srvDesc, &mDepthMapSRV));
// View saves a reference to the texture so we can release our
// reference.
ReleaseCOM(depthMap);
}

正如我们将看到的那样,阴影映射算法需要两次渲染过程。 在第一个中,我们从光线的视点渲染场景深度到阴影图中; 在第二遍中,我们将场景渲染为来自“播放器”相机的后台缓冲区的正常,但是使用阴影贴图作为着色器输入来实现遮蔽算法。 我们提供了一种访问阴影贴图的着色器资源视图的方法:

ID3D11ShaderResourceView* ShadowMap::DepthMapSRV()
{
return mDepthMapSRV;
}

最后,下面的方法准备用于渲染阴影贴图的OM阶段。 注意到我们设置了一个空渲染目标,它实质上禁用了颜色写入。 这是因为当我们将场景渲染到阴影贴图时,我们所关心的只是场景相对于光源的深度值。 图形卡仅针对绘图深度进行了优化; 只有渲染通道的深度比绘制颜色和深度要快得多。

void ShadowMap::BindDsvAndSetNullRenderTarget(ID3D11DeviceContext* dc)
{
dc->RSSetViewports(1, &mViewport);
// Set null render target because we are only going to draw
// to depth buffer. Setting a null render target will disable
// color writes.
ID3D11RenderTargetView* renderTargets[1] = {0};
dc->OMSetRenderTargets(1, renderTargets, mDepthMapDSV);
dc->ClearDepthStencilView(mDepthMapDSV, D3D11_CLEAR_DEPTH, 1.0f, 0);
}

在完成绘制到阴影贴图后,调用应用程序负责将后台缓冲区恢复为渲染目标,原始深度/模板缓冲区和原始视口。

NOTE:处理多个渲染目标的更好设计是实现一个堆栈,以便您可以推送和弹出渲染目标。 弹出会自动恢复以前的渲染目标,深度/模板缓冲区和视口。

21.2 ORYOGRAPHIC预测

到目前为止,在本书中,我们一直在使用透视投影。透视投影的关键特性是随着距离眼睛距离的增加,物体被视为越来越小。这与我们如何看待现实生活中的事物一致。另一种类型的投影是正投影。这种投影主要用于三维科学或工程应用,在投影后希望平行线保持平行。然而,正交投影将使我们能够模拟平行光产生的阴影。使用正射投影时,观察体积是一个盒子,轴线与宽度为w,高度为h,靠近平面n的视图空间和沿视图空间的正z轴向下的远平面f对齐(见图21.1)。这些数字(相对于视图空间坐标系定义)定义了盒视图体积。

通过正投影,投影线平行于视图空间z轴(图21.2)。我们看到顶点(x,y,z)的2D投影只是(x,y)。


图21.1 正视观看音量是一个与视图坐标系统轴对齐的盒子。

与透视投影一样,我们希望保持相对深度信息,并且我们需要标准化的设备坐标。要将视图体从视图空间 [w2,w2]×[h2,h2]×[n,f] [ − w 2 , w 2 ] × [ − h 2 , h 2 ] × [ n , f ] 转换为NDC空间 [1,1]×[1,1]×[0,1] [ − 1 , 1 ] × [ − 1 , 1 ] × [ 0 , 1 ] ,我们需要重新缩放并转换,以将视图空间视图体映射到NDC空间视图体积。我们可以通过工作坐标来确定这个映射。对于前两个坐标,很容易看出这些间隔只有一个比例因子:
2w[w2,w2]=[1,1]2h[h2,h2]=[1,1] 2 w · [ − w 2 , w 2 ] = [ − 1 , 1 ] 2 h · [ − h 2 , h 2 ] = [ − 1 , 1 ]

对于第三个坐标,我们需要映射[n,f]→[0,1]。 我们假设映射的形式为g(z)= az + b(即缩放和转换)。 我们有条件g(n)= 0和g(f)= 1,这使我们能够解决a和b:
an+b=0af+b=1 a n + b = 0 a f + b = 1


图21.2 点到投影平面上的正投影。投影线与正视投影平行于视图空间Z轴。

第一个等式意味着b = -an。 将其插入第二个等式中,我们得到:
afan=1a=1fn a f − a n = 1 a = 1 f − n

所以:
nfn=b − n f − n = b

因此得:
g(z)=zfnnfn g ( z ) = z f − n − n f − n

读者可能希望在域[n,f]上为g(z)绘制各种n和f,使得f> n。
最后,从视图空间坐标(x,y,z)到NDC空间坐标(x’,y’,z’)的正交转换为:
x=2wxy=2hyz=zfnnfn x ′ = 2 w x y ′ = 2 h y z ′ = z f − n − n f − n

或者以矩阵形式表述:
[x,y,z,1]=[x,y,z,1]2w00002h00001fnnnf0001 [ x ′ , y ′ , z ′ , 1 ] = [ x , y , z , 1 ] [ 2 w 0 0 0 0 2 h 0 0 0 0 1 f − n 0 0 0 n n − f 1 ]

上述公式中的4×4矩阵是正交投影矩阵。

回想一下,在透视投影变换的情况下,我们不得不将它分成两部分:由投影矩阵描述的线性部分和由除以w描述的非线性部分。 相反,正交投影变换是完全线性的 - 没有w除以。 乘以正交投影矩阵直接将我们转换成NDC坐标。

21.3投影纹理坐标

投影纹理是所谓的,因为它允许我们将纹理投影到任意几何图形上,就像投影机一样。 图21.3显示了投影纹理的一个例子。

投影纹理对幻灯机投影灯的建模可能很有用,但正如我们将在第21.4节中看到的那样,它也可用作阴影映射的中间步骤。

投影纹理的关键是为每个像素生成纹理坐标,以便应用的纹理看起来像已投影到几何图形上。我们将调用这样生成的纹理坐标投影纹理坐标。

从图21.4中,我们看到纹理坐标(u,v)标识了应投影到3D点p上的纹理元素。但坐标(u,v)相对于投影窗口上的纹理空间坐标系精确地标识投影窗口上的p的投影。所以生成投影纹理坐标的策略如下:
1.将点p投影到灯的投影窗口上,并将坐标转换为NDC空间。
2.将投影坐标从NDC空间转换到纹理空间,从而有效地将它们转换为纹理坐标。


图21.3 头骨纹理(右)投影到场景几何图形(左)。


图21.4 由相对于投影窗口上的纹理空间的坐标(u,v)标识的纹理元素通过跟随从光源到p点的视线被投影到点p上。

可以通过将投光器当作照相机来实现步骤1。 我们为投光器定义了视图矩阵V和投影矩阵P. 总之,这些矩阵基本上定义了投影机在世界上的位置,方向和平截头体。矩阵V将坐标从世界空间转换为投影机的坐标系。 一旦坐标相对于光坐标系统,投影矩阵连同均匀分割被用于将顶点投影到光投影平面上。 回顾第5.6.3.5节,在均匀划分之后,坐标位于NDC空间中。

步骤2通过以下坐标转换改变,从NDC空间转换到纹理空间完成:

u=0.5x+0.5v=0.5y+0.5 u = 0.5 x + 0.5 v = − 0.5 y + 0.5

这里,u,v∈[0,1]提供了x,y∈[-1,1]。 我们将y坐标缩小为负值以反转轴,因为NDC坐标中的正y轴与纹理坐标中正v轴的方向相反。 纹理空间变换可以用矩阵来表示(回忆第3章的练习21):
[x,y,0,1]0.5000.500.500.500100001=[u,v,0,1] [ x , y , 0 , 1 ] [ 0.5 0 0 0 0 − 0.5 0 0 0 0 1 0 0.5 0.5 0 1 ] = [ u , v , 0 , 1 ]

让我们称之为“纹理矩阵”的前一个矩阵T,它从NDC空间转换到纹理空间。 我们可以形成将我们从世界空间直接带到纹理空间的复合变换VPT。 在我们乘以这个变换之后,我们仍然需要做视角分割来完成变换; 请参阅第5章练习8,了解为什么我们可以在进行纹理变换之后进行透视分割。

21.3.1代码实施

生成投影纹理坐标的代码如下所示:

struct VertexOut
{
float4 PosH : SV_POSITION;
float3 PosW : POSITION;
float3 TangentW : TANGENT;
float3 NormalW : NORMAL;
float2 Tex : TEXCOORD0;
float4 ProjTex : TEXCOORD1;
};
VertexOut VS(VertexIn vin)
{
VertexOut vout;
[...]
// Transform to light′s projective space.
vout.ProjTex = mul(float4(vIn.posL, 1.0f),
gLightWorldViewProjTexture);
[...]
return vout;
}
float4 PS(VertexOut pin) : SV_Target
{
// Complete projection by doing division by w.
pin.ProjTex.xyz /= pin.ProjTex.w;
// Depth in NDC space.
float depth = pin.ProjTex.z;
// Sample the texture using the projective tex-coords.
float4 c = gTextureMap.Sample(sampler, pin.ProjTex.xy);
[...]
}

21.3.2点以外的点

在渲染管线中,截锥体外的几何体被剪切。然而,当我们通过从投影机的角度投影几何图形来生成投影纹理坐标时,不会执行剪裁 - 我们只需投影顶点。因此,投影机平截头体外的几何体会接收[0,1]范围外的投影纹理坐标。根据启用的地址模式(请参阅(§8.8)),在[0,1]范围函数之外的投影纹理坐标与[0,1]范围之外的常规纹理坐标相同。

一般来说,我们不想在投影机的平截头体之外构造任何几何图形,因为它没有意义(这种几何图形没有从投影机接收光线)。使用零色的边框颜色地址模式是常用的解决方案。另一种策略是将聚光灯(请参阅§7.10)与投影机相关联,以便聚光灯视野外的任何物体都不会亮起(即表面不接收投影光线)。使用聚光灯的优点是投影机的光强度在聚光灯锥体的中心最强,并且随着-L和d之间的角度φ增加而平滑淡出(其中L是到表面点的光矢量d是聚光灯的方向)。

21.3.3正交投影

到目前为止,我们已经使用透视投影(平截体形体积)说明了投影纹理。但是,我们可以使用正交投影来代替投影过程的透视投影。在这种情况下,纹理通过盒子投射在光线的z轴方向上。

我们所谈到的与投影纹理坐标有关的所有内容在使用正交投影时也适用,除了一些事情。首先,通过正投影法,用于处理投影机音量以外的点的聚光灯策略不起作用。这是因为聚光灯锥体在某种程度上近似于平截头体的体积,但它并不近似于盒子。但是,我们仍然可以使用纹理地址模式处理投影机体积之外的点。这是因为正交投影仍然会生成NDC坐标,并且NDC空间中的点(x,y,z)位于该体积内当且仅当:

1x11y10z1 − 1 ≤ x ≤ 1 − 1 ≤ y ≤ 1 0 ≤ z ≤ 1

其次,用正交投影法,我们不需要用w进行除法; 也就是说,我们不需要这一行:

// Complete projection by doing division by w.
pin.ProjTex.xyz /= pin.ProjTex.w;

这是因为在正投影之后,坐标已经在NDC空间中。 这是更快的,因为它避免了透视投影所需的每像素分割。 另一方面,留在分区不会因为它除以1而产生伤害(正交投影不会改变w坐标,所以w将是1)。 如果我们在着色器代码中将w除以w,那么着色器代码对于透视投影和正交投影均匀地起作用。 尽管如此,对这种一致性的权衡是你用正交投影做了一个多余的分工。

21.4阴影映射

21.4.1算法描述

阴影映射算法的思想是将从光视点到场景深度的纹理渲染到称为阴影贴图的深度缓冲区中。完成后,阴影贴图将包含从光线角度看所有可见像素的深度值。 (由其他像素遮挡的像素将不在阴影贴图中,因为它们将无法进行深度测试并且被覆盖或从未写入。)

为了从光的视角渲染场景,我们需要定义一个光视图矩阵,它将坐标从世界空间转换为光的空间,还需要一个光投影矩阵,它描述了光在世界中发射的体积。这可以是平截体积(透视投影)或箱体积(正交投影)。通过在锥台内嵌入聚光灯锥体,可以使用平截光体的光量来模拟聚光灯。一个箱子的光量可以用来模拟平行灯。但是,平行光现在是有界的,只能通过盒体;因此,它可能只会触及现场的一个子集(见图21.5)。对于撞击整个场景(如太阳)的光源,我们可以使光线足够大以容纳整个场景。


图21.5 平行光线穿过光线量,因此只有体积内的一部分场景会接收光线。如果光源需要击中整个场景,我们可以设置光量大小以包含整个场景。


图21.6 在左边,来自光的像素p的深度是d(p)。 然而,沿着同一视线最接近光的像素的深度具有深度s(p),并且d(p)> s(p)。 因此,我们得出结论,从光的角度来看p的前面有一个物体,所以p在阴影中。 在右边,像素p距离光线的深度是d(p),它也恰好是沿着视线最靠近光线的像素,即s(p)= d(p),所以 我们得出结论p不在阴影中。

一旦我们建立了阴影贴图,我们就从“玩家”相机的角度渲染场景。 对于渲染的每个像素p,我们还计算了光源的深度,我们用d(p)表示。 另外,使用投影贴图,我们沿着从光源到像素p的视线对阴影贴图进行采样,以获得存储在阴影贴图中的深度值s(p) 这个值是沿着从光线位置到p的视线最接近光线的像素的深度。 然后,从图21.6看出,当且仅当d(p)> s(p)时,像素p在阴影中。 因此,当且仅当d(p)≤s(p)时,像素不在阴影中。

NOTE:比较的深度值在NDC坐标中。 这是因为阴影贴图是深度缓存,它将深度值存储在NDC坐标中。 当我们查看代码时,完成这个过程将会很清楚。

21.4.2偏置和混叠

阴影图存储最近可见像素相对于其相关光源的深度。 但是,阴影图只有一些有限的分辨率。 所以每个阴影贴图texel对应于场景的一个区域。 因此,阴影图仅仅是从光线角度来看场景深度的离散采样。 这会导致称为阴影痤疮的别名问题(参见图21.7)。


图21.7 注意光线和阴影之间的“阶梯式”交替在地平面上的走样。 这种别名错误通常被称为阴影痤疮。

图21.8显示了一个简单的图解释阴影痤疮发生的原因。 一个简单的解决方案是应用恒定偏差来抵消阴影图深度。 图21.9显示了这是如何纠正问题的。

图21.8。 阴影贴图对场景的深度进行采样。 观察到由于阴影贴图的有限分辨率,每个阴影贴图texel对应于场景的一个区域。 眼睛E在场景 p1p2 p 1 和 p 2 上看到对应于不同屏幕像素的两个点。 然而,从光的角度来看,两个点都被同一个阴影贴图texel覆盖(即 s(p1)=s(p2)=s s ( p 1 ) = s ( p 2 ) = s )。 当我们进行阴影映射测试时,我们有 d(p1)>s d ( p 1 ) > s d(p2)s d ( p 2 ) ≤ s 。 因此, p1 p 1 将被着色,就好像它在阴影中一样,并且 p2 p 2 将被着色,就好像它不在阴影中一样。 这会导致阴影痤疮。


图21.9。 通过偏置阴影贴图中的深度值,不会出现虚假阴影。 我们有 d(p1)sd(p2)s d ( p 1 ) ≤ s 和 d ( p 2 ) ≤ s 。找到正确的深度偏差通常是通过实验完成的。

过多的偏置会导致称为彼特宁的伪影,其中阴影似乎与对象分离(见图21.10)。

图21.10 由于深度偏差很大,彼得摇摆 - 阴影从柱子上脱落。

不幸的是,固定偏差不适用于所有几何。特别是,图21.11显示了具有大斜率的三角形(相对于光源)需要较大的偏差。选择足够大的深度偏差来处理所有斜坡是很有诱惑力的。 但是,如图21.10所示,这导致了彼此摇摆。

图21.11 相对于光源而言,具有大斜率的多边形要求比具有相对于光源的小斜率的多边形更多的偏压。

我们想要的是一种方法来测量相对于光源的多边形斜率,并为较大的倾斜多边形应用更多的偏差。 幸运的是,图形硬件通过所谓的斜坡比例偏置光栅化状态属性为其提供内在的支持:

typedef struct D3D11_RASTERIZER_DESC {
[...]
INT DepthBias;
FLOAT DepthBiasClamp;
FLOAT SlopeScaledDepthBias;
[...]
} D3D11_RASTERIZER_DESC;

1.DepthBias:适用的固定偏见; 请参阅以下注释以了解如何将此整数值用于UNORM深度缓冲区格式。
2.DepthBiasClamp:允许的最大深度偏差。 这使得我们可以设定深度偏差的界限,因为我们可以想象,对于非常陡峭的斜坡,偏斜斜率比例偏差太大并且导致平移伪像。
3.SlopeScaledDepthBias:根据多边形斜率来控制偏移量的比例因子; 请参阅下面的公式注释。
请注意,当我们将场景渲染到阴影贴图时,我们应用斜率比例偏置。 这是因为我们希望根据相对于光源的多边形斜率进行偏置。 因此,我们正在偏置阴影贴图值。 在我们的演示中,我们使用这些值:

RasterizerState Depth
{
// [From MSDN]
// If the depth buffer currently bound to the output-merger stage
// has a UNORM format or no depth buffer is bound the bias value
// is calculated like this:
//
// Bias = (float)DepthBias * r + SlopeScaledDepthBias * MaxDepthSlope;
//
// where r is the minimum representable value > 0 in the
// depth-buffer format converted to float32.
// [/End MSDN]
//
// For a 24-bit depth buffer, r = 1 / 2^24.
//
// Example: DepthBias = 100000 ==> Actual DepthBias = 100000/2^24 = .006
// These values are highly scene dependent, and you will need
// to experiment with these values for your scene to find the
// best values.
DepthBias = 100000;
DepthBiasClamp = 0.0f;
SlopeScaledDepthBias = 1.0f;
};

NOTE:深度偏差发生在光栅化过程中(剪切后),所以不会影响几何裁剪。
NOTE:有关深度偏差的完整详细信息,请在SDK文档中搜索“深度偏移”,它将提供所有应用程序的规则以及它如何用于浮点深度缓冲区。

21.4.3 PCF过滤

用于采样阴影贴图的投影纹理坐标(u,v)通常不会与阴影贴图中的纹理元素重合。通常,它将在四个纹素之间。使用彩色纹理时,可以用双线性插值(第8.4.1节)解决。然而,[Kilgard01]指出,我们不应该平均深度值,因为它可能导致关于像素在阴影中被标记的错误结果。 (出于同样的原因,我们也不能生成阴影贴图的mipmap。)我们不用内插深度值,而是插入结果 - 这被称为百分比近似滤波(PCF)。也就是说,我们使用点滤波(MIN_MAG_MIP_POINT)并且对坐标(u,v),(u +Δx,v),(u,v +Δx),(u +Δx,v +Δx) ),其中Δx= 1 / SHADOW_MAP_SIZE。因为我们正在使用点采样,所以这四个点将分别到达(u,v)周围最接近的四个像素 s0,s1,s2s3 s 0 , s 1 , s 2 和 s 3 ,如图21.12所示。然后,我们对每个这些采样深度执行阴影贴图测试,并对阴影贴图结果进行双线性插值:


图21.12 以四个阴影地图样本。

static const float SMAP_SIZE = 2048.0f;
static const float SMAP_DX = 1.0f / SMAP_SIZE;
...
// Sample shadow map to get nearest depth to light.
float s0 = gShadowMap.Sample(gShadowSam,
projTexC.xy).r;
float s1 = gShadowMap.Sample(gShadowSam,
projTexC.xy + float2(SMAP_DX, 0)).r;
float s2 = gShadowMap.Sample(gShadowSam,
projTexC.xy + float2(0, SMAP_DX)).r;
float s3 = gShadowMap.Sample(gShadowSam,
projTexC.xy + float2(SMAP_DX, SMAP_DX)).r;
// Is the pixel depth <= shadow map value?
float result0 = depth <= s0;
float result1 = depth <= s1;
float result2 = depth <= s2;
float result3 = depth <= s3;
// Transform to texel space.
float2 texelPos = SMAP_SIZE*projTexC.xy;
// Determine the interpolation amounts.
float2 t = frac(texelPos);
// Interpolate results.
return lerp(lerp(result0, result1, t.x),
lerp(result2, result3, t.x), t.y);

这样,它不是一个全或无的情况; 一个像素可以部分处于阴影中。 例如,如果两个样本在阴影中,另外两个不在阴影中,则该像素在阴影中为50%。 这可以创建从阴影像素到非阴影像素的平滑过渡(请参见图21.13)。

HLSL压缩函数返回浮点数的小数部分(即尾数)。 例如,如果SMAP_SIZE = 1024和projTex.xy =(0.23,0.68),则texelPos =(235.52,696.32)和frac(texelPos)=(0.52,0.32)。 这些分数告诉我们在样本之间插入多少。 HLSL lerp(x,y,s)函数是线性插值函数,并返回x + s(y - x)=(1 - s)x + sy。

NOTE:即使使用我们的过滤,阴影仍然非常困难,并且混叠伪像仍然不能令人满意。 可以使用更积极的方法; 例如参见[Uralsky05]。 我们还注意到使用更高分辨率的阴影贴图有助于提高成本,但成本过高。

如前所述,PCF滤波的主要缺点是它需要四个纹理样本。 采样纹理是现代GPU上更昂贵的操作之一,因为内存带宽和内存延迟并未像GPU的原始计算能力那样得到提高[Möller08]。 幸运的是,Direct3D 11图形硬件通过SampleCmpLevelZero方法构建了对PCF的支持:


图21.13。 在顶部图像中,观察阴影边界上的“阶梯”伪影。 在底部图像上,这些混叠伪像通过滤波进行了平滑处理。

Texture2D gShadowMap;
SamplerComparisonState samShadow
{
Filter = COMPARISON_MIN_MAG_LINEAR_MIP_POINT;
// Return 0 for points outside the light frustum
// to put them in shadow.
AddressU = BORDER;
AddressV = BORDER;
AddressW = BORDER;
BorderColor = float4(0.0f, 0.0f, 0.0f, 0.0f);
ComparisonFunc = LESS_EQUAL;
};
// Automatically does a 4-tap PCF. The compare
percentLit = shadowMap.SampleCmpLevelZero(
samShadow, shadowPosH.xy, depth).r;

方法名称的LevelZero部分意味着它只查看顶部的mipmap级别,这很好,因为这是我们想要的阴影映射(我们不会为阴影映射生成mipmap链)。 此方法不使用典型的采样器对象,而是使用所谓的比较采样器。 这样硬件就可以进行阴影图比较测试,这需要在过滤结果之前完成。 对于PCF,您需要使用过滤器COMPARISON_MIN_MAG_LINEAR_MIP_POINT并将比较函数设置为LESS_EQUAL(LESS也适用,因为我们偏向于深度)。 第一个和第二个参数分别是比较采样器和纹理坐标。 第三个参数是与阴影贴图样本进行比较的值。 因此,将比较值设置为深度,并将比较函数设置为LESS_EQUAL,我们正在进行比较:

float result0 = depth <= s0;
float result1 = depth <= s1;
float result2 = depth <= s2;
float result3 = depth <= s3;

然后硬件双线性插值结果来完成PCF。

NOTE:从SDK文档中,只有以下格式支持比较过滤器:R32_FLOAT_X8X24_TYPELESS,R32_FLOAT,R24_UNORM_X8_TYPELESS,R16_UNORM。

到目前为止,在本节中,我们使用了一个4阶PCF内核。可以使用更大的内核来使阴影边缘更大,更平滑,但需要额外的SampleCmpLevelZero调用。在我们的演示中,我们使用3×3盒式过滤器模式调用SampleCmpLevelZero。由于每个SampleCmpLevelZero调用执行4次抽头PCF,因此我们使用阴影贴图中的4×4个独特样本点(根据我们的样式,样本点有一些重叠)。使用大型过滤内核可能会导致阴影痤疮问题返回;我们解释了为什么并在§21.5中描述了一个解决方案。

一个观察结果是,PCF确实只需要在阴影边缘执行。在影子里面,没有混合,在影子外面没有混合。基于这种观察,已经设计了仅在阴影边缘进行PCF的方法。 [Isidoro06b]描述了一种方法来做到这一点。这种技术需要在着色器代码中使用动态分支:“如果我们处于阴影边缘,请执行昂贵的PCF,否则只需要一个阴影贴图样本。”

请注意,如果您的PCF内核很大(例如5×5或更大),那么执行此类方法的额外开支只会有价值;但是,这只是一般性建议,您需要通过配置文件来验证成本/收益。

最后一点是你的PCF内核不需要是盒式过滤器网格。关于在PCF内核中随机挑选点的文章已经写了很多。

21.4.4构建阴影贴图

阴影贴图的第一步是构建阴影贴图。 为此,我们创建一个ShadowMap实例:

static const int SMapSize = 2048;
mSmap = new ShadowMap(md3dDevice, SMapSize, SMapSize);

然后,我们定义一个轻视图矩阵和投影矩阵(表示轻框和视图体积)。轻视图矩阵是从主光源导出的,并且计算光视图体积以适合整个场景的边界球。

// Estimate the scene bounding sphere manually since we know
// how the scene was constructed. The grid is the "widest object"
// with a width of 20 and depth of 30.0f, and centered at the world
// space origin. In general, you need to loop over every world space
// vertex position and compute the bounding sphere.
struct BoundingSphere
{
BoundingSphere() : Center(0.0f, 0.0f, 0.0f), Radius(0.0f) {}
XMFLOAT3 Center;
float Radius;
};
BoundingSphere mSceneBounds;
mSceneBounds.Center = XMFLOAT3(0.0f, 0.0f, 0.0f);
mSceneBounds.Radius = sqrtf(10.0f*10.0f + 15.0f*15.0f);
void ShadowsApp::UpdateScene(float dt)
{
[...]
//
// Animate the lights (and hence shadows).
//
mLightRotationAngle += 0.1f*dt;
XMMATRIX R = XMMatrixRotationY(mLightRotationAngle);
for(int i = 0; i < 3; ++i)
{
XMVECTOR lightDir = XMLoadFloat3(&mOriginalLightDir[i]);
lightDir = XMVector3TransformNormal(lightDir, R);
XMStoreFloat3(&mDirLights[i].Direction, lightDir);
}
BuildShadowTransform();
}
void ShadowsApp::BuildShadowTransform()
{
// Only the first "main" light casts a shadow.
XMVECTOR lightDir = XMLoadFloat3(&mDirLights[0].Direction);
XMVECTOR lightPos = -2.0f*mSceneBounds.Radius*lightDir;
XMVECTOR targetPos = XMLoadFloat3(&mSceneBounds.Center);
XMVECTOR up = XMVectorSet(0.0f, 1.0f, 0.0f, 0.0f);
XMMATRIX V = XMMatrixLookAtLH(lightPos, targetPos, up);
// Transform bounding sphere to light space.
XMFLOAT3 sphereCenterLS;
XMStoreFloat3(&sphereCenterLS, XMVector3TransformCoord(targetPos, V));
// Ortho frustum in light space encloses scene.
float l = sphereCenterLS.x - mSceneBounds.Radius;
float b = sphereCenterLS.y - mSceneBounds.Radius;
float n = sphereCenterLS.z - mSceneBounds.Radius;
float r = sphereCenterLS.x + mSceneBounds.Radius;
float t = sphereCenterLS.y + mSceneBounds.Radius;
float f = sphereCenterLS.z + mSceneBounds.Radius;
XMMATRIX P = XMMatrixOrthographicOffCenterLH(l, r, b, t, n, f);
// Transform NDC space [-1,+1]^2 to texture space [0,1]^2
XMMATRIX T(
0.5f, 0.0f, 0.0f, 0.0f,
0.0f, -0.5f, 0.0f, 0.0f,
0.0f, 0.0f, 1.0f, 0.0f,
0.5f, 0.5f, 0.0f, 1.0f);
XMMATRIX S = V*P*T;
XMStoreFloat4x4(&mLightView, V);
XMStoreFloat4x4(&mLightProj, P);
XMStoreFloat4x4(&mShadowTransform, S);
}
Rendering the scene into the shadow map is done like so:
Rendering the scene into the shadow map is done like so:
mSmap->BindDsvAndSetNullRenderTarget(md3dImmediateContext);
DrawSceneToShadowMap();
md3dImmediateContext->RSSetState(0);
//
// Restore the back and depth buffer to the OM stage.
//
ID3D11RenderTargetView* renderTargets[1] = {mRenderTargetView};
md3dImmediateContext->OMSetRenderTargets(1, renderTargets, mDepthStencilView);
md3dImmediateContext->RSSetViewports(1, &mScreenViewport);

我们用于从光线角度渲染场景的效果文件非常简单,因为我们只是构建阴影贴图,因此我们不需要执行任何复杂的像素着色器工作。

cbuffer cbPerFrame
{
float4x4 gLightWVP;
};
// Nonnumeric values cannot be added to a cbuffer.
Texture2D gDiffuseMap;
SamplerState samLinear
{
Filter = MIN_MAG_MIP_LINEAR;
AddressU = Wrap;
AddressV = Wrap;
};
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;
vout.PosH = mul(float4(vin.PosL, 1.0f), gWorldViewProj);
vout.Tex = mul(float4(vin.Tex, 0.0f, 1.0f), gTexTransform).xy;
return vout;
}
// This is only used for alpha cut out geometry, so that shadows
// show up correctly. Geometry that does not need to sample a
// texture can use a NULL pixel shader for depth pass.
void PS(VertexOut pin)
{
float4 diffuse = gDiffuseMap.Sample(samLinear, pin.Tex);
// Don′t write transparent pixels to the shadow map.
clip(diffuse.a - 0.15f);
}
RasterizerState Depth
{/
/ [From MSDN]
// If the depth buffer currently bound to the output-merger stage has a UNORM
// format or no depth buffer is bound the bias value is calculated like this:
//
// Bias = (float)DepthBias * r + SlopeScaledDepthBias * MaxDepthSlope;
//
// where r is the minimum representable value > 0 in the depth-buffer format
// converted to float32.
// [/End MSDN]
//
// For a 24-bit depth buffer, r = 1 / 2^24.
//
// Example: DepthBias = 100000 ==> Actual DepthBias = 100000/2^24 = .006
// You need to experiment with these values for your scene.
DepthBias = 100000;
DepthBiasClamp = 0.0f;
SlopeScaledDepthBias = 1.0f;
};
technique11 BuildShadowMapTech
{
pass P0
{
SetVertexShader(CompileShader(vs_5_0, VS()));
SetGeometryShader(NULL);
SetPixelShader(NULL);
SetRasterizerState(Depth);
}
}
technique11 BuildShadowMapAlphaClipTech
{
pass P0
{
SetVertexShader(CompileShader(vs_5_0, VS()));
SetGeometryShader(NULL);
SetPixelShader(CompileShader(ps_5_0, PS()));
}
}

请注意,像素着色器不返回值,因为我们只需要输出深度值。 像素着色器仅用于剪切具有零或低α值的像素片段,我们假定它表示完全透明。 例如,考虑图21.14中的树叶纹理; 在这里,我们只想绘制带有白色alpha值的像素到阴影贴图。 为了便于这一点,我们提供了两种技术:一种是用于剪辑操作的,另一种则不用。 如果不需要完成alpha剪辑,那么我们可以绑定一个null像素着色器,这比绑定仅对纹理进行采样并执行剪辑操作的像素着色器要快。


图21.14 叶纹理。

NOTE:尽管为了简洁起见没有示出,但用于渲染镶嵌几何的深度的着色器稍微涉及更多。 将曲面细分几何绘制到阴影贴图中时,我们需要按照与绘制到后台缓冲区时相同的方式(即基于与玩家眼睛的距离)对棋盘格进行细分。 这是为了一致性; 眼睛看到的几何形状应该与光线看到的几何形状相同。 也就是说,如果棋盘格几何不会移位太多,位移在阴影中甚至可能不会明显; 因此,可能的优化可能不是在渲染阴影贴图时细化几何图形。 这种优化可以提高速度的准确性。

21.4.5影子因子

影子因素是我们添加到照明方程的一个新因素。 阴影因子是标量,范围为0到1.值为0表示点在阴影中,值为1表示点不在阴影中。 使用PCF(第21.4.3节),点也可以部分处于阴影中,此时阴影因子将介于0和1之间。CalcShadowFactor实现位于LightHelper.fx中。

static const float SMAP_SIZE = 2048.0f;
static const float SMAP_DX = 1.0f / SMAP_SIZE;
float CalcShadowFactor(SamplerComparisonState samShadow,
Texture2D shadowMap,
float4 shadowPosH)
{
// Complete projection by doing division by w.
shadowPosH.xyz /= shadowPosH.w;
// Depth in NDC space.
float depth = shadowPosH.z;
// Texel size.
const float dx = SMAP_DX;
float percentLit = 0.0f;
const float2 offsets[9] =
{
float2(-dx, -dx), float2(0.0f, -dx), float2(dx, -dx),
float2(-dx, 0.0f), float2(0.0f, 0.0f), float2(dx, 0.0f),
float2(-dx, +dx), float2(0.0f, +dx), float2(dx, +dx)
};
// 3×3 box filter pattern. Each sample does a 4-tap PCF.
[unroll]
for(int i = 0; i < 9; ++i)
{
percentLit += shadowMap.SampleCmpLevelZero(samShadow,
shadowPosH.xy + offsets[i], depth).r;
}
// Average the samples.
return percentLit /= 9.0f;
}

在我们的模型中,阴影因子将乘以漫反射和镜面光照条件:

// Only the first light casts a shadow.
float3 shadow = float3(1.0f, 1.0f, 1.0f);
shadow[0] = CalcShadowFactor(samShadow, gShadowMap, pin.ShadowPosH);
// Sum the light contribution from each light source.
[unroll]
for(int i = 0; i < gLightCount; ++i)
{
float4 A, D, S;
ComputeDirectionalLight(gMaterial, gDirLights[i],
bumpedNormalW, toEye, A, D, S);
ambient += A;
diffuse += shadow[i]*D;
spec += shadow[i]*S;
}

阴影因子不会影响环境光线,因为这是间接光线,也不会影响来自环境贴图的反射光线。

21.4.6阴影映射测试

我们现在显示在阴影贴图建立后,用于从玩家相机视点绘制场景的效果代码。 关键问题是计算每个像素p的d(p)和s(p)。 通过将点转换为光的NDC空间来找到值d(p);然后z坐标给出来自光源的点的归一化深度值。 通过使用投影纹理将光影贴图投影到场景中,通过光源的视图体积可以找到该值。 请注意,通过这种设置,d(p)和s(p)都在光的NDC空间中测量,因此可以进行比较。 变换矩阵gShadowTransform从局部空间变换到阴影贴图纹理空间(第21.3节)。

相关代码已加粗。

#include "LightHelper.fx"
cbuffer cbPerFrame
{
DirectionalLight gDirLights[3];
float3 gEyePosW;
float gFogStart;
float gFogRange;
float4 gFogColor;
};
cbuffer cbPerObject
{
float4x4 gWorld;
float4x4 gWorldInvTranspose;
float4x4 gWorldViewProj;
float4x4 gTexTransform;
float4x4 gShadowTransform;
Material gMaterial;
};
// Nonnumeric values cannot be added to a cbuffer.
Texture2D gShadowMap;
Texture2D gDiffuseMap;
Texture2D gNormalMap;
TextureCube gCubeMap;
SamplerState samLinear
{
Filter = MIN_MAG_MIP_LINEAR;
AddressU = WRAP;
AddressV = WRAP;
};
SamplerComparisonState samShadow
{
Filter = COMPARISON_MIN_MAG_LINEAR_MIP_POINT;
AddressU = BORDER;
AddressV = BORDER;
AddressW = BORDER;
BorderColor = float4(0.0f, 0.0f, 0.0f, 0.0f);
ComparisonFunc = LESS_EQUAL;
};
struct VertexIn
{
float3 PosL : POSITION;
float3 NormalL : NORMAL;
float2 Tex : TEXCOORD;
float3 TangentL : TANGENT;
};
struct VertexOut
{
float4 PosH : SV_POSITION;
float3 PosW : POSITION;
float3 NormalW : NORMAL;
float3 TangentW : TANGENT;
float2 Tex : TEXCOORD0;
float4 ShadowPosH : TEXCOORD1;
};
VertexOut VS(VertexIn vin)
{
VertexOut vout;
// Transform to world space space.
vout.PosW = mul(float4(vin.PosL, 1.0f), gWorld).xyz;
vout.NormalW = mul(vin.NormalL, (float3x3)gWorldInvTranspose);
vout.TangentW = mul(vin.TangentL, (float3x3)gWorld);
// 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;
// Generate projective tex-coords to project shadow map onto scene.
vout.ShadowPosH = mul(float4(vin.PosL, 1.0f), gShadowTransform);
return vout;
}
float4 PS(VertexOut pin,
uniform int gLightCount,
uniform bool gUseTexure,
uniform bool gAlphaClip,
uniform bool gFogEnabled,
uniform bool gReflectionEnabled) : SV_Target
{
// Interpolating normal can unnormalize it, so normalize it.
pin.NormalW = normalize(pin.NormalW);
// The toEye vector is used in lighting.
float3 toEye = gEyePosW - pin.PosW;
// Cache the distance to the eye from this surface point.
float distToEye = length(toEye);
// Normalize.
toEye /= distToEye;
// Default to multiplicative identity.
float4 texColor = float4(1, 1, 1, 1);
if(gUseTexure)
{
// Sample texture.
texColor = gDiffuseMap.Sample(samLinear, pin.Tex);
f(gAlphaClip)
{
// Discard pixel if texture alpha < 0.1. Note that we
// do this test as soon as possible so that we can
// potentially exit the shader early, thereby skipping
// the rest of the shader code.
clip(texColor.a - 0.1f);
}
}
//
// Normal mapping
//
float3 normalMapSample = gNormalMap.Sample(samLinear, pin.Tex).rgb;
float3 bumpedNormalW = NormalSampleToWorldSpace(
normalMapSample, pin.NormalW, pin.TangentW);
//
// Lighting.
//
float4 litColor = texColor;
if(gLightCount > 0)
{
// Start with a sum of zero.
float4 ambient = float4(0.0f, 0.0f, 0.0f, 0.0f);
float4 diffuse = float4(0.0f, 0.0f, 0.0f, 0.0f);
float4 spec = float4(0.0f, 0.0f, 0.0f, 0.0f);
// Only the first light casts a shadow.
float3 shadow = float3(1.0f, 1.0f, 1.0f);
shadow[0] = CalcShadowFactor(samShadow, gShadowMap, pin.ShadowPosH);
// Sum the light contribution from each light source.
[unroll]
for(int i = 0; i < gLightCount; ++i)
{
float4 A, D, S;
ComputeDirectionalLight(gMaterial, gDirLights[i],
bumpedNormalW, toEye, A, D, S);
ambient += A;
diffuse += shadow[i]*D;
spec += shadow[i]*S;
}
litColor = texColor*(ambient + diffuse) + spec;
if(gReflectionEnabled)
{
float3 incident = -toEye;
float3 reflectionVector = reflect(incident, bumpedNormalW);
float4 reflectionColor = gCubeMap.Sample(
samLinear, reflectionVector);
litColor += gMaterial.Reflect*reflectionColor;
}
}
//
// Fogging
//
if(gFogEnabled)
{
float fogLerp = saturate((distToEye - gFogStart) / gFogRange);
// Blend the fog color and the lit color.
litColor = lerp(litColor, gFogColor, fogLerp);
}
// Common to take alpha from diffuse material and texture.
litColor.a = gMaterial.Diffuse.a * texColor.a;
return litColor;
}

21.4.7渲染阴影贴图

在这个演示中,我们还将阴影贴图渲染到占据屏幕右下角的四边形上。 这使我们能够看到每个帧的阴影贴图的样子。 回想一下,阴影贴图只是D3D11_BIND_SHADER_RESOURCE标志的深度缓冲区纹理,因此它也可以用于纹理表面。 阴影贴图呈现为灰度图像,因为它在每个像素处存储一维值(深度值)。 图21.15显示了“Shadow Map”演示的屏幕截图。


图21.15 “阴影地图”演示的屏幕截图。

21.5大型PCF内核

在本节中,我们讨论使用大型PCF内核时何时发生问题。 我们的演示不使用大的PCF内核,因此本节在某种意义上是可选的,但它引入了一些有趣的想法。

参考图21.16,我们正在计算对于眼睛可见的像素p的阴影测试。 在没有PCF的情况下,我们计算光源p的距离d = d(p),并将其与相应的阴影图值 s0=s(p) s 0 = s ( p ) 进行比较。 使用PCF,我们还将相邻的阴影贴图值 s1s1 s − 1 和 s 1 与d进行比较。 但是,将d与 s1s1 s − 1 和 s 1 进行比较是无效的。纹理元素 s1s1 s − 1 和 s 1 描述场景中不同区域的深度,这些区域可能与p不在同一个多边形上。


图21.16。 将深度d(p)与s0进行比较是正确的,因为纹理元素s0覆盖了p所包含的场景区域。然而,将d(p)与s-1和s1进行比较是不正确的,因为这些纹理元素覆盖了 与p无关的场景。

图21.16中的场景实际上导致了PCF中的错误。 具体来说,当我们进行阴影贴图测试时,我们计算:
lit0=ds0(true)lit1=ds1(true)lit1=ds1(false) l i t 0 = d ≤ s 0 ( t r u e ) l i t − 1 = d ≤ s − 1 ( t r u e ) l i t 1 = d ≤ s 1 ( f a l s e )

当插值结果时,我们得到p是阴影中的1/3,这是不正确的,因为没有任何东西在遮挡p。

从图21.16可以看出,更多的深度偏置可以解决这个错误。 然而,在这个例子中,我们只是在阴影贴图中对隔壁邻居纹素进行采样。 如果我们扩大PCF内核,那么需要更多的偏置。 因此,对于小的PCF内核,只需按照§21.4.2中的解释深入分析就足以解决这个问题,无需担心。 但对于用于制作柔和阴影的大型PCF内核(例如5×5或9×9),这可能成为一个实际问题。

21.5.1 DDX和DDY功能

在我们看到这个问题的近似解决方案之前,我们首先需要讨论ddx和ddy HLSL函数。 这些函数分别估计∂p/∂x和∂p/∂y,其中x是屏幕空间x轴,y是屏幕空间y轴。 利用这些函数,您可以确定每个像素的像素数量p如何随像素而变化。 派生函数可以用于的示例:
1.估计颜色如何逐个像素地变化。
2.估算深度如何逐个像素地变化。
3.估计法线如何逐个像素地变化。
硬件如何估算这些偏导数并不复杂。硬件并行处理2×2四边形像素。然后,可以通过前向差分方程$q_{x+1,y} -q_{x,y}(估计量q如何从像素(x,y)变为像素(x + 1,y))来估计x方向上的偏导数 ,并且类似地对于y方向上的偏导数。

21.5.2解决大型PCF内核问题

我们描述的解决方案来自[Tuft10]。该策略是假定p的相邻像素与p位于同一平面上。这个假设并不总是如此,但它是我们最好的工作。

令p=(u,v,z)为光空间中的坐标。坐标(u,v)用于索引阴影贴图,值z是与阴影贴图测试中使用的光源的距离。我们可以使用位于多边形的切平面中的ddx和ddy来计算矢量 px=(ux,vx,zx)py=(uy,vy,zy) ∂ p ∂ x = ( ∂ u ∂ x , ∂ v ∂ x , ∂ z ∂ x ) 与 ∂ p ∂ y = ( ∂ u ∂ y , ∂ v ∂ y , ∂ z ∂ y ) 。这告诉我们当我们在屏幕空间移动时,我们如何在光线空间中移动。 特别是,如果我们在屏幕空间中移动(△x,△y)单位,我们会沿着切向量的方向移动光照空间中 Δx(ux,vx,zx)+Δy(uy,vy,zy) Δ x ( ∂ u ∂ x , ∂ v ∂ x , ∂ z ∂ x ) + Δ y ( ∂ u ∂ y , ∂ v ∂ y , ∂ z ∂ y ) 的单位。忽略当前的深度项,如果我们在屏幕空间中移动(Δx,Δy)单位,则我们在uv平面上的轻空间中移动 Δx(ux,vx)+Δy(uy,vy) Δ x ( ∂ u ∂ x , ∂ v ∂ x ) + Δ y ( ∂ u ∂ y , ∂ v ∂ y ) 单位;这可以用矩阵方程表示:

[Δx,Δy][uxuyvxvy]=Δx(ux,vx)+Δy(uy,vy)=[Δu,Δv] [ Δ x , Δ y ] [ ∂ u ∂ x ∂ v ∂ x ∂ u ∂ y ∂ v ∂ y ] = Δ x ( ∂ u ∂ x , ∂ v ∂ x ) + Δ y ( ∂ u ∂ y , ∂ v ∂ y ) = [ Δ u , Δ v ]

所以有
[Δx,Δy]=[Δu,Δv][uxuyvxvy]1=[Δu,Δv]1uxvyvxuyvyuyvxux [ Δ x , Δ y ] = [ Δ u , Δ v ] [ ∂ u ∂ x ∂ v ∂ x ∂ u ∂ y ∂ v ∂ y ] − 1 = [ Δ u , Δ v ] 1 ∂ u ∂ x ∂ v ∂ y − ∂ v ∂ x ∂ u ∂ y [ ∂ v ∂ y ∂ v ∂ x ∂ u ∂ y ∂ u ∂ x ]

回忆第二章中的
\left[
\begin{matrix}
A_{11}&A_{12}\\
A_{21}&A_{22}}
\end{matrix}
\right]^{-1}=
\frac{1}{A_{11}A_{22}-A_{12}A_{21}}
\left[
\begin{matrix}
A_{22}&-A_{12}\\
-A_{21}&A_{11}}
\end{matrix}
\right]
\left[\begin{matrix}A_{11}&A_{12}\\A_{21}&A_{22}}\end{matrix}\right]^{-1}=\frac{1}{A_{11}A_{22}-A_{12}A_{21}}\left[\begin{matrix}A_{22}&-A_{12}\\-A_{21}&A_{11}}\end{matrix}\right]

这个新的方程告诉我们,如果我们在uv平面上的光空间中移动(Δu,Δv)单位,那么我们在屏幕空间中移动(Δx,Δy)单位。那么为什么方程21.1对我们很重要? 那么,当我们构建我们的PCF内核时,我们将我们的纹理坐标抵消为在阴影图中对相邻值进行采样:

// Texel size.
const float dx = SMAP_DX;
float percentLit = 0.0f;
const float2 offsets[9] =
{
float2(-dx, -dx), float2(0.0f, -dx), float2(dx, -dx),
float2(-dx, 0.0f), float2(0.0f, 0.0f), float2(dx, 0.0f),
float2(-dx, +dx), float2(0.0f, +dx), float2(dx, +dx)
};
// 3x3 box filter pattern. Each sample does a 4-tap PCF.
[unroll]
for(int i = 0; i < 9; ++i)
{
percentLit += shadowMap.SampleCmpLevelZero(samShadow,
shadowPosH.xy + offsets[i], depth).r;
}

换句话说,我们知道我们知道在uv平面的光空间中有多少位移(Δu,Δv)。 公式21.1告诉我们,当我们在光空间中移动(Δu,Δv)单位时,我们在屏幕空间中移动(Δx,Δy)。

现在,让我们回到我们一直忽略的深度术语。如果我们在屏幕空间中移动(Δx,Δy)单位,则光照空间深度会移动 Δz=Δxzx+Δyzy Δ z = Δ x ∂ z ∂ x + Δ y ∂ z ∂ y 。因此,当我们抵消我们的纹理坐标来完成PCF时,我们可以相应地修改深度测试中使用的深度值: z=z+z z ′ = z + △ z (见图21.17)。


图21.17 为简单起见,我们在2D中进行说明 如果我们在u方向上偏移Δu得到(u +Δu,z),那么我们需要偏移Δz以保留在多边形上以得到p’=(u + Δu,z +Δz)。

让我们总结一下:
1.在我们的PCF实现中,我们将纹理坐标抵消为在阴影贴图中对相邻值进行采样。 因此,对于每个样本,我们知道(Δu,Δv)。
2.我们可以使用公式21.1来查找光空间中偏移(Δu,Δv)单位时的屏幕空间偏移量(Δx,Δy)。
3.用(Δx,Δy)求解, Δz=Δxzx+Δyzy Δ z = Δ x ∂ z ∂ x + Δ y ∂ z ∂ y 适用于计算亮空间深度变化。
DirectX 11 SDK中的“CascadedShadowMaps11”演示在CalculateRightAndUpTexelDepthDeltas和CalculatePCFPercentLit函数中实现了此方法。

21.5.3 大型PCF核问题的另一种解决方案

[Isidoro06]中介绍的这种解决方案与前一节的精神相同,但采用了稍微不同的方法。

令p=(u,v,z)为光空间中的坐标。坐标(u,v)用于索引阴影贴图,值z是与阴影贴图测试中使用的光源的距离。我们可以用ddx和ddy来计算 px=(ux,vx,zx)py=(uy,vy,zy) ∂ p ∂ x = ( ∂ u ∂ x , ∂ v ∂ x , ∂ z ∂ x ) 和 ∂ p ∂ y = ( ∂ u ∂ y , ∂ v ∂ y , ∂ z ∂ y )

特别是,我们可以采用这些衍生物的事实意味着u=u(x,y),v=v(x,y)并且z=z(x,y)都是x和y的函数。但是,我们也可以把z看作u和v的函数,即z=z(u,v),当我们在u和v方向的光空间中移动时,深度z沿着多边形平面变化。通过连锁规则,我们有:

zx=zuux+zvvxzy=zuuy+zvvy ∂ z ∂ x = ∂ z ∂ u ∂ u ∂ x + ∂ z ∂ v ∂ v ∂ x ∂ z ∂ y = ∂ z ∂ u ∂ u ∂ y + ∂ z ∂ v ∂ v ∂ y

或以矩阵符号表示:
\left[ 
\frac{∂z}{∂x} \frac{∂z}{∂y}
\right]=
\left[ 
{\frac{∂z}{∂u}} \frac{∂z}{∂v}
\right]
\left[
 begin{matrix}
  \frac{∂u}{∂x}&\frac{∂u}{∂y} \\
  \frac{∂v}{∂x}&\frac{∂v}{∂y}  
\end{matrix}
\right]
\left[ \frac{∂z}{∂x} \frac{∂z}{∂y}\right]=\left[ {\frac{∂z}{∂u}} \frac{∂z}{∂v}\right]\left[ begin{matrix}  \frac{∂u}{∂x}&\frac{∂u}{∂y} \\  \frac{∂v}{∂x}&\frac{∂v}{∂y}  \end{matrix}\right]

取相反的结果:
\left[ \frac{∂z}{∂u} \frac{∂z}{∂v}\right]=\left[ \frac{∂z}{∂x} \frac{∂z}{∂y}\right]
\left[
 begin{matrix}
  \frac{∂u}{∂x}&\frac{∂u}{∂y} \\
  \frac{∂v}{∂x}&\frac{∂v}{∂y}  
\end{matrix}
\right]^{-1}\\
=\frac{\left[ \frac{∂z}{∂x} \frac{∂z}{∂y}\right]}{\frac{∂u}{∂x}\frac{∂v}{∂y}+\frac{∂u}{∂y}\frac{∂v}{∂x}}
\left[
 begin{matrix}
  \frac{∂v}{∂y}&-\frac{∂u}{∂y} \\
  -\frac{∂v}{∂x}&\frac{∂u}{∂x}  
\end{matrix}
\right]
\left[ \frac{∂z}{∂u} \frac{∂z}{∂v}\right]=\left[ \frac{∂z}{∂x} \frac{∂z}{∂y}\right]\left[ begin{matrix}  \frac{∂u}{∂x}&\frac{∂u}{∂y} \\  \frac{∂v}{∂x}&\frac{∂v}{∂y}  \end{matrix}\right]^{-1}\\=\frac{\left[ \frac{∂z}{∂x} \frac{∂z}{∂y}\right]}{\frac{∂u}{∂x}\frac{∂v}{∂y}+\frac{∂u}{∂y}\frac{∂v}{∂x}}\left[ begin{matrix}  \frac{∂v}{∂y}&-\frac{∂u}{∂y} \\  -\frac{∂v}{∂x}&\frac{∂u}{∂x}  \end{matrix}\right]

我们现在已经解决了 zuzv ∂ z ∂ u 和 ∂ z ∂ v (等式右边的所有内容都是已知的)。如果我们在光照空间的uv平面上移动(△u,△v)单位,那么光照空间深度就会移动 z=uzu+vzv △ z =△ u ∂ z ∂ u + △ v ∂ z ∂ v

因此,采用这种方法,我们不必转换到屏幕空间,但可以保持在光线空间中 - 原因是我们直接了解u和v变化时深度如何变化,而在上一节中,我们只知道深度 当x和y在屏幕空间中更改时更改。

21.6总结

1.后台缓存不一定是渲染目标;我们可以渲染到不同的纹理。渲染纹理为GPU提供了在运行时更新纹理内容的有效方法。渲染到纹理后,我们可以将纹理绑定为着色器输入并将其映射到几何图形上。许多特殊效果需要渲染纹理功能,如阴影贴图,水模拟和通用GPU编程。
2.对于正投影,观察体积是一个盒子(见图21.1),其宽度为w,高度为h,靠近平面n,远平面为f,投影线平行于视空间z轴。这种投影主要用于三维科学或工程应用,在投影后希望平行线保持平行。但是,我们可以使用正交投影来模拟平行光产生的阴影。
3.投影纹理是所谓的,因为它允许我们将纹理投影到任意几何图形上,就像投影机一样。投影纹理的关键是为每个像素生成纹理坐标,以便应用的纹理看起来像已投影到几何图形上。这种纹理坐标被称为投影纹理坐标。通过投影到投影仪的投影平面上,然后将其映射到纹理坐标系,我们获得像素的投影纹理坐标。
4.阴影映射是一种实时阴影技术,可以遮蔽任意几何图形(不限于平面阴影)。阴影映射的想法是将光线的视点深度渲染成阴影图;因此,在此之后,阴影图存储从光的角度可见的所有像素的深度。然后,我们再次从相机的角度渲染场景,并使用投影纹理将阴影贴图投影到场景中。令s(p)为从阴影图投影到像素p上的深度值,并设d(p)为来自光源的像素深度。那么如果d(p)> s(p),即如果像素的深度大于投影像素深度s(p),则p在阴影中,那么必须存在靠近遮挡p的光的像素,从而在阴影中投射p。
5.阴影贴图是最大的挑战。阴影图存储最近可见像素相对于其相关光源的深度。但是,阴影图只有一些有限的分辨率。所以每个阴影贴图texel都对应于场景的一个区域。因此,阴影图仅仅是从光线角度来看场景深度的离散采样。这会导致称为阴影痤疮的别名问题。使用图形硬件对斜率缩放偏移(在光栅化渲染状态)的固有支持是解决阴影痤疮的常见策略。阴影贴图的有限分辨率也会在阴影边缘造成混叠。 PCF是一个流行的解决方案。用于混叠问题的更高级的解决方案是级联阴影贴图和方差阴影贴图。

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
在Unity中,阴影接收器是指能够接收其他物体投射的阴影的物体。为了实现阴影效果,我们需要在shader中对阴影映射纹理进行采样,并将采样结果与最终的光照结果相乘,从而产生阴影效果。\[1\] 在实时渲染中,Unity使用ShadowMap技术来计算光源的阴影映射纹理。首先,将相机的位置放置在光源的位置上,这样阴影区域就是相机看不到的地方。Unity会为光源计算阴影映射纹理,它本质上是一张深度图。为了更新映射纹理,需要在shader中添加一个额外的pass,并将LightMode设置为"ShadowCaster",只有这样才能投射阴影。\[2\] 然而,在之前的fallback中提供的shadowcaster实现往往不能实现透明物体的阴影。因此,在透明度测试的shader中,我们需要添加关于阴影的计算,并将fallback修改为vertexlit,以确保透明物体的阴影计算正确。\[3\] #### 引用[.reference_title] - *1* [shader学习摘要(九)unity阴影](https://blog.csdn.net/overwhelming_kda/article/details/126428963)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control_2,239^v3^insert_chatgpt"}} ] [.reference_item] - *2* *3* [UnityShader入门精要-阴影](https://blog.csdn.net/weixin_58624886/article/details/126124585)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control_2,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值