很宝贵的资料,总结的相当不错。目前原创作者未知,如有知情者肯请告知,感激不尽~
这个教程主要面对DirectX9.0的初学者,文中代码说明部分以DirectX9.0c SDK(August2006)中的ShadowMap Sample 为例进行讲解。如果没有D3D矢量运算基础,HLSL,或是对D3D流程不熟悉的朋友推荐《 3D游戏程序设计入门》(翁云兵翻译)这本电子文档,图书推荐《Visual C++/DirectX9 3D游戏开发导引》(叶至军)作为入门读物,另外directx9_c的帮助文件永远是开发人员的参考手册。
二.介绍
阴影贴图(Shadow Map)是Williams在1978年提出的,可以说是所有阴影处理中最简单的方法,图1-1展示了使用这一方法的渲染效果.
![](http://hi.csdn.net/attachment/201108/7/0_1312714414GWBY.gif)
图1-1:图中为一立方体,在一聚光灯照射下地面呈现的阴影效果
三.原理
在阴影贴图算法中,每个光源都相当于一个独立的摄像机,都有个一个独立的阴影贴图。
(比如说场景需要两盏灯光,那么就需要建立三个摄像机,一号二号摄像机的位置和朝向与两个灯光相同,三号摄像机为真正渲染成最终画面的摄像机。)
这里的阴影贴图(IDirect3DTexture9)不同于我们以往使用的D3DFMT_A8R8G8B8类型的彩色贴图,而是D3DFMT_R32F即每点存储32位灰度的格式。此外每个阴影贴图还要使用深度模板(DepthStencilSurface)。
图2-2展示了一个局外视角观察的场景中摄像机,物体,灯光的全境。
![](http://hi.csdn.net/attachment/201108/7/0_1312714420qN2y.gif)
图2-2:摄像机坐标系,世界坐标系,灯光坐标系。
立方体下的阴影可以用如下方法描述:
1.首先以灯光为视点,渲染出一幅带深度缓冲的平面阴影贴图(Shadow Map),图1-3展示的是以灯光为视点我们我看到的平面阴影贴图。
![](http://hi.csdn.net/attachment/201108/7/0_1312714427IZ7C.gif)
图1-3:以灯光为摄像机位置,我们所看到的图像。
此平面图就是一张32位灰度经过深度模版处理过的阴影贴图,灰度代表以灯光视点出发,穿过投影面这点,形成的射线经过的所有场景中的点中最近的那个点地深度值,把该值通过某种映射变为灰度值,并且该条射线所有穿过的点灰度值都被设定为了这个灰度。
可以看到在阴影贴图中我们是看不到任何阴影的,这符合我们的常识,图中的颜色灰度记录了从灯光视角点出发一条射线上所有顶点的Z深度值信息(我们把Z深度转换为灰度颜色渲染成图,这些灰度我们成为“深度相对灰度”),那些被遮挡的顶点恰恰是产生阴影的点。(有关Z深度解释请查看前面提供的参考书)。
2.下面我们的任务就是要在正常坐标系中找出这些被遮挡的点,然后把它们渲染成黑色。就完成了我门全部的工作。我们发现这些在灯光摄像机下的被遮挡点何其它点在正常摄像机下的区别是:把正常摄像机坐标系的点变换到灯光坐标系,然后投影变换,得到了和图1-3相同视角的图.
![](http://hi.csdn.net/attachment/201108/7/0_13127144340qHQ.gif)
图1-4:没有阴影贴图的正常摄像机视角
那些被遮挡点代表深度相对灰度与阴影贴图的代表灰度的颜色相比是大的,而其它亮区的点经过正常摄像机到灯光摄像机变换,灯光摄像机投影变换这两次坐标系变换后,深度相对灰度的颜色与阴影贴图的灰度的颜色相比是相同的。
图1-5显示了这一比较渲染过程,A,B,C三点都是正常摄像机可以看到的点,但在灯光摄像机下被投影成了一条线上灰度相同为A:Zlight=2,B:Zlight=2,C:Zlight=2 (均为C点的深度相对灰度)的点,并被制成阴影贴图。我们把正常摄像机下的点变换到灯坐标系A,B,C,投影化,仍然保留了原来的深度相对灰度A:Zcamera=7,B:Zcamera=6,C:Zcamera=2 ,用原来的深度相对灰度和阴影图同样点的灰度作比较就可以得出该点是否处在阴影中的结论了。
![](http://hi.csdn.net/attachment/201108/7/0_1312714441sGf7.gif)
3.我们就可以用Shader把这些点单独处理成阴影。
四.代码及注释
我们主要分析DirectX9.0c SDK(August2006)中的ShadowMap例子。该源文件在
\Microsoft DirectX SDK (August 2006)\Samples\C++\Direct3D\ShadowMap中
这里简单提一下DXUT的结构:
DXUT入口函数是WinMain()
WinMain()依次调用:
DXUTSetCursorSettings();//设定鼠标指针
DXUTInit();//初始化DXUT管理结构
DXUTCreateWindow();//创建窗口
设定消息处理函数为用户函数MsgProc()
MsgProc()再把消息传递KeyboardProc(),MouseProc()
DXUTCreateDevice()//创建D3D设备接口,
其中调用用户函数OnCreateDevice()OnResetDevice()
DXUTMainLoop();//主循环
掉用OnFrameMove(),OnFrameRender()实现在处理运动,和渲染
DXUTGetExitCode()//销毁接口
OnLostDevice(),OnDestroyDevice()
在MainLoop()之前我们都可以加入我们自己的初始化函数,比如InitializeDialogs()。
DXUTSetCallbackDeviceCreated( OnCreateDevice );
DXUTSetCallbackDeviceReset( OnResetDevice );
DXUTSetCallbackDeviceLost( OnLostDevice );
DXUTSetCallbackDeviceDestroyed( OnDestroyDevice );
DXUTSetCallbackMsgProc( MsgProc );
DXUTSetCallbackKeyboard( KeyboardProc );
DXUTSetCallbackMouse( MouseProc );
DXUTSetCallbackFrameRender( OnFrameRender );
DXUTSetCallbackFrameMove( OnFrameMove );
是为了让DUXT在该调用的时候调用我们自己写的函数,比如OnCreateDevice()
IsDeviceAcceptable()是测试硬件支持的用户函数,
ModifyDeviceSettings()是更改设备调用的用户函数,之后调用OnResetDevice()
我们的主要工作将集中在:
1.Main()里的初始化
2. OnCreateDevice()OnResetDevice()
3.OnFrameMove(),OnFrameRender()
其中OnFrameRender()调用RenderScene()来辅助完成渲染。
ShadowMap.cpp文件
1。初始化:
首先定义ShadowMap贴图的大小为512*512
#define SHADOWMAP_SIZE 512
然后设定正常摄像机的位置
D3DXVECTOR3 vFromPt = D3DXVECTOR3( 0.0f, 5.0f, -18.0f );
D3DXVECTOR3 vLookatPt = D3DXVECTOR3( 0.0f, -1.0f, 0.0f );
g_VCamera.SetViewParams( &vFromPt, &vLookatPt );
设定灯光摄像机的位置
vFromPt = D3DXVECTOR3( 0.0f, 0.0f, -12.0f );
vLookatPt = D3DXVECTOR3( 0.0f, -2.0f, 1.0f );
g_LCamera.SetViewParams( &vFromPt, &vLookatPt );
设定灯光的锥体张开角度大小
g_fLightFov = D3DX_PI / 2.0f;
灯光漫反射和位置
g_Light.Diffuse.r = 1.0f;
g_Light.Diffuse.g = 1.0f;
g_Light.Diffuse.b = 1.0f;
g_Light.Diffuse.a = 1.0f;
g_Light.Position = D3DXVECTOR3( -8.0f, -8.0f, 0.0f );
g_Light.Direction = D3DXVECTOR3( 1.0f, -1.0f, 0.0f );
D3DXVec3Normalize( (D3DXVECTOR3*)&g_Light.Direction, (D3DXVECTOR3*)&g_Light.Direction );
g_Light.Range = 10.0f;
g_Light.Theta = g_fLightFov / 2.0f;
g_Light.Phi = g_fLightFov / 2.0f;
OnCreateDevice()& OnResetDevice( )
创建效果接口
D3DXCreateEffectFromFile()
创建顶点声明
CreateVertexDeclaration()
加载网格模型并把模型顶点格式变为顶点声明格式
g_Obj[i].m_Mesh.Create()
g_Obj[i].m_Mesh.SetVertexDecl()
设定正常摄像机和灯光摄像机的投影矩阵
g_VCamera.SetProjParams( D3DX_PI/4, fAspectRatio, 0.1f, 100.0f );
g_LCamera.SetProjParams( D3DX_PI/4, fAspectRatio, 0.1f, 100.0f );
创建默认纹理
CreateTexture(&g_pTexDef)
把灯光的漫反射,张角传递给Shader
SetVector( "g_vLightDiffuse", (D3DXVECTOR4 *)&g_Light.Diffuse ) );
SetFloat( "g_fCosTheta", cosf( g_Light.Theta ) ) );
创建ShadowMap纹理
V_RETURN( pd3dDevice->CreateTexture( SHADOWMAP_SIZE, SHADOWMAP_SIZE,
1, D3DUSAGE_RENDERTARGET,
D3DFMT_R32F,
D3DPOOL_DEFAULT,
& g_pShadowMap,
NULL ) );
创建深度模版
V_RETURN( pd3dDevice->CreateDepthStencilSurface( SHADOWMAP_SIZE,
SHADOWMAP_SIZE,
D3DFMT_D24X8,
D3DMULTISAMPLE_NONE,
0,
TRUE,
& g_pDSShadow,
NULL ) );
设定ShadowMap的投影矩阵
D3DXMatrixPerspectiveFovLH( &g_mShadowProj, g_fLightFov, 1, 0.01f, 100.0f);
OnFrameMove()
更新模型位置,更新摄像机位置
OnFrameRender()
这是程序的重头戏
首先判断灯光是否为自用移动状态,为了便于说明,以下我们仅以自由灯光为例
if( g_bFreeLight )
把灯光摄像机变换矩阵赋予mLightView
mLightView = *g_LCamera.GetViewMatrix();
然后把设备下的旧的RenderTarget,和DepthStencilSurface保存起来,并把新的RenderTarget设为ShadowMap纹理下的表面,把DepthStencilSurface设为我们要返回的目标,目的是把图像渲染到ShadowMap纹理中,用g_pDSShadow保存深度信息,流程见图1-6
![](http://hi.csdn.net/attachment/201108/7/0_13127144482MtR.gif)
然后调用RenderScene( pd3dDevice, true, fElapsedTime, &mLightView, &g_mShadowProj )
开始渲染ShadowMap纹理和深度模版
这里我们把原程序改一下以便理解
渲染ShadowMap时,可以不进行SetVector( "g_vLightPos", &v4 )和SetVector( "g_vLightDir", &v4 )的操作。(SetVector( "g_vLightPos", &v4 )和SetVector( "g_vLightDir", &v4 )是第二次渲染正常摄像机时候需要传读的参数)
直接进行SetTechnique( "RenderShadow" )然后以灯光摄像机为视点开始渲染深度
下面进入RenderShadow 的Shader代码段
顶点处理
void VertShadow( float4 Pos : POSITION,//位置
float3 Normal : NORMAL,//法线
out float4 oPos : POSITION,//输出位置
out float2 Depth : TEXCOORD0 )//深度
{
oPos = mul( Pos, g_mWorldView );把顶点从世界坐标变到灯光摄像机坐标系
oPos = mul( oPos, g_mProj );//进行投影变换
Depth.xy = oPos.zw;//深度为投影变换后的ZW分量
}
像素处理
void PixShadow( float2 Depth : TEXCOORD0,//输入深度
out float4 Color : COLOR )//输出颜色灰度
{
Color = Depth.x / Depth.y;//把深度两个分量相除,得到颜色灰度,这就是我们说的深度到灰度的映射关系
}
下面我们分析一下代码在做什么
假定输入顶点经过灯光摄像机坐标系变化后的位置为(x,y,z),
下面进行投影变换
![](http://hi.csdn.net/attachment/201108/7/0_1312714453S6A4.gif)
该灰度被渲染到了ShadowMap纹理中,然后我们取出旧的RenderTarget面,和深度模版面,渲染正常摄像机的场景
在调用RenderScene( pd3dDevice, false, fElapsedTime, pmView, g_VCamera.GetProjMatrix() )前我们多了一个矩阵mViewToLightProj,这个矩阵目的是把正常摄像机坐标系中的点变换到世界坐标系下,在变换到灯光摄像机坐标系下,在做投影变换。
![](http://hi.csdn.net/attachment/201108/7/0_1312714462FuDY.gif)
图中a点是最下方黑点的正常摄像机坐标系下的向量,通过世界变换矩阵变到了b向量,再通过灯光摄像机矩阵变到了c,c在通过投影矩阵变到了灯光摄像机的投影空间。这个投影空间的深度灰度要比阴影贴图的深度灰度大,因为c的长度大于黑点在阴影贴图中的深度d, 所以那个黑点是位于阴影区域的。
RenderScene():
设置SetTechnique( "RenderScene" )
正常摄像机的Shader代码段
顶点处理
void VertScene( float4 iPos : POSITION,
float3 iNormal : NORMAL,
float2 iTex : TEXCOORD0,
out float4 oPos : POSITION,
out float2 Tex : TEXCOORD0,
out float4 vPos : TEXCOORD1,
out float3 vNormal : TEXCOORD2,
out float4 vPosLight : TEXCOORD3 )
{vPos = mul( iPos, g_mWorldView );
oPos = mul( vPos, g_mProj );//输出投影变换后的顶点
vNormal = mul( iNormal, (float3x3)g_mWorldView );//对法线进行摄像机坐标系变换
Tex = iTex; //纹理坐标复制
vPosLight = mul( vPos, g_mViewToLightProj );//把正常摄像机坐标系的顶点变到灯光投影面中
}
float4 PixScene( float2 Tex : TEXCOORD0,//坐标
float4 vPos : TEXCOORD1,//顶点输出位置
float3 vNormal : TEXCOORD2,//顶点法线
float4 vPosLight : TEXCOORD3 ) : COLOR//灯光摄像机的投影
{float4 Diffuse;
float3 vLight = normalize( float3( vPos - g_vLightPos ) );取得灯到点的向量
//如果这个向量在灯光主轴上的投影大于一个值则说明这个点超出了照射范围
//应该给处理为暗点
if( dot( vLight, g_vLightDir ) > g_fCosTheta ) // Light must face the pixel (within Theta)
//以下是投影空间到纹理空间的变换
{float2 ShadowTexC = 0.5 * vPosLight.xy / vPosLight.w + float2( 0.5, 0.5 );
ShadowTexC.y = 1.0f - ShadowTexC.y;
float2 texelpos = SMAP_SIZE * ShadowTexC;
//以下分了四种情况进行灰度的判断。并进行了空域滤波
float2 lerps = frac( texelpos );
float sourcevals[4];
sourcevals[0] = (tex2D( g_samShadow, ShadowTexC ) + SHADOW_EPSILON < vPosLight.z / vPosLight.w)? 0.0f: 1.0f;
sourcevals[1] = (tex2D( g_samShadow, ShadowTexC + float2(1.0/SMAP_SIZE, 0) ) + SHADOW_EPSILON < vPosLight.z / vPosLight.w)? 0.0f: 1.0f;
sourcevals[2] = (tex2D( g_samShadow, ShadowTexC + float2(0, 1.0/SMAP_SIZE) ) + SHADOW_EPSILON < vPosLight.z / vPosLight.w)? 0.0f: 1.0f;
sourcevals[3] = (tex2D( g_samShadow, ShadowTexC + float2(1.0/SMAP_SIZE, 1.0/SMAP_SIZE) ) + SHADOW_EPSILON < vPosLight.z / vPosLight.w)? 0.0f: 1.0f;
float LightAmount = lerp( lerp( sourcevals[0], sourcevals[1], lerps.x ),
lerp( sourcevals[2], sourcevals[3], lerps.x ),
lerps.y );
//如果是纯阴影的话我们可以知道LightAmount的值是0,如果是灯光照射面的话//LightAmount的值是1,如果是影的边缘的话,LightAmount值介于1和0之间。
Diffuse = ( saturate( dot( -vLight, normalize( vNormal ) ) ) * LightAmount * ( 1 - g_vLightAmbient ) + g_vLightAmbient )
* g_vMaterial;
//如果在灯照范围之外的点进行下面处理
} else
{
Diffuse = g_vLightAmbient * g_vMaterial;
}
//返回这个像素点在纹理贴图的像素颜色,并乘了一个代表明暗的漫反射系数代表光照
return tex2D( g_samScene, Tex ) * Diffuse;
}
说明:
如果不考虑空域滤波,我们可以让LightAmount =tex2D( g_samShadow, ShadowTexC ) + SHADOW_EPSILON < vPosLight.z / vPosLight.w)? 0.0f: 1.0f;
代码的意思是,tex2D( g_samShadow, ShadowTexC )-----阴影贴图上的该像素点的深度相应灰度。
vPosLight.z / vPosLight.w-----正常摄像机变到灯光投影后Z,W分量相除结果。
想一下这个变换公式:
![](http://hi.csdn.net/attachment/201108/7/0_1312714469pa5D.gif)
就会发现我们对vPosLight.z / vPosLight.w进行了同样的深度灰度映射,只不过我们没有再渲染纹理。这样两个经过同样处理的值可以进行比较了。比较的结果就是该点是否处于阴影区域的判据。
然后我们让阴影区域中的象素点漫反射乘0渲染,在灯光区域中的象素点漫反射乘1渲染。
到此为止ShadowMap 的思想和核心代码已经讲述完毕了。希望大家多研究研究代码还是有好处的,越学会觉得自己知识越贫乏,这就对了.