Skip to main content

Command Palette

Search for a command to run...

Unity URP - Renderer Shader User Value (RSUV)

Updated
โ€ข10 min read
Unity URP - Renderer Shader User Value (RSUV)
E

๐Ÿ‘‹๐Ÿพ Hello! ๐Ÿ‘‹๐Ÿพ My name is Esteban and I love video games and learning about making games. I'm sharing here my progress as I build up my skills and learn new technologies.

Context

In my previous article, I wrote about how Unity developers at runtime through C# code can change the material input values on game objects that share a material, like in our 10 by 10 grid of cubes:

Shortly after, I got great feedback from a good friend in the industry:

So I decided to do just that, I followed my friend's advice!

On this quick follow-up article, I'm going to introduce a new way to achieve the result from the previous tutorial, with that new Renderer Shader User Value (RSVU) feature my friend mentioned, that I wasn't aware of. And trust me, I recommend that you learn it as well.

RSUVs landed in Unity 6.3 LTS, and let you assign a custom 32-bit unsigned integer value to either a MeshRenderer or SkinnedMeshRenderer, using the following API:

MeshRenderer.SetShaderUserValue(uint value);
SkinnedMeshRenderer.SetShaderUserValue(uint value);

Whatever value you decide to assign will be accessible within shader HLSL code through the unity_RendererUserValue property. Importantly, this functionality introduces no additional CPU overhead and does not interfere with batching.

Quick script updates

Let's first change our ModificationMethod enum to add a new value, RSVU.:

using UnityEngine;

[RequireComponent(typeof(Renderer))]
public class MaterialBaseColorModifier : MonoBehaviour
{
    public enum ModificationMethod
    {
        none,             // No modification
        SharedMaterial,   // Modifies the root asset, affects all
        MaterialInstance, // Creates a new instance of the material 
        PropertyBlock,    // Per-instance input value modification
        RSUV              // per-renderer data, new in Unity 6.3 LTS!
    }
    
    ...

    public void ApplyModification(Color colorValue)
    {
        ...
    }

    ...
}

Now let's update our ApplyModification(Color colorValue) function to handle this new modification method:

public void ApplyModification(Color colorValue)
{
    if (_renderer == null)
        return;

    switch (modificationMethod)
    {
        case ModificationMethod.SharedMaterial:
            ...
            break;

        case ModificationMethod.MaterialInstance:
            ...
            break;

        case ModificationMethod.PropertyBlock:
            ...
            break;

        case ModificationMethod.RSUV:
            // 1st storing the color value from a float decimal representation to an 8-bit byte one (per color channel)
            Color32 colorb = colorValue;

            // packing each color value (RBG) into a 32-bit unsigned integer, using bitwise operations
            uint packedColor = 
                (uint)colorb.r | ((uint)colorb.g << 8) | ((uint)colorb.b << 16);

            _renderer.SetShaderUserValue(packedColor);
            break;
        }
    }

Note: This is a base case for using this value, remember that this is essentially a 32-bit buffer that you can use for whatever rendering purpose.

After the script compiles, we now can see that we can select RSVUin the modification method field in the editor. Let's test it out!

Spoiler alert... this will not work... yet.

Our first test!

When we press play, we see that even though one can expect that we'd be updating the base color like before, but nothing is happening yet:

And the reason why it's not working was hinted at us in the previous image. Our TutorialMaterial is still using the URP's own built-in Lit shader, and that code doesn't access the RSUV value:

So we need to find a way to access that per-renderer value, and bring it into our material.

A new shader approaches!

We have a some options that we can consider, the 1st one is creating a blank Lit shader graph. We can do this by right-clicking on the folder we'd like to store our new shader graph file and go to Create > Shader Graph > URP > Lit Shader Graph. Then name the new file something like "RSUV_Lit":

When we open the file you'll see this in the shader graph editor:

Now press the space bar, which will bring up the search bar for the multiple available shader graph nodes, and search for the Custom Function node, which let's you inject your own custom HLSL code in shader graphs:

You'll now see this empty custom function node. It doesn't do anything at the moment, because it's an empty function:

If you activate the shader graph editor inspector, you can look at the node's properties, which by default are entirely empty:

We won't need to add any inputs in this basic example, but we'll need to add an output, since we'll extract our color from the unity_RendererUserValue property that I mentioned before.

Here's a small recording of how to add a function name, something like getBaseColorFromRSUV , and adding the Vector4 output called baseColorFromRSUV:

We now need to add the body or source of the function, custom functions can come from an external source file, or through a directly-embedded string.

For now, we'll use the following sample snippet, based on Unity's own documentation on the RSUV:

uint c = unity_RendererUserValue;

// Note: setting the output color, needs to be called exactly how it's
// specified in the Shadergraph UI
baseColorFromRSUV = float4((float)((c >> 0) & 255) * (1.f / 255.f),
                        (float)((c >> 8) & 255) * (1.f / 255.f),
                        (float)((c >> 16) & 255) * (1.f / 255.f),
                        (float)1.f);

Change the Type to String, then paste the code in the Body field. This will make your custom function node look like this:

We now can use our custom function in the shader graph! Use the output value and plug it directly to the SG's Base Color field, like so:

And last, but not least, we can now set our TutorialMaterial to use our new shader graph:

Note: Notice we don't have any of the useful inputs from URP's Lit shader from before, we'll address this later.

Now when try and play the scene one more time, we see we get a similar result as before, and it's a very efficient method because we're using the same root material without any new instances or modifications. All of this by using a very memory cost-effective solution of 1 unsigned integer (or 32-bits) per renderer. Since we're using 100 renderers on scene, that's only an additional 400 bytes ( < 1KB).

A new URP Lit shader approaches!

Like we saw before, we now have a new shader that's using the base Lit material from the universal settings in the shader graph file.

But this is not the same as using something like the standard URP Lit shader that's part of the built-in assets of the URP render pipeline, which exposes various input fields, and does more precise physics-based lighting calculations. Luckily, we have new resources available in Unity 6.3!

Shader templates to the rescue!

Starting in Unity 6.3, we can create new shader graphs based on built-in templates, and one of them is based on the URP Lit shader!

To get started, right click on the folder you'd like to store your new shader and go to Create > Shader Graph > From Template... . From there you'll get the following window:

Note: There are plenty of useful templates here! Feel free to explore the other options in future projects.

Pick the Lit Full template, and we can call it something like "RSUV_URP_Lit":

When the file gets created and opened, you'll quickly notice that this is a much more complex shader graph:

Woah! ๐Ÿ˜…
Well, the description did say: A complex shader that closely mimics the functionality of URP's Lit shader (the code version)...

Here's a quick comparison of the input layouts, see that they're very similar:

On the left, a quick copy I created of the TutorialMaterial is using the standard URP Lit shader, vs. the one on the right using the Full Lit shader graph version from the template

Enter the RSUV

Although this is a much more complex shader, we don't need to do a lot of modifications for our purposes. Notice that at the top of the shader file, you'll see a section called BaseColor :

There's an instance of a Color field being used here to be multiplied with the base texture color.

Let's now bring in our custom function node. Thankfully, you can can copy the one from the RSUV_Lit shader graph we previously worked on, and paste it directly onto this new one, like so:

To very quickly test this out, can do something like what I did here where I multiplied the Color parameter value with the color from the getBaseColorFromRSUV function, essentially the full base color being:

baseTexture *(baseColorFromRSUV * Color)

And here's how that looks like in the shader graph. The default value of the Color field is all ones (1,1,1,1), so you won't get any color weirdness when mixed in with the RSUV color (unless you'd like to change it):

Note: Yep, you could just switch out the Color field with the RSUV one, or add them, etc. This is just to get something working.

Lastly, let's set our material to use this new shader graph:

And check it out, we now have achieved our result, but using a very similar shader to the one we used in the beginning:

A great external tool - RSUV Bit Packer!

Before I end this tutorial, I simply must share about this exploratory (but very cool) package called the RSUV Bit Packer, by Fred Moreau, a Shader and Visual Effects Product Manager, from Unity!

This awesome tool introduces an editor-friendly way to set and retrieve values from the RSUV. If you look closely, it lets you define data bit by bit with a very user-friendly editor inspector GUI!

https://www.youtube.com/watch?v=7KDWA-HkTr8

And fun fact, Fred has some good news for adopters of the latest Unity versions like 6.5 and above:

Unity 6.5 and above

In Unity 6.5, Shader Includes are generated using the Shader Function Reflection API syntax, which makes them automatically accessible in Shader Graph without having to manually configure a Custom Function Node.

โš ๏ธ I must say however, it's not an official Unity package, and he's very honest with his disclaimer: โš ๏ธ

This is personal exploratory work that Unity has no liability with.
It may or may not work, comes without support nor maintenance. Use at your own risk.

But I've used this tool, and you can easily see that it definitely came from a very passionate creator that wants to further help Unity users! I hope that his solution gets improved and potentially integrated into the official URP package in the future.

I've said it before and I'll say it again, the game dev. community is awesome!

Lessons learned

  • RSUVs are definitely a very cost-efficient way of modifying the visual style of objects sharing the same material!

  • It's definitely worth it to keep learning about what new features are available when a new version of your favorite game engine drops.

Please let me know what you think!

  • Have you used the RSUV field before?

  • What are other ways you've used to give different visual results to objects sharing the same material?

๐ŸŽ‰๐ŸŽ‰๐ŸŽ‰ Happy building! ๐ŸŽ‰๐ŸŽ‰๐ŸŽ‰