(轉)【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.

 

       本節完。

 

       點擊這裏下載程序源代碼、可執行文件。

相關文章
相關標籤/搜索