Search Unity

Question Faster method for getting a pixel color.

Discussion in 'Scripting' started by Airashawn, Jan 11, 2024.

  1. Airashawn

    Airashawn

    Joined:
    Jun 9, 2020
    Posts:
    18
    I have created a system that paints terrain in play mode. Think Splatoon but for Unity's terrain. What I've been working on requires me to occasionally know what color the painted terrain is. The many methods I have seen to do this work well enough and I've found one that works if my texture size is 512x512 or smaller but I would like to go larger as it looks sleeker, however, the larger I go in pixel size, the more it affects the framerate of the camera. I should add that I'm completely self-taught (good old YouTube academy) and reading Unity documentation makes my brain overheat.

    Here's what I currently have to determine my pixel information:

    Code (CSharp):
    1.  
    2. public Color ReturnColor(RaycastHit _hit)
    3. {
    4.     RenderTexture _splatmap = _hit.collider.GetComponent<PaintableTerrain>()._splatmap;
    5.     RenderTexture temp = RenderTexture.GetTemporary(_splatmap.width, _splatmap.height, 0, RenderTextureFormat.ARGBFloat, RenderTextureReadWrite.Linear);
    6.  
    7.     Graphics.Blit(_splatmap, temp);
    8.  
    9.     RenderTexture previous = RenderTexture.active;
    10.     RenderTexture.active = temp;
    11.  
    12.     if (myTexture2D == null || myTexture2D.width != _splatmap.width || myTexture2D.height != _splatmap.height)
    13.     {
    14.         myTexture2D.Reinitialize(_splatmap.width, _splatmap.height, TextureFormat.ARGB32, false);
    15.     }
    16.  
    17.     myTexture2D.ReadPixels(new Rect(0, 0, temp.width, temp.height), 0, 0);
    18.     myTexture2D.Apply();
    19.  
    20.     RenderTexture.active = previous;
    21.  
    22.     Vector2 pixelPos = _hit.textureCoord;
    23.     pixelPos.x *= _splatmap.width;
    24.     pixelPos.y *= _splatmap.height;
    25.  
    26.     Color pixelColor = myTexture2D.GetPixel((int)pixelPos.x, (int)pixelPos.y);
    27.     RenderTexture.ReleaseTemporary(temp);
    28.  
    29.  
    30.     return pixelColor;
    31. }
    32.  
     
  2. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    38,697
    Does it need to query the array every frame? First step might be to only get that ReadPixels array if you know it will be different from last frame.

    "Know it will be different" can be a boolean set whenever you actually do paint on the ground, assuming that is less-frequently than you query the ground.

    Otherwise... maybe keep your own Color[,] array in conjunction and use that for querying??


    But as always...

    For all performance and optimization issues, ALWAYS start by using the Profiler window:

    Window -> Analysis -> Profiler

    DO NOT OPTIMIZE "JUST BECAUSE..." If you don't have a problem, DO NOT OPTIMIZE!

    If you DO have a problem, there is only ONE way to find out: measuring with the profiler.

    Failure to use the profiler first means you're just guessing, making a mess of your code for no good reason.

    Not only that but performance on platform A will likely be completely different than platform B. Test on the platform(s) that you care about, and test to the extent that it is worth your effort, and no more.

    https://forum.unity.com/threads/is-...ng-square-roots-in-2021.1111063/#post-7148770

    Remember that you are gathering information at this stage. You cannot FIX until you FIND.

    Remember that optimized code is ALWAYS harder to work with and more brittle, making subsequent feature development difficult or impossible, or incurring massive technical debt on future development.

    Don't forget about the Frame Debugger window either, available right near the Profiler in the menu system.

    Notes on optimizing UnityEngine.UI setups:

    https://forum.unity.com/threads/how...form-data-into-an-array.1134520/#post-7289413

    At a minimum you want to clearly understand what performance issues you are having:

    - running too slowly?
    - loading too slowly?
    - using too much runtime memory?
    - final bundle too large?
    - too much network traffic?
    - something else?

    If you are unable to engage the profiler, then your next solution is gross guessing changes, such as "reimport all textures as 32x32 tiny textures" or "replace some complex 3D objects with cubes/capsules" to try and figure out what is bogging you down.

    Each experiment you do may give you intel about what is causing the performance issue that you identified. More importantly let you eliminate candidates for optimization. For instance if you swap out your biggest textures with 32x32 stamps and you STILL have a problem, you may be able to eliminate textures as an issue and move onto something else.

    This sort of speculative optimization assumes you're properly using source control so it takes one click to revert to the way your project was before if there is no improvement, while carefully making notes about what you have tried and more importantly what results it has had.

    "Software does not run in a magic fairy aether powered by the fevered dreams of CS PhDs." - Mike Acton
     
  3. Airashawn

    Airashawn

    Joined:
    Jun 9, 2020
    Posts:
    18
    I'll add a ton of screenshots to help explain what's happening. The dip in the CPU usage is while ReturnColor is being run every frame. The black is what's being painted. It is alive, moving, and growing, or at least that's how it is supposed to feel. As it stands, I could have anywhere between 1-4 player characters needing to know if they are standing in the darkness. Maybe some enemies too if I can decrease the frame drop. Keeping a Color[,] array might work but I'm not entirely sure where to start when I want the darkness to have such soft edges.
    upload_2024-1-11_17-13-53.png
    upload_2024-1-11_17-15-32.png
    upload_2024-1-11_17-17-13.png
    upload_2024-1-11_17-27-58.png
    The top is just to the left of the grey NPC. The middle is just under the Player. the bottom is the dark hazy area. There is more paint than just darkness but this is a good example. When the numbers reach certain thresholds I want different things to happen so knowing the exact color is very helpful.
    upload_2024-1-11_17-31-31.png
     
  4. Airashawn

    Airashawn

    Joined:
    Jun 9, 2020
    Posts:
    18
    Didn't realize I could reply directly. Using the ReturnColor method always seems to cause a noticeable frame drop.
     
  5. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    38,697
    Right... I noted that it gets ALL the pixels every time it needs to give you ONE pixel.

    That is why I wrote these two specific ideas:

     
  6. Airashawn

    Airashawn

    Joined:
    Jun 9, 2020
    Posts:
    18
    The painting occurs more frequently than the query and it affects more than just one area at a time. It paints gradually in one area over a few seconds with mathematical falloff around the edges. Ideally, I'd like to know what color everyone is standing on at all times but I could settle for every 1/4 - 1/2 second. As for trying to make an array, I'm concerned about losing the falloff effect of the painting process. I want areas of not complete darkness, the falloff is where my darkness gets its creepy tendril look (more impressive when not screenshots). Standing in darker areas should affect the players more than lighter areas but I don't know how to include that without massive calculations bogging up the system every paint stroke. With the array system, I would have to keep track of the main area of darkness being painted and then figure out what the falloff is in the surrounding areas and store that info, but when another paint stroke goes over the same area it would change all those values, and this would happen frequently. Is the method I use for getting the pixel color the best way to get the pixel color? I'm wondering if there is a way to reduce the amount of pixels it has to count in the ReadPixels process rather than checking the whole texture every time I need to Return Color. What I thought might work is separating the main texture we're checking with ReturnColor into more manageable pieces as if I were to crop the edges of the texture to the area my player characters are in or separate the texture into quadrants and then check the pixel colors because I notice a difference in frame drops with lower a resolution render texture.
     
  7. Ryiah

    Ryiah

    Joined:
    Oct 11, 2012
    Posts:
    21,157
    No. GetPixel() has to decompress (assuming the texture is compressed) and convert the pixel data which is where the bulk of the slowdown is coming from. I've never bothered with any of these methods beyond playing around with them so I don't know how effective they are but you could see if any of these help.

    Note that these were written with the assistance of ChatGPT (GPT-4). Both of these approaches assume that the texture is stored as ARGB32. If they don't work correctly I can try to fix any bugs but I don't have an easy way to verify that they work on my end.

    Code (csharp):
    1. public Color ReturnColor(RaycastHit _hit)
    2. {
    3.     RenderTexture _splatmap = _hit.collider.GetComponent<PaintableTerrain>()._splatmap;
    4.     RenderTexture temp = RenderTexture.GetTemporary(_splatmap.width, _splatmap.height, 0, RenderTextureFormat.ARGBFloat, RenderTextureReadWrite.Linear);
    5.  
    6.     Graphics.Blit(_splatmap, temp);
    7.  
    8.     RenderTexture previous = RenderTexture.active;
    9.     RenderTexture.active = temp;
    10.  
    11.     if (myTexture2D == null || myTexture2D.width != _splatmap.width || myTexture2D.height != _splatmap.height)
    12.     {
    13.         myTexture2D.Reinitialize(_splatmap.width, _splatmap.height, TextureFormat.ARGB32, false);
    14.     }
    15.  
    16.     myTexture2D.ReadPixels(new Rect(0, 0, temp.width, temp.height), 0, 0);
    17.     myTexture2D.Apply();
    18.  
    19.     RenderTexture.active = previous;
    20.  
    21.     Vector2 pixelPos = _hit.textureCoord;
    22.     pixelPos.x *= _splatmap.width;
    23.     pixelPos.y *= _splatmap.height;
    24.  
    25.     // Using GetPixelData to access the pixel data
    26.     var pixelData = myTexture2D.GetPixelData<Color32>(0);
    27.     Color32 pixelColor32 = pixelData[(int)pixelPos.y * _splatmap.width + (int)pixelPos.x];
    28.     Color pixelColor = pixelColor32;
    29.  
    30.     RenderTexture.ReleaseTemporary(temp);
    31.  
    32.     return pixelColor;
    33. }

    Code (csharp):
    1. public Color ReturnColor(RaycastHit _hit)
    2. {
    3.     RenderTexture _splatmap = _hit.collider.GetComponent<PaintableTerrain>()._splatmap;
    4.     RenderTexture temp = RenderTexture.GetTemporary(_splatmap.width, _splatmap.height, 0, RenderTextureFormat.ARGBFloat, RenderTextureReadWrite.Linear);
    5.  
    6.     Graphics.Blit(_splatmap, temp);
    7.  
    8.     RenderTexture previous = RenderTexture.active;
    9.     RenderTexture.active = temp;
    10.  
    11.     if (myTexture2D == null || myTexture2D.width != _splatmap.width || myTexture2D.height != _splatmap.height)
    12.     {
    13.         myTexture2D = new Texture2D(_splatmap.width, _splatmap.height, TextureFormat.ARGB32, false, true);
    14.     }
    15.  
    16.     myTexture2D.ReadPixels(new Rect(0, 0, temp.width, temp.height), 0, 0);
    17.     myTexture2D.Apply();
    18.  
    19.     RenderTexture.active = previous;
    20.  
    21.     Vector2 pixelUV = _hit.textureCoord;
    22.     pixelUV.x *= myTexture2D.width;
    23.     pixelUV.y *= myTexture2D.height;
    24.  
    25.     Color pixelColor = Color.clear;
    26.     NativeArray<byte> pixelData = myTexture2D.GetRawTextureData<byte>();
    27.  
    28.     // Assuming the texture is ARGB32, each pixel is represented by 4 bytes
    29.     int index = (int)(pixelUV.y * myTexture2D.width + pixelUV.x) * 4;
    30.     if (index < pixelData.Length - 4)
    31.     {
    32.         pixelColor.r = pixelData[index + 1] / 255f;
    33.         pixelColor.g = pixelData[index + 2] / 255f;
    34.         pixelColor.b = pixelData[index + 3] / 255f;
    35.         pixelColor.a = pixelData[index] / 255f;
    36.     }
    37.  
    38.     RenderTexture.ReleaseTemporary(temp);
    39.  
    40.     return pixelColor;
    41. }
     
  8. Airashawn

    Airashawn

    Joined:
    Jun 9, 2020
    Posts:
    18
    Please forgive my confusion, you believe that GetPixel is what's causing the frame drop? Not the ReadPixels? I had assumed (and perhaps incorrectly) that ReadPixels was the culprit because when I lowered the render texture resolution the frame drops improved.
     
  9. spiney199

    spiney199

    Joined:
    Feb 11, 2021
    Posts:
    7,861
    The profiler can answer this question for you. Deep-profile. Explore the call-stacks. See where most of the overhead is. Don't assume, measure.
     
    Bunny83 likes this.
  10. Ryiah

    Ryiah

    Joined:
    Oct 11, 2012
    Posts:
    21,157
    Both of them can be problematic but for different reasons. GetPixel() has to do conversion work while ReadPixels() has to wait on the GPU. A well written alternative is more than just a few lines of code.

    If I weren't just going to take the second approach provided by @Kurt-Dekker (ie storing a second copy of my data whenever I'm writing to the terrain textures) I'd rewrite the code to not perform excessive operations. For example modifying my code to grab the data from the GPU once per frame instead of every call to ReturnColor().

    Speaking of getting data from the GPU the best way to do it is with the following method not ReadPixels() as it isn't necessary to wait on the GPU to be finished with its scheduled tasks before you can access the data.

    https://docs.unity3d.com/ScriptReference/Rendering.AsyncGPUReadback.RequestIntoNativeArray.html
     
    Last edited: Jan 13, 2024
  11. Airashawn

    Airashawn

    Joined:
    Jun 9, 2020
    Posts:
    18
    I'm rather unfamiliar with the profiler. What you see in my screenshots is my only practical experience with it. It's a little hard for me to know where to look first but I think it is here?
    Before ReturnColor:
    upload_2024-1-12_19-27-52.png
    During ReturnColor:
    upload_2024-1-12_19-27-29.png

    PlayerLoop is now taking up 75.1%


    I'm most definitely looking to understand why what I'm doing is incorrect. I do not want a quick fix to this. All of these comments are incredibly helpful to me in better grasping this concept. I think you must have edited your post while writing this because I was going to follow up what you said with an additional question but now I don't see what I was going to ask about. Originally I think you asked if it was necessary to call ReadPixels every time I needed the pixel color but I've tried to only use GetPixel to no avail and I never understood why.

    As for the link to AsyncGPUReadback, do you know of any good youtube tutorials on the subject, with Unity Documentation I get lost and overwhelmed with what I'm looking for in relation to my often VERY specific use case.
     
  12. spiney199

    spiney199

    Joined:
    Feb 11, 2021
    Posts:
    7,861
    You need to dig into the overview heirarchy (the part at the bottom) and see what time is being spent on each individual method. Not just look at the top level methods.

    You may need to enable deep profiling (one of the buttons at the top) to help with this.
     
  13. Ryiah

    Ryiah

    Joined:
    Oct 11, 2012
    Posts:
    21,157
    No, but that's because I don't bother with tutorials for anything that is older than approximately April 2023. If that sounds very specific it's because that's the current approximate cutoff date for ChatGPT (it's dependent on the info though as to how old it covers).

    It does have its drawbacks like the free version (GPT-3.5) being far weaker than the paid version (GPT-4), and it will hallucinate (https://www.ibm.com/topics/ai-hallucinations) creating incorrect code that it's never quite able to make work even though it's confident that it will, but if you can live with these limits it's far beyond most tutorials.

    For example here's the result of my asking for an example of AsyncGPUReadback:

    Code (csharp):
    1. using UnityEngine;
    2. using UnityEngine.Rendering;
    3.  
    4. public class AsyncGPUReadbackExample : MonoBehaviour
    5. {
    6.     void Start()
    7.     {
    8.         // Create a texture
    9.         Texture2D texture = new Texture2D(256, 256, TextureFormat.RGBA32, false);
    10.  
    11.         // Request Async GPU Readback
    12.         AsyncGPUReadback.Request(texture, 0, TextureFormat.RGBA32, OnCompleteReadback);
    13.     }
    14.  
    15.     void OnCompleteReadback(AsyncGPUReadbackRequest request)
    16.     {
    17.         if (request.hasError)
    18.         {
    19.             Debug.Log("GPU readback error detected.");
    20.             return;
    21.         }
    22.  
    23.         // Process the data
    24.         Texture2D texture = new Texture2D(256, 256, TextureFormat.RGBA32, false);
    25.         texture.LoadRawTextureData(request.GetData<uint>());
    26.         texture.Apply();
    27.  
    28.         // Use the texture as needed
    29.     }
    30. }

    Additionally, unlike a tutorial that you've stumbled upon online, you can ask questions. For example I asked for an explanation of the above code and it gave this.


    It's ability to explain the code doesn't necessarily stop there either. You can ask for a more in-depth explanation of specific sections of the code. Whether it will be able to comply is another matter but that goes back to the issue of hallucinations. You have to be careful that it's explained correctly. It's always confident even when it's wrong.

    If you haven't already I recommend trying your code out in a build rather than in the editor. Here's the docs entry on how to attach the profiler to a build.

    https://docs.unity3d.com/Manual/profiler-profiling-applications.html
     
    Last edited: Jan 13, 2024
    spiney199 likes this.
  14. Airashawn

    Airashawn

    Joined:
    Jun 9, 2020
    Posts:
    18
    Does this look right? I activated the deep profiler and this is what appears to be taking up a majority of that percent
    upload_2024-1-12_20-48-43.png


    I wasn't aware Chat-GPT had become so versatile and useful for learning code! I appreciate the suggestion! I knew that the editor loop was a big reason as to why you would make a build version but is there any other reason I'm unaware of?
     
  15. spiney199

    spiney199

    Joined:
    Feb 11, 2021
    Posts:
    7,861
    This just means your CPU is waiting on the GPU to finish rendering. Quick google search could've told you this.

    So whatever you're doing is trying to access the GPU while it's unavailable, holding up Unity's main thread. So you probably need to look at the async methods that were mentioned by Ryiah.

    Also word of caution on Chat GPT: It tends to make up stuff a lot. Seen a lot of newbies here with incorrect ideas because Chat GPT made up nonsense for them.
     
    Ryiah likes this.
  16. Ryiah

    Ryiah

    Joined:
    Oct 11, 2012
    Posts:
    21,157
    Nothing that immediately comes to mind aside from general editor jank.

    Yes, like @spiney199 mentioned Semaphore.WaitForSignal is the CPU waiting on the GPU, but the part that wasn't mentioned is its parent Gfx.ReadbackImage which is ReadPixels(). It's forcing the semaphore because it's blocking (ie pausing) the main thread until data has been read.

    Kinda. It's a little more complex because the semaphore is a side effect rather than the actual culprit.
     
    Last edited: Jan 13, 2024
    spiney199 likes this.
  17. Airashawn

    Airashawn

    Joined:
    Jun 9, 2020
    Posts:
    18
    Okay, having spent several days trying to understand AsyncGPUReadback and the examples presented to me in this forum, I think I reached the solution! Here's my code slightly modified to include AsyncGPUReadback and exclude readpixels:

    Code (CSharp):
    1. public Color ReturnColor(RaycastHit _hit)
    2. {
    3.     RenderTexture _splatmap = _hit.collider.GetComponent<PaintableTerrain>()._splatmap;
    4.     RenderTexture temp = RenderTexture.GetTemporary(_splatmap.width, _splatmap.height, 0, RenderTextureFormat.ARGBFloat, RenderTextureReadWrite.Linear);
    5.  
    6.     Graphics.Blit(_splatmap, temp);
    7.  
    8.     RenderTexture previous = RenderTexture.active;
    9.     RenderTexture.active = temp;
    10.  
    11.     if (myTexture2D == null || myTexture2D.width != _splatmap.width || myTexture2D.height != _splatmap.height)
    12.     {
    13.         myTexture2D.Reinitialize(_splatmap.width, _splatmap.height, TextureFormat.ARGB32, false);
    14.     }
    15.  
    16.     AsyncGPUReadback.Request(_splatmap, 0, TextureFormat.ARGB32, OnCompleteReadback);
    17.  
    18.     //myTexture2D.ReadPixels(new Rect(0, 0, temp.width, temp.height), 0, 0);
    19.  
    20.     //myTexture2D.Apply();
    21.  
    22.     RenderTexture.active = previous;
    23.  
    24.     Vector2 pixelPos = _hit.textureCoord;
    25.     pixelPos.x *= _splatmap.width;
    26.     pixelPos.y *= _splatmap.height;
    27.  
    28.     Color pixelColor = myTexture2D.GetPixel((int)pixelPos.x, (int)pixelPos.y);
    29.     RenderTexture.ReleaseTemporary(temp);
    30.  
    31.  
    32.     return pixelColor;
    33. }
    34.  
    35. void OnCompleteReadback(AsyncGPUReadbackRequest request)
    36. {
    37.     if (request.hasError)
    38.     {
    39.         Debug.Log("GPU readback error detected.");
    40.         return;
    41.     }
    42.  
    43.     myTexture2D.LoadRawTextureData(request.GetData<uint>());
    44.     myTexture2D.Apply();
    45. }

    The frame drops have disappeared or at least become unnoticeable to the eye (and profiler!) The only remaining frame stutter I have appears to be the editor loop and should go away in a build version according to what I have read online.

    Here's the profiler with ReturnColor intermixed
    upload_2024-1-18_13-4-43.png
    The dip seems to be the editor loop:
    upload_2024-1-18_13-7-59.png

    Is there anything I am still doing that isn't necessary or is amateurish that I should change? Understanding why or how I am making mistakes is a big help to a self-taught programmer like me! If everything looks good I want to thank all of you for your help!