TextMeshProのOutlineを使ってみた

こんにちは。カヤックアキバスタジオでエンジニアをやっている臼井です。

この記事は 面白法人グループ Advent Calendar 2022 の25日目の記事です。

クリスマス、みなさまいかがお過ごしでしょうか?

今年も残り後わずか、あっという間にAdvent Calendar最終日!!!

最終日はTextMeshProを最近になって、ようやく使い始めたのでそのお話をしようと思います。

はじめに

テキストを表示するとき、アウトラインを使うことってよくありますよね。
UGUIのTextコンポーネントを使う際は、Outlineコンポーネントをアタッチするとアウトライン表示ができます。
簡単、お手軽、便利ですね。
TextMeshProの場合、Outlineコンポーネントをアタッチしてもアウトライン表示がされませんでした。
なぜ?となりながら色々と試してみた内容を備忘録的に紹介していこうと思います。

アウトライン設定をしてみる

アウトライン設定どこにあるんだろう?と探してみると、
TMProTextオブジェクトを選択してinspectorの下の方に表示されてるMaterial表示内にありました。

Outline項目にチェックを入れ、Thicknessの値を増やすとアウトライン表示されました。
Thicknessだけいじるとあまり綺麗に見えなかったので、Face項目のDilateも合わせて調整すると良さそうです。

ここで問題が発生・・・。
選択してるオブジェクトだけアウトライン表示される事を期待していたのですが、
他のテキストも一緒にアウトライン表示されてしまいました。
同じMaterialを使ってるからそうなりますよね。

調べてみると、別々にしたい場合はMaterial Presetを作って対応するとの事で
Material Presetを作って試してみました。

Material Presetを使ってみる

Material Preset作成

まずは下記手順でMaterial Presetを作成します。

  1. Projectビュー内にある、Font Assetが保持しているMaterialを選択
  2. inspectorに表示されている名前横のアイコンを右クリックしてメニューを表示
  3. Create Material Presetを選択

今回は、白とオレンジ用のMaterial Presetを用意しました。

Material Preset設定

作成したMaterial Presetを設定してみます。

  1. Material PresetのOutlineを有効にしパラメータを調整
  2. 設定したいTMProTextオブジェクトを選択
  3. inspectorに表示されている、Material Preset項目に使いたいMaterial Presetを設定

それぞれ、違うアウトラインが表示されました。
やりたかった事が出来てよかった!と思っていたのですが、
種類が増えるたびにMaterial Presetを準備するのは不便だな・・・と思ってしまったので
Material Presetを準備せずにできる方法はないかなと思いやってみたのが次になります。

Material Presetを使わずにやってみる

Material Presetを使った時は事前にMaterialを作って使用しているので、
スクリプト内でMaterialを生成して使用する形にすればいけるのでは?
という事で、IMaterialModifierというインターフェースがあったのでそれを利用してみました。 docs.unity3d.com

IMaterialModifierを継承して、GetModifiedMaterial関数が呼ばれた際に
渡ってきたMaterialを元に新しくMaterialを生成して使用する形をとってみました。
ソースはこちらになります。

using TMPro;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;

#nullable enable

[ExecuteAlways]
[DisallowMultipleComponent]
[RequireComponent(typeof(TextMeshProUGUI))]
public class TMProUguiOutline : UIBehaviour, IMaterialModifier
{
    [SerializeField] bool _useFace = true;
    [SerializeField, ColorUsage(true, true)] Color _faceColor = Color.white;
    [SerializeField, Range(0f, 1f)] float _faceSoftness = 0f;
    [SerializeField, Range(-1f, 1f)] float _faceDilate = 0f;

    [SerializeField] bool _useOutline = true;
    [SerializeField, ColorUsage(true, true)] Color _outlineColor = Color.black;
    [SerializeField, Range(0f, 1f)] float _outlineWidth = 0f;

    Graphic? _graphic;
    TextMeshProUGUI? _textComponent;
    Material? _material;

    Graphic? graphic => _graphic ??= GetComponent<Graphic>();

    TextMeshProUGUI? textComponent => _textComponent ??= GetComponent<TextMeshProUGUI>();

    protected override void OnEnable()
    {
        base.OnEnable();
        if (graphic != null)
        {
            graphic.SetMaterialDirty();
        }
    }

    protected override void OnDisable()
    {
        base.OnDisable();
        if (graphic != null)
        {
            graphic.SetMaterialDirty();
        }

        if (textComponent != null && textComponent.font != null)
        {
            textComponent.fontMaterial = textComponent.font.material;
        }
    }

    protected override void OnDestroy()
    {
        DestroyMaterial();
    }

    public Material GetModifiedMaterial(Material baseMaterial)
    {
        if (!IsActive() || graphic == null || textComponent == null)
        {
            return baseMaterial;
        }

        if (_material != null && _material.name != baseMaterial.name)
        {
            DestroyMaterial();
        }

        var isNewMaterial = false;
        if (_material == null)
        {
            _material = new Material(baseMaterial)
            {
                name = baseMaterial.name + "(TMProUguiOutline Instance)",
                hideFlags = HideFlags.HideAndDontSave,
                shaderKeywords = baseMaterial.shaderKeywords
            };

            isNewMaterial = true;
        }

        _material.CopyPropertiesFromMaterial(textComponent.font.material);

        if (_useFace && _material.HasProperty(ShaderUtilities.ID_FaceColor))
        {
            _material.SetColor(ShaderUtilities.ID_FaceColor, _faceColor);
            _material.SetFloat(ShaderUtilities.ID_FaceDilate, _faceDilate);
            _material.SetFloat(ShaderUtilities.ID_OutlineSoftness, _faceSoftness);
        }

        if (_useOutline && _material.HasProperty(ShaderUtilities.ID_OutlineColor))
        {
            _material.EnableKeyword(ShaderUtilities.Keyword_Outline);
            _material.SetColor(ShaderUtilities.ID_OutlineColor, _outlineColor);
            _material.SetFloat(ShaderUtilities.ID_OutlineWidth, _outlineWidth);
        }
        else
        {
            _material.DisableKeyword(ShaderUtilities.Keyword_Outline);
        }

        if (_material.HasProperty(ShaderUtilities.ID_StencilComp))
        {
            var stencilID = baseMaterial.GetFloat(ShaderUtilities.ID_StencilID);
            var stencilComp = baseMaterial.GetFloat(ShaderUtilities.ID_StencilComp);
            _material.SetFloat(ShaderUtilities.ID_StencilID, stencilID);
            _material.SetFloat(ShaderUtilities.ID_StencilComp, stencilComp);
        }

        if (isNewMaterial)
        {
            textComponent.fontMaterial = _material;
            textComponent.ForceMeshUpdate();
        }
        else
        {
            textComponent.UpdateMeshPadding();
        }

        return _material;
    }

    void DestroyMaterial()
    {
        if (_material != null)
        {
#if UNITY_EDITOR
            DestroyImmediate(_material);
#else
            Destroy(_material);
#endif
            _material = null;
        }
    }

#if UNITY_EDITOR
    protected override void OnValidate()
    {
        base.OnValidate();
        if (!IsActive() || graphic == null) return;
        graphic.SetMaterialDirty();
    }
#endif
}

TMProTextオブジェクトに作成したスクリプトをアタッチした結果がこちら

Merry Christmas2022それぞれをinspectorから指定した色でアウトライン表示が出来ました!

おわりに

いかがでしたでしょうか?
今回とったアプローチでは、Material Presetを作って設定する手間はなくなりましたが、別々のMaterialのため

  • Set Passが増える
  • 同じ値のものでも調整する際に個別に調整する必要がある

というデメリットもあります。
使い所を限定したり、後で使ってる情報を元にMaterial Presetを生成して置き換えるというのも手かなと思います。
見た目は同じでも色々なやり方があって面白いですよね!
他にもやり方がないか探しながら引き出しを増やしていこうと思います!

それではみなさま、Merry Christmas! & 良いお年を!