(轉)【D3D11游戲編程】學習筆記二十一:Cube Mapping及其應用之一:天空盒的實現


(注:【D3D11游戲編程】學習筆記系列由CSDN作者BonChoix所寫,轉載請注明出處:http://blog.csdn.net/BonChoix,謝謝~)

 

       這一節討論有關紋理映射的進階內容:Cube Mapping。

       1. 簡介

       單從名字上,就大概可以看出點端倪了,翻譯成中文為立方體映射,因此肯定跟立方體有關系。確實,Cube Mapping就是使用六張正方形的圖片來進行紋理映射的。這六張圖片分別對應了一個立方體中的六個面。由於這個立方體是軸對齊的,因此每個面可以用坐標系中的六個軸方向來惟一的表示:正X面,負X面,正Y面,負Y面,正Z面,負Z面。我們把對應這六個面的六張紋理圖,稱之為“Cube Map”。 如下就是一張從立方體展開的Cube Map:

 

       2. 紋理映射方法

       通常情況下,紋理映射是通過頂點上的紋理坐標u,v來進行的。對於一個二維紋理來說,u,v就惟一確定了texel的位置。但對於一張Cube Map,僅僅給出u,v坐標是無法確定像素位置的,因為它有六張紋理圖。因此,這時我們需要一種新的映射技術,以一對一地確定紋理中對應的texel。

       實際上,在Cube Mapping中,映射是通過一個三維的向量實現的。該三維向量可以設想為以立方體的中心為起點,指向立方體外面。該向量與立方體的交點處對應的texel就是映射結果對應的texel。顯然,一個三維向量與立方體有且僅有一個交點,因此這種方法可以用來實現立方體映射。

       以下是在二維平面上進行的圖示:v代表映射使用的向量,正方形代表cube map,圖中給出的交點處對應的texel即我們要的結果。

       下面我們從數學的角度來導出映射過程:

       第一步,給出一個3D向量[x,y,z],首先找出值最大的那一維。以[-3.2, 5.1, -8.4]為例子, 這時最大維為Z,最大維用來把cube map從六張圖定位到一張圖上。-8.4為負數,對應了立方體的負Z面,因此接下來我們關注的焦點將是立方體負Z面對應的那張紋理;

       第二步,把向量中另外兩維分別除以最大維,得到一個二維向量,(3.2/8.4, -5.1/8.4)。很容易知道,這個二維向量中的數值范圍位於[-1, 1]之間。

       第三步,把上階段中得到的二維向量轉換到[0, 1]之間。很簡單,把位於[-1, 1]之間的數轉換到[0, 1]之間的方法為: (x + 1) / 2。對於上面的例子, 為3.2/8.4 * 2 + 0.5 = 0.31, -5.1 / 8.4 * 2 + 0.5 = 0.51。因此得到二維向量(0.31, 0.51)。這個二維向量就是我們用來在負Z軸上獲得texel的紋理坐標。

      

       小結一下,Cube Mapping中通過3D向量進行紋理映射的過程分為三步:

       1. 根據最大維惟一地確定一張紋理圖

       2. 使用其他兩維作為二維向量,並分別除以最大維

       3. 把二維向量中的值轉換到[0, 1]之間,得到二維紋理坐標,以通過常規方法對二維紋理進行映射。

 

       3. D3D11中Cube Map的使用

       在HLSL中,用專門用來表示cube map的類型,即TextureCube,紋理采樣方法與常規二維紋理完全一樣。 如下所示:

[cpp]  view plain copy
  1. TextureCube g_cubeMap;  
  2.   
  3. SamplerState samTexture  
  4. {  
  5.     Filter = MIN_MAG_MIP_LINEAR;  
  6.     AddressU = Wrap;  
  7.     AddressV = Wrap;  
  8. };  

       給定一個3維向量,使用cube map來獲取紋理值的方法如下:

[cpp]  view plain copy
  1. g_cubeMap.Sample(samTexture,dir);  

       看得出來,與常規二維紋理基本一樣,區別僅僅在於第二個參數從二維紋理坐標變成了三維。

       在C++程序中,用來存放cube map的類型依然是ID3D11Texture2D。

       在之前的所有程序中,我們用它來存放單張二維紋理圖。實際上,這個接口類型的功能十分強大,除了可用來存放單張紋理外,還可以存放紋理數組。紋理數組,即多張紋理圖片。不僅僅是紋理數組,每張紋理的帶個mip鏈都可以存放在其中。對我們來說,只需要通過ID3D11Texture2D來表示。如下圖示清楚地表示了使用該類型表示多張紋理、及其mip鏈時,各個紋理圖的分布:

從左到右分別表示各個不同的紋理,垂直方向上分別對應各個紋理的mip鏈。

有關ID3D11Texture2D的詳細內容,在后面會有專門文章進入講解,這里暫時不關注這些細節。而是把注意力集中在cube map的表示上。

 

       此外,使用d3d自帶的.dds格式的圖片,可以集中存放cube map中的六張紋理圖。讀取圖片獲得紋理視圖的方法與之前的二維紋理完全一樣:

[cpp]  view plain copy
  1. D3DX11CreateShaderResourceViewFromFile(m_d3dDevice,L"textures/snowcube1024.dds",0,0,&m_SkyBoxSRV,0)  

       同樣,通過這個視圖:m_SkyBoxSRV,可以直接用來給HLSL中的Texture2D賦值。

 

       4. 使用Cube Mapping實現天空盒效果(SkyBox)

       為了給人以身臨其境的感覺,游戲中所有的場景都會使用天空盒技術,用以模擬無窮遠處的天空、山巒等景象。天空盒的實現有以下幾個關鍵點:

       1. 天空理論上應該位於無限遠處,場景中任何物品都位於天空盒前部,而不會被其遮住;

       2. 當在場景中移動時,場景中的物體會相對角色移動,但天空盒與自身相對位置保持靜止不變;

       針對1, 可以通過讓天空盒的變換后的z坐標位於可視范圍內的最大值。這樣場景中任何物品都能被正確渲染,並能遮擋遠處的天空。因而符合實際情況;

       針對2, 為了實現讓天空盒相對自己靜止的效果,有兩種方法:一是針對任一時刻自己的世界坐標,讓天空盒具有相同的平移,這樣自己將永遠位於天空盒中心,從而相對靜止;另一種方法是在渲染天空盒時,不對天空盒進行世界變換,並且讓其視角矩陣中平移部分變為[0, 0, 0],這樣天空盒可以保持在原點,由於視角空間中相機永遠位於原點,因此自己相對天空盒位置總是保持相對靜止。

      

       下面來介紹一下天空盒實現的具體步驟:

       1. 天空盒的幾何體表示

       在現實生活中,天空給我們的感覺是一個半球形。因此這里我們使用一個圓球,作為天空盒的幾何體表示;

       2. 天空盒紋理圖

       為了表示無限遠處的天空、山巒等現象,我們需要在該圓球表面貼上包含空間任一角度的紋理圖。

       Cube map由於包含六張表示立方體各個面的貼圖,而整個貼圖把立方體內部整個空間完整地包圍起來,因此可以用它來模擬周圍的環境。這處情況下的應用,我們稱之為環境映射(Environment Mapping)。這個想法其實很容易理解,考慮以下情形:把攝像機調整為90度視角,寬、高比設為1,放於某一點處。然后向周邊上、下、左、右、前、后六個方向上分別拍照。這種情況下攝像機所能拍攝到的部分將囊括整個場景,而這六張拍攝的圖片所組成的Cube Map即可作為表示相機位置處的周邊場景的環境圖(Environment Map)。

       3. 天空紋理映射

       有了天空盒的幾何表示以及環境圖,現在的問題就是如何映射該環境圖了。在上面介紹Cube Mapping時我們說過,Cube Mapping使用3D向量來實現紋理映射。這里Environment Mapping作為一種特殊的Cube Mapping,當然也可以使用這種映射方法。

       請看如下圖示,該圖很好地解釋了天空盒紋理映射的方法:

       由圖可知,對於圓球上的任一點,可以使用從球中心出發、經過該點的3D向量來對環境圖進行映射,這也符合我們現實當中觀察物體的情形。

   

       4. 程序實現

       天空盒的實現,核心在於shader部分。在C++程序中,我們要做的與之前的程序完全一樣:創建圓球及其對應的頂點、索引緩沖區,創建天空紋理視圖等等。因此這里只給出了Shader部分的關鍵點的實現:

       首先是頂點着色器中的輸入頂點結構,由於我們這里實現天空盒時不進行光照計算,而只進行紋理映射,因此頂點結構只包含頂點位置。沒有紋理坐標,因為這里通過自己計算得到的3D向量來進行映射。如下:

[cpp]  view plain copy
  1. struct VertexIn  
  2. {  
  3.     float3 posL: POSITION;  
  4. };  

       對於輸出頂點,投影空間的位置永遠是必須的。此外,為了在像素着色器中計算3D向量以用於紋理映射,我們還需要保留模型空間的頂點坐標。

[cpp]  view plain copy
  1. struct VertexOut  
  2. {  
  3.     float4 posH: SV_POSITION;  
  4.     float3 posL: POSITION;  
  5. };  

       現在是最關鍵的地方:在前面提到過,天空盒的一個關鍵點是它總是位於無窮遠,即場景中任何物體的后面。換句話說,它永遠位於可視距離的最遠處。由於不同場景的大小是不固定的,因此通過給圓球指定特定的半徑不是一種好辦法。因為這種情況下我們在使用天空盒時總是需要指定其合適的半徑。這里,我們使用了一種更為巧妙的方法,位於頂點着色器中,代碼如下:

[cpp]  view plain copy
  1. VertexOut VS(VertexIn vin)  
  2. {  
  3.     VertexOut vout;  
  4.     vout.posL = vin.posL;  
  5.     vout.posH = mul(float4(vin.posL,1.f), g_worldViewProj).xyww;  
  6.   
  7.     return vout;  
  8. }  

       關鍵是第三行對頂點進行的變換。一般情況下,我們需要的是對頂點進行“世界、視角、投影變換”后的坐標,即xyzw成分。但這里,我們並沒有接受z分量,而是使用了w分量作為投影變換后的z分量。乍看下很是奇怪,但這種方法確實很實用。我們知道,對於[x, y, z, w]形表示的3D坐標,其對應到實際3D空間中表示為[x/w, y/w, z/w]。因此,這里把z分量變為w后,該點真實坐標為[x/w, y/w, 1],即x,y坐標保持不變,z分量始終為1. 還要知道,由於現在頂點投影空間,可視范圍(z值)被變換到[0, 1]之間,1即表示最大可視距離。因此,天空盒幾何體上的任一點,不論其世界空間中距離攝像機的遠近,在經過投影變換后,其始終保證位於“無窮遠處”(最大可視距離處),從而達到了我們的要求。

       在像素着色器中,我們要做的僅僅是進行紋理映射獲得天空任一點對應的顏色值:

[cpp]  view plain copy
  1. float4 PS(VertexOut pin): SV_TARGET  
  2. {  
  3.     return g_cubeMap.Sample(samTexture,pin.posL);  
  4. }  

       這里,我們直接使用圓球世界空間下的頂點作為3D向量,因為向量的起始點位於原點。

       最后還有2點要注意:

       1. 由於在清屏時我們把默認深度值設為1.0, 而此時天空盒上所有頂點的深度值全為1,此時需要相應地設置渲染狀態,令深度測試函數為LESS_EQUAL,以保證天空盒全部通過深度測試。

       2. 由於我們現在是位於圓球內部,此時觀察到的圓球內表面的三角形頂點順序為逆時針,因此還需要將渲染狀態設置為:逆時針為正面。

       相應的設置如下:

[cpp]  view plain copy
  1. RasterizerState CounterClockFrontRS  
  2. {  
  3.     FrontCounterClockwise = true;  
  4. };  
  5.   
  6. DepthStencilState LessEqualDSS  
  7. {  
  8.     DepthFunc = LESS_EQUAL;  
  9. };  

       並在technique中開啟這些設置:

[html]  view plain copy
  1. technique11 SkyBoxTech  
  2. {  
  3.     pass P0  
  4.     {  
  5.         SetVertexShader( CompileShader(vs_5_0, VS()) );  
  6.         SetPixelShader( CompileShader(ps_5_0, PS()) );  
  7.         SetDepthStencilState(LessEqualDSS, 0);  
  8.         SetRasterizerState(CounterClockFrontRS);  
  9.     }  
  10. }  

 

       5. 本節示例程序

       好啦,整個天空盒的實現細節就這些,這里提供一個非常簡單的示例程序,以演示天空盒的效果。為了簡單明了地突出天空盒的實現,場景中除了天空之外沒有任何其他物體。以下是一張截圖:

       點擊這里獲取源代碼

 
 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM