本文最后更新于 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,它们的生命周期函数都包含了一些比较重要的参数类型。下面进行解释。
用于记录和执行渲染命令的工具。通过它可以在渲染过程中插入自定义的绘制指令。常用的指令有:DrawMesh
、DrawRenderer
、SetGlobalTexture
、Clear
等。
使用CommandBufferPool.Get(“名称”)
在不包含CommandBuffer的函数中获取命令缓冲区。
包含了当前渲染的所有必要数据的结构体。内含当前相机、RT与光源等的信息。
常用的字段有cameraData
、cullResults
、lightData
等。时常利用cameraData
来设置RT、调整相机属性等。
用于与渲染管线交互的核心结构。
常用方法有ExecuteCommandBuffer
、DrawRenderers
、Submit
等。
常用于提交命令缓冲区、控制渲染流程。
执行渲染操作的具体实现。
常用字段/方法有:cameraColorTargetHandle
、cameraDepthTargetHandle
、EnqueuePass()
等。
包含了关于相机的具体数据。
常用字段有:camera
、pixelRect
、projectionMatrix
、viewMatrix
、cameraType
等。
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
代替RenderTargetHandle
和RenderTargetIdentifier
,用于标记一个Render Target。
要分配一张临时RT,首先需要一个定义了RT格式的结构体,在URP中表现为RenderTextureDescriptor
类型。在进行全屏后处理的情况下,我们直接使用下列代码:
1 2 RenderTextureDescriptor opaqueDesc = renderingData.cameraData.cameraTargetDesc; opaqueDesc.depthBufferBits = 0 ;
随后,使用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.SetGlobalTexture
或material.SetTexture()
传入Shader。
需要注意的是,Blitter
API调用后同样需要context.ExecuteCommandBuffer(cmd)
进行提交。
Shader
若通过Blitter.BlitCameraTexture(RTHandle cameraColor, RTHandle cameraColor, Material processMat, int passIndex)
进行全屏后处理,有以下需要注意的点:
设置Render Type为Opaque,RenderPipeline为UniversalPipeline
引入"Packages/com.unity.render-pipelines.core/Runtime/Utilities/Blit.hlsl"
头文件
去除原本的Attributes和Varyings结构体和原本的vert函数
设置#pragma vertex Vert
使用TEXTURE2D_X
采样_CameraOpaqueTexture
在frag中,首先调用UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(input);
然后,使用SAMPLE_TEXTURE_2D_X
对_CameraOpaqueTexture
采样
撰写后处理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 { [System.Serializable ] public class CustomRenderFeatureSettings { public Color baseColor = Color.white; public RenderPassEvent renderPassEvent = RenderPassEvent.AfterRenderingOpaques; public Material baseMat; } public CustomRenderFeatureSettings settings; CustomRenderPass m_ScriptablePass; public override void Create () { m_ScriptablePass = new CustomRenderPass(settings.baseMat, settings.renderPassEvent); } public override void SetupRenderPasses (ScriptableRenderer renderer, in RenderingData renderingData ) { m_ScriptablePass.InitMatParams(settings.baseColor); } public override void AddRenderPasses (ScriptableRenderer renderer, ref RenderingData renderingData ) { if (renderingData.cameraData.cameraType == CameraType.Game) { renderer.EnqueuePass(m_ScriptablePass); } } class CustomRenderPass : ScriptableRenderPass { public Color baseColor = Color.white; public Material baseMat; private RTHandle m_CameraColorAttachment; public CustomRenderPass (Material mat, RenderPassEvent passEvent ) { this .baseMat = mat; renderPassEvent = passEvent; } public override void OnCameraSetup (CommandBuffer cmd, ref RenderingData renderingData ) { ConfigureInput(ScriptableRenderPassInput.Color); m_CameraColorAttachment = renderingData.cameraData.renderer.cameraColorTargetHandle; } public void InitMatParams (Color color ) { this .baseColor = color; } public override void Execute (ScriptableRenderContext context, ref RenderingData renderingData ) { CommandBuffer cmd = CommandBufferPool.Get("CustomRenderPass" ); baseMat.SetColor("_BaseColor" , baseColor); Blitter.BlitCameraTexture(cmd, m_CameraColorAttachment, m_CameraColorAttachment, baseMat, 0 ); context.ExecuteCommandBuffer(cmd); cmd.Clear(); CommandBufferPool.Release(cmd); } 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集成,需要遵循下列步骤:
首先创建继承自VolumeComponent
、IPostProcessComponent
的类,添加[Serializable, VolumeComponent(“一级菜单/后处理名称”)]
。
实现IsActive()
和IsTileCompatible()
方法。对于前者,需要额外定义一个BoolParameter
类型的字段,并将IsActive()
的返回值设置为该字段。对于后者,通常设置为返回false。
随后,添加后处理所需的材质参数。需要注意的是,不可用普通类型定义参数,而是用诸如BoolParameter
、ColorParameter
、IntParameter
、FloatParamter
、CubeParameter
、TextureParameter
等类型定义,否则这些属性将无法暴露在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); public bool IsActive () { return enableEffect.value ; } 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 { [System.Serializable ] public class CustomRenderFeatureSettings { public Color baseColor = Color.white; public RenderPassEvent renderPassEvent = RenderPassEvent.AfterRenderingOpaques; public Material baseMat; } public CustomRenderFeatureSettings settings; CustomRenderPass m_ScriptablePass; public override void Create () { m_ScriptablePass = new CustomRenderPass(settings.baseMat, settings.renderPassEvent); } 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); } } 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; } class CustomRenderPass : ScriptableRenderPass { public Color baseColor = Color.white; public Material baseMat; private RTHandle m_CameraColorAttachment; public CustomRenderPass (Material mat, RenderPassEvent passEvent ) { this .baseMat = mat; renderPassEvent = passEvent; } public override void OnCameraSetup (CommandBuffer cmd, ref RenderingData renderingData ) { ConfigureInput(ScriptableRenderPassInput.Color); m_CameraColorAttachment = renderingData.cameraData.renderer.cameraColorTargetHandle; } public void InitMatParams (Color color ) { this .baseColor = color; } public override void Execute (ScriptableRenderContext context, ref RenderingData renderingData ) { CommandBuffer cmd = CommandBufferPool.Get("CustomRenderPass" ); baseMat.SetColor("_BaseColor" , baseColor); Blitter.BlitCameraTexture(cmd, m_CameraColorAttachment, m_CameraColorAttachment, baseMat, 0 ); context.ExecuteCommandBuffer(cmd); cmd.Clear(); CommandBufferPool.Release(cmd); } public override void OnCameraCleanup (CommandBuffer cmd ) { } } }
Tips
通过[System.Serializable]属性,我们可以将一个自定义类作为Pass的设置,暴露在Inspector中,同时减少Pass初始化的传参数量。
using (new ProfilingScope(cmd, m_ProfilingSampler))
块可以让其中的代码在Frame Dubugger上有独特标记。其中,m_ProfilingSampler
为ProfilingSampler
类型的字段,其构造函数仅包含一个字符串,用于Frame Debugger的标识。
在Execute
中,我们通过CommandBufferPool.Get()
方法获取命令缓冲区,以进行SetGlobalFloat
等操作。通过这种方式获取的cmd
必须用CommandBufferPool.Release()
方法进行释放。
在Execute
中,记得使用cameraData.camera.cameraType
判断此Pass生效的相机类型。常见的是仅在CameraType.Game
中生效。