第六课后半部分

Reference

Lesson 6bis: tangent space normal mapping · ssloy/tinyrenderer Wiki (github.com)
从零开始的TinyRenderer(6.5)——切线空间法线贴图 - 知乎 (zhihu.com)
几何向量:计算光线反射reflect向量_反射向量-CSDN博客
为什么要有切线空间(Tangent Space),它的作用是什么? - 鸡哥的回答 - 知乎
为什么要有切线空间(Tangent Space),它的作用是什么? - Milo Yip的回答 - 知乎

上节课的末尾

漫反射

这里的漫反射只是使用了phone模型

diff = l dot n

高光

我们可得高光模型:

spe = r dot v

6.5 切线空间

为什么要用切线空间?

原因一

如图中: 左边没用切线空间贴图,右边使用了,可以看出左边该被照亮的地方没有被点亮,因为使用的是原来的法线,法线并没有随着嘴唇的位置改变而改变。

而切线空间的法线是通过UV计算出来的,模型变化了,法线也会变化。

原因二

节约纹理大小,可以看到纹理中只有手臂一侧的纹理和尾巴一侧的纹理,如果使用切线空间他们都是一样的(因为是通过UV计算得出来的),可以用同一个纹理表示。

怎么使用?

第一步使用最普通的PhongShading作为基础

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class TangentNormalShader : public Shader  
{
public:
Vector3f ndl; //法线与光方向
Matrix<float, 2, 3> uv; //uv
virtual Eigen::Vector4f vertex(int iface, int ivertex) override
{
//顶点
Vector3f v = model->vert(iface, ivertex);
//法线
Vector3f n = model->normal(iface, ivertex).normalized();
//光方向与法线方向的点乘
ndl[ivertex] = max(.0f, n.dot(light_dir));
//纹理
uv.col(ivertex) = model->uv(iface, ivertex);
return m_viewport * m_projection * m_viewcamera * Vector4f(v[0], v[1], v[2], 1.);
}
virtual bool fragment(Vector3f barycentric, TGAColor& color) override
{
float intensity = ndl.dot(barycentric);
Vector2f temp_uv = uv * barycentric;
color = model->diffuse(temp_uv);
color = color * intensity;
return false;
}};

为了方便理解如何用UV计算切线空间下的法线用一张网格图片展示一下UV坐标

如下图: 红色的线是U轴(单价与x轴),蓝色的线是V轴(等价于z轴),切线空间下的y轴就是与UV正交的任意轴。

如此一来便构成了切线空间

计算UV

但是我们现在只有 三个点(三角形的三个点)三个点的UV,我们如何建立起他的切线坐标系呢?

好吧这个求解还是有一点困难的。

我们把x y z看作基向量i j k也就得到了从三个点(三角形的三个点)三个点的UV得到切线空间

于是可以由uv计算ij,下面的A矩阵就是上面推导出来的这个:

开始编写代码

这里记得要把世界空间的法线纹理替换成切线空间下的哦,不然会有问题。

这里为什么要varying_nrm.col(ivertex) = ((m_projection * m_viewcamera).transpose() * model->normal(iface, ivertex).homogeneous()).head<3>();
我还不清楚,如果有知道可以告诉我,为什么要乘以 MVP的转置,normal不是切线坐标么?这样不是将本地坐标转换到世界坐标么?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
class TangentNormalShader : public Shader  
{
public:
Vector3f ndl; //法线与光方向
Matrix3f ndc_tri; //三个顶点
Matrix3f varying_nrm; //法线
Matrix<float, 2, 3> varying_uv; //uv
virtual Eigen::Vector4f vertex(int iface, int ivertex) override
{
//顶点
Vector3f v = model->vert(iface, ivertex);
ndc_tri.col(ivertex) = v;
//法线
varying_nrm.col(ivertex) = ((m_projection * m_viewcamera).transpose() *
model->normal(iface, ivertex).homogeneous()).head<3>();
//纹理
varying_uv.col(ivertex) = model->uv(iface, ivertex);
return m_viewport * m_projection * m_viewcamera * v.homogeneous();
}
virtual bool fragment(Vector3f barycentric, TGAColor& color) override
{
Vec3f bn = (varying_nrm * barycentric).normalized();
Vec2f uv = varying_uv * barycentric;

Matrix3f A;
A.row(0) = ndc_tri.col(1) - ndc_tri.col(0); // (p1 - p0)对应(p0p1)向量
// A[0][0]/A[0][1]/A[0][2]对应x、y、z三个分量
A.row(1) = ndc_tri.col(2) - ndc_tri.col(0); // Same as above
A.row(2) = bn; // bn表示的是原始法线向量n

Matrix3f AI = A.inverse(); // AI是A的逆矩阵

// (varying_uv[0][1] - varying_uv[0][0]) 表示(u1 - u0)
// (varying_uv[0][2] - varying_uv[0][0]) 表示(u2 - u0)
Vec3f i = AI * Vec3f(varying_uv.row(0)[1] - varying_uv.row(0)[0],
varying_uv.row(0)[2] - varying_uv.row(0)[0],
0);

// (varying_uv[1][1] - varying_uv[1][0]) 表示(v1 - v0)
// (varying_uv[1][2] - varying_uv[1][0]) 表示(v2 - v0)
Vec3f j = AI * Vec3f(varying_uv.row(1)[1] - varying_uv.row(1)[0],
varying_uv.row(1)[2] - varying_uv.row(1)[0],
0);

// Change of basis in 3D space
// 向量(i j bn)是Darboux坐标系的基准
Matrix3f B;
B.col(0) = i.normalized(); //rows[2][0] = (u1 - u0), rows[1][0] = (u2 - u0), rows[0][0] = (0)
B.col(1) = j.normalized(); //rows[2][1] = (v1 - v0), rows[1][1] = (v2 - v0), rows[0][1] = (0)
B.col(2) = bn; // rows[2][2] = bn.x, rows[1][2] = bn.y, rows[0][2] = bn.z

// 新的法线向量n(Darboux框架)
// 把其他相关向量转换到切线空间
Vec3f n = (B * model->normal(uv)).normalized();

color = model->diffuse(uv) * diff;
return false;
}
};

看起来还是有点问题的,头像的下半部分有明显的三角形。

原来是代码写错了A.row(2) = bn; // bn表示的是原始法线向量n一开始我写成了A.row(3),现在已经改正过来了,上面的代码时正确的。

至此学习完了6bit

留下的疑惑

这里为什么要varying_nrm.col(ivertex) = ((m_projection * m_viewcamera).transpose() * model->normal(iface, ivertex).homogeneous()).head<3>();
我还不清楚,如果有知道可以告诉我,为什么要乘以 MVP的转置,normal不是切线坐标么?这样不是将本地坐标转换到世界坐标么?可以将切线坐标转换到世界坐标么?