赞
踩
为了给我们的对象添加更多细节,我们可以使用每个顶点的颜色来创建一些有趣的图像。然而,为了获得真实感,我们必须有很多顶点,以便我们可以指定很多颜色。这会占用相当多的额外开销,因为每个模型都需很多的顶点,并且每个顶点还需要一个颜色属性。
纹理是用于向对象添加细节的 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 提供了更多选项:
每个选项都有不同的视觉输出。让我们看看这些在样本纹理图像上的样子:
上述每个选项都可以按坐标轴(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_NEAREST和GL_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); // 放大时线性过滤
想象一下,我们有一个大房间,里面有数千个物体,每个物体都有一个纹理。远处的物体与靠近观察者的物体具有相同的高分辨率纹理。由于对象距离很远并且可能只产生几个片段,OpenGL 很难从高分辨率纹理中检索其片段的正确颜色值,因为它必须为跨越大部分纹理的片段选择纹理颜色. 这将在小物体上产生可见的伪影,更不用说在小物体上使用高分辨率纹理会浪费内存带宽。
为了解决这个问题,OpenGL 使用了一个叫做 mipmap纹理图像,它就是一系列的纹理图像,每一张纹理图像是前一张的1/4。mipmaps 的想法很容易理解:在距离观察者一定的距离阈值,OpenGL 将使用最适合与对象距离的 mipmap 纹理。由于物体距离较远,用户不会注意到较小的分辨率。然后,OpenGL 能够对正确的纹素进行采样,并且在对那部分 mipmap 进行采样时所涉及的缓存内存更少。让我们仔细看看 mipmapped 纹理是什么样子的:
手动为每个纹理图像创建一组 mipmapped 纹理很麻烦,但幸运的是 OpenGL 能够在我们创建了纹理之后通过一次调用glGenerateMipmaps为我们完成所有工作 。
在渲染期间切换 mipmap 时,OpenGL 可能会显示一些伪像,例如两个 mipmap 层之间可见的锐利边缘。就像普通的纹理过滤一样,也可以使用NEAREST和LINEAR过滤在 mipmap 之间进行过滤,以便在 mipmap 之间切换。要指定 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);
这是一个包含相当多参数的大型函数,因此我们将逐步介绍它们:
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, <
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。