Search Unity

  1. Megacity Metro Demo now available. Download now.
    Dismiss Notice
  2. Unity support for visionOS is now available. Learn more in our blog post.
    Dismiss Notice

Did some testing on best practices for changing the color of a large number of objects...

Discussion in 'General Graphics' started by AdamBL, Aug 13, 2018.

  1. AdamBL

    AdamBL

    Joined:
    Oct 27, 2016
    Posts:
    29
    Originally, this came from a desire to recreate different view modes, similar to something like Cities Skyline:



    Basically, when you switch view modes, items get re-colored based on their status (i.e. blue if they have power, red if they don't). The challenge is that there could potentially be hundreds (if not thousands) of objects on screen, and you want to change the look of each of them quickly and efficiently

    The only information I could find online was a Reddit thread which suggested that they might've used replacement shaders to achieve the effect. However, I don't think that's the solution (or the entirety of it at least) because while this would work if you knew what color a building would be in each view mode (i.e. Red in View Mode 1, Blue in View Mode 2), in this case each view mode represents a range of possible colors (in Power Mode it's Blue if healthy, Red if not, in water mode it's Green if healthy, Yellow if not...) and I believe you'd need something more complex than replacement shaders to achieve that.

    Generally, I came up with 4 possible approaches, arranged from simplest to most complex:

    1. Change the color of the material
      • Pros: Simple
      • Cons: Creates a new material instance for every object
    2. Change the material itself
      • Pros: Can batch draw calls if pulling from a limited set of materials
      • Cons: Potentially many materials to manage and update
    3. Change the color of the material via a Material Property Block
      • Pros: Potentially more efficient since it doesn't create a new instance of the material
      • Cons: Still can't use batching
    4. Provide a texture atlas of various color and update UVs to proper position
      • Pros: Potentially most efficient, since the entire scene would share one material
      • Cons: Requires computation to calculate UVs, requires baking colors to materials
    Originally, I picked option 3 and it worked ok. However, as our project got more complex we started seeing some frame rate lags, and I decided to investigate which of these worked best.

    My test environment was as follows: Spawn 3,375 (15x15x15) cubes into an empty scene with one directional light and then swap all of their colors at random via the 4 different methods. This was all done on my 2015 MacBook Pro, running Unity 2017.3. In general, without any of these effects applied, the scene ran at ~35-40 FPS. If anyone has any comments on my methodology, or suggestions of other methods to test, I'd love to hear them!

    Here are my results:

    Color Change Method



    Avg. FPS: 15-20
    Spike on Switch: Yes

    The simplest method, as it turns out, was one of the worst. Apparently having 3,000+ materials in your scene isn't great. Who knew? There's a big frame rate spike when you first call it, and the frame rate was ~1/2 of what it was. Would not recommend.

    Material Change Method



    Avg. FPS: 35-40
    Spike on Switch: No

    The second simplest method, as it turns out, was probably the best. There was no frame rate spike upon switching, and it didn't affect the frame rate of the scene at all. The only downside is that you need to update and maintain a separate material for each possible view mode state (power healthy, power unhealthy, water healthy, water unhealthy, etc...)

    Material Property Block Method



    Avg. FPS: Variable
    Spike on Switch: Variable

    There is some weirdness around using Material Property Blocks that I don't entirely understand. If you run the scene without shadows, it's right around as efficient as material swapping: no spike, full framerate. If you turn shadows on, however, it actually ends up being the worst method: massive spike, around 10-15 FPS. I have no idea why this is, but it seems like Unity has to do extra shadow draw calls or something? If anyone has any ideas, I'd love to hear them.

    UV Swap Method



    Avg. FPS: 35-40
    Spike on Switch: Yes


    This method was as efficient as swapping the materials (if not a tiny bit faster) but the major downside is the fairly large framerate spike you get when you switch. You are doing 24 calculations per cube, which adds up to 81,000 calculations, which ends up being fairly time consuming. The slight plus is it might make it easier to manage all the potential statuses for all the different view modes, but probably isn't worth it.

    Bonus Method: Replacement Shaders



    Avg. FPS: 35-40
    Spike on Switch: No


    On a whim, I decided to try to implement a basic replacement shader method. It actually worked pretty well, not really adding any overhead to the existing material swap method. But, again, as far as I can tell you'd still need the exact same number of materials as you would without using replacement shaders, it'd just be a different method for switching between them.

    Summary

    As basic as it sounds, my suggestion for if you need to switch the color for 1,000s of objects at once is to simply switch their material. The key is to have a reference list of materials to choose from rather than instancing a new one when you switch.

    A lot of this was a learning exercise for me, so if anyone has any thoughts or suggestions I'd love to hear them!
     
  2. brownboot67

    brownboot67

    Joined:
    Jan 5, 2013
    Posts:
    375
    For relatively simple views and relatively low numbers it's conceivable you could pack everything into one shader and fade them all by setting a global shader property. Then you're only limited by how complex that shader is.
     
  3. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,329
    A key thing you're missing. The best way to use material property blocks is in conjunction with an instanced shader which has the color as an instanced property. Ignoring what seems to be a bug for shadows, with out instancing it's effectively the same as having a unique material per object.
     
  4. AdamBL

    AdamBL

    Joined:
    Oct 27, 2016
    Posts:
    29
    Interesting, how would you propose changing the color?

    That's a good point, in our main project we use a custom shader that used [PerRendererData] on the _Color component so it could be instanced properly. What's interesting is that it actually seemed to work really well just using the standard shader... there's essentially no difference in framerate.

    However I did some additional tests, once using the "Enable GPU Instancing" checkbox, and once by modifying the standard shader I'm using by adding [PerRendererData] to the _Color property.
    1. Standard shader, no modifications: Works flawlessly, maybe a ~5 FPS drop after changing the property blocks
    2. Standard shader, Enable GPU Instancing checked: Pretty significant framerate drop just from using GPU Instancing (~10-15 FPS). You then lose another 10-15 when changing the property blocks
    3. Standard shader, [PerRenderData] added: Right around as fast as standard shader, maybe a smidge faster?
    This makes me think that maybe Unity has some sort of system for automatically modifying the shader when you change a property block? Also, I'm not entirely sure of the value of GPU instancing... unless maybe my GPU just isn't powerful enough to benefit from it.
     
  5. brownboot67

    brownboot67

    Joined:
    Jan 5, 2013
    Posts:
    375
    Code (csharp):
    1. float4 color = lerp(someColor, someOtherColor, _GlobalLerpValue);
     
  6. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,329
    This doesn’t actually do anything. It’s existance predates instancing support in Unity and is completely unrelated. It’s mainly used for sprite shaders, but even there I believe it’s use is vestigial. I've seen a lot of tutorials out there talk about it as being some major performance win, to use along side MaterialPropertyBlocks, but I've never found them to have any impact on anything. As best I can tell they are simply a way to hide the value in the inspector and have no other impact. For a shader property to be instanced, you need to modify the shader code in a specific way. Otherwise 1 and 3 are identical.
    https://docs.unity3d.com/Manual/GPUInstancing.html

    Instancing has some overhead involved, and your GPU may be a little too slow to make full use of this technique. However once you start changing the material properties, if your material isn't set to use instanced, or if you haven’t setup the property you’re changing to use instancing, or you’re not using Material Property Blocks to change those instanced properties, it’ll end up being no better than the unique material per object option. The built in Standard shader doesn't have any of its properties setup for instancing, so it can only instance objects using exactly the same material with no material property block (or potentially when using the identical material property block values).

    For this to work you'd need to make a new shader, like a Surface Shader, and setup a _Color property like shown in the examples on the page linked above. Once that's setup, the result should be that every object can have a unique color and it'll be no slower or faster than every object having the same color, and you can modify those values at whim.

    https://catlikecoding.com/unity/tutorials/rendering/part-19/
     
  7. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,329
    Also, I wonder if the reason why you're having performance problems with MaterialPropertyBlocks have nothing to do with the rendering side of things, and is perhaps an issue of how you're using them. MaterialPropertyBlock is a class, so if you're using new MaterialPropertyBlock() every frame you're creating a ton of garbage every frame. The way I use them is by creating a block on Awake() or Start() and reuse it every time. If you know you're only changing a single property value over and over, just set the value on the block and apply it to the renderer component. You can even use a single block to assign the color to every object. If you have a fixed set of colors, create that number of blocks with the color pre-assigned at start and then apply the blocks as necessary to swap the color.