engine icon indicating copy to clipboard operation
engine copied to clipboard

Open post-processing pipeline

Open zhuxudong opened this issue 6 months ago • 0 comments

背景

  • 1.3 版本只支持了 Bloom 和 Tonemapping 特效,内部实现还是用的 internal 方式。
  • 1.4 需要开放后处理管线,方便用户灵活拓展后处理效果。
  • 为了兼容以后的3D空间后处理(不同包围体下触发不同后处理效果),后处理职责需要归 Component 管理,挂到 Entity 下。

Unity 调研

概要:

  • 通过 Renderer Feature 拓展全屏 Pass 后处理,且通过 RenderPassEvent 来决定 Execute 的执行时机。
  • 在后处理 pass 中,uber 部分的属性从 VolumeManager 实例对象中获取,具体 Effect 的属性放在相应的 VolumeComponent 中。
  • VolumeManager 除了 Effect 的管理,还负责 Local 模式下 Blend、碰撞体的检测判断。

后处理管线

image

SRP

后处理生效范围

Unity 是根据当前节点树的体积来的。

image

image

image

其中,camera 还有一个开关 Volume Mask 可以设置生效多个体积中的哪一个: image

拓展后处理方法

1. Full Screen Renderer Feature

在 Unity 中拓展后处理,官方文档推荐的方式是通过 Scriptable Renderer 面板 Add Renderer Feature添加内置的 Full Screen Renderer Feature, 会保存到 Scriptable Renderer 的 rendererFeatures 属性中,然后绑定材质(pass material),

image

image

材质如果是 ShaderGraph 做的,sample source 那里可以选择来源是 BlitSource,普通材质可以直接使用 _MainTexture 作为采样纹理:

image

Render Feature 这里还提供了插入位置用来决定何时渲染这个后处理效果:

image

2. Custom Renderer Feature

如果官方内置的 Full Screen Renderer Feature 不能满足,还可以新建自定义 Renderer Feature 进行拓展:

image

参考官网 Demo,代码跟 Full Screen 的差不多,拿官网的举例:

image

Renderer Feature:

internal class ColorBlitRendererFeature : ScriptableRendererFeature
{
    public Shader m_Shader;
    public float m_Intensity;

    Material m_Material;

    ColorBlitPass m_RenderPass = null;

    public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData){
    		renderer.EnqueuePass(m_RenderPass);
    }

    public override void Create()
    {
        m_Material = CoreUtils.CreateEngineMaterial(m_Shader);
        m_RenderPass = new ColorBlitPass(m_Material);
    }
}

SRP:

internal class ColorBlitPass : ScriptableRenderPass
{
    ProfilingSampler m_ProfilingSampler = new ProfilingSampler("ColorBlit");
    Material m_Material;
    float m_Intensity;

    public ColorBlitPass(Material material)
    {
        m_Material = material;
        renderPassEvent = RenderPassEvent.BeforeRenderingPostProcessing;
    }

    public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
    {
        var cameraData = renderingData.cameraData;

        if (m_Material == null)
            return;

        CommandBuffer cmd = CommandBufferPool.Get();
        using (new ProfilingScope(cmd, m_ProfilingSampler))
        {
            m_Material.SetFloat("_Intensity", m_Intensity);
            Blitter.BlitCameraTexture(cmd, m_CameraColorTarget, m_CameraColorTarget, m_Material, 0);
        }
        context.ExecuteCommandBuffer(cmd);
        cmd.Clear();

        CommandBufferPool.Release(cmd);
    }
}

Shader

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
        }
    }
}

3. Renderer Feature + Volume

Unity 6000 版本,Unity 资产还新增了 Post-Processing Effect 模板,本质上是新建了上文说的 Renderer Feature 和 Volume 两个脚本,Volume 脚本用来控制后处理特效有哪些属性,Renderer Feature 用来创建 pass,读取 Volume 属性,执行 pass 等逻辑 。

image

本质上没区别,还是需要在 Assets/Settings/RendererData 添加 RenderFeature:

image

然后在后处理组件添加 Override:

image

代码也没啥差异,就是把本来一坨全写在 Renderer Feature 的代码,拆开到了 Volume 组件中,使逻辑更加清晰。

补充:CommandBuffer

这里简称 CB。

概念

在前面的自定义后处理实现方案中,无论是 SRP 还是脚本方式,想要进行 Blit 操作,都用到了 CB 相关技术,CB 设计提供了一种拓展渲染管线的方法,可以立即执行或者在渲染管线的各个时机执行封装的一系列渲染命令。关于 CB 的设计并不是 Unity 独创的,在 DX,Metal 中都可以看到相似的命令缓冲区概念,至于 Unity 早期的设计考虑可以参考这篇博客

