作者: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矩阵值得好好研究。它说明了在数学这个强大的工具
面前,一切不过是浮云。
这里,我觉得
首先,得到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组成,点就是像素着色器了。
我们也可以看看不采用双线性插值和空域滤波是啥效果:
另外:在ShadowMap.fx中定义的 #define SHADOW_EPSILON 0.00005f
这是个控制两个浮点数比较精度的量
虽然我现在完全不能保证马上能手动实现ShadowMap特效了,但还是学到了不少东西。持之以恒吧!