基于Shader实现的UGUI描边解决方案

前言

大扎好,我系狗猥。当大家都以为我鸽了的时候,我又出现了,这也是一种鸽。创业两年失败后归来,今天想给大家分享一个我最近研究出来的好康的,比游戏还刺激,还可以教你登dua郎喔(大误

这次给大家带来的是基于Shader实现的UGUI描边,也支持对Text组件使用。

首先请大家看看最终效果(上面放了一个Image和一个Text):

(8102年了怎么还在舰

接下来,我会向大家介绍思路和具体实现过程。如果你想直接代到项目里使用,请自行跳转到本文最后,那里有完整的C#和Shader代码。

本方案在Unity 2018.4.0f1下测试通过。

本文参考了http://blog.sina.com.cn/s/blog_6ad33d350102xb7v.html

转载请注明出处:https://www.cnblogs.com/GuyaWeiren/p/9665106.html


为什么要这么做

就我参加工作这些年接触到的UI美术来看,他们都挺喜欢用描边效果。诚然这个效果可以让文字更加突出,看着也挺不错。对美术来说做描边简单的一比,PS里加个图层样式就搞定,但是对我们程序来说就是一件很痛苦的事。

UGUI自带的Outline组件用过的同学都知道,本质上是把元素复制四份,然后做一些偏移绘制出来。但是把偏移量放大,瞬间就穿帮了。如果美术要求做一个稍微宽一点的描边,这个组件是无法实现的。

然后有先辈提出按照Outline实现方式,增加复制份数的方法。请参考https://github.com/n-yoda/unity-vertex-effects。确实非常漂亮。但是这个做法有一个非常严重的问题:数量如此大的顶点数,对性能会有影响。我们知道每个字符是由两个三角形构成,总共6个顶点。如果文字数量大,再加上一个复制N份的脚本,顶点数会分分钟炸掉。

以复制8次为例,一段200字的文本在进行处理后会生成200 * 6 * (8+1) = 10800 个顶点,多么可怕。并且,Unity5.2以前的版本要求,每一个Canvas下至多只能有65535个顶点,超过就会报错。

TextMeshPro能做很多漂亮的效果。但是它的做法类似于图字,要提供所有会出现的字符。对于字符很少的英语环境,这没有问题,但对于中文环境,把所有字符弄进去是不现实的。还有最关键的是,它是作用于TextMesh组件,而不是UGUI的Text

于是乎,使用Shader变成了最优解。

概括讲,这个实现就是在C#代码中对UI顶点根据描边宽度进行外扩,然后在Shader的像素着色器中对像素的一周以描边宽度为半径采N个样,最后将颜色叠加起来。通常需要描边的元素尺寸都不大,故多重采样带来的性能影响几乎是可以忽略的。


在Shader中实现描边

创建一个OutlineEx.shader。对于描边,我们需要两个参数:描边的颜色和描边的宽度。所以首先将这两个参数添加到Shader的属性中:

  1. _OutlineColor("Outline Color", Color) = (1, 1, 1, 1)
  2. _OutlineWidth("Outline Width", Int) = 1

采样坐标用圆的参数方程计算。在Shader中进行三角函数运算比较吃性能,并且这里采样的角度是固定的,所以我们可以把坐标直接写死。在Shader中添加采样的函数。因为最终进行颜色混合的时候只需要用到alpha值,所以函数不返回rgb:

  1. fixed SampleAlpha(int pIndex, v2f IN)
  2. {
  3. const fixed sinArray[12] = { 0, 0.5, 0.866, 1, 0.866, 0.5, 0, -0.5, -0.866, -1, -0.866, -0.5 };
  4. const fixed cosArray[12] = { 1, 0.866, 0.5, 0, -0.5, -0.866, -1, -0.866, -0.5, 0, 0.5, 0.866 };
  5. float2 pos = IN.texcoord + _MainTex_TexelSize.xy * float2(cosArray[pIndex], sinArray[pIndex]) * _OutlineWidth;
  6. return (tex2D(_MainTex, pos) + _TextureSampleAdd).w * _OutlineColor.w;
  7. }

然后在像素着色器中增加对方法的调用。

  1. fixed4 frag(v2f IN) : SV_Target
  2. {
  3. fixed4 color = (tex2D(_MainTex, IN.texcoord) + _TextureSampleAdd) * IN.color;
  4. half4 val = half4(_OutlineColor.x, _OutlineColor.y, _OutlineColor.z, 0);
  5. // 注意:这里为了简化代码用了循环
  6. // 尽量不要在Shader中使用循环,多复制几次代码都行
  7. for (int i = 0; i < 12; i++)
  8. {
  9. val.w += SampleAlpha(i, IN);
  10. }
  11. color = (val * (1.0 - color.a)) + (color * color.a);
  12. return color;
  13. }

接下来,在Unity中新建一个材质球,把Shader赋上去,挂在一个UGUI组件上,然后调整描边颜色和宽度,可以看到效果:

可以看到描边已经出现了,但是超出图片范围的部分被裁减掉了。所以接下来,我们需要对图片的区域进行调整,保证描边的部分也被包含在区域内。


在C#层进行区域扩展

要扩展区域,就得修改顶点。Unity提供了BaseMeshEffect类供开发者对UI组件的顶点进行修改。

创建一个OutlineEx类,继承于BaseMeshEffect类,实现其中的ModifyMesh(VertexHelper)方法。参数VertexHelper类提供了GetUIVertexStream(List<UIVertex>)AddUIVertexTriangleStream(List<UIVertex>)方法用于获取和设置UI物件的顶点。

这里我们可以把参数需要的List提出来做成静态变量,这样能够避免每次ModifyMesh调用时创建List对象。

  1. public class OutlineEx : BaseMeshEffect
  2. {
  3. public Color OutlineColor = Color.white;
  4. [Range(0, 6)]
  5. public int OutlineWidth = 0;
  6. private static List<UIVertex> m_VetexList = new List<UIVertex>();
  7. protected override void Awake()
  8. {
  9. base.Awake();
  10. var shader = Shader.Find("TSF Shaders/UI/OutlineEx");
  11. base.graphic.material = new Material(shader);
  12. var v1 = base.graphic.canvas.additionalShaderChannels;
  13. var v2 = AdditionalCanvasShaderChannels.Tangent;
  14. if ((v1 & v2) != v2)
  15. {
  16. base.graphic.canvas.additionalShaderChannels |= v2;
  17. }
  18. this._Refresh();
  19. }
  20. #if UNITY_EDITOR
  21. protected override void OnValidate()
  22. {
  23. base.OnValidate();
  24. if (base.graphic.material != null)
  25. {
  26. this._Refresh();
  27. }
  28. }
  29. #endif
  30. private void _Refresh()
  31. {
  32. base.graphic.material.SetColor("_OutlineColor", this.OutlineColor);
  33. base.graphic.material.SetInt("_OutlineWidth", this.OutlineWidth);
  34. base.graphic.SetVerticesDirty();
  35. }
  36. public override void ModifyMesh(VertexHelper vh)
  37. {
  38. vh.GetUIVertexStream(m_VetexList);
  39. this._ProcessVertices();
  40. vh.Clear();
  41. vh.AddUIVertexTriangleStream(m_VetexList);
  42. }
  43. private void _ProcessVertices()
  44. {
  45. // TODO: 处理顶点
  46. }
  47. }

现在已经可以获取到所有的顶点信息了。接下来我们对它进行外扩。

我们知道每三个顶点构成一个三角形,所以需要对构成三角形的三个顶点进行处理,并且要将它的UV坐标(决定图片在图集中的范围)也做对应的外扩,否则从视觉上看起来就只是图片被放大了一点点。

于是完成_ProcessVertices方法:

  1. private void _ProcessVertices()
  2. {
  3. for (int i = 0, count = m_VetexList.Count - 3; i <= count; i += 3)
  4. {
  5. var v1 = m_VetexList[i];
  6. var v2 = m_VetexList[i + 1];
  7. var v3 = m_VetexList[i + 2];
  8. // 计算原顶点坐标中心点
  9. //
  10. var minX = _Min(v1.position.x, v2.position.x, v3.position.x);
  11. var minY = _Min(v1.position.y, v2.position.y, v3.position.y);
  12. var maxX = _Max(v1.position.x, v2.position.x, v3.position.x);
  13. var maxY = _Max(v1.position.y, v2.position.y, v3.position.y);
  14. var posCenter = new Vector2(minX + maxX, minY + maxY) * 0.5f;
  15. // 计算原始顶点坐标和UV的方向
  16. //
  17. Vector2 triX, triY, uvX, uvY;
  18. Vector2 pos1 = v1.position;
  19. Vector2 pos2 = v2.position;
  20. Vector2 pos3 = v3.position;
  21. if (Mathf.Abs(Vector2.Dot((pos2 - pos1).normalized, Vector2.right))
  22. > Mathf.Abs(Vector2.Dot((pos3 - pos2).normalized, Vector2.right)))
  23. {
  24. triX = pos2 - pos1;
  25. triY = pos3 - pos2;
  26. uvX = v2.uv0 - v1.uv0;
  27. uvY = v3.uv0 - v2.uv0;
  28. }
  29. else
  30. {
  31. triX = pos3 - pos2;
  32. triY = pos2 - pos1;
  33. uvX = v3.uv0 - v2.uv0;
  34. uvY = v2.uv0 - v1.uv0;
  35. }
  36. // 为每个顶点设置新的Position和UV
  37. //
  38. v1 = _SetNewPosAndUV(v1, this.OutlineWidth, posCenter, triX, triY, uvX, uvY);
  39. v2 = _SetNewPosAndUV(v2, this.OutlineWidth, posCenter, triX, triY, uvX, uvY);
  40. v3 = _SetNewPosAndUV(v3, this.OutlineWidth, posCenter, triX, triY, uvX, uvY);
  41. // 应用设置后的UIVertex
  42. //
  43. m_VetexList[i] = v1;
  44. m_VetexList[i + 1] = v2;
  45. m_VetexList[i + 2] = v3;
  46. }
  47. }
  48. private static UIVertex _SetNewPosAndUV(UIVertex pVertex, int pOutLineWidth,
  49. Vector2 pPosCenter,
  50. Vector2 pTriangleX, Vector2 pTriangleY,
  51. Vector2 pUVX, Vector2 pUVY)
  52. {
  53. // Position
  54. var pos = pVertex.position;
  55. var posXOffset = pos.x > pPosCenter.x ? pOutLineWidth : -pOutLineWidth;
  56. var posYOffset = pos.y > pPosCenter.y ? pOutLineWidth : -pOutLineWidth;
  57. pos.x += posXOffset;
  58. pos.y += posYOffset;
  59. pVertex.position = pos;
  60. // UV
  61. var uv = pVertex.uv0;
  62. uv += pUVX / pTriangleX.magnitude * posXOffset * (Vector2.Dot(pTriangleX, Vector2.right) > 0 ? 1 : -1);
  63. uv += pUVY / pTriangleY.magnitude * posYOffset * (Vector2.Dot(pTriangleY, Vector2.up) > 0 ? 1 : -1);
  64. pVertex.uv0 = uv;
  65. return pVertex;
  66. }
  67. private static float _Min(float pA, float pB, float pC)
  68. {
  69. return Mathf.Min(Mathf.Min(pA, pB), pC);
  70. }
  71. private static float _Max(float pA, float pB, float pC)
  72. {
  73. return Mathf.Max(Mathf.Max(pA, pB), pC);
  74. }

然后可以在编辑器中调整描边颜色和宽度,可以看到效果:

OJ8K,现在范围已经被扩大,可以看到上下左右四个边的描边宽度没有被裁掉了。


UV裁剪,排除不需要的像素

在上一步的效果图中,我们可以注意到图片的边界出现了被拉伸的部分。如果使用了图集或字体,在UV扩大后图片附近的像素也会被包含进来。为什么会变成这样呢?(先打死)

因为前面说过,UV裁剪框就相当于图集中每个小图的范围。直接扩大必然会包含到小图邻接的图的像素。所以这一步我们需要对最终绘制出的图进行裁剪,保证这些不要的像素不被画出来。

裁剪的逻辑也很简单。如果该像素处于被扩大前的UV范围外,则设置它的alpha为0。这一步需要放在像素着色器中完成。如何将原始UV区域传进Shader是一个问题。对于Text组件,所有字符的顶点都会进入Shader处理,所以在Shader中添加属性是不现实的。

好在Unity为我们提供了门路,可以看UIVertex结构体的成员:

  1. public struct UIVertex
  2. {
  3. public static UIVertex simpleVert;
  4. public Vector3 position;
  5. public Vector3 normal;
  6. public Color32 color;
  7. public Vector2 uv0;
  8. public Vector2 uv1;
  9. public Vector2 uv2;
  10. public Vector2 uv3;
  11. public Vector4 tangent;
  12. }

而Unity默认只会使用到positionnormaluv0color,其他成员是不会使用的。所以我们可以考虑将原始UV框的数据(最小x,最小y,最大x,最大y)赋值给tangent成员,因为它刚好是一个Vector4类型。

当然,你想把数据分别放在uv1uv2中也是可以的。

这里感谢真木网友的指正,UI在缩放时,tangent的值会被影响,导致描边显示不全甚至完全消失,所以应该赋值给uv1uv2。经测试,Unity 5.6自身有bug,uv2uv3无论怎么设置都不会被传入shader,但在2017.3.1p1和2018上测试通过。如果必须要使用低版本Unity,可以考虑使用uv1tangent.zw存储原始UV框的四个值,但要求UI的Z轴不能缩放,且Canvas和摄像机必须正交。

需要注意的是,在Unity5.4(大概是这个版本吧,记不清了)之后,UIVertex的非必须成员的数据默认不会被传递进Shader。所以我们需要修改UI组件的CanvasadditionalShaderChannels属性,让uv1uv2成员也传入Shader。

  1. var v1 = base.graphic.canvas.additionalShaderChannels;
  2. var v2 = AdditionalCanvasShaderChannels.TexCoord1;
  3. if ((v1 & v2) != v2)
  4. {
  5. base.graphic.canvas.additionalShaderChannels |= v2;
  6. }
  7. v2 = AdditionalCanvasShaderChannels.TexCoord2;
  8. if ((v1 & v2) != v2)
  9. {
  10. base.graphic.canvas.additionalShaderChannels |= v2;
  11. }

将原始UV框赋值给uv1uv2成员

  1. var uvMin = _Min(v1.uv0, v2.uv0, v3.uv0);
  2. var uvMax = _Max(v1.uv0, v2.uv0, v3.uv0);
  3. vertex.uv1 = new Vector2(pUVOrigin.x, pUVOrigin.y);
  4. vertex.uv2 = new Vector2(pUVOrigin.z, pUVOrigin.w);
  5. private static Vector2 _Min(Vector2 pA, Vector2 pB, Vector2 pC)
  6. {
  7. return new Vector2(_Min(pA.x, pB.x, pC.x), _Min(pA.y, pB.y, pC.y));
  8. }
  9. private static Vector2 _Max(Vector2 pA, Vector2 pB, Vector2 pC)
  10. {
  11. return new Vector2(_Max(pA.x, pB.x, pC.x), _Max(pA.y, pB.y, pC.y));
  12. }

然后在Shader的顶点着色器中获取它:

  1. struct appdata
  2. {
  3. // 省略
  4. float2 texcoord1 : TEXCOORD1;
  5. float2 texcoord2 : TEXCOORD2;
  6. };
  7. struct v2f
  8. {
  9. // 省略
  10. float2 uvOriginXY : TEXCOORD1;
  11. float2 uvOriginZW : TEXCOORD2;
  12. };
  13. v2f vert(appdata IN)
  14. {
  15. // 省略
  16. o.uvOriginXY = IN.texcoord1;
  17. o.uvOriginZW = IN.texcoord2;
  18. // 省略
  19. }

判定一个点是否在给定矩形框内,可以用到内置的step函数。它常用于作比较,替代if/else语句提高效率。它的逻辑是:顺序给定两个参数a和b,如果 a > b 返回0,否则返回1。

添加判定函数:

  1. fixed IsInRect(float2 pPos, float2 pClipRectXY, float2 pClipRectZW)
  2. {
  3. pPos = step(pClipRectXY, pPos) * step(pPos, pClipRectZW);
  4. return pPos.x * pPos.y;
  5. }

然后在采样和像素着色器中添加对它的调用:

  1. fixed SampleAlpha(int pIndex, v2f IN)
  2. {
  3. // 省略
  4. return IsInRect(pos, IN.uvOriginXY, IN.uvOriginZW) * (tex2D(_MainTex, pos) + _TextureSampleAdd).w * _OutlineColor.w;
  5. }
  6. fixed4 frag(v2f IN) : SV_Target
  7. {
  8. // 省略
  9. if (_OutlineWidth > 0)
  10. {
  11. color.w *= IsInRect(IN.texcoord, IN.uvOriginXY, IN.uvOriginZW);
  12. // 省略
  13. }
  14. }

最终代码

那么现在就可以得到最终效果了。在我的代码中,对每个像素做了12次采样。如果美术要求对大图片进行比较粗的描边,需要增加采样次数。当然,如果字本身小,也可以降低次数。

由于这个Shader是给UI用的,所以需要将UI-Default.shader中的一些属性和设置复制到我们的Shader中。

  1. //————————————————————————————————————————————
  2. // OutlineEx.cs
  3. //
  4. // Created by Chiyu Ren on 2018/9/12 23:03:51
  5. //————————————————————————————————————————————
  6. using UnityEngine;
  7. using UnityEngine.UI;
  8. using System.Collections.Generic;
  9. namespace TooSimpleFramework.UI
  10. {
  11. /// <summary>
  12. /// UGUI描边
  13. /// </summary>
  14. public class OutlineEx : BaseMeshEffect
  15. {
  16. public Color OutlineColor = Color.white;
  17. [Range(0, 6)]
  18. public int OutlineWidth = 0;
  19. private static List<UIVertex> m_VetexList = new List<UIVertex>();
  20. protected override void Start()
  21. {
  22. base.Start();
  23. var shader = Shader.Find("TSF Shaders/UI/OutlineEx");
  24. base.graphic.material = new Material(shader);
  25. var v1 = base.graphic.canvas.additionalShaderChannels;
  26. var v2 = AdditionalCanvasShaderChannels.TexCoord1;
  27. if ((v1 & v2) != v2)
  28. {
  29. base.graphic.canvas.additionalShaderChannels |= v2;
  30. }
  31. v2 = AdditionalCanvasShaderChannels.TexCoord2;
  32. if ((v1 & v2) != v2)
  33. {
  34. base.graphic.canvas.additionalShaderChannels |= v2;
  35. }
  36. this._Refresh();
  37. }
  38. #if UNITY_EDITOR
  39. protected override void OnValidate()
  40. {
  41. base.OnValidate();
  42. if (base.graphic.material != null)
  43. {
  44. this._Refresh();
  45. }
  46. }
  47. #endif
  48. private void _Refresh()
  49. {
  50. base.graphic.material.SetColor("_OutlineColor", this.OutlineColor);
  51. base.graphic.material.SetInt("_OutlineWidth", this.OutlineWidth);
  52. base.graphic.SetVerticesDirty();
  53. }
  54. public override void ModifyMesh(VertexHelper vh)
  55. {
  56. vh.GetUIVertexStream(m_VetexList);
  57. this._ProcessVertices();
  58. vh.Clear();
  59. vh.AddUIVertexTriangleStream(m_VetexList);
  60. }
  61. private void _ProcessVertices()
  62. {
  63. for (int i = 0, count = m_VetexList.Count - 3; i <= count; i += 3)
  64. {
  65. var v1 = m_VetexList[i];
  66. var v2 = m_VetexList[i + 1];
  67. var v3 = m_VetexList[i + 2];
  68. // 计算原顶点坐标中心点
  69. //
  70. var minX = _Min(v1.position.x, v2.position.x, v3.position.x);
  71. var minY = _Min(v1.position.y, v2.position.y, v3.position.y);
  72. var maxX = _Max(v1.position.x, v2.position.x, v3.position.x);
  73. var maxY = _Max(v1.position.y, v2.position.y, v3.position.y);
  74. var posCenter = new Vector2(minX + maxX, minY + maxY) * 0.5f;
  75. // 计算原始顶点坐标和UV的方向
  76. //
  77. Vector2 triX, triY, uvX, uvY;
  78. Vector2 pos1 = v1.position;
  79. Vector2 pos2 = v2.position;
  80. Vector2 pos3 = v3.position;
  81. if (Mathf.Abs(Vector2.Dot((pos2 - pos1).normalized, Vector2.right))
  82. > Mathf.Abs(Vector2.Dot((pos3 - pos2).normalized, Vector2.right)))
  83. {
  84. triX = pos2 - pos1;
  85. triY = pos3 - pos2;
  86. uvX = v2.uv0 - v1.uv0;
  87. uvY = v3.uv0 - v2.uv0;
  88. }
  89. else
  90. {
  91. triX = pos3 - pos2;
  92. triY = pos2 - pos1;
  93. uvX = v3.uv0 - v2.uv0;
  94. uvY = v2.uv0 - v1.uv0;
  95. }
  96. // 计算原始UV框
  97. //
  98. var uvMin = _Min(v1.uv0, v2.uv0, v3.uv0);
  99. var uvMax = _Max(v1.uv0, v2.uv0, v3.uv0);
  100. var uvOrigin = new Vector4(uvMin.x, uvMin.y, uvMax.x, uvMax.y);
  101. // 为每个顶点设置新的Position和UV,并传入原始UV框
  102. //
  103. v1 = _SetNewPosAndUV(v1, this.OutlineWidth, posCenter, triX, triY, uvX, uvY, uvOrigin);
  104. v2 = _SetNewPosAndUV(v2, this.OutlineWidth, posCenter, triX, triY, uvX, uvY, uvOrigin);
  105. v3 = _SetNewPosAndUV(v3, this.OutlineWidth, posCenter, triX, triY, uvX, uvY, uvOrigin);
  106. // 应用设置后的UIVertex
  107. //
  108. m_VetexList[i] = v1;
  109. m_VetexList[i + 1] = v2;
  110. m_VetexList[i + 2] = v3;
  111. }
  112. }
  113. private static UIVertex _SetNewPosAndUV(UIVertex pVertex, int pOutLineWidth,
  114. Vector2 pPosCenter,
  115. Vector2 pTriangleX, Vector2 pTriangleY,
  116. Vector2 pUVX, Vector2 pUVY,
  117. Vector4 pUVOrigin)
  118. {
  119. // Position
  120. var pos = pVertex.position;
  121. var posXOffset = pos.x > pPosCenter.x ? pOutLineWidth : -pOutLineWidth;
  122. var posYOffset = pos.y > pPosCenter.y ? pOutLineWidth : -pOutLineWidth;
  123. pos.x += posXOffset;
  124. pos.y += posYOffset;
  125. pVertex.position = pos;
  126. // UV
  127. var uv = pVertex.uv0;
  128. uv += pUVX / pTriangleX.magnitude * posXOffset * (Vector2.Dot(pTriangleX, Vector2.right) > 0 ? 1 : -1);
  129. uv += pUVY / pTriangleY.magnitude * posYOffset * (Vector2.Dot(pTriangleY, Vector2.up) > 0 ? 1 : -1);
  130. pVertex.uv0 = uv;
  131. // 原始UV框
  132. pVertex.uv1 = new Vector2(pUVOrigin.x, pUVOrigin.y);
  133. pVertex.uv2 = new Vector2(pUVOrigin.z, pUVOrigin.w);
  134. return pVertex;
  135. }
  136. private static float _Min(float pA, float pB, float pC)
  137. {
  138. return Mathf.Min(Mathf.Min(pA, pB), pC);
  139. }
  140. private static float _Max(float pA, float pB, float pC)
  141. {
  142. return Mathf.Max(Mathf.Max(pA, pB), pC);
  143. }
  144. private static Vector2 _Min(Vector2 pA, Vector2 pB, Vector2 pC)
  145. {
  146. return new Vector2(_Min(pA.x, pB.x, pC.x), _Min(pA.y, pB.y, pC.y));
  147. }
  148. private static Vector2 _Max(Vector2 pA, Vector2 pB, Vector2 pC)
  149. {
  150. return new Vector2(_Max(pA.x, pB.x, pC.x), _Max(pA.y, pB.y, pC.y));
  151. }
  152. }
  153. }

Shader

  1. Shader "TSF Shaders/UI/OutlineEx"
  2. {
  3. Properties
  4. {
  5. _MainTex ("Main Texture", 2D) = "white" {}
  6. _Color ("Tint", Color) = (1, 1, 1, 1)
  7. _OutlineColor ("Outline Color", Color) = (1, 1, 1, 1)
  8. _OutlineWidth ("Outline Width", Int) = 1
  9. _StencilComp ("Stencil Comparison", Float) = 8
  10. _Stencil ("Stencil ID", Float) = 0
  11. _StencilOp ("Stencil Operation", Float) = 0
  12. _StencilWriteMask ("Stencil Write Mask", Float) = 255
  13. _StencilReadMask ("Stencil Read Mask", Float) = 255
  14. _ColorMask ("Color Mask", Float) = 15
  15. [Toggle(UNITY_UI_ALPHACLIP)] _UseUIAlphaClip ("Use Alpha Clip", Float) = 0
  16. }
  17. SubShader
  18. {
  19. Tags
  20. {
  21. "Queue"="Transparent"
  22. "IgnoreProjector"="True"
  23. "RenderType"="Transparent"
  24. "PreviewType"="Plane"
  25. "CanUseSpriteAtlas"="True"
  26. }
  27. Stencil
  28. {
  29. Ref [_Stencil]
  30. Comp [_StencilComp]
  31. Pass [_StencilOp]
  32. ReadMask [_StencilReadMask]
  33. WriteMask [_StencilWriteMask]
  34. }
  35. Cull Off
  36. Lighting Off
  37. ZWrite Off
  38. ZTest [unity_GUIZTestMode]
  39. Blend SrcAlpha OneMinusSrcAlpha
  40. ColorMask [_ColorMask]
  41. Pass
  42. {
  43. Name "OUTLINE"
  44. CGPROGRAM
  45. #pragma vertex vert
  46. #pragma fragment frag
  47. sampler2D _MainTex;
  48. fixed4 _Color;
  49. fixed4 _TextureSampleAdd;
  50. float4 _MainTex_TexelSize;
  51. float4 _OutlineColor;
  52. int _OutlineWidth;
  53. struct appdata
  54. {
  55. float4 vertex : POSITION;
  56. float2 texcoord : TEXCOORD0;
  57. float2 texcoord1 : TEXCOORD1;
  58. float2 texcoord2 : TEXCOORD2;
  59. fixed4 color : COLOR;
  60. };
  61. struct v2f
  62. {
  63. float4 vertex : SV_POSITION;
  64. float2 texcoord : TEXCOORD0;
  65. float2 uvOriginXY : TEXCOORD1;
  66. float2 uvOriginZW : TEXCOORD2;
  67. fixed4 color : COLOR;
  68. };
  69. v2f vert(appdata IN)
  70. {
  71. v2f o;
  72. o.vertex = UnityObjectToClipPos(IN.vertex);
  73. o.texcoord = IN.texcoord;
  74. o.uvOriginXY = IN.texcoord1;
  75. o.uvOriginZW = IN.texcoord2;
  76. o.color = IN.color * _Color;
  77. return o;
  78. }
  79. fixed IsInRect(float2 pPos, float2 pClipRectXY, float2 pClipRectZW)
  80. {
  81. pPos = step(pClipRectXY, pPos) * step(pPos, pClipRectZW);
  82. return pPos.x * pPos.y;
  83. }
  84. fixed SampleAlpha(int pIndex, v2f IN)
  85. {
  86. const fixed sinArray[12] = { 0, 0.5, 0.866, 1, 0.866, 0.5, 0, -0.5, -0.866, -1, -0.866, -0.5 };
  87. const fixed cosArray[12] = { 1, 0.866, 0.5, 0, -0.5, -0.866, -1, -0.866, -0.5, 0, 0.5, 0.866 };
  88. float2 pos = IN.texcoord + _MainTex_TexelSize.xy * float2(cosArray[pIndex], sinArray[pIndex]) * _OutlineWidth;
  89. return IsInRect(pos, IN.uvOriginXY, IN.uvOriginZW) * (tex2D(_MainTex, pos) + _TextureSampleAdd).w * _OutlineColor.w;
  90. }
  91. fixed4 frag(v2f IN) : SV_Target
  92. {
  93. fixed4 color = (tex2D(_MainTex, IN.texcoord) + _TextureSampleAdd) * IN.color;
  94. if (_OutlineWidth > 0)
  95. {
  96. color.w *= IsInRect(IN.texcoord, IN.uvOriginXY, IN.uvOriginZW);
  97. half4 val = half4(_OutlineColor.x, _OutlineColor.y, _OutlineColor.z, 0);
  98. val.w += SampleAlpha(0, IN);
  99. val.w += SampleAlpha(1, IN);
  100. val.w += SampleAlpha(2, IN);
  101. val.w += SampleAlpha(3, IN);
  102. val.w += SampleAlpha(4, IN);
  103. val.w += SampleAlpha(5, IN);
  104. val.w += SampleAlpha(6, IN);
  105. val.w += SampleAlpha(7, IN);
  106. val.w += SampleAlpha(8, IN);
  107. val.w += SampleAlpha(9, IN);
  108. val.w += SampleAlpha(10, IN);
  109. val.w += SampleAlpha(11, IN);
  110. val.w = clamp(val.w, 0, 1);
  111. color = (val * (1.0 - color.a)) + (color * color.a);
  112. }
  113. return color;
  114. }
  115. ENDCG
  116. }
  117. }
  118. }

最终效果:


优化点

可以看到在最后的像素着色器中使用了if语句。因为我比较菜,写出来的颜色混合算法在描边宽度为0的时候看起来效果很不好。

如果有大神能提供一个更优的算法,欢迎在评论中把我批判一番。把if语句去掉,可以提升一定的性能。

还有一点是,如果将图片或文字本身的透明度设为0,并不能得到镂空的效果。如果美术提出要这个效果,请毫不犹豫打死(误

最后一点,仔细观察上面最终效果的Ass,可以发现它们的字符本身被后一个字符的描边覆盖了一部分。使用两个Pass可以解决,一个只绘制描边,另一个只绘制本身。

Pass1

  1. fixed4 frag(v2f IN) : SV_Target
  2. {
  3. // 省略
  4. val.w = clamp(val.w, 0, 1);
  5. return val;
  6. }

Pass2

  1. fixed4 frag(v2f IN) : SV_Target
  2. {
  3. fixed4 color = (tex2D(_MainTex, IN.texcoord) + _TextureSampleAdd) * IN.color;
  4. color.w *= IsInRect(IN.texcoord, IN.uvOriginXY, IN.uvOriginZW);
  5. return color;
  6. }

改动很简单,具体实现就留给读者了。


后记

首先要感谢提供这个思路的原作者。不然我还真想不出可以这么做。看来我毕竟还是图样。

希望这篇博文能帮到需要的朋友,因为网上几乎没有这个的教程。之前在别人的博客看到一句话:人生就是水桶,前三十年大家给你灌水,后三十年你给大家灌水。感觉挺有意思。今后会继续分享一些自己搞出的、网上少有的东西(虽然我还没到30)。

最近倒是没有特别在做什么,不过有在学习Shader,进入了未知♂领域。买了一些书,想给大家推荐冯乐乐的《Unity Shader入门精要》(博客https://blog.csdn.net/candycat1992/),对入门挺有帮助。知道该书作者是比我小一岁但是比我牛逼太多的美女程序媛(不要YY了,有对象的)的时候我真的受到了极大刺激。一个妹子都能钻得这么深,我应该更加努力啊。学习是从摇篮到坟墓的过程,希望大家不管学什么都要坚持。

还有一点就是创业真的要谨慎。最近了解到国家出了条例要对国产游戏限量发行,对各个游戏公司想必都是一记闷锤。加之统一征收社保,引起的连锁反应必然会波及到游戏行业。唯一欣慰的是我们还能做游戏,还能在这条路上继续走。那么就继续走下去吧,不要停下来啊!(指加班)

很惭愧,就做了一点微小的工作,谢谢大家!

基于Shader实现的UGUI描边解决方案的更多相关文章

  1. #研发解决方案介绍#基于ES的搜索+筛选+排序解决方案

    郑昀 基于胡耀华和王超的设计文档 最后更新于2014/12/3 关键词:ElasticSearch.Lucene.solr.搜索.facet.高可用.可伸缩.mongodb.SearchHub.商品中 ...

  2. 基于webpack的前端工程化开发解决方案探索(一):动态生成HTML(转)

    1.什么是工程化开发 软件工程的工程化开发概念由来已久,但对于前端开发来说,我们没有像VS或者eclipse这样量身打造的IDE,因为在大多数人眼中,前端代码无需编译,因此只要一个浏览器来运行调试就行 ...

  3. 基于K2的集成供应链流程解决方案

    基于K2的集成供应链流程解决方案http://www.k2software.cn/zh-hans/scm-solution 一.详细功能模块 需求管理模块多渠道管理.需求计划.需求感知与传递市场营销及 ...

  4. 基于QT的换肤整体解决方案(QSkinStyle)(提供Linux的XP风格)

    基于QT的换肤整体解决方案(QSkinStyle) 对QT这个成功的跨平台GUI库,本身内置了对换肤功能的实现,比如cleanlooks.plastique等跨平台风格:还有一些是和平台相关的风格,比 ...

  5. [项目回顾]基于Redis的在线用户列表解决方案

    迁移:基于Redis的在线用户列表解决方案 前言: 由于项目需求,需要在集群环境下实现在线用户列表的功能,并依靠在线列表实现用户单一登陆(同一账户只能一处登陆)功能: 在单机环境下,在线列表的实现方案 ...

  6. 基于webpack的前端工程化开发解决方案探索(二):代码分割与图片加载

    今天我们继续来进行webpack工程化开发的探索! 首先来验证上一篇文章   基于webpack的前端工程化开发解决方案探索(一):动态生成HTML  中的遗留问题:webpack将如何处理按需加载的 ...

  7. Unity Shader 卡通渲染 基于退化四边形的实时描边

    从csdn转移过来,顺便把写过的文章改写一下转过来. 一.边缘检测算法 3D模型描边有两种方式,一种是基于图像,即在所有3D模型渲染完成一张图片后,对这张图片进行边缘检测,最后得出描边效果.一种是基于 ...

  8. 基于WCF MSMQ 的企业应用解决方案

    最近研究了一下基于MSMQ的WCF应用,从书上.网上查了很多资料,但始终没能彻底理解WCF-MSMQ的工作原理,也没能得到一个合理的应用解决方案.索性还是自己做个实验,探索一下吧.经过反复试验,颇有收 ...

  9. 基于JVM规范的并发编程解决方案

    在并发的世界里,选择合适的状态处理方法将对并发性和正确性起到决定性的影响.这方面可选的方法有:共享可变性.隔离可变性以及完全不可变性. 对于并发问题来说最好的解决方法是从根本上消灭它而不是花很多时间解 ...

随机推荐

  1. Vue以CDN方式调用Swiper轮播异常

    问题概览: 有一个小型单页应用项目,嫌用组件式调用vue-swiper麻烦,因此以CDN的方式调用swiper,结果轮播的图不动了! 爬了半天百度和谷歌都无解决方案,最后曲线救国的方式解决了问题. 解 ...

  2. css常见的概念

    padding-top:10px;是指容器内的内容距离容器的顶部有10个像素,是包含在容器内的: margin-top:10px;是指容器本身的顶部距离其他容器有10个像素,不包含在容器内: top: ...

  3. [python]通过uiautomator实现返回当前程序包名

    # -*- coding: utf-8 -*- from uiautomator import device as d def getCurrentPackageName(): info = d.in ...

  4. Universal-Image-Loader源码分析(二)——载入图片的过程分析

    之前的文章,在上面建立完config之后,UIl通过ImageLoader.getInstance().init(config.build());来初始化ImageLoader对象,之后就可以用Ima ...

  5. Integer、String、StringBuffer、StringBuilder

    Integer Interger 是int基本数据类型的包装类,在Integer内部封装了一个final int value的属性. 构造方法: Integer类提供了两种构造方法:它们都会返回一个I ...

  6. OpenCV——模板匹配

    minMaxLoc函数: void minMaxLoc( const Mat& src, double* minVal, double* maxVal=0, Point* minLoc=0, ...

  7. OpenCV——膨胀与腐蚀

  8. FreeBSD 12.0 版发布

    FreeBSD 是一个自由且开源的类 Unix 操作系统,是 BSD Unix 的直系继承者.起始于 1993 年,FreeBSD 拥有悠久的历史与诸多衍生版本.其饱经考验,是最广泛应用的开源 BSD ...

  9. python 连接操作mysql数据库

    开发数据库程序流程: 1.创建connection对象,获取cursor 2.使用cursor执行SQL 3.使用cursor获取数据.判断执行状态 4.提交事务 或者 回滚事务 import: 数据 ...

  10. cache-fusion笔记

    GRD  (global resource directory)保存着所有实例中资源的分布情况 GCS  (global cache service)具体执行cache fusion 工作的服务,对应 ...