16 KiB
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,而应优先完成:
ShaderMaterialBuiltin Pass ContractRenderer-owned Feature Contract
2. 为什么下一阶段不是 Render Graph
render graph 不是简单优化项,它本质上是更高一层的资源依赖与多 pass 调度框架。
但当前工程还没有满足它最该承接的前提:
- 还没有足够正式的 pass 分类体系
- 还没有正式的 shader / material 执行契约
- editor helper pass 与 runtime pass 还没有统一语义
- 还没有稳定的 renderer feature 输入输出边界
如果现在直接上 render graph,会出现一个问题:
- graph 框架先做了
- 真正的 shader/material/pass 契约还没收紧
- 最后 graph 里承载的还是一批语义松散的临时 pass
这会让架构“看起来高级”,但基础层仍然不稳。
因此下一阶段正确顺序应当是:
- 先收紧 shader/material/pass contract
- 再把更多 renderer feature 统一为正式能力
- 等真正的多 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,至少需要明确区分:
ForwardLitUnlitDepthOnlyShadowCasterObjectId- 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 名称:
ForwardLitUnlitDepthOnlyShadowCasterObjectId
说明:
ForwardLit支撑当前主线Unlit用于 editor helper、gizmo、调试对象、简单 UI meshDepthOnly和ShadowCaster为后面阴影与可见性阶段铺路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 策略
这一节必须进一步说透两个问题:
- 三后端语言不同,shader 资产到底怎么组织
- 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. 推荐的落地顺序
阶段 A:Formalize Builtin Pass Metadata
先完成:
ForwardLitUnlitDepthOnlyObjectId
需要落地的内容:
- pass name
- pass tag
- renderer 如何选择某个 material 的 pass
- builtin pipeline 不再依赖模糊字符串判断
完成标志:
MatchesBuiltinPass(...)不再只是当前这种最小过滤,而是更接近正式 pass contract
阶段 B:Material GPU Binding 最小闭环
先完成:
- material 常量数据打包
- texture binding 正式映射
- sampler 策略统一
- material 脏标记 -> GPU 缓存更新
完成标志:
- forward pipeline 不再只会找一张
_MainTex风格贴图 - material property 真正进入 shader 执行链
阶段 C:Shader Asset 真正进入 Render Pipeline
先完成:
- builtin shader 资产化
- 每个 builtin shader 具备 backend variant
- pipeline state 从 shader asset pass 创建
完成标志:
BuiltinForwardPipeline.cpp中的大段后端 shader 字符串不再是长期正式实现
阶段 D:Renderer-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. 阶段成功标准
这一阶段完成时,至少应满足:
- shader asset 已进入 renderer 主路径
- material property 与 texture binding 已形成正式 GPU 绑定链
- builtin pass contract 已具备最小完整集
- 三后端 shader 不再依赖 pipeline cpp 中散乱硬编码作为长期实现
- object-id / outline / grid 等 renderer feature 边界进一步明确
- renderer/editor 对应测试体系同步补齐
11. 一句话总结
Renderer 下一阶段的正确主线不是 render graph,而是:
- 先把
Shader MaterialPass ContractRenderer Feature Contract
做成正式能力。
只有这一层站稳之后,后面的:
- 阴影
- 更多 pass
- render graph
- C# SRP
才会是顺势生长,而不是继续堆临时方案。