MaterialPropertyBlock equivalent for shaderKeywords

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:

        MaterialPropertyBlock mpb = new MaterialPropertyBlock();
        mpb.SetColor("_MyColor",Color.white);
        myRenderer.SetPropertyBlock(mpb);

        MaterialKeywordBlock mkb = new MaterialKeywordBlock();
        mkb.EnableKeyword("SOME_KEYWORD");
        myRenderer.SetKeywordBlock(mkb);

Get existing keywords, to remain compatible with other scripts that might also want to set the property/keyword blocks:

        MaterialPropertyBlock mpb = new MaterialPropertyBlock();
        myRenderer.GetPropertyBlock(mpb);
        mpb.SetTexture("_Texture", someTexture);
        myRenderer.SetPropertyBlock(mpb);

        MaterialKeywordBlock mkb = new MaterialKeywordBlock();
        myRenderer.GetKeywordBlock(mkb);
        mkb.EnableKeyword("SOME_KEYWORD");
        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.

Bump…

Bump

Bump

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.

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.

Bump.

@phil_lira @Tim-C @quixotic

Not sure how else to get dev response since the feedback site does not exist.

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 :frowning:

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?!

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…

Bump

Bump :frowning:

Noted. We’ll think about this :slight_smile:

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.

Yeah, I agree I think all you really need is Enable/disable keyword on the renderer.

Bump :frowning:

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.

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!

Well, that’s the thing - LIGHTMAP_ON is a builtin global keyword. It is enabled or disabled per batch deep in the engine :slight_smile:

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?

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)
}
}