# **URP Shaders and Shader Graph in Unity 6 (Shader Graph, Hand-Written HLSL, Render Graph Renderer Features)**
**Unity Version: 6000.5 and above, Universal Render Pipeline (URP 17.x+).** In Unity 6, URP's renderer runs on the **Render Graph** system. Old-style `ScriptableRenderPass.Execute()` and the `cameraColorTargetHandle` pattern are legacy (Compatibility Mode, deprecated and being removed). Everything below uses `RecordRenderGraph`.
## Part 0 – URP vs HDRP: Which One?
| Choose **URP** when | Choose **HDRP** when |
|---|---|
| Any platform: mobile, Switch, XR/Quest, WebGPU, consoles, PC | High-end PC / current consoles only |
| You want stylised or performant visuals | You want physically-based, photoreal lighting (physical light units, volumetrics, path tracing) |
| Small to mid team, fast iteration | Teams with lighting/tech-art expertise |
| You need custom render passes that are easy to write | You need Custom Passes, ray tracing, advanced post |
Unity has stated URP is the long-term focus for the unified renderer direction, so for most new projects in Unity 6 **URP is the default choice**. Switching later is painful (materials, shaders, lighting all differ), so decide early.
---
## Part 1 – Project Setup
1. Unity Hub > New Project > **Universal 3D** template (6000.5+).
2. Confirm the pipeline asset: *Edit > Project Settings > Graphics > Default Render Pipeline* = `PC_RPAsset` (or Mobile). Each Quality level can override it.
3. Find the **Renderer** asset referenced by that pipeline asset (`PC_Renderer`). This is where Renderer Features are added in Part 5.
4. Make sure *Project Settings > Graphics > Render Graph > Compatibility Mode* is **off** (it is off by default in new Unity 6 projects).
---
## Part 2 – Shader Graph: Dissolve Effect
**Goal:** an object that dissolves with a glowing edge, driven by a `Dissolve` float (0 = solid, 1 = gone).
### Steps
1. *Project window > Create > Shader Graph > URP > Lit Shader Graph*. Name it `SG_Dissolve`. Double-click to open.
2. In the **Graph Inspector > Graph Settings**: Surface Type **Opaque**, enable **Alpha Clipping**. (Alpha clip on opaque is cheaper than transparency and still writes depth/shadows correctly.)
3. **Blackboard** (+ button) – add properties:
- `BaseMap` (Texture2D), `BaseColor` (Color, white)
- `Dissolve` (Float, Slider 0–1, default 0)
- `NoiseScale` (Float, default 20)
- `EdgeWidth` (Float, default 0.05)
- `EdgeColor` (Color, **HDR** mode, e.g. orange intensity 4)
4. Build the nodes:
- `Sample Texture 2D` (BaseMap) × `BaseColor` → **Base Color**.
- `Simple Noise` (UV default, Scale = `NoiseScale`) → call this **N**.
- **Alpha:** connect **N** to the master stack **Alpha**.
- **Alpha Clip Threshold:** connect `Dissolve` directly. Pixels where N < Dissolve are clipped.
- **Edge glow:** `Add` (Dissolve + EdgeWidth) → `Step` (Edge = that sum, In = N) gives 1 where noise is *above* the band. `One Minus` that result → band mask (1 inside the thin edge band just above the clip line). Multiply band mask × `EdgeColor` → **Emission**.
5. **Save Asset** (top left). Right-click the graph > Create > Material. Assign to a mesh.
6. Animate `Dissolve` in the material Inspector (or from script below).
**Why it works:** Alpha clipping discards fragments where `Alpha < Threshold`. Since Alpha = noise and Threshold = Dissolve, raising Dissolve eats away the low-noise regions first. The edge band is simply "noise between Dissolve and Dissolve + EdgeWidth".
Driving it from code (use property IDs, and `MaterialPropertyBlock`-free instancing via the renderer's material instance is fine for one-off objects):
```csharp
using UnityEngine;
public class DissolveDriver : MonoBehaviour
{
[SerializeField] float duration = 2f;
static readonly int DissolveId = Shader.PropertyToID("_Dissolve");
Material mat;
float t;
void Awake() => mat = GetComponent<Renderer>().material; // instance per object
void Update()
{
t += Time.deltaTime / duration;
mat.SetFloat(DissolveId, Mathf.PingPong(t, 1f));
}
}
```
> **Reference name:** select `Dissolve` in the Blackboard and check the **Reference** field in the Graph Inspector. By default it is `_Dissolve`, which is what `Shader.PropertyToID` must match.
> **SRP Batcher:** Shader Graph properties declared in the Blackboard live in the `UnityPerMaterial` CBUFFER automatically, so the graph stays SRP Batcher compatible. Avoid per-object `MaterialPropertyBlock` with SRP Batcher materials; use material instances or GPU Resident Drawer–friendly approaches.
---
## Part 3 – Shader Graph: Rim / Fresnel Glow
1. Create > Shader Graph > URP > **Lit Shader Graph**, name `SG_Rim`.
2. Blackboard: `BaseColor` (Color), `RimColor` (Color, HDR), `RimPower` (Float, default 3), `RimStrength` (Float, default 1).
3. Nodes:
- `Fresnel Effect` node (Normal = world normal, View Dir = world view direction — both defaults; Power = `RimPower`).
- Multiply Fresnel × `RimColor` × `RimStrength` → **Emission**.
- `BaseColor` → **Base Color**.
4. Save, create material, try RimPower 1.5 (soft) vs 6 (thin outline).
**Explanation:** Fresnel = `pow(1 - saturate(dot(N, V)), Power)`. Surfaces facing you give ~0; grazing angles give ~1. That's the hand-written formula we'll reuse in Part 4.
**Variation – hologram:** set Surface Type to **Transparent**, Blend Additive, feed Fresnel into Alpha, and add scrolling lines: `Position (World).y` × 40 + `Time` → `Fraction` → `Step(0.5)` × Fresnel.
---
## Part 4 – Hand-Written HLSL Shaders for URP
URP shaders must use **HLSLPROGRAM** (not CGPROGRAM), include URP's `Core.hlsl`, and put material properties in `CBUFFER_START(UnityPerMaterial)` for SRP Batcher compatibility.
### 4a. Unlit (textured + tint + fresnel)
Create *Assets/Shaders/URPUnlitRim.shader*:
```hlsl
Shader "Tutorial/URP Unlit Rim"
{
Properties
{
[MainTexture] _BaseMap ("Base Map", 2D) = "white" {}
[MainColor] _BaseColor ("Base Color", Color) = (1,1,1,1)
[HDR] _RimColor ("Rim Color", Color) = (0,1,1,1)
_RimPower ("Rim Power", Range(0.5, 8)) = 3
}
SubShader
{
Tags { "RenderType"="Opaque" "RenderPipeline"="UniversalPipeline" "Queue"="Geometry" }
Pass
{
Name "Unlit"
Tags { "LightMode"="UniversalForward" }
HLSLPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma multi_compile_instancing
#pragma multi_compile_fog
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
TEXTURE2D(_BaseMap);
SAMPLER(sampler_BaseMap);
// Everything from Properties (except textures) goes here => SRP Batcher compatible.
CBUFFER_START(UnityPerMaterial)
float4 _BaseMap_ST;
half4 _BaseColor;
half4 _RimColor;
half _RimPower;
CBUFFER_END
struct Attributes
{
float4 positionOS : POSITION;
float3 normalOS : NORMAL;
float2 uv : TEXCOORD0;
UNITY_VERTEX_INPUT_INSTANCE_ID
};
struct Varyings
{
float4 positionHCS : SV_POSITION;
float2 uv : TEXCOORD0;
float3 normalWS : TEXCOORD1;
float3 positionWS : TEXCOORD2;
half fogFactor : TEXCOORD3;
UNITY_VERTEX_INPUT_INSTANCE_ID
UNITY_VERTEX_OUTPUT_STEREO
};
Varyings vert (Attributes IN)
{
Varyings OUT;
UNITY_SETUP_INSTANCE_ID(IN);
UNITY_TRANSFER_INSTANCE_ID(IN, OUT);
UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(OUT);
VertexPositionInputs pos = GetVertexPositionInputs(IN.positionOS.xyz);
VertexNormalInputs nrm = GetVertexNormalInputs(IN.normalOS);
OUT.positionHCS = pos.positionCS;
OUT.positionWS = pos.positionWS;
OUT.normalWS = nrm.normalWS;
OUT.uv = TRANSFORM_TEX(IN.uv, _BaseMap);
OUT.fogFactor = ComputeFogFactor(pos.positionCS.z);
return OUT;
}
half4 frag (Varyings IN) : SV_Target
{
UNITY_SETUP_INSTANCE_ID(IN);
half4 col = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, IN.uv) * _BaseColor;
float3 N = normalize(IN.normalWS);
float3 V = GetWorldSpaceNormalizeViewDir(IN.positionWS);
half rim = pow(1.0 - saturate(dot(N, V)), _RimPower);
col.rgb += _RimColor.rgb * rim;
col.rgb = MixFog(col.rgb, IN.fogFactor);
return col;
}
ENDHLSL
}
}
}
```
**Key points:**
- `"RenderPipeline"="UniversalPipeline"` makes URP pick this SubShader.
- `LightMode = UniversalForward` is the main forward pass. (In Forward+ / Deferred URP still renders unlit through the forward path.)
- `GetVertexPositionInputs` / `GetVertexNormalInputs` are URP helpers that compute world/view/clip positions in one go.
- Check SRP Batcher compatibility: select the shader asset, the Inspector shows **SRP Batcher: compatible**.
### 4b. Simple Lit (Main + Additional lights + Shadows, Forward+ aware)
Create *Assets/Shaders/URPSimpleLit.shader*. This hand-rolls a Lambert + Blinn-Phong model using URP's light API. It includes a ShadowCaster and DepthOnly pass so the object casts shadows and works with depth prepass/SSAO.
```hlsl
Shader "Tutorial/URP Simple Lit"
{
Properties
{
[MainTexture] _BaseMap ("Base Map", 2D) = "white" {}
[MainColor] _BaseColor ("Base Color", Color) = (1,1,1,1)
_Smoothness ("Smoothness (spec power)", Range(1, 256)) = 32
_SpecColor ("Specular Color", Color) = (0.3,0.3,0.3,1)
}
SubShader
{
Tags { "RenderType"="Opaque" "RenderPipeline"="UniversalPipeline" "Queue"="Geometry" }
HLSLINCLUDE
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
CBUFFER_START(UnityPerMaterial)
float4 _BaseMap_ST;
half4 _BaseColor;
half _Smoothness;
half4 _SpecColor;
CBUFFER_END
ENDHLSL
Pass
{
Name "ForwardLit"
Tags { "LightMode"="UniversalForward" }
HLSLPROGRAM
#pragma vertex vert
#pragma fragment frag
// URP lighting keywords (main light shadows, additional lights, Forward+ clustering, soft shadows)
#pragma multi_compile _ _MAIN_LIGHT_SHADOWS _MAIN_LIGHT_SHADOWS_CASCADE _MAIN_LIGHT_SHADOWS_SCREEN
#pragma multi_compile _ _ADDITIONAL_LIGHTS_VERTEX _ADDITIONAL_LIGHTS
#pragma multi_compile_fragment _ _ADDITIONAL_LIGHT_SHADOWS
#pragma multi_compile_fragment _ _SHADOWS_SOFT _SHADOWS_SOFT_LOW _SHADOWS_SOFT_MEDIUM _SHADOWS_SOFT_HIGH
#pragma multi_compile _ _CLUSTER_LIGHT_LOOP
#pragma multi_compile_fog
#pragma multi_compile_instancing
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"
TEXTURE2D(_BaseMap); SAMPLER(sampler_BaseMap);
struct Attributes
{
float4 positionOS : POSITION;
float3 normalOS : NORMAL;
float2 uv : TEXCOORD0;
UNITY_VERTEX_INPUT_INSTANCE_ID
};
struct Varyings
{
float4 positionCS : SV_POSITION;
float2 uv : TEXCOORD0;
float3 normalWS : TEXCOORD1;
float3 positionWS : TEXCOORD2;
half fogFactor : TEXCOORD3;
UNITY_VERTEX_INPUT_INSTANCE_ID
};
Varyings vert (Attributes IN)
{
Varyings OUT = (Varyings)0;
UNITY_SETUP_INSTANCE_ID(IN);
UNITY_TRANSFER_INSTANCE_ID(IN, OUT);
VertexPositionInputs p = GetVertexPositionInputs(IN.positionOS.xyz);
OUT.positionCS = p.positionCS;
OUT.positionWS = p.positionWS;
OUT.normalWS = TransformObjectToWorldNormal(IN.normalOS);
OUT.uv = TRANSFORM_TEX(IN.uv, _BaseMap);
OUT.fogFactor = ComputeFogFactor(p.positionCS.z);
return OUT;
}
half3 Shade(Light light, float3 N, float3 V, half3 albedo)
{
half atten = light.distanceAttenuation * light.shadowAttenuation;
half NdotL = saturate(dot(N, light.direction));
half3 H = normalize(light.direction + V);
half spec = pow(saturate(dot(N, H)), _Smoothness) * NdotL;
return (albedo * NdotL + _SpecColor.rgb * spec) * light.color * atten;
}
half4 frag (Varyings IN) : SV_Target
{
UNITY_SETUP_INSTANCE_ID(IN);
half3 albedo = (SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, IN.uv) * _BaseColor).rgb;
float3 N = normalize(IN.normalWS);
float3 V = GetWorldSpaceNormalizeViewDir(IN.positionWS);
// Main light with shadows
float4 shadowCoord = TransformWorldToShadowCoord(IN.positionWS);
Light mainLight = GetMainLight(shadowCoord);
half3 color = Shade(mainLight, N, V, albedo);
// Ambient from spherical harmonics (light probes / skybox)
color += albedo * SampleSH(N);
// Additional lights. LIGHT_LOOP_BEGIN handles both classic and Forward+ clustered loops.
// Forward+ needs InputData for the cluster lookup.
InputData inputData = (InputData)0;
inputData.positionWS = IN.positionWS;
inputData.normalWS = N;
inputData.viewDirectionWS = V;
inputData.normalizedScreenSpaceUV = GetNormalizedScreenSpaceUV(IN.positionCS);
// Forward+/Deferred+: non-main DIRECTIONAL lights are not in the clustered loop.
#if USE_CLUSTER_LIGHT_LOOP
UNITY_LOOP
for (uint lightIndex = 0; lightIndex < min(URP_FP_DIRECTIONAL_LIGHTS_COUNT, MAX_VISIBLE_LIGHTS); lightIndex++)
{
Light dl = GetAdditionalLight(lightIndex, IN.positionWS, half4(1,1,1,1));
color += Shade(dl, N, V, albedo);
}
#endif
uint lightCount = GetAdditionalLightsCount();
LIGHT_LOOP_BEGIN(lightCount)
Light l = GetAdditionalLight(lightIndex, IN.positionWS, half4(1,1,1,1));
color += Shade(l, N, V, albedo);
LIGHT_LOOP_END
color = MixFog(color, IN.fogFactor);
return half4(color, 1);
}
ENDHLSL
}
// Reuse URP's built-in passes so we cast shadows and write depth.
UsePass "Universal Render Pipeline/Lit/ShadowCaster"
UsePass "Universal Render Pipeline/Lit/DepthOnly"
UsePass "Universal Render Pipeline/Lit/DepthNormals"
}
}
```
**Explanation:**
- `Lighting.hlsl` provides `Light`, `GetMainLight`, `GetAdditionalLight`, `GetAdditionalLightsCount` and the `LIGHT_LOOP_BEGIN/END` macros that work in both Forward and Forward+ (the macro reads `inputData` for the cluster lookup, so the variable must be named `inputData`).
- The Forward+ cluster keyword was renamed in recent URP versions; Unity 6.1+ uses `_CLUSTER_LIGHT_LOOP` (it covers Forward+ and Deferred+); `_FORWARD_PLUS` from 6000.0 is deprecated, so don't use it in 6000.5. In the clustered path, extra directional lights are looped separately (the `URP_FP_DIRECTIONAL_LIGHTS_COUNT` loop above), exactly as in Unity's *Render additional lights in a shader* manual page.
- `UsePass` borrows the Lit shader's ShadowCaster/DepthOnly/DepthNormals passes. Those read `_BaseMap`/`_BaseColor` from **their own** CBUFFER layout, which differs from ours, so this breaks SRP Batcher compatibility. For production, copy URP's `ShadowCasterPass.hlsl` pattern into your own pass instead. For learning, `UsePass` keeps the file short.
- If you just want "PBR with a twist", it's usually better to write a Shader Graph, or to call URP's `UniversalFragmentPBR(inputData, surfaceData)` from your own HLSL, rather than reimplementing BRDFs.
---
## Part 5 – A Fullscreen Effect with a ScriptableRendererFeature + Render Graph
**Goal:** a tinted, vignette + grayscale post effect applied after post-processing, written the Unity 6 way.
There are two routes:
1. **No code:** Add the built-in **Full Screen Pass Renderer Feature** to your Renderer asset and assign a material made from a **Fullscreen Shader Graph** (*Create > Shader Graph > URP > Fullscreen Shader Graph*, use the `URP Sample Buffer` node set to *BlitSource*). Great for most effects.
2. **Code:** a custom `ScriptableRendererFeature` that records a Render Graph pass. That's what we build here, since it teaches the Render Graph model.
### 5a. The fullscreen shader
*Assets/Shaders/FullscreenTint.shader*:
```hlsl
Shader "Tutorial/Fullscreen Tint Vignette"
{
Properties
{
_Tint ("Tint", Color) = (1, 0.9, 0.8, 1)
_Grayscale ("Grayscale", Range(0,1)) = 0.5
_Vignette ("Vignette Strength", Range(0,2)) = 0.8
}
SubShader
{
Tags { "RenderType"="Opaque" "RenderPipeline"="UniversalPipeline" }
ZWrite Off ZTest Always Cull Off Blend Off
Pass
{
Name "TintVignette"
HLSLPROGRAM
#pragma vertex Vert // Provided by Blit.hlsl: fullscreen triangle, sets texcoord
#pragma fragment Frag
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
// Declares _BlitTexture, _BlitScaleBias, Attributes/Varyings, and Vert()
#include "Packages/com.unity.render-pipelines.core/Runtime/Utilities/Blit.hlsl"
half4 _Tint;
half _Grayscale;
half _Vignette;
half4 Frag (Varyings input) : SV_Target
{
UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(input);
float2 uv = input.texcoord;
half4 col = SAMPLE_TEXTURE2D_X(_BlitTexture, sampler_LinearClamp, uv);
half lum = dot(col.rgb, half3(0.2126, 0.7152, 0.0722));
col.rgb = lerp(col.rgb, lum.xxx, _Grayscale) * _Tint.rgb;
float2 d = uv - 0.5;
half vig = saturate(1.0 - dot(d, d) * _Vignette * 2.0);
col.rgb *= vig;
return col;
}
ENDHLSL
}
}
}
```
Create a material from it: `M_FullscreenTint`.
### 5b. The Renderer Feature (Render Graph)
*Assets/Scripts/Rendering/TintVignetteFeature.cs*:
```csharp
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.RenderGraphModule;
using UnityEngine.Rendering.RenderGraphModule.Util;
using UnityEngine.Rendering.Universal;
public class TintVignetteFeature : ScriptableRendererFeature
{
[System.Serializable]
public class Settings
{
public Material material;
public RenderPassEvent passEvent = RenderPassEvent.AfterRenderingPostProcessing;
[Range(0, 1)] public float grayscale = 0.5f;
}
public Settings settings = new();
TintVignettePass pass;
public override void Create()
{
pass = new TintVignettePass { renderPassEvent = settings.passEvent };
}
public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData)
{
if (settings.material == null) return;
// Skip preview / reflection cameras.
var camType = renderingData.cameraData.cameraType;
if (camType == CameraType.Preview || camType == CameraType.Reflection) return;
pass.Setup(settings);
renderer.EnqueuePass(pass);
}
class TintVignettePass : ScriptableRenderPass
{
static readonly int GrayscaleId = Shader.PropertyToID("_Grayscale");
Settings s;
public TintVignettePass()
{
// We read the camera color and write a new texture, so we need an
// intermediate texture rather than rendering straight to the backbuffer.
requiresIntermediateTexture = true;
}
public void Setup(Settings settings) => s = settings;
public override void RecordRenderGraph(RenderGraph renderGraph, ContextContainer frameData)
{
UniversalResourceData resourceData = frameData.Get<UniversalResourceData>();
// Can't sample the backbuffer as a texture.
if (resourceData.isActiveTargetBackBuffer)
{
Debug.LogWarning("TintVignette skipped: camera is rendering to the backbuffer.");
return;
}
s.material.SetFloat(GrayscaleId, s.grayscale);
TextureHandle source = resourceData.activeColorTexture;
// Describe a destination with the same size/format as the camera color.
TextureDesc desc = renderGraph.GetTextureDesc(source);
desc.name = "_TintVignetteColor";
desc.clearBuffer = false;
TextureHandle destination = renderGraph.CreateTexture(desc);
// AddBlitPass records a raster pass that draws a fullscreen triangle with our
// material, binding 'source' as _BlitTexture.
var blit = new RenderGraphUtils.BlitMaterialParameters(source, destination, s.material, 0);
renderGraph.AddBlitPass(blit, passName: "Tint Vignette");
// Swap: later passes (and the final blit to screen) now read our result.
// No copy-back needed.
resourceData.cameraColor = destination;
}
}
}
```
**Explanation – the Render Graph model:**
- You don't execute commands directly. `RecordRenderGraph` *declares* passes and the resources they read/write. Render Graph then culls unused passes, allocates/aliases textures from a pool, and merges passes (native render passes on tile GPUs).
- `ContextContainer frameData` holds frame data objects: `UniversalResourceData` (camera color, depth, GBuffer, motion vectors handles), `UniversalCameraData`, `UniversalLightData`, etc.
- `TextureHandle`s are only valid inside the current graph recording. Never cache them across frames; cache `RTHandle`s and import them with `renderGraph.ImportTexture` if you need persistent textures (e.g., history buffers).
- Setting `resourceData.cameraColor = destination` is the idiomatic "ping-pong" in URP 17: avoids a second copy.
### 5c. Writing the pass manually (when AddBlitPass isn't enough)
If you need multiple draws, custom MRTs, or extra inputs (e.g. the depth texture), build a raster pass yourself. Replace the `AddBlitPass` call with:
```csharp
class PassData
{
public TextureHandle source;
public Material material;
}
// ...inside RecordRenderGraph, after creating 'destination':
using (var builder = renderGraph.AddRasterRenderPass<PassData>("Tint Vignette (manual)", out var passData))
{
passData.source = source;
passData.material = s.material;
builder.UseTexture(source, AccessFlags.Read); // declare read
builder.SetRenderAttachment(destination, 0, AccessFlags.Write); // declare color target 0
builder.SetRenderFunc((PassData data, RasterGraphContext ctx) =>
{
// Binds data.source as _BlitTexture and draws a fullscreen triangle.
Blitter.BlitTexture(ctx.cmd, data.source, new Vector4(1, 1, 0, 0), data.material, 0);
});
}
resourceData.cameraColor = destination;
```
Rules: the render func must only use what's in `PassData` (it may run later, and lambdas must not capture outside state); every texture you touch must be declared with `UseTexture` / `SetRenderAttachment`, or Render Graph will error or cull the pass.
### 5d. Hook it up
1. Select `PC_Renderer` (the Universal Renderer Data asset).
2. **Add Renderer Feature > Tint Vignette Feature**. Assign `M_FullscreenTint`.
3. Enter Play mode or look at the Game view: grayscale tint + vignette.
4. Open **Window > Analysis > Render Graph Viewer** to see your "Tint Vignette" pass, its reads/writes, and whether it merged with neighbours. This is the #1 debugging tool for Render Graph work; the **Frame Debugger** is still useful to see actual draw calls.
### 5e. Making it Volume-driven (optional)
To let artists blend the effect per area, create a `VolumeComponent`:
```csharp
using UnityEngine.Rendering;
[System.Serializable, VolumeComponentMenu("Tutorial/Tint Vignette")]
public class TintVignetteVolume : VolumeComponent
{
public ClampedFloatParameter grayscale = new(0f, 0f, 1f);
public bool IsActive() => grayscale.value > 0f;
}
```
Then in `RecordRenderGraph` read it:
```csharp
var vol = VolumeManager.instance.stack.GetComponent<TintVignetteVolume>();
if (vol == null || !vol.IsActive()) return;
s.material.SetFloat(GrayscaleId, vol.grayscale.value);
```
Add a Global Volume, Add Override > Tutorial > Tint Vignette.
---
## Troubleshooting
| Symptom | Fix |
|---|---|
| Pink material | Shader missing `"RenderPipeline"="UniversalPipeline"` or compile error (check shader Inspector). |
| "SRP Batcher: not compatible" | A property is outside `UnityPerMaterial` CBUFFER, or passes have different CBUFFER layouts. |
| Feature does nothing | Material not assigned, effect running before post-process overwrote it (check `renderPassEvent`), or camera writes to backbuffer – keep `requiresIntermediateTexture = true`. |
| Errors about `Execute` / `OnCameraSetup` obsolete | Old non-Render-Graph code. Port to `RecordRenderGraph`. |
| Additional lights missing in Forward+ | Missing `_CLUSTER_LIGHT_LOOP` keyword or `inputData` not set before `LIGHT_LOOP_BEGIN`. |
## Further Reading (Unity 6 docs)
- URP manual: *Render graph system in URP* and *Write a render pass using the render graph system*.
- URP manual: *Writing custom shaders* (URP unlit basic shader examples).
- Shader Graph manual: *Fullscreen Master Stack*, *Fresnel Effect node*, *Alpha Clipping*.
- Package samples: *Package Manager > Universal RP > Samples > URP RenderGraph Samples* – official working examples of every pattern above.
## See also
- [[Lighting and Baking]] — GPU lightmapper, lightmaps, APV / Light Probes, reflection probes, Quest lighting budgets