LearnOpenGL学习笔记:纹理
( 本文对应学习章节:https://learnopengl-cn.github.io/01%20Getting%20started/06%20Textures/ )
0.前言
之前我们是用顶点和颜色来完成几何图形的绘制,但是遇到复杂的图形就会变得很复杂,这时候纹理(Texture)就登场了。纹理是一个2D图片(甚至也有1D和3D的纹理),它可以用来添加物体的细节。因为我们可以在一张图片上插入非常多的细节,这样就可以让物体非常精细而不用指定额外的顶点。
1.认识纹理
为了把纹理映射(Map)到一个区域我,我们需要指定区域的每个顶点各自对应纹理的哪个部分。这样每个顶点就会关联一个纹理坐标(Texture Coordinate),用来标明该从纹理图像的哪个部分采样(采集片段颜色)。之后在图形的其它片段上进行片段插值(Fragment Interpolation)。
纹理坐标在x和y轴上,范围为0到1之间(注意我们使用的是2D纹理图像)。使用纹理坐标获取纹理颜色叫做采样(Sampling)。纹理坐标起始于(0, 0),也就是纹理图片的左下角,终始于(1, 1),即纹理图片的右上角。下面的图片展示了我们是如何把纹理坐标映射到三角形上的(来自LearnOpenGL)。
纹理坐标现在就是这样:
float texCoords[] = {
0.0f, 0.0f, // 左下角
1.0f, 0.0f, // 右下角
0.5f, 1.0f // 上中
};
对纹理采样的解释非常宽松,它可以采用几种不同的插值方式。所以我们需要自己告诉OpenGL该怎样对纹理采样。 教程里主要讲了纹理环绕方式、纹理过滤、多级渐远纹理三种,直接通过代码来学习更容易理解点。
2.纹理的使用
这部分代码相对于前面的更麻烦点,除了使用着色器章节自定义的着色器类,教程还引入了一个图像加载库stb_image.h,由于官网下载太慢了,我直接clone的LearnOpenGL的github项目代码:https://github.com/JoeyDeVries/LearnOpenGL 。里面有教程用到的库,以及教程的源码,方便对比学习。
和VBO、EBO一样,纹理也是使用ID引用的。生成一个纹理的过程是这样的:
unsigned int texture;
// 创建纹理对象
glGenTextures(1, &texture);
// 将命名纹理绑定到纹理目标,参数1为绑定的目标,参数2为纹理对象
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;
unsigned char *data = stbi_load("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
{
std::cout << "Failed to load texture" << std::endl;
}
// 生成了纹理和相应的多级渐远纹理后,释放图像的内存
stbi_image_free(data);
要注意的是,默认设置跑出来的纹理图是上下颠倒的。这是因为OpenGL要求y轴0.0坐标是在图片的底部的,但是图片的y轴0.0坐标通常在顶部。教程使用stb_image.h帮助我们翻转y轴,只需要在加载任何图像前加入一句:
stbi_set_flip_vertically_on_load(true);
当然你也可以把纹理顶点坐标翻转,或者着色器里修改顶点或坐标。
3.代码
下面是原图和代码效果的对比,因为我设置了纹理参数和着色器,所以看起来变了样子。 代码里Shader类和图片路径我都改了,这里只做参考。
#include <glad/glad.h>
#include <GLFW/glfw3.h>
#include <iostream>
#define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h"
#include "MyShader.h"
void framebuffer_size_callback(GLFWwindow* window, int width, int height);
void processInput(GLFWwindow* window);
const std::string vertexShaderSource = R"(
#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 = vec2(aTexCoord.x, aTexCoord.y);
}
)";
const std::string fragmentShaderSource = R"(
#version 330 core
out vec4 FragColor;
in vec3 ourColor;
in vec2 TexCoord;
// texture sampler
uniform sampler2D texture1;
void main()
{
FragColor = texture(texture1, TexCoord)*vec4(ourColor,1.0f);
}
)";
int main()
{
// glfw: initialize and configure
glfwInit();
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
// glfw window creation
GLFWwindow* window = glfwCreateWindow(800, 600, "LearnOpenGL", NULL, NULL);
if (window == NULL)
{
std::cout << "Failed to create GLFW window" << std::endl;
glfwTerminate();
return -1;
}
glfwMakeContextCurrent(window);
glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);
// glad: load all OpenGL function pointers
if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
{
std::cout << "Failed to initialize GLAD" << std::endl;
glfwTerminate();
return -1;
}
// build and compile our shader zprogram
MyShader ourShader(vertexShaderSource, fragmentShaderSource);
// set up vertex data (and buffer(s)) and configure vertex attributes
float vertices[] = {
// positions // colors // texture coords
1.0f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 2.0f, 2.0f, // top right
1.0f, -1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 2.0f, 0.0f, // bottom right
-1.0f, -1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, // bottom left
-1.0f, 1.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 2.0f // top left
};
unsigned int indices[] = {
0, 1, 3, // first triangle
1, 2, 3 // second triangle
};
unsigned int VBO, VAO, EBO;
glGenVertexArrays(1, &VAO);
glGenBuffers(1, &VBO);
glGenBuffers(1, &EBO);
glBindVertexArray(VAO);
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
// position attribute
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
// color attribute
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(3 * sizeof(float)));
glEnableVertexAttribArray(1);
// texture coord attribute
glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(6 * sizeof(float)));
glEnableVertexAttribArray(2);
// 创建一个纹理对象
unsigned int texture;
glGenTextures(1, &texture);
// 将命名纹理绑定到纹理目标,很明显这又是状态机系统的体现
// void glBindTexture(GLenum target, GLuint texture);
// 参数1指定纹理绑定的目标,参数2指定纹理名称
glBindTexture(GL_TEXTURE_2D, texture);
// 设置纹理参数
// void glTexParameterf(GLenum target, GLenum pname, GLfloat param);
// void glTexParameteri(GLenum target, GLenum pname, GLint param);
//参数1纹理目标,参数2指定单值纹理参数的名称,参数3指定值的参数
//设置纹理环绕参数
//默认为GL_REPEAT,重复纹理图像(坐标的S\T\R等效于xyz)
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); //GL_CLAMP_TO_EDGE
//如果纹理环绕设置为GL_CLAMP_TO_BORDER,还需要设置一个边框颜色
//把纹理坐标设置在范围之外才看得到
//float borderColor[] = { 1.0f, 1.0f, 0.0f, 1.0f };
//glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, borderColor);
//设置纹理过滤参数
//默认为GL_NEAREST临近过滤,选择附近那个像素,
//GL_LINEAR线性过滤则做插值计算。(Min Mag分别制定放大和缩小的过滤方式)
//当要纹理化的像素映射到大于一个纹理元素的区域时,就使用纹理最小化功能:GL_TEXTURE_MIN_FILTER
//当被纹理化的像素映射到小于或等于一个纹理元素的区域时,将使用纹理放大功能:GL_TEXTURE_MAG_FILTER
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
// 加载图像,创建纹理并生成mipmap
int width, height, nrChannels;
//OpenGL要求y轴0.0坐标是在图片的底部的,但是图片的y轴0.0坐标通常在顶部,于是造成了上下翻转。
//stb_image.h能够在图像加载时帮助我们翻转y轴,使用stbi_set_flip_vertically_on_load(true);
stbi_set_flip_vertically_on_load(true);
unsigned char* data = stbi_load(R"(F:\Src\textures\brickwall.jpg)", &width, &height, &nrChannels, 0);
if (data)
{
//指定二位纹理图像
//void glTexImage2D(GLenum target, GLint level, GLint internalFormat, GLsizei width, GLsizei height, GLint border, GLenum format, GLenum type, const GLvoid * data);
//参数1指定目标纹理,参数2指定详细级别编号,参数3纹理中颜色分量的数量
//参数4指定纹理图像的宽度,参数2指定纹理图像的高度或纹理阵列中的层数
//参数5边界为0,参数6指定像素数据格式
//参数7指定像素数据的数据类型,参数8图像数据
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, data);
//为指定的纹理目标生成mipmap
//void glGenerateMipmap(GLenum target);
glGenerateMipmap(GL_TEXTURE_2D);
}
else
{
std::cout << "Failed to load texture" << std::endl;
}
stbi_image_free(data);
glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
// render loop
while (!glfwWindowShouldClose(window))
{
// input
processInput(window);
// render
glClear(GL_COLOR_BUFFER_BIT);
// bind Texture
glBindTexture(GL_TEXTURE_2D, texture);
// render container
ourShader.useProgram();
glBindVertexArray(VAO);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
glfwSwapBuffers(window);
glfwPollEvents();
}
glDeleteVertexArrays(1, &VAO);
glDeleteBuffers(1, &VBO);
glDeleteBuffers(1, &EBO);
glfwTerminate();
return 0;
}
void processInput(GLFWwindow* window)
{
if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)
glfwSetWindowShouldClose(window, true);
}
void framebuffer_size_callback(GLFWwindow* window, int width, int height)
{
glViewport(0, 0, width, height);
}
4.纹理单元
由于我的代码是复制粘贴的示例,一开始我还没发现问题。他怎么莫名其妙就在着色器里声明了一个 “uniform sampler2D texture1;” ,都没看到外部调用类似glUniform的函数去赋值。使用glUniform1i,我们可以给纹理采样器分配一个位置值,这样的话我们能够在一个片段着色器中设置多个纹理。一个纹理的位置值通常称为一个纹理单元(Texture Unit)。一个纹理的默认纹理单元是0,它是默认的**纹理单元,所以教程前面部分我们没有分配一个位置值。
纹理单元的主要目的是让我们在着色器中可以使用多于一个的纹理。通过把纹理单元赋值给采样器,我们可以一次绑定多个纹理,只要我们首先**对应的纹理单元。就像glBindTexture一样,我们可以使用glActiveTexture**纹理单元,传入我们需要使用的纹理单元:
glActiveTexture(GL_TEXTURE0); // 在绑定纹理之前先**纹理单元
glBindTexture(GL_TEXTURE_2D, texture);
**纹理单元之后,接下来的glBindTexture函数调用会绑定这个纹理到当前**的纹理单元,纹理单元GL_TEXTURE0默认总是被**,所以我们在前面的例子里当我们使用glBindTexture的时候,无需**任何纹理单元。
OpenGL至少保证有16个纹理单元供你使用,也就是说你可以**从GL_TEXTURE0到GL_TEXTRUE15。它们都是按顺序定义的,所以我们也可以通过GL_TEXTURE0 + 8的方式获得GL_TEXTURE8,这在当我们需要循环一些纹理单元的时候会很有用。
对于多个纹理的场景,就不能像第一个示例那样使用默认的状态。先使用glUniform1i设置每个采样器的方式告诉OpenGL每个着色器采样器属于哪个纹理单元(只需要设置一次即可)。还要改变一点渲染流程,先绑定两个纹理到对应的纹理单元,接着定义哪个uniform采样器对应哪个纹理单元。大致流程如下:
ourShader.use(); // 在**着色器前先设置uniform
glUniform1i(glGetUniformLocation(ourShader.ID, "texture1"), 0); // 手动设置
ourShader.setInt("texture2", 1); // 或者使用着色器类设置
while(...)
{
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, texture1);
glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_2D, texture2);
ourShader.use();
glBindVertexArray(VAO);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
}
5.参考
纹理设置文档:https://www.khronos.org/opengl/wiki/GLAPI/glTexParameter
纹理翻转:https://www.cnblogs.com/bokeofzp/p/5967512.html
另外,我的练习代码github地址为:https://github.com/gongjianbo/LearnTheOpenGL