カメラからスクリーンショットを撮影(+URP対応)【Unity】

2021年11月11日

Unityでカメラから写った内容をスクリーンショットに撮影する方法を調べていたところ、Built-in(普通の)とURP(Universal Render Pipeline)でやり方が変わるらしく、苦戦したのでここに両方のやり方をまとめます。ついでに実践的に撮ったスクリーンショットをImage Raw(UI)に反映するところまで載せときます。

カメラからスクリーンショットを撮影

スクリーンショットの撮影はいくつか流派がありますが、今回は指定したカメラから撮影する方法で行います。コールバックを使ったりするので、利用例のサンプルも合わせて確認していただければと思います。

今回利用するスクリプト、クラス構成

今回はBuilt-in(いつもの)とURP両方対応するため、3つのスクリプトを合わせて利用します。クラス構成と各スクリプトはこちら。

using UnityEngine;
public abstract class ScreenshotHandlerBase : MonoBehaviour
{
	protected bool m_bTakeScreenshotNextFrame;
	protected System.Action<Texture2D> onScreenshotTaken;
	protected Vector2Int m_RequestSize;
	protected Camera m_Camera;
	protected virtual void setCameraTargetTexture(int _iWidth, int _iHeight)
	{
		m_RequestSize = new Vector2Int(_iWidth, _iHeight);
		if (m_Camera != null)
		{
			m_Camera.targetTexture = RenderTexture.GetTemporary(
				_iWidth,
				_iHeight, 16);
		}
	}
	public bool TakeScreenshot(int _iWidth, int _iHeight, System.Action<Texture2D> _onTaken)
	{
		if (m_bTakeScreenshotNextFrame)
		{
			Debug.LogError("Screen Shot can 1 time by 1 Render!");
			return false;
		}
		setCameraTargetTexture(_iWidth, _iHeight);
		m_bTakeScreenshotNextFrame = true;
		onScreenshotTaken = _onTaken;
		return true;
	}
	public bool TakeScreenshot(int _iWidth, int _iHeight)
	{
		return TakeScreenshot(_iWidth, _iHeight, (tex2d) => { });
	}
	public bool TakeScreenshotScreenSize(System.Action<Texture2D> _onTake)
	{
		return TakeScreenshot(Screen.width, Screen.height, _onTake);
	}
	public bool SaveScreenshot(int _iWidth, int _iHeight, string _strFilePath, System.Action _onFinished)
	{
		return TakeScreenshotScreenSize((tex2d) => {
			byte[] byteArr = tex2d.EncodeToPNG();
			System.IO.File.WriteAllBytes(_strFilePath, byteArr);
			_onFinished.Invoke();
		});
	}
	public bool SaveScreenshotScreenSize(string _strFilePath, System.Action _onFinished)
	{
		return SaveScreenshot(Screen.width, Screen.height, _strFilePath, _onFinished);
	}
	public void SaveScreenshotSimple()
	{
		SaveScreenshotScreenSize(Application.dataPath + "/current.png" , ()=> { });
	}
}
using UnityEngine;
[RequireComponent(typeof(Camera))]
public class ScreenshotHandlerBuiltin : ScreenshotHandlerBase
{
	private void Awake()
	{
		m_Camera = GetComponent<Camera>();
	}
	private void OnPostRender()
	{
		if (m_bTakeScreenshotNextFrame == false)
		{
			return;
		}
		m_bTakeScreenshotNextFrame = false;
		RenderTexture renderTexture = m_Camera.targetTexture;
		Texture2D renderResult = new Texture2D(
			renderTexture.width,
			renderTexture.height,
			TextureFormat.ARGB32,
			false);
		Rect rect = new Rect(0f, 0f, renderTexture.width, renderTexture.height);
		renderResult.ReadPixels(rect, 0, 0);
		onScreenshotTaken?.Invoke(renderResult);
		m_Camera.targetTexture = null;
	}
}
using UnityEngine;
using UnityEngine.Rendering;
public class ScreenshotHandlerURP : ScreenshotHandlerBase
{
	void OnEnable()
	{
		RenderPipelineManager.endCameraRendering += OnEndCameraRendering;
	}
	void OnDisable()
	{
		RenderPipelineManager.endCameraRendering -= OnEndCameraRendering;
	}
	private void OnEndCameraRendering(ScriptableRenderContext context, Camera camera)
	{
		if (m_bTakeScreenshotNextFrame)
		{
			m_bTakeScreenshotNextFrame = false;
			int iWidth = m_RequestSize.x;
			int iHeight = m_RequestSize.y;
			Texture2D screenshotTexture = new Texture2D(iWidth, iHeight, TextureFormat.ARGB32, false);
			Rect rect = new Rect(0, 0, iWidth, iHeight);
			screenshotTexture.ReadPixels(rect, 0, 0);
			screenshotTexture.Apply();
			onScreenshotTaken?.Invoke(screenshotTexture);
		}
	}
}

