docs: sync mesh renderer component docs
This commit is contained in:
@@ -6,64 +6,127 @@
|
||||
|
||||
**头文件**: `XCEngine/Components/MeshRendererComponent.h`
|
||||
|
||||
**描述**: 保存材质槽、阴影开关和渲染层等绘制配置,声明“这个对象上的 Mesh 应该如何被渲染”。
|
||||
**描述**: 保存材质槽、资产引用、阴影开关和渲染层等绘制配置,把场景对象与具体 `Material` 资源绑定起来。
|
||||
|
||||
## 角色概述
|
||||
|
||||
`MeshRendererComponent` 负责的是“绘制配置”,不是“几何来源”。
|
||||
`MeshRendererComponent` 负责回答“这个对象上的 mesh 应该用什么材质、以什么附加状态被渲染”。它不提供几何来源,也不直接发起 draw call;当前链路里它主要扮演四个角色:
|
||||
|
||||
- [MeshFilterComponent](../MeshFilterComponent/MeshFilterComponent.md) 负责网格
|
||||
- `MeshRendererComponent` 负责材质槽和渲染附加参数
|
||||
- 为运行时保存可直接使用的 `Material` 句柄。
|
||||
- 为场景文本保存稳定的材质路径数组。
|
||||
- 为项目资产保存 `AssetRef`,让场景在资源移动或重命名后仍有机会恢复材质绑定。
|
||||
- 为脚本层、编辑器和渲染提取层提供统一的材质槽视图。
|
||||
|
||||
两者配合后,`RenderSceneExtractor` 才能把场景对象整理成可提交到渲染管线的可见项。
|
||||
和它配套工作的主要对象是:
|
||||
|
||||
## 当前实现行为
|
||||
- [MeshFilterComponent](../MeshFilterComponent/MeshFilterComponent.md):负责 mesh 资源本身。
|
||||
- `ResourceManager`:负责按路径或 `AssetRef` 加载 `Material`,也负责延迟异步加载。
|
||||
- [RenderSceneExtractor](../../Rendering/RenderSceneExtractor/RenderSceneExtractor.md):只有 mesh 和材质都可用时,才会把对象整理成可见渲染项。
|
||||
|
||||
### 1. 同时维护材质 handle 和路径数组
|
||||
这种拆分很接近商业引擎里常见的 `MeshFilter + MeshRenderer` 思路。好处是“几何来源”和“绘制配置”可以分别序列化、分别编辑,也更适合后续做资源热更新、材质复用和编辑器 Inspector。
|
||||
|
||||
内部维护两套并行数组:
|
||||
## 当前状态模型
|
||||
|
||||
- `m_materials`
|
||||
- `m_materialPaths`
|
||||
当前实现维护了五份与材质槽相关的状态:
|
||||
|
||||
这和 `MeshFilterComponent` 的思路一致,目的是同时满足:
|
||||
| 状态 | 类型 | 作用 |
|
||||
|------|------|------|
|
||||
| `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 个材质槽”。
|
||||
|
||||
### 2. 材质槽会按需自动扩容
|
||||
## 绑定与解析流程
|
||||
|
||||
`SetMaterialPath()` 和 `SetMaterial()` 在写入指定槽位前,都会先调用内部 `EnsureMaterialSlot(index)`。
|
||||
### 1. 路径驱动的绑定
|
||||
|
||||
因此:
|
||||
[SetMaterialPath](SetMaterialPath.md) 会:
|
||||
|
||||
- 可以直接写入一个较大的槽位索引
|
||||
- 中间缺失槽位会被自动补成空材质
|
||||
- 先确保槽位存在。
|
||||
- 清掉该槽位旧的异步挂起状态。
|
||||
- 写入新的 `m_materialPaths[index]`。
|
||||
- 立即调用 `ResourceManager::Load<Material>(path)` 做一次同步加载尝试。
|
||||
- 再调用 `TryGetAssetRef(path, ResourceType::Material, ...)` 尝试回填 `AssetRef`。
|
||||
|
||||
`tests/Components/test_mesh_render_components.cpp` 已覆盖这一行为。
|
||||
即使同步加载失败,路径仍会保留;如果这条路径能映射到项目资产,`m_materialRefs[index]` 也可能仍然有效。
|
||||
|
||||
### 3. 越界读取是“安全空值”语义
|
||||
要注意一个很容易误解的点:按当前源码,`SetMaterialPath()` 本身仍是“立即同步加载”的接口,它不会因为 deferred scene load 模式而自动改成纯记录路径。
|
||||
|
||||
按当前实现:
|
||||
### 2. 句柄驱动的绑定
|
||||
|
||||
- `GetMaterial(index)` 越界时返回 `nullptr`
|
||||
- `GetMaterialHandle(index)` 越界时返回静态空 handle
|
||||
- `GetMaterialPath(index)` 越界时返回静态空字符串
|
||||
[SetMaterial](SetMaterial.md) 会:
|
||||
|
||||
这让上层调用少了很多显式边界判断,但也意味着调用者不能把空返回值误读成“这个槽位一定存在但内容为空”。
|
||||
- 保存句柄。
|
||||
- 通过 `material->GetPath()` 反推路径。
|
||||
- 再按路径尝试回填 `AssetRef`。
|
||||
|
||||
### 4. 反序列化按路径重建槽位
|
||||
这让“运行时直接塞一个已经加载好的材质”与“场景序列化仍能恢复路径/资产引用”两件事可以同时成立。
|
||||
|
||||
`Deserialize()` 会:
|
||||
### 3. 反序列化后的恢复策略
|
||||
|
||||
- 先清空旧材质与标记
|
||||
- 解析 `materials=` 字段
|
||||
- 用 `|` 分隔多材质路径
|
||||
- 对每个槽位调用 `SetMaterialPath()` 尝试重新加载
|
||||
[Deserialize](Deserialize.md) 现在会同时处理:
|
||||
|
||||
即使资源此刻没有加载成功,路径数组也会被保留下来。这一点同样有测试覆盖。
|
||||
- `materialPaths=<path0|path1|...>`
|
||||
- `materialRefs=<guid,localId,resourceType|...>`
|
||||
- 历史兼容键 `materials=<path0|path1|...>`
|
||||
|
||||
## 阴影和渲染层的现实状态
|
||||
恢复优先级大致是:
|
||||
|
||||
1. 如果 `materialRef` 有效,优先尝试按 `AssetRef` 恢复。
|
||||
2. 如果当前是 deferred scene load,尽量先把 `AssetRef` 解析回路径,但不立即同步兑现材质。
|
||||
3. 如果没有可用 `AssetRef`,再按路径恢复。
|
||||
|
||||
### 4. 首次访问时的异步兑现
|
||||
|
||||
[GetMaterial](GetMaterial.md) 和 [GetMaterialHandle](GetMaterialHandle.md) 虽然是 `const`,但当前实现会通过 `const_cast` 内部触发:
|
||||
|
||||
1. `EnsureDeferredAsyncMaterialLoadStarted(index)`
|
||||
2. `ResolvePendingMaterials()`
|
||||
|
||||
也就是说,这两个访问器不只是“读缓存”,还会推动路径恢复后的材质兑现流程向前走。
|
||||
|
||||
从行为上看,它们的主用途是支持 deferred scene load,但按当前源码,只要某个槽位“有路径、还没有 material、也还没发起过请求”,首次访问就可能触发一次异步加载尝试。
|
||||
|
||||
## 序列化语义
|
||||
|
||||
当前序列化会输出五段键值:
|
||||
|
||||
```text
|
||||
materialPaths=<fallbackPath0|fallbackPath1|...>;
|
||||
materialRefs=<guid,localId,resourceType|...>;
|
||||
castShadows=1;
|
||||
receiveShadows=1;
|
||||
renderLayer=0;
|
||||
```
|
||||
|
||||
其中最关键的设计点是:
|
||||
|
||||
- `materialRefs` 是主身份信息,面向项目资产恢复。
|
||||
- `materialPaths` 更像回退字段,只在该槽位没有有效 `AssetRef` 时才真正写出路径。
|
||||
|
||||
例如一个项目资产材质槽,当前文本很可能是:
|
||||
|
||||
```text
|
||||
materialPaths=;
|
||||
materialRefs=<有效 guid,localId,type>;
|
||||
```
|
||||
|
||||
这不是数据丢失,而是说明当前序列化更信任 `AssetRef` 作为稳定身份。
|
||||
|
||||
## 与渲染链路的关系
|
||||
|
||||
当前 `RenderSceneUtility` 会把 `MeshRendererComponent*` 直接挂进可见渲染对象,再由后续材质提取逻辑读取材质槽。
|
||||
|
||||
这意味着:
|
||||
|
||||
- `MeshRendererComponent` 是否“有槽位”不重要。
|
||||
- 重要的是在真正提取材质时,对应槽位能否拿到有效 `Material`。
|
||||
- 在 deferred / async 场景里,对象可能已经有 `MeshFilterComponent + MeshRendererComponent`,但某个时刻材质仍为空,直到异步结果被 `ResolvePendingMaterials()` 收口。
|
||||
|
||||
## 阴影与渲染层的现实状态
|
||||
|
||||
当前组件公开了:
|
||||
|
||||
@@ -71,35 +134,36 @@
|
||||
- `GetReceiveShadows()` / `SetReceiveShadows()`
|
||||
- `GetRenderLayer()` / `SetRenderLayer()`
|
||||
|
||||
但按当前 [RenderSceneExtractor](../../Rendering/RenderSceneExtractor/RenderSceneExtractor.md) 实现,场景提取时还没有看到这些字段被真正消费。也就是说:
|
||||
但按当前源码检索,这几个字段目前主要被:
|
||||
|
||||
- 这些字段当前可以保存
|
||||
- 也可以被序列化
|
||||
- 但它们还没有完整接入当前已取证的渲染提取路径
|
||||
- 组件测试、场景序列化测试
|
||||
- Mono scripting internal call
|
||||
|
||||
文档必须把这一点说清楚,避免用户把“字段存在”误解成“功能已完整生效”。
|
||||
消费;还没有看到它们被当前已取证的渲染提取路径真正读取。也就是说:
|
||||
|
||||
## 序列化语义
|
||||
- 它们当前可以保存。
|
||||
- 可以被序列化。
|
||||
- 也可以被脚本层读写。
|
||||
- 但不能简单等同于“阴影层级和 render layer 已完整进入 renderer 主路径”。
|
||||
|
||||
当前写出:
|
||||
## 测试与真实使用点
|
||||
|
||||
- `materials`,多个路径用 `|` 分隔
|
||||
- `castShadows`
|
||||
- `receiveShadows`
|
||||
- `renderLayer`
|
||||
- `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 的脚本读写入口。
|
||||
|
||||
测试还覆盖了“末尾空材质槽”会被保留的情况,这对编辑器材质槽 UI 很重要。
|
||||
|
||||
## 线程语义
|
||||
## 线程与访问语义
|
||||
|
||||
- 当前实现没有内部加锁。
|
||||
- 资源路径解析和材质切换默认按主线程配置路径使用。
|
||||
- 路径写入、序列化和槽位编辑默认按主线程使用。
|
||||
- `GetMaterial()` / `GetMaterialHandle()` 是带副作用的读访问器;如果调用方只想查看元数据而不触发兑现,应优先使用 [GetMaterialPath](GetMaterialPath.md)、[GetMaterialPaths](GetMaterialPaths.md) 或 [GetMaterialAssetRefs](GetMaterialAssetRefs.md)。
|
||||
|
||||
## 当前实现限制
|
||||
|
||||
- 当前文档目录原先缺少 `GetMaterialPath()` 和 `SetMaterialPath()` 独立方法页,本轮已补齐。
|
||||
- `castShadows`、`receiveShadows` 和 `renderLayer` 还没有完整接入当前已取证的场景提取逻辑。
|
||||
- 它只负责声明绘制配置,不等于完整 renderer feature 集合。
|
||||
- 当前只维护“平铺材质槽数组”,不处理 submesh 级独立绑定策略、材质实例化策略或 streaming 策略。
|
||||
- `GetMaterial()` / `GetMaterialHandle()` 可能在首次访问时触发异步加载,因此它们不是纯只读 getter。
|
||||
- `materialPaths` 在序列化文本里只作为 fallback 字段输出,不应把“文本里 path 为空”误解成“组件内存里没有路径缓存”。
|
||||
- `castShadows`、`receiveShadows` 和 `renderLayer` 还没有完整接入当前已取证的 renderer 主路径。
|
||||
|
||||
## 相关方法
|
||||
|
||||
@@ -108,6 +172,7 @@
|
||||
- [GetMaterialHandle](GetMaterialHandle.md)
|
||||
- [GetMaterialPath](GetMaterialPath.md)
|
||||
- [GetMaterialPaths](GetMaterialPaths.md)
|
||||
- [GetMaterialAssetRefs](GetMaterialAssetRefs.md)
|
||||
- [SetMaterial](SetMaterial.md)
|
||||
- [SetMaterialPath](SetMaterialPath.md)
|
||||
- [SetMaterials](SetMaterials.md)
|
||||
|
||||
Reference in New Issue
Block a user