Unity URP14.0 自定义后措置系统
Unity版本:2022URP版本:14.0.6
项目连接:
场景来源网络
在Unity官方文档中,给出了两种使用后措置的方式。第一种使用Global Volume,但仅限于使用内置后措置,自定义后措置需要改削URP,十分麻烦。第二种使用自定义RenderFeature添加自定义RenderPass,但一个后措置效果对应两个脚本,而且会带来多个RenderPass,不竭纹理申请和获取的消耗。
在这个基础上,我们创建一个本身的后措置系统,使得Renderer挂载一个RenderFeature,而且一个后措置效果只需要创建一个脚本。
建议先过一遍官方文档,出格是三部门,Render feature、Volume和Fullscreen blit部门。
官方文档分析
Global Volume 后措置
URP使用Volume框架来措置后措置,但是这些后措置都是URP自带的,也就是在RP里写死了的。
给场景添加一个Global Volume,它用来挂载Volume Profile,而Volume Profile用来挂载具体的后措置实例。通过对分歧场景的Global Volume挂载不异Volume Profile或分歧Volume Profile实现场景不异或分歧的后措置效果。例如下图,给场景添加了一个Bloom效果。
通过查看Bloom.cs源码,发现它是一个担任于VolumeComponent和IPostProcessComponent的子类,而且使用VolumeComponentMenuForRenderPipeline向Volume添加后措置选项。
自定义RendererFeature/Pass
在官方文档的Fullscreen blit中,举了一个用担任于
ScriptableRendererFeature和ScriptableRenderPass的脚本分袂自定义RendererFeature和RendererPass,从而实现一个绿色filter的后措置系统。
此中,担任于ScriptableRendererFeature的脚本代码如下。
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;
internal class ColorBlitRendererFeature : ScriptableRendererFeature{
public Shader m_Shader;
public float m_Intensity;
private Material m_Material;
private ColorBlitPass m_RenderPass = null;
public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData) {
if (renderingData.cameraData.cameraType == CameraType.Game)
// Pass入队
renderer.EnqueuePass(m_RenderPass);
}
public override void SetupRenderPasses(ScriptableRenderer renderer, in RenderingData renderingData) {
// 只对游戏摄像机应用后措置(还有预览摄像机等)
if (renderingData.cameraData.cameraType == CameraType.Game) {
// 设置向pass输入color (m_RenderPass父类)
m_RenderPass.ConfigureInput(ScriptableRenderPassInput.Color);
// 设置RT为相机的color
m_RenderPass.SetTarget(renderer.cameraColorTargetHandle, m_Intensity);
}
}
// 基类的抽象函数 OnEnable和OnValidate时调用
public override void Create() {
// 创建一个附带m_Shader的material
m_Material = CoreUtils.CreateEngineMaterial(m_Shader);
// 创建BiltPass脚本实例
m_RenderPass = new ColorBlitPass(m_Material);
}
protected override void Dispose(bool disposing) {
CoreUtils.Destroy(m_Material);
}
}由于用来自定义RendererFeature,所以这个脚本的用处也显而易见:首先Create()函数将在OnEnable和OnValidate时被调用,它创建一个挂载指定Shader的材质。当为每个camera设置一个renderer时,AddRenderPasses函数将被调用,从而向这个camera的renderer入队自定义RenderPass。当每个camera衬着前,SetupRenderPasses函数将被调用,从而设置自定义RenderPass的衬着源RT和衬着方针RT。
担任于SetupRenderPasses的脚本如下。
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;
internal class ColorBlitPass : ScriptableRenderPass{
// 给profiler入一个新的事件
private ProfilingSampler m_ProfilingSampler = new ProfilingSampler(”ColorBlit”);
private Material m_Material;
// RTHandle,封装了纹理及相关信息,可以认为是CPU端纹理
private RTHandle m_CameraColorTarget;
private float m_Intensity;
public ColorBlitPass(Material material) {
m_Material = material;
// 指定执行这个Pass的时机
renderPassEvent = RenderPassEvent.BeforeRenderingPostProcessing;
}
// 指定进行后措置的target
public void SetTarget(RTHandle colorHandle, float intensity) {
m_CameraColorTarget = colorHandle;
m_Intensity = intensity;
}
// OnCameraSetup是纯虚函数,相机初始化时调用
public override void OnCameraSetup(CommandBuffer cmd, ref RenderingData renderingData) {
// (父类函数)指定pass的render target
ConfigureTarget(m_CameraColorTarget);
}
// Execute时抽象函数,把cmd命令添加到context中(然后进一步送到GPU调用)
public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData) {
var cameraData = renderingData.cameraData;
if (cameraData.cameraType != CameraType.Game)
return;
if (m_Material == null)
return;
// 获取commandbuffer
CommandBuffer cmd = CommandBufferPool.Get();
// 把cmd里执行的命令添加到m_ProfilingSampler定义的profiler块中
// using用来自动释放new的资源
using (new ProfilingScope(cmd, m_ProfilingSampler)) {
m_Material.SetFloat(”_Intensity”, m_Intensity);
// 使用cmd里的命令(设置viewport等,分辩率等),执行m_Material的pass0,将m_CameraColorTarget衬着到m_CameraColorTarget
// 本质上画了一个覆盖屏幕的三角形
Blitter.BlitCameraTexture(cmd, m_CameraColorTarget, m_CameraColorTarget, m_Material, 0);
}
// 把cmd中的命令入到context中
context.ExecuteCommandBuffer(cmd);
// 清空cmd栈
cmd.Clear();
CommandBufferPool.Release(cmd);
}
}此中最重要的是Execute函数,它措置了后措置的逻辑。具体用途看代码。
最后就是具体的ColorBlitShader,很简单就不解释了。
Shader ”ColorBlit”
{
SubShader
{
Tags { ”RenderType”=”Opaque” ”RenderPipeline” = ”UniversalPipeline”}
LOD 100
ZWrite Off Cull Off
Pass
{
Name ”ColorBlitPass”
HLSLPROGRAM
#include ”Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl”
// The Blit.hlsl file provides the vertex shader (Vert),
// input structure (Attributes) and output strucutre (Varyings)
#include ”Packages/com.unity.render-pipelines.core/Runtime/Utilities/Blit.hlsl”
#pragma vertex Vert
#pragma fragment frag
TEXTURE2D_X(_CameraOpaqueTexture);
SAMPLER(sampler_CameraOpaqueTexture);
float _Intensity;
half4 frag (Varyings input) : SV_Target
{
UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(input);
float4 color = SAMPLE_TEXTURE2D_X(_CameraOpaqueTexture, sampler_CameraOpaqueTexture, input.texcoord);
return color * float4(0, _Intensity, 0, 1);
}
ENDHLSL
}
}
}
要应用这个Shader,需要将这个RendererFeature脚本挂载给管线Renderer。
fullscreen blit的效果如下,由于只打开对Game相机的后措置,所以Scene是不能预览的,而且也不管你相机打没打开后措置。
后措置系统设计分析
通过上两个官方文档中的例子,可以发现第二种使用ReanderFeature/Pass来创建一个指定的后措置效果其实就是一个后措置系统的雏形。
然而这样会由一些性能问题。在类似ColorBlitFeature.cs的具体后措置Feature中,它们都在AddRenderPasses()中调用EnqueuePass函数。每个后措置效果城市创建一个RenderPass,即使它们在不异的注入点。这样会导致一些性能问题,例如不竭地请求纹理和创建纹理。
为了解决这个问题,我们可以优化衬着过程。我们可以将不异注入点的后措置效果杂交到一个RenderPass中,然后再通过RendererFeature对这个杂交的RenderPass调用EnqueuePass函数。这样,不异注入点的后措置效果可以共享一个RenderPass,在这个RenderPass的Execute中调用每个具体后措置的衬着逻辑,在不异纹理签名间Blit,从而节省性能消耗。换句话说,让不异注入点的RenderPass包含许多子Pass(子Pass=>一次Blit),每个子Pass措置对应的后措置效果。通过上述描述,可以得出我们只需要向Renderer添加一个RenderFeature即可。
除此之外,这样的设计也很繁琐。每次创建一个新的后措置Shader时,都需要额外创建两个脚本:一个RendererFeature用于创建RenderPass,一个RenderPass用于执行Blit命令。
第一种使用Global Volume的方式就很便利,但仅适用于自带的后措置,要添加自定义后措置可能涉及到管线的改削,不采用。于是有了一个想法,即将第一种和第二种方式杂交一下。
因此,我们想到了一个基本的思路:创建一个后措置基类CustomPostProcessing.cs,让具体的后措置效果担任自这个基类。具体的后措置效果只需要实现本身的衬着逻辑即可。然后,我们需要一个自定义的RendererFeature类来获取所有Volume中CustomPostProcessing.cs子类,并按照它们的具体设置(如注入位置等)创建对应的自定义RenderPass实例。每个RenderPass类在Execute函数中调用对应所有CustomPostProcessing基类的Render函数,从而实现具体后措置脚本的衬着。
大致框架如下。
后措置基类
颠末上面分析,首先需要将CustomPostProcessing的基类添加到Volume的Override菜单里。这里通过前面对Bloom.cs源码的查看。
发现只需要担任VolumeComponent和IPostProcessComponent即可。基类添加VolumeComponentMenuForRenderPipeline()来添加菜单。由于衬着时会生成临时RT,所以还需要担任IDisposable。
public abstract class CustomPostProcessing : VolumeComponent, IPostProcessComponent, IDisposable{
}
然后添加IPostProcessComponent必需要override的一些函数,同样可以通过查看Bloom.cs来填写。(其实是按照不写会报错)
IPostProcessComponent要求定义IsActive()用来返回当前后措置是否active;IsTileCompatible()不知道用来干嘛的,但Bloom.cs里get值false,抄下来就行了。
#region IPostProcessComponent
public abstract bool IsActive();
public virtual bool IsTileCompatible() => false;
#endregion
IDispose部门如下:
#region IDisposable
public void Dispose() {
Dispose(true);
GC.SuppressFinalize(this);
}
public virtual void Dispose(bool disposing) {
}
#endregion
最后为其添加每个公用的属性和函数。
首先是这个后措置效果的注入点,这里先分三个。而且,插手当前后措置在注入点的执行挨次。
public enum CustomPostProcessInjectionPoint{
AfterOpaqueAndSky,
BeforePostProcess,
AfterPostProcess
}
public abstract class CustomPostProcessing : VolumeComponent, IPostProcessComponent, IDisposable{
// 注入点
public virtual CustomPostProcessInjectionPoint InjectionPoint => CustomPostProcessInjectionPoint.AfterPostProcess;
//在注入点的挨次
public virtual int OrderInInjectionPoint => 0;
}
最后,插手Setup配置当前后措置,Render衬着当前后措置。
// 配置当前后措置
public abstract void Setup();
// 执行衬着
public abstract void Render(CommandBuffer cmd, ref RenderingData renderingData, RTHandle source, RTHandle destination);
测试一下子类是否能在Volume组件里显示出来,这里新写一个ColorBlit.cs:
public class ColorBlit : CustomPostProcessing{
public ClampedFloatParameter intensity = new(0.0f, 0.0f, 2.0f)
public override bool IsActive() => true;
public override void Setup() {
}
public override void Render(CommandBuffer cmd, ref RenderingData renderingData, RTHandle source, RTHandle destination) {
}
public override void Dispose(bool disposing) {
}
}
成功。
抓取Volume内后措置组件
由前面的分析,CustomRedererFeature应该抓取Volume内后措置的组件,然后按照注入点位置创建3种RenderPass,入到队列中。
CustomRenderPass
首先填写CustomRenderPass的必要代码。为了在Execute内调用对应注入点的CustomPostProcessing的Render函数,我们需要在构造函数中向其传递当前注入点的所有CustomPostProcessing实例。同时,再声明一些必要的变量。
CustomPostProcessingPass.cs
public class CustomPostProcessingPass : ScriptableRenderPass{
// 所有自定义后措置基类
private List<CustomPostProcessing> mCustomPostProcessings;
// 当前active组件下标
private List<int> mActiveCustomPostProcessingIndex;
// 每个组件对应的ProfilingSampler
private string mProfilerTag;
private List<ProfilingSampler> mProfilingSamplers;
// 声明RT
private RTHandle mSourceRT;
private RTHandle mDesRT;
private RTHandle mTempRT0;
private RTHandle mTempRT1;
private string mTempRT0Name => ”_TemporaryRenderTexture0”;
private string mTempRT1Name => ”_TemporaryRenderTexture1”;
public CustomPostProcessingPass(string profilerTag, List<CustomPostProcessing> customPostProcessings) {
mProfilerTag = profilerTag;
mCustomPostProcessings = customPostProcessings;
mActiveCustomPostProcessingIndex = new List<int>(customPostProcessings.Count);
// 将自定义后措置器对象列表转换成一个性能采样器对象列表
mProfilingSamplers = customPostProcessings.Select(c => new ProfilingSampler(c.ToString())).ToList();
mTempRT0 = RTHandles.Alloc(mTempRT0Name, name: mTempRT0Name);
mTempRT1 = RTHandles.Alloc(mTempRT1Name, name: mTempRT1Name);
}
}值得注意的是,在URP14.0(或者在这之前)中,丢弃了原有RenderTargetHandle,而通通使用RTHandle。本来的Init也变成了RTHandles.Alloc,具体更新内容可以看最后的参考连接。
CustomRenderFeature
然后需要在CustomRenderFeature中抓取所有Volume中的后措置组件并按照注入点分类。这在Create()函数里进行,它会在OnEnable和OnValidate时被调用。
管线源码分析
下面讨论如何抓取所有Volume中的组件。
首先,与Volume相关代码封装在VolumeManager.cs中。首先可以发现,它采用单例模式。
忽略那些Internal函数,它提供了一个baseComponentArray属性,而且它是public的。
按照描述,它是担任于VolumeComponent的所有子类类型的列表(注意是类型,而非实例)。注意这个描述,这说明非论后措置基类是否在Volume中,它城市存在baseComponentArray里面。
跟踪一下给这个属性赋值的处所,找到ReloadBaseType()函数。
在函数里获取了所有担任自VolumeComponent的非抽象类类型派生。而且,循环baseComponentTypeArray,把它作为VolumeComponent派生类实例添加到m_ComponentDefaultState里面,这个m_ComponentDefaultState在后面也有用处。
按照注释描述,得知它只会在运行时调用一次,而每次脚本重载在编纂器中启动时,我们需要跟踪项目中的任何兼容组件。
继续跟踪ReloadBaseTypes()函数,发现它是在构造函数里创建的(忽略Editor Only)。的确是运行时只调用一次。也就是说,这个单例被创建时,就填充baseComponentTypeArray。
通过上述讨论,我们得知,如果想要获取所有担任自VolumeComponent的派生类类型,而且筛选出派生类型为CustomPostProcessing的列表,代码如下:
var derivedVolumeComponentTypes = VolumeManager.instance.baseComponentTypeArray;
var customPostProcessingTypes = derivedVolumeComponentTypes.Where(t => t.IsSubclassOf(typeof(CustomPostProcessing))).Tolist(;但是这只能筛选出担任自VolumeComponent的派生类类型的列表,并不能获取具体派生类实例。继续分析。
注意到这个构造函数里还有一个CreateStack()函数赋值给m_DefaultStack,而且将stack赋值。首先stack是public属性,我们可以直接通过单例访谒到它。然后跟踪CreateStack()。
发现它用Reload(m_ComponentDefaultStata)函数填充stack并返回。继续跟踪Reload函数。发现它就是把m_ComponentDefaultStata里存放的VolumeComponent派生实例的类型和实例分割,放到一个字典components里。
也就是说,如果我们想要知道一个VolumeComponent派生类的具体实例,只需要访谒components即可。但是components是internal的,我们对它进行跟踪,找到GetComponent()函数,正是我们需要的public的返回components的函数。
那么获取所有担任于VolumeComponent的CustomPostProcessing实例的思路就很清晰了:首先获取所有类型为CustomProcessing的元素,让后将它们替换为stack.components里对应的实例。这样既保证了获取的是实例,又保证了实例类型是CustomProcessing。
代码如下:
// 获取VolumeStack
var stack = VolumeManager.instance.stack;
// 获取volumeStack中所有CustomPostProcessing实例
var customPostProcessings = VolumeManager.instance.baseComponentTypeArray
.Where(t => t.IsSubclassOf(typeof(CustomPostProcessing))) // 筛选出volumeStack中的CustomPostProcessing类型元素 非论是否在Volume中 非论是否激活
.Select(t => stack.GetComponent(t) as CustomPostProcessing) // 将类型元素转换为实例
.ToList(); // 转换为List具体代码
颠末上述分析,我们首先需要在Create()里抓取VolumeComponent的所有派生类实例。
CustomPostProcessingFeature.cs
public override void Create() {
// 获取VolumeStack
var stack = VolumeManager.instance.stack;
// 获取所有的CustomPostProcessing实例
mCustomPostProcessings = VolumeManager.instance.baseComponentTypeArray
.Where(t => t.IsSubclassOf(typeof(CustomPostProcessing))) // 筛选出VolumeComponent派生类类型中所有的CustomPostProcessing类型元素 非论是否在Volume中 非论是否激活
.Select(t => stack.GetComponent(t) as CustomPostProcessing) // 将类型元素转换为实例
.ToList(); // 转换为List
}
下一步,对抓取到的所有CustomPostProcessing实例按照注入点分类,而且按照在注入点的挨次进行排序。再把对应CPPs实例传递给RenderPass实例,依据注入点分类通过renderPassEvent设置衬着时间。
// 初始化分歧插入点的render pass
// 找到在透明物和天空后衬着的CustomPostProcessing
var afterOpaqueAndSkyCPPs = mCustomPostProcessings
.Where(c => c.InjectionPoint == CustomPostProcessInjectionPoint.AfterOpaqueAndSky) // 筛选出所有CustomPostProcessing类中注入点为透明物体和天空后的实例
.OrderBy(c => c.OrderInInjectionPoint) // 按照挨次排序
.ToList(); // 转换为List
// 创建CustomPostProcessingPass类
mAfterOpaqueAndSkyPass = new CustomPostProcessingPass(”Custom PostProcess after Skybox”, afterOpaqueAndSkyCPPs);
// 设置Pass执行时间
mAfterOpaqueAndSkyPass.renderPassEvent = RenderPassEvent.AfterRenderingSkybox;
var beforePostProcessingCPPs = mCustomPostProcessings
.Where(c => c.InjectionPoint == CustomPostProcessInjectionPoint.BeforePostProcess)
.OrderBy(c => c.OrderInInjectionPoint)
.ToList();
mBeforePostProcessPass = new CustomPostProcessingPass(”Custom PostProcess before PostProcess”, beforePostProcessingCPPs);
mBeforePostProcessPass.renderPassEvent = RenderPassEvent.BeforeRenderingPostProcessing;
var afterPostProcessCPPs = mCustomPostProcessings
.Where(c => c.InjectionPoint == CustomPostProcessInjectionPoint.AfterPostProcess)
.OrderBy(c => c.OrderInInjectionPoint)
.ToList();
mAfterPostProcessPass = new CustomPostProcessingPass(”Custom PostProcess after PostProcessing”, afterPostProcessCPPs);
mAfterPostProcessPass.renderPassEvent = RenderPassEvent.AfterRenderingPostProcessing;
抓取完CustomPostProcessing实例后,我们需要在Dispose里全部释放他们。
CustomPostProcessingFeature.cs
protected override void Dispose(bool disposing) {
base.Dispose(disposing);
if (disposing && mCustomPostProcessings != null) {
foreach (var item in mCustomPostProcessings) {
item.Dispose();
}
}
}注入Pass
前面提到,通过VolumeManager抓取是所有VolumeComponent的派生类,不管是否在Volume中,所以我们还需要在RenderPass中判断杂交的CustomPostProcessing是否是激活状态。
CustomPostProcessing.cs
// 获取active的CPPs下标,并返回是否存在有效组件
public bool SetupCustomPostProcessing() {
mActiveCustomPostProcessingIndex.Clear();
for (int i = 0; i < mCustomPostProcessings.Count; i++) {
mCustomPostProcessings.Setup();
if (mCustomPostProcessings.IsActive()) {
mActiveCustomPostProcessingIndex.Add(i);
}
}
return mActiveCustomPostProcessingIndex.Count != 0;
}
下面完成RenderFeature的第二个任务,将RenderPass EnqueuePass。它在AddRenderPasses函数中进行,我们将分歧的注入点的RenderPass注入到renderer中。
CustomRenderFeature.cs
// 当为每个摄像机设置一个衬着器时,调用此方式
// 将分歧注入点的RenderPass注入到renderer中
public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData) {
// 当前衬着的游戏相机撑持后措置
if (renderingData.cameraData.postProcessEnabled) {
// 为每个render pass设置RT
// 而且将pass列表加到renderer中
if (mAfterOpaqueAndSkyPass.SetupCustomPostProcessing()) {
mAfterOpaqueAndSkyPass.ConfigureInput(ScriptableRenderPassInput.Color);
renderer.EnqueuePass(mAfterOpaqueAndSkyPass);
}
if (mBeforePostProcessPass.SetupCustomPostProcessing()) {
mBeforePostProcessPass.ConfigureInput(ScriptableRenderPassInput.Color);
renderer.EnqueuePass(mBeforePostProcessPass);
}
if (mAfterPostProcessPass.SetupCustomPostProcessing()) {
mAfterPostProcessPass.ConfigureInput(ScriptableRenderPassInput.Color);
renderer.EnqueuePass(mAfterPostProcessPass);
}
}
}
网上有些资料在这个函数里配置RenderPass的源RT和方针RT,具体来说使用类似RenderPass.Setup(renderer.cameraColorTargetHandle, renderer.cameraColorTargetHandle)的方式,但是这在URP14.0中会报错,提示renderer.cameraColorTargetHandle只能在ScriptableRenderPass子类里调用。具体细节可以查看最后的参考连接。
下面再讨论一种错误做法。转到RenderPass中,在OnCameraSetup重载函数中设置当前RenderPass的源RT和方针RT,它将在相机衬着前被调用。
public override void OnCameraSetup(CommandBuffer cmd, ref RenderingData renderingData) {
RenderTextureDescriptor blitTargetDescriptor = renderingData.cameraData.cameraTargetDescriptor;
blitTargetDescriptor.depthBufferBits = 0;
var renderer = renderingData.cameraData.renderer;
// 源RT固定为相机的颜色RT ”_CameraColorAttachmentA”
mSourceRT = renderer.cameraColorTargetHandle;
mDesRT = renderer.cameraColorTargetHandle;
}这会带来什么错误呢?如果注入点非后措置后,则相机源和方针应该为_CameraColorAttachmentA。但是注入点在后措置后时,RP会进行一次FinalBlit,最终相机的源和方针应该为_CameraColorAttachmentB。不外这不需要判断,直接在Execute里面赋值即可。(按理来说可能消耗会更高,但是我不知道怎样在有finalBlit的情况下提前获取_CameraColorAttachmentB)
如图,这是只有注入点在衬着天空盒后的Frame Debugger。它没有Final Blit,所以可以把RT设置为_CameraColorAttachmentA。
然后,这是添加注入点在后措置后的Frame Debugger。它生出了Final Blit,而且Final Blit的输入源RT为_CameraColorAttachmentB。
所以这个注入点在后措置后的后措置RT应该为_CameraColorAttchmentB。
RenderPass衬着
下面完成最后一步,即在CustomPostProcessingPass的Execute函数里填写具体的衬着代码。
主要流程如下
[*]声明临时纹理
[*]设置源衬着纹理mSourceRT方针衬着纹理mDesRT为衬着数据的相机颜色方针措置。(区分有无finalBlit)
[*]如果只有一个后措置效果,则直接将这个后措置效果从mSourceRT衬着到mTempRT0。
[*]如果有多个后措置效果,则逐后措置的在mTempRT0和mTempRT1之间衬着。由于每次循环结束交换它们,所以最终纹理依然存在mTempRT0。
[*]使用Blitter.BlitCameraTexture函数将mTempRT0中的成果复制到方针衬着纹理mDesRT中。
完整代码如下:
// 实现衬着逻辑
public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData) {
// 初始化commandbuffer
var cmd = CommandBufferPool.Get(mProfilerTag);
context.ExecuteCommandBuffer(cmd);
cmd.Clear();
// 获取相机Descriptor
var descriptor = renderingData.cameraData.cameraTargetDescriptor;
descriptor.msaaSamples = 1;
descriptor.depthBufferBits = 0;
// 初始化临时RT
bool rt1Used = false;
// 设置源和方针RT为本次衬着的RT 在Execute里进行 特殊措置后措置后注入点
mDesRT = renderingData.cameraData.renderer.cameraColorTargetHandle;
mSourceRT = renderingData.cameraData.renderer.cameraColorTargetHandle;
// 声明temp0临时纹理
// cmd.GetTemporaryRT(Shader.PropertyToID(mTempRT0.name), descriptor);
// mTempRT0 = RTHandles.Alloc(mTempRT0.name);
RenderingUtils.ReAllocateIfNeeded(ref mTempRT0, descriptor, name: mTempRT0Name);
// 执行每个组件的Render方式
if (mActiveCustomPostProcessingIndex.Count == 1) {
int index = mActiveCustomPostProcessingIndex;
using (new ProfilingScope(cmd, mProfilingSamplers)) {
mCustomPostProcessings.Render(cmd, ref renderingData, mSourceRT, mTempRT0);
}
}
else {
// 如果有多个组件,则在两个RT上来回bilt
RenderingUtils.ReAllocateIfNeeded(ref mTempRT1, descriptor, name: mTempRT1Name);
rt1Used = true;
Blit(cmd, mSourceRT, mTempRT0);
for (int i = 0; i < mActiveCustomPostProcessingIndex.Count; i++) {
int index = mActiveCustomPostProcessingIndex;
var customProcessing = mCustomPostProcessings;
using (new ProfilingScope(cmd, mProfilingSamplers)) {
customProcessing.Render(cmd, ref renderingData, mTempRT0, mTempRT1);
}
CoreUtils.Swap(ref mTempRT0, ref mTempRT1);
}
}
Blitter.BlitCameraTexture(cmd, mTempRT0, mDesRT);
// 释放
cmd.ReleaseTemporaryRT(Shader.PropertyToID(mTempRT0.name));
if (rt1Used) cmd.ReleaseTemporaryRT(Shader.PropertyToID(mTempRT1.name));
context.ExecuteCommandBuffer(cmd);
CommandBufferPool.Release(cmd);
}例子:ColorBlit
在这之前,注意把CustomPostProcessingFeature添加到Renderer中。
下面把官方文档里的ColorBlit例子改为我们CPP系统能用的脚本。
ColorBlit.cs
C#很简单,首先创建一个挂载ColorBlit.shader的材质,然后在Render()函数中调用Blit从sourceRT调用材质的Shader衬着给destinationRT。
需要注意的是,我们的RenderFeature抓取的CusomProcessing是全部基于VolumenComponent的派生类,而不是当前场景Global Volume组件里的后措置。所以为了RenderPass能够判断当前后措置是否有效,最好给每个后措置IsActive()除了判断材质非空外加上一个属性的约束。
using CPP;
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;
namespace CPP.EFFECTS{
public class ColorBlit : CustomPostProcessing{
public ClampedFloatParameter intensity = new(0.0f, 0.0f, 2.0f);
private Material mMaterial;
private const string mShaderName = ”Hidden/PostProcess/ColorBlit”;
public override bool IsActive() => mMaterial != null && intensity.value > 0;
public override CustomPostProcessInjectionPoint InjectionPoint => CustomPostProcessInjectionPoint.AfterOpaqueAndSky;
public override int OrderInInjectionPoint => 0;
public override void Setup() {
if (mMaterial == null)
mMaterial = CoreUtils.CreateEngineMaterial(mShaderName);
}
public override void Render(CommandBuffer cmd, ref RenderingData renderingData, RTHandle source, RTHandle destination) {
if (mMaterial == null) return;
mMaterial.SetFloat(”_Intensity”, intensity.value);
cmd.Blit(source, destination, mMaterial, 0);
}
public override void Dispose(bool disposing) {
base.Dispose(disposing);
CoreUtils.Destroy(mMaterial);
}
}
}PostProcessing.hlsl
在官方文档中,对相机RT的采样用的是对_BlitTexture的SAMPLE_TEXTURE2D_X。_BlitTexture在Blit.hlsl库中声明。但是使用Blit函数衬着到的相机纹理是_MainTex(不知道是不是API调用的原因)。而且,_MainTex必需在Properties中定义。
这里仿照官方提供的Blit.hlsl,将公用的声明和常用函数放到Common/PostProcessing.hlsl中。
#ifndef POSTPROCESSING_INCLUDED
#define POSTPROCESSING_INCLUDED
#include ”Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl”
TEXTURE2D(_MainTex);
SAMPLER(sampler_MainTex);
TEXTURE2D(_CameraDepthTexture);
SAMPLER(sampler_CameraDepthTexture);
struct Attributes {
float4 positionOS : POSITION;
float2 uv : TEXCOORD0;
};
struct Varyings {
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
UNITY_VERTEX_OUTPUT_STEREO
};
half4 SampleSourceTexture(float2 uv) {
return SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, uv);
}
half4 SampleSourceTexture(Varyings input) {
return SampleSourceTexture(input.uv);
}
Varyings Vert(Attributes input) {
Varyings output = (Varyings)0;
// 分配instance id
UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(output);
VertexPositionInputs vertexInput = GetVertexPositionInputs(input.positionOS.xyz);
output.vertex = vertexInput.positionCS;
output.uv = input.uv;
return output;
}
#endifColorBlit.shader
Shader ”Hidden/PostProcess/ColorBlit” {
Properties {
// 显式声明出来_MainTex
_MainTex (”Base (RGB)”, 2D) = ”white” {}
}
SubShader {
Tags {
”RenderType”=”Opaque”
”RenderPipeline” = ”UniversalPipeline”
}
LOD 200
Pass {
Name ”ColorBlitPass”
HLSLPROGRAM
#include ”Common/PostProcessing.hlsl”
#pragma vertex Vert
#pragma fragment frag
float _Intensity;
half4 frag(Varyings input) : SV_Target {
float4 color = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, input.uv);
return color * float4(0, _Intensity, 0, 1);
}
ENDHLSL
}
}
}例子:ColorAdjustment
最基本的色彩调整后措置。
ColorAdjustment.cs
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;
using CPP;
namespace CPP.EFFECTS{
public class ColorAdjustments : CustomPostProcessing{
#region Parameters Define
// 后曝光
public FloatParameter postExposure = new FloatParameter(0.0f);
// 对比度
public ClampedFloatParameter contrast = new ClampedFloatParameter(0.0f, 0.0f, 100.0f);
// 颜色滤镜
public ColorParameter colorFilter = new ColorParameter(Color.white, true, false, false);
// 色相偏移
public ClampedFloatParameter hueShift = new ClampedFloatParameter(0.0f, -180.0f, 180.0f);
// 饱和度
public ClampedFloatParameter saturation = new ClampedFloatParameter(0.0f, -100.0f, 100.0f);
#endregion
private Material mMaterial;
private const string mShaderName = ”Hidden/PostProcess/ColorAdjusments”;
#region Active State Check
public override bool IsActive() =>
mMaterial != null && (IsPostExposureActive() || IsContrastActive() || IsContrastActive() || IsColorFilterActive() || IsHueShiftActive() || IsSaturationActive());
private bool IsPostExposureActive() => postExposure.value != 0.0f;
private bool IsContrastActive() => contrast.value != 0.0f;
private bool IsColorFilterActive() => colorFilter.value != Color.white;
private bool IsHueShiftActive() => hueShift.value != 0.0f;
private bool IsSaturationActive() => saturation.value != 0.0f;
#endregion
public override CustomPostProcessInjectionPoint InjectionPoint => CustomPostProcessInjectionPoint.AfterPostProcess;
public override int OrderInInjectionPoint => 99;
private int mColorAdjustmentsId = Shader.PropertyToID(”_ColorAdjustments”),
mColorFilterId = Shader.PropertyToID(”_ColorFilter”);
private const string mExposureKeyword = ”EXPOSURE”,
mContrastKeyword = ”CONTRAST”,
mHueShiftKeyword = ”HUE_SHIFT”,
mSaturationKeyword = ”SATURATION”,
mColorFilterKeyword = ”COLOR_FILTER”;
public override void Setup() {
if (mMaterial == null)
mMaterial = CoreUtils.CreateEngineMaterial(mShaderName);
}
public override void Render(CommandBuffer cmd, ref RenderingData renderingData, RTHandle source, RTHandle destination) {
if (mMaterial == null) return;
Vector4 colorAdjustmentsVector4 = new Vector4(
Mathf.Pow(2f, postExposure.value), // 曝光度 曝光单元是2的幂次
contrast.value * 0.01f + 1f, // 对比度 将范围从[-100, 100]映射到
hueShift.value * (1.0f / 360.0f), // 色相偏移 将范围从[-180, 180]转换到[-0.5, 0.5]
saturation.value * 0.01f + 1.0f); // 饱和度 将范围从[-100, 100]转换到
mMaterial.SetVector(mColorAdjustmentsId, colorAdjustmentsVector4);
mMaterial.SetColor(mColorFilterId, colorFilter.value);
// 按照是否激活对应调整设置keyword
SetKeyWord(mExposureKeyword, IsPostExposureActive());
SetKeyWord(mContrastKeyword, IsContrastActive());
SetKeyWord(mHueShiftKeyword, IsHueShiftActive());
SetKeyWord(mSaturationKeyword, IsSaturationActive());
SetKeyWord(mColorFilterKeyword, IsColorFilterActive());
cmd.Blit(source, destination, mMaterial, 0);
}
private void SetKeyWord(string keyword, bool enabled = true) {
if (enabled) mMaterial.EnableKeyword(keyword);
else mMaterial.DisableKeyword(keyword);
}
public override void Dispose(bool disposing) {
base.Dispose(disposing);
CoreUtils.Destroy(mMaterial);
}
}
}ColorAdjustment.shader
Shader ”Hidden/PostProcess/ColorAdjusments” {
Properties {
_MainTex (”Base (RGB)”, 2D) = ”white” {}
}
SubShader {
Tags {
”RenderType” = ”Opaque”
”RenderPipeline” = ”UniversalPipeline”
}
LOD 200
Pass {
name ”ColorAdjustmentPass”
HLSLPROGRAM
#include ”Common/PostProcessing.hlsl”
#include ”Packages/com.unity.render-pipelines.core/ShaderLibrary/Color.hlsl”
#pragma vertex Vert
#pragma fragment frag
#pragma shader_feature EXPOSURE
#pragma shader_feature CONTRAST
#pragma shader_feature COLOR_FILTER
#pragma shader_feature HUE_SHIFT
#pragma shader_feature SATURATION
// 曝光度 对比度 色相偏移 饱和度
float4 _ColorAdjustments;
float4 _ColorFilter;
// 后曝光
half3 ColorAdjustmentExposure(half3 color) {
return color * _ColorAdjustments.x;
}
// 对比度
half3 ColorAdjustmentContrast(float3 color) {
// 为了更好的效果 将颜色从线性空间转换到logC空间(因为要取美术中灰)
color = LinearToLogC(color);
// 从颜色中减去均匀的中间灰度,然后通过对比度进行缩放,然后在中间添加中间灰度
color = (color - ACEScc_MIDGRAY) * _ColorAdjustments.y + ACEScc_MIDGRAY;
return LogCToLinear(color);
}
// 颜色滤镜
half3 ColorAdjustmentColorFilter(float3 color) {
color = SRGBToLinear(color);
color = color * _ColorFilter.rgb;
return color;
}
// 色相偏移
half3 ColorAdjustmentHueShift(half3 color) {
// 将颜色格式从rgb转换为hsv
color = RgbToHsv(color);
// 将色相偏移添加到h
float hue = color.x + _ColorAdjustments.z;
// 如果色相超出范围 将其截断
color.x = RotateHue(hue, 0.0, 1.0);
// 将颜色格式从hsv转换为rgb
return HsvToRgb(color);
}
// 饱和度
half3 ColorAdjustmentSaturation(half3 color) {
// 获取颜色的亮度
float luminance = Luminance(color);
// 从颜色中减去亮度,然后通过饱和度进行缩放,然后在中间添加亮度
return (color - luminance) * _ColorAdjustments.w + luminance;
}
half3 ColorAdjustment(half3 color) {
// 防止颜色值过大的潜在隐患
color = min(color, 60.0);
// 后曝光
#ifdef EXPOSURE
color = ColorAdjustmentExposure(color);
#endif
// 对比度
#ifdef CONTRAST
color = ColorAdjustmentContrast(color);
#endif
// 颜色滤镜
#ifdef COLOR_FILTER
color = ColorAdjustmentColorFilter(color);
#endif
// 当对比度增加时,会导致颜色分量变暗,在这之后将颜色钳位
color = max(color, 0.0);
// 色相偏移
#ifdef HUE_SHIFT
color = ColorAdjustmentHueShift(color);
#endif
// 饱和度
#ifdef SATURATION
color = ColorAdjustmentSaturation(color);
#endif
// 当饱和度增加时,可能发生负数,在这之后将颜色钳位
return max(color, 0.0);
return color;
}
half4 frag(Varyings input) : SV_Target {
half3 color = SampleSourceTexture(input).xyz;
half3 finalCol = ColorAdjustment(color);
return half4(finalCol, 1.0);
}
ENDHLSL
}
}
}其他
首先,编纂模式下。切换场景,Scene和Game视图相机里的后措置并不会刷新,此时先封锁Renderer的CustomPostProcessingFeature组件再打开,后措置就刷新了。
但是,在播放模式下,通过场景切换脚本,后措置还是可以刷新的。
目前还没找到编纂模式下直接刷新的方式,以后找到了再更新一下。
此外,衬着的性能也没怎么优化,例如Blit替换为全屏三角形等,后面再更新。
先把这套用一用再更新一些细节。
更新:绘制法式化全屏三角形
在之前的衬着方式中,我们使用带材质参数的Blit函数,通过查看frame debugger,可以明确它实际上绘制了一个四边形。
然而,我们使用不带有材质参数的Blit时,它会调用CoreBlit.shader,而且只绘制一个三角形。这使得顶点和索引数量减少,带来的消耗必定更少了。
所以我们的目的就是放弃使用带材质参数的Blit函数,而是自定义一个函数来使用绘制法式化三角形的方式进行衬着。
在后措置基类中,新建一个Draw函数,它接收CommandBuffer,源、方针RT以及调用pass索引。绘制法式如下:将GPU端的”_SourceTexture”纹理设置为sourceRT,将RenderTarget设置为destination,绘制法式化三角形。
CustomPostProcessing.cs
// 材质声明
protected Material mMaterial = null;
private Material mCopyMaterial = null;
private const string mCopyShaderName = ”Hidden/PostProcess/PostProcessCopy”;
protected override void OnEnable() {
base.OnEnable();
if (mCopyMaterial == null) {
mCopyMaterial = CoreUtils.CreateEngineMaterial(mCopyShaderName);
}
#region Draw Function
private int mSourceTextureId = Shader.PropertyToID(”_SourceTexture”);
public virtual void Draw(CommandBuffer cmd, in RTHandle source, in RTHandle destination, int pass = -1) {
// 将GPU端_SourceTexture设置为source
cmd.SetGlobalTexture(mSourceTextureId, source);
// 将RT设置为destination 不关心初始状态(直接填充) 需要存储
cmd.SetRenderTarget(destination, RenderBufferLoadAction.DontCare, RenderBufferStoreAction.Store);
// 绘制法式化三角形
if (pass == -1 || mMaterial == null)
cmd.DrawProcedural(Matrix4x4.identity, mCopyMaterial, 0, MeshTopology.Triangles, 3);
else
cmd.DrawProcedural(Matrix4x4.identity, mMaterial, pass, MeshTopology.Triangles, 3);
}
#endregion这个三角形应该是覆盖整个屏幕的大三角形,则此时屏幕xy只占了三角形xy的一半,而且索引挨次按顺时针。屏幕坐标范围为 [-1,1] ,所以这个三角形的(裁剪)坐标分袂为 (-1, -1),(-1,3),(3,-1) 。UV范围为 [-1,1] ,所以这个三角形的UV坐标(屏幕)分袂为 (0,0), (0, 2), (2, 0) 。
然后更新后措置shader公用基础Shader,顶点着色器中,我们通过顶点索引判断裁剪空间顶点坐标和屏幕UV。这里仿照VertexPositionInputs的方式写了一个转换函数,用于便利可能呈现的自定义顶点着色器情况。
PostProcessing.hlsl
struct ScreenSpaceData {
float4 positionCS;
float2 uv;
};
ScreenSpaceData GetScreenSpaceData(uint vertexID : SV_VertexID) {
ScreenSpaceData output;
// 按照id判断三角形顶点的坐标
// 坐标挨次为(-1, -1) (-1, 3) (3, -1)
output.positionCS = float4(vertexID <= 1 ? -1.0 : 3.0, vertexID == 1 ? 3.0 : -1.0, 0.0, 1.0);
output.uv = float2(vertexID <= 1 ? 0.0 : 2.0, vertexID == 1 ? 2.0 : 0.0);
// 分歧API可能会发生倒置的情况 进行判断
if (_ProjectionParams.x < 0.0) {
output.uv.y = 1.0 - output.uv.y;
}
return output;
}
Varyings Vert(uint vertexID : SV_VertexID) {
Varyings output;
ScreenSpaceData ssData = GetScreenSpaceData(vertexID);
output.positionCS = ssData.positionCS;
output.uv = ssData.uv;
return output;
}
然后再定义一些基本的功能。
#include ”Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl”
#include ”Packages/com.unity.render-pipelines.universal/ShaderLibrary/UnityInput.hlsl”
TEXTURE2D(_SourceTexture);
SAMPLER(sampler_SourceTexture);
TEXTURE2D(_CameraDepthTexture);
SAMPLER(sampler_CameraDepthTexture);
struct Varyings {
float4 positionCS : SV_POSITION;
float2 uv : TEXCOORD0;
UNITY_VERTEX_OUTPUT_STEREO
};
half4 GetSource(float2 uv) {
return SAMPLE_TEXTURE2D(_SourceTexture, sampler_SourceTexture, uv);
}
half4 GetSource(Varyings input) {
return GetSource(input.uv);
}
float SampleDepth(float2 uv) {
#if defined(UNITY_STEREO_INSTANCING_ENABLED) || defined(UNITY_STEREO_MULTIVIEW_ENABLED)
return SAMPLE_TEXTURE2D_ARRAY(_CameraDepthTexture, sampler_CameraDepthTexture, uv, unity_StereoEyeIndex).r;
#else
return SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, sampler_CameraDepthTexture, uv);
#endif
}
float SampleDepth(Varyings input) {
return SampleDepth(input.uv);
}
注意,此时的后措置Shader需要封锁深度写入,封锁剔除,即:
ZWrite Off
Cull Off
然后再写一个用了复制color buffer的Shader。
PostProcessCopy.shader
Shader ”Hidden/PostProcess/PostProcessCopy” {
SubShader {
Tags {
”RenderType”=”Opaque”
”RenderPipeline” = ”UniversalPipeline”
}
LOD 200
ZWrite Off
Cull Off
HLSLINCLUDE
#include ”PostProcessing.hlsl”
ENDHLSL
Pass {
Name ”CopyPass”
HLSLPROGRAM
#pragma vertex Vert
#pragma fragment frag;
half4 frag(Varyings input) : SV_Target {
half4 color = GetSource(input);
return half4(color.rgb, 1.0);
}
ENDHLSL
}
}
}
最后,把自定义后措置的C#脚本中的Blit函数通通换成Draw函数。
// cmd.Blit(source, destination, mMaterial, 0);
Draw(cmd, source, destination, 0);更新
2023/8/23改了临时纹理的声明和释放逻辑。
具体来说,在CustomPostProcessingPass的OnCameraSetup()函数中分配本次主Pass的中间纹理,而且分袂该主Pass下的CustomPostProcessing实例的OnCameraSetup()函数,它们各自在此中分配本身的临时纹理。纹理的分配使用RenderingUtils.ReAllocateIfNeeded函数,猜测它不像CommandBuffer加命令那样可以让GPU按挨次执行分配纹理和之后的衬着逻辑,所以只能在正式衬着前将纹理全部门配,最后在统一释放。这也是Unity文档和我搜索资料的写法,有没有更好的方式我暂时没有看到。
最重要的是释放纹理,否则会直接死机。参考Unity文档的写法,在CustomPostProcessingFeature的Dispose()函数中,首先调用各个主Pass实例和CustomPostProcessing实例的Dispose()函数,它们在此中分袂释放各自纹理。使用RTHandle.Release()函数。
2023/8/23添加更多后措置效果,部门参考毛星云前辈的X-PostProcessing库。
参考
pamisu: 为了更好用的后措置——扩展URP后措置踩坑记录
tatsuya-kosuda: URPCustomPostProcessing
Adapt URP to use RTHandles
(URP 13.1.8) Proper RTHandle usage in a Renderer Feature
How to Blit in URP - Documentation & Unity blog post on every Blit function
页:
[1]