LoginSignup
5
0

More than 1 year has passed since last update.

UGUIのOutlineを元に綺麗なアウトラインを描画する

Last updated at Posted at 2022-06-26

今更感のある記事ですが、そういえば記事としてまとめた事が無かったので書いてみます

Outlineの弱点

手軽に境界線が付けられるOutlineコンポーネントですが、弱点が有ります。
1つは、色の合成が乗算のため、明るい色の境界線を描こうとするとテクスチャのカラーが乗ってしまう事。
こちらの解決方法については、以前書いた記事で解決できるかと思います。

そしてもう一つの弱点ですが、指定したeffectDistanceに合わせて4方に元画像をずらして描画を行う都合上「尖った形状に弱い」という事が有ります。

スクリーンショット 2022-06-27 035121.png

いわゆる「先割れスプーン現象」というヤツですね。

弱点を克服するために

これを解決するための手段は色々あります。
パッと思いつく手法としては

  • TextMeshProUGUIの様にSDF(SignedDistanceField)を作ってシェーダで綺麗にくり抜く
    → シェーダが必要なのと、元画像にSDF加工が必要になる?
  • 隣接ピクセルのアルファ値を読み取るシェーダを書いてアルファの薄い部分に色を塗る
    → サンプリング負荷が怖い
  • 4方に描画する回数を増やして8方向、16方向に描画を行う
    → 一番シンプル。だけど同じ回数描画を重ねるのでフィルレート(というかOverdraw)が高まる

などがあげられるかと思いますが、前段の2つは苦労の割に負荷が高そう&思ったほど綺麗にならなさそうなので
3つ目の 「4方に描画する回数を増やして8方向、16方向に描画を行う」 を手軽に行えるためのコンポーネントを作ってみましょう。

Outlineコンポーネントを見てみる

4方を8方にするために、先ずは大本となるUGUIのOutlineコンポーネントを見てみましょう

Outline.cs
using UnityEngine.Pool;

namespace UnityEngine.UI
{
    [AddComponentMenu("UI/Effects/Outline", 81)]
    /// <summary>
    /// Adds an outline to a graphic using IVertexModifier.
    /// </summary>
    public class Outline : Shadow
    {
        protected Outline()
        {}

        public override void ModifyMesh(VertexHelper vh)
        {
            if (!IsActive())
                return;

            var verts = ListPool<UIVertex>.Get();
            vh.GetUIVertexStream(verts);

            var neededCpacity = verts.Count * 5;
            if (verts.Capacity < neededCpacity)
                verts.Capacity = neededCpacity;

            var start = 0;
            var end = verts.Count;
            ApplyShadowZeroAlloc(verts, effectColor, start, verts.Count, effectDistance.x, effectDistance.y);

            start = end;
            end = verts.Count;
            ApplyShadowZeroAlloc(verts, effectColor, start, verts.Count, effectDistance.x, -effectDistance.y);

            start = end;
            end = verts.Count;
            ApplyShadowZeroAlloc(verts, effectColor, start, verts.Count, -effectDistance.x, effectDistance.y);

            start = end;
            end = verts.Count;
            ApplyShadowZeroAlloc(verts, effectColor, start, verts.Count, -effectDistance.x, -effectDistance.y);

            vh.Clear();
            vh.AddUIVertexTriangleStream(verts);
            ListPool<UIVertex>.Release(verts);
        }
    }
}

さて、何をしているかコードの中身をガッツリ追ったりはしませんが、簡単に書くと

  • VertexHelper(vh)から元画像の頂点情報を取得する
    → vh.GetUIVertexStream(verts);
  • 受け取った頂点情報を元に5倍の頂点キャパを取得している(元の絵+4方分)
    → var neededCpacity = verts.Count * 5;
  • 元の絵を配列末尾に運びつつ、位置をずらした元の絵頂点を、配列の頭側に複製して足していく
    → ApplyShadowZeroAlloc()
  • 加工した頂点をvhに登録しなおしておく
    → vh.AddUIVertexTriangleStream(verts);

といった処理を行っています。
何故元画像を配列末尾に移動しているかというと、アウトラインの後に元画像を描画する必要があるわけですね。
一括でズバッと書く都合上、先に配列前方の頂点から描画されてしまうので、アウトライン用に複製した画像の頂点を配列の後半に置いてしまうとアウトラインの画像が手前に来てしまう・・・のでそれを避けているというわけです。

Outlineを派生して8方向アウトライン用コンポーネントを作る

という事で8方向Outlineコンポーネントの実装です。
Outlineを派生して実装してみましょう。

Outline8.cs
using UnityEngine;
using UnityEngine.Pool;
using UnityEngine.UI;

namespace ScreenPocket
{
    public class Outline8 : Outline
    {
        protected Outline8()
        {}

        public override void ModifyMesh(VertexHelper vh)
        {
            if (!IsActive())
                return;
            
            //Outlineの処理を終わらせておく
            base.ModifyMesh(vh);

            var verts = ListPool<UIVertex>.Get();
            vh.GetUIVertexStream(verts);

            var outline4VertexCount = verts.Count;
            //本描画の頂点数を保持
            var baseCount = outline4VertexCount / 5;
            
            var neededCapacity = baseCount * 9;//9個分(本描画+8方向)のキャパを取得
            if (verts.Capacity < neededCapacity)
                verts.Capacity = neededCapacity;

            //ずらし幅を取得しておく
            var length = effectDistance.magnitude;

            //上
            var start = outline4VertexCount - baseCount;
            var end = verts.Count;
            ApplyShadowZeroAlloc(verts, effectColor, start, end, 0f, length);

            //右
            start = end;
            end = verts.Count;
            ApplyShadowZeroAlloc(verts, effectColor, start, end, length, 0f);

            //下
            start = end;
            end = verts.Count;
            ApplyShadowZeroAlloc(verts, effectColor, start, end, 0f, -length);

            //左
            start = end;
            end = verts.Count;
            ApplyShadowZeroAlloc(verts, effectColor, start, end, -length, 0f);
            
            vh.Clear();
            vh.AddUIVertexTriangleStream(verts);
            ListPool<UIVertex>.Release(verts);
        }
    }
}

処理の流れとしては、下記です。

  • 大本のOutlineコンポーネントの処理を終わらせておく
  • Outlineで*5されたものを/5して大本の画像の頂点数を取得しなおしておく
  • 新たに9個分のCapacityを取得するようにしておく
  • magnitudeを用いて、effectDistanceの縦横にふさわしいずらし幅を取得
  • 後は、縦横にずらした頂点を4つ追加して登録しなおす

という事で、仕上がったアウトラインはこちら。
スクリーンショット 2022-06-27 035155.png

大体の形状はこれでフォローできるはずです。

それでも満足できない人の為に、16方向Outline

Outline8でアウトラインの質を高められたかと思いますが、まだ満足できない人もいるでしょう。
何なら頂点数や塗りつぶしが増えても良いから、もっとアウトラインを綺麗にしたい! というパターン。
そういった要望に応えるために、Outline8を更に派生したOutline16も用意してみました

コードはこちら

Outline16.cs
using UnityEngine;
using UnityEngine.Pool;
using UnityEngine.UI;

namespace ScreenPocket
{
    public class Outline16 : Outline8
    {
        protected Outline16()
        {}

        public override void ModifyMesh(VertexHelper vh)
        {
            if (!IsActive())
                return;

            base.ModifyMesh(vh);

            var verts = ListPool<UIVertex>.Get();
            vh.GetUIVertexStream(verts);

            var outline8Count = verts.Count;
            var baseCount = verts.Count / 9;
            var neededCapacity = baseCount * 17;//本描画+16方向
            if (verts.Capacity < neededCapacity)
                verts.Capacity = neededCapacity;

            var rot = 90f / 4f;
            //ずらす
            var rotVectorA = Quaternion.AngleAxis(rot, Vector3.forward) * effectDistance;
            
            var start = outline8Count - baseCount;
            var end = verts.Count;
            ApplyShadowZeroAlloc(verts, effectColor, start, end, rotVectorA.x, rotVectorA.y);

            start = end;
            end = verts.Count;
            ApplyShadowZeroAlloc(verts, effectColor, start, end, rotVectorA.x, -rotVectorA.y);

            start = end;
            end = verts.Count;
            ApplyShadowZeroAlloc(verts, effectColor, start, end, -rotVectorA.x, rotVectorA.y);

            start = end;
            end = verts.Count;
            ApplyShadowZeroAlloc(verts, effectColor, start, end, -rotVectorA.x, -rotVectorA.y);
            //反対側にずらす
            var rotVectorB = Quaternion.AngleAxis(-rot, Vector3.forward) * effectDistance;
            start = end;
            end = verts.Count;
            ApplyShadowZeroAlloc(verts, effectColor, start, end, rotVectorB.x, rotVectorB.y);

            start = end;
            end = verts.Count;
            ApplyShadowZeroAlloc(verts, effectColor, start, end, rotVectorB.x, -rotVectorB.y);

            start = end;
            end = verts.Count;
            ApplyShadowZeroAlloc(verts, effectColor, start, end, -rotVectorB.x, rotVectorB.y);

            start = end;
            end = verts.Count;
            ApplyShadowZeroAlloc(verts, effectColor, start, end, -rotVectorB.x, -rotVectorB.y);
            
            
            vh.Clear();
            vh.AddUIVertexTriangleStream(verts);
            ListPool<UIVertex>.Release(verts);
        }
    }
}

やっていることはOutline8とほぼ変わりませんが、ずらし幅の求め方だけが少し変わっています。
Outline8は縦横にeffectDistanceの長さ分ずらせば良いだけだったのでmagnitudeを使いましたが、
今回は、「斜め45度と水平/垂直の間に1方向ずつずらした位置に方向を追加したい」という事でeffectDistanceを90°/4=22.5°ずつ前後に回したベクトルを用いて、ずらし画像を追加していることが見て取れるかと思います。
※綺麗なアウトラインを求めている時点で「Outlineの縦横は同値(つまり基準となる角度は45°)にせざるを得ないだろう」という事で、今回は22.5°固定のズラしとなっています。

20220627a.png

Outline16によって作られたアウトラインはこのようになります。
スクリーンショット 2022-06-27 035227.png

通常のOutlineコンポーネントの仕上がりと比較すると、非常に綺麗になったのではないでしょうか。
スクリーンショット 2022-06-27 035121.pngスクリーンショット 2022-06-27 035155.pngスクリーンショット 2022-06-27 035227.png

終わりに

という事で、綺麗なアウトラインを作れるコンポーネントを、元のクラスを活用しつつ準備してみました。
派生が嫌いな人は、いっそOutline.csをコピペした上で、頂点追加部分だけを追加してOutline8、16を用意するのも良いかと思います。
また、当然ながら Outlineと比べて頂点数は多くなっていきます & 重ね描画回数が増すのでフィルレートが高くなってしまいます。
使い処を考えて、処理負荷とのトレードオフで使用していただきますようお願いいたします。

5
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
0