Unity URP/HDRP管线通用:手把手教你写屏幕后处理特效(从亮度Shader到CommandBuffer进阶)
在Unity游戏开发中,屏幕后处理特效是提升画面表现力的重要手段。随着Unity渲染管线的演进,从传统的Built-in管线到现在的URP(Universal Render Pipeline)和HDRP(High Definition Render Pipeline),实现屏幕后处理的方式也发生了显著变化。本文将深入探讨如何在不同渲染管线下实现亮度、饱和度和对比度调整,并介绍CommandBuffer等进阶技术。
1. 现代Unity渲染管线中的后处理实现差异
Unity的三种主要渲染管线(Built-in、URP、HDRP)在后处理实现上有着根本性的区别:
Built-in管线:
- 使用
OnRenderImage方法和Graphics.Blit进行后处理 - 通过
[ImageEffectOpaque]标签控制执行时机 - 需要手动管理渲染纹理和材质
URP管线:
- 使用
ScriptableRendererFeature系统 - 通过
RenderPass实现后处理效果 - 内置了完整的后处理堆栈(Post-processing Stack)
HDRP管线:
- 同样使用
ScriptableRendererFeature系统 - 但需要处理更复杂的渲染路径和光照计算
- 需要考虑HDR颜色空间和色调映射
重要提示:从Built-in迁移到SRP(URP/HDRP)时,最大的变化是需要完全重构后处理效果的实现方式,而不仅仅是简单的API替换。
2. URP中的亮度/饱和度/对比度实现
在URP中实现这些基础后处理效果,我们需要创建一个自定义的RendererFeature和RenderPass:
// 亮度饱和度对比度RenderFeature public class BrightnessSaturationContrastFeature : ScriptableRendererFeature { [System.Serializable] public class Settings { public Material material; [Range(0.1f, 3.0f)] public float brightness = 1.0f; [Range(0.1f, 3.0f)] public float saturation = 1.0f; [Range(0.1f, 3.0f)] public float contrast = 1.0f; } public Settings settings = new Settings(); private BrightnessSaturationContrastPass pass; public override void Create() { pass = new BrightnessSaturationContrastPass(settings); } public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData) { renderer.EnqueuePass(pass); } }对应的RenderPass实现:
class BrightnessSaturationContrastPass : ScriptableRenderPass { private Material material; private float brightness; private float saturation; private float contrast; private RenderTargetIdentifier source; private RenderTargetHandle tempTexture; public BrightnessSaturationContrastPass(BrightnessSaturationContrastFeature.Settings settings) { material = settings.material; brightness = settings.brightness; saturation = settings.saturation; contrast = settings.contrast; tempTexture.Init("_TempBrightnessSaturationContrastTexture"); } public void Setup(RenderTargetIdentifier source) { this.source = source; } public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData) { CommandBuffer cmd = CommandBufferPool.Get("BrightnessSaturationContrast"); RenderTextureDescriptor opaqueDesc = renderingData.cameraData.cameraTargetDescriptor; opaqueDesc.depthBufferBits = 0; cmd.GetTemporaryRT(tempTexture.id, opaqueDesc); material.SetFloat("_Brightness", brightness); material.SetFloat("_Saturation", saturation); material.SetFloat("_Contrast", contrast); cmd.Blit(source, tempTexture.Identifier(), material); cmd.Blit(tempTexture.Identifier(), source); context.ExecuteCommandBuffer(cmd); CommandBufferPool.Release(cmd); } public override void FrameCleanup(CommandBuffer cmd) { cmd.ReleaseTemporaryRT(tempTexture.id); } }对应的Shader需要适配URP的着色器语言和包含文件:
Shader "PostProcessing/BrightnessSaturationContrast" { HLSLINCLUDE #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl" TEXTURE2D(_MainTex); SAMPLER(sampler_MainTex); float _Brightness; float _Saturation; float _Contrast; struct Attributes { float4 positionOS : POSITION; float2 uv : TEXCOORD0; }; struct Varyings { float4 positionCS : SV_POSITION; float2 uv : TEXCOORD0; }; Varyings Vert(Attributes input) { Varyings output; output.positionCS = TransformObjectToHClip(input.positionOS.xyz); output.uv = input.uv; return output; } half4 Frag(Varyings input) : SV_Target { half4 tex = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, input.uv); half3 finalColor = tex.rgb * _Brightness; half luminance = dot(finalColor, half3(0.2126, 0.7152, 0.0722)); half3 luminanceColor = half3(luminance, luminance, luminance); finalColor = lerp(luminanceColor, finalColor, _Saturation); half3 avgColor = half3(0.5, 0.5, 0.5); finalColor = lerp(avgColor, finalColor, _Contrast); return half4(finalColor, tex.a); } ENDHLSL SubShader { Cull Off ZWrite Off ZTest Always Pass { HLSLPROGRAM #pragma vertex Vert #pragma fragment Frag ENDHLSL } } }3. HDRP中的特殊考量
在HDRP中实现相同的效果需要考虑更多因素:
- 颜色空间:HDRP使用线性颜色空间,需要确保Shader正确处理HDR值
- 色调映射:后处理效果应该在色调映射之前应用
- 性能优化:HDRP场景通常更复杂,需要更注重性能
HDRP的实现结构与URP类似,但Shader需要适配HDRP的包含文件和函数:
Shader "PostProcessing/BrightnessSaturationContrastHDRP" { HLSLINCLUDE #include "Packages/com.unity.render-pipelines.core/ShaderLibrary/Common.hlsl" #include "Packages/com.unity.render-pipelines.high-definition/Runtime/ShaderLibrary/ShaderVariables.hlsl" TEXTURE2D_X(_MainTex); float _Brightness; float _Saturation; float _Contrast; struct Attributes { uint vertexID : SV_VertexID; }; struct Varyings { float4 positionCS : SV_POSITION; float2 texcoord : TEXCOORD0; }; Varyings Vert(Attributes input) { Varyings output; output.positionCS = GetFullScreenTriangleVertexPosition(input.vertexID); output.texcoord = GetFullScreenTriangleTexCoord(input.vertexID); return output; } half4 Frag(Varyings input) : SV_Target { float2 uv = input.texcoord; half4 tex = SAMPLE_TEXTURE2D_X(_MainTex, s_linear_clamp_sampler, uv); // HDRP中可能需要额外的颜色空间转换 half3 hdrColor = tex.rgb; hdrColor *= _Brightness; half luminance = dot(hdrColor, half3(0.2126, 0.7152, 0.0722)); hdrColor = lerp(luminance, hdrColor, _Saturation); half3 avgColor = half3(0.5, 0.5, 0.5); hdrColor = lerp(avgColor, hdrColor, _Contrast); return half4(hdrColor, tex.a); } ENDHLSL SubShader { Tags { "RenderPipeline" = "HDRenderPipeline" } Pass { Cull Off ZWrite Off ZTest Always HLSLPROGRAM #pragma vertex Vert #pragma fragment Frag ENDHLSL } } }4. CommandBuffer进阶实现
对于需要更精细控制或更高性能的场景,可以使用CommandBuffer直接操作渲染流程。这种方式在三种管线中都适用,但具体实现略有不同。
通用CommandBuffer实现步骤:
- 创建CommandBuffer并设置名称
- 获取临时渲染纹理
- 设置渲染目标
- 执行绘制命令
- 执行CommandBuffer
- 释放临时资源
// 使用CommandBuffer实现后处理 public class BrightnessSaturationContrastCommandBuffer : MonoBehaviour { [Range(0.1f, 3.0f)] public float brightness = 1.0f; [Range(0.1f, 3.0f)] public float saturation = 1.0f; [Range(0.1f, 3.0f)] public float contrast = 1.0f; public Material effectMaterial; private CommandBuffer commandBuffer; private RenderTexture tempTexture; private void OnEnable() { commandBuffer = new CommandBuffer { name = "BrightnessSaturationContrast" }; GetComponent<Camera>().AddCommandBuffer(CameraEvent.AfterEverything, commandBuffer); } private void OnDisable() { if (commandBuffer != null) { GetComponent<Camera>().RemoveCommandBuffer(CameraEvent.AfterEverything, commandBuffer); commandBuffer.Dispose(); } if (tempTexture != null) RenderTexture.ReleaseTemporary(tempTexture); } private void Update() { if (effectMaterial == null) return; effectMaterial.SetFloat("_Brightness", brightness); effectMaterial.SetFloat("_Saturation", saturation); effectMaterial.SetFloat("_Contrast", contrast); } private void OnRenderImage(RenderTexture src, RenderTexture dest) { if (effectMaterial == null) { Graphics.Blit(src, dest); return; } commandBuffer.Clear(); tempTexture = RenderTexture.GetTemporary(src.width, src.height, 0); commandBuffer.Blit(src, tempTexture); commandBuffer.Blit(tempTexture, dest, effectMaterial); Graphics.ExecuteCommandBuffer(commandBuffer); RenderTexture.ReleaseTemporary(tempTexture); } }三种管线中的CommandBuffer差异对比:
| 特性 | Built-in管线 | URP | HDRP |
|---|---|---|---|
| 添加位置 | CameraEvent枚举 | ScriptableRenderer.EnqueueCommandBuffer | RenderPipelineManager事件 |
| 执行上下文 | 直接执行 | 通过ScriptableRenderContext | 通过HDCamera |
| 纹理处理 | 标准RenderTexture | RTHandle系统 | RTHandle系统 |
| 性能考量 | 中等 | 优化较好 | 需要特别注意性能 |
专业建议:在URP/HDRP中,除非有特殊需求,否则优先使用RendererFeature而不是CommandBuffer,因为URP的内部实现已经对CommandBuffer的使用进行了优化封装。
5. 性能优化与多平台适配
实现跨管线的屏幕后处理效果时,性能优化至关重要。以下是一些关键优化策略:
1. 渲染纹理优化:
- 使用适当的分辨率(全屏、半屏或四分之一屏)
- 选择合适的纹理格式(通常RGB111110Float或ARGBHalf足够)
- 及时释放临时纹理
2. Shader优化技巧:
- 减少纹理采样次数
- 使用更高效的数学运算
- 避免分支语句
// 优化后的片段着色器代码示例 half4 FragOptimized(Varyings input) : SV_Target { half4 tex = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, input.uv); half luminance = dot(tex.rgb, half3(0.2126, 0.7152, 0.0722)); // 合并亮度饱和度的计算 half3 adjustedColor = tex.rgb * _Brightness; adjustedColor = lerp(luminance, adjustedColor, _Saturation); // 使用预计算的对比度基准 static const half3 midGray = 0.5h; return half4(lerp(midGray, adjustedColor, _Contrast), tex.a); }3. 多平台适配注意事项:
移动平台:
- 使用更简单的Shader变体
- 考虑使用ES2.0或ES3.0兼容的语法
- 测试低精度浮点数的表现
PC/主机平台:
- 可以利用更复杂的计算
- 支持更高精度的纹理格式
- 可以使用计算着色器优化
4. 动态质量调整:
// 根据性能动态调整后处理质量的示例 public class DynamicEffectQuality : MonoBehaviour { public float targetFrameTime = 0.0167f; // 60FPS public float[] qualityLevels = { 1.0f, 0.75f, 0.5f, 0.25f }; private int currentQualityLevel = 0; private float lastFrameTime; private void Update() { lastFrameTime = Time.unscaledDeltaTime; if (lastFrameTime > targetFrameTime && currentQualityLevel < qualityLevels.Length - 1) { currentQualityLevel++; ApplyQualitySettings(); } else if (lastFrameTime < targetFrameTime * 0.8f && currentQualityLevel > 0) { currentQualityLevel--; ApplyQualitySettings(); } } private void ApplyQualitySettings() { float resolutionScale = qualityLevels[currentQualityLevel]; // 调整渲染纹理分辨率等参数 } }在实际项目中,我发现动态调整后处理分辨率对性能提升最为明显,特别是在移动设备上。通过将渲染目标分辨率降低到屏幕分辨率的50%,通常可以获得2倍以上的性能提升,而视觉质量损失在运动状态下几乎不可察觉。