这节教程我们将介绍一种生成Shadow(阴影)的主流技术,程序结构如下:
读懂此节教程你应该先懂得的技术: (1)D3D11如何求得DepthBuffer(深度缓存值),D3D11教程二十五之DepthBuffer(深度缓存)
(2)D3D11如何使用RTT技术(渲染到纹理技术) ,D3D11教程十四之RenderToTexture(RTT技术)
(3)D3D11如何使用ProjectiveTexturing(投影纹理技术),D3D11教程三十之ProjectiveTexturing(投影纹理)
同学们读此教程前,务必得搞清楚上面三项技术,否则这节教程学的将有些吃力,我敢说,把上面三个教程的技术看懂并亲自实现一遍,那么你对D3D11渲染管线的掌握已经是入门了。
事先说明我们这节教程是针对聚光灯生成阴影的,在强调一遍,我们这节教程是针对聚光灯成阴影的,利用平行光生成阴影的ShadowMap教程以后我再给出。
一,ShadowMap的简介。
ShdowMap和Shadow Volume是3D渲染中流行的技术,ShadowMap按中文翻译的意思是阴影贴图,实际上很多不明白原理的新人就会认为是绘制着黑色阴影的贴图,但这是错误的想法。ShadowMap其实是渲染3D场景得到的深度图(DepthMap),存储着每个像素的深度缓存值(DepthBuufer).
那么用渲染阴影(Shadow)的步骤,我分为三步:
第一步,先规定这里有两类对象:生成阴影的物体(树,房子,人,等等)和阴影所在的平面(地面墙面等),利用RTT技术,渲染这两类对象,对它们进行深度写,生成ShdowMap。
第二步,正常渲染生成阴影的物体(树,房子,人).
第三步,利步用ShdowMap进行阴影所在的平面(地面墙面等)的渲染。
按原教程的算法步骤,其实第二步和第三步是合并在一起的,共用一个Shader,但是我觉得如果是仅仅是地面有阴影出现,分开来更合适,第二步和第三步的Shader不是同一个Shader。
第一步,渲染生成阴影的物体以及阴影所在的平面,生成ShadowMap。
怎么生成ShadowMap(阴影贴图或者深度值图)呢?其实很简单,得注意两大点:
第一大点,还记得
D3D11教程十四之RenderToTexture(RTT技术)我们将一个立方体渲染到蓝色的RenderTargetTexture上,但那节教程进行的是渲染到目标视图RenderTarget,也就是把3D场景的颜色写到2D纹理上,而这节教程我们是将整个场景的深度值(Depth)写到2D纹理上,所以这次的RenderModelToTexure类是有些不同的,下面放出代码:
RenderModelToTexure.h
#pragma once
#ifndef _RENDER_MODEL_TO_TEXTURE_H
#define _RENDER_MODEL_TO_TEXTURE_H
#include<Windows.h>
#include<D3D11.h>
#include<xnamath.h>
#include"Macro.h"
class RenderModelToTextureClass
{
private:
ID3D11ShaderResourceView* mShaderResourceView; //Shader资源视图
ID3D11Texture2D* mDepthStencilTexture;
ID3D11DepthStencilView* mDepthStencilView;
D3D11_VIEWPORT mViewPort; //视口
public:
RenderModelToTextureClass();
RenderModelToTextureClass(const RenderModelToTextureClass&other);
~RenderModelToTextureClass();
bool Initialize(ID3D11Device* d3dDevice,int TextureWidth,int TexureHeight);
void ShutDown();
void SetRenderTarget(ID3D11DeviceContext* deviceContext);
void ClearRenderTarget(ID3D11DeviceContext* deviceContext, float red, float green, float blue, float alpha);
ID3D11ShaderResourceView* GetShaderResourceView();
};
#endif // !_RENDER_3D_MODEL_TO_TEXTURE_H
RenderModelToTexure.CPP
#include"RenderModelToTexure.h"
RenderModelToTextureClass::RenderModelToTextureClass()
{
mShaderResourceView = NULL;
mDepthStencilTexture = NULL;
mDepthStencilView = NULL;
}
RenderModelToTextureClass::RenderModelToTextureClass(const RenderModelToTextureClass&other)
{
}
RenderModelToTextureClass::~RenderModelToTextureClass()
{
}
bool RenderModelToTextureClass::Initialize(ID3D11Device* d3dDevice, int TextureWidth, int TexureHeight)
{
//第一,填充深度视图的2D纹理形容结构体,并创建2D渲染纹理
D3D11_TEXTURE2D_DESC depthBufferDesc;
ZeroMemory(&depthBufferDesc, sizeof(depthBufferDesc));
depthBufferDesc.Width = TextureWidth;
depthBufferDesc.Height = TexureHeight;
depthBufferDesc.MipLevels = 1;
depthBufferDesc.ArraySize = 1;
depthBufferDesc.Format = DXGI_FORMAT_R24G8_TYPELESS; //24位是为了深度缓存,8位是为了模板缓存
depthBufferDesc.SampleDesc.Count = 1;
depthBufferDesc.SampleDesc.Quality = 0;
depthBufferDesc.Usage = D3D11_USAGE_DEFAULT;
depthBufferDesc.BindFlags = D3D11_BIND_DEPTH_STENCIL | D3D11_BIND_SHADER_RESOURCE; //注意深度缓存(纹理)的绑定标志
depthBufferDesc.CPUAccessFlags = 0;
depthBufferDesc.MiscFlags = 0;
HR(d3dDevice->CreateTexture2D(&depthBufferDesc, NULL, &mDepthStencilTexture));
//第二,填充深度缓存视图形容结构体,并创建深度缓存视图
D3D11_DEPTH_STENCIL_VIEW_DESC depthStencilViewDesc;
ZeroMemory(&depthStencilViewDesc, sizeof(depthStencilViewDesc));
depthStencilViewDesc.Format = DXGI_FORMAT_D24_UNORM_S8_UINT;
depthStencilViewDesc.ViewDimension = D3D11_DSV_DIMENSION_TEXTURE2D;
depthStencilViewDesc.Texture2D.MipSlice = 0;
HR(d3dDevice->CreateDepthStencilView(mDepthStencilTexture, &depthStencilViewDesc, &mDepthStencilView));
//第三,填充着色器资源视图形容体,并进行创建着色器资源视图,注意这是用深度缓存(纹理)来创建的,而不是渲染目标缓存(纹理)创建的
D3D11_SHADER_RESOURCE_VIEW_DESC shaderResourceViewDesc;
shaderResourceViewDesc.Format = DXGI_FORMAT_R24_UNORM_X8_TYPELESS; //此时因为是仅仅进行深度写,而不是颜色写,所以此时Shader资源格式跟深度缓存是一样的
shaderResourceViewDesc.ViewDimension = D3D11_SRV_DIMENSION_TEXTURE2D;
shaderResourceViewDesc.Texture2D.MostDetailedMip = 0;
shaderResourceViewDesc.Texture2D.MipLevels = depthBufferDesc.MipLevels;
HR(d3dDevice->CreateShaderResourceView(mDepthStencilTexture, &shaderResourceViewDesc, &mShaderResourceView));
//第四,设置视口的属性
mViewPort.Width = (float)TextureWidth;
mViewPort.Height = (float)TexureHeight;
mViewPort.MinDepth = 0.0f;
mViewPort.MaxDepth = 1.0f;
mViewPort.TopLeftX = 0.0f;
mViewPort.TopLeftY = 0.0f;
return true;
}
void RenderModelToTextureClass::ShutDown()
{
ReleaseCOM(mDepthStencilTexture);
ReleaseCOM(mDepthStencilView);
ReleaseCOM(mShaderResourceView);
}
//让此时所有图形渲染到这个目前渲染的位置
void RenderModelToTextureClass::SetRenderTarget(ID3D11DeviceContext* deviceContext)
{
ID3D11RenderTargetView* renderTarget[1] = { 0 };
//绑定渲染目标视图和深度模板视图到输出渲染管线
deviceContext->OMSetRenderTargets(1,renderTarget, mDepthStencilView);
//设置视口
deviceContext->RSSetViewports(1, &mViewPort);
}
//不用清除背后缓存,因为不需要进颜色写(ColorWrite),仅仅进行深度写
void RenderModelToTextureClass::ClearRenderTarget(ID3D11DeviceContext* deviceContext, float red, float green, float blue, float alpha)
{
//设置清除缓存为的颜色
float color[4];
color[0] = red;
color[1] = green;
color[2] = blue;
color[3] = alpha;
//清除深度缓存和模板缓存
deviceContext->ClearDepthStencilView(mDepthStencilView, D3D11_CLEAR_DEPTH | D3D11_CLEAR_STENCIL, 1.0f, 0);
}
// 将“被渲染模型到纹理的纹理”作为ShaderResourceView资源返回,这个资源将会跟其它的ShaderResourceView资源一样被送入Shader里计算.
ID3D11ShaderResourceView* RenderModelToTextureClass::GetShaderResourceView()
{
return mShaderResourceView;
}
看上面的源代码,我们完全没有使用 D3D11教程十四之RenderToTexture(RTT技术)教程里的渲染目标纹理ID3D11Texture2D* mRenderTargetTexture以及渲染目标视图ID3D11RenderTargetView* mRenderTargetView,代替而来我们在本教程使用了深度缓存纹理ID3D11Texture2D* mDepthStencilTexture和深度缓存视图ID3D11DepthStencilView* mDepthStencilView,我们这样做理由有两点是:
(1)渲染目标纹理其实就是BackBuffer,其像素存储的是Color颜色值,而深度缓存纹理其实就是DepthBuffer,其像素存储的就是Depth深度值,而我们的目标仅仅是得到像素的深度值。
(2)仅仅进行深度写(DepthWrite),而不进行颜色写(ColorWrite),可以大大提高我们3D程序的性能,因此我们设置渲染目标视图为NULL,
//让此时所有图形渲染到这个类的深度缓存
void RenderModelToTextureClass::SetRenderTarget(ID3D11DeviceContext* deviceContext)
{
ID3D11RenderTargetView* renderTarget[1] = { 0 };
//绑定渲染目标视图和深度模板视图到输出渲染管线
deviceContext->OMSetRenderTargets(1,renderTarget, mDepthStencilView);
//设置视口
deviceContext->RSSetViewports(1, &mViewPort);
}
这里容易出错的两处地方是:1,各个接口创建对应的Format格式
2,depthBufferDesc.BindFlags = D3D11_BIND_DEPTH_STENCIL | D3D11_BIND_SHADER_RESOURCE; //注意深度缓存(纹理)的绑定标志
第二大点,我们在这一步中用的相机变换矩阵和透视变换矩阵是基于点光源计算出来的,这点到第三步我再解释。
第二步,正常渲染要生成阴影的物体。
这一步没多少好说的,只要记住这一步使用的正常的相机变换矩阵和透视变换矩阵,即相机变换矩阵和透视变换矩阵基于观察相机得到的。
第三步,利用ShdowMap进行阴影所在的平面(地面墙面等)的渲染.
这一步就不得不说说ShadowMap的原理了,其实ShaderMap的原理很简单。下面放图:
文章刚开头我强调过这节教程将是点光源成阴影的,看图LightPosition是点光源的位置,上面的球体是生成阴影的物体,而下面的平面是阴影所在的平面,这里的点光源就相当于
D3D11教程三十之ProjectiveTexturing(投影纹理)的投影相机了,其实用投影相机成像的原理和聚光灯成阴影原理很相似啊,所以上面是基于灯泡建立起的一个视截体,为了方面解释,就用了视截体的在某个平面的投影来解释,红色部分为视平面。
先假设点光源发出一条光线,d(p)为光源到地面的深度缓存值,s(p)为视平面(深度纹理)A点位置的深度缓存值,上面图中由于球体挡在地面之前,d(P)>s(P,)经过Z缓存剔除,视平面(深度纹理)A点位置的深度缓存值为s(p).,此时地面相应的点成阴影
在来看一张图:
假设点光源发出一条光线,d(p)为点光源到地面的深度缓存值,s(p)为视平面(深度纹理)B点位置的深度缓存值,那么在B点,由于球体并没有挡在地面之前,d(P)=s(P)或者说
s(P)>=d(P),此时地面相应点不能成阴影。
一切迎刃而解,下面直接给出我们绘制地面的Shader代码吧:
DrawShadowShader.fx
Texture2D BaseTexture:register(t0); //基础纹理
Texture2D ShadowMap:register(t1); //投影纹理
SamplerState WrapSampleType:register(s0); //采样方式
SamplerState ClampSampleType:register(s1); //采样方式
//VertexShader
cbuffer CBMatrix:register(b0)
{
matrix World;
matrix View;
matrix Proj;
matrix WorldInvTranspose;
matrix ProjectorView;
matrix ProjectorProj;
};
cbuffer CBLight:register(b1)
{
float4 DiffuseColor;
float4 AmbientColor;
float3 PointLightPos;
float pad; //填充系数
}
struct VertexIn
{
float3 Pos:POSITION;
float2 Tex:TEXCOORD0; //多重纹理可以用其它数字
float3 Normal:NORMAL;
};
struct VertexOut
{
float4 Pos:SV_POSITION;
float4 ProjPos:POSITION; //基于点光源投影在齐次裁剪空间的坐标
float2 Tex:TEXCOORD0;
float3 W_Normal:NORMAL; //世界空间的法线
float3 Pos_W:NORMAL1; //物体在世界空间的顶点坐标
};
VertexOut VS(VertexIn ina)
{
VertexOut outa;
//将坐标变换到观察相机下的齐次裁剪空间
outa.Pos = mul(float4(ina.Pos,1.0f), World);
outa.Pos = mul(outa.Pos, View);
outa.Pos = mul(outa.Pos, Proj);
//将顶点法向量由局部坐标变换到世界坐标
outa.W_Normal = mul(ina.Normal, (float3x3)WorldInvTranspose); //此事世界逆转置矩阵的第四行本来就没啥用
//对世界空间的顶点法向量进行规格化
outa.W_Normal = normalize(outa.W_Normal);
//获取纹理坐标
outa.Tex= ina.Tex;
//将坐标变换到投影相机下的齐次裁剪空间
outa.ProjPos= mul(float4(ina.Pos, 1.0f), World);
outa.ProjPos = mul(outa.ProjPos, ProjectorView);
outa.ProjPos = mul(outa.ProjPos, ProjectorProj);
//获取物体在世界空间下的坐标
outa.Pos_W= (float3)mul(float4(ina.Pos, 1.0f), World);
return outa;
}
float4 PS(VertexOut outa) : SV_Target
{
float4 TexColor; //采集基础纹理颜色
float ShadowMapDepth; //a,g,b存储的都是深度
float DiffuseFactor;
float4 DiffuseLight;
float2 ShadowTex; //阴影纹理坐标
float4 color = {0.0f,0.0f,0.0f,0.0f}; //最终输出的颜色
float Depth;
float bias;
//设置偏斜量
bias = 0.001f;
//第一,获取基础纹理的采样颜色
TexColor = BaseTexture.Sample(WrapSampleType, outa.Tex);
//第二,不管有没有遮挡,都应该具备环境光,注意环境光不生成阴影,这里仅仅是漫反射光生成阴影
color = AmbientColor;
//第三,求出相应顶点坐标对应在ShdowMap上的深度值
//获取投影相机下的投影纹理空间的坐标值[0.0,1.0] u=0.5*x+0.5; v=-0.5*y+0.5; -w<=x<=w -w<=y<=w
ShadowTex.x = (outa.ProjPos.x / outa.ProjPos.w)*0.5f + 0.5f;
ShadowTex.y = (outa.ProjPos.y / outa.ProjPos.w)*(-0.5f) + 0.5f;
//第四,由于3D模型可能超出投影相机下的视截体,其投影纹理可能不在[0.0,1.0],所以得进行判定这个3D物体投影的部分是否在视截体内(没SV_POSITION签名 显卡不会进行裁剪)
if (saturate(ShadowTex.x) == ShadowTex.x&&saturate(ShadowTex.y) == ShadowTex.y)
{
//求出顶点纹理坐标对应的深度值
ShadowMapDepth = ShadowMap.Sample(ClampSampleType, ShadowTex).r;
//求出顶点坐标相应的深度值(点光源到渲染点的深度值)
Depth = outa.ProjPos.z / outa.ProjPos.w;
//减去阴影偏斜量
ShadowMapDepth = ShadowMapDepth + bias;
//如果不被遮挡,则物体具备漫反射光
if (ShadowMapDepth >= Depth)
{
//求出漫反射光的的方向
float3 DiffuseDir = outa.Pos_W - PointLightPos;
//求出点光源到像素的距离
float distance = length(DiffuseDir);
//求出衰减因子
float atten1 = 0.5f;
float atten2 = 0.1f;
float atten3 = 0.0f;
float LightIntensity = 1.0f / (atten1 + atten2*distance + atten3*distance*distance);
//求漫反射光的反光向
float3 InvseDiffuseDir = -DiffuseDir;
//求出漫反射因子[0.0,1.0]
DiffuseFactor = saturate(dot(InvseDiffuseDir,outa.W_Normal));
//求出漫射光
DiffuseLight = DiffuseFactor*DiffuseColor*LightIntensity;
//颜色加上漫反射光
color += DiffuseLight;
color = saturate(color);
}
}
color = color*TexColor;
return color;
}
请注意两点:1,我们的环境(AmbientColor)光并没有生成阴影,不管地面相应位置有没有阴影,其都应该具备环境光(AmbientColor)。
2,ShadowMap的像素的r值存放深度缓存值.
3,注意阴影偏斜量(bias)的设置,bias不应该过于大,也不应该过于小,过大导致地面阴影无法出现,过小导致地面小细点出现
4, 还有记得ShadowMap的分辨率要大些,过于小的话会导致阴影的锯齿过大.
最后要说的是,我怀疑原教程的作者生成ShadowMap的RTT是错误的,我用的是D3D11龙书的RTT。
放出程序运行图:
(1)参数:bias=0.001 SHADOW_MAP_WIDTH = 1024 SHADOW_MAP_HEIGHT = 1024,可以看到阴影总体虽然存在锯齿,但效果不错。
(2)参数:bias=0.0000001f
SHADOW_MAP_WIDTH = 1024
SHADOW_MAP_HEIGHT = 1024,由于bias过于小,可以看到地面的很多小黑点,俗称Shadow Acne.
(3)参数:bias=0.3f
SHADOW_MAP_WIDTH = 1024
SHADOW_MAP_HEIGHT = 1024,由于bias过于大,无法看到阴影。
(4)参数:bias=0.001f
SHADOW_MAP_WIDTH = 256
SHADOW_MAP_HEIGHT = 256,由于ShadowMap分辨率过于小,阴影锯齿很明显。
二,ShadowMap方法带来的问题,阴影的锯齿和阴影粉刺(ShadowAcne).
其实上阴影锯齿和阴影粉刺两个问题的诞生都是由于ShadowMap上一个像素对应于3D场景的一个区域造成的,或者说是ShadowMap上一个像素对应于3D场景的多个像素造成的。
(1),阴影粉刺(ShadowAcne)
看到上面程序运行的第二幅图了?那些地面黑色的小斑点就是阴影粉刺(ShadowAcne)了,那么阴影粉刺是怎么产生的呢? 先来看一副图,假设下面是地面的无阴影部分:
看上面图,假设E为相机,ScenePoly为地面的无阴影部分。由于像素P1点和P2点对应ShadowMap的A点,这时候P1点和P2点都是ScenePoly的无阴影像素点,实际上应该是d(p1)和d(p2)=s,但是由于图中所示,可以知道P1和P2点对应ShadowMap的深度为s,而计算出的P1与P2与投影相机(点光源)之间的深度为d(p1)和d(p2),由于d(p1)>s,则p1处于光亮部分,而d(p2)<s,p2处于阴影部分,这样就出现了黑色粉刺。那么怎么解决这个问题呢?对,我们可以通过bias偏移量来解决这个问题,来看下面的图:
将ScenePoly往平移BiasedPoly的位置,这时候P1和P2对应的ShadowMap的像素代表的深度为s,可以看出 s>=d(p1),s>=d(p2),,假设从ShadowMap采集的像素深度为Sample(ShadowMap) ,看到上面图中那段红色的线段就bias了,s=Sample(ShadowMap)+bias,即Sample(ShadowMap)+bias>=d(p1),Sample(ShadowMap)+bias>=d(p2),
那么就不会出现粉刺了。
注意Sample(ShadowMap)+bias不一定等于d(p1)和d(p2),所以是">=",也要注意bias不要过于大,否则会导致地面的阴影部分消失。
下面放出我的源代码链接:
关于锯齿的解决我们在下一个教程详细说明。