OpenGL基础46:切线空间

 

到这里,关于OpenGL基础的了解要接近尾声了,上一个节点是《OpenGL基础25:多光源》。在此章之后,学习openGL的各种教程的同时,可以转战想要了解的渲染引擎,也可以去github上看看项目,这些都是不错的选择

后面的内容重点为最简单常用的光照效果优化与图像处理

再次声明:直至这一章,主要还是参考于 https://learnopengl.com/,当然这不仅是翻译,也结合了不少其它的文献和教学视频,和原文略有不同,很多地方去掉了繁杂或不重要的描述,加入了自己的看法,整体更加整洁易懂

  • 如有问题/错误,求反馈

一、什么是切线空间(Tangent Space)

在《OpenGL基础11:空间》中提到了观察空间、裁剪空间、世界空间等。切线空间和它们一样,都属于坐标空间

上面就是一个切线空间的例子,对于切线空间:

  • N:该顶点本身的法线方向,z轴
  • T:该顶点的一条切线,但由于切线数量有无数条,其一般由模型给定,对应着UV图中的U,也就是使用和纹理坐标方向相同的那条
  • B:由前两者叉乘得到,对应着UV图中的V

UV图:用于告知计算机,如何用2维的贴图包住3维的物体,本质上UV图提供了一种模型表面与纹理图像之间的连接关系,也就是确定纹理图像上的每一个像素应该放置在模型表面的哪一个顶点上,如果没有UV图,多边形网格将不能被渲染出纹理,其中U和V分别指的是纹理空间的水平轴和垂直轴

 

二、为什么需要切线空间

在此之前,先需要大致了解一下法线贴图(Normal Mapping):为了得到正确的光照,需要知道物体每个顶点的法向量,但为了保证效率,一般物体的顶点不会太多,就像一块砖块,它的表面往往凹凸不平,但事实上它可能单纯的只是一个立方体,每一面给上了一个贴图。这样如果还想要体现出物体“凹凸不平”的效果,就需要用到法线贴图或者高度贴图,也就是对于纹理的每一个像素,都指定一个特定的法向量!

很巧,法向量是个3维向量,而颜色正好也是一个3维向量,所以直接将向量信息存储成颜色信息没有任何的压力

可是这样就出现了另一个问题:就像下图是一个黑色小包模型的一部分,中间有两个拉链拉头,这两个子模型是完全一样的,唯一的区别就是位置不同从而光照的效果不同。那么很明显,为了节省空间和性能,用的也会是同样的贴图,但是!他们的法向量却不一样,也就是说这两个完全相同的物体并不能使用同一张法线贴图(法线贴图可以说只是个数据存储媒介,和颜色没有关系)

确实可以为这些相同的子模型准备不同的法线贴图,就像一个六个面相同的立方体,为它专门准备6张法线贴图,但是有可能这样的子模型特别多,并且方向都不相同,这个时候还准备不同的法线贴图就比较尴尬了

上面的问题归根结底就是物体的朝向问题,我们只要找到这样的一个空间:无论当前是什么朝向,所有“纹理像素点”的法向量一定都是不变的,并且可以通过空间变换就可以得到世界坐标下正确的法向量,就可以解决问题了。所有相同的面,都可以赋予同样的法线贴图

这个空间就是切线空间

 

三、切线空间的计算

从最简单的开始算:一个4个顶点100%平坦的平面,怎么求出它的切线空间呢?

T (Tangent) B (Bitangent) N (Normal) 向量一个一个来:

N 就不说了,它就是法向量,然后就是 T 切线向量:

肯定的,T、B、N都是单位长度,这样的话根据右图可以得到:

\begin{array}{l} E_{1}=\Delta U_{1} T+\Delta V_{1} B \\ E_{2}=\Delta U_{2} T+\Delta V_{2} B \end{array}

只要理解了这个公式后面就都好办了,首先可以看出,这是求的顶点 P_2 的切线空间,而 P_2, P_1, P_3 是当前 P_2 所在的一个三角形片元,其次,E_1 和 E_2 其实已经当前点的其中两条切线了,只不过为了统一切线,切线的方向必须是纹理UV的方向,正好点 P_1 和 P_3 的纹理坐标映射在主副切线方向上的向量之和正是 E_1 和 E_2,也就是说:E_2 是一个三角形的边,这个三角形的另外两条边是 \Delta U_2 和 \Delta V_2,它们与切线向量 T 和副切线向量 B 方向相同

不过由于最后向量需要归一化(只需要知道它的方向),因此在计算的时候 EUV 可以是增量长度