実際に使ってみた

上記スクリプトを利用して実際にスクリーンショットを作成する方法。今回はいずれのパターンもスクリーンショット画像をpngファイルで保存するパターンで行きます。いずれも上記3つのスクリプトはプロジェクトに入れている状態から開始してください。

Builtinバージョン

  • 撮影したいカメラにAddComponentでScreenshotHandlerBuiltinを追加してください。
  • ボタンなどからScreenshotHandlerBuiltin.SaveScreenshotSimple()を呼び出します。
  • プロジェクトAssets直下のフォルダ内にcurrent.pngファイルが保存されていることを確認。

SaveScreenshotSimpleはスクリーンのサイズのまま画像を保存します。SetDirtyを行っていないため、Unityの再生と一回止めないとファイルが認識されないかもしれません。確認する際は注意してください。

AddComponentはカメラにするべし

BuiltinバージョンではOnPostRenderを利用しているため、同インスペクターにCameraコンポーネントが存在しないと撮影がうまく出来ません。RequireComponentがあるので、カメラがついてないGameObjectにAddComponentすると大変なことになるぞ☆RequireComponentがどういったものかわからない場合は調べて見よう!(簡単に説明すると同インスペクター内に対象のコンポーネントが存在することを保証してくれます。)

URPバージョン(Universal Render Pipeline Version)

  • 空のGameObjectを用意する(Aとする)
  • AにAddComponentでScreenshotHandlerURPを追加
  • ボタンなどからScreenshotHandlerURP.SaveScreenshotSimple()を呼び出します。
  • プロジェクトAssets直下のフォルダ内にcurrent.pngファイルが保存されていることを確認。

注意!!
Unityエディター上で行う際、シーンカメラのレンダリング終わりをキャッチしてしまう場合があります。その場合シーンビューを非表示にした状態でアプリケーションを実行してください。(Gameビューのタブと切り替えで見えなくするとかでOK)

endCameraRenderingイベントを利用する

URPではOnPostRenderが使えません。
その代わり、UnityEngine.Renderで提供されているイベントを利用することが出来ます。今回はRenderPipelineManager.endCameraRenderingを利用して描画終了後の画面をスクリーンショットすることが出来ます。

撮影後のスクリーンショット情報を扱う

一応上のプログラムでスクリーンショット画像を保存することは出来るようになりました。他のメソッドでTexture2Dを取得することが出来るようにはしてあるので、頑張ってね!と言いたいところですが、具体例もらわないと分かりづらいですよね。一応汎用性を持たせるためにTexture2Dを返すまでにしてますので、どうやって使うかのサンプルも載せておきます。以下、統一してBuilt-Inバージョン

Raw Imageに画像を流し込む

UIとかで利用されるRaw Imageにスクリーンショットで撮影した画像を流し込むサンプルを作成しました。Githubにも同サンプルを上げてますのでスクリプトや指示内容だけでわからない場合はダウンロードなりクローンなりして確認してください。

https://github.com/everystudio/sample.screenshotbuiltin

  • 実際に使ってみた>Builtinバージョンを用意
  • シーン内にRaw Imageとボタンを用意(ボタンは同じものでもOK)
  • 下記スクリプトを空のGameObjectにAddComponent
  • インスペクターにScreenshotHandlerBuiltinとRawImageをセット
  • 用意してあるボタンからScreenshotSetter.TakeScreenshotを呼び出す。
using UnityEngine;
using UnityEngine.UI;
public class ScreenshotSetter : MonoBehaviour
{
	[SerializeField]
	private ScreenshotHandlerBuiltin m_screenshotHandler;
	[SerializeField]
	private RawImage m_img;
	public void TakeScreenshot()
	{
		m_screenshotHandler.TakeScreenshot(100,100,(tex2d) =>
		{
			Texture2D temp = new Texture2D(1, 1, TextureFormat.ARGB32, false);
			temp.LoadImage(tex2d.EncodeToPNG());
			m_img.texture = temp;
		});
	}
}

Saveボタン:現在の画面をスクリーンショットに撮ります
Setterボタン:撮影したスクリーンショットを右上のRaw Imageに表示します