纹理映射(Texture mapping) agile Posted on Oct 2 2021 优秀博文 > 本文由 [简悦 SimpRead](http://ksria.com/simpread/) 转码, 原文地址 [zhuanlan.zhihu.com](https://zhuanlan.zhihu.com/p/364045620) 为什么需要纹理映射 --------- 在上一篇文章中,我们学习了 [Blinn-Phong 反射模型与着色频率](https://zhuanlan.zhihu.com/p/364086530),假设我们使用 Phong 着色并且只考虑漫反射的情况,我们知道一个着色点也就是一个像素的颜色应该是由下面这个式子算出来了。 > ![](https://www.zhihu.com/equation?tex=L_d+%3D+k_d+%5Ctimes+%5Cfrac%7BI%7D%7Br%5E2%7D+%5Ctimes+max%280%2C+%5Cvec%7Bl%7D%5Ccdot+%5Cvec%7Bn%7D%29) 那么如果我们要把一个长方体着色为白色(如下图),只需要每个着色点的 kd 值都为白色即可,也就是每个着色点的 kd 值都相同。 ![](https://pic3.zhimg.com/80/v2-0ee359a0b4c7677fbad418a438602766_1440w.jpg) 但是大千世界千变万化,怎么可能都是纯色的东西呢,如果我们要把长方体着色成墙一样(如下图),那么 kd 的值应该怎么设置? ![](https://pic2.zhimg.com/80/v2-ffb8dd3870fe42f458535bc5bef2b7d1_1440w.jpg) 从图中可以看出,此时每个像素的颜色基本都不一样,也就是说每个着色点的 kd 值都不相同,那么我们应该怎么来设置这个 kd 值,总不可能一个个着色点去单独设置吧,此时纹理映射就可以帮我们来解决这样的问题。 物体表面与纹理 ------- 我们知道我们显示的三维物体,其实都是三维物体的表面,而这些**表面其实本质上都是二维的**。这些物体表面由一个个三角形组成,我们只需要从中撕开一些口子,就可以把它们展开成一个二维的表面,就像生活中的剥橘子一样。 ![](https://pic3.zhimg.com/v2-0618f6a3a988e7d1845a836b11da91c2_r.jpg) 那么这样,我们就可以把三维物体表面与二维的图联系在一起了,就好像地球仪与地图的关系(如下图),三维的地球仪上的任意一个地方(一点)可以同样对应到二维地图上的这个地方。 ![](https://pic4.zhimg.com/v2-51af75aa40fdd190f04c855e9ec10fbf_r.jpg) 纹理映射的纹理,指定就是我们前面所说的这些二维的图片。我们假设这些图我们可以随意的拉伸扭曲,那么我们就可以把这些图包裹到三维物体上,那么这个过程我们就可以称之为纹理映射。例如把地图裹到一个球上面,那么这个球就变成了地球仪。 而我们之前例子中的长方体的纹理就如下图: ![](https://pic1.zhimg.com/v2-cea5fc3ac59c24137a12b60f4dd568dc_r.jpg) 何为映射 ---- 前面所说的映射是把纹理包裹到三维物体上,是一个很笼统的说法。实际上纹理映射,我们只需要知道物体表面上的点(也就是着色点)与纹理的对应关系,即**三维表面的某个着色点是纹理上的哪个位置,然后着色时 kd 的取值按照纹理上的值即可**。 例如下图,我们有个狗狗的模型(资源来自 [Dog Knight PBR Polyart](https://assetstore.unity.com/packages/3d/characters/animals/dog-knight-pbr-polyart-135227)),通过前面学习的着色,我们可以得到如下效果: ![](https://pic3.zhimg.com/v2-868306d59f7534a7e3305de99da6ba72_r.jpg) 然后我们下面这张纹理来与之对应 ![](data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='408' height='409'></svg>) 即可得到纹理映射后的效果: ![](https://pic4.zhimg.com/v2-aa281108b64b806e95e79d0d5ffde12b_r.jpg) 我们来看下他们的对应关系,例如我们来看鼻子这部分,它同样是由三角形组成,这些三角形会对应到纹理右下部分的鼻子那块(如下图),这样我们就知道鼻子这应该显示什么颜色了。 ![](https://pic3.zhimg.com/v2-28a928cdc778a3bef184f3fc0f4ec556_r.jpg) 至于为什么这些三角形会对应到纹理的那些部位,这个在美术建模的时候,就由建模软件制定好了关系,我们就当做我们能够知道模型表面上任意一点在纹理的哪个位置即可。 我们再来看个比较有意思的例子,另一只狗狗,如下: ![](https://pic2.zhimg.com/v2-e368dfa8f5a22e1974c45caa9017bbd5_r.jpg) 它的纹理是什么样的呢?我们看下图,是这只狗狗的纹理 ![](data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='418' height='392'></svg>) 是不是很神奇,居然都是纯色小块,其实我们从表面的三角形(如下图)可以发现每个三角形确实都是纯色的。 ![](data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='366' height='389'></svg>) 纹理坐标 UV ------- 前面既然提到了纹理上的某个位置,那么我们是不是就可以在二维的纹理上定义一个二维的坐标系,这样纹理上任意一点的位置我们就可以使用 (x, y) 的方式来表述了。事实上,就是这样的,不过对于**纹理的坐标系我们通常用 uv 来表示**,而不是 xy,但是意义是一样的,**u 代表纹理的横坐标,v 代表纹理的纵坐标**。 我们知道不同的纹理,它们的大小可能都是都是不一样的,甚至有些可能是正方形,有些是长方形,因此纹理坐标 uv 的定义和纹理尺寸以及形状没有关系。我们认为对于任何一个纹理,它的 **u 和 v 的值都是从 0 到 1**,例如 uv(0,0) 代表纹理的左下角,uv(0.5,0.5) 代表纹理的中心的,uv(1,1) 代表纹理的右上角。 例如我们前面狗狗的纹理,它的 uv 如下: ![](https://pic4.zhimg.com/v2-fa367b28d7709ed63d892eb7137c0f3b_r.jpg) 这样,当我们知道一个三角形三个顶点对应到纹理上的 uv 坐标后,我们就可以通过[重心坐标](https://zhuanlan.zhihu.com/p/361943207)来计算出该三角形内任意一点所对应的 uv。然后我们通过 uv 坐标即可在纹理上采样到对应的颜色值作为改点的漫反射系数 kd 的值,这样就等于把改图贴到(映射到)了物体上。 模糊 / 锯齿现象 --------- 我们来对比下下面两张图: ![](data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='372' height='364'></svg>)![](data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='409' height='384'></svg>) 我们可以发现图二明显的比图一模糊了很多,但是基本纹路来看他们是一模一样的,这是为什么呢?**模糊其实就是因为我们纹理过小,在覆盖物体表面时被放大了所导致的。** 我们再来看一张图: ![](data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='286' height='292'></svg>) 这张图同样是一张模糊的图,不过我们可以从中发现很多的锯齿,本质上模糊和锯齿的原理是一样的。 我们的纹理其实就是一张图片,因此它也存在自身的分辨率,即由像素组成,每个像素有自己的下标,**纹理上的像素我们常称为 texel**。uv 对应的像素坐标我们也是可以计算出来的,例如我们纹理每行有 1000 个像素,那么当 u=0.25 时,对应的像素横坐标就是 39(下标从 0 开始),v 自然也是同理。 而我们物体表面最终会显示在屏幕上,屏幕自然也有它的像素,**屏幕像素我们称之为 pixel**。我们每个屏幕像素都会对应到三角形内的一个点,而三角形内的点会有它对应的 uv 坐标,然后我们通过 uv 坐标可以找到纹理上对应的纹理像素。也就是说**在使用纹理映射时,屏幕像素会对应到纹理像素上**。(注:后续会说到很多的屏幕像素和纹理像素,注意区分) 那么当纹理分辨率低时,也就是纹理内部的像素少,这样屏幕像素对应到纹理像素上,它可能就不是一个整数下标的纹理像素,而变成了浮点数。例如我们屏幕像素 (50, 50) 对应到纹理像素(5,5),屏幕像素(51, 50) 对应到纹理像素(5.1,5),屏幕像素(52, 50) 对应到纹理像素(5.2,5),然后对于浮点数我们会四舍五入成整数,那么屏幕像素(50, 50),(51, 50),(52, 50) 对应的纹理像素都是(5,5),也就是说**当我们纹理太小的时候,我们多个屏幕像素会对应到一个相同的纹理像素上,所以产生了模糊或者锯齿**。 双线性插值(Bilinear interpolation) ----------------------------- 既然模糊了,我们就要对其进行优化,尽量让即使纹理过小,着色出来的效果也不错。而其中一个优化的技术就是双线性插值,接下来我们来看看它的原理是什么。 我们先来看看**线性插值**(Linear interpolation)是什么,其实它就类似于我们的[直线重心坐标](https://zhuanlan.zhihu.com/p/361943207),如下图: ![](data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='295' height='88'></svg>) 我们有 ABC 三个点,C 点在 AB 之间,ABC 点的位置信息我们都知道,根据长度我们设 ![](https://www.zhihu.com/equation?tex=%5Cfrac%7BAC%7D%7BAB%7D%3Dx) ,x 的范围为 0-1,那么当我们知道 AB 的其他属性(例如颜色,法线等)时,C 点对应的属性即为: > ![](https://www.zhihu.com/equation?tex=C%3Dlerp%28x%2C+A%2C+B%29%3DA%2Bx%28B-A%29) lerp 就代表线性插值的意思,例如 x=0,得到的就是 A 的属性。 理解了线性插值后,我们再来看看什么是双线性插值,以及怎么在纹理上应用。先看下面这张图片: ![](data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='368' height='359'></svg>) 图中的每个小方格我们当做是一个纹理像素,小蓝点即使纹理像素的中心点。此时我们有个屏幕像素对应到了纹理像素的红点 Q 位置。 若按照四舍五入的方式,那么此时 Q 点得到的颜色就是 B 点所在纹理像素的颜色,会造成的问题也就是我们前面所说的模糊。 那么双线性插值会怎么计算 Q 的值呢?首先它会找到红点 Q 附近的四个纹理像素,也就是图中的 ABCD 四个点,然后利用这四个点的值插值出 Q 点的值。如下图: ![](https://pic3.zhimg.com/v2-d82a6112421ee840d849e0659728fb3a_r.jpg) 既然是周边的四个顶点,那么肯定会有个左下角点,也就是图中的 C 点,我们设 ![](https://www.zhihu.com/equation?tex=s%3D%5Cfrac%7BCJ%7D%7BCD%7D) , ![](https://www.zhihu.com/equation?tex=t%3D%5Cfrac%7BCG%7D%7BCA%7D) ,那么 Q 点就可以用 (s, t) 的方式来表示。其中 s 和 t 的范围都是 0-1。并且 s 和 v 我们都是可以通过 ABCDQ 五个点的 uv 坐标计算出来的。 看到这,是不是很容易就能联想到我们前面的线性插值了?没错,我们可以通过线性插值很容易求出 G,H,I,J 四个点插值出来的颜色。当然我们只需要知道其中两个点的即可,I 和 J 或者 G 和 H,这里我们先求 I 和 J 的值,套用上面的公式,即为: > ![](https://www.zhihu.com/equation?tex=I%3Dlerp%28s%2C+A%2C+B%29%3DA%2Bs%28B-A%29) > ![](https://www.zhihu.com/equation?tex=J%3Dlerp%28s%2C+C%2C+D%29%3DC%2Bs%28D-C%29) 然后 Q 不就 IJ 之间了么,我们又可以通过 IJ 做一次线性插值,即可得到 Q 插值后的颜色: > ![](https://www.zhihu.com/equation?tex=Q%3Dlerp%28t%2C+J%2C+I%29%3DJ%2Bt%28I-J%29) 因为前后一共做了两趟线性插值(虽然第一趟做了两次),所以我们称之为双线性插值。双线性插值后,Q 的颜色就会和边上四个像素结合起来,而不再简单的等于 B 的颜色。这样当多个屏幕像素对应到一个像素上时,这几个屏幕像素的颜色也会有一个线性的变化,而不再一模一样,模糊或锯齿的效果就得到减弱。 除了双线性插值外,还有**双三次插值(Bicubic interpolation)**,得到的效果就会更好。双三次插值取得则是周围十六个纹理像素做插值(该插值方法不是线性插值),具体原理这里就不过多介绍了。 利用它们得到的效果如下图,可以明显发现画质效果变好了。 ![](https://pic1.zhimg.com/v2-d10b842cbc78e2dc2da16fbc170cd30c_r.jpg) 摩尔纹 --- 现在我们现在有如下一个纹理 ![](https://pic3.zhimg.com/v2-56949803f47949c1b5d6dd28b594ea5e_r.jpg) 我们把它应用到一个平面上,如下图: ![](https://pic3.zhimg.com/v2-98d173b37fcc2548be31130e667c49be_r.jpg) 我们可以发现,近处变模糊了,而远处就更离谱了,直接就看不清是啥了,对于远处这种效果,我们称之为摩尔纹。那么为什么会变得近处模糊,远处摩尔纹呢?我们还是从屏幕像素和纹理像素的对应关系入手理解。 从纹理图我们可以看出,我们的格子其实都是一样大小的,也就是说每个格子所占的纹理像素是一样多的。但是因为透视投影的**近大远小**效果,我们近处格子看起来会很大,也就是说会有很多的屏幕像素来显示一个格子,那不就导致多个屏幕像素会对应到一个纹理像素了么,也就是我们前面所说的模糊问题。而在远处就恰恰相反了,用极少的像素显示一个甚至多个格子,也就是说我们**一个屏幕像素会对应到多个纹理像素,这就产生了摩尔纹**。 画成示意图的话,就如下: ![](data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='378' height='377'></svg>) 我们假设在远处一个屏幕像素覆盖了十六个纹理像素,该屏幕像素中心点对应到图中的 Q 点。根据前面四舍五入的方法,我们知道 Q 点的颜色就是 A 点对应纹理像素的颜色,同时这个颜色也代表了该屏幕像素覆盖的十六个纹理像素的颜色,这显然是不对的。 在[采样与走样](https://zhuanlan.zhihu.com/p/363284887)里提过,摩尔纹属于欠采样所造成的,即我们很多纹理像素却只采样了其中一个像素的值。那么只需要利用 **MSAA** 的原理,即在一个像素内增加采样点,然后求个平均,来反走样解决问题。 例如上图,原本我们一个屏幕像素一个采样点,覆盖了十六个纹理像素,那么如果我们一个像素里用十六个采样点,那么覆盖的纹理像素不就基本都可以被采样到,然后利用它们的平均值来代表这个屏幕像素的颜色,而不是某个纹理像素的值来代表。这么做确实能得到不错的效果,但是会增加很大的计算量,造成性能消耗。 那么有没有什么更好的方法呢?有!就是 **Mipmap**。一句骚话就是,采样当频率不足时会造成走样,那么我们不采样不就行了。在介绍 Mipmap 之前,我们先来介绍下下面两种查询方式。 点查询(Point Query)和范围查询(Range Query) ---------------------------------- 我们前面讲到双线性插值其实就属于一种点查询方式,我们得知纹理上的任意一点,要得知其对应的颜色值。 而对于摩尔纹,我们要用的则是范围查询,即我们知道一定范围的纹理像素,要查询出它的平均值。当然应对不同的情况我们也可查询一个范围内的最大值或最小值。那么当我们得知一个范围后,如果能立刻得知它的平均值,不就可以在不增加运算量的情况下,解决摩尔纹的问题了么。 Mipmap ------ Mipmap 就是一种可以帮我们实现范围查询的方法,**它速度快,但并不是特别的准确,结果是一个近似值,此外它只能做正方形的范围查询**。 Mipmap 的本质,其实就是一张纹理生成一系列的纹理,如下图: ![](https://pic4.zhimg.com/v2-19228fc05d03ccd89c9ea7e5bbddd797_r.jpg) 我们假设原本的纹理是 n*n 大小的(纹理大小也就是纹理像素的数量),为第 0 层。然后我们用它增加更多层的纹理,每一层的长宽大小都是上一层的一半,那么总共就会有 logn 层。 这样我们只需要在使用前先生成好 mipmap,然后使用时直接使用它做查询,就可以节省下使用时很多的计算时间,从而保证效果还不错。 ### 占用空间 既然多了这么多的纹理,那么势必会增加存储空间,那么一张 n*n 的纹理使用 mipmap 后会增加多大的存储空间呢?答案是**增加了三分之一**。 我们来看看这个值怎么得到的,我们设原图占的存储空间为 x,那么第一层 mipmap,长宽各缩小了一倍,因此占的存储空间为 x/4,第二层再缩小一倍,即为 x/16,...,我们设所有 mipmap 的占用为 y,那么我们就可以得到下面的公式: > ![](https://www.zhihu.com/equation?tex=y%3D%5Cfrac%7Bx%7D%7B4%7D%2B%5Cfrac%7Bx%7D%7B4%5E2%7D%2B%5Cfrac%7Bx%7D%7B4%5E3%7D%2B...%2B%5Cfrac%7Bx%7D%7B4%5E%7Blog_2n-1%7D%7D%2B%5Cfrac%7Bx%7D%7B4%5E%7Blog_2n%7D%7D) 这个求值方法有点类似我们二叉树的求节点数量,我们可以先两边都乘以 3,得到: ![](https://www.zhihu.com/equation?tex=3y%3D%5Cfrac%7B3x%7D%7B4%7D%2B%5Cfrac%7B3x%7D%7B4%5E2%7D%2B%5Cfrac%7B3x%7D%7B4%5E3%7D%2B...%2B%5Cfrac%7B3x%7D%7B4%5E%7Blog_2n-1%7D%7D%2B%5Cfrac%7B3x%7D%7B4%5E%7Blog_2n%7D%7D) 然后再给它们各加上 ![](https://www.zhihu.com/equation?tex=%5Cfrac%7Bx%7D%7B4%5E%7Blog_2n%7D%7D) 得到: ![](https://www.zhihu.com/equation?tex=3y%2B%5Cfrac%7Bx%7D%7B4%5E%7Blog_2n%7D%7D%3D%5Cfrac%7B3x%7D%7B4%7D%2B%5Cfrac%7B3x%7D%7B4%5E2%7D%2B%5Cfrac%7B3x%7D%7B4%5E3%7D%2B...%2B%5Cfrac%7B3x%7D%7B4%5E%7Blog_2n-1%7D%7D%2B%5Cfrac%7B3x%7D%7B4%5E%7Blog_2n%7D%7D%2B%5Cfrac%7Bx%7D%7B4%5E%7Blog_2n%7D%7D) 此时 ![](https://www.zhihu.com/equation?tex=%5Cfrac%7B3x%7D%7B4%5E%7Blog_2n%7D%7D%2B%5Cfrac%7Bx%7D%7B4%5E%7Blog_2n%7D%7D) 的值正好为 ![](https://www.zhihu.com/equation?tex=%5Cfrac%7Bx%7D%7B4%5E%7Blog_2n-1%7D%7D) ,可以在和前面的项相加得到 ![](https://www.zhihu.com/equation?tex=%5Cfrac%7B3x%7D%7B4%5E%7Blog_2n-1%7D%7D%2B%5Cfrac%7Bx%7D%7B4%5E%7Blog_2n-1%7D%7D%3D%5Cfrac%7Bx%7D%7B4%5E%7Blog_2n-2%7D%7D) ,然后又可以和前一项相加,一直往前加,我们就可以得到: ![](https://www.zhihu.com/equation?tex=3y%2B%5Cfrac%7Bx%7D%7B4%5E%7Blog_2n%7D%7D%3Dx) 因为当 n 很大时, ![](https://www.zhihu.com/equation?tex=%5Cfrac%7Bx%7D%7B4%5E%7Blog_2n%7D%7D) 趋向于无穷小,因此 y = x / 3,即增加了三分之一的存储空间。 我们也可以利用几何的方式来理解,如下图,我们用三倍的 mipmap 纹理,最终会填充出一个 x 的大小。 ![](https://pic3.zhimg.com/v2-3204707c1292995817baed8881bb2b22_r.jpg) ### 查询范围 使用 mipmap 我们怎么建立查询关系呢,也就是说我怎么知道我们的某个屏幕像素应该使用哪个层级的纹理。前面我们说了,一个屏幕像素会覆盖多个纹理像素,由于 mipmap 只能进行正方形查询,因此我们就要把覆盖范围近似成正方形,我们假设边长为 L。 那么假如一个屏幕像素覆盖了 4 个左右的纹理像素,即 L=2,那么我们自然要使用第一层的 mipmap,而要是覆盖了 16 个左右像素,L=4,就应该用第二层的。也就是说**如果一个屏幕像素覆盖了 L*L 个纹理像素,那么就应该使用 logL 层 mipmap**。那么我们就要知道我们的一个屏幕像素到底覆盖了多少的纹理像素,也就是求 L 的值。 怎么算,很简单,例如我们屏幕像素 (x,y) 对应的一块纹理像素的中心点为 (u,v),那么我们再取它周边的一个屏幕像素,例如 (x+dx,y+dy),算出对应的纹理像素,假设为 (u+du, v+dv),那么我们就可以近似的求出 L 的值: > ![](https://www.zhihu.com/equation?tex=L%3Dmax%28%5Csqrt%7B%28%5Cfrac%7Bdu%7D%7Bdx%7D%29%5E2%2B%28%5Cfrac%7Bdv%7D%7Bdx%7D%29%5E2%7D%2C%5Csqrt%7B%28%5Cfrac%7Bdu%7D%7Bdy%7D%29%5E2%2B%28%5Cfrac%7Bdv%7D%7Bdy%7D%29%5E2%7D%29) 示意图如下: ![](https://pic4.zhimg.com/v2-198a67a07b38f60b91a14a1d65aa75eb_r.jpg) 得到的结果如下: ![](data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='420' height='413'></svg>) ### 三线性插值(Trilinear interpolation) 前面我们可以通过计算 L 的值来计算出应该查询哪一层,但是实际情况下,从近到远,我们 L 的值是线性增长的,比如从 1 到 2 到 3... 到 n。但是由于 LogL 的值取得是整数(设为 D),当我们 L=1 时,D=0,L=2 时,D=1,但是当 L=3 到 5 时,D 都等于 2。也就是说当 L=3 时,不存在 D=1.58 的情况,那么就会造成屏幕像素的颜色不是线性变化的。 模拟效果如下,颜色都是根据远近突然变化的。 ![](https://pic2.zhimg.com/v2-e6200508ff0d659bf56f2d5680b151b1_r.jpg) 为了追求更好的效果,那么我们能不能在知道 D=1 和 D=2 的 mipmap 时,求出 D=1.58 乃至其他浮点数的 mipmap 呢?可以,答案还是线性插值,如下图: ![](https://pic1.zhimg.com/v2-e37136a8715e0265fad09eb50aa43544_r.jpg) 我们先用双线性插值求出 D 层和 D+1 层的值,然后再线性插值求出 D+x 层的值(x 范围 0-1),这样等于在双线性插值的基础上再做了一趟线性插值,所以我们称之为三线性插值。这样就可以使得 mipmap 层与层之间的颜色变化是连续的。 使用三线性插值后,我们之前的模拟图效果就会变为下图所示,变得完美了很多。 ![](https://pic4.zhimg.com/v2-30d9e89372671a97c84e4fe5eafe92f7_r.jpg) ### 缺点 前面我们说了 mipmap 可以解决摩尔纹的问题,但是有些情况下,它的效果并不是很好,例如下图 ![](https://pic3.zhimg.com/v2-17b5b04f6ca6e21ffecaf51259cd688e_r.jpg)![](https://pic2.zhimg.com/v2-1e9259f47a630e0f78e98e42fc9421cd_r.jpg) 图一是我们想要得到的结果,而图二是 mipmap 得到的结果,可以发现在远处变得很模糊了,这是为什么呢? 因为我们的 mipmap **只能做正方形的查询**,在计算覆盖范围时都是按照正方形去考虑的,而实际上屏幕像素的覆盖性质并不可能那么的完美,例如下图: ![](https://pic1.zhimg.com/v2-7cd079fc504949af2cd224766593c4f4_r.jpg) 我们可以发现这种情况下,一个屏幕像素覆盖的纹理像素范围更多的是长方形,甚至是斜条,那么再用正方形去计算就会出现很多的问题,这也正是 mipmap 的不足之处。而针对覆盖范围更多是长方形的情况,我们有更好的做法,即 Ripmap。 Ripmap,各向异性过滤(Anisotropic Filtering) ------------------------------------ Ripmap,我们也可称之为各向异性过滤,它和 mipmap 的不同之处就是它可以支持长方形的查询,生成出来的纹理如下: ![](data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='397' height='369'></svg>) 左上角为原始纹理,在水平方向只进行宽度的压缩,在竖直方向只进行高度的压缩,那么压缩后的图片任意一点还原到原始图片时,代表的都是一个长方形的区域。 从图中我们也可大致看出,使用 Ripmap 会导致存储空间变为原来的四倍左右,造成较大的显存占用。但是当压缩的层级 x 越高,增加的空间也会越小,例如当 x=5 和 x=10,其实图片大小差距不大,所以打游戏的时候,设置里可以开的越高越好。 Ripmap 可以很好的解决覆盖范围为长方形的情况,但是对于更奇怪的覆盖范围,例如斜条等,同样不能很好的解决问题,对于这类情况我们还可以使用 **EWA Filtering** 来解决,当然运算量又会增加,具体 EWA Filtering 的实现原理,这里就不过多介绍了。 纹理映射除了可以定义前面所说的颜色之外,它还可以定义顶点的其他各种属性,例如顶点法线,顶点位移,金属度等,下面举例一些常见的纹理。(有些就简单介绍一下,等之后学习了更多知识之后再详细补充) 无缝贴图 ---- 先看下面这个例子 ![](data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='348' height='318'></svg>) 我们可以发现,立方体的六个面都是同一个纹理,**也就是一张纹理贴图并不一定要覆盖一个物体表面所有三角形,它可以被重复利用。** 如果一个平面重复利用一个纹理,效果如下: ![](https://pic1.zhimg.com/v2-2a4eb49220ee9e0bd2af8db48e1d1c48_r.jpg) 这种情况就贴瓷砖一般,对于墙壁,地板这类面积较大平面非常的适用。但是我们可以发现这种情况下纹理边缘十分的明显,多数情况下我们并不希望这样,更希望它们看起来像个整体。 因此无缝贴图很适用这种情况,无缝贴图的平铺效果,可以做到边缘的无缝衔接。例如我们最初的砖块的纹理其实就是一个无缝贴图,应用到上面的表面效果如下: ![](https://pic2.zhimg.com/v2-a7f4a3910395b591f7a3f7fef38b0631_r.jpg) 这个墙壁其实是九张一样的纹理拼接起来的,但是因为基本看不出什么缝隙,看起来就如同一个整体一般。 法线贴图(Normal mapping) -------------------- 用纹理来定义顶点的法线,对于这样的纹理我们称之为法线贴图或者凹凸贴图(Bump mapping)。 我们还是以之前的狗狗模型为例,如下图 ![](data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='389' height='409'></svg>) 我们可以发现此时狗狗的皮肤是很光滑的状态,但是我们知道实际上生物的皮肤应该会有各种凹凸不平的褶皱,也就是说更真实一点的话,应该如下图这个样子: ![](data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='326' height='433'></svg>) 但是如果我们使用三角形来表达这些褶皱,那就需要添加无数个细小的三角形来产生凹凸不平的感觉,这明显是一个很难的工作。 因此我们可以利于一个复杂的法线贴图,来定义模型表面三角形各个顶点法线的相对变化。文章最初我们提到了漫反射的公式: ![](https://www.zhihu.com/equation?tex=L_d+%3D+k_d+%5Ctimes+%5Cfrac%7BI%7D%7Br%5E2%7D+%5Ctimes+max%280%2C+%5Cvec%7Bl%7D%5Ccdot+%5Cvec%7Bn%7D%29) 其中向量 n 代表的就是着色点法线,因此当顶点的法线发生变化,那么着色的结果也就会发生变化,那么就可以得到明暗不同的着色结果,让人产生凹凸感。 因此利用**法线贴图并不会改变模型的几何信息**,即原本各个顶点位置的不变,只是通过设置一些假的顶点法线来制造出假的着色结果,来给人凹凸不平的感觉。 该狗狗模型的法线贴图如下: ![](https://pic2.zhimg.com/v2-2cdffc6b448f24d31114d60cafc03ae5_r.jpg) ### 切线空间 从法线贴图中我们可以看出,这张图怎么基本都是蓝绿色的?很鸡儿怪。这是因为我们的切线是定义在切线空间当中的。 何为切线空间呢?切线空间其实是一个局部的空间,即**每个顶点都会有一个它所对应的切线空间**。既然是空间,那么自然会有对应的坐标系,对于空间中的一个坐标系,我们自然要定义它的三个轴的方向以及原点的位置。对于某一个顶点而言,它的**切线空间的坐标系的原点就是顶点本身**。我们知道每个顶点都有它对应的法线,这个**法线方向就是该顶点切线空间的 Z 轴方向**,如下图。 ![](https://pic1.zhimg.com/v2-4cdf8c632459eae1f8d42db44d656a40_r.jpg) 那么接下来我们自然还要定义 x 轴和 y 轴,我们知道 x,y 垂直于 z,那么 x,y 自然在一个与法线垂直且相交于 B 的平面上。但是对于一个平面而言可以找出无数条垂直于 z 轴的线,但模型一般会给定每个顶点一个切线(tangent),因此我们就用该**切线作为切线空间的 x 轴**,那么 **y 轴自然可以由 x 和 z 叉乘得到**,y 轴方向我们称为该顶点的**副切线**。 因此点 B 的切线空间坐标轴如下(emmm,图画的不是很好,抽象的理解下): ![](https://pic2.zhimg.com/v2-22ac80973d896b55b382375c07354729_r.jpg) 定义好坐标系后,我们就可以用这个坐标系的 (x,y,z) 来代表法线了,切线空间的 (0,0,1) 就代表原本的法线,如果要使法线和原本法线有便宜,那么降低 z 轴的值,增加 x 和 y 的值即可。 我们知道 (0,0,1) 在 rbg 里代表蓝色,那么是不是说我切线空间里的坐标系 (0,0,1) 对应到法线贴图里的颜色是蓝色的?答案是错误的! 在切线空间里坐标系确实代表着法线,但是由于我们的法线它在 xyz 三个轴上可能是负数,例如 (0,0,-1) 代表该点新的法线和原法线相反,而 (0,0,-1) 并不能对应到 rbg 颜色上。 也就是说我们**切线空间上 x,y,z 的取值范围是 -1 到 1**,而 rbg 的取值范围是 0 到 1,因此我们需要一个转换,转换公式如下: > rgb = (normal + 1) * 0.5 当然也可以逆变换回去: > normal = rgb * 2 - 1 那么我们原本法线 (0,0,1) 对应的 rgb 值即为((0+1)*0.5 ,(0+1)*0.5, (1+1)*0.5)=(0.5, 0.5, 1),它的颜色如下: ![](https://pic1.zhimg.com/v2-62d301dc5168bd37adccaeb31d900988_r.jpg) 如果我们用这张图作为法线贴图,就会发现,模型表面不会发生任何的变化,因为所有法线依旧保持原样。 位移贴图(Displacement mapping) -------------------------- 前面说了纹理贴图实际上并没有改变三角形顶点的位置,所以在边缘处我们仍旧可以很明显的看出物体表面其实没有凹凸,如下图,球的外边缘依旧是很圆滑的,包括影子也是。 ![](data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='308' height='346'></svg>) 现在有一种贴图称为位移贴图,它就是真的改变了三角形顶点的位置而产生凹凸感,效果也要好很多,如下图: ![](data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='302' height='363'></svg>) 可以看出由于移动了顶点位置,边缘处依旧是凹凸不平的,包括阴影也是。 不过要使用位移贴图,首先需要三角形数量足够多,要跟得上位移贴图定义的频率。在 DirectX 中,提供了一个**动态细分**的方法,即一开始可以三角形偏少,当需要应用位移贴图时再自动细分三角形,即把一个三角形分成很多个小三角形,来匹配位移贴图的频率。 三维纹理 ---- 前面我们定义的纹理都是对于物体表面而言的,例如前面的砖块,但是如果我们这时候把它从中间切开两半,对于内部而言其实就没有纹理与之对应了。 对于这种情况,人们又发明了一种三维纹理,它实际上是**三维空间上的一种噪声函数**,常见的有**柏林噪声**(perlin noise)。对于空间中任何一个点它都能够算出这个噪声的值是多少。也就是说三维空间中有个噪声,然后我们经过一系列的处理可以把它变成我们想要的样子,例如大理石的纹理。 此外三维纹理也可应用在体积渲染里。 光照贴图(Lighting mapping) ---------------------- 光照贴图想必大家也不陌生,其原理就是把我们物体的光照信息:颜色,阴影等,烘焙到贴图上。这样在运行时使用光照贴图就可以在没有全局光照的情况下虚拟出场景被光照的感觉。因为全局光照是很耗性能的,使用这种技术可以降低性能的消耗。当然缺点也很明显,因为是事先烘焙好的阴影,因此在运行时这些阴影不会受外界的影响而改变。 【Unity】使用Compute Shader实现Hi-z遮挡剔除(Occlusion Culling) 【Unity】深度图(Depth Texture)的简单介绍