Render Feature 相当于对于 SRP 注入了自定义的部分,可以实现比如后处理、中间量记录 等等功能

在 Unity 6 / URP 17 之后,Renderer Feature 的基本结构没变,但 ScriptableRenderPass 默认改为 Render Graph:

ScriptableRendererFeature
    ├── Create()
    ├── AddRenderPasses()
    └── ScriptableRenderPass
            └── RecordRenderGraph()
            

旧版常用的 OnCameraSetup + Execute + CommandBuffer 属于 Compatibility Mode

那么 Render Graph 有什么好处呢

现在RT之类的资源管理(生命周期、读写并行等等)基本就全权交给 Unity 内部了,而且也有 Render Graph 窗口方便的查看所有RT的使用情况什么的

# 大致架构

目前大概有两个主要需要考虑的类

ScriptableRendererFeature 和 ScriptableRenderPass

我这边版本的默认的模板似乎 ScriptableRenderPass 是 ScriptableRendererFeature 的内部类,不过我们把他们当成两个比较独立的模块考虑,或者当成上下级也行,两者没有特别杂糅

重新捋一下就是

ScriptableRendererFeature 负责创建 ScriptableRenderPass 与参数对接

ScriptableRenderPass 负责处理资源和渲染pass

下面我们对这两个类进行比较详细的介绍,利用一个后处理的案例

# ScriptableRendererFeature

它负责

  • 暴露可调节参数,和 monobehaviour 差不多
  • 初始化创建 ScriptableRenderPass 等内容,Create 方法
  • 渲染流程中注入 Pass,AddRenderPasses 方法,按相机每帧调用
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;

public sealed class CustomFullScreenFeature : ScriptableRendererFeature
{
    [System.Serializable]
    public sealed class Settings
    {
        public RenderPassEvent renderPassEvent =
            RenderPassEvent.AfterRenderingPostProcessing;

        public Shader shader;

        [Range(0f, 1f)]
        public float intensity = 1f;
    }

    [SerializeField]
    private Settings settings = new();

    private Material material;
    private CustomFullScreenPass renderPass;

    public override void Create()
    {
        if (settings.shader == null)
            return;

        // Inspector 中修改 Feature 时,Create 可能再次调用。
        CoreUtils.Destroy(material);

        material = CoreUtils.CreateEngineMaterial(settings.shader);

        renderPass = new CustomFullScreenPass(material)
        {
            renderPassEvent = settings.renderPassEvent
        };
    }

    public override void AddRenderPasses(
        ScriptableRenderer renderer,
        ref RenderingData renderingData)
    {
        if (renderPass == null || material == null)
            return;

        // 通常避免作用于材质预览、反射探针等特殊相机。
        if (renderingData.cameraData.cameraType != CameraType.Game)
            return;

        material.SetFloat("_Intensity", settings.intensity);

        renderer.EnqueuePass(renderPass);
    }

    protected override void Dispose(bool disposing)
    {
        CoreUtils.Destroy(material);
    }
}

# Render Graph Pass

负责处理一个 Render Pass 做了什么

using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.RenderGraphModule;
using UnityEngine.Rendering.RenderGraphModule.Util;
using UnityEngine.Rendering.Universal;

public sealed class CustomFullScreenPass : ScriptableRenderPass
{
    private const string CopyPassName = "Custom Full Screen Copy";
    private const string EffectPassName = "Custom Full Screen Effect";

    private readonly Material material;

    public CustomFullScreenPass(Material material)
    {
        this.material = material;

        // 声明这个 Pass 会读取颜色缓冲。
        ConfigureInput(ScriptableRenderPassInput.Color);
    }

    // 核心方法
    public override void RecordRenderGraph(
        RenderGraph renderGraph,
        ContextContainer frameData)
    {
        if (material == null)
            return;

        // 从Render Pipeline 拿到需要的数据
        UniversalResourceData resourceData =
            frameData.Get<UniversalResourceData>();

        UniversalCameraData cameraData =
            frameData.Get<UniversalCameraData>();

        TextureHandle cameraColor = resourceData.activeColorTexture;

        if (!cameraColor.IsValid())
            return;

        // 创建RT需要 Descriptor 作为初始化参数

        RenderTextureDescriptor descriptor =
            cameraData.cameraTargetDescriptor;

        descriptor.depthBufferBits = 0;
        descriptor.msaaSamples = 1;

        TextureHandle temporaryTexture =
            UniversalRenderer.CreateRenderGraphTexture(
                renderGraph,
                descriptor,
                "_CustomFullScreenTemporary",
                clear: false
            );

        if (!temporaryTexture.IsValid())
            return;

        /*
         * 不能直接安全地:
         *
         * cameraColor -> cameraColor
         *
         * 同一个 RenderGraph Pass 不能对同一个 RT 同时声明 Read 和 Write
         *
         * 所以使用:
         * cameraColor -> temporaryTexture -> cameraColor
         */

        var copyParameters =
            new RenderGraphUtils.BlitMaterialParameters(
                cameraColor, // 输入RT
                temporaryTexture, // 输出RT
                material, // 后处理材质
                shaderPass: 0 // 材质shader的第几个Pass
            );

        renderGraph.AddBlitPass(
            copyParameters,
            EffectPassName
        );

        var returnParameters =
            new RenderGraphUtils.BlitMaterialParameters(
                temporaryTexture,
                cameraColor,
                material: null,
                shaderPass: 0
            );

        renderGraph.AddBlitPass(
            returnParameters,
            CopyPassName
        );
    }
}

这里使用 UniversalResourceData.activeColorTexture 获得当前相机颜色,临时纹理由 Render Graph 管理,再通过两个 AddBlitPass 完成:

相机颜色
   ↓ 使用效果材质
临时纹理
   ↓ 普通复制
相机颜色

# Shader

shader显然不是这篇文章的重点,但是也贴上来吧,保证可以跑通流程,也能一定程度解释前面的Intensity参数啥的

Shader "Hidden/CustomFullScreenEffect"
{
    SubShader
    {
        Tags
        {
            "RenderPipeline" = "UniversalPipeline"
        }

        ZWrite Off
        ZTest Always
        Cull Off

        Pass
        {
            Name "Custom Full Screen Effect"

            HLSLPROGRAM

            #pragma vertex Vert
            #pragma fragment Frag

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

            float _Intensity;

            half4 Frag(Varyings input) : SV_Target
            {
                UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(input);

                float2 uv = input.texcoord;

                half4 color = SAMPLE_TEXTURE2D_X(
                    _BlitTexture,
                    sampler_LinearClamp,
                    uv
                );

                half luminance = dot(
                    color.rgb,
                    half3(0.2126h, 0.7152h, 0.0722h)
                );

                half3 grayscale = luminance.xxx;

                color.rgb = lerp(
                    color.rgb,
                    grayscale,
                    _Intensity
                );

                return color;
            }

            ENDHLSL
        }
    }
}

# 添加到 Render Data

Render Data 配置文件添加 Renderer Feature 能找到我们写的了

# 写法对比

# 旧版

public override void OnCameraSetup(
    CommandBuffer cmd,
    ref RenderingData renderingData)
{
}

public override void Execute(
    ScriptableRenderContext context,
    ref RenderingData renderingData)
{
    CommandBuffer cmd = CommandBufferPool.Get();

    Blitter.BlitCameraTexture(cmd, source, destination, material, 0);

    context.ExecuteCommandBuffer(cmd);
    CommandBufferPool.Release(cmd);
}

# 新版

public override void RecordRenderGraph(
    RenderGraph renderGraph,
    ContextContainer frameData)
{
    UniversalResourceData resources =
        frameData.Get<UniversalResourceData>();

    TextureHandle source = resources.activeColorTexture;

    renderGraph.AddBlitPass(...);
}

# 主要变化

旧 APIRender Graph
Execute()RecordRenderGraph()
RTHandleTextureHandle
自己获取 CommandBufferRender Graph 执行阶段提供
renderer.cameraColorTargetHandleUniversalResourceData.activeColorTexture
RenderingData.cameraDataUniversalCameraData
手动管理临时 RTCreateRenderGraphTexture
Blitter.BlitCameraTextureAddBlitPass 或 Raster Pass

RecordRenderGraph() 本身主要负责声明 Pass、输入、输出和资源依赖;真正的 GPU 命令会在 Render Graph 后续执行阶段记录

# 让我们更深入一些

# Pass

注意和Shader的Pass区分开,这里是Render Graph的Pass,类似是一次操作之类的

Pass 类型Context能画 MeshDispatch ComputeRenderGraph 优化常见用途
Raster PassRasterGraphContext⭐⭐⭐⭐⭐后处理、Forward、Deferred、阴影、GBuffer、全屏 Pass
Compute PassComputeGraphContext⭐⭐⭐⭐⭐GPU 粒子、FFT、Prefix Sum、体素、GPU Culling、SDF
Unsafe PassUnsafeGraphContext⭐⭐兼容旧 CommandBuffer、插件、遗留渲染代码
Low Level Pass低层 Context视接口而定视接口而定⭐⭐⭐底层渲染控制、HDRP 或高级定制场景

# Raster Pass

using (var builder = renderGraph.AddRasterRenderPass<PassData>(
    "My Pass",
    out var passData))
{
    builder.UseTexture(...);
    builder.SetRenderAttachment(...);

    builder.SetRenderFunc((PassData data, RasterGraphContext ctx) =>
    {
        // DrawMesh
        // DrawRendererList
        // Blitter
        // SetGlobalTexture
    });
}
# Blit Pass

最上面的案例使用的

简化的 Raster Pass,语法糖之类的?后处理什么的经常用

不过底层是同类型的 Pass

renderGraph.AddBlitPass(...)

using (var builder =
    renderGraph.AddRasterRenderPass<PassData>(
        "Blit",
        out var passData))
{
    builder.UseTexture(source, AccessFlags.Read);

    builder.SetRenderAttachment(
        destination,
        0,
        AccessFlags.Write);

    builder.SetRenderFunc((data, ctx) =>
    {
        Blitter.BlitTexture(
            ctx.cmd,
            data.source,
            ...);
    });
}

# Compute Pass

用于 Dispatch Compute Shader

using(var builder =
    renderGraph.AddComputePass<PassData>(
        "Compute",
        out var passData))
{
    builder.UseTexture(texture, AccessFlags.ReadWrite);

    builder.SetRenderFunc((PassData data, ComputeGraphContext ctx)=>
    {
        ctx.cmd.DispatchCompute(...);
    });
}

# Unsafe Pass

RenderGraph 不再帮你保证状态,直接用 CommandBuffer

一般用于把旧代码直接搬过来

# LowLevel Pass

LowLevel Pass 的基本结构与 Raster Pass 类似,但这里 Render Graph 只负责资源生命周期和依赖;RenderTarget、Viewport、Clear、Draw、Dispatch 等状态要由你自己设置。

using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.RenderGraphModule;

class PassData
{
    public TextureHandle source;
    public TextureHandle destination;
    public Material material;
}

static void AddLowLevelPass(
    RenderGraph renderGraph,
    TextureHandle source,
    TextureHandle destination,
    Material material)
{
    using var builder =
        renderGraph.AddLowLevelPass<PassData>(
            "My LowLevel Pass",
            out var passData);

    passData.source = source;
    passData.destination = destination;
    passData.material = material;

    // 仍然必须向 Render Graph 声明资源依赖
    builder.UseTexture(source, AccessFlags.Read);
    builder.UseTexture(destination, AccessFlags.Write);

    builder.SetRenderFunc(
        static (PassData data, LowLevelGraphContext context) =>
        {
            // LowLevel Pass 不会自动绑定 RT
            context.cmd.SetRenderTarget(data.destination);

            // 自己设置 Viewport、Clear 等状态
            context.cmd.ClearRenderTarget(
                clearDepth: false,
                clearColor: true,
                backgroundColor: Color.black);

            // 绘制全屏三角形
            context.cmd.DrawProcedural(
                Matrix4x4.identity,
                data.material,
                shaderPass: 0,
                MeshTopology.Triangles,
                vertexCount: 3,
                instanceCount: 1);
        });
}

# 中间值

ContextContainer 是当前帧数据的集合。

方法作用
Get<T>()获取已经存在的帧数据
GetOrCreate<T>()获取数据,不存在则创建
Contains<T>()检查是否存在指定数据
Dispose()清理容器内容,通常不手动调用

比如

UniversalResourceData resourceData =
    frameData.Get<UniversalResourceData>();

UniversalCameraData cameraData =
    frameData.Get<UniversalCameraData>();

UniversalRenderingData renderingData =
    frameData.Get<UniversalRenderingData>();

UniversalLightData lightData =
    frameData.Get<UniversalLightData>();

自定义数据,比如这里我们传递纹理句柄

public sealed class MyFrameData : ContextItem
{
    public TextureHandle outlineTexture;

    public override void Reset()
    {
        outlineTexture = TextureHandle.nullHandle;
    }
}

写入:

MyFrameData data = frameData.GetOrCreate<MyFrameData>();
data.outlineTexture = texture;

另一个 Pass 读取:

MyFrameData data = frameData.Get<MyFrameData>();
TextureHandle texture = data.outlineTexture;

# 常用预定义中间值

# UniversalResourceData

成员含义常见用途
activeColorTexture当前活动颜色纹理全屏后处理输入
activeDepthTexture当前活动深度附件深度测试或深度读取
cameraColor相机颜色目标访问相机颜色
cameraDepth相机深度目标访问相机深度
cameraOpaqueTextureURP Opaque Texture折射、扭曲
cameraDepthTexture可采样的相机深度纹理深度重建
cameraNormalsTexture相机法线纹理描边、SSAO
motionVectorColor运动矢量颜色纹理TAA、运动模糊
motionVectorDepth运动矢量对应深度运动矢量处理
gbufferDeferred 的 GBuffer 数组自定义延迟效果
mainShadowsTexture主光源阴影纹理自定义阴影采样
additionalShadowsTexture附加光阴影纹理多光源阴影
ssaoTextureSSAO 结果自定义合成
isActiveTargetBackBuffer当前目标是否直接是后备缓冲判断能否安全采样

# UniversalCameraData

成员含义
camera当前 Unity Camera
cameraTypeGame、SceneView、Preview 等相机类型
cameraTargetDescriptor相机目标纹理描述
renderTypeBase Camera 或 Overlay Camera
resolveFinalTarget当前相机是否输出最终结果
isSceneViewCamera是否 Scene View
isPreviewCamera是否 Preview Camera
isHdrEnabled是否启用 HDR
postProcessEnabled是否开启后处理
requiresDepthTexture是否需要深度纹理
requiresOpaqueTexture是否需要 Opaque Texture
renderer当前 ScriptableRenderer
xrXR 相关信息
defaultOpaqueSortFlags默认不透明排序方式
cameraStackCamera Stack 信息
worldSpaceCameraPos世界空间相机位置

创建中间纹理时通常取:

RenderTextureDescriptor descriptor =
    cameraData.cameraTargetDescriptor;

descriptor.depthBufferBits = 0;
descriptor.msaaSamples = 1;

# UniversalRenderingData

这个类主要用于绘制场景中的 Renderer,而不是单纯做全屏 Blit。

成员含义
cullResults相机剔除结果
supportsDynamicBatching是否支持 Dynamic Batching
perObjectDataShader 需要的逐物体数据
renderingMode当前渲染模式
stencilLodCrossFadeEnabledLOD Cross Fade 模板支持

最重要的是:

renderingData.cullResults

它会用于创建 RendererList:

RendererListParams rendererListParams =
    new RendererListParams(
        renderingData.cullResults,
        drawingSettings,
        filteringSettings
    );

Renderer List 可以理解为 根据剔除结果、Shader Pass、Layer、RenderQueue 和排序规则筛选出来的一批待绘制物体

private sealed class PassData
{
    public RendererListHandle rendererList;
}

using IRasterRenderGraphBuilder builder =
    renderGraph.AddRasterRenderPass<PassData>(
        "Draw Objects",
        out PassData passData
    );

passData.rendererList =
    renderGraph.CreateRendererList(rendererListParams);

builder.UseRendererList(passData.rendererList);
builder.SetRenderAttachment(color, 0, AccessFlags.Write);
builder.SetRenderAttachmentDepth(depth, AccessFlags.Write);

builder.SetRenderFunc(
    static (PassData data, RasterGraphContext context) =>
    {
        context.cmd.DrawRendererList(data.rendererList);
    }
);

# 纹理相关

TextureHandle、TextureDesc 是Render Graph 特有的系统,其中 TextureHandle 只是一个标签,让渲染管线去编排之类的,运行时会创建RTHandle,但只有RTHandle会对应到真实的显存数据等等

# TextureHandle
成员 / 用法作用
IsValid()判断句柄是否有效
nullHandle空纹理句柄
renderGraph.CreateTexture()创建 Render Graph 管理的纹理
renderGraph.ImportTexture()导入外部 RTHandle
# TextureDesc
成员作用
width / height固定宽高
scale相对相机尺寸缩放
colorFormat颜色格式
depthBufferBits深度位数
msaaSamplesMSAA 样本数
clearBuffer第一次使用前是否清理
clearColor清理颜色
filterModePoint / Bilinear / Trilinear
wrapModeClamp / Repeat
enableRandomWrite是否允许 Compute Shader UAV 写入
useMipMap是否创建 Mipmap
autoGenerateMips是否自动生成 Mipmap
dimension2D、2DArray、3D、Cube
slices数组层数
name调试名称
# RenderTextureDescriptor

这是 Unity 原有的纹理描述结构,不是 Render Graph 专属,但在 URP 中非常常用。

URP 提供了快捷创建方法:

TextureHandle temp =
    UniversalRenderer.CreateRenderGraphTexture(
        renderGraph,
        descriptor,
        "_TemporaryColor",
        clear: false
    );
成员作用
width / height分辨率
graphicsFormat像素格式
depthBufferBits深度位数
msaaSamplesMSAA
volumeDepth纹理层数
dimension纹理维度
enableRandomWriteCompute 写入
useMipMapMipmap
sRGB是否 sRGB
vrUsageXR 用途

# Volume

自定义后处理通常还会配合 Volume 系统

就可以放到 场景的volume组件内 override 了

[System.Serializable]
[VolumeComponentMenu("Custom/My Effect")]
public sealed class MyVolumeComponent :
    VolumeComponent,
    IPostProcessComponent
{
    public ClampedFloatParameter intensity =
        new ClampedFloatParameter(0f, 0f, 1f);

    public bool IsActive()
    {
        return intensity.value > 0f;
    }
}
作用
VolumeComponent自定义 Volume 组件基类
IPostProcessComponent标识组件是否为后处理
VolumeManager访问当前 Volume Stack
VolumeStack当前相机混合后的 Volume 结果
VolumeParameter<T>Volume 参数基类
ClampedFloatParameter限制范围的 float
FloatParameterfloat 参数
BoolParameterbool 参数
ColorParameterColor 参数
TextureParameterTexture 参数
Vector2ParameterVector2 参数
Vector3ParameterVector3 参数
NoInterpFloatParameter不插值的 float