Search Unity

Resolved Is there some sensible way to override materials for models with multiple materials?

Discussion in 'Graphics for ECS' started by ArunasProk, Jan 20, 2023.

  1. ArunasProk

    ArunasProk

    Joined:
    Mar 21, 2020
    Posts:
    13
    ECS splits up models with multiple materials into multiple entities (one per material). So when you want to override a material, you would be adding/modifying the component on one of its children.

    The order of those children is mostly random, and the only thing unique about them is the Material/Mesh id. So you have to search through them, retrieve the mesh id and match it to the material array, where you have the material names. You can't Burst this, and I'm not sure how much of this you can cache, given that ECS shuffles everything around after the structural changes.

    Am I missing something?
     
  2. DreamingImLatios

    DreamingImLatios

    Joined:
    Jun 3, 2017
    Posts:
    4,264
    You can simply look at the MaterialMeshInfo's Submesh. That should correspond to the material index in your MeshRenderer.
     
    JussiKnuuttila likes this.
  3. ArunasProk

    ArunasProk

    Joined:
    Mar 21, 2020
    Posts:
    13
    Yeah, I know how to check if the entity has the material I want (I tried to imply it).

    My issue is that I can think of a way to get the entity efficiently. Most operations will happen on the parent (like modifying transform and such). If, in addition to that, I want to have some mesh effects, it seems my choices are to tag all children for processing (to figure out which one has the right mesh), or create an entity query in the job that modifies the parent, which prevents Burst usage.

    It feels that there should be less stressful ways of overriding materials on objects with 2+ of them when overriding 1 material is so easy. (And it doesn't help that the material overriding docs don't even mention that 2+ materials are a concern.)
     
  4. DreamingImLatios

    DreamingImLatios

    Joined:
    Jun 3, 2017
    Posts:
    4,264
    Oh. I thought you were digging through the RenderMeshArray, but rereading your post, you did (somewhat ambiguously) specify what I described. But at the same time, you also said this:
    That suggests you are doing something wrong because you shouldn't need the name of the material to do what you want to do.

    EntityQueries can be created with Burst (outside a job), so I suspect you are misunderstanding some things about Burst.

    Really, I think you need a custom baking system, which I will admit is pretty frickin hard to write correctly. I plan to do a detailed write-up for the community on the topic after the next prerelease (assuming Unity doesn't include more documentation about baking systems in their next prerelease).
     
  5. ArunasProk

    ArunasProk

    Joined:
    Mar 21, 2020
    Posts:
    13
    That's exactly how I feel. But I have no idea how to go about getting the material id otherwise. It's not like I'm creating it or assigning it at any point - it's part of the prefab. Usually, you'd get it by using its index, but that information is lost in ECS. Custom bakers did cross my mind, but the baking docs are as rudimentary as material overriding. But anyway, that is not the main issue here.

    My misunderstanding stuff is quite likely, as I'm still learning ECS, but let me elaborate, as it seems I'm doing a poor job explaining my problem.

    If I wanted to move and highlight an Entity with one material, I could do this (ignore small mistakes - untested code):
    Code (CSharp):
    1. [BurstCompile]
    2. public partial struct HighlightJob : IJobEntity {
    3.     public EntityCommandBuffer.ParallelWriter Buffer;
    4.  
    5.     private void Execute(
    6.         [EntityIndexInQuery] int sortKey, Entity entity, in HighlightData data,
    7.         TransformAspect aspect) {
    8.  
    9.         aspect.LocalPosition = data.position;
    10.         Buffer.SetComponent(sortKey, entity,
    11.             new URPMaterialPropertyBaseColor { Value = data.colour, });
    12.     }
    13. }
    With multiple materials, this doesn't work, as ECS creates children to stash the materials. It would be nice if you could do something along the lines of:
    Code (CSharp):
    1. private void Execute(
    2.     [EntityIndexInQuery] int sortKey, Entity entity, in HighlightData data,
    3.     in DynamicBuffer<Child> children, TransformAspect aspect) {
    4.  
    5.     aspect.LocalPosition = data.position;
    6.     Buffer.SetComponent(sortKey, children[1],
    7.        new URPMaterialPropertyBaseColor { Value = data.colour, });
    8. }
    Unfortunately, this also does not work, as the order of children is random. So I'm stuck with something like this:
    Code (CSharp):
    1. private void Execute(
    2.     [EntityIndexInQuery] int sortKey, Entity entity, in HighlightData data,
    3.     in DynamicBuffer<Child> children, TransformAspect aspect) {
    4.  
    5.     aspect.LocalPosition = data.position;
    6.     for (var i = 0; i < children.Length; i++) {
    7.         var info = World.DefaultGameObjectInjectionWorld.EntityManager
    8.              .GetComponentData<MaterialMeshInfo>(children[i].Value);
    9.         if (!EvalMeshSomehow(info))
    10.             continue;
    11.  
    12.         Buffer.SetComponent(sortKey, children[i],
    13.             new URPMaterialPropertyBaseColor { Value = data.colour, });
    14.     }
    15. }
    Which is horrible, and not burstable, or something like this:
    Code (CSharp):
    1.  
    2. private void Execute(
    3.     [EntityIndexInQuery] int sortKey, in HighlightData data,
    4.     in DynamicBuffer<Child> children, TransformAspect aspect) {
    5.  
    6.     aspect.LocalPosition = data.position;
    7.     for (var i = 0; i < children.Length; i++)
    8.         Buffer.AddComponent<CheckMeshAndHighlighTag>(sortKey, children[i]);
    9.     // Do actual evaluation and assigning in a separate job.
    10.     }
    11. }
    And I'm obviously not happy with either of the two approaches, even ignoring the fact that I have to mess with material name strings for the evaluation part.
     
    Last edited: Jan 20, 2023
  6. DreamingImLatios

    DreamingImLatios

    Joined:
    Jun 3, 2017
    Posts:
    4,264
    Code (CSharp):
    1. bool EvalMeshSomehow(in MaterialMeshInfo info) => originalIndexInMeshRenderersMaterialArray == infoSubmesh;
    Baking the reference to the child entity with the correct material is stupidly difficult for a beginner. I can help you with it, but right now I think it is best that you learn to get this code Burst-compiled and jobified without that first, because I see a lot of other misunderstandings in your code that will make it very difficult for you to follow along with me.
     
  7. ArunasProk

    ArunasProk

    Joined:
    Mar 21, 2020
    Posts:
    13
    The code part you've written is obvious. Get the id in the system, pass it into the job through the public variable etc. It was emitted because it was trivial, not because that's where the problem lies.

    And the only part I don't know how to Jobify/Burst is the GetComponentData inside the job (short of delegating to another job in my last example). So any hints on what I should be researching to solve that? Or what are some of my other misunderstandings?


    P.S. Sorry if I sound snarky. I came here because I know that I am missing critical pieces, and the docs don't have the answers. Repeated replies of "Oh, you just don't understand stuff." are rather frustrating.
     
    Last edited: Jan 20, 2023
  8. DreamingImLatios

    DreamingImLatios

    Joined:
    Jun 3, 2017
    Posts:
    4,264
    You say it is trivial, but you also kept mentioning strings even though there's no strings involved, so it is difficult for me to figure out where you are struggling sometimes.

    This is probably where most of your misunderstandings lie. There is something called ComponentLookup. You can get one via SystemAPI, and it allows you to look up components using Entity references, like the entities in the child buffer. You can also test if that component is even present, and by either making your job single-threaded or by adding [NativedisableParallelForRestriction] to the ComponentLookup field in the job, you can even write new values.

    In this case, because you are iterating parents, you can safely use [NativeDisableParallelForRestriction] to write component values to the children using ComponentLookup. No EntityCommandBuffer needed.

    No worries. I'm just trying to take things only a couple pieces at a time.
     
  9. ArunasProk

    ArunasProk

    Joined:
    Mar 21, 2020
    Posts:
    13
    It's a separate issue.
    I'm loading prefabs (a model with materials) without using a baker. That means Unity splits up the model however it wants, and I get no references to the child objects or the submeshes/materials. The only information I have is what is on the prefab - the material names (as their order is also lost). So to get the ids, I have to do a lookup in RenderMeshArray using the name strings. It's not really affecting anything, as it can be done once per update and passed to the jobs, it's just, you know - "ew, strings". (And yes, I know, comparing pointers to managed mesh objects would make it slightly better, but not by much.)
    I'm also aware that making a proper baker would be a way to go but have to catch up on the more urgent subjects first.
    Thanks, that looks exactly like what I need.

    And it also looks like there's no choice but to read the whole of the scripting API for now, as there is no mention of it in the manual...
     
  10. DreamingImLatios

    DreamingImLatios

    Joined:
    Jun 3, 2017
    Posts:
    4,264
    The manual is not the only necessary resource for learning these initial things. You also should probably read this: https://github.com/Unity-Technologi.../tree/master/EntitiesSamples/EntitiesTutorial

    I think you need to elaborate on this. Because either you have the original GameObject at runtime, or you are using bakers indirectly and completely worked around a non-existent issue if you had just used a baker to capture the data you care about.
     
  11. ArunasProk

    ArunasProk

    Joined:
    Mar 21, 2020
    Posts:
    13
    I have a MonoBehaviour that holds references to prefabs in public fields. And I do use a simple baker to convert it into a managed component. With the basic baker, I do not get any control over how those prefabs are getting converted.

    Capturing the simple material information (string name/pointer), which I can later use for RenderMeshArray lookup, is easy. Anything beyond that - I have no idea where to start. I simply don't know enough about the baking/instantiation process to figure out how to work around the arbitrary shuffling of child entities (they appear different between chunks).

    Also, thanks for the link.
     
  12. DreamingImLatios

    DreamingImLatios

    Joined:
    Jun 3, 2017
    Posts:
    4,264
    Don't capture the string name. Capture the index in the sharedMaterials array. Then as long as you have a reference to the parent entity (seems like you do), then you just need to iterate through the children and look for the one that has Submesh that matches the captured index. No more strings. More Burst.
     
  13. ArunasProk

    ArunasProk

    Joined:
    Mar 21, 2020
    Posts:
    13
    Is it safe to store and index of that array?
    I sort of assumed that things in it can swap places like in the other containers if things get destroyed.

    I really need a cheat sheet with what's safe to hold a reference and what's not...
     
  14. DreamingImLatios

    DreamingImLatios

    Joined:
    Jun 3, 2017
    Posts:
    4,264
    It is not an index into the child array. It is an index into the Mesh Renderer (from classical GameObjects) sharedMaterials array. Unity internally captures the index of each material in that array and saves it as the Submesh of MaterialMeshInfo when it bakes the MeshRenderer. So if you also capture the index, you'll have the same index as Unity does, and you can find the MaterialMeshInfo with the matching index instead of doing string comparisons. You still have to do a search, so it still isn't the most optimal solution. But integer comparisons work in Burst, and once you understand that, then it will be easier to walk you through a more performant solution (if you even need more performance by that point).
     
    lclemens likes this.
  15. ArunasProk

    ArunasProk

    Joined:
    Mar 21, 2020
    Posts:
    13
    The info that the material array index corresponds to the sub-meshes is helpful, thanks. In retrospect, I probably should have assumed/checked that much.

    And I still have no idea why you got the impression that I have any misunderstandings about Burst. I didn't care about Bursting this, because mesh id lookup is 1/update at worst, which I don't need to do inside the jobs. There's no practical difference between strings/ints (and burst/no burst) unless it helps me cut down the number of searches somehow.
     
    Last edited: Jan 21, 2023
  16. DreamingImLatios

    DreamingImLatios

    Joined:
    Jun 3, 2017
    Posts:
    4,264
    Then what were these statements about?

    I'm not trying to argue here. I'm genuinely confused.

    I'm not sure if I agree with this, but I'm not sure I even know what part of the problem you are referring to either.

    My assumption is that you were trying to set the material colors for many entities in a job, and so you were trying to Burst-ify it and parallelize it in as performant and simplest way possible. That assumption seems incorrect now.
     
    lclemens likes this.
  17. ArunasProk

    ArunasProk

    Joined:
    Mar 21, 2020
    Posts:
    13
    Seriously, if it did not occur that I might be referring to the World.Entitymanager call inside my job as "non-burstable" and keep referring to strings (which by the way can be bursted by using FixedLengthString), it might not be me who doesn't quite grasp Burst.

    Anyways, all my Burst problems went away the second you pointed me towards the ComponentLookup, so again thanks for that. I still don't like that I have to keep performing the search on the child components, and I suspect that a custom baker might help me out there, but it's an issue I can live with for now.
     
  18. MostHated

    MostHated

    Joined:
    Nov 29, 2015
    Posts:
    1,235
    The thing I have found rather annoying about this is, if a renderer had multiple materials and gets separated out into new entities per material/submesh, the original entity then becomes the parent of those new entities but for some reason does not retain any components that I place on it during baking. I would certainly be helpful for identification purposes if it didn't strip everything off, or had a way to let the new entities 'inherit' components which were added to the original before it was split.

    Ex:

    Neither the original child entity, nor the new children have the custom component I put on the original

    Code (CSharp):
    1.  ── GameObject Root/
    2.    ├── Renderer: Material Count: 1 : MyCustomComponent1 /
    3.    │   └── Becomes -> Entity: MyCustomComponent1 (New Entity has added component)
    4.    ├── ...
    5.    └── Renderer: Material Count: 2 : MyCustomComponent2 /
    6.        └── Becomes -> Entity: DynamicBuffer<Child>[2] -- (No Custom Component) /
    7.            └── Children:/
    8.                ├── New Entity 1: Material 1 (No Custom Component)
    9.                └── New Entity 2: Material 2 (No Custom Component)
    It would be nice to have one of the following:

    Either the top example, where the original (now parent) keeps the added component
    Or the second lower in which the child entities get the components that the parent was supposed to have.

    Code (CSharp):
    1. ── GameObject Root/
    2.    ├── Renderer: Material Count: 2 : MyCustomComponent2 /
    3.    │   └── Becomes -> Entity: MyCustomComponent2 : DynamicBuffer<Child>[2] (Has Custom Component)/
    4.    │       └── Children:/
    5.    │           ├── New Entity 1: Material 1 (No Custom Component)
    6.    │           └── New Entity 2: Material 2 (No Custom Component)
    7.    ├── ... OR ...
    8.    └── Renderer: Material Count: 2 : MyCustomComponent2 /
    9.        └── Becomes -> Entity : DynamicBuffer<Child>[2] (No Custom Component) /
    10.            └── Children:/
    11.                ├── New Entity 1: MyCustomComponent2 : Material 1  (Has Custom Component)
    12.                └── New Entity 2: MyCustomComponent2 : Material 2  (Has Custom Component)
    Hopefully, this makes sense. Trying to incorporate ascii diagrams can be pretty hit or miss, lol.
     
  19. DreamingImLatios

    DreamingImLatios

    Joined:
    Jun 3, 2017
    Posts:
    4,264
    Entities Graphics 1.1 allows for a single entity to have multiple materials. However, there's a bug currently where the first material has to be unique to the prefab or else the rest of the materials will get assigned incorrectly.
     
    MostHated likes this.
  20. MostHated

    MostHated

    Joined:
    Nov 29, 2015
    Posts:
    1,235
    That sounds promising, at least. Any idea how material overrides work in that case? I believe only one component of that type can be added per entity. Hopefully, it allows for one component to change the properties of multiple materials? That would be idea for my current goal, at least. Though, probably not desired in all instances.
     
  21. DreamingImLatios

    DreamingImLatios

    Joined:
    Jun 3, 2017
    Posts:
    4,264
    That is precisely how it works.
     
    MostHated likes this.