標籤:
本系列文章主要翻譯和參考自《Real-Time 3D Rendering with DirectX and HLSL》一書(感謝原書作者),同時會加上一點個人理解和拓展,文章中如有錯誤,歡迎指正。
這裡是書中的代碼和資源。
紋理映射主要是指將貼圖添加到3D模型表面的過程。這有點類似禮物封裝,而你的封裝紙就是一張2D的貼圖。紋理映射是現代即時渲染技術的基礎,並且可以用來實現很多很有意思的圖形效果。
很多書或者文章中可能會把texture翻譯成材質,在這裡,我將保持unity Game Engine中的習慣,將texture統一翻譯為紋理,material為材質。
本文索引:
- 一 關於紋理映射的介紹
- 二 一個實現紋理映射的effect
- 1 Commentthe Preprocessor and Annotations注釋前置處理器以及註解
- 2 Texture Objects and Samplers紋理對象和採樣器
- 3 Texture Coordinates紋理座標
- 4 效果查看
- 三 紋理過濾
- 1 Magnification 放大化
- 2 Minification 縮小化
- 3 Mipmaps
- 4 SamplerState Filtering Options
- 四 Texture Addressing Modes紋理定址模式
- 1 Wrap 迴圈模式
- 2 Mirror 鏡像模式
- 3 Clamp 展開模式
- 4 Border邊框模式
- 五 擴充
- 1 更換紋理過濾方式查看效果
- 2 更換定址方式查看效果
- 六 總結
一、 關於紋理映射的介紹
一般情況下,你應該會希望自己的模型擁有更多的細節,而不是像上一篇文章中的HelloShader和HelloStructs中輸出的效果那樣只有一個純色。在前面的幾篇文章中已經講到過3D模型是由頂點組成的,而頂點間又會組成三角面片。這些頂點在最簡單的情況下至少會描述這個點的座標位置。當然也可能會描述一些其他資訊,比如顏色,法線之類的。就是一個頂點上帶有了位置和顏色資訊的立方體。
左邊的立方體中,每個頂點帶有不同的顏色;而右邊的立方體所有頂點都帶有同樣的顏色。顯然,左邊的立方體看起來細節要豐富的多。顏色使得立方體看起來更有趣並且也更易分辨出立方體的各個面。並且左邊的立方體中每個像素的顏色與頂點位置相關。你的3D模型頂點數越多其表面的顏色細節就越豐富。然而,當攝像機視角拉近時,你將發現無論多少個頂點都無法達到高精度的以定義頂點顏色進行的渲染,同時,想要渲染出不同的顏色效果必然會考慮到頂點。由此,提出了紋理映射這個概念。
為了將紋理映射到三角面片上,你需要為每個頂點定義一個二維的紋理座標。使用這個座標你就可以找到紋理上座標組應位置的顏色值。這個過程將會在像素著色器中執行,在該著色器中會為每個三角面片上的像素尋找其在紋理對應座標位置上的顏色值。由此,就可以用三角面片頂點對應紋理座標的方式取代單純為頂點定義顏色這種渲染方式。
DirectX紋理座標範圍為[0,1],並且水平和垂直方向上都是這個範圍,而原點則是圖片的左上方。這兩個軸一般被稱為u(水平方向上)和v(垂直方向上)。展示了由兩個三角形組成的四邊形,並且高亮顯示了其UV座標。
| 注意 |
| Direct3D支援一維、二維以及三維的紋理,並以紋理列和立方體紋理的形式提供。紋理座標的維度取決於紋理的維度。 |
二、 一個實現紋理映射的effect(1) Comment,the Preprocessor and Annotations:注釋、前置處理器以及註解
下面的程式碼片段是一個實現了紋理映射的effect代碼。首先需要在FX Composer下建立一個新的effect,然後將這些代碼添加進去。
程式碼片段Listing5.1 TextureMapping.fx
#define FLIP_TEXTURE_Y 1cbuffer CBufferPerObject{ float4x4 WorldViewProjection : WORLDVIEWPROJECTION<string UIWidget="None";>;}RasterizerState DisableCulling{ CullMode = NONE; };Texture2D ColorTexture< string ResourceName = "default_color.dds"; string UIName = "Color Texture"; string ResourceType = "2D";>;SamplerState ColorSampler{ Filter = MIN_MAG_MIP_LINEAR; AddressU = WRAP; AddressU = WRAP;};/****************** Date Structures ********************/struct VS_INPUT{ float4 ObjectPosition : POSITION; float2 TextureCoordinate : TEXCOORD;};struct VS_OUTPUT{ float4 Position : SV_POSITION; float2 TextureCoordinate : TEXCOORD;};/****************** Utility Functions ********************/float2 get_corrected_texture_coordinate(float2 textureCoordinate){ #if FLIP_TEXTURE_Y return float2(textureCoordinate.x, 1.0 - textureCoordinate.y); #else return textureCoordinate; #endif}/****************** Vertex Shader ********************/VS_OUTPUT vertex_shader(VS_INPUT IN){ VS_OUTPUT OUT = (VS_OUTPUT)0; OUT.Position = mul(IN.ObjectPosition, WorldViewProjection); OUT.TextureCoordinate = get_corrected_texture_coordinate(IN.TextureCoordinate); return OUT;}/****************** Pixel Shader ********************/float4 pixel_shader(VS_OUTPUT IN) : SV_Target{ return ColorTexture.Sample(ColorSampler, IN.TextureCoordinate); }/****************** Techniques ********************/technique10 main10{ pass p0 { SetVertexShader(CompileShader(vs_4_0, vertex_shader())); SetGeometryShader(NULL); SetPixelShader(CompileShader(ps_4_0, pixel_shader())); SetRasterizerState(DisableCulling); }}
在這個effect中可以看到多種語言的文法。
首先是shader中的注釋。HLSL支援C語言風格的單行注釋(//)也支援多行注釋(/*注釋內容*/)。
其次,在effect第一行中的宏命令#define FLIP_TEXTURE_Y 1,和C/C++中的使用方式是完全一樣的。而HLSL中也確實包含很多預置的命令,包括#if,#else,#endif 以及 #include。
最後,我們再來看下在cbuffer中聲明的WorldViewProjection常量。這個常量和在上一篇文章中聲明的類似,但在其結尾增加了一個註解聲明。註解是CPU端應用的注釋,被括在一對角括弧當中。這個注釋不會影響shader的執行,但是CPU中的應用會使用到。例如,關聯到WorldViewProjection的UIWidget註解,將會控制FX Composer將如何處理這個shader常量。當被賦予None值時,NVIDIA FX Composer的屬性面板中會將這個值從可見材質屬性列表中移除。圖5.3中顯示了NVIDIA FX Composer的屬性面板在兩種情況下的顯示結果。注意,即使這個值在屬性面板中被隱藏了,他仍然會在CPU中更新。將這個值隱藏可以避免你對其做不必要的手動修改。
(2) Texture Objects and Samplers:紋理對象和採樣器
在HLSL effect中要使用紋理涉及到三個步驟。
首先,你必須聲明紋理對象(如下面的程式碼片段中所示)。一個HLSL紋理的聲明中可以指定具體的子類型(例如:Texture2D)或者其他更通用的紋理資料類型。紋理對象的聲明不能包含在cbuffer中。
程式碼片段Listing 5.2 TextureMapping.fx檔案中紋理對象的聲明
Texture2D ColorTexture< string ResourceName = "default_color.dds"; string UIName = "Color Texture"; string ResourceType = "2D";>;
| 注意 |
| 在上面的程式碼片段中有三個註解被關聯到ColorTexture的聲明中。所有的這些聲明都是可選的,但在NVIDIA FX Composer這個編輯器環境下,這些註解會提升材質的使用效率。 |
| UIName註解使你可以自訂顯示在屬性面板中紋理的名字。ResourceType定義了該紋理可以被賦值為 什麼類型,而ResourceName允許當使用者沒有設定紋理時將會使用一個預設的紋理。 |
接下來,需要定義和初始化一個紋理採樣器(參考程式碼片段Listing5.3所示)。採樣器控制如何從紋理中擷取顏色值。Direct3D 10中引入了SamplerState資料類型,他直接映射了Direct3D中的C結構體,其成員變數包括filtering和texture address modes。
Listing5.3 TextureMapping.fx中的採樣器
SamplerState ColorSampler{ Filter = MIN_MAG_MIP_LINEAR; AddressU = WRAP; AddressU = WRAP;};
最後,使用聲明的採樣器對象對紋理進行採樣。這個步驟將在像素著色器中執行。(詳見程式碼片段Listing5.4)
程式碼片段Listing 5.4 TextureMapping.fx檔案中的像素著色器
float4 pixel_shader(VS_OUTPUT IN) : SV_Target{ return ColorTexture.Sample(ColorSampler, IN.TextureCoordinate); }
Sample函數的調用類似C++的對象成員函數引用。該函數的第一個參數是採樣器對象,第二個參數是用來在紋理中尋找顏色的紋理座標。
(3) Texture Coordinates:紋理座標
頂點流(vertex stream)中會提供在紋理上採樣的座標,而在shader中依靠VS_INPUT和VS_OUTPUT成員結構體進行頂點流資料轉送。以下代碼中注意觀察新增加的變數TextureCoordinate及與其相對應的語義字串TEXCOORD。
程式碼片段Listing5.5 TextrueMapping.fx檔案中的頂點著色器輸入輸出結構體定義
struct VS_INPUT{ float4 ObjectPosition : POSITION; float2 TextureCoordinate : TEXCOORD;};struct VS_OUTPUT{ float4 Position : SV_POSITION; float2 TextureCoordinate : TEXCOORD;};
頂點著色器在調用了get_corrected_texture_coordinate()函數後將經過處理的紋理座標傳輸到下一個渲染階段。HLSL支援使用者自訂的、C語言風格的輔助函數,而這裡的這個協助函數主要在FLIP_TEXTURE_Y為非零值時將紋理座標在垂直方向上進行翻轉。這個步驟在NVIDIA FX Composer中是很必要的,因為這個軟體使用OPENGL風格的紋理座標來構建預置的這些3D模型(軟體中提供的Sphere,Teapot,Torus以及平面)。OpenGL中紋理座標的原點位於紋理的左下角,而DirectX中紋理座標的原點位於紋理的左上方。因此,當你使用NVIDIA FX Composer軟體中預置的幾個模型時,你需要反轉垂直方向的紋理座標。如果你向
NVIDIA FX Composer中匯入一個自訂的DirectX風格紋理座標的模型,就可以不進行紋理的反轉操作。
程式碼片段Listing5.6 TextureMapping.fx檔案中頂點著色器的內容
float2 get_corrected_texture_coordinate(float2 textureCoordinate){ #if FLIP_TEXTURE_Y return float2(textureCoordinate.x, 1.0 - textureCoordinate.y); #else return textureCoordinate; #endif}VS_OUTPUT vertex_shader(VS_INPUT IN){ VS_OUTPUT OUT = (VS_OUTPUT)0; OUT.Position = mul(IN.ObjectPosition, WorldViewProjection); OUT.TextureCoordinate = get_corrected_texture_coordinate(IN.TextureCoordinate); return OUT;}
(4) 效果查看
展示了這個effect的渲染效果
預設情況下不會顯示這個貼圖,需要在material的屬性中對紋理貼圖進行設定。
三、 紋理過濾
在我們講解紋理座標和採樣的時候,你可能沒有注意觀察過紋理映射模型和通過一一對應將紋理上的元素渲染到螢幕上的區別。你的攝像機可能會以任意角度和距離來觀察貼上了紋理的模型。試想一下,當你的攝像機逐漸靠近這個模型,光柵階段將會決定模型的哪些像素會被傳送到像素著色器中進行紋理座標的插值。如果你的攝像機拍攝到的部分剛好位於沒有紋理定義的像素上,也就是你正在試圖渲染一個比原紋理解析度更高的紋理貼圖那應該如何處理?這部分文章的內容texture filtering紋理過濾決定了將賦予這些像素怎樣的顏色值,這是因為這部分像素在本來的紋理貼圖中已經不再能夠找到對應的像素顏色了。
(1) Magnification 放大化
在需要將原模型放大查看的情況下,需要渲染的像素遠比紋理已經提供的像素要多。Direct3D支援三種類型的過濾方式來決定這些中間像素的顏色:point filtering,linear interpolation,anisotropic filtering。
-
Point Filtering 點過濾
點過濾是所有過濾方式當中最快的一種,但是渲染的品質是最低的。也被稱為近鄰取樣過濾(nearest-neighbor filtering),點過濾方式只是簡單的使用最鄰近像素中心的顏色來為當前像素賦值。下面左圖所顯示的就是點過濾方式產生的渲染結果,你可以看到像素以一塊一塊的方式渲染出來。
-
Linear Interpolation 線性過濾
線性過濾能夠提供更好的渲染效果,當前像素點的顏色通過使用周圍(包括垂直和水平方向上)帶有紋理像素的點的顏色值進行插值計算得到。先用u方向上兩個臨近的像素進行插值再用v方向上兩個臨近的值進行插值從而計算出最終結果。圖5.5右邊顯示的是經過線性插值計算出的結果,注意該圖中顯示效果要顯得更平滑一些。
-
Anisotropic Filtering 各向異性過濾
各向異性用來處理當模型表面以十分傾斜的角度朝向攝像機而造成的紋理變形。各向異性過濾減少了變形並提升了渲染效果。這是使用代價最高的過濾方式,但所產生的效果是非常好的。展示了兩種過濾方式的渲染效果區別。
(2) Minification 縮小化
縮小化是放大化的反面,用來處理當一個像素需要從原紋理的多個像素點中選取顏色的情況。這種情況通常發生在當攝像機離模型或者說紋理表面很遠的情況下,這時Direct3D必須為這個像素選擇一個合適的顏色值。縮小化與放大化有著同樣的過濾方案選擇,但相對來說,縮小化要更為複雜。比如使用線性插值方式的情況下,當前像素點的計算方式仍然是使用離他最近的周圍四個紋理像素點來進行插值計算,但如果有更多的紋理像素點都與當前像素點關聯,則都會被忽略,這種情況下的計算結果降低了渲染品質。
你可能已經想到可以用所有這些與當前像素點相關聯的紋理像素點取平均值來計算,這樣就可以採樣任意規模的縮小化紋理值。但其實這種想法是不切實際的。然後,我們可以採用另一種替代方式,通過與計算技術mipmapping來實現。
(3) Mipmaps
mipmaps是原紋理不同版本的縮小版,一般會預先計算好並儲存在同一個檔案中。每個mip-level依次逐步除以2,直到變成1*1像素的紋理。展示了一張512*512像素的地球貼圖拆分成9個mip-level後的結果。
當應用mipmap技術時,至少需要兩個步驟來擷取最終顏色。首先,需要為放大化或者縮小化選擇mip-level。其次,通過選定的mip-level擷取對應的顏色值。
點過濾或者線性過濾方式可以用來協助選擇mip-level。點過濾僅僅會選擇最接近的mip-level,而線性過濾則會選擇臨近的兩個mip-level。然後已選擇的mip-level會通過點、線性、各向異性過濾法進行採樣。如果使用的是線性過濾法則會對兩個mip-level進行採樣並將兩個結果進行插值計算。這種技術將會獲得最高的渲染效果。
最後,需要注意的是mipmap技術雖然提升了渲染品質,但也同時增加了近33%的記憶體佔用率。
(4) SamplerState Filtering Options
讓我們回顧一下TextureMapping.fx代碼中的SamplerState對象(可參見下面的程式碼片段),你可以看到這個對象中的Filter成員變數被賦予了MIN_MAG_MIP_LINEAR這個值。這個字串設定採樣器使用線性插值作為放大、縮小和mip-level的採樣方式。Direct3D也允許為這些過程設定獨立的值。Table5.1中提供這些變換的樣本。你也可以在MSDN上的Direct3D文章中找到完整的列表。
Listing5.7 TextureMapping.fx中聲明的SamplerState對象
SamplerState ColorSampler{ Filter = MIN_MAG_MIP_LINEAR; AddressU = WRAP; AddressU = WRAP;};
四、 Texture Addressing Modes:紋理定址模式
你可能已經注意到在對SamplerState對象賦值的時候,還為下面這兩個成員變數進行了賦值:AddressU = WARP 以及 AddressV = WRAP。這被稱為定址模式,用來控制當紋理座標超出了[0, 1]範圍時應該如何渲染這些像素點。Direct3D的定址模式包括Wrap,Mirror,Clamp以及Border。
(1) Wrap 迴圈模式
在迴圈紋理定址模式下,你的紋理會在低於0或高於1的地方不斷進行重複。例如,為紋理座標為(0.0, 0.0),(3.0, 0.0),(0.0, 3.0),(3.0, 3.0)的四邊形進行紋理映射,結果顯示貼圖在每個軸向上重複了三次。如所示:
| 注意 |
| 每個重複的貼圖周圍的黑色邊框不是在warping處理的過程中產生的。是為了分辨整數重複次數的邊界。 |
(2) Mirror 鏡像模式
鏡像定址模式類似於迴圈模式,只是鏡像模式會以紋理所處的整形邊界為鏡像而不是簡單的重複。顯示了同樣的紋理座標下以鏡像模式渲染的四邊形。
(3) Clamp 展開模式
在展開模式下,紋理不會再被簡單的重複平鋪開來。整個紋理只會被使用一次,所有超出[0, 1]範圍的像素都會以展開的形式從前一像素擷取值。是使用展開模式渲染的模型,這個效果似的超出部分的貼圖看起來好像被拖拉過。
| 注意 |
| 這裡使用的紋理和之前兩種模式使用的紋理不一樣。這張紋理去掉了之前紋理的黑色邊框,並進行了一些裁剪。如果繼續使用之前那張紋理,那麼超出部分的像素應該和邊框一樣都是黑色的。 |
(4) Border邊框模式
和展開模式類似,邊框定址模式也只有使用一次紋理。但是在紋理的邊緣部分並不會進行拖拉,邊框顏色將會應用到超出紋理部分的像素中。如所示,這裡的貼圖也是使用最初我們使用的帶黑色邊框的那張,在邊框模式下超出部分被渲染成了藍色的邊框色。
五、 擴充(1) 更換紋理過濾方式查看效果
參考文章之前提供的表格,將紋理過濾方式做不同的修改,可以看到如下結果:
點過濾方式(MIN_MAG_MIP_POINT)與線性過濾方式(MIN_MAG_MAP_LINEAR)在將模式放到足夠大之後看到的效果:
對於模型表面傾斜角度大時各向異性過濾方式(ANISOTROPIC)與線性過濾方式(MIN_MAG_MAP_LINEAR)的渲染效果對比:
(2) 更換定址方式查看效果
-
warp模式下的貼圖顯示:
-
mirror模式下的貼圖顯示:
-
clamp模式下的貼圖顯示:
-
border模式下的貼圖顯示:
六、 總結
在本文中,具體講解了紋理映射的各個細節。瞭解了一個複雜紋理的映射效果並學習了更多的HLSL文法。也瞭解到了關於放大化、縮小化、mipmaping以及用於進行紋理採樣的過濾處理過程。文章的最後,我們講解了對於那些超過[0, 1]紋理座標範圍的像素點將如何進行處理。
紋理映射是學習現代3D渲染技術的基礎,之後的文章內容將會基於前面的這幾篇文章。
【DirectX11】第六篇 紋理映射