Assets内のフォルダーにあるSpriteRendererで構成した2D Prefabを、1つずつ512×512pxのPNGへ書き出すEditor拡張を掲載します。Canvas上のUIはuGUI Image用の出力手順で条件を分けます。
仕様の参照は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のSpriteRenderer。sprite未設定と非Activeのルートはスキップ。ActiveなMesh・Particleなどが混在するPrefabもスキップ。 |
| 撮影範囲 | SpriteRenderer.boundsの外接範囲。正方形に収めるため長辺側へ合わせ、各辺に5%分の余白を加える。透明ピクセルの自動除去ではない。 |
| PNG寸法 | 512×512px。長細いPrefabでは短辺側に余白が残る。 |
| 保存名 | prefab_名前_GUID.png。同名の別PrefabをGUIDで区別し、実行ごとに別の出力フォルダーを作る。 |
| 描画方式 | Built-inはCamera.Render。SRPはStandardRequestのサポートを問い合わせ、未対応ならそのPrefabを失敗として記録。対応回答だけで色・透過・全Shaderの動作は保証しない。 |
単純な正面向きの2DスプライトとUnlit系のマテリアルを最初の確認対象にします。SpriteMask、独自Shader、照明を必要とするSprite、パーティクル、3D、アニメーションの特定フレームは別の撮影条件が必要です。
次は元記事にあった出力資料です。旧コードとは保存名、対象条件、フレームの計算が変わっています。

導入して1Prefabから試す
- プロジェクトを複製し、Assets/EditorにSpritePrefabPngExporter.csとして下の全文を保存します。旧PrefabScreenshotまたはUGUIPrefabScreenshotを使っていた場合は、旧メニューと新メニューを取り違えないよう旧スクリプトを外します。同じ新クラスを2つ置かないでください。
- asmdefを使っている場合はEditor専用Assemblyへ配置します。Consoleのコンパイルエラーを確認してから進みます。GameObjectへ付けるMonoBehaviourではありません。
- 確認用フォルダーに、Spriteが設定された有効なSpriteRendererだけのPrefabを1つ保存します。最初はMaskや独自スクリプトを外した単純なものを使います。
- Projectで確認用フォルダーを1つ選び、Tools → CG Method → Export Sprite 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/SpritePrefabPngExporter.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;
public static class SpritePrefabPngExporter
{
private const int Size = 512;
private const double Padding = 0.05;
[MenuItem("Tools/CG Method/Export Sprite 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, "sprite_" +
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, SpritePrefabPngExporterFormatting.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 renderers = copy.GetComponentsInChildren<Renderer>(false)
.Where(r => r.enabled && r.gameObject.activeInHierarchy).ToArray();
if (renderers.Any(r => !(r is SpriteRenderer)))
{ reason = "This sample accepts SpriteRenderer-only prefabs, not Mesh/Particle renderers."; return false; }
var sprites = renderers.OfType<SpriteRenderer>().Where(r => r.sprite != null).ToArray();
if (sprites.Length == 0) { reason = "No active enabled SpriteRenderer with a sprite."; return false; }
bounds = sprites[0].bounds;
foreach (var sprite in sprites.Skip(1)) bounds.Encapsulate(sprite.bounds);
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;
pw = ph = Size;
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)SpritePrefabPngExporterFormatting.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;
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 SpritePrefabPngExporterFormatting
{
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";
}
}
フレームとファイル名を確認した範囲
カメラの半高さは、縦の半分と「横幅÷出力aspect÷2」の大きい方です。512×512pxではaspectが1なので、長い方へ合わせます。画像を引き伸ばす処理ではなく、余白を含めて範囲を収める計算です。orthographicSizeの公式仕様とaspectの仕様を基準にしています。
掲載した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、Mesh/Particle混在はこの例の対象外。 |
| SRPのサポートエラー | 有効なパイプラインとCamera条件を確認。未対応をCamera.Renderへ無条件で戻して成功扱いにしない。 |
| 背景が黒い・全面透明・見た目が違う | RGBAを扱えるビューアー、Shader、Alpha、照明、パイプラインを確認。完全透明のPNGは警告を出すが、保存成功件数と描画品質の合格は別。 |
| 輪郭が切れる・余白が多い | Paddingとboundsを確認。SpriteMaskやShaderで変化する描画範囲は単純なboundsと分けて調べる。 |
| 同名・再実行で結果が見つからない | 名前_GUID.pngと実行ごとのOutputパスを確認。GUI上のPrefab名だけで結果を探さない。 |
| 途中で失敗・取消・保存できない | 候補と成功数を比べ、失敗パスと例外を確認。書込権限、長いパス、ディスク容量を確認。完了ログだけで全件成功と判断しない。 |
関連するPrefabの出力ツール
uGUI ImageのPNG出力、Prefab内部のGameObjectとコンポーネントCSV、Prefabファイルの保存階層CSVは出力対象が違います。見た目の確認と内部構造の一覧を使い分けてください。旧GitHub配布の01.csは改稿前のコードなので、今回の修正版は本文のコード全体を使用します。
エディタ拡張の記事は、ほかにもあります。




