天天看点

ShadowMap渲染阴影方法及问题

ShadowMap基于的原理:SM算法是一个2-pass绘制算法,第一pass从光源视点绘制场景,生成SM纹理,第2pass从视点视图按常规方法绘制场景

从光源的位置观察场景,这时候我们看不到的地方就是该有阴影的地方,于是可以使用比较像素到光源距离的方法来确定某个像素是否在阴影之中。

于是我们需要记录我们看得到的像素的距离值,以便做比较。

首先,创建在光源位置处的观察坐标系,这一步应该在CPU阶段完成,这里为描述方便,写为HLSL代码

这里假设观察方向指向原点

float3 dirZ = -normalize(lightPos);

float3 up = float3(0,0,1);

float3 dirX = cross(up, dirZ);

float3 dirY = cross(dirZ, dirX);

接下来,把场景中所有顶点变换到这个光源-观察空间中(light-view-space)

实际中应使用矩阵进行变换,这里直接做

   float4 vPos;

   vInPos.xyz-=vLightPos.xyz; //首先是平移变换

   vPos.x=dot(vInPos,x_dir);   //接下来是分别绕3个轴旋转

   vPos.y=dot(vInPos,y_dir);

   vPos.z=dot(vInPos,z_dir);

   vPos.w=1;

然后进行light-view-space空间里的投影变换,所使用的矩阵要根据光源的特点进行改变,如FOV等

最后将这时得到的结果渲染到一张纹理上,把它称作shadowmap

这张纹理一般使用R16F,或者R32F格式,而一些集成显卡中或者OPENGL不支持这样的格式,还可以使用整数纹理,这时需要对深度值进行压缩。

渲染结束后,在纹理中保存的就是在光源处能看见的像素到光源的距离了,为了节省shader指令,可以使用距离的平方

即在浮点纹理中为 return dot(vLightVec,vLightVec);

在整数纹理中为  float fDepth=dot(vLightVec,vLightVec);

return float4( floor(fDepth) / 256.0f, frac(fDepth), frac(fDepth), frac(fDepth), frac(fDepth));

通过把frac(fDepth)同时写入蓝色和alpha通道,这样可以节省一条指令,否则需要另一个指令来填充这些通道

mov r2.gba r0.g   //r0.g包含frac(fDepth)

vertex shader

float4x4 matViewProjection;

float4x4 matProjection;

float4 vLightPos;

float fDistScale;

float fTime0_X;

struct VS_OUTPUT

{

float4 Position : POSITION0;

float3 vLightVec:TEXCOORD0;

};

VS_OUTPUT vs_main(float4 vInPos:POSITION )

{

VS_OUTPUT output;

output.Position = mul(vInPos, matViewProjection );

//光源运动,做demo时应在cpu阶段完成

float3 vLightPos;

vLightPos.x=cos(1.321*fTime0_X);

vLightPos.z=sin(0.923*fTime0_X);

vLightPos.xz=normalize(vLightPos.xz)*100;

vLightPos.y=100;

//创建在光源处的坐标系,应在cpu完成

float3 z_dir=-normalize(vLightPos);

float3 up=float3(0,0,1);

float3 x_dir=cross(up,z_dir);

float3 y_dir=cross(z_dir,x_dir);

//将顶点变换到光源空间中,实际中应用矩阵变换

float4 vPos;

vInPos.xyz-=vLightPos.xyz;

vPos.x=dot(vInPos,x_dir);

vPos.y=dot(vInPos,y_dir);

vPos.z=dot(vInPos,z_dir);

vPos.w=1;

//在光源空间中投影,实际中应根据FOV等因素构造特殊的投影矩阵,这里用默认的

output.Position=mul(vPos,matProjection);

//fDistScale用于把距离值规格化到[0,1]之间,fDistScale一般取1/farZClip

output.vLightVec=fDistScale*vInPos;

return( output );

pixel shader

float4 ps_main(float3 vLightVec:TEXCOORD0) : COLOR0

{

//压缩深度值到整数纹理

float fDepth=dot(vLightVec,vLightVec);

return float4(floor(fDepth)/256.0f,frac(fDepth),frac(fDepth),frac(fDepth));

}

到这里准备工作就结束了,接下来就是要渲染阴影的时候了

这时回到正常观察位置即camera位置,不再在light-view-space中观察

将场景中每个顶点再变换到light-view-space中,这是为了要找到顶点在light-view-space中的位置,以便与纹理中的距离值做比较

将已经变换的顶点再进行投影,再将投影平面坐标变换到纹理坐标空间,即把范围为[-1,1]的x,y坐标变换到[0,1]的范围中去,以便匹配相应的纹素texel

然后就是光照,既然有阴影必然有光照效果,而且应该是逐像素光照,可以使用任何光照模型,phong,blinn或者Oren-Nayar

最后的距离比较决定是否为阴影,光照计算,都在像素shader中进行

float fBackProjCut;

float fKa;

float fKd;

float fKs;

float4 vLightColor;

float fShadowBias;

sampler ShadowMap;

sampler SpotLight;

float4 ps_main(

float3 vNormal:TEXCOORD0,

float3 vViewVec:TEXCOORD1,

float3 vLightVec:TEXCOORD2,

float4 vShadowCrd:TEXCOORD3) : COLOR0

{

vNormal=normalize(vNormal);

float fDepth=dot(vLightVec,vLightVec);

vLightVec=normalize(vLightVec);;

float fDiffuse=saturate(dot(vLightVec,vNormal));

float fSpecular=pow(saturate(dot(reflect(-normalize(vViewVec),vNormal),vLightVec)),16);

float3 vShadowMap=tex2Dproj(ShadowMap,vShadowCrd);

float fClosestDepth=vShadowMap.r*256+vShadowMap.g;

//光照图,聚光灯

float fSpotLight=tex2Dproj(SpotLight,vShadowCrd).r;

//把像素的深度值与对应纹理中的深度值比较,得出阴影值0(无) or 1(有)

float fShadow=(fDepth-fShadowBias<fClosestDepth);

//cut back projection保证不会照亮聚光灯背后的像素

fShadow=fShadow*(vShadowCrd.w>fBackProjCut);

fShadow*=fSpotLight;

return fKa*vLightColor+(fKd*fDiffuse*vLightColor+fKs*fSpecular)*fShadow;

}

ShadowMap渲染阴影方法及问题
ShadowMap渲染阴影方法及问题

由于SM算法基于图像空间,所以有一些缺陷,如果视点与光源位置差异很大,会产生明显走样,阴影边缘处会出现明显的阶梯状,这可以使用靠近百分比过滤PCF来改善,因为SM纹理中每个纹素texel可能不是投影到单一屏幕像素上,纹理分辨率越低,走样越严重。

这里的算法也不适用于点光源,仅仅在聚光灯时有效,要用于点光源,则需要3D纹理,进行6次渲染,过几天把这个做一下。

继续阅读