OpenGL学习笔记(3) 纹理
关于纹理
一般游戏里的物体不一定都是纯色的物体,物体上面会有一些图片贴在上面,比如墙壁,箱子,地板,可以看到砖头、木板和大理石组成的图片,要把图片贴到计算机里的几何图形的话,就要把图片的颜色采样贴到几何图形上,采样是计算机经常干的工作,计算机要处理自然中的数据就需要对数据进行采样,比如说对声音采样就是采集声音的频率和频幅,分别代表声音的音色和声量,当然,采集到的是一个模拟量,然而计算机无法处理模拟量,所以需要将模拟量量化为数字量,也就是二进制数进行存储,同理,颜色也是一样,现代计算机中颜色一般以RGB的形式,分别代表Red,Green,Blud三种颜色在这个颜色中的占比,一般使用3个字节存储,也就是说颜色的数量可以达到2^24种,已经足够用了,想要把采集到的颜色贴到物体上的话,我们需要指定顶点的纹理坐标,告诉着色器要从纹理的哪个点开始采样, 纹理坐标的范围是0到1
项目的代码
learnOpenGL里的图解
纹理环绕
纹理坐标的范围是0到1,假如超出这个范围的话,在OpenGL里会有几种方式来贴图,这些方式叫做纹理环绕方式
- GL_REPEAT 对纹理的默认行为。重复纹理图像。
- GL_MIRRORED_REPEAT 和GL_REPEAT一样,但每次重复图片是镜像放置的。
- GL_CLAMP_TO_EDGE 纹理坐标会被约束在0到1之间,超出的部分会重复纹理坐标的边缘,产生一种边缘被拉伸的效果。
- GL_CLAMP_TO_BORDER 超出的坐标为用户指定的边缘颜色。
在我的工程里有用来测试的函数SurroundTest,把环绕方式和过滤模式封装成枚举方便调试
//环绕模式
enum class SurroundMode
{
Repeat = GL_REPEAT, //重复纹理图像
MirroredRepeat = GL_MIRRORED_REPEAT, //镜像重复纹理图像
ClampToEdge = GL_CLAMP_TO_EDGE, //将边缘拉伸
ClampToBoreder = GL_CLAMP_TO_BORDER //超出的部分为指定边缘颜色
};
//过滤方式
enum class FilteringMode
{
Nearest = GL_NEAREST,
Linear = GL_LINEAR,
};
将纹理坐标的分量值设置到[0,1]之外,进行测试
float vertices[] = {
// ---- 位置 ---- - 纹理坐标 -
0.5f, 0.5f, 0.0f, 2.0f, 2.0f, // 右上
0.5f, -0.5f, 0.0f, 2.0f, -1.0f, // 右下
-0.5f, -0.5f, 0.0f, -1.0f, -1.0f, // 左下
-0.5f, 0.5f, 0.0f, -1.0f, 2.0f // 左上
};
测试结果
过滤模式
可以看到环绕方式测试的图片是有一丢丢模糊的(其实是用错过滤模式了),这是因为用了线性过滤模式(Linear),使用线性过滤模式采样的时候,每个像素的颜色会和周围的颜色进行混合算出一个插值,这个插值近似于这些像素
而采用 邻近过滤(Nearest) 的话,那么像素的颜色就是采样器采到的颜色,如果分辨率小的图片贴到大的物体上的话,就会出现颗粒状的图案,但使用线性过滤模式的话这些颗粒就会变得比较平滑
测试一下
是不是可以很明显地看出Linear方式采样的珂朵莉的眼神比较温柔啊,这就是经过平滑处理过的图片。在这里顺便说下unity的三种过滤模式
- Point 点像素过滤,纹理像素会在附近变为块状。
- Bilinear 双线性过滤,平均纹理样本
- Trilinear 三线性过滤,平均纹理样本,同时也在多级渐远纹理级别之间混合
测试代码
首先是读取图片用的代码,对 learnOpenGL 里的代码进行了一次封装,针对不同位深度的图片进行处理,位深度就是图片记录每个像素点的位数,24位表示3个float,也就是RGB值,32位则是4个float,即RGBA,读取图片用了官方介绍的stb_image库
//加载纹理
template <typename S1 = std::string>
void LoadTexture(unsigned int &texture, S1&& pic)
{
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;
stbi_set_flip_vertically_on_load(true);
unsigned char *data = stbi_load((TEXTURE_PATH + std::forward<std::string>(pic)).c_str(), &width, &height, &nrChannels, 0);
std::cout << "nrChannels = " << nrChannels << endl;
if (data)
{
//位深度为24,3个通道(jpg
if (nrChannels == 3)
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, data);
//位深度为32,4个通道(png
else if (nrChannels == 4)
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 texture" << std::endl;
}
stbi_image_free(data);
data = nullptr;
}
着色器代码
顶点着色器
vertex_3.vs
#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec2 aTexCoord;
out vec2 TexCoord;
void main()
{
gl_Position = vec4(aPos, 1.0);
TexCoord = aTexCoord;
}
位置0是顶点的坐标,位置1是输入的纹理坐标,要通过顶点着色器将纹理坐标输出到片段着色器
片段着色器
fragment_3.fs
#version 330 core
out vec4 FragColor;
in vec2 TexCoord;
uniform sampler2D ourTexture;
void main()
{
FragColor = texture(ourTexture, TexCoord);
}
TexCoord是从顶点着色器接收的纹理坐标,使用texture函数来采样,第一个参数是采样器(Sampler),第二个参数是纹理坐标,着色器输出过滤后的颜色值FragColor
接下来是就是正常的测试代码了
void NormalTest()
{
float vertices[] = {
// ---- 位置 ---- - 纹理坐标 -
0.9f, 0.9f, 0.0f, 1.0f, 1.0f, // 右上
0.9f, -0.9f, 0.0f, 1.0f, 0.0f, // 右下
-0.9f, -0.9f, 0.0f, 0.0f, 0.0f, // 左下
-0.9f, 0.9f, 0.0f, 0.0f, 1.0f // 左上
};
//索引
unsigned int indices[] = {
0,1,3,
1,2,3
};
//编译着色器
Shader ourShader("vertex_3.vs", "fragment_3.fs");
ourShader.use();//glUseProgram(shaderProgram);
unsigned int VAO, VBO, EBO;
//顶点数组
glGenVertexArrays(1, &VAO);
glBindVertexArray(VAO);
//绑定顶点数组缓存
glGenBuffers(1, &VBO);
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
//索引缓存
glGenBuffers(1, &EBO);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
//生成纹理
unsigned int texture1;
LoadTexture(texture1, std::move("picture4.png")); //加载纹理
glBindTexture(GL_TEXTURE_2D, texture1);//绑定纹理
// 位置属性
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
// 纹理属性
glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)(3 * sizeof(float))); //最后一个参数是数据的起点
glEnableVertexAttribArray(1);
while (!glfwWindowShouldClose(glWindow))
{
glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
//draw
glBindVertexArray(VAO);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
glBindVertexArray(0);
glfwPollEvents();
glfwSwapBuffers(glWindow);
}
glDeleteVertexArrays(1, &VAO);
glDeleteBuffers(1, &VBO);
glDeleteBuffers(1, &EBO);
glfwTerminate();
}
测试结果
珂朵莉好可爱~