1 Unity中实现屏幕特效的基本步骤
什么叫屏幕后处理(Screen post-processing effects)——渲染完整个场景得到屏幕图像后对图像进行一系列操作,实现各种屏幕特效,这一步我们可以添加很多例如景深(Depth of Field)、运动模糊(Motion Blur)等效果。
基本步骤大概分为:
-
检查资源和条件是否满足(Pass掉)
《入门精要》中提到在进行后处理前需要判断当前平台是否支持渲染纹理和屏幕特效、使用的Unity Shader是否支持等等,实现这一步我们需要创建一个用于判断的基类脚本,后续所有进行后处理的脚本都需要继承这个基类。
但C#脚本中用于检查的判断语法,我在真正实践时发现在Unity现在的版本中已经将它们舍弃了,因此我在做后处理练习时并没有完全按照《入门精要》上面的操作去执行它的基类,具体后面会有所体现。
-
添加用于屏幕后处理的脚本
C#脚本和之前写的Shader文件不同,有了脚本文件Unity才会听懂哪个步骤是抓取屏幕图像、哪个步骤是对当前图像进行处理
-
获取当前屏幕图像
这一步用Unity提供的OnRenderImage函数来实现
- 使用Shader对图像进行处理
用Unity提供的Graphics.Blit函数实现
- 把结果显示在屏幕上
2 准备脚本
2.1 省去了PostEffectsBase脚本
首先说一下《入门精要》中提前准备的一个PostEffectsBase的脚本,其中:
提示这两个最重要的判断语法都被弃用了,所以我认为之后的判断也都不是很有必要,所以就省去了PostEffectsBase这个前提脚本。
2.2 Camera挂的脚本
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
[ExecuteInEditMode]
public class BrightnessSaturationAndContrast : MonoBehaviour
{
public Shader briSatConShader;
public Material briSatConMaterial;
[Range(0.0f, 3.0f)]
public float brightness = 1.0f;
[Range(0.0f, 3.0f)]
public float saturation = 1.0f;
[Range(0.0f, 3.0f)]
public float contrast = 1.0f;
//运用 OnRenderImage(src, des)
void OnRenderImage(RenderTexture source, RenderTexture destination)
{
if (briSatConMaterial != null)
{
briSatConMaterial.SetFloat("_Brightness", brightness);
briSatConMaterial.SetFloat("_Saturation", saturation);
briSatConMaterial.SetFloat("_Contrast", contrast);
Graphics.Blit(source, destination, briSatConMaterial);
}
else
{
Debug.LogWarning("Please input your Material!");
Graphics.Blit(source, destination);
}
}
}
其中涉及到了两个第1小节已经提到过的重要函数:
OnRenderImage函数
关于该函数的用法,官方介绍:
MonoBehaviour-OnRenderImage(RenderTexture,RenderTexture) - Unity 脚本 API
第一个参数是当前屏幕的渲染纹理/上一步处理后得到的渲染纹理,第二个参数是目标渲染纹理,也是最后输出在屏幕上的东西。
Graphics.Blit函数
这个函数通常都会包括在一个OnRenderImage函数里,官方介绍:
它有超多种形式
这次用的是第二种,
- mat——是我们使用的材质,材质的Shader就是真正进行后处理操作的部分
- pass——表示将会依次调用Shader中的所有Pass
3 最重要的Shader部分
首先,亮度、饱和度、对比度这三个值其实是数字图像处理中常见属性,其他常见的还有锐化、分辨率等等属性。,图像的亮度、饱和度、对比度和锐化之间不是相互独立的。之前进行色彩基础学习的时候总结了一点点色彩相关的东西:【技术美术图形部分】2.1 色彩空间
参考:
【数字图像处理系列二】基本概念:亮度、对比度、饱和度、锐化、分辨率
3.1 亮度部分
亮度是这三个参数里最好理解的,就是颜色的明亮程度,它直接跟RGB三个分量的大小相关,所以RGB值越大,颜色亮度也越大。因此调整亮度很简单,直接在采样得到的像素R/G/B分别×亮度修改值Brightness就行:
//亮度
fixed3 finalColor = renderTex.rgb * _Brightness;
3.2 饱和度部分
饱和度是指图像颜色种类的多少、色彩的纯度,通俗一点将也就是颜色的鲜艳程度,他也是HSV(hue-saturaion-value)色彩模型中包含的色相、饱和度、明度三个属性之一,调整饱和度可以修正过度曝光/未充分曝光的照片,让图片看上去更加自然。
下面是Shader中调整饱和度的代码部分:
//饱和度
fixed luminance = 0.2125 * renderTex.r + 0.7154 * renderTex.g + 0.0721 * renderTex.b; //计算该像素的亮度值
fixed3 luminanceColor = fixed3(luminance, luminance, luminance); //创建饱和度为0的颜色
finalColor = lerp(luminanceColor, finalColor, _Saturation);
这是在通过RGB三通道的值转灰度图。由于人眼对红绿蓝的敏感程度不同,所以计算灰度时需要根据R/G/B三个通道的值进行加权平均。
那么(0.2125, 0.7154, 0.0721)怎么得到的这三组值到底是怎么来的?搜到的说这三组值是YUV BT709中的RGB转YUV的算法,而RGB转YUV的算法有很多种。简单了解一下YUV:YUV是一种颜色编码方法,其中
- Y表示明度(Luminance/Luma),即灰阶值
- U和V表示色度,作用是描述色彩及饱和度,用于指定像素的颜色
下面这是搜到的网上BT709的RGB和YUV互相转的算法:
上面Shader代码中改变饱和度的方法是先获得灰度图(三通道都取计算的Y灰阶值),然后用这个灰度颜色跟上一步修改亮度之后的颜色中间做插值,插值的依据就是用户输入的_Saturation的调整值。
3.3 对比度部分
对比度指的是图像暗和亮的落差值,也是图像最大灰度和最小灰度之间的差值。通俗一点讲,对比度越高,图像越清晰;对比度越小,图像越灰蒙蒙的。
对比度和亮度的区别就在于,亮度表达的是图像的黑白占比,亮度越大整体趋向于白色,亮度越小趋向于黑色;而对比度这个考虑的是图像中最亮和最暗之间的差距,灰色是对比度为0的归宿,而整个画面黑白超级明显则是对比度为1呈现的效果。
代码中的实现也很简单:
//contrast
fixed3 avgColor = fixed3(0.5, 0.5, 0.5);
finalColor = lerp(avgColor, finalColor, _Contrast);
先取一个对比度为0的颜色值(R/G/B均为0.5),Unity对于RGBA的取值可参考UnityEngine.Color - Unity 脚本 API),然后也是用一个插值范围函数lerp将用户输入的对比度控制值_Contrast包含进去。
3.4 完整代码
Shader "Unity Shaders Book/Chapter 12/BrightnessSaturationAndContrast"
{
Properties
{
_MainTex ("Base(RGB", 2D) = "white" {}
//从脚本传递更好,这里可以直接省略这些值的展示
_Brightness ("Brightness", float) = 1
_Saturation ("Saturation", float) = 1
_Contrast ("Contrast", float) = 1
}
SubShader
{
Pass
{
//关闭深度写入
ZTest Always Cull Off Zwrite Off
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
//properties
sampler2D _MainTex;
half _Brightness;
half _Saturation;
half _Contrast;
struct v2f {
float4 pos : SV_POSITION;
half2 uv : TEXCOORD0;
};
//使用了内置的appdata_img结构体作为顶点着色器的输入
v2f vert(appdata_img v) {
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.uv = v.texcoord;
return o;
}
fixed4 frag(v2f i) : SV_Target {
//获得屏幕图像的采样
fixed4 renderTex = tex2D(_MainTex, i.uv);
//亮度
fixed3 finalColor = renderTex.rgb * _Brightness;
//饱和度
fixed luminance = 0.2125 * renderTex.r + 0.7154 * renderTex.g + 0.0721 * renderTex.b; //计算该像素的亮度值
fixed3 luminanceColor = fixed3(luminance, luminance, luminance); //创建饱和度为0的颜色
finalColor = lerp(luminanceColor, finalColor, _Saturation);
//contrast
fixed3 avgColor = fixed3(0.5, 0.5, 0.5);
finalColor = lerp(avgColor, finalColor, _Contrast);
return fixed4(finalColor, renderTex.a);
}
ENDCG
}
}
FallBack Off
}
4 效果展示
这里我用了一个大家都会用到的Unity商城里的免费资源,给Main Camera挂上了上面写的脚本,效果如下: