144 lines
23 KiB
Markdown
144 lines
23 KiB
Markdown
# 第五章 渲染引擎核心模块设计与实现
|
||
|
||
上一章已经对渲染引擎的总体分层、模块划分和数据流关系进行了说明。本章进一步下沉到运行时主体,围绕当前项目中已经形成的几个核心模块展开分析,重点说明这些模块在工程实现中的职责边界、关键数据结构和协同方式。结合现有代码实现,本章主要讨论 RHI 抽象层、资源系统、场景与组件系统、渲染主链、模型材质与着色器系统、多光源与简单阴影,以及 C# 脚本系统。它们共同构成了当前渲染引擎的主体能力,也是后续编辑器工作流和体积渲染模块接入的基础。
|
||
|
||
## 5.1 RHI 抽象层设计与实现
|
||
|
||
### 5.1.1 抽象目标与接口边界
|
||
|
||
渲染引擎的底层必须直接面对图形后端差异。不同图形 API 在资源创建方式、命令提交模型、描述符组织形式以及状态切换机制上均存在明显区别。如果上层渲染模块直接依赖某一个后端实现,那么渲染主链、资源绑定和管线状态组织都将与具体平台高度耦合,不利于后续扩展和维护。因此,本项目在底层建立了 RHI(Rendering Hardware Interface)抽象层,将图形设备能力统一为一组稳定接口,使上层模块更多围绕“渲染什么”和“如何组织渲染阶段”来展开,而不是反复处理后端 API 细节。
|
||
|
||
从当前实现看,`RHIDevice` 是这一抽象层的核心入口。它统一提供缓冲、纹理、交换链、命令列表、命令队列、着色器、管线状态、管线布局、同步栅栏、采样器、渲染通道、帧缓冲、描述符池、描述符集以及各类资源视图的创建接口。这样一来,渲染层在组织离屏纹理、深度表面、阴影图、材质资源绑定和绘制命令时,都可以基于统一对象模型展开。
|
||
|
||
### 5.1.2 多后端统一封装方式
|
||
|
||
当前项目的 RHI 已经形成了多后端组织结构。在工程构建层面,`engine/CMakeLists.txt` 中已经纳入了 `D3D12`、`OpenGL` 和 `Vulkan` 三套后端实现,以及与之对应的缓冲、纹理、资源视图、交换链、命令队列、命令列表、描述符、管线状态和截图支持等对象。对应地,`RHIFactory` 负责根据 `RHIType` 或字符串名称创建目标后端设备,从而将设备实例化过程与上层运行逻辑解耦。
|
||
|
||
这种设计并不是简单追求“支持多个 API”,更重要的是建立统一的资源语义。例如,上层不再分别讨论 D3D12 的描述符堆、OpenGL 的纹理单元或 Vulkan 的描述符集布局,而是通过统一的缓冲、纹理、采样器、描述符集和资源视图概念组织资源绑定;同样,上层也不直接处理各后端的原生命令对象,而是通过命令队列和命令列表接口完成绘制、状态切换与结果提交。当前项目的体积渲染研究阶段以 D3D12 为重点推进,但从引擎主体架构看,多后端 RHI 已经为引擎保留了较好的平台弹性。
|
||
|
||
### 5.1.3 RHI 对上层模块的支撑作用
|
||
|
||
RHI 抽象层在整个引擎中承担的是“运行时图形基础设施”的角色。资源系统最终生成的网格、纹理、材质常量和着色器变体,都需要落到 RHI 对象上;渲染主链中的主场景绘制、阴影绘制、对象 ID 绘制、后处理和最终输出,也都依赖 RHI 提供的离屏表面、命令提交和资源状态切换能力;编辑器视口同样是通过对渲染表面的申请和复用接入渲染主链的。可以说,RHI 为整个渲染引擎提供了统一而稳定的底层执行面,是后续所有高层模块成立的前提。
|
||
|
||
## 5.2 资源系统设计与实现
|
||
|
||
### 5.2.1 项目资源组织方式
|
||
|
||
一个可持续使用的渲染引擎不能只依赖运行时直接读取源文件,而必须建立源资源、导入信息和运行时产物之间的清晰关系。当前项目采用了 `Assets + .meta + Library` 的组织方式。`Assets` 目录保存模型、纹理、材质、着色器、场景等源文件;`.meta` 文件为每个资源记录稳定的 GUID 和导入相关信息;`Library` 目录则缓存导入后的 artifact,供运行时和编辑器直接使用。
|
||
|
||
`AssetDatabase` 是这一组织方式的核心实现。它内部区分 `SourceAssetRecord` 和 `ArtifactRecord` 两类记录:前者描述源资源路径、GUID、导入器名称、版本号、源文件哈希、`.meta` 哈希等信息,后者描述 artifactKey、主产物路径、资源类型、依赖项和主资源 localID 等内容。通过这种方式,资源的“工程身份”和“运行时形态”被明确分离,避免了项目规模扩大后路径变化、重复导入和资源引用失效等问题。
|
||
|
||
### 5.2.2 资源导入与 artifact 缓存
|
||
|
||
当前资源系统并不是简单地把文件复制到运行时目录,而是通过导入器将不同类型资源转换为适合引擎使用的中间产物。`AssetDatabase` 中已经实现了贴图、材质、模型、着色器、UI 文档等导入入口,并为每个 artifact 记录来源文件、依赖文件、文件尺寸和写入时间等信息。这样在执行 `Refresh`、`EnsureArtifact`、`ReimportAsset` 或 `ReimportAllAssets` 时,系统就可以根据源文件哈希、`.meta` 哈希、导入器版本以及依赖是否变化来判断是否需要重新导入。
|
||
|
||
在这一基础上,`AssetImportService` 对外进一步封装了项目资源维护流程。它负责初始化项目根目录、引导工程资源、刷新数据库、清理或重建缓存,并维护一份最近导入状态快照,包括当前操作、目标路径、成功与否、耗时以及本次导入和清理的资源数量。这样的封装一方面降低了上层模块直接操作 `AssetDatabase` 的复杂度,另一方面也为编辑器中的资源刷新、重导入和状态显示提供了统一接口。
|
||
|
||
### 5.2.3 运行时资源加载与缓存管理
|
||
|
||
在 artifact 已经准备完成后,运行时仍然需要一个统一入口将资源装载为引擎对象。当前项目中的 `ResourceManager` 承担了这一职责。它内部维护资源缓存、引用计数、资源路径映射、资源加载器注册表、异步加载器以及项目资源索引,并对外提供同步加载、异步加载、资源卸载、未使用资源回收和缓存刷新等能力。
|
||
|
||
值得注意的是,资源系统并没有把“项目资源定位”和“运行时资源加载”混在一起处理。前者主要由 `AssetImportService` 和项目索引负责,后者则由 `ResourceManager` 与具体 loader 负责执行。组件层中保存的通常是资源句柄、资源路径和 `AssetRef`,真正进入渲染阶段时,再由资源管理器解析为 `Mesh`、`Material`、`Shader`、`Texture` 等运行时对象。这样的分层使项目目录结构、资源导入规则和运行时访问机制能够各自独立演进,提高了工程实现的清晰度。
|
||
|
||
## 5.3 场景与组件系统设计与实现
|
||
|
||
### 5.3.1 Scene 与 GameObject 的层级组织
|
||
|
||
渲染引擎需要一种稳定的数据组织方式来承载场景内容。当前项目采用 `Scene - GameObject - Component` 的组织结构。其中,`Scene` 作为场景容器,负责对象创建与销毁、根节点管理、按名称或 ID 查找对象、场景序列化与反序列化,以及 `Update`、`FixedUpdate`、`LateUpdate` 等调度入口;同时,`Scene` 还提供对象创建和销毁事件,这一机制后续被脚本系统用来跟踪运行时脚本实例。
|
||
|
||
`GameObject` 则作为场景中的基本实体,维护对象 ID、UUID、名称、标签、层级、激活状态以及父子关系。每个对象默认持有 `TransformComponent`,从而形成统一的空间变换基础。通过 `SetParent`、`DetachFromParent`、`IsActiveInHierarchy` 等接口,项目已经能够表达典型的场景树结构。这样一来,无论是渲染系统进行层级遍历,还是编辑器显示 Hierarchy 结构,或者脚本逻辑查找父子对象,都可以建立在同一套对象模型之上。
|
||
|
||
### 5.3.2 基于组件的能力拼装方式
|
||
|
||
与把所有能力都堆叠到单一对象类中的做法相比,组件系统更适合渲染引擎这类需要持续扩展的工程。当前项目中的 `Component` 基类定义了 `Awake`、`Start`、`Update`、`FixedUpdate`、`LateUpdate`、`OnEnable`、`OnDisable` 和 `OnDestroy` 等基本生命周期接口,并提供启用状态控制。`GameObject` 则负责组件的添加、查找、删除以及层级中的递归查询。这样,对象本身只承担容器职责,而具体能力则由独立组件实现。
|
||
|
||
这种设计的直接收益体现在模块边界上。渲染系统只需要关注相机、光源、网格和材质相关组件;脚本系统只需要关注脚本组件和生命周期调度;编辑器则能够围绕组件列表构建属性面板。由于不同能力不再被硬编码到同一个对象类型中,引擎在扩展新组件时不会破坏既有对象模型,系统可维护性也更强。
|
||
|
||
### 5.3.3 关键渲染相关组件
|
||
|
||
当前项目中已经形成了较为完整的渲染相关组件集合。`CameraComponent` 用于描述观察参数和输出策略,包含透视与正交投影模式、视口矩形、近远裁剪面、清屏模式、相机深度、主相机标记、相机栈类型、裁剪掩码、天空盒材质以及后处理与最终颜色覆盖设置等内容。由此可见,相机在当前引擎中已经不只是一个“观察点”,而是一个完整的渲染请求配置载体。
|
||
|
||
`LightComponent` 当前支持方向光、点光和聚光三种类型,并提供颜色、强度、范围、聚光角和是否投射阴影等参数;`MeshFilterComponent` 负责维护网格资源句柄及其异步装载状态;`MeshRendererComponent` 则负责材质槽、材质路径、阴影投射与接收标记以及渲染层配置。通过 `MeshFilter + MeshRenderer` 的拆分,网格数据与绘制表现被明确分离,便于后续扩展更多绘制策略。除此之外,`ScriptComponent` 作为行为扩展入口,也已经被纳入组件体系之中,为脚本驱动场景对象提供了承载位置。
|
||
|
||
## 5.4 渲染主链设计与实现
|
||
|
||
### 5.4.1 从场景数据到渲染数据的提取
|
||
|
||
当前项目中的渲染主链并不是直接对场景树进行即时绘制,而是先将场景信息提取为更适合渲染阶段消费的中间数据。`RenderSceneExtractor` 在这一过程中承担核心职责。它首先根据当前场景和可用相机选择目标相机,构建 `RenderCameraData`,随后从场景根节点开始递归遍历对象,将满足激活状态与裁剪掩码条件的可见对象整理为 `VisibleRenderItem` 集合,并在提取完成后执行稳定排序。
|
||
|
||
除可见对象外,提取器还会同步整理场景光照信息。当前实现中,系统会先选出主方向光,再从其余光源中筛选附加光源并写入 `RenderLightingData`。这样,进入绘制阶段时,渲染系统面对的已经不是原始场景树,而是一份按当前相机视角整理好的渲染数据快照,从而避免在具体绘制过程中频繁穿透业务对象结构。
|
||
|
||
### 5.4.2 渲染请求规划与阶段拆分
|
||
|
||
在提取出场景数据之后,项目并没有立即绘制,而是进一步由 `SceneRenderRequestPlanner` 生成相机级别的渲染请求。该规划器首先收集可用相机,在处理 overrideCamera、主相机和叠加相机关系后,为每个相机构造一个 `CameraRenderRequest`。这一请求对象不仅保存目标场景、相机、上下文和输出表面,还显式拆分出 `DepthOnly`、`ShadowCaster`、`MainScene`、`PostProcess`、`FinalOutput`、`ObjectId`、`OverlayPasses` 等多个阶段,使一帧的执行结构从一开始就是清晰可分的。
|
||
|
||
这种“先规划、后执行”的方式是当前渲染主链的重要特征。它带来的一个直接优势是,各种离屏渲染需求都可以在请求阶段明确下来,而不必散落在绘制代码中临时拼接。无论是阴影贴图、对象 ID、后处理链,还是编辑器叠加层,都能统一纳入相机请求结构中管理。对于后续继续接入体积渲染等新阶段而言,这种阶段化请求模型也更容易扩展。
|
||
|
||
### 5.4.3 SceneRenderer、CameraRenderer 与当前管线
|
||
|
||
在执行层面,`SceneRenderer` 负责面向整个场景组织渲染流程,它能够根据场景、相机和目标表面构建请求列表,并为后处理和最终输出阶段准备必要的中间表面。之后,单个请求交由 `CameraRenderer` 继续处理。`CameraRenderer` 内部负责解析阴影绘制请求、调用 `RenderSceneExtractor` 生成本次绘制所需的 `RenderSceneData`,再将这些数据与目标表面一起提交给具体渲染管线执行。
|
||
|
||
当前主场景渲染采用 `RenderPipeline` 接口与 `BuiltinForwardPipeline` 实现相结合的方式。`RenderPipeline` 只规定初始化、销毁和渲染三个核心动作,而 `BuiltinForwardPipeline` 则给出了当前项目的具体实现形式。从代码结构看,这一前向渲染管线已经被进一步划分为不透明、天空盒和透明三个 pass,并能配合对象 ID、阴影和后处理阶段完成完整的一帧输出。这说明当前项目的渲染主链已经脱离了早期单通道绘制模式,形成了更接近实际引擎运行时的阶段化执行框架。
|
||
|
||
## 5.5 模型、材质与着色器系统
|
||
|
||
### 5.5.1 模型资源与网格数据组织
|
||
|
||
模型渲染能力是当前引擎主体功能中的重要组成部分。当前项目已经能够完成 OBJ 模型的导入和渲染,而运行时模型数据主要由 `Mesh` 资源承载。`Mesh` 内部保存顶点数据、索引数据、顶点属性标记、分段信息、包围盒以及与材质和纹理的关联关系。其中,`MeshSection` 进一步描述了每一段网格的顶点范围、索引范围和材质编号,这为多材质模型的绘制组织提供了基础。
|
||
|
||
这种网格表示方式并不依赖某一种具体模型格式,而是将导入结果统一整理为引擎内部可消费的数据结构。模型导入完成后,渲染阶段只需要根据 `MeshFilterComponent` 提供的网格资源句柄读取网格数据,再结合对应 `MeshRendererComponent` 的材质槽信息完成绘制。与此同时,网格包围盒还会参与阴影规划和可见对象组织,因此模型资源不仅服务于绘制本身,也服务于渲染流程中的其他阶段。
|
||
|
||
### 5.5.2 材质系统的参数化表达
|
||
|
||
材质系统承担的是“如何绘制对象”的职责。当前项目中的 `Material` 已经不仅仅保存一个着色器引用,而是同时维护渲染队列、渲染状态覆盖、标签、关键字集合、常量缓冲布局、纹理绑定和缓冲绑定等内容。材质可以按名称设置浮点、向量、整型、布尔、纹理和缓冲参数,并通过 `UpdateConstantBuffer` 将这些高层参数同步到运行时常量数据中。对于频繁变化的材质实例而言,`changeVersion` 还可以作为缓存失效和描述符重建的依据。
|
||
|
||
这种设计使材质在引擎中成为了连接“美术参数”和“渲染状态”的中间层。同一个着色器可以对应多份材质实例,不同实例可以在颜色、贴图、关键字、透明度、剔除方式和混合状态上表现不同,从而显著提高资源复用能力。由于材质本身也被纳入资源系统管理,因此它可以像模型和纹理一样参与导入、缓存、序列化和异步加载流程。
|
||
|
||
### 5.5.3 着色器通道与后端变体组织
|
||
|
||
着色器系统在当前项目中被设计为较强的数据驱动形式。`Shader` 资源内部包含属性描述、pass 列表、资源绑定描述、关键字声明以及不同阶段和不同后端的变体数据。每个 `ShaderPass` 可以携带固定功能状态、pass tag、资源绑定规则和关键字声明,而 `ShaderStageVariant` 则记录着色器阶段、语言类型、后端类型、入口点、profile 以及源码或编译后的二进制数据。通过这种方式,材质属性、资源绑定和后端着色器变体不再是松散分离的,而是围绕同一份着色器资源组织起来。
|
||
|
||
在当前渲染主链中,`BuiltinForwardPipeline` 会根据材质的 render state、着色器、pass 名称和关键字签名解析或缓存管线状态对象,并按渲染队列区分不透明与透明对象绘制。与此同时,引擎还内置了 forward lit、unlit 和 skybox 等基础着色器资源。这种组织方式说明当前项目已经形成了“网格描述几何、材质描述参数与状态、着色器描述通道与资源布局”的清晰分工,能够较稳定地支撑当前模型渲染和后续功能扩展。
|
||
|
||
## 5.6 多光源与简单阴影实现
|
||
|
||
### 5.6.1 多光源数据组织
|
||
|
||
为了满足基本场景表现需求,当前引擎已经实现了多光源照明。`RenderSceneExtractor` 在提取光照数据时,会先根据场景和裁剪掩码选出主方向光,并将其写入 `RenderLightingData::mainDirectionalLight`。对于其余光源,系统会根据光源类型、影响程度和对象顺序进行排序,再从中选出最多 8 个附加光源写入 `additionalLights` 数组。当前附加光源支持方向光、点光和聚光三种类型,其中点光和聚光会额外携带位置、范围和聚光角等参数。
|
||
|
||
这种主光源加附加光源的组织方式比较符合当前项目的工程阶段。一方面,主方向光能够稳定承担场景中的整体照明和阴影来源;另一方面,附加光源数量被限制在一个可控范围内,便于前向渲染管线在常量数据和着色器循环中保持相对稳定的开销。对于偏工程设计类课题而言,这种方案在效果与复杂度之间取得了较合适的平衡。
|
||
|
||
### 5.6.2 简单方向光阴影实现
|
||
|
||
当前阴影系统以主方向光阴影为主,并未扩展到点光阴影、聚光阴影或级联阴影等更复杂形式。其核心流程由 `SceneRenderRequestPlanner` 完成规划,再由 `CameraRenderer` 与渲染主链执行。具体来说,规划器会在构建相机请求时检查当前相机是否需要规划方向光阴影,如果场景中存在可投影的主方向光,则根据相机视锥范围、观察方向和阴影投射对象的包围盒,计算出一个聚焦于当前相机可见区域的正交阴影相机,生成 `DirectionalShadowRenderPlan`,并同步填充 `ShadowCaster` 阶段的绘制请求。
|
||
|
||
在执行时,阴影阶段首先将投射阴影的对象绘制到深度表面,再在主场景绘制阶段以阴影贴图形式参与采样。`MeshRendererComponent` 中的 `castShadows` 和 `receiveShadows` 开关决定了对象是否参与阴影投射和接收。当前实现虽然属于“简单阴影”,但已经完成了从阴影请求规划、阴影图绘制到主场景采样使用的完整闭环,能够支持当前场景验证需求。
|
||
|
||
### 5.6.3 当前效果验证情况
|
||
|
||
从测试工程可以看出,当前引擎已经围绕典型渲染能力建立了一组集成测试场景。其中,`multi_light_scene` 和 `spot_light_scene` 用于验证多光源和聚光照明效果,`directional_shadow_scene` 用于验证当前方向光阴影流程,`transparent_material_scene` 用于验证透明材质绘制顺序,`backpack_scene` 与 `backpack_lit_scene` 则用于验证模型导入、材质绑定和基础光照结果。这些测试说明当前多光源与简单阴影并不是停留在接口层,而是已经能够进入实际场景输出环节。
|
||
|
||
## 5.7 C# 脚本系统设计与实现
|
||
|
||
### 5.7.1 托管运行时总体结构
|
||
|
||
在当前项目中,脚本系统已经成为渲染引擎主体的一部分,而不是附着在外部的独立工具。整个系统由 `ScriptEngine`、`IScriptRuntime`、`ScriptComponent` 和 `managed` 目录中的托管程序集共同构成。`ScriptEngine` 负责从引擎角度管理运行状态和生命周期调度,`IScriptRuntime` 用于抽象具体脚本运行时,当前实际实现为基于 Mono 的 `MonoScriptRuntime`。在托管侧,`managed/CMakeLists.txt` 会构建 `XCEngine.ScriptCore.dll` 和 `GameScripts.dll`,并将程序集与 `mscorlib.dll` 复制到输出目录以及项目的 `Library/ScriptAssemblies` 中,为运行时加载提供基础。
|
||
|
||
这一设计的关键点在于:原生引擎和托管脚本并不是彼此孤立的。原生侧维护场景对象、组件和渲染状态,托管侧则通过 `ScriptCore` 暴露出的包装类型访问这些对象。这样既保留了引擎主体在 C++ 侧的执行效率,也使场景行为逻辑具备了更灵活的扩展方式。
|
||
|
||
### 5.7.2 脚本组件与生命周期调度
|
||
|
||
`ScriptComponent` 是场景对象接入脚本系统的直接入口。它保存脚本程序集名、命名空间、类名、脚本组件 UUID 以及字段存储信息,从而能够稳定标识一个脚本实例。运行时启动后,`ScriptEngine` 会记录当前运行场景,订阅场景对象创建事件,收集场景中的脚本组件,并以“对象 UUID + 脚本组件 UUID”作为键建立脚本实例状态。之后,引擎会按照 `Awake`、`OnEnable`、`Start`、`Update`、`FixedUpdate`、`LateUpdate` 的顺序驱动脚本生命周期;在脚本禁用、销毁或运行时停止时,再执行 `OnDisable` 和 `OnDestroy` 等清理流程。
|
||
|
||
从当前实现可以看到,脚本系统已经不只是简单地“能调一下 Update”。`ScriptEngine` 对脚本可运行状态、实例创建状态、Awake 是否调用、Start 是否待执行、是否处于启用状态等信息都做了显式跟踪,并能够在脚本类变化时重建对应状态。这使托管脚本与场景生命周期之间建立了相对完整的同步关系,也为后续编辑器字段检查、运行时重载和调试支持打下了基础。
|
||
|
||
### 5.7.3 Mono 桥接与托管 API 暴露
|
||
|
||
`MonoScriptRuntime` 负责原生引擎与托管程序集之间的真正桥接。它在初始化时加载核心程序集和应用程序集,发现继承自 `MonoBehaviour` 的脚本类,缓存生命周期方法和字段元数据,并在需要时创建、销毁和调用托管对象。当前实现还支持字段读写、字段默认值提取、`SerializeField` 标记识别,以及 `GameObject`、组件引用等对象的托管侧表示,这意味着脚本实例不只是一个黑盒,而是已经能够与原生对象系统双向同步数据。
|
||
|
||
与此同时,`XCEngine.ScriptCore` 已经提供了 `GameObject`、`Transform`、`Input`、`Camera`、`Light`、`MeshFilter`、`MeshRenderer`、`Debug`、`Time`、`Vector3`、`Quaternion` 等基础托管 API。这使场景脚本能够直接操纵对象变换、读取输入、访问常用组件并控制运行行为。对于当前渲染引擎而言,C# 脚本系统的意义并不只是增加一种开发语言,而是使场景层逻辑与渲染引擎主体之间形成了更加完整的工程闭环。
|
||
|
||
## 5.8 本章小结
|
||
|
||
本章围绕当前项目已经完成的渲染引擎主体实现,对 RHI 抽象层、资源系统、场景与组件系统、渲染主链、模型材质与着色器系统、多光源与简单阴影,以及 C# 脚本系统进行了分析。可以看到,当前项目已经形成了较完整的运行时基础:底层具备多后端图形抽象能力,中层具备资源导入与缓存、场景组织与渲染请求规划能力,上层具备模型渲染、材质系统、基础光照阴影和脚本扩展能力。这些模块共同构成了当前渲染引擎的主体,也是后续编辑器工作流和体积渲染模块继续展开的工程基础。
|