首先感谢两位精彩的笔记总结和Games101的授课老师
https://zhuanlan.zhihu.com/p/473890803
https://blog.csdn.net/Motarookie/article/details/121775835
如图,球上的点共用同一个着色模型(着色计算方式),但不同位置有不同颜色,什么情况?
就漫反射这一项来看,其实是球上不同点的漫反射系数kd不同,所以计算出来每个点的漫反射项Ld是不同的

如何快捷方便的定义球上不同位置的对应不同属性呢?
任何三维物体的表面可以展开成一个平面,
纹理:纹理是二维的图像 Surfaces are 2D
纹理映射:
每个三维物体表面上的一个点在2D图像中对应一个位置 Every 3D surface point also has a place where it goes in the 2D image (texture),根本作用是定义物体表面任何一点的不同属性
下图就是一个球映射一张世界地图,最终在屏幕空间中输出结果是一个地球仪

再比如下面这个孤儿图,运用布林冯模型计算着色后输出到屏幕和映射一张纹理图后输出到屏幕的差别
模型每个三角形面都对应到纹理上的某个三角形

学习纹理后,我们的模型顶点存放的就不只是坐标信息了,还要存放uv坐标用于纹理映射
定义纹理坐标uv(u, v约定俗成范围0~1)、将纹理映射到表面的过程
顶点内存放了uv坐标就知道该顶点应该映射到纹理的哪个点,然后三角形的三个顶点都有各自对应的纹理图上的uv坐标后,三角形内部的面应该对应到哪里就能够知道了,插值呗
有一类纹理是循环使用的,比如以下这个建筑,一张纹理图被贴了很多很多次,右图的颜色代表纹理坐标大小,深色接近1,浅色接近0,可以明显看到纹理坐标在相邻两张纹理图的连接处会突变,但是实际图像中却看不出突变,因为纹理被做成了左右/上下边界完美重复的类型

重心坐标插值 Interpolation Across Triangles: Barycentric Coordinates
为什么要进行插值 Why do we want to interpolate?
上节课的像素着色那里提到,为了在三角形内部得到平滑的过渡 Obtain smoothly varying values across triangles
纹理坐标、颜色、向量的插值等属性都可以插值 Texture coordinates, colors, normal vector
重心坐标概念
- 三角形任何一个点(包括顶点)都可以表示成三个顶点的线性组合
- 满足以下关系式的三个系数(α,β,γ)就是该点的重心坐标
- 还有一个限制条件是这三个系数必须非负,否则就在三角形外了
- 简而言之,就是三角形内部一个点(x,y)在重心坐标下的表示就是(α,β,γ)

- 其中三个顶点自己的重心坐标分别是
A:(1,0,0)
B:(0,1,0)
C:(0,0,1)
重心坐标计算方式Barycentric Coordinates: Formulas

由此可知,三角形质心重心的坐标是\(\left( \frac{1}{3},\frac{1}{3},\frac{1}{3} \right)\)
也可以直接用以下公式计算:
\[ \beta = \frac{(y – y_A)(x_C – x_A) – (x – x_A)(y_C – y_A)}{(y_B – y_A)(x_C – x_A) – (x_B – x_A)(y_C – y_A)} \] \[ \gamma = \frac{(y – y_A)(x_B – x_A) – (x – x_A)(y_B – y_A)}{(y_C – y_A)(x_B – x_A) – (x_C – x_A)(y_B – y_A)} \] \[ \alpha = 1 – \beta – \gamma \]
- A(xA,yA)、B(xB,yB)、C(xC,yC):三角形三个顶点
- P(x,y)待求坐标的点
- α,β,γ:点 P 对应顶点 A,B,C 的重心坐标,满足 α+β+γ=1
重心坐标的使用
已知重心坐标的计算方法,怎么插值属性呢?
出来的重心坐标(α,β,γ),其实就相当于一个权重比,直接把对应的VAVBVC换成uv坐标、颜色、法线、深度值、材质属性,V即所求 can be positions, texture coordinates, color, normal, depth, material attributes…
- 重心坐标在投影变换下会发生变换,如果想利用重心坐标插值三维中的属性,应该在投影前就插值,具体的操作是对投影后的三角形应用逆变换再插值 Barycentric coordinates are not invariant under projection
纹理映射产生的问题
- 像素(Pixel):是屏幕上的最小发光点。你在显示器上看到的每一个小方块,就是一个像素
- 纹素(Texel):是图片文件(纹理)里的最小色块。当你在 Photoshop 里打开一张游戏贴图放大看时,那些马赛克小方块就是纹素
我们要渲染的物体正面对着摄像机,并且刚好纹理大小与渲染输出分辨率相同,则映射每个 pixel 到 texel 时,就能得到整数的纹素。但是大多数情况下,物体不是正正好好对着摄像机,就算是正好对着摄像机,也可能出现纹理太小/太大,像素映射纹素的时候映射不到整数位置
纹理太小

当你把一个物体拉近镜头、或者纹理本身分辨率很低时,屏幕上需要用很多 pixel 来显示一个很小的纹理区域。但纹理区域里的 texel 数量不够细,于是就出现:
- 在查询纹理贴图时会导致多个像素(pixel)映射到了同一个纹素(texel) ,视觉上就会变得“糊/马赛克/细节不够
- 解决方法:插值
比如一面墙渲染出来分辨率1024*1024,但是它对应的纹理图仅有256×256。为了把纹理图应用到墙上,显然纹理需要放大,然后贴到墙上。结果就是:我的墙可能会查到一些不是很正(不在纹素的中心)的值
对应歪了咋办?根据处理办法不同会产生以下效果

取最近(Nearst)
- 直接四舍五入取最近的一个纹素作为映射结果
- 注意下图中的一个方块可不是一个像素,而是n·n个像素,只是位置上接近的好几个像素(如4×4个像素)映射到同一个纹素,则这堆像素显示同一个颜色,所以这一块像素区域看起来就是一个大像素,出现严重锯齿感觉

这样太丑了,所以还有后面两种方法
双线性插值(Bilinear Interpolation)
从纹理图的角度来看映射不到整数的情况,屏幕上的一个像素映射到纹理图上的某个点(红点),这个点是没有属性定义的,上个方法是把这个坐标直接使用最近的一个texel的信息,导致很附近多个像素都显示一样的颜色——明显锯齿

双线性插值:根据四周的纹素属性,算出红点处的颜色应该是什么样的
找到实际映射点(红点)附近的4个纹素点
(1)水平上,用距离s,根据线性插值公式,很容易计算出u0和u1的属性
(2)竖向上,用距离t,对u0和u1再做一次插值,即最终结果。

从而得到有一点模糊但是更平滑的图形。
本质还是走样(Aliasing):
采样频率-分辨率
信号频率 – UV分辨率,
如果纹素和像素的对应关系没处理好,远处的纹理会闪烁、出现摩尔纹(锯齿状波纹)。
业内还有一种是从纹理图的解决方案Mipmap(多级渐远纹理):会给纹理准备不同大小的版本(原图、1/2 大小、1/4 大小…)。远处的物体用小纹理(纹素大),近处的物体用大纹理(纹素小)
双立方插值(Bicubic Interpolation)
跟双线性插值类似,但取的是周围16个texel。用得不多。
纹理太大
当像素覆盖纹理过于大时,很难用一个像素代表覆盖纹理上多个纹素,就会出现走样现象
会造成进处出现锯齿,远处出现摩尔纹。


这个图能完美的解释为什么近处是锯齿,远处是摩尔纹,采样频率为像素点的密度
- 近处的高频信息发生在边界处,边界处变化太快(高频),而采样频率只有像素中心那么密,边界只能被拼成台(MSAA/SSAA就是在边界附近提高采样密度,相当于提高 f_s,让一个像素里能“看见”边界穿过的比例。)
- 远处时,物体在屏幕上很小:一个像素对应物体表面一大片区域。此时“高频信息”不再主要来自几何边界(边界可能只占几个像素),而是来自纹理/表面细节,但采样仍然是用这一个点代表一整块区域这些“随机漏采/错采”的结果,会形成高频细节被欠采样后折叠成低频假花纹
怎么解决?
- 解决办法1:增加采样点个数
如下图每个像素不再是只采样中心点,增加到512个点,采样512次最后再对采样结果求平均。可行但是性能会明显降低

- 解决方案2: MipMap
采样会引起走样,如果我们不采样,只得到一定范围的平均值就能解决问题
之前的,一个像素对应一个纹素,或者纹理过大一个像素对应不到一个纹素,对应歪了,我们采用的方式是取最近纹素或者插值求出该对应点的颜色属性,这叫点查询
而MipMap就是范围查询的算法代表了
MipMap – 多级渐远纹理
纹理图上任意一个区域,我要很快速的得到这个区域的颜色平均值是多少这就是MipMap的意义。
MipMap的特点:范围查询特别快、数值不准、必须是方形查询
MipMap:就是用一张图生成一系列图
原始图为0层,层数越高分辨率越低。分辨率越低,其实图片的尺寸是会变小的(比如1层应该是0层的4分之一),但是为了看得清楚,还是保持原来大小。这么多层的额外存储量只有1/3(等比数列求和)

计算mipmap等级 Computing Mipmap Level D
选择哪一层MipMap作为我们要使用的那一张纹理呢?
通过将像素的中点映射到纹理上再计算距离的方法来得到一个像素近似的正方形区域边长L是多少
然后计算应该选择MipMap哪一层来查询
如果区域大小为1×1,不就意味着1个像素映射过去刚好是1像素大小的区域,直接在0层查询。
如果区域大小为4×4(一个像素对应更大的范围,远处),如果还用第一层纹理就会丢失3乘3的区域的信息)因此我们要找到在第几层区域对应的大小能恢复成1乘1,显然这个区域会在1层上变成2×2,在2层上会变成1×1,则应该在2层查询。
以此类推可以得到公式
因此取2的对数得到等级D(即为求在第几层是该像素映射的纹理也是一个像素大小的区域)

问题又来了,D算出来如果是0.8层呢?
三线性插值 Trilinear Interpolation
三线性插值即可先用双线性插值分别算出0层对应的值x1,1层的值x2,再纵向的做一次插值lerp(0.8,x1,x2)即可
- 简单通过这种方法只能查询整数层数上的情况,在实际应用纹理时不连续,因此在查询时可以先用双线性插值分别得到相邻两个整数层级上的值,再用得到的值再做一次插值(三线性插值)得到连续的值
- 三线性插值在实时渲染有着广泛应用,因为它能得到连续的结果,并且只需要做两次查询和一次插值,开销较小


Mipmap的问题
- 与超采样结果相比,使用mipmap得到的结果在远处出现了过渡模糊的现象(Overblur)

这是因为mipmap只能查询正方形区域,映射得到的结果近似为正方形太过勉强,另外三线性插值本身也是近似,将不规则的图形近似为矩形
各向异性过滤 Anisotropic Filtering

mipmap相当于只可以查询下图(Ripmap)左上到右下对角线上为正方形的纹理,而使用各向异性过滤可以查询矩形区域,可以较好解决映射在纹理上的竖直或横向的矩形区域,但是对于斜向的长条状区域仍然不能很好的解决。游戏设置中多少X就是分别在两个方向压缩几次,随着X不断变大,最终会收敛到原图3倍的空间
EWA过滤 EWA filtering
EWA过滤可以把任意不规则的形状拆成很多个的圆形去覆盖这个形状,但是多次查询会增加耗时

纹理贴图的应用 Application of Texture
总述:有许多种类的贴图 Many, Many Uses for Texturing
- 在现代GPU中,纹理就是一块可以快速做点查询、范围查询的内存区域,不仅限于图像 In modern GPUs, texture = memory + range query (filtering)
- Environment lighting
- Store microgeometry
- Procedural textures
- Solid modeling
- Volume rendering
- …
环境光贴图 Environment Map
环境光贴图把来自各个方向的光照记录下来,假设环境光来自无限远,只记录方向,来表示环境光(右侧为经典模型犹他茶壶 Utahpot)

球面环境映射(Spherical Environment Map)可以类比为地球仪,但是贴图上下部分存在拉伸和扭曲问题

Cube Map把环境光记录在立方体的表面上,沿法线方向把正方体映射到球面上,缺点是多了计算方向判断在正方体哪个面上

凹凸/法线贴图(Bump/Normal Map)
不改变表面真实几何位置,只修改光照计算用的法线方向,让平面看起来像有凹凸细节

- 在着色计算过程中逐像素地扰动表面法线,由此定义每个纹素高度偏移 Perturb surface normal per pixel(for shading computations only), “Height shift” per texel defined by a texture
- 二维下扰动法线的具体过程 How to perturb the normal (in flatland):定义切线然后算出法线的偏移
- 原本的表面法线 \(n(p) = (0, 1)\)
- p的导数 \(dp = c \cdot [h(p+1) – h(p)]\)
- 扰动法线是 \(n(p) = (-dp, 1).\text{normalized}()\)

- 原本的表面法线 \(n(p) = (0, 0, 1)\)
- \(p\) 的导数 \(\frac{dp}{du} = c_1 \cdot [h(u+1) – h(u)]\),\(\frac{dp}{dv} = c_2 \cdot [h(v+1) – h(v)]\)
- 扰动法线是 \(n = \left(-\frac{dp}{du}, -\frac{dp}{dv}, 1\right).\text{normalized}()\)
位移贴图 Displacement mapping
用纹理里的高度值直接移动表面顶点或表面点的位置,从而真的改变几何形状
- 相比于凹凸贴图更真实,使用相同的原理和纹理使顶点发生移动
- 代价是要求模型本身面数足够多,需要顶点间间隔比定义的采样频率高(DirectX提供了动态的曲面细分,仅在需要的时候提供足够的面数)

normal mapping的问题:
normal map 能改表面朝向的视觉感受,但不能改物体外轮廓,因此在边缘处就露馅了,你看和右边对比左侧的轮廓是平的
如果真有凹槽,裂缝,孔洞,这些结构,那么光线打过来时:
- 凸起会挡住后面的地方
- 凹进去的地方会更难见到光
- 视线方向变化时,有些地方会被自己挡住
这些都依赖于真实高度和空间位置。但 normal map 没有真的把点移开,只是把法线改了,因此当自身结构复杂时无法正确显示自己与自己的投影关系。
程序化纹理 3D Procedural Noise&Procedural textures
不用存一张现成图片,而是用数学函数根据位置直接算出每个点的颜色或材质属性
- 定义了三维空间中的噪声函数,并没有真正生成一张图像
- 比如有柏林噪声函数 Perlin noise :n(x,y,z)
并不是先生成了一张 1024×1024×1024 的大体素图再去查,而是“问到哪里,就算哪里” - 渲染时某个像素对应到表面上一点 p,就算:n(p_x,p_y,p_z)
- 如果另一个像素对应另一点 q,再算:n(q_x,q_y,q_z)
- 比如有柏林噪声函数 Perlin noise :n(x,y,z)
- 函数可以经过各种处理变成自己想要的样子,如使用纹理表示山脉起伏的信息
- 原始噪声通常只是一个基础信号,整体看起来随机, 但邻近位置的值是平滑连续的
它本身不一定直接像山、像木纹、像大理石, 要靠后续加工
- 原始噪声通常只是一个基础信号,整体看起来随机, 但邻近位置的值是平滑连续的

预计算着色 Precomputed Shading
环境光遮蔽(Ambient occlusion)可以提前计算好储存在贴图中,使用时将其他着色结果乘以环境光遮蔽的结果得到带有环境光遮蔽的结果

三维纹理和体渲染 Solid modeling and Volume rendering
采样的不是表面,而是整个体积内部。光线穿过去时不断读取密度并累积,就能把内部信息显示出来







