(转)【D3D11游戏编程】学习笔记二十三:Cube Mapping进阶之动态环境图

(注:【D3D11游戏编程】学习笔记系列由CSDN做者BonChoix所写,转载请注明出处:http://blog.csdn.net/BonChoix,谢谢~)前端

  

       在前面两篇介绍Cube Mapping的文章中,咱们所使用到的Cube Map都是事先制做好的,这样的一个好处就是运行时效率很高,适合于大多数情形。但若是对于即时动态变化的场景来讲,依靠静态图来实现反射效果就再也不适用了。由于在不一样时刻,一个物体周围的场景是不断变化的,想要把这些变化在物表的反射中体现出来,就须要一张动态的环境图。编程

 

       一、Cube Map相关参数

       动态的环境图显然是不可能事先制做出来的,而须要在运行时实时的计算得出。在前面咱们也不止一次地提到过环境图的得到方法:即把照相机放置于物体中心,视角调整为90度,向其正X、负X、正Y、负Y、正Z、负Z六个方向上拍摄对应的六张照片,这样获得的六张图即组成了当前时刻该物体周边的环境图。此外,这里还要指出下,仅仅拥有的这六张图是不够,六张图的顺序、相机摆放也很重要,这样才能被正确地映射(至少在使用HLSL中的Sample函数时是这样)。数组

       在前面咱们仅仅是用到了已经制做好的cube map,所以这些因素就不须要再去关注了。但如今咱们须要本身来生成环境图,所以必需要搞清楚。在拍摄环境图时,咱们遵循的顺序为:正负X、正负Y、正负Z。以下图为一张环境图的展开图及其对应的序号:(这里针对的是左手坐标系)app

        关于相机摆放,从上图的展开情形应该也能看得出来:默认情形下相机面朝Z+方向。所以对于正负X面的两张图,只须要相应地让相机水平旋转到正X和负X面上便可;对于正负Y面,须要把相机沿X轴旋转使其四脚朝天、低头朝下摆放;正负Z面同正负X面相似,水平旋转使其对准Z+和Z-便可。函数

       涉及到程序中对相机摆放的设置,咱们须要指定的基本参数依然是:位置、观察点、相机对应的up向量。假设一个物体位于(x, y, z)处,如今拍摄它的环境图,相机的设置相关代码以下:oop

[cpp]  view plain copy
  1. XMFLOAT3 ups[6] =   
  2. {  
  3.     XMFLOAT3( 0.f, 1.f, 0.f),  
  4.     XMFLOAT3( 0.f, 1.f, 0.f),  
  5.     XMFLOAT3( 0.f, 0.f,-1.f),  
  6.     XMFLOAT3( 0.f, 0.f, 1.f),  
  7.     XMFLOAT3( 0.f, 1.f, 0.f),  
  8.     XMFLOAT3( 0.f, 1.f, 0.f)  
  9. };  
  10.   
  11. XMFLOAT3 targets[6] =   
  12. {  
  13.     XMFLOAT3( x + 1.f, 0.f, 0.f),  
  14.     XMFLOAT3( x - 1.f, 0.f, 0.f),  
  15.     XMFLOAT3( 0.f, y + 1.f, 0.f),  
  16.     XMFLOAT3( 0.f, y - 1.f, 0.f),  
  17.     XMFLOAT3( 0.f, 0.f, z + 1.f),  
  18.     XMFLOAT3( 0.f, 0.f, z - 1.f)  
  19. };  
  20.   
  21. for(UINT i=0; i<6; ++i)  
  22. {  
  23.     m_dynamicCameras[i].LookAt(XMFLOAT3(x, y, z), targets[i], ups[i]);  
  24.     m_dynamicCameras[i].SetLens(XM_PI*0.5f,1.f,1.f,1000.f);  
  25.     m_dynamicCameras[i].UpdateView();  
  26. }  

       这里六个ups向量分别对应拍摄六张图时相机对应的上方向, targets为对应的六个观察点,经过LookAt函数设置相机摆放,SetLens设置相机高、宽比为1且视角为90度。UpdateView函数更新相机相关矩阵。学习

       2. Render to Texture技术

       好了,如今相机已经准备就绪,能够进行拍摄了。接下来的问题是,拍到的照片放哪儿去? 咱们拍照的目的是得到环境图,并把该环境图看成纹理资源接下来用于映射。所以咱们如今还须要一个用于保存拍照结果的缓冲区。这就涉及到图形技术上一个新的的概念:Render To Texture优化

       为此,咱们要建立一个空的纹理,该纹理包含六张普通二维纹理,分别用于保存六张拍摄到的照片。为了让拍摄的内容保存到该纹理中,咱们须要将该纹理看成新的Render Target,分别使用六个相机对应的变换矩阵如同正常状况同样对场景进行六次绘制。每次绘制时,使用纹理中相应的那一张做为当前的Render Target,这样场景绘制完六次后,其所获得的内容将会留在对应的纹理上。this

       以前全部的程序中,咱们的Render Target一直是后缓冲区,绘制完后经过交换链将内容显示到前端(显示器)。这里咱们使用了另外一张纹理做为Render Target, 即把当前一帧绘制到纹理中,所以这种技术称为“绘制到纹理中”,即“Render To Target”,就是这么通俗。。。。spa

       Render To Target如今被普遍应用于各类特效的实现中,这里仅仅是其应用之一,其余方面的应用也特别多,好比用于阴影绘制的Shadow Mapping,其中使用的Shadow Map就是经过Render To Texture技术获得的(不过这里并非将纹理做为Render Target而绘制到其中的,而是经过渲染场景将深度信息记录到咱们额外建立的深度缓冲区中去,这个深度缓冲区就是咱们要的Shadow Map),有关Shadow Mapping的详细介绍在后面会有,敬请关注~

      

       3. 环境图的建立、绘制及使用

       继续回来正题上来,经过Render To Target, 咱们如愿得到了“求之不得”的环境图~ 到这步为止,相比以前的静态环境图例子,咱们仅仅至关于得到了该图片。为了将其用于后面的映射, 如前面同样,咱们要使用该纹理对应的纹理视图,即D3D11中的Shader Resource View。

       在前面介绍纹理基础的文章中,提到了从纹理(ID3D11Texture2D)建立相应视图的方法,这里是同样的道理。

       注意到,这里建立纹理资源视图用到的纹理,与以前做为Render Target的纹理,是同一个!在物理内存中只存在一个纹理,但咱们将其用于两个地方:做为Render Target,以及做为Shader Resource。换句话说,该纹理能够被绑定到渲染管线的多个阶段,只要在建立纹理时指明其能够被绑定到的阶段(D3D11_BIND_XXX),这样对于每一个不一样的用途,分别建立相应的视图(View)便可。   这正是D3D11处理纹理的方法,详细状况能够参考D3D11中纹理的使用

       纹理资源视图也建立好,以后就是使用它进行Cube Mapping了,相关步骤与前面的静态环境图例子彻底同样。

 

       接下来,咱们把重点放在环境图纹理的建立、绘制上面来。

              3.1 环境图及其相关视图的建立

        首先,咱们即将建立的纹理须要包含六张普通2维纹理,前面刚开始介绍Cube Map时也提过,d3d11中的ID3D11Texture2D不只能够用来表明一张2维纹理,也能够用于表明一个纹理数组,以及每张纹理的全部mip链。

       其次,该纹理须要绑定到管线的两个阶段:Render Target和Shader Resource。

       除了这两点核心问题以外,再加上一些好比纹理宽度、高度等信息便可。如下为建立纹理相关代码:

[cpp]  view plain copy
  1. D3D11_TEXTURE2D_DESC cubeMapDesc;           //纹理描述  
  2. cubeMapDesc.Width = m_cubeMapWidth;         //宽和高,必须为同样,好比512.  
  3. cubeMapDesc.Height = m_cubeMapHeight;  
  4. cubeMapDesc.Format = DXGI_FORMAT_R32G32B32A32_FLOAT;        //数据格式,跟正常状况同样  
  5. //指定两个绑定阶段:Render Target 和 Shader Resource  
  6. cubeMapDesc.BindFlags = D3D11_BIND_RENDER_TARGET | D3D11_BIND_SHADER_RESOURCE;  
  7. cubeMapDesc.ArraySize = 6;                  //该纹理数组中包含6张纹理  
  8. cubeMapDesc.Usage = D3D11_USAGE_DEFAULT;  
  9. cubeMapDesc.CPUAccessFlags = 0;  
  10. cubeMapDesc.MipLevels = 0;                  //0,表示产生全部的mip链  
  11. //Misc flags: 第一个指定该纹理用于cube map,第二个指定让系统本身产生全部的mip链  
  12. cubeMapDesc.MiscFlags = D3D11_RESOURCE_MISC_TEXTURECUBE | D3D11_RESOURCE_MISC_GENERATE_MIPS;      
  13. cubeMapDesc.SampleDesc.Count = 1;           //这里不使用多重采样抗锯齿  
  14. cubeMapDesc.SampleDesc.Quality = 0;  
  15.   
  16. //使用以上描述建立纹理  
  17. ID3D11Texture2D *cubeMap(NULL);  
  18. m_d3dDevice->CreateTexture2D(&cubeMapDesc,0,&cubeMap);  

 

       纹理建立好了,如今须要针对Render Target和Shader Resource分别对其建立相应的视图:

       首先针对六个面分别建立Render Target视图:

[cpp]  view plain copy
  1. D3D11_RENDER_TARGET_VIEW_DESC rtvDesc;  
  2. rtvDesc.Format = cubeMapDesc.Format;                //格式同样  
  3. rtvDesc.ViewDimension = D3D11_RTV_DIMENSION_TEXTURE2DARRAY;     //指明这是一个纹理数组  
  4. rtvDesc.Texture2DArray.ArraySize = 1;                           //每一个视图中针对其中一张纹理  
  5. rtvDesc.Texture2DArray.MipSlice = 0;                            //每一个视图只使用最高层的mip链  
  6.   
  7. //逐个建立视图  
  8. for(UINT i=0; i<6; ++i)  
  9. {  
  10.     //每一个视图使用对应的那张纹理  
  11.     //这里指定了纹理数组中的起始索引,由于上面ArraySize指定为1,即只使用一张,  
  12.     //所以这样就单独锁定一个纹理了  
  13.     rtvDesc.Texture2DArray.FirstArraySlice = i;                   
  14.     m_d3dDevice->CreateRenderTargetView(cubeMap,&rtvDesc,&m_dynamicRTV[i]);  
  15. }  


       而后是Shader Resource视图:

[cpp]  view plain copy
  1. D3D11_SHADER_RESOURCE_VIEW_DESC srvDesc;  
  2. srvDesc.Format = cubeMapDesc.Format;                //格式同样  
  3. srvDesc.ViewDimension = D3D11_SRV_DIMENSION_TEXTURECUBE;            //指定这里将它看成cube map  
  4. srvDesc.TextureCube.MipLevels = -1;                 //-1 表示使用其全部的mip链(有多少使用多少)  
  5. srvDesc.TextureCube.MostDetailedMip = 0;            //指定最精细的mip层,0表示高层  
  6. //建立视图  
  7. m_d3dDevice->CreateShaderResourceView(cubeMap,&srvDesc,&m_dynamicSRV);  


       这样,用于绘制环境图的核心问题解决了,如今就等着往该纹理中绘制内容了。不过还有一个小问题还没有解决:绘制场景离不开深度缓冲区,而深度缓冲区与Render Target是一一对应的,所以不能使用后缓冲区对应的那个深度缓冲区了,须要咱们本身再建立一个,建立方法是彻底同样的(唯一的区别是,这里不须要模板缓冲区,所以相比正常状况下的数据格式:DXGI_FORMAT_D24_UNORM_S8_UINT,这里咱们使用DXGI_FORMAT_D32_FLOAT):

[cpp]  view plain copy
  1. D3D11_TEXTURE2D_DESC dsDesc;                    //缓冲区描述  
  2. dsDesc.Width = m_cubeMapWidth;                  //尺寸与cube map一致  
  3. dsDesc.Height = m_cubeMapHeight;  
  4. dsDesc.Format = DXGI_FORMAT_D32_FLOAT;          //这里咱们只须要深度值,不须要模板值  
  5. dsDesc.ArraySize = 1;  
  6. dsDesc.BindFlags = D3D11_BIND_DEPTH_STENCIL;  
  7. dsDesc.Usage = D3D11_USAGE_DEFAULT;  
  8. dsDesc.SampleDesc.Count = 1;                    //同cube map一致,不使用MSAA  
  9. dsDesc.SampleDesc.Quality = 0;  
  10. dsDesc.CPUAccessFlags = 0;  
  11. dsDesc.MiscFlags = 0;  
  12. dsDesc.MipLevels = 1;  
  13.   
  14. ID3D11Texture2D *depthStencilBuffer(NULL);  
  15. m_d3dDevice->CreateTexture2D(&dsDesc,0,&depthStencilBuffer);  
  16. m_d3dDevice->CreateDepthStencilView(depthStencilBuffer,0,&m_dynamicDSV);  

 

              3.2 环境图的绘制 及 使用

       如今是绘制场景的时候了,整个绘制过程分为2个pass:

       第一个pass的目的是绘制物体周边的环境,保存到纹理中。此次绘制不包含该物体自己,除此以外,一切绘制按正常流程进行便可。该阶段包含六次场景绘制,即针对六个面,使用相应的照相机、设置好相应的Render Target逐个绘制:

[cpp]  view plain copy
  1. //便于循环,使用一个共享指针,每一个面绘制时指向不一样的Render Target  
  2. ID3D11RenderTargetView *rtv[1] = {0};  
  3. //便于循环,使用一个共享指针,每一个面绘制时指向不一样的Camera  
  4. Camera *tmpCamera(NULL);  
  5. //设置好相应的viewport  
  6. m_deviceContext->RSSetViewports(1,&m_dynamicViewport);  
  7. //针对六个面,绘制场景六次  
  8. for(UINT i=0; i<6; ++i)  
  9. {  
  10.     rtv[0] = m_dynamicRTV[i];  
  11.     tmpCamera = &m_dynamicCameras[i];  
  12.       
  13.     //设置相应Render Target  
  14.     m_deviceContext->OMSetRenderTargets(1,&rtv[0],m_dynamicDSV);  
  15.       
  16.     //跟正常绘制同样,清屏操做  
  17.     m_deviceContext->ClearRenderTargetView(rtv[0],reinterpret_cast<const float*>(&Colors::Silver));  
  18.     m_deviceContext->ClearDepthStencilView(m_dynamicDSV,D3D11_CLEAR_DEPTH,1.0f,0);   
  19.       
  20.     //场景绘制过程省略  
  21.     ……  
  22. }  
  23.   
  24. //六次绘制完成后,自动产生mip链  
  25. m_deviceContext->GenerateMips(m_dynamicSRV);  


       第二个pass中,除了绘制pass 1中的全部内容外,还包括使用刚获得的环境图做为纹理资源,经过cube mapping对该物体进行绘制,从而实现物体表面的反射效果。固然,如今第帧的环境图是即时更新的,从而实现了咱们想要的动态反射效果。以下所示:

[cpp]  view plain copy
  1. //恢复正常Render Target,即后缓冲区  
  2. m_deviceContext->OMSetRenderTargets(1,&m_renderTargetView,m_depthStencilView);  
  3. //恢复正常viewport  
  4. m_deviceContext->RSSetViewports(1,&m_viewport);  
  5.   
  6. //清屏  
  7. m_deviceContext->ClearRenderTargetView(m_renderTargetView,reinterpret_cast<const float*>(&Colors::Silver));  
  8. m_deviceContext->ClearDepthStencilView(m_depthStencilView,D3D11_CLEAR_DEPTH|D3D11_CLEAR_STENCIL,1.f,0);  
  9.   
  10. //场景渲染过程省略  
  11. ……  

 

       至此,动态cube mapping效果大功告成!

      

       4. 小结

       总结一下整个绘制过程:

       1. 设置摄像机参数。针对每一个面,分别指定好相应的相机参数,包括:位置、观察点、up向量,获得相应的相机变换矩阵;

       2. 建立一个空的cube map,并分别建立其对应的Render Target视图、Shader Resource视图, 以及对应的深度缓冲区;

       3. 绘制场景:包括两个pass:

              3.1. 使用建立的cube map做为render target,并使用相应的深度缓冲区、视口变换矩阵、相机矩阵,按正常过程绘制整个场景(不包含反射物体自己);

              3.2 恢复使用后缓冲区做为Render Target,及深度缓冲区、视口变换矩阵、相机矩阵,按同上同样的方法绘制整个场景,包含使用新的环境图绘制反射物体自己。

 

       5. 示例程序

       做为本节的示例程序,为了展现动态反射的效果,场景内容在上一篇文章的基础上加入了一个绕圆球转动的箱子,以下是一张运行截图:

       固然,为了更直观地感觉动态的效果,还须要自行运行程序。

      

       此外,关于代码,还有很大的改进空间,好比场景绘制过程,其实屡次绘制场景的过程当中绝大多数的代码是彻底同样的,为了减小代码冗余,能够把它们写到一个函数中。针对绘制与不绘制反射物体自己的区别,能够经过一个布尔变量来控制。感兴趣的读者能够本身来优化一下。

 

       最后须要指出的是,动态环境图的实现成本是至关高的。毕竟,相比使用静态图,动态图的得到须要额外的六次整个场景的绘制! 这样每一帧中须要对场景进行高达七次的绘制! 把上图中的帧率与上一篇文章的帧率对比一比即明显地感觉到区别。所以,除非必要,尽可能使用静态环境图来实现反射,仅仅当有必要须要特别突出显示某物体的动态反射时再使用动态环境图。 此外,动态环境图的尺寸的选择也会大大的影响渲染成本,好比512 * 512与1024 * 1024, 每张图就有4倍的区别,这样绘制六张后,将是24倍的区别! 所以也须要尽量地减少其尺寸,本节中使用的尺寸为512 * 512.

 

       本节完。

 

       点击这里下载程序源代码、可执行文件。

相关文章
相关标签/搜索