One shader for multiple materials with different textures

So i have enemy model that uses 3 materials (each having it own’s texture). I created dissolve shader, and applied it in C# code to those materials. My question is:

Is there a way to apply shader to material with ceratin texture, and then apply the same shader with different texture to different material?

What i’m trying to do is dissolve a enemy body after he died, but make him keep propper texture while dissolving (now he turns gray after shader is applied).

This is mostly a c# question rather than directly shader graph related. There’s kind of two ways to go about this.

One is to try to make sure your dissolve shader’s properties use the same reference names as the shader you’re swapping from. Both the LWRP and HDRP use “_BaseMap” for the main texture for example. Then, to swap, just change the shader on the materials directly. If you use renderer.materials[ ].shader it’ll make a new material “instance” (really just a copy) that you can modify without affecting the original material asset. Then swap the shader or the whole material back (use .sharedMaterials[ ] if you go back to the original material, and keep the material “instance” around for later when you want to dissolve again).

The other option, and the one I personally use, is to create a MaterialPropertyArray, copy the properties of the original material I care about into it, set that on the renderer for that material slot, then replace the .sharedMaterials[ ] entry with the one dissolve material. That way no new materials are ever created.

4 Likes

Thank you very much, that solution worked just fine for me :slight_smile:

Hi
How can I do that in shader graph?

Hello,

I found this post while searching for the same type of issue I am having. Would you mind expanding on and explaining the method you prefer with some detail/code? I would really appreciate it. Or perhaps the API documentation/example of this in action?

Thanks!

Well, I have a typo in there that’s probably confusing you a bit. You’d want to set MaterialPropertyBlocks, not a “MaterialPropertyArray”.

The basic code would be something like this:

public Renderer rend;
public Material dissolveMaterial;
private Material[] originalMaterials;
private Material[] dissolveMaterials;
private MaterialPropertyBlock matBlock;

void Start()
{
  matBlock = new MaterialPropertyBlock();
  int numMaterials = rend.sharedMaterials.Length;

  // make copy of original materials for later
  originalMaterials = rend.sharedMaterials;

  // make array of dissolve materials
  dissolveMaterials = new Material[numMaterials];

  // iterate over shared materials
  for (int i=0; i<numMaterials; i++)
  {
    matBlock.Clear();

    // copy the properties of the material you want
    // these property names are whatever the values that are important to your shaders are
    matBlock.SetTexture("_MainTex", rend.sharedMaterials[i].GetTexture("_MainTex"));
    matBlock.SetColor("_Color", rend.sharedMaterials[i].GetColor("_Color"));
    matBlock.SetFloat("_SomeFloat", rend.sharedMaterials[i].GetColor("_SomeFloat"));
    // etc ...

    // set the property block on the renderer for that material index
    rend.SetPropertyBlock(matBlock, i);

    // fill in the dissolve materials array
    dissolveMaterials[i] = dissolveMaterial;
  }

}

void SetDissolved(float dissolve)
{
  // assuming disolve 0.0 is fully visible
  if (dissolve <= 0.0f)
  {
    rend.sharedMaterials = originalMaterials;
    return;
  }

  // else swap to dissolve material
  rend.sharedMaterials = dissolveMaterials;

  // iterate over property blocks
  int numMaterials = rend.sharedMaterials.Length;
  for (int i=0; i<numMaterials; i++)
  {
    // and set the dissolve amount
    matBlock = rend.GetPropertyBlock(i);
    matBlock.SetFloat("_Dissolve", dissolve);
    rend.SetPropertyBlock(matBlock. i);
  }
}

}

4 Likes

Wow, I wasn’t sure if I’d get a response for a while. Thank you for providing the example and the documentation link. I am going to give it a shot tomorrow to get it working in my project and will reply back with the results. :slight_smile: Really appreciate it!!!

