Search Unity

Unable to set texture in code without generating a brand new material instance?

Discussion in 'Editor & General Support' started by AzzyDude24601, Mar 12, 2021.

  1. AzzyDude24601

    AzzyDude24601

    Joined:
    Sep 28, 2016
    Posts:
    51
    I am creating a type of data visualiser for images in Unity. The user can make queries that results in several hundreds game objects appearing at once with images from the dataset set as their textures. As a result, I am running into performance issues with large numbers of images, which I am trying to improve.

    For example, I have discovered that sometimes the same image may appear more than once in the results, but because I am editing the mainTextture of the GameObject's material, it is generating a brand new instance of the material even though one already exists. This creates a new draw call for every GameObject, even though some of them should be identical. Here is the gist of my current code:

    Code (CSharp):
    1.  
    2. private void GenerateCells(List<Cell> cellData)
    3.     {
    4.         GameObject cellPrefab = Resources.Load("Prefabs/CellPrefab") as GameObject;
    5.         MaterialPropertyBlock materialSettings = new MaterialPropertyBlock();
    6.  
    7.         // loop through all cells data from server
    8.         foreach (var newCellData in cellData)
    9.         {
    10.             GameObject cell = Instantiate(cellPrefab);  
    11.             byte[] imageBytes = File.ReadAllBytes(imageLocation + newCellData.ImageName);
    12.             Texture2D imageTexture = ConvertImage(imageBytes);    
    13.             materialSettings.SetTexture("_MainTex", imageTexture);      
    14.             cell.GetComponent<MeshRenderer>().SetPropertyBlock(materialSettings);
    15.         }
    16.     }
    17.  
    The CellPrefab has a material I made in the Resources folder using GPU instancing and I was trying to use MaterialPropertyBlock() but that still creates a new Material instance when I use SetTexture, even when using the same Texture2D with the same image. I am also aware of sharedMaterial but I can't figure out how to use that without changing every material in the scene to the same texture.

    How am I supposed to cache and apply materials generated in code without duplicating them?
     
  2. AzzyDude24601

    AzzyDude24601

    Joined:
    Sep 28, 2016
    Posts:
    51
    Actually I think this is working? Is this a correct way to do it, or is there a better way?

    Code (CSharp):
    1. private void GenerateCells(List<Cell> cellData)
    2.     {
    3.  
    4.         GameObject cellPrefab = Resources.Load("Prefabs/CellPrefab") as GameObject;
    5.  
    6.         List<KeyValuePair<string, Material>> uniqueMaterials = new List<KeyValuePair<string, Material>>(); // added this
    7.  
    8.         foreach (var newCellData in cellData)
    9.         {
    10.             GameObject cell = Instantiate(cellPrefab);
    11.             byte[] imageBytes = File.ReadAllBytes(imageLocation + newCellData.ImageName);
    12.             Texture2D imageTexture = ConvertImage(imageBytes);
    13.  
    14.  
    15.            // and then checked for duplicate materials here
    16.             int index = uniqueMaterials.FindIndex(a => a.Key == newCellData.ImageName);
    17.             if (index == -1)
    18.             {
    19.                 Material newMaterial = new Material(Resources.Load("Materials/CellMaterial") as Material);
    20.                 newMaterial.mainTexture = newCellData.ImageTexture;
    21.                 KeyValuePair<string, Material> uniqueMaterial = new KeyValuePair<string, Material>(newCellData.ImageName, newMaterial);
    22.                 uniqueMaterials.Add(uniqueMaterial);
    23.                 cell.GetComponent<MeshRenderer>().material = newMaterial;
    24.             }
    25.             else
    26.             {
    27.                 cell.GetComponent<MeshRenderer>().material = uniqueMaterials[index].Value;
    28.             }
    29.         }
    30.     }
     
  3. Madgvox

    Madgvox

    Joined:
    Apr 13, 2014
    Posts:
    1,317
    It's creating new draw calls for the same image -- even when using MPBs -- because you're creating a new Texture for each image, even if it's the same. As a technical note, when using MPBs you're not creating new materials, but you are creating new draw calls. Your second snippet is closer because you're effectively de-duplicating the images. Combine your second snippet's de-duplication with your first snippet's use of MPBs and you will get the best of both worlds.

    If you want to reduce your draw calls even further, you can blit the downloaded images onto a single atlas texture and then set the UV coordinates to change the image drawn. Though more technically complicated to implement, this will generate new draw calls limited only by max texture size or max vertex batch size.
     
  4. AzzyDude24601

    AzzyDude24601

    Joined:
    Sep 28, 2016
    Posts:
    51
    Sorry, what do you mean by MPBs? I haven't seen that acronym before.

    That's exactly my end goal!
     
  5. Madgvox

    Madgvox

    Joined:
    Apr 13, 2014
    Posts:
    1,317
    MaterialPropertyBlocks.
     
  6. AzzyDude24601

    AzzyDude24601

    Joined:
    Sep 28, 2016
    Posts:
    51
    Ah right, but storing an instance of the entire material for each unique image I'm using is the correct way to do what I'm trying to do?

    I tried just storing the Texture2D's but that obviously doesn't work because when you apply the texture, even if it was already used, it creates a new instance of the material.
     
  7. Madgvox

    Madgvox

    Joined:
    Apr 13, 2014
    Posts:
    1,317
    You only create a new material if you access (not just assign, access) the
    MeshRenderer.material
    property. The purpose of MBPs is to avoid making new instances of materials.

    Store the textures uniquely, apply them to MPBs on the renderer, and it won't create new materials. It may create new draw calls, which you can analyze using the frame debugger -- it will tell you why it wasn't able to batch.
     
  8. AzzyDude24601

    AzzyDude24601

    Joined:
    Sep 28, 2016
    Posts:
    51
    Yeah, the new draw calls are the issue that I am trying to get rid of. As far as I can see, it is impossible to apply the same texture to two renderers without additional draw calls without employing the strategy I posted above.
     
  9. AzzyDude24601

    AzzyDude24601

    Joined:
    Sep 28, 2016
    Posts:
    51
    So I've implemented the MPB's like below and the draw calls returned to their higher values for some reason? Have I implemented it incorrectly?

    For the record I am only creating one Texture2D for each unique image elsewhere in the code so I do not think that is related.

    Code (CSharp):
    1. private void GenerateCells(List<Cell> cellData)
    2.     {
    3.         GameObject cellPrefab = Resources.Load("Prefabs/CellPrefab") as GameObject;
    4.         List<KeyValuePair<string, MaterialPropertyBlock>> uniqueMaterialProperties = new List<KeyValuePair<string, MaterialPropertyBlock>>();
    5.  
    6.         foreach (var newCellData in cellData)
    7.         {
    8.             GameObject cell = Instantiate(cellPrefab);
    9.  
    10.             int index = uniqueMaterialProperties.FindIndex(a => a.Key == newCellData.ImageName);
    11.             if (index == -1)
    12.             {
    13.                 MaterialPropertyBlock newMaterialProperty = new MaterialPropertyBlock();
    14.                 newMaterialProperty.SetTexture("_MainTex", newCellData.ImageTexture);
    15.                 KeyValuePair<string, MaterialPropertyBlock> uniqueMaterialProperty = new KeyValuePair<string, MaterialPropertyBlock>(newCellData.ImageName, newMaterialProperty);
    16.                 uniqueMaterialProperties.Add(uniqueMaterialProperty);
    17.                 cell.GetComponent<MeshRenderer>().SetPropertyBlock(newMaterialProperty);
    18.             }
    19.             else
    20.             {
    21.                 cell.GetComponent<MeshRenderer>().SetPropertyBlock(uniqueMaterialProperties[index].Value);
    22.             }
    23.         }
    24.  
    25.     }