文章來源於: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來轉換圖像格式。