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.
  2. Dismiss Notice

Question How best to save a frag shader into a PNG? ReadPixels is a bottleneck, AsyncGPUReadback is slower!

Discussion in 'Scripting' started by BionicWombatGames, Aug 10, 2023.

  1. BionicWombatGames

    BionicWombatGames

    Joined:
    Oct 5, 2020
    Posts:
    33
    I'm trying to cache the output of a shader as a PNG on disk so I can use it in the future as a straight-up texture and not require the shader to run. My project does a lot of procedural generation and caching the textures for hundreds of different instances will save gobbles of memory and gpu perf. However, getting the texture data from a RenderTexture onto the CPU and then saved onto disk is very slow. It is well documented that ReadPixels is slow, but the recommended alternative, AsyncGPUReadback, is actually slower for me.

    My general algorithm is as follows (full code below):

    1) Set a ComputeBuffer on my shader that contains the necessary data
    2) RenderTexture.GetTemporary
    3) Blit

    Then either:
    4) texture.ReadPixels
    Or
    4) AsyncGPUReadback.Request -> ImageConversion.EncodeNativeArrayToPNG

    5) File.WriteAllBytes

    When I benchmark this with a 1024x1024 image, ReadPixels takes 40-60ms, Writing takes 30ms, Blit only takes 1ms. If I use AsyncGPUReadback, .Request takes 50ms, ConvertToPNG 30ms, and writing only 9ms.

    Is there a faster way to do this? This is just running on my PC, but eventually I want to support more platforms.

    Code (CSharp):
    1.  
    2.   ComputeBuffer cornersBuffer = new ComputeBuffer(points.Length, Marshal.SizeOf<Vector2>());
    3.   cornersBuffer.SetData(points);
    4.   mat.SetBuffer(Shader.PropertyToID("_Corners"), cornersBuffer);
    5.  
    6.   RenderTexture renderTexture = RenderTexture.GetTemporary(res, res, 0, RenderTextureFormat.ARGB32);
    7.  
    8.   Graphics.SetRenderTarget(renderTexture);
    9.   GL.Clear(true, true, Color.black);
    10.   Graphics.Blit(null, renderTexture, mat);
    11.   Graphics.SetRenderTarget(null);
    12.   CoalescingTimer.Coalesce(st, "Blit");
    13.  
    14.   RenderTexture.active = renderTexture;
    15.   Texture2D texture = new Texture2D(renderTexture.width, renderTexture.height, TextureFormat.RGBA32, false);
    16.  
    17.   if (method == 0) {
    18.     texture.ReadPixels(new Rect(0, 0, renderTexture.width, renderTexture.height), 0, 0);
    19.     texture.Apply();
    20.  
    21.     RenderTexture.ReleaseTemporary(renderTexture);
    22.     TextureStorageManager.WriteTexture(texture, indexEntry);
    23.  
    24.     GetComponent<Renderer>().sharedMaterial.mainTexture = texture;
    25.  
    26.   } else if (method == 1) {
    27.     StartCoroutine(
    28.       CaptureAndSave(renderTexture, (NativeArray<byte> bytesNA) => {
    29.         bytesNA = ImageConversion.EncodeNativeArrayToPNG(bytesNA, texture.graphicsFormat, (uint)res, (uint)res);
    30.         byte[] bytes = bytesNA.ToArray();
    31.  
    32.         RenderTexture.ReleaseTemporary(renderTexture);
    33.         TextureStorageManager.WriteTextureBytes(bytes, indexEntry);
    34.  
    35.         texture.LoadImage(bytes);
    36.         GetComponent<Renderer>().sharedMaterial.mainTexture = texture;
    37.  
    38.         st.Stop();
    39.     }));
    40.   }
    41.  
    42.  
    43.  
     
  2. wideeyenow_unity

    wideeyenow_unity

    Joined:
    Oct 7, 2020
    Posts:
    728
    I recently played with saving textures as PNG, here is my result:
    Code (CSharp):
    1.     static Texture2D CreateBlankMap()
    2.     {
    3.         return new Texture2D(100, 100, TextureFormat.RGBA32, false);
    4.     }
    5.  
    6.     static Texture2D TextureFromColorArray(Color32[] array)
    7.     {
    8.         Texture2D image = CreateBlankMap();
    9.         image.SetPixels32(array);
    10.         image.Apply(); // false if not GPU used
    11.         return image;
    12.     }
    13.  
    14.     static void SaveTexture2D(Texture2D image, string imageName)
    15.     {
    16.         byte[] bytes = image.EncodeToPNG();
    17.         System.IO.File.WriteAllBytes(Application.dataPath +
    18.             mapSavePath + imageName + ".png", bytes);
    19.         AssetDatabase.SaveAssets();
    20.         AssetDatabase.Refresh();
    21.     }
    22.  
    23.     static void SavedPNGsettings(string imageName)
    24.     {
    25.         TextureImporter importer = (TextureImporter)AssetImporter.GetAtPath(
    26.             "Assets" + mapSavePath + imageName + ".png");
    27.         TextureImporterPlatformSettings settings = new TextureImporterPlatformSettings();
    28.  
    29.         importer.textureType = TextureImporterType.Sprite;
    30.         importer.mipmapEnabled = false;
    31.         importer.isReadable = true;
    32.         settings.format = TextureImporterFormat.RGBA32;
    33.  
    34.         importer.SetPlatformTextureSettings(settings);
    35.         AssetDatabase.ImportAsset("Assets" + mapSavePath + imageName + ".png",
    36.             ImportAssetOptions.ForceUpdate);
    37.     }
    But if I recall, even just using a 100x100 texture did prove to freeze frame for about a second. So if you ever find a way to make it flawless, without freezing, I would be most curious to know how. :)
     
  3. BionicWombatGames

    BionicWombatGames

    Joined:
    Oct 5, 2020
    Posts:
    33
    Thank you for your response. The issue is not saving the texture to disk, that is well documented. I'm trying to figure out how best to overcome the latency of passing an image from GPU to CPU so that it can be saved to disk.
     
  4. wideeyenow_unity

    wideeyenow_unity

    Joined:
    Oct 7, 2020
    Posts:
    728
    Ohh, sorry, thought there was another way to read pixels so I could just say put them into a Color[].. But according to the documentation:
    https://docs.unity3d.com/ScriptReference/RenderTexture.html
    "A render texture only has a data representation on the GPU and you need to use Texture2D.ReadPixels to transfer its contents to CPU memory."

    But I also see:
    https://blog.unity.com/engine-platform/accessing-texture-data-efficiently
    "For most types of textures, Unity stores two copies of the pixel data: one in GPU memory, which is required for rendering, and the other in CPU memory. This copy is optional and allows you to read from, write to, and manipulate pixel data on the CPU. A texture with a copy of its pixel data stored in CPU memory is called a readable texture."

    So I'm not sure how your setup is, or even if said "readable texture" exists in your case, as that might be if you convert texture2D to a renderTexture. Other than that, it kinda seems like your stuck doing it that way, and the only thing that might help just a bit is this posts answer:
    https://stackoverflow.com/questions/45100993/rendertexture-to-texture2d-is-very-slowly

    But I saw with your mention of Blit, that it might be the same thing as ReleaseTemporary(), which is supposedly faster. And my assumption of converting the renderTexture to a Color[], is probably the same thing as ReadPixels(), or possibly even slower or more out of the way than that.

    Sorry to say, but it looks like that might be the only way. :(