MIT 许可证 NuGet

首页 / 功能 / 渲染管线

渲染管线

没有帧图,也没有通道调度器。一帧的顺序只写一次,在 Rendering/RenderPass.cs 里,每个后端都遵循它。那 损失了一些调度通用性;换来的是一个你一个下午就能读完、 并且能在四种图形 API 之间逐像素对比的管线。

一帧,按顺序

FrameStart Compute → Shadow → Scene → AfterScene Compute → Post → FinalBlit → Overlay
阶段那里运行什么
FrameStart Compute天空和大气 LUT、云噪声、SDF 体素化、DDGI 探针追踪 — 场景通道将要采样的任何东西。
Shadow级联太阳阴影加一个点状槽,渲染进单一深度图集。
Scene不透明和透明几何、天空、3D 精灵。写入场景颜色、深度和运动矢量。
AfterScene ComputeGTAO、TAA 解析、泛光降采样链、调试视图生成。
Post色调映射,并把后处理链合成进最终颜色目标。
FinalBlit解析到交换链,包括设计分辨率缩放。
Overlay2D 精灵、形状和 MSDF 文本,在其他一切之后以屏幕空间绘制。

那条链上缺了一个通道,因为它是条件性的: OutlineMask 位于 AfterScene compute 阶段和 Post 之间,它把请求了屏幕空间描边的对象渲染进一张遮罩, 合成步骤随后膨胀它。四个后端都实现了它,而在没有任何东西 请求描边的帧上它零成本 — 后端在分配遮罩 目标之前就提前退出了。

通道标识符

通道用枚举而不是字符串命名,这意味着忘记实现某个通道的后端 会编译失败,而不是绘制失败。

public enum RenderPassId
{
    Shadow,
    Scene,
    Post,
    OutlineMask,
    FinalBlit,
    Overlay
}

两个 compute 阶段不是通道标识符。它们是排程里 派发已注册 compute 工作的时点,下文会描述。

逐通道状态契约

每个通道都由一个携带其颜色、 深度和速度目标的 PassDesc 描述。固定排程的价值不在于排程本身 — 而在于每个通道周围的规则能用三行说清,然后就可以依赖它。

契约

资源状态在通道边界处转换,绝不在通道主体内部。 没有绑定能从一个通道存活到下一个;每个通道重新绑定它需要的东西。 视口和剪刀由通道框架设置,而 Shadow 通道是唯一 自己覆盖视口的地方,因为它把四个瓦片渲染进一张 图集。

那份契约是四个后端保持可对比的原因。Vulkan 屏障、D3D12 资源 转换和 Metal 编码器边界都落在同一个位置,所以两个 后端之间的分歧是某个通道里的 bug,而不是调度哲学的差异。

FrameSchedule:所有人共享的槽位

通道之间不互相查找。它们读写 FrameSchedule 上的一小组静态槽位,那就是 通道间通信的全部界面。

槽位类型含义
SceneColorRenderTarget?Scene 通道写入的 HDR 场景颜色。
SceneDepthRenderTarget?场景深度,由 GTAO、TAA 和调试视图读取。
SceneVelocityRenderTarget?运动矢量;关闭运动矢量时为 null。
ShadowMapRenderTarget?共享的级联图集。
PostColorRenderTarget?Post 通道合成进的目标。
RenderShadowAction<IGraphics>?Shadow 通道调用来绘制投影体的回调。
RenderPostAction<IGraphics, RenderTarget>?Post 通道调用来合成的回调。
BloomTexturestring?泛光结果的名字,或泛光关闭时为 null。
AoTexturestring?AO 结果的名字,或 null。
SceneColorOverridestring?让一个特效用自己的输出替代场景颜色。
SkyViewTexturestring?大气特效发布的天空视图 LUT。
CloudNoiseTexturestring?云噪声体积的名字。
AerialLutTexturestring?空气透视 LUT 的名字。
TaaActiveboolTAA 正在解析时为 true,相机因此施加抖动。

string? 槽位是刻意的。一个特效在 compute:// 名下发布一张纹理,并把那个名字写进 槽位;发现 null 的消费者就走它的无光照路径。因此关掉一个特效 和初始化它失败会产生相同的、正确的帧。

两个 compute 阶段

Compute 工作不硬编码进排程。特效把自己注册到一个 阶段,而阶段只决定它们是看到场景还是先于场景。

public enum ComputePhase
{
    FrameStart,
    AfterScene
}
// Returns false when the backend cannot
// run compute, or Initialize failed.
// Either way nothing is left behind.
bool ok = FrameSchedule.RegisterCompute(
    graphics, new MyEffect());

契约。 一个 compute 特效把自己注册进两个 compute 阶段之一,并且当后端无法运行它时不得留下残渣。这就是为什么同样的场景 代码在 GI、AO 和 TAA 全部关掉时也能产生正确的帧。

后处理与 Compute 特效 走读 随引擎发布的特效;编写一个 compute 特效 是 操作指南。

固定排程让你付出什么

值得直说,因为这是渲染器的核心设计取舍。

  • 你不能从应用代码添加通道。 添加一个意味着编辑 枚举和每个后端。Compute 特效的存在正是为了让大多数扩展 不需要新通道。
  • 没有自动的屏障放置或生命周期分析。 目标 为帧分配,而不是由资源分配器做别名复用。
  • 没有 G-buffer,没有延迟路径。 着色是前向的,组织为 不透明、透明和淡出组,配以剔除变体。每像素多光源不是 这个渲染器构建的目标 — 点状预算是每帧八个。
  • 作为交换: 整个帧是一个可读的文件,四个后端可以 互相 diff,而一个错误的像素只有少数几种可能的 原因。