Files
XCEngine/docs/api/XCEngine/Components/MeshRendererComponent/MeshRendererComponent.md

8.6 KiB
Raw Blame History

MeshRendererComponent

命名空间: XCEngine::Components

类型: class

头文件: XCEngine/Components/MeshRendererComponent.h

描述: 保存材质槽、资产引用、阴影开关和渲染层等绘制配置,把场景对象与具体 Material 资源绑定起来。

角色概述

MeshRendererComponent 负责回答“这个对象上的 mesh 应该用什么材质、以什么附加状态被渲染”。它不提供几何来源,也不直接发起 draw call当前链路里它主要扮演四个角色

  • 为运行时保存可直接使用的 Material 句柄。
  • 为场景文本保存稳定的材质路径数组。
  • 为项目资产保存 AssetRef,让场景在资源移动或重命名后仍有机会恢复材质绑定。
  • 为脚本层、编辑器和渲染提取层提供统一的材质槽视图。

和它配套工作的主要对象是:

  • MeshFilterComponent:负责 mesh 资源本身。
  • ResourceManager:负责按路径或 AssetRef 加载 Material,也负责延迟异步加载。
  • RenderSceneExtractor:只有 mesh 和材质都可用时,才会把对象整理成可见渲染项。

这种拆分很接近商业引擎里常见的 MeshFilter + MeshRenderer 思路。好处是“几何来源”和“绘制配置”可以分别序列化、分别编辑,也更适合后续做资源热更新、材质复用和编辑器 Inspector。

当前状态模型

当前实现维护了五份与材质槽相关的状态:

状态 类型 作用
m_materials std::vector<ResourceHandle<Material>> 当前已兑现的运行时材质句柄。
m_materialPaths std::vector<std::string> 可序列化、可调试的路径数组。
m_materialRefs std::vector<Resources::AssetRef> 项目资产数据库可解析的稳定引用。
m_pendingMaterialLoads std::vector<std::shared_ptr<...>> 异步加载尚未收口时的挂起结果。
m_asyncMaterialLoadRequested std::vector<bool> 防止同一槽位重复发起异步加载。

这几组数组会尽量保持同一槽位语义对齐。换句话说,slot 3 的 handle、path、AssetRef、pending state 都是在描述“第 3 个材质槽”。

绑定与解析流程

1. 路径驱动的绑定

SetMaterialPath 会:

  • 先确保槽位存在。
  • 清掉该槽位旧的异步挂起状态。
  • 写入新的 m_materialPaths[index]
  • 立即调用 ResourceManager::Load<Material>(path) 做一次同步加载尝试。
  • 再调用 TryGetAssetRef(path, ResourceType::Material, ...) 尝试回填 AssetRef

即使同步加载失败,路径仍会保留;如果这条路径能映射到项目资产,m_materialRefs[index] 也可能仍然有效。

要注意一个很容易误解的点:按当前源码,SetMaterialPath() 本身仍是“立即同步加载”的接口,它不会因为 deferred scene load 模式而自动改成纯记录路径。

2. 句柄驱动的绑定

SetMaterial 会:

  • 保存句柄。
  • 通过 material->GetPath() 反推路径。
  • 再按路径尝试回填 AssetRef

这让“运行时直接塞一个已经加载好的材质”与“场景序列化仍能恢复路径/资产引用”两件事可以同时成立。

3. 反序列化后的恢复策略

Deserialize 现在会同时处理:

  • materialPaths=<path0|path1|...>
  • materialRefs=<guid,localId,resourceType|...>
  • 历史兼容键 materials=<path0|path1|...>

恢复优先级大致是:

  1. 如果 materialRef 有效,优先尝试按 AssetRef 恢复。
  2. 如果当前是 deferred scene load尽量先把 AssetRef 解析回路径,但不立即同步兑现材质。
  3. 如果没有可用 AssetRef,再按路径恢复。

4. 首次访问时的异步兑现

GetMaterialGetMaterialHandle 虽然是 const,但当前实现会通过 const_cast 内部触发:

  1. EnsureDeferredAsyncMaterialLoadStarted(index)
  2. ResolvePendingMaterials()

也就是说,这两个访问器不只是“读缓存”,还会推动路径恢复后的材质兑现流程向前走。

从行为上看,它们的主用途是支持 deferred scene load但按当前源码只要某个槽位“有路径、还没有 material、也还没发起过请求”首次访问就可能触发一次异步加载尝试。

序列化语义

当前序列化会输出五段键值:

materialPaths=<fallbackPath0|fallbackPath1|...>;
materialRefs=<guid,localId,resourceType|...>;
castShadows=1;
receiveShadows=1;
renderLayer=0;

其中最关键的设计点是:

  • materialRefs 是主身份信息,面向项目资产恢复。
  • materialPaths 更像回退字段,只在该槽位没有有效 AssetRef 时才真正写出路径。

例如一个项目资产材质槽,当前文本很可能是:

materialPaths=;
materialRefs=<有效 guid,localId,type>;

这不是数据丢失,而是说明当前序列化更信任 AssetRef 作为稳定身份。

与渲染链路的关系

当前 RenderSceneUtility 会把 MeshRendererComponent* 直接挂进可见渲染对象,再由后续材质提取逻辑读取材质槽。

这意味着:

  • MeshRendererComponent 是否“有槽位”不重要。
  • 重要的是在真正提取材质时,对应槽位能否拿到有效 Material
  • 在 deferred / async 场景里,对象可能已经有 MeshFilterComponent + MeshRendererComponent,但某个时刻材质仍为空,直到异步结果被 ResolvePendingMaterials() 收口。

阴影与渲染层的现实状态

当前组件公开了:

  • GetCastShadows() / SetCastShadows()
  • GetReceiveShadows() / SetReceiveShadows()
  • GetRenderLayer() / SetRenderLayer()

但按当前源码检索,这几个字段目前主要被:

  • 组件测试、场景序列化测试
  • Mono scripting internal call

消费;还没有看到它们被当前已取证的渲染提取路径真正读取。也就是说:

  • 它们当前可以保存。
  • 可以被序列化。
  • 也可以被脚本层读写。
  • 但不能简单等同于“阴影层级和 render layer 已完整进入 renderer 主路径”。

测试与真实使用点

  • tests/Components/test_mesh_render_components.cpp 覆盖了槽位扩容、双轨序列化、历史键兼容、项目材质 AssetRef 恢复,以及 deferred async material load。
  • tests/Scene/test_scene.cpp 覆盖了场景序列化后对 castShadows / receiveShadows / renderLayer 的恢复。
  • engine/src/Scripting/Mono/MonoScriptRuntime.cpp 暴露了阴影和 render layer 的脚本读写入口。

线程与访问语义

  • 当前实现没有内部加锁。
  • 路径写入、序列化和槽位编辑默认按主线程使用。
  • GetMaterial() / GetMaterialHandle() 是带副作用的读访问器;如果调用方只想查看元数据而不触发兑现,应优先使用 GetMaterialPathGetMaterialPathsGetMaterialAssetRefs

当前实现限制

  • 当前只维护“平铺材质槽数组”,不处理 submesh 级独立绑定策略、材质实例化策略或 streaming 策略。
  • GetMaterial() / GetMaterialHandle() 可能在首次访问时触发异步加载,因此它们不是纯只读 getter。
  • materialPaths 在序列化文本里只作为 fallback 字段输出,不应把“文本里 path 为空”误解成“组件内存里没有路径缓存”。
  • castShadowsreceiveShadowsrenderLayer 还没有完整接入当前已取证的 renderer 主路径。

相关方法

相关文档