Search Unity

  1. Unity 6 Preview is now available. To find out what's new, have a look at our Unity 6 Preview blog post.
    Dismiss Notice
  2. Unity is excited to announce that we will be collaborating with TheXPlace for a summer game jam from June 13 - June 19. Learn more.
    Dismiss Notice

Resolved Save RenderTexture or Texture2D as image file UTILITY

Discussion in 'General Graphics' started by atomicjoe, Aug 20, 2022.

  1. atomicjoe

    atomicjoe

    Joined:
    Apr 10, 2013
    Posts:
    1,869
    - EDIT: arkano22 made a better asynchronous version down there so that it doesn't freeze the render pipeline plus it can resize the texture before saving: check it out here

    Original post:

    I can't believe we're in 2022 and Unity still hasn't a straightforward way of saving a render texture to disk...
    I spent more time searching for a method that would do that than the time it took me to actually code it using the included APIs... :mad:

    So I'll just post this here, for people in the future searching for a ready-made method for something that should be included by default.

    Just create a new C# script named SaveTextureToFileUtility.cs and copy-paste this inside:

    Code (CSharp):
    1. using UnityEngine;
    2.  
    3. public class SaveTextureToFileUtility
    4. {
    5.    public enum SaveTextureFileFormat
    6.    {
    7.        EXR, JPG, PNG, TGA
    8.    };
    9.  
    10.    /// <summary>
    11.    /// Saves a Texture2D to disk with the specified filename and image format
    12.    /// </summary>
    13.    /// <param name="tex"></param>
    14.    /// <param name="filePath"></param>
    15.    /// <param name="fileFormat"></param>
    16.    /// <param name="jpgQuality"></param>
    17.    static public void SaveTexture2DToFile(Texture2D tex, string filePath, SaveTextureFileFormat fileFormat, int jpgQuality = 95)
    18.    {
    19.        switch (fileFormat)
    20.        {
    21.            case SaveTextureFileFormat.EXR:
    22.                System.IO.File.WriteAllBytes(filePath + ".exr", tex.EncodeToEXR());
    23.                break;
    24.            case SaveTextureFileFormat.JPG:
    25.                System.IO.File.WriteAllBytes(filePath + ".jpg", tex.EncodeToJPG(jpgQuality));
    26.                break;
    27.            case SaveTextureFileFormat.PNG:
    28.                System.IO.File.WriteAllBytes(filePath + ".png", tex.EncodeToPNG());
    29.                break;
    30.            case SaveTextureFileFormat.TGA:
    31.                System.IO.File.WriteAllBytes(filePath + ".tga", tex.EncodeToTGA());
    32.                break;
    33.        }
    34.    }
    35.  
    36.  
    37.    /// <summary>
    38.    /// Saves a RenderTexture to disk with the specified filename and image format
    39.    /// </summary>
    40.    /// <param name="renderTexture"></param>
    41.    /// <param name="filePath"></param>
    42.    /// <param name="fileFormat"></param>
    43.    /// <param name="jpgQuality"></param>
    44.    static public void SaveRenderTextureToFile(RenderTexture renderTexture, string filePath, SaveTextureFileFormat fileFormat = SaveTextureFileFormat.PNG, int jpgQuality = 95)
    45.    {
    46.        Texture2D tex;
    47.        if (fileFormat != SaveTextureFileFormat.EXR)
    48.            tex = new Texture2D(renderTexture.width, renderTexture.height, TextureFormat.ARGB32, false, false);
    49.        else
    50.            tex = new Texture2D(renderTexture.width, renderTexture.height, TextureFormat.RGBAFloat, false, true);
    51.        var oldRt = RenderTexture.active;
    52.        RenderTexture.active = renderTexture;
    53.        tex.ReadPixels(new Rect(0, 0, renderTexture.width, renderTexture.height), 0, 0);
    54.        tex.Apply();
    55.        RenderTexture.active = oldRt;
    56.        SaveTexture2DToFile(tex, filePath, fileFormat, jpgQuality);
    57.        if (Application.isPlaying)
    58.            Object.Destroy(tex);
    59.        else
    60.            Object.DestroyImmediate(tex);
    61.  
    62.    }
    63.  
    64. }
    65.  
    Put that script somewhere in your project and now, just use SaveTextureToFileUtility.SaveRenderTextureToFile or SaveTextureToFileUtility.SaveTexture2DToFile to save a RenderTexture or a regular Texture2D to disk to the specified path using the specified file format on your own scripts.
    You can choose between JPG, PNG, EXR or TGA image formats.

    I have only tried the 8bit file formats for now, although it's coded to handle HDR images, but let me know if there is any issues with them or with Linear vs sRGB gamma or things like that.
     
    Last edited: Aug 28, 2022
    Tu-Le, tun1018, Sluggy and 8 others like this.
  2. arkano22

    arkano22

    Joined:
    Sep 20, 2012
    Posts:
    1,971
    Thanks for sharing this!

    Maybe that means that the existing API is simple enough that there's no point in adding a specific method for this? Once you have your Texture2D ready, the gist of it is a one-liner:

    Code (CSharp):
    1. File.WriteAllBytes(filePath, tex.EncodeToPNG());
    There's not much advantage in some hypothetical method like:

    Code (CSharp):
    1. tex.SaveAsPNG(filePath);
    If your texture resides on the GPU (RenderTexture), you have to bring the data back to the CPU (Texture2D) before you can write to disk, and this can be done in a variety of ways: using ReadPixels, using AsyncGPUReadback, etc... you might also want to crop the image, which these methods allow. Not sure how things could be made simpler on this front tbh.
     
    Last edited: Aug 24, 2022
  3. atomicjoe

    atomicjoe

    Joined:
    Apr 10, 2013
    Posts:
    1,869
    By having a method that does that, like SaveRenderTextureToFile
    One would expect Unity to be able to do that by itself without having to jump through loops like all the code I put up there.
     
    Last edited: Aug 24, 2022
    Airmouse likes this.
  4. arkano22

    arkano22

    Joined:
    Sep 20, 2012
    Posts:
    1,971
    What I mean is that your method covers a specific use case: synchronously read a render texture back from GPU as a texture of the same size, then save it to disk. Ofc you could add optional parameters or overrides to SaveRenderTextureToFile to account for all possible variations, for instance a boolean to avoid stalling the GPU, a callback method to get notified when the texture has been saved, a Rect to determine which part of the render texture to crop, a size for the target texture, etc.

    But if all of this can be easily done in just a few lines using existing, more atomic API methods, I'm not sure if bloating the API with an additional methods would be justified. Your method (excluding the switch statement to select formats) is about 10 lines of code which isn't much, nor very complex.

    In a similar line of thought, you could also have transform.RotateAndTranslate(), transform.RotateTranslateAndScale(), transform.RotateAndScale(), etc, but it's often better to just have simpler methods that can be composed to do what you want.

    Just my two cents. This code will save time to some people though, so it's a good thing to share anyway.
     
    c0d3_m0nk3y likes this.
  5. atomicjoe

    atomicjoe

    Joined:
    Apr 10, 2013
    Posts:
    1,869
    Yes, I do like those. But I also like to be able to save a render texture to disk without having to: create a new Texture2D, copy the contents of the RenderTexture to the Texture2D, encode said Texture2D to PNG, save that to disk and dispose of the Texture2D.

    My point is: saving a RenderTexture to file should be as easy as saving a Texture2D to file is.
    It's nonsensical that there isn't a RenderTexture.EncodeToPNG method like there is a Texture2D.EncodeToPNG method.
     
  6. arkano22

    arkano22

    Joined:
    Sep 20, 2012
    Posts:
    1,971
    Unlike dealing with a Texture2D, bringing the contents of a RenderTexture to cpu is extremely slow, as it stalls the entire render pipeline. Mirroring EncodeToPNG in RenderTextures would hide that cost away from the user and could become a potential pitfall.

    I think it is within reason to consider A) Create a Texture2D from a RenderTexture and B) Write a Texture2D to disk separate operations.
     
  7. atomicjoe

    atomicjoe

    Joined:
    Apr 10, 2013
    Posts:
    1,869
    I really don't understand what is the problem with having an extra method that simply saves a render texture to a file.
    You prefer to do it manually? Fine, just do that. Personally, I'll use a method that does that for me, and I suspect there is a lot of people that would prefer that too.

    Could it be faster? Absolutely! But if you only want to save a RenderTexture to disk as a snapshot and don't need it to go at 60 fps at runtime, it's perfectly fine, honestly. (and if you want that level of performance, maybe you should just use the Unity Recorder package)

    But by all means, feel free to improve upon it or completely rewrite that method to be an async operation and don't stall the render thread.
    You can then post it here for everyone to enjoy :)
     
  8. arkano22

    arkano22

    Joined:
    Sep 20, 2012
    Posts:
    1,971
    What's problematic is to apply that logic to everything: you'd end up bloating the engine with tons of methods, most of which are in essence boilerplate code that just pipes together 2-3 other methods.

    It's absolutely fine to write these for yourself, and sharing them so that people with no programming knowledge can benefit from them, but they really shouldn't make it into the engine imho. That's how you end up with humongous, confusing APIs with dozens of methods that are just variations on the same thing (and there will always be some specific variation that someone feels it's missing, the battle is lost before it's even fought).

    You want a way to directly write a RenderTexture to disk, that's 3 different tasks: #1) bring the render texture data into the cpu, #2) encode the cpu texture data into a format suitable for serialization, and #3) actually writing the data to disk. Unity already exposes methods to do all these 3 things, you just need to chain them together.

    If there was no way to do what you want using the existing API, or if there was a significant amount of work or complexity involved in doing so, I would agree for the engine to provide a ready-made solution as it would enable new use cases or simplify complex ones. But as you mentioned, it takes more time to look up whether such a solution exists than to write it yourself using the existing methods, which is kinda my point too: why would you need the engine to provide a method that you can write yourself in a couple minutes using the existing API? what's the problem you solve by doing that? does it justify polluting the engine with random stuff?
     
    Last edited: Aug 24, 2022
  9. atomicjoe

    atomicjoe

    Joined:
    Apr 10, 2013
    Posts:
    1,869
    What you call "pollution" is actually "user friendliness" and it's the difference between a low level and a high level API.

    Again, if you don't like the method up there, either improve it or don't use it but don't ruin it for everybody else.
     
  10. arkano22

    arkano22

    Joined:
    Sep 20, 2012
    Posts:
    1,971
    Let's agree to disagree then, no problem.

    At no point did I say "I don't like the method", quite the contrary: I thanked you for writing and sharing it. I just don't think it should be an integral part of Unity because of the reasons I stated above, that's all.
     
    AndBje likes this.
  11. atomicjoe

    atomicjoe

    Joined:
    Apr 10, 2013
    Posts:
    1,869
    And I think it should.

    I guess the only remaining thing for us to do now is to partake in a deadly duel.
    Choose your weapon.
    I'll get the polearm.
    EN GARDE!
     
    arkano22 likes this.
  12. atomicjoe

    atomicjoe

    Joined:
    Apr 10, 2013
    Posts:
    1,869
    I also use TABs instead of spaces to indent code.
    In case you needed something more to disagree with me on. :p
     
  13. arkano22

    arkano22

    Joined:
    Sep 20, 2012
    Posts:
    1,971
    oh la la! I'd better get ready to be beaten to death then. I have the physical prowess of a brick.

    That's something I'd have to agree with :). Tabs result in nice, consistent indentation.

    Regarding your original method, will try to put together an async version and share it here in case someone finds it useful.
     
  14. arkano22

    arkano22

    Joined:
    Sep 20, 2012
    Posts:
    1,971
    Here's a similar method that takes either a RenderTexture or a Texture as input, and writes it to disk either asynchronously or synchronously. Can resize the output as well (pass -1 for width and height if you want to keep the original size):

    Code (CSharp):
    1. static public void SaveTextureToFile(Texture source,
    2.                                          string filePath,
    3.                                          int width,
    4.                                          int height,
    5.                                          SaveTextureFileFormat fileFormat = SaveTextureFileFormat.PNG,
    6.                                          int jpgQuality = 95,
    7.                                          bool asynchronous = true,
    8.                                          System.Action<bool> done = null)
    9.     {
    10.         // check that the input we're getting is something we can handle:
    11.         if (!(source is Texture2D || source is RenderTexture))
    12.         {
    13.             done?.Invoke(false);
    14.             return;
    15.         }
    16.  
    17.         // use the original texture size in case the input is negative:
    18.         if (width < 0 || height < 0)
    19.         {
    20.             width = source.width;
    21.             height = source.height;
    22.         }
    23.  
    24.         // resize the original image:
    25.         var resizeRT = RenderTexture.GetTemporary(width, height, 0);
    26.         Graphics.Blit(source, resizeRT);
    27.  
    28.         // create a native array to receive data from the GPU:
    29.         var narray = new NativeArray<byte>(width * height * 4, Allocator.Persistent, NativeArrayOptions.UninitializedMemory);
    30.  
    31.         // request the texture data back from the GPU:
    32.         var request = AsyncGPUReadback.RequestIntoNativeArray (ref narray, resizeRT, 0, (AsyncGPUReadbackRequest request) =>
    33.         {
    34.             // if the readback was successful, encode and write the results to disk
    35.             if (!request.hasError)
    36.             {
    37.                 NativeArray<byte> encoded;
    38.  
    39.                 switch (fileFormat)
    40.                 {
    41.                     case SaveTextureFileFormat.EXR:
    42.                         encoded = ImageConversion.EncodeNativeArrayToEXR(narray, resizeRT.graphicsFormat, (uint)width, (uint)height);
    43.                         break;
    44.                     case SaveTextureFileFormat.JPG:
    45.                         encoded = ImageConversion.EncodeNativeArrayToJPG(narray, resizeRT.graphicsFormat, (uint)width, (uint)height, 0, jpgQuality);
    46.                         break;
    47.                     case SaveTextureFileFormat.TGA:
    48.                         encoded = ImageConversion.EncodeNativeArrayToTGA(narray, resizeRT.graphicsFormat, (uint)width, (uint)height);
    49.                         break;
    50.                     default:
    51.                         encoded = ImageConversion.EncodeNativeArrayToPNG(narray, resizeRT.graphicsFormat, (uint)width, (uint)height);
    52.                         break;
    53.                 }
    54.  
    55.                 System.IO.File.WriteAllBytes(filePath, encoded.ToArray());
    56.                 encoded.Dispose();
    57.             }
    58.  
    59.             narray.Dispose();
    60.  
    61.             // notify the user that the operation is done, and its outcome.
    62.             done?.Invoke(!request.hasError);
    63.         });
    64.  
    65.         if (!asynchronous)
    66.             request.WaitForCompletion();
    67.     }
     
  15. atomicjoe

    atomicjoe

    Joined:
    Apr 10, 2013
    Posts:
    1,869
    NOW that's more like it! :D
    Putting your code where your mouth is! ;)
    I have linked to this version in the first post so that people know there is a better, sexier, fancier version.
     
    Last edited: Aug 24, 2022
    neilybod and arkano22 like this.
  16. neilybod

    neilybod

    Joined:
    Apr 3, 2020
    Posts:
    2
    Thanks to both of you for sharing this, and also for the entertaining battle :D
    Just a note to anyone using this, you'll need the following refs in the top of your script:

    Code (CSharp):
    1. using UnityEngine.Rendering;
    2. using Unity.Collections;
    Thanks again - just what I needed
     
  17. dmitrydemos

    dmitrydemos

    Joined:
    Apr 7, 2023
    Posts:
    2
    Hello everyone!
    As soon as I started to figure out the simpliest way to exactly export texture2d to any other file either than *.asset - there was no any working solution for begginers. Almost at all. :( So I'm very sorry about my question (I am just noob-beginner, and I am not planning to study whole C# and Unity just for understanding how to execute this code), but what are the full instruction of how and where that code can be applied?

    I created SaveTextureToFileUtility.cs (inserted the code into it), put this one into "Assets/Scripts" folder, and unfortunatly, there was nothing happend... What should I do next to execute this?

    That clever advice for applying this code can help not just me, but every new Unity beginner, who faced such trouble as me. I would be very grateful for your help!
     
  18. atomicjoe

    atomicjoe

    Joined:
    Apr 10, 2013
    Posts:
    1,869
    This code is intended to be called as a single instruction from other scripts, not to be used from the editor interface.
    If you don't have code skills, this will be of no use for you.
    In that case, I would recommend that you purchase something like Adventure Creator or PlayMaker from the asset store as an all in one solution for non programmers.
     
    Last edited: Apr 8, 2023
  19. dmitrydemos

    dmitrydemos

    Joined:
    Apr 7, 2023
    Posts:
    2
    Ok, thank you for the direction for thinking and experimenting!
     
  20. skullthug

    skullthug

    Joined:
    Oct 16, 2011
    Posts:
    202
    Boy I really wish I had this thread in 2021 and 2020. Thank you for sharing your utility!
     
    atomicjoe likes this.
  21. Abdo-Reda

    Abdo-Reda

    Joined:
    Oct 8, 2015
    Posts:
    13
    This forum was very entertaining and informative. Thanks both for sharing this <3
     
  22. Shanmf3d

    Shanmf3d

    Joined:
    Aug 1, 2017
    Posts:
    8
    Thanks for sharing your code, its really sped things up for me. Appreciate it.
     
  23. Voimmamored

    Voimmamored

    Joined:
    Dec 5, 2021
    Posts:
    1
    Nice code. Nice battle.
     
  24. YoxerG

    YoxerG

    Joined:
    Mar 27, 2020
    Posts:
    1
    A little editor window for people without coding skills.
    Create a new script called SaveTextureToFileWindow.cs, copy-paste in all the code from below.
    (Be sure to have the upgraded version of the SaveTextureToFileUtility.cs file from above too.)

    To use: In the Unity menu bar up at the top, there should be a new option called
    Tools -> Save Texture To File Window.
    And it should look something like this when clicked:
    upload_2024-3-14_15-24-52.png

    Clicking the Debug.Log in your console pings the new texture asset.
    Don't miss the
    #if UNITY_EDITOR
    and
    #endif
    lines, or put this script in a folder called Editor, otherwise you won't be able to build your game.

    Code (CSharp):
    1. #if UNITY_EDITOR
    2. using UnityEditor;
    3. using UnityEditor.UIElements;
    4. using UnityEngine;
    5. using UnityEngine.UIElements;
    6.  
    7. public class SaveTextureToFileWindow : EditorWindow
    8. {
    9.     private ObjectField texture;
    10.     private TextField filePath;
    11.     private Vector2IntField size;
    12.     private EnumField format;
    13.     private Button button;
    14.  
    15.     private string uniqueFilePath;
    16.  
    17.  
    18.     [MenuItem("Tools/Save Texture To File Window")]
    19.     public static void ShowWindow()
    20.     {
    21.         SaveTextureToFileWindow wnd = GetWindow<SaveTextureToFileWindow>();
    22.         wnd.minSize = new Vector2(300, 105);
    23.         wnd.titleContent = new GUIContent("Save Texture To File");
    24.     }
    25.  
    26.     public void CreateGUI()
    27.     {
    28.         VisualElement root = rootVisualElement;
    29.         texture = new ObjectField("Texture") { objectType = typeof(Texture) };
    30.         root.Add(texture);
    31.         filePath = new TextField("File Path") { value = "Assets/texture.png" };
    32.         root.Add(filePath);
    33.         size = new Vector2IntField("Size") { value = new Vector2Int(-1, -1), tooltip = "Negative values mean original width and height." };
    34.         root.Add(size);
    35.         format = new EnumField("Format", SaveTextureToFileUtility.SaveTextureFileFormat.PNG);
    36.         root.Add(format);
    37.         button = new Button(Save) { text = "Save" };
    38.         root.Add(button);
    39.     }
    40.  
    41.     private void Save()
    42.     {
    43.         uniqueFilePath = AssetDatabase.GenerateUniqueAssetPath(filePath.value);
    44.         SaveTextureToFileUtility.SaveTextureToFile(
    45.             (Texture)texture.value,
    46.             uniqueFilePath,
    47.             size.value.x,
    48.             size.value.y,
    49.             (SaveTextureToFileUtility.SaveTextureFileFormat)format.value,
    50.             done: DebugResult);
    51.     }
    52.  
    53.     private void DebugResult(bool success)
    54.     {
    55.         if (success)
    56.         {
    57.             AssetDatabase.Refresh();
    58.             Object file = AssetDatabase.LoadAssetAtPath(uniqueFilePath, typeof(Texture2D));
    59.             Debug.Log($"Texture saved to [{uniqueFilePath}]", file);
    60.         }
    61.         else
    62.         {
    63.             Debug.LogError($"Failed to save texture.");
    64.         }
    65.     }
    66. }
    67. #endif
     

    Attached Files:

    Last edited: Mar 15, 2024
    skullthug likes this.