Performance issue with performing Material.Lerp ~3k times.

Hello there,

Could someone help me out with the following problem? I have 1,5k buildings in my scene and I wanted to perform a change in window material for each one of them based on the day-night cycle. Each building has two “sets” of windows underneath them named “BuildingWindowsHD1” and “BuildingWindowsHD2” as can be seen in my code below. The code is run from my GameManager script, this function is called during update. However running this from my GameManager causes significant fps drops, any ideas on how to tackle this performance issue is appreciated!

void DayNightCycle(float Daytime)
    {
        List<PostProcessVolume> PP_Volumes = new List<PostProcessVolume>(Camera.main.GetComponents<PostProcessVolume>());
        PP_Volumes[0].weight = Daytime;

        foreach (GameObject building in buildings)
        {
            Material window1 = building.transform.Find("BuildingWindowsHD1").GetComponent<Renderer>().material;
            Material window2 = building.transform.Find("BuildingWindowsHD2").GetComponent<Renderer>().material;

            int randomVariation = Random.Range(1, 3);

            if (randomVariation == 1)
            {
                window1.Lerp(windowMaterialNightUnlit, windowMaterialDay, Daytime);
                window2.Lerp(windowMaterialNightLit, windowMaterialDay, Daytime);
            }
            else if(randomVariation == 2)
            {
                window1.Lerp(windowMaterialNightLit, windowMaterialDay, Daytime);
                window2.Lerp(windowMaterialNightUnlit, windowMaterialDay, Daytime);
            }
        }
    }
  1. Using Find/GetComponent every time you call DayNightCycle is going to be slow. You could cache every window instead ahead of time. So instead of just a ‘buildings’ collection, also have a ‘windows’ list that gets filled up with the windows when buildings are added to ‘buildings’. Though this probably isn’t the biggest bottleneck here, and may not even be necessary if you optimize different ways (2 & 3 in my post)

  2. By accessing ‘material’ you’re generating a unique material per window. If all your windows behave similarly you could just have a single material shared between all of them. And instead of access ‘sharedMaterial’ and set it once (it’ll get used amongst all the windows):
    Unity - Scripting API: Renderer.sharedMaterial

  3. If you can’t use sharedMaterial because a lot of windows need unique materials for some reason. Then you can instead have a ‘global variable’ in your shader. Then you just set the value of that variable on the Shader (only once per DayNightCycle call), and all materials using that shader will automatically get it:
    Unity - Scripting API: Shader.SetGlobalFloat

2 Likes

Thanks a lot! I was aware of the .Find and GetComponent, but I figured like you that that wasn’t the biggest bottleneck (Will still change it however, this code was still kind of dirty). I was not aware however of the existence of sharedMaterial !!! And that sounds exactly like the kind of thing that i need, I will check it out thanks!

The docs page is a bit of a stub page, but it seems like sharedMaterial is a property of Renderer. My GameManager’s Renderer does not have any of the three materials (windowMaterialNightLit, windowMaterialNightUnlit, windowMaterialDay) however.

Could you tell me how I would access the sharedMaterial of those materials preferably not by attaching scripts to all the window objects since I only want to run it once per update? I’m assuming that I can somehow modify sharedMaterial within my GameManager script no?

You don’t really need to reference the windows’ renderer components at all in your game loop. You can loop through them once, store a reference to their sharedMaterial in a List, and then just loop through that list making the needed changes.

I think we misunderstand eachother this: [quote=“StarManta, post:5, topic: 792013, username:StarManta”]
You can loop through them once
[/quote]
is what I am trying to prevent. All windows share the same material at the start, so I would like to get that reference to the sharedMaterial ONCE instead of looping through all, getting the same material everytime. Thanks for spending the time to help though, really appreciate it!

When you get your list of buildings (I don’t know where you get it… maybe it’s done through the inspector so maybe you’d do this on ‘Start’… but whenever ‘buildings’ gets filled). You’ll want to get the unique list of materials.

Something like this:

List<Material> windowMaterials;

void Start()
{
    var hset = new HashSet<Material>(); //to force uniqueness as we loop, hashset's don't allow duplicates of an item
    foreach(var b in buildings)
    {
        var w1 = building.transform.Find("BuildingWindowsHD1").GetComponent<Renderer>().sharedMaterial;
        var w2 = building.transform.Find("BuildingWindowsHD2").GetComponent<Renderer>().sharedMaterial;
        hset .Add(w1);
        hset .Add(w2);
    }
    windowMaterials = new List<Material>(hset);
}

Doing this you’ll have a distinct list of any sharedMaterial’s out there.

I get what you’re saying and I could probably make it work with a HashSet list, but what I am saying is that since the material is the same on every window at the start, isn’t that entire foreach loop in your code unnessecary? Can’t I just get the one material once?

On another note, I am currently trying some things out with sharedMaterial and I’m running into a bug that I suspect has something to do with material instances. Is it possible that instances of the material (materials attached at runtime) are not called upon when you call upon the sharedMaterial?

I don’t know how many materials you have out there.

With my code if only 1 material is shared… you’d get a list of just 1 entry.

If you had 3 different materials shared amongst 100 buildings (maybe “blue glass” “green glass” and “red glass”), you’d end up with a list of 3 materials.

The entire hashset is to reduce out duplicates so you end up with a distinct list of the sharedmaterials used by all the buildings.

I was trying to preserve your workflow without too many changes, but you can absolutely just create a public List, and drag the material assets into that list, and change them from there.

Some visual reference, in order to clear some things up.
Currently I have:

3 materials.(A,B,C for simplicity’s sake)
2 gameobjects under each building GO. (BuildingWindowsHD1 and BuildingWindowsHD2)

both BWHD1 and BWHD2 start out with material A attached (however since i want to do two different Lerps, I’m thinking I need to get two objects versions of material A, even though visually they’re the same.)

Daytime is a value between 0 and 1 obviously.


So that is what I have.
What I want is to Lerp material A (and possibly A2), to Material B or C (note: OR = this should be random)
So either group BWHD1 and BWHD2 can be Lerped to B or C at daytime = 0, but never both lerped to the same material. (eg. both B is not possible)

Edit: And what I would like to prevent is looping through all buildings even in a start function if possible.

So what I am thinking is this:

Attached to each building a script that assigns A1 and A2 to BWHD1 and BWHD2 at random.

public class WindowRandomizer : MonoBehaviour
{
    public Material materialDayLit, materialDayUnlit;
    private Material windowGroup1Material, windowGroup2Material;
    void Start()
    {
        windowGroup1Material = transform.Find("BuildingWindowsHD1").GetComponent<Renderer>().material;
        windowGroup2Material = transform.Find("BuildingWindowsHD1").GetComponent<Renderer>().material;

        int randomVariation = Random.Range(1, 3);

        if (randomVariation == 1)
        {
            windowGroup1Material = materialDayLit; // Material A1
            windowGroup2Material = materialDayUnlit; // Material A2 // these look the same visually but lerp into different materials.
        }
        else if (randomVariation == 2)
        {
            windowGroup1Material = materialDayUnlit;
            windowGroup2Material = materialDayLit;
        }

    }
}

Then somewhere in one single update (in GameManager script for example) something like this:

if (variation1){
    /*(For BWHD1)*/ sharedMat.Lerp(sharedMatA1, sharedMatB, GameManager.Instance.DayTime);
    /*(For BWHD2)*/ sharedMat.Lerp(sharedMatA2, sharedMatC, GameManager.Instance.DayTime);
}
else if (variation2)
{
    /*(For BWHD1)*/ sharedMat.Lerp(sharedMatA1, sharedMatC, GameManager.Instance.DayTime);
    /*(For BWHD2)*/ sharedMat.Lerp(sharedMatA2, sharedMatB, GameManager.Instance.DayTime);
}

Ok so I solved it, first off there was a mistake in having transform.Find(“BuildingWindowsHD1”) twice instead of one transform.Find(“BuildingWindowsHD1”) and one transform.Find(“BuildingWindowsHD2”) in my windowRandomizer script (you can still see this error in my code above)… Classic!

Secondly and this one is weird… appearently changing the material directly like so:

windowGroup1Material = transform.Find("BuildingWindowsHD1").GetComponent<Renderer>().material
windowGroup1Material = .....

Causes unity to create an instance of a material instead of assigning the material, this caused issues with sharedMaterial I’m assuming.

Instead here is what I did:

windowGroup1Material = transform.Find("BuildingWindowsHD1").GetComponent<Renderer>();
windowGroup1Material.material = .....

For the actual sharedMaterial changing I did the following I created two 3D planes, deleted the colliders and disabled the meshrenderers in the inspector. Then I gave both a different daytime material (they look the same but are not) and added this code:

public class WindowLighting : MonoBehaviour
{
    public Material materialNight;
    private Material sharedMat, materialDay;
    private Renderer rend;
    void Start()
    {
        rend = GetComponent<Renderer>();
        sharedMat = rend.sharedMaterial;
        materialDay = new Material(sharedMat);
    }

    void Update()
    {
        sharedMat.Lerp(materialNight, materialDay, GameManager.Instance.DayTime);
    }
}

I know that using two invisible planes with the material attached isn’t the most elegant, but I suppose it works and I think quite well performance wise as well.

Thanks for all your help guys! sharedMaterial was definetly the way to go!

Edit: it turned out great :slight_smile: