US3S10L5——亮度、饱和度、对比度

改变屏幕的亮度、饱和度、对比度

亮度、饱和度和对比度的计算规则是在视觉科学、色彩理论和图像处理技术的基础上逐步发展起来的,
这些规则不是由某个人发明的,而是通过多年的研究和实践得出的,
这些计算方法被广泛应用于各种图像处理工具和计算机图形学中,用于实现图像的调整和优化

颜色亮度的基本原理

想要改变图像颜色的亮度,只需要对图像的每个像素进行加法(减法-加负数)或乘法(除法-乘小数)运算即可实现

  • 增加亮度就是增加像素的 RGB 值
  • 减小亮度则是减少像素的 RGB 值

也就是说我们只需要在 Shader 当中加入一个控制亮度的 float​ 类型的变量
然后用颜色的 RGB 乘以该变量或者加上该变量即可,一般我们会采用乘法的形式

即:最终颜色 = 原始颜色 * 亮度变量

  • 亮度变量 > 1 时,图像变亮
  • 亮度变量 < 1 时,图像变暗
  • 亮度变量 = 1 时,图像不变

颜色饱和度的基本原理

色彩的饱和度(saturation)指色彩的鲜艳程度,也称作纯度。在hue-saturation-value(HSV)色彩模型下,饱和度是色彩的3个属性之一,另外两个属性为色相(hue)和明度(value);在此模型下色相的取值范围为0°到360°,饱和度和明度取值范围为 0 到 100%。在色彩学中,原色饱和度最高,随着饱和度降低,色彩变得暗淡直至成为无彩色,即失去色相的色彩。

饱和度(色彩概念)_百度百科

想要改变图像颜色的饱和度,只需要对图像的 每个像素的颜色值 相对于 灰度颜色 进行插值来实现

灰度颜色(Grayscale Color)是指只含有亮度信息而没有色相和饱和度的颜色,即图像中的每个像素只有从黑到白的不同灰度级别。
在最简单的形式中,灰度图像的每个像素可以由一个值表示,这个值通常是从 0(黑色)到 255(白色)之间的整数。

利用图像颜色 RGB 计算一个平均值,即可得到一个灰度值,因为人眼对不同颜色敏感度不同,因此会对三个颜色分别乘以不同的权数,用加权平均法来计算灰度值

基本原理为以下三步:

  1. 计算灰度值(亮度)

    利用图像颜色RGB计算一个平均值,得到一个灰度值
    但是由于人眼对不同颜色的敏感度不同,所以在计算平均值时不会直接使用算数平均:R+G+B3\frac{R+G+B}{3}

    在图形学中我们一般使用加权平均法来计算灰度值,
    所谓加权平均法就是通过对不同数据分配不同权重,计算出更符合实际情况的平均值
    常用的权重基于 Rec. 709 标准(高清电视和许多数字图像格式中常用的标准)

    • R 红色通道的权重:0.2126
    • G 绿色通道的权重:0.7152
    • B 蓝色通道的权重:0.0722

    这些权重反映了人眼对绿色最敏感,对蓝色最不敏感
    加权平均法公式:灰度值(亮度)L=0.2126×R+0.7152×G+0.0722×BL = 0.2126 \times R + 0.7152 \times G + 0.0722 \times B

  2. 生成灰度颜色

    利用第一步中计算出来的灰度值,生成一个灰度颜色:灰度颜色 = (L, L, L)

  3. 插值计算

    使用插值函数 lerp​,在灰度颜色和原始颜色之间进行插值运算,
    插值系数就是用于控制饱和度的 float​ 类型的变量,公式如下:

    最终颜色 = lerp(灰度颜色, 原始颜色, 饱和度变量)

    lerp​ 计算原理:最终颜色=灰度颜色+(原始颜色灰度颜色)×饱和度变量最终颜色 = 灰度颜色 + (原始颜色 − 灰度颜色) \times 饱和度变量

    • 饱和度变量 = 0 时,结果为灰度颜色
    • 饱和度变量 = 1 时,保持原始颜色不变
    • 饱和度变量 = 0~1 之间时,灰度颜色和原始颜色的混合
    • 饱和度变量 > 1 时,颜色的RGB值超出原始范围,从而使颜色看起来更饱和

颜色对比度的基本原理

对比度指的是一幅图像中明暗区域最亮的白和最暗的黑之间不同亮度层级的测量,
差异范围越大代表对比越大,差异范围越小代表对比越小,
好的对比率 120:1 就可容易地显示生动、丰富的色彩,
当对比率高达 300:1 时,便可支持各阶的颜色。

对比度_百度百科

想要改变图像颜色的对比度,只需要对图像的每个像素的颜色值,相对于中性灰色进行插值来实现

中性灰色,是指在 RGB 色彩模式下,R:G:B = 1:1:1 ,
即红绿蓝三色数值相等,即为中性灰,当 R=G=B=128,被称作“绝对中性灰”

基本原理为以下两步:

  1. 声明中性灰色变量,即 RGB 都为 0.5 的颜色变量

    中性灰颜色 = (0.5,0.5,0.5)

  2. 在中性灰色和原始颜色之间进行插值运算

    最终颜色 = lerp(中性灰色, 原始颜色, 对比度变量)

    lerp​ 计算原理:最终颜色=中性灰色+(原始颜色中性灰色)×对比度变量最终颜色 = 中性灰色 + (原始颜色 − 中性灰色) \times 对比度变量

    • 对比度变量 = 0 时,此时对比度降到最低,变为中性灰色
    • 对比度变量 = 1 时,保持原始颜色不变
    • 对比度变量 = 0~1 之间时,降低对比度效果,图像的亮度差异减少,使图像颜色看起来更平淡
    • 对比度变量 > 1 时,颜色的RGB值超出原始范围,从而使颜色亮部更亮,暗部更暗,从而增加对比度

实现亮度、饱和度、对比度屏幕后期处理效果对应 Shader

  1. 新建一个 Shader,名为 BrightnessSaturationContrast​(亮度饱和度对比度),删除其中无用代码

  2. 声明变量,并进行属性映射

    • 主纹理 _MainTex​ 2D
    • 亮度 _Brightness​ Float
    • 饱和度 _Saturation​ Float
    • 对比度 _Contrast​ Float
    1
    2
    3
    4
    5
    6
    7
    Properties
    {
    _MainTex("Texture", 2D) = "white"{}
    _Brightness("Brightness", Float) = 1 // 亮度变量
    _Saturation("Saturation", Float) = 1 // 饱和度变量
    _Contrast("Contrast", Float) = 1 // 对比度变量
    }
  3. 设置深度测试、剔除、深度写入

    • ​ZTest Always​ 开启深度测试
    • ​Cull Off​ 关闭剔除
    • ​ZWrite Off​ 关闭深度写入

    这样的设置是屏幕后处理的标配,因为屏幕后处理效果相当于在场景上绘制了一个与屏幕同宽高的四边形面片
    这样做的目的是避免它 “挡住” 后面的渲染物体,比如我们在 OnRenderImage​ 前加入 [ImageEffectOpaque]​ 特性时
    透明物体会晚于该该屏幕后处理效果渲染,如果不关闭深度写入会影响后面的透明相关 Pass

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    Pass
    {
    ZTest Always
    Cull Off
    ZWrite Off

    CGPROGRAM
    // ...
    ENDCG
    }
  4. 结构体相关

    顶点、纹理坐标

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    struct v2f
    {
    float2 uv : TEXCOORD0;
    float4 vertex : SV_POSITION;
    };

    sampler2D _MainTex;
    float4 _MainTex_ST;
    half _Brightness;
    half _Saturation;
    half _Contrast;
  5. 顶点着色器

    顶点转裁剪空间,uv缩放偏移

    1
    2
    3
    4
    5
    6
    7
    v2f vert (appdata_base v)
    {
    v2f o;
    o.vertex = UnityObjectToClipPos(v.vertex); // 顶点转换到裁剪空间下
    o.uv = TRANSFORM_TEX(v.texcoord, _MainTex); // 缩放偏移UV坐标
    return o;
    }
  6. 片元着色器

    对主纹理进行采样,分别利用公式计算 亮度、饱和度、对比度,返回处理后的颜色

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    fixed4 frag (v2f i) : SV_Target
    {
    fixed4 renderTexColor = tex2D(_MainTex, i.uv);
    // 亮度计算
    fixed3 finalColor = renderTexColor.rgb * _Brightness;
    // 饱和度计算
    fixed L = 0.2126 * finalColor.r + 0.7152 * finalColor + 0.722 * finalColor.b;
    fixed3 LColor = fixed3(L, L, L);
    finalColor = lerp(LColor, finalColor, _Saturation);
    // 对比度计算
    fixed3 avgColor = fixed3(0.5, 0.5, 0.5);
    finalColor = lerp(avgColor, finalColor, _Contrast);

    return fixed4(finalColor.rgb, 1);
    }
  7. FallBack Off

    这里不需要后备 Shader,因为如果 Shader 不支持就直接不执行了

完整 Shader 代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
Shader "PostEffect/BrightnessSaturationContrast"
{
Properties
{
_MainTex("Texture", 2D) = "white"{}
_Brightness("Brightness", Float) = 1 // 亮度变量
_Saturation("Saturation", Float) = 1 // 饱和度变量
_Contrast("Contrast", Float) = 1 // 对比度变量
}
SubShader
{
Tags { "RenderType"="Opaque" }

Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag

#include "UnityCG.cginc"

struct v2f
{
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
};

sampler2D _MainTex;
float4 _MainTex_ST;
half _Brightness;
half _Saturation;
half _Contrast;

v2f vert (appdata_base v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex); // 顶点转换到裁剪空间下
o.uv = TRANSFORM_TEX(v.texcoord, _MainTex); // 缩放偏移UV坐标
return o;
}

fixed4 frag (v2f i) : SV_Target
{
fixed4 renderTexColor = tex2D(_MainTex, i.uv);
// 亮度计算
fixed3 finalColor = renderTexColor.rgb * _Brightness;
// 饱和度计算
fixed L = 0.2126 * finalColor.r + 0.7152 * finalColor + 0.722 * finalColor.b;
fixed3 LColor = fixed3(L, L, L);
finalColor = lerp(LColor, finalColor, _Saturation);
// 对比度计算
fixed3 avgColor = fixed3(0.5, 0.5, 0.5);
finalColor = lerp(avgColor, finalColor, _Contrast);

return fixed4(finalColor.rgb, 1);
}
ENDCG
}
}

Fallback Off
}

实现亮度、饱和度、对比度屏幕后期处理效果对应 C#代码

  1. 创建 C# 脚本,名为 BrightnessSaturationContrast​(亮度饱和度对比度)

  2. 继承屏幕后处理基类 PostEffect

    基类代码详见:US3S10L4——屏幕后处理基类

  3. 声明亮度饱和度对比度变量,用于控制效果变化

  4. 重写 OnRenderImage​(UpdateProperty​)方法,在其中设置材质球对应属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
using UnityEngine;

public class BrightnessSaturationContrast : PostEffect
{
[Range(0f, 5f)]
public float Brightness = 1f;
[Range(0f, 3f)]
public float Saturation = 1f;
[Range(0f, 5f)]
public float Contrast = 1f;

protected override void UpdateProperty()
{
if (PostEffectMaterial != null)
{
PostEffectMaterial.SetFloat("_Brightness", Brightness);
PostEffectMaterial.SetFloat("_Saturation", Saturation);
PostEffectMaterial.SetFloat("_Contrast", Contrast);
}
}
}

显示效果:

  • 原始图像

    image-20250104185740-6p5vgbr

  • 亮度为1.5

    image-20250104190000-icvbuqd

  • 饱和度为1.5

    image-20250104190140-sjultut

  • 对比度为1.5

    image-20250104190208-3af3x18