うどんてっくメモ

技術的なメモをまったりと

【Unity】TextMeshProで一部の文字だけ精度を上げて綺麗に描画したい時の工夫

TextMeshProでUIを作成していて「どうしてもこの部分は大きいサイズで綺麗に文字を見せたい」というケースは多々あると思います。
TextMeshProを使用する場合、文字の精度はFontAssetにおけるテクスチャ上での文字のサイズを表すSampling Point Sizeに依存します。 小さめのSampling Point Sizeで設定したFontAssetを使用しても大きいサイズの文字は描画可能ですが、その綺麗さに影響します。
次の画像を見ればエッジのがたつきなどがわかるかと思います。

これに対応するにはSampling Point Sizeを上げたFontAssetを使用すれば良いのですが、当然必要となるテクスチャのサイズも上がってしまい、メモリへの懸念が大きくなります。 特に、すべての文字出力に使うフォントで文字サイズを上げるとなるとそれ相応のテクスチャのサイズ拡大が見込まれます。
源ノ角ゴシックこちらのサイトに乗っている文字を全て載せた場合を検証した所、4KテクスチャでSampling Point Sizeは31、8Kというサイズまで上げてようやくSampling Point Sizeは84となりました。

そうすると、通常使いするFontAssetをいじらずに調整したくなってきます。このような場合、特定の部分のみに使用する文字だけを集約したFontAssetを利用するという工夫が可能です。
限られた文字のみであれば、大きいSampling Point Sizeでもテクスチャのサイズを抑えられます。Fallbackには通常使いするFontAssetを指定しておけば、想定外の文字が表示されることも防止できます。

使用する文字が限定できること、開発上使用する部分の設定に気を使うこと、開発途中で文字が増えたら対応することなど、色々と制約も存在しますが、覚えておいて損はないテクニックです。 演出の都合など、どうしても大きい文字を出したいケースなどで使ってみてください。

Rust製プラグインで動くGodotのサンプルゲームを公開しました

はじめに

Rust製プラグインで動くGodotのサンプルrungame_sample_godotを公開しました。

rungame_sample_godot

github.com

Godotのプロジェクトとプラグイン、そして必要なアセットが入ったリポジトリです。 submoduleとして後述するrungame_sample_rustを参照しています。

シーン配置とスクリプトのバインドだけを行っており、GDScriptなどのRust以外の実装は行っていません。 基本的にすべてRustのプラグインを読み込み、その動作に委ねます。

オブジェクトの位置や当たり判定などは雑に動けばいいや程度の作りになっています。
ちなみにアセット自体はMagicaVoxelで自分で適当に作ったものです。

rungame_sample_rust

GDNativeのRustライブラリを活用したプラグインです。

github.com

ゲームロジックであるプレイヤーの移動やルールの処理などを行っています。

Player

プレイヤーはPlayerというstructで表現し、入力をもとにRust側での速度を反映したのちに、Godot側のRigidbodyの更新を呼び出しています。

#[export]
fn _physics_process(&mut self, owner: &RigidBody, delta: f64) {
    // 終了時に飛んできた際の処理
    if self.is_active == false {
        owner.set_linear_velocity(Vector3::zero());
        owner.set_angular_velocity(Vector3::zero());
        return;
    }
    
    // Godotの入力
    let input = Input::godot_singleton();
    
    // 単純な加速処理
    if self.move_velocity.z < self.max_forward_speed {
        self.move_velocity.z += self.move_acceleration * delta as f32; 
    } else if self.move_velocity.z > self.max_forward_speed {
        self.move_velocity.z -= self.move_acceleration * delta as f32;
    };
    
    // 左右の移動速度処理
    self.move_velocity.x = 0.;
    if input.is_action_pressed("ui_left") {
        self.move_velocity.x += self.move_horizontal_speed;
    }

    if input.is_action_pressed("ui_right") {
        self.move_velocity.x -= self.move_horizontal_speed;
    }

    // Rust側の速度をGodot側に反映
    owner.set_linear_velocity(self.move_velocity);
}

Field

ステージ内の加速ゾーンや障害物はFieldというPlayerに干渉するtraitとして実装しています。

// コース上のPlayerに干渉するtrait
pub trait Field {
    fn on_player_entered(&self, player: &mut Player);
}

この実装部分で共通する部分をいい感じにmacroで共通化しようと思ったのですが、あえて至極シンプルにしようとコピペしています。

Rule

ゲーム全体のルールはRuleというstructでゲーム状況と時間を更新します。

#[export]
fn _physics_process(&mut self, owner: &Node, delta: f64) {
    let screen = &mut self.screen;
    // ゲームステートの監視
    match &self.state {
        GameState::Ready => {
            // カウントダウンするUIの更新
            if let Some(start_timer) = &self.start_timer {
                screen.set_countdown(start_timer.time_left() as i64);
            }
        }
        GameState::Game => {
            // 時間の更新
            self.time += delta;
            screen.set_time(self.time);

            let player = unsafe {
                owner
                    .get_node_as_instance::<Player>("World/Player")
                    .expect("Playerが取得できなかった")
            };

            player.map(|player, _| {
                screen.set_player_speed(player.move_velocity.z as f64)
            }).expect("Playerを参照できなかった");
        }
        GameState::Over => {}
    }
}

色々とコメントは残してあるので、詳細な実装はリポジトリを確認してみてください。また、間違っている部分などあればぜひ指摘いただけると大変助かります。

おわりに

Rustでゲームを実装してみたい!という選択肢のうち、一番実装環境が整っているのがGodot+Rustでの開発かなと思います。
BevyなどのRust製ゲームエンジンの開発も進んではいるものの、まだGUIのグラフィカルなエディタや便利なカスタマイズなどは充実していない印象です。 とりあえずRustをゲームで書いてみたい方にはおすすめできる選択肢です。

参考文献

環境構築に当たって、以下のリンクを参考にさせていただきました。

Godot EngineからRustを呼ぶ

また、実装に当たって、以下のリンクを参考にさせていただきました。

Godot + Rust + wasmによる3Dブラウザゲームの作り方またはRustはゲーム制作向き言語なのかの考察的な何か - Qiita

Getting Started - The godot-rust Book

gdnative - Rust

Godot API — Godot Engine (stable)の日本語のドキュメント

技術書典13に個人と会社として技術書を出しました

2022年9月10日から開催される技術書典に個人としてと会社としてと2冊の本を出しました。 また、9月11日に開催されたオフラインでの技術書典にも参加してきました。

オフラインでの実施は久しぶりで、実際にイベントとして書いた本を受け取ってもらうのが懐かしい感覚でした。 後述するUniTipsシリーズのブースとして参加したのですが、多くの方に来ていただけて嬉しかったです。 設営も後輩が頑張ってくれた結果、過去一番しっかりしたブースの見た目になりました。

既刊もVol.7とVol.8の物理本で在庫があったものを持ち込みました。Vol.8については持ち込んだ分が見事に完売しまして、皆様ありがとうございました。 久々の物理参加でバタバタするところもありましたが、実りのある機会になったかなと思います。
最後の方にイベント運営の方も「いつも参加していただきありがとうございます」と挨拶をわざわざしに来ていただきました。連続参加していることも認知していただいており嬉しかったです。

個人ではUnity+Mac環境の開発で採用されることの多いIDE「Rider」について機能をまとめた「Rider Guide Tips with Unity」を執筆しました。 IDEとして基本的な機能から知っておくと少しコードを書くのが効率的になる豆知識的な機能まで色々と取り上げています。

リンクはこちらになります。現在も販売中ですので、興味のある方はこちらからお願いします。

techbookfest.org

そして会社としては、UnityのいろんなTipsを有志で書き連ねた「UniTips Vol.9」を執筆しました。
自分は隠れた(?)機能であるUnityのエディタ上でチュートリアルを製作するためのパッケージであるTutorial Frameworkについて紹介しています。

リンクはこちらになります。こちらも販売中ですので、興味のある方はこちらからお願いします。

techbookfest.org

UniTipsも技術書典5から継続して刊行しており、なんと9作目という形になりました。 当時内定者として企画の立ち上げに携わった身として非常に感慨深いです。

技術書典も9回連続サークルとして参加させていただいており、本当にありがたい限りです。 技術書の執筆は大変ですが、自分の知識を見直しながら学ぶことが可能で自己学習としても非常にいい機会となっております。 今後も積極的に参加していく予定です。

【Unity】DeviceSimulatorPluginを使ってDevice Simulatorの機能を拡張する

本記事で紹介および検証を行なっているツールのバージョンは次の通りです。

  • Unity 2021.3.1f1

バージョンによっては挙動に差異がある場合もありますので、ご了承ください。(少なくともUnity2021.1.0f1以降のバージョンで提供される機能となります)


UnityにはDevice Simulatorというエディタ上でモバイル端末などの出力をシミュレートするGame Viewが提供されています。

myudon.hatenablog.com

そんなDevice Simulatorですが、DeviceSimulatorPluginを活用し、機能を拡張することが可能です。

docs.unity3d.com

実際に上記の公式ページにあるサンプルを実装したスクリプトをAssets配下に置き、Device Simulatorの画面を見てみます。

Control Panelに「Touch Info」という名前のメニューが追加されました。実行し、タッチ操作を行うとその合計回数が表示されるようになっています。
サンプルではDeviceSimulatorPluginを継承したクラスを用意し、「Touch Info」というメニューの名前を指定するtitle、生成時処理を行うOnCreate、中身のUIを構成するOnCreateUIをoverrideして実装されています。 サンプルのコードを引用して次に示します。

public class TouchInfoPlugin : DeviceSimulatorPlugin
{
    public override string title => "Touch Info";

...

    public override void OnCreate()
    {
        deviceSimulator.touchScreenInput += touchEvent =>
        {
            m_TouchCount += 1;
            UpdateTouchCounterText();
            m_LastTouchEvent.text = $"Last touch event: {touchEvent.phase.ToString()}";
        };
    }

    public override VisualElement OnCreateUI()
    {
        VisualElement root = new VisualElement();

        m_LastTouchEvent = new Label("Last touch event: None");

        m_TouchCountLabel = new Label();
        UpdateTouchCounterText();

        m_ResetCountButton = new Button {text = "Reset Count" };
        m_ResetCountButton.clicked += () =>
        {
            m_TouchCount = 0;
            UpdateTouchCounterText();
        };

        root.Add(m_LastTouchEvent);
        root.Add(m_TouchCountLabel);
        root.Add(m_ResetCountButton);

        return root;
    }

...

}

DeviceSimulatorPluginではこれらのoverrideするメソッドの他に、DeviceSimulatorの参照がpublicで用意されています。

/// <summary>
///   <para>Extend this class to create a Device Simulator plug-in.</para>
/// </summary>
/// <footer><a href="https://docs.unity3d.com/2021.3/Documentation/ScriptReference/30_search.html?q=DeviceSimulatorPlugin">`DeviceSimulatorPlugin` on docs.unity3d.com</a></footer>
public abstract class DeviceSimulatorPlugin
{
  internal string resolvedTitle;

  /// <summary>
  ///   <para>Device Simulator in which this plug-in is instantiated.</para>
  /// </summary>
  /// <footer><a href="https://docs.unity3d.com/2021.3/Documentation/ScriptReference/30_search.html?q=DeviceSimulation.DeviceSimulatorPlugin-deviceSimulator">`DeviceSimulatorPlugin.deviceSimulator` on docs.unity3d.com</a></footer>
  public DeviceSimulator deviceSimulator { get; internal set; }

  /// <summary>
  ///   <para>Title for the plug-in UI.</para>
  /// </summary>
  /// <footer><a href="https://docs.unity3d.com/2021.3/Documentation/ScriptReference/30_search.html?q=DeviceSimulation.DeviceSimulatorPlugin-title">`DeviceSimulatorPlugin.title` on docs.unity3d.com</a></footer>
  public abstract string title { get; }

  /// <summary>
  ///   <para>Called when Unity creates the Device Simulator window.</para>
  /// </summary>
  /// <footer><a href="https://docs.unity3d.com/2021.3/Documentation/ScriptReference/30_search.html?q=DeviceSimulation.DeviceSimulatorPlugin.OnCreate">`DeviceSimulatorPlugin.OnCreate` on docs.unity3d.com</a></footer>
  public virtual void OnCreate()
  {
  }

  /// <summary>
  ///   <para>Called when Device Simulator window is destroyed.</para>
  /// </summary>
  /// <footer><a href="https://docs.unity3d.com/2021.3/Documentation/ScriptReference/30_search.html?q=DeviceSimulation.DeviceSimulatorPlugin.OnDestroy">`DeviceSimulatorPlugin.OnDestroy` on docs.unity3d.com</a></footer>
  public virtual void OnDestroy()
  {
  }

  /// <summary>
  ///   <para>The VisualElement that this method returns is embedded in the Device Simulator window. If the method returns null, plug-in UI is not embedded.</para>
  /// </summary>
  /// <footer><a href="https://docs.unity3d.com/2021.3/Documentation/ScriptReference/30_search.html?q=DeviceSimulation.DeviceSimulatorPlugin.OnCreateUI">`DeviceSimulatorPlugin.OnCreateUI` on docs.unity3d.com</a></footer>
  public virtual VisualElement OnCreateUI() => (VisualElement) null;
}

DeviceSimulator自体はとくに多くの操作や情報が取れるというわけではなく、タッチした際のイベントとなるtouchScreenInputを登録する程度です。

/// <summary>
///   <para>Class for interacting with a Device Simulator window from a script.</para>
/// </summary>
/// <footer><a href="https://docs.unity3d.com/2021.3/Documentation/ScriptReference/30_search.html?q=DeviceSimulator">`DeviceSimulator` on docs.unity3d.com</a></footer>
public class DeviceSimulator
{
  internal ApplicationSimulation applicationSimulation;

  internal DeviceSimulator()
  {
  }

  public event Action<TouchEvent> touchScreenInput;

  internal void OnTouchScreenInput(TouchEvent touchEvent)
  {
    Delegate[] invocationList = this.touchScreenInput?.GetInvocationList();
    if (invocationList == null)
      return;
    foreach (Action<TouchEvent> action in invocationList)
    {
      try
      {
        action(touchEvent);
      }
      catch (Exception ex)
      {
        Debug.LogException(ex);
      }
    }
  }
}

TouchEventには座標とその動作となる情報が含まれています。

/// <summary>
///   <para>Representation of a single touch event coming from a Device Simulator. Subscribe to DeviceSimulator.touchScreenInput to receive these events.</para>
/// </summary>
/// <footer><a href="https://docs.unity3d.com/2021.3/Documentation/ScriptReference/30_search.html?q=TouchEvent">`TouchEvent` on docs.unity3d.com</a></footer>
public struct TouchEvent
{
  internal TouchEvent(int touchId, Vector2 position, TouchPhase phase)
  {
    this.touchId = touchId;
    this.position = position;
    this.phase = phase;
  }

  /// <summary>
  ///   <para>The unique identifier for the touch. Unity reuses identifiers after the touch ends.</para>
  /// </summary>
  /// <footer><a href="https://docs.unity3d.com/2021.3/Documentation/ScriptReference/30_search.html?q=DeviceSimulation.TouchEvent-touchId">`TouchEvent.touchId` on docs.unity3d.com</a></footer>
  public int touchId { get; }

  /// <summary>
  ///   <para>On-screen position of the touch event. The zero point is at the bottom-left corner of the screen in pixel coordinates.</para>
  /// </summary>
  /// <footer><a href="https://docs.unity3d.com/2021.3/Documentation/ScriptReference/30_search.html?q=DeviceSimulation.TouchEvent-position">`TouchEvent.position` on docs.unity3d.com</a></footer>
  public Vector2 position { get; }

  /// <summary>
  ///   <para>Phase of the touch event.</para>
  /// </summary>
  /// <footer><a href="https://docs.unity3d.com/2021.3/Documentation/ScriptReference/30_search.html?q=DeviceSimulation.TouchEvent-phase">`TouchEvent.phase` on docs.unity3d.com</a></footer>
  public TouchPhase phase { get; }
}

たとえば最後にタッチした座標のCanvas座標とワールド座標を常時表示するようにしたり、タッチした部分にヒットしたオブジェクトを表示したりといった機能ならば、このDeviceSimulatorPluginsを使ってお手軽に追加することが可能そうです。

参考・引用

docs.unity3d.com

【Unity】「uPalette」で色を管理すると捗るという話

はじめに

UIデザインを作っていく上で必要になってくるのが色を管理する仕組みです。 決定形のボタンにはこの青色を、警告的な文言にはこの赤色を、レベルに合わせてこの色の段階を、などなど、色のルールづけが多い場面は多々あります。 そんな時に便利なのがuPaletteです。Haruma-K(@harumak_11) さんがOSSとして公開しているツールで、プロジェクト内の色の管理及び一元変更を実現してくれます。

light11.hatenadiary.com

github.com

実際に開発で使っている筆者が、uPaletteはいいぞという点をいくつか紹介します。 本記事で紹介および検証を行なっているツールのバージョンは次の通りです。

  • Unity 2021.3.0f1
  • uPalette 2.1.1

バージョンによっては挙動に差異がある場合もありますので、ご了承ください。

基礎的な機能の利便性

GUI操作によるシンプルな色の設定の実現

uPaletteではColorSynchronizerというコンポーネントをGraphicにアタッチし、色の設定を行います。 UnityのWindowとしてエディタ拡張が実装されており、GUIの操作をぽちぽちすることで色の登録からColorSynchronizerのアタッチ、色の反映までを実現します。 UIを量産する上で簡単なGUI操作でポチポチと設定を変えられるのはとても重要で、uPalleteはしっかりその部分が作り込まれています。

特徴的なのが、設定側の色の変更に対してリアルタイムで対応するGraphicの色が変わることです。設定側でやっぱり色味を変えようとした際に一律でシーンやPrefab Modeで色の更新を確認することができます。 また、この色を使ってるのどれだっけ?みたいな場面で、逆にハイライトすることが可能です。

もちろんColorSynchronizerのコンポーネントから設定した色は参照可能で、インスペクターから変更することも可能です。

このように、UIパーツの色を管理する上で必要な機能はとても使いやすい形で提供されています。

システム的な使い勝手を考慮した設計

静的に配置しているオブジェクトの色をよしなに変えられることも大切ですが、システムを開発する上では動的にスクリプト側から色を変えたい場合も存在します。 たとえば動的に表示したいデータが持つenumに伴って、このenumは赤、このenumは青という色の変化を行うケースです。 そんな時はスクリプトからの反映が必要ですが、uPaletteではそれらの機能もスクリプト側で公開されており、システム側でもよしなに取り扱うことが可能です。 まずは設定した色の参照ですが、uPaletteではPaletteというクラスで管理されています。

public abstract class Palette<T> : ISerializationCallbackReceiver
{
    ...

    public IObservable<(string entryId, int index)> EntryOrderChangedAsObservable => _entryOrderChangedSubject;
    public IObservable<(string themeId, int index)> ThemeOrderChangedAsObservable => _themeOrderChangedSubject;

    public IReadOnlyObservableProperty<Theme> ActiveTheme => _activeTheme;
    public IReadOnlyObservableDictionary<string, Theme> Themes => _themes;
    public IReadOnlyObservableDictionary<string, Entry<T>> Entries => _entries;

    ...

色はもちろん、2.0.0から追加されたGradationなどの情報もこのPaletteで管理されています。 それぞれのPaletteはPalleteStoreというシングルトンで管理されており、シンプルな実装で色を参照可能です。

public sealed class PaletteStore : ScriptableObject
{
    ...

    public Palette<Color> ColorPalette => _colorPalette;
    public Palette<Gradient> GradientPalette => _gradientPalette;
    public Palette<CharacterStyle> CharacterStylePalette => _characterStylePalette;
    public Palette<CharacterStyleTMP> CharacterStyleTMPPalette => _characterStyleTMPPalette;

    ...
image.color = PaletteStore.Instance.ColorPalette.Entries["color_id"];

色の反映対象となるColorSynchronizerのコンポーネントについても、継承することで独自の拡張をすることを想定した作りになっています。 これによって独自で実装した描画コンポーネントなどにもuPaletteの反映ロジックを適応させることが可能になっています。 わかりやすい例としてそもそものuPaletteが実装するGraphicに色をつけるコンポーネントの実装とその元となるクラスを示します。

// 何かしらにuPaletteが作用するための大元のクラス
public abstract class ValueSynchronizerBase<T> : MonoBehaviour
{
        
    public abstract EntryId EntryId { get; }

    protected virtual void OnEnable()
    {
        StartObserving();
    }

    protected virtual void OnDisable()
    {
        StopObserving();
    }

    internal abstract Palette<T> GetPalette(PaletteStore store);

    protected abstract void OnValueChanged(T value);
}

// ValueSynchronizerBase<T>を継承した色の反映クラス
public abstract class ColorSynchronizer : ValueSynchronizer<UnityEngine.Color>
{
    [SerializeField] private ColorEntryId _entryId = new ColorEntryId();

    public override EntryId EntryId => _entryId;

    internal override Palette<UnityEngine.Color> GetPalette(PaletteStore store)
    {
        return store.ColorPalette;
    }
}

public abstract class ColorSynchronizer<T> : ColorSynchronizer where T : Component
{
    [SerializeField] [HideInInspector] private T _component;

    protected T Component
    {
        get
        {
            if (_component == null)
            {
                _component = GetComponent<T>();
            }

            return _component;
        }
    }
}

// Graphicを対象に色の反映を行うクラス
// Attributeはエディタ拡張で色を設定する際に対応するValueSynchronizerを見つけるために必要
[ColorSynchronizer(typeof(Graphic), "Color")]
public sealed class GraphicColorSynchronizer : ColorSynchronizer<Graphic>
{
    protected internal override UnityEngine.Color GetValue()
    {
        return Component.color;
    }

    protected internal override void SetValue(UnityEngine.Color value)
    {
        Component.color = value;
    }

    protected override bool EqualsToCurrentValue(UnityEngine.Color value)
    {
        return Component.color == value;
    }
}

独自のコンポーネントに色をいい感じにつける場合にはColorSynchronizerを継承した独自クラスを定義し、反映部分のロジックだけ実装すればいいわけです。 このように、uPaletteはシステム的にエンジニアが使いやすいように作られている点も個人的な推しポイントです。

ver2.0.0でさらに便利になったポイント

uPaletteは最近2.0としてメジャーアップデートがありました。実は1.0では「こうなると嬉しいな〜」と思っていた点があったのですが、いい感じに改修していただけました。 改修された点についてもいくつか紹介します。

enumが自動で生成されるように

前述にもあるように、uPaletteではstringのidをキーとして色を管理します。エンジニアとしてはstringのidをそのままコードに使うよりは何かしらの識別子が欲しいものです。 筆者も1.0の頃は独自のenumを定義しており、色の設定更新にともなってよしなにuPaletteが作るようにならないかなと思っていました。 2.0からは色の設定を更新すると、次のようなenumと付随する実装が生成されます。

namespace uPalette.Generated
{
    public enum ColorTheme
    {
        Default,
    }

    public static class ColorThemeExtensions
    {
        public static string ToThemeId(this ColorTheme theme)
        {
            switch (theme)
            {
                case ColorTheme.Default:
                    return "ef6ad2f2-968e-4e08-b17d-45be08828273";
                default:
                    throw new ArgumentOutOfRangeException(nameof(theme), theme, null);
            }
        }
    }

    public enum ColorEntry
    {
        ColorName1,
        ColorName2,
    }

    public static class ColorEntryExtensions
    {
        public static string ToEntryId(this ColorEntry entry)
        {
            switch (entry)
            {
                case ColorEntry.ColorName1:
                    return "1cb426b2-2b90-4234-b637-3bee8c7d3157";
                case ColorEntry.ColorName2:
                    return "c7c6fc92-47c0-4ff1-b0ec-4b507b25a97e";
                default:
                    throw new ArgumentOutOfRangeException(nameof(entry), entry, null);
            }
        }
    }
...

enumだけでなく、拡張メソッドとしてidへの変換まで用意してくれるのは親切です。 生成するかどうかの設定や生成先はProject Settingsから調整可能です。

Gradationやテキストのスタイルにも対応

色だけではなく、GradationやテキストのスタイルまでuPalette上で管理できるようになりました。

色同様にUIを量産する上で一元管理したい要素なのでこれもありがたい更新です。

一連の色の設定をまとめたThemeという単位で管理が可能に

個々の色の設定だけでなく、一定の設定をまとめてテーマという単位で取り扱うことも可能になりました。 こちらは公式のデモを見るのが一番わかりやすいです。

色はもちろん、文字のスタイルなどもテーマごとに一括で切り替わっているのがわかるかと思います。 たとえば画面ごとに全体の雰囲気を変える必要があるようなケースだったり、設定でダークモードなどの色のテーマを選択できるような機能を作りたい場合にはかなり重宝する機能です。

おわりに

uPaletteを活用することで、色の一元管理を行う機能をさくっとプロジェクトに導入することができます。 独自でこの辺りの機能を作るのが億劫なみなさまはぜひ一度試してみて欲しいです。

引用

github.com

【Unity】TextMeshProのアウトラインを理解する

はじめに

本記事で紹介および検証を行なっているツール、ライブラリのバージョンは次の通りです。

  • Unity 2021.3.1f1
  • com.unity.textmeshpro@3.0.6

バージョンによっては挙動に差異がある場合もありますので、ご了承ください。

TextMeshProのアウトライン

UIを作っていく上で必要になりがちなアウトライン表現ですが、TextMeshProでやる場合にはMaterialのパラメータで実現できます。

Thicknessの値を大きくすれば、アウトラインを太くすることが可能です。

適当にアウトラインをつけたいだけならこれでいいのですが、たとえば5pxのアウトラインをつけたい!ということをする際、パラメータをどう設定すればいいかいまいち掴めませんでした。 そこで、Forumを漁ってみると公式の説明が投稿されていました。

forum.unity.com

この説明をもとに、アウトラインの大きさや見え方の仕組みについて紹介します。

アウトラインの大きさ

まず最終的なアウトラインの大きさの式から示します。

アウトラインのサイズ(大体) = FontSize * Padding / Sampling Point Size * Thickness * 2

たとえばテキストのFontSizeが50px、参照するFontAssetのSampling Point Sizeが100px、Paddingが9px、そしてMaterialのThicknessが0.5であれば、アウトライン幅は50 * 0.09 * 0.5 * 2 = 4.5pxになります。ここで大体という注釈をつけているのは、内部計算で端数の切り捨てや補完の計算が働くケースもあるためです。詳細は本記事では省略するため、実際のシェーダーの中身を参照してもらうといいかと思います。

FontSize

そのままで、TextMeshProによるGUIコンポーネントに設定されたFont Sizeです。

Padding / Sampling Point Size

わかりにくいですが、FontAssetのGeneration SettingsにあるSampling Point SizeをPaddingで割ったものになります。 Sampling Point Sizeが100でPaddingが9であれば「サンプリングされている文字のサイズに対するパディングの割合値」は0.09といった形です。

Sampling Point SizeとPaddingはTextMeshProにおけるSDF Textureでの文字ひとつひとつのサイズと間隔を表します。Atlas Population Modeについて、動的にサンプリングするDynamicであればこの画像にもあるGeneration Settingsの値を変更して調整しますが、事前にサンプリングしておくStaticであればFont Asset Creatorによる生成時にバインドされます。

この生成時にも、SP/PD Ratioという値で出力されてたりします。

Thickness

冒頭でも説明した、MaterialのOutlineにあるパラメータです。

内側と外側のアウトライン

注意しなければならないのが、このアウトラインは文字の輪郭を中心軸に外側と内側に描画されることです。 なので、大きさを求める計算式では最後に2を掛けています。 また、アウトラインの見せ方にも注意が必要です。たとえば外側にアウトラインをつけたいだけだと、内側に描画されてしまう分文字部分が潰れてしまいます。 対応するにはMaterialのFaceにあるDilateというパラメータを調整します。Dilateは文字の太さの拡縮の役割があります。

以上のように、アウトラインの大きさや見え方を調整するにはさまざまなパラメータを考慮する必要があります。

まとめ

これまで雰囲気でTextMeshProのアウトラインをつけていましたが、TextMeshProのパラメータを理解するいい機会になりました。 文中でも言及しましたが、詳細なロジックは実際のTextMeshProのシェーダーで確認できます。興味のある方はぜひのぞいてみるのがおすすめです。 また、本記事に誤りがある場合には筆者までコメントなどで指摘いただけますと幸いです。

【Unity】TextMeshProのバージョンを更新してRectMask2Dのsoftnessを反映させる

本記事で紹介および検証を行なっているツールのバージョンは次の通りです。

  • Unity 2021.2.0f1
  • com.unity.textmeshpro@3.2.0-pre.2

バージョンによっては挙動に差異がある場合もありますので、ご了承ください。


RectMask2Dのsoftnessを使ってソフトマスクをかけていたところ、効果がTextMeshProに反映されていないことに気づきました。

f:id:myudon:20220313010801g:plain

issueを調べてみると、どうやらTextMeshPro3.2.0-pre.2以降で対応されているそうです。

issuetracker.unity3d.com

当然preview版のパッケージなので不具合が発生する可能性もあるのですが、更新することでsoftnessの問題は解決しそうです。(ちなみにこの記事を検証しているUnity2021.2.0f1のTextMesh Proのバージョンは3.0.6となります。) その場合、Package ManagerからTextMeshProのバージョンを上げる必要があります。 preview版なので、Add by package name...から直接入力します。

f:id:myudon:20220227213926p:plain:w500

f:id:myudon:20220227214242p:plain:w500

TextMesh Proのバージョンを上げたらEssential Resourcesも同時に更新する必要があります。 Window -> TextMesh Pro -> Import Essential Resourcesから更新してください。

f:id:myudon:20220313004743p:plain:w500

f:id:myudon:20220313005220p:plain:w500

以上の工程で正常にバージョンを上げると、softnessが反映されることが確認できます。

f:id:myudon:20220313010830g:plain

バージョン差分は公式のよりChangeLogより参照できるので、気になる方は確認してください。

docs.unity3d.com