阴影既暗示着光源相对于观察者的位置关系,也从侧面传达了场景中各物体之间的相对位置。本章将起底最基础的阴影映射算法,而像复杂如级联阴影映射这样的技术,也是在阴影映射的基础上发展而来的。html
学习目标:git
DirectX11 With Windows SDK完整目录github
欢迎加入QQ群: 727623616 能够一块儿探讨DX11,以及有什么问题也能够在这里汇报。windows
阴影映射技术的核心思想其实不复杂。对于场景中的一点,若是该点可以被摄像机观察到,却不能被光源定义的虚拟摄像机所观察到,那么场景中的这一点则能够被断定为光源所照射不到的阴影区域。缓存
如下图为例,眼睛观察到地面上最左边的一点,而且从光源处观察也能看到该点。所以该点不会产生阴影。ide
再看下面的图,眼睛能够观察到地面上中间那一点,可是从光源处观察不能看到该点。所以该点会产生阴影。函数
具体落实下来应该怎么作呢?对于点光源来讲,因为它的光是朝全部方向四射散开的,但为了方便,咱们能够像摄像机那样选取视锥体区域(使用一个观察矩阵 + 透视投影矩阵来定义),而后通过正常的变换后就能计算出光源到区域内物体的深度值;而对于平行光(方向光)来讲,咱们能够采用正交投影的方式来选取一个矩形区域(使用一个观察矩阵 + 正交投影矩阵定义)。和通常的渲染流程不一样的是,咱们只须要记录深度值到深度缓冲区,而不须要将颜色绘制到后备缓冲区。学习
阴影贴图技术也是一种变相的“渲染到纹理”技术。它以光源的视角来渲染场景深度信息,即在光源处有一个虚拟摄像机,它将观察到的物体的深度信息保存到深度缓冲区中。这样咱们就能够知道那些离光源最近的像素片元信息,同时这些点天然是不在阴影范围之中。测试
一般该技术须要用到一个深度/模板缓冲区、一个与之对应的视口、针对该深度/模板缓冲区的着色器资源视图(SRV)和深度/模板视图(DSV),而用于阴影贴图的那个深度/模板缓冲区也被称为阴影贴图。
在考虑点光源的投影和方向光的投影时可能会有些困难,但这两个问题其实能够转化成虚拟摄像机的透视投影和正交投影。
对于透视投影来讲,其实咱们也已经很是熟悉了。在这种作法下咱们只考虑虚拟摄像机的视锥体区域(即尽管点光源是朝任意方向照射的,但咱们只看点光源往该视锥体范围内照射的区域),而后对物体惯例进行世界变换、以光源为视角的观察变换、光源的透视投影变换,这样物体就被变换到了以光源为视角的NDC空间。
而对于正交投影而言,咱们也是同样的作法。正交投影的视景体是一个轴对齐于观察坐标系的长方体。尽管咱们很差描述一个方向光的光源,但为了方便,咱们把光源定义在视景体xOy切面中心所处的那条直线上。这样咱们就只须要给出视景体的宽度、高度、近平面、远平面信息就能够构造出一个正交投影矩阵了。
咱们能够看到,正交投影的投影线均平行于观察空间的z轴。
正交投影矩阵在第四章变换已经讲过,就再也不赘述。
投影纹理贴图技术可以将纹理投射到任意形状的几何体上,又由于其原理与投影机的工做方式比较类似,由此得名。例以下图中,右边的骷髅头纹理被投射到左边场景中的多个几何体上。
投影纹理贴图的关键在于为每一个像素生成对应的投影纹理坐标,从视觉上给人一种纹理被投射到几何体上的感受。
下图是光源观察的视野,其中点p是待渲染的一点,而纹理坐标(u, v)则指定了应当被投射到3D点p上的纹素,而且坐标(u, v)与投影到屏幕上的NDC坐标有特定联系。咱们能够将投影纹理坐标的生成过程分为以下步骤:
而步骤2中的变换过程则取决于下面的坐标变换:
即从x, y∈[-1, 1]映射到u, v∈[0, 1]。(y轴和v轴是相反的)
这种线性变换能够用矩阵表示:
那么物体上的一点p从局部坐标系到最终的纹理坐标点t的变换过程为:
这里补上了世界变换矩阵,是由于这一步容易在后面的代码实践中被漏掉。但此时的t还须要通过透视除法,才是咱们最终须要的纹理坐标。
下面的HLSL代码展现了顶点着色器计算投影纹理坐标的过程:
// 顶点着色器 VertexPosHWNormalTexShadowPosH VS(VertexPosNormalTex vIn) { VertexPosHWNormalTexShadowPosH vOut; matrix viewProj = mul(g_View, g_Proj); vector posW = mul(float4(vIn.PosL, 1.0f), g_World); vOut.PosW = posW.xyz; // ... // 把顶点变换到光源的投影空间 vOut.ShadowPosH = mul(posW, g_ShadowTransform); return vOut; } // 像素着色器 float4 PS(VertexPosHWNormalTexShadowPosH pIn) : SV_Target { // 透视除法 pIn.ShadowPosH.xyz /= pIn.ShadowPosH.w; // NDC空间中的深度值 float depth = pIn.ShadowPosH.z; // 经过投影纹理坐标来对纹理采样 // 采样出的r份量即为光源观察该点时的深度值 float4 c = g_ShadowMap.Sample(g_Sam, pIn.ShadowPosH.xy); // ... }
在渲染管线中,位于视锥体以外的几何体是要被裁剪掉的。可是,在咱们以光源设置的视角投影几何体而为之生成投影纹理坐标时,并不须要执行裁剪操做——只须要简单投影顶点便可。所以,位于视锥体以外的几何体顶点会获得[0, 1]区间以外的投影纹理坐标。而后具体的采样行为则须要依赖于咱们设置的采样器。
通常来讲,咱们并不但愿对位于视锥体外的几何体顶点进行贴图,由于这并无任何意义。考虑到可视深度在NDC空间的最大值为1.0f,咱们能够采用边界深度值为1.0f的边框寻址模式。
另外一种作法则是结合聚光灯的策略,使聚光灯照射范围以外的部分不受光照,亦即不在阴影的计算范围内。
来到正交投影,由于咱们依然是要计算出NDC坐标,对于NDC空间范围外的点,咱们依然能够采用上面的寻址模式策略,但聚光灯的策略就不适用了。
此外,正交投影无需进行透视除法,由于正交投影后的坐标w值老是1.0f。但保留透视除法可让咱们的这套着色器能够同时工做在正交投影和透视投影上。若是没有透视除法,则只能在正交投影中工做。
既然阴影贴图和RTT有着许多类似的地方,那何不把它也放到TextureRender里面共用呢?只要添加一个开关控制该RTT是否用做阴影贴图便可。
class TextureRender { public: template<class T> using ComPtr = Microsoft::WRL::ComPtr<T>; TextureRender() = default; ~TextureRender() = default; // 不容许拷贝,容许移动 TextureRender(const TextureRender&) = delete; TextureRender& operator=(const TextureRender&) = delete; TextureRender(TextureRender&&) = default; TextureRender& operator=(TextureRender&&) = default; HRESULT InitResource(ID3D11Device* device, int texWidth, int texHeight, bool shadowMap = false, bool generateMips = false); // 开始对当前纹理进行渲染 // 阴影贴图无需提供背景色 void Begin(ID3D11DeviceContext* deviceContext, const FLOAT backgroundColor[4]); // 结束对当前纹理的渲染,还原状态 void End(ID3D11DeviceContext * deviceContext); // 获取渲染好的纹理的着色器资源视图 // 阴影贴图返回的是深度缓冲区 // 引用数不增长,仅用于传参 ID3D11ShaderResourceView* GetOutputTexture(); // 设置调试对象名 void SetDebugObjectName(const std::string& name); private: ComPtr<ID3D11ShaderResourceView> m_pOutputTextureSRV; // 输出的纹理(或阴影贴图)对应的着色器资源视图 ComPtr<ID3D11RenderTargetView> m_pOutputTextureRTV; // 输出的纹理对应的渲染目标视图 ComPtr<ID3D11DepthStencilView> m_pOutputTextureDSV; // 输出纹理所用的深度/模板视图(或阴影贴图) D3D11_VIEWPORT m_OutputViewPort = {}; // 输出所用的视口 ComPtr<ID3D11RenderTargetView> m_pCacheRTV; // 临时缓存的后备缓冲区 ComPtr<ID3D11DepthStencilView> m_pCacheDSV; // 临时缓存的深度/模板缓冲区 D3D11_VIEWPORT m_CacheViewPort = {}; // 临时缓存的视口 bool m_GenerateMips = false; // 是否生成mipmap链 bool m_ShadowMap = false; // 是否为阴影贴图 };
在做为RTT时,须要建立纹理与它的SRV和RTV、深度/模板缓冲区和它的DSV、视口
而做为阴影贴图时,须要建立深度缓冲区与它的SRV和DSV、视口
下面的代码只关注建立阴影贴图的部分:
HRESULT TextureRender::InitResource(ID3D11Device* device, int texWidth, int texHeight, bool shadowMap, bool generateMips) { // 防止重复初始化形成内存泄漏 m_pOutputTextureSRV.Reset(); m_pOutputTextureRTV.Reset(); m_pOutputTextureDSV.Reset(); m_pCacheRTV.Reset(); m_pCacheDSV.Reset(); m_ShadowMap = shadowMap; m_GenerateMips = false; HRESULT hr; // ... // ****************** // 建立与纹理等宽高的深度/模板缓冲区或阴影贴图,以及对应的视图 // CD3D11_TEXTURE2D_DESC texDesc((m_ShadowMap ? DXGI_FORMAT_R24G8_TYPELESS : DXGI_FORMAT_D24_UNORM_S8_UINT), texWidth, texHeight, 1, 1, D3D11_BIND_DEPTH_STENCIL | (m_ShadowMap ? D3D11_BIND_SHADER_RESOURCE : 0)); ComPtr<ID3D11Texture2D> depthTex; hr = device->CreateTexture2D(&texDesc, nullptr, depthTex.GetAddressOf()); if (FAILED(hr)) return hr; CD3D11_DEPTH_STENCIL_VIEW_DESC dsvDesc(depthTex.Get(), D3D11_DSV_DIMENSION_TEXTURE2D, DXGI_FORMAT_D24_UNORM_S8_UINT); hr = device->CreateDepthStencilView(depthTex.Get(), &dsvDesc, m_pOutputTextureDSV.GetAddressOf()); if (FAILED(hr)) return hr; if (m_ShadowMap) { // 阴影贴图的SRV CD3D11_SHADER_RESOURCE_VIEW_DESC srvDesc(depthTex.Get(), D3D11_SRV_DIMENSION_TEXTURE2D, DXGI_FORMAT_R24_UNORM_X8_TYPELESS); hr = device->CreateShaderResourceView(depthTex.Get(), &srvDesc, m_pOutputTextureSRV.GetAddressOf()); if (FAILED(hr)) return hr; } // ****************** // 初始化视口 // m_OutputViewPort.TopLeftX = 0.0f; m_OutputViewPort.TopLeftY = 0.0f; m_OutputViewPort.Width = static_cast<float>(texWidth); m_OutputViewPort.Height = static_cast<float>(texHeight); m_OutputViewPort.MinDepth = 0.0f; m_OutputViewPort.MaxDepth = 1.0f; return S_OK; }
须要注意的是,在建立深度缓冲区时,若是还想为他建立SRV,就不能将DXGI格式定义成DXGI_FORMAT_D24_UNORM_S8_UINT
这些带D的类型,而应该是DXGI_FORMAT_R24G8_TYPELESS
而后在建立阴影贴图的SRV时,则须要指定为DXGI_FORMAT_R24_UNORM_X8_TYPELESS
开始阴影贴图的渲染前,不须要设置RTV,只须要绑定DSV。
void TextureRender::Begin(ID3D11DeviceContext* deviceContext, const FLOAT backgroundColor[4]) { // 缓存渲染目标和深度模板视图 deviceContext->OMGetRenderTargets(1, m_pCacheRTV.GetAddressOf(), m_pCacheDSV.GetAddressOf()); // 缓存视口 UINT num_Viewports = 1; deviceContext->RSGetViewports(&num_Viewports, &m_CacheViewPort); // 清空缓冲区 // ... deviceContext->ClearDepthStencilView(m_pOutputTextureDSV.Get(), D3D11_CLEAR_DEPTH | (m_ShadowMap ? 0 : D3D11_CLEAR_STENCIL), 1.0f, 0); // 设置渲染目标和深度模板视图 deviceContext->OMSetRenderTargets((m_ShadowMap ? 0 : 1), (m_ShadowMap ? nullptr : m_pOutputTextureRTV.GetAddressOf()), m_pOutputTextureDSV.Get()); // 设置视口 deviceContext->RSSetViewports(1, &m_OutputViewPort); }
渲染完成后,和往常同样还原便可。
阴影图存储的是距离光源最近的可视像素深度值,可是它的分辨率有限,致使每个阴影图纹素都要表示场景中的一片区域。所以,阴影图只是以光源视角针对场景深度进行的离散采样,这将会致使所谓的阴影粉刺等图像走样问题。以下图所示(注意图中地面上光影之间轮流交替的“阶梯状”条纹):
而下图则简单展现了为何会发生阴影粉刺这种现象。因为阴影图的分辨率有限,因此每一个阴影图纹素要对应于长江中的一块区域(而不是点对点的关系,一个坡面表明阴影图中一个纹素的对应范围)。从观察点E查看场景中的两个点p1与p2,它们分别对应于两个不一样的屏幕像素。可是,从光源的观察角度来看,它们却都有着相同的阴影图纹素(即s(p1)=s(p2)=s,因为分辨率的缘由)。当咱们在执行阴影图检测时,会获得d(p1) > s 及 d(p2) <= s这两个测试结果,这样一来,p1将会被绘制为如同它在阴影中的颜色,p2将被渲染为好似它在阴影以外的颜色,从而致使阴影粉刺。
所以,咱们能够经过偏移阴影图中的深度值来防止出现错误的阴影效果。此时咱们就能够保证d(p1) <= s 及 d(p2) <= s。可是寻找合适的深度偏移须要反复尝试。
偏移量过大会致使名为peter-panning(彼得·潘,即小飞侠,他曾在一次逃跑时弄丢了本身的影子)的失真效果,使得阴影看起来与物体相分离。
然而,并无哪种固定的偏移量能够正确地运用于全部几何体的阴影绘制。特别是下图那种(从光源的角度来看)有着极大斜率的三角形,这时候就须要选取更大的偏移量。可是,若是试图经过一个过大的深度偏移量来处理全部的斜边,则又会形成peter-panning问题。
所以,咱们绘制阴影的方式就是先以光源视角度量多边形斜面的斜率,并为斜率较大的多边形应用更大的偏移量。而图形硬件内部对此有相关技术的支持,咱们经过名为斜率缩放偏移的光栅化状态属性就可以轻松实现。
typedef struct D3D11_RASTERIZER_DESC { // ... INT DepthBias; FLOAT DepthBiasClamp; FLOAT SlopeScaledDepthBias; BOOL DepthClipEnable; // ... } D3D11_RASTERIZER_DESC;
DepthBias
:一个固定的应用偏移量。DepthBiasClamp
:所容许的最大深度偏移量。以此来设置深度偏移量的上限。不难想象,及其陡峭的倾斜度会致使斜率缩放偏移量过大,从而形成peter-panning失真SlopeScaledDepthBias
:根据多边形的斜率来控制偏移程度的缩放因子。注意,在将场景渲染至阴影贴图时,便会应用该斜率缩放偏移量。这是因为咱们但愿以光源的视角基于多边形的斜率而进行偏移操做,从而避免阴影失真。所以,咱们就会对阴影图中的数值进行偏移计算(即由硬件将像素的深度值与偏移值相加)。在本Demo中采用的具体数值以下:
// [出自MSDN] // 若是当前的深度缓冲区采用UNORM格式而且绑定在输出合并阶段,或深度缓冲区尚未被绑定 // 则偏移量的计算过程以下: // // Bias = (float)DepthBias * r + SlopeScaledDepthBias * MaxDepthSlope; // // 这里的r是在深度缓冲区格式转换为float32类型后,其深度值可取到大于0的最小可表示的值 // MaxDepthSlope则是像素在水平方向和竖直方向上的深度斜率的最大值 // [结束MSDN引用] // // 对于一个24位的深度缓冲区来讲, r = 1 / 2^24 // // 例如:DepthBias = 100000 ==> 实际的DepthBias = 100000/2^24 = .006 // // 本Demo中的方向光始终与地面法线呈45度夹角,故取斜率为1.0f // 如下数据极其依赖于实际场景,所以咱们须要对特定场景反复尝试才能找到最合适 rsDesc.DepthBias = 100000; rsDesc.DepthBiasClamp = 0.0f; rsDesc.SlopeScaledDepthBias = 1.0f
注意:深度偏移发生在光栅化期间(裁剪以后),所以不会对几何体裁剪形成影响。
在RenderStates
中咱们添加了这样一个光栅化状态:
// 深度偏移模式 rasterizerDesc.FillMode = D3D11_FILL_SOLID; rasterizerDesc.CullMode = D3D11_CULL_BACK; rasterizerDesc.FrontCounterClockwise = false; rasterizerDesc.DepthClipEnable = true; rasterizerDesc.DepthBias = 100000; rasterizerDesc.DepthBiasClamp = 0.0f; rasterizerDesc.SlopeScaledDepthBias = 1.0f; HR(device->CreateRasterizerState(&rasterizerDesc, RSDepth.GetAddressOf()));
MSDN文档Depth Bias讲述了该技术相关的所有规则,而且介绍了如何使用浮点深度缓冲区进行工做。
在使用投影纹理坐标(u, v)对阴影图进行采样时,每每不会命中阴影图中纹素的准确位置,而是一般位于阴影图中的4个纹素之间。然而,咱们不该该对深度值采用双线性插值法,由于4个纹素之间的深度值不必定知足线性过渡,插值出来的深度值跟实际的深度值有误差,这样可能会致使把像素错误标入阴影中这样的错误结果(所以咱们也不能为阴影图生成mipmap)。
出于这样的缘由,咱们应该对采样的结果进行插值,而不是对深度值进行插值。这种作法称为——百分比渐近过滤。即咱们以点过滤(MIN_MAG_MIP_POINT
)的方式在坐标(u, v)、(u+△x, v)、(u, v+△x)、(u+△x, v+△x)处对纹理进行采样,其中△x=1/SHADOW_MAP_SIZE(除以的是引用贴图的宽高)。因为是点采样,这4个采样点分别命中的是围绕坐标(u, v)最近的4个阴影图纹素s0、s一、s二、s3,以下图所示。
接下来,咱们会对这些采集的深度值进行阴影图检测,并对测试的结果展开双线性插值。
static const float SMAP_SIZE = 2048.0f; static const float SMAP_DX = 1.0f / SMAP_SIZE; // ... // // 采样操做 // // 对阴影图进行采样以获取离光源最近的深度值 float s0 = g_ShadowMap.Sample(g_SamShadow, tex.xy).r; float s1 = g_ShadowMap.Sample(g_SamShadow, tex.xy + float2(SMAP_DX, 0)).r; float s2 = g_ShadowMap.Sample(g_SamShadow, tex.xy + float2(0, SMAP_DX)).r; float s3 = g_ShadowMap.Sample(g_SamShadow, tex.xy + float2(SMAP_DX, SMAP_DX)).r; // 该像素的深度值是否小于等于阴影图中的深度值 float r0 = (depth <= s0); float r1 = (depth <= s1); float r2 = (depth <= s2); float r3 = (depth <= s3); // // 双线性插值操做 // // 变换到纹素空间 float2 texelPos = SMAP_SIZE * tex.xy; // 肯定插值系数(frac()返回浮点数的小数部分) float2 t = frac(texelPos); // 对比较结果进行双线性插值 return lerp(lerp(r0, r1, t.x), lerp(r2, r3, t.x), t.y);
若采用这种计算方法,则一个像素就可能局部处于阴影之中,而不是非0即1.例如,如有4个样本,三个在阴影中,一个在阴影外,那么该像素有75%处于阴影之中。这就让阴影内外的像素之间有了更加平滑的过渡,而不是棱角分明。
但这种过滤方法产生的阴影看起来仍然很是生硬,且锯齿失真问题的最终处理效果仍是不能使人十分满意。PCF的主要缺点是须要4个纹理样本,而纹理采样自己就是现代GPU代价较高的操做之一,由于存储器的带宽与延迟并无随着GPU计算能力的剧增而获得相近程度的巨大改良。幸运的是,Direct3D 11+版本的图形硬件对PCF技术已经有了内部支持,上面的一大堆代码能够用SampleCmpLevelZero
函数来替代。
float percentage = g_ShadowMap.SampleCmpLevelZero(g_SamShadow, shadowPosH.xy, depth).r;
方法中的LevelZero
部分意味着它只能在最高的mipmap层级中进行采样。另外,该方法使用的并不是通常的采样器对象,而是比较采样器。这使得硬件可以执行阴影图的比较测试,而且须要在过滤采样结果以前完成。对于PCF技术来讲,咱们须要使用的是D3D11_FILTER_COMPARISON_MIN_MAG_LINEAR_MIP_POINT
过滤器,并将比较函数设置为LESS_EQUAL
(因为对深度值进行了偏移,因此也要用到LESS
比较函数)。
函数中传入的depth
将会出如今比较运算符的左边,即:
depth <= sampleDepth
在RenderStates
中咱们添加了这样一个采样器:
ComPtr<ID3D11SamplerState> RenderStates::SSShadow = nullptr; // 采样器状态:深度比较与Border模式 ZeroMemory(&sampDesc, sizeof(sampDesc)); sampDesc.Filter = D3D11_FILTER_COMPARISON_MIN_MAG_LINEAR_MIP_POINT; sampDesc.AddressU = D3D11_TEXTURE_ADDRESS_BORDER; sampDesc.AddressV = D3D11_TEXTURE_ADDRESS_BORDER; sampDesc.AddressW = D3D11_TEXTURE_ADDRESS_BORDER; sampDesc.ComparisonFunc = D3D11_COMPARISON_LESS_EQUAL; sampDesc.BorderColor[0] = { 1.0f }; sampDesc.MinLOD = 0; sampDesc.MaxLOD = D3D11_FLOAT32_MAX; HR(device->CreateSamplerState(&sampDesc, SSShadow.GetAddressOf()));
注意:根据SDK文档所述,只有
R32_FLOAT_X8X24_TYPELESS
、R32_FLOAT
,R24_UNORM_X8_TYPELESS
、R16_UNORM
格式才能用于比较过滤器。
到目前为止,咱们在本节中一直使用的是4-tap PCF核(输入4个样原本执行的PCF)。PCF核越大,阴影的边缘轮廓也就越丰满、越平滑,固然,花费在SampleCmpLevelZero
函数上的开销也就越大。在本Demo中,咱们是按3x3正方形的均值滤波方式来执行PCF。因为每次调用SampleCmpLevelZero
函数实际所执行的都是4-tap PCF,因此一共采样了36次,其中有4x4个独立采样点。此外,采用过大的滤波核还会致使以前所述的阴影粉刺问题,但本章不打算讲述,有兴趣能够回到龙书阅读(过大的PCF核)。
显然,PCF技术通常来讲只需在阴影的边缘进行,由于阴影内外两部分并不涉及混合操做(只有阴影边缘才是渐变的)。基于此,只要能对阴影边缘的PCF设计相应的处理方案就行了。但这种作法通常要求咱们所用的PCF核足够大(5x5及更大)时才划算(由于动态分支也有开销)。不过最终是要效率仍是要画质仍是取决于你本身。
注意:实际工程中所用的PCF核不必定是方形的过滤栅格。很多文献也指出,随机的拾取点也能够做为PCF核。
考虑到在作比较时,若是处于阴影外的值为1,在阴影内的值为0,在采用SampleCmpLevelZero
和均值滤波后,咱们用范围值0~1来表示处于阴影外的程度。随着值的增长,该点也变得越亮。咱们可使用下面的函数来计算3x3正方形的均值滤波下的阴影因子:
float CalcShadowFactor(SamplerComparisonState samShadow, Texture2D shadowMap, float4 shadowPosH) { // 透视除法 shadowPosH.xyz /= shadowPosH.w; // NDC空间的深度值 float depth = shadowPosH.z; // 纹素在纹理坐标下的宽高 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) }; [unroll] for (int i = 0; i < 9; ++i) { percentLit += shadowMap.SampleCmpLevelZero(samShadow, shadowPosH.xy + offsets[i], depth).r; } return percentLit /= 9.0f; }
而后在咱们的光照模型中,只有第一个方向光才参与到阴影的计算,而且阴影因子将与直接光照(漫反射和镜面反射光)项相乘。
// ... float shadow[5] = { 1.0f, 1.0f, 1.0f, 1.0f, 1.0f }; // 仅第一个方向光用于计算阴影 shadow[0] = CalcShadowFactor(g_SamShadow, g_ShadowMap, pIn.ShadowPosH); [unroll] for (i = 0; i < 5; ++i) { ComputeDirectionalLight(g_Material, g_DirLight[i], pIn.NormalW, toEyeW, A, D, S); ambient += A; diffuse += shadow[i] * D; spec += shadow[i] * S; } // ...
因为环境光是间接光,因此阴影因子不受影响。而且,阴影因子也不会对来自环境映射的反射光构成影响。
本章开始的代码引入了EffectHelper
来管理着色器所需的资源(咱们能够无需手动建立并交给它来托管),并应用在了全部的Effect
类当中。除了IEffect
接口类,目前还引入了IEffectTransform
接口类来统一变换的设置。随着抽象类的增长,像GameObject
这样的类就能够对IEffect
接口类对象查询是否有某一特定接口类或具体类来执行额外的复杂操做。
此外,SkyRender
类也所以有了轻微的变更。具体想了解仍是去源码翻阅,这里不展开。
首先咱们要在GameApp::InitResource
中建立一副2048x2048的阴影贴图:
m_pShadowMap = std::make_unique<TextureRender>(); HR(m_pShadowMap->InitResource(m_pd3dDevice.Get(), 2048, 2048, true));
在本Demo中,光照方向每帧都在变更,咱们但愿让投影立方体与光照所属的变换轴对齐,而且中心可以坐落在原点。所以在GameApp::UpdateScene
能够这么作:
// // 投影区域为正方体,以原点为中心,以方向光为+Z朝向 // XMMATRIX LightView = XMMatrixLookAtLH(dirVec * 20.0f * (-2.0f), g_XMZero, g_XMIdentityR1); m_pShadowEffect->SetViewMatrix(LightView); // 将NDC空间 [-1, +1]^2 变换到纹理坐标空间 [0, 1]^2 static 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); // S = V * P * T m_pBasicEffect->SetShadowTransformMatrix(LightView * XMMatrixOrthographicLH(40.0f, 40.0f, 20.0f, 60.0f) * T);
至于绘制部分,本Demo将和阴影有联系的场景对象放入了另外一个重载函数DrawScene
中(具体实现不在这给出),整体状况以下:
void GameApp::DrawScene() { // ... // ****************** // 绘制到阴影贴图 m_pShadowMap->Begin(m_pd3dImmediateContext.Get(), nullptr); { DrawScene(true); } m_pShadowMap->End(m_pd3dImmediateContext.Get()); // ****************** // 正常绘制场景 m_pBasicEffect->SetTextureShadowMap(m_pShadowMap->GetOutputTexture()); DrawScene(false, m_EnableNormalMap); // 绘制天空盒 m_pDesert->Draw(m_pd3dImmediateContext.Get(), *m_pSkyEffect, *m_pCamera); // 解除深度缓冲区绑定 m_pBasicEffect->SetTextureShadowMap(nullptr); m_pBasicEffect->Apply(m_pd3dImmediateContext.Get()); // ... }
本Demo提供了5种斜率下的方向光,对应主键盘数字键1-5,Q键开关法线贴图,E键开关阴影贴图的显示,G键切换阴影贴图的显示模式。
DirectX11 With Windows SDK完整目录
欢迎加入QQ群: 727623616 能够一块儿探讨DX11,以及有什么问题也能够在这里汇报。