第7章 URP后处理系统

第7章 URP后处理系统

理论讲解

7.1 Volume框架详解

URP的后处理系统基于Volume框架实现,这是一个灵活的系统,允许开发者在场景中定义不同的后处理区域。Volume框架是URP中管理后处理效果的核心机制,它将后处理效果封装为可复用的组件。

Volume系统包含两个核心概念:

  1. Volume:定义了后处理效果的应用区域和权重
  2. Volume Profile:包含实际的后处理效果配置

Volume系统的工作原理:

  • 场景中的Volume组件定义了后处理效果的生效范围
  • 每个Volume引用一个Volume Profile,其中包含具体的后处理设置
  • 多个Volume可以混合,通过权重控制效果的强度
  • 相机根据与Volume的距离和权重计算最终的后处理效果

7.2 主要后处理效果

Bloom(泛光效果)

Bloom是模拟真实相机镜头的光晕效果,使明亮区域产生光晕扩散。在URP中,Bloom效果通过以下参数控制:

  • Threshold(阈值):定义哪些像素被认为是”明亮”的
  • Intensity(强度):控制泛光的整体强度
  • Scatter(散射):控制光晕的扩散程度
  • Clamp(钳制):限制泛光的最大亮度

Color Grading(色彩分级)

Color Grading用于调整场景的整体色调和色彩平衡,包括:

  • Tone Mapping:色调映射算法(ACES, Neutral, ACES Legacy)
  • White Balance:白平衡调整
  • Tone:阴影、中间调、高光的色调调整
  • Channel Mixer:RGB通道混合
  • Color Wheels:阴影、中间调、高光的色彩校正

Tonemapping(色调映射)

Tonemapping将高动态范围(HDR)的颜色值转换为适合显示的低动态范围值。URP支持多种Tonemapping算法:

  • ACES:电影级色调映射
  • Neutral:中性色调映射
  • ACES Legacy:旧版ACES算法

Depth of Field(景深)

景深模拟相机的焦点效果,使前景或背景产生模糊。关键参数包括:

  • Focus Distance:焦点距离
  • Aperture:光圈大小
  • Focal Length:焦距
  • Kernel Size:模糊核大小

Motion Blur(运动模糊)

运动模糊模拟快速移动时的模糊效果,增强动态感。包含:

  • Shutter Angle:快门角度
  • Sample Count:采样数量
  • Velocity Scale:速度缩放

其他效果

  • Vignette(暗角):边缘暗化效果
  • Chromatic Aberration(色差):颜色边缘分离效果
  • Grain(颗粒):胶片颗粒效果
  • Lift Gamma Gain:色彩调整
  • Split Toning:分离色调

7.3 全局Volume与局部Volume

URP支持两种类型的Volume:

  • 全局Volume:影响整个场景,优先级较低
  • 局部Volume:只影响特定区域,优先级较高

Volume的混合规则:

  1. 按优先级排序(高优先级覆盖低优先级)
  2. 相同优先级时,根据权重混合
  3. 使用Blend Distance实现平滑过渡

7.4 性能考虑

后处理效果对性能有显著影响,需要合理配置:

  • 避免在移动设备上使用过多后处理效果
  • 调整Sample Count以平衡质量和性能
  • 在性能受限时禁用非关键后处理效果
  • 使用LOD系统根据距离调整后处理质量

代码示例

7.5 创建自定义后处理效果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;

[System.Serializable, VolumeComponentMenu("Custom Post-Processing/CustomEffect")]
public class CustomEffect : VolumeComponent, IPostProcessComponent
{
public ClampedFloatParameter intensity = new ClampedFloatParameter(0f, 0f, 1f);
public ColorParameter color = new ColorParameter(Color.white);
public BoolParameter enabled = new BoolParameter(false);

public bool IsActive() => enabled.value && intensity.value > 0f;

public bool IsTileCompatible() => false;
}

public class CustomEffectRenderer : VolumeRenderer<CustomEffect>
{
static class ShaderIDs
{
internal static readonly int _Intensity = Shader.PropertyToID("_Intensity");
internal static readonly int _Color = Shader.PropertyToID("_Color");
}

public override void Render(CommandBuffer cmd, ref RenderingData renderingData,
ref PostProcessData postProcessData, TextureDescriptor sourceDescriptor,
RenderTargetIdentifier source, RenderTargetIdentifier destination)
{
var sheet = postProcessData.propertySheets.Get(Shader.Find("Custom/CustomEffect"));

sheet.ClearKeywords();
sheet.EnableKeyword("CUSTOM_EFFECT_ON");

sheet.SetFloat(ShaderIDs._Intensity, m_Settings.intensity.value);
sheet.SetColor(ShaderIDs._Color, m_Settings.color.value);

cmd.Blit(source, destination, sheet, 0);
}
}

7.6 自定义后处理Shader

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
Shader "Custom/CustomEffect"
{
Properties
{
_MainTex ("Source", 2D) = "white" {}
}

SubShader
{
Tags { "RenderType" = "Opaque" "RenderPipeline" = "UniversalPipeline" }

Pass
{
Name "CustomEffect"

HLSLPROGRAM
#pragma vertex Vert
#pragma fragment Frag
#pragma multi_compile _ _MAIN_LIGHT_SHADOWS
#pragma multi_compile _ _MAIN_LIGHT_SHADOWS_CASCADE
#pragma multi_compile _ _SHADOWS_SOFT

#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"

TEXTURE2D(_MainTex);
SAMPLER(sampler_MainTex);
float4 _MainTex_TexelSize;

CBUFFER_START(UnityPerMaterial)
float _Intensity;
float4 _Color;
CBUFFER_END

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;

#if UNITY_UV_STARTS_AT_TOP
output.uv.y = 1.0 - output.uv.y;
#endif

return output;
}

half4 Frag(Varyings input) : SV_Target
{
half4 color = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, input.uv);

// 应用自定义效果
half3 effect = color.rgb * _Color.rgb * _Intensity;
color.rgb += effect;

return color;
}
ENDHLSL
}
}
}

7.7 Volume组件脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
using UnityEngine;
using UnityEngine.Rendering;

public class VolumeController : MonoBehaviour
{
[Header("Volume Settings")]
public Volume volume;
public float blendDistance = 1f;
public LayerMask volumeLayer = -1;

[Header("Priority Settings")]
public float priority = 0f;

private void Start()
{
if (volume == null)
volume = GetComponent<Volume>();

if (volume != null)
{
volume.blendDistance = blendDistance;
volume.priority = priority;
}

// 设置碰撞层
gameObject.layer = volumeLayer.value;
}

// 动态调整Volume权重
public void SetWeight(float weight)
{
if (volume != null)
{
volume.weight = Mathf.Clamp01(weight);
}
}

// 激活/停用Volume
public void SetActive(bool active)
{
if (volume != null)
{
volume.enabled = active;
}
}
}

7.8 后处理效果切换管理器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;

public class PostProcessManager : MonoBehaviour
{
[System.Serializable]
public class PostProcessPreset
{
public string name;
public VolumeProfile profile;
[Range(0f, 1f)]
public float weight = 1f;
}

[Header("Presets")]
public List<PostProcessPreset> presets = new List<PostProcessPreset>();

[Header("Current Settings")]
public Camera targetCamera;
public VolumeProfile currentProfile;

private Volume currentVolume;
private Dictionary<string, VolumeProfile> profileMap = new Dictionary<string, VolumeProfile>();

private void Start()
{
Initialize();
}

private void Initialize()
{
if (targetCamera == null)
targetCamera = Camera.main;

// 构建预设映射表
foreach (var preset in presets)
{
if (preset.profile != null)
{
profileMap[preset.name] = preset.profile;
}
}

// 创建临时Volume用于运行时切换
GameObject volumeGO = new GameObject("RuntimeVolume");
volumeGO.transform.SetParent(targetCamera.transform);
volumeGO.transform.localPosition = Vector3.zero;
volumeGO.transform.localRotation = Quaternion.identity;

currentVolume = volumeGO.AddComponent<Volume>();
currentVolume.sharedProfile = currentProfile;
currentVolume.isGlobal = true;
currentVolume.priority = 1000f; // 确保优先级最高
}

public void SwitchToPreset(string presetName)
{
if (profileMap.ContainsKey(presetName))
{
currentVolume.profile = profileMap[presetName];
}
}

public void SetPresetWeight(string presetName, float weight)
{
if (currentVolume.profile != null)
{
currentVolume.weight = Mathf.Clamp01(weight);
}
}

public void EnablePostProcess(bool enable)
{
if (currentVolume != null)
{
currentVolume.enabled = enable;
}
}

// 性能优化:根据平台调整后处理质量
public void SetQualityLevel(int qualityLevel)
{
switch (qualityLevel)
{
case 0: // 低质量
DisableExpensiveEffects();
break;
case 1: // 中等质量
EnableModerateEffects();
break;
case 2: // 高质量
EnableAllEffects();
break;
}
}

private void DisableExpensiveEffects()
{
if (currentProfile != null)
{
var motionBlur = currentProfile.GetComponent<MotionBlur>();
if (motionBlur != null) motionBlur.active = false;

var depthOfField = currentProfile.GetComponent<DepthOfField>();
if (depthOfField != null) depthOfField.active = false;
}
}

private void EnableModerateEffects()
{
if (currentProfile != null)
{
var bloom = currentProfile.GetComponent<Bloom>();
if (bloom != null) bloom.active = true;
}
}

private void EnableAllEffects()
{
if (currentProfile != null)
{
var components = currentProfile.components;
foreach (var component in components)
{
component.SetActive(true);
}
}
}
}

实践练习

7.9 练习1:创建基础后处理效果

目标:创建一个包含Bloom、Color Grading和Vignette的后处理配置

步骤

  1. 在场景中创建一个空对象,添加Volume组件
  2. 将Volume设置为全局(isGlobal = true)
  3. 创建新的Volume Profile
  4. 添加Bloom、Color Grading、Vignette三个组件
  5. 调整各组件参数,观察效果变化

具体参数设置

  • Bloom:Intensity=0.5, Threshold=0.9, Scatter=0.7
  • Color Grading:Tone=ACES, Temperature=10, Tint=5
  • Vignette:Intensity=0.3, Smoothness=0.2, Roundness=1.0

7.10 练习2:创建局部Volume效果

目标:创建一个雾效Volume,仅在特定区域生效

步骤

  1. 创建一个Capsule对象作为雾效区域
  2. 添加Volume组件,设置为非全局
  3. 创建新的Volume Profile,添加Fog组件
  4. 调整Volume的Blend Distance实现平滑过渡
  5. 在场景中放置测试对象,观察雾效的区域性

参数设置

  • Fog:Color=淡蓝色, Density=0.1, Start Distance=0
  • Volume:Blend Distance=5,Priority=10

7.11 练习3:实现昼夜切换效果

目标:创建白天和夜晚两种后处理预设,实现无缝切换

步骤

  1. 创建两个Volume Profile:DayProfile和NightProfile
  2. DayProfile设置:Color Grading偏向暖色调,Bloom强度适中
  3. NightProfile设置:Color Grading偏向冷色调,增加Grain效果
  4. 编写脚本实现平滑切换
  5. 添加UI按钮控制切换

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;

public class DayNightSwitcher : MonoBehaviour
{
[Header("Profiles")]
public VolumeProfile dayProfile;
public VolumeProfile nightProfile;

[Header("Settings")]
public AnimationCurve transitionCurve = AnimationCurve.EaseInOut(0, 0, 1, 1);
public float transitionDuration = 2f;

private Volume volume;
private float currentWeight = 0f;
private bool isTransitioning = false;

void Start()
{
// 创建运行时Volume
GameObject volumeGO = new GameObject("DayNightVolume");
volume = volumeGO.AddComponent<Volume>();
volume.isGlobal = true;
volume.priority = 999f;
}

public void SwitchToDay()
{
StartCoroutine(TransitionToProfile(dayProfile, 0f));
}

public void SwitchToNight()
{
StartCoroutine(TransitionToProfile(nightProfile, 1f));
}

System.Collections.IEnumerator TransitionToProfile(VolumeProfile profile, float targetWeight)
{
if (isTransitioning) yield break;

isTransitioning = true;
float elapsedTime = 0f;

while (elapsedTime < transitionDuration)
{
elapsedTime += Time.deltaTime;
float progress = elapsedTime / transitionDuration;
float curveValue = transitionCurve.Evaluate(progress);

// 根据目标权重调整
float currentWeight = targetWeight == 1f ?
Mathf.Lerp(0f, 1f, curveValue) :
Mathf.Lerp(1f, 0f, curveValue);

volume.weight = currentWeight;
volume.profile = profile;

yield return null;
}

volume.weight = targetWeight;
isTransitioning = false;
}
}

7.12 练习4:性能优化测试

目标:测试不同后处理效果对性能的影响

步骤

  1. 创建性能测试场景(包含多个对象)
  2. 分别启用不同的后处理效果组合
  3. 使用Unity Profiler记录性能数据
  4. 对比启用/禁用后处理的效果
  5. 记录FPS变化和渲染时间

测试配置

  • 基准:无后处理效果
  • 配置1:仅Bloom
  • 配置2:Bloom + Color Grading
  • 配置3:完整后处理栈(Bloom, Color Grading, DOF, Motion Blur, Vignette)

总结

第7章详细介绍了URP的后处理系统,这是提升视觉质量的重要工具。Volume框架提供了灵活的后处理管理机制,允许开发者创建全局或局部的后处理效果,并支持多Volume的混合。

关键要点总结:

  1. Volume系统:基于优先级和权重的后处理管理框架
  2. 主要效果:Bloom、Color Grading、Tonemapping等核心后处理效果
  3. 性能考虑:后处理对性能有显著影响,需根据平台合理配置
  4. 自定义扩展:通过VolumeComponent和VolumeRenderer可扩展自定义效果
  5. 实践应用:结合代码实现动态后处理切换和优化

后处理系统是URP中实现电影级视觉效果的关键组件,掌握其原理和应用对于提升游戏视觉品质至关重要。在实际项目中,需要在视觉效果和性能之间找到平衡点,根据目标平台的性能特点合理配置后处理效果。

下一章将深入探讨URP相机系统与渲染顺序,了解如何管理多相机渲染和渲染队列。