Search Unity

Feedback MaterialPropertyBlock equivalent for shaderKeywords

Discussion in 'General Graphics' started by Prodigga, Feb 28, 2019.

  1. Prodigga

    Prodigga

    Joined:
    Apr 13, 2011
    Posts:
    1,123
    Hey there!

    Requested functionality:

    We should be able to set a list of shader keywords (Preferably NonAlloc API) on the renderer instead of on individual materials. Something similar to the MaterialPropertyBlock API but for keywords. It would be good to be able to add keywords that we want enabled, and keywords we want to disable, and the contents of this 'MaterialKeywordBlock' will 'merge in' with existing keywords and stomp over keywords that were redefined - the same way MaterialPropertyBlock works. Usage could mirror the MaterialPropertyBlock API.

    Some mock examples:

    Basic usage:
    Code (CSharp):
    1.      
    2.         MaterialPropertyBlock mpb = new MaterialPropertyBlock();
    3.         mpb.SetColor("_MyColor",Color.white);
    4.         myRenderer.SetPropertyBlock(mpb);
    5.  
    6.         MaterialKeywordBlock mkb = new MaterialKeywordBlock();
    7.         mkb.EnableKeyword("SOME_KEYWORD");
    8.         myRenderer.SetKeywordBlock(mkb);
    9.        
    Get existing keywords, to remain compatible with other scripts that might also want to set the property/keyword blocks:
    Code (CSharp):
    1.         MaterialPropertyBlock mpb = new MaterialPropertyBlock();
    2.         myRenderer.GetPropertyBlock(mpb);
    3.         mpb.SetTexture("_Texture", someTexture);
    4.         myRenderer.SetPropertyBlock(mpb);
    5.  
    6.         MaterialKeywordBlock mkb = new MaterialKeywordBlock();
    7.         myRenderer.GetKeywordBlock(mkb);
    8.         mkb.EnableKeyword("SOME_KEYWORD");
    9.         myRenderer.SetKeywordBlock(mkb);
    Existing Solutions and their problems:

    Iterate over sharedMaterails, and set keywords directly

    Similar to setting a property directly on the material, this 'dirties' the material asset. It means that the changes you make in the editor will persist when you stop playing the game, potentially corrupting project data, and it will also mean that every renderer using the material will be effected by the keyword changes.
    On Awake, create an instance of sharedMaterial, and manage the keywords on that

    There is multiple issues with this. Firstly, it ruins the editor workflow. The designer would like to, at any time, change the material on the renderer. If you are managing an instance of this material via your code, you will need to write code to detect when the user changed the material reference, so you can destroy your old material and instantiate a copy of the new material that the designer has selected. Its a mess. Secondly, it means we need to manage the lifecycle of the instantiated material - which can get fairly dangerous if you want your script to execute during edit mode. For example, if you do not revert the shared material correctly when the user is editing a prefab with your script on it, then the prefab would save to disk with your material instance and it'll loose the reference to its original material. Thirdly, it means that different scripts will not be able to coexist as easily, as they will all be attempting to manage their own instance of the material. The 'last' script to execute will be the only one that works.
    How the proposed feature fixes the issue:

    MaterialPropertyBlocks make it easy to create and drive complex effects on any renderer in the project by removing the need to micro manage materials and providing API's for different scripts to coexist (Renderer.GetProperyBlock). The proposed feature will bring the same feature set to shader keywords.
     
    Last edited: May 3, 2019
  2. Prodigga

    Prodigga

    Joined:
    Apr 13, 2011
    Posts:
    1,123
    Bump...
     
  3. Prodigga

    Prodigga

    Joined:
    Apr 13, 2011
    Posts:
    1,123
  4. Prodigga

    Prodigga

    Joined:
    Apr 13, 2011
    Posts:
    1,123
  5. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,352
    The values that MaterialPropertyBlock can modify are explicitly only ones that do not change the render state. For example a MaterialPropertyBlock will not affect the blend mode or zwrite for shaders that have those tied to a material property, only the values that the shader code sees. Keywords change the shader variant, which means the GPU uses a completely different shader as far as it's concerned, which is about the biggest render state change you can make.

    So, no, this is not a feature that is likely to be added to the MaterialPropertyBlock.

    Though perhaps an alternate feature request would be an option to override keywords and other render state settings per renderer / material index in a way similar to property blocks.
     
  6. Prodigga

    Prodigga

    Joined:
    Apr 13, 2011
    Posts:
    1,123
    Hi thanks for the reply. This is literally what I am suggesting. I am not suggesting a change to the material property block system. I am suggesting an entirely new, seperate system that works in a similar fashion to the MPB api for us to manage keywords. Note my examples use a made up class called "MaterialKeywordBlock". I am basing my suggestion around MaterialPropertyBlock API because the features it offers are proven to be flexible enough in our projects that contain custom effects that drive material properties. We only run into issues when shader keywords are involved, because those custom effects have no nice/clean way to alter the keywords.
     
  7. Prodigga

    Prodigga

    Joined:
    Apr 13, 2011
    Posts:
    1,123
  8. Prodigga

    Prodigga

    Joined:
    Apr 13, 2011
    Posts:
    1,123
  9. jawad_ahmad

    jawad_ahmad

    Joined:
    Apr 28, 2017
    Posts:
    35
    It's funny. I was looking for this exact functionality and so was happy to see that at least someone else is also requesting it. Sadly it seems like the feature never came into fruition :(

    It's frustrating because I really don't want to create a new material instance for setting a keyword. What's further frustrating is that I know the engine can add keywords internally somewhere as a property override per renderer. Why can't we do the same?!
     
    Last edited: Aug 17, 2019
    Nevermiss likes this.
  10. Prodigga

    Prodigga

    Joined:
    Apr 13, 2011
    Posts:
    1,123
    I have the exact same frustrations.

    Lightmapping is a good example of this feature in action. Unity seems to automatically enable the LIGHTMAP keyword for renderers that are lightmaped. No need for separate materials! So the functionality is in there to some extent...
     
    Last edited: Aug 17, 2019
  11. Prodigga

    Prodigga

    Joined:
    Apr 13, 2011
    Posts:
    1,123
  12. TJHeuvel-net

    TJHeuvel-net

    Joined:
    Jul 31, 2012
    Posts:
    838
    Bump :(
     
    Prodigga likes this.
  13. aleksandrk

    aleksandrk

    Unity Technologies

    Joined:
    Jul 3, 2017
    Posts:
    3,028
    Noted. We'll think about this :)
     
    Nevermiss and TJHeuvel-net like this.
  14. TJHeuvel-net

    TJHeuvel-net

    Joined:
    Jul 31, 2012
    Posts:
    838
    Thanks so much!

    At the moment i use the first approach described here, iterate over sharedmaterials and set a keyword. Instead of dirtying the original we have a material-factory class that creates duplicates of materials with keywords set or unset.

    I personally dont really see the benefit of @Prodigga suggestion of a new `MaterialKeywordBlock`, instead of just adding EnableKeyword/DisableKeyword methods to the existing MaterialPropertyBlock. I'm already used to the Renderer API, the more the MaterialPropertyBlock is like it the better in my opinion.
     
    Prodigga likes this.
  15. Prodigga

    Prodigga

    Joined:
    Apr 13, 2011
    Posts:
    1,123
    Yeah, I agree I think all you really need is Enable/disable keyword on the renderer.
     
  16. temresenpopcore

    temresenpopcore

    Joined:
    Jun 16, 2020
    Posts:
    2
  17. aleksandrk

    aleksandrk

    Unity Technologies

    Joined:
    Jul 3, 2017
    Posts:
    3,028
    Hi!
    Sorry for the long silence here, got sidetracked by other stuff.
    We evaluated this request and decided that we are not going to implement this.

    Can't you get away with the existing system here? The example above with lightmaps could be solved with a global keyword, unless I missed something.
     
  18. Prodigga

    Prodigga

    Joined:
    Apr 13, 2011
    Posts:
    1,123
    How would Global Keyword fix the problem exactly?
    • Say you have 5 renderers in the scene.
    • 1 material with a shader on it.
    • Shader has a Color property and 5 keywords for 5 different intermixable effects/options.
    How would you give them all unique colours?
    • Create a material property block
    • set color property
    • Apply to renderer
    • Easy, all have the same single material, all have different colour
    How would I go about enabling different keywords on each renderer?
    • Instantiate their materials
    • Enable keyword on material instance
    • Set as main material on the renderer
    • Make sure I clean up instantiated materials on destroy
    • All renderers now have different materials that have to be micromanaged, gets especially difficult with effects that can mix and want to activate different keywords on the same renderer through different scripts (who "owns" the material instance and is in charge of cleaning/instantiating?)
    Ok so that's a mess. But there is a scenario where Unity has to the deal with the same issue - Lightmaps! What if 3/5 of the renderers were lightmapped? How does Unity handle activating the Lightmap keyword for only those 3 renderers?
    • 3 of the renderers that are lightmapped with "automatically" have their Lightmap keyword enabled. The other 2 won't.
    • This isn't a Global Keyword. It is per renderer.
    • There is no cloned material instances, the keyword "just works" and gets applied per renderer, just like material property block values.
    • This is what we would like to be able to do.
    Appreciate the honesty and I hope we can revisit this feature request once you and your team understands the problem properly. Hopefully this explanation was clearer!
     
    Last edited: Feb 8, 2021
  19. aleksandrk

    aleksandrk

    Unity Technologies

    Joined:
    Jul 3, 2017
    Posts:
    3,028
    Well, that's the thing - LIGHTMAP_ON is a builtin global keyword. It is enabled or disabled per batch deep in the engine :)

    What exactly are you trying to achieve? I understand that you want to modify the keywords without creating new materials, but what do you want this for specifically?
     
  20. Prodigga

    Prodigga

    Joined:
    Apr 13, 2011
    Posts:
    1,123
    Right, I see.

    We have an uber shader in our game that has a list of special effects. For the sake of this example, lets say we have a special effect to render something as 'selected/highlighted' that tints the game object a specific color.

    You have a SelectionEffectManager script that sits on the renderer. If you set 'Selected' bool to true, it enables the SELECTIONHIGHLIGHT_ON keyword. It also exposes a color variable that sets the color of the selection effect.

    This is just one of say, two or three similar XXXXXEffectManager scripts that also want to control keywords and properties on the renderer for their own effects. So it is important that they all operate in a way where they do not stomp on each other. This made up example is enough to show all the issues we are facing.

    Also, we want this script to ExecuteAlways, so we can see what it looks like while we are authoring the scene at edit time. We want the MeshRenderer to work as you would expect, though - so if the artist on our team is viewing a MeshRenderer in the scene with some effects turned on, we want them to be able to drag and drop a new material into the Material slot on the MeshRenderer and have it 'just work'. We also want to make sure the prefabs being edited dont get corrupted by our custom effects instantiating and disassociating the MeshRenderer with its original material...

    Lets ignore keywords for a second and just deal with properties. The solution is easy. Pseudo code:


    SelectionEffectManager {

    Color SelectionColor;

    Update {
    renderer.GetPropertyBlock(mpb);
    mpb.SetColor("SelectionColor",SelectionColor);
    renderer.SetPropertyBlock(mpb)
    }
    }


    You can have as many different effect managers as you want. So long as they all GetPropertyBlock, they can all cooperate and drive their own properties. All good. All of the requirements are met.

    Since we aren't managing material instance, it also works perfectly with ExecuteAlways at Edit time. The MeshRenderer's material field is never altered, so the artist can drag and drop different materials into the MeshRenderer and all of the material property overrides work as expected as they flick through materials.

    But soon as you throw keywords into the mix, it falls apart.
    • We had to write our own 'MaterialKeywordManager' behaviour that is in charge of instantiating the original material and managing the copy.
    • It has public API for setting Keywords.
    • It hooks into...:
      • RenderPipelineManager.beginFrameRendering callback to set the MeshRenderers material to the Material Instance
      • RenderPipelineManager.endFrameRendering callback so it can revert the material back to the original material. (this way, artist Editor workflow is respected... the material is swapped in and out every frame so that what the artist sees in the Inspector is the original material)
    • Since the material on the MeshRenderer can change any time, it has to continuously compare the MeshRenderer material to its instantiated material. If there is a change, it needs to dispose of its instantiated material and instantiate a copy of the new material that was set in the MeshRenderer (again, possibily by the artist during edit-time).
    • Unfortunately, since we are modifying the Material array on the MeshRenderer, the Material field on the Renderer appear as a 'prefab override' modification on any Nested Prefabs that contain the MaterialKeywordManager...
    It is quiet the hot mess. If we just had a way to enable/disable Keyword on the Renderer instead of on the material, we can avoid all of this hassle and everything would just work fine, as it does with material property blocks. Something like:


    SelectionEffectManager {
    bool Selected;
    Color SelectionColor;

    Update {
    renderer.SetKeyword("SELECTIONHIGHLIGHT_ON", Selected);

    renderer.GetPropertyBlock(mpb);
    mpb.SetColor("SelectionColor",SelectionColor);
    renderer.SetPropertyBlock(mpb)
    }
    }
     
    goncalo-vasconcelos likes this.
  21. aleksandrk

    aleksandrk

    Unity Technologies

    Joined:
    Jul 3, 2017
    Posts:
    3,028
    OK, I see.

    Could you setup a separate scene (or several) for the artists for iteration and a set of materials that have all keyword combinations enabled that the artists can use in this test scene?
    And then you would have a simple script for the play mode that swaps the material when needed.
    Wouldn't this work for you?
     
  22. Prodigga

    Prodigga

    Joined:
    Apr 13, 2011
    Posts:
    1,123
    Unity is a very flexible tool so if there is a limitation it is possible to work around it. I didn't want to list specific examples because I kind of knew the conversation would shift towards 'solving' my problems with workarounds. I've already figured out workarounds that address all of my use cases but it all feels really hacky and is really inefficient.

    Imagine if you wanted to change the Tint property on the Renderer as the player mouses on and over a renderer. You'd use MaterialPropertyBlocks. The original material on the renderer would remain in tack, you would have no material instances to manage, the property changes would be non-destructive thanks to the MPB.

    This is what we are missing for Keywords.

    This feature request would 'solve' the same workflow issues for Keywords as MaterialPropertyBlocks solves for properties. There is not much more to it then that. Just imagine a world without MaterialPropertyBlocks. That's what we're dealing with now except its Keywords and not Properties.

    It would be extremely useful if I could just tell the Renderer itself that 'SOMEKEYWORD' is enabled and that keyword is additively applied on top of the keywords defined on the material.
     
    goncalo-vasconcelos likes this.
  23. aleksandrk

    aleksandrk

    Unity Technologies

    Joined:
    Jul 3, 2017
    Posts:
    3,028
    Let me give you some background on this.
    Changing shader keywords in general means "use a different shader" from the graphics API standpoint. This is one of the most expensive state changes out there. Batching tries to minimise that.
    All the batching paths in the Unity codebase right now assume that once we got the keywords from the material they are not going to change. If we were to introduce a possibility to override this per renderer, this assumption doesn't hold any more. The batching code would become less efficient, and this would affect performance for all projects, not just the ones that want to use this.
     
  24. DonCornholio

    DonCornholio

    Joined:
    Feb 27, 2017
    Posts:
    92
    I guess the best thing would be to open source Unity just like Unreal, Godot and Cry Engine. If it doesnt work with your monetization model you could just change it so that developer without hundreds of thousands of dollars upfront can have a good time and not regularly run into walls like this. Make it optional and make it cost a portion of the games revenue just like Unreal does, that's all i want for christmas :rolleyes:

    I love unity but this is the sole reason why i will grab the next job opportunity and switch to another engine asap.After working with unity for at least 6 years now, this is happening way to often to me (don't even mention engine bugs that are "working as intended"). The community and the asset store are Unitys most vital advantage and there's so much potential just waiting to be unleashed by opening source code for the masses...