【OpenGL学习笔记④】——纹理贴图【SOIL2的配置 + 绘制木板 + 纹理环绕】
关键词:OpenGL、SOIL2的配置、绘制纹理、纹理环绕
🍻 国庆节快乐!
有了新装的正方形 ☁️
上一篇文章地址链接:【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)
,它以纹理类型作为后缀,比如 sampler1D
、sampler3D
,或在我们的例子中的 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.
🍻 国庆节快乐!
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)