本章需要掌握的内容:①法线贴图②TBN坐标系
1.使用法线贴图的动机
左图是使用法线贴图后的结果,右图是没有使用法线贴图(使用法线插值)的结果。可以明显感知到,高光在右图中是分布在一条直线上的,并没有符合实际,而左图光照效果完美契合了纹理。
2.什么是法线贴图
法线贴图也是一种纹理,只是每一个纹素存储的并非RGB数据,红绿蓝三种分量依次存储的是压缩后的x,y,z坐标,这里的坐标实则定义的是法向量。每一个像素存储的是一个法向量。
这里的x,y,z坐标是相对于什么坐标轴而言的呢?下图👇:根据左手螺旋定律:TBN--XYZ
如图19.3,其中的向量大多都近似平行于z轴,换言之,这些向量的z轴取得了3种坐标分量中的最大值。如果将法线图看作正常RGB图的话,整体会呈现蓝色,因为z坐标存于蓝色通道之中。
压缩操作:如何将单位向量(每个坐标范围都被限定在[-1,1]),如果以平移及缩放的手段将其区间变换至[0,1],再乘以255,再截断(truncate)小数部分,最终得到[0,255]的某个整数。变换函数为:
压缩处理的逆操作:
我们不需要亲自动手参与压缩处理,因为借助PS插件就能将图片轻松转换为法线图,所以只是在PS中对法线图进行采样时,还是要实现解压缩变换过程中的一些步骤。在shader中对法线图进行采样:
float3 normalT = gNormalMap.Sample(gTriLinearSam, pin.Tex);
此时normalT将获取归一化分量构成的坐标(r,g,b),其中0≤r,g,b≤1。
也就是说,坐标的解压工作已经自动实现了一部分(从[0,255]的整数->[0,1]的浮点数),接下来,我们需要在PS中手动实现[0,1]->[-1,1]的转换,转换函数:
pixel shader代码中:
normalT = 2.f*normalT-1.f;
// 标量1.f会被扩充为向量(1,1,1),来与向量进行减法
3.纹理空间/切线空间 TBN
将纹理映射到3D三角形上时,观察纹理坐标系与三角形所处平面的关系。纹理uv坐标与三角形平面重合,再结合三角形的平面法线N,我们便得到了一个位于三角形所在平面内的3D TBN基(TBN-basis),它常被称为纹理空间(texture space)或切线空间(tangent space,正切空间、切空间)。注意:切线空间会随不同的三角形而发生改变👇。
区分:"纹理坐标系"和"纹理空间/切线空间"
- 纹理坐标系是2D坐标系,uv空间
- 纹理空间/切线空间是3D坐标系
这里的法线是基于TBN空间(纹理空间)所定义的,而为了计算光照效果,法向量需要转换到世界空间中。所以现在的目标是实现切线空间->局部空间(->世界空间)。
纹理空间中,假设顶点对应的纹理坐标(0,0),那么顶点
,如果知道TBN三个坐标轴分别相对于局部空间的坐标(假设:T、B、N),那么我们可以将顶点坐标转换到局部空间:
使用矩阵表示👆:
是局部空间中顶点坐标,
是两个顶点对应的纹理坐标,所以根据这两个我们能算出T、B向量在局部空间中的坐标。
逆矩阵性质:假设矩阵
,则有:
切向量T和B在物体空间中一般均不是单位长度,而且,若纹理发生了扭曲形变,那么这两个向量也将不再互为正交规范化向量。
按照惯例,T、B、N三个向量通常分别被称为切线(tangent,或主切线)、副法线(bionormal,或bitangent副切线)以及法线(normal)。
4.顶点切线空间
如果使用上述纹理空间进行采样时,会出现问题,得到的效果会呈现明显的三角形划分痕迹,这是因为切线空间位于三角形所在平面,以致使贴图面不够圆滑,直来直去全是棱角。所以我们需要在每个顶点处指定切向量,并使用求平均值的技术,使得顶点法线更趋于平滑的表面。
求共享顶点的平均法线,是在创建模型时求得,参考本书8.2.1节。
比如每处理一个三角形时,就对其中包含的顶点的法线进行累加,这样就能最后得到平均法线。
步骤:①我们通过计算网格中共用顶点v的每个三角形的切向量平均值,作为顶点v处的切向量②副切向量的计算同理③计算完上述平均值之后,往往还需要对TBN基进行正交规范化处理,使这3个向量相互正交且具有单位长度[格拉姆-施密特过程]。
我们不会把副切向量B直接存于内存中,要用到B时直接计算N×T即可,公式中的N即顶点法线的平均值(平均顶点法线)。此时,所定义的顶点结构体如下:
struct Vertex
{
XMFLOAT3 Pos;
XMFLOAT3 Normal; // N
XMFLOAT2 TexC;
XMFLOAT3 TangentU; // T
};
5.在切线空间与物体空间之间进行转换
切线空间=>物体空间的变换矩阵:
该矩阵是正交矩阵,所以其逆矩阵就是它的转置矩阵。
所以要将切线转换到世界空间,也可以使用TBN在世界空间中的坐标作为变换矩阵的行向量。
6.法线贴图的着色器代码
⭐注意:①插值得到的法向量(顶点法向量)是用来构建TBN基;②法线贴图采集的法向量作为像素点处的法向量。
// 法线图样本转换到世界空间
float3 NormalSampleToWorldSpace(float3 normalMapSample,
float3 unitNormalW,
float3 tangentW)
{
float3 normalT = 2.f*normalMapSample-1.f;
float3 N = unitNormalW; // 这里假设了unitNormalW是规范化向量
float3 T = normalize(tangentW-dot(tangentW,N)*N);
float3 B = cross(N,T);
float3x3 TBN = float3x3(T,B,N);
float3 bumpedNormalW = mul(normalT,TBN);
return bumpedNormalW;
}
// PS:
float3 normalMapSample = gNormalMaps.Sample(samLinear,pin.Tex).rgb;
float3 bumpedNormalW = NormalSampleToWorldSpace(normalMapSample,pin.NormalW,pin.TangentW);
插值完成后,切向量与法向量可能变成非正交规范向量。以上两行代码通过T减去其N方向上的分量(投影),再对结果进行规范化处理,从而使T称为规范化向量且正交于N。
T·N=||T||·||N||·cosθ=||T||·cosθ 就是T在N边上的投影大小,再乘上方向向量N。
struct VertexIn
{
...
float3 TangentU : TANGENT; // (局部空间)切线
};
struct VertexOut
{
...
float3 TangentW : TANGENT; // (世界空间)切线
};
另外,我们在法线图的alpha通道存储的是光泽度掩码(shiness mask),它控制着像素级别上物体表面的光泽度[1~0:白色~黑色]。
// PS:
const float shiness = (1.f-roughness)*normalMapSample.a;
课后练习题:
1.2.生成法线图的工具:①PhotoShop中NVIDIA公司制作的法线图插件②CrazyBump程序,自动生成法线图和位移图。
3.如果对纹理进行旋转,那么我们需要旋转相应的TBN,我们需要在世界空间中使切线T绕着法线N旋转,这涉及大量三角函数运算开销[绕任意轴旋转的变换矩阵]。所以我们可以将T先变换到TBN空间中,旋转,再转换回世界空间。
5.位移贴图(displacement mapping)的思路是引入一种名为高度图(heightmap)的新品贴图来描述物体表面的凹凸不平。此图常与硬件曲面细分配合使用,它指示新增加顶点在法向量上的偏移量,来为网格增添几何细节。
我们可以用位移贴图实现海平面效果:设置多张位移贴图,在平坦的顶点栅格上,以不同的速度和方向"滚动"这些高度图。分别对这些高度图采样,高度之和就是顶点的高度(y坐标)。
问题:位移贴图会修改xyz分量?位移贴图是相对于物体局部空间吗(应该是)?