OpenGL学习 (三)
本节主要涉及纹理方面的知识。
纹理坐标
纹理坐标在x和y轴上,范围为0到1之间(注意我们使用的是2D纹理图像)。使用纹理坐标获取纹理颜色叫做采样(Sampling)。纹理坐标起始于(0, 0),也就是纹理图片的左下角,终始于(1, 1),即纹理图片的右上角。上面的图片展示了我们是如何把纹理坐标映射到三角形上的。纹理环绕方式
环绕方式 | 描述 |
---|---|
GL_REPEAT | 对纹理的默认行为。重复纹理图像。 |
GL_MIRRORED_REPEAT | 和GL_REPEAT一样,但每次重复图片是镜像放置的。 |
GL_CLAMP_TO_EDGE | 纹理坐标会被约束在0到1之间,超出的部分会重复纹理坐标的边缘,产生一种边缘被拉伸的效果。 |
GL_CLAMP_TO_BORDER | 超出的坐标为用户指定的边缘颜色。 |
当纹理坐标超出默认范围时,每个选项都有不同的视觉效果输出。我们来看看这些纹理图像的例子:
前面提到的每个选项都可以使用glTexParameter*函数对单独的一个坐标轴设置(s、t(如果是使用3D纹理那么还有一个r)它们和x、y、z是等价的):1 | glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_MIRRORED_REPEAT); |
纹理过滤
纹理贴到三维图形上时,纹理与三维图形尺寸可能会不一致。一个像素一般不会正好对应于一个纹素(texel),所以像素的颜色无法直接得到 (可能一个像素会对应多个纹素此时看起来会模糊,也可能一个纹素对应多个像素,这样看起来采样不够会有锯齿),需要经过一定的运算,这个过程就是纹理过滤。
OpenGL中纹理过滤的选项
GL_NEAREST
(也叫邻近过滤,Nearest Neighbor Filtering)是OpenGL默认的纹理过滤方式。当设置为GL_NEAREST
的时候,OpenGL会选择中心点最接近纹理坐标的那个像素。下图中你可以看到四个像素,加号代表纹理坐标。左上角那个纹理像素的中心距离纹理坐标最近,所以它会被选择为样本颜色:
当进行放大(Magnify)和缩小(Minify)操作的时候可以设置纹理过滤的选项,比如你可以在纹理被缩小的时候使用邻近过滤,被放大时使用线性过滤。我们需要使用glTexParameter*
函数为放大和缩小指定过滤方式。这段代码看起来会和纹理环绕方式的设置很相似:
1 | glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); |
多级渐远纹理
多级渐远纹理(Mipmap),它简单来说就是一系列的纹理图像,后一个纹理图像是前一个的二分之一。多级渐远纹理背后的理念很简单:距观察者的距离超过一定的阈值,OpenGL会使用不同的多级渐远纹理,即最适合物体的距离的那个。由于距离远,解析度不高也不会被用户注意到。同时,多级渐远纹理另一加分之处是它的性能非常好,它消耗的存储量仅仅比原图多\(\frac{1}{3}\)。
如上图所示Mipmap会创建logn张贴图,分别在摄像机处于不同远近位置的时候播放相应分辨率的贴图。手工为每个纹理图像创建一系列多级渐远纹理很麻烦,OpenGL有一个glGenerateMipmaps
函数,在创建完一个纹理后调用它OpenGL就会承担接下来的所有工作了。
在渲染中切换多级渐远纹理级别(Level)时,OpenGL在两个不同级别的多级渐远纹理层之间会产生不真实的生硬边界。就像普通的纹理过滤一样,切换多级渐远纹理级别时你也可以在两个不同多级渐远纹理级别之间使用NEAREST和LINEAR过滤。为了指定不同多级渐远纹理级别之间的过滤方式,你可以使用下面四个选项中的一个代替原有的过滤方式:
过滤方式 | 描述 |
---|---|
GL_NEAREST_MIPMAP_NEAREST | 使用最邻近的多级渐远纹理来匹配像素大小,并使用邻近插值进行纹理采样。 |
GL_LINEAR_MIPMAP_NEAREST | 使用最邻近的多级渐远纹理级别,并使用线性插值进行采样 |
GL_NEAREST_MIPMAP_LINEAR | 在两个最匹配像素大小的多级渐远纹理之间进行线性插值,使用邻近插值进行采样 |
GL_LINEAR_MIPMAP_LINEAR | 在两个邻近的多级渐远纹理之间使用线性插值,并使用线性插值进行采样。 |
就像纹理过滤一样,我们可以使用glTexParameteri
将过滤方式设置为前面四种提到的方法之一:
1 | glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR); |
生成纹理
1 | //和之前生成的OpenGL对象一样,纹理也是使用ID引用的 |
glGenTextures
函数首先需要输入生成纹理的数量,然后把它们储存在第二个参数的unsigned int数组中(我们的例子中只是单独的一个unsigned int),就像其他对象一样,我们需要绑定它,让之后任何的纹理指令都可以配置当前绑定的纹理:
1 | glBindTexture(GL_TEXTURE_2D, texture); |
glTexImage2D
参数详解:
- 第一个参数指定了纹理目标(Target)。设置为GL_TEXTURE_2D意味着会生成与当前绑定的纹理对象在同一个目标上的纹理(任何绑定到GL_TEXTURE_1D和GL_TEXTURE_3D的纹理不会受到影响,就是要和上面glBindTexture的第一个参数相同才会奏效)。
- 第二个参数为纹理指定多级渐远纹理的级别,如果你希望单独手动设置每个多级渐远纹理的级别的话。这里我们填0,也就是基本级别。
- 第三个参数告诉OpenGL我们希望把纹理储存为何种格式。我们的图像只有RGB值,因此我们也把纹理储存为RGB值。
- 第四个和第五个参数设置最终的纹理的宽度和高度。我们之前加载图像的时候储存了它们,所以我们使用对应的变量。
- 下个参数应该总是被设为0(历史遗留的问题)
- 第七第八个参数定义了源图的格式和数据类型。我们使用RGB值加载这个图像,并把它们储存为char(byte)数组,我们将会传入对应值。
- 最后一个参数是真正的图像数据。
当调用glTexImage2D时,当前绑定的纹理对象就会被附加上纹理图像。然而,目前只有基本级别(Base-level)的纹理图像被加载了,如果要使用多级渐远纹理,我们必须手动设置所有不同的图像(不断递增第二个参数)。或者,直接在生成纹理之后调用glGenerateMipmap。这会为当前绑定的纹理自动生成所有需要的多级渐远纹理。
生成一个纹理的过程应该看起来像这样:
1 | unsigned int texture; |
应用纹理
我们添加了一个额外的顶点属性,我们必须告诉OpenGL我们新的顶点格式:1 | //告诉OpenGL如何解析纹理数据,就和之前我们告知如何解析VBO的数据一样! |
绑定纹理,它会自动把纹理赋值给片段着色器的采样器:
1 | //每次使用不同的模型的时候会使用不同的贴图,因此在使用贴图之前你必须要先 |
GLSL中使用纹理数据
1 |
|
GLSL有一个供纹理对象使用的内建数据类型,叫做采样器(Sampler),它以纹理类型作为后缀,比如sampler1D、sampler3D,或在我们的例子中的sampler2D。
我们使用GLSL内建的texture
函数来采样纹理的颜色,它第一个参数是纹理采样器,第二个参数是对应的纹理坐标。texture
函数会使用之前设置的纹理参数对相应的颜色值进行采样。这个片段着色器的输出就是纹理的(插值)纹理坐标上的(过滤后的)颜色。
为什么上面的着色器代码中sampler2D变量是个uniform,我们却不用glUniform给它赋值?
一个纹理的位置值通常称为一个纹理单元(Texture Unit)。一个纹理的默认纹理单元是0,它是默认的激活纹理单元,所以如果只打算用这一张贴图我们不需要手动uniform。如果你想使用多张贴图,使用glUniform1i
,我们可以给纹理采样器分配一个位置值,这样的话我们能够在一个片段着色器中设置多个纹理。
通过把纹理单元赋值给采样器,我们可以一次绑定多个纹理,只要我们首先激活对应的纹理单元。就像glBindTexture一样,我们可以使用glActiveTexture激活纹理单元,传入我们需要使用的纹理单元:
1 | glActiveTexture(GL_TEXTURE0); // 在绑定纹理之前先激活纹理单元 |
OpenGL至少保证有16个纹理单元供你使用,也就是说你可以激活从GL_TEXTURE0到GL_TEXTRUE15。它们都是按顺序定义的,所以我们也可以通过GL_TEXTURE0 + 8的方式获得GL_TEXTURE8,这在当我们需要循环一些纹理单元的时候会很有用。
使用多个纹理单元流程
使用多个纹理单元(这里以两个单元为例)的写法,我们必须改变一点渲染流程,先绑定两个纹理到对应的纹理单元,然后定义哪个uniform采样器对应哪个纹理单元:
1)shader代码中增加一个sampler变量
1 |
|
2)生成纹理
1 | /*生成第一张纹理*/ |
3)通过使用glUniform1i设置每个采样器的方式告诉OpenGL每个采样器属于哪个纹理单元。我们只需要设置一次即可,所以这个会放在渲染循环的前面
1 | myShader->use(); // 别忘记在激活着色器前先设置uniform! |
4)应用纹理,绘制
1 | while(...) |
纹理总结
到这里我不妨大胆假设,由于OpenGL这种状态机的性质,在往后的一些内容中,凡是涉及到bind的,在这个状态机上应该都有相应的接口,因此可以继续想象在这张图上添加新的内容。
参考资料
LearnOpenGL CN