Files
XCEngine/docs/plan/毕设/初稿/第五章.md

144 lines
23 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 第五章 渲染引擎核心模块设计与实现
上一章已经对渲染引擎的总体分层、模块划分和数据流关系进行了说明。本章进一步下沉到运行时主体,围绕当前项目中已经形成的几个核心模块展开分析,重点说明这些模块在工程实现中的职责边界、关键数据结构和协同方式。结合现有代码实现,本章主要讨论 RHI 抽象层、资源系统、场景与组件系统、渲染主链、模型材质与着色器系统、多光源与简单阴影,以及 C# 脚本系统。它们共同构成了当前渲染引擎的主体能力,也是后续编辑器工作流和体积渲染模块接入的基础。
## 5.1 RHI 抽象层设计与实现
### 5.1.1 抽象目标与接口边界
渲染引擎的底层必须直接面对图形后端差异。不同图形 API 在资源创建方式、命令提交模型、描述符组织形式以及状态切换机制上均存在明显区别。如果上层渲染模块直接依赖某一个后端实现,那么渲染主链、资源绑定和管线状态组织都将与具体平台高度耦合,不利于后续扩展和维护。因此,本项目在底层建立了 RHIRendering 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# 脚本系统进行了分析。可以看到,当前项目已经形成了较完整的运行时基础:底层具备多后端图形抽象能力,中层具备资源导入与缓存、场景组织与渲染请求规划能力,上层具备模型渲染、材质系统、基础光照阴影和脚本扩展能力。这些模块共同构成了当前渲染引擎的主体,也是后续编辑器工作流和体积渲染模块继续展开的工程基础。