【Unity Shaders】学习笔记——SurfaceShader(七)法线贴图
写作本系列文章时使用的是Unity5.3。
写代码之前:
当然啦,如果Unity都没安装的话肯定不会来学Unity Shaders吧?
阅读本系列文章之前你需要有一些编程的概念。
在VS里面,Unity Shaders是没有语法高亮显示和智能提示的,VS党可以参考一下这篇文章使代码高亮显示,也可以下载shaderlabvs或NShader之类的插件使代码高亮显示。
这是针对小白的Unity Shaders的基础知识,如果你已经有了基础或者你是大神,那么这些文章不适合你。
由于作者水平的局限,文中或许会有谬误之处,恳请指出。
法线贴图是一种在低模上模拟高模的效果的技术。这是维基对它的介绍。
法线贴图类似凹凸贴图的升级版,凹凸贴图记录了物体表面凹凸的情况,法线贴图记录了物体表面凹凸的光照信息。光照信息即是入射光与法线的夹角信息。
为了提高性能,模型的面数越少越好,很多细节的东西都是用贴图去弥补。但是光照是基于顶点去计算的,这样高光阴影等光照的表现就不够真实。于是前辈们发明了法线贴图这个办法,用贴图记录表面的光照信息,也就是用RGB值存储法线坐标的XYZ值,使低模也能够有高模的光照信息,从而表现出高模的光照效果。这是一种存储空间换计算时间的方法。这篇文章介绍了3DMax制作法线贴图的方法,看完应该会更理解法线贴图的作用。
下面这张图便说明了法线贴图的原理:
接下来就来学习如何编写有法线贴图的Shader吧。
首先准备一张NormalMap,要将它的Texture Type改为Normal Map:
先定义几个Properties:
Properties
{
//Add these Properties
_MainTint ("Diffuse Tint", Color) = (1,1,1,1)
_NormalTex ("Normal Map", 2D) = "bump" {}
_NormalIntensity ("Normal Map Intensity", Range(0,2)) = 1
}
_NormalTex就是法线贴图,_NormalIntensity是法线的强度,也就是控制凹凸的强度。
在SubShader里定义同名的变量:
//Link the property to the CG program
sampler2D _NormalTex;
float4 _MainTint;
float _NormalIntensity;
在Input结构里定义法线贴图的纹理坐标:
//Make sure you get the uvs for the texture in the Struct
struct Input
{
float2 uv_NormalTex;
};
编写surf函数:
void surf (Input IN, inout SurfaceOutput o)
{
//Get the normal Data out of the normal map textures
//using the UnpackNormal() function.
float3 normalMap = UnpackNormal(tex2D(_NormalTex, IN.uv_NormalTex));
normalMap = float3(normalMap.x * _NormalIntensity,
normalMap.y * _NormalIntensity, normalMap.z);
//Apply the new normals to the lighting model
o.Normal = normalMap.rgb;
o.Albedo = _MainTint.rgb;
o.Alpha = _MainTint.a;
}
解释
法线贴图中记录了高模的法线信息。法线的值是[-1,1],RGB的值是[0,1],所以将法线信息存储在RGB中需要转换。方法是((x,y,z)+(1,1,1))*0.5。也就是原先(-1,0,1)坐标变成了(0,0.5,1)。UnpackNormal函数的作用就是转换坐标点。
在Unity安装目录(/Editor/Data/CGIncludes/)下的Unity.cginc文件里有UnpackNormal函数的定义。
inline fixed3 UnpackNormalDXT5nm (fixed4 packednormal)
{
fixed3 normal;
normal.xy = packednormal.wy * 2 - 1;
normal.z = sqrt(1 - saturate(dot(normal.xy, normal.xy)));
return normal;
}
inline fixed3 UnpackNormal(fixed4 packednormal)
{
#if defined(UNITY_NO_DXT5nm)
return packednormal.xyz * 2 - 1;
#else
return UnpackNormalDXT5nm(packednormal);
#endif
}
我们看到,有两种转换法线的函数,这两种函数是针对不同法线贴图格式的。
如果是未压缩的格式,就将法线贴图里存储的xyz值乘2减1,变回原来的坐标。
如果是DXT5nm压缩格式(可以推测这应该是Unity使用的压缩格式),则要用另外一种转换方法。(这也是为什么要将纹理类型设置为Normal Map的原因,因为不同类型的图片要用不同的方法转换)
DXT5nm压缩格式,只有G和A通道。因为法线是单位向量,所以只要知道任意两个坐标值,就能求出另一个坐标值(z2=1-x2-y2),只要两个通道存储两个坐标值即可。因为这样的计算并不复杂,所以可以节省空间以使用更多的贴图。DXT5nm将R通道的值移到了Alpha通道,保留G通道的值,R和B通道则以某种颜色填充。
所以转换DXT5nm的法线贴图,要先将A通道和G通道的值(wy)转换为xy,再计算z值。
后面的代码就好解释了。转换了法线坐标以后将法线赋值给输出结构体的Normal变量。因为法线(0,0,1)就表明这点的法线和高模的法线相同,所以Z坐标没有必要乘_NormalIntensity。
法线贴图原理
法线贴图的基本原理前面已经讲过了,就是将高模的法线信息xyz存储在rgb中,应用法线贴图的时候将rgb值转换为xyz,在计算光照的时候,使用高模的法线进行计算,从而得到高模的光影效果,造成低模呈现高模较为平滑的表面的假象,这是一种视觉欺骗。
这个小节里要再详细谈谈法线的坐标。
要表示一个向量在三维空间的位置需要一个三维坐标系作参考。要表示法线的方位可以用世界坐标(World Space),也可以用模型坐标(Object Space)。这两个坐标系大家应该听说过。
如果用世界坐标,那么模型就不能旋转和移动了。因为模型的位置改变了,它在世界坐标系里的坐标也改变了,但法线的坐标是基于世界坐标系的,这样法线的位置就不对了。不然读取了法线信息还要再进行世界坐标的转换。
如果用模型坐标,就没有世界坐标不能旋转和移动的局限了。不管模型怎么旋转移动,法线的位置是相对于模型的,所以法线的坐标不会变。相比于世界坐标,使用模型坐标还要进行模型坐标到世界坐标的转换,效率低点,但灵活性更好了。
但是使用模型坐标还有局限,就是模型不能变形。因为模型变形了,法线相对模型的坐标也变了,一些会有形变的物体(比如有骨骼蒙皮动画的模型)就不适用基于模型坐标的法线贴图。
所以还需要另外一种坐标系,使法线贴图不仅仅适用于某个模型,还可以用在其他模型上。
这种坐标系就是切线坐标(Tangent Space)。切线坐标就以表面上某一点的切线作为XY轴,法线作为Z轴(即垂至表面的轴)。这样的话,符合要求的坐标系有无数种。我们可以直接将纹理的UV坐标当作XY轴,这样就有一个现成的坐标系可以使用。
可以这样理解切线坐标,它表示的是高模上的法线相对对应点上的低模的法线的扰动程度。当法线坐标是(0,0,1)的时候,说明高模法线和低模法线一致,相当于切线坐标表示的是高模法线相对低模法线在XY方向上的偏移程度。这也解释了上文代码中为什么Z轴不用乘_NormalIntensity。
法线贴图是用于表现表面微小的凹凸的,比如皮肤皱纹、鱼鳞等,所以法线的扰动值不会太大,也就是Z坐标的值是比较大,所以B通道的值比较大,所以法线贴图通常都是蓝盈盈的。
使用切线坐标也就是说每个面都有一个自己的坐标系。在Vertex Shader里进行光照计算的时候要将光线向量的坐标从World Space转换到Tangent Space。所以我们可以在Surface Shader里使用lightDir变量和normal变量进行计算,因为Unity帮我们做了很多工作。
相比模型坐标,切线坐标更加灵活,可以应用在与原模型形状不同的模型上,比如可以把花岗岩的法线贴图应用在一个圆柱体表面,使圆柱体表面也具有花岗岩的凹凸效果。
法线贴图通常有两种,一种是Object Space Normal Map,一种是Tangent Space Normal Map。如果模型没有动画,那么可以使用Object Space Normal Map,如果模型有动画,或者会变形(比如流动的水、火焰)那就要使用Tangent Space Normal Map。因为Tangent Space Normal Map要灵活的多,可以应用于不同的物体,所以Tangent Space Normal Map使用得更多一些。