1. 原理和过程
屏幕后处理是绑定摄像机的,通过抓取当前摄像机渲染的图像作为 SrcTextrue,然后按需依次调用处理接口,对 SrcTexture 进行处理,最后将处理完成的 DstTexture 显示到屏幕上,整个过程的调度通过 C# 脚本完成。
抓取摄像机当前渲染图像使用的接口如下:
OnRenderImage(RenderTexture _src, RenderTexture _dst)
其中:
- _src为抓取到的当前绑定摄像机的渲染图像
- _dst为处理结束时的目标纹理
调用的处理接口如下:
Graphics.Blit(Texture _src, RenderTexture _dst)
Graphics.Blit(Texture _src, RenderTexture _dst, Material _mat)
Graphics.Blit(Texture _src, Material _mat, int _passIndex = -1)
其中:
- _src 为要处理的原始图像
- _dst 为处理结束后存储到的目标纹理
- _mat 为本次处理指定的材质,其主要作用是为本次处理提供使用的Shader,需要注意的是,_src会被赋值给该_mat所携带Shader的_MainTex变量,因此在实现Shader时,也需要声明对应名字的变量用于接收原始图像纹理
- _passIndex 为本次处理指定使用的Pass索引,默认为-1,表示按照顺序依次执行Pass,否则为使用指定索引的Pass
2. 后处理脚本父类
我们可以建立一个后处理的父类,提供所有后处理脚本的基础功能,如检查效果可用性、初始化后处理所用材质等等。
脚本如下:
using UnityEngine;
[ExecuteInEditMode]
[RequireComponent(typeof(Camera))]
public class PostEffectBase : MonoBehaviour
{
void Start()
{
CheckResource();
}
void CheckResource()
{
bool _supported = CheckDefaultSupported() && CheckSpecificSupported();
if (!_supported) OnNotSurported();
}
/// <summary>
/// 用于后处理模块通用设置
/// 可被子类重写,因为可能存在某个子类不需要指定条件的情况
/// </summary>
/// <returns></returns>
protected virtual bool CheckDefaultSupported()
{
return true;
}
/// <summary>
/// 不同后处理子类可以分别实现各自特殊的检查
/// </summary>
/// <returns></returns>
protected virtual bool CheckSpecificSupported()
{
return true;
}
/// <summary>
/// 不满足后处理启用条件时禁用脚本
/// </summary>
void OnNotSurported()
{
enabled = false;
}
/// <summary>
/// 初始化后处理使用的材质和Shader
/// </summary>
/// <param name="_shader"></param>
/// <param name="_material"></param>
/// <returns></returns>
protected Material CheckShaderAndMaterial(Shader _shader, Material _material)
{
if (null == _shader || !_shader.isSupported) return null;
if (null == _material || _material.shader != _shader)
{
_material = new Material(_shader);
_material.hideFlags = HideFlags.DontSave;
}
return _material;
}
}
3. 调整亮度、饱和度、对比度
下面的例子中,我们实现一个用于调整屏幕亮度、饱和度以及对比度的后处理效果。
首先,从Shader出发,考虑如何实现对于上述三项的调整:
- 对纹理(抓取的屏幕图像)进行采样,得到原始颜色
- 调整亮度:用原始颜色乘以亮度系数 _Brightness,即可得到调整后的处理颜色①
- 调整饱和度:通过 0.2125 * R + 0.7154 * G + 0.0721 * B 公式对原始颜色进行处理,得到纹理灰度值,然后按照饱和度系数 _Saturation 从灰度值到处理颜色①进行插值,即可得到增加了饱和度变化的处理颜色②
- 调整对比度:构建一个 (0.5, 0.5, 0.5) 的对比度为 0 的颜色,按照对比度系数 _Contrast 向处理颜色②进行插值,得到最终的颜色
然后,创建调用后处理的脚本,继承自上文后处理父类:
- 为脚本指定所需Shader
- 实现OnRenderImage方法,为Shader所需的 _Brightness、 _Saturation、_Contrast 赋值,并调用 Blit 方法执行后处理
测试脚本:
using UnityEngine;
public class PostEffect_BrightnessSaturationContrast : PostEffectBase
{
public Shader BscShader;
public Material BscMat;
[Range(0.0f, 3.0f)]
public float Brightness = 1;
[Range(0.0f, 3.0f)]
public float Saturation = 0.5f;
[Range(0.0f, 3.0f)]
public float Contrast = 0.5f;
private void OnRenderImage(RenderTexture src, RenderTexture dest)
{
Material _mat = CheckShaderAndMaterial(BscShader, BscMat);
if (null == _mat) Graphics.Blit(src, dest);
else
{
_mat.SetFloat("_Brightness", Brightness);
_mat.SetFloat("_Saturation", Saturation);
_mat.SetFloat("_Contrast", Contrast);
Graphics.Blit(src, dest, _mat);
}
}
}
测试Shader:
Shader "MyShader/Chapter_12/Chapter_12_BSC_Shader"
{
Properties
{
_MainTex("MainTex", 2D) = "white"{}
}
SubShader
{
Pass
{
Tags{"LightMode" = "ForwardBase"}
ZTest Always
ZWrite Off
Cull Off
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma multi_compile_fwdbase
#include "UnityCG.cginc"
struct v2f
{
float4 pos : SV_POSITION;
float2 uv : TEXCOORD0;
};
sampler2D _MainTex;
float4 _MainTex_ST;
fixed _Brightness;
fixed _Saturation;
fixed _Contrast;
v2f vert(appdata_img v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.uv = v.texcoord;
return o;
}
fixed4 frag(v2f i) : SV_Target
{
fixed3 _samplerColor = tex2D(_MainTex, i.uv).rgb;
fixed3 _finalColor = _samplerColor * _Brightness;
_samplerColor *= _Brightness;
//0.2125 * R + 0.7154 * G + 0.0721 * B
fixed _luminance = 0.2125 * _samplerColor.r + 0.7154 * _samplerColor.g + 0.0721 * _samplerColor.b;
_finalColor = lerp(float3(_luminance, _luminance, _luminance), _finalColor, _Saturation);
_finalColor = lerp(float3(0.5, 0.5, 0.5), _finalColor, _Contrast);
return fixed4(_finalColor, 1);
}
ENDCG
}
}
}
测试效果:
亮度:
饱和度:
对比度: