要在3D游戏中,制造出高效的,逼真的阴影一直是3D游戏的追求。本文要记录下一种最为游戏开发者熟悉的阴影做法:ShadowMap。
ShadowMap 有些文章翻译为阴影图,我觉得有点不贴切。从实现原理来说,ShadowMap存储的并不是最终的阴影图。还有一种阴影的技术叫做shadowTexture,这种技术才应该叫做阴影图做法,这里不多ShadowTexture做过多介绍,在引擎lithtech的早些版本中(之前使用过,好像是03年左右的版本,不知道现在有没有新版本),就是使用ShadowTexture作为阴影的实现技术,可以参考游戏《蜀山》《仙剑神曲》中的阴影的效果。
ShadowMap 按照实现的做法来说,应该翻译为深度贴图技术(这么翻译好像也不是很好,囧)。要实现效果理想的,逼真的,带有alpha穿透效果的shadowmap,是很多3D游戏都要完成的任务。要精度高,所以我选择使用shadowmap的纹理格式为D3DFMT_R32F的格式,因为我的shadowmap中只需要存储一个深度信息,所以一个32f的值足够使用,精度又可以达到最高。至于透明物体穿透,只需要更具物体最后的alpha值来判断,是否需要把这点的深度写到shadowmap中就可以了,足够透明的物体,不产生阴影,自然也就不需要把深度写到shadowmap中去。
ShadowMap的原理很容易理解,在使用Shadowmao技术的场景绘制过程中,可以粗瑞的分为2个部分。
第一步,以光源为视角对场景进行绘制,绘制的结果是将场景内要产生阴影的物体相对光源的深度信息写入纹理中,这个纹理就是ShadowMap,这里写入纹理的不是RGBA的颜色信息,而是一个深度值。
shader大致如下:
VS:
struct VS_INPUT
{
float3 pos : POSITION;
float3 norm : NORMAL;
float2 texc0 : TEXCOORD0;
float4 indiff : COLOR0;
};
void modelShadowShader( VS_INPUT In,
out float4 oPos : POSITION,
out float4 Depth : TEXCOORD0)
{
// 把顶点转换到灯光的世界坐标下
float4 tmpPos = mul(float4(In.pos,1.0f) ,worldMatTran);
// 转换到屏幕坐标
oPos = mul(tmpPos,viewPorjMatTran);
// 用纹理坐标来存储深度信息,传到pix shader
Depth.zw = oPos.zw;
}
PS:
float alpaRef : register(c0); // 传入的值,决定alpha低于多少的像素是要丢弃的,也就是透明的物体是不会产生阴影的
sampler2D s1 : register(s0);
void PixShadow( float4 Depth : TEXCOORD0,
out float4 Color : COLOR )
{
float a = tex2D(s1,Depth.xy).a;
if (a <= alpaRef)
discard;
float f = clamp(Depth.z/Depth.w, 0, 1);
Color = float4(f,f,f,1.0);
}
第二步,绘制场景时逐像素对比相对光源的深度值与ShadowMap中的深度信息,当深度大于ShadowMap中的深度时,说明该像素处于阴影中。
在正常渲染场景的时候,首先在vs shader中,要计算当前点对应shadowmap的纹理坐标索引。这涉及到一个知识点,就是投影纹理映射。也就是把那张shadowmap正确的映射到当前渲染的物体上面。
在ps 中,得到shadowmap的深度信息,和当前点的深度信息做比较,如果当前点的深度更大,说明被阴影覆盖,算最后颜色的时候,就要考虑到阴影的颜色,反之就是没有被阴影覆盖的区域,不需要考虑阴影的颜色。