优劣

  • 优势
    • 更加灵活。相比较 OpenGL 的流水线式管线工作,CB 可以自己决定何时运行,在脚本等入口都可以调用后处理。
    • RHI 适配。封装成 CB 概念后,可以为以后的 WebGPU 等架构做 RHI 适配。
    • 高性能。从单状态机封装成多状态机,方便多线程并行。
  • 劣势
    • 使用 CB 时,需要手动管理渲染状态和资源,过于开放的指令调用,在某些情况下会造成困扰和负优化。

执行时机

目前 CB 有这些渲染命令,其中 commandBuffer.Blit 在 URP 中已经被弃用,推荐使用 Blitter

image

目前的解决方案是基于 SRP 的 RenderPassEvent,然后在相应的管线里面手动调用 Graphics.ExecuteCommandBuffer 或者 ScriptableRenderContext .ExecuteCommandBuffer

总结

  • 优势
    • 编辑器封装足够强大,其 Volume 体积的设计,能够满足全局、包围盒等各种情况下的特效组合。
    • 封装的 Full Screen Renderer Feature 可以满足大部分后处理场景(暂时没想到需要用到 Custom Feature 的后处理场景)。
  • 缺点
    • 这个 Global 概念太含糊,调研下来才知道是针对根Scene,即当前节点树的。

Babylon 调研

文档:https://doc.babylonjs.com/features/featuresDeepDive/postProcesses/usePostProcesses API:https://doc.babylonjs.com/typedoc/classes/BABYLON.PostProcess

设计思路

image

有 4 个维度,从小到大分别是 Post Process、Effect、 Pipeline、Manager。

  • Post Process

后处理的最小维度就是 PostProcess, Babylon 内置了一系列 XXXPostProcess 后处理特效,指定绑定到某个相机下的 pass 中,比如下图的 Blur 模糊后处理,然后用 updateEffect来更新效果

image

  • Effect

以 Bloom 效果举例,BloomEffect 里面其实包含了** Hightlight、Blur、BloomMerge** 等 Post Process,所以需要一个比 Post Process 更高维度的概念,好处就是灵活复用各种原子后处理效果,并用入参来表示 PostProcess 的**种类和顺序,**伪代码:

const bloomEffect = new PostProcessRenderEffect(engine, "bloom", function () {
  // 这里可以调整顺序和种类
  return [hightLight, horizontalBlur, verticalBlur, bloomMerge];
});

Pipeline 的维度就更高了,可以封装一系列 Effect,通过 Pipeline.addEffect(effect) 的方式来添加/调整顺序。 Babylon 还提供了一个专门的默认后处理管线,默认开启 HDR,里面包含了 HDR、Bloom、FXAA、DOF 等 Process 和 Effect,方便用户快速调节各种后处理,参考这个案例

  • Manager

创建完 Pipeline 后,还需要将管道添加到 Manager 并附加到相机:

scene.postProcessRenderPipelineManager.addPipeline(standardPipeline);
scene.postProcessRenderPipelineManager.attachCamerasToRenderPipeline("standardPipeline", camera);

比如前面的默认后处理管线的源代码里面:

image

image

  • 特殊 case

如果 Post Process 已经绑定到相机,那么后面无论是绑定到 Effect 还是 Pipeline,都是会执行后处理的,这块设计有点缺陷?像是暴露过于灵活了。

自定义后处理

如果要自己写一个后处理效果的话,参考这篇文档,总结如下:

  • 先写一个片元着色器,片元代码会内置 vUV 纹理坐标和 textureSampler 纹理(上一个屏幕输出的),源码:

image

  • 声明 uniform,分辨率等信息:

image

  • 更新 uniform,setTextureFromPostProcess可以拿到后处理的输出:

image

总结

  • 优点
    • 颗粒度比较细。像 BloomEffect 里面的 Hightlight、Blur 等 PostProcess 都可以单独使用。
    • 4 层维度封装的很灵活(有两面性),可以自由拼装/插拔/调整顺序 自定义后处理,包括内置后处理。
    • 不用感知 Blit ,切换 RT 等指令操作(有两面性),用户只需要写片元着色器。
  • 缺点
    • 过于灵活,没有 UberShader 节省性能,且有设计漏洞。如果 Post Process 已经绑定到相机,那么更高维度的 Effect 或者 Pipeline,即使没有绑定到相机也是会执行后处理的。
    • Pipeline 虽然可以设置多个,但是只能在相机渲染最后时机执行后处理,Unity 可以控制触发时机。
    • 健壮性可能偏差。没有 Blit 操作, 只暴露 shaderData 的黑盒操作虽然方便,但是面对复杂场景可能就会束手无策,比如自定义顶点着色器、延迟执行。

Laya 调研

创建方式

image

拓展方式

