OpenGL 纹理漫谈

文章目录
  1. 1. 2D 纹理映射
    1. 1.1. 读取纹理图
    2. 1.2. 生成纹理
    3. 1.3. 调整纹理参数
      1. 1.3.1. 反卷
      2. 1.3.2. 过滤
    4. 1.4. 定义纹理
    5. 1.5. 对多边形进行纹理映射
    6. 1.6. 设置纹理环境
    7. 1.7. 设置纹理坐标
  2. 2. 1D 纹理映射
    1. 2.1. 生成纹理
    2. 2.2. 定义纹理
    3. 2.3. 生成纹理

2D 纹理映射

纹理映射一般是在图形的坐标值已经计算完成时附加在顶点信息上的,而非对纹理进行透视、变换再显示。下面逐步介绍如何在 OpenGL 中开启纹理映射,它的顺序有先后但并不唯一,若是想颠倒一下顺序也不妨试试看会有什么效果🙃️

读取纹理图

纹理图的格式一般为原始 RGB 格式,一个字节(8 bits)代表一种颜色通道,一个像素的颜色用三个字节表示,例如白色记为 #FFFFFF,是不是很熟悉,这种表示方式即 RGB888,由或者称其为 24 位真彩色。除此之外还有一些其他常用的格式:RGBA8888,RGB565,RGBA5551……

纹理图通常是正方形,若图形 API 没有严格限制,也可以为矩形。大多数图形卡及图形 API 为了效率的原因,限制图像分辨率为 2 的幂,例如 256*256,具体要求请参考文档。

Photoshop 导出原始 RGB 图像

这里我们使用 Photoshop 将图片裁剪成 512*512 分辨率,然后将其导出为原始 RGB 格式的图像,只要在存储时选择 Photoshop raw 格式即可(别和单反的那种 RAW 搞混了哦)。导出后的文件记录着自左上角向右下角逐行扫描的 RGB 信息。

查看 RAW 文件

使用十六进制文本查看器查看导出的文件,可以看到共 512*512*3 = 786432 字节,查看前三个字节组成的颜色 #444135,大致是深褐色。

若在原始 RGB 文件头加入图像的高度和宽度信息,则类似于 ppm 格式。除此之外,纹理图还有压缩格式,比较著名的是 S3TC 格式,它被大多数图形卡支持,它提高了内存读取效率,游戏引擎大多会用到这样的技术。

读取原始 RGB 格式的图片是很简单的,一个简单的函数就可以完成这项工作。读取成功后,一个 GLubyte 类型的指针指向存储纹理图的地址,它的大小为 width*height*3unsigned char,可以理解为一个三维数组texImage[width][height][3]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
GLuint raw_texture_load(const char *filename, int width, int height) {
GLubyte *data;
FILE *infile;

// Open texture data
infile = fopen(filename, "rb");
if (NULL == infile) {
fprintf(stderr, "%s open failed.\n", filename);
return 0;
}

// Allocate buffer
data = (GLubyte*)malloc(width * height * 3);

fread(data, (width*height*3), 1, infile);
fclose(infile);

}

除了从文件读取纹理图之外,还有其他方式获得纹理图,例如使用函数生成,或者直接将颜色缓冲区的内容读取出用作纹理。

使用图像纹理图是很普遍的做法,OpenGL 也支持一些其他类型的纹理数据,比如强度、明度等,在制作地形效果时可以利用到。

生成纹理

将原始 RGB 信息保存到内存中只是第一步,接下来需要使用 API 来生成纹理。首先我们询问 API 获得纹理名称,使用得到的纹理名称与某种纹理类型进行绑定,之后调整纹理参数,最后将将纹理数据和纹理的元信息告诉 API,此时纹理便生成完毕,生成的纹理通过纹理名称来访问。

纹理名称的获取与绑定一般代码如下。

1
2
3
4
5
6
7
8
9
10
GLuint raw_texture_load(const char *filename, int width, int height) {
GLuint texture;

// Allocate a texture name
glGenTextures(1, &texture);

// Select our current texture
glBindTexture(GL_TEXTURE_2D, texture);

}

调整纹理参数

OpenGL 提供了一系列的 GLenum 类型的参数,查看源码可以发现它是一些宏定义。这里我们通过调用函数 glTexParameterf(GLenum target, GLenum pname, GLfloat param) 进行纹理参数设置。纹理参数设置函数有多个系列,目前除了 glTextureParameter 系列函数只被 OpenGL 4.5 支持外,其余函数系列都是向后兼容的,具体信息请参考文档

1
2
3
4
5
6
7
8
GLuint raw_texture_load(const char *filename, int width, int height) {

glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_WRAP_S,GL_CLAMP);
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_WRAP_T,GL_CLAMP);
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINEAR);

}

函数的第一个参数为 target,我们根据纹理类型的不同选择调用不同的宏。纹理类型是指颜色信息存在的维度。一般来说我们使用 2D 纹理,某些场合下可以用到 1D 或 3D 纹理,

后面两个参数需要搭配在一起使用,这里我们主要关心两种搭配,反卷与过滤。如上所说,纹理映射的方式并不是简单的透视变换,而是将纹理信息附加在计算好的多边形的顶点信息上。纹理图的坐标范围为 [0, 1],反卷指当纹理坐标超出这个范围如何处理;过滤则涉及到纹理图的映射方法。

反卷

UV 坐标系

出现反卷纹理的情况是由于纹理坐标值不一定在 [0…1] 之间,与纹理数据所不同,纹理空间的坐标是典型的笛卡尔坐标系,原点在左下角。这与 Direct X 所不同(原点在左上角)。纹理坐标值以 (u, v) 表示,有人称其为 UV 坐标系。OpenGL 将纹理的第一二三及齐次坐标定义为 GL_S、GL_T、GL_R、GL_Q。调用 glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_WRAP_S,GL_CLAMP) 就完成单独指定 u 方向的反卷方式为 GL_CLAMP

Wrap 方式

过滤

在纹理映射的过程中,当纹理图的每个像素都恰好映射到屏幕上的每个像素时,纹理图是清晰的。当纹理图不能恰好映射的时候,就会出现插值的问题。先考虑纹理图映射到比自身分辨率大的屏幕上,这时纹理密度小于 1,每个纹元对应多个像素,若是使用简单的映射方法,将会出现块状纹理。OpenGL 提供了放大过滤器,用户可以选择过滤器的技术。同样,若纹理密度大于 1 也会出现多个纹元对应一个像素的问题,类似的技术和方法可以将其解决。

游戏引擎中经常要渲染大量纹理,为了避免因纹理密度导致的问题,提出了多纹理技术(MIP)。当进行纹理映射时,图形 API 选择合适分辨率的纹理,从而提升性能和质量。MIP 技术也可以有效解决倾斜表面的纹理渲染问题,一些常用的纹理滤波技术可以参考百科,游戏玩家对于这些名词是熟悉的。

下面以最临近插值和线性插值做对比,可以观察出细微的不同。

GL_NEAREST

GL_LINAR

定义纹理

1
2
3
4
5
6
7
GLuint raw_texture_load(const char *filename, int width, int height) {

glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB8, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, data);

free(data);
return texture;
}

用户使用 gllTexImage2D(GLenum target, GLint level, GLint internal format, GLsizei width, GLsizei height, GLint border, GLenum format, GLenum type, const GLvoid *pixels) 函数传递纹理数据指针 pixels 和纹理元数据,与之前用户调用 glBindTexture 声明的纹理进行关联。完成这一步之后,接下来就可以通过纹理名称来访问这个纹理。

函数的参数含义如下:

  • target: 纹理类型
  • level: MIP 层次
  • internal format: 有人也称其为 Dest,指这里定义纹理的格式
  • width / height: 纹理图的宽度和高度,OpenGL 的实现可以保证支持到 1024 像素宽和高。1D 纹理图的高度为 1,不需要设置。
  • border: 是否包含边界,1 为是,0 为否。文档指出此值必须为 0 (该参数已废弃,但仍需传值)。
  • format: 有人称 Source,指纹理数据的格式。
  • type: 纹理数据的数据类型,例如 GL_UNSIGNED_BYTE
  • pixels: 纹理数据指针。

简单来讲,这个函数将用户的纹理数据定义为纹理,供 OpenGL 使用。

对多边形进行纹理映射

纹理坐标可以直接设置,也可以通过参数生成,这里先介绍第一种方法。

在绘制多边形之前,使用 glEnable 开启纹理映射,接下来使用 glTexEnvi 设置纹理环境(值得注意的是该调用环境会影响到该语句之后的所有纹理映射),并传入纹理名称到 glBindTexture 完成纹理映射准备。

代码如下,在下面进行介绍。

1
2
3
4
5
6
7
8
9
10
11
12
glEnable(GL_TEXTURE_2D);
glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_DECAL);
glBindTexture(GL_TEXTURE_2D, texName[0]);
glBegin(GL_QUADS);
glNormal3f(0.0, 1.0, 0.0);
glTexCoord2f( 0.0, 0.0); glVertex3fv(plane[0]);
glTexCoord2f( 0.0, 1.0); glVertex3fv(plane[1]);
glTexCoord2f( 1.0, 1.0); glVertex3fv(plane[2]);
glTexCoord2f( 1.0, 0.0); glVertex3fv(plane[3]);
glEnd();
glFlush();
glDisable(GL_TEXTURE_2D);

设置纹理环境