So here is my code thus far. I have it almost working? I can get it to change the material to my dissolve material, and it will dissolve into the scene but its not grabbing the original materials “_BaseColor” and placing it on the dissolve material. The dissolve material is retaining its _BaseColor value that is set inside the shader.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class MaterialPropertyDebugger : MonoBehaviour
{

    public Renderer rend;
    public Material dissolveMaterial;
    public Material[] originalMaterials;
    public Material[] dissolveMaterials;
    private MaterialPropertyBlock matBlock;
 
    void Start()
    {
        // Create new MPB
        matBlock = new MaterialPropertyBlock();
        int numMaterials = rend.sharedMaterials.Length;

        // make a copy of original materials for later use
        originalMaterials = rend.sharedMaterials;

        // make array of dissolve materials
        dissolveMaterials = new Material[numMaterials];     

        // iterate over shared materials
        for (int i=0; i<numMaterials; i++)
        {
            matBlock.Clear();

            // copy the properties of the material we want
            //these are the property values from the shader that we care about

            // Grab the shared materials from original material. URP uses "_BaseColor" instead of _"Color"
            matBlock.SetColor("_BaseColor", rend.sharedMaterials[i].GetColor("_BaseColor"));
         
            //matBlock.SetTexture("_Texture", rend.sharedMaterials[i].GetTexture("_Texture"));

            // set the property block on the renderer for that material index
            rend.SetPropertyBlock(matBlock, i);

            // fill in the dissolve materials array
            dissolveMaterials[i] = dissolveMaterial;
        }
     
     
    }

    // Update is called once per frame
    void Update()
    {
        if(Input.GetKeyDown(KeyCode.D))
        {
            // swap to dissolve material
            rend.sharedMaterials = dissolveMaterials;

            // iterate over property block(s)
            int numMaterials = rend.sharedMaterials.Length;
            for (int i=0; i<numMaterials; i++)
            {
                // set the dissolve trigger
                matBlock = rend.GetPropertyBlock(i);
                matBlock.SetFloat("Vector1_83EFB224", Time.time);
                rend.SetPropertyBlock(matBlock, i);
            }
        }
     
    }

I believe the issue is with line matBlock = rend.GetPropertyBlock(i); I have to leave this commented out in order to have it working as described before. I’ve tried getting this line of code to work but keeps telling me "cannot convert from ‘int’ to ‘UnityEngine.MaterialPropertyBlock’
}

Edit: My DissolveIN shader was made using URP PBR Graph. Not sure if this is also part of the problem? Any further help is appreciated.

Before and After pictures of the inspector if this helps.


Because I wrote it wrong. I just typed all that code out from memory, I didn’t try to run it. :wink: Replace:

matBlock = rend.GetPropertyBlock(i);

With:

rend.GetPropertyBlock(matBlock, i);

You won’t necessarily see the value change in the inspector. But also you don’t necessarily have a _BaseColor on that dissolve shader to set, unless you actually changed the reference name to _BaseColor and not left it as the usual Vector4_R4ND0M5TUFF.

1 Like

That is what I was doing wrong, I needed to change the “_BaseColor” to
“Color_18AF198E” It didn’t click at first what you meant and then found this page Shader Graph Edit Parameters From Script - Questions & Answers - Unity Discussions and realized I’m dumb cause the property names are in the inspector.

Thanks for all your help, its working as intended now. :slight_smile:

You can also just rename the references to something more sane.

1 Like

Yeah thats my next move. Just got the _BumpMap in and working. Going to test on something with more than one material.

Edit: Everything is working great. Thanks again! @bgolus

Having another issue. This code works fine if there is only one material on each renderer. When I add a second material to any of the renderer’s, it starts breaking. I noticed that the dissolveMaterials array will remain the total length of whatever the last renderer it iterated through. When I go to run my public void dissolveBuildingIn2() it thinks that all of the dissolvedMaterials are of size 2. I believe this is the part in my function that I need to change rend[r].sharedMaterials= dissolveMaterials;

I was going to create another array that holds all of the dissolveMaterials inside their own index spot so I could then iterate through it correctly in my function but I am not sure if that is correct and having trouble implementing this. Maybe you can see something that I cannot but I’ve been debugging and trying everything I can think of, hoping you can take a peak… Sry to keep bugging you.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Texture2dDissolveIN : MonoBehaviour
{
    public GameObject objectWeAreDissolvingIN;
    public Renderer[] rend;
    public Material dissolveMaterial;
    public Material[] originalMaterials;
    private Material[] dissolveMaterials;

    private MaterialPropertyBlock matBlock;
   
    void Start()
    {
        rend = objectWeAreDissolvingIN.GetComponentsInChildren<Renderer>();
        int numRendChildren = rend.Length;
       

        for (int r=0; r<numRendChildren; r++)
        {
        // Create new MPB
        matBlock = new MaterialPropertyBlock();
        int numMaterials = rend[r].sharedMaterials.Length;
        Debug.Log("The number of materials under renderer index- " + r + " is equal to " + numMaterials);
        // make a copy of original materials for later use
        originalMaterials = rend[r].sharedMaterials;
        // make array of dissolve materials
        dissolveMaterials = new Material[numMaterials];
       

       
        // iterate over shared materials
            for (int i=0; i<numMaterials; i++)
            {
                matBlock.Clear();

                if(rend[r].sharedMaterials[i].GetTexture("_BaseMap") != null)
                {
                    //Debug.Log(rend[r]);
                    // Get and set texture with new dissolve IN shader
                    matBlock.SetTexture("_MainTex", rend[r].sharedMaterials[i].GetTexture("_BaseMap"));
                    matBlock.SetTexture("_NrmMapTex", rend[r].sharedMaterials[i].GetTexture("_BumpMap"));
                    matBlock.SetColor("_BaseColor", rend[r].sharedMaterials[i].GetColor("_BaseColor"));
                }
                else
                {
                    matBlock.SetColor("_BaseColor", rend[r].sharedMaterials[i].GetColor("_BaseColor"));
                }
                // set the property block on the renderer for that material index
                rend[r].SetPropertyBlock(matBlock, i);
                // fill in the dissolve materials array
                dissolveMaterials[i] = dissolveMaterial;
                Debug.Log("The number of materials under renderer index- " + r + " is equal to " + numMaterials);
               
            }
        }

    }
    // Update is called once per frame
    public void dissolveBuildingIn2()
    {
            int numRendChildren = rend.Length;
           
            for (int r=0; r<numRendChildren; r++)
            {
               
                // swap to dissolve material
                Debug.Log("DissolveBuildingIN2 - dissolve materials array length for renderer index " + r + " is equal to " + dissolveMaterials.Length);
                int numOfSharedMaterials = rend[r].sharedMaterials.Length;
                Debug.Log("DissolveBuildingIN2 - sharedMaterials array length for renderer index " + r + " is equal to " + rend[r].sharedMaterials.Length);
               
                rend[r].sharedMaterials= dissolveMaterials;
               

                // iterate over property block(s)
                int numMaterials = rend[r].sharedMaterials.Length;

                for (int i=0; i<numMaterials; i++)
                {
                    // set the dissolve trigger
                    rend[r].GetPropertyBlock(matBlock, i);
                    matBlock.SetFloat("Vector1_83EFB224", Time.time);
                    rend[r].SetPropertyBlock(matBlock, i);
                }
           

       
       
    }
    }
   
}

Part of the benefit of this technique is there aren’t multiple dissolve materials. At least assuming all objects that are dissolving are all using the same common opaque material settings. The arrays are just lists of the same material over and over.

However the array assigned to the renderer’s .sharedMaterials needs to match the size of that renderer’s mesh’s material count. If you assign an array that’s smaller, then parts of the mesh won’t render at all. If you assign an array that’s longer, then some parts will render multiple times! The pseudo code example above was written assuming it was be a unique script component per renderer component, not a single component to drive multiple renderers. If that’s the case you’ll need to copy off the arrays of every renderer component uniquely, at least if you need the ability to re-assign the original materials.

If your enemies are simply destroyed and new ones are instantiated, then don’t worry about that. Just make a new materials array populated with the dissolveMaterial for each renderer component when you start to dissolve them. Then you don’t have to worry about re-assigning the sharedMaterials array every time.

1 Like