📸
【Unity】フレーム落ちしないUnityカメラ画像のGPUキャプチャ
はじめに
こんにちは、まつさこ です。
この記事では、Unityアプリにおけるランタイムでのカメラ画像のキャプチャを、フレーム落ちすることなく実行させる方法を紹介します。
GPUからピクセルデータを非同期で読み取ることで、ゲームのメイン処理を圧迫することなく、指定したカメラのキャプチャ画像を保存することが出来ます。
開発環境
筆者の開発環境は以下です。
- Unity 2021.3.5f1
- Rider 2023.2.3
また、UniTask
ライブラリを使用しています。
スクリプトの紹介
早速、該当のサンプルコードを紹介します。
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Cysharp.Threading.Tasks;
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.SceneManagement;
public class ScreenshotHandler : MonoBehaviour
{
// カメラの参照
[SerializeField] Camera camera;
// 保存するスクリーンショットの上限数
const int UPPER_LIMIT_SAVE_PICTURE = 5;
// スクリーンショットのファイル形式
const string PNG = ".png";
// スクリーンショットを撮影し、保存するメソッド
public void CaptureScreenshotWithAsyncGPUReadback(int width = 1920, int height = 1080)
{
// カメラの描画結果を一時的に保存するためのRenderTextureを作成
var rt = RenderTexture.GetTemporary(width, height, 24, RenderTextureFormat.ARGB32);
var oldTarget = camera.targetTexture;
// カメラの描画先を一時的に作成したRenderTextureに変更して、レンダリング
camera.targetTexture = rt;
camera.Render();
// カメラの描画先を元に戻す
camera.targetTexture = oldTarget;
// GPUからピクセルデータを非同期で読み取る
AsyncGPUReadback.Request(rt, 0, async request =>
{
if (request.hasError)
{
// 読み取りにエラーがあった場合はログを出力
Debug.LogError("AsyncGPUReadbackにエラーが発生しました。");
}
else
{
// 現在のシーン名を使用してファイルパスを生成
string path = SceneManager.GetActiveScene().name;
// 保存ディレクトリからファイルパスのリストを取得
List<string> imageFilePaths = GetAllFileFromDirectory(GetSaveDirectoryPath(path));
// ファイル数が上限に達していた場合、最も古いファイルを削除
if (imageFilePaths.Count >= UPPER_LIMIT_SAVE_PICTURE)
{
File.Delete(imageFilePaths[0]);
}
// リクエストから生のピクセルデータを取得
var data = request.GetData<Color32>();
var format = rt.graphicsFormat;
// 画像を保存するための完全なファイルパスを生成
var saveFilePath = GetSaveFilePath(path, PNG);
// 別のスレッドでピクセルデータをPNGにエンコード
var bytes = await UniTask.RunOnThreadPool(() =>
{
var bytes = ImageConversion.EncodeNativeArrayToPNG(data, format, (uint)width, (uint)height);
return bytes;
});
// エンコードされたバイトを配列に変換
var pngBytes = bytes.ToArray();
// 別のスレッドでPNGデータをファイルに書き込む
await UniTask.RunOnThreadPool(async () =>
{
using var fs = new FileStream(saveFilePath, FileMode.Create, FileAccess.Write);
{
await fs.WriteAsync(pngBytes, 0, pngBytes.Length);
}
});
// 必要ない一時的なRenderTextureを解放
RenderTexture.ReleaseTemporary(rt);
}
});
}
// "ディレクトリ配下のファイル"が全て入ったリストを返す
// 最も古いファイルが[0]番目
List<string> GetAllFileFromDirectory(string directoryName)
{
//古いものが先頭にくるようにファイルをソート
List<string> imageFilePathList = Directory
//Imageディレクトリ内の全ファイルを取得
.GetFiles(directoryName, "*", SearchOption.AllDirectories)
//.DS_Storeは除く
.Where(filePath => Path.GetFileName(filePath) != ".DS_Store")
//日付順に降順でソート
.OrderBy(filePath => File.GetLastWriteTime(filePath).Date)
//同じ日付内で時刻順に降順でソート
.ThenBy(filePath => File.GetLastWriteTime(filePath).TimeOfDay)
.ToList();
return imageFilePathList;
}
// 保存ディレクトリのパスを取得するメソッド
string GetSaveDirectoryPath(string folderName)
{
string directoryPath = Application.persistentDataPath + "/" + folderName + "/";
if (!Directory.Exists(directoryPath))
{
//まだ存在してなかったら作成
Directory.CreateDirectory(directoryPath);
return directoryPath;
}
return directoryPath;
}
// 保存先のファイルのパス取得
string GetSaveFilePath(string folderName, string fileType)
{
return GetSaveDirectoryPath(folderName) + DateTime.Now.ToString("yyyyMMddHHmmss") + fileType;
}
}
スクリプト内の各処理は、コメントアウトに記載の通りです。
カメラのRenderTextureを一時的に作成し、そのピクセルデータをGPUから非同期で読み取ることで、軽量な処理を実現しています。
CaptureScreenshotWithAsyncGPUReadback()
メソッドを実行することで、 persistentDataPath/シーン名
フォルダに 20231113150030.png
などの名前でスクリーンショットが保存されます。
まとめ
この記事では、Unityアプリにおけるランタイムでのカメラ画像のキャプチャを、フレーム落ちすることなく実行させる方法を紹介しました。
読んでくださりありがとうございました🤗
Discussion