纹理
零: 首先我觉得需要提出来的是,为什么在OpenGL中uv坐标的范围始终是0-1呢,而不是实际图形的二维坐标。
- 纹理元素在表示纹理的数组中的二维下标(即它在位图中的二维坐标)称为纹理坐标,一般以字母表示为(u,v),也称为实际纹理坐标。假设位图的宽、高分别为w、h,显然,0 ≤ u ≤ w,0 ≤ v ≤ h
- 因为在一个图形显示系统中往往存在多幅不同的纹理,它们的宽、高也不尽相同,用实际纹理坐标表示纹理元素的位置在计算上很难统一,所以经常使用相对纹理坐标[设为(u′,v′)]代替实际纹理坐标,u′、v′分别是u、v所占宽、高的百分比:u′ = u / w,v′ = v / h
- 因此,在D3D中用两个0~1的浮点值(U,V)来设置一个点的纹理坐标,U是横轴、V是纵轴。纹理的左上角为(0,0),右下角为(1,1)。但在OpenGL中默认左下角为(0,0)。
一: 什么是纹理
- 纹理是一个2D图片(甚至也有1D和3D的纹理),它可以用来添加物体的细节;
- 你可以想象纹理是一张绘有砖块的纸,无缝折叠贴合到你的3D的房子上,这样你的房子看起来就像有砖墙外表了。因为我们可以在一张图片上插入非常多的细节,这样就可以让物体非常精细而不用指定额外的顶点。
- 为了能够把纹理映射(Map)到三角形上,我们需要指定三角形的每个顶点各自对应纹理的哪个部分。这样每个顶点就会关联着一个纹理坐标(Texture Coordinate),用来标明该从纹理图像的哪个部分采样(采集片段颜色)。之后在图形的其它片段上进行片段插值(Fragment Interpolation)。
- 关于纹理坐标:纹理坐标在x和y轴上,范围为0到1之间(注意我们使用的是2D纹理图像)。使用纹理坐标获取纹理颜色叫做采样(Sampling)。纹理坐标起始于(0, 0),也就是纹理图片的左下角,终始于(1, 1),即纹理图片的右上角。
- 我们只要给顶点着色器传递这每个顶点对应的纹理坐标就行了,接下来它们会被传片段着色器中,它会为每个片段进行纹理坐标的插值。
二: 纹理环绕方式
纹理坐标的范围通常是从(0, 0)到(1, 1),那如果我们把纹理坐标设置在范围之外会发生什么?OpenGL默认的行为是重复这个纹理图像(我们基本上忽略浮点纹理坐标的整数部分),但OpenGL提供了更多的选择。
三: 纹理过滤
- 纹理坐标不依赖于分辨率(Resolution),它可以是任意浮点值,所以OpenGL需要知道怎样将纹理像素(Texture Pixel,也叫Texel,译注1)映射到纹理坐标。当你有一个很大的物体但是纹理的分辨率很低的时候这就变得很重要了。
Texture Pixel也叫Texel,你可以想象你打开一张.jpg格式图片,不断放大你会发现它是由无数像素点组成的,这个点就是纹理像素;注意不要和纹理坐标搞混,纹理坐标是你给模型顶点设置的那个数组,OpenGL以这个顶点的纹理坐标数据去查找纹理图像上的像素,然后进行采样提取纹理像素的颜色。 - 纹理过滤有很多个选项,但是现在我们只讨论最重要的两种:GL_NEAREST和GL_LINEAR。
2.1 GL_NEAREST(也叫邻近过滤,Nearest Neighbor Filtering)是OpenGL默认的纹理过滤方式。当设置为GL_NEAREST的时候,OpenGL会选择中心点最接近纹理坐标的那个像素。
2.2 GL_LINEAR(也叫线性过滤,(Bi)linear Filtering)它会基于纹理坐标附近的纹理像素,计算出一个插值,近似出这些纹理像素之间的颜色。一个纹理像素的中心距离纹理坐标越近,那么这个纹理像素的颜色对最终的样本颜色的贡献越大。返回的颜色是邻近像素的混合色.
四: 加载与创建纹理
- 头文件:图像加载库,它能够加载大部分流行的文件格式,并且能够很简单得整合到你的工程之中
#define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h"
- 生成纹理:纹理也是使用ID引用的
2.1 当调用glTexImage2D时,当前绑定的纹理对象就会被附加上纹理图像
2.2 直接在生成纹理之后调用glGenerateMipmap。这会为当前绑定的纹理自动生成所有需要的多级渐远纹理。
五: 应用纹理
- 为了告知OpenGL如何采样纹理,所以我们必须使用纹理坐标更新顶点数据:
- 由于我们添加了一个额外的顶点属性,我们必须告诉OpenGL我们新的顶点格式。
- 接着我们需要调整顶点着色器使其能够接受顶点坐标为一个顶点属性,并把坐标传给片段着色器:
- 片段着色器应该接下来会把输出变量TexCoord作为输入变量。
- 片段着色器也应该能访问纹理对象,但是如何能把纹理对象传给片段着色器:也就是刚刚创建的纹理对象
5.1 GLSL有一个供纹理对象使用的内建数据类型,叫做采样器(Sampler),它以纹理类型作为后缀,比如sampler1D、sampler3D,或在我们的例子中的sampler2D。我们可以简单声明一个uniform sampler2D把一个纹理添加到片段着色器中,稍后我们会把纹理赋值给这个uniform。
5.2 在片段着色器里面:使用GLSL内建的texture函数来采样纹理的颜色,它第一个参数是纹理采样器,第二个参数是对应的纹理坐标(已经从顶点着色器中传给片段着色器了)。texture函数会使用之前设置的纹理参数对相应的颜色值进行采样。这个片段着色器的输出就是纹理的(插值)纹理坐标上的(过滤后的)颜色。
5.3 现在只剩下在调用glDrawElements之前绑定纹理了,它会自动把纹理赋值给片段着色器的采样器。
5.4 还可以把得到的纹理颜色与顶点颜色混合,来获得更有趣的效果。我们只需把纹理颜色与顶点颜色在片段着色器中相乘来混合二者的颜色(在片段着色器中)
六: 纹理单元
- 一个纹理的位置值通常称为一个纹理单元(Texture Unit)。一个纹理的默认纹理单元是0,它是默认的激活纹理单元,所以教程前面部分我们没有分配一个位置值。
- sampler2D变量是个uniform,我们却不用glUniform给它赋值。使用glUniform1i,我们可以给纹理采样器分配一个位置值,这样的话我们能够在一个片段着色器中设置多个纹理。
- 纹理单元的主要目的是让我们在着色器中可以使用多于一个的纹理。通过把纹理单元赋值给采样器,我们可以一次绑定多个纹理,只要我们首先激活对应的纹理单元。就像glBindTexture一样,我们可以使用glActiveTexture激活纹理单元,传入我们需要使用的纹理单元。
- 激活纹理单元之后,接下来的glBindTexture函数调用会绑定这个纹理到当前激活的纹理单元,纹理单元GL_TEXTURE0默认总是被激活,所以我们在前面的例子里当我们使用glBindTexture的时候,无需激活任何纹理单元。
- 为了使用第二个纹理(以及第一个),我们必须改变一点渲染流程,先绑定两个纹理到对应的纹理单元,然后定义哪个uniform采样器对应哪个纹理单元:通过使用glUniform1i设置每个采样器的方式告诉OpenGL每个着色器采样器属于哪个纹理单元。我们只需要设置一次即可,所以这个会放在渲染循环的前面:
七: 注意事项
- 纹理上下颠倒:这是因为OpenGL要求y轴0.0坐标是在图片的底部的,但是图片的y轴0.0坐标通常在顶部。很幸运,stb_image.h能够在图像加载时帮助我们翻转y轴,只需要在加载任何图像前加入以下语句即可:stbi_set_flip_vertically_on_load(true);
- 。。。
Either Excellent or Rusty