OpenGL纹理

OpenGL纹理

为了给我们的对象添加更多细节,我们可以使用每个顶点的颜色来创建一些有趣的图像。然而,为了获得真实感,我们必须有很多顶点,以便我们可以指定很多颜色。这会占用相当多的额外开销,因为每个模型都需很多的顶点,并且每个顶点还需要一个颜色属性。

纹理是用于向对象添加细节的 2D 图像(甚至存在 1D 和 3D 纹理),把纹理想象成一张纸,上面有漂亮的砖块图像,整齐地折叠在你的 3D 房子上,这样你的房子看起来就像是石头外墙。因为我们可以在单个图像中插入很多细节,所以我们可以给对象提供非常详细的错觉,而无需指定额外的顶点。

为了将纹理映射到三角形中,我们需要告诉三角形的每个顶点它对应于纹理的哪个部分。因此,每个顶点应该有一个纹理坐标与它们相关联,指定从纹理图像的哪个部分进行采样。片段插值然后为其他片段做其余的工作。

纹理坐标范围在x和y轴从0到1(记住我们使用的是 2D 纹理图像)。使用纹理坐标检索纹理颜色称为采样. 纹理坐标从纹理图像的左下角(0,0)开始到纹理图像的右上角(1,1)。下图显示了我们如何将纹理坐标映射到三角形:
在这里插入图片描述

我们为三角形指定了 3 个纹理坐标点。我们希望三角形的左下角与纹理的左下角对应,因此我们使用(0,0)为三角形左下角顶点的纹理坐标。(1,0)为纹理坐标的右下侧。三角形的顶部应与纹理图像的顶部中心相对应,因此我们将(0.5,1.0)其作为其纹理坐标。我们只需要将 3 个纹理坐标传递给顶点着色器,然后顶点着色器将它们传递给片段着色器,片段着色器巧妙地为每个片段插入所有纹理坐标。

生成的纹理坐标将如下所示:

float texCoords[] = {
   
    0.0f, 0.0f,  // lower-left corner  
    1.0f, 0.0f,  // lower-right corner
    0.5f, 1.0f   // top-center corner
};

纹理映射

纹理坐标的范围通常从(0,0)到(1,1),但是如果我们指定超出此范围的坐标会发生什么?OpenGL 的默认行为是重复纹理图像(我们基本上忽略浮点纹理坐标的整数部分),但 OpenGL 提供了更多选项:

  • GL_REPEAT:纹理的默认行为,重复纹理图像。
  • GL_MIRRORED_REPEAT:与GL_REPEAT相同,但每次重复都会镜像图像。
  • GL_CLAMP_TO_EDGE:将坐标限制在0和1之间,超出的坐标重复绘制边缘的像素,导致边缘图案被拉伸。
  • GL_CLAMP_TO_BORDER:范围之外的坐标被绘制成用户指定的边框颜色。

每个选项都有不同的视觉输出。让我们看看这些在样本纹理图像上的样子:
在这里插入图片描述
上述每个选项都可以按坐标轴(s, t(r如果您使用 3D 纹理)等价于x, y, z)进行设置glTexParameteri函数:

glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_MIRRORED_REPEAT); // X坐标
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_MIRRORED_REPEAT); // Y坐标

第一个参数指定纹理目标;我们正在处理 2D 纹理,因此纹理目标是GL_TEXTURE_2D。第二个参数要求我们告诉我们要设置哪个选项以及哪个纹理轴;最后一个参数要求我们传入我们想要的纹理环绕模式,在这种情况下,OpenGL 将使用GL_MIRRORED_REPEAT在当前活动纹理上设置其纹理环绕选项。

如果我们选择GL_CLAMP_TO_BORDER选项,我们还应该指定边框颜色。glTexParameteri使用GL_TEXTURE_BORDER_COLOR作为其选项的函数,我们在其中传入边框颜色值的浮点数组:

float borderColor[] = {
    1.0f, 1.0f, 0.0f, 1.0f };
glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, borderColor);  

纹理过滤

纹理坐标采用浮点值的形式不依赖于分辨率,因此 OpenGL 必须计算出纹理像素(也称为纹素) 与纹理坐标之间的对应关系。如果您有一个非常大的对象和一个低分辨率的纹理图,这种操作变得尤为重要。OpenGL 有几个选项可进行纹理过滤,但现在我们将讨论最重要的选项:GL_NEARESTGL_LINEAR

GL_NEAREST(也称为最近点过滤)是OpenGL默认的纹理过滤方法。当设置为GL_NEAREST 时,OpenGL 选择中心最接近纹理坐标的纹素。您可以在下方看到 4 个像素,其中十字代表精确的纹理坐标。左上角纹素的中心最接近纹理坐标,因此被选为采样颜色:
在这里插入图片描述
GL_LINEAR(也称为(双)线性过滤) 从纹理坐标的相邻纹素中获取内插值,近似于纹素之间的颜色。从纹理坐标到纹素中心的距离越小,纹素颜色对采样颜色的贡献就越大。下面我们可以看到返回了相邻像素的混合颜色:
在这里插入图片描述
但是这样的纹理过滤方式的视觉效果如何呢?让我们看看在大对象上使用低分辨率纹理时这些方法是如何工作的:
在这里插入图片描述
GL_NEAREST产生块状图案,我们可以清楚地看到形成纹理的像素,而GL_LINEAR产生更平滑的图案,其中单个像素不太明显,GL_LINEAR产生更逼真的输出。

可以设置纹理过滤 放大 和 缩小操作(向上或向下缩放时),因此您可以在纹理向下缩放时使用最近邻过滤和对向上缩放的纹理使用线性过滤。因此,我们必须通过以下方式为两个选项指定过滤方法glTexParameteri参数:

glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);  // 缩小时最近邻过滤
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); // 放大时线性过滤

Mipmap多级渐进纹理

想象一下,我们有一个大房间,里面有数千个物体,每个物体都有一个纹理。远处的物体与靠近观察者的物体具有相同的高分辨率纹理。由于对象距离很远并且可能只产生几个片段,OpenGL 很难从高分辨率纹理中检索其片段的正确颜色值,因为它必须为跨越大部分纹理的片段选择纹理颜色. 这将在小物体上产生可见的伪影,更不用说在小物体上使用高分辨率纹理会浪费内存带宽。

为了解决这个问题,OpenGL 使用了一个叫做 mipmap纹理图像,它就是一系列的纹理图像,每一张纹理图像是前一张的1/4。mipmaps 的想法很容易理解:在距离观察者一定的距离阈值,OpenGL 将使用最适合与对象距离的 mipmap 纹理。由于物体距离较远,用户不会注意到较小的分辨率。然后,OpenGL 能够对正确的纹素进行采样,并且在对那部分 mipmap 进行采样时所涉及的缓存内存更少。让我们仔细看看 mipmapped 纹理是什么样子的:
在这里插入图片描述
手动为每个纹理图像创建一组 mipmapped 纹理很麻烦,但幸运的是 OpenGL 能够在我们创建了纹理之后通过一次调用glGenerateMipmaps为我们完成所有工作 。

在渲染期间切换 mipmap 时,OpenGL 可能会显示一些伪像,例如两个 mipmap 层之间可见的锐利边缘。就像普通的纹理过滤一样,也可以使用NEAREST和LINEAR过滤在 mipmap 之间进行过滤,以便在 mipmap 之间切换。要指定 mipmap 之间的过滤方法,我们可以使用以下四个选项之一替换原始过滤方法:

  • GL_NEAREST_MIPMAP_NEAREST:采用最近的 mipmap 来匹配像素大小,并使用最近邻插值进行纹理采样。
  • GL_LINEAR_MIPMAP_NEAREST:采用最近的 mipmap 并使用线性插值对该级别进行采样。
  • GL_NEAREST_MIPMAP_LINEAR:在最接近像素大小的两个 mipmap 之间进行线性插值,并通过最近邻插值对插值级别进行采样。
  • GL_LINEAR_MIPMAP_LINEAR:在两个最接近的 mipmap 之间进行线性插值,并通过线性插值对插值级别进行采样。

就像纹理过滤一样,我们可以使用以下方法将过滤方法glTexParameteri设置为上述 4 种方法之一 :

glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

一个常见的错误是将 mipmap 过滤选项之一设置为放大过滤器。这没有任何影响,因为 mipmap 主要用于纹理缩小时:纹理放大不使用 mipmap,并为其提供 mipmap 过滤选项将生成 OpenGL GL_INVALID_ENUM错误代码。

加载和创建纹理

要实际使用纹理,我们需要做的第一件事是将它们加载到我们的应用程序中。纹理图像可以存储为几十种文件格式,每种格式都有自己的结构和数据顺序,那么我们如何在应用程序中获取这些图像呢?一个解决方案是选择一种我们想要使用的文件格式,比如说.PNG编写我们自己的图像加载器来将图像格式转换为一个大的字节数组。虽然编写自己的图像加载器并不难,但仍然很麻烦,如果您想支持更多文件格式怎么办?然后,您必须为要支持的每种格式编写一个图像加载器。另一种解决方案,是使用支持多种流行格式并为我们完成所有繁重工作的图像加载库。
stb_image.h是Sean Barrett 的一个非常流行的单头图像加载库,它能够加载最流行的文件格式,并且很容易集成到您的项目中。只需下载单个头文件,将其添加到您的项目中,然后使用以下代码创建一个额外的 C++ 文件:

#define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h"

通过定义STB_IMAGE_IMPLEMENTATION预处理器修改头文件,使其只包含相关的定义源代码,有效地将头文件转换为.cpp文件。现在只需stb_image.h在程序中的某处包含并编译。

对于以下纹理部分,我们将使用木制容器的图像。要使用stb_image.h我们加载图像,我们使用它的stbi_load 功能:

int width, height, nrChannels;
unsigned char *data = stbi_load("container.jpg", &width, &height, &nrChannels, 0); 

该函数首先将图像文件的位置作为输入。然后用三个int作为其第二,第三和第四参数,stb_image.h将产生的图像的补宽度,高度和数量的颜色通道。我们需要图像的宽度和高度来稍后生成纹理。

生成纹理

与之前 OpenGL 中的任何对象一样,纹理使用 ID 引用;让我们创建一个:

unsigned int texture;
glGenTextures(1, &texture);  

这纹理函数首先将我们想要生成多少纹理作为输入,并将它们存储在unsigned int作为第二个参数给出的数组中(在我们的例子中只有一个unsigned int)。就像其他对象一样,我们需要绑定它,以便任何后续的纹理命令都将配置当前绑定的纹理:

glBindTexture(GL_TEXTURE_2D, texture);  

现在纹理已绑定,我们可以开始使用之前加载的图像数据生成纹理,纹理是用glTexImage2D

glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, data);
glGenerateMipmap(GL_TEXTURE_2D);

这是一个包含相当多参数的大型函数,因此我们将逐步介绍它们:

  • 第一个参数指定纹理目标;将此设置为GL_TEXTURE_2D意味着此操作将在同一目标的当前绑定纹理对象上生成纹理(因此任何绑定到目标GL_TEXTURE_1D或GL_TEXTURE_3D 的纹理都不会受到影响)。
  • 如果您想手动设置每个 mipmap 级别,第二个参数指定我们要为其创建纹理的 mipmap 级别,但我们将其保留在基本级别,即0.
  • 第三个参数告诉 OpenGL 我们想要以什么样的格式存储纹理。我们的图像只有RGB值,所以我们也将存储带有RGB值的纹理。
  • 第 4 个和第 5 个参数设置生成的纹理的宽度和高度。我们在加载图像时预先存储了这些,因此我们将使用相应的变量。
  • 下一个参数总是0(一些遗留的东西)。
  • 第 7 个和第 8 个参数指定源图像的格式和数据类型。我们用RGB值加载图像并将它们存储为chars(字节),因此我们将传入相应的值。
  • 最后一个参数是实际的图像数据。

glTexImage2D被调用,当前绑定的纹理对象现在附加了纹理图像。然而,目前它只加载了纹理图像的基础级别,如果我们想使用 mipmap,,我们可以调用glGenerateMipmap生成纹理后,手动指定所有不同的图像(通过不断增加第二个参数)。这将自动为当前绑定的纹理生成所有必需的 mipmap。

在我们完成生成纹理及其相应的 mipmap 之后,释放图像内存是一个很好的做法:

stbi_image_free(data);

因此,生成纹理的整个过程如下所示:

unsigned int texture;
glGenTextures(1, &texture);
glBindTexture(GL_TEXTURE_2D, texture);
// set the texture wrapping/filtering options (on the currently bound texture object)
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);
// load and generate the texture
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);

应用纹理

对于接下来的部分,我们将使用绘制的矩形形状应用纹理。我们需要通知 OpenGL 如何对纹理进行采样,因此我们必须使用纹理坐标更新顶点数据:

float vertices[] = {
   
    // 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.0f, 0.0f,   // bottom right
    -0.5f, -0.5f, 0.0f,   0.0f, 0.0f, 1.0f,   
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值