OpenGL学习笔记6——贴图

  • 1 加载图片
  • 2 概念
  • 2.1 UV
  • 2.2 纹理过滤
  • 2.3 多级渐远纹理
  • 3 应用纹理
  • 3.1 设置uv信息
  • 3.2 修改着色器
  • 3.3 渲染贴图
  • 4 第二张纹理
  • 4.1 纹理单元
  • 4.2 指定两张纹理


过了一段时间没搞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);

运行结果:

opengl 地球 贴图 python opengl载入贴图_cpp


下面的数据分别是3行为一个像素,分辨是rgb的值。现在图片文件时读进来了没有问题。

2 概念

2.1 UV

就是顶点对应纹理的坐标。通过uv才能让贴图对应到我们想要的位置,uv的取值是0~1的,如果超过这个范围会怎么显示呢?会根据纹理环绕方式来决定。OpenGL提供了以下四种方式(直接拿LearnOpenGL的图)。

opengl 地球 贴图 python opengl载入贴图_图形学_02

  • 重复,就是重复图像(我猜直接取uv的小数和符号就可以实现这个了吧)。
  • 镜像,跟重复差不多,区别就是相邻的两个图是镜像的。
  • 重复边缘,重复边缘的颜色。
  • 指定颜色,指定为某一种颜色。

2.2 纹理过滤

贴图在极大多数情况下都不会正好的对应分辨率的显示出来,有时候会小些有时候会大些。怎么显示这些情况下的贴图,就用到了纹理过滤。(再次盗图)纹理过滤有多种方式,这里先说的是邻近过滤线性过滤。肉眼可见的区别邻近过滤更像是一个个分明的像素点,线性过滤则是有一定的模糊。所以邻近过滤更适合像素风格的画面,如果像素风格使用线性过滤反而会一片模糊。对应Unity中过滤模式的Point。

opengl 地球 贴图 python opengl载入贴图_opengl_03


本质区别就是邻近过滤取像素中点距离纹理坐标最接近颜色,线性过滤取周围像素的混合色。

2.3 多级渐远纹理

在纹理距离摄像头特别远(即纹理很小)的时候,直接对高清的纹理进行过滤的话效果会不太好。同时也会浪费很多性能。

这就出现了多级渐远纹理,就是生成几个分辨率更小的纹理,根据显示的距离选择合适的分辨率。典型的空间换取时间。

opengl 地球 贴图 python opengl载入贴图_opengl_04

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);

运行起来效果就如下了(自己瞎画的一个贴图)。

opengl 地球 贴图 python opengl载入贴图_opengl 地球 贴图 python_05


如果改下片元着色器,把顶点颜色和贴图颜色相乘。就会有如下的结果。

FragColor = texture(ourTexture, TexCoord) * Color;

opengl 地球 贴图 python opengl载入贴图_贴图_06


完整代码好多,放这里了。

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);

没有问题的话应该就能看到结果了。

opengl 地球 贴图 python opengl载入贴图_cpp_07


话说我这个笑脸的外面还有一圈白色,不知道是啥问题。教程的效果和这个稍有出入。