noradninja

PSVitaTextureOptimizer

Dec 24th, 2025 (edited)
338
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
C# 18.93 KB | Source Code | 0 0
  1. using System;
  2. using System.IO;
  3. using System.Text;
  4. using System.Collections.Generic;
  5. using UnityEditor;
  6. using UnityEngine;
  7. using UnityEngine.Profiling;
  8.  
  9. public class PSVitaTextureOptimizer : EditorWindow
  10. {
  11.     // ============================
  12.     // UI State
  13.     // ============================
  14.  
  15.     static readonly int[] kMaxSizes = { 16, 32, 64, 128, 256, 512, 1024, 2048 };
  16.     static readonly string[] kMaxSizeLabels = { "16", "32", "64", "128", "256", "512", "1024", "2048" };
  17.  
  18.     static readonly TextureImporterFormat[] kRGBFormats =
  19.     {
  20.         TextureImporterFormat.PVRTC_RGB2,
  21.         TextureImporterFormat.PVRTC_RGB4
  22.     };
  23.  
  24.     static readonly TextureImporterFormat[] kRGBAFormats =
  25.     {
  26.         TextureImporterFormat.PVRTC_RGBA2,
  27.         TextureImporterFormat.PVRTC_RGBA4
  28.     };
  29.  
  30.     int maxSizeIndex = 4;   // 256
  31.     int rgbFormatIndex = 1;
  32.     int rgbaFormatIndex = 1;
  33.  
  34.     bool generateMipmaps = false;
  35.     bool convertAllTextures = true;
  36.     bool forcePSP2MaxSize = false;
  37.     bool addBackupToGitIgnore = true;
  38.  
  39.     const string BackupRoot = "Assets/PSVita/TextureBackup";
  40.  
  41.     // ============================
  42.     // Menu
  43.     // ============================
  44.  
  45.     [MenuItem("PSVita/PSP2 2PVRTC")]
  46.     static void Open()
  47.     {
  48.         GetWindow<PSVitaTextureOptimizer>("PSP2 2PVRTC");
  49.     }
  50.  
  51.     // ============================
  52.     // UI
  53.     // ============================
  54.  
  55.     void OnGUI()
  56.     {
  57.         GUILayout.Label("Configuration", EditorStyles.boldLabel);
  58.  
  59.         maxSizeIndex = EditorGUILayout.Popup("Max Texture Size", maxSizeIndex, kMaxSizeLabels);
  60.         rgbFormatIndex = EditorGUILayout.Popup("RGB Textures", rgbFormatIndex, new[] { "PVRTC RGB 2bpp", "PVRTC RGB 4bpp" });
  61.         rgbaFormatIndex = EditorGUILayout.Popup("RGBA Textures", rgbaFormatIndex, new[] { "PVRTC RGBA 2bpp", "PVRTC RGBA 4bpp" });
  62.  
  63.         generateMipmaps = EditorGUILayout.Toggle("Generate Mipmaps", generateMipmaps);
  64.         forcePSP2MaxSize = EditorGUILayout.Toggle("Force PSP2 Max Size (Override Existing)", forcePSP2MaxSize);
  65.  
  66.         GUILayout.Space(6);
  67.         EditorGUILayout.HelpBox(
  68.             "Rules:\n" +
  69.             "• UI / Sprites → PVRTC RGBA 4bpp (forced)\n" +
  70.             "• Normal Maps → PVRTC RGB 4bpp (forced, mipmaps ON)\n" +
  71.             "• Color Textures → RGB or RGBA based on Import Alpha\n" +
  72.             "• PSVita publishing folders are auto-excluded\n" +
  73.             "• When force is OFF, textures are NEVER upscaled",
  74.             MessageType.Info
  75.         );
  76.  
  77.         GUILayout.Space(8);
  78.  
  79.         convertAllTextures = EditorGUILayout.Toggle("Convert ALL textures", convertAllTextures);
  80.         addBackupToGitIgnore = EditorGUILayout.Toggle("Add backup folder to .gitignore", addBackupToGitIgnore);
  81.  
  82.         GUILayout.Space(10);
  83.  
  84.         if (GUILayout.Button("Analyze"))
  85.             AnalyzeTextures();
  86.  
  87.         if (GUILayout.Button("Backup"))
  88.             BackupTextures();
  89.  
  90.         if (GUILayout.Button("Optimize"))
  91.             OptimizeTextures();
  92.  
  93.         if (GUILayout.Button("Restore From Backup"))
  94.             RestoreFromBackup();
  95.     }
  96.  
  97.     // ============================
  98.     // Texture Enumeration
  99.     // ============================
  100.  
  101.     string[] GetTargetTextureGUIDs()
  102.     {
  103.         if (convertAllTextures)
  104.             return AssetDatabase.FindAssets("t:Texture");
  105.  
  106.         UnityEngine.Object[] selected =
  107.             Selection.GetFiltered(typeof(Texture), SelectionMode.DeepAssets);
  108.  
  109.         string[] guids = new string[selected.Length];
  110.         for (int i = 0; i < selected.Length; i++)
  111.             guids[i] = AssetDatabase.AssetPathToGUID(
  112.                 AssetDatabase.GetAssetPath(selected[i])
  113.             );
  114.  
  115.         return guids;
  116.     }
  117.  
  118.     // ============================
  119.     // PSVita Publishing Folder Detection
  120.     // ============================
  121.  
  122.     HashSet<string> GetPSVitaPublishingFolders()
  123.     {
  124.         HashSet<string> folders = new HashSet<string>();
  125.  
  126.         AddFolderFromPath(folders, PlayerSettings.PSVita.liveAreaPath);
  127.         AddFolderFromPath(folders, PlayerSettings.PSVita.liveAreaGatePath);
  128.         AddFolderFromPath(folders, PlayerSettings.PSVita.liveAreaBackroundPath);
  129.  
  130.         return folders;
  131.     }
  132.  
  133.     void AddFolderFromPath(HashSet<string> folders, string assetPath)
  134.     {
  135.         if (string.IsNullOrEmpty(assetPath))
  136.             return;
  137.  
  138.         if (!assetPath.StartsWith("Assets/"))
  139.             assetPath = "Assets/" + assetPath.TrimStart('/');
  140.  
  141.         string dir = Path.GetDirectoryName(assetPath);
  142.         if (string.IsNullOrEmpty(dir))
  143.             return;
  144.  
  145.         dir = dir.Replace('\\', '/');
  146.  
  147.         if (Directory.Exists(dir))
  148.             folders.Add(dir);
  149.     }
  150.  
  151.     bool IsUnderAnyFolder(string assetPath, HashSet<string> folders)
  152.     {
  153.         assetPath = assetPath.Replace('\\', '/');
  154.  
  155.         foreach (string folder in folders)
  156.         {
  157.             if (assetPath.StartsWith(folder + "/"))
  158.                 return true;
  159.         }
  160.         return false;
  161.     }
  162.  
  163.     // ============================
  164.     // MANUAL BACKUP
  165.     // ============================
  166.  
  167.     void BackupTextures()
  168.     {
  169.         if (addBackupToGitIgnore)
  170.             EnsureBackupIgnoredByGit();
  171.  
  172.         string[] guids = GetTargetTextureGUIDs();
  173.         if (guids.Length == 0)
  174.             return;
  175.  
  176.         AssetDatabase.StartAssetEditing();
  177.         try
  178.         {
  179.             for (int i = 0; i < guids.Length; i++)
  180.             {
  181.                 EditorUtility.DisplayProgressBar(
  182.                     "Backing Up Textures",
  183.                     "Backing up textures...",
  184.                     (float)i / guids.Length
  185.                 );
  186.  
  187.                 string path = AssetDatabase.GUIDToAssetPath(guids[i]);
  188.                 if (string.IsNullOrEmpty(path) || path.StartsWith(BackupRoot))
  189.                     continue;
  190.  
  191.                 BackupSourceTexture(path);
  192.             }
  193.         }
  194.         finally
  195.         {
  196.             EditorUtility.ClearProgressBar();
  197.             AssetDatabase.StopAssetEditing();
  198.             AssetDatabase.Refresh();
  199.         }
  200.  
  201.         EditorUtility.DisplayDialog(
  202.             "Backup Complete",
  203.             "Textures have been backed up to:\n\n" + BackupRoot,
  204.             "OK"
  205.         );
  206.     }
  207.  
  208.     // ============================
  209.     // ANALYZE → CSV
  210.     // ============================
  211.  
  212.     void AnalyzeTextures()
  213.     {
  214.         string[] guids = GetTargetTextureGUIDs();
  215.         if (guids.Length == 0)
  216.             return;
  217.  
  218.         HashSet<string> publishingFolders = GetPSVitaPublishingFolders();
  219.  
  220.         StringBuilder csv = new StringBuilder(4096);
  221.         csv.AppendLine(
  222.             "AssetPath,SourceMB,EstimatedPVRTC_MB,ReductionPercent," +
  223.             "ImporterType,UsesAlpha,Classification," +
  224.             "PSP2_EffectiveMax,UserTargetMax,PSP2_OutputMax," +
  225.             "OriginalFormat,TargetFormat," +
  226.             "MipmapsOriginal,MipmapsTarget," +
  227.             "ForcedOverride,WillChange,ExcludedReason"
  228.         );
  229.  
  230.         int userMax = kMaxSizes[maxSizeIndex];
  231.         TextureImporterFormat targetRGB = kRGBFormats[rgbFormatIndex];
  232.         TextureImporterFormat targetRGBA = kRGBAFormats[rgbaFormatIndex];
  233.  
  234.         foreach (string guid in guids)
  235.         {
  236.             string path = AssetDatabase.GUIDToAssetPath(guid);
  237.             if (string.IsNullOrEmpty(path) || path.StartsWith(BackupRoot))
  238.                 continue;
  239.  
  240.             TextureImporter importer = AssetImporter.GetAtPath(path) as TextureImporter;
  241.             if (!importer)
  242.                 continue;
  243.  
  244.             bool isExcluded = IsUnderAnyFolder(path, publishingFolders);
  245.             string excludedReason = isExcluded ? "Publishing Texture" : "";
  246.  
  247.             TextureImporterPlatformSettings psp2 =
  248.                 importer.GetPlatformTextureSettings("PSP2");
  249.  
  250.             int currentMax =
  251.                 psp2.overridden && psp2.maxTextureSize > 0
  252.                     ? psp2.maxTextureSize
  253.                     : importer.maxTextureSize;
  254.  
  255.             int outputMax =
  256.                 isExcluded
  257.                     ? currentMax
  258.                     : (forcePSP2MaxSize ? userMax : Mathf.Min(currentMax, userMax));
  259.  
  260.             bool usesAlpha = importer.alphaSource != TextureImporterAlphaSource.None;
  261.             bool mipTarget = importer.mipmapEnabled;
  262.  
  263.             string classification = "Color";
  264.             string forced = "None";
  265.             TextureImporterFormat finalFormat = psp2.format;
  266.  
  267.             if (isExcluded)
  268.             {
  269.                 classification = "SystemReserved";
  270.                 forced = "PSVitaPublishing";
  271.             }
  272.             else if (importer.textureType == TextureImporterType.NormalMap)
  273.             {
  274.                 classification = "Normal";
  275.                 forced = "NormalMap";
  276.                 finalFormat = TextureImporterFormat.PVRTC_RGB4;
  277.                 mipTarget = true;
  278.             }
  279.             else if (importer.textureType == TextureImporterType.Sprite)
  280.             {
  281.                 classification = "UI";
  282.                 forced = "UI";
  283.                 finalFormat = TextureImporterFormat.PVRTC_RGBA4;
  284.             }
  285.             else
  286.             {
  287.                 finalFormat = usesAlpha ? targetRGBA : targetRGB;
  288.             }
  289.  
  290.             Texture tex = AssetDatabase.LoadAssetAtPath<Texture>(path);
  291.             if (!tex)
  292.                 continue;
  293.  
  294.             long sourceBytes = Profiler.GetRuntimeMemorySizeLong(tex);
  295.             float sourceMB = sourceBytes / (1024f * 1024f);
  296.  
  297.             float estimatedMB = sourceMB;
  298.             if (!isExcluded)
  299.             {
  300.                 int w = Mathf.Min(tex.width, outputMax);
  301.                 int h = Mathf.Min(tex.height, outputMax);
  302.  
  303.                 int bpp =
  304.                     finalFormat == TextureImporterFormat.PVRTC_RGB2 ||
  305.                     finalFormat == TextureImporterFormat.PVRTC_RGBA2 ? 2 : 4;
  306.  
  307.                 estimatedMB = (w * h * bpp) / 8f / (1024f * 1024f);
  308.             }
  309.  
  310.             float reductionPercent =
  311.                 sourceMB > 0.0001f
  312.                     ? (1f - (estimatedMB / sourceMB)) * 100f
  313.                     : 0f;
  314.  
  315.             bool willChange =
  316.                 !isExcluded &&
  317.                 (
  318.                     !psp2.overridden ||
  319.                     psp2.maxTextureSize != outputMax ||
  320.                     psp2.format != finalFormat ||
  321.                     importer.mipmapEnabled != mipTarget
  322.                 );
  323.  
  324.             csv.AppendLine(string.Format(
  325.                 "{0},{1:F3},{2:F3},{3:F1}," +
  326.                 "{4},{5},{6},{7},{8},{9}," +
  327.                 "{10},{11},{12},{13},{14},{15},{16}",
  328.                 path.Substring(7),
  329.                 sourceMB,
  330.                 estimatedMB,
  331.                 reductionPercent,
  332.                 importer.textureType,
  333.                 usesAlpha ? "TRUE" : "FALSE",
  334.                 classification,
  335.                 currentMax,
  336.                 userMax,
  337.                 outputMax,
  338.                 psp2.overridden ? psp2.format.ToString() : "Automatic",
  339.                 finalFormat,
  340.                 importer.mipmapEnabled ? "TRUE" : "FALSE",
  341.                 mipTarget ? "TRUE" : "FALSE",
  342.                 forced,
  343.                 willChange ? "TRUE" : "FALSE",
  344.                 excludedReason
  345.             ));
  346.         }
  347.  
  348.         WriteAndSelectCSV(csv.ToString());
  349.     }
  350.  
  351.     // ============================
  352.     // OPTIMIZE (skips publishing folders)
  353.     // ============================
  354.  
  355.     void OptimizeTextures()
  356.     {
  357.         if (addBackupToGitIgnore)
  358.             EnsureBackupIgnoredByGit();
  359.  
  360.         string[] guids = GetTargetTextureGUIDs();
  361.         HashSet<string> publishingFolders = GetPSVitaPublishingFolders();
  362.  
  363.         int userMax = kMaxSizes[maxSizeIndex];
  364.         TextureImporterFormat targetRGB = kRGBFormats[rgbFormatIndex];
  365.         TextureImporterFormat targetRGBA = kRGBAFormats[rgbaFormatIndex];
  366.  
  367.         List<string> modified = new List<string>();
  368.         bool cancelled = false;
  369.  
  370.         AssetDatabase.StartAssetEditing();
  371.         try
  372.         {
  373.             for (int i = 0; i < guids.Length; i++)
  374.             {
  375.                 if (EditorUtility.DisplayCancelableProgressBar(
  376.                     "PSVita Texture Optimizer",
  377.                     "Optimizing textures...",
  378.                     (float)i / guids.Length))
  379.                 {
  380.                     cancelled = true;
  381.                     break;
  382.                 }
  383.  
  384.                 string path = AssetDatabase.GUIDToAssetPath(guids[i]);
  385.                 if (string.IsNullOrEmpty(path) || path.StartsWith(BackupRoot))
  386.                     continue;
  387.  
  388.                 TextureImporter importer = AssetImporter.GetAtPath(path) as TextureImporter;
  389.                 if (!importer)
  390.                     continue;
  391.  
  392.                 BackupSourceTexture(path);
  393.  
  394.                 if (IsUnderAnyFolder(path, publishingFolders))
  395.                     continue;
  396.  
  397.                 TextureImporterPlatformSettings psp2 =
  398.                     importer.GetPlatformTextureSettings("PSP2");
  399.  
  400.                 int currentMax =
  401.                     psp2.overridden && psp2.maxTextureSize > 0
  402.                         ? psp2.maxTextureSize
  403.                         : importer.maxTextureSize;
  404.  
  405.                 int outputMax =
  406.                     forcePSP2MaxSize ? userMax : Mathf.Min(currentMax, userMax);
  407.  
  408.                 bool usesAlpha = importer.alphaSource != TextureImporterAlphaSource.None;
  409.                 bool mipTarget = generateMipmaps;
  410.                 TextureImporterFormat finalFormat;
  411.  
  412.                 if (importer.textureType == TextureImporterType.NormalMap)
  413.                 {
  414.                     finalFormat = TextureImporterFormat.PVRTC_RGB4;
  415.                     mipTarget = true;
  416.                 }
  417.                 else if (importer.textureType == TextureImporterType.Sprite)
  418.                 {
  419.                     finalFormat = TextureImporterFormat.PVRTC_RGBA4;
  420.                 }
  421.                 else
  422.                 {
  423.                     finalFormat = usesAlpha ? targetRGBA : targetRGB;
  424.                 }
  425.  
  426.                 psp2.overridden = true;
  427.                 psp2.maxTextureSize = outputMax;
  428.                 psp2.format = finalFormat;
  429.  
  430.                 importer.mipmapEnabled = mipTarget;
  431.                 importer.SetPlatformTextureSettings(psp2);
  432.                 importer.SaveAndReimport();
  433.  
  434.                 modified.Add(path);
  435.             }
  436.         }
  437.         finally
  438.         {
  439.             EditorUtility.ClearProgressBar();
  440.             AssetDatabase.StopAssetEditing();
  441.             AssetDatabase.Refresh();
  442.         }
  443.  
  444.         if (cancelled)
  445.         {
  446.             AssetDatabase.StartAssetEditing();
  447.             try
  448.             {
  449.                 foreach (string path in modified)
  450.                     RestoreSingleFromBackup(path);
  451.             }
  452.             finally
  453.             {
  454.                 AssetDatabase.StopAssetEditing();
  455.                 AssetDatabase.Refresh();
  456.             }
  457.  
  458.             EditorUtility.DisplayDialog(
  459.                 "Optimization Cancelled",
  460.                 "Operation cancelled.\nRollback complete. No changes were applied.",
  461.                 "Dismiss"
  462.             );
  463.         }
  464.     }
  465.  
  466.     // ============================
  467.     // RESTORE
  468.     // ============================
  469.  
  470.     void RestoreFromBackup()
  471.     {
  472.         if (!EditorUtility.DisplayDialog(
  473.             "Restore From Backup",
  474.             "Overwrite textures with backed-up originals?\n\nThis cannot be undone.",
  475.             "Restore",
  476.             "Cancel"))
  477.             return;
  478.  
  479.         string[] guids = GetTargetTextureGUIDs();
  480.         if (guids.Length == 0)
  481.             return;
  482.  
  483.         AssetDatabase.Refresh();
  484.  
  485.         try
  486.         {
  487.             for (int i = 0; i < guids.Length; i++)
  488.             {
  489.                 EditorUtility.DisplayProgressBar(
  490.                     "Restoring Textures",
  491.                     "Restoring backed-up textures...",
  492.                     (float)i / guids.Length
  493.                 );
  494.  
  495.                 RestoreSingleFromBackup(
  496.                     AssetDatabase.GUIDToAssetPath(guids[i])
  497.                 );
  498.             }
  499.         }
  500.         finally
  501.         {
  502.             EditorUtility.ClearProgressBar();
  503.             AssetDatabase.Refresh();
  504.         }
  505.  
  506.         EditorUtility.DisplayDialog(
  507.             "Restore Complete",
  508.             "Textures have been restored from backup.",
  509.             "OK"
  510.         );
  511.     }
  512.  
  513.     // ============================
  514.     // Backup helpers
  515.     // ============================
  516.  
  517.     string GetBackupPath(string assetPath)
  518.     {
  519.         return assetPath.StartsWith("Assets/")
  520.             ? Path.Combine(BackupRoot, assetPath.Substring(7))
  521.             : null;
  522.     }
  523.  
  524.     void BackupSourceTexture(string assetPath)
  525.     {
  526.         string backup = GetBackupPath(assetPath);
  527.         if (backup == null || File.Exists(backup))
  528.             return;
  529.  
  530.         Directory.CreateDirectory(Path.GetDirectoryName(backup));
  531.         File.Copy(assetPath, backup);
  532.     }
  533.  
  534.     void RestoreSingleFromBackup(string assetPath)
  535.     {
  536.         if (string.IsNullOrEmpty(assetPath))
  537.             return;
  538.  
  539.         string backup = GetBackupPath(assetPath);
  540.         if (backup == null || !File.Exists(backup))
  541.             return;
  542.  
  543.         // Ensure destination directory exists
  544.         Directory.CreateDirectory(Path.GetDirectoryName(assetPath));
  545.  
  546.         // Replace the file safely (avoids file locks)
  547.         FileUtil.ReplaceFile(backup, assetPath);
  548.  
  549.         // Force Unity to fully reimport and apply importer settings
  550.         AssetDatabase.ImportAsset(
  551.             assetPath,
  552.             ImportAssetOptions.ForceUpdate | ImportAssetOptions.ForceSynchronousImport
  553.         );
  554.  
  555.         // Explicitly clear dirty state
  556.         TextureImporter importer = AssetImporter.GetAtPath(assetPath) as TextureImporter;
  557.         if (importer != null)
  558.             EditorUtility.SetDirty(importer);
  559.     }
  560.  
  561.  
  562.     // ============================
  563.     // Git ignore
  564.     // ============================
  565.  
  566.     void EnsureBackupIgnoredByGit()
  567.     {
  568.         const string entry = "Assets/PSVita/TextureBackup/";
  569.         string path = Path.Combine(Directory.GetCurrentDirectory(), ".gitignore");
  570.  
  571.         if (!File.Exists(path))
  572.             File.WriteAllText(path, entry + Environment.NewLine);
  573.         else if (!File.ReadAllText(path).Contains(entry))
  574.             File.AppendAllText(path, Environment.NewLine + entry + Environment.NewLine);
  575.     }
  576.  
  577.     // ============================
  578.     // CSV Output
  579.     // ============================
  580.  
  581.     void WriteAndSelectCSV(string content)
  582.     {
  583.         string folder = "Assets/PSVita/TextureAnalysis";
  584.         Directory.CreateDirectory(folder);
  585.  
  586.         string path = folder + "/TextureAnalysis_" +
  587.                       DateTime.Now.ToString("yyyy-MM-dd_HHmmss") + ".csv";
  588.  
  589.         File.WriteAllText(path, content, Encoding.UTF8);
  590.         AssetDatabase.Refresh();
  591.  
  592.         Selection.activeObject =
  593.             AssetDatabase.LoadAssetAtPath<UnityEngine.Object>(path);
  594.     }
  595. }
  596.  
Advertisement
Add Comment
Please, Sign In to add comment