OpenGL Texture
纹理(Texture)
纹理我们可以理解成是一张2D图片,给定一组纹理坐标,告诉GPU需要将图片放在哪个位置。同时如果位置上有像素点,则会覆盖上去,目前阶段,纹理就是无缝贴合在你给定的位置中。
对于一些精细的物体,需要对不同位置进行不同的贴图,从而到达模型精细真实的效果。
我们将用这张地板贴图,进行简单纹理演示。
纹理映射(Map)
我们需要给出一定的纹理坐标,让GPU知道纹理贴图应该贴在哪里,因此我们需要在顶点坐标数组中,再添加一组纹理坐标:
//position array
float positions[] = {
// positions // colors // texture coords
0.5f, 0.5f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, // top right
0.5f, -0.5f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0, 0.0f, // bottom right
-0.5f, -0.5f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, // bottom left
-0.5f, 0.5f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f // top left
};
其中,一般来说窗口左下角为纹理坐标的 (0,0),右上角为(1,1)。而不同类型的图片格式坐标体系是不同的,例如 png 格式的图片,是由上往下扫描,因此对于这类的图片格式,我们需要在使用前进行filp反转,这个后文会提及。
纹理环绕
通常,纹理坐标范围是在零到一之间,而OpenGL是允许超过区间的。现实中的情况也是如此,往往物体和贴图不能很完美的契合,因此我们还需要采用纹理环绕的方式,即扩充图片:
环绕参数 | 作用 |
---|---|
GL_REPEAT | 重复图像 |
GL_MIRRORED_REPEAT | 重复镜像图像 |
GL_CLAMP_TO_EDGE | 重复纹理图像边缘像素点 |
GL_CLAMP_TO_BORDER | 超出范围用指定颜色填充 |
在创建纹理对象时,这些参数都需要单独为每个坐标轴设置:
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_MIRRORED_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_MIRRORED_REPEAT);
第一个参数,设置为2D纹理图像;第二个参数为设置S轴环绕方式(S,T,R分别代表常见坐标轴中的xyz);第三个参数确定环绕方式为镜像重复。
如果第三个参数设为 GL_CLAMP_TO_BORDER,则需要在后面紧跟以下代码,告诉GPU填充颜色:
float borderColor[] = { 1.0f, 1.0f, 0.0f, 1.0f };
glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, borderColor);
纹理过滤(Filtering)
在实际应用中,图像或物体得到分辨率往往不是统一的,对应不同场景,同一图像物体的分辨率都可能发生变化,因此当分辨率不匹配时,我们需要采用纹理过滤的方式:
- GL_NEAREST 邻近过滤。其实就是常见处理图像中的临近插值算法,OpenGL会选择该像素点最近的纹理坐标的像素点颜色,作为填充。也因此,直接填充,导致图像会有较为明显的割裂感,往往在纹理缩小时使用。
- GL_LINEAR 线性过滤。即常见处理图像中的线性插值算法,会根据临近纹理坐标几种颜色的占比,进行计算,从而得出一个较为平均的颜色,因此在图像上较为平滑,适合在纹理被放大时使用。
加载图片
纹理的基础用法介绍完了,现在需要将纹理贴图载入进程序中,一般使用 stb_image.h 库,它可以加载当今流行的大部分图片格式,并且直接下载放在程序中即可使用。
Do this:
#define STB_IMAGE_IMPLEMENTATION
before you include this file in *one* C or C++ file to create the implementation.
上述为文件中提及,我们需要创建一个cpp文件,然后先定义 STB_IMAGE_IMPLIEMENTATTION 宏,再包含该头文件,即可使用。
#define STB_IMAGE_IMPLEMENTATION
#include"stb_image.h"
当需要载入png图像时,前文提过,坐标系的规定是不一样的,因此我们需要先反转,后调用load函数载入图像即可,(参数后文会提及):
stbi_set_flip_vertically_on_load(1);
m_LocalBuffer = stbi_load(m_FilePath.c_str(), &m_Width, &m_Height, &m_BPP, 4);
自定义纹理类
在使用着色器或是纹理时,我比较喜欢封装成类,然后再对类进行功能扩充和调整。首先定义一个Texture类:
class Texture {
private:
unsigned int m_Render_ID; //纹理ID
std::string m_FilePath; //纹理贴图路径
unsigned char* m_LocalBuffer; //纹理缓冲区
int m_Width, m_Height, m_BPP; //宽,高,通道数
public:
Texture(const std::string& file_path);
~Texture();
void Bind(unsigned int slot = 0) const;
void UnBind() const;
void SetParameteri(unsigned int target, unsigned int pname, int param);
inline int GetWidth() const { return m_Width; };
inline int GetHeight() const { return m_Height; };
inline unsigned int GetTextureID() const { return m_Render_ID; };
};
最关键的构造函数定义如下:
Texture::Texture(const std::string& file_path)
: m_Render_ID(0), m_FilePath(file_path), m_LocalBuffer(nullptr),
m_Width(0), m_Height(0), m_BPP(0)
{
stbi_set_flip_vertically_on_load(1);
m_LocalBuffer = stbi_load(m_FilePath.c_str(), &m_Width, &m_Height, &m_BPP, 4);
glGenTextures(1, &m_Render_ID);
glBindTexture(GL_TEXTURE_2D, m_Render_ID);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
if (m_LocalBuffer) {
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, m_Width, m_Height, 0, GL_RGBA, GL_UNSIGNED_BYTE, m_LocalBuffer);
glBindTexture(GL_TEXTURE_2D, 0);
}
else {
std::cout << "Failed to load texture" << std::endl;
}
stbi_image_free(m_LocalBuffer);
}
首先,我们构造类对象时,获取文件路径,确定反转图像同时载入图像。载入图像 stbi_load() 需要文件路径参数,纹理图像宽高文件通道和纹理贴图通道(此时贴图为RGBA4通道)。
与着色器一样,我们先生成纹理区,再绑定纹理区。并分别设置好参数:纹理放大缩小的处理方式以及S轴 T轴的环绕方式。
最后将载入的图像,传入纹理缓冲区中。
应用纹理
float positions[] = {
// positions // colors // texture coords
0.5f, 0.5f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, // top right
0.5f, -0.5f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0, 0.0f, // bottom right
-0.5f, -0.5f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, // bottom left
-0.5f, 0.5f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f // top left
};
顶点坐标如上,我们需要告诉GPU每个数组元素的意义:
//set mode in position
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
//set mode in color
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(3 * sizeof(float)));
glEnableVertexAttribArray(1);
//set mode in texture
glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 8 * (sizeof(float)), (void*)(6 * sizeof(float)));
glEnableVertexAttribArray(2);
对于位置信息,是前三个;对于颜色信息,从第四个开始计算,有三个元素;对于纹理信息,从第七个开始计算,有两个元素。
在这我载入两个纹理贴图,生成两个纹理对象,分别对应0和1:
// texture
Texture texture1("res\\texture\\img\\wall.png");
Texture texture2("res\\texture\\img\\awesomeface.png");
glUniform1i(shader.GetUniformLoaction("texture1"), 0);
glUniform1i(shader.GetUniformLoaction("texture2"), 1);
在主题循环中,我们需要将纹理缓冲区绑定给着色器,并输出出去:
shader.BindTexture(GL_TEXTURE_2D, texture1.GetTextureID());
shader.BindTexture(GL_TEXTURE_2D, texture2.GetTextureID());
shader.UseProgram();
texture1.Bind(0);
texture2.Bind(1);
glBindVertexArray(VAO);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, nullptr);
最终呈现结果: