【UnityShader】凹凸映射
纹理的另一种常见应用就是凹凸映射,目的是使用一张纹理来修改模型表面的法线。让模型看起来‘’凹凸不平‘’。
主要有两种方法:一种是高度纹理,一种是法线纹理。这里主要介绍法线纹理。
由于法线方向的分量范围在【-1,1】,但像素的范围是【0,1】,所以要做一个映射:normal = pixel*2-1
在实际制作中,我们一般使用模型顶点的切线空间,法线一般为z轴,切线为x轴,法线和切线的叉积构成的副切线为y轴。
下面介绍在切线空间下计算纹理:在片元着色器中通过纹理采样得到切线空间的法线,在于切线空间下的视角方向、光照方向等进行计算,得到最终的光照结果。
为此,我们需要一个变化矩阵来将模型空间的参数转化为切线空间。
切线空间的转化矩阵容易求得,只需要将切线空间的坐标轴按列排列即可,数学原理这里省略,有兴趣可以自己推导。
Shader "Custom/Chapter7-NormalMapTangentSpace" { Properties{ _Color ("Color Tint", Color) = (1, 1, 1, 1) _MainTex ("Main Tex", 2D) = "white" {} _BumpMap ("Normal Map", 2D) = "bump" {} _BumpScale ("Bump Scale", Float) = 1.0 _Specular ("Specular", Color) = (1, 1, 1, 1) _Gloss ("Gloss", Range(8.0, 256)) = 20 } SubShader{ pass{ Tags{"LightMode" = "ForwardBase"} CGPROGRAM #pragma vertex vert #pragma fragment frag #include "Lighting.cginc" fixed4 _Color; sampler2D _MainTex; sampler2D _BumpMap; float4 _BumpMap_ST; float _BumpScale; fixed4 _Specular; float _Gloss; float4 _MainTex_ST;//纹理类型属性 struct a2v{ float4 vertex:POSITION; float3 normal:NORMAL; float4 tangent:TANGENT; //顶点切线方向填充,四维的原因是要用w决定副切线的方向性 float4 texcoord:TEXCOORD0;//存储纹理坐标 }; struct v2f{ float4 pos:SV_POSITION; float4 uv:TEXCOORD0;//纹理采样用 两个纹理所以用float4 float3 lightDir:TEXCOORD1; float3 viewDir:TEXCOORD2; }; v2f vert(a2v v){ v2f o; o.pos = UnityObjectToClipPos(v.vertex); o.uv.xy = v.texcoord.xy*_MainTex_ST.xy+_MainTex_ST.zw; o.uv.zw = v.texcoord.xy*_BumpMap_ST.xy+_BumpMap_ST.zw; //两张纹理,分别存储纹理坐标 float3 binormal = cross(normalize(v.normal),normalize(v.tangent.xyz))*v.tangent.w;//计算副法线 float3x3 rotation = float3x3(v.tangent.xyz,binormal,v.normal); o.lightDir = mul(rotation,ObjSpaceLightDir(v.vertex)).xyz; o.viewDir = mul(rotation,ObjSpaceViewDir(v.vertex)).xyz; return o; } fixed4 frag(v2f i):SV_Target{ fixed3 tangentLightDir = normalize(i.lightDir); fixed3 tangentViewDir = normalize(i.viewDir); //首先对法线纹理进行采样 fixed4 packedNormal = tex2D(_BumpMap,i.uv.zw); //对像素反映射得到原来的发现方向,由于再切线空间先 保证z为正 fixed3 tangentNormal; //纹理类型不为Normal map时 Unity一般为Normal map 所以下面方式计算的话结果会出差 //tangentNormal.xy = (packedNormal.xy*2-1)*_BumpScale; //tangentNormal.z = sqrt(1.0-saturate(dot(tangentNormal.xy,tangentNormal.xy)));//因为是单位矢量,所以可以通过xy计算z tangentNormal = UnpackNormal(packedNormal); tangentNormal.xy *=_BumpScale; tangentNormal.z = sqrt(1.0-saturate(dot(tangentNormal.xy,tangentNormal.xy))); fixed3 albedo = tex2D(_MainTex,i.uv).rgb*_Color.rgb; fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz*albedo; fixed3 diffuse = _LightColor0.rgb*albedo*max(0,dot(tangentNormal,tangentLightDir)); fixed3 halfDir = normalize(tangentLightDir+tangentViewDir); fixed3 specular = _LightColor0.rgb*_Specular.rgb*pow(max(0,dot(tangentNormal,halfDir)),_Gloss); return fixed4(ambient+diffuse+specular,1.0); } ENDCG } } FallBack "Diffuse" }
因为模型的法线,切线都可以求得,所有副法线也容易就得到了,进而转化矩阵也容易求得。
需要注意的是_BumpMap为法线纹理,bump为默认值,_BumpScale控制凹凸程度。
由于是两个纹理,所以uv声明为了float4.xy存储_MainTex纹理坐标,zw存储_BumpMap纹理坐标。
tangent切线是float4的原因是w分量决定副法线的方向。
两个纹理一般使用的是同一个纹理坐标texcoord。
注意法线纹理类型若是Normal map就需要使用Unity提供的UnpackNormal来映射,因为Unity会根据不同的平台对纹理进行压缩,再通过此函数进行采样。
下面是_BumpScale为-0.8和0.8时的情况: