简而言之,Texture(纹理)是一幅图片,或者说,一个 2D 颜色阵列。Texture 坐标是一个 2D 坐标 (s, t),s/t 取值区间 [0, 1]。给定一个 texture 对象,拿一个 texture 坐标 (s, t) 就能找到一个对应的颜色值:
Texture 坐标原点在左下角,而通常图片像素坐标的原点在左上角,这点要了解,后面程序加载图片用作 texture 时,需要对纵坐标(Y)方向进行调整(翻转)。
作为建模阶段的产出,3D 模型的每个顶点的属性除了位置/坐标,也包含 texture 坐标。和坐标/颜色等属性一样,Rasterizer 也是通过插值运算确定每个点的 texture 坐标。
这一课的例程,绘制一个正方形并给它贴上 texture,如下图:
Texture attribute array 中除了顶点坐标、颜色,加进了 texture 坐标。共 4 个顶点。:
C
const GLfloat vertices[] = {
// #0
0.5f, 0.5f, // Position: top right
1.0f, 0.0f, 0.0f, // Color: RED
1.0f, 1.0f, // Texture coords
// #1
0.5f, -0.5f, // Bottom right
0.0f, 1.0f, 0.0f, // GREEN
1.0f, 0.0f, //
// #2
-0.5f, -0.5f, // Bottom left
0.0f, 0.0f, 1.0f, // BLUE
0.0f, 0.0f, //
// #3
-0.5f, 0.5f, // Top left
1.0f, 1.0f, 0.0f, // YELLOW
0.0f, 1.0f, //
};
const GLushort indices[] = {
0, 1, 3, 2, // Triagnle strip: {0, 1, 3}, {3, 1, 2}
};
Vertex shader 中增加了一个 attribute
类型的变量用来接收 texture 坐标,同时增加一个 varying
将它传递给 fragment shader。注意 texture 坐标是 2D 的:
C++
attribute vec4 a_pos;
attribute mediump vec4 a_color;
attribute mediump vec2 a_tex;
varying mediump vec4 v_color;
varying mediump vec2 v_tex;
void main()
{
gl_Position = a_pos;
v_color = a_color;
v_tex = a_tex;
}
程序里需绑定 texture attribute
变量:
C
GLint attr_tex = glGetAttribLocation(prog, "a_tex");
const GLsizei stride = 7 * sizeof(GLfloat);
glEnableVertexAttribArray(attr_tex);
glVertexAttribPointer(attr_tex, 2, GL_FLOAT, GL_FALSE, stride, (void *)(5 * sizeof(GLfloat)));
注意到 texture 坐标数据偏移量是 5 个 float
(位置坐标 x/y,颜色坐标 r/g/b)。
现在创建 texture object。Texture object 包含实际的 texture 数据:
C
// Use tightly packed data
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
GLuint tex;
glGenTextures(1, &tex);
glBindTexture(GL_TEXTURE_2D, tex);
GLint w = ...
GLint h = ...
const void *pixels = ...
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, w, h, 0, GL_RGB, GL_UNSIGNED_BYTE, pixels);
// Set the filtering mode
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
这里假定 texture 图片数据是按 RGB 顺序排列的字节数组,即每个像素 3 字节。此外,每行之间紧密排列,没有间隔。
OpenGL 有一个状态数据 GL_UNPACK_ALIGNMENT
,在读取 texture 图片数据时,这个状态的值确定每行数据的起始偏移量,默认是 4,这就要求每行的第一个像素数据的偏移量是 4 的倍数。我们加载的图片,每行之间没有间隔,因此需要将 GL_UNPACK_ALIGNMENT
修改为 1(代码第 2 行)。
代码第 5 行,调用 glGenTextures()
创建一个 texture object。在为 texture object 复制数据之前,要先将它绑定到 GL_TEXTURE_2D
目标上(第 7 行)。"目标"是 OpenGL 定义的一个术语,其含义将在下一课中揭晓。
第 12 行调用 glTexImage2D()
函数,向 OpenGL 指定数据格式,并复制数据。注意该函数第 1 个参数是 GL_TEXTURE_2D
目标,而不是 texture object 本身。这部分地解释了为什么在调用这个函数之前要先进行绑定。
第 15、16 行,设置 texture filter,这一点在后面用一个小节详细说明。
Fragment shader :
C++
precision mediump float;
varying vec4 v_color;
varying vec2 v_tex;
uniform sampler2D s_tex;
void main()
{
gl_FragColor = texture2D(s_tex, v_tex);
}
这里除了定义 2 个 varying
分别接收颜色和 texture 坐标,还有一个 sampler2D
类型的 uniform
,它就是程序里创建并绑定的 texture object。
在 main()
函数中,调用内建函数 texture2D()
使用 texture 坐标从 texture object 取得 texture 颜色值,并将其赋予 fragment,成为屏幕上最终显示的颜色。
程序运行效果如本文前面的图片所示。完整代码在 gitlab.com/sihokk/lear...
注意到上面的 fragment shader 中,我们没有使用 varying
颜色变量 v_color
。可以将这个颜色值和 texture 颜色进行叠加:
C++
// gl_FragColor = texture2D(s_tex, v_tex);
gl_FragColor = texture2D(s_tex, v_tex) * v_color;
这里使用的是乘运算。运行效果如下:

GLSL 的 vector 乘法运算,是逐分量进行的。举例来说,对于两个颜色值 c1 = (r1, g1, b1, a1) 和 c2 = (r2, g2, b2, a2),c = c1 * c2;
的结果相当于
C++
vec4 c;
c.r = c1.r * c2.r;
c.g = c1.g * c2.g;
c.b = c1.b * c2.b;
c.a = c1.a * c2.a;
像素/颜色的乘运算,是 Porter-Duff 运算之一种。例如,在 Android API 文档中对乘运算的定义是:
表格最后一列内容 [...] 中 2 项数值分别表示 alpha 分量和颜色分量(r/g/b)的计算方法;Sx 和 Dx 分别表示参与运算的 2 个颜色的某一分量(x = r/g/b/a)。这与 GLSL vector 乘运算是一致的。
Mipmap 及 Filter
看看 glTexImage2D()
的完整原型:
C
void glTexImage2D(GLenum target, GLint level, GLint internalformat, GLsizei width, GLsizei height, GLint border, GLenum format, GLenum type, const void *pixels)
其中第 2 个参数 level
表示 mipmap 级别。Mipmap 是 OpenGL 为加快 texture 颜色查询而提供的设施。要理解 mipmap 必须先理解 filter。
如前所述,texture object 本质上是一幅 2D 像素阵列,即:其像素数量是有限的。当用一个 texture 坐标 (s, t) 去获取一个像素的颜色时,最简单的做法是直接采用坐标所在像素的颜色,但这也带来一个问题,当 texture 与 3D 模型大小不同时,texture 图像将被放大或者缩小,这将导致最终呈现出来的图像存在视觉瑕疵,比如下面这样:

为提高 texture 渲染质量,OpenGL 提供了多种获取 texture 像素的算法,例如:取 (s, t) 邻近 4 个像素的颜色值并进行插值运算。这些算法就称为 filter,例如:
GL_NEAREST
- 此即前述最简算法,取 (s, t) 所在像素的值GL_LINEAR
- 取相邻像素进行线性插值
OpenGL 要求为 texture 图像被放大和缩小这两种情况分别指定 filter,例如在前面的例程代码中:
C
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
其中,参数值 GL_TEXTURE_MIN_FILTER
表示缩小,GL_TEXTURE_MAG_FILTER
表示放大。
回到 mipmap,它是这样的。从原始图像开始,将其宽、高缩小一半,得到一幅新的图像;反复进行这样的缩小,直到最终得到 1x1 大小的图像。这样就形成了一条链,链的第一个环节是原始图像,记为级别 0,后面的环节按顺序记为级别 1, 2, ...。
可见,Mipmap 只对 texture 图像缩小的情况有用。它是用预先计算 + 存储空间换取运行时效率的提高。
在 mipmap 机制下,在 texture 缩小的情况下定义了另外几种 filter。连同之前已列出的 2 种,一共有:
GL_NEAREST
- 从 mipmap 级别 0 取一个像素GL_LINEAR
- 从级别 0 取相邻像素进行线性插值GL_NEAREST_MIPMAP_NEAREST
- 从最接近的 mipmap 级别取一个像素GL_NEAREST_MIPMAP_LINEAR
- 从最近 2 个 mipmap 级别各取一个像素并进行插值GL_LINEAR_MIPMAP_NEAREST
- 从最近 mipmap 级别取相邻像素进行插值GL_LINEAR_MIPMAP_LINEAR
- 从最近 2 个 mipmap 级别各取相邻像素进行插值,然后对结果再次进行插值
程序可以手工计算 mipmap 并调用函数 glTexImage2D()
复制给 texture object,也可以用 OpenGL 提供的命令自动生成 mipmap:
C
void glGenerateMipmap(GLenum target)
SDL2_image 加载图像
原课程是用 stb_image.h
来加载 texture 图像。因为我已经使用了 SDL2 作为窗口管理框架,便顺手用了 SDL2 的附属库 SDL2_image 来进行图像加载。SDL2_image 支持常见图片格式。
在 Debian/Ubuntu 系统中,可从包管理直接安装:
sh
sudo apt-get install libsdl2-image-dev
和 SDL2 一样,SDL2_image 需要进行初始化和销毁。SDL2_image 加载图片后得到的是一个 SDL_Surface
对象。用 SDL2_image 加载图片的过程如下:
C
#include <SDL2/SDL_image.h>
...
SDL_Surface *surf;
{
const int flags = IMG_INIT_JPG | IMG_INIT_PNG;
if (flags != IMG_Init(flags))
{
...
}
surf = IMG_Load(path);
if (NULL == surf)
{
...
}
IMG_Quit();
}
SDL_Surface
本身定义在 SDL2 中。按照前面例程的预设,要求图片的像素格式是 RGB24。恰好这次用的图片正是这一格式,否则需要进行格式转换。其实用 SDL2 进行像素格式转换很简单,只需要调用一个函数,但目前从略,以后有需要再说。目前只是检查一下像素格式:
C
if (SDL_PIXELFORMAT_RGB24 != surf->format->format)
{
// TODO: Convert surface
}
开头已经说过,图片像素坐标的原点在左上角,而 texture 坐标原点则在左下角,因此,需要对图片进行垂直翻转。具体算法为:依次交换第 1 行和倒数第 1 行,交换第 2 行和倒数第 2 行,依此类推。详见 git 代码。
接下来便可将 SDL_Surface
数据复制给 texture object 了。注意 SDL_Surface
用完要释放资源:
C
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, surf->w, surf->h, 0, GL_RGB, GL_UNSIGNED_BYTE, surf->pixels);
...
SDL_FreeSurface(surf);
~ La fin ~