13 KiB
Renderer模块设计与实现
1. 背景
XCEngine 当前已经完成了较为可用的 RHI 抽象层,且已经具备:
Scene + GameObject + Component基础场景模型CameraComponent/LightComponent等基础组件Mesh/Material/Texture/Shader等资源类型- D3D12 / OpenGL 双后端 RHI 抽象与测试体系
下一阶段不应该继续封闭式打磨 RHI,而应该在 RHI 之上正式建立 Renderer 模块。
这里的 Renderer 模块不是“最终形态的 SRP”,而是:
- 先建立一层 原生渲染运行时
- 先让场景对象能够以正式渲染链路被绘制
- 同时在设计上预留未来 C# Scriptable Render Pipeline(SRP) 的接入点
也就是说,当前阶段的正确目标不是直接实现 Unity 的 URP/HDRP,而是先建立一套 与 Unity 渲染架构方向一致的原生基础层,后续让 C# SRP 驱动它。
2. 设计目标
Renderer 模块的目标分为两层:
2.1 当前阶段目标
先完成一套最小但完整的原生渲染链路:
- 从
Scene中提取可渲染对象 - 通过
Camera构建视图与投影数据 - 通过
Material/Mesh/Texture构建 GPU 绘制数据 - 在 RHI 之上完成正式的 frame 渲染
- 支持 swapchain 输出与离屏输出
- 建立独立于 editor 的渲染宿主模型
2.2 面向未来 C# SRP 的目标
当前阶段的实现必须为后续演进预留稳定边界:
- 未来允许用 C# 定义
RenderPipelineAsset/RenderPipeline - 未来允许用 C# 组织 render pass
- 未来允许 editor
SceneView/GameView通过同一套 renderer 输出 - 未来允许 C# 脚本控制 camera 渲染、pass 排序、目标输出与 command buffer
因此,当前阶段的原生 Renderer 不能做成一个写死的“大一统内建渲染函数”,而应该一开始就具备“可被上层 pipeline 驱动”的结构。
3. 与 Unity 渲染架构的对应关系
当前建议的路线与 Unity 的总体方向是对齐的,但要注意分层位置。
3.1 推荐分层
Scene / Components / Resources
↓
Renderer 模块(原生渲染运行时)
↓
未来 C# SRP 层(脚本化渲染管线)
↓
RHI 抽象层
↓
D3D12 / OpenGL / Vulkan 后端
3.2 各层职责
Scene / Components / Resources
负责描述“要渲染什么”,例如:
- 场景对象
- 相机
- 灯光
- 网格
- 材质
- 贴图
这一层不应该直接持有后端 API 对象。
Renderer 模块(本阶段核心)
负责描述“如何从场景变成 draw call”,例如:
- 渲染对象抽取
- 可见性裁剪
- GPU 资源缓存
- camera frame 数据组织
- render target / depth target 管理
- render pass 调度
- 内建前向管线
这一层是未来 C# SRP 的原生支撑层。
未来 C# SRP 层
负责描述“以脚本方式控制渲染流程”,例如:
RenderPipelineAssetRenderPipelineScriptableRenderContextCommandBufferRenderPassEvent- pass 注入与重排
这一层不应该直接绕过 Renderer 模块去操作后端 API。
RHI 抽象层
负责统一 GPU 接口与资源对象,是渲染系统的执行后端,而不是场景渲染逻辑本身。
3.3 与 Unity 的概念映射
| Unity 概念 | XCEngine 当前/规划对应 |
|---|---|
Camera |
CameraComponent |
Light |
LightComponent |
MeshFilter |
计划新增 MeshFilterComponent |
MeshRenderer |
计划新增 MeshRendererComponent |
RenderPipelineAsset |
未来 Renderer 模块上的 pipeline asset 抽象 |
RenderPipeline |
未来 Renderer 模块上的 pipeline 实例抽象 |
ScriptableRenderContext |
未来 Renderer 模块对脚本暴露的原生 render context |
CommandBuffer |
未来 Renderer 模块对脚本暴露的命令缓冲抽象 |
GraphicsDevice / native render backend |
当前 RHI + 后端实现 |
结论是:
- 方向上符合 Unity 渲染架构
- 当前阶段实现的应是 Unity 渲染体系中的原生底座
- 而不是直接跳到最终的脚本化 SRP
4. 核心设计原则
4.1 先建立原生渲染运行时,再开放脚本化管线
如果现在直接做 C# SRP,而原生 Renderer 边界还不存在,后续会出现:
- C# API 直接耦合 RHI
- editor viewport 与 runtime camera 逻辑混杂
- 资源对象与 GPU 对象生命周期混乱
因此必须先收敛原生 Renderer 模块。
4.2 Scene 层只描述逻辑对象,不持有后端对象
GameObject、Component、Mesh、Material 等对象只能描述逻辑与资源,不应该直接持有 D3D12/OpenGL 私有对象。
GPU 对象应由 Renderer 内部缓存层负责创建和复用。
4.3 editor 只是渲染宿主,不是渲染逻辑本体
GameView / SceneView 最终只是 Renderer 的输出宿主。
Renderer 本身必须先支持:
- 输出到 swapchain
- 输出到离屏纹理
然后 editor 再把离屏纹理接进 ImGui 面板。
4.4 为未来 SRP 预留 pipeline 抽象
即使第一阶段先做内建前向渲染,也不应该把逻辑写死成单一 SceneRenderer::DrawEverything()。
应该从一开始就保留:
RenderPipelineRenderPipelineAssetRenderContext- camera 列表驱动
- pass 分阶段执行
这样未来 C# 只是在这个原生结构上做绑定,而不是重做一遍架构。
4.5 测试体系与渲染层分离
tests/RHI/ 继续只验证 RHI。
Renderer 模块应建立自己的测试体系:
tests/Rendering/unit/tests/Rendering/integration/
这样职责边界才清晰。
5. 模块划分建议
建议新增 Rendering 模块,作为场景与 RHI 之间的正式中间层。
5.1 推荐目录结构
engine/
├── include/XCEngine/Rendering/
│ ├── RenderSurface.h
│ ├── RenderContext.h
│ ├── RenderPipeline.h
│ ├── RenderPipelineAsset.h
│ ├── SceneRenderer.h
│ ├── RenderSceneExtractor.h
│ ├── RenderCameraData.h
│ ├── VisibleRenderObject.h
│ ├── RenderResourceCache.h
│ └── Pipelines/
│ └── BuiltinForwardPipeline.h
└── src/Rendering/
├── RenderSurface.cpp
├── SceneRenderer.cpp
├── RenderSceneExtractor.cpp
├── RenderResourceCache.cpp
└── Pipelines/
└── BuiltinForwardPipeline.cpp
5.2 组件层建议
为了尽可能对齐 Unity,而不是做一个临时过渡方案,建议直接采用:
MeshFilterComponentMeshRendererComponent
其中:
MeshFilterComponent
负责“这个对象使用哪一个 mesh”:
ResourceHandle<Mesh>
MeshRendererComponent
负责“这个对象如何被渲染”:
- 材质数组
- cast shadow / receive shadow
- render queue / layer / enable 状态
- 未来可扩展 light probe / motion vector / static batching 标记
这样做的好处是:
- 更贴近 Unity 的对象模型
- 更容易映射未来 C# API
- 更容易在 editor Inspector 中呈现
- 更容易为
SkinnedMeshRenderer、SpriteRenderer等后续组件扩展留位置
5.3 Renderer 内部运行时对象
RenderSurface
统一表示渲染输出目标:
- 交换链输出
- 离屏 color/depth 输出
- editor viewport 输出
RenderSceneExtractor
负责从 Scene 中提取本帧可渲染对象:
- mesh
- material
- transform
- bounds
- render state
RenderResourceCache
负责把资源模块对象转成 GPU 可用对象:
- mesh -> vertex/index buffer
- texture -> RHI texture / resource view
- material -> descriptor set / uniform buffer / pipeline key
- shader pass -> pipeline state
RenderContext
作为原生渲染执行上下文,未来用于承接脚本化 pipeline 的调度。
它应封装:
- 当前 frame 的 command list
- render target 设置
- clear / draw / submit
- camera 相关渲染上下文
RenderPipeline
用于抽象具体渲染流程。
第一阶段只有一个原生内建实现:
BuiltinForwardPipeline
未来再开放:
- native 可切换 pipeline
- C# 绑定的 scriptable pipeline
6. 第一阶段实现边界
第一阶段只做最小可用链路,不做“大而全”。
6.1 第一阶段要做
Rendering模块骨架MeshFilterComponent/MeshRendererComponentRenderSurfaceRenderSceneExtractorRenderResourceCacheSceneRendererBuiltinForwardPipeline- 单 camera
- 单方向的 opaque forward 渲染
- 深度测试与深度写入
- 交换链输出
- 离屏输出
6.2 第一阶段先不做
- 阴影
- 后处理
- 延迟渲染
- 完整 PBR
- render graph
- C# SRP 真正落地
- editor viewport 完整交互
- 多 camera 叠加
6.3 第一阶段材质能力建议
建议先支持两档:
UnlitTextureSimpleLit
其中:
UnlitTexture用于先打通最小链路SimpleLit用于验证灯光、法线与材质基础通路
7. 面向未来 C# SRP 的预留设计
虽然第一阶段先做原生内建渲染,但必须提前约束下面这些方向。
7.1 先定义 pipeline 边界,再定义内建实现
正确顺序应当是:
- 先定义
RenderPipeline抽象 - 再实现
BuiltinForwardPipeline - 后续 C# SRP 只是在这个边界上做脚本绑定
而不是:
- 先写死一个
SceneRenderer - 以后再强行拆成 pipeline
第二种方式后续返工会很大。
7.2 Renderer 模块应向未来脚本层暴露的概念
当前阶段不一定全部实现,但结构上要留位置:
RenderPipelineAssetRenderPipelineRenderContextCullingResultsDrawingSettingsFilteringSettingsShaderTagCommandBufferRendererList
这些概念不一定要立刻与 Unity 一字不差,但应该在职责上能对应上。
7.3 材质与 Shader 资产模型不能停留在“单 shader 文件 + 属性包”
未来做 SRP 时,shader pass 选择、render queue、tag、render state 都是必要能力。
因此当前阶段即使先不全量实现,也不能把资产模型彻底锁死在过于简单的结构上。
这一点单独列为 issue。
8. 分阶段推进建议
阶段 A:Renderer v0 骨架
目标:
- 建立
Rendering模块 - 建立
MeshFilterComponent/MeshRendererComponent - 建立
RenderSurface - 建立
BuiltinForwardPipeline
验收:
- 可以通过 Renderer 正式绘制一个 textured quad 场景
- 输出到 swapchain
- 输出到离屏 RT
阶段 B:真实资源场景接入
目标:
- 接入 mesh / texture / material 资源模块
- 跑通
backpack这样的真实模型场景
验收:
- 真实 obj 资源经资源模块导入后可通过 Renderer 正式绘制
- D3D12 / OpenGL 双后端结果一致
阶段 C:基础光照
目标:
- 接入
LightComponent - 跑通最基础的单方向光前向渲染
验收:
sphere/backpack存在正确基础明暗- 材质参数与法线链路可验证
阶段 D:pipeline 抽象显式化
目标:
- 把内建前向渲染切到
RenderPipeline抽象之下 - 支持 camera 列表驱动
- 为未来 C# SRP 绑定准备原生接口
验收:
- 原生内建 pipeline 通过统一接口驱动
- renderer 不再依赖单一路径写死执行
阶段 E:editor viewport 接入
目标:
SceneView/GameView使用 Renderer 的离屏输出
验收:
- editor 面板只是渲染宿主
- 不额外复制一套渲染逻辑
阶段 F:C# SRP 桥接
目标:
- 在既有 Renderer 模块基础上绑定脚本化 pipeline
验收:
- C# 可以控制 camera 渲染流程
- 原生 Renderer 继续负责底层资源、上下文与执行
9. 测试体系建议
Renderer 模块需要独立测试体系。
9.1 单元测试
建议放在:
tests/Rendering/unit/
测试内容:
- render object 抽取
- material 参数打包
- pipeline key 构建
- GPU cache 命中与失效
- render surface 创建与 resize
9.2 集成测试
建议放在:
tests/Rendering/integration/
建议场景:
textured_quad_scenebackpack_scenelit_sphere_scene
仍然维持当前 RHI 抽象测试的好习惯:
- 一场景一张
GT.ppm - D3D12 / OpenGL 都与同一张 GT 比对
9.3 与 RHI 测试的关系
tests/RHI/ 继续用于验证:
- API 抽象正确性
- 后端行为一致性
- 资源 / 命令 /格式映射等底层问题
tests/Rendering/ 则验证:
- 场景渲染链路
- 组件与资源到渲染结果的闭环
10. 当前已识别的不适配问题
以下问题不适合直接塞进本设计文档正文实现里,而应该独立跟踪:
Scene / Components层还没有MeshFilter / MeshRenderer抽象Editor还没有 viewport 的离屏渲染宿主接入层Material / Shader资产模型还不足以支撑未来 SRP 的 pass/tag 语义
对应 issue:
docs/issues/Renderer模块_Scene层缺少MeshFilter与MeshRenderer抽象.mddocs/issues/Renderer模块_EditorViewport缺少RenderSurface接入层.mddocs/issues/Renderer模块_Material与Shader资产模型暂不满足SRP演进需求.md
11. 结论
当前 Renderer 阶段的正确方向是:
- 在 RHI 之上建立 原生渲染运行时
- 用它先承接基础前向渲染
- 同时提前为未来 C# SRP 留出清晰接口
因此,下一阶段的 Renderer 规划如果按本文执行,是与 Unity 渲染架构方向相容的,而且比“先做一个临时内建 renderer,后面再拆”更稳。
一句话概括:
- 现在做的是 Unity 式渲染体系的原生底座
- 以后在这个底座之上接 C# SRP