How To Write A Custom URP Shader With DOTS Instancing Support
I wanted to enhance the look of my boids BatchRendererGroup (BRG) sample with more suitable meshes instead of cubes. For that I decided to use a custom shader with some simple procedural vertex animation. For me, who hasn’t worked with URP or HDRP in production, this turned out to be quite a journey. So I am sharing the solution so you don’t have to spend your time.
For impatient: the template of a custom URP shader with DOTS instancing support on GitHub: UniversalRenderPipelineTemplateWithDotsInstancing.shader
What Is DOTS Instancing?
First of all, DOTS instancing is a new shader instancing mode used by BRG to render large instance counts efficiently. I wouldn’t copy-paste here the docs, but at the time of writing my post this docs page was only a week old and was the only info I had found about DOTS instancing. And to be fair, it didn’t help that much with how to write a custom shader.
Why DOTS instancing?
I used BRG to render boids in my previous post. So according to the docs, the only way to use a custom shader is to make it DOTS instancing compatible. And as it turned out later, it must also be SRP Batcher compatible.
A Custom Shader Template
If you check any URP shader bundled with your Unity installation, or any example from Graphics repo, or any sample project from numerous repositories provided by Unity Technologies, e.g. EntityComponentSystemSamples, you will see the heavy macros usage. No wonder DOTS instancing support comes down to adding a few magical macros.
Add UNITY_VERTEX_INPUT_INSTANCE_ID to your structs that are passed into vertex and fragment shaders:
struct Attributes { float4 positionOS : POSITION; float2 uv : TEXCOORD0; float4 color : COLOR; UNITY_VERTEX_INPUT_INSTANCE_ID }; struct Varyings { float4 positionCS : SV_POSITION; float2 uv : TEXCOORD0; float4 color : COLOR; UNITY_VERTEX_INPUT_INSTANCE_ID };
Then we need to declare out properties inside the pass using another macro UNITY_DOTS_INSTANCED_PROP, which is mentioned in the docs. However, the docs are missing a declaration of properties between CBUFFER_START and CBUFFER_END macros, so make sure to add it also to your shader:
CBUFFER_START(UnityPerMaterial) float4 _BaseMap_ST; float4 _BaseColor; CBUFFER_END #ifdef UNITY_DOTS_INSTANCING_ENABLED UNITY_DOTS_INSTANCING_START(MaterialPropertyMetadata) UNITY_DOTS_INSTANCED_PROP(float4, _BaseColor) UNITY_DOTS_INSTANCING_END(MaterialPropertyMetadata) #define _BaseColor UNITY_ACCESS_DOTS_INSTANCED_PROP_WITH_DEFAULT(float4, _BaseColor) #endif
And the final touch is to add the following macros to vertex and fragment shaders:
Varyings UnlitPassVertex(Attributes input) { Varyings output; UNITY_SETUP_INSTANCE_ID(input); UNITY_TRANSFER_INSTANCE_ID(input, output); const VertexPositionInputs positionInputs = GetVertexPositionInputs(input.positionOS.xyz); output.positionCS = positionInputs.positionCS; output.uv = TRANSFORM_TEX(input.uv, _BaseMap); output.color = input.color; return output; } half4 UnlitPassFragment(Varyings input) : SV_Target { UNITY_SETUP_INSTANCE_ID(input); half4 baseMap = half4(SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, input.uv)); return baseMap * _BaseColor * input.color; }
And that’s it. Here is the example of this shader used with BRG rendering 50000 spheres. It allows to set the color of each mesh since the shader has _BaseColor property, we just need to add an appropriate data to the GraphicsBuffer. And of course the full source code is available on GitHub:
And here is the whole shader, available as well on GitHub UniversalRenderPipelineTemplateWithDotsInstancing.shader:
Shader "Universal Render Pipeline/Custom/UnlitWithDotsInstancing" { Properties { _BaseMap ("Base Texture", 2D) = "white" {} _BaseColor ("Base Colour", Color) = (1, 1, 1, 1) } SubShader { Tags { "RenderPipeline"="UniversalPipeline" "Queue"="Geometry" } Pass { Name "Forward" Tags { "LightMode"="UniversalForward" } Cull Back HLSLPROGRAM #pragma exclude_renderers gles gles3 glcore #pragma target 4.5 #pragma vertex UnlitPassVertex #pragma fragment UnlitPassFragment #pragma multi_compile_instancing #pragma instancing_options renderinglayer #pragma multi_compile _ DOTS_INSTANCING_ON #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl" struct Attributes { float4 positionOS : POSITION; float2 uv : TEXCOORD0; float4 color : COLOR; UNITY_VERTEX_INPUT_INSTANCE_ID }; struct Varyings { float4 positionCS : SV_POSITION; float2 uv : TEXCOORD0; float4 color : COLOR; UNITY_VERTEX_INPUT_INSTANCE_ID }; CBUFFER_START(UnityPerMaterial) float4 _BaseMap_ST; float4 _BaseColor; CBUFFER_END #ifdef UNITY_DOTS_INSTANCING_ENABLED UNITY_DOTS_INSTANCING_START(MaterialPropertyMetadata) UNITY_DOTS_INSTANCED_PROP(float4, _BaseColor) UNITY_DOTS_INSTANCING_END(MaterialPropertyMetadata) #define _BaseColor UNITY_ACCESS_DOTS_INSTANCED_PROP_WITH_DEFAULT(float4, _BaseColor) #endif TEXTURE2D(_BaseMap); SAMPLER(sampler_BaseMap); Varyings UnlitPassVertex(Attributes input) { Varyings output; UNITY_SETUP_INSTANCE_ID(input); UNITY_TRANSFER_INSTANCE_ID(input, output); const VertexPositionInputs positionInputs = GetVertexPositionInputs(input.positionOS.xyz); output.positionCS = positionInputs.positionCS; output.uv = TRANSFORM_TEX(input.uv, _BaseMap); output.color = input.color; return output; } half4 UnlitPassFragment(Varyings input) : SV_Target { UNITY_SETUP_INSTANCE_ID(input); half4 baseMap = half4(SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, input.uv)); return baseMap * _BaseColor * input.color; } ENDHLSL } } }
Further Steps
This shader can be improved and made into lit. You can check a lit shader template shared by lead of URP at Unity: UniversalPipelineTemplateShader.shader. It doesn’t support instancing, but you can combine it with my template to get it working with BRG.
If you would like to read more interesting posts and tips about Unity and gamedev in general, follow my telegram channel and Twitter. I post there stuff I find interesting daily.
Leave a Reply