Elytra
Blog游戏引擎

Unity SRP:从 Camera 到 GPU 理解渲染管线架构

从 RenderPipeline、Camera、Culling、RendererList、CommandBuffer 到 Render Graph,沿着一帧的真实路径理解 Unity Scriptable Render Pipeline 的架构、核心组件与设计动机。

Unity 中经常能见到一串看起来彼此相似的名字:

  • Render Pipeline
  • Scriptable Render Pipeline
  • Universal Render Pipeline
  • ScriptableRenderContext
  • Renderer
  • ScriptableRenderer
  • Render Pass
  • Renderer Feature
  • Render Graph
  • SRP Batcher

如果只是分别查 API,很容易得到一种奇怪的结果:每个名词都大概认识,却不知道一帧究竟是怎样被它们串起来的。

真正值得先理解的不是某个类的成员函数,而是一条更根本的路径:

Loading diagram…

SRP 的本质,是让我们用 C# 决定这一帧渲染工作如何被组织,而不是让 C# 自己去光栅化三角形。

Unity 仍然负责大量底层工作:场景数据、原生渲染后端、图形 API 抽象、资源管理、平台差异以及最终与 GPU 的交互。SRP 则位于更高的一层,负责告诉 Unity:

哪台 Camera 要渲染什么?
哪些对象需要参与?
按什么顺序画?
使用哪些 Shader Pass?
写入哪个 Render Target?
什么时候生成阴影、深度、颜色和后处理结果?

本文会从一条极简的自定义 SRP 出发,沿着一帧真正经历的流程逐层拆开这些问题。

本文以 Unity 6 的 API 为主要参照。许多较早的 SRP 教程仍然直接使用 ScriptableRenderContext.DrawRenderers();在 Unity 6 中该 API 已标记为 Obsolete,官方建议改用 RendererList:先通过 CreateRendererList() 创建列表,再用 CommandBuffer.DrawRendererList() 调度绘制。本文会使用较新的写法,同时说明旧 API 背后的同一套思想。1


1为什么 Unity 需要 Scriptable Render Pipeline?

在理解 SRP 之前,可以先问一个更基本的问题:

游戏引擎不是本来就应该负责渲染吗?为什么还需要让开发者“编程渲染管线”?

因为“把一个 Mesh 画出来”和“决定一整帧应该怎样被组织”是两件不同的事情。

一个现代实时渲染器通常不只是:

找到物体
→ 画出来

而更像:

Culling
↓
Shadow Map
↓
Depth Prepass
↓
Opaque
↓
Lighting
↓
SSAO / SSR / Decal / ...
↓
Transparent
↓
Post Processing
↓
Final Output

不同项目对这条流程的需求可能完全不同。

移动游戏可能希望尽量减少 Render Target 和带宽;高端 PC 项目可能需要 Deferred、体积雾、复杂阴影和大量屏幕空间效果;VR 又特别在意双眼渲染、延迟和 GPU 时间。

如果整个 Render Loop 完全固定在引擎内部,那么开发者只能在有限的插槽中修改行为。

Unity 的 Built-in Render Pipeline 并不是完全不可扩展,它长期以来也提供 Command Buffer、Camera Event 等接口。但渲染流程的主体仍主要由引擎定义。

SRP 改变的是这一层边界。

Unity 将 SRP 定义为一个较薄的 API 层:开发者可以使用 C# 调度和配置渲染命令,这些命令随后交给 Unity 的底层图形架构,再发送给实际 Graphics API。URP 和 HDRP 都建立在 SRP 之上,也可以基于 SRP 编写自己的 Render Pipeline。2

可以粗略理解为:

Loading diagram…

1.1“Scriptable” 到底意味着什么?

这里的 Scriptable 并不是:

“用 C# 实现一个软件光栅化器。”

也不是:

“以后不需要 Shader 了。”

而是:

把 Render Loop 中相当一部分“什么时候做什么”的组织权交给 C#。

例如,我们可以在 C# 中决定:

先画 Shadow
还是先做 Depth Prepass?

Opaque 用什么规则筛选?

Transparent 怎样排序?

什么时候切换 Render Target?

什么时候 Dispatch Compute Shader?

什么时候做 Post Process?

真正处理顶点、片元、采样纹理和光栅化的,依然是 Shader 与 GPU。

因此可以把职责粗略分成三层:

层主要问题
C# / SRP这一帧做什么、按什么顺序做
Shader一次绘制中的顶点和像素怎么算
GPU / Graphics API这些命令最终怎样在硬件上执行

这也是为什么学习 SRP 的重点不应该从 HLSL 开始,而应该先理解 Render Loop。


2先看整张图:一帧渲染经历了什么?

先不考虑阴影、光照和后处理,一条极简 SRP 可以抽象为:

Loading diagram…

如果进一步抽象,场景中的所有可渲染对象可以看成集合 SS。

Camera 和 Culling 决定可见集合 CC,Filtering 再从中挑选当前 Pass 想画的对象,最后 Sorting 决定顺序:

D=Sort⁡(Filter⁡(Cull⁡(S)))D = \operatorname{Sort}(\operatorname{Filter}(\operatorname{Cull}(S)))

这里的 DD 才是当前一次绘制真正关心的对象集合。

例如第一次:

Cull
↓
只筛 Opaque
↓
Front-to-Back 排序
↓
Draw

第二次则可以:

同一个 CullingResults
↓
只筛 Transparent
↓
Back-to-Front 排序
↓
Draw

这已经暴露出 SRP 的一个重要设计:

“场景里有哪些东西可能可见”与“这一道渲染步骤实际要画哪些东西”是分开的。

后面许多 API 都只是围绕这个思想逐渐具体化。


3SRP 最核心的两个对象

一个自定义 SRP 最先遇到的两个类型是:

RenderPipelineAsset
RenderPipeline

Unity 官方也把它们视为 SRP 的两个核心自定义元素。2

它们看起来很像,但职责完全不同。

3.1RenderPipelineAsset:配置与创建 Pipeline

RenderPipelineAsset 是一个 ScriptableObject。

它存在于 Project 中,可以序列化,也可以出现在 Inspector 里:

using UnityEngine;
using UnityEngine.Rendering;

[CreateAssetMenu(menuName = "Rendering/Tiny Render Pipeline")]
public sealed class TinyRenderPipelineAsset : RenderPipelineAsset
{
    [SerializeField]
    private Color clearColor = new(0.05f, 0.05f, 0.05f);

    protected override RenderPipeline CreatePipeline()
    {
        return new TinyRenderPipeline(clearColor);
    }
}

它最关键的方法就是:

CreatePipeline()

可以把 Asset 理解成:

Pipeline 的可序列化配置 + Factory。

例如它可以存:

Shadow Distance
MSAA
HDR
Render Scale
Lighting Quality
Post Processing Settings
...

