|
游戏中随处可见的纹理,其实就是许多二维的图片“粘贴”到具体的模型上。下图就可以称作纹理,要是对其再稍加处理,在四周进行连续化,使得许多张该图连接在一起时也不会有断裂感,就完全可以把这张图贴在地面上,当做地面纹理。
纹理
随便拿市面上一款游戏来看,地面纹理是肯定会运用上的。就像上图,如果不追求极致的地形,仅用一张纹理贴图,便可以在表达地面复杂情况的同时,节约大量的开销(一个石头模型,一张石头材质就是一次绘制调用DrawCall,上图一堆石头,要是真的一个一个渲染出来,是很消耗性能的。当然,也有一种方法能解决模型数量庞大开销大的问题,那就是实例化)。
地面运用纹理贴图的例子
1. 纹理的作用[1]
对于之前的光照模型试验,我们始终对一个物体用的是相同的颜色,因此在片段着色器中,对每个顶点都是处理成同一种颜色。真实世界中的物体颜色错综复杂,我们不可能通过手动的方式去给片段着色器设置每一个顶点相应的颜色,那是一个庞大的数量。所以需要把这个手动设置顶点颜色的过程自动化,因此纹理的作用就体现出来了。在OpenGL中,我们只需在片段着色器中定义一个2D采样器,就可以对相应的纹理进行采样,然后再把采样的结果设置到当前正在处理的顶点。
如下图的模型,其顶点数量是极其庞大的,通过手动去为每个顶点去设置颜色根本不实际。
通过纹理,就可以直接将模型上的顶点和纹理中的坐标映射起来,即UV Mapping,这也就是为什么会经常把纹理叫做纹理贴图的原因,因为纹理的作用就是根据映射,“粘贴”到模型表面。
模型中的每一个顶点都会映射到纹理中的一个UV坐标,然后通过采样器来对纹理中UV坐标的颜色值进行采样。在得到模型所有顶点的颜色值后,就可以通过重心坐标插值的方法将三角面中的所有点的颜色都计算出来。
模型中的顶点通过什么方式映射到纹理中的呢?
① 艺术家手动设置映射;②自动生成映射;③参数化设置映射;
所以纹理的作用可以说是为模型的每个顶点都自动采样上了颜色,通过插值为顶点组成的三角面内的坐标也计算出颜色,使得模型的颜色表现更加丰富。同时,纹理采样得到的顶点颜色值,也可以作为冯氏光照模型中漫反射系数的组成,体现出在漫反射下,人眼所能接收到的颜色。
2. 重心坐标插值
重心坐标 —— Barycentric Coordinates
如果只知道纹理映射得到顶点的颜色属性,然后直接渲染,那就只有顶点会有颜色。而对于一个模型来说,更需要表达的是面。在OpenGL中,我们只设置了四个点的顶点属性,并通过glDrawElements绘制矩形后,却会得到很平滑的颜色过渡,矩形面中间的那些坐标是如何计算出颜色的?那就需要用到重心坐标插值。
OpenGL通过重心坐标插值的方法计算面内坐标颜色
以三角形的颜色插值为例,已知三个顶点A、B、C的顶点颜色,就可以通过以下公式来得到三角面内任何一个点的颜色。
公式中的A、B、C分别是顶点颜色值。而α、β、γ则是通过相应的面积比算出,下图公式中,等号左边为待求重心坐标系数,右侧为系数对应坐标不相邻的三角形面积与总面积之比。
经推导,也可以通过坐标间加减法运算代替面积计算来得到重心坐标系数:
因此通过重心坐标插值的办法,就可以仅知道周围顶点颜色来计算它们构成的面内坐标对应的颜色。在图形学中,不仅可以对顶点的颜色进行插值,还可以对其他各种顶点属性进行插值,例如 Gouraud Shading (在顶点着色器实现光照模型),就可以通过直接插值顶点的颜色或插值顶点的法向量来进行实现,又或者是深度缓冲也需要用到重心坐标插值等等。但是要注意的是,在三维空间中映射二维纹理,在投影时不能保证其重心坐标仍保持不变,因此在三维空间进行重心坐标的计算时,也需要把坐标的第三维加入进来。
3. 纹理的采样
(1)高分辨率屏幕像素与低分辨率纹素
考虑一个这样的场景,一个1920 × 1080分辨率的屏幕,看一个只有200 × 200分辨率的纹理,会是什么样的效果。那就是多个屏幕像素采集同一个纹素,在使用 近似Nearest 处理时,就会产生如下效果:
在1920 × 1080分辨率的显示屏中,表示长有1920个单位,宽有1080个单位,这单位就是屏幕像素,简称像素pixel;也就是整个屏幕有1920 × 1080 = 2,073,600个像素。
在 200 × 200分辨率的纹理中,表示长有200个单位,宽由200个单位,这单位就是纹理像素,简称纹素texel;也就是整个纹理有 200 × 200 = 40000个纹素。
因为这种处理方法的思想是:屏幕像素采集到一个纹素,则该屏幕像素输出的颜色值就是该纹素的颜色值。这就导致了这种处理方法看到的就还是和原来差不多的图形,具有很明显的锯齿。那如果考虑插值的办法呢?
(2)双线性插值 Bilinear Interpolation
双线性插值就是通过采集当前屏幕像素周围的4个纹素进行水平和垂直的双线性插值得到最终的结果。
以2次水平插值1次垂直插值为例:红点为屏幕像素,带黑点的白色矩形为纹素。首先对上面两个纹素进行颜色的线性插值,然后再对下两个纹素进行颜色的线性插值,最后再将上述两个水平纹素线性插值得到的结果进行垂直的线性插值,即得到最终结果。同理,2次垂直插值和1次水平插值也能达到相同效果。
这里的双线性插值不是说只插值两次,而是说从两个维度进行插值,实际上进行了三次线性插值。对200 × 200纹理进行双线性插值后效果:
对于要更平滑的新效果,就可以使用双立方插值Bicubic,其原理和双线性插值类似,在屏幕像素采集到的纹素周围取16个纹素进行插值。得到更好的效果的同时但也提高了计算开销。
(3)低分辨率屏幕像素和高分辨率纹素
正常图片
图片缩小后产生摩尔纹
观察上面两个图片,两者是同一个图片,但是在正常和缩小情况下,却得到了两种不一样的观感。这就是因为在图片缩小后,图片的纹素数量仍保持不变,但是一个屏幕像素所包含的纹素数量就更多,在这种一对多的情况下,屏幕像素只能取其中一个纹素来代表它包含的一整块纹素区的颜色,从而产生走样,即失去大部分有效信息。这就是低分辨率屏幕像素采集高分辨率纹素会出现的问题,凡是采样频率低于信息频率,都会出现走样问题。
图片缩小后产生摩尔纹现象,是采样频率跟不上信息频率的结果,即屏幕像素数量低于纹素数量。同理,在显示屏内三维世界中,一张图片离我们的远近也能产生相同的结果,图片离我们远,相当于图片被缩小,因此也会产生摩尔纹现象。如下图所示,显示屏通过屏幕像素对纹理进行采样,当图片离我们很近时,1个像素只能采样到1个纹素,此时该像素就可以通过上面所说的三种纹理插值过滤方法进行处理。当图片离我们很远时,1个像素几乎包含了12个纹素,而像素的采样同样只能通过插值过滤方法进行处理,但相比于距离近时采样的纹理,1个像素所包含的信息就十分杂糅,也许是近似采样,也许是平均采样,但所采样到的信息就是没有图片在近处时采样的精确。
当这种1个屏幕像素包含非常多的纹素时,像素如何去对这些纹素进行采样处理,那就由不了我们了。如下图所示,在像素纹素一对多及其严重的区域,像素直接将所包含的纹素全部平均化了。
走样导致图片信息丢失
为什么人眼在看远处和近处图片时,不会出现摩尔纹现象?
按研究的说法,人眼的分辨率达到5.76亿像素,如果把人眼看成是正方形的屏幕,将会是 24,000 × 24,000分辨率的屏幕,即采样频率非常的高。 因此解决因采样频率差异而导致的摩尔纹问题,从像素方面考虑,就是换一个更高分辨率的显示器;而从纹素方面考虑,就是让纹素数量随着距离越远而逐渐减少,使得采样频率能尽可能与纹素频率接近。因此Mipmap技术就出现了。
(4)Mipmap 多级渐远纹理
既然屏幕像素在对远处纹理进行采样时,都陷入如何对包含的大量纹素进行合理处理时,那不如让屏幕像素在采集远处纹理时,也能像采集近处纹理一样,一个像素对应一个纹素。这样既能保留我们所指定的信息,又能减少各种纹理过滤插值的计算。多级渐远纹理 Mipmap 就是用来实现一个像素采样尽可能少的纹素。
为不同距离准备不同分辨率的纹理。纹理离视口越远,则mipmap取的层级就越高,mipmap层级越高,纹素就越少,这也就实现了纹素随距离变化,避免了采样频率和纹素频率差别过大的情况出现。
那对于一个已经生成mipmap的纹理,如何知道在哪个距离用哪一个层级的图片呢?
屏幕中的一个像素映射到纹理中,总不是一个规规整整的正方形,因为纹理是随着模型在三维空间中的变化而变化,而屏幕始终保持不变。如下图所示,当屏幕中的像素映射到纹理中,变成了不规则的四边形。现在这种情况已经是一个像素采样多个纹素了,肯定会产生走样现象,那如何确定现在这种情况要用哪一层级的贴图?
首先找到相邻的像素中心点,求出距离N,N的单位是纹素,即表示一个像素到另一个像素的中心点距离几个纹素。然后再通过这个距离N来构建一个以N为边长的正方形来近似替换当前的不规则像素(如图3)。这就能近似出当前一个像素包含多少个纹素。
如何计算两个像素中心点之间的距离?——> 像素映射到纹理中,则像素就会有其在纹理中的UV坐标,已知两点间的坐标,那两点间距离就很容易可以算出来了。
图1 —— 图2 —— 图3
再通过公式: Layer = log_{2}N
即可求出当前应该采集mipmap中哪一层纹理。例如当前算出N = 2,则通过公式得出应该采样的层数为1;正好第0层和第1层之间的同一像素内的纹素比为1:4。
还有一个问题要解决,当用公式算出来的层数不是整数怎么办。
(5)三次线性插值 Trilinear Interpolation
例如现在算出的层数为2.8层,三次线性插值的做法是:
分别取第2层和第3层进行 双线性插值Bilinear Interpolation ,然后再把第2层和第3层进行一次线性插值。
即 层内双线性插值 + 层间线性插值。共7次插值:第2层双线性插值(3次) + 第3层双线性插值(3次) + 第2层和第3层线性插值(1次)。
为一张纹理生成mipmap,会增加1/3的显存,但是减轻走样的同时也减少了计算量。
(6)各向异性过滤 Anisotropic Filtering
正如之前所说的,屏幕像素映射到三维空间中的纹理并不是正方形,而是形状不规则的四边形,这时候就要通过近似法去近似一个正方形的像素来计算其包含了多少纹素,从而计算mipmap的层数。如下图所示,很明显,计算出的近似像素和原像素差距过大,导致在mipmap中采样时也避免不了选错层数导致采样误差过大的情况。为什么又会出现这种情况?本质就是mipmap生成的每层都是原图的等比例缩小版,面对与原图比例不相等的像素映射,也只能用原图比例的近似像素进行计算。因此解决这个问题的突破口就是每层的缩小比例问题。
各向异性过滤,相比于mipmap只生成等比例缩小的图片(下图中对角线的图片),每层还多生成了竖直和水平方向上的缩小,当遇到不规则的像素映射时,就有更多的近似像素进行选择,每一个近似像素都可以对应到下图中的某一张纹理。虽然说对上面那个极其不规则还是对角线分布的像素映射优化作用不大,但是对于长方形的像素映射来说,会比mipmap有着更好的效果。
图4
多级渐远纹理和各向异性过滤的近似计算区别
在游戏中,各向异性过滤有2×/4×/8×/16× 的选项,也就是生成多少层各向异性纹理。由图4可知,无论各项异性过滤的选项有多高,最终所占显存都会收敛到原纹理的4倍。因此只要显存足够的情况下,各项异性过滤可以随便开,有多大就开多大。
4. 拓展:
UE的纹理流送 Texture Streaming
UE4中的纹理流送就是在一定程度上就是通过Mipmap去实现的。它有一个纹理流送池,在游戏运行中用来存储mipmap的某一层纹理。在实际应用中,会预先计算出一张纹理的mipmap,然后随着距离的远近,将mipmap的某一层纹理流送到纹理流送池中,从而减少显存的占用。具体参见:UE4官方文档:纹理流送
当纹理流送出现错误时,即距离视口非常近的模型却流送出mipmap中层级较高的纹理(分辨率较低的纹理),就会出现如下效果。蜡状感明显,其实就是纹理流送没反应过来,当重新从mipmap中计算出对应的层级纹理并取到流送池,再输出到视口,就可以变回正常的样子了。
自己做的游戏Demo —— 纹理流送错误
正常的纹理流送
5. 总结
纹理还有很多作用,这里只提到了比较基本的图片采样,即如何让一个物体模型显现出真实世界中不考虑环境的颜色。此外,还有环境光贴图、天空盒、天空球贴图等等,都与纹理息息相关。下一次,我们要通过OpenGL来实现纹理的应用,封装一个Texture类。
参考
- ^闫令琪 Games101https://www.bilibili.com/video/BV1X7411F744?p=8&vd_source=f71d0ee35208236308b8150f67209c92
|
本帖子中包含更多资源
您需要 登录 才可以下载或查看,没有账号?立即注册
×
|