Assets内のフォルダーにあるImageで構成したuGUI Prefabを、RectTransformの範囲からPNGへ書き出すEditor拡張を掲載します。SpriteRendererの2Dキャラクターやアイコンは2D Prefab用の出力手順を使います。
仕様の参照はUnity 6.3 LTS(6000.3)です。掲載コードの寸法・フレーム・保存名の整形クラスはWindowsの.NETでコンパイルして確認しました。Unityでのコンパイル、カメラ描画、PNG生成、URP・HDRPの動作は未確認です。まずプロジェクトの複製と最小のPrefabで試してください。元記事の画像は旧コードの資料で、今回の修正版を実行した画像ではありません。

対象にするPrefabと出力の仕様
| 項目・症状 | 条件・確認先 |
|---|---|
| 入力 | Projectで選んだAssets内の1フォルダーと、その子フォルダーの.prefab。複数選択、Scene、Packages、FBXなどのModel Prefabは対象外。 |
| 撮影対象 | ActiveかつenabledのuGUI Image。Text、RawImage、TMP等のGraphicやRendererが混在するPrefab、ルート以外のCanvasがあるPrefabはスキップ。 |
| 撮影範囲 | 各ImageのRectTransform四隅を合成した外接範囲。各辺に2%分の余白。Imageの不透明部分やMask適用後の輪郭を切り抜く処理ではない。 |
| PNG寸法 | 初期値はCanvasのワールド1単位を1pxとして計算。長辺2048px上限で密度を共通縮小し、最終ピクセル比率からカメラを合わせる。 |
| 保存名 | prefab_名前_GUID.png。同名の別PrefabをGUIDで区別し、実行ごとに別の出力フォルダーを作る。 |
| 描画方式 | Built-inはCamera.Render。SRPはStandardRequestのサポートを問い合わせ、未対応ならそのPrefabを失敗として記録。対応回答だけで色・透過・全Shaderの動作は保証しない。 |
この例はImageだけのUI部品用です。UI全体のスクリーンショット、ゲーム画面、文字を含むボタン、入れ子Canvasの撮影を同じコードへ任せる設計にはしていません。非Activeの要素は有効化せず、CanvasGroupで隠れたImageの矩形は範囲に残る場合があります。
次は元記事にあった出力資料です。旧コードとは保存名、対象条件、フレームの計算が変わっています。

Canvasと親の寸法を先に決める
- ルートがCanvasのPrefabでは、正の幅・高さを持つRectTransformを用意します。撮影用の複製ではCanvasをWorld Spaceにし、位置0・回転0・Scale 1へそろえ、CanvasScalerを無効にします。画面サイズに応じたScale With Screen Sizeを再現する処理ではありません。
- ルートがImage等でCanvasを持たないPrefabでは、撮影用の親Canvasを1920×1080単位で用意します。Stretchアンカーはこの親寸法を基準に配置されます。元のゲーム画面と同じ親寸法が必要ならReferenceWidthとReferenceHeightを変更してください。
- 複製のレイアウトを更新した後でImageの四隅を取得します。Content Size FitterやLayout Group、Mask、Preserve Aspect、Sliced、Filled、Outlineがある場合は、1Prefabずつ表示範囲と見た目を確認します。矩形と実際の描画輪郭が一致するとは限りません。
Canvasの描画モードと親子関係はuGUI公式のCanvas資料、レイアウト更新はForceUpdateCanvasesを参照します。GetWorldCornersが返すのは矩形の四隅です。
導入して1Prefabから試す
- プロジェクトを複製し、Assets/EditorにImagePrefabPngExporter.csとして下の全文を保存します。旧PrefabScreenshotまたはUGUIPrefabScreenshotを使っていた場合は、旧メニューと新メニューを取り違えないよう旧スクリプトを外します。同じ新クラスを2つ置かないでください。
- asmdefを使っている場合はEditor専用Assemblyへ配置します。UnityEngine.UIを参照できる構成が必要です。Consoleのコンパイルエラーを確認してから進みます。GameObjectへ付けるMonoBehaviourではありません。
- 確認用フォルダーに、幅と高さが正のImageだけのPrefabを1つ保存します。最初はMaskや独自スクリプトを外した単純なものを使います。
- Projectで確認用フォルダーを1つ選び、Tools → CG Method → Export Image Prefabs to PNGを実行します。Play Mode中は実行しません。
- 保存先には最初はAssetsの外の書込可能なフォルダーを選びます。保存ダイアログの取消ではファイルを作りません。処理中の取消は、それまでに出力したPNGを残して終了します。
- ConsoleのOutputパスを開き、候補・処理済み・成功・スキップ・失敗・取消の値を確認します。最小の対象では成功1とPNG1枚を期待します。画像の寸法、上下方向、輪郭切れ、背景のAlpha、色、余白を画像ビューアーで確認してください。
- 通常Sceneに別のSpriteやUIを置いたケース、同名Prefab2つ、横長・縦長、非Active、読取・書込失敗を追加します。処理後にSceneやPrefabのdirty状態とConsoleも確認します。
次のUnity画面も旧版の資料です。新しいメニュー名・出力フォルダー・ファイル名は、上の手順とコードに従います。

