從0開始的OpenGL學習(五)-紋理

本文主要解決一個問題:

在OpenGL中如何使用紋理?

一、什么是紋理?

紋理,英文是texture,中文可以翻譯成紋理、紋理圖、紋理映射等等一堆東西。不過不管翻譯成啥,講的都是一個東西。我們通常說的紋理,指的是一張二維的圖片,把它像貼紙一樣貼在什么東西上面,讓那個東西看起來像我們貼紙所要表現的東西那樣。

舉例來說,假如我們想繪制一面磚墻,我們該怎么辦?根據我們已經掌握的知識來看,我們需要用成千上萬的點來模擬它的顏色,我的天,這要搞到猴年馬月才能搞出來?顯然不現實!于是聰明的程序員們想出了一個好方法,就是用一張圖“貼”到物體的表面上,讓它看起來像是一面磚墻的樣子,省時省力省心。

用一句話來總結,紋理就是一張貼到物體上的2維圖像。

二、映射方式

既然是要把圖貼到物體上,自然就要想怎么貼才行。我們可以橫貼、豎貼、斜貼怎么貼都行,但是怎樣貼才能達到我們想要的效果呢?總要有個規章制度來規定一下怎么貼才行吧。這個貼法,就稱為映射。

規則是:以左下角為原點,向右伸展到1.0的位置,向上伸展到1.0的位置,表示一整張的紋理圖像。

紋理圖坐標(看著美女圖是不是比磚塊圖有激情多了?)

使用紋理圖的時候,我們需要在頂點數據中添加一個紋理坐標的數據,標明我們是如何將紋理上的元素映射到頂點上的,這個我們在后面的實現環節再詳細說明。

三、紋理環繞方式(Texture Wrapping)

通常,紋理坐標的范圍在(0,0)到(1,1)之間,但是如果我們制定的坐標在這之外呢?OpenGL會如何做出反應?默認情況下,OpenGL會重復繪制紋理圖,不過,OpenGL也提供了更多的選擇方案:

  • GL_REPEAT: 默認方案,重復紋理圖片。
  • GL_MIRRORED_REPEAT:類似于默認方案,不過每次重復的時候進行鏡像重復。
  • GL_CLAMP_TP_EDGE:將坐標限制在0到1之間。超出的坐標會重復繪制邊緣的像素,變成一種擴展邊緣的圖案。(通常很難看)
  • GL_CLAMP_TO_BORDER:超出的坐標將會被繪制成用戶指定的邊界顏色。

每種方案的顯示效果截然不同,不必擔心你會搞混了,看看效果就知道了。

四種紋理環繞方式的效果對比

怎么樣,只要視力在1000度之內,都能看出明顯的區別吧。

設置紋理環繞方式的方法是調用glTexParameteri函數,具體方式如下:

glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);    //橫坐標
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);    //縱坐標

至于當你設定了GL_CLAMP_TO_BORDER的環繞方式,想要指定邊界顏色,就需要使用glTexParameterfv函數了,像這樣:

float borderColor[] = { 1.0f, 1.0f, 0.0f, 1.0f };  //指定成黃色
glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, borderColor); 

記得在設置了環繞方式之后用!

四、紋理過濾(Texture Filtering)

紋理坐標采用了浮點數的形式,表明了它和分辨率無關。OpenGL需要非常精確的計算出紋理像素(通常被稱為紋素)和紋理坐標之間的對應關系。當你有一張低分辨率的紋理圖,但是需要用到一個非常大的物體上時,這種操作的重要性就更加明顯了。聰明的你可能已經猜到了,沒錯,OpenGL提供了幾種不同的方案來解決這個問題,我們只討論最重要的兩種:GL_NEAREST和GL_LINEAR。

  • GL_NEAREST
    最近點過濾。指的是紋理坐標最靠近哪個紋素,就用哪個紋素。這是OpenGL默認的過濾方式,速度最快,但是效果最差。
  • GL_LINEAR
    (雙)線性過濾。指的是紋理坐標位置附近的幾個紋素值進行某種插值計算之后的結果。這是應用最廣泛的一種方式,效果一般,速度較快。

不過,你一定有個疑問,這些方法使用后,顯示上有啥區別?別急,我們來看兩張圖就知道了。

最近點過濾和線性過濾的區別(圖片資源來自:www.learningopengl.com)

什么,你問我那張美女圖呢?這個,不是我不想用,是那張圖實在是太不明顯了,怕你看不出來。我們還是來看這張圖,效果杠杠的。

很明顯,最近點過濾的像素痕跡非常明顯,看著就跟“屎大棒”似的。而線性過濾的方式效果就好上很多了,雖然感覺很模糊,但我們完全能理解一張小圖放大之后會模糊這件事。

設置過濾方式還是使用glTexParameteri,像這樣:

glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);  //縮小時的過濾方式
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);  //放大時的過濾方式

五、Mipmaps

多級漸進紋理。雖然有這樣的翻譯,但是這種翻譯太小眾了,還是決定使用英文來表示。(筆者對自己的英文能力還是有信心的,不然也不會去看英文資料,嘿嘿~)

言歸正傳!想象這樣一個場景,有非常多的同種物體,距離觀察者的遠近各不相同,我們可以對這種物體使用同一張紋理貼圖,但是問題來了,對于那些離觀察者比較遠的物體,真的有必要用原始的紋理貼圖去映射嗎?答案當然是否定的。太遠的物體我們看不清楚,顯示的非常精細沒有意義,而且使用原始貼圖計算映射起來太麻煩,所以,我們使用一種mipmaps的方式來進行處理。

所謂的mipmaps,就是一系列的紋理圖片,每一張紋理圖的大小都是前一張的1/4,直到剩最后一個像素為止??雌饋砭拖袷沁@一個樣子:

mipmaps示例(圖片資源來自:www.learningopengl.com)

當物體越離越遠的時候,就可以選用較小紋理去映射,這樣不僅效果好,而且速度也快。

你可能會有疑問,這個圖片是程序弄的還是美術弄的呢?對于這個問題,我的回答是:美術弄不弄我不知道,但是程序肯定可以弄,而且弄起來還非常方便。我們只要調用一個函數就行了,那就是glGenerateMipmaps。

那我們開始用吧!哦偶,還不行,還有一個問題沒解決。

疑問:
看上去很好,可是我物體的大小剛好在兩張mipmaps之間怎么辦呢?

對于剛好在兩張圖片之間的物體,我們可以參考前面兩種過濾方式,最近點(采用最接近的圖)或者線性(采用兩張圖的加權平均)。這樣,我們就有了四種不同的過濾方案。

  • GL_NEAREST_MIPMAP_NEAREST:采用最近的mipmap圖,在紋理采樣的時候使用最近點過濾采樣。
  • GL_LINEAR_MIPMAP_NEAREST:采用最近的mipmap圖,紋理采樣的時候使用線性過濾采樣。
  • GL_NEAREST_MIPMAP_LINEAR:采用兩張mipmap圖的線性插值紋理圖,紋理采樣的時候采用最近點過濾采樣。
  • GL_LINEAR_MIPMAP_LINEAR:采用兩張mipmap圖的線性插值紋理圖,紋理采樣的時候采用線性過濾采樣。

好了,這次是真的可以用了。使用的函數還是一樣,glTexParameteri。像這樣:

glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);  //都是線性過濾
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

這里要注意一點,mipmaps是用來處理物體變小時候如何進行貼圖的問題的,所以不需要設置GL_TEXTURE_MAG_FILTER成mipmap方式。如果強行使用了,會報錯。(不信的話去試試?。?/p>

六、使用

準備

我們要做的第一件事就是將紋理圖加載到我們的應用之中,但是,圖片的格式不計其數,難道我們要對每一種格式都寫一個加載的模塊嗎?這顯然不是我們想要的。最好能有一個庫來加載所有的圖片格式,讓我們可以直接用,這樣我們就可以專注在映射上了。

幸運的是,確實有這樣一個庫我們可以直接用(鏈接)。打包下載之后,將其中的stb_image.h文件包含到我們的項目中去,我們要使用這個文件。

使用方式

我們使用stbi_load函數來加載圖片,并且將圖片格式信息保存起來。像這樣:

int width, height, nrChannels;
unsigned char *data = stbi_load("beauty.jpg", &width, &height, &nrChannels, 0); 

width,height和nrChannels變量中分別會保存圖片的寬度、高度和顏色通道數量的信息,這些都是在之后有用的數據。

頂點數據

到這里,我們的頂點已經有許多相關聯的數據了,比如顏色,比如紋理坐標,是時候該給我們的頂點數組升升級了。

我們的頂點數組中需要三樣東西:位置、顏色、紋理坐標。每個頂點都需要這三組數據,在數組中依次排下去。于是,頂點數組就成了現在這個樣子:

float vertices[] = {
    //位置                  // 顏色                 //紋理坐標
    0.5f,   0.5f,   0.0f,   1.0f,   0.0f,   0.0f,   1.0f,   1.0f,   //右上角
    0.5f,   -0.5f,  0.0f,   0.0f,   1.0f,   0.0f,   1.0f,   0.0f,   //右下角
    -0.5f,  -0.5f,  0.0f,   0.0f,   0.0f,   1.0f,   0.0f,   0.0f,   //左下角
    -0.5f,  0.5f,   0.0f,   1.0f,   1.0f,   0.0f,   0.0f,   1.0f    //左上角

};
頂點數組的結構

可以看到,我們的跨度變成了32,也就是8sizeof(float),顏色的起始偏移值為3sizeof(float),紋理的起始偏移值為6*sizeof(float)。這樣,我們指定頂點屬性的時候自然需要指定相應的位置和偏移。

顏色屬性:

glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(3 * sizeof(float)));
glEnableVertexAttribArray(1);

