Unity Shader入门教程(八):基础纹理之凹凸映射
发表于2018-06-06
凹凸映射的目的是使用一张纹理来修改模型表面的法线,以便为模型提供更多的细节。
一、2种法线纹理
法线纹理中存储的就是表面的法线方向,由于法线的方向的分量范围在[-1,1],而像素的分量范围为[0,1],因此我们需要做一个映射:
pixel =0.5*normal + 0.5
模型空间法线纹理:对于模型自带的表面法线,修改后存储在此纹理中。
切线空间法线纹理:对于模型的每个定点都有一个属于自己的切线空间,这个切线空间的原点就是该定点的本身,而z轴是定点的法线方向,x轴是切线方向,而y轴有法线和切线的叉积得到,也被称为是副切线或副法线。
Obj-Space normal map 与 Tagent-Space normal map
左边的模型空间法线之所以是五颜六色的,是因为所有法线所在的坐标空间是同一个坐标空间,即模型空间,而每个点存储的法线的方向是各异的。
右边的切线空间法线纹理看起来几乎全部是浅蓝色的。这是因为,每个法线方向所在的坐标空间是不一样的,即是表面各点各自的切线空间。【这种法线纹理其实就是存储了每个点在各自的切线空间中的法线扰动方向!】
二、切线空间下计算光照模型
基本思路:
在顶点着色器中,首先通过将顶点vertex作为实参传入给ObjSpaceLightDir与ObjSpaceViewDir两个函数,得到模型空间中的光照方向、视角方向,然后通过切线到模型空间的转换矩阵的逆转置矩阵得到光照方向、视角方向在切线空间下的表示,最后通过v2f结构体将这两个方向传递给片元着色器。在片元着色器中通过纹理采样得到切线空间下的法线,然后再与切线空间下的视角方向、光照方向等进行计算。
上代码:
Shader "Custom/Edu/NormalMapTangentSpace" { Properties { _MainTex("MainTex",2D) = "white"{} _BumpMap("BumpMap",2D) = "white"{} _BumpScale("BumpScale",Range(-2.0,2.0)) = 1.0 _Specular("Specular",Color) = (1,1,1,1) _Diffuse("Diffuse",Color) = (1,1,1,1) _Gloss("Gloss",Range(8,256)) = 40 _Color("ColorTint",Color) = (1,1,1,1) } SubShader { Pass { Tags{"LightMode" = "ForwardBase"} CGPROGRAM #pragma vertex vert #pragma fragment frag #include "Lighting.cginc" #include "UnityCG.cginc" sampler2D _MainTex; float4 _MainTex_ST; sampler2D _BumpMap; float4 _BumpMap_ST; float _BumpScale; fixed4 _Specular; fixed4 _Diffuse; fixed4 _Color; float _Gloss; struct a2v { float4 vertex:POSITION; float4 texcoord:TEXCOORD0; float3 normal:NORMAL; float4 tangent:TANGENT; }; struct v2f { float4 pos:SV_POSITION; float4 uv:TEXCOORD0; float3 lightDir:TEXCOORD1; float3 viewDir:TEXCOORD2; }; v2f vert(a2v v) { v2f o; o.pos = mul(UNITY_MATRIX_MVP,v.vertex); o.uv.xy = TRANSFORM_TEX(v.texcoord,_MainTex); o.uv.zw = TRANSFORM_TEX(v.texcoord,_BumpMap); //与法线与切线都垂直的方向有两个,而w分量决定我们使用哪一个 float3 bionormal = cross(normalize(v.normal),normalize(v.tangent)) * v.tangent.w; //切线空间:x轴为切线方向,z轴向外为法线方向,y轴为副法线方向 //切线空间中的z轴在模型空间中的表示为v.normal //切线空间中的x轴在模型空间中的表示为v.tangent //bionormal通过对前面两个向量进行叉积运算得到 //则切线空间到模型空间的的变换矩阵M1为v.tangent、bionormal、v.normal按列排列组成矩阵 //又模型空间到切线空间的变换矩阵M2仅存在平移和旋转,所以为正交矩阵,正交矩阵的逆矩阵等于其转置矩阵 //所以 M2 = M1 的转置 //利用特性:正交矩阵 = 自身的逆矩阵的转置 float3x3 rotation = float3x3(v.tangent.xyz,bionormal,v.normal); //或者使用内置的宏: TANGENT_SPACE_ROTATION; 来得到rotation矩阵 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); fixed3 tangentNormal ; tangentNormal = UnpackNormal(packedNormal); tangentNormal.xy *= _BumpScale; //这里我们要保证tangentNormal在经过上一步处理后仍然是一个单位向量 tangentNormal.z = sqrt(1 - dot(tangentNormal.xy,tangentNormal.xy)); fixed3 albedo = tex2D(_MainTex,i.uv.xy).rgb * _Color.rgb; fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo; fixed3 diffuse = _LightColor0.rgb * albedo * _Diffuse.rgb * saturate(dot(tangentNormal,tangentLightDir)); fixed3 halfDir = normalize(tangentLightDir + tangentViewDir); fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(saturate(dot(tangentNormal,halfDir)),_Gloss); return fixed4(ambient + diffuse + specular,1.0); } ENDCG } } FallBack "Diffuse" }
三、世界空间下计算光照模型
基本思路:
由于我们只能在片元着色器中获得bumpMap的纹素信息,所以我们需要在片元着色器中使用切线空间到世界空间的转换矩阵。首先在顶点着色器中获得切线空间各个坐标轴在世界空间中的表示,副切线副通过转换好的worldNormal与worldTangent的叉积获得,注意方向靠v.tangent的w分量来确定。在v2f结构体声明了TtoW0、TtoW1、TtoW2三个纹理寄存器,用来分别将存放转换矩阵的每一行。由于需要在片元着色器用到worldPos来计算worldViewDir以及worldLightDir,所以需要向片元着色器传递该信息,传递方式为将该变量的三个分量分别存放到TtoW0、TtoW1、TtoW2的w分量中。
上代码:
Shader "Custom/Edu/NormalMapWorldSpace" { Properties { _Color ("Color", Color) = (1,1,1,1) _MainTex ("Albedo (RGB)", 2D) = "white" {} _BumpMap ("BumpMap",2D) = "white"{} _BumpScale("BumpScale",Range(-2.0,2.0)) = 1.0 _Specular("Specular",Color)=(1,1,1,1) _Diffuse("Diffuse",Color)=(1,1,1,1) _Gloss("Gloss",Range(8,256)) = 40 } SubShader { Pass { Tags{"LightMode" = "ForwardBase"} CGPROGRAM #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc" #include "Lighting.cginc" fixed4 _Color; sampler2D _MainTex; float4 _MainTex_ST; sampler2D _BumpMap; float4 _BumpMap_ST; float _BumpScale; fixed4 _Specular; fixed4 _Diffuse; float _Gloss; struct a2v { float4 vertex:POSITION; float3 normal:NORMAL; float4 texcoord:TEXCOORD0; float4 tangent:TANGENT; }; struct v2f { float4 pos:SV_POSITION; float4 TtoW0:TEXCOORD0; float4 TtoW1:TEXCOORD1; float4 TtoW2:TEXCOORD2; float4 uv:TEXCOORD3; }; v2f vert(a2v v) { v2f o; o.pos = mul(UNITY_MATRIX_MVP,v.vertex); //注意运算的结果要取xyz分量 float3 worldPos = mul(_Object2World,v.vertex).xyz; float3 worldNormal = mul(v.normal,(float3x3)_World2Object); //这里要怎样计算?有两种方式 float3 worldTangent = UnityObjectToWorldDir(v.tangent); //float3 worldTangent = mul((float3x3)_Object2World,v.tagent); //这里要怎样计算? //叉积求得 float3 worldBionormal = cross(worldNormal,worldTangent) * v.tangent.w; //我们计算了世界空间下顶点切线、副切线和法线的表示,并把它们按列摆放得到从切线空间到世界空间的变换矩阵 //把该矩阵的每一行分别存放在TtoW0、TtoW1、TtoW2中 //最后把世界空间下顶点位置x、y、z分量分别存储在这些变量的w分量中 o.TtoW0 = float4(worldTangent.x,worldBionormal.x,worldNormal.x,worldPos.x); o.TtoW1 = float4(worldTangent.y,worldBionormal.y,worldNormal.y,worldPos.y); o.TtoW2 = float4(worldTangent.z,worldBionormal.z,worldNormal.z,worldPos.z); o.uv.xy = TRANSFORM_TEX(v.texcoord,_MainTex); o.uv.zw = TRANSFORM_TEX(v.texcoord,_BumpMap); //o.uv.xy = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw; //o.uv.zw = v.texcoord.xy * _BumpMap_ST.xy + _BumpMap_ST.zw; return o; } //为什么老是忘记设置SV_Target这个语义? fixed4 frag(v2f i):SV_Target { //i.uv.xy fixed3 albedo = tex2D(_MainTex,i.uv).rgb * _Color.rgb; fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.rgb * albedo; float3 worldPos = float3(i.TtoW0.w,i.TtoW1.w,i.TtoW2.w); float3 worldViewDir =normalize( UnityWorldSpaceViewDir(worldPos) ); float3 worldLightDir =normalize( UnityWorldSpaceLightDir(worldPos) ); //不需要这个值了,所有法线信息都是用bumpMap中的!!!包括漫反射的计算! float3 worldNormal = normalize( float3(i.TtoW0.z,i.TtoW1.z,i.TtoW2.z) ); fixed3 bump = UnpackNormal(tex2D(_BumpMap,i.uv.zw)); //倘若_BumpScale为0,那么bump=float3(0,0,1)就相当于入射表面没有任何扰动 bump.xy *= _BumpScale; //需要保证经过处理的法线仍然是归一化的单位向量 //dot计算时,对应的分量相乘然后相加,dot(bump.xy,bump.xy)=x*x + y*y bump.z = sqrt(1.0- saturate( dot(bump.xy,bump.xy) )); //将法线从切线空间转换到世界空间,这里是在模拟bump左乘【切线空间到世界空间的转换矩阵】 //我潜意识中两种种错误的写法 //bump = normalize(float3(bump.x * i.TtoW0.xyz,bump.y*i.TtoW1.xyz,bump.z*i.TtoW2.xyz)); //bump = normalize(float3(i.TtoW0.xyz * bump,i.TtoW1.xyz * bump, //正确写法,原来,单个向量默认相当于列向量 bump = normalize(half3(dot(i.TtoW0.xyz,bump),dot(i.TtoW1.xyz,bump),dot(i.TtoW2.xyz,bump))); //计算漫发射时同样适用BumpMap中的法线信息! fixed3 diffuse = _LightColor0.rgb * albedo* saturate(dot(bump,worldLightDir)); float3 halfDir = normalize(worldViewDir + worldLightDir); //一定千万要注意,前面废了好大的劲,就是为了在这一步计算高光时能用上!!! fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(saturate(dot(bump,halfDir)),_Gloss); return fixed4(ambient + diffuse + specular, 1.0); } ENDCG } } FallBack "Diffuse" }
四、效果图:
上面一行使用的切线空间计算,下面一行使用的世界空间计算。
每一列都是用了相同的BumpScale,从左到右分别是-0.8、0、0.8。
这两种计算方式看起来没有任何差别。