1. ShadowMap原理:
首先准备一张纹理贴图,和一个深度模版。
准备深度模版的作用是为了进行深度测试,使得纹理贴图只用记录每个像素深度最小的值。
1:.将世界坐标系下的顶点转换到光源视点下,再投影,最后渲染一遍场景,得到一张带深度缓冲区的纹理贴图(以纹理贴图为渲染目标),但是纹理贴图中的记录的不是颜色值,而是深度值,由于使用了深度测试模版所以该贴图里得到的深度值是每个像素点的最小深度值(即最接近光源的深度值)此时得到了shadowmap。
2:将相机视点下的顶点转换到光源视点下投影,逐像素的用当前像素的深度值与纹理贴图中对应的像素点进行比较深度值(把投影后的x,y坐标计算u,v坐标 便可找到对应的点),来判断是否会产生阴影。最后在视点下绘制
2. 例子分析:
在OnCreateDevice() 函数中建立.fx文件与应用程序之间的关联,g_pEffect是指向该效果的指针
在OnResetDevice()中创建阴影图所需要的纹理贴图以及深度模版,并设定深度模版的行为是自动处理深度模版测试
在OnFrameRender()中将我们创建的g_pShadowMap作为渲染目标,g_pDSShadow作为深度模版,并保留原来的渲染目标与深度模版
下面开始分析OnFrameRender()中的代码
g-bFreeLight是用来判断当前的光源是否为自由光(在例子有2个光源,一个是自由光 一个是汽车的车头灯)。如果g_bFreeLight为true 那么就把光源相机的视点矩阵赋值给mLightView; g_LCamera是一个放在光源处的模拟相机(生成阴影图需要以光源处的视野来做投影,也就相当于将相机放在光源位置,朝着光源的照射方向)。
如果bFreeLight为flase则将汽车的世界矩阵传递给mLightView,把汽车的位置传递给vPos,方向传递给vDir(这个方向是在局部坐标系下的 汽车的朝向)将mLightView的第4个分量设置为0 是由于vDir的w分量是1,但是vDir是向量 所以为了保证Transform()的正确性 将其设为0。
我们利用处理过的mLightView变换Dir,使其变成在世界坐标系下的汽车朝向。
在这里将vDir.w=0.0f, 是为了保证下面的单位化向量的正确性,因为将w置为0之后,Dir其实被变成了一个三维空间的点.(x,yz,0)单位化以后 是和(x,y,z)一样的。
我们将使用VPos和VDir 作为D3DXMatrixLookAtLH()中的相机位置 和 相机观察点 这2个参数。
但是此时的vPos和VDir还无法直接传进去,因为此时vPos只是 汽车的位置,而不是车头灯光的位置,所以应该将vPos往车头方向移动一些。
vDir始终是在vPos的基础上 朝vDir的方向移动一些,这样保证了灯光与车的朝向一致
此时以车灯为视点的观察矩阵已经求好了,下面调用RenderScene()渲染场景(也就是绘制阴影图)
下面分析RenderScene()中的代码
在RenderScene()中定义g_pEffect中的一些变量的值,然后调用了SetTechnique()使用不同的technique.绘制阴影图调用的technique是technique RenderShadow,
在vershadow中,首先将pos先转到世界坐标系再转换到光源视点的坐标系(g_mWorldView),再将其投影(g_mProj)
用Depth记录z值。
在PixShadow中,用Depth.x/Depth.y也就是oPos.z/oPos.w.在渲染的过程中,由于我们启用了自动的深度模版测试,所以每个像素的z值都是该像素的最小z值.
至此 已经拥有了一张阴影图g_pShadowMap。
继续回到OnFrameRender();
下面进行第二次渲染首先将原来的渲染目标和深度模版都替换回去
g_bCameraPerspective为true时,从相机视角的坐标系下渲染场景,为false时,从光源视角下的坐标系渲染场景,并且把我们刚才得到的shadowmap与fx文件里的g_txShadow进行关联
mViewToLightProj 首先是将pmview求逆, (得确保这个定点在世界坐标系下)再换到光源视点坐标下下,再进行投影的变换矩阵,设置完g_mViewToLightProj之后 再调用了一次RenderScene();
与上一次调用相比,参数二变成了false ,参数四由光源视点矩阵,变成了pmView(pmView有可能是mLightView 也有可能是正常相机的视点),参数五由光源的投影矩阵变成了正常相机的投影矩阵
在RenderScene()中,调用了g_pEffect->SetTechnique( "RenderScene" )
这个technique包含2个渲染器VertScene()和PixScene()
Mul(iPos,g_mWorldView) 是将顶点转换到世界坐标系下,再转换到pmView表示坐标系下.
Mul(vPos,g_mProj) 投影
Mul(iNormal,(float3x3)g_mWorldView);将法线转换到世界坐标系下,再转换到pmView表示坐标系下.
Text = iTex 复制纹理
vPosLiht = mul(vPos,g_mViewToLightProj) 首先会将vPos转换到世界坐标系下(因为取反了pmView)再转换到光源的视野坐标系,再投影.
vLight是 点到光源向量的单位向量
Dot(vLight,g_vLightDir)是cos(点到光源的向量与光源的方向向量的夹角).
如果这个值大于g_fCosTheta(是光源的角度的一半)则说明在灯光中。ShadwoTexC是纹理贴图坐标,通过这个坐标可以从我们绘制好的阴影图中采样
float2 ShadowTexC = 0.5 * vPosLight.xy / vPosLight.w + float2( 0.5, 0.5 );这一步将在投影空间下的坐标变成0~1的范围
ShadowTexC.y = 1.0f - ShadowTexC.y; 由于在贴图空间内,坐标轴y是向下的,与投影坐标系内正好相反,所以要把投影坐标系里的y值反向。到此ShadowTexC已经变成了贴图空间的坐标.
Texelpos是纹理空间中对应的像素坐标,lerps是Texelpos的小数部分。屏幕中的像素点是不存在小数的,所以在这里产生了一个误差,无法找到正确的深度值做为对比对象
例子中使用了2X2插值的办法来弥补这个误差,取临近4个点,做差值,得到LightAmount,用这个LightAmount来控制光线。
Diffuse由漫反射光和环境光组成,其中漫反射光为 saturate( dot( -vLight, normalize( vNormal ) ) ) * LightAmount *( 1 - g_vLightAmbient ).
( 1 - g_vLightAmbient ).为光源的亮度,dot( -vLight, normalize( vNormal ) ) 计算的是光源视点下的顶点法线与点到光源的单位向量的夹角。
如果该像素不在灯光的照射中,那么Diffuse就是环境光*材质,之后返回纹理*Diffuse