背景を除外してRectTransformの範囲をファイルに保存する

Canvas 内の RectTransform の範囲を背景を無視して切り取って画像に保存する手順です。かなり限定的な状況となりますが uGUI の Image などを子要素に持つ RectTransform を表示されている内容で保存することができます。

まずサブカメラを作成 → RenderTexture にUIレイヤーだけをカメラ表示を出力するように設定しておきます。

この時カメラの BackGroundType は SliodColor で色は 0x00000000(全部ゼロ)にしておきます。

で、以下のコードから ScriptableObject を作成してインスペクターに必要な項目を設定しておきます(有料アセットのOdinを使ってます)

[CreateAssetMenu(menuName = "ScriptableObjects/Save RectTransform")]
public class SaveRectTransform : ScriptableObject
{
    //
    // Inspector
    // - - - - - - - - - - - - - - - - - - - -

    // カメラが映した映像(UIレイヤーのみ)
    [SerializeField] RenderTexture _renderTexture;

    // 保存対象が配置されているキャンバスオブジェクト
    [SerializeField] Canvas _targetCanvas;

    // ファイルとして保存する対象のオブジェクト
    [SerializeField] RectTransform _targetRectTransfrom;
    
    // 保存策ディレクトリ
    [SerializeField] string _saveDir = "Assets/Saved";

    
    CanvasScaler _canvasScaler;
    float _canvasWidth;
    float _canvasHeight;

    //
    // Methods
    // - - - - - - - - - - - - - - - - - - - -

    private void CheckObject()
    {
        if (_canvasScaler.uiScaleMode != CanvasScaler.ScaleMode.ScaleWithScreenSize)
        {
            throw new NotSupportedException
                ("CanvasScaler.ScaleMode は CanvasScaler.ScaleMode.ScaleWithScreenSize しかサポートしません。");
        }

        _canvasWidth = _canvasScaler.referenceResolution.x;
        _canvasHeight = _canvasScaler.referenceResolution.y;
        float renderTextureWidth = _renderTexture.width;
        float renderTextureHeight = _renderTexture.height;
        if (_canvasWidth != renderTextureWidth || _canvasHeight != renderTextureHeight)
        {
            throw new NotSupportedException
                ("RenderTexture と CanvasScaler のサイズが一致していません。");
        }
    }

    private byte[] CreatePngStream()
    {
        Texture2D tex = null;

        try
        {
            // 左下原点の大きさを取得
            Rect screenRect = _targetRectTransfrom.GetScreenRect();
            if (screenRect.width.HasFlac() || screenRect.height.HasFlac())
            {
                throw new NotSupportedException("対象の大きさに少数が設定されています。");
            }
            else if (screenRect.x.HasFlac() || screenRect.y.HasFlac())
            {
                throw new NotSupportedException("対象の位置に少数が設定されています。");
            }

            tex = new Texture2D((int)screenRect.width, 
                (int)screenRect.height, TextureFormat.RGBA32, false);

            RenderTexture.active = _renderTexture;
            var rect = 
                new Rect(screenRect.x, 
                    _canvasHeight - screenRect.y - screenRect.height,
                        screenRect.width, screenRect.height);

            tex.ReadPixels(rect, 0, 0);
            tex.Apply();

            return tex.EncodeToPNG();
        }
        finally
        {
            if (tex is not null)
            {
                DestroyImmediate(tex);
            }
        }
    }

    /// <summary>
    /// 設定された条件でファイルを保存します。
    /// </summary>
    [Button]
    public void Save()
    {
        _canvasScaler = _targetCanvas.GetComponent<CanvasScaler>();

        CheckObject();

        string outPath = Path.Combine(_saveDir, _targetRectTransfrom.gameObject.name) + ".png";
        byte[] pngStream = CreatePngStream();

        if (!Directory.Exists(_saveDir))
        {
            Directory.CreateDirectory(_saveDir);
        }
        
        File.WriteAllBytes(outPath, pngStream);

#if UNITY_EDITOR
        if (_saveDir.ToLower().StartsWith("assets"))
        {
            UnityEditor.AssetDatabase.
                ImportAsset(outPath, UnityEditor.ImportAssetOptions.ForceUpdate);
        }
#endif
        Debug.Log($"Completed. path={outPath}");
    }
}

public static class FloatExtension
{
    // 小数部だけ取得する
    public static float Frac(this float value) => value - Mathf.FloorToInt(value);
    // 小数部に数値を持っているか判定する
    public static bool HasFlac(this float value) => value == 0;
    // ** Not considering computer epsilon
}

public static class RectTransformExtension
{
    public static Rect GetScreenRect(this RectTransform self)
    {
        Canvas canvas = self.GetComponentInParent<Canvas>();
        Camera cam = canvas.worldCamera;
        
        var fc = new Vector3[4];
        self.GetWorldCorners(fc);
        if (cam != null)
        {
            fc[0] = RectTransformUtility.WorldToScreenPoint(cam, fc[0]);
            fc[2] = RectTransformUtility.WorldToScreenPoint(cam, fc[2]);
        }

        var rect = new Rect()
        {
            x = fc[0].x,
            y = fc[0].y
        };
        rect.width = fc[2].x - rect.x;
        rect.height = fc[2].y - rect.y;
        return rect;
    }
}

これでインスペクターに保存したい RectTransfrom を設定を指定して Save ボタンを押せばプロジェクト内位の所定の場所に RectTransform の範囲がクリップされて保存されます。

で、話には続きがあってこれ保存したら背景の黒色とアルファ加算されて画像のふちがガビガビになっちゃうので、保存したい Image はシェーダーの Blend を以下の通り変更します。

//Blend SrcAlpha OneMinusSrcAlpha
Blend One Zero

これで保存するとき背景色を無視して自分の色で保存されるようになります。