From 7ee28a7969b6f59665cfcde41855c7add7bab3c9 Mon Sep 17 00:00:00 2001 From: ssdfasd <2156608475@qq.com> Date: Sun, 12 Apr 2026 11:15:59 +0800 Subject: [PATCH] Add gaussian splat asset caching groundwork --- ...ap与SourceHash校验解耦修复计划_2026-04-11.md | 174 ++++++ ...通用Shader多Pass执行重构计划_2026-04-12.md | 564 ++++++++++++++++++ .../shaders/gaussian-splat-composite.shader | 46 ++ .../shaders/gaussian-splat-utilities.shader | 24 +- .../builtin/shaders/gaussian-splat.shader | 13 +- .../XCEngine/Core/Asset/AssetDatabase.h | 5 + .../Passes/BuiltinGaussianSplatPass.h | 22 + .../XCEngine/Resources/BuiltinResources.h | 1 + engine/src/Core/Asset/AssetDatabase.cpp | 142 +++-- .../Passes/BuiltinGaussianSplatPass.cpp | 352 ++++++++++- engine/src/Resources/BuiltinResources.cpp | 21 + project/Assets/Scenes/Main.xc | 2 +- project/Assets/Scenes/NahidaPreview.xc | 2 +- .../integration/gaussian_splat_scene/main.cpp | 231 ++++++- tests/core/Asset/test_resource_manager.cpp | 139 +++++ tests/editor/nahida_preview_regenerator.cpp | 2 +- 16 files changed, 1652 insertions(+), 88 deletions(-) create mode 100644 docs/plan/Library启动Bootstrap与SourceHash校验解耦修复计划_2026-04-11.md create mode 100644 docs/plan/Rendering通用Shader多Pass执行重构计划_2026-04-12.md create mode 100644 engine/assets/builtin/shaders/gaussian-splat-composite.shader diff --git a/docs/plan/Library启动Bootstrap与SourceHash校验解耦修复计划_2026-04-11.md b/docs/plan/Library启动Bootstrap与SourceHash校验解耦修复计划_2026-04-11.md new file mode 100644 index 00000000..a96fc96e --- /dev/null +++ b/docs/plan/Library启动Bootstrap与SourceHash校验解耦修复计划_2026-04-11.md @@ -0,0 +1,174 @@ +# Library启动Bootstrap与SourceHash校验解耦修复计划 + +日期:2026-04-11 + +## 0. 计划定位 + +这份计划专门处理当前 `Library` 主线收口时暴露出来的新根因问题: + +1. `SetResourceRoot()` 把 `BootstrapProjectAssets()` 接到了启动同步路径。 +2. `BootstrapProjectAssets()` 进一步进入 `AssetDatabase::Refresh()`。 +3. `Refresh()` 在扫描 `Assets/cloud.nvdb` 时同步计算 `sourceHash`。 +4. `cloud.nvdb` 是超大源文件,导致 editor 启动直接卡在主线程。 + +本计划不回退单文件 artifact container,不推翻 `@entry=main`,也不回退现有 `Library` 架构。 + +## 1. 当前根因 + +已经确认的实际阻塞链路如下: + +1. `ResourceManager::SetResourceRoot()` +2. `BootstrapProjectAssets()` +3. `AssetImportService::BootstrapProject()` +4. `AssetDatabase::Refresh()` +5. `EnsureMetaForPath(Assets/cloud.nvdb)` +6. `ComputeFileHash(cloud.nvdb)` + +结论: + +1. 慢的不是 `ArtifactContainer` 的 `offset / entry` 读取。 +2. 慢的不是 `VolumeField` artifact payload 本身。 +3. 真正把时间炸掉的是“启动阶段同步执行大源文件内容级校验”。 + +## 2. 修复目标 + +本计划的目标不是取消 `Library` 启动检查,而是把检查做对。 + +目标如下: + +1. 打开项目时仍然会检查 `Library`。 +2. 启动检查只做便宜元数据检查,不做大文件内容级哈希。 +3. `Bootstrap` 与 `EnsureArtifact / Reimport` 的职责彻底拆开。 +4. `sourceHash` 只在真正需要导入、重导入、显式全量重建时才计算。 +5. `Volume`、`Shader`、`Model`、`Material` 在现有 container 架构下继续保持功能正确。 + +## 3. 核心原则 + +### 3.1 启动阶段只做便宜检查 + +启动阶段应该只检查: + +1. `assets.db / artifacts.db` 是否存在、是否可读。 +2. schema 是否匹配。 +3. 源文件是否存在。 +4. `fileSize / writeTime / importerVersion / metaHash` 是否变化。 +5. 哪些资源只是 `DirtyCandidate`。 + +启动阶段不应该做: + +1. 对每个源文件重新算 `sourceHash`。 +2. 对所有资源同步 `ImportAsset`。 +3. 对大资源执行内容级证明。 + +### 3.2 sourceHash 只服务导入正确性 + +`sourceHash` 的职责应该下沉到真正需要它的地方: + +1. `EnsureArtifact()` +2. `ReimportAsset()` +3. `ReimportAllAssets()` +4. 用户显式 `RebuildProjectAssetCache()` + +### 3.3 日常启动和冷启动重建要分开 + +语义上必须区分: + +1. 日常启动: + - 快速检查 + - 只找出可能脏的资源 + - 不同步重导所有大资源 +2. 删除 `Library` 后的首次冷启动: + - 允许重建索引和数据库 + - 但依然不应该把所有超大源文件的内容校验都塞进主线程同步路径 + +## 4. 计划拆解 + +### 阶段A:冻结当前正确成果 + +目的: + +1. 不回退 `ArtifactContainer` +2. 不回退 `@entry=main` +3. 不破坏 `ArtifactDB schema=2` +4. 不破坏 `runtimeLoadPath` 现有语义 + +交付标准: + +1. 旧 `Library` 统一容器计划正式转入阶段归档。 +2. 后续启动链路修复不影响现有 container 主线。 + +### 阶段B:拆开 Bootstrap 与导入职责 + +动作: + +1. 调整 `AssetImportService::BootstrapProject()` 的语义。 +2. 让 `AssetDatabase::Refresh()` 只负责 fast refresh。 +3. 明确 `Refresh()` 不再承担内容级导入校验责任。 + +交付标准: + +1. `SetResourceRoot()` 不再把大文件 `sourceHash` 强行拉进启动同步路径。 +2. `assets.db` 仍能在启动时正确恢复。 + +### 阶段C:把脏判断改成元数据优先 + +动作: + +1. 在 `EnsureMetaForPath()` 中优先使用: + - `sourceFileSize` + - `sourceWriteTime` + - `metaHash` + - `importerVersion` +2. 启动扫描阶段只更新这些便宜字段。 +3. 明确区分“元数据变化”和“必须立刻重导”。 + +交付标准: + +1. 打开项目时不会因为 `cloud.nvdb` 被同步全量哈希而长时间阻塞。 +2. 资源脏状态仍可被发现。 + +### 阶段D:把 sourceHash 下沉到真正需要的路径 + +动作: + +1. `EnsureArtifact()` 真正需要生成或验证 artifact 时,再决定是否补做 `sourceHash`。 +2. `ReimportAllAssets()`、`RebuildProjectAssetCache()` 继续保留严格路径。 +3. 对已有 artifact 且 `size/writeTime/meta/importerVersion` 未变化的资源,优先直接复用。 + +交付标准: + +1. 日常启动快。 +2. 显式重导仍然正确。 +3. `VolumeFieldImporter` 不再在启动同步阶段把大文件成本炸出来。 + +### 阶段E:Volume 与 Editor 回归 + +动作: + +1. 回归 `VolumeFieldLoader.AssetDatabaseCreatesVolumeArtifactAndReusesItWithoutReimport` +2. 回归 `VolumeFieldLoader.ResourceManagerLoadsVolumeByAssetRefFromProjectAssets` +3. 回归 editor 打开 `project` 的启动路径 +4. 回归 `asset / ui / shader / gaussian_splat` + +交付标准: + +1. `project/Library` 删除后可重新生成。 +2. editor 打开项目不再出现 30s 级主线程阻塞。 +3. `cloud.nvdb` 在真正请求时仍能正确导入并渲染。 + +## 5. 风险控制 + +1. 不能为了消掉启动卡顿,直接把 `sourceHash` 整体删掉。 +2. 不能回退当前 `Library` 单文件 container 主线。 +3. 不能把 `Bootstrap` 改成完全不检查 `Library`,否则会把错误拖到运行时爆炸。 +4. 必须特别盯住 `Volume`,因为它是最能放大启动语义错误的大资源类型。 + +## 6. 完成定义 + +当以下条件全部满足时,本计划收口: + +1. 打开项目时 `SetResourceRoot()` 不再同步对 `cloud.nvdb` 做内容级哈希。 +2. `project/Library` 删除后重新打开项目,数据库能够恢复。 +3. `cloud.nvdb` 在真正被请求时仍能正确生成 volume artifact。 +4. `Volume / Asset / UI / Shader / GaussianSplat` 关键回归全部通过。 +5. 启动检查、按需导入、显式全量重建三种语义边界清晰稳定。 diff --git a/docs/plan/Rendering通用Shader多Pass执行重构计划_2026-04-12.md b/docs/plan/Rendering通用Shader多Pass执行重构计划_2026-04-12.md new file mode 100644 index 00000000..257199a5 --- /dev/null +++ b/docs/plan/Rendering通用Shader多Pass执行重构计划_2026-04-12.md @@ -0,0 +1,564 @@ +# Rendering 通用 Shader 多 Pass 执行重构计划 + +日期: `2026-04-12` + +## 1. 文档定位 + +这份计划用于正式解决当前渲染系统里的一个根问题: + +- `Shader` 资源层已经支持定义多个 `Pass` +- `UsePass` 也已经能被解析和导入 +- 但主场景 `BuiltinForwardPipeline` 还没有把“同一个材质的多个 surface pass 按顺序执行”做成正式运行时能力 + +这不是 Nahida 特例,也不是某一个卡通 shader 的临时问题,而是当前 `Rendering` 模块在通用材质执行模型上的结构性缺口。 + +本计划的目标不是给 Nahida 加一个专用补丁,而是把“Unity 式 shader 多 pass 执行”补成引擎正式能力。 + +## 2. 结论摘要 + +### 2.1 当前系统到底有没有多 pass + +当前系统是“局部有多 pass,通用 surface 没有多 pass”。 + +- 有: + - `Shader` 资源对象可以持有多个 `Pass` + - `UsePass` 可用 + - `DepthOnly` / `ShadowCaster` / `ObjectId` / `SelectionMask` 这类专用 pass 会按 pass type 单独解析 + - `PostProcess` / `FinalOutput` 这类 fullscreen pass sequence 也支持多 pass 串联 +- 没有: + - 主场景里“同一个材质的多个 graphics surface pass 自动执行” + - 当前 `BuiltinForwardPipeline` 只会为一个材质挑一个主 surface pass 来画 + +### 2.2 当前根因 + +根因不是 shader authoring 语法不支持多 pass,而是主场景 surface draw 路径还停留在“单 pass 材质模型”。 + +当前主路径的关键限制是: + +- `TryResolveSurfacePassType()` 只认 `Unlit` / `ForwardLit` +- `ResolveSurfaceShaderPass()` 只返回一个 pass +- `DrawVisibleItems()` 对每个 `VisibleRenderItem` 只执行一次主 surface draw + +所以: + +- 你可以在 shader 里写 `ForwardLit + Outline` +- 资源也能读进来 +- 但运行时不会自动再画第二遍 `Outline` + +### 2.3 是否需要 Render Graph + +这轮不需要 Render Graph,而且不应该先上 Render Graph。 + +原因很明确: + +- 当前问题是“主场景通用材质的多 pass 调度缺失” +- 不是“跨资源依赖分析 / 瞬态资源分配 / 全帧拓扑求解”问题 +- 现有架构已经有显式的 `RenderPipeline` + `RenderPassSequence` +- 这次只需要把 `BuiltinForwardPipeline` 从“单 surface pass 执行器”升级成“多 surface pass 执行器” + +结论: + +- 先不用 Render Graph +- 先把主场景通用 multipass 执行能力补齐 +- 后续如果将来做更复杂的 frame dependency,再考虑 Render Graph + +## 3. 当前现状拆解 + +## 3.1 Shader 资源层 + +当前 `Shader` 资源层已经具备以下能力: + +- 一个 shader 可以拥有多个 `Pass` +- pass 有自己的: + - `name` + - `tags` + - `resources` + - `keywordDeclarations` + - `variants` +- `UsePass` 会在构建时导入引用 pass + +这说明“shader 文件里写多个 pass”本身不是问题。 + +## 3.2 专用渲染 pass 层 + +当前系统已经有一些“按 pass type 单独拉取并执行”的路径: + +- `DepthOnly` +- `ShadowCaster` +- `ObjectId` +- `SelectionMask` +- `Skybox` +- `GaussianSplat` +- `Volumetric` + +它们说明当前引擎已经具备“识别 pass 元数据并挑一个对应 pass 执行”的机制,但这套机制目前没有扩展到主场景通用材质 surface 路径。 + +## 3.3 主场景 surface 路径 + +当前主场景 forward 渲染顺序是: + +- `ExecuteForwardOpaquePass` +- `ExecuteForwardSkyboxPass` +- `BuiltinGaussianSplatPass` +- `BuiltinVolumetricPass` +- `ExecuteForwardTransparentPass` + +但 `ExecuteForwardOpaquePass/TransparentPass` 内部依然是“每个物体只解析一个主 surface pass”的模型。 + +这意味着: + +- 旧的单 pass lit/unlit 材质可以工作 +- Unity 式 `Forward + Outline` 这类角色 shader 不能完整工作 + +## 3.4 当前缺口的精确定义 + +缺的不是“多 pass 文件格式”,而是下面这套正式能力: + +1. 主场景 surface pass 的收集 +2. 主场景 surface pass 的排序 +3. 同一 `VisibleRenderItem` 的多次 graphics draw +4. 主 surface pass 与附加 surface pass 的阶段归属 +5. 与现有 opaque / transparent / skybox / depth / shadow / objectId 的兼容 + +## 4. 本轮设计选择 + +## 4.1 选择的正式方案 + +本轮选择: + +- 不做 Nahida 特判 +- 不在某个 shader 上硬编码“再画一遍 outline” +- 不重写整个渲染框架为 Render Graph +- 直接把 `BuiltinForwardPipeline` 重构为“支持通用 surface multipass” + +## 4.2 明确拒绝的方案 + +### 方案 A: Nahida / Toon 专用补丁 + +拒绝原因: + +- 只解决一个案例 +- 会把根因藏在角色特判里 +- 后续别的 Unity shader 还是一样会坏 + +### 方案 B: 先全面 Render Graph 化 + +拒绝原因: + +- 工作量过大 +- 与当前问题不对焦 +- 会把原本中等规模的结构重构,升级成高风险基础设施重写 + +### 方案 C: 继续维持单 surface pass,只在 shader 层绕 + +拒绝原因: + +- 不能从根上支持 `ForwardLit + Outline` +- 和 Unity 式多 pass 材质模型不一致 + +## 5. 目标架构 + +## 5.1 目标状态 + +主场景渲染的目标状态是: + +- 一个材质可以在 shader 内声明多个 surface pass +- 渲染时会先收集这些 pass +- 再按引擎定义好的 scene phase 顺序执行 +- 同一个 mesh / material 在一帧里可以被绘制多次 + +最小闭环至少要支持: + +- `Unlit` +- `ForwardLit` +- `Outline` + +并为后续保留扩展点: + +- 更多角色附加 pass +- 深度依赖的 rim pass +- 特殊透明角色 pass + +## 5.2 推荐的主场景 surface 阶段模型 + +本轮建议把主场景通用 surface pass 明确拆成以下阶段: + +1. `OpaqueBase` +2. `Skybox` +3. `OpaqueAuxiliary` +4. `TransparentBase` +5. `TransparentAuxiliary` + +其中: + +- `Unlit` / `ForwardLit` 默认属于 `Base` +- `Outline` 默认属于 `Auxiliary` +- 本轮重点落地 `OpaqueBase + OpaqueAuxiliary` + +这么拆的原因是: + +- `Outline` 一般要在角色主表面之后绘制 +- 又通常希望在透明物体之前完成 +- 这和当前 forward pipeline 的大框架可以自然兼容 + +## 5.3 推荐的 pass 类型模型 + +当前 `BuiltinMaterialPass` 需要正式扩展,而不是继续只停留在: + +- `ForwardLit` +- `Unlit` + +建议新增: + +- `Outline` + +并在后续根据需要继续扩展。 + +这里的重点不是“枚举值多一个”本身,而是: + +- 主场景 surface 路径终于承认“一个材质有多个可执行的主 graphics pass” +- 不再把 `Outline` 当作特殊插件逻辑,而是当作正式 pass type + +## 6. 核心改造点 + +## 6.1 Pass 元数据层 + +涉及模块: + +- `engine/include/XCEngine/Rendering/Builtin/BuiltinPassTypes.h` +- `engine/include/XCEngine/Rendering/Builtin/BuiltinPassMetadataUtils.h` +- `engine/include/XCEngine/Rendering/Builtin/BuiltinPassLayoutUtils.h` + +任务: + +- 新增 `BuiltinMaterialPass::Outline` +- 增加 `Outline` 的 canonical name 解析 +- 为 `Outline` 建立默认资源绑定规则 +- 保持现有 `ForwardLit / Unlit / DepthOnly / ShadowCaster / ObjectId ...` 兼容 + +验收: + +- shader 中 `Name "Outline"` 或 `Tags { "LightMode" = "Outline" }` 能稳定识别 +- 不影响现有 builtin pass 匹配行为 + +## 6.2 主场景 surface pass 收集层 + +涉及模块: + +- `engine/include/XCEngine/Rendering/Pipelines/BuiltinForwardPipeline.h` +- `engine/src/Rendering/Pipelines/Internal/BuiltinForwardPipelineResources.cpp` + +当前问题: + +- `ResolveSurfaceShaderPass()` 是单返回值模型 + +本轮需要改成: + +- `CollectSurfaceShaderPasses()` 或等价结构 +- 返回一组“已解析的 surface pass 列表” + +每个条目至少包含: + +- `passType` +- `shader` +- `shaderPass` +- `passName` +- `scenePhase` +- `effectiveRenderState` + +验收: + +- 同一材质可同时解析出 `ForwardLit + Outline` +- 旧的单 pass 材质仍只解析出一个条目 + +## 6.3 主场景 surface pass 排序与阶段归属 + +涉及模块: + +- `BuiltinForwardPipeline.cpp` +- `BuiltinForwardPipelineResources.cpp` + +任务: + +- 不再只按 `opaque/transparent` 二元划分来理解主 surface +- 要引入“主场景 surface 子阶段”的概念 +- 至少实现: + - opaque base + - opaque auxiliary + - transparent base + +本轮建议的执行顺序: + +1. `OpaqueBase` +2. `Skybox` +3. `OpaqueAuxiliary` +4. `GaussianSplat` +5. `Volumetric` +6. `TransparentBase` +7. `TransparentAuxiliary` + +说明: + +- `Outline` 先放在 `OpaqueAuxiliary` +- 本轮先不支持“透明物体 outline”的复杂排序 +- 透明附加 pass 只保留接口,不要求首轮全部打通 + +## 6.4 Draw 级执行模型 + +当前问题: + +- `DrawVisibleItems()` 对每个 item 只会 draw 一次主 surface + +重构目标: + +- 对每个 `VisibleRenderItem`,先收集其可执行 surface pass +- 再按当前 scene phase 过滤 +- 每个 pass 单独: + - 解析 pipeline state + - 绑定 descriptor sets + - 执行 draw + +这一步是整个重构的真正核心。 + +验收: + +- 一个物体在同一帧可发生多次主场景 draw +- 每次 draw 都有独立 `passName + renderState` +- pipeline cache 仍然以 `shader + passName + renderState + keywordSignature + surface format` 为 key 稳定工作 + +## 6.5 资源绑定与 layout 缓存 + +当前系统在这方面基础是够的,因为: + +- `PassLayoutKey` 已经包含 `shader + passName` +- `PipelineStateKey` 已经包含 `passName` + +所以本轮不需要重写 cache 架构,只需要保证: + +- 多 pass 场景下 cache key 继续区分不同 pass +- `Outline` 这种 pass 的资源绑定计划能正确建立 +- `MaterialTexture` / `MaterialConstants` / `PerObject` 等绑定仍走现有机制 + +重点检查: + +- `Outline` 是否需要 `Lighting` +- `Outline` 是否需要 `ShadowReceiver` +- `Outline` 是否只需 `PerObject + Material + MaterialTextures + Sampler` + +## 6.6 Shader authoring 约束正式化 + +这轮需要把“主场景 surface multipass”的 authoring 约束写清楚,而不是默认靠猜。 + +建议明确约定: + +- 主场景可执行的 surface pass 必须有明确 `Name` 或 `LightMode` +- pass 名称与 builtin canonical name 的映射规则固定 +- `ForwardLit` / `Unlit` / `Outline` 属于主场景通用 surface pass +- `DepthOnly` / `ShadowCaster` / `ObjectId` / `SelectionMask` 继续属于专用路径 + +这样做的价值是: + +- shader authoring 规则清晰 +- 不会再出现“写了 pass 但没人知道该在哪个阶段执行” + +## 6.7 XCCharacterToon.shader 的正式接入方式 + +在 multipass 正式能力完成后,`XCCharacterToon.shader` 的正确接法应为: + +- `ForwardLit` 负责角色主表面 +- `Outline` 负责描边 +- `DepthOnly` / `ShadowCaster` 继续沿用已有专用 pass + +本轮对 Nahida 的定位是: + +- 不再作为特判对象 +- 只作为 multipass 正式化后的第一个高价值验证样本 + +## 7. 分阶段执行计划 + +## Phase 0: 基线确认与测试样本准备 + +### 目标 + +在改主路径前固定当前行为,防止重构期间把旧材质全带坏。 + +### 任务 + +- 盘点当前所有依赖 `ForwardLit / Unlit` 的单 pass 集成测试 +- 新建一个最小 multipass 测试场景: + - 一个简单 mesh + - 一个 `ForwardLit + Outline` 测试 shader +- 明确 Nahida 作为高复杂度回归样本,不作为最小开发起点 + +### 完成标准 + +- 有一个简单到足以定位多 pass 执行问题的专门测试场景 +- 现有 lit/unlit 场景回归基线不丢 + +## Phase 1: 主场景通用 surface multipass 基础设施 + +### 目标 + +让 `BuiltinForwardPipeline` 具备“一个物体可执行多个主 surface pass”的正式能力。 + +### 任务 + +- 扩展 `BuiltinMaterialPass` +- 新增 `Outline` pass type +- 单 pass 解析模型改成 multi-pass collection 模型 +- 引入主场景 surface phase +- 重写 `DrawVisibleItems()` 执行逻辑 + +### 完成标准 + +- 最小 multipass 测试 shader 能完成两次 draw +- 单 pass shader 行为不回归 + +## Phase 2: Outline 正式落地 + +### 目标 + +让 `Outline` 成为主场景正式 pass,而不是外置补丁。 + +### 任务 + +- 为 `Outline` 补齐 builtin metadata / layout / binding 规则 +- 在 `XCCharacterToon.shader` 中加入正式 `Outline` pass +- 验证 `Cull Front / ZTest / ZWrite / Blend` 等状态是否符合需求 +- 首轮先以 static mesh + vertex color alpha 宽度控制闭环 + +### 明确暂缓 + +- `smoothNormal` 新顶点语义支持 +- skinned mesh outline +- 透明角色 outline 排序 + +### 完成标准 + +- 最小 multipass 测试场景通过 +- Nahida 在 `original` 模式里开始出现正确的独立 outline draw + +## Phase 3: Nahida / Unity 风格角色卡通验证 + +### 目标 + +把 multipass 正式能力用于 Nahida,验证这套方案确实能支撑 Unity 风格角色 shader。 + +### 任务 + +- 将 `XCCharacterToon.shader` 的 `Outline` 接入正式主场景 multipass +- 重新生成 `nahida.png` +- 对比 `unlit`、`forward lit`、`original` 三种模式的画面差异 +- 评估是否可以锁定新的 `GT.ppm` + +### 完成标准 + +- Nahida 的描边不再依赖临时逻辑 +- `original` 渲染链路进入可持续迭代状态 + +## Phase 4: 通用化与规则收口 + +### 目标 + +把这次重构从“够 Nahida 用”收口成“引擎正式通用能力”。 + +### 任务 + +- 补文档,明确 shader multipass authoring 规范 +- 视情况支持更多主场景 surface pass type +- 清理旧的单 pass 假设与命名 +- 审查编辑器 / 材质检查器 / shader 资源导入链路是否需要显示 pass 信息 + +### 完成标准 + +- Multipass 不再是隐式能力 +- 规则、测试、运行时行为三者一致 + +## 8. 测试计划 + +## 8.1 单元测试 + +重点新增或补强: + +- `BuiltinPassMetadataUtils` + - `Outline` canonical name 匹配 +- `BuiltinPassLayoutUtils` + - `Outline` 资源绑定计划 +- `BuiltinForwardPipeline` + - 单材质多 surface pass 收集 + - scene phase 排序 + - 单 pass 回归不变 + +## 8.2 集成测试 + +建议新增: + +- `tests/Rendering/integration/multipass_outline_scene` + - 最小 multipass 样例 +- 继续保留: + - `nahida_preview_scene` + - 现有 lit/unlit/backpack/material_state 等基础场景 + +## 8.3 人工验收 + +人工验收重点不只是“有没有画出来”,而是: + +- 是否真的发生了两次 draw +- state / cull / depth 是否正确 +- 单 pass 材质是否回归 +- Nahida 的 outline 是否来自正式 pass,而不是额外补丁 + +## 9. 风险与控制 + +## 9.1 最大风险 + +最大风险不是代码量,而是“把旧的单 pass 假设改坏”。 + +具体风险包括: + +- 单 pass lit/unlit 材质回归 +- opaque / transparent 分类被打乱 +- pipeline cache 或 descriptor set 复用逻辑出错 +- 新增 `Outline` 后错误进入 shadow/depth/objectId 路径 + +## 9.2 风险控制策略 + +- 先做最小 multipass 场景,不直接拿 Nahida 起步 +- 先只开放 `Outline` 这一种主场景 auxiliary pass +- 暂缓透明 multipass 与 depth-driven rim +- 每个阶段都跑现有 forward 基础集成测试 + +## 10. 本轮不做的内容 + +本计划明确不把以下内容混进首轮 multipass 重构: + +- Render Graph 化 +- SkinnedMesh / 骨骼动画 +- GPU skinning +- Transparent multipass 完整排序体系 +- Scene depth texture 的通用相机绑定 +- Unity 全量角色 shader 语义一次性补齐 + +这些都不是当前根因的第一优先级。 + +## 11. 完成判定 + +当满足以下条件时,才算这次“通用 shader 多 pass 执行重构”完成: + +1. 主场景 surface 路径正式支持一个材质执行多个 pass +2. `Outline` 成为正式 builtin surface pass +3. 现有单 pass 材质与基础场景不回归 +4. Nahida 的描边来自正式 multipass 执行,而不是特判 +5. 文档、测试、实现三者一致 + +## 12. 下一步建议 + +这份计划写完后的下一步,不是直接去碰 Nahida 复杂 shader 细节,而是: + +1. 先做 `Phase 0` +2. 新建最小 multipass outline 场景与测试 shader +3. 再开始 `BuiltinForwardPipeline` 的 multipass 基础设施改造 + +顺序不能反。 + +如果一上来就直接拿 Nahida 开刀,很容易把“结构性问题”和“角色 shader 细节问题”混在一起,最后继续变成补丁式推进。 diff --git a/engine/assets/builtin/shaders/gaussian-splat-composite.shader b/engine/assets/builtin/shaders/gaussian-splat-composite.shader new file mode 100644 index 00000000..e5f2faf0 --- /dev/null +++ b/engine/assets/builtin/shaders/gaussian-splat-composite.shader @@ -0,0 +1,46 @@ +Shader "Builtin Gaussian Splat Composite" +{ + SubShader + { + Pass + { + Name "GaussianComposite" + Tags { "LightMode" = "GaussianComposite" } + Cull Off + ZWrite Off + ZTest Always + Blend One OneMinusSrcAlpha + HLSLPROGRAM + #pragma target 4.5 + #pragma vertex MainVS + #pragma fragment MainPS + + Texture2D GaussianSplatAccumulationTexture; + + struct VSOutput + { + float4 position : SV_POSITION; + }; + + VSOutput MainVS(uint vertexId : SV_VertexID) + { + const float2 positions[3] = { + float2(-1.0f, -1.0f), + float2(-1.0f, 3.0f), + float2( 3.0f, -1.0f) + }; + + VSOutput output; + output.position = float4(positions[vertexId], 1.0f, 1.0f); + return output; + } + + float4 MainPS(VSOutput input) : SV_TARGET + { + const int2 pixelCoord = int2(input.position.xy); + return GaussianSplatAccumulationTexture.Load(int3(pixelCoord, 0)); + } + ENDHLSL + } + } +} diff --git a/engine/assets/builtin/shaders/gaussian-splat-utilities.shader b/engine/assets/builtin/shaders/gaussian-splat-utilities.shader index 708f1f4b..656481bf 100644 --- a/engine/assets/builtin/shaders/gaussian-splat-utilities.shader +++ b/engine/assets/builtin/shaders/gaussian-splat-utilities.shader @@ -229,21 +229,21 @@ Shader "Builtin Gaussian Splat Utilities" } float3 CalcCovariance2D( - float3 viewPosition, + float3 localPosition, float3 covariance3D0, float3 covariance3D1, - float4x4 viewMatrix, + float4x4 modelViewMatrix, float4x4 projectionMatrix, float2 screenSize) { + const float3 viewPosition = mul(modelViewMatrix, float4(localPosition, 1.0)).xyz; if (abs(viewPosition.z) <= 1.0e-5) { return float3(0.3, 0.0, 0.3); } - const float aspect = projectionMatrix[0][0] / projectionMatrix[1][1]; const float tanFovX = rcp(projectionMatrix[0][0]); - const float tanFovY = rcp(projectionMatrix[1][1] * aspect); + const float tanFovY = rcp(projectionMatrix[1][1]); const float limitX = 1.3 * tanFovX; const float limitY = 1.3 * tanFovY; @@ -264,7 +264,7 @@ Shader "Builtin Gaussian Splat Utilities" 0.0, 0.0, 0.0); - const float3x3 worldToView = (float3x3)viewMatrix; + const float3x3 worldToView = (float3x3)modelViewMatrix; const float3x3 transform = mul(jacobian, worldToView); const float3x3 covariance3D = float3x3( covariance3D0.x, covariance3D0.y, covariance3D0.z, @@ -284,7 +284,7 @@ Shader "Builtin Gaussian Splat Utilities" const float offDiagonal = covariance2D.y; const float mid = 0.5 * (diagonal0 + diagonal1); const float radius = length(float2((diagonal0 - diagonal1) * 0.5, offDiagonal)); - const float lambda0 = max(mid + radius, 0.1); + const float lambda0 = mid + radius; const float lambda1 = max(mid - radius, 0.1); float2 basis = normalize(float2(offDiagonal, lambda0 - diagonal0)); @@ -425,22 +425,22 @@ Shader "Builtin Gaussian Splat Utilities" GaussianSplatSortDistances[index] = FloatToSortableUint(viewCenter.z); const float4 clipCenter = mul(gProjectionMatrix, float4(viewCenter, 1.0)); - if (clipCenter.w > 0.0) + const float nearClip = max(gCameraWorldPos.w, 1.0e-4); + if (clipCenter.w > 0.0 && viewCenter.z > nearClip) { - const float3x3 modelLinear = (float3x3)gModelMatrix; + const float4x4 modelViewMatrix = mul(gViewMatrix, gModelMatrix); const float3x3 rotationScaleMatrix = CalcMatrixFromRotationScale(otherData.rotation, otherData.scaleReserved.xyz); - const float3x3 worldRotationScale = mul(modelLinear, rotationScaleMatrix); float3 covariance3D0 = 0.0; float3 covariance3D1 = 0.0; - CalcCovariance3D(worldRotationScale, covariance3D0, covariance3D1); + CalcCovariance3D(rotationScaleMatrix, covariance3D0, covariance3D1); const float3 covariance2D = CalcCovariance2D( - viewCenter, + localCenter, covariance3D0, covariance3D1, - gViewMatrix, + modelViewMatrix, gProjectionMatrix, gScreenParams.xy); diff --git a/engine/assets/builtin/shaders/gaussian-splat.shader b/engine/assets/builtin/shaders/gaussian-splat.shader index 93a86ab1..5ce5ac3a 100644 --- a/engine/assets/builtin/shaders/gaussian-splat.shader +++ b/engine/assets/builtin/shaders/gaussian-splat.shader @@ -4,6 +4,7 @@ Shader "Builtin Gaussian Splat" { _PointScale ("Point Scale", Float) = 1.0 _OpacityScale ("Opacity Scale", Float) = 1.0 + _DebugViewMode ("Debug View Mode", Float) = 0.0 } HLSLINCLUDE cbuffer PerObjectConstants @@ -23,6 +24,7 @@ Shader "Builtin Gaussian Splat" { float4 gPointScaleParams; float4 gOpacityScaleParams; + float4 gDebugViewModeParams; }; struct GaussianSplatViewData @@ -91,8 +93,14 @@ Shader "Builtin Gaussian Splat" discard; } - return float4(input.colorOpacity.rgb, alpha); + if (gDebugViewModeParams.x >= 0.5) + { + return float4(alpha, alpha, alpha, alpha); + } + + return float4(input.colorOpacity.rgb * alpha, alpha); } + ENDHLSL SubShader @@ -105,12 +113,13 @@ Shader "Builtin Gaussian Splat" Cull Off ZWrite Off ZTest LEqual - Blend SrcAlpha OneMinusSrcAlpha + Blend OneMinusDstAlpha One HLSLPROGRAM #pragma target 4.5 #pragma vertex MainVS #pragma fragment MainPS ENDHLSL } + } } diff --git a/engine/include/XCEngine/Core/Asset/AssetDatabase.h b/engine/include/XCEngine/Core/Asset/AssetDatabase.h index 12f277d8..a102e9ba 100644 --- a/engine/include/XCEngine/Core/Asset/AssetDatabase.h +++ b/engine/include/XCEngine/Core/Asset/AssetDatabase.h @@ -112,6 +112,10 @@ public: private: static constexpr Core::uint32 kBaseImporterVersion = 7; + enum class SourceHashPolicy : Core::uint8 { + PreserveOrClear = 0, + EnsureCurrent = 1 + }; void EnsureProjectLayout(); void LoadSourceAssetDB(); @@ -126,6 +130,7 @@ private: bool EnsureMetaForPath(const std::filesystem::path& sourcePath, bool isFolder, + SourceHashPolicy sourceHashPolicy, SourceAssetRecord& outRecord); bool ReadMetaFile(const std::filesystem::path& metaPath, SourceAssetRecord& inOutRecord) const; diff --git a/engine/include/XCEngine/Rendering/Passes/BuiltinGaussianSplatPass.h b/engine/include/XCEngine/Rendering/Passes/BuiltinGaussianSplatPass.h index 22decc77..fdcd4d5e 100644 --- a/engine/include/XCEngine/Rendering/Passes/BuiltinGaussianSplatPass.h +++ b/engine/include/XCEngine/Rendering/Passes/BuiltinGaussianSplatPass.h @@ -70,6 +70,16 @@ private: RHI::RHIDescriptorSet* set = nullptr; }; + struct CompositePipelineResources { + RHI::RHIPipelineLayout* pipelineLayout = nullptr; + RHI::RHIPipelineState* pipelineState = nullptr; + OwnedDescriptorSet textureSet = {}; + RHI::RHIResourceView* boundAccumulationView = nullptr; + RHI::Format renderTargetFormat = RHI::Format::Unknown; + Core::uint32 sampleCount = 1u; + Core::uint32 sampleQuality = 0u; + }; + struct PassLayoutKey { const Resources::Shader* shader = nullptr; Containers::String passName; @@ -285,10 +295,21 @@ private: const RenderSurface& surface, const RenderSceneData& sceneData, const VisibleGaussianSplatItem& visibleGaussianSplat); + bool EnsureCompositeResources( + const RenderContext& context, + const RenderSurface& surface); + bool CreateCompositeResources( + const RenderContext& context, + const RenderSurface& surface); + void DestroyCompositeResources(); + bool CompositeAccumulationSurface( + const RenderPassContext& context, + RHI::RHIResourceView* accumulationTextureView); RHI::RHIDevice* m_device = nullptr; RHI::RHIType m_backendType = RHI::RHIType::D3D12; Resources::ResourceHandle m_builtinGaussianSplatShader; + Resources::ResourceHandle m_builtinGaussianSplatCompositeShader; Resources::ResourceHandle m_builtinGaussianSplatUtilitiesShader; std::unique_ptr m_builtinGaussianSplatMaterial; RenderResourceCache m_resourceCache; @@ -298,6 +319,7 @@ private: std::unordered_map m_pipelineStates; std::unordered_map m_computePipelineStates; std::unordered_map m_dynamicDescriptorSets; + CompositePipelineResources m_compositeResources = {}; }; } // namespace Passes diff --git a/engine/include/XCEngine/Resources/BuiltinResources.h b/engine/include/XCEngine/Resources/BuiltinResources.h index 72514396..92d75fce 100644 --- a/engine/include/XCEngine/Resources/BuiltinResources.h +++ b/engine/include/XCEngine/Resources/BuiltinResources.h @@ -39,6 +39,7 @@ Containers::String GetBuiltinSelectionMaskShaderPath(); Containers::String GetBuiltinSelectionOutlineShaderPath(); Containers::String GetBuiltinSkyboxShaderPath(); Containers::String GetBuiltinGaussianSplatShaderPath(); +Containers::String GetBuiltinGaussianSplatCompositeShaderPath(); Containers::String GetBuiltinGaussianSplatUtilitiesShaderPath(); Containers::String GetBuiltinVolumetricShaderPath(); Containers::String GetBuiltinColorScalePostProcessShaderPath(); diff --git a/engine/src/Core/Asset/AssetDatabase.cpp b/engine/src/Core/Asset/AssetDatabase.cpp index a77a3613..522df6db 100644 --- a/engine/src/Core/Asset/AssetDatabase.cpp +++ b/engine/src/Core/Asset/AssetDatabase.cpp @@ -689,6 +689,7 @@ std::vector CollectBuiltinShaderAssetPaths() { GetBuiltinSelectionOutlineShaderPath(), GetBuiltinSkyboxShaderPath(), GetBuiltinGaussianSplatShaderPath(), + GetBuiltinGaussianSplatCompositeShaderPath(), GetBuiltinGaussianSplatUtilitiesShaderPath(), GetBuiltinVolumetricShaderPath(), GetBuiltinColorScalePostProcessShaderPath(), @@ -1734,7 +1735,11 @@ bool AssetDatabase::ReimportAsset(const Containers::String& requestPath, } SourceAssetRecord sourceRecord; - if (!EnsureMetaForPath(absoluteFsPath, false, sourceRecord)) { + if (!EnsureMetaForPath( + absoluteFsPath, + false, + SourceHashPolicy::EnsureCurrent, + sourceRecord)) { SetLastErrorMessage(Containers::String("Failed to prepare asset metadata: ") + absolutePath); return false; } @@ -1806,18 +1811,35 @@ bool AssetDatabase::ReimportAllAssets(MaintenanceStats* outStats) { bool allSucceeded = true; MaintenanceStats localStats; for (const SourceAssetRecord& record : importableRecords) { - ArtifactRecord rebuiltRecord; - if (!ImportAsset(record, rebuiltRecord)) { + const fs::path sourcePath = fs::path(m_projectRoot.CStr()) / record.relativePath.CStr(); + + SourceAssetRecord currentRecord; + if (!EnsureMetaForPath( + sourcePath, + false, + SourceHashPolicy::EnsureCurrent, + currentRecord)) { Debug::Logger::Get().Error( Debug::LogCategory::FileSystem, - Containers::String("[AssetDatabase] ReimportAllAssets failed path=") + record.relativePath); + Containers::String("[AssetDatabase] ReimportAllAssets failed to refresh metadata path=") + + record.relativePath); allSucceeded = false; continue; } - m_artifactsByGuid[record.guid] = rebuiltRecord; - m_sourcesByGuid[record.guid].lastKnownArtifactKey = rebuiltRecord.artifactKey; - m_sourcesByPathKey[ToStdString(MakeKey(record.relativePath))].lastKnownArtifactKey = rebuiltRecord.artifactKey; + ArtifactRecord rebuiltRecord; + if (!ImportAsset(currentRecord, rebuiltRecord)) { + Debug::Logger::Get().Error( + Debug::LogCategory::FileSystem, + Containers::String("[AssetDatabase] ReimportAllAssets failed path=") + currentRecord.relativePath); + allSucceeded = false; + continue; + } + + m_artifactsByGuid[currentRecord.guid] = rebuiltRecord; + m_sourcesByGuid[currentRecord.guid].lastKnownArtifactKey = rebuiltRecord.artifactKey; + m_sourcesByPathKey[ToStdString(MakeKey(currentRecord.relativePath))].lastKnownArtifactKey = + rebuiltRecord.artifactKey; ++localStats.importedAssetCount; } @@ -2086,7 +2108,11 @@ void AssetDatabase::ScanAssetPath(const fs::path& path, const bool isFolder = fs::is_directory(path); SourceAssetRecord record; - if (EnsureMetaForPath(path, isFolder, record)) { + if (EnsureMetaForPath( + path, + isFolder, + SourceHashPolicy::PreserveOrClear, + record)) { seenPaths[ToStdString(MakeKey(record.relativePath))] = true; } @@ -2194,6 +2220,7 @@ Core::uint32 AssetDatabase::CleanupOrphanedArtifacts() const { bool AssetDatabase::EnsureMetaForPath(const fs::path& sourcePath, bool isFolder, + SourceHashPolicy sourceHashPolicy, SourceAssetRecord& outRecord) { const Containers::String relativePath = NormalizeRelativePath(sourcePath); if (relativePath.Empty()) { @@ -2216,6 +2243,35 @@ bool AssetDatabase::EnsureMetaForPath(const fs::path& sourcePath, outRecord.importerName = GetImporterNameForPath(relativePath, isFolder); outRecord.importerVersion = GetCurrentImporterVersion(outRecord.importerName); + const auto refreshSourceSnapshot = [&]() { + if (isFolder) { + outRecord.sourceHash.Clear(); + outRecord.sourceFileSize = 0; + outRecord.sourceWriteTime = 0; + return; + } + + const Core::uint64 fileSize = GetFileSizeValue(sourcePath); + const Core::uint64 writeTime = GetFileWriteTimeValue(sourcePath); + const bool canReuseExistingHash = + existingIt != m_sourcesByPathKey.end() && + existingIt->second.sourceFileSize == fileSize && + existingIt->second.sourceWriteTime == writeTime && + !existingIt->second.sourceHash.Empty(); + + if (sourceHashPolicy == SourceHashPolicy::EnsureCurrent) { + outRecord.sourceHash = + canReuseExistingHash ? existingIt->second.sourceHash : ComputeFileHash(sourcePath); + } else if (canReuseExistingHash) { + outRecord.sourceHash = existingIt->second.sourceHash; + } else { + outRecord.sourceHash.Clear(); + } + + outRecord.sourceFileSize = fileSize; + outRecord.sourceWriteTime = writeTime; + }; + if (UsesExternalSyntheticSourceRecord(relativePath)) { if (!outRecord.guid.IsValid()) { outRecord.guid = HashStringToAssetGUID(NormalizePathString(sourcePath)); @@ -2233,24 +2289,7 @@ bool AssetDatabase::EnsureMetaForPath(const fs::path& sourcePath, outRecord.guid = HashStringToAssetGUID(duplicateSignature); } - if (isFolder) { - outRecord.sourceHash.Clear(); - outRecord.sourceFileSize = 0; - outRecord.sourceWriteTime = 0; - } else { - const Core::uint64 fileSize = GetFileSizeValue(sourcePath); - const Core::uint64 writeTime = GetFileWriteTimeValue(sourcePath); - if (existingIt != m_sourcesByPathKey.end() && - existingIt->second.sourceFileSize == fileSize && - existingIt->second.sourceWriteTime == writeTime && - !existingIt->second.sourceHash.Empty()) { - outRecord.sourceHash = existingIt->second.sourceHash; - } else { - outRecord.sourceHash = ComputeFileHash(sourcePath); - } - outRecord.sourceFileSize = fileSize; - outRecord.sourceWriteTime = writeTime; - } + refreshSourceSnapshot(); m_sourcesByPathKey[pathKey] = outRecord; m_sourcesByGuid[outRecord.guid] = outRecord; @@ -2290,24 +2329,7 @@ bool AssetDatabase::EnsureMetaForPath(const fs::path& sourcePath, } outRecord.metaHash = HashStringToAssetGUID(ReadWholeFileText(metaPath)).ToString(); - if (isFolder) { - outRecord.sourceHash.Clear(); - outRecord.sourceFileSize = 0; - outRecord.sourceWriteTime = 0; - } else { - const Core::uint64 fileSize = GetFileSizeValue(sourcePath); - const Core::uint64 writeTime = GetFileWriteTimeValue(sourcePath); - if (existingIt != m_sourcesByPathKey.end() && - existingIt->second.sourceFileSize == fileSize && - existingIt->second.sourceWriteTime == writeTime && - !existingIt->second.sourceHash.Empty()) { - outRecord.sourceHash = existingIt->second.sourceHash; - } else { - outRecord.sourceHash = ComputeFileHash(sourcePath); - } - outRecord.sourceFileSize = fileSize; - outRecord.sourceWriteTime = writeTime; - } + refreshSourceSnapshot(); m_sourcesByPathKey[pathKey] = outRecord; m_sourcesByGuid[outRecord.guid] = outRecord; @@ -2512,6 +2534,11 @@ bool AssetDatabase::ShouldReimport(const SourceAssetRecord& sourceRecord, return true; } + if (!sourceRecord.isFolder && + (sourceRecord.sourceHash.Empty() || artifactRecord->sourceHash.Empty())) { + return true; + } + return artifactRecord->importerVersion != sourceRecord.importerVersion || artifactRecord->sourceHash != sourceRecord.sourceHash || artifactRecord->metaHash != sourceRecord.metaHash || @@ -2580,8 +2607,14 @@ bool AssetDatabase::EnsureArtifact(const Containers::String& requestPath, return false; } + const bool isFolder = fs::is_directory(absoluteFsPath); + SourceAssetRecord sourceRecord; - if (!EnsureMetaForPath(absoluteFsPath, fs::is_directory(absoluteFsPath), sourceRecord)) { + if (!EnsureMetaForPath( + absoluteFsPath, + isFolder, + SourceHashPolicy::PreserveOrClear, + sourceRecord)) { SetLastErrorMessage(Containers::String("Failed to prepare asset metadata: ") + absolutePath); return false; } @@ -2628,6 +2661,25 @@ bool AssetDatabase::EnsureArtifact(const Containers::String& requestPath, } if (ShouldReimport(sourceRecord, artifactRecord)) { + if (!EnsureMetaForPath( + absoluteFsPath, + isFolder, + SourceHashPolicy::EnsureCurrent, + sourceRecord)) { + SetLastErrorMessage(Containers::String("Failed to validate asset metadata: ") + absolutePath); + return false; + } + + artifactIt = m_artifactsByGuid.find(sourceRecord.guid); + artifactRecord = artifactIt != m_artifactsByGuid.end() ? &artifactIt->second : nullptr; + + if (!ShouldReimport(sourceRecord, artifactRecord)) { + SaveSourceAssetDB(); + PopulateResolvedAssetResult(m_projectRoot, sourceRecord, *artifactRecord, false, outAsset); + ClearLastErrorMessage(); + return true; + } + if (ShouldTraceAssetPath(requestPath)) { Debug::Logger::Get().Info( Debug::LogCategory::FileSystem, diff --git a/engine/src/Rendering/Passes/BuiltinGaussianSplatPass.cpp b/engine/src/Rendering/Passes/BuiltinGaussianSplatPass.cpp index 5eb4cdda..621fb059 100644 --- a/engine/src/Rendering/Passes/BuiltinGaussianSplatPass.cpp +++ b/engine/src/Rendering/Passes/BuiltinGaussianSplatPass.cpp @@ -65,6 +65,22 @@ const Resources::ShaderPass* FindCompatibleComputePass( : nullptr; } +const Resources::ShaderPass* FindCompatibleGraphicsPass( + const Resources::Shader& shader, + const Containers::String& passName, + const Resources::ShaderKeywordSet& keywordSet, + Resources::ShaderBackend backend) { + const Resources::ShaderPass* shaderPass = shader.FindPass(passName); + if (shaderPass == nullptr) { + return nullptr; + } + + return shader.FindVariant(passName, Resources::ShaderType::Vertex, backend, keywordSet) != nullptr && + shader.FindVariant(passName, Resources::ShaderType::Fragment, backend, keywordSet) != nullptr + ? shaderPass + : nullptr; +} + RHI::GraphicsPipelineDesc CreatePipelineDesc( RHI::RHIType backendType, RHI::RHIPipelineLayout* pipelineLayout, @@ -105,6 +121,44 @@ RHI::GraphicsPipelineDesc CreatePipelineDesc( return pipelineDesc; } +RHI::GraphicsPipelineDesc CreateCompositePipelineDesc( + RHI::RHIType backendType, + RHI::RHIPipelineLayout* pipelineLayout, + const Resources::Shader& shader, + const Resources::ShaderPass& shaderPass, + const Containers::String& passName, + const RenderSurface& surface) { + RHI::GraphicsPipelineDesc pipelineDesc = {}; + pipelineDesc.pipelineLayout = pipelineLayout; + pipelineDesc.topologyType = static_cast(RHI::PrimitiveTopologyType::Triangle); + ::XCEngine::Rendering::Internal::ApplySingleColorAttachmentPropertiesToGraphicsPipelineDesc(surface, pipelineDesc); + pipelineDesc.depthStencilFormat = + static_cast(::XCEngine::Rendering::Internal::ResolveSurfaceDepthFormat(surface)); + ApplyResolvedRenderState(&shaderPass, nullptr, pipelineDesc); + + const Resources::ShaderBackend backend = ::XCEngine::Rendering::Internal::ToShaderBackend(backendType); + if (const Resources::ShaderStageVariant* vertexVariant = + shader.FindVariant(passName, Resources::ShaderType::Vertex, backend)) { + ::XCEngine::Rendering::Internal::ApplyShaderStageVariant( + shader.GetPath(), + shaderPass, + backend, + *vertexVariant, + pipelineDesc.vertexShader); + } + if (const Resources::ShaderStageVariant* fragmentVariant = + shader.FindVariant(passName, Resources::ShaderType::Fragment, backend)) { + ::XCEngine::Rendering::Internal::ApplyShaderStageVariant( + shader.GetPath(), + shaderPass, + backend, + *fragmentVariant, + pipelineDesc.fragmentShader); + } + + return pipelineDesc; +} + RHI::ComputePipelineDesc CreateComputePipelineDesc( RHI::RHIType backendType, RHI::RHIPipelineLayout* pipelineLayout, @@ -208,6 +262,10 @@ void BindDescriptorSetRanges( } } +RHI::Format ResolveGaussianAccumulationFormat() { + return RHI::Format::R16G16B16A16_Float; +} + } // namespace BuiltinGaussianSplatPass::~BuiltinGaussianSplatPass() { @@ -304,10 +362,31 @@ bool BuiltinGaussianSplatPass::Execute(const RenderPassContext& context) { return false; } - RHI::RHICommandList* commandList = context.renderContext.commandList; - RHI::RHIResourceView* renderTarget = colorAttachments[0]; - commandList->SetRenderTargets(1, &renderTarget, context.surface.GetDepthAttachment()); + if (!EnsureCompositeResources(context.renderContext, context.surface)) { + Debug::Logger::Get().Error( + Debug::LogCategory::Rendering, + "BuiltinGaussianSplatPass failed to initialize composite resources"); + return false; + } + Internal::BuiltinGaussianSplatPassResources::AccumulationSurface* accumulationSurface = nullptr; + if (m_passResources == nullptr || + !m_passResources->EnsureAccumulationSurface( + m_device, + context.surface.GetWidth(), + context.surface.GetHeight(), + ResolveGaussianAccumulationFormat(), + accumulationSurface) || + accumulationSurface == nullptr || + accumulationSurface->renderTargetView == nullptr || + accumulationSurface->shaderResourceView == nullptr) { + Debug::Logger::Get().Error( + Debug::LogCategory::Rendering, + "BuiltinGaussianSplatPass failed to allocate gaussian accumulation surface"); + return false; + } + + RHI::RHICommandList* commandList = context.renderContext.commandList; const RHI::Viewport viewport = { static_cast(renderArea.x), static_cast(renderArea.y), @@ -322,9 +401,31 @@ bool BuiltinGaussianSplatPass::Execute(const RenderPassContext& context) { renderArea.x + renderArea.width, renderArea.y + renderArea.height }; + const RHI::Rect clearRects[] = { scissorRect }; + const float clearColor[4] = { 0.0f, 0.0f, 0.0f, 0.0f }; + + RenderSurface accumulationRenderSurface(context.surface.GetWidth(), context.surface.GetHeight()); + accumulationRenderSurface.SetColorAttachment(accumulationSurface->renderTargetView); + accumulationRenderSurface.SetDepthAttachment(context.surface.GetDepthAttachment()); + accumulationRenderSurface.SetRenderArea(renderArea); + accumulationRenderSurface.SetAutoTransitionEnabled(false); + accumulationRenderSurface.SetSampleDesc(1u, 0u); + + commandList->EndRenderPass(); + if (accumulationSurface->currentColorState != RHI::ResourceStates::RenderTarget) { + commandList->TransitionBarrier( + accumulationSurface->renderTargetView, + accumulationSurface->currentColorState, + RHI::ResourceStates::RenderTarget); + accumulationSurface->currentColorState = RHI::ResourceStates::RenderTarget; + } + + RHI::RHIResourceView* accumulationRenderTarget = accumulationSurface->renderTargetView; + commandList->SetRenderTargets(1, &accumulationRenderTarget, context.surface.GetDepthAttachment()); commandList->SetViewport(viewport); commandList->SetScissorRect(scissorRect); commandList->SetPrimitiveTopology(RHI::PrimitiveTopology::TriangleList); + commandList->ClearRenderTarget(accumulationRenderTarget, clearColor, 1u, clearRects); for (const VisibleGaussianSplatItem& visibleGaussianSplat : context.sceneData.visibleGaussianSplats) { if (!MarkVisibleGaussianSplatChunks( @@ -350,14 +451,23 @@ bool BuiltinGaussianSplatPass::Execute(const RenderPassContext& context) { if (!DrawVisibleGaussianSplat( context.renderContext, - context.surface, + accumulationRenderSurface, context.sceneData, visibleGaussianSplat)) { return false; } } - return true; + commandList->EndRenderPass(); + if (accumulationSurface->currentColorState != RHI::ResourceStates::PixelShaderResource) { + commandList->TransitionBarrier( + accumulationSurface->shaderResourceView, + accumulationSurface->currentColorState, + RHI::ResourceStates::PixelShaderResource); + accumulationSurface->currentColorState = RHI::ResourceStates::PixelShaderResource; + } + + return CompositeAccumulationSurface(context, accumulationSurface->shaderResourceView); } void BuiltinGaussianSplatPass::Shutdown() { @@ -372,6 +482,8 @@ bool BuiltinGaussianSplatPass::EnsureInitialized(const RenderContext& context) { if (m_device == context.device && m_backendType == context.backendType && m_builtinGaussianSplatShader.IsValid() && + m_builtinGaussianSplatCompositeShader.IsValid() && + m_builtinGaussianSplatUtilitiesShader.IsValid() && m_builtinGaussianSplatMaterial != nullptr) { return true; } @@ -393,6 +505,16 @@ bool BuiltinGaussianSplatPass::CreateResources(const RenderContext& context) { return false; } + m_builtinGaussianSplatCompositeShader = Resources::ResourceManager::Get().Load( + Resources::GetBuiltinGaussianSplatCompositeShaderPath()); + if (!m_builtinGaussianSplatCompositeShader.IsValid()) { + Debug::Logger::Get().Error( + Debug::LogCategory::Rendering, + "BuiltinGaussianSplatPass failed to load builtin gaussian composite shader resource"); + DestroyResources(); + return false; + } + m_builtinGaussianSplatUtilitiesShader = Resources::ResourceManager::Get().Load( Resources::GetBuiltinGaussianSplatUtilitiesShaderPath()); if (!m_builtinGaussianSplatUtilitiesShader.IsValid()) { @@ -415,6 +537,207 @@ bool BuiltinGaussianSplatPass::CreateResources(const RenderContext& context) { return true; } +bool BuiltinGaussianSplatPass::EnsureCompositeResources( + const RenderContext& context, + const RenderSurface& surface) { + const RHI::Format renderTargetFormat = + ::XCEngine::Rendering::Internal::ResolveSurfaceColorFormat(surface, 0u); + const Core::uint32 sampleCount = + ::XCEngine::Rendering::Internal::ResolveSurfaceSampleCount(surface); + const Core::uint32 sampleQuality = + ::XCEngine::Rendering::Internal::ResolveSurfaceSampleQuality(surface); + + if (m_compositeResources.pipelineLayout != nullptr && + m_compositeResources.pipelineState != nullptr && + m_compositeResources.textureSet.set != nullptr && + m_compositeResources.renderTargetFormat == renderTargetFormat && + m_compositeResources.sampleCount == sampleCount && + m_compositeResources.sampleQuality == sampleQuality) { + return true; + } + + DestroyCompositeResources(); + return CreateCompositeResources(context, surface); +} + +bool BuiltinGaussianSplatPass::CreateCompositeResources( + const RenderContext& context, + const RenderSurface& surface) { + if (!context.IsValid() || !m_builtinGaussianSplatCompositeShader.IsValid()) { + return false; + } + + RHI::Format renderTargetFormat = RHI::Format::Unknown; + if (!::XCEngine::Rendering::Internal::TryResolveSingleColorAttachmentFormat(surface, renderTargetFormat)) { + Debug::Logger::Get().Error( + Debug::LogCategory::Rendering, + "BuiltinGaussianSplatPass composite requires a valid single-color destination surface"); + return false; + } + + const Containers::String compositePassName("GaussianComposite"); + const Resources::Shader& shader = *m_builtinGaussianSplatCompositeShader.Get(); + const Resources::ShaderBackend backend = ::XCEngine::Rendering::Internal::ToShaderBackend(context.backendType); + const Resources::ShaderPass* compositePass = FindCompatibleGraphicsPass( + shader, + compositePassName, + Resources::ShaderKeywordSet(), + backend); + if (compositePass == nullptr) { + Debug::Logger::Get().Error( + Debug::LogCategory::Rendering, + "BuiltinGaussianSplatPass failed to resolve gaussian composite shader pass"); + return false; + } + + RHI::DescriptorSetLayoutBinding textureBinding = {}; + textureBinding.binding = 0u; + textureBinding.type = static_cast(RHI::DescriptorType::SRV); + textureBinding.count = 1u; + textureBinding.visibility = static_cast(RHI::ShaderVisibility::All); + + RHI::DescriptorSetLayoutDesc textureLayout = {}; + textureLayout.bindings = &textureBinding; + textureLayout.bindingCount = 1u; + + RHI::RHIPipelineLayoutDesc pipelineLayoutDesc = {}; + pipelineLayoutDesc.setLayouts = &textureLayout; + pipelineLayoutDesc.setLayoutCount = 1u; + m_compositeResources.pipelineLayout = m_device->CreatePipelineLayout(pipelineLayoutDesc); + if (m_compositeResources.pipelineLayout == nullptr) { + Debug::Logger::Get().Error( + Debug::LogCategory::Rendering, + "BuiltinGaussianSplatPass failed to create gaussian composite pipeline layout"); + DestroyCompositeResources(); + return false; + } + + RHI::DescriptorPoolDesc poolDesc = {}; + poolDesc.type = RHI::DescriptorHeapType::CBV_SRV_UAV; + poolDesc.descriptorCount = 1u; + poolDesc.shaderVisible = true; + m_compositeResources.textureSet.pool = m_device->CreateDescriptorPool(poolDesc); + if (m_compositeResources.textureSet.pool == nullptr) { + Debug::Logger::Get().Error( + Debug::LogCategory::Rendering, + "BuiltinGaussianSplatPass failed to create gaussian composite descriptor pool"); + DestroyCompositeResources(); + return false; + } + + m_compositeResources.textureSet.set = + m_compositeResources.textureSet.pool->AllocateSet(textureLayout); + if (m_compositeResources.textureSet.set == nullptr) { + Debug::Logger::Get().Error( + Debug::LogCategory::Rendering, + "BuiltinGaussianSplatPass failed to allocate gaussian composite descriptor set"); + DestroyCompositeResources(); + return false; + } + + m_compositeResources.pipelineState = m_device->CreatePipelineState( + CreateCompositePipelineDesc( + m_backendType, + m_compositeResources.pipelineLayout, + shader, + *compositePass, + compositePassName, + surface)); + if (m_compositeResources.pipelineState == nullptr || !m_compositeResources.pipelineState->IsValid()) { + Debug::Logger::Get().Error( + Debug::LogCategory::Rendering, + "BuiltinGaussianSplatPass failed to create gaussian composite pipeline state"); + DestroyCompositeResources(); + return false; + } + + m_compositeResources.renderTargetFormat = renderTargetFormat; + m_compositeResources.sampleCount = + ::XCEngine::Rendering::Internal::ResolveSurfaceSampleCount(surface); + m_compositeResources.sampleQuality = + ::XCEngine::Rendering::Internal::ResolveSurfaceSampleQuality(surface); + return true; +} + +void BuiltinGaussianSplatPass::DestroyCompositeResources() { + m_compositeResources.boundAccumulationView = nullptr; + + if (m_compositeResources.pipelineState != nullptr) { + m_compositeResources.pipelineState->Shutdown(); + delete m_compositeResources.pipelineState; + m_compositeResources.pipelineState = nullptr; + } + + DestroyOwnedDescriptorSet(m_compositeResources.textureSet); + + if (m_compositeResources.pipelineLayout != nullptr) { + m_compositeResources.pipelineLayout->Shutdown(); + delete m_compositeResources.pipelineLayout; + m_compositeResources.pipelineLayout = nullptr; + } + + m_compositeResources.renderTargetFormat = RHI::Format::Unknown; + m_compositeResources.sampleCount = 1u; + m_compositeResources.sampleQuality = 0u; +} + +bool BuiltinGaussianSplatPass::CompositeAccumulationSurface( + const RenderPassContext& context, + RHI::RHIResourceView* accumulationTextureView) { + if (m_compositeResources.pipelineLayout == nullptr || + m_compositeResources.pipelineState == nullptr || + m_compositeResources.textureSet.set == nullptr || + accumulationTextureView == nullptr) { + Debug::Logger::Get().Error( + Debug::LogCategory::Rendering, + "BuiltinGaussianSplatPass composite failed: composite resources are incomplete"); + return false; + } + + const std::vector& colorAttachments = context.surface.GetColorAttachments(); + if (colorAttachments.empty() || colorAttachments[0] == nullptr) { + return false; + } + + if (m_compositeResources.boundAccumulationView != accumulationTextureView) { + m_compositeResources.textureSet.set->Update(0u, accumulationTextureView); + m_compositeResources.boundAccumulationView = accumulationTextureView; + } + + const Math::RectInt renderArea = context.surface.GetRenderArea(); + const RHI::Viewport viewport = { + static_cast(renderArea.x), + static_cast(renderArea.y), + static_cast(renderArea.width), + static_cast(renderArea.height), + 0.0f, + 1.0f + }; + const RHI::Rect scissorRect = { + renderArea.x, + renderArea.y, + renderArea.x + renderArea.width, + renderArea.y + renderArea.height + }; + + RHI::RHICommandList* commandList = context.renderContext.commandList; + RHI::RHIResourceView* renderTarget = colorAttachments[0]; + commandList->SetRenderTargets(1u, &renderTarget, context.surface.GetDepthAttachment()); + commandList->SetViewport(viewport); + commandList->SetScissorRect(scissorRect); + commandList->SetPrimitiveTopology(RHI::PrimitiveTopology::TriangleList); + commandList->SetPipelineState(m_compositeResources.pipelineState); + + RHI::RHIDescriptorSet* descriptorSet = m_compositeResources.textureSet.set; + commandList->SetGraphicsDescriptorSets( + 0u, + 1u, + &descriptorSet, + m_compositeResources.pipelineLayout); + commandList->Draw(3u, 1u, 0u, 0u); + return true; +} + void BuiltinGaussianSplatPass::DestroyResources() { if (m_passResources != nullptr) { m_passResources->Shutdown(); @@ -450,8 +773,11 @@ void BuiltinGaussianSplatPass::DestroyResources() { } m_passResourceLayouts.clear(); + DestroyCompositeResources(); + m_builtinGaussianSplatMaterial.reset(); m_builtinGaussianSplatUtilitiesShader.Reset(); + m_builtinGaussianSplatCompositeShader.Reset(); m_builtinGaussianSplatShader.Reset(); m_device = nullptr; m_backendType = RHI::RHIType::D3D12; @@ -1148,10 +1474,10 @@ bool BuiltinGaussianSplatPass::MarkVisibleGaussianSplatChunks( sceneData.cameraData.projection, sceneData.cameraData.view, visibleGaussianSplat.localToWorld.Transpose(), - visibleGaussianSplat.localToWorld.Inverse(), + visibleGaussianSplat.localToWorld.Inverse().Transpose(), Math::Vector4(sceneData.cameraData.worldRight, 0.0f), Math::Vector4(sceneData.cameraData.worldUp, 0.0f), - Math::Vector4(sceneData.cameraData.worldPosition, 0.0f), + Math::Vector4(sceneData.cameraData.worldPosition, sceneData.cameraData.nearClipPlane), Math::Vector4( static_cast(sceneData.cameraData.viewportWidth), static_cast(sceneData.cameraData.viewportHeight), @@ -1325,10 +1651,10 @@ bool BuiltinGaussianSplatPass::PrepareVisibleGaussianSplat( sceneData.cameraData.projection, sceneData.cameraData.view, visibleGaussianSplat.localToWorld.Transpose(), - visibleGaussianSplat.localToWorld.Inverse(), + visibleGaussianSplat.localToWorld.Inverse().Transpose(), Math::Vector4(sceneData.cameraData.worldRight, 0.0f), Math::Vector4(sceneData.cameraData.worldUp, 0.0f), - Math::Vector4(sceneData.cameraData.worldPosition, 0.0f), + Math::Vector4(sceneData.cameraData.worldPosition, sceneData.cameraData.nearClipPlane), Math::Vector4( static_cast(sceneData.cameraData.viewportWidth), static_cast(sceneData.cameraData.viewportHeight), @@ -1509,10 +1835,10 @@ bool BuiltinGaussianSplatPass::SortVisibleGaussianSplat( sceneData.cameraData.projection, sceneData.cameraData.view, visibleGaussianSplat.localToWorld.Transpose(), - visibleGaussianSplat.localToWorld.Inverse(), + visibleGaussianSplat.localToWorld.Inverse().Transpose(), Math::Vector4(sceneData.cameraData.worldRight, 0.0f), Math::Vector4(sceneData.cameraData.worldUp, 0.0f), - Math::Vector4(sceneData.cameraData.worldPosition, 0.0f), + Math::Vector4(sceneData.cameraData.worldPosition, sceneData.cameraData.nearClipPlane), Math::Vector4( static_cast(sceneData.cameraData.viewportWidth), static_cast(sceneData.cameraData.viewportHeight), @@ -1682,10 +2008,10 @@ bool BuiltinGaussianSplatPass::DrawVisibleGaussianSplat( sceneData.cameraData.projection, sceneData.cameraData.view, visibleGaussianSplat.localToWorld.Transpose(), - visibleGaussianSplat.localToWorld.Inverse(), + visibleGaussianSplat.localToWorld.Inverse().Transpose(), Math::Vector4(sceneData.cameraData.worldRight, 0.0f), Math::Vector4(sceneData.cameraData.worldUp, 0.0f), - Math::Vector4(sceneData.cameraData.worldPosition, 0.0f), + Math::Vector4(sceneData.cameraData.worldPosition, sceneData.cameraData.nearClipPlane), Math::Vector4( static_cast(sceneData.cameraData.viewportWidth), static_cast(sceneData.cameraData.viewportHeight), diff --git a/engine/src/Resources/BuiltinResources.cpp b/engine/src/Resources/BuiltinResources.cpp index 19a2e60b..cd4dd57f 100644 --- a/engine/src/Resources/BuiltinResources.cpp +++ b/engine/src/Resources/BuiltinResources.cpp @@ -37,6 +37,8 @@ constexpr const char* kBuiltinSelectionMaskShaderPath = "builtin://shaders/selec constexpr const char* kBuiltinSelectionOutlineShaderPath = "builtin://shaders/selection-outline"; constexpr const char* kBuiltinSkyboxShaderPath = "builtin://shaders/skybox"; constexpr const char* kBuiltinGaussianSplatShaderPath = "builtin://shaders/gaussian-splat"; +constexpr const char* kBuiltinGaussianSplatCompositeShaderPath = + "builtin://shaders/gaussian-splat-composite"; constexpr const char* kBuiltinGaussianSplatUtilitiesShaderPath = "builtin://shaders/gaussian-splat-utilities"; constexpr const char* kBuiltinVolumetricShaderPath = "builtin://shaders/volumetric"; @@ -73,6 +75,8 @@ constexpr const char* kBuiltinSkyboxShaderAssetRelativePath = "engine/assets/builtin/shaders/skybox.shader"; constexpr const char* kBuiltinGaussianSplatShaderAssetRelativePath = "engine/assets/builtin/shaders/gaussian-splat.shader"; +constexpr const char* kBuiltinGaussianSplatCompositeShaderAssetRelativePath = + "engine/assets/builtin/shaders/gaussian-splat-composite.shader"; constexpr const char* kBuiltinGaussianSplatUtilitiesShaderAssetRelativePath = "engine/assets/builtin/shaders/gaussian-splat-utilities.shader"; constexpr const char* kBuiltinVolumetricShaderAssetRelativePath = @@ -174,6 +178,9 @@ const char* GetBuiltinShaderAssetRelativePath(const Containers::String& builtinS if (builtinShaderPath == Containers::String(kBuiltinGaussianSplatShaderPath)) { return kBuiltinGaussianSplatShaderAssetRelativePath; } + if (builtinShaderPath == Containers::String(kBuiltinGaussianSplatCompositeShaderPath)) { + return kBuiltinGaussianSplatCompositeShaderAssetRelativePath; + } if (builtinShaderPath == Containers::String(kBuiltinGaussianSplatUtilitiesShaderPath)) { return kBuiltinGaussianSplatUtilitiesShaderAssetRelativePath; } @@ -754,6 +761,10 @@ Shader* BuildBuiltinGaussianSplatShader(const Containers::String& path) { return TryLoadBuiltinShaderFromAsset(path); } +Shader* BuildBuiltinGaussianSplatCompositeShader(const Containers::String& path) { + return TryLoadBuiltinShaderFromAsset(path); +} + Shader* BuildBuiltinVolumetricShader(const Containers::String& path) { return TryLoadBuiltinShaderFromAsset(path); } @@ -876,6 +887,10 @@ bool TryGetBuiltinShaderPathByShaderName( outPath = GetBuiltinGaussianSplatShaderPath(); return true; } + if (shaderName == "Builtin Gaussian Splat Composite") { + outPath = GetBuiltinGaussianSplatCompositeShaderPath(); + return true; + } if (shaderName == "Builtin Gaussian Splat Utilities") { outPath = GetBuiltinGaussianSplatUtilitiesShaderPath(); return true; @@ -971,6 +986,10 @@ Containers::String GetBuiltinGaussianSplatShaderPath() { return Containers::String(kBuiltinGaussianSplatShaderPath); } +Containers::String GetBuiltinGaussianSplatCompositeShaderPath() { + return Containers::String(kBuiltinGaussianSplatCompositeShaderPath); +} + Containers::String GetBuiltinGaussianSplatUtilitiesShaderPath() { return Containers::String(kBuiltinGaussianSplatUtilitiesShaderPath); } @@ -1095,6 +1114,8 @@ LoadResult CreateBuiltinShaderResource(const Containers::String& path) { shader = BuildBuiltinSkyboxShader(path); } else if (path == GetBuiltinGaussianSplatShaderPath()) { shader = BuildBuiltinGaussianSplatShader(path); + } else if (path == GetBuiltinGaussianSplatCompositeShaderPath()) { + shader = BuildBuiltinGaussianSplatCompositeShader(path); } else if (path == GetBuiltinGaussianSplatUtilitiesShaderPath()) { shader = TryLoadBuiltinShaderFromAsset(path); } else if (path == GetBuiltinVolumetricShaderPath()) { diff --git a/project/Assets/Scenes/Main.xc b/project/Assets/Scenes/Main.xc index 69771a30..c9c09f38 100644 --- a/project/Assets/Scenes/Main.xc +++ b/project/Assets/Scenes/Main.xc @@ -23,7 +23,7 @@ active=1 layer=0 parent=0 transform=position=5.87107,3.04462,0.305848;rotation=0.0591537,-0.523754,0.799249,0.288762;scale=1,1,1; -component=Light;type=0;color=1,1,1,1;intensity=0.5;range=10;spotAngle=30;shadows=1; +component=Light;type=0;color=1,1,1,1;intensity=0.5;range=10;spotAngle=30;shadows=0; gameobject_end gameobject_begin diff --git a/project/Assets/Scenes/NahidaPreview.xc b/project/Assets/Scenes/NahidaPreview.xc index fdd4b160..4821365c 100644 --- a/project/Assets/Scenes/NahidaPreview.xc +++ b/project/Assets/Scenes/NahidaPreview.xc @@ -10,7 +10,7 @@ tag=Untagged active=1 layer=0 parent=0 -transform=position=0,1.2,-4.25;rotation=0.104528,0,0,0.994522;scale=1,1,1; +transform=position=0,1.3,-2.75;rotation=0.104528,0,0,0.994522;scale=1,1,1; component=Camera;projection=0;fov=35;orthoSize=5;near=0.01;far=100;depth=0;primary=1;clearMode=0;stackType=0;cullingMask=4294967295;viewportRect=0,0,1,1;clearColor=0.04,0.05,0.07,1;skyboxEnabled=0;skyboxMaterialPath=;skyboxMaterialRef=;skyboxTopColor=0.18,0.36,0.74,1;skyboxHorizonColor=0.78,0.84,0.92,1;skyboxBottomColor=0.92,0.93,0.95,1;finalColorOverrideOutputTransferEnabled=0;finalColorOverrideOutputTransferMode=0;finalColorOverrideExposureModeEnabled=0;finalColorOverrideExposureMode=0;finalColorOverrideExposureValueEnabled=0;finalColorOverrideExposureValue=1;finalColorOverrideToneMappingModeEnabled=0;finalColorOverrideToneMappingMode=0;finalColorOverrideScaleEnabled=0;finalColorOverrideScale=1,1,1,1;postProcessPassCount=0; gameobject_end diff --git a/tests/Rendering/integration/gaussian_splat_scene/main.cpp b/tests/Rendering/integration/gaussian_splat_scene/main.cpp index 7ddac325..fbd996f7 100644 --- a/tests/Rendering/integration/gaussian_splat_scene/main.cpp +++ b/tests/Rendering/integration/gaussian_splat_scene/main.cpp @@ -28,10 +28,13 @@ #include "../../../RHI/integration/fixtures/RHIIntegrationFixture.h" #include +#include +#include #include #include #include #include +#include #include #include @@ -48,12 +51,23 @@ namespace { constexpr const char* kD3D12Screenshot = "gaussian_splat_scene_d3d12.ppm"; constexpr const char* kOpenGLScreenshot = "gaussian_splat_scene_opengl.ppm"; constexpr const char* kVulkanScreenshot = "gaussian_splat_scene_vulkan.ppm"; +constexpr const char* kD3D12AlphaDebugScreenshot = "gaussian_splat_scene_d3d12_alpha.ppm"; +constexpr const char* kOpenGLAlphaDebugScreenshot = "gaussian_splat_scene_opengl_alpha.ppm"; +constexpr const char* kVulkanAlphaDebugScreenshot = "gaussian_splat_scene_vulkan_alpha.ppm"; constexpr uint32_t kFrameWidth = 1280; constexpr uint32_t kFrameHeight = 720; -constexpr uint32_t kBaselineSubsetSplatCount = 65536u; +constexpr uint32_t kBaselineSubsetSplatCount = 262144u; constexpr const char* kSubsetGaussianSplatAssetPath = "Assets/room_subset.xcgsplat"; constexpr float kTargetSceneExtent = 4.0f; -constexpr float kGaussianPointScale = 3.00f; +constexpr float kGaussianPointScale = 1.00f; +const Vector3 kDefaultCameraPosition(0.0f, 1.0f, 1.0f); +const Vector3 kDefaultCameraLookAt(0.0f, 1.0f, 0.0f); +const Vector3 kDefaultRootPosition = Vector3::Zero(); + +enum class GaussianSplatDebugView : uint8_t { + Scene = 0, + Alpha = 1 +}; XCEngine::Core::uint16 FloatToHalfBits(float value) { uint32_t bits = 0u; @@ -128,15 +142,178 @@ void LinkOrCopyFixture(const std::filesystem::path& sourcePath, const std::files ASSERT_FALSE(ec) << ec.message(); } -const char* GetScreenshotFilename(RHIType backendType) { +GaussianSplatDebugView GetDebugViewFromEnvironment() { + const char* debugViewValue = std::getenv("XCENGINE_GAUSSIAN_SPLAT_DEBUG_VIEW"); + if (debugViewValue == nullptr) { + return GaussianSplatDebugView::Scene; + } + + if (_stricmp(debugViewValue, "alpha") == 0 || + std::strcmp(debugViewValue, "1") == 0) { + return GaussianSplatDebugView::Alpha; + } + + return GaussianSplatDebugView::Scene; +} + +float GetFloatFromEnvironment(const char* name, float defaultValue) { + const char* value = std::getenv(name); + if (value == nullptr || value[0] == '\0') { + return defaultValue; + } + + char* end = nullptr; + const float parsedValue = std::strtof(value, &end); + return end != value ? parsedValue : defaultValue; +} + +uint32_t GetUIntFromEnvironment(const char* name, uint32_t defaultValue) { + const char* value = std::getenv(name); + if (value == nullptr || value[0] == '\0') { + return defaultValue; + } + + char* end = nullptr; + const unsigned long parsedValue = std::strtoul(value, &end, 10); + return end != value ? static_cast(parsedValue) : defaultValue; +} + +Vector3 GetVector3FromEnvironment(const char* name, const Vector3& defaultValue) { + const char* value = std::getenv(name); + if (value == nullptr || value[0] == '\0') { + return defaultValue; + } + + float x = 0.0f; + float y = 0.0f; + float z = 0.0f; + if (std::sscanf(value, "%f,%f,%f", &x, &y, &z) == 3) { + return Vector3(x, y, z); + } + + return defaultValue; +} + +void ExpectVector3Near(const Vector3& actual, const Vector3& expected, float epsilon = 1.0e-6f) { + 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 = 1.0e-6f) { + 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 = 1.0e-6f) { + 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 ExpectGaussianSplatRoundTripMatches( + const GaussianSplat& source, + const GaussianSplat& loaded) { + ASSERT_EQ(source.GetSplatCount(), loaded.GetSplatCount()); + ASSERT_EQ(source.GetChunkCount(), loaded.GetChunkCount()); + ASSERT_EQ(source.GetSHOrder(), loaded.GetSHOrder()); + ExpectVector3Near(loaded.GetBounds().GetMin(), source.GetBounds().GetMin()); + ExpectVector3Near(loaded.GetBounds().GetMax(), source.GetBounds().GetMax()); + ASSERT_NE(source.GetPositionRecords(), nullptr); + ASSERT_NE(loaded.GetPositionRecords(), nullptr); + ASSERT_NE(source.GetOtherRecords(), nullptr); + ASSERT_NE(loaded.GetOtherRecords(), nullptr); + ASSERT_NE(source.GetColorRecords(), nullptr); + ASSERT_NE(loaded.GetColorRecords(), nullptr); + ASSERT_NE(source.GetSHRecords(), nullptr); + ASSERT_NE(loaded.GetSHRecords(), nullptr); + + const uint32_t sampleIndices[] = { + 0u, + source.GetSplatCount() / 2u, + source.GetSplatCount() - 1u + }; + for (uint32_t sampleIndex : sampleIndices) { + ExpectVector3Near( + loaded.GetPositionRecords()[sampleIndex].position, + source.GetPositionRecords()[sampleIndex].position); + ExpectQuaternionNear( + loaded.GetOtherRecords()[sampleIndex].rotation, + source.GetOtherRecords()[sampleIndex].rotation); + ExpectVector3Near( + loaded.GetOtherRecords()[sampleIndex].scale, + source.GetOtherRecords()[sampleIndex].scale); + ExpectVector4Near( + loaded.GetColorRecords()[sampleIndex].colorOpacity, + source.GetColorRecords()[sampleIndex].colorOpacity); + + for (uint32_t coefficientIndex = 0u; coefficientIndex < kGaussianSplatSHCoefficientCount; ++coefficientIndex) { + EXPECT_NEAR( + loaded.GetSHRecords()[sampleIndex].coefficients[coefficientIndex], + source.GetSHRecords()[sampleIndex].coefficients[coefficientIndex], + 1.0e-6f); + } + } + + const auto* sourceChunkSection = static_cast( + source.GetSectionData(GaussianSplatSectionType::Chunks)); + const auto* loadedChunkSection = static_cast( + loaded.GetSectionData(GaussianSplatSectionType::Chunks)); + ASSERT_NE(sourceChunkSection, nullptr); + ASSERT_NE(loadedChunkSection, nullptr); + if (source.GetChunkCount() > 0u) { + const uint32_t chunkIndices[] = { 0u, source.GetChunkCount() - 1u }; + for (uint32_t chunkIndex : chunkIndices) { + EXPECT_EQ(loadedChunkSection[chunkIndex].colR, sourceChunkSection[chunkIndex].colR); + EXPECT_EQ(loadedChunkSection[chunkIndex].colG, sourceChunkSection[chunkIndex].colG); + EXPECT_EQ(loadedChunkSection[chunkIndex].colB, sourceChunkSection[chunkIndex].colB); + EXPECT_EQ(loadedChunkSection[chunkIndex].colA, sourceChunkSection[chunkIndex].colA); + ExpectVector3Near( + Vector3( + loadedChunkSection[chunkIndex].posX.x, + loadedChunkSection[chunkIndex].posY.x, + loadedChunkSection[chunkIndex].posZ.x), + Vector3( + sourceChunkSection[chunkIndex].posX.x, + sourceChunkSection[chunkIndex].posY.x, + sourceChunkSection[chunkIndex].posZ.x)); + ExpectVector3Near( + Vector3( + loadedChunkSection[chunkIndex].posX.y, + loadedChunkSection[chunkIndex].posY.y, + loadedChunkSection[chunkIndex].posZ.y), + Vector3( + sourceChunkSection[chunkIndex].posX.y, + sourceChunkSection[chunkIndex].posY.y, + sourceChunkSection[chunkIndex].posZ.y)); + EXPECT_EQ(loadedChunkSection[chunkIndex].sclX, sourceChunkSection[chunkIndex].sclX); + EXPECT_EQ(loadedChunkSection[chunkIndex].sclY, sourceChunkSection[chunkIndex].sclY); + EXPECT_EQ(loadedChunkSection[chunkIndex].sclZ, sourceChunkSection[chunkIndex].sclZ); + EXPECT_EQ(loadedChunkSection[chunkIndex].shR, sourceChunkSection[chunkIndex].shR); + EXPECT_EQ(loadedChunkSection[chunkIndex].shG, sourceChunkSection[chunkIndex].shG); + EXPECT_EQ(loadedChunkSection[chunkIndex].shB, sourceChunkSection[chunkIndex].shB); + } + } +} + +const char* GetScreenshotFilename(RHIType backendType, GaussianSplatDebugView debugView) { switch (backendType) { case RHIType::D3D12: - return kD3D12Screenshot; + return debugView == GaussianSplatDebugView::Alpha + ? kD3D12AlphaDebugScreenshot + : kD3D12Screenshot; case RHIType::Vulkan: - return kVulkanScreenshot; + return debugView == GaussianSplatDebugView::Alpha + ? kVulkanAlphaDebugScreenshot + : kVulkanScreenshot; case RHIType::OpenGL: default: - return kOpenGLScreenshot; + return debugView == GaussianSplatDebugView::Alpha + ? kOpenGLAlphaDebugScreenshot + : kOpenGLScreenshot; } } @@ -180,6 +357,8 @@ GaussianSplat* CreateGaussianSplatSubset( std::vector subsetOther(subsetSplatCount); std::vector subsetColors(subsetSplatCount); std::vector subsetSh(shSection != nullptr && sh != nullptr ? subsetSplatCount : 0u); + Bounds subsetBounds; + bool hasSubsetBounds = false; for (uint32_t subsetIndex = 0u; subsetIndex < subsetSplatCount; ++subsetIndex) { const uint32_t sourceIndex = selectedIndices[subsetIndex]; subsetPositions[subsetIndex] = positions[sourceIndex]; @@ -188,6 +367,14 @@ GaussianSplat* CreateGaussianSplatSubset( if (!subsetSh.empty()) { subsetSh[subsetIndex] = sh[sourceIndex]; } + + const Vector3& position = subsetPositions[subsetIndex].position; + if (!hasSubsetBounds) { + subsetBounds.SetMinMax(position, position); + hasSubsetBounds = true; + } else { + subsetBounds.Encapsulate(position); + } } const uint32_t subsetChunkCount = @@ -296,7 +483,7 @@ GaussianSplat* CreateGaussianSplatSubset( GaussianSplatMetadata metadata = source.GetMetadata(); metadata.splatCount = subsetSplatCount; - metadata.bounds = source.GetBounds(); + metadata.bounds = subsetBounds; metadata.chunkCount = subsetChunkCount; metadata.cameraCount = 0u; metadata.chunkFormat = GaussianSplatSectionFormat::ChunkFloat32; @@ -336,10 +523,12 @@ private: ResourceHandle m_gaussianSplat; ResourceHandle m_subsetGaussianSplat; Material* m_material = nullptr; + GaussianSplatDebugView m_debugView = GaussianSplatDebugView::Scene; }; void GaussianSplatSceneTest::SetUp() { RHIIntegrationFixture::SetUp(); + m_debugView = GetDebugViewFromEnvironment(); PrepareRuntimeProject(); m_sceneRenderer = std::make_unique(); @@ -452,7 +641,7 @@ void GaussianSplatSceneTest::PrepareRuntimeProject() { std::unique_ptr subsetGaussianSplat( CreateGaussianSplatSubset( *m_gaussianSplat.Get(), - kBaselineSubsetSplatCount, + GetUIntFromEnvironment("XCENGINE_GAUSSIAN_SPLAT_SUBSET_COUNT", kBaselineSubsetSplatCount), kSubsetGaussianSplatAssetPath)); ASSERT_NE(subsetGaussianSplat, nullptr); ASSERT_TRUE(subsetGaussianSplat->IsValid()); @@ -474,6 +663,7 @@ void GaussianSplatSceneTest::PrepareRuntimeProject() { ASSERT_GT(m_subsetGaussianSplat->GetSplatCount(), 0u); ASSERT_EQ(m_subsetGaussianSplat->GetSHOrder(), 3u); ASSERT_NE(m_subsetGaussianSplat->FindSection(GaussianSplatSectionType::Chunks), nullptr); + ExpectGaussianSplatRoundTripMatches(*subsetGaussianSplat, *m_subsetGaussianSplat.Get()); database.Shutdown(); } @@ -490,8 +680,11 @@ void GaussianSplatSceneTest::BuildScene() { m_material->Initialize(params); m_material->SetShader(ResourceManager::Get().Load(GetBuiltinGaussianSplatShaderPath())); m_material->SetRenderQueue(MaterialRenderQueue::Transparent); - m_material->SetFloat("_PointScale", kGaussianPointScale); + m_material->SetFloat( + "_PointScale", + GetFloatFromEnvironment("XCENGINE_GAUSSIAN_SPLAT_POINT_SCALE", kGaussianPointScale)); m_material->SetFloat("_OpacityScale", 1.0f); + m_material->SetFloat("_DebugViewMode", m_debugView == GaussianSplatDebugView::Alpha ? 1.0f : 0.0f); GameObject* cameraObject = m_scene->CreateGameObject("MainCamera"); auto* camera = cameraObject->AddComponent(); @@ -512,10 +705,12 @@ void GaussianSplatSceneTest::BuildScene() { const float sizeY = std::max(boundsMax.y - boundsMin.y, 0.001f); const float sizeZ = std::max(boundsMax.z - boundsMin.z, 0.001f); const float maxExtent = std::max(sizeX, std::max(sizeY, sizeZ)); - const float uniformScale = kTargetSceneExtent / maxExtent; + const float uniformScale = + GetFloatFromEnvironment("XCENGINE_GAUSSIAN_SPLAT_TARGET_EXTENT", kTargetSceneExtent) / maxExtent; GameObject* root = m_scene->CreateGameObject("GaussianSplatRoot"); - root->GetTransform()->SetLocalPosition(Vector3::Zero()); + root->GetTransform()->SetLocalPosition( + GetVector3FromEnvironment("XCENGINE_GAUSSIAN_SPLAT_ROOT_POS", kDefaultRootPosition)); root->GetTransform()->SetLocalScale(Vector3(uniformScale, uniformScale, uniformScale)); GameObject* splatObject = m_scene->CreateGameObject("RoomGaussianSplat"); @@ -527,7 +722,11 @@ void GaussianSplatSceneTest::BuildScene() { splatRenderer->SetMaterial(m_material); splatRenderer->SetCastShadows(false); splatRenderer->SetReceiveShadows(false); - cameraObject->GetTransform()->SetLocalPosition(Vector3(0.0f, 1.0f, 1.0f)); + + cameraObject->GetTransform()->SetLocalPosition( + GetVector3FromEnvironment("XCENGINE_GAUSSIAN_SPLAT_CAMERA_POS", kDefaultCameraPosition)); + cameraObject->GetTransform()->LookAt( + GetVector3FromEnvironment("XCENGINE_GAUSSIAN_SPLAT_CAMERA_LOOK_AT", kDefaultCameraLookAt)); } RHIResourceView* GaussianSplatSceneTest::GetCurrentBackBufferView() { @@ -585,7 +784,8 @@ TEST_P(GaussianSplatSceneTest, RenderRoomGaussianSplatScene) { ASSERT_NE(swapChain, nullptr); constexpr int kTargetFrameCount = 2; - const char* screenshotFilename = GetScreenshotFilename(GetBackendType()); + const GaussianSplatDebugView debugView = GetDebugViewFromEnvironment(); + const char* screenshotFilename = GetScreenshotFilename(GetBackendType(), debugView); for (int frameCount = 0; frameCount <= kTargetFrameCount; ++frameCount) { if (frameCount > 0) { @@ -599,6 +799,11 @@ TEST_P(GaussianSplatSceneTest, RenderRoomGaussianSplatScene) { commandQueue->WaitForIdle(); ASSERT_TRUE(TakeScreenshot(screenshotFilename)); + if (debugView != GaussianSplatDebugView::Scene) { + SUCCEED() << "Debug view screenshot captured for inspection: " << screenshotFilename; + break; + } + const std::filesystem::path gtPath = ResolveRuntimePath("GT.ppm"); if (!std::filesystem::exists(gtPath)) { GTEST_SKIP() << "GT.ppm missing, screenshot captured for baseline generation: " << screenshotFilename; diff --git a/tests/core/Asset/test_resource_manager.cpp b/tests/core/Asset/test_resource_manager.cpp index 8e5b7287..c8348a0f 100644 --- a/tests/core/Asset/test_resource_manager.cpp +++ b/tests/core/Asset/test_resource_manager.cpp @@ -15,6 +15,7 @@ #include #include #include +#include #include #include @@ -109,6 +110,19 @@ bool PumpAsyncLoadsUntil(ResourceManager& manager, return condition(); } +std::filesystem::path GetRepositoryRoot() { + std::filesystem::path current = std::filesystem::path(__FILE__).parent_path(); + while (!current.empty()) { + if (std::filesystem::exists(current / "project") && + std::filesystem::exists(current / "engine")) { + return current; + } + current = current.parent_path(); + } + + return std::filesystem::path(__FILE__).parent_path(); +} + bool DirectoryHasEntries(const std::filesystem::path& directoryPath) { std::error_code ec; if (!std::filesystem::exists(directoryPath, ec) || !std::filesystem::is_directory(directoryPath, ec)) { @@ -118,6 +132,38 @@ bool DirectoryHasEntries(const std::filesystem::path& directoryPath) { return std::filesystem::directory_iterator(directoryPath) != std::filesystem::directory_iterator(); } +std::vector SplitTabSeparatedLine(const std::string& line) { + std::vector fields; + std::stringstream stream(line); + std::string field; + while (std::getline(stream, field, '\t')) { + fields.push_back(field); + } + return fields; +} + +std::string ReadSourceHashFromAssetsDb(const std::filesystem::path& assetsDbPath, + const std::string& relativePath) { + std::ifstream input(assetsDbPath, std::ios::binary); + if (!input.is_open()) { + return std::string(); + } + + std::string line; + while (std::getline(input, line)) { + if (line.empty() || line[0] == '#') { + continue; + } + + const std::vector fields = SplitTabSeparatedLine(line); + if (fields.size() > 7 && fields[1] == relativePath) { + return fields[7]; + } + } + + return std::string(); +} + std::vector ListArtifactEntries(const std::filesystem::path& artifactsRoot) { namespace fs = std::filesystem; @@ -424,6 +470,47 @@ TEST(AssetImportService_Test, EnsureArtifactExposesContainerEntryRuntimeLoadPath fs::remove_all(projectRoot); } +TEST(AssetImportService_Test, BootstrapProjectDefersSourceHashUntilArtifactIsNeeded) { + namespace fs = std::filesystem; + + AssetImportService importService; + importService.Initialize(); + + const fs::path projectRoot = fs::temp_directory_path() / "xc_asset_import_service_deferred_source_hash_test"; + const fs::path assetsDir = projectRoot / "Assets"; + const fs::path materialPath = assetsDir / "runtime.material"; + + fs::remove_all(projectRoot); + fs::create_directories(assetsDir); + { + std::ofstream materialFile(materialPath); + ASSERT_TRUE(materialFile.is_open()); + materialFile << "{\n"; + materialFile << " \"renderQueue\": \"geometry\"\n"; + materialFile << "}\n"; + } + + importService.SetProjectRoot(projectRoot.string().c_str()); + ASSERT_TRUE(importService.BootstrapProject()); + + const fs::path libraryRoot(importService.GetLibraryRoot().CStr()); + const fs::path assetsDbPath = libraryRoot / "assets.db"; + ASSERT_TRUE(fs::exists(assetsDbPath)); + EXPECT_FALSE(DirectoryHasEntries(libraryRoot / "Artifacts")); + EXPECT_TRUE(ReadSourceHashFromAssetsDb(assetsDbPath, "Assets/runtime.material").empty()); + + AssetImportService::ImportedAsset importedAsset; + ASSERT_TRUE(importService.EnsureArtifact("Assets/runtime.material", ResourceType::Material, importedAsset)); + EXPECT_TRUE(importedAsset.exists); + EXPECT_TRUE(importedAsset.artifactReady); + EXPECT_TRUE(importedAsset.imported); + EXPECT_FALSE(ReadSourceHashFromAssetsDb(assetsDbPath, "Assets/runtime.material").empty()); + EXPECT_TRUE(DirectoryHasEntries(libraryRoot / "Artifacts")); + + importService.Shutdown(); + fs::remove_all(projectRoot); +} + TEST(ResourceManager_Test, RebuildProjectAssetCacheRefreshesLookupState) { namespace fs = std::filesystem; @@ -545,6 +632,58 @@ TEST(ResourceManager_Test, SetResourceRootBootstrapsProjectAssetCache) { fs::remove_all(projectRoot); } +TEST(ResourceManager_ProjectSample, BootstrapProjectKeepsCloudSourceHashDeferred) { + namespace fs = std::filesystem; + + const fs::path repositoryRoot = GetRepositoryRoot(); + const fs::path projectRoot = repositoryRoot / "project"; + const fs::path volumePath = projectRoot / "Assets" / "cloud.nvdb"; + + if (!fs::exists(volumePath)) { + GTEST_SKIP() << "Project cloud volume fixture is not available."; + } + + ResourceManager& manager = ResourceManager::Get(); + manager.Initialize(); + + struct ResourceManagerGuard { + ResourceManager* manager = nullptr; + ~ResourceManagerGuard() { + if (manager != nullptr) { + manager->Shutdown(); + } + } + } resourceManagerGuard{ &manager }; + + struct CurrentPathGuard { + fs::path previousPath; + ~CurrentPathGuard() { + if (!previousPath.empty()) { + fs::current_path(previousPath); + } + } + } currentPathGuard{ fs::current_path() }; + + fs::current_path(projectRoot); + manager.SetResourceRoot(projectRoot.string().c_str()); + + AssetRef volumeRef; + EXPECT_TRUE(manager.TryGetAssetRef("Assets/cloud.nvdb", ResourceType::VolumeField, volumeRef)); + EXPECT_TRUE(volumeRef.IsValid()); + + const AssetImportService::ImportStatusSnapshot status = manager.GetProjectAssetImportStatus(); + EXPECT_TRUE(status.HasValue()); + EXPECT_FALSE(status.inProgress); + EXPECT_TRUE(status.success); + EXPECT_EQ(std::string(status.operation.CStr()), "Bootstrap Project"); + + const fs::path assetsDbPath = projectRoot / "Library" / "assets.db"; + ASSERT_TRUE(fs::exists(assetsDbPath)); + EXPECT_TRUE(ReadSourceHashFromAssetsDb(assetsDbPath, "Assets/cloud.nvdb").empty()); + + manager.SetResourceRoot(""); +} + TEST(AssetImportService_Test, ClearLibraryAndReimportAllAssetsManageArtifactsExplicitly) { namespace fs = std::filesystem; diff --git a/tests/editor/nahida_preview_regenerator.cpp b/tests/editor/nahida_preview_regenerator.cpp index 05c8a80d..562343d6 100644 --- a/tests/editor/nahida_preview_regenerator.cpp +++ b/tests/editor/nahida_preview_regenerator.cpp @@ -39,7 +39,7 @@ std::shared_ptr MakeModelAssetItem( void ConfigurePreviewCamera(XCEngine::Components::GameObject& gameObject) { using namespace XCEngine; - gameObject.GetTransform()->SetLocalPosition(Math::Vector3(0.0f, 1.2f, -4.25f)); + gameObject.GetTransform()->SetLocalPosition(Math::Vector3(0.0f, 1.3f, -2.75f)); gameObject.GetTransform()->SetLocalRotation(Math::Quaternion(0.104528f, 0.0f, 0.0f, 0.994522f)); auto* camera = gameObject.AddComponent();