本文版权归博客园  mavaL所有,如有转载请按如下方式详细标明原创作者及出处,以示尊重!!

原创作者:mavaL

原文链接:DirectX9 ShadowMap例子学习笔记

学习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
这是个控制两个浮点数比较精度的量,参见这个帖子:
http://topic.csdn.net/u/20090818/09/d42dd7d8-9aed-4116-99b2-d227ce2cf04b.html

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

DirectX9 ShadowMap例子学习笔记相关推荐

  1. Qt例子学习笔记 - Examples/Qt-6.2.0/charts/callout

    //QChart 类提供了两种在场景坐标和系列域(由轴范围定义)之间映射的方法. //QPointF QChart::mapToPosition(const QPointF &value, Q ...

  2. python基础代码事例-学习笔记:python3,代码。小例子习作(2017)

    http://www.cnblogs.com/qq21270/p/7634025.html 学习笔记:python3,一些基本语句(一些基础语法的代码,被挪到这里了) 日期和时间操作 http://b ...

  3. TensorFlow学习笔记(二):快速理解Tutorial第一个例子-MNIST机器学习入门 标签: 机器学习SoftmaxTensorFlow教程 2016-08-02 22:12 3729人阅

    TensorFlow学习笔记(二):快速理解Tutorial第一个例子-MNIST机器学习入门 标签: 机器学习SoftmaxTensorFlow教程 2016-08-02 22:12 3729人阅读 ...

  4. Tsai笔记:GPOPS学习笔记(1)—— 高斯伪谱法的最基本优化方程求解思路(例子介绍)

    Tsai笔记:GPOPS学习笔记(1)-- 高斯伪谱法的最基本优化方程求解思路(例子介绍) Tsai三步.(第一步,基本说明.第二步,结果图显示.第三步,代码展示.) 第一步,基本说明. A.问题说明 ...

  5. ceres快速教材及学习笔记(四)bundl adjustment,《视觉slam十四讲》第十讲ceres例子

    0. 前言 本文是根据ceres官方教程内容ceres-solver官方教程链接,再结合自己理解的一个ceres快速学习笔记. 在博文ceres快速教材及学习笔记(一)hello,world!中,我们 ...

  6. 关于一个非寿险费率厘定例子的学习笔记

    关于一个非寿险费率厘定例子的学习笔记 一.计算在当前费率水平下的已赚保费 1.1实操 1.2 理论补充 1.2.1 风险基础和风险单位 1.2.2 保费及其构成 1.2.3 数据的汇总方法 1.2.4 ...

  7. java学习笔记:里氏代换原则的两个例子

    java学习笔记:里氏代换原则的两个例子 (根据里氏代换原则[能使用父类型的地方一定能使用子类型],抓取 ExampleA类型异常的catch块能够抓住try块中抛出的ExampleB类型的异常) p ...

  8. 小傻羊7.31学习笔记——AOP例子和日志的实现实例

    7.31学习笔记--AOP例子和日志的实现实例 转载小栗子 https://blog.csdn.net/zhuzhezhuzhe1/article/details/80565067 切口的实现,sys ...

  9. 阿里云HaaS100物联网开发板学习笔记(六)做个智能灯---一个完整的开发例子

    摘要:本篇文章将前期几个专题综合起来,基于阿里云HaaS100的新固件设计制作一个智能灯.这个智能灯由云平台.手机APP端和设备端组成,基本上涵盖了一个物联网小项目所需的主要步骤. 目录 1.在阿里云 ...

最新文章

  1. 用小神经网络和光谱仪优化关键词识别
  2. opc ua 服务器模拟_西门子S71500的OPC通讯组态
  3. 关于Android的EditText焦点问题
  4. server2008R2平台部署exchange2010
  5. android studio复选按钮样式_Ubuntu与Android开发的邂逅
  6. 非阻塞I/O多路复用机制
  7. python游戏入门书籍推荐
  8. ansible 并发设置_如何使用Ansible通过Prometheus设置系统监视
  9. sqlserver_identity
  10. 图解MySQL的各种 JOIN,看完不懂来找我!
  11. 数据结构-直接选择排序
  12. 【架构】需求决定架构 —— 萌Mark的架构升级之路
  13. MTK平台Metadata的加载(4)—Q版本后
  14. 企业业务架构设计方法论及实践(二)
  15. 红队快速打点工具(POC bomber)
  16. 【转】PID算法原理 一图看懂PID的三个参数
  17. 小傻蛋的妹妹跟随小甲鱼学习Python的第五节005
  18. 基于机器学习建模的 XSS 攻击防范检测
  19. Java面试知识点之Java基础
  20. 洛谷 P5027 Barracuda 题解

热门文章

  1. Anguar 使用interceptor拦截器设置请求头传入jwt token
  2. java web不用框架_初学javaweb,远离各自框架
  3. 森林怎么训练野人_第五人格:野人技能曝光!野猪可以骑,庄园中或将迎来新玩法!...
  4. ashx在web.config中如何配置_网络中,什么是半双工与全双工?它们如何配置
  5. 2019年开发者必读!20位阿里技术大牛们帮你列了一份经典书单!...
  6. 学习网页前的网页知识储备
  7. 诺基亚在日本测试5G网络 网速可达256MB/s
  8. 【吐血经验】在 windows 上安装 spark 遇到的一些坑 | 避坑指南
  9. mysql双主+keepalived【转】
  10. 几行 python 代码合成 gif / 微信表情~与恶意合成软件说再见【文末附代码】