纹理高级
一般情况下,我们设置纹理的环境为GL_MODULATE模式,在这种情况下,受到光照的几何图形会和纹理的颜色进行结合。正常情况下,OpenGL进行光照计算,并根据标准的光照模型进行单个片段的颜色计算。然后,再把片段的颜色乘以纹理的颜色,等到结合后的颜色。但是这样的话会削弱图形的光照效果。因为经过光照计算过后的片段的颜色值最大值是1.0(即最亮的颜色),任何值乘以小于1.0的值,必定小于其本身(即不可能比原来更亮)。(if y <= 1.0 then x * y <= x. x y是正数)。
没有应用纹理之前:
应用纹理之后光照效果被削弱了:
要解决这个问题,我们可以在纹理映射之后再应用镜面光高亮的效果(通过加而不是乘的方式)。这个技巧成为辅助镜面光颜色。通过设置光照的模型来达到此目的,函数调用如下:
glLightModeli(GL_LIGHT_MODEL_COLOR_CONTROL, GL_SEPARATE_SPECULAR_COLOR);
加了这一行之后的效果如下:
要切回正常状态,指定光照模型为GL_SINGLE_COLOR即可,函数调用如下:
glLightModeli(GL_LIGHT_COLOR_CONTROL, GL_COLOR_SINGLE);
使用没有开启光照,我们可以手动来设置辅助颜色,通过glSecondarycolor函数调用设置辅助颜色和通过glEnable(GL_COLOR_SUM);来开启。手动设置的辅助颜色只有在没有开启光照的情况下有作用。
各向异性过滤
各向异性过滤并非OpenGL核心API的一部分,但其作为扩展被广泛用于提升纹理过滤操作的质量。在先前学习的两个基本的过滤器最邻近过滤(GL_NEAREST)和线性过滤(GL_LINEAR)。OpenGL使用纹理坐标计算得到纹理将映射到几何图形的哪一个片段上。然后通过对该位置周围的纹理元素以GL_NEAREST过滤或GL_LINEAR过滤方式进行采样。
当我们的视角是垂直于该几何图形的时候,这样的方式没有问题。然而当我们的视角与几何图形形成一个斜角的时候,以常规的方式对周边纹理进行采样会丢失一些纹理的信息,它看起来变模糊了。更真实和精确的采样是,沿着平面倾斜的方向,拉长纹理的采样。如下的第二个图:
我们可以把各向异性过滤应用去基本的和mipmap方式的纹理过滤模式上。在使用之前我们需要检查各向异性过滤扩展是否被支持,使用glTools函数里的函数:
if(gltIsExtSupported(“GL_EXT_texture_filter_anisotropic”))
如果扩展是被支持的,我们可以查到支持各向异性过滤的最大值。通过调用glGetFloatv参数为GL_MAX_TEXTURE_MAX_ANISOTROPY_EXT。
GLfloat fLargest;
glGetFloatv(GL_MAX_TEXTURE_MAX_ANISOTROPY_EXT, &fLargest);
值越大各向异性过滤的粒度越大,如果值为1.0就代表普通的纹理过滤。各向异性过滤会带来一定的开销。现代的显卡都已经支持各向异性过滤,而且做了优化。最后我们通过glTexParameterf函数来设置各向异性过滤的最大值,如下:
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAX_ANISOTROPY_EXT, fLargest);
非开启各向异性过滤的通道效果图,可以看到远处的砖块较模糊:
开启各向异性过滤之后的效果图:
纹理压缩
使用纹理的缺陷是纹理需要大量的内存来存储和处理。在早期我们会把纹理压缩成JPG的格式,然后在加载之前(调用glTexImage之前)对其进行解压。这样仅仅是节省了磁盘的空间以及加快了在网络上传输纹理的速度,但并没有减少对显存(加载到显存中还是原格式那么大)。
在OpenGL1.3后,OpenGL原生支持了纹理压缩的特性。在更低的版本中,通过扩展来支持,你可以通过GL_ARB_texture_compression来检查是否支持这个扩展。OpenGL对纹理的压缩不仅仅是加载压缩的纹理,而且在显卡内存中也是保存着压缩的纹理。这可以减少加载纹理时使用的内存以及提升处理纹理的性能(减少了移动纹理和切换纹理的时间,因为要操作的内存空间变小了)。
你可以通过下表的一个常量作为glTexImage函数中internalFormat参数的值,来达到压缩纹理的目的。当纹理无法被压缩时,将使用对应的基本内部格式。
压缩格式 | 基本内部格式 |
GL_COMPRESSED_ALPHA | GL_ALPHA |
GL_COMPRESSED_LUMINANCE | GL_LUMINANCE |
GL_COMPRESSED_LUMINANCE_ALPHA | GL_LUMINANCE_ALPHA |
GL_COMPRESSED_RGB | GL_RGB |
GL_COMPRESSED_RGBA | GL_RGBA |
GL_COMPRESSED_INTENSITY | GL_INTENSITY |
在这种方式下,加载压缩的图像会多耗一点时间,但却提升了处理纹理内存的速度。但你使用这种方式压缩了纹理之后,你可以通过glGetTexLevelParameteriv参数为GL_TEXTURE_COMPRESSED来检查纹理是否压缩成功。
GLint compFlag;
glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_COMPRESSED, &compFlag);
此函数接受的参数如下表:
参数 | 返回值 |
GL_TEXTURE_COMPRESSED | 返回1代表压缩成功,0代表失败 |
GL_TEXTURE_COMPRESSED_IMAGE_SIZE | 返回压缩后纹理的大小(字节为单位) |
GL_TEXTURE_INTERNAL_FORMAT | 使用的压缩格式 |
GL_NUM_COMPRESSED_TEXTURE_FORMATS | 支持的压缩格式的数量 |
GL_COMPRESSED_TEXTURE_FORMATS | 返回一个保存每一个被支持的压缩格式的数组常量 |
GL_TEXTURE_COMPRESSION_HINT | 纹理压缩的提示值 |
我们还可以通过glHint函数来告诉OpenGL我们要用的是最快的压缩算法还是最高质量的压缩算法。通过使用GL_NUM_COMPRESSED_TEXTURE_FORMATS和GL_COMPRESSED_TEXTURE_FORMATS来获得被支持的压缩格式的列表。几乎所有的OpenGl实现都支持GL_EXT_texture_compression_s3tc纹理压缩格式,如果这个扩展被支持那下面表格的所有格式都是支持的(仅适用于2维纹理)
格式 | 描述 |
GL_COMPRESSED_RGB_S3TC_DXT1 | RGB数据被压缩。alpha为1.0 |
GL_COMPRESSED_RGBA_S3TC_DXT1 | RGB数据被压缩。alpha值为1.0或0.0 |
GL_COMPRESSED_RGBA_S3TC_DXT3 | RGB数据被压缩。alpha值用4位存储 |
GL_COMPRESSED_RGBA_S3TC_DXT5 | RGB数据被压缩。alpha为一些8位值的加权平均值 |
加载压缩的纹理
在前面我们已经介绍了,如何压缩纹理数据。然后我们可以通过glGetCompressedTexImage(与glGetTexImage获取未压缩数据一样)来获取被压缩的数据,并把它存到硬盘上。在随后的加载中,直接加载已经压缩过的纹理数据会更快。此技术完全依赖于硬件的实现。
加载已经预先压缩过的纹理数据,可以调用下面的函数:
void glCompressedTexImage1D(GLenum target, GLint level, GLenum internalFormat, GLsizei width, GLint border, GLsizei imageSize, void *data);
void glCompressedTexImage2D(GLenum target, GLint level, GLenum internalFormat, GLsizei width, GLsizei height, GLint border, GLsizei imageSize, void *data);
void glCompressedTexImage3D(GLenum target, GLint level, GLenum internalFormat, GLint width, GLint height, GLint depth, GLint border, GLint imageSize, void *data);
这个方法与glTexImage几乎是一样的,不一样的是其internalFormat必须是压缩的格式。如果实现支持GL_EXT_texture_compression_s3tc扩展,那么其参数值就可以是上面的表格列出的值。当然也有glCompressedTexSubImage函数来更新部分已加载的压缩过的纹理数据,就像glTexSubImage一样。
纹理压缩时非常流行的特性。更小的纹理意味着更快的加载速度,更快地在网上传输,更快地拷贝到显卡中,可以加载更多的纹理。下面做了个简单的实验:
不压缩和压缩后的图片大小的对比,压缩前是196kb左右,压缩后只有32kb了:
GLint flag;
glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_COMPRESSED, &flag);
printf("compress flag : %d\n", flag);
glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_COMPRESSED_IMAGE_SIZE, &flag);
printf("compress size : %d\n", flag);
生成纹理坐标
在前面我们学习过使用纹理坐标来把纹理映射到几何图形上。在球体和平滑的平面上手动指定纹理坐标是简单的。但是遇到了复杂的表面,我们要为其指定纹理坐标就有写困难了。OpenGL提供了自动生成纹理坐标的特性来解决这个问题。
通过glEnable来开启S,T,R和Q的纹理坐标自动生成:
glEnable(GL_TEXTURE_GEN_S);
glEnable(GL_TEXTURE_GEN_T);
glEnable(GL_TEXTURE_GEN_R);
glEnable(GL_TEXTURE_GEN_Q);
当自动生成纹理坐标的功能被开启,那么glTexCoord的函数调用将被忽略。OpenGL为自动为每一个顶点计算纹理坐标。我们可以通过相应的glDisable来关闭纹理坐标的自动生成。
我们可以通过下面的两个函数来设置自动生成纹理坐标的方法:
void glTexGenf(GLenum coord, GLenum pname, GLfloat param);
void glTexGenfv(GLenum coord, GLenum pname, GLfloat *param);
第一个参数指定了纹理坐标轴,可以是GL_S,GL_T,GL_R或GL_Q。第二个参数必须是GL_TEXTURE_SPHERE,GL_OBJECT_PLANE或GL_EYE_PLANE.最后一个参数设置纹理生成的方法或模式。glTexGen也有相应的GLint和GLdouble模式。
下面是TEXGEN示例:
#include "gltools.h" #include <stdio.h> #define ENV 0 #define STRIPES 1 #define TEXTURENUM 2 const char* texFileName[] = {"..\\Environment.tga","..\\stripes.tga"}; static GLuint textureName[TEXTURENUM]; static GLfloat yRot = 0.0f; static GLfloat zPos = -2.0f; static GLint iRenderMode = 3; void ProcessMenu(int value)
{ //投影平面 GLfloat zPlane[] = {0.0f, 0.0f, 1.0f, 0.0f}; //渲染模式 iRenderMode = value; switch(value)
{ case 1: //物体线性 glTexGeni(GL_S, GL_TEXTURE_GEN_MODE, GL_OBJECT_LINEAR);
glTexGeni(GL_T, GL_TEXTURE_GEN_MODE, GL_OBJECT_LINEAR);
glTexGenfv(GL_S, GL_OBJECT_PLANE, zPlane);
glTexGenfv(GL_T, GL_OBJECT_PLANE, zPlane); break; case 2: //视觉线性 glTexGeni(GL_S, GL_TEXTURE_GEN_MODE, GL_EYE_LINEAR);
glTexGeni(GL_T, GL_TEXTURE_GEN_MODE, GL_EYE_LINEAR);
glTexGenfv(GL_S, GL_EYE_PLANE, zPlane);
glTexGenfv(GL_T, GL_EYE_PLANE, zPlane); break; case 3: default: //球体贴图 glTexGeni(GL_S, GL_TEXTURE_GEN_MODE, GL_SPHERE_MAP);
glTexGeni(GL_T, GL_TEXTURE_GEN_MODE, GL_SPHERE_MAP);
}
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)
{ //绑定纹理 glBindTexture(GL_TEXTURE_2D, textureName[i]); //构建mimap 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_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
}
free(pImage);
} if (gltIsExtSupported("GL_EXT_texture_filter_anisotropic"))
{
GLfloat fLargest;
glGetFloatv(GL_MAX_TEXTURE_MAX_ANISOTROPY_EXT, &fLargest);
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAX_ANISOTROPY_EXT, fLargest);
}
glEnable(GL_TEXTURE_2D); //设置为球体贴图 glTexGeni(GL_S, GL_TEXTURE_GEN_MODE, GL_SPHERE_MAP);
glTexGeni(GL_T, GL_TEXTURE_GEN_MODE, GL_SPHERE_MAP); //开启S、T坐标的纹理坐标生成 glEnable(GL_TEXTURE_GEN_S);
glEnable(GL_TEXTURE_GEN_T);
} void ShutdownRC()
{
glDeleteTextures(TEXTURENUM, textureName);
} void RenderScene()
{
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glMatrixMode(GL_PROJECTION); //背景图,使用正交投影 glPushMatrix();
glLoadIdentity();
gluOrtho2D(0.0, 1.0, 0.0, 1.0);
glDepthMask(GL_FALSE);
glMatrixMode(GL_MODELVIEW);
glLoadIdentity();
glBindTexture(GL_TEXTURE_2D, textureName[ENV]); //关闭纹理坐标的生成 glDisable(GL_TEXTURE_GEN_S);
glDisable(GL_TEXTURE_GEN_T);
glBegin(GL_QUADS);
glTexCoord2f(0.0f, 0.0f);
glVertex2f(0.0f, 0.0f);
glTexCoord2f(1.0f, 0.0f);
glVertex2f(1.0f, 0.0f);
glTexCoord2f(1.0f, 1.0f);
glVertex2f(1.0f, 1.0f);
glTexCoord2f(0.0f, 1.0f);
glVertex2f(0.0f, 1.0f);
glEnd(); //还原投影矩阵 glMatrixMode(GL_PROJECTION);
glPopMatrix();
glMatrixMode(GL_MODELVIEW);
glEnable(GL_TEXTURE_GEN_S);
glEnable(GL_TEXTURE_GEN_T);
glDepthMask(GL_TRUE); if (iRenderMode != 3)
{
glBindTexture(GL_TEXTURE_2D, textureName[STRIPES]);
}
glPushMatrix();
glTranslatef(0.0f, 0.0f, zPos);
glRotatef(yRot, 0.0f, 1.0f, 0.0f);
gltDrawTorus(0.35, 0.15, 61, 37);
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("TEXGEN");
glutReshapeFunc(ChangeSize);
glutDisplayFunc(RenderScene);
glutSpecialFunc(SpecialKey);
glutCreateMenu(ProcessMenu);
glutAddMenuEntry("Object Linear", 1);
glutAddMenuEntry("Eye linear", 2);
glutAddMenuEntry("sphere map", 3);
glutAttachMenu(GLUT_RIGHT_BUTTON);
SetupRC();
glutMainLoop();
ShutdownRC(); return 0;
}
物体线性映射
当设置纹理生成的模式为GL_OBJECT_LINEAR的时候,纹理坐标生成使用的公式如下:
coord = P1*X + P2*Y + P3*Z + P4*W
其中X,Y,Z,W是被映射物体的顶点坐标值,P1-P4是平面方程的系数。纹理坐标是从此平面透视投影到几何图形上的。例如,为了从平面Z=0上投影纹理坐标S和T我们可以使用下面的代码:
//投影平面
GLfloat zPlane[] = {0.0f, 0.0f, 1.0f, 0.0f};
...
...
//物体线性
glTexGeni(GL_S, GL_TEXTURE_GEN_MODE, GL_OBJECT_LINEAR);
glTexGeni(GL_T, GL_TEXTURE_GEN_MODE, GL_OBJECT_LINEAR);
glTexGenfv(GL_S, GL_OBJECT_PLANE, zPlane);
glTexGenfv(GL_T, GL_OBJECT_PLANE, zPlane);
注意每个坐标都可以用不同的平面方程来生成纹理坐标,我们这里把S和T坐标的平面方程设置成一样的。在这里使用了物体线性的模式,不管你怎么调整这个圆环,纹理总是固定在几何图元上的。效果如下:
视觉线性映射
当选择视觉线性模式是,纹理坐标的生成方程与物体线性模式是相似的。不同的是现在的X,Y,Z和W值代表着视点的纹理(照相机或眼睛的位置)。平面方程的那些系数也要反转过来。事实上现在所有东西都用视觉坐标来表示了。代码如下:
//投影平面
GLfloat zPlane[] = {0.0f, 0.0f, 1.0f, 0.0f};
...
...
//视觉线性
glTexGeni(GL_S, GL_TEXTURE_GEN_MODE, GL_EYE_LINEAR);
glTexGeni(GL_T, GL_TEXTURE_GEN_MODE, GL_EYE_LINEAR);
glTexGenfv(GL_S, GL_EYE_PLANE, zPlane);
glTexGenfv(GL_T, GL_EYE_PLANE, zPlane);
效果如下,纹理会随着你视角的旋转而改变了:
球体映射
当纹理生成模式设置为GL_SPHERE_MAP的时候,OpenGL生成坐标的方式是物体呈现着当前纹理的倒影。想象一下鱼眼睛的效果。示例中设置球体映射模式的代码如下:
glTexGeni(GL_S, GL_TEXTURE_GEN_MODE, GL_SPHERE_MAP);
glTexGeni(GL_T, GL_TEXTURE_GEN_MODE, GL_SPHERE_MAP);
效果如下:
为了获得更为逼真的效果,使用立方体映射。但球体映射还是有一定用途的,因为它只要求1个纹理开销较小,而立方体映射则要6个纹理,如果你不需要真正的反射,球体映射可以满足你的要求了。