OpenGL学习笔记6——贴图
过了一段时间没搞OpenGL了,接着来学学。
1 加载图片
首先我们要把图片(即是纹理)加载进来。在这里下下来stb_image.h。用这个头文件帮助我们加载图片。把头文件添加到项目中来。
先引用头文件。
#define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h"
然后尝试读取以下图片,我先在项目的目录里面新建一个Textures文件夹,随便做了一个128*128的jpg图片放进去,叫做test。
先用以下的代码测试一下,stbi_load用来加载图片,第一个参数是图片的路径,width返回图片的宽,height返回图片的高,nrChannels返回图片通道的数量。函数返回一个char的指针,指向图片的数据。
int width, height, nrChannels;
unsigned char *data = stbi_load("Textures/test.jpg", &width, &height, &nrChannels, 0);
// 输出一下图片的参数
cout << "width:" << width << endl << "height:" << height << endl << "nrChannels:" << nrChannels << endl;
// 输出前100个像素的数据
for (int i = 0; i < 100; i++)
{
cout << (int)data[i] << endl;
}
// 释放内存
stbi_image_free(data);
运行结果:
下面的数据分别是3行为一个像素,分辨是rgb的值。现在图片文件时读进来了没有问题。
2 概念
2.1 UV
就是顶点对应纹理的坐标。通过uv才能让贴图对应到我们想要的位置,uv的取值是0~1的,如果超过这个范围会怎么显示呢?会根据纹理环绕方式来决定。OpenGL提供了以下四种方式(直接拿LearnOpenGL的图)。
- 重复,就是重复图像(我猜直接取uv的小数和符号就可以实现这个了吧)。
- 镜像,跟重复差不多,区别就是相邻的两个图是镜像的。
- 重复边缘,重复边缘的颜色。
- 指定颜色,指定为某一种颜色。
2.2 纹理过滤
贴图在极大多数情况下都不会正好的对应分辨率的显示出来,有时候会小些有时候会大些。怎么显示这些情况下的贴图,就用到了纹理过滤。(再次盗图)纹理过滤有多种方式,这里先说的是邻近过滤和线性过滤。肉眼可见的区别邻近过滤更像是一个个分明的像素点,线性过滤则是有一定的模糊。所以邻近过滤更适合像素风格的画面,如果像素风格使用线性过滤反而会一片模糊。对应Unity中过滤模式的Point。
本质区别就是邻近过滤取像素中点距离纹理坐标最接近颜色,线性过滤取周围像素的混合色。
2.3 多级渐远纹理
在纹理距离摄像头特别远(即纹理很小)的时候,直接对高清的纹理进行过滤的话效果会不太好。同时也会浪费很多性能。
这就出现了多级渐远纹理,就是生成几个分辨率更小的纹理,根据显示的距离选择合适的分辨率。典型的空间换取时间。
3 应用纹理
3.1 设置uv信息
首先和以往不同的,给每个顶点信息加上两个作为纹理坐标。然后用EBO来设置顶点的索引。
// 三角形的顶点数据
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 // 左上
};
unsigned int indices[] = { // 注意索引从0开始!
0, 1, 2, // 第一个三角形
0, 2, 3 // 第二个三角形
};
因为修改了顶点的数据,VAO这边也要做相应的修改,新开辟一个2号属性存uv,数量为2。每一个偏移改为8 * sizeof(float),uv从6 * sizeof(float)开始。
// 顶点坐标
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);
// 顶点UV
glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(6 * sizeof(float)));
glEnableVertexAttribArray(2);
使用EBO,需要创建EBO和绑定索引数组。
// EBO
unsigned int EBO;
glGenBuffers(1, &EBO);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
以及改为用以下语句渲染。
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
3.2 修改着色器
顶点着色器
#version 330 core
layout(location = 0) in vec3 aPos;
layout(location = 1) in vec3 aColor;
layout(location = 2) in vec2 aTexCoord; // 新增uv的数据,从VAO2号属性中获取数据
out vec3 Color;
out vec2 TexCoord; // 将uv传去流水线下一步骤
void main() {
gl_Position = vec4(aPos, 1.0);
Color = aColor;
TexCoord = aTexCoord; // 不做处理
}
片元着色器
#version 330 core
uniform sampler2D ourTexture; // 通过uniform在外部传入一个sampler2D类型的贴图
in vec3 Color;
in vec2 TexCoord; // 纹理坐标信息
out vec4 FragColor;
void main(){
// 从ourTexture中取得TexCoord位置的颜色
FragColor = texture(ourTexture, TexCoord);
}
3.3 渲染贴图
先加载贴图,
// 贴图
unsigned int TexBuffer;
glGenTextures(1, &TexBuffer);
glBindTexture(GL_TEXTURE_2D, TexBuffer);
// 加载图片
int width, height, nrChannels;
unsigned char *data = stbi_load("Textures/test.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); // 生成MipMap即是多级渐远纹理
}
else {
cout << "Load Texture failed." << endl;
}
stbi_image_free(data); // 释放内存
在渲染循环中加上这一句绑定贴图。
glBindTexture(GL_TEXTURE_2D, TexBuffer);
运行起来效果就如下了(自己瞎画的一个贴图)。
如果改下片元着色器,把顶点颜色和贴图颜色相乘。就会有如下的结果。
FragColor = texture(ourTexture, TexCoord) * Color;
完整代码好多,放这里了。
4 第二张纹理
4.1 纹理单元
之前的unifrom sampler2D没有赋值也能用,是因为个纹理的默认纹理单元是0,0号纹理单元是默认开启的。OpenGL中至少有16个纹理单元可以使用。
应用多个纹理就需要手动的激活纹理单元。
// === 贴图 ===
stbi_set_flip_vertically_on_load(true); // 反转Y
int width, height, nrChannels;
unsigned char *data;
// 第一张图
unsigned int TexBuffer1;
glGenTextures(1, &TexBuffer1);
glActiveTexture(GL_TEXTURE0); // 激活0号单元
glBindTexture(GL_TEXTURE_2D, TexBuffer1);
data = stbi_load("Textures/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 {
cout << "Load Texture failed." << endl;
}
stbi_image_free(data);
// 第二张图
unsigned int TexBuffer2;
glGenTextures(1, &TexBuffer2);
glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_2D, TexBuffer2);
data = stbi_load("Textures/awesomeface.png", &width, &height, &nrChannels, 0);
if (data) {
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, data); // 这里要是RGBA
glGenerateMipmap(GL_TEXTURE_2D);
}
else {
cout << "Load Texture failed." << endl;
}
stbi_image_free(data);
4.2 指定两张纹理
在渲染循环前通过设置unifrom设置两张贴图的纹理单元ID,需要先use。
/*指定贴图*/
myShader.use();
myShader.setInt("texture1", 0);
myShader.setInt("texture2", 1);
在渲染循环中两个纹理都需要激活绑定。
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, TexBuffer1);
glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_2D, TexBuffer2);
没有问题的话应该就能看到结果了。
话说我这个笑脸的外面还有一圈白色,不知道是啥问题。教程的效果和这个稍有出入。
完整代码