标签:
转载 |
Unity 3D Tutorial: How to Improve Text Field Outlines
If you’ve ever used the “outline” component for Unity UI Text
Fields, you might have noticed that the outlines are quite sharp
and can lead to rendering errors
Making text outlines readable in Unity
UV Clipping
The challenge here is that to render batch
text,
To work around this problem, we have to tell our shader what the UV
bounds of a single character are.
The vertex format of Unity’s “UIVertex” is the following:
Name
|
Type
|
---|---|
pos | float3 |
uv0 | float2 |
uv1 | float2 |
normal | float3 |
tangent | float4 |
color | float4 |
The vertex attributes in bold are not used by the default UI
shader. Thus, we’re able to add additional data here to use in our
custom shader for font rendering.
As we need four values, we use the tangent of the vertex data to
put a UV rect inside of it. We then use this rectangle in our
fragment shader to clip unwanted areas.
using System.Collections.Generic; using UnityEngine; using UnityEngine.UI; public class MeshModTextGlow :
BaseMeshEffect { // use temporary list to prevent
allocations private static readonly List s_tempVertices
= new List(); public override void ModifyMesh(VertexHelper
_vh) { // retrieve vertex data _vh.GetUIVertexStream(s_tempVertices); // for every triangle... for (var i = 0; i <=
s_tempVertices.Count - 3; i += 3) { // retrieve UV minimum and
maximum Vector2 uvMin = Min(s_tempVertices[i +
0].uv0, s_tempVertices[i + 1].uv0, s_tempVertices[i +
2].uv0); Vector2 uvMax = Max(s_tempVertices[i +
0].uv0, s_tempVertices[i + 1].uv0, s_tempVertices[i +
2].uv0); // create a Vector4 with UV min. and
max. var tangent
= new Vector4(uvMin.x, uvMin.y,
uvMax.x, uvMax.y); // now set these tangent in each
vertex for (var v = 0; v < 3;
++v) { UIVertex vertex = s_tempVertices[i +
v]; vertex.tangent = tangent; s_tempVertices[i + v] =
vertex; } } // apply modified vertices _vh.Clear(); _vh.AddUIVertexTriangleStrea } } |
For the shader code, I used a function that branchlessly determines if a point is contained in a given rectangle:
float2 inside =
step(clipRect.xy, uv.xy) * step(uv.xy, clipRect.zw); float pointInRect = inside.x *
inside.y; |
pointInRect
After manipulation of the alpha output of the shader
Now that we
float2
g_kernelOffsets[ 8 ] = { {- 0.7 ,- 0.7 }, { 0 ,- 1 }, { 0.7 ,- 0.7 }, {- 1 , 0 } 1 , 0 }, {- 0.7 , 0.7 }, { 0 , 1 }, { 0.7 , 0.7 } }; |
The center part of this matrix is left out because it represents the normal font texture itself, there is no need to sample this position twice.
Adding a glow color and a strength, we come up with this result:
Glow color and strength added to outline
Enlarging the Character Quads
You can see immediately that the quads for each character are not
large enough to cover the glow area. This can be solved by
enlarging the character quads. We go back to our mesh modifier,
calculate the center of each quad and then extrude the vertices
away from the center point.
The result looks like this:
https://blog.innogames.com/wp-content/uploads/2016/03/image2016-2-12-12-24-55.pngugui
If you look carefully you’ll notice that the aspect ratio of the glow is not correct. The reason for this is that the glow size so far has been hardcoded into the glow shader and the value just offsets UV coordinates. If the texture is not quadratic though, the result is that the glow size is not evenly distributed between the X and Y axes.
So to fix this and to make the component more usable, I
added
This requires a little bit of math to calculate. The reason is that Unity sometimes decides to put characters into the texture atlas with rotation if it fits better. In this example, the “W” character is put onto the texture in a 90° rotation. So the whole code must be independent of any assumptions regarding axes.
What we have to do is calculating a UV shift for the X axis and also a UV shift for the Y axis per pixel. So for each axis, the result is a 2D vector that represents the amount of UV change when moving left/right or up/down.
After multiplying these vectors with the desired glow size, it can be stored in a vertex attribute that is used by the shader to have a glow in pixels as a result.
Comparison:
https://blog.innogames.com/wp-content/uploads/2016/03/glowaspect.gifugui
Our mesh modifier code size has increased a lot and now should look something like this:
using System.Collections.Generic; using UnityEngine; using UnityEngine.UI; [ExecuteInEditMode] public class MeshModTextGlow :
BaseMeshEffect { [Range(0,10)] public float m_size = 3.0f; // defines the glow color +
opacity public Color
m_glowColor; // use temporary list to prevent
allocations private static readonly List s_tempVertices
= new List(); public override void ModifyMesh(VertexHelper
_vh) { _vh.GetUIVertexStream(s_tempVertices); // for every triangle... for (var i = 0; i <=
s_tempVertices.Count - 3; i += 3) { UIVertex v0 = s_tempVertices[i +
0]; UIVertex v1 = s_tempVertices[i +
1]; UIVertex v2 = s_tempVertices[i +
2]; // 2D points please var xy0
= new Vector2(v0.position.x,
v0.position.y); var xy1
= new Vector2(v1.position.x,
v1.position.y); var xy2
= new Vector2(v2.position.x,
v2.position.y); // build two vectors Vector2 deltaA = (xy1 -
xy0).normalized; Vector2 deltaB = (xy2 -
xy1).normalized; Vector2 vecUvX; Vector2 vecUvY; Vector2 vecX; Vector2 vecY; // calculate UV vectors for the X and Y
axes if (Mathf.Abs(Vector2.Dot(deltaA, Vector2.right))
> Mathf.Abs(Vector2.Dot(deltaB, Vector2.right))) { vecX = xy1 - xy0; vecY = xy2 - xy1; vecUvX = v1.uv0 - v0.uv0; vecUvY = v2.uv0 - v1.uv0; } else { vecX = xy2 - xy1; vecY = xy1 - xy0; vecUvX = v2.uv0 - v1.uv0; vecUvY = v1.uv0 - v0.uv0; } // retrieve UV minimum and
maximum Vector2 uvMin = Min(v0.uv0, v1.uv0,
v2.uv0); Vector2 uvMax = Max(v0.uv0, v1.uv0,
v2.uv0); // also retrieve the XY mininum and
maximum float xMin = Min(v0.position.x,
v1.position.x, v2.position.x); float yMin = Min(v0.position.y,
v1.position.y, v2.position.y); float xMax = Max(v0.position.x,
v1.position.x, v2.position.x); float yMax = Max(v0.position.y,
v1.position.y, v2.position.y); var xyMin
= new Vector2(xMin,
yMin); var xyMax
= new Vector2(xMax,
yMax); // store UV min. and max. in the tangent
of each vertex var tangent
= new Vector4(uvMin.x, uvMin.y,
uvMax.x, uvMax.y); // calculate center of UV and
pos Vector2 xyCenter = (xyMin + xyMax) *
0.5f; // we need the vector lengths inside our
loop, precalculate them here float vecXLen =
vecX.magnitude; float vecYLen =
vecY.magnitude; // now manipulate each
vertex for (var v = 0; v < 3;
++v) { UIVertex vertex = s_tempVertices[i +
v]; // extrude each vertex to the outside
'm_size' pixels wide. // we need the extrude to create more
space for the glow var posOld
= new Vector2(vertex.position.x,
vertex.position.y); Vector2 posNew = posOld; float addX = (vertex.position.x
> xyCenter.x) ? m_size : -m_size; float addY = (vertex.position.y
> xyCenter.y) ? m_size : -m_size; float signX = Vector2.Dot(vecX,
Vector2.right) > 0 ? 1 : -1; float signY = Vector2.Dot(vecY,
Vector2.up) > 0 ? 1 : -1; posNew.x += addX; posNew.y += addY; vertex.position
= new Vector3(posNew.x, posNew.y,
m_glowColor.a); // re-calculate UVs accordingly to
prevent scaled texts Vector2 uvOld = vertex.uv0; vertex.uv0 += vecUvX / vecXLen * addX *
signX; vertex.uv0 += vecUvY / vecYLen * addY *
signY; // set the tangent so we know the UV
boundaries. We use this to // prevent smearing into other
characters in the texture atlas vertex.tangent = tangent; // normal is used as glow
color vertex.normal.x =
m_glowColor.r; vertex.normal.y =
m_glowColor.g; vertex.normal.z =
m_glowColor.b; // uv1 is glow size vertex.uv1 = vertex.uv0 -
uvOld; // needs to be positive vertex.uv1.x =
Mathf.Abs(vertex.uv1.x); vertex.uv1.y =
Mathf.Abs(vertex.uv1.y); s_tempVertices[i + v] =
vertex; } } _vh.Clear(); _vh.AddUIVertexTriangleStrea } #if
UNITY_EDITOR protected override void OnValidate() { base .OnValidate(); graphic.SetVerticesDirty(); } #endif private static float Min( float _a, float _b, float _c) { return Mathf.Min(_a, Mathf.Min(_b,
_c)); } private static float Max( float _a, float _b, float _c) { return Mathf.Max(_a, Mathf.Max(_b,
_c)); } private static Vector2 Min(Vector2 _a,
Vector2 _b, Vector2 _c) { return new Vector2(Min(_a.x, _b.x,
_c.x), Min(_a.y, _b.y, _c.y)); } private static Vector2 Max(Vector2 _a,
Vector2 _b, Vector2 _c) { return new Vector2(Max(_a.x, _b.x,
_c.x), Max(_a.y, _b.y, _c.y)); } } |