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

@@ -1,6 +1,6 @@
# MeshFilterComponent::ClearMesh
清空当前 mesh。
清空当前 mesh 绑定状态
```cpp
void ClearMesh();
@@ -8,10 +8,18 @@ void ClearMesh();
## 行为说明
当前实现会重置内部 handle并清空缓存路径。
当前实现会同时重置:
- `m_pendingMeshLoad`
- `m_asyncMeshLoadRequested`
- `m_mesh`
- `m_meshPath`
- `m_meshRef`
因此它不仅移除当前 mesh也会清掉 deferred scene load 的待完成状态和资产引用元数据。
## 相关文档
- [返回类型总览](MeshFilterComponent.md)
- [SetMesh](SetMesh.md)
- [GetMeshPath](GetMeshPath.md)
- [SetMeshPath](SetMeshPath.md)

View File

@@ -1,26 +1,89 @@
# MeshFilterComponent::Deserialize
反序列化 mesh 信息。
反序列化 mesh 绑定信息。
```cpp
void Deserialize(std::istream& is) override;
```
## 行为说明
## 当前实现行为
当前实现会:
当前实现会先清空
1. 先清空现有 handle 和路径。
2.`;` 分割 token。
3. 识别 `mesh=<path>`
4. 若路径非空,则调用 `ResourceManager::Get().Load<Resources::Mesh>()` 尝试重新加载。
- `m_pendingMeshLoad`
- `m_asyncMeshLoadRequested`
- `m_mesh`
- `m_meshPath`
- `m_meshRef`
## 当前实现说明
然后只解析两项键值:
- 如果资源加载失败,`m_mesh` 可能仍然为空,但 `m_meshPath` 会保留反序列化得到的路径。
- 当前序列化格式很轻量,适合人类可读文本,但没有更强的容错与版本化机制。
- `meshRef=<guid,localId,resourceType>`
- `meshPath=<path>`
## 恢复顺序
### 1. `meshRef` 有效
这是当前项目资产恢复的主路径。
当前会先保存 `m_meshRef = pendingMeshRef`,然后分两种情况:
-`ResourceManager::IsDeferredSceneLoadEnabled()` 为真
- 先尝试 `TryResolveAssetPath(meshRef, resolvedPath)`
- 成功时只恢复 `m_meshPath = resolvedPath`
- 不立即同步加载 mesh
- 若不在 deferred 模式
- 直接 `Load<Mesh>(pendingMeshRef)`
- 加载成功则从资源句柄反写 `m_meshPath`
- 失败时才回退到文本里读到的 `pendingMeshPath`
### 2. `meshRef` 无效,但 `meshPath` 是 virtual scheme
当前只接受这类路径回退:
- 若处于 deferred scene load直接保留 `m_meshPath`
- 否则调用 [SetMeshPath](SetMeshPath.md) 走同步加载
### 3. `meshRef` 无效,且 `meshPath` 只是普通项目路径
当前会直接忽略。
也就是说,像下面这种旧数据:
```text
meshPath=Assets/runtime.obj;meshRef=;
```
当前不会再被当成正式项目资产绑定恢复。
## 已删除的旧心智
这页最需要明确的一点是:
- 当前不再兼容 `mesh=<path>`
- 当前也不再把“没有 `AssetRef` 的普通项目路径”当成长期场景协议
只有这两类数据还能稳定恢复:
- 有效 `meshRef`
- 带 virtual scheme 的 `meshPath`
## 与 deferred scene load 的关系
在 deferred 模式下,反序列化阶段更像是恢复绑定元数据,而不是立刻拿到 mesh 句柄。
真正的兑现一般留到后续:
- [GetMesh](GetMesh.md)
- [GetMeshHandle](GetMeshHandle.md)
第一次访问这些接口时,内部才会启动或收口异步加载。
## 相关文档
- [返回类型总览](MeshFilterComponent.md)
- [Serialize](Serialize.md)
- [GetMesh](GetMesh.md)
- [GetMeshHandle](GetMeshHandle.md)
- [GetMeshAssetRef](GetMeshAssetRef.md)

View File

@@ -1,17 +1,33 @@
# MeshFilterComponent::GetMesh
获取当前 mesh 指针。
获取当前可用的 mesh 指针。
```cpp
Resources::Mesh* GetMesh() const;
```
## 行为说明
当前实现不是单纯返回 `m_mesh.Get()`,还会在内部额外执行两步:
1. `EnsureDeferredAsyncMeshLoadStarted()`:在 deferred scene load 模式下,如果当前只有路径还没有实际 mesh会按 `m_meshPath` 发起一次异步加载。
2. `ResolvePendingMesh()`:如果异步回调已经完成,则把结果写回 `m_mesh``m_meshPath``m_meshRef`
因此,这个访问器既是读取接口,也是 deferred mesh 解析流程的推进点。
## 返回值
- 当前 mesh 指针;如果没有设置则返回 `nullptr`
- 如果当前已经有可用 mesh返回对应指针
- 如果没有 mesh或异步加载尚未完成返回 `nullptr`
- 异步加载失败时也返回 `nullptr`,同时保留已有路径信息。
## 注意事项
- 在延迟场景加载模式下,第一次调用可能只是发起异步加载,本次仍返回空指针。
- 渲染提取逻辑会直接依赖这个返回值;返回 `nullptr` 时,对象不会进入可见渲染列表。
## 相关文档
- [返回类型总览](MeshFilterComponent.md)
- [GetMeshHandle](GetMeshHandle.md)
- [SetMesh](SetMesh.md)
- [Deserialize](Deserialize.md)

View File

@@ -0,0 +1,31 @@
# MeshFilterComponent::GetMeshAssetRef
获取当前 mesh 的资产引用。
```cpp
const Resources::AssetRef& GetMeshAssetRef() const;
```
## 返回值
- 返回内部保存的 `AssetRef` 常量引用。
## 行为说明
`m_meshRef` 会在以下路径里更新:
- [SetMeshPath](SetMeshPath.md) 成功从路径反查资产引用时
- [SetMesh](SetMesh.md) 成功从 mesh 路径反查资产引用时
- [Deserialize](Deserialize.md) 直接读到 `meshRef=` 字段时
- 异步 mesh 兑现后,再次按最终路径回填资产引用时
## 注意事项
- 这是只读元数据访问器,不会触发加载。
- 当路径无法映射到项目资产,或反序列化数据里没有有效 `meshRef` 时,返回值可能是无效引用。
## 相关文档
- [返回类型总览](MeshFilterComponent.md)
- [GetMeshPath](GetMeshPath.md)
- [Deserialize](Deserialize.md)

View File

@@ -6,11 +6,25 @@
const Resources::ResourceHandle<Resources::Mesh>& GetMeshHandle() const;
```
## 行为说明
当前实现和 [GetMesh](GetMesh.md) 一样,会先尝试推进 deferred scene load
- 如果还没请求过异步加载,会按 `m_meshPath` 发起一次 `LoadAsync()`
- 如果异步结果已经回到 `m_pendingMeshLoad`,会先把结果兑现到 `m_mesh` 再返回句柄引用。
## 返回值
- 当前保存的 mesh `ResourceHandle` 常量引用。
- 返回内部 `m_mesh` 常量引用。
- 在 mesh 尚未加载完成时,返回的句柄可能为空。
## 注意事项
- 这是“可能带副作用”的访问器,而不是纯 getter。
- 如果调用方只想读取序列化主数据而不触发加载,应优先看 [GetMeshPath](GetMeshPath.md) 或 [GetMeshAssetRef](GetMeshAssetRef.md)。
## 相关文档
- [返回类型总览](MeshFilterComponent.md)
- [GetMesh](GetMesh.md)
- [GetMeshAssetRef](GetMeshAssetRef.md)

