UnityのGUILayout.Boxを角丸のまま背景色を付ける方法



# はじめに
UnityのIMGUIの`GUILayout.Box`は手軽にUIを作れるので何かと便利ですが、`GUI.backgroundColor` が効かず、背景色を変えるには`Texture2D`を指定します。しかし、`Texture2D.redTexture;` のようなデフォルトのテクスチャを指定すると、元々角丸だったBoxの角が四角になって、形が変わってしまいます。さらに、枠線も消えてしまいます。

Boxの形状をそのままに背景の色を色々と設定する方法を探しても見つからなかったので、力技で作成してみました。

## 使用例
以下のコードのように、`GUILayout.Box` の代わりに `ColoredBoxHelper.ColoredBox` を呼び出して使用します。

``` ColoredBoxTest.cs
using UnityEngine;

public class ColoredBoxTest : MonoBehaviour
{
    private void OnGUI()
    {
        GUILayout.Box("デフォルトのボックス");
        
        ColoredBoxHelper.ColoredBox("色付きのボックス", Color.magenta);
        ColoredBoxHelper.ColoredBox("半透明のボックス\n\t半透明のボックス\n\t半透明のボックス", new Color(0f, 1f, 0f, 0.2f));
        ColoredBoxHelper.ColoredBox("半透明 + 文字色", new Color(0f, 1f, 0f, 0.2f), Color.black);
    }
}
```

すると、以下のように角丸のまま指定した背景色が表示されます。
![スクリーンショット 2023-03-25 184639.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/606045/68ea9bd5-5c6b-d996-d1f2-11bffa5f79d9.jpeg)

Boxの形状は手動で作成しているので、元のBoxとの細かい差異はあると思います。

### コード
利用するには以下のコードをプロジェクト内に追加します。

``` ColoredBoxHelper.cs
using System.Collections.Generic;
using UnityEngine;

public static class ColoredBoxHelper
{
    /* config */
    // use texture2D cache for each color ever used. 
    private static readonly bool EnableCacheTexture2D = true;
    // radius value for rounding box corners
    private static readonly float CornerRadius = 3f;


    /// <summary>
    /// Utility method to use colored GUILayout.Box(...);
    /// Usage:
    /// ColoredBoxHelper.ColoredBox(text, new new Color(1f, 0f, 0f, 0.4f));
    /// </summary>
    public static void ColoredBox(string text, Color boxColor)
    {
        ColoredBox(text, boxColor, Color.white);
    }

    /// <summary>
    /// Utility method to use colored GUILayout.Box(...);
    /// Usage:
    /// ColoredBoxHelper.ColoredBox(text, new new Color(1f, 0f, 0f, 0.4f));
    /// </summary>
    public static void ColoredBox(string text, Color boxColor, Color textColor)
    {
        var originalColor = GUI.contentColor;
        GUI.contentColor = textColor;
        GUILayout.Box(text, GetCustomBoxStyle(boxColor, textColor));
        GUI.contentColor = originalColor;
    }

    private static GUIStyle GetCustomBoxStyle(Color boxColor, Color textColor)
    {
        var style = new GUIStyle(GUI.skin.box);
        style.normal.background = GetOrCreateTexture(boxColor);
        style.normal.textColor = textColor;
        return style;
    }

    private static Dictionary<Color, Texture2D> _texture2DCache = new Dictionary<Color, Texture2D>();

    private static Texture2D GetOrCreateTexture(Color boxColor)
    {
        if (!EnableCacheTexture2D)
            return GenerateRoundedCornerTexture2D(boxColor);

        if (!_texture2DCache.ContainsKey(boxColor))
        {
            _texture2DCache[boxColor] = GenerateRoundedCornerTexture2D(boxColor);
        }

        return _texture2DCache[boxColor];
    }

    private static Texture2D GenerateRoundedCornerTexture2D(Color color)
    {
        int width = 64;
        int height = 64;

        Texture2D texture = new Texture2D(width, height);
        Color[] pixels = new Color[width * height];

        float radius = CornerRadius;

        for (int x = 0; x < width; x++)
        {
            for (int y = 0; y < height; y++)
            {
                int dy = Mathf.Min(y, height - 1 - y);

                if (dy < radius)
                {
                    int dx = Mathf.Min(x, width - 1 - x);
                    var r = Mathf.Sqrt((radius - dx) * (radius - dx) + (radius - dy) * (radius - dy));
                    
                    if (r > radius && dx < radius) continue; // 角の円の外のpixelを除外

                    bool IsBoarder()
                    {
                        if (dx > radius) return false;

                        var rdx = Mathf.Sqrt((radius - dx + 1) * (radius - dx + 1) + (radius - dy) * (radius - dy));
                        var rdy = Mathf.Sqrt((radius - dx) * (radius - dx) + (radius - dy + 1) * (radius - dy + 1));
                        return (rdx > radius || rdy > radius);
                    }

                    if (IsBoarder())
                    {
                        pixels[x + y * width] = Color.black;
                        continue;
                    }
                }

                bool isBoarder = x == 0 || y == 0 || x == width - 1 || y == height - 1;
                Color pixel = isBoarder ? Color.black : color;
                pixels[x + y * width] = pixel;
            }
        }

        for (int x = 1; x < width - 1; x++)
        {
            for (int y = 1; y < height - 1; y++)
            {
                if (pixels[x + y * width] == Color.black)
                {
                    bool hasNearbyBoarderY = false;
                    bool hasNearbyBoarderX = false;
                    
                    if (pixels[x + 1 + y * width] == Color.black) hasNearbyBoarderX = true;
                    if (pixels[x + (y+1) * width] == Color.black) hasNearbyBoarderY = true; 
                    if (pixels[x-1 + y * width] == Color.black) hasNearbyBoarderX = true;
                    if (pixels[x + (y-1) * width] == Color.black) hasNearbyBoarderY = true;

                    if (hasNearbyBoarderX && hasNearbyBoarderY)
                    {
                        // prune angular boarder pixel;
                        pixels[x + y * width] = Color.clear;
                    }
                }
            }
        }

        texture.SetPixels(pixels);
        texture.Apply();

        return texture;
    }
}
```

- Texture2D を pixel単位で手動生成して、力技で角丸四角形を描画しています。
- 毎フレームTextureを作成するのは気がひけるので、デフォルトでは作成したTexture2Dをキャッシュします。
- コード内部の`CornerRadius` などの値を調整することで角の丸みを調整することもできます。ただ、丸みを大きくすると、曲線の歪みが目立ちます。
- 後処理で枠線をなめらかにしようとして頑張っています