OpenGL深入探索——纹理加载(ImageMagick)与贴图
转载自:第十六课 基本的纹理贴图
背景
纹理贴图就是将任意一种类型的图片应用到 3D 模型的一个或多个面上。图片(也可以称之为纹理)内容可以是任何东西,但是他们一般都是一些比如砖,叶子,地面等的图案,纹理贴图增加了场景的真实性。例如,对比下面的两幅图片。
为了实现纹理贴图,你需要进行三个步骤:将图片加载到 OpenGL 中;定义模型顶点的纹理坐标(以对其进行贴图);用纹理坐标对图片进行采样操作进而得到像素颜色。因为我们可能会对三角形进行缩放,旋转,平移,并最终投影到屏幕上,所以它最终显示到屏幕上的方式是多种多样的,并且由于相机参数的不同看起来也会有很大差异。GPU 需要做的就是让图片跟随着三角形的顶点而运动而使场景看上去真实可信。为了实现这些功能,开发者为每个顶点绑定一个纹理坐标。当 GPU 光栅化这些三角形的时候,它会对整个三角形表面的纹理坐标进行插值计算,之后在片元着色器中我们将这些纹理坐标映射到纹理上,这个行为被称为‘采样’。采样的结果是产生纹素(纹理中的),纹素通常都包含一种颜色,我们将这个从纹理中提取出来的颜色绘制到对应的屏幕像素上实现对模型的着色。在接下来的章节中将会看到一个纹理像素可以包含不同类型的数据,并且用于产生多种效果。
OpenGL 支持多种类型的纹理比如 1D,2D,3D,立方体纹理等等,不同类型的纹理可被用于不同的技术中。这里我们选择使用 2D 纹理,一个 2D 纹理可以有任意的高度和宽度,但必须是在规定的范围之内。用高度 / 宽度可以得到纹理的像素值。但是我们不会用这个高度值和宽度值来作为定点的纹理坐标,因为那样做局限性太大,因为如果用不同 width / height 的纹理替换原来的纹理时,我们就必须更新所有顶点的纹理坐标。理想的做法是只更新纹理但不更新纹理坐标,因而所有纹理坐标是被指定在 ‘纹理空间’(规范化的范围是[0,1])中的。这表示纹理坐标通常都是一个小数,让它乘以纹理的宽度 / 高度就得到了纹理中该像素的坐标。例如,如果一个纹理坐标为[0.5,0.1],纹理的宽度为 320,高度为 200,那么这个像素的坐标是(160,20)(0.5 320 = 160 , 0.1 * 200 = 20)。
通常习惯用法是使用 U 和 V 作为纹理空间的轴,U 对应于在 2D 笛卡尔坐标系中的 X 轴,V 则对应 Y 轴。OpenGL 认为 U 轴的方向是从左指向右,V 轴上的值从下指向上。如下图:
这幅图片表示纹理坐标系,你可以看见纹理坐标系的原点在左下角。U 轴向右逐渐变大,V 轴向上逐渐变大。现在考虑有一个三角形,且其纹理坐标如下定义:
假设我们在模型上应用了一个纹理,并且使用这些纹理坐标,我们得到了一个如上图所示的一个结果。现在这个三角形经过许多变换,在进行光栅化的时候,它看起来如下:
如你所看见的,纹理坐标作为定点的核心属性,在各种变换过程中并不改变。当对纹理进行纹理坐标插值时,大部分像素会得到与原来的图片中相同的纹理坐标(它们相对于顶点来说相对位置没有改变),并且因为三角形是不断运动的,所以贴在它上面的纹理也跟着做同样的运动。这意味着原来的三角形旋转,放大,缩小,纹理也紧跟着这样变换。注意也有改变纹理坐标的方法,这是为了在表面对纹理进行移动或其他处理,但是现在我们不讨论这一情况。
另一个与纹理相关的重要概念是 ‘过滤’。我们已经讨论过了如何把纹理坐标映射到一个纹素上。在纹理上纹素的位置总是被指定为整数,但是如果纹理坐标(记住纹理坐标总是在 0 到 1 之间)映射到纹理上的位置为(152.34,745.14)将会发生什么呢?最容易的想到的就是四舍五入到(152,745). 嗯,这种方法确实有效并且会提供一个精确的结果,不过在某些情况下这个效果不怎么好。一个更好的方法是取四个相邻的纹素位置如((152,745), (153,745), (152,744) 和(153,744)),在它们的颜色之间做线性插值。这个线性插值的结果一定要反应出每个纹素点与(152.34,745.14)之间的相对距离之间的关系。最靠近(152.34,745.14)的纹素对最终得到的结果影响最大,而远一点的坐标对最终得到的结果影响就小一些,这种办法看上去会比之前的那种方法更好。
这种确定最终纹素值的方法被称为过滤。那种简单的通过四舍五入的方式得到纹素的方法叫做 ‘nearest filtering’ ,而刚刚我们介绍的更复杂一点的方法叫做‘linear filtering’。‘nearest filtering’另外一个名字是‘point filtering’。OpenGL 支持多种类型的过滤,你可以选择这其中的一种。通常要得到更好的过滤结果需要更多的 GPU 运算资源,并且对帧速率有一定的影响。选择过滤方法应考虑过滤效果与目标平台的性能之间的平衡。
既然我们已经理解了纹理坐标的概念,现在让我们现在看看 OpenGL 中的纹理映射是如何完成的。要在 OpenGL 中进行纹理贴图,我们需要弄清楚这几个概念:纹理对象,纹理单元,采样器对象,采样器一致变量。
纹理对象包含它本身纹理图片的数据,比如纹素。纹理可以是不同的类型的(1D,2D等等),分别有不同维度。其底层数据类型有不同的格式(RGB,RGBA等等)。OpenGL 提供一个方法,用来指定数据和上述所有属性在内存中的开始地址,并将数据加载进入 GPU。也有很多其他的参数你可以控制,比如控制过滤类型等等。与顶点缓冲区对象类似,纹理对象也有一个句柄。在创建句柄和加载纹理数据和参数后,你可以通过将不同的纹理句柄绑定到 OpenGl 的状态机上来实现纹理的切换,这样你就不需要重新将数据加载进 GPU 中。而且从此,确保数据在渲染开始之前被及时的加载进入 GPU 就成为了 OpenGL 驱动程序的工作。
纹理对象并不是直接被绑定到着色器(实际采样的地方),而是被绑定到一个纹理单元,纹理单元的索引会被传递到着色器中。所以着色器是通过纹理单元来访问纹理对象。OpenGL 中通常可以有多个纹理单元,具体的个数取决于你显卡的性能。为了绑定一个纹理对象 A 到纹理单元 0,首先你需要确定纹理单元 0 被激活,之后再绑定纹理对象 A。你现在可以激活纹理单元 1 并给它绑定一个不同的(甚至是相同的)纹理对象。此时纹理单元 0 仍然保持与纹理对象 A 的绑定。
事实上每一个纹理单元可以同时绑定多个不同的纹理对象,前提是这几个纹理对象的类型是不同的。纹理对象的类型被称为纹理对象的 ‘target’。当你绑定一个纹理对象到一个纹理单元时,你可以为其指定 target(1D,2D等等)。所以你可以将纹理对象 A 的 target 设定为 1D ,纹理对象 B 的 target 可以设定为 2D,这样这两个纹理对象就可以绑定到同一个纹理单元。
采样操作通常发生在片元着色器的内部,并有一个特殊的函数来实现采样操作。采样函数需要知道纹理单元的句柄,因为着色器中可能会有多个纹理单元,OpenGL提供了一系列的 uniform 变量来存放从程序中传入的纹理单元变量,这些变量根据纹理的 target 不同分为:'sampler1D', 'sampler2D', 'sampler3D', 'samplerCube'等等。你可以按需要定义多个采样器一致变量,然后通过程序将每个纹理单元的值分配给每个一致变量。当你通过采样器变量来调用采样函数时,相应的纹理单元(和纹理对象)就会被使用。
最后一个概念就是采样器对象,不要将他与采样一致变量混淆!它们两个是完全不相关的东西,区别在于纹理对象既包含纹理数据,又包含设置采样操作的参数,这些参数是采样状态的一部分。然而你也可以自己建立一个采样器对象,并对它的采样参数进行设置,并把它绑定到纹理单元。这样做之后,之前在纹理对象中定义的采样状态将会被覆盖。这里先不用担心,因为现在我们不会用到采样器对象,但是我们需要知道它的存在。
下面的一幅图中总结了我们上面介绍的纹理的概念之间的关系:
代码
OpenGL 可以从内存中加载不同类型的纹理数据,但是却不提供将纹理文件加载到内存的功能(如PNG、JPG图片文件),我们可以使用一个扩展库来完成这些工作,这里我们使用 ImageMagick 来加载图片文件,它是一个免费的软件库,可以支持许多图片类型并且支持跨平台。
大部分处理纹理的操作都被封装在下面这个类:
-
(ogldev_texture.h:27)
-
class Texture
-
{
-
public:
-
Texture(GLenum TextureTarget, const std::string& FileName);
-
bool Load();
-
void Bind(GLenum TextureUnit);
-
};
-
(ogldev_texture.cpp:31)
-
try {
-
m_pImage = new Magick::Image(m_fileName);
-
m_pImage->write(&m_blob, "RGBA");
-
}
-
catch (Magick::Error& Error) {
-
std::cout << "Error loading texture '" <<m_fileName << "': " << Error.what() << std::endl;
-
return false;
-
}
-
(ogldev_texture.cpp:40)
-
glGenTextures(1,&m_textureObj);
-
(ogldev_texture.cpp:41)
-
glBindTexture(m_textureTarget,m_textureObj);
-
(ogldev_texture.cpp:42)
-
glTexImage2D(m_textureTarget, 0, GL_RGBA, m_pImage->columns(),m_pImage->rows(), 0, GL_RGBA, GL_UNSIGNED_BYTE, m_blob.data());
LOD
一个纹理对象可以包含同一个纹理的多个不同分辨率版本,这就是 mip-mapping。每一个 mip-map 有着一个不同的 LOD 值,从 0(代表最高的分辨率)开始,随分辨率降低 LOD 值增大
-
(ogldev_texture.cpp:43)
-
glTexParameterf(m_textureTarget, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
-
glTexParameterf(m_textureTarget, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameterf
-
(ogldev_texture.cpp:49)
-
void Texture::Bind(GLenum TextureUnit)
-
{
-
glActiveTexture(TextureUnit);
-
glBindTexture(m_textureTarget, m_textureObj);
-
}
-
(shader.vs)
-
#version 330
-
layout (location = 0) in vec3 Position;
-
layout (location = 1) in vec2 TexCoord;
-
uniform mat4 gWVP;
-
out vec2 TexCoord0;
-
void main()
-
{
-
gl_Position = gWVP * vec4(Position, 1.0);
-
TexCoord0 = TexCoord;
-
};
-
(shader.fs)
-
in vec2 TexCoord0;
-
out vec4 FragColor;
-
uniform sampler2D gSampler;
-
void main()
-
{
-
FragColor = texture2D(gSampler, TexCoord0.xy);
-
};
-
(tutorial16.cpp:128)
-
Vertex Vertices[4] = {
-
Vertex(Vector3f(-1.0f, -1.0f, 0.5773f), Vector2f(0.0f, 0.0f)),
-
Vertex(Vector3f(0.0f, -1.0f, -1.15475), Vector2f(0.5f, 0.0f)),
-
Vertex(Vector3f(1.0f, -1.0f, 0.5773f), Vector2f(1.0f, 0.0f)),
-
Vertex(Vector3f(0.0f, 1.0f, 0.0f), Vector2f(0.5f, 1.0f))
-
};
-
(tutorial16.cpp:80)
-
...
-
glEnableVertexAttribArray(1);
-
...
-
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), 0);
-
glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex), (constGLvoid*)12);
-
...
-
pTexture->Bind(GL_TEXTURE0);
-
...
-
glDisableVertexAttribArray(1);
-
(tutorial16.cpp:253)
-
glFrontFace(GL_CW);
-
glCullFace(GL_BACK);
-
glEnable(GL_CULL_FACE);
-
(tutorial16.cpp:262)
-
glUniform1i(gSampler,0);
-
(tutorial16.cpp:264)
-
pTexture = newTexture(GL_TEXTURE_2D, "test.png");
-
if(!pTexture->Load()) {
-
return 1;
-
}
文章来源: panda1234lee.blog.csdn.net,作者:panda1234lee,版权归原作者所有,如需转载,请联系作者。
原文链接:panda1234lee.blog.csdn.net/article/details/51745558
- 点赞
- 收藏
- 关注作者
评论(0)