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

150 lines
26 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 设计目标与总体思路
RHI 的设计目标主要有两个。其一,是在 `D3D12``OpenGL``Vulkan` 三套后端之上建立统一接口,使上层渲染模块不再直接依赖具体图形 API其二是为资源系统、材质系统、渲染主链和后续高级渲染特性的接入提供一致的底层语义屏蔽不同后端的差异。
从设计思路看,当前 RHI 遵循“求同存异、分层抽象、特性降级、底层逃逸”的原则。所谓求同存异,是优先提取不同图形 API 在资源、命令、状态与同步层面的共性能力,并把差异控制在后端内部;所谓分层抽象,是把上层渲染流程与底层 API 实现隔开使渲染管线层只面向统一接口编写所谓特性降级是通过硬件能力查询接口把不同后端与设备的能力差异转化为上层可判断、可选择的运行条件所谓底层逃逸则是保留原生设备或原生句柄访问入口以便在调试、扩展或特殊场景下直接调用底层对象。这样设计后RHI 既不会因为过度抽象而失去实际可用性,也不会因为直接暴露底层细节而破坏整个引擎的模块边界。
### 5.1.2 统一控制模型
当前项目中的 RHI 抽象并不是把不同后端的接口简单套上一层统一名称,而是围绕一套完整的显式控制模型展开。其起点是统一的资源描述方式。无论是缓冲、纹理、交换链,还是着色器、管线状态和资源视图,上层都先通过统一描述结构表达“需要什么样的 GPU 对象”,然后再交由具体后端完成实例化。这样做的意义在于,把资源需求先从具体 API 中抽离出来,使资源系统、材质系统和渲染主链在进入底层之前就能围绕同一种对象语义组织数据,而不是在不同后端之间来回切换思维方式。
在资源被创建出来之后RHI 进一步把资源状态、描述符绑定、管线布局、命令录制与提交、渲染通道与帧缓冲组织成一条连续的控制链。资源状态解决的是“当前资源处于什么用途、下一步将被怎样访问”的问题;描述符绑定与管线布局解决的是“资源如何进入着色器、以什么绑定关系参与绘制或计算”的问题;命令列表与命令队列解决的是“这些状态和资源按什么顺序被提交给 GPU 执行”的问题;渲染通道与帧缓冲则进一步规定“本次执行向哪些目标输出、怎样装载和保存结果”。经过这层整理,场景绘制、阴影贴图、对象 ID 输出、离屏渲染和后处理就不再是零散的 API 调用,而是可以被统一组织和复用的阶段化流程。
这套控制模型本质上更接近 `D3D12``Vulkan` 这类高级图形 API 的设计取向因为它们天然强调资源、状态、绑定、命令和同步的显式管理。与此同时RHI 还通过能力查询把不同硬件和后端的差异转化为上层可判断的运行条件使某些高级特性能够根据实际能力选择启用、降级或关闭。也正因为如此RHI 在当前引擎中承担的并不是单纯的“后端适配”职责,而是为整个渲染系统建立一套统一、可控、可扩展的底层执行模型。
### 5.1.3 显式控制模型的后端落地
从实际实现看,当前 RHI 的整体结构明显以 `D3D12``Vulkan` 这类显式图形 API 为主干。无论是资源描述到对象创建的转换,还是资源状态切换、描述符绑定、管线布局、命令列表录制、命令队列提交以及渲染通道组织,这些抽象都天然更贴近显式 API 的工作方式。换句话说,这套 RHI 并不是先从三个后端求一个平均值,再拼出一层抽象;它首先确立的是一种偏高级 API 的控制模式,再让不同后端向这套模式靠拢。
在这种前提下,`D3D12``Vulkan` 的接入相对直接,因为它们本身就具备较强的显式控制特征。相比之下,`OpenGL` 属于典型的隐式状态机 API本身并不天然提供与描述符集、管线布局、显式同步和阶段化渲染通道完全对应的机制。如果直接按照 OpenGL 的原生使用方式暴露给上层,那么整个 RHI 统一抽象就会迅速失去约束力。为了解决这一问题,当前项目在 OpenGL 后端中做了较多模拟实现,把原本分散在上下文状态中的绑定、同步和资源使用过程重新整理为接近显式 API 的形式。例如,在资源绑定层,通过绑定点映射、纹理单元分配和统一缓冲管理来模拟描述符集与管线布局;在同步层,通过 `GLsync` 配合 CPU 侧计数方式适配统一的 Fence 语义;在渲染输出层,则通过帧缓冲和渲染通道封装,把 OpenGL 的输出过程纳入与其他后端一致的阶段化组织方式。
因此,三套后端虽然共同服务于同一套 RHI 接口,但它们在这套模型中的角色并不完全对称。`D3D12``Vulkan` 更像是这套显式控制模型的直接承载者,`OpenGL` 则是在保留自身底层实现的同时通过模拟与适配被收束到同一框架之中。对上层模块而言这种差异被有效屏蔽资源系统、渲染主链、材质绑定和编辑器视口都可以围绕统一的执行语义工作而对底层实现而言这又允许不同后端保留各自的实现特点。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# 脚本系统进行了分析。可以看到,当前项目已经形成了较完整的运行时基础:底层具备多后端图形抽象能力,中层具备资源导入与缓存、场景组织与渲染请求规划能力,上层具备模型渲染、材质系统、基础光照阴影和脚本扩展能力。这些模块共同构成了当前渲染引擎的主体,也是后续编辑器工作流和体积渲染模块继续展开的工程基础。