一、需要提前了解的
其实关于屏幕后处理(Screen Post-processing Effects)相关的知识,前面已经提过了不少:
这些是 OpenGL 渲染部分:
- OpenGL基础35:帧缓冲(下)之简单图像处理:图像处理更多的是思维和算法,和你的平台/语言都无关
- OpenGL基础33:帧缓冲(上)之离屏渲染
- OpenGL基础34:帧缓冲(中)之附件
搞懂了上面这些后,就可以进入 Unity3D 了:
- UnityShader19:渲染纹理(上)之截屏功能实现:里面有关 RenderImage 函数和 Bilt 函数的讲解,这里会用到
- UnityShader20:CommandBuffer初见(上)
- UnityShader20.1:CommandBuffer初见(下)
了解了这些内容之后,这一章的内容就非常好理解
二、屏幕后处理 C# 部分逻辑
- 屏幕后处理的时机:完成整个场景、得到屏幕图像后
- 屏幕后处理的目的:对整个游戏画面添加各种艺术效果和屏幕特技
对于上面的时机和目的,单纯的一个或多个 Shader 就可能难以去实现完整的屏幕后处理逻辑,还需要脚本的帮助
先解决第一个问题:抓取屏幕,这就依赖于 OnRenderImage 函数和 Graphics.Bilt 函数
using UnityEngine;
using System.Collections;
[ExecuteInEditMode]
//当在物体上添加该脚本时,Camera组件也会被自动被添加上去(如果没有的话)
[RequireComponent(typeof(Camera))]
public class PostEffectsBase: MonoBehaviour
{
protected void Start()
{
//如果显卡支持图像后期处理效果
if (!SystemInfo.supportsImageEffects)
{
//设置当前组件为关闭状态
enabled = false;
}
}
protected Material CheckShaderAndCreateMaterial(Shader shader, Material material)
{
//判断当前着色器是否可在当前显卡设备上使用,如果对应的着色器的所有Fallback都不可支持或者传入的着色器就不存在,返回null
if (shader == null || !shader.isSupported)
return null;
if (material && material.shader == shader)
return material;
else
{
material = new Material(shader);
//设置当前材质不会被保存在场景中
material.hideFlags = HideFlags.HideAndDontSave;
if (material)
return material;
else
return null;
}
}
}
上面这个是基类,主要用于检查条件和资源是否支持我们进行图像后处理,以及组合 Shader 和 Meterial
- SystemInfo.supportsImageEffects:如果显卡支持图像后期处理效果,返回 true
- [RequireComponent(typeof(cName))]:添加依赖项:当你设置一个用了 RequireComponent 的脚本为组件,与此同时组件 cName 也会被自动被添加上去(如果没有的话)且不可被移除
- shader.isSupported:当前着色器是否可在当前显卡设备上使用,如果对应的着色器的所有 Fallback 都不可支持,返回 false
- object.hideFlags:设置的值应为 Unity 内置的枚举 HideFlags,用于控制对象的销毁、保存和在检查器中的可见性
using UnityEngine;
using System.Collections;
public class PostEffectExample: PostEffectsBase
{
public Shader shader;
private Material _material;
public Material material
{
get
{
_material = CheckShaderAndCreateMaterial(shader, _material);
return _material;
}
}
[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;
void OnRenderImage(RenderTexture src, RenderTexture dest)
{
if (material != null)
{
material.SetFloat("_Brightness", brightness);
material.SetFloat("_Saturation", saturation);
material.SetFloat("_Contrast", contrast);
Graphics.Blit(src, dest, material);
}
else
{
Graphics.Blit(src, dest);
}
}
}
这是屏幕后处理的核心脚本:通过 OnRenderTexture 拿到当前屏幕 RT,之后再通过 Blit 将它作为绑定了指定 Shader Meterial 的主纹理,进行一次或多次渲染,得到的最终结果会显示到屏幕上
- MonoBehaviour.OnRenderImage(srcRT, targetRT):中文简单介绍参见这里,它在所有不透明和透明的 Pass 执行完毕后被调用,以便对场景中所有的游戏对象都产生影响
- Graphics.Blit(…):中文简单介绍参见这里
三、亮度、对比度与饱和度
和这一章一样,亮度、对比度与饱和度的计算当然是在着色器中:
Shader "Jaihk662/PPEExample1"
{
Properties
{
_MainTex("Base(RGB)", 2D) = "white" {}
_Brightness("Brightness", Float) = 1 //亮度
_Saturation("Saturation", Float) = 1 //饱和度
_Contrast("Contrast", Float) = 1 //对比度
}
CGINCLUDE //和之前常用的CGPROGRAM不同,在CGINCLUDE和ENDCG范围内插入的shader代码会被插入到所有Pass中
#include "UnityCG.cginc"
sampler2D _MainTex;
half _Brightness;
half _Saturation;
half _Contrast;
struct vert2frag
{
float4 pos: SV_POSITION;
float2 uv: TEXCOORD0;
};
vert2frag vert(appdata_img v)
{
vert2frag o;
o.pos = UnityObjectToClipPos(v.vertex);
o.uv = v.texcoord;
return o;
}
half4 frag(vert2frag 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);
finalColor = lerp(luminanceColor, finalColor, _Saturation);
//计算对比度
fixed3 avgColor = fixed3(0.5, 0.5, 0.5);
finalColor = lerp(avgColor, finalColor, _Contrast);
return fixed4(finalColor, renderTex.a);
}
ENDCG
Subshader
{
Pass
{
ZTest Always Cull Off ZWrite Off
Fog { Mode off } //设置雾模式:关闭
CGPROGRAM
//使用低精度(FP16)以提升fragment着色器的运行速度,减少时间
#pragma fragmentoption ARB_precision_hint_fastest
#pragma vertex vert
#pragma fragment frag
ENDCG
}
}
}
然后只需要将这个 Shader 给到对应的后处理脚本就 OK,这个 Shader 就是 Bilt 操作时 Meterial 对应的 Shader
上面的参数对应的亮度为 2.74,饱和度为 0.83,对比度为 0.2,效果和原图如下: