OPENGL 紋理貼圖 過濾 mipmaps (shader)

文章來源於:http://www.opengl-tutorial.org/cn/beginners-tutorials/tutorial-5-a-textured-cube/

本課學習如下幾點:

  • 什麼是UV座標
  • 怎樣自行加載紋理
  • 怎樣在OpenGL中使用紋理
  • 什麼是過濾?什麼是mipmap?怎樣使用?
  • 怎樣利用GLFW更加魯棒地加載紋理?
  • 什麼是alpha通道?

關於UV座標

給模型貼紋理時,我們需要通過UV座標來告訴OpenGL用哪塊圖像填充三角形。

每個頂點除了位置座標外還有兩個浮點數座標:U和V。這兩個座標用於訪問紋理,如下圖所示:

注意觀察紋理是怎樣在三角形上扭曲的。

自行加載.BMP圖片

不用花太多心思瞭解BMP文件格式:很多庫可以幫你加載BMP文件。但BMP格式極爲簡單,可以幫助你理解那些庫的工作原理。所以,我們從頭開始寫一個BMP文件加載器,不過千萬別在實際工程中使用這個實驗品

如下是加載函數的聲明:

1 GLuint loadBMP_custom(const char * imagepath);

使用方式如下:

1 GLuint image = loadBMP_custom("./my_texture.bmp");

接下來看看如何讀取BMP文件。

首先需要一些數據。讀取文件時將設置這些變量。

1 // Data read from the header of the BMP file
2 unsigned char header[54]; // Each BMP file begins by a 54-bytes header
3 unsigned int dataPos;     // Position in the file where the actual data begins
4 unsigned int width, height;
5 unsigned int imageSize;   // = width*height*3
6 // Actual RGB data
7 unsigned char * data;

現在正式開始打開文件。

1 // Open the file
2 FILE * file = fopen(imagepath,"rb");
3 if (!file)
4 {
5     printf("Image could not be openedn");
6     return 0;
7 }

文件一開始是54字節長的文件頭,用於標識”這是不是一個BMP文件”、圖像大小、像素位等等。來讀取文件頭吧:

1 if ( fread(header, 1, 54, file)!=54 ){ // If not 54 bytes read : problem
2     printf("Not a correct BMP filen");
3     return false;
4 }

文件頭總是以”BM”開頭。實際上,如果用十六進制編輯器打開BMP文件,您會看到如下情形:

因此得檢查一下頭兩個字節是否確爲‘B’和‘M’:

1 if ( header[0]!='B' || header[1]!='M' ){
2     printf("Not a correct BMP filen");
3     return 0;
4 }

現在可以讀取文件中圖像大小、數據位置等信息了:

1 // Read ints from the byte array
2 dataPos    = *(int*)&(header[0x0A]);
3 imageSize  = *(int*)&(header[0x22]);
4 width      = *(int*)&(header[0x12]);
5 height     = *(int*)&(header[0x16]);

如果這些信息缺失,您得手動補齊:

1 // Some BMP files are misformatted, guess missing information
2 if (imageSize==0)    imageSize=width*height*3; // 3 : one byte for each Red, Green and Blue component
3 if (dataPos==0)      dataPos=54; // The BMP header is done that way

現在我們知道了圖像的大小,可以爲之分配一些內存,把圖像讀進去:

1 // Create a buffer
2 data = new unsigned char [imageSize];
3 
4 // Read the actual data from the file into the buffer
5 fread(data,1,imageSize,file);
6 
7 //Everything is in memory now, the file can be closed
8 fclose(file);

到了真正的OpenGL部分了。創建紋理和創建頂點緩衝差不多:創建一個紋理、綁定、填充、配置。

在glTexImage2D函數中,GL_RGB表示顏色由三個分量構成,GL_BGR則說明了顏色在內存中的存儲格式。實際上,BMP存儲的並不是RGB,而是BGR,因此得把這個告訴OpenGL。

 1 // Create one OpenGL texture
 2 GLuint textureID;
 3 glGenTextures(1, &textureID);
 4 
 5 // "Bind" the newly created texture : all future texture functions will modify this texture
 6 glBindTexture(GL_TEXTURE_2D, textureID);
 7 
 8 // Give the image to OpenGL
 9 glTexImage2D(GL_TEXTURE_2D, 0,GL_RGB, width, height, 0, GL_BGR, GL_UNSIGNED_BYTE, data);
10 
11 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
12 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);

稍後再解釋最後兩行代碼。同時,得在C++代碼中使用剛寫好的函數加載一個紋理:

1 GLuint Texture = loadBMP_custom("uvtemplate.bmp");

另外十分重要的一點:** 使用2次冪(power-of-two)的紋理!**

  • 優質紋理: 128128, 256256, 10241024, 2*2…
  • 劣質紋理: 127128, 35, …
  • 勉強可以但很怪異的紋理: 128*256

在OpenGL中使用紋理

先來看看片段着色器。大部分代碼一目瞭然:

 1 #version 330 core
 2 
 3 // Interpolated values from the vertex shaders
 4 in vec2 UV;
 5 
 6 // Ouput data
 7 out vec3 color;
 8 
 9 // Values that stay constant for the whole mesh.
10 uniform sampler2D myTextureSampler;
11 
12 void main(){
13 
14     // Output color = color of the texture at the specified UV
15     color = texture( myTextureSampler, UV ).rgb;
16 }

注意三點:

  • 片段着色器需要UV座標。看似合情合理。
  • 同時也需要一個”Sampler2D”來獲知要加載哪一個紋理(同一個着色器中可以訪問多個紋理)
  • 最後一點,用texture()訪問紋理,該方法返回一個(R,G,B,A)的vec4變量。馬上就會瞭解到分量A。

頂點着色器也很簡單,只需把UV座標傳給片段着色器:

 1 #version 330 core
 2 
 3 // Input vertex data, different for all executions of this shader.
 4 layout(location = 0) in vec3 vertexPosition_modelspace;
 5 layout(location = 1) in vec2 vertexUV;
 6 
 7 // Output data ; will be interpolated for each fragment.
 8 out vec2 UV;
 9 
10 // Values that stay constant for the whole mesh.
11 uniform mat4 MVP;
12 
13 void main(){
14 
15     // Output position of the vertex, in clip space : MVP * position
16     gl_Position =  MVP * vec4(vertexPosition_modelspace,1);
17 
18     // UV of the vertex. No special space for this one.
19     UV = vertexUV;
20 }

還記得第四課中的”layout(location = 1) in vec2 vertexUV”嗎?我們得在這兒把相同的事情再做一遍,但這次的緩衝中放的不是(R,G,B)三元組,而是(U,V)數對。

 1 // Two UV coordinatesfor each vertex. They were created with Blender. You'll learn shortly how to do this yourself.
 2 static const GLfloat g_uv_buffer_data[] = {
 3     0.000059f, 1.0f-0.000004f,
 4     0.000103f, 1.0f-0.336048f,
 5     0.335973f, 1.0f-0.335903f,
 6     1.000023f, 1.0f-0.000013f,
 7     0.667979f, 1.0f-0.335851f,
 8     0.999958f, 1.0f-0.336064f,
 9     0.667979f, 1.0f-0.335851f,
10     0.336024f, 1.0f-0.671877f,
11     0.667969f, 1.0f-0.671889f,
12     1.000023f, 1.0f-0.000013f,
13     0.668104f, 1.0f-0.000013f,
14     0.667979f, 1.0f-0.335851f,
15     0.000059f, 1.0f-0.000004f,
16     0.335973f, 1.0f-0.335903f,
17     0.336098f, 1.0f-0.000071f,
18     0.667979f, 1.0f-0.335851f,
19     0.335973f, 1.0f-0.335903f,
20     0.336024f, 1.0f-0.671877f,
21     1.000004f, 1.0f-0.671847f,
22     0.999958f, 1.0f-0.336064f,
23     0.667979f, 1.0f-0.335851f,
24     0.668104f, 1.0f-0.000013f,
25     0.335973f, 1.0f-0.335903f,
26     0.667979f, 1.0f-0.335851f,
27     0.335973f, 1.0f-0.335903f,
28     0.668104f, 1.0f-0.000013f,
29     0.336098f, 1.0f-0.000071f,
30     0.000103f, 1.0f-0.336048f,
31     0.000004f, 1.0f-0.671870f,
32     0.336024f, 1.0f-0.671877f,
33     0.000103f, 1.0f-0.336048f,
34     0.336024f, 1.0f-0.671877f,
35     0.335973f, 1.0f-0.335903f,
36     0.667969f, 1.0f-0.671889f,
37     1.000004f, 1.0f-0.671847f,
38     0.667979f, 1.0f-0.335851f
39 };

上述UV座標對應於下面的模型:

其餘的就很清楚了。創建一個緩衝、綁定、填充、配置,像往常一樣繪製頂點緩衝對象。要注意把glVertexAttribPointer的第二個參數(大小)3改成2。

結果如下:

放大後:

什麼是過濾和mipmap?怎樣使用?

正如在上面截圖中看到的,紋理質量不是很好。這是因爲在loadBMP_custom函數中,有如下兩行代碼:

1 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
2 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);

這意味着在片段着色器中,texture()將直接提取位於(U,V)座標的紋素(texel)。

有幾種方法可以改善這一狀況。

線性過濾(Linear filtering)

若採用線性過濾。texture()會查看周圍的紋素,然後根據UV座標距離各紋素中心的距離來混合顏色。這就避免了前面看到的鋸齒狀邊緣。

線性過濾可以顯著改善紋理質量,應用的也很多。但若想獲得更高質量的紋理,可以採用各向異性過濾,不過速度有些慢。

各向異性過濾(Anisotropic filtering)

這種方法逼近了真正片斷中的紋素區塊。例如下圖中稍稍旋轉了的紋理,各向異性過濾將沿藍色矩形框的主方向,作一定數量的採樣(即所謂的”各向異性層級”),計算出其內的顏色。

Mipmaps

線性過濾和各向異性過濾都存在一個共同的問題。那就是如果從遠處觀察紋理,只對4個紋素作混合顯得不夠。實際上,如果3D模型位於很遠的地方,屏幕上只看得見一個片斷(像素),那計算平均值得出最終顏色值時,圖像所有的紋素都應該考慮在內。很顯然,這種做法沒有考慮性能問題。撇開兩種過濾方法不談,這裏要介紹的是mipmap技術:

  • 一開始,把圖像縮小到原來的1/2,然後依次縮小,直到圖像只有1x1大小(應該是圖像所有紋素的平均值)
  • 繪製模型時,根據紋素大小選擇合適的mipmap。
  • 可以選用nearest、linear、anisotropic等任意一種濾波方式來對mipmap採樣。
  • 要想效果更好,可以對兩個mipmap採樣然後混合,得出結果。

好在這個比較簡單,OpenGL都幫我們做好了,只需一個簡單的調用:

1 // When MAGnifying the image (no bigger mipmap available), use LINEAR filtering
2 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
3 // When MINifying the image, use a LINEAR blend of two mipmaps, each filtered LINEARLY too
4 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
5 // Generate mipmaps, by the way.
6 glGenerateMipmap(GL_TEXTURE_2D);

怎樣利用GLFW加載紋理?

我們的loadBMP_custom函數很棒,因爲這是我們自己寫的!不過用專門的庫更好。GLFW就可以加載紋理(僅限TGA文件):

 1 GLuint loadTGA_glfw(const char * imagepath){
 2 
 3     // Create one OpenGL texture
 4     GLuint textureID;
 5     glGenTextures(1, &textureID);
 6 
 7     // "Bind" the newly created texture : all future texture functions will modify this texture
 8     glBindTexture(GL_TEXTURE_2D, textureID);
 9 
10     // Read the file, call glTexImage2D with the right parameters
11     glfwLoadTexture2D(imagepath, 0);
12 
13     // Nice trilinear filtering.
14     glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
15     glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
16     glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
17     glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
18     glGenerateMipmap(GL_TEXTURE_2D);
19 
20     // Return the ID of the texture we just created
21     return textureID;
22 }

壓縮紋理

學到這兒,您可能會問:那JPEG格式的紋理又該怎樣加載呢?

簡答:用不着考慮這些文件格式,您還有更好的選擇。

創建壓縮紋理

  • 下載The Compressonator,一款ATI工具
  • 用它加載一個二次冪紋理
  • 將其壓縮成DXT1、DXT3或DXT5格式(這些格式之間的差別請參考Wikipedia):

  • 生成mipmap,這樣就不用在運行時生成mipmap了。
  • 導出爲.DDS文件。

至此,圖像已壓縮爲可被GPU直接使用的格式。在着色中隨時調用texture()均可以實時解壓。這一過程看似很慢,但由於它節省了很多內存空間,傳輸的數據量就少了。傳輸內存數據開銷很大;紋理解壓縮卻幾乎不耗時(有專門的硬件負責此事)。一般情況下,採用壓縮紋理可使性能提升20%。

使用壓縮紋理

來看看怎樣加載壓縮紋理。這和加載BMP的代碼很相似,只不過文件頭的結構不一樣:

 1 GLuint loadDDS(const char * imagepath){
 2 
 3     unsigned char header[124];
 4 
 5     FILE *fp;
 6 
 7     /* try to open the file */
 8     fp = fopen(imagepath, "rb");
 9     if (fp == NULL)
10         return 0;
11 
12     /* verify the type of file */
13     char filecode[4];
14     fread(filecode, 1, 4, fp);
15     if (strncmp(filecode, "DDS ", 4) != 0) {
16         fclose(fp);
17         return 0;
18     }
19 
20     /* get the surface desc */
21     fread(&header, 124, 1, fp); 
22 
23     unsigned int height      = *(unsigned int*)&(header[8 ]);
24     unsigned int width         = *(unsigned int*)&(header[12]);
25     unsigned int linearSize     = *(unsigned int*)&(header[16]);
26     unsigned int mipMapCount = *(unsigned int*)&(header[24]);
27     unsigned int fourCC      = *(unsigned int*)&(header[80]);

文件頭之後是真正的數據:緊接着是mipmap層級。可以一次性批量地讀取:

1 unsigned char * buffer;
2     unsigned int bufsize;
3     /* how big is it going to be including all mipmaps? */
4     bufsize = mipMapCount > 1 ? linearSize * 2 : linearSize;
5     buffer = (unsigned char*)malloc(bufsize * sizeof(unsigned char));
6     fread(buffer, 1, bufsize, fp);
7     /* close the file pointer */
8     fclose(fp);

這裏要處理三種格式:DXT1、DXT3和DXT5。我們得把”fourCC”標識轉換成OpenGL能識別的值。

 1 unsigned int components  = (fourCC == FOURCC_DXT1) ? 3 : 4;
 2     unsigned int format;
 3     switch(fourCC)
 4     {
 5     case FOURCC_DXT1:
 6         format = GL_COMPRESSED_RGBA_S3TC_DXT1_EXT;
 7         break;
 8     case FOURCC_DXT3:
 9         format = GL_COMPRESSED_RGBA_S3TC_DXT3_EXT;
10         break;
11     case FOURCC_DXT5:
12         format = GL_COMPRESSED_RGBA_S3TC_DXT5_EXT;
13         break;
14     default:
15         free(buffer);
16         return 0;
17     }

像往常一樣創建紋理:

1 // Create one OpenGL texture
2     GLuint textureID;
3     glGenTextures(1, &textureID);
4 
5     // "Bind" the newly created texture : all future texture functions will modify this texture
6     glBindTexture(GL_TEXTURE_2D, textureID);

現在只需逐個填充mipmap:

 1 unsigned int blockSize = (format == GL_COMPRESSED_RGBA_S3TC_DXT1_EXT) ? 8 : 16;
 2     unsigned int offset = 0;
 3 
 4     /* load the mipmaps */
 5     for (unsigned int level = 0; level < mipMapCount && (width || height); ++level)
 6     {
 7         unsigned int size = ((width+3)/4)*((height+3)/4)*blockSize;
 8         glCompressedTexImage2D(GL_TEXTURE_2D, level, format, width, height, 
 9             0, size, buffer + offset);
10 
11         offset += size;
12         width  /= 2;
13         height /= 2;
14     }
15     free(buffer); 
16 
17     return textureID;

反轉UV座標

DXT壓縮源自DirectX。和OpenGL相比,DirectX中的V紋理座標是反過來的。所以使用壓縮紋理時,得用(coord.v, 1.0-coord.v)來獲取正確的紋素。可以在導出腳本、加載器、着色器等環節中執行這步操作

總結

剛纔我們學習了創建、加載以及在OpenGL中使用紋理。

總的來說,壓縮紋理體積小、加載迅速、使用便捷,應該只用壓縮紋理;主要的缺點是得用The Compressonator來轉換圖像格式。


發佈了7 篇原創文章 · 獲贊 39 · 訪問量 10萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章