调用 glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE) 设置纹理环境,其中 GL_MODULATE 是纹理环境的默认方法,其余方法有 GL_BLENDGL_DECALGL_REPLACE。用户可以在这项模式中切换。这些参数的含义与纹理数据与物体表面颜色之间混合的方式有关,关于这些技术具体的实现和作用,请参考文档

设置纹理坐标

在绘制前设置法向量,它决定了纹理的方向。绘制顶点时调用 glTexCoord2f(u, v) 即可完成纹理坐标与多边形坐标的绑定,这里 (u, v) 可以超出 [0…1] 的限制,此时纹理的行为由纹理反卷方式决定。在渲染过程中,系统将根据顶点中纹理坐标的信息进行纹理映射。

多个纹理

我们可以生成多个纹理,通过不同的纹理名称重复地对多边形进行映射,这与 MIP 类似但不同。

1D 纹理映射

ChromaDepth

接下来我们使用函数生成 1D 纹理,再使用生成纹理坐标的方法将其渲染到模型上,这里我们使用一种称为 ChromaDepth 的 3D 技术(在地形显示方面也有所应用)。这是一种比较古老的虚拟 3D 技术,只要有对应的眼镜就可以看到类似 3D 物体的效果。要找个类比的话,大概就是红蓝 3D 吧。近处物体渲染为红色,远处物体渲染为蓝色,通过眼镜观看,由于眼镜的衍射率不同,红色大于蓝色,人眼会认为衍射率大的红色比蓝色近,从而产生 3D 视觉效果。

我们只要定义 1D 的纹理,再通过 glTexGeni 函数将纹理坐标自动生成出来就可以了。

生成纹理

使用 RGB 色彩空间创建从红到蓝的渐变并不方便,我们使用 HSV 色彩空间,喜欢数码绘图的朋友一定会很熟悉 HSV 拾色器。设置饱和度(S)和亮度(V)都为 1,将色度(H)由 0 度(红色)变换到 240 度(蓝色)。将这一变化序列转化为 RGB 存储并生成 1D 纹理。这里不再介绍如何 RGB 与 HSV 的转换。创建纹理数据的一般代码如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void makeRamp(void) {
int i;
float h, s, v, r, g, b;

// 0 to 240 ramp, total 256 steps
for (i = 0; i < 256; i++) {
h = (float)i * 240.0 / 255.0;
s = 1.0;
v = 1.0;
convertHSV2RGB(h, s, v, &r, &g, &b);
ramp[i][0] = r;
ramp[i][1] = g;
ramp[i][2] = b;
}
}

定义纹理

这里我们得到了一个长度为 256 的 1D 纹理数据。与 2D 纹理定义类似,接下来开启 1D 纹理映射,调整纹理参数,定义一个 1D 纹理。在开启 1D 纹理映射之后,我们调用 glEnable(GL_TEXTURE_GEN_S) 开启纹理 S 方向上的坐标生成。

1
2
3
4
5
6
glEnable(GL_TEXTURE_1D);
glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE);
glTexParameterf(GL_TEXTURE_1D, GL_TEXTURE_WRAP_S, GL_CLAMP);
glTexParameterf(GL_TEXTURE_1D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameterf(GL_TEXTURE_1D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexImage1D(GL_TEXTURE_1D, 0, GL_RGB8, 256, 0, GL_RGB, GL_FLOAT, ramp);

生成纹理

我们使用眼空间来生成纹理,函数调用的顺序不唯一,但要确保 glTexGenfvgluLookAt 之前调用,代码如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
D1 = 5.0;
D2 = 15.0;
texParms[0] = texParms[1] = 0.0;
texParms[2] = -1.0 / (D2-D1);
texParms[3] = -D1/(D2-D1);

glEnable(GL_TEXTURE_1D);
glTexGenfv(GL_S, GL_EYE_PLANE, texParms);
glBindTexture(GL_TEXTURE_1D, texName);
glTexGeni(GL_S, GL_TEXTURE_GEN_MODE, GL_EYE_LINEAR);

// eye point center of view up
gluLookAt(camX, camY, camZ, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0);

glutSolidTeapot(5.0);
glDisable(GL_TEXTURE_1D);

前 5 行代码设置了四元数组,将数组元素分别记作 A B C D。

在眼坐标空间中生产纹理坐标时,系统在纹理坐标 S 方向进行这样的计算:s = A*x + B*y + C*z + D,OpenGL 的眼坐标空间向场景远处的方向为 +z 方向,将 A 和 B 设置为 0,即 x、y 方向不影响纹理坐标,只有 z 方向影响。观察 C 和 D 的计算方式,可知方程 s 是一个关于 z 的一元一次方程,斜率小于 0,s 值随着 z 的减小而增大。有点晕……冷静一下,OpenGL 的眼坐标系为左手坐标系,(0, 0, -1) 指向目标点,于是随着 z 值增大,茶壶的颜色也就越发蓝。详细资料请参考下面最后一个引用链接。

相关资料: