@
遮罩纹理
什么是遮罩呢?简单来讲,遮罩可以允许我们保护某些区域,使它们免于 某些修改。例如,在之前的实现中,我们都是把高光反射应用到模型表面的所有地方,即所有的像素都使用同样大小的高光强度和高光指数。但有时我们希望模型表面的某些区域的反光强一些,而某些区域弱一些。为了得到更加细腻的效果,我们就可以使用一张遮罩纹理来控制光照。另一种是常见的应用是在制作地形材质时需要混合多张图片,例如表现草地的纹理、表现石子的纹理、表现裸露土地的纹理等,使用遮罩纹理可以控制如何混合这些纹理。
使用遮罩纹理的一般流程是:通过采样得到遮罩纹理的纹素值,然后使用其中某个(或几个)通道的值(texel.r)来与某种表面属性进行相乘,这样当该通道的值为0时,可以保护表面不受该属性影响。总而言之,使用遮罩纹理可以让美术人员更加精准(像素级别)地控制模型表面的各种性质。
1.实践
在本节中,我们将学习如何使用一张高光遮罩纹理,逐像素的控制模型表面的高光反射强度。下图西安设了只包含漫反射、未使用高光反射和使用遮罩的高光反射的对比效果。
我们使用的遮罩纹理如图所示,可以看出,遮罩纹理可以让我们更加精细地控制光照细节,得到更加细腻的效果。
步骤:
(1)我们需要在Properties语义块中声明更多的变量来控制高光反射:
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
_SpecularMask("Specular Mask",2D)="white"{}
_SpecuarScale("Specular Scale",Float)=1.0
_Specular("Specular",Color)=(1,1,1,1)
_Gloss("Gloss",Range(8.0,256))=20
}
上面属性中的_SpecularMask既是我们需要使用的高光反射遮罩纹理,_SpecularScale则是用于控制遮罩影响度的系数。
(2)然后,我们在SubShader语义块中定义了Pass语义块,并在Pass的第一行指明了该Pass的光照模式:
SubShader{
Pass{
Tags{"LightMode"="ForwardBase"}
}
}
(3)我们需要定义和Properties中各个属性类型相匹配的变量:
fixed4 _Color;
sampler2D _MainTex;
float4 _MainTex_ST;
sampler2D _BumpMap;
float _BumpScale;
sampler2D _SpecularMask;
float _SpecularScale;
fixed4 _Specular;
float _Gloss;
我们为主纹理_MainTex、法线纹理_BumpMap和遮罩纹理_SpecularMask定义了它们共同使用的纹理属性_MainTex_ST。这意味着,在材质面板中修改主纹理的平铺系数和偏移系数会同时影响3个纹理采样。使用这种方式可以让我们节省需要存储的纹理坐标数目,如果我们为每一个纹理都使用一个单独的属性变量TextureName_ST,那么随着纹理数目的增加,我们会迅速占满顶点着色器中可以使用的插值寄存器。而很多时候,我们不需要对纹理进行平铺和位移操作,或者很多纹理可以使用同一种平铺和位移操作,此时我们就可以对这些纹理使用同一个变换后的纹理坐标进行采样。
(4)定义顶点着色器的输入和输出结构体
struct a2v{
float4 vertex:POSITION;
float3:NORMAL;
float4 tangent:TANGENT;
float4 texcoord:TEXCOORD0;
};
struct v2f{
float4 pos:SV_POSITION;
float2 uv:TEXCOORD0;
float3 lightDir:TEXCOORD1;
float3 viewDir:TEXCOORD2;
};
(5)在顶点着色器中,我们对光照方向和视角方向进行了坐标空间变换,把它们从模型空间变换到了切线空间中,以便在片元着色器中和法线进行光照运算:
v2f vert(a2v v){
v2f o;
o.pos=mul(UNITY_MATRIX_MVP,v.vertex);
o.uv.xy=v.texcoord.xy*_MainTex_ST.xy+_MainTex_ST.zw;
TANGENT_SPACE_ROTATION;
o.lightDir=mul(rotation,ObjSpaceLightDir(v.vertex)).xyz;
o.viewDir=mul(rotation,ObjSpaceViewDir(v.vertex)).xyz;
return o;
}
(6)使用遮罩纹理的地方是片元着色器,我们使用它来控制模型表面的高光反射强度:
fixed4 frag(v2f i):SV_Target{
fixed3 tangentLightDir=normalize(i.LightDir);
fixed3 tangentViewDir=normalize(i.viewDir);
fixed3 tangentNormal=UnpackNormal(tex2D(_BumpMap,i.uv));
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);
//Get the value
fixed specularMask=tex2D(_SpecularMask,i.uv).r*_SpecularScale;
//Compute specular term with the specular mask
fixed3 specular=_LightColor0.rgb*_Specular.rgb*pow(max(0,dot(tangentNormal,halfDir)),_Gloss)*specularMask;
return fixed(ambient+diffuse+specular,1.0);
}
环境光照和漫反射光照和之前使用过的代码完全一样。在计算高光反射时,我们首先对遮罩纹理_SpecularMask进行采样。由于本书中使用的遮罩纹理的每个纹素的rgb分量其实都是一样的,表明了该点对应的高光反射强度,在这里我们选择使用r分量来计算掩码值。然后我们得到的掩码值和_SpecularScale相乘,一起来控制高光的反射强度。
需要说明的是,我们使用的这张遮罩纹理其实有很多空间被浪费了——它的rgb分量存储的都是同一个值。在实际的游戏制作中,我们往往会充分利用遮罩纹理中的每一个颜色通道来存储不同的表面属性。
(7)最后我们为该UnityShader设置合适的Fallback:
Fallback"Specular"
2.其它遮罩纹理
在真实的游戏制作过程中,遮罩纹理已经不止限于保护某些区域使它们免于修改,而是存储任何我们希望逐像素控制的表面属性。通常,我们会充分利用一张纹理的RGBA四个通道,用于存储不同的属性。例如我们把高光反射强度存储在R通道,把边缘光照的强度存储在G通道,把高光发射的指数部分存储在B通道,最后把自发光强度存储在A通道,