LabHub
学习 学习路径 课程

着色器与 GPU 管线

把法线装进纹理里

在 LabHub 中继续学习

一句话总结

照明只根据法线来确定亮度,因此如果从纹理中读取法线,平坦的表面也会看起来不平整。将该法线包含在哪个坐标系中是接线空间的问题。

概念图: 亮度的变化 · 只在每个像素上更改法线 · 要包含哪个坐标系的法线 · 接线空间

为什么需要这个?

如果把砖墙的凸凹做成实际的几何形状,需要几十万个三角形。但是,人们识别凸凹的线索大部分是亮度的变化。突出的部分正对着光线,亮,而进入的部分斜着接收光线,暗。

正如在前面的赛道上看到的,亮度是由法线和光方向的内在决定。那么,与其改变几何形状,不如只在每个像素上更改法线,就可以产生相同的亮度变化。在两个三角形的平面上会产生砖头的凹凸。轮廓仍然是平坦的,但从正面看很难区分。

怎么行动

法线是每个成分都在-1到1之间的单位向量,纹理的通道在0到255之间。所以需要转移的规则。

저장할 때  c = (n * 0.5 + 0.5) * 255
읽을 때    n = c / 255 * 2 - 1     (그리고 다시 정규화)

平坦部分的法线(0, 0, 1)变为(128, 128, 255)。正则地图整体看起来呈淡紫色,这就是原因。那个颜色就是“这里是平坦的”的意思。

要包含哪个坐标系的法线是下一个问题。如果直接包含世界空间的法线,该纹理只有在物体处于那个姿势时才正确。如果物体旋转,全部都错了。

所以写接线空间。在表面的每个点上建立以该表面为基准的坐标系。三个轴是这样的。

N (normal)    표면에 수직인 방향
T (tangent)   표면 위에서 텍스처의 u 가 늘어나는 방향
B (bitangent) 표면 위에서 텍스처의 v 가 늘어나는 방향

在这个坐标系中,“平坦性”总是(0, 0, 1)。无论物体是如何放置的,都是一样的。所以可以把相同的法向图贴在多个物体上,即使物体移动,也会保持一致。

T和B是通过顶点位置和UV计算的。将三角形的两边和对应的UV变化量联立求解即可得出。

e1 = p1 - p0,  e2 = p2 - p0
d1 = uv1 - uv0,  d2 = uv2 - uv0
r  = 1 / (d1.x * d2.y - d2.x * d1.y)
T  = (e1 * d2.y - e2 * d1.y) * r
B  = (e2 * d1.x - e1 * d2.x) * r

分母有时为0。当三角形的UV聚集在一个点上或放在一条直线上时,这种三角形就没有定义切线空间。这是展开UV时常见的问题,所以工具会发出警告。

普通地图通常在**高分辨率模型中烘烤出来。**制作一个由数百万个三角形的模型,在低分辨率模型的每个点找到高分辨率表面的法线,然后将其转移到切线空间中写入纹理。所以游戏中的角色只有几万个三角形,但表面的皱纹是保留了数百万个模型的。

也有几种相关的技法。高度地图只保存一个高度,而不是法线,在着色器中计算倾斜度来制作法线。虽然保存的只有三分之一,但计算需要更多。相位映射根据观察方向稍微推动UV,以模仿深度感,在一定程度上表达了仅用普通地图无法表现的“遮挡”。偏移映射完全将顶点推开,所以轮廓也会改变,但需要更多的几何。

在现场相遇的样子

贴上了正常地图,但灯光反向显示的问题大部分是绿色通道的方向。有些工具认为v轴朝上,所以弯曲,有些工具认为朝下。两种惯例混合在一起,凸起就会反向显示。解决办法是在遮罩中翻转绿色通道或重新渲染纹理。

另一个是压缩。如果将普通地图直接使用普通图像压缩,就会明显损坏。这是因为颜色微小的误差人看不到,但法线的误差会因亮度误差而放大。所以使用普通地图专用压缩格式,然后不保存z成分。z = sqrt(1 - x² - y²)也可以恢复为。

而且普通地图不能改变轮廓。物体的边缘仍然看起来很平坦,从斜着看的话,凹凸就会消失。所以轮廓重要的地方,会写其他技法,比如实际高度或时差映射。

最后整理一下普通地图和照明计算相遇的地方。方法有两种。一种是从纹理中读取的切线空间法向量用TBN矩阵转移到世界空间,对世界空间的光进行内积,另一种是相反地将光的方向转移到切线空间,在那里进行内积。结果是一样的,但成本不同。如果只有一束光的话,转移光比较便宜,而且可以在顶点着色器中提前转移进行插值。相反,如果有多束光或同时使用环境地图的话,将法向量转移到世界空间比较好。

下次实习要做的事情

制定将法线转换为颜色并存储的规则,直接生成半球以网格排列的正则地图。然后在切线空间的三个轴上根据顶点位置和UV计算,将相同的平面分别映射为恒定法线和正则地图,比较画面。最后,将光源从左到右移动,抽取六张,用数字确认亮的一面沿着光源移动。