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 ~

相关推荐
byxdaz11 天前
Qt OpenGL 3D 编程入门
qt·opengl
byxdaz12 天前
Qt OpenGL 相机实现
opengl
二进制人工智能1 个月前
【OpenGL学习】(二)OpenGL渲染简单图形
c++·opengl
六bring个六1 个月前
qtcreater配置opencv
c++·qt·opencv·计算机视觉·图形渲染·opengl
爱看书的小沐1 个月前
【小沐学GIS】基于C++绘制二维瓦片地图2D Map(QT、OpenGL、GIS)
c++·qt·gis·opengl·glfw·glut·二维地图
六bring个六1 个月前
图形渲染+事件处理最终版
c++·qt·图形渲染·opengl
星火撩猿1 个月前
OpenGl实战笔记(3)基于qt5.15.2+mingw64+opengl实现光照变化效果
笔记·qt·opengl·光照效果
星火撩猿1 个月前
OpenGl实战笔记(2)基于qt5.15.2+mingw64+opengl实现纹理贴图
笔记·qt·opengl·纹理贴图
程序员爱德华1 个月前
计算机图形学中的深度学习
图形学·opengl
:mnong1 个月前
开放原子大赛石油软件赛道参赛经验分享
c++·qt·hdfs·开放原子·图形渲染·webgl·opengl