class CustomPostProcessEffect extends PostProcessEffect {
  render(context: PostProcessRenderContext): void {}
}
postProcess.addEffect(new CustomPostProcessEffect());

PostProcessRenderContext里面保存了各种后处理状态,包括 CommandBuffer,用来 blit,切换 RT 等操作:

image

  • 主链路

在 mainPass 最后会调用 postProcess 的 _render() 方法,最后会调用 CommandBuffer:

image 然后遍历执行每个 Effect 的 render(context) 方法,并把 context 传入:

image

编辑器设计

https://layaair.com/3.x/doc/3D/advanced/PostProcessing/readme.html

image

总结

  • 优点
    • 可以自由拼装/插拔/调整顺序 自定义后处理,包括内置后处理。
    • 有 CommandBuffer 机制,方便拓展后处理。
  • 缺点
    • 这个 addEffect 潜规则有点多?Uber 和内置、重复的 Effect 的处理逻辑。
    • 后处理 Pass 只能在相机渲染最后执行,Unity 可以控制触发时机。
    • 后处理是绑定在 camera 下面的,不方便多相机复用/组合。
    • 编辑器这个设计有点 hack,拓展起来不太方便。

Unreal 调研

概要:

  • 跟 Unity 一样,模板自带 GlobalPostProcessVolume 节点。
  • 通过 Actor 创建后处理 Volume,且每一份都是实例对象,不共享后处理配置。
  • camera 没有 Volume mask 来决定相应 layer 生效的后处理;但是 camera 自己也有一份后处理配置,可以覆盖或者混合节点树中的后处理配置。

添加方式

在 Actor 面板拖入即可,且模版自带 GlobalPostProcessVolume

image

配置方式

拖入后,在 Details 面板可以配置相应后处理,和 Unity 差不多,Infinite Extent 勾上时对应 Unity 的 Global mode 。

Unity 使用 Profile 来共享后处理配置,Unreal 复制Actor后每一份都是 Instance,不共享配置。

image

整体调研总结

拓展后处理方案

从功能上看的确 Unity 最全面,但是考虑到 Unity 的3种实现方案都离不开 Renderer Feature,但是 6000 版本开始明显希望将 Volume 的逻辑从 Feature 拆开来,然后在 Volume 组件中直接添加自定义 Volume,但是由于 Feature 的历史包袱,还是需要在两个地方分别进行绑定自定义后处理。

所以我们的设计着重要解决下面几个问题:

  • 在后处理组件的地方,统一而且自然的使用内置 uber 后处理,以及添加自定义后处理。
  • 自定义后处理要和内置后处理使用同一个维度,在引擎/编辑器里面统一调整属性,执行时机。
  • 合理设计 Local/Global 模式,满足全局/碰撞体后处理,以及在单个/多个相机下的各种情况。

引擎改动点:

  • 将后处理从 Scene.postProcessManager 改成Component,放到 Entity 下面。
  • renderer_BlitTexture 作为纹理输入源。其他 Uniform/Macro 在脚本里面自己组织,即映射 UI 属性。
  • 不使用 Renderer Feature 维度,直接在后处理组件中使用内置和拓展后处理。
  • 隐藏 Blit 操作,在 onEnd 钩子里根据执行时机,自动决策渲染到哪个 render target。

引擎设计

yuque_diagram

  • Manager 的 getFinalEffect 方法用来模拟 VolumeStack 的实现,管理后处理效果的最终数值。
  • Manager 创建双 RT,自动管理自定义后处理的 blitMaterial 到 uberMaterial 的切换。

编辑器设计

  • 之前放在 Scene 面板的后处理移到 Entity 下面,作为 Component 的一种。

  • 模版默认内置 GlobalPostProcess 节点,自带 PostComponent,Priority 为 0,isGlobal 为 true。

  • 增强脚本参数:支持双语 Tooltip 和回调事件。

    import { Script } from '@galacean/engine';
    import { inspect } from "@galacean/editor-decorators";
    
    export default class extends Script {
    	blitMaterial: Material
    
      @inspect('Number', {
        min: 0, // 最小值
        max: 10, // 最大值
        dragStep: 0.1, // 拖拽步长
        property: 'rotate', // 对应到引擎对象的属性名,默认为装饰器所修饰的属性名
        label: 'Rotate', // 在检查器面板中显示的名称,默认为装饰器所修饰的属性名
        info: 'Rotate speed', // 在检查器面板中显示的描述信息,
    
        // 需要新增
        i18n:{zh,en},
    		onChange:(value, component)=>{
    			component.blitMaterial.shaderData.setFloat("material_Intensity", value);
    		}
      })
      intensity = 1;
    }
    
    • 需要支持代码类资产的 ProjectLoader 解析,不然解析不了脚本。

zhuxudong avatar Aug 20 '24 08:08 zhuxudong