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



所有评论(0)