View File

@@ -8,14 +8,22 @@ const std::string& GetMeshPath() const;
## 返回值
- 当前存的 mesh 路径。
- 返回组件当前存的路径字符串
## 行为特点
- 这是当前场景文本序列化的主数据之一。
- 当 mesh 通过句柄设置或异步加载成功后,路径可能被更新为资源对象实际返回的 `GetPath()`
- 即使 mesh 当前还没加载成功,路径也可能已经存在。
## 注意事项
- 当前路径只是组件内部缓存值,不保证文件一定存在
- 它不会触发异步加载,也不会等待待完成的 mesh 解析
- 路径存在不代表 `GetMesh()` 一定非空。
## 相关文档
- [返回类型总览](MeshFilterComponent.md)
- [GetMeshAssetRef](GetMeshAssetRef.md)
- [Serialize](Serialize.md)
- [Deserialize](Deserialize.md)

View File

@@ -6,97 +6,150 @@
**头文件**: `XCEngine/Components/MeshFilterComponent.h`
**描述**: 保存网格资源引用及其路径,让场景对象能够声明“我使用哪一个 Mesh”
**描述**: 保存单个 mesh 绑定状态,把运行时 `Mesh` 句柄、项目资产 `AssetRef` 和少量 virtual path 协议收口到同一个组件里
## 角色概述
`MeshFilterComponent` 负责“几何数据来自哪里”,而不是“怎么绘制”。在当前对象模型里
`MeshFilterComponent` 回答的是一个很具体的问题
- `MeshFilterComponent` 提供网格
- [MeshRendererComponent](../MeshRendererComponent/MeshRendererComponent.md) 提供材质和渲染附加配置
- 两者一起被 [RenderSceneExtractor](../../Rendering/RenderSceneExtractor/RenderSceneExtractor.md) 消费
- 这个对象当前引用哪一个 mesh
这和 Unity 里 `MeshFilter` / `MeshRenderer` 分工的思路是一致的,优点是几何与渲染配置解耦,便于编辑器或脚本单独替换。
它不处理材质,也不直接发起绘制。当前真正的职责有三层:
## 当前实现行为
- 保存运行时 `Mesh` 句柄,供渲染提取和运行时逻辑直接使用
- 保存项目资产 `AssetRef`,让场景在资源移动或重命名后仍能稳定恢复
- 仅在没有可用 `AssetRef` 时,为 `builtin://` 这类 virtual path 保留文本路径
### 1. 同时保存资源句柄和资源路径
这意味着当前文档里最需要纠正的一点是:
内部状态有两份:
- 项目资产的正式序列化协议已经不再是普通路径字符串
- 对项目 mesh当前主身份是 `meshRef`
- `meshPath` 主要保留给 `builtin://` 或其它 virtual scheme
- `m_mesh``ResourceHandle<Mesh>`
- `m_meshPath``std::string`
## 当前状态模型
这样设计的直接收益是
当前实现维护 5 份与 mesh 绑定相关的状态
- 运行时可以快速拿到已加载资源
- 序列化时又能稳定写出路径
| 状态 | 类型 | 作用 |
|------|------|------|
| `m_mesh` | `ResourceHandle<Resources::Mesh>` | 当前已兑现的运行时 mesh 句柄。 |
| `m_meshPath` | `std::string` | 当前内存里的路径缓存;对项目资产通常会是 `Assets/...`,对 virtual 资源则可能是 `builtin://...`。 |
| `m_meshRef` | `Resources::AssetRef` | 项目资产的稳定身份。 |
| `m_pendingMeshLoad` | `std::shared_ptr<...>` | deferred async load 的挂起结果。 |
| `m_asyncMeshLoadRequested` | `bool` | 防止重复发起异步 mesh 加载。 |
### 2. `SetMeshPath()` 会尝试加载,但即使加载失败也保留路径
这几份状态不是完全等价的,尤其要区分:
按当前实现:
- `m_meshRef` 是项目资产正式协议
- `m_meshPath` 是运行时缓存与 virtual path 回退字段
- 传入空路径时会清空 handle 和 path
- 传入非空路径时会调用 `ResourceManager::Get().Load<Mesh>(...)`
- 无论资源是否成功加载,`m_meshPath` 都会先被更新
## 绑定与恢复流程
这是一条很重要的行为约定。它意味着:
### 1. 运行时按路径设置
- 文档页或编辑器可以先持有“用户想要的路径”
- 即使资源此刻没加载出来,序列化信息也不会丢
[SetMeshPath](SetMeshPath.md) 当前仍然是“立即尝试同步加载”的接口:
这一点已经被 `tests/Components/test_mesh_render_components.cpp` 覆盖。
1. 清掉旧的 pending async load 状态
2. 直接写入 `m_meshPath`
3. 若路径为空,则清空句柄与 `AssetRef`
4. 否则同步 `Load<Mesh>(path)`
5. 再尝试按路径回填 `m_meshRef`
### 3. 直接设置 handle 时会反向推导路径
因此对项目路径 `Assets/...` 来说,运行时是允许直接设置路径的;但这不等于序列化后仍靠路径当正式协议。
`SetMesh(const ResourceHandle<Mesh>&)` 会:
### 2. 运行时按句柄设置
- 保存 handle
- 如果 handle 非空,则从 `mesh->GetPath()` 反推出 `m_meshPath`
- 如果 handle 为空,则清空路径
[SetMesh](SetMesh.md) 会保存句柄,并从 `mesh->GetPath()` 反推路径,再按路径尝试回填 `AssetRef`
这保证了路径和运行时资源引用不会长期脱节
这保证了“直接塞一个已加载 mesh”与“后续还能序列化出稳定项目身份”这两件事可以同时成立
### 3. 反序列化恢复
[Deserialize](Deserialize.md) 当前只识别:
- `meshRef=<guid,localId,resourceType>`
- `meshPath=<virtual path>`
恢复顺序是:
1. 如果 `meshRef` 有效,优先按 `AssetRef` 恢复
2. 若当前处于 deferred scene load先尝试 `TryResolveAssetPath(meshRef, path)` 恢复 `Assets/...` 路径,但不立即同步加载
3. 若不在 deferred 模式,则直接按 `AssetRef` 加载 mesh
4. 若没有有效 `meshRef`,只接受带 virtual scheme 的 `meshPath`
5. 没有 `AssetRef` 的普通项目路径会被忽略
这里是当前实现最容易被旧文档写错的地方:
- `mesh=...` 已经不是当前协议
- `meshPath=Assets/...` 且没有 `meshRef`,当前不会被当成正式项目资产绑定恢复
## 序列化语义
当前序列化只写一个字段
当前 [Serialize](Serialize.md) 的真实输出规则是
- `mesh=<path>`
- 总是写 `meshRef=...;`
- 只有在没有有效 `AssetRef``m_meshPath` 是 virtual scheme 时,才额外写 `meshPath=...;`
反序列化时会先清空旧状态,再通过 `SetMeshPath()` 恢复路径并尝试加载资源。
因此:
这意味着:
- 项目资产 mesh 常见输出是只有 `meshRef`
- `builtin://meshes/cube` 这类路径才会稳定出现在 `meshPath`
- 场景文件的真实持久化载体是路径
- 运行时 handle 是可重建缓存,而不是主数据
这和商业引擎里“项目资产走稳定内部引用builtin/虚拟资源走协议路径”的思路是一致的。
## 与渲染提取的关系
## 与 deferred scene load 的关系
`RenderSceneExtractor` 只有在以下条件都满足时才会把对象当成可见渲染项:
[GetMesh](GetMesh.md) 和 [GetMeshHandle](GetMeshHandle.md) 都不是纯 getter。
- `GameObject` 处于激活层级
- 同时存在 `MeshFilterComponent``MeshRendererComponent`
- 两个组件都已启用
- `MeshFilterComponent::GetMesh()` 返回非空且 `mesh->IsValid()`
当前它们会内部触发:
因此,只有 `MeshFilterComponent` 还不够,它必须和 `MeshRendererComponent` 配对使用。
1. `EnsureDeferredAsyncMeshLoadStarted()`
2. `ResolvePendingMesh()`
## 线程语义
因此在 deferred scene load 模式下,反序列化阶段往往只恢复:
- 当前实现没有内部加锁。
- 资源加载调用走 `ResourceManager`,但本组件文档不提供额外线程安全保证。
- `m_meshRef`
- `m_meshPath`
## 当前实现限制
真正 mesh 句柄的兑现,通常发生在后续访问器首次被调用时。
- 它只负责引用网格不处理子网格裁剪、LOD 或网格 streaming 策略。
- 场景保存后真正持久化的是路径,不是已经加载好的资源实例。
- 当前目录下只有 [SetMesh](SetMesh.md) 方法页,`SetMeshPath()` 也是 public API应优先参考本页的行为说明。
## 与渲染链路的关系
`RenderSceneUtility::AppendRenderItemsForGameObject()` 只有在以下条件同时满足时,才会把对象加入可见渲染项:
- 对象激活
- `MeshFilterComponent``MeshRendererComponent` 都存在且启用
- `meshFilter->GetMesh()` 返回非空并且 mesh 有效
所以当前 `MeshFilterComponent` 的真实影响是:
- 它决定对象什么时候拥有几何体
- 但 deferred load 下,“路径和 `AssetRef` 已恢复”不等于“这一帧已经可绘制”
## 测试与锚点
- `tests/Components/test_mesh_render_components.cpp`
- 覆盖 builtin virtual path 的序列化 / 反序列化
- 覆盖“没有 `AssetRef` 的普通路径会被忽略”
- 覆盖项目 mesh 按 `AssetRef` 序列化与恢复
- 覆盖 deferred async load
- `tests/Scene/test_scene.cpp`
- 覆盖 builtin mesh path 的场景保存 / 加载
- 覆盖样例场景里项目 mesh `Assets/Models/backpack/backpack.obj` 的恢复和延迟加载
## 当前实现边界
- 当前只绑定单个 mesh不处理 LOD、mesh streaming 或多几何切换
- 项目 mesh 的正式序列化协议已经收口到 `AssetRef`,不再鼓励把普通项目路径当长期场景格式依赖
- 没有 `AssetRef` 的普通项目路径在反序列化时当前会被忽略;只有 `builtin://` / 其它 virtual scheme 还能单独靠路径存活
- `GetMesh()` / `GetMeshHandle()` 是带副作用的访问器;只读元数据时应优先用 [GetMeshPath](GetMeshPath.md) 和 [GetMeshAssetRef](GetMeshAssetRef.md)
## 相关方法
- [GetMesh](GetMesh.md)
- [GetMeshHandle](GetMeshHandle.md)
- [GetMeshPath](GetMeshPath.md)
- [GetMeshAssetRef](GetMeshAssetRef.md)
- [SetMesh](SetMesh.md)
- [SetMeshPath](SetMeshPath.md)
- [ClearMesh](ClearMesh.md)