修正版コード全文
一時的なPreview SceneにPrefabを複製し、手動撮影用カメラを作ります。対象外は理由を付けてスキップし、個別の失敗はConsoleへ残して次へ進む構成です。AssetDatabase.Refreshは呼びません。Assets内へ保存した場合はEditorの監視によるImportが起きるため、最初はAssets外へ保存します。
// Unity 6.3 API-based sample. Unity compilation/rendering has not been verified.
// Save in Assets/Editor/ImagePrefabPngExporter.cs. Keep only one copy of this class.
using System;
using System.IO;
using System.Linq;
using UnityEditor;
using UnityEditor.SceneManagement;
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.SceneManagement;
using UnityEngine.UI;
public static class ImagePrefabPngExporter
{
private const double Density = 1.0; // Pixels per Canvas world unit.
private const int MaximumPixels = 2048;
private const double Padding = 0.02;
private const float ReferenceWidth = 1920f, ReferenceHeight = 1080f;
[MenuItem("Tools/CG Method/Export Image Prefabs to PNG")]
public static void Export()
{
if (EditorApplication.isPlayingOrWillChangePlaymode)
throw new InvalidOperationException("Use this command outside Play Mode.");
var selected = Selection.objects;
if (selected.Length != 1)
throw new InvalidOperationException("Select exactly one Assets folder.");
string folder = AssetDatabase.GetAssetPath(selected[0]);
if (!(folder == "Assets" || folder.StartsWith("Assets/", StringComparison.Ordinal)) ||
!AssetDatabase.IsValidFolder(folder))
throw new InvalidOperationException("Select a folder below Assets in the Project window.");
var paths = AssetDatabase.FindAssets("t:Prefab", new[] { folder })
.Select(AssetDatabase.GUIDToAssetPath)
.Where(p => p.EndsWith(".prefab", StringComparison.OrdinalIgnoreCase))
.Distinct().OrderBy(p => p, StringComparer.Ordinal).ToArray();
if (paths.Length == 0) { Debug.Log("No .prefab assets found: " + folder); return; }
string parent = EditorUtility.OpenFolderPanel("Choose PNG output folder", "", "");
if (string.IsNullOrEmpty(parent)) return;
string output = Path.Combine(parent, "ugui_" +
DateTime.UtcNow.ToString("yyyyMMdd_HHmmss") + "_" + Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(output);
int success = 0, skipped = 0, failed = 0, processed = 0;
bool cancelled = false;
try
{
foreach (string path in paths)
{
if (EditorUtility.DisplayCancelableProgressBar("Prefab PNG", path,
(float)processed / paths.Length)) { cancelled = true; break; }
try
{
var asset = AssetDatabase.LoadAssetAtPath<GameObject>(path);
if (asset == null) throw new InvalidOperationException("Prefab could not be loaded.");
string png = Path.Combine(output, ImagePrefabPngExporterFormatting.FileName(
Path.GetFileNameWithoutExtension(path), AssetDatabase.AssetPathToGUID(path)));
string reason;
if (Capture(asset, png, out reason)) success++;
else { skipped++; Debug.LogWarning("Skipped " + path + ": " + reason); }
}
catch (Exception error) { failed++; Debug.LogError("Failed " + path + ": " + error); }
finally { processed++; }
}
}
finally
{
EditorUtility.ClearProgressBar();
Debug.Log("Candidates=" + paths.Length + ", processed=" + processed +
", success=" + success + ", skipped=" + skipped + ", failed=" + failed +
", cancelled=" + cancelled + "\nOutput: " + output);
}
}
private static bool Capture(GameObject asset, string png, out string reason)
{
reason = "";
Scene preview = EditorSceneManager.NewPreviewScene();
RenderTexture previous = RenderTexture.active;
RenderTexture target = null;
Texture2D image = null;
Camera camera = null;
try
{
var copy = PrefabUtility.InstantiatePrefab(asset, preview) as GameObject;
if (copy == null) throw new InvalidOperationException("Could not instantiate prefab.");
if (!copy.activeInHierarchy) { reason = "Inactive prefab root."; return false; }
foreach (var c in copy.GetComponentsInChildren<Camera>(true)) c.enabled = false;
// Do not enable hidden objects or execute Play Mode animation.
Bounds bounds;
var canvases = copy.GetComponentsInChildren<Canvas>(true);
if (canvases.Any(c => c.gameObject != copy))
{ reason = "Nested or child Canvas is outside this sample's supported conditions."; return false; }
var graphics = copy.GetComponentsInChildren<Graphic>(false)
.Where(g => g.isActiveAndEnabled).ToArray();
if (graphics.Any(g => !(g is Image)))
{ reason = "This sample is Image-only: Text, RawImage and TMP need a separate capture design."; return false; }
if (copy.GetComponentsInChildren<Renderer>(false).Any(r => r.enabled))
{ reason = "Non-uGUI Renderer is outside the supported conditions."; return false; }
Canvas canvas = copy.GetComponent<Canvas>();
if (canvas == null)
{
var canvasObject = new GameObject("CG Method capture canvas", typeof(RectTransform));
SceneManager.MoveGameObjectToScene(canvasObject, preview);
canvas = canvasObject.AddComponent<Canvas>();
((RectTransform)canvas.transform).sizeDelta = new Vector2(ReferenceWidth, ReferenceHeight);
copy.transform.SetParent(canvas.transform, false);
}
else
{
var scaler = canvas.GetComponent<CanvasScaler>();
if (scaler != null) scaler.enabled = false; // Temporary copy only.
}
canvas.renderMode = RenderMode.WorldSpace;
canvas.transform.SetPositionAndRotation(Vector3.zero, Quaternion.identity);
canvas.transform.localScale = Vector3.one;
canvas.enabled = true;
var rootRect = (RectTransform)canvas.transform;
if (rootRect.rect.width <= 0 || rootRect.rect.height <= 0)
{ reason = "Set a positive root Canvas size in the prefab."; return false; }
Canvas.ForceUpdateCanvases();
LayoutRebuilder.ForceRebuildLayoutImmediate(rootRect);
Canvas.ForceUpdateCanvases();
var images = copy.GetComponentsInChildren<Image>(false)
.Where(i => i.isActiveAndEnabled).ToArray();
if (images.Length == 0) { reason = "No active enabled Image."; return false; }
bool first = true;
bounds = new Bounds();
var corners = new Vector3[4];
foreach (var item in images)
{
item.rectTransform.GetWorldCorners(corners);
foreach (var corner in corners)
{
if (first) { bounds = new Bounds(corner, Vector3.zero); first = false; }
else bounds.Encapsulate(corner);
}
}
if (bounds.size.x <= 0 || bounds.size.y <= 0)
{ reason = "Bounds have zero width or height."; return false; }
var cameraObject = new GameObject("CG Method capture camera");
SceneManager.MoveGameObjectToScene(cameraObject, preview);
camera = cameraObject.AddComponent<Camera>();
camera.enabled = false;
camera.scene = preview;
camera.clearFlags = CameraClearFlags.SolidColor;
camera.backgroundColor = new Color(0, 0, 0, 0);
camera.orthographic = true;
camera.allowHDR = false;
camera.allowMSAA = false;
camera.cullingMask = ~0;
int pw, ph;
ImagePrefabPngExporterFormatting.Dimensions(bounds.size.x * (1.0 + Padding * 2.0),
bounds.size.y * (1.0 + Padding * 2.0), Density, MaximumPixels, out pw, out ph);
double framedWidth = bounds.size.x * (1.0 + Padding * 2.0);
double framedHeight = bounds.size.y * (1.0 + Padding * 2.0);
camera.aspect = (float)pw / ph;
camera.orthographicSize = (float)ImagePrefabPngExporterFormatting.HalfHeight(framedWidth, framedHeight, pw, ph);
camera.transform.SetPositionAndRotation(new Vector3(bounds.center.x,
bounds.center.y, bounds.min.z - 10f), Quaternion.identity);
camera.nearClipPlane = 0.01f;
camera.farClipPlane = Mathf.Max(20f, bounds.size.z + 20f);
target = new RenderTexture(pw, ph, 24, RenderTextureFormat.ARGB32);
target.antiAliasing = 1;
if (!target.Create()) throw new InvalidOperationException("RenderTexture allocation failed.");
camera.targetTexture = target;
canvas.worldCamera = camera;
Canvas.ForceUpdateCanvases();
if (GraphicsSettings.currentRenderPipeline == null) camera.Render();
else
{
var request = new RenderPipeline.StandardRequest { destination = target };
if (!RenderPipeline.SupportsRenderRequest(camera, request))
throw new NotSupportedException("The active pipeline does not support StandardRequest. " +
"Check pipeline/camera configuration; do not assume SRP or alpha support.");
RenderPipeline.SubmitRenderRequest(camera, request);
}
RenderTexture.active = target;
image = new Texture2D(pw, ph, TextureFormat.RGBA32, false);
image.ReadPixels(new Rect(0, 0, pw, ph), 0, 0, false);
image.Apply(false, false);
byte[] bytes = image.EncodeToPNG();
if (bytes == null || bytes.Length == 0) throw new InvalidOperationException("PNG encoding failed.");
if (!image.GetPixels32().Any(pixel => pixel.a != 0))
Debug.LogWarning("PNG is fully transparent; inspect rendering conditions: " + png);
// Publish only a complete file. Do not replace an existing output.
string temporary = png + ".tmp";
bool ownsTemporary = false;
try
{
using (var file = new FileStream(temporary, FileMode.CreateNew, FileAccess.Write))
{
ownsTemporary = true;
file.Write(bytes, 0, bytes.Length);
}
File.Move(temporary, png);
}
finally
{
if (ownsTemporary && File.Exists(temporary)) File.Delete(temporary);
}
Debug.Log("Saved " + pw + "x" + ph + ": " + png);
return true;
}
finally
{
RenderTexture.active = previous;
if (camera != null) camera.targetTexture = null;
try
{
if (image != null) UnityEngine.Object.DestroyImmediate(image);
if (target != null) { target.Release(); UnityEngine.Object.DestroyImmediate(target); }
}
finally
{
// Close also if explicit resource cleanup throws.
if (preview.IsValid() && !EditorSceneManager.ClosePreviewScene(preview))
Debug.LogError("Could not close capture preview scene.");
}
}
}
}
public static class ImagePrefabPngExporterFormatting
{
public static void Dimensions(double width, double height, double density,
int limit, out int pixelsWide, out int pixelsHigh)
{
if (double.IsNaN(width) || double.IsInfinity(width) || width <= 0 ||
double.IsNaN(height) || double.IsInfinity(height) || height <= 0 ||
double.IsNaN(density) || double.IsInfinity(density) || density <= 0 || limit < 1)
throw new ArgumentOutOfRangeException("Positive finite dimensions required.");
double longest = Math.Max(width, height);
double scale = Math.Min(density, limit / longest);
pixelsWide = Math.Max(1, Math.Min(limit, (int)Math.Ceiling(width * scale)));
pixelsHigh = Math.Max(1, Math.Min(limit, (int)Math.Ceiling(height * scale)));
}
public static double HalfHeight(double width, double height, int pw, int ph)
{
return Math.Max(height / 2.0, width * ph / (2.0 * pw));
}
public static string FileName(string name, string guid)
{
if (string.IsNullOrEmpty(guid) || guid.Length != 32 ||
!guid.All(c => "0123456789abcdefABCDEF".IndexOf(c) >= 0))
throw new ArgumentException("Expected an AssetDatabase GUID.");
var safe = new string((name ?? "Prefab").Select(c =>
c < 32 || "<>:\"/\\|?*".IndexOf(c) >= 0 ? '_' : c).ToArray());
safe = safe.Trim().TrimEnd('.');
if (safe.Length == 0) safe = "Prefab";
if (safe.Length > 64) safe = safe.Substring(0, 64);
// Prefix also avoids reserved Windows base names such as CON.
return "prefab_" + safe + "_" + guid.ToLowerInvariant() + ".png";
}
}
フレームとファイル名を確認した範囲
幅と高さを別々に2048pxでClampすると出力aspectが変わり、縦だけにカメラを合わせたときに横が切れることがあります。共通の密度を下げ、整数pxへ切り上げた後のaspectで縦横の両方を収めます。幅400・高さ100・密度20・余白なしの式では2048×512px、幅100・高さ400では512×2048pxです。初期コードのDensityは1、Paddingは0.02なので、この式の例と初期値の出力寸法は区別してください。
掲載した2つの整形クラスそのものを.NETでコンパイルし、横長・縦長・正方形・極端な横長・小数・大きな寸法の12ケース、無効な幅8ケース、ファイル名6呼出しを確認しました。異なるGUIDの同名Prefabは別名になり、禁則文字とWindowsの予約名を考慮する形式です。Unity API部分のコンパイルやPNG描画を実行した結果ではありません。長い保存先パスやOSの権限は実環境で確認します。
Scene・描画方式・透過の注意点
NewPreviewSceneとCamera.sceneを使って撮影対象を指定します。finallyでRenderTexture.activeを元へ戻し、カメラのtargetTextureを外し、Texture2D・RenderTextureを破棄してPreview Sceneを閉じます。ReleaseだけではRenderTextureオブジェクトは破棄されません。
元Prefabを保存するAPIは呼びません。ただし、Prefabの複製時に動くEdit Modeの独自スクリプトやEditor処理の副作用まで無変更と保証する例ではありません。初回は複製で、通常Sceneの写り込み、生成物の残留、処理前後のdirty状態を確認してください。
SRPの分岐はStandardRequestとSupportsRenderRequestの仕様を参照したものです。URPではBase Cameraとカメラスタック、HDRPでは出力設定等の条件があります。このサンプルはパイプライン固有の追加設定やLightを用意せず、すべてのレンダラー・Shaderで同じ表示になると保証していません。
背景Alpha 0とRGBA32で読戻す設定を入れていますが、半透明・Alphaの保持は使用するパイプライン、マテリアル、後処理、画像ビューアーで確認します。ReadPixelsはGPUの完了待ちを伴い得るため、大量Prefabの速度は測定していません。EncodeToPNGのPNGはガンマ補正情報や色プロフィールを含まない仕様なので、色空間と比較用ビューアーもそろえて確認します。
出力できない・画像がおかしいとき
| 項目・症状 | 条件・確認先 |
|---|---|
| メニューがない・コンパイルエラー | .cs拡張子、Editorフォルダー/asmdef、クラス重複、Unity版を確認。旧版へそのまま適用できる保証はない。 |
| 選択エラー・候補0 | ProjectでAssets内のフォルダーを1つ選ぶ。.prefabと子フォルダーを確認。ファイルそのものやPackagesは選ばない。 |
| スキップがある | Consoleの対象パスと理由を確認。非Active、無対象、幅・高さ0、Image以外のGraphicや子Canvasはこの例の対象外。 |
| SRPのサポートエラー | 有効なパイプラインとCamera条件を確認。未対応をCamera.Renderへ無条件で戻して成功扱いにしない。 |
| 背景が黒い・全面透明・見た目が違う | RGBAを扱えるビューアー、Shader、Alpha、照明、パイプラインを確認。完全透明のPNGは警告を出すが、保存成功件数と描画品質の合格は別。 |
| 輪郭が切れる・余白が多い | Paddingとboundsを確認。RectTransformと実画像の余白、Mask、Outline、親Canvas寸法を分けて調べる。文字を含むUIは対象外。 |
| 同名・再実行で結果が見つからない | 名前_GUID.pngと実行ごとのOutputパスを確認。GUI上のPrefab名だけで結果を探さない。 |
| 途中で失敗・取消・保存できない | 候補と成功数を比べ、失敗パスと例外を確認。書込権限、長いパス、ディスク容量を確認。完了ログだけで全件成功と判断しない。 |
関連するPrefabの出力ツール
SpriteRendererの2D PNG出力、Prefab内部のGameObjectとコンポーネントCSV、Prefabファイルの保存階層CSVは出力対象が違います。見た目の確認と内部構造の一覧を使い分けてください。旧GitHub配布の01.csは改稿前のコードなので、今回の修正版は本文のコード全体を使用します。
エディタ拡張の記事は、ほかにもあります。




