Directx11教程四十四之CascadeShadowMap(层级阴影)(上)

好久没写博客了,紧接着前面的博客,不过这次读取纹理的借口换为了DXUT框架里面的接口。

程序结构

如果没有学会ShadowMap,PCF软阴影原理的建议回去回顾下面几个教程:

Directx11教程三十一之ShadowMap(阴影贴图)之聚光灯光源成影

Directx11教程三十三之Soft Shadows—PCF(Percent closer Filter)

Directx11教程三十一之ShadowMap(阴影贴图)之平行光成影

程序的框架如下所示:


这里有关CascadeShadowMap的实现代码基本封装在CascasdeShadowMapManagerClass里。

CascadeShadowMap(层级阴影贴图)的原理思想

        我们都知道,在实时渲染中,存在这样的一个渲染优化手段,也就是“LOD”,level of details,也就是细节等级的意思,总体来说也就是根据我们观察相机距离被观察物体的距离来决定优化被观察物体的渲染细节,比如说一个物体距离观察相机很远的时候,渲染Shader可以只采用最基本的DiffuseMap,并进行简单的光照计算,毕竟人眼睛会一定程序忽略远处物体的细节。而当该物体距离相机很近时,渲染Shader可以采用DiffuseMap,NormalMap,ParallaxMap等,并计算复杂的光照计算,以显示更多的细节。

       CascadeShadowMap其实就是来源于LOD的思想,总体的原理: 将我们观察相机的视截体根据与相机空间的原点的远近距离来划分为多个部分(也就是所谓的层级Cascade),并将这几个部分的物体渲染到相应的ShadowMap。

 因为CascasdeShadowMap主要是用于主光源的阴影投射的,也就是平行光,下面重点讲解平行光下的CascasdeShadowMap算法。


CascadeShadowMap(层级阴影贴图)的计算过程

视截体划根据远近距离分为多个层级:

先来看看我们的视截体:


我们视截体取横截面进行观察:


然后我们根据在相机空间下坐标点距离原点的远近,划分视截体为三个层级,其中箭头为平行光方向如下所示:


因为平行光的投影变换矩阵是正交投影矩阵(OrthoProjMatrix),从上面给出的几个教程我们知道:

平行光渲染ShadowMap的变换公式: objectPos*WorldMatrix*LightViewMatrix*OrthoProjMatrix,

这里毫无疑问 objectPos(物体的顶点局部空间位置),WorldMatrix(世界变换矩阵),LightViewMatrix(光源相机空间的变换矩阵)都是已经知道的。

那么下面我们要求的就是观察相机的视截体的Near,Middle,Far三个部分在平行光源的相机空间形成的OrthoProjMatrix(注意理解平行光的相机空间,我在前面的教程将形成阴影的光照比作投影相机),先看看普通情况的平行光x相机空间的OrthoProjMatrix,如下所示:


上面的箭头也就是平行光的方向,我选取平行光源的XZ平面来说明,上图中ABCD也就是平行光的相机空间代表正交投影矩阵的长方体在XZ平面的截面。这里代表正交投影矩阵的长方体其实是个AABB(轴对齐包围盒)

那么我们相机的视截体的Near,Middle,Far三个部分是如何在平行光的相机空间计算AABB或者说计算正交投影矩阵的长方体,请看下图:




其实就是计算观察相机的视截体三个层级(Near,Middle,Far)在平行光源空间形成的AABB包围盒,这样自然而然得到相应的正交投影变换矩阵了。那么如何计算Near,Middle,Far三个部分形成的AABB包围盒呢?这里分为三步:

(1)首先计算观察相机的三个层级(3D梯形)的顶点的位置,注意一个3D梯形有八个顶点,因为是3D梯形,因为我这里是二维平面截图,你看到是4个顶点,别以为只是4个顶点。计算如下图所示:


计算公式:


DX11计算一个层级八个顶点的代码:

//求出相机的一个层级的八个顶点
void CascadedShadowsManager::CreateFrustumPointsFromCascadeInterval(FLOAT fCascadeIntervalBegin, FLOAT fCascadeIntervalEnd, CXMMATRIX vProjection, XMVECTOR* pvCornerPointsWorld)
{
	BoundingFrustum vViewFrust(vProjection);
	vViewFrust.Near = fCascadeIntervalBegin;
	vViewFrust.Far = fCascadeIntervalEnd;

	static const XMVECTORU32 vGrabY = { 0x00000000,0xFFFFFFFF,0x00000000,0x00000000 };
	static const XMVECTORU32 vGrabX = { 0xFFFFFFFF,0x00000000,0x00000000,0x00000000 };

	XMVECTORF32 vRightTop = { vViewFrust.RightSlope,vViewFrust.TopSlope,1.0f,1.0f };
	XMVECTORF32 vLeftBottom = { vViewFrust.LeftSlope,vViewFrust.BottomSlope,1.0f,1.0f };
	XMVECTORF32 vNear = { vViewFrust.Near,vViewFrust.Near ,vViewFrust.Near,1.0f };
	XMVECTORF32 vFar = { vViewFrust.Far,vViewFrust.Far,vViewFrust.Far,1.0f };
	XMVECTOR vRightTopNear = XMVectorMultiply(vRightTop, vNear);
	XMVECTOR vRightTopFar = XMVectorMultiply(vRightTop, vFar);
	XMVECTOR vLeftBottomNear = XMVectorMultiply(vLeftBottom, vNear);
	XMVECTOR vLeftBottomFar = XMVectorMultiply(vLeftBottom, vFar);

	//near
	pvCornerPointsWorld[0] = vRightTopNear;  //近平面右上角
	pvCornerPointsWorld[1] = XMVectorSelect(vRightTopNear, vLeftBottomNear, vGrabX); //近平面左上角
	pvCornerPointsWorld[2] = vLeftBottomNear;  //近平面左下角
	pvCornerPointsWorld[3] = XMVectorSelect(vRightTopNear, vLeftBottomNear, vGrabY); //近平面右下角

	//far
	pvCornerPointsWorld[4] = vRightTopFar;  //远平面右上角
	pvCornerPointsWorld[5] = XMVectorSelect(vRightTopFar, vLeftBottomFar, vGrabX); //远平面左上角
	pvCornerPointsWorld[6] = vLeftBottomFar;  //远平面左下角
	pvCornerPointsWorld[7] = XMVectorSelect(vRightTopFar, vLeftBottomFar, vGrabY); //远平面右下角

}
(2)进行了上面的一步,得到相应层级在观察相机的相机空间(ViewSpace)的八个顶点位置,然后将这八个顶点乘以CameraViewMatrix的逆矩阵 变换到到世界空间

            WorldSpacePos = CameraViewPos*Inverse(CameraViewMatrix)


然后乘以平行光源的相机变换矩阵变换到平行光源的ViewSpace,也就是

           LightViewSpacePos= WorldSpacePos *LightViewMatrix


(3)求相应层级在LightViewSpace 空间(可称为LightSpace)的八个顶点形成的AABB包围盒,也就是这八个顶点的每个分量最大值形成的顶点和分量最小值形成的顶点围成的长方体。计算如下:

 

        lightCameraFrustumOrthoMin = g_XMFltMax;
		lightCameraFrustumOrthoMax = g_XMFltMin;

		XMVECTOR vTempTranslateCornerPoints;
		//将视截体的八个点从相机空间变换到光照空间,并求其在光照空间的AABB
		for (int index = 0; index < 8; ++index)
		{
			//将视截体的点从相机空间变换到世界空间
			frustumPoints[index] = XMVector3Transform(frustumPoints[index], cameraInverseViewMatrix);

			//将视截体的点从世界空间变换到光照空间
			vTempTranslateCornerPoints = XMVector3Transform(frustumPoints[index], lightViewMatrix);

			//求出AABB体的位置最大点和最小点
			lightCameraFrustumOrthoMin = XMVectorMin(lightCameraFrustumOrthoMin, vTempTranslateCornerPoints);
			lightCameraFrustumOrthoMax = XMVectorMax(lightCameraFrustumOrthoMax, vTempTranslateCornerPoints);
		}
那么这个层级的OrthoProjMatrix就可以计算出来, 
 

	mShadowProj[iCascadeIndex] = XMMatrixOrthographicOffCenterLH(XMVectorGetX(lightCameraFrustumOrthoMin),
			XMVectorGetX(lightCameraFrustumOrthoMax),
			XMVectorGetY(lightCameraFrustumOrthoMin),
			XMVectorGetY(lightCameraFrustumOrthoMax), XMVectorGetZ(lightCameraFrustumOrthoMin), XMVectorGetZ(lightCameraFrustumOrthoMax));

Shader实现代码:

#ifndef CASCADE_COUNT_FLAG
#define CASCADE_COUNT_FLAG 3
#endif


//常量缓存数据
cbuffer CB_ALL_SHADOW_DATA:register(b0)
{
	matrix mWorldViewProj;
	matrix mWorldView;
	matrix mWorld;
	matrix mShadowView;
	float4 mCascadeOffset[8];
	float4 mCascadeScale[8];

	int mCascadeNum;
	int mVisualizeCascade;
	int mPCFBlurForLoopStart;
	int mPCFBlurForLoopEnd;

	float mMinBorderPadding;
	float mMaxBorderPadding;
	float mShadowBias; //阴影bias,解决阴影粉刺
	float mShadowPartitionSize;
	
	float mTexelSize;  //ShadowMap纹理像素大小
	float mNativeTexelSizeInX; // Texel size in native map ( textures are packed )
	float mCascadeBlendArea;
	float pad;

	float4 mCascadeFrustumViewSpaceDepth[2];
	float4 mCascadeFrustumViewSpaceDepthFloat4[8];
	float4 mLightDir;
};


//纹理
Texture2D gDiffuse:register(t0);  //diffuseMap
Texture2D gShadow:register(t5); //shadowMap


//采样器
SamplerState samLinear:register(s0);  
SamplerState samPoint:register(s1);
SamplerComparisonState samShadow:register(s5);


struct VertexIn
{
	float3 Pos:POSITION;
	float3 Normal:NORMAL;
	float2 Tex:TEXCOORD0;  
	
};


struct VertexOut
{
	float4 Pos:SV_POSITION;
	float3 WorldNormal:NORMAL;  //世界空间的法线
	float2 DiffuseTex:TEXCOORD0;
	float4 ShadowTex:TEXCOORD1;
	float4 InterPos:TEXCOORD2;
	float Depth: TEXCOORD3;
};



//每个层级的可视化颜色
static const float4 vCascadeColorsMultiplier[8] =
{
	float4 (1.5f, 0.0f, 0.0f, 1.0f),
	float4 (0.0f, 1.5f, 0.0f, 1.0f),
	float4 (0.0f, 0.0f, 5.5f, 1.0f),
	float4 (1.5f, 0.0f, 5.5f, 1.0f),
	float4 (1.5f, 1.5f, 0.0f, 1.0f),
	float4 (1.0f, 1.0f, 1.0f, 1.0f),
	float4 (0.0f, 1.0f, 5.5f, 1.0f),
	float4 (0.5f, 3.5f, 0.75f, 1.0f)
};

void ComputeCoordinatesTransform(in int iCascadeIndex,in out float4 vShadowTexCoord)
{
	vShadowTexCoord.x *= mShadowPartitionSize;     //1.0f / (float)mCascadeConfig.m_nCascadeLevels; 因为多张DepthRT合成了一张DepthRT

	vShadowTexCoord.x += (mShadowPartitionSize * (float)iCascadeIndex);

}


void CalculatePCFPercentLit(in float4 vShadowTexCoord, in float fBlurRowSize, out float fPercentLit)
{
	fPercentLit = 0.0f;

	for (int x = mPCFBlurForLoopStart; x < mPCFBlurForLoopEnd; ++x)
	{
		for (int y = mPCFBlurForLoopStart; y < mPCFBlurForLoopEnd; ++y)
		{
			float depthcompare = vShadowTexCoord.z;

			depthcompare -= mShadowBias;
			/*float2 texUV = float2(vShadowTexCoord.x + ((float)x) * mNativeTexelSizeInX, vShadowTexCoord.y + ((float)y) * mTexelSize);
			float depth = gShadow.Sample(samPoint, texUV).r;
			fPercentLit += (depth >= depthcompare);*/
			fPercentLit += gShadow.SampleCmpLevelZero(samShadow,
				float2(vShadowTexCoord.x + ((float)x) * mNativeTexelSizeInX,vShadowTexCoord.y + ((float)y) * mTexelSize),
				depthcompare
			);
		}
	}
	fPercentLit /= (float)fBlurRowSize;
}





VertexOut VS(VertexIn vin)
{
	VertexOut vout;
	vout.Pos = mul(float4(vin.Pos, 1.0), mWorldViewProj);
	vout.WorldNormal = mul(vin.Normal, (float3x3)mWorld);
	vout.DiffuseTex = vin.Tex;
	vout.InterPos = float4(vin.Pos, 1.0);
	vout.Depth = mul(float4(vin.Pos,1.0), mWorldView).z;

	vout.ShadowTex = mul(float4(vin.Pos, 1.0), mShadowView);

	return vout;
}


float4 PS(VertexOut pin) : SV_Target
{
	float4 diffuseColor = gDiffuse.Sample(samLinear, pin.DiffuseTex);

	float4 shadowMapTextureCoord = 0.0f;

	float4 visualCascadeColor = float4(0.0f, 0.0f, 0.0f, 1.0f);
	float fPercentLit = 0.0f;

	//模糊范围大小
	int iBlurRowSize = mPCFBlurForLoopEnd - mPCFBlurForLoopStart;
	iBlurRowSize *= iBlurRowSize;
	float fBlurRowSize = (float)iBlurRowSize;

	int iCascadeFound = 0;

	int iCurrentCascadeIndex = 0;

	float4 shadowMapTextureViewSpaceCoord = pin.ShadowTex;

	//将shadowMap的采样坐标从ViewSpace变换到纹理投影空间
	for (int iCascadeIndex = 0; iCascadeIndex < CASCADE_COUNT_FLAG&&iCascadeFound == 0; ++iCascadeIndex)
	{
		shadowMapTextureCoord = shadowMapTextureViewSpaceCoord * mCascadeScale[iCascadeIndex];
		shadowMapTextureCoord += mCascadeOffset[iCascadeIndex];

		if (min(shadowMapTextureCoord.x, shadowMapTextureCoord.y) > mMinBorderPadding
			&&max(shadowMapTextureCoord.x, shadowMapTextureCoord.y) < mMaxBorderPadding&&shadowMapTextureCoord.z>0.0f&&mMaxBorderPadding&&shadowMapTextureCoord.z<1.0f)
		{
			iCurrentCascadeIndex = iCascadeIndex;
			iCascadeFound = 1;
		}
	}

	ComputeCoordinatesTransform(iCurrentCascadeIndex,shadowMapTextureCoord);

	//层级标记颜色
	visualCascadeColor = vCascadeColorsMultiplier[iCurrentCascadeIndex];

	//shadowMap的深度和像素的深度进行比较
	CalculatePCFPercentLit(shadowMapTextureCoord,fBlurRowSize, fPercentLit);

	if (!mVisualizeCascade)
		visualCascadeColor = float4(1.0f, 1.0f, 1.0f, 1.0f);


	float3 vLightDir1 = float3(-1.0f, 1.0f, -1.0f);
	float3 vLightDir2 = float3(1.0f, 1.0f, -1.0f);
	float3 vLightDir3 = float3(0.0f, -1.0f, 0.0f);
	float3 vLightDir4 = float3(1.0f, 1.0f, 1.0f);

	//模拟ambient-Light
	float fLighting =
		saturate(dot(vLightDir1, pin.WorldNormal))*0.05f +
		saturate(dot(vLightDir2, pin.WorldNormal))*0.05f +
		saturate(dot(vLightDir3, pin.WorldNormal))*0.05f +
		saturate(dot(vLightDir4, pin.WorldNormal))*0.05f;

	float4 vShadowLighting = fLighting*0.5f;

	float3 lightDir = -normalize(mLightDir).xyz;
	fLighting += saturate(dot(pin.WorldNormal, lightDir));

	fLighting = lerp(vShadowLighting, fLighting, fPercentLit);
	
	float4 finalColor = fLighting*visualCascadeColor*diffuseColor;
	float4 color = pow(finalColor, 1.0f / 2.2f);
	return color;

}
Shader解惑:

(1)注意这里有三个层级,理论上有对应的三张ShadowMap,但是合成了一张ShadowMap,也就是说如果ShadowMap的分辨率是1024X1024,则合成的ShadowMap分辨率为3072X1024,因此可以看到计算ShadowMap采样坐标的代码为:

void ComputeCoordinatesTransform(in int iCascadeIndex,in out float4 vShadowTexCoord)
{
	vShadowTexCoord.x *= mShadowPartitionSize;     //1.0f / (float)mCascadeConfig.m_nCascadeLevels; 因为多张DepthRT合成了一张DepthRT

	vShadowTexCoord.x += (mShadowPartitionSize * (float)iCascadeIndex);

}
其中 mShadowPartitionSize= 1.0/mCascadeNum,  mCascadeNum为总共划分视截体的层级数。

PCF的代码为:

void CalculatePCFPercentLit(in float4 vShadowTexCoord, in float fBlurRowSize, out float fPercentLit)
{
	fPercentLit = 0.0f;

	for (int x = mPCFBlurForLoopStart; x < mPCFBlurForLoopEnd; ++x)
	{
		for (int y = mPCFBlurForLoopStart; y < mPCFBlurForLoopEnd; ++y)
		{
			float depthcompare = vShadowTexCoord.z;

			depthcompare -= mShadowBias;
			/*float2 texUV = float2(vShadowTexCoord.x + ((float)x) * mNativeTexelSizeInX, vShadowTexCoord.y + ((float)y) * mTexelSize);
			float depth = gShadow.Sample(samPoint, texUV).r;
			fPercentLit += (depth >= depthcompare);*/
			fPercentLit += gShadow.SampleCmpLevelZero(samShadow,
				float2(vShadowTexCoord.x + ((float)x) * mNativeTexelSizeInX,vShadowTexCoord.y + ((float)y) * mTexelSize),
				depthcompare
			);
		}
	}
	fPercentLit /= (float)fBlurRowSize;
}
这里假设ShadowMap的分辨率的Height为X,则width为3X,所以PCF的ShadowMap采样坐标的横向平移单位和纵向平移单位是不一样的,

这里 mTexelSize = 1.0 / X                                mNativeTexelInX = mTexelSize / mCascadeNum


渲染效果:

未可视化Cascade:


可视化Cascade:




参考资料:

[1]. http://ogldev.atspace.co.uk/www/tutorial49/tutorial49.html

[2].https://msdn.microsoft.com/en-us/library/windows/desktop/ee416307?ranMID=24542&ranEAID=TnL5HPStwNw&ranSiteID=TnL5HPStwNw-6XqNnhs2CFZrh4f6WF_b9w&tduid=(6936a70c84b18e2114dc180de68c9033)(256380)(2459594)(TnL5HPStwNw-6XqNnhs2CFZrh4f6WF_b9w)()

[3].DirectxSampleSDK的CascadeShaowMap例子

[4].http://blog.csdn.net/qq_29523119/article/details/72862517


源码链接:

Directx11的版本:

http://download.csdn.net/download/qq_29523119/10244932

模型资源在 directx-sdk-samples 的Media\powerplant 目录

OpenGL的版本:

http://download.csdn.net/download/qq_29523119/10153078


CSM算法实现余留的问题

   (1)阴影不稳定,或者部分阴影无法显示出来的问题,看下面:




毫无疑问,部门阴影丢掉了,不见了,这在我分享的Directx11或者OpenGL实现CSM算法都会出现这个问题,

http://ogldev.atspace.co.uk/www/tutorial49/tutorial49.html分享的CSM算法是粗糙的,并没有说明这种情况。

   (2)相机移动过程中的阴影抖动(jitter)问题


想知道如何解决CSM的阴影丢失问题和相机移动时的阴影抖动(jitter)问题,请看下一个教程:


猜你喜欢

转载自blog.csdn.net/qq_29523119/article/details/79266293