View File

@@ -1,22 +1,67 @@
# MeshFilterComponent::Serialize
序列化当前 mesh 信息。
序列化当前 mesh 绑定信息。
```cpp
void Serialize(std::ostream& os) const override;
```
## 行为说明
## 当前实现行为
当前实现会输出
当前实现会先准备一份待写出的 `meshRef`
- 优先使用当前内存中的 `m_meshRef`
- 如果 `m_meshRef` 还无效,但 `m_meshPath` 是普通项目路径而不是 virtual scheme则会再尝试一次 `TryGetAssetRef(...)`
随后输出规则只有两条:
1. 总是写出 `meshRef=<...>;`
2. 只有当 `meshRef` 仍无效,且 `m_meshPath` 是 virtual scheme 时,才额外写出 `meshPath=<...>;`
## 当前序列化结果
### 项目资产 mesh
例如当前组件绑定的是 `Assets/runtime.obj` 且能解析出有效 `AssetRef`,输出通常会像:
```text
mesh=<path>;
meshRef=<guid,localId,resourceType>;
```
这里只序列化路径,不会序列化 mesh 二进制内容本身。
注意:
- 这时不会再写 `meshPath=Assets/runtime.obj;`
### builtin / virtual path mesh
例如当前组件绑定的是 `builtin://meshes/cube`,而它没有项目资产 `AssetRef`,输出会像:
```text
meshRef=;
meshPath=builtin://meshes/cube;
```
## 关键语义
- 当前已经不再输出旧的 `mesh=<path>`
- 项目资产的正式场景身份是 `meshRef`,不是普通路径字符串
- `meshPath` 现在主要是 virtual path 回退字段
## 设计说明
这套设计的价值在于:
- 项目资源移动、改名或重新导入后,场景仍可按稳定身份恢复
- builtin / 虚拟资源又不必强行塞进项目资产数据库
因此它不是“路径 + `AssetRef` 两份主数据并列”,而是:
- 项目资产优先 `AssetRef`
- virtual resource 回退 `meshPath`
## 相关文档
- [返回类型总览](MeshFilterComponent.md)
- [GetMeshAssetRef](GetMeshAssetRef.md)
- [GetMeshPath](GetMeshPath.md)
- [Deserialize](Deserialize.md)

View File

@@ -1,6 +1,6 @@
# MeshFilterComponent::SetMesh
设置当前 mesh。
直接设置当前 mesh 资源
```cpp
void SetMesh(const Resources::ResourceHandle<Resources::Mesh>& mesh);
@@ -9,18 +9,26 @@ void SetMesh(Resources::Mesh* mesh);
## 行为说明
无论使用哪种重载,当前实现会:
两个重载最终都会走 `ResourceHandle<Mesh>` 版本。当前实现会:
- 更新内部 `ResourceHandle`
- 如果 mesh 非空,则把其 `GetPath()` 转成 `std::string` 写入 `m_meshPath`
- 否则清空路径
1. 清掉待完成的异步加载状态。
2. 保存传入的 mesh 句柄。
3. 如果句柄非空,则从 `mesh->GetPath()` 反推出 `m_meshPath`
4. 再尝试按该路径反查 `m_meshRef`
5. 如果句柄为空或路径为空,则清空 `m_meshRef`
## 参数
- `mesh` - 要设置的 mesh 句柄或裸指针。
- `mesh` - 要绑定的 mesh 句柄或裸指针。
## 设计含义
这让运行时代码可以直接复用已加载 mesh同时仍保留后续序列化和场景保存所需的路径 / 资产引用信息。
## 相关文档
- [返回类型总览](MeshFilterComponent.md)
- [GetMesh](GetMesh.md)
- [GetMeshPath](GetMeshPath.md)
- [GetMeshAssetRef](GetMeshAssetRef.md)
- [ClearMesh](ClearMesh.md)

View File

@@ -1,30 +1,35 @@
# SetMeshPath
# MeshFilterComponent::SetMeshPath
**所属类型**: [MeshFilterComponent](MeshFilterComponent.md)
## 签名
按资源路径声明当前组件应引用哪个 mesh。
```cpp
void SetMeshPath(const std::string& meshPath);
```
## 作用
## 行为说明
按资源路径声明当前组件应引用哪个 `Mesh`,并尝试通过 `ResourceManager` 立即加载该资源。
当前实现会按以下顺序执行:
## 当前实现行为
1. 清掉 `m_pendingMeshLoad`,并把 `m_asyncMeshLoadRequested` 复位为 `false`
2.`m_meshPath` 直接更新为传入值。
3. 如果路径为空,则清空 `m_mesh``m_meshRef` 后返回。
4. 如果路径非空,则立即调用 `ResourceManager::Load<Mesh>(meshPath)` 尝试同步加载 mesh。
5. 无论 mesh 是否加载成功,都会再尝试按路径反查 `AssetRef`
- 先把 `m_meshPath` 更新为传入值。
- 如果路径为空,清空当前 `m_mesh` handle 并返回。
- 如果路径非空,调用 `ResourceManager::Get().Load<Resources::Mesh>(meshPath.c_str())`
- 即使加载失败,路径也会被保留下来。
## 参数
## 使用建议
- `meshPath` - 要绑定的 mesh 资源路径。
这是编辑器或序列化恢复路径更应该使用的接口,因为它保留了“目标资源路径”这一主数据。
## 关键语义
- 这个 setter 会把传入路径直接写进运行时缓存,即使加载失败也会保留路径文本。
- 这个 API 是“立即同步尝试加载”的语义deferred scene load 只影响 [Deserialize](Deserialize.md) 后的恢复路径,不会改变这里的行为。
- 如果资产数据库能识别这条路径,`m_meshRef` 可能有效,即使 `m_mesh` 仍为空。
- 对项目资产来说,当前场景正式协议仍优先写 `AssetRef`;这里保留的路径更像运行时输入和缓存,而不是长期持久化格式本体。
## 相关文档
- [MeshFilterComponent](MeshFilterComponent.md)
- [返回类型总览](MeshFilterComponent.md)
- [SetMesh](SetMesh.md)
- [Serialize](Serialize.md)
- [Deserialize](Deserialize.md)

View File

@@ -6,44 +6,76 @@
void Deserialize(std::istream& is) override;
```
## 行为说明
## 当前实现行为
当前实现会:
当前实现会
1. 先清空材质并重置默认标志。
2. 解析 `materialPaths``materialRefs``castShadows``receiveShadows``renderLayer`
3. 兼容读取历史 `materials=` 键,并把它当作 `materialPaths=` 处理。
4.`m_materials``m_materialPaths``m_materialRefs`、pending 数组和 async 标记数组在槽位数量上重新对齐。
1. 调用 `ClearMaterials()`
2. 把默认值重置为:
- `castShadows = true`
- `receiveShadows = true`
- `renderLayer = 0`
随后每个槽位按下面的优先级恢复
然后只解析
1. 如果 `materialRef` 有效,先尝试按 `AssetRef` 恢复。
2. 如果当前处于 deferred scene load优先尝试把 `AssetRef` 重新解析回路径,但不立即同步加载材质。
3. 如果没有有效 `AssetRef`,或者按 `AssetRef` 恢复失败,再退回到路径恢复。
4. 只有在非 deferred 路径下,才会直接调用 [SetMaterialPath](SetMaterialPath.md) 做同步加载。
- `materialPaths=<path0|path1|...>`
- `materialRefs=<guid,localId,resourceType|...>`
- `castShadows`
- `receiveShadows`
- `renderLayer`
这意味着“反序列化完成”并不等于“所有材质句柄都已经可用”。
## 恢复顺序
## 当前实现说明
### 1. 某槽位 `materialRef` 有效
- 默认值是:`castShadows = true``receiveShadows = true``renderLayer = 0`
- 如果某个槽位最终只恢复出了路径或 `AssetRef`,对应 `m_materials[index]` 仍可能为空。
- 在 deferred scene load 模式下,真正的兑现通常要等首次 [GetMaterial](GetMaterial.md) 或 [GetMaterialHandle](GetMaterialHandle.md) 调用时才会触发。
这是当前项目材质恢复的主路径
## 项目资产恢复语义
当前会先保存 `m_materialRefs[i] = pendingMaterialRefs[i]`,然后分两种情况:
当前实现特别强调项目资产的稳定恢复:
-`ResourceManager::IsDeferredSceneLoadEnabled()` 为真
- 先尝试 `TryResolveAssetPath(materialRef, resolvedPath)`
- 成功时只恢复 `m_materialPaths[i] = resolvedPath`
- 不立即同步加载材质
- 若不在 deferred 模式,或 deferred 下未成功解析路径
- 直接 `Load<Material>(pendingMaterialRef)`
- 成功时从句柄反写 `m_materialPaths[i]`
- 当场景文本里有有效 `materialRef=` 时,组件会尽量依赖它,而不是单纯依赖字符串路径。
- 如果 `AssetRef` 能解析到当前项目中的材质,路径会被重新规范化回 `Assets/...`
- 如果解析失败,仍会回退到文本路径。
### 2. 某槽位 `materialRef` 无效
和商业引擎常见的“文本可读路径 + 稳定内部引用”双轨设计一致,优点是既便于调试,也能提高资源重命名后的恢复成功率。
时只剩路径回退分支:
## 兼容性说明
-`m_materialPaths[i]` 不是 virtual scheme当前会直接清空该路径
- 若是 virtual scheme 且处于 deferred 模式,则只保留路径
- 若是 virtual scheme 且不在 deferred 模式,则调用 [SetMaterialPath](SetMaterialPath.md) 走同步加载
- 仍兼容历史 `materials=` 文本。
- 新版 [Serialize](Serialize.md) 不再输出 `materials=`
## 已删除的旧心智
这页当前必须明确:
- 不再读取历史 `materials=`
- 不再把“没有 `AssetRef` 的普通项目路径”当作正式项目材质恢复协议
因此像下面这种旧式数据:
```text
materialPaths=Assets/runtime.material;materialRefs=;
```
当前恢复后,这个普通路径会被清掉,而不是继续被保留。
## 与 deferred scene load 的关系
在 deferred 模式下,反序列化阶段通常只恢复:
- `m_materialRefs`
- `m_materialPaths`
真正材质句柄的兑现,往往留到后续:
- [GetMaterial](GetMaterial.md)
- [GetMaterialHandle](GetMaterialHandle.md)
这些访问器第一次被调用时,内部才会启动或收口异步材质加载。
## 相关文档

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)
## 相关方法

