纹理

484 阅读20分钟

资料援引

图形学:纹理概述

Learn OpenGL 纹理

OpenGL ES 2.0 Reference Pages

为什么需要纹理?

真实世界中的物体颜色错综复杂,如果想要通过增加顶点数量来指定足够多的颜色,以增加渲染的真实性,那么将会产生很多额外开销,因为每个模型都会需求更多的顶点,每个顶点又需求一个颜色属性。

“如果手动设置顶点颜色的过程可以自动化就好了”。在这样的诉求下,纹理应运而生。纹理是一个2D图片(甚至也有1D3D的纹理),它可以用来添加物体的细节,这样就可以让物体更加精细的同时不用指定额外的顶点。

除了图像以外,纹理也可以被用来储存大量的数据,这些数据可以发送到着色器上。

通过纹理,就可以直接将模型上的顶点和纹理中的坐标映射起来,即UV Mapping,这也就是为什么会经常把纹理叫做纹理贴图的原因,因为纹理的作用就是根据映射,“粘贴”到模型表面。

纹理坐标

为了能够把纹理映射到物体上,需要指定物体的每个顶点各自对应纹理的哪个部分,即每个顶点关联一个纹理坐标,用来表示该从纹理图像的哪个部分采集片段颜色,以便于在其它片段上进行片段插值。

纹理坐标在xy轴上,范围为0~12D纹理图像)。使用纹理坐标获取纹理颜色叫做采样。纹理坐标起始于(0, 0),也就是纹理图片的左下角,终始于(1, 1),即纹理图片的右上角。下面的图片展示了如何把纹理坐标映射到三角形上:

向顶点着色器传递如图三个纹理坐标,之后传入片段着色器中,对每个片段进行纹理坐标的插值。纹理坐标如下:

float texCoords[] = {
    0.0f, 0.0f, // 左下角
    1.0f, 0.0f, // 右下角
    0.5f, 1.0f // 上中
};

纹理采样可以采用几种不同的插值方式,因此需要告诉OpenGL该怎样对纹理采样

纹理环绕

纹理坐标的范围通常是从(0, 0)(1, 1)。把纹理坐标设置在范围之外时,OpenGL提供了以下环绕方式:

用图片来直观感受一下:

前面提到的每个选项都可以使用glTexParameter*函数对单独的一个坐标轴设置(str它们和xyz是等价的):

// GL_TEXTURE_WRAP_S:S方向上的贴图模式
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_MIRRORED_REPEAT);
// GL_TEXTURE_WRAP_T:T方向上的贴图模式
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_MIRRORED_REPEAT);

纹理采样/过滤 (Texture Sampling/Filtering)

定义:纹理采样(或称过滤)决定了在找到对应的纹素(Texel,即纹理像素)位置后,如何确定最终输出的颜色值。

功能:它是一种插值或过滤算法,用于解决屏幕像素与纹理像素之间一对一映射不准确的问题(例如纹理放大或缩小时的模糊或锯齿问题)。

常见算法

  • 最近邻采样(Nearest Neighbor) :直接取最近的像素颜色(速度快,有锯齿)。
  • 双线性插值(Bilinear Filtering) :对周围 4 个像素进行插值(较平滑,计算量稍大)。
  • 三线性插值(Trilinear Filtering) :结合 Mipmap 技术进行平滑处理。
  • 各向异性过滤(Anisotropic Filtering) :提供最高质量的过滤(尤其是在倾斜角度下)。

像素 (Pixel)

像素是屏幕空间(Screen Space) 的概念。在光栅化过程中,三角形被转换成一系列的像素,并写入 FrameBuffer。你的代码中帧缓冲尺寸是 800x600,最终保存的 .ppm 图片就是由这些像素构成的。

纹素 (Texel)

纹素是纹理空间(Texture Space) 的概念。它是原始图像数据的基本单位。GPU 的任务就是将模型顶点的 UV 坐标(浮点值,不依赖分辨率)映射到纹理图像上的特定纹素位置(整数坐标),然后进行纹理采样(如最近邻或双线性插值),最终获取颜色并应用到对应的像素上。

纹理坐标通常用归一化浮点值表示,范围在 [0,1],与纹理分辨率无关。但坐标并不局限于[0,1],可以是任意浮点值,超出范围时由采样模式决定如何处理。

