1、Bump Mapping综述
2、Unity3D ShaderLab开发实战详解
其实搞清了切线空间的问题之后,凹凸贴图这个论题就没有什么特别的难点了,因此这里只做简单的整理。
使用凹凸贴图,是为了给光滑的平面,在不增加顶点的情况下,增加一些凹凸的变化。他的原理是通过法向量的变化,来产生光影的变化,从而产生凹凸感。实际上并没有顶点(即Geometry)的变化。
Bump Map和Normal Map这两个术语现在已经经常混用,但精确来说,Bump Map使用的是高度图,保存的是纹理坐标点(u,v)处的高度,即该点沿法线方向的变化值。有了高度信息之后,可以比较该点与周围各点的高度差,从而得到该点处的坡度,再扰动法向量。
算法如下:
du = 1 / HightMapWidth;
dv = 1 / HightMapHeight;
u_gradient = Height(u-du, v) - Height (u+du, v); //U方向的坡度
v_gradient = Height(u, v-dv) - Height (u, v+dv); //V方向的坡度
New_Normal = Normal + (T * u_gradient) + (B * v_gradient)
由于这样的计算方法需要取样多次,在实时计算中非常的费,所以现在已经很少使用。但是他是凹凸贴图的鼻祖,下面的种种方法,可以认为都是对他的改进,核心的思路并没有改变,都是要去扰动法向量。
目前最常用的凹凸贴图技术(很可能没有之一),就是Normal Map。
前面的Bump Map可以看到,要求取正确的法线结果,非常的麻烦,每个点需要取样4次。所以Normal Map就简单粗暴的直接将正确的Normal值保存到一张纹理中去,那么在使用的时候直接从贴图中取即可。
New_Normal = NormalMap(u, v);
使用时需要注意的地方有两点:
1)Normal Map的压缩:在unity中,将一个纹理设置为Normal Map,则会根据平台的不同进行压缩(对非移动平台为DXT5nm,移动平台不压缩),那么数据结构会发生一些变化,不能直接使用,需要解压缩。Unity为我们提供了这个方法UnpackNormal,在使用时需要调用:
New_Normal = UnpackNormal( tex2D(_NormalMap, uv) )
2)
Normal Map中取样出的 New_Normal 数据,是位于切空间的。使用时,需要将光照转移到切空间来进行运算。
有关切空间 [T, B, N] 的说明,可以参考我之前写的《Unity中的坐标系》。其实前面的 Bump Map 运算出来的结果也是位于切空间的(这是由于要使用 du、dv做坐标轴)。
那么上面的Normal Map是不是完美无缺了呢?不是的。从公式中可以看到,New_Normal 与当前的视角无关。但实际上,从不同的角度去观察一个凹凸不平的物体上的同一位置,看到的法线应该是不一样的。
这张图来自“Parallax Mapping with Offset Limiting: A PerPixel Approximation of Uneven"(你可能已经见过它无数次了)。这张图是Parallel Map 和 Relief Map的核心,下面来说明这张图。
假定我们的视线范围很小,只有一条线。如果从正上方观察点T,那么我们只能看到点T,此时他的法线应该取用normal map 中点T的法线,即法线的扰动量为 AT;但如果从eye位置观察点T,那么我们只能看到点B,此时法线的扰动量是 BT,所以应该取用 Normal Map 中点 T(corrected)处的法线。
但是要找到 B 点是比较难的,因为需要求取视线和 Normal Map 的交汇点,而 Normal Map 又不能用公式描述,所以要精确确定非常麻烦。
但是 Parallax 算法的发明人发现,使用一个近似的、容易计算的、随着视角变化的偏移量,就可以达到不错的效果:
在Unity中,这个偏移值是这样计算的:
inline float2 ParallaxOffset( half h, half height, half3 viewDir )
{
h = h * height - height/2.0; //即: h = height/2 * (h * 2 - 1) 即先将取值范围为[0, 1]的 h 投影到 [-1, 1]区间,再乘以 height/2
float3 v = normalize(viewDir);
v.z += 0.42;
return h * (v.xy / v.z);
}
其中 height 是用来控制偏移大小的参数。h 是该顶点位置的高度,从 heightMap (或者命名为 ParallaxMap)采样得到。viewDir需要转换到切线空间。
取得这个偏移之后,就可以再来用正确的uv进行采样了:
uv_offset = ParallaxOffset( tex2D(heightMap, uv), _Parallax, viewDirInTBN );
New_Normal = UnpackNormal( tex2D(normalMap, uv + uv_offset) )
虽然Parallax Map的效果已经不错,但是毕竟只是一个近似,离真实的效果还是有一定差距的。那么,如果求取出精确的 T(Correct) 点,再进行Normal采样,这就是 Relief Map了。
前面也说到由于Normal Map不能用公式精确描述,所以要求这样的点,没有公式法可以求。所以做法就是通过步进法逐步逼近。
整体算法思路很简单,可以参考 《ShaderLab 开发实战详解》中Relief Mapping 算法一节,这里只解释一点,即所要求取的目标点 T(Correct)的特征是什么?是此时切线空间内,ViewDir的 Z 分量(即在Normal方向上的投影),与从 Height Map中采样得到的高度值 h 相等。把握住这一要素,看懂算法就很容易了。(由于懒,这里就不再复述一边算法了,总之就是步进就是了。)