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 工程):

https://github.com/sweetdark/openglex

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值