Unityで重なっている複数UIの合成結果を１枚のテクスチャとして取得する

# はじめに
実行時に複数のUIが重なった状態の画像を出力する必要があったので実装してみました。
UIが持つテクスチャの合成結果がほしいといった際に使えるテクニックとなります。
![capture0.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2743276/3f44a342-9ee4-fbe9-2eae-f212177fe794.gif)

上記画像では以下の2枚のテクスチャをImageで重ねて表示しており、それを別のRawImageにてテクスチャを表示させています。
別でテクスチャを表示させたり、テクスチャをファイル保存するなど用途にあった利用ができます。

後景画像
<img width="300" alt="background1.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2743276/061f41d1-5c19-2f5b-82f2-7793d99cbb29.png">

前景画像
<img width="300" alt="Sign_Okay.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2743276/678dc24c-91be-12a1-2b56-46fdabadbf60.png">

## 実現方法
配置済みUIのコピーを別に用意したCanvasに配置して、そのCanvasを投影するカメラのテクスチャをキャプチャすることで複数UIが重なった状態を1枚のテクスチャとして取得します。

## 実装手順

以下の手順で実現できます。

1. キャプチャ用のカメラとキャンバスを用意
1. キャプチャするスクリプトの用意
1. キャプチャ実行処理


## この方法のメリットとデメリット
実装へ入る前に、今回のやり方がすべてのニーズを満たすことができるものではないと認識しているので、仕様や環境にマッチするかの判断ができるようにメリットとデメリットを挙げておきます。

### メリット
* キャプチャしたいUIの上に不必要なUIが被っていても問題ない
必要なオブジェクトのみキャプチャすることができますし、GameObjectをコピーしたあと必要のないUIを削除してからキャプチャ撮ることもできます。
キャプチャのタイミングが非表示でも同じ様に可能です。

* 見た目が維持できる
UIに設定している画像、アルファ値、マテリアルもコピーされるので見た目そのままキャプチャできます。

### デメリット
* キャプチャ対象のUIをキャンバスにコピーする際の挙動を把握しておく必要がある
UIをコピーした際にUnityイベントのAwakeとStart、Updateやコピー削除時にOnDestroyが走ります。
これを前提とした作りになっていないと、不要な処理が走ったり、必要なオブジェクトを削除してしまうなどが考えられます。
それを考慮したスクリプトにしておく必要があります。　

* 描画を待つ必要がある
キャンバスに配置してカメラからテクスチャが取得できるようになるまで描画を待つ必要があります。
 リアルタイム性が必要なら事前にキャプチャを取得しておくなど対応が必要になります。

これらのメリットとデメリットからこの方法が適しているか判断するのがよいでしょう。



## 実装
それでは実装に移ります。
今回の内容は
Unity 2021.3.33f1
UniTask 2.5.3
にて動作確認しています。

### 構成
説明に使うヒエラルキーは以下の構成としています。
IconWindowパネルの上にIcon（キャラクター後景とキャラクターをImageで構成したもの）が配置されているイメージになります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2743276/7abb5740-d8e0-89ee-5045-990cc07ff83b.png)

シーンビューはこの様な配置です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2743276/e2141981-f167-73b9-4923-5d89c0100b4e.png)

今回はあえてIconの上にキャプチャするボタンを配置しています。
キャプチャボタンを押すとIconの右へ配置したRawImage（四角いボックス）にキャプチャしたテクスチャを表示させます。


それでは、内容に入ります。

まずは新規でカメラとキャンバスを追加します。
ここではそれぞれ名前をUICaptureCameraとUICaptureCanvasとしています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2743276/098eb328-d6a3-20c2-aeaa-271991d46ee7.png)

続いてカメラとキャンバスの設定です。

