Search Unity

  1. Welcome to the Unity Forums! Please take the time to read our Code of Conduct to familiarize yourself with the forum rules and how to post constructively.

[Editor Utility] Crunch All Textures

Discussion in 'Immediate Mode GUI (IMGUI)' started by kideternal, Nov 17, 2017.

  1. kideternal

    kideternal

    Joined:
    Nov 20, 2014
    Posts:
    82
    Before releasing a downloadable project, I like to Crunch all textures to dramatically reduce filesize (~30%) but not visual fidelity. (It also improves load times.) This process can be a pain, as selecting just those textures that are crunchable can be difficult, and should you make a mistake you have to start all over. So, I tend to do so in batches, which can take 15 minutes a piece to crunch, making it something I can't just walk-away from and return to hours later. Until now.

    This simple script iterates through your entire Asset folder and Crunches any un-crunched textures. (Un-crunchable textures are skipped.) I've found 75% to be a good trade-off between filesize and quality, so I've hardcoded it to that. (I've yet to find a community discussion with a consensus on what this value should be. The default 50% seems a bit too lossy to me.)

    It can take several hours to complete depending on project size, so save and backup your project first. It isn't cancel-able once begun, as I've not found a way to do that. However, should you need to you can just End-Task Unity and restart it. Re-executing the script will resume from where it left off. (You can ignore the "failed asset import" message on restart, as it will be the next asset crunched.)

    I've tested it on my own projects and it works well, so I thought it worth sharing. YMMV.

    Just drop it in an "Assets/Editor" folder and run it via the "Assets > Crunch All Textures" menu item.

    Code (CSharp):
    1. using UnityEditor;
    2. using UnityEngine;
    3.  
    4. public class CrunchAllTextures : ScriptableObject {
    5.  
    6.     [MenuItem("Assets/Crunch All Textures")]
    7.     static void Crunch() {
    8.         int quality = 75, count = 0;
    9.         foreach (string guid in AssetDatabase.FindAssets("t:texture", null)) {
    10.             string path = AssetDatabase.GUIDToAssetPath(guid);
    11.             TextureImporter textureImporter = AssetImporter.GetAtPath(path) as TextureImporter;
    12.             if (textureImporter && !textureImporter.crunchedCompression && textureImporter.compressionQuality != quality) {
    13.                 textureImporter.crunchedCompression = true;
    14.                 textureImporter.compressionQuality = quality;
    15.                 AssetDatabase.ImportAsset(path);
    16.                 count++;
    17.             }
    18.         }
    19.         Debug.Log(count + " textures crunched");
    20.     }
    21. }
     

    Attached Files:

    Last edited: Nov 17, 2017
  2. Fajlworks

    Fajlworks

    Joined:
    Sep 8, 2014
    Posts:
    344
    Hey, thanks for sharing the code. I went and improved it a bit - it's an EditorWindow now with coroutine support, so it won't block the editor and it sports a progress bar.

    Code (CSharp):
    1. using UnityEditor;
    2. using UnityEngine;
    3. using System.Collections;
    4. using System.Linq;
    5.  
    6. public class TextureCruncher : EditorWindow
    7. {
    8.     #region Variables
    9.  
    10.     int compressionQuality = 75;
    11.     int processingSpeed = 10;
    12.  
    13.     IEnumerator jobRoutine;
    14.     IEnumerator messageRoutine;
    15.  
    16.     float progressCount = 0f;
    17.     float totalCount = 1f;
    18.  
    19.     #endregion
    20.  
    21.  
    22.  
    23.     #region Properties
    24.  
    25.     float NormalizedProgress
    26.     {
    27.         get { return progressCount / totalCount; }
    28.     }
    29.  
    30.     float Progress
    31.     {
    32.         get { return progressCount / totalCount * 100f; }
    33.     }
    34.  
    35.     string FormattedProgress
    36.     {
    37.         get { return Progress.ToString ("0.00") + "%"; }
    38.     }
    39.  
    40.     #endregion
    41.  
    42.  
    43.     #region Script Lifecylce
    44.  
    45.     [MenuItem("Window/Texture Cruncher")]
    46.     static void Init()
    47.     {
    48.         var window = (TextureCruncher)EditorWindow.GetWindow (typeof (TextureCruncher));
    49.         window.Show();
    50.     }
    51.  
    52.     public void OnInspectorUpdate()
    53.     {
    54.         Repaint();
    55.     }
    56.  
    57.     void OnGUI ()
    58.     {
    59.         EditorGUILayout.LabelField ("Texture Cruncher", EditorStyles.boldLabel);
    60.  
    61.         compressionQuality = EditorGUILayout.IntSlider ("Compression quality:", compressionQuality, 0, 100);
    62.         processingSpeed = EditorGUILayout.IntSlider ("Processing speed:", processingSpeed, 1, 20);
    63.  
    64.         string buttonLabel = jobRoutine != null ? "Cancel" : "Begin";
    65.         if (GUILayout.Button (buttonLabel))
    66.         {
    67.             if (jobRoutine != null)
    68.             {
    69.                 messageRoutine = DisplayMessage ("Cancelled. "+FormattedProgress+" complete!", 4f);
    70.                 jobRoutine = null;
    71.             }
    72.             else
    73.             {
    74.                 jobRoutine = CrunchTextures();
    75.             }
    76.         }
    77.  
    78.         if (jobRoutine != null)
    79.         {
    80.             EditorGUILayout.BeginHorizontal();
    81.  
    82.             EditorGUILayout.PrefixLabel (FormattedProgress);
    83.  
    84.             var rect = EditorGUILayout.GetControlRect ();
    85.             rect.width = rect.width * NormalizedProgress;
    86.             GUI.Box (rect, GUIContent.none);
    87.  
    88.             EditorGUILayout.EndHorizontal();
    89.         }
    90.         else if (!string.IsNullOrEmpty (_message))
    91.         {
    92.             EditorGUILayout.HelpBox (_message, MessageType.None);
    93.         }
    94.     }
    95.  
    96.     void OnEnable ()
    97.     {
    98.         EditorApplication.update += HandleCallbackFunction;
    99.     }
    100.  
    101.     void HandleCallbackFunction ()
    102.     {
    103.         if (jobRoutine != null && !jobRoutine.MoveNext())
    104.             jobRoutine = null;
    105.      
    106.  
    107.         if (messageRoutine != null && !messageRoutine.MoveNext())
    108.             messageRoutine = null;
    109.     }
    110.  
    111.     void OnDisable ()
    112.     {
    113.         EditorApplication.update -= HandleCallbackFunction;
    114.     }
    115.  
    116.     #endregion
    117.  
    118.  
    119.  
    120.     #region Logic
    121.  
    122.     string _message = null;
    123.  
    124.     IEnumerator DisplayMessage (string message, float duration = 0f)
    125.     {
    126.         if (duration <= 0f || string.IsNullOrEmpty (message))
    127.             goto Exit;
    128.  
    129.         _message = message;
    130.  
    131.         while (duration > 0)
    132.         {
    133.             duration -= 0.01667f;
    134.             yield return null;
    135.         }
    136.  
    137.         Exit:
    138.         _message = string.Empty;
    139.     }
    140.  
    141.     IEnumerator CrunchTextures()
    142.     {
    143.         DisplayMessage (string.Empty);
    144.  
    145.         var assets = AssetDatabase.FindAssets ("t:texture", null).Select (o => AssetImporter.GetAtPath (AssetDatabase.GUIDToAssetPath(o)) as TextureImporter);
    146.         var eligibleAssets = assets.Where (o => o != null).Where (o => o.compressionQuality != compressionQuality || !o.crunchedCompression);
    147.  
    148.         totalCount = (float)eligibleAssets.Count();
    149.         progressCount = 0f;
    150.  
    151.         int quality = compressionQuality;
    152.         int limiter = processingSpeed;
    153.         foreach (var textureImporter in eligibleAssets)
    154.         {
    155.             progressCount += 1f;
    156.  
    157.             textureImporter.compressionQuality = quality;
    158.             textureImporter.crunchedCompression = true;
    159.             AssetDatabase.ImportAsset(textureImporter.assetPath);
    160.          
    161.             limiter -= 1;
    162.             if (limiter <= 0)
    163.             {
    164.                 yield return null;
    165.  
    166.                 limiter = processingSpeed;
    167.             }
    168.         }
    169.    
    170.         messageRoutine = DisplayMessage ("Crunching complete!", 6f);
    171.         jobRoutine = null;
    172.     }
    173.  
    174.     #endregion
    175.  
    176. }
    In idle looks like this:


    And while processing textures:


    The good thing is, it looks like a good base to add more features. If anyone wants to upgrade or build upon, please do! Best regards.

    EDIT: I wanted to edit the code a bit and messed the whole message, whoops!
     
    Last edited: Nov 18, 2017
  3. kideternal

    kideternal

    Joined:
    Nov 20, 2014
    Posts:
    82
    Cool stuff, but the Editor pops-up a modal message when crunching, so there's no way to cancel-out of the process, at least not with large textures that can take 15+ seconds each. (You can't click anything, let alone a Cancel button.) It might be worth exploring a short (3 second?) "countdown to next crunch" timer between crunches that allows for Cancel input.

    Really wish there was a way to crunch in the background so you could continue working. Also, I can't believe that the resource-import process isn't multi-threaded by now. Projects can take forever to migrate to a new version! There's a suggestion to vote-on for this here: https://feedback.unity3d.com/suggestions/editor-import-assets-in-paralle
     
    Last edited: Nov 18, 2017
    horeaper likes this.
  4. Fajlworks

    Fajlworks

    Joined:
    Sep 8, 2014
    Posts:
    344
    Hmm, I'm using 5.3.7f1 version and I didn't have modal message blocking, everything works out okay and I can even edit the scene while it works (on processing speed = 1). Perhaps you are using newer version of Unity? I noticed I couldn't access the TextureImporter field:

    Code (CSharp):
    1.  textureImporter.crunchedCompression = true; //compile error on 5.3.7f1
    Unfortunately, I have aversion towards unstable versions of Unity 2017 so I can't help with this. Maybe someone else might be of assistance. Sorry!
     
  5. Gerard_Slee

    Gerard_Slee

    Joined:
    Apr 3, 2017
    Posts:
    11
    This is great, thank you saved me having to run through my textures. Also having issue with modal message blocking unity, but still a time saver.
     
  6. pRoFlT

    pRoFlT

    Joined:
    Dec 20, 2014
    Posts:
    26
    Oh this is nice. i wish there were better editor window tutorials on how all this stuff worked.

    I'm borrowing some of that for my asset browser editor. i need a progress bar :) coroutines... I was just thinking about that.

    Thanks.