chore: checkpoint current workspace changes

This commit is contained in:
2026-04-11 22:14:02 +08:00
parent 3e55f8c204
commit 8848cfd958
227 changed files with 34027 additions and 6711 deletions

View File

@@ -670,4 +670,4 @@ ID3D12Resource* CreateTexture2D(ID3D12GraphicsCommandList* inCommandList,
}
ID3D12Device* GetD3DDevice() {
return gD3D12Device;
}
}

View File

@@ -305,4 +305,4 @@ int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPWSTR lpCmdLi
}
}
return 0;
}
}

251
README.md
View File

@@ -1,6 +1,6 @@
# XCEngine
`XCEngine` 是一个基于 `C++20` 游戏引擎。当前代码中已经包含 `RHI``Rendering``Scene``Resources``Editor``Mono C# Scripting``XCUI` 相关模块;本 README 只描述当前已经实现已经接入的能力。
`XCEngine` 是一个 Windows-first、editor-first 的 `C++20` 游戏引擎工作区。当前代码中已经形成 `RHI``Rendering``Scene``Resources``Editor``Mono C# Scripting``XCUI` 相关主线;本 README 只描述当前已经实现已经接入或已经成为正式工作流一部分的能力。
## 核心特性
@@ -91,7 +91,8 @@
- `UIScreenDocumentHost`
- `UIScreenPlayer`
- `UISystem`
- 新编辑器路线基于 `XCUIEditorLib` 推进当前已经包含树视图、列表视图、菜单、标签条、属性网格、字段控件、workspace / dock / viewport shell 等基础组件
- `tests/UI/` 是当前 XCUI `Core / Editor / Runtime` 三层的正式基础层验证入口
- `new_editor/` 是未来正式编辑器主线不再只是临时沙盒当前已经包含树视图、列表视图、菜单、标签条、属性网格、字段控件、workspace / dock / viewport shell 等基础组件
### 音频
@@ -122,7 +123,7 @@
- 当前 `RHI` 后端为 `D3D12``OpenGL``Vulkan`
- 当前项目资产工作流已经接入 `AssetDatabase + Artifact + Library`
- 当前 `Mono C#` 脚本程序集与运行时链路已经接入
- 当前 `XCUI` 新编辑器路线仍在继续实现中
- 当前 `XCUI` 新编辑器路线`new_editor/` 为正式主线继续推进
## 快速开始
@@ -170,6 +171,12 @@ cmake --build build --config Debug --target xcengine_project_managed_assemblies
cmake --build build --config Debug --target XCUIEditorApp
```
运行 XCUI 新编辑器宿主:
```powershell
.\new_editor\bin\Debug\XCUIEditor.exe
```
## 测试入口
```powershell
@@ -189,10 +196,248 @@ cmake --build build --config Debug --target editor_ui_all_tests
cmake --build build --config Debug --target runtime_ui_all_tests
```
## 完整目录结构
以下目录树按当前工作树整理,保留了真实使用的生成目录与关键子树;省略了 `.git/``build/_deps/`、部分重复资源文件,以及 `docs/used/` 中大量历史归档的长尾条目。
```text
XCEngine/
|- .gitattributes
|- .gitignore
|- AGENT.md
|- CMakeLists.txt
|- README.md
|- build/ # 本地 CMake 构建输出
|- docs/
| |- api/
| | |- XCEngine/
| | |- _guides/
| | |- _meta/
| | |- _tools/
| | `- main.md
| |- issues/
| | `- Editor模块_Console面板错误绑定fallback sink导致运行时日志不显示4.3.md
| |- plan/
| | |- end/
| | | |- RHI模块设计与实现/
| | | | |- RHIFence.md
| | | | `- RHI模块总览.md
| | | `- 编辑器与运行时分层架构设计.md
| | |- 开题报告和任务书/
| | |- 旧版题目/
| | |- 3DGS专用PLY导入器与GaussianSplat资源缓存正式化计划_2026-04-10.md
| | |- API文档目录结构第二轮并行任务板_2026-04-09.md
| | |- API文档目录结构第二轮重构计划_2026-04-09.md
| | |- API文档目录结构重大重构并行任务板_2026-04-09.md
| | |- API文档目录结构重构并行任务板_2026-04-09_第二轮.md
| | |- API文档目录重构计划_2026-04-09.md
| | |- C#脚本模块下一阶段计划.md
| | |- Editor架构说明.md
| | |- Library启动预热与运行时异步加载混合重构计划_2026-04-04.md
| | |- Library启动预热与运行时异步加载混合重构计划_进度更新_2026-04-04.md
| | |- NanoVDB体积云加载阻塞与Runtime上传修复计划_2026-04-10.md
| | |- Unity SRP API参考文档.md
| | |- Unity绝区零开发文档还原版.md
| | |- Unity风格模型导入与Model资产架构重构计划_2026-04-10.md
| | |- XCUI_NewEditor主线重建计划_2026-04-07.md
| | `- XCUI完整架构设计与执行计划.md
| |- used/ # 历史材料、阶段归档和旧计划背景
| | |- API文档实时同步任务池_2026-04-03.md
| | |- Library资产导入与缓存系统收口计划_完成归档_2026-04-03.md
| | |- NanoVDB稀疏体积渲染后续正式化计划_阶段归档_2026-04-10.md
| | |- NanoVDB稀疏体积渲染正式集成计划_第一阶段完成归档_2026-04-09.md
| | |- Renderer当前阶段正式收口计划_阶段归档_2026-04-10.md
| | |- Renderer剩余收口与体积渲染多后端正式化计划_完成归档_2026-04-10.md
| | |- Renderer下一阶段_Unity风格Shader体系正式化计划_完成归档_2026-04-07.md
| | |- SceneViewport_Overlay_Gizmo_Rework_Plan_完成归档_2026-04-04.md
| | |- Unity式SceneView_Gizmo系统完整审查与正式化重构方案_完成归档_2026-04-06.md
| | `- XCUI_Phase_Status_2026-04-05.md
| |- api-skill.md
| |- blueprint-skill.md
| `- blueprint.md
|- editor/
| |- CMakeLists.txt
| |- README.md
| |- bin/
| |- resources/
| | `- Icons/
| `- src/
| |- Actions/
| |- Commands/
| |- ComponentEditors/
| |- Core/
| |- Layers/
| |- Layout/
| |- Managers/
| |- panels/
| |- Platform/
| |- Scripting/
| |- UI/
| |- Utils/
| |- Viewport/
| | |- Passes/
| | |- SceneViewportChrome.*
| | |- SceneViewportInteractionFrame.h
| | |- SceneViewportNavigation.h
| | |- SceneViewportOverlayBuilder.*
| | |- SceneViewportOverlayFrameCache.*
| | |- SceneViewportOverlaySpriteResources.*
| | |- SceneViewportPassSpecs.h
| | |- SceneViewportPicker.*
| | |- SceneViewportResourcePaths.h
| | |- SceneViewportTransformGizmoCoordinator.*
| | `- ViewportHostService.h
| |- Application.cpp
| |- Application.h
| |- Theme.cpp
| `- main.cpp
|- engine/
| |- CMakeLists.txt
| |- include/
| | `- XCEngine/
| | |- Audio/
| | |- Components/
| | |- Core/
| | |- Debug/
| | |- Input/
| | |- Memory/
| | |- Platform/
| | |- Rendering/
| | | |- Caches/
| | | |- Execution/
| | | |- Extraction/
| | | |- FrameData/
| | | |- Materials/
| | | |- Passes/
| | | |- Picking/
| | | |- Pipelines/
| | | `- Planning/
| | |- Resources/
| | | |- AudioClip/
| | | |- Material/
| | | |- Mesh/
| | | |- Shader/
| | | |- Texture/
| | | `- Volume/
| | |- RHI/
| | | |- D3D12/
| | | |- OpenGL/
| | | `- Vulkan/
| | |- Scene/
| | |- Scripting/
| | |- Threading/
| | `- UI/
| |- src/
| |- third_party/
| `- tools/
|- managed/
| |- CMakeLists.txt
| |- GameScripts/
| `- XCEngine.ScriptCore/
|- mvs/
| |- 3DGS-Unity/
| |- D3D12/
| |- OpenGL/
| |- RenderDoc/
| |- ui/
| `- VolumeRenderer/
|- new_editor/
| |- app/
| | |- Host/
| | |- Application.cpp
| | |- Application.h
| | `- main.cpp
| |- bin/
| |- include/
| | `- XCEditor/
| | |- Collections/
| | |- Fields/
| | |- Foundation/
| | |- Shell/
| | `- Widgets/
| |- src/
| | |- Collections/
| | |- Fields/
| | |- Foundation/
| | |- Shell/
| | `- Widgets/
| |- ui/
| | |- themes/
| | `- views/
| `- CMakeLists.txt
|- project/
| |- .xceditor/
| | |- imgui_layout.ini
| | `- thumbs/
| |- Assets/
| | |- Materials/
| | |- Models/
| | |- Scenes/
| | `- Scripts/
| |- Library/
| | |- ArtifactDB/
| | |- Artifacts/
| | |- ScriptAssemblies/
| | `- SourceAssetDB/
| |- Assets.meta
| `- Project.xcproject
|- scripts/
| `- Run-RendererPhaseRegression.ps1
|- tests/
| |- CMakeLists.txt
| |- TEST_SPEC.md
| |- Components/
| |- Core/
| |- Debug/
| |- Editor/
| |- Fixtures/
| |- Input/
| |- Memory/
| |- NewEditor/ # 当前为空的预留测试根目录
| |- Rendering/
| | |- integration/
| | | |- alpha_cutout_scene/
| | | |- camera_post_process_scene/
| | | |- directional_shadow_scene/
| | | |- final_color_scene/
| | | |- multi_light_scene/
| | | |- skybox_scene/
| | | |- volume_occlusion_scene/
| | | |- volume_scene/
| | | `- volume_transform_scene/
| | `- unit/
| |- Resources/
| |- RHI/
| |- Scene/
| |- Scripting/
| |- Threading/
| `- UI/
| |- Core/
| |- Editor/
| |- Runtime/
| `- TEST_SPEC.md
|- 参考/
| |- Fermion/
| |- TransformGizmo/
| |- unity editor/
| |- unity-editor-icons/
| |- unity-icons/
| `- UnityRuntimeSceneGizmo-master/
`- .vscode/
```
## 文档入口
- `docs/api/main.md`
- `editor/README.md`
- `AGENT.md`
- `docs/blueprint.md`
- `tests/TEST_SPEC.md`
- `tests/UI/TEST_SPEC.md`
- `docs/plan/Editor架构说明.md`
- `docs/plan/Library启动预热与运行时异步加载混合重构计划_2026-04-04.md`
- `docs/plan/XCUI_NewEditor主线重建计划_2026-04-07.md`
- `docs/plan/XCUI完整架构设计与执行计划.md`
- `docs/plan/NanoVDB体积云加载阻塞与Runtime上传修复计划_2026-04-10.md`
- `docs/plan/Unity风格模型导入与Model资产架构重构计划_2026-04-10.md`
- `docs/plan/3DGS专用PLY导入器与GaussianSplat资源缓存正式化计划_2026-04-10.md`

View File

@@ -1,13 +1,13 @@
# API 文档重构状态
**生成时间**: `2026-04-10 17:58:41`
**生成时间**: `2026-04-10 18:41:38`
**来源**: `docs/api/_tools/audit_api_docs.py`
## 摘要
- Markdown 页面数(全部): `3973`
- Markdown 页面数canonical: `3945`
- Markdown 页面数(全部): `4006`
- Markdown 页面数canonical: `3978`
- Public headers 数: `384`
- `XCEditor` public headers 数: `64`canonical 已覆盖 `64`
- `XCEngine` public headers 数: `320`canonical 已覆盖 `320`
@@ -83,8 +83,8 @@
| 字段 | 页面数 |
|------|--------|
| `命名空间` | `2567` |
| `类型` | `2567` |
| `描述` | `617` |
| `头文件` | `1972` |
| `命名空间` | `2570` |
| `类型` | `2570` |
| `描述` | `618` |
| `头文件` | `1975` |
| `源文件` | `520` |

View File

@@ -0,0 +1,248 @@
# 3DGS 渲染路径对齐参考实现修复计划
日期2026-04-11
## 1. 文档定位
旧计划《3DGS渲染集成测试与Renderer正式接入计划》已经完成了主体接入工作
1. `GaussianSplat` 资源链路已经打通
2. `GaussianSplatRendererComponent` / `VisibleGaussianSplatItem` / `BuiltinGaussianSplatPass` 已经落地
3. `tests/Rendering/integration/gaussian_splat_scene` 已经建立
当前剩下的不是“是否接进 Renderer”而是“当前 Renderer 中的 3DGS 正式路径与参考实现仍有关键偏差,导致画面结果错误”。
因此这份新计划只聚焦当前的渲染正确性修复与测试收口,不再重复旧计划里已经完成的接入事项。
## 2. 已确认的根因
这轮问题已经不是相机摆放、PLY 导入或简单参数调节问题,而是渲染路径本身与参考实现不一致。
### 2.1 当前引擎缺少正式的 accumulation + composite 两段式路径
参考实现的关键流程是:
1. 先把 splat 绘制到单独的半浮点累积 RT
2. splat draw 使用前向累积专用 blend
3. 最后再把累积 RT composite 回主场景颜色缓冲
参考位置:
1. `mvs/3DGS-Unity/Shaders/RenderGaussianSplats.shader`
2. `mvs/3DGS-Unity/Shaders/GaussianComposite.shader`
3. `mvs/3DGS-Unity/Runtime/GaussianSplatRenderer.cs`
当前引擎却是:
1.`engine/assets/builtin/shaders/gaussian-splat.shader` 中直接输出到主场景颜色
2. `BuiltinGaussianSplatPass` 直接把 camera color attachment 作为 render target
3. 虽然 `BuiltinGaussianSplatPassResources` 已经有 `AccumulationSurface` 抽象,但执行路径没有真正使用它
这说明当前实现是“资源抽象已经开始正式化,但执行流仍停在临时路径上”,这是第一根因。
### 2.2 当前排序方向与当前 blend 方程不匹配
当前引擎:
1. `PrepareOrder` 阶段使用相机空间 `viewCenter.z` 作为排序距离
2. bitonic 结果本质上是按升序排列
3. draw shader 却使用普通透明混合 `Blend SrcAlpha OneMinusSrcAlpha`
如果保留当前普通透明混合,就应当使用严格的 back-to-front 语义;
如果要对齐参考实现的 front-to-back 累积,就必须同时切到参考的累积 blend 与独立 accumulation target。
也就是说,当前并不是“排序可能有一点不准”,而是“排序约定与 blend 合约本身冲突”。
### 2.3 当前测试参数只是在放大问题,不是问题本体
当前集成测试里:
1. 只取了 `65536` 个 splat 子集
2. `_PointScale = 3.0`
3. 最终输出直接落在主 backbuffer
这些会让错误更明显,但不会单独制造当前这种大面积拖影、糊片、黑色尖刺。
真正的问题仍然是 draw/composite 路径设计错误。
## 3. 本轮修复目标
本轮只做一件事:把当前引擎中的 3DGS 渲染主链,彻底对齐到参考实现所依赖的那组最小正确语义。
具体目标:
1. 建立正式的 accumulation render target
2. draw shader 改为服务 accumulation 的输出与 blend 合约
3. 新增正式的 composite pass把 accumulation 结果混回主场景
4. 统一排序方向与 blend 语义,不允许继续“排序一套、混合一套”
5. 保证 `gaussian_splat_scene` 在 D3D12 / Vulkan / OpenGL 三后端下都能稳定回归
## 4. 非目标
这轮明确不做:
1. 不接 editor 中的 3DGS 显示与交互
2. 不做 selection / cutout / 编辑工具链
3. 不做 3DGS 的 streaming / LOD / chunk 级高级优化
4. 不在这轮重做 `GaussianSplat` artifact schema
5. 不引入 compute shader 之外的新渲染架构分支
## 5. 执行原则
### 5.1 先对齐正确性,再谈进一步优化
这轮优先级必须是:
1. 先把“渲染方程”和“执行路径”对齐
2. 先让集成测试输出正确
3. 再考虑性能、压缩、子集规模、排序频率等问题
### 5.2 不允许继续保留半套正式化、半套临时方案
一旦正式启用 accumulation/composite就必须
1. 把 draw shader、pass 资源、pass 执行流一起接完整
2. 把当前直接画到 scene color 的临时路径清掉
3. 不保留两个语义不同但名字相同的渲染路线
### 5.3 修复必须由测试驱动收口
本轮所有核心改动都必须由以下验证闭环约束:
1. `gaussian_splat_scene` 的三后端输出
2. 中间调试图可视化检查
3. 现有 `GaussianSplat` 资源与缓存相关测试不回退
## 6. 分阶段计划
### Phase 1补齐 3DGS 中间结果观测面
目标:
1. 在不改渲染语义之前,先把中间状态可视化出来
任务:
1. 允许 `gaussian_splat_scene` 在调试模式下输出 accumulation RT
2. 允许单独检查 sort/order 与 view-data 是否为空或异常
3. 固化一套最小调试截图流程,避免后续继续靠猜
验收标准:
1. 可以直接看到 accumulation RT 的内容
2. 可以区分“prepare 阶段错误”和“draw/composite 阶段错误”
### Phase 2把 accumulation surface 正式接入 `BuiltinGaussianSplatPass`
目标:
1.`BuiltinGaussianSplatPassResources::AccumulationSurface` 从未使用状态变成正式执行资源
任务:
1. 在 pass 执行前按 viewport 尺寸创建或复用 accumulation RT
2.`alpha = 0` 清空 accumulation RT
3. draw 阶段只向 accumulation RT 输出,不再直接写主场景颜色
4. 明确 accumulation RT 的资源状态流转
验收标准:
1. draw pass 不再直接绑定主场景颜色附件
2. accumulation RT 生命周期与 working set 生命周期边界清晰
### Phase 3对齐 draw shader 的输出语义与 blend 合约
目标:
1. 让 splat draw 输出和参考实现使用同一套前向累积语义
任务:
1. 对齐 draw shader 输出格式
2. 对齐 draw shader blend 状态
3. 确认当前相机空间 `z` 约定下,排序方向与累积方向一致
4. 不允许通过后端分支打补丁解决语义冲突
验收标准:
1. draw shader 的颜色输出、alpha 输出、blend 方程是自洽的
2. `PrepareOrder -> Sort -> Draw` 是同一套排序约定
### Phase 4新增正式的 composite pass
目标:
1. 把 accumulation RT 稳定地合成回主场景颜色缓冲
任务:
1. 新增 builtin composite shader
2.`BuiltinGaussianSplatPass` 内建立 draw + compose 的完整顺序
3. 明确主场景颜色、深度与 accumulation RT 的交互边界
4. 不通过测试侧手写 command list 绕过 renderer
验收标准:
1. 最终颜色由 composite pass 统一输出
2. 3DGS pass 对主场景颜色缓冲的写入语义单一清晰
### Phase 5回归基线与参数收口
目标:
1. 在正确渲染路径跑通后,再校正当前测试参数
任务:
1. 重新评估当前子集数量是否足够形成稳定 GT
2. 重新评估 `_PointScale` 的默认值
3. 必要时重建 `GT.ppm`
4. 三后端重新运行并固定基线
验收标准:
1. `gaussian_splat_scene` 的 GT 是基于正式渲染路径得到的
2. 不再把“错误输出截图”继续当成 GT
## 7. 风险与提前约束
### 7.1 不能把参考实现照搬成 Unity 特有依赖
本轮只吸收以下内容:
1. 累积 RT + composite 的渲染语义
2. 排序与 blend 的契约关系
3. 中间资源与 draw/composite 的分层方式
不吸收:
1. Unity 特有 `CommandBuffer` 接口设计
2. Unity 运行时生命周期组织方式
3. Unity 专有材质属性命名与 editor 逻辑
### 7.2 不能把 accumulation/composite 做成测试专用旁路
这条路径必须属于正式 renderer而不是
1. 只在 `gaussian_splat_scene` 里额外手写一套
2. 只在 D3D12 下工作
3. 通过测试宏临时切换
### 7.3 不允许继续保留错误 GT
如果当前 `GT.ppm` 来自错误渲染路径,就必须重建。
否则后续再怎么修,测试也会被旧基线误导。
## 8. 完成标志
当以下条件同时成立,这份计划才算完成:
1. `BuiltinGaussianSplatPass` 正式使用 accumulation surface
2. draw shader 与 sort 语义彻底自洽
3. composite pass 正式接回主场景颜色
4. `gaussian_splat_scene` 在 D3D12 / Vulkan / OpenGL 下都能稳定输出正确图像
5. 调试图证明 accumulation RT 本身干净且可解释
6. 当前 3DGS 渲染路径中不再残留“直接写 scene color 的临时旧路线”
## 9. 一句话结论
当前 3DGS 的问题不是“参数没调对”,而是“渲染执行链条还停在错误的临时语义上”。
下一步必须把 `Prepare/Sort -> Accumulation Draw -> Composite` 这条正式路径一次接完整,再重新建立三后端的 GT 基线。

View File

@@ -2,42 +2,42 @@
## 1.1 课题背景
随着实时渲染技术的发展,渲染引擎所面对的应用场景已经逐步扩展游戏、虚拟现实、数字内容生产和交互式可视化等多个方向,逐渐演变为集图形接口抽象、资源导入与管理、场景组织、材质与光照、脚本运行时以及编辑器工作流于一体的综合软件系统。围绕渲染引擎整体架构开展设计与实现,更便于将运行时系统、工具链以及后续渲染扩展放在统一平台中加以组织。
随着实时渲染技术的发展,渲染引擎的应用场景已经逐步扩展游戏、虚拟现实、数字内容生产和交互式可视化等多个方向,逐渐演变为集图形硬件接口、资源导入与管理、场景组织、材质与光照、脚本运行时以及编辑器工作流于一体的综合软件系统。围绕渲染引擎整体架构开展设计与实现,更便于将运行时系统、工具链以及后续渲染扩展放在统一平台中加以组织。
在现代实时渲染中,云、雾、烟、火等体积特效已经成为常见重要的视觉元素。与基于表面的传统渲染不同,体积渲染需要处理光线在参与介质中的吸收、散射和透射率累积过程,通常伴随着大量采样和较高的计算开销。特别是在实时应用环境下,如何在有限的帧长兼顾体积效的空间层次感、光照表现和运行效率是极具价值的技术问题。也正因如此,体积渲染既具有较强的理论背景,也具有较高的工程实现价值。
在现代实时渲染中,云、雾、烟、火等体积特效已经成为常见重要的视觉元素。与基于表面的传统渲染不同,体积渲染需要处理光线在参与介质中的吸收、散射和透射率累积过程,通常伴随着大量采样和较高的计算开销。特别是在实时应用场景下,如何在有限的帧长兼顾体积效的空间层次感、光照表现和运行效率是极具价值的技术问题。也正因如此,体积渲染既具有较强的理论背景,也具有较高的工程实现价值。
在体数据表达方面OpenVDB 为稀疏体积数据组织提供了成熟思路,而 NanoVDB 通过线性化数据结构进一步提升了 GPU 访问友好性使其更适合实时渲染场景。与此同时DirectX 12 提供了较底层的资源、命令和同步控制能力,便于开发者更直接地组织 GPU 数据上传、状态切换与渲染调度流程。将 NanoVDB 稀疏体数据组织方式与 DirectX 12 图形接口结合起来,不仅适合开展体积特效的实现研究,也能够作为扩展渲染引擎高级渲染能力的一条现实技术路径。
在体数据表达方面OpenVDB 为稀疏体积数据组织提供了成熟思路,而 NanoVDB 通过线性化数据结构进一步提升了 GPU 访问友好性使其更适合实时渲染场景。与此同时DirectX 12 提供了较底层的渲染资源、命令和同步控制能力,便于开发者更直接地组织 GPU 数据上传、状态切换与渲染调度流程。将 NanoVDB 稀疏体数据组织方式与 DirectX 12 图形接口结合起来,不仅适合开展体积渲染的理论研究,也能够作为扩展渲染引擎高级渲染特性的技术路径。
## 1.2 课题意义
从体积渲染理论与实现的角度看,本课题围绕 NanoVDB 稀疏体数据在 DirectX 12 环境下的实时渲染展开重点关注体数据加载、GPU 访问、光线步进、空域跳过和体积阴影等关键问题。相关工作的完成,有助于为云、雾、烟等体积特效实时工程实现提供一条较清晰的技术路径,也有助于加深对参与介质渲染、体绘制方程简化以及性能优化方法的理解。
从体积渲染理论与实现的角度看,本课题围绕 NanoVDB 稀疏体数据在 DirectX 12 环境下的实时渲染展开重点关注体数据加载、GPU 访问、光线步进、空域跳过和体积阴影等关键问题。相关工作的完成,有助于为云、雾、烟等体积特效实时渲染的工程实现提供一条较清晰的技术路径,也有助于加深对参与介质渲染、体渲染方程数值化以及性能优化方法的理解。
从系统设计与工程实践的角度看,本课题在体积渲染实现之外,还涵盖了渲染引擎主体架构、运行时模块和编辑器工作流的设计与实现。当前项目已经形成了包含 RHI 抽象、资源系统、场景与组件系统、渲染主链、材质与光照、C# 脚本系统以及编辑器工具链在内的主体框架,体积渲染是在这一基础上的重要高级扩展。这体现了渲染引擎开发中的模块协同关系,同时满足了系统性、完整性和可扩展性的要求。
从系统设计与工程实践的角度看,本课题在体积渲染实现之外,还涵盖了渲染引擎主体架构、运行时模块和编辑器工作流的设计与实现。当前项目已经形成了包含 RHI 抽象、资源缓存系统、场景与组件系统、渲染主链、材质与光照、C# 脚本系统以及编辑器工具链在内的主体框架,体积渲染是在这一基础上的重要高级扩展。这体现了渲染引擎开发中的模块协同关系,同时满足了系统性、完整性和可扩展性的要求。
## 1.3 本课题的主要内容
结合当前项目的实际进展,本文的研究与实现内容主要由渲染引擎主体部分和体积渲染扩展部分两方面构成。
本文的研究与实现内容主要由渲染引擎主体部分和体积渲染扩展部分两方面构成。
渲染引擎主体部分围绕运行时系统与编辑器工作流展开。在运行时层面项目已经建立起平台层、图形接口抽象层、资源系统、场景与组件系统、渲染主链、模型与材质系统、多光源与简单阴影、C# 脚本运行时等核心模块,能够支持基础场景的组织、加载与实时渲染。在编辑器层面,项目已经形成 Scene 视口、Game 视口、Hierarchy、Inspector、Project、Console 等主要界面,并提供对象拾取、轮廓高亮、网格显示、变换 Gizmo 以及脚本构建与重载等辅助能力,具备较完整的开发与调试闭环。
渲染引擎主体部分围绕运行时系统与编辑器工作流展开。在运行时层面,项目已经建立起平台层、图形接口抽象层、资源缓存系统、场景与组件系统、渲染主链、模型与材质系统、多光源与简单阴影、C# 脚本运行时等核心模块,能够支持基础场景的组织、加载与实时渲染。在编辑器层面,项目已经形成 Scene 视口、Game 视口、Hierarchy、Inspector、Project、Console 等主要界面,并提供对象拾取、轮廓高亮、网格显示、变换 Gizmo 以及脚本构建与重载等辅助能力,具备较完整的开发与调试闭环。
体积渲染扩展部分建立在现有引擎主体之上,重点研究参与介质渲染的基本理论以及 NanoVDB 稀疏体数据在 DirectX 12 环境下的工程实现方式。其核心内容包括体积渲染基本物理量分析、体绘制方程的简化理解、光线步进流程、稀疏体数据加载与 GPU 上传、Shader 侧体数据访问、空域跳过优化和体积阴影等。
体积渲染扩展部分建立在现有引擎主体之上,重点研究参与介质渲染的基本理论以及 NanoVDB 稀疏体数据在 DirectX 12 环境下的工程实现方式。其核心内容包括体积渲染基本物理量分析、体绘制方程的简化理解、光线步进流程、稀疏体数据加载与 GPU 上传、Shader 侧体数据访问、空域跳过优化和体积阴影等。成功完成了基于 NanoVDB 的体积渲染管线,支持了云、雾、烟等体积特效的实时渲染。
## 1.4 本文的主要工作
围绕上述目标,本文已开展并完成的主要工作如下。
1. 完成了渲染引擎总体架构的设计与模块划分构建了平台层、RHI 层、资源与场景层、渲染层、脚本层和编辑器层之间的基本组织关系。
2. 实现了渲染引擎运行时主体能力,完成了缓冲、纹理、资源视图、管线状态、交换链等核心图形对象封装,并在此基础上建立了渲染请求规划、场景提取和相机执行等主链流程。
3. 实现了资源导入与管理、场景与组件组织、OBJ 模型渲染、材质系统、多光源和简单阴影等基础渲染能力,使引擎具备了较完整的场景渲染闭环。
2. 实现了渲染引擎运行时主体能力,完成了图形缓冲、纹理、资源视图、管线状态、交换链等核心图形对象封装,并在此基础上建立了渲染请求规划、场景提取和相机执行等主链流程。
3. 实现了资源导入与缓存管理、场景与组件组织、OBJ 模型渲染、材质系统、多光源和简单阴影等基础渲染能力,使引擎具备了较完整的场景渲染闭环。
4. 实现了基于 Mono 的 C# 脚本系统以及编辑器工作界面支持脚本程序集构建、脚本运行时装载、Scene/Game 视口显示、Hierarchy 与 Inspector 联动、Project 资源浏览和 Console 调试输出等功能。
5. 完成了基于 NanoVDB 的体积渲染管线,实现 `.nvdb` 数据加载、GPU Buffer 上传、HLSL 侧 PNanoVDB 访问、光线步进、HDDA 跳空和体积阴影等流程。
5. 完成了基于 NanoVDB 的体积渲染管线,实现 .nvdb 数据加载、GPU Buffer 上传、HLSL 侧 PNanoVDB 访问、光线步进、HDDA 跳空和体积阴影等流程。
## 1.5 论文结构安排
全文共分为九章,各章安排如下。
1. 第1章为绪论主要说明课题背景、课题意义、本文的主要内容、已完成的主要工作以及全文结构安排。
2. 第2章介绍渲染引擎相关技术基础,为后续引擎架构设计与核心模块实现提供技术铺垫。
2. 第2章介绍渲染引擎发展历程、当前主流实时渲染引擎特点以及本课题渲染引擎概况,为后续引擎架构设计与核心模块实现铺垫。
3. 第3章介绍体积渲染理论基础重点说明参与介质、透射率、体绘制方程、光线步进和稀疏体数据等内容。
4. 第4章对渲染引擎总体架构进行设计说明给出系统分层、模块划分、数据流关系以及体积渲染模块在整体架构中的位置。
5. 第5章围绕渲染引擎核心模块展开说明 RHI、资源系统、场景与组件系统、渲染主链、材质光照和脚本系统等关键内容的设计与实现。

View File

@@ -1,6 +1,6 @@
# 第三章 体积渲染理论基础
第二章已经渲染引擎角度说明了运行时系统、资源系统、场景组织和编辑器工作流等基础内容。本章进一步转入体积渲染本身的理论部分,重点讨论参与介质、光在介质中的衰减与散射、体绘制方程、光线步进以及稀疏体数据与空域跳过等关键概念,为后续基于 NanoVDB 的体积渲染管线设计与实现提供充足的理论基础。
第二章已经对实时渲染引擎进行了详细的介绍,包括渲染引擎发展历程、当前主流实时渲染引擎特点以及本课题渲染引擎概况等。本章进一步转入体积渲染本身的理论部分,重点讨论参与介质、光在介质中的衰减与散射、体绘制方程、光线步进以及稀疏体数据与空域跳过等关键概念,为后续基于 NanoVDB 的体积渲染管线设计与实现提供充足的理论基础。
## 3.1 参与介质与体积渲染基本概念
@@ -8,7 +8,7 @@
传统表面渲染主要关心光线在物体表面处发生的反射与折射而体积渲染关注的对象则是空间中的参与介质Participating Media。所谓参与介质是指光在传播过程中会与其内部粒子持续发生相互作用的介质例如云、雾、烟、火焰、水汽等。这类介质并不像三角形网格那样只在边界上发生光学变化而是会在体积内部对光线产生吸收、散射甚至发射作用因此它们的成像过程天然具有空间累积特征。
从工程角度看,参与介质通常可以分为均匀介质和非均匀介质两类。均匀介质在空间中的光学参数保持不变,便于分析和推导;非均匀介质的密度或光学参数随位置变化,更接近真实云层和烟雾的外观,也是实际应用中更常见的情况。在本文项目后续的体积渲染实现中,体数据本质上就是对这种空间变化进行离散表达,因此理解参与介质的基本性质是后续实现的前提。
从工程角度看,参与介质通常可以分为均匀介质和非均匀介质两类。均匀介质在空间中的光学参数保持不变,便于分析和推导;非均匀介质的密度或光学参数随位置变化,更接近真实云层和烟雾的外观,也是实际应用中更常见的情况。在本文项目后续的体积渲染实现中,稀疏体数据本质上就是对这种空间变化进行离散表达,因此理解参与介质的基本性质是后续实现的前提。
### 3.1.2 吸收、散射与透射率
@@ -18,15 +18,13 @@ $$
\sigma_t = \sigma_a + \sigma_s
$$
在很多实时体积渲染实现中,密度场会被用来调制这些系数,使介质在空间上的不透明度和亮度分布发生变化。也就是说,密度并不是与吸收、散射并列的第三类光学现象,而更像是对局部光学参数的空间缩放密度越大,局部吸收和散射通常也越强;密度越小,介质对光的影响则越弱。
在很多实时体积渲染实现中,密度场会被用来调制这些系数,使介质在空间上的不透明度和亮度分布发生变化。密度值是对局部光学参数的空间缩放密度越大,局部吸收和散射通常也越强;密度越小,介质对光的影响则越弱。
透射率Transmittance描述的是光线在介质中传播一段距离后仍然保留下来的比例其取值范围在 0 到 1 之间。透射率越接近 1说明光几乎未被削弱透射率越接近 0说明光在介质中已被显著衰减。透射率是体积渲染中极为核心的量因为无论是背景光穿过体积后的结果还是光源传播到采样点的有效光照最终都要依赖透射率来表达。
### 3.1.3 体积颜色形成的基本原因
### 3.1.3 体积颜色形成原因
体积图像的形成并不是单纯“给体素上色”,而是光在介质中传播、衰减与散射共同作用的结果。从观察者方向看,体积颜色主要来自两个来源。其一是背景或后方物体发出的光在穿过介质时被衰减后剩余的部分;其二是来自光源的入射光在介质内部发生散射后,被重新导向观察方向所产生的内散射贡献。当前项目后续实现的重点主要也集中在这两部分,即背景透射与单次散射近似
如果介质本身还会主动发光,例如火焰、高温气体等,则还需要考虑发射项。不过从本文当前项目的体积模块实现状态来看,重点仍然放在云、烟等主要受吸收和散射影响的体数据渲染上,因此本章后续分析将以吸收、散射和透射率累积为主。
体积图像的形成是光在介质中传播、衰减与散射共同作用的结果。从观察者方向看,体积颜色主要来自两个来源。其一是背景或体积后方物体发出的光在穿过介质时被衰减后剩余的部分;其二是来自光源的入射光在介质内部发生散射后,被重新导向观察方向所产生的内散射贡献。如果介质本身还会主动发光,例如火焰、高温气体等,则还需要考虑发射项
## 3.2 比尔-朗伯定律与光线透射
@@ -104,39 +102,33 @@ $$
其中 $g \in [-1,1]$ 为非对称因子。当 $g=0$ 时退化为各向同性散射;当 $g>0$ 时表现为前向散射;当 $g<0$ 时表现为后向散射。后续第7章的工程实现虽然会采用简化形式但相位函数这一思想仍然是构成单次散射项的理论基础。
### 3.3.4 为什么在实时系统中常采用简化模型
如果严格考虑多次散射、复杂相位函数、多个动态光源和高精度体数据积分,那么体积渲染的计算量会迅速增大,很难满足实时应用对帧率的要求。因此,实时系统通常会在若干环节上做简化:例如只考虑单次散射、采用固定步长或分层步进、使用简化相位函数、对阴影进行近似积分,或者在必要时对空区域进行跳过。
这种简化并不意味着理论被放弃,而是意味着在已知完整物理模型的前提下,有选择地保留最影响画面结果的部分。对于工程设计类项目而言,这种从完整理论到可实时实现之间的取舍非常重要。后续章节的体积模块实现也正是在这一原则下,选择了适合当前项目阶段的实时方案。
## 3.4 光线步进算法原理
### 3.4.1 Ray Marching 的基本思想
由于体绘制方程通常难以直接求得解析解实际实现中往往采用数值积分近似而光线步进Ray Marching就是其中最典型的方法。其基本思想是先求出相机光线与体积包围区域的进入点和离开点再把这段区间划分为若干个小步长在每个采样点处估计局部密度、局部消光、局部散射贡献与透射率最后将这些局部结果累积起来近似连续积分。
若步长足够小,则每个小区间都可以视作局部均匀,这样就能用离散求和逼近连续积分。也正因为这一思想简单直接,光线步进非常适合作为体积渲染的工程实现起点。本文后续的 NanoVDB 体积原型同样以相机光线与体积边界求交为起点,然后在体积内部做离散采样累积。
若步长足够小,则每个小区间都可以视作局部均匀,这样就能用离散求和逼近连续积分。也正因为这一思想简单直接,光线步进算法非常适合作为体积渲染的工程实现起点。本文后续的 NanoVDB 体积渲染同样以相机光线与体积边界求交为起点,然后在体积内部做离散采样累积。
### 3.4.2 正向步进与反向步进
按观察方向组织时,光线步进通常可以分为正向步进和反向步进两种。正向步进一般指从靠近相机的一侧向远处也可理解为前向累积front-to-back反向步进则从远处向近处积分也可理解为后向累积back-to-front
按观察方向分类,光线步进通常可以分为正向步进和反向步进两种。正向步进一般指从靠近相机的一侧向远处也可理解为前向累积front-to-back反向步进则从远处向近处积分也可理解为后向累积back-to-front
从数值结果上看,两者都可以逼近同一个积分目标,但在工程实现上,正向步进往往更适合实时系统。原因在于,正向步进可以显式维护“当前剩余透射率”,当透射率已经很低时,后续更远处的采样贡献可以近似忽略,从而支持提前终止优化。反向步进虽然在某些推导上较直观,但不如正向步进方便做前端遮挡裁剪。当前项目后续的体积实现也更接近前向累积方式。
从数值结果上看,两者都可以逼近同一个积分目标,但在工程实现上,正向步进往往更适合实时系统。原因在于,正向步进支持提前终止优化。反向步进虽然在推导上较直观,但不如正向步进方便做前端遮挡裁剪。本课题采用前向累积方式。
### 3.4.3 步长、最大步数与误差控制
步长是光线步进中的关键参数。步长越小,离散积越接近连续积分,画面通常越平滑,细节也越稳定;但采样次数随之增加,运行开销也会显著变大。步长过大时,则容易出现条带感、细节丢失和阴影估计不稳定等问题。与步长相对应的另一个参数是最大步数,它决定了一条光线最多允许采样多少次,用于限制最坏情况下的开销。
步长是光线步进中的关键参数。步长越小,离散积越接近连续积分,画面通常越平滑,细节也越稳定;但采样次数随之增加,运行开销也会显著变大。步长过大时,则容易出现条带感、细节丢失和阴影估计不稳定等问题。与步长相对应的另一个参数是最大步数,它决定了一条光线最多允许采样多少次,用于限制最坏情况下的开销。
因此,实时体积渲染本质上是在“精度”和“性能”之间寻找平衡。工程上通常会根据体素分辨率、包围盒大小、屏幕分辨率以及目标帧率来选择合适步长,并在必要时为主光线和阴影光线设置不同的采样密度。这种参数平衡在后续测试章节中也会体现出来。
### 3.4.4 提前终止、抖动等常见优化
### 3.4.4 提前终止和采样抖动优化
在体积积分过程中并不是每一个采样点都同样重要因此常会配合若干优化策略。最常见的一类是提前终止Early Termination当累计透射率已经低于某个阈值时说明后续更远区域的贡献非常有限此时可以直接结束当前光线步进。对于较浓的烟雾或高密度区域这类优化能节省大量无效计算。
另一类常见方法是采样抖动Jitter。固定步长加固定采样起点容易带来规则性的条纹或分层感而在初始采样位置上引入轻微随机偏移可以打散这种结构化误差使图像在视觉上更平滑。除此之外还可以通过多分辨率步进、阴影光线使用更粗步长、分层包围盒裁剪等方式进一步优化。对于本文后续实现而言提前终止和空域跳过具有更直接的工程意义。
## 3.5 稀疏体数据与空域跳过思想
## 3.5 稀疏体数据与空域跳过
### 3.5.1 稠密体素网格的问题
@@ -146,11 +138,11 @@ $$
### 3.5.2 稀疏体数据结构的意义
稀疏体数据结构的核心思想是只为真正包含有效信息的区域分配更细粒度的存储而对大量空区域或均匀区域采用更粗层级的表示。OpenVDB 及其面向 GPU 的 NanoVDB 就属于这一思路。它们通过层级化节点结构组织体数据,使得体素值访问不再局限于简单的三维数组索引,而是能够根据当前区域是否活跃、当前层级分辨率以及节点类型进行更有选择性访问。
稀疏体数据结构的核心思想是只为真正包含有效信息的区域分配更细粒度的存储而对大量空区域或均匀区域采用更粗层级的表示。OpenVDB 及其面向 GPU 的 NanoVDB 就属于这一思路。它们通过层级化节点结构组织体数据,使得体素值访问不再局限于简单的三维数组索引,而是能够根据当前区域是否活跃、当前层级分辨率以及节点类型进行选择性访问。
对于实时渲染而言NanoVDB 的价值尤其明显。它在保留 VDB 层级稀疏表达思想的基础上,对数据进行了线性化组织,使 GPU 更容易访问。这样一来,体数据不但可以以更紧凑的形式存储在显存中,还能够在 Shader 中配合层级遍历或辅助访问器实现更高效的采样与判断。后续第7章的工程实现正是建立在这种 GPU 友好的稀疏表示之上。
### 3.5.3 空区域跳过对实时性的作用
### 3.5.3 空区域跳实时性优化
空域跳过Empty Space Skipping的目标是尽量避免在无效区域上进行逐步长采样。其基本思想不是改变体绘制方程而是在数值积分过程中快速定位“哪些区域值得细采样哪些区域可以直接跳过”。如果能在光线进入空区域时一次跨过较长距离而不是继续做多个低价值采样那么体积渲染的实时性就会明显提升。

View File

@@ -1,24 +1,30 @@
# 第五章 渲染引擎核心模块设计与实现
上一章已经对渲染引擎的总体分层、模块划分和数据流关系进行了说明。本章进一步下沉到运行时主体,围绕当前项目中已经形成的几个核心模块展开分析,重点说明这些模块在工程实现中的职责边界、关键数据结构和协同方式。结合现有代码实现,本章主要讨论 RHI 抽象层、资源系统、场景与组件系统、渲染主链、模型材质与着色器系统、多光源与简单阴影,以及 C# 脚本系统。它们共同构成了当前渲染引擎的主体能力,也是后续编辑器工作流和体积渲染模块接入的基础。
上一章已经对渲染引擎的总体分层、模块划分和数据流关系进行了说明。本章进一步围绕当前项目的几个核心模块展开分析,重点说明这些模块在工程实现中的职责边界、关键数据结构和协同方式。结合现有代码实现,本章主要讨论 RHI 抽象层、资源系统、场景与组件系统、渲染主链、模型材质与着色器系统、多光源与简单阴影,以及 C# 脚本系统。它们共同构成了当前渲染引擎的主体能力,也是后续编辑器工作流和体积渲染模块接入的基础。
## 5.1 RHI 抽象层设计与实现
### 5.1.1 抽象目标与接口边界
### 5.1.1 设计目标与总体思路
渲染引擎的底层必须直接面对图形后端差异。不同图形 API 在资源创建方式、命令提交模型、描述符组织形式以及状态切换机制上均存在明显区别。如果上层渲染模块直接依赖某一个后端实现,那么渲染主链、资源绑定和管线状态组织都将与具体平台高度耦合,不利于后续扩展和维护。因此,本项目在底层建立了 RHIRendering Hardware Interface抽象层将图形设备能力统一为一组稳定接口使上层模块更多围绕“渲染什么”和“如何组织渲染阶段”来展开而不是反复处理后端 API 细节
RHI 的设计目标主要有两个。其一,是在 `D3D12``OpenGL``Vulkan` 三套后端之上建立统一接口,使上层渲染模块不再直接依赖具体图形 API其二是为资源系统、材质系统、渲染主链和后续高级渲染特性的接入提供一致的底层语义屏蔽不同后端的差异
当前实现看,`RHIDevice` 是这一抽象层的核心入口。它统一提供缓冲、纹理、交换链、命令列表、命令队列、着色器、管线状态、管线布局、同步栅栏、采样器、渲染通道、帧缓冲、描述符池、描述符集以及各类资源视图的创建接口。这样一来,渲染层在组织离屏纹理、深度表面、阴影图、材质资源绑定和绘制命令时,都可以基于统一对象模型展开
设计思路看,当前 RHI 遵循“求同存异、分层抽象、特性降级、底层逃逸”的原则。所谓求同存异,是优先提取不同图形 API 在资源、命令、状态与同步层面的共性能力,并把差异控制在后端内部;所谓分层抽象,是把上层渲染流程与底层 API 实现隔开使渲染管线层只面向统一接口编写所谓特性降级是通过硬件能力查询接口把不同后端与设备的能力差异转化为上层可判断、可选择的运行条件所谓底层逃逸则是保留原生设备或原生句柄访问入口以便在调试、扩展或特殊场景下直接调用底层对象。这样设计后RHI 既不会因为过度抽象而失去实际可用性,也不会因为直接暴露底层细节而破坏整个引擎的模块边界
### 5.1.2 多后端统一封装方式
### 5.1.2 统一控制模型
当前项目的 RHI 已经形成了多后端组织结构。在工程构建层面,`engine/CMakeLists.txt` 中已经纳入了 `D3D12``OpenGL``Vulkan` 三套后端实现,以及与之对应的缓冲、纹理、资源视图、交换链、命令队列、命令列表、描述符、管线状态和截图支持等对象。对应地,`RHIFactory` 负责根据 `RHIType` 或字符串名称创建目标后端设备,从而将设备实例化过程与上层运行逻辑解耦
当前项目的 RHI 抽象并不是把不同后端的接口简单套上一层统一名称,而是围绕一套完整的显式控制模型展开。其起点是统一的资源描述方式。无论是缓冲、纹理、交换链,还是着色器、管线状态和资源视图,上层都先通过统一描述结构表达“需要什么样的 GPU 对象”,然后再交由具体后端完成实例化。这样做的意义在于,把资源需求先从具体 API 中抽离出来,使资源系统、材质系统和渲染主链在进入底层之前就能围绕同一种对象语义组织数据,而不是在不同后端之间来回切换思维方式
这种设计并不是简单追求“支持多个 API”更重要的是建立统一的资源语义。例如上层不再分别讨论 D3D12 的描述符堆、OpenGL 的纹理单元或 Vulkan 的描述符集布局,而是通过统一的缓冲、纹理、采样器、描述符集和资源视图概念组织资源绑定;同样,上层也不直接处理各后端的原生命令对象,而是通过命令队列和命令列表接口完成绘制、状态切换与结果提交。当前项目的体积渲染研究阶段以 D3D12 为重点推进,但从引擎主体架构看,多后端 RHI 已经为引擎保留了较好的平台弹性
在资源被创建出来之后RHI 进一步把资源状态、描述符绑定、管线布局、命令录制与提交、渲染通道与帧缓冲组织成一条连续的控制链。资源状态解决的是“当前资源处于什么用途、下一步将被怎样访问”的问题;描述符绑定与管线布局解决的是“资源如何进入着色器、以什么绑定关系参与绘制或计算”的问题;命令列表与命令队列解决的是“这些状态和资源按什么顺序被提交给 GPU 执行”的问题;渲染通道与帧缓冲则进一步规定“本次执行向哪些目标输出、怎样装载和保存结果”。经过这层整理,场景绘制、阴影贴图、对象 ID 输出、离屏渲染和后处理就不再是零散的 API 调用,而是可以被统一组织和复用的阶段化流程
### 5.1.3 RHI 对上层模块的支撑作用
这套控制模型本质上更接近 `D3D12``Vulkan` 这类高级图形 API 的设计取向因为它们天然强调资源、状态、绑定、命令和同步的显式管理。与此同时RHI 还通过能力查询把不同硬件和后端的差异转化为上层可判断的运行条件使某些高级特性能够根据实际能力选择启用、降级或关闭。也正因为如此RHI 在当前引擎中承担的并不是单纯的“后端适配”职责,而是为整个渲染系统建立一套统一、可控、可扩展的底层执行模型。
RHI 抽象层在整个引擎中承担的是“运行时图形基础设施”的角色。资源系统最终生成的网格、纹理、材质常量和着色器变体,都需要落到 RHI 对象上;渲染主链中的主场景绘制、阴影绘制、对象 ID 绘制、后处理和最终输出,也都依赖 RHI 提供的离屏表面、命令提交和资源状态切换能力编辑器视口同样是通过对渲染表面的申请和复用接入渲染主链的。可以说RHI 为整个渲染引擎提供了统一而稳定的底层执行面,是后续所有高层模块成立的前提。
### 5.1.3 显式控制模型的后端落地
从实际实现看,当前 RHI 的整体结构明显以 `D3D12``Vulkan` 这类显式图形 API 为主干。无论是资源描述到对象创建的转换,还是资源状态切换、描述符绑定、管线布局、命令列表录制、命令队列提交以及渲染通道组织,这些抽象都天然更贴近显式 API 的工作方式。换句话说,这套 RHI 并不是先从三个后端求一个平均值,再拼出一层抽象;它首先确立的是一种偏高级 API 的控制模式,再让不同后端向这套模式靠拢。
在这种前提下,`D3D12``Vulkan` 的接入相对直接,因为它们本身就具备较强的显式控制特征。相比之下,`OpenGL` 属于典型的隐式状态机 API本身并不天然提供与描述符集、管线布局、显式同步和阶段化渲染通道完全对应的机制。如果直接按照 OpenGL 的原生使用方式暴露给上层,那么整个 RHI 统一抽象就会迅速失去约束力。为了解决这一问题,当前项目在 OpenGL 后端中做了较多模拟实现,把原本分散在上下文状态中的绑定、同步和资源使用过程重新整理为接近显式 API 的形式。例如,在资源绑定层,通过绑定点映射、纹理单元分配和统一缓冲管理来模拟描述符集与管线布局;在同步层,通过 `GLsync` 配合 CPU 侧计数方式适配统一的 Fence 语义;在渲染输出层,则通过帧缓冲和渲染通道封装,把 OpenGL 的输出过程纳入与其他后端一致的阶段化组织方式。
因此,三套后端虽然共同服务于同一套 RHI 接口,但它们在这套模型中的角色并不完全对称。`D3D12``Vulkan` 更像是这套显式控制模型的直接承载者,`OpenGL` 则是在保留自身底层实现的同时通过模拟与适配被收束到同一框架之中。对上层模块而言这种差异被有效屏蔽资源系统、渲染主链、材质绑定和编辑器视口都可以围绕统一的执行语义工作而对底层实现而言这又允许不同后端保留各自的实现特点。RHI 的真正价值也正体现在这里,即以统一的显式控制模型支撑整个渲染引擎,同时把后端差异压缩在可管理的范围之内。
## 5.2 资源系统设计与实现

View File

@@ -1,10 +1,10 @@
# 第六章 编辑器与引擎工作流设计与实现
上一章已经围绕渲染引擎运行时的核心模块展开分析,说明了底层图形抽象、资源管理、场景组织、渲染主链以及脚本系统的实现方式。在此基础上,本章进一步转向引擎的可视化界面与工作流部分,重点说明当前项目中的编辑器如何围绕场景编辑、资源浏览、参数调整、脚本运行和调试输出等功能组织起来。对于本课题而言,编辑器是连接资源系统、场景系统、渲染系统和脚本系统的直接入口,也是展示整个系统功能的重要部分。
上一章已经围绕渲染引擎运行时的核心模块展开分析,说明了各个模块的实现方式。在此基础上,本章进一步转向引擎的可视化界面与工作流部分,重点说明当前项目中的编辑器如何围绕场景编辑、资源浏览、参数调整、脚本运行和调试输出等功能组织起来。对于本课题而言,编辑器是连接资源系统、场景系统、渲染系统和脚本系统的直接入口,也是展示整个系统功能的重要部分。
## 6.1 编辑器在引擎中的定位
当前项目中的编辑器由 `Application``EditorLayer``EditorWorkspace` 以及各类面板共同构成,其初始化过程直接建立在现有引擎能力之上。编辑器启动后,会依次完成窗口渲染器初始化、项目根目录设置、`ResourceManager` 与脚本运行时初始化、ImGui 后端桥接以及视口离屏渲染资源接管,随后进入逐帧更新与绘制阶段。从系统定位看,编辑器并不是独立于引擎主体之外的附加程序,而是直接复用资源系统、渲染链路、场景数据和脚本运行时能力的可视化工作界面它承担的核心任务包括场景编辑、资源管理、渲染结果验证以及脚本与调试支撑,并通过 `Scene``Game``Hierarchy``Inspector``Project``Console` 等主要面组织这些工作。
当前项目中的编辑器由 `Application``EditorLayer``EditorWorkspace` 以及各类面板共同构成,其初始化过程直接建立在现有引擎能力之上。编辑器启动后,会依次完成窗口渲染器初始化、项目根目录设置、`ResourceManager` 与脚本运行时初始化、ImGui 后端桥接以及视口离屏渲染资源接管,随后进入逐帧更新与绘制阶段。编辑器是直接复用资源系统、渲染链路、场景数据和脚本运行时能力的可视化工作界面它承担的核心任务包括场景编辑、资源管理、渲染结果验证以及脚本与调试支撑,并通过 `Scene``Game``Hierarchy``Inspector``Project``Console` 等主要面组织这些工作。
编辑器与运行时之间的关系并不是简单的界面展示关系,而是围绕统一上下文形成了完整闭环。`EditorContext` 负责组织事件总线、选择管理器、场景管理器、项目管理器、撤销管理器和视口宿主服务,使不同面板之间的数据与事件能够保持联动;`EditorWorkspace` 负责在运行阶段组织菜单栏、停靠布局和各类面板的更新与绘制;`PlaySessionController` 则负责连接编辑态与运行态,在进入播放模式前保存场景快照、停止播放后恢复快照。这样一来,编辑器既能够把 `Game` 视口中的脚本逻辑和运行时场景更新真实执行出来,又能够在退出运行后回到稳定的编辑状态,因此它构成了当前渲染引擎工作流中的核心组织层。

View File

@@ -1,22 +1,20 @@
# 第四章 渲染引擎总体架构设计
前两章已经分别给出了渲染引擎概述和体积渲染理论基础。在此基础上,本章从系统设计层面对当前项目中的渲染引擎进行说明,重点讨论引擎设计目标、总体架构与模块划分、模块之间的协同关系,以及体积渲染模块在整体架构中的位置。本章的任务是整个系统的组织方式讲清楚为后续第5章、第6章和第7章的具体实现分析建立统一框架。
前两章已经分别给出了渲染引擎概述和体积渲染理论基础。在此基础上,本章从系统设计层面对当前项目中的渲染引擎进行说明,重点讨论引擎设计目标、总体架构与模块划分、模块之间的协同关系,以及体积渲染模块在整体架构中的位置。本章的任务是说明整个系统的组织方式为后续第5章、第6章和第7章的具体实现分析建立统一框架。
## 4.1 引擎设计目标
本项目的架构设计以渲染引擎为主体展开,目标是在统一框架下组织图形接口、资源管理、场景组织、渲染执行、脚本扩展和编辑器工作流等能力。这样的架构安排使系统能够围绕同一套运行时基础持续扩展,并将图形绘制、资源处理和工具能力组织在同一体系中。当前项目不仅包含运行时系统,也包含围绕资源浏览、场景编辑、参数调整和结果验证构建的编辑器工作流
渲染引擎的架构设计不仅服务于当前已完成的基础能力,也需要为后续扩展保留空间。体积渲染作为当前项目中最重要的高级渲染扩展,需要依附既有资源系统、渲染主链和编辑器验证能力接入系统。因此,在总体架构层面,需要预留新资源类型、新渲染阶段和新调试入口能够自然接入的位置,使扩展能够沿既有资源、渲染和验证链路进入系统。
本项目的架构设计以渲染引擎为主体展开,目标是在统一框架下组织图形硬件接口、资源管理、场景组织、渲染执行、脚本扩展和编辑器工作流等基础功能。同时体积渲染作为当前项目中最重要的高级渲染扩展,需要依附既有资源系统、渲染主链和编辑器验证能力接入系统。因此,在总体架构层面,需要兼顾基础功能完善和可扩展性
## 4.2 引擎总体架构与模块划分
结合当前项目的代码结构和功能组织方式,本文将渲染引擎总体概括为平台层、图形接口抽象层、资源与场景层、渲染组织层以及脚本与编辑器层五个主要层级。在此之外,系统中还存在内存、线程、调试、音频和 UI 等支撑模块,但从论文主体展开角度看,上述五层构成了最核心的结构主线。围绕这一分层结构,引擎主体的核心模块可以进一步归纳为 `RHI``Resources / Assets``Scene / Components``Rendering``Scripting``Editor` 六个部分,它们分别落在不同层次上,共同构成当前系统的主体架构。
本文将渲染引擎总体概括为平台层、图形接口抽象层、资源与场景层、渲染组织层以及脚本与编辑器层五个主要层级。在此之外,系统中还存在内存、线程、调试和 UI 等支撑模块,但从论文主体展开角度看,上述五层构成了最核心的结构主线。围绕这一分层结构,引擎主体的核心模块可以进一步归纳为 `RHI``Resources / Assets``Scene / Components``Rendering``Scripting``Editor` 六个部分,它们分别落在不同层次上,共同构成当前系统的主体架构。
【插图:渲染引擎总体分层架构】
### 平台层
平台层位于整个系统的底部,负责窗口、消息循环、输入处理、时间管理和文件系统访问等基础运行环境。它决定了主循环如何驱动系统运行,也决定了渲染结果最终如何呈现到设备屏幕上。平台层本身不直接组织场景和资源,但它为上层所有模块提供统一的执行环境。
平台层位于整个系统的底部,负责窗口、消息循环、输入处理、时间管理和文件系统访问等基础运行环境。它决定了主循环如何驱动系统运行,也决定了渲染结果最终如何呈现到设备屏幕上。平台层本身不直接组织场景和资源,但它为上层所有模块提供统一的执行环境。本课题项目主要建立在 Windows 平台上,同时也考虑到了跨平台的扩展性。
### 图形接口抽象层
@@ -40,7 +38,7 @@
### 4.3.1 资源从工程目录进入运行时的路径
工程目录中的模型、纹理、材质、着色器和场景文件首先经过资源系统的扫描、导入和缓存,随后被装载为运行时可直接使用的资源对象。这些资源再进一步被场景中的对象和组件引用,最终参与渲染和逻辑更新。由此形成从工程资源到运行时内容的第一条主线。
工程目录中的模型、纹理、材质、着色器、稀疏体积数据和场景文件首先经过资源系统的扫描、导入和缓存,随后被装载为运行时可直接使用的资源对象。这些资源再进一步被场景中的对象和组件引用,最终参与渲染和逻辑更新。由此形成从工程资源到运行时内容的第一条主线。
### 4.3.2 场景状态到渲染数据的转换关系
@@ -62,6 +60,6 @@
## 4.5 本章小结
本章从系统设计层面对当前项目中的渲染引擎进行了分析,明确了引擎架构的主要目标,包括形成完整工作框架、建立基础场景渲染闭环、兼顾运行时能力与编辑器工作流,以及为高级渲染特性扩展预留空间。在此基础上,将系统概括为平台层、图形接口抽象层、资源与场景层、渲染组织层以及脚本与编辑器层五个主要层级,并在同一结构下说明了 `RHI``Resources / Assets``Scene / Components``Rendering``Scripting``Editor` 六个核心模块在整体架构中的归属关系。
本章从系统设计层面对本课题的渲染引擎进行了分析,明确了引擎架构的主要目标,将系统概括为平台层、图形接口抽象层、资源与场景层、渲染组织层以及脚本与编辑器层五个主要层级,并在同一结构下说明了 `RHI``Resources / Assets``Scene / Components``Rendering``Scripting``Editor` 六个核心模块在整体架构中的归属关系。
同时,本章还从资源进入运行时、场景状态转化为渲染数据、编辑器视口接入渲染主链以及脚本驱动场景与渲染结果联动四个角度说明了模块之间的协同关系,并明确了体积渲染模块在总体架构中的位置。基于这一总体架构,下一章将进一步转入渲染引擎核心模块设计与实现的具体分析。

View File

@@ -0,0 +1,190 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &-3290751858395990286
MonoBehaviour:
m_ObjectHideFlags: 11
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: d0353a89b1f911e48b9e16bdc9f2e058, type: 3}
m_Name:
m_EditorClassIdentifier:
version: 7
--- !u!21 &2100000
Material:
serializedVersion: 8
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_Name: Nahida_Base
m_Shader: {fileID: 4800000, guid: 43a94ff316d044749abc5a8bce3d6902, type: 3}
m_Parent: {fileID: 0}
m_ModifiedSerializedProperties: 0
m_ValidKeywords: []
m_InvalidKeywords: []
m_LightmapFlags: 4
m_EnableInstancingVariants: 0
m_DoubleSidedGI: 0
m_CustomRenderQueue: -1
stringTagMap: {}
disabledShaderPasses: []
m_LockedProperties:
m_SavedProperties:
serializedVersion: 3
m_TexEnvs:
- _BaseMap:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _BumpMap:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _DetailAlbedoMap:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _DetailMask:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _DetailNormalMap:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _EmissionMap:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _FaceLightMap:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _FaceShadow:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _LightMap:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _MainTex:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _MetalMap:
m_Texture: {fileID: 2800000, guid: d7d1337cec6422b4f9db7325b520d61f, type: 3}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _MetallicGlossMap:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _NormalMap:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _OcclusionMap:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _ParallaxMap:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _ShadowRamp:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _SpecGlossMap:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- unity_Lightmaps:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- unity_LightmapsInd:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- unity_ShadowMasks:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
m_Ints: []
m_Floats:
- _AlphaClip: 0
- _AlphaToMask: 0
- _Blend: 0
- _BlendModePreserveSpecular: 1
- _BumpScale: 1
- _ClearCoatMask: 0
- _ClearCoatSmoothness: 0
- _Cull: 2
- _CustomMaterialType: 1
- _Cutoff: 0.5
- _DetailAlbedoMapScale: 1
- _DetailNormalMapScale: 1
- _DoubleSided: 0
- _DstBlend: 0
- _DstBlendAlpha: 0
- _EmissionIntensity: 0.2
- _EnvironmentReflections: 1
- _FaceBlushStrength: 0
- _FaceShadowOffset: 0
- _GlossMapScale: 0
- _Glossiness: 0
- _GlossyReflections: 0
- _IsDay: 1
- _IsFace: 0
- _Metallic: 0
- _MetallicIntensity: 8
- _NonmetallicIntensity: 0.3
- _OcclusionStrength: 1
- _OutlineWidth: 1.6
- _OutlineZOffset: 0.1
- _Parallax: 0.005
- _QueueOffset: 0
- _ReceiveShadows: 1
- _RimIntensity: 0.5
- _RimOffset: 5
- _RimThreshold: 0.5
- _ShadowOffset: 0.1
- _ShadowSmoothness: 0.4
- _Smoothness: 0.5
- _SmoothnessTextureChannel: 0
- _SpecularHighlights: 1
- _SpecularSmoothness: 5
- _SrcBlend: 1
- _SrcBlendAlpha: 1
- _Surface: 0
- _UseCustomMaterialType: 0
- _UseEmission: 0
- _UseNormalMap: 0
- _UseRim: 0
- _UseSmoothNormal: 0
- _UseSpecular: 0
- _WorkflowMode: 1
- _ZWrite: 1
m_Colors:
- _BaseColor: {r: 1, g: 1, b: 1, a: 1}
- _Color: {r: 1, g: 1, b: 1, a: 1}
- _EmissionColor: {r: 0, g: 0, b: 0, a: 1}
- _FaceBlushColor: {r: 1, g: 0.72156864, b: 0.69803923, a: 1}
- _FaceDirection: {r: 0, g: 0, b: 1, a: 0}
- _LightDirectionMultiplier: {r: 1, g: 0.5, b: 1, a: 0}
- _OutlineColor: {r: 0.5176471, g: 0.35686275, b: 0.34117648, a: 1}
- _OutlineColor2: {r: 0.3529412, g: 0.3529412, b: 0.3529412, a: 1}
- _OutlineColor3: {r: 0.47058824, g: 0.47058824, b: 0.5647059, a: 1}
- _OutlineColor4: {r: 0.5176471, g: 0.35686275, b: 0.34117648, a: 1}
- _OutlineColor5: {r: 0.35, g: 0.35, b: 0.35, a: 1}
- _OutlineWidthParams: {r: 0, g: 6, b: 0.1, a: 0.6}
- _ScreenOffset: {r: 0, g: 0, b: 0, a: 0}
- _ShadowColor: {r: 1.1, g: 1.1, b: 1.1, a: 1}
- _SpecColor: {r: 0.19999996, g: 0.19999996, b: 0.19999996, a: 1}
m_BuildTextureStacks: []

View File

@@ -0,0 +1,70 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &-3290751858395990286
MonoBehaviour:
m_ObjectHideFlags: 11
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: d0353a89b1f911e48b9e16bdc9f2e058, type: 3}
m_Name:
m_EditorClassIdentifier:
version: 7
--- !u!21 &2100000
Material:
serializedVersion: 8
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_Name: Nahida_Body
m_Shader: {fileID: 4800000, guid: 43a94ff316d044749abc5a8bce3d6902, type: 3}
m_Parent: {fileID: 2100000, guid: 30ad25ccfe2f70b4e8544dc4c3a455fa, type: 2}
m_ModifiedSerializedProperties: 0
m_ValidKeywords:
- _EMISSION
- _NORMAL_MAP
- _RIM
- _SPECULAR
m_InvalidKeywords: []
m_LightmapFlags: 4
m_EnableInstancingVariants: 0
m_DoubleSidedGI: 0
m_CustomRenderQueue: -1
stringTagMap: {}
disabledShaderPasses: []
m_LockedProperties:
m_SavedProperties:
serializedVersion: 3
m_TexEnvs:
- _BaseMap:
m_Texture: {fileID: 2800000, guid: 57a770f4365fbb14c84e40cfa596739c, type: 3}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _LightMap:
m_Texture: {fileID: 2800000, guid: 16957b0cd55aa5543a2de2ec31fb4c85, type: 3}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _NormalMap:
m_Texture: {fileID: 2800000, guid: bf09bf6ec9e03bf40a0ec73e7a3f4c5b, type: 3}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _ShadowRamp:
m_Texture: {fileID: 2800000, guid: 1b40895a669bdab449fecf3c904f1a40, type: 3}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
m_Ints: []
m_Floats:
- _DoubleSided: 0
- _IsDay: 1
- _OutlineWidth: 1.6
- _UseEmission: 1
- _UseNormalMap: 1
- _UseRim: 1
- _UseSmoothNormal: 1
- _UseSpecular: 1
m_Colors: []
m_BuildTextureStacks: []

View File

@@ -0,0 +1,65 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &-3290751858395990286
MonoBehaviour:
m_ObjectHideFlags: 11
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: d0353a89b1f911e48b9e16bdc9f2e058, type: 3}
m_Name:
m_EditorClassIdentifier:
version: 7
--- !u!21 &2100000
Material:
serializedVersion: 8
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_Name: Nahida_Brow
m_Shader: {fileID: 4800000, guid: 43a94ff316d044749abc5a8bce3d6902, type: 3}
m_Parent: {fileID: 2100000, guid: 30ad25ccfe2f70b4e8544dc4c3a455fa, type: 2}
m_ModifiedSerializedProperties: 0
m_ValidKeywords:
- _IS_FACE
- _RIM
m_InvalidKeywords: []
m_LightmapFlags: 4
m_EnableInstancingVariants: 0
m_DoubleSidedGI: 0
m_CustomRenderQueue: -1
stringTagMap: {}
disabledShaderPasses: []
m_LockedProperties:
m_SavedProperties:
serializedVersion: 3
m_TexEnvs:
- _BaseMap:
m_Texture: {fileID: 2800000, guid: e44c095e8f6db98499384bf085687aa5, type: 3}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _FaceLightMap:
m_Texture: {fileID: 2800000, guid: d933369d3fe294a4da5485a61032669e, type: 3}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _FaceShadow:
m_Texture: {fileID: 2800000, guid: d4fe3d81f5078b240a29ad0d13d5ebcc, type: 3}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _ShadowRamp:
m_Texture: {fileID: 2800000, guid: 1b40895a669bdab449fecf3c904f1a40, type: 3}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
m_Ints: []
m_Floats:
- _IsFace: 1
- _OutlineWidth: 0
- _UseCustomMaterialType: 1
- _UseRim: 1
m_Colors:
- _BaseColor: {r: 0.9764706, g: 0.80103135, b: 0.76164705, a: 1}
m_BuildTextureStacks: []

View File

@@ -0,0 +1,71 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &-3290751858395990286
MonoBehaviour:
m_ObjectHideFlags: 11
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: d0353a89b1f911e48b9e16bdc9f2e058, type: 3}
m_Name:
m_EditorClassIdentifier:
version: 7
--- !u!21 &2100000
Material:
serializedVersion: 8
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_Name: Nahida_Dress1
m_Shader: {fileID: 4800000, guid: 43a94ff316d044749abc5a8bce3d6902, type: 3}
m_Parent: {fileID: 2100000, guid: 30ad25ccfe2f70b4e8544dc4c3a455fa, type: 2}
m_ModifiedSerializedProperties: 0
m_ValidKeywords:
- _DOUBLE_SIDED
- _EMISSION
- _NORMAL_MAP
- _RIM
- _SPECULAR
m_InvalidKeywords: []
m_LightmapFlags: 4
m_EnableInstancingVariants: 0
m_DoubleSidedGI: 0
m_CustomRenderQueue: -1
stringTagMap: {}
disabledShaderPasses: []
m_LockedProperties:
m_SavedProperties:
serializedVersion: 3
m_TexEnvs:
- _BaseMap:
m_Texture: {fileID: 2800000, guid: 57a770f4365fbb14c84e40cfa596739c, type: 3}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _LightMap:
m_Texture: {fileID: 2800000, guid: 16957b0cd55aa5543a2de2ec31fb4c85, type: 3}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _NormalMap:
m_Texture: {fileID: 2800000, guid: bf09bf6ec9e03bf40a0ec73e7a3f4c5b, type: 3}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _ShadowRamp:
m_Texture: {fileID: 2800000, guid: 1b40895a669bdab449fecf3c904f1a40, type: 3}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
m_Ints: []
m_Floats:
- _Cull: 0
- _DoubleSided: 1
- _OutlineZOffset: 0.5
- _UseEmission: 1
- _UseNormalMap: 1
- _UseRim: 1
- _UseSmoothNormal: 1
- _UseSpecular: 1
m_Colors: []
m_BuildTextureStacks: []

View File

@@ -0,0 +1,70 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &-3290751858395990286
MonoBehaviour:
m_ObjectHideFlags: 11
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: d0353a89b1f911e48b9e16bdc9f2e058, type: 3}
m_Name:
m_EditorClassIdentifier:
version: 7
--- !u!21 &2100000
Material:
serializedVersion: 8
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_Name: Nahida_Dress2
m_Shader: {fileID: 4800000, guid: 43a94ff316d044749abc5a8bce3d6902, type: 3}
m_Parent: {fileID: 2100000, guid: 30ad25ccfe2f70b4e8544dc4c3a455fa, type: 2}
m_ModifiedSerializedProperties: 0
m_ValidKeywords:
- _DOUBLE_SIDED
- _EMISSION
- _NORMAL_MAP
- _RIM
- _SPECULAR
m_InvalidKeywords: []
m_LightmapFlags: 4
m_EnableInstancingVariants: 0
m_DoubleSidedGI: 0
m_CustomRenderQueue: -1
stringTagMap: {}
disabledShaderPasses: []
m_LockedProperties:
m_SavedProperties:
serializedVersion: 3
m_TexEnvs:
- _BaseMap:
m_Texture: {fileID: 2800000, guid: f771c421ae0f7e84891ad26baa73e626, type: 3}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _LightMap:
m_Texture: {fileID: 2800000, guid: 7c407627b6cc5474d87601dddeddf75b, type: 3}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _NormalMap:
m_Texture: {fileID: 2800000, guid: b3dd0683c060c8b4d8e23df5776b9d56, type: 3}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _ShadowRamp:
m_Texture: {fileID: 2800000, guid: 59a761e1753da09429a2c063c23def23, type: 3}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
m_Ints: []
m_Floats:
- _Cull: 0
- _DoubleSided: 1
- _OutlineZOffset: 0.5
- _UseEmission: 1
- _UseNormalMap: 1
- _UseRim: 1
- _UseSpecular: 1
m_Colors: []
m_BuildTextureStacks: []

View File

@@ -0,0 +1,66 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &-3290751858395990286
MonoBehaviour:
m_ObjectHideFlags: 11
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: d0353a89b1f911e48b9e16bdc9f2e058, type: 3}
m_Name:
m_EditorClassIdentifier:
version: 7
--- !u!21 &2100000
Material:
serializedVersion: 8
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_Name: Nahida_Face
m_Shader: {fileID: 4800000, guid: 43a94ff316d044749abc5a8bce3d6902, type: 3}
m_Parent: {fileID: 2100000, guid: 30ad25ccfe2f70b4e8544dc4c3a455fa, type: 2}
m_ModifiedSerializedProperties: 0
m_ValidKeywords:
- _IS_FACE
- _RIM
m_InvalidKeywords: []
m_LightmapFlags: 4
m_EnableInstancingVariants: 0
m_DoubleSidedGI: 0
m_CustomRenderQueue: -1
stringTagMap: {}
disabledShaderPasses: []
m_LockedProperties:
m_SavedProperties:
serializedVersion: 3
m_TexEnvs:
- _BaseMap:
m_Texture: {fileID: 2800000, guid: e44c095e8f6db98499384bf085687aa5, type: 3}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _FaceLightMap:
m_Texture: {fileID: 2800000, guid: d933369d3fe294a4da5485a61032669e, type: 3}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _FaceShadow:
m_Texture: {fileID: 2800000, guid: d4fe3d81f5078b240a29ad0d13d5ebcc, type: 3}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _ShadowRamp:
m_Texture: {fileID: 2800000, guid: 1b40895a669bdab449fecf3c904f1a40, type: 3}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
m_Ints: []
m_Floats:
- _FaceBlushStrength: 0.3
- _IsFace: 1
- _OutlineZOffset: 0.5
- _UseCustomMaterialType: 1
- _UseRim: 1
m_Colors:
- _OutlineWidthParams: {r: 0, g: 6, b: 0.1, a: 0.6}
m_BuildTextureStacks: []

View File

@@ -0,0 +1,69 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &-3290751858395990286
MonoBehaviour:
m_ObjectHideFlags: 11
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: d0353a89b1f911e48b9e16bdc9f2e058, type: 3}
m_Name:
m_EditorClassIdentifier:
version: 7
--- !u!21 &2100000
Material:
serializedVersion: 8
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_Name: Nahida_Hair
m_Shader: {fileID: 4800000, guid: 43a94ff316d044749abc5a8bce3d6902, type: 3}
m_Parent: {fileID: 2100000, guid: 30ad25ccfe2f70b4e8544dc4c3a455fa, type: 2}
m_ModifiedSerializedProperties: 0
m_ValidKeywords:
- _EMISSION
- _NORMAL_MAP
- _RIM
- _SPECULAR
m_InvalidKeywords: []
m_LightmapFlags: 4
m_EnableInstancingVariants: 0
m_DoubleSidedGI: 0
m_CustomRenderQueue: -1
stringTagMap: {}
disabledShaderPasses: []
m_LockedProperties:
m_SavedProperties:
serializedVersion: 3
m_TexEnvs:
- _BaseMap:
m_Texture: {fileID: 2800000, guid: f771c421ae0f7e84891ad26baa73e626, type: 3}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _LightMap:
m_Texture: {fileID: 2800000, guid: 7c407627b6cc5474d87601dddeddf75b, type: 3}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _NormalMap:
m_Texture: {fileID: 2800000, guid: b3dd0683c060c8b4d8e23df5776b9d56, type: 3}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _ShadowRamp:
m_Texture: {fileID: 2800000, guid: 59a761e1753da09429a2c063c23def23, type: 3}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
m_Ints: []
m_Floats:
- _UseEmission: 1
- _UseNormalMap: 1
- _UseRim: 1
- _UseSmoothNormal: 1
- _UseSpecular: 1
m_Colors:
- _OutlineColor: {r: 0.2784314, g: 0.18039216, b: 0.14901961, a: 1}
- _OutlineWidthParams: {r: 0, g: 6, b: 0.1, a: 0.6}
m_BuildTextureStacks: []

View File

@@ -0,0 +1,21 @@
# Nahida Unity Reference Snapshot
This directory stores the Unity-side raw reference files that describe how the original Nahida sample is assembled.
Why it lives under `docs/reference/` instead of `project/Assets/`:
- The engine asset pipeline imports `.mat` as engine-native materials.
- The engine asset pipeline imports `.shader` as engine-native shaders.
- These files are Unity formats, not engine-native formats.
- Keeping them out of `project/Assets` avoids false imports and noisy asset errors.
What is kept here:
- Unity materials
- Unity shader and HLSL files
- Unity runtime helper scripts
- Unity sample scene
- Unity URP settings assets
The engine-native migration target remains:
- `project/Assets/Characters/Nahida/`
Use this folder as semantic reference only while rebuilding the runtime path inside XCEngine.

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,33 @@
using System.Collections.Generic;
using UnityEngine;
namespace Nahida
{
public class MaterialUpdater : MonoBehaviour
{
[SerializeField]
private GameObject m_HeadBone;
[SerializeField]
private Vector3 m_HeadDirection = Vector3.up;
[SerializeField]
private List<SkinnedMeshRenderer> m_FaceRenderers;
private void Update()
{
if (m_FaceRenderers == null || m_HeadBone == null)
{
return;
}
Vector3 direction = m_HeadBone.transform.rotation * m_HeadDirection;
foreach (var renderer in m_FaceRenderers)
{
foreach (var material in renderer.materials)
{
material.SetVector("_FaceDirection", direction);
}
}
}
}
}

View File

@@ -0,0 +1,9 @@
namespace Nahida.Rendering
{
public enum BloomMode
{
None,
Color,
Brightness
}
}

View File

@@ -0,0 +1,36 @@
using System;
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;
namespace Nahida.Rendering
{
[Serializable]
[VolumeComponentMenuForRenderPipeline("Custom/Bloom", typeof(UniversalRenderPipeline))]
public class BloomVolume : VolumeComponent, IPostProcessComponent
{
public VolumeParameter<BloomMode> mode = new VolumeParameter<BloomMode>();
public MinFloatParameter threshold = new MinFloatParameter(0.7f, 0f);
public MinFloatParameter intensity = new MinFloatParameter(1.5f, 0f);
public Vector4Parameter weights = new Vector4Parameter(0.25f * Vector4.one);
public ColorParameter color = new ColorParameter(Color.white);
public MinFloatParameter blurRadius = new MinFloatParameter(2f, 0f);
public ClampedFloatParameter downSampleScale = new ClampedFloatParameter(0.5f, 0.1f, 1f);
public bool IsActive()
{
return mode.value != BloomMode.None;
}
public bool IsTileCompatible()
{
return false;
}
}
}

View File

@@ -0,0 +1,29 @@
using System;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;
namespace Nahida.Rendering
{
[Serializable]
[VolumeComponentMenuForRenderPipeline("Custom/ColorGrading", typeof(UniversalRenderPipeline))]
public class ColorGradingVolume : VolumeComponent, IPostProcessComponent
{
public BoolParameter useTonemapping = new BoolParameter(false);
public MinFloatParameter exposure = new MinFloatParameter(1f, 0f);
public ClampedFloatParameter contrast = new ClampedFloatParameter(0f, -100f, 100f);
public ClampedFloatParameter saturation = new ClampedFloatParameter(0f, -100f, 100f);
public bool IsActive()
{
return useTonemapping.value || exposure.value != 1f || contrast.value != 0f || saturation.value != 0f;
}
public bool IsTileCompatible()
{
return false;
}
}
}

View File

@@ -0,0 +1,36 @@
using UnityEngine;
using UnityEngine.Rendering.Universal;
namespace Nahida.Rendering
{
public class PostProcessFeature : ScriptableRendererFeature
{
[SerializeField]
private RenderPassEvent m_RenderPassEvent = RenderPassEvent.BeforeRenderingPostProcessing;
[SerializeField]
private Shader m_Shader;
private PostProcessPass _postProcessPass;
public const int BloomIterations = 4;
public override void Create()
{
if (m_Shader == null)
m_Shader = Shader.Find("URPGenshinPostProcess");
_postProcessPass = new PostProcessPass(m_RenderPassEvent, m_Shader, BloomIterations);
}
public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData)
{
renderer.EnqueuePass(_postProcessPass);
}
protected override void Dispose(bool disposing)
{
_postProcessPass?.Dispose();
_postProcessPass = null;
}
}
}

View File

@@ -0,0 +1,165 @@
using System;
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;
namespace Nahida.Rendering
{
public class PostProcessPass : ScriptableRenderPass
{
private Material _material;
private RTHandle[] _bloomBufferA;
private RTHandle[] _bloomBufferB;
private bool _useBloom;
private int _iterations;
private float _downSampleScale;
public PostProcessPass(RenderPassEvent renderPassEvent, Shader shader, int iterations)
{
_material = CoreUtils.CreateEngineMaterial(shader);
_iterations = iterations;
_bloomBufferA = new RTHandle[_iterations];
_bloomBufferB = new RTHandle[_iterations];
base.profilingSampler = new ProfilingSampler(nameof(PostProcessPass));
base.renderPassEvent = renderPassEvent;
}
public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
{
var cameraType = renderingData.cameraData.cameraType;
if (cameraType == CameraType.Preview || cameraType == CameraType.Reflection)
{
return;
}
var stack = VolumeManager.instance.stack;
var bloomVolume = stack.GetComponent<BloomVolume>();
var colorGradingVolume = stack.GetComponent<ColorGradingVolume>();
if (!bloomVolume.IsActive() && !colorGradingVolume.IsActive())
{
return;
}
_useBloom = bloomVolume.IsActive();
_downSampleScale = bloomVolume.downSampleScale.value;
SetupBuffer(ref renderingData);
SetupMaterial(bloomVolume, colorGradingVolume, ref renderingData);
var command = CommandBufferPool.Get();
Render(command, ref renderingData);
context.ExecuteCommandBuffer(command);
CommandBufferPool.Release(command);
}
private void SetupBuffer(ref RenderingData renderingData)
{
var cameraDescriptor = renderingData.cameraData.cameraTargetDescriptor;
var descriptor = new RenderTextureDescriptor(cameraDescriptor.width, cameraDescriptor.height, cameraDescriptor.graphicsFormat, 0, 0);
if (_useBloom)
{
descriptor.width = (int)Math.Round(descriptor.width * _downSampleScale);
descriptor.height = (int)Math.Round(descriptor.height * _downSampleScale);
for (int i = 0; i < _iterations; i++)
{
RenderingUtils.ReAllocateIfNeeded(ref _bloomBufferA[i], descriptor, FilterMode.Bilinear, name: $"_BloomBufferA_{i}");
RenderingUtils.ReAllocateIfNeeded(ref _bloomBufferB[i], descriptor, FilterMode.Bilinear, name: $"_BloomBufferB_{i}");
descriptor.width = Math.Max(descriptor.width / 2, 1);
descriptor.height = Math.Max(descriptor.height / 2, 1);
}
}
}
private void SetupMaterial(BloomVolume bloomVolume, ColorGradingVolume colorGradingVolume, ref RenderingData renderingData)
{
float screenFactor = renderingData.cameraData.cameraTargetDescriptor.height / 1080f;
CoreUtils.SetKeyword(_material, "_BLOOM_COLOR", bloomVolume.mode.value == BloomMode.Color);
CoreUtils.SetKeyword(_material, "_BLOOM_BRIGHTNESS", bloomVolume.mode.value == BloomMode.Brightness);
CoreUtils.SetKeyword(_material, "_TONEMAPPING", colorGradingVolume.useTonemapping.value);
_material.SetFloat("_Exposure", colorGradingVolume.exposure.value);
_material.SetFloat("_Contrast", 1f + colorGradingVolume.contrast.value / 100f);
_material.SetFloat("_Saturation", 1f + colorGradingVolume.saturation.value / 100f);
if (_useBloom)
{
_material.SetFloat("_BloomThreshold", bloomVolume.threshold.value);
_material.SetFloat("_BloomIntensity", bloomVolume.intensity.value);
_material.SetVector("_BloomWeights", bloomVolume.weights.value);
_material.SetColor("_BloomColor", bloomVolume.color.value);
_material.SetFloat("_BlurRadius", bloomVolume.blurRadius.value * _downSampleScale * screenFactor);
}
}
private void Render(CommandBuffer commandBuffer, ref RenderingData renderingData)
{
var source = renderingData.cameraData.renderer.cameraColorTargetHandle;
using (new ProfilingScope(commandBuffer, profilingSampler))
{
if (_useBloom)
{
RTHandle[] bufferA = _bloomBufferA, bufferB = _bloomBufferB;
Blit(commandBuffer, source, bufferA[0], Pass.BloomPrefilter);
Blit(commandBuffer, bufferA[0], bufferB[0], Pass.BloomHorizontalBlur1x);
Blit(commandBuffer, bufferB[0], bufferA[0], Pass.BloomVerticalBlur1x);
for (int i = 1; i < _iterations; i++)
{
Blit(commandBuffer, bufferA[i - 1], bufferB[i], Pass.BloomHorizontalBlur2x);
Blit(commandBuffer, bufferB[i], bufferA[i], Pass.BloomVerticalBlur1x);
}
commandBuffer.SetGlobalTexture("_BloomTextureA", bufferA[0]);
commandBuffer.SetGlobalTexture("_BloomTextureB", bufferA[1]);
commandBuffer.SetGlobalTexture("_BloomTextureC", bufferA[2]);
commandBuffer.SetGlobalTexture("_BloomTextureD", bufferA[3]);
Blit(commandBuffer, bufferB[0], bufferB[0], Pass.BloomUpsample);
commandBuffer.SetGlobalTexture("_BloomTextureA", bufferB[0]);
}
base.Blit(commandBuffer, ref renderingData, _material, (int)Pass.ColorGrading);
}
}
private void Blit(CommandBuffer commandBuffer, RTHandle source, RTHandle destination, Pass pass)
{
const RenderBufferLoadAction Load = RenderBufferLoadAction.DontCare;
const RenderBufferStoreAction Save = RenderBufferStoreAction.Store;
Blitter.BlitCameraTexture(commandBuffer, source, destination, Load, Save, _material, (int)pass);
}
public void Dispose()
{
for (int i = 0; i < _iterations; i++)
{
_bloomBufferA[i]?.Release();
_bloomBufferB[i]?.Release();
}
CoreUtils.Destroy(_material);
}
public enum Pass
{
Blit,
BloomPrefilter,
BloomHorizontalBlur1x,
BloomHorizontalBlur2x,
BloomVerticalBlur1x,
BloomVerticalBlur2x,
BloomUpsample,
ColorGrading
}
}
}

View File

@@ -0,0 +1,29 @@
using UnityEngine;
namespace Nahida
{
public class TransformRotator : MonoBehaviour
{
[SerializeField]
private float m_Cycle;
[SerializeField]
private Vector3 m_Axis;
private Quaternion _rotation;
private float _startTime;
private void OnEnable()
{
_rotation = transform.rotation;
_startTime = Time.time;
}
private void Update()
{
float angle = 360f * (Time.time - _startTime) / m_Cycle;
transform.rotation = Quaternion.AngleAxis(angle, m_Axis) * _rotation;
}
}
}

View File

@@ -0,0 +1,187 @@
Shader "Genshin"
{
Properties
{
[Header(General)]
[MainTexture]_BaseMap("Base Map", 2D) = "white" {}
[MainColor] _BaseColor("Base Color", Color) = (1,1,1,1)
[ToggleUI] _IsDay("Is Day", Float) = 1
[Toggle(_DOUBLE_SIDED)] _DoubleSided("Double Sided", Float) = 0
[Enum(UnityEngine.Rendering.CullMode)] _Cull("Cull", Float) = 2
[Enum(UnityEngine.Rendering.BlendMode)] _SrcBlend("Src Blend", Float) = 1
[Enum(UnityEngine.Rendering.BlendMode)] _DstBlend("Dst Blend", Float) = 0
[Header(Shadow)]
_LightMap("Light Map", 2D) = "white" {}
_LightDirectionMultiplier("Light Direction Multiplier", Vector) = (1,1,1,0)
_ShadowOffset("Shadow Offset", Float) = 0
_ShadowSmoothness("Shadow Smoothness", Float) = 0
[HDR] _ShadowColor("Shadow Color", Color) = (1,1,1,1)
_ShadowRamp("Shadow Ramp", 2D) = "white" {}
[ToggleUI] _UseCustomMaterialType("Use Custom Material Type", Float) = 0
_CustomMaterialType("Custom Material Type", Float) = 1
[Header(Emission)]
[Toggle(_EMISSION)] _UseEmission("Use Emission", Float) = 0
_EmissionIntensity("Emission Intensity", Float) = 1
[Header(Normal)]
[Toggle(_NORMAL_MAP)] _UseNormalMap("Use Normal Map", Float) = 0
[Normal] _NormalMap("Normal Map", 2D) = "bump" {}
[Header(Face)]
[Toggle(_IS_FACE)] _IsFace("Is Face", Float) = 0
_FaceDirection("Face Direction", Vector) = (0,0,1,0)
_FaceShadowOffset("Face Shadow Offset", Float) = 0
_FaceBlushColor("Face Blush Color", Color) = (1,1,1,1)
_FaceBlushStrength("Face Blush Strength", Float) = 1
_FaceLightMap("Face Light Map", 2D) = "white" {}
_FaceShadow("Face Shadow", 2D) = "white" {}
[Header(Specular)]
[Toggle(_SPECULAR)] _UseSpecular("Use Specular", Float) = 0
_SpecularSmoothness("Specular Smoothness", Float) = 1
_NonmetallicIntensity("Nonmetallic Intensity", Float) = 1
_MetallicIntensity("Metallic Intensity", Float) = 1
_MetalMap("Metal Map", 2D) = "white" {}
[Header(Rim Light)]
[Toggle(_RIM)] _UseRim("Use Rim", Float) = 0
_RimOffset("Rim Offset", Float) = 1
_RimThreshold("Rim Threshold", Float) = 1
_RimIntensity("Rim Intensity", Float) = 1
[Header(Outline)]
[ToggleUI] _UseSmoothNormal("Use Smooth Normal", Float) = 0
_OutlineWidth("Outline Width", Float) = 1
_OutlineWidthParams("Outline Width Params", Vector) = (0,1,0,1)
_OutlineZOffset("Outline Z Offset", Float) = 0
_ScreenOffset("Screen Offset", Vector) = (0,0,0,0)
_OutlineColor("Outline Color", Color) = (0,0,0,1)
_OutlineColor2("Outline Color 2", Color) = (0,0,0,1)
_OutlineColor3("Outline Color 3", Color) = (0,0,0,1)
_OutlineColor4("Outline Color 4", Color) = (0,0,0,1)
_OutlineColor5("Outline Color 5", Color) = (0,0,0,1)
}
Subshader
{
Tags
{
"RenderType" = "Opaque"
"RenderPipeline" = "UniversalPipeline"
"UniversalMaterialType" = "Lit"
"IgnoreProjector" = "True"
}
Pass
{
Name "Forward"
Tags {"LightMode" = "UniversalForward"}
Cull[_Cull]
ZWrite On
Blend[_SrcBlend][_DstBlend]
HLSLPROGRAM
// Universal Pipeline keywords
#pragma multi_compile _ _MAIN_LIGHT_SHADOWS _MAIN_LIGHT_SHADOWS_CASCADE _MAIN_LIGHT_SHADOWS_SCREEN
#pragma multi_compile _ _ADDITIONAL_LIGHTS_VERTEX _ADDITIONAL_LIGHTS
#pragma multi_compile_fragment _ _ADDITIONAL_LIGHT_SHADOWS
#pragma multi_compile_fragment _ _REFLECTION_PROBE_BLENDING
#pragma multi_compile_fragment _ _REFLECTION_PROBE_BOX_PROJECTION
#pragma multi_compile_fragment _ _SHADOWS_SOFT
#pragma multi_compile_fragment _ _SCREEN_SPACE_OCCLUSION
#pragma multi_compile_fragment _ _DBUFFER_MRT1 _DBUFFER_MRT2 _DBUFFER_MRT3
#pragma multi_compile_fragment _ _LIGHT_LAYERS
#pragma multi_compile_fragment _ _LIGHT_COOKIES
#pragma multi_compile _ _FORWARD_PLUS
#pragma multi_compile_fragment _ _WRITE_RENDERING_LAYERS
// Unity defined keywords
#pragma multi_compile _ LIGHTMAP_SHADOW_MIXING
#pragma multi_compile _ SHADOWS_SHADOWMASK
#pragma multi_compile _ DIRLIGHTMAP_COMBINED
#pragma multi_compile _ LIGHTMAP_ON
#pragma multi_compile _ DYNAMICLIGHTMAP_ON
#pragma multi_compile_fragment _ LOD_FADE_CROSSFADE
#pragma multi_compile_fog
#pragma multi_compile_fragment _ DEBUG_DISPLAY
#pragma shader_feature_local_fragment _DOUBLE_SIDED
#pragma shader_feature_local_fragment _EMISSION
#pragma shader_feature_local_fragment _NORMAL_MAP
#pragma shader_feature_local_fragment _IS_FACE
#pragma shader_feature_local_fragment _SPECULAR
#pragma shader_feature_local_fragment _RIM
#pragma vertex ForwardPassVertex
#pragma fragment ForwardPassFragment
#include "GenshinInput.hlsl"
#include "GenshinForwardPass.hlsl"
ENDHLSL
}
Pass
{
Name "ShadowCaster"
Tags{"LightMode" = "ShadowCaster"}
ZWrite On
ZTest LEqual
ColorMask 0
Cull[_Cull]
HLSLPROGRAM
#pragma vertex ShadowPassVertex
#pragma fragment ShadowPassFragment
#include "Packages/com.unity.render-pipelines.universal/Shaders/LitInput.hlsl"
#include "Packages/com.unity.render-pipelines.universal/Shaders/ShadowCasterPass.hlsl"
ENDHLSL
}
Pass
{
Name "DepthOnly"
Tags{"LightMode" = "DepthOnly"}
ZWrite On
ColorMask R
Cull[_Cull]
HLSLPROGRAM
#pragma vertex DepthOnlyVertex
#pragma fragment DepthOnlyFragment
#include "Packages/com.unity.render-pipelines.universal/Shaders/LitInput.hlsl"
#include "Packages/com.unity.render-pipelines.universal/Shaders/DepthOnlyPass.hlsl"
ENDHLSL
}
Pass
{
Name "Outline"
Tags {"LightMode" = "SRPDefaultUnlit"}
Cull Front
HLSLPROGRAM
#pragma vertex OutlinePassVertex
#pragma fragment OutlinePassFragment
#include "GenshinInput.hlsl"
#include "GenshinOutlinePass.hlsl"
ENDHLSL
}
}
}

View File

@@ -0,0 +1,183 @@
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/DeclareDepthTexture.hlsl"
struct Attributes
{
float4 positionOS : POSITION;
float3 normalOS : NORMAL;
float4 tangentOS : TANGENT;
float4 color : COLOR;
float2 uv : TEXCOORD0;
float2 backUV : TEXCOORD1;
};
struct Varyings
{
float2 uv : TEXCOORD0;
float2 backUV : TEXCOORD1;
float3 positionWS : TEXCOORD2;
half3 tangentWS : TEXCOORD3;
half3 bitangentWS : TEXCOORD4;
half3 normalWS : TEXCOORD5;
float4 positionNDC : TEXCOORD6;
half4 color : COLOR;
float4 positionCS : SV_POSITION;
};
half GetShadow(Varyings input, half3 lightDirection, half aoFactor)
{
half NDotL = dot(input.normalWS, lightDirection);
half halfLambert = 0.5 * NDotL + 0.5;
half shadow = saturate(2.0 * halfLambert * aoFactor);
return lerp(shadow, 1.0, step(0.9, aoFactor));
}
half GetFaceShadow(Varyings input, half3 lightDirection)
{
half3 F = SafeNormalize(half3(_FaceDirection.x, 0.0, _FaceDirection.z));
half3 L = SafeNormalize(half3(lightDirection.x, 0.0, lightDirection.z));
half FDotL = dot(F, L);
half FCrossL = cross(F, L).y;
half2 shadowUV = input.uv;
shadowUV.x = lerp(shadowUV.x, 1.0 - shadowUV.x, step(0.0, FCrossL));
half faceShadowMap = SAMPLE_TEXTURE2D(_FaceLightMap, sampler_FaceLightMap, shadowUV).r;
half faceShadow = step(-0.5 * FDotL + 0.5 + _FaceShadowOffset, faceShadowMap);
half faceMask = SAMPLE_TEXTURE2D(_FaceShadow, sampler_FaceShadow, input.uv).a;
half maskedFaceShadow = lerp(faceShadow, 1.0, faceMask);
return maskedFaceShadow;
}
half3 GetShadowColor(half shadow, half material, half day)
{
int index = 4;
index = lerp(index, 1, step(0.2, material));
index = lerp(index, 2, step(0.4, material));
index = lerp(index, 0, step(0.6, material));
index = lerp(index, 3, step(0.8, material));
half rangeMin = 0.5 + _ShadowOffset - _ShadowSmoothness;
half rangeMax = 0.5 + _ShadowOffset;
half2 rampUV = half2(smoothstep(rangeMin, rangeMax, shadow), index / 10.0 + 0.5 * day + 0.05);
half3 shadowRamp = SAMPLE_TEXTURE2D(_ShadowRamp, sampler_ShadowRamp, rampUV);
half3 shadowColor = shadowRamp * lerp(_ShadowColor, 1.0, smoothstep(0.9, 1.0, rampUV.x));
shadowColor = lerp(shadowColor, 1.0, step(rangeMax, shadow));
return shadowColor;
}
half3 GetSpecular(Varyings input, half3 lightDirection, half3 albedo, half3 lightMap)
{
half3 V = GetWorldSpaceNormalizeViewDir(input.positionWS);
half3 H = SafeNormalize(lightDirection + V);
half NDotH = dot(input.normalWS, H);
half blinnPhong = pow(saturate(NDotH), _SpecularSmoothness);
half3 normalVS = TransformWorldToViewNormal(input.normalWS, true);
half2 matcapUV = 0.5 * normalVS.xy + 0.5;
half3 metalMap = SAMPLE_TEXTURE2D(_MetalMap, sampler_MetalMap, matcapUV);
half3 nonMetallic = step(1.1, lightMap.b + blinnPhong) * lightMap.r * _NonmetallicIntensity;
half3 metallic = blinnPhong * lightMap.b * albedo * metalMap * _MetallicIntensity;
half3 specular = lerp(nonMetallic, metallic, step(0.9, lightMap.r));
return specular;
}
half GetRim(Varyings input)
{
half3 normalVS = TransformWorldToViewNormal(input.normalWS, true);
float2 uv = input.positionNDC.xy / input.positionNDC.w;
float2 offset = float2(_RimOffset * normalVS.x / _ScreenParams.x, 0.0);
float depth = LinearEyeDepth(SampleSceneDepth(uv), _ZBufferParams);
float offsetDepth = LinearEyeDepth(SampleSceneDepth(uv + offset), _ZBufferParams);
half rim = smoothstep(0.0, _RimThreshold, offsetDepth - depth) * _RimIntensity;
half3 V = GetWorldSpaceNormalizeViewDir(input.positionWS);
half NDotV = dot(input.normalWS, V);
half fresnel = pow(saturate(1.0 - NDotV), 5.0);
return rim * fresnel;
}
Varyings ForwardPassVertex(Attributes input)
{
VertexPositionInputs vertexInput = GetVertexPositionInputs(input.positionOS.xyz);
VertexNormalInputs normalInput = GetVertexNormalInputs(input.normalOS, input.tangentOS);
Varyings output = (Varyings)0;
output.uv = TRANSFORM_TEX(input.uv, _BaseMap);
output.backUV = TRANSFORM_TEX(input.backUV, _BaseMap);
output.positionWS = vertexInput.positionWS;
output.tangentWS = normalInput.tangentWS;
output.bitangentWS = normalInput.bitangentWS;
output.normalWS = normalInput.normalWS;
output.positionNDC = vertexInput.positionNDC;
output.color = input.color;
output.positionCS = vertexInput.positionCS;
output.positionCS.xy += _ScreenOffset.xy * output.positionCS.w;
return output;
}
half4 ForwardPassFragment(Varyings input, FRONT_FACE_TYPE facing : FRONT_FACE_SEMANTIC) : SV_TARGET
{
#if _DOUBLE_SIDED
input.uv = lerp(input.uv, input.backUV, IS_FRONT_VFACE(facing, 0.0, 1.0));
#endif
half4 baseMap = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, input.uv);
half3 albedo = baseMap.rgb * _BaseColor.rgb;
half alpha = baseMap.a;
#if _IS_FACE
albedo = lerp(albedo, _FaceBlushColor.rgb, _FaceBlushStrength * alpha);
#endif
#if _NORMAL_MAP
half3x3 tangentToWorld = half3x3(input.tangentWS, input.bitangentWS, input.normalWS);
half4 normalMap = SAMPLE_TEXTURE2D(_NormalMap, sampler_NormalMap, input.uv);
half3 normalTS = UnpackNormal(normalMap);
half3 normalWS = TransformTangentToWorld(normalTS, tangentToWorld, true);
input.normalWS = normalWS;
#endif
Light mainLight = GetMainLight();
half3 lightDirection = SafeNormalize(mainLight.direction * _LightDirectionMultiplier);
half4 lightMap = SAMPLE_TEXTURE2D(_LightMap, sampler_LightMap, input.uv);
half material = lerp(lightMap.a, _CustomMaterialType, _UseCustomMaterialType);
#if _IS_FACE
half shadow = GetFaceShadow(input, lightDirection);
#else
half aoFactor = lightMap.g * input.color.r;
half shadow = GetShadow(input, lightDirection, aoFactor);
#endif
half3 shadowColor = GetShadowColor(shadow, material, _IsDay);
half3 specular = 0.0;
#if _SPECULAR
specular = GetSpecular(input, lightDirection, albedo, lightMap.rgb);
#endif
half3 emission = 0.0;
#if _EMISSION
emission = albedo * _EmissionIntensity * alpha;
#endif
half3 rim = 0.0;
#if _RIM
rim = albedo * GetRim(input);
#endif
half3 finalColor = albedo * shadowColor + specular + emission + rim;
half finalAlpha = 1.0;
return half4(finalColor, finalAlpha);
}

View File

@@ -0,0 +1,51 @@
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
CBUFFER_START(UnityPerMaterial)
float4 _BaseMap_ST;
half4 _BaseColor;
half _IsDay;
half _Cull;
half _SrcBlend;
half _DstBlend;
half4 _LightDirectionMultiplier;
half _ShadowOffset;
half _ShadowSmoothness;
half4 _ShadowColor;
half _UseCustomMaterialType;
half _CustomMaterialType;
half _EmissionIntensity;
half4 _FaceDirection;
half _FaceShadowOffset;
half4 _FaceBlushColor;
half _FaceBlushStrength;
half _SpecularSmoothness;
half _NonmetallicIntensity;
half _MetallicIntensity;
half _RimOffset;
half _RimThreshold;
half _RimIntensity;
half _UseSmoothNormal;
half _OutlineWidth;
half4 _OutlineWidthParams;
half _OutlineZOffset;
half4 _ScreenOffset;
half4 _OutlineColor;
half4 _OutlineColor2;
half4 _OutlineColor3;
half4 _OutlineColor4;
half4 _OutlineColor5;
CBUFFER_END
TEXTURE2D(_BaseMap); SAMPLER(sampler_BaseMap);
TEXTURE2D(_LightMap); SAMPLER(sampler_LightMap);
TEXTURE2D(_ShadowRamp); SAMPLER(sampler_ShadowRamp);
TEXTURE2D(_NormalMap); SAMPLER(sampler_NormalMap);
TEXTURE2D(_FaceLightMap); SAMPLER(sampler_FaceLightMap);
TEXTURE2D(_FaceShadow); SAMPLER(sampler_FaceShadow);
TEXTURE2D(_MetalMap); SAMPLER(sampler_MetalMap);

View File

@@ -0,0 +1,81 @@
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"
struct Attributes
{
float4 positionOS : POSITION;
float3 normalOS : NORMAL;
float4 tangentOS : TANGENT;
float4 color : COLOR;
float2 uv : TEXCOORD0;
float3 smoothNormal : TEXCOORD7;
};
struct Varyings
{
float2 uv : TEXCOORD0;
float4 positionCS : SV_POSITION;
};
float GetOutlineWidth(float positionVS_Z)
{
float fovFactor = 2.414 / UNITY_MATRIX_P[1].y;
float z = abs(positionVS_Z * fovFactor);
float4 params = _OutlineWidthParams;
float k = saturate((z - params.x) / (params.y - params.x));
float width = lerp(params.z, params.w, k);
return 0.01 * _OutlineWidth * width;
}
float4 GetOutlinePosition(VertexPositionInputs vertexInput, VertexNormalInputs normalInput, half4 vertexColor)
{
float z = vertexInput.positionVS.z;
float width = GetOutlineWidth(z) * vertexColor.a;
half3 normalVS = TransformWorldToViewNormal(normalInput.normalWS);
normalVS = SafeNormalize(half3(normalVS.xy, 0.0));
float3 positionVS = vertexInput.positionVS;
positionVS += 0.01 * _OutlineZOffset * SafeNormalize(positionVS);
positionVS += width * normalVS;
float4 positionCS = TransformWViewToHClip(positionVS);
positionCS.xy += _ScreenOffset.zw * positionCS.w;
return positionCS;
}
Varyings OutlinePassVertex(Attributes input)
{
VertexPositionInputs vertexInput = GetVertexPositionInputs(input.positionOS.xyz);
VertexNormalInputs normalInput = GetVertexNormalInputs(input.normalOS, input.tangentOS);
half3x3 tangentToWorld = half3x3(normalInput.tangentWS, normalInput.bitangentWS, normalInput.normalWS);
half3 normalTS = 2.0 * (input.smoothNormal - 0.5);
half3 normalWS = TransformTangentToWorld(normalTS, tangentToWorld, true);
normalInput.normalWS = lerp(normalInput.normalWS, normalWS, _UseSmoothNormal);
float4 positionCS = GetOutlinePosition(vertexInput, normalInput, input.color);
Varyings output = (Varyings)0;
output.uv = TRANSFORM_TEX(input.uv, _BaseMap);
output.positionCS = positionCS;
return output;
}
half4 OutlinePassFragment(Varyings input) : SV_TARGET
{
half4 lightMap = SAMPLE_TEXTURE2D(_LightMap, sampler_LightMap, input.uv);
half material = lightMap.a;
half4 color = _OutlineColor5;
color = lerp(color, _OutlineColor4, step(0.2, material));
color = lerp(color, _OutlineColor3, step(0.4, material));
color = lerp(color, _OutlineColor2, step(0.6, material));
color = lerp(color, _OutlineColor, step(0.8, material));
return color;
}

View File

@@ -0,0 +1,262 @@
Shader "URPGenshinPostProcess"
{
HLSLINCLUDE
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
#include "Packages/com.unity.render-pipelines.core/Runtime/Utilities/Blit.hlsl"
#pragma multi_compile_local_fragment _ _BLOOM_COLOR _BLOOM_BRIGHTNESS
#pragma multi_compile_local_fragment _ _TONEMAPPING
float2 _BlitTexture_TexelSize;
half _BloomThreshold;
half _BloomIntensity;
half4 _BloomWeights;
half4 _BloomColor;
half _BlurRadius;
half _Exposure;
half _Contrast;
half _Saturation;
TEXTURE2D(_BloomTextureA);
TEXTURE2D(_BloomTextureB);
TEXTURE2D(_BloomTextureC);
TEXTURE2D(_BloomTextureD);
const static int kernelSize = 9;
const static float kernelOffsets[9] = {
-4.0,
-3.0,
-2.0,
-1.0,
0.0,
1.0,
2.0,
3.0,
4.0,
};
const static float kernel[9] = {
0.01621622,
0.05405405,
0.12162162,
0.19459459,
0.22702703,
0.19459459,
0.12162162,
0.05405405,
0.01621622
};
half4 Prefilter(Varyings input) : SV_TARGET
{
float2 uv = UnityStereoTransformScreenSpaceTex(input.texcoord);
half4 color = SAMPLE_TEXTURE2D_X(_BlitTexture, sampler_LinearClamp, uv);
#if _BLOOM_BRIGHTNESS
half brightness = max(max(color.r, color.g), color.b);
color.rgb *= saturate(brightness - _BloomThreshold);
#else
color.rgb = max(color.rgb - _BloomThreshold, 0.0);
#endif
return color;
}
half4 GaussianBlur(float2 uv, float2 direction)
{
float2 offset = _BlurRadius * _BlitTexture_TexelSize * direction;
half4 color = 0.0;
UNITY_UNROLL
for (int i = 0; i < kernelSize; i++)
{
float2 sampleUV = uv + kernelOffsets[i] * offset;
color += kernel[i] * SAMPLE_TEXTURE2D_X(_BlitTexture, sampler_LinearClamp, sampleUV);
}
return color;
}
half4 HorizontalBlur1x(Varyings input) : SV_TARGET
{
float2 uv = UnityStereoTransformScreenSpaceTex(input.texcoord);
return GaussianBlur(uv, float2(1.0, 0.0));
}
half4 HorizontalBlur2x(Varyings input) : SV_TARGET
{
float2 uv = UnityStereoTransformScreenSpaceTex(input.texcoord);
return GaussianBlur(uv, float2(2.0, 0.0));
}
half4 VerticalBlur1x(Varyings input) : SV_TARGET
{
float2 uv = UnityStereoTransformScreenSpaceTex(input.texcoord);
return GaussianBlur(uv, float2(0.0, 1.0));
}
half4 VerticalBlur2x(Varyings input) : SV_TARGET
{
float2 uv = UnityStereoTransformScreenSpaceTex(input.texcoord);
return GaussianBlur(uv, float2(0.0, 2.0));
}
half4 Upsample(Varyings input) : SV_TARGET
{
float2 uv = UnityStereoTransformScreenSpaceTex(input.texcoord);
half4 color = 0.0;
half4 weights = _BloomWeights;
color += SAMPLE_TEXTURE2D_X(_BloomTextureA, sampler_LinearClamp, uv) * weights.x;
color += SAMPLE_TEXTURE2D_X(_BloomTextureB, sampler_LinearClamp, uv) * weights.y;
color += SAMPLE_TEXTURE2D_X(_BloomTextureC, sampler_LinearClamp, uv) * weights.z;
color += SAMPLE_TEXTURE2D_X(_BloomTextureD, sampler_LinearClamp, uv) * weights.w;
return color;
}
half3 Tonemap(half3 color)
{
half3 c0 = (1.36 * color + 0.047) * color;
half3 c1 = (0.93 * color + 0.56) * color + 0.14;
return saturate(c0 / c1);
}
half4 ColorGrading(Varyings input) : SV_TARGET
{
float2 uv = UnityStereoTransformScreenSpaceTex(input.texcoord);
half4 baseMap = SAMPLE_TEXTURE2D_X(_BlitTexture, sampler_LinearClamp, uv);
half3 color = baseMap.rgb;
half alpha = baseMap.a;
#if _BLOOM_COLOR || _BLOOM_BRIGHTNESS
// Bloom
half3 bloom = SAMPLE_TEXTURE2D_X(_BloomTextureA, sampler_LinearClamp, uv).rgb;
bloom *= _BloomIntensity * _BloomColor.rgb;
color += bloom;
#endif
// Exposure
color *= _Exposure;
#if _TONEMAPPING
// Tonemapping
color = Tonemap(color);
#endif
// Contrast
half3 colorLog = LinearToLogC(color);
colorLog = lerp(ACEScc_MIDGRAY, colorLog, _Contrast);
color = LogCToLinear(colorLog);
// Saturation
half luma = dot(color, half3(0.2126, 0.7152, 0.0722));
color = lerp(luma, color, _Saturation);
return float4(color, alpha);
}
ENDHLSL
Subshader
{
Tags { "RenderPipeline" = "UniversalPipeline" }
ZWrite Off ZTest Always Blend Off Cull Off
Pass
{
Name "Blit"
HLSLPROGRAM
#pragma vertex Vert
#pragma fragment FragBilinear
ENDHLSL
}
Pass
{
Name "BloomPrefilter"
HLSLPROGRAM
#pragma vertex Vert
#pragma fragment Prefilter
ENDHLSL
}
Pass
{
Name "BloomHorizontalBlur1x"
HLSLPROGRAM
#pragma vertex Vert
#pragma fragment HorizontalBlur1x
ENDHLSL
}
Pass
{
Name "BloomHorizontalBlur2x"
HLSLPROGRAM
#pragma vertex Vert
#pragma fragment HorizontalBlur2x
ENDHLSL
}
Pass
{
Name "BloomVerticalBlur1x"
HLSLPROGRAM
#pragma vertex Vert
#pragma fragment VerticalBlur1x
ENDHLSL
}
Pass
{
Name "BloomVerticalBlur2x"
HLSLPROGRAM
#pragma vertex Vert
#pragma fragment VerticalBlur2x
ENDHLSL
}
Pass
{
Name "BloomUpsample"
HLSLPROGRAM
#pragma vertex Vert
#pragma fragment Upsample
ENDHLSL
}
Pass
{
Name "ColorGrading"
HLSLPROGRAM
#pragma vertex Vert
#pragma fragment ColorGrading
ENDHLSL
}
}
}

View File

@@ -0,0 +1,114 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &11400000
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: bf2edee5c58d82540a51f03df9d42094, type: 3}
m_Name: URPAsset
m_EditorClassIdentifier:
k_AssetVersion: 11
k_AssetPreviousVersion: 11
m_RendererType: 1
m_RendererData: {fileID: 0}
m_RendererDataList:
- {fileID: 11400000, guid: c40be3174f62c4acf8c1216858c64956, type: 2}
m_DefaultRendererIndex: 0
m_RequireDepthTexture: 1
m_RequireOpaqueTexture: 0
m_OpaqueDownsampling: 1
m_SupportsTerrainHoles: 1
m_SupportsHDR: 1
m_HDRColorBufferPrecision: 1
m_MSAA: 1
m_RenderScale: 1
m_UpscalingFilter: 0
m_FsrOverrideSharpness: 0
m_FsrSharpness: 0.92
m_EnableLODCrossFade: 1
m_LODCrossFadeDitheringType: 1
m_ShEvalMode: 0
m_MainLightRenderingMode: 1
m_MainLightShadowsSupported: 1
m_MainLightShadowmapResolution: 4096
m_AdditionalLightsRenderingMode: 1
m_AdditionalLightsPerObjectLimit: 8
m_AdditionalLightShadowsSupported: 1
m_AdditionalLightsShadowmapResolution: 4096
m_AdditionalLightsShadowResolutionTierLow: 128
m_AdditionalLightsShadowResolutionTierMedium: 256
m_AdditionalLightsShadowResolutionTierHigh: 512
m_ReflectionProbeBlending: 1
m_ReflectionProbeBoxProjection: 1
m_ShadowDistance: 150
m_ShadowCascadeCount: 4
m_Cascade2Split: 0.25
m_Cascade3Split: {x: 0.1, y: 0.3}
m_Cascade4Split: {x: 0.067, y: 0.2, z: 0.467}
m_CascadeBorder: 0.1
m_ShadowDepthBias: 1
m_ShadowNormalBias: 0
m_AnyShadowsSupported: 1
m_SoftShadowsSupported: 1
m_ConservativeEnclosingSphere: 0
m_NumIterationsEnclosingSphere: 64
m_SoftShadowQuality: 3
m_AdditionalLightsCookieResolution: 4096
m_AdditionalLightsCookieFormat: 4
m_UseSRPBatcher: 1
m_SupportsDynamicBatching: 0
m_MixedLightingSupported: 1
m_SupportsLightCookies: 1
m_SupportsLightLayers: 0
m_DebugLevel: 0
m_StoreActionsOptimization: 0
m_EnableRenderGraph: 0
m_UseAdaptivePerformance: 1
m_ColorGradingMode: 0
m_ColorGradingLutSize: 32
m_UseFastSRGBLinearConversion: 0
m_SupportDataDrivenLensFlare: 1
m_ShadowType: 1
m_LocalShadowsSupported: 0
m_LocalShadowsAtlasResolution: 256
m_MaxPixelLights: 0
m_ShadowAtlasResolution: 256
m_VolumeFrameworkUpdateMode: 0
m_Textures:
blueNoise64LTex: {fileID: 2800000, guid: e3d24661c1e055f45a7560c033dbb837, type: 3}
bayerMatrixTex: {fileID: 2800000, guid: f9ee4ed84c1d10c49aabb9b210b0fc44, type: 3}
m_PrefilteringModeMainLightShadows: 1
m_PrefilteringModeAdditionalLight: 4
m_PrefilteringModeAdditionalLightShadows: 1
m_PrefilterXRKeywords: 0
m_PrefilteringModeForwardPlus: 1
m_PrefilteringModeDeferredRendering: 1
m_PrefilteringModeScreenSpaceOcclusion: 1
m_PrefilterDebugKeywords: 0
m_PrefilterWriteRenderingLayers: 0
m_PrefilterHDROutput: 0
m_PrefilterSSAODepthNormals: 0
m_PrefilterSSAOSourceDepthLow: 0
m_PrefilterSSAOSourceDepthMedium: 0
m_PrefilterSSAOSourceDepthHigh: 0
m_PrefilterSSAOInterleaved: 0
m_PrefilterSSAOBlueNoise: 0
m_PrefilterSSAOSampleCountLow: 0
m_PrefilterSSAOSampleCountMedium: 0
m_PrefilterSSAOSampleCountHigh: 0
m_PrefilterDBufferMRT1: 0
m_PrefilterDBufferMRT2: 0
m_PrefilterDBufferMRT3: 0
m_PrefilterSoftShadowsQualityLow: 0
m_PrefilterSoftShadowsQualityMedium: 0
m_PrefilterSoftShadowsQualityHigh: 0
m_PrefilterSoftShadows: 0
m_PrefilterScreenCoord: 0
m_PrefilterNativeRenderPass: 0
m_ShaderVariantLogLevel: 0
m_ShadowCascades: 1

View File

@@ -0,0 +1,126 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &-9000257669431859462
MonoBehaviour:
m_ObjectHideFlags: 3
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 3dcd9373f10f3874a949a399856fa187, type: 3}
m_Name: PostProcessVolume
m_EditorClassIdentifier:
active: 1
<displayName>k__BackingField:
enabled:
m_OverrideState: 1
m_Value: 1
useBloom:
m_OverrideState: 1
m_Value: 1
bloomThreshold:
m_OverrideState: 1
m_Value: 0.7
bloomScatter:
m_OverrideState: 1
m_Value: 0.7
bloomIntensity:
m_OverrideState: 1
m_Value: 1.5
blurIterations:
m_OverrideState: 1
m_Value: 4
blurRadius:
m_OverrideState: 1
m_Value: 2
blurDownSampleScale:
m_OverrideState: 1
m_Value: 0.5
useTonemapping:
m_OverrideState: 1
m_Value: 1
exposure:
m_OverrideState: 1
m_Value: 1
contrast:
m_OverrideState: 1
m_Value: 0
saturation:
m_OverrideState: 1
m_Value: 0
--- !u!114 &-4271347055193007441
MonoBehaviour:
m_ObjectHideFlags: 3
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 76dafa8030f42e648a782b9ab29b6ccb, type: 3}
m_Name: BloomVolume
m_EditorClassIdentifier:
active: 1
mode:
m_OverrideState: 1
m_Value: 1
threshold:
m_OverrideState: 1
m_Value: 0.7
intensity:
m_OverrideState: 1
m_Value: 1.5
weights:
m_OverrideState: 1
m_Value: {x: 0.1, y: 0.2, z: 0.3, w: 0.4}
color:
m_OverrideState: 1
m_Value: {r: 1, g: 1, b: 1, a: 1}
blurRadius:
m_OverrideState: 1
m_Value: 2
downSampleScale:
m_OverrideState: 1
m_Value: 0.5
--- !u!114 &-2939227666677998292
MonoBehaviour:
m_ObjectHideFlags: 3
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: b065805f0c20d0946afc33f94ae2f89d, type: 3}
m_Name: ColorGradingVolume
m_EditorClassIdentifier:
active: 1
useTonemapping:
m_OverrideState: 1
m_Value: 1
exposure:
m_OverrideState: 1
m_Value: 1.05
contrast:
m_OverrideState: 1
m_Value: 0
saturation:
m_OverrideState: 1
m_Value: 0
--- !u!114 &11400000
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: d7fd9488000d3734a9e00ee676215985, type: 3}
m_Name: URPGenshinVolumeProfile
m_EditorClassIdentifier:
components:
- {fileID: -4271347055193007441}
- {fileID: -2939227666677998292}

View File

@@ -0,0 +1,114 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &-6004539541889354362
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: a75e1afdd7c0c4644bf9681fa79463d8, type: 3}
m_Name: PostProcessFeature
m_EditorClassIdentifier:
m_Active: 1
m_RenderPassEvent: 600
m_Shader: {fileID: 4800000, guid: b0d6c4974782c8942a7269d3635777ce, type: 3}
--- !u!114 &-1878332245247344467
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: f62c9c65cf3354c93be831c8bc075510, type: 3}
m_Name: SSAO
m_EditorClassIdentifier:
m_Active: 0
m_Settings:
AOMethod: 1
Downsample: 0
AfterOpaque: 0
Source: 1
NormalSamples: 1
Intensity: 0.5
DirectLightingStrength: 0.25
Radius: 0.25
Samples: 0
BlurQuality: 0
Falloff: 100
SampleCount: -1
m_BlueNoise256Textures:
- {fileID: 2800000, guid: 36f118343fc974119bee3d09e2111500, type: 3}
- {fileID: 2800000, guid: 4b7b083e6b6734e8bb2838b0b50a0bc8, type: 3}
- {fileID: 2800000, guid: c06cc21c692f94f5fb5206247191eeee, type: 3}
- {fileID: 2800000, guid: cb76dd40fa7654f9587f6a344f125c9a, type: 3}
- {fileID: 2800000, guid: e32226222ff144b24bf3a5a451de54bc, type: 3}
- {fileID: 2800000, guid: 3302065f671a8450b82c9ddf07426f3a, type: 3}
- {fileID: 2800000, guid: 56a77a3e8d64f47b6afe9e3c95cb57d5, type: 3}
m_Shader: {fileID: 4800000, guid: 0849e84e3d62649e8882e9d6f056a017, type: 3}
--- !u!114 &11400000
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: de640fe3d0db1804a85f9fc8f5cadab6, type: 3}
m_Name: URPRendererData
m_EditorClassIdentifier:
debugShaders:
debugReplacementPS: {fileID: 4800000, guid: cf852408f2e174538bcd9b7fda1c5ae7,
type: 3}
hdrDebugViewPS: {fileID: 4800000, guid: 573620ae32aec764abd4d728906d2587, type: 3}
m_RendererFeatures:
- {fileID: -1878332245247344467}
- {fileID: -6004539541889354362}
m_RendererFeatureMap: adc0de57c6d2eee586f5e59d1997abac
m_UseNativeRenderPass: 0
postProcessData: {fileID: 11400000, guid: 41439944d30ece34e96484bdb6645b55, type: 2}
xrSystemData: {fileID: 11400000, guid: 60e1133243b97e347b653163a8c01b64, type: 2}
shaders:
blitPS: {fileID: 4800000, guid: c17132b1f77d20942aa75f8429c0f8bc, type: 3}
copyDepthPS: {fileID: 4800000, guid: d6dae50ee9e1bfa4db75f19f99355220, type: 3}
screenSpaceShadowPS: {fileID: 0}
samplingPS: {fileID: 4800000, guid: 04c410c9937594faa893a11dceb85f7e, type: 3}
stencilDeferredPS: {fileID: 4800000, guid: e9155b26e1bc55942a41e518703fe304, type: 3}
fallbackErrorPS: {fileID: 4800000, guid: e6e9a19c3678ded42a3bc431ebef7dbd, type: 3}
fallbackLoadingPS: {fileID: 4800000, guid: 7f888aff2ac86494babad1c2c5daeee2, type: 3}
materialErrorPS: {fileID: 4800000, guid: 5fd9a8feb75a4b5894c241777f519d4e, type: 3}
coreBlitPS: {fileID: 4800000, guid: 93446b5c5339d4f00b85c159e1159b7c, type: 3}
coreBlitColorAndDepthPS: {fileID: 4800000, guid: d104b2fc1ca6445babb8e90b0758136b,
type: 3}
blitHDROverlay: {fileID: 4800000, guid: a89bee29cffa951418fc1e2da94d1959, type: 3}
cameraMotionVector: {fileID: 4800000, guid: c56b7e0d4c7cb484e959caeeedae9bbf,
type: 3}
objectMotionVector: {fileID: 4800000, guid: 7b3ede40266cd49a395def176e1bc486,
type: 3}
dataDrivenLensFlare: {fileID: 4800000, guid: 6cda457ac28612740adb23da5d39ea92,
type: 3}
m_AssetVersion: 2
m_OpaqueLayerMask:
serializedVersion: 2
m_Bits: 4294967295
m_TransparentLayerMask:
serializedVersion: 2
m_Bits: 4294967295
m_DefaultStencilState:
overrideStencilState: 0
stencilReference: 0
stencilCompareFunction: 8
passOperation: 2
failOperation: 0
zFailOperation: 0
m_ShadowTransparentReceive: 1
m_RenderingMode: 0
m_DepthPrimingMode: 0
m_CopyDepthMode: 2
m_AccurateGbufferNormals: 0
m_IntermediateTextureMode: 1

View File

@@ -0,0 +1,41 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &11400000
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 2ec995e51a6e251468d2a3fd8a686257, type: 3}
m_Name: UniversalRenderPipelineGlobalSettings
m_EditorClassIdentifier:
k_AssetVersion: 3
m_RenderingLayerNames:
- Light Layer default
- Light Layer 1
- Light Layer 2
- Light Layer 3
- Light Layer 4
- Light Layer 5
- Light Layer 6
- Light Layer 7
m_ValidRenderingLayers: 255
lightLayerName0: Light Layer default
lightLayerName1: Light Layer 1
lightLayerName2: Light Layer 2
lightLayerName3: Light Layer 3
lightLayerName4: Light Layer 4
lightLayerName5: Light Layer 5
lightLayerName6: Light Layer 6
lightLayerName7: Light Layer 7
m_StripDebugVariants: 1
m_StripUnusedPostProcessingVariants: 1
m_StripUnusedVariants: 1
m_StripUnusedLODCrossFadeVariants: 1
m_StripScreenCoordOverrideVariants: 1
supportRuntimeDebugDisplay: 0
m_ShaderVariantLogLevel: 0
m_ExportShaderVariants: 1

View File

@@ -0,0 +1,406 @@
# 3DGS 渲染集成测试与 Renderer 正式接入计划
日期2026-04-10
## 1. 文档定位
这份计划承接已经完成的 `GaussianSplat 资源 / Artifact / ResourceManager / GPU cache` 正式化工作,正式进入下一阶段:
1.`tests/Rendering/integration` 中把 3DGS 正式渲染出来
2. 为引擎 renderer 增加一条正式的 3DGS 场景渲染路径
这份计划的首要目标不是 editor 接入,也不是 3DGS 编辑工具,而是:
1.`room.ply -> GaussianSplat -> Scene -> Renderer -> GT 比对` 成为一条正式、可回归、可维护的链路
这份计划明确不做的事:
1. 不做 editor 里的 3DGS Inspector / gizmo / 交互编辑
2. 不做 cutout、selection、导出、编辑态 GPU buffer
3. 不在这一轮提前做 streaming / LOD / chunk 压缩升级
4. 不把 3DGS 临时塞进 `MeshRenderer``VolumeRenderer` 路径里凑合出图
---
## 2. 当前基线与已具备能力
当前已完成的正式基础:
1. `.ply -> GaussianSplatImporter -> main.xcgsplat -> GaussianSplatLoader -> ResourceManager` 已打通
2. `RenderResourceCache` 已具备 `GaussianSplat` 静态 GPU 驻留能力
3. `room.ply` 已可稳定导入、复用 artifact、并通过测试验证
当前 renderer / RHI 已具备的关键能力:
1. `StructuredBuffer / RawBuffer / RWStructuredBuffer / RWRawBuffer` shader 资源描述能力已具备
2. compute shader 与 `Dispatch` 已具备
3. `Draw(vertexCount, instanceCount, ...)` 已具备,可以走 instanced quad 而不需要先补新的 RHI 接口
4. renderer 已有成熟的 integration test 模式:
1. 一个场景一个 target
2. 真正创建 `Scene + SceneRenderer + RenderSurface`
3. 输出 PPM 并和 `GT.ppm` 比对
5. 当前已有可借鉴的参考:
1. [mvs/3DGS-Unity/Runtime/GaussianSplatRenderer.cs](D:/Xuanchi/Main/XCEngine/mvs/3DGS-Unity/Runtime/GaussianSplatRenderer.cs)
2. [tests/Rendering/integration/volume_scene](D:/Xuanchi/Main/XCEngine/tests/Rendering/integration/volume_scene)
3. [tests/Rendering/integration/backpack_lit_scene](D:/Xuanchi/Main/XCEngine/tests/Rendering/integration/backpack_lit_scene)
---
## 3. 当前还缺什么
虽然资源链已经完整,但 renderer 侧现在仍然没有正式的 3DGS 路径:
1. 没有 `GaussianSplatRendererComponent`
2. 没有 `VisibleGaussianSplatItem`
3. `RenderSceneData` 没有 `visibleGaussianSplats`
4. `RenderSceneExtractor / RenderSceneUtility` 不会提取 3DGS 场景对象
5. `BuiltinForwardPipeline` 没有 3DGS 专用 pass
6. 还没有用于 3DGS 的 transient GPU working buffers
1. sort distance / key
2. view-data
3. offscreen accumulation / compose target
7. `tests/Rendering/integration` 还没有 3DGS 场景测试
所以这一步不是“写个测试 main.cpp 就行”,而是要正式把 3DGS 纳入 renderer 架构。
---
## 4. 本轮核心架构决策
### 4.1 必须引入专用场景组件 `GaussianSplatRendererComponent`
不允许:
1.`MeshRendererComponent` 直接持有 `GaussianSplat`
2. 复用 `VolumeRendererComponent`
3. 在测试里绕过组件层,直接把 `GaussianSplat` 塞进 pass
正式方案应为:
1. 新增 `GaussianSplatRendererComponent`
2. 它持有:
1. `ResourceHandle<GaussianSplat>`
2. `Material*` 或正式 `Material` 句柄
3. 运行时参数例如缩放、透明度倍率、SH 阶数、排序策略等
原因:
1. 这条路径是专用渲染 primitive不是 mesh也不是 volume
2. 场景提取、渲染排序、后续 editor 支持,都需要一个正式组件边界
3. 这和参考项目中单组件 `GaussianSplatRenderer` 的职责边界是一致的
### 4.2 3DGS 必须走专用 `VisibleGaussianSplatItem`
应新增:
1. `Rendering/FrameData/VisibleGaussianSplatItem.h`
2. `RenderSceneData.visibleGaussianSplats`
3. `AppendVisibleGaussianSplatsForGameObject(...)`
原因:
1. 3DGS 与 mesh、volume 的排序依据和 GPU 资源都不同
2. 把它塞到 `visibleItems``visibleVolumes` 里只会制造更多特判
### 4.3 3DGS 必须作为 `BuiltinForwardPipeline` 中的一条正式 pass
不允许:
1. 直接把 3DGS 逻辑硬塞进 `DrawVisibleItems`
2. 把 3DGS 伪装成普通 forward lit / unlit surface
3. 在测试代码里直接手写 command list 绕过 renderer
正式方案应为:
1. 新增 `BuiltinGaussianSplatPass`
2. 它在 `BuiltinForwardPipeline` 中作为一条显式阶段执行
3. 先按当前 pipeline 约束放在:
1. `Opaque`
2. `Skybox`
3. `GaussianSplat`
4. `Volumetric`
5. `Transparent`
这一步的理由是:
1. 参考 Unity MVS 的落点本质上属于 `BeforeForwardAlpha`
2. 3DGS 需要依赖 opaque depth但又不应混入普通 mesh transparent 队列
3. 当前 pipeline 还没有更通用的 feature graph因此先做一条正式内建 pass 是合理路径
### 4.4 静态 GPU 资产与每帧 working set 必须分层
已经完成的静态层:
1. `RenderResourceCache::CachedGaussianSplat`
1. positions
2. other
3. color
4. sh
5. chunks
下一步必须新增的是每帧 / 每相机 working set而不是污染静态 cache
1. sort distance buffer
2. sort key / order buffer
3. view-data buffer
4. splat accumulation render target
结论:
1. `RenderResourceCache` 继续负责静态 asset residency
2. `BuiltinGaussianSplatPass` 或其专属 surface cache 负责 transient working resources
### 4.5 第一张图必须先在 rendering integration test 里落地
这一轮的第一个正式验收点不是 editor 里看到 3DGS而是
1. 新增 `tests/Rendering/integration/gaussian_splat_scene`
2. 使用真实 `room.ply`
3. 跑通过至少 D3D12 / OpenGL / Vulkan 的 GT 比对
原因:
1. 这能把资源链问题、scene 提取问题、shader 问题、pass 问题一次性锁进正式回归
2. editor 接入如果先做,调试成本会更高,问题边界也更模糊
---
## 5. 渲染技术路线
### 5.1 第一轮渲染方案
当前最合适的正式方案是:
1. 使用 instanced quad 绘制 splat
2. 顶点着色器通过 `SV_InstanceID / gl_InstanceID``StructuredBuffer` 读取 per-splat 数据
3. compute 负责:
1. 计算相机空间深度或排序 key
2. 生成 draw 顺序
3. 预计算 view-data
4. graphics pass 负责:
1. 深度测试
2. splat 展开
3. 累积到专用 render target
5. fullscreen compose pass 负责把结果混合回主场景颜色
这条路线的优点:
1. 与参考 `GaussianSplatRenderer.cs` 的总体思路一致
2. 与现有 RHI 能力匹配,不需要新发明 RHI 接口
3. 可以自然落到当前 built-in pipeline 中
### 5.2 关于排序
第一轮正式实现就应该有排序,不建议无排序硬上 GT。
原因:
1. 3DGS 本质上依赖近似正确的后向前顺序
2. 如果先做无排序integration test 的 GT 价值会很差
3. 后面再补排序,很可能把第一版 shader / pass 全打碎
因此建议:
1. 第一张 integration 图就包含 compute 排序链
2. 如果排序系统需要简化,也只能简化为“全局每帧排序”,不能退化到“不排序”
### 5.3 关于 SH 与颜色
当前 cooked-v1 中已有:
1. `color = SH0ToColor(dc0) + opacity`
2. `sh = f_rest_0..44`
正式阶段建议:
1. 第一版 pass 即支持当前 artifact 语义
2. 不为了追求和 Unity 一模一样的颜色纹理缓存形态,先回头推翻当前 cooked-v1
3. 如果实际实现中发现 `color buffer` 明显不适合后续长期演进,再做正式 schema 升级,而不是临时特判
也就是说:
1. 当前目标是先让 `room.ply` 通过正式 renderer 路径稳定出图
2. 不是在出第一张图之前就重做第二版 artifact 格式
---
## 6. Renderer 需要做的正式改动
### Phase 1场景对象与提取层正式接入
目标:
1. 让 Scene 能正式携带 3DGS 渲染对象
任务:
1. 新增 `GaussianSplatRendererComponent`
2. 新增 `VisibleGaussianSplatItem`
3. 扩展 `RenderSceneData`
4. 扩展 `RenderSceneUtility / RenderSceneExtractor`
5. 补对应 unit tests覆盖
1. 提取
2. culling mask
3. render queue
4. 排序稳定性
验收标准:
1. `RenderSceneExtractor` 能输出 `visibleGaussianSplats`
2. 不污染 mesh / volume 路径
### Phase 23DGS pass 的 GPU working set 正式化
目标:
1. 让 3DGS 渲染时所需的每帧资源有正式边界
任务:
1.`BuiltinGaussianSplatPass` 设计 transient resource 集合
2. 明确静态 cache 与 transient working set 的所有权
3. 实现:
1. sort distances / keys
2. order buffer
3. view-data buffer
4. accumulation target
4. 补 cache / pass smoke tests
验收标准:
1. pass 在没有 editor 的情况下独立准备好运行所需 GPU 资源
2. 不把每帧资源塞回 `RenderResourceCache`
### Phase 3BuiltinGaussianSplatPass 正式落地
目标:
1. 在 renderer 中画出第一张 3DGS 正式图
任务:
1. 新增内建 shader / material 资产:
1. sort compute
2. view-data compute
3. splat draw
4. compose
2. 新增 `BuiltinGaussianSplatPass`
3. 接入 `BuiltinForwardPipeline`
4. 定义与主深度、主颜色的交互规则
验收标准:
1. pass 不绕过 `SceneRenderer`
2. pass 不绕过 `RenderSceneData`
3. `BuiltinForwardPipeline` 的阶段顺序清晰且可测试
### Phase 4rendering integration test 正式落地
目标:
1. 把 3DGS 第一张图纳入 GT 回归
任务:
1. 新增 `tests/Rendering/integration/gaussian_splat_scene`
2. 使用真实样本:
1. [room.ply](D:/Xuanchi/Main/XCEngine/mvs/3DGS-Unity/room.ply)
3. 复制运行所需资源到测试输出目录
4. 建立 GT 基线图
5. 至少覆盖:
1. D3D12
2. OpenGL
3. Vulkan
建议场景内容:
1. 相机
2. 黑或深灰背景
3. 单个 `GaussianSplatRendererComponent`
4. 不引入外部灯光依赖作为第一张图前提
原因:
1. 3DGS 的基础显示不应依赖当前主光 / 多光源系统是否正确
2. 第一张图应尽量减少无关变量
### Phase 5测试收口与回归闭环
任务:
1. 跑通:
1. `gaussian_splat_tests`
2. `rendering_unit_tests`
3. `rendering_integration_gaussian_splat_scene`
4. 相关既有 rendering integration tests
2. 修复跨后端 shader / 资源绑定差异
3. 输出阶段总结
验收标准:
1. 3DGS 进入正式 rendering regression 集
2. 不存在“只有 editor 里能看,测试里没有”的非正式路径
---
## 7. 需要提前注意的风险
### 7.1 当前 command list 没有 `DrawProcedural`
这不是 blocker。
当前可行正式方案是:
1. 使用固定 6 顶点 quad
2. `Draw(6, splatCount, 0, 0)`
3. 顶点阶段通过 instance id 读 buffer
也就是说,不需要先为了 3DGS 强行扩展新的 RHI draw API。
### 7.2 3DGS 与 volume 都属于“专用透明路径”
这意味着当前 built-in pipeline 中将出现:
1. `VolumetricPass`
2. `GaussianSplatPass`
两者在这一轮可以并存,但必须保证:
1. 顺序明确
2. 资源边界明确
3. 不相互复用错误的数据结构
如果过程中发现当前 `BuiltinForwardPipeline` 已经开始过载,就应该正式抽出更清晰的内建 feature 组织层,而不是继续堆 if 分支。
### 7.3 integration test 先用真实 `room.ply`
这意味着:
1. 样本量不小
2. shader / sort / view-data 任何一步出错,画面都会直接坏掉
但这恰恰是好事,因为:
1. 第一张图就能真实暴露架构问题
2. 不会被“用一个简化假样本先糊过去”的临时路线误导
---
## 8. 本轮完成标志
当以下条件同时成立,这份计划才算完成:
1. `GaussianSplatRendererComponent` 已存在且被 `SceneRenderer` 正式消费
2. `RenderSceneData.visibleGaussianSplats` 已成为正式帧数据
3. `BuiltinGaussianSplatPass` 已正式接入 `BuiltinForwardPipeline`
4. `room.ply` 能在 `tests/Rendering/integration/gaussian_splat_scene` 中稳定渲染
5. 至少 D3D12 / OpenGL / Vulkan 的 GT 回归通过
6. 现有 `gaussian_splat_tests` 与相关 rendering tests 不被破坏
7. 不存在任何 render pass 直接读取 source `.ply` 的旁路
---
## 9. 一句话结论
3DGS 的下一步不是直接去 editor 里“先看见再说”,而是先在 renderer 正式增加一条 `GaussianSplatRendererComponent -> VisibleGaussianSplatItem -> BuiltinGaussianSplatPass -> rendering integration GT` 的闭环路径;
只有这条闭环成立,后面的 editor 显示、选择、材质面板和 SRP 演进才有可靠基础。

View File

@@ -1,6 +1,7 @@
# Assets目录Watcher与自动导入刷新计划
日期: 2026-04-10
状态: 已完成并归档
## 1. 背景

View File

@@ -0,0 +1,736 @@
# Library 统一 Artifact 容器与多后端 Shader 缓存重构计划
日期2026-04-11
## 0. 当前执行状态2026-04-11 更新)
当前整体进度:约 `88%`
已完成:
1. `ArtifactContainer` 读写、校验、虚拟 entry path 已落地,并有核心单测覆盖。
2. `ShaderCompilationCache` 已正式进入 `Library` 体系,`D3D12` 已有稳定命中路径与单测覆盖。
3. `Texture / Material / Shader / Model / Volume / GaussianSplat` 已切到单文件 artifact container 写入与读取语义。
4. `Model` 子资源已从真实 sibling 文件切到 container entry`AssetDatabase` 子资源解析优先走 entry 解析。
5. `XCUI` 文档 artifact 也已补齐为单文件 container避免 `Library` 内继续残留旧式 artifact directory 生产路径。
6. 新格式写入路径已经统一到 `Library/Artifacts/<shard>/<artifactKey>.<ext>`,旧格式保持读取兼容。
7. `artifact_inspect` 已落地,可直接查看 container entry 列表并导出指定 entry payload。
已验证:
1. `ArtifactContainer.*`
2. `ShaderCompilationCache.*`
3. `TextureLoader` 关键 artifact 回归
4. `MaterialLoader` 关键 artifact / reimport 回归
5. `ShaderLoader` 关键 artifact / dependency 回归
6. `ModelLoader / ModelImportPipeline / MeshLoader` 关键 container-entry 回归
7. `VolumeFieldLoader` 关键 artifact 回归
8. `GaussianSplatLoader.*`
9. `UIDocumentLoader.*`
10. `UISchemaDocument.*`
尚未收口:
1. `ArtifactDB` 还没有彻底升级到“显式 entry 元数据” schema当前仍保留一部分旧字段语义用于兼容迁移。
2. `Library` 清理工具、最终性能报告、旧格式彻底收口文档还没完成。
3. `Vulkan / OpenGL` 目前已经接入统一 shader cache 框架边界,但还没有做到和 `D3D12` 同等级的完整产物复用能力。
## 1. 文档定位
这份计划不是在讨论“把 `Library/Artifacts` 的目录名字改得更像 Unity”。
这份计划要解决的是更底层的问题:
1. 现在的 artifact 基本存储单位是“一个目录 + 多个真实文件”,不是“一个统一工件”。
2. 复杂资源类型一增加,导入器、加载器、子资源解析、测试都会继续把这种多文件假设扩散到更多模块。
3. shader 运行时编译缓存虽然已经开始建立,但还没有上升为整个 `Library` 体系中的正式基础设施。
因此,本计划的目标是正式把 `Library` 重构为一套统一的缓存架构:
1. 导入产物统一落成单文件 artifact container。
2. 子资源从“磁盘上的 sibling 文件”变成“artifact 内部 entry”。
3. shader 编译产物纳入 `Library` 正式缓存体系,并支持多后端。
4. 整个系统继续保留现有 `AssetDatabase / ResourceManager / ArtifactDB` 的架构思路,不搞一套平行新系统。
---
## 2. 当前状态与根本问题
### 2.1 当前已经成立的基础
目前 `Library` 体系已经具备以下基础能力:
1. `assets.db``artifacts.db` 已经位于 `Library` 根目录。
2. `AssetDatabase` 已经负责 source asset 扫描、依赖签名、artifact 复用和重导。
3. 各资源导入器已经能够把 source asset 写入 artifact。
4. shader 编译缓存已经有初步命中机制,且收益已经被实际验证过。
### 2.2 当前真正的问题
当前问题不在于“有没有缓存”,而在于“缓存的基本抽象还不够统一”。
以模型为例,当前一个导入结果并不是一个真正的工件,而是一整个目录:
1. `main.xcmodel`
2. `mesh_0.xcmesh`
3. `material_0.xcmat`
4. `texture_0.xctex`
5. `subassets.tsv`
这意味着:
1. artifact 的语义是“目录”,不是“文件”。
2. loader 和测试可以直接依赖某个 sibling 文件的真实路径。
3. 子资源关系是通过外部文件名和外部 manifest 维持的。
4. 新增资源类型时,导入器很容易继续沿用“往 artifact 目录里再塞几个文件”的模式。
### 2.3 为什么这个问题越晚改越难
随着资源类型增加,当前模式会继续扩散到:
1. `Model`
2. `Shader`
3. `Volume`
4. `GaussianSplat`
5. 未来的 `AnimationClip / Skeleton / Audio / VFX / Prefab-like Asset`
越往后,耦合会越来越深:
1. 更多 importer 会直接生成多个磁盘文件。
2. 更多 loader 会默认自己拿到的就是磁盘路径。
3. 更多测试会把 artifact 文件名结构视为系统契约。
4. 后面再做统一 container 时,每种资源都要单独回迁一次。
所以这次重构如果要做,越早越值。
---
## 3. 重构目标
本轮重构的最终目标如下:
1. `Library` 中“一个 artifactKey 对应一个单文件 container”。
2. artifact container 内部可以保存主资源和多个子资源 entry。
3. `ArtifactDB` 记录的是“工件文件 + 条目索引信息”,不再把目录结构当成核心语义。
4. loader 不再依赖 sibling 文件真实存在,而是通过 container entry 加载。
5. shader 编译缓存成为正式的 `Library` 子系统,支持 `D3D12 / Vulkan / OpenGL` 多后端扩展。
6. 老项目和旧 `Library` 尽量兼容迁移,避免一次性炸库。
---
## 4. 核心架构决策
### 4.1 一个 artifact 必须变成一个单文件 container
正式改为:
1. 一个 `artifactKey` 只对应一个 artifact 文件。
2. artifact 文件内部包含 header、目录表、entry 表、payload 区。
3. entry 可以表达:
- main asset
- subasset
- import metadata
- dependency snapshot
- debug manifest
建议目标形态:
1. `Library/Artifacts/07/071822a133c727103210833b93d2b846.xca`
2. 或保留无扩展 hash 文件形式,但内部仍然是统一 container
这里更重要的是“单文件容器语义”,不是扩展名本身。
### 4.2 子资源必须从真实文件变成逻辑 entry
当前的:
1. `mesh_0.xcmesh`
2. `material_0.xcmat`
3. `texture_0.xctex`
4. `subassets.tsv`
后续都不再作为 artifact 目录中的真实文件暴露。
改为:
1. container entry `main`
2. container entry `mesh/0`
3. container entry `material/0`
4. container entry `texture/0`
5. container entry `manifest/subassets`
这样做的目的不是“隐藏文件”,而是统一系统抽象:
1. importer 只负责产出 entry。
2. loader 只负责按 entry 解析。
3. 外部磁盘布局不再影响运行时语义。
### 4.3 `ArtifactDB` 只维护工件语义,不维护目录语义
`ArtifactDB` 需要从当前的字段语义升级为:
1. `artifactKey`
2. `artifactPath`
3. `artifactFormatVersion`
4. `mainEntryId`
5. `resourceType`
6. `dependencySignature`
7. `sourceHash / metaHash / importerVersion`
8. 可选的 `subasset index snapshot`
不再把下列内容作为核心前提:
1. `artifactDirectory`
2. `mainArtifactPath` 必须是磁盘上的某个独立 sibling 文件
### 4.4 Shader 编译缓存必须纳入 Library 正式体系
shader 编译缓存不能继续以“某个功能修复补丁”的方式存在,必须被定义为 `Library` 正式缓存层的一部分。
正式目标:
1. 以编译 key 唯一标识一份 shader bytecode / program binary。
2. 编译 key 必须包含:
- shader 源文件签名
- include 依赖签名
- keyword / variant 宏
- backend 类型
- compiler 类型与版本
- profile / entry point
- 相关编译选项
3. 多后端共用同一套 key 生成规则和缓存查询流程。
4. 后端各自写入自己的 payload不共享错误的二进制格式。
### 4.5 必须坚持“统一接口,多后端实现”
这次重构不能只为 `D3D12` 做一套临时特化路径。
必须采用:
1. 上层统一的 `ShaderCompilationCache` 接口
2. 后端相关的 key 参与规则
3. 后端相关的 payload 类型
4. 后端相关的反序列化逻辑
这样后面:
1. `D3D12` 存 DXIL / DXBC
2. `Vulkan` 存 SPIR-V
3. `OpenGL` 可以先存 source variant 或 program binary capability 结果
都能挂到同一架构下,而不是分裂成三套缓存系统。
---
## 5. 目标目录与数据布局
### 5.1 Library 根目录
建议最终保留如下根层文件:
1. `Library/assets.db`
2. `Library/artifacts.db`
3. `Library/shadercache.db`
4. `Library/assets.db.tmp/.bak/.lock`
5. `Library/artifacts.db.tmp/.bak/.lock`
6. `Library/shadercache.db.tmp/.bak/.lock`
说明:
1. 单文件数据库应放在 `Library` 根目录,不再额外套目录。
2. `.tmp/.bak/.lock` 是数据库运行期一致性与恢复所需的辅助文件,属于正常成本,应保留。
### 5.2 Artifact 内容目录
建议统一为:
1. `Library/Artifacts/<2-hex-shard>/<artifactKey>.xca`
说明:
1. 仍然保留分片目录,避免单目录堆积过多文件。
2. 从“目录工件”改为“单文件工件”。
3. 这里已经足够接近 Unity 的外观,但关键收益来自统一 container不来自“像 Unity”本身。
### 5.3 Shader 编译缓存目录
建议使用:
1. `Library/ShaderCache/<backend>/<2-hex-shard>/<compileKey>.xcbc`
说明:
1. 这是运行时编译产物,不建议和 source artifact 混在同一目录。
2. 它本质上也是缓存工件,但生命周期和失效条件不同。
3. 数据库仍然在根目录,二进制 payload 用分片目录管理。
---
## 6. Artifact Container 格式设计
### 6.1 容器最小结构
每个 `.xca` 至少包含:
1. 文件头
2. 格式版本
3. 工件类型
4. entry 数量
5. entry 目录表
6. payload 区
7. 校验信息
### 6.2 Entry 元信息
每个 entry 需要至少记录:
1. `entryId`
2. `entryName`
3. `resourceType`
4. `localID`
5. `offset`
6. `size`
7. `compression`
8. `flags`
### 6.3 Manifest 内置化
原本外置的:
1. `subassets.tsv`
后续变为:
1. container 内置 manifest entry
2. 或者直接由 entry table 本身承担 manifest 作用
原则是:
1. 子资源映射不能再依赖工件目录里的额外文本文件。
### 6.4 调试能力不能丢
虽然外部从多文件变成单文件,但不能把可调试性彻底丢掉。
因此建议提供:
1. debug dump 工具
2. artifact inspect 命令
3. container 列表查看能力
4. 按需导出某个 entry 的开发期工具
这样能避免后续排障时变成纯黑盒。
---
## 7. 对导入链路的改造要求
### 7.1 Importer 层统一改为“写 entry”
各 importer 不再自己决定在 artifact 目录里创建几个文件。
统一改为:
1. importer 构建内存中的 artifact package
2. 向 package 写入 main entry
3. 按需写入 subasset entry
4. 由统一 `ArtifactWriter` 一次性落成单文件
### 7.2 各类资源的落地规则
建议采用如下规则:
1. `Texture`
- 通常一个 main entry 即可
2. `Material`
- 通常一个 main entry 即可
3. `Shader`
- 主 entry + 变体元数据 entry + 预编译记录 entry
4. `Model`
- 主 model entry + mesh/material/texture subasset entries
5. `Volume`
- 主 volume entry + 可选页表/brick 元数据 entry
6. `GaussianSplat`
- 主 splat entry + 索引/排序辅助元数据 entry
### 7.3 导入器必须停止暴露真实 sibling 文件路径
后续 importer 不应再让外部系统依赖:
1. `mesh_0.xcmesh`
2. `material_3.xcmat`
3. `texture_1.xctex`
如果某处仍然需要这种路径式访问,只能通过兼容层临时桥接,不能作为新架构继续扩散。
---
## 8. 对加载链路的改造要求
### 8.1 Loader 必须从“读文件路径”改成“读 artifact entry”
未来 loader 的输入应逐步变为:
1. artifact 文件路径
2. entry id / local id / entry name
而不是:
1. 一个已经存在于磁盘上的子文件路径
### 8.2 ResourceManager / AssetDatabase 需要新增解析能力
需要正式提供:
1. `ResolveMainArtifactEntry(...)`
2. `ResolveSubAssetEntry(...)`
3. `OpenArtifactContainer(...)`
4. `LoadResourceFromArtifactEntry(...)`
这样 `AssetDatabase``ResourceManager` 就能以“工件条目”作为统一边界。
### 8.3 允许阶段性兼容旧式 Loader
为了降低一次性改造风险,允许在迁移阶段存在:
1. 新容器 loader
2. 旧文件路径 loader
但要求:
1. 新导入默认只产出新容器
2. 旧 loader 仅承担读旧缓存或读测试基线的过渡职责
3. 最终必须收口到容器 entry 模型
---
## 9. Shader 编译缓存设计
### 9.1 目标
shader 缓存系统要解决三件事:
1. 避免同一 shader variant 在同一 backend 上反复现场编译
2. 避免 editor 启动时因大批 shader 同步编译造成长时间卡顿
3. 把编译结果纳入 `Library` 生命周期管理,而不是散落在临时模块里
### 9.2 Shader 缓存分层
建议分成两层:
1. `Shader Artifact`
- 记录 shader 源资源、变体定义、依赖签名、反射元数据
2. `Shader Bytecode Cache`
- 记录某 backend、某 profile、某 variant 的实际编译产物
这样做的原因是:
1. shader 资源本身是 source artifact 的一部分
2. 字节码则是“shader artifact 在某后端上的派生产物”
### 9.3 多后端适配策略
必须从一开始就定义好多后端字段:
1. `backend = D3D12 / Vulkan / OpenGL`
2. `compiler = DXC / glslang / driver / other`
3. `profile = ps_6_0 / vs_6_0 / spirv-vulkan1.2 / glsl-450`
4. `bytecodeFormat = DXIL / SPIRV / GL_PROGRAM_BINARY / SOURCE_FALLBACK`
### 9.4 OpenGL 的现实处理
OpenGL 不一定能像 D3D12 一样稳定落真正可移植字节码,因此计划要区分:
1. 理想路径:驱动支持 program binary可缓存 program binary
2. 保守路径:缓存预处理后的 shader source / variant expansion 结果
3. 最低保证:即使不能完全复用最终 program也要复用前置预处理和 variant 展开结果
也就是说,多后端适配不等于三端完全同构,而是统一框架下允许 payload 能力不同。
---
## 10. 迁移策略
### 10.1 不能一次性硬切
因为当前项目已经存在:
1.`Library`
2. 老 artifact 目录结构
3. 依赖老结构的 loader
4. 依赖老结构的测试
所以必须采用兼容迁移。
### 10.2 建议迁移阶段
#### 阶段 A底层能力先落地
1. 新增 `ArtifactContainerReader/Writer`
2. 新增 container format version
3. 新增 `ShaderCompilationCache` 正式接口
4. 新增调试工具和 dump 工具
#### 阶段 B数据库与解析接口升级
1. `ArtifactDB` 加入新字段
2. `AssetDatabase` 提供 entry 级解析
3. `ResourceManager` 能按 entry 加载
#### 阶段 C先迁移低复杂度资源
1. `Texture`
2. `Material`
3. `Shader`
原因:
1. 这三类资源 entry 结构简单
2. 容器化收益直接
3. 风险比 `Model`
#### 阶段 D迁移复杂资源
1. `Model`
2. `Volume`
3. `GaussianSplat`
这里才是这次重构的真正硬骨头。
#### 阶段 E兼容收口
1. 新导入全面只写新 container
2. 老 artifact 仅保留读取兼容
3. 清理临时桥接路径
4. 测试与工具全部对齐新语义
### 10.3 旧缓存处理策略
建议采用:
1. 读旧、写新
2. 命中新格式则直接用
3. 命中旧格式则读取并标记可重导
4. 必要时提供“一键清空旧缓存并全量重导”入口
---
## 11. 分阶段执行计划
## Phase 1方案冻结与底层抽象
目标:
1. 冻结 artifact container 格式
2. 冻结 shader compile key 规则
3. 冻结 `ArtifactDB` 新字段语义
交付物:
1. `ArtifactContainer` 设计文档
2. `ShaderCompilationCache` 设计文档
3. `Library` 目录与文件命名规范
验收:
1. 所有核心模块对新名词和新边界达成一致
2. 不再新增基于目录 sibling 文件的新逻辑
## Phase 2容器读写基础设施
目标:
1. 写出可工作的 `.xca` 读写器
2. 写出 entry table、payload、校验、版本管理
交付物:
1. `ArtifactContainerWriter`
2. `ArtifactContainerReader`
3. artifact inspect / dump 工具
4. 单元测试
验收:
1. 可写入多个 entry
2. 可随机读取指定 entry
3. 可检测格式版本与损坏文件
## Phase 3数据库与资源解析升级
目标:
1.`AssetDatabase``ResourceManager` 理解 entry 语义
交付物:
1. `ArtifactDB` schema 升级
2. entry 级 resolve API
3. 新旧格式兼容查询路径
验收:
1. 不依赖真实 sibling 文件也能定位主资源和子资源
2. 旧缓存仍可读
## Phase 4Shader 缓存正式化
目标:
1. 把 shader 编译缓存正式接入 `Library`
2. 打通多后端 key 体系
交付物:
1. `shadercache.db`
2. `Library/ShaderCache/...`
3. D3D12 正式字节码缓存
4. Vulkan/OpenGL 占位与接口接入
5. 计时日志、命中日志、失效日志
验收:
1. D3D12 重启 editor 后能稳定命中缓存
2. shader compile log 能明确区分 cache hit / miss
3. Vulkan/OpenGL 不破坏接口一致性
## Phase 5低复杂度资源迁移
目标:
1. 先迁移 `Texture / Material / Shader`
交付物:
1. 新 importer 写 container
2. 新 loader 读 container
3. 旧逻辑兼容桥接
验收:
1. 三类资源脱离目录式 artifact
2. 现有测试通过
## Phase 6复杂资源迁移
目标:
1. 迁移 `Model / Volume / GaussianSplat`
交付物:
1. `Model` 子资源 entry 化
2. `Volume` 复杂数据 entry 化
3. `GaussianSplat` 复杂数据 entry 化
4. 新的 subasset resolve 机制
验收:
1. 不再需要 `subassets.tsv`
2. 不再依赖 `mesh_0.xcmesh/material_0.xcmat/...` 真实文件
3. 场景加载、运行时 streaming、编辑器选择与重载行为正常
## Phase 7兼容收口与库清理
目标:
1. 清理临时桥接逻辑
2. 完成旧格式收口
交付物:
1. 旧 artifact 兼容策略文档
2. `Library` 清理工具
3. 最终回归测试与性能报告
验收:
1. 新项目默认只产生新格式
2. 旧项目迁移路径明确
3. 文档、测试、工具全部收口
---
## 12. 风险评估
### 12.1 高风险点
1. `Model` 子资源引用路径改造
2. 旧 loader 和新 loader 并存期间的行为分叉
3. `ArtifactDB` schema 迁移
4. shader 缓存 key 设计不完整导致误命中或漏命中
5. OpenGL 后端缓存能力不如 D3D12/Vulkan 稳定
### 12.2 主要回归风险
1. 资源重复导入
2. 子资源引用失效
3. 重启后缓存不命中
4. editor 启动时再次出现同步长阻塞
5. 清理 `Library` 后行为和旧项目不一致
### 12.3 风险控制原则
1. 每种资源类型单独迁移、单独验收
2. 每阶段都保留明确日志
3. 优先做“读旧写新”,不要先做“只认新格式”
4. 性能优化必须建立在日志可证伪的基础上,不能靠感觉判断
---
## 13. 验收标准
重构完成后,应满足以下标准:
1. `Library/Artifacts` 中一个 artifactKey 对应一个单文件 container。
2. `assets.db / artifacts.db / shadercache.db` 位于 `Library` 根目录。
3. `Texture / Material / Shader / Model / Volume / GaussianSplat` 都能通过统一 container 读写。
4. 新增资源类型时,不需要再发明“这个类型自己往 artifact 目录塞几个文件”的模式。
5. shader 缓存在 `D3D12` 上稳定命中,`Vulkan / OpenGL` 具备同框架下的扩展位。
6. editor 启动和场景 ready 时间相较当前基线有可测收益。
7. 调试工具可以查看 container 内部 entry不会因为单文件化而失去排障能力。
---
## 14. 本计划明确不做的事情
本计划不做以下偏题内容:
1. 为了“长得像 Unity”而做无意义目录重命名
2. 把所有缓存强行塞进一个数据库 blob 字段里
3. 为了单文件化而牺牲调试能力
4. 只做 D3D12 特化、把其他后端晾着不管
5. 一次性删掉旧 `Library` 兼容路径
---
## 15. 推荐执行顺序
建议严格按下面顺序推进:
1. 先冻结 container 格式和 shader key 规则
2. 再做底层 reader/writer 与数据库升级
3. 先迁移 `Texture / Material / Shader`
4. 再迁移 `Model / Volume / GaussianSplat`
5. 最后做兼容收口、清理和性能复盘
这条顺序的原因很简单:
1. 先做底层,后续每种资源都能复用
2. 先啃简单资源,能尽快验证架构不是错的
3. 把最难的复杂资源放在基础设施稳定之后处理,整体风险最低
---
## 16. 最终判断
这个重构的代价不小,但如果项目接下来还会持续扩展资源类型,那么它属于“越早做越划算”的长期基础设施工程。
真正值得做的不是把目录外观改得像 Unity而是
1. 统一 artifact 的存储模型
2. 统一子资源的表达方式
3. 统一 shader 编译缓存的 Library 化管理
4. 统一多后端的缓存接口
只要这四件事做对了,后面资源类型再增加,系统复杂度才不会继续失控。

View File

@@ -0,0 +1,342 @@
# Nahida Unity工程静态卡通渲染迁移计划
日期: 2026-04-10
状态: 已按“仅静态卡通渲染”范围重写
## 1. 范围锁定
这份计划只服务一个目标:
- 把 Nahida 的静态卡通渲染效果迁到 XCEngine。
本轮明确不做:
- 骨骼动画
- 运行时 skinning
- `SkinnedMeshRenderer` 运行链路
- blend shape 驱动
- Animator / Humanoid / Retargeting
- Unity 动画系统还原
如果后面发现 Unity 原样例里某些视觉细节依赖骨骼或 blend shape本轮处理原则是
- 先接受静态近似
- 不为此把范围扩成动画系统
## 2. 当前目标
本轮要落地的是一套“静态可渲染”的 Nahida 角色效果,重点是:
- body / hair / face / brow / dress 的分材质关系
- toon 明暗分层
- face shadow 基本成立
- outline 基本成立
- 在独立预览场景里稳定对比和调试
换句话说,本轮不是“把 Unity 角色系统搬过来”,而是:
- 先把 Nahida 的静态画面效果在你自己的引擎里立住
## 3. 已知事实
### 3.1 资源基线已经整理
当前已经完成的 Phase 0 资源整备:
- 引擎项目内静态资源基线:
- `project/Assets/Characters/Nahida/`
- Unity 原始参考快照:
- `docs/reference/NahidaUnity/`
已落地的关键资源包括:
- Nahida 的 `FBX`
- body / hair / face / shared 贴图集
- `Nahida_Body_Smooth.mesh`
- Unity 材质、shader、scene、脚本参考文件
### 3.2 当前运行时能力
当前引擎已经具备:
- `MeshFilterComponent + MeshRendererComponent` 静态网格渲染路径
- `MeshRendererComponent` 的多材质槽能力
- `Material` / `Shader` 资源导入体系
当前引擎没有现成闭环:
- skinning
- skeleton runtime
- blend shape runtime
所以当前正确路线不是补动画,而是:
- 先基于现有静态渲染路径做 Nahida 的静态效果落地
### 3.3 Nahida 的 Unity 语义里哪些需要保留
从 Unity 原工程看,本轮静态渲染仍然必须保留这些语义:
- 7 份材质的分工关系
- `_LightMap` 驱动的 toon 阴影分层
- `_FaceLightMap + _FaceShadow + _FaceDirection` 的面部阴影语义
- `_MetalMap`、rim、outline 的材质逻辑
- `Nahida_Body_Smooth.mesh` 对描边效果的贡献
### 3.4 哪些 Unity 语义本轮只记录、不实现
本轮只作为参考保留,不进入当前执行范围:
- `SkinnedMeshRenderer`
- blend shape 默认值
- 头骨驱动的面部朝向更新
- 完整 URP 后处理还原
## 4. 迁移原则
### 4.1 先静态成立,再谈角色系统
当前第一优先级是:
- 让 Nahida 在静态场景里正确显示
不是:
- 先做角色运行时架构大一统
### 4.2 先复用现有静态网格路径
只要当前导入出来的 Nahida mesh / section / material slot 足够支撑静态装配,就优先用现有:
- `MeshFilterComponent`
- `MeshRendererComponent`
不额外引入:
- `ModelComponent`
- `SkinnedMeshRendererComponent`
### 4.3 Unity 文件继续只做参考
`docs/reference/NahidaUnity/` 里的 `.mat``.shader``.unity` 继续只作为语义来源。
最终落地仍然必须是:
- 引擎自己的 `Shader`
- 引擎自己的 `Material`
- 引擎自己的 `Scene`
### 4.4 面部朝向先做静态近似
本轮不做基于头骨的 `_FaceDirection` 驱动。
优先级改为:
- 先给面部材质写一个可接受的静态 `_FaceDirection`
- 如果后面确实需要,再加一个基于对象整体朝向的简化驱动
不进入本轮的:
- 基于 head bone 的动态朝向更新
## 5. 分阶段方案
### Phase 0: 资源整备
状态:
- 已完成
已完成内容:
- 建立 `project/Assets/Characters/Nahida/` 资源树
- 补齐缺失贴图
- 补齐 `Nahida_Body_Smooth.mesh`
- 建立 `docs/reference/NahidaUnity/` 参考区
- 写出资源说明与依赖映射
### Phase 1: 静态装配基线
目标:
- 确认当前 Nahida 的 FBX 导入产物能否直接支撑静态场景装配
- 建立一个专门用于调试 Nahida 的静态预览场景
任务:
- 盘点 Nahida 当前导入出来的 mesh / submesh / material slot
- 确认应该使用哪些 meshRef 来组装静态角色
- 创建 `NahidaPreview.xc`
- 在场景中放入 Nahida 的静态实例
- 先用占位或默认材质把所有 slot 挂齐
验收:
- Nahida 能在独立场景中稳定显示
- mesh slot 拆分关系清楚
- 预览场景不依赖主场景的历史脏数据
### Phase 2: Toon Shader移植
目标:
- 在 XCEngine 里建立 Nahida 用的引擎原生 toon shader
任务:
- 新建 Nahida toon shader
- 先移植最关键的静态视觉能力:
- base map
- base color
- `_LightMap`
- `_FaceLightMap`
- `_FaceShadow`
- `_MetalMap`
- rim
- outline
- 不依赖 Unity URP include
验收:
- body / hair / face / dress 的 toon 分层开始成立
- 面部和头发不再是默认 PBR 外观
### Phase 3: 引擎原生材质落地
目标:
- 建立 Nahida 的引擎原生材质资产
任务:
- 建立:
- `Nahida_Base.material`
- `Nahida_Body.material`
- `Nahida_Hair.material`
- `Nahida_Face.material`
- `Nahida_Brow.material`
- `Nahida_Dress1.material`
- `Nahida_Dress2.material`
- 按依赖图绑定贴图和参数
- 把材质正式挂到静态预览场景
验收:
- 7 份材质语义清楚
- 各 slot 的贴图和参数不串位
- 删除 Unity 参考 `.mat` 后不影响引擎内显示
### Phase 4: 描边与面部阴影补全
目标:
- 把最关键的卡通识别特征补完整
任务:
- 接入 `Nahida_Body_Smooth.mesh` 或等价静态描边路径
- 补全 outline 参数
- 给 face 材质写静态 `_FaceDirection`
- 如确有必要,增加基于角色整体朝向的简化 `_FaceDirection` 驱动
验收:
- outline 视觉成立
- face shadow 不明显错误
- 不引入骨骼依赖
### Phase 5: 预览场景与视觉调优
目标:
- 做一个可稳定对照 Unity 的静态 Nahida 预览场景
任务:
- 固定相机
- 固定主光
- 简化背景
- 对 body / hair / face / outline 做迭代调参
- 如确有必要,再补最小后处理
验收:
- 可以稳定截图对比
- 能快速判断问题属于材质、shader、描边还是灯光
## 6. 风险点
### 6.1 静态路线与 Unity 原样不会完全一致
因为本轮不做:
- skinning
- blend shape
- head bone 驱动
所以最终效果可以接近,但不保证和 Unity 样例逐像素一致。
### 6.2 描边依赖特殊 mesh
如果忽略 `Nahida_Body_Smooth.mesh`outline 很可能明显跑偏。
### 6.3 Face shadow 不是纯贴图问题
即使不做骨骼,`_FaceDirection` 也至少要给一个合理静态值,否则面部阴影会别扭。
### 6.4 不应该一上来做后处理
如果在 shader、材质、outline 还没立住前就先做 bloom / tonemap会把问题源混在一起。
## 7. 验收标准
### 档位A静态资源正确
- `project/Assets/Characters/Nahida/` 资源完整
- `docs/reference/NahidaUnity/` 参考区完整
- 预览场景可稳定打开
### 档位B静态角色正确
- Nahida 静态显示成立
- body / hair / face / brow / dress 分材质正确
- toon 阴影、outline、face shadow 基本成立
### 档位C视觉接近样例
- 角色整体风格接近 Unity 样例
- 关键差异只剩高级细节,而不是基础渲染错误
## 8. 推荐执行顺序
1. 先确认 Nahida 当前导入产物的静态装配方式
2. 再做引擎原生 toon shader
3. 再做 7 份引擎原生材质
4. 再补 outline 和 face shadow
5. 最后做预览场景调优与可选后处理
明确不要做成:
1. 先补 skinning
2. 先补动画系统
3. 先补骨骼驱动组件
这不在当前范围内。
## 9. 下一步
基于当前状态,下一步不是继续做计划,也不是做骨骼运行时,而是:
1. 先确认 Nahida 当前导入出来的静态 mesh / section / material slot 布局是否足够支撑静态装配。
2. 如果足够,直接开始建立 `NahidaPreview.xc`、引擎原生 toon shader、以及 Nahida 的材质壳子。
当前代码现状对这个判断是偏乐观的:
- 现有渲染路径已经支持 `MeshFilter + MeshRenderer`
- `MeshRendererComponent` 已经支持多材质槽
- Nahida 当前导入产物里已经存在大量静态 mesh 子资源
所以最合理的下一执行切口是:
- 直接进入“静态预览场景 + toon shader + native materials”这一段
而不是去碰动画系统。

View File

@@ -0,0 +1,650 @@
# NanoVDB 体积云场景首帧阻塞根因修复计划
日期2026-04-10
## 1. 文档定位
这份计划只解决一个具体问题:
-`project` 项目中打开带有 `cloud.nvdb` 体积云对象的 `Main.xc` 场景时Editor 在前期可交互,但在约 8 秒后出现一次明显的整窗卡死,随后再经过十多秒体积云才真正显示出来。
这份计划不讨论以下内容:
- 不讨论 build/package/runtime 发布格式。
- 不讨论 NanoVDB 渲染算法本身是否继续升级。
- 不讨论多后端 rollout。
- 不讨论重新设计整个 `Library` 体系。
这份计划关注的是:
- 为什么当前 `Library` 已经存在,但含 `.nvdb` 的场景打开仍然会在首帧附近严重阻塞。
- 为什么 `mvs/VolumeRenderer` 用同一份 `cloud.nvdb` 可以约 3 秒完成显示,而 Editor 要慢很多。
- 应该如何把当前这条链路收口成一套真正可用的正式方案。
---
## 2. 问题现象
当前复现现象已经很明确:
1. 直接打开 `project` 项目。
2. 打开 `project/Assets/Scenes/Main.xc`
3. 界面会显示 `Runtime streaming scene assets...`,这段时间窗口还能继续操作。
4. 在大约 8000 ms 左右Editor 突然开始明显卡死。
5. 卡死十多秒后,体积云对象才真正显示出来,随后窗口恢复。
对比样本:
- `mvs/VolumeRenderer` 使用的是同一份 `cloud.nvdb`
- 运行 `mvs/VolumeRenderer/run.bat` 时,大约 3 秒左右即可加载并显示。
这说明:
- 慢点不在“这份 `.nvdb` 根本无法解析”。
- 慢点也不在“当前机器根本无法承载这份体积数据”。
- 真正的问题出在 Editor 主线里的额外同步收口路径。
---
## 3. 当前已经确认的事实
### 3.1 当前 `Library` 对体积资源做的不是“运行时加速缓存”
当前源文件:
- `project/Assets/cloud.nvdb`
- 文件大小:`590,241,000` bytes
当前 artifact
- `project/Library/Artifacts/.../main.xcvol`
- 文件大小:`590,240,896` bytes
这说明当前 `.xcvol` 基本就是:
- 一个较小的 header
- 加上一份几乎原样的 NanoVDB payload
也就是说,当前 `Library` 在体积资源这条链路上做的是:
- 导入身份缓存
- metadata 缓存
- source -> artifact 统一入口
但它还没有做到:
- 为运行时准备更轻的 cooked payload
- 为 GPU 上传准备更直接的 runtime-ready 数据
- 为首帧显示准备真正低成本的预热结果
结论:
- 当前 `Library``.nvdb` 是“导入缓存”,不是“运行时性能缓存”。
### 3.2 当前 VolumeField CPU 侧存在多次大拷贝
当前体积资源进入运行时时,大致会经历:
1.`.xcvol` 读取整个 payload 到临时缓冲。
2. `VolumeField::Create(...)` 再把 payload 拷贝进 `VolumeField::m_payload`
3. `RenderResourceCache::UploadVolumeField(...)` 再构造一个新的 `uploadData`
4. 再把这份数据写入 RHI buffer。
对 590MB 级别的体积数据来说,这不是“小开销”,而是主路径上的重负担。
### 3.3 当前 GPU 上传不是在后台完成,而是在第一次真正绘制时同步触发
当前体积云真正进入渲染时,会在 `BuiltinVolumetricPass::DrawVisibleVolume(...)` 中走:
- `RenderResourceCache::GetOrCreateVolumeField(...)`
- `UploadVolumeField(...)`
也就是说:
- 体积资源即使 CPU 侧已经异步读好了,
- GPU 侧 residency 的真正建立仍然是在第一次真正绘制该体积对象时才触发,
- 而且当前实现是同步发生在渲染路径里。
这正好解释了现在的现象:
- 前期 `Runtime streaming scene assets...` 时还能操作。
- 到第一次真正要画体积云时,主线程/渲染线程突然进入大开销同步路径。
### 3.4 当前体积 shader / PSO 首次创建也会叠加到这次阻塞里
当前体积材质使用的是:
- `builtin://shaders/volumetric`
该 builtin shader 最终会加载:
- `engine/assets/builtin/shaders/volumetric.shader`
这个 shader 直接包含:
- `PNanoVDB.hlsl`
而当前 D3D12 shader 编译路径明确使用:
- `D3DCOMPILE_DEBUG | D3DCOMPILE_SKIP_OPTIMIZATION`
这意味着:
- 体积 pass 第一次真正创建 pipeline state 时,
- 很可能还会在主线程/渲染线程上同步现编一个包含 `PNanoVDB` 的大 shader 变体。
因此这次卡死不是单一开销,而是两类重活叠加:
1. 590MB 体积 payload 的运行时实现化
2. 体积 shader / PSO 的首次真实可绘制化
### 3.5 当前场景加载状态文本会误导真实阶段
当前 `Runtime streaming scene assets...` 的状态主要依赖:
- `ResourceManager::GetAsyncPendingCount()`
它只能说明:
- 异步 loader 队列还有没有待完成任务
它不能说明:
- GPU 上传是否已经完成
- 体积 shader 是否已经编好
- PSO 是否已经 ready
- 体积对象是否真的达到 render-ready
所以现在 UI 上看到的“streaming 快结束了”并不等于体积云真的快 ready 了。
---
## 4. 最根本的原因
把这次问题压缩成一句话:
**当前 `Library` 缓存住的是体积资源的“导入结果”,但没有缓存住体积云真正昂贵的“运行时实现化结果”;而这部分实现化又被推迟到了第一次真正绘制时同步完成。**
更展开一点,根因可以拆成四层:
### 4.1 第一层根因:缓存层次不对
现在缓存住的是:
- source asset 身份
- artifact 文件
- metadata
没有缓存住的是:
- 低拷贝可消费的 volume payload 视图
- GPU-ready residency
- shader / PSO 可直接绘制状态
### 4.2 第二层根因:时机不对
当前重活发生在:
- 不是项目打开时
- 不是场景结构恢复后后台预热时
- 而是在第一次真正绘制体积云时
这导致体感非常差,因为卡顿被集中释放在用户已经开始操作视口之后。
### 4.3 第三层根因:实现路径太重
当前 590MB 体积数据在 Editor 里会经历多次 CPU 侧复制和一次同步 GPU 上传。
对这种量级的数据,这本身就足以形成长时间阻塞。
### 4.4 第四层根因:渲染首帧还叠加了 shader / PSO 首编
体积 pass 的首次真正绘制不是“只差最后一次 draw call”而是还可能同时触发
- shader variant 首编
- pipeline layout 创建
- PSO 创建
- descriptor set 初始化
所以这次卡死是“重资源 + 重 shader”叠加不是单点故障。
---
## 5. 为什么 `mvs/VolumeRenderer` 更快
`mvs/VolumeRenderer` 当前更快,不是因为它“缓存更高级”,而是因为它路径短得多。
它做的是:
- 直接读源 `.nvdb`
- 直接生成 GPU buffer
- 直接编少量 shader
- 直接渲染
它没有承担下面这些 Editor 主线额外职责:
- `AssetDatabase`
- `AssetRef -> path` 解析
- scene/component 反序列化恢复
- `VolumeField` 运行时抽象包装
- `RenderResourceCache` 的通用缓存层
- `BuiltinVolumetricPass` 的通用 descriptor / pass / pipeline 约束
- 视口首帧时的 editor 额外渲染流
所以 MVS 快,说明的是:
- 这份 `.nvdb` 并不是天然慢到 15 秒
而不是说明:
- 现在的 Editor 路线只需要继续堆更多导入缓存就能变快
---
## 6. 修复目标
本轮修复目标不是“让 `.xcvol` 体积更小”,也不是“强行把所有工作前置到项目启动”。
本轮的正式目标是:
### 6.1 交互目标
- 打开 `Main.xc` 后,不允许再出现“先可交互,再突然长时间整窗卡死”的体验。
- 一旦场景已经进入可交互状态,后续体积云就只能继续后台预热,不能把窗口重新拖回不可响应。
### 6.2 架构目标
- 把体积资源的“导入缓存”和“运行时实现化”明确分成两个阶段。
- 首次绘制路径不能再承载 590MB 级别的同步重活。
- 首次绘制路径只能消费已经 ready 的资源,或优雅跳过未 ready 的体积对象。
### 6.3 性能目标
在当前开发机和当前 `cloud.nvdb` 样本下:
- warm cache 场景再次打开时:
- 不允许出现超过 `200 ms` 的二次窗口无响应段。
- 体积云从场景打开到可见的时间目标收敛到 `3 s` 以内。
- 该目标以当前 `mvs/VolumeRenderer` 的约 `3 s` 作为对齐基线,正式目标是不慢于 MVS。
- cold import 首次打开时:
- 允许总体耗时更长,
- 但不允许在体积资源真正显示前出现长时间窗口卡死。
### 6.4 诊断目标
- 必须能明确区分:
- scene structure ready
- CPU payload ready
- GPU upload in progress
- shader / PSO prewarm in progress
- render-ready
---
## 7. 本轮明确不做的错误修法
以下方案不能作为本轮主方案:
### 7.1 不能只继续优化 `AssetDatabase::EnsureArtifact()`
原因:
- 这次 warm cache 场景下的主问题已经不是 source import 了。
- 再继续只抠导入判定和 reimport不会解决“第一次真正绘制才卡死”。
### 7.2 不能只加更多“异步读文件”
原因:
- 当前前半段已经异步了。
- 真正卡死点在后半段 render-time realization。
### 7.3 不能一上来就先加第二套磁盘缓存目录
原因:
- 当前最大问题首先是同步时机和多次拷贝。
- 如果不先把“谁在什么时候做重活”改对,再加新 cache 文件夹只是继续堆复杂度。
### 7.4 不能只通过隐藏 UI 文本来掩盖问题
原因:
- 现在不是提示文案不对,而是真有一段重度同步阻塞。
---
## 8. 正式修复方向
本轮采用四条主线并行收口,但执行顺序必须严格分阶段。
### 8.1 主线 A先把真实耗时切开看清楚谁最重
虽然根因已经明确,但时间占比仍需正式打点。
本阶段必须先拿到真实分段耗时,不允许后续继续靠体感猜。
需要新增的时间切片:
1. `Scene Deserialize`
2. `AssetRef Resolve`
3. `VolumeFieldLoader.ReadArtifact`
4. `VolumeField.Create`
5. `Volume GPU Upload`
6. `Volumetric Shader Variant Compile`
7. `Volumetric PSO Create`
8. `First Volume Visible`
需要新增的日志与状态:
- `Main.xc` 打开时,针对 `cloud.nvdb` 输出完整链路耗时。
- 把“异步流式加载完成”和“体积 render-ready”拆开显示。
这一阶段的目的不是修性能,而是:
- 锁死真正的大头时间占比
- 避免后续错误优化无关路径
### 8.2 主线 B去掉体积 payload 的多次大拷贝
这是本轮最核心的工程改动之一。
正式方向:
1. `VolumeField` 不能继续默认把 590MB payload 再拷进一份新的 `m_payload`
2. `.xcvol` 读取后,应该改成:
- 文件映射
- 或共享只读 blob
- 或单所有权 payload 容器
3. `RenderResourceCache::UploadVolumeField(...)` 不能再额外构造一份等体积的 `uploadData` 再拷一次。
4. 上传路径必须直接消费 loader 产出的只读 payload 视图。
本阶段完成后,应达到:
- `.xcvol -> VolumeField` 不再发生无意义的大内存复制。
- `VolumeField -> RenderResourceCache` 也不再产生第二份 590MB 临时副本。
这一步做完,即使还没异步 GPU 上传,卡顿也会先明显下降。
### 8.3 主线 C把 GPU 上传从首次绘制路径里拿出去
当前真正错误的不是“上传很重”,而是“上传发生在第一次真正 draw 的时候”。
正式方向:
1.`VolumeField` 建立明确的 runtime residency 状态机:
- `Unloaded`
- `CpuReady`
- `GpuUploading`
- `GpuReady`
- `Failed`
2. CPU 侧 payload 一旦 ready就立即进入独立的 GPU 预热队列。
3. `BuiltinVolumetricPass::DrawVisibleVolume(...)` 不允许再承担首次重量级上传。
4. draw path 的职责改为:
- 如果 `GpuReady`,正常绘制
- 如果未 ready跳过或显示占位不得同步收口
对 D3D12 的具体要求:
1. `BufferType::Storage` 不能继续把大体积 volume payload 当普通 upload-heap 常驻缓冲来处理。
2. 需要引入正式的:
- staging/upload buffer
- default heap storage buffer
- copy queue 或专用 upload path
3. volume buffer 上传完成后再切换到可读状态,而不是在 draw path 上临时补。
这一步是解决“8000 ms 后突然卡死”的主修复点。
### 8.4 主线 D把体积 shader / PSO 首编从体积首帧里拿出去
当前体积云第一次真正绘制时,渲染链路里还会叠加:
- builtin volumetric shader variant 首次真正编译
- pipeline layout / descriptor layout 构建
- PSO 创建
正式方向:
1. `builtin://shaders/volumetric` 的实际运行时使用变体需要正式预热。
2. 预热时机不放在“第一次真正 draw”。
3. 预热应在以下时机之一完成:
- scene structure ready 之后的后台预热阶段
- volume material 绑定后立刻排队预热
4. `BuiltinVolumetricPass` 首次执行时只能命中已存在的 shader variant / PSO cache。
当前 D3D12 路线还要额外处理一个问题:
- 现在编译 flags 是 `D3DCOMPILE_DEBUG | D3DCOMPILE_SKIP_OPTIMIZATION`
这本身会显著拉长编译时间。
本轮需要明确一条正式策略:
1. Debug 能力是否仍要保留。
2. 如果保留,默认 editor 交互路径不能在首帧同步承担这份成本。
3. 可以接受“后台慢编”,不能接受“首帧卡死慢编”。
### 8.5 主线 E修正场景加载进度模型
当前 `Runtime streaming scene assets...` 的语义不完整。
这会直接误导调试,也会误导后续调度逻辑。
正式方向:
1. 把 scene load progress 拆成四段:
- `Structure Ready`
- `CPU Asset Streaming`
- `GPU Residency Prewarm`
- `Render Warmup`
2. `pendingAsyncLoads == 0` 时,只能说明 CPU loader 阶段接近结束。
3. 只有 volume GPU ready 且体积 pass 关键 shader/PSO ready 时,才能算真正 ready。
4. UI 需要把这几个阶段明确显示出来,避免再把后半段卡顿误判成“明明 streaming 都结束了却又卡”。
---
## 9. 执行阶段
### Phase 0诊断与基线固化
目标:
- 用日志和时间切片把当前问题彻底量化。
任务:
1.`Main.xc``cloud.nvdb` 打通完整性能时间线。
2. 输出 warm cache / cold cache 两组基线。
3. 输出 MVS 对比基线。
验收标准:
1. 能明确给出 CPU 读取、CPU 拷贝、GPU 上传、shader 编译、PSO 创建各自耗时。
2. 不再用“感觉像是这里慢”做判断。
### Phase 1体积 payload 低拷贝重构
目标:
- 消除 `.xcvol -> VolumeField -> UploadVolumeField` 链路中的重复大拷贝。
任务:
1. 重构 `VolumeField` 的 payload 持有方式。
2. 重构 `.xcvol` loader 的结果表示。
3. 重构 `RenderResourceCache::UploadVolumeField(...)` 的输入形式。
验收标准:
1. warm cache 情况下CPU 内存峰值明显下降。
2. `VolumeField` 载入完成时不再出现等量级的重复 payload 副本。
### Phase 2异步 GPU 上传与渲染脱钩
目标:
- 第一次绘制体积对象时,不再承担首次 GPU upload。
任务:
1. 增加 volume GPU prewarm 队列。
2. 建立 GPU residency 状态机。
3. 改掉 draw path 的同步上传兜底。
验收标准:
1. 首次打开 `Main.xc` 后,不再在体积第一次出现在视野中时发生长时间整窗阻塞。
2. 体积对象未 ready 时允许暂时不显示,但不允许卡死窗口。
### Phase 3体积 shader / PSO 预热
目标:
- 不再把 `volumetric.shader` 的首次真实可绘制准备放在体积首帧。
任务:
1. 为 active backend 预热体积 shader variant。
2. 为 volume material / pass 预热关键 PSO。
3. 把 shader/PSO 首编首建从 draw path 移走。
验收标准:
1. 首次显示体积云时,不再同时伴随大段 shader/PSO 同步创建时间。
2. 日志能证明首帧命中的是已准备好的可绘制状态。
### Phase 4场景状态机与 UI 收口
目标:
- 让场景打开状态和真实资源准备阶段一致。
任务:
1. 拆分 load status 阶段。
2. 修正 `Runtime streaming scene assets...` 的语义。
3. 在 Project/Viewport/Console 中统一反映同一份阶段状态。
验收标准:
1. UI 文案与真实阶段一致。
2. 不再出现“streaming 结束了但实际上后面还有一次大卡死”的错误认知。
### Phase 5回归、压力样本与收口
目标:
- 用真实样本和自动化验证确认这条路线已经稳定。
任务:
1. 用当前 `cloud.nvdb` 做 warm/cold 双场景回归。
2. 回归 `Main.xc` 打开、关闭、再次打开。
3. 检查体积云显示、Editor 响应性、日志阶段切片。
验收标准:
1. warm cache 打开 `Main.xc` 时无二次长阻塞。
2. cold cache 首次导入时也保持可交互。
3. 体积云显示时间显著收敛,接近 MVS 量级。
---
## 10.1 当前执行进展2026-04-10
当前代码实现已经进入正式重构阶段,已落地的第一批改动如下:
1. `VolumeField` 新增 owned payload 创建路径,`.xcvol` 读取结果不再在 `VolumeField::Create(...)` 内发生第二次整块复制。
2. `VolumeFieldLoader` 的 artifact 载入路径已改为“读入 payload -> 直接 move 进 `VolumeField`”,去掉 artifact load 阶段的重复大拷贝。
3. `RenderResourceCache::UploadVolumeField(...)` 不再构造整块 `uploadData` 临时副本,改为直接消费 `VolumeField` payload。
4. `RenderResourceCache` 已为 volume 引入最小可用的 residency 状态:
- `Uninitialized`
- `Uploading`
- `Ready`
- `Failed`
5. volume GPU 上传已改为分帧推进,当前实现按固定 chunk 预算逐帧写入,避免第一次真正绘制时一次性同步吞下整块 payload。
6. `BuiltinVolumetricPass` 已拆出资源预热步骤:
- 先推进 volume upload
- 再预建 volume pass layout / pipeline
- draw path 只消费 `Ready` 的 volume未 ready 时直接跳过,不再兜底触发重量级上传
7. 当前已补最小日志:
- `Volume GPU upload started`
- `Volume GPU upload ready`
这批改动的意义是:
- 先把 warm cache 路径里最重的两类同步收口拆开:
- `.xcvol -> VolumeField` 的重复 CPU 大拷贝
- 首次 draw path 内的一次性整块 GPU 上传
- 先把“卡死 Editor”问题从根上打散成可推进、可观察、可继续优化的状态。
当前这批改动还没有完成的部分:
1. 还没有把 `.xcvol` 升级成真正 runtime-ready 的 cooked artifact当前只是先把现有 artifact 的运行时消费链路做轻。
2. 还没有补齐完整的分段耗时打点。
3. 还没有把 volumetric shader / PSO 的首编完全前移到更早阶段。
---
## 11. 涉及模块范围
预计会涉及但不限于以下模块:
- `engine/include/XCEngine/Resources/Volume/VolumeField.h`
- `engine/src/Resources/Volume/VolumeField.cpp`
- `engine/src/Resources/Volume/VolumeFieldLoader.cpp`
- `engine/include/XCEngine/Rendering/Caches/RenderResourceCache.h`
- `engine/src/Rendering/Caches/RenderResourceCache.cpp`
- `engine/include/XCEngine/Rendering/Passes/BuiltinVolumetricPass.h`
- `engine/src/Rendering/Passes/BuiltinVolumetricPass.cpp`
- `engine/src/RHI/D3D12/D3D12Device.cpp`
- `engine/src/RHI/D3D12/D3D12Buffer.cpp`
- `engine/src/Resources/BuiltinResources.cpp`
- `editor/src/Managers/SceneManager.cpp`
- `editor/src/Viewport/ViewportHostService.h`
如果 Phase 2 仍然不足,再考虑是否需要新增专门的 runtime-prewarm 辅助模块。
但在本轮中,不应先为了结构好看而过早引入新的大模块。
---
## 12. 风险与边界
### 11.1 风险一:只优化 CPU 拷贝,但仍保留首帧同步 GPU 上传
结果:
- 会变快,但不会从根上解决“突然卡死”。
### 11.2 风险二:只做 GPU 上传异步,但 shader / PSO 首编仍在首帧
结果:
- 体积数据路径变快,但体积首帧依然可能因为 shader 现编而卡死。
### 11.3 风险三:一上来先设计新的磁盘 runtime cache
结果:
- 复杂度先上去了,
- 但如果真正的大头是同步上传和首编,收益会被高估。
因此本轮策略必须是:
1. 先打点
2. 先移走同步重活
3. 再决定是否需要更重的磁盘级 runtime cache
---
## 13. 完成标志
当以下条件同时成立时,这份计划才算完成:
1. 打开 `project/Assets/Scenes/Main.xc`Editor 不再出现二次长时间整窗卡死。
2. `cloud.nvdb` 的 warm cache 路径已经不再依赖首次绘制时的同步重资源收口。
3. 体积 shader / PSO 的首次准备不再挤在体积首帧。
4. 场景进度状态能正确区分 CPU streaming、GPU prewarm 和 render-ready。
5. 当前这条路径的耗时分布可以通过日志直接解释,不再需要靠猜。
---
## 14. 一句话结论
这次问题的根不在“`Library` 有没有命中”,而在“当前 `Library` 只缓存了导入结果,没有缓存运行时真正昂贵的实现化结果,而且这部分实现化被错误地放到了体积第一次真正绘制时同步完成”。
本轮修复必须围绕这个根因展开,而不是继续把注意力放回导入判定本身。

View File

@@ -1,6 +1,6 @@
# XCUI NewEditor主线重建计划
日期2026-04-07
日期2026-04-09
## 1. 文档定位
@@ -33,6 +33,7 @@
- `new_editor/` 不再只是临时名字上的沙盒。
- 它应被视为未来正式编辑器的新主线工作区。
- 后续真正重建编辑器时,应以 `new_editor/` 为宿主和产品主线,而不是回头改旧 `editor/`
- 但它不是当前测试体系入口;当前阶段不承担基础层验证职责。
### 2.3 `tests/UI` 仍然是基础层唯一实验场
@@ -40,6 +41,7 @@
- `tests/UI/Editor` 只验证 Editor 基础层能力。
- `tests/UI/Runtime` 只验证 Runtime 层能力。
- 所有基础层验证、交互试验、截图检查,都优先放在 `tests/UI`
- `tests/UI` 同时也是当前 XCUI 的唯一正式验证入口。
### 2.4 `new_editor/` 当前不承担“实验面板堆场”
@@ -49,12 +51,19 @@
- 在 Editor 基础层收口前,禁止把 `new_editor/` 继续做成杂乱的试验场或验证面板集合。
- 需要人工操作检查的内容,仍然通过 `tests/UI/*/integration` 提供。
### 2.5 当前主线优先级是 `Editor`,不是 `Runtime`
### 2.5 Core/Runtime 可资源化Editor 固定代码样式
- `Core / Runtime` 仍可继续推进资源化、热重载与资源驱动验证。
- `Editor` 当前默认样式、palette、metrics 与视觉语义固定在代码层,不再把 Editor 主题解析作为主线。
- workspace、panel session、状态机、命令派发、业务装配与控制器仍保留在代码层。
- 在 `Core / Editor` 基础层没有收口前,不提前在 `new_editor/` 推进业务面板。
### 2.6 当前主线优先级是 `Editor`,不是 `Runtime`
- `Runtime` 继续按三层设计保留,但不是当前最高优先级。
- 当前最高优先级是先把 `UI Core + UI Editor` 做成熟,并让其具备支撑 `new_editor` 正式重建的能力。
### 2.6 Editor 视觉基线以旧 `editor/` 为准
### 2.7 Editor 视觉基线以旧 `editor/` 为准
- `XCEditor` 当前默认视觉还没有达到旧 `editor/` 的程度。
- 后续 `Editor UI` 的默认主题、控件密度、边框、分隔线、状态反馈强度,都要以旧 `editor/` 现有风格为基线。
@@ -100,11 +109,13 @@
- editor-only widget
- editor-only shell / dock / workspace / panel session / viewport shell
- editor 风格语义
- 默认 editor theme token 与资源化样式体系
- 固定代码样式下的默认视觉常量与结构语义
`Editor` 不负责:
- 旧 `editor/` 的就地替换
- 当前测试体系入口
- Editor 主题解析主线
- 直接承担游戏 Runtime UI
---
@@ -141,16 +152,11 @@
当前离“可正式支撑 `new_editor` 重建”的差距主要在以下几点:
### 4.2.1 Editor 默认样式资源层太薄
### 4.2.1 Editor 固定样式常量仍未收口
- 当前 `.xctheme` 只能控制少量颜色、spacing、radius
- 远不足以承载旧 editor 那种完整的产品级视觉体系
- 前很多控件仍然只是在通用深色皮肤上运行,不是正式 Editor 风格。
### 4.2.2 Widget 视觉参数仍未充分资源化
- 仍有大量 palette / metrics / font size / rounding / inset 硬编码在 C++ 里。
- 这会导致 `.xctheme` 只能“改一点颜色”,无法真正控制 Editor 外观。
- 当前大量 Editor palette / metrics / font size / rounding / inset 仍分散在不同控件实现里
- 问题不在于 `.xctheme` 不够厚,而在于固定代码样式尚未统一收口
- 前很多控件仍停留在通用深色皮肤,不是正式 Editor 风格。
### 4.2.3 视觉基线还未对齐旧 editor
@@ -203,17 +209,19 @@
都必须优先回补到 `Core` 或 shared 层,而不是在 `Editor` 层写临时绕过实现。
### 5.3 Editor 风格必须资源化,但语义归 Editor 层管理
### 5.3 Editor 样式固定在代码层,资源与代码边界固定
- 样式机制归 `Core`
- Editor 默认风格语义归 `Editor`
- 颜色 / spacing / radius / border / density / typography 应尽量走资源化 token
- 控件行为与结构语义仍由 `Editor` 代码层控制
- `Core / Runtime` 可以继续使用资源化样式与热重载
- `Editor` 默认颜色 / spacing / radius / border / density / typography 固定在代码层
- 控件行为、状态机、workspace/panel session、命令派发与业务装配仍由 `Editor` 代码层控制
### 5.4 `new_editor/` 只做正式产品装配,不做测试堆场
- 验证入口继续`tests/UI`
- 当前正式验证入口继续固定`tests/UI`
- `new_editor/` 只保留未来正式编辑器真正需要的宿主、装配、资源与业务层结构
- `new_editor/` 只做宿主装配与后续业务承载,不承担当前基础层验证入口职责
---
@@ -223,10 +231,10 @@
### 6.1 视觉与样式层
- 已建立足够厚的 Editor token 体系
- widget 的 palette / metrics 基本完成 theme 驱动
- menu / popup / tab / panel / property / list / tree / status / scrollbar / splitter 都已接入 Editor 主题
- 新主题能稳定逼近旧 editor 的视觉基线
- 已建立统一的 Editor 固定样式常量出口
- widget 的 palette / metrics 不再分散硬编码,而是通过统一代码入口收口
- menu / popup / tab / panel / property / list / tree / status / scrollbar / splitter 都已切到同一套 Editor 固定样式语义
- 默认样式已稳定逼近旧 editor 的视觉基线
### 6.2 字段件层
@@ -250,16 +258,16 @@
## 7. 分阶段执行计划
## Phase AEditor主题系统重构
## Phase AEditor固定样式系统收口
### 目标
`Editor` 当前“薄主题 + 硬编码 widget 视觉”的状态,重构为“厚 token + 统一默认 Editor Theme”
`Editor` 当前分散的硬编码视觉常量,收口为统一的固定代码样式体系
### 任务
- 定义 Editor 主题 token 命名规范。
- 覆盖以下语义槽位:
- 定义统一的 Editor 样式常量与 helper 命名规范。
- 收口以下语义槽位:
- workspace
- panel
- header
@@ -273,13 +281,13 @@
- splitter
- scrollbar
- selection / hover / active / focus
- 把现有 widget 中的默认 palette / metrics 尽量迁出为 Editor theme 可控项
- 把现有 widget 中分散的默认 palette / metrics 汇总到统一代码出口
- 建立“旧 editor 风格对齐”的基准场景与截图检查。
### 完成标准
- 主题资源文件已足够控制主要 Editor 视觉
- 修改主题时,不再需要频繁改 widget 绘制代码
- Editor 默认视觉不再依赖主题解析测试
- 调整 Editor 默认样式时,改动集中在统一代码出口,而不是散落在各控件里
## Phase B字段件体系补齐
@@ -306,12 +314,12 @@
- `TextField` 已完成正式接入:
- `XCEditor` 已提供 `UIEditorTextField``UIEditorTextFieldInteraction`
- `UIEditorTheme` 已补齐 `TextField` theme resolver 与 hosted builder
- 已补齐 `TextField` hosted builder 与固定样式装配入口
- `PropertyGrid``Text` 行已切到正式 `TextField` 复用链路
- `Vector2Field` 已完成第一批复合字段件接入:
- `XCEditor` 已提供 `UIEditorVector2Field``UIEditorVector2FieldInteraction`
- `UIEditorTheme` 已补齐 `Vector2Field` theme resolver 与 hosted builder
- `tests/UI/Editor` 已补齐 `Vector2Field` 的 layout / hit-test / interaction / theme 单测
- 已补齐 `Vector2Field` hosted builder 与固定样式装配入口
- `tests/UI/Editor` 已补齐 `Vector2Field` 的 layout / hit-test / interaction 单测
- `editor_ui_vector2_field_basic_validation` 已可编译、运行、自动截图,并已接入 `editor_ui_integration_tests`
- 当前 `tests/UI/Editor` 中,这两批字段件都遵守同一条验证规范:
- 顶部必须明确写“这个测试验证什么功能”
@@ -452,17 +460,14 @@
### 9.2 已被覆盖的旧结论
本节即当前归档说明。仓库当前未单独维护 `docs/plan/used` 目录时,凡被本节点名覆盖的旧口径,一律视为已归档、不再执行。
以下旧结论不再作为当前主线执行依据:
- “后续主要目标是把 XCUI 直接嵌回旧 `editor/` 并替掉 ImGui”
- “`new_editor/` 长期只作为临时沙盒存在”
- “可以在 `new_editor/` 中继续堆各类试验面板作为主验证入口”
---
补充归档结论:
- `docs/plan/used/XCUI_Phase_Status_2026-04-05.md` 已转入归档,不再作为当前主线执行依据。
- “可以在 `Core / Editor` 基础层未完成前,直接进入 `new_editor` 业务面板开发”
## 10. 当前结论

View File

@@ -164,18 +164,20 @@ project/
tests/
UI/
```
### 4.2.1 当前执行覆盖规则2026-04-06
### 4.2.1 当前执行覆盖规则2026-04-09
以下规则在当前阶段覆盖本节中所有与目录落点有关的模糊表述:
- `editor/` 当前视为 ImGui 版本冻结区;在 XCUI editor shell 成熟前,不直接在该目录中推进替换开发。
- `tests/UI` 是当前 `Editor` 基础层的唯一实验场;所有基础能力验证、交互试验、状态流检查都必须优先放在 `tests/UI/Editor/unit``tests/UI/Editor/integration`
- `new_editor/` 当前不作为试验场;它承载 `Editor` 基础层库与临时宿主骨架,禁止往其中追加业务面板、临时验证逻辑或与 `tests/UI` 重复的实验入口。
- `tests/UI` 是当前 XCUI 的正式验证入口,也是 `Editor` 基础层的唯一实验场;所有基础能力验证、交互试验、状态流检查都必须优先放在 `tests/UI/Editor/unit``tests/UI/Editor/integration`
- `new_editor/` 当前不作为试验场,也不是验证入口;它承载未来 editor 重建所需的 `Editor` 基础层库与宿主骨架,禁止往其中追加业务面板、临时验证逻辑或与 `tests/UI` 重复的实验入口。
- `new_editor/` 当前目录结构按库式 `Editor UI` 形态维护:公共头放在 `include/XCEditor/Core|Widgets`,实现放在 `src/Core|Widgets`,宿主代码放在 `app/Application.*|Host/*`,不再把库层与宿主层揉成一套业务目录。
- `Core / Runtime` 当前仍可继续推进资源化、热重载与资源驱动验证。
- `Editor` 当前采用固定代码样式Editor 默认样式、palette、metrics 与视觉语义由代码层维护,不再把 Editor 主题解析作为当前主线,也不在基础层未完成前把业务推进到 `new_editor/`
- `engine/UI` 当前继续只放 `Core / Runtime / shared` 部分,不再继续沉积 editor-only 代码。
- 等 `Editor` 基础层在 `tests/UI/Editor` 中稳定收口后,再规划从 `new_editor/` 迁入正式 `editor/` 的接入阶段,而不是继续在旧 `editor/``new_editor/` 之间双线推进。
#### 当前过渡期目录(自 2026-04-06 起执行)
#### 当前过渡期目录(自 2026-04-09 起执行)
```text
engine/
include/XCEngine/UI/
@@ -194,7 +196,7 @@ editor/
tests/
UI/
# XCUI 验证体系入口,不承载正式 editor 实现
# XCUI 正式验证体系入口,不承载正式 editor 实现
```
### 4.3 模块划分
@@ -383,6 +385,7 @@ Markup 中支持的表达式范围只建议包括:
### 7.2 `.xctheme`
用于定义 theme token 和 widget style。
当前执行口径补充:`.xctheme` 的主适用范围是 `Core / Runtime / shared``Editor` 当前不以 `.xctheme` 解析作为样式主线。
建议结构:
@@ -1137,6 +1140,8 @@ V1 必须具备:
## 22. 测试策略
当前正式验证入口固定为 `tests/UI``Core / Runtime / Editor` 三层都在该测试树下收口;`new_editor` 只承担未来产品宿主与装配冒烟,不承担当前基础层验证入口职责。
建议新增测试目录:
```text
@@ -1414,21 +1419,22 @@ tests/UI/Editor/
---
## 24.1 当前执行优先级修正2026-04-06
## 24.1 当前执行优先级修正2026-04-09
近期执行顺序调整如下:
1. 冻结当前基于 ImGui 的 `editor/` 目录,不把它作为本阶段 XCUI 替换开发的主工作区。
2. 当前阶段所有 `Editor` 基础层实验、交互验证、状态流验证,一律放在 `tests/UI/Editor` 中完成。
3. `new_editor/` 不作为试验场;当前只允许维护 `Editor` 基础层库与宿主骨架,不往里面追加业务面板或验证逻辑。
2. 当前阶段所有 `Editor` 基础层实验、交互验证、状态流验证,一律放在 `tests/UI/Editor` 中完成`tests/UI` 是唯一正式验证入口
3. `new_editor/` 不作为试验场,也不是当前验证入口;当前只允许维护 `Editor` 基础层库与宿主骨架,不往里面追加业务面板或验证逻辑。
4. 在具体 editor 面板之前,优先完成 editor shell 基础能力:
- Splitter / pane resize
- Tab strip
- Workspace compose
- Hierarchy / Inspector / Console 所需的 editor-only 基础件
- 固定代码样式下的视觉常量、结构语义与交互收口
5. `Runtime UI` 仍然是 XCUI 的长期目标,但在 editor shell 替换路径稳定之前,不作为当前主线。
### 24.2 Core当前阶段判断与Editor推进边界2026-04-06
### 24.2 Core当前阶段判断与Editor推进边界2026-04-09
当前判断:
@@ -1438,10 +1444,11 @@ tests/UI/Editor/
执行硬规则:
- `Editor` 当前阶段只负责 editor shell、panel 生命周期、workspace 装配、menu/shortcut 这类 editor-only 上层能力。
- `Core / Runtime` 仍可继续走资源化路线;`Editor` 当前默认样式固定在代码层,不再围绕 Editor 主题解析扩展主线。
- 凡是发现 `layout / input / style / text / render contract / shared widget` 等共享能力缺口,必须优先回补到 `Core` 或 shared UI 层。
- 禁止在 `Editor` 层硬写临时替代实现去绕过 `Core` 缺口;否则后面迁移到正式 editor 时会再次返工。
- `tests/UI/Editor` 是当前 `Editor` 基础层的唯一实验与验证入口;需要人工操作检查的内容,也必须做成这里的集成测试场景。
- `new_editor/` 当前不作为试验场,但作为 `Editor` 基础层库与临时宿主骨架存在;允许承载迁移必需的基础层代码,不允许堆业务面板或测试验证逻辑。
- `new_editor/` 当前不作为试验场,但作为未来 editor 重建的宿主骨架存在;允许承载迁移必需的基础层代码,不允许堆业务面板或测试验证逻辑。
- `tests/UI/Editor` 当前只验证 `Editor` 基础壳层与状态流,不提前承担具体业务面板复刻。
### 24.3 Editor基础层当前推进顺序2026-04-06

View File

@@ -11,14 +11,21 @@
#include "SceneCommands.h"
#include "Utils/FileDialogUtils.h"
#include "Utils/ProjectFileUtils.h"
#include "Utils/UndoUtils.h"
#include <XCEngine/Core/Asset/ResourceManager.h>
#include <XCEngine/Components/MeshFilterComponent.h>
#include <XCEngine/Components/MeshRendererComponent.h>
#include <XCEngine/Debug/Logger.h>
#include <XCEngine/Scene/ModelSceneInstantiation.h>
#include <algorithm>
#include <cwctype>
#include <filesystem>
#include <fstream>
#include <sstream>
#include <string>
#include <unordered_map>
namespace XCEngine {
namespace Editor {
@@ -56,12 +63,181 @@ inline bool IsSameOrDescendantProjectPath(const std::filesystem::path& path, con
return pathKey.rfind(rootKey, 0) == 0;
}
inline bool HasVirtualMaterialPathScheme(const std::string& path) {
return path.find("://") != std::string::npos;
}
inline std::filesystem::path GetModelMaterialOverridePath(const std::string& modelFullPath) {
return std::filesystem::path(modelFullPath + ".materialmap");
}
inline std::vector<std::string> SplitOverrideMaterialPaths(const std::string& value) {
std::vector<std::string> paths;
if (value.empty()) {
return paths;
}
size_t start = 0;
while (true) {
const size_t separator = value.find('|', start);
const std::string token = separator == std::string::npos
? value.substr(start)
: value.substr(start, separator - start);
paths.push_back(ProjectFileUtils::Trim(token));
if (separator == std::string::npos) {
break;
}
start = separator + 1;
}
return paths;
}
inline bool LoadModelMaterialOverrides(
const std::filesystem::path& overridePath,
std::unordered_map<::XCEngine::Resources::LocalID, std::vector<std::string>>& outOverrides,
std::string* outErrorMessage = nullptr) {
outOverrides.clear();
std::ifstream input(overridePath);
if (!input.is_open()) {
if (outErrorMessage != nullptr) {
*outErrorMessage = "Failed to open material override file.";
}
return false;
}
std::string line;
size_t lineNumber = 0;
while (std::getline(input, line)) {
++lineNumber;
const std::string trimmed = ProjectFileUtils::Trim(line);
if (trimmed.empty() || trimmed[0] == '#') {
continue;
}
const size_t separator = trimmed.find('=');
if (separator == std::string::npos) {
if (outErrorMessage != nullptr) {
*outErrorMessage =
"Missing '=' on line " + std::to_string(lineNumber) + " in material override file.";
}
return false;
}
const std::string meshLocalIdText = ProjectFileUtils::Trim(trimmed.substr(0, separator));
const std::string materialPathsText = ProjectFileUtils::Trim(trimmed.substr(separator + 1));
if (meshLocalIdText.empty() || materialPathsText.empty()) {
if (outErrorMessage != nullptr) {
*outErrorMessage =
"Incomplete override entry on line " + std::to_string(lineNumber) + " in material override file.";
}
return false;
}
try {
const auto meshLocalId =
static_cast<::XCEngine::Resources::LocalID>(std::stoull(meshLocalIdText));
outOverrides[meshLocalId] = SplitOverrideMaterialPaths(materialPathsText);
} catch (...) {
if (outErrorMessage != nullptr) {
*outErrorMessage =
"Invalid mesh localID on line " + std::to_string(lineNumber) + " in material override file.";
}
return false;
}
}
return true;
}
inline void ApplyModelMaterialOverrides(
IEditorContext& context,
const std::string& modelFullPath,
const ::XCEngine::ModelSceneInstantiationResult& result) {
namespace fs = std::filesystem;
const fs::path overridePath = GetModelMaterialOverridePath(modelFullPath);
if (!fs::exists(overridePath) || !fs::is_regular_file(overridePath)) {
return;
}
std::unordered_map<::XCEngine::Resources::LocalID, std::vector<std::string>> overrides;
std::string errorMessage;
if (!LoadModelMaterialOverrides(overridePath, overrides, &errorMessage)) {
::XCEngine::Debug::Logger::Get().Error(
::XCEngine::Debug::LogCategory::FileSystem,
::XCEngine::Containers::String("[Editor] Failed to parse model material overrides: ") +
::XCEngine::Containers::String(overridePath.string().c_str()) +
" error=" +
::XCEngine::Containers::String(errorMessage.c_str()));
return;
}
if (overrides.empty()) {
return;
}
auto& resourceManager = ::XCEngine::Resources::ResourceManager::Get();
resourceManager.Initialize();
if (!context.GetProjectPath().empty() &&
std::string(resourceManager.GetResourceRoot().CStr()) != context.GetProjectPath()) {
resourceManager.SetResourceRoot(context.GetProjectPath().c_str());
}
for (auto* meshObject : result.meshObjects) {
if (meshObject == nullptr) {
continue;
}
auto* meshFilter = meshObject->GetComponent<::XCEngine::Components::MeshFilterComponent>();
auto* meshRenderer = meshObject->GetComponent<::XCEngine::Components::MeshRendererComponent>();
if (meshFilter == nullptr || meshRenderer == nullptr) {
continue;
}
const auto& meshRef = meshFilter->GetMeshAssetRef();
if (!meshRef.IsValid()) {
continue;
}
const auto overrideIt = overrides.find(meshRef.localID);
if (overrideIt == overrides.end()) {
continue;
}
const auto& slotMaterialPaths = overrideIt->second;
for (size_t slotIndex = 0; slotIndex < slotMaterialPaths.size(); ++slotIndex) {
const std::string& materialPath = slotMaterialPaths[slotIndex];
if (materialPath.empty()) {
continue;
}
if (HasVirtualMaterialPathScheme(materialPath)) {
meshRenderer->SetMaterialPath(slotIndex, materialPath);
continue;
}
::XCEngine::Resources::AssetRef materialRef;
if (resourceManager.TryGetAssetRef(materialPath.c_str(), ::XCEngine::Resources::ResourceType::Material, materialRef)) {
meshRenderer->SetMaterialAssetRef(slotIndex, materialRef);
continue;
}
meshRenderer->SetMaterialPath(slotIndex, materialPath);
}
}
}
} // namespace detail
inline bool CanOpenAsset(const AssetItemPtr& item) {
return item != nullptr && (item->isFolder || item->type == "Scene");
}
inline bool IsModelAsset(const AssetItemPtr& item) {
return item != nullptr && !item->isFolder && item->type == "Model";
}
inline bool OpenAsset(IEditorContext& context, const AssetItemPtr& item) {
if (!CanOpenAsset(item)) {
return false;
@@ -349,6 +525,63 @@ inline Resources::ResourceManager& GetProjectResourceManager(IEditorContext& con
return resourceManager;
}
inline bool CanInstantiateModelAsset(const IEditorContext& context, const AssetItemPtr& item) {
return IsProjectDocumentEditingAllowed(context) &&
!context.GetProjectPath().empty() &&
IsModelAsset(item);
}
inline ::XCEngine::Components::GameObject* InstantiateModelAsset(
IEditorContext& context,
const AssetItemPtr& item,
::XCEngine::Components::GameObject* parent = nullptr,
const std::string& commandLabel = "Instantiate Model") {
if (!CanInstantiateModelAsset(context, item)) {
return nullptr;
}
GetProjectResourceManager(context);
::XCEngine::Components::GameObject* created = nullptr;
UndoUtils::ExecuteSceneCommand(context, commandLabel, [&]() {
auto& sceneManager = context.GetSceneManager();
if (!sceneManager.HasActiveScene() || sceneManager.GetScene() == nullptr) {
sceneManager.NewScene("Untitled Scene");
}
::XCEngine::Components::Scene* scene = sceneManager.GetScene();
if (scene == nullptr) {
return;
}
::XCEngine::ModelSceneInstantiationResult result;
::XCEngine::Containers::String errorMessage;
if (!::XCEngine::InstantiateModelHierarchy(
*scene,
item->fullPath.c_str(),
parent,
&result,
&errorMessage)) {
::XCEngine::Debug::Logger::Get().Error(
::XCEngine::Debug::LogCategory::FileSystem,
::XCEngine::Containers::String("[Editor] InstantiateModelAsset failed path=") +
::XCEngine::Containers::String(item->fullPath.c_str()) +
" error=" +
errorMessage);
return;
}
created = result.rootObject;
detail::ApplyModelMaterialOverrides(context, item->fullPath, result);
sceneManager.NotifyExternalSceneMutation(
created != nullptr ? created->GetID() : ::XCEngine::Components::GameObject::INVALID_ID);
if (created != nullptr) {
context.GetSelectionManager().SetSelectedEntity(created->GetID());
}
});
return created;
}
inline bool CanReimportSelectedAsset(IEditorContext& context) {
if (!CanManageProjectAssetCache(context)) {
return false;

View File

@@ -68,6 +68,7 @@ public:
virtual const ::XCEngine::Components::Scene* GetScene() const = 0;
virtual SceneLoadProgressSnapshot GetSceneLoadProgress() const = 0;
virtual void NotifySceneViewportFramePresented(std::uint32_t pendingAsyncLoads) = 0;
virtual void NotifyExternalSceneMutation(::XCEngine::Components::GameObject::ID primaryEntityId = 0) {}
virtual SceneSnapshot CaptureSceneSnapshot() const = 0;
virtual bool RestoreSceneSnapshot(const SceneSnapshot& snapshot) = 0;
virtual void CreateDemoScene() = 0;

View File

@@ -3,6 +3,7 @@
#include "Core/EditorEvents.h"
#include "Utils/ProjectFileUtils.h"
#include <XCEngine/Core/Asset/ResourceManager.h>
#include <XCEngine/Debug/Logger.h>
#include <XCEngine/Components/ComponentFactoryRegistry.h>
#include <XCEngine/Components/CameraComponent.h>
#include <XCEngine/Components/LightComponent.h>
@@ -23,6 +24,12 @@ std::pair<std::string, std::string> SerializeComponent(const ::XCEngine::Compone
return { component->GetName(), payload.str() };
}
void LogSceneLoadTrace(const std::string& message) {
::XCEngine::Containers::String entry("[SceneLoadTrace] ");
entry += message.c_str();
::XCEngine::Debug::Logger::Get().Info(::XCEngine::Debug::LogCategory::General, entry);
}
} // namespace
SceneManager::SceneManager(EventBus* eventBus)
@@ -405,6 +412,11 @@ void SceneManager::NotifySceneViewportFramePresented(std::uint32_t pendingAsyncL
m_sceneLoadProgress.firstFrameAtMs = nowMs;
m_sceneLoadProgress.interactive = true;
m_sceneLoadProgress.interactiveAtMs = nowMs;
LogSceneLoadTrace(
"FirstFramePresented elapsed_ms=" +
std::to_string(nowMs - m_sceneLoadProgress.startedAtMs) +
" pending_async=" +
std::to_string(pendingAsyncLoads));
}
if (pendingAsyncLoads == 0) {
@@ -412,6 +424,16 @@ void SceneManager::NotifySceneViewportFramePresented(std::uint32_t pendingAsyncL
m_sceneLoadProgress.streamingCompletedAtMs = nowMs;
m_sceneLoadProgress.inProgress = false;
m_sceneLoadProgress.message = "Scene ready.";
LogSceneLoadTrace(
"SceneReady elapsed_ms=" +
std::to_string(nowMs - m_sceneLoadProgress.startedAtMs) +
" first_frame_ms=" +
std::to_string(
m_sceneLoadProgress.firstFrameAtMs >= m_sceneLoadProgress.startedAtMs
? (m_sceneLoadProgress.firstFrameAtMs - m_sceneLoadProgress.startedAtMs)
: 0ull) +
" peak_pending_async=" +
std::to_string(m_sceneLoadProgress.peakPendingAsyncLoads));
return;
}
@@ -420,6 +442,23 @@ void SceneManager::NotifySceneViewportFramePresented(std::uint32_t pendingAsyncL
"Runtime streaming scene assets... (" + std::to_string(pendingAsyncLoads) + ")";
}
void SceneManager::NotifyExternalSceneMutation(::XCEngine::Components::GameObject::ID primaryEntityId) {
SyncRootEntities();
MarkSceneDirty();
if (primaryEntityId != ::XCEngine::Components::GameObject::INVALID_ID) {
OnEntityCreated.Invoke(primaryEntityId);
if (m_eventBus) {
m_eventBus->Publish(EntityCreatedEvent{ primaryEntityId });
}
}
OnSceneChanged.Invoke();
if (m_eventBus) {
m_eventBus->Publish(SceneChangedEvent{});
}
}
void SceneManager::RenameEntity(::XCEngine::Components::GameObject::ID id, const std::string& newName) {
if (!m_scene) return;

View File

@@ -69,6 +69,7 @@ public:
const ::XCEngine::Components::Scene* GetScene() const override { return m_scene.get(); }
SceneLoadProgressSnapshot GetSceneLoadProgress() const override { return m_sceneLoadProgress; }
void NotifySceneViewportFramePresented(std::uint32_t pendingAsyncLoads) override;
void NotifyExternalSceneMutation(::XCEngine::Components::GameObject::ID primaryEntityId = 0) override;
SceneSnapshot CaptureSceneSnapshot() const override;
bool RestoreSceneSnapshot(const SceneSnapshot& snapshot) override;
void CreateDemoScene() override;

View File

@@ -1,6 +1,7 @@
#include "Passes/SceneViewportEditorOverlayPass.h"
#include "Viewport/SceneViewportMath.h"
#include "Rendering/Internal/RenderSurfacePipelineUtils.h"
#include <XCEngine/RHI/RHIBuffer.h>
#include <XCEngine/RHI/RHICommandList.h>
@@ -187,14 +188,16 @@ private:
RHI::GraphicsPipelineDesc BuildLinePipelineDesc(
RHI::RHIPipelineLayout* pipelineLayout,
const Rendering::RenderSurface& surface,
bool depthTestEnabled) {
RHI::GraphicsPipelineDesc pipelineDesc = {};
pipelineDesc.pipelineLayout = pipelineLayout;
pipelineDesc.topologyType = static_cast<uint32_t>(RHI::PrimitiveTopologyType::Line);
pipelineDesc.renderTargetCount = 1;
pipelineDesc.renderTargetFormats[0] = static_cast<uint32_t>(RHI::Format::R8G8B8A8_UNorm);
pipelineDesc.depthStencilFormat = static_cast<uint32_t>(RHI::Format::D24_UNorm_S8_UInt);
pipelineDesc.sampleCount = 1;
::XCEngine::Rendering::Internal::ApplySingleColorAttachmentPropertiesToGraphicsPipelineDesc(
surface,
pipelineDesc);
pipelineDesc.depthStencilFormat =
static_cast<uint32_t>(::XCEngine::Rendering::Internal::ResolveSurfaceDepthFormat(surface));
pipelineDesc.inputLayout.elements = {
{ "POSITION", 0, static_cast<uint32_t>(RHI::Format::R32G32B32_Float), 0, 0, 0, 0 },
@@ -238,14 +241,16 @@ RHI::GraphicsPipelineDesc BuildLinePipelineDesc(
RHI::GraphicsPipelineDesc BuildSpritePipelineDesc(
RHI::RHIPipelineLayout* pipelineLayout,
const Rendering::RenderSurface& surface,
bool depthTestEnabled) {
RHI::GraphicsPipelineDesc pipelineDesc = {};
pipelineDesc.pipelineLayout = pipelineLayout;
pipelineDesc.topologyType = static_cast<uint32_t>(RHI::PrimitiveTopologyType::Triangle);
pipelineDesc.renderTargetCount = 1;
pipelineDesc.renderTargetFormats[0] = static_cast<uint32_t>(RHI::Format::R8G8B8A8_UNorm);
pipelineDesc.depthStencilFormat = static_cast<uint32_t>(RHI::Format::D24_UNorm_S8_UInt);
pipelineDesc.sampleCount = 1;
::XCEngine::Rendering::Internal::ApplySingleColorAttachmentPropertiesToGraphicsPipelineDesc(
surface,
pipelineDesc);
pipelineDesc.depthStencilFormat =
static_cast<uint32_t>(::XCEngine::Rendering::Internal::ResolveSurfaceDepthFormat(surface));
pipelineDesc.inputLayout.elements = {
{ "POSITION", 0, static_cast<uint32_t>(RHI::Format::R32G32B32_Float), 0, 0, 0, 0 },
@@ -292,14 +297,16 @@ RHI::GraphicsPipelineDesc BuildSpritePipelineDesc(
RHI::GraphicsPipelineDesc BuildScreenTrianglePipelineDesc(
RHI::RHIPipelineLayout* pipelineLayout,
const Rendering::RenderSurface& surface,
bool depthTestEnabled) {
RHI::GraphicsPipelineDesc pipelineDesc = {};
pipelineDesc.pipelineLayout = pipelineLayout;
pipelineDesc.topologyType = static_cast<uint32_t>(RHI::PrimitiveTopologyType::Triangle);
pipelineDesc.renderTargetCount = 1;
pipelineDesc.renderTargetFormats[0] = static_cast<uint32_t>(RHI::Format::R8G8B8A8_UNorm);
pipelineDesc.depthStencilFormat = static_cast<uint32_t>(RHI::Format::D24_UNorm_S8_UInt);
pipelineDesc.sampleCount = 1;
::XCEngine::Rendering::Internal::ApplySingleColorAttachmentPropertiesToGraphicsPipelineDesc(
surface,
pipelineDesc);
pipelineDesc.depthStencilFormat =
static_cast<uint32_t>(::XCEngine::Rendering::Internal::ResolveSurfaceDepthFormat(surface));
pipelineDesc.inputLayout.elements = {
{ "POSITION", 0, static_cast<uint32_t>(RHI::Format::R32G32_Float), 0, 0, 0, 0 },
@@ -382,7 +389,20 @@ bool SceneViewportEditorOverlayPassRenderer::Render(
return true;
}
if (!EnsureInitialized(renderContext)) {
const std::vector<RHI::RHIResourceView*>& colorAttachments = surface.GetColorAttachments();
if (!::XCEngine::Rendering::Internal::HasSingleColorAttachment(surface) ||
colorAttachments.empty() ||
colorAttachments[0] == nullptr ||
surface.GetDepthAttachment() == nullptr) {
return false;
}
const Math::RectInt renderArea = surface.GetRenderArea();
if (renderArea.width <= 0 || renderArea.height <= 0) {
return false;
}
if (!EnsureInitialized(renderContext, surface)) {
return false;
}
@@ -451,41 +471,43 @@ bool SceneViewportEditorOverlayPassRenderer::Render(
OverlayConstants constants = {};
constants.viewProjection = BuildSceneViewportViewProjectionMatrix(
frameData.overlay,
static_cast<float>(surface.GetWidth()),
static_cast<float>(surface.GetHeight())).Transpose();
static_cast<float>(renderArea.width),
static_cast<float>(renderArea.height)).Transpose();
constants.viewportSizeAndInvSize = Math::Vector4(
static_cast<float>(surface.GetWidth()),
static_cast<float>(surface.GetHeight()),
surface.GetWidth() > 0u ? 1.0f / static_cast<float>(surface.GetWidth()) : 0.0f,
surface.GetHeight() > 0u ? 1.0f / static_cast<float>(surface.GetHeight()) : 0.0f);
static_cast<float>(renderArea.width),
static_cast<float>(renderArea.height),
renderArea.width > 0 ? 1.0f / static_cast<float>(renderArea.width) : 0.0f,
renderArea.height > 0 ? 1.0f / static_cast<float>(renderArea.height) : 0.0f);
m_constantSet->WriteConstant(0, &constants, sizeof(constants));
RHI::RHICommandList* commandList = renderContext.commandList;
const std::vector<RHI::RHIResourceView*>& colorAttachments = surface.GetColorAttachments();
if (colorAttachments.empty() || colorAttachments[0] == nullptr) {
return false;
}
RHI::RHIResourceView* renderTarget = colorAttachments[0];
commandList->TransitionBarrier(
renderTarget,
surface.GetColorStateAfter(),
RHI::ResourceStates::RenderTarget);
commandList->SetRenderTargets(1, &renderTarget, surface.GetDepthAttachment());
RHI::RHIResourceView* depthAttachment = surface.GetDepthAttachment();
if (surface.IsAutoTransitionEnabled()) {
commandList->TransitionBarrier(
renderTarget,
surface.GetColorStateAfter(),
RHI::ResourceStates::RenderTarget);
commandList->TransitionBarrier(
depthAttachment,
surface.GetDepthStateAfter(),
RHI::ResourceStates::DepthWrite);
}
commandList->SetRenderTargets(1, &renderTarget, depthAttachment);
const RHI::Viewport viewport = {
0.0f,
0.0f,
static_cast<float>(surface.GetWidth()),
static_cast<float>(surface.GetHeight()),
static_cast<float>(renderArea.x),
static_cast<float>(renderArea.y),
static_cast<float>(renderArea.width),
static_cast<float>(renderArea.height),
0.0f,
1.0f
};
const RHI::Rect scissorRect = {
0,
0,
static_cast<int32_t>(surface.GetWidth()),
static_cast<int32_t>(surface.GetHeight())
renderArea.x,
renderArea.y,
renderArea.x + renderArea.width,
renderArea.y + renderArea.height
};
commandList->SetViewport(viewport);
commandList->SetScissorRect(scissorRect);
@@ -540,15 +562,30 @@ bool SceneViewportEditorOverlayPassRenderer::Render(
}
}
commandList->TransitionBarrier(
renderTarget,
RHI::ResourceStates::RenderTarget,
surface.GetColorStateAfter());
commandList->EndRenderPass();
if (surface.IsAutoTransitionEnabled()) {
commandList->TransitionBarrier(
renderTarget,
RHI::ResourceStates::RenderTarget,
surface.GetColorStateAfter());
commandList->TransitionBarrier(
depthAttachment,
RHI::ResourceStates::DepthWrite,
surface.GetDepthStateAfter());
}
return true;
}
bool SceneViewportEditorOverlayPassRenderer::EnsureInitialized(
const Rendering::RenderContext& renderContext) {
const Rendering::RenderContext& renderContext,
const Rendering::RenderSurface& surface) {
const RHI::Format renderTargetFormat =
::XCEngine::Rendering::Internal::ResolveSurfaceColorFormat(surface, 0u);
const RHI::Format depthStencilFormat =
::XCEngine::Rendering::Internal::ResolveSurfaceDepthFormat(surface);
const uint32_t renderTargetSampleCount =
::XCEngine::Rendering::Internal::ResolveSurfaceSampleCount(surface);
if (m_device == renderContext.device &&
m_backendType == renderContext.backendType &&
m_linePipelineLayout != nullptr &&
@@ -564,22 +601,35 @@ bool SceneViewportEditorOverlayPassRenderer::EnsureInitialized(
m_samplerPool != nullptr &&
m_constantSet != nullptr &&
m_samplerSet != nullptr &&
m_sampler != nullptr) {
m_sampler != nullptr &&
m_renderTargetFormat == renderTargetFormat &&
m_depthStencilFormat == depthStencilFormat &&
m_renderTargetSampleCount == renderTargetSampleCount) {
return true;
}
DestroyResources();
return CreateResources(renderContext);
return CreateResources(renderContext, surface);
}
bool SceneViewportEditorOverlayPassRenderer::CreateResources(
const Rendering::RenderContext& renderContext) {
if (!renderContext.IsValid() || renderContext.backendType != RHI::RHIType::D3D12) {
const Rendering::RenderContext& renderContext,
const Rendering::RenderSurface& surface) {
if (!renderContext.IsValid()) {
return false;
}
if (!::XCEngine::Rendering::Internal::HasSingleColorAttachment(surface) ||
::XCEngine::Rendering::Internal::ResolveSurfaceColorFormat(surface, 0u) == RHI::Format::Unknown ||
::XCEngine::Rendering::Internal::ResolveSurfaceDepthFormat(surface) == RHI::Format::Unknown) {
return false;
}
m_device = renderContext.device;
m_backendType = renderContext.backendType;
m_renderTargetFormat = ::XCEngine::Rendering::Internal::ResolveSurfaceColorFormat(surface, 0u);
m_depthStencilFormat = ::XCEngine::Rendering::Internal::ResolveSurfaceDepthFormat(surface);
m_renderTargetSampleCount = ::XCEngine::Rendering::Internal::ResolveSurfaceSampleCount(surface);
RHI::DescriptorSetLayoutBinding constantBinding = {};
constantBinding.binding = 0;
@@ -691,7 +741,7 @@ bool SceneViewportEditorOverlayPassRenderer::CreateResources(
m_samplerSet->UpdateSampler(0, m_sampler);
const RHI::GraphicsPipelineDesc depthTestedLineDesc =
BuildLinePipelineDesc(m_linePipelineLayout, true);
BuildLinePipelineDesc(m_linePipelineLayout, surface, true);
m_depthTestedLinePipelineState = m_device->CreatePipelineState(depthTestedLineDesc);
if (m_depthTestedLinePipelineState == nullptr || !m_depthTestedLinePipelineState->IsValid()) {
DestroyResources();
@@ -699,7 +749,7 @@ bool SceneViewportEditorOverlayPassRenderer::CreateResources(
}
const RHI::GraphicsPipelineDesc alwaysOnTopLineDesc =
BuildLinePipelineDesc(m_linePipelineLayout, false);
BuildLinePipelineDesc(m_linePipelineLayout, surface, false);
m_alwaysOnTopLinePipelineState = m_device->CreatePipelineState(alwaysOnTopLineDesc);
if (m_alwaysOnTopLinePipelineState == nullptr || !m_alwaysOnTopLinePipelineState->IsValid()) {
DestroyResources();
@@ -707,7 +757,7 @@ bool SceneViewportEditorOverlayPassRenderer::CreateResources(
}
const RHI::GraphicsPipelineDesc depthTestedScreenTriangleDesc =
BuildScreenTrianglePipelineDesc(m_linePipelineLayout, true);
BuildScreenTrianglePipelineDesc(m_linePipelineLayout, surface, true);
m_depthTestedScreenTrianglePipelineState = m_device->CreatePipelineState(depthTestedScreenTriangleDesc);
if (m_depthTestedScreenTrianglePipelineState == nullptr ||
!m_depthTestedScreenTrianglePipelineState->IsValid()) {
@@ -716,7 +766,7 @@ bool SceneViewportEditorOverlayPassRenderer::CreateResources(
}
const RHI::GraphicsPipelineDesc alwaysOnTopScreenTriangleDesc =
BuildScreenTrianglePipelineDesc(m_linePipelineLayout, false);
BuildScreenTrianglePipelineDesc(m_linePipelineLayout, surface, false);
m_alwaysOnTopScreenTrianglePipelineState = m_device->CreatePipelineState(alwaysOnTopScreenTriangleDesc);
if (m_alwaysOnTopScreenTrianglePipelineState == nullptr ||
!m_alwaysOnTopScreenTrianglePipelineState->IsValid()) {
@@ -725,7 +775,7 @@ bool SceneViewportEditorOverlayPassRenderer::CreateResources(
}
const RHI::GraphicsPipelineDesc depthTestedSpriteDesc =
BuildSpritePipelineDesc(m_spritePipelineLayout, true);
BuildSpritePipelineDesc(m_spritePipelineLayout, surface, true);
m_depthTestedSpritePipelineState = m_device->CreatePipelineState(depthTestedSpriteDesc);
if (m_depthTestedSpritePipelineState == nullptr || !m_depthTestedSpritePipelineState->IsValid()) {
DestroyResources();
@@ -733,7 +783,7 @@ bool SceneViewportEditorOverlayPassRenderer::CreateResources(
}
const RHI::GraphicsPipelineDesc alwaysOnTopSpriteDesc =
BuildSpritePipelineDesc(m_spritePipelineLayout, false);
BuildSpritePipelineDesc(m_spritePipelineLayout, surface, false);
m_alwaysOnTopSpritePipelineState = m_device->CreatePipelineState(alwaysOnTopSpriteDesc);
if (m_alwaysOnTopSpritePipelineState == nullptr || !m_alwaysOnTopSpritePipelineState->IsValid()) {
DestroyResources();
@@ -986,6 +1036,9 @@ void SceneViewportEditorOverlayPassRenderer::DestroyResources() {
m_lineVertexBufferCapacity = 0u;
m_screenTriangleVertexBufferCapacity = 0u;
m_spriteVertexBufferCapacity = 0u;
m_renderTargetFormat = RHI::Format::Unknown;
m_depthStencilFormat = RHI::Format::Unknown;
m_renderTargetSampleCount = 1u;
m_device = nullptr;
m_backendType = RHI::RHIType::D3D12;
}

View File

@@ -3,6 +3,7 @@
#include "Viewport/SceneViewportEditorOverlayData.h"
#include "Viewport/SceneViewportOverlaySpriteResources.h"
#include <XCEngine/RHI/RHIEnums.h>
#include <XCEngine/Rendering/RenderContext.h>
#include <XCEngine/Rendering/RenderPass.h>
#include <XCEngine/Rendering/RenderSurface.h>
@@ -39,8 +40,12 @@ public:
const SceneViewportOverlayFrameData& frameData);
private:
bool EnsureInitialized(const Rendering::RenderContext& renderContext);
bool CreateResources(const Rendering::RenderContext& renderContext);
bool EnsureInitialized(
const Rendering::RenderContext& renderContext,
const Rendering::RenderSurface& surface);
bool CreateResources(
const Rendering::RenderContext& renderContext,
const Rendering::RenderSurface& surface);
bool EnsureLineBufferCapacity(size_t requiredVertexCount);
bool EnsureScreenTriangleBufferCapacity(size_t requiredVertexCount);
bool EnsureSpriteBufferCapacity(size_t requiredVertexCount);
@@ -72,6 +77,9 @@ private:
uint64_t m_lineVertexBufferCapacity = 0;
uint64_t m_screenTriangleVertexBufferCapacity = 0;
uint64_t m_spriteVertexBufferCapacity = 0;
RHI::Format m_renderTargetFormat = RHI::Format::Unknown;
RHI::Format m_depthStencilFormat = RHI::Format::Unknown;
uint32_t m_renderTargetSampleCount = 1u;
SceneViewportOverlaySpriteResourceCache m_overlaySpriteResources = {};
};

View File

@@ -76,8 +76,12 @@ bool SceneViewportSelectionOutlinePassRenderer::Render(
return m_outlinePass.Render(
renderContext,
surface,
targets.selectionMaskShaderView,
targets.depthShaderView,
{
targets.selectionMaskShaderView,
targets.selectionMaskState,
targets.depthShaderView,
surface.GetDepthStateAfter()
},
ToBuiltinSceneViewportSelectionOutlineStyle(style));
}

View File

@@ -106,7 +106,7 @@ inline SceneViewportTransformGizmoHandleBuildInputs BuildSceneViewportTransformG
return inputs;
}
namespace Detail {
namespace Internal {
inline constexpr int kSceneViewportHandlePrioritySceneIcon = 100;
inline constexpr int kSceneViewportHandlePriorityRotateAxis = 311;
@@ -554,21 +554,21 @@ inline void AppendScaleGizmoScreenTriangles(
}
}
} // namespace Detail
} // namespace Internal
inline void AppendTransformGizmoHandleRecords(
SceneViewportOverlayFrameData& frameData,
const SceneViewportTransformGizmoHandleBuildInputs& inputs) {
if (inputs.moveGizmo != nullptr) {
Detail::AppendMoveGizmoHandleRecords(frameData, *inputs.moveGizmo, inputs.moveEntityId);
Internal::AppendMoveGizmoHandleRecords(frameData, *inputs.moveGizmo, inputs.moveEntityId);
}
if (inputs.rotateGizmo != nullptr) {
Detail::AppendRotateGizmoHandleRecords(frameData, *inputs.rotateGizmo, inputs.rotateEntityId);
Internal::AppendRotateGizmoHandleRecords(frameData, *inputs.rotateGizmo, inputs.rotateEntityId);
}
if (inputs.scaleGizmo != nullptr) {
Detail::AppendScaleGizmoHandleRecords(frameData, *inputs.scaleGizmo, inputs.scaleEntityId);
Internal::AppendScaleGizmoHandleRecords(frameData, *inputs.scaleGizmo, inputs.scaleEntityId);
}
}
@@ -576,15 +576,15 @@ inline void AppendTransformGizmoScreenTriangles(
SceneViewportOverlayFrameData& frameData,
const SceneViewportTransformGizmoHandleBuildInputs& inputs) {
if (inputs.moveGizmo != nullptr) {
Detail::AppendMoveGizmoScreenTriangles(frameData, *inputs.moveGizmo);
Internal::AppendMoveGizmoScreenTriangles(frameData, *inputs.moveGizmo);
}
if (inputs.rotateGizmo != nullptr) {
Detail::AppendRotateGizmoScreenTriangles(frameData, *inputs.rotateGizmo);
Internal::AppendRotateGizmoScreenTriangles(frameData, *inputs.rotateGizmo);
}
if (inputs.scaleGizmo != nullptr) {
Detail::AppendScaleGizmoScreenTriangles(frameData, *inputs.scaleGizmo);
Internal::AppendScaleGizmoScreenTriangles(frameData, *inputs.scaleGizmo);
}
}

View File

@@ -9,7 +9,7 @@
namespace XCEngine {
namespace Editor {
namespace Detail {
namespace Internal {
inline bool IsPointInsideSceneViewportScreenRect(
const Math::Vector2& point,
@@ -144,7 +144,7 @@ inline bool TryBuildSceneViewportOverlayHandleHitMetrics(
}
}
} // namespace Detail
} // namespace Internal
inline SceneViewportOverlayHandleHitResult HitTestSceneViewportOverlayHandles(
const SceneViewportOverlayFrameData& frameData,
@@ -156,7 +156,7 @@ inline SceneViewportOverlayHandleHitResult HitTestSceneViewportOverlayHandles(
for (const SceneViewportOverlayHandleRecord& handleRecord : frameData.handleRecords) {
float distanceSq = 0.0f;
float depth = 0.0f;
if (!Detail::TryBuildSceneViewportOverlayHandleHitMetrics(
if (!Internal::TryBuildSceneViewportOverlayHandleHitMetrics(
frameData,
viewportSize,
handleRecord,

View File

@@ -153,7 +153,7 @@ void AppendHandleRecord(
handleRecord.handleId = handleId;
handleRecord.entityId = entityId;
handleRecord.shape = SceneViewportOverlayHandleShape::WorldRect;
handleRecord.priority = Detail::kSceneViewportHandlePrioritySceneIcon;
handleRecord.priority = Internal::kSceneViewportHandlePrioritySceneIcon;
handleRecord.worldPosition = worldPosition;
handleRecord.sizePixels = sizePixels;
handleRecord.sortDepth = sortDepth;

View File

@@ -7,7 +7,7 @@
namespace XCEngine {
namespace Editor {
namespace Detail {
namespace Internal {
inline Containers::String NormalizeSceneViewportResourcePath(const std::filesystem::path& path) {
return Containers::String(path.lexically_normal().generic_string().c_str());
@@ -29,10 +29,10 @@ inline Containers::String BuildSceneViewportEditorResourcePath(const std::filesy
relativePath);
}
} // namespace Detail
} // namespace Internal
inline Containers::String GetSceneViewportInfiniteGridShaderPath() {
return Detail::BuildSceneViewportEditorResourcePath(
return Internal::BuildSceneViewportEditorResourcePath(
std::filesystem::path("shaders") /
"scene-viewport" /
"infinite-grid" /
@@ -40,22 +40,22 @@ inline Containers::String GetSceneViewportInfiniteGridShaderPath() {
}
inline Containers::String GetSceneViewportCameraGizmoIconPath() {
return Detail::BuildSceneViewportEditorResourcePath(
return Internal::BuildSceneViewportEditorResourcePath(
std::filesystem::path("Icons") / "camera_gizmo.png");
}
inline Containers::String GetSceneViewportDirectionalLightGizmoIconPath() {
return Detail::BuildSceneViewportEditorResourcePath(
return Internal::BuildSceneViewportEditorResourcePath(
std::filesystem::path("Icons") / "directional_light_gizmo.png");
}
inline Containers::String GetSceneViewportPointLightGizmoIconPath() {
return Detail::BuildSceneViewportEditorResourcePath(
return Internal::BuildSceneViewportEditorResourcePath(
std::filesystem::path("Icons") / "point_light_gizmo.png");
}
inline Containers::String GetSceneViewportSpotLightGizmoIconPath() {
return Detail::BuildSceneViewportEditorResourcePath(
return Internal::BuildSceneViewportEditorResourcePath(
std::filesystem::path("Icons") / "spot_light_gizmo.png");
}

View File

@@ -91,7 +91,7 @@ inline Rendering::RenderSurface BuildViewportSelectionMaskSurface(const Viewport
targets.selectionMaskState);
}
namespace Detail {
namespace Internal {
template <typename ResourceType>
inline void ShutdownAndDelete(ResourceType*& resource) {
@@ -232,7 +232,7 @@ inline bool CreateViewportTextureDescriptor(
return true;
}
} // namespace Detail
} // namespace Internal
inline void DestroyViewportRenderTargets(
UI::ImGuiBackendBridge* backend,
@@ -241,19 +241,19 @@ inline void DestroyViewportRenderTargets(
backend->FreeTextureDescriptor(targets.imguiCpuHandle, targets.imguiGpuHandle);
}
Detail::ShutdownAndDelete(targets.objectIdView);
Detail::ShutdownAndDelete(targets.objectIdShaderView);
Detail::ShutdownAndDelete(targets.objectIdDepthView);
Detail::ShutdownAndDelete(targets.objectIdDepthTexture);
Detail::ShutdownAndDelete(targets.objectIdTexture);
Detail::ShutdownAndDelete(targets.selectionMaskView);
Detail::ShutdownAndDelete(targets.selectionMaskShaderView);
Detail::ShutdownAndDelete(targets.selectionMaskTexture);
Detail::ShutdownAndDelete(targets.depthShaderView);
Detail::ShutdownAndDelete(targets.depthView);
Detail::ShutdownAndDelete(targets.depthTexture);
Detail::ShutdownAndDelete(targets.colorView);
Detail::ShutdownAndDelete(targets.colorTexture);
Internal::ShutdownAndDelete(targets.objectIdView);
Internal::ShutdownAndDelete(targets.objectIdShaderView);
Internal::ShutdownAndDelete(targets.objectIdDepthView);
Internal::ShutdownAndDelete(targets.objectIdDepthTexture);
Internal::ShutdownAndDelete(targets.objectIdTexture);
Internal::ShutdownAndDelete(targets.selectionMaskView);
Internal::ShutdownAndDelete(targets.selectionMaskShaderView);
Internal::ShutdownAndDelete(targets.selectionMaskTexture);
Internal::ShutdownAndDelete(targets.depthShaderView);
Internal::ShutdownAndDelete(targets.depthView);
Internal::ShutdownAndDelete(targets.depthTexture);
Internal::ShutdownAndDelete(targets.colorView);
Internal::ShutdownAndDelete(targets.colorTexture);
targets.width = 0;
targets.height = 0;
@@ -281,12 +281,12 @@ inline bool CreateViewportRenderTargets(
targets.width = width;
targets.height = height;
if (!Detail::CreateViewportColorResources(device, targets) ||
!Detail::CreateViewportDepthResources(device, targets) ||
if (!Internal::CreateViewportColorResources(device, targets) ||
!Internal::CreateViewportDepthResources(device, targets) ||
(ViewportRequiresObjectIdResources(kind) &&
(!Detail::CreateViewportObjectIdResources(device, targets) ||
!Detail::CreateViewportSelectionMaskResources(device, targets))) ||
!Detail::CreateViewportTextureDescriptor(backend, device, targets)) {
(!Internal::CreateViewportObjectIdResources(device, targets) ||
!Internal::CreateViewportSelectionMaskResources(device, targets))) ||
!Internal::CreateViewportTextureDescriptor(backend, device, targets)) {
DestroyViewportRenderTargets(backend, targets);
return false;
}

View File

@@ -479,6 +479,10 @@ void ProjectPanel::DrawProjectContextMenu(IProjectManager& manager, const Contex
const bool canCreate = !target.createFolderPath.empty();
const bool canShowInExplorer = !target.subjectPath.empty();
const bool canOpen = target.item != nullptr && Commands::CanOpenAsset(target.item);
const bool canInstantiateModel =
target.item != nullptr &&
m_context != nullptr &&
Commands::CanInstantiateModelAsset(*m_context, target.item);
const bool canDelete = target.item != nullptr;
const bool canRename = target.item != nullptr;
const std::string copyPath = BuildProjectRelativeAssetPath(
@@ -518,6 +522,12 @@ void ProjectPanel::DrawProjectContextMenu(IProjectManager& manager, const Contex
});
});
Actions::DrawMenuAction(Actions::MakeAction("Instantiate In Scene", nullptr, false, canInstantiateModel), [&]() {
QueueDeferredAction(m_deferredContextAction, [this, target]() {
Commands::InstantiateModelAsset(*m_context, target.item);
});
});
Actions::DrawMenuAction(Actions::MakeOpenAssetAction(canOpen), [&]() {
QueueDeferredAction(m_deferredContextAction, [this, target]() {
Actions::OpenProjectAsset(*m_context, target.item);

View File

@@ -378,6 +378,7 @@ add_library(XCEngine STATIC
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Resources/Material/Material.h
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Resources/Material/MaterialLoader.h
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Resources/Shader/Shader.h
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Resources/Shader/ShaderCompilationCache.h
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Resources/Shader/ShaderLoader.h
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Resources/Volume/VolumeField.h
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Resources/Volume/VolumeFieldLoader.h
@@ -405,6 +406,7 @@ add_library(XCEngine STATIC
${CMAKE_CURRENT_SOURCE_DIR}/src/Resources/Material/Material.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/Resources/Material/MaterialLoader.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/Resources/Shader/Shader.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/Resources/Shader/ShaderCompilationCache.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/Resources/Shader/ShaderLoader.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/Resources/Shader/ShaderIR.h
${CMAKE_CURRENT_SOURCE_DIR}/src/Resources/Shader/Internal/ShaderFileUtils.h
@@ -702,7 +704,8 @@ if(MSVC)
$<$<CONFIG:Debug,RelWithDebInfo>:/Z7>)
target_link_libraries(XCEngine PUBLIC
delayimp
d3dcompiler)
d3dcompiler
bcrypt)
target_link_options(XCEngine INTERFACE "/DELAYLOAD:assimp-vc143-mt.dll")
set_target_properties(XCEngine PROPERTIES
MSVC_DEBUG_INFORMATION_FORMAT "$<$<CONFIG:Debug,RelWithDebInfo>:Embedded>"

View File

@@ -78,7 +78,7 @@ Shader "Builtin Forward Lit"
return output;
}
float ComputeShadowAttenuation(float3 positionWS)
float ComputeShadowAttenuation(float3 positionWS, float3 normalWS, float3 lightDirectionWS)
{
#ifndef XC_MAIN_LIGHT_SHADOWS
return 1.0f;
@@ -87,7 +87,10 @@ Shader "Builtin Forward Lit"
return 1.0f;
}
const float4 shadowClip = mul(gWorldToShadowMatrix, float4(positionWS, 1.0f));
const float nDotL = saturate(dot(normalize(normalWS), normalize(lightDirectionWS)));
const float normalBiasWorld = gShadowOptions.y * gShadowOptions.z * (1.0f - nDotL);
const float3 shadowPositionWS = positionWS + normalize(normalWS) * normalBiasWorld;
const float4 shadowClip = mul(gWorldToShadowMatrix, float4(shadowPositionWS, 1.0f));
if (shadowClip.w <= 0.0f) {
return 1.0f;
}
@@ -116,9 +119,28 @@ Shader "Builtin Forward Lit"
const float receiverDepth = shadowNdc.z - gShadowBiasAndTexelSize.x;
#endif
const float shadowDepth = ShadowMapTexture.Sample(ShadowMapSampler, shadowUv).r;
const float2 shadowTexelSize = gShadowBiasAndTexelSize.yz;
float visibility = 0.0f;
[unroll]
for (int offsetY = -1; offsetY <= 1; ++offsetY) {
[unroll]
for (int offsetX = -1; offsetX <= 1; ++offsetX) {
const float2 sampleUv =
shadowUv + float2((float)offsetX, (float)offsetY) * shadowTexelSize;
if (sampleUv.x < 0.0f || sampleUv.x > 1.0f ||
sampleUv.y < 0.0f || sampleUv.y > 1.0f) {
visibility += 1.0f;
continue;
}
const float shadowDepth = ShadowMapTexture.Sample(ShadowMapSampler, sampleUv).r;
visibility += receiverDepth <= shadowDepth ? 1.0f : 0.0f;
}
}
visibility *= (1.0f / 9.0f);
const float shadowStrength = saturate(gShadowBiasAndTexelSize.w);
return receiverDepth <= shadowDepth ? 1.0f : (1.0f - shadowStrength);
return lerp(1.0f - shadowStrength, 1.0f, visibility);
#endif
}
@@ -207,7 +229,7 @@ Shader "Builtin Forward Lit"
const float3 directionToLightWS = normalize(gMainLightDirectionAndIntensity.xyz);
const float diffuse = saturate(dot(normalWS, directionToLightWS));
const float shadowAttenuation =
diffuse > 0.0f ? ComputeShadowAttenuation(input.positionWS) : 1.0f;
diffuse > 0.0f ? ComputeShadowAttenuation(input.positionWS, normalWS, directionToLightWS) : 1.0f;
lighting +=
gMainLightColorAndFlags.rgb *
(diffuse * gMainLightDirectionAndIntensity.w * shadowAttenuation);

View File

@@ -62,6 +62,10 @@ Shader "Builtin Shadow Caster"
{
Name "ShadowCaster"
Tags { "LightMode" = "ShadowCaster" }
Cull Back
ZWrite On
ZTest LEqual
Offset 1.0, 2
HLSLPROGRAM
#pragma target 4.5
#pragma vertex MainVS

View File

@@ -12,6 +12,12 @@
namespace XCEngine {
namespace Resources {
enum class ArtifactStorageKind : Core::uint8 {
Unknown = 0,
LegacyDirectory = 1,
SingleFileContainer = 2
};
class Mesh;
class Material;
@@ -49,8 +55,10 @@ public:
Containers::String importerName;
Core::uint32 importerVersion = 0;
ResourceType resourceType = ResourceType::Unknown;
ArtifactStorageKind storageKind = ArtifactStorageKind::Unknown;
Containers::String artifactDirectory;
Containers::String mainArtifactPath;
Containers::String mainEntryName;
Containers::String sourceHash;
Containers::String metaHash;
Core::uint64 sourceFileSize = 0;
@@ -68,7 +76,10 @@ public:
AssetGUID assetGuid;
ResourceType resourceType = ResourceType::Unknown;
Containers::String artifactMainPath;
Containers::String artifactMainEntryPath;
Containers::String artifactDirectory;
ArtifactStorageKind artifactStorageKind = ArtifactStorageKind::Unknown;
Containers::String mainEntryName;
LocalID mainLocalID = kMainAssetLocalID;
};
@@ -100,7 +111,7 @@ public:
const Containers::String& GetLastErrorMessage() const { return m_lastErrorMessage; }
private:
static constexpr Core::uint32 kCurrentImporterVersion = 7;
static constexpr Core::uint32 kBaseImporterVersion = 7;
void EnsureProjectLayout();
void LoadSourceAssetDB();
@@ -126,6 +137,7 @@ private:
static Containers::String NormalizePathString(const Containers::String& path);
static Containers::String MakeKey(const Containers::String& path);
static Containers::String GetImporterNameForPath(const Containers::String& relativePath, bool isFolder);
static Core::uint32 GetCurrentImporterVersion(const Containers::String& importerName);
static ResourceType GetPrimaryResourceTypeForImporter(const Containers::String& importerName);
bool ShouldReimport(const SourceAssetRecord& sourceRecord,
@@ -154,6 +166,8 @@ private:
const SourceAssetRecord& sourceRecord,
const std::vector<ArtifactDependencyRecord>& dependencies = {}) const;
Containers::String BuildArtifactDirectory(const Containers::String& artifactKey) const;
Containers::String BuildArtifactFilePath(const Containers::String& artifactKey,
const char* extension) const;
static Containers::String ReadWholeFileText(const std::filesystem::path& path);
static Containers::String ComputeFileHash(const std::filesystem::path& path);
static Core::uint64 GetFileSizeValue(const std::filesystem::path& path);

View File

@@ -47,7 +47,11 @@ public:
AssetGUID assetGuid;
ResourceType resourceType = ResourceType::Unknown;
Containers::String runtimeLoadPath;
Containers::String artifactMainPath;
Containers::String artifactMainEntryPath;
Containers::String artifactDirectory;
ArtifactStorageKind artifactStorageKind = ArtifactStorageKind::Unknown;
Containers::String mainEntryName;
LocalID mainLocalID = kMainAssetLocalID;
};

View File

@@ -20,6 +20,7 @@ public:
virtual void SetTopology(uint32_t topologyType) = 0; // PrimitiveTopologyType
virtual void SetRenderTargetFormats(uint32_t count, const uint32_t* formats, uint32_t depthFormat) = 0;
virtual void SetSampleCount(uint32_t count) = 0;
virtual void SetSampleQuality(uint32_t quality) = 0;
virtual void SetComputeShader(RHIShader* shader) = 0;
// State query

View File

@@ -54,6 +54,7 @@ struct RenderDirectionalShadowData {
bool enabled = false;
Math::Matrix4x4 viewProjection = Math::Matrix4x4::Identity();
Math::Vector4 shadowParams = Math::Vector4::Zero();
Math::Vector4 shadowOptions = Math::Vector4::Zero();
RHI::RHIResourceView* shadowMap = nullptr;
bool IsValid() const {

View File

@@ -29,6 +29,11 @@ struct BuiltinSkyboxMaterialData {
BuiltinSkyboxTextureMode textureMode = BuiltinSkyboxTextureMode::None;
};
struct BuiltinDepthStyleMaterialConstants {
Math::Vector4 baseColorFactor = Math::Vector4::One();
Math::Vector4 alphaCutoffParams = Math::Vector4(0.5f, 0.0f, 0.0f, 0.0f);
};
struct MaterialConstantLayoutView {
const Resources::MaterialConstantFieldDesc* fields = nullptr;
size_t count = 0;
@@ -277,6 +282,40 @@ inline MaterialConstantPayloadView ResolveSchemaMaterialConstantPayload(const Re
return { constantBufferData.Data(), constantBufferData.Size(), layoutView };
}
inline BuiltinDepthStyleMaterialConstants BuildBuiltinDepthStyleMaterialConstants(
const Resources::Material* material) {
BuiltinDepthStyleMaterialConstants constants = {};
constants.baseColorFactor = ResolveBuiltinBaseColorFactor(material);
constants.alphaCutoffParams = Math::Vector4(ResolveBuiltinAlphaCutoff(material), 0.0f, 0.0f, 0.0f);
return constants;
}
inline MaterialConstantPayloadView ResolveBuiltinDepthStyleMaterialConstantPayload(
const Resources::Material* material,
BuiltinDepthStyleMaterialConstants& outConstants,
Resources::MaterialConstantFieldDesc (&outLayout)[2]) {
outConstants = BuildBuiltinDepthStyleMaterialConstants(material);
outLayout[0].name = "gBaseColorFactor";
outLayout[0].type = Resources::MaterialPropertyType::Float4;
outLayout[0].offset = 0u;
outLayout[0].size = static_cast<Core::uint32>(sizeof(Math::Vector4));
outLayout[0].alignedSize = static_cast<Core::uint32>(sizeof(Math::Vector4));
outLayout[1].name = "gAlphaCutoffParams";
outLayout[1].type = Resources::MaterialPropertyType::Float4;
outLayout[1].offset = static_cast<Core::uint32>(sizeof(Math::Vector4));
outLayout[1].size = static_cast<Core::uint32>(sizeof(Math::Vector4));
outLayout[1].alignedSize = static_cast<Core::uint32>(sizeof(Math::Vector4));
MaterialConstantLayoutView layoutView = {};
layoutView.fields = outLayout;
layoutView.count = 2u;
layoutView.size = sizeof(BuiltinDepthStyleMaterialConstants);
return { &outConstants, sizeof(BuiltinDepthStyleMaterialConstants), layoutView };
}
inline bool TryResolveMaterialBufferResourceView(
const Resources::Material* material,
const BuiltinPassResourceBindingDesc& binding,

View File

@@ -43,8 +43,8 @@ public:
const Containers::String& GetShaderPath() const;
private:
bool EnsureInitialized(const RenderContext& renderContext, RHI::Format renderTargetFormat);
bool CreateResources(const RenderContext& renderContext, RHI::Format renderTargetFormat);
bool EnsureInitialized(const RenderContext& renderContext, const RenderSurface& surface);
bool CreateResources(const RenderContext& renderContext, const RenderSurface& surface);
void DestroyResources();
void DestroyOwnedDescriptorSet(OwnedDescriptorSet& descriptorSet);
@@ -53,6 +53,8 @@ private:
RHI::RHIDevice* m_device = nullptr;
RHI::RHIType m_backendType = RHI::RHIType::D3D12;
RHI::Format m_renderTargetFormat = RHI::Format::Unknown;
uint32_t m_renderTargetSampleCount = 1u;
uint32_t m_renderTargetSampleQuality = 0u;
Resources::ResourceHandle<Resources::Shader> m_shader;
RHI::RHISampler* m_sampler = nullptr;
RHI::RHIPipelineLayout* m_pipelineLayout = nullptr;

View File

@@ -133,6 +133,8 @@ private:
uint32_t renderTargetCount = 0;
uint32_t renderTargetFormat = 0;
uint32_t depthStencilFormat = 0;
uint32_t sampleCount = 1;
uint32_t sampleQuality = 0;
bool operator==(const PipelineStateKey& other) const {
return renderState == other.renderState &&
@@ -141,7 +143,9 @@ private:
keywordSignature == other.keywordSignature &&
renderTargetCount == other.renderTargetCount &&
renderTargetFormat == other.renderTargetFormat &&
depthStencilFormat == other.depthStencilFormat;
depthStencilFormat == other.depthStencilFormat &&
sampleCount == other.sampleCount &&
sampleQuality == other.sampleQuality;
}
};
@@ -154,6 +158,8 @@ private:
hash ^= std::hash<uint32_t>{}(key.renderTargetCount) + 0x9e3779b9u + (hash << 6) + (hash >> 2);
hash ^= std::hash<uint32_t>{}(key.renderTargetFormat) + 0x9e3779b9u + (hash << 6) + (hash >> 2);
hash ^= std::hash<uint32_t>{}(key.depthStencilFormat) + 0x9e3779b9u + (hash << 6) + (hash >> 2);
hash ^= std::hash<uint32_t>{}(key.sampleCount) + 0x9e3779b9u + (hash << 6) + (hash >> 2);
hash ^= std::hash<uint32_t>{}(key.sampleQuality) + 0x9e3779b9u + (hash << 6) + (hash >> 2);
return hash;
}
};

View File

@@ -43,8 +43,8 @@ public:
const Containers::String& GetShaderPath() const;
private:
bool EnsureInitialized(const RenderContext& renderContext, RHI::Format renderTargetFormat);
bool CreateResources(const RenderContext& renderContext, RHI::Format renderTargetFormat);
bool EnsureInitialized(const RenderContext& renderContext, const RenderSurface& surface);
bool CreateResources(const RenderContext& renderContext, const RenderSurface& surface);
void DestroyResources();
void DestroyOwnedDescriptorSet(OwnedDescriptorSet& descriptorSet);
@@ -53,6 +53,8 @@ private:
RHI::RHIDevice* m_device = nullptr;
RHI::RHIType m_backendType = RHI::RHIType::D3D12;
RHI::Format m_renderTargetFormat = RHI::Format::Unknown;
uint32_t m_renderTargetSampleCount = 1u;
uint32_t m_renderTargetSampleQuality = 0u;
Resources::ResourceHandle<Resources::Shader> m_shader;
RHI::RHISampler* m_sampler = nullptr;
RHI::RHIPipelineLayout* m_pipelineLayout = nullptr;

View File

@@ -55,8 +55,8 @@ public:
const InfiniteGridPassData& data);
private:
bool EnsureInitialized(const RenderContext& renderContext);
bool CreateResources(const RenderContext& renderContext);
bool EnsureInitialized(const RenderContext& renderContext, const RenderSurface& surface);
bool CreateResources(const RenderContext& renderContext, const RenderSurface& surface);
void DestroyResources();
RHI::RHIDevice* m_device = nullptr;
@@ -67,6 +67,9 @@ private:
RHI::RHIDescriptorSet* m_constantSet = nullptr;
Containers::String m_shaderPath;
Resources::ResourceHandle<Resources::Shader> m_builtinInfiniteGridShader;
RHI::Format m_renderTargetFormat = RHI::Format::Unknown;
RHI::Format m_depthStencilFormat = RHI::Format::Unknown;
uint32_t m_renderTargetSampleCount = 1u;
};
} // namespace Passes

View File

@@ -31,6 +31,11 @@ struct ObjectIdOutlineStyle {
bool debugSelectionMask = false;
};
struct ObjectIdOutlinePassInputs {
RHI::RHIResourceView* objectIdTextureView = nullptr;
RHI::ResourceStates objectIdTextureState = RHI::ResourceStates::PixelShaderResource;
};
class BuiltinObjectIdOutlinePass {
public:
explicit BuiltinObjectIdOutlinePass(Containers::String shaderPath = Containers::String());
@@ -50,7 +55,7 @@ public:
bool Render(
const RenderContext& renderContext,
const RenderSurface& surface,
RHI::RHIResourceView* objectIdTextureView,
const ObjectIdOutlinePassInputs& inputs,
const std::vector<uint64_t>& selectedObjectIds,
const ObjectIdOutlineStyle& style = {});
@@ -62,8 +67,8 @@ private:
std::array<Math::Vector4, kMaxSelectedObjectCount> selectedObjectColors = {};
};
bool EnsureInitialized(const RenderContext& renderContext);
bool CreateResources(const RenderContext& renderContext);
bool EnsureInitialized(const RenderContext& renderContext, const RenderSurface& surface);
bool CreateResources(const RenderContext& renderContext, const RenderSurface& surface);
void DestroyResources();
bool HasCreatedResources() const;
void ResetState();
@@ -78,6 +83,8 @@ private:
RHI::RHIDescriptorSet* m_textureSet = nullptr;
Containers::String m_shaderPath;
std::optional<Resources::ResourceHandle<Resources::Shader>> m_builtinObjectIdOutlineShader;
RHI::Format m_renderTargetFormat = RHI::Format::Unknown;
uint32_t m_renderTargetSampleCount = 1u;
};
} // namespace Passes

View File

@@ -27,6 +27,13 @@ struct SelectionOutlineStyle {
bool debugSelectionMask = false;
};
struct SelectionOutlinePassInputs {
RHI::RHIResourceView* selectionMaskTextureView = nullptr;
RHI::ResourceStates selectionMaskState = RHI::ResourceStates::PixelShaderResource;
RHI::RHIResourceView* depthTextureView = nullptr;
RHI::ResourceStates depthTextureState = RHI::ResourceStates::DepthWrite;
};
class BuiltinSelectionOutlinePass {
public:
explicit BuiltinSelectionOutlinePass(Containers::String shaderPath = Containers::String());
@@ -44,8 +51,7 @@ public:
bool Render(
const RenderContext& renderContext,
const RenderSurface& surface,
RHI::RHIResourceView* selectionMaskTextureView,
RHI::RHIResourceView* depthTextureView,
const SelectionOutlinePassInputs& inputs,
const SelectionOutlineStyle& style = {});
private:
@@ -56,8 +62,8 @@ private:
Math::Vector4 depthParams = Math::Vector4::Zero();
};
bool EnsureInitialized(const RenderContext& renderContext);
bool CreateResources(const RenderContext& renderContext);
bool EnsureInitialized(const RenderContext& renderContext, const RenderSurface& surface);
bool CreateResources(const RenderContext& renderContext, const RenderSurface& surface);
void DestroyResources();
bool HasCreatedResources() const;
void ResetState();
@@ -72,6 +78,8 @@ private:
RHI::RHIDescriptorSet* m_textureSet = nullptr;
Containers::String m_shaderPath;
std::optional<Resources::ResourceHandle<Resources::Shader>> m_builtinSelectionOutlineShader;
RHI::Format m_renderTargetFormat = RHI::Format::Unknown;
uint32_t m_renderTargetSampleCount = 1u;
};
} // namespace Passes

View File

@@ -41,6 +41,9 @@ public:
const char* GetName() const override;
bool Initialize(const RenderContext& context) override;
bool PrepareVolumeResources(
const RenderContext& context,
const RenderSceneData& sceneData);
bool Execute(const RenderPassContext& context) override;
void Shutdown() override;
@@ -140,6 +143,8 @@ private:
uint32_t renderTargetCount = 0;
uint32_t renderTargetFormat = 0;
uint32_t depthStencilFormat = 0;
uint32_t sampleCount = 1;
uint32_t sampleQuality = 0;
bool operator==(const PipelineStateKey& other) const {
return renderState == other.renderState &&
@@ -148,7 +153,9 @@ private:
keywordSignature == other.keywordSignature &&
renderTargetCount == other.renderTargetCount &&
renderTargetFormat == other.renderTargetFormat &&
depthStencilFormat == other.depthStencilFormat;
depthStencilFormat == other.depthStencilFormat &&
sampleCount == other.sampleCount &&
sampleQuality == other.sampleQuality;
}
};
@@ -161,6 +168,8 @@ private:
hash ^= std::hash<uint32_t>{}(key.renderTargetCount) + 0x9e3779b9u + (hash << 6) + (hash >> 2);
hash ^= std::hash<uint32_t>{}(key.renderTargetFormat) + 0x9e3779b9u + (hash << 6) + (hash >> 2);
hash ^= std::hash<uint32_t>{}(key.depthStencilFormat) + 0x9e3779b9u + (hash << 6) + (hash >> 2);
hash ^= std::hash<uint32_t>{}(key.sampleCount) + 0x9e3779b9u + (hash << 6) + (hash >> 2);
hash ^= std::hash<uint32_t>{}(key.sampleQuality) + 0x9e3779b9u + (hash << 6) + (hash >> 2);
return hash;
}
};

View File

@@ -128,6 +128,7 @@ struct DirectionalShadowRenderPlan {
Math::Vector3 lightDirection = Math::Vector3::Back();
Math::Vector3 focusPoint = Math::Vector3::Zero();
float orthographicHalfExtent = 0.0f;
float texelWorldSize = 0.0f;
float nearClipPlane = 0.1f;
float farClipPlane = 0.0f;
uint32_t mapWidth = 0;

View File

@@ -22,6 +22,7 @@ struct RenderPassContext {
const RenderSceneData& sceneData;
const RenderSurface* sourceSurface = nullptr;
RHI::RHIResourceView* sourceColorView = nullptr;
RHI::ResourceStates sourceColorState = RHI::ResourceStates::Common;
};
class RenderPass {

View File

@@ -51,6 +51,16 @@ public:
void SetColorStateAfter(RHI::ResourceStates state) { m_colorStateAfter = state; }
RHI::ResourceStates GetColorStateAfter() const { return m_colorStateAfter; }
void SetDepthStateBefore(RHI::ResourceStates state) { m_depthStateBefore = state; }
RHI::ResourceStates GetDepthStateBefore() const { return m_depthStateBefore; }
void SetDepthStateAfter(RHI::ResourceStates state) { m_depthStateAfter = state; }
RHI::ResourceStates GetDepthStateAfter() const { return m_depthStateAfter; }
void SetSampleDesc(uint32_t sampleCount, uint32_t sampleQuality = 0);
uint32_t GetSampleCount() const { return m_sampleCount; }
uint32_t GetSampleQuality() const { return m_sampleQuality; }
private:
uint32_t m_width = 0;
uint32_t m_height = 0;
@@ -63,6 +73,10 @@ private:
bool m_autoTransition = true;
RHI::ResourceStates m_colorStateBefore = RHI::ResourceStates::Present;
RHI::ResourceStates m_colorStateAfter = RHI::ResourceStates::Present;
RHI::ResourceStates m_depthStateBefore = RHI::ResourceStates::DepthWrite;
RHI::ResourceStates m_depthStateAfter = RHI::ResourceStates::DepthWrite;
uint32_t m_sampleCount = 1;
uint32_t m_sampleQuality = 0;
};
} // namespace Rendering

View File

@@ -1,13 +1,19 @@
#pragma once
#include <XCEngine/Core/Containers/Array.h>
#include <XCEngine/Core/Containers/String.h>
#include <XCEngine/Core/IO/IResourceLoader.h>
#include <XCEngine/Core/Types.h>
namespace XCEngine {
namespace Resources {
class GaussianSplat;
bool SerializeGaussianSplatArtifactPayload(const GaussianSplat& gaussianSplat,
Containers::Array<Core::uint8>& outPayload,
Containers::String* outErrorMessage = nullptr);
bool WriteGaussianSplatArtifactFile(const Containers::String& artifactPath,
const GaussianSplat& gaussianSplat,
Containers::String* outErrorMessage = nullptr);

View File

@@ -1,13 +1,19 @@
#pragma once
#include <XCEngine/Core/Containers/Array.h>
#include <XCEngine/Core/Containers/String.h>
#include <XCEngine/Core/IO/IResourceLoader.h>
#include <XCEngine/Core/Types.h>
namespace XCEngine {
namespace Resources {
class Model;
bool SerializeModelArtifactPayload(const Model& model,
Containers::Array<Core::uint8>& outPayload,
Containers::String* outErrorMessage = nullptr);
bool WriteModelArtifactFile(const Containers::String& artifactPath,
const Model& model,
Containers::String* outErrorMessage = nullptr);

View File

@@ -0,0 +1,91 @@
#pragma once
#include <XCEngine/Core/Asset/AssetGUID.h>
#include <XCEngine/Core/Containers/Array.h>
#include <XCEngine/Core/Containers/String.h>
#include <XCEngine/Core/Types.h>
#include <XCEngine/Resources/Shader/Shader.h>
#include <unordered_map>
namespace XCEngine {
namespace Resources {
constexpr Core::uint32 kShaderCompilationCacheSchemaVersion = 1;
enum class ShaderBytecodeFormat : Core::uint32 {
Unknown = 0,
DXIL,
DXBC,
SPIRV,
GLSLSource,
OpenGLProgramBinary
};
struct ShaderCompileKey {
Containers::String shaderPath;
Containers::String sourceHash;
Containers::String dependencyHash;
Containers::String passName;
Containers::String entryPoint;
Containers::String profile;
Containers::String compilerName;
Containers::String compilerVersion;
Containers::String optionsSignature;
ShaderType stage = ShaderType::Fragment;
ShaderLanguage sourceLanguage = ShaderLanguage::HLSL;
ShaderBackend backend = ShaderBackend::Generic;
Containers::Array<Containers::String> keywords;
void Normalize();
Containers::String BuildSignature() const;
Containers::String BuildCacheKey() const;
};
struct ShaderCacheEntry {
ShaderCompileKey key;
ShaderBytecodeFormat format = ShaderBytecodeFormat::Unknown;
Containers::Array<Core::uint8> payload;
};
class ShaderCompilationCache {
public:
void Initialize(const Containers::String& libraryRoot);
void Shutdown();
bool IsInitialized() const { return !m_libraryRoot.Empty(); }
const Containers::String& GetLibraryRoot() const { return m_libraryRoot; }
const Containers::String& GetDatabasePath() const { return m_databasePath; }
Core::uint32 GetRecordCount() const { return static_cast<Core::uint32>(m_records.size()); }
Containers::String BuildCacheKey(const ShaderCompileKey& key) const;
Containers::String BuildCacheRelativePath(const ShaderCompileKey& key) const;
Containers::String BuildCacheAbsolutePath(const ShaderCompileKey& key) const;
bool Store(const ShaderCacheEntry& entry,
Containers::String* outErrorMessage = nullptr);
bool TryLoad(const ShaderCompileKey& key,
ShaderCacheEntry& outEntry,
Containers::String* outErrorMessage = nullptr) const;
private:
struct ShaderCacheRecord {
ShaderBackend backend = ShaderBackend::Generic;
ShaderBytecodeFormat format = ShaderBytecodeFormat::Unknown;
Containers::String relativePath;
Core::uint64 payloadSize = 0;
};
void LoadDatabase();
void SaveDatabase() const;
static Containers::String BuildBackendDirectoryName(ShaderBackend backend);
static AssetGUID ComputeKeyGuid(const Containers::String& cacheKey);
Containers::String m_libraryRoot;
Containers::String m_databasePath;
std::unordered_map<std::string, ShaderCacheRecord> m_records;
};
} // namespace Resources
} // namespace XCEngine

View File

@@ -0,0 +1,144 @@
#pragma once
#include <XCEngine/Core/Containers/String.h>
#include <XCEngine/Resources/Texture/TextureImportSettings.h>
#include <algorithm>
#include <cctype>
#include <initializer_list>
#include <string>
namespace XCEngine {
namespace Resources {
namespace Detail {
enum class TextureSemanticUsage {
Unknown,
Color,
Data
};
inline std::string ToLowerCopy(const Containers::String& value) {
std::string lower = value.CStr() != nullptr ? std::string(value.CStr()) : std::string();
std::transform(lower.begin(), lower.end(), lower.begin(), [](unsigned char character) {
return static_cast<char>(std::tolower(character));
});
return lower;
}
inline bool ContainsAnyToken(const std::string& value, std::initializer_list<const char*> tokens) {
for (const char* token : tokens) {
if (token != nullptr && value.find(token) != std::string::npos) {
return true;
}
}
return false;
}
inline TextureSemanticUsage ClassifyTextureSemanticUsage(const std::string& identifierLower) {
if (identifierLower.empty()) {
return TextureSemanticUsage::Unknown;
}
// Color ramps are authored visually even when they drive toon bands.
if (ContainsAnyToken(identifierLower, { "shadow_ramp", "specular_ramp", "toon_ramp", "ramp" })) {
return TextureSemanticUsage::Color;
}
if (ContainsAnyToken(
identifierLower,
{ "normal",
"nrm",
"lightmap",
"light_map",
"metal",
"metallic",
"rough",
"roughness",
"smoothness",
"gloss",
"mask",
"occlusion",
"ao",
"orm",
"height",
"displace",
"face_shadow",
"faceshadow",
"lookup" })) {
return TextureSemanticUsage::Data;
}
if (ContainsAnyToken(
identifierLower,
{ "diffuse",
"albedo",
"basecolor",
"base_color",
"basemap",
"base_map",
"maintex",
"main_tex",
"colormap",
"color_map",
"emissive",
"emission" })) {
return TextureSemanticUsage::Color;
}
return TextureSemanticUsage::Unknown;
}
inline bool ResolveTextureSRGBUsage(const Containers::String& identifier, const char* propertyName) {
const TextureSemanticUsage identifierUsage = ClassifyTextureSemanticUsage(ToLowerCopy(identifier));
if (identifierUsage == TextureSemanticUsage::Color) {
return true;
}
if (identifierUsage == TextureSemanticUsage::Data) {
return false;
}
if (propertyName != nullptr && propertyName[0] != '\0') {
const Containers::String propertyIdentifier(propertyName);
const TextureSemanticUsage propertyUsage =
ClassifyTextureSemanticUsage(ToLowerCopy(propertyIdentifier));
if (propertyUsage == TextureSemanticUsage::Color) {
return true;
}
if (propertyUsage == TextureSemanticUsage::Data) {
return false;
}
}
// Match Unity-style defaults: unknown image assets are assumed to be color textures.
return true;
}
} // namespace Detail
inline TextureImportSettings BuildTextureImportSettingsFromIdentifier(
const Containers::String& identifier,
const char* propertyName = nullptr) {
TextureImportSettings settings;
const bool importAsSRGB = Detail::ResolveTextureSRGBUsage(identifier, propertyName);
settings.SetSRGB(importAsSRGB);
settings.SetTargetFormat(importAsSRGB ? TextureFormat::RGBA8_SRGB : TextureFormat::RGBA8_UNORM);
return settings;
}
inline TextureImportSettings BuildTextureImportSettingsForMaterialProperty(
const char* propertyName,
const Containers::String& texturePath = Containers::String()) {
if (!texturePath.Empty()) {
return BuildTextureImportSettingsFromIdentifier(texturePath, propertyName);
}
return BuildTextureImportSettingsFromIdentifier(
propertyName != nullptr ? Containers::String(propertyName) : Containers::String(),
propertyName);
}
} // namespace Resources
} // namespace XCEngine

View File

@@ -0,0 +1,341 @@
#pragma once
#include <XCEngine/UI/Core/UIInvalidation.h>
#include <XCEngine/UI/Types.h>
#include <algorithm>
#include <cmath>
#include <cstdint>
#include <span>
#include <string>
#include <string_view>
namespace XCEngine::UI::Widgets {
enum class UIDragDropOperation : std::uint8_t {
None = 0,
Copy = 1 << 0,
Move = 1 << 1,
Link = 1 << 2
};
inline constexpr UIDragDropOperation operator|(
UIDragDropOperation left,
UIDragDropOperation right) {
return static_cast<UIDragDropOperation>(
static_cast<std::uint8_t>(left) |
static_cast<std::uint8_t>(right));
}
inline constexpr UIDragDropOperation operator&(
UIDragDropOperation left,
UIDragDropOperation right) {
return static_cast<UIDragDropOperation>(
static_cast<std::uint8_t>(left) &
static_cast<std::uint8_t>(right));
}
inline constexpr UIDragDropOperation& operator|=(
UIDragDropOperation& left,
UIDragDropOperation right) {
left = left | right;
return left;
}
inline constexpr bool HasAnyUIDragDropOperation(
UIDragDropOperation value,
UIDragDropOperation flags) {
return (value & flags) != UIDragDropOperation::None;
}
struct UIDragDropPayload {
std::string typeId = {};
std::string itemId = {};
std::string label = {};
};
struct UIDragDropSourceDescriptor {
UIElementId ownerId = 0u;
std::string sourceId = {};
UIPoint pointerDownPosition = {};
UIDragDropPayload payload = {};
UIDragDropOperation allowedOperations = UIDragDropOperation::Move;
float activationDistance = 4.0f;
};
struct UIDragDropTargetDescriptor {
UIElementId ownerId = 0u;
std::string targetId = {};
std::span<const std::string_view> acceptedPayloadTypes = {};
UIDragDropOperation acceptedOperations =
UIDragDropOperation::Copy |
UIDragDropOperation::Move |
UIDragDropOperation::Link;
UIDragDropOperation preferredOperation = UIDragDropOperation::Move;
};
struct UIDragDropState {
bool armed = false;
bool active = false;
UIElementId sourceOwnerId = 0u;
std::string sourceId = {};
UIPoint pointerDownPosition = {};
UIPoint pointerPosition = {};
UIDragDropPayload payload = {};
UIDragDropOperation allowedOperations = UIDragDropOperation::None;
float activationDistance = 4.0f;
UIElementId targetOwnerId = 0u;
std::string targetId = {};
UIDragDropOperation previewOperation = UIDragDropOperation::None;
};
struct UIDragDropResult {
bool armed = false;
bool activated = false;
bool targetChanged = false;
bool completed = false;
bool cancelled = false;
UIElementId sourceOwnerId = 0u;
UIElementId targetOwnerId = 0u;
std::string sourceId = {};
std::string targetId = {};
std::string payloadTypeId = {};
std::string payloadItemId = {};
UIDragDropOperation operation = UIDragDropOperation::None;
};
inline constexpr bool IsUIDragDropInProgress(const UIDragDropState& state) {
return state.armed || state.active;
}
inline constexpr bool HasResolvedUIDragDropTarget(const UIDragDropState& state) {
return state.targetOwnerId != 0u &&
!state.targetId.empty() &&
state.previewOperation != UIDragDropOperation::None;
}
inline bool DoesUIDragDropPayloadTypeMatch(
std::string_view payloadType,
std::span<const std::string_view> acceptedPayloadTypes) {
if (acceptedPayloadTypes.empty()) {
return true;
}
return std::find(
acceptedPayloadTypes.begin(),
acceptedPayloadTypes.end(),
payloadType) != acceptedPayloadTypes.end();
}
inline constexpr UIDragDropOperation PickUIDragDropOperation(
UIDragDropOperation availableOperations,
UIDragDropOperation preferredOperations = UIDragDropOperation::None) {
const UIDragDropOperation preferredAvailable =
availableOperations & preferredOperations;
if (HasAnyUIDragDropOperation(preferredAvailable, UIDragDropOperation::Move)) {
return UIDragDropOperation::Move;
}
if (HasAnyUIDragDropOperation(preferredAvailable, UIDragDropOperation::Copy)) {
return UIDragDropOperation::Copy;
}
if (HasAnyUIDragDropOperation(preferredAvailable, UIDragDropOperation::Link)) {
return UIDragDropOperation::Link;
}
if (HasAnyUIDragDropOperation(availableOperations, UIDragDropOperation::Move)) {
return UIDragDropOperation::Move;
}
if (HasAnyUIDragDropOperation(availableOperations, UIDragDropOperation::Copy)) {
return UIDragDropOperation::Copy;
}
if (HasAnyUIDragDropOperation(availableOperations, UIDragDropOperation::Link)) {
return UIDragDropOperation::Link;
}
return UIDragDropOperation::None;
}
inline UIDragDropOperation ResolveUIDragDropOperation(
UIDragDropOperation allowedOperations,
const UIDragDropTargetDescriptor& target) {
const UIDragDropOperation supportedOperations =
allowedOperations & target.acceptedOperations;
if (supportedOperations == UIDragDropOperation::None) {
return UIDragDropOperation::None;
}
return PickUIDragDropOperation(
supportedOperations,
target.preferredOperation);
}
inline UIDragDropOperation ResolveUIDragDropOperation(
const UIDragDropState& state,
const UIDragDropTargetDescriptor& target) {
if (!state.active ||
!DoesUIDragDropPayloadTypeMatch(state.payload.typeId, target.acceptedPayloadTypes)) {
return UIDragDropOperation::None;
}
return ResolveUIDragDropOperation(state.allowedOperations, target);
}
inline void FillUIDragDropResult(
const UIDragDropState& state,
UIDragDropResult& result) {
result.sourceOwnerId = state.sourceOwnerId;
result.targetOwnerId = state.targetOwnerId;
result.sourceId = state.sourceId;
result.targetId = state.targetId;
result.payloadTypeId = state.payload.typeId;
result.payloadItemId = state.payload.itemId;
result.operation = state.previewOperation;
}
inline bool BeginUIDragDrop(
const UIDragDropSourceDescriptor& descriptor,
UIDragDropState& outState,
UIDragDropResult* outResult = nullptr) {
if (descriptor.ownerId == 0u ||
descriptor.allowedOperations == UIDragDropOperation::None) {
return false;
}
outState = {};
outState.armed = true;
outState.sourceOwnerId = descriptor.ownerId;
outState.sourceId = descriptor.sourceId;
outState.pointerDownPosition = descriptor.pointerDownPosition;
outState.pointerPosition = descriptor.pointerDownPosition;
outState.payload = descriptor.payload;
outState.allowedOperations = descriptor.allowedOperations;
outState.activationDistance = (std::max)(descriptor.activationDistance, 0.0f);
if (outResult != nullptr) {
*outResult = {};
outResult->armed = true;
FillUIDragDropResult(outState, *outResult);
}
return true;
}
inline bool UpdateUIDragDropPointer(
UIDragDropState& state,
const UIPoint& pointerPosition,
UIDragDropResult* outResult = nullptr) {
if (!IsUIDragDropInProgress(state)) {
return false;
}
state.pointerPosition = pointerPosition;
UIDragDropResult localResult = {};
FillUIDragDropResult(state, localResult);
if (!state.active) {
const float deltaX = pointerPosition.x - state.pointerDownPosition.x;
const float deltaY = pointerPosition.y - state.pointerDownPosition.y;
const float distance = std::sqrt(deltaX * deltaX + deltaY * deltaY);
if (distance >= state.activationDistance) {
state.active = true;
localResult.activated = true;
}
}
if (outResult != nullptr) {
*outResult = std::move(localResult);
}
return true;
}
inline void ClearUIDragDropTarget(
UIDragDropState& state,
UIDragDropResult* outResult = nullptr) {
const bool changed =
state.targetOwnerId != 0u ||
!state.targetId.empty() ||
state.previewOperation != UIDragDropOperation::None;
state.targetOwnerId = 0u;
state.targetId.clear();
state.previewOperation = UIDragDropOperation::None;
if (outResult != nullptr) {
*outResult = {};
FillUIDragDropResult(state, *outResult);
outResult->targetChanged = changed;
}
}
inline bool UpdateUIDragDropTarget(
UIDragDropState& state,
const UIDragDropTargetDescriptor* target,
UIDragDropResult* outResult = nullptr) {
if (!state.active) {
ClearUIDragDropTarget(state, outResult);
return false;
}
if (target == nullptr) {
ClearUIDragDropTarget(state, outResult);
return false;
}
const UIDragDropOperation resolvedOperation =
ResolveUIDragDropOperation(state, *target);
if (resolvedOperation == UIDragDropOperation::None) {
ClearUIDragDropTarget(state, outResult);
return false;
}
const bool changed =
state.targetOwnerId != target->ownerId ||
state.targetId != target->targetId ||
state.previewOperation != resolvedOperation;
state.targetOwnerId = target->ownerId;
state.targetId = target->targetId;
state.previewOperation = resolvedOperation;
if (outResult != nullptr) {
*outResult = {};
FillUIDragDropResult(state, *outResult);
outResult->targetChanged = changed;
}
return true;
}
inline bool EndUIDragDrop(
UIDragDropState& state,
UIDragDropResult& outResult) {
if (!IsUIDragDropInProgress(state)) {
return false;
}
outResult = {};
FillUIDragDropResult(state, outResult);
if (state.active && HasResolvedUIDragDropTarget(state)) {
outResult.completed = true;
} else {
outResult.cancelled = true;
}
state = {};
return true;
}
inline bool CancelUIDragDrop(
UIDragDropState& state,
UIDragDropResult* outResult = nullptr) {
if (!IsUIDragDropInProgress(state)) {
return false;
}
if (outResult != nullptr) {
*outResult = {};
FillUIDragDropResult(state, *outResult);
outResult->cancelled = true;
}
state = {};
return true;
}
} // namespace XCEngine::UI::Widgets

View File

@@ -27,6 +27,7 @@ public:
bool RemoveSelection(std::string_view selectionId);
bool ClearSelection();
bool ToggleSelection(std::string selectionId);
bool ToggleSelectionMembership(std::string selectionId, bool makePrimary = true);
private:
static void NormalizeSelectionIds(std::vector<std::string>& selectionIds);

View File

@@ -1,6 +1,9 @@
#include "Components/VolumeRendererComponent.h"
#include "Core/Asset/ResourceManager.h"
#include "Debug/Logger.h"
#include <chrono>
namespace XCEngine {
namespace Components {
@@ -47,10 +50,24 @@ std::string MaterialPathFromHandle(const Resources::ResourceHandle<Resources::Ma
return material.Get() != nullptr ? ToStdString(material->GetPath()) : std::string();
}
uint64_t GetVolumeTraceSteadyMs() {
using Clock = std::chrono::steady_clock;
static const Clock::time_point s_start = Clock::now();
return static_cast<uint64_t>(std::chrono::duration_cast<std::chrono::milliseconds>(
Clock::now() - s_start).count());
}
void LogVolumeTraceFileSystem(const std::string& message) {
Containers::String entry("[VolumeTrace] ");
entry += message.c_str();
Debug::Logger::Get().Info(Debug::LogCategory::FileSystem, entry);
}
} // namespace
struct VolumeRendererComponent::PendingVolumeLoadState {
Resources::LoadResult result;
uint64_t requestedAtMs = 0u;
bool completed = false;
};
@@ -311,12 +328,30 @@ void VolumeRendererComponent::BeginAsyncVolumeLoad(const std::string& volumeFiel
m_asyncVolumeLoadRequested = true;
m_volumeField.Reset();
m_pendingVolumeLoad = std::make_shared<PendingVolumeLoadState>();
m_pendingVolumeLoad->requestedAtMs = GetVolumeTraceSteadyMs();
LogVolumeTraceFileSystem(
"BeginAsyncVolumeLoad path=" + volumeFieldPath +
" steady_ms=" + std::to_string(m_pendingVolumeLoad->requestedAtMs));
std::weak_ptr<PendingVolumeLoadState> weakState = m_pendingVolumeLoad;
Resources::ResourceManager::Get().LoadAsync(
volumeFieldPath.c_str(),
Resources::ResourceType::VolumeField,
[weakState](Resources::LoadResult result) {
[weakState, volumeFieldPath](Resources::LoadResult result) {
if (std::shared_ptr<PendingVolumeLoadState> state = weakState.lock()) {
const uint64_t completedAtMs = GetVolumeTraceSteadyMs();
const bool succeeded = static_cast<bool>(result) && result.resource != nullptr;
size_t payloadBytes = 0u;
if (succeeded) {
if (auto* volumeField = static_cast<Resources::VolumeField*>(result.resource)) {
payloadBytes = volumeField->GetPayloadSize();
}
}
LogVolumeTraceFileSystem(
"AsyncVolumeLoadCompleted path=" + volumeFieldPath +
" steady_ms=" + std::to_string(completedAtMs) +
" elapsed_ms=" + std::to_string(completedAtMs - state->requestedAtMs) +
" success=" + std::to_string(succeeded ? 1 : 0) +
" payload_bytes=" + std::to_string(payloadBytes));
state->result = std::move(result);
state->completed = true;
}
@@ -349,6 +384,13 @@ void VolumeRendererComponent::ResolvePendingVolumeField() {
return;
}
const uint64_t resolvedAtMs = GetVolumeTraceSteadyMs();
LogVolumeTraceFileSystem(
"ResolvePendingVolumeField path=" + VolumeFieldPathFromHandle(m_volumeField) +
" steady_ms=" + std::to_string(resolvedAtMs) +
" elapsed_ms=" + std::to_string(resolvedAtMs - completedLoad->requestedAtMs) +
" payload_bytes=" + std::to_string(m_volumeField->GetPayloadSize()));
m_volumeFieldPath = VolumeFieldPathFromHandle(m_volumeField);
if (!Resources::ResourceManager::Get().TryGetAssetRef(
m_volumeFieldPath.c_str(),

View File

@@ -27,6 +27,7 @@
#include "Rendering/Internal/ShaderVariantUtils.h"
#include <algorithm>
#include <array>
#include <cctype>
#include <cstring>
#include <d3dcompiler.h>
@@ -37,6 +38,10 @@
#include <unordered_set>
#include <vector>
#if defined(_WIN32)
#include <bcrypt.h>
#endif
namespace XCEngine {
namespace Resources {
@@ -44,6 +49,158 @@ namespace fs = std::filesystem;
namespace {
class IncrementalGuidHasher {
public:
void Append(const void* data, size_t size) {
if (data == nullptr || size == 0) {
return;
}
m_hasBytes = true;
const auto* bytes = static_cast<const Core::uint8*>(data);
for (size_t index = 0; index < size; ++index) {
m_high ^= static_cast<Core::uint64>(bytes[index]);
m_high *= 1099511628211ULL;
m_low ^= static_cast<Core::uint64>(bytes[index]);
m_low *= 1099511628211ULL;
}
}
AssetGUID Finish() const {
if (!m_hasBytes) {
return AssetGUID();
}
return AssetGUID(m_high, m_low);
}
private:
bool m_hasBytes = false;
Core::uint64 m_high = 14695981039346656037ULL;
Core::uint64 m_low = 1099511628211ULL ^ 0x9e3779b97f4a7c15ULL;
};
#if defined(_WIN32)
bool TryComputeFastFileHash(const fs::path& path, Containers::String& outHash) {
outHash.Clear();
std::ifstream input(path, std::ios::binary);
if (!input.is_open()) {
return false;
}
BCRYPT_ALG_HANDLE algorithm = nullptr;
BCRYPT_HASH_HANDLE hash = nullptr;
std::vector<UCHAR> hashObject;
std::vector<UCHAR> digest;
auto cleanup = [&]() {
if (hash != nullptr) {
BCryptDestroyHash(hash);
hash = nullptr;
}
if (algorithm != nullptr) {
BCryptCloseAlgorithmProvider(algorithm, 0);
algorithm = nullptr;
}
};
NTSTATUS status = BCryptOpenAlgorithmProvider(
&algorithm,
BCRYPT_SHA256_ALGORITHM,
nullptr,
0);
if (!BCRYPT_SUCCESS(status)) {
cleanup();
return false;
}
DWORD hashObjectSize = 0;
DWORD bytesCopied = 0;
status = BCryptGetProperty(
algorithm,
BCRYPT_OBJECT_LENGTH,
reinterpret_cast<PUCHAR>(&hashObjectSize),
sizeof(hashObjectSize),
&bytesCopied,
0);
if (!BCRYPT_SUCCESS(status) || hashObjectSize == 0) {
cleanup();
return false;
}
DWORD digestSize = 0;
status = BCryptGetProperty(
algorithm,
BCRYPT_HASH_LENGTH,
reinterpret_cast<PUCHAR>(&digestSize),
sizeof(digestSize),
&bytesCopied,
0);
if (!BCRYPT_SUCCESS(status) || digestSize == 0) {
cleanup();
return false;
}
hashObject.resize(hashObjectSize);
digest.resize(digestSize);
status = BCryptCreateHash(
algorithm,
&hash,
hashObject.data(),
static_cast<ULONG>(hashObject.size()),
nullptr,
0,
0);
if (!BCRYPT_SUCCESS(status)) {
cleanup();
return false;
}
std::array<char, 256 * 1024> buffer = {};
bool hasBytes = false;
while (input) {
input.read(buffer.data(), static_cast<std::streamsize>(buffer.size()));
const std::streamsize bytesRead = input.gcount();
if (bytesRead <= 0) {
continue;
}
hasBytes = true;
status = BCryptHashData(
hash,
reinterpret_cast<PUCHAR>(buffer.data()),
static_cast<ULONG>(bytesRead),
0);
if (!BCRYPT_SUCCESS(status)) {
cleanup();
return false;
}
}
if (!hasBytes) {
cleanup();
outHash = HashBytesToAssetGUID(nullptr, 0).ToString();
return true;
}
status = BCryptFinishHash(
hash,
digest.data(),
static_cast<ULONG>(digest.size()),
0);
if (!BCRYPT_SUCCESS(status)) {
cleanup();
return false;
}
cleanup();
outHash = HashBytesToAssetGUID(digest.data(), digest.size()).ToString();
return true;
}
#endif
std::string ToStdString(const Containers::String& value) {
return std::string(value.CStr());
}
@@ -86,6 +243,34 @@ constexpr const char* kSourceAssetDbFileName = "assets.db";
constexpr const char* kArtifactDbFileName = "artifacts.db";
constexpr const char* kLegacySourceAssetDbDirectory = "SourceAssetDB";
constexpr const char* kLegacyArtifactDbDirectory = "ArtifactDB";
constexpr Core::uint32 kArtifactDbSchemaVersion = 2;
ArtifactStorageKind InferArtifactStorageKind(const Containers::String& projectRoot,
const AssetDatabase::ArtifactRecord& record) {
if (!record.mainArtifactPath.Empty()) {
const Containers::String absoluteMainArtifactPath =
NormalizeArtifactPathString(fs::path(projectRoot.CStr()) / record.mainArtifactPath.CStr());
if (IsArtifactContainerFile(absoluteMainArtifactPath)) {
return ArtifactStorageKind::SingleFileContainer;
}
}
return record.artifactDirectory.Empty()
? ArtifactStorageKind::Unknown
: ArtifactStorageKind::LegacyDirectory;
}
Containers::String BuildArtifactMainEntryLoadPath(const Containers::String& artifactMainPath,
ArtifactStorageKind storageKind,
const Containers::String& mainEntryName) {
if (storageKind == ArtifactStorageKind::SingleFileContainer &&
!artifactMainPath.Empty() &&
!mainEntryName.Empty()) {
return BuildArtifactContainerEntryPath(artifactMainPath, mainEntryName);
}
return artifactMainPath;
}
struct ModelSubAssetManifestEntry {
LocalID localID = kInvalidLocalID;
@@ -107,10 +292,18 @@ void PopulateResolvedAssetResult(const Containers::String& projectRoot,
outAsset.relativePath = sourceRecord.relativePath;
outAsset.assetGuid = sourceRecord.guid;
outAsset.resourceType = artifactRecord.resourceType;
outAsset.artifactDirectory =
ToContainersString((fs::path(projectRoot.CStr()) / artifactRecord.artifactDirectory.CStr()).lexically_normal().generic_string());
outAsset.artifactMainPath =
ToContainersString((fs::path(projectRoot.CStr()) / artifactRecord.mainArtifactPath.CStr()).lexically_normal().generic_string());
outAsset.artifactStorageKind = artifactRecord.storageKind;
outAsset.mainEntryName = artifactRecord.mainEntryName;
if (!artifactRecord.artifactDirectory.Empty()) {
outAsset.artifactDirectory = ToContainersString(
(fs::path(projectRoot.CStr()) / artifactRecord.artifactDirectory.CStr()).lexically_normal().generic_string());
}
outAsset.artifactMainPath = ToContainersString(
(fs::path(projectRoot.CStr()) / artifactRecord.mainArtifactPath.CStr()).lexically_normal().generic_string());
outAsset.artifactMainEntryPath = BuildArtifactMainEntryLoadPath(
outAsset.artifactMainPath,
artifactRecord.storageKind,
artifactRecord.mainEntryName);
outAsset.mainLocalID = artifactRecord.mainLocalID;
}
@@ -1753,8 +1946,25 @@ void AssetDatabase::LoadArtifactDB() {
}
std::string line;
Core::uint32 schemaVersion = 1;
while (std::getline(input, line)) {
if (line.empty() || line[0] == '#') {
if (line.empty()) {
continue;
}
if (line[0] == '#') {
const std::string schemaToken = "schema=";
const size_t schemaTokenPosition = line.find(schemaToken);
if (schemaTokenPosition != std::string::npos) {
const size_t valueStart = schemaTokenPosition + schemaToken.length();
const std::string valueText = line.substr(valueStart);
if (!valueText.empty()) {
schemaVersion = static_cast<Core::uint32>(std::stoul(valueText));
}
} else if (line.find("storageKind") != std::string::npos &&
line.find("mainEntryName") != std::string::npos) {
schemaVersion = std::max(schemaVersion, kArtifactDbSchemaVersion);
}
continue;
}
@@ -1776,7 +1986,26 @@ void AssetDatabase::LoadArtifactDB() {
record.sourceFileSize = static_cast<Core::uint64>(std::stoull(fields[9]));
record.sourceWriteTime = fields.size() > 10 ? static_cast<Core::uint64>(std::stoull(fields[10])) : 0;
record.mainLocalID = fields.size() > 11 ? static_cast<LocalID>(std::stoull(fields[11])) : kMainAssetLocalID;
for (size_t index = 12; index + 3 < fields.size(); index += 4) {
size_t dependencyFieldStart = 12;
if (schemaVersion >= kArtifactDbSchemaVersion && fields.size() > 13) {
record.storageKind = static_cast<ArtifactStorageKind>(std::stoul(fields[12]));
record.mainEntryName = ToContainersString(fields[13]);
dependencyFieldStart = 14;
}
if (record.storageKind == ArtifactStorageKind::Unknown) {
record.storageKind = InferArtifactStorageKind(m_projectRoot, record);
}
if (record.mainEntryName.Empty() &&
record.storageKind == ArtifactStorageKind::SingleFileContainer) {
record.mainEntryName = "main";
}
if (record.artifactDirectory.Empty() && !record.mainArtifactPath.Empty()) {
record.artifactDirectory =
NormalizePathString(fs::path(record.mainArtifactPath.CStr()).parent_path());
}
for (size_t index = dependencyFieldStart; index + 3 < fields.size(); index += 4) {
ArtifactDependencyRecord dependency;
dependency.path = ToContainersString(fields[index + 0]);
dependency.hash = ToContainersString(fields[index + 1]);
@@ -1801,7 +2030,8 @@ void AssetDatabase::SaveArtifactDB() const {
return;
}
output << "# artifactKey\tassetGuid\timporter\tversion\ttype\tartifactDir\tmainArtifact\tsourceHash\tmetaHash\tsize\twriteTime\tmainLocalID\t(depPath\tdepHash\tdepSize\tdepWriteTime)...\n";
output << "# schema=" << kArtifactDbSchemaVersion << "\n";
output << "# artifactKey\tassetGuid\timporter\tversion\ttype\tartifactDir\tmainArtifact\tsourceHash\tmetaHash\tsize\twriteTime\tmainLocalID\tstorageKind\tmainEntryName\t(depPath\tdepHash\tdepSize\tdepWriteTime)...\n";
for (const auto& [guid, record] : m_artifactsByGuid) {
output << EscapeField(ToStdString(record.artifactKey)) << '\t'
<< EscapeField(ToStdString(record.assetGuid.ToString())) << '\t'
@@ -1814,7 +2044,9 @@ void AssetDatabase::SaveArtifactDB() const {
<< EscapeField(ToStdString(record.metaHash)) << '\t'
<< record.sourceFileSize << '\t'
<< record.sourceWriteTime << '\t'
<< record.mainLocalID;
<< record.mainLocalID << '\t'
<< static_cast<Core::uint32>(record.storageKind) << '\t'
<< EscapeField(ToStdString(record.mainEntryName));
for (const ArtifactDependencyRecord& dependency : record.dependencies) {
output << '\t' << EscapeField(ToStdString(dependency.path))
<< '\t' << EscapeField(ToStdString(dependency.hash))
@@ -2494,8 +2726,10 @@ bool AssetDatabase::ImportTextureAsset(const SourceAssetRecord& sourceRecord,
outRecord.importerName = sourceRecord.importerName;
outRecord.importerVersion = sourceRecord.importerVersion;
outRecord.resourceType = ResourceType::Texture;
outRecord.storageKind = ArtifactStorageKind::SingleFileContainer;
outRecord.artifactDirectory = artifactDir;
outRecord.mainArtifactPath = mainArtifactPath;
outRecord.mainEntryName = "main";
outRecord.sourceHash = sourceRecord.sourceHash;
outRecord.metaHash = sourceRecord.metaHash;
outRecord.sourceFileSize = sourceRecord.sourceFileSize;
@@ -2553,8 +2787,10 @@ bool AssetDatabase::ImportMaterialAsset(const SourceAssetRecord& sourceRecord,
outRecord.importerName = sourceRecord.importerName;
outRecord.importerVersion = sourceRecord.importerVersion;
outRecord.resourceType = ResourceType::Material;
outRecord.storageKind = ArtifactStorageKind::SingleFileContainer;
outRecord.artifactDirectory = artifactDir;
outRecord.mainArtifactPath = mainArtifactPath;
outRecord.mainEntryName = "main";
outRecord.sourceHash = sourceRecord.sourceHash;
outRecord.metaHash = sourceRecord.metaHash;
outRecord.sourceFileSize = sourceRecord.sourceFileSize;
@@ -2743,8 +2979,10 @@ bool AssetDatabase::ImportModelAsset(const SourceAssetRecord& sourceRecord,
outRecord.importerName = sourceRecord.importerName;
outRecord.importerVersion = sourceRecord.importerVersion;
outRecord.resourceType = ResourceType::Model;
outRecord.storageKind = ArtifactStorageKind::SingleFileContainer;
outRecord.artifactDirectory = artifactDir;
outRecord.mainArtifactPath = mainArtifactPath;
outRecord.mainEntryName = "main";
outRecord.sourceHash = sourceRecord.sourceHash;
outRecord.metaHash = sourceRecord.metaHash;
outRecord.sourceFileSize = sourceRecord.sourceFileSize;
@@ -2809,8 +3047,10 @@ bool AssetDatabase::ImportShaderAsset(const SourceAssetRecord& sourceRecord,
outRecord.importerName = sourceRecord.importerName;
outRecord.importerVersion = sourceRecord.importerVersion;
outRecord.resourceType = ResourceType::Shader;
outRecord.storageKind = ArtifactStorageKind::SingleFileContainer;
outRecord.artifactDirectory = artifactDir;
outRecord.mainArtifactPath = mainArtifactPath;
outRecord.mainEntryName = "main";
outRecord.sourceHash = sourceRecord.sourceHash;
outRecord.metaHash = sourceRecord.metaHash;
outRecord.sourceFileSize = sourceRecord.sourceFileSize;
@@ -2874,8 +3114,10 @@ bool AssetDatabase::ImportGaussianSplatAsset(const SourceAssetRecord& sourceReco
outRecord.importerName = sourceRecord.importerName;
outRecord.importerVersion = sourceRecord.importerVersion;
outRecord.resourceType = ResourceType::GaussianSplat;
outRecord.storageKind = ArtifactStorageKind::SingleFileContainer;
outRecord.artifactDirectory = artifactDir;
outRecord.mainArtifactPath = mainArtifactPath;
outRecord.mainEntryName = "main";
outRecord.sourceHash = sourceRecord.sourceHash;
outRecord.metaHash = sourceRecord.metaHash;
outRecord.sourceFileSize = sourceRecord.sourceFileSize;
@@ -2953,8 +3195,10 @@ bool AssetDatabase::ImportVolumeFieldAsset(const SourceAssetRecord& sourceRecord
outRecord.importerName = sourceRecord.importerName;
outRecord.importerVersion = sourceRecord.importerVersion;
outRecord.resourceType = ResourceType::VolumeField;
outRecord.storageKind = ArtifactStorageKind::SingleFileContainer;
outRecord.artifactDirectory = artifactDir;
outRecord.mainArtifactPath = mainArtifactPath;
outRecord.mainEntryName = "main";
outRecord.sourceHash = sourceRecord.sourceHash;
outRecord.metaHash = sourceRecord.metaHash;
outRecord.sourceFileSize = sourceRecord.sourceFileSize;
@@ -3040,8 +3284,10 @@ bool AssetDatabase::ImportUIDocumentAsset(const SourceAssetRecord& sourceRecord,
outRecord.importerName = sourceRecord.importerName;
outRecord.importerVersion = sourceRecord.importerVersion;
outRecord.resourceType = resourceType;
outRecord.storageKind = ArtifactStorageKind::SingleFileContainer;
outRecord.artifactDirectory = artifactDir;
outRecord.mainArtifactPath = mainArtifactPath;
outRecord.mainEntryName = "main";
outRecord.sourceHash = sourceRecord.sourceHash;
outRecord.metaHash = sourceRecord.metaHash;
outRecord.sourceFileSize = sourceRecord.sourceFileSize;
@@ -3315,19 +3561,31 @@ Containers::String AssetDatabase::ReadWholeFileText(const fs::path& path) {
}
Containers::String AssetDatabase::ComputeFileHash(const fs::path& path) {
#if defined(_WIN32)
Containers::String fastHash;
if (TryComputeFastFileHash(path, fastHash)) {
return fastHash;
}
#endif
std::ifstream input(path, std::ios::binary);
if (!input.is_open()) {
return Containers::String();
}
std::vector<Core::uint8> bytes(
(std::istreambuf_iterator<char>(input)),
std::istreambuf_iterator<char>());
if (bytes.empty()) {
return HashBytesToAssetGUID(nullptr, 0).ToString();
IncrementalGuidHasher hasher;
std::array<char, 64 * 1024> buffer = {};
while (input) {
input.read(buffer.data(), static_cast<std::streamsize>(buffer.size()));
const std::streamsize bytesRead = input.gcount();
if (bytesRead <= 0) {
continue;
}
hasher.Append(buffer.data(), static_cast<size_t>(bytesRead));
}
return HashBytesToAssetGUID(bytes.data(), bytes.size()).ToString();
return hasher.Finish().ToString();
}
Core::uint64 AssetDatabase::GetFileSizeValue(const fs::path& path) {

View File

@@ -341,9 +341,17 @@ AssetImportService::ImportedAsset AssetImportService::ConvertResolvedAsset(
importedAsset.relativePath = resolvedAsset.relativePath;
importedAsset.assetGuid = resolvedAsset.assetGuid;
importedAsset.resourceType = resolvedAsset.resourceType;
importedAsset.artifactMainPath = resolvedAsset.artifactMainPath;
importedAsset.artifactMainEntryPath = resolvedAsset.artifactMainEntryPath;
importedAsset.runtimeLoadPath =
resolvedAsset.artifactReady ? resolvedAsset.artifactMainPath : resolvedAsset.absolutePath;
resolvedAsset.artifactReady
? (!resolvedAsset.artifactMainEntryPath.Empty()
? resolvedAsset.artifactMainEntryPath
: resolvedAsset.artifactMainPath)
: resolvedAsset.absolutePath;
importedAsset.artifactDirectory = resolvedAsset.artifactDirectory;
importedAsset.artifactStorageKind = resolvedAsset.artifactStorageKind;
importedAsset.mainEntryName = resolvedAsset.mainEntryName;
importedAsset.mainLocalID = resolvedAsset.mainLocalID;
return importedAsset;
}

View File

@@ -1,4 +1,5 @@
#include <XCEngine/Core/IO/IResourceLoader.h>
#include <XCEngine/Core/Asset/ArtifactContainer.h>
#include <XCEngine/Core/Asset/ResourceManager.h>
#include <filesystem>
#include <fstream>
@@ -53,16 +54,23 @@ Containers::Array<Core::uint8> IResourceLoader::ReadFileData(const Containers::S
}
Containers::String IResourceLoader::GetExtension(const Containers::String& path) {
Containers::String normalizedPath = path;
Containers::String containerPath;
Containers::String entryName;
if (TryParseArtifactContainerEntryPath(path, containerPath, entryName)) {
normalizedPath = containerPath;
}
Containers::String ext;
size_t dotPos = Containers::String::npos;
for (size_t i = path.Length(); i > 0; --i) {
if (path[i - 1] == '.') {
for (size_t i = normalizedPath.Length(); i > 0; --i) {
if (normalizedPath[i - 1] == '.') {
dotPos = i - 1;
break;
}
}
if (dotPos != Containers::String::npos) {
ext = path.Substring(dotPos + 1);
ext = normalizedPath.Substring(dotPos + 1);
}
return ext;
}

View File

@@ -1,5 +1,9 @@
#include "XCEngine/RHI/D3D12/D3D12Buffer.h"
#include "Debug/Logger.h"
#include <cstdio>
namespace XCEngine {
namespace RHI {
@@ -46,6 +50,28 @@ bool D3D12Buffer::Initialize(
);
if (FAILED(hResult)) {
char message[256];
std::snprintf(
message,
sizeof(message),
"D3D12Buffer::Initialize CreateCommittedResource failed: hr=0x%08X size=%llu heapType=%u flags=0x%X initialState=0x%X",
static_cast<unsigned int>(hResult),
static_cast<unsigned long long>(size),
static_cast<unsigned int>(heapType),
static_cast<unsigned int>(resourceFlags),
static_cast<unsigned int>(initialState));
Debug::Logger::Get().Error(Debug::LogCategory::Rendering, message);
const HRESULT removedReason = device->GetDeviceRemovedReason();
if (FAILED(removedReason)) {
char removedMessage[128];
std::snprintf(
removedMessage,
sizeof(removedMessage),
"D3D12Buffer::Initialize device removed reason: 0x%08X",
static_cast<unsigned int>(removedReason));
Debug::Logger::Get().Error(Debug::LogCategory::Rendering, removedMessage);
}
return false;
}

View File

@@ -170,13 +170,13 @@ void D3D12CommandList::SetPipelineLayout(D3D12PipelineLayout* layout) {
}
void D3D12CommandList::TransitionBarrier(RHIResourceView* resource, ResourceStates stateBefore, ResourceStates stateAfter) {
if (!resource || !resource->IsValid()) return;
if (stateBefore == stateAfter || !resource || !resource->IsValid()) return;
D3D12ResourceView* d3d12View = static_cast<D3D12ResourceView*>(resource);
TransitionBarrierInternal(d3d12View->GetResource(), stateBefore, stateAfter);
}
void D3D12CommandList::TransitionBarrier(ID3D12Resource* resource, ResourceStates stateBefore, ResourceStates stateAfter) {
if (!resource) return;
if (stateBefore == stateAfter || !resource) return;
TransitionBarrierInternal(resource, stateBefore, stateAfter);
}

View File

@@ -624,6 +624,10 @@ void OpenGLCommandList::SetUniformMat4(const char* name, const float* value) {
}
void OpenGLCommandList::TransitionBarrier(RHIResourceView* resource, ResourceStates stateBefore, ResourceStates stateAfter) {
if (stateBefore == stateAfter) {
return;
}
(void)stateBefore;
if (resource != nullptr && resource->IsValid()) {
OpenGLResourceView* view = static_cast<OpenGLResourceView*>(resource);

View File

@@ -677,7 +677,17 @@ std::vector<std::filesystem::path> CollectShaderIncludeDirectories(const ShaderC
return directories;
}
std::wstring BuildIncludeDirectoryArguments(const ShaderCompileDesc& desc) {
std::wstring BuildGlslangIncludeDirectoryArguments(const ShaderCompileDesc& desc) {
std::wstring arguments;
const std::vector<std::filesystem::path> includeDirectories = CollectShaderIncludeDirectories(desc);
for (const std::filesystem::path& includeDirectory : includeDirectories) {
arguments += L" -I\"" + includeDirectory.wstring() + L"\"";
}
return arguments;
}
std::wstring BuildDxcIncludeDirectoryArguments(const ShaderCompileDesc& desc) {
std::wstring arguments;
const std::vector<std::filesystem::path> includeDirectories = CollectShaderIncludeDirectories(desc);
for (const std::filesystem::path& includeDirectory : includeDirectories) {
@@ -795,7 +805,7 @@ bool CompileGlslToSpirv(const ShaderCompileDesc& desc,
return false;
}
const std::wstring includeArguments = BuildIncludeDirectoryArguments(desc);
const std::wstring includeArguments = BuildGlslangIncludeDirectoryArguments(desc);
const std::wstring workingDirectory = ResolveCompilerWorkingDirectory(desc);
const std::wstring arguments =
SpirvTargetArguments(targetEnvironment) + L" -S " + ShaderStageArgument(type) +
@@ -888,7 +898,7 @@ bool CompileHlslToSpirv(const ShaderCompileDesc& desc,
const std::string profile = NarrowAscii(desc.profile);
const std::wstring targetProfile =
WidenAscii(profile.empty() ? "ps_6_0" : profile.c_str());
const std::wstring includeArguments = BuildIncludeDirectoryArguments(desc);
const std::wstring includeArguments = BuildDxcIncludeDirectoryArguments(desc);
const std::wstring workingDirectory = ResolveCompilerWorkingDirectory(desc);
const std::wstring arguments =
@@ -971,7 +981,7 @@ bool CompileHlslToSpirv(const ShaderCompileDesc& desc,
return false;
}
const std::wstring includeArguments = BuildIncludeDirectoryArguments(desc);
const std::wstring includeArguments = BuildGlslangIncludeDirectoryArguments(desc);
const std::wstring workingDirectory = ResolveCompilerWorkingDirectory(desc);
const std::wstring arguments =
L"-D --auto-map-bindings " + SpirvTargetArguments(targetEnvironment) +

View File

@@ -365,6 +365,10 @@ void VulkanCommandList::TransitionTexture(VulkanTexture* texture, ResourceStates
}
void VulkanCommandList::TransitionBarrier(RHIResourceView* resource, ResourceStates stateBefore, ResourceStates stateAfter) {
if (stateBefore == stateAfter) {
return;
}
(void)stateBefore;
auto* view = static_cast<VulkanResourceView*>(resource);
if (view != nullptr && view->GetTexture() != nullptr) {

View File

@@ -80,6 +80,7 @@ bool DirectionalShadowSurfaceCache::EnsureSurface(
m_depthShaderView = depthShaderView;
m_surface = RenderSurface(plan.mapWidth, plan.mapHeight);
m_surface.SetDepthAttachment(depthView);
m_surface.SetSampleDesc(1u, 0u);
return true;
}

View File

@@ -103,6 +103,7 @@ public:
entry.surface = RenderSurface(width, height);
entry.surface.SetColorAttachment(entry.renderTargetView);
entry.surface.SetSampleDesc(1u, 0u);
entry.surface.SetAutoTransitionEnabled(true);
entry.surface.SetColorStateBefore(RHI::ResourceStates::Common);
entry.surface.SetColorStateAfter(RHI::ResourceStates::PixelShaderResource);

View File

@@ -18,6 +18,7 @@ namespace Rendering {
namespace {
constexpr size_t kVolumeWordStride = sizeof(uint32_t);
constexpr uint32_t kGaussianSplatPackedPositionStride = sizeof(float) * 4u;
uint64_t GetVolumeTraceSteadyMs() {
using Clock = std::chrono::steady_clock;
@@ -236,6 +237,15 @@ bool ValidateGaussianSplatSectionLayout(
return expectedDataSize == section.dataSize;
}
bool ShouldPadGaussianSplatSectionToAlignedFloat4(
Resources::GaussianSplatSectionType sectionType,
Resources::GaussianSplatSectionFormat format,
uint32_t stride) {
return sectionType == Resources::GaussianSplatSectionType::Positions &&
format == Resources::GaussianSplatSectionFormat::VectorFloat32 &&
stride == sizeof(Resources::GaussianSplatPositionRecord);
}
} // namespace
RenderResourceCache::~RenderResourceCache() {
@@ -709,29 +719,54 @@ bool RenderResourceCache::UploadGaussianSplatSection(
return false;
}
std::vector<Core::uint8> alignedUploadData;
const void* uploadData = sectionData;
uint64_t uploadSize = section->dataSize;
uint32_t uploadStride = resolvedStride;
if (ShouldPadGaussianSplatSectionToAlignedFloat4(sectionType, section->format, resolvedStride)) {
alignedUploadData.resize(
static_cast<size_t>(section->elementCount) * static_cast<size_t>(kGaussianSplatPackedPositionStride),
static_cast<Core::uint8>(0u));
const auto* sourcePositions = static_cast<const Resources::GaussianSplatPositionRecord*>(sectionData);
auto* destinationWords = reinterpret_cast<float*>(alignedUploadData.data());
for (uint32_t elementIndex = 0u; elementIndex < section->elementCount; ++elementIndex) {
const Resources::GaussianSplatPositionRecord& source = sourcePositions[elementIndex];
const size_t destinationIndex = static_cast<size_t>(elementIndex) * 4u;
destinationWords[destinationIndex + 0u] = source.position.x;
destinationWords[destinationIndex + 1u] = source.position.y;
destinationWords[destinationIndex + 2u] = source.position.z;
destinationWords[destinationIndex + 3u] = 0.0f;
}
uploadData = alignedUploadData.data();
uploadSize = static_cast<uint64_t>(alignedUploadData.size());
uploadStride = kGaussianSplatPackedPositionStride;
}
RHI::BufferDesc bufferDesc = {};
bufferDesc.size = section->dataSize;
bufferDesc.stride = resolvedStride;
bufferDesc.size = uploadSize;
bufferDesc.stride = uploadStride;
bufferDesc.bufferType = static_cast<uint32_t>(RHI::BufferType::Storage);
bufferDesc.flags = 0u;
cachedSection.buffer = device->CreateBuffer(
bufferDesc,
sectionData,
static_cast<size_t>(section->dataSize),
uploadData,
static_cast<size_t>(uploadSize),
RHI::ResourceStates::GenericRead);
if (cachedSection.buffer == nullptr) {
return false;
}
cachedSection.buffer->SetStride(resolvedStride);
cachedSection.buffer->SetStride(uploadStride);
cachedSection.buffer->SetBufferType(RHI::BufferType::Storage);
RHI::ResourceViewDesc viewDesc = {};
viewDesc.dimension = RHI::ResourceViewDimension::StructuredBuffer;
viewDesc.firstElement = 0u;
viewDesc.elementCount = section->elementCount;
viewDesc.structureByteStride = resolvedStride;
viewDesc.structureByteStride = uploadStride;
cachedSection.shaderResourceView = device->CreateShaderResourceView(cachedSection.buffer, viewDesc);
if (cachedSection.shaderResourceView == nullptr) {
@@ -740,9 +775,9 @@ bool RenderResourceCache::UploadGaussianSplatSection(
cachedSection.type = sectionType;
cachedSection.format = section->format;
cachedSection.elementStride = resolvedStride;
cachedSection.elementStride = uploadStride;
cachedSection.elementCount = section->elementCount;
cachedSection.payloadSize = section->dataSize;
cachedSection.payloadSize = uploadSize;
return true;
}

View File

@@ -1,438 +0,0 @@
#pragma once
#include <XCEngine/Core/Containers/String.h>
#include <XCEngine/RHI/RHIEnums.h>
#include <XCEngine/RHI/RHIPipelineState.h>
#include <XCEngine/Rendering/Builtin/BuiltinPassLayoutUtils.h>
#include <XCEngine/Resources/BuiltinResources.h>
#include <XCEngine/Resources/Shader/Shader.h>
#include <regex>
#include <string>
namespace XCEngine {
namespace Rendering {
namespace Detail {
inline Resources::ShaderBackend ToShaderBackend(RHI::RHIType backendType) {
switch (backendType) {
case RHI::RHIType::D3D12:
return Resources::ShaderBackend::D3D12;
case RHI::RHIType::Vulkan:
return Resources::ShaderBackend::Vulkan;
case RHI::RHIType::OpenGL:
default:
return Resources::ShaderBackend::OpenGL;
}
}
inline RHI::ShaderLanguage ToRHIShaderLanguage(Resources::ShaderLanguage language) {
switch (language) {
case Resources::ShaderLanguage::HLSL:
return RHI::ShaderLanguage::HLSL;
case Resources::ShaderLanguage::SPIRV:
return RHI::ShaderLanguage::SPIRV;
case Resources::ShaderLanguage::GLSL:
default:
return RHI::ShaderLanguage::GLSL;
}
}
inline std::wstring ToWideAscii(const Containers::String& value) {
std::wstring wide;
wide.reserve(value.Length());
for (size_t index = 0; index < value.Length(); ++index) {
wide.push_back(static_cast<wchar_t>(value[index]));
}
return wide;
}
inline std::string ToStdString(const Containers::String& value) {
return std::string(value.CStr(), value.Length());
}
inline std::string EscapeRegexLiteral(const Containers::String& value) {
std::string escaped;
escaped.reserve(value.Length() * 2u);
for (size_t index = 0; index < value.Length(); ++index) {
const char ch = value[index];
switch (ch) {
case '\\':
case '^':
case '$':
case '.':
case '|':
case '?':
case '*':
case '+':
case '(':
case ')':
case '[':
case ']':
case '{':
case '}':
escaped.push_back('\\');
break;
default:
break;
}
escaped.push_back(ch);
}
return escaped;
}
inline bool TryCollectShaderPassResourceBindings(
const Resources::ShaderPass& pass,
Containers::Array<Resources::ShaderResourceBindingDesc>& outBindings) {
outBindings.Clear();
if (pass.resources.Empty()) {
return false;
}
outBindings.Reserve(pass.resources.Size());
for (const Resources::ShaderResourceBindingDesc& binding : pass.resources) {
outBindings.PushBack(binding);
}
return true;
}
inline bool TryRewriteHlslRegisterBindingWithName(
std::string& sourceText,
const Containers::String& declarationName,
const char* registerPrefix,
Core::uint32 bindingIndex,
Core::uint32 setIndex,
bool includeRegisterSpace,
Resources::ShaderResourceType resourceType) {
if (declarationName.Empty()) {
return false;
}
const std::string registerClause =
includeRegisterSpace
? std::string("register(") + registerPrefix +
std::to_string(bindingIndex) +
", space" +
std::to_string(setIndex) +
")"
: std::string("register(") + registerPrefix +
std::to_string(bindingIndex) +
")";
const std::string escapedName = EscapeRegexLiteral(declarationName);
if (resourceType == Resources::ShaderResourceType::ConstantBuffer) {
const std::regex pattern(
"(cbuffer\\s+" + escapedName + "\\s*)(:\\s*register\\s*\\([^\\)]*\\))?(\\s*\\{)",
std::regex::ECMAScript);
const std::string rewritten =
std::regex_replace(sourceText, pattern, "$1: " + registerClause + "$3");
if (rewritten != sourceText) {
sourceText = rewritten;
return true;
}
return false;
}
if (resourceType == Resources::ShaderResourceType::StructuredBuffer ||
resourceType == Resources::ShaderResourceType::RWStructuredBuffer) {
const std::regex pattern(
"((?:globallycoherent\\s+)?(?:StructuredBuffer|RWStructuredBuffer)\\s*<[^;\\r\\n>]+>\\s+" +
escapedName + "\\s*)(:\\s*register\\s*\\([^\\)]*\\))?(\\s*;)",
std::regex::ECMAScript);
const std::string rewritten =
std::regex_replace(sourceText, pattern, "$1: " + registerClause + "$3");
if (rewritten != sourceText) {
sourceText = rewritten;
return true;
}
return false;
}
if (resourceType == Resources::ShaderResourceType::RawBuffer ||
resourceType == Resources::ShaderResourceType::RWRawBuffer) {
const std::regex pattern(
"((?:globallycoherent\\s+)?(?:ByteAddressBuffer|RWByteAddressBuffer)\\s+" + escapedName +
"\\s*)(:\\s*register\\s*\\([^\\)]*\\))?(\\s*;)",
std::regex::ECMAScript);
const std::string rewritten =
std::regex_replace(sourceText, pattern, "$1: " + registerClause + "$3");
if (rewritten != sourceText) {
sourceText = rewritten;
return true;
}
return false;
}
const std::regex pattern(
"((?:Texture2D|TextureCube|SamplerState|SamplerComparisonState)\\s+" + escapedName +
"\\s*)(:\\s*register\\s*\\([^\\)]*\\))?(\\s*;)",
std::regex::ECMAScript);
const std::string rewritten =
std::regex_replace(sourceText, pattern, "$1: " + registerClause + "$3");
if (rewritten != sourceText) {
sourceText = rewritten;
return true;
}
return false;
}
inline const char* TryGetHlslRegisterPrefix(Resources::ShaderResourceType type) {
switch (type) {
case Resources::ShaderResourceType::ConstantBuffer:
return "b";
case Resources::ShaderResourceType::Texture2D:
case Resources::ShaderResourceType::TextureCube:
case Resources::ShaderResourceType::StructuredBuffer:
case Resources::ShaderResourceType::RawBuffer:
return "t";
case Resources::ShaderResourceType::Sampler:
return "s";
case Resources::ShaderResourceType::RWStructuredBuffer:
case Resources::ShaderResourceType::RWRawBuffer:
return "u";
default:
return nullptr;
}
}
inline bool TryRewriteHlslRegisterBinding(
std::string& sourceText,
const Resources::ShaderResourceBindingDesc& binding,
bool includeRegisterSpace) {
const char* registerPrefix = TryGetHlslRegisterPrefix(binding.type);
if (registerPrefix == nullptr) {
return false;
}
if (TryRewriteHlslRegisterBindingWithName(
sourceText,
binding.name,
registerPrefix,
binding.binding,
binding.set,
includeRegisterSpace,
binding.type)) {
return true;
}
return false;
}
inline bool TryBuildRuntimeShaderBindings(
const Resources::ShaderPass& pass,
Resources::ShaderBackend backend,
Containers::Array<Resources::ShaderResourceBindingDesc>& outBindings,
bool& outIncludeRegisterSpace) {
outBindings.Clear();
outIncludeRegisterSpace = false;
if (!TryCollectShaderPassResourceBindings(pass, outBindings)) {
return false;
}
if (backend == Resources::ShaderBackend::Vulkan) {
outIncludeRegisterSpace = true;
return true;
}
if (backend != Resources::ShaderBackend::D3D12 &&
backend != Resources::ShaderBackend::OpenGL) {
outBindings.Clear();
return false;
}
Core::uint32 nextConstantBufferRegister = 0;
Core::uint32 nextTextureRegister = 0;
Core::uint32 nextSamplerRegister = 0;
Core::uint32 nextUnorderedAccessRegister = 0;
for (Resources::ShaderResourceBindingDesc& binding : outBindings) {
binding.set = 0;
switch (binding.type) {
case Resources::ShaderResourceType::ConstantBuffer:
binding.binding = nextConstantBufferRegister++;
break;
case Resources::ShaderResourceType::Texture2D:
case Resources::ShaderResourceType::TextureCube:
case Resources::ShaderResourceType::StructuredBuffer:
case Resources::ShaderResourceType::RawBuffer:
binding.binding = nextTextureRegister++;
break;
case Resources::ShaderResourceType::Sampler:
binding.binding = nextSamplerRegister++;
break;
case Resources::ShaderResourceType::RWStructuredBuffer:
case Resources::ShaderResourceType::RWRawBuffer:
default:
binding.binding = nextUnorderedAccessRegister++;
break;
}
}
return true;
}
inline std::string BuildRuntimeShaderSource(
const Resources::ShaderPass& pass,
Resources::ShaderBackend backend,
const Resources::ShaderStageVariant& variant) {
std::string sourceText = ToStdString(variant.sourceCode);
if (variant.language != Resources::ShaderLanguage::HLSL ||
backend == Resources::ShaderBackend::Generic) {
return sourceText;
}
Containers::Array<Resources::ShaderResourceBindingDesc> bindings;
bool includeRegisterSpace = false;
if (!TryBuildRuntimeShaderBindings(pass, backend, bindings, includeRegisterSpace)) {
return sourceText;
}
for (const Resources::ShaderResourceBindingDesc& binding : bindings) {
TryRewriteHlslRegisterBinding(sourceText, binding, includeRegisterSpace);
}
return sourceText;
}
inline void AddShaderCompileMacro(
RHI::ShaderCompileDesc& compileDesc,
const wchar_t* name,
const wchar_t* definition = L"1") {
if (name == nullptr || *name == L'\0') {
return;
}
for (const RHI::ShaderCompileMacro& existingMacro : compileDesc.macros) {
if (existingMacro.name == name) {
return;
}
}
RHI::ShaderCompileMacro macro = {};
macro.name = name;
macro.definition = definition != nullptr ? definition : L"";
compileDesc.macros.push_back(std::move(macro));
}
inline void InjectShaderBackendMacros(
Resources::ShaderBackend backend,
RHI::ShaderCompileDesc& compileDesc) {
switch (backend) {
case Resources::ShaderBackend::OpenGL:
AddShaderCompileMacro(compileDesc, L"SHADER_API_GLCORE");
AddShaderCompileMacro(compileDesc, L"UNITY_UV_STARTS_AT_TOP", L"0");
AddShaderCompileMacro(compileDesc, L"UNITY_NEAR_CLIP_VALUE", L"-1");
break;
case Resources::ShaderBackend::Vulkan:
AddShaderCompileMacro(compileDesc, L"SHADER_API_VULKAN");
AddShaderCompileMacro(compileDesc, L"UNITY_UV_STARTS_AT_TOP", L"1");
AddShaderCompileMacro(compileDesc, L"UNITY_NEAR_CLIP_VALUE", L"0");
break;
case Resources::ShaderBackend::D3D12:
AddShaderCompileMacro(compileDesc, L"SHADER_API_D3D12");
AddShaderCompileMacro(compileDesc, L"UNITY_UV_STARTS_AT_TOP", L"1");
AddShaderCompileMacro(compileDesc, L"UNITY_NEAR_CLIP_VALUE", L"0");
break;
case Resources::ShaderBackend::Generic:
default:
break;
}
}
inline void ApplyShaderStageVariant(
const Resources::ShaderStageVariant& variant,
RHI::ShaderCompileDesc& compileDesc) {
compileDesc.source.assign(
variant.sourceCode.CStr(),
variant.sourceCode.CStr() + variant.sourceCode.Length());
compileDesc.sourceLanguage = ToRHIShaderLanguage(variant.language);
compileDesc.entryPoint = ToWideAscii(variant.entryPoint);
compileDesc.profile = ToWideAscii(variant.profile);
}
inline std::wstring ResolveRuntimeShaderSourcePath(const Containers::String& shaderPath) {
Containers::String resolvedPath = shaderPath;
if (resolvedPath.Empty()) {
return std::wstring();
}
if (Resources::IsBuiltinShaderPath(resolvedPath)) {
Containers::String builtinAssetPath;
if (!Resources::TryResolveBuiltinShaderAssetPath(resolvedPath, builtinAssetPath)) {
return std::wstring();
}
resolvedPath = builtinAssetPath;
}
return ToWideAscii(resolvedPath);
}
inline void ApplyShaderStageVariant(
const Containers::String& shaderPath,
const Resources::ShaderPass& pass,
Resources::ShaderBackend backend,
const Resources::ShaderStageVariant& variant,
RHI::ShaderCompileDesc& compileDesc) {
const std::string sourceText = BuildRuntimeShaderSource(pass, backend, variant);
compileDesc.source.assign(sourceText.begin(), sourceText.end());
compileDesc.fileName = ResolveRuntimeShaderSourcePath(shaderPath);
compileDesc.sourceLanguage = ToRHIShaderLanguage(variant.language);
compileDesc.entryPoint = ToWideAscii(variant.entryPoint);
compileDesc.profile = ToWideAscii(variant.profile);
InjectShaderBackendMacros(backend, compileDesc);
}
inline void ApplyShaderStageVariant(
const Resources::ShaderPass& pass,
Resources::ShaderBackend backend,
const Resources::ShaderStageVariant& variant,
RHI::ShaderCompileDesc& compileDesc) {
ApplyShaderStageVariant(Containers::String(), pass, backend, variant, compileDesc);
}
inline Containers::String BuildShaderKeywordSignature(
const Resources::ShaderKeywordSet& keywordSet) {
Resources::ShaderKeywordSet normalizedKeywords = keywordSet;
Resources::NormalizeShaderKeywordSetInPlace(normalizedKeywords);
Containers::String signature;
for (size_t keywordIndex = 0; keywordIndex < normalizedKeywords.enabledKeywords.Size(); ++keywordIndex) {
if (keywordIndex > 0) {
signature += ";";
}
signature += normalizedKeywords.enabledKeywords[keywordIndex];
}
return signature;
}
inline bool ShaderPassHasGraphicsVariants(
const Resources::Shader& shader,
const Containers::String& passName,
Resources::ShaderBackend backend,
const Resources::ShaderKeywordSet& enabledKeywords = Resources::ShaderKeywordSet()) {
return shader.FindVariant(
passName,
Resources::ShaderType::Vertex,
backend,
enabledKeywords) != nullptr &&
shader.FindVariant(
passName,
Resources::ShaderType::Fragment,
backend,
enabledKeywords) != nullptr;
}
} // namespace Detail
} // namespace Rendering
} // namespace XCEngine

View File

@@ -100,7 +100,9 @@ bool ExecuteScenePassRequest(
context,
request.surface,
sceneData,
nullptr
nullptr,
nullptr,
RHI::ResourceStates::Common
};
return pass->Execute(passContext);
}
@@ -122,7 +124,9 @@ bool ExecuteStandalonePass(
context,
surface,
sceneData,
nullptr
nullptr,
nullptr,
RHI::ResourceStates::Common
};
return pass->Execute(passContext);
}
@@ -242,6 +246,7 @@ bool ExecuteFullscreenPassSequenceStage(
const RenderSurface* currentSourceSurface = passContext.sourceSurface;
RHI::RHIResourceView* currentSourceColorView = passContext.sourceColorView;
RHI::ResourceStates currentSourceColorState = passContext.sourceColorState;
for (size_t passIndex = 0; passIndex < sequence->GetPassCount(); ++passIndex) {
const bool isLastPass = (passIndex + 1u) == sequence->GetPassCount();
@@ -264,7 +269,8 @@ bool ExecuteFullscreenPassSequenceStage(
*outputSurface,
passContext.sceneData,
currentSourceSurface,
currentSourceColorView
currentSourceColorView,
currentSourceColorState
};
if (!sequence->ExecutePass(passIndex, chainedContext)) {
return false;
@@ -274,6 +280,7 @@ bool ExecuteFullscreenPassSequenceStage(
intermediateEntry->currentColorState = RHI::ResourceStates::PixelShaderResource;
currentSourceSurface = &intermediateEntry->surface;
currentSourceColorView = intermediateEntry->shaderResourceView;
currentSourceColorState = intermediateEntry->currentColorState;
}
}
@@ -290,7 +297,8 @@ RenderPassContext BuildFrameStagePassContext(
outputSurface != nullptr ? *outputSurface : request.surface,
sceneData,
request.GetSourceSurface(stage),
request.GetSourceColorView(stage)
request.GetSourceColorView(stage),
request.GetSourceColorState(stage)
};
}
@@ -373,11 +381,21 @@ RenderDirectionalShadowData BuildDirectionalShadowData(
shadowData.enabled = true;
shadowData.viewProjection = plan.cameraData.viewProjection;
shadowData.shadowMap = shadowMapView;
const float texelWorldSize = plan.texelWorldSize > Math::EPSILON
? plan.texelWorldSize
: (plan.orthographicHalfExtent > Math::EPSILON && plan.mapWidth > 0u
? (plan.orthographicHalfExtent * 2.0f) / static_cast<float>(plan.mapWidth)
: 0.0f);
shadowData.shadowParams = Math::Vector4(
0.0015f,
1.0f / static_cast<float>(plan.mapWidth),
1.0f / static_cast<float>(plan.mapHeight),
0.85f);
shadowData.shadowOptions = Math::Vector4(
1.0f,
texelWorldSize,
1.5f,
0.0f);
return shadowData;
}
@@ -619,43 +637,74 @@ bool CameraRenderer::ExecuteRenderPlan(
bool CameraRenderer::Render(
const CameraRenderRequest& request) {
if (!request.IsValid() || m_pipeline == nullptr) {
Debug::Logger::Get().Error(
Debug::LogCategory::Rendering,
"CameraRenderer::Render failed: request invalid or pipeline missing");
return false;
}
const RenderSurface& mainSceneSurface = request.GetMainSceneSurface();
if (mainSceneSurface.GetRenderAreaWidth() == 0 ||
mainSceneSurface.GetRenderAreaHeight() == 0) {
Debug::Logger::Get().Error(
Debug::LogCategory::Rendering,
"CameraRenderer::Render failed: main scene surface render area is empty");
return false;
}
if (request.depthOnly.IsRequested() &&
!request.depthOnly.IsValid()) {
Debug::Logger::Get().Error(
Debug::LogCategory::Rendering,
"CameraRenderer::Render failed: depth-only request invalid");
return false;
}
if (request.postProcess.IsRequested() &&
!request.postProcess.IsValid()) {
Debug::Logger::Get().Error(
Debug::LogCategory::Rendering,
"CameraRenderer::Render failed: post-process request invalid");
return false;
}
if (request.finalOutput.IsRequested() &&
!request.finalOutput.IsValid()) {
Debug::Logger::Get().Error(
Debug::LogCategory::Rendering,
"CameraRenderer::Render failed: final-output request invalid");
return false;
}
if (request.objectId.IsRequested() &&
!request.objectId.IsValid()) {
Debug::Logger::Get().Error(
Debug::LogCategory::Rendering,
"CameraRenderer::Render failed: object-id request invalid");
return false;
}
ShadowCasterRenderRequest resolvedShadowCaster = {};
RHI::RHIResourceView* shadowMapView = nullptr;
if (!ResolveShadowCasterRequest(request, resolvedShadowCaster, shadowMapView)) {
Debug::Logger::Get().Error(
Debug::LogCategory::Rendering,
"CameraRenderer::Render failed: ResolveShadowCasterRequest returned false");
return false;
}
RenderSceneData sceneData = {};
if (!BuildSceneDataForRequest(request, shadowMapView, sceneData)) {
Debug::Logger::Get().Error(
Debug::LogCategory::Rendering,
"CameraRenderer::Render failed: BuildSceneDataForRequest returned false");
return false;
}
return ExecuteRenderPlan(request, resolvedShadowCaster, sceneData);
if (!ExecuteRenderPlan(request, resolvedShadowCaster, sceneData)) {
Debug::Logger::Get().Error(
Debug::LogCategory::Rendering,
"CameraRenderer::Render failed: ExecuteRenderPlan returned false");
return false;
}
return true;
}
} // namespace Rendering

View File

@@ -0,0 +1,137 @@
#pragma once
#include <XCEngine/Rendering/RenderSurface.h>
#include <XCEngine/RHI/RHIResourceView.h>
#include <array>
#include <cstdint>
namespace XCEngine {
namespace Rendering {
namespace Internal {
inline constexpr uint32_t kMaxPipelineColorAttachmentCount = 8u;
inline uint32_t ResolveSurfaceColorAttachmentCount(const RenderSurface& surface) {
uint32_t count = 0;
for (RHI::RHIResourceView* attachment : surface.GetColorAttachments()) {
if (attachment == nullptr || count >= kMaxPipelineColorAttachmentCount) {
break;
}
++count;
}
return count;
}
inline bool HasSingleColorAttachment(const RenderSurface& surface) {
return ResolveSurfaceColorAttachmentCount(surface) == 1u;
}
inline bool HasZeroOrSingleColorAttachment(const RenderSurface& surface) {
return ResolveSurfaceColorAttachmentCount(surface) <= 1u;
}
inline RHI::Format ResolveSurfaceColorFormat(
const RenderSurface& surface,
uint32_t attachmentIndex = 0u) {
const std::vector<RHI::RHIResourceView*>& colorAttachments = surface.GetColorAttachments();
if (attachmentIndex >= colorAttachments.size() || colorAttachments[attachmentIndex] == nullptr) {
return RHI::Format::Unknown;
}
return colorAttachments[attachmentIndex]->GetFormat();
}
inline bool TryResolveSingleColorAttachmentFormat(
const RenderSurface& surface,
RHI::Format& outFormat) {
if (!HasSingleColorAttachment(surface)) {
outFormat = RHI::Format::Unknown;
return false;
}
outFormat = ResolveSurfaceColorFormat(surface, 0u);
return outFormat != RHI::Format::Unknown;
}
inline bool HasKnownSingleColorAttachmentFormat(const RenderSurface& surface) {
return !HasSingleColorAttachment(surface) ||
ResolveSurfaceColorFormat(surface, 0u) != RHI::Format::Unknown;
}
inline std::array<uint32_t, kMaxPipelineColorAttachmentCount> ResolveSurfaceColorFormats(
const RenderSurface& surface) {
std::array<uint32_t, kMaxPipelineColorAttachmentCount> formats = {};
const uint32_t colorAttachmentCount = ResolveSurfaceColorAttachmentCount(surface);
for (uint32_t attachmentIndex = 0; attachmentIndex < colorAttachmentCount; ++attachmentIndex) {
formats[attachmentIndex] = static_cast<uint32_t>(ResolveSurfaceColorFormat(surface, attachmentIndex));
}
return formats;
}
inline RHI::Format ResolveSurfaceDepthFormat(const RenderSurface& surface) {
if (RHI::RHIResourceView* depthAttachment = surface.GetDepthAttachment();
depthAttachment != nullptr) {
return depthAttachment->GetFormat();
}
return RHI::Format::Unknown;
}
inline bool HasKnownDepthAttachmentFormat(const RenderSurface& surface) {
return ResolveSurfaceDepthFormat(surface) != RHI::Format::Unknown;
}
inline uint32_t ResolveSurfaceSampleCount(const RenderSurface& surface) {
return surface.GetSampleCount();
}
inline uint32_t ResolveSurfaceSampleQuality(const RenderSurface& surface) {
return surface.GetSampleQuality();
}
inline bool HasValidPipelineSampleDescription(const RenderSurface& surface) {
const uint32_t sampleCount = ResolveSurfaceSampleCount(surface);
const uint32_t sampleQuality = ResolveSurfaceSampleQuality(surface);
return sampleCount > 0u &&
(sampleCount > 1u || sampleQuality == 0u);
}
inline bool IsDepthStyleCompatibleSurface(const RenderSurface& surface) {
return HasZeroOrSingleColorAttachment(surface) &&
HasKnownSingleColorAttachmentFormat(surface) &&
HasKnownDepthAttachmentFormat(surface) &&
HasValidPipelineSampleDescription(surface);
}
inline void ApplySurfacePropertiesToGraphicsPipelineDesc(
const RenderSurface& surface,
RHI::GraphicsPipelineDesc& pipelineDesc) {
const std::array<uint32_t, kMaxPipelineColorAttachmentCount> renderTargetFormats =
ResolveSurfaceColorFormats(surface);
pipelineDesc.renderTargetCount = ResolveSurfaceColorAttachmentCount(surface);
for (uint32_t attachmentIndex = 0; attachmentIndex < pipelineDesc.renderTargetCount; ++attachmentIndex) {
pipelineDesc.renderTargetFormats[attachmentIndex] = renderTargetFormats[attachmentIndex];
}
pipelineDesc.depthStencilFormat = static_cast<uint32_t>(ResolveSurfaceDepthFormat(surface));
pipelineDesc.sampleCount = ResolveSurfaceSampleCount(surface);
pipelineDesc.sampleQuality = ResolveSurfaceSampleQuality(surface);
}
inline void ApplySingleColorAttachmentPropertiesToGraphicsPipelineDesc(
const RenderSurface& surface,
RHI::GraphicsPipelineDesc& pipelineDesc) {
pipelineDesc.renderTargetCount = HasSingleColorAttachment(surface) ? 1u : 0u;
pipelineDesc.renderTargetFormats[0] =
static_cast<uint32_t>(ResolveSurfaceColorFormat(surface, 0u));
pipelineDesc.sampleCount = ResolveSurfaceSampleCount(surface);
pipelineDesc.sampleQuality = ResolveSurfaceSampleQuality(surface);
}
} // namespace Internal
} // namespace Rendering
} // namespace XCEngine

Some files were not shown because too many files have changed in this diff Show More