【读书笔记】计算机图形学基础(虎书)第11章 - 纹理映射(Texture Mapping)
除了物体的颜色以外,我们希望能够模拟真实物体表面的各类细节(磨损、凹凸等)。这些细节会改变物体的质感,但是不会改变物体的整体形状。我们把这类细节的总和叫做材质/纹理。为了让相同的物体可以快速套用不同的材质,我们选择的做法是将纹理制作成纹理图(Texture Map/Image),再将物体表面的每一个点与这个纹理图上的某一个点一一对应,使得物体表面获得纹理图的效果(一个比方是将一块布包在球体上)。这个一一对应的过程叫做纹理映射(Texture Mapping)。这种操作方法除了形容物体表面以外,其后又发展出了很多应用,例如储存每个点法向量的Normal Map,储存光线方向的Light Map等,都称为纹理。书中章节介绍了纹理映射在表面细节、阴影和反射上的应用。
需要注意的是
- 纹理映射本身是一个采样再重构的过程,所以会存在走样的问题。
- 我们要确保映射时模型和纹理图的对应方式与映射方式较为接近。哪怕没有走样如果映射的方法错误也会让最后呈现出来的效果不好(比如脸扭曲在一起,只长了眼睛其他都没有等)。
- 动画和纹理映射结合时会有更严重的走样问题,需要运用一些反走样技术来降噪。
1. 查询纹理颜色值
纹理映射是为了给顶点查询出颜色或者其他属性信息,因此查询纹理值的过程就是给定顶点,寻找其在纹理图中的对应位置,再在该位置上提取值。其中第二步属于简单的提取像素,因此更为重要的是第一步寻找对应坐标。我们把第一步的映射称为Texture Coodinate Function(纹理坐标方程),它是一个三维表面S投影到二维纹理T的过程。我们通常把二维纹理用\((u,v)\in[0,1]^2\)来表示,我们也把纹理坐标叫做UV坐标。
我们通常关注一下两个问题
- 纹理坐标方程是如何定义的,是否合理。书中的例子是一个倾斜的木板纹路,我们应该用斜面一一映射还是直接使用xz平面的投影来一一映射。另外一个常见的例子是世界地图,不同地区的大小会因为经纬度有所改变。
- 提取出的纹理值会不会带来走样,见下图,远处出现了走样问题。
2. 纹理坐标方程
我们通常将纹理坐标方程看做
- 将三维表面慢慢捋顺成一个平面的过程,例如地球地图
- 将二维UV图通过形变覆盖到三维表面的过程
我们通常在以下几点中进行权衡: - 双射(Bijective),通俗来说就是一一对应。如果多个表面点对应同一个UV纹理点,则会放大UV走样的问题。
- 尺寸变形(Size distortion),纹理映射的单元间距最好保持同样的比率,类似于线性变换的网格一样,而不是有的密有的稀。
- 形状变形(Shape distortion),纹理图中的某一个形状在覆盖到三维表面后也应该维持类似的形状。
- 连续性(Continuity),纹理图中连续的点在三维表面中也是连续的。
纹理坐标方程对于平面而言较为简单,直接利用参数方程(p+au+bv)中稍作修改即可。对于复杂的隐式表面而言,我们通常选择利用几何坐标系进行计算。对于多个三角形或者宏观意义上的多面体表面,我们利用顶点的纹理坐标插值进行处理。
2.1 几何坐标系计算
几何坐标系适用于简单的形状,或者手工调试的起点。以下介绍了几种方法
平面投影 Plannar Projection
平面投射基本和相机坐标到标准空间坐标的投影一样,将表面简单投影到二维UV平面上,这样显然存在维度的丢失,法向量方向的三维表面(类似于线性代数里的null space)会拥有极其类似或者一样的UV坐标,造成下图的结果。但是当表面是一个平面,厚度可忽略时就会有较好的结果。
球投影 Spherical Projection
球投影指的是如同地球地图一样,按照经纬度方式进行投影。我们采用球体坐标(半径,横向角度。纵向角度)的话使用后两者可以直接得出。注意这种方法球的两极会存在聚集和失真现象,在中间又会因为运动过快产生缩小的现象。为了方便计算有以下公式。这个方法在两个极上不连续。
圆柱投影 Cylindrical Projection
圆柱投影对球投影进行了一定的修改,改为一个轴+圆周运动的模拟,公式如下。上下公式对比为y值的差别(圆周到轴)。这个方法使得UV图中两个对边聚集在两个点上,形成了一些不连续性(可以想象一个地图作为卷轴再把上下两条边粘起来)。这个也是标准地球地图的制作方法,也被称为墨卡托投影。
立方体投影 Cube Map
从球到圆柱,我们能减少尺寸和形状变形,却增加了不连续性。我们进一步在这个方向上行走便来到了立方体投影。立方体投影利用了六个不同的纹理拼接而成,具体可以参考我的世界。这个方法在所有边上都是不连续的,但是在面上拥有极好的尺寸和形状不变形。需要注意的是纹理投影函数和正方体的纹理设置必须一一匹配,否则可能会有错误的摆放情况(比如我的世界脸放反了)。这个方法在之后说的环境映射中也会用到。一个惯例是对于\((x,y,z)\)而言,它会被投影到绝对值最大的那个对应平面,例如\((-1,-0.5,-.5)\)会被投影到\(x=-1\)平面。其次\((u,v)\)从立方形中间往外看应该总在右下角。OpenGL版本(第二个图)则是左上角。可以用手卡着一个手势然后随着视角移动来对照。
2.2 纹理坐标插值
这个方法就是直接基于顶点上的纹理坐标用重心坐标来进行插值。这个方法非常的简便好用,但是也会出现一些问题。如下图所示,顶点插值的结果其实和将左边的图拼接黏贴是一样的,那么很显然左图最外围轮廓均为不连续的,拼接起来会有问题。
如果三维坐标和纹路坐标不具有类似的形状则会出现较大的形状变形。
如果三维坐标和纹路坐标三角形面积的比率没有被维持,那么可能会出现较大的面积变形。
2.3 瓦片,卷回模式和纹理变换(Tile, Wrapping Modes and Texture Transformation)
因为一些计算问题或者设计模式,我们可能需要计算超出边界的纹理值。我们有几种不同的处理思路。
- 当纹理只希望覆盖一部分的表面,但整个表面与标准\([0,1]^2\)纹理空间一一对应,我们可以选择制作一个带有空白背景色而部分含有原纹理图的新图直接盖到空间上,也可以选择将纹理空间的尺寸放大对应至整个表面再最后用单位格代表那一小部分。
- 对纹理超出界限的情况,我们可以选择设置一个背景色,选择最接近的值直接裁剪采集(clamp)或者利用循环/求余的方式加以寻找(即最右边超出一点和最左边多一点是一样的,称为瓦片Tiling)。这两种都被称为处理越界纹理坐标值的卷回模式。我们可以通过矩阵或函数的方式处理超出边界的纹理坐标。
2.4 透视视角下的纹理
从下图我们可以看到,如果从屏幕空间进行线性插值我们会得到不正确的透视结果。左图是正确透视而右边是错误的。这个问题产生的主要原因是因为如果在屏幕空间进行纹理的线性插值,我们只知道四个顶点的UV值,插值在屏幕空间下将会是线性的,产生右图的均匀结果。
我们实际上应该做的是在世界坐标下进行纹理映射,再在屏幕空间中找到正确的点。就GAME101所说我们可以利用转换矩阵的逆矩阵来在fragment shader处反向求值,也可以有如下的方法。推导公式可以见此链接。
与其做对比的更简单的方法是在透视转换时便计算UV坐标,即将UV坐标与正常的向量[x,y,z,w]一起转换,具体如下图。
2.5 连续性与接缝 (Continuity and Seam)
之前提到过在闭合三维体(球体、柱、立方体)上进行纹理映射时,一个较为简单的映射方法是将纹理图的边缘拼接在一起,形成一条接缝,这样可以保证除了接缝外的点都拥有双射(bijective)和连续的特征。通常而言,我们复制接缝的点,并赋予他们截然不同的UV值(例如0和1代表纹理图的左右两侧),这个方法与立方体8个顶点实际利用24个顶点来实现相同顶点不同面不同法向量的方法类似。在此方法下我们可以保证跨越接缝的两点之间也拥有正常的插值,即类似 [0.8->1]->[0->0.2] 的分两段插值。
3 纹理查询中的反走样
因为纹理映射涉及到纹理细节的投影以及基于有限像素的重建,纹理映射也会涉及到常规采样流程中各类困难,例如各类不同的走样问题。因为纹理映射本身容易含有高度细节化的信息,因为细节对应的高频信号所引起的走样问题会更加明显。与之前章节类似,解决走样问题的方法包含但不限于超采样(super sampling)、采样与重建过滤器等。
3.1 Footprint of Pixel (以下均主要采取英文,简单而言是 “屏幕空间内像素在纹理图所占区域”)
在下图我们可以看到,同样是屏幕空间的像素,他们在纹理图中所占据的区域有所不同。屏幕空间远处的点因为透视占据纹理图更大的面积,导致远处像素拥有噪声更高,随机性更大的纹理值。即使纹理映射时需要一些不规则边框的投影(例如圆形或者曲形),我们也通常利用线性近似对纹理值加以计算。即假设一个屏幕空间像素内XY移动量为\(\Delta X, \Delta Y \in [-1,1]^2\),那么其对应的纹理图UV改变量可以由投影函数对应XY的偏微分计算出,即\(\Delta U = \Delta X \frac{\partial U}{\partial X} + \Delta Y \frac{\partial U}{\partial Y}\),\(\Delta V\)类似。当然这样属于一种线性近似,仅在较小范围内拥有较高的准确度。这种线性近似会将屏幕空间的正方形投影至纹理图的一个平行四边形,并将其中的UV值进行平均化的操作。当平行四边形面积较大且纹理图较大时,这种平均化操作会带来较大的开销。因此我们也有下文提供的一系列优化方法。
3.2 重建滤波器
在纹理转换中我们会更多的碰见在像素格中散落分布的情况,而不是恰好在像素点上。现在我们采用的方法是bilinearly interpolation, 即已知输入UV的情况下,寻找其UV值所在的纹理图像素格,并基于其四个角的纹理图像素颜色进行bilinear interpolation,代码如下。这个流程与不同尺寸图片之间互相转换的流程类似(超采样等)。
3.3 Mipmap
详情见GAMES101纹理映射部分,简单来说与LOD(Level Of Details)类似,根据不同情况选用尺寸大小不同的纹理图进行映射。不同纹理图的大小应该以两倍缩小来进行层次转移,这样可以保证纹理从高往低改变时不会落在高纹理的纹理像素格内,而是一定落在高纹理像素的值上。假设我们有1024*1024的图片,那么接下来的Mipmap层分别有边长512,256,128,64,32,16,8,4,2,1。 Mipmap的设计意义和LOD有些差距,Mipmap是为了在搜索一定区域内的纹理平均值时直接搜索已经计算好的低清纹理图,而LOD是为了省模型精度带来的计算空间。因为mipmap只能搜索正方形的平均值,长方形的变体称为各向异性(anisotropic),还有各类不规则图形的变体例如EWA过滤。从代码上可以看出,当我们的目标大小与偏移合适时,我们根据Footprint的长边长度D来算出我们需要第几层的Mipmap,并直接搜索对应Mipmap层中的值。对于大小和偏移非恰好对准的情况可以搜索GAMES101,简单而言我们可以在Mipmap的临近层之间形成trilinear interpolation。
4 纹理映射的应用
- 更改PBR的参数,包括但不限于颜色,粗糙度,金属度,高光等
- 更改法向量
- 更改模型,包括但不限于位移量(Displacement)
- 储存需要复杂计算的数值,例如点光源形成的shadow map(阴影图)以及environment map(环境图)。原文所说的shadow mapping和纹理的关系较弱,是光栅化中从光源出发绘画出shadow map,储存每个像素格的光照深度。在屏幕空间中判断某点是否处于某光源所带来的影子中时,我们将该点在光源视角下的深度与光源的shadow map深度值进行对比,前者大于后者则说明发生了遮挡,因此在影子中。有些纹理图是为了简略计算光影,例如将天气系统的24小时分别绘制一份阴影图,并在时间流逝中进行插值来计算影子。Environment Map往往被用于计算球形物体收到某环境造成的反射效果,如果不是球形物体则加以变形,它类似一种假设总能反射时环境造成的底色变化。
5. 程序化生成(Procedural )三维纹理
三维纹理指我们不使用二维的纹理图片,而是生成真正的三维模型来储存纹理值。这种办法可以有效的提高准确度,但是额外一维的空间会带来大量的空间开销。因此我们引入了程序化生成三维纹理的方法,即用数学语言或一个固定函数来快速生成整个纹理再加以查找,而不是每次寻找对应值。
5.1 条纹
对于条纹的交替我们可以用周期性三角函数sin是否大于0来实现,条纹交替的频率和长度则可以由三角函数的长度来改变,渐变等效果也可以用三角函数的直接值而不是是否大于0来实现。
5.2 噪声
Perlin Noise(柏林噪声),Worley Noise增加了更多的不规则度。这部分本书介绍的比较少,推荐看其他地方的更细节化的处理。值得注意的是噪声有很多应用,比如游戏中血液和尸体如何慢慢消失,以及程序化生成Minecraft的地形。