欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

LearnOpenGL学习笔记:纹理

程序员文章站 2022-07-16 21:26:26
...

( 本文对应学习章节: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)。

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类和图片路径我都改了,这里只做参考。

LearnOpenGL学习笔记:纹理 LearnOpenGL学习笔记:纹理

#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