既然 EUV 都是已知的,那么就可以求出 T 和 B 了:

上面的公式可以写成:

\begin{array}{l} \left(E_{1 x}, E_{1 y}, E_{1 z}\right)=\Delta U_{1}\left(T_{x}, T_{y}, T_{z}\right)+\Delta V_{1}\left(B_{x}, B_{y}, B_{z}\right) \\ \left(E_{2 x}, E_{2 y}, E_{2 z}\right)=\Delta U_{2}\left(T_{x}, T_{y}, T_{z}\right)+\Delta V_{2}\left(B_{x}, B_{y}, B_{z}\right) \end{array}

转成矩阵就是:

\left[\begin{array}{ccc} E_{1 x} & E_{1 y} & E_{1 z} \\ E_{2 x} & E_{2 y} & E_{2 z} \end{array}\right]=\left[\begin{array}{cc} \Delta U_{1} & \Delta V_{1} \\ \Delta U_{2} & \Delta V_{2} \end{array}\right]\left[\begin{array}{ccc} T_{x} & T_{y} & T_{z} \\ B_{x} & B_{y} & B_{z} \end{array}\right]

两边都乘以 \Delta U 和 \Delta V 的逆矩阵:

\left[\begin{array}{cc} \Delta U_{1} & \Delta V_{1} \\ \Delta U_{2} & \Delta V_{2} \end{array}\right]^{-1}\left[\begin{array}{ccc} E_{1 x} & E_{1 y} & E_{1 z} \\ E_{2 x} & E_{2 y} & E_{2 z} \end{array}\right]=\left[\begin{array}{ccc} T_{x} & T_{y} & T_{z} \\ B_{x} & B_{y} & B_{z} \end{array}\right]

\left[\begin{array}{ccc} T_{x} & T_{y} & T_{z} \\ B_{x} & B_{y} & B_{z} \end{array}\right]=\frac{1}{\Delta U_{1} \Delta V_{2}-\Delta U_{2} \Delta V_{1}}\left[\begin{array}{cc} \Delta V_{2} & -\Delta V_{1} \\ -\Delta U_{2} & \Delta U_{1} \end{array}\right]\left[\begin{array}{ccc} E_{1 x} & E_{1 y} & E_{1 z} \\ E_{2 x} & E_{2 y} & E_{2 z} \end{array}\right]

好了搞定,把未知数成功扔到了左边

转成代码就是(代码中的数据输入是最简单情况:一个平面正方形,4个顶点分别是:(-1, -1, 0)、(-1, 1, 0)、(1, -1, 0)和(1, 1, 0),对应的纹理顶点为(0, 0)、(0, 1)、(1, 0)和(1, 1)):

