混合模式
我们的着色器可以用来创建无光不透明的材料。可以改变颜色的alpha值,通常表示透明度,但目前没有效果。我们也可以将渲染队列设置为Transparent,但这只改变对象被绘制的顺序,对解决没有什么影响。
我们不需要编写一个单独的shader来支持透明的材质。通过一些工作,我们的Unlit shader可以同时支持不透明和透明渲染。
不透明和透明渲染之间的主要区别是,我们是要替换之前绘制的结果,还是与之前的结果结合以产生透明的效果。我们可以通过设置源颜色和目标颜色的混合模式来控制。这里source指的是现在绘制的内容,destination指的是之前绘制的内容以及结果的最终位置。为此添加两个着色器属性:_SrcBlend和_DstBlend。它们是混合模式的枚举,但我们可以使用的最佳类型是Float,默认情况下为_SrcBlend设置为1,_DstBlend设置为0。
Properties {
_BaseColor("Color", Color) = (1.0, 1.0, 1.0, 1.0)
_SrcBlend ("Src Blend", Float) = 1
_DstBlend ("Dst Blend", Float) = 0
}
为了便于编辑,我们可以添加Enum关键字到属性中,并使用完全限定的UnityEngine.Rendering.BlendMode枚举类型作为参数。
[Enum(UnityEngine.Rendering.BlendMode)] _SrcBlend ("Src Blend", Float) = 1
[Enum(UnityEngine.Rendering.BlendMode)] _DstBlend ("Dst Blend", Float) = 0
现在所显示的结果就是我们设置的默认值,源颜色的混合模式是1,目标颜色的混合模式是0,就是说源颜色会将目标颜色完全覆盖。
标准的透明模式源颜色的混合模式是设为SrcAlpha,目标颜色的混合模式是OneMinusSrcAlpha,颜色的计算公式是
。
混合模式的定义是在Pass块中实现,语法是Blend关键字后面跟着两个混合模式。
Pass {
Blend [_SrcBlend] [_DstBlend]
HLSLPROGRAM
…
ENDHLSL
}
透明渲染通常不会写入深度缓冲区,因为这没有多大意义,甚至可能产生不想要的结果。我们可以通过ZWrite语句控制写入深度信息。跟混合模式一样,我们再定义一个_ZWrite字段来控制深度写入功能。
Blend [_SrcBlend] [_DstBlend]
ZWrite [_ZWrite]
为了便于编辑,我们来设计一个on-off开关来控制写入深度信息的设置。
[Enum(UnityEngine.Rendering.BlendMode)] _SrcBlend ("Src Blend", Float) = 1
[Enum(UnityEngine.Rendering.BlendMode)] _DstBlend ("Dst Blend", Float) = 0
[Enum(Off, 0, On, 1)] _ZWrite ("Z Write", Float) = 1
纹理贴图
在第三章中,我们曾经使用了一张灰度图来创建非均匀半透明材质,现在来让我们的unlit shader也支持这个功能吧。
首先我们在Properties块中添加一个_BaseMap纹理属性,贴图的类型是2D纹理,同时我们给它设置默认值,采用的是unity自带的白色纹理。
_BaseMap("Texture", 2D) = "white" {}
_BaseColor("Color", Color) = (1.0, 1.0, 1.0, 1.0)
接下来我们需要把纹理上传到GPU内存,这由unity来为我们完成,不过shader需要有相应纹理的句柄,这个我们可以像定义字段那样去定义它,这里我们使用TEXTURE2D这个宏来定义,把字段名像参数一样传递给它即可。另外,我们还需要给纹理贴图定义一个采样状态来控制它的采样模式,这取决于它的包装和过滤模式。这通过SAMPLE宏来完成,就像TEXTURE2D一样,不过需要在名字前面加个前缀sample。
纹理跟采样器状态是shader的资源,并不可以由实例提供,所以我们需要将它们定义为全局变量,把它们定义在UnlitPass.hlsl文件中。
TEXTURE2D(_BaseMap);
SAMPLER(sampler_BaseMap);
UNITY_INSTANCING_BUFFER_START(UnityPerMaterial)
UNITY_DEFINE_INSTANCED_PROP(float4, _BaseColor)
UNITY_INSTANCING_BUFFER_END(UnityPerMaterial)
此外,unity还提供了一个float4变量来控制纹理的平铺和偏移,这个变量跟纹理属性同名,不过需要加_ST后缀。这个变量可以放到UnityPerMaterial缓冲区中,这样可以按实例设置。
UNITY_INSTANCING_BUFFER_START(UnityPerMaterial)
UNITY_DEFINE_INSTANCED_PROP(float4, _BaseMap_ST)
UNITY_DEFINE_INSTANCED_PROP(float4, _BaseColor)
UNITY_INSTANCING_BUFFER_END(UnityPerMaterial)
要对纹理进行采样我们需要知道纹理坐标,这也是顶点自带的一个属性,unity的模型顶点最多可以设置四组纹理坐标,这里我们只需要第一组纹理坐标,让我们在Attributes结构体中添加一个新的字段baseUV,后面需要加一个语义:TEXCOORD0,表示读的是第一组纹理坐标。
struct Attributes {
float3 positionOS : POSITION;
float2 baseUV : TEXCOORD0;
UNITY_VERTEX_INPUT_INSTANCE_ID
};
接着我们需要把纹理坐标传递给片元处理函数,因为最终是由它来完成纹理采样的。在Varyings结构体中,我们新添加一个字段baseUV,后面给它添加语义VAR_BASE_UV。
struct Varyings {
float4 positionCS : SV_POSITION;
float2 baseUV : VAR_BASE_UV;
UNITY_VERTEX_INPUT_INSTANCE_ID
};
当我们在UnlitPassVertex中对顶点坐标进行变换运算的时候,我们也可以对纹理坐标进行变换运算,可以对它进行缩放和平移,这些数据都存储在_BaseMap_ST中,XY保存的是x,y方向的缩放比例,ZW保存的是x,y方向的偏移量。
Varyings UnlitPassVertex (Attributes input) {
…
float4 baseST = UNITY_ACCESS_INSTANCED_PROP(UnityPerMaterial, _BaseMap_ST);
output.baseUV = input.baseUV * baseST.xy + baseST.zw;
return output;
}
最后我们在UnlitPassFragment函数中对纹理进行采样,我们采用的是SAMPLE_TEXTURE2D宏,然后将纹理,采样器以及纹理坐标作为参数传给SAMPLE_TEXTURE2D。最后返回的结果是纹理采样的值与_BaseColor的乘积。
float4 UnlitPassFragment (Varyings input) : SV_TARGET {
UNITY_SETUP_INSTANCE_ID(input);
float4 baseMap = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, input.baseUV);
float4 baseColor = UNITY_ACCESS_INSTANCED_PROP(UnityPerMaterial, _BaseColor);
return baseMap * baseColor;
}
alpha裁剪
透视表面还有一种方法是在模型表面切孔,shader可以通过丢弃某些片元来做到这一点。但是这样做会产生硬边,而不像我们现在看到的平滑过渡,这种技术叫做alpha裁剪。它的原理是设置一个裁剪阈值。alpha值低于此阈值的片段将被丢弃,而所有其它片段将保留。
我们新加一个shader属性_Cutoff,数据类型是Range(0.0,1.0),因为alpha值范围就在0和1之间,默认值我们设为0.5.
_BaseColor("Color", Color) = (1.0, 1.0, 1.0, 1.0)
_Cutoff ("Alpha Cutoff", Range(0.0, 1.0)) = 0.5
我们同样把这个值添加到UnityPerMaterial结构体中,这样可以按实例设置。
UNITY_DEFINE_INSTANCED_PROP(float4, _BaseColor)
UNITY_DEFINE_INSTANCED_PROP(float, _Cutoff)
丢弃片元使用的是clip函数,当我们传递的参数小于等于0时就会丢弃该片元。
float4 baseMap = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, input.baseUV);
float4 baseColor = UNITY_ACCESS_INSTANCED_PROP(UnityPerMaterial, _BaseColor);
float4 base = baseMap * baseColor;
clip(base.a - UNITY_ACCESS_INSTANCED_PROP(UnityPerMaterial, _Cutoff));
return base;
一个材质经常会用到透明度混合或者alpha裁剪,当时一般不会同时使用。一个典型的裁剪材料除了被丢弃的片元外是完全不透明的,并且会写入深度缓存区。它使用AlphaTest队列,这意味着它是在所有完全不透明物体被渲染完之后才会被渲染。这样做是因为丢弃片元使某些GPU优化无法实现,因为不再假定三角形完全覆盖了它们后面的内容。而首先绘制完全不透明物体,那么alpha裁剪的对象被覆盖的部分则无需再处理。
但是裁剪操作我们必须要保证是在需要时才执行,为此我们需要添加一个切换开关,使用关键字Toggle创建一个属性_CLIPPING,shader属性的名字是_Clipping,在这里没有太大意义。
_Cutoff ("Alpha Cutoff", Range(0.0, 1.0)) = 0.5
[Toggle(_CLIPPING)] _Clipping ("Alpha Clipping", Float) = 0
开启这个开关的时候,_CLIPPING关键字将会被添加到材质的活性关键字列表中,关闭则会删除。然而这个关键字单独是没有任何作用的,我们需要告诉unity根据是否定义了这个关键字来编译着色器的不同版本。为此,我们将#pragma shader_feature _CLIPPING添加到其Pass块的指令中。
#pragma shader_feature _CLIPPING
#pragma multi_compile_instancing
现在,unity就会将我们的shader编译成两个版本,一个定义了_CLIPPING一个没有定义。它将会产生一个或两个变体,这取决于我们怎么定义我们的材质。
#if defined(_CLIPPING)
clip(base.a - UNITY_ACCESS_INSTANCED_PROP(UnityPerMaterial, _Cutoff));
#endif
因为cutoff是UnityPerMaterial缓存的一部分,它可以按实例设置。因此,让我们将该功能添加到PerObjectMaterialProperties中。 除了需要在属性块上调用SetFloat而不是SetColor之外,它的作用与颜色相同。
static int baseColorId = Shader.PropertyToID("_BaseColor");
static int cutoffId = Shader.PropertyToID("_Cutoff");
static MaterialPropertyBlock block;
[SerializeField]
Color baseColor = Color.white;
[SerializeField, Range(0f, 1f)]
float cutoff = 0.5f;
…
void OnValidate () {
…
block.SetColor(baseColorId, baseColor);
block.SetFloat(cutoffId, cutoff);
GetComponent<Renderer>().SetPropertyBlock(block);
}