纹理高级

一般情况下,我们设置纹理的环境为GL_MODULATE模式,在这种情况下,受到光照的几何图形会和纹理的颜色进行结合。正常情况下,OpenGL进行光照计算,并根据标准的光照模型进行单个片段的颜色计算。然后,再把片段的颜色乘以纹理的颜色,等到结合后的颜色。但是这样的话会削弱图形的光照效果。因为经过光照计算过后的片段的颜色值最大值是1.0(即最亮的颜色),任何值乘以小于1.0的值,必定小于其本身(即不可能比原来更亮)。(if y <= 1.0 then x * y <= x. x y是正数)。

没有应用纹理之前:

image

应用纹理之后光照效果被削弱了:

image

要解决这个问题,我们可以在纹理映射之后再应用镜面光高亮的效果(通过加而不是乘的方式)。这个技巧成为辅助镜面光颜色。通过设置光照的模型来达到此目的,函数调用如下:

glLightModeli(GL_LIGHT_MODEL_COLOR_CONTROL, GL_SEPARATE_SPECULAR_COLOR);

加了这一行之后的效果如下:

image

要切回正常状态,指定光照模型为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过滤方式进行采样。

当我们的视角是垂直于该几何图形的时候,这样的方式没有问题。然而当我们的视角与几何图形形成一个斜角的时候,以常规的方式对周边纹理进行采样会丢失一些纹理的信息,它看起来变模糊了。更真实和精确的采样是,沿着平面倾斜的方向,拉长纹理的采样。如下的第二个图:

image

我们可以把各向异性过滤应用去基本的和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);

非开启各向异性过滤的通道效果图,可以看到远处的砖块较模糊:

image

开启各向异性过滤之后的效果图:

image

纹理压缩

使用纹理的缺陷是纹理需要大量的内存来存储和处理。在早期我们会把纹理压缩成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一样。

纹理压缩时非常流行的特性。更小的纹理意味着更快的加载速度,更快地在网上传输,更快地拷贝到显卡中,可以加载更多的纹理。下面做了个简单的实验:

image

不压缩和压缩后的图片大小的对比,压缩前是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);

image

生成纹理坐标

在前面我们学习过使用纹理坐标来把纹理映射到几何图形上。在球体和平滑的平面上手动指定纹理坐标是简单的。但是遇到了复杂的表面,我们要为其指定纹理坐标就有写困难了。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坐标的平面方程设置成一样的。在这里使用了物体线性的模式,不管你怎么调整这个圆环,纹理总是固定在几何图元上的。效果如下:

image

视觉线性映射

当选择视觉线性模式是,纹理坐标的生成方程与物体线性模式是相似的。不同的是现在的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);

效果如下,纹理会随着你视角的旋转而改变了:

image

球体映射

当纹理生成模式设置为GL_SPHERE_MAP的时候,OpenGL生成坐标的方式是物体呈现着当前纹理的倒影。想象一下鱼眼睛的效果。示例中设置球体映射模式的代码如下:

glTexGeni(GL_S, GL_TEXTURE_GEN_MODE, GL_SPHERE_MAP);

glTexGeni(GL_T, GL_TEXTURE_GEN_MODE, GL_SPHERE_MAP);

效果如下:

image

为了获得更为逼真的效果,使用立方体映射。但球体映射还是有一定用途的,因为它只要求1个纹理开销较小,而立方体映射则要6个纹理,如果你不需要真正的反射,球体映射可以满足你的要求了。

posted @ 2017-07-12 20:56  匿名者2  阅读(544)  评论(0编辑  收藏  举报