### カメラ
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2743276/2ddd4c78-52a0-8abd-6362-fcb5faa279d2.png)
デフォルトからの変更点を箇条書きにします。
* コンポーネントは「OFF」
キャプチャ時にしか必要ないので描画負荷を与えないようにします
* Clear Flagsを「Solid Color」
* Backgroundを「255,255,255,0」(α値を0にする）
テクスチャにしたときの背景カラーになります、α値を0️にしておくことでUIテクスチャ以外のピクセルが透過になります。
* Projectionを「Orthographic」
平行投影にすることでカメラ画角とキャプチャするUI表示矩形の一致が簡単になります。

### キャンバス
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2743276/3cd300dd-4cef-94cb-dc47-96c2a2fa82c0.png)
こちらも変更点を箇条書きにします。
* コンポーネントは「OFF」
* RenderModeを「Screen Space - Camera」
　カメラを設定できるようにして、先程追加設定したカメラをRenderCameraに設定します。
* このあと紹介するキャプチャ処理を記述したスクリプト「UICapture」を追加　

## UICapture(UIをコピーしてキャプチャするスクリプト)
このスクリプトをキャプチャ用キャンバスへAddComponentする想定にしています。
※必要な部分を簡潔に見られるようnullチェックやエラー処理などは端折っています

```C#:UICapture.cs
using Cysharp.Threading.Tasks;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

[RequireComponent(typeof(Canvas))]
public class UICapture : MonoBehaviour
{
    /// <summary>
    /// キャプチャするUIオブジェクトが配置されるキャンバス
    /// </summary>
    private Canvas captureTargetCanvas = null;

    /// <summary>
    /// キャンバスを投影するカメラ
    /// </summary>
    private Camera captureCamera = null;


    private void Awake()
    {
        captureTargetCanvas = GetComponent<Canvas>();
        captureCamera = captureTargetCanvas.worldCamera;

        captureTargetCanvas.enabled = false;
        captureCamera.enabled = false;
    }

    /// <summary>
    /// 指定のオブジェクトをコピーしてテクスチャを返す
    /// </summary>
    /// <param name="captureTargetGameObject"></param>
    /// <returns></returns>
    public async UniTask<RenderTexture> Capture(GameObject captureTargetGameObject)
    {
        captureTargetCanvas.enabled = true;
        captureCamera.enabled = true;

        //対象のオブジェクトのコピーを作成してキャンバスに中央配置
        GameObject copiedObject = Instantiate(captureTargetGameObject, captureTargetCanvas.gameObject.transform);
        var rectTransform = copiedObject.GetComponent<RectTransform>();
        rectTransform.pivot = new Vector2(0.5f, 0.5f);
        rectTransform.anchoredPosition = new Vector3(0, 0, 0);
        rectTransform.anchorMin = new Vector2(0.5f, 0.5f);
        rectTransform.anchorMax = new Vector2(0.5f, 0.5f);

        //オブジェクトと同等サイズのテクスチャを作成
        Vector2 size = rectTransform.sizeDelta;
        RenderTexture captureTexture = new RenderTexture((int)(size.x + 0.5f), (int)(size.y + 0.5f), 0);
        captureTexture.wrapMode = TextureWrapMode.Clamp;
        captureTexture.filterMode = FilterMode.Bilinear;

        captureCamera.targetTexture = captureTexture;
        //カメラのサイズをUIに合わせる
        //(平行投影時には縦画面半分のUnit数を設定する、SpriteのPixels Per Unitを100以外にしていればこの計算式も変更する必要がある）
        captureCamera.orthographicSize = size.y / 100.0f / 2;

        //描画されるまで待つ
        await UniTask.WaitForEndOfFrame(this);

        captureCamera.targetTexture = null;

        captureTargetCanvas.enabled = false;
        captureCamera.enabled = false;

        //コピーしたオブジェクトは削除
        Destroy(copiedObject);

        //キャプチャしたテクスチャを返す
        return captureTexture;

    }
}
```
CaptureメソッドにキャプチャしたいUIのGameObjectを渡すことでRenderTextureが返るようにしました。
今回はコピーとキャプチャを一つのメソッドにしてしまっていますが、オブジェクトのコピー処理とキャプチャ処理を分割すれば、自由度が上がります。


### キャプチャ対象
今回はヒエラルキー上のIconをキャプチャ対象とします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2743276/a2c204ab-d0f7-b583-5955-badc772b4fae.png)


冒頭で紹介した後景と前景がそれぞれBackとCharacterにImageとして配置されています。
若干見栄えを良くするのに円形のMaskをIconPartsに入れてます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2743276/dbd5f038-1da8-34e9-183f-ed377e65622a.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2743276/949f107f-b102-8b88-3ad9-cb17a8586b3c.png)

### キャプチャするコントローラー
実際にキャプチャして画面に反映するロジックを構築していきます。
ここの処理はそれぞれご自身の仕様や環境にあったものを用意してください。
今回は説明用にすべてのオブジェクトを決め打ちとして、ボタンを押すことでキャプチャを実行する形としました。

```C#:CaptureController.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using UniRx;
using Cysharp.Threading.Tasks;

public class CaptureController : MonoBehaviour
{
    /// <summary>
    /// キャプチャボタン
    /// </summary>
    [SerializeField]
    private Button captureButton = null;

    /// <summary>
    /// キャプチャするUIオブジェクト
    /// </summary>
    [SerializeField]
    private GameObject captureTarget = null;

    /// <summary>
    /// キャプチャ処理を行うクラス
    /// </summary>
    [SerializeField]
    private UICapture capture = null;

    /// <summary>
    /// キャプチャ後の画像表示
    /// </summary>
    [SerializeField]
    private RawImage capturedTextureImage = null;


    // Start is called before the first frame update
    void Start()
    {
        //キャプチャボタン押されたときの処理
        captureButton.OnClickAsObservable().TakeUntilDestroy(this).Subscribe( async _ => await OnCapture());
    }

    /// <summary>
    /// キャプチャ処理
    /// </summary>
    /// <returns></returns>
    private async UniTask OnCapture()
    {
        //キャプチャしたテクスチャをRawImageにセットする
        RenderTexture capturedTexture = await capture.Capture(captureTarget);
        DestroyCapturedTexture();
        capturedTextureImage.texture = capturedTexture;
        capturedTextureImage.SetNativeSize();
    }

    /// <summary>
    /// テクスチャ削除
    /// </summary>
    private void DestroyCapturedTexture()
    {
        if (capturedTextureImage.texture != null)
        {
            Destroy(capturedTextureImage.texture);
            capturedTextureImage.texture = null;
        }
    }

    /// <summary>
    /// 破棄
    /// </summary>
    private void OnDestroy()
    {
        DestroyCapturedTexture();
    }
}
```
このスクリプトを任意のオブジェクトにAddComponentします。
今回は説明しやすいように決め打ちで設定していますが、実際に使用する場合には、仕様や環境にあった方法でキャプチャのタイミングやオブジェクトを指定するようにしましょう。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2743276/574d8cc5-b573-0ea1-c1f2-dc3f360403c8.png)

実行してキャプチャボタンを押すと冒頭での動きになります。
![capture0.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2743276/3f44a342-9ee4-fbe9-2eae-f212177fe794.gif)
あとは、このテクスチャを画像ファイルとして保存したり、シェーダーで効果をつけて表示するなどといったことができます。


# まとめ
いかがでしたでしょうか。

今回紹介した方法はデメリットも存在するものの、オブジェクトをコピーするので画面上に影響を与えずキャプチャが可能になります。
選択枠を外してキャプチャしたい、非表示のオブジェクトをキャプチャしたいといったニーズにも対応できる手法だと思います。

オブジェクトをD&Dする際のドラッグ中画像を生成してもよいですし、エディタ拡張に組み込んで選択しているのUIオブジェクトのテクスチャをファイルに出力するなんかも可能になります。
カメラの設定とスクリプトを修正すれば、UIでなくても応用が効きそうです。

使い方は色々ありますので、是非工夫してみてください。

## ライセンス
![image.png](http://unity-chan.com/images/imageLicenseLogo.png)
本コンテンツは[ユニティちゃんライセンス条項](http://unity-chan.com/contents/license_jp/ "ユニティちゃんライセンス条項")の元に提供されています
© Unity Technologies Japan/UCL