Writing Shader Code for the Universal RP

Shaderlab

Shader files in Unity are written using the Shaderlab syntax to define the shader properties, subshaders and passes, while the actual shader code is written in HLSL (High level shading language) inside those passes.

Much of the Shaderlab stuff hasn’t changed compared to the built-in pipeline so I’ll provide an example, but rather than going into great detail I’ll be linking to the Unity shader documentation where it can still apply to URP. The only real differences are the “RenderPipeline” tag and “LightMode” tags for URP.

First up, the Shader block :

Shader "Custom/UnlitShaderExample" {
	...
}

Inside here, we need a Properties block and Subshader block (and inside that Pass blocks) :

Properties {
//	[name] ("[name in inspector]", [type]) = [default value]
	_BaseMap ("Base Texture", 2D) = "white" {}
	_BaseColor ("Base Colour", Color) = (0, 0.66, 0.73, 1)
//	_ExampleDir ("Example Vector", Vector) = (0, 1, 0, 0)
//	_ExampleFloat ("Example Float (Vector1)", Float) = 0.5
}

The Properties block is for any values that need to be exposed to the Material Inspector, so that we can use the same shader for materials with different textures/colours for example.

We don’t need to put properties here if we want to control a value from script (e.g. using material.SetColor/SetFloat/SetVector)… However if values are going to be different per object, we should put them here as it causes some glitchy rendering with the SRP Batcher trying to batch objects with different unexposed values. If they aren’t different per object, it’s easier to use Shader.SetGlobalColor/Float/Vector instead. We’ll discuss this more later on when starting the HLSL code.

SubShader { 
		Tags { "RenderType"="Opaque"
		"Queue"="Geometry" 
		"RenderPipeline"="UniversalPipeline" }
		
		HLSLINCLUDE
		...
		ENDHLSL
		
		Pass {
			Name "ExamplePass"
			Tags { "LightMode"="UniversalForward" }
			
			HLSLPROGRAM
			...
			ENDHLSL
		}
	}

Unity will use the first Subshader block that is supported on the GPU, and due to our “RenderPipeline”=”UniversalPipeline” tag, it won’t run in the built-in pipeline or HDRP and instead will try the next Subshader defined in the shader. If there are no Subshaders supported it’ll show the magenta error shader instead.

Note I’m not sure if the RenderType tag is important in URP. It’s typically used in the built-in pipeline for replacement shaders (which URP doesn’t support, although there’s an overrideMaterial for Forward Renderer features which is kinda similar). The Queue tag is the order that objects are rendered in, which is mostly useful for overriding how transparent objects should be sorted or used alongside stencil operations. You can find more information on these tags here.

Multiple Pass blocks can be defined in the Subshader, but each should be tagged with a specific LightMode (see below). URP uses a single-pass forward renderer, so only the first “UniversalForward” pass (that is supported by the GPU) will be used to render objects – you can’t have multiple and have them both render. While you can have other passed untagged, be aware that they will break batching with the SRP Batcher. It is instead recommended to use separate shaders/materials, either on separate MeshRenderers or use the Render Objects feature on the Forward Renderer to re-render objects on a specific layer with an overrideMaterial.

URP LightMode tags :
  • UniversalForward – used to render objects with the Forward Renderer.
  • ShadowCaster – used for casting shadows
  • DepthOnly – seems to be used when rendering the depth texture for the scene view, but not in-game? Some renderer features might make use of it though.
  • Meta – used during lightmap baking only
  • Universal2D – used when the 2D Renderer is enabled, instead of the Forward Renderer.
  • UniversalGBuffer – related to deferred rendering. I think this is in development / testing. URP v10+?

You’ll also notice that the Pass has been given a “Name”, which is used by the UsePass command. Instead of writing out an entire pass, it’s possible to just use a pass from a different shader. Note that the name gets converted to uppercase internally. For example, if we wanted to use this pass in a different shader we could do :

UsePass “Custom/UnlitShaderExample/EXAMPLEPASS”

That pass would then be included in that other shader… However, in order for shaders to be compatible with the SRP Batcher all of their passes must share the same UnityPerMaterial CBUFFER, which is a problem if we use UsePass and they don’t match (at least in the current URP versions, it’s possible this will be fixed/changed in the future). I’ll be going over what this CBUFFER actually is in the next section.

In the Pass block you’ll also commonly see Cull, ZWrite and ZTest, which default to Cull Back, ZWrite On and ZTest LEqual if not defined. For shaders in the Transparent queue, you usually also need to define Blend operations. Stencil operations can also be defined here too.

The complete Shaderlab looks something like this. In the next section we’ll go over the HLSL parts.

Shader "Custom/UnlitShaderExample" {
    Properties {
		_BaseMap ("Example Texture", 2D) = "white" {}
		_BaseColor ("Example Colour", Color) = (0, 0.66, 0.73, 1)
		//_ExampleDir ("Example Vector", Vector) = (0, 1, 0, 0)
		//_ExampleFloat ("Example Float (Vector1)", Float) = 0.5
    }
    SubShader { 
		Tags { "RenderType"="Opaque" 
		"Queue"="Geometry"
		"RenderPipeline"="UniversalPipeline" }
		
		HLSLINCLUDE
		...
		ENDHLSL
		
		Pass {
			Name "Example"
			Tags { "LightMode"="UniversalForward" }
			
			HLSLPROGRAM
			...
			ENDHLSL
		}
	}
}