前言
- 材质和光的相互作用很重要。
- VertexShader和FragmentShader。
- 纹理贴图Texture mapping。
一、在三角形中插值
- 为什么要在三角形内部插值?虽然我们的操作很多是在三角形顶点上进行计算的,但是对于三角形内部我们也希望每个像素点能得到一个值,以此实现一个平滑的过渡。
- 我们要插值什么内容?可以插值的内容很多,比如颜色、纹理坐标、法向量。颜色插值使得三角形内部不至于全黑,并且插值使得颜色有一个自然平滑的过渡。纹理坐标插值使得三角形内部每个像素点也能具有自己的纹理。法向量插值使得在Phong模型中我们可以计算三角形内部每个像素的颜色。
- 插值可以对三角形顶点的任意属性进行。
重心坐标
-
对于任意三角形ABC,如果存在一点D位于三角形ABC所处平面内,那么我们就可以把D(x,y)表示成三角形三个顶点坐标的线性组合,且三个系数之和为1。如果三个系数都非负,那么这个点比如在三角形内部。
-
根据上述内容,我们可以使用重心坐标(aef,bet,gama)来表示三角形内部任意一个顶点。条件:aef、bet、gama>0,(x,y)=aefA+betB+gama*C,aef+bet+gama=1。
-
三角形内部的点也包括三个顶点:
-
通过面积比计算重心坐标
-
三角形重心的计算
-
像素点属性的插值(先算出重心坐标,再用重心坐标插值)
-
投影之后点的重心坐标会发生改变,而实际情况发生在三维空间中,因此我们应该在三维空间中对像素点进行插值。比如在光栅化时,每一个像素都要比较覆盖了它的三角形的片段的深度,以此来进行深度测试。而很多像素都不是顶点,它们的深度信息从何而来?当然是插值,也就是用重心坐标表示,而这时都已经映射到二维平面空间了,怎么办?当然是对三角形顶点坐标进行逆变换,变换回三维空间中,然后计算重心坐标,最后在投影空间中插值出像素点的深度值。(要注意深度测试不是在世界坐标系中)
二、应用纹理
- 基于插值我们得到每个像素的纹理坐标,根据纹理坐标我们可以获取到像素的属性,这种属性可以是颜色、漫反射系数、镜面反射系数、法向量。
三、Texture Magnification
(1)纹理较小
如果纹理分辨率较小,应用到较大分辨率的平面上,会发生什么呢?
1.1 Nearest
当纹理放大时,也就是说纹理的分辨率不变,其中包含的像素点数目不变。但是纹理放大要贴到一个更大的物体上,导致纹理包含了更多的屏幕像素点,这些屏幕像素点太多了,采样就会出现像我的世界一样的效果,观察下图:
Nearset是一种纹理的过滤方式,它表示当屏幕上的像素点插值出来的纹理坐标为(x,y)时,找到纹理贴图中与(x,y)距离最近的像素点,把贴图中像素点的属性赋值给屏幕像素点。当纹理放大后,很可能纹理贴图中的像素点数目已经远小于屏幕像素点了,这将导致4、5、6甚至多个屏幕像素点匹配到一个纹理像素点。如果是采样颜色的化,如上图最左边所示,就会出现像素方风方格。这是由于多个相邻屏幕像素点采样到一个颜色值,一模一样的像素点就组成了一块块的像素风方格。
1.2 Bicubic
看起来Bilinear和Bicubic过滤方式实现的效果比Nearest好得多,那么我们如何实现Bilinear呢?我们根据像素的纹理坐标,找到它四周的纹理像素点,根据纹理坐标到纹理像素点之间水平和竖直的相对距离进行插值。插值计算过程如下图所示。如果纹理坐标位于四个点正中间,那么插值的属性值就等于四个点属性值的平均。这就是双线性插值(竖直和水平顺序任意)。
选取周围16个像素点进行竖直和水平插值,每次用4个做三次的插值,而非线性插值。运算量会增大,但是带来的效果较好。
(2)纹理较大
如果纹理分辨率较大,应用到较小分辨率的平面上,会发生什么呢?
走样
对下图中左边图像中的地板应用纹理,结果就是出现下图右边的情况:近处出现锯齿、远处出现摩尔纹。
由近及远,屏幕上一个像素所覆盖的纹理区域会越来越大,当到达地平线时,一个像素覆盖一大块的纹理区域。而我们要想采集这个像素的颜色值,我们会使用它的中心坐标进行采样,将一大块纹理区域的平均颜色值赋值给这个像素。
使用超采样技术进行反走样,如下图能得到一个较好的结果,但是会大幅增加算法的时间。
问题分析:当在远处时,一个像素点内部包含了一大块纹理区域,这一大块纹理区内颜色的变化是很快的,而我们只使用一个采样点区采样它,因此我们得到的颜色值是不足以代表这一整块区域的,也因此我们的采样频率远低于信号的频率,就会发生走样。当我们对每个像素内添加512个采样点时,我们增加了采样频率,我们以512个采样点采取到的平均颜色值作为像素点的颜色值即可正常采样(较高频率的使用了一大块纹理信息)。但是如果我们不想有这么大的开销呢?
如果我们能知道任何一个纹理区域内所有纹素的平均颜色值不就好了?这采样效率和一个采样点一样,而且查到的精度可能比512个采样点还高。这涉及范围查询,它保证可以查询任意大小区域。
四、 Mipmap
可以做近似的、正方形的范围查询,它的特点:
- fast快
- approx有误差
- square正方形
Mipmap其实就是一张图生成一系列图。当我们有一张纹理时,我们先生成纹理的所有Mipmap,过程如下图所示:
现在有了Mipmap,当纹理矩阵很远相对于像素的分辨率很高时,我们直接采样对应Mipmap纹理即可。那么我们如何知道采样第几层的Mipmap呢?
如上图所示,我们找到像素点上下左右四个邻近像素点,然后计算像素点纹理坐标和临近像素点纹理坐标的距离的最大值,我们定义它为L。当L为1时,我们直接采样原纹理图片即第0层纹理可;当L=2时,像素方格内包含了两个纹理点,因此我们使用预先生成的第1层纹理即可。第1层纹理已经将纹理中每两个颜色值做了平均,因此我们可以直接查询。易知,我们使用D=log2L层纹理采样像素中心点的纹理坐标即可。效果如下图所示:
可以看到,当我们使用整数层级时走样已经消失了,但随之而来一个问题,那就是纹理颜色变换的不连续,这是因为当距离一远就会使层级D+1或者D-1,我们直接换了一个分辨率更高或者更低的纹理区采样,导致颜色没有渐变和不连续,这该怎么解决呢?我们发现层级的变换不是连续的,因为我们的层级是整数。如果我们想要将D=1.8层显示出它自己的效果,即我们不想让它和D=2.4使用一样Mipmap,那该怎么办呢?
同样是进行插值,诸如上图,我们先在D=1层对像素纹理坐标进行双线性插值,采样出D=1层的纹理颜色。然后我们在D=2层也同样对像素纹理坐标进行双线性插值,得到D=2层的纹理坐标。然后我们再对D=1和D=2层采样到的纹理颜色进行插值,这三步插值(双线性插值+线性插值)我们叫它三线性插值。
到现在,当纹理分辨率过低时,不管纹理坐标是小数还是整数,我们都可以使用双线性插值获得一个连续的纹理颜色。当纹理分辨率过大时,不管算出来的D层级是不是整数,我们都可以使用三线性插值出一个连续的纹理颜色。这样无论是远还是近,无论分辨率高还是低,我们都会获得采样到连续变化的颜色,即不会再走样了。效果如下所示:
当我们将插值的Mipmap用在地板上时,又发生了一些问题,如下图所示,可以看到远处的像素点都被模糊了。
各向异性过滤。Mipmap就是把一种原始的图不断缩小,每次长宽各缩小一半。
Mipmap对纹理的压缩是图中对角线形式的,即在长和宽上实现均匀压缩。因此在上文中我们使用Mipmap时是讲纹理坐标覆盖的一块区域映射回了原图的一块矩形区域,但我们会发现很多纹理覆盖的区域都不是矩阵的,如下图所示:
如上图所示,可以看到很多纹理对于的纹理区域都不是矩形的,当我们使用方格矩阵Mipmap时,我们的方格会扩住一个更大的区域,即我们求的不是精确的覆盖的纹理区域,而是能覆盖它的一个大方格,因此这个平均会很模糊。当我们引入各向异性过滤时,我们不用受限于一个正方形区域,可以使用一个矩形即长宽不等比的区域区查询。看上个图,也可以看到各向异性过滤代价是Mipmap的三倍。EWA可以描述不等比的形状,但是会多次查询。各向异性使用更多的显存空间,但其实影响不大。