最近工作中需要使用阴影算法,于是把阴影图(shadowmap)从无到有进行了一遍学习并实现,而且进行了优化,此算法的具体用途和应用就不多做描述,转入实现细节。
阴影图有两遍渲染:
第一遍渲染:
A、首先,这里会生成一张保存当前所有模型里面离光源最近点的距离信息的纹理,这张纹理需要有一个东西来保存,从而我们可以在第二遍渲染的时候可以使用它
我们可以在渲染之前生成一张空纹理,并把它绑定到一个rendertarget里面去,并把这个rendertarget于当前device绑定;
纹理的大小与阴影的精度有密切关系,512x512的纹理比1024x1024的纹理要粗糙很多
B、将需要生成阴影的模型顶点输入vertexshader;
C、在vertexshader的坐标转换中,使用以光源位置为参考的观察矩阵和投影矩阵对顶点进行坐标变换;
这里的光源的投影矩阵可以是正交投影,适用于平行光
也可以是透视投影,适用于聚光灯,或者限定方向的点光源
投影矩阵的近裁面决定距离模型多近阴影就会消失
投影矩阵的远裁面大小决定阴影生成的精度,越大,精度越低
代码片段:
float4 vPos = mul( Pos, g_mWorldView ); //转换到观察坐标系
oPos = mul( vPos, g_mProj ); //转换到投影坐标系
D、把这些模型的顶点转换到投影空间的坐标值作为纹理坐标,在pixelshader中根据这些纹理坐标,把光源和当前顶点的距离信息作为颜色值保存起来;
1、普通的sm采用的算法
将顶点转换到投影坐标系后,将深度信息作为颜色值保存进阴影图,这个处理过程很简单
代码片段:
return oPos.z/oPos.w;
这种处理方式在光源发出的光线和物体某一些面平行的时候,因为这些平面上的点,从光源的位置看过去的深度差距此时是最小值,从而导致很大范围的深度信息被缩减到很小的颜色范围,
第二次渲染的时候因为颜色保存的值精度不够而产生严重的阴影错误;
2、vsm采用的算法
将顶点转换到观察坐标系后,原点是光源所在的点,所以这个时候求顶点到原点的距离就是顶点到光源的距离,可以使用如下公式:
length(vPos.x, vPos.y, vPos.z);
来计算观察坐标系下顶点到光源的距离
我使用的优化后的阴影图算法就是用的这个距离作为颜色保存进阴影图内的,保存的值计算方式如下
float2 moments;
moments.x = length(vPos.x, vPos.y, vPos.z); //计算顶点到光源距离
float dx = ddx(moments.x); //计算x方向偏导数
float dy = ddy(moments.y); //计算y方向偏导数
moments.y = moments.x * moments.x + 0.25*(dx*dx + dy*dy) //计算需要保存的颜色值的分量之一,dx和dx主要是根据距离变化,作为第二次渲染时计算是否包含在阴影内的一个动态变化的bias,减少阴影生成时因为精度损失造成的z-fighting
Color = float4(moments.xy, 0, 1); //传出颜色信息
这里需要注意的是使用这种算法,之前绑定rendertarget的纹理生成参数格式需要是D3DFMT_G32R32F,这一个格式可以有效的保证用来保存光源和模型顶点距离的颜色值不会造成精度丢失,OpenGL实现时也可以使用类似的格式