
这次同样是为了之后做的某个效果所做的铺垫,主要会讲一种基于数学过程的法线混合算法。
先简单讲讲什么是法线贴图。如果对更多细节感兴趣,可以看看我很早之前写的一篇文章【链接:https://pan.baidu.com/s/1SAYkZrJT1--UNzU89T5qAg 密码:w5bd】。不过我在写这篇文的时候刚接触shader,虽然写的详细到了把读者当傻子的程度,但可能有些细节上的理解是存在错误的- -不过如果能耐下性子看完也基本能让人吃透法线贴图到底是怎么回事_(:з」∠)_
所谓法线贴图,一般情况下指切线空间法线贴图(关于啥是切线空间,看之前的网盘文章或网页链接的第一节),是如今游戏里用烂了的一种低模表现高模细节光影的造假手段。(但不会改变模型的结构,也无法产生遮挡效果。之前的网页链接可以解决遮挡的问题)

上图的左上角就是案例使用的法线贴图。
经常有人好奇,为啥法线贴图总是蓝不拉几的颜色,这和它存储的信息与编码方式有关。法线贴图的RGB存储的是:做了一次[-1,1]->[0,1]的线性映射的单位法向量坐标。比如原本的单位向量n=(-3/5,0,4/5),换算成归一化RGB色值就是0.5n+0.5=(0.2,0.5,0.9)。
之所以做这个映射,是因为单位向量的坐标范围是[-1,1],而图像颜色是[0,1]。为了让图像能记录负值,所以加一层编码。在切空间下,法向量的坐标一般都是在沿z轴方向(0,0,1)的基础上有一定的向x或y轴方向的偏移,也就是说大部分情况下,z值总是3个分量中最大的。z在转换后对应的就是法线贴图中的蓝通道,所以法线贴图总是偏蓝。
额外说一嘴,这里其实有一个优化点。因为存储的坐标是单位向量,即R^2+G^2+B^2=1所以就意味着任意知道其中两个就可以算出第三个,也就是说三个通道中,实际上有一个是冗余的。有时可以利用这点节省出一个通道塞点其他的东西进去。
如果不考虑合理性,最常见的法线混合算法可能就是解码之后,分别乘个法线强度然后相加,类似下面这样。
half3 nDirTS_1 = UnpackNormal(tex2D(_NormalMap_1, i.uv_1_2.xy));
nDirTS_1.xy *= _NormalInt_1;
half3 nDirTS_2 = UnpackNormal(tex2D(_NormalMap_2, i.uv_1_2.zw));
nDirTS_2.xy *= _NormalInt_2;
half3 nDirTS = normalize(nDirTS_1 + nDirTS_2); 其实大部分情况下,这么做简单高效易理解,效果上个人觉得也没啥肉眼可见的大问题。

如上图,黑线左侧是直接相加的算法,右侧是用本文的数学过程计算的效果。在法线强度都是1的前提下,直接相加的算法其实就相当于两张法线图做了一个平均,所以其凹凸感会减弱。但即便如此,通过调参也可以让直接相加的效果逼近本文算法的效果。
那么本文的探索除了自嗨以外还有什么意义?个人觉得,可以类比光照模型中的PBR渲染。虽然用像是lambert,blinn等最基础的光照模型再加上一些定制化的造假算法,效果上总是能逼近PBR基于物理的渲染效果的(我现在用的各种光照模型也基本是这种套路,而很少直接用PBR)。但PBR具有一种泛用性,即只要渲染风格是偏写实的,不管什么场景什么角度,用它就能保证光影效果总是物理正确的,而不用像定制化的光照模型,可能不同场景都得单独调一份材质球。
好,继续往下写的意义已经编出来了_(:з」∠)_那么接下来就是让大家“欲罢不能”的数学阶段了。
对于一个3D曲面方程F(x,y,z)=0,其上每一点的法向量可以表示为F的偏导数组合(Fx,Fy,Fz)。而对于切线空间中的法线贴图,其对应的曲面又都可以表示为z=F(x,y)的高度场。而z=F(x,y)等价于z-F(x,y)=0,进一步的,其法向量便可表示为如下形式。
在程序中,我们经常会给法线贴图配一个“法线强度”的参数,程序中是用该参数去乘切空间法向量的xy分量,然后将其转到世界空间进行后续计算。这个过程以数学的方式描述即z-kF(x,y)=0,k对应法线强度参数。结合上式可得缩放后的法向量如下。
法线贴图的混合可以看做是“若干个高度场线性相加,求总高度场的法向量”这么个问题。对于总高度场可以表示为如下和式。
其中,ki对应第i张法线贴图的法线强度。用与单一高度场时相同的方法,我们就可以得到总高度场法向量的表达式。
需要注意的是,在实际程序中,我们通过纹理采样得到的向量坐标还需要按第一节中所说的规则反向解码才能得到第三节中的数学形式。
对于U3D图像导入设置中设置为NormalMap的纹理,我们需要用以下方法获得切空间法向量。
half3 nDirTS = UnpackNormal(tex2D(_NormalMap, i.uv)); 而如果是自定义法线纹理,纹理导入设置中的类型需要设为Default,且取消勾选sRGB,并在程序中手动解码。例如若是将法线纹理编码在rgb通道中,解码过程如下。
half3 nDirTS = tex2D(_NormalMap, i.uv).rgb * 2.0 - 1.0; 注意,此时的nDirTS还不是第三节中的n,还需要在法线混合时对xy分量除z分量,例如下面两个法线混合时的情况。
half3 nDirTS = half3(nDirTS_1.xy / nDirTS_1.z + nDirTS_2.xy / nDirTS_2.z, 1.0);