虽然是接着“游戏中还原真实世界”来写的,但是在做新选题时我逐渐意识到必须要补充一些图形学的简单概念才能继续讲大点的课题了,否则说到“云”、“雾”、“水体”、“阴影”等等这些根本就没法谈。
虽然仅仅介绍图形学没有那么多实际游戏的例子,会显得略微枯燥一些,但我觉得知道各种技术方案为什么被发明出来也是一件有趣的事情。
这里面我也基本不会介绍各种计算的过程或原理,主要是抛出各种“是什么”以及“为什么”。很多东西虽然实现复杂,但是目标其实挺简单的。
最后简单说一下,这个系列主要也是锻炼我把复杂东西说得简单有趣的能力,本身介绍的东西都不新鲜了;很多人即使不理解这些也以学习工具的方式用引擎做了一堆东西了——不过我觉得工具性的理解始终不如成系统的理解。
那么让我们从纹理(Texture )开始——有些应用场合也被称为贴图,两者虽然不完全一样但也不必较真:
(图中展示了不使用纹理和使用纹理能呈现的模型渲染效果)
游戏发展的过程中,2D是先于3D的,所以会使用纹理或贴图的出发点很好理解——在三角面有限的情况下,想让颜色更丰富一些,在三角面的范围内呈现出颜色变化。
之所谓常常被人称为贴图,也是因为其最常见的存储方式就是一张图片。有了这张图片读进显存后,就能以一定的坐标系来查询其中的像素,这种坐标系就被称为纹理坐标(Texture Coordinates)。
例如一张512*512的图片,输入x值100、y值100(或者其归一化之后的值),这时就能查询到一个颜色。纹理坐标命名上为了避开模型空间的xyz坐标,通常就被称为UV坐标——即纹理坐标中的x、y值也对应写作u、v。(纹理坐标后续还有2个维度wq,和本文提到的应用无关,这里不展开了)
可以认为纹理坐标是关于颜色查询的。当然实际使用中面临的往往不是“输入2个数输出一个颜色”这么简单,第一步就是模型上存储的uv值是如何得到的。这就引入了下面一个话题:
在渲染物体的过程中,物体顶点坐标经历了从物体空间到参数空间(或裁剪空间)的矩阵变换(此时还没到屏幕空间的具体像素)——这里矩阵变换指运算过程依赖的是矩阵的加或乘,最后可用的结果既可能是坐标(向量)也可能是矩阵。
对一个三角面来说,通过3个顶点的UV值,已经可以确定其在纹理坐标中包围起来的范围。
网格和UV的对应关系不一定是完全对应覆盖的。对于超出纹理坐标范围的UV值,人们在纹理映射的过程中引入了环绕模式(WrapMode)的概念,用来定义超出时纹理像素范围的颜色如何显示处理。
常见的重复模式有:Repeat、Clamps、Mirror几种情况。
Repeat比较好理解,就是循环重复整个纹理。例如一大片地砖就常常使用这种方案,地面可能只有1个四边面,覆盖的纹理范围却是循环了很多次的。
Mirror就是镜像纹理,有些引擎还有MirrorOnce的选项。
Clamps可以理解成边缘颜色外扩,主要是处理纹理坐标中的“空白”(也可以用作透明)区域,这里就不展开了。
如果2个顶点连的一条直线是几何意义上连续的,那么很多问题都不用讨论了。但是实际上显然不是,不仅屏幕空间最小单位是像素,纹理坐标的最小单位也是像素,这就跨越了2个离散的空间。
这之间经历了几轮空间变换之后,带来了或多或少的精度损失;如何减少过程中的精度损失并尽量在最终视觉上显得更连续或更还原,就成了纹理映射诞生之后不断在发展改进的一系列技术方向。
插值(Interpolation)思想被广泛运用于图形学的颜色采样和动画采样等方面,其中的线性插值(Linear Interpolate)可以简单的理解为取平均值。例如(0,0,0)和(1,1,1),其40%位置的线性插值就是(0.4,0.4,0.4)。
如果是一段动画,在2个关键帧(位置、缩放、旋转)之间需要插值,动画概念里也被称为补间(但往往不是线性的,而是由其它曲线函数定义的过程)。那么纹理坐标对应的颜色采样什么时候需要插值呢?
常见的一种情况就是纹理放大,如果显示尺寸大于纹理尺寸,那么就需要考虑2个颜色之间显示什么颜色的问题。双线性插值( Bilinear Interpolation )就是在2个轴上一起对颜色值进行线性插值,取得的值相当于纹理坐标上4个像素之内的一个中间值。
如果不用这种插值方法,采样也可以使用最近颜色(Nearest)方案,呈现出的效果感觉就是像素颗粒更大的原图。(这里有一个Filtering的概念没展开,我觉得这只是一种命名,其实说的是一回事)
如果是纹理缩小,例如——屏幕上10个像素,对应纹理上1000个颜色渐变的像素。这种极端情况下如果每隔100个像素在纹理坐标上采样,映射到屏幕10个像素会显得非常不连续,因为每100个像素的颜色值已经完全变了。改进的基本思路则是对这100个像素做插值,由此诞生了Mipmap。
(左边展示了纹理缩小的一种失真情况,右边使用mipmap缓解了这种视觉上的失真)
Mip之所以很少使用全称,因为其来源是一个意义不太一样的拉丁词 multum in parvo ("much in a small space"),所以说到这个概念基本就是用缩写,也没有很好的中文翻译。硬要说的话,可以理解为多层纹理技术。
其基本思想是——针对原始纹理,每次计算其双轴取一半分辨率,并把颜色线性插值的结果存储为一个次级纹理;这样递归若干次(基本是7次)得到的结果集合就被称为mipmap。
低分辨率纹理中的颜色已经是逐步插值取色后的结果了,这样呈现的视觉效果也可以理解为相邻颜色的一种混合。有了mipmap后,纹理的等比缩小就都可以按最接近的分辨率选择合适的纹理层级(mip level),显示出“混合得更好的颜色”了。
经过这样处理后,得到的纹理在存储上已经比原来的大了约30%。所以这也是一种利用预计算结果牺牲空间换显示效果的技术,最主要的应用就是减少纹理缩小时采样的失真感。
实际应用中,纹理的缩小往往不是双轴等比的,所以后续人们还引入了各向异性(Anisotropic)的mipmap——各向异性可以简单理解成每个轴不一样,这里就是缩放比例每个轴不一样。
(各向异性mipmap的例子,显然这样又消耗更大的存储空间了)
结合上mipmap后还诞生了纹理的三线性插值(Trilinear Interpolation)的技术,其基本思路就是在双线性插值的基础上,在2层相邻的mipmap之间再做一次插值。篇幅原因这个点就不展开了,可以去看Games101或者wiki。
顺带一提,mipmap也被用于流式加载,其基本思路是可以先加载低分辨率的纹理,然后多线程加载并渐变成高分辨率的,属于一套存储结构2种用途的情况了。
抗锯齿是玩游戏时常见的翻译,其实其英文原词更接近的翻译是反走样(因为与其相应的信号系统中的概念被翻译成失真或走样)。
部分抗锯齿的方案已经和纹理映射没关系了,但大部分对于像素颜色处理的思路是类似的,无外乎是各种插值与混合。
1)超采样 —— Super-Sampling Anti-aliasing,简称SSAA
简单来说就是以超过屏幕分辨率的尺寸来渲染,这样映射到屏幕分辨率的像素时会有更好的效果,但性能消耗是很大的。
2)多重采样—— MultiSampling Anti-Aliasing,简称MSAA
只对深度缓冲(Z-Buffer)和模板缓冲(Stencil Buffer)中的数据进行超级采样抗锯齿的处理。可以简单理解为只对多边形的边缘进行抗锯齿处理,相对前者处理的数据量会小一些,性能有所提高。(深度缓冲上一篇文章解释过,而模板缓冲这个概念不展开了)
3)Fast Approximate Anti-Aliasing,简称FXAA
只基于图像渲染结果进行若干个步骤的估算,例如对比度、亮度、方向等,这里不展开了。
4)Temporal Anti-Aliasing ,简称TAA
通过综合历史帧的方式来进行,对动态物体需要结合Motion Vector来计算,这里不展开了。(*这里FXAA和TAA没有合适的翻译,建议结合描述来理解)
FXAA和TAA是目前游戏引擎中比较主流的两种方案。
纹理映射时通过引入一个偏移量来动态改变纹理坐标,就可以实现很多简单又丰富的UV动画效果。
例如在一个循环的屏幕上播放循环的卷轴效果,或者是通过纯纹理实现的水流或岩浆效果等。一般来说这种纹理是需要能四方连续的。
(图中展示了UV动画的一种应用。有些地方也提出了Flow map的概念,特指专用于UV动画表现流动的纹理)
结合这里还没谈到的透明颜色混合模式(Blend Mode),用多个纹理的UV动画结合,并开发对应的着色器代码,就能实现非常丰富的特效了。
图形学的第一课就是如何在屏幕上画一根直线,这种把“连续”想办法变为最接近的“离散”的思想也贯穿了纹理相关的技术以及很多其它的图形学技术。
而Mipmap则结合了“预计算”与“存储空间换效果”的思想,这种思路也广泛运用在了很多其它的图形学技术中。引擎中所谓的要烘焙的数据或纹理贴图几乎都属于此类。
制作植物树叶或者人物头发等等常使用有透明像素的纹理,篇幅所限本文也没有讨论透明纹理与其要面对的其它问题——毕竟透明与半透明讨论起来又是另一个深坑了。
评论区
共 1 条评论热门最新