而真正负责每一帧渲染的不是 Asset,而是它创建出来的 RenderPipeline 实例。


3.2RenderPipeline:真正执行渲染

真正的 Render Loop 位于:

RenderPipeline

中。

Unity 6 当前的核心入口是:

protected override void Render(
    ScriptableRenderContext context,
    List<Camera> cameras)

Render() 就是 SRP 的主入口。Unity 会在需要渲染时调用它。3

最小骨架可以写成:

using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Rendering;

public sealed class TinyRenderPipeline : RenderPipeline
{
    private readonly Color clearColor;

    public TinyRenderPipeline(Color clearColor)
    {
        this.clearColor = clearColor;
    }

    protected override void Render(
        ScriptableRenderContext context,
        List<Camera> cameras)
    {
        foreach (Camera camera in cameras)
        {
            RenderCamera(context, camera);
        }
    }

    private void RenderCamera(
        ScriptableRenderContext context,
        Camera camera)
    {
        // 后面逐步实现
    }
}

此时我们已经拥有了一条合法的“渲染管线”。

只不过它什么都没有画。

这件事本身很重要:

Render Pipeline 并不神秘。最小意义上的 Pipeline,就是 Unity 每帧调用的一段 C# Render Loop。

真正的复杂度来自后面不断增加的能力,而不是来自 RenderPipeline 这个类本身。


3.3Asset 和 Runtime Instance 为什么要分开?

如果把配置和执行全部塞进一个对象,会遇到一些生命周期问题。

Asset 更适合:

保存
序列化
Inspector 编辑
不同质量等级配置
创建 Runtime Instance

Pipeline 实例更适合:

持有运行时资源
执行 Render Loop
管理临时状态
创建 Buffer
执行每帧逻辑

因此结构是:

Loading diagram…

Unity 当前使用的 Render Pipeline Asset 可以通过 Graphics Settings / Quality Settings 决定;GraphicsSettings.currentRenderPipeline 则表示当前质量等级实际生效的 RenderPipelineAsset。4


4写一个最小的 Scriptable Render Pipeline

接下来真正让它开始工作。

先暂时不画任何物体,只做三件事:

  1. 设置 Camera;
  2. 清空颜色和深度;
  3. 提交。
private void RenderCamera(
    ScriptableRenderContext context,
    Camera camera)
{
    context.SetupCameraProperties(camera);

    CommandBuffer cmd =
        CommandBufferPool.Get("Tiny SRP");

    cmd.ClearRenderTarget(
        clearDepth: true,
        clearColor: true,
        backgroundColor: clearColor
    );

    context.ExecuteCommandBuffer(cmd);

    context.Submit();

    CommandBufferPool.Release(cmd);
}

这段代码虽然简单,却已经引出了 SRP 中三个非常核心的角色。

4.1SetupCameraProperties()

它会调度 Camera 相关的全局 Shader 状态,例如 View、Projection 和裁剪平面等。5

也就是说 Camera 并不是只提供一个位置:

Camera
├─ View Matrix
├─ Projection Matrix
├─ Frustum
├─ Near / Far
├─ Culling Mask
└─ Rendering State

渲染某台 Camera 前,需要让后续 Shader 与绘制工作处于这台 Camera 的上下文中。


4.2CommandBuffer

CommandBuffer 本质上是一份:

待执行的图形命令列表。

例如:

SetRenderTarget
ClearRenderTarget
SetGlobalTexture
DrawMesh
DrawRendererList
DispatchCompute
Blit
...

它不是图片,也不是 GPU。

它更接近:

“之后请执行这些事情。”

4.3Submit()

很多 SRP API 的行为不是:

调用这一行以后 GPU 立刻完成工作。

而是:

将工作调度到当前 ScriptableRenderContext。

ScriptableRenderContext.Submit() 才会把此前已经调度的命令提交给渲染循环执行。6

因此:

C# 调用 API
↓
Schedule Commands
↓
ScriptableRenderContext
↓
Submit
↓
Unity Graphics Backend
↓
Graphics API / GPU

Submit() 的意义是提交已经调度的渲染工作,而不是“等待 GPU 完成整帧”。不要把它理解成一个普通的 CPU → GPU 同步阻塞点。

现在我们的 Pipeline 能正确清屏,但还是看不到 Scene 中的 Mesh。

因为我们还没有回答一个重要的问题:

到底应该画哪些对象?


5Culling:为什么第一步不是直接 Draw?

假设 Scene 里有十万个 Renderer。

一个 Camera 可能只看得到其中几千个。

如果我们每一次 Pass 都从:

Scene 中所有对象

开始重新筛选,成本和架构都会非常糟糕。

于是 SRP 首先需要进行 Culling。

5.1从 Camera 获取 Culling 参数

if (!camera.TryGetCullingParameters(
out ScriptableCullingParameters cullingParameters))
{
    return;
}

这些参数来自 Camera,可以包含:

  • Culling Mask
  • Culling Matrix
  • Culling Planes
  • Shadow Distance
  • LOD 参数
  • Occlusion Culling 选项
  • 最大可见光数量

等信息。7

5.2执行 Cull

CullingResults cullingResults =
context.Cull(ref cullingParameters);

得到:

CullingResults

它代表后续渲染可以使用的可见性结果。

5.3后续多个 Pass 复用结果

Opaque、Transparent、Shadow 等不同步骤,可以从同一个或相关的可见性信息出发,再分别应用自己的绘制规则。

整体关系是:

Loading diagram…

5.4Culling 不只是“看物体在不在屏幕里”

最容易把 Culling 简化成:

Frustum Culling

但 ScriptableCullingParameters 描述的范围更广。

例如 Unity 的 CullingOptions.OcclusionCull 可以让当前 Culling 操作包含 Occlusion Culling;该选项默认还会受到 Camera.useOcclusionCulling 的影响。7

所以更准确的理解是:

Culling 是根据当前 Camera 和管线配置,建立这一视图后续渲染所需的可见性数据。

Unity 6 中 URP/HDRP 还存在 GPU Occlusion Culling 等更现代的 GPU-driven 技术路径,那属于另一层优化机制,不应该简单等同于这里最基础的 context.Cull() 调用。


6从“可见”到“真正要画”:Sorting、Filtering 与 Drawing

有了 CullingResults,仍然不能直接说:

“全部画出来。”

因为某一道 Pass 通常只需要其中一部分对象。

例如:

Opaque Pass
只画不透明物体

Transparent Pass
只画透明物体

Shadow Pass
只画 Shadow Caster

Depth Pass
只需要能参与 Depth 的 Shader Pass

这就是三个重要结构出现的原因:

类型解决的问题
FilteringSettings哪些对象有资格进入这一批绘制?
SortingSettings这些对象按什么顺序?
DrawingSettings应该怎样画、选择哪个 Shader Pass?

6.1FilteringSettings:画谁?

最简单的例子是区分 Opaque 与 Transparent:

var filtering = new FilteringSettings(
    RenderQueueRange.opaque
);

或者:

var filtering = new FilteringSettings(
    RenderQueueRange.transparent
);

FilteringSettings 还可以根据:

GameObject Layer
Rendering Layer
Render Queue
Motion Vector 状态
...

继续过滤。8

因此:

CullingResults
↓
FilteringSettings
↓
当前 Pass 真正关心的对象

6.2SortingSettings:先画谁?

Opaque 和 Transparent 往往不能采用完全相同的顺序。

对于不透明物体,一个常见策略是大致 Front-to-Back:

var sorting = new SortingSettings(camera)
{
    criteria = SortingCriteria.CommonOpaque
};

这样较近的不透明物体可以更早写入 Depth,后续被遮挡片元有机会更早被拒绝,同时 Unity 也会考虑减少状态切换。

透明对象则通常需要:

criteria = SortingCriteria.CommonTransparent;

也就是包含 Back-to-Front 排序的常用透明规则。Unity 官方的 SortingCriteria 文档也明确区分了 CommonOpaque 与 CommonTransparent。9

因此顺序不是一个纯粹的美学问题,它会直接影响:

正确性
+
Overdraw
+
State Change
+
性能

6.3DrawingSettings:怎样画?

DrawingSettings 决定的事情包括:

Sorting
Shader Pass
Per-object Data
Instancing
Override Material / Shader
...

最基础的写法:

static readonly ShaderTagId ShaderTag =
    new("SRPDefaultUnlit");

var drawing = new DrawingSettings(
    ShaderTag,
    sorting
)
{
    enableInstancing = true
};

这里出现了一个非常关键的东西:

ShaderTagId

它把 Render Pipeline 的某个绘制阶段 和 Shader 中的某个 Pass 联系起来。

这会在后面详细解释。


7Unity 6:RendererList 把这些规则组合起来

很多经典 SRP 教程会写:

context.DrawRenderers(
    cullingResults,
    ref drawingSettings,
    ref filteringSettings
);

这个接口很好地展示了 SRP 思想,因此非常适合学习。

但是在 Unity 6 中,ScriptableRenderContext.DrawRenderers() 已经标记为 Obsolete,官方推荐使用 RendererList API。1

新的关系更接近:

Loading diagram…

代码如下:

private static readonly ShaderTagId ShaderTag =
    new("SRPDefaultUnlit");

private static void DrawObjects(
    ScriptableRenderContext context,
    CommandBuffer cmd,
    Camera camera,
    CullingResults cullingResults,
    bool transparent)
{
    var sortingSettings = new SortingSettings(camera)
    {
        criteria = transparent
            ? SortingCriteria.CommonTransparent
            : SortingCriteria.CommonOpaque
    };

    var drawingSettings = new DrawingSettings(
        ShaderTag,
        sortingSettings
    )
    {
        enableInstancing = true
    };

    var filteringSettings = new FilteringSettings(
        transparent
            ? RenderQueueRange.transparent
            : RenderQueueRange.opaque
    );

    var rendererListParams = new RendererListParams(
        cullingResults,
        drawingSettings,
        filteringSettings
    );

    RendererList rendererList =
        context.CreateRendererList(ref rendererListParams);

    cmd.DrawRendererList(rendererList);

    context.ExecuteCommandBuffer(cmd);
    cmd.Clear();
}

这里非常值得停下来观察。

我们仍然没有:

foreach (Renderer renderer in scene)
{
    renderer.Draw();
}

而是在描述:

从已经 Cull 得到的结果中,按照这些 Filtering、Sorting 和 Drawing 规则,构造我想要绘制的一组可见对象。

这就是 RendererList 的意义。

Unity 将其定义为一组可见 GameObject;创建它时通常提供 CullingResults、DrawingSettings 与 FilteringSettings,然后通过 CommandBuffer.DrawRendererList() 将绘制加入命令流。1


8把 Opaque、Skybox 和 Transparent 串起来

现在已经可以真正构造一个非常小的 Render Loop:

Camera
↓
Cull
↓
Opaque
↓
Skybox
↓
Transparent
↓
Submit

Skybox 在现代 API 下也可以创建自己的 Renderer List:

RendererList skybox =
    context.CreateSkyboxRendererList(camera);

cmd.DrawRendererList(skybox);

context.ExecuteCommandBuffer(cmd);
cmd.Clear();

于是完整的 Camera 渲染函数可以写成:

private void RenderCamera(
    ScriptableRenderContext context,
    Camera camera)
{
    if (!camera.TryGetCullingParameters(
            out var cullingParameters))
    {
        return;
    }

    CullingResults cullingResults =
        context.Cull(ref cullingParameters);

    context.SetupCameraProperties(camera);

    CommandBuffer cmd =
        CommandBufferPool.Get(camera.name);

    cmd.ClearRenderTarget(
        clearDepth: true,
        clearColor: true,
        backgroundColor: clearColor
    );

    context.ExecuteCommandBuffer(cmd);
    cmd.Clear();

    // 1. Opaque
    DrawObjects(
        context,
        cmd,
        camera,
        cullingResults,
        transparent: false
    );

    // 2. Skybox
    RendererList skybox =
        context.CreateSkyboxRendererList(camera);

    cmd.DrawRendererList(skybox);

    context.ExecuteCommandBuffer(cmd);
    cmd.Clear();

    // 3. Transparent
    DrawObjects(
        context,
        cmd,
        camera,
        cullingResults,
        transparent: true
    );

    context.Submit();

    CommandBufferPool.Release(cmd);
}

这已经非常接近一条最原始的 Forward Render Pipeline 骨架。

但它仍然缺少很多东西:

Lighting
Shadow
Depth Texture
Reflection Probe
Post Processing
Editor Gizmo
XR
Camera Stack
MSAA
HDR
...

这反而正是这个示例的价值:

把现代管线中的复杂功能全部拿掉以后,下面仍然是 Camera → Cull → Select → Draw → Submit。


9一帧 SRP 的完整骨架

如果把前面的代码整理一下,一条教学用 SRP 可以写成:

using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Rendering;

public sealed class TinyRenderPipeline : RenderPipeline
{
    private static readonly ShaderTagId ShaderTag =
        new("SRPDefaultUnlit");

    private readonly Color clearColor;

    public TinyRenderPipeline(Color clearColor)
    {
        this.clearColor = clearColor;
    }

    protected override void Render(
        ScriptableRenderContext context,
        List<Camera> cameras)
    {
        BeginContextRendering(context, cameras);

        foreach (Camera camera in cameras)
        {
            BeginCameraRendering(context, camera);

            RenderCamera(context, camera);

            EndCameraRendering(context, camera);
        }

        EndContextRendering(context, cameras);
    }

    private void RenderCamera(
        ScriptableRenderContext context,
        Camera camera)
    {
        if (!camera.TryGetCullingParameters(
                out var cullingParameters))
        {
            return;
        }

        CullingResults cullingResults =
            context.Cull(ref cullingParameters);

        context.SetupCameraProperties(camera);

        CommandBuffer cmd =
            CommandBufferPool.Get(camera.name);

        cmd.ClearRenderTarget(
            clearDepth: true,
            clearColor: true,
            backgroundColor: clearColor
        );

        context.ExecuteCommandBuffer(cmd);
        cmd.Clear();

        DrawObjects(
            context,
            cmd,
            camera,
            cullingResults,
            transparent: false
        );

        RendererList skybox =
            context.CreateSkyboxRendererList(camera);

        cmd.DrawRendererList(skybox);

        context.ExecuteCommandBuffer(cmd);
        cmd.Clear();

        DrawObjects(
            context,
            cmd,
            camera,
            cullingResults,
            transparent: true
        );

        context.Submit();

        CommandBufferPool.Release(cmd);
    }

    private static void DrawObjects(
        ScriptableRenderContext context,
        CommandBuffer cmd,
        Camera camera,
        CullingResults cullingResults,
        bool transparent)
    {
        var sortingSettings = new SortingSettings(camera)
        {
            criteria = transparent
                ? SortingCriteria.CommonTransparent
                : SortingCriteria.CommonOpaque
        };

        var drawingSettings = new DrawingSettings(
            ShaderTag,
            sortingSettings
        )
        {
            enableInstancing = true
        };

        var filteringSettings = new FilteringSettings(
            transparent
                ? RenderQueueRange.transparent
                : RenderQueueRange.opaque
        );

        var rendererListParams = new RendererListParams(
            cullingResults,
            drawingSettings,
            filteringSettings
        );

        RendererList rendererList =
            context.CreateRendererList(ref rendererListParams);

        cmd.DrawRendererList(rendererList);

        context.ExecuteCommandBuffer(cmd);
        cmd.Clear();
    }
}

这里的:

BeginContextRendering(...)
BeginCameraRendering(...)
EndCameraRendering(...)
EndContextRendering(...)

并不是 GPU 绘制的必要组成部分,而是在自定义 SRP 中正确触发 RenderPipelineManager 相应事件的入口。

URP 和 HDRP 会自动处理这些回调;自己实现 SRP 时,如果希望外部代码能够使用相应的 Rendering Event,则需要在合适的位置调用它们。Unity 6 还推荐使用 beginContextRendering / endContextRendering 替代旧的 Frame 版本,因为旧的 beginFrameRendering / endFrameRendering 会带来额外堆分配。10


10ScriptableRenderContext:C# 与 Unity 渲染后端之间的桥

到这里,ScriptableRenderContext 已经出现了很多次:

context.Cull(...)
context.SetupCameraProperties(...)
context.CreateRendererList(...)
context.ExecuteCommandBuffer(...)
context.Submit()

那么它究竟是什么?

可以把整个关系画成:

Loading diagram…

Unity 官方将 ScriptableRenderContext 描述为自定义 C# Render Pipeline 与 Unity 低层图形代码之间的接口。2

它不是:

GPU

也不是:

CommandBuffer

更不是:

一张 Render Texture

它代表的是当前 SRP 可以用来:

Cull
创建 RendererList
建立 Camera 状态
调度 CommandBuffer
创建 Shadow / Skybox RendererList
最终 Submit

的一组渲染上下文能力。


11CommandBuffer 与 ScriptableRenderContext 有什么区别?

这两个概念很容易混。

11.1ScriptableRenderContext 更像“当前渲染会话的接口”

例如:

context.Cull(...)
context.CreateRendererList(...)
context.Submit()

它知道当前 SRP 渲染所处的 Unity 上下文。


11.2CommandBuffer 更像“要执行的命令序列”

例如:

cmd.ClearRenderTarget(...);
cmd.SetGlobalTexture(...);
cmd.DrawMesh(...);
cmd.DrawRendererList(...);
cmd.DispatchCompute(...);

最后:

context.ExecuteCommandBuffer(cmd);

是把这组命令调度进 Context。

值得特别注意的是,ExecuteCommandBuffer() 并不等价于:

“GPU 此刻立即完成这些命令。”

官方描述是:该调用会把 Command Buffer 注册进 ScriptableRenderContext 的内部命令序列,实际执行在随后 Submit() 所提交的工作中发生。11

因此:

Loading diagram…

11.3为什么还需要两种 Draw?

你可能还会遇到:

cmd.DrawMesh(...)

以及:

cmd.DrawRendererList(...)

二者虽然都能产生 Draw,但解决的问题不同。

DrawMesh() 更像显式命令:

画这个 Mesh,用这个 Material。

而 RendererList 更像场景驱动的批量绘制描述:

从这台 Camera 的可见对象中,按照这些过滤、排序和 Shader Pass 规则,把符合条件的 Renderer 画出来。

典型场景渲染通常更需要后者。


12Renderer、Material、Shader 是怎样进入 SRP 的?

到这里还有一个很自然的疑问:

Scene 中明明摆的是 MeshRenderer、Material 和 Shader,为什么 Render Loop 里好像从来没有逐个访问它们?

因为这部分连接发生在 Culling、RendererList 和 Drawing 规则之间。

可以把关系理解成:

Loading diagram…

Scene 中的 Renderer 负责让对象具有可渲染表示,并关联 Material 等数据。

但 SRP 并不是:

foreach (Renderer renderer in scene)
{
    ...
}

而更多是:

Scene
↓
Cull
↓
Visible Renderers
↓
Filtering / Drawing / Sorting
↓
RendererList
↓
Draw

这让底层能够拥有更大的优化空间,而 Pipeline 只负责描述自己需要什么。


13ShaderTagId:Pipeline 与 Shader 之间的协议

前面的代码里写过:

private static readonly ShaderTagId ShaderTag =
    new("SRPDefaultUnlit");

为什么 C# 需要知道一个字符串?

因为一个 Shader 可能存在多个 Pass:

SubShader
{
    Pass
    {
        Tags { "LightMode" = "MyForward" }
    }

    Pass
    {
        Tags { "LightMode" = "ShadowCaster" }
    }

    Pass
    {
        Tags { "LightMode" = "DepthOnly" }
    }
}

同一个对象可能在不同阶段执行不同 Shader Pass。

于是 Render Pipeline 需要表达:

这一次我要这个 Shader 的哪种角色?

这就是 LightMode / ShaderTagId 形成的一种约定。

例如自定义 SRP 完全可以约定:

TinyForward
TinyDepth
TinyShadowCaster

C#:

new ShaderTagId("TinyForward");

Shader:

Tags { "LightMode" = "TinyForward" }

只要双方约定一致即可。

URP 中常见的:

UniversalForward
UniversalGBuffer
ShadowCaster
DepthOnly
SRPDefaultUnlit

本质上也是 URP 自己定义的一套协议。

这非常重要,因为它意味着:

UniversalForward 并不是“SRP 标准”。它是 URP 对自己 Render Loop 和 Shader 角色的定义。


14一个最小的 SRP Shader

为了配合前面的 SRPDefaultUnlit,可以使用一个最简单的 Shader:

Shader "TinySRP/Unlit"
{
    Properties
    {
        _BaseColor ("Base Color", Color) = (1, 1, 1, 1)
    }

    SubShader
    {
        Tags
        {
            "Queue" = "Geometry"
        }

        Pass
        {
            Tags
            {
                "LightMode" = "SRPDefaultUnlit"
            }

            HLSLPROGRAM

            #pragma vertex Vert
            #pragma fragment Frag

            #include "Packages/com.unity.render-pipelines.core/ShaderLibrary/Common.hlsl"
            #include "Packages/com.unity.render-pipelines.core/ShaderLibrary/SpaceTransforms.hlsl"

            struct Attributes
            {
                float3 positionOS : POSITION;
            };

            struct Varyings
            {
                float4 positionCS : SV_POSITION;
            };

            CBUFFER_START(UnityPerMaterial)
                float4 _BaseColor;
            CBUFFER_END

            Varyings Vert(Attributes input)
            {
                Varyings output;
                output.positionCS =
                    TransformObjectToHClip(input.positionOS);
                return output;
            }

            half4 Frag(Varyings input) : SV_Target
            {
                return _BaseColor;
            }

            ENDHLSL
        }
    }
}

这里只做:

Object Space Position
↓
Clip Space Position
↓
输出 Base Color

没有光照,没有阴影。

但这已经足以看清:

C# Render Pipeline
    ↓
ShaderTagId("SRPDefaultUnlit")

Shader
    ↓
LightMode = "SRPDefaultUnlit"

两边匹配
    ↓
RendererList 可以选择这个 Shader Pass

15Render Pass 和 Shader Pass 不是一回事

这是 Unity Rendering 中非常容易混淆的一对名字。

15.1Shader Pass

存在于 ShaderLab:

Pass
{
    Tags { "LightMode" = "ShadowCaster" }

    HLSLPROGRAM
    ...
    ENDHLSL
}

它回答:

同一个 Shader 在这一种绘制角色下,该执行怎样的 GPU 程序和 Render State?


15.2Render Pass

则是渲染流程中的一个工作阶段,例如:

Shadow Pass
Depth Pass
Opaque Pass
SSAO Pass
Transparent Pass
Post Process Pass

它回答:

这一阶段整个 Renderer 要完成什么工作?

一个 Render Pass 可能绘制几千个对象,而这些对象可能选择某个特定 Shader Pass。

因此关系更接近:

Loading diagram…

所以:

Render Pass 是 Pipeline 层的概念;Shader Pass 是 Shader 层的概念。

名字相同,是因为二者都表达了“同一流程的一次阶段”,但抽象层完全不同。


16Render Target:这些东西到底画到哪里?

到目前为止我们一直在说:

Draw
Draw
Draw

但 GPU 产生的结果必须写到某个地方。

最基础的是:

Color Buffer
Depth Buffer

颜色可以想象成:

C(x,y)=(R,G,B,A)C(x,y) = (R,G,B,A)

深度则记录对应像素当前的 Depth:

D(x,y)=zD(x,y)=z

当然真实 GPU 中的格式、精度、MSAA、压缩和 Tile Memory 要复杂得多,但在 Pipeline 层,我们首先只需要建立这个抽象。

16.1为什么现代 Pipeline 会有那么多 Texture?

因为很多渲染算法不是:

Scene
↓
Screen

一次完成的。

例如 Deferred Rendering:

Geometry
↓
GBuffer

GBuffer + Lights
↓
Lighting

SSAO:

Depth + Normal
↓
AO Texture
↓
Lighting / Composite

Bloom:

Scene Color
↓
Threshold
↓
Downsample
↓
Blur
↓
Upsample
↓
Composite

因此一条现代 Render Pipeline 实际上更像:

Loading diagram…

也就是说:

现代 Render Pipeline 很大程度上就是“Pass 与 Resource 的网络”。

这句话会直接引出后面的 Render Graph。


17一条真正的 Pipeline 不只有 Opaque + Transparent

我们刚才写出来的 Tiny SRP:

Cull
↓
Opaque
↓
Skybox
↓
Transparent

只是为了把骨架暴露出来。

真正的实时 Renderer 可能包括:

Culling

↓
Shadow Culling
↓
Shadow Maps

↓
Depth Prepass
↓
Depth Pyramid

↓
GBuffer / Forward Opaque
↓
Decals
↓
Lighting
↓
SSAO
↓
SSR
↓
Volumetric Lighting

↓
Transparent
↓
Distortion

↓
TAA / Upscaler
↓
Bloom
↓
Depth of Field
↓
Tonemapping

↓
UI
↓
Final Output

而且其中很多步骤并不是简单 Raster Pass,还可能包含:

Compute Pass
Copy Pass
Ray Tracing Pass
Async Compute
GPU-driven Culling

所以真正让一条 Pipeline 变复杂的,并不只是“代码行数很多”。

更本质的是:

Pass 数量、Resource 数量,以及它们之间的依赖关系迅速增加。


18阴影也只是另一种“从可见性到绘制”

以 Shadow Map 为例,思路其实仍然没有逃离我们前面建立的框架。

普通 Camera:

Camera
↓
Cull
↓
Visible Objects
↓
Draw to Camera Target

Shadow:

Light View
↓
Shadow Culling
↓
Shadow Casters
↓
Draw Depth to Shadow Map

Unity 的现代 ScriptableRenderContext 甚至提供:

CreateShadowRendererList(...)

来为 Shadow 绘制创建对应的 Renderer List。1

这说明 Culling → Selection → RendererList → Draw 不是“只适用于 Opaque”的偶然写法,而是一种更加普遍的组织模式。


19光照是怎样加入这套结构的?

我们当前的 Shader 是 Unlit,因为真正做 Lighting 会立刻引出:

Visible Lights
Main Light
Additional Lights
Light Culling
Shadow Data
Reflection Probe
Light Probe
Shader Constant Buffer
Forward / Deferred

但从架构角度看,它仍然只是继续丰富前面的数据流。

例如一个最朴素的 Forward Renderer:

Loading diagram…