因此OpenGL需要处理两个问题:

  1. 纹理坐标超出[0,1]范围时的寻址方式(纹理环绕);
  2. 纹素到像素映射时的过滤方式。

OpenGL有多个纹理过滤方式,这里只讨论最重要的两种:GL_NEAREST(最邻近采样)和GL_LINEAR(双线性过滤)。

  • GL_NEAREST(邻近过滤,Nearest Neighbor Filtering):OpenGL默认的纹理过滤方式,具体行为是选择中心点最接近纹理坐标的那个像素。下图中有四个像素,加号代表纹理坐标。左上角那个纹理像素的中心距离纹理坐标最近,所以它会被选择为样本颜色:

  • 优点

    • 性能极高:计算量最小,因为它只需要查找一个像素的颜色值,无需插值计算,处理速度最快。
    • 保留 像素 边缘:保留了原始纹理的锐利边缘,不会引入平滑模糊效果。这对于像素艺术风格(8-bit look)的游戏或需要保持图像清晰度的特定应用非常理想。
    • 无颜色混合:由于不进行插值,不会出现跨颜色边界的意外颜色混合。 
  • 缺点

    • 图像质量 :在纹理放大时,图像会出现明显的块状(blocky)或马赛克效果(pixelization)。
    • 明显的锯齿(走样) :在纹理缩小或旋转时,容易产生闪烁(shimmering)和阶梯状的边缘(stair-case effect)。

最近邻采样:阶梯状边缘 (Stair-case Effect),右侧是理想中平滑的线条作为对比:

最近邻采样:阶梯状边缘 (Stair-case Effect),右侧是理想中平滑的线条作为对比。

最近邻采样:闪烁 (Shimmering),展示一个高频纹理(例如棋盘格)在缩小并轻微移动时,如何由于采样的不确定性而看起来不稳定和抖动:

  • GL_LINEAR([双]线性过滤,[Bi]linear Filtering):基于纹理坐标附近的纹理像素,计算出一个插值,近似出这些纹理像素之间的颜色。一个纹理像素的中心距离纹理坐标越近,那么这个纹理像素的颜色在最终的样本颜色中所占权重越大:

当一个物体很大但纹理分辨率很低的时候两种不同的过滤方式得到的结果如图所示:

  • GL_NEAREST产生了颗粒状的图案,能够清晰看到组成纹理的像素;
  • GL_LINEAR产生的图案更平滑,很难看出单个的纹理像素,输出更真实;
  • 两者并无优劣之分,某些情况下更需要8-bit风格(像素游戏),所以会用前者。

当进行放大(Magnify)缩小(Minify)操作的时候,可以通过glTexParameter*函数设置纹理过滤的选项:

// GL_TEXTURE_MIN_FILTER 缩小过滤
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
// GL_TEXTURE_MAG_FILTER 放大过滤
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
  • 使用最近邻采样,画面会显得 块状、 像素化:

  • 使用双线性采样,画面会显得 模糊、平滑:

高分辨率屏幕像素与低分辨率纹素

假设一种场景:

  • 屏幕 分辨率:1920 × 1080(全高清显示器,一共约 200 万个像素)
  • 纹理 分辨率:256 × 256(只有约 6.5 万个纹素 texel)

当把这张 256×256 的纹理 拉伸到覆盖整个 1920×1080 的屏幕区域时:

  • 每个纹素(texel)需要对应多个屏幕像素(pixel)。

  • 具体比例:

    • 水平方向:1920 ÷ 256 ≈ 7.5
    • 垂直方向:1080 ÷ 256 ≈ 4.2
  • 也就是说,一个纹素大约要填充 7.5 × 4.2 ≈ 31 个屏幕 像素

image.png

因为屏幕像素比纹素多很多,显卡会用 插值(bilinear/trilinear filtering)重复(nearest neighbor) 来决定这些额外像素的颜色。

低分辨率屏幕像素和高分辨率纹素

现在想象一种物体很小但纹理分辨率很高的情况。假设有一个包含着上千物体的房间,每个物体上都有纹理。有些物体很远,但其纹理会拥有与近处物体同样高的分辨率。物体越小,它的单位像素所包含的纹素数量就更多,此时采样频率低于信息频率(像素只能取其中一个纹素来代表它包含的一整块纹素区的颜色),从而产生走样(失去大部分有效信息,产生不真实的感觉)。

对小物体使用高分辨率纹理是种浪费内存的行为。

以图片距离,正常图片:

