OpenGL ES 2.0 笔记 #5:Texture

简而言之,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 ~

相关推荐
扶尔魔ocy2 天前
【qt openGL入门笔记】windows编译MinGW版本的osgearth(2.10.1)
qt·opengl·osg
扶尔魔ocy3 天前
【QT OpenGL】知识点编目及传送门
qt·opengl·osg
扶尔魔ocy16 天前
【qt openGL入门笔记】OSG实现飞机模型在网格图上飞行并进行态势标绘(四)
qt·opengl·osg
:mnong17 天前
Web 三维可视化技术对比:ParaView Web vs 本项目 wasm + Three.js
前端·webgl·opengl·opencl·科学可视化
扶尔魔ocy18 天前
【qt openGL入门笔记】OSG实现显示飞机模型飞行的点迹(二)
qt·opengl·osg
扶尔魔ocy18 天前
【qt openGL入门笔记】OSG实现飞机模型在网格图上飞行(三)
opengl·osg
扶尔魔ocy19 天前
【qt openGL入门笔记】OSG实现飞机模型在网格图上飞行
qt·opengl·osg
扶尔魔ocy23 天前
【qt openGL入门笔记】FPS相机
qt·opengl
扶尔魔ocy23 天前
qt openGL入门笔记】FPS相机和轨道相机
qt·opengl
扶尔魔ocy25 天前
【qt openGL入门笔记】基础光照
qt·opengl