游戏里经常需要在角色上做描边,这里总结一下平时几种常见的描边做法。
一,两批次法:
优点是简单,效果直接,性价比高。
1. 定点对着法线方向外移,缺点是可以看出顶点之间有断裂
Shader "ly/Outline_2Pass_1" { Properties { _MainTex("Texture", 2D) = "white"{} _Outline("Outline", range(0, 1)) = 0.02 _OutlineColor("Outline Color", Color) = (1,1,1,1) } SubShader { //第一个批次,画描边 Pass { //Cull掉前面的一半,只让描边显示在后面 Cull Front CGPROGRAM #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc" fixed _Outline; fixed4 _OutlineColor; struct v2f { float4 pos : SV_POSITION; float4 color : COLOR; }; v2f vert (appdata_full v) { v2f o; //源顶点位置添加法线方向乘以参数的偏移量 v.vertex.xyz += v.normal * _Outline; //位置从自身坐标系转换到投影空间 //旧版本o.pos = mul(UNITY_MATRIX_MVP,v.vertex); o.pos = UnityObjectToClipPos(v.vertex); //描边颜色 o.color = _OutlineColor; return o; } float4 frag (v2f i) : COLOR { return i.color; //描边 } ENDCG } //第二个批次 Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc" sampler2D _MainTex; half4 _MainTex_ST; struct v2f { float4 pos : SV_POSITION; float2 uv : TEXCOORD0; fixed4 color : COLOR; }; v2f vert(appdata_base v) { v2f o; o.pos = UnityObjectToClipPos(v.vertex); o.uv = v.texcoord; o.color = fixed4(0, 0, 0, 1); return o; } fixed4 frag(v2f i) : SV_Target { fixed4 col = tex2D(_MainTex, i.uv); return col; } ENDCG } } }
2. 得到法线在投影空间上的xy轴,作为偏移方向将顶点外移,得到的结果类似1,也有断裂
3. 顶点的位置作为方向矢量,则不会因为方向差距较大而断裂
Shader "ly/Outline_2Pass_2" { Properties { _MainTex("Texture", 2D) = "white"{} _Outline("Outline", range(0, 1)) = 0.02 _OutlineColor("Outline Color", Color) = (1,1,1,1) } SubShader { //第一个批次,画描边 Pass { //Cull掉前面的一半,只让描边显示在后面 Cull Front CGPROGRAM #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc" fixed _Outline; fixed4 _OutlineColor; struct v2f { float4 pos : SV_POSITION; float4 uv : TEXCOORD0; }; v2f vert (appdata_full v) { v2f o; //位置从自身坐标系转换到投影空间 //旧版本o.pos = mul(UNITY_MATRIX_MVP,v.vertex); o.pos = UnityObjectToClipPos(v.vertex); //方式二,扩张顶点位置 //法线变换到投影空间 //float3 normal = mul((float3x3)UNITY_MATRIX_IT_MV, v.normal); //得到投影空间的偏移 //float2 offset = TransformViewToProjection(normal.xy); ////方式三,把顶点当做方向矢量,在方向矢量的方向偏移 float3 dir = normalize(v.vertex.xyz); dir = mul((float3x3)UNITY_MATRIX_IT_MV, dir); float2 offset = TransformViewToProjection(dir.xy); //有一些情况下,侧边看不到,所以把方式一和二的算法相结合 //float3 dir = normalize(v.vertex.xyz); //float3 dir2 = v.normal; //float D = dot(dir, dir2); //D = (1 + D / _Outline) / (1 + 1 / _Outline); //dir = lerp(dir2, dir, D); //dir = mul((float3x3)UNITY_MATRIX_IT_MV, dir); //float2 offset = TransformViewToProjection(dir.xy); //offset = normalize(offset); //在xy两个方向上偏移顶点的位置 o.pos.xy += offset * o.pos.z * _Outline; return o; } float4 frag (v2f i) : COLOR { return _OutlineColor; //描边 } ENDCG } //第二个批次,略 }
二,边缘光
顶点的视角dir和法线dir点乘,得出偏离度,越靠近边缘,颜色的强度越高。
优点是节约批次。
v2f vert (appdata_full v) { v2f o; //略
//_RimColor边缘光颜色
//_RimPower边缘光强度
float3 viewDir = normalize(ObjSpaceViewDir(v.vertex)); float dotProduct = 1 - dot(normalize(v.normal), viewDir); fixed3 rimCol = smoothstep(1 - _RimPower, 1.0, dotProduct) * _RimColor; o.color = rimCol; //略 return o; }
三,后处理方式来画描边
优点是效果完美,缺点是消耗性能。
摄像机上挂一个脚本,处理后处理的步骤,outlineCamera 为临时摄像机,参数与主摄像机相同,看着同样的Unit层。
临时摄像机渲染到RT上,先画剪影,然后用自定义的描边shader画上去。
using UnityEngine; using UnitySampleAssets.ImageEffects; [RequireComponent(typeof(Camera))] [AddComponentMenu("Image Effects/Other/Post Effect Outline")] class PostEffectOutline : PostEffectsBase { public enum OutLineMethod { eIteration, eScale, } private Camera attachCamera; private Camera outlineCamera; private Shader simpleShader; private Shader postOutlineShader; private Material postOutlineMat; private RenderTexture mTempRT; public Color outlineColor = new Color(0, 1f, 0, 1f);// Color.green; [Range(0, 10)] public int outlineWidth = 1; [Range(1, 9)] public int iterations = 1; public OutLineMethod outlineMethod = OutLineMethod.eIteration; void Awake() { FindShaders(); } void FindShaders() { if (!simpleShader) simpleShader = Shader.Find("ly/DrawSimple"); if (outlineMethod == OutLineMethod.eIteration) { if (!postOutlineShader) postOutlineShader = Shader.Find("ly/PostOutlineIteration"); } else { if (!postOutlineShader) postOutlineShader = Shader.Find("ly/PostOutlineScale"); } } protected override void Start() { base.Start(); attachCamera = GetComponent<Camera>(); if (outlineCamera == null) { outlineCamera = new GameObject().AddComponent<Camera>(); outlineCamera.enabled = false; outlineCamera.transform.parent = attachCamera.transform; outlineCamera.name = "outlineCam"; } postOutlineMat = new Material(postOutlineShader); } public override bool CheckResources() { CheckSupport(false); if (!isSupported) ReportAutoDisable(); return isSupported; } private void OnRenderImage(RenderTexture source, RenderTexture destination) { if (CheckResources() == false) { Graphics.Blit(source, destination); return; } outlineCamera.CopyFrom(attachCamera); outlineCamera.clearFlags = CameraClearFlags.Color; outlineCamera.backgroundColor = Color.black; outlineCamera.cullingMask = 1 << LayerMask.NameToLayer("Unit"); if (mTempRT == null) mTempRT = RenderTexture.GetTemporary(source.width, source.height, source.depth); mTempRT.Create(); outlineCamera.targetTexture = mTempRT; outlineCamera.RenderWithShader(simpleShader, ""); postOutlineMat.SetTexture("_SceneTex", source); postOutlineMat.SetColor("_Color", outlineColor); postOutlineMat.SetInt("_Width", outlineWidth); postOutlineMat.SetInt("_Iterations", iterations); //画描边混合材质 Graphics.Blit(mTempRT, destination, postOutlineMat); mTempRT.Release(); } }
先用简单的shader画出剪影
Shader "ly/DrawSimple" { FallBack OFF }
然后就是这个自定义的描边shader画的过程。
第一种是类似高斯模糊的方式来迭代,迭代次数越多则越细腻。
// ly 类似高斯模糊方式迭代循环处理描边 Shader "ly/PostOutlineIteration" { Properties { _MainTex("Main Texture", 2D) = "black"{} //画完物体面积后的纹理 _SceneTex("Scene Texture", 2D) = "black"{} //原场景纹理 _Color("Outline Color", Color) = (0,1,0,0.8) //描边颜色 _Width("Outline Width", int) = 1 //描边宽度 _Iterations("Iterations", int) = 1 //描边迭代次数(越多越平滑,消耗越高) } SubShader { Pass { CGPROGRAM sampler2D _MainTex; float2 _MainTex_TexelSize; sampler2D _SceneTex; fixed4 _Color; float _Width; int _Iterations; #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc" struct v2f { float4 pos : SV_POSITION; float2 uv : TEXCOORD0; }; v2f vert(appdata_base v) { v2f o; o.pos = UnityObjectToClipPos(v.vertex); o.uv = v.texcoord.xy; return o; } half4 frag(v2f i) : COLOR { //迭代次数为奇数,保证对称 int iterations = _Iterations * 2 + 1; float ColorIntensityInRadius; float Tx_x = _MainTex_TexelSize.x * _Width; float Tx_y = _MainTex_TexelSize.y * _Width; //计算是否大于0,则此像素属于外边的范围内 for (int k=0; k<iterations; k+=1) { for (int j=0; j<iterations; j+=1) { ColorIntensityInRadius += tex2D(_MainTex, i.uv.xy + float2((k - iterations / 2) * Tx_x, (j - iterations / 2) * Tx_y)); } } //如果有颜色,或者不在外边的范围内,则渲染原场景。否则,在外边内,渲染描边。 if (tex2D(_MainTex, i.uv.xy).r > 0 || ColorIntensityInRadius == 0) return tex2D(_SceneTex, i.uv); else return _Color.a * _Color + (1 - _Color.a)*tex2D(_SceneTex, i.uv); } ENDCG } } }
第二种方法简单些,直接把剪影的部分uv扩大,再把原图叠上去。
// ly 扩张剪影uv来填充描边 Shader "ly/PostOutlineScale" { Properties { _MainTex("Main Texture", 2D) = "black"{} //画完物体面积后的纹理 _SceneTex("Scene Texture", 2D) = "black"{} //原场景纹理 _Color("Outline Color", Color) = (0,1,0,1) //描边颜色 _Width("Outline Width", float) = 1 //描边宽度 } SubShader { Pass { CGPROGRAM sampler2D _MainTex; sampler2D _SceneTex; float2 _SceneTex_TexelSize; fixed4 _Color; float _Width; #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc" struct v2f { float4 pos : SV_POSITION; half2 uv[2] : TEXCOORD0; half2 uv2[4] : TEXCOORD2; }; v2f vert(appdata_base v) { v2f o; o.pos = UnityObjectToClipPos(v.vertex); o.uv[0] = v.texcoord.xy; o.uv[1] = v.texcoord.xy; half2 offs = _SceneTex_TexelSize.xy * _Width; o.uv2[0].x = v.texcoord.x - offs.x; o.uv2[0].y = v.texcoord.y - offs.y; o.uv2[1].x = v.texcoord.x + offs.x; o.uv2[1].y = v.texcoord.y - offs.y; o.uv2[2].x = v.texcoord.x + offs.x; o.uv2[2].y = v.texcoord.y + offs.y; o.uv2[3].x = v.texcoord.x - offs.x; o.uv2[3].y = v.texcoord.y + offs.y; if (_SceneTex_TexelSize.y < 0) { o.uv[1].y = 1 - o.uv[1].y; o.uv2[0].y = 1 - o.uv2[0].y; o.uv2[1].y = 1 - o.uv2[1].y; o.uv2[2].y = 1 - o.uv2[2].y; o.uv2[3].y = 1 - o.uv2[3].y; } return o; } half4 frag(v2f i) : COLOR { fixed4 stencil = tex2D(_MainTex, i.uv[1]); // 有剪影的部分,显示原图 if (any(stencil.rgb)) { fixed4 framebuffer = tex2D(_SceneTex, i.uv[0]); return framebuffer; } // 没有剪影的部分,先把剪影扩张,扩张出颜色的部分用剪影,没有颜色的用原图 else { fixed4 color1 = tex2D(_MainTex, i.uv2[0]); fixed4 color2 = tex2D(_MainTex, i.uv2[1]); fixed4 color3 = tex2D(_MainTex, i.uv2[2]); fixed4 color4 = tex2D(_MainTex, i.uv2[3]); fixed4 color; color.rgb = max(color1.rgb, color2.rgb); color.rgb = max(color.rgb, color3.rgb); color.rgb = max(color.rgb, color4.rgb); if (any(color.rgb)) { return _Color; //color.a = (color1.a + color2.a + color3.a + color4.a) * 0.25; //return color; } else { fixed4 framebuffer = tex2D(_SceneTex, i.uv[0]); return framebuffer; } } } ENDCG } } SubShader { Pass { SetTexture[_MainTex]{} } } Fallback Off }