纹理的应用:法线贴图、凹凸贴图与阴影贴图等相关应用原理

《重心坐标与插值、纹理映射》 的内容中,我们详细介绍了纹理映射的概念,以及纹理贴图过大过小带来的种种问题与解决方案,但纹理映射的应用远不止单单作为diffuse的反射系数来表现出不同颜色。本文会详细介绍一些主要的纹理映射的应用及其原理,首先从环境光贴图开始说起。

环境光贴图 Environment Maps

环境光映射,顾名思义就是将环境光存储在一个贴图之上。想象这样一个情形,光照离我们的物体的距离十分遥远,因此对于物体上的各个点光照方向几乎没有区别,那么唯一的变量就是人眼所观察的方向了,因此各个方向的光源就可以用一个球体进行存储,即任意一个3D方向,都标志着一个texel:

进一步就像地球仪一样,利用墨卡托投影或是其它类似的方法将球上的信息转换成一个平面上,就得到了环境Texture了:

但是用一个球体来存储环境光有一个比较明显的缺点,仔细观察上文当中展开的那副Texture图可以观察看到,上方和下方均有较为严重的扭曲,因此另外一种存储的方法就是Cube Map,也就是天空盒:

一个天空盒有6幅Texture来表示,明显相对球体少了很多扭曲的情况,但是中间多了一步从方向到面上的计算:

简单来说就是利用方向计算出与对应平面上的交点坐标,剔除平面所对应的一维,剩下来的两维坐标转换到(0,1)范围之内即为(u,v)坐标。

举个例子: 一个方向为(1, 2, 3) 则其与 z = 1平面的交点为(1/3, 2/3, 1) (找最近的平面交点),剔除 z 轴之后剩下为(1/3, 2/3),在进行(1+x) /2 的转换 (因为方向存在负值,而 uv 坐标不存在),则得到 z = 1的那一幅 Texture 上的 uv坐标为 (2/3, 5/6)。

以下给出分别在光线追踪以及Blinn-Phong模型利用环境映射的伪代码:

光线追踪:

1trace_ray(ray, scene) {
2	if (surface = scene.intersect(ray)) {
3    return surface.shade(ray)
4  } else {
5		u, v = spheremap_coords(r.direction) 
6  	return texture_lookup(scene.env_map, u, v)
7	}
8}

Blinn-Phong模型:

1shade_fragment(view_dir, normal) {
2	out_color = diffuse_shading(k_d, normal)
3	out_color += specular_shading(k_s, view_dir, normal)
4	u, v = spheremap_coords(reflect(view_dir, normal)) 
5	out_color += k_m * texture_lookup(environment_map, u, v)
6}

法线贴图 Normal Maps

在Blinn-Phong光照模型中,法线向量扮演着重要的一环,不同的法线向量对光照的计算结果有着很大的影响,打个比方,倘若将一个高精度模型法线信息套用在低精度模型之上,会使低精度模型的渲染效果有着巨大的提升。

那么如何做到呢?我们知道Texture上可以存储3维的颜色信息作为漫反射系数,那么自然也就可以存储法线向量的信息!同样利用(u, v) 坐标去查询每个点的法线向量,而不使用原来模型法线信息,达到各种不同的效果,这就是Normal Maps。

法线贴图的作用是在平面上模拟凹凸效果,以达到节省模型资源的目的的。所以要理解法线原理,就要先理解人眼是如何识别凹凸效果的。这并不是什么复杂的科学知识,只要你还记得初中物理的基础光线反射原理就能理解。可以理解为当光线反射角度发生变化时,我们就能看到(后者理解到)凹凸结构。此时变位思考一下,只要我们改变光线反射角度我们就能模拟一个凹凸结构。

对于法线贴图的三个通道里包含的黑白图片才是法线贴图的根本。他们的作用在于用黑白的数值来控制光线的反射角度。

红通道(R)控制光线的左右角度,X 轴偏向

绿通道(G)控制光线的上下角度,Y 轴偏向

蓝通道(B)模拟模型的深浅,Z 轴偏向

以上所说的左右上下都只针对于贴图的方向,即左右为 X 轴、上下为 Y 轴、深浅为 Z 轴,永恒不变!并不针对于模型的方向,我们所说的法线贴图属于物体法线,并非世界法线,请牢记!如果是世界法线,计算方式是完全不同的!

为什么法线贴图都呈现紫蓝色?

了解这一点对于使用法线贴图并不重要!我们可以将黑白颜色按照灰度值(0~255)来理解,红通道中,黑色即为 0 白色即为 255,每个数值对应控制一个角度,因为灰度值不存才小数点,所以0~255一共256个数值平分0~180度,约等于每个数值控制0.7度角。

RGB 颜色值用于存储矢量的 X、Y、Z 方向,其中的 Z 为“向上”(与 Unity 通常使用 Y 作为“向上”的惯例相反)。此外,纹理中的值视为经过减半处理,即添加了 0.5 的系数。这样就能存储所有方向的矢量。因此,为了将 RGB 颜色转换为矢量方向,必须乘以 2,然后减去 1。例如,RGB 值 (0.5, 0.5, 1) 或十六进制的 #8080FF 将得到矢量 (0, 0, 1),这便是用于法线贴图的 “向上”,并表示模型表面没有变化。所以法线贴图很多区域都是蓝色的原因。

凹凸贴图 Bump Maps

Bump Maps 其实与 Normal Maps 十分类似,Normal Maps直接存储了法线信息,而Bump Maps存储的是该点逻辑上的相对高度 (可为负值),该高度的变化实际上表现了物体表面凹凸不平的特质,利用该高度信息,再计算出该点法线向量,最后再利用该法线计算光照,这就是Bump Maps的过程,只不过比直接的Normal Maps多了一步从height到normal向量。

如何从相对高度计算出法线向量呢?

其实很简单,对于二维的情况来说,我们只需要根据模型表面法线找到对应的高度,再计算该点的法线即可:

三维情况可以类推得到,只是提升了一个维度而已,需要注意的是,所有计算出来的法线都是局部坐标即切线空间之下,因此还需要左乘切线空间的变换矩阵转为世界空间。

位移贴图 Displacement Maps

Displacement Maps其实与Bump Maps 十分类似了,Bump Maps是逻辑上的高度改变,而Displacement Maps则是物理上的高度改变,二者的区别就在此处,可以通过物体阴影的边缘发现这点,可以发现位移贴图实际上是移动了顶点,相当于模型本身就已经变了:

阴影贴图 Shadow Maps

从光栅化的过程一路走来会发现阴影这个问题一直没有涉及,今天就可以真正的利用阴影贴图来一定程度上的解决这个问题了!

首先,思考一个问题,为什么会有阴影?

因为光源照射不到,更具体点,摄像机能看到的地方,光源 “看” 不见。

而这正是启发阴影贴图这种做法的动机,接下来我们便来看看详细过程是怎么样的。

第一步,把光源当做一个摄像机让它去看,去渲染整个场景一遍从而得到从光源视角的深度Buffer,记为 $d_{map}$ ,注意:这里的 $d_{map}$ 即为阴影贴图。

第二步,从设定好的摄像机位置去真正的渲染场景得到摄像机视角的深度Buffer,记为 d

将所有摄像机视角可见点,利用光源视角下的那一套投影矩阵,重新投影回光源,找到与之对应的 $d_{map}$ 上的深度值,如果该点在 $d_{map}$ 上的深度值与 d 上的深度值相等,则说明此点可被光源与摄像机共同看见,因此不在阴影中,如下图这种情况

如果该点在 $d_{map}$ 上的深度值小于 d 上的深度值,则说明此点不可被光源看见,但摄像机看得见,即该点前方有物体遮挡,因此在阴影中,如下图这种情况

如此便能确定每个可见像素点是否在阴影之中了,如果在阴影之中就不去计算 Blinn-Phong 中的镜面反射项与漫反射项。效果如下图:

对应可视化的 shadow maps 如下,距离光源越近代表深度越小,所以颜色越黑,反之亦然:

对于 shadow maps 还有几点小细节可以谈 1、浮点数难以判断相等,所以一般会有一个tolerance

2、shadow maps 查询时不采用双线性插值,只寻找最近的点,因为倘若插值发生在物体边缘时,与邻接点的深度差距很大,会导致插值结果会有很大的误差

3、属于硬阴影,只适用于点光源

Reference

《GAMES101-现代计算机图形学入门-闫令琪》

《法线贴图原理解析》

《Fundamentals of Computer Graphics 4th》

《计算机图形学笔记》

《Unity User Manual 2021.1 StandardShaderMaterialParameterNormalMap 》