- 注意!本文是在下几年前入门期间所写(young and naive),其中许多表述可能不正确,为防止误导,请各位读者仔细鉴别。
法线贴图
法线贴图简介
法线贴图我们都很熟悉了,法线一般是存在切线空间里的,一般来说大多数位置的法线方向都是和面方向偏差不大,所以法线贴图总是蓝蓝的,也就是对应rgb(0,0.5,0)。
法线的三个维度的取值范围一般都是[-1,1],而贴图采样出来的范围都是[0,1],所以我们要处理一下,把采样结果乘2减1。
下面简述一下我们要怎么使用法线贴图,首先我们要知道每个顶点的切线方向(u的偏导方向),然后在顶点着色器里把切线和法线都变换到世界坐标。然后世界坐标里就可以得到插值后的切线和法线,用这两输入值作施密特正交化得到切线空间的三个轴的坐标,也就得到了切线空间到局部空间的变换矩阵,然后把贴图上采样得到的法线变换到世界坐标来计算光照。
现在问题来了,我们要怎么知道每个顶点的切线方向呢?和顶点法线类似的,三角形法线法线可以用三角形的两边叉乘得到,顶点法线是所有经过这个顶点的三角面的法线的均值。而顶点切线,就是每个经过这个顶点的面的切线均值。现在问题就变成了求三角面的切线。首先我们规定一点,切向和uv的u的方向是一致的,每个顶点信息里都存了对应的uv,根据这一点我们可以求切线。
首先我们有三角形的两个边,我们把这两边对应的顶点的uv作差,有
(Δu0, Δv0) = (u1 − u0, v1 − v0)
(Δu1, Δv1) = (u2 − u0, v2 − v0)
然后观察下图
其中N是normal,T是tangent,B是bitangent,TBN就是切线空间,然后u的方向与T一致,v则和B一致,那么有
写成矩阵形式
Δu0、Δv0、Δu1、Δv1通过顶点uv作差得到,e0和e1就是局部坐标系下的边向量,顶点坐标作差就可以得到,现在我们就可以反解出T和B向量
其中
然后面的切线有了,顶点的切线就取下均值就可以得到,这个步骤可以在我们载入模型的时候完成,算好后就存在顶点的结构体里。
接下来我们考虑切线空间到世界空间的变换矩阵,因为要用这个矩阵来变换采样得到的法线到世界空间,首先我们知道T,B,N在局部空间的坐标,那么局部空间到切线空间的便便就是这三个向量拼在一起,那么切线到局部的变换则是这个矩阵的逆,又因为这个矩阵是正交阵,逆就是转置,所以切线空间到局部空间的变换矩阵是
这个矩阵在乘上世界矩阵,就是切线空间到世界空间的变换矩阵,注意这里我们只用来变换向量,所以只要3x3。
可以看出,切线空间到世界空间的变换矩阵其实就是世界坐标系下的T,B,N三个向量拼起来。
但是把T,B,N变换到世界坐标系下,这三个向量可能就不正交了,所以我们一般只在定点里存T和N,B则是临时算出来,T和N变换到世界坐标系后再施密特正交化,再算出B,这样就可以保证这个变换矩阵是正交阵了。
法线贴图demo
接下来我们实现一个应用了法线贴图的场景demo。
首先C++部分和之前基本上没什么区别,只要在材质里面多记录一个法线贴图的index,然后传到shader里就行。
接下来是shader部分,我们在Common.hlsl中封装一个用来将法线贴图变换到世界空间的函数。如下
//---------------------------------------------------------------------------------------
// Transforms a normal map sample to world space.
//---------------------------------------------------------------------------------------
float3 NormalSampleToWorldSpace(float3 normalMapSample, float3 unitNormalW, float3 tangentW)
{
// Uncompress each component from [0,1] to [-1,1].
float3 normalT = 2.0f*normalMapSample - 1.0f;
// Build orthonormal basis.
float3 N = unitNormalW;
float3 T