加载中…
个人资料
  • 博客等级:
  • 博客积分:
  • 博客访问:
  • 关注人气:
  • 获赠金笔:0支
  • 赠出金笔:0支
  • 荣誉徽章:
正文 字体大小:

[转载]Unity3d ugui 的字体描边算法(一)

(2016-08-26 11:36:37)
标签:

转载

https://blog.innogames.com/unity-3d-tutorial-how-to-improve-text-field-outlines

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 when the outline size is too large. In this tutorial, I will show you how to minimize errors and improve the visual quality of outlines to increase readability. The idea is to create a shader that creates the effect by multi-sampling the texture that contains the text characters with an offset to have smooth outlines. The same technique is also used when creating outer glow effects or blur post processing of a whole image.

https://blog.innogames.com/wp-content/uploads/2016/03/Unity-3D-Tutorial-How-to-Improve-Outlines.jpgugui 的字体描边算法(一)" />

Making text outlines readable in Unity

UV Clipping

The challenge here is that to render batch text, Unity3D puts all characters of a particular font into an atlas texture. Meaning, that adding an offset to the UVs is not enough. As you can see from my first test below, adding an offset to the UVs makes neighboring characters that are part of the texture bleed into out rendering output:

https://blog.innogames.com/wp-content/uploads/2016/03/glow00.gifugui 的字体描边算法(一)" />

First test

To work around this problem, we have to tell our shader what the UV bounds of a single character are. Unity3D UI has builtin support for mesh modifiers. If you derive a component from IMeshModifier, you get a callback to modify the mesh data after the UI has created the vertex buffers for a particular component.

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.AddUIVertexTriangleStream(s_tempVertices);
    }
}

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 is now zero if it’s outside of clipRect or one if it’s insides. We use this result to manipulate the alpha output of our shader to be able to come up with this:

https://blog.innogames.com/wp-content/uploads/2016/03/glow01_clipped.gifugui 的字体描边算法(一)" />

After manipulation of the alpha output of the shader

Now that we solved the bleeding problem, we can start implementing the glow filter kernel. The basic idea is to sample the texture multiple times with an offset and sum the values together to create the effect. As a trade-off between quality and performance, I decided to use a 3×3 filter kernel for this:

float2 g_kernelOffsets[8] =
{
    {-0.7,-0.7}, {0,-1}, {0.7,-0.7},
    {-1  0         , {1  0  },
    {-0.70.7}, {01}, {0.70.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:

https://blog.innogames.com/wp-content/uploads/2016/03/glow_cutoff.pngugui 的字体描边算法(一)" />

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. To prevent the characters from getting larger while doing this, you have to downscale the UVs accordingly.

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 a glow size parameter to the mesh modifier component that is based on pixels.

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.AddUIVertexTriangleStream(s_tempVertices);
    }
#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));
    }
}


0

  

新浪BLOG意见反馈留言板 欢迎批评指正

新浪简介 | About Sina | 广告服务 | 联系我们 | 招聘信息 | 网站律师 | SINA English | 产品答疑

新浪公司 版权所有