原帖地址:http://ogldev.atspace.co.uk/www/tutorial16/tutorial16.html
紋理映射意思就是把圖片(或者說紋理)映射到3D模型的一個或多個面上。紋理可以是任何圖片,使用紋理映射可以增加3D物體的真實感,我們常見的紋理有磚,植物葉子等等。
下圖中是使用紋理映射和沒有使用紋理映射四面體的比較。
要使用紋理映射,我們必須做以下三件事情:在OpenGL中裝入紋理,為頂點提供紋理坐標(為了把紋理映射到頂點),用紋理坐標在紋理上執行一個采樣操作,得到一個像素顏色。
三維空間中的物體經過縮放,旋轉,平移,最終投影到屏幕上,依賴於攝像機位置和方位的不同,最終呈現的形式可能千差萬別,但根據紋理坐標,GPU會保證最終的紋理映射結果是正確的。在光柵化階段,GPU也會插值紋理坐標,這樣,每個片元都有一個對應紋理坐標。在片元shader中,片元(或像素)會根據紋理坐標,采樣得到最終的紋理單元顏色,並把這些顏色和當點片元的顏色或者根據光照計算的顏色混合,從而輸出像素的最終顏色。下面的教程中,我們將看到,紋理單元能夠包含不同的數據,實現很多特效。
OpenGL支持 1D, 2D, 3D, cube等等多種紋理,這些紋理在不同的技術中使用。我們首先來學習2D紋理,2D紋理通常來說就是一塊有高度和寬度的surface(表面),寬度乘以高度的結果就是紋理單元的數目。那么如何指定頂點的紋理坐標呢?其實頂點的紋理坐標並不是頂點在紋理surface上的坐標,否則的話,那受限制就太大了,因為我們的三維物體表面是變化的,有的大,有的小,這樣的話,意味着我們要不斷更新紋理坐標,這顯然很難做到。因此,存在紋理坐標空間,每維的紋理坐標范圍都是[0,1],所以紋理坐標通常都是[0,1]之間的一個浮點數,我們用紋理坐標乘以紋理的高度或寬度,就可以得到頂點在紋理上對應的紋理單元位置,例如:如果紋理位置是 [0.5,0.1],紋理寬度是320,紋理高度是200,那個對應紋理單元位置就是 (160,20) (0.5 * 320 = 160 和 0.1 * 200 = 20)。
通常紋理空間又叫UV空間,U對應2維笛卡爾坐標的x軸,V對應y軸,OpenGL中,U軸方向從左到右,V軸方向從下到上,如下圖所示,可以看到(0,0)位置在左下角,向上V增加,向右U增加:
下圖中的三角形被指定紋理坐標:
當三角形做了各種變化后,它的紋理坐標保持不變,假設三角形光柵化前,它的位置如下。
紋理坐標是三角形頂點的屬性,無論三角形怎么變化,對於頂點來說,紋理坐標相對位置都不變,當然也可以在頂點shader中,動態改變紋理坐標,這個主要用於實現一些特殊的效果,比如水面效果等等。在本教程中,我們將保持紋理坐標不變。
另一個和紋理映射相關的概念是“濾波”,前面我們討論了通過一個紋理坐標,得到相應的紋理單元,由於紋理坐標是[0,1]之間的浮點數,它乘以紋理高度寬度,可能得到一個浮點的映射坐標,比如我們把紋理坐標映射到紋理單元 (152.34,745.14),此時怎么得到紋理單元呢?最簡單的方法,我們可以四舍五入,得到 (152,745),這種方法,可以工作,但是在一些情況下,效果並不是很好,一個更佳的方案是:得到一個2×2的quad紋理單元, ( (152,745), (153,745), (152,744) 和(153,744) ),然后在這些紋理單元顏色之間進行線性插值操作,線性插值和該紋理單元到(152.34,745.14)的距離有關,越接近這個坐標,影響就越大,越遠,影響越小,這個效果要比四射五入直接選取紋理單元要好。
決定最終哪一個紋理單元被選擇的方法就稱作“濾波”,最簡單的方法就是前面說的四舍五入方法,這種濾波方式又叫nearst濾波 (nearest filtering),這是一種點采樣的濾波方式,前面說的基於線性插值的濾波稱作線性濾波 。OpenGL提供多種采樣方式,你可以選擇其中任意一種,通常,更好的濾波效果需要更高的GPU運算能力,這有可能影響幀率。選擇更好的效果和更流暢的畫面是個balance問題。
下面我們看看OpenGL中如何實現紋理映射:在OpenGL中使用紋理,我們首先要學習四個概念:紋理對象,紋理單元,采樣對象以及shader中的采樣uniform變量。
紋理對象本身包括紋理需要的數據,比如圖像數據。根據存儲的數據格式(RGB,RGBA等等),紋理可分為1維紋理,2維紋理,3維紋理等等,OpenGL提供了一種方便的函數,只要指定數據的起始地址,以及數據格式屬性,就可以很方便的把數據裝入GPU,紋理通常就是通過這種方法裝入video memory,在裝入紋理時候,可以指定多個參數,比如濾波方式等等。類似頂點緩沖數據,我們也可以把紋理和句柄關聯起來。當創建句柄,裝入紋理后,我們能夠實時切換紋理,和不同的OpenGL句柄進行綁定,而不需要再次裝入數據,此時,OpenGL的driver會保證,渲染前,紋理數據被裝入video memory。
紋理對象並不直接和shader(紋理采樣實際上在shader中實施)打交道,而是通過一個紋理單元(texture unit),該紋理單元的索引被傳遞到shader中。這樣,shader就能通過該紋理單元訪問紋理對象。通常我們可以使用多個紋理單元(數目和具體gpu有關), 為了把一個紋理對象A綁定到紋理單元0,我們首先要激活紋理單元0,然后才能綁定到紋理對象A,此時,如果要使用第二個紋理對象,可以激活紋理單元1,然后綁定到相應的紋理對象。
實際的情況可能有點復雜,一個紋理單元其實可以同時綁定多個紋理對象,只要這些紋理對象的類型不同,這類型是紋理對象的target,比如1D,2D等等,綁定紋理對象和紋理單元時,我們必須指定target,例如:我們可以把targe維1D紋理對象A和target維2D的紋理對象B同時綁定到同一個紋理單元。
采樣操作通常在片元shader中實施,具體操作是通過一個采樣函數,采樣函數需要知道采樣的紋理單元,因為shader中可能有多個紋理單元。具體是通過一組紋理uniform變量來區分不同的紋理單元,這些uniform變量和紋理單元是一一對應關系,當你對某個uniform變量進行采樣操作時,該變量對應的紋理對象被使用。
最后再來看一下采樣對象,注意不要把它和采樣uniform變量混淆。紋理對象包含圖像數據,也包括配置采樣操作的參數等等,這些參數是采樣狀態的一部分,我們也可以創建一個采樣對象,配置它的參數,並把它綁定到紋理單元,這將會重載紋理對象中定義的采樣狀態,本文中,我們並沒有實施采樣對象。
下圖總結了我們前面學習的一些概念:
主要代碼:
OpenGL能夠裝入內存中的紋理數據,但並沒有提供一個方法,把圖像文件,比如PNG,JPG等,裝入到內存中,我們使用一個開源的圖像處理庫 ImageMagick, 該庫支持多種格式的圖像處理。在程序代碼中,直接包含了該庫的源代碼。
大部分的紋理操作被包裝在texture類中:
texture.h
class Texture
{
public:
Texture(GLenum TextureTarget, const std::string& FileName);
bool Load();
void Bind(GLenum TextureUnit);
};
創建一個紋理對象時候,我們需要指定target(我們用GL_TEXTURE_2D),以及圖像文件名字,之后,我們可以調用Load函數,來裝入紋理數據。如果需要把紋理對象綁定到特殊的紋理單元,我們可以用Bind函數。
texture.cpp
try {
m_pImage = new Magick::Image(m_fileName);
m_pImage->write(&m_blob, "RGBA");
}
catch (Magick::Error& Error) {
std::cout << "Error loading texture '" << m_fileName << "': " << Error.what() << std::endl;
return false;
}
用上面的代碼,我們把圖像文件裝入內存(此時在system memory中),並准備裝入OpenGL。我們使用了Magic::Image實例,並提供圖像文件名字,使用該函數后,將把紋理圖像數據裝入m_pImage對象內部,OpenGL不能直接訪問,所以我們接着做一個write操作,把紋理數據寫到m_blob變量表示的內存中,我們使用的圖像格式是RGBA。BLOB (Binary Large Object)是一個二進制文件塊,常用來存儲圖像塊,以便其它程序使用。
glGenTextures(1, &m_textureObj);
上面這個OpenGL函數和 glGenBuffers()很相似,第一個參數是個數字,指定要創建的紋理對象數量,第二個參數是紋理對象數組。在本教程中,我們使用一個紋理對象。
glBindTexture(m_textureTarget, m_textureObj);
通過glBindTexture()函數,我們綁定一個紋理對象,這樣下面所有對紋理的操作都是基於該對象,如果我們要操作別的紋理對象,需要重新使用glBindTexture()函數綁定別的紋理對象。glBindTexture()函數中第二個參數是紋理對象句柄,第一個參數是紋理target,它的值可能是 GL_TEXTURE_1D, GL_TEXTURE_2D等等。不同的紋理對象同時只能綁定一個target,在本教程中,target在紋理類構造函數中實施,我們使用的是GL_TEXTURE_2D 。
glTexImage2D(m_textureTarget, 0, GL_RGBA, m_pImage->columns(), m_pImage->rows(), 0, GL_RGBA, GL_UNSIGNED_BYTE, m_blob.data());
glTexImage2D函數用來裝入紋理對象的數據,也就是把system memory中的數據(m_blob)和紋理對象關聯起來,可能在該函數調用時候就拷貝到video memory,也可能是延時拷貝,這個是由driver控制的。 glTexImage* 函數有幾個版本,每個版本都對應一個紋理target。該函數的第一個參數是紋理target,第二個參數是LOD(層次細節),一個紋理對象可能包含多個分辨率的相同圖像,這些圖像稱作mipmap層,每個mipmap層數都有一個LOD索引,范圍從0到最高分辨率。本教程程序中,只有一個mipmap層,所以該值為0 。
第三個參數是紋理對象的格式,你可以指定為4通道的顏色RGBA,或者僅指定紅色通道GL_RED,本教程程序中,我們使用GL_RGBA ,接下來的2個參數是紋理的高度和寬度,通過 ImageMagick的內部函數rows和colomns,我們可以很方便的得到這兩個值。第5個參數是紋理的邊選項,本程序中我們設置為0。
最后的三個參數指定源紋理數據的格式,類型以及數據內存地址。格式指定顏色channel的格式,這必須和m_blob中的data相匹配,類型描述每個顏色channel的格式,本程序中為無符號8位數字GL_UNSIGNED_BYTE,最后一個參數是紋理數據內存地址。
glTexParameterf(m_textureTarget, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameterf(m_textureTarget, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
上面兩個函數指定紋理采樣的方式,紋理采樣方式是紋理狀態的一部分。對於magnification 和 minification ( http://www.cnblogs.com/mikewolf2002/archive/2012/04/07/2436063.html , 關於這兩個概念請參考這個鏈接 ),我們都指定線性濾波.
(texture.cpp)
void Texture::Bind(GLenum TextureUnit)
{
glActiveTexture(TextureUnit);
glBindTexture(m_textureTarget, m_textureObj);
}
在3D程序中,可能有多個draw,每個draw提交前,可能需要綁定不同的紋理,以便在shader中使用,上面的Bind函數就是使我們方便的切換不同的紋理,它的參數是一個紋理單元。
layout (location = 0) in vec3 Position;
layout (location = 1) in vec2 TexCoord;
uniform mat4 gWVP;
out vec2 TexCoord0;
void main()
{
gl_Position = gWVP * vec4(Position, 1.0);
TexCoord0 = TexCoord;
};
這是更新后的頂點shader,這兒有一個輸入參數紋理坐標,是一個2D向量,在頂點shader中我們並沒有對紋理坐標進行任何變化,而是直接輸出,但在片元shader前的光柵化階段,會對紋理坐標進行插值操作。
in vec2 TexCoord0;
out vec4 FragColor;
uniform sampler2D gSampler;
void main()
{
FragColor = texture2D(gSampler, TexCoord0.st);
};
上面是更新后的片元shader,其中有一個輸入變量TexCoord0,它包含了插值后的紋理坐標,還有一個uniform變量gSampler,它是sampler2D類型,應用程序必須設置紋理單元值以便和這個uniform變量連接起來,這樣shader才能訪問紋理,返回值就是采樣的紋理單元顏色。后面的光照的教程中,都是根據光照因子乘以這個采樣顏色,從而得到最終的像素顏色。
Vertex Vertices[4] = {
Vertex(Vector3f(-1.0f, -1.0f, 0.5773f), Vector2f(0.0f, 0.0f)),
Vertex(Vector3f(0.0f, -1.0f, -1.15475), Vector2f(0.5f, 0.0f)),
Vertex(Vector3f(1.0f, -1.0f, 0.5773f), Vector2f(1.0f, 0.0f)),
Vertex(Vector3f(0.0f, 1.0f, 0.0f), Vector2f(0.5f, 1.0f)) };
新的頂點結構包括頂點位置以及頂點紋理坐標。
tutorial16.cpp
...
glEnableVertexAttribArray(1);
...
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), 0);
glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex), (const GLvoid*)12);
...
pTexture->Bind(GL_TEXTURE0);
...
glDisableVertexAttribArray(1);
在渲染循環中也有一些代碼變動,因為增加了紋理坐標屬性,所以我們啟動了屬性1,這和頂點shader中的layout是一致的,接着我們會調用glVertexAttribPointer指定頂點緩沖中紋理坐標的位置,紋理坐標是2個浮點數,所以函數第二個參數是2,注意第五個參數都是頂點結構的大小,這對於位置和紋理屬性是一樣的, 這個參數稱作 'vertex stride',就是2個頂點之間的字節數目。在我們的頂點緩沖中,包含pos0, texture coords0, pos1, texture coords1, 等等,前面的教程中,只有一個位置屬性,所以該參數設置為0。最后一個參數頂點結構起始地址到紋理屬性的偏移字節數。
在draw調用前,我們進行一次紋理綁定操作,注意下面的禁止頂點屬性函數調用,指定頂點屬性后,我們需要在一次禁止它。
glFrontFace(GL_CW);
glCullFace(GL_BACK);
glEnable(GL_CULL_FACE);
上面三個函數設定三角形面背面剔除功能,啟用該功能后,會在PA階段,剔除法向朝后的三角面(背面三角形本來就看不見),從而這些面不會做片元shader,從而提高程序性能。第一個函數指定三角形頂點為順時針順序,就是說從前面看向三角形時,它的頂點是順時針排列,第二個函數指定剔除背面(而不是前面),第三個參數開啟剔除功能。
glUniform1i(gSampler, 0);
設定紋理單元的索引,我們將會在片元shader中通過uniform變量使用紋理。在前面的代碼中,gSampler會通過 glGetUniformLocation()函數得到。
pTexture = new Texture(GL_TEXTURE_2D, "test.png");
if (!pTexture->Load()) {
return 1;
}
上面的代碼創建紋理對象,並裝入它。
程序執行后界面如下: