本篇博文讲解OpenGL纹理贴图
一.纹理
纹理:Texture,大部分情况下是一个2D图片(也有1D和3D的纹理)
可以想象纹理是一张绘有砖块的纸,贴到一个3D的房子上,这样房子看起来就像有砖墙外表了
如下512x512的砖墙图就是一个纹理:
接下来就学习怎么把这张纹理贴到之前绘制的三角形上
首先需要定义一组纹理坐标,指定三角形的每个顶点各自对应纹理的哪个部分
二.纹理坐标
2D纹理图像的纹理坐标在x和y轴上,范围(0,1),原点是左下角坐标(0, 0),右上角坐标是(1, 1)
使用纹理坐标获取纹理颜色叫做采样(采集片段颜色)
下图展示纹理坐标与三角形的映射:
我们只要给顶点着色器传递映射到三角形的三个纹理坐标就行了,
接下来它们会被传到片段着色器中,它会为每个片段进行纹理坐标的插值
纹理坐标:
float texCoords[] = {
0.0f, 0.0f, // 左下角
1.0f, 0.0f, // 右下角
0.5f, 1.0f // 上中
};
设置了纹理坐标后,
还要告诉OpenGL进行纹理采样的方式
这就是下面还需要继续配置的纹理环绕模式和纹理过滤方式
三.纹理环绕
上一节讲到,纹理坐标的范围是从(0,0)到(1,1)
超过这个范围的,OpenGL会默认重复这个纹理图像
其实OpenGL是由其他更多选择的,只不过需要我们进行配置:
环绕方式 |
描述 |
GL_REPEAT |
默认:重复纹理图像 |
GL_MIRRORED_REPEAT |
纹理图像镜像重复 |
GL_CLAMP_TO_EDGE |
纹理图像边缘重复 |
GL_CLAMP_TO_BORDER |
用户指定的边缘颜色填充 |
这些选项可以使用 glTexParameter() 函数对单独的坐标轴进行设置
2D纹理坐标轴:s、t
3D纹理坐标轴:s、t、r
示例:
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_MIRRORED_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_MIRRORED_REPEAT);
参数一:纹理目标
参数二:纹理坐标轴
参数三:环绕方式
如果是GL_CLAMP_TO_BORDER选项,还需要调用glTexParameterfv()函数指定一个边缘颜色:
float borderColor[] = { 1.0f, 1.0f, 0.0f, 1.0f };
glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, borderColor);
四.纹理过滤
纹理过滤可以理解成OpenGL在用纹理进行贴图时,对纹理像素进行采样的一种方式
纹理过滤有很多个选项,最重要的有两种:GL_NEAREST 和 GL_LINEAR
GL_NEAREST:邻近过滤(Nearest Neighbor Filtering),OpenGL默认的纹理过滤方式
OpenGL会选择中心点最接近纹理坐标的那个像素作为采样颜色
下图中有四个像素,加号代表纹理坐标,左上角的纹理像素中心离纹理坐标最近,所以它的像素颜色会被采样:
GL_LINEAR:线性过滤(Linear Filtering)
OpenGL会基于纹理坐标附近的纹理像素,计算出一个近似于这些纹理像素之间的颜色。
一个纹理像素的中心距离纹理坐标越近,那么这个纹理像素的颜色对最终的样本颜色的贡献越大。
下图中你可以看到返回的颜色是邻近像素的混合色:
在一个很大的物体上应用一张低分辨率的纹理,这两种纹理过滤方式有什么不同的效果,
看下面这张图:
GL_NEAREST:锐化的颗粒状图案,能够清晰看到组成纹理的像素
GL_LINEAR:平滑模糊化的图案,很难看出单个的纹理像素
纹理被缩小时使用邻近过滤
纹理被放大时使用线性过滤
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
五.多级渐远纹理
当对远近不同的多个物体进行纹理贴图时,远的物体就没有必要再使用正常分辨率的纹理进行贴图了
这么做不仅浪费内存,还影响OpenGL渲染性能,而且小物体上使用大分辨率纹理也会产生不真实感
针对这一点,OpenGL使用了一种叫做多级渐远纹理(Mipmap)的方式
这个概念如果基于官方文档的释义有点难以理解透彻
在解释多级渐元纹理先介绍一个图形图像领域里很常见的术语:图像金字塔
就是一张原始大小的图以一个scalar系数进行阶梯式缩小采样,创建不同scalar层级的图像,就叫图像金字塔
OpenGL多级渐远纹理模式里,这个scalar是1/2
创建一个纹理后,调用glGenerateMipmaps()函数,并传入绑定了纹理ID的OpenGL纹理类型,就可以开启这个纹理的多级渐远模式:
unsigned int texture;
glGenTextures(1, &texture);
glBindTexture(GL_TEXTURE_2D, texture);
...
glGenerateMipmap(GL_TEXTURE_2D);
OpenGL会根据物体的远近,从这个纹理的图像金字塔中选取一张分辨率最适合物体的纹理进行贴图
OpenGL多级渐远纹理示例:
图像金字塔创建和选取的细节我们都不需要关心,OpenGL会自动为我们处理好,我们需要做的仅仅是调用glGenerateMipmaps()
在渲染中切换多级渐远纹理级别(Level)时,OpenGL在两个不同级别的多级渐远纹理层之间会产生不真实的生硬边界。
就像普通的纹理过滤一样,切换多级渐远纹理级别时你也可以在两个不同多级渐远纹理级别之间使用NEAREST和LINEAR过滤。
为了指定不同多级渐远纹理级别之间的过滤方式,可以使用下面四个选项中的一个代替原有的过滤方式:
过滤方式 | 描述 |
GL_NEAREST_MIPMAP_NEAREST | 使用最邻近的多级渐远纹理来匹配像素大小,并使用邻近插值进行纹理采样 |
GL_LINEAR_MIPMAP_NEAREST | 使用最邻近的多级渐远纹理级别,并使用线性插值进行采样 |
GL_LINEAR_MIPMAP_NEAREST | 使用最邻近的多级渐远纹理级别,并使用线性插值进行采样 |
GL_LINEAR_MIPMAP_LINEAR | 在两个邻近的多级渐远纹理之间使用线性插值,并使用线性插值采样 |
多级渐远纹理只在纹理被缩小的情况下使用
纹理放大时使用多级渐远纹理不会有任何效果,并且会产生GL_INVALID_ENUM错误代码
六.加载和创建纹理
1.stb_image.h
纹理贴图需要将不同格式图片(png,jpg,bmp....)进行字节序列化,可以使用开源的stb_image.h库
与其说是库,其实就是一个.h文件,对各种格式图片进行二进制解析的函数声明和定义都在这个.h文件中
我们只需要将其下载放到inc中,然后在代码中define和include就行了
下载地址:stb/stb_image.h at master · nothings/stb · GitHub
代码加载:
(1)头文件加载
#define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h"
#define STB_IMAGE_IMPLEMENTATION:预处理器会修改头文件,让其只包含相关的函数定义源码,相当于将这个头文件变为一个 .cpp 文件了,代码中这句#define 要在 #include "stb_image.h" 前,要不然就会报如下错:
(2)代码中load图片:
int width, height, nrChannels;
unsigned char *data = stbi_load("container.jpg", &width, &height, &nrChannels, 0);
stbi_load()函数:
参数一:图片路径
参数二、三:图片宽高
参数四:颜色通道个数
2.生成纹理
(1) 创建纹理ID:
unsigned int texture;
glGenTextures(1, &texture);
(2) 绑定纹理ID到OpenGL:
glBindTexture(GL_TEXTURE_2D, texture);
(3) glTexImage2D()生成纹理:
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, data);
glGenerateMipmap(GL_TEXTURE_2D);
glTexImage2D()函数:
参数一:当前绑定到OpenGL的2D纹理
参数二:多级渐远纹理级别,如果单独手动设置,就填0
参数三:纹理被存储的格式
参数四、五:纹理宽高
参数六:设置为0,别问为什么,OpenGL历史遗留问题
参数七、八:源图格式和数据类型 (我们使用RGB值加载这个图像,并把它们储存为char(byte)数组)
参数九:真正的图像数据
调用glTexImage2D()后,当前绑定到OpenGL的纹理对象就会被附加上纹理图像
如果不设置多级渐远纹理,OpenGL就只会加载原始纹理图像
参数二可以让我们手动设置多级渐远纹理级别(也就是图像金字塔层级),也可以在外部调用glGenerateMipmap()单独设置让OpenGL自行配置层级
(4) 释放图像内存:
stbi_image_free(data);
(5) 生成一个纹理的总体过程:
//创建、绑定纹理
unsigned int texture;
glGenTextures(1, &texture);
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);
3.绘制纹理:
(1).定义添加了纹理坐标的顶点数据:
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 // 左上
};
(2).配置顶点属性
新的顶点格式:
顶点属性配置代码:
glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(6 * sizeof(float)));
glEnableVertexAttribArray(2);
(3)修改着色器代码
修改顶点着色器代码,增加纹理坐标
#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 = aTexCoord;
}
修改片段着色器代码,将顶点着色器中纹理坐标的输出变量TexCoord作为输入变量
#version 330 core
out vec4 FragColor;
in vec3 ourColor;
in vec2 TexCoord;
uniform sampler2D ourTexture;
void main()
{
FragColor = texture(ourTexture, TexCoord);
}
片段着色器里也应当能访问到纹理对象
Sample2D:GLSL的内建数据类型SampleXD,也叫作采样器,可以用于添加纹理对象到着色器
texture():GLSL内建函数,参数一是纹理采样器,参数二是纹理坐标
texture函数会使用之前设置的纹理参数对相应的颜色值进行采样
这个片段着色器的输出就是纹理插值过滤后的颜色
(4)绘制
绑定纹理,glDrawElements()绘制
glBindTexture(GL_TEXTURE_2D, texture);
glBindVertexArray(VAO);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
编译、运行:
修改片段着色器,将纹理颜色与顶点颜色混合:
FragColor = texture(ourTexture, TexCoord) * vec4(ourColor, 1.0);
七.混合纹理
1.纹理单元
木箱纹理贴图效果在上述过程中已经实现
其中有个细节,就是在片段着色器中我们为纹理对象定义了一个sampler2D类型的uniform变量:ourTexture
之前的博文中有讲到过,着色器中的uniform全局变量可以在代码中使用glUniformX()类型函数进行赋值
那么,我们就可以在片段着色器中定义多个纹理
这些纹理在片段着色器中的位置值(也可以理解成索引值)叫作纹理单元
纹理对象在绑定之前需要先调用glActiveTexture()进行激活
glActiveTexture(GL_TEXTURE0); // 在绑定纹理之前先激活纹理单元
glBindTexture(GL_TEXTURE_2D, texture);
OpenGL默认激活GL_TEXTURE0
因为之前的代码中只有一个纹理,所以我们并没有调用glActiveTexture()对纹理专门进行激活操作
OpenGL至少保证有16个纹理单元,也就是说可以激活从GL_TEXTURE0到GL_TEXTRUE15
它们都是按顺序定义的,所以我们也可以通过GL_TEXTURE0 + 8的方式获得GL_TEXTURE8
这在当我们需要循环一些纹理单元的时候会很有用
2.混合渲染两个纹理
两个纹理混合贴图,先对片段着色器代码进行修改:
#version 330 core
...
uniform sampler2D texture1;
uniform sampler2D texture2;
void main()
{
FragColor = mix(texture(texture1, TexCoord), texture(texture2, TexCoord), 0.2);
}
mix():GLSL内建函数,使用第三个参数加权进行线性插值
输出:参数1*(1-参数3) + 参数2*参数3
新增一个纹理后,除了片段着色器的代码需要进行修改外,还有渲染流程代码也需要进行相应更改
其中体现在两个地方:
(1).纹理对象绑定到OpenGL之前,先要对其进行激活
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, texture1);
glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_2D, texture2);
glBindVertexArray(VAO);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
(2).向OpenGL设置纹理采样器的纹理单元
保证每个uniform采样器对应着正确的纹理单元
// 不要忘记在设置uniform变量之前激活着色器程序!
ourShader.use();
// 手动设置
glUniform1i(glGetUniformLocation(ourShader.ID, "texture1"), 0);
// 或者使用我们前文中封装好的着色器类的api设置
ourShader.setInt("texture2", 1);
while(...)
{
[...]
}
另外还有一点需要注意的是
OpenGL要求y轴0.0坐标是在图片的底部的,但是图片的y轴0.0坐标通常在顶部
所以代码中还需要通过stb_image.h的api来讲图像进行y轴翻转:
stbi_set_flip_vertically_on_load(true);