5

I would like to set the saturation of an entire color channel in my main camera. The closest option that I've found was the Hue vs. Sat(uration) Grading Curve. In the background of the scene is a palm tree that is colored teal. I want the green level of the tree to still show. Same with the top of the grass in the foreground, It's closer to yellow than green, but I'd still want to see the little bit of green value that it has.

I have been searching the Unity documentation and the asset store for a possible 3rd party shader for weeks, but have come up empty handed. My current result is the best I could come up with, any help would be greatly appreciated. Thank you Current Result

Grading Curve

SOLVED -by check-marked answer. Just wanted to share what the results look like for anyone in the future who stumbles across this issue. Compare the above screenshot, where the palm tree in the background and the grass tops in the foreground are just black and white, to the after screenshot below. Full control in the scene of RGB saturation! GreennBlacknWhite RednBlacknWhite

Ruzihm
  • 19,749
  • 5
  • 36
  • 48
Rickest Rick
  • 1,519
  • 1
  • 15
  • 28

2 Answers2

3

Examples using this method: example using URP


Below is a postprocessing shader intended to let you set the saturation of each color channel.

It first takes the original pixel color, gets the hue, saturation, and luminance. That color is taken to its most saturated, neutral-luminance version. The rgb of that is then multiplied by the desaturation factor to compute the rgb of the new hue. The magnitude of that rgb is multiplied by the original saturation to get the new saturation. This new hue and saturation is fed back in with the original luminance to compute the new color.

Shader "Custom/ChannelSaturation" {
    Properties{
        _MainTex("Base", 2D) = "white" {}
        _rSat("Red Saturation", Range(0, 1)) = 1
        _gSat("Green Saturation", Range(0, 1)) = 1
        _bSat("Blue Saturation", Range(0, 1)) = 1
    }
        SubShader{
            Pass {
                CGPROGRAM
                #pragma vertex vert_img
                #pragma fragment frag
                #include "UnityCG.cginc"

                uniform sampler2D _MainTex;
                float _rSat;
                float _gSat;
                float _bSat;

                /*
                  source: modified version of https://www.shadertoy.com/view/MsKGRW
                  written @ https://gist.github.com/hiroakioishi/
                            c4eda57c29ae7b2912c4809087d5ffd0
                */
                float3 rgb2hsl(float3 c) {
                    float epsilon = 0.00000001;
                    float cmin = min( c.r, min( c.g, c.b ) );
                    float cmax = max( c.r, max( c.g, c.b ) );
                    float cd   = cmax - cmin;
                    float3 hsl = float3(0.0, 0.0, 0.0);
                    hsl.z = (cmax + cmin) / 2.0;
                    hsl.y = lerp(cd / (cmax + cmin + epsilon), 
                            cd / (epsilon + 2.0 - (cmax + cmin)), 
                            step(0.5, hsl.z));

                    float3 a = float3(1.0 - step(epsilon, abs(cmax - c)));
                    a = lerp(float3(a.x, 0.0, a.z), a, step(0.5, 2.0 - a.x - a.y));
                    a = lerp(float3(a.x, a.y, 0.0), a, step(0.5, 2.0 - a.x - a.z));
                    a = lerp(float3(a.x, a.y, 0.0), a, step(0.5, 2.0 - a.y - a.z));
    
                    hsl.x = dot( float3(0.0, 2.0, 4.0) + ((c.gbr - c.brg) 
                            / (epsilon + cd)), a );
                    hsl.x = (hsl.x + (1.0 - step(0.0, hsl.x) ) * 6.0 ) / 6.0;
                    return hsl;
                }

                /*
                  source: modified version of
                          https://stackoverflow.com/a/42261473/1092820
                */
                float3 hsl2rgb(float3 c) {
                    float3 rgb = clamp(abs(fmod(c.x * 6.0 + float3(0.0, 4.0, 2.0),
                            6.0) - 3.0) - 1.0, 0.0, 1.0);
                    return c.z + c.y * (rgb - 0.5) * (1.0 - abs(2.0 * c.z - 1.0));
                }

                float4 frag(v2f_img i) : COLOR {
                    float3 sat = float3(_rSat, _gSat, _bSat);

                    float4 c = tex2D(_MainTex, i.uv);
                    float3 hslOrig = rgb2hsl(c.rgb);

                    float3 rgbFullSat = hsl2rgb(float3(hslOrig.x, 1, .5));
                    float3 diminishedrgb = rgbFullSat * sat;

                    float diminishedHue = rgb2hsl(diminishedrgb).x;

                    float diminishedSat = hslOrig.y * length(diminishedrgb);
                    float3 mix = float3(diminishedHue, diminishedSat, hslOrig.z);
                    float3 newc = hsl2rgb(mix);

                    float4 result = c;
                    result.rgb = newc;

                    return result;
                }
                ENDCG
            }
        }
}

If you're using URP (Universal Rendering Pipeline), which is recommended, you can create a new forward renderer pipeline asset, assign the shader to that asset, and configure it appropriately. Further information including diagrams can be found in the official unity tutorial for custom render passes with URP.


If you aren't using URP, you have other options. You could attach it to specific materials, or use the below script from Wikibooks to the camera's gameobject to apply a material using the above shader as a postprocessing effect to the camera:

using System;
using UnityEngine;

[RequireComponent(typeof(Camera))]
[ExecuteInEditMode]

public class PostProcessingEffectScript : MonoBehaviour {

   public Material material;
   
   void OnEnable() 
   {
      if (null == material || null == material.shader || 
         !material.shader.isSupported)
      {
         enabled = false;
      } 
   }

   void OnRenderImage(RenderTexture source, RenderTexture destination)
   {
      Graphics.Blit(source, destination, material);
   }
}

If you use the postprocessing effect, you will want to render the things you want to exclude from the effect with a different camera, then put everything together. However, this is a bit out of scope for this answer.

Ruzihm
  • 19,749
  • 5
  • 36
  • 48
  • 1
    Thats it! Your last round of revisions did it! I had copied your code before you took it down earlier and was making some changes myself, but didn't have your re-write to the frag method. Thanks for the work, will send the bounty when the cooldown is up tomorrow. – Rickest Rick Dec 08 '21 at 02:47
  • 1
    Wow, nice step through of the Rendering pipeline.. too bad you didn't post that 30 minutes earlier! :) – Rickest Rick Dec 08 '21 at 02:50
-1

My best guess would be to use a custom shader or camera FX that would gives you control over each channel.

Hope that helped ;)

Thomas Finet
  • 222
  • 1
  • 6
  • Hi Thomas, do you have a specific shader in mind? I've tried to look through the asset store, but haven't yet found what I'm looking for. I've started a 50pt bounty on this question for a specific answer, if you can find what I'm looking for add it in your answer! – Rickest Rick Dec 07 '21 at 17:31