zoukankan      html  css  js  c++  java
  • 法线贴图那些事儿

    概述

    在学习法线贴图的过程中,有几个比较难以理解的概念,这里记录一下。特别说一下,本文的法线贴图是切线空间下的法线贴图。

    空间变换

    空间变换示意图

    如上图所示,简单表达了在使用法线贴图的过程中,涉及到的几个空间变换:

    1. 切线空间:从法线贴图中采样得到的法线,在切线空间中;

    2. 对象空间:物体的本地坐标空间,顶点的相关信息,在对象空间;

    3. 世界空间:光源位置、观察者位置等,在世界空间中。

    在空间变换的过程中,主要涉及到了两个变换矩阵:

    1. (TBN)矩阵:从切线空间变换到对象空间;

    2. (Model)矩阵:从对象空间变换到世界空间。

    对于上述概念,大部分都是比较熟悉的,只有法线贴图、切线空间和(TBN)矩阵比较陌生。下面,将分别介绍一下。

    法线贴图

    在3D计算机图形学中,法线贴图是一种用于伪造凹凸光照的技术,是凹凸贴图的一种实现。它用于添加细节,而不使用更多的多边形。这种技术的一个常见用途是,通过从高精度多边形或高度图生成法线贴图,来极大地增强低精度多边形的外观和细节。下图来自Paolo Cignoni,图中对比了两种方式:

    法线可以使低精度模型实现高精度模型的效果

    法线贴图通常存储为常规RGB图像,其中RGB分量分别对应于表面法线的X,Y和Z坐标。

    法线的每个分量的值的范围是([-1,1]),而RGB分量的值的范围是([0,1])。所以,在将法线存储为RGB图像时,需要对每个分量做一个映射:

    [vec3 quad rgb\_normal = normal * 0.5 + 0.5 ]

    这里要注意,将法线存储到法线贴图的过程中,需要进行上述操作。当我们从法线贴图中读取到法线数据后,需要进行上述变换的逆变换,即从([0,1])映射到([-1,1])

    切线空间

    那么,法线向量应该相对于哪个坐标系呢?我们可以选择模型顶点的的坐标系,即对象空间;也可以选择模型纹理所在的坐标系,即切线空间,也称为纹理空间。

    对象空间中,法线信息是相对于对象空间的朝向的,各个方向的法线向量都有,所有贴图看起来色彩比较丰富;而在切线空间中,法线是相对于顶点的,大致指向顶点信息中的法线方向,即法线向量接近于((0,0,1)),映射到RGB是((0.5,0.5,1)),这是一种偏蓝的颜色。下图分别是对象空间和切线空间下的法线纹理。

    对象空间和切线空间下的法线纹理

    那么,怎么进行选择呢?考虑一下二者的优缺点:

    1. 重用性:对于对象空间的法线贴图,它是相对于特定对象的,假如应用到其他的对象上,可能效果就不正确了;而切线空间中的法线贴图,记录的是相对法线信息,所以可以把它应用到其他对象上,也能得到正确的结果。

    2. 可压缩:考虑到法线向量是单位向量,而且Z分量总是正的,可以只存储XY方向,而推导出Z方向。

    综上所述,我们一般选择切线空间下的法线贴图。

    (TBN)矩阵

    在光照的计算过程中,需要用到光线方向、视线方向和法线方向等,为了得到正确的结果,这些变量必须在同一坐标系下计算。参考一下本文开头的“坐标变换示意图”。

    在纹理坐标系中,x和y分量与2D图片的水平方向和垂直方向对齐,而z分量指向图片外部的上方。如下图所示:

    纹理坐标系

    为了正确使用贴图中的纹理信息,我们必须找到一种方法——从切线坐标空间变换到对象空间。这可以通过指定切线坐标系的坐标轴在对象空间中的方向来达到。

    对一个单独的三角形面片来说,我们可以认为纹理贴图覆盖在三角形的表面上,如下图所示:

    根据上图,可以得出:三角面片和纹理贴图是共面的。那么,根据平行四边形法则,可以得出:

    [egin{matrix} E_{1} = Delta U_{1}U + Delta V_{1}V \ E_{2} = Delta U_{2}U + Delta V_{2}V \ end{matrix} ]

    其中,(E_{1})(E_{2})是两个顶点之间的向量差,可以根据顶点的坐标计算出来;(Delta U_{1})(Delta V_{1})(Delta U_{2})(Delta V_{2})分别是纹理坐标的水平和垂直方向的差,可以根据纹理坐标计算得到。(U)(V)分别是纹理的水平和垂直坐标轴,是要计算的未知量。

    写成坐标表示:

    [egin{matrix} left( E_{1x},E_{1y},E_{1z} ight) = Delta U_{1}left(U_{x},U_{y},U_{z} ight) + Delta V_{1}left( V_{x},V_{y},V_{z} ight) \ left( E_{2x},E_{2y},E_{2z} ight) = Delta U_{2}left(U_{x},U_{y},U_{z} ight) + Delta V_{2}left( V_{x},V_{y},V_{z} ight) \ end{matrix} ]

    上面的方程,也可以写成矩阵乘法的形式:

    [egin{bmatrix} E_{1x} & E_{1y} & E_{1z} \ E_{2x} & E_{2y} & E_{2z} \ end{bmatrix}= egin{bmatrix} Delta U_{1} & Delta V_{1} \ Delta U_{2} & Delta V_{2} \ end{bmatrix} egin{bmatrix} U_{x} & U_{y} & U_{z} \ V_{x} & V_{y} & V_{z} \ end{bmatrix} ]

    两边同时乘以(Delta U Delta V)的逆矩阵,可得:

    [egin{bmatrix} U_{x} & U_{y} & U_{z} \ V_{x} & V_{y} & V_{z} \ end{bmatrix}= egin{bmatrix} Delta U_{1} & Delta V_{1} \ Delta U_{2} & Delta V_{2} \ end{bmatrix}^{-1} egin{bmatrix} E_{1x} & E_{1y} & E_{1z} \ E_{2x} & E_{2y} & E_{2z} \ end{bmatrix} ]

    求逆矩阵不太方便,可以使用伴随矩阵法

    [egin{bmatrix} U_x & U_y & U_z \ V_x & V_y & V_z end{bmatrix} = frac{1}{Delta U_1 Delta V_2 - Delta U_2 Delta V_1} egin{bmatrix} Delta V_2 & -Delta V_1 \ -Delta U_2 & Delta U_1 end{bmatrix} egin{bmatrix} E_{1x} & E_{1y} & E_{1z} \ E_{2x} & E_{2y} & E_{2z} end{bmatrix} ]

    至此,我们求出了(U)(V)向量。但是我们需要的构成(TBN)空间的坐标轴是正交的,这里求出的(U)(V)并不一定能满足正交的条件。这里,顶点的法线(N)是已知的,我们可以根据(U)(V)(N),根据格拉姆-施密特正交化方法,求出与(N)正交的(T)(B)(此处假设切线空间是右手坐标系):

    [egin{aligned} T &= normalize left( U - dot left( U,N ight) * N ight) \ B &= normalize left( cross left( N,T ight) ight) \ TBN &= mat3 left( T,B,N ight) end{aligned} ]

    这样,我们获得了坐标轴相互正交的(TBN)矩阵。

    实际使用中的法线贴图

    看完上面计算切线空间的(TBN)矩阵的部分,估计也是头大。不禁想到,每次使用法线贴图的过程,真的如此麻烦吗?

    幸运的是,答案是否定的。

    一般情况下,模型存储的顶点信息中,都包含了顶点的法线和切线的数据,这样,我们就不用进行上面的复杂计算了,直接使用法线和切线的叉乘,求出副切线,从而构成(TBN)矩阵。

    参考

  • 相关阅读:
    python字符串格式化 %操作符 {}操作符---总结
    Python中TKinter模块中的Label组件
    Python中urlopen()介绍
    Python中else语句块(和if、while、for、try搭配使用)
    Python文件操作汇总
    python socket 编程之三:长连接、短连接以及心跳
    python 源代码分析之调试设置
    python 调用shell或windows命令
    python socket 编程之二:tcp三次握手
    python socket 编程之一:编写socket的基本步骤
  • 原文地址:https://www.cnblogs.com/bzyzhang/p/12954603.html
Copyright © 2011-2022 走看看