void GetTangent()
{
    glm::vec3 pos1(-1.0, 1.0, 0.0);
    glm::vec3 pos2(-1.0, -1.0, 0.0);
    glm::vec3 pos3(1.0, -1.0, 0.0);
    glm::vec3 pos4(1.0, 1.0, 0.0);
    glm::vec2 uv1(0.0, 1.0);
    glm::vec2 uv2(0.0, 0.0);
    glm::vec2 uv3(1.0, 0.0);
    glm::vec2 uv4(1.0, 1.0);
    glm::vec3 normal(0.0, 0.0, 1.0);

    glm::vec3 tangent1, bitangent1;     //第一个三角形(1,2,3)的顶点切线空间
    glm::vec3 tangent2, bitangent2;     //第二个三角形(1,3,4)的顶点切线空间

    glm::vec3 edge1 = pos2 - pos1;
    glm::vec3 edge2 = pos3 - pos1;
    glm::vec2 deltaUV1 = uv2 - uv1;
    glm::vec2 deltaUV2 = uv3 - uv1;
    GLfloat f = 1.0f / (deltaUV1.x * deltaUV2.y - deltaUV2.x * deltaUV1.y);
    tangent1.x = f * (deltaUV2.y * edge1.x - deltaUV1.y * edge2.x);
    tangent1.y = f * (deltaUV2.y * edge1.y - deltaUV1.y * edge2.y);
    tangent1.z = f * (deltaUV2.y * edge1.z - deltaUV1.y * edge2.z);
    tangent1 = glm::normalize(tangent1);
    bitangent1.x = f * (-deltaUV2.x * edge1.x + deltaUV1.x * edge2.x);
    bitangent1.y = f * (-deltaUV2.x * edge1.y + deltaUV1.x * edge2.y);
    bitangent1.z = f * (-deltaUV2.x * edge1.z + deltaUV1.x * edge2.z);
    bitangent1 = glm::normalize(bitangent1);

    edge1 = pos3 - pos1;
    edge2 = pos4 - pos1;
    deltaUV1 = uv3 - uv1;
    deltaUV2 = uv4 - uv1;
    f = 1.0f / (deltaUV1.x * deltaUV2.y - deltaUV2.x * deltaUV1.y);
    tangent2.x = f * (deltaUV2.y * edge1.x - deltaUV1.y * edge2.x);
    tangent2.y = f * (deltaUV2.y * edge1.y - deltaUV1.y * edge2.y);
    tangent2.z = f * (deltaUV2.y * edge1.z - deltaUV1.y * edge2.z);
    tangent2 = glm::normalize(tangent2);
    bitangent2.x = f * (-deltaUV2.x * edge1.x + deltaUV1.x * edge2.x);
    bitangent2.y = f * (-deltaUV2.x * edge1.y + deltaUV1.x * edge2.y);
    bitangent2.z = f * (-deltaUV2.x * edge1.z + deltaUV1.x * edge2.z);
    bitangent2 = glm::normalize(bitangent2);

    GLfloat quadVertices[] =
    {
        //位置                  //法向量          //纹理坐标    //切线                              //副切线
        pos1.x, pos1.y, pos1.z, nm.x, nm.y, nm.z, uv1.x, uv1.y, tangent1.x, tangent1.y, tangent1.z, bitangent1.x, bitangent1.y, bitangent1.z,
        pos2.x, pos2.y, pos2.z, nm.x, nm.y, nm.z, uv2.x, uv2.y, tangent1.x, tangent1.y, tangent1.z, bitangent1.x, bitangent1.y, bitangent1.z,
        pos3.x, pos3.y, pos3.z, nm.x, nm.y, nm.z, uv3.x, uv3.y, tangent1.x, tangent1.y, tangent1.z, bitangent1.x, bitangent1.y, bitangent1.z,
        pos1.x, pos1.y, pos1.z, nm.x, nm.y, nm.z, uv1.x, uv1.y, tangent2.x, tangent2.y, tangent2.z, bitangent2.x, bitangent2.y, bitangent2.z,
        pos3.x, pos3.y, pos3.z, nm.x, nm.y, nm.z, uv3.x, uv3.y, tangent2.x, tangent2.y, tangent2.z, bitangent2.x, bitangent2.y, bitangent2.z,
        pos4.x, pos4.y, pos4.z, nm.x, nm.y, nm.z, uv4.x, uv4.y, tangent2.x, tangent2.y, tangent2.z, bitangent2.x, bitangent2.y, bitangent2.z
    };
    //……
}

 

四、TBN与空间变换

其实关于切线空间的了解到这里就结束了,下面可以当作扩展记录一下:

仔细看上面转换过程中的一个式子:

\left[\begin{array}{ccc} E_{1 x} & E_{1 y} & E_{1 z} \\ E_{2 x} & E_{2 y} & E_{2 z} \end{array}\right]=\left[\begin{array}{cc} \Delta U_{1} & \Delta V_{1} \\ \Delta U_{2} & \Delta V_{2} \end{array}\right]\left[\begin{array}{ccc} T_{x} & T_{y} & T_{z} \\ B_{x} & B_{y} & B_{z} \end{array}\right]

换一种表示方法:

\left[\begin{array}{cc} E_{1 x} & E_{2 x} \\ E_{1 y} & E_{2 y} \\ E_{1 z} & E_{2 z} \end{array}\right] =\left[\begin{array}{cc} T_{x} & B_{x} \\ T_{y} & B_{y} \\ T_{z} & B_{z}\end{array}\right]\left[\begin{array}{cc} \Delta U_{1} & \Delta U_{2} \\ \Delta V_{1} & \Delta V_{2} \end{array}\right]

或许就可以更容易发现, T 和 B 其实正是一组基向量,抛开法向量那一维,T 和 B 正好可以将二维uv空间中的向量和点,转到三维世界空间的某个平面上,而这个平面正好是切线平面

也就是说,如果T和B是已知的、切线空间是已知的,那么我们很容易就可以将二维纹理坐标映射到三维空间顶点坐标,而上面计算的所以意义,就在于我们知道二维空间纹理坐标,也知道三维空间顶点坐标,要反过来去找这个空间

如果理解了这个,就好办了,不用纠结于坐标和法线,它的本质就是如此

 

  • 5
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值