概要
ゲーム画面のスクリーンショットを撮影したい、というのはよくある要望でしょう。
そしてさらに「特定のUIだけを除いて」撮影したい、というのもわりとある要望だと思います。
例えば操作用のUIは非表示だけども、ステータス表示などは表示しておきたい、などですね。
今回はそれを実現する方法について書きたいと思います。
なお、今回の記事の動作サンプルはGithubにアップしてあります。
実際に動かした動画↓
特定のUIだけ除いてキャプチャ pic.twitter.com/fy2g0Itg7a
— edom18@VR (@edo_m18) February 8, 2019
大まかなフロー
まず詳細を説明する前に、必要な項目について簡単に説明しておきます。
- GUI用のカメラを用意する
- 無視したいUIを持っているCanvasのレイヤーを適切に設定する
- CommandBufferを用いて、UI以外の要素がレンダリングされたあとのタイミングでバッファをコピーする
- GUI描画時に、カメラのレンダリングするレイヤー(
cullingMask
)を変更する - GUI用カメラを、(3)で取得したテクスチャに追加で描画する
という流れになります。
詳細については順に説明していきます。
GUI用カメラを用意する
Canvas要素はデフォルトではRender Mode
がScreen Space - Overlay
になっています。
これをまずScreen Space - Camera
に変更します。
すると以下のように、GUI要素をレンダリングするためのカメラを設定する項目が表示されるのでGUI用に用意したカメラを設定します。
なお、GUI用に用意したカメラではClear Flags
をDepth Only
に変更しておきます。
これでカメラ側の準備はOKです。
Canvasにレイヤーを設定する
続いて、Layerを設定していきます。
Cameraにはレイヤーごとにレンダリングするかしないか、というマスクが設定できるようになっており、これを利用してキャプチャ時に描画対象とするか、を切り替えます。
なので、まずは無視するUIのレイヤーを設定できるように新しいレイヤーを追加します。
(今回のサンプルではMenuUI
という名前にしました)
そして新しいレイヤーが追加できたら、キャプチャ時に無視したいUI Canvasにそのレイヤーを設定します。
なお、レイヤーマスクの設定で無視されるかどうかはCanvasのレイヤー設定のみが反映されるようです。
なので、UI要素に対して個別にレイヤーを設定してもレンダリングされてしまいます。
もし細かく制御したい場合は、Canvasを入れ子にして、そのCanvasにレイヤーを設定し、さらにそのCanvasの小要素としてキャプチャされたくないUI要素を入れることで対応することができます。
CommandBufferを用いてシーンの状況をキャプチャする
今回はシーンのキャプチャにCommandBuffer
を用いることにしました。
(もちろん、それ以外の方法でもキャプチャできます)
なお、CommandBuffer自体の詳細については凹みさんの記事がとても分かりやすく書かれているのでオススメです。
CommandBufferをカメラに登録する
CommandBufferは、カメラのレンダリングのパイプラインの中で、特定のイベントに紐づけて呼び出される「コマンド」を追加できる仕組みです。
今回はすべてのオブジェクトがレンダリングされ終わったタイミングでキャプチャしたかったのでCameraEvent.BeforeImageEffects
というタイミングでキャプチャを行っています。
コマンドバッファの生成は以下のようにしています。
/// <summary> /// バッファを生成する /// </summary> private void CreateBuffer() { _buf = new RenderTexture(Screen.width, Screen.height, 0); _commandBuffer = new CommandBuffer(); _commandBuffer.name = "CaptureScene"; _commandBuffer.Blit(BuiltinRenderTextureType.CurrentActive, _buf); }
行っていることは単純に、生成したRenderTextureに、現在の(CurrentActive
)状態をコピーしているだけです。
あとは、スクリーンショットを撮影するタイミングでこれをカメラに追加してやれば、適切なタイミングで処理が実行されます。
実際に実行すると以下のような形になります。
private void Update() { if (Input.GetKeyDown(KeyCode.S)) { TakeScreenshot(); } } /// <summary> /// スクリーンショットを撮影する /// </summary> public void TakeScreenshot() { Camera.main.AddCommandBuffer(_cameraEvent, _commandBuffer); StartCoroutine(WaitCapture()); } /// <summary> /// コマンドバッファの処理を待つ /// </summary> private IEnumerator WaitCapture() { yield return new WaitForEndOfFrame(); BlendGUI(); Camera.main.RemoveCommandBuffer(_cameraEvent, _commandBuffer); }
スクリーンショットを撮影したいタイミングでコマンドバッファをカメラに追加し、そのフレームの終わりまで待機した上でコマンドバッファを取り除いています。
ここで取り除いているのは、取り除かないと以後常にコマンドバッファが呼ばれてしまうため一度だけシーンをコピーしたら呼び出されないようにしています。
ここまでで、GUIを除く部分がRenderTextureに描かれている状態となります。
あとはGUI部分を適切に設定して描画してやれば目的の絵を得ることができます。
描画対象のGUIだけを上乗せする
最後は、必要なGUI部分を描くことができれば目的達成です。
GUI部分を描画するには以下のようにします。
/// <summary> /// GUI要素をブレンドする /// </summary> private void BlendGUI() { _guiCamera.targetTexture = _buf; int tmp = _guiCamera.cullingMask; _guiCamera.cullingMask = _captureTargetLayer; _guiCamera.Render(); _guiCamera.cullingMask = tmp; _guiCamera.targetTexture = null; }
上記でやっていることは、GUI用カメラのtargetTexture
に、コマンドバッファによってシーンがコピーされたバッファを適用し、また取り除きたいレイヤーを設定した上でGUI用カメラのレンダリングを行っています。
cullingMask
が、レンダリング対象となるかどうかのマスク情報を設定するレイヤーマスクです。
ここに、今回はMenuUI
レイヤーを持つ要素を外してレンダリング(_guiCamera.Render()
)しています。
最後にtargetTexture
とcullingMask
を元に戻して終わりです。
これで、最終的に得たい、不要なGUI要素が除かれた状態でシーンをキャプチャすることができます。
コード全文
最後に、今回のサンプルのコード全文を掲載しておきます。
なお、撮影したスクショをファイルに保存する処理についてはGithubのプロジェクトを参照してください。
(今回の主題からははずれるので割愛します)
using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.Rendering; public class SelectGUICapture : MonoBehaviour { [SerializeField, Tooltip("GUIをレンダリングしているカメラ")] private Camera _guiCamera = null; [SerializeField, Tooltip("キャプチャするタイミング")] private CameraEvent _cameraEvent = CameraEvent.BeforeImageEffects; [SerializeField, Tooltip("合成時に無視されるUIのレイヤー")] private LayerMask _captureTargetLayer = -1; private Camera _mainCamera = null; private RenderTexture _buf = null; private CommandBuffer _commandBuffer = null; #region ### MonoBehaviour ### private void Awake() { CreateBuffer(); } private void Update() { if (Input.GetKeyDown(KeyCode.S)) { TakeScreenshot(); } } /// <summary> /// 動作確認用にGizmoでテクスチャを表示する /// </summary> private void OnGUI() { if (_buf == null) return; GUI.DrawTexture(new Rect(5f, 5f, Screen.width * 0.5f, Screen.height * 0.5f), _buf); } #endregion ### MonoBehaviour ### /// <summary> /// バッファを生成する /// </summary> private void CreateBuffer() { _buf = new RenderTexture(Screen.width, Screen.height, 0); _commandBuffer = new CommandBuffer(); _commandBuffer.name = "CaptureScene"; _commandBuffer.Blit(BuiltinRenderTextureType.CurrentActive, _buf); } /// <summary> /// スクリーンショットを撮影する /// </summary> public void TakeScreenshot() { AddCommandBuffer(); StartCoroutine(WaitCapture()); } /// <summary> /// コマンドバッファの処理を待つ /// </summary> private IEnumerator WaitCapture() { yield return new WaitForEndOfFrame(); BlendGUI(); RemoveCommandBuffer(); } /// <summary> /// メインカメラにコマンドバッファを追加する /// </summary> private void AddCommandBuffer() { if (_mainCamera == null) { _mainCamera = Camera.main; } _mainCamera.AddCommandBuffer(_cameraEvent, _commandBuffer); } /// <summary> /// メインカメラからコマンドバッファを削除する /// </summary> private void RemoveCommandBuffer() { if (_mainCamera == null) { return; } _mainCamera.RemoveCommandBuffer(_cameraEvent, _commandBuffer); } /// <summary> /// GUI要素をブレンドする /// </summary> private void BlendGUI() { _guiCamera.targetTexture = _buf; int tmp = _guiCamera.cullingMask; _guiCamera.cullingMask = _captureTargetLayer; _guiCamera.Render(); _guiCamera.cullingMask = tmp; _guiCamera.targetTexture = null; } }