目录

  • 1 法线贴图
    • 1.1 为什么需要?
    • 1.2 怎么做法线映射?
  • 2 切线空间
    • 2.1 为什么需要切线空间?
    • 2.2 切线空间是什么?
    • 2.3 TBN矩阵
    • 2.4 TBN矩阵计算
  • 3 光照计算是在`世界空间`还是`切线空间`
    • 3.1 在世界空间计算
    • 3.2 在切线空间中计算

这个概念也困扰了我很久,我觉得网上的文章对于我这种菜鸟非常不友好,因此本文以尽可能简单的语言说清楚 切线空间、TBN矩阵的相关概念。
不说给完全整明白,整个差不多明白吧

1 法线贴图

1.1 为什么需要?

\quad\quad 参考下面这张图,4个顶点两个三角形组成的一个平面,只是映射了一个颜色纹理贴图,属实非常光滑。

\quad\quad 光滑是因为每个顶点定义的法线都是垂直于这个平面的,在像素着色器中,每个像素通过插值得到的法线也都是一模一样垂直于这个平面,然后用每个像素的法线进行光照计算,得到的肯定是这种完全平坦的渲染结果。
\quad\quad 想要增加它的细节,让墙面看起来更真实(凹凸不平、麻麻赖赖),一种方法就是用更多的三角形来表现这一面墙,比如几千个,但这样的性能开销就提高了几百甚至几千倍,不行。
\quad\quad 模型太费性能,我们不从模型这方面入手,而是从光照角度来看。为什么表面被照亮成一个完全平坦的平面呢?答案是表面每个片段的法向量整整齐齐非常一致(如下图),光照的计算不管你模型什么样,只看法线,因此计算出来的光照没什么差异,仅仅是每个点颜色不同而已。下面这张图可以看到,实际表面(Actual surface)的每个片段法线都很一致,因此通过光照计算,让我们感知到的表面(Perceived surface)就很平坦

这时候自然就有一个想法:像映射颜色贴图一样,搞一个法线贴图啊,这样不就每个片段的法线都很多样性了嘛!

\quad\quad 非常正确,通过让每个片段法线方向都不一致,我们可以欺骗光,使光线相信表面不是平的,是由许多微小的平面组成,从而使表面在细节上得到巨大的提升。这种技术就是 法线映射凹凸贴图。应用于砖平面,它看起来有点像这样:
成本相对较低的情况下提供了巨大的提升,只更改每个片段的法向量,因此无需更改关照计算方式


1.2 怎么做法线映射?

\quad\quad 使用2D纹理来存储每个片段的法线数据。通过这种方式,我们可以对2D纹理进行采样,以获得该特定片段的法向量。
\quad\quad 一般纹理上的每个纹理像素(简称纹素texel)格式都是RBG或者RGBA,即取值范围是 [0,1],法线3D矢量每一个分量的取值范围是 [-1,1],因此首先要把法线映射到[0,1]范围来存储它。将法向量转换为像这样的 RGB 颜色分量,我们可以将每个片段法线存储到 2D 纹理上。

vec3 rgb_normal = normal * 0.5 + 0.5; // transforms from [-1,1] to [0,1]

为什么几乎所有法线贴图,都是蓝色色调?—— 这是因为法线都紧挨着正 z 轴(0,0,1)向外指向屏幕外,RGB(0,0,1)就是蓝色。其深浅略有不同是因为每个片段的法线都相对于正z轴略微有偏移。图中的每块砖的上边缘部分,颜色更偏绿色(0,1,0),是因为现实中的砖,在其上部接缝处的法线就是更接近正y轴,也就会偏绿色多一些。

通过像素着色器中每个片段都采样该法线纹理,计算光照后就能得到很真实的效果,因为效果看起来变得凹凸不平了,所以法线贴图可以叫做凹凸贴图


2 切线空间

2.1 为什么需要切线空间?

\quad\quad 前面提到过,法线贴图上的法向量都指向正z方向。上面的图之所以能够正确渲染,是因为砖墙平面正好朝向正z方向。

如果我们对铺设在地面上的一个表面,这个表面朝向正y轴,映射相同的法线贴图,会怎样?
—— 得到一个完全错误的结果

我现在想要的是各个片段的法线方向大致指向正y轴,并且各不相同,各自表现着细节。但是映射法线贴图之后,所有的片段拿到的法线还是指向正z轴,拿这些法线计算光照,就会产生错误。

一张法线贴图只能用在一个平面上?
一个模型有上百万的三角面,我要搞几百万张贴图?

为了解决复用问题,切线空间应运而生


2.2 切线空间是什么?

切线空间(Tangent Space):一种以顶点的法线为Z轴,以及顶点所在表面的切向量(Tangent Vector)、副切向量(Bitangent Vector)为基础的局部坐标系。切向量、副切向量、法向量相互正交,是模型局部空间中,一种特殊的局部坐标系


2.3 TBN矩阵

\quad\quad TBN矩阵是将切线空间中的向量转换到模型空间所必须的矩阵。

  • T:正切向量 Tangent
  • B:副切向量 Bitangent
  • N:法向量 Normal

只要已知T、B、N三个向量就能组成一个TBN矩阵

只需要算出这个矩阵,就能解决所有问题。那么问题就只剩一个:如何计算TBN矩阵?

2.4 TBN矩阵计算

首先提一些很容易被忽略,但却对理解 TBN矩阵法线空间 至关重要的概念(可能是默认大家都会吧)

  • 任何一个表面都有一个局部空间,比如:以该表面左下角为原点,可以精准的表示在这个表面上的任何点坐标,甚至是表面上空的坐标,因为这些坐标是相对于这个表面左下角而言的,因此他们是局部坐标。
  • TBN矩阵是针对目标表面的,不同表面对应不同的TBN矩阵,因此很明显,计算TBN矩阵需肯定是通过目标表面的一些属性
  • 因为我们要把法线贴图上定义在切线空间的法向量,从切线空间转换到目标表面的局部空间,所以需要一个转换矩阵TBN

计算TBN矩阵,我们需要目标平面上的三个相互垂直的向量:up vectorright vectorforward vector

  • up vector: 对应于N向量。其实是已知的。它是目标平面的法向量,通过组成该表面的顶点计算而来。面法线 = 周围N个顶点的法线平均值

    注意:每个顶点依然有带法线属性,虽然我们没用他们来插值每个片段。而是每个片段映射法线贴图来获取法线

  • right vector: 对应于T向量
  • forward vector: 对应于B向量

计算T、B向量比较麻烦,需要:该三角形面的3个顶点的位置坐标纹理坐标

如果不喜欢看推导过程可直接不看,把公式翻译成代码计算就行

假设一个三角形为 P 1 , P 2 , P 3 P1,P2,P3 P1,P2,P3,纹理坐标分别是 ( U 1 , V 1 ) , ( U 2 , V 2 ) , ( U 3 , V 3 ) (U_1,V_1),(U_2,V_2), (U_3,V_3) (U1​,V1​),(U2​,V2​),(U3​,V3​)。

  • 先看边 E 2 E_2 E2​

    • E 2 E_2 E2​的纹理坐标差值为ΔU2和ΔV2,注意这里的两个差值为 标量
    • E 2 E_2 E2​的位置坐标差值,为 向量(公式中加粗了)
    • 建立第一个方程(未知数为T、B向量)
      E 2 = Δ U 2 T + Δ V 2 B \mathbf{E_2} = \Delta U_2T +\Delta V_2B E2​=ΔU2​T+ΔV2​B
  • 同理 E 1 E_1 E1​边也可以用这种方式得到一个方程
    E 1 = Δ U 1 T + Δ V 1 B \mathbf{E_1} = \Delta U_1T +\Delta V_1B E1​=ΔU1​T+ΔV1​B

很明显了吧,两个方程两个未知数,T和B是必然能够求出来的。但是这里面的变量不是一维,因此要用线性代数的知识来解这个矩阵方程(应该可以这么叫吧,线代很多名词已经忘了- -)。

把上面的方程组每个分量都显示出来,则变成下面这种形式:
( E 1 x , E 1 y , E 1 z ) = Δ U 1 ( T x , T y , T z ) + Δ V 1 ( B x , B y , B z ) ( E 2 x , E 2 y , E 2 z ) = Δ U 2 ( T x , T y , T z ) + Δ V 2 ( B x , B y , B z ) (E_{1x},E_{1y},E_{1z})= \Delta U_1(T_x,T_y,T_z) + \Delta V_1(B_x,B_y, B_z) \\ \quad \\ (E_{2x},E_{2y},E_{2z})= \Delta U_2(T_x,T_y,T_z) + \Delta V_2(B_x,B_y, B_z) (E1x​,E1y​,E1z​)=ΔU1​(Tx​,Ty​,Tz​)+ΔV1​(Bx​,By​,Bz​)(E2x​,E2y​,E2z​)=ΔU2​(Tx​,Ty​,Tz​)+ΔV2​(Bx​,By​,Bz​)

再变换成矩阵乘法的形式:
[ E 1 x E 1 y E 1 z E 2 x E 2 y E 2 z ] = [ Δ U 1 Δ V 1 Δ U 2 Δ V 2 ] ⋅ [ T x T y T z B x B y B z ] \begin{bmatrix} E_{1x}&E_{1y}&E_{1z}\\E_{2x}&E_{2y}&E_{2z} \end{bmatrix} = \begin{bmatrix} \Delta U_1&\Delta V_1 \\ \Delta U_2&\Delta V_2 \end{bmatrix} · \begin{bmatrix} T_x&T_y&T_z \\ B_x&B_y&B_z \end{bmatrix} [E1x​E2x​​E1y​E2y​​E1z​E2z​​]=[ΔU1​ΔU2​​ΔV1​ΔV2​​]⋅[Tx​Bx​​Ty​By​​Tz​Bz​​]
再稍微做变换一下,左乘 [ Δ ] − 1 [\Delta]^{-1} [Δ]−1(稍微偷懒一下)得到:
[ Δ U 1 Δ V 1 Δ U 2 Δ V 2 ] − 1 ⋅ [ E 1 x E 1 y E 1 z E 2 x E 2 y E 2 z ] = [ T x T y T z B x B y B z ] \begin{bmatrix} \Delta U_1&\Delta V_1 \\ \Delta U_2&\Delta V_2 \end{bmatrix}^{-1} · \begin{bmatrix} E_{1x}&E_{1y}&E_{1z}\\E_{2x}&E_{2y}&E_{2z} \end{bmatrix} = \begin{bmatrix} T_x&T_y&T_z \\ B_x&B_y&B_z \end{bmatrix} [ΔU1​ΔU2​​ΔV1​ΔV2​​]−1⋅[E1x​E2x​​E1y​E2y​​E1z​E2z​​]=[Tx​Bx​​Ty​By​​Tz​Bz​​]
这样的形式,就很明显,左边全是已知量,右边是目标求解的矩阵,逆矩阵我们是极力避免的,因此逆矩阵可以通过公式替换掉 A − 1 = A ∗ ∣ A ∣ A^{-1}=\Large \frac{A^*}{|A|} A−1=∣A∣A∗​,还好是二阶,伴随矩阵是张口就来【主对调,副变号】所以得到:
[ T x T y T z B x B y B z ] = 1 Δ U 1 Δ V 2 − Δ U 2 Δ V 1 [ Δ V 2 − Δ V 1 − Δ U 2 Δ V 1 ] [ E 1 x E 1 y E 1 z E 2 x E 2 y E 2 z ] \begin{bmatrix} T_x&T_y&T_z \\ B_x&B_y&B_z \end{bmatrix}= \frac{1}{\Delta U_1\Delta V_2 - \Delta U_2\Delta V_1} \begin{bmatrix} \Delta V_2 & -\Delta V_1 \\ -\Delta U_2&\Delta V_1 \end{bmatrix} \begin{bmatrix} E_{1x}&E_{1y}&E_{1z}\\E_{2x}&E_{2y}&E_{2z} \end{bmatrix} [Tx​Bx​​Ty​By​​Tz​Bz​​]=ΔU1​ΔV2​−ΔU2​ΔV1​1​[ΔV2​−ΔU2​​−ΔV1​ΔV1​​][E1x​E2x​​E1y​E2y​​E1z​E2z​​]

  • 根据上面这个公式,通过面内一个三角形的 位置纹理坐标 属性 可以计算出切向量T和副切向量B
  • 因为TBN三个轴相互垂直,N已知,计算出TB中的任何一个,另一个都可以通过叉乘得到,可以少一半计算量

重要:计算出TBN三个轴后,这三个轴对于面内所有三角形的所有点是共用的,所以一个面只需要找一个三角形计算TBN即可


TBN矩阵的组装

  • 注意TBN矩阵是通过模型的顶点位置和纹理坐标算出来的,TBN三个轴是位于模型空间
  • 我们一般其实不需要自己手算TBN矩阵,三方模型读取的库已经提供了。顶点着色器拿到TBN后,通常需要先乘上一个Model矩阵中,因为我们是想通过TBN矩阵做世界空间与切线空间的相互转换注意
    • 是乘以model的 逆矩阵的转置矩阵(这是矢量变换矩阵,具体原因请自行搜索文章查阅啦,涉及标量旋转和矢量旋转的差异)
    void main()
    {//在顶点着色器中组装TBN矩阵mat3 vectorMatrix = transpose(inverse(mat3(model))); // 移除位移部分vec3 T = vectorMatrix * aTangent;vec3 B = vectorMatrix * aBitangent;vec3 N = vectorMatrix * aNormal;mat3 TBN = mat3(T, B, N);
    }
    

3 光照计算是在世界空间还是切线空间

3.1 在世界空间计算

  • 像素着色器中,使用TBN矩阵将采样拿到的法线从切线空间转换到世界空间,然后法线与光源、相机处于世界空间中,再进行光照计算。

        vec3 normal = vec3(texture(normalMap, TexCoords));normal = normal * 2.0 - 1.0;            // 采样的法线从3个分量从[0,1]映射回[-1,1]normal = normalize(TBN * normal);      //..光照计算..
    
  • 代价:逐像素做1次矩阵乘法

3.2 在切线空间中计算

错误方式:

  • 首先计算在顶点着色器中计算TBN的逆矩阵并传给像素着色器
    (因为要把光源方向和相机方向转换到切线空间,所以需要逆变换)
  • 在像素着色器中将光源方向视点方向从世界空间转换到切线空间,采样的法线也位于切线空间,再进行光照计算。按照这个逻辑,应该是下面这样的流程(伪代码)
    // 在顶点着色器中先对TBN求逆,因为正交矩阵 所以转置即可,之后直接传给像素着色器
    TBN = transpose(mat3(T, B, N));
    
    // 像素着色器中不对法线进行空间转换,而把对光入射方向以及视线方向转换到切线空间再进行光照计算
    vec3 normal = vec3(texture(normalMap, TexCoords));
    normal = normalize(normal * 2.0 - 1.0);   vec3 lightDir = TBN * normalize(lightPos - FragPos); // 世界空间光的入射方向转到切线空间
    vec3 viewDir  = TBN * normalize(viewPos - FragPos);
    [..光照计算..]
    
  • 代价:逐像素2次矩阵乘法

都说在切线空间计算光照更节省性能,相比世界空间光照计算的1次矩阵乘法,好像切线空间的效率更低啊?----上面这思路错了


正确的切线空间光照计算方式:

  • 顶点着色器 将所有相关变量(光源位置、相机位置、定点位置)转换到切线空间,再把这些属性传给像素着色器,每个片段插值出相应的属性即可

    // 记得保证TBN是相互垂直的三个向量
    mat3 TBN = transpose(mat3(T, B, N));vs_out.TangentLightPos = TBN * lightPos;
    vs_out.TangentViewPos = TBN * viewPos;
    vs_out.TangentFragPos = TBN * vs_out.FragPos;
    
  • 这种方式在片段着色器中实际上不需要对任何向量做矩阵乘法,而在世界空间中的光照计算则必须逐像素做一次,因为采样的法线向量是针对每个片段着色器运行的
  • 我们不需要将TBN矩阵的逆发送给片段着色器,而是在顶点着色器中将切线空间的光照位置、相机位置和顶点位置转换到切线空间后,发送给片段着色器。这使我们不必在片段着色器中进行矩阵乘法。这是一个很好的优化,因为顶点着色器比碎片着色器的运行频率要低得多。这也是为什么这种方法通常是首选方法的原因。

参考文章:Learn OpenGL:Normal Mapping

切线空间、法线贴图、TBN矩阵相关推荐

  1. Learn OpenGL 笔记6.5 Normal Mapping(法线贴图)

    我们通过在这些平面三角形上包裹 2D 纹理来增强真实感,隐藏多边形只是很小的平面三角形的事实. 从照明技术的角度来看,确定对象形状的唯一方法是通过其垂直法向量. 这种使用每片段法线与每表面法线相比的技 ...

  2. UE4 无需切线空间应用凹凸贴图

    Unreal Engine 4.9 照亮环境 凹凸贴图(Bump mapping) 最早由一名图形程序员发明(1978 James Blinn),它通过调整后的着色计算 来创建凹凸表面的假象,无需增加 ...

  3. opengl高级光照之法线贴图

    法线贴图 opengl官方文档 核心修改的就是片段着色器中的normal值 uniform sampler2D normalMap; void main() { // 从法线贴图范围[0,1]获取法线 ...

  4. GAMES101学习-法线贴图学习笔记

    参考:法线贴图_洛阳李四的博客 Bump.Normal和Displacement贴图的区别 闫神课上讲的关于法线贴图的定义和与位移贴图的区别已经很详细了,仅补充一些自己的理解和学习点. 法线贴图 No ...

  5. LearnOpenGL笔记——五、高级光照:“法线贴图”和”视差贴图“

    五.高级光照:"法线贴图"和"视差贴图" 5.4 法线贴图 以光照算法的视角考虑的话,只有一件事决定物体的形状,这就是垂直于它的法线向量 砖块表面只有一个法线向 ...

  6. shader graph_在Shader Graph中使用表面梯度框架进行法线贴图合成

    shader graph A recent Unity Labs paper introduces a new framework for blending normal maps that is e ...

  7. shader 获取法线_Unity Shader 入门到改行5——法线贴图

    the best of blur 1. 法线贴图理论 1.1 什么是法线贴图 一般的贴图中存储的是表面颜色值(RGBA),而法线贴图存放的则是法线信息(xyzw),假设某顶点处的 uv 坐标为 (u, ...

  8. OpenGL 法线贴图Normal Mapping

    OpenGL法线贴图Normal Mapping 法线贴图Normal Mapping简介 法线贴图 切线空间 手工计算切线和副切线 切线空间法线贴图 复杂物体 最后一件事 法线贴图Normal Ma ...

  9. 3D游戏建模入门初级教学:制作纹理逼真的法线贴图

    下图是一只恐龙的低模布线,细心的朋友估计会看到恐龙头部的布线密度是要远远高于身体和四肢的,这种布线的好处就是可以在你需要着重刻画的部位经过细分后生成的模型面数会远远高于那些次要部分,会使你的细节刻画更 ...

  10. 实时法线贴图dxt压缩算法

    JMP van Waveren  id Software,Inc.   NVIDIA公司IgnacioCastaño 2008年2月7日 ©2008,id Software,Inc. 抽象 原文下载地 ...

最新文章

  1. 怎么让上下两排对齐_为什么你家装饰画怎么挂都怪怪的?看完再装立马就能美翻了!...
  2. js中内置对象Math()常用方法笔记
  3. Java 8系列(一): 日期/时间- JSR310( Date and Time API)
  4. CentOS7.0 安装nginx-1.9.10
  5. ***脚本***普及
  6. C语言创建map,遍历map
  7. lintcode二叉树的锯齿形层次遍历 (双端队列)
  8. [LeetCode]LRU Cache有个问题,求大神解答【已解决】
  9. php完美导出word,PHP使用phpword生成word文档
  10. Unity3D学习笔记(二十五):文件操作
  11. Java之乘积最大子数组
  12. SysUtils.StrLCat
  13. 20150406--RBAC+添加字段栏目
  14. 从「集装箱」思考Docker风潮
  15. 【转载】JavaScript进阶问题列表
  16. 币圈投资,想要规避风险,这些提示还是要铭记在心
  17. win10无限蓝屏_快速解决Win10无限重启的方法
  18. C++题2:“五彩斑斓”
  19. 羽毛球拍15元,球3元,水2元。200元每种至少一个,有多少可能?
  20. 微信小程序-自定义导航组件

热门文章

  1. 哪些蓝牙耳机防水性强?防水蓝牙耳机推荐
  2. HTML5期末大作业:动漫人物介绍网站设计——柯南(5页) HTML+CSS+JavaScript 学生DW网页设计作业成品 学生动漫网页设计模板下载 海贼王大学生HTML网页制作作品 简单漫画网
  3. 智能优化算法——python手动实现交叉进化算法
  4. lumia手机邮件hotmail服务器设置,采用Windows10Mobile系统的Lumia手机用户手册-Microsoft.PDF...
  5. 爬取一个月内指定微博用户博文数据
  6. 深入java--与MySQL连接时的时间类问题以及Calendar的用法
  7. java中的进度条的显示
  8. CPA广告 CPS广告 CPC广告 CPM广告分别什么意思?
  9. 前端实现ctrl+F搜索效果
  10. java xsd校验,java中使用xsd验证xml | 学步园