16 KiB
Rendering 通用 Shader 多 Pass 执行重构计划
日期: 2026-04-12
1. 文档定位
这份计划用于正式解决当前渲染系统里的一个根问题:
Shader资源层已经支持定义多个PassUsePass也已经能被解析和导入- 但主场景
BuiltinForwardPipeline还没有把“同一个材质的多个 surface pass 按顺序执行”做成正式运行时能力
这不是 Nahida 特例,也不是某一个卡通 shader 的临时问题,而是当前 Rendering 模块在通用材质执行模型上的结构性缺口。
本计划的目标不是给 Nahida 加一个专用补丁,而是把“Unity 式 shader 多 pass 执行”补成引擎正式能力。
2. 结论摘要
2.1 当前系统到底有没有多 pass
当前系统是“局部有多 pass,通用 surface 没有多 pass”。
- 有:
Shader资源对象可以持有多个PassUsePass可用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/ForwardLitResolveSurfaceShaderPass()只返回一个 passDrawVisibleItems()对每个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 有自己的:
nametagsresourceskeywordDeclarationsvariants
UsePass会在构建时导入引用 pass
这说明“shader 文件里写多个 pass”本身不是问题。
3.2 专用渲染 pass 层
当前系统已经有一些“按 pass type 单独拉取并执行”的路径:
DepthOnlyShadowCasterObjectIdSelectionMaskSkyboxGaussianSplatVolumetric
它们说明当前引擎已经具备“识别 pass 元数据并挑一个对应 pass 执行”的机制,但这套机制目前没有扩展到主场景通用材质 surface 路径。
3.3 主场景 surface 路径
当前主场景 forward 渲染顺序是:
ExecuteForwardOpaquePassExecuteForwardSkyboxPassBuiltinGaussianSplatPassBuiltinVolumetricPassExecuteForwardTransparentPass
但 ExecuteForwardOpaquePass/TransparentPass 内部依然是“每个物体只解析一个主 surface pass”的模型。
这意味着:
- 旧的单 pass lit/unlit 材质可以工作
- Unity 式
Forward + Outline这类角色 shader 不能完整工作
3.4 当前缺口的精确定义
缺的不是“多 pass 文件格式”,而是下面这套正式能力:
- 主场景 surface pass 的收集
- 主场景 surface pass 的排序
- 同一
VisibleRenderItem的多次 graphics draw - 主 surface pass 与附加 surface pass 的阶段归属
- 与现有 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 在一帧里可以被绘制多次
最小闭环至少要支持:
UnlitForwardLitOutline
并为后续保留扩展点:
- 更多角色附加 pass
- 深度依赖的 rim pass
- 特殊透明角色 pass
5.2 推荐的主场景 surface 阶段模型
本轮建议把主场景通用 surface pass 明确拆成以下阶段:
OpaqueBaseSkyboxOpaqueAuxiliaryTransparentBaseTransparentAuxiliary
其中:
Unlit/ForwardLit默认属于BaseOutline默认属于Auxiliary- 本轮重点落地
OpaqueBase + OpaqueAuxiliary
这么拆的原因是:
Outline一般要在角色主表面之后绘制- 又通常希望在透明物体之前完成
- 这和当前 forward pipeline 的大框架可以自然兼容
5.3 推荐的 pass 类型模型
当前 BuiltinMaterialPass 需要正式扩展,而不是继续只停留在:
ForwardLitUnlit
建议新增:
Outline
并在后续根据需要继续扩展。
这里的重点不是“枚举值多一个”本身,而是:
- 主场景 surface 路径终于承认“一个材质有多个可执行的主 graphics pass”
- 不再把
Outline当作特殊插件逻辑,而是当作正式 pass type
6. 核心改造点
6.1 Pass 元数据层
涉及模块:
engine/include/XCEngine/Rendering/Builtin/BuiltinPassTypes.hengine/include/XCEngine/Rendering/Builtin/BuiltinPassMetadataUtils.hengine/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.hengine/src/Rendering/Pipelines/Internal/BuiltinForwardPipelineResources.cpp
当前问题:
ResolveSurfaceShaderPass()是单返回值模型
本轮需要改成:
CollectSurfaceShaderPasses()或等价结构- 返回一组“已解析的 surface pass 列表”
每个条目至少包含:
passTypeshadershaderPasspassNamescenePhaseeffectiveRenderState
验收:
- 同一材质可同时解析出
ForwardLit + Outline - 旧的单 pass 材质仍只解析出一个条目
6.3 主场景 surface pass 排序与阶段归属
涉及模块:
BuiltinForwardPipeline.cppBuiltinForwardPipelineResources.cpp
任务:
- 不再只按
opaque/transparent二元划分来理解主 surface - 要引入“主场景 surface 子阶段”的概念
- 至少实现:
- opaque base
- opaque auxiliary
- transparent base
本轮建议的执行顺序:
OpaqueBaseSkyboxOpaqueAuxiliaryGaussianSplatVolumetricTransparentBaseTransparentAuxiliary
说明:
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 + passNamePipelineStateKey已经包含passName
所以本轮不需要重写 cache 架构,只需要保证:
- 多 pass 场景下 cache key 继续区分不同 pass
Outline这种 pass 的资源绑定计划能正确建立MaterialTexture/MaterialConstants/PerObject等绑定仍走现有机制
重点检查:
Outline是否需要LightingOutline是否需要ShadowReceiverOutline是否只需PerObject + Material + MaterialTextures + Sampler
6.6 Shader authoring 约束正式化
这轮需要把“主场景 surface multipass”的 authoring 约束写清楚,而不是默认靠猜。
建议明确约定:
- 主场景可执行的 surface pass 必须有明确
Name或LightMode - pass 名称与 builtin canonical name 的映射规则固定
ForwardLit/Unlit/Outline属于主场景通用 surface passDepthOnly/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 - 新增
Outlinepass 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中加入正式Outlinepass - 验证
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.shader的Outline接入正式主场景 multipass - 重新生成
nahida.png - 对比
unlit、forward lit、original三种模式的画面差异 - 评估是否可以锁定新的
GT.ppm
完成标准
- Nahida 的描边不再依赖临时逻辑
original渲染链路进入可持续迭代状态
Phase 4: 通用化与规则收口
目标
把这次重构从“够 Nahida 用”收口成“引擎正式通用能力”。
任务
- 补文档,明确 shader multipass authoring 规范
- 视情况支持更多主场景 surface pass type
- 清理旧的单 pass 假设与命名
- 审查编辑器 / 材质检查器 / shader 资源导入链路是否需要显示 pass 信息
完成标准
- Multipass 不再是隐式能力
- 规则、测试、运行时行为三者一致
8. 测试计划
8.1 单元测试
重点新增或补强:
BuiltinPassMetadataUtilsOutlinecanonical name 匹配
BuiltinPassLayoutUtilsOutline资源绑定计划
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 执行重构”完成:
- 主场景 surface 路径正式支持一个材质执行多个 pass
Outline成为正式 builtin surface pass- 现有单 pass 材质与基础场景不回归
- Nahida 的描边来自正式 multipass 执行,而不是特判
- 文档、测试、实现三者一致
12. 下一步建议
这份计划写完后的下一步,不是直接去碰 Nahida 复杂 shader 细节,而是:
- 先做
Phase 0 - 新建最小 multipass outline 场景与测试 shader
- 再开始
BuiltinForwardPipeline的 multipass 基础设施改造
顺序不能反。
如果一上来就直接拿 Nahida 开刀,很容易把“结构性问题”和“角色 shader 细节问题”混在一起,最后继续变成补丁式推进。