Search Unity

.material behaving as .sharedmaterial

Discussion in 'General Graphics' started by Binary-Soul, Sep 24, 2019.

  1. Binary-Soul

    Binary-Soul

    Joined:
    Nov 25, 2014
    Posts:
    33
    Hi everyone,

    I'll explain what's happening in my project and would be really thankful if anyone knows why this is happening. Perhaps my understanding on how .material and .sharedmaterial is wrong, if so, please correct me so I can fix it!

    This is my case:

    1- I have a prefab, let's call it: MyPrefab.
    2- I instantiate it (GameObject.Instantiate(MyPrefab)) and add it to my scene. Let's call this first GameObject MyInstance1.
    3- I then duplicate this instanced object MyInstance1 by using: GameObject.Instantiate(MyInstance1). Let's call this second copy MyInstance2.
    4- I then repeat step 3, but this time on MyInstance2, so I do GameObject.Instantiate(MyInstance2). Let's call this third and final copy MyInstance3.

    Now if I try to change the color of my materials, if I'm not wrong, the following should happen:

    For ANY of the 3 instances:
    If I do: MyInstanceX.GetComponent<Renderer>().sharedmaterial.color = Color.red
    All 3 GameObjects should change their color to red.

    For ANY of the 3 instances:
    If I do: MyInstanceX.GetComponent<Renderer>().material.color = Color.red
    Only the selected GameObject should change its color to red.

    Are those 2 statements right? If not, please let me know what I got wrong.

    However what is actually happening is the following:

    For ANY of the 3 instances:
    If I do: MyInstanceX.GetComponent<Renderer>().sharedmaterial.color = Color.red
    All 3 GameObjects should change their color to red.
    WORKING AS INTENDED.

    If I do: MyInstance1.GetComponent<Renderer>().material.color = Color.red
    All 3 GameObjects are changing their color to red.
    NOT WORKING AS INTENDED!!!!

    If I do: MyInstance2.GetComponent<Renderer>().material.color = Color.red
    Only MyInstance2 is changing its color to red.
    WORKING AS INTENDED.

    If I do: MyInstance3.GetComponent<Renderer>().material.color = Color.red
    Only MyInstance3 is changing its color to red.
    WORKING AS INTENDED.

    However, if I change the color in either MyInstance2 or MyInstance3 throught material.color before doing it on MyInstance1, their materials get instanced as new materials and then the connection is broken, changing MyInstance1 won't change them any more.

    Why is the first copy behaving differently and .material is behaving as .sharedmaterial?

    I'd be really thankful if someone can shed some light on this.

    Thank you!
     
  2. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,352
    Modifying .sharedmaterial.color before modifying .material should indeed modify all instances as they're all using the same material still. Once you use .material.color it should replace that renderer's material with a copy, thus modifications to the sharedmaterial of other instances should no longer affect it as it now has a unique material.

    Basically, I agree with all your assumptions. And this almost sounds like a bug, except...

    There's one thing that AFAIK is a behavioural change from past versions of Unity as I remember it. I believe when you instantiate a prefab from the asset, it automatically replaces its .material with a copy. This means that setting .material doesn't create a new material because it's already a new material from the original prefab. Instantiating multiple objects from the asset would produce unique materials for each object.

    I'm not entirely sure when this change happened, or if it was even intended, but I've seen a few people complain about this. It's certainly not behavior I would want, but you should double check what the material on the instantiated game object is called and see if it's called "material name (instance)" before you've even modified the material.

    When you instantiate using a game object in the scene, it functions as it used to making copies of the object without modifying or making a copy of the material. This means that all of the objects are using the same material, that new instance that was generated for the initial prefab...

    Basically there's a behavioral difference between instantiating a prefab asset and a game object that's causing you problems here. Personally I feel like this is a bug, though I have some vague memories of others reporting the issue and it getting closed as "Working as Intended". I'd still report it again if I were you.
     
  3. Binary-Soul

    Binary-Soul

    Joined:
    Nov 25, 2014
    Posts:
    33
    Ok, after further testing.. I've realized something from what I explained was wrong. My scene was a complex one so I thought other ingredients might be involved so I isolated the problem in a simple test scene. The problem is even unrelated to prefabs and even weirder... and very easy to reproduce in a scene with cubes.

    Following this steps, this is what happens:

    You create a first cube:
    GameObject cube1 = GameObject.CreatePrimitive(PrimitiveType.Cube);

    You create a second cube from the first:
    GameObject cube2 = GameObject.Instantiate(Cube1);

    You create a third cube from the second:
    GameObject cube3 = GameObject.Instantiate(Cube2);

    If you change the color of ANY cube through:
    cubeX.GetComponent<Renderer>().material.color = Color.red;

    Everything works fine, just the cube you change changes color, the others are left unchanged.

    However...
    If you change the color of a cube, before instantiating the next one... when you change the original cube's color, both will change together.

    Example:
    Create cube:
    GameObject cube1 = GameObject.CreatePrimitive(PrimitiveType.Cube);
    Change cube color:
    cube1.GetComponent<Renderer>().material.color = Color.red;
    Instantiate a second cube:
    GameObject cube2 = GameObject.Instantiate(Cube1);
    Change the color of the fist cube again:
    cube1.GetComponent<Renderer>().material.color = Color.blue;

    For some weird reason, both cubes turn blue.

    However, if you change the second cube, only that cube changes.
    cube2.GetComponent<Renderer>().material.color = Color.green;

    Now cube1 is blue, cube2 is green.

    Now that the problem is totally isolated and easily reproducable... I'll ask again. Does anybody have the slightest clue of why this happens?

    Thank you!

    PS: Working on Unity 2019.1.3f1, in case that helps.
     
  4. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,352
    As I explained in my previous post, if you instantiate an existing game object the new object's .sharedmaterial is the same as the original object's .material. Changing the original game object's .material color affects all the children it makes since they all share that material.

    When you modify .material it creates a new material if non has been created before, and changes .sharedmaterial to that new material. If you haven't done that to cube1 before instantiating, it has no ".material" yet, so the instantiated children don't use it as their .sharedmaterial.

    Again, look at the name of the material on the renderer component between each step.
     
  5. Binary-Soul

    Binary-Soul

    Joined:
    Nov 25, 2014
    Posts:
    33
    I understand what you explain, and yes, that is exactly what happens. However my point is, is that a desired behaviour?
    What is the point of having the .material property of the first item modify all of the duplicates? You can already do that using .sharedmaterial...

    So if I want to create duplicates that don't change when I change the original (once the .material has been set by modifying it at least once), what would your approach be?

    Right now, what I'm doing is creating a new instance of the material whenever the color gets modified. It works fine, however I feel that this shouldn't be necessary... feels like I'm doing by hand something that should be "out of the box" if I did it some other way (which I can't seem to find).

    I'm doing this whenever I change the item's color:

    Material newMat = new Material(cubeX.material);
    cubeX.material = newMat;

    Any other approach in mind?
     
  6. Binary-Soul

    Binary-Soul

    Joined:
    Nov 25, 2014
    Posts:
    33
  7. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,352
    When the game is running, accessing .material creates a new material, if one does not already exist, and replaces both the .material and .sharedmaterial for that renderer with that new material.

    Copying an existing game object using instantiate copies all of that object's settings, including whatever .sharedmaterial it has.

    Changing the material of the parent renderer after instantiating children should update it's children as they're all using the same material. The parent already has a .material, so modifying it does not create a new one.

    This is all working as expected. There are no bugs. Solutions on Unity's side would be unwanted as I do not want Unity generating a ton of identical materials in the case I do want them all to share the same material.

    Yes. Never use .color, or otherwise create or modify materials directly. Ever.

    Use material property blocks instead. These live on each renderer, and have no concept of inheritance once applied. They also don't generate a bunch of new materials unnecessarily.
    Code (csharp):
    1. private MaterialPropertyBlock mpb;
    2. private int colorID = Shader.PropertyToID("_Color");
    3.  
    4. void Awake()
    5. {
    6.     mpb = new MaterialPropertyBlock();
    7. }
    8.  
    9. void SetMaterialColor(Color color)
    10. {
    11.     mpb.SetColor(colorID, color);
    12.     GetComponent<Renderer>().SetPropertyBlock(mpb);
    13. }[/color]
    14.  
    15. Alternative #2: Null the material after setting it to force Unity to create a new one every time (highly not recommended), or before doing the instantiate.
    16. [code]rend.material.color = color;
    17. Material tempMat = rend.material;
    18. rend.material = null;
    19. rend.sharedmaterial = tempMat;
    Alternative #3: Never instantiate an existing object, always instantiate the prefab.
     
  8. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,352
    That just means they didn't already have a matching bug in their database that has been closed or marked as not-a-bug. I suspect it'll get closed once an actual dev looks at it.
     
  9. Binary-Soul

    Binary-Soul

    Joined:
    Nov 25, 2014
    Posts:
    33
    Ohh alright. Thought it meant it was "accepted" as it took some time for it to be published and what I send was re-written by someone at Unity, meaning someone had a look at it.

    I fully understand why it happens since one of your previous explanations (.material being created if it doesn't exist) I just believe it's quite counter-intuitive that the parent object and all others behave differently. When accessing a .material, I believe it should do something like:

    - Check if any other objects are using this same material.
    - If true, then instantiate a new one instead of changing the shared one.

    That way, the behaviour of .material and .sharedmaterial would be consistent throught every single object, no matter if it's the original or a copy.