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来转换图像格式。


  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值