Search Unity

Resolved Adding and removing a material at runtime

Discussion in 'Scripting' started by matzomat, Jan 30, 2021.

  1. matzomat

    matzomat

    Joined:
    Mar 4, 2018
    Posts:
    63
    Hei,

    I expected this to be an easy question but I couldn't find a solution so far. I want to add a material to an object (a net) and remove it later by script. As the materials are an array, I'm clueless here.
     
  2. PraetorBlue

    PraetorBlue

    Joined:
    Dec 13, 2012
    Posts:
    7,909
    You need to get a reference to the GameObject's Renderer component. From there you can simply set the Renderer's
     material
    property to whatever you please.
     
  3. matzomat

    matzomat

    Joined:
    Mar 4, 2018
    Posts:
    63
    Thank you for your answer but I want to add, not change.
     
  4. PraetorBlue

    PraetorBlue

    Joined:
    Dec 13, 2012
    Posts:
    7,909
    There's also a property to set materials as an array.
     
  5. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    38,689
    If you have an object with multiple Materials, that ALSO implies that the mesh has multiple submeshes.

    Each Material in the
    .materials
    array corresponds to a submesh, so just adding a material to an object isn't likely to do much.

    If you think you still want to, you need to:

    1. create a new blank array of Materials of the right size (+1 or -1 in size)
    2. copy the existing Materials out to the correct spot in this temp array
    3. add (or omit) the newly added (or removed) material.
    4. assign the array of Materials into the
    .materials
    property all at once.

    You can NOT simply assign one element of the
    .materials
    array. You must at least pluck the whole thing out, change it, then also put it back. The reason: accessing it causes Unity to make you a copy.

    EDIT: The same goes for
    .material
    : that returns a copy (as per the docs). You can copy it out, change it, but you MUST assign it back in.
     
    Last edited: Jun 3, 2021
    matzomat and PraetorBlue like this.
  6. matzomat

    matzomat

    Joined:
    Mar 4, 2018
    Posts:
    63
    Thank you for your answer. I feared that.
     
  7. matzomat

    matzomat

    Joined:
    Mar 4, 2018
    Posts:
    63
    After going through all that and taking all possibilities into account, I made this decision for my project:
    - All Prefabs which can be caught (by a net), must have an additional material layer that I can use to show the net.
    - These Prefabs must serve a way to access that material and check on startup if there is more then one material and otherwise report a warning.
    - as I'm in space, nearly anything can be caught with a net (depending on the net size). I will implement the functionality in a class all objects are deriving from.
     
  8. matzomat

    matzomat

    Joined:
    Mar 4, 2018
    Posts:
    63
    That didn't work out that well. In the end I used @Kurt-Dekker s method:
    Code (CSharp):
    1.     public void SetAdditionalMaterial(Material newMaterial)
    2.     {
    3.         if (additionalMaterialApplied)
    4.         {
    5.             Debug.LogError("Tried to add additional material even though it was already added on " + name);
    6.             return;
    7.         }
    8.         Material[] materialsArray = new Material[(this.GetComponent<Renderer>().materials.Length +1)];
    9.         this.GetComponent<Renderer>().materials.CopyTo(materialsArray,0);
    10.         materialsArray[materialsArray.Length - 1] = newMaterial;
    11.         this.GetComponent<Renderer>().materials = materialsArray;
    12.         additionalMaterialApplied = true;
    13.     }
    14.  
    15.     public void ClearAdditionalMaterial()
    16.     {
    17.         if (!additionalMaterialApplied)
    18.         {
    19.             Debug.LogError("Tried to delete additional material even though none was added before on " + name);
    20.             return;
    21.         }
    22.         Material[] materialsArray = new Material[(this.GetComponent<Renderer>().materials.Length - 1)];
    23.         for (int i = 0; i < this.GetComponent<Renderer>().materials.Length - 1; i++)
    24.         {
    25.             materialsArray[i] = this.GetComponent<Renderer>().materials[i];
    26.         }
    27.         this.GetComponent<Renderer>().materials = materialsArray;
    28.     }
    29.  
    It's working as intended but giving me a warning, like @Kurt-Dekker said:
    "This renderer has more materials than the Mesh has submeshes" and so on. Makes me feel I will run into trouble. How could I achieve
    - When an object is "catched" ingame, an additional texture of a net is displayed on the object. The net texture is coming from and is controlled by another object.
     
  9. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    38,689
    Read my post above again. If an object has X submeshes (that means something VERY specific in the Unity context), then it has X materials. You can't just add and remove materials in that list without ALSO adding and removing submeshes, which requires you to procedurally modify the geometry.

    Maybe it would be better if you explained what effect you're trying to do here (crude drawings are also helpful for good communications), because it does not seem like the approach chosen is likely to get you joy.
     
  10. matzomat

    matzomat

    Joined:
    Mar 4, 2018
    Posts:
    63
    And I fear you are right.

    I want to draw a net on an Object like this. This is how it works right now with above posted script:

    In this case a drone is using a cheap cargonet to catch an Asteroid. What material is put on top can differ (material, age,...) and has to come from the interacting GameObject.
     
  11. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    38,689
    I would do this: make two objects side by side in your 3D modeling program, one of them the asteroid, one of them the net.

    That way as you are making them you can align them just so.

    Once imported and placed into a prefab, just turn the net on and off when you capture it!

    That way if in the future you want a crazy complicated animated net with cloth strings and an animation driving it so it looks like it flaps in the breeze on reentry to a planet or when your ships thrusters are pointed at it, you just make that animation and control that animation, and everything else works.

    If you did the above in Blender3D, just go into edit mode, select the cargonet, press P to partition it and don't move it and everything else can pretty much work. I would parent them both to an empty axes object if I was in Blender.
     
    matzomat likes this.
  12. matzomat

    matzomat

    Joined:
    Mar 4, 2018
    Posts:
    63
    I love that!

    Thing is, I'm using objects from the Assetstore only. But you inspired me to just duplicate the object, resized it (*1.001) and still do the same. What do you think about it?
     
  13. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    38,689
    I LOVE IT! In fact, I'm fairly sure I've done that approach myself somewhere but I can't recall when.

    The only concern I can see is that 1.001 might not be enough and you will get Z-fighting flickering, especially once the object is farther from the camera. But if this is mostly a 2D game, maybe just find the scale you need?

    You could also leave it the same size and move it towards the camera some larger amount. The cargo netting on the far side won't render anyway (depending on shader / material) since the far side polys are pointed away...

    But scaling might be cool because the net will look like it has some thickness at the very edges of the rock.

    What about just making a copy of the surface texture, painting the mesh on it, and then swap the texture out and call it a day? I dunno if that would look as nice and even without a lot of texture fiddling, but it might look okay...
     
    matzomat likes this.
  14. matzomat

    matzomat

    Joined:
    Mar 4, 2018
    Posts:
    63
    Great idea but in my special case not possible. I need the material to be exchangable and I can't tell yet what I will need in future. It's also 3D. I just found out that *1.01 looks best in my setting and I'll start with that and see where this goes.

    Thank you very much for your help!

    functions:
    Code (CSharp):
    1.    
    2. ...
    3.     [Header("Additional Material handling")]
    4.     private MeshRenderer additionalMaterialContainer;
    5. ...
    6.     virtual public void OnDisable()
    7.     {
    8.        if (additionalMaterialContainer.gameObject.activeSelf) ClearAdditionalMaterial();
    9.     }
    10. ...
    11. public void SetAdditionalMaterial(Material newMaterial)
    12.     {
    13.         if (additionalMaterialContainer==null)
    14.         {
    15.             Debug.LogError("Tried to add additional material but no additionalMaterialContainer is configured on " + name);
    16.             return;
    17.         }
    18.         if (additionalMaterialContainer.gameObject.activeSelf)
    19.         {
    20.             Debug.LogError("Tried to add additional material even though it was already added on " + name);
    21.             return;
    22.         }
    23.         additionalMaterialContainer.GetComponent<Renderer>().material=newMaterial;
    24.         additionalMaterialContainer.gameObject.SetActive(true);
    25.     }
    26.  
    27.     public void ClearAdditionalMaterial()
    28.     {
    29.         if (additionalMaterialContainer == null)
    30.         {
    31.             Debug.LogError("Tried to delete additional material but no additionalMaterialContainer is configured on " + name);
    32.             return;
    33.         }
    34.         if (!additionalMaterialContainer.gameObject.activeSelf)
    35.         {
    36.             Debug.LogError("Tried to delete additional material even though none was added before on " + name);
    37.             return;
    38.         }
    39.         additionalMaterialContainer.GetComponent<Renderer>().material = null;
    40.         additionalMaterialContainer.gameObject.SetActive(false);
    41.     }
    42.  
     
    Kurt-Dekker likes this.