初探URP(三) - RenderFeature进阶与全屏后处理模板

本文最后更新于 2025年2月12日 下午

Render Feature详解

Render Feature表现为一个C#类,继承自ScriptableRenderFeature类。该类通常包含一个继承自ScriptableRenderPass的内部类定义,用于定义需要向管线内插入的Pass。同时,也会声明一个此Pass类的变量。

1
2
3
4
5
6
public class CustomRenderFeature : ScriptableRenderFeature{
public class CustomRenderPass : ScriptableRenderPass{
//类定义
}
CustomRenderPass customRenderPass; //变量
}

无论是Feature还是Pass,它们的生命周期函数都包含了一些比较重要的参数类型。下面进行解释。

  • CommandBuffer

用于记录和执行渲染命令的工具。通过它可以在渲染过程中插入自定义的绘制指令。常用的指令有:DrawMeshDrawRendererSetGlobalTextureClear等。

使用CommandBufferPool.Get(“名称”)在不包含CommandBuffer的函数中获取命令缓冲区。

  • RenderingData

包含了当前渲染的所有必要数据的结构体。内含当前相机、RT与光源等的信息。

常用的字段有cameraDatacullResultslightData等。时常利用cameraData来设置RT、调整相机属性等。

  • ScriptableRenderContext

用于与渲染管线交互的核心结构。

常用方法有ExecuteCommandBufferDrawRenderersSubmit等。

常用于提交命令缓冲区、控制渲染流程。

  • ScriptableRenderer

执行渲染操作的具体实现。

常用字段/方法有:cameraColorTargetHandlecameraDepthTargetHandleEnqueuePass()等。

  • CameraData

包含了关于相机的具体数据。

常用字段有:camerapixelRectprojectionMatrixviewMatrixcameraType等。

Pass类

生命周期函数

Pass类包含下列生命周期函数。

函数 描述 作用
OnCameraSetup(CommandBuffer cmd, ref RenderingData renderingData) 在调用一个相机的绘制过程之前执行。每帧执行。 用于改变相机的Render Target、Clear Flag,以及为临时RT分配内存
Configure(CommandBuffer cmd, RenderTextureDescriptor cameraTextureDescriptor) 在执行一个Pass前执行 OnCameraSetup基本一致
Execute(ScriptableRenderContext context, ref RenderingData renderingData) 执行Pass。每帧执行。 通常执行DrawMesh、Blit等操作。
OnCameraCleanUp(CommandBuffer cmd) 完成一个相机的绘制过程后调用。每帧执行 释放任何临时资源
OnFinishCameraStackRendering(CommandBuffer cmd) 完成一个相机栈的绘制过程后调用 一次性释放Pass中与相机栈中所有相机有关的临时资源

Feature类

生命周期函数

函数 描述 作用
Create() Render Feature初始化时调用 定义Feature名、创建材质、初始化Pass
OnCameraPreCull(ScriptableRenderer renderer, in CameraData cameraData) 在相机做剔除之前调用 实现一些特殊效果
AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData) 每帧调用 根据Pass的renderPassEvent,按时机将Pass注入渲染器。此处禁止传递RTHandle
SetupRenderPasses(ScriptableRenderer renderer, in RenderingData renderingData) 每帧调用 将源RT(已初始化)传入Pass
Dispose(bool disposing) 每帧调用 清理资源

应用全屏后处理

在Built-in中,我们通过OnRenderImage、Shader和Blit的结合进行后处理。

在URP中,我们借助Render Feature、Render Pass和Blitter API进行后处理。

RTHandle

在URP14+中,我们使用RTHandle代替RenderTargetHandleRenderTargetIdentifier,用于标记一个Render Target。

要分配一张临时RT,首先需要一个定义了RT格式的结构体,在URP中表现为RenderTextureDescriptor类型。在进行全屏后处理的情况下,我们直接使用下列代码:

1
2
RenderTextureDescriptor opaqueDesc = renderingData.cameraData.cameraTargetDesc;
opaqueDesc.depthBufferBits = 0; // RTHandle处理的RT无法将深度与颜色绑定在一起。

随后,使用RenderingUtils提供的API分配临时RT:

1
RenderingUtils.ReAllocateIfNeeded(ref destination, cameraTextureDescriptor, name: "_BaseMap");

我们使用opaqueDesc结构体描述RT参数,并指定了它的过滤、环绕模式,同时指定了该RT的属性名,用于在Frame Debugger中检视。

使用RTHandles.Alloc分配的了临时RT必须在OnCameraCleanup()中释放。

Blitter

在URP14+中,使用Blitter API取代cmd.Blit

假设我们有一个名为rt_Blur01的源RTHandle,一个名为rt_Blur02的目标RTHandle,一个名为blurShader的用于后处理的Shader,我们应当遵循下列步骤:

在Feature的AddRenderPass中,使用CoreUtils.CreateEngineMaterial(blurShader)创建运行时材质blurMat

将运行时材质传入Pass的初始化方法,并给Pass的m_blurMat字段赋值。

在Pass的Execute方法中,首先使用cmd.SetGlobalFloat()m_blurMat.SetFloat()接口设置材质属性。

然后,使用**Blitter.BlitCameraTexture(cmd, rt_Blur01, rt_Blur02, m_blurMat, 0)**接口进行Blit操作。

此时,rt_Blur02存储着我们需要的Render Texture。我们可以通过rt_Blur02.nameID获取其RenderTargetIdentifier,并将其作为参数,借助cmd.SetGlobalTexturematerial.SetTexture()传入Shader。

需要注意的是,Blitter API调用后同样需要context.ExecuteCommandBuffer(cmd)进行提交。

Shader

若通过Blitter.BlitCameraTexture(RTHandle cameraColor, RTHandle cameraColor, Material processMat, int passIndex)进行全屏后处理,有以下需要注意的点:

  1. 设置Render Type为Opaque,RenderPipeline为UniversalPipeline
  2. 引入"Packages/com.unity.render-pipelines.core/Runtime/Utilities/Blit.hlsl"头文件
  3. 去除原本的Attributes和Varyings结构体和原本的vert函数
  4. 设置#pragma vertex Vert
  5. 使用TEXTURE2D_X采样_CameraOpaqueTexture
  6. 在frag中,首先调用UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(input);
  7. 然后,使用SAMPLE_TEXTURE_2D_X_CameraOpaqueTexture采样
  8. 撰写后处理Shader代码

模板代码

RenderFeature部分
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
using UnityEngine;
using UnityEngine.Experimental.Rendering;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;

public class URPPostProcessTemplateRenderFeature : ScriptableRendererFeature
{
/// <summary>
/// 定义了Feature的设置。
/// 可以将全局材质属性和渲染事件设置在这里。
/// </summary>
[System.Serializable]
public class CustomRenderFeatureSettings
{
public Color baseColor = Color.white;
public RenderPassEvent renderPassEvent = RenderPassEvent.AfterRenderingOpaques;
public Material baseMat;
}

public CustomRenderFeatureSettings settings; // Feature的设置
CustomRenderPass m_ScriptablePass; // 自定义的RenderPass实例

/// <summary>
/// 在这里创建Pass实例,向构造函数传递材质。
/// </summary>
public override void Create()
{
m_ScriptablePass = new CustomRenderPass(settings.baseMat, settings.renderPassEvent);
}

/// <summary>
/// 在这里初始化Pass实例
/// </summary>
public override void SetupRenderPasses(ScriptableRenderer renderer, in RenderingData renderingData)
{
m_ScriptablePass.InitMatParams(settings.baseColor);
}

/// <summary>
/// 在这里进行相机类型判断,并将Pass添加到渲染队列中。
/// </summary>
public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData)
{
if (renderingData.cameraData.cameraType == CameraType.Game)
{
renderer.EnqueuePass(m_ScriptablePass);
}
}

/// <summary>
/// 自定义的RenderPass类
/// </summary>
class CustomRenderPass : ScriptableRenderPass
{
// ---这里定义材质属性和RenderTexture---
public Color baseColor = Color.white;
public Material baseMat;

private RTHandle m_CameraColorAttachment;

/// <summary>
/// 构造函数,用于创建Pass实例,并传入基本参数。
/// </summary>
/// <param name="mat">进行后处理的材质</param>
public CustomRenderPass(Material mat, RenderPassEvent passEvent)
{
this.baseMat = mat;
renderPassEvent = passEvent;
}

/// <summary>
/// 每帧调用,用于给RT_Handle赋值。
/// </summary>
public override void OnCameraSetup(CommandBuffer cmd, ref RenderingData renderingData)
{
ConfigureInput(ScriptableRenderPassInput.Color);
m_CameraColorAttachment = renderingData.cameraData.renderer.cameraColorTargetHandle;
//----若需要分配临时RT,请在这里调用RenderingUtils.ReAllocateIfNeeded()----

//-------------------------------------------------------------------
}

/// <summary>
/// 在这里初始化材质属性
/// </summary>
public void InitMatParams(Color color)
{
this.baseColor = color;
}

/// <summary>
/// 类比于OnRenderImage(),这里用于Blit操作。
/// </summary>
/// <param name="context"></param>
/// <param name="renderingData"></param>
public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
{
// 获取命令缓冲区
CommandBuffer cmd = CommandBufferPool.Get("CustomRenderPass");
// ----这里进行Blit操作----
baseMat.SetColor("_BaseColor", baseColor);
Blitter.BlitCameraTexture(cmd, m_CameraColorAttachment, m_CameraColorAttachment, baseMat, 0);
// ---------------------
//执行命令
context.ExecuteCommandBuffer(cmd);
cmd.Clear(); // 清空命令缓冲区
CommandBufferPool.Release(cmd); // 释放命令缓冲区
}

/// <summary>
/// 如果分配了临时RT,需要在这里释放。
/// </summary>
/// <param name="cmd"></param>
public override void OnCameraCleanup(CommandBuffer cmd)
{

}
}
}
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
Shader "YoiToolkit/URPPostProcessingTemplate"
{
Properties
{
_BaseColor("Base Color",Color) = (1,1,1,1)
}
SubShader
{
tags{"RenderType"="Opaque" "RenderPipeline"="UniversalPipeline"}
ZWrite Off
Pass
{
HLSLINCLUDE

#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
#include "Packages/com.unity.render-pipelines.core/Runtime/Utilities/Blit.hlsl"

CBUFFER_START(UnityPerMaterial)
half4 _BaseColor;
CBUFFER_END

ENDHLSL


HLSLPROGRAM
#pragma vertex Vert
#pragma fragment frag

TEXTURE2D_X(_CameraOpaqueTexture);
SAMPLER(sampler_CameraOpaqueTexture);

half4 frag(Varyings IN):SV_Target
{
UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(input);
half4 color = SAMPLE_TEXTURE2D_X(_CameraOpaqueTexture, sampler_CameraOpaqueTexture, IN.texcoord);
return color * _BaseColor;
}
ENDHLSL
}
}
}

与URP Volume集成

URP提供了Volume组件,可以快速应用全屏后处理,也支持Box Volume的区域转换。

要想让自己写的全屏后处理Feature与URP Volume集成,需要遵循下列步骤:

  • 定义Volume组件。

首先创建继承自VolumeComponentIPostProcessComponent的类,添加[Serializable, VolumeComponent(“一级菜单/后处理名称”)]

实现IsActive()IsTileCompatible()方法。对于前者,需要额外定义一个BoolParameter类型的字段,并将IsActive()的返回值设置为该字段。对于后者,通常设置为返回false。

随后,添加后处理所需的材质参数。需要注意的是,不可用普通类型定义参数,而是用诸如BoolParameterColorParameterIntParameterFloatParamterCubeParameterTextureParameter等类型定义,否则这些属性将无法暴露在Volume的Inspector中。

  • 修改Feature,使其能够获取Stack中的该组件并获取其中的属性。

通过VolumeManager.instance.stack可以获取后处理Stack。对Stack使用GetComponent<Volume类>即可获取刚才定义的Volume组件。随后,便可以拿到组件中的属性。

1
2
3
4
5
6
public T GetVolume<T>() where T : VolumeComponent
{
var stack = VolumeManager.instance.stack;
T component = stack.GetComponent<T>();
return component;
}
1
2
3
4
5
URPVolumeComponentTemplate component = GetVolume<URPVolumeComponentTemplate>();
if (component)
{
m_ScriptablePass.InitMatParams(component.baseColor.value);
}

完整的模板代码如下:

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
using System;
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;

namespace YoiToolKit.Rendering
{
[Serializable,VolumeComponentMenu("CustomPostStack/Template")]
public class URPVolumeComponentTemplate : VolumeComponent, IPostProcessComponent
{
private BoolParameter enableEffect = new(true); // 是否启用。必选。
// -----此处放置后处理所需的材质参数-----
public ColorParameter baseColor = new ColorParameter(Color.white);
// --------------------------------

/// <summary>
/// 是否启用。
/// </summary>
public bool IsActive()
{
return enableEffect.value;
}

/// <summary>
/// 是否兼容TBDR。
/// </summary>
public bool IsTileCompatible()
{
return false;
}
}
}
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
132
133
134
135
using UnityEngine;
using UnityEngine.Experimental.Rendering;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;
using YoiToolKit.Rendering;

public class URPPostProcessTemplateRenderFeature : ScriptableRendererFeature
{
/// <summary>
/// 定义了Feature的设置。
/// 可以将全局材质属性和渲染事件设置在这里。
/// </summary>
[System.Serializable]
public class CustomRenderFeatureSettings
{
public Color baseColor = Color.white;
public RenderPassEvent renderPassEvent = RenderPassEvent.AfterRenderingOpaques;
public Material baseMat;
}

public CustomRenderFeatureSettings settings; // Feature的设置
CustomRenderPass m_ScriptablePass; // 自定义的RenderPass实例

/// <summary>
/// 在这里创建Pass实例,向构造函数传递材质。
/// </summary>
public override void Create()
{
m_ScriptablePass = new CustomRenderPass(settings.baseMat, settings.renderPassEvent);
}

/// <summary>
/// 在这里初始化Pass实例
/// </summary>
public override void SetupRenderPasses(ScriptableRenderer renderer, in RenderingData renderingData)
{
URPVolumeComponentTemplate component = GetVolume<URPVolumeComponentTemplate>();
if (component)
{
m_ScriptablePass.InitMatParams(component.baseColor.value);
}
else
{
m_ScriptablePass.InitMatParams(settings.baseColor);
}
}

/// <summary>
/// 在这里进行相机类型判断,并将Pass添加到渲染队列中。
/// </summary>
public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData)
{
if (renderingData.cameraData.cameraType == CameraType.Game)
{
renderer.EnqueuePass(m_ScriptablePass);
}
}

public T GetVolume<T>() where T : VolumeComponent
{
var stack = VolumeManager.instance.stack;
T component = stack.GetComponent<T>();
return component;
}

/// <summary>
/// 自定义的RenderPass类
/// </summary>
class CustomRenderPass : ScriptableRenderPass
{
// ---这里定义材质属性和RenderTexture---
public Color baseColor = Color.white;
public Material baseMat;

private RTHandle m_CameraColorAttachment;

/// <summary>
/// 构造函数,用于创建Pass实例,并传入基本参数。
/// </summary>
/// <param name="mat">进行后处理的材质</param>
public CustomRenderPass(Material mat, RenderPassEvent passEvent)
{
this.baseMat = mat;
renderPassEvent = passEvent;
}

/// <summary>
/// 每帧调用,用于给RT_Handle赋值。
/// </summary>
public override void OnCameraSetup(CommandBuffer cmd, ref RenderingData renderingData)
{
ConfigureInput(ScriptableRenderPassInput.Color);
m_CameraColorAttachment = renderingData.cameraData.renderer.cameraColorTargetHandle;
//----若需要分配临时RT,请在这里调用RenderingUtils.ReAllocateIfNeeded()----

//-------------------------------------------------------------------
}

/// <summary>
/// 在这里初始化材质属性
/// </summary>
public void InitMatParams(Color color)
{
this.baseColor = color;
}

/// <summary>
/// 类比于OnRenderImage(),这里用于Blit操作。
/// </summary>
/// <param name="context"></param>
/// <param name="renderingData"></param>
public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
{
// 获取命令缓冲区
CommandBuffer cmd = CommandBufferPool.Get("CustomRenderPass");
// ----这里进行Blit操作----
baseMat.SetColor("_BaseColor", baseColor);
Blitter.BlitCameraTexture(cmd, m_CameraColorAttachment, m_CameraColorAttachment, baseMat, 0);
// ---------------------
//执行命令
context.ExecuteCommandBuffer(cmd);
cmd.Clear(); // 清空命令缓冲区
CommandBufferPool.Release(cmd); // 释放命令缓冲区
}

/// <summary>
/// 如果分配了临时RT,需要在这里释放。
/// </summary>
/// <param name="cmd"></param>
public override void OnCameraCleanup(CommandBuffer cmd)
{

}
}
}

Tips

  • 通过[System.Serializable]属性,我们可以将一个自定义类作为Pass的设置,暴露在Inspector中,同时减少Pass初始化的传参数量。
  • using (new ProfilingScope(cmd, m_ProfilingSampler))块可以让其中的代码在Frame Dubugger上有独特标记。其中,m_ProfilingSamplerProfilingSampler类型的字段,其构造函数仅包含一个字符串,用于Frame Debugger的标识。
  • Execute中,我们通过CommandBufferPool.Get()方法获取命令缓冲区,以进行SetGlobalFloat等操作。通过这种方式获取的cmd必须用CommandBufferPool.Release()方法进行释放。
  • Execute中,记得使用cameraData.camera.cameraType判断此Pass生效的相机类型。常见的是仅在CameraType.Game中生效。

初探URP(三) - RenderFeature进阶与全屏后处理模板
http://example.com/2025/02/12/初探URP(三)-RenderFeature进阶与全屏后处理模板/
作者
Yoi
发布于
2025年2月12日
许可协议