Search Unity

Question Why are MaterialPropertyBlocks So unrealiable?

Discussion in 'Scripting' started by DanielRiches, Sep 1, 2020.

  1. DanielRiches

    DanielRiches

    Joined:
    Nov 23, 2019
    Posts:
    166
    I've been experimenting with MPB's and it's starting to become silly just to tile a simple texture...

    A) it seems, unless a material has an emission map applied already a MPB wont even copy the fact it COULD have one, meaning you can't add an emission texture via code, the only workaround i've found for this is to stick a white 32x32 texture in the emissive map field of any material that isnt supposed to have one by default, otherwise I can't seem to apply textures to the _EmissiveColorMap......why???? ( I cant even add the 32x32 after hitting play before getting the initial MPB, it's emission map before then or nada)

    B) then, even if I manage to get a texture to register, theres no SetTextureScale command? i'm trying to tile the map according to the localScale of the object, but I can't see how to even access the scale parameter?




    https://docs.unity3d.com/2020.2/Documentation/ScriptReference/MaterialPropertyBlock.html

    These MaterialPropertyBlocks are quite confusing, they copy properties but NOT the default values, and wont copy properties for any map fields if they are null to begin with?

    I put together a small test script so you can see what I mean about the textures, just set the size of the emissiveArray to 2 in the inspector then drag 2 random textures into the fields, the script is set up so a left click will set it off.


    Code (CSharp):
    1. using System.Collections;
    2. using System.Collections.Generic;
    3. using UnityEngine;
    4.  
    5. public class TMPTest : MonoBehaviour
    6. {
    7.     #region Managers / Scripts / Components
    8.     private Renderer myMeshRenderer;
    9.  
    10.     private MaterialPropertyBlock theMaterial;
    11.     #endregion
    12.  
    13.     #region Variables
    14.     private bool currentlyBulging;
    15.     private bool emissiveMapPresent;
    16.  
    17.     private bool bulgedEmissiveTexture;
    18.     private bool bulgedEmissiveTexturePreset;
    19.     private bool bulgedEmissiveTextureSolo;
    20.     private float bulgeEmissiveTextureElapsedTime = 0f;
    21.     private float bulgeEmissiveTextureDuration = 0.15f;
    22.  
    23.     [ColorUsage(true, true)] private Color startingMeshColor;
    24.     [SerializeField, ColorUsage(true, true)] private Color startingEmissionColor;
    25.     #endregion
    26.  
    27.     [Header("- Material Target -")]
    28.     private int materialElement;
    29.     private Material meshMaterial;
    30.  
    31.     [SerializeField] private Texture startingEmissionMap;
    32.     public Texture nonEmissiveTexture;
    33.     public Texture[] emissionTextures;
    34.  
    35.     [Header("- Restrictions -")]
    36.     public bool bulgeMe;
    37.  
    38.     public bool bulgeEmissiveTexture;
    39.  
    40.  
    41.     public void Start()
    42.     {
    43.         theMaterial = new MaterialPropertyBlock();
    44.  
    45.         #region Get Components
    46.         // Get Renderers
    47.         myMeshRenderer = GetComponent<Renderer>();
    48.  
    49.         // Get Object Defaults
    50.         if (myMeshRenderer != null)
    51.         {
    52.             // Cache mesh material to copy default values
    53.             meshMaterial = myMeshRenderer.materials[materialElement];
    54.  
    55.             if (meshMaterial.HasProperty("_EmissiveColorMap"))
    56.             {
    57.                 if (meshMaterial.GetTexture("_EmissiveColorMap") != null)
    58.                 {
    59.                     Debug.Log("Emissive Map present, Setting MPB to match");
    60.  
    61.                     emissiveMapPresent = true;
    62.  
    63.                     startingEmissionMap = meshMaterial.GetTexture("_EmissiveColorMap");
    64.                 }
    65.                 else
    66.                 {
    67.                     Debug.Log("Emissive Map null, Setting MPB to match");
    68.  
    69.                     meshMaterial.SetTexture("_EmissiveColorMap", nonEmissiveTexture); // set Emissive Map to nonEmissiveTexture
    70.  
    71.                     startingEmissionMap = meshMaterial.GetTexture("_EmissiveColorMap"); // This wont work unless there was a map to begin with? despite setting it on previous line.
    72.                 }
    73.             }
    74.  
    75.             // Get chosen element properties from the Mesh renderer and assign it to theMaterial MaterialPropertyBlock
    76.             myMeshRenderer.GetPropertyBlock(theMaterial, materialElement);
    77.  
    78.             // If you accidentally choose a non-existant element
    79.             if (materialElement > myMeshRenderer.materials.Length - 1)
    80.             {
    81.                 Debug.Log("<color=orange>Warning</color> : Selected a non-existant material on <color=green>" + this.gameObject.name + "</color>, Element <color=green>0</color> selected as default");
    82.                 materialElement = 0;
    83.             }
    84.  
    85.             // Set mesh default values, this will detect which property the mesh uses for it's base color and sets starting color field accordingly
    86.             if (meshMaterial.HasProperty("_BaseColor"))
    87.             {
    88.                 startingMeshColor = meshMaterial.GetColor("_BaseColor");
    89.             }
    90.             else if (meshMaterial.HasProperty("_Color"))
    91.             {
    92.                 startingMeshColor = meshMaterial.GetColor("_Color");
    93.             }
    94.  
    95.             // Apply the base color to the appropriate MaterialPropertyBlock field
    96.             theMaterial.SetVector("_Color", startingMeshColor);
    97.  
    98.             // If mesh material has emission field, record default color and set MaterialPropertyBlock field to match
    99.             if (meshMaterial.HasProperty("_EmissiveColor"))
    100.             {
    101.                 startingEmissionColor = meshMaterial.GetColor("_EmissiveColor");
    102.  
    103.                 theMaterial.SetVector("_EmissiveColor", startingEmissionColor);
    104.             }
    105.         }        
    106.         #endregion
    107.  
    108.         #region Apply Start Values
    109.         // Apply Edited Values To Appropriate Renderer Material Element.
    110.         if (myMeshRenderer != null)
    111.         {
    112.             myMeshRenderer.SetPropertyBlock(theMaterial, materialElement);
    113.         }
    114.  
    115.         #endregion
    116.     }
    117.  
    118.     public void Update()
    119.     {
    120.         #region Get current MaterialPropertyBlock Values
    121.         // Get Current Values Of Material Properties In theMaterial.
    122.         if (myMeshRenderer != null)
    123.         {
    124.             myMeshRenderer.GetPropertyBlock(theMaterial, materialElement);
    125.         }
    126.         #endregion
    127.  
    128.         #region Input
    129.         if (Input.GetButtonDown("Escape"))
    130.         {
    131.             Application.Quit();
    132.         }
    133.  
    134.         if (Input.GetButtonDown("Left Click"))
    135.         {
    136.             currentlyBulging = true;
    137.         }
    138.         #endregion
    139.  
    140.  
    141.         if (this.gameObject.activeInHierarchy)
    142.         {
    143.             // Texture
    144.             if (currentlyBulging)
    145.             {
    146.                 if (bulgeEmissiveTexture)
    147.                 {
    148.                    bulgedEmissiveTexture = true;
    149.                    currentlyBulging = false;
    150.                 }
    151.             }
    152.         }
    153.  
    154.  
    155.             // Emission Bulge Texture Change
    156.             if (bulgedEmissiveTexture)
    157.             {
    158.                theMaterial.SetTexture("_EmissiveColorMap", emissionTextures[0]);
    159.                bulgedEmissiveTexture = false;
    160.                bulgedEmissiveTextureSolo = true;
    161.                
    162.             }
    163.             if (bulgedEmissiveTextureSolo)
    164.             {
    165.                 bulgeEmissiveTextureElapsedTime += Time.deltaTime;
    166.  
    167.                 if (bulgeEmissiveTextureElapsedTime >= bulgeEmissiveTextureDuration)
    168.                 {
    169.                     bulgedEmissiveTextureSolo = false;
    170.  
    171.                     theMaterial.SetTexture("_EmissiveColorMap", emissionTextures[1]);
    172.  
    173.                     bulgeEmissiveTextureElapsedTime = 0f;
    174.                 }
    175.             }  
    176.  
    177.  
    178.  
    179.         #region Apply Edited Values To Appropriate Renderer Material Element.
    180.         // Apply Edited Values To Appropriate Renderer Material Element.
    181.         if (myMeshRenderer != null)
    182.         {
    183.             myMeshRenderer.SetPropertyBlock(theMaterial, materialElement);
    184.         }
    185.         #endregion
    186.     }
    187. }
    188.  
     
    Last edited: Sep 1, 2020
  2. Adrian

    Adrian

    Joined:
    Apr 5, 2008
    Posts:
    1,066
    A) This is likely do to shader features that are enabled/disabled using keywords by a custom shader editor. Meaning that if you don't set an emission texture, the emission feature is completely removed from the shader and you can't later use it at runtime.

    Using a dummy texture is a workaround but you can also set the shader keyword yourself, though figuring out what the keyword is can be a bit tricky. You can look at the shader or switch the inspector into debug mode and then the «Shader Keywords» field appears, where you can see which keywords are currently set for the material. Then use Material.EnableKeyword before you set the emission texture.

    B) This is poorly documented. The SetTextureScale/SetTextureOffset methods set the x/y and z/w components of the "_NAME_ST" vector property. So instead of using these methods, you can use SetVector instead:
    Code (CSharp):
    1. block.SetVector("_EmissiveColorMap_ST", new Vector4(scaleX, scaleY, offsetX, offsetY));
     
    SolidAlloy likes this.
  3. DanielRiches

    DanielRiches

    Joined:
    Nov 23, 2019
    Posts:
    166
    bless you sir, I JUST got pointed to the documentation for this and said myself "i cant see any of this anywhere near the MaterialPropertyBlock docs!" lol!

    as a matter of fact, im currently trying:

    Code (CSharp):
    1. theMaterial.SetVector("_EmissiveColorMap_ST", new Vector4(transform.localScale.x, transform.localScale.y, _EmissiveColorMap_ST.z, _EmissiveColorMap_ST.w));
    to try and scale the texture of the emission map of a standard HDRP Lit shader, so it matches the objects localScale, so as it scales the texture shouldn't stretch with it, but can't think of how I would just keep the Z and W values as whatever they happen to be at the time (as i'm going to be dealing with them seperately), would you know how to do that at all?

    Your definately right, the docs on this is sparse, yet from my experience this is quite a good way of optimising performance and not sure why Unity wouldn't make use of this system under the hood by default? i'm sure theres a reason though, there usually is :p

    thanks for investigating!
     
  4. Adrian

    Adrian

    Joined:
    Apr 5, 2008
    Posts:
    1,066
    You should be able to get the original Vector from the Material and then override it in the block:
    Code (CSharp):
    1. var emissiveSt = material.GetVector("_EmissiveColorMap_ST");
    2. emissiveSt.x = transform.localScale.x;
    3. emissiveSt.y = transform.localScale.y;
    4. propertyBlock.SetVector("_EmissiveColorMap_ST", emissiveSt);
     
    DanielRiches likes this.
  5. DanielRiches

    DanielRiches

    Joined:
    Nov 23, 2019
    Posts:
    166
    Previous reply was on the money, hopefully if people run into these problems using MaterialPropertyBlocks they can find guidance here.

    For standard HDRP Lit Shaders, the keyword was:

    Code (CSharp):
    1. meshMaterial.EnableKeyword("_EMISSIVE_COLOR_MAP");
     
    DeathPro likes this.