Renderer Shader User Value: Customize your visual per renderer

New in 6.3: Renderer Shader User Value (RSUV)

History

In certain scenarios, games may need to manage a large number of objects (e.g., MeshRenderers) while applying unique visual customization to each instance. Prior to the introduction of the Scriptable Render Pipeline (SRP), the most efficient method for achieving this was through the use of Material Property Blocks (MPBs).

With the advent of the SRP Batcher, however, a more performant approach has been to generate a dedicated Material for each customized renderer. This method has demonstrated significantly better runtime performance compared to MPBs.

Nevertheless, in many cases the required customization per object is limited to only a small set of parameters. While duplicating the entire Material for each object is a nice and simple solution, a more focused and efficient alternative can now be employed.

Renderer Shader User Value (RSUV)

In Unity 6.3, we introduced a small yet powerful feature accessible via the MeshRenderer and SkinnedMeshRenderer APIs:

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

This API enables the assignment of a custom 32-bit unsigned integer value to each renderer. The value is accessible within shader HLSL code through the unity_RendererUserValue property. Importantly, this functionality introduces no additional CPU overhead and does not interfere with batching.

The primary advantage of this approach is that all customized renderers continue to share a single Material. As a result, in most scenarios it yields a notable performance improvement (see the Performance section below).

Note: From the shader side, the RSUV value can be easily accessed within Shader Graph by means of a custom HLSL node.

Use case and ideas

At first glance, a 32-bit value may seem insufficient for customizing visuals on a per-renderer basis. However, the unity_RendererUserValue (RSUV) can be applied flexibly in a variety of scenarios. Examples include:

Note: RSUV usage and encoding are shader-specific. For instance, one shader might interpret the RSUV as a color, while another may treat it as an index into auxiliary data. The potential applications are numerous, even when utilizing just a single 32-bit value.

Performance

Benchmark project

While theoretical considerations are useful, performance measurements provide the most reliable insight on the value of the feature.

To this end, we conducted a test project involving 900 MeshRenderers. Each renderer was assigned a unique color and scale value. Performance was evaluated by averaging the CPU profiler marker RenderLoop.DrawSRPBatcher, measured in milliseconds. It is important to note that this metric reflects only the CPU rendering workload, not the total frame time.

Note: To better approximate a real-world game scenario, a new sphere mesh was instantiated every 10 objects, effectively simulating the presence of a new mesh model at this interval.

The benchmark scene is shown below, where each object exhibits a randomized color and scale:

We evaluated four methods to achieve the desired customization:

  1. Material Property Block (MPB):
    A Shader Graph was configured to use a Vector4 for color and a float for uniform scale. At initialization, each renderer was assigned a unique MPB containing randomized color and scale values.
  2. Multiple Materials (MM):
    At initialization, a distinct Material was instantiated for each renderer, with each Material containing a unique color and scale value.
  3. GPU Resident Drawer (GRD) with Multiple Materials:
    Identical to the Multiple Materials method, but with GRD enabled.
  4. Renderer User Value (RSUV):
    At initialization, a unique RSUV value was assigned to each renderer using the new API. The value holds 24 bits for RGB color and 8 bits for uniform scale. A Shader Graph, extended with a custom HLSL node, was used to decode the RSUV value. GRD was disabled in this configuration.
  5. GRD + RSUV:
    Same as the RSUV method above, but with GRD enabled.

We begin by comparing the timing results of the legacy MPB approach against the standard SRP Batcher Multiple Materials method.

Note: The use of Material Property Blocks (MPBs) is notably inefficient in this particular scenario, as we are dealing with 900 individual renderers, each requiring two unique custom values, as this creates a large amount of batches. The performance disadvantage would be less significant if multiple renderers shared the same color or scale values. Nonetheless, it is important to emphasize that MPBs generally offer lower performance compared to using separate materials, especially since the introduction of the SRP Batcher.

Let us now set aside the legacy MPB approach and concentrate on the four alternative methods:

As a timing chart should always be accompanied by an explanation, here are some technical details regarding the reported numbers:

  • MM
    (Multiple Materials): 1.42 ms serves as the baseline, where each renderer uses a unique material.

  • GRD+MM
    When enabling GRD, one might expect a significant performance improvement. However, in this context—where each renderer has a different material (to allow for random color assignments)—this represents the worst-case scenario for GRD. GPU instancing cannot be utilized, resulting in one draw call per object, in addition to the overhead of switching materials between objects. Nevertheless, GRD+MM still beats MM, with a rendering time of 1.26 ms.

  • RSUV
    Achieving a time of 1.08 ms, RSUV is faster than GRD+MM. This is because, unlike GRD+MM—which incurs both draw call and material switching costs per object—RSUV does not require separate materials and therefore avoids the material switching overhead.

  • GRD+RSUV
    This combination yields the best results. Since RSUV does not require different materials, it allows GRD to fully optimize draw calls (in this scene, one draw call can handle 10 objects). Individual object customization with random values is still possible, while obtaining the maximum performance benefits from GRD.

As a result, we achieved an impressive 5x performance increase with the combination of GRD and RSUV compared to the baseline MM approach!

Last note

We designed the RSUV feature to introduce no additional CPU overhead. To demonstrate this, we modified our benchmark scene so that no per-renderer customization was applied—all spheres shared the same color and size. Under these conditions, the classic SRP Batcher executed in 1.09 ms. If we disregard performance measurement jitter, its performance matches that of RSUV alone.

Under the same conditions, GRD rendered in 0.27 ms, identical to the performance of GRD combined with RSUV.

These results clearly demonstrate that RSUV enables per gameobject shader customization with virtually no additional CPU overhead.

55 Likes

Thanks for this! Will be looking into it when we migrate to 6.3.

This is awesome!
Might write some custom shader graph blocks for this to decode certain things like color and more!

Has it already landed in .3.0a5?

Sounds great! Thanks.
Could you please provide a sample project with this?

1 Like

I am confused by this. It’s been a bit since I dove into instancing in Unity, but I’m pretty sure there were already mechanisms to automatically batch multiple materials (same shader/keywords, different values) into much fewer instanced draw call as long as the shader implements the correct setup (cbuffer stuff, full of Unity-specific macros). I think 2023.3 alpha was the first version to implement ‘DOTS instancing’ (even more boilerplate) as the BatchRendererGroup equivalent (basis for GPU Resident Drawer), but it was mostly SRPBatcher vs old draw call gains.

The data types supported for instancing with the existing implementations are the same as any graphics API would provide, if I’m not mistaken. Most types work fine with the exception of textures/samplers for which you’d move to Texture Arrays or Virtual Textures to still benefit from instancing and/or multi-draw (index would likely be held in the instance buffer/block).

There were also additional limitations in Unity with the particle shaders (no DOTS instancing) and skinned meshes (no instancing since only compute-based skinning is currently supported and no multi-draw draw calls to compensate for the duplicated/reprocessed pre-skinned meshes), which this likely won’t address.

So, I’m not really catching the improvement here. I don’t have any experience with MaterialPropertyBlock since when I started looking into all of this, Unity was already discouraging its use in favor of SRPBatcher, and later GPU Resident Drawer. The only thing I’d think of is replacing MaterialPropertyBlocks for procedural draw cases, but those would shortcircuit the use of GameObjects/components (which this new feature seems to be based on).

I’m always interested in ways to reduce draw calls, as I believe Unity still has to catch up with now aging standard features on that end, so I really hope I just misunderstood some very neat feature. Given the use of code, it’s still not as perfect of a workflow as the usual automagic management, but if it’s improving on limitations, it’s always a gain (as long as it doesn’t get replaced every two weeks).

Edit: I guess the main point is modifying instance properties specifically at runtime rather than strictly being about instancing? Never done that myself since I’ve so far only faced cases of pre-determined states and I prefer data in such cases to be stored in assets rather than code, but if modifying instance data at runtime required MaterialPropertyBlocks before, that’d definitely be an improvement given its incompatibility with the newer batching system.

Edit 2: That reminds me, I think the particle shaders were specifically using MaterialPropertyBlocks for modifying instance properties from the CPU? So I guess more of the official shaders (possibly including VFX Graph too) will be compatible with the GPU Resident Drawer now?

Is there a code example of “an index into a larger data structure stored in a global GraphicsBuffer”?
I see that this approach is identical to how DOTS Instancing works, but it would be very nice to have overriding of any property out of the box similar to Entities.

1 Like

Too bad I still can’t use it to assign a unique (randomly sized) 3D texture per renderer :downcast_face_with_sweat:
(Apart from insane tricks like storing an index to a structure containing offset/scale into a huge atlas of many 3D textures - but that’s way too many indirections to be a reasonable workaround).
It would be nice if MPBs actually worked (without breaking all sorts of batching). Kinda ridiculous that SRP batching disallows changing textures (not constants) per object.
For constants, because current MPB implementation isn’t useful, I pass a lot of extra data via renderer.realtimeLightmapScaleOffset lol… that’s already 4 floats (or ints)! Works perfectly with SRP batching.

2 Likes

Having this is great, but it doesn’t beat the simplicity, power and versatility of MPBs. Why don’t you make MPB work with SRP batcher, even if behind the scenes it creates new materials, and at least we can use a single familiar system with any batching system.

3 Likes

Yes. It’s landed in 6000.3.0a5, with an API improvement (a getter) to come in the next public version.

Yes. We should add a sample before final release.

5 Likes

:folded_hands:

We appreciate that.
This new feature is not meant to replace MPB, but it does fit many use cases where MBPs would’ve been an obvious choice at the expense of renderers being discarded by the SRP Batcher.

That’s a bit more involved, but MPB x SRP Batcher is on our roadmap.

6 Likes

I wish this thread should show example with shadergraph

Sounds great!
Now I can use per object ID without sacrificing RenderLayer function. I have used the ID to emphasize selected objects or flash and shake damaged objects, by shader.

I can understand that draw GRD-BRG with many materials breaks instancing, but draw DOTS-BRG has a better option for material overrides. I believe this override function doesn’t duplicate materials, use UNITY_DOTS_INSTANCED_PROP instead.
So maybe GRD-BRG has room to use this instead of breaking instancing with many materials..

We will share examples.

In Shader Graph you can use a Custom Function Node with:

Out = unity_RendererUserValue;

3 Likes

Thanks for making me double-check. I was going crazy because I’ve been using that exact setup since 2023.3 beta, but it looks like even with all the boilerplate on the shader side, the renderer (GRD/non-DOTS URP) isn’t able to batch the varying instances properly even when using basic types for properties. I was sure it did (maybe the old Frame Debugger pre-6.0 reported so). I also remember applying DOTS_INSTANCING_ON to be more finicky, but it looks like just the basic cbuffer with some includes/pragmas is enough for it to be enabled.

For those of us who hate shader graph and prefer to write shaders, is there a specific way we have to write our shaders to support this when it comes to writing our cbuffers?

Also given MPB x SRP Batcher is on the roadmap will that eventually make these other solutions redundant?

got the delta with MPB on BIRP?

Unlikely.

At the moment, the MPB x SRP Batcher initiative is for us to assess where MPB are still relevant, and design a solution that’s as easy – if not easier – for developers and artists to override materials properties per renderer instance, and as efficient as possible for SRB Batcher and GPU Resident Drawer to deal with.

The modern approach would be something like Material Runtime Variants.
The goal being to provide full flexibility in overriding existing properties in a material, for purposes like animation, it is unlikely to beat this Renderer Shader User Value in terms of performance.

3 Likes

You’re right. In our roadmap we have a code unification between GRD and entities.graphics. So these two features will share more code, including better option for material overrides in BRG. I can’t give any ETA but we have this on our radar.

3 Likes

You don’t have to worry about anything, just use the unity_RendererUserValue property name. ( URP and HDRP shader code base already do the right includes for you).
Under the hood, unity_RendererUserValue will be a simple constant from UnityPerDraw UBO in case of standard SRP Batcher code path. It will be a SSBO 32bits fetch in case of GRD rendering.

1 Like