Best performance for hundreds or more meshes with slightly varied materials (e.g. color in URP)?

I am working on a URP game in DOTS that has several hundred enemies onscreen at a time, and the player can deal damage in large areas that potentially damage hundreds of enemies at once. I am hoping to implement a simple white blinking effect when the enemies take damage (with a fade out, so not a binary standard material/white material), but I’m not sure what the most performant way to create the effect would be.

From what I understand URP’s SRP batching works on shaders, not materials, so based on that I should create a separate material at runtime for each enemy and change the color on the individual material for an enemy when they take damage. However, I’m not sure how SRP batching differs or interacts with GPU material instancing, or if material property blocks would be more performant with GPU instancing.

As a third, extreme option I’m debating converting all of the enemies to individual particles on a global VFX graph since that’s what I’m already doing with projectiles, but that approach adds a lot of complexity that I would like to avoid if performant per-enemy materials can be achieved another way.

I’m somewhat inexperienced with shaders and materials, especially at this scale, so thanks in advance for any insights!

Last time I checked (which, admittedly was a couple years ago) the SRP batcher didn’t support Material Property Blocks. But luckily that doesn’t really matter. In this case the easiest way is probably just to instantiate a new material via grabbing each renderer’s “.material” property and change the property(ies) needed for the effect.

Performance-wise this won’t hurt rendering. It’s exactly the sort of thing the SRP batcher is made to do. I’m not sure about memory usage though so that might be worth looking at. Like the above post said, it’s probably better to get real numbers though. I’d make a small scene with a couple scripts to test what it’s like grabbing the “.material” value of each renderer and changing their properties. If everything is fine then you are in business. Just, don’t fall into the trap of looking at the draw calls (unless you’re getting close to the hundreds-of-thousands, then it might matter). The SetPass calls is really what is being affected. And even then the frame time is the true indicator of performance.

Keep in mind this is all for desktop. I can’t really say about any other platform.

Hi @NonPolynomialTim,

I posted on the topic some time ago here.

If you were to use Material Property Blocks on 100+ instances, you would definitely take a substantial performance hit.

Long story short, we currently don’t have a solution that makes it as easy as using Material Property Blocks while not breaking the SRP Batcher.
That said, I have made a prototype to simplify things. You’ll find it here on my private github.
User disclaimer: this is personal exploratory work, that Unity isn’t liable for in any form.

Now, since you mentioned using DOTS, you certainly want to take a look at this post on BatchRendererGroup.

Hope this helps. Please keep me posted on how this works for you.

3 Likes

I knew I was probably missing some DOTS-specific solution, so thank you for pointing me in the right direction @FredMoreau , and for the bit about draw calls vs SetPass calls @Sluggy1 . I was able to use the URPMaterialPropertyBaseColor ECS component to drive the blink without a hitch.

And to @BasedDestiny2025, I’d like to think that it wasn’t your intention, which is why I’m letting you know—your response was incredibly patronizing and assumptive. Recommending to always take the naive approach until it becomes a problem is not great advice. While premature optimization can be problematic, frequently large performance bottlenecks can be foreseen and are easier to handle as early as possible in the development cycle. This is why someone might choose to use DOTS from inception instead of developing 3/4 of a game with MonoBehaviours and then realizing that they need to rewrite everything in DOTS.

Telling people that their game is meaningless is also hardly productive.

Thanks again for the insightful responses!

In all honestly, that removed poster looked extremely suspiciously like a chatbot. They were nearly quoting another well-known poster on the forums with the exception that the person being quoted knows when he’s dealing with a complete beginner or someone that knows something already. Unfortunately, due to the frequency of his posts, the AI bots have picked up on what he says and started repeating them verbatim to anything that looks like it could apply. Anyway, neither here nor there.

I looks like I missed the rather large detail that you were dealing with ECS and not GameObject rendered objects. Whoops. Luckily I think it should still be relatively the same. The BRG will likely do the trick. You could create some GPU buffers with the extra data per object. How exactly you get the instance to each is beyond my knowledge since I’ve yet to play with it. That being said I think it might still be worth looking into just instancing new materials at runtime. That’s what I’m doing currently and it doesn’t seem to hurt much even with thousands of unique materials and tens of thousands of draw calls.