Files
XCEngine/docs/plan/Unity式Library资产导入与缓存系统重构方案.md

39 KiB
Raw Blame History

Unity式 Library 资产导入与缓存系统重构方案

0. 当前实施进度2026-04-02

本节用于记录方案的实际落地状态,避免文档只停留在设计层。

0.1 阶段进度表

阶段 状态 当前结果
阶段 0基础类型与工程边界收口 已完成 已引入 AssetGUIDAssetRef,开始把运行时 ResourceGUID 与编辑器资产身份拆开。
阶段 1.meta 与 SourceAssetDB 已完成 已支持自动生成 .meta、扫描 Assets、建立 guid/path/importer 索引,并支持路径到 GUID/AssetRef 的反查。
阶段 2Artifact Store 与 ArtifactDB 已完成 已落地 Library/SourceAssetDBLibrary/ArtifactDBLibrary/Artifacts,支持 artifact key、导入失效判断与产物目录管理。
阶段 3TextureImporter 已完成 已支持纹理导入为 xctex,运行时可直接从 artifact 读取,不再总是解码原始图片。
阶段 4ModelImporter 已完成(初版) 已支持模型导入为 xcmesh,并缓存模型关联纹理的 xctexResourceManager 会优先走 artifact。
阶段 5Material 系统 lazy 引用化 部分完成 已补齐材质序列化所需的 tag/property/texture binding 访问接口,但仍是 eager 贴图加载,未完成真正的延迟纹理解析。
阶段 6Scene / Component 引用迁移 部分完成 MeshFilterComponentMeshRendererComponent 已开始双写路径和 AssetRef,并已在 editor 场景打开路径接入延迟恢复。Scene 全量格式迁移尚未完成。
阶段 7异步导入与异步加载 部分完成 AsyncLoader 已具备真实 worker 线程与主线程 completion pumpeditor 打开 scene 时已改为 deferred restore但通用导入队列、placeholder/UI、全资源类型异步化尚未完成。
阶段 8清理、GC、工具与可视化 未开始 还没有 Reimport All、依赖图查看、orphan artifact 清理、导入面板等工具。

0.2 本轮已经落地的具体内容

  • 新增 AssetGUIDAssetRefArtifactFormatsAssetDatabase,正式建立 Unity 式资产身份与导入缓存基础设施。
  • ResourceManager 已接入 AssetDatabase,资源加载时会先尝试 EnsureArtifact(...),命中后走 Library 产物。
  • TextureLoader 已支持 xctexMeshLoader 已支持 xcmesh,从而把“导入”和“运行时加载”从代码路径上拆开。
  • ProjectManager 已支持忽略 .meta 文件显示,并在资源删除、移动、重命名时携带 .meta sidecar。
  • MeshFilterComponentMeshRendererComponent 已开始从纯路径引用迁到 AssetRef,目前是“旧路径兼容 + 新引用双写”模式。
  • AsyncLoader 已从空壳改为真实后台加载器,支持 worker thread、完成队列与主线程回调泵。
  • editor 的 SceneManager::LoadScene(...) / RestoreSceneSnapshot(...) 已接入 deferred scene load scopescene 打开时不再在组件反序列化阶段同步恢复 mesh/material。
  • EditorWorkspace::Update(...) 已开始逐帧 pump async completionMeshFilterComponent / MeshRendererComponent 现在会在资源真正完成后再接管 handle。
  • 已为纹理 artifact、模型 artifact、组件 AssetRef 序列化补充回归测试。

0.3 已验证结果

  • Debugtexture_tests.exe 已通过,覆盖 .meta 自动生成、Library 产物生成、二次 EnsureArtifact 不重复导入、按 AssetRef 再加载纹理。
  • Debugcomponents_tests.exe 已通过,覆盖 MeshRendererComponentAssetRef 双写序列化/反序列化,以及项目内材质按 AssetRef 的 deferred async 恢复。
  • Releasemesh_tests.exe 已通过,覆盖 obj + mtl + 贴图 导入为 xcmesh + xctex、二次导入命中缓存、按 AssetRef 再加载模型。
  • ReleaseXCEditor 已构建通过。

0.4 当前版本仍然存在的限制

  • editor 场景打开阶段虽然已经不再在组件反序列化里同步恢复大资源,但首次真正需要该资源时,若 artifact 缺失或失效,后台任务仍可能较重,且缺少显式进度/UI。
  • 即使已经命中 artifact当前模型 artifact 读取后仍会立即把关联贴图一起加载到内存,尚未做到材质纹理 lazy load。
  • 首帧渲染阶段仍会发生 GPU 资源创建与上传,这部分也会带来一次性卡顿。
  • MaterialImporter 还没有形成完整的独立 artifact 格式,材质系统仍处于过渡状态。
  • Scene 级别仍是“组件局部迁移”,还不是一套完整的新序列化协议。

1. 文档目的

本文档用于给 XCEngine 当前资源系统做一次正式的、可实施的重构设计,目标是把当前“打开场景时直接同步读取原始资源”的模式,重构为一套接近 Unity 的:

  • Assets 作为源资源目录
  • .meta 作为稳定资产身份与导入配置
  • Library 作为导入产物与索引缓存
  • 场景、组件、材质、渲染运行时全部通过稳定资产引用访问缓存产物

本文档不是一句“加个缓存”式的小修,而是资产身份、导入、缓存、运行时加载、组件引用、编辑器流程的系统级重构方案。


2. 这次重构要解决什么问题

2.1 当前最直接的问题

当前 editor 打开一个引用了 obj 模型的 scene 时,慢的不是 scene 文件本身,而是 scene 打开过程中同步触发了原始资源导入。

以当前 project/Assets/Scenes/Backpack.xc 为例scene 文件本身很小,但里面的 MeshFilter 直接引用了:

  • Assets/Models/backpack/backpack.obj

当前调用链是:

  1. SceneManager 打开 scene
  2. Scene 反序列化 GameObject 与 Component
  3. MeshFilterComponent 反序列化时,立刻按路径同步 Load<Mesh>()
  4. MeshLoader 直接用 Assimp 读取原始 obj
  5. MeshLoader 顺手把模型材质和贴图一起导入
  6. TextureLoader 直接读取并解码原始 png/jpg
  7. 首帧渲染再把 CPU 资源上传到 GPU

这意味着当前“打开 scene”其实包含了“首次导入资源”。

2.2 当前系统的结构性问题

当前系统慢,不是某一个函数写得慢,而是系统边界本身就不对。问题包括:

  • 资产身份不稳定。当前 ResourceGUID 本质上是路径 hash不是独立资产 ID。文件改名、移动、目录调整都会导致身份变化。
  • Scene/Component 引用层错误。当前 scene 和组件直接存路径字符串,而不是稳定资产引用。
  • 资源导入和运行时加载混在一起。IResourceLoader 现在既承担“从源文件导入”,又承担“运行时拿到资源对象”。
  • 没有持久化导入缓存。当前没有等价于 Unity Library/Artifacts 的正式产物目录。
  • 没有源资源数据库。当前没有等价于 Unity SourceAssetDB 的资产身份与路径索引层。
  • 没有 artifact 数据库。当前没有等价于 Unity ArtifactDB 的导入结果索引层。
  • 没有依赖图。模型依赖哪些贴图、材质、侧车文件,目前没有正式记录。
  • 组件反序列化会立即加载资源。Scene 打开直接卡在主线程。
  • 异步加载器目前基本是空壳,不能承担真正的后台导入与后台加载。
  • 渲染时还有 editor 额外开销,例如 Scene View object-id pass 的首帧资源上传,但这属于次要问题。

2.3 为什么 Unity 看起来没这么卡

Unity 在打开 scene 时,通常不是重新解析 obj/png/jpg 源文件,而是:

  • 通过 .metaguid 识别资产
  • 通过 Library 中已经存在的导入产物恢复资源
  • 只有源文件、导入设置、依赖变化时,才重新导入

也就是说:

  • Unity 把“导入”提前做了
  • 你当前的 editor 把“导入”塞进了“打开场景”

这就是 Unity 里大场景不一定很卡,而你这里小 scene 也会卡的根本原因。


3. 本地 Unity 项目观察结果

本次方案参考了本地 Unity 项目:

  • D:\Xuanchi\Main\Editor

从该项目可以明确观察到以下结构:

3.1 .meta 文件

Unity 的每个源资源旁边都有 .meta 文件,最核心字段是:

  • guid
  • importer 类型
  • importer 设置

例如 backpack.obj.meta 中可以看到:

  • guid
  • ModelImporter
  • 材质导入模式
  • mesh 导入配置
  • tangent/normal 配置

这说明 Unity 的源资源身份和导入配置是放在源资源旁的,而不是临时内存状态。

3.2 Library 的职责分层

在本地 Unity 项目里,Library 下可以看到:

  • SourceAssetDB
  • ArtifactDB
  • Artifacts
  • ShaderCache
  • ScriptAssemblies
  • StateCache

这说明 Unity 的 Library 不是一个单独的缓存目录,而是多个子系统缓存的集合。

3.3 本次只模仿哪一部分

本次 XCEngine 初版只模仿 Unity 的“资产导入与缓存”这一部分,不复制以下内容:

  • 脚本编译缓存
  • 包管理缓存
  • build 缓存
  • editor 状态缓存
  • shader 编译缓存

本次重点只放在:

  • Assets
  • .meta
  • SourceAssetDB
  • ArtifactDB
  • Artifacts

4. 重构目标

4.1 总目标

把当前资源系统重构为:

源文件系统资产身份系统导入系统artifact 缓存系统运行时资源加载系统场景资产引用系统

六层明确分离的正式资产管线。

4.2 直接目标

  • 打开 scene 时不再直接读取原始 obj/png/jpg
  • 首次导入后,后续打开 scene 优先命中 Library artifact
  • 文件改名/移动后scene 引用不失效
  • 模型、材质、贴图引用不再依赖路径字符串
  • 模型导入不再把所有外部贴图同步塞进 Mesh 对象里
  • 资源导入的失效条件可控、可追踪、可重建

4.3 长期目标

  • 为后续 prefab、动画、shader、音频、打包系统留出统一入口
  • 为后续后台导入、导入进度 UI、资产依赖分析、项目迁移工具提供基础设施
  • 为后续 player 构建系统提供统一的“已导入资源来源”

5. 非目标

这次重构的第一版不追求:

  • 100% 复制 Unity 内部数据库格式
  • 立即实现所有资源类型的 importer
  • 一步到位做完脚本编译缓存与 ShaderCache
  • 一次性改完整个 editor 的所有资产面板功能
  • 一开始就做压缩纹理格式转码、平台分包、远程缓存

第一版必须先把“资产导入缓存”这条主链路做对。


6. 设计原则

6.1 资产身份必须独立于路径

路径只是定位信息,不应该承担身份。

新的系统必须引入独立 AssetGUID,并把它落盘到 .meta。改名和移动只影响路径索引,不影响资产身份。

6.2 导入和运行时加载必须分离

导入器负责:

  • 读取原始源文件
  • 生成规范化中间产物
  • 记录依赖
  • 生成 artifact

运行时加载器负责:

  • 读取 artifact
  • 创建运行时资源对象
  • 加入内存 cache

不能再让一个 Load(path) 同时承担两件事。

6.3 Scene 反序列化不能立即触发重型资源导入

Scene 打开时应该只恢复引用关系和组件数据,不应在反序列化过程中同步读取大模型和大贴图源文件。

6.4 运行时资源对象不能再“顺手拥有所有关联资源”

当前最大的结构性浪费之一是:

  • Mesh 导入时直接把材质和贴图都挂进去
  • Material 运行时对象也直接持有纹理 handle

这样会导致:

  • 只需要 mesh 时,材质和贴图也跟着进来
  • 只渲染 base color 时normal/specular/roughness/ao 也同步加载

重构后应改成:

  • Mesh 只管 mesh 数据和默认材质引用
  • Material 只管材质属性和纹理引用
  • 纹理运行时加载按需触发

6.5 缓存键必须可重复计算

artifact 是否有效,必须由可重复计算的输入决定,而不是靠“我觉得没变”。

artifact key 必须包含:

  • 源文件内容 hash
  • 导入设置 hash
  • importer version
  • 依赖文件/依赖资产 hash
  • 平台或目标格式

7. 新系统的核心概念

7.1 AssetGUID

新增稳定资产 ID。

定义建议:

  • 128-bit
  • 文本形式为 32 位小写十六进制
  • .meta 首次生成时随机创建
  • 之后不再因为路径变化而变化

注意:

  • 当前 ResourceGUID 不是这个概念
  • 当前 ResourceGUID 可以暂时保留给运行时 cache 用
  • AssetGUID 用于编辑器和资产引用层

7.2 LocalID

一个源资产导入后,可能产生多个子资源。

例如一个 obj 模型可能产生:

  • 主模型资产
  • 一个或多个 Mesh
  • 一个或多个默认 Material
  • 内嵌纹理子资产

因此需要 LocalID 表示“某个资产内部的某个子对象”。

定义建议:

  • 64-bit
  • 在 importer 内根据“子资源逻辑路径”稳定生成
  • 不依赖内存地址
  • 尽量在重导入后保持稳定

7.3 AssetRef

新的统一资产引用结构:

AssetRef = { assetGuid, localID, resourceType }

所有 scene、prefab、material、component 最终都应该依赖这个结构,而不是路径字符串。

7.4 SourceAssetDB

源资产数据库,用来记录:

  • AssetGUID -> 源路径
  • 源路径 -> AssetGUID
  • .meta 内容
  • importer 类型
  • importer 设置 hash
  • 源文件指纹
  • 是否为文件/文件夹
  • 上次导入状态

7.5 ArtifactKey

导入产物的内容键。

它不是资产 GUID而是本次“导入结果”的键。

同一个 AssetGUID 在不同 importer 设置或不同依赖版本下,会产生不同 ArtifactKey

7.6 ArtifactDB

导入结果数据库,用来记录:

  • (AssetGUID, importer signature) -> ArtifactKey
  • ArtifactKey -> 产物清单
  • ArtifactKey -> 依赖集合
  • 主子资源映射
  • sub-asset LocalID -> artifact object

7.7 Artifact Store

真正落盘的导入产物目录,对应 Unity 的 Library/Artifacts

7.8 Importer

新的导入器不再等于运行时 loader而是只负责

  • 从源资源生成 artifact
  • 声明依赖
  • 产出 sub-asset 映射

7.9 Runtime Artifact Loader

新的运行时资源加载器负责:

  • 从 artifact 读取 xcmesh/xctex/xcmat/...
  • 创建 Mesh/Texture/Material 等运行时对象
  • 写入 ResourceManager

8. 总体架构

8.1 目标数据流

Assets + .meta
    ->
Project Asset Scan
    ->
SourceAssetDB
    ->
Importer Registry
    ->
ArtifactDB + Artifact Store
    ->
Runtime Artifact Loader
    ->
ResourceManager
    ->
Scene / Component / Renderer

8.2 目标职责边界

源文件层

只负责:

  • Assets 下的真实源文件
  • 同路径 .meta

编辑器资产数据库层

只负责:

  • 维护 AssetGUID
  • 管理路径与 meta
  • 决定是否需要 reimport
  • 维护依赖索引

导入层

只负责:

  • 从源文件读取
  • 生成 artifact
  • 写入 Library

运行时资源层

只负责:

  • 读取 artifact
  • 构造运行时资源对象
  • 做内存 cache

Scene/Component 层

只负责:

  • 保存 AssetRef
  • 请求资源
  • 不直接操作源文件

9. 推荐的目录结构

建议项目目录最终如下:

project/
├── Assets/
│   ├── Scenes/
│   ├── Models/
│   ├── Textures/
│   ├── Materials/
│   └── ... + 对应 .meta
├── Library/
│   ├── SourceAssetDB/
│   │   ├── assets.json
│   │   ├── guid_index.json
│   │   ├── path_index.json
│   │   └── folders.json
│   ├── ArtifactDB/
│   │   ├── artifacts.json
│   │   ├── asset_to_artifact.json
│   │   ├── dependency_index.json
│   │   └── subasset_index.json
│   ├── Artifacts/
│   │   ├── 00/
│   │   ├── 01/
│   │   ├── ...
│   │   └── ff/
│   ├── ImportLogs/
│   └── CacheVersion.json
├── .xceditor/
│   └── imgui_layout.ini
└── Project.xcproject

9.1 为什么初版不直接做成 Unity 那种单文件 DB

Unity 的 SourceAssetDBArtifactDB 在本地看起来更像单文件数据库。

但 XCEngine 初版建议先做成:

  • 同名目录
  • 多个版本化 JSON 索引文件

理由:

  • 易实现
  • 易调试
  • 易人工修复
  • 易写测试
  • 后续可以在不改外部结构的前提下替换为二进制 DB 或 SQLite

也就是说:

  • 逻辑结构模仿 Unity
  • 存储后端先选工程上可落地的版本

10. .meta 系统设计

10.1 总体目标

每个 Assets 内的文件和文件夹都应该有 .meta

.meta 要承担:

  • 稳定 AssetGUID
  • importer 类型
  • importer 设置
  • importer 版本
  • 用户数据与扩展字段

10.2 初版格式建议

为了最大程度贴近 Unity推荐采用“Unity 风格文本 + 严格子集解析器”的方式,而不是纯 JSON。

示例:

fileFormatVersion: 1
guid: 07215647f41c2504abc024d70182ba63
folderAsset: false
importer: ModelImporter
importerVersion: 1
userData:
labels: []
ModelImporter:
  materials:
    importMode: ImportAsSubAssets
    searchMode: LocalDirectory
  meshes:
    globalScale: 1.0
    generateNormals: FromSource
    generateTangents: IfNeeded
    importCameras: false
    importLights: false
    weldVertices: true

10.3 为什么不直接上完整 YAML

完整 YAML 解析超出当前工程必要范围。

初版建议:

  • 文本表现模仿 Unity
  • 解析器只支持我们定义的固定字段子集
  • 统一由 editor 写回,尽量不手写任意结构

10.4 .meta 的生成规则

  • Assets 下扫描到新文件/文件夹时,若缺少 .meta,自动生成
  • 新生成 .meta 时写入随机 AssetGUID
  • 重复 GUID 检测到时,自动再生并记录错误日志
  • 文件重命名、移动时 .meta 跟随文件一起移动
  • 文件夹同样有 .meta

11. 资产数据库设计

11.1 SourceAssetDB 记录结构

建议的 SourceAssetRecord

guid
relativePath
metaPath
isFolder
importerName
importerVersion
metaHash
sourceHash
sourceFileSize
sourceWriteTime
importSettingsHash
lastKnownArtifactKey
state

其中:

  • sourceHash 用于真正判断内容是否变化
  • sourceFileSize + sourceWriteTime 用作快速短路
  • metaHash 用于判断 importer 设置变化

11.2 ArtifactDB 记录结构

建议的 ArtifactRecord

artifactKey
assetGuid
importerName
importerVersion
targetProfile
inputSignatureHash
artifactPath
mainObjectLocalID
subAssets[]
dependencies[]
createdAt

11.3 依赖记录结构

依赖应支持两种:

  • 源文件依赖
  • 资产依赖

建议字段:

dependencyKind = SourceFile | Asset
sourcePath / assetGuid
expectedHash

12. 哈希与键设计

12.1 AssetGUID

用途:

  • 标识“这个资产是谁”

生成方式:

  • 首次生成时随机 UUID 风格 128-bit

12.2 ContentHash

用途:

  • 标识“这个文件内容是什么”

建议实现:

  • 引入 xxHashXXH3_128
  • 用于源文件内容 hash、meta 文本 hash、artifact 输入签名 hash

12.3 ArtifactKey

用途:

  • 标识“这次导入结果是什么”

建议组成:

ArtifactKey = Hash128(
    importerName,
    importerVersion,
    assetGuid,
    sourceContentHash,
    importSettingsHash,
    dependencySignatureHash,
    targetProfile,
    artifactSchemaVersion
)

12.4 Runtime Resource GUID

当前 ResourceGUID 不应再直接由源路径生成。

重构后建议:

  • ResourceGUID 用于运行时对象 cache
  • artifactKey + localID + resourceType 生成

这样:

  • 资产改名不会改变运行时对象身份
  • 同一 artifact 的 sub-asset 可以稳定寻址

13. Artifact Store 设计

13.1 落盘组织方式

建议采用哈希分片目录:

Library/Artifacts/07/07ab23cd.../

目录名是完整 artifactKey,前两位做分片。

13.2 每个 artifact 包内的内容

建议不要做成一个不可读 blob而是一个小型包目录

Library/Artifacts/07/07ab23cd.../
├── artifact.json
├── main.xcasset
├── mesh_1001.xcmesh
├── material_2001.xcmat
├── material_2002.xcmat
└── embedded_texture_3001.xctex

这样做的原因:

  • 易调试
  • 易增量扩展
  • 易人工清理
  • 适合初版实现

13.3 artifact.json 内容

建议至少包含:

  • artifactKey
  • assetGuid
  • importer
  • importerVersion
  • schemaVersion
  • mainObject
  • subAssets
  • dependencies
  • producedFiles

14. Importer 体系重构

14.1 现有问题

当前 MeshLoaderTextureLoader 直接从源文件创建运行时对象。

这导致:

  • 每次首次访问都重复导入
  • 无法形成 Library artifact
  • 无法精确 reimport

14.2 新的职责拆分

建议把当前体系拆成两类接口:

IAssetImporter

负责:

  • 从源文件导入到 artifact
  • 生成 sub-asset
  • 记录依赖

IArtifactLoader

负责:

  • 从 artifact 还原为运行时资源对象

14.3 推荐接口草案

IAssetImporter
- GetImporterName()
- GetImporterVersion()
- CanImport(sourcePath, meta)
- Import(request, output)
- GatherDependencies(request, outDeps)
- BuildDefaultMeta()

IArtifactLoader
- GetResourceType()
- CanLoad(artifactObject)
- LoadFromArtifact(artifactObject, loadContext)

14.4 对现有类的映射

建议演进为:

  • TextureLoader 拆为 TextureImporter + TextureArtifactLoader
  • MeshLoader 拆为 ModelImporter + MeshArtifactLoader
  • MaterialLoader 拆为 MaterialImporter + MaterialArtifactLoader

15. 各资产类型的初版实现细节

15.1 Texture 资产

15.1.1 输入

  • .png
  • .jpg
  • .jpeg
  • 后续再扩展其它格式

15.1.2 输出

建议输出:

  • xctex 二进制文件
  • 可选 artifact.json

15.1.3 初版 xctex 内容

  • magic
  • schemaVersion
  • textureFormat
  • width
  • height
  • mipCount
  • colorSpace
  • importFlags
  • payloadOffset
  • payloadSize
  • pixel/compressed data

15.1.4 初版策略

初版先不做 GPU 压缩纹理缓存,先做:

  • RGBA8_UNORM
  • 可选 sRGB 标记
  • 可选 mipmap 预生成

先把“只导一次、只读 artifact”做对再谈 BC 压缩。

15.2 Material 资产

15.2.1 当前问题

当前 Material 运行时对象直接持有纹理 handle容易在加载材质时把所有纹理一并拉进来。

15.2.2 重构目标

Material artifact 应该存:

  • shader 标识
  • render state
  • float/int/vector 属性
  • 纹理槽位对应的 AssetRef<Texture>

而不是直接保存已加载的纹理对象。

15.2.3 运行时行为

运行时 Material 应支持:

  • 保存纹理引用
  • 延迟请求实际 Texture
  • 由渲染管线按需解析纹理

这样可以避免打开 scene 时把所有材质贴图全同步加载。

15.3 Model 资产

15.3.1 当前问题

当前 MeshLoader 会:

  • 读取 obj
  • 生成 mesh
  • 导入材质
  • 导入贴图
  • 把材质和贴图一起挂到 Mesh

这会造成过度加载。

15.3.2 新设计

ModelImporter 应该把模型导入成一个 artifact 包,内部可包含多个 sub-asset

  • 主模型对象
  • 一个或多个 Mesh sub-asset
  • 零个或多个默认 Material sub-asset
  • 零个或多个内嵌纹理 sub-asset

外部贴图不应再被复制进入 mesh runtime 对象。

15.3.3 外部贴图处理

如果 obj/mtl 引用的是外部贴图文件:

  • 先通过相对路径解析到 Assets 内真实资源
  • 找到该贴图的 AssetGUID
  • 在导出的 Material sub-asset 中写入 AssetRef<Texture>
  • 不在 model artifact 中嵌入外部贴图像素数据

只有模型文件内嵌的纹理,才作为 model 的 sub-asset 输出。

15.3.4 xcmesh 内容建议

  • header
  • vertex buffer layout
  • vertex count
  • index count
  • bounds
  • sections
  • 默认材质 AssetRef
  • vertex data blob
  • index data blob

15.3.5 mesh runtime 对象要求

新的 Mesh 运行时对象应该:

  • 持有顶点、索引、section、bounds
  • 可选保存默认材质引用
  • 不再持有 Texture*
  • 不再默认拥有一堆已创建好的 Material*

15.4 Scene 资产

15.4.1 这次不把 Scene 打成必须的二进制 artifact

初版不要求 scene 一定导成二进制 artifact。

原因:

  • 当前性能瓶颈不在 scene 文本解析
  • 瓶颈在 scene 打开时触发原始模型和贴图导入

所以初版 scene 可以继续保留 .xc 文本格式,但必须改引用方式。

15.4.2 Scene 引用重构

scene 中不应再保存:

  • mesh=Assets/Models/backpack/backpack.obj

而应改成:

  • meshGuid=<guid>
  • meshLocalID=<id>

或者后续再统一为通用 AssetRef 文本格式。

15.4.3 兼容期

初版建议保留一段兼容窗口:

  • 读取优先使用 guid + localID
  • 如果没有,再回退读取旧路径字段
  • 迁移完成后,写出只写新格式

16. 组件层重构要求

16.1 MeshFilterComponent

当前问题:

  • 反序列化时立刻同步 Load<Mesh>()

新要求:

  • 反序列化只恢复 AssetRef<Mesh>
  • 不立即导入或加载
  • GetMesh() 可做 lazy resolve
  • editor 可根据需要同步或异步请求资源

16.2 MeshRendererComponent

当前问题:

  • 材质路径字符串 + 反序列化时直接同步加载

新要求:

  • 保存 AssetRef<Material> 列表
  • 反序列化只恢复引用,不立即创建资源
  • 渲染时按需 resolve

16.3 为什么这一步必须做

如果不改组件层,即使有 Library

  • scene 打开时仍会同步加载所有资源
  • 只是“从 artifact 同步加载”,但主线程仍会卡

所以这次重构不只是 cache 落盘,还必须把组件反序列化改成纯数据恢复。


17. Runtime 资源加载重构

17.1 ResourceManager 的新职责

重构后 ResourceManager 应只负责:

  • 运行时对象 cache
  • artifact object 到 runtime object 的映射
  • 引用计数
  • 卸载策略

不再直接负责:

  • 解析 .obj
  • 解码 .png/.jpg
  • 决定 asset 是否需要 reimport

17.2 AssetDatabase 与 ResourceManager 的关系

建议关系:

  • AssetDatabase 负责编辑器世界的资产身份与 artifact 定位
  • ResourceManager 负责运行时对象生命周期

流程:

  1. Component 持有 AssetRef
  2. 请求资源时交给 AssetDatabase
  3. AssetDatabase 找到当前有效 artifact object
  4. ResourceManagerIArtifactLoader 加载 artifact
  5. 缓存 runtime object

17.3 渲染时的按需纹理解析

例如 forward pipeline 只需要 baseColorTexture 时:

  • 只解析该槽位对应的 AssetRef<Texture>
  • 不应自动加载 normal/specular/occlusion

这一步是解决“模型材质导入过重”的关键。


18. 依赖系统设计

18.1 为什么一定要有依赖系统

没有依赖系统,就无法正确回答:

  • obj 改了要不要重导?
  • mtl 改了要不要重导?
  • 外部贴图改了要不要重导?
  • importer 设置改了要不要重导?
  • 导入器版本升级了要不要重导?

18.2 依赖类型

初版至少支持:

  • 主源文件依赖
  • 侧车文件依赖
  • 资产依赖
  • importer 版本依赖

18.3 以 OBJ 为例

一个 backpack.obj 的导入依赖可能包含:

  • backpack.obj
  • backpack.mtl
  • .mtl 解析出的外部贴图路径
  • ModelImporter 的导入设置 hash
  • ModelImporter 版本号

18.4 失效规则

以下任一变化应触发 reimport

  • 源文件内容 hash 变化
  • .meta 设置变化
  • importer version 变化
  • 依赖文件内容变化
  • 依赖资产引用变化
  • artifact 文件缺失
  • artifact schema version 不匹配

19. 异步导入与异步加载设计

19.1 当前问题

当前 AsyncLoader 不是真正的后台导入系统。

19.2 新的任务模型

建议分成两类任务:

Import Task

负责:

  • 检查 artifact 是否失效
  • 读取源文件
  • 执行 importer
  • 写 Library

Runtime Load Task

负责:

  • 从 artifact 读取
  • 创建运行时对象

19.3 初版落地策略

建议分两阶段:

第一阶段

  • 先做正式持久化 Library
  • artifact miss 时允许同步 reimport
  • 目标是第二次打开同资源时显著加速

第二阶段

  • 引入后台 import 队列
  • scene 打开时只恢复引用并排队资源
  • 用 placeholder 和进度状态代替主线程长阻塞

这样风险更低,也更容易调试。


20. Editor 侧流程重构

20.1 打开项目

打开项目时应执行:

  1. 确保 Assets/ 存在
  2. 确保 Library/ 存在
  3. 扫描 Assets/
  4. 自动补齐缺失 .meta
  5. 构建或修复 SourceAssetDB
  6. 校验 ArtifactDB
  7. 清理明显损坏或孤立的 artifact 记录

20.2 新增/导入文件

新文件出现时:

  • 自动生成 .meta
  • 分配 AssetGUID
  • 进入 SourceAssetDB
  • 标记为 NeedsImport

20.3 重命名/移动文件

在 editor 内部重命名/移动时:

  • 文件与 .meta 一起移动
  • AssetGUID 保持不变
  • 只更新路径索引
  • scene 不需要改引用

这是新系统最核心的收益之一。

20.4 删除文件

删除文件时:

  • SourceAssetDB 标记删除
  • ArtifactDB 中对应 artifact 标记为 orphan
  • 后台或手动 GC 清理 artifact 文件

20.5 Reimport

需要支持:

  • Reimport 当前资产
  • Reimport All
  • 查看导入日志
  • 查看当前 artifact key
  • 查看依赖列表

21. 场景与序列化迁移方案

21.1 目标

把 scene 与 prefab 从路径引用迁移到 AssetRef

21.2 迁移策略

推荐采用三阶段迁移:

阶段 A双读

  • 读新格式
  • 读不到则回退旧路径格式

阶段 B双写

  • 新保存写 guid + localID
  • 可选保留 pathFallback

阶段 C单写

  • 正式只写新格式
  • 旧格式仅保留读兼容

21.3 迁移工具

需要提供一个项目级迁移工具:

  • 扫描所有 scene / prefab / material
  • 尝试把路径解析成 AssetGUID
  • 找不到则报错并生成报告
  • 成功后回写新引用

22. 对当前 Backpack 问题的直接收益

backpack.obj 为例,重构后流程将变为:

第一次导入

  • ModelImporter 读取 backpack.obj
  • 解析 backpack.mtl
  • 为模型生成 Mesh artifact
  • 为默认材质生成 Material sub-asset
  • 外部贴图只写成 AssetRef<Texture>
  • 贴图由各自的 TextureImporter 生成单独 artifact
  • 导入结果写入 Library/Artifacts

之后打开 scene

  • scene 只恢复 AssetRef<Mesh>
  • editor 查 ArtifactDB
  • 直接从 xcmesh 读取 mesh
  • 材质只恢复引用
  • forward pipeline 只在需要时解析 baseColorTexture
  • 不再重新解析原始 obj
  • 不再重新解码所有原始 jpg/png

也就是说:

  • 首次导入仍然可能慢
  • 但第二次及以后scene 打开不应再把 OBJ/贴图导入当场重做

23. 推荐的模块拆分

23.1 Engine 层新增/调整

建议新增或调整以下模块:

engine/include/XCEngine/Core/Asset/
├── AssetGUID.h
├── AssetRef.h
├── ArtifactKey.h
├── ArtifactManifest.h
├── IArtifactLoader.h
└── RuntimeAssetResolver.h
engine/src/Core/Asset/
├── AssetGUID.cpp
├── AssetRef.cpp
├── ArtifactKey.cpp
├── ArtifactManifest.cpp
└── RuntimeAssetResolver.cpp

23.2 Editor 层新增模块

建议新增:

editor/src/AssetPipeline/
├── AssetMetaSerializer.h/.cpp
├── SourceAssetDatabase.h/.cpp
├── ArtifactDatabase.h/.cpp
├── ArtifactStore.h/.cpp
├── AssetImporterRegistry.h/.cpp
├── TextureImporter.h/.cpp
├── ModelImporter.h/.cpp
├── MaterialImporter.h/.cpp
├── AssetDependencyScanner.h/.cpp
├── ImportScheduler.h/.cpp
├── ImportLogger.h/.cpp
└── ProjectAssetScanner.h/.cpp

23.3 对现有模块的调整

  • ResourceManager 从“源文件加载器入口”改为“artifact runtime cache”
  • MeshLoader 拆分
  • TextureLoader 拆分
  • MaterialLoader 拆分
  • MeshFilterComponent 改为存 AssetRef<Mesh>
  • MeshRendererComponent 改为存 AssetRef<Material>
  • ProjectManager 扫描时支持 .meta
  • Scene 序列化升级版本

24. 分阶段实施计划

阶段 0基础类型与工程边界收口

目标:

  • 引入 AssetGUID
  • 引入 AssetRef
  • 把“路径 hash 充当资产身份”的设计正式停止

产出:

  • AssetGUID 类型
  • AssetRef 类型
  • ResourceGUIDAssetGUID 的职责拆分

当前状态:

  • 已完成。
  • 已新增 AssetGUID / AssetRef 基础类型,并开始接入运行时加载链路。

阶段 1.meta 与 SourceAssetDB

目标:

  • Assets 中所有文件/文件夹补齐 .meta
  • 建立 guid/path/meta/importer 索引

产出:

  • .meta 读写器
  • 资产扫描器
  • SourceAssetDB
  • 资产移动/重命名时 GUID 保持不变

当前状态:

  • 已完成。
  • 已实现 .meta 自动生成、读取、写回与资产扫描。
  • 已在 ProjectManager 中处理 .meta sidecar 的删除、移动、重命名与隐藏显示。

阶段 2Artifact Store 与 ArtifactDB

目标:

  • 把导入结果正式落盘进 Library/Artifacts
  • ArtifactDB 记录导入产物和依赖

产出:

  • ArtifactKey
  • ArtifactDB
  • ArtifactStore
  • 依赖与失效判断

当前状态:

  • 已完成。
  • 已落地 Library/SourceAssetDBLibrary/ArtifactDBLibrary/Artifacts 三层结构。
  • 已实现基于 sourceHash/metaHash/importerVersion/size/writeTime 的失效判断。

阶段 3TextureImporter

目标:

  • 纹理先走通从源文件到 artifact 的闭环

产出:

  • TextureImporter
  • TextureArtifactLoader
  • xctex 格式

当前状态:

  • 已完成。
  • 当前纹理资源首次导入时会生成 xctex,后续优先从 artifact 读取。

阶段 4ModelImporter

目标:

  • 模型导入改为 artifact 生成,不再 runtime 直接读源 obj

产出:

  • ModelImporter
  • xcmesh 格式
  • 默认材质 sub-asset
  • 外部贴图改成 AssetRef<Texture>

当前状态:

  • 已完成初版。
  • 当前模型首次导入时会生成 main.xcmesh 与关联 texture_N.xctex
  • 仍未完成“外部贴图全局去重 + 材质独立 artifact + 真正按引用延迟加载”的最终形态,因此这里只能算阶段 4 初版完成。

阶段 5Material 系统 lazy 引用化

目标:

  • 材质不再一加载就拉起所有纹理

产出:

  • xcmat
  • 纹理引用延迟解析
  • Material runtime 结构调整

当前状态:

  • 部分完成。
  • 已补齐材质 artifact 读写所需的 tag/property/texture binding 访问接口。
  • xcmat 尚未落地,贴图仍是 eager load不是最终目标状态。

阶段 6Scene / Component 引用迁移

目标:

  • 路径引用改成 guid + localID

产出:

  • 新 scene 序列化格式
  • 旧路径兼容读取
  • 迁移工具

当前状态:

  • 部分完成。
  • MeshFilterComponentMeshRendererComponent 已双写 path + AssetRef,并保留旧数据兼容。
  • Scene 全量序列化升级、统一迁移工具、其他组件资产引用迁移尚未完成。

阶段 7异步导入与异步加载

目标:

  • 打开 scene 主线程只恢复引用,不做大规模阻塞导入

产出:

  • import task 队列
  • runtime load task 队列
  • placeholder 与状态提示

当前状态:

  • 部分完成。
  • AsyncLoader 已具备真实 worker thread、pending/completed queue、主线程 completion callback pump。
  • editor SceneManager 已在 LoadScene(...)RestoreSceneSnapshot(...) 路径启用 deferred scene load scope。
  • MeshFilterComponentMeshRendererComponent 已支持“反序列化只恢复引用并提交异步请求,真正 handle 接管延后到完成回调之后”。
  • 仍未完成通用 import job 系统、统一 placeholder 状态、批量队列调度、更多资源类型的异步恢复,以及失败/进度 UI。

阶段 8清理、GC、工具与可视化

目标:

  • 系统可维护、可调试、可清理

产出:

  • orphan artifact 清理
  • Reimport All
  • 依赖图查看
  • 导入日志面板

当前状态:

  • 未开始。

25. 测试与验证方案

25.1 单元测试

必须补这些测试:

  • .meta 缺失时自动生成 GUID
  • 重命名/移动后 GUID 保持不变
  • 同一资产不同 importer 设置产生不同 artifact key
  • source 改变时 artifact 失效
  • 依赖贴图变化时 material 或 model 重新判定
  • sub-asset localID 稳定性
  • scene 新旧格式兼容读取

25.2 集成测试

必须补这些流程测试:

  • 首次打开 backpack scene 触发导入
  • 第二次打开 backpack scene 不再读取原始 obj
  • 仅改 diffuse.jpg 时,不重新导入无关资产
  • 移动模型文件后 scene 仍能打开
  • 删除 artifact 后系统能够自动重建

25.3 性能验证

必须记录的指标:

  • 首次导入耗时
  • warm 打开 scene 耗时
  • 主线程阻塞时间
  • texture 解码时间
  • model import 时间
  • GPU 首帧上传时间

建议加入正式 instrumentation

  • Scene open
  • Asset resolve
  • Import task
  • Artifact load
  • GPU upload

26. 风险与注意事项

26.1 不能再把路径当 GUID

这是本次重构的底线。如果不先解决这个问题,后面所有缓存都会在重命名/移动后变脆。

26.2 sub-asset LocalID 稳定性很重要

如果 obj 重导后 mesh localID 改了,即使 AssetGUID 不变scene 里的引用仍然会断。

因此 localID 的生成规则必须:

  • 基于稳定逻辑路径
  • 不依赖运行时顺序
  • 在 importer 内版本化

26.3 初版不要急着做复杂压缩

真正需要优先解决的是:

  • 导入和加载分离
  • Library 持久化
  • scene 引用改成 GUID

纹理压缩和高级优化可以后做。

26.4 当前 Preview 缓存可以暂不并入新 Library

当前 editor 已经有 .xceditor/thumbs 预览缓存。

初版资产导入缓存系统可以先不动它,后续再考虑统一到 Library/PreviewCache

26.5 需要预留 schema/version

以下内容都必须版本化:

  • .meta format version
  • artifact manifest version
  • xcmesh/xctex/xcmat format version
  • importer version
  • project Library cache version

否则后续升级会非常痛苦。


27. 最终预期结果

这次重构完成后,系统应达到以下效果:

  • 打开 scene 时,不再直接解析源 obj/png/jpg
  • 资产改名或移动后scene 引用仍然稳定
  • 首次导入慢,但只慢一次
  • 后续打开 scene 优先走 Library
  • MeshMaterialTexture 的职责边界清晰
  • editor 可以正式支持 Reimport、依赖跟踪、缓存清理
  • 后续扩展 prefab、动画、打包、后台导入时不需要推倒重来

一句话总结:

这次要做的不是“给现在的 ResourceManager 加缓存”,而是把 XCEngine 的资产系统重构成:

Assets + .meta
    ->
SourceAssetDB
    ->
Importer
    ->
ArtifactDB + Artifacts
    ->
Artifact Loader
    ->
ResourceManager
    ->
Scene / Renderer

只有这样,当前“打开 scene 时同步导入 obj 模型导致卡顿”的问题,才会被系统性解决,而不是被局部绕开。