Files
XCEngine/docs/plan/Rendering通用Shader多Pass执行重构计划_2026-04-12.md

16 KiB
Raw Blame History

Rendering 通用 Shader 多 Pass 执行重构计划

日期: 2026-04-12

1. 文档定位

这份计划用于正式解决当前渲染系统里的一个根问题:

  • Shader 资源层已经支持定义多个 Pass
  • UsePass 也已经能被解析和导入
  • 但主场景 BuiltinForwardPipeline 还没有把“同一个材质的多个 surface pass 按顺序执行”做成正式运行时能力

这不是 Nahida 特例,也不是某一个卡通 shader 的临时问题,而是当前 Rendering 模块在通用材质执行模型上的结构性缺口。

本计划的目标不是给 Nahida 加一个专用补丁而是把“Unity 式 shader 多 pass 执行”补成引擎正式能力。

2. 结论摘要

2.1 当前系统到底有没有多 pass

当前系统是“局部有多 pass通用 surface 没有多 pass”。

  • 有:
    • Shader 资源对象可以持有多个 Pass
    • UsePass 可用
    • DepthOnly / ShadowCaster / ObjectId / SelectionMask 这类专用 pass 会按 pass type 单独解析
    • PostProcess / FinalOutput 这类 fullscreen pass sequence 也支持多 pass 串联
  • 没有:
    • 主场景里“同一个材质的多个 graphics surface pass 自动执行”
    • 当前 BuiltinForwardPipeline 只会为一个材质挑一个主 surface pass 来画

2.2 当前根因

根因不是 shader authoring 语法不支持多 pass而是主场景 surface draw 路径还停留在“单 pass 材质模型”。

当前主路径的关键限制是:

  • TryResolveSurfacePassType() 只认 Unlit / ForwardLit
  • ResolveSurfaceShaderPass() 只返回一个 pass
  • DrawVisibleItems() 对每个 VisibleRenderItem 只执行一次主 surface draw

所以:

  • 你可以在 shader 里写 ForwardLit + Outline
  • 资源也能读进来
  • 但运行时不会自动再画第二遍 Outline

2.3 是否需要 Render Graph

这轮不需要 Render Graph而且不应该先上 Render Graph。

原因很明确:

  • 当前问题是“主场景通用材质的多 pass 调度缺失”
  • 不是“跨资源依赖分析 / 瞬态资源分配 / 全帧拓扑求解”问题
  • 现有架构已经有显式的 RenderPipeline + RenderPassSequence
  • 这次只需要把 BuiltinForwardPipeline 从“单 surface pass 执行器”升级成“多 surface pass 执行器”

结论:

  • 先不用 Render Graph
  • 先把主场景通用 multipass 执行能力补齐
  • 后续如果将来做更复杂的 frame dependency再考虑 Render Graph

3. 当前现状拆解

3.1 Shader 资源层

当前 Shader 资源层已经具备以下能力:

  • 一个 shader 可以拥有多个 Pass
  • pass 有自己的:
    • name
    • tags
    • resources
    • keywordDeclarations
    • variants
  • UsePass 会在构建时导入引用 pass

这说明“shader 文件里写多个 pass”本身不是问题。

3.2 专用渲染 pass 层

当前系统已经有一些“按 pass type 单独拉取并执行”的路径:

  • DepthOnly
  • ShadowCaster
  • ObjectId
  • SelectionMask
  • Skybox
  • GaussianSplat
  • Volumetric

它们说明当前引擎已经具备“识别 pass 元数据并挑一个对应 pass 执行”的机制,但这套机制目前没有扩展到主场景通用材质 surface 路径。

3.3 主场景 surface 路径

当前主场景 forward 渲染顺序是:

  • ExecuteForwardOpaquePass
  • ExecuteForwardSkyboxPass
  • BuiltinGaussianSplatPass
  • BuiltinVolumetricPass
  • ExecuteForwardTransparentPass

ExecuteForwardOpaquePass/TransparentPass 内部依然是“每个物体只解析一个主 surface pass”的模型。

这意味着:

  • 旧的单 pass lit/unlit 材质可以工作
  • Unity 式 Forward + Outline 这类角色 shader 不能完整工作

3.4 当前缺口的精确定义

缺的不是“多 pass 文件格式”,而是下面这套正式能力:

  1. 主场景 surface pass 的收集
  2. 主场景 surface pass 的排序
  3. 同一 VisibleRenderItem 的多次 graphics draw
  4. 主 surface pass 与附加 surface pass 的阶段归属
  5. 与现有 opaque / transparent / skybox / depth / shadow / objectId 的兼容

4. 本轮设计选择

4.1 选择的正式方案

本轮选择:

  • 不做 Nahida 特判
  • 不在某个 shader 上硬编码“再画一遍 outline”
  • 不重写整个渲染框架为 Render Graph
  • 直接把 BuiltinForwardPipeline 重构为“支持通用 surface multipass”

4.2 明确拒绝的方案

方案 A: Nahida / Toon 专用补丁

拒绝原因:

  • 只解决一个案例
  • 会把根因藏在角色特判里
  • 后续别的 Unity shader 还是一样会坏

方案 B: 先全面 Render Graph 化

拒绝原因:

  • 工作量过大
  • 与当前问题不对焦
  • 会把原本中等规模的结构重构,升级成高风险基础设施重写

方案 C: 继续维持单 surface pass只在 shader 层绕

拒绝原因:

  • 不能从根上支持 ForwardLit + Outline
  • 和 Unity 式多 pass 材质模型不一致

5. 目标架构

5.1 目标状态

主场景渲染的目标状态是:

  • 一个材质可以在 shader 内声明多个 surface pass
  • 渲染时会先收集这些 pass
  • 再按引擎定义好的 scene phase 顺序执行
  • 同一个 mesh / material 在一帧里可以被绘制多次

最小闭环至少要支持:

  • Unlit
  • ForwardLit
  • Outline

并为后续保留扩展点:

  • 更多角色附加 pass
  • 深度依赖的 rim pass
  • 特殊透明角色 pass

5.2 推荐的主场景 surface 阶段模型

本轮建议把主场景通用 surface pass 明确拆成以下阶段:

  1. OpaqueBase
  2. Skybox
  3. OpaqueAuxiliary
  4. TransparentBase
  5. TransparentAuxiliary

其中:

  • Unlit / ForwardLit 默认属于 Base
  • Outline 默认属于 Auxiliary
  • 本轮重点落地 OpaqueBase + OpaqueAuxiliary

这么拆的原因是:

  • Outline 一般要在角色主表面之后绘制
  • 又通常希望在透明物体之前完成
  • 这和当前 forward pipeline 的大框架可以自然兼容

5.3 推荐的 pass 类型模型

当前 BuiltinMaterialPass 需要正式扩展,而不是继续只停留在:

  • ForwardLit
  • Unlit

建议新增:

  • Outline

并在后续根据需要继续扩展。

这里的重点不是“枚举值多一个”本身,而是:

  • 主场景 surface 路径终于承认“一个材质有多个可执行的主 graphics pass”
  • 不再把 Outline 当作特殊插件逻辑,而是当作正式 pass type

6. 核心改造点

6.1 Pass 元数据层

涉及模块:

  • engine/include/XCEngine/Rendering/Builtin/BuiltinPassTypes.h
  • engine/include/XCEngine/Rendering/Builtin/BuiltinPassMetadataUtils.h
  • engine/include/XCEngine/Rendering/Builtin/BuiltinPassLayoutUtils.h

任务:

  • 新增 BuiltinMaterialPass::Outline
  • 增加 Outline 的 canonical name 解析
  • Outline 建立默认资源绑定规则
  • 保持现有 ForwardLit / Unlit / DepthOnly / ShadowCaster / ObjectId ... 兼容

验收:

  • shader 中 Name "Outline"Tags { "LightMode" = "Outline" } 能稳定识别
  • 不影响现有 builtin pass 匹配行为

6.2 主场景 surface pass 收集层

涉及模块:

  • engine/include/XCEngine/Rendering/Pipelines/BuiltinForwardPipeline.h
  • engine/src/Rendering/Pipelines/Internal/BuiltinForwardPipelineResources.cpp

当前问题:

  • ResolveSurfaceShaderPass() 是单返回值模型

本轮需要改成:

  • CollectSurfaceShaderPasses() 或等价结构
  • 返回一组“已解析的 surface pass 列表”

每个条目至少包含:

  • passType
  • shader
  • shaderPass
  • passName
  • scenePhase
  • effectiveRenderState

验收:

  • 同一材质可同时解析出 ForwardLit + Outline
  • 旧的单 pass 材质仍只解析出一个条目

6.3 主场景 surface pass 排序与阶段归属

涉及模块:

  • BuiltinForwardPipeline.cpp
  • BuiltinForwardPipelineResources.cpp

任务:

  • 不再只按 opaque/transparent 二元划分来理解主 surface
  • 要引入“主场景 surface 子阶段”的概念
  • 至少实现:
    • opaque base
    • opaque auxiliary
    • transparent base

本轮建议的执行顺序:

  1. OpaqueBase
  2. Skybox
  3. OpaqueAuxiliary
  4. GaussianSplat
  5. Volumetric
  6. TransparentBase
  7. TransparentAuxiliary

说明:

  • Outline 先放在 OpaqueAuxiliary
  • 本轮先不支持“透明物体 outline”的复杂排序
  • 透明附加 pass 只保留接口,不要求首轮全部打通

6.4 Draw 级执行模型

当前问题:

  • DrawVisibleItems() 对每个 item 只会 draw 一次主 surface

重构目标:

  • 对每个 VisibleRenderItem,先收集其可执行 surface pass
  • 再按当前 scene phase 过滤
  • 每个 pass 单独:
    • 解析 pipeline state
    • 绑定 descriptor sets
    • 执行 draw

这一步是整个重构的真正核心。

验收:

  • 一个物体在同一帧可发生多次主场景 draw
  • 每次 draw 都有独立 passName + renderState
  • pipeline cache 仍然以 shader + passName + renderState + keywordSignature + surface format 为 key 稳定工作

6.5 资源绑定与 layout 缓存

当前系统在这方面基础是够的,因为:

  • PassLayoutKey 已经包含 shader + passName
  • PipelineStateKey 已经包含 passName

所以本轮不需要重写 cache 架构,只需要保证:

  • 多 pass 场景下 cache key 继续区分不同 pass
  • Outline 这种 pass 的资源绑定计划能正确建立
  • MaterialTexture / MaterialConstants / PerObject 等绑定仍走现有机制

重点检查:

  • Outline 是否需要 Lighting
  • Outline 是否需要 ShadowReceiver
  • Outline 是否只需 PerObject + Material + MaterialTextures + Sampler

6.6 Shader authoring 约束正式化

这轮需要把“主场景 surface multipass”的 authoring 约束写清楚,而不是默认靠猜。

建议明确约定:

  • 主场景可执行的 surface pass 必须有明确 NameLightMode
  • pass 名称与 builtin canonical name 的映射规则固定
  • ForwardLit / Unlit / Outline 属于主场景通用 surface pass
  • DepthOnly / ShadowCaster / ObjectId / SelectionMask 继续属于专用路径

这样做的价值是:

  • shader authoring 规则清晰
  • 不会再出现“写了 pass 但没人知道该在哪个阶段执行”

6.7 XCCharacterToon.shader 的正式接入方式

在 multipass 正式能力完成后,XCCharacterToon.shader 的正确接法应为:

  • ForwardLit 负责角色主表面
  • Outline 负责描边
  • DepthOnly / ShadowCaster 继续沿用已有专用 pass

本轮对 Nahida 的定位是:

  • 不再作为特判对象
  • 只作为 multipass 正式化后的第一个高价值验证样本

7. 分阶段执行计划

Phase 0: 基线确认与测试样本准备

目标

在改主路径前固定当前行为,防止重构期间把旧材质全带坏。

任务

  • 盘点当前所有依赖 ForwardLit / Unlit 的单 pass 集成测试
  • 新建一个最小 multipass 测试场景:
    • 一个简单 mesh
    • 一个 ForwardLit + Outline 测试 shader
  • 明确 Nahida 作为高复杂度回归样本,不作为最小开发起点

完成标准

  • 有一个简单到足以定位多 pass 执行问题的专门测试场景
  • 现有 lit/unlit 场景回归基线不丢

Phase 1: 主场景通用 surface multipass 基础设施

目标

BuiltinForwardPipeline 具备“一个物体可执行多个主 surface pass”的正式能力。

任务

  • 扩展 BuiltinMaterialPass
  • 新增 Outline pass type
  • 单 pass 解析模型改成 multi-pass collection 模型
  • 引入主场景 surface phase
  • 重写 DrawVisibleItems() 执行逻辑

完成标准

  • 最小 multipass 测试 shader 能完成两次 draw
  • 单 pass shader 行为不回归

Phase 2: Outline 正式落地

目标

Outline 成为主场景正式 pass而不是外置补丁。

任务

  • Outline 补齐 builtin metadata / layout / binding 规则
  • XCCharacterToon.shader 中加入正式 Outline pass
  • 验证 Cull Front / ZTest / ZWrite / Blend 等状态是否符合需求
  • 首轮先以 static mesh + vertex color alpha 宽度控制闭环

明确暂缓

  • smoothNormal 新顶点语义支持
  • skinned mesh outline
  • 透明角色 outline 排序

完成标准

  • 最小 multipass 测试场景通过
  • Nahida 在 original 模式里开始出现正确的独立 outline draw

Phase 3: Nahida / Unity 风格角色卡通验证

目标

把 multipass 正式能力用于 Nahida验证这套方案确实能支撑 Unity 风格角色 shader。

任务

  • XCCharacterToon.shaderOutline 接入正式主场景 multipass
  • 重新生成 nahida.png
  • 对比 unlitforward litoriginal 三种模式的画面差异
  • 评估是否可以锁定新的 GT.ppm

完成标准

  • Nahida 的描边不再依赖临时逻辑
  • original 渲染链路进入可持续迭代状态

Phase 4: 通用化与规则收口

目标

把这次重构从“够 Nahida 用”收口成“引擎正式通用能力”。

任务

  • 补文档,明确 shader multipass authoring 规范
  • 视情况支持更多主场景 surface pass type
  • 清理旧的单 pass 假设与命名
  • 审查编辑器 / 材质检查器 / shader 资源导入链路是否需要显示 pass 信息

完成标准

  • Multipass 不再是隐式能力
  • 规则、测试、运行时行为三者一致

8. 测试计划

8.1 单元测试

重点新增或补强:

  • BuiltinPassMetadataUtils
    • Outline canonical name 匹配
  • BuiltinPassLayoutUtils
    • Outline 资源绑定计划
  • BuiltinForwardPipeline
    • 单材质多 surface pass 收集
    • scene phase 排序
    • 单 pass 回归不变

8.2 集成测试

建议新增:

  • tests/Rendering/integration/multipass_outline_scene
    • 最小 multipass 样例
  • 继续保留:
    • nahida_preview_scene
    • 现有 lit/unlit/backpack/material_state 等基础场景

8.3 人工验收

人工验收重点不只是“有没有画出来”,而是:

  • 是否真的发生了两次 draw
  • state / cull / depth 是否正确
  • 单 pass 材质是否回归
  • Nahida 的 outline 是否来自正式 pass而不是额外补丁

9. 风险与控制

9.1 最大风险

最大风险不是代码量,而是“把旧的单 pass 假设改坏”。

具体风险包括:

  • 单 pass lit/unlit 材质回归
  • opaque / transparent 分类被打乱
  • pipeline cache 或 descriptor set 复用逻辑出错
  • 新增 Outline 后错误进入 shadow/depth/objectId 路径

9.2 风险控制策略

  • 先做最小 multipass 场景,不直接拿 Nahida 起步
  • 先只开放 Outline 这一种主场景 auxiliary pass
  • 暂缓透明 multipass 与 depth-driven rim
  • 每个阶段都跑现有 forward 基础集成测试

10. 本轮不做的内容

本计划明确不把以下内容混进首轮 multipass 重构:

  • Render Graph 化
  • SkinnedMesh / 骨骼动画
  • GPU skinning
  • Transparent multipass 完整排序体系
  • Scene depth texture 的通用相机绑定
  • Unity 全量角色 shader 语义一次性补齐

这些都不是当前根因的第一优先级。

11. 完成判定

当满足以下条件时,才算这次“通用 shader 多 pass 执行重构”完成:

  1. 主场景 surface 路径正式支持一个材质执行多个 pass
  2. Outline 成为正式 builtin surface pass
  3. 现有单 pass 材质与基础场景不回归
  4. Nahida 的描边来自正式 multipass 执行,而不是特判
  5. 文档、测试、实现三者一致

12. 下一步建议

这份计划写完后的下一步,不是直接去碰 Nahida 复杂 shader 细节,而是:

  1. 先做 Phase 0
  2. 新建最小 multipass outline 场景与测试 shader
  3. 再开始 BuiltinForwardPipeline 的 multipass 基础设施改造

顺序不能反。

如果一上来就直接拿 Nahida 开刀,很容易把“结构性问题”和“角色 shader 细节问题”混在一起,最后继续变成补丁式推进。