简介
以前经常听说“模型不好看啊,怎么办啊?”答曰“加法线”,”做了个高模,准备烘一下法线贴图”,“有的美术特别屌,直接画法线贴图”.....法线贴图到底是个什么鬼,当年天真的我真的被这个图形学的奇淫杂技忽悠了,然而毕竟本人还算有点刨根问底的精神,决定研究一下法线贴图的原理以及Unity下的实现。本人才疏学浅,如有错误,欢迎指正。
法线贴图是目前游戏开发中最常见的贴图之一。我们知道,一般情况下,模型面数越高,可以表现的细节越多,效果也越好。但是,由于面数多了,顶点数多了,计算量也就上去了,效果永远是和性能成反比的。怎么样用尽可能简单模型来做出更好的效果就成了大家研究的方向之一。纹理映射是最早的一种,通过纹理直接贴在模型表面,提供了一些细节,但是普通的纹理贴图只是影响最终像素阶段输出的颜色值,不能让模型有一些凹凸之类的细节表现。而法线贴图就是为了解决上面的问题,给我们提供了通过低面数模型来模拟高面数模型的效果,增加细节层次感,效果与高模相差不多,但是大大降低了模型的面数。
法线贴图原理
如果还是没理解,再看一套图片,同样一张图片,旋转180度后的结果完全相反。不信可以去截图放到MSPaint里面转一下试试,反正我是试了....
既然一个面的光照条件(亮度)的改变,就可以让我们感觉这个面有凹凸感,那么上面说的,通过改变法线来改变面上某点的光照条件,进而忽悠观察者,让他们感觉这个面有凹凸感的方法就行得通了。
假如下面是我们的低面数模型,上面是我们的高面数模型,上面的模型在计算光照时,由于面数多,每个面的法线方向不同,所以各个面的光照计算结果都不同,就有凹凸的感觉了,而下面的低模,只有一个面,整个面的光照条件都是一致的,就没有凹凸的感觉了。我们如果把上面的高模的法线信息保存下来,类似纹理贴图那样,存在一张图里,再给低模使用,低模就可以有跟高模一样的法线,进而在计算光照时达到和高模类似的效果,这也就是常说的烘法线的原理。
凹凸贴图(Bump Map)
- //Bump Map
- //by:puppet_master
- //2016.12.13
- Shader "ApcShader/BumpMap"
- {
- //属性
- Properties{
- _Diffuse("Diffuse", Color) = (1,1,1,1)
- _MainTex("Base 2D", 2D) = "white"{}
- _BumpMap("Bump Map", 2D) = "black"{}
- _BumpScale ("Bump Scale", Range(0.1, 30.0)) = 10.0
- }
- //子着色器
- SubShader
- {
- Pass
- {
- //定义Tags
- Tags{ "RenderType" = "Opaque" }
- CGPROGRAM
- //引入头文件
- #include "Lighting.cginc"
- //定义Properties中的变量
- fixed4 _Diffuse;
- sampler2D _MainTex;
- //使用了TRANSFROM_TEX宏就需要定义XXX_ST
- float4 _MainTex_ST;
- sampler2D _BumpMap;
- float4 _BumpMap_TexelSize;
- float _BumpScale;
- //定义结构体:应用阶段到vertex shader阶段的数据
- struct a2v
- {
- float4 vertex : POSITION;
- float3 normal : NORMAL;
- float4 texcoord : TEXCOORD0;
- };
- //定义结构体:vertex shader阶段输出的内容
- struct v2f
- {
- float4 pos : SV_POSITION;
- float3 worldNormal : TEXCOORD0;
- //转化纹理坐标
- float2 uv : TEXCOORD1;
- };
- //定义顶点shader
- v2f vert(a2v v)
- {
- v2f o;
- o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
- //把法线转化到世界空间
- o.worldNormal = mul(v.normal, (float3x3)_World2Object);
- //通过TRANSFORM_TEX宏转化纹理坐标,主要处理了Offset和Tiling的改变,默认时等同于o.uv = v.texcoord.xy;
- o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
- return o;
- }
- //定义片元shader
- fixed4 frag(v2f i) : SV_Target
- {
- //unity自身的diffuse也是带了环境光,这里我们也增加一下环境光
- fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * _Diffuse.xyz;
- //归一化法线,即使在vert归一化也不行,从vert到frag阶段有差值处理,传入的法线方向并不是vertex shader直接传出的
- fixed3 worldNormal1 = normalize(i.worldNormal);
- //采样bump贴图,需要知道该点的斜率,xy方向分别求,所以对于一个点需要采样四次
- fixed bumpValueU = tex2D(_BumpMap, i.uv + fixed2(-1.0 * _BumpMap_TexelSize.x, 0)).r - tex2D(_BumpMap, i.uv + fixed2(1.0 * _BumpMap_TexelSize.x, 0)).r;
- fixed bumpValueV = tex2D(_BumpMap, i.uv + fixed2(0, -1.0 * _BumpMap_TexelSize.y)).r - tex2D(_BumpMap, i.uv + fixed2(0, 1.0 * _BumpMap_TexelSize.y)).r;
- //用上面的斜率来修改法线的偏移值
- fixed3 worldNormal = fixed3(worldNormal1.x * bumpValueU * _BumpScale, worldNormal1.y * bumpValueV * _BumpScale, worldNormal1.z);
- //把光照方向归一化
- fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);
- //根据半兰伯特模型计算像素的光照信息
- fixed3 lambert = 0.5 * dot(worldNormal, worldLightDir) + 0.5;
- //最终输出颜色为lambert光强*材质diffuse颜色*光颜色
- fixed3 diffuse = lambert * _Diffuse.xyz * _LightColor0.xyz + ambient;
- //进行纹理采样
- fixed4 color = tex2D(_MainTex, i.uv);
- return fixed4(diffuse * color.rgb, 1.0);
- }
- //使用vert函数和frag函数
- #pragma vertex vert
- #pragma fragment frag
- ENDCG
- }
- }
- //前面的Shader失效的话,使用默认的Diffuse
- FallBack "Diffuse"
- }
效果如下:
法线贴图(Normal Map)
法线贴图是怎样存储的
- 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
- }
做法很简单,乘2 减1大法好,转化区间没烦恼(什么鬼....)
为什么法线贴图存储在切线空间
N = T × normalize(dx/dv, dy/dv, dz/dv)
B = N × T
- // Declares 3x3 matrix 'rotation', filled with tangent space basis
- #define TANGENT_SPACE_ROTATION \
- float3 binormal = cross( normalize(v.normal), normalize(v.tangent.xyz) ) * v.tangent.w; \
- float3x3 rotation = float3x3( v.tangent.xyz, binormal, v.normal )
为什么法线贴图都是蓝色的
Unity下法线贴图Shader实现
- //Bump Map
- //by:puppet_master
- //2016.12.14
- Shader "ApcShader/NormalMap"
- {
- //属性
- Properties{
- _Diffuse("Diffuse", Color) = (1,1,1,1)
- _MainTex("Base 2D", 2D) = "white"{}
- _BumpMap("Bump Map", 2D) = "bump"{}
- _BumpScale ("Bump Scale", Range(0.1, 30.0)) = 10.0
- }
- //子着色器
- SubShader
- {
- Pass
- {
- //定义Tags
- Tags{ "RenderType" = "Opaque" }
- CGPROGRAM
- //引入头文件
- #include "Lighting.cginc"
- //定义Properties中的变量
- fixed4 _Diffuse;
- sampler2D _MainTex;
- //使用了TRANSFROM_TEX宏就需要定义XXX_ST
- float4 _MainTex_ST;
- sampler2D _BumpMap;
- float _BumpScale;
- //定义结构体:vertex shader阶段输出的内容
- struct v2f
- {
- float4 pos : SV_POSITION;
- //转化纹理坐标
- float2 uv : TEXCOORD0;
- //tangent空间的光线方向
- float3 lightDir : TEXCOORD1;
- };
- //定义顶点shader
- v2f vert(appdata_tan v)
- {
- v2f o;
- o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
- //这个宏为我们定义好了模型空间到切线空间的转换矩阵rotation,注意后面有个;
- TANGENT_SPACE_ROTATION;
- //ObjectSpaceLightDir可以把光线方向转化到模型空间,然后通过rotation再转化到切线空间
- o.lightDir = mul(rotation, ObjSpaceLightDir(v.vertex));
- //通过TRANSFORM_TEX宏转化纹理坐标,主要处理了Offset和Tiling的改变,默认时等同于o.uv = v.texcoord.xy;
- o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
- return o;
- }
- //定义片元shader
- fixed4 frag(v2f i) : SV_Target
- {
- //unity自身的diffuse也是带了环境光,这里我们也增加一下环境光
- fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * _Diffuse.xyz;
- //直接解出切线空间法线
- float3 tangentNormal = UnpackNormal(tex2D(_BumpMap, i.uv));
- //normalize一下切线空间的光照方向
- float3 tangentLight = normalize(i.lightDir);
- //根据半兰伯特模型计算像素的光照信息
- fixed3 lambert = 0.5 * dot(tangentNormal, tangentLight) + 0.5;
- //最终输出颜色为lambert光强*材质diffuse颜色*光颜色
- fixed3 diffuse = lambert * _Diffuse.xyz * _LightColor0.xyz + ambient;
- //进行纹理采样
- fixed4 color = tex2D(_MainTex, i.uv);
- return fixed4(diffuse * color.rgb, 1.0);
- }
- //使用vert函数和frag函数
- #pragma vertex vert
- #pragma fragment frag
- ENDCG
- }
- }
- //前面的Shader失效的话,使用默认的Diffuse
- FallBack "Diffuse"
- }
结果: