docs: sync mesh component assetref docs

This commit is contained in:
2026-04-03 16:02:03 +08:00
parent 7ab49fdbf3
commit ee44e14960
14 changed files with 550 additions and 236 deletions

View File

@@ -6,164 +6,156 @@
**头文件**: `XCEngine/Components/MeshRendererComponent.h`
**描述**: 保存材质槽、资产引用、阴影开关和渲染层等绘制配置,把场景对象与具体 `Material` 资源绑定起来
**描述**: 保存材质槽、项目资产 `AssetRef`、运行时材质句柄以及少量渲染附加状态,把对象的“怎么画”这一侧数据收口到一个组件里
## 角色概述
`MeshRendererComponent` 负责回答“这个对象上的 mesh 应该用什么材质、以什么附加状态被渲染”。它不提供几何来源,也不直接发起 draw call当前链路里它主要扮演四个角色
`MeshRendererComponent` 回答的问题是
- 为运行时保存可直接使用的 `Material` 句柄。
- 为场景文本保存稳定的材质路径数组。
- 为项目资产保存 `AssetRef`,让场景在资源移动或重命名后仍有机会恢复材质绑定。
- 为脚本层、编辑器和渲染提取层提供统一的材质槽视图。
- 这个对象的 mesh 该用哪些材质、带哪些附加渲染状态去画?
和它配套工作的主要对象是
当前它的职责可以分成四层
- [MeshFilterComponent](../MeshFilterComponent/MeshFilterComponent.md):负责 mesh 资源本身。
- `ResourceManager`:负责按路径或 `AssetRef` 加载 `Material`,也负责延迟异步加载。
- [RenderSceneExtractor](../../Rendering/RenderSceneExtractor/RenderSceneExtractor.md):只有 mesh 和材质都可用时,才会把对象整理成可见渲染项。
- 保存运行时 `Material` 句柄数组
- 保存项目材质的稳定 `AssetRef`
- 仅对 `builtin://` 这类 virtual path 保留 `materialPaths`
- 保存 `castShadows / receiveShadows / renderLayer`
这种拆分很接近商业引擎里常见的 `MeshFilter + MeshRenderer` 思路。好处是“几何来源”和“绘制配置”可以分别序列化、分别编辑,也更适合后续做资源热更新、材质复用和编辑器 Inspector。
和 [MeshFilterComponent](../MeshFilterComponent/MeshFilterComponent.md) 一样,当前最需要纠正的文档心智是:
- 对项目资产材质,正式序列化协议已经是 `materialRefs`
- `materialPaths` 主要保留给 virtual path而不是给普通项目路径长期兜底
## 当前状态模型
当前实现维护了五份与材质槽相关的状态:
当前实现维护 5 组与材质槽相关的状态:
| 状态 | 类型 | 作用 |
|------|------|------|
| `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>` | 防止同一槽位重复发起异步加载。 |
| `m_materialPaths` | `std::vector<std::string>` | 槽位路径缓存;项目资产通常会是 `Assets/...`virtual 资源可能是 `builtin://...`。 |
| `m_materialRefs` | `std::vector<Resources::AssetRef>` | 项目资产稳定身份。 |
| `m_pendingMaterialLoads` | `std::vector<std::shared_ptr<...>>` | deferred async material load 的挂起结果。 |
| `m_asyncMaterialLoadRequested` | `std::vector<bool>` | 防止重复发起异步加载。 |
这几组数组会尽量保持同一槽位语义对齐。换句话说,`slot 3`handle、path、`AssetRef`pending state 都在描述“第 3 个材质槽
所有数组都按槽位对齐;`slot 2`path、ref、handle 和 pending state 都在描述同一材质槽。
## 绑定与解析流程
## 绑定与恢复流程
### 1. 路径驱动的绑定
### 1. 运行时按路径设置
[SetMaterialPath](SetMaterialPath.md)
[SetMaterialPath](SetMaterialPath.md) 当前仍然是同步接口
- 先确保槽位存在。
- 清掉该槽位旧的异步挂起状态
- 写入新的 `m_materialPaths[index]`
- 立即调用 `ResourceManager::Load<Material>(path)` 做一次同步加载尝试。
- 再调用 `TryGetAssetRef(path, ResourceType::Material, ...)` 尝试回填 `AssetRef`
1. 自动扩容到目标槽位
2. 清掉旧 pending async load 状态
3. 写入 `m_materialPaths[index]`
4. 若路径为空,则清掉 handle 和 `AssetRef`
5. 若路径非空,则同步 `Load<Material>(path)`
6. 再尝试按路径回填 `m_materialRefs[index]`
即使同步加载失败,路径仍会保留;如果这条路径能映射到项目资产,`m_materialRefs[index]` 也可能仍然有效
因此对项目路径 `Assets/runtime.material` 来说,运行时是允许直接按路径设置的;但场景文本正式协议已经不是“只存路径”
要注意一个很容易误解的点:按当前源码,`SetMaterialPath()` 本身仍是“立即同步加载”的接口,它不会因为 deferred scene load 模式而自动改成纯记录路径。
### 2. 运行时按句柄设置
### 2. 句柄驱动的绑定
[SetMaterial](SetMaterial.md) / [SetMaterials](SetMaterials.md) 会从句柄反推出路径,再按路径尝试回填 `AssetRef`
[SetMaterial](SetMaterial.md) 会:
这让“运行时直接塞句柄”和“后续还能稳定序列化回项目身份”保持一致。
- 保存句柄。
- 通过 `material->GetPath()` 反推路径。
- 再按路径尝试回填 `AssetRef`
### 3. 反序列化恢复
这让“运行时直接塞一个已经加载好的材质”与“场景序列化仍能恢复路径/资产引用”两件事可以同时成立。
### 3. 反序列化后的恢复策略
[Deserialize](Deserialize.md) 现在会同时处理:
[Deserialize](Deserialize.md) 当前只识别:
- `materialPaths=<path0|path1|...>`
- `materialRefs=<guid,localId,resourceType|...>`
- 历史兼容键 `materials=<path0|path1|...>`
- `castShadows`
- `receiveShadows`
- `renderLayer`
恢复优先级大致是:
恢复顺序是:
1. 如果 `materialRef` 有效,优先尝试`AssetRef` 恢复
2. 如果当前是 deferred scene load尽量先把 `AssetRef` 解析回路径,但不立即同步兑现材质。
3. 如果没有可用 `AssetRef`,再按路径恢复。
1. 某槽位若有有效 `materialRef`,优先按 `AssetRef` 恢复
2. 若处于 deferred scene load先尝试 `TryResolveAssetPath(materialRef, path)` 恢复 `Assets/...`
3. 若不在 deferred 模式,则直接按 `AssetRef` 加载材质
4. 若没有有效 `AssetRef`,只接受带 virtual scheme 的 `materialPaths`
5. 没有 `AssetRef` 的普通项目路径会被清空
### 4. 首次访问时的异步兑现
也就是说,下面这种旧式数据:
[GetMaterial](GetMaterial.md) 和 [GetMaterialHandle](GetMaterialHandle.md) 虽然是 `const`,但当前实现会通过 `const_cast` 内部触发:
```text
materialPaths=Assets/runtime.material;materialRefs=;
```
1. `EnsureDeferredAsyncMaterialLoadStarted(index)`
2. `ResolvePendingMaterials()`
也就是说,这两个访问器不只是“读缓存”,还会推动路径恢复后的材质兑现流程向前走。
从行为上看,它们的主用途是支持 deferred scene load但按当前源码只要某个槽位“有路径、还没有 material、也还没发起过请求”首次访问就可能触发一次异步加载尝试。
当前不会再被当成正式项目材质绑定恢复。
## 序列化语义
当前序列化会输出五段键值
当前 [Serialize](Serialize.md) 会写出
```text
materialPaths=<fallbackPath0|fallbackPath1|...>;
materialRefs=<guid,localId,resourceType|...>;
materialPaths=<slot0|slot1|...>;
materialRefs=<slot0|slot1|...>;
castShadows=1;
receiveShadows=1;
renderLayer=0;
```
其中最关键的设计点是:
其中最关键的规则是:
- `materialRefs` 是主身份信息,面向项目资产恢复。
- `materialPaths` 更像回退字段,只在该槽位没有有效 `AssetRef` 时才真正写出路径
- 对有有效 `AssetRef` 的项目材质槽,`materialPaths` 会被清空
- 对没有 `AssetRef` 且是 virtual scheme 的槽位,`materialPaths` 才保留真实路径
例如一个项目资产材质槽,当前文本很可能是
因此
```text
materialPaths=;
materialRefs=<有效 guid,localId,type>;
```
- 项目材质的主协议是 `materialRefs`
- `builtin://materials/default-primitive` 这类虚拟路径才长期留在 `materialPaths`
这不是数据丢失,而是说明当前序列化更信任 `AssetRef` 作为稳定身份。
## 与 deferred scene load 的关系
[GetMaterial](GetMaterial.md) 和 [GetMaterialHandle](GetMaterialHandle.md) 当前都带副作用。
它们会内部触发:
1. `EnsureDeferredAsyncMaterialLoadStarted(index)`
2. `ResolvePendingMaterials()`
所以在 deferred 场景里,反序列化后常见状态是:
- `m_materialRefs[index]` 已恢复
- `m_materialPaths[index]` 可能已恢复
- `m_materials[index]` 仍然为空
直到后续第一次访问材质句柄时,异步兑现才真正开始或收口。
## 与渲染链路的关系
当前 `RenderSceneUtility` 会把 `MeshRendererComponent*` 直接挂进可见渲染对象,再由后续材质提取逻辑读取材质
当前 `RenderSceneUtility` 会把 `MeshRendererComponent*` 挂进可见,再由后续材质提取逻辑读取具体槽位材质。
这意味着
因此当前对 renderer 的真实影响是
- `MeshRendererComponent` 是否“有槽位”不重要。
- 重要的是在真正提取材质时,对应槽位能否拿到有效 `Material`
- 在 deferred / async 场景里,对象可能已经有 `MeshFilterComponent + MeshRendererComponent`,但某个时刻材质仍为空,直到异步结果被 `ResolvePendingMaterials()` 收口。
- 组件是否存在并不等于材质已经可用
- deferred load 下,材质槽元数据可能先恢复,材质句柄后到
## 阴影与渲染层的现实状态
另外要继续明确一个实现边界:
当前组件公开了:
- `castShadows``receiveShadows``renderLayer` 当前已可保存、序列化、脚本读写
- 但还不能简单等同于“这些标志已经完整进入当前 renderer 主路径”
- `GetCastShadows()` / `SetCastShadows()`
- `GetReceiveShadows()` / `SetReceiveShadows()`
- `GetRenderLayer()` / `SetRenderLayer()`
## 测试与锚点
但按当前源码检索,这几个字段目前主要被:
- `tests/Components/test_mesh_render_components.cpp`
- 覆盖 builtin virtual path 的序列化 / 反序列化
- 覆盖“没有 `AssetRef` 的普通项目路径不会作为正式协议保留”
- 覆盖项目材质按 `AssetRef` 序列化与恢复
- 覆盖 deferred async material load
- `tests/Scene/test_scene.cpp`
- 覆盖 builtin material path 的场景保存 / 加载
- 组件测试、场景序列化测试
- 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()` 是带副作用的读访问器;如果调用方只想查看元数据而不触发兑现,应优先使用 [GetMaterialPath](GetMaterialPath.md)、[GetMaterialPaths](GetMaterialPaths.md) 或 [GetMaterialAssetRefs](GetMaterialAssetRefs.md)。
## 当前实现限制
- 当前只维护“平铺材质槽数组”,不处理 submesh 级独立绑定策略、材质实例化策略或 streaming 策略。
- `GetMaterial()` / `GetMaterialHandle()` 可能在首次访问时触发异步加载,因此它们不是纯只读 getter。
- `materialPaths` 在序列化文本里只作为 fallback 字段输出,不应把“文本里 path 为空”误解成“组件内存里没有路径缓存”。
- `castShadows``receiveShadows``renderLayer` 还没有完整接入当前已取证的 renderer 主路径。
- 当前只维护平铺材质槽数组,不处理更复杂的实例化或 streaming 策略
- 项目材质的正式序列化协议已经收口到 `materialRefs`
- 没有 `AssetRef` 的普通项目路径在反序列化时当前会被清掉;只有 virtual scheme 仍可单独依赖路径
- `GetMaterial()` / `GetMaterialHandle()` 是带副作用的读访问器;只读元数据时应优先使用 [GetMaterialPath](GetMaterialPath.md)、[GetMaterialPaths](GetMaterialPaths.md) 和 [GetMaterialAssetRefs](GetMaterialAssetRefs.md)
## 相关方法