如何能够高效的产生更接近真实的阴影一直是视频游戏的一个很有挑战的工作,本文介绍目前所为人熟知的两种阴影技术之一的ShadowMap(阴影图)技术。

ShadowMap

ShadowMap技术的概念应该说是最早应用在视频游戏中的阴影实现技术,有着非常高效和快速的特点,在实现阴影的同时只需要相对很小的计算负担。

ShadowMap技术很好理解:主要是分成两步。第一步是从光源方向(DirectionLight)/光源点(spotLight)绘制场景,将场景渲染到一张纹理中(ShadowMap)。第二步就从主相机渲染,在渲染过程中将每个像素的深度(光照空间)和ShadowMap中采样得到的深度进行比较,如果小于则不处于阴影中,否则处于阴影中,然后在进行融合就好了。

shadowmap

但这个技术存在两个问题:1.由于ShadowMap的分辨率有限,导致阴影有明显的锯齿。 2.阴影没有半影区,边缘很硬,不真实。于是我们使用PCF(percentage close filter)进行优化,使用PCF的原因是:对性能影响小,容易实现。

PCF(percentage close filter)

PCF 之所以说percentage 就是因为PCF阴影的标记不再只有0,1两个值,出现了小数,这个小树就表示阴影的权重,我想可能现在你已经想到了,没错,PCF 就是对ShadowMap中一定半径内的纹素进行采样,分别和当前点的深度对比(光照空间),sampleDepth < depth ? 0 : 1 ,然后求和除以采样数得到权重,这里采用的是泊松采样。

PCF

下面看下代码:

ShadowMap中的vs:

VertexOut VS(VertexIn vin)
{
 		VertexOut vout;
 		// 转换到齐次剪裁空间
 		vout.PosH = mul(float4(vin.PosL, 1.0f), World);
 		vout.PosH = mul(vout.PosH, View);
 		vout.PosH = mul(vout.PosH, Proj);
 		vout.Depth = vout.PosH.zw;
 		return vout;
}

ShadowMap中的PS:

float4 PS(VertexOut pin) : SV_Target
{
 		float d = pin.Depth.x / pin.Depth.y;
 		float4 color = float4(d, d, d, 1.0f);
 		return color;
}

阴影渲染的VS:

VertexOut VS(VertexIn vin)
{
	VertexOut vout;
 
	// 转换到齐次剪裁空间
	vout.PosH = mul(float4(vin.PosL, 1.0f), World);
 		vout.PosW = vout.PosH.xyz;
 		vout.PosH = mul(vout.PosH, View);
 		vout.PosH = mul(vout.PosH, Proj);
 		//将法线变换到世界坐标系
 		vout.Normal = mul(float4(vin.Normal, 0.0f), InverseTransposeWorld).rgb;	
 		vout.Tangent = mul(float4(vin.Tangent, 0.0f), World).rgb;
 		vout.Tex = vin.Tex;
 		//顶点在摄像机视角齐次裁剪空间
 		vout.PosLightH = mul(float4(vin.PosL, 1.0f), LightView);
 		vout.PosLightH = mul(vout.PosLightH, LightProj);
 
	return vout;
}

阴影渲染的PS:

float2 poissonDisk[16] = {
 		float2(-0.94201624, -0.39906216),
 		float2(0.94558609, -0.76890725),
 		float2(-0.094184101, -0.92938870),
 		float2(0.34495938, 0.29387760),
 		float2(-0.91588581, 0.45771432),
 		float2(-0.81544232, -0.87912464),
 		float2(-0.38277543, 0.27676845),
 		float2(0.97484398, 0.75648379),
 		float2(0.44323325, -0.97511554),
 		float2(0.53742981, -0.47373420),
 		float2(-0.26496911, -0.41893023),
 		float2(0.79197514, 0.19090188),
 		float2(-0.24188840, 0.99706507),
 		float2(-0.81409955, 0.91437590),
 		float2(0.19984126, 0.78641367),
 		float2(0.14383161, -0.14100790)
};

float PCF_NUM_SAMPLES = 16.0f;
float SHADOW_EPSILON = 0.000004;//深度信息不是线性的,存在误差,手动调整
float2 SMAP_SIZE = { 800, 600 };

float PCF_Filter(float2 uv, float zReceiver, float filterRadiusUV)    //zReceiver为深度信息
{
	float sum = 0.0f;
 	for (int i = 0; i < PCF_NUM_SAMPLES; ++i)
 	{
  		float2 offset = poissonDisk[i] * filterRadiusUV / SMAP_SIZE;  //SMAP_SIZE是Shadow Map的材质大小
  		float Zdepth = depthTex.Sample(samplerClamp, uv + offset).x + SHADOW_EPSILON;
  		if (Zdepth >= zReceiver)
   			sum += 1.0f;
 		}
 		return sum / PCF_NUM_SAMPLES;
}

float4 PS(VertexOut pin) : SV_Target
{
	 int lightCount = 1;
	 float3 toEye = normalize(g_eyePos - pin.PosW);

	 //将投影空间的坐标转化为纹理空间坐标
	 float2 depthCoord = 0.5*(pin.PosLightH.xy / pin.PosLightH.w + float2(1.0f, 1.0f));
	 depthCoord.y = 1.0f - depthCoord.y;

	 float4 ambient = { 0.0f, 0.0f, 0.0f, 0.0f };
	 float4 diffuse = { 0.0f, 0.0f, 0.0f, 0.0f };
	 float4 specular = { 0.0f, 0.0f, 0.0f, 0.0f };

	 //纹理的颜色
	 float4 texColor = woodTex.Sample(samplerTex, pin.Tex);

	 //法线贴图采样
	 float3 texnormal = normalTex.Sample(samplerTex, pin.Tex).rgb;
	 float3 normal = NormalSampleToWorldSpace(texnormal, pin.Normal, pin.Tangent);//这个函数是上一篇NormalMap中的函数,用来把采样来的法线转换到世界坐标系

	 //下面这段是用来计算光照的
	 for (int i = 0; i < lightCount; ++i)
	 {
		 float4 a, d, s;
		 ComputeDirLight(g_material, g_lights[i], normal, toEye, a, d, s);//这是我自己的函数,没有贴在这里
		 ambient += a;
		 diffuse += d;
		 specular += s;
	 }

	 float4 color;
	 float LightAmount = PCF_Filter(depthCoord, pin.PosLightH.z / pin.PosLightH.w, 1.0f);

	 //不在阴影中
	 if (LightAmount == 1.0f)
	 {
	 	color = texColor*saturate(ambient + diffuse);// +saturate(specular);
	 }
	 else//根据权重决定颜色(阴影中)
	 {
	 	color = texColor*saturate(ambient + diffuse*LightAmount);
	 }
	 return color;
}

效果图:

ShadowMap+PCF


ShiweyYan

A game developer who graduates from SCUT.