转:http://windsmoon.com/2017/11/28/%E5%88%87%E7%BA%BF%E7%A9%BA%E9%97%B4-Tangent-Space-%E7%9A%84%E8%AE%A1%E7%AE%97%E4%B8%8E%E5%BA%94%E7%94%A8/?utm_medium=social&utm_source=ZHShareTargetIDMore

概要

本篇文章主要讲解计算机图形学中切线空间是如何计算的,且会以法线贴图的例子来验证切线空间是否计算正确,以及展现切线空间的用途.

本文需要读者掌握一定的 3D 坐标空间变换和简单光照相关的知识,以及法线贴图的基本知识(但切线空间不仅仅只用于法线贴图)。

认识切线空间

什么是切线空间

切线空间 (Tangent Space) 与 世界空间 (World Space) 和 观察空间 (View Space) 一样,都是一个坐标空间,它是由顶点所构成的平面的 UV 坐标轴以及表面的法线所构成,一般用 T (Tangent), B (Bitangent), N (Normal) 三个字母表示,即切线,副切线,法线, TT 对应 UV 中的 UU, BB 对应 UV 中的 VV,下图是切线空间的示意图:

这里可能会有一个疑问,就是为什么 TT 对应 UV 中的 UU, BB 对应 UV 中的 VV 。理论上,只要 TT 和 BB 垂直且都位于三角形的平面内,就可以达到使用切线空间的目的,因为这样我们总可以把所有需要的数据变换到同一个坐标空间下,但由于我们知道 UV 坐标的值,所以用 UV 坐标来对应 TT 和 BB 计算出数据了。

为什么要有切线空间

要理解为什么要有切线空间,可以从法线贴图入手。众所周知,绝大部分的法线贴图,颜色都是偏蓝色的,这是因为法线贴图中存储的法线向量大部分都是朝向或者接近 z 轴的,即 (0,0,1)(0,0,1),换算到 RGB 中,就是偏向蓝色,即 (0.5,0,5,1)(0.5,0,5,1) (后面的 Shader 中有算法),这种贴图就是切线空间 (Tangent Space)下的贴图。这显然存在一个问题,想象一个位于世界坐标原点且没有进行任何变换的立方体,表面法线方向就有 6 个,因为有 6 个不同朝向的面(确切的说,可能是 12 个面,因为一个矩形一般由两个三角形组成),而且每个面完全相同,所以这时候我应该只需要一个面的法线贴图就可以了。但其实这时再用这种偏蓝色的法线贴图就不行了,因为立方体的上表面在世界空间的法线方向为 (0,1,0)(0,1,0),而在法线贴图中采样出来的法线基本都是接近于 (0,0,1)(0,0,1) 的,使用错误的法线会得到错误的光照结果。所以这时候需要做一张包含立方体所有面的法线信息的法线贴图,也就是模型空间 (Object Space)下的法线贴图,而这种贴图看起来就不单单是偏蓝色了,而是包含了多种颜色。

这样看起来好像也没什么问题,但其实用切线空间下的法线贴图要比用模型空间下的法线贴图要有一些优势:

  • 可以复用:比如上文提到的立方体,如果每个面都完全相同,则可以只制作一个面的法线贴图,然后就可以复用到所有面上,类似的复用需求还有很多,这可以减小内存占用和包体大小。
  • 纹理可以压缩:因为切线空间下,一般来说法线方向不会是朝向表面内部,即法线贴图中的 z 值不会是负数,而我们使用的法线又是归一化的,所以完全可以根据 x 和 y 的值来推导出 z 的值,所以贴图中只需要存储 x 和 y 的值即可,可进行纹理压缩。
  • 待补充

综上所述,一般的法线贴图都是使用切线空间的,而直接使用切线空间下的法线贴图又会出现之前提到的立方体的那个问题,所以我们在使用前需要先进行切线空间相关的变换,把所需要的数据变换到同一个坐标空间下再进行计算(可以全部变换到世界空间也可以全部变换到切线空间)。

切线空间的计算

求切线和副切线

要进行切线空间相关的计算,需要先求出构成切线空间三个轴的单位基向量,然后就可以构造出从切线空间变换到世界空间的矩阵,从而进行之后的计算。

切线空间的计算可以通过前面的示意图来理解,这里为了方便,再放一次:

设:

则由图和共面向量基本定理可知:

观察这两个等式,我们发现这其实可以写成矩阵乘法的形式,如下所示:

如果你求解一下等号右边的矩阵乘法,你就会发现,他就是我们在上面得到的等式。根据这个矩阵形式的等式,我们不难求解 TB 矩阵,只需要两边同时左乘 ΔUΔV 的逆矩阵,再进行计算即可,步骤如下:

逆矩阵的计算公式为 矩阵的行列式的值的倒数再乘以它的伴随矩阵 (Adjugate Matrix, 如果对这些概念不熟悉需要读者自行查阅),其实伴随矩阵的求解并不容易,不过 二阶矩阵的伴随矩阵 有一个简单的公式,即 主对角线的元素互换,副对角线的元素乘以 −1 ,所以最终结果如下所示:

似乎我们还缺少 E1和 E2 的信息,但其实这个信息是已知的,因为他们就是三角形的两个边,而三角形的顶点坐标是我们知道的,所以求出 T 和 B 所需的数据我们都已经有了,只需要代入公式就可以了。

设:

则:

B 也可以如此求解,但其实只需要用 T 和 法线向量 叉乘 即可。

归一化

因为 E1 和 E2是用顶点坐标表示的,而 U 和 V 是纹理坐标,他们的坐标单位是不同的,所以我们求出的结果自然不太可能是已经归一化了的,而我们使用坐标空间转换矩阵的时候需要的是归一化的坐标,所以我们需要进行归一化。

法线贴图的例子

本节将以一个法线贴图的例子,来展示切线空间是如何工作的,在这个例子中,我只计算了漫反射等颜色(因为除了法线贴图外我只找到一张漫反射的贴图,但足够演示用了,不过光照效果看起来未必会很好),下面两张图是我使用的漫反射贴图和法线贴图:

计算顶点数据

为了方便展示,我准备了一个立方体的顶点数据,一共有36个顶点(6个面,每个面2个三角形),为了这篇文章的编写方便,我采用直接绘制顶点而非索引的方式,并且之后的一些计算会有些暴力。

36个顶点数据如下所示,每一行分别为顶点坐标(3个),法线向量(3个),以及纹理坐标(2个),每6行为一个面。

float vertices[] = {-0.5f, -0.5f, -0.5f, 0.0f, 0.0f, -1.0f, 0.0f, 0.0f,0.5f, -0.5f, -0.5f, 0.0f, 0.0f, -1.0f, 1.0f, 0.0f,0.5f, 0.5f, -0.5f, 0.0f, 0.0f, -1.0f, 1.0f, 1.0f,0.5f, 0.5f, -0.5f, 0.0f, 0.0f, -1.0f, 1.0f, 1.0f,-0.5f, 0.5f, -0.5f, 0.0f, 0.0f, -1.0f, 0.0f, 1.0f,-0.5f, -0.5f, -0.5f, 0.0f, 0.0f, -1.0f, 0.0f, 0.0f,-0.5f, -0.5f, 0.5f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f,0.5f, -0.5f, 0.5f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f,0.5f, 0.5f, 0.5f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f,0.5f, 0.5f, 0.5f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f,-0.5f, 0.5f, 0.5f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f,-0.5f, -0.5f, 0.5f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f,-0.5f, 0.5f, 0.5f, -1.0f, 0.0f, 0.0f, 1.0f, 0.0f,-0.5f, 0.5f, -0.5f, -1.0f, 0.0f, 0.0f, 1.0f, 1.0f,-0.5f, -0.5f, -0.5f, -1.0f, 0.0f, 0.0f, 0.0f, 1.0f,-0.5f, -0.5f, -0.5f, -1.0f, 0.0f, 0.0f, 0.0f, 1.0f,-0.5f, -0.5f, 0.5f, -1.0f, 0.0f, 0.0f, 0.0f, 0.0f,-0.5f, 0.5f, 0.5f, -1.0f, 0.0f, 0.0f, 1.0f, 0.0f,0.5f, 0.5f, 0.5f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f,0.5f, 0.5f, -0.5f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f,0.5f, -0.5f, -0.5f, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f,0.5f, -0.5f, -0.5f, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f,0.5f, -0.5f, 0.5f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f,0.5f, 0.5f, 0.5f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f,-0.5f, -0.5f, -0.5f, 0.0f, -1.0f, 0.0f, 0.0f, 1.0f,0.5f, -0.5f, -0.5f, 0.0f, -1.0f, 0.0f, 1.0f, 1.0f,0.5f, -0.5f, 0.5f, 0.0f, -1.0f, 0.0f, 1.0f, 0.0f,0.5f, -0.5f, 0.5f, 0.0f, -1.0f, 0.0f, 1.0f, 0.0f,-0.5f, -0.5f, 0.5f, 0.0f, -1.0f, 0.0f, 0.0f, 0.0f,-0.5f, -0.5f, -0.5f, 0.0f, -1.0f, 0.0f, 0.0f, 1.0f,-0.5f, 0.5f, -0.5f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f,0.5f, 0.5f, -0.5f, 0.0f, 1.0f, 0.0f, 1.0f, 1.0f,0.5f, 0.5f, 0.5f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f,0.5f, 0.5f, 0.5f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f,-0.5f, 0.5f, 0.5f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f,-0.5f, 0.5f, -0.5f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f};
 
 

我们还要给每一行再增加6个数据,即 TT 和 BB 各 3 个坐标。上面一共有 288 个浮点数,让我直接再写 216 个会累死我的,所以切线空间的数据我直接用代码算出来了,实际使用过程中也许在导入模型的时候就可以直接导入切线空间了,当然没有也没关系,因为我已经讲了如何计算切线空间,并且这里也给出了一个例子。下面的代码就是计算切线空间的代码,就如之前说的,很暴力,我并没有多写几个循环来减少几行代码,何必呢。

float tbnFloats[216]; // 36 个顶点的切线和副切线向量一共有 216 个浮点数// 一个立方体一共有 12 个三角形面(每 2 个构成一个立方体面)for (int i = 0; i < 12; ++i){Vector3 tbn;int firstIndex = i * 24; // 三角形第 1 个顶点坐标起始索引int secondIndex = firstIndex + 8; // 三角形第 2 个顶点坐标起始索引int thirdIndex = secondIndex + 8; // 三角形第 3 个顶点坐标起始索引// 求得一个三角形的三个顶点坐标Vector3 pos1(vertices[firstIndex], vertices[firstIndex + 1], vertices[firstIndex + 2]);Vector3 pos2(vertices[secondIndex], vertices[secondIndex + 1], vertices[secondIndex + 2]);Vector3 pos3(vertices[thirdIndex], vertices[thirdIndex + 1], vertices[thirdIndex + 2]);// 求得一个三角形的三个顶点对应的 UV 坐标Vector2 uv1(vertices[firstIndex + 6], vertices[firstIndex + 7]);Vector2 uv2(vertices[secondIndex + 6], vertices[secondIndex + 7]);Vector2 uv3(vertices[thirdIndex + 6], vertices[thirdIndex + 7]);// 求出三角形的两条边的向量以及 UV 坐标之间的差向量,用于代入公式// 需要注意的是,当表示 UV 坐标时,x 对应 U,y 对应 VVector3 edge1 = pos2 - pos1;Vector3 edge2 = pos3 - pos1;Vector2 deltaUV1 = uv2 - uv1;Vector2 deltaUV2 = uv3 - uv1;// 计算切线和副切线向量// 其实这里就是套用上面求出来的公式Vector3 tangent;Vector3 bitTangent;GLfloat f = 1.0f / (deltaUV1.x * deltaUV2.y - deltaUV2.x * deltaUV1.y);tangent.x = f * (deltaUV2.y * edge1.x - deltaUV1.y * edge2.x);tangent.y = f * (deltaUV2.y * edge1.y - deltaUV1.y * edge2.y);tangent.z = f * (deltaUV2.y * edge1.z - deltaUV1.y * edge2.z);tangent = glm::normalize(tangent);bitTangent.x = f * (-deltaUV2.x * edge1.x + deltaUV1.x * edge2.x);bitTangent.y = f * (-deltaUV2.x * edge1.y + deltaUV1.x * edge2.y);bitTangent.z = f * (-deltaUV2.x * edge1.z + deltaUV1.x * edge2.z);bitTangent = glm::normalize(bitTangent);// 将每个三角形顶点的切线和副切线数据放到数组里int startTBNIndex = i * 18;tbnFloats[startTBNIndex + 0] = tangent.x;tbnFloats[startTBNIndex + 1] = tangent.y;tbnFloats[startTBNIndex + 2] = tangent.z;tbnFloats[startTBNIndex + 3] = bitTangent.x;tbnFloats[startTBNIndex + 4] = bitTangent.y;tbnFloats[startTBNIndex + 5] = bitTangent.z;tbnFloats[startTBNIndex + 6] = tangent.x;tbnFloats[startTBNIndex + 7] = tangent.y;tbnFloats[startTBNIndex + 8] = tangent.z;tbnFloats[startTBNIndex + 9] = bitTangent.x;tbnFloats[startTBNIndex + 10] = bitTangent.y;tbnFloats[startTBNIndex + 11] = bitTangent.z;tbnFloats[startTBNIndex + 12] = tangent.x;tbnFloats[startTBNIndex + 13] = tangent.y;tbnFloats[startTBNIndex + 14] = tangent.z;tbnFloats[startTBNIndex + 15] = bitTangent.x;tbnFloats[startTBNIndex + 16] = bitTangent.y;tbnFloats[startTBNIndex + 17] = bitTangent.z;}
 
 

接着我们将两个数组合并成一个新的顶点数组:

float finishVertices[504];// 一共 36 个顶点,按照特定顺序合并即可for (int i = 0; i < 36; ++i){int finishStartIndex = i * 14;int verticesStartIndex = i * 8;int tbnStartIndex = i * 6;finishVertices[finishStartIndex + 0] = vertices[verticesStartIndex + 0];finishVertices[finishStartIndex + 1] = vertices[verticesStartIndex + 1];finishVertices[finishStartIndex + 2] = vertices[verticesStartIndex + 2];finishVertices[finishStartIndex + 3] = vertices[verticesStartIndex + 3];finishVertices[finishStartIndex + 4] = vertices[verticesStartIndex + 4];finishVertices[finishStartIndex + 5] = vertices[verticesStartIndex + 5];finishVertices[finishStartIndex + 6] = vertices[verticesStartIndex + 6];finishVertices[finishStartIndex + 7] = vertices[verticesStartIndex + 7];finishVertices[finishStartIndex + 8] = tbnFloats[tbnStartIndex + 0];finishVertices[finishStartIndex + 9] = tbnFloats[tbnStartIndex + 1];finishVertices[finishStartIndex + 10] = tbnFloats[tbnStartIndex + 2];finishVertices[finishStartIndex + 11] = tbnFloats[tbnStartIndex + 3];finishVertices[finishStartIndex + 12] = tbnFloats[tbnStartIndex + 4];finishVertices[finishStartIndex + 13] = tbnFloats[tbnStartIndex + 5];}
 
 

这样我们就有一个新的包含 504 个浮点数的数组了,把数组抽象成行列的形式,每个顶点一行,每一行从左到右的形式是这样的:顶点坐标(3个),法线向量(3个),以及纹理坐标(2个),切线向量(3个),副切线向量(3个)。

但其实我这里多了一步,就是当我们求出切线后,只需要让其和三角形表面法线 叉乘 即可,因为他们都是互相垂直的,不过我这里没这么写。

不使用切线空间

这个例子使用 OpenGL 编写,我没有全部给出代码,比如如何将这些数据传给 Shader 等,这些对于本篇文章并不重要,也不是本篇文章所要讲的,我在这里会直接给出相关的 Shader 片段,我觉得这就足够了。例子里只有一个立方体,一个平行光,且平行光垂直于立方体朝向世界坐标 Z 轴的一面,而法线贴图采用切线空间下的法线贴图,也就是看起来偏蓝色的法线贴图,这意味着大部分法线值都是偏向正 Z 轴的。

首先我们不使用法线贴图,只使用顶点数组里的顶点法线,来观察一下它的样子,如下图所示:

然后我们加入法线贴图,但不使用切线空间,直接从法线贴图中采样法线向量,再来看下它的样子,如下图所示:

通关观察可以发现,上面两张图中前者是相对正常的,因为整个世界里只有一个垂直于亮面的平行光,所以只能看到一个面有颜色,其它面都是黑色。而后者中,除了垂直于平行光的面,其余面也是有颜色的,这显然是不对的,因为按照物理法则,其余几个面不应该被任何光照到(我也没有添加环境光),所以应该是黑色的。之所以有这样错误的效果,是因为这个立方体六个面都用的相同的漫反射贴图和法线贴图,每一个面不管朝向哪里,采样出来的都是偏向正 Z 轴的值,所以 Shader 代码自然会认为这个面中大部分片段就是面向正 Z 轴的,而我们的平行光正好是照着负 Z 轴,所以这时每个面看起来都有了颜色,这也是我在前面提到的法线贴图的一个问题。

另外,如果你仔细发现,你会看到后者大面积对着屏幕的那一面要比前者大面积对着屏幕的那一面要稍微更有立体感,因为后者我使用了法线贴图,这是法线贴图最基本的作用。但这里的确不明显,因为我为了方便演示,并没有花时间调整出好的光照效果,毕竟这篇文章不是演示法线贴图的,而是用另一个方式去验证切线空间是否计算正确。

使用切线空间

为了解决前面的问题,我们需要使用切线空间。切线空间有两种方式可以得到正确的光照结果:

  • 将数据变换到 世界空间 来计算
  • 将数据变换到 切线空间 来计算

很多人喜欢在世界空间中计算,因为将所有数据转换到世界空间再进行计算,是非常直观的,对于我们在讨论的问题也是如此。但这里我们使用第二种方式来计算,原因是它更高效。

如果我们使用第一种方式,我们需要将每个从法线贴图中采样出来的法线变换到世界空间,这一步是在 片段着色器 中完成的,因为必须知道每个片段对应的的法线值,而不能简单的在顶点着色器中采样出来然后再插值到片段着色器中。如果我们使用第二种方式,我们会在 顶点着色器 中把所需要的数据,在这个例子中有平行光方向向量,顶点坐标,观察坐标(因为这个例子只有一个漫反射贴图,所以其实这个数据并没什么卵用)变换到切线空间,然后在片段着色器中只需要采样出法线向量,不需要再进行其他转换就可以直接进行计算了。而一般来说片段着色器执行的次数远大于顶点着色器执行的次数,所以第二种方式一般来说更高效。

当然这里你可能有一个疑问,我们将一些数据从世界空间转换到切线空间,会涉及到矩阵的求逆,这一步是开销比较大的。理论上说,是的,但实际上,我们利用一个性质,即 正交矩阵的逆矩阵等于它的转置矩阵 就可以做到高效求逆矩阵,你在后面会看到。

顶点 Shader

首先我们将顶点数组传入顶点着色器,然后构造 TBN 矩阵 来把一些数据变换到切线空间,最后再传入到片段着色器里。我先列出顶点着色器中所需要的数据(除传入的顶点数据外,其余数据都是在世界空间下)

#version 330 corelayout (location = 0) in vec3 vertexPosition; // 顶点坐标layout (location = 1) in vec3 vertexNormal; // 顶点法线layout (location = 2) in vec2 textureCoordinate; // 顶点纹理采样坐标layout (location = 3) in vec3 tangent; // 顶点切线layout (location = 4) in vec3 bitTangent; // 顶点副切线// 这是 OpenGL 中的 uniform 缓存,就是把一次渲染中不变的通用数据从外部代码传给 Shaderlayout (std140) uniform CameraInfo{vec3 viewPosition; // 摄像机位置(观察位置)};// 平行光的数据struct DirectionalLight{vec3 direction; // 方向vec3 diffuseColor; // 漫反射颜色};uniform mat4 mvpMatrix;uniform mat4 modelMatrix;uniform DirectionalLight directionalLight;
 
 

然后我们还需要定义输出给片段着色器的数据:

out V_OUT{vec2 textureCoordinate; // 纹理坐标vec3 vertexPosition; // 切线空间顶点坐标vec3 normal; // 发现向量vec3 viewPosition; // 切线空间观察坐标vec3 directionalLightDirection; // 切线空间平行光方向} v_out;
 
 

这些数据定义好后,我们就可以着手编写转换各个数据到切线空间的代码了:

void main(){// 计算顶点的世界坐标vec4 vertexPositionVector = vec4(vertexPosition, 1.f);gl_Position = mvpMatrix * vertexPositionVector;// 计算法线矩阵(这个矩阵可以使法线的坐标空间变换更精确,详细信息可以查阅【法线矩阵】 或 【Normal Transform】)mat3 normalMatrix = transpose(inverse(mat3(modelMatrix)));// 求 TBN 矩阵,三个向量均变换到世界空间vec3 T = normalize(normalMatrix * tangent);vec3 B = normalize(normalMatrix * bitTangent);vec3 N = normalize(normalMatrix * vertexNormal);// 求 TBN 矩阵的逆矩阵,因为 TBN 矩阵由三个互相垂直的单位向量组成,所以它是一个正交矩阵// 正如前面所说,正交矩阵的逆矩阵等于它的转置,所以无需真的求逆矩阵// 详情可查阅 【正交矩阵】 或 【Orthogonal Matrix】mat3 inverseTBN = transpose(mat3(T, B, N));// 将一些数据从世界空间变换到切线空间(并非所有数据都需要变换),然后传给片段着色器v_out.directionalLightDirection = inverseTBN * directionalLight.direction;v_out.vertexPosition = inverseTBN * vec3(gl_Position);v_out.viewPosition = inverseTBN * viewPosition;v_out.textureCoordinate = textureCoordinate;v_out.normal = N;}
 
 

写到这里我发现,我本来想只放出 Shader 片段的,但最后还是把整个顶点着色器的代码都写上了。我在里面添加了详细的注释,应该不会有什么很困惑的地方。

片段 Shader

由于我们将数据都变换到了切线空间下,那么片段着色器在计算的时候就方便多了,因为它们都在同一个空间下了。同样我们先定义所需要的数据:

#version 330 coreout vec4 f_color; // 输出的颜色// 这个跟顶点着色器中的 out 一致in V_OUT{vec2 textureCoordinate;vec3 vertexPosition;vec3 normal;vec3 viewPosition;vec3 directionalLightDirection;} v_out;struct Material{sampler2D diffuseTexture; // 漫反射贴图sampler2D normalTexture; // 法线贴图};// 跟顶点着色器中的一致struct DirectionalLight{vec3 direction;vec3 diffuseColor;};uniform Material material; // 材质uniform DirectionalLight directionalLight; // 平行光信息
 
 

最后计算最终的颜色:

vec3 viewDirection; // 观察方向vec3 CaculateDiractionalLightColor(){// 从法线贴图中采样出数据,并转换成法线值// 转过算法为:贴图中存储 0 到 1 的值,而法线值是 -1 到 1vec3 normal = vec3(texture(material.normalTexture, v_out.textureCoordinate));normal = normalize(normal * 2.0 - 1.0);// 计算漫反射float diffuseRatio = max(dot(-v_out.directionalLightDirection, normal), 0.0);vec3 diffuseColor = directionalLight.diffuseColor * diffuseRatio * vec3(texture(material.diffuseTexture0, v_out.textureCoordinate));// 因为这个例子只用了漫反射贴图和法线贴图,所以其余如镜面反射或者环境光等就不计算了return diffuseColor;}void main(){viewDirection = normalize(v_out.vertexPosition - v_out.viewPosition);f_color = vec4(CaculateDiractionalLightColor(), 1.0); // 输出最终颜色}
 
 

例子的结果

最终的结果如下图所示:

从图中可以看到,除了正对着平行光的一面外,其余面在凹凸的地方会有一点颜色,而其他地方依然是黑色。这是因为对于这个砖墙的图来说,在法线贴图中砖的凹凸处所对应的法线向量显然不是 (0,0,1)(0,0,1) ,所以在这个使用了切线空间的例子中,平行于平行光方向的面转换到切线空间后,可以直接对法线贴图进行采样,而砖墙的大部分面积采样出来的法线向量是 (0,0,1)(0,0,1) ,所以对于平行于平行光方向的墙面来说,大部分像素的法线向量都垂直于平行光照射的方向,所以计算出的颜色自然为0,而砖墙的凹凸处的法线值不垂直于平行光照射的方向,所以会得到一些颜色,这应该足以说明我们的切线空间计算结果是正确的。

切线空间(Tangent Space) 的计算与应用相关推荐

  1. [图形学]切向空间(Tangent Space)

    2009年3月17日 阅读评论 发表评论     这个应该算是补遗漏,去年在MSN Space上写过一篇关于凹凸贴图的,当时写了半天其实写的一点也不明白,呵呵,因为有很多细节其实我也没搞太清楚,现在这 ...

  2. OpenGL 法线贴图 切线空间 整理

    1. What`s Bump Mapping? Bump Mapping通过改变几何体表面各点的法线,使本来是平的东西看起来有凹凸的效果,是一种欺骗眼睛的技术:). 我们知道,如果几何体表面有高低不平 ...

  3. 切线空间、法线贴图、TBN矩阵

    目录 1 法线贴图 1.1 为什么需要? 1.2 怎么做法线映射? 2 切线空间 2.1 为什么需要切线空间? 2.2 切线空间是什么? 2.3 TBN矩阵 2.4 TBN矩阵计算 3 光照计算是在` ...

  4. 切线空间(Tangent Space)

    1. 线性变换 2. 切线空间(坐标系) 2.1 切线空间的构成 2.2 切线空间中光照计算及其弊端 Reference 1. 线性变换 在掌握切线空间之前我们先来简单了解线性变换与向量空间.矩阵的关 ...

  5. 冯乐乐 unity_Unity常用矩阵运算的推导补遗——切线空间

    在上一篇文章中,我写了一些关于Unity中各个坐标空间及其转换矩阵是如何得到的,说实在的,我是那种"记忆需要依靠外部装置存储"类.如同<攻壳机动队>的电子脑一样的人,每 ...

  6. OpenGL基础46:切线空间

    到这里,关于OpenGL基础的了解要接近尾声了,上一个节点是<OpenGL基础25:多光源>.在此章之后,学习openGL的各种教程的同时,可以转战想要了解的渲染引擎,也可以去github ...

  7. Unity Shader - 切线空间的法线贴图应用(T2W W2T)

    法线贴图 法线贴图(或是法线纹理)其实就是一张图片中的RGB通道分别存储着法线方向的纹理(有些为了数据压缩将X,Y存储在RG通道,Z是通过1-dot(xy,xy)来近似计算). 它的由来是因为高模运行 ...

  8. Unity3D 法线转换与切线空间总结

    在Shader编程中经常会使用一些矩阵变换函数接口,其实它就是把固定流水线中的矩阵变换转移到了可编程流水线或者说GPU中,先看下面的函数语句: // Transform the normal from ...

  9. OpenGL核心技术之切线空间

    笔者介绍:姜雪伟,IT公司技术合伙人,IT高级讲师,CSDN社区专家,特邀编辑,畅销书作者;已出版书籍:<手把手教你架构3D游戏引擎>电子工业出版社和<Unity3D实战核心技术详解 ...

  10. 处理顶点——通过切线空间的凹凸映射添加逐像素细节

    问题 虽然前一个教程中具有不变法线的平面物体工作良好,但如果对一个曲面或有转角的表面进行凹凸映射仍会遇到麻烦. 主要问题是包含在凹凸映射中的偏离法线是在切线空间中的,这意味着它与默认法线有联系. 为了 ...

最新文章

  1. 宝马无人车体验:把司机彻底干掉,有必要吗?
  2. usb dfu和usb fastboot的区别
  3. 也谈ASP.NET页面事件
  4. 专访.NET平台上类RoR开源项目Castle[转载]
  5. Prometheus 系统监控方案
  6. PHPCMS V9 为今天或几天前文章加new
  7. 数据库设计(关系型)
  8. 样条曲线_Apollo规划算法基于样条曲线的平滑分析(一)
  9. 美国重金投资3D芯片项目!MIT+美独资公司攻关,旨在继续领先中国
  10. 分布式数据:缓存技术
  11. mousetrap.js使用详解
  12. 第22节 NAT(网络地址转换)—实现公网IP和私网IP之间的转换
  13. 华三AP(wa4320acn)wifi设置(命令行)
  14. 计算机发展史观后感50字,《计算机:一部历史》读后感_1300字
  15. 拟合函数未知数个数与用于拟合的序列点数的关系
  16. 第一章 大数据发展数据与鲲鹏大数据
  17. 如何通过移动广告平台实现手游推广
  18. Unhandled Exception:System.DllNotFoundException: Unable to load DLLquot;**quot;:找不到指定的模块
  19. tgp进游戏不显示服务器连接异常,TGP启用腾讯游戏提示“TCLS_CORE异常退出”的解决方法...
  20. Hadoop集群的安装

热门文章

  1. Unity版本升级指南 从unity xx 到 unity 20xx
  2. grep, sed, awk 的用法
  3. 关于TC Games针对没有耳机接口的Type-C用户玩手游如何传音和语音
  4. 【Rustdesk】最友好的开源远程桌面软件——Rustdesk 实现 Windows、Linux、MacOS 之间远程连接桌面
  5. vue项目使用eslint和prettier格式化项目
  6. 容器化部署openvpn,访问策略配置
  7. 总结一下m3u8格式相关问题
  8. python爬斗鱼直播房间名和主播名,Python爬虫获取斗鱼主播信息
  9. WordPress SEO优化:纯代码添加canonical标签
  10. 希尔伯特曲线 java_希尔伯特曲线(示例代码)