View File

@@ -6,33 +6,71 @@
void Serialize(std::ostream& os) const override;
```
## 行为说明
## 当前实现行为
当前实现不再输出旧的 `materials=` 键,而是并行写出材质路径 fallback 和稳定资产引用
当前实现会先准备两份待写出的槽位数组
- `serializedRefs`
- `serializedPaths`
对每个槽位,处理规则是:
1. 若当前 `AssetRef` 无效、`materialPath` 非空、且路径不是 virtual scheme则再尝试一次 `TryGetAssetRef(...)`
2. 若该槽位最终有有效 `AssetRef`,或路径本来就不是 virtual scheme则把 `serializedPaths[i]` 清空
然后按槽位顺序输出:
```text
materialPaths=|builtin://materials/default;
materialRefs=<guid,localId,resourceType>|;
materialPaths=<slot0|slot1|...>;
materialRefs=<slot0|slot1|...>;
castShadows=1;
receiveShadows=0;
renderLayer=3;
receiveShadows=1;
renderLayer=0;
```
其中:
## 当前序列化结果
- `materialPaths``materialRefs` 都按材质槽顺序用 `|` 分隔。
- 对于能稳定映射到项目资产的槽位,当前会优先写 `materialRefs`,并把同槽位 `materialPaths` 留空。
- 对于没有有效 `AssetRef` 的槽位,例如 `builtin://` 这类虚拟路径,才会把路径写进 `materialPaths`
- 会保留尾部空槽位,避免编辑器材质槽数量在反序列化后缩水。
### 项目材质槽
## 当前实现含义
如果某槽位绑定的是项目材质 `Assets/runtime.material`,且能解析出有效 `AssetRef`,常见输出会像:
- 场景文本现在同时保存“人类可读路径 fallback”和“项目资产稳定身份”。
- 项目资产改名或移动后,更应依赖 `materialRefs` 恢复绑定,而不是依赖旧路径字符串。
- 旧版依赖 `materials=` 的读取逻辑仍由 [Deserialize](Deserialize.md) 保留兼容。
```text
materialPaths=;
materialRefs=<guid,localId,resourceType>;
```
也就是说:
- 不会再把普通项目路径当正式场景数据保留下来
### builtin / virtual path 槽
如果某槽位绑定的是 `builtin://materials/default-primitive`,而它没有项目资产 `AssetRef`,则常见输出会像:
```text
materialPaths=builtin://materials/default-primitive;
materialRefs=;
```
## 关键语义
- 当前不再输出旧的 `materials=`
- 项目资产材质的正式场景身份是 `materialRefs`
- `materialPaths` 现在主要承担 virtual path 回退职责
- 会保留尾部空槽位,避免反序列化后材质槽数量缩水
## 设计说明
这种设计和 `MeshFilterComponent` 的协议方向一致:
- 项目资源走稳定 `AssetRef`
- builtin / 虚拟资源走显式协议路径
这样既能提高资源改名、移动后的恢复成功率,也不会强迫 virtual 资源进入项目资产数据库。
## 相关文档
- [返回类型总览](MeshRendererComponent.md)
- [Deserialize](Deserialize.md)
- [GetMaterialAssetRefs](GetMaterialAssetRefs.md)
- [GetMaterialPaths](GetMaterialPaths.md)
- [Deserialize](Deserialize.md)

View File

@@ -21,14 +21,15 @@ void SetMaterialPath(size_t index, const std::string& materialPath);
- 这是一个同步设置接口,不是“只记录路径、等以后再加载”的接口。
- 虚拟路径或内置路径也允许写入,但通常拿不到有效 `AssetRef`
- 如果后续重新设置同一槽位,旧的异步状态会被显式丢弃。
- 对项目材质来说,这个 API 写入的是运行时路径缓存;真正序列化时只要能得到有效 `AssetRef`,普通项目路径就不会作为正式场景数据长期保留。
## 使用建议
这比直接设置 handle 更适合:
- 编辑器材质面板
- 场景文本反序列化后的路径恢复
- 希望保留稳定文本路径的工具链
- 运行时或工具侧按路径即时指定材质
- virtual path 资源输入
如果调用方已经手里拿到了一个确定可用的材质句柄,并且希望以运行时对象为主写入,则可以考虑 [SetMaterial](SetMaterial.md)。