Writing Shader Code for the Universal RP

Lighting Introduction

In the built-in pipeline, custom shaders that required lighting/shading was usually handled by Surface Shaders. There was an option to choose which lighting model to use, either the physically-based Standard and StandardSpecular or Lambert (diffuse) and BlinnPhong (specular) models. You could also write custom lighting models, which you could use if you wanted to produce a toon shaded result for example.

The Universal RP does not support surface shaders, however the ShaderLibrary does provide functions to help handle a lot of the lighting calculations for us. These are contained in Lighting.hlsl – (which isn’t included automatically).

There are functions inside that lighting file that can completely handle lighting for us, including UniversalFragmentPBR which we’ll discuss in the next section. For now I’m going over much simpler lighting and shadows for the main directional light only, as an example to ease a little into the more complicated PBR lighting model.

In Lighting.hlsl there’s a GetMainLight function, which you may already know about if you are familiar with custom lighting in shader graph. In order to use this we first include the Lighting.hlsl file, and while we’re at the top of the HLSLPROGRAM I’ll also add in some multi_compile directives which provide keywords that are required in order to receive shadows.

#pragma multi_compile _ _MAIN_LIGHT_SHADOWS
#pragma multi_compile _ _MAIN_LIGHT_SHADOWS_CASCADE
#pragma multi_compile _ _SHADOWS_SOFT

#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"

Next, we’ll need the vertex normals in order to handle the shading/lighting, so we’ll add them to our Attributes and Varyings structs, and update the vertex shader. Here I’m only showing the added code based on the Unlit shader we made through the previous sections.

struct Attributes {
	float4 normalOS		: NORMAL;

struct Varyings {
	float3 normalWS		: NORMAL;
	float3 positionWS	: TEXCOORD2;
Varyings vert(Attributes IN) {
	Varyings OUT;
	VertexPositionInputs positionInputs = GetVertexPositionInputs(IN.positionOS.xyz);
	OUT.positionWS = positionInputs.positionWS;

	VertexNormalInputs normalInputs = GetVertexNormalInputs(IN.normalOS.xyz);
	OUT.normalWS = normalInputs.normalWS;

	return OUT;

In the fragment shader we can now take the worldspace normal, and use the world space position to calculate the shadow coordinate. (Technically, we could also calculate the shadow coordinate in the vertex shader and pass it through, but only for when the shadow cascades keyword is disabled. For now I’d like to keep it simple, but that will be included in the example in the next section).

half4 frag(Varyings IN) : SV_Target {
	half4 baseMap = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, IN.uv);
	half4 color = baseMap * _BaseColor * IN.color;

	float4 shadowCoord = TransformWorldToShadowCoord(IN.positionWS.xyz);
	Light light = GetMainLight(shadowCoord);

	half3 diffuse = LightingLambert(light.color, light.direction, IN.normalWS);

	return half4(color.rgb * diffuse * light.shadowAttenuation, color.a);

While our shader will receive shadows from other shaders, note that it doesn’t  have a ShadowCaster pass so won’t cast shadows, onto itself or other objects. See the ShadowCaster section.

If we wanted shadows, but no diffuse shading on the object, we can also remove the diffuse shading calculation and just use light.shadowAttenuation.

If you want to extend this further to include ambient/bakedGI and additional lights, look at the UniversalFragmentBlinnPhong method in Lighting.hlsl as an example, or just let it handle the lighting for you. It uses a InputData struct, which is also used by the PBR example discussed in the next section.