OpenGL超级宝典笔记——纹理映射Mipmap
Mipmapping
Mipmap是一个功能强大的纹理技术,它可以提高渲染的性能以及提升场景的视觉质量。它可以用来解决使用一般的纹理贴图会出现的两个常见的问题:
- 闪烁,当屏幕上被渲染物体的表面与它所应用的纹理图像相比显得非常小时,就会出现闪烁。尤其当相机和物体在移动的时候,这种负面效果更容易被看到。
- 性能问题。加载了大量的纹理数据之后,还要对其进行过滤处理(缩小),在屏幕上显示的只是一小部分。纹理越大,所造成的性能影响就越大。
Mipmap就可以解决上面那两个问题。当加载纹理的时候,不单单是加载一个纹理,而是加载一系列从大到小的纹理当mipmapped纹理状态中。然后OpenGl会根据给定的几何图像的大小选择最合适的纹理。Mipmap是把纹理按照2的倍数进行缩放,直到图像为1x1的大小,然后把这些图都存储起来,当要使用的就选择一个合适的图像。这会增加一些额外的内存。在正方形的纹理贴图中使用mipmap技术,大概要比原先多出三分之一的内存空间。
mipmap有多少个层级是有glTexImage的第二个参数level决定的。层级从0开始,0,1,2,3这样递增。如果没有使用mipmap技术,只有第0层的纹理会被加载。在默认情况下,为了使用mipmap,所有层级都会被加载。但我们可以通过纹理参数来控制要加载的层级范围,使用glTexParameteri, 第二个参数为GL_TEXTURE_BASE_LEVEL来指定最低层级的level,第二个参数为GL_TEXTURE_MAX_LEVEL指定最高层级的level。例如我只需要加载0到4层级的纹理:
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 0);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 4);
除此之外,我们还可通过GL_TEXTURE_MIN_LOD和GL_TEXTURE_MAX_LOD来限制纹理的使用范围(最底层和最高层)。
Mipmap过滤
Mipmap的纹理过滤模式如下表:
常量 | 描述 |
GL_NEAREST | 在mip基层上使用最邻近过滤 |
GL_LINEAR | 在mip基层上使用线性过滤 |
GL_NEAREST_MIPMAP_NEAREST | 选择最邻近的mip层,并使用最邻近过滤 |
GL_NEAREST_MIPMAP_LINEAR | 在mip层之间使用线性插值和最邻近过滤 |
GL_LINEAR_MIPMAP_NEAREST | 选择最邻近的mip层,使用线性过滤 |
GL_LINEAR_MIPMAP_LINEAR | 在mip层之间使用线性插值和使用线性过滤,又称三线性mipmap |
如果纹理过滤选择为GL_NEAREST或GL_LINEAR模式,那么只有基层的纹理会被加载,其他的纹理将会被忽略。我们必须指定其中一个mipmap过滤器,这样才能使用所有已加载的纹理。这个mipmap过滤器的常量是GL_FILTER_MIPMAP_SELECTOR的形式。其中FLILTER指定了过滤模式,SELECTOR指定了如何选择mipmap层。例如GL_NEAREST_MIPMAP_LINEAR模式,它的SELECTOR是GL_LINEAR,它会在两个最邻近的mip层中执行线性插值,然后得出的结果又由被选择的过滤器GL_NEAREST进行过滤。
其中GL_NEAREST_MIPMAP_NEAAREST具有很好的性能,也能够解决闪烁的问题,但在视觉效果上会比较差。其中GL_LINEAR_MIPMAP_NEAREST常用于游戏加速,使用了质量较高的线性过滤,和快速的选择的方式(最邻近方式)。
使用最邻近的方式作为mipmap选择器的效果依然不能令人满意。从某一个角度去看,常常可以看到物体表面从一个mip层到另一个mip层的转变。GL_LINEAR_MIPMAP_LINEAR和GL_NEAREST_MIPMAP_LINEAR过滤器在mip层之间执行一些额外的线性插值,以消除不同层之间的变换痕迹,但也需要一些额外的性能开销。GL_LINEAR_MIPMAP_LINEAR具有最高的精度。
构建Mip层
mip贴图需要加载更小的基本纹理图像以便使用。但我们手头上没有这些更小的纹理图像,怎么办呢。GLU函数库提供了一个很方便的方法gluBuildMipmaps,它会帮我们缩放图像并通过类似glTexImage的函数加载图像。支持1维、2维、3维的图像,函数原型如下:
int gluBuild1DMipmaps(GLenum target, GLint internalFormat, GLint width, GLenum format, GLenum type, const void *data);
int gluBuild2DMipmaps(GLenum target, GLint internalFormat, GLint width, GLint height, GLenum format, GLenum type, const void *data);
int gluBuild3DMipmaps(GLenum target, GLint internalFormat, GLint width, GLint height, GLint depth, GLenum format, GLenum type, const void *data);
参数的意义与glTexImage相同。但没有level参数来指定mipmap的层级,也不支持纹理边界。使用这个函数未必能够获得高质量的较小的纹理贴图,只是比较方便。要使用高质量的不同比例的纹理贴图,最好是自己手工制作,然后加载。GLU库是使用box过滤器(简单地就是对给定范围的像素进行加权平均,例如7X7的box filter,你就需要对49个像素进行平均)
新版的GLU库中可以使用gluBuild*MipmapLevels来更好的控制加载的纹理层级
int gluBuild1DMipmapLevels(GLenum target, GLint internalFormat, GLint width, GLenum format, GLenum type, GLint base, GLint max, const void *data);
int gluBuild2DMipmapLevels(GLenum target, GLint internalFormat, GLint width, GLint height, GLenum format, GLenum type, GLint base, GLint max, const void *data);
int gluBuild3DMipmapLevels(GLenum target, GLint internalFormat, GLint widht, GLint height, GLint depth, GLenum format, GLenum type, GLint base, GLint max, const void *data);
创建从base到max层的纹理数据。
Mipmaps 硬件生成
使用OpenGL的硬件加速来生成所需要的纹理。函数调用如下:
glTexParameteri(GL_TEXTURE_2D, GL_GENERATE_MIPMAP, GL_TRUE);
当这个参数被设置为GL_TRUE时,所有调用glTexImage或者glTexSubImage都会自动更新纹理贴图(第0级)和所有更低层的纹理。通过使用硬件加速会比使用glu库中的gluBuildMipmap要快,但这个特性本来只是个扩展,在OpenGL1.4才被纳入OpenGL核心API的。
LOD(多细节层次)偏好
当mipmapping被启用时,OpenGL会根据各个mip层的大小和几何图形在屏幕上的面积来决定哪一个mip层被选择。OpenGL会选择最合适的mip贴图层与屏幕上的纹理表示形式进行匹配。我们可以告诉OpenGL向后(选择更大的mip层)或向前(选择更小的mipmap层)来调整选择的标准。使用更小的mip层可以提高性能,选择更大的mip层可以锐化纹理映射的对象。这个偏好设置示例如下:
glTexEnvf(GL_TEXTURE_FILTER_CONTROL, GL_TEXTURE_LOD_BIAS, –1.5);
上面会使的细节纹理层倾向于使用更高层的细节(更小的level层参数),从而使得纹理的外观更为锐利,代价是纹理处理的开销大一些。
纹理对象
glTexImage, glTexSubImage和gluBuildMipmaps这些函数的调用消耗的时间特别多。这些函数大量的移动内存,有时需要重新调整数据的格式来适应一些内部的表示。在纹理之间切换或者重新加载不同的纹理图片会带来较大的性能开销。
为了减少这些开销,我们可以使用纹理对象。纹理对象允许你一次性加载多个纹理状态(包括纹理图像),然后在它们之间快速切换。纹理状态由当前绑定的纹理对象来维护。纹理的名称由unsigned int类型来标识。使用下面的函数来生成一定数量的纹理对象:
void glGenTextures(GLsizei n, GLuint *textures);
上面的函数调用指定了纹理对象的数量,和存储纹理对象名称的数组。我们可以通过纹理对象名称来操作纹理状态。绑定其中的一个纹理状态的函数调用如下:
void glBindTexture(GLenum target, GLuint texture);
target参数必须是GL_TEXTURE_1D,GL_TEXTURE_2D或者GL_TEXTURE_3D.texture是纹理名称指定要绑定的纹理对象。在此函数之后,纹理图像的加载和纹理参数的设置都只影响当前绑定的纹理对象。最后删除纹理对象的函数如下:
void glDeleteTextures(GLsizei n, GLuint *texture);
参数的意义与glGenTextures相同。不一定需要每次产生纹理对象使用后就删除所有的纹理对象。多次调用glGenTextures的开销较小,但多次调用glDeleteTextures会有导致一些延迟,原因是需要释放大量的能存空间。在不再需要此纹理对象时,要把该纹理对象删除,防止内存泄露。
判断纹理对象名称是否可用可以通过下面的函数调用来判断:
GLboolean glIsTexture(GLuint texture);
返回GL_TRUE代表可用,GL_FALSE代表不可用。
管理多个纹理
一般而言,在程序初始化时加载多个纹理对象,然后在渲染期间不断地切换,在不再使用时删除纹理对象。下面是一个通道tunnel的例子,此例在启动时加载三个纹理对象,然后通过切换来绘制通道的砖墙,天花板和地板。此例中还演示了不同的mipmap模式,通过右键菜单来切换,通过上下箭头键来在通道中移动,通过左右键来旋转通道。
完整示例如下:
#include "gltools.h"
#include <stdio.h>
//定义宏常量
#define CEILING 0
#define BRICK 1
#define FLOOR 2
#define TEXTURENUM 3
//纹理图像的路径
const char* texFileName[] = {"..\\ceiling.tga","..\\brick.tga","..\\floor.tga"};
//纹理对象名称
static GLuint textureName[TEXTURENUM];
//旋转与移动
static GLfloat yRot = 0.0f;
static GLfloat zPos = 0.0f;
//切换不同的纹理模式
void ProcessMenu(int value)
{
switch (value)
{
case 0:
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
break;
case 1:
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
break;
case 2:
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST_MIPMAP_NEAREST);
break;
case 3:
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_NEAREST);
break;
case 4:
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST_MIPMAP_LINEAR);
break;
case 5:
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
break;
case 6:
if (gltIsExtSupported("GL_EXT_texture_filter_anisotropic"))
{
//开启各向异性过滤
GLfloat fLargest;
glGetFloatv(GL_MAX_TEXTURE_MAX_ANISOTROPY_EXT, &fLargest);
printf("anisotropic:%f\n", fLargest);
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAX_ANISOTROPY_EXT, fLargest);
}
break;
default:
break;
}
glutPostRedisplay();
}
void SetupRC()
{
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
//开启深度测试,消除隐藏面,避免后画的墙画到前面来
glEnable(GL_DEPTH_TEST);
//纹理图像的信息
GLint iWidth, iHeight, iComponents;
GLenum eFormat;
//设置纹理环境
glTexEnvi(GL_TEXTURE_2D, GL_TEXTURE_ENV, GL_REPLACE);
//生成纹理对象
glGenTextures(TEXTURENUM, textureName);
for (int i = 0; i < TEXTURENUM; ++i)
{
void *pImage = gltLoadTGA(texFileName[i], &iWidth, &iHeight, &iComponents, &eFormat);
if (pImage)
{
//绑定纹理对象,生成mipmap
glBindTexture(GL_TEXTURE_2D, textureName[i]);
gluBuild2DMipmaps(GL_TEXTURE_2D, iComponents, iWidth, iHeight, eFormat, GL_UNSIGNED_BYTE, pImage);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
}
free(pImage);
}
glEnable(GL_TEXTURE_2D);
}
void ShutdownRC()
{
//最后删除纹理对象
glDeleteTextures(TEXTURENUM, textureName);
}
void RenderScene()
{
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glPushMatrix();
//移动和旋转
glTranslatef(0.0f, 0.0f, zPos);
glRotatef(yRot, 0.0f, 1.0f, 0.0f);
for(GLfloat z = -60.0f; z <= 0.0f; z += 10.0f)
{
//绑定地板纹理绘制地板,注意glBeindTexture在glBegin和glEnd中是无效的
glBindTexture(GL_TEXTURE_2D, textureName[FLOOR]);
glBegin(GL_QUADS);
glTexCoord2f(0.0f, 0.0f);
glVertex3f(-10.0f, -10.0f, z);
glTexCoord2f(1.0f, 0.0f);
glVertex3f(-10.0f, -10.0f, z + 10.0f);
glTexCoord2f(1.0f, 1.0f);
glVertex3f(10.0f, -10.0f, z + 10.0f);
glTexCoord2f(0.0f, 1.0f);
glVertex3f(10.0f, -10.0f, z);
glEnd();
//绑定天花板纹理
glBindTexture(GL_TEXTURE_2D, textureName[CEILING]);
glBegin(GL_QUADS);
glTexCoord2f(0.0f, 0.0f);
glVertex3f(-10.0f, 10.0f, z);
glTexCoord2f(1.0f, 0.0f);
glVertex3f(-10.0f, 10.0f, z + 10.0f);
glTexCoord2f(1.0f, 1.0f);
glVertex3f(10.0f, 10.0f, z + 10.0f);
glTexCoord2f(0.0f, 1.0f);
glVertex3f(10.0f, 10.0f, z);
glEnd();
//绑定砖墙的纹理
glBindTexture(GL_TEXTURE_2D, textureName[BRICK]);
glBegin(GL_QUADS);
glTexCoord2f(0.0f, 0.0f);
glVertex3f(-10.0f, -10.0f, z);
glTexCoord2f(1.0f, 0.0f);
glVertex3f(-10.0f, 10.0f, z);
glTexCoord2f(1.0f, 1.0f);
glVertex3f(-10.0f, 10.0f, z + 10.0f);
glTexCoord2f(0.0f, 1.0f);
glVertex3f(-10.0f, -10.0f, z + 10.0f);
glTexCoord2f(0.0f, 0.0f);
glVertex3f(10.0f, -10.0f, z);
glTexCoord2f(1.0f, 0.0f);
glVertex3f(10.0f, 10.0f, z);
glTexCoord2f(1.0f, 1.0f);
glVertex3f(10.0f, 10.0f, z + 10.0f);
glTexCoord2f(0.0f, 1.0f);
glVertex3f(10.0f, -10.0f, z + 10.0f);
glEnd();
}
//GLclampf prioritize[TEXTURENUM] = {0.0f, 0.0f, 1.0f};
//glPrioritizeTextures(TEXTURENUM, textureName, prioritize);
//GLboolean isResident[TEXTURENUM];
//if (glAreTexturesResident(TEXTURENUM, textureName, isResident))
//{
// printf("all texture is resident\n");
//}
//else
//{
// printf("texture resident is : %d %d %d", isResident[0], isResident[1], isResident[2]);
//}
glPopMatrix();
glutSwapBuffers();
}
void ChangeSize(GLsizei w, GLsizei h)
{
if (h == 1)
h = 0;
glViewport(0, 0, w, h);
GLfloat aspect = (GLfloat)w/(GLfloat)h;
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
gluPerspective(35.5, aspect, 1.0, 150.0);
glMatrixMode(GL_MODELVIEW);
glLoadIdentity();
glutPostRedisplay();
}
void SpecialKey(int value, int x, int y)
{
if (value == GLUT_KEY_LEFT)
{
yRot += 0.5f;
}
if (value == GLUT_KEY_RIGHT)
{
yRot -= 0.5f;
}
if (value == GLUT_KEY_UP)
{
zPos += 0.5f;
}
if (value == GLUT_KEY_DOWN)
{
zPos -= 0.5f;
}
if (yRot > 365.5f)
{
yRot = 0.0f;
}
glutPostRedisplay();
}
int main(int arg, char **argv)
{
glutInit(&arg, argv);
glutInitDisplayMode(GL_RGB | GL_DOUBLE | GL_DEPTH);
glutInitWindowSize(800, 600);
glutCreateWindow("tunel");
glutReshapeFunc(ChangeSize);
glutDisplayFunc(RenderScene);
glutSpecialFunc(SpecialKey);
glutCreateMenu(ProcessMenu);
glutAddMenuEntry("GL_NEAREST", 0);
glutAddMenuEntry("GL_LINEAR", 1);
glutAddMenuEntry("GL_NEAREST_MIPMAP_NEAREST", 2);
glutAddMenuEntry("GL_LINEAR_MIPMAP_NEAREST", 3);
glutAddMenuEntry("GL_NEAREST_MIPMAP_LINEAR", 4);
glutAddMenuEntry("GL_LINEAR_MIPMAP_LINEAR", 5);
glutAddMenuEntry("ANISOTROPIC", 6);
glutAttachMenu(GLUT_RIGHT_BUTTON);
SetupRC();
glutMainLoop();
ShutdownRC();
return 0;
}
GL_NEAREST效果图(纹理比较锐利):
GL_LINEAR_MIPMAP_LINEAR效果图(线性插值后的纹理过渡较平滑):
在我们程序初始化时,生成了多个纹理对象,加载了多个纹理。这样我们在使用时就非常的方便,只要通过glBindTexutre切换纹理就可以了。使用完之后就释放纹理。
for(GLfloat z = -60.0f; z <= 0.0f; z += 10.0f)
{
//绑定地板纹理绘制地板,注意glBeindTexture在glBegin和glEnd中是无效的
glBindTexture(GL_TEXTURE_2D, textureName[FLOOR]);
glBegin(GL_QUADS);
glTexCoord2f(0.0f, 0.0f);
glVertex3f(-10.0f, -10.0f, z);
glTexCoord2f(1.0f, 0.0f);
glVertex3f(-10.0f, -10.0f, z + 10.0f);
glTexCoord2f(1.0f, 1.0f);
glVertex3f(10.0f, -10.0f, z + 10.0f);
glTexCoord2f(0.0f, 1.0f);
glVertex3f(10.0f, -10.0f, z);
glEnd();
//绑定天花板纹理
glBindTexture(GL_TEXTURE_2D, textureName[CEILING]);
glBegin(GL_QUADS);
glTexCoord2f(0.0f, 0.0f);
glVertex3f(-10.0f, 10.0f, z);
glTexCoord2f(1.0f, 0.0f);
glVertex3f(-10.0f, 10.0f, z + 10.0f);
glTexCoord2f(1.0f, 1.0f);
glVertex3f(10.0f, 10.0f, z + 10.0f);
glTexCoord2f(0.0f, 1.0f);
glVertex3f(10.0f, 10.0f, z);
glEnd();
//绑定砖墙的纹理
glBindTexture(GL_TEXTURE_2D, textureName[BRICK]);
glBegin(GL_QUADS);
glTexCoord2f(0.0f, 0.0f);
glVertex3f(-10.0f, -10.0f, z);
glTexCoord2f(1.0f, 0.0f);
glVertex3f(-10.0f, 10.0f, z);
glTexCoord2f(1.0f, 1.0f);
glVertex3f(-10.0f, 10.0f, z + 10.0f);
glTexCoord2f(0.0f, 1.0f);
glVertex3f(-10.0f, -10.0f, z + 10.0f);
glTexCoord2f(0.0f, 0.0f);
glVertex3f(10.0f, -10.0f, z);
glTexCoord2f(1.0f, 0.0f);
glVertex3f(10.0f, 10.0f, z);
glTexCoord2f(1.0f, 1.0f);
glVertex3f(10.0f, 10.0f, z + 10.0f);
glTexCoord2f(0.0f, 1.0f);
glVertex3f(10.0f, -10.0f, z + 10.0f);
glEnd();
}
tunnel示例中,切换mipmap纹理过滤器时,只修改了缩小过滤器
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
典型的情况下,在OpenGL选择了最大的可用mip层之后,就没有更大的mip层可供选择了。这相当于设置了一条门槛,更大的图形也只能使用这个mip层,没有更大的mip层了。
常驻纹理
在大多数的OpenGL实现中,都提供了一定数量的常驻纹理来提高性能。显卡的内存(显存)是有限的,在纹理切换中当显存不够时,就要把一些纹理数据从显存中移除(暂时存放到硬盘或内存中)。
为了优化性能,OpenGL会自动的把最经常使用的纹理保存在显存中作为常驻纹理。判断当前纹理是否是常驻纹理,通过下面的函数调用来测试:
GLboolean glAreTexturesResident(GLsizei n, const GLuint *texture, GLboolean *residences);
第一个参数是纹理对象的个数,第二是纹理对象名称数组,第三个是输出参数,记录着对应的纹理是否是常驻纹理。如果所有的纹理对象都是常驻纹理则glAreTexutresResident返回GL_TRUE。
纹理优先级
默认情况下,OpenGL实现是使用最经常使用(MFFU)来决定纹理是否常驻显存。如果最经常使用的纹理很小,而大纹理不经常使用,那么就会导致小纹理常驻显存,而大纹理却经常被移除。为了避免此问题,我们可以手动设置纹理的优先级。通过下面的函数调用:
void glPrioritizeTextures(GLsizei n, const GLuint *texture, const GLclampf *priorites);
第一个参数是纹理对象的个数,第二个是纹理名称数组,第三个是纹理的优先级参数,范围为[0.0,1.0]。低优先级告诉OpenGL当显存不够时有限考虑移除此低优先级的纹理。示例:
GLclampf prioritize[TEXTURENUM] = {0.0f, 0.0f, 1.0f};
glPrioritizeTextures(TEXTURENUM, textureName, prioritize);
GLboolean isResident[TEXTURENUM];
if (glAreTexturesResident(TEXTURENUM, textureName, isResident))
{
printf("all texture is resident\n");
}
else
{
printf("texture resident is : %d %d %d", isResident[0], isResident[1], isResident[2]);
}
回顾
在纹理这一章,我们学习如何加载纹理glTexImage,设置纹理参数glTexParameter,设置纹理环境glTexEnv,使用mipmap,管理纹理对象,多纹理的使用和切换,纹理过滤器等等。详细的介绍了纹理的各种参数。
代码以及数据纹理和库都在此地址可以获取(上面的示例为tunnel工程):