Add model and GaussianSplat asset pipelines
This commit is contained in:
693
docs/plan/3DGS专用PLY导入器与GaussianSplat资源缓存正式化计划_2026-04-10.md
Normal file
693
docs/plan/3DGS专用PLY导入器与GaussianSplat资源缓存正式化计划_2026-04-10.md
Normal file
@@ -0,0 +1,693 @@
|
||||
# 3DGS 专用 PLY 导入器与 GaussianSplat 资源缓存正式化计划
|
||||
|
||||
日期:2026-04-10
|
||||
|
||||
## 1. 文档定位
|
||||
|
||||
这份计划只覆盖 3DGS 落地中的前两层基础设施:
|
||||
|
||||
1. `3DGS 专用 PLY importer`
|
||||
2. `GaussianSplat 资源 / Artifact / ResourceManager / 缓存层`
|
||||
|
||||
这份计划明确不讨论以下内容:
|
||||
|
||||
1. 不实现最终的 3DGS 渲染 pass
|
||||
2. 不实现 editor 里的 3DGS 编辑工具
|
||||
3. 不实现 cutout、selection、导出、相机激活等 Unity 参考项目中的高级功能
|
||||
4. 不提前把 3DGS 强行塞进现有 mesh / volume 路径
|
||||
|
||||
这份计划的目标不是“先把 `.ply` 读出来”,而是把 3DGS 资产链正式纳入引擎现有的资源系统,使它从一开始就是一条可缓存、可复用、可异步、可测试、可长期维护的正式路径。
|
||||
|
||||
---
|
||||
|
||||
## 2. 当前参考与现状
|
||||
|
||||
当前参考工程是:
|
||||
|
||||
1. [mvs/3DGS-Unity](D:/Xuanchi/Main/XCEngine/mvs/3DGS-Unity)
|
||||
|
||||
当前测试样本是:
|
||||
|
||||
1. [room.ply](D:/Xuanchi/Main/XCEngine/mvs/3DGS-Unity/room.ply)
|
||||
|
||||
当前已确认的事实:
|
||||
|
||||
1. `3DGS-Unity` 并不是运行时直接渲染 `.ply`,而是先把 `.ply` 转成更接近 GPU 消费形态的运行时资产。
|
||||
2. 它的导入工作流核心在 [GaussianSplatAssetCreator.cs](D:/Xuanchi/Main/XCEngine/mvs/3DGS-Unity/Editor/GaussianSplatAssetCreator.cs)。
|
||||
3. 它的 `.ply` 读取器 [PLYFileReader.cs](D:/Xuanchi/Main/XCEngine/mvs/3DGS-Unity/Editor/Utils/PLYFileReader.cs) 本质上是一个偏工程化的快速路径,不是健壮的通用 PLY 解析器。
|
||||
4. 它的运行时资产 [GaussianSplatAsset.cs](D:/Xuanchi/Main/XCEngine/mvs/3DGS-Unity/Runtime/GaussianSplatAsset.cs) 已经把数据拆成了 `pos / other / sh / color / chunk` 几类 GPU 资源。
|
||||
5. 它的运行时渲染 [GaussianSplatRenderer.cs](D:/Xuanchi/Main/XCEngine/mvs/3DGS-Unity/Runtime/GaussianSplatRenderer.cs) 依赖 compute、StructuredBuffer、RawBuffer、procedural draw、fullscreen composite。
|
||||
|
||||
对当前引擎的现状判断:
|
||||
|
||||
1. 引擎已经具备 `StructuredBuffer / RawBuffer / RWStructuredBuffer / RWRawBuffer` 级别的 shader 资源识别能力。
|
||||
2. 引擎已经具备 compute shader 与 `Dispatch` 的 RHI 基础能力。
|
||||
3. 引擎已经具备 runtime material buffer binding 能力。
|
||||
4. 引擎已经具备 `AssetDatabase -> Library/Artifacts -> ResourceManager` 的正式资源链。
|
||||
5. 引擎当前还没有 `GaussianSplat` 这种正式资源类型。
|
||||
6. 引擎当前还没有 3DGS 专用 importer、artifact schema、loader、GPU residency cache。
|
||||
|
||||
---
|
||||
|
||||
## 3. 本轮最核心的架构决策
|
||||
|
||||
### 3.1 不允许运行时直接消费 `.ply`
|
||||
|
||||
正式方案必须是:
|
||||
|
||||
1. `Assets/*.ply`
|
||||
2. 经过 `GaussianSplatImporter`
|
||||
3. 生成 `Library/Artifacts/.../main.xcgsplat`
|
||||
4. 运行时只加载 `xcgsplat`
|
||||
|
||||
不允许的错误方案:
|
||||
|
||||
1. renderer 首次遇到 `.ply` 时再现场解析
|
||||
2. component 里直接持有 `.ply` 文件句柄
|
||||
3. 把 `.ply` 读取逻辑塞进 render pass
|
||||
4. 为了尽快出图先做“临时直接加载 `.ply`”然后以后再回收
|
||||
|
||||
原因很明确:
|
||||
|
||||
1. `.ply` 是 source asset,不是 runtime-ready asset
|
||||
2. 直接 runtime 解析会破坏 `AssetDatabase / ResourceManager / Library` 体系
|
||||
3. 3DGS 数据量远大于普通 mesh,更不能把 source 解析和 GPU 上传压到 draw path 上
|
||||
|
||||
### 3.2 不做通用 PLY 导入器,先做 3DGS 专用 PLY 导入器
|
||||
|
||||
本轮 importer 的职责不是支持一切 PLY 变体,而是支持当前 3DGS 工作流所需的那类 PLY。
|
||||
|
||||
正式边界是:
|
||||
|
||||
1. 支持 `binary_little_endian`
|
||||
2. 只关心 `element vertex`
|
||||
3. 通过属性名映射识别 3DGS 语义字段
|
||||
4. 对不支持的属性布局给出明确错误
|
||||
|
||||
不做的事:
|
||||
|
||||
1. 不支持带面片索引的通用模型 PLY
|
||||
2. 不支持 ASCII PLY
|
||||
3. 不支持任意 list property
|
||||
4. 不支持“能读但语义不清晰”的模糊推断
|
||||
|
||||
这不是退让,而是边界明确。当前目标是 3DGS 正式化,不是通用点云 SDK。
|
||||
|
||||
### 3.3 运行时正式资源类型命名为 `GaussianSplat`
|
||||
|
||||
建议引入:
|
||||
|
||||
1. `ResourceType::GaussianSplat`
|
||||
2. `Resources::GaussianSplat`
|
||||
3. `GaussianSplatLoader`
|
||||
4. `GaussianSplatImporter`
|
||||
|
||||
不建议把运行时资源叫:
|
||||
|
||||
1. `GaussianSplatAsset`
|
||||
2. `GaussianAsset`
|
||||
3. `PLYAsset`
|
||||
|
||||
原因:
|
||||
|
||||
1. 引擎当前 `ResourceType` 命名体系都是运行时资源名,不是 editor 资产名
|
||||
2. `Asset` 更适合出现在导入流程和文档语义中,不适合塞进正式 runtime `ResourceType`
|
||||
|
||||
### 3.4 Artifact 采用单文件主 artifact,而不是 Unity 式多 TextAsset 拼装
|
||||
|
||||
建议正式主 artifact 为:
|
||||
|
||||
1. `main.xcgsplat`
|
||||
|
||||
不建议照抄 Unity MVS 的多文件拆分为:
|
||||
|
||||
1. `*_pos.bytes`
|
||||
2. `*_other.bytes`
|
||||
3. `*_sh.bytes`
|
||||
4. `*_col.bytes`
|
||||
5. `*_chk.bytes`
|
||||
|
||||
Unity 那样做是受 Unity 资产模型约束。我们自己的引擎不需要跟着它的工程妥协走。
|
||||
|
||||
本轮更合理的正式方案是:
|
||||
|
||||
1. 单个 `xcgsplat` 文件包含 header、section table、payload
|
||||
2. loader 一次读入 metadata,按 section 定位各 payload
|
||||
3. 后续如果要做分段流式或 memory mapping,再在 schema 上扩展,不先把文件形态做碎
|
||||
|
||||
这样做的好处:
|
||||
|
||||
1. artifact 边界清晰
|
||||
2. `ArtifactDB` 记录简单
|
||||
3. 依赖跟踪简单
|
||||
4. reimport 稳定
|
||||
5. 不会出现多 sidecar 丢失或部分过期的问题
|
||||
|
||||
---
|
||||
|
||||
## 4. 对参考 MVS 的正式吸收方式
|
||||
|
||||
`mvs/3DGS-Unity` 里真正值得吸收的是流程,不是实现细节原样照搬。
|
||||
|
||||
本轮吸收的内容:
|
||||
|
||||
1. 输入语义
|
||||
2. 数据重排思路
|
||||
3. 运行时数据拆分维度
|
||||
4. `chunk` 概念
|
||||
5. 颜色纹理化而不是纯 buffer 化
|
||||
6. 未来 compute 排序与 view-data 预处理对资源格式的需求
|
||||
|
||||
本轮不直接照搬的内容:
|
||||
|
||||
1. Unity 的 `TextAsset` 资产组织方式
|
||||
2. 依赖 `UnsafeUtility.SizeOf<InputSplatData>() == vertexStride` 的固定内存布局导入
|
||||
3. editor 窗口与工具链
|
||||
4. HDRP/URP feature 接入方式
|
||||
5. 所有编辑态 GPU buffer
|
||||
|
||||
简单说:
|
||||
|
||||
1. 流程借鉴
|
||||
2. 数据语义借鉴
|
||||
3. 工程架构不照抄 Unity 的壳
|
||||
|
||||
---
|
||||
|
||||
## 5. `room.ply` 的正式支持目标
|
||||
|
||||
当前基线样本 [room.ply](D:/Xuanchi/Main/XCEngine/mvs/3DGS-Unity/room.ply) 已确认包含如下字段:
|
||||
|
||||
1. `x y z`
|
||||
2. `nx ny nz`
|
||||
3. `f_dc_0..2`
|
||||
4. `f_rest_0..44`
|
||||
5. `opacity`
|
||||
6. `scale_0..2`
|
||||
7. `rot_0..3`
|
||||
|
||||
本轮 importer 至少必须把这类文件稳定导入。
|
||||
|
||||
本轮导入结果必须包含:
|
||||
|
||||
1. splat 数量
|
||||
2. bounds
|
||||
3. position
|
||||
4. rotation
|
||||
5. scale
|
||||
6. opacity
|
||||
7. color / dc0
|
||||
8. SH 数据
|
||||
9. 供后续 chunk 化、排序、view-data 预计算所需的稳定 layout
|
||||
|
||||
本轮不要求:
|
||||
|
||||
1. 用 room.ply 直接出图
|
||||
2. 完成 chunk 压缩优化后的最终视觉验证
|
||||
|
||||
但必须做到:
|
||||
|
||||
1. room.ply 可以稳定导入成正式 artifact
|
||||
2. runtime 可以正式加载该 artifact
|
||||
3. 资源缓存与异步链路已经为后续渲染阶段准备好
|
||||
|
||||
---
|
||||
|
||||
## 6. 目标资源模型设计
|
||||
|
||||
### 6.1 `Resources::GaussianSplat`
|
||||
|
||||
建议 `GaussianSplat` 运行时资源至少包含:
|
||||
|
||||
1. `splatCount`
|
||||
2. `boundsMin / boundsMax`
|
||||
3. `dataFormatVersion`
|
||||
4. `positionFormat`
|
||||
5. `otherFormat`
|
||||
6. `colorFormat`
|
||||
7. `shFormat`
|
||||
8. `chunkCount`
|
||||
9. `cameraInfoCount`
|
||||
10. 各 section 的只读数据视图
|
||||
|
||||
这里的 section 建议为:
|
||||
|
||||
1. `positions`
|
||||
2. `other`
|
||||
3. `color`
|
||||
4. `sh`
|
||||
5. `chunks`
|
||||
6. `cameras`
|
||||
|
||||
### 6.2 `other` 的语义边界
|
||||
|
||||
建议 `other` 区域承担:
|
||||
|
||||
1. rotation
|
||||
2. scale
|
||||
3. opacity
|
||||
4. 可选 SH 索引或 chunk 相关辅助字段
|
||||
|
||||
原因:
|
||||
|
||||
1. 这与参考 MVS 的消费模型更接近
|
||||
2. compute 阶段天然会把位置和其它数据拆开消费
|
||||
3. 将来做压缩时,位置和其它数据的量化策略也不同
|
||||
|
||||
### 6.3 `color` 保持纹理友好布局
|
||||
|
||||
建议 `color` section 不是简单“每 splat 一行 float4”,而是直接按运行时纹理消费友好的布局存储。
|
||||
|
||||
原因:
|
||||
|
||||
1. 参考 MVS 最终就是把颜色上传成纹理
|
||||
2. 对 3DGS 而言,颜色作为纹理读取是合理路径
|
||||
3. 如果导入期就固化好 texel 布局,运行时不必再做一次昂贵重排
|
||||
|
||||
### 6.4 `chunks` 作为正式字段预留
|
||||
|
||||
即使第一阶段先允许 `chunkCount == 0`,artifact schema 也要正式留出 chunk 区域。
|
||||
|
||||
因为:
|
||||
|
||||
1. chunk 数据不是可有可无的小优化,它影响后续压缩、解码和排序输入
|
||||
2. 后面一旦渲染 pass 接上,就会很自然依赖 chunk
|
||||
3. 现在先把 schema 打对,比后面再迁移 artifact 版本更划算
|
||||
|
||||
---
|
||||
|
||||
## 7. Importer 设计
|
||||
|
||||
### 7.1 引入 `GaussianSplatImporter`
|
||||
|
||||
`AssetDatabase` 对 `.ply` 的识别不应直接复用 `ModelImporter`。
|
||||
|
||||
建议规则:
|
||||
|
||||
1. `.ply` 不作为通用模型格式挂进 `ModelImporter`
|
||||
2. 本轮把 `.ply` 明确识别为 `GaussianSplatImporter`
|
||||
3. 后续如果将来要支持“通用点云 PLY”,再单独扩展,不污染当前 3DGS 主线
|
||||
|
||||
### 7.2 Header 解析不能依赖固定顺序
|
||||
|
||||
正式解析流程必须是:
|
||||
|
||||
1. 读取 header
|
||||
2. 收集 `element vertex`
|
||||
3. 收集每个 `property` 的名字、类型、偏移
|
||||
4. 建立 3DGS 语义字段到 property 的映射
|
||||
5. 校验必需字段是否完整
|
||||
|
||||
不允许的方案:
|
||||
|
||||
1. 直接假定 `InputSplatData` 与文件二进制布局完全一致
|
||||
2. 直接假定 `f_rest_*` 顺序永远固定且不校验
|
||||
3. 因为 room.ply 能过就默认所有训练器导出的 PLY 都一样
|
||||
|
||||
### 7.3 importer 输出的是 cooked runtime layout,不是 source mirror
|
||||
|
||||
导入器的正式职责不是把 `.ply` 原样搬进 artifact,而是做以下转换:
|
||||
|
||||
1. 按语义解包 source data
|
||||
2. 生成规范化内部 splat 记录
|
||||
3. 计算 bounds
|
||||
4. 可选做 Morton reorder
|
||||
5. 可选做 chunk 构建
|
||||
6. 输出运行时友好的 section layout
|
||||
|
||||
这一步就是 source -> cooked artifact,而不是 source -> source copy。
|
||||
|
||||
### 7.4 关于压缩策略
|
||||
|
||||
本轮计划分两步:
|
||||
|
||||
1. 第一阶段先实现无损或近无损基础 cooked 布局
|
||||
2. 第二阶段再把参考 MVS 的压缩格式体系正式移植进来
|
||||
|
||||
原因:
|
||||
|
||||
1. 先把 artifact、loader、cache 链路跑通
|
||||
2. 再叠加压缩和 chunking
|
||||
3. 避免 importer、artifact schema、runtime loader、未来 renderer 四件事同时出错
|
||||
|
||||
第一阶段允许:
|
||||
|
||||
1. `position = float32`
|
||||
2. `other = float32 / uint32 packed`
|
||||
3. `color = RGBA32F` 或 `RGBA16F`
|
||||
4. `sh = float32`
|
||||
5. `chunk = 0`
|
||||
|
||||
第二阶段再引入:
|
||||
|
||||
1. `Norm16 / Norm11 / Norm6`
|
||||
2. `BC7 / Norm8x4`
|
||||
3. `SH clustering`
|
||||
4. `chunk` 正式压缩路径
|
||||
|
||||
---
|
||||
|
||||
## 8. Artifact 设计
|
||||
|
||||
### 8.1 主文件
|
||||
|
||||
主文件建议:
|
||||
|
||||
1. `main.xcgsplat`
|
||||
|
||||
### 8.2 文件内容建议
|
||||
|
||||
建议 `xcgsplat` 文件包含:
|
||||
|
||||
1. 文件头
|
||||
2. schema version
|
||||
3. source metadata snapshot
|
||||
4. splat metadata
|
||||
5. section table
|
||||
6. payload blob
|
||||
|
||||
section table 至少描述:
|
||||
|
||||
1. section type
|
||||
2. byte offset
|
||||
3. byte size
|
||||
4. element count
|
||||
5. element stride
|
||||
6. format enum
|
||||
|
||||
### 8.3 version 策略
|
||||
|
||||
建议单独引入:
|
||||
|
||||
1. `kGaussianSplatArtifactSchemaVersion`
|
||||
|
||||
不要复用其它 importer 的 schema version。
|
||||
|
||||
版本提升触发条件:
|
||||
|
||||
1. section 布局改变
|
||||
2. chunk 编码改变
|
||||
3. 颜色纹理布局改变
|
||||
4. SH 格式或 camera 区块布局改变
|
||||
|
||||
### 8.4 `.meta` 设计
|
||||
|
||||
即使本轮先不做完整 Inspector,也应该为后续 importer settings 预留正式字段。
|
||||
|
||||
建议至少预留:
|
||||
|
||||
1. `reorderMorton`
|
||||
2. `buildChunks`
|
||||
3. `positionFormat`
|
||||
4. `otherFormat`
|
||||
5. `colorFormat`
|
||||
6. `shFormat`
|
||||
7. `importCameras`
|
||||
|
||||
第一阶段如果先不开放 UI,也要把默认设置结构体和 hash 纳入 artifact key 计算。
|
||||
|
||||
---
|
||||
|
||||
## 9. Loader 与 ResourceManager 接入
|
||||
|
||||
### 9.1 `GaussianSplatLoader`
|
||||
|
||||
需要新增:
|
||||
|
||||
1. `GaussianSplatLoader`
|
||||
|
||||
职责:
|
||||
|
||||
1. 读取 `xcgsplat`
|
||||
2. 构建 `Resources::GaussianSplat`
|
||||
3. 提供各 section 的稳定只读视图
|
||||
|
||||
### 9.2 `ResourceManager` 正式接入
|
||||
|
||||
正式链路应支持:
|
||||
|
||||
1. `Load<GaussianSplat>("Assets/.../room.ply")`
|
||||
2. `AssetDatabase::EnsureArtifact(...)`
|
||||
3. `ResourceManager` 实际加载 `main.xcgsplat`
|
||||
|
||||
也必须支持:
|
||||
|
||||
1. `Load<GaussianSplat>("Library/Artifacts/.../main.xcgsplat")`
|
||||
|
||||
### 9.3 不能把 GPU 上传塞进 loader
|
||||
|
||||
`GaussianSplatLoader` 只负责 CPU 运行时资源,不负责 GPU residency。
|
||||
|
||||
原因:
|
||||
|
||||
1. loader 属于资源层
|
||||
2. GPU residency 属于渲染缓存层
|
||||
3. 如果在 loader 里直接创 GPU 资源,会重复 volume 这条链已经暴露过的架构问题
|
||||
|
||||
---
|
||||
|
||||
## 10. 缓存与预热设计
|
||||
|
||||
### 10.1 资源缓存层必须提前设计 GPU residency 状态机
|
||||
|
||||
即使本轮还不接最终 render pass,也必须把状态机设计写进正式方案:
|
||||
|
||||
1. `Uninitialized`
|
||||
2. `CpuReady`
|
||||
3. `GpuUploading`
|
||||
4. `GpuReady`
|
||||
5. `Failed`
|
||||
|
||||
后续 `BuiltinGaussianSplatPass` 只能消费:
|
||||
|
||||
1. `GpuReady`
|
||||
|
||||
不允许 draw path 现场把 `CpuReady -> GpuReady` 做完。
|
||||
|
||||
### 10.2 建议新增 `CachedGaussianSplat`
|
||||
|
||||
建议未来挂在 `RenderResourceCache` 或其正式拆分后的 GPU 资源缓存模块中。
|
||||
|
||||
它至少应持有:
|
||||
|
||||
1. `posBuffer`
|
||||
2. `otherBuffer`
|
||||
3. `shBuffer`
|
||||
4. `colorTexture`
|
||||
5. `chunkBuffer`
|
||||
6. `sortKeyBuffer`
|
||||
7. `sortDistanceBuffer`
|
||||
8. `viewDataBuffer`
|
||||
9. runtime-ready flag / state
|
||||
|
||||
本轮即使还不把所有 GPU 辅助 buffer 全建出来,也要把正式边界写清楚:
|
||||
|
||||
1. asset static payload buffer/texture
|
||||
2. per-frame transient / reusable working buffer
|
||||
|
||||
### 10.3 首次可见前预热,而不是首次 draw 同步补做
|
||||
|
||||
这点必须作为硬约束写死:
|
||||
|
||||
1. `GaussianSplat` 被场景反序列化后,CPU artifact 加载完成就进入 GPU 预热队列
|
||||
2. GPU 上传在后台或明确的准备阶段完成
|
||||
3. 首次 draw 只允许跳过未 ready 对象,不允许同步创建大资源
|
||||
|
||||
原因:
|
||||
|
||||
1. 3DGS 资产通常很大
|
||||
2. room.ply 这种样本数据量已经足够把 draw path 压垮
|
||||
3. 当前 volume 这条链已经证明“首次绘制再上传”不是可接受正式方案
|
||||
|
||||
### 10.4 warm cache 验收标准
|
||||
|
||||
本轮资源 / 缓存层至少要达到:
|
||||
|
||||
1. 第二次加载 room.ply 时不重新解析 source `.ply`
|
||||
2. 直接命中 `Library/Artifacts`
|
||||
3. `ResourceManager` 不会因为 cache hit 又走 source importer
|
||||
4. 后续 GPU 预热可以稳定复用 artifact 输出
|
||||
|
||||
---
|
||||
|
||||
## 11. 测试计划
|
||||
|
||||
### 11.1 基线样本
|
||||
|
||||
统一使用:
|
||||
|
||||
1. [room.ply](D:/Xuanchi/Main/XCEngine/mvs/3DGS-Unity/room.ply)
|
||||
|
||||
它将承担:
|
||||
|
||||
1. importer 基线
|
||||
2. artifact 基线
|
||||
3. cache hit 基线
|
||||
4. future renderer 接入基线
|
||||
|
||||
### 11.2 Unit Tests
|
||||
|
||||
本轮至少要补齐以下单测:
|
||||
|
||||
1. `PLY header parser` 正确识别 `vertexCount / properties / offsets`
|
||||
2. `GaussianSplatImporter` 能正确识别 room.ply 的必需字段
|
||||
3. 缺字段时给出明确错误
|
||||
4. 非法格式时给出明确错误
|
||||
5. `xcgsplat` 写入 / 读取 roundtrip 正确
|
||||
6. `GaussianSplatLoader` 能读取 artifact 并恢复 metadata 与 section view
|
||||
7. `ResourceManager` 能从 `Assets/.../room.ply` 正式加载 `GaussianSplat`
|
||||
8. `AssetDatabase` 对 `.ply` 的 `EnsureArtifact` 能稳定复用
|
||||
|
||||
### 11.3 Integration Tests
|
||||
|
||||
本轮先做资源链集成测试,不做最终出图测试。
|
||||
|
||||
至少要有:
|
||||
|
||||
1. `room.ply -> EnsureArtifact -> Load<GaussianSplat>` 全链通过
|
||||
2. 二次加载命中 artifact,不触发 reimport
|
||||
3. 修改 source writeTime 后能触发 reimport
|
||||
4. 清库后能重建 artifact
|
||||
|
||||
### 11.4 为后续渲染阶段准备的 smoke test
|
||||
|
||||
虽然本轮不做 3DGS pass,但建议提前补一个 GPU 资源 smoke test:
|
||||
|
||||
1. 读取 `GaussianSplat`
|
||||
2. 构建最小 GPU cache entry
|
||||
3. 创建 `pos/other/sh/chunk` buffer 与 `color` texture
|
||||
4. 验证状态进入 `GpuReady`
|
||||
|
||||
这样后续 renderer 接入时,不会把“资源问题”和“渲染问题”混成一团。
|
||||
|
||||
---
|
||||
|
||||
## 12. 分阶段执行计划
|
||||
|
||||
### Phase 1:资源类型与 artifact schema 落地
|
||||
|
||||
目标:
|
||||
|
||||
1. 正式引入 `ResourceType::GaussianSplat`
|
||||
2. 正式引入 `Resources::GaussianSplat`
|
||||
3. 正式定义 `xcgsplat` artifact schema
|
||||
|
||||
任务:
|
||||
|
||||
1. 扩展 `ResourceType`
|
||||
2. 新增 `GaussianSplat` 运行时资源类
|
||||
3. 设计 artifact header 与 section table
|
||||
4. 新增 `WriteGaussianSplatArtifactFile / LoadGaussianSplatArtifact`
|
||||
|
||||
验收标准:
|
||||
|
||||
1. `xcgsplat` 可写可读
|
||||
2. 资源元数据可稳定 roundtrip
|
||||
|
||||
### Phase 2:3DGS 专用 PLY importer 正式化
|
||||
|
||||
目标:
|
||||
|
||||
1. 把 `.ply` 纳入 `GaussianSplatImporter`
|
||||
|
||||
任务:
|
||||
|
||||
1. 新增 header parser
|
||||
2. 新增 3DGS property mapping
|
||||
3. 读取 room.ply 并转换成规范化内部 splat 数据
|
||||
4. 输出基础 cooked artifact
|
||||
|
||||
验收标准:
|
||||
|
||||
1. room.ply 可稳定导入
|
||||
2. 不依赖固定 struct stride == 文件 stride
|
||||
3. 错误路径有清晰日志
|
||||
|
||||
### Phase 3:AssetDatabase / Library / ResourceManager 接入
|
||||
|
||||
目标:
|
||||
|
||||
1. 把 `GaussianSplat` 完整接进项目资源工作流
|
||||
|
||||
任务:
|
||||
|
||||
1. `.ply -> GaussianSplatImporter`
|
||||
2. `EnsureArtifact(..., ResourceType::GaussianSplat)`
|
||||
3. `GaussianSplatLoader`
|
||||
4. `Load<GaussianSplat>()`
|
||||
|
||||
验收标准:
|
||||
|
||||
1. 可以通过 `Assets/.../room.ply` 正式加载
|
||||
2. cache hit 时不重走 source parse
|
||||
|
||||
### Phase 4:资源缓存与 GPU residency 预热骨架
|
||||
|
||||
目标:
|
||||
|
||||
1. 正式建立 3DGS GPU 资源缓存的边界
|
||||
|
||||
任务:
|
||||
|
||||
1. 设计 `CachedGaussianSplat`
|
||||
2. 建立 GPU residency 状态机
|
||||
3. 实现最小 GPU 资源构建 smoke path
|
||||
4. 明确禁止 draw path 首次同步上传
|
||||
|
||||
验收标准:
|
||||
|
||||
1. room.ply 对应的 `GaussianSplat` 可以被 GPU cache 预热成 ready 状态
|
||||
2. 资源层与渲染层边界清晰
|
||||
|
||||
### Phase 5:测试补齐与收口
|
||||
|
||||
目标:
|
||||
|
||||
1. 让这条链路可回归、可持续演进
|
||||
|
||||
任务:
|
||||
|
||||
1. 补全 importer / loader / cache hit / reimport 单测与集成测试
|
||||
2. 输出阶段性说明
|
||||
3. 为后续 renderer 接入保留唯一正式资源路径
|
||||
|
||||
验收标准:
|
||||
|
||||
1. room.ply 全链路测试稳定
|
||||
2. 不存在“临时直接读 ply”的旁路
|
||||
|
||||
---
|
||||
|
||||
## 13. 明确不允许出现的临时方案
|
||||
|
||||
以下方案本轮禁止出现:
|
||||
|
||||
1. 为了尽快出图,先在 render pass 里直接解析 `.ply`
|
||||
2. 先做一个 `BinaryResource` 包 `.ply` 内容,后面再说
|
||||
3. 先把 `.ply` 当 `Mesh` 导入
|
||||
4. 把 3DGS 的 GPU buffer 直接挂在 `Material` 资源本体上作为持久化资产
|
||||
5. 首次 draw 时同步创建 `pos / other / sh / color` GPU 资源
|
||||
6. 把 `room.ply` 单独写死成特判
|
||||
|
||||
这些做法都会把本该正式化的主线重新拉回临时方案。
|
||||
|
||||
---
|
||||
|
||||
## 14. 本轮完成标志
|
||||
|
||||
当以下条件同时成立时,这份计划才算完成:
|
||||
|
||||
1. `.ply` 已正式被 `GaussianSplatImporter` 接管
|
||||
2. `GaussianSplat` 已成为正式 `ResourceType`
|
||||
3. `room.ply` 能稳定导入成 `xcgsplat`
|
||||
4. `ResourceManager` 能正式加载 `GaussianSplat`
|
||||
5. 二次加载能稳定命中 artifact
|
||||
6. GPU residency cache 骨架已经建立,不允许首次 draw 同步补做
|
||||
7. 资源层与缓存层测试已经覆盖 room.ply 主路径
|
||||
|
||||
---
|
||||
|
||||
## 15. 一句话结论
|
||||
|
||||
这条主线的第一步不是“做一个 ply 读取器”,而是把 3DGS 正式升级为引擎里的 `GaussianSplat` 资源体系:
|
||||
由 `GaussianSplatImporter` 把 `.ply` 转成 `xcgsplat` cooked artifact,由 `GaussianSplatLoader` 与 `ResourceManager` 正式接管加载,再由独立的 GPU residency cache 提前完成资源预热,为后续 3DGS 渲染 pass 提供唯一、稳定、无旁路的正式输入。
|
||||
779
docs/plan/Unity风格模型导入与Model资产架构重构计划_2026-04-10.md
Normal file
779
docs/plan/Unity风格模型导入与Model资产架构重构计划_2026-04-10.md
Normal file
@@ -0,0 +1,779 @@
|
||||
# Unity 风格模型导入与 Model 资产架构重构计划
|
||||
|
||||
日期:2026-04-10
|
||||
|
||||
## 1. 文档定位
|
||||
|
||||
这份计划覆盖 XCEngine 现阶段外部模型资源导入链路的结构性重构,目标不是“再给 `MeshLoader` 多加几个扩展名”,而是把模型资源正式提升为接近 Unity 的 `ModelImporter + ModelAsset` 工作流。
|
||||
|
||||
本计划默认遵循以下战略判断:
|
||||
|
||||
1. 当前工程已经具备基于 `Assimp` 的基础静态模型读取能力,`.obj/.fbx/.gltf/.glb/.dae/.stl` 都能进入导入链。
|
||||
2. 当前主资源仍然是 `Mesh`,这导致导入时会把大量上层语义压扁到静态网格层。
|
||||
3. 如果要走 Unity 风格,后续继续在 `Mesh` 主资产上堆功能会越来越别扭,必须先补一层真正的 `Model` 资产。
|
||||
4. 骨骼动画不是本轮主目标,但本轮的数据结构和 artifact 设计必须为它预留正式扩展位。
|
||||
|
||||
本轮计划的本质是:
|
||||
|
||||
1. 把“source model import”和“runtime mesh load”拆开。
|
||||
2. 把“外部模型文件的主资产类型”从 `Mesh` 提升到 `Model`。
|
||||
3. 把 `.obj/.fbx/...` 的导入体验统一到一条 `ModelImporter` 主链上。
|
||||
|
||||
---
|
||||
|
||||
## 2. 当前状态判断
|
||||
|
||||
基于当前代码结构,可以确认已有能力与缺口如下。
|
||||
|
||||
### 2.1 已有能力
|
||||
|
||||
1. `MeshLoader` 已通过 `Assimp` 支持 `.fbx/.obj/.gltf/.glb/.dae/.stl/.xcmesh`。
|
||||
2. `AssetDatabase` 已将 `.obj/.fbx/.gltf/.glb/.dae/.stl` 识别为 `ModelImporter`。
|
||||
3. 编辑器 `MeshFilter` 的资源选择 UI 已允许 `.fbx/.obj/.gltf/.glb`。
|
||||
4. 工程已接入 `assimp-vc143-mt.dll`,说明构建链和基础运行依赖已经建立。
|
||||
5. 当前 artifact 管线已经具备 `xcmesh/xcmat/xctex` 的写入与回读能力。
|
||||
|
||||
### 2.2 当前核心问题
|
||||
|
||||
1. `ModelImporter` 的主资源类型实际上仍然是 `Mesh`,并不是真正的 `Model`。
|
||||
2. 导入时递归遍历 Assimp node,但会把 node transform 烘进顶点,原始层级、局部变换、pivot 语义丢失。
|
||||
3. `Mesh` 顶点结构当前仍是静态网格视角,只覆盖 `position/normal/tangent/bitangent/uv0`。
|
||||
4. `MeshImportSettings` 中虽然已经出现 `ImportSkinning / ImportAnimations / ImportCameras / ImportLights` 等标志,但没有形成对应的正式数据通路。
|
||||
5. `.meta` 当前只写 importer 名称和版本,没有形成 Unity 风格的模型导入设置持久化。
|
||||
6. 测试主要覆盖 `obj` 路径,针对真实 `fbx` fixture 的链路验证明显不足。
|
||||
7. 拖模型进场景时,现有工作流更接近“给一个 `GameObject` 绑定单个 `Mesh`”,而不是实例化一棵模型层级。
|
||||
|
||||
### 2.3 当前阶段最重要的判断
|
||||
|
||||
当前问题不在于“FBX 能不能读进来”,而在于“读进来以后是被当成 `Mesh` 还是被当成 `Model`”。
|
||||
|
||||
如果继续让外部模型文件直接产出主 `Mesh`:
|
||||
|
||||
1. `OBJ` 还能勉强成立。
|
||||
2. `FBX` 会不断丢失层级、pivot、子节点、多部件组织等上层语义。
|
||||
3. 后续无论做子资产展开、模型 prefab 化、骨骼动画还是稳定 reimport,都会越来越难。
|
||||
|
||||
---
|
||||
|
||||
## 3. 目标架构决策
|
||||
|
||||
### 3.1 总体决策
|
||||
|
||||
长期目标不是:
|
||||
|
||||
1. `OBJ -> Mesh`
|
||||
2. `FBX -> Model`
|
||||
|
||||
而是统一到:
|
||||
|
||||
1. 外部模型格式统一进入 `ModelImporter`
|
||||
2. `ModelImporter` 的主产物统一是 `Model`
|
||||
3. `Model` 内部再引用一个或多个 `Mesh/Material/Texture`
|
||||
|
||||
即:
|
||||
|
||||
1. `.obj -> 简化 Model`
|
||||
2. `.fbx -> 完整 Model`
|
||||
3. `.gltf/.glb/... -> 同样走 Model 主链`
|
||||
|
||||
这样做的好处是:
|
||||
|
||||
1. 资源系统主线统一。
|
||||
2. 编辑器工作流统一。
|
||||
3. 子资产稳定引用机制统一。
|
||||
4. 骨骼、动画、blend shape、嵌入纹理等后续扩展都有正式落点。
|
||||
|
||||
### 3.2 `Mesh` 与 `Model` 的职责边界
|
||||
|
||||
`Mesh` 只回答一件事:
|
||||
|
||||
1. 这个表面怎么画。
|
||||
|
||||
它应该承载:
|
||||
|
||||
1. 顶点数据
|
||||
2. 索引数据
|
||||
3. section
|
||||
4. bounds
|
||||
5. 材质槽位数量与 section-material 映射
|
||||
|
||||
`Model` 回答另一件事:
|
||||
|
||||
1. 这个资源整体怎么组织。
|
||||
|
||||
它应该承载:
|
||||
|
||||
1. 节点层级
|
||||
2. 节点局部 TRS
|
||||
3. 节点名字与路径
|
||||
4. 节点绑定的 mesh 引用
|
||||
5. 每个 mesh 节点的材质槽绑定
|
||||
6. 根节点信息
|
||||
7. 导入元数据
|
||||
8. 未来的 skeleton / animation / blend shape 扩展位
|
||||
|
||||
### 3.3 运行时链路的长期形态
|
||||
|
||||
长期上应当形成三层:
|
||||
|
||||
1. `ModelImporter`
|
||||
负责 source file -> artifact graph
|
||||
2. `ModelLoader`
|
||||
负责读取 `xcmodel`
|
||||
3. `MeshLoader`
|
||||
负责读取 `xcmesh`
|
||||
|
||||
其中:
|
||||
|
||||
1. `MeshLoader` 不再承担完整 source scene import 的主责任。
|
||||
2. `MeshLoader` 更适合退化成“运行时 mesh artifact 加载器 + builtin mesh loader”。
|
||||
3. 所有 source model 的正式导入都应该通过 `ModelImporter` 的专用实现完成。
|
||||
|
||||
---
|
||||
|
||||
## 4. 本轮范围与明确不做的内容
|
||||
|
||||
### 4.1 本轮必须完成
|
||||
|
||||
1. `Model` 资源类型与 `xcmodel` artifact。
|
||||
2. 统一的 `ModelImporter` 主链。
|
||||
3. `.meta` 中的 Model Import Settings 持久化。
|
||||
4. 保留层级的静态模型导入。
|
||||
5. 场景中实例化模型层级的工作流。
|
||||
6. 子资产与稳定 `LocalID` 规则。
|
||||
7. 编辑器中的模型导入设置 Inspector。
|
||||
8. `OBJ/FBX` 静态模型的正式测试闭环。
|
||||
|
||||
### 4.2 本轮暂不做
|
||||
|
||||
1. `SkinnedMeshRenderer` 正式渲染链。
|
||||
2. 骨骼矩阵上传与 GPU skinning。
|
||||
3. 动画 clip runtime 播放系统。
|
||||
4. blend shape runtime。
|
||||
5. 完整相机/灯光从 `FBX` 到 scene 的自动生成。
|
||||
6. 模型子资产在 Project 面板中的完整可展开 UI。
|
||||
|
||||
### 4.3 本轮只预留不落地的内容
|
||||
|
||||
1. `Skeleton` 资源类型接口
|
||||
2. `AnimationClip` 资源类型接口
|
||||
3. `Model` 中 skeleton/animation 的 artifact 扩展位
|
||||
4. skinning/blend shape 所需的稳定 source 标识规则
|
||||
|
||||
---
|
||||
|
||||
## 5. 目标数据结构设计
|
||||
|
||||
### 5.1 新增资源类型
|
||||
|
||||
建议在 `ResourceType` 中正式新增:
|
||||
|
||||
1. `Model`
|
||||
|
||||
同时保留已有:
|
||||
|
||||
1. `Mesh`
|
||||
2. `Material`
|
||||
3. `Texture`
|
||||
4. `AnimationClip`
|
||||
5. `Skeleton`
|
||||
|
||||
即使 `AnimationClip/Skeleton` 本轮不真正导出,也应在设计上与 `Model` 并列存在。
|
||||
|
||||
### 5.2 `Model` 资源结构建议
|
||||
|
||||
`Model` 至少包含以下结构:
|
||||
|
||||
1. `ModelNode`
|
||||
- `name`
|
||||
- `parentIndex`
|
||||
- `firstChildIndex / childCount` 或 child index array
|
||||
- `localPosition`
|
||||
- `localRotation`
|
||||
- `localScale`
|
||||
- `meshBindingStart`
|
||||
- `meshBindingCount`
|
||||
2. `ModelMeshBinding`
|
||||
- `meshLocalID`
|
||||
- `materialBindingStart`
|
||||
- `materialBindingCount`
|
||||
3. `ModelMaterialBinding`
|
||||
- `slotIndex`
|
||||
- `materialLocalID`
|
||||
4. `ModelImportMetadata`
|
||||
- importer version
|
||||
- source file signature
|
||||
- import settings snapshot
|
||||
|
||||
### 5.3 `Mesh` 资源职责调整
|
||||
|
||||
`Mesh` 继续作为低层渲染资源保留,但职责要收紧:
|
||||
|
||||
1. 不再把 source model file 当成它的长期主输入。
|
||||
2. 主要负责:
|
||||
- `xcmesh`
|
||||
- builtin mesh
|
||||
3. source format 直接读取只作为过渡能力,后续应弱化。
|
||||
|
||||
### 5.4 `Model` artifact 文件布局
|
||||
|
||||
建议 artifact 目录布局统一为:
|
||||
|
||||
1. `main.xcmodel`
|
||||
2. `mesh_0.xcmesh`
|
||||
3. `mesh_1.xcmesh`
|
||||
4. `material_0.xcmat`
|
||||
5. `material_1.xcmat`
|
||||
6. `texture_0.xctex`
|
||||
7. `texture_1.xctex`
|
||||
8. 未来可扩展:
|
||||
- `skeleton_0.xcskel`
|
||||
- `anim_0.xcanim`
|
||||
|
||||
### 5.5 `LocalID` 稳定规则
|
||||
|
||||
这是整个 Unity 风格方案里最关键的一部分。
|
||||
|
||||
不能简单使用“导入顺序下标”作为长期稳定标识。应当引入基于源语义的稳定 `LocalID` 生成策略:
|
||||
|
||||
1. Node:基于节点路径生成
|
||||
2. Mesh:基于节点路径 + mesh name + source mesh index 生成
|
||||
3. Material:基于 material name + source material index 生成
|
||||
4. Texture:基于 source texture path 或 embedded texture key 生成
|
||||
5. 后续骨骼/动画:基于骨骼名、clip 名、source index 生成
|
||||
|
||||
目标是:
|
||||
|
||||
1. 同一个模型在普通 reimport 后,子资产 `LocalID` 不变。
|
||||
2. 场景/prefab/材质槽引用不因为 artifact 目录变化而失效。
|
||||
|
||||
---
|
||||
|
||||
## 6. 导入链路重构方案
|
||||
|
||||
### 6.1 引入专用 source importer
|
||||
|
||||
建议新增专用模块,例如:
|
||||
|
||||
1. `AssimpModelImporter`
|
||||
|
||||
它的职责是:
|
||||
|
||||
1. 读取 `.obj/.fbx/.gltf/.glb/...`
|
||||
2. 产出统一中间结构 `ImportedModel`
|
||||
3. 再由 artifact writer 写出 `xcmodel/xcmesh/xcmat/xctex`
|
||||
|
||||
不建议继续把这部分主逻辑堆在 `MeshLoader` 内部。
|
||||
|
||||
### 6.2 导入中间结构
|
||||
|
||||
建议先建立 importer 内部中间结构:
|
||||
|
||||
1. `ImportedModel`
|
||||
2. `ImportedNode`
|
||||
3. `ImportedMesh`
|
||||
4. `ImportedMaterial`
|
||||
5. `ImportedTexture`
|
||||
|
||||
这样可以把:
|
||||
|
||||
1. Assimp scene 读取
|
||||
2. 引擎内部资源表示
|
||||
3. artifact 写出
|
||||
|
||||
三者解耦。
|
||||
|
||||
### 6.3 节点与变换处理原则
|
||||
|
||||
这是与当前实现最大的差异之一。
|
||||
|
||||
当前做法是把 `node` 变换乘到顶点上,再把所有几何收进同一个 `Mesh`。本轮要改成:
|
||||
|
||||
1. Mesh 顶点保持在 mesh local space
|
||||
2. Node local transform 单独存进 `Model`
|
||||
3. Scene 实例化时再把 node local transform 还原到 `GameObject`
|
||||
|
||||
只有这样才能保留:
|
||||
|
||||
1. 层级
|
||||
2. pivot
|
||||
3. 本地 TRS
|
||||
4. 后续动画骨骼的基础结构
|
||||
|
||||
### 6.4 轴系与缩放的处理原则
|
||||
|
||||
建议明确以下规则:
|
||||
|
||||
1. 轴系转换属于导入标准化步骤,但不能以“摧毁原始层级”为代价。
|
||||
2. 全局 import scale 应优先体现在 root 侧或 mesh 侧的单一标准化策略中,不要 mesh 与 node 两边重复施加。
|
||||
3. 一旦规则确定,要在 `ModelImportSettings` 中固定,并进入 `metaHash`。
|
||||
|
||||
本轮必须把“坐标转换在哪里做、缩放在哪里做”写成正式约束,而不是散落在 loader 里。
|
||||
|
||||
### 6.5 `OBJ` 的导入策略
|
||||
|
||||
`OBJ` 仍然统一走 `ModelImporter`,但导入结果通常是一个简化模型:
|
||||
|
||||
1. 一个 root node
|
||||
2. 一个或少量 mesh node
|
||||
3. 少量 material bindings
|
||||
|
||||
即:
|
||||
|
||||
1. `OBJ` 不是继续作为架构特例保留在 `Mesh` 主链外面
|
||||
2. 它只是“语义很简单的 model source”
|
||||
|
||||
### 6.6 `FBX` 的导入策略
|
||||
|
||||
`FBX` 则要保留完整静态结构:
|
||||
|
||||
1. node hierarchy
|
||||
2. local transform
|
||||
3. mesh attachments
|
||||
4. material slots
|
||||
5. embedded/external textures
|
||||
6. 后续可扩展 skeleton/animation metadata
|
||||
|
||||
即使本轮不做动画,也不应该再把 `FBX` 烘成单个主 `Mesh`。
|
||||
|
||||
---
|
||||
|
||||
## 7. AssetDatabase 与 ArtifactDatabase 改造计划
|
||||
|
||||
### 7.1 主资产类型调整
|
||||
|
||||
当前 `ModelImporter` 的主资产仍是 `Mesh`。本轮要改成:
|
||||
|
||||
1. `ModelImporter` 主资产类型为 `Model`
|
||||
2. `mainLocalID` 指向 `Model` 主资产
|
||||
3. `Mesh/Material/Texture` 成为其子资产
|
||||
|
||||
### 7.2 `EnsureArtifact` 语义调整
|
||||
|
||||
对 `Assets/Models/robot.fbx` 执行 `EnsureArtifact(..., ResourceType::Model)` 时:
|
||||
|
||||
1. 返回主 `xcmodel`
|
||||
2. 内部子资产可通过 `AssetRef(assetGuid, localID, resourceType)` 访问
|
||||
|
||||
若后续有需要,`MeshFilter` 等组件可直接引用某个 `Mesh` 子资产,而不是引用主 `Model`。
|
||||
|
||||
### 7.3 依赖追踪调整
|
||||
|
||||
当前依赖追踪对 `obj -> mtl -> texture` 已有基础覆盖,但本轮要升级成通用模型依赖追踪:
|
||||
|
||||
1. source model file
|
||||
2. 外部纹理
|
||||
3. embedded texture 的派生 key
|
||||
4. 导入设置 meta
|
||||
5. future:外部 animation source / skeleton source
|
||||
|
||||
目标是:
|
||||
|
||||
1. 改 texture 会触发模型 reimport
|
||||
2. 改 importer settings 会触发模型 reimport
|
||||
3. 子资产 artifact 会稳定重建
|
||||
|
||||
### 7.4 资源查询接口调整
|
||||
|
||||
建议新增或调整:
|
||||
|
||||
1. `TryGetAssetRef(path, ResourceType::Model, ...)`
|
||||
2. `TryGetSubAssetRef(path, subAssetKey or localID, ResourceType::Mesh, ...)`
|
||||
3. `TryGetPrimaryAssetPath(guid, ...)` 对 `Model` 继续保持主资产语义
|
||||
|
||||
同时要保证:
|
||||
|
||||
1. 主 `Model` 与子 `Mesh` 的引用路径语义清晰
|
||||
2. 编辑器与场景序列化知道自己引用的是主资产还是子资产
|
||||
|
||||
---
|
||||
|
||||
## 8. `.meta` 与导入设置计划
|
||||
|
||||
### 8.1 当前问题
|
||||
|
||||
当前 `.meta` 只有:
|
||||
|
||||
1. `guid`
|
||||
2. `folderAsset`
|
||||
3. `importer`
|
||||
4. `importerVersion`
|
||||
|
||||
这不足以支撑 Unity 风格模型工作流。
|
||||
|
||||
### 8.2 新增 `ModelImportSettings`
|
||||
|
||||
建议正式引入或升级为:
|
||||
|
||||
1. `ModelImportSettings`
|
||||
|
||||
至少包括:
|
||||
|
||||
1. `globalScale`
|
||||
2. `axisConversion`
|
||||
3. `flipUVs`
|
||||
4. `flipWindingOrder`
|
||||
5. `generateNormals`
|
||||
6. `generateTangents`
|
||||
7. `importMaterials`
|
||||
8. `extractEmbeddedTextures`
|
||||
9. `preserveHierarchy`
|
||||
10. `mergeStaticMeshes`
|
||||
11. 预留:
|
||||
- `importSkinning`
|
||||
- `importAnimations`
|
||||
- `importCameras`
|
||||
- `importLights`
|
||||
|
||||
### 8.3 `.meta` 持久化规则
|
||||
|
||||
应当把上述设置正式写入 `.meta`,并参与 `metaHash`。
|
||||
|
||||
目标是:
|
||||
|
||||
1. 切换 importer setting 后 artifact key 会变化
|
||||
2. `Reimport` 有明确语义
|
||||
3. Inspector 改完设置能稳定反映到导入结果
|
||||
|
||||
### 8.4 版本策略
|
||||
|
||||
建议:
|
||||
|
||||
1. `ModelImporter` 单独维护 importer version
|
||||
2. 当 `xcmodel` schema 或子资产布局变化时,显式提升版本号
|
||||
3. 不再使用“整个 AssetDatabase 共用一个笼统版本号”来覆盖全部导入器变化
|
||||
|
||||
---
|
||||
|
||||
## 9. 编辑器工作流计划
|
||||
|
||||
### 9.1 Project 面板语义
|
||||
|
||||
`.obj/.fbx/.gltf/.glb/...` 在 Project 面板中应统一显示为:
|
||||
|
||||
1. `Model`
|
||||
|
||||
而不是继续让用户默认把这些资源理解成“单个 mesh 文件”。
|
||||
|
||||
### 9.2 Model Importer Inspector
|
||||
|
||||
选中模型资源时,Inspector 需要出现真正的导入设置面板:
|
||||
|
||||
1. Import Settings
|
||||
2. Apply
|
||||
3. Revert
|
||||
4. Reimport
|
||||
|
||||
首批显示的设置至少包括:
|
||||
|
||||
1. Scale
|
||||
2. Axis Conversion
|
||||
3. Preserve Hierarchy
|
||||
4. Import Materials
|
||||
5. Generate Normals
|
||||
6. Generate Tangents
|
||||
7. Flip UVs
|
||||
|
||||
### 9.3 场景拖拽行为
|
||||
|
||||
把模型资源拖进场景时,不应只创建一个空物体加一个 `MeshFilter`。
|
||||
|
||||
推荐行为:
|
||||
|
||||
1. 读取 `Model`
|
||||
2. 生成对应 `GameObject` 树
|
||||
3. 按节点局部变换恢复 TRS
|
||||
4. 对带 mesh 的节点挂 `MeshFilter + MeshRenderer`
|
||||
5. 材质槽按 `Model` 内的绑定还原
|
||||
|
||||
这才接近 Unity 的“拖入模型得到层级实例”。
|
||||
|
||||
### 9.4 子资产访问
|
||||
|
||||
本轮不强制要求 Project 面板完整展开子资产树,但至少应保证:
|
||||
|
||||
1. `MeshFilter` 能引用模型产出的某个 `Mesh` 子资产
|
||||
2. 场景序列化后引用稳定
|
||||
3. 后续补 Project 子资产展开 UI 时不需要推倒重来
|
||||
|
||||
---
|
||||
|
||||
## 10. 场景实例化与运行时路径
|
||||
|
||||
### 10.1 不建议增加 `ModelComponent`
|
||||
|
||||
为了贴近 Unity,建议不要把“整个模型实例”抽成一个新的 `ModelComponent` 挂在单个对象上。
|
||||
|
||||
更合理的做法是:
|
||||
|
||||
1. `Model` 是资源
|
||||
2. “实例化模型”是一个 utility / service
|
||||
3. 实例化结果是 `GameObject` 层级
|
||||
|
||||
### 10.2 建议新增实例化工具层
|
||||
|
||||
可以新增例如:
|
||||
|
||||
1. `ModelInstantiationUtility`
|
||||
2. `InstantiateModelAsset(...)`
|
||||
|
||||
职责:
|
||||
|
||||
1. 根据 `Model` 创建场景层级
|
||||
2. 为每个节点恢复局部 TRS
|
||||
3. 绑定子 `Mesh`
|
||||
4. 绑定默认材质或导入材质
|
||||
|
||||
### 10.3 与现有渲染链的兼容策略
|
||||
|
||||
现有渲染链以 `MeshFilter + MeshRenderer` 为中心,这一层本轮不应推翻。
|
||||
|
||||
本轮应当:
|
||||
|
||||
1. 保持 runtime 渲染主链稳定
|
||||
2. 通过新的 `Model -> GameObject hierarchy` 实例化路径,把 `Model` 翻译回现有组件体系
|
||||
|
||||
这样风险最小,且后续加 `SkinnedMeshRenderer` 时也有自然落点。
|
||||
|
||||
---
|
||||
|
||||
## 11. 测试与验证计划
|
||||
|
||||
### 11.1 Fixture 规划
|
||||
|
||||
必须新增真实模型 fixture,不再只停留在字符串层面的 `CanLoad("test.fbx")`。
|
||||
|
||||
建议至少准备:
|
||||
|
||||
1. `single_mesh_static.obj`
|
||||
2. `single_mesh_static.fbx`
|
||||
3. `multi_node_static.fbx`
|
||||
4. `multi_material_static.fbx`
|
||||
5. `embedded_texture_static.fbx`
|
||||
6. `external_texture_static.fbx`
|
||||
|
||||
### 11.2 Unit Test
|
||||
|
||||
建议新增或扩展:
|
||||
|
||||
1. `ModelLoader` 基础加载测试
|
||||
2. `ModelImportSettings` 持久化测试
|
||||
3. `ModelArtifact` 写入/回读测试
|
||||
4. `LocalID` 稳定性测试
|
||||
5. `AssetDatabase` 主资产/子资产引用测试
|
||||
|
||||
### 11.3 Integration Test
|
||||
|
||||
建议新增:
|
||||
|
||||
1. `FBX -> xcmodel -> scene instantiate` 集成测试
|
||||
2. reimport 后 `AssetRef` 稳定性测试
|
||||
3. 多节点模型实例化后层级与局部变换恢复测试
|
||||
4. 多材质槽绑定测试
|
||||
|
||||
### 11.4 Editor Test
|
||||
|
||||
建议补:
|
||||
|
||||
1. Project 面板模型类型识别
|
||||
2. Model Importer Inspector 设置编辑
|
||||
3. Apply/Reimport 行为
|
||||
4. 拖模型到场景生成层级
|
||||
|
||||
### 11.5 验收原则
|
||||
|
||||
本轮不能以“能读进一个 FBX 文件”作为完成标准。
|
||||
|
||||
必须同时满足:
|
||||
|
||||
1. artifact 正确
|
||||
2. hierarchy 正确
|
||||
3. reimport 稳定
|
||||
4. editor 工作流成立
|
||||
|
||||
---
|
||||
|
||||
## 12. 分阶段执行计划
|
||||
|
||||
### Phase 1:`Model` 资源与 artifact 基础设施落地
|
||||
|
||||
目标:
|
||||
|
||||
1. 新增 `ResourceType::Model`
|
||||
2. 新增 `Model` 类与 `ModelLoader`
|
||||
3. 新增 `xcmodel` artifact 格式
|
||||
|
||||
任务:
|
||||
|
||||
1. 设计 `ModelNode / ModelMeshBinding / ModelMaterialBinding`
|
||||
2. 在 `ArtifactFormats` 中定义 `ModelArtifactHeader`
|
||||
3. 实现 `WriteModelArtifactFile / LoadModelArtifact`
|
||||
4. 让 `ResourceManager` 注册 `ModelLoader`
|
||||
|
||||
验收标准:
|
||||
|
||||
1. `xcmodel` 能写出与读回
|
||||
2. `Model` 可被 `ResourceManager` 正式加载
|
||||
|
||||
### Phase 2:source import 主链从 `Mesh` 迁移到 `Model`
|
||||
|
||||
目标:
|
||||
|
||||
1. 建立 `AssimpModelImporter`
|
||||
2. 将 `.obj/.fbx/...` 的正式导入主链切换到 `Model`
|
||||
|
||||
任务:
|
||||
|
||||
1. 建立 `ImportedModel` 中间结构
|
||||
2. 从 Assimp scene 提取节点层级、局部 TRS、mesh、material、texture
|
||||
3. 停止在导入主链中把 node transform 烘平到单个主 mesh
|
||||
4. 输出 `main.xcmodel + sub assets`
|
||||
|
||||
验收标准:
|
||||
|
||||
1. `OBJ` 被导入成简化 `Model`
|
||||
2. `FBX` 被导入成保留 hierarchy 的 `Model`
|
||||
|
||||
### Phase 3:`AssetDatabase` 子资产与稳定 `LocalID`
|
||||
|
||||
目标:
|
||||
|
||||
1. 完成主资产/子资产语义
|
||||
2. 建立稳定 `LocalID` 规则
|
||||
|
||||
任务:
|
||||
|
||||
1. 修改 `ModelImporter` 的主资源类型
|
||||
2. 实现模型子资产 `AssetRef`
|
||||
3. 让 `EnsureArtifact`、`TryGetAssetRef`、序列化链路理解 `Model` 主资产与子 `Mesh`
|
||||
4. 建立 reimport 稳定性测试
|
||||
|
||||
验收标准:
|
||||
|
||||
1. reimport 后子资产引用不漂移
|
||||
2. 场景中的 mesh 引用可稳定恢复
|
||||
|
||||
### Phase 4:`.meta` 与 Model Import Settings 正式化
|
||||
|
||||
目标:
|
||||
|
||||
1. 让模型导入参数进入正式工作流
|
||||
|
||||
任务:
|
||||
|
||||
1. 定义 `ModelImportSettings`
|
||||
2. 把 settings 写入 `.meta`
|
||||
3. 调整 `metaHash`
|
||||
4. 保证 settings 变化触发 reimport
|
||||
|
||||
验收标准:
|
||||
|
||||
1. 改 scale/axis/material 等设置会稳定影响导入结果
|
||||
2. artifact key 与导入设置一致变化
|
||||
|
||||
### Phase 5:编辑器 Inspector 与场景实例化工作流
|
||||
|
||||
目标:
|
||||
|
||||
1. 建立 Unity 风格的模型资源使用体验
|
||||
|
||||
任务:
|
||||
|
||||
1. Project 面板统一把模型文件识别为 `Model`
|
||||
2. 新增 Model Importer Inspector
|
||||
3. 实现 `Apply/Reimport`
|
||||
4. 实现拖模型到场景生成 `GameObject` 层级
|
||||
|
||||
验收标准:
|
||||
|
||||
1. 从 editor 可完整配置模型导入
|
||||
2. 拖入场景后层级与局部变换正确
|
||||
|
||||
### Phase 6:清理过渡路径与补齐文档测试
|
||||
|
||||
目标:
|
||||
|
||||
1. 收紧旧路径,避免双轨架构长期并存
|
||||
|
||||
任务:
|
||||
|
||||
1. 评估并逐步弱化“source file 直接由 `MeshLoader` 作为主入口”的旧路径
|
||||
2. 清理命名、文档、注释、测试目录结构
|
||||
3. 输出阶段总结
|
||||
|
||||
验收标准:
|
||||
|
||||
1. 主链路清晰,旧路径只保留必要兼容
|
||||
2. 文档与测试同步完成
|
||||
|
||||
---
|
||||
|
||||
## 13. 关键风险与应对
|
||||
|
||||
### 13.1 最大风险:`LocalID` 不稳定
|
||||
|
||||
风险:
|
||||
|
||||
1. reimport 后子资产重排
|
||||
2. 场景与 prefab 引用失效
|
||||
|
||||
应对:
|
||||
|
||||
1. 在 Phase 1 之前先写清 `LocalID` 规则
|
||||
2. 在 Phase 3 前就建立稳定性测试
|
||||
|
||||
### 13.2 第二风险:轴系与缩放规则前后不一致
|
||||
|
||||
风险:
|
||||
|
||||
1. 同一模型在 direct load、artifact load、scene instantiate 三条路径下结果不一致
|
||||
|
||||
应对:
|
||||
|
||||
1. 明确标准化规则只允许一个真值来源
|
||||
2. 写入 importer settings 并进入测试
|
||||
|
||||
### 13.3 第三风险:编辑器体验与底层数据不同步
|
||||
|
||||
风险:
|
||||
|
||||
1. Inspector 改了设置但 reimport 行为不稳定
|
||||
2. Project 面板显示的是 `Model`,内部实际仍按 `Mesh` 走
|
||||
|
||||
应对:
|
||||
|
||||
1. 先完成 Resource/AssetDatabase 语义,再接 editor UI
|
||||
2. 不允许 editor 先行伪装完成
|
||||
|
||||
### 13.4 第四风险:过渡期双轨逻辑长期共存
|
||||
|
||||
风险:
|
||||
|
||||
1. `MeshLoader` source import 与 `ModelImporter` source import 两套路径并行,长期维护成本失控
|
||||
|
||||
应对:
|
||||
|
||||
1. 在 Phase 6 明确收紧旧路径
|
||||
2. 把 source import 主入口统一到 `ModelImporter`
|
||||
|
||||
---
|
||||
|
||||
## 14. 本轮完成标志
|
||||
|
||||
当以下条件同时成立时,本轮才算真正完成:
|
||||
|
||||
1. `.obj/.fbx/.gltf/.glb/...` 的主资产统一为 `Model`
|
||||
2. `Model` artifact 已正式落地并进入 `ResourceManager`
|
||||
3. `OBJ` 能导入成简化 `Model`
|
||||
4. `FBX` 能导入成保留 hierarchy 的静态 `Model`
|
||||
5. 模型导入设置已写入 `.meta` 并进入 reimport 逻辑
|
||||
6. editor 中已具备 Model Importer Inspector
|
||||
7. 拖模型到场景时会生成 `GameObject` 层级,而非单个烘平 mesh
|
||||
8. 子资产 `LocalID` 在普通 reimport 下稳定
|
||||
9. 测试已覆盖真实 `FBX` fixture 的关键路径
|
||||
|
||||
---
|
||||
|
||||
## 15. 一句话结论
|
||||
|
||||
这一轮不是“给 FBX 补支持”,而是把 XCEngine 的外部模型资源体系从“以 `Mesh` 为主资产的静态导入器”升级成“以 `Model` 为主资产、以子资产和稳定 reimport 为基础、可持续扩展到骨骼动画的 Unity 风格模型工作流”。
|
||||
@@ -363,6 +363,13 @@ add_library(XCEngine STATIC
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Resources/Texture/TextureLoader.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Resources/Texture/TextureImportSettings.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Resources/BuiltinResources.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Resources/Model/AssimpModelImporter.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Resources/Model/Model.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Resources/Model/ModelArtifactIO.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Resources/Model/ModelLoader.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Resources/GaussianSplat/GaussianSplat.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Resources/GaussianSplat/GaussianSplatArtifactIO.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Resources/GaussianSplat/GaussianSplatLoader.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Resources/Mesh/Mesh.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Resources/Mesh/MeshLoader.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Resources/Mesh/MeshImportSettings.h
|
||||
@@ -382,6 +389,14 @@ add_library(XCEngine STATIC
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/Resources/Texture/TextureLoader.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/Resources/Texture/TextureImportSettings.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/Resources/BuiltinResources.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/Resources/Model/AssimpModelImporter.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/Resources/Model/Model.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/Resources/Model/ModelArtifactIO.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/Resources/Model/ModelLoader.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/Resources/GaussianSplat/GaussianSplat.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/Resources/GaussianSplat/GaussianSplatArtifactIO.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/Resources/GaussianSplat/GaussianSplatLoader.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/Resources/GaussianSplat/Internal/GaussianSplatPlyImporter.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/Resources/Mesh/Mesh.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/Resources/Mesh/MeshLoader.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/Resources/Mesh/MeshImportSettings.cpp
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
#pragma once
|
||||
|
||||
#include <XCEngine/Core/Asset/AssetGUID.h>
|
||||
#include <XCEngine/Core/Math/Quaternion.h>
|
||||
#include <XCEngine/Core/Math/Bounds.h>
|
||||
#include <XCEngine/Core/Types.h>
|
||||
#include <XCEngine/Resources/Material/Material.h>
|
||||
#include <XCEngine/Resources/Mesh/Mesh.h>
|
||||
#include <XCEngine/Resources/Texture/Texture.h>
|
||||
#include <XCEngine/Resources/GaussianSplat/GaussianSplat.h>
|
||||
#include <XCEngine/Resources/UI/UIDocumentTypes.h>
|
||||
|
||||
namespace XCEngine {
|
||||
@@ -16,6 +19,8 @@ constexpr Core::uint32 kMeshArtifactSchemaVersion = 2;
|
||||
constexpr Core::uint32 kShaderArtifactSchemaVersion = 5;
|
||||
constexpr Core::uint32 kUIDocumentArtifactSchemaVersion = 2;
|
||||
constexpr Core::uint32 kVolumeFieldArtifactSchemaVersion = 2;
|
||||
constexpr Core::uint32 kModelArtifactSchemaVersion = 1;
|
||||
constexpr Core::uint32 kGaussianSplatArtifactSchemaVersion = 1;
|
||||
|
||||
struct TextureArtifactHeader {
|
||||
char magic[8] = { 'X', 'C', 'T', 'E', 'X', '0', '1', '\0' };
|
||||
@@ -66,6 +71,38 @@ struct MaterialPropertyArtifact {
|
||||
MaterialProperty::Value value = {};
|
||||
};
|
||||
|
||||
struct ModelArtifactFileHeader {
|
||||
char magic[8] = { 'X', 'C', 'M', 'O', 'D', '0', '1', '\0' };
|
||||
Core::uint32 schemaVersion = kModelArtifactSchemaVersion;
|
||||
};
|
||||
|
||||
struct ModelArtifactHeader {
|
||||
Core::uint32 nodeCount = 0;
|
||||
Core::uint32 meshBindingCount = 0;
|
||||
Core::uint32 materialBindingCount = 0;
|
||||
Core::uint32 rootNodeIndex = 0xffffffffu;
|
||||
};
|
||||
|
||||
struct ModelNodeArtifactHeader {
|
||||
Core::int32 parentIndex = -1;
|
||||
Core::uint32 meshBindingStart = 0;
|
||||
Core::uint32 meshBindingCount = 0;
|
||||
Math::Vector3 localPosition = Math::Vector3::Zero();
|
||||
Math::Quaternion localRotation = Math::Quaternion::Identity();
|
||||
Math::Vector3 localScale = Math::Vector3(1.0f, 1.0f, 1.0f);
|
||||
};
|
||||
|
||||
struct ModelMeshBindingArtifact {
|
||||
LocalID meshLocalID = kInvalidLocalID;
|
||||
Core::uint32 materialBindingStart = 0;
|
||||
Core::uint32 materialBindingCount = 0;
|
||||
};
|
||||
|
||||
struct ModelMaterialBindingArtifact {
|
||||
Core::uint32 slotIndex = 0;
|
||||
LocalID materialLocalID = kInvalidLocalID;
|
||||
};
|
||||
|
||||
struct ShaderArtifactFileHeader {
|
||||
char magic[8] = { 'X', 'C', 'S', 'H', 'D', '0', '5', '\0' };
|
||||
Core::uint32 schemaVersion = kShaderArtifactSchemaVersion;
|
||||
@@ -144,5 +181,36 @@ struct VolumeFieldArtifactHeader {
|
||||
Core::uint64 payloadSize = 0;
|
||||
};
|
||||
|
||||
struct GaussianSplatArtifactFileHeader {
|
||||
char magic[8] = { 'X', 'C', 'G', 'S', 'P', '0', '1', '\0' };
|
||||
Core::uint32 schemaVersion = kGaussianSplatArtifactSchemaVersion;
|
||||
};
|
||||
|
||||
struct GaussianSplatArtifactHeader {
|
||||
Core::uint32 contentVersion = 1;
|
||||
Core::uint32 splatCount = 0;
|
||||
Core::uint32 chunkCount = 0;
|
||||
Core::uint32 cameraCount = 0;
|
||||
Math::Vector3 boundsMin = Math::Vector3::Zero();
|
||||
Math::Vector3 boundsMax = Math::Vector3::Zero();
|
||||
Core::uint32 positionFormat = static_cast<Core::uint32>(GaussianSplatSectionFormat::VectorFloat32);
|
||||
Core::uint32 otherFormat = static_cast<Core::uint32>(GaussianSplatSectionFormat::OtherFloat32);
|
||||
Core::uint32 colorFormat = static_cast<Core::uint32>(GaussianSplatSectionFormat::ColorRGBA32F);
|
||||
Core::uint32 shFormat = static_cast<Core::uint32>(GaussianSplatSectionFormat::SHFloat32);
|
||||
Core::uint32 chunkFormat = static_cast<Core::uint32>(GaussianSplatSectionFormat::Unknown);
|
||||
Core::uint32 cameraFormat = static_cast<Core::uint32>(GaussianSplatSectionFormat::Unknown);
|
||||
Core::uint32 sectionCount = 0;
|
||||
Core::uint64 payloadSize = 0;
|
||||
};
|
||||
|
||||
struct GaussianSplatArtifactSectionRecord {
|
||||
Core::uint32 sectionType = static_cast<Core::uint32>(GaussianSplatSectionType::Unknown);
|
||||
Core::uint32 format = static_cast<Core::uint32>(GaussianSplatSectionFormat::Unknown);
|
||||
Core::uint64 payloadOffset = 0;
|
||||
Core::uint64 dataSize = 0;
|
||||
Core::uint32 elementCount = 0;
|
||||
Core::uint32 elementStride = 0;
|
||||
};
|
||||
|
||||
} // namespace Resources
|
||||
} // namespace XCEngine
|
||||
|
||||
@@ -82,6 +82,7 @@ public:
|
||||
bool TryGetAssetGuid(const Containers::String& requestPath, AssetGUID& outGuid) const;
|
||||
bool TryGetImportableResourceType(const Containers::String& requestPath, ResourceType& outType) const;
|
||||
bool TryGetAssetRef(const Containers::String& requestPath, ResourceType resourceType, AssetRef& outRef) const;
|
||||
bool TryResolveAssetPath(const AssetRef& assetRef, Containers::String& outPath);
|
||||
bool ReimportAsset(const Containers::String& requestPath,
|
||||
ResolvedAsset& outAsset,
|
||||
MaintenanceStats* outStats = nullptr);
|
||||
@@ -139,6 +140,8 @@ private:
|
||||
ArtifactRecord& outRecord);
|
||||
bool ImportShaderAsset(const SourceAssetRecord& sourceRecord,
|
||||
ArtifactRecord& outRecord);
|
||||
bool ImportGaussianSplatAsset(const SourceAssetRecord& sourceRecord,
|
||||
ArtifactRecord& outRecord);
|
||||
bool ImportVolumeFieldAsset(const SourceAssetRecord& sourceRecord,
|
||||
ArtifactRecord& outRecord);
|
||||
bool ImportUIDocumentAsset(const SourceAssetRecord& sourceRecord,
|
||||
@@ -161,7 +164,7 @@ private:
|
||||
ArtifactDependencyRecord& outRecord) const;
|
||||
bool AreDependenciesCurrent(const std::vector<ArtifactDependencyRecord>& dependencies) const;
|
||||
bool CollectModelDependencies(const SourceAssetRecord& sourceRecord,
|
||||
const Mesh& mesh,
|
||||
const std::vector<Containers::String>& importedTexturePaths,
|
||||
std::vector<ArtifactDependencyRecord>& outDependencies) const;
|
||||
bool CollectMaterialDependencies(const Material& material,
|
||||
std::vector<ArtifactDependencyRecord>& outDependencies) const;
|
||||
|
||||
@@ -75,6 +75,8 @@ public:
|
||||
bool TryGetAssetRef(const Containers::String& requestPath,
|
||||
ResourceType resourceType,
|
||||
AssetRef& outRef) const;
|
||||
bool TryResolveAssetPath(const AssetRef& assetRef,
|
||||
Containers::String& outPath);
|
||||
bool TryGetPrimaryAssetPath(const AssetGUID& guid, Containers::String& outRelativePath) const;
|
||||
void BuildLookupSnapshot(LookupSnapshot& outSnapshot) const;
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ public:
|
||||
const Containers::String& path,
|
||||
ResourceType resourceType,
|
||||
AssetRef& outRef) const;
|
||||
bool TryResolveAssetPath(const AssetImportService& importService,
|
||||
bool TryResolveAssetPath(AssetImportService& importService,
|
||||
const AssetRef& assetRef,
|
||||
Containers::String& outPath) const;
|
||||
void RememberResolvedPath(const AssetGUID& assetGuid, const Containers::String& relativePath);
|
||||
|
||||
@@ -26,7 +26,9 @@ enum class ResourceType : Core::uint8 {
|
||||
UIView,
|
||||
UITheme,
|
||||
UISchema,
|
||||
VolumeField
|
||||
VolumeField,
|
||||
Model,
|
||||
GaussianSplat
|
||||
};
|
||||
|
||||
constexpr const char* GetResourceTypeName(ResourceType type) {
|
||||
@@ -47,6 +49,8 @@ constexpr const char* GetResourceTypeName(ResourceType type) {
|
||||
case ResourceType::UITheme: return "UITheme";
|
||||
case ResourceType::UISchema: return "UISchema";
|
||||
case ResourceType::VolumeField: return "VolumeField";
|
||||
case ResourceType::Model: return "Model";
|
||||
case ResourceType::GaussianSplat: return "GaussianSplat";
|
||||
default: return "Unknown";
|
||||
}
|
||||
}
|
||||
@@ -101,6 +105,8 @@ template<> inline ResourceType GetResourceType<class UIView>() { return Resource
|
||||
template<> inline ResourceType GetResourceType<class UITheme>() { return ResourceType::UITheme; }
|
||||
template<> inline ResourceType GetResourceType<class UISchema>() { return ResourceType::UISchema; }
|
||||
template<> inline ResourceType GetResourceType<class VolumeField>() { return ResourceType::VolumeField; }
|
||||
template<> inline ResourceType GetResourceType<class Model>() { return ResourceType::Model; }
|
||||
template<> inline ResourceType GetResourceType<class GaussianSplat>() { return ResourceType::GaussianSplat; }
|
||||
|
||||
} // namespace Resources
|
||||
} // namespace XCEngine
|
||||
|
||||
138
engine/include/XCEngine/Resources/GaussianSplat/GaussianSplat.h
Normal file
138
engine/include/XCEngine/Resources/GaussianSplat/GaussianSplat.h
Normal file
@@ -0,0 +1,138 @@
|
||||
#pragma once
|
||||
|
||||
#include <XCEngine/Core/Asset/IResource.h>
|
||||
#include <XCEngine/Core/Containers/Array.h>
|
||||
#include <XCEngine/Core/Math/Bounds.h>
|
||||
#include <XCEngine/Core/Math/Quaternion.h>
|
||||
#include <XCEngine/Core/Math/Vector3.h>
|
||||
#include <XCEngine/Core/Math/Vector4.h>
|
||||
#include <XCEngine/Core/Types.h>
|
||||
|
||||
namespace XCEngine {
|
||||
namespace Resources {
|
||||
|
||||
enum class GaussianSplatSectionType : Core::uint32 {
|
||||
Unknown = 0,
|
||||
Positions = 1,
|
||||
Other = 2,
|
||||
Color = 3,
|
||||
SH = 4,
|
||||
Chunks = 5,
|
||||
Cameras = 6
|
||||
};
|
||||
|
||||
enum class GaussianSplatSectionFormat : Core::uint32 {
|
||||
Unknown = 0,
|
||||
VectorFloat32 = 1,
|
||||
VectorFloat16 = 2,
|
||||
VectorNorm16 = 3,
|
||||
VectorNorm11 = 4,
|
||||
VectorNorm6 = 5,
|
||||
OtherFloat32 = 16,
|
||||
OtherPacked32 = 17,
|
||||
ColorRGBA32F = 32,
|
||||
ColorRGBA16F = 33,
|
||||
ColorRGBA8 = 34,
|
||||
ColorBC7 = 35,
|
||||
SHFloat32 = 48,
|
||||
SHFloat16 = 49,
|
||||
SHNorm11 = 50,
|
||||
SHNorm6 = 51,
|
||||
ChunkFloat32 = 64,
|
||||
CameraFloat32 = 80
|
||||
};
|
||||
|
||||
constexpr Core::uint32 kGaussianSplatSHCoefficientCount = 45;
|
||||
|
||||
struct GaussianSplatPositionRecord {
|
||||
Math::Vector3 position = Math::Vector3::Zero();
|
||||
};
|
||||
|
||||
struct GaussianSplatOtherRecord {
|
||||
Math::Quaternion rotation = Math::Quaternion::Identity();
|
||||
Math::Vector3 scale = Math::Vector3::Zero();
|
||||
float reserved = 0.0f;
|
||||
};
|
||||
|
||||
struct GaussianSplatColorRecord {
|
||||
Math::Vector4 colorOpacity = Math::Vector4::Zero();
|
||||
};
|
||||
|
||||
struct GaussianSplatSHRecord {
|
||||
float coefficients[kGaussianSplatSHCoefficientCount] = {};
|
||||
};
|
||||
|
||||
struct GaussianSplatSection {
|
||||
GaussianSplatSectionType type = GaussianSplatSectionType::Unknown;
|
||||
GaussianSplatSectionFormat format = GaussianSplatSectionFormat::Unknown;
|
||||
Core::uint64 dataOffset = 0;
|
||||
Core::uint64 dataSize = 0;
|
||||
Core::uint32 elementCount = 0;
|
||||
Core::uint32 elementStride = 0;
|
||||
};
|
||||
|
||||
struct GaussianSplatMetadata {
|
||||
Core::uint32 contentVersion = 1;
|
||||
Core::uint32 splatCount = 0;
|
||||
Core::uint32 chunkCount = 0;
|
||||
Core::uint32 cameraCount = 0;
|
||||
Math::Bounds bounds;
|
||||
GaussianSplatSectionFormat positionFormat = GaussianSplatSectionFormat::VectorFloat32;
|
||||
GaussianSplatSectionFormat otherFormat = GaussianSplatSectionFormat::OtherFloat32;
|
||||
GaussianSplatSectionFormat colorFormat = GaussianSplatSectionFormat::ColorRGBA32F;
|
||||
GaussianSplatSectionFormat shFormat = GaussianSplatSectionFormat::SHFloat32;
|
||||
GaussianSplatSectionFormat chunkFormat = GaussianSplatSectionFormat::Unknown;
|
||||
GaussianSplatSectionFormat cameraFormat = GaussianSplatSectionFormat::Unknown;
|
||||
};
|
||||
|
||||
class GaussianSplat : public IResource {
|
||||
public:
|
||||
GaussianSplat();
|
||||
~GaussianSplat() override;
|
||||
|
||||
ResourceType GetType() const override { return ResourceType::GaussianSplat; }
|
||||
const Containers::String& GetName() const override { return m_name; }
|
||||
const Containers::String& GetPath() const override { return m_path; }
|
||||
ResourceGUID GetGUID() const override { return m_guid; }
|
||||
bool IsValid() const override { return m_isValid; }
|
||||
size_t GetMemorySize() const override { return m_memorySize; }
|
||||
void Release() override;
|
||||
|
||||
bool CreateOwned(const GaussianSplatMetadata& metadata,
|
||||
Containers::Array<GaussianSplatSection>&& sections,
|
||||
Containers::Array<Core::uint8>&& payload);
|
||||
void Clear();
|
||||
|
||||
const GaussianSplatMetadata& GetMetadata() const { return m_metadata; }
|
||||
Core::uint32 GetContentVersion() const { return m_metadata.contentVersion; }
|
||||
Core::uint32 GetSplatCount() const { return m_metadata.splatCount; }
|
||||
Core::uint32 GetChunkCount() const { return m_metadata.chunkCount; }
|
||||
Core::uint32 GetCameraCount() const { return m_metadata.cameraCount; }
|
||||
const Math::Bounds& GetBounds() const { return m_metadata.bounds; }
|
||||
GaussianSplatSectionFormat GetPositionFormat() const { return m_metadata.positionFormat; }
|
||||
GaussianSplatSectionFormat GetOtherFormat() const { return m_metadata.otherFormat; }
|
||||
GaussianSplatSectionFormat GetColorFormat() const { return m_metadata.colorFormat; }
|
||||
GaussianSplatSectionFormat GetSHFormat() const { return m_metadata.shFormat; }
|
||||
GaussianSplatSectionFormat GetChunkFormat() const { return m_metadata.chunkFormat; }
|
||||
GaussianSplatSectionFormat GetCameraFormat() const { return m_metadata.cameraFormat; }
|
||||
const Containers::Array<GaussianSplatSection>& GetSections() const { return m_sections; }
|
||||
const GaussianSplatSection* FindSection(GaussianSplatSectionType type) const;
|
||||
const void* GetSectionData(GaussianSplatSectionType type) const;
|
||||
const GaussianSplatPositionRecord* GetPositionRecords() const;
|
||||
const GaussianSplatOtherRecord* GetOtherRecords() const;
|
||||
const GaussianSplatColorRecord* GetColorRecords() const;
|
||||
const GaussianSplatSHRecord* GetSHRecords() const;
|
||||
const Core::uint8* GetPayloadData() const { return m_payload.Data(); }
|
||||
size_t GetPayloadSize() const { return m_payload.Size(); }
|
||||
|
||||
private:
|
||||
bool ValidateSections(const Containers::Array<GaussianSplatSection>& sections, size_t payloadSize) const;
|
||||
void UpdateMemorySize();
|
||||
|
||||
GaussianSplatMetadata m_metadata = {};
|
||||
Containers::Array<GaussianSplatSection> m_sections;
|
||||
Containers::Array<Core::uint8> m_payload;
|
||||
};
|
||||
|
||||
} // namespace Resources
|
||||
} // namespace XCEngine
|
||||
@@ -0,0 +1,18 @@
|
||||
#pragma once
|
||||
|
||||
#include <XCEngine/Core/Containers/String.h>
|
||||
#include <XCEngine/Core/IO/IResourceLoader.h>
|
||||
|
||||
namespace XCEngine {
|
||||
namespace Resources {
|
||||
|
||||
class GaussianSplat;
|
||||
|
||||
bool WriteGaussianSplatArtifactFile(const Containers::String& artifactPath,
|
||||
const GaussianSplat& gaussianSplat,
|
||||
Containers::String* outErrorMessage = nullptr);
|
||||
|
||||
LoadResult LoadGaussianSplatArtifact(const Containers::String& path);
|
||||
|
||||
} // namespace Resources
|
||||
} // namespace XCEngine
|
||||
@@ -0,0 +1,21 @@
|
||||
#pragma once
|
||||
|
||||
#include <XCEngine/Core/IO/IResourceLoader.h>
|
||||
|
||||
namespace XCEngine {
|
||||
namespace Resources {
|
||||
|
||||
class GaussianSplatLoader : public IResourceLoader {
|
||||
public:
|
||||
GaussianSplatLoader();
|
||||
~GaussianSplatLoader() override;
|
||||
|
||||
ResourceType GetResourceType() const override { return ResourceType::GaussianSplat; }
|
||||
Containers::Array<Containers::String> GetSupportedExtensions() const override;
|
||||
bool CanLoad(const Containers::String& path) const override;
|
||||
LoadResult Load(const Containers::String& path, const ImportSettings* settings = nullptr) override;
|
||||
ImportSettings* GetDefaultSettings() const override;
|
||||
};
|
||||
|
||||
} // namespace Resources
|
||||
} // namespace XCEngine
|
||||
@@ -0,0 +1,43 @@
|
||||
#pragma once
|
||||
|
||||
#include <XCEngine/Core/Containers/String.h>
|
||||
#include <XCEngine/Core/Asset/AssetGUID.h>
|
||||
#include <XCEngine/Resources/Mesh/MeshImportSettings.h>
|
||||
|
||||
#include <vector>
|
||||
|
||||
namespace XCEngine {
|
||||
namespace Resources {
|
||||
|
||||
class Model;
|
||||
class Mesh;
|
||||
class Material;
|
||||
class Texture;
|
||||
|
||||
struct ImportedModelMesh {
|
||||
LocalID localID = kInvalidLocalID;
|
||||
Mesh* mesh = nullptr;
|
||||
std::vector<LocalID> materialLocalIDs;
|
||||
};
|
||||
|
||||
struct ImportedModelMaterial {
|
||||
LocalID localID = kInvalidLocalID;
|
||||
Material* material = nullptr;
|
||||
};
|
||||
|
||||
struct ImportedModelData {
|
||||
Model* model = nullptr;
|
||||
std::vector<ImportedModelMesh> meshes;
|
||||
std::vector<ImportedModelMaterial> materials;
|
||||
std::vector<Texture*> textures;
|
||||
|
||||
void Reset();
|
||||
};
|
||||
|
||||
bool ImportAssimpModel(const Containers::String& sourcePath,
|
||||
const MeshImportSettings& settings,
|
||||
ImportedModelData& outData,
|
||||
Containers::String* outErrorMessage = nullptr);
|
||||
|
||||
} // namespace Resources
|
||||
} // namespace XCEngine
|
||||
72
engine/include/XCEngine/Resources/Model/Model.h
Normal file
72
engine/include/XCEngine/Resources/Model/Model.h
Normal file
@@ -0,0 +1,72 @@
|
||||
#pragma once
|
||||
|
||||
#include <XCEngine/Core/Asset/IResource.h>
|
||||
#include <XCEngine/Core/Asset/AssetGUID.h>
|
||||
#include <XCEngine/Core/Containers/Array.h>
|
||||
#include <XCEngine/Core/Containers/String.h>
|
||||
#include <XCEngine/Core/Math/Quaternion.h>
|
||||
#include <XCEngine/Core/Math/Vector3.h>
|
||||
|
||||
namespace XCEngine {
|
||||
namespace Resources {
|
||||
|
||||
constexpr Core::uint32 kInvalidModelNodeIndex = 0xffffffffu;
|
||||
|
||||
struct ModelMaterialBinding {
|
||||
Core::uint32 slotIndex = 0;
|
||||
LocalID materialLocalID = kInvalidLocalID;
|
||||
};
|
||||
|
||||
struct ModelMeshBinding {
|
||||
LocalID meshLocalID = kInvalidLocalID;
|
||||
Core::uint32 materialBindingStart = 0;
|
||||
Core::uint32 materialBindingCount = 0;
|
||||
};
|
||||
|
||||
struct ModelNode {
|
||||
Containers::String name;
|
||||
Core::int32 parentIndex = -1;
|
||||
Core::uint32 meshBindingStart = 0;
|
||||
Core::uint32 meshBindingCount = 0;
|
||||
Math::Vector3 localPosition = Math::Vector3::Zero();
|
||||
Math::Quaternion localRotation = Math::Quaternion::Identity();
|
||||
Math::Vector3 localScale = Math::Vector3(1.0f, 1.0f, 1.0f);
|
||||
};
|
||||
|
||||
class Model : public IResource {
|
||||
public:
|
||||
Model();
|
||||
~Model() override;
|
||||
|
||||
ResourceType GetType() const override { return ResourceType::Model; }
|
||||
const Containers::String& GetName() const override { return m_name; }
|
||||
const Containers::String& GetPath() const override { return m_path; }
|
||||
ResourceGUID GetGUID() const override { return m_guid; }
|
||||
bool IsValid() const override { return m_isValid; }
|
||||
size_t GetMemorySize() const override { return m_memorySize; }
|
||||
void Release() override;
|
||||
|
||||
void SetRootNodeIndex(Core::uint32 index);
|
||||
Core::uint32 GetRootNodeIndex() const { return m_rootNodeIndex; }
|
||||
bool HasRootNode() const { return m_rootNodeIndex != kInvalidModelNodeIndex; }
|
||||
|
||||
void AddNode(const ModelNode& node);
|
||||
void AddMeshBinding(const ModelMeshBinding& binding);
|
||||
void AddMaterialBinding(const ModelMaterialBinding& binding);
|
||||
void ClearGraph();
|
||||
|
||||
const Containers::Array<ModelNode>& GetNodes() const { return m_nodes; }
|
||||
const Containers::Array<ModelMeshBinding>& GetMeshBindings() const { return m_meshBindings; }
|
||||
const Containers::Array<ModelMaterialBinding>& GetMaterialBindings() const { return m_materialBindings; }
|
||||
|
||||
private:
|
||||
void UpdateMemorySize();
|
||||
|
||||
Core::uint32 m_rootNodeIndex = kInvalidModelNodeIndex;
|
||||
Containers::Array<ModelNode> m_nodes;
|
||||
Containers::Array<ModelMeshBinding> m_meshBindings;
|
||||
Containers::Array<ModelMaterialBinding> m_materialBindings;
|
||||
};
|
||||
|
||||
} // namespace Resources
|
||||
} // namespace XCEngine
|
||||
18
engine/include/XCEngine/Resources/Model/ModelArtifactIO.h
Normal file
18
engine/include/XCEngine/Resources/Model/ModelArtifactIO.h
Normal file
@@ -0,0 +1,18 @@
|
||||
#pragma once
|
||||
|
||||
#include <XCEngine/Core/Containers/String.h>
|
||||
#include <XCEngine/Core/IO/IResourceLoader.h>
|
||||
|
||||
namespace XCEngine {
|
||||
namespace Resources {
|
||||
|
||||
class Model;
|
||||
|
||||
bool WriteModelArtifactFile(const Containers::String& artifactPath,
|
||||
const Model& model,
|
||||
Containers::String* outErrorMessage = nullptr);
|
||||
|
||||
LoadResult LoadModelArtifact(const Containers::String& path);
|
||||
|
||||
} // namespace Resources
|
||||
} // namespace XCEngine
|
||||
21
engine/include/XCEngine/Resources/Model/ModelLoader.h
Normal file
21
engine/include/XCEngine/Resources/Model/ModelLoader.h
Normal file
@@ -0,0 +1,21 @@
|
||||
#pragma once
|
||||
|
||||
#include <XCEngine/Core/IO/IResourceLoader.h>
|
||||
|
||||
namespace XCEngine {
|
||||
namespace Resources {
|
||||
|
||||
class ModelLoader : public IResourceLoader {
|
||||
public:
|
||||
ModelLoader();
|
||||
~ModelLoader() override;
|
||||
|
||||
ResourceType GetResourceType() const override { return ResourceType::Model; }
|
||||
Containers::Array<Containers::String> GetSupportedExtensions() const override;
|
||||
bool CanLoad(const Containers::String& path) const override;
|
||||
LoadResult Load(const Containers::String& path, const ImportSettings* settings = nullptr) override;
|
||||
ImportSettings* GetDefaultSettings() const override;
|
||||
};
|
||||
|
||||
} // namespace Resources
|
||||
} // namespace XCEngine
|
||||
@@ -14,6 +14,12 @@
|
||||
#include <XCEngine/Resources/Texture/TextureLoader.h>
|
||||
#include <XCEngine/Resources/Texture/TextureImportSettings.h>
|
||||
#include <XCEngine/Resources/BuiltinResources.h>
|
||||
#include <XCEngine/Resources/GaussianSplat/GaussianSplat.h>
|
||||
#include <XCEngine/Resources/GaussianSplat/GaussianSplatArtifactIO.h>
|
||||
#include <XCEngine/Resources/GaussianSplat/GaussianSplatLoader.h>
|
||||
#include <XCEngine/Resources/Model/Model.h>
|
||||
#include <XCEngine/Resources/Model/ModelArtifactIO.h>
|
||||
#include <XCEngine/Resources/Model/ModelLoader.h>
|
||||
#include <XCEngine/Resources/Mesh/Mesh.h>
|
||||
#include <XCEngine/Resources/Mesh/MeshLoader.h>
|
||||
#include <XCEngine/Resources/Mesh/MeshImportSettings.h>
|
||||
|
||||
@@ -2,8 +2,13 @@
|
||||
|
||||
#include <XCEngine/Core/Asset/ArtifactFormats.h>
|
||||
#include <XCEngine/Debug/Logger.h>
|
||||
#include <XCEngine/Resources/GaussianSplat/GaussianSplatArtifactIO.h>
|
||||
#include <XCEngine/Resources/Material/MaterialLoader.h>
|
||||
#include <XCEngine/Resources/Mesh/MeshImportSettings.h>
|
||||
#include <XCEngine/Resources/Mesh/MeshLoader.h>
|
||||
#include <XCEngine/Resources/Model/AssimpModelImporter.h>
|
||||
#include <XCEngine/Resources/Model/ModelArtifactIO.h>
|
||||
#include "Resources/GaussianSplat/Internal/GaussianSplatPlyImporter.h"
|
||||
#include <XCEngine/Resources/Shader/ShaderLoader.h>
|
||||
#include <XCEngine/Resources/Texture/TextureLoader.h>
|
||||
#include <XCEngine/Resources/UI/UIDocumentCompiler.h>
|
||||
@@ -48,6 +53,13 @@ Containers::String ToContainersString(const std::string& value) {
|
||||
|
||||
Containers::String NormalizeArtifactPathString(const fs::path& path);
|
||||
Containers::String NormalizeArtifactPathString(const Containers::String& path);
|
||||
constexpr const char* kModelSubAssetManifestFileName = "subassets.tsv";
|
||||
|
||||
struct ModelSubAssetManifestEntry {
|
||||
LocalID localID = kInvalidLocalID;
|
||||
ResourceType resourceType = ResourceType::Unknown;
|
||||
Containers::String artifactPath;
|
||||
};
|
||||
|
||||
void PopulateResolvedAssetResult(const Containers::String& projectRoot,
|
||||
const AssetDatabase::SourceAssetRecord& sourceRecord,
|
||||
@@ -320,6 +332,85 @@ std::vector<std::string> SplitFields(const std::string& line) {
|
||||
return fields;
|
||||
}
|
||||
|
||||
bool WriteModelSubAssetManifest(const fs::path& manifestPath,
|
||||
const std::vector<ModelSubAssetManifestEntry>& entries) {
|
||||
std::ofstream output(manifestPath, std::ios::out | std::ios::trunc);
|
||||
if (!output.is_open()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
output << "# localID\tresourceType\tartifactPath\n";
|
||||
for (const ModelSubAssetManifestEntry& entry : entries) {
|
||||
if (entry.localID == kInvalidLocalID ||
|
||||
entry.resourceType == ResourceType::Unknown ||
|
||||
entry.artifactPath.Empty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
output << entry.localID << '\t'
|
||||
<< static_cast<Core::uint32>(entry.resourceType) << '\t'
|
||||
<< EscapeField(ToStdString(entry.artifactPath)) << '\n';
|
||||
}
|
||||
|
||||
return static_cast<bool>(output);
|
||||
}
|
||||
|
||||
bool TryReadModelSubAssetManifest(const fs::path& manifestPath,
|
||||
std::vector<ModelSubAssetManifestEntry>& outEntries) {
|
||||
outEntries.clear();
|
||||
|
||||
std::ifstream input(manifestPath);
|
||||
if (!input.is_open()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string line;
|
||||
while (std::getline(input, line)) {
|
||||
if (line.empty() || line[0] == '#') {
|
||||
continue;
|
||||
}
|
||||
|
||||
const std::vector<std::string> fields = SplitFields(line);
|
||||
if (fields.size() < 3) {
|
||||
continue;
|
||||
}
|
||||
|
||||
ModelSubAssetManifestEntry entry;
|
||||
entry.localID = static_cast<LocalID>(std::stoull(fields[0]));
|
||||
entry.resourceType = static_cast<ResourceType>(std::stoul(fields[1]));
|
||||
entry.artifactPath = ToContainersString(fields[2]);
|
||||
if (entry.localID == kInvalidLocalID ||
|
||||
entry.resourceType == ResourceType::Unknown ||
|
||||
entry.artifactPath.Empty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
outEntries.push_back(entry);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool TryResolveModelSubAssetArtifactPath(const fs::path& manifestPath,
|
||||
const AssetRef& assetRef,
|
||||
Containers::String& outArtifactPath) {
|
||||
std::vector<ModelSubAssetManifestEntry> entries;
|
||||
if (!TryReadModelSubAssetManifest(manifestPath, entries)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const ModelSubAssetManifestEntry& entry : entries) {
|
||||
if (entry.localID != assetRef.localID || entry.resourceType != assetRef.resourceType) {
|
||||
continue;
|
||||
}
|
||||
|
||||
outArtifactPath = entry.artifactPath;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void WriteString(std::ofstream& stream, const Containers::String& value) {
|
||||
const Core::uint32 length = static_cast<Core::uint32>(value.Length());
|
||||
stream.write(reinterpret_cast<const char*>(&length), sizeof(length));
|
||||
@@ -825,6 +916,74 @@ bool AssetDatabase::TryGetAssetRef(const Containers::String& requestPath,
|
||||
return true;
|
||||
}
|
||||
|
||||
bool AssetDatabase::TryResolveAssetPath(const AssetRef& assetRef, Containers::String& outPath) {
|
||||
outPath.Clear();
|
||||
ClearLastErrorMessage();
|
||||
|
||||
if (!assetRef.IsValid()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (assetRef.localID == kMainAssetLocalID) {
|
||||
return TryGetPrimaryAssetPath(assetRef.assetGuid, outPath);
|
||||
}
|
||||
|
||||
const auto sourceIt = m_sourcesByGuid.find(assetRef.assetGuid);
|
||||
if (sourceIt == m_sourcesByGuid.end()) {
|
||||
SetLastErrorMessage(Containers::String("Unknown asset GUID for sub-asset path resolution: ") +
|
||||
assetRef.assetGuid.ToString());
|
||||
return false;
|
||||
}
|
||||
|
||||
const SourceAssetRecord& sourceRecord = sourceIt->second;
|
||||
const ResourceType primaryType = GetPrimaryResourceTypeForImporter(sourceRecord.importerName);
|
||||
if (primaryType == ResourceType::Unknown) {
|
||||
SetLastErrorMessage(Containers::String("Asset does not have an importable primary type: ") +
|
||||
sourceRecord.relativePath);
|
||||
return false;
|
||||
}
|
||||
|
||||
auto resolveFromArtifactRecord = [&](const ArtifactRecord& artifactRecord) -> bool {
|
||||
const fs::path manifestPath =
|
||||
fs::path(m_projectRoot.CStr()) /
|
||||
artifactRecord.artifactDirectory.CStr() /
|
||||
kModelSubAssetManifestFileName;
|
||||
|
||||
Containers::String artifactPath;
|
||||
if (!TryResolveModelSubAssetArtifactPath(manifestPath, assetRef, artifactPath)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
outPath = NormalizePathString(fs::path(m_projectRoot.CStr()) / artifactPath.CStr());
|
||||
return true;
|
||||
};
|
||||
|
||||
auto artifactIt = m_artifactsByGuid.find(assetRef.assetGuid);
|
||||
if (artifactIt != m_artifactsByGuid.end() &&
|
||||
!ShouldReimport(sourceRecord, &artifactIt->second) &&
|
||||
resolveFromArtifactRecord(artifactIt->second)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
ResolvedAsset resolvedAsset;
|
||||
if (!EnsureArtifact(sourceRecord.relativePath, primaryType, resolvedAsset)) {
|
||||
if (m_lastErrorMessage.Empty()) {
|
||||
SetLastErrorMessage(Containers::String("Failed to import asset while resolving sub-asset path: ") +
|
||||
sourceRecord.relativePath);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
artifactIt = m_artifactsByGuid.find(assetRef.assetGuid);
|
||||
if (artifactIt != m_artifactsByGuid.end() && resolveFromArtifactRecord(artifactIt->second)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
SetLastErrorMessage(Containers::String("Sub-asset localID was not found in artifact manifest: ") +
|
||||
sourceRecord.relativePath);
|
||||
return false;
|
||||
}
|
||||
|
||||
bool AssetDatabase::ReimportAsset(const Containers::String& requestPath,
|
||||
ResolvedAsset& outAsset,
|
||||
MaintenanceStats* outStats) {
|
||||
@@ -1442,6 +1601,9 @@ Containers::String AssetDatabase::GetImporterNameForPath(const Containers::Strin
|
||||
if (ext == ".obj" || ext == ".fbx" || ext == ".gltf" || ext == ".glb" || ext == ".dae" || ext == ".stl") {
|
||||
return Containers::String("ModelImporter");
|
||||
}
|
||||
if (ext == ".ply") {
|
||||
return Containers::String("GaussianSplatImporter");
|
||||
}
|
||||
if (ext == ".shader") {
|
||||
return Containers::String("ShaderImporter");
|
||||
}
|
||||
@@ -1468,7 +1630,10 @@ ResourceType AssetDatabase::GetPrimaryResourceTypeForImporter(const Containers::
|
||||
return ResourceType::Texture;
|
||||
}
|
||||
if (importerName == "ModelImporter") {
|
||||
return ResourceType::Mesh;
|
||||
return ResourceType::Model;
|
||||
}
|
||||
if (importerName == "GaussianSplatImporter") {
|
||||
return ResourceType::GaussianSplat;
|
||||
}
|
||||
if (importerName == "MaterialImporter") {
|
||||
return ResourceType::Material;
|
||||
@@ -1488,6 +1653,10 @@ bool AssetDatabase::ShouldReimport(const SourceAssetRecord& sourceRecord,
|
||||
return true;
|
||||
}
|
||||
|
||||
if (artifactRecord->resourceType != GetPrimaryResourceTypeForImporter(sourceRecord.importerName)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (artifactRecord->artifactKey.Empty() ||
|
||||
artifactRecord->mainArtifactPath.Empty()) {
|
||||
return true;
|
||||
@@ -1525,8 +1694,10 @@ bool AssetDatabase::ImportAsset(const SourceAssetRecord& sourceRecord,
|
||||
return ImportTextureAsset(sourceRecord, outRecord);
|
||||
case ResourceType::Material:
|
||||
return ImportMaterialAsset(sourceRecord, outRecord);
|
||||
case ResourceType::Mesh:
|
||||
case ResourceType::Model:
|
||||
return ImportModelAsset(sourceRecord, outRecord);
|
||||
case ResourceType::GaussianSplat:
|
||||
return ImportGaussianSplatAsset(sourceRecord, outRecord);
|
||||
case ResourceType::Shader:
|
||||
return ImportShaderAsset(sourceRecord, outRecord);
|
||||
case ResourceType::VolumeField:
|
||||
@@ -1764,34 +1935,51 @@ bool AssetDatabase::ImportMaterialAsset(const SourceAssetRecord& sourceRecord,
|
||||
|
||||
bool AssetDatabase::ImportModelAsset(const SourceAssetRecord& sourceRecord,
|
||||
ArtifactRecord& outRecord) {
|
||||
MeshLoader loader;
|
||||
const Containers::String absolutePath = NormalizePathString(fs::path(m_projectRoot.CStr()) / sourceRecord.relativePath.CStr());
|
||||
LoadResult result = loader.Load(absolutePath);
|
||||
if (!result || result.resource == nullptr) {
|
||||
const Containers::String absolutePath =
|
||||
NormalizePathString(fs::path(m_projectRoot.CStr()) / sourceRecord.relativePath.CStr());
|
||||
|
||||
MeshImportSettings importSettings;
|
||||
ImportedModelData importedModel;
|
||||
Containers::String importErrorMessage;
|
||||
if (!ImportAssimpModel(absolutePath, importSettings, importedModel, &importErrorMessage)) {
|
||||
if (!importErrorMessage.Empty()) {
|
||||
SetLastErrorMessage(importErrorMessage);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
Mesh* mesh = static_cast<Mesh*>(result.resource);
|
||||
std::vector<Containers::String> importedTexturePaths;
|
||||
importedTexturePaths.reserve(importedModel.textures.size());
|
||||
for (Texture* texture : importedModel.textures) {
|
||||
if (texture == nullptr || texture->GetPath().Empty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
importedTexturePaths.push_back(texture->GetPath());
|
||||
}
|
||||
|
||||
std::vector<ArtifactDependencyRecord> dependencies;
|
||||
CollectModelDependencies(sourceRecord, *mesh, dependencies);
|
||||
CollectModelDependencies(sourceRecord, importedTexturePaths, dependencies);
|
||||
const Containers::String artifactKey = BuildArtifactKey(sourceRecord, dependencies);
|
||||
const Containers::String artifactDir = BuildArtifactDirectory(artifactKey);
|
||||
const Containers::String mainArtifactPath = NormalizePathString(fs::path(artifactDir.CStr()) / "main.xcmesh");
|
||||
const Containers::String mainArtifactPath =
|
||||
NormalizePathString(fs::path(artifactDir.CStr()) / "main.xcmodel");
|
||||
|
||||
std::error_code ec;
|
||||
fs::remove_all(fs::path(m_projectRoot.CStr()) / artifactDir.CStr(), ec);
|
||||
ec.clear();
|
||||
fs::create_directories(fs::path(m_projectRoot.CStr()) / artifactDir.CStr(), ec);
|
||||
if (ec) {
|
||||
DestroyImportedMesh(mesh);
|
||||
importedModel.Reset();
|
||||
return false;
|
||||
}
|
||||
|
||||
bool writeOk = true;
|
||||
std::vector<ModelSubAssetManifestEntry> subAssetManifestEntries;
|
||||
std::unordered_map<const Texture*, Containers::String> textureArtifactPaths;
|
||||
std::unordered_map<const Texture*, AssetRef> textureAssetRefs;
|
||||
for (size_t textureIndex = 0; writeOk && textureIndex < mesh->GetTextures().Size(); ++textureIndex) {
|
||||
Texture* texture = mesh->GetTextures()[textureIndex];
|
||||
for (size_t textureIndex = 0; writeOk && textureIndex < importedModel.textures.size(); ++textureIndex) {
|
||||
Texture* texture = importedModel.textures[textureIndex];
|
||||
if (texture == nullptr) {
|
||||
continue;
|
||||
}
|
||||
@@ -1815,12 +2003,10 @@ bool AssetDatabase::ImportModelAsset(const SourceAssetRecord& sourceRecord,
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<Containers::String> materialArtifactPaths;
|
||||
materialArtifactPaths.reserve(mesh->GetMaterials().Size());
|
||||
for (size_t materialIndex = 0; writeOk && materialIndex < mesh->GetMaterials().Size(); ++materialIndex) {
|
||||
Material* material = mesh->GetMaterials()[materialIndex];
|
||||
if (material == nullptr) {
|
||||
materialArtifactPaths.emplace_back();
|
||||
std::unordered_map<LocalID, Containers::String> materialArtifactPathsByLocalID;
|
||||
for (size_t materialIndex = 0; writeOk && materialIndex < importedModel.materials.size(); ++materialIndex) {
|
||||
const ImportedModelMaterial& materialEntry = importedModel.materials[materialIndex];
|
||||
if (materialEntry.material == nullptr) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -1828,7 +2014,7 @@ bool AssetDatabase::ImportModelAsset(const SourceAssetRecord& sourceRecord,
|
||||
NormalizePathString(fs::path(artifactDir.CStr()) / ("material_" + std::to_string(materialIndex) + ".xcmat"));
|
||||
writeOk = WriteMaterialArtifactFile(
|
||||
fs::path(m_projectRoot.CStr()) / materialArtifactPath.CStr(),
|
||||
*material,
|
||||
*materialEntry.material,
|
||||
textureArtifactPaths,
|
||||
textureAssetRefs,
|
||||
this);
|
||||
@@ -1836,17 +2022,58 @@ bool AssetDatabase::ImportModelAsset(const SourceAssetRecord& sourceRecord,
|
||||
break;
|
||||
}
|
||||
|
||||
materialArtifactPaths.push_back(materialArtifactPath);
|
||||
materialArtifactPathsByLocalID.emplace(materialEntry.localID, materialArtifactPath);
|
||||
subAssetManifestEntries.push_back(
|
||||
ModelSubAssetManifestEntry{ materialEntry.localID, ResourceType::Material, materialArtifactPath });
|
||||
}
|
||||
|
||||
writeOk = writeOk &&
|
||||
WriteMeshArtifactFile(
|
||||
fs::path(m_projectRoot.CStr()) / mainArtifactPath.CStr(),
|
||||
*mesh,
|
||||
materialArtifactPaths);
|
||||
for (size_t meshIndex = 0; writeOk && meshIndex < importedModel.meshes.size(); ++meshIndex) {
|
||||
const ImportedModelMesh& meshEntry = importedModel.meshes[meshIndex];
|
||||
if (meshEntry.mesh == nullptr) {
|
||||
continue;
|
||||
}
|
||||
|
||||
DestroyImportedMesh(mesh);
|
||||
std::vector<Containers::String> meshMaterialArtifactPaths;
|
||||
meshMaterialArtifactPaths.reserve(meshEntry.materialLocalIDs.size());
|
||||
for (LocalID materialLocalID : meshEntry.materialLocalIDs) {
|
||||
const auto materialPathIt = materialArtifactPathsByLocalID.find(materialLocalID);
|
||||
meshMaterialArtifactPaths.push_back(
|
||||
materialPathIt != materialArtifactPathsByLocalID.end()
|
||||
? materialPathIt->second
|
||||
: Containers::String());
|
||||
}
|
||||
|
||||
const Containers::String meshArtifactPath =
|
||||
NormalizePathString(fs::path(artifactDir.CStr()) / ("mesh_" + std::to_string(meshIndex) + ".xcmesh"));
|
||||
writeOk = WriteMeshArtifactFile(
|
||||
fs::path(m_projectRoot.CStr()) / meshArtifactPath.CStr(),
|
||||
*meshEntry.mesh,
|
||||
meshMaterialArtifactPaths);
|
||||
if (writeOk) {
|
||||
subAssetManifestEntries.push_back(
|
||||
ModelSubAssetManifestEntry{ meshEntry.localID, ResourceType::Mesh, meshArtifactPath });
|
||||
}
|
||||
}
|
||||
|
||||
Containers::String modelWriteErrorMessage;
|
||||
if (writeOk) {
|
||||
writeOk = WriteModelArtifactFile(
|
||||
NormalizePathString(fs::path(m_projectRoot.CStr()) / mainArtifactPath.CStr()),
|
||||
*importedModel.model,
|
||||
&modelWriteErrorMessage);
|
||||
}
|
||||
|
||||
if (writeOk) {
|
||||
writeOk = WriteModelSubAssetManifest(
|
||||
fs::path(m_projectRoot.CStr()) / artifactDir.CStr() / kModelSubAssetManifestFileName,
|
||||
subAssetManifestEntries);
|
||||
}
|
||||
|
||||
importedModel.Reset();
|
||||
if (!writeOk) {
|
||||
if (!modelWriteErrorMessage.Empty()) {
|
||||
SetLastErrorMessage(modelWriteErrorMessage);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1854,7 +2081,7 @@ bool AssetDatabase::ImportModelAsset(const SourceAssetRecord& sourceRecord,
|
||||
outRecord.assetGuid = sourceRecord.guid;
|
||||
outRecord.importerName = sourceRecord.importerName;
|
||||
outRecord.importerVersion = sourceRecord.importerVersion;
|
||||
outRecord.resourceType = ResourceType::Mesh;
|
||||
outRecord.resourceType = ResourceType::Model;
|
||||
outRecord.artifactDirectory = artifactDir;
|
||||
outRecord.mainArtifactPath = mainArtifactPath;
|
||||
outRecord.sourceHash = sourceRecord.sourceHash;
|
||||
@@ -1919,6 +2146,65 @@ bool AssetDatabase::ImportShaderAsset(const SourceAssetRecord& sourceRecord,
|
||||
return true;
|
||||
}
|
||||
|
||||
bool AssetDatabase::ImportGaussianSplatAsset(const SourceAssetRecord& sourceRecord,
|
||||
ArtifactRecord& outRecord) {
|
||||
const Containers::String absolutePath =
|
||||
NormalizePathString(fs::path(m_projectRoot.CStr()) / sourceRecord.relativePath.CStr());
|
||||
|
||||
LoadResult result = Internal::ImportGaussianSplatPlyFile(absolutePath);
|
||||
if (!result || result.resource == nullptr) {
|
||||
if (!result.errorMessage.Empty()) {
|
||||
SetLastErrorMessage(result.errorMessage);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
GaussianSplat* gaussianSplat = static_cast<GaussianSplat*>(result.resource);
|
||||
const Containers::String artifactKey = BuildArtifactKey(sourceRecord);
|
||||
const Containers::String artifactDir = BuildArtifactDirectory(artifactKey);
|
||||
const Containers::String mainArtifactPath =
|
||||
NormalizePathString(fs::path(artifactDir.CStr()) / "main.xcgsplat");
|
||||
|
||||
std::error_code ec;
|
||||
fs::remove_all(fs::path(m_projectRoot.CStr()) / artifactDir.CStr(), ec);
|
||||
ec.clear();
|
||||
fs::create_directories(fs::path(m_projectRoot.CStr()) / artifactDir.CStr(), ec);
|
||||
if (ec) {
|
||||
delete gaussianSplat;
|
||||
return false;
|
||||
}
|
||||
|
||||
Containers::String writeErrorMessage;
|
||||
const Containers::String gaussianSplatArtifactWritePath =
|
||||
NormalizePathString(fs::path(m_projectRoot.CStr()) / mainArtifactPath.CStr());
|
||||
const bool writeOk = WriteGaussianSplatArtifactFile(
|
||||
gaussianSplatArtifactWritePath,
|
||||
*gaussianSplat,
|
||||
&writeErrorMessage);
|
||||
delete gaussianSplat;
|
||||
if (!writeOk) {
|
||||
if (!writeErrorMessage.Empty()) {
|
||||
SetLastErrorMessage(writeErrorMessage);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
outRecord.artifactKey = artifactKey;
|
||||
outRecord.assetGuid = sourceRecord.guid;
|
||||
outRecord.importerName = sourceRecord.importerName;
|
||||
outRecord.importerVersion = sourceRecord.importerVersion;
|
||||
outRecord.resourceType = ResourceType::GaussianSplat;
|
||||
outRecord.artifactDirectory = artifactDir;
|
||||
outRecord.mainArtifactPath = mainArtifactPath;
|
||||
outRecord.sourceHash = sourceRecord.sourceHash;
|
||||
outRecord.metaHash = sourceRecord.metaHash;
|
||||
outRecord.sourceFileSize = sourceRecord.sourceFileSize;
|
||||
outRecord.sourceWriteTime = sourceRecord.sourceWriteTime;
|
||||
outRecord.mainLocalID = kMainAssetLocalID;
|
||||
outRecord.dependencies.clear();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool AssetDatabase::ImportVolumeFieldAsset(const SourceAssetRecord& sourceRecord,
|
||||
ArtifactRecord& outRecord) {
|
||||
VolumeFieldLoader loader;
|
||||
@@ -2157,7 +2443,7 @@ bool AssetDatabase::AreDependenciesCurrent(
|
||||
}
|
||||
|
||||
bool AssetDatabase::CollectModelDependencies(const AssetDatabase::SourceAssetRecord& sourceRecord,
|
||||
const Mesh& mesh,
|
||||
const std::vector<Containers::String>& importedTexturePaths,
|
||||
std::vector<AssetDatabase::ArtifactDependencyRecord>& outDependencies) const {
|
||||
outDependencies.clear();
|
||||
|
||||
@@ -2177,12 +2463,12 @@ bool AssetDatabase::CollectModelDependencies(const AssetDatabase::SourceAssetRec
|
||||
}
|
||||
}
|
||||
|
||||
for (Texture* texture : mesh.GetTextures()) {
|
||||
if (texture == nullptr || texture->GetPath().Empty()) {
|
||||
for (const Containers::String& texturePathValue : importedTexturePaths) {
|
||||
if (texturePathValue.Empty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const std::string texturePath = ToStdString(texture->GetPath());
|
||||
const std::string texturePath = ToStdString(texturePathValue);
|
||||
if (texturePath.find('#') != std::string::npos) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -302,6 +302,16 @@ bool AssetImportService::TryGetAssetRef(const Containers::String& requestPath,
|
||||
return m_assetDatabase.TryGetAssetRef(requestPath, resourceType, outRef);
|
||||
}
|
||||
|
||||
bool AssetImportService::TryResolveAssetPath(const AssetRef& assetRef,
|
||||
Containers::String& outPath) {
|
||||
std::lock_guard<std::recursive_mutex> lock(m_mutex);
|
||||
if (m_projectRoot.Empty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return m_assetDatabase.TryResolveAssetPath(assetRef, outPath);
|
||||
}
|
||||
|
||||
bool AssetImportService::TryGetPrimaryAssetPath(const AssetGUID& guid, Containers::String& outRelativePath) const {
|
||||
std::lock_guard<std::recursive_mutex> lock(m_mutex);
|
||||
if (m_projectRoot.Empty()) {
|
||||
|
||||
@@ -131,13 +131,17 @@ bool ProjectAssetIndex::TryGetAssetRef(AssetImportService& importService,
|
||||
return resolved;
|
||||
}
|
||||
|
||||
bool ProjectAssetIndex::TryResolveAssetPath(const AssetImportService& importService,
|
||||
bool ProjectAssetIndex::TryResolveAssetPath(AssetImportService& importService,
|
||||
const AssetRef& assetRef,
|
||||
Containers::String& outPath) const {
|
||||
if (!assetRef.IsValid()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (assetRef.localID != kMainAssetLocalID) {
|
||||
return importService.TryResolveAssetPath(assetRef, outPath);
|
||||
}
|
||||
|
||||
bool resolved = false;
|
||||
{
|
||||
std::shared_lock<std::shared_mutex> lock(m_mutex);
|
||||
@@ -149,7 +153,7 @@ bool ProjectAssetIndex::TryResolveAssetPath(const AssetImportService& importServ
|
||||
}
|
||||
|
||||
if (!resolved) {
|
||||
resolved = importService.TryGetPrimaryAssetPath(assetRef.assetGuid, outPath);
|
||||
resolved = importService.TryResolveAssetPath(assetRef, outPath);
|
||||
if (resolved) {
|
||||
const_cast<ProjectAssetIndex*>(this)->RememberResolvedPath(assetRef.assetGuid, outPath);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
#include <XCEngine/Core/Asset/ResourceHandle.h>
|
||||
#include <XCEngine/Core/Asset/ResourceTypes.h>
|
||||
#include <XCEngine/Core/IO/ResourceFileSystem.h>
|
||||
#include <XCEngine/Resources/GaussianSplat/GaussianSplatLoader.h>
|
||||
#include <XCEngine/Resources/Material/MaterialLoader.h>
|
||||
#include <XCEngine/Resources/Model/ModelLoader.h>
|
||||
#include <XCEngine/Resources/Mesh/MeshLoader.h>
|
||||
#include <XCEngine/Resources/Shader/ShaderLoader.h>
|
||||
#include <XCEngine/Resources/Texture/TextureLoader.h>
|
||||
@@ -43,7 +45,9 @@ void RegisterBuiltinLoader(ResourceManager& manager, TLoader& loader) {
|
||||
}
|
||||
}
|
||||
|
||||
GaussianSplatLoader g_gaussianSplatLoader;
|
||||
MaterialLoader g_materialLoader;
|
||||
ModelLoader g_modelLoader;
|
||||
MeshLoader g_meshLoader;
|
||||
ShaderLoader g_shaderLoader;
|
||||
TextureLoader g_textureLoader;
|
||||
@@ -87,7 +91,9 @@ void ResourceManager::EnsureInitialized() {
|
||||
Core::UniqueRef<AsyncLoader> asyncLoader = Core::MakeUnique<AsyncLoader>();
|
||||
asyncLoader->Initialize(2);
|
||||
|
||||
RegisterBuiltinLoader(*this, g_gaussianSplatLoader);
|
||||
RegisterBuiltinLoader(*this, g_materialLoader);
|
||||
RegisterBuiltinLoader(*this, g_modelLoader);
|
||||
RegisterBuiltinLoader(*this, g_meshLoader);
|
||||
RegisterBuiltinLoader(*this, g_shaderLoader);
|
||||
RegisterBuiltinLoader(*this, g_textureLoader);
|
||||
@@ -522,8 +528,6 @@ LoadResult ResourceManager::LoadResource(const Containers::String& path,
|
||||
ImportSettings* settings) {
|
||||
EnsureInitialized();
|
||||
|
||||
const ResourceGUID guid = ResourceGUID::Generate(path);
|
||||
|
||||
if (ShouldTraceResourcePath(path)) {
|
||||
Debug::Logger::Get().Info(
|
||||
Debug::LogCategory::FileSystem,
|
||||
@@ -535,15 +539,6 @@ LoadResult ResourceManager::LoadResource(const Containers::String& path,
|
||||
m_resourceRoot);
|
||||
}
|
||||
|
||||
if (IResource* cached = FindInCache(guid)) {
|
||||
if (ShouldTraceResourcePath(path)) {
|
||||
Debug::Logger::Get().Info(
|
||||
Debug::LogCategory::FileSystem,
|
||||
Containers::String("[ResourceManager] LoadResource cache-hit path=") + path);
|
||||
}
|
||||
return LoadResult(cached);
|
||||
}
|
||||
|
||||
IResourceLoader* loader = FindLoader(type);
|
||||
if (loader == nullptr) {
|
||||
Debug::Logger::Get().Warning(Debug::LogCategory::FileSystem,
|
||||
@@ -552,6 +547,50 @@ LoadResult ResourceManager::LoadResource(const Containers::String& path,
|
||||
return LoadResult(false, "Loader not found");
|
||||
}
|
||||
|
||||
Containers::String loadPath = path;
|
||||
AssetImportService::ImportedAsset resolvedAsset;
|
||||
ResourceType importableType = ResourceType::Unknown;
|
||||
const bool shouldUseProjectArtifact =
|
||||
!m_resourceRoot.Empty() &&
|
||||
m_assetImportService.TryGetImportableResourceType(path, importableType) &&
|
||||
importableType == type;
|
||||
|
||||
if (shouldUseProjectArtifact &&
|
||||
m_assetImportService.EnsureArtifact(path, type, resolvedAsset) &&
|
||||
resolvedAsset.artifactReady) {
|
||||
m_projectAssetIndex.RememberResolvedPath(resolvedAsset.assetGuid, resolvedAsset.relativePath);
|
||||
loadPath = resolvedAsset.runtimeLoadPath;
|
||||
if (ShouldTraceResourcePath(path)) {
|
||||
Debug::Logger::Get().Info(
|
||||
Debug::LogCategory::FileSystem,
|
||||
Containers::String("[ResourceManager] LoadResource artifact path=") +
|
||||
path +
|
||||
" artifact=" +
|
||||
loadPath);
|
||||
}
|
||||
} else if (ShouldTraceResourcePath(path)) {
|
||||
Debug::Logger::Get().Info(
|
||||
Debug::LogCategory::FileSystem,
|
||||
Containers::String("[ResourceManager] LoadResource direct path=") +
|
||||
path +
|
||||
" loadPath=" +
|
||||
loadPath);
|
||||
}
|
||||
|
||||
const ResourceGUID guid = ResourceGUID::Generate(loadPath);
|
||||
|
||||
if (IResource* cached = FindInCache(guid)) {
|
||||
if (ShouldTraceResourcePath(path)) {
|
||||
Debug::Logger::Get().Info(
|
||||
Debug::LogCategory::FileSystem,
|
||||
Containers::String("[ResourceManager] LoadResource cache-hit path=") +
|
||||
path +
|
||||
" loadPath=" +
|
||||
loadPath);
|
||||
}
|
||||
return LoadResult(cached);
|
||||
}
|
||||
|
||||
const InFlightLoadKey inFlightKey{ guid, type };
|
||||
std::shared_ptr<InFlightLoadState> inFlightState;
|
||||
bool shouldExecuteLoad = false;
|
||||
@@ -610,36 +649,6 @@ LoadResult ResourceManager::LoadResource(const Containers::String& path,
|
||||
: Containers::String("In-flight load completed without cached resource"));
|
||||
}
|
||||
|
||||
Containers::String loadPath = path;
|
||||
AssetImportService::ImportedAsset resolvedAsset;
|
||||
ResourceType importableType = ResourceType::Unknown;
|
||||
const bool shouldUseProjectArtifact =
|
||||
!m_resourceRoot.Empty() &&
|
||||
m_assetImportService.TryGetImportableResourceType(path, importableType) &&
|
||||
importableType == type;
|
||||
|
||||
if (shouldUseProjectArtifact &&
|
||||
m_assetImportService.EnsureArtifact(path, type, resolvedAsset) &&
|
||||
resolvedAsset.artifactReady) {
|
||||
m_projectAssetIndex.RememberResolvedPath(resolvedAsset.assetGuid, resolvedAsset.relativePath);
|
||||
loadPath = resolvedAsset.runtimeLoadPath;
|
||||
if (ShouldTraceResourcePath(path)) {
|
||||
Debug::Logger::Get().Info(
|
||||
Debug::LogCategory::FileSystem,
|
||||
Containers::String("[ResourceManager] LoadResource artifact path=") +
|
||||
path +
|
||||
" artifact=" +
|
||||
loadPath);
|
||||
}
|
||||
} else if (ShouldTraceResourcePath(path)) {
|
||||
Debug::Logger::Get().Info(
|
||||
Debug::LogCategory::FileSystem,
|
||||
Containers::String("[ResourceManager] LoadResource direct path=") +
|
||||
path +
|
||||
" loadPath=" +
|
||||
loadPath);
|
||||
}
|
||||
|
||||
LoadResult result;
|
||||
try {
|
||||
result = loader->Load(loadPath, settings);
|
||||
|
||||
109
engine/src/Resources/GaussianSplat/GaussianSplat.cpp
Normal file
109
engine/src/Resources/GaussianSplat/GaussianSplat.cpp
Normal file
@@ -0,0 +1,109 @@
|
||||
#include <XCEngine/Resources/GaussianSplat/GaussianSplat.h>
|
||||
|
||||
namespace XCEngine {
|
||||
namespace Resources {
|
||||
|
||||
GaussianSplat::GaussianSplat() = default;
|
||||
|
||||
GaussianSplat::~GaussianSplat() = default;
|
||||
|
||||
void GaussianSplat::Release() {
|
||||
delete this;
|
||||
}
|
||||
|
||||
bool GaussianSplat::CreateOwned(const GaussianSplatMetadata& metadata,
|
||||
Containers::Array<GaussianSplatSection>&& sections,
|
||||
Containers::Array<Core::uint8>&& payload) {
|
||||
if (!ValidateSections(sections, payload.Size())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
m_metadata = metadata;
|
||||
m_sections = std::move(sections);
|
||||
m_payload = std::move(payload);
|
||||
m_isValid = true;
|
||||
UpdateMemorySize();
|
||||
return true;
|
||||
}
|
||||
|
||||
void GaussianSplat::Clear() {
|
||||
m_metadata = {};
|
||||
m_sections.Clear();
|
||||
m_payload.Clear();
|
||||
m_isValid = false;
|
||||
UpdateMemorySize();
|
||||
}
|
||||
|
||||
const GaussianSplatSection* GaussianSplat::FindSection(GaussianSplatSectionType type) const {
|
||||
for (const GaussianSplatSection& section : m_sections) {
|
||||
if (section.type == type) {
|
||||
return §ion;
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const void* GaussianSplat::GetSectionData(GaussianSplatSectionType type) const {
|
||||
const GaussianSplatSection* section = FindSection(type);
|
||||
if (section == nullptr || section->dataSize == 0) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return m_payload.Data() + static_cast<size_t>(section->dataOffset);
|
||||
}
|
||||
|
||||
const GaussianSplatPositionRecord* GaussianSplat::GetPositionRecords() const {
|
||||
return static_cast<const GaussianSplatPositionRecord*>(GetSectionData(GaussianSplatSectionType::Positions));
|
||||
}
|
||||
|
||||
const GaussianSplatOtherRecord* GaussianSplat::GetOtherRecords() const {
|
||||
return static_cast<const GaussianSplatOtherRecord*>(GetSectionData(GaussianSplatSectionType::Other));
|
||||
}
|
||||
|
||||
const GaussianSplatColorRecord* GaussianSplat::GetColorRecords() const {
|
||||
return static_cast<const GaussianSplatColorRecord*>(GetSectionData(GaussianSplatSectionType::Color));
|
||||
}
|
||||
|
||||
const GaussianSplatSHRecord* GaussianSplat::GetSHRecords() const {
|
||||
return static_cast<const GaussianSplatSHRecord*>(GetSectionData(GaussianSplatSectionType::SH));
|
||||
}
|
||||
|
||||
bool GaussianSplat::ValidateSections(const Containers::Array<GaussianSplatSection>& sections,
|
||||
size_t payloadSize) const {
|
||||
for (size_t index = 0; index < sections.Size(); ++index) {
|
||||
const GaussianSplatSection& section = sections[index];
|
||||
if (section.type == GaussianSplatSectionType::Unknown) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (section.dataOffset > payloadSize || section.dataSize > payloadSize) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const Core::uint64 sectionEnd = section.dataOffset + section.dataSize;
|
||||
if (sectionEnd < section.dataOffset || sectionEnd > payloadSize) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (size_t otherIndex = index + 1; otherIndex < sections.Size(); ++otherIndex) {
|
||||
if (sections[otherIndex].type == section.type) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void GaussianSplat::UpdateMemorySize() {
|
||||
m_memorySize =
|
||||
sizeof(GaussianSplat) +
|
||||
m_name.Length() +
|
||||
m_path.Length() +
|
||||
(m_sections.Size() * sizeof(GaussianSplatSection)) +
|
||||
m_payload.Size();
|
||||
}
|
||||
|
||||
} // namespace Resources
|
||||
} // namespace XCEngine
|
||||
202
engine/src/Resources/GaussianSplat/GaussianSplatArtifactIO.cpp
Normal file
202
engine/src/Resources/GaussianSplat/GaussianSplatArtifactIO.cpp
Normal file
@@ -0,0 +1,202 @@
|
||||
#include <XCEngine/Resources/GaussianSplat/GaussianSplatArtifactIO.h>
|
||||
|
||||
#include <XCEngine/Core/Asset/ArtifactFormats.h>
|
||||
#include <XCEngine/Core/Asset/ResourceManager.h>
|
||||
#include <XCEngine/Resources/GaussianSplat/GaussianSplat.h>
|
||||
|
||||
#include <cstring>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <string>
|
||||
|
||||
namespace XCEngine {
|
||||
namespace Resources {
|
||||
|
||||
namespace {
|
||||
|
||||
Containers::String GetResourceNameFromPath(const Containers::String& path) {
|
||||
const std::filesystem::path filePath(path.CStr());
|
||||
const std::string fileName = filePath.filename().string();
|
||||
if (!fileName.empty()) {
|
||||
return Containers::String(fileName.c_str());
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
std::filesystem::path ResolveArtifactPath(const Containers::String& path) {
|
||||
std::filesystem::path resolvedPath(path.CStr());
|
||||
if (!resolvedPath.is_absolute() && !std::filesystem::exists(resolvedPath)) {
|
||||
const Containers::String& resourceRoot = ResourceManager::Get().GetResourceRoot();
|
||||
if (!resourceRoot.Empty()) {
|
||||
resolvedPath = std::filesystem::path(resourceRoot.CStr()) / resolvedPath;
|
||||
}
|
||||
}
|
||||
|
||||
return resolvedPath.lexically_normal();
|
||||
}
|
||||
|
||||
LoadResult CreateOwnedGaussianSplatResource(const Containers::String& path,
|
||||
const GaussianSplatMetadata& metadata,
|
||||
Containers::Array<GaussianSplatSection>&& sections,
|
||||
Containers::Array<Core::uint8>&& payload) {
|
||||
auto* gaussianSplat = new GaussianSplat();
|
||||
|
||||
IResource::ConstructParams params;
|
||||
params.name = GetResourceNameFromPath(path);
|
||||
params.path = path;
|
||||
params.guid = ResourceGUID::Generate(path);
|
||||
params.memorySize = payload.Size();
|
||||
gaussianSplat->Initialize(params);
|
||||
|
||||
if (!gaussianSplat->CreateOwned(metadata, std::move(sections), std::move(payload))) {
|
||||
delete gaussianSplat;
|
||||
return LoadResult(Containers::String("Failed to create GaussianSplat resource: ") + path);
|
||||
}
|
||||
|
||||
return LoadResult(gaussianSplat);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool WriteGaussianSplatArtifactFile(const Containers::String& artifactPath,
|
||||
const GaussianSplat& gaussianSplat,
|
||||
Containers::String* outErrorMessage) {
|
||||
const std::filesystem::path resolvedPath = ResolveArtifactPath(artifactPath);
|
||||
std::error_code ec;
|
||||
const std::filesystem::path parentPath = resolvedPath.parent_path();
|
||||
if (!parentPath.empty()) {
|
||||
std::filesystem::create_directories(parentPath, ec);
|
||||
if (ec) {
|
||||
if (outErrorMessage != nullptr) {
|
||||
*outErrorMessage =
|
||||
Containers::String("Failed to create GaussianSplat artifact directory: ") +
|
||||
Containers::String(parentPath.generic_string().c_str());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
std::ofstream output(resolvedPath, std::ios::binary | std::ios::trunc);
|
||||
if (!output.is_open()) {
|
||||
if (outErrorMessage != nullptr) {
|
||||
*outErrorMessage = Containers::String("Failed to open GaussianSplat artifact for write: ") + artifactPath;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
GaussianSplatArtifactFileHeader fileHeader;
|
||||
output.write(reinterpret_cast<const char*>(&fileHeader), sizeof(fileHeader));
|
||||
|
||||
const GaussianSplatMetadata& metadata = gaussianSplat.GetMetadata();
|
||||
GaussianSplatArtifactHeader header;
|
||||
header.contentVersion = metadata.contentVersion;
|
||||
header.splatCount = metadata.splatCount;
|
||||
header.chunkCount = metadata.chunkCount;
|
||||
header.cameraCount = metadata.cameraCount;
|
||||
header.boundsMin = metadata.bounds.GetMin();
|
||||
header.boundsMax = metadata.bounds.GetMax();
|
||||
header.positionFormat = static_cast<Core::uint32>(metadata.positionFormat);
|
||||
header.otherFormat = static_cast<Core::uint32>(metadata.otherFormat);
|
||||
header.colorFormat = static_cast<Core::uint32>(metadata.colorFormat);
|
||||
header.shFormat = static_cast<Core::uint32>(metadata.shFormat);
|
||||
header.chunkFormat = static_cast<Core::uint32>(metadata.chunkFormat);
|
||||
header.cameraFormat = static_cast<Core::uint32>(metadata.cameraFormat);
|
||||
header.sectionCount = static_cast<Core::uint32>(gaussianSplat.GetSections().Size());
|
||||
header.payloadSize = static_cast<Core::uint64>(gaussianSplat.GetPayloadSize());
|
||||
output.write(reinterpret_cast<const char*>(&header), sizeof(header));
|
||||
|
||||
for (const GaussianSplatSection& section : gaussianSplat.GetSections()) {
|
||||
GaussianSplatArtifactSectionRecord sectionRecord;
|
||||
sectionRecord.sectionType = static_cast<Core::uint32>(section.type);
|
||||
sectionRecord.format = static_cast<Core::uint32>(section.format);
|
||||
sectionRecord.payloadOffset = section.dataOffset;
|
||||
sectionRecord.dataSize = section.dataSize;
|
||||
sectionRecord.elementCount = section.elementCount;
|
||||
sectionRecord.elementStride = section.elementStride;
|
||||
output.write(reinterpret_cast<const char*>(§ionRecord), sizeof(sectionRecord));
|
||||
}
|
||||
|
||||
if (gaussianSplat.GetPayloadSize() > 0) {
|
||||
output.write(reinterpret_cast<const char*>(gaussianSplat.GetPayloadData()), gaussianSplat.GetPayloadSize());
|
||||
}
|
||||
|
||||
if (!output && outErrorMessage != nullptr) {
|
||||
*outErrorMessage = Containers::String("Failed to write GaussianSplat artifact: ") + artifactPath;
|
||||
}
|
||||
|
||||
return static_cast<bool>(output);
|
||||
}
|
||||
|
||||
LoadResult LoadGaussianSplatArtifact(const Containers::String& path) {
|
||||
const std::filesystem::path resolvedPath = ResolveArtifactPath(path);
|
||||
|
||||
std::ifstream input(resolvedPath, std::ios::binary);
|
||||
if (!input.is_open()) {
|
||||
return LoadResult(Containers::String("Failed to read GaussianSplat artifact: ") + path);
|
||||
}
|
||||
|
||||
GaussianSplatArtifactFileHeader fileHeader;
|
||||
input.read(reinterpret_cast<char*>(&fileHeader), sizeof(fileHeader));
|
||||
if (!input) {
|
||||
return LoadResult(Containers::String("Failed to parse GaussianSplat artifact file header: ") + path);
|
||||
}
|
||||
|
||||
const bool validFileHeader =
|
||||
std::memcmp(fileHeader.magic, "XCGSP01", 7) == 0 &&
|
||||
fileHeader.schemaVersion == kGaussianSplatArtifactSchemaVersion;
|
||||
if (!validFileHeader) {
|
||||
return LoadResult(Containers::String("Invalid GaussianSplat artifact file header: ") + path);
|
||||
}
|
||||
|
||||
GaussianSplatArtifactHeader header;
|
||||
input.read(reinterpret_cast<char*>(&header), sizeof(header));
|
||||
if (!input) {
|
||||
return LoadResult(Containers::String("Failed to parse GaussianSplat artifact header: ") + path);
|
||||
}
|
||||
|
||||
Containers::Array<GaussianSplatSection> sections;
|
||||
sections.Reserve(header.sectionCount);
|
||||
for (Core::uint32 index = 0; index < header.sectionCount; ++index) {
|
||||
GaussianSplatArtifactSectionRecord sectionRecord;
|
||||
input.read(reinterpret_cast<char*>(§ionRecord), sizeof(sectionRecord));
|
||||
if (!input) {
|
||||
return LoadResult(Containers::String("Failed to read GaussianSplat artifact section table: ") + path);
|
||||
}
|
||||
|
||||
GaussianSplatSection section;
|
||||
section.type = static_cast<GaussianSplatSectionType>(sectionRecord.sectionType);
|
||||
section.format = static_cast<GaussianSplatSectionFormat>(sectionRecord.format);
|
||||
section.dataOffset = sectionRecord.payloadOffset;
|
||||
section.dataSize = sectionRecord.dataSize;
|
||||
section.elementCount = sectionRecord.elementCount;
|
||||
section.elementStride = sectionRecord.elementStride;
|
||||
sections.PushBack(section);
|
||||
}
|
||||
|
||||
Containers::Array<Core::uint8> payload;
|
||||
payload.Resize(static_cast<size_t>(header.payloadSize));
|
||||
if (header.payloadSize > 0) {
|
||||
input.read(reinterpret_cast<char*>(payload.Data()), static_cast<std::streamsize>(header.payloadSize));
|
||||
if (!input) {
|
||||
return LoadResult(Containers::String("Failed to read GaussianSplat artifact payload: ") + path);
|
||||
}
|
||||
}
|
||||
|
||||
GaussianSplatMetadata metadata;
|
||||
metadata.contentVersion = header.contentVersion;
|
||||
metadata.splatCount = header.splatCount;
|
||||
metadata.chunkCount = header.chunkCount;
|
||||
metadata.cameraCount = header.cameraCount;
|
||||
metadata.bounds.SetMinMax(header.boundsMin, header.boundsMax);
|
||||
metadata.positionFormat = static_cast<GaussianSplatSectionFormat>(header.positionFormat);
|
||||
metadata.otherFormat = static_cast<GaussianSplatSectionFormat>(header.otherFormat);
|
||||
metadata.colorFormat = static_cast<GaussianSplatSectionFormat>(header.colorFormat);
|
||||
metadata.shFormat = static_cast<GaussianSplatSectionFormat>(header.shFormat);
|
||||
metadata.chunkFormat = static_cast<GaussianSplatSectionFormat>(header.chunkFormat);
|
||||
metadata.cameraFormat = static_cast<GaussianSplatSectionFormat>(header.cameraFormat);
|
||||
|
||||
return CreateOwnedGaussianSplatResource(path, metadata, std::move(sections), std::move(payload));
|
||||
}
|
||||
|
||||
} // namespace Resources
|
||||
} // namespace XCEngine
|
||||
40
engine/src/Resources/GaussianSplat/GaussianSplatLoader.cpp
Normal file
40
engine/src/Resources/GaussianSplat/GaussianSplatLoader.cpp
Normal file
@@ -0,0 +1,40 @@
|
||||
#include <XCEngine/Resources/GaussianSplat/GaussianSplatLoader.h>
|
||||
|
||||
#include <XCEngine/Core/Asset/ResourceManager.h>
|
||||
#include <XCEngine/Resources/GaussianSplat/GaussianSplatArtifactIO.h>
|
||||
|
||||
namespace XCEngine {
|
||||
namespace Resources {
|
||||
|
||||
GaussianSplatLoader::GaussianSplatLoader() = default;
|
||||
|
||||
GaussianSplatLoader::~GaussianSplatLoader() = default;
|
||||
|
||||
Containers::Array<Containers::String> GaussianSplatLoader::GetSupportedExtensions() const {
|
||||
Containers::Array<Containers::String> extensions;
|
||||
extensions.PushBack("xcgsplat");
|
||||
return extensions;
|
||||
}
|
||||
|
||||
bool GaussianSplatLoader::CanLoad(const Containers::String& path) const {
|
||||
return GetExtension(path).ToLower() == "xcgsplat";
|
||||
}
|
||||
|
||||
LoadResult GaussianSplatLoader::Load(const Containers::String& path, const ImportSettings* settings) {
|
||||
(void)settings;
|
||||
|
||||
if (!CanLoad(path)) {
|
||||
return LoadResult(Containers::String("Unsupported GaussianSplat format: ") + GetExtension(path).ToLower());
|
||||
}
|
||||
|
||||
return LoadGaussianSplatArtifact(path);
|
||||
}
|
||||
|
||||
ImportSettings* GaussianSplatLoader::GetDefaultSettings() const {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
REGISTER_RESOURCE_LOADER(GaussianSplatLoader);
|
||||
|
||||
} // namespace Resources
|
||||
} // namespace XCEngine
|
||||
@@ -0,0 +1,634 @@
|
||||
#include "Resources/GaussianSplat/Internal/GaussianSplatPlyImporter.h"
|
||||
|
||||
#include <XCEngine/Core/Asset/ResourceManager.h>
|
||||
#include <XCEngine/Resources/GaussianSplat/GaussianSplat.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <limits>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
|
||||
namespace XCEngine {
|
||||
namespace Resources {
|
||||
namespace Internal {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr float kSHC0 = 0.2820948f;
|
||||
|
||||
enum class PlyScalarType {
|
||||
Invalid = 0,
|
||||
Int8,
|
||||
UInt8,
|
||||
Int16,
|
||||
UInt16,
|
||||
Int32,
|
||||
UInt32,
|
||||
Float32,
|
||||
Float64
|
||||
};
|
||||
|
||||
enum class GaussianSplatSemantic {
|
||||
Ignore = 0,
|
||||
PositionX,
|
||||
PositionY,
|
||||
PositionZ,
|
||||
DC0R,
|
||||
DC0G,
|
||||
DC0B,
|
||||
Opacity,
|
||||
Scale0,
|
||||
Scale1,
|
||||
Scale2,
|
||||
Rot0,
|
||||
Rot1,
|
||||
Rot2,
|
||||
Rot3,
|
||||
SHRest0
|
||||
};
|
||||
|
||||
struct PlyPropertyDesc {
|
||||
std::string name;
|
||||
PlyScalarType type = PlyScalarType::Invalid;
|
||||
GaussianSplatSemantic semantic = GaussianSplatSemantic::Ignore;
|
||||
Core::uint32 shRestIndex = 0;
|
||||
};
|
||||
|
||||
struct ParsedPlyHeader {
|
||||
Core::uint32 vertexCount = 0;
|
||||
Core::uint32 vertexStride = 0;
|
||||
std::vector<PlyPropertyDesc> vertexProperties;
|
||||
};
|
||||
|
||||
Containers::String ToContainersString(const std::string& value) {
|
||||
return Containers::String(value.c_str());
|
||||
}
|
||||
|
||||
std::string TrimAscii(const std::string& value) {
|
||||
const auto begin = std::find_if_not(value.begin(), value.end(), [](unsigned char ch) {
|
||||
return std::isspace(ch) != 0;
|
||||
});
|
||||
const auto end = std::find_if_not(value.rbegin(), value.rend(), [](unsigned char ch) {
|
||||
return std::isspace(ch) != 0;
|
||||
}).base();
|
||||
|
||||
if (begin >= end) {
|
||||
return std::string();
|
||||
}
|
||||
|
||||
return std::string(begin, end);
|
||||
}
|
||||
|
||||
PlyScalarType ParsePlyScalarType(const std::string& typeName) {
|
||||
if (typeName == "char" || typeName == "int8") {
|
||||
return PlyScalarType::Int8;
|
||||
}
|
||||
if (typeName == "uchar" || typeName == "uint8") {
|
||||
return PlyScalarType::UInt8;
|
||||
}
|
||||
if (typeName == "short" || typeName == "int16") {
|
||||
return PlyScalarType::Int16;
|
||||
}
|
||||
if (typeName == "ushort" || typeName == "uint16") {
|
||||
return PlyScalarType::UInt16;
|
||||
}
|
||||
if (typeName == "int" || typeName == "int32") {
|
||||
return PlyScalarType::Int32;
|
||||
}
|
||||
if (typeName == "uint" || typeName == "uint32") {
|
||||
return PlyScalarType::UInt32;
|
||||
}
|
||||
if (typeName == "float" || typeName == "float32") {
|
||||
return PlyScalarType::Float32;
|
||||
}
|
||||
if (typeName == "double" || typeName == "float64") {
|
||||
return PlyScalarType::Float64;
|
||||
}
|
||||
return PlyScalarType::Invalid;
|
||||
}
|
||||
|
||||
Core::uint32 GetPlyScalarTypeSize(PlyScalarType type) {
|
||||
switch (type) {
|
||||
case PlyScalarType::Int8:
|
||||
case PlyScalarType::UInt8:
|
||||
return 1u;
|
||||
case PlyScalarType::Int16:
|
||||
case PlyScalarType::UInt16:
|
||||
return 2u;
|
||||
case PlyScalarType::Int32:
|
||||
case PlyScalarType::UInt32:
|
||||
case PlyScalarType::Float32:
|
||||
return 4u;
|
||||
case PlyScalarType::Float64:
|
||||
return 8u;
|
||||
default:
|
||||
return 0u;
|
||||
}
|
||||
}
|
||||
|
||||
bool TryParseSHRestIndex(const std::string& propertyName, Core::uint32& outIndex) {
|
||||
static constexpr const char* kPrefix = "f_rest_";
|
||||
if (propertyName.rfind(kPrefix, 0) != 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const std::string suffix = propertyName.substr(std::char_traits<char>::length(kPrefix));
|
||||
if (suffix.empty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (char ch : suffix) {
|
||||
if (!std::isdigit(static_cast<unsigned char>(ch))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
outIndex = static_cast<Core::uint32>(std::stoul(suffix));
|
||||
return true;
|
||||
}
|
||||
|
||||
GaussianSplatSemantic MapPropertySemantic(const std::string& propertyName, Core::uint32& outSHRestIndex) {
|
||||
outSHRestIndex = 0u;
|
||||
|
||||
if (propertyName == "x") return GaussianSplatSemantic::PositionX;
|
||||
if (propertyName == "y") return GaussianSplatSemantic::PositionY;
|
||||
if (propertyName == "z") return GaussianSplatSemantic::PositionZ;
|
||||
if (propertyName == "f_dc_0") return GaussianSplatSemantic::DC0R;
|
||||
if (propertyName == "f_dc_1") return GaussianSplatSemantic::DC0G;
|
||||
if (propertyName == "f_dc_2") return GaussianSplatSemantic::DC0B;
|
||||
if (propertyName == "opacity") return GaussianSplatSemantic::Opacity;
|
||||
if (propertyName == "scale_0") return GaussianSplatSemantic::Scale0;
|
||||
if (propertyName == "scale_1") return GaussianSplatSemantic::Scale1;
|
||||
if (propertyName == "scale_2") return GaussianSplatSemantic::Scale2;
|
||||
if (propertyName == "rot_0") return GaussianSplatSemantic::Rot0;
|
||||
if (propertyName == "rot_1") return GaussianSplatSemantic::Rot1;
|
||||
if (propertyName == "rot_2") return GaussianSplatSemantic::Rot2;
|
||||
if (propertyName == "rot_3") return GaussianSplatSemantic::Rot3;
|
||||
if (TryParseSHRestIndex(propertyName, outSHRestIndex)) return GaussianSplatSemantic::SHRest0;
|
||||
return GaussianSplatSemantic::Ignore;
|
||||
}
|
||||
|
||||
bool ReadScalarAsFloat(std::ifstream& input, PlyScalarType type, float& outValue) {
|
||||
switch (type) {
|
||||
case PlyScalarType::Int8: {
|
||||
std::int8_t value = 0;
|
||||
input.read(reinterpret_cast<char*>(&value), sizeof(value));
|
||||
outValue = static_cast<float>(value);
|
||||
return static_cast<bool>(input);
|
||||
}
|
||||
case PlyScalarType::UInt8: {
|
||||
std::uint8_t value = 0;
|
||||
input.read(reinterpret_cast<char*>(&value), sizeof(value));
|
||||
outValue = static_cast<float>(value);
|
||||
return static_cast<bool>(input);
|
||||
}
|
||||
case PlyScalarType::Int16: {
|
||||
std::int16_t value = 0;
|
||||
input.read(reinterpret_cast<char*>(&value), sizeof(value));
|
||||
outValue = static_cast<float>(value);
|
||||
return static_cast<bool>(input);
|
||||
}
|
||||
case PlyScalarType::UInt16: {
|
||||
std::uint16_t value = 0;
|
||||
input.read(reinterpret_cast<char*>(&value), sizeof(value));
|
||||
outValue = static_cast<float>(value);
|
||||
return static_cast<bool>(input);
|
||||
}
|
||||
case PlyScalarType::Int32: {
|
||||
std::int32_t value = 0;
|
||||
input.read(reinterpret_cast<char*>(&value), sizeof(value));
|
||||
outValue = static_cast<float>(value);
|
||||
return static_cast<bool>(input);
|
||||
}
|
||||
case PlyScalarType::UInt32: {
|
||||
std::uint32_t value = 0;
|
||||
input.read(reinterpret_cast<char*>(&value), sizeof(value));
|
||||
outValue = static_cast<float>(value);
|
||||
return static_cast<bool>(input);
|
||||
}
|
||||
case PlyScalarType::Float32: {
|
||||
float value = 0.0f;
|
||||
input.read(reinterpret_cast<char*>(&value), sizeof(value));
|
||||
outValue = value;
|
||||
return static_cast<bool>(input);
|
||||
}
|
||||
case PlyScalarType::Float64: {
|
||||
double value = 0.0;
|
||||
input.read(reinterpret_cast<char*>(&value), sizeof(value));
|
||||
outValue = static_cast<float>(value);
|
||||
return static_cast<bool>(input);
|
||||
}
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool ParsePlyHeader(std::ifstream& input,
|
||||
ParsedPlyHeader& outHeader,
|
||||
Containers::String& outErrorMessage) {
|
||||
outHeader = {};
|
||||
outErrorMessage.Clear();
|
||||
|
||||
std::string line;
|
||||
if (!std::getline(input, line)) {
|
||||
outErrorMessage = "PLY header is empty";
|
||||
return false;
|
||||
}
|
||||
|
||||
line = TrimAscii(line);
|
||||
if (line != "ply") {
|
||||
outErrorMessage = "PLY header missing magic 'ply'";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!std::getline(input, line)) {
|
||||
outErrorMessage = "PLY header missing format line";
|
||||
return false;
|
||||
}
|
||||
|
||||
line = TrimAscii(line);
|
||||
if (line != "format binary_little_endian 1.0") {
|
||||
outErrorMessage = Containers::String("Unsupported PLY format: ") + ToContainersString(line);
|
||||
return false;
|
||||
}
|
||||
|
||||
bool seenVertexElement = false;
|
||||
bool sawNonVertexElementBeforeVertex = false;
|
||||
bool parsingVertexProperties = false;
|
||||
std::unordered_set<std::string> propertyNames;
|
||||
|
||||
while (std::getline(input, line)) {
|
||||
line = TrimAscii(line);
|
||||
if (line.empty() || line == "comment" || line.rfind("comment ", 0) == 0 || line.rfind("obj_info ", 0) == 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (line == "end_header") {
|
||||
break;
|
||||
}
|
||||
|
||||
std::istringstream lineStream(line);
|
||||
std::string token;
|
||||
lineStream >> token;
|
||||
if (token == "element") {
|
||||
std::string elementName;
|
||||
std::uint64_t elementCount = 0;
|
||||
lineStream >> elementName >> elementCount;
|
||||
if (!lineStream) {
|
||||
outErrorMessage = Containers::String("Malformed PLY element line: ") + ToContainersString(line);
|
||||
return false;
|
||||
}
|
||||
|
||||
parsingVertexProperties = (elementName == "vertex");
|
||||
if (parsingVertexProperties) {
|
||||
if (sawNonVertexElementBeforeVertex) {
|
||||
outErrorMessage = "Unsupported PLY layout: vertex element must be the first data element";
|
||||
return false;
|
||||
}
|
||||
outHeader.vertexCount = static_cast<Core::uint32>(elementCount);
|
||||
seenVertexElement = true;
|
||||
} else if (!seenVertexElement) {
|
||||
sawNonVertexElementBeforeVertex = true;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (token == "property") {
|
||||
std::string typeName;
|
||||
lineStream >> typeName;
|
||||
if (!lineStream) {
|
||||
outErrorMessage = Containers::String("Malformed PLY property line: ") + ToContainersString(line);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (typeName == "list") {
|
||||
outErrorMessage = Containers::String("Unsupported PLY list property: ") + ToContainersString(line);
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string propertyName;
|
||||
lineStream >> propertyName;
|
||||
if (!lineStream) {
|
||||
outErrorMessage = Containers::String("Malformed PLY property line: ") + ToContainersString(line);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!parsingVertexProperties) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!propertyNames.insert(propertyName).second) {
|
||||
outErrorMessage = Containers::String("Duplicate PLY vertex property: ") + ToContainersString(propertyName);
|
||||
return false;
|
||||
}
|
||||
|
||||
PlyPropertyDesc desc;
|
||||
desc.name = propertyName;
|
||||
desc.type = ParsePlyScalarType(typeName);
|
||||
if (desc.type == PlyScalarType::Invalid) {
|
||||
outErrorMessage = Containers::String("Unsupported PLY property type: ") + ToContainersString(typeName);
|
||||
return false;
|
||||
}
|
||||
|
||||
desc.semantic = MapPropertySemantic(propertyName, desc.shRestIndex);
|
||||
outHeader.vertexStride += GetPlyScalarTypeSize(desc.type);
|
||||
outHeader.vertexProperties.push_back(desc);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (!input) {
|
||||
outErrorMessage = "Failed while parsing PLY header";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!seenVertexElement || outHeader.vertexCount == 0u) {
|
||||
outErrorMessage = "PLY file does not declare a valid vertex element";
|
||||
return false;
|
||||
}
|
||||
|
||||
static constexpr const char* kRequiredProperties[] = {
|
||||
"x", "y", "z",
|
||||
"f_dc_0", "f_dc_1", "f_dc_2",
|
||||
"opacity",
|
||||
"scale_0", "scale_1", "scale_2",
|
||||
"rot_0", "rot_1", "rot_2", "rot_3"
|
||||
};
|
||||
for (const char* propertyName : kRequiredProperties) {
|
||||
if (propertyNames.find(propertyName) == propertyNames.end()) {
|
||||
outErrorMessage = Containers::String("PLY file is missing required 3DGS property: ") + propertyName;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
for (Core::uint32 index = 0; index < kGaussianSplatSHCoefficientCount; ++index) {
|
||||
const std::string propertyName = "f_rest_" + std::to_string(index);
|
||||
if (propertyNames.find(propertyName) == propertyNames.end()) {
|
||||
outErrorMessage = Containers::String("PLY file is missing required 3DGS SH property: ") +
|
||||
ToContainersString(propertyName);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
Containers::String GetResourceNameFromPath(const Containers::String& path) {
|
||||
const std::filesystem::path filePath(path.CStr());
|
||||
const std::string fileName = filePath.filename().string();
|
||||
if (!fileName.empty()) {
|
||||
return Containers::String(fileName.c_str());
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
std::filesystem::path ResolveSourcePath(const Containers::String& path) {
|
||||
std::filesystem::path resolvedPath(path.CStr());
|
||||
if (!resolvedPath.is_absolute() && !std::filesystem::exists(resolvedPath)) {
|
||||
const Containers::String& resourceRoot = ResourceManager::Get().GetResourceRoot();
|
||||
if (!resourceRoot.Empty()) {
|
||||
resolvedPath = std::filesystem::path(resourceRoot.CStr()) / resolvedPath;
|
||||
}
|
||||
}
|
||||
|
||||
return resolvedPath.lexically_normal();
|
||||
}
|
||||
|
||||
float Sigmoid(float value) {
|
||||
return 1.0f / (1.0f + std::exp(-value));
|
||||
}
|
||||
|
||||
Math::Vector3 SH0ToColor(const std::array<float, 3>& dc0) {
|
||||
return Math::Vector3(
|
||||
dc0[0] * kSHC0 + 0.5f,
|
||||
dc0[1] * kSHC0 + 0.5f,
|
||||
dc0[2] * kSHC0 + 0.5f);
|
||||
}
|
||||
|
||||
Math::Vector3 LinearScale(const std::array<float, 3>& logScale) {
|
||||
return Math::Vector3(
|
||||
std::abs(std::exp(logScale[0])),
|
||||
std::abs(std::exp(logScale[1])),
|
||||
std::abs(std::exp(logScale[2])));
|
||||
}
|
||||
|
||||
Math::Quaternion NormalizeRotationWXYZ(const std::array<float, 4>& rotationWXYZ) {
|
||||
const float magnitudeSq =
|
||||
rotationWXYZ[0] * rotationWXYZ[0] +
|
||||
rotationWXYZ[1] * rotationWXYZ[1] +
|
||||
rotationWXYZ[2] * rotationWXYZ[2] +
|
||||
rotationWXYZ[3] * rotationWXYZ[3];
|
||||
if (magnitudeSq <= std::numeric_limits<float>::epsilon()) {
|
||||
return Math::Quaternion::Identity();
|
||||
}
|
||||
|
||||
const float inverseMagnitude = 1.0f / std::sqrt(magnitudeSq);
|
||||
return Math::Quaternion(
|
||||
rotationWXYZ[1] * inverseMagnitude,
|
||||
rotationWXYZ[2] * inverseMagnitude,
|
||||
rotationWXYZ[3] * inverseMagnitude,
|
||||
rotationWXYZ[0] * inverseMagnitude);
|
||||
}
|
||||
|
||||
bool IsFinite(const Math::Vector3& value) {
|
||||
return std::isfinite(value.x) && std::isfinite(value.y) && std::isfinite(value.z);
|
||||
}
|
||||
|
||||
bool IsFinite(const Math::Vector4& value) {
|
||||
return std::isfinite(value.x) && std::isfinite(value.y) && std::isfinite(value.z) && std::isfinite(value.w);
|
||||
}
|
||||
|
||||
bool IsFinite(const Math::Quaternion& value) {
|
||||
return std::isfinite(value.x) && std::isfinite(value.y) && std::isfinite(value.z) && std::isfinite(value.w);
|
||||
}
|
||||
|
||||
void BuildSections(Core::uint32 vertexCount,
|
||||
Containers::Array<GaussianSplatSection>& outSections,
|
||||
size_t& outPayloadSize) {
|
||||
outSections.Clear();
|
||||
outSections.Reserve(4u);
|
||||
|
||||
size_t payloadOffset = 0u;
|
||||
auto appendSection = [&](GaussianSplatSectionType type,
|
||||
GaussianSplatSectionFormat format,
|
||||
size_t elementStride) {
|
||||
GaussianSplatSection section;
|
||||
section.type = type;
|
||||
section.format = format;
|
||||
section.dataOffset = payloadOffset;
|
||||
section.dataSize = elementStride * static_cast<size_t>(vertexCount);
|
||||
section.elementCount = vertexCount;
|
||||
section.elementStride = static_cast<Core::uint32>(elementStride);
|
||||
outSections.PushBack(section);
|
||||
payloadOffset += section.dataSize;
|
||||
};
|
||||
|
||||
appendSection(
|
||||
GaussianSplatSectionType::Positions,
|
||||
GaussianSplatSectionFormat::VectorFloat32,
|
||||
sizeof(GaussianSplatPositionRecord));
|
||||
appendSection(
|
||||
GaussianSplatSectionType::Other,
|
||||
GaussianSplatSectionFormat::OtherFloat32,
|
||||
sizeof(GaussianSplatOtherRecord));
|
||||
appendSection(
|
||||
GaussianSplatSectionType::Color,
|
||||
GaussianSplatSectionFormat::ColorRGBA32F,
|
||||
sizeof(GaussianSplatColorRecord));
|
||||
appendSection(
|
||||
GaussianSplatSectionType::SH,
|
||||
GaussianSplatSectionFormat::SHFloat32,
|
||||
sizeof(GaussianSplatSHRecord));
|
||||
|
||||
outPayloadSize = payloadOffset;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
LoadResult ImportGaussianSplatPlyFile(const Containers::String& path) {
|
||||
const std::filesystem::path resolvedPath = ResolveSourcePath(path);
|
||||
std::ifstream input(resolvedPath, std::ios::binary);
|
||||
if (!input.is_open()) {
|
||||
return LoadResult(Containers::String("Failed to read GaussianSplat source file: ") + path);
|
||||
}
|
||||
|
||||
ParsedPlyHeader header;
|
||||
Containers::String headerError;
|
||||
if (!ParsePlyHeader(input, header, headerError)) {
|
||||
return LoadResult(Containers::String("Failed to parse GaussianSplat PLY header: ") + path + " - " + headerError);
|
||||
}
|
||||
|
||||
Containers::Array<GaussianSplatSection> sections;
|
||||
size_t payloadSize = 0u;
|
||||
BuildSections(header.vertexCount, sections, payloadSize);
|
||||
|
||||
Containers::Array<Core::uint8> payload;
|
||||
payload.Resize(payloadSize);
|
||||
|
||||
auto* positions = reinterpret_cast<GaussianSplatPositionRecord*>(
|
||||
payload.Data() + static_cast<size_t>(sections[0].dataOffset));
|
||||
auto* other = reinterpret_cast<GaussianSplatOtherRecord*>(
|
||||
payload.Data() + static_cast<size_t>(sections[1].dataOffset));
|
||||
auto* colors = reinterpret_cast<GaussianSplatColorRecord*>(
|
||||
payload.Data() + static_cast<size_t>(sections[2].dataOffset));
|
||||
auto* shRecords = reinterpret_cast<GaussianSplatSHRecord*>(
|
||||
payload.Data() + static_cast<size_t>(sections[3].dataOffset));
|
||||
|
||||
Math::Bounds bounds;
|
||||
bool hasBounds = false;
|
||||
|
||||
for (Core::uint32 vertexIndex = 0; vertexIndex < header.vertexCount; ++vertexIndex) {
|
||||
std::array<float, 3> position = { 0.0f, 0.0f, 0.0f };
|
||||
std::array<float, 3> dc0 = { 0.0f, 0.0f, 0.0f };
|
||||
std::array<float, 3> scale = { 0.0f, 0.0f, 0.0f };
|
||||
std::array<float, 4> rotation = { 1.0f, 0.0f, 0.0f, 0.0f };
|
||||
std::array<float, kGaussianSplatSHCoefficientCount> sh = {};
|
||||
float opacity = 0.0f;
|
||||
|
||||
for (const PlyPropertyDesc& property : header.vertexProperties) {
|
||||
float value = 0.0f;
|
||||
if (!ReadScalarAsFloat(input, property.type, value)) {
|
||||
return LoadResult(
|
||||
Containers::String("Failed to read GaussianSplat vertex data: ") +
|
||||
path +
|
||||
" at vertex " +
|
||||
Containers::String(std::to_string(vertexIndex).c_str()));
|
||||
}
|
||||
|
||||
switch (property.semantic) {
|
||||
case GaussianSplatSemantic::PositionX: position[0] = value; break;
|
||||
case GaussianSplatSemantic::PositionY: position[1] = value; break;
|
||||
case GaussianSplatSemantic::PositionZ: position[2] = value; break;
|
||||
case GaussianSplatSemantic::DC0R: dc0[0] = value; break;
|
||||
case GaussianSplatSemantic::DC0G: dc0[1] = value; break;
|
||||
case GaussianSplatSemantic::DC0B: dc0[2] = value; break;
|
||||
case GaussianSplatSemantic::Opacity: opacity = value; break;
|
||||
case GaussianSplatSemantic::Scale0: scale[0] = value; break;
|
||||
case GaussianSplatSemantic::Scale1: scale[1] = value; break;
|
||||
case GaussianSplatSemantic::Scale2: scale[2] = value; break;
|
||||
case GaussianSplatSemantic::Rot0: rotation[0] = value; break;
|
||||
case GaussianSplatSemantic::Rot1: rotation[1] = value; break;
|
||||
case GaussianSplatSemantic::Rot2: rotation[2] = value; break;
|
||||
case GaussianSplatSemantic::Rot3: rotation[3] = value; break;
|
||||
case GaussianSplatSemantic::SHRest0:
|
||||
if (property.shRestIndex < sh.size()) {
|
||||
sh[property.shRestIndex] = value;
|
||||
}
|
||||
break;
|
||||
case GaussianSplatSemantic::Ignore:
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
positions[vertexIndex].position = Math::Vector3(position[0], position[1], position[2]);
|
||||
other[vertexIndex].rotation = NormalizeRotationWXYZ(rotation);
|
||||
other[vertexIndex].scale = LinearScale(scale);
|
||||
other[vertexIndex].reserved = 0.0f;
|
||||
|
||||
const Math::Vector3 color = SH0ToColor(dc0);
|
||||
colors[vertexIndex].colorOpacity = Math::Vector4(color, Sigmoid(opacity));
|
||||
|
||||
for (size_t shIndex = 0; shIndex < sh.size(); ++shIndex) {
|
||||
shRecords[vertexIndex].coefficients[shIndex] = sh[shIndex];
|
||||
}
|
||||
|
||||
if (!IsFinite(positions[vertexIndex].position) ||
|
||||
!IsFinite(other[vertexIndex].rotation) ||
|
||||
!IsFinite(other[vertexIndex].scale) ||
|
||||
!IsFinite(colors[vertexIndex].colorOpacity)) {
|
||||
return LoadResult(
|
||||
Containers::String("GaussianSplat source contains non-finite values: ") +
|
||||
path +
|
||||
" at vertex " +
|
||||
Containers::String(std::to_string(vertexIndex).c_str()));
|
||||
}
|
||||
|
||||
if (!hasBounds) {
|
||||
bounds.SetMinMax(positions[vertexIndex].position, positions[vertexIndex].position);
|
||||
hasBounds = true;
|
||||
} else {
|
||||
bounds.Encapsulate(positions[vertexIndex].position);
|
||||
}
|
||||
}
|
||||
|
||||
GaussianSplatMetadata metadata;
|
||||
metadata.contentVersion = 1u;
|
||||
metadata.splatCount = header.vertexCount;
|
||||
metadata.chunkCount = 0u;
|
||||
metadata.cameraCount = 0u;
|
||||
metadata.bounds = bounds;
|
||||
metadata.positionFormat = GaussianSplatSectionFormat::VectorFloat32;
|
||||
metadata.otherFormat = GaussianSplatSectionFormat::OtherFloat32;
|
||||
metadata.colorFormat = GaussianSplatSectionFormat::ColorRGBA32F;
|
||||
metadata.shFormat = GaussianSplatSectionFormat::SHFloat32;
|
||||
metadata.chunkFormat = GaussianSplatSectionFormat::Unknown;
|
||||
metadata.cameraFormat = GaussianSplatSectionFormat::Unknown;
|
||||
|
||||
auto* gaussianSplat = new GaussianSplat();
|
||||
IResource::ConstructParams params;
|
||||
params.name = GetResourceNameFromPath(path);
|
||||
params.path = path;
|
||||
params.guid = ResourceGUID::Generate(path);
|
||||
params.memorySize = payload.Size();
|
||||
gaussianSplat->Initialize(params);
|
||||
|
||||
if (!gaussianSplat->CreateOwned(metadata, std::move(sections), std::move(payload))) {
|
||||
delete gaussianSplat;
|
||||
return LoadResult(Containers::String("Failed to create GaussianSplat resource from PLY: ") + path);
|
||||
}
|
||||
|
||||
return LoadResult(gaussianSplat);
|
||||
}
|
||||
|
||||
} // namespace Internal
|
||||
} // namespace Resources
|
||||
} // namespace XCEngine
|
||||
@@ -0,0 +1,14 @@
|
||||
#pragma once
|
||||
|
||||
#include <XCEngine/Core/Containers/String.h>
|
||||
#include <XCEngine/Core/IO/IResourceLoader.h>
|
||||
|
||||
namespace XCEngine {
|
||||
namespace Resources {
|
||||
namespace Internal {
|
||||
|
||||
LoadResult ImportGaussianSplatPlyFile(const Containers::String& path);
|
||||
|
||||
} // namespace Internal
|
||||
} // namespace Resources
|
||||
} // namespace XCEngine
|
||||
1054
engine/src/Resources/Model/AssimpModelImporter.cpp
Normal file
1054
engine/src/Resources/Model/AssimpModelImporter.cpp
Normal file
File diff suppressed because it is too large
Load Diff
57
engine/src/Resources/Model/Model.cpp
Normal file
57
engine/src/Resources/Model/Model.cpp
Normal file
@@ -0,0 +1,57 @@
|
||||
#include <XCEngine/Resources/Model/Model.h>
|
||||
|
||||
namespace XCEngine {
|
||||
namespace Resources {
|
||||
|
||||
Model::Model() = default;
|
||||
|
||||
Model::~Model() {
|
||||
Release();
|
||||
}
|
||||
|
||||
void Model::Release() {
|
||||
ClearGraph();
|
||||
SetInvalid();
|
||||
}
|
||||
|
||||
void Model::SetRootNodeIndex(Core::uint32 index) {
|
||||
m_rootNodeIndex = index;
|
||||
UpdateMemorySize();
|
||||
}
|
||||
|
||||
void Model::AddNode(const ModelNode& node) {
|
||||
m_nodes.PushBack(node);
|
||||
UpdateMemorySize();
|
||||
}
|
||||
|
||||
void Model::AddMeshBinding(const ModelMeshBinding& binding) {
|
||||
m_meshBindings.PushBack(binding);
|
||||
UpdateMemorySize();
|
||||
}
|
||||
|
||||
void Model::AddMaterialBinding(const ModelMaterialBinding& binding) {
|
||||
m_materialBindings.PushBack(binding);
|
||||
UpdateMemorySize();
|
||||
}
|
||||
|
||||
void Model::ClearGraph() {
|
||||
m_rootNodeIndex = kInvalidModelNodeIndex;
|
||||
m_nodes.Clear();
|
||||
m_meshBindings.Clear();
|
||||
m_materialBindings.Clear();
|
||||
UpdateMemorySize();
|
||||
}
|
||||
|
||||
void Model::UpdateMemorySize() {
|
||||
m_memorySize =
|
||||
m_nodes.Size() * sizeof(ModelNode) +
|
||||
m_meshBindings.Size() * sizeof(ModelMeshBinding) +
|
||||
m_materialBindings.Size() * sizeof(ModelMaterialBinding);
|
||||
|
||||
for (const ModelNode& node : m_nodes) {
|
||||
m_memorySize += node.name.Length();
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Resources
|
||||
} // namespace XCEngine
|
||||
261
engine/src/Resources/Model/ModelArtifactIO.cpp
Normal file
261
engine/src/Resources/Model/ModelArtifactIO.cpp
Normal file
@@ -0,0 +1,261 @@
|
||||
#include <XCEngine/Resources/Model/ModelArtifactIO.h>
|
||||
|
||||
#include <XCEngine/Core/Asset/ArtifactFormats.h>
|
||||
#include <XCEngine/Core/Asset/ResourceManager.h>
|
||||
#include <XCEngine/Resources/Model/Model.h>
|
||||
|
||||
#include <cstring>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <string>
|
||||
|
||||
namespace XCEngine {
|
||||
namespace Resources {
|
||||
|
||||
namespace {
|
||||
|
||||
Containers::String GetResourceNameFromPath(const Containers::String& path) {
|
||||
const std::filesystem::path filePath(path.CStr());
|
||||
const std::string fileName = filePath.filename().string();
|
||||
if (!fileName.empty()) {
|
||||
return Containers::String(fileName.c_str());
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
std::filesystem::path ResolveArtifactPath(const Containers::String& path) {
|
||||
std::filesystem::path resolvedPath(path.CStr());
|
||||
if (!resolvedPath.is_absolute() && !std::filesystem::exists(resolvedPath)) {
|
||||
const Containers::String& resourceRoot = ResourceManager::Get().GetResourceRoot();
|
||||
if (!resourceRoot.Empty()) {
|
||||
resolvedPath = std::filesystem::path(resourceRoot.CStr()) / resolvedPath;
|
||||
}
|
||||
}
|
||||
|
||||
return resolvedPath.lexically_normal();
|
||||
}
|
||||
|
||||
void WriteString(std::ofstream& stream, const Containers::String& value) {
|
||||
const Core::uint32 length = static_cast<Core::uint32>(value.Length());
|
||||
stream.write(reinterpret_cast<const char*>(&length), sizeof(length));
|
||||
if (length > 0) {
|
||||
stream.write(value.CStr(), length);
|
||||
}
|
||||
}
|
||||
|
||||
Containers::String ReadString(std::ifstream& stream) {
|
||||
Core::uint32 length = 0;
|
||||
stream.read(reinterpret_cast<char*>(&length), sizeof(length));
|
||||
if (!stream || length == 0) {
|
||||
return Containers::String();
|
||||
}
|
||||
|
||||
std::string buffer(length, '\0');
|
||||
stream.read(buffer.data(), length);
|
||||
if (!stream) {
|
||||
return Containers::String();
|
||||
}
|
||||
|
||||
return Containers::String(buffer.c_str());
|
||||
}
|
||||
|
||||
LoadResult CreateOwnedModelResource(const Containers::String& path,
|
||||
const ModelArtifactHeader& header,
|
||||
Containers::Array<ModelNode>&& nodes,
|
||||
Containers::Array<ModelMeshBinding>&& meshBindings,
|
||||
Containers::Array<ModelMaterialBinding>&& materialBindings) {
|
||||
auto* model = new Model();
|
||||
|
||||
IResource::ConstructParams params;
|
||||
params.name = GetResourceNameFromPath(path);
|
||||
params.path = path;
|
||||
params.guid = ResourceGUID::Generate(path);
|
||||
params.memorySize = 0;
|
||||
model->Initialize(params);
|
||||
|
||||
if (header.rootNodeIndex != kInvalidModelNodeIndex) {
|
||||
model->SetRootNodeIndex(header.rootNodeIndex);
|
||||
}
|
||||
|
||||
for (const ModelNode& node : nodes) {
|
||||
model->AddNode(node);
|
||||
}
|
||||
|
||||
for (const ModelMeshBinding& binding : meshBindings) {
|
||||
model->AddMeshBinding(binding);
|
||||
}
|
||||
|
||||
for (const ModelMaterialBinding& binding : materialBindings) {
|
||||
model->AddMaterialBinding(binding);
|
||||
}
|
||||
|
||||
return LoadResult(model);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool WriteModelArtifactFile(const Containers::String& artifactPath,
|
||||
const Model& model,
|
||||
Containers::String* outErrorMessage) {
|
||||
const std::filesystem::path resolvedPath = ResolveArtifactPath(artifactPath);
|
||||
std::error_code ec;
|
||||
const std::filesystem::path parentPath = resolvedPath.parent_path();
|
||||
if (!parentPath.empty()) {
|
||||
std::filesystem::create_directories(parentPath, ec);
|
||||
if (ec) {
|
||||
if (outErrorMessage != nullptr) {
|
||||
*outErrorMessage =
|
||||
Containers::String("Failed to create model artifact directory: ") +
|
||||
Containers::String(parentPath.generic_string().c_str());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
std::ofstream output(resolvedPath, std::ios::binary | std::ios::trunc);
|
||||
if (!output.is_open()) {
|
||||
if (outErrorMessage != nullptr) {
|
||||
*outErrorMessage = Containers::String("Failed to open model artifact for write: ") + artifactPath;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
ModelArtifactFileHeader fileHeader;
|
||||
output.write(reinterpret_cast<const char*>(&fileHeader), sizeof(fileHeader));
|
||||
|
||||
ModelArtifactHeader header;
|
||||
header.nodeCount = static_cast<Core::uint32>(model.GetNodes().Size());
|
||||
header.meshBindingCount = static_cast<Core::uint32>(model.GetMeshBindings().Size());
|
||||
header.materialBindingCount = static_cast<Core::uint32>(model.GetMaterialBindings().Size());
|
||||
header.rootNodeIndex = model.GetRootNodeIndex();
|
||||
output.write(reinterpret_cast<const char*>(&header), sizeof(header));
|
||||
|
||||
for (const ModelNode& node : model.GetNodes()) {
|
||||
WriteString(output, node.name);
|
||||
|
||||
ModelNodeArtifactHeader nodeHeader;
|
||||
nodeHeader.parentIndex = node.parentIndex;
|
||||
nodeHeader.meshBindingStart = node.meshBindingStart;
|
||||
nodeHeader.meshBindingCount = node.meshBindingCount;
|
||||
nodeHeader.localPosition = node.localPosition;
|
||||
nodeHeader.localRotation = node.localRotation;
|
||||
nodeHeader.localScale = node.localScale;
|
||||
output.write(reinterpret_cast<const char*>(&nodeHeader), sizeof(nodeHeader));
|
||||
}
|
||||
|
||||
for (const ModelMeshBinding& binding : model.GetMeshBindings()) {
|
||||
ModelMeshBindingArtifact bindingArtifact;
|
||||
bindingArtifact.meshLocalID = binding.meshLocalID;
|
||||
bindingArtifact.materialBindingStart = binding.materialBindingStart;
|
||||
bindingArtifact.materialBindingCount = binding.materialBindingCount;
|
||||
output.write(reinterpret_cast<const char*>(&bindingArtifact), sizeof(bindingArtifact));
|
||||
}
|
||||
|
||||
for (const ModelMaterialBinding& binding : model.GetMaterialBindings()) {
|
||||
ModelMaterialBindingArtifact bindingArtifact;
|
||||
bindingArtifact.slotIndex = binding.slotIndex;
|
||||
bindingArtifact.materialLocalID = binding.materialLocalID;
|
||||
output.write(reinterpret_cast<const char*>(&bindingArtifact), sizeof(bindingArtifact));
|
||||
}
|
||||
|
||||
if (!output && outErrorMessage != nullptr) {
|
||||
*outErrorMessage = Containers::String("Failed to write model artifact: ") + artifactPath;
|
||||
}
|
||||
|
||||
return static_cast<bool>(output);
|
||||
}
|
||||
|
||||
LoadResult LoadModelArtifact(const Containers::String& path) {
|
||||
const std::filesystem::path resolvedPath = ResolveArtifactPath(path);
|
||||
|
||||
std::ifstream input(resolvedPath, std::ios::binary);
|
||||
if (!input.is_open()) {
|
||||
return LoadResult(Containers::String("Failed to read model artifact: ") + path);
|
||||
}
|
||||
|
||||
ModelArtifactFileHeader fileHeader;
|
||||
input.read(reinterpret_cast<char*>(&fileHeader), sizeof(fileHeader));
|
||||
if (!input) {
|
||||
return LoadResult(Containers::String("Failed to parse model artifact file header: ") + path);
|
||||
}
|
||||
|
||||
const bool validFileHeader =
|
||||
std::memcmp(fileHeader.magic, "XCMOD01", 7) == 0 &&
|
||||
fileHeader.schemaVersion == kModelArtifactSchemaVersion;
|
||||
if (!validFileHeader) {
|
||||
return LoadResult(Containers::String("Invalid model artifact file header: ") + path);
|
||||
}
|
||||
|
||||
ModelArtifactHeader header;
|
||||
input.read(reinterpret_cast<char*>(&header), sizeof(header));
|
||||
if (!input) {
|
||||
return LoadResult(Containers::String("Failed to parse model artifact header: ") + path);
|
||||
}
|
||||
|
||||
if (header.rootNodeIndex != kInvalidModelNodeIndex &&
|
||||
header.rootNodeIndex >= header.nodeCount) {
|
||||
return LoadResult(Containers::String("Invalid model artifact root node index: ") + path);
|
||||
}
|
||||
|
||||
Containers::Array<ModelNode> nodes;
|
||||
nodes.Reserve(header.nodeCount);
|
||||
for (Core::uint32 index = 0; index < header.nodeCount; ++index) {
|
||||
ModelNode node;
|
||||
node.name = ReadString(input);
|
||||
|
||||
ModelNodeArtifactHeader nodeHeader;
|
||||
input.read(reinterpret_cast<char*>(&nodeHeader), sizeof(nodeHeader));
|
||||
if (!input) {
|
||||
return LoadResult(Containers::String("Failed to read model node artifact: ") + path);
|
||||
}
|
||||
|
||||
node.parentIndex = nodeHeader.parentIndex;
|
||||
node.meshBindingStart = nodeHeader.meshBindingStart;
|
||||
node.meshBindingCount = nodeHeader.meshBindingCount;
|
||||
node.localPosition = nodeHeader.localPosition;
|
||||
node.localRotation = nodeHeader.localRotation;
|
||||
node.localScale = nodeHeader.localScale;
|
||||
nodes.PushBack(std::move(node));
|
||||
}
|
||||
|
||||
Containers::Array<ModelMeshBinding> meshBindings;
|
||||
meshBindings.Reserve(header.meshBindingCount);
|
||||
for (Core::uint32 index = 0; index < header.meshBindingCount; ++index) {
|
||||
ModelMeshBindingArtifact bindingArtifact;
|
||||
input.read(reinterpret_cast<char*>(&bindingArtifact), sizeof(bindingArtifact));
|
||||
if (!input) {
|
||||
return LoadResult(Containers::String("Failed to read model mesh binding artifact: ") + path);
|
||||
}
|
||||
|
||||
ModelMeshBinding binding;
|
||||
binding.meshLocalID = bindingArtifact.meshLocalID;
|
||||
binding.materialBindingStart = bindingArtifact.materialBindingStart;
|
||||
binding.materialBindingCount = bindingArtifact.materialBindingCount;
|
||||
meshBindings.PushBack(binding);
|
||||
}
|
||||
|
||||
Containers::Array<ModelMaterialBinding> materialBindings;
|
||||
materialBindings.Reserve(header.materialBindingCount);
|
||||
for (Core::uint32 index = 0; index < header.materialBindingCount; ++index) {
|
||||
ModelMaterialBindingArtifact bindingArtifact;
|
||||
input.read(reinterpret_cast<char*>(&bindingArtifact), sizeof(bindingArtifact));
|
||||
if (!input) {
|
||||
return LoadResult(Containers::String("Failed to read model material binding artifact: ") + path);
|
||||
}
|
||||
|
||||
ModelMaterialBinding binding;
|
||||
binding.slotIndex = bindingArtifact.slotIndex;
|
||||
binding.materialLocalID = bindingArtifact.materialLocalID;
|
||||
materialBindings.PushBack(binding);
|
||||
}
|
||||
|
||||
return CreateOwnedModelResource(
|
||||
path,
|
||||
header,
|
||||
std::move(nodes),
|
||||
std::move(meshBindings),
|
||||
std::move(materialBindings));
|
||||
}
|
||||
|
||||
} // namespace Resources
|
||||
} // namespace XCEngine
|
||||
40
engine/src/Resources/Model/ModelLoader.cpp
Normal file
40
engine/src/Resources/Model/ModelLoader.cpp
Normal file
@@ -0,0 +1,40 @@
|
||||
#include <XCEngine/Resources/Model/ModelLoader.h>
|
||||
|
||||
#include <XCEngine/Core/Asset/ResourceManager.h>
|
||||
#include <XCEngine/Resources/Model/ModelArtifactIO.h>
|
||||
|
||||
namespace XCEngine {
|
||||
namespace Resources {
|
||||
|
||||
ModelLoader::ModelLoader() = default;
|
||||
|
||||
ModelLoader::~ModelLoader() = default;
|
||||
|
||||
Containers::Array<Containers::String> ModelLoader::GetSupportedExtensions() const {
|
||||
Containers::Array<Containers::String> extensions;
|
||||
extensions.PushBack("xcmodel");
|
||||
return extensions;
|
||||
}
|
||||
|
||||
bool ModelLoader::CanLoad(const Containers::String& path) const {
|
||||
return GetExtension(path).ToLower() == "xcmodel";
|
||||
}
|
||||
|
||||
LoadResult ModelLoader::Load(const Containers::String& path, const ImportSettings* settings) {
|
||||
(void)settings;
|
||||
|
||||
if (!CanLoad(path)) {
|
||||
return LoadResult(Containers::String("Unsupported model format: ") + GetExtension(path).ToLower());
|
||||
}
|
||||
|
||||
return LoadModelArtifact(path);
|
||||
}
|
||||
|
||||
ImportSettings* ModelLoader::GetDefaultSettings() const {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
REGISTER_RESOURCE_LOADER(ModelLoader);
|
||||
|
||||
} // namespace Resources
|
||||
} // namespace XCEngine
|
||||
@@ -1,6 +1,8 @@
|
||||
#include <gtest/gtest.h>
|
||||
#include <XCEngine/Core/Asset/ResourceTypes.h>
|
||||
#include <XCEngine/Core/Types.h>
|
||||
#include <XCEngine/Resources/GaussianSplat/GaussianSplat.h>
|
||||
#include <XCEngine/Resources/Model/Model.h>
|
||||
#include <XCEngine/Resources/UI/UIDocuments.h>
|
||||
|
||||
using namespace XCEngine::Resources;
|
||||
@@ -25,6 +27,8 @@ TEST(Resources_Types, ResourceType_EnumValues) {
|
||||
EXPECT_EQ(static_cast<uint8_t>(ResourceType::UITheme), 14);
|
||||
EXPECT_EQ(static_cast<uint8_t>(ResourceType::UISchema), 15);
|
||||
EXPECT_EQ(static_cast<uint8_t>(ResourceType::VolumeField), 16);
|
||||
EXPECT_EQ(static_cast<uint8_t>(ResourceType::Model), 17);
|
||||
EXPECT_EQ(static_cast<uint8_t>(ResourceType::GaussianSplat), 18);
|
||||
}
|
||||
|
||||
TEST(Resources_Types, GetResourceTypeName) {
|
||||
@@ -35,6 +39,8 @@ TEST(Resources_Types, GetResourceTypeName) {
|
||||
EXPECT_STREQ(GetResourceTypeName(ResourceType::UITheme), "UITheme");
|
||||
EXPECT_STREQ(GetResourceTypeName(ResourceType::UISchema), "UISchema");
|
||||
EXPECT_STREQ(GetResourceTypeName(ResourceType::VolumeField), "VolumeField");
|
||||
EXPECT_STREQ(GetResourceTypeName(ResourceType::Model), "Model");
|
||||
EXPECT_STREQ(GetResourceTypeName(ResourceType::GaussianSplat), "GaussianSplat");
|
||||
EXPECT_STREQ(GetResourceTypeName(ResourceType::Unknown), "Unknown");
|
||||
}
|
||||
|
||||
@@ -49,6 +55,8 @@ TEST(Resources_Types, GetResourceType_TemplateSpecializations) {
|
||||
EXPECT_EQ(GetResourceType<UITheme>(), ResourceType::UITheme);
|
||||
EXPECT_EQ(GetResourceType<UISchema>(), ResourceType::UISchema);
|
||||
EXPECT_EQ(GetResourceType<VolumeField>(), ResourceType::VolumeField);
|
||||
EXPECT_EQ(GetResourceType<Model>(), ResourceType::Model);
|
||||
EXPECT_EQ(GetResourceType<GaussianSplat>(), ResourceType::GaussianSplat);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
# ============================================================
|
||||
|
||||
add_subdirectory(Texture)
|
||||
add_subdirectory(GaussianSplat)
|
||||
add_subdirectory(Model)
|
||||
add_subdirectory(Mesh)
|
||||
add_subdirectory(Material)
|
||||
add_subdirectory(Shader)
|
||||
|
||||
34
tests/Resources/GaussianSplat/CMakeLists.txt
Normal file
34
tests/Resources/GaussianSplat/CMakeLists.txt
Normal file
@@ -0,0 +1,34 @@
|
||||
# ============================================================
|
||||
# GaussianSplat Tests
|
||||
# ============================================================
|
||||
|
||||
set(GAUSSIAN_SPLAT_TEST_SOURCES
|
||||
test_gaussian_splat.cpp
|
||||
test_gaussian_splat_loader.cpp
|
||||
)
|
||||
|
||||
add_executable(gaussian_splat_tests ${GAUSSIAN_SPLAT_TEST_SOURCES})
|
||||
|
||||
if(MSVC)
|
||||
set_target_properties(gaussian_splat_tests PROPERTIES
|
||||
LINK_FLAGS "/NODEFAULTLIB:libcpmt.lib /NODEFAULTLIB:libcmt.lib"
|
||||
)
|
||||
endif()
|
||||
|
||||
target_link_libraries(gaussian_splat_tests
|
||||
PRIVATE
|
||||
XCEngine
|
||||
GTest::gtest
|
||||
GTest::gtest_main
|
||||
)
|
||||
|
||||
target_include_directories(gaussian_splat_tests PRIVATE
|
||||
${CMAKE_SOURCE_DIR}/engine/include
|
||||
)
|
||||
|
||||
target_compile_definitions(gaussian_splat_tests PRIVATE
|
||||
XCENGINE_TEST_ROOM_PLY_PATH="${CMAKE_SOURCE_DIR}/mvs/3DGS-Unity/room.ply"
|
||||
)
|
||||
|
||||
include(GoogleTest)
|
||||
gtest_discover_tests(gaussian_splat_tests)
|
||||
73
tests/Resources/GaussianSplat/test_gaussian_splat.cpp
Normal file
73
tests/Resources/GaussianSplat/test_gaussian_splat.cpp
Normal file
@@ -0,0 +1,73 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <XCEngine/Core/Math/Bounds.h>
|
||||
#include <XCEngine/Resources/GaussianSplat/GaussianSplat.h>
|
||||
|
||||
using namespace XCEngine::Resources;
|
||||
using namespace XCEngine::Math;
|
||||
|
||||
namespace {
|
||||
|
||||
TEST(GaussianSplat, CreateOwnedStoresMetadataSectionsAndPayload) {
|
||||
GaussianSplat gaussianSplat;
|
||||
|
||||
GaussianSplatMetadata metadata;
|
||||
metadata.contentVersion = 7u;
|
||||
metadata.splatCount = 2u;
|
||||
metadata.bounds.SetMinMax(Vector3(-1.0f, -2.0f, -3.0f), Vector3(4.0f, 5.0f, 6.0f));
|
||||
metadata.positionFormat = GaussianSplatSectionFormat::VectorFloat32;
|
||||
metadata.otherFormat = GaussianSplatSectionFormat::OtherFloat32;
|
||||
metadata.colorFormat = GaussianSplatSectionFormat::ColorRGBA32F;
|
||||
metadata.shFormat = GaussianSplatSectionFormat::SHFloat32;
|
||||
|
||||
XCEngine::Containers::Array<GaussianSplatSection> sections;
|
||||
sections.PushBack(GaussianSplatSection{
|
||||
GaussianSplatSectionType::Positions,
|
||||
GaussianSplatSectionFormat::VectorFloat32,
|
||||
0u,
|
||||
24u,
|
||||
2u,
|
||||
12u
|
||||
});
|
||||
|
||||
XCEngine::Containers::Array<XCEngine::Core::uint8> payload;
|
||||
payload.Resize(24u);
|
||||
for (size_t index = 0; index < payload.Size(); ++index) {
|
||||
payload[index] = static_cast<XCEngine::Core::uint8>(index);
|
||||
}
|
||||
|
||||
ASSERT_TRUE(gaussianSplat.CreateOwned(metadata, std::move(sections), std::move(payload)));
|
||||
EXPECT_TRUE(gaussianSplat.IsValid());
|
||||
EXPECT_EQ(gaussianSplat.GetType(), ResourceType::GaussianSplat);
|
||||
EXPECT_EQ(gaussianSplat.GetContentVersion(), 7u);
|
||||
EXPECT_EQ(gaussianSplat.GetSplatCount(), 2u);
|
||||
EXPECT_EQ(gaussianSplat.GetBounds().GetMin(), Vector3(-1.0f, -2.0f, -3.0f));
|
||||
EXPECT_EQ(gaussianSplat.GetBounds().GetMax(), Vector3(4.0f, 5.0f, 6.0f));
|
||||
ASSERT_NE(gaussianSplat.FindSection(GaussianSplatSectionType::Positions), nullptr);
|
||||
EXPECT_EQ(gaussianSplat.GetPayloadSize(), 24u);
|
||||
EXPECT_NE(gaussianSplat.GetSectionData(GaussianSplatSectionType::Positions), nullptr);
|
||||
}
|
||||
|
||||
TEST(GaussianSplat, RejectsInvalidSectionLayout) {
|
||||
GaussianSplat gaussianSplat;
|
||||
|
||||
GaussianSplatMetadata metadata;
|
||||
metadata.splatCount = 1u;
|
||||
|
||||
XCEngine::Containers::Array<GaussianSplatSection> sections;
|
||||
sections.PushBack(GaussianSplatSection{
|
||||
GaussianSplatSectionType::Positions,
|
||||
GaussianSplatSectionFormat::VectorFloat32,
|
||||
16u,
|
||||
16u,
|
||||
1u,
|
||||
12u
|
||||
});
|
||||
|
||||
XCEngine::Containers::Array<XCEngine::Core::uint8> payload;
|
||||
payload.Resize(24u);
|
||||
|
||||
EXPECT_FALSE(gaussianSplat.CreateOwned(metadata, std::move(sections), std::move(payload)));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
526
tests/Resources/GaussianSplat/test_gaussian_splat_loader.cpp
Normal file
526
tests/Resources/GaussianSplat/test_gaussian_splat_loader.cpp
Normal file
@@ -0,0 +1,526 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <XCEngine/Core/Asset/AssetDatabase.h>
|
||||
#include <XCEngine/Core/Asset/AssetRef.h>
|
||||
#include <XCEngine/Core/Asset/ResourceManager.h>
|
||||
#include <XCEngine/Core/Math/Bounds.h>
|
||||
#include <XCEngine/Resources/GaussianSplat/GaussianSplat.h>
|
||||
#include <XCEngine/Resources/GaussianSplat/GaussianSplatArtifactIO.h>
|
||||
#include <XCEngine/Resources/GaussianSplat/GaussianSplatLoader.h>
|
||||
|
||||
#include <chrono>
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
using namespace XCEngine::Resources;
|
||||
using namespace XCEngine::Math;
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr float kSHC0 = 0.2820948f;
|
||||
|
||||
struct SyntheticGaussianSplatVertex {
|
||||
Vector3 position = Vector3::Zero();
|
||||
Vector3 dc0 = Vector3::Zero();
|
||||
float opacity = 0.0f;
|
||||
Vector3 scaleLog = Vector3::Zero();
|
||||
float rotationWXYZ[4] = { 1.0f, 0.0f, 0.0f, 0.0f };
|
||||
float sh[kGaussianSplatSHCoefficientCount] = {};
|
||||
};
|
||||
|
||||
struct SampleArtifactData {
|
||||
GaussianSplatMetadata metadata;
|
||||
XCEngine::Containers::Array<GaussianSplatSection> sections;
|
||||
XCEngine::Containers::Array<XCEngine::Core::uint8> payload;
|
||||
};
|
||||
|
||||
std::filesystem::path GetRoomPlyPath() {
|
||||
return std::filesystem::path(XCENGINE_TEST_ROOM_PLY_PATH);
|
||||
}
|
||||
|
||||
void ExpectVector3Near(const Vector3& actual, const Vector3& expected, float epsilon = 1e-5f) {
|
||||
EXPECT_NEAR(actual.x, expected.x, epsilon);
|
||||
EXPECT_NEAR(actual.y, expected.y, epsilon);
|
||||
EXPECT_NEAR(actual.z, expected.z, epsilon);
|
||||
}
|
||||
|
||||
void ExpectVector4Near(const Vector4& actual, const Vector4& expected, float epsilon = 1e-5f) {
|
||||
EXPECT_NEAR(actual.x, expected.x, epsilon);
|
||||
EXPECT_NEAR(actual.y, expected.y, epsilon);
|
||||
EXPECT_NEAR(actual.z, expected.z, epsilon);
|
||||
EXPECT_NEAR(actual.w, expected.w, epsilon);
|
||||
}
|
||||
|
||||
void ExpectQuaternionNear(const Quaternion& actual, const Quaternion& expected, float epsilon = 1e-5f) {
|
||||
EXPECT_NEAR(actual.x, expected.x, epsilon);
|
||||
EXPECT_NEAR(actual.y, expected.y, epsilon);
|
||||
EXPECT_NEAR(actual.z, expected.z, epsilon);
|
||||
EXPECT_NEAR(actual.w, expected.w, epsilon);
|
||||
}
|
||||
|
||||
float Sigmoid(float value) {
|
||||
return 1.0f / (1.0f + std::exp(-value));
|
||||
}
|
||||
|
||||
Vector3 LinearScale(const Vector3& value) {
|
||||
return Vector3(
|
||||
std::abs(std::exp(value.x)),
|
||||
std::abs(std::exp(value.y)),
|
||||
std::abs(std::exp(value.z)));
|
||||
}
|
||||
|
||||
Quaternion NormalizeRotationWXYZ(float w, float x, float y, float z) {
|
||||
const float magnitude = std::sqrt(w * w + x * x + y * y + z * z);
|
||||
if (magnitude <= 1e-8f) {
|
||||
return Quaternion::Identity();
|
||||
}
|
||||
|
||||
return Quaternion(x / magnitude, y / magnitude, z / magnitude, w / magnitude);
|
||||
}
|
||||
|
||||
Vector4 SH0ToColorOpacity(const Vector3& dc0, float opacityRaw) {
|
||||
return Vector4(
|
||||
dc0.x * kSHC0 + 0.5f,
|
||||
dc0.y * kSHC0 + 0.5f,
|
||||
dc0.z * kSHC0 + 0.5f,
|
||||
Sigmoid(opacityRaw));
|
||||
}
|
||||
|
||||
void WriteBinaryFloat(std::ofstream& output, float value) {
|
||||
output.write(reinterpret_cast<const char*>(&value), sizeof(value));
|
||||
}
|
||||
|
||||
void WriteSyntheticGaussianSplatPly(
|
||||
const std::filesystem::path& path,
|
||||
const std::vector<SyntheticGaussianSplatVertex>& vertices) {
|
||||
std::ofstream output(path, std::ios::binary | std::ios::trunc);
|
||||
ASSERT_TRUE(output.is_open());
|
||||
|
||||
output << "ply\n";
|
||||
output << "format binary_little_endian 1.0\n";
|
||||
output << "element vertex " << vertices.size() << "\n";
|
||||
output << "property float opacity\n";
|
||||
output << "property float y\n";
|
||||
output << "property float scale_2\n";
|
||||
output << "property float rot_3\n";
|
||||
output << "property float f_dc_1\n";
|
||||
output << "property float x\n";
|
||||
output << "property float scale_0\n";
|
||||
output << "property float rot_1\n";
|
||||
output << "property float f_dc_2\n";
|
||||
output << "property float z\n";
|
||||
output << "property float scale_1\n";
|
||||
output << "property float rot_0\n";
|
||||
output << "property float f_dc_0\n";
|
||||
output << "property float rot_2\n";
|
||||
for (XCEngine::Core::uint32 index = 0; index < kGaussianSplatSHCoefficientCount; ++index) {
|
||||
output << "property float f_rest_" << index << "\n";
|
||||
}
|
||||
output << "end_header\n";
|
||||
|
||||
for (const SyntheticGaussianSplatVertex& vertex : vertices) {
|
||||
WriteBinaryFloat(output, vertex.opacity);
|
||||
WriteBinaryFloat(output, vertex.position.y);
|
||||
WriteBinaryFloat(output, vertex.scaleLog.z);
|
||||
WriteBinaryFloat(output, vertex.rotationWXYZ[3]);
|
||||
WriteBinaryFloat(output, vertex.dc0.y);
|
||||
WriteBinaryFloat(output, vertex.position.x);
|
||||
WriteBinaryFloat(output, vertex.scaleLog.x);
|
||||
WriteBinaryFloat(output, vertex.rotationWXYZ[1]);
|
||||
WriteBinaryFloat(output, vertex.dc0.z);
|
||||
WriteBinaryFloat(output, vertex.position.z);
|
||||
WriteBinaryFloat(output, vertex.scaleLog.y);
|
||||
WriteBinaryFloat(output, vertex.rotationWXYZ[0]);
|
||||
WriteBinaryFloat(output, vertex.dc0.x);
|
||||
WriteBinaryFloat(output, vertex.rotationWXYZ[2]);
|
||||
for (float coefficient : vertex.sh) {
|
||||
WriteBinaryFloat(output, coefficient);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SampleArtifactData BuildSampleArtifactData() {
|
||||
const GaussianSplatPositionRecord positions[2] = {
|
||||
{ Vector3(0.0f, 1.0f, 2.0f) },
|
||||
{ Vector3(3.0f, 4.0f, 5.0f) }
|
||||
};
|
||||
const GaussianSplatOtherRecord other[2] = {
|
||||
{ Quaternion::Identity(), Vector3(1.0f, 1.0f, 1.0f), 0.0f },
|
||||
{ Quaternion(0.0f, 0.5f, 0.0f, 0.8660254f), Vector3(2.0f, 2.0f, 2.0f), 0.0f }
|
||||
};
|
||||
const GaussianSplatColorRecord colors[2] = {
|
||||
{ Vector4(1.0f, 0.0f, 0.0f, 0.25f) },
|
||||
{ Vector4(0.0f, 1.0f, 0.0f, 0.75f) }
|
||||
};
|
||||
|
||||
GaussianSplatSHRecord sh[2];
|
||||
for (XCEngine::Core::uint32 index = 0; index < kGaussianSplatSHCoefficientCount; ++index) {
|
||||
sh[0].coefficients[index] = 0.01f * static_cast<float>(index + 1u);
|
||||
sh[1].coefficients[index] = -0.02f * static_cast<float>(index + 1u);
|
||||
}
|
||||
|
||||
SampleArtifactData sample;
|
||||
sample.metadata.contentVersion = 3u;
|
||||
sample.metadata.splatCount = 2u;
|
||||
sample.metadata.bounds.SetMinMax(Vector3(-2.0f, -1.0f, -3.0f), Vector3(5.0f, 4.0f, 6.0f));
|
||||
sample.metadata.positionFormat = GaussianSplatSectionFormat::VectorFloat32;
|
||||
sample.metadata.otherFormat = GaussianSplatSectionFormat::OtherFloat32;
|
||||
sample.metadata.colorFormat = GaussianSplatSectionFormat::ColorRGBA32F;
|
||||
sample.metadata.shFormat = GaussianSplatSectionFormat::SHFloat32;
|
||||
|
||||
sample.sections.Reserve(4u);
|
||||
size_t payloadOffset = 0u;
|
||||
auto appendSection = [&](GaussianSplatSectionType type,
|
||||
GaussianSplatSectionFormat format,
|
||||
const void* data,
|
||||
size_t dataSize,
|
||||
XCEngine::Core::uint32 elementCount,
|
||||
XCEngine::Core::uint32 elementStride) {
|
||||
GaussianSplatSection section;
|
||||
section.type = type;
|
||||
section.format = format;
|
||||
section.dataOffset = payloadOffset;
|
||||
section.dataSize = dataSize;
|
||||
section.elementCount = elementCount;
|
||||
section.elementStride = elementStride;
|
||||
sample.sections.PushBack(section);
|
||||
|
||||
const size_t newPayloadSize = sample.payload.Size() + dataSize;
|
||||
sample.payload.Resize(newPayloadSize);
|
||||
std::memcpy(sample.payload.Data() + payloadOffset, data, dataSize);
|
||||
payloadOffset = newPayloadSize;
|
||||
};
|
||||
|
||||
appendSection(
|
||||
GaussianSplatSectionType::Positions,
|
||||
GaussianSplatSectionFormat::VectorFloat32,
|
||||
positions,
|
||||
sizeof(positions),
|
||||
2u,
|
||||
sizeof(GaussianSplatPositionRecord));
|
||||
appendSection(
|
||||
GaussianSplatSectionType::Other,
|
||||
GaussianSplatSectionFormat::OtherFloat32,
|
||||
other,
|
||||
sizeof(other),
|
||||
2u,
|
||||
sizeof(GaussianSplatOtherRecord));
|
||||
appendSection(
|
||||
GaussianSplatSectionType::Color,
|
||||
GaussianSplatSectionFormat::ColorRGBA32F,
|
||||
colors,
|
||||
sizeof(colors),
|
||||
2u,
|
||||
sizeof(GaussianSplatColorRecord));
|
||||
appendSection(
|
||||
GaussianSplatSectionType::SH,
|
||||
GaussianSplatSectionFormat::SHFloat32,
|
||||
sh,
|
||||
sizeof(sh),
|
||||
2u,
|
||||
sizeof(GaussianSplatSHRecord));
|
||||
|
||||
return sample;
|
||||
}
|
||||
|
||||
GaussianSplat BuildSampleGaussianSplat(const char* artifactPath) {
|
||||
SampleArtifactData sample = BuildSampleArtifactData();
|
||||
|
||||
GaussianSplat gaussianSplat;
|
||||
XCEngine::Resources::IResource::ConstructParams params;
|
||||
params.name = "sample.xcgsplat";
|
||||
params.path = artifactPath;
|
||||
params.guid = ResourceGUID::Generate(params.path);
|
||||
gaussianSplat.Initialize(params);
|
||||
const bool created = gaussianSplat.CreateOwned(
|
||||
sample.metadata,
|
||||
std::move(sample.sections),
|
||||
std::move(sample.payload));
|
||||
EXPECT_TRUE(created);
|
||||
return gaussianSplat;
|
||||
}
|
||||
|
||||
std::filesystem::path CreateTestProjectRoot(const char* folderName) {
|
||||
return std::filesystem::current_path() / "__xc_gaussian_splat_test_runtime" / folderName;
|
||||
}
|
||||
|
||||
void LinkOrCopyFixture(const std::filesystem::path& sourcePath, const std::filesystem::path& destinationPath) {
|
||||
std::error_code ec;
|
||||
std::filesystem::create_hard_link(sourcePath, destinationPath, ec);
|
||||
if (ec) {
|
||||
ec.clear();
|
||||
std::filesystem::copy_file(sourcePath, destinationPath, std::filesystem::copy_options::overwrite_existing, ec);
|
||||
}
|
||||
ASSERT_FALSE(ec);
|
||||
}
|
||||
|
||||
XCEngine::Core::uint32 ReadPlyVertexCount(const std::filesystem::path& path) {
|
||||
std::ifstream input(path, std::ios::binary);
|
||||
EXPECT_TRUE(input.is_open());
|
||||
|
||||
std::string line;
|
||||
while (std::getline(input, line)) {
|
||||
if (line == "end_header") {
|
||||
break;
|
||||
}
|
||||
|
||||
if (line.rfind("element vertex ", 0) == 0) {
|
||||
return static_cast<XCEngine::Core::uint32>(std::stoul(line.substr(15)));
|
||||
}
|
||||
}
|
||||
|
||||
return 0u;
|
||||
}
|
||||
|
||||
TEST(GaussianSplatLoader, GetResourceType) {
|
||||
GaussianSplatLoader loader;
|
||||
EXPECT_EQ(loader.GetResourceType(), ResourceType::GaussianSplat);
|
||||
}
|
||||
|
||||
TEST(GaussianSplatLoader, CanLoad) {
|
||||
GaussianSplatLoader loader;
|
||||
EXPECT_TRUE(loader.CanLoad("sample.xcgsplat"));
|
||||
EXPECT_TRUE(loader.CanLoad("sample.XCGSPLAT"));
|
||||
EXPECT_FALSE(loader.CanLoad("sample.ply"));
|
||||
}
|
||||
|
||||
TEST(GaussianSplatLoader, LoadInvalidPath) {
|
||||
GaussianSplatLoader loader;
|
||||
const LoadResult result = loader.Load("invalid/path/sample.xcgsplat");
|
||||
EXPECT_FALSE(result);
|
||||
}
|
||||
|
||||
TEST(GaussianSplatLoader, WritesAndLoadsArtifact) {
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
const fs::path tempDir = fs::temp_directory_path() / "xc_gaussian_splat_artifact_test";
|
||||
const fs::path artifactPath = tempDir / "sample.xcgsplat";
|
||||
|
||||
fs::remove_all(tempDir);
|
||||
fs::create_directories(tempDir);
|
||||
|
||||
const GaussianSplat source = BuildSampleGaussianSplat(artifactPath.string().c_str());
|
||||
XCEngine::Containers::String errorMessage;
|
||||
ASSERT_TRUE(WriteGaussianSplatArtifactFile(artifactPath.string().c_str(), source, &errorMessage))
|
||||
<< errorMessage.CStr();
|
||||
|
||||
GaussianSplatLoader loader;
|
||||
const LoadResult result = loader.Load(artifactPath.string().c_str());
|
||||
ASSERT_TRUE(result);
|
||||
ASSERT_NE(result.resource, nullptr);
|
||||
|
||||
auto* gaussianSplat = static_cast<GaussianSplat*>(result.resource);
|
||||
ASSERT_NE(gaussianSplat, nullptr);
|
||||
EXPECT_EQ(gaussianSplat->GetContentVersion(), 3u);
|
||||
EXPECT_EQ(gaussianSplat->GetSplatCount(), 2u);
|
||||
EXPECT_EQ(gaussianSplat->GetBounds().GetMin(), Vector3(-2.0f, -1.0f, -3.0f));
|
||||
EXPECT_EQ(gaussianSplat->GetBounds().GetMax(), Vector3(5.0f, 4.0f, 6.0f));
|
||||
ASSERT_EQ(gaussianSplat->GetSections().Size(), 4u);
|
||||
const GaussianSplatSection* shSection = gaussianSplat->FindSection(GaussianSplatSectionType::SH);
|
||||
ASSERT_NE(shSection, nullptr);
|
||||
EXPECT_EQ(shSection->elementCount, 2u);
|
||||
EXPECT_EQ(shSection->elementStride, sizeof(GaussianSplatSHRecord));
|
||||
ASSERT_NE(gaussianSplat->GetColorRecords(), nullptr);
|
||||
EXPECT_EQ(gaussianSplat->GetColorRecords()[1].colorOpacity, Vector4(0.0f, 1.0f, 0.0f, 0.75f));
|
||||
|
||||
delete gaussianSplat;
|
||||
fs::remove_all(tempDir);
|
||||
}
|
||||
|
||||
TEST(GaussianSplatLoader, ResourceManagerRegistersGaussianSplatLoader) {
|
||||
ResourceManager& manager = ResourceManager::Get();
|
||||
manager.Initialize();
|
||||
|
||||
EXPECT_NE(manager.GetLoader(ResourceType::GaussianSplat), nullptr);
|
||||
|
||||
manager.Shutdown();
|
||||
}
|
||||
|
||||
TEST(GaussianSplatLoader, ResourceManagerLoadsArtifactByPath) {
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
const fs::path tempDir = fs::temp_directory_path() / "xc_gaussian_splat_manager_load_test";
|
||||
const fs::path artifactPath = tempDir / "sample.xcgsplat";
|
||||
|
||||
fs::remove_all(tempDir);
|
||||
fs::create_directories(tempDir);
|
||||
|
||||
const GaussianSplat source = BuildSampleGaussianSplat(artifactPath.string().c_str());
|
||||
XCEngine::Containers::String errorMessage;
|
||||
ASSERT_TRUE(WriteGaussianSplatArtifactFile(artifactPath.string().c_str(), source, &errorMessage))
|
||||
<< errorMessage.CStr();
|
||||
|
||||
ResourceManager& manager = ResourceManager::Get();
|
||||
manager.Initialize();
|
||||
|
||||
{
|
||||
const auto handle = manager.Load<GaussianSplat>(artifactPath.string().c_str());
|
||||
ASSERT_TRUE(handle.IsValid());
|
||||
EXPECT_EQ(handle->GetSplatCount(), 2u);
|
||||
EXPECT_EQ(handle->GetContentVersion(), 3u);
|
||||
ASSERT_NE(handle->FindSection(GaussianSplatSectionType::Color), nullptr);
|
||||
}
|
||||
|
||||
manager.UnloadAll();
|
||||
manager.Shutdown();
|
||||
fs::remove_all(tempDir);
|
||||
}
|
||||
|
||||
TEST(GaussianSplatLoader, AssetDatabaseImportsSyntheticPlyAndLinearizesData) {
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
const fs::path projectRoot = CreateTestProjectRoot("gaussian_splat_synthetic_import");
|
||||
const fs::path assetsDir = projectRoot / "Assets";
|
||||
const fs::path sourcePath = assetsDir / "sample.ply";
|
||||
|
||||
fs::remove_all(projectRoot);
|
||||
fs::create_directories(assetsDir);
|
||||
|
||||
std::vector<SyntheticGaussianSplatVertex> vertices(2);
|
||||
vertices[0].position = Vector3(1.0f, 2.0f, 3.0f);
|
||||
vertices[0].dc0 = Vector3(0.2f, -0.1f, 0.0f);
|
||||
vertices[0].opacity = 0.25f;
|
||||
vertices[0].scaleLog = Vector3(0.0f, std::log(2.0f), std::log(4.0f));
|
||||
vertices[0].rotationWXYZ[0] = 2.0f;
|
||||
vertices[0].rotationWXYZ[1] = 0.0f;
|
||||
vertices[0].rotationWXYZ[2] = 0.0f;
|
||||
vertices[0].rotationWXYZ[3] = 0.0f;
|
||||
for (XCEngine::Core::uint32 index = 0; index < kGaussianSplatSHCoefficientCount; ++index) {
|
||||
vertices[0].sh[index] = 0.01f * static_cast<float>(index + 1u);
|
||||
}
|
||||
|
||||
vertices[1].position = Vector3(-4.0f, -5.0f, -6.0f);
|
||||
vertices[1].dc0 = Vector3(1.0f, 0.5f, -0.5f);
|
||||
vertices[1].opacity = -1.0f;
|
||||
vertices[1].scaleLog = Vector3(std::log(0.5f), 0.0f, std::log(3.0f));
|
||||
vertices[1].rotationWXYZ[0] = 0.0f;
|
||||
vertices[1].rotationWXYZ[1] = 0.0f;
|
||||
vertices[1].rotationWXYZ[2] = 3.0f;
|
||||
vertices[1].rotationWXYZ[3] = 4.0f;
|
||||
for (XCEngine::Core::uint32 index = 0; index < kGaussianSplatSHCoefficientCount; ++index) {
|
||||
vertices[1].sh[index] = -0.02f * static_cast<float>(index + 1u);
|
||||
}
|
||||
|
||||
WriteSyntheticGaussianSplatPly(sourcePath, vertices);
|
||||
|
||||
AssetDatabase database;
|
||||
database.Initialize(projectRoot.string().c_str());
|
||||
|
||||
AssetDatabase::ResolvedAsset resolved;
|
||||
ASSERT_TRUE(database.EnsureArtifact("Assets/sample.ply", ResourceType::GaussianSplat, resolved));
|
||||
ASSERT_TRUE(resolved.artifactReady);
|
||||
EXPECT_TRUE(fs::exists(resolved.artifactMainPath.CStr()));
|
||||
EXPECT_EQ(fs::path(resolved.artifactMainPath.CStr()).extension().generic_string(), ".xcgsplat");
|
||||
|
||||
GaussianSplatLoader loader;
|
||||
LoadResult result = loader.Load(resolved.artifactMainPath);
|
||||
ASSERT_TRUE(result);
|
||||
ASSERT_NE(result.resource, nullptr);
|
||||
|
||||
auto* gaussianSplat = static_cast<GaussianSplat*>(result.resource);
|
||||
ASSERT_NE(gaussianSplat, nullptr);
|
||||
EXPECT_EQ(gaussianSplat->GetSplatCount(), 2u);
|
||||
EXPECT_EQ(gaussianSplat->GetChunkCount(), 0u);
|
||||
EXPECT_EQ(gaussianSplat->GetCameraCount(), 0u);
|
||||
ExpectVector3Near(gaussianSplat->GetBounds().GetMin(), Vector3(-4.0f, -5.0f, -6.0f));
|
||||
ExpectVector3Near(gaussianSplat->GetBounds().GetMax(), Vector3(1.0f, 2.0f, 3.0f));
|
||||
|
||||
ASSERT_NE(gaussianSplat->GetPositionRecords(), nullptr);
|
||||
ASSERT_NE(gaussianSplat->GetOtherRecords(), nullptr);
|
||||
ASSERT_NE(gaussianSplat->GetColorRecords(), nullptr);
|
||||
ASSERT_NE(gaussianSplat->GetSHRecords(), nullptr);
|
||||
|
||||
ExpectVector3Near(gaussianSplat->GetPositionRecords()[0].position, vertices[0].position);
|
||||
ExpectVector3Near(gaussianSplat->GetPositionRecords()[1].position, vertices[1].position);
|
||||
ExpectQuaternionNear(
|
||||
gaussianSplat->GetOtherRecords()[0].rotation,
|
||||
NormalizeRotationWXYZ(2.0f, 0.0f, 0.0f, 0.0f));
|
||||
ExpectQuaternionNear(
|
||||
gaussianSplat->GetOtherRecords()[1].rotation,
|
||||
NormalizeRotationWXYZ(0.0f, 0.0f, 3.0f, 4.0f));
|
||||
ExpectVector3Near(gaussianSplat->GetOtherRecords()[0].scale, LinearScale(vertices[0].scaleLog));
|
||||
ExpectVector3Near(gaussianSplat->GetOtherRecords()[1].scale, LinearScale(vertices[1].scaleLog));
|
||||
ExpectVector4Near(gaussianSplat->GetColorRecords()[0].colorOpacity, SH0ToColorOpacity(vertices[0].dc0, vertices[0].opacity));
|
||||
ExpectVector4Near(gaussianSplat->GetColorRecords()[1].colorOpacity, SH0ToColorOpacity(vertices[1].dc0, vertices[1].opacity));
|
||||
EXPECT_NEAR(gaussianSplat->GetSHRecords()[0].coefficients[0], vertices[0].sh[0], 1e-6f);
|
||||
EXPECT_NEAR(gaussianSplat->GetSHRecords()[0].coefficients[44], vertices[0].sh[44], 1e-6f);
|
||||
EXPECT_NEAR(gaussianSplat->GetSHRecords()[1].coefficients[0], vertices[1].sh[0], 1e-6f);
|
||||
EXPECT_NEAR(gaussianSplat->GetSHRecords()[1].coefficients[44], vertices[1].sh[44], 1e-6f);
|
||||
|
||||
delete gaussianSplat;
|
||||
database.Shutdown();
|
||||
fs::remove_all(projectRoot);
|
||||
}
|
||||
|
||||
TEST(GaussianSplatLoader, RoomPlyBuildsArtifactAndLoadsThroughResourceManager) {
|
||||
namespace fs = std::filesystem;
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
const fs::path fixturePath = GetRoomPlyPath();
|
||||
ASSERT_TRUE(fs::exists(fixturePath));
|
||||
|
||||
const XCEngine::Core::uint32 expectedVertexCount = ReadPlyVertexCount(fixturePath);
|
||||
ASSERT_GT(expectedVertexCount, 0u);
|
||||
|
||||
const fs::path projectRoot = CreateTestProjectRoot("gaussian_splat_room_import");
|
||||
const fs::path assetsDir = projectRoot / "Assets";
|
||||
const fs::path roomPath = assetsDir / "room.ply";
|
||||
|
||||
fs::remove_all(projectRoot);
|
||||
fs::create_directories(assetsDir);
|
||||
LinkOrCopyFixture(fixturePath, roomPath);
|
||||
|
||||
ResourceManager& manager = ResourceManager::Get();
|
||||
manager.Initialize();
|
||||
manager.SetResourceRoot(projectRoot.string().c_str());
|
||||
|
||||
{
|
||||
const auto firstHandle = manager.Load<GaussianSplat>("Assets/room.ply");
|
||||
ASSERT_TRUE(firstHandle.IsValid());
|
||||
EXPECT_EQ(firstHandle->GetSplatCount(), expectedVertexCount);
|
||||
EXPECT_EQ(firstHandle->GetPositionFormat(), GaussianSplatSectionFormat::VectorFloat32);
|
||||
EXPECT_EQ(firstHandle->GetOtherFormat(), GaussianSplatSectionFormat::OtherFloat32);
|
||||
EXPECT_EQ(firstHandle->GetColorFormat(), GaussianSplatSectionFormat::ColorRGBA32F);
|
||||
EXPECT_EQ(firstHandle->GetSHFormat(), GaussianSplatSectionFormat::SHFloat32);
|
||||
|
||||
AssetRef assetRef;
|
||||
ASSERT_TRUE(manager.TryGetAssetRef("Assets/room.ply", ResourceType::GaussianSplat, assetRef));
|
||||
EXPECT_TRUE(assetRef.IsValid());
|
||||
|
||||
AssetDatabase database;
|
||||
database.Initialize(projectRoot.string().c_str());
|
||||
|
||||
AssetDatabase::ResolvedAsset firstResolve;
|
||||
ASSERT_TRUE(database.EnsureArtifact("Assets/room.ply", ResourceType::GaussianSplat, firstResolve));
|
||||
ASSERT_TRUE(firstResolve.artifactReady);
|
||||
EXPECT_TRUE(fs::exists(firstResolve.artifactMainPath.CStr()));
|
||||
EXPECT_EQ(fs::path(firstResolve.artifactMainPath.CStr()).extension().generic_string(), ".xcgsplat");
|
||||
const auto originalArtifactWriteTime = fs::last_write_time(firstResolve.artifactMainPath.CStr());
|
||||
|
||||
std::this_thread::sleep_for(50ms);
|
||||
|
||||
AssetDatabase::ResolvedAsset secondResolve;
|
||||
ASSERT_TRUE(database.EnsureArtifact("Assets/room.ply", ResourceType::GaussianSplat, secondResolve));
|
||||
EXPECT_EQ(firstResolve.artifactMainPath, secondResolve.artifactMainPath);
|
||||
EXPECT_EQ(originalArtifactWriteTime, fs::last_write_time(secondResolve.artifactMainPath.CStr()));
|
||||
|
||||
database.Shutdown();
|
||||
|
||||
manager.UnloadAll();
|
||||
|
||||
const auto secondHandle = manager.Load<GaussianSplat>(assetRef);
|
||||
ASSERT_TRUE(secondHandle.IsValid());
|
||||
EXPECT_EQ(secondHandle->GetSplatCount(), expectedVertexCount);
|
||||
ASSERT_NE(secondHandle->FindSection(GaussianSplatSectionType::SH), nullptr);
|
||||
}
|
||||
|
||||
manager.SetResourceRoot("");
|
||||
manager.Shutdown();
|
||||
fs::remove_all(projectRoot);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
@@ -6,6 +6,7 @@
|
||||
#include <XCEngine/Resources/Mesh/MeshLoader.h>
|
||||
#include <XCEngine/Resources/Mesh/MeshImportSettings.h>
|
||||
#include <XCEngine/Resources/Material/Material.h>
|
||||
#include <XCEngine/Resources/Model/Model.h>
|
||||
#include <XCEngine/Resources/Texture/Texture.h>
|
||||
#include <XCEngine/Core/Asset/ResourceTypes.h>
|
||||
#include <XCEngine/Core/Containers/Array.h>
|
||||
@@ -301,11 +302,15 @@ TEST(MeshLoader, ProjectBackpackSampleArtifactRetainsSectionMaterialTextures) {
|
||||
database.Initialize(projectRoot.string().c_str());
|
||||
|
||||
AssetDatabase::ResolvedAsset resolvedAsset;
|
||||
ASSERT_TRUE(database.EnsureArtifact("Assets/Models/backpack/backpack.obj", ResourceType::Mesh, resolvedAsset));
|
||||
ASSERT_TRUE(database.EnsureArtifact("Assets/Models/backpack/backpack.obj", ResourceType::Model, resolvedAsset));
|
||||
ASSERT_TRUE(resolvedAsset.artifactReady);
|
||||
EXPECT_EQ(fs::path(resolvedAsset.artifactMainPath.CStr()).filename().string(), "main.xcmodel");
|
||||
|
||||
const fs::path meshArtifactPath = fs::path(resolvedAsset.artifactDirectory.CStr()) / "mesh_0.xcmesh";
|
||||
ASSERT_TRUE(fs::exists(meshArtifactPath));
|
||||
|
||||
MeshLoader loader;
|
||||
const LoadResult result = loader.Load(resolvedAsset.artifactMainPath.CStr());
|
||||
const LoadResult result = loader.Load(meshArtifactPath.string().c_str());
|
||||
ASSERT_TRUE(result);
|
||||
ASSERT_NE(result.resource, nullptr);
|
||||
|
||||
@@ -370,21 +375,25 @@ TEST(MeshLoader, AssetDatabaseCreatesModelArtifactAndReusesItWithoutReimport) {
|
||||
database.Initialize(projectRoot.string().c_str());
|
||||
|
||||
AssetDatabase::ResolvedAsset firstResolve;
|
||||
ASSERT_TRUE(database.EnsureArtifact("Assets/textured_triangle.obj", ResourceType::Mesh, firstResolve));
|
||||
ASSERT_TRUE(database.EnsureArtifact("Assets/textured_triangle.obj", ResourceType::Model, firstResolve));
|
||||
ASSERT_TRUE(firstResolve.exists);
|
||||
ASSERT_TRUE(firstResolve.artifactReady);
|
||||
EXPECT_EQ(fs::path(firstResolve.artifactMainPath.CStr()).filename().string(), "main.xcmodel");
|
||||
EXPECT_TRUE(fs::exists(projectRoot / "Assets" / "textured_triangle.obj.meta"));
|
||||
EXPECT_TRUE(fs::exists(projectRoot / "Library" / "SourceAssetDB" / "assets.db"));
|
||||
EXPECT_TRUE(fs::exists(projectRoot / "Library" / "ArtifactDB" / "artifacts.db"));
|
||||
EXPECT_TRUE(fs::exists(firstResolve.artifactMainPath.CStr()));
|
||||
EXPECT_TRUE(fs::exists((fs::path(firstResolve.artifactDirectory.CStr()) /
|
||||
("material_" + std::to_string(sourceMaterialIndex) + ".xcmat"))));
|
||||
EXPECT_TRUE(fs::exists(fs::path(firstResolve.artifactDirectory.CStr()) / "mesh_0.xcmesh"));
|
||||
EXPECT_TRUE(fs::exists(
|
||||
fs::path(firstResolve.artifactDirectory.CStr()) /
|
||||
("material_" + std::to_string(sourceMaterialIndex) + ".xcmat")));
|
||||
EXPECT_TRUE(fs::exists((fs::path(firstResolve.artifactDirectory.CStr()) / "texture_0.xctex")));
|
||||
|
||||
MaterialLoader materialLoader;
|
||||
LoadResult materialArtifactResult =
|
||||
materialLoader.Load((fs::path(firstResolve.artifactDirectory.CStr()) /
|
||||
("material_" + std::to_string(sourceMaterialIndex) + ".xcmat")).string().c_str());
|
||||
materialLoader.Load(
|
||||
(fs::path(firstResolve.artifactDirectory.CStr()) /
|
||||
("material_" + std::to_string(sourceMaterialIndex) + ".xcmat")).string().c_str());
|
||||
ASSERT_TRUE(materialArtifactResult);
|
||||
ASSERT_NE(materialArtifactResult.resource, nullptr);
|
||||
auto* artifactMaterial = static_cast<Material*>(materialArtifactResult.resource);
|
||||
@@ -405,7 +414,8 @@ TEST(MeshLoader, AssetDatabaseCreatesModelArtifactAndReusesItWithoutReimport) {
|
||||
delete artifactMaterial;
|
||||
|
||||
MeshLoader meshLoader;
|
||||
LoadResult meshArtifactResult = meshLoader.Load(firstResolve.artifactMainPath.CStr());
|
||||
LoadResult meshArtifactResult =
|
||||
meshLoader.Load((fs::path(firstResolve.artifactDirectory.CStr()) / "mesh_0.xcmesh").string().c_str());
|
||||
ASSERT_TRUE(meshArtifactResult);
|
||||
ASSERT_NE(meshArtifactResult.resource, nullptr);
|
||||
auto* artifactMesh = static_cast<Mesh*>(meshArtifactResult.resource);
|
||||
@@ -430,14 +440,14 @@ TEST(MeshLoader, AssetDatabaseCreatesModelArtifactAndReusesItWithoutReimport) {
|
||||
delete artifactMesh;
|
||||
|
||||
AssetRef assetRef;
|
||||
ASSERT_TRUE(database.TryGetAssetRef("Assets/textured_triangle.obj", ResourceType::Mesh, assetRef));
|
||||
ASSERT_TRUE(database.TryGetAssetRef("Assets/textured_triangle.obj", ResourceType::Model, assetRef));
|
||||
EXPECT_TRUE(assetRef.IsValid());
|
||||
|
||||
const auto originalArtifactWriteTime = fs::last_write_time(firstResolve.artifactMainPath.CStr());
|
||||
std::this_thread::sleep_for(50ms);
|
||||
|
||||
AssetDatabase::ResolvedAsset secondResolve;
|
||||
ASSERT_TRUE(database.EnsureArtifact("Assets/textured_triangle.obj", ResourceType::Mesh, secondResolve));
|
||||
ASSERT_TRUE(database.EnsureArtifact("Assets/textured_triangle.obj", ResourceType::Model, secondResolve));
|
||||
EXPECT_EQ(firstResolve.artifactMainPath, secondResolve.artifactMainPath);
|
||||
EXPECT_EQ(originalArtifactWriteTime, fs::last_write_time(secondResolve.artifactMainPath.CStr()));
|
||||
|
||||
@@ -469,7 +479,7 @@ TEST(MeshLoader, AssetDatabaseReimportsModelWhenDependencyChanges) {
|
||||
database.Initialize(projectRoot.string().c_str());
|
||||
|
||||
AssetDatabase::ResolvedAsset firstResolve;
|
||||
ASSERT_TRUE(database.EnsureArtifact("Assets/textured_triangle.obj", ResourceType::Mesh, firstResolve));
|
||||
ASSERT_TRUE(database.EnsureArtifact("Assets/textured_triangle.obj", ResourceType::Model, firstResolve));
|
||||
ASSERT_TRUE(firstResolve.artifactReady);
|
||||
const String firstArtifactPath = firstResolve.artifactMainPath;
|
||||
database.Shutdown();
|
||||
@@ -484,7 +494,7 @@ TEST(MeshLoader, AssetDatabaseReimportsModelWhenDependencyChanges) {
|
||||
|
||||
database.Initialize(projectRoot.string().c_str());
|
||||
AssetDatabase::ResolvedAsset secondResolve;
|
||||
ASSERT_TRUE(database.EnsureArtifact("Assets/textured_triangle.obj", ResourceType::Mesh, secondResolve));
|
||||
ASSERT_TRUE(database.EnsureArtifact("Assets/textured_triangle.obj", ResourceType::Model, secondResolve));
|
||||
ASSERT_TRUE(secondResolve.artifactReady);
|
||||
EXPECT_NE(firstArtifactPath, secondResolve.artifactMainPath);
|
||||
const String secondArtifactPath = secondResolve.artifactMainPath;
|
||||
@@ -495,7 +505,7 @@ TEST(MeshLoader, AssetDatabaseReimportsModelWhenDependencyChanges) {
|
||||
|
||||
database.Initialize(projectRoot.string().c_str());
|
||||
AssetDatabase::ResolvedAsset thirdResolve;
|
||||
ASSERT_TRUE(database.EnsureArtifact("Assets/textured_triangle.obj", ResourceType::Mesh, thirdResolve));
|
||||
ASSERT_TRUE(database.EnsureArtifact("Assets/textured_triangle.obj", ResourceType::Model, thirdResolve));
|
||||
ASSERT_TRUE(thirdResolve.artifactReady);
|
||||
EXPECT_NE(secondArtifactPath, thirdResolve.artifactMainPath);
|
||||
EXPECT_TRUE(fs::exists(thirdResolve.artifactMainPath.CStr()));
|
||||
@@ -504,7 +514,7 @@ TEST(MeshLoader, AssetDatabaseReimportsModelWhenDependencyChanges) {
|
||||
fs::remove_all(projectRoot);
|
||||
}
|
||||
|
||||
TEST(MeshLoader, ResourceManagerLoadsModelByAssetRefFromProjectAssets) {
|
||||
TEST(MeshLoader, ResourceManagerLoadsImportedMeshSubAssetByAssetRefFromProjectAssets) {
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
ResourceManager& manager = ResourceManager::Get();
|
||||
@@ -527,60 +537,97 @@ TEST(MeshLoader, ResourceManagerLoadsModelByAssetRefFromProjectAssets) {
|
||||
|
||||
manager.SetResourceRoot(projectRoot.string().c_str());
|
||||
|
||||
const auto firstHandle = manager.Load<Mesh>("Assets/textured_triangle.obj");
|
||||
ASSERT_TRUE(firstHandle.IsValid());
|
||||
EXPECT_EQ(firstHandle->GetVertexCount(), 3u);
|
||||
EXPECT_EQ(firstHandle->GetIndexCount(), 3u);
|
||||
EXPECT_GE(firstHandle->GetMaterials().Size(), 1u);
|
||||
EXPECT_EQ(firstHandle->GetTextures().Size(), 0u);
|
||||
const auto initialMaterialCount = firstHandle->GetMaterials().Size();
|
||||
const XCEngine::Core::uint32 firstSectionMaterialIndex = GetFirstSectionMaterialIndex(*firstHandle.Get());
|
||||
EXPECT_LT(firstSectionMaterialIndex, initialMaterialCount);
|
||||
Material* firstMaterial = GetFirstSectionMaterial(*firstHandle.Get());
|
||||
ASSERT_NE(firstMaterial, nullptr);
|
||||
ASSERT_NE(firstMaterial->GetShader(), nullptr);
|
||||
EXPECT_EQ(firstMaterial->GetShader()->GetPath(), GetBuiltinForwardLitShaderPath());
|
||||
EXPECT_EQ(firstMaterial->GetTextureBindingCount(), 1u);
|
||||
EXPECT_EQ(firstMaterial->GetTextureBindingName(0), "_MainTex");
|
||||
EXPECT_FALSE(firstMaterial->GetTextureBindingPath(0).Empty());
|
||||
const ResourceHandle<Texture> firstLazyTexture = firstMaterial->GetTexture("_MainTex");
|
||||
EXPECT_FALSE(firstLazyTexture.IsValid());
|
||||
EXPECT_GT(manager.GetAsyncPendingCount(), 0u);
|
||||
ASSERT_TRUE(PumpAsyncLoadsUntilIdle(manager));
|
||||
const ResourceHandle<Texture> firstResolvedTexture = firstMaterial->GetTexture("_MainTex");
|
||||
ASSERT_TRUE(firstResolvedTexture.IsValid());
|
||||
EXPECT_EQ(firstResolvedTexture->GetWidth(), 2u);
|
||||
EXPECT_EQ(firstResolvedTexture->GetHeight(), 2u);
|
||||
{
|
||||
const auto directSourceMesh = manager.Load<Mesh>("Assets/textured_triangle.obj");
|
||||
ASSERT_TRUE(directSourceMesh.IsValid());
|
||||
EXPECT_EQ(directSourceMesh->GetVertexCount(), 3u);
|
||||
EXPECT_EQ(directSourceMesh->GetIndexCount(), 3u);
|
||||
EXPECT_EQ(directSourceMesh->GetTextures().Size(), 1u);
|
||||
Material* directMaterial = GetFirstSectionMaterial(*directSourceMesh.Get());
|
||||
ASSERT_NE(directMaterial, nullptr);
|
||||
const ResourceHandle<Texture> directTexture = directMaterial->GetTexture("_MainTex");
|
||||
ASSERT_TRUE(directTexture.IsValid());
|
||||
EXPECT_EQ(directTexture->GetWidth(), 2u);
|
||||
EXPECT_EQ(directTexture->GetHeight(), 2u);
|
||||
}
|
||||
|
||||
AssetRef assetRef;
|
||||
ASSERT_TRUE(manager.TryGetAssetRef("Assets/textured_triangle.obj", ResourceType::Mesh, assetRef));
|
||||
EXPECT_TRUE(assetRef.IsValid());
|
||||
AssetRef modelAssetRef;
|
||||
AssetRef meshAssetRef;
|
||||
String resolvedMeshPath;
|
||||
{
|
||||
const auto modelHandle = manager.Load<Model>("Assets/textured_triangle.obj");
|
||||
ASSERT_TRUE(modelHandle.IsValid());
|
||||
ASSERT_TRUE(manager.TryGetAssetRef("Assets/textured_triangle.obj", ResourceType::Model, modelAssetRef));
|
||||
ASSERT_TRUE(modelHandle->GetMeshBindings().Size() >= 1u);
|
||||
|
||||
meshAssetRef.assetGuid = modelAssetRef.assetGuid;
|
||||
meshAssetRef.localID = modelHandle->GetMeshBindings()[0].meshLocalID;
|
||||
meshAssetRef.resourceType = ResourceType::Mesh;
|
||||
ASSERT_TRUE(meshAssetRef.IsValid());
|
||||
|
||||
ASSERT_TRUE(manager.TryResolveAssetPath(meshAssetRef, resolvedMeshPath));
|
||||
EXPECT_EQ(fs::path(resolvedMeshPath.CStr()).filename().string(), "mesh_0.xcmesh");
|
||||
}
|
||||
|
||||
manager.UnloadAll();
|
||||
|
||||
const auto secondHandle = manager.Load<Mesh>(assetRef);
|
||||
ASSERT_TRUE(secondHandle.IsValid());
|
||||
EXPECT_EQ(secondHandle->GetPath(), "Assets/textured_triangle.obj");
|
||||
EXPECT_EQ(secondHandle->GetVertexCount(), 3u);
|
||||
EXPECT_EQ(secondHandle->GetIndexCount(), 3u);
|
||||
EXPECT_EQ(secondHandle->GetMaterials().Size(), initialMaterialCount);
|
||||
EXPECT_EQ(secondHandle->GetTextures().Size(), 0u);
|
||||
EXPECT_EQ(GetFirstSectionMaterialIndex(*secondHandle.Get()), firstSectionMaterialIndex);
|
||||
Material* secondMaterial = GetFirstSectionMaterial(*secondHandle.Get());
|
||||
ASSERT_NE(secondMaterial, nullptr);
|
||||
ASSERT_NE(secondMaterial->GetShader(), nullptr);
|
||||
EXPECT_EQ(secondMaterial->GetShader()->GetPath(), GetBuiltinForwardLitShaderPath());
|
||||
EXPECT_EQ(secondMaterial->GetTextureBindingCount(), 1u);
|
||||
EXPECT_EQ(secondMaterial->GetTextureBindingName(0), "_MainTex");
|
||||
EXPECT_FALSE(secondMaterial->GetTextureBindingPath(0).Empty());
|
||||
const ResourceHandle<Texture> secondLazyTexture = secondMaterial->GetTexture("_MainTex");
|
||||
EXPECT_FALSE(secondLazyTexture.IsValid());
|
||||
EXPECT_GT(manager.GetAsyncPendingCount(), 0u);
|
||||
ASSERT_TRUE(PumpAsyncLoadsUntilIdle(manager));
|
||||
const ResourceHandle<Texture> secondResolvedTexture = secondMaterial->GetTexture("_MainTex");
|
||||
ASSERT_TRUE(secondResolvedTexture.IsValid());
|
||||
EXPECT_EQ(secondResolvedTexture->GetWidth(), 2u);
|
||||
EXPECT_EQ(secondResolvedTexture->GetHeight(), 2u);
|
||||
XCEngine::Core::uint32 initialMaterialCount = 0;
|
||||
XCEngine::Core::uint32 firstSectionMaterialIndex = 0;
|
||||
{
|
||||
const auto firstHandle = manager.Load<Mesh>(meshAssetRef);
|
||||
ASSERT_TRUE(firstHandle.IsValid());
|
||||
EXPECT_EQ(firstHandle->GetPath(), resolvedMeshPath);
|
||||
EXPECT_EQ(firstHandle->GetVertexCount(), 3u);
|
||||
EXPECT_EQ(firstHandle->GetIndexCount(), 3u);
|
||||
EXPECT_GE(firstHandle->GetMaterials().Size(), 1u);
|
||||
EXPECT_EQ(firstHandle->GetTextures().Size(), 0u);
|
||||
initialMaterialCount = firstHandle->GetMaterials().Size();
|
||||
firstSectionMaterialIndex = GetFirstSectionMaterialIndex(*firstHandle.Get());
|
||||
EXPECT_LT(firstSectionMaterialIndex, initialMaterialCount);
|
||||
Material* firstMaterial = GetFirstSectionMaterial(*firstHandle.Get());
|
||||
ASSERT_NE(firstMaterial, nullptr);
|
||||
ASSERT_NE(firstMaterial->GetShader(), nullptr);
|
||||
EXPECT_EQ(firstMaterial->GetShader()->GetPath(), GetBuiltinForwardLitShaderPath());
|
||||
EXPECT_EQ(firstMaterial->GetTextureBindingCount(), 1u);
|
||||
EXPECT_EQ(firstMaterial->GetTextureBindingName(0), "_MainTex");
|
||||
EXPECT_FALSE(firstMaterial->GetTextureBindingPath(0).Empty());
|
||||
const ResourceHandle<Texture> firstLazyTexture = firstMaterial->GetTexture("_MainTex");
|
||||
EXPECT_FALSE(firstLazyTexture.IsValid());
|
||||
EXPECT_GT(manager.GetAsyncPendingCount(), 0u);
|
||||
ASSERT_TRUE(PumpAsyncLoadsUntilIdle(manager));
|
||||
const ResourceHandle<Texture> firstResolvedTexture = firstMaterial->GetTexture("_MainTex");
|
||||
ASSERT_TRUE(firstResolvedTexture.IsValid());
|
||||
EXPECT_EQ(firstResolvedTexture->GetWidth(), 2u);
|
||||
EXPECT_EQ(firstResolvedTexture->GetHeight(), 2u);
|
||||
}
|
||||
|
||||
manager.UnloadAll();
|
||||
|
||||
{
|
||||
const auto secondHandle = manager.Load<Mesh>(meshAssetRef);
|
||||
ASSERT_TRUE(secondHandle.IsValid());
|
||||
EXPECT_EQ(secondHandle->GetPath(), resolvedMeshPath);
|
||||
EXPECT_EQ(secondHandle->GetVertexCount(), 3u);
|
||||
EXPECT_EQ(secondHandle->GetIndexCount(), 3u);
|
||||
EXPECT_EQ(secondHandle->GetMaterials().Size(), initialMaterialCount);
|
||||
EXPECT_EQ(secondHandle->GetTextures().Size(), 0u);
|
||||
EXPECT_EQ(GetFirstSectionMaterialIndex(*secondHandle.Get()), firstSectionMaterialIndex);
|
||||
Material* secondMaterial = GetFirstSectionMaterial(*secondHandle.Get());
|
||||
ASSERT_NE(secondMaterial, nullptr);
|
||||
ASSERT_NE(secondMaterial->GetShader(), nullptr);
|
||||
EXPECT_EQ(secondMaterial->GetShader()->GetPath(), GetBuiltinForwardLitShaderPath());
|
||||
EXPECT_EQ(secondMaterial->GetTextureBindingCount(), 1u);
|
||||
EXPECT_EQ(secondMaterial->GetTextureBindingName(0), "_MainTex");
|
||||
EXPECT_FALSE(secondMaterial->GetTextureBindingPath(0).Empty());
|
||||
const ResourceHandle<Texture> secondLazyTexture = secondMaterial->GetTexture("_MainTex");
|
||||
EXPECT_FALSE(secondLazyTexture.IsValid());
|
||||
EXPECT_GT(manager.GetAsyncPendingCount(), 0u);
|
||||
ASSERT_TRUE(PumpAsyncLoadsUntilIdle(manager));
|
||||
const ResourceHandle<Texture> secondResolvedTexture = secondMaterial->GetTexture("_MainTex");
|
||||
ASSERT_TRUE(secondResolvedTexture.IsValid());
|
||||
EXPECT_EQ(secondResolvedTexture->GetWidth(), 2u);
|
||||
EXPECT_EQ(secondResolvedTexture->GetHeight(), 2u);
|
||||
}
|
||||
|
||||
manager.SetResourceRoot("");
|
||||
manager.Shutdown();
|
||||
|
||||
42
tests/Resources/Model/CMakeLists.txt
Normal file
42
tests/Resources/Model/CMakeLists.txt
Normal file
@@ -0,0 +1,42 @@
|
||||
# ============================================================
|
||||
# Model Tests
|
||||
# ============================================================
|
||||
|
||||
set(MODEL_TEST_SOURCES
|
||||
test_model.cpp
|
||||
test_model_loader.cpp
|
||||
test_model_import_pipeline.cpp
|
||||
)
|
||||
|
||||
add_executable(model_tests ${MODEL_TEST_SOURCES})
|
||||
|
||||
if(MSVC)
|
||||
set_target_properties(model_tests PROPERTIES
|
||||
LINK_FLAGS "/NODEFAULTLIB:libcpmt.lib /NODEFAULTLIB:libcmt.lib"
|
||||
)
|
||||
endif()
|
||||
|
||||
target_link_libraries(model_tests
|
||||
PRIVATE
|
||||
XCEngine
|
||||
GTest::gtest
|
||||
GTest::gtest_main
|
||||
)
|
||||
|
||||
target_include_directories(model_tests PRIVATE
|
||||
${CMAKE_SOURCE_DIR}/engine/include
|
||||
${CMAKE_SOURCE_DIR}/tests/Fixtures
|
||||
)
|
||||
|
||||
target_compile_definitions(model_tests PRIVATE
|
||||
XCENGINE_TEST_FIXTURES_DIR="${CMAKE_SOURCE_DIR}/tests/Fixtures"
|
||||
)
|
||||
|
||||
add_custom_command(TARGET model_tests POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
||||
${CMAKE_SOURCE_DIR}/engine/third_party/assimp/bin/assimp-vc143-mt.dll
|
||||
$<TARGET_FILE_DIR:model_tests>/assimp-vc143-mt.dll
|
||||
)
|
||||
|
||||
include(GoogleTest)
|
||||
gtest_discover_tests(model_tests)
|
||||
97
tests/Resources/Model/test_model.cpp
Normal file
97
tests/Resources/Model/test_model.cpp
Normal file
@@ -0,0 +1,97 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <XCEngine/Resources/Model/Model.h>
|
||||
|
||||
using namespace XCEngine::Resources;
|
||||
using namespace XCEngine::Math;
|
||||
|
||||
namespace {
|
||||
|
||||
TEST(Model, DefaultConstructor) {
|
||||
Model model;
|
||||
EXPECT_EQ(model.GetType(), ResourceType::Model);
|
||||
EXPECT_FALSE(model.HasRootNode());
|
||||
EXPECT_EQ(model.GetRootNodeIndex(), kInvalidModelNodeIndex);
|
||||
EXPECT_TRUE(model.GetNodes().Empty());
|
||||
EXPECT_TRUE(model.GetMeshBindings().Empty());
|
||||
EXPECT_TRUE(model.GetMaterialBindings().Empty());
|
||||
EXPECT_EQ(model.GetMemorySize(), 0u);
|
||||
}
|
||||
|
||||
TEST(Model, AddGraphDataUpdatesState) {
|
||||
Model model;
|
||||
|
||||
IResource::ConstructParams params;
|
||||
params.name = "robot.fbx";
|
||||
params.path = "Library/Artifacts/aa/main.xcmodel";
|
||||
params.guid = ResourceGUID::Generate(params.path);
|
||||
model.Initialize(params);
|
||||
|
||||
model.SetRootNodeIndex(0u);
|
||||
|
||||
ModelNode rootNode;
|
||||
rootNode.name = "Root";
|
||||
rootNode.parentIndex = -1;
|
||||
rootNode.meshBindingStart = 0u;
|
||||
rootNode.meshBindingCount = 1u;
|
||||
rootNode.localPosition = Vector3(1.0f, 2.0f, 3.0f);
|
||||
rootNode.localScale = Vector3(1.0f, 1.5f, 2.0f);
|
||||
model.AddNode(rootNode);
|
||||
|
||||
ModelMeshBinding meshBinding;
|
||||
meshBinding.meshLocalID = 11u;
|
||||
meshBinding.materialBindingStart = 0u;
|
||||
meshBinding.materialBindingCount = 2u;
|
||||
model.AddMeshBinding(meshBinding);
|
||||
|
||||
ModelMaterialBinding materialBinding0;
|
||||
materialBinding0.slotIndex = 0u;
|
||||
materialBinding0.materialLocalID = 21u;
|
||||
model.AddMaterialBinding(materialBinding0);
|
||||
|
||||
ModelMaterialBinding materialBinding1;
|
||||
materialBinding1.slotIndex = 1u;
|
||||
materialBinding1.materialLocalID = 22u;
|
||||
model.AddMaterialBinding(materialBinding1);
|
||||
|
||||
ASSERT_TRUE(model.HasRootNode());
|
||||
EXPECT_EQ(model.GetRootNodeIndex(), 0u);
|
||||
ASSERT_EQ(model.GetNodes().Size(), 1u);
|
||||
EXPECT_EQ(model.GetNodes()[0].name, "Root");
|
||||
EXPECT_EQ(model.GetNodes()[0].localPosition, Vector3(1.0f, 2.0f, 3.0f));
|
||||
EXPECT_EQ(model.GetNodes()[0].localScale, Vector3(1.0f, 1.5f, 2.0f));
|
||||
ASSERT_EQ(model.GetMeshBindings().Size(), 1u);
|
||||
EXPECT_EQ(model.GetMeshBindings()[0].meshLocalID, 11u);
|
||||
ASSERT_EQ(model.GetMaterialBindings().Size(), 2u);
|
||||
EXPECT_EQ(model.GetMaterialBindings()[1].materialLocalID, 22u);
|
||||
EXPECT_GT(model.GetMemorySize(), 0u);
|
||||
}
|
||||
|
||||
TEST(Model, ReleaseClearsGraphData) {
|
||||
Model model;
|
||||
|
||||
IResource::ConstructParams params;
|
||||
params.name = "robot.fbx";
|
||||
params.path = "Library/Artifacts/aa/main.xcmodel";
|
||||
params.guid = ResourceGUID::Generate(params.path);
|
||||
model.Initialize(params);
|
||||
|
||||
model.SetRootNodeIndex(0u);
|
||||
|
||||
ModelNode node;
|
||||
node.name = "Root";
|
||||
model.AddNode(node);
|
||||
model.AddMeshBinding(ModelMeshBinding{ 3u, 0u, 0u });
|
||||
model.AddMaterialBinding(ModelMaterialBinding{ 0u, 7u });
|
||||
|
||||
model.Release();
|
||||
|
||||
EXPECT_FALSE(model.IsValid());
|
||||
EXPECT_FALSE(model.HasRootNode());
|
||||
EXPECT_TRUE(model.GetNodes().Empty());
|
||||
EXPECT_TRUE(model.GetMeshBindings().Empty());
|
||||
EXPECT_TRUE(model.GetMaterialBindings().Empty());
|
||||
EXPECT_EQ(model.GetMemorySize(), 0u);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
185
tests/Resources/Model/test_model_import_pipeline.cpp
Normal file
185
tests/Resources/Model/test_model_import_pipeline.cpp
Normal file
@@ -0,0 +1,185 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <XCEngine/Core/Asset/AssetDatabase.h>
|
||||
#include <XCEngine/Core/Asset/ResourceManager.h>
|
||||
#include <XCEngine/Resources/Mesh/Mesh.h>
|
||||
#include <XCEngine/Resources/Mesh/MeshLoader.h>
|
||||
#include <XCEngine/Resources/Model/Model.h>
|
||||
#include <XCEngine/Resources/Model/ModelLoader.h>
|
||||
|
||||
#include <filesystem>
|
||||
|
||||
#ifdef _WIN32
|
||||
#ifndef NOMINMAX
|
||||
#define NOMINMAX
|
||||
#endif
|
||||
#include <windows.h>
|
||||
#endif
|
||||
|
||||
using namespace XCEngine::Resources;
|
||||
|
||||
namespace {
|
||||
|
||||
std::string GetMeshFixturePath(const char* fileName) {
|
||||
return (std::filesystem::path(XCENGINE_TEST_FIXTURES_DIR) / "Resources" / "Mesh" / fileName).string();
|
||||
}
|
||||
|
||||
std::filesystem::path GetRepositoryRoot() {
|
||||
return std::filesystem::path(__FILE__).parent_path().parent_path().parent_path().parent_path();
|
||||
}
|
||||
|
||||
void CopyTexturedTriangleFixture(const std::filesystem::path& assetsDir) {
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
fs::copy_file(
|
||||
GetMeshFixturePath("textured_triangle.obj"),
|
||||
assetsDir / "textured_triangle.obj",
|
||||
fs::copy_options::overwrite_existing);
|
||||
fs::copy_file(
|
||||
GetMeshFixturePath("textured_triangle.mtl"),
|
||||
assetsDir / "textured_triangle.mtl",
|
||||
fs::copy_options::overwrite_existing);
|
||||
fs::copy_file(
|
||||
GetMeshFixturePath("checker.bmp"),
|
||||
assetsDir / "checker.bmp",
|
||||
fs::copy_options::overwrite_existing);
|
||||
}
|
||||
|
||||
#ifdef _WIN32
|
||||
struct AssimpDllGuard {
|
||||
HMODULE module = nullptr;
|
||||
|
||||
~AssimpDllGuard() {
|
||||
if (module != nullptr) {
|
||||
FreeLibrary(module);
|
||||
}
|
||||
}
|
||||
};
|
||||
#endif
|
||||
|
||||
TEST(ModelImportPipeline, AssetDatabaseImportsObjAsModelArtifact) {
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
const fs::path projectRoot = fs::temp_directory_path() / "xc_model_import_pipeline_asset_database";
|
||||
const fs::path assetsDir = projectRoot / "Assets";
|
||||
|
||||
fs::remove_all(projectRoot);
|
||||
fs::create_directories(assetsDir);
|
||||
CopyTexturedTriangleFixture(assetsDir);
|
||||
|
||||
#ifdef _WIN32
|
||||
AssimpDllGuard dllGuard;
|
||||
const fs::path assimpDllPath = GetRepositoryRoot() / "engine" / "third_party" / "assimp" / "bin" / "assimp-vc143-mt.dll";
|
||||
ASSERT_TRUE(fs::exists(assimpDllPath));
|
||||
dllGuard.module = LoadLibraryW(assimpDllPath.wstring().c_str());
|
||||
ASSERT_NE(dllGuard.module, nullptr);
|
||||
#endif
|
||||
|
||||
AssetDatabase database;
|
||||
database.Initialize(projectRoot.string().c_str());
|
||||
|
||||
ResourceType importType = ResourceType::Unknown;
|
||||
ASSERT_TRUE(database.TryGetImportableResourceType("Assets/textured_triangle.obj", importType));
|
||||
EXPECT_EQ(importType, ResourceType::Model);
|
||||
|
||||
AssetDatabase::ResolvedAsset resolvedAsset;
|
||||
ASSERT_TRUE(database.EnsureArtifact("Assets/textured_triangle.obj", ResourceType::Model, resolvedAsset));
|
||||
ASSERT_TRUE(resolvedAsset.artifactReady);
|
||||
EXPECT_EQ(fs::path(resolvedAsset.artifactMainPath.CStr()).filename().string(), "main.xcmodel");
|
||||
EXPECT_TRUE(fs::exists(resolvedAsset.artifactMainPath.CStr()));
|
||||
EXPECT_TRUE(fs::exists(fs::path(resolvedAsset.artifactDirectory.CStr()) / "mesh_0.xcmesh"));
|
||||
EXPECT_TRUE(fs::exists(fs::path(resolvedAsset.artifactDirectory.CStr()) / "material_0.xcmat"));
|
||||
EXPECT_TRUE(fs::exists(fs::path(resolvedAsset.artifactDirectory.CStr()) / "texture_0.xctex"));
|
||||
|
||||
ModelLoader modelLoader;
|
||||
const LoadResult modelResult = modelLoader.Load(resolvedAsset.artifactMainPath);
|
||||
ASSERT_TRUE(modelResult);
|
||||
ASSERT_NE(modelResult.resource, nullptr);
|
||||
|
||||
auto* model = static_cast<Model*>(modelResult.resource);
|
||||
ASSERT_NE(model, nullptr);
|
||||
EXPECT_TRUE(model->HasRootNode());
|
||||
EXPECT_GE(model->GetNodes().Size(), 1u);
|
||||
EXPECT_EQ(model->GetRootNodeIndex(), 0u);
|
||||
EXPECT_EQ(model->GetMeshBindings().Size(), 1u);
|
||||
EXPECT_EQ(model->GetMaterialBindings().Size(), 1u);
|
||||
EXPECT_NE(model->GetMeshBindings()[0].meshLocalID, kInvalidLocalID);
|
||||
EXPECT_NE(model->GetMaterialBindings()[0].materialLocalID, kInvalidLocalID);
|
||||
delete model;
|
||||
|
||||
MeshLoader meshLoader;
|
||||
const LoadResult meshResult =
|
||||
meshLoader.Load((fs::path(resolvedAsset.artifactDirectory.CStr()) / "mesh_0.xcmesh").string().c_str());
|
||||
ASSERT_TRUE(meshResult);
|
||||
ASSERT_NE(meshResult.resource, nullptr);
|
||||
|
||||
auto* mesh = static_cast<Mesh*>(meshResult.resource);
|
||||
ASSERT_NE(mesh, nullptr);
|
||||
EXPECT_EQ(mesh->GetVertexCount(), 3u);
|
||||
EXPECT_EQ(mesh->GetIndexCount(), 3u);
|
||||
EXPECT_EQ(mesh->GetSections().Size(), 1u);
|
||||
EXPECT_EQ(mesh->GetMaterials().Size(), 1u);
|
||||
EXPECT_NE(mesh->GetMaterial(0), nullptr);
|
||||
delete mesh;
|
||||
|
||||
database.Shutdown();
|
||||
fs::remove_all(projectRoot);
|
||||
}
|
||||
|
||||
TEST(ModelImportPipeline, ResourceManagerLoadsModelFromProjectAsset) {
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
const fs::path projectRoot = fs::temp_directory_path() / "xc_model_import_pipeline_resource_manager";
|
||||
const fs::path assetsDir = projectRoot / "Assets";
|
||||
|
||||
fs::remove_all(projectRoot);
|
||||
fs::create_directories(assetsDir);
|
||||
CopyTexturedTriangleFixture(assetsDir);
|
||||
|
||||
#ifdef _WIN32
|
||||
AssimpDllGuard dllGuard;
|
||||
const fs::path assimpDllPath = GetRepositoryRoot() / "engine" / "third_party" / "assimp" / "bin" / "assimp-vc143-mt.dll";
|
||||
ASSERT_TRUE(fs::exists(assimpDllPath));
|
||||
dllGuard.module = LoadLibraryW(assimpDllPath.wstring().c_str());
|
||||
ASSERT_NE(dllGuard.module, nullptr);
|
||||
#endif
|
||||
|
||||
ResourceManager& manager = ResourceManager::Get();
|
||||
manager.Initialize();
|
||||
manager.SetResourceRoot(projectRoot.string().c_str());
|
||||
|
||||
{
|
||||
const auto modelHandle = manager.Load<Model>("Assets/textured_triangle.obj");
|
||||
ASSERT_TRUE(modelHandle.IsValid());
|
||||
EXPECT_EQ(modelHandle->GetPath(), "Assets/textured_triangle.obj");
|
||||
EXPECT_TRUE(modelHandle->HasRootNode());
|
||||
EXPECT_EQ(modelHandle->GetMeshBindings().Size(), 1u);
|
||||
EXPECT_EQ(modelHandle->GetMaterialBindings().Size(), 1u);
|
||||
}
|
||||
|
||||
{
|
||||
const auto meshHandle = manager.Load<Mesh>("Assets/textured_triangle.obj");
|
||||
ASSERT_TRUE(meshHandle.IsValid());
|
||||
EXPECT_EQ(meshHandle->GetVertexCount(), 3u);
|
||||
EXPECT_EQ(meshHandle->GetIndexCount(), 3u);
|
||||
}
|
||||
|
||||
AssetRef modelAssetRef;
|
||||
ASSERT_TRUE(manager.TryGetAssetRef("Assets/textured_triangle.obj", ResourceType::Model, modelAssetRef));
|
||||
EXPECT_TRUE(modelAssetRef.IsValid());
|
||||
|
||||
manager.UnloadAll();
|
||||
|
||||
{
|
||||
const auto modelHandle = manager.Load<Model>(modelAssetRef);
|
||||
ASSERT_TRUE(modelHandle.IsValid());
|
||||
EXPECT_EQ(modelHandle->GetMeshBindings().Size(), 1u);
|
||||
EXPECT_EQ(modelHandle->GetMaterialBindings().Size(), 1u);
|
||||
}
|
||||
|
||||
manager.SetResourceRoot("");
|
||||
manager.Shutdown();
|
||||
fs::remove_all(projectRoot);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
122
tests/Resources/Model/test_model_loader.cpp
Normal file
122
tests/Resources/Model/test_model_loader.cpp
Normal file
@@ -0,0 +1,122 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <XCEngine/Core/Asset/ResourceManager.h>
|
||||
#include <XCEngine/Resources/Model/Model.h>
|
||||
#include <XCEngine/Resources/Model/ModelArtifactIO.h>
|
||||
#include <XCEngine/Resources/Model/ModelLoader.h>
|
||||
|
||||
#include <filesystem>
|
||||
|
||||
using namespace XCEngine::Resources;
|
||||
using namespace XCEngine::Math;
|
||||
|
||||
namespace {
|
||||
|
||||
Model BuildSampleModel(const char* artifactPath) {
|
||||
Model model;
|
||||
|
||||
IResource::ConstructParams params;
|
||||
params.name = "robot.fbx";
|
||||
params.path = artifactPath;
|
||||
params.guid = ResourceGUID::Generate(params.path);
|
||||
model.Initialize(params);
|
||||
|
||||
model.SetRootNodeIndex(0u);
|
||||
|
||||
ModelNode rootNode;
|
||||
rootNode.name = "Root";
|
||||
rootNode.parentIndex = -1;
|
||||
rootNode.meshBindingStart = 0u;
|
||||
rootNode.meshBindingCount = 1u;
|
||||
rootNode.localPosition = Vector3(0.0f, 1.0f, 2.0f);
|
||||
rootNode.localRotation = Quaternion::FromEulerAngles(0.1f, 0.2f, 0.3f);
|
||||
rootNode.localScale = Vector3(1.0f, 1.0f, 1.0f);
|
||||
model.AddNode(rootNode);
|
||||
|
||||
ModelNode childNode;
|
||||
childNode.name = "Body";
|
||||
childNode.parentIndex = 0;
|
||||
childNode.meshBindingStart = 1u;
|
||||
childNode.meshBindingCount = 1u;
|
||||
childNode.localPosition = Vector3(3.0f, 4.0f, 5.0f);
|
||||
childNode.localRotation = Quaternion::Identity();
|
||||
childNode.localScale = Vector3(0.5f, 0.5f, 0.5f);
|
||||
model.AddNode(childNode);
|
||||
|
||||
model.AddMeshBinding(ModelMeshBinding{ 101u, 0u, 2u });
|
||||
model.AddMeshBinding(ModelMeshBinding{ 102u, 2u, 1u });
|
||||
|
||||
model.AddMaterialBinding(ModelMaterialBinding{ 0u, 201u });
|
||||
model.AddMaterialBinding(ModelMaterialBinding{ 1u, 202u });
|
||||
model.AddMaterialBinding(ModelMaterialBinding{ 0u, 203u });
|
||||
|
||||
return model;
|
||||
}
|
||||
|
||||
TEST(ModelLoader, GetResourceType) {
|
||||
ModelLoader loader;
|
||||
EXPECT_EQ(loader.GetResourceType(), ResourceType::Model);
|
||||
}
|
||||
|
||||
TEST(ModelLoader, CanLoad) {
|
||||
ModelLoader loader;
|
||||
EXPECT_TRUE(loader.CanLoad("test.xcmodel"));
|
||||
EXPECT_TRUE(loader.CanLoad("test.XCMODEL"));
|
||||
EXPECT_FALSE(loader.CanLoad("test.fbx"));
|
||||
EXPECT_FALSE(loader.CanLoad("test.txt"));
|
||||
}
|
||||
|
||||
TEST(ModelLoader, LoadInvalidPath) {
|
||||
ModelLoader loader;
|
||||
const LoadResult result = loader.Load("invalid/path/model.xcmodel");
|
||||
EXPECT_FALSE(result);
|
||||
}
|
||||
|
||||
TEST(ModelLoader, WritesAndLoadsModelArtifact) {
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
const fs::path tempDir = fs::temp_directory_path() / "xc_model_artifact_test";
|
||||
const fs::path artifactPath = tempDir / "sample.xcmodel";
|
||||
|
||||
fs::remove_all(tempDir);
|
||||
fs::create_directories(tempDir);
|
||||
|
||||
const Model sourceModel = BuildSampleModel(artifactPath.string().c_str());
|
||||
XCEngine::Containers::String errorMessage;
|
||||
ASSERT_TRUE(WriteModelArtifactFile(artifactPath.string().c_str(), sourceModel, &errorMessage))
|
||||
<< errorMessage.CStr();
|
||||
|
||||
ModelLoader loader;
|
||||
const LoadResult result = loader.Load(artifactPath.string().c_str());
|
||||
ASSERT_TRUE(result);
|
||||
ASSERT_NE(result.resource, nullptr);
|
||||
|
||||
auto* model = static_cast<Model*>(result.resource);
|
||||
ASSERT_NE(model, nullptr);
|
||||
ASSERT_TRUE(model->HasRootNode());
|
||||
EXPECT_EQ(model->GetRootNodeIndex(), 0u);
|
||||
ASSERT_EQ(model->GetNodes().Size(), 2u);
|
||||
EXPECT_EQ(model->GetNodes()[0].name, "Root");
|
||||
EXPECT_EQ(model->GetNodes()[1].name, "Body");
|
||||
EXPECT_EQ(model->GetNodes()[1].parentIndex, 0);
|
||||
EXPECT_EQ(model->GetNodes()[1].localPosition, Vector3(3.0f, 4.0f, 5.0f));
|
||||
ASSERT_EQ(model->GetMeshBindings().Size(), 2u);
|
||||
EXPECT_EQ(model->GetMeshBindings()[0].meshLocalID, 101u);
|
||||
EXPECT_EQ(model->GetMeshBindings()[1].materialBindingCount, 1u);
|
||||
ASSERT_EQ(model->GetMaterialBindings().Size(), 3u);
|
||||
EXPECT_EQ(model->GetMaterialBindings()[1].materialLocalID, 202u);
|
||||
|
||||
delete model;
|
||||
fs::remove_all(tempDir);
|
||||
}
|
||||
|
||||
TEST(ModelLoader, ResourceManagerRegistersModelLoader) {
|
||||
ResourceManager& manager = ResourceManager::Get();
|
||||
manager.Initialize();
|
||||
|
||||
EXPECT_NE(manager.GetLoader(ResourceType::Model), nullptr);
|
||||
|
||||
manager.Shutdown();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
Reference in New Issue
Block a user