Files
XCEngine/docs/used/Renderer下一阶段_ShaderMaterial与Pass体系设计.md

16 KiB
Raw Permalink Blame History

Renderer 下一阶段Shader、Material 与 Pass 体系设计

日期:2026-04-02

1. 阶段判断

当前 Renderer 阶段已经完成的事情,是把下面这条主链正式接通并收口:

RHI -> Rendering -> Editor Scene/Game Viewport

当前已经具备:

  • SceneRenderer -> CameraRenderer -> RenderPipeline 的主执行边界
  • scene camera request 组织能力
  • built-in forward 主几何绘制
  • object-id 渲染与 editor picking
  • built-in post-process 入口
  • editor viewport 宿主接入
  • 对应的 renderer/editor 自动化测试闭环

这意味着 Renderer 已经不再是“RHI 之上的一堆零散 draw call”而是已经形成了真实的模块边界。

但这并不意味着 Renderer 已经进入“可长期扩展”的状态。

当前阶段的真正短板,不是 render graph而是

  • shader 还没有进入正式 renderer 主路径
  • material 还不是正式 GPU 参数绑定载体
  • pass contract 还不完整
  • 三后端虽然都能跑,但 shader authoring 仍是内建硬编码

所以 Renderer 的下一阶段主线,不应优先做 render graph而应优先完成

  • Shader
  • Material
  • Builtin Pass Contract
  • Renderer-owned Feature Contract

2. 为什么下一阶段不是 Render Graph

render graph 不是简单优化项,它本质上是更高一层的资源依赖与多 pass 调度框架。

但当前工程还没有满足它最该承接的前提:

  1. 还没有足够正式的 pass 分类体系
  2. 还没有正式的 shader / material 执行契约
  3. editor helper pass 与 runtime pass 还没有统一语义
  4. 还没有稳定的 renderer feature 输入输出边界

如果现在直接上 render graph会出现一个问题

  • graph 框架先做了
  • 真正的 shader/material/pass 契约还没收紧
  • 最后 graph 里承载的还是一批语义松散的临时 pass

这会让架构“看起来高级”,但基础层仍然不稳。

因此下一阶段正确顺序应当是:

  1. 先收紧 shader/material/pass contract
  2. 再把更多 renderer feature 统一为正式能力
  3. 等真正的多 pass 复杂度上来后,再引入 render graph

3. 当前 Renderer 的真实问题

3.1 Shader 仍未进入正式主路径

当前 built-in forward pipeline 的 shader 仍然是直接硬编码在 C++ 中:

  • D3D12: HLSL
  • OpenGL: GLSL 430
  • Vulkan: GLSL 450

这意味着:

  • Material::GetShader() 虽然存在,但不控制当前主渲染
  • shader 资源尚未成为正式运行时契约
  • 新增 pass 或 shader 变体时,仍然需要直接改 pipeline C++

这不符合后续 Unity 风格 SRP 的方向。

3.2 Material 还是“资源状态载体”,不是正式 GPU 材质实例

当前 Material 已具备:

  • render queue
  • rasterizer / blend / depth-stencil render state
  • tag
  • property
  • texture binding
  • shader 引用

但真正进入 GPU 执行链路的内容,仍然很少:

  • per-object constant
  • 一张主纹理
  • 一个 sampler
  • 少量 pass metadata 过滤

也就是说:

  • material property 还没有正式映射到 GPU layout
  • material constant buffer 还没有进入正式绑定链路
  • texture binding 还没有从“约定名字查一张图”升级为“按 pass layout 正式绑定”

3.3 Pass contract 仍然不够完整

当前比较明确的 pass 只有:

  • forward 主几何
  • object-id
  • grid
  • selection outline
  • debug mask

但如果后面要走 Unity 风格 renderer至少需要明确区分

  • ForwardLit
  • Unlit
  • DepthOnly
  • ShadowCaster
  • ObjectId
  • editor helper pass

否则后面一旦加入阴影、深度预通道、材质分流、后处理和 SRP 注入就会重新回到“if/else 管线”。

3.4 三后端能跑,但还不是正式 shader 资产体系

当前三后端运行没问题,不代表 shader 体系已经成熟。

当前缺的是:

  • 统一的 shader 资产描述
  • 每个 shader pass 的 backend variant 管理
  • 统一的 descriptor / constant layout 描述
  • renderer 内部的 pipeline cache key 规范

现在只是“每个 backend 有一份能工作的内建 shader”离正式体系还有明显距离。


4. 下一阶段的核心目标

下一阶段目标不是“把所有渲染功能做完”,而是建立正式可扩展的 renderer 执行契约。

核心目标分四层。

4.1 正式建立 Shader Asset Contract

下一阶段应把 Shader 从资源占位,提升为 renderer 正式消费的资源。

建议 shader 资产至少包含:

  • shader 名称与 GUID
  • pass 列表
  • 每个 pass 的逻辑名称
  • 每个 pass 的 tag例如 LightMode
  • 每个 pass 的 property layout
  • 每个 pass 的 resource binding layout
  • 每个 backend 的 shader variant

建议的概念模型:

ShaderAsset
  -> ShaderPassDesc[]
       -> passName
       -> tags
       -> propertyLayout
       -> backendVariants

这里的关键点不是一开始就做复杂 shader graph而是先明确

  • renderer 以后创建 pipeline不再直接从 C++ 字符串取 shader
  • renderer 应从 shader asset 的某个 pass 中取当前 backend 对应 variant

4.2 正式建立 Material Instance Contract

Material 下一阶段要从“资源状态容器”升级为“可绑定的材质实例”。

Material 至少应当明确:

  • 它引用哪个 shader
  • 它选择 shader 的哪个 pass
  • 它当前有哪些 property override
  • 它有哪些 texture binding
  • 它导出的 render state 是什么

下一阶段不要求一开始就做复杂材质编辑器,但必须完成:

  • property -> GPU 常量区布局
  • texture binding -> descriptor binding
  • pass 过滤规则
  • material instance 的缓存与脏标记

4.3 正式建立 Builtin Pass Contract

下一阶段应当把当前 renderer 内建 pass 明确分层:

  • 几何主 pass
  • 深度/阴影类 pass
  • object-id/editor helper pass
  • post-process / overlay pass

建议第一批正式化的 pass 名称:

  • ForwardLit
  • Unlit
  • DepthOnly
  • ShadowCaster
  • ObjectId

说明:

  • ForwardLit 支撑当前主线
  • Unlit 用于 editor helper、gizmo、调试对象、简单 UI mesh
  • DepthOnlyShadowCaster 为后面阴影与可见性阶段铺路
  • ObjectId 让 editor/runtime picking 有正式 renderer 合约

4.4 正式建立 Renderer Feature Contract

当前 grid / outline / object-id / debug mask 已经部分进入 renderer但仍然带有明显 editor 来路。

下一阶段应继续把它们定义为 renderer feature而不是 editor 特判:

  • object-id output
  • selection / mask debug
  • overlay helper contract
  • camera request 上的 feature request

目标不是马上做完整 feature graph而是明确

  • 哪些 feature 属于 renderer
  • 哪些输入由 editor 组装
  • 哪些输出由 renderer 提供

5. 三后端 Shader 策略

这一节必须进一步说透两个问题:

  1. 三后端语言不同shader 资产到底怎么组织
  2. shader authoring 到底采用“Unity 式一份 shader 里写多个 stage”还是把 vertex / fragment 拆成独立文件

5.1 当前阶段不建议直接追求“单源码全平台自动转译”

理论上可以追求:

  • 统一 HLSL
  • 再编到 SPIR-V / GLSL

但这会立刻引入:

  • 工具链依赖
  • shader reflection
  • backend 兼容差异处理
  • 调试复杂度

对于当前工程,这不是下一阶段最优先的问题。

5.2 下一阶段建议采用“统一逻辑资产 + 后端分 variant”的务实方案

建议下一阶段的 shader 策略是:

  • 逻辑上一个 shader asset
  • 资产里按 pass 持有多个 backend variant
  • renderer 根据 backend 选择对应源码/二进制

例如:

BuiltinLit.shader
  Pass: ForwardLit
    D3D12  -> HLSL
    OpenGL -> GLSL 430
    Vulkan -> GLSL 450 / SPIR-V

这样做的优点:

  • 三后端路径清晰
  • 不引入过早的跨编译复杂度
  • 仍然能在资产层统一 shader 逻辑身份
  • 后续要切换成统一 authoring 也有演进空间

5.3 Vulkan 的长期方向

Vulkan 长期更适合进入:

  • 预编译 SPIR-V
  • 反射生成 binding layout

但这属于再下一阶段的工程化增强。

当前下一阶段只要求:

  • Vulkan 不再依赖 pipeline cpp 内嵌 shader 字符串的散乱模式
  • Vulkan variant 被纳入正式 shader asset contract

5.4 推荐的 Shader 资产组织方式

建议下一阶段采用:

  • 逻辑上一个 ShaderAsset
  • 资产内部按 Pass 组织
  • 每个 Pass 内再按 Stage 与 Backend 持有 variant

推荐概念模型:

ShaderAsset
  -> Pass: ForwardLit
       -> Stage: Vertex
            -> D3D12  : xxx.vs.hlsl
            -> OpenGL : xxx.vs.glsl
            -> Vulkan : xxx.vs.vk.glsl / xxx.vs.spv
       -> Stage: Fragment
            -> D3D12  : xxx.ps.hlsl
            -> OpenGL : xxx.fs.glsl
            -> Vulkan : xxx.fs.vk.glsl / xxx.fs.spv

  -> Pass: DepthOnly
       -> ...

也就是说:

  • 对 renderer 来说,真正识别的是一个 ShaderAsset
  • 对 pass 来说,拿到的是“这个 pass 在当前 backend 下对应的各 stage 变体”
  • 对后端来说,最终看到的仍然是它自己能吃的 shader 源码或二进制

这样处理后:

  • shader 逻辑身份是统一的
  • backend 差异被收进 variant 层
  • pass / stage / backend 三个维度都清楚

5.5 不建议直接照搬 Unity ShaderLab 的单文件大一统方案

如果完全模仿 Unity最直观的形式是

  • 一份 shader 文件
  • 里面写 Pass
  • Pass 里面再写 Vertex/Fragment

这种方式的优点是:

  • 对材质和 pass 关系表达很强
  • 很适合后续 editor / inspector / C# SRP 暴露

但它当前直接落地的缺点也很明显:

  • 你现在有三个 backend
  • backend shader 语言并不统一
  • 还没有自己的 shader import / include / preprocess / reflection 工具链

如果现在直接做成 Unity 那种单文件 DSL等于同时要解决

  • shader 语法设计
  • parser
  • include 系统
  • multi-pass 语义
  • backend variant 分发
  • material property layout

这会把下一阶段的复杂度一下子拉爆。

5.6 下一阶段更务实的做法

下一阶段建议采用“两层模型”:

第一层:逻辑层接近 Unity

保留 Unity 风格的核心语义:

  • 一个 shader 有多个 pass
  • 每个 pass 有名字和 tag
  • material 绑定的是 shader 与 pass

第二层:物理文件层先按 stage 分开

实际文件先拆成:

  • *.vs.hlsl
  • *.ps.hlsl
  • *.vs.glsl
  • *.fs.glsl
  • *.vs.vk.glsl
  • *.fs.vk.glsl

或者编译后:

  • *.spv

也就是说:

  • 逻辑上用 Unity 式“一个 shader 拥有多个 pass”
  • 物理落地上暂时不用 Unity 式“一个文件里硬塞所有 backend/stage”

这是当前阶段最稳妥的方案。

5.7 对“vertex / fragment 是合一还是分开”的明确结论

结论分两层:

从逻辑资产视角看

应当是“合一”的。

也就是:

  • 一个 ShaderAsset
  • 下面有一个或多个 Pass
  • 一个 Pass 同时拥有 vertex / fragment 等 stage

这和 Unity 的思路一致。

从实际源码文件视角看

应当先“分开”。

也就是:

  • vertex shader 单独文件
  • fragment shader 单独文件
  • backend variant 单独文件

原因:

  • 更容易做 backend 分发
  • 更容易调试编译错误
  • 更容易做最小 shader import
  • 更适合你当前三后端并行维护

所以不要把“逻辑合一”和“源码物理文件合一”混为一谈。

最终建议是:

  • 逻辑模型采用 Unity 风格
  • 文件组织先采用分 stage、分 backend

5.8 未来再向 Unity ShaderLab 靠拢的演进路径

等下一阶段把下面这些都做稳之后:

  • shader asset contract
  • material property binding
  • backend variant 选择
  • pass contract

后面再往上加一层更接近 Unity ShaderLab 的 authoring 语法,就顺理成章:

ShaderLab-like Authoring
    -> Shader Importer
    -> ShaderAsset / Pass / Variant
    -> RenderPipeline Runtime

也就是说:

  • 现在先做 runtime contract
  • 以后再做更高级的 shader authoring front-end

这样不会把工程顺序做反。


6. 建议的新分层

建议 Renderer 下一阶段形成下面这套更稳定的分层:

Scene / Components / Resources
    -> RenderSceneExtractor / RenderRequestPlanner
    -> Shader & Material Runtime
    -> CameraRenderer / RenderPipeline
    -> Builtin Feature Passes
    -> RHI
    -> D3D12 / OpenGL / Vulkan

其中新增的重点层是:

6.1 Shader & Material Runtime

负责:

  • 解析 shader asset / material asset
  • 生成 pass 级 binding layout
  • 维护 material GPU 数据与脏标记
  • 提供 pipeline cache key

6.2 Builtin Feature Passes

负责:

  • object-id
  • grid
  • outline
  • shadow/depth 等 builtin pass

这样可以让:

  • RenderPipeline 更聚焦于场景主流程组织
  • 各 pass 更聚焦于自己真正的职责

7. 推荐的落地顺序

阶段 AFormalize Builtin Pass Metadata

先完成:

  • ForwardLit
  • Unlit
  • DepthOnly
  • ObjectId

需要落地的内容:

  • pass name
  • pass tag
  • renderer 如何选择某个 material 的 pass
  • builtin pipeline 不再依赖模糊字符串判断

完成标志:

  • MatchesBuiltinPass(...) 不再只是当前这种最小过滤,而是更接近正式 pass contract

阶段 BMaterial GPU Binding 最小闭环

先完成:

  • material 常量数据打包
  • texture binding 正式映射
  • sampler 策略统一
  • material 脏标记 -> GPU 缓存更新

完成标志:

  • forward pipeline 不再只会找一张 _MainTex 风格贴图
  • material property 真正进入 shader 执行链

阶段 CShader Asset 真正进入 Render Pipeline

先完成:

  • builtin shader 资产化
  • 每个 builtin shader 具备 backend variant
  • pipeline state 从 shader asset pass 创建

完成标志:

  • BuiltinForwardPipeline.cpp 中的大段后端 shader 字符串不再是长期正式实现

阶段 DRenderer-owned Feature Contract 收口

先完成:

  • object-id request/output 正式化
  • outline/grid 继续从 editor 逻辑脱耦
  • camera request 上的 feature request 更明确

完成标志:

  • editor 侧更多只做 request 装配,而不是 feature 逻辑承担者

阶段 E验证与回归

必须补的测试:

  • renderer unit tests
  • shader/material runtime unit tests
  • pass metadata 选择测试
  • backend integration smoke tests
  • editor viewport regression

8. 下一阶段明确不做的内容

以下内容不进入下一阶段主线:

  • render graph
  • 完整 shader graph
  • 完整 deferred renderer
  • 大规模后处理栈
  • 完整阴影系统产品化
  • C# SRP 真正脚本驱动落地

原因不是这些不重要,而是它们依赖下一阶段先把基础契约做稳。


9. 与 Unity 风格架构的承接关系

如果后面目标是做 Unity 风格的 C# SRP那么下一阶段必须先把“原生 renderer 可被脚本驱动”的基础层做好。

Unity 风格承接关系应理解为:

Shader / Material / Pass Contract
    -> Native Render Pipeline Runtime
    -> ScriptableRenderContext / CommandBuffer
    -> C# RenderPipelineAsset / RenderPipeline

也就是说:

  • 当前下一阶段做的不是 SRP 本身
  • 但做的是 SRP 能否成立的地基

如果这一层继续缺失,后面脚本层会直接耦合到一堆临时内建逻辑上,最后 SRP 只会沦为“脚本包装的硬编码 forward pipeline”。


10. 阶段成功标准

这一阶段完成时,至少应满足:

  1. shader asset 已进入 renderer 主路径
  2. material property 与 texture binding 已形成正式 GPU 绑定链
  3. builtin pass contract 已具备最小完整集
  4. 三后端 shader 不再依赖 pipeline cpp 中散乱硬编码作为长期实现
  5. object-id / outline / grid 等 renderer feature 边界进一步明确
  6. renderer/editor 对应测试体系同步补齐

11. 一句话总结

Renderer 下一阶段的正确主线不是 render graph而是

  • 先把 Shader
  • Material
  • Pass Contract
  • Renderer Feature Contract

做成正式能力。

只有这一层站稳之后,后面的:

  • 阴影
  • 更多 pass
  • render graph
  • C# SRP

才会是顺势生长,而不是继续堆临时方案。