Banner

「Custom SRP」:渲染管线架构与 Render Graph

Note 1 · 渲染管线架构与 Render Graph

系列首篇。本篇定位是引擎的”执行骨架”——它定义了所有其他系统(光照、阴影、后处理、相机管理)的宿主形态。理解这一篇,后续每个系统的接入方式都不再陌生。

TL;DR

  • 声明式资源生命周期:Render Graph 把资源依赖、Pass 顺序、attachment 绑定全部显式化。引擎在编译期完成依赖分析、裁剪冗余 Pass、规划 RT 复用——开发者只声明意图,不管理生命周期。
  • Raster Pass 合并机制:禁止 Pass 内切换 RenderTarget 是为了让引擎能把相邻 Pass 合并为单个原生 render pass。在移动端 TBR 架构下,attachment 全程留在片上内存,省下整屏量级的带宽往返(详见 §7)。
  • RendererList 剔除前置:所有几何提交统一到 RendererListHandle。剔除可并行调度,空 List 触发 Pass 自动消除——dead-code elimination 在渲染管线层的体现。
  • API 演进基线:全文基于 Unity 6.3 的 AddRasterRenderPass / SetRenderAttachment / RecordAndExecute 现代范式,已废弃的 AddRenderPass 与手动 SetRenderTarget 不再涉及。

1. SRP 的执行模型

SRP 的本质是把渲染管线变成一个用户态实现。Unity 引擎只负责剔除、网格上传、命令提交、原生 API 调用等底层任务,而”什么时候画什么、用什么 RT、按什么顺序”则全部交由 C# 决定。

整个体系建立在两个核心类型之上:

  • RenderPipelineAssetScriptableObject,作为配置容器与工厂,负责创建 RenderPipeline 实例
  • RenderPipeline — 实际承担渲染工作的对象,每帧由引擎调用其 Render 方法
1
2
3
4
5
6
7
8
9
10
11
12
13
[CreateAssetMenu(menuName = "Rendering/Custom Render Pipeline")]
public partial class CustomRenderPipelineAsset : RenderPipelineAsset
{
[SerializeField] CameraBufferSettings cameraBuffer;
[SerializeField] ShadowSettings shadows;
[SerializeField] PostFXSettings postFXSettings;

protected override RenderPipeline CreatePipeline()
{
return new CustomRenderPipeline(
cameraBuffer, shadows, postFXSettings, ...);
}
}

CustomRenderPipeline 重写 Render(ScriptableRenderContext, List<Camera>),按相机列表依次驱动渲染:

1
2
3
4
5
6
7
8
protected override void Render(
ScriptableRenderContext context, List<Camera> cameras)
{
for (int i = 0; i < cameras.Count; i++)
{
renderer.Render(renderGraph, context, cameras[i], settings);
}
}

ScriptableRenderContext 是与底层引擎之间的桥梁,提供剔除、查询场景资源、提交命令等接口。但在 Render Graph 时代,它退到次要位置——绝大部分工作通过 RenderGraph 对象进行声明式描述,只有命令缓冲的最终提交仍然落到 context.ExecuteCommandBuffer / Submit 上。


2. Render Graph:声明式渲染

2.1 设计动机

传统命令式渲染存在三个无法回避的问题。第一,RT 的生命周期由开发者手动管理,资源复用与释放容易出错或浪费。第二,Pass 之间的依赖关系隐式编码在代码顺序里,引擎无法据此做调度优化。第三,tile-based GPU 想要利用片上内存做 frame-buffer fetch,必须知道哪些 Pass 可以合并到同一原生 render pass 内——而这种结构化信息在传统命令流中是缺失的。

Render Graph 把这些信息全部显式化:每个 Pass 在录制阶段声明它读哪些资源、写哪些资源、绑定哪些 attachment。引擎在执行前先做依赖分析,再决定真正的资源分配、Pass 调度、Pass 合并。

2.2 资源句柄

