Monochrome Full-Screen Shader for Unity URP

A URP full-screen monochrome shader for Unity 6: duotone grey or sepia, an optional 1-bit threshold, and a keep-one-colour mode. Applied with the Full Screen Pass Renderer Feature.

Also called: grayscale effect, black and white shader, sepia filter, noir effect, selective color.

Full-screen effects URP, Unity 6000.0+ beginner MIT licensed

Use it for: Noir and horror looks, flashbacks, a death or pause screen, a one-colour accent like a red coat in a grey world, or a 1-bit retro style.

Inspector Try the properties
Shader Scripts For Unity/Full Screen/Monochrome
Monochrome Drag to rotate

The preview runs a WebGL copy of the effect over a small test scene. In Unity it applies to everything your camera renders.

What it does

  • Duotone: maps brightness between a dark and a light colour, so grey, sepia or any two-tone look is two colour picks
  • Contrast control applied in gamma space, so the middle of the slider looks like middle grey
  • 1-bit mode: a hard threshold to pure dark and light
  • Keep One Color: leaves saturated pixels near a chosen hue in colour
  • Intensity slider to fade the effect in and out from a script
  • Runs as one full-screen pass with URP's built-in Full Screen Pass Renderer Feature; no C# needed

How it works

The Full Screen Pass Renderer Feature copies the camera's image into _BlitTexture and draws this shader over the whole screen. For each pixel the shader takes its luminance, converts it to gamma space, applies the contrast, and uses the result to blend between the dark and light colours.

In Keep One Color mode the pixel's hue is compared with the hue of the colour to keep, around the colour wheel. Pixels close to it, and saturated enough to have a clear hue, are blended back towards their original colour.

Setup

  1. Save the file as Monochrome.shader under Assets and create a material that uses Scripts For Unity > Full Screen > Monochrome.
  2. Select your URP Renderer asset (the Universal Renderer Data your URP asset uses) and click Add Renderer Feature > Full Screen Pass Renderer Feature.
  3. Assign the material to Pass Material, set Injection Point to After Rendering Post Processing, and keep Requirements on Color.
  4. To fade it from a script, set the material's _Intensity: monochromeMaterial.SetFloat("_Intensity", value);

Common mistakes

  • Nothing changes: the feature was added to a different Renderer asset than the one your camera uses. Check the URP asset's renderer list and the camera's Renderer setting.
  • Only need plain grey? URP's own Color Adjustments override with Saturation at -100 does that without a renderer feature. Use this shader for tints, 1-bit or keep-one-colour.
  • UI turns grey too: Screen Space - Overlay canvases draw after the effect and stay in colour; Screen Space - Camera and World Space canvases are part of the image and go grey with it.

Questions

Can I fade it in and out?
Yes. Set the material's _Intensity from a script, from 0 (off) to 1 (full).
How do I apply it to only one camera?
A renderer feature applies to every camera that uses that Renderer. Add a second Renderer to your URP asset's renderer list, put the feature only on it, and pick that Renderer on the camera.
Is this the same as Saturation at -100?
For plain grey, yes: URP's Color Adjustments override does that without a renderer feature. This shader adds a two-colour tint, a 1-bit threshold and keep-one-colour.

Source

Monochrome.shader · 82 lines · ShaderLab and HLSL for the Universal Render Pipeline 17 (Unity 6)

Monochrome.shader
cpp
// Monochrome: a full-screen duotone. Grey, sepia, 1-bit or "keep one
// colour", applied with URP's Full Screen Pass Renderer Feature.
// URP 17 (Unity 6). Scripts For Unity, MIT licensed.
Shader "Scripts For Unity/Full Screen/Monochrome"
{
    Properties
    {
        _Intensity ("Intensity", Range(0, 1)) = 1
        _DarkColor ("Dark Color", Color) = (0.06, 0.06, 0.08, 1)
        _LightColor ("Light Color", Color) = (0.96, 0.94, 0.88, 1)
        _Contrast ("Contrast", Range(0, 3)) = 1.15
        [Toggle(_ONE_BIT)] _OneBit ("1-Bit (Hard Threshold)", Float) = 0
        _Threshold ("Threshold", Range(0, 1)) = 0.5
        [Toggle(_KEEP_COLOR)] _KeepColorOn ("Keep One Color", Float) = 0
        _KeepColor ("Color To Keep", Color) = (0.85, 0.12, 0.12, 1)
        _KeepRange ("Keep Range", Range(0, 0.5)) = 0.06
    }

    SubShader
    {
        Tags { "RenderType" = "Opaque" "RenderPipeline" = "UniversalPipeline" }
        ZWrite Off
        ZTest Always
        Cull Off

        Pass
        {
            Name "Monochrome"

            HLSLPROGRAM
            #pragma vertex Vert
            #pragma fragment Frag
            #pragma shader_feature_local_fragment _ONE_BIT
            #pragma shader_feature_local_fragment _KEEP_COLOR
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
            #include "Packages/com.unity.render-pipelines.core/ShaderLibrary/Color.hlsl"
            // Blit.hlsl provides Vert, Varyings, _BlitTexture and the global samplers
            #include "Packages/com.unity.render-pipelines.core/Runtime/Utilities/Blit.hlsl"

            CBUFFER_START(UnityPerMaterial)
                float _Intensity;
                half4 _DarkColor;
                half4 _LightColor;
                float _Contrast;
                float _OneBit;
                float _Threshold;
                float _KeepColorOn;
                half4 _KeepColor;
                float _KeepRange;
            CBUFFER_END

            half4 Frag(Varyings input) : SV_Target
            {
                UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(input);
                half4 color = SAMPLE_TEXTURE2D_X(_BlitTexture, sampler_LinearClamp, input.texcoord);

                // Brightness in gamma space, so the middle of every slider looks like middle grey
                float luma = LinearToSRGB(saturate(Luminance(color.rgb)).xxx).x;
                luma = saturate((luma - 0.5) * _Contrast + 0.5);
            #if defined(_ONE_BIT)
                luma = step(_Threshold, luma);
            #endif
                float3 mono = lerp(_DarkColor.rgb, _LightColor.rgb, luma);

                float amount = _Intensity;
            #if defined(_KEEP_COLOR)
                // Leave saturated pixels near the chosen hue in colour
                float3 hsv = RgbToHsv(saturate(color.rgb));
                float keepHue = RgbToHsv(saturate(_KeepColor.rgb)).x;
                float hueDistance = abs(hsv.x - keepHue);
                hueDistance = min(hueDistance, 1.0 - hueDistance);
                float keep = (1.0 - smoothstep(_KeepRange, _KeepRange + 0.04, hueDistance)) * smoothstep(0.15, 0.35, hsv.y);
                amount *= 1.0 - keep;
            #endif

                return half4(lerp(color.rgb, mono, amount), color.a);
            }
            ENDHLSL
        }
    }
}

More shaders