Unity屏幕滤镜实战:用OnRenderImage和Shader实现亮度、饱和度、对比度自由调节(附完整代码)
Unity屏幕滤镜系统从原理到动态效果的全流程开发指南在游戏开发中画面表现力往往决定了玩家的第一印象。想象一下当玩家从阴暗的地下城步入阳光明媚的草原时画面逐渐变亮、色彩逐渐鲜艳或是角色进入中毒状态时屏幕饱和度降低、对比度增强——这些细腻的画面变化都能极大增强游戏沉浸感。本文将带你深入Unity屏幕后处理技术构建一个完整的、可动态调节的屏幕滤镜系统。1. 屏幕后处理核心架构设计屏幕后处理Screen Post-Processing是现代游戏引擎的标配功能它允许开发者在摄像机完成场景渲染后对最终图像进行二次加工。Unity中的这一流程主要依赖于两个关键组件OnRenderImage生命周期方法和Graphics.Blit渲染指令。1.1 OnRenderImage工作机制详解OnRenderImage是MonoBehaviour提供的特殊方法当摄像机完成所有渲染后会自动调用。其标准签名如下void OnRenderImage(RenderTexture src, RenderTexture dest)其中src是摄像机渲染的原始图像dest是处理后输出的目标纹理。如果不做任何处理默认行为是将src直接复制到dest。我们可以通过添加[ImageEffectOpaque]属性标签来控制执行时机[ImageEffectOpaque] // 在不透明Pass后立即执行 void OnRenderImage(RenderTexture src, RenderTexture dest) { // 处理逻辑 }注意在URP/HDRP管线中屏幕后处理的实现方式有所不同需要通过Renderer Features实现本文聚焦传统Built-in管线方案。1.2 Graphics.Blit的三种应用模式Graphics.Blit是Unity提供的纹理拷贝工具方法它有多种重载形式简单拷贝模式Graphics.Blit(source, dest);材质处理模式最常用Graphics.Blit(source, dest, material);指定Pass模式Graphics.Blit(source, dest, material, passIndex);在材质处理模式下Shader中必须声明_MainTex属性Unity会自动将source纹理赋值给它。一个典型的后处理Shader结构如下Shader Custom/PostEffectBase { Properties { _MainTex (Base (RGB), 2D) white {} } SubShader { Pass { // Shader代码 } } }2. 色彩调节的数学原理与Shader实现理解亮度、饱和度和对比度的数学本质是构建高质量滤镜系统的前提。下面我们分别解析这三种效果的实现原理。2.1 亮度调节算法亮度调整本质上是颜色值的线性缩放finalColor originalColor * brightness在Shader中实现时需要考虑Gamma空间和线性空间的差异// Gamma空间下的亮度调整 fixed3 ApplyBrightness(fixed3 color, float brightness) { return color * brightness; } // 线性空间下的亮度调整 fixed3 ApplyBrightnessLinear(fixed3 color, float brightness) { return pow(color, 2.2) * brightness; }2.2 饱和度调节算法饱和度调整需要先计算颜色的亮度值luminance然后在原始颜色和灰度颜色之间插值fixed3 ApplySaturation(fixed3 color, float saturation) { fixed luminance dot(color, fixed3(0.2125, 0.7154, 0.0721)); fixed3 gray fixed3(luminance, luminance, luminance); return lerp(gray, color, saturation); }其中0.2125, 0.7154, 0.0721是RGB到亮度的转换系数符合人眼对不同颜色的敏感度差异。2.3 对比度调节算法对比度调整以中性灰0.5为基准进行非线性缩放fixed3 ApplyContrast(fixed3 color, float contrast) { fixed3 avgColor fixed3(0.5, 0.5, 0.5); return lerp(avgColor, color, contrast); }2.4 完整Shader实现将三种效果组合起来得到完整的屏幕滤镜ShaderShader Custom/BrightnessSaturationContrast { Properties { _MainTex (Base (RGB), 2D) white {} _Brightness (Brightness, Float) 1 _Saturation (Saturation, Float) 1 _Contrast (Contrast, Float) 1 } SubShader { Pass { CGPROGRAM #pragma vertex vert_img #pragma fragment frag #include UnityCG.cginc sampler2D _MainTex; half _Brightness; half _Saturation; half _Contrast; fixed4 frag(v2f_img i) : SV_Target { fixed4 tex tex2D(_MainTex, i.uv); // 亮度处理 fixed3 finalColor tex.rgb * _Brightness; // 饱和度处理 fixed luminance dot(finalColor, fixed3(0.2125, 0.7154, 0.0721)); finalColor lerp(fixed3(luminance,luminance,luminance), finalColor, _Saturation); // 对比度处理 finalColor lerp(fixed3(0.5,0.5,0.5), finalColor, _Contrast); return fixed4(finalColor, tex.a); } ENDCG } } }3. 工程化实现与性能优化将技术原型转化为可复用的生产级组件需要考虑架构设计和性能优化。3.1 可配置的滤镜组件创建ScreenFilter组件类支持编辑器实时调节[ExecuteInEditMode] [RequireComponent(typeof(Camera))] public class ScreenFilter : MonoBehaviour { [Range(0.1f, 3f)] public float brightness 1f; [Range(0.1f, 3f)] public float saturation 1f; [Range(0.1f, 3f)] public float contrast 1f; private Material _material; void OnEnable() { _material new Material(Shader.Find(Custom/BrightnessSaturationContrast)); _material.hideFlags HideFlags.DontSave; } 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); } } void OnDisable() { if (_material ! null) { DestroyImmediate(_material); } } }3.2 材质实例管理策略为避免每帧创建材质带来的性能开销推荐以下优化方案材质缓存在OnEnable中创建材质OnDisable中销毁共享材质多个摄像机共享同一个材质实例参数批量设置使用MaterialPropertyBlock避免材质实例化// 优化后的材质管理 private static Material _sharedMaterial; private MaterialPropertyBlock _propertyBlock; void OnEnable() { if (_sharedMaterial null) { _sharedMaterial new Material(Shader.Find(Custom/BSC)); _sharedMaterial.hideFlags HideFlags.DontSave; } _propertyBlock new MaterialPropertyBlock(); } void OnRenderImage(RenderTexture src, RenderTexture dest) { _propertyBlock.SetFloat(_Brightness, brightness); _propertyBlock.SetFloat(_Saturation, saturation); _propertyBlock.SetFloat(_Contrast, contrast); Graphics.Blit(src, dest, _sharedMaterial, -1, _propertyBlock); }3.3 多滤镜组合方案当需要同时应用多个效果时可以采用以下架构方案优点缺点单一Shader性能最佳耦合度高难以维护多Pass渲染模块化带宽开销大中间纹理灵活性高内存占用大推荐的多滤镜实现结构void OnRenderImage(RenderTexture src, RenderTexture dest) { RenderTexture rt RenderTexture.GetTemporary(src.width, src.height); // 第一个效果 Graphics.Blit(src, rt, effectMaterial1); // 第二个效果 Graphics.Blit(rt, dest, effectMaterial2); RenderTexture.ReleaseTemporary(rt); }4. 动态滤镜效果实战案例静态参数调节只是基础真正的价值在于实现随时间变化的动态效果。4.1 昼夜循环亮度变化通过动画曲线控制亮度参数模拟自然光照变化public AnimationCurve dayNightCurve; public float cycleDuration 60f; void Update() { float time Time.time % cycleDuration / cycleDuration; brightness dayNightCurve.Evaluate(time); }4.2 场景过渡效果使用协程实现平滑的场景过渡滤镜IEnumerator TransitionEffect(float targetBrightness, float duration) { float start brightness; float elapsed 0f; while (elapsed duration) { brightness Mathf.Lerp(start, targetBrightness, elapsed / duration); elapsed Time.deltaTime; yield return null; } brightness targetBrightness; }4.3 角色状态反馈将滤镜参数与游戏逻辑关联增强玩家反馈public void OnPlayerHealthChanged(float healthRatio) { saturation healthRatio; contrast 1 (1 - healthRatio) * 2; }4.4 性能敏感型设备适配根据设备性能动态调整效果精度void AdjustForPerformance() { int qualityLevel QualitySettings.GetQualityLevel(); if (qualityLevel 1) { // 低端设备 this.enabled false; } else if (qualityLevel 3) { // 中端设备 UpdateInterval 0.1f; // 每0.1秒更新一次 } // 高端设备保持全效果 }在移动设备上可以进一步优化Shader计算精度// 使用half精度替代float half _Brightness; half _Saturation; half _Contrast;