也就是说 Pipeline 做的事情变成:

  1. 从 CullingResults 获得可见 Light;
  2. 整理当前 Lighting Strategy 需要的数据;
  3. 写入全局 Buffer / Constant;
  4. 选择相应 Shader Pass;
  5. Draw。

真正复杂的 URP/HDRP 正是在不断扩展这些步骤。


20为什么一条简单的 Render() 最终会失控?

如果只有:

Opaque
Transparent

我们完全可以手写:

DrawOpaque();
DrawTransparent();

但随着功能增加:

RenderShadow();
RenderDepth();
RenderOpaque();
RenderSSAO();
RenderSSR();
RenderTransparent();
RenderBloom();
RenderPostProcess();

问题开始出现。

20.1问题一:Render Target 生命周期

例如:

AO Texture

只在:

SSAO → Lighting

之间需要存在。

之后它的 GPU Memory 就可能被别的临时资源复用。

如果所有 Texture 都手动维护:

Create
Use
Release

资源生命周期会越来越难推理。


20.2问题二:Pass 之间存在依赖

例如:

Depth
↓
SSAO
↓
Composite

SSAO 必须等 Depth 完成。

而另一些工作可能彼此独立:

Shadow Map
Environment Processing

理论上可能存在更复杂的调度空间。


20.3问题三:有些 Pass 其实不需要运行

假如:

Pass A
↓
生成 TempTexture

但最终没有任何后续 Pass 读取这个 Texture,那么 A 对最终输出没有贡献。

如果渲染系统能看见完整依赖关系,就可能直接把它裁掉。

这正是 Render Graph 这类系统出现的背景。


21Render Graph 在 SRP 中处于什么位置?

Render Graph 很容易被误解成:

“Unity 又发明了一种新的 Render Pipeline。”

不是。

SRP 回答的是:

怎样构建和控制渲染管线?

Render Graph 更关注:

当管线里已经存在很多 Pass 时,它们怎样描述资源依赖和生命周期?

可以粗略画成:

Loading diagram…

传统命令式思维更像:

创建 TempA
↓
执行 Pass A
↓
切 RenderTarget
↓
执行 Pass B
↓
释放 TempA
↓
创建 TempB
...

Render Graph 则更倾向于声明:

Pass A:
Write Texture A

Pass B:
Read Texture A
Write Texture B

Pass C:
Read Texture B
Write Camera Color

也就是:

从“我一步一步手动管理资源”转向“我告诉系统依赖关系”。

这使系统有机会进行:

Resource Lifetime Analysis
Transient Resource Reuse
Pass Culling
Pass Scheduling
Debug Visualization

等优化。

在 Unity 6 / URP 17 中,Render Graph 已成为新 URP 项目的默认路径。Unity 仍提供 Compatibility Mode 运行旧式非 Render Graph API,但官方升级文档明确说明,不再继续开发或改进不使用 Render Graph API 的渲染路径。URP 与 HDRP 当前都支持 Render Graph。12

但这篇文章不会继续深入:

TextureHandle
RasterGraphContext
IRasterRenderGraphBuilder
Pass Culling
Resource Aliasing

因为那些属于 Render Graph 本身的问题。

这里最重要的结论只有:

Render Graph 没有取代 SRP;它是在现代复杂 SRP 内部帮助组织 Pass 与 Resource 的基础设施。


22SRP Core 又是什么?

除了 Unity Engine 自身提供的核心 SRP API,还有一个很容易与 SRP 本身混淆的名字:

Scriptable Render Pipeline Core

也就是常见的:

com.unity.render-pipelines.core

SRP Core 提供大量构建 Render Pipeline 时可以复用的代码,例如:

Render Graph
RTHandle
Volume Framework
Core Utilities
Shader Library
平台相关辅助
公共渲染工具
...

Unity 官方明确说明,URP 和 HDRP 都使用 SRP Core,其中包含平台相关图形 API 的样板代码、常见渲染操作工具以及 Shader Library。13

所以可以这样区分:

SRP
= Unity 允许你构建 Scriptable Render Pipeline 的核心机制

SRP Core
= Unity 提供的一组可复用渲染基础设施和工具库

它们有关,但不是同一个概念。


23URP 与 HDRP 是怎样建立在 SRP 之上的?

现在终于可以回到日常开发最常见的:

URP
HDRP

它们并不是:

SRP 的两个模式开关

而是:

Unity 基于 SRP 构建好的两条具体 Render Pipeline。

结构可以粗略表示为:

Loading diagram…

23.1URP 做的事情

URP 在基础 SRP 上继续定义了大量自己的架构:

UniversalRenderPipeline
ScriptableRenderer
UniversalRenderer
ScriptableRendererFeature
ScriptableRenderPass
URP Lighting
URP Shader Tags
URP Render Graph Frame Data
...

23.2HDRP 做的事情

HDRP 则建立另一套更加面向高端渲染的结构:

HDRenderPipeline
HDRP Lighting
GBuffer
Volumetrics
Custom Pass
HDRP Shader Pass
Ray Tracing
...

所以:

ScriptableRenderer 并不是 SRP 的标准组件。

它是 URP 的设计。

同样:

ScriptableRendererFeature 和 ScriptableRenderPass 也不是所有 SRP 都必须拥有。

这解释了为什么 HDRP 没有 Renderer Feature,却依然是一条完全正常的 SRP。


24为什么学习 SRP 时不应该从 ScriptableRenderer 开始?

如果第一次理解 Unity Rendering 就从 URP 源码开始,很容易形成:

SRP
=
ScriptableRenderer
+
Renderer Feature
+
ScriptableRenderPass

但这是把:

一个具体产品的架构

误当成:

底层框架本身

真正更底层的关系是:

RenderPipeline
↓
Render()
↓
ScriptableRenderContext
↓
Cull
↓
RendererList / Commands
↓
Submit

URP 的 ScriptableRenderer 是 Unity 在这套能力之上又设计出来的一层 Renderer 抽象。

理解这一点以后,再去看:

UniversalRenderer
ForwardRenderer
2D Renderer
Renderer Feature

就会容易很多。


25从 SRP 到 Renderer Feature / Custom Pass

理解完整 SRP 后,一个非常实际的问题是:

如果我只是想做 Outline,有必要自己写一整条 Render Pipeline 吗?

通常没有。

这其实是两个不同层级的问题。

25.1改整条 Pipeline

例如你真的要设计:

新的 Lighting Model
新的 GBuffer Layout
特殊的 Visibility Pipeline
完全不同的 Shadow Architecture
研究性质 Renderer

这时可能需要:

Custom SRP

25.2只往现有 Pipeline 增加一步

例如:

Outline
X-Ray
Object ID
Scanner
特殊 Mask
Fullscreen Effect

更合理的方式往往是扩展现有 Pipeline:

URP
↓
Renderer Feature
↓
ScriptableRenderPass


HDRP
↓
Custom Pass

因此:

Loading diagram…

这也是为什么即使大部分游戏开发者永远不会亲手维护一条完整 SRP,理解 SRP 仍然很有价值:

只有知道整台机器怎样运行,才能真正理解那些扩展点究竟插在什么地方。


26一条现代 Render Pipeline 可以怎样理解?

到这里,可以把文章中出现的主要组件放回同一张地图。

Loading diagram…

如果只记住一张图,就应该是这张。


27常见概念混淆

27.1SRP vs URP / HDRP

SRP
= 构建 Scriptable Render Pipeline 的基础机制

URP / HDRP
= Unity 基于 SRP 构建的具体 Pipeline

不是:

SRP = URP

也不是:

HDRP 建立在 URP 上

而是:

        SRP
       /   \
     URP   HDRP

27.2Renderer vs ScriptableRenderer

Renderer 是 Unity Scene 中的 Component 基类之一,例如:

MeshRenderer
SkinnedMeshRenderer
ParticleSystemRenderer

它代表一个可被渲染的场景对象。

而:

ScriptableRenderer

是 URP 的 Renderer 架构。

二者名字相似,但不在一个层级。


27.3Render Pass vs Shader Pass

Render Pass
= Pipeline 的一个工作阶段

Shader Pass
= Shader 中的一套 GPU Program + Render State

一次 Render Pass 可以让大量 Renderer 执行同一种角色的 Shader Pass。


27.4SRP vs SRP Core

SRP
= 渲染管线机制

SRP Core
= Unity 提供的共享渲染工具包

URP 与 HDRP 都建立在 SRP 上,也都大量复用 SRP Core。13


27.5SRP vs SRP Batcher

名字尤其容易误导。

SRP Batcher 并不是:

“把整个 SRP 批处理。”

它是一种 Draw Call CPU 侧优化路径,目标主要是减少使用相同 Shader Variant 的绘制之间反复设置渲染状态和材质常量数据的成本。Unity 6 文档列出的兼容范围包括 URP、HDRP 和 Custom SRP,但不包括 Built-in Render Pipeline。14

所以:

SRP
= Render Pipeline Architecture

SRP Batcher
= Render State / Draw Submission Optimization

完全不是同一个抽象层。


27.6Culling vs GPU Occlusion Culling

基础 SRP 中:

context.Cull(...)

负责根据 ScriptableCullingParameters 建立可见性结果,并可以根据选项包含传统 Occlusion Culling。

现代 URP/HDRP 的 GPU Occlusion Culling 则属于更加 GPU-driven 的可见性优化机制。

它们都叫 Culling,但不要简单认为:

context.Cull()
=
GPU Occlusion Culling

27.7CommandBuffer vs Render Graph

CommandBuffer
= 我记录一系列要执行的图形命令

Render Graph
= 我描述 Pass、Resource 和依赖关系

Render Graph 最终仍然需要产生实际 GPU 工作。

因此 Render Graph 并不是“CommandBuffer 的另一个名字”,而是更高层的组织和调度模型。


28为什么理解 SRP 比记住 API 更重要?

Unity 的 API 会继续变化。

例如很多经典文章中的:

context.DrawRenderers(...)

在 Unity 6 已经让位于 RendererList API。

URP 17 又进一步把自定义 ScriptableRenderPass 推向 Render Graph-first 的写法。12

未来一些:

Draw Submission
GPU-driven Rendering
Render Graph
Resource Management

还可能继续演进。

但下面这些关系不会那么容易过时:

Camera
↓
Visibility

Visibility
↓
Selection

Selection
↓
Draw Work

Draw Work
↓
Resources / Render Targets

Passes
↓
Dependencies

Commands
↓
Graphics Backend
↓
GPU

所以真正值得长期记住的是:

渲染 API 的名字会变,但 Render Pipeline 必须解决的问题不会凭空消失。


29总结:从 Camera 到 GPU

现在再回头看 SRP,一帧最核心的路径已经非常清楚。

1. Unity 进入 RenderPipeline.Render()

当前的 RenderPipelineAsset 创建 Runtime Pipeline 实例,Unity 在需要渲染时进入它的 Render()。

2. Pipeline 处理 Camera

为每台 Camera 建立 View、Projection 以及当前视图相关的渲染状态。

3. Culling

从 Camera 得到 ScriptableCullingParameters:

Camera
↓
Cull
↓
CullingResults

将整个 Scene 收缩成后续渲染需要考虑的可见性数据。

4. Filtering、Sorting、Drawing

不同 Render Pass 决定:

画谁?
先画谁?
用哪个 Shader Pass?

5. RendererList

把:

CullingResults
+
Filtering
+
Drawing
+
Sorting

组合成当前步骤需要的可见对象集合。

6. CommandBuffer

记录:

Clear
Set Render Target
Draw RendererList
Draw Mesh
Dispatch Compute
...

等具体 GPU 工作。

7. ScriptableRenderContext

作为 C# Pipeline 与 Unity 原生渲染后端之间的接口,收集和调度这些工作。

8. Submit

最终:

Submit
↓
Unity Graphics Backend
↓
DirectX / Vulkan / Metal / ...
↓
GPU

因此,SRP 最核心的心智模型可以压缩成:

Camera
↓
RenderPipeline.Render()
↓
Cull
↓
CullingResults
↓
Filter + Sort + Drawing Rules
↓
RendererList
↓
CommandBuffer
↓
ScriptableRenderContext
↓
Submit
↓
Graphics API
↓
GPU

SRP 的本质不是“用 C# 实现渲染”,而是让 C# 获得组织 Render Loop 的权力:决定这一帧有哪些工作、它们以什么顺序发生、绘制哪些对象、使用哪些资源以及怎样把这些工作提交给 Unity 的底层图形系统。

理解这一层之后,URP 中为什么会出现:

ScriptableRenderer
Renderer Feature
ScriptableRenderPass
RecordRenderGraph

HDRP 中为什么会出现:

Custom Pass
Injection Point

以及现代 Unity 为什么越来越依赖:

Render Graph

就不再是一堆彼此孤立的 API。

它们只是在继续回答三个不同层级的问题:

SRP
“整条 Render Pipeline 怎样运行?”

↓


自定义渲染
“怎样往现有 URP / HDRP 中插入自己的工作?”

↓


Render Graph
“越来越多的 Pass 和 Resource 应该怎样组织?”

这也正好构成接下来两篇文章的方向。


注释

  1. Unity Scripting API,ScriptableRenderContext.CreateRendererList、RendererListParams、CommandBuffer.DrawRendererList 与 ScriptableRenderContext.DrawRenderers,Unity 6.0。Unity 6 已将直接 DrawRenderers 标记为 Obsolete,推荐通过 Renderer List API 描述和调度可见对象绘制。 ↩ ↩2 ↩3 ↩4

  2. Unity Manual,Scriptable Render Pipeline fundamentals / 可编程渲染管线基础知识,Unity 6.0。用于说明 SRP 是允许 C# 调度和配置渲染命令的薄 API 层,以及 URP、HDRP 与 Custom SRP 都建立在 SRP 之上。 ↩ ↩2 ↩3

  3. Unity Scripting API,RenderPipeline.Render,Unity 6.0。当前文档将 Render() 定义为自定义 Render Pipeline 的入口,并使用 ScriptableRenderContext 与 Camera 列表作为渲染上下文。 ↩

  4. Unity Scripting API,GraphicsSettings.currentRenderPipeline,Unity 6.0。说明当前实际生效的 Render Pipeline Asset 如何由 Graphics Settings 和 Quality Settings 决定。 ↩

  5. Unity Scripting API,ScriptableRenderContext.SetupCameraProperties,Unity 6.0。该调用负责调度 Camera 相关的 View、Projection、Clipping Plane 等全局 Shader 状态。 ↩

  6. Unity Scripting API,ScriptableRenderContext.Submit,Unity 6.0。官方定义为将已经调度的命令提交给 Rendering Loop 执行。 ↩

  7. Unity Scripting API,ScriptableCullingParameters、CullingOptions,Unity 6.0。说明 Culling 参数来自 Camera.TryGetCullingParameters(),并由 ScriptableRenderContext.Cull() 产生 CullingResults;CullingOptions.OcclusionCull 可以控制 Occlusion Culling。 ↩ ↩2

  8. Unity Scripting API,FilteringSettings 与 RenderQueueRange,Unity 6.0。FilteringSettings 可根据 Render Queue、GameObject Layer、Rendering Layer 等从可见对象中继续筛选子集。 ↩

  9. Unity Scripting API,SortingCriteria,Unity 6.0。官方提供 CommonOpaque 与 CommonTransparent 两种常用组合,其中透明对象需要 Back-to-Front 排序。 ↩

  10. Unity Scripting API,RenderPipeline.BeginContextRendering、BeginCameraRendering、EndCameraRendering 与 RenderPipelineManager,Unity 6.0。自定义 SRP 若希望触发这些 Rendering Event,需要在 Render Loop 的相应位置调用;旧 Frame Rendering 回调目前主要为兼容保留。 ↩

  11. Unity Scripting API,CommandBuffer 与 ScriptableRenderContext.ExecuteCommandBuffer,Unity 6.0。官方说明 ExecuteCommandBuffer() 会把命令注册到 Context 中,随后通过 Submit() 提交执行。 ↩

  12. Unity Manual,Upgrade to URP 17 (Unity 6)、Render Graph Viewer 与 Render Pipeline feature comparison,Unity 6.0。URP 17 在 Unity 6 中默认使用 Render Graph,新自定义 Pass 推荐使用 Render Graph API;Compatibility Mode 仍可运行旧路径,但官方说明不再继续开发或改进非 Render Graph 路径。当前 URP 与 HDRP 均支持 Render Graph。 ↩ ↩2

  13. Unity Manual,Scriptable Render Pipeline Core,Unity 6.0。SRP Core 提供可复用的平台抽象样板、渲染工具与 Shader Library,并由 URP、HDRP 共同使用。 ↩ ↩2

  14. Unity Manual,Scriptable Render Pipeline Batcher,Unity 6.0。SRP Batcher 主要通过减少具有相同 Shader Variant 的绘制之间的状态切换和 CPU 设置成本来优化 Draw Submission;支持 URP、HDRP 与 Custom SRP。 ↩

CONTINUE EXPLORING

沿着这篇文章继续

在地图中查看

On this page

1为什么 Unity 需要 Scriptable Render Pipeline?1.1“Scriptable” 到底意味着什么?2先看整张图:一帧渲染经历了什么?3SRP 最核心的两个对象3.1RenderPipelineAsset:配置与创建 Pipeline3.2RenderPipeline:真正执行渲染3.3Asset 和 Runtime Instance 为什么要分开?4写一个最小的 Scriptable Render Pipeline4.1SetupCameraProperties()4.2CommandBuffer4.3Submit()5Culling:为什么第一步不是直接 Draw?5.1从 Camera 获取 Culling 参数5.2执行 Cull5.3后续多个 Pass 复用结果5.4Culling 不只是“看物体在不在屏幕里”6从“可见”到“真正要画”:Sorting、Filtering 与 Drawing6.1FilteringSettings:画谁?6.2SortingSettings:先画谁?6.3DrawingSettings:怎样画?7Unity 6:RendererList 把这些规则组合起来8把 Opaque、Skybox 和 Transparent 串起来9一帧 SRP 的完整骨架10ScriptableRenderContext:C# 与 Unity 渲染后端之间的桥11CommandBuffer 与 ScriptableRenderContext 有什么区别?11.1ScriptableRenderContext 更像“当前渲染会话的接口”11.2CommandBuffer 更像“要执行的命令序列”11.3为什么还需要两种 Draw?12Renderer、Material、Shader 是怎样进入 SRP 的?13ShaderTagId:Pipeline 与 Shader 之间的协议14一个最小的 SRP Shader15Render Pass 和 Shader Pass 不是一回事15.1Shader Pass15.2Render Pass16Render Target:这些东西到底画到哪里?16.1为什么现代 Pipeline 会有那么多 Texture?17一条真正的 Pipeline 不只有 Opaque + Transparent18阴影也只是另一种“从可见性到绘制”19光照是怎样加入这套结构的?20为什么一条简单的 Render() 最终会失控?20.1问题一:Render Target 生命周期20.2问题二:Pass 之间存在依赖20.3问题三:有些 Pass 其实不需要运行21Render Graph 在 SRP 中处于什么位置?22SRP Core 又是什么?23URP 与 HDRP 是怎样建立在 SRP 之上的?23.1URP 做的事情23.2HDRP 做的事情24为什么学习 SRP 时不应该从 ScriptableRenderer 开始?25从 SRP 到 Renderer Feature / Custom Pass25.1改整条 Pipeline25.2只往现有 Pipeline 增加一步26一条现代 Render Pipeline 可以怎样理解?27常见概念混淆27.1SRP vs URP / HDRP27.2Renderer vs ScriptableRenderer27.3Render Pass vs Shader Pass27.4SRP vs SRP Core27.5SRP vs SRP Batcher27.6Culling vs GPU Occlusion Culling27.7CommandBuffer vs Render Graph28为什么理解 SRP 比记住 API 更重要?29总结:从 Camera 到 GPU1. Unity 进入 RenderPipeline.Render()2. Pipeline 处理 Camera3. Culling4. Filtering、Sorting、Drawing5. RendererList6. CommandBuffer7. ScriptableRenderContext8. Submit注释