一、凹凸贴图

凹凸贴图主要用于增强表现细节,核心在于法线,因为光照模型中法线是影响物体表面细节的重要因素,而法线贴图就是一种非常常见和重要的凹凸贴图。

二、法线贴图

法线贴图其实就是将物体的法线信息存入到贴图中,一个像素点的4个分量可以分别代表不同的信息,当它的xyz都用来存储物体的法线信息的时候这就是一张法线贴图,如果存储的是别的属性信息,那它就是别的图。

切线空间的法线:x轴代表该点的切线方向,z轴代表该点的法线方向,y轴是通过x轴和z轴叉积得到,又称作副切线,因此切线空间的法线贴图是容易压缩的。

法线的分量是-1到1,而像素分量是0到1,所以要注意与像素之间做好转换,unity中有相应的函数进行处理,随后会讲到。

三、切线空间的法线贴图着色器实现思路

首先我们使用的是blin-phong光照模型,或者phong模型也一样,我们需要得到一个坐标点的三个方向,这是最主要的任务。

一个是视点的方向,也就是视点到顶点的方向v,一个是点光源(注意不是方向光)到顶点的方向l,还有一个是顶点的法线方向n。因为我们是在切线空间中计算,所以我们要把向量都转换到切线空间中。法线方向存储在法线贴图中,我们使用的法线贴图本来就是基于切线空间的,所以不用做空间的转换,如下,我们只需要对贴图采样,然后使用UnpackNormal函数,unity会自动把像素转换到向量(前提是导入法线贴图的时候要在贴图类型中勾选Normal Map),之后我们要注意后面的处理,xy分量乘上scale,由于法线向量模始终要为1,所以切线和副切线的比重越大,凹凸就越明显,因为平的地方xy分量靠近0,受scale影响较小,scale能够放大凹凸的程度。

fixed4 packedNormal = tex2D(_Bump, i.uv.zw);
fixed3 tangentNormal = UnpackNormal(packedNormal);
tangentNormal.xy *= _BumpScale;
tangentNormal.z = sqrt(1-pow(tangentNormal.x,2)-pow(tangentNormal.y,2));

而v和l就需要转换到切线空间了,这个时候需要计算切线矩阵,切线矩阵也是由伸缩旋转和平移三大变换组成,首先伸缩变换是不需要的,因为放缩没有意义,我们在切线空间中处理的都是向量;平移变换也不需要,因为切线空间是每个顶点的切线空间,所以切线空间和模型空间的原点是相同的,所以我们要计算的就是旋转矩阵,这个旋转矩阵就是我们最终从模型空间到切线空间的转换矩阵

在unity的内置文件UnityCG.cginc也提供了相应的宏TANGENT_SPACE_ROTATION,使用这个宏我们直接就得到rotation,也就是切线矩阵,我们在顶点着色器内计算切线矩阵,在顶点着色器内每个顶点的计算时相互独立的,它们拥有各自的切线矩阵,之后在顶点着色器内就可以进行矩阵变换。

TANGENT_SPACE_ROTATION;
o.tangentLightDir = normalize(mul(rotation,ObjSpaceLightDir(v.vertex))).xyz;
o.tangentViewDir = normalize(mul(rotation, ObjSpaceViewDir(v.vertex))).xyz;

但是这个宏需要顶点的法线信息和切线信息,所以在顶点着色器的输入中,我们要接受法线和切线信息的输入。

struct a2v{
	float4 vertex:POSITION;
	float3 normal:NORMAL;
	float4 tangent:TANGENT;
	float4 texcoord:TEXCOORD0;
};

法线的归一化操作可不可以在顶点着色器内完成:这是面试时面试官提出的一个问题,要明白,顶点着色器中进行的是逐顶点操作,顶点的数量肯定是远小于像素数量的,所以如果一个计算能够在顶点着色器内完成,那肯定会大大降低计算量;但问题是,在顶点着色器内会不会出错,如果在顶点着色器内进行归一化,而没有在片元着色器内归一化,那么在光栅化阶段所做的就是插值来计算片元的法线,插值得到的法线可未必是归一化的了,例如(1,0,0)和(0,1,0),其中点为(0.5,0.5,0),就不是一个归一化法线数据;

四、参考资料 

1.这里面介绍了unity法线贴图的很多细节部分,非常nice:UnityShader 法线贴图。

2.法线贴图在屏幕特效中的使用:屏幕后处理效果之碎屏特效。