在OpenGL中,​​纹理(Texture)​​ 是用于包裹在模型表面或实现复杂视觉效果的一种图像数据,本质上是GPU管理的​​多维数组​​(如2D图片、立方体贴图等)。

在前几章我们已经渲染出一个三角形,现在我们如果要将我们所选中的2D图片映射到三角形中,我们需要指定三角形的每个顶点对应图片的哪一部分,这样每个顶点就会关联着一个纹理坐标(Texture Coordinate),用来标明该从纹理图像的哪个部分采样.

纹理坐标在x和y轴上,范围为0到1之间(注意我们使用的是2D纹理图像)。使用纹理坐标获取纹理颜色叫做采样(Sampling)。纹理坐标起始于(0, 0),也就是纹理图片的左下角,终止于(1, 1),即纹理图片的右上角。

纹理坐标看起来就像这样:

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

纹理环绕方式

在OpenGL中,​​纹理环绕方式(Texture Wrapping)​​ 定义了当纹理坐标(UV坐标)超出默认范围 [0, 1] 时,如何采样纹理边缘的行为。这一机制解决了模型UV坐标超出边界时的视觉衔接问题。

四种主要环绕模式​

通过 glTexParameteri 分别设置 ​​S轴(水平)​​ 和 ​​T轴(垂直)​​ 的环绕行为:

环绕模式 效果描述 视觉示例
GL_REPEAT ​重复纹理​​(默认模式)
超出部分平铺重复
[瓷砖式重复]
GL_MIRRORED_REPEAT ​镜像重复​
纹理交替正反镜像复制
[镜像对称的拼贴]
GL_CLAMP_TO_EDGE ​边缘拉伸​
超出部分强制采样纹理边缘的像素颜色
[边缘延伸为纯色带]
GL_CLAMP_TO_BORDER ​自定义边界色​
超出部分填充通过 glTexParameterfv 指定的边框颜色
[外围填充指定颜色]

纹理过滤

在OpenGL中,​​纹理过滤(Texture Filtering)​​ 决定了当纹理被放大(Magnification)或缩小(Minification)时,如何从纹理像素(Texel)计算出最终屏幕像素(Pixel)的颜色。它是控制纹理渲染质量的关键设置。

两种基础的纹理过滤方式

GL_NEAREST(也叫邻近过滤,Nearest Neighbor Filtering)是OpenGL默认的纹理过滤方式。当设置为GL_NEAREST的时候,OpenGL会选择中心点最接近纹理坐标的那个像素。

GL_LINEAR(也叫线性过滤,(Bi)linear Filtering)它会基于纹理坐标附近的纹理像素,计算出一个插值,近似出这些纹理像素之间的颜色。一个纹理像素的中心距离纹理坐标越近,那么这个纹理像素的颜色对最终的样本颜色的贡献越大

代码示例

// 绑定纹理
glBindTexture(GL_TEXTURE_2D, textureID);

// 基础过滤
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

// 环绕方式
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);

// 生成Mipmap
glGenerateMipmap(GL_TEXTURE_2D);

加载和创建纹理

首先我们需要使用到stb_image.h这一图像加载库,将它下载之后以stb_image.h的名字加入你的工程,在Cpp文件中加入下面代码

#define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h"

下面选取一张我们喜欢的图片,使用stb_image.h来把它加载出来,我们需要使用它的stbi_load函数.

#include <stb_image.h>

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

第一个参数是导入图像的名字,第二,三个参数是我们定义的图像的宽度与高度,第四个参数表示的是原始图像的通道数,1为灰度图,3为RGB,4为RGBA.最后一个参数表示我们期望的通道数,0表示与原通道数一致,1/3/4表示做更改.

接下来我们来生成纹理

和之前一样,纹理也是使用ID引用

unsigned int TexBufferA;
glGenTextures(1,&TexBufferA);//1表示生成一个纹理数量
glBindTexture(GL_TEXTURE_2D,TexBufferA);//将纹理对象TexBufferA绑定到当前激活的纹理单元

现在纹理已经绑定了,我们可以使用前面载入的图片数据生成一个纹理了。纹理可以通过glTexImage2D来生成

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

glTexImage2D的各参数含义如下

glTexImage2D(
    GL_TEXTURE_2D,      // 目标纹理类型(2D纹理)
    0,                  // Mipmap层级(0表示基础级别)
    GL_RGB,             // 纹理在GPU中的存储格式(RGB三通道)
    width,              // 纹理宽度(像素)
    height,             // 纹理高度(像素)
    0,                  // 历史遗留参数(必须为0)
    GL_RGB,             // 上传数据的像素格式(CPU端数据格式)
    GL_UNSIGNED_BYTE,   // 像素数据类型(无符号字节,0-255)
    data                // 指向像素数据的指针
);

当调用glTexImage2D时,当前绑定的纹理对象就会被附加上纹理图像。然而,目前只有基本级别(Base-level)的纹理图像被加载了,如果要使用多级渐远纹理,我们必须手动设置所有不同的图像(不断递增第二个参数)。或者,直接在生成纹理之后调用glGenerateMipmap。这会为当前绑定的纹理自动生成所有需要的多级渐远纹理。

生成纹理之后我们需要释放相应内存

stbi_image_free(data);

完整工作流程示例:

// 1. 生成纹理ID
unsigned int texture;
glGenTextures(1, &texture);
glBindTexture(GL_TEXTURE_2D, texture);

// 2. 上传数据并设置参数
int width, height, nrChannels;
unsigned char* data = stbi_load("texture.jpg", &width, &height, &nrChannels, 0);
glTexImage2D(GL_TEXTURE_2D,0,GL_RGB,width,height,0,GL_RGB,GL_UNSIGNED_BYTE,data);

// 3. 生成Mipmap
glGenerateMipmap(GL_TEXTURE_2D);

// 4. 设置过滤方式(Mipmap需配合此参数)
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

// 5. 释放CPU端数据
stbi_image_free(data);

当我们使用纹理后我们必须更新顶点数据

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    // 左上
};

因为我们添加了一个新的顶点格式,所以我们必须告诉OpenGL我们新的顶点格式

 glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)0);
 glEnableVertexAttribArray(0);
 //颜色属性
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(3 * sizeof(float)));
 glEnableVertexAttribArray(1);
 //贴图属性
 glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(3 * sizeof(float)));
 glEnableVertexAttribArray(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;
}
#version 330 core
out vec4 FragColor;

in vec3 ourColor;
in vec2 TexCoord;

uniform sampler2D ourTexture;//采样器

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

我们使用GLSL内建的texture函数来采样纹理的颜色,它第一个参数是纹理采样器,第二个参数是对应的纹理坐标。texture函数会使用之前设置的纹理参数对相应的颜色值进行采样。这个片段着色器的输出就是纹理的(插值)纹理坐标上的(过滤后的)颜色。

现在只剩下在调用glDrawElements之前绑定纹理了,它会自动把纹理赋值给片段着色器的采样器

你可能会奇怪为什么sampler2D变量是个uniform,我们却不用glUniform给它赋值。使用glUniform1i,我们可以给纹理采样器分配一个位置值,这样的话我们能够在一个片段着色器中设置多个纹理。一个纹理的位置值通常称为一个纹理单元(Texture Unit)。一个纹理的默认纹理单元是0,它是默认的激活纹理单元,所以教程前面部分我们没有分配一个位置值。

纹理单元的主要目的是让我们在着色器中可以使用多于一个的纹理。通过把纹理单元赋值给采样器,我们可以一次绑定多个纹理,只要我们首先激活对应的纹理单元。就像glBindTexture一样,我们可以使用glActiveTexture激活纹理单元,传入我们需要使用的纹理单元

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

我们仍然需要编辑片段着色器来接收另一个采样器。

#version 330 core
in vec4 vertexColor;
in vec2 TexCoord;

uniform sampler2D ourTexture;
uniform sampler2D ouruv;

out vec4 FragColor;

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

最终输出颜色现在是两个纹理的结合。GLSL内建的mix函数需要接受两个值作为参数,并对它们根据第三个参数进行线性插值。如果第三个值是0.0,它会返回第一个输入;如果是1.0,会返回第二个输入值。0.2会返回80%的第一个输入颜色和20%的第二个输入颜色,即返回两个纹理的混合色。

所以接下来我们要绘入我们第二个纹理单元,流程与第一个类似,不过渲染阶段要有所更改

unsigned int TexbufferB;
glGenTextures(1, &TexbufferB);
glActiveTexture(GL_TEXTURE3);
glBindTexture(GL_TEXTURE_2D, TexbufferB);

unsigned char* data2 = stbi_load("container.jpg", &width, &height, &nrChannel, 0);
if (data2)
{
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, data2);
    glGenerateMipmap(GL_TEXTURE_2D);
}
else
{
    cout << "load iname failed" << endl;
}
stbi_image_free(data2);

注意,我们现在要读取一张包含alpha(透明度)通道的.png图片,这意味着我们现在需要使用GL_RGBA参数,指定该图片数据包含了alpha通道;否则OpenGL将无法正确解析图片数据。

为了使用第二个纹理(以及第一个),我们必须改变一点渲染流程,先绑定两个纹理到对应的纹理单元,然后定义哪个uniform采样器对应哪个纹理单元

 glActiveTexture(GL_TEXTURE0);
 glBindTexture(GL_TEXTURE_2D,TexBufferA);
 glActiveTexture(GL_TEXTURE3);
 glBindTexture(GL_TEXTURE_2D, TexbufferB);
 glBindVertexArray(VAO);

我们还要通过使用glUniform1i设置每个采样器的方式告诉OpenGL每个着色器采样器属于哪个纹理单元。

glUniform1i(glGetUniformLocation(testShader->ID, "ourTexture"), 0);
glUniform1i(glGetUniformLocation(testShader->ID, "ourcongyu"), 3);

但是纹理是颠倒的所以我们还需要下面这句代码来翻转y轴

stbi_set_flip_vertically_on_load(true);

本文参考:Learn OpenGL, extensive tutorial resource for learning Modern OpenGL

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