紋理屬性:

glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(6 * sizeof(float)));
glEnableVertexAttribArray(2);

頂點著色器中,我們需要采用輸入的顏色和紋理坐標進行操作,所以,在著色器中添加兩個輸入是非常好的選擇。

layout (location = 1) in vec3 aColor;
layout (location = 2) in vec2 aTexCoord;

然后,我們的頂點著色器代碼就成了這個樣子:

//頂點著色器代碼
#version 330 core

layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aColor;
layout (location = 2) in vec2 aTexCoord;

out vec3 ourColor;
out vec2 TexCoord;

void main()
{
    gl_Position = vec4(aPos, 1.0);
    ourColor = aColor;
    TexCoord = aTexCoord;
}

代碼都是自解釋的,非常容易理解,就不再浪費口舌了。相應的,我們的片元著色器也就成了這個樣子:

//片元著色器代碼
#version 330 core

out vec4 FragColor;

in vec3 ourColor;
in vec2 TexCoord;

uniform sampler2D ourTexture;

void main()
{
    FragColor = texture(ourTexture, TexCoord);  //對紋理指定位置進行采樣
}

創建紋理

想OpenGL里的其他東西那樣,紋理也需要一個唯一ID,我們來創建一個:

unsigned int texture;
glGenTextures(1, &texture);

glGenTextures的第一個參數是要創建的紋理數量,后面的參數就是保存這么多數量的整型數數組。當然,創建完了之后我們也需要綁定到OpenGL的環境里才能操作。

glBindTexture(GL_TEXTURE_2D, texture);

綁定完成后,我們就可以把之前加載的圖片數據放到紋理中去了。

glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, data);
glGenerateMipmap(GL_TEXTURE_2D);

又是一個擁有很多參數的龐大函數。我們一點點啃死它!

  • 參數一:指定目標紋理。GL_TEXTURE_2D就表示當前的操作會對綁定的2D紋理產生作用(GL_TEXTURE_1D和GL_TEXTURE_3D里的東西就不會受影響)
  • 參數二:mipmap層級。我們之后會調用glGenerateMipmap來創建,這里只需要創建原始圖就行了。(或者你也可以手動的一次次調用這個函數來創建,(壞笑~))
  • 參數三:我們需要保存的紋理格式。我們的圖片只有RGB信息,所以用GL_RGB格式。
  • 參數四和參數五:紋理圖片的寬高。之前保存的那個。
  • 參數六:一定要設置成0(有一些遺留的工作)
  • 參數七和參數八:源圖片的格式和數據類型。我們加載的圖片中有RGB值,并且以字節的方式保存。所以我們傳遞了這兩個參數。
  • 參數九:加載的圖片數據。

調用glTexImage2D之后,當前的紋理對象就和我們的紋理圖綁定起來了。然后,我們再調用glGenerateMipmap函數創建mipmaps,非常流暢~

操作完成之后,最好要把圖片釋放掉,因為數據已經都保存到紋理對象中了,這樣干:

stbi_image_free(data);

整個代碼塊就是這個樣子:

//紋理
unsigned int texture;
glGenTextures(1, &texture);
glBindTexture(GL_TEXTURE_2D, texture);
//設置紋理包裝和過濾的方式
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
int width, height, nrChannels;
//stbi_set_flip_vertically_on_load(true);
unsigned char* data = stbi_load("beauty.jpg", &width, &height, &nrChannels, 0);
if (data) {
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, data);
    glGenerateMipmap(GL_TEXTURE_2D);
}
else
    std::cout << "無法加載問題,請檢查代碼或資源是否有誤。" << std::endl;
stbi_image_free(data);

七、運行

好,咱就運行起來。

運行效果

嗯?不對啊,怎么是倒的?

這是因為OpenGL期待原點(0,0)位于左下角,而通常一張圖片的原點位于左上角。不過,這不是問題,stb庫早就已經為我們準備了解決方案。在加載圖片之前調用stbi_set_flip_vertically_on_load(true);

哈哈,是不是注意到了我之前注釋掉的那行代碼,沒錯,就是為了顯示出倒圖而故意注掉的。

好,再次編譯運行,這次的效果才對!

正確的效果

八、結語

呼~ 好長的一篇,不過總算是弄了點東西,辛苦沒有白費~

下一篇
目錄
上一篇

參考資料:
www.learningopengl.com(非常好的網站,建議也去學習)

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容