我的第一个法线贴图
2018.1.16更新:在看一本书时,里面提到了“增强法线贴图的强度”:采样normalTex后,归一化前,让法线分量的x和y乘以一个强度参数:
normal = normal*fixed3(param,param,1);,然后再归一化,当param>1时,模型更加“凹凸”了,原因是:如果没凹凸,则normal=(0,0,1)(可以参考深入理解法线贴图),x和y乘以一个>1的数后,normal的z分量被“稀释”,即越发地偏离(0,0,1),即光照计算结果变大,所以就更加凹凸了。
2017.11.8更新:以前不懂tangent.w的含义,读此文得到:“where m = ±1 represents the handedness of the tangent space”,大意是不同模型使用左手或右手螺旋得到B,所以必须根据模型确定B的方向,所以就把这个方向(±1)存到tangent.w里了;
另外需要提及的是:
下面的:
float3x3 objectToTanM = float3x3(tangent, binnormal, normal);//tangent是行向量 mul(objectToTanM, ObjSpaceLightDir(v.vertex));
这里有几个点被省略了。因为这里的第二行使用的是矩阵右乘向量,所以构建切线空间基矩阵时应当使用列向量矩阵:[T,B,N](都是列向量),我们要转换向量到切线空间则需使用他的逆,又因为[T, B, N]基本正交,所以直接使用它的转置作为转换矩阵:[T', B', N'](T'是行向量),这就是上面构建objectToTanM这个转到切线空间矩阵所对应的。
同理,如果第二行使用的是mul(ObjSpaceLightDir(v.vertex), objectToTanM),则是向量左乘变换矩阵,所以空间基应当是行向量矩阵:[T',B',N'],其逆用其转置代替:[T,B,N](都是列向量),这样objectToTanM就需要在原来的基础上转置一下了。
先上简单的surf版法线贴图:
Shader "Custom/NormalSurfShader" { Properties { _Color ("Color", Color) = (1,1,1,1) _MainTex ("Albedo (RGB)", 2D) = "white" {} _NormalMap("Normal Map", 2D) = "white" {} } SubShader { Tags { "RenderType"="Opaque" } LOD 200 CGPROGRAM // Physically based Standard lighting model, and enable shadows on all light types #pragma surface surf BlinnPhong fullforwardshadows // Use shader model 3.0 target, to get nicer looking lighting #pragma target 3.0 struct Input { float2 uv_MainTex; float2 uv_NormalMap; }; sampler2D _MainTex; sampler2D _NormalMap; fixed4 _Color; void surf (Input IN, inout SurfaceOutput o) { o.Albedo = tex2D (_MainTex, IN.uv_MainTex) * _Color; o.Normal = UnpackNormal(tex2D(_NormalMap, IN.uv_NormalMap)); } ENDCG } FallBack "Diffuse" }
效果:
然后上顶点shader:
Shader "Unlit/NormalVFShader" { //法线贴图是,在主贴图基础上,先算出在切线空间的光照方向lightDir,视线方向viewDir //接着,通过Normal Map采样得到各个顶点的在切线空间的法线Normal,然后由此三者根据光照模型 //算出该点漫反射和高光,则该点的像素值就是diff+specular+ambient(环境光) //所以法贴是为了计算漫反射和高光,即为光照模型服务。surf shader把这个过程都封装起来了。 Properties { _MainTex ("Texture", 2D) = "white" {} _NormalMap("Normal Map", 2D) = "white" {} _SpecColor("Specular Color", Color) = (1,1,1,1) _Shininess("Shininess", Float) = 228 } SubShader { Tags { "RenderType"="Opaque" } LOD 100 Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc" struct appdata { float4 vertex : POSITION; float2 uv : TEXCOORD0; }; struct v2f { float2 uv : TEXCOORD0; float4 vertex : SV_POSITION; float3 lightDir: TEXCOORD1; float3 viewDir : TEXCOORD2; }; sampler2D _MainTex; sampler2D _NormalMap; float4 _SpecColor; float _Shininess; uniform float4 _LightColor0; float4 _MainTex_ST; v2f vert (appdata_tan v) { v2f o; o.vertex = mul(UNITY_MATRIX_MVP, v.vertex); o.uv = TRANSFORM_TEX(v.texcoord.xy, _MainTex); //下面的6句是为了得到切线空间的光照方向和视角方向,为什么要转到切线空间?因为法贴中的法线是以切线空间坐标存的, //后面计算漫反射,高光需要他们统一坐标系,所以其实也可以把大家(3者)都转到世界坐标系,但那样效率估计比这样慢一点 float3 normal = v.normal; float3 tangent = v.tangent; float3 binnormal = cross(normal, tangent) * v.tangent.w;//为什么乘tangent.w?记住就好 float3x3 objectToTanM = float3x3(tangent, binnormal, normal); //TANGENT_SPACE_ROTATION 这个宏可以代替上面4句 o.lightDir = mul(objectToTanM, ObjSpaceLightDir(v.vertex)); o.viewDir = mul(objectToTanM, ObjSpaceViewDir(v.vertex)); return o; } fixed4 frag (v2f i) : SV_Target { // sample the texture fixed4 col = tex2D(_MainTex, i.uv); float3 ambient = col * UNITY_LIGHTMODEL_AMBIENT.rgb; fixed4 encodeNormal = tex2D(_NormalMap, i.uv);//切线空间的法线 fixed4 normal = encodeNormal * 2 - 1;//(0-1)=》(-1,1), normal.z = sqrt(1 - saturate(dot(normal.xy, normal.xy)));//解压出z //漫反射 // I = k*texColor*LightColor*(NL),这里的texColor我是抄Lighting.cginc里的 float3 diff = col.rgb * _LightColor0.rgb * max(0, dot(normal, i.lightDir)) * 2;//*2是我自己加的,为了亮一点 //计算高光,使用Blinn-Phong光照模型 // (ns) //I = k*LightColor*(NH) , H = (L+V)/|L+V| float3 H = normalize(i.lightDir+i.viewDir); float3 specular = _SpecColor * _LightColor0 * pow(max(0, dot(normal, H)), _Shininess); return float4(ambient + diff + specular, col.a); } ENDCG } } }
效果:
当然,因为资源、光照等原因,这样的对比是完全不靠谱的,但我想说的是,vfshader是可控的,这点就足够好了,关键注释都在代码里了。