上接:https://blog.csdn.net/weixin_44506615/article/details/149894322
完整代码:https://gitee.com/Duo1J/learn-open-gl
一、纹理
我们已经为我们的图形添加了颜色,现在我们想要添加更多的细节,就要用到纹理(Texture),纹理就是一张图片,通常是2D,也有1D和3D的纹理。
本节之前,大家可以先在上方的git仓库的Resource目录,或是这里和这里(来自LearnOpenGL网站)下载到本节要用的两张图片,分别命名为T_Wall.jpg和T_Face.png
接下来先了解几个纹理相关的概念和设置
1. 纹理坐标 (UV/ST)
现在我们要将纹理映射到我们的图形上,也就是我们的图形上的某一点需要知道对于纹理的哪一个位置,我们就需要为每一个顶点关联一个纹理坐标(Texture Coordinate),通常也称作UV坐标,但是在OpenGL这里则是用ST来表示,纹理坐标是**(0, 0)到(1, 1)**,如下图 (图片来自于LearnOpenGL)
2. 纹理环绕方式
那假如我们使用一个 (0, 0) ~ (1, 1) 范围之外的坐标去采样纹理,这种行为该如何定义呢?
接下来就需要引入纹理的环绕方式,OpenGL提供了以下几种选择
环绕方式 | 描述 |
---|---|
GL_REPEAT | 默认,重复纹理图像 |
GL_MIRRORED_REPEAT | 和GL_REPEAT 一样,但是是镜像的 |
GL_CLAMP_TO_EDGE | 纹理坐标会被约束到0~1之间,超出部分会重复纹理的边缘,产生一种边缘被拉伸的效果 |
GL_CLAMP_TO_BORDER | 超出部分为用户指定的边缘颜色 |
可以参考以下图片 (图片来自于LearnOpenGL)
我们可以通过以下代码来设置纹理环绕方式,S、T对应于U、V或是X、Y
// 参数1:纹理类型
// 参数2:坐标轴
// 参数3:环绕方式
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_MIRRORED_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_MIRRORED_REPEAT);
3. 纹理过滤
纹理坐标可能是任意的浮点值,它最后的采样位置并不会和图片的像素一一对应,所以我们需要知道怎样将纹理像素(Texture Pixel)映射到纹理坐标上
OpenGL提供了几个纹理过滤的选项,我们来主要了解一下GL_NEAREST和GL_LINEAR
GL_NEAREST,临近过滤,是OpenGL的默认纹理过滤方式,该方式会直接选择离采样中心点最近的那个像素,如下图所示 (图片来自于LearnOpenGL)
GL_LINEAR,线性过滤或称双线性过滤,他会基于纹理坐标附近的纹理像素,计算一次插值,来近似出这些纹理像素之间的颜色,并且它会根据距离的远近来权重插值,如下图所示 (图片来自于LearnOpenGL)
接下来看看这两种过滤方式的效果
图片来自于LearnOpenGL
并且我们还可以根据这个纹理是放大还是缩小来决定所要使用的过滤方式,如以下代码,GL_TEXTURE_MIN_FILTER表示缩小时,GL_TEXTURE_MAG_FILTER表示放大时
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
4. 多级渐远纹理 (MipMap)
在一个场景中,物体有远有近,远近不同的物体会产生不同数量的片段
像是远处的物体会产生较少的片段,那么它在对纹理进行采样的时候,就会因为采样不足而导致一系列的走样、锯齿的问题。并且一个显示上较小的物体用一个大尺寸的纹理也会造成空间的浪费,所以现在引入多级渐远纹理(MipMap)
简单来说就是一系列分级的纹理图片,第0级就是原图,后面的每一级的宽高都是前一级的二分之一,之后便会根据距离来选择合适的纹理,如下图所示 (图片来自LearnOpenGL)
同时,MipMap也有一系列的纹理过滤方式,如下表所示
过滤方式 | 描述 |
---|---|
GL_NEAREST_MIPMAP_NEAREST | 使用最邻近的MipMap,并且使用邻近插值进行采样 |
GL_LINEAR_MIPMAP_NEAREST | 使用最邻近的MipMap,并且使用线性插值进行采样 |
GL_NEAREST_MIPMAP_LINEAR | 使用两个最邻近的MipMap之间进行线性插值,并且使用邻近插值进行采样 |
GL_LINEAR_MIPMAP_LINEAR | 使用两个最邻近的MipMap之间进行线性插值,并且使用线性插值进行采样 |
可以通过以下代码进行设置 |
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR_MIPMAP_NEAREST);
二、纹理应用
1. 加载、创建、绑定、赋值纹理并生成MipMap
了解完纹理的一些基础概念和设置之后,我们就可以开始应用纹理了
首先我们需要将纹理从磁盘中加载出来,这里引入一个库:stb_image,这是一个单头文件图像加载库,可以在这里或是最上方的git仓库中下载
然后我们再创建一个stb_image.cpp文件,按他所说添加一段代码
// stb_image.h
Do this:
#define STB_IMAGE_IMPLEMENTATION
before you include this file in *one* C or C++ file to create the implementation.
// i.e. it should look like this:
#include ...
#include ...
#include ...
#define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h"
// stb_image.cpp
#define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h"
我们可以用以下代码来加载、创建、绑定、赋值纹理并生成MipMap
// OpenGL要求图片y轴的0坐标是在底部,而图片y轴0坐标往往是在顶部,在加载前需要设置翻转
stbi_set_flip_vertically_on_load(true);
// 加载纹理,并获取宽高和颜色通道数
int texWidth, texHeight, nrChannels;
const char* wallTexPath = "F:/Scripts/Cpp/LearnOpenGL/learn-open-gl/Resource/T_Wall.jpg";
unsigned char* wallTexData = stbi_load(wallTexPath, &texWidth, &texHeight, &nrChannels, 0);
unsigned int wallTex;
// 创建纹理
glGenTextures(1, &wallTex);
if (wallTexData)
{
// 绑定纹理
glBindTexture(GL_TEXTURE_2D, wallTex);
// 设置纹理参数
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);
// 传入纹理数据
// 参数1:纹理类型
// 参数2:Mipmap级别
// 参数3:纹理存储格式
// 参数4、5:宽高
// 参数6: 历史遗留原因,总是0
// 参数7、8:源图格式和数据类型,我们使用RGB值加载图像并存储为char(byte)数组
// 参数9:纹理数据
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, texWidth, texHeight, 0, GL_RGB, GL_UNSIGNED_BYTE, wallTexData);
// 生成Mipmap
// 要使用Mipmap,我们需要不断递增`glTexImage2D`的第二个参数来调用,或是直接使用`glGenerateMipmap`
glGenerateMipmap(GL_TEXTURE_2D);
// 释放
stbi_image_free(wallTexData);
}
else
{
std::cout << "[Error] Failed to load texture: " << wallTexPath << std::endl;
EXIT
}
我们可以用几乎同样的方式加载第二张纹理T_Face.png并存放到变量faceTex中,唯一需要注意第二张纹理是png格式,带有Alpha通道,所以
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, texWidth, texHeight, 0, GL_RGBA, GL_UNSIGNED_BYTE, faceTexData);
2. 应用纹理
纹理准备好之后,接下来就可以开始应用了,首先修改一下我们的顶点数据,带上纹理坐标
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 // 左上
};
那么目前顶点的排布就变成了X、Y、Z、R、G、B、S、T,修改顶点属性设置
int step = 8, curStep = 0;
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, step * sizeof(float), (void*)curStep);
glEnableVertexAttribArray(0);
curStep += 3;
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, step * sizeof(float), (void*)(curStep * sizeof(float)));
glEnableVertexAttribArray(1);
curStep += 3;
glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, step * sizeof(float), (void*)(curStep * sizeof(float)));
glEnableVertexAttribArray(2);
curStep += 2;
接下来修改顶点着色器
// VertexShader.vs
#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aColor;
// 接收纹理坐标
layout (location = 2) in vec2 aTexCoord;
out vec3 vertexColor;
out vec2 texCoord;
void main()
{
gl_Position = vec4(aPos, 1.0);
vertexColor = aColor;
// 将纹理坐标传给后续的片段着色器,以供采样
texCoord = aTexCoord;
}
接下来修改片段着色器
// FragmentShader.fs
#version 330 core
out vec4 FragColor;
in vec3 vertexColor;
// 接收顶点着色器传来的纹理坐标
in vec2 texCoord;
// 声明纹理采样器
uniform sampler2D texture1;
uniform sampler2D texture2;
void main()
{
// 调用texture(tex, st)来使用st纹理坐标对tex纹理进行采样
// mix(a, b, rate)会以rate的比率在a、b两颜色之间进行混合
// 例如以下代码会返回80%的texture1和20%的texture2
FragColor = mix(texture(texture1, texCoord), texture(texture2, texCoord), 0.2);
}
着色器修改好了之后,我们还需要为uniform采样器赋值
再来了解一下纹理单元,一个纹理单元可以理解为一个纹理的位置
可以通过GL_TEXTURE0、GL_TEXTURE1…来表示纹理单元,GL_TEXTURE0默认总是被激活的
OpenGL至少保证有16个纹理单元供我们使用
最后,修改一下我们的主循环代码
// 由于我们目前使用的是标准设备坐标的顶点,为了不使图像变形,我们修改一下分辨率为4:3分辨率
GLFWwindow* window = glfwCreateWindow(800, 600, "OpenGLRenderer", NULL, NULL);
while (!glfwWindowShouldClose(window))
{
glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
ProcessInput(window);
shader.Use();
// 设置uniform采样器对应的纹理单元
shader.SetInt("texture1", 0);
shader.SetInt("texture2", 1);
glBindVertexArray(VAO);
// 激活纹理单元并绑定纹理
// GL_TEXTURE0默认激活,这一步可以省略
// glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, wallTex);
glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_2D, faceTex);
// 换回glDrawElements
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
glfwSwapBuffers(window);
glfwPollEvents();
}
编译运行,顺利的话,我们可以看见以下图像
如果你的笑脸是颠倒的,请回到加载纹理前设置
stbi_set_flip_vertically_on_load(true);
代码文件较多,之后就不贴完整代码了
完整代码和资源请看最上方的git仓库
下接:https://blog.csdn.net/weixin_44506615/article/details/150052880?spm=1001.2014.3001.5502