Writing Shader Code for the Universal RP

Vertex Shader

The main thing that our vertex shader needs to do is convert the object space position from the mesh into a clip space position. This is required in order to correctly render fragments/pixels in the intended screen position.

In built-in shaders you would do this with the UnityObjectToClipPos function, but this has been renamed to TransformObjectToHClip (which you can find in the pipeline-core SpaceTransforms.hlsl). That said, there’s another way to handle the transform in URP as shown below.

Varyings vert(Attributes IN) {
	Varyings OUT;

	VertexPositionInputs positionInputs = GetVertexPositionInputs(IN.positionOS.xyz);
	OUT.positionCS = positionInputs.positionCS;
	// Or this :
	//OUT.positionCS = TransformObjectToHClip(IN.positionOS.xyz);

	OUT.uv = TRANSFORM_TEX(IN.uv, _BaseMap);
	OUT.color = IN.color;
	return OUT;
}

GetVertexPositionInputs computes the position in different spaces. It used to be a part of Core.hlsl, but was separated into it’s own file – ShaderVariablesFunctions.hlsl in URP v9.x.x versions, but this is automatically included when we include Core.hlsl anyway.

We input the object space position from the Attributes and obtain a VertexPositionInputs struct, which contains:

  • positionWS, the position in World space
  • positionVS, the position in View space
  • positionCS, the position in Clip space
  • positionNDC, the position in Normalised Device Coordinates

We don’t need the position in any of these other coordinate spaces for our Unlit shader, but it is useful for shaders where we do, and the unused ones won’t be included in the compiled shader anyway so there isn’t any unnecessary calculations.

The vertex shader is also responsible for passing data to the fragment. For the vertex color, that’s just a simple “OUT.color = IN.color;”.

If we want to be able to sample a texture, our model’s UVs (texture coordinates) also need to passed through. While we could just do “OUT.uv = IN.uv;” (assuming both are float2), it’s common to use the TRANSFORM_TEX macro which takes the uv and texture property name and applies the offset and tiling from the material inspector (which is stored in “_BaseMap” + “_ST”, S for scale and T for translate). This macro is in both built-in and URP (inside core/ShaderLibrary/Macros.hlsl, which should automatically be included with Core.hlsl).

This is actually just shorthand for “IN.uv.xy * _BaseMap_ST.xy + _BaseMap_ST.zw” so you could also just write that (swapping out the _BaseMap for the intended texture property of course. The “(texture)_ST” float4 variable must also be added to the UnityPerMaterial CBUFFER (discussed in the HLSL section).

While we don’t have any normal/tangent data for our Unlit shader, there is also :

VertexNormalInputs normalInputs = GetVertexNormalInputs(IN.normalOS, IN.tangentOS);

GetVertexNormalInputs can be used to convert the object space normal and tangent into world space. It contains normalWS, tangentWS and bitangentWS vectors. There is also a version that only takes the normal as an input, which leaves tangentWS as (1,0,0) and bitangentWS as (0,1,0), or you can also use TransformObjectToWorldNormal(IN.normalOS) instead.