在屏幕效果中使用亮度, 饱和度和对比度

现在我们有了自己的屏幕效果系统并且能正常运行,我们就可以去探索在当今的游戏当中更多涉及到像素操作的一些更常用的屏幕效果。

首先,使用屏幕效果来调节游戏整体的最终颜色效果,这肯定可以给艺术家对于游戏最终的样子,有一个全局的控制。比如可以用一些颜色滑动条用来调节游戏最终渲染结果的 R,G,B 颜色强度。又或者是给整个屏幕填充大量的某个颜色这样看起来就像是一种深褐色的胶片效果。

对于这个特殊的知识点,我们将会涵盖一些可以在图像上进行的更加核心的颜色修改操作。它们是 亮度(brightness), 饱和度(saturation)对比度(contrast)。学习如何对这些颜色调整过程进行编码,将给我们学习屏幕的艺术效果一个很好的基础。

  • 始前准备
    这里我们需要创建一些新的资源。我们可以利用同样的场景作为我们的测试场景,但是我们需要一个新的脚本和着色器:
    • 1.创建一个新的名为 BSC_ImageEffect 的脚本。
    • 2.创建一个名为 BSC_Effect 的新着色器。
    • 3.现在我们需要简单将前一个知识点中的脚本代码复制到现在这个新的脚本中来。这样的话可以让我们把重点放在亮度,饱和度和对比度的数学原理上。
    • 4.把上一个知识点中的着色器代码复制到我们这个新的着色器中。
    • 5.在场景中创建几个新的游戏对象,然后添加几个不同颜色的漫反射材质球,然后把这些材质球随机的添加给场景中这几个新的游戏对象。这将会给我们一个很好的颜色范围来测试我们的新屏幕效果。

      当这些完成之后,你将会有一个类似于下图的游戏场景:
      diagram

uniform sampler2D _MainTex;
fixed _BrightnessAmount;
fixed _satAmount;
fixed _conAmount;
- 4.现在我们需要添加一些操作好用来表现我们的亮度,饱和度和对比度效果。在我们的着色器中添加下面的新函数,在 **frag()** 函数上面添加即可。不要担心它现在虽然还没啥用;我们将在下一个知识点中解释所有的代码:   
```c#
float3 ContrastSaturationBrightness(float3 color, float brt, float sat, float con)
  {
      float AvgLumR = 0.5;
      float AvgLumG = 0.5;
      float AvgLumB = 0.5;

      float LuminanceCoeff = float3(0.2125, 0.7154, 0.0721);

      float3 AvgLumin = float3(AvgLumR, AvgLumG, AvgLumB);
      float3 brtColor = color * brt;
      float intensityf = dot(brtColor, LuminanceCoeff);
      float3 intensity = float3(intensityf, intensityf, intensityf);

      float3 satColor = lerp(intensity, brtColor, sat);

      float3 conColor = lerp(AvgLumin, satColor, con);
      return conColor;
  }
  • 5.最后,我们需要更新 frag() 函数去使用 ContrastSaturationBrightness() 函数。这将会处理我们渲染纹理的所有像素并且传回给我们的C#脚本:
fixed4 frag(v2f_img i) : COLOR
  {
      fixed4 renderTex = tex2D(_MainTex, i.uv);
      renderTex.rgb = ContrastSaturationBrightness(renderTex.rgb, _BrightnessAmount, _satAmount, _conAmount);
      return renderTex;
  }

着色器代码写好之后,返回Unity编辑器让它编译着色器。如果没有报错,我们返回代码编辑器来编辑C#脚本了。开始时先创建几行新的代码用来发送合适数据给着色器:

  • 1.我们的第一步是修改我们的脚本添加合适的变量,这些变量将驱动屏幕效果中相应的值。在这里,我们分别需要三个滑动条,它们分别用来调整亮度,饱和度和对比度:
#region Variables
public Shader curShader;
public Material curMaterial;
public float brightnessAmount = 1.0f;
public float saturationAmount = 1.0f;
public float contrastAmount = 1.0f;
#endregion
  • 2.设置好我们的变量后,现在我们需要让脚本把它的数据传给着色器。我们在 OnRenderImage() 方法实现这一操作:
private void OnRenderImage(RenderTexture src, RenderTexture dest)
  {
      if (curShader != null)
      {
          material.SetFloat("_BrightnessAmount", brightnessAmount);
          material.SetFloat("_satAmount", saturationAmount);
          material.SetFloat("_conAmount", contrastAmount);
          Graphics.Blit(src, dest, material);
      }
      else
      {
          Graphics.Blit(src, dest);
      }
  }
  • 3.最后,我们需要将变量的值控制在一个合理的范围。这些控制范围内的值完全优先,这样你可以使用任何你觉得合适的值:
private void Update()
  {
      brightnessAmount = Mathf.Clamp(brightnessAmount, 0.0f, 2.0f);
      saturationAmount = Mathf.Clamp(brightnessAmount, 0.0f, 2.0f);
      contrastAmount = Mathf.Clamp(brightnessAmount, 0.0f, 3.0f);
  }

  • 原理介绍
    从我们知道基本的屏幕效果系统是如何工作的之后,我们就在 ContrastSaturationBrightness() 函数中实现了这一逐像素操作。

    函数的开头接收了一些参数,首先也是最重要的就是当前的渲染纹理。而其他的参数仅是用来调整屏幕效果的整体效果并且它们就是对应在 检查面板(Inspector tab) 中的那些滑动条。当这个函数接收到渲染纹理和那些调整后的值后,它就声明了一些常量,我们用这些常量去跟原始的渲染纹理进行修改和比较。

    变量 luminanceCoeff 中保存的值可以给我们当前图像的整体亮度。这个系数是基于 CIE(国际照明委员会) 色 匹配函数并且对于整个工业来说还挺标准的。通过当前图像和这个亮度系数的 dot 操作的投影,我们可以得到图像的整体亮度。一旦我们有了亮度,我就用一些 lerp 函数,来混合亮度操作的灰度图和跟亮度值进行了乘操作的原始图像, 这些参数传递给lerp函数。【抱歉,前面这一句我不会翻译,这里我觉得有问题,所以把英文贴出来:Once we have the brightness, we simply use a couple of lerp functions to blend from the grayscale version of the brightness operation and the original image multiplied by the brightness value, being passed into the function.】

    屏幕效果对于你的游戏获得高质量的图形来说是至关重要的,就好比我们这个屏幕效果,因为它不用你麻烦的去编辑你当前游戏场景中的每一个材质就可以改变游戏的最终样子。