Unity描边所使用的方式何其多,每种都有其两面性,有些效果不太好,但实现简单效率也高;有些效果棒棒哒,但耗性能。我们需要在了解每种方式的厉害后,再根据项目的情况来择优用之方为上上策。本篇就先从比较简单说起:利用模板测试的方式来实现描边效果。
本篇必备技能:模板测试的基础知识
一、原理
- 先用一个pass把模板缓冲区的值刷至特定值(比如 2),并在该pass里进行正常的模型渲染(纹理采样、高光、漫反等等)。
- 再用一个pass进行描边。首先需要把模型的顶点延法线方向向外延伸一定值,也就是描边的厚度/宽度(众多描边实现方法中,超过一大半都是用来膨胀模型的方式,所以膨胀模型本身所带来的弊端也会体现在这些实现方法中,比如对于凹多边形的mesh,由于它们的法线方向可能是相向的,就容易造成穿模;还有像立方体这种低模的,描边就会断掉,不连续;如果你的模型有镂空的地方,那也会被描上边,尽管你不愿意。所以描边厚度不要太大,以免在某些模型中加剧上述问题)。然后模板的Ref值设置成第1步的值,comp一般取NotEqual,Pass时保持模板缓冲区的值。这样得到的效果就是只有多余膨胀的部分会被渲染,渲染的这部分就是我们看到的描边了。此时在我们的片元着色器里输出描边颜色就可以了。
- 需要注意的是我们需要保证第1步的pass先执行,第2步的pass后执行,所有需要控制一下两个pass的 Queue值,比如本篇案第1个pass的Queue = “Gemetry + 1”,第2个pass的Queue = “Gemetry + 2”
二、案例
以下代码只是为了验证上述原理,不能直接用于项目中
Shader "Unlit/StencilOutline"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
_OutlineColor("outline Color",Color) = (0,0,0,0)
_OutlineWidth("outline Width",Range(0,0.05)) = 0.01
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 100
Pass
{
Tags{"Queue" = "Gemetry + 1"}
Stencil
{
Ref 1
Comp Always
Pass Replace
}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
// make fog work
#pragma multi_compile_fog
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f
{
float2 uv : TEXCOORD0;
UNITY_FOG_COORDS(1)
float4 vertex : SV_POSITION;
};
sampler2D _MainTex;
float4 _MainTex_ST;
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
UNITY_TRANSFER_FOG(o,o.vertex);
return o;
}
fixed4 frag (v2f i) : SV_Target
{
// sample the texture
fixed4 col = tex2D(_MainTex, i.uv);
// apply fog
UNITY_APPLY_FOG(i.fogCoord, col);
return col;
}
ENDCG
}
pass
{
Tags{"Queue" = "Gemetry + 2"}
Stencil
{
Ref 1
Comp NotEqual
Pass keep
}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
// make fog work
#pragma multi_compile_fog
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float3 normal : NORMAL;
};
struct v2f
{
float4 vertex : SV_POSITION;
};
fixed4 _OutlineColor;
float _OutlineWidth;
v2f vert (appdata v)
{
v2f o;
//在模型空间里直接膨胀模型,也可以在别的空间里做
//需要注意法线的变换,还要注意在别的空间里膨胀的话,顶点变换到裁剪空间时不是MVP了
v.vertex.xyz += v.normal * _OutlineWidth;
o.vertex = UnityObjectToClipPos(v.vertex);
在裁剪空间里膨胀,可以解决描边近大远小的问题
先把法线从模型空间转到相机空间
这里直接使用unity给我们提供的宏,等价于 vNormal = normalize(mul((float3x3)UNITY_MATRIX_IT_MV, v.normal))
为什么是(float3x3)UNITY_MATRIX_IT_MV,而不是 (float3x3)UNITY_MATRIX_MV,冯乐乐美女的4.7节是有这个解释的
// fixed3 vNormal = COMPUTE_VIEW_NORMAL;
再把 vNormal 的xy转化到裁剪空间里去,z不要了
// fixed2 pNormal = TransformViewToProjection(vNormal.xy);
开始膨胀模型
// o.vertex.xy += pNormal * _OutlineWidth;
return o;
}
fixed4 frag (v2f i) : SV_Target
{
return _OutlineColor;
}
ENDCG
}
}
}
三、优缺点
优点
正如前文所说,实现简单,效率也高,只用未开启混合的两个pass就能做到。
缺点
除了第2个步骤里提到的,还有一些
1、同样厚度的描边存在近大远小的问题,也有人说这不算缺点,因为透视相机的原因,属于正常现象;如果需要解决也很简单,直接在把膨胀步骤放到裁剪空间里做,只是要多些把法线的转换到相机空间步骤。(我觉得没必要,真没多大区别)
2、多个单位有重叠时,描边断了,如下图
这是因为当有重叠时,重叠部分的模板测试未通过(我们设置的是不等于时通过,而此时是等于的关系,所以未通过),所以描边没有被渲染出来。