作者:mavaL


学习SDK例子真是快速加强编程能力的途径,然而虽如此,微软不仅在每个例子中展示了本次的技术重点,如
这个例子的ShadowMap,还煞费苦心把DEMO做得很好看,很复杂。不仅给我们看了固定的聚光灯光源制造的阴影,还
顺带展示了怎么模拟车前灯.看来在3D世界,模拟一切也不过是数学的问题,数学啊!T_T

这就给我们初学者者带来了:
1.学习难度直接成倍增加,面对纷繁复杂的知识点糅合在一起,我第一遍看代码时直接头晕目眩,找不到方向,
信心倍受打击。

2.这简直就是3D中的知识大餐,只要你有足够的耐心,坚定的决心和持久激昂的兴趣,以及分析归纳各个知识点,
各个击破,从而掌握的学习能力,那么学习透彻一个sample后,我想水平肯定是大大的进步啊。

关于这个ShadowMap例子,做做笔记,归纳一下知识点。

PS:总结的都是我自己思考的结果,不能保证正确,毕竟我也是一个正在学习的小菜鸟。

1.在程序开始处,手动构造了场景中所有模型的世界矩阵:D3DXMATRIXA16 g_amInitObjWorld[NUM_OBJ]。
学懂了矩阵变换,就搞得懂手动是怎么构造的了。关于矩阵的3D变换可是块难啃的骨头,得经过长期的学习实践和思考。
可参看《3D数学基础:图形与游戏开发》一书。
比如room.x这个模型的世界矩阵 3.5f, 0.0f, 0.0f, 0.0f
                             0.0f, 3.5f, 0.0f, 0.0f
                             0.0f, 0.0f, 3.5f, 0.0f
                             0.0f, 0.0f, 0.0f, 1.0f  是这样来的:
首先前三行代表线性变换,最后一行代表平移变换。
因为每一个行向量代表变换后的基向量,所以我们可以看出,room模型在变换到世界空间后,right,up,ahead指向还是没变。
还是符合标准左手坐标系。但是3.5则代表了缩放因子,我们看到转换后每个基向量都乘了缩放因子3.5,所以在世界空间中
room模型的大小被放大了3.5倍。这显然是为了符合场景需要而试探出来的。
最后一个行向量表示模型的原点经平移变换后在世界空间的新坐标。

plane在DEMO中飞行时我们看到其机身有点偏转,正是我们对其物体空间中的基向量进行世界变换的结果。
原来的right轴被变换成了(0.43301f, 0.25f, 0.0f, 0.0f),up轴被变换成了(-0.25f, 0.43301f, 0.0f, 0.0f)。
所以在世界空间中plane呈那种倾斜姿态。

另外,从car模型的世界矩阵中我们还能看出点端倪。我们看到其坐标原点被变换为(-14.5f, -7.1f, 0.0f,1.0f)。这也是
非常值得探讨的。因为car模型在其物体空间中是车头向-Z轴的,那么要想在DEMO中使其绕Y轴逆时针行驶的话,其初始位置必须放置在其行驶的圆的(-r,0)处。我想,当熟练掌握了这些知识点,下次自己布置场景时,就可以根据需要来手动构造模型的世界矩阵了。

2.在OnFrameRender中,这段计算当light固定在车头的灯光view矩阵值得好好研究。它说明了在数学这个强大的工具
面前,一切不过是浮云。
 

else
{
// Light attached to car.
mLightView = g_Obj[2].m_mWorld;
D3DXVECTOR3 vPos( mLightView._41, mLightView._42, mLightView._43 ); // Offset z by -2 so that it's closer to headlight
D3DXVECTOR4 vDir = D3DXVECTOR4( 0.0f, 0.0f, -1.0f, 1.0f ); // In object space, car is facing -Z
mLightView._41 = mLightView._42 = mLightView._43 = 0.0f; // Remove the translation
D3DXVec4Transform( &vDir, &vDir, &mLightView ); // Obtain direction in world space
vDir.w = 0.0f; // Set w 0 so that the translation part below doesn't come to play
D3DXVec4Normalize( &vDir, &vDir );
vPos.x += vDir.x * 4.0f; // Offset the center by 4 so that it's closer to the headlight
vPos.y += vDir.y * 4.0f;
vPos.z += vDir.z * 4.0f;
vDir.x += vPos.x; // vDir denotes the look-at point
vDir.y += vPos.y;
vDir.z += vPos.z;
D3DXVECTOR3 vUp( 0.0f, 1.0f, 0.0f );
D3DXMatrixLookAtLH( &mLightView, &vPos, ( D3DXVECTOR3* )&vDir, &vUp );
}

      这里,我觉得 
     

D3DXVECTOR4 vDir = D3DXVECTOR4( 0.0f, 0.0f, -1.0f, 1.0f );  // In object space, car is facing -Z
mLightView._41 = mLightView._42 = mLightView._43 = 0.0f; // Remove the translation
D3DXVec4Transform( &vDir, &vDir, &mLightView ); // Obtain direction in world space
vDir.w = 0.0f; // Set w 0 so that the translation part below doesn't come to play
这四句改成这样应该更好理解:
D3DXVECTOR4 vDir = D3DXVECTOR4( 0.0f, 0.0f, -1.0f, 0.0f ); // In object space, car is facing -Z
D3DXVec4Transform( &vDir, &vDir, &mLightView ); // Obtain direction in world space


 

      首先,得到car的世界矩阵,注意在OnFrameRender前面的OnFrameMove中,对car的旋转已经连接到了其世界矩阵中。
然后vPos记录其在世界空间中的位置。我们最终要求得的是车前灯的位置和其指向。车灯的指向等于车头的指向,而car在
物体空间中是面向-Z轴的,即(0,0,-1,0)。所以将其变换到世界空间,就得到了在世界空间中的指向。那么车头灯的位置
也能计算出来了:通过把vPos往车头方向移动一段距离就行了。
      最后求light的view矩阵: D3DXMatrixLookAtLH( &mLightView, &vPos, ( D3DXVECTOR3* )&vDir, &vUp );

3.在RenderScene中,D3DXVECTOR3 v = *g_LCamera.GetEyePt();
我们查看GetEyePt的定义是:const D3DXVECTOR3* GetEyePt() const { return (D3DXVECTOR3*)&m_mCameraWorld._41; }
这里我不懂为什么要这样定义,更直观的是这样吧:
return &D3DXVECTOR3(m_mCameraWorld._41,m_mCameraWorld._42,m_mCameraWorld._43);
还有像这种语句: *( D3DXVECTOR3* )&v4 = *g_LCamera.GetWorldAhead();  这样绕来绕去的摆弄指针,到底是什么意思?

4.ShadowMap的生成过程。生成阴影图是每次渲染的第一步。
 在OnFrameRender中,出现了几个东西:surface,render target,depth-stencil surface.
 一个Device只能有一个render target,比如我们默认的render target就是swap chain中的back buffer.
因为我们的阴影图是生成在一个texture上,所以我们要把render target设定为该texture上的surface:
 g_pShadowMap->GetSurfaceLevel( 0, &pShadowSurf );
 pd3dDevice->SetRenderTarget( 0, pShadowSurf );   这样我们的渲染操作就到阴影texture上了。
 另外:程序为新的render target设定了新的深度-模板缓冲:
 pd3dDevice->SetDepthStencilSurface( g_pDSShadow );  这样,在渲染时,设备会自动进行深度测试,记录Z值。
 因为在OnCreateDevice中,我们创建了g_pDSShadow,并设定其行为为自动处理深度模板测试:
  pd3dDevice->CreateDepthStencilSurface( SHADOWMAP_SIZE,SHADOWMAP_SIZE,d3dSettings.d3d9.pp.AutoDepthStencilFormat,
                                         D3DMULTISAMPLE_NONE,0,TRUE,&g_pDSShadow,NULL )

 在RenderScene中程序设定了ShadowMap.fx中的变量,然后:g_pEffect->SetTechnique( "RenderShadow" );
 我们现在可以看特效文件中产生阴影图的technique了。它是由1个pass,2个shader组成:VertShadow和PixShadow。
 这2个shader都很短,应该还是很好理解的。顶点着色器中,我们用一个输出纹理坐标保存了顶点变换后的z,w值:
   Depth.xy = oPos.zw;
然后在像素着色器中,程序输出最终的深度Z值到阴影图中:
   Color = Depth.x / Depth.y;
 这样,ShadowMap就生成了。

5.现在开始正常渲染场景了。mViewToLightProj这个矩阵很重要,通过它,原来摄像机空间的点先被转换回世界空间,再转换到光源的view空间,最后转换
到了我们要采样的纹理投影空间。

先是RenderScene这个technique,由2个shader:VertScene和PixScene组成,点就是像素着色器了。

float4 PixScene( float2 Tex : TEXCOORD0,
float4 vPos : TEXCOORD1,
float3 vNormal : TEXCOORD2,
float4 vPosLight : TEXCOORD3 ) : COLOR
{
float4 Diffuse; // vLight is the unit vector from the light to this pixel
float3 vLight = normalize( float3( vPos - g_vLightPos ) ); // Compute diffuse from the light
if( dot( vLight, g_vLightDir ) > g_fCosTheta ) // Light must face the pixel (within Theta)
{   //下面是投影纹理映射技术(Projective Texture Mapping),即根据投影后的顶点坐标来求其纹理贴图的对应纹理坐标。
//也就是最终顶点位置和我们的深度纹理图的纹理坐标的对应关系,是这样求的:我们知道,vPosLight是顶点在光源下的
//投影坐标,首先除以其次坐标w使其范围在[-1,1]内,然后乘以1/2再加1/2,使其转换到纹理坐标范围[0,1]内。这就是投影
//纹理映射。
float2 ShadowTexC = 0.5 * vPosLight.xy / vPosLight.w + float2( 0.5, 0.5 );   //我们生成的纹理贴图的y方向是向下为正,而投影空间是y向上为正。而且投影空间的y坐标0处对应纹理贴图的y坐标1处,
//想想场景投影到纹理贴图的方向吧。
ShadowTexC.y = 1.0f - ShadowTexC.y;   //这是根据纹理坐标计算texel
float2 texelpos = SMAP_SIZE * ShadowTexC;

// Determine the lerp amounts
float2 lerps = frac( texelpos ); //下面几句是所谓的啥空域滤波,就是使图像更精准的数字处理技术吧,我也不懂。
float sourcevals[4];
   //在取得的texel,根据其上下左右4个相邻的texel,来取样纹理贴图的数据,与该顶点的深度值作比较。
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 );
// Light it
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;
}

我们也可以看看不采用双线性插值和空域滤波是啥效果:

float LightAmount = (tex2D( g_samShadow, ShadowTexC ) + SHADOW_EPSILON < vPosLight.z / vPosLight.w)? 0.0f: 1.0f;


另外:在ShadowMap.fx中定义的  #define SHADOW_EPSILON 0.00005f

这是个控制两个浮点数比较精度的量

虽然我现在完全不能保证马上能手动实现ShadowMap特效了,但还是学到了不少东西。持之以恒吧!