Render Graph 中所有资源以句柄形式存在:TextureHandleBufferHandleRendererListHandle。这些句柄是引用,不是对象——真正的 GPU 资源在 Compile 阶段才被分配,并按生命周期被多个句柄复用。

1
2
3
4
5
6
TextureHandle colorAttachment = renderGraph.CreateTexture(new TextureDesc {
width = bufferSize.x,
height = bufferSize.y,
format = colorFormat,
name = "Color Attachment",
});

句柄的意义在录制阶段是”占位符”,在执行阶段才被解析为真实的 RenderTargetIdentifier

2.3 录制与执行分离

整个相机渲染包裹在一对 RecordAndExecute 调用里:

1
2
3
4
5
6
7
8
9
10
11
12
13
using (renderGraph.RecordAndExecute(renderGraphParameters))
{
SetupPass.Record(...);
LightingPass.Record(...);
ShadowsPass.Record(...);
GeometryPass.Record(...);
SkyboxPass.Record(...);
CopyAttachmentsPass.Record(...);
PostFXPass.Record(...);
FinalPass.Record(...);
DebugPass.Record(...);
GizmosPass.Record(...);
}

using (renderGraph.RecordAndExecute(...)) 是 Unity 6 引入 Render Graph 后的现代范式。RecordAndExecute 返回一个 IDisposable:录制阶段在 using 块内进行,块退出时触发 Dispose,引擎在此刻才完成依赖编译并执行图。相对手写 Begin/End 配对的核心优势是异常安全——录制中途抛错时引擎仍能正确释放资源、关闭 profiling scope,不会留下损坏的图状态。

RecordAndExecute 块结束时,Render Graph 完成四步工作:

  1. 分析所有 Pass 的输入输出依赖
  2. 裁剪没有任何下游消费的 Pass(dead code elimination)
  3. 决定每个资源的实际分配时机和复用方式
  4. 在原生层执行——Unity 6.3 起默认启用 native render passes

RenderGraphParameters 携带本次录制所需的上下文:

1
2
3
4
5
6
7
8
var rgParameters = new RenderGraphParameters
{
commandBuffer = CommandBufferPool.Get(),
currentFrameIndex = Time.frameCount,
executionName = cameraSampler.name,
rendererListCulling = true,
scriptableRenderContext = context,
};

rendererListCulling = true 是关键开关——它允许引擎根据 RendererList 是否为空,自动裁剪不会产生绘制的 Pass。


3. Pass 类型与实现模式

3.1 三种 Pass 类型

Render Graph 把 Pass 分为三档,限制由松到紧:

类型 API 用途 关键限制
Unsafe Pass AddUnsafePass 需要任意 CommandBuffer 操作的特殊场景 不参与合并、不参与 native pass 优化
Compute Pass AddComputePass 纯 Compute Shader 工作(如 LUT 生成、Tile Forward+ 光源分配) 不能写 attachment
Raster Pass AddRasterRenderPass 标准光栅化绘制 Pass 内不允许 SetRenderTarget;attachment 必须显式声明

Raster Pass 是核心。它的限制——禁止 Pass 内切换 RT——正是引擎能做合并优化的前提。当连续多个 Raster Pass 渲染到同一组 attachment 时,引擎可以把它们合并为单个原生 render pass,让中间结果留在 tile-based GPU 的片上内存里,避免一次往返显存的带宽开销。

Unsafe Pass 是过渡性兜底:当某个 Pass 暂时无法满足 Raster Pass 的限制时,先用它跑通;后续再逐步收敛到 Raster Pass。Compute Pass 专门用来组织 Compute Shader 的 Dispatch。

3.2 标准 Pass 实现模板

每个 Pass 在 Custom SRP 里通常是一个独立类,约定包含三部分:

  • 一个数据结构(Pass Data),由 Render Graph 内部池化复用
  • 一个静态 Record 方法,在录制阶段调用,负责声明资源与注册执行函数
  • 一个静态 Render 方法,由 Render Graph 在执行阶段回调

GeometryPass 为例:

CSHARP
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
public class GeometryPass
{
static readonly ProfilingSampler
samplerOpaque = new("Opaque Geometry"),
samplerTransparent = new("Transparent Geometry");

RendererListHandle list;

static void Render(GeometryPass pass, RasterGraphContext context)
{
context.cmd.DrawRendererList(pass.list);
}

public static void Record(
RenderGraph renderGraph,
Camera camera,
CullingResults cullingResults,
bool opaque,
in CameraRendererTextures textures,
in LightResources lightData)
{
ProfilingSampler sampler = opaque ? samplerOpaque : samplerTransparent;

using var builder = renderGraph.AddRasterRenderPass(
sampler.name, out GeometryPass pass, sampler);

pass.list = builder.UseRendererList(
renderGraph.CreateRendererList(
new RendererListDesc(shaderTagId, cullingResults, camera) { ... }));

builder.SetRenderAttachment(textures.colorAttachment, 0);
builder.SetRenderAttachmentDepth(textures.depthAttachment);
builder.UseBuffer(lightData.directionalLightDataBuffer);

builder.SetRenderFunc<GeometryPass>(Render);
}
}

这个模板有几个值得记住的约束。Pass Data 实例由 Render Graph 池化,不要在其上保存帧间状态Render 函数签名固定为 (PassData, RasterGraphContext),函数体只能通过 context.cmd(一个受限的 RasterCommandBuffer)发出命令。SetRenderFunc 调用必须在 builder 销毁之前完成——这正是 using 语法承担的职责。

3.3 资源声明 API

builder 提供三类资源声明方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 作为采样源(不写入)
builder.UseTexture(handle, AccessFlags.Read);

// 作为 color attachment
builder.SetRenderAttachment(handle, index: 0, AccessFlags.Write);

// 作为 depth attachment
builder.SetRenderAttachmentDepth(handle, AccessFlags.ReadWrite);

// 作为 RendererList(同时声明几何依赖)
pass.list = builder.UseRendererList(listHandle);

// 作为 ComputeBuffer / StructuredBuffer
builder.UseBuffer(bufferHandle, AccessFlags.Read);

SetRenderAttachment 的 attachment index 在使用 MRT(Multiple Render Target)时区分多个颜色目标。Skybox 这类只写颜色不写深度的 Pass,需要把深度 attachment 的访问权限设为只读:

1
builder.SetRenderAttachmentDepth(textures.depthAttachment, AccessFlags.Read);

attachment 的绑定与解绑由引擎在 Pass 起止时自动完成。Pass 函数体内调用 SetRenderTarget 是非法的——这条规则一旦违反,所有合并优化都会失效。

3.4 Native Render Passes 与 Pass 合并

Unity 6.3 起,renderGraph.nativeRenderPassesEnabled 默认为 true。引擎主动尝试把相邻 Raster Pass 合并为单个原生 render pass。在 Render Graph Viewer 中,被合并的 Pass 会以蓝色横条标记,并显示合并的判断依据。

合并发生需要满足以下条件:

  • 都是 Raster Pass(Unsafe Pass、Compute Pass 一律打断合并)
  • attachment 集合视为同一组:颜色与深度的数量、格式、load/store 行为兼容
  • 没有外部依赖把它们隔开(比如某个中间 Pass 把 attachment 当纹理采样了)

典型的合并组合:Skybox + 不透明几何(同一对 color/depth);透明几何 + Unsupported Shaders 调试 Pass;颜色与深度的两个独立 Copy Pass。

合并是引擎层的优化,业务代码不需要改动,但业务代码必须正确遵守 Raster Pass 的所有限制——任何错误的 attachment 声明都会让引擎无法判定 Pass 可合并。


4. Renderer Lists:几何提交的统一句柄

RendererList 是几何剔除与提交的统一抽象。在 Render Graph 时代,几何不再通过 context.DrawRenderers 直接提交,而是先创建 RendererListHandle,再绑定到 Pass 上由引擎统一调度。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
RendererListHandle list = renderGraph.CreateRendererList(
new RendererListDesc(shaderTagId, cullingResults, camera)
{
sortingCriteria = SortingCriteria.CommonOpaque,
renderQueueRange = RenderQueueRange.opaque,
rendererConfiguration =
PerObjectData.ReflectionProbes |
PerObjectData.Lightmaps |
PerObjectData.ShadowMask |
PerObjectData.LightProbe |
PerObjectData.OcclusionProbe |
PerObjectData.LightProbeProxyVolume |
PerObjectData.OcclusionProbeProxyVolume,
});

通过 builder.UseRendererList(list) 注册到 Pass 后,引擎在执行时会自动安排剔除任务。这带来两个直接收益。其一,所有 RendererList 的剔除工作可以并行调度——传统 cull-draw-cull-draw 的串行依赖被打破,剔除阶段集中前置,绘制阶段集中后置。其二,空 List 触发 Pass 自动裁剪:如果一个 Pass 仅依赖一个 RendererList,且 List 中无可见对象,整个 Pass 会被引擎在 Compile 阶段直接消除。

执行阶段的提交一行搞定:

1
context.cmd.DrawRendererList(list);

需要特别记住一条限制:RendererList 不可复用。每个 List 只允许在一个 Pass 中绘制一次。点光源的 6 面 cube shadow 必须创建 6 个独立的 RendererList,即使每个 List 的 ShadowDrawingSettings 完全相同。

阴影 List 与天空盒 List 各有专门构造接口:

1
2
renderGraph.CreateShadowRendererList(in shadowDrawingSettings);
renderGraph.CreateSkyboxRendererList(camera);

天空盒还有一个特殊点:它在引擎眼里不算几何,所以默认的”裁剪空 Pass”逻辑会误删 SkyboxPass。需要显式声明该 Pass 不允许被裁剪:

1
builder.AllowPassCulling(false);

5. 相机渲染编排

5.1 单相机执行流

每个 Camera 独立完成一次 Render Graph 录制与执行。整体生命周期如下:

flowchart TD
    A([Camera 渲染入口]) --> B[解析 per-camera 配置
Sampler / CameraSettings] B --> C[context.Cull
→ CullingResults] C --> D[RecordAndExecute 块开启] D --> E[Record Phase
SetupPass · Lighting · Shadows
Geometry · Skybox · CopyAttachments
PostFX · Debug · Gizmos
逐一声明资源与执行函数] E --> F[Compile Phase
依赖分析 · 裁剪空 Pass · RT 复用规划] F --> G[Execute Phase
Native Render Pass 合并 · 命令录制] G --> H[ExecuteCommandBuffer · Submit] H --> I([帧提交完成]) style D fill:#e3f2fd,stroke:#1976d2,stroke-width:2px style E fill:#e3f2fd,stroke:#1976d2 style F fill:#fff3e0,stroke:#f57c00,stroke-width:2px style G fill:#e8f5e9,stroke:#388e3c,stroke-width:2px

CameraRenderer.Render 是这条生命线的代码实现:

CSHARP
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
public void Render(
RenderGraph renderGraph,
ScriptableRenderContext context,
Camera camera,
CustomRenderPipelineSettings settings)
{
// 1. 解析 per-camera 配置
ProfilingSampler cameraSampler;
CameraSettings cameraSettings;
if (camera.TryGetComponent(out CustomRenderPipelineCamera crpCamera))
{
cameraSampler = crpCamera.Sampler;
cameraSettings = crpCamera.Settings;
}
else
{
cameraSampler = GetDefaultProfileSampler(camera);
cameraSettings = settings.defaultCameraSettings;
}

// 2. 剔除
if (!camera.TryGetCullingParameters(out var cullingParameters)) return;
cullingParameters.shadowDistance = Mathf.Min(
settings.shadows.maxDistance, camera.farClipPlane);
var cullingResults = context.Cull(ref cullingParameters);

// 3. 录制并执行 Render Graph
var rgParameters = new RenderGraphParameters
{
commandBuffer = CommandBufferPool.Get(),
currentFrameIndex = Time.frameCount,
executionName = cameraSampler.name,
rendererListCulling = true,
scriptableRenderContext = context,
};

using (renderGraph.RecordAndExecute(rgParameters))
{
using var _ = new RenderGraphProfilingScope(renderGraph, cameraSampler);

var textures = SetupPass.Record(renderGraph, ..., camera);
var lightResources = LightingPass.Record(renderGraph, cullingResults, ...);
ShadowsPass.Record(renderGraph, cullingResults, lightResources, ...);

GeometryPass.Record(renderGraph, camera, cullingResults,
opaque: true, textures, lightResources);
SkyboxPass.Record(renderGraph, camera, textures);
CopyAttachmentsPass.Record(renderGraph, ..., textures);
GeometryPass.Record(renderGraph, camera, cullingResults,
opaque: false, textures, lightResources);

if (hasActivePostFX)
PostFXPass.Record(renderGraph, postFXSettings, textures, ...);
else
FinalPass.Record(renderGraph, copier, textures);

DebugPass.Record(renderGraph, settings, camera, lightResources);
GizmosPass.Record(renderGraph, copier, textures);
}

// 4. 提交
context.ExecuteCommandBuffer(rgParameters.commandBuffer);
CommandBufferPool.Release(rgParameters.commandBuffer);
context.Submit();
}

整个流程的关键节奏:先做剔除拿到 CullingResults,再用它驱动整个 Render Graph 录制。所有 XxxPass.Record 调用都不立即执行——它们仅在图中登记意图。using 块退出时引擎才真正编译并执行。

5.2 Per-Camera 配置

CustomRenderPipelineCamera 是挂在 Camera GameObject 上的扩展组件,承载相机级别的设置:

1
2
3
4
5
6
7
8
9
10
11
[DisallowMultipleComponent, RequireComponent(typeof(Camera))]
public class CustomRenderPipelineCamera : MonoBehaviour
{
[SerializeField] CameraSettings settings = default;

ProfilingSampler sampler;
public ProfilingSampler Sampler =>
sampler ??= new ProfilingSampler(GetComponent<Camera>().name);

public CameraSettings Settings => settings ??= new CameraSettings();
}

CameraSettings 通常包含:自定义 final blend 模式、RenderingLayerMask、是否启用 PostFX、Render Scale 倍数等。具体字段会在 Note 8(相机系统与工程架构)展开。

ProfilingSampler 缓存有一个 editor-only 边界:相机改名后,缓存的 Sampler 仍然带着旧名字,Frame Debugger 会显示错误。解决方法是组件 OnEnable 时清除缓存:

1
2
3
#if UNITY_EDITOR || DEVELOPMENT_BUILD
void OnEnable() => sampler = null;
#endif

这样进入 Play Mode 或重新启用相机时,Sampler 会重建。

5.3 隐式相机

除了游戏相机,Render 还会被以下相机调用:Scene View、Material Preview、Realtime Reflection Probe、用户通过 Camera.Render() 主动触发的 manual render。这些相机一般没有 CustomRenderPipelineCamera 组件,需要 fallback:

1
2
3
4
5
6
7
8
static ProfilingSampler GetDefaultProfileSampler(Camera camera)
{
#if UNITY_EDITOR || DEVELOPMENT_BUILD
return ProfilingSampler.Get(camera.cameraType);
#else
return defaultSamplerCatchAll;
#endif
}

Unity 6 在 release build 中存在一个已知 bug:ProfilingSampler.Get 在某些情况下返回 null,会导致 reflection probe 渲染崩溃。生产环境用一个统一的 catch-all sampler 兜底是当前最稳妥的做法。

5.4 Render Graph Debug Data

Render Graph Viewer 需要相机渲染时启用 debug data 才能呈现内容。但启用会引入额外的内存分配与 CPU 开销,所以只对真正需要观察的相机开启。参考 URP 的做法,跳过预览相机和 manual render 请求:

1
2
3
4
5
if (camera.cameraType != CameraType.Preview &&
!camera.isProcessingRenderRequest)
{
renderGraph.RequestCaptureDebugData(cameraSampler.name);
}

调试完毕后关闭 Render Graph Viewer 窗口,引擎会自动停止 debug data 收集。


6. 调试工具索引

三套工具,各有职责。

Frame DebuggerWindow / Analysis / Frame Debugger
观察实际的 GPU 提交序列。每个 DrawCall、SetGlobalTexture、ClearRenderTarget 都展开为一行,可以单步检查中间 RT。Frame Debugger 反映的是命令流,看不到 Render Graph 的资源依赖。需要快速验证”某个像素是怎么画出来的”时首选。

Render Graph ViewerWindow / Analysis / Render Graph Viewer
观察 Pass 之间的依赖关系与资源生命周期。Pass 之间的连线对应资源读写,被裁剪的 Pass 标灰,被合并的 Raster Pass 序列以蓝色横条标记。Unity 6.3 后这是诊断 native render pass 优化是否生效的主要工具。注意打开窗口本身会触发 debug data 分配,调试完关闭。

RenderDoc
当 Frame Debugger 信息不够细时(Compute Shader 的 thread group 行为、tile memory 实际占用、API 提交层的真实顺序),RenderDoc 抓帧分析能给出引擎与 Frame Debugger 都无法呈现的硬件层细节。


7. TA Takeaway:从 API 设计到硬件带宽

Render Graph 的所有限制——Raster Pass 禁止 SetRenderTarget、attachment 必须显式声明、RendererList 不可复用——单看 API 显得繁琐,但每条限制都对应一个移动端硬件层面的带宽收益

7.1 TBR 架构下的带宽真相

主流移动 GPU(ARM Mali、Qualcomm Adreno、Apple Silicon、PowerVR)普遍采用 TBR / TBDR(Tile-Based / Deferred Rendering)架构。GPU 把屏幕划分为若干小 Tile(典型尺寸 16×16 至 64×64 像素),每个 Tile 在渲染时占用一块 On-chip Memory(片上内存,物理上是 SRAM),其访问延迟与带宽相比主显存(DRAM)有一个数量级的优势。

传统命令式管线频繁切换 RenderTarget 时,硬件被迫执行两个动作:

  • Store:把当前 Tile 的 color / depth 数据从片上内存回写到主显存
  • Load:把新目标对应的 Tile 数据从主显存加载回片上内存

每次 SetRenderTarget 都隐含全屏所有 Tile 的一轮 store-load 往返。以 1080p、4 byte color + 4 byte depth 计算,单次切换的带宽成本约为 16 MB。一帧若发生 5–10 次切换,叠加多相机栈与后处理链,仅 RT 切换就吞掉数百 MB——而移动端 GPU 的可用带宽预算典型值仅 25–50 GB/s。发热、降频、续航崩塌的根因,往往就是这个。

7.2 Render Graph 限制的硬件意义

把 Raster Pass 的核心限制和 TBR 行为对应起来看:

API 层限制 硬件层意义
Pass 内禁止 SetRenderTarget 保证 attachment 在 Pass 执行期间稳定,不触发中途的 store-load
SetRenderAttachment 必须显式声明 引擎能精确识别”哪些相邻 Pass 共享同一对 color/depth”
Native Render Pass 合并 共享 attachment 的 Raster Pass 在硬件层被压缩为单个原生 render pass:attachment 全程留在片上内存,整段链只在开始 Load、结束 Store

Render Graph Viewer 中的蓝色合并条,本质上就是在可视化”这些 Pass 之间省下了多少 store-load 往返”。在移动端项目里,这个数字直接决定 60 FPS 能否稳住、设备温度能否压住、电池续航如何。

7.3 实践推论

结合上述硬件视角,后续 7 篇笔记的所有 Pass 划分都遵循以下隐含原则:

  • 几何类 Pass 一律 Raster Pass。Skybox、不透明几何、半透明几何、debug 渲染都走 AddRasterRenderPass。Unsafe Pass 是兜底,不是默认。
  • 避免中途采样未结束的 attachment。在某个 Pass 中读取尚未结束的 color/depth attachment 作为纹理,会强制 store 到主存,切断整段合并链。如需要场景颜色或深度的副本,通过专门的 CopyAttachmentsPass 显式生成独立纹理,让原 attachment 留在合并链上。
  • PostFX 必然打断合并链。后处理需要采样上一阶段结果,store-load 无法避免——这是必要代价。但合并链应该在 PostFX 之前尽可能延长。

这条隐性约束会贯穿所有后续笔记。当某个 Pass 看似可以”图省事用 Unsafe Pass”时,要意识到代价是放弃了整段合并优化——在移动端这通常意味着几个毫秒的 GPU 时间和一个台阶的设备温度。


附录:项目文件与 Pass 类型速查

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Runtime/
├── CustomRenderPipelineAsset.cs // ScriptableObject 配置入口
├── CustomRenderPipeline.cs // RenderPipeline 实现
├── CustomRenderPipelineCamera.cs // 相机扩展组件
├── CustomRenderPipelineSettings.cs // 设置容器
├── CameraRenderer.cs // 单相机渲染入口
├── CameraRendererCopier.cs // RT 复制工具
├── CameraRendererTextures.cs // 相机纹理句柄包
├── CameraSettings.cs // 相机级别配置
└── Passes/
├── SetupPass.cs // 创建 attachments、清屏
├── LightingPass.cs // 光源数据 Buffer 上传
├── ShadowsPass.cs // 阴影 atlas 渲染
├── GeometryPass.cs // 不透明 / 透明几何(共用)
├── SkyboxPass.cs // 天空盒
├── CopyAttachmentsPass.cs // 颜色 / 深度备份
├── PostFXPass.cs // 后处理栈
├── FinalPass.cs // 直接输出(无 PostFX 路径)
├── DebugPass.cs // Tile Forward+ 等可视化
└── GizmosPass.cs // 编辑器 gizmos

每个 Pass 类的契约一致:私有 Pass Data 字段集合、一个静态 Record 方法负责录制、一个静态 Render 方法由 Render Graph 在执行阶段回调。后续每篇笔记中提到具体 Pass 时,都默认遵循这一形态。


本篇关键 API 速查

CSHARP
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
// RenderGraph 录制入口
using (renderGraph.RecordAndExecute(rgParameters)) { ... }

// 三种 Pass 注册
renderGraph.AddRasterRenderPass<TPassData>(name, out passData, sampler);
renderGraph.AddComputePass<TPassData>(name, out passData, sampler);
renderGraph.AddUnsafePass<TPassData>(name, out passData, sampler);

// 资源声明
builder.SetRenderAttachment(handle, index, AccessFlags);
builder.SetRenderAttachmentDepth(handle, AccessFlags);
builder.UseTexture(handle, AccessFlags);
builder.UseBuffer(handle, AccessFlags);
builder.UseRendererList(listHandle);
builder.AllowPassCulling(false);

// 注册执行函数
builder.SetRenderFunc<TPassData>(static (data, ctx) => { ... });

// RendererList 创建
renderGraph.CreateRendererList(rendererListDesc);
renderGraph.CreateShadowRendererList(in shadowDrawingSettings);
renderGraph.CreateSkyboxRendererList(camera);

// 资源创建
renderGraph.CreateTexture(textureDesc);
renderGraph.CreateBuffer(bufferDesc);

// 命令上下文
context.cmd.DrawRendererList(list); // RasterCommandBuffer