图片缩小后产生摩尔纹:

图片缩小后产生摩尔纹现象,是采样频率跟不上信息频率的结果。如下图所示,显示屏通过屏幕像素对纹理进行采样,当图片离屏幕很近(放大)时,1个像素只能采样到1个纹素;当图片离屏幕很远(缩小)时,1个像素几乎包含了12个纹素。

OpenGL使用一种叫做多级渐远纹理(Mipmap)的概念来解决这个问题,简单来说就是一系列的纹理图像,后一个纹理图像是前一个的二分之一。多级渐远纹理背后的理念很简单:距观察者的距离超过一定的阈值,OpenGL会使用不同的多级渐远纹理,以实现一个像素采样尽可能少的纹素。

  • mipmap层级越高,纹素就越少:

  • 在同一个视口(屏幕)查看不同的mipmap层级:

使用多级渐远纹理有两个好处:

  • 由于距离远,解析度不高也不会被用户注意到;
  • 为一张纹理生成mipmap,会增加1/3的显存,但是减轻走样的同时也减少了计算量。

手工为每个纹理图像创建一系列多级渐远纹理很麻烦,幸好OpenGL有一个glGenerateMipmaps函数,在创建完一个纹理后调用它OpenGL就会承担接下来的所有工作了。

在渲染中切换多级渐远纹理级别(Level)时,OpenGL在两个不同级别的多级渐远纹理层之间会产生不真实的生硬边界。可以通过在两个不同多级渐远纹理级别之间使用NEARESTLINEAR过滤,来削弱这种不真实的边界。多级渐远纹理级别之间的过滤方式有四种:

上面四种方式可以总结为:GL_采样方式_MIPMAP_匹配像素方式

同样的,可以使用glTexParameteri来设置过滤方式:

// 缩小过滤,线性插值采样,线性插值匹配像素。
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
// 放大过滤,线性过滤。
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

一个常见的错误是,将放大过滤多级渐远纹理级别之间的过滤方式配合使用。这样没有任何效果,因为多级渐远纹理主要是在纹理被缩小的情况下使用的,纹理放大不会使用多级渐远纹理。为放大过滤设置多级渐远纹理的选项会产生一个GL_INVALID_ENUM错误代码。

glTexParameter

纹理设置函数,设置纹理环绕方式或纹理过滤方式时都会用到。

void glTexParameterf(GLenum target,GLenum pname,GLfloat param);

void glTexParameteri(GLenum target,GLenum pname,GLint param);

void glTexParameterfv(GLenum target,GLenum pname,const GLfloat * params);

void glTexParameteriv(GLenum target,GLenum pname,const GLint * params);
  • target:指定了纹理目标;

    • GL_TEXTURE_2D:2D纹理。
    • GLES11Ext.GL_TEXTURE_EXTERNAL_OES:Android特有的OES纹理,预览相机或者视频时使用。
  • pname:指定设置的选项。可以选择纹理环绕(WRAP)或者纹理过滤(FILTER)
  • param:指定设置选项的值。环绕的具体方式/过滤的具体方式。

    • 如果环绕方式选择GL_CLAMP_TO_BORDER,那么还需要指定边缘颜色。这就需要使用后两种glTexParameter函数,用GL_TEXTURE_BORDER_COLOR作为pname的值,并且传递一个float数组作为边缘颜色的值:
float borderColor[] = { 1.0f, 1.0f, 0.0f, 1.0f };
glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, borderColor);

加载与创建纹理

将纹理加载到应用中才能使用,但是纹理图像可能被储存为各种各样的格式,每种都有自己的数据结构和排列。所以如何才能把这些图像加载到应用中呢?

  1. 选一个文件格式,比如.PNG,然后自实现一个图像加载器,把图像转化为字节序列。但是如果支持的文件格式很多就不得不为每种格式写对应的加载器,工作量太大。
  1. 幸而可以使用一个支持多种流行格式的图像加载库来解决这个问题。比如stb_image.h库。

stb_image.h

  1. 下载stb_image.h头文件,不改变名字加入工程,并另创建一个新的C++文件,输入以下代码:
// 通过STB_IMAGE_IMPLEMENTATION,预处理器会修改头文件,让其只包含相关的函数定义源码,
// 等于是将 stb_image.h 变为一个 .cpp 文件了。
#define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h"

2. 用stb_image.hstbi_load函数来加载一张木箱图片:

int width, height, nrChannels;
unsigned char *data = 
          stbi_load("container.jpg", &width, &height, &nrChannels, 0);

stbi_load 通过传入的第一个参数(图片路径/名称)解析得到宽、高、通道数,最后一个参数是你希望的通道数,彩色图像通常存储为RedGreenBlue,因此这些图像有3个通道。数据的总大小是width*height*channels

函数返回加载出来的图像数据,并保存在unsigned char*缓冲区中;如果无法在文件中读取,该函数将返回0。【详情参考

glGenTextures

  1. 和之前生成的OpenGL对象一样,纹理也是使用ID引用的:
unsigned int texture;
glGenTextures(1, &texture);

函数原型:

void glGenTextures(GLsizei n, GLuint *textures);
  • n:用来生成纹理的数量;
  • textures:存储纹理索引的第一个元素指针。

glBindTexture

  1. 绑定纹理,以便和之后任何的纹理指令关联:
glBindTexture(GL_TEXTURE_2D, texture);

函数原型:

void glBindTexture(GLenum target, GLuint texture);
  • target:指定待绑定的纹理的类型。必须是GL_TEXTURE_2DGL_TEXTURE_CUBE_MAP
  • texture:指定要绑定的纹理的名称。

glTexImage2D / glGenerateMipmap

  1. 使用前面载入的图片数据生成一个纹理:
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, data);
glGenerateMipmap(GL_TEXTURE_2D);

glTexImage2D函数原型:

void glTexImage2D(GLenum target, 
                  GLint level, 
                  GLint internalformat, 
                  GLsizei width, 
                  GLsizei height, 
                  GLint border, 
                  GLenum format, 
                  GLenum type, 
                  const GLvoid * data);
  • target:指定当前绑定的纹理单元的纹理类型(设置为GL_TEXTURE_2D那么任何绑定到GL_TEXTURE_1DGL_TEXTURE_3D的纹理不会受到影响)。
  • level:纹理指定多级渐远纹理的级别。0级是基本图像级别,此时可手动设置每个多级渐远纹理的级别。
  • internalformat:指定把纹理储存为何种格式。例子中的图像只有RGB值,因此把纹理储存为RGB值。
  • width/height:宽高。使用图片宽高对应的变量即可。
  • border:指定边框的宽度。(历史遗留的问题导致必须是0)。
  • format:指定纹理数据来源的格式。必须与internalformat匹配。
  • type:指定纹理数据来源的数据类型。
  • data:该指针指向内存中图像数据。

当调用glTexImage2D之后,当前绑定的纹理对象就会被附加上纹理图像。

如果要使用多级渐远纹理,还必须手动设置所有不同的层级图像(不断递增第二个参数)。

或者直接在生成纹理之后调用glGenerateMipmap。这会为当前绑定的纹理自动生成所有需要的多级渐远纹理。

glGenerateMipmap函数原型:

void glGenerateMipmap(GLenum target);
  • target:指定当前绑定的纹理单元的纹理类型,生成对应的Mipmap

stbi_image_free

生成了纹理和相应的多级渐远纹理后,要记得释放图像的内存。

stbi_image_free(data);

生成纹理

生成一个纹理的过程应该看起来像这样:

unsigned int texture;
glGenTextures(1, &texture);
glBindTexture(GL_TEXTURE_2D, texture);
// 为当前绑定的纹理对象设置环绕、过滤方式
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);   
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
// 加载并生成纹理
int width, height, nrChannels;
unsigned char *data = stbi_load("container.jpg", &width, &height, &nrChannels, 0);
if (data)
{
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, data);
    glGenerateMipmap(GL_TEXTURE_2D);
}
else
{
    std::cout << "Failed to load texture" << std::endl;
}
stbi_image_free(data);

应用纹理

  1. 在顶点数据中加入纹理坐标,以告知OpenGL如何采样纹理:
float vertices[] = {
//     ---- 位置 ----       ---- 颜色 ----     - 纹理坐标 -
     0.5f,  0.5f, 0.0f,   1.0f, 0.0f, 0.0f,   1.0f, 1.0f,   // 右上
     0.5f, -0.5f, 0.0f,   0.0f, 1.0f, 0.0f,   1.0f, 0.0f,   // 右下
    -0.5f, -0.5f, 0.0f,   0.0f, 0.0f, 1.0f,   0.0f, 0.0f,   // 左下
    -0.5f,  0.5f, 0.0f,   1.0f, 1.0f, 0.0f,   0.0f, 1.0f    // 左上
};

2. 调整顶点着色器使其能够接受纹理坐标为一个顶点属性,并把坐标传给片段着色器:

#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aColor;
layout (location = 2) in vec2 aTexCoord;

out vec3 ourColor;
out vec2 TexCoord;

void main(){
    gl_Position = vec4(aPos, 1.0);
    ourColor = aColor;
    TexCoord = aTexCoord;
}

3. 怎样能把纹理对象传给片段着色器呢?GLSL有一个供纹理对象使用的内建数据类型,采样器(Sampler),它以纹理类型(sampler1Dsampler2Dsampler3D)作为后缀。生成一个采样器的具体做法是声明一个uniform sampler2D

#version 330 core
out vec4 FragColor;

in vec3 ourColor;
in vec2 TexCoord;

uniform sampler2D ourTexture;

void main(){
    FragColor = texture(ourTexture, TexCoord);
}

使用GLSL内建的texture函数来采样纹理的颜色,它第一个参数是纹理绑定到的纹理采样器,第二个参数是被采样纹理对应的纹理坐标。此时就将纹理“贴”到了物体上。

  1. 由于添加了一个额外的顶点属性(纹理坐标),新的顶点格式如下:

// 新增顶点属性纹理坐标,layout (location = 2),每个纹理坐标有两个组件:S T
glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(6 * sizeof(float)));
glEnableVertexAttribArray(2);

5. 在调用glDrawElements之前调用glBindTexture,从而自动把纹理赋值给片段着色器的采样器:

glBindTexture(GL_TEXTURE_2D, texture);
glBindVertexArray(VAO);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);

绘制木箱

绘制木箱的全部代码详见。

绘制三色木箱

略微修改片段着色器代码:

使得不仅将纹理贴到物体上,而且为其上色,还记得绘制三色三角形时片段着色器的内容吗?

因此会得到一个三色木箱:

通过纹理单元使用多个纹理

一个纹理的位置值通常称为一个纹理单元(Texture Unit)。一个纹理的默认纹理单元是0,纹理单元的主要目的是在着色器中使用多个纹理。通过把纹理单元赋值给采样器,可以一次绑定多个纹理。

OpenGL保证至少有16个纹理单元供使用(GL_TEXTURE0GL_TEXTRUE15),可以通过GL_TEXTURE0 + 8的方式获得GL_TEXTURE8

glActiveTexture

  1. 在绑定纹理glBindTexture之前先激活纹理单元glActiveTexture
glActiveTexture(GL_TEXTURE0); // 纹理单元 GL_TEXTURE0 默认被激活
glBindTexture(GL_TEXTURE_2D, texture);

函数原型:

void glActiveTexture(GLenum texture);
  • texture:指定要激活的纹理单元。
  1. 编辑片段着色器来接收另一个采样器:
#version 330 core
out vec4 FragColor;
 
in vec3 ourColor;
in vec2 TexCoord;
 
// texture sampler
uniform sampler2D texture1;
uniform sampler2D texture2;
 
void main()
{
    // 将纹理颜色与定点颜色混合
    FragColor = mix(texture(texture1, TexCoord), texture(texture2, TexCoord), 0.2);
}
 

GLSL内建的mix函数需要接受两个值作为参数,并对它们根据第三个参数进行线性插值:

  • 第三个值是0.0,会返回第一个输入;
  • 1.0,返回第二个输入值;
  • 0.2会返回80%的第一个输入颜色和20%的第二个输入颜色,即返回两个纹理的混合色。
  1. 载入并创建另一个纹理笑脸
glGenTextures(1, &texture2);
glBindTexture(GL_TEXTURE_2D, texture2);

glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
// load image, create texture and generate mipmaps
data = stbi_load("awesomeface.png", &width, &height, &nrChannels, 0);
if (data)
{
    // note that the awesomeface.png has transparency and thus an alpha channel, so make sure to tell OpenGL the data type is of GL_RGBA
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, data);
    glGenerateMipmap(GL_TEXTURE_2D);
}
else {
    std::cout << "Failed to load texture2" << std::endl;
}
stbi_image_free(data);

4. 定义哪个uniform采样器对应哪个纹理单元,由于只需要设置一次,所以会放在渲染循环的前面:

ourShader.use(); // 不要忘记在设置uniform变量之前激活着色器程序!
glUniform1i(glGetUniformLocation(ourShader.ID, "texture1"), 0); // 手动设置
ourShader.setInt("texture2", 1); // 或者使用着色器类设置

5. 改变渲染流程,先绑定两个纹理到对应的纹理单元,再绘制:

// bind Texture
// glActiveTexture(GL_TEXTURE0); 纹理单元 GL_TEXTURE0 默认被激活,因此可省略
glBindTexture(GL_TEXTURE_2D, texture1);
glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_2D, texture2);

// render container
ourShader.use();
glBindVertexArray(VAO);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);

颠倒的图片

开始绘制,但是得到一张颠倒的图片:

这是因为OpenGL要求y0.0坐标是在图片的底部的,但是图片的y轴0.0坐标通常在顶部。因此需要stb_image.h来在图像加载时翻转y轴:

stbi_set_flip_vertically_on_load(true);

这样图片就正了:

绘制上图的全部代码详见。

改变笑脸朝向

通过修改片段着色器来让笑脸朝向改变:

#version 330 core
out vec4 FragColor;

in vec3 ourColor;
in vec2 TexCoord;

uniform sampler2D ourTexture1;
uniform sampler2D ourTexture2;

void main()
{
    FragColor = mix(texture(ourTexture1, TexCoord), texture(ourTexture2, vec2(1.0 - TexCoord.x, TexCoord.y)), 0.2);
}

绘制结果:

生成四个笑脸

  1. 设定一个从0.0f2.0f范围内的(而不是原来的0.0f1.0f)纹理坐标:
float vertices[] = {
    // positions          // colors           // texture coords
    0.5f,  0.5f, 0.0f,   1.0f, 0.0f, 0.0f,   2.0f, 2.0f, // top right
    0.5f, -0.5f, 0.0f,   0.0f, 1.0f, 0.0f,   2.0f, 0.0f, // bottom right
    -0.5f, -0.5f, 0.0f,   0.0f, 0.0f, 1.0f,   0.0f, 0.0f, // bottom left
    -0.5f,  0.5f, 0.0f,   1.0f, 1.0f, 0.0f,   0.0f, 2.0f  // top left
};

2. 配合用不同的纹理环绕方式:

// 木箱的环绕方式由 GL_REPEAT 改为 GL_CLAMP_TO_EDGE
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
// 笑脸的环绕方式不改变

3. 从而生成4个笑脸:

改变纹理可见度

  1. 使用一个uniform变量作为mix函数的第三个参数,通过接收上和下键的输入来改变两个纹理可见度:
#version 330 core
out vec4 FragColor;
 
in vec3 ourColor;
in vec2 TexCoord;
 
// texture sampler
uniform sampler2D texture1;
uniform sampler2D texture2;
uniform float mixValue;
 
void main()
{
    FragColor = mix(texture(texture1, TexCoord), texture(texture2, TexCoord), mixValue);
}

2. 更改接收键入信息的函数:

void processInput(GLFWwindow *window)
{
    if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)
        glfwSetWindowShouldClose(window, true);
    if (glfwGetKey(window, GLFW_KEY_UP) == GLFW_PRESS) {
        mixValue += 0.001f; // 相应地更改此值(根据系统硬件,可能太慢或太快)
        if (mixValue >= 1.0f) {
            mixValue = 1.0f;
        }
    }
    if (glfwGetKey(window, GLFW_KEY_DOWN) == GLFW_PRESS) {
        mixValue -= 0.001f;
        if (mixValue <= 0.0f) {
            mixValue = 0.0f;
        }
    }
}

3. 在循环渲染中更新 mixValue 的值:

// render loop
while (!glfwWindowShouldClose(window))
{
    // input
    processInput(window);

    // render
    glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
    glClear(GL_COLOR_BUFFER_BIT);

    // bind Texture
    glBindTexture(GL_TEXTURE_2D, texture1);
    glActiveTexture(GL_TEXTURE1);
    glBindTexture(GL_TEXTURE_2D, texture2);
    
    ourShader.setFloat("mixValue", mixValue);
    
    // render container
    ourShader.use();
    glBindVertexArray(VAO);
    glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);

    // glfw: swap buffers and poll IO events (keys pressed/released, mouse moved etc.)
    // -------------------------------------------------------------------------------
    glfwSwapBuffers(window);
    glfwPollEvents();
}

全部代码详见。