🍻 国庆节快乐!



有了新装的正方形 ☁️

上一篇文章地址链接【OpenGL学习笔记】计算机图形学③——⭐着色器【GLSL Uniform 彩色三角形 变色正方形】⭐.
下一篇文章地址链接【OpenGL学习笔记⑤】——纹理变换【glm配置+两张图片交替渐变变换 + 纹理平移 + ⭐实现雪花飘落⭐】.
OpenGL总学习目录: https://blog.csdn.net/Wang_Dou_Dou_/article/details/121240714.


零、成果预览图:

           

  ◆ 说明:左图是一张简单的图片(纹理)。右图是通过一张图片经过 “镜像复制——纹理环绕” 的方式生成的。



一、SOIL2的配置:


  ● SOIL 是简易 OpenGL 图像库(Simple OpenGL Image Library)的缩写,它能帮我们读取图片并做相应的处理。

  ● 当前最新版的是 SOIL2,需要我们自己动手配置。推荐一篇SOIL2环境配置的文章,写得很详细:链接: 《(图文)SOIL2环境配置》



三、纹理坐标

  ● 装载各顶点位置的数组

/* 编写各顶点信息 */
GLfloat vertices_1[] =
{
	//position				// color				// texture coords(纹理坐标)
	0.5f, 0.5f, 0.0f,		0.0f, 0.0f, 0.0f,		1.0f, 1.0f,		// 右上顶点		编号0
	0.5f, -0.5f, 0.0f,		0.0f, 0.0f, 0.0f,		1.0f, 0.0f,		// 右下顶点		编号1
	-0.5f, -0.5f, 0.0f,		0.0f, 0.0f, 0.0f,		0.0f, 0.0f,		// 左下顶点		编号2
	-0.5f, 0.5f, 0.0f,		0.0f, 0.0f, 0.0f,		0.0f, 1.0f		// 左上顶点		编号3
};

在这里插入图片描述

  ● 在绘制纹理映射场景时,不仅要给每个顶点定义几何坐标(这里OpenGL用的是标准化设备坐标,如上图),而且也要定义OpenGL纹理坐标(如右下图)。经过多种变换后,几何坐标决定顶点在屏幕上绘制的位置,而纹理坐标决定纹理图像中的哪一个纹素赋予该顶点。并且顶点之间的纹理坐标插值与平滑着色插值方法相同。

在这里插入图片描述

  ● 纹理图像是方形数组,纹理坐标通常可定义成一、二、三或四维形式,称为 s,t,r和q坐标 ,分别对应于物体坐标的几何坐标(x, y, z, w)和其他坐标。首先,其中的q是一个缩放因子,相当于顶点坐标中的 w 。实际在纹理读取中的坐标应该分别是 s/q、t/q、r/q。默认情况下,q 是1.0。通常情况下貌似没什么用,但是在一些产生纹理坐标的高级算法比如阴影贴图中,比较有用。

  ● s、t、r 分别相当于普通坐标系中的 x、y、z 三个方向。(分别对应 glTexImage3D 中的参数width、height、depth)。所以,一维纹理常用 s 坐标表示,二维纹理常用 (s,t) 坐标表示。;因为我们要绘制的是 2D 图像,所以只需要用二维纹理 (s,t) 即可。所以在代码里面,“texture coords(纹理坐标)” 只有两列。

  ◆ 补充说明:一般的 Windows 电脑坐标系如左上图。可以看出它和 OpenGL 的纹理坐标是 “相反的” 。所以在通过 “计算机方式” 读取图像后,再通过 OpenGL 绘出时,会出现图像倒着的情况,后面我们将会对其进行处理。



四、改写顶点着色器

#version 330 core		// 3.30版本 
layout(location = 0) in vec3 position;			// 位置变量的顶点属性位置值为 0
layout(location = 1) in vec3 color;				// 颜色变量的顶点属性位置值为 1
layout(location = 2) in vec2 textureCoords;		// 纹理坐标只有两个浮点数 (s,t), 属性位置值为 2 
out vec3 ourColor;								
out vec2 ourTextureCoords;						// 将纹理坐标传到片元着色器
void main()
{
	gl_Position = vec4(position, 1.0f);			// 核心函数(位置信息赋值)
	// ourColor = color;
	ourTextureCoords = vec2(textureCoords.x, 1-textureCoords.y);
} 

  ◆ 补充说明
    ① 因为要翻转图片,所以不能简单地用 “ourTextureCoords = textureCoords.x;”。
    ② 注释掉颜色是因为,我们只需要绘制纹理,并不需要图像做 “画图” 处理。( 但因为我定义的 “Shader.h” 头文件中有对颜色的处理,所以在 vertices_1[] 数组中依旧要把颜色的值加上 )



五、改写片元着色器

#version 330 core				// 3.30版本
in vec3 ourColor;
in vec2 ourTextureCoords;		// 将纹理坐标引入(从顶点着色器过来的)
out vec4 FragColor;				// 输出是四个浮点数构成的一个向量 RGB+aerfa
uniform sampler2D ourTexture;	// 纹理对象引入(从主函数过来的)
void main()
{
	// FragColor = vec4(ourColor, 1.0f);
	FragColor = texture(ourTexture, ourTextureCoords);	// 采样获得纹理坐标
}

  ◆ 补充说明
    ① 顶点着色器传给片元着色器的只有纹理坐标,但还差纹理对象。但是我们怎样能把纹理对象传给片段着色器呢?GLSL 有一个供纹理对象使用的内建数据类型,叫做采样器(Sampler),它以纹理类型作为后缀,比如 sampler1Dsampler3D,或在我们的例子中的 sampler2D。我们可以简单声明一个 uniform sampler2D把一个纹理添加到片段着色器中,稍后我们会在主函数中把这个纹理对象赋值给这个 “ourTexture”。
    ② 我们使用 GLSL 内建的 texture()函数来采样纹理的颜色,它第一个参数是纹理采样器,第二个参数是对应的纹理坐标texture()函数会使用之前设置的 “纹理参数对” 相应的颜色值进行采样。这个片段着色器的输出就是纹理在 (插值) 纹理坐标上 (过滤后的) 颜色。



六、读取纹理

/* 读取纹理 + 生成原理 */
int width, height;
unsigned char* image = SOIL_load_image("T_image3.png", &width, &height, 0, SOIL_LOAD_RGB);	

  ◆ 补充说明一:函数首先需要输入图片文件的路径。然后需要两个 int 类型的指针作为第二个和第三个参数,SOIL 会分别返回图片的宽度和高度到其中。因为后面我们在生成纹理的时候会用图像的宽度和高度。第四个参数指定图片的通道 (Channel) 数量,但是这里我们只需留为 0 。最后一个参数告诉 SOIL 如何来加载图片,结果会储存为一个很大的 char/byte 数组。

  ◆ 补充说明二:图片放置位置,需要和 .cpp 源代码文件 放在同一目录。如下图所示:

在这里插入图片描述



七、生成纹理

  ● 和之前生成的 OpenGL 对象一样 (比如 VAO),纹理也是使用 ID 引用的。

GLuint texture;
glGenTextures(1, &texture);				
glBindTexture(GL_TEXTURE_2D, texture);

  ◆ 补充说明
    ① glGenTexture()函数中第一个参数为 1 的原因是,后面我们绑定的 VAO、VBO、EBO 的 ID 都是人为地设置的 1 。
    ② glGenTextures()函数:将一个命名的纹理绑定到一个纹理目标上。
    ③ 我们可以这样理解,GL_TEXTURE_1D, GL_TEXTURE_2D, GL_TEXTURE_3D等就是很多变量,当使用 glBindTexture()函数,我们就会使用一张纹理对这些变量进行赋值。这些函数里面的 GL_TEXTURE_2D 就等价与我们之前绑定的纹理,所以我们对 GL_TEXTURE_2D 的操作就会影响到之前的纹理,这和C++中的引用有点类似。



  ● 现在纹理已经绑定了,我们可以使用前面载入的图片数据生成一个纹理了。纹理可以通过glTexImage2D()来生成:

glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, image);
glGenerateMipmap(GL_TEXTURE_2D);	// 多层渐进纹理

  ◆ 补充说明一
    ① 第一个参数:指定了纹理目标。
    ② 第二个参数:为纹理指定多级渐远纹理的级别。这里我们填 0,也就是基本级别。(因为,我们还没有“摄像机”)
    ③ 第三个参数:告诉 OpenGL 我们希望把纹理储存为何种格式。
    ④ 第四个参数:设置最终纹理宽度。
    ⑤ 第五个参数:设置最终纹理高度。
    ⑥ 第六个参数:这个参数应该总是被设为0(历史遗留的问题)。
    ⑦ 第七个参数:定义了源图的格式。我们使用 RGBA 值加载这个图像.
    ⑧ 第八个参数:设置源图的数据类型。
    ⑨ 第九个参数:源图像数据。

  ◆ 补充说明二:如果要使用多级渐远纹理,我们需要生成纹理之后调用 glGenerateMipmap()。这会为当前绑定的纹理自动生成所有需要的多级渐远纹理。(多级纹理可以这样理解:当站在远处看图像时,很多像素点就可以“凑”成一个主要的像素点;当站在进除看图像时,像素点就需要“铺开”,让图像变得清晰)但是因为我们还没学“摄影机”,所以也可以不用做这不处理。而且如果要生成多级渐远纹理,则图像的宽和高都必须是 2 的次方,宽和高不一定相同,但其值必须是 2 的次方。

  ● 生成了纹理和相应的多级渐远纹理后,释放图像的内存并解绑纹理对象是一个很好的习惯。(就像我们在上一篇文章写 “Shader.h” 头文件时,在生成着色器程序后,就删除顶点着色器和片元着色器,因为它俩已经链接到了着色器程序中了,已经不再需要了。)

SOIL_free_image_data(image);


  ● 纹理坐标的范围通常是从 (0, 0) 到 (1, 1) ,那如果我们把纹理坐标设置在范围之外会发生什么?OpenGL 默认的行为是重复这个纹理图像,但 OpenGL 提供了更多的选择:

纹理环绕方式描述
GL_REPEAT对纹理的默认行为。重复纹理图像。
GL_MIRRORED_REPEAT和GL_REPEAT一样,但每次重复图片是镜像放置的。
GL_CLAMP_TO_EDGE纹理坐标会被约束在0到1之间,超出的部分会重复纹理坐标的边缘,产生一种边缘被拉伸的效果。
GL_CLAMP_TO_BORDER超出的坐标为用户指定的边缘颜色。

  每种选择的示意图:

在这里插入图片描述

  ● 纹理环绕方式的设置

glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);	// S 坐标(自配的 WRAP 选项 + S 纹理轴)
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);	// T 坐标


  ● 除了要设置纹理环绕方式外,还要设置纹理过滤方式:GL_NEAREST(邻近过滤) 产生了颗粒状的图案,我们能够清晰看到组成纹理的像素,而 GL_LINEAR(线性过滤) 能够产生更平滑的图案,很难看出单个的纹理像素。这是两种常用的过滤方式。
在这里插入图片描述

  ● 纹理过滤方式的设置

glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);		// 当纹理被放大时, 用 GL_LINEAR 方式过滤
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);		// 当纹理被缩小时, 用 GL_NEAREST 方式过滤


八、绘制纹理

  ● 显卡中有 N 个纹理单元(具体数目依赖你的显卡能力),每个纹理单元(GL_TEXTURE0、GL_TEXTURE1等)都有 GL_TEXTURE_1D、GL_TEXTURE_2D、GL_TEXTURE_3D 等纹理目标

  ● 纹理单元的主要目的是让我们在着色器中可以使用多于一个的纹理。通过把纹理单元赋值给采样器(即 Uniform 属性的 “ourTexture变量” ),我们可以一次绑定多个纹理。只要我们首先激活对应的纹理单元,就像glBindTexture()一样,我们可以使用glActiveTexture()来激活纹理单元,传入我们需要使用的纹理单元:

  ● 我们还要通过使用glUniform1i()设置每个采样器的方式告诉 OpenGL 每个着色器采样器属于哪个纹理单元。我们只需要设置一次即可,所以这个会放在渲染循环的前面。

glActiveTexture(GL_TEXTURE0);			// 激活 0 号纹理单元(这条语句可以省略,因为纹理单元GL_TEXTURE0默认总是被激活)。最多 16 个通道
glBindTexture(GL_TEXTURE_2D, texture);	// 绑定这个纹理到当前激活的纹理目标
int textureLocation = glGetUniformLocation(ourShader.Program, "ourTexture");	// 找到着色器中 uniform 属性的名为"ourTexture"的纹理的索引
glUniform1i(textureLocation, 0);		// 告诉 OpenGL 的着色器采样器属于哪个纹理单元			


九、完整代码(主函数)


  ● 头文件 Shader.h 依旧沿用上一篇的代码⭐着色器【GLSL Uniform 彩色三角形 变色正方形】⭐主函数如下

/* 引入相应的库 */		
#include <iostream>
using namespace std;
#define GLEW_STATIC	
#include"Shader.h"
#include"glew-2.2.0\include\GL\glew.h"					// 注:这一部分要根据个人情况进行设定
#include"glfw-3.3.4.bin.WIN32\include\GLFW\glfw3.h"
#include"SOIL2\include\stb_image.h"
#include"SOIL2\include\SOIL2.h"

int width, height;

//* (样例一:木板) 编写各顶点位置  + 读取纹理 */
GLfloat vertices_1[] =
{
	//position				// color				// texture coords(纹理坐标)
	0.5f, 0.5f, 0.0f,		0.0f, 0.0f, 0.0f,		1.0f, 1.0f,		// 右上顶点		编号0
	0.5f, -0.5f, 0.0f,		0.0f, 0.0f, 0.0f,		1.0f, 0.0f,		// 右下顶点		编号1
	-0.5f, -0.5f, 0.0f,		0.0f, 0.0f, 0.0f,		0.0f, 0.0f,		// 左下顶点		编号2
	-0.5f, 0.5f, 0.0f,		0.0f, 0.0f, 0.0f,		0.0f, 1.0f		// 左上顶点		编号3
};
unsigned char* image = SOIL_load_image("T_image4.png", &width, &height, 0, SOIL_LOAD_RGBA);			// 获取图片


* (样例二:4只猫) 编写各顶点位置  + 读取纹理 */			
//GLfloat vertices_1[] =
//{
//	//position				// color				// texture coords(纹理坐标)
//	1.0f, 1.0f, 0.0f,		0.0f, 0.0f, 0.0f,		2.0f, 2.0f,		// 右上顶点		编号0
//	1.0f, -1.0f, 0.0f,		0.0f, 0.0f, 0.0f,		2.0f, 0.0f,		// 右下顶点		编号1
//	-1.0f, -1.0f, 0.0f,		0.0f, 0.0f, 0.0f,		0.0f, 0.0f,		// 左下顶点		编号2
//	-1.0f, 1.0f, 0.0f,		0.0f, 0.0f, 0.0f,		0.0f, 2.0f		// 左上顶点		编号3
//};
//unsigned char* image = SOIL_load_image("T_image3.png", &width, &height, 0, SOIL_LOAD_RGBA);			// 获取图片


/* 四个顶点的连接信息给出来 */
GLuint indices_1[] =
{
	0, 1, 3,		// 序号为 0、1、3 的顶点组合成一个三角形
	1, 2, 3			// 序号为 1、2、3 的顶点组合成一个三角形
};

const GLint WIDTH = 600, HEIGHT = 600;

int main()
{
	glfwInit();
	GLFWwindow* window_1 = glfwCreateWindow(WIDTH, HEIGHT, "Learn OpenGL Texture test", nullptr, nullptr);
	int screenWidth_1, screenHeight_1;
	glfwGetFramebufferSize(window_1, &screenWidth_1, &screenHeight_1);
	cout << "screenWidth_1 = " << screenWidth_1 << ", screenHeight = " << screenHeight_1 << endl;
	glfwMakeContextCurrent(window_1);
	glewInit();

	/* 将我们自己设置的着色器文本传进来 */
	Shader ourShader = Shader("shader_v.txt", "shader_f.txt");		// 相对路径

	/* 设置顶点缓冲对象(VBO) + 设置顶点数组对象(VAO) + 索引缓冲对象(EBO)  */
	GLuint VAO, VBO, EBO;				
	glGenVertexArrays(1, &VAO);		
	glGenBuffers(1, &VBO);		
	glGenBuffers(1, &EBO);	
	glBindVertexArray(VAO);
	glBindBuffer(GL_ARRAY_BUFFER, VBO);	
	glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
	glBufferData(GL_ARRAY_BUFFER, sizeof(vertices_1), vertices_1, GL_STATIC_DRAW);	
	glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices_1), indices_1, GL_STATIC_DRAW); 

	/* 设置链接顶点属性 */
	glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 8*sizeof(GLfloat), (GLvoid*)0);
	glEnableVertexAttribArray(0);	// 通道 0 打开
	glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 8*sizeof(GLfloat), (GLvoid*)(3*sizeof(GLfloat)));
	glEnableVertexAttribArray(1);	// 通道 1 打开
	glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 8*sizeof(GLfloat), (GLvoid*)(6*sizeof(GLfloat)));
	glEnableVertexAttribArray(2);	// 通道 2 打开


	/* 生成纹理 */
	GLuint texture;
	glGenTextures(1, &texture);
	glBindTexture(GL_TEXTURE_2D, texture);
	glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, image);			// 读取图片信息
	glGenerateMipmap(GL_TEXTURE_2D);	// 多层渐进纹理
	SOIL_free_image_data(image);

	/* 纹理环绕方式 */
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_MIRRORED_REPEAT);	// S 坐标
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_MIRRORED_REPEAT);	// T 坐标

	/* 纹理过滤 */
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

	/* 纹理单元:下一节重点使用 */
	ourShader.Use();	// don't forget to activate/use the shader before setting uniforms!
	int textureLocation = glGetUniformLocation(ourShader.Program, "ourTexture");	// 找到着色器中 uniform 属性的名为"ourTexture"的纹理的索引
	glUniform1i(textureLocation, 0);		// 告诉 OpenGL 的着色器采样器属于哪个纹理单元	

	/* draw loop 画图循环 */
	while (!glfwWindowShouldClose(window_1))
	{
		/* 视口 + 时间 */
		glViewport(0, 0, screenWidth_1, screenHeight_1);
		glfwPollEvents();
		
		/* 渲染 + 清除颜色缓冲 */
		glClearColor(0.5f, 0.8f, 0.5f, 1.0f);
		glClear(GL_COLOR_BUFFER_BIT);

		/* 生成纹理 */
		glActiveTexture(GL_TEXTURE0);			// 激活 0 号纹理单元。最多 16 个通道
		glBindTexture(GL_TEXTURE_2D, texture);	// 绑定这个纹理到当前激活的纹理目标			

		/* 绘制图形 */
		ourShader.Use();										
		glBindVertexArray(VAO);									// 绑定 VAO
		glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);				// 绑定 EBO
		glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);	// 画两个三角形 从第0个顶点开始 一共画6次
		glBindVertexArray(0);									// 解绑定 VAO
		glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);				// 解绑定 EBO
		glBindTexture(GL_TEXTURE_2D, 0);						// 解绑定 纹理

		/* 交换缓冲 */
		glfwSwapBuffers(window_1);
	}

	/* 释放资源 */
	glDeleteVertexArrays(1, &VAO);	
	glDeleteBuffers(1, &VBO);
	glDeleteBuffers(1, &EBO);

	glfwTerminate();	// 结束
	return 0;
}

  ● 运行结果:(注:样例二也在代码中,把注释打开即可)

           



十、参考附录:

[1] 《LearnOpenGL CN —— 纹理》
链接: https://learnopengl-cn.github.io/01%20Getting%20started/06%20Textures/.

[2] 《OpenGL三维纹理坐标》
链接: http://blog.sina.com.cn/s/blog_687960370101gyh8.html.

[3] 《(图文)SOIL2环境配置(OpenGL)》
链接: https://blog.csdn.net/weixin_44165937/article/details/117261166.

[4] 《OpenGL纹理坐标 与 Cocos2d-x 纹理坐标》
链接: https://blog.csdn.net/wlk1229/article/details/85077819.

[5] 《【OpenGL】关于OpenGL中glBindTexture函数的理解》
链接: https://blog.csdn.net/u010029439/article/details/98500262.

上一篇文章地址链接【OpenGL学习笔记】计算机图形学③——⭐着色器【GLSL Uniform 彩色三角形 变色正方形】⭐.

下一篇文章地址链接【OpenGL学习笔记⑤】——纹理变换【glm配置+两张图片交替渐变变换 + 纹理平移 + ⭐实现雪花飘落⭐】.

OpenGL总学习目录: https://blog.csdn.net/Wang_Dou_Dou_/article/details/121240714.


🍻 国庆节快乐!

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