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; 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; 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); A.row(1) = ndc_tri.col(2) - ndc_tri.col(0); A.row(2) = bn; Matrix3f AI = A.inverse(); 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); 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); Matrix3f B; B.col(0) = i.normalized(); B.col(1) = j.normalized(); B.col(2) = bn; 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不是切线坐标么?这样不是将本地坐标转换到世界坐标么?可以将切线坐标转换到世界坐标么?