Add Nahida model import and preview pipeline
174
docs/plan/Nahida Unity式Model导入与Genshin卡通渲染正式化计划_2026-04-11.md
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
# Nahida Unity式Model导入与Genshin卡通渲染正式化计划
|
||||||
|
|
||||||
|
日期:2026-04-11
|
||||||
|
|
||||||
|
## 1. 目标
|
||||||
|
|
||||||
|
这一轮不再按“静态拆件预览”推进,而是正式切到更接近 Unity 的方案:
|
||||||
|
|
||||||
|
- FBX/OBJ 等外部模型统一作为 `Model` 主资产导入
|
||||||
|
- 场景侧通过 `Model -> GameObject hierarchy` 实例化恢复节点层级
|
||||||
|
- `Mesh` 继续只负责底层几何与 section/material slot
|
||||||
|
- Nahida 的卡通渲染改为“对齐 Unity 旧工程 `Genshin.shader` 语义”,而不是继续维持近似版 shader
|
||||||
|
|
||||||
|
本轮仍然明确不做:
|
||||||
|
|
||||||
|
- 骨骼动画
|
||||||
|
- GPU skinning
|
||||||
|
- `SkinnedMeshRenderer` 运行时
|
||||||
|
- BlendShape
|
||||||
|
- Animator / AnimationClip 播放系统
|
||||||
|
|
||||||
|
## 2. 当前判断
|
||||||
|
|
||||||
|
### 2.1 Model链路已经具备的能力
|
||||||
|
|
||||||
|
代码里已经有以下基础设施:
|
||||||
|
|
||||||
|
- `ResourceType::Model`
|
||||||
|
- `Model` / `ModelLoader`
|
||||||
|
- `xcmodel` artifact
|
||||||
|
- `AssimpModelImporter`
|
||||||
|
- `AssetDatabase` 中 `ModelImporter` 以 `Model` 为主资产导入
|
||||||
|
- sub-asset manifest 与 `LocalID -> artifact path` 解析
|
||||||
|
|
||||||
|
这说明“Unity式 Model 主资产”不是从零开始,而是缺最后几段关键闭环。
|
||||||
|
|
||||||
|
### 2.2 当前最主要的缺口
|
||||||
|
|
||||||
|
真正还没闭环的是:
|
||||||
|
|
||||||
|
1. `Model` 还不能正式实例化成场景层级
|
||||||
|
2. `MeshFilter/MeshRenderer` 还缺稳定的 sub-asset `AssetRef` 绑定入口
|
||||||
|
3. 编辑器侧还没有把模型资产当成“可实例化层级对象”来用
|
||||||
|
4. Nahida 当前 `XCCharacterToon.shader` 不是 Unity 旧工程 `Genshin.shader` 的等价移植
|
||||||
|
5. FBX 导入后的静态 mesh 目前丢失了 Unity shader 依赖的顶点语义,尤其是 `vertex color` 与 `UV1/backUV`
|
||||||
|
|
||||||
|
### 2.3 关于当前 shader 的明确结论
|
||||||
|
|
||||||
|
当前使用的是:
|
||||||
|
|
||||||
|
- `project/Assets/Shaders/XCCharacterToon.shader`
|
||||||
|
|
||||||
|
Unity 参考原件是:
|
||||||
|
|
||||||
|
- `docs/reference/NahidaUnity/Shaders/Genshin.shader`
|
||||||
|
- `docs/reference/NahidaUnity/Shaders/GenshinInput.hlsl`
|
||||||
|
- `docs/reference/NahidaUnity/Shaders/GenshinForwardPass.hlsl`
|
||||||
|
- `docs/reference/NahidaUnity/Shaders/GenshinOutlinePass.hlsl`
|
||||||
|
|
||||||
|
当前 shader 只是第一版近似实现,不是原 shader 原样移植。当前效果发怪,核心原因不是简单调参,而是语义缺口:
|
||||||
|
|
||||||
|
1. 没有按 Unity 的 `_IS_FACE / _SPECULAR / _RIM / _NORMAL_MAP / _DOUBLE_SIDED` 关键字分支工作
|
||||||
|
2. 面部阴影没有按 `_FaceDirection + _FaceLightMap + _FaceShadow + _FaceShadowOffset` 的逻辑算
|
||||||
|
3. 阴影 ramp 还没有按 Unity 的 material ID 分层抽样
|
||||||
|
4. specular 还不是 Unity 那套 `lightMap + metalMap + matcap` 路径
|
||||||
|
5. rim 不是基于 scene depth 的 URP 风格边缘检测
|
||||||
|
6. outline pass 还没有正式接回 `Nahida_Body_Smooth.mesh` 与 outline color 分档逻辑
|
||||||
|
7. Unity 里还有 `MaterialUpdater.cs` 在运行时写 `_FaceDirection`,当前引擎里这条驱动链不存在
|
||||||
|
8. 更关键的是,当前引擎导入出的 Nahida mesh 只有 `Position/Normal/Tangent/Bitangent/UV0`,缺 `Color/UV1`
|
||||||
|
9. Unity 的 `GenshinForwardPass.hlsl` 明确依赖 `input.color.r` 与 `backUV`
|
||||||
|
10. 这会直接破坏 body/hair 分支的 `aoFactor = lightMap.g * input.color.r` 与双面材质背面采样,因此“脸基本对、身体和头发大面积偏色”是符合代码现状的结果
|
||||||
|
|
||||||
|
所以它现在看起来不像原工程,是正常结果。
|
||||||
|
|
||||||
|
## 3. 本轮正式范围
|
||||||
|
|
||||||
|
### Phase 1:Model实例化基础链路
|
||||||
|
|
||||||
|
目标:
|
||||||
|
|
||||||
|
- 把 `Model` 正式实例化为场景 `GameObject` 层级
|
||||||
|
|
||||||
|
任务:
|
||||||
|
|
||||||
|
- 为 `MeshFilterComponent` 增加 mesh sub-asset `AssetRef` 绑定入口
|
||||||
|
- 为 `MeshRendererComponent` 增加 material sub-asset `AssetRef` 绑定入口
|
||||||
|
- 保证这些 sub-asset 引用能被场景序列化稳定保存
|
||||||
|
- 新增 `Model -> Scene hierarchy` 实例化工具
|
||||||
|
- 恢复节点本地 TRS
|
||||||
|
- 恢复 mesh binding 与 material slot binding
|
||||||
|
|
||||||
|
验收:
|
||||||
|
|
||||||
|
- 导入一个 `OBJ/FBX Model` 后,可以在场景里创建出层级对象
|
||||||
|
- 场景存盘后 sub-asset 引用不丢
|
||||||
|
- 不需要把 FBX 拆成手工摆放的多个静态对象
|
||||||
|
|
||||||
|
### Phase 2:编辑器侧 Model使用工作流
|
||||||
|
|
||||||
|
目标:
|
||||||
|
|
||||||
|
- 让编辑器把 `Model` 当成 Unity 式模型资产使用
|
||||||
|
|
||||||
|
任务:
|
||||||
|
|
||||||
|
- 增加“从模型资产创建场景对象”的命令入口
|
||||||
|
- 后续接到 Project/Hierarchy/Viewport 的拖拽放置链路
|
||||||
|
- 让 Nahida 预览场景切到 `Model` 驱动的实例化路径
|
||||||
|
|
||||||
|
验收:
|
||||||
|
|
||||||
|
- 编辑器里不再主要依赖“手工往 `MeshFilter` 塞 FBX 路径”
|
||||||
|
- Nahida 预览对象结构来自 `Model` 实例化,而不是手工拆件
|
||||||
|
|
||||||
|
### Phase 3:Genshin shader 语义对齐
|
||||||
|
|
||||||
|
目标:
|
||||||
|
|
||||||
|
- 让 Nahida 使用更接近 Unity 旧工程的卡通渲染语义
|
||||||
|
|
||||||
|
任务:
|
||||||
|
|
||||||
|
- 先补齐静态 mesh 顶点语义链路:`Color`、`UV1/backUV`
|
||||||
|
- 让前向渲染输入布局与 `XCCharacterToon.shader` 能真正接到 `COLOR` 与 `TEXCOORD1`
|
||||||
|
- 恢复 body/hair 分支里的 `lightMap.g * input.color.r`
|
||||||
|
- 恢复双面材质对 `backUV` 的切换采样
|
||||||
|
- 按 Unity 原 shader 拆分 forward / outline 两条主路径
|
||||||
|
- 补 `_IS_FACE / _SPECULAR / _RIM / _NORMAL_MAP / _DOUBLE_SIDED` 语义
|
||||||
|
- 补 face shadow 正式逻辑
|
||||||
|
- 补 outline pass 与 `Nahida_Body_Smooth.mesh`
|
||||||
|
- 视情况补一个轻量 `_FaceDirection` 运行时驱动
|
||||||
|
|
||||||
|
验收:
|
||||||
|
|
||||||
|
- 角色明暗分层、面部阴影、描边、头发高光接近 Unity 参考
|
||||||
|
|
||||||
|
## 4. 执行顺序
|
||||||
|
|
||||||
|
这一轮按下面顺序推进:
|
||||||
|
|
||||||
|
1. 先补 `Model` 实例化与 sub-asset 引用闭环
|
||||||
|
2. 再把 Nahida 预览场景切到 `Model` 路径
|
||||||
|
3. 再补 FBX 静态 mesh 的 `Color/UV1` 导入、artifact 保存、渲染输入布局与 shader 接线
|
||||||
|
4. 再做 shader 语义对齐
|
||||||
|
5. 最后做画面调参与残余语义收口
|
||||||
|
|
||||||
|
不能反过来做。因为当前 shader 再怎么调,只要场景组织和资源绑定语义不对,结果都不稳定。
|
||||||
|
|
||||||
|
## 5. 当前这一刀
|
||||||
|
|
||||||
|
这一轮立刻执行的第一刀是:
|
||||||
|
|
||||||
|
- 补齐 FBX 静态 mesh 的 `vertex color` 与 `UV1/backUV` 导入
|
||||||
|
- 让 `BuiltinForwardPipeline` 与 `XCCharacterToon.shader` 真正吃到 `COLOR/TEXCOORD1`
|
||||||
|
- 补 Nahida 集成诊断与回归验证,先把 body/hair 的基础色输入纠正
|
||||||
|
- 在这个基础上继续向 Unity 式 `Model -> hierarchy` 与完整 shader 语义收口推进
|
||||||
|
|
||||||
|
这是 Nahida 当前从“基础贴图都不稳定”切回“先保证静态基础颜色正确”的前置条件,也是后续继续 Unity 式正式方案的必要补丁。
|
||||||
|
|
||||||
|
## 6. 2026-04-11 新发现补充
|
||||||
|
|
||||||
|
在实际跑 `tests/Rendering/integration/nahida_preview_scene` 之后,问题又进一步收窄了:
|
||||||
|
|
||||||
|
- Nahida 的 `Body_Mesh0` 运行时已经有 `uv1` 数值,且当前实现会在缺少第二套 UV 时回填 `uv1 = uv0`
|
||||||
|
- 但导入器只在 `mesh.HasTextureCoords(1)` 为真时才打 `VertexAttribute::UV1` 标记
|
||||||
|
- 结果就是:数据层面有 fallback `uv1`,语义层面却仍被当成“没有 UV1”
|
||||||
|
- 这会导致后续依赖 `backUV` 的 shader / 诊断逻辑继续走错分支
|
||||||
|
|
||||||
|
因此当前这一刀的执行重点再精确一步,变成:
|
||||||
|
|
||||||
|
1. 修复 `AssimpModelImporter` / `MeshLoader` 中“fallback uv1 已写入但 `UV1` flag 未置位”的不一致
|
||||||
|
2. 提升 `ModelImporter` 版本,强制 Nahida 现有 `.xcmodel` artifact 重导
|
||||||
|
3. 补针对 Nahida FBX 的导入回归测试,确保 `UV1` fallback 与 `Color` 语义不会再次退化
|
||||||
|
4. 再继续跑 Nahida 集成图验证 body/hair 基础色是否进一步收敛
|
||||||
@@ -325,6 +325,7 @@ add_library(XCEngine STATIC
|
|||||||
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Core/Asset/AssetGUID.h
|
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Core/Asset/AssetGUID.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Core/Asset/AssetRef.h
|
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Core/Asset/AssetRef.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Core/Asset/ArtifactFormats.h
|
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Core/Asset/ArtifactFormats.h
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Core/Asset/ArtifactContainer.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Core/Asset/AssetDatabase.h
|
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Core/Asset/AssetDatabase.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Core/Asset/AssetImportService.h
|
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Core/Asset/AssetImportService.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Core/Asset/ProjectAssetIndex.h
|
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Core/Asset/ProjectAssetIndex.h
|
||||||
@@ -336,6 +337,7 @@ add_library(XCEngine STATIC
|
|||||||
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Core/Asset/AsyncLoader.h
|
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Core/Asset/AsyncLoader.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Core/Asset/ResourceDependencyGraph.h
|
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Core/Asset/ResourceDependencyGraph.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/src/Core/Asset/AssetGUID.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/src/Core/Asset/AssetGUID.cpp
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/src/Core/Asset/ArtifactContainer.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/src/Core/Asset/AssetDatabase.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/src/Core/Asset/AssetDatabase.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/src/Core/Asset/AssetImportService.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/src/Core/Asset/AssetImportService.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/src/Core/Asset/ProjectAssetIndex.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/src/Core/Asset/ProjectAssetIndex.cpp
|
||||||
@@ -543,10 +545,12 @@ add_library(XCEngine STATIC
|
|||||||
|
|
||||||
# Scene
|
# Scene
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Scene/Scene.h
|
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Scene/Scene.h
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Scene/ModelSceneInstantiation.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Scene/SceneRuntime.h
|
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Scene/SceneRuntime.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Scene/RuntimeLoop.h
|
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Scene/RuntimeLoop.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Scene/SceneManager.h
|
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Scene/SceneManager.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/src/Scene/Scene.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/src/Scene/Scene.cpp
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/src/Scene/ModelSceneInstantiation.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/src/Scene/SceneRuntime.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/src/Scene/SceneRuntime.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/src/Scene/RuntimeLoop.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/src/Scene/RuntimeLoop.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/src/Scene/SceneManager.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/src/Scene/SceneManager.cpp
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ public:
|
|||||||
const Resources::AssetRef& GetMeshAssetRef() const { return m_meshRef; }
|
const Resources::AssetRef& GetMeshAssetRef() const { return m_meshRef; }
|
||||||
|
|
||||||
void SetMeshPath(const std::string& meshPath);
|
void SetMeshPath(const std::string& meshPath);
|
||||||
|
void SetMeshAssetRef(const Resources::AssetRef& meshRef);
|
||||||
void SetMesh(const Resources::ResourceHandle<Resources::Mesh>& mesh);
|
void SetMesh(const Resources::ResourceHandle<Resources::Mesh>& mesh);
|
||||||
void SetMesh(Resources::Mesh* mesh);
|
void SetMesh(Resources::Mesh* mesh);
|
||||||
void ClearMesh();
|
void ClearMesh();
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ public:
|
|||||||
const std::vector<Resources::AssetRef>& GetMaterialAssetRefs() const { return m_materialRefs; }
|
const std::vector<Resources::AssetRef>& GetMaterialAssetRefs() const { return m_materialRefs; }
|
||||||
|
|
||||||
void SetMaterialPath(size_t index, const std::string& materialPath);
|
void SetMaterialPath(size_t index, const std::string& materialPath);
|
||||||
|
void SetMaterialAssetRef(size_t index, const Resources::AssetRef& materialRef);
|
||||||
void SetMaterial(size_t index, const Resources::ResourceHandle<Resources::Material>& material);
|
void SetMaterial(size_t index, const Resources::ResourceHandle<Resources::Material>& material);
|
||||||
void SetMaterial(size_t index, Resources::Material* material);
|
void SetMaterial(size_t index, Resources::Material* material);
|
||||||
void SetMaterials(const std::vector<Resources::ResourceHandle<Resources::Material>>& materials);
|
void SetMaterials(const std::vector<Resources::ResourceHandle<Resources::Material>>& materials);
|
||||||
|
|||||||
105
engine/include/XCEngine/Core/Asset/ArtifactContainer.h
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <XCEngine/Core/Asset/AssetGUID.h>
|
||||||
|
#include <XCEngine/Core/Asset/ResourceTypes.h>
|
||||||
|
#include <XCEngine/Core/Containers/Array.h>
|
||||||
|
#include <XCEngine/Core/Containers/String.h>
|
||||||
|
#include <XCEngine/Core/Types.h>
|
||||||
|
|
||||||
|
namespace XCEngine {
|
||||||
|
namespace Resources {
|
||||||
|
|
||||||
|
constexpr Core::uint32 kArtifactContainerSchemaVersion = 1;
|
||||||
|
|
||||||
|
enum class ArtifactContainerCompression : Core::uint32 {
|
||||||
|
None = 0
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ArtifactContainerEntry {
|
||||||
|
Containers::String name;
|
||||||
|
ResourceType resourceType = ResourceType::Unknown;
|
||||||
|
LocalID localID = kInvalidLocalID;
|
||||||
|
Core::uint32 flags = 0;
|
||||||
|
ArtifactContainerCompression compression = ArtifactContainerCompression::None;
|
||||||
|
Containers::Array<Core::uint8> payload;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ArtifactContainerEntryView {
|
||||||
|
Containers::String name;
|
||||||
|
ResourceType resourceType = ResourceType::Unknown;
|
||||||
|
LocalID localID = kInvalidLocalID;
|
||||||
|
Core::uint32 flags = 0;
|
||||||
|
ArtifactContainerCompression compression = ArtifactContainerCompression::None;
|
||||||
|
Core::uint64 payloadOffset = 0;
|
||||||
|
Core::uint64 payloadSize = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
class ArtifactContainerWriter {
|
||||||
|
public:
|
||||||
|
void Clear();
|
||||||
|
void AddEntry(const ArtifactContainerEntry& entry);
|
||||||
|
void AddEntry(ArtifactContainerEntry&& entry);
|
||||||
|
|
||||||
|
const Containers::Array<ArtifactContainerEntry>& GetEntries() const { return m_entries; }
|
||||||
|
|
||||||
|
bool WriteToFile(const Containers::String& path,
|
||||||
|
Containers::String* outErrorMessage = nullptr) const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
Containers::Array<ArtifactContainerEntry> m_entries;
|
||||||
|
};
|
||||||
|
|
||||||
|
class ArtifactContainerReader {
|
||||||
|
public:
|
||||||
|
bool Open(const Containers::String& path,
|
||||||
|
Containers::String* outErrorMessage = nullptr);
|
||||||
|
void Close();
|
||||||
|
|
||||||
|
bool IsOpen() const { return !m_path.Empty(); }
|
||||||
|
const Containers::String& GetPath() const { return m_path; }
|
||||||
|
const Containers::Array<ArtifactContainerEntryView>& GetEntries() const { return m_entries; }
|
||||||
|
Core::uint32 GetEntryCount() const { return static_cast<Core::uint32>(m_entries.Size()); }
|
||||||
|
|
||||||
|
const ArtifactContainerEntryView* FindEntryByName(const Containers::String& name) const;
|
||||||
|
const ArtifactContainerEntryView* FindEntry(ResourceType resourceType,
|
||||||
|
LocalID localID) const;
|
||||||
|
|
||||||
|
bool ReadEntryPayload(const ArtifactContainerEntryView& entry,
|
||||||
|
Containers::Array<Core::uint8>& outPayload,
|
||||||
|
Containers::String* outErrorMessage = nullptr) const;
|
||||||
|
bool ReadEntryPayload(const Containers::String& name,
|
||||||
|
Containers::Array<Core::uint8>& outPayload,
|
||||||
|
Containers::String* outErrorMessage = nullptr) const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
Containers::String m_path;
|
||||||
|
Containers::Array<ArtifactContainerEntryView> m_entries;
|
||||||
|
Core::uint64 m_payloadStart = 0;
|
||||||
|
Core::uint64 m_payloadSize = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
bool WriteArtifactContainer(const Containers::String& path,
|
||||||
|
const Containers::Array<ArtifactContainerEntry>& entries,
|
||||||
|
Containers::String* outErrorMessage = nullptr);
|
||||||
|
bool IsArtifactContainerFile(const Containers::String& path);
|
||||||
|
Containers::String BuildArtifactContainerEntryPath(const Containers::String& containerPath,
|
||||||
|
const Containers::String& entryName);
|
||||||
|
bool TryParseArtifactContainerEntryPath(const Containers::String& path,
|
||||||
|
Containers::String& outContainerPath,
|
||||||
|
Containers::String& outEntryName);
|
||||||
|
bool ReadArtifactContainerEntryPayload(const Containers::String& containerPath,
|
||||||
|
const Containers::String& entryName,
|
||||||
|
ResourceType expectedType,
|
||||||
|
Containers::Array<Core::uint8>& outPayload,
|
||||||
|
Containers::String* outErrorMessage = nullptr);
|
||||||
|
bool ReadArtifactContainerPayloadByPath(const Containers::String& path,
|
||||||
|
ResourceType expectedType,
|
||||||
|
Containers::Array<Core::uint8>& outPayload,
|
||||||
|
Containers::String* outErrorMessage = nullptr);
|
||||||
|
bool ReadArtifactContainerMainEntryPayload(const Containers::String& path,
|
||||||
|
ResourceType expectedType,
|
||||||
|
Containers::Array<Core::uint8>& outPayload,
|
||||||
|
Containers::String* outErrorMessage = nullptr);
|
||||||
|
|
||||||
|
} // namespace Resources
|
||||||
|
} // namespace XCEngine
|
||||||
@@ -175,6 +175,7 @@ private:
|
|||||||
Core::uint64 materialVersion = 0;
|
Core::uint64 materialVersion = 0;
|
||||||
RHI::RHIResourceView* baseColorTextureView = nullptr;
|
RHI::RHIResourceView* baseColorTextureView = nullptr;
|
||||||
RHI::RHIResourceView* shadowMapTextureView = nullptr;
|
RHI::RHIResourceView* shadowMapTextureView = nullptr;
|
||||||
|
std::vector<RHI::RHIResourceView*> materialTextureViews;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct ResolvedShaderPass {
|
struct ResolvedShaderPass {
|
||||||
@@ -305,6 +306,9 @@ private:
|
|||||||
const Resources::Texture* ResolveTexture(const Resources::Material* material) const;
|
const Resources::Texture* ResolveTexture(const Resources::Material* material) const;
|
||||||
RHI::RHIResourceView* ResolveTextureView(const Resources::Texture* texture);
|
RHI::RHIResourceView* ResolveTextureView(const Resources::Texture* texture);
|
||||||
RHI::RHIResourceView* ResolveTextureView(const VisibleRenderItem& visibleItem);
|
RHI::RHIResourceView* ResolveTextureView(const VisibleRenderItem& visibleItem);
|
||||||
|
RHI::RHIResourceView* ResolveMaterialTextureView(
|
||||||
|
const Resources::Material* material,
|
||||||
|
const BuiltinPassResourceBindingDesc& binding);
|
||||||
static LightingConstants BuildLightingConstants(const RenderLightingData& lightingData);
|
static LightingConstants BuildLightingConstants(const RenderLightingData& lightingData);
|
||||||
static AdditionalLightConstants BuildAdditionalLightConstants(const RenderAdditionalLightData& lightData);
|
static AdditionalLightConstants BuildAdditionalLightConstants(const RenderAdditionalLightData& lightData);
|
||||||
bool HasSkybox(const RenderSceneData& sceneData) const;
|
bool HasSkybox(const RenderSceneData& sceneData) const;
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
#include <XCEngine/Core/Math/Bounds.h>
|
#include <XCEngine/Core/Math/Bounds.h>
|
||||||
#include <XCEngine/Core/Math/Vector2.h>
|
#include <XCEngine/Core/Math/Vector2.h>
|
||||||
#include <XCEngine/Core/Math/Vector3.h>
|
#include <XCEngine/Core/Math/Vector3.h>
|
||||||
|
#include <XCEngine/Core/Math/Vector4.h>
|
||||||
#include <XCEngine/Core/Types.h>
|
#include <XCEngine/Core/Types.h>
|
||||||
|
|
||||||
namespace XCEngine {
|
namespace XCEngine {
|
||||||
@@ -38,6 +39,8 @@ struct StaticMeshVertex {
|
|||||||
Math::Vector3 tangent = Math::Vector3::Zero();
|
Math::Vector3 tangent = Math::Vector3::Zero();
|
||||||
Math::Vector3 bitangent = Math::Vector3::Zero();
|
Math::Vector3 bitangent = Math::Vector3::Zero();
|
||||||
Math::Vector2 uv0 = Math::Vector2::Zero();
|
Math::Vector2 uv0 = Math::Vector2::Zero();
|
||||||
|
Math::Vector2 uv1 = Math::Vector2::Zero();
|
||||||
|
Math::Vector4 color = Math::Vector4::One();
|
||||||
};
|
};
|
||||||
|
|
||||||
struct MeshSection {
|
struct MeshSection {
|
||||||
|
|||||||
37
engine/include/XCEngine/Scene/ModelSceneInstantiation.h
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <XCEngine/Core/Asset/AssetRef.h>
|
||||||
|
#include <XCEngine/Core/Containers/String.h>
|
||||||
|
#include <XCEngine/Resources/Model/Model.h>
|
||||||
|
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace XCEngine {
|
||||||
|
|
||||||
|
namespace Components {
|
||||||
|
class GameObject;
|
||||||
|
class Scene;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ModelSceneInstantiationResult {
|
||||||
|
Components::GameObject* rootObject = nullptr;
|
||||||
|
std::vector<Components::GameObject*> nodeObjects;
|
||||||
|
std::vector<Components::GameObject*> meshObjects;
|
||||||
|
};
|
||||||
|
|
||||||
|
bool InstantiateModelHierarchy(
|
||||||
|
Components::Scene& scene,
|
||||||
|
const Resources::Model& model,
|
||||||
|
const Resources::AssetRef& modelAssetRef,
|
||||||
|
Components::GameObject* parent = nullptr,
|
||||||
|
ModelSceneInstantiationResult* outResult = nullptr,
|
||||||
|
Containers::String* outErrorMessage = nullptr);
|
||||||
|
|
||||||
|
bool InstantiateModelHierarchy(
|
||||||
|
Components::Scene& scene,
|
||||||
|
const Containers::String& modelPath,
|
||||||
|
Components::GameObject* parent = nullptr,
|
||||||
|
ModelSceneInstantiationResult* outResult = nullptr,
|
||||||
|
Containers::String* outErrorMessage = nullptr);
|
||||||
|
|
||||||
|
} // namespace XCEngine
|
||||||
@@ -106,6 +106,31 @@ void MeshFilterComponent::SetMeshPath(const std::string& meshPath) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void MeshFilterComponent::SetMeshAssetRef(const Resources::AssetRef& meshRef) {
|
||||||
|
m_pendingMeshLoad.reset();
|
||||||
|
m_asyncMeshLoadRequested = false;
|
||||||
|
m_mesh.Reset();
|
||||||
|
m_meshPath.clear();
|
||||||
|
m_meshRef = meshRef;
|
||||||
|
if (!m_meshRef.IsValid()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Containers::String resolvedPath;
|
||||||
|
if (Resources::ResourceManager::Get().TryResolveAssetPath(m_meshRef, resolvedPath)) {
|
||||||
|
m_meshPath = ToStdString(resolvedPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Resources::ResourceManager::Get().IsDeferredSceneLoadEnabled()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_mesh = Resources::ResourceManager::Get().Load<Resources::Mesh>(m_meshRef);
|
||||||
|
if (m_mesh.Get() != nullptr && m_meshPath.empty()) {
|
||||||
|
m_meshPath = ToStdString(m_mesh->GetPath());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void MeshFilterComponent::SetMesh(const Resources::ResourceHandle<Resources::Mesh>& mesh) {
|
void MeshFilterComponent::SetMesh(const Resources::ResourceHandle<Resources::Mesh>& mesh) {
|
||||||
m_pendingMeshLoad.reset();
|
m_pendingMeshLoad.reset();
|
||||||
m_asyncMeshLoadRequested = false;
|
m_asyncMeshLoadRequested = false;
|
||||||
@@ -271,7 +296,8 @@ void MeshFilterComponent::ResolvePendingMesh() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
m_meshPath = ToStdString(m_mesh->GetPath());
|
m_meshPath = ToStdString(m_mesh->GetPath());
|
||||||
if (!Resources::ResourceManager::Get().TryGetAssetRef(m_meshPath.c_str(), Resources::ResourceType::Mesh, m_meshRef)) {
|
if (!m_meshRef.IsValid() &&
|
||||||
|
!Resources::ResourceManager::Get().TryGetAssetRef(m_meshPath.c_str(), Resources::ResourceType::Mesh, m_meshRef)) {
|
||||||
m_meshRef.Reset();
|
m_meshRef.Reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -193,6 +193,32 @@ void MeshRendererComponent::SetMaterialPath(size_t index, const std::string& mat
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void MeshRendererComponent::SetMaterialAssetRef(size_t index, const Resources::AssetRef& materialRef) {
|
||||||
|
EnsureMaterialSlot(index);
|
||||||
|
m_pendingMaterialLoads[index].reset();
|
||||||
|
m_asyncMaterialLoadRequested[index] = false;
|
||||||
|
m_materials[index].Reset();
|
||||||
|
m_materialPaths[index].clear();
|
||||||
|
m_materialRefs[index] = materialRef;
|
||||||
|
if (!m_materialRefs[index].IsValid()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Containers::String resolvedPath;
|
||||||
|
if (Resources::ResourceManager::Get().TryResolveAssetPath(m_materialRefs[index], resolvedPath)) {
|
||||||
|
m_materialPaths[index] = ToStdString(resolvedPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Resources::ResourceManager::Get().IsDeferredSceneLoadEnabled()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_materials[index] = Resources::ResourceManager::Get().Load<Resources::Material>(m_materialRefs[index]);
|
||||||
|
if (m_materials[index].Get() != nullptr && m_materialPaths[index].empty()) {
|
||||||
|
m_materialPaths[index] = MaterialPathFromHandle(m_materials[index]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void MeshRendererComponent::SetMaterial(size_t index, const Resources::ResourceHandle<Resources::Material>& material) {
|
void MeshRendererComponent::SetMaterial(size_t index, const Resources::ResourceHandle<Resources::Material>& material) {
|
||||||
EnsureMaterialSlot(index);
|
EnsureMaterialSlot(index);
|
||||||
m_pendingMaterialLoads[index].reset();
|
m_pendingMaterialLoads[index].reset();
|
||||||
@@ -445,7 +471,8 @@ void MeshRendererComponent::ResolvePendingMaterials() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
m_materialPaths[index] = MaterialPathFromHandle(m_materials[index]);
|
m_materialPaths[index] = MaterialPathFromHandle(m_materials[index]);
|
||||||
if (!Resources::ResourceManager::Get().TryGetAssetRef(m_materialPaths[index].c_str(),
|
if (!m_materialRefs[index].IsValid() &&
|
||||||
|
!Resources::ResourceManager::Get().TryGetAssetRef(m_materialPaths[index].c_str(),
|
||||||
Resources::ResourceType::Material,
|
Resources::ResourceType::Material,
|
||||||
m_materialRefs[index])) {
|
m_materialRefs[index])) {
|
||||||
m_materialRefs[index].Reset();
|
m_materialRefs[index].Reset();
|
||||||
|
|||||||
571
engine/src/Core/Asset/ArtifactContainer.cpp
Normal file
@@ -0,0 +1,571 @@
|
|||||||
|
#include <XCEngine/Core/Asset/ArtifactContainer.h>
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
#include <cstring>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <fstream>
|
||||||
|
#include <limits>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace XCEngine {
|
||||||
|
namespace Resources {
|
||||||
|
|
||||||
|
namespace fs = std::filesystem;
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
constexpr const char* kArtifactContainerEntryPathToken = "@entry=";
|
||||||
|
|
||||||
|
struct ArtifactContainerFileHeader {
|
||||||
|
char magic[8] = { 'X', 'C', 'A', 'R', 'T', '0', '1', '\0' };
|
||||||
|
Core::uint32 schemaVersion = kArtifactContainerSchemaVersion;
|
||||||
|
Core::uint32 entryCount = 0;
|
||||||
|
Core::uint64 directorySize = 0;
|
||||||
|
Core::uint64 payloadSize = 0;
|
||||||
|
Core::uint64 contentHashHigh = 0;
|
||||||
|
Core::uint64 contentHashLow = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ArtifactContainerEntryHeader {
|
||||||
|
Core::uint32 resourceType = 0;
|
||||||
|
Core::uint32 compression = 0;
|
||||||
|
Core::uint32 flags = 0;
|
||||||
|
Core::uint32 nameLength = 0;
|
||||||
|
Core::uint64 localID = 0;
|
||||||
|
Core::uint64 payloadOffset = 0;
|
||||||
|
Core::uint64 payloadSize = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
class IncrementalArtifactHasher {
|
||||||
|
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;
|
||||||
|
};
|
||||||
|
|
||||||
|
Containers::String MakeError(const char* message) {
|
||||||
|
return Containers::String(message == nullptr ? "" : message);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool TryWriteBytes(std::ofstream& output, const void* data, size_t size) {
|
||||||
|
output.write(static_cast<const char*>(data), static_cast<std::streamsize>(size));
|
||||||
|
return static_cast<bool>(output);
|
||||||
|
}
|
||||||
|
|
||||||
|
void AppendBytes(const void* data, size_t size, std::vector<Core::uint8>& outBytes) {
|
||||||
|
if (data == nullptr || size == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto* begin = static_cast<const Core::uint8*>(data);
|
||||||
|
outBytes.insert(outBytes.end(), begin, begin + size);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool IsExpectedMagic(const ArtifactContainerFileHeader& header) {
|
||||||
|
return std::memcmp(header.magic, "XCART01", 7) == 0 &&
|
||||||
|
header.schemaVersion == kArtifactContainerSchemaVersion;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
void ArtifactContainerWriter::Clear() {
|
||||||
|
m_entries.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ArtifactContainerWriter::AddEntry(const ArtifactContainerEntry& entry) {
|
||||||
|
m_entries.PushBack(entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ArtifactContainerWriter::AddEntry(ArtifactContainerEntry&& entry) {
|
||||||
|
m_entries.PushBack(std::move(entry));
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ArtifactContainerWriter::WriteToFile(const Containers::String& path,
|
||||||
|
Containers::String* outErrorMessage) const {
|
||||||
|
return WriteArtifactContainer(path, m_entries, outErrorMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool WriteArtifactContainer(const Containers::String& path,
|
||||||
|
const Containers::Array<ArtifactContainerEntry>& entries,
|
||||||
|
Containers::String* outErrorMessage) {
|
||||||
|
std::vector<Core::uint8> directoryBytes;
|
||||||
|
Core::uint64 payloadBytes = 0;
|
||||||
|
|
||||||
|
for (const ArtifactContainerEntry& entry : entries) {
|
||||||
|
if (entry.name.Length() > std::numeric_limits<Core::uint32>::max()) {
|
||||||
|
if (outErrorMessage != nullptr) {
|
||||||
|
*outErrorMessage = MakeError("ArtifactContainer entry name is too long.");
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Core::uint64 entryPayloadSize = static_cast<Core::uint64>(entry.payload.Size());
|
||||||
|
if (payloadBytes > std::numeric_limits<Core::uint64>::max() - entryPayloadSize) {
|
||||||
|
if (outErrorMessage != nullptr) {
|
||||||
|
*outErrorMessage = MakeError("ArtifactContainer payload size overflow.");
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
ArtifactContainerEntryHeader entryHeader = {};
|
||||||
|
entryHeader.resourceType = static_cast<Core::uint32>(entry.resourceType);
|
||||||
|
entryHeader.compression = static_cast<Core::uint32>(entry.compression);
|
||||||
|
entryHeader.flags = entry.flags;
|
||||||
|
entryHeader.nameLength = static_cast<Core::uint32>(entry.name.Length());
|
||||||
|
entryHeader.localID = entry.localID;
|
||||||
|
entryHeader.payloadOffset = payloadBytes;
|
||||||
|
entryHeader.payloadSize = entryPayloadSize;
|
||||||
|
AppendBytes(&entryHeader, sizeof(entryHeader), directoryBytes);
|
||||||
|
AppendBytes(entry.name.CStr(), entry.name.Length(), directoryBytes);
|
||||||
|
|
||||||
|
payloadBytes += entryPayloadSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
IncrementalArtifactHasher hasher;
|
||||||
|
if (!directoryBytes.empty()) {
|
||||||
|
hasher.Append(directoryBytes.data(), directoryBytes.size());
|
||||||
|
}
|
||||||
|
for (const ArtifactContainerEntry& entry : entries) {
|
||||||
|
if (!entry.payload.Empty()) {
|
||||||
|
hasher.Append(entry.payload.Data(), entry.payload.Size());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ArtifactContainerFileHeader fileHeader = {};
|
||||||
|
fileHeader.entryCount = static_cast<Core::uint32>(entries.Size());
|
||||||
|
fileHeader.directorySize = static_cast<Core::uint64>(directoryBytes.size());
|
||||||
|
fileHeader.payloadSize = payloadBytes;
|
||||||
|
const AssetGUID contentHash = hasher.Finish();
|
||||||
|
fileHeader.contentHashHigh = contentHash.high;
|
||||||
|
fileHeader.contentHashLow = contentHash.low;
|
||||||
|
|
||||||
|
std::error_code ec;
|
||||||
|
const fs::path targetPath(path.CStr());
|
||||||
|
if (!targetPath.parent_path().empty()) {
|
||||||
|
fs::create_directories(targetPath.parent_path(), ec);
|
||||||
|
if (ec) {
|
||||||
|
if (outErrorMessage != nullptr) {
|
||||||
|
*outErrorMessage = MakeError("Failed to create ArtifactContainer parent directory.");
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::ofstream output(targetPath, std::ios::binary | std::ios::trunc);
|
||||||
|
if (!output.is_open()) {
|
||||||
|
if (outErrorMessage != nullptr) {
|
||||||
|
*outErrorMessage = MakeError("Failed to open ArtifactContainer output file.");
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TryWriteBytes(output, &fileHeader, sizeof(fileHeader)) ||
|
||||||
|
(!directoryBytes.empty() &&
|
||||||
|
!TryWriteBytes(output, directoryBytes.data(), directoryBytes.size()))) {
|
||||||
|
if (outErrorMessage != nullptr) {
|
||||||
|
*outErrorMessage = MakeError("Failed to write ArtifactContainer header or directory.");
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const ArtifactContainerEntry& entry : entries) {
|
||||||
|
if (!entry.payload.Empty() &&
|
||||||
|
!TryWriteBytes(output, entry.payload.Data(), entry.payload.Size())) {
|
||||||
|
if (outErrorMessage != nullptr) {
|
||||||
|
*outErrorMessage = MakeError("Failed to write ArtifactContainer payload.");
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool IsArtifactContainerFile(const Containers::String& path) {
|
||||||
|
std::ifstream input(path.CStr(), std::ios::binary);
|
||||||
|
if (!input.is_open()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
ArtifactContainerFileHeader fileHeader = {};
|
||||||
|
input.read(reinterpret_cast<char*>(&fileHeader), sizeof(fileHeader));
|
||||||
|
return input && IsExpectedMagic(fileHeader);
|
||||||
|
}
|
||||||
|
|
||||||
|
Containers::String BuildArtifactContainerEntryPath(const Containers::String& containerPath,
|
||||||
|
const Containers::String& entryName) {
|
||||||
|
if (containerPath.Empty() || entryName.Empty()) {
|
||||||
|
return Containers::String();
|
||||||
|
}
|
||||||
|
|
||||||
|
Containers::String result = containerPath;
|
||||||
|
result += kArtifactContainerEntryPathToken;
|
||||||
|
result += entryName;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool TryParseArtifactContainerEntryPath(const Containers::String& path,
|
||||||
|
Containers::String& outContainerPath,
|
||||||
|
Containers::String& outEntryName) {
|
||||||
|
outContainerPath.Clear();
|
||||||
|
outEntryName.Clear();
|
||||||
|
|
||||||
|
const std::string text(path.CStr());
|
||||||
|
const std::string token(kArtifactContainerEntryPathToken);
|
||||||
|
const size_t tokenPos = text.rfind(token);
|
||||||
|
if (tokenPos == std::string::npos) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const size_t entryNamePos = tokenPos + token.length();
|
||||||
|
if (tokenPos == 0 || entryNamePos >= text.length()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
outContainerPath = Containers::String(text.substr(0, tokenPos).c_str());
|
||||||
|
outEntryName = Containers::String(text.substr(entryNamePos).c_str());
|
||||||
|
return !outContainerPath.Empty() && !outEntryName.Empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ArtifactContainerReader::Open(const Containers::String& path,
|
||||||
|
Containers::String* outErrorMessage) {
|
||||||
|
Close();
|
||||||
|
|
||||||
|
std::ifstream input(path.CStr(), std::ios::binary);
|
||||||
|
if (!input.is_open()) {
|
||||||
|
if (outErrorMessage != nullptr) {
|
||||||
|
*outErrorMessage = MakeError("Failed to open ArtifactContainer file.");
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
ArtifactContainerFileHeader fileHeader = {};
|
||||||
|
input.read(reinterpret_cast<char*>(&fileHeader), sizeof(fileHeader));
|
||||||
|
if (!input || !IsExpectedMagic(fileHeader)) {
|
||||||
|
if (outErrorMessage != nullptr) {
|
||||||
|
*outErrorMessage = MakeError("ArtifactContainer header is invalid.");
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
input.seekg(0, std::ios::end);
|
||||||
|
const std::streamoff fileSize = input.tellg();
|
||||||
|
const Core::uint64 expectedFileSize =
|
||||||
|
static_cast<Core::uint64>(sizeof(fileHeader)) +
|
||||||
|
fileHeader.directorySize +
|
||||||
|
fileHeader.payloadSize;
|
||||||
|
if (fileSize < static_cast<std::streamoff>(sizeof(fileHeader)) ||
|
||||||
|
expectedFileSize != static_cast<Core::uint64>(fileSize)) {
|
||||||
|
if (outErrorMessage != nullptr) {
|
||||||
|
*outErrorMessage = MakeError("ArtifactContainer file size does not match header.");
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
input.seekg(static_cast<std::streamoff>(sizeof(fileHeader)), std::ios::beg);
|
||||||
|
std::vector<Core::uint8> directoryBytes(static_cast<size_t>(fileHeader.directorySize));
|
||||||
|
if (!directoryBytes.empty()) {
|
||||||
|
input.read(reinterpret_cast<char*>(directoryBytes.data()),
|
||||||
|
static_cast<std::streamsize>(directoryBytes.size()));
|
||||||
|
if (!input) {
|
||||||
|
if (outErrorMessage != nullptr) {
|
||||||
|
*outErrorMessage = MakeError("Failed to read ArtifactContainer directory.");
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Containers::Array<ArtifactContainerEntryView> parsedEntries;
|
||||||
|
parsedEntries.Reserve(fileHeader.entryCount);
|
||||||
|
|
||||||
|
size_t cursor = 0;
|
||||||
|
for (Core::uint32 entryIndex = 0; entryIndex < fileHeader.entryCount; ++entryIndex) {
|
||||||
|
if (cursor + sizeof(ArtifactContainerEntryHeader) > directoryBytes.size()) {
|
||||||
|
if (outErrorMessage != nullptr) {
|
||||||
|
*outErrorMessage = MakeError("ArtifactContainer directory is truncated.");
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
ArtifactContainerEntryHeader entryHeader = {};
|
||||||
|
std::memcpy(&entryHeader, directoryBytes.data() + cursor, sizeof(entryHeader));
|
||||||
|
cursor += sizeof(entryHeader);
|
||||||
|
|
||||||
|
if (cursor + entryHeader.nameLength > directoryBytes.size()) {
|
||||||
|
if (outErrorMessage != nullptr) {
|
||||||
|
*outErrorMessage = MakeError("ArtifactContainer entry name is truncated.");
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entryHeader.payloadOffset > fileHeader.payloadSize ||
|
||||||
|
entryHeader.payloadSize > fileHeader.payloadSize - entryHeader.payloadOffset) {
|
||||||
|
if (outErrorMessage != nullptr) {
|
||||||
|
*outErrorMessage = MakeError("ArtifactContainer entry payload range is invalid.");
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
ArtifactContainerEntryView& view = parsedEntries.EmplaceBack();
|
||||||
|
view.name = Containers::String(
|
||||||
|
reinterpret_cast<const char*>(directoryBytes.data() + cursor),
|
||||||
|
entryHeader.nameLength);
|
||||||
|
view.resourceType = static_cast<ResourceType>(entryHeader.resourceType);
|
||||||
|
view.localID = entryHeader.localID;
|
||||||
|
view.flags = entryHeader.flags;
|
||||||
|
view.compression = static_cast<ArtifactContainerCompression>(entryHeader.compression);
|
||||||
|
view.payloadOffset = entryHeader.payloadOffset;
|
||||||
|
view.payloadSize = entryHeader.payloadSize;
|
||||||
|
cursor += entryHeader.nameLength;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cursor != directoryBytes.size()) {
|
||||||
|
if (outErrorMessage != nullptr) {
|
||||||
|
*outErrorMessage = MakeError("ArtifactContainer directory has trailing garbage.");
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
IncrementalArtifactHasher hasher;
|
||||||
|
if (!directoryBytes.empty()) {
|
||||||
|
hasher.Append(directoryBytes.data(), directoryBytes.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
std::array<Core::uint8, 4096> buffer = {};
|
||||||
|
Core::uint64 remainingPayloadBytes = fileHeader.payloadSize;
|
||||||
|
while (remainingPayloadBytes > 0) {
|
||||||
|
const size_t chunkSize = static_cast<size_t>(std::min<Core::uint64>(
|
||||||
|
remainingPayloadBytes,
|
||||||
|
static_cast<Core::uint64>(buffer.size())));
|
||||||
|
input.read(reinterpret_cast<char*>(buffer.data()), static_cast<std::streamsize>(chunkSize));
|
||||||
|
if (!input) {
|
||||||
|
if (outErrorMessage != nullptr) {
|
||||||
|
*outErrorMessage = MakeError("Failed to hash ArtifactContainer payload.");
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
hasher.Append(buffer.data(), chunkSize);
|
||||||
|
remainingPayloadBytes -= static_cast<Core::uint64>(chunkSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
const AssetGUID expectedHash(fileHeader.contentHashHigh, fileHeader.contentHashLow);
|
||||||
|
const AssetGUID actualHash = hasher.Finish();
|
||||||
|
if (expectedHash != actualHash) {
|
||||||
|
if (outErrorMessage != nullptr) {
|
||||||
|
*outErrorMessage = MakeError("ArtifactContainer content hash mismatch.");
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_path = path;
|
||||||
|
m_entries = std::move(parsedEntries);
|
||||||
|
m_payloadStart =
|
||||||
|
static_cast<Core::uint64>(sizeof(ArtifactContainerFileHeader)) +
|
||||||
|
fileHeader.directorySize;
|
||||||
|
m_payloadSize = fileHeader.payloadSize;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ArtifactContainerReader::Close() {
|
||||||
|
m_path.Clear();
|
||||||
|
m_entries.Clear();
|
||||||
|
m_payloadStart = 0;
|
||||||
|
m_payloadSize = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ArtifactContainerEntryView* ArtifactContainerReader::FindEntryByName(
|
||||||
|
const Containers::String& name) const {
|
||||||
|
for (const ArtifactContainerEntryView& entry : m_entries) {
|
||||||
|
if (entry.name == name) {
|
||||||
|
return &entry;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ArtifactContainerEntryView* ArtifactContainerReader::FindEntry(
|
||||||
|
ResourceType resourceType,
|
||||||
|
LocalID localID) const {
|
||||||
|
for (const ArtifactContainerEntryView& entry : m_entries) {
|
||||||
|
if (entry.resourceType == resourceType && entry.localID == localID) {
|
||||||
|
return &entry;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ArtifactContainerReader::ReadEntryPayload(const ArtifactContainerEntryView& entry,
|
||||||
|
Containers::Array<Core::uint8>& outPayload,
|
||||||
|
Containers::String* outErrorMessage) const {
|
||||||
|
outPayload.Clear();
|
||||||
|
if (!IsOpen()) {
|
||||||
|
if (outErrorMessage != nullptr) {
|
||||||
|
*outErrorMessage = MakeError("ArtifactContainer is not open.");
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entry.payloadOffset > m_payloadSize ||
|
||||||
|
entry.payloadSize > m_payloadSize - entry.payloadOffset) {
|
||||||
|
if (outErrorMessage != nullptr) {
|
||||||
|
*outErrorMessage = MakeError("ArtifactContainer entry payload range is invalid.");
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entry.payloadSize == 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entry.payloadSize > static_cast<Core::uint64>(std::numeric_limits<size_t>::max())) {
|
||||||
|
if (outErrorMessage != nullptr) {
|
||||||
|
*outErrorMessage = MakeError("ArtifactContainer entry payload is too large.");
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::ifstream input(m_path.CStr(), std::ios::binary);
|
||||||
|
if (!input.is_open()) {
|
||||||
|
if (outErrorMessage != nullptr) {
|
||||||
|
*outErrorMessage = MakeError("Failed to reopen ArtifactContainer file.");
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
input.seekg(static_cast<std::streamoff>(m_payloadStart + entry.payloadOffset), std::ios::beg);
|
||||||
|
if (!input) {
|
||||||
|
if (outErrorMessage != nullptr) {
|
||||||
|
*outErrorMessage = MakeError("Failed to seek ArtifactContainer payload.");
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
outPayload.ResizeUninitialized(static_cast<size_t>(entry.payloadSize));
|
||||||
|
input.read(reinterpret_cast<char*>(outPayload.Data()),
|
||||||
|
static_cast<std::streamsize>(entry.payloadSize));
|
||||||
|
if (!input) {
|
||||||
|
outPayload.Clear();
|
||||||
|
if (outErrorMessage != nullptr) {
|
||||||
|
*outErrorMessage = MakeError("Failed to read ArtifactContainer payload.");
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ArtifactContainerReader::ReadEntryPayload(const Containers::String& name,
|
||||||
|
Containers::Array<Core::uint8>& outPayload,
|
||||||
|
Containers::String* outErrorMessage) const {
|
||||||
|
const ArtifactContainerEntryView* entry = FindEntryByName(name);
|
||||||
|
if (entry == nullptr) {
|
||||||
|
outPayload.Clear();
|
||||||
|
if (outErrorMessage != nullptr) {
|
||||||
|
*outErrorMessage = MakeError("ArtifactContainer entry was not found.");
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ReadEntryPayload(*entry, outPayload, outErrorMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ReadArtifactContainerEntryPayload(const Containers::String& containerPath,
|
||||||
|
const Containers::String& entryName,
|
||||||
|
ResourceType expectedType,
|
||||||
|
Containers::Array<Core::uint8>& outPayload,
|
||||||
|
Containers::String* outErrorMessage) {
|
||||||
|
outPayload.Clear();
|
||||||
|
|
||||||
|
ArtifactContainerReader reader;
|
||||||
|
if (!reader.Open(containerPath, outErrorMessage)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ArtifactContainerEntryView* entry = reader.FindEntryByName(entryName);
|
||||||
|
if (entry == nullptr) {
|
||||||
|
if (outErrorMessage != nullptr) {
|
||||||
|
*outErrorMessage = MakeError("ArtifactContainer entry was not found.");
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (expectedType != ResourceType::Unknown && entry->resourceType != expectedType) {
|
||||||
|
if (outErrorMessage != nullptr) {
|
||||||
|
*outErrorMessage = MakeError("ArtifactContainer entry resource type did not match.");
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return reader.ReadEntryPayload(*entry, outPayload, outErrorMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ReadArtifactContainerPayloadByPath(const Containers::String& path,
|
||||||
|
ResourceType expectedType,
|
||||||
|
Containers::Array<Core::uint8>& outPayload,
|
||||||
|
Containers::String* outErrorMessage) {
|
||||||
|
Containers::String containerPath;
|
||||||
|
Containers::String entryName;
|
||||||
|
if (TryParseArtifactContainerEntryPath(path, containerPath, entryName)) {
|
||||||
|
return ReadArtifactContainerEntryPayload(
|
||||||
|
containerPath,
|
||||||
|
entryName,
|
||||||
|
expectedType,
|
||||||
|
outPayload,
|
||||||
|
outErrorMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ReadArtifactContainerMainEntryPayload(path, expectedType, outPayload, outErrorMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ReadArtifactContainerMainEntryPayload(const Containers::String& path,
|
||||||
|
ResourceType expectedType,
|
||||||
|
Containers::Array<Core::uint8>& outPayload,
|
||||||
|
Containers::String* outErrorMessage) {
|
||||||
|
outPayload.Clear();
|
||||||
|
|
||||||
|
ArtifactContainerReader reader;
|
||||||
|
if (!reader.Open(path, outErrorMessage)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ArtifactContainerEntryView* entry = reader.FindEntry(expectedType, kMainAssetLocalID);
|
||||||
|
if (entry == nullptr) {
|
||||||
|
entry = reader.FindEntryByName("main");
|
||||||
|
}
|
||||||
|
if (entry == nullptr) {
|
||||||
|
if (outErrorMessage != nullptr) {
|
||||||
|
*outErrorMessage = MakeError("ArtifactContainer main entry was not found.");
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return reader.ReadEntryPayload(*entry, outPayload, outErrorMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace Resources
|
||||||
|
} // namespace XCEngine
|
||||||
@@ -75,6 +75,9 @@ bool SerializeMaterialArtifactPayload(
|
|||||||
const AssetDatabase* assetDatabase);
|
const AssetDatabase* assetDatabase);
|
||||||
bool SerializeShaderArtifactPayload(const Shader& shader,
|
bool SerializeShaderArtifactPayload(const Shader& shader,
|
||||||
Containers::Array<Core::uint8>& outPayload);
|
Containers::Array<Core::uint8>& outPayload);
|
||||||
|
bool SerializeMeshArtifactPayload(const Mesh& mesh,
|
||||||
|
const std::vector<Containers::String>& materialArtifactPaths,
|
||||||
|
Containers::Array<Core::uint8>& outPayload);
|
||||||
bool WriteSingleEntryArtifactContainerFile(const fs::path& artifactPath,
|
bool WriteSingleEntryArtifactContainerFile(const fs::path& artifactPath,
|
||||||
ResourceType resourceType,
|
ResourceType resourceType,
|
||||||
const Containers::Array<Core::uint8>& payload);
|
const Containers::Array<Core::uint8>& payload);
|
||||||
@@ -1225,11 +1228,28 @@ bool WriteShaderArtifactFile(const fs::path& artifactPath, const Shader& shader)
|
|||||||
bool WriteMeshArtifactFile(const fs::path& artifactPath,
|
bool WriteMeshArtifactFile(const fs::path& artifactPath,
|
||||||
const Mesh& mesh,
|
const Mesh& mesh,
|
||||||
const std::vector<Containers::String>& materialArtifactPaths) {
|
const std::vector<Containers::String>& materialArtifactPaths) {
|
||||||
|
Containers::Array<Core::uint8> payload;
|
||||||
|
if (!SerializeMeshArtifactPayload(mesh, materialArtifactPaths, payload)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
std::ofstream output(artifactPath, std::ios::binary | std::ios::trunc);
|
std::ofstream output(artifactPath, std::ios::binary | std::ios::trunc);
|
||||||
if (!output.is_open()) {
|
if (!output.is_open()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!payload.Empty()) {
|
||||||
|
output.write(reinterpret_cast<const char*>(payload.Data()), static_cast<std::streamsize>(payload.Size()));
|
||||||
|
}
|
||||||
|
|
||||||
|
return static_cast<bool>(output);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool SerializeMeshArtifactPayload(const Mesh& mesh,
|
||||||
|
const std::vector<Containers::String>& materialArtifactPaths,
|
||||||
|
Containers::Array<Core::uint8>& outPayload) {
|
||||||
|
std::ostringstream output(std::ios::binary | std::ios::out);
|
||||||
|
|
||||||
MeshArtifactHeader header;
|
MeshArtifactHeader header;
|
||||||
header.vertexCount = mesh.GetVertexCount();
|
header.vertexCount = mesh.GetVertexCount();
|
||||||
header.vertexStride = mesh.GetVertexStride();
|
header.vertexStride = mesh.GetVertexStride();
|
||||||
@@ -1259,7 +1279,13 @@ bool WriteMeshArtifactFile(const fs::path& artifactPath,
|
|||||||
WriteString(output, NormalizeArtifactPathString(materialArtifactPath));
|
WriteString(output, NormalizeArtifactPathString(materialArtifactPath));
|
||||||
}
|
}
|
||||||
|
|
||||||
return static_cast<bool>(output);
|
if (!output) {
|
||||||
|
outPayload.Clear();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
outPayload = ToByteArray(output.str());
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void DestroyImportedMesh(Mesh* mesh) {
|
void DestroyImportedMesh(Mesh* mesh) {
|
||||||
@@ -1438,6 +1464,20 @@ bool AssetDatabase::TryResolveAssetPath(const AssetRef& assetRef, Containers::St
|
|||||||
}
|
}
|
||||||
|
|
||||||
auto resolveFromArtifactRecord = [&](const ArtifactRecord& artifactRecord) -> bool {
|
auto resolveFromArtifactRecord = [&](const ArtifactRecord& artifactRecord) -> bool {
|
||||||
|
const Containers::String absoluteMainArtifactPath =
|
||||||
|
NormalizePathString(fs::path(m_projectRoot.CStr()) / artifactRecord.mainArtifactPath.CStr());
|
||||||
|
|
||||||
|
ArtifactContainerReader reader;
|
||||||
|
Containers::String containerError;
|
||||||
|
if (reader.Open(absoluteMainArtifactPath, &containerError)) {
|
||||||
|
const ArtifactContainerEntryView* entry =
|
||||||
|
reader.FindEntry(assetRef.resourceType, assetRef.localID);
|
||||||
|
if (entry != nullptr) {
|
||||||
|
outPath = BuildArtifactContainerEntryPath(absoluteMainArtifactPath, entry->name);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const fs::path manifestPath =
|
const fs::path manifestPath =
|
||||||
fs::path(m_projectRoot.CStr()) /
|
fs::path(m_projectRoot.CStr()) /
|
||||||
artifactRecord.artifactDirectory.CStr() /
|
artifactRecord.artifactDirectory.CStr() /
|
||||||
@@ -2170,7 +2210,7 @@ Core::uint32 AssetDatabase::GetCurrentImporterVersion(const Containers::String&
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (importerName == "ModelImporter") {
|
if (importerName == "ModelImporter") {
|
||||||
return 10;
|
return 12;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (importerName == "GaussianSplatImporter") {
|
if (importerName == "GaussianSplatImporter") {
|
||||||
@@ -2557,21 +2597,24 @@ bool AssetDatabase::ImportModelAsset(const SourceAssetRecord& sourceRecord,
|
|||||||
std::vector<ArtifactDependencyRecord> dependencies;
|
std::vector<ArtifactDependencyRecord> dependencies;
|
||||||
CollectModelDependencies(sourceRecord, importedTexturePaths, dependencies);
|
CollectModelDependencies(sourceRecord, importedTexturePaths, dependencies);
|
||||||
const Containers::String artifactKey = BuildArtifactKey(sourceRecord, dependencies);
|
const Containers::String artifactKey = BuildArtifactKey(sourceRecord, dependencies);
|
||||||
const Containers::String artifactDir = BuildArtifactDirectory(artifactKey);
|
const Containers::String legacyArtifactDir = BuildArtifactDirectory(artifactKey);
|
||||||
const Containers::String mainArtifactPath =
|
const Containers::String mainArtifactPath = BuildArtifactFilePath(artifactKey, ".xcmodel");
|
||||||
NormalizePathString(fs::path(artifactDir.CStr()) / "main.xcmodel");
|
const Containers::String artifactDir =
|
||||||
|
NormalizePathString(fs::path(mainArtifactPath.CStr()).parent_path());
|
||||||
|
|
||||||
std::error_code ec;
|
std::error_code ec;
|
||||||
fs::remove_all(fs::path(m_projectRoot.CStr()) / artifactDir.CStr(), ec);
|
fs::remove_all(fs::path(m_projectRoot.CStr()) / legacyArtifactDir.CStr(), ec);
|
||||||
ec.clear();
|
ec.clear();
|
||||||
fs::create_directories(fs::path(m_projectRoot.CStr()) / artifactDir.CStr(), ec);
|
fs::create_directories(
|
||||||
|
(fs::path(m_projectRoot.CStr()) / mainArtifactPath.CStr()).parent_path(),
|
||||||
|
ec);
|
||||||
if (ec) {
|
if (ec) {
|
||||||
importedModel.Reset();
|
importedModel.Reset();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ArtifactContainerWriter writer;
|
||||||
bool writeOk = true;
|
bool writeOk = true;
|
||||||
std::vector<ModelSubAssetManifestEntry> subAssetManifestEntries;
|
|
||||||
std::unordered_map<const Texture*, Containers::String> textureArtifactPaths;
|
std::unordered_map<const Texture*, Containers::String> textureArtifactPaths;
|
||||||
std::unordered_map<const Texture*, AssetRef> textureAssetRefs;
|
std::unordered_map<const Texture*, AssetRef> textureAssetRefs;
|
||||||
for (size_t textureIndex = 0; writeOk && textureIndex < importedModel.textures.size(); ++textureIndex) {
|
for (size_t textureIndex = 0; writeOk && textureIndex < importedModel.textures.size(); ++textureIndex) {
|
||||||
@@ -2580,16 +2623,23 @@ bool AssetDatabase::ImportModelAsset(const SourceAssetRecord& sourceRecord,
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Containers::String textureArtifactPath =
|
const Containers::String entryName =
|
||||||
NormalizePathString(fs::path(artifactDir.CStr()) / ("texture_" + std::to_string(textureIndex) + ".xctex"));
|
Containers::String(("texture_" + std::to_string(textureIndex) + ".xctex").c_str());
|
||||||
writeOk = WriteTextureArtifactFile(
|
Containers::Array<Core::uint8> payload;
|
||||||
fs::path(m_projectRoot.CStr()) / textureArtifactPath.CStr(),
|
writeOk = SerializeTextureArtifactPayload(*texture, payload);
|
||||||
*texture);
|
|
||||||
if (!writeOk) {
|
if (!writeOk) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
textureArtifactPaths.emplace(texture, textureArtifactPath);
|
ArtifactContainerEntry entry;
|
||||||
|
entry.name = entryName;
|
||||||
|
entry.resourceType = ResourceType::Texture;
|
||||||
|
entry.payload = std::move(payload);
|
||||||
|
writer.AddEntry(std::move(entry));
|
||||||
|
|
||||||
|
textureArtifactPaths.emplace(
|
||||||
|
texture,
|
||||||
|
BuildArtifactContainerEntryPath(mainArtifactPath, entryName));
|
||||||
|
|
||||||
if (!texture->GetPath().Empty()) {
|
if (!texture->GetPath().Empty()) {
|
||||||
AssetRef textureAssetRef;
|
AssetRef textureAssetRef;
|
||||||
@@ -2606,11 +2656,12 @@ bool AssetDatabase::ImportModelAsset(const SourceAssetRecord& sourceRecord,
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Containers::String materialArtifactPath =
|
const Containers::String entryName =
|
||||||
NormalizePathString(fs::path(artifactDir.CStr()) / ("material_" + std::to_string(materialIndex) + ".xcmat"));
|
Containers::String(("material_" + std::to_string(materialIndex) + ".xcmat").c_str());
|
||||||
writeOk = WriteMaterialArtifactFile(
|
Containers::Array<Core::uint8> payload;
|
||||||
fs::path(m_projectRoot.CStr()) / materialArtifactPath.CStr(),
|
writeOk = SerializeMaterialArtifactPayload(
|
||||||
*materialEntry.material,
|
*materialEntry.material,
|
||||||
|
payload,
|
||||||
textureArtifactPaths,
|
textureArtifactPaths,
|
||||||
textureAssetRefs,
|
textureAssetRefs,
|
||||||
this);
|
this);
|
||||||
@@ -2618,9 +2669,16 @@ bool AssetDatabase::ImportModelAsset(const SourceAssetRecord& sourceRecord,
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
materialArtifactPathsByLocalID.emplace(materialEntry.localID, materialArtifactPath);
|
ArtifactContainerEntry entry;
|
||||||
subAssetManifestEntries.push_back(
|
entry.name = entryName;
|
||||||
ModelSubAssetManifestEntry{ materialEntry.localID, ResourceType::Material, materialArtifactPath });
|
entry.resourceType = ResourceType::Material;
|
||||||
|
entry.localID = materialEntry.localID;
|
||||||
|
entry.payload = std::move(payload);
|
||||||
|
writer.AddEntry(std::move(entry));
|
||||||
|
|
||||||
|
materialArtifactPathsByLocalID.emplace(
|
||||||
|
materialEntry.localID,
|
||||||
|
BuildArtifactContainerEntryPath(mainArtifactPath, entryName));
|
||||||
}
|
}
|
||||||
|
|
||||||
for (size_t meshIndex = 0; writeOk && meshIndex < importedModel.meshes.size(); ++meshIndex) {
|
for (size_t meshIndex = 0; writeOk && meshIndex < importedModel.meshes.size(); ++meshIndex) {
|
||||||
@@ -2639,30 +2697,37 @@ bool AssetDatabase::ImportModelAsset(const SourceAssetRecord& sourceRecord,
|
|||||||
: Containers::String());
|
: Containers::String());
|
||||||
}
|
}
|
||||||
|
|
||||||
const Containers::String meshArtifactPath =
|
const Containers::String entryName =
|
||||||
NormalizePathString(fs::path(artifactDir.CStr()) / ("mesh_" + std::to_string(meshIndex) + ".xcmesh"));
|
Containers::String(("mesh_" + std::to_string(meshIndex) + ".xcmesh").c_str());
|
||||||
writeOk = WriteMeshArtifactFile(
|
Containers::Array<Core::uint8> payload;
|
||||||
fs::path(m_projectRoot.CStr()) / meshArtifactPath.CStr(),
|
writeOk = SerializeMeshArtifactPayload(*meshEntry.mesh, meshMaterialArtifactPaths, payload);
|
||||||
*meshEntry.mesh,
|
if (!writeOk) {
|
||||||
meshMaterialArtifactPaths);
|
break;
|
||||||
if (writeOk) {
|
|
||||||
subAssetManifestEntries.push_back(
|
|
||||||
ModelSubAssetManifestEntry{ meshEntry.localID, ResourceType::Mesh, meshArtifactPath });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ArtifactContainerEntry entry;
|
||||||
|
entry.name = entryName;
|
||||||
|
entry.resourceType = ResourceType::Mesh;
|
||||||
|
entry.localID = meshEntry.localID;
|
||||||
|
entry.payload = std::move(payload);
|
||||||
|
writer.AddEntry(std::move(entry));
|
||||||
}
|
}
|
||||||
|
|
||||||
Containers::String modelWriteErrorMessage;
|
Containers::String modelWriteErrorMessage;
|
||||||
if (writeOk) {
|
if (writeOk) {
|
||||||
writeOk = WriteModelArtifactFile(
|
Containers::Array<Core::uint8> modelPayload;
|
||||||
NormalizePathString(fs::path(m_projectRoot.CStr()) / mainArtifactPath.CStr()),
|
writeOk = SerializeModelArtifactPayload(*importedModel.model, modelPayload, &modelWriteErrorMessage);
|
||||||
*importedModel.model,
|
if (writeOk) {
|
||||||
&modelWriteErrorMessage);
|
ArtifactContainerEntry mainEntry;
|
||||||
}
|
mainEntry.name = "main";
|
||||||
|
mainEntry.resourceType = ResourceType::Model;
|
||||||
if (writeOk) {
|
mainEntry.localID = kMainAssetLocalID;
|
||||||
writeOk = WriteModelSubAssetManifest(
|
mainEntry.payload = std::move(modelPayload);
|
||||||
fs::path(m_projectRoot.CStr()) / artifactDir.CStr() / kModelSubAssetManifestFileName,
|
writer.AddEntry(std::move(mainEntry));
|
||||||
subAssetManifestEntries);
|
writeOk = writer.WriteToFile(
|
||||||
|
NormalizePathString(fs::path(m_projectRoot.CStr()) / mainArtifactPath.CStr()),
|
||||||
|
&modelWriteErrorMessage);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
importedModel.Reset();
|
importedModel.Reset();
|
||||||
@@ -2940,14 +3005,19 @@ bool AssetDatabase::ImportUIDocumentAsset(const SourceAssetRecord& sourceRecord,
|
|||||||
}
|
}
|
||||||
|
|
||||||
const Containers::String artifactKey = BuildArtifactKey(sourceRecord, dependencies);
|
const Containers::String artifactKey = BuildArtifactKey(sourceRecord, dependencies);
|
||||||
const Containers::String artifactDir = BuildArtifactDirectory(artifactKey);
|
const Containers::String legacyArtifactDir = BuildArtifactDirectory(artifactKey);
|
||||||
const Containers::String mainArtifactPath =
|
const Containers::String extension =
|
||||||
NormalizePathString(fs::path(artifactDir.CStr()) / artifactFileName);
|
Containers::String(fs::path(artifactFileName == nullptr ? "" : artifactFileName).extension().string().c_str());
|
||||||
|
const Containers::String mainArtifactPath = BuildArtifactFilePath(artifactKey, extension.CStr());
|
||||||
|
const Containers::String artifactDir =
|
||||||
|
NormalizePathString(fs::path(mainArtifactPath.CStr()).parent_path());
|
||||||
|
|
||||||
std::error_code ec;
|
std::error_code ec;
|
||||||
fs::remove_all(fs::path(m_projectRoot.CStr()) / artifactDir.CStr(), ec);
|
fs::remove_all(fs::path(m_projectRoot.CStr()) / legacyArtifactDir.CStr(), ec);
|
||||||
ec.clear();
|
ec.clear();
|
||||||
fs::create_directories(fs::path(m_projectRoot.CStr()) / artifactDir.CStr(), ec);
|
fs::create_directories(
|
||||||
|
(fs::path(m_projectRoot.CStr()) / mainArtifactPath.CStr()).parent_path(),
|
||||||
|
ec);
|
||||||
if (ec) {
|
if (ec) {
|
||||||
SetLastErrorMessage(
|
SetLastErrorMessage(
|
||||||
Containers::String("Failed to create UI artifact directory: ") + artifactDir);
|
Containers::String("Failed to create UI artifact directory: ") + artifactDir);
|
||||||
|
|||||||
@@ -106,6 +106,38 @@ RHI::InputLayoutDesc BuiltinForwardPipeline::BuildInputLayout() {
|
|||||||
texcoord.alignedByteOffset = static_cast<uint32_t>(offsetof(Resources::StaticMeshVertex, uv0));
|
texcoord.alignedByteOffset = static_cast<uint32_t>(offsetof(Resources::StaticMeshVertex, uv0));
|
||||||
inputLayout.elements.push_back(texcoord);
|
inputLayout.elements.push_back(texcoord);
|
||||||
|
|
||||||
|
RHI::InputElementDesc backTexcoord = {};
|
||||||
|
backTexcoord.semanticName = "TEXCOORD";
|
||||||
|
backTexcoord.semanticIndex = 1;
|
||||||
|
backTexcoord.format = static_cast<uint32_t>(RHI::Format::R32G32_Float);
|
||||||
|
backTexcoord.inputSlot = 0;
|
||||||
|
backTexcoord.alignedByteOffset = static_cast<uint32_t>(offsetof(Resources::StaticMeshVertex, uv1));
|
||||||
|
inputLayout.elements.push_back(backTexcoord);
|
||||||
|
|
||||||
|
RHI::InputElementDesc tangent = {};
|
||||||
|
tangent.semanticName = "TEXCOORD";
|
||||||
|
tangent.semanticIndex = 2;
|
||||||
|
tangent.format = static_cast<uint32_t>(RHI::Format::R32G32B32_Float);
|
||||||
|
tangent.inputSlot = 0;
|
||||||
|
tangent.alignedByteOffset = static_cast<uint32_t>(offsetof(Resources::StaticMeshVertex, tangent));
|
||||||
|
inputLayout.elements.push_back(tangent);
|
||||||
|
|
||||||
|
RHI::InputElementDesc bitangent = {};
|
||||||
|
bitangent.semanticName = "TEXCOORD";
|
||||||
|
bitangent.semanticIndex = 3;
|
||||||
|
bitangent.format = static_cast<uint32_t>(RHI::Format::R32G32B32_Float);
|
||||||
|
bitangent.inputSlot = 0;
|
||||||
|
bitangent.alignedByteOffset = static_cast<uint32_t>(offsetof(Resources::StaticMeshVertex, bitangent));
|
||||||
|
inputLayout.elements.push_back(bitangent);
|
||||||
|
|
||||||
|
RHI::InputElementDesc color = {};
|
||||||
|
color.semanticName = "COLOR";
|
||||||
|
color.semanticIndex = 0;
|
||||||
|
color.format = static_cast<uint32_t>(RHI::Format::R32G32B32A32_Float);
|
||||||
|
color.inputSlot = 0;
|
||||||
|
color.alignedByteOffset = static_cast<uint32_t>(offsetof(Resources::StaticMeshVertex, color));
|
||||||
|
inputLayout.elements.push_back(color);
|
||||||
|
|
||||||
return inputLayout;
|
return inputLayout;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -132,17 +164,26 @@ bool BuiltinForwardPipeline::Render(
|
|||||||
const RenderSurface& surface,
|
const RenderSurface& surface,
|
||||||
const RenderSceneData& sceneData) {
|
const RenderSceneData& sceneData) {
|
||||||
if (!Initialize(context)) {
|
if (!Initialize(context)) {
|
||||||
|
Debug::Logger::Get().Error(
|
||||||
|
Debug::LogCategory::Rendering,
|
||||||
|
"BuiltinForwardPipeline::Render failed: Initialize returned false");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (m_volumetricPass != nullptr &&
|
if (m_volumetricPass != nullptr &&
|
||||||
!sceneData.visibleVolumes.empty() &&
|
!sceneData.visibleVolumes.empty() &&
|
||||||
!m_volumetricPass->PrepareVolumeResources(context, sceneData)) {
|
!m_volumetricPass->PrepareVolumeResources(context, sceneData)) {
|
||||||
|
Debug::Logger::Get().Error(
|
||||||
|
Debug::LogCategory::Rendering,
|
||||||
|
"BuiltinForwardPipeline::Render failed: PrepareVolumeResources returned false");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (m_gaussianSplatPass != nullptr &&
|
if (m_gaussianSplatPass != nullptr &&
|
||||||
!sceneData.visibleGaussianSplats.empty() &&
|
!sceneData.visibleGaussianSplats.empty() &&
|
||||||
!m_gaussianSplatPass->PrepareGaussianSplatResources(context, sceneData)) {
|
!m_gaussianSplatPass->PrepareGaussianSplatResources(context, sceneData)) {
|
||||||
|
Debug::Logger::Get().Error(
|
||||||
|
Debug::LogCategory::Rendering,
|
||||||
|
"BuiltinForwardPipeline::Render failed: PrepareGaussianSplatResources returned false");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -156,6 +197,9 @@ bool BuiltinForwardPipeline::Render(
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (!BeginForwardScenePass(passContext)) {
|
if (!BeginForwardScenePass(passContext)) {
|
||||||
|
Debug::Logger::Get().Error(
|
||||||
|
Debug::LogCategory::Rendering,
|
||||||
|
"BuiltinForwardPipeline::Render failed: BeginForwardScenePass returned false");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -167,15 +211,35 @@ bool BuiltinForwardPipeline::Render(
|
|||||||
bool renderResult = ExecuteForwardOpaquePass(passContext);
|
bool renderResult = ExecuteForwardOpaquePass(passContext);
|
||||||
if (renderResult) {
|
if (renderResult) {
|
||||||
renderResult = ExecuteForwardSkyboxPass(passContext);
|
renderResult = ExecuteForwardSkyboxPass(passContext);
|
||||||
|
if (!renderResult) {
|
||||||
|
Debug::Logger::Get().Error(
|
||||||
|
Debug::LogCategory::Rendering,
|
||||||
|
"BuiltinForwardPipeline::Render failed: ExecuteForwardSkyboxPass returned false");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (renderResult && m_gaussianSplatPass != nullptr) {
|
if (renderResult && m_gaussianSplatPass != nullptr) {
|
||||||
renderResult = m_gaussianSplatPass->Execute(passContext);
|
renderResult = m_gaussianSplatPass->Execute(passContext);
|
||||||
|
if (!renderResult) {
|
||||||
|
Debug::Logger::Get().Error(
|
||||||
|
Debug::LogCategory::Rendering,
|
||||||
|
"BuiltinForwardPipeline::Render failed: BuiltinGaussianSplatPass::Execute returned false");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (renderResult && m_volumetricPass != nullptr) {
|
if (renderResult && m_volumetricPass != nullptr) {
|
||||||
renderResult = m_volumetricPass->Execute(passContext);
|
renderResult = m_volumetricPass->Execute(passContext);
|
||||||
|
if (!renderResult) {
|
||||||
|
Debug::Logger::Get().Error(
|
||||||
|
Debug::LogCategory::Rendering,
|
||||||
|
"BuiltinForwardPipeline::Render failed: BuiltinVolumetricPass::Execute returned false");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (renderResult) {
|
if (renderResult) {
|
||||||
renderResult = ExecuteForwardTransparentPass(passContext);
|
renderResult = ExecuteForwardTransparentPass(passContext);
|
||||||
|
if (!renderResult) {
|
||||||
|
Debug::Logger::Get().Error(
|
||||||
|
Debug::LogCategory::Rendering,
|
||||||
|
"BuiltinForwardPipeline::Render failed: ExecuteForwardTransparentPass returned false");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sampledDirectionalShadow) {
|
if (sampledDirectionalShadow) {
|
||||||
|
|||||||
@@ -438,6 +438,37 @@ BuiltinForwardPipeline::CachedDescriptorSet* BuiltinForwardPipeline::GetOrCreate
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (setLayout.usesMaterialTextures) {
|
||||||
|
if (material == nullptr) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cachedDescriptorSet.materialTextureViews.size() != setLayout.materialTextureBindings.size()) {
|
||||||
|
cachedDescriptorSet.materialTextureViews.assign(
|
||||||
|
setLayout.materialTextureBindings.size(),
|
||||||
|
nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (size_t bindingIndex = 0; bindingIndex < setLayout.materialTextureBindings.size(); ++bindingIndex) {
|
||||||
|
const BuiltinPassResourceBindingDesc& textureBinding =
|
||||||
|
setLayout.materialTextureBindings[bindingIndex];
|
||||||
|
RHI::RHIResourceView* resolvedTextureView =
|
||||||
|
ResolveMaterialTextureView(material, textureBinding);
|
||||||
|
if (resolvedTextureView == nullptr) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cachedDescriptorSet.materialTextureViews[bindingIndex] != resolvedTextureView) {
|
||||||
|
cachedDescriptorSet.descriptorSet.set->Update(
|
||||||
|
textureBinding.location.binding,
|
||||||
|
resolvedTextureView);
|
||||||
|
cachedDescriptorSet.materialTextureViews[bindingIndex] = resolvedTextureView;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (!cachedDescriptorSet.materialTextureViews.empty()) {
|
||||||
|
cachedDescriptorSet.materialTextureViews.clear();
|
||||||
|
}
|
||||||
|
|
||||||
if (setLayout.usesLighting) {
|
if (setLayout.usesLighting) {
|
||||||
if (!passLayout.lighting.IsValid() || passLayout.lighting.set != setIndex) {
|
if (!passLayout.lighting.IsValid() || passLayout.lighting.set != setIndex) {
|
||||||
return nullptr;
|
return nullptr;
|
||||||
@@ -556,6 +587,33 @@ RHI::RHIResourceView* BuiltinForwardPipeline::ResolveTextureView(
|
|||||||
return textureView != nullptr ? textureView : m_fallbackTexture2DView;
|
return textureView != nullptr ? textureView : m_fallbackTexture2DView;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
RHI::RHIResourceView* BuiltinForwardPipeline::ResolveMaterialTextureView(
|
||||||
|
const Resources::Material* material,
|
||||||
|
const BuiltinPassResourceBindingDesc& binding) {
|
||||||
|
const bool expectsCubemap = binding.resourceType == Resources::ShaderResourceType::TextureCube;
|
||||||
|
RHI::RHIResourceView* fallbackView = expectsCubemap ? m_fallbackTextureCubeView : m_fallbackTexture2DView;
|
||||||
|
if (material == nullptr) {
|
||||||
|
return fallbackView;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Resources::ResourceHandle<Resources::Texture> textureHandle = material->GetTexture(binding.name);
|
||||||
|
const Resources::Texture* texture = textureHandle.Get();
|
||||||
|
if (texture == nullptr) {
|
||||||
|
return fallbackView;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Resources::TextureType textureType = texture->GetTextureType();
|
||||||
|
const bool isCubemap =
|
||||||
|
textureType == Resources::TextureType::TextureCube ||
|
||||||
|
textureType == Resources::TextureType::TextureCubeArray;
|
||||||
|
if (expectsCubemap != isCubemap) {
|
||||||
|
return fallbackView;
|
||||||
|
}
|
||||||
|
|
||||||
|
RHI::RHIResourceView* textureView = ResolveTextureView(texture);
|
||||||
|
return textureView != nullptr ? textureView : fallbackView;
|
||||||
|
}
|
||||||
|
|
||||||
BuiltinForwardPipeline::LightingConstants BuiltinForwardPipeline::BuildLightingConstants(
|
BuiltinForwardPipeline::LightingConstants BuiltinForwardPipeline::BuildLightingConstants(
|
||||||
const RenderLightingData& lightingData) {
|
const RenderLightingData& lightingData) {
|
||||||
LightingConstants lightingConstants = {};
|
LightingConstants lightingConstants = {};
|
||||||
@@ -664,7 +722,7 @@ bool BuiltinForwardPipeline::DrawVisibleItem(
|
|||||||
? sceneData.lighting.mainDirectionalShadow.shadowParams
|
? sceneData.lighting.mainDirectionalShadow.shadowParams
|
||||||
: Math::Vector4::Zero(),
|
: Math::Vector4::Zero(),
|
||||||
sceneData.lighting.HasMainDirectionalShadow()
|
sceneData.lighting.HasMainDirectionalShadow()
|
||||||
? Math::Vector4(1.0f, 0.0f, 0.0f, 0.0f)
|
? sceneData.lighting.mainDirectionalShadow.shadowOptions
|
||||||
: Math::Vector4::Zero()
|
: Math::Vector4::Zero()
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -730,7 +788,8 @@ bool BuiltinForwardPipeline::DrawVisibleItem(
|
|||||||
const Resources::Material* materialKey =
|
const Resources::Material* materialKey =
|
||||||
(setLayout.usesMaterial ||
|
(setLayout.usesMaterial ||
|
||||||
setLayout.usesBaseColorTexture ||
|
setLayout.usesBaseColorTexture ||
|
||||||
setLayout.usesMaterialBuffers)
|
setLayout.usesMaterialBuffers ||
|
||||||
|
setLayout.usesMaterialTextures)
|
||||||
? material
|
? material
|
||||||
: nullptr;
|
: nullptr;
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
#include <XCEngine/Resources/Mesh/MeshLoader.h>
|
#include <XCEngine/Resources/Mesh/MeshLoader.h>
|
||||||
|
#include <XCEngine/Core/Asset/ArtifactContainer.h>
|
||||||
#include <XCEngine/Core/Asset/ArtifactFormats.h>
|
#include <XCEngine/Core/Asset/ArtifactFormats.h>
|
||||||
#include <XCEngine/Resources/BuiltinResources.h>
|
#include <XCEngine/Resources/BuiltinResources.h>
|
||||||
#include <XCEngine/Resources/Material/MaterialLoader.h>
|
#include <XCEngine/Resources/Material/MaterialLoader.h>
|
||||||
@@ -229,12 +230,27 @@ Containers::String ResolveArtifactDependencyPath(const Containers::String& depen
|
|||||||
}
|
}
|
||||||
|
|
||||||
std::filesystem::path dependencyFsPath(dependencyPath.CStr());
|
std::filesystem::path dependencyFsPath(dependencyPath.CStr());
|
||||||
|
Containers::String containerPathText;
|
||||||
|
Containers::String entryName;
|
||||||
|
const bool isContainerEntryPath =
|
||||||
|
TryParseArtifactContainerEntryPath(dependencyPath, containerPathText, entryName);
|
||||||
|
if (isContainerEntryPath) {
|
||||||
|
dependencyFsPath = std::filesystem::path(containerPathText.CStr());
|
||||||
|
}
|
||||||
|
|
||||||
|
auto rebuildResolvedPath = [&entryName, isContainerEntryPath](const std::filesystem::path& resolvedPath) {
|
||||||
|
const Containers::String normalizedPath = NormalizePathString(resolvedPath);
|
||||||
|
return isContainerEntryPath
|
||||||
|
? BuildArtifactContainerEntryPath(normalizedPath, entryName)
|
||||||
|
: normalizedPath;
|
||||||
|
};
|
||||||
|
|
||||||
if (dependencyFsPath.is_absolute() && std::filesystem::exists(dependencyFsPath)) {
|
if (dependencyFsPath.is_absolute() && std::filesystem::exists(dependencyFsPath)) {
|
||||||
return NormalizePathString(dependencyFsPath);
|
return rebuildResolvedPath(dependencyFsPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (std::filesystem::exists(dependencyFsPath)) {
|
if (std::filesystem::exists(dependencyFsPath)) {
|
||||||
return NormalizePathString(dependencyFsPath);
|
return rebuildResolvedPath(dependencyFsPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
const Containers::String& resourceRoot = ResourceManager::Get().GetResourceRoot();
|
const Containers::String& resourceRoot = ResourceManager::Get().GetResourceRoot();
|
||||||
@@ -242,7 +258,7 @@ Containers::String ResolveArtifactDependencyPath(const Containers::String& depen
|
|||||||
const std::filesystem::path projectRelativeCandidate =
|
const std::filesystem::path projectRelativeCandidate =
|
||||||
std::filesystem::path(resourceRoot.CStr()) / dependencyFsPath;
|
std::filesystem::path(resourceRoot.CStr()) / dependencyFsPath;
|
||||||
if (std::filesystem::exists(projectRelativeCandidate)) {
|
if (std::filesystem::exists(projectRelativeCandidate)) {
|
||||||
return NormalizePathString(projectRelativeCandidate);
|
return rebuildResolvedPath(projectRelativeCandidate);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -254,7 +270,7 @@ Containers::String ResolveArtifactDependencyPath(const Containers::String& depen
|
|||||||
const std::filesystem::path ownerRelativeCandidate =
|
const std::filesystem::path ownerRelativeCandidate =
|
||||||
ownerArtifactFsPath.parent_path() / dependencyFsPath;
|
ownerArtifactFsPath.parent_path() / dependencyFsPath;
|
||||||
if (std::filesystem::exists(ownerRelativeCandidate)) {
|
if (std::filesystem::exists(ownerRelativeCandidate)) {
|
||||||
return NormalizePathString(ownerRelativeCandidate);
|
return rebuildResolvedPath(ownerRelativeCandidate);
|
||||||
}
|
}
|
||||||
|
|
||||||
std::filesystem::path current = ownerArtifactFsPath.parent_path();
|
std::filesystem::path current = ownerArtifactFsPath.parent_path();
|
||||||
@@ -264,7 +280,7 @@ Containers::String ResolveArtifactDependencyPath(const Containers::String& depen
|
|||||||
if (!projectRoot.empty()) {
|
if (!projectRoot.empty()) {
|
||||||
const std::filesystem::path projectRelativeCandidate = projectRoot / dependencyFsPath;
|
const std::filesystem::path projectRelativeCandidate = projectRoot / dependencyFsPath;
|
||||||
if (std::filesystem::exists(projectRelativeCandidate)) {
|
if (std::filesystem::exists(projectRelativeCandidate)) {
|
||||||
return NormalizePathString(projectRelativeCandidate);
|
return rebuildResolvedPath(projectRelativeCandidate);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@@ -675,42 +691,66 @@ ImportedMeshData ImportSingleMesh(const aiMesh& mesh,
|
|||||||
result.vertices.reserve(mesh.mNumVertices);
|
result.vertices.reserve(mesh.mNumVertices);
|
||||||
result.indices.reserve(mesh.mNumFaces * 3);
|
result.indices.reserve(mesh.mNumFaces * 3);
|
||||||
|
|
||||||
|
const bool hasNormals = mesh.HasNormals();
|
||||||
|
const bool hasTangentsAndBitangents = mesh.HasTangentsAndBitangents();
|
||||||
|
const bool hasUv0 = mesh.HasTextureCoords(0);
|
||||||
|
const bool hasUv1 = mesh.HasTextureCoords(1);
|
||||||
|
const bool hasVertexColors = mesh.HasVertexColors(0);
|
||||||
|
const bool useFallbackUv1 = hasUv0 && !hasUv1;
|
||||||
|
|
||||||
VertexAttribute attributes = VertexAttribute::Position;
|
VertexAttribute attributes = VertexAttribute::Position;
|
||||||
if (mesh.HasNormals()) {
|
if (hasNormals) {
|
||||||
attributes = attributes | VertexAttribute::Normal;
|
attributes = attributes | VertexAttribute::Normal;
|
||||||
}
|
}
|
||||||
if (mesh.HasTangentsAndBitangents()) {
|
if (hasTangentsAndBitangents) {
|
||||||
attributes = attributes | VertexAttribute::Tangent | VertexAttribute::Bitangent;
|
attributes = attributes | VertexAttribute::Tangent | VertexAttribute::Bitangent;
|
||||||
}
|
}
|
||||||
if (mesh.HasTextureCoords(0)) {
|
if (hasUv0) {
|
||||||
attributes = attributes | VertexAttribute::UV0;
|
attributes = attributes | VertexAttribute::UV0;
|
||||||
}
|
}
|
||||||
|
if (hasUv1 || useFallbackUv1) {
|
||||||
|
attributes = attributes | VertexAttribute::UV1;
|
||||||
|
}
|
||||||
|
if (hasVertexColors) {
|
||||||
|
attributes = attributes | VertexAttribute::Color;
|
||||||
|
}
|
||||||
|
|
||||||
const Math::Matrix4 normalTransform = worldTransform.Inverse().Transpose();
|
const Math::Matrix4 normalTransform = worldTransform.Inverse().Transpose();
|
||||||
const float appliedScale = globalScale;
|
const float appliedScale = globalScale;
|
||||||
|
|
||||||
for (Core::uint32 vertexIndex = 0; vertexIndex < mesh.mNumVertices; ++vertexIndex) {
|
for (Core::uint32 vertexIndex = 0; vertexIndex < mesh.mNumVertices; ++vertexIndex) {
|
||||||
StaticMeshVertex vertex;
|
StaticMeshVertex vertex = {};
|
||||||
|
|
||||||
const aiVector3D& position = mesh.mVertices[vertexIndex];
|
const aiVector3D& position = mesh.mVertices[vertexIndex];
|
||||||
const Math::Vector3 transformedPosition = worldTransform.MultiplyPoint(Math::Vector3(position.x, position.y, position.z));
|
const Math::Vector3 transformedPosition = worldTransform.MultiplyPoint(Math::Vector3(position.x, position.y, position.z));
|
||||||
vertex.position = transformedPosition * appliedScale + offset;
|
vertex.position = transformedPosition * appliedScale + offset;
|
||||||
|
|
||||||
if (mesh.HasNormals()) {
|
if (hasNormals) {
|
||||||
const aiVector3D& normal = mesh.mNormals[vertexIndex];
|
const aiVector3D& normal = mesh.mNormals[vertexIndex];
|
||||||
vertex.normal = normalTransform.MultiplyVector(Math::Vector3(normal.x, normal.y, normal.z)).Normalized();
|
vertex.normal = normalTransform.MultiplyVector(Math::Vector3(normal.x, normal.y, normal.z)).Normalized();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mesh.HasTangentsAndBitangents()) {
|
if (hasTangentsAndBitangents) {
|
||||||
const aiVector3D& tangent = mesh.mTangents[vertexIndex];
|
const aiVector3D& tangent = mesh.mTangents[vertexIndex];
|
||||||
const aiVector3D& bitangent = mesh.mBitangents[vertexIndex];
|
const aiVector3D& bitangent = mesh.mBitangents[vertexIndex];
|
||||||
vertex.tangent = normalTransform.MultiplyVector(Math::Vector3(tangent.x, tangent.y, tangent.z)).Normalized();
|
vertex.tangent = normalTransform.MultiplyVector(Math::Vector3(tangent.x, tangent.y, tangent.z)).Normalized();
|
||||||
vertex.bitangent = normalTransform.MultiplyVector(Math::Vector3(bitangent.x, bitangent.y, bitangent.z)).Normalized();
|
vertex.bitangent = normalTransform.MultiplyVector(Math::Vector3(bitangent.x, bitangent.y, bitangent.z)).Normalized();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mesh.HasTextureCoords(0)) {
|
if (hasUv0) {
|
||||||
const aiVector3D& uv = mesh.mTextureCoords[0][vertexIndex];
|
const aiVector3D& uv = mesh.mTextureCoords[0][vertexIndex];
|
||||||
vertex.uv0 = Math::Vector2(uv.x, uv.y);
|
vertex.uv0 = Math::Vector2(uv.x, uv.y);
|
||||||
|
vertex.uv1 = vertex.uv0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasUv1) {
|
||||||
|
const aiVector3D& uv = mesh.mTextureCoords[1][vertexIndex];
|
||||||
|
vertex.uv1 = Math::Vector2(uv.x, uv.y);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasVertexColors) {
|
||||||
|
const aiColor4D& color = mesh.mColors[0][vertexIndex];
|
||||||
|
vertex.color = Math::Vector4(color.r, color.g, color.b, color.a);
|
||||||
}
|
}
|
||||||
|
|
||||||
result.vertices.push_back(vertex);
|
result.vertices.push_back(vertex);
|
||||||
@@ -831,14 +871,72 @@ void ApplyMaterialProperty(Material& material, const MaterialProperty& property)
|
|||||||
}
|
}
|
||||||
|
|
||||||
LoadResult LoadMeshArtifact(const Containers::String& path) {
|
LoadResult LoadMeshArtifact(const Containers::String& path) {
|
||||||
std::ifstream input(path.CStr(), std::ios::binary);
|
Containers::Array<Core::uint8> data;
|
||||||
if (!input.is_open()) {
|
|
||||||
|
auto tryRead = [&data](const std::filesystem::path& filePath, bool& opened) {
|
||||||
|
opened = false;
|
||||||
|
data.Clear();
|
||||||
|
|
||||||
|
Containers::String containerError;
|
||||||
|
if (ReadArtifactContainerPayloadByPath(
|
||||||
|
Containers::String(filePath.generic_string().c_str()),
|
||||||
|
ResourceType::Mesh,
|
||||||
|
data,
|
||||||
|
&containerError)) {
|
||||||
|
opened = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::ifstream input(filePath, std::ios::binary | std::ios::ate);
|
||||||
|
if (!input.is_open()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
opened = true;
|
||||||
|
const std::streamsize size = input.tellg();
|
||||||
|
if (size < 0) {
|
||||||
|
data.Clear();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
input.seekg(0, std::ios::beg);
|
||||||
|
data.Resize(static_cast<size_t>(size));
|
||||||
|
if (size > 0 &&
|
||||||
|
!input.read(reinterpret_cast<char*>(data.Data()), size)) {
|
||||||
|
data.Clear();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
bool opened = false;
|
||||||
|
std::filesystem::path resolvedPath(path.CStr());
|
||||||
|
tryRead(resolvedPath, opened);
|
||||||
|
if (!opened && !resolvedPath.is_absolute()) {
|
||||||
|
const Containers::String& resourceRoot = ResourceManager::Get().GetResourceRoot();
|
||||||
|
if (!resourceRoot.Empty()) {
|
||||||
|
resolvedPath = std::filesystem::path(resourceRoot.CStr()) / resolvedPath;
|
||||||
|
tryRead(resolvedPath, opened);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!opened || data.Size() < sizeof(MeshArtifactHeader)) {
|
||||||
return LoadResult(Containers::String("Failed to read mesh artifact: ") + path);
|
return LoadResult(Containers::String("Failed to read mesh artifact: ") + path);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
size_t offset = 0;
|
||||||
|
auto readBytes = [&data, &offset](void* destination, size_t byteCount) -> bool {
|
||||||
|
if (offset + byteCount > data.Size()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (byteCount > 0) {
|
||||||
|
std::memcpy(destination, data.Data() + offset, byteCount);
|
||||||
|
offset += byteCount;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
MeshArtifactHeader header;
|
MeshArtifactHeader header;
|
||||||
input.read(reinterpret_cast<char*>(&header), sizeof(header));
|
if (!readBytes(&header, sizeof(header))) {
|
||||||
if (!input) {
|
|
||||||
return LoadResult(Containers::String("Failed to parse mesh artifact header: ") + path);
|
return LoadResult(Containers::String("Failed to parse mesh artifact header: ") + path);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -859,28 +957,29 @@ LoadResult LoadMeshArtifact(const Containers::String& path) {
|
|||||||
Containers::Array<MeshSection> sections;
|
Containers::Array<MeshSection> sections;
|
||||||
sections.Resize(header.sectionCount);
|
sections.Resize(header.sectionCount);
|
||||||
for (Core::uint32 index = 0; index < header.sectionCount; ++index) {
|
for (Core::uint32 index = 0; index < header.sectionCount; ++index) {
|
||||||
input.read(reinterpret_cast<char*>(§ions[index]), sizeof(MeshSection));
|
if (!readBytes(§ions[index], sizeof(MeshSection))) {
|
||||||
if (!input) {
|
|
||||||
return LoadResult(Containers::String("Failed to read mesh sections: ") + path);
|
return LoadResult(Containers::String("Failed to read mesh sections: ") + path);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Containers::Array<Core::uint8> vertexData;
|
Containers::Array<Core::uint8> vertexData;
|
||||||
vertexData.Resize(static_cast<size_t>(header.vertexDataSize));
|
vertexData.Resize(static_cast<size_t>(header.vertexDataSize));
|
||||||
if (header.vertexDataSize > 0) {
|
if (header.vertexDataSize > static_cast<Core::uint64>(data.Size() - offset)) {
|
||||||
input.read(reinterpret_cast<char*>(vertexData.Data()), static_cast<std::streamsize>(header.vertexDataSize));
|
return LoadResult(Containers::String("Failed to read mesh vertex data: ") + path);
|
||||||
if (!input) {
|
}
|
||||||
return LoadResult(Containers::String("Failed to read mesh vertex data: ") + path);
|
if (header.vertexDataSize > 0 &&
|
||||||
}
|
!readBytes(vertexData.Data(), static_cast<size_t>(header.vertexDataSize))) {
|
||||||
|
return LoadResult(Containers::String("Failed to read mesh vertex data: ") + path);
|
||||||
}
|
}
|
||||||
|
|
||||||
Containers::Array<Core::uint8> indexData;
|
Containers::Array<Core::uint8> indexData;
|
||||||
indexData.Resize(static_cast<size_t>(header.indexDataSize));
|
indexData.Resize(static_cast<size_t>(header.indexDataSize));
|
||||||
if (header.indexDataSize > 0) {
|
if (header.indexDataSize > static_cast<Core::uint64>(data.Size() - offset)) {
|
||||||
input.read(reinterpret_cast<char*>(indexData.Data()), static_cast<std::streamsize>(header.indexDataSize));
|
return LoadResult(Containers::String("Failed to read mesh index data: ") + path);
|
||||||
if (!input) {
|
}
|
||||||
return LoadResult(Containers::String("Failed to read mesh index data: ") + path);
|
if (header.indexDataSize > 0 &&
|
||||||
}
|
!readBytes(indexData.Data(), static_cast<size_t>(header.indexDataSize))) {
|
||||||
|
return LoadResult(Containers::String("Failed to read mesh index data: ") + path);
|
||||||
}
|
}
|
||||||
|
|
||||||
mesh->SetVertexData(vertexData.Data(),
|
mesh->SetVertexData(vertexData.Data(),
|
||||||
@@ -901,11 +1000,31 @@ LoadResult LoadMeshArtifact(const Containers::String& path) {
|
|||||||
bounds.SetMinMax(header.boundsMin, header.boundsMax);
|
bounds.SetMinMax(header.boundsMin, header.boundsMax);
|
||||||
mesh->SetBounds(bounds);
|
mesh->SetBounds(bounds);
|
||||||
|
|
||||||
|
auto readBinaryString = [&data, &offset, &readBytes](Containers::String& outValue) -> bool {
|
||||||
|
Core::uint32 length = 0;
|
||||||
|
if (!readBytes(&length, sizeof(length))) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (length == 0) {
|
||||||
|
outValue.Clear();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (offset + length > data.Size()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
outValue = Containers::String(reinterpret_cast<const char*>(data.Data() + offset), length);
|
||||||
|
offset += length;
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
MaterialLoader materialLoader;
|
MaterialLoader materialLoader;
|
||||||
|
|
||||||
for (Core::uint32 materialIndex = 0; materialIndex < header.materialCount; ++materialIndex) {
|
for (Core::uint32 materialIndex = 0; materialIndex < header.materialCount; ++materialIndex) {
|
||||||
const Containers::String materialArtifactPath = ReadBinaryString(input);
|
Containers::String materialArtifactPath;
|
||||||
if (!input) {
|
if (!readBinaryString(materialArtifactPath)) {
|
||||||
return LoadResult(Containers::String("Failed to read mesh material artifact path: ") + path);
|
return LoadResult(Containers::String("Failed to read mesh material artifact path: ") + path);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -640,37 +640,61 @@ ImportedMeshData ImportSingleMesh(const aiMesh& mesh) {
|
|||||||
result.vertices.reserve(mesh.mNumVertices);
|
result.vertices.reserve(mesh.mNumVertices);
|
||||||
result.indices.reserve(mesh.mNumFaces * 3);
|
result.indices.reserve(mesh.mNumFaces * 3);
|
||||||
|
|
||||||
|
const bool hasNormals = mesh.HasNormals();
|
||||||
|
const bool hasTangentsAndBitangents = mesh.HasTangentsAndBitangents();
|
||||||
|
const bool hasUv0 = mesh.HasTextureCoords(0);
|
||||||
|
const bool hasUv1 = mesh.HasTextureCoords(1);
|
||||||
|
const bool hasVertexColors = mesh.HasVertexColors(0);
|
||||||
|
const bool useFallbackUv1 = hasUv0 && !hasUv1;
|
||||||
|
|
||||||
VertexAttribute attributes = VertexAttribute::Position;
|
VertexAttribute attributes = VertexAttribute::Position;
|
||||||
if (mesh.HasNormals()) {
|
if (hasNormals) {
|
||||||
attributes = attributes | VertexAttribute::Normal;
|
attributes = attributes | VertexAttribute::Normal;
|
||||||
}
|
}
|
||||||
if (mesh.HasTangentsAndBitangents()) {
|
if (hasTangentsAndBitangents) {
|
||||||
attributes = attributes | VertexAttribute::Tangent | VertexAttribute::Bitangent;
|
attributes = attributes | VertexAttribute::Tangent | VertexAttribute::Bitangent;
|
||||||
}
|
}
|
||||||
if (mesh.HasTextureCoords(0)) {
|
if (hasUv0) {
|
||||||
attributes = attributes | VertexAttribute::UV0;
|
attributes = attributes | VertexAttribute::UV0;
|
||||||
}
|
}
|
||||||
|
if (hasUv1 || useFallbackUv1) {
|
||||||
|
attributes = attributes | VertexAttribute::UV1;
|
||||||
|
}
|
||||||
|
if (hasVertexColors) {
|
||||||
|
attributes = attributes | VertexAttribute::Color;
|
||||||
|
}
|
||||||
|
|
||||||
for (Core::uint32 vertexIndex = 0; vertexIndex < mesh.mNumVertices; ++vertexIndex) {
|
for (Core::uint32 vertexIndex = 0; vertexIndex < mesh.mNumVertices; ++vertexIndex) {
|
||||||
StaticMeshVertex vertex;
|
StaticMeshVertex vertex = {};
|
||||||
const aiVector3D& position = mesh.mVertices[vertexIndex];
|
const aiVector3D& position = mesh.mVertices[vertexIndex];
|
||||||
vertex.position = Math::Vector3(position.x, position.y, position.z);
|
vertex.position = Math::Vector3(position.x, position.y, position.z);
|
||||||
|
|
||||||
if (mesh.HasNormals()) {
|
if (hasNormals) {
|
||||||
const aiVector3D& normal = mesh.mNormals[vertexIndex];
|
const aiVector3D& normal = mesh.mNormals[vertexIndex];
|
||||||
vertex.normal = Math::Vector3(normal.x, normal.y, normal.z).Normalized();
|
vertex.normal = Math::Vector3(normal.x, normal.y, normal.z).Normalized();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mesh.HasTangentsAndBitangents()) {
|
if (hasTangentsAndBitangents) {
|
||||||
const aiVector3D& tangent = mesh.mTangents[vertexIndex];
|
const aiVector3D& tangent = mesh.mTangents[vertexIndex];
|
||||||
const aiVector3D& bitangent = mesh.mBitangents[vertexIndex];
|
const aiVector3D& bitangent = mesh.mBitangents[vertexIndex];
|
||||||
vertex.tangent = Math::Vector3(tangent.x, tangent.y, tangent.z).Normalized();
|
vertex.tangent = Math::Vector3(tangent.x, tangent.y, tangent.z).Normalized();
|
||||||
vertex.bitangent = Math::Vector3(bitangent.x, bitangent.y, bitangent.z).Normalized();
|
vertex.bitangent = Math::Vector3(bitangent.x, bitangent.y, bitangent.z).Normalized();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mesh.HasTextureCoords(0)) {
|
if (hasUv0) {
|
||||||
const aiVector3D& uv = mesh.mTextureCoords[0][vertexIndex];
|
const aiVector3D& uv = mesh.mTextureCoords[0][vertexIndex];
|
||||||
vertex.uv0 = Math::Vector2(uv.x, uv.y);
|
vertex.uv0 = Math::Vector2(uv.x, uv.y);
|
||||||
|
vertex.uv1 = vertex.uv0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasUv1) {
|
||||||
|
const aiVector3D& uv = mesh.mTextureCoords[1][vertexIndex];
|
||||||
|
vertex.uv1 = Math::Vector2(uv.x, uv.y);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasVertexColors) {
|
||||||
|
const aiColor4D& color = mesh.mColors[0][vertexIndex];
|
||||||
|
vertex.color = Math::Vector4(color.r, color.g, color.b, color.a);
|
||||||
}
|
}
|
||||||
|
|
||||||
result.vertices.push_back(vertex);
|
result.vertices.push_back(vertex);
|
||||||
|
|||||||
228
engine/src/Scene/ModelSceneInstantiation.cpp
Normal file
@@ -0,0 +1,228 @@
|
|||||||
|
#include <XCEngine/Scene/ModelSceneInstantiation.h>
|
||||||
|
|
||||||
|
#include <XCEngine/Components/GameObject.h>
|
||||||
|
#include <XCEngine/Components/MeshFilterComponent.h>
|
||||||
|
#include <XCEngine/Components/MeshRendererComponent.h>
|
||||||
|
#include <XCEngine/Core/Asset/ResourceManager.h>
|
||||||
|
#include <XCEngine/Scene/Scene.h>
|
||||||
|
|
||||||
|
namespace XCEngine {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
using namespace Components;
|
||||||
|
using namespace Resources;
|
||||||
|
|
||||||
|
Containers::String MakeNodeName(const ModelNode& node, Core::uint32 nodeIndex) {
|
||||||
|
if (!node.name.Empty()) {
|
||||||
|
return node.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Containers::String(("ModelNode_" + std::to_string(nodeIndex)).c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
Containers::String MakeMeshObjectName(const Containers::String& nodeName, Core::uint32 meshBindingOffset) {
|
||||||
|
return Containers::String(
|
||||||
|
(std::string(nodeName.CStr()) + "_Mesh" + std::to_string(meshBindingOffset)).c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
AssetRef MakeSubAssetRef(const AssetRef& modelAssetRef, LocalID localID, ResourceType resourceType) {
|
||||||
|
AssetRef ref;
|
||||||
|
ref.assetGuid = modelAssetRef.assetGuid;
|
||||||
|
ref.localID = localID;
|
||||||
|
ref.resourceType = resourceType;
|
||||||
|
return ref;
|
||||||
|
}
|
||||||
|
|
||||||
|
void SetErrorMessage(Containers::String* outErrorMessage, const Containers::String& message) {
|
||||||
|
if (outErrorMessage != nullptr) {
|
||||||
|
*outErrorMessage = message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AttachMeshBinding(
|
||||||
|
GameObject& targetObject,
|
||||||
|
const Model& model,
|
||||||
|
const AssetRef& modelAssetRef,
|
||||||
|
const ModelMeshBinding& meshBinding,
|
||||||
|
Containers::String* outErrorMessage,
|
||||||
|
std::vector<GameObject*>* outMeshObjects) {
|
||||||
|
if (!modelAssetRef.IsValid()) {
|
||||||
|
SetErrorMessage(outErrorMessage, "Model asset ref is required when instantiating mesh bindings.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto* meshFilter = targetObject.GetComponent<MeshFilterComponent>();
|
||||||
|
if (meshFilter == nullptr) {
|
||||||
|
meshFilter = targetObject.AddComponent<MeshFilterComponent>();
|
||||||
|
}
|
||||||
|
|
||||||
|
auto* meshRenderer = targetObject.GetComponent<MeshRendererComponent>();
|
||||||
|
if (meshRenderer == nullptr) {
|
||||||
|
meshRenderer = targetObject.AddComponent<MeshRendererComponent>();
|
||||||
|
}
|
||||||
|
|
||||||
|
meshFilter->SetMeshAssetRef(MakeSubAssetRef(modelAssetRef, meshBinding.meshLocalID, ResourceType::Mesh));
|
||||||
|
|
||||||
|
const auto& materialBindings = model.GetMaterialBindings();
|
||||||
|
const Core::uint32 materialBindingEnd = meshBinding.materialBindingStart + meshBinding.materialBindingCount;
|
||||||
|
if (materialBindingEnd > materialBindings.Size()) {
|
||||||
|
SetErrorMessage(outErrorMessage, "Model mesh binding references material bindings outside the model range.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (Core::uint32 materialBindingIndex = meshBinding.materialBindingStart;
|
||||||
|
materialBindingIndex < materialBindingEnd;
|
||||||
|
++materialBindingIndex) {
|
||||||
|
const ModelMaterialBinding& materialBinding = materialBindings[materialBindingIndex];
|
||||||
|
meshRenderer->SetMaterialAssetRef(
|
||||||
|
static_cast<size_t>(materialBinding.slotIndex),
|
||||||
|
MakeSubAssetRef(modelAssetRef, materialBinding.materialLocalID, ResourceType::Material));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (outMeshObjects != nullptr) {
|
||||||
|
outMeshObjects->push_back(&targetObject);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
bool InstantiateModelHierarchy(
|
||||||
|
Components::Scene& scene,
|
||||||
|
const Resources::Model& model,
|
||||||
|
const Resources::AssetRef& modelAssetRef,
|
||||||
|
Components::GameObject* parent,
|
||||||
|
ModelSceneInstantiationResult* outResult,
|
||||||
|
Containers::String* outErrorMessage) {
|
||||||
|
if (!model.IsValid()) {
|
||||||
|
SetErrorMessage(outErrorMessage, "Model is invalid.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!model.HasRootNode()) {
|
||||||
|
SetErrorMessage(outErrorMessage, "Model does not have a root node.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto& nodes = model.GetNodes();
|
||||||
|
const auto& meshBindings = model.GetMeshBindings();
|
||||||
|
if (model.GetRootNodeIndex() >= nodes.Size()) {
|
||||||
|
SetErrorMessage(outErrorMessage, "Model root node index is outside the node range.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
ModelSceneInstantiationResult localResult;
|
||||||
|
localResult.nodeObjects.resize(nodes.Size(), nullptr);
|
||||||
|
for (Core::uint32 nodeIndex = 0; nodeIndex < nodes.Size(); ++nodeIndex) {
|
||||||
|
const ModelNode& node = nodes[nodeIndex];
|
||||||
|
localResult.nodeObjects[nodeIndex] = scene.CreateGameObject(MakeNodeName(node, nodeIndex).CStr(), nullptr);
|
||||||
|
if (localResult.nodeObjects[nodeIndex] == nullptr) {
|
||||||
|
SetErrorMessage(outErrorMessage, "Failed to create a model node game object.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto* transform = localResult.nodeObjects[nodeIndex]->GetTransform();
|
||||||
|
transform->SetLocalPosition(node.localPosition);
|
||||||
|
transform->SetLocalRotation(node.localRotation);
|
||||||
|
transform->SetLocalScale(node.localScale);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (Core::uint32 nodeIndex = 0; nodeIndex < nodes.Size(); ++nodeIndex) {
|
||||||
|
const ModelNode& node = nodes[nodeIndex];
|
||||||
|
GameObject* nodeObject = localResult.nodeObjects[nodeIndex];
|
||||||
|
if (nodeObject == nullptr) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node.parentIndex >= 0) {
|
||||||
|
const Core::uint32 parentIndex = static_cast<Core::uint32>(node.parentIndex);
|
||||||
|
if (parentIndex >= localResult.nodeObjects.size() || localResult.nodeObjects[parentIndex] == nullptr) {
|
||||||
|
SetErrorMessage(outErrorMessage, "Model node references an invalid parent index.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
nodeObject->SetParent(localResult.nodeObjects[parentIndex], false);
|
||||||
|
} else if (parent != nullptr) {
|
||||||
|
nodeObject->SetParent(parent, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (Core::uint32 nodeIndex = 0; nodeIndex < nodes.Size(); ++nodeIndex) {
|
||||||
|
const ModelNode& node = nodes[nodeIndex];
|
||||||
|
GameObject* nodeObject = localResult.nodeObjects[nodeIndex];
|
||||||
|
if (nodeObject == nullptr || node.meshBindingCount == 0u) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Core::uint32 meshBindingEnd = node.meshBindingStart + node.meshBindingCount;
|
||||||
|
if (meshBindingEnd > meshBindings.Size()) {
|
||||||
|
SetErrorMessage(outErrorMessage, "Model node references mesh bindings outside the model range.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node.meshBindingCount == 1u) {
|
||||||
|
if (!AttachMeshBinding(
|
||||||
|
*nodeObject,
|
||||||
|
model,
|
||||||
|
modelAssetRef,
|
||||||
|
meshBindings[node.meshBindingStart],
|
||||||
|
outErrorMessage,
|
||||||
|
&localResult.meshObjects)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Containers::String nodeName = MakeNodeName(node, nodeIndex);
|
||||||
|
for (Core::uint32 meshBindingIndex = node.meshBindingStart; meshBindingIndex < meshBindingEnd; ++meshBindingIndex) {
|
||||||
|
GameObject* meshObject = scene.CreateGameObject(
|
||||||
|
MakeMeshObjectName(nodeName, meshBindingIndex - node.meshBindingStart).CStr(),
|
||||||
|
nodeObject);
|
||||||
|
if (meshObject == nullptr) {
|
||||||
|
SetErrorMessage(outErrorMessage, "Failed to create an auxiliary mesh binding game object.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!AttachMeshBinding(
|
||||||
|
*meshObject,
|
||||||
|
model,
|
||||||
|
modelAssetRef,
|
||||||
|
meshBindings[meshBindingIndex],
|
||||||
|
outErrorMessage,
|
||||||
|
&localResult.meshObjects)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
localResult.rootObject = localResult.nodeObjects[model.GetRootNodeIndex()];
|
||||||
|
if (outResult != nullptr) {
|
||||||
|
*outResult = std::move(localResult);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool InstantiateModelHierarchy(
|
||||||
|
Components::Scene& scene,
|
||||||
|
const Containers::String& modelPath,
|
||||||
|
Components::GameObject* parent,
|
||||||
|
ModelSceneInstantiationResult* outResult,
|
||||||
|
Containers::String* outErrorMessage) {
|
||||||
|
ResourceManager& resourceManager = ResourceManager::Get();
|
||||||
|
const ResourceHandle<Resources::Model> modelHandle = resourceManager.Load<Resources::Model>(modelPath);
|
||||||
|
if (!modelHandle.IsValid()) {
|
||||||
|
SetErrorMessage(outErrorMessage, Containers::String("Failed to load model asset: ") + modelPath);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
AssetRef modelAssetRef;
|
||||||
|
if (!resourceManager.TryGetAssetRef(modelPath, ResourceType::Model, modelAssetRef)) {
|
||||||
|
SetErrorMessage(outErrorMessage, Containers::String("Failed to resolve model asset ref: ") + modelPath);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return InstantiateModelHierarchy(scene, *modelHandle, modelAssetRef, parent, outResult, outErrorMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace XCEngine
|
||||||
5
project/Assets/Characters.meta
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
fileFormatVersion: 1
|
||||||
|
guid: b728e58114669fc3dd9e6521bd22933a
|
||||||
|
folderAsset: true
|
||||||
|
importer: FolderImporter
|
||||||
|
importerVersion: 7
|
||||||
195
project/Assets/Characters/Nahida/DependencyMap.md
Normal file
@@ -0,0 +1,195 @@
|
|||||||
|
# Nahida Dependency Map
|
||||||
|
|
||||||
|
This document captures the Unity sample semantics that must survive the port.
|
||||||
|
|
||||||
|
Source roots:
|
||||||
|
- Unity reference project: `mvs/NahidaRender`
|
||||||
|
- Engine project staging area: `project/Assets/Characters/Nahida`
|
||||||
|
|
||||||
|
## Texture groups
|
||||||
|
|
||||||
|
Body and Dress1:
|
||||||
|
- `Textures/Nahida/Avatar_Loli_Catalyst_Nahida_Tex_Body_Diffuse.png`
|
||||||
|
- `Textures/Nahida/Avatar_Loli_Catalyst_Nahida_Tex_Body_Lightmap.png`
|
||||||
|
- `Textures/Nahida/Avatar_Loli_Catalyst_Nahida_Tex_Body_Normalmap.png`
|
||||||
|
- `Textures/Nahida/Avatar_Loli_Catalyst_Nahida_Tex_Body_Shadow_Ramp.png`
|
||||||
|
|
||||||
|
Hair and Dress2:
|
||||||
|
- `Textures/Nahida/Avatar_Loli_Catalyst_Nahida_Tex_Hair_Diffuse.png`
|
||||||
|
- `Textures/Nahida/Avatar_Loli_Catalyst_Nahida_Tex_Hair_Lightmap.png`
|
||||||
|
- `Textures/Nahida/Avatar_Loli_Catalyst_Nahida_Tex_Hair_Normalmap.png`
|
||||||
|
- `Textures/Nahida/Avatar_Loli_Catalyst_Nahida_Tex_Hair_Shadow_Ramp.png`
|
||||||
|
|
||||||
|
Face and shared:
|
||||||
|
- `Textures/Nahida/Avatar_Loli_Catalyst_Nahida_Tex_Face_Diffuse.png`
|
||||||
|
- `Textures/Shared/Avatar_Loli_Tex_FaceLightmap.png`
|
||||||
|
- `Textures/Shared/Avatar_Tex_Face_Shadow.png`
|
||||||
|
- `Textures/Shared/Avatar_Tex_MetalMap.png`
|
||||||
|
|
||||||
|
Reference-only extra:
|
||||||
|
- `Textures/Nahida/Avatar_Loli_Catalyst_Nahida_Tex_Body_Specular_Ramp.png`
|
||||||
|
- Present in the sample resources.
|
||||||
|
- Not directly referenced by the seven Unity Nahida materials that were copied into `docs/reference/NahidaUnity/Materials`.
|
||||||
|
|
||||||
|
## Material graph
|
||||||
|
|
||||||
|
Shared base material:
|
||||||
|
- `docs/reference/NahidaUnity/Materials/Nahida_Base.mat`
|
||||||
|
- Uses `docs/reference/NahidaUnity/Shaders/Genshin.shader`
|
||||||
|
- Binds shared `_MetalMap = Textures/Shared/Avatar_Tex_MetalMap.png`
|
||||||
|
- Carries shared shadow, rim, outline, and specular defaults
|
||||||
|
|
||||||
|
Body branch:
|
||||||
|
- `docs/reference/NahidaUnity/Materials/Nahida_Body.mat`
|
||||||
|
- `_BaseMap = Body_Diffuse`
|
||||||
|
- `_LightMap = Body_Lightmap`
|
||||||
|
- `_NormalMap = Body_Normalmap`
|
||||||
|
- `_ShadowRamp = Body_Shadow_Ramp`
|
||||||
|
- Enables emission, normal map, rim, specular, and smooth normal
|
||||||
|
|
||||||
|
- `docs/reference/NahidaUnity/Materials/Nahida_Dress1.mat`
|
||||||
|
- Reuses the same body texture group
|
||||||
|
- Double-sided
|
||||||
|
- Uses smooth normal
|
||||||
|
|
||||||
|
Hair branch:
|
||||||
|
- `docs/reference/NahidaUnity/Materials/Nahida_Hair.mat`
|
||||||
|
- `_BaseMap = Hair_Diffuse`
|
||||||
|
- `_LightMap = Hair_Lightmap`
|
||||||
|
- `_NormalMap = Hair_Normalmap`
|
||||||
|
- `_ShadowRamp = Hair_Shadow_Ramp`
|
||||||
|
- Enables emission, normal map, rim, specular, and smooth normal
|
||||||
|
|
||||||
|
- `docs/reference/NahidaUnity/Materials/Nahida_Dress2.mat`
|
||||||
|
- Reuses the same hair texture group
|
||||||
|
- Double-sided
|
||||||
|
|
||||||
|
Face branch:
|
||||||
|
- `docs/reference/NahidaUnity/Materials/Nahida_Face.mat`
|
||||||
|
- `_BaseMap = Face_Diffuse`
|
||||||
|
- `_FaceLightMap = Avatar_Loli_Tex_FaceLightmap`
|
||||||
|
- `_FaceShadow = Avatar_Tex_Face_Shadow`
|
||||||
|
- `_ShadowRamp = Body_Shadow_Ramp`
|
||||||
|
- Marks `_IsFace = 1`
|
||||||
|
- Depends on runtime `_FaceDirection`
|
||||||
|
|
||||||
|
- `docs/reference/NahidaUnity/Materials/Nahida_Brow.mat`
|
||||||
|
- Reuses the same face texture group
|
||||||
|
- Uses a tint override through `_BaseColor`
|
||||||
|
- Sets outline width to `0`
|
||||||
|
|
||||||
|
## Shader semantics
|
||||||
|
|
||||||
|
Core shader files:
|
||||||
|
- `docs/reference/NahidaUnity/Shaders/Genshin.shader`
|
||||||
|
- `docs/reference/NahidaUnity/Shaders/GenshinInput.hlsl`
|
||||||
|
- `docs/reference/NahidaUnity/Shaders/GenshinForwardPass.hlsl`
|
||||||
|
- `docs/reference/NahidaUnity/Shaders/GenshinOutlinePass.hlsl`
|
||||||
|
|
||||||
|
Important shader features to preserve:
|
||||||
|
- `_LightMap` driven shadow partitioning
|
||||||
|
- `_FaceLightMap + _FaceShadow + _FaceDirection` face shading path
|
||||||
|
- `_MetalMap` driven specular/metal response
|
||||||
|
- Rim light
|
||||||
|
- Separate outline pass
|
||||||
|
- `_UseSmoothNormal` switch for outline data path
|
||||||
|
|
||||||
|
## Scene semantics from Unity sample
|
||||||
|
|
||||||
|
Reference scene:
|
||||||
|
- `docs/reference/NahidaUnity/Scenes/SampleScene.unity`
|
||||||
|
|
||||||
|
Observed setup:
|
||||||
|
- Instantiates `Avatar_Loli_Catalyst_Nahida.fbx` as a prefab instance
|
||||||
|
- Renames the root object to `NahidaUnityModel`
|
||||||
|
- Uses `SkinnedMeshRenderer`, not a static `MeshFilter + MeshRenderer` setup
|
||||||
|
- Overrides one renderer mesh with `Meshes/Nahida_Body_Smooth.mesh`
|
||||||
|
- Assigns body renderer material slots in this order:
|
||||||
|
- slot 0: `Nahida_Hair.mat`
|
||||||
|
- slot 1: `Nahida_Body.mat`
|
||||||
|
- slot 2: `Nahida_Dress1.mat`
|
||||||
|
- slot 3: `Nahida_Dress2.mat`
|
||||||
|
- Additional renderers use:
|
||||||
|
- `Nahida_Face.mat`
|
||||||
|
- `Nahida_Brow.mat`
|
||||||
|
- One face-related renderer writes `BlendShapeWeights[0] = 100`
|
||||||
|
|
||||||
|
## Current static assembly baseline in XCEngine
|
||||||
|
|
||||||
|
Preview scene:
|
||||||
|
- `Assets/Scenes/NahidaPreview.xc`
|
||||||
|
|
||||||
|
Current static assembly uses the existing imported FBX sub-meshes from:
|
||||||
|
- `Assets/Models/nahida/Avatar_Loli_Catalyst_Nahida.fbx`
|
||||||
|
|
||||||
|
Scene-side material path mapping in `Assets/Scenes/NahidaPreview.xc`:
|
||||||
|
- `Nahida_Hair`
|
||||||
|
- mesh localID: `5268898388415806497`
|
||||||
|
- engine material: `Assets/Characters/Nahida/Materials/Nahida_Hair.mat`
|
||||||
|
- `Nahida_Body`
|
||||||
|
- mesh localID: `5268897288904178286`
|
||||||
|
- engine material: `Assets/Characters/Nahida/Materials/Nahida_Body.mat`
|
||||||
|
- `Nahida_Dress1`
|
||||||
|
- mesh localID: `5268896189392550075`
|
||||||
|
- engine material: `Assets/Characters/Nahida/Materials/Nahida_Dress1.mat`
|
||||||
|
- `Nahida_Dress2`
|
||||||
|
- mesh localID: `5268895089880921864`
|
||||||
|
- engine material: `Assets/Characters/Nahida/Materials/Nahida_Dress2.mat`
|
||||||
|
- `Nahida_Brow`
|
||||||
|
- mesh localID: `15841426242793151682`
|
||||||
|
- engine material: `Assets/Characters/Nahida/Materials/Nahida_Brow.mat`
|
||||||
|
- `Nahida_EffectMesh`
|
||||||
|
- mesh localID: `692846506840157104`
|
||||||
|
- temporary fallback material: `builtin://materials/default-primitive`
|
||||||
|
- `Nahida_EyeStar`
|
||||||
|
- mesh localID: `8234240765526303311`
|
||||||
|
- engine material: `Assets/Characters/Nahida/Materials/Nahida_Face.mat`
|
||||||
|
- `Nahida_Face`
|
||||||
|
- mesh localID: `1306782875462705981`
|
||||||
|
- engine material: `Assets/Characters/Nahida/Materials/Nahida_Face.mat`
|
||||||
|
- `Nahida_FaceEye`
|
||||||
|
- mesh localID: `977130118610429631`
|
||||||
|
- engine material: `Assets/Characters/Nahida/Materials/Nahida_Face.mat`
|
||||||
|
|
||||||
|
Current engine-native shader and materials:
|
||||||
|
- Shader: `Assets/Shaders/XCCharacterToon.shader`
|
||||||
|
- Materials:
|
||||||
|
- `Assets/Characters/Nahida/Materials/Nahida_Hair.mat`
|
||||||
|
- `Assets/Characters/Nahida/Materials/Nahida_Body.mat`
|
||||||
|
- `Assets/Characters/Nahida/Materials/Nahida_Dress1.mat`
|
||||||
|
- `Assets/Characters/Nahida/Materials/Nahida_Dress2.mat`
|
||||||
|
- `Assets/Characters/Nahida/Materials/Nahida_Face.mat`
|
||||||
|
- `Assets/Characters/Nahida/Materials/Nahida_Brow.mat`
|
||||||
|
|
||||||
|
Current model material remap sidecars:
|
||||||
|
- `Assets/Models/nahida/Avatar_Loli_Catalyst_Nahida.fbx.materialmap`
|
||||||
|
- `Assets/Characters/Nahida/Model/Avatar_Loli_Catalyst_Nahida.fbx.materialmap`
|
||||||
|
|
||||||
|
Format:
|
||||||
|
- `meshLocalID=Assets/.../Material.mat`
|
||||||
|
- Multiple material slots can be provided with `|`
|
||||||
|
|
||||||
|
Current gaps:
|
||||||
|
- No dedicated outline pass yet.
|
||||||
|
- Face shading logic now matches the Unity-style forward path more closely, but `_FaceDirection` is still not driven at runtime.
|
||||||
|
|
||||||
|
## Runtime driver semantics
|
||||||
|
|
||||||
|
Runtime driver:
|
||||||
|
- `docs/reference/NahidaUnity/Scripts/MaterialUpdater.cs`
|
||||||
|
|
||||||
|
Behavior:
|
||||||
|
- Reads the head bone transform every frame
|
||||||
|
- Rotates the configured local head direction
|
||||||
|
- Writes `_FaceDirection` into every material on the configured face renderers
|
||||||
|
|
||||||
|
This means face shading is not static material data. The engine port needs a runtime material-parameter driver.
|
||||||
|
|
||||||
|
## Post-process reference
|
||||||
|
|
||||||
|
Reference files:
|
||||||
|
- `docs/reference/NahidaUnity/Shaders/URPGenshinPostProcess.shader`
|
||||||
|
- `docs/reference/NahidaUnity/Scripts/Rendering/`
|
||||||
|
- `docs/reference/NahidaUnity/URPSettings/`
|
||||||
|
|
||||||
|
These are kept only as semantic reference for a later phase. They should not be treated as engine-native runtime assets.
|
||||||
5
project/Assets/Characters/Nahida/DependencyMap.md.meta
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
fileFormatVersion: 1
|
||||||
|
guid: 28da23fea182596ac7927a1f42a87cd8
|
||||||
|
folderAsset: false
|
||||||
|
importer: DefaultImporter
|
||||||
|
importerVersion: 7
|
||||||
5
project/Assets/Characters/Nahida/Materials.meta
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
fileFormatVersion: 1
|
||||||
|
guid: 725c8d70965b4172e982c891fbc0f6e6
|
||||||
|
folderAsset: true
|
||||||
|
importer: FolderImporter
|
||||||
|
importerVersion: 7
|
||||||
19
project/Assets/Characters/Nahida/Materials/Nahida_Body.mat
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"shader": "Assets/Shaders/XCCharacterToon.shader",
|
||||||
|
"renderQueue": "geometry",
|
||||||
|
"keywords": ["_EMISSION", "_NORMAL_MAP", "_RIM", "_SPECULAR"],
|
||||||
|
"properties": {
|
||||||
|
"_UseEmission": 1.0,
|
||||||
|
"_UseNormalMap": 1.0,
|
||||||
|
"_UseRim": 1.0,
|
||||||
|
"_UseSmoothNormal": 1.0,
|
||||||
|
"_UseSpecular": 1.0
|
||||||
|
},
|
||||||
|
"textures": {
|
||||||
|
"_BaseMap": "Assets/Characters/Nahida/Textures/Nahida/Avatar_Loli_Catalyst_Nahida_Tex_Body_Diffuse.png",
|
||||||
|
"_LightMap": "Assets/Characters/Nahida/Textures/Nahida/Avatar_Loli_Catalyst_Nahida_Tex_Body_Lightmap.png",
|
||||||
|
"_NormalMap": "Assets/Characters/Nahida/Textures/Nahida/Avatar_Loli_Catalyst_Nahida_Tex_Body_Normalmap.png",
|
||||||
|
"_ShadowRamp": "Assets/Characters/Nahida/Textures/Nahida/Avatar_Loli_Catalyst_Nahida_Tex_Body_Shadow_Ramp.png",
|
||||||
|
"_MetalMap": "Assets/Characters/Nahida/Textures/Shared/Avatar_Tex_MetalMap.png"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
fileFormatVersion: 1
|
||||||
|
guid: 4e3f0d5ffce7442ab339368d71155d11
|
||||||
|
folderAsset: false
|
||||||
|
importer: MaterialImporter
|
||||||
|
importerVersion: 8
|
||||||
19
project/Assets/Characters/Nahida/Materials/Nahida_Brow.mat
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"shader": "Assets/Shaders/XCCharacterToon.shader",
|
||||||
|
"renderQueue": "geometry",
|
||||||
|
"keywords": ["_IS_FACE", "_RIM"],
|
||||||
|
"properties": {
|
||||||
|
"_BaseColor": [0.9764706, 0.80103135, 0.76164705, 1.0],
|
||||||
|
"_FaceDirection": [-0.0051237254, -0.11509954, -0.99334073, 0.0],
|
||||||
|
"_IsFace": 1.0,
|
||||||
|
"_OutlineWidth": 0.0,
|
||||||
|
"_UseCustomMaterialType": 1.0,
|
||||||
|
"_UseRim": 1.0
|
||||||
|
},
|
||||||
|
"textures": {
|
||||||
|
"_BaseMap": "Assets/Characters/Nahida/Textures/Nahida/Avatar_Loli_Catalyst_Nahida_Tex_Face_Diffuse.png",
|
||||||
|
"_FaceLightMap": "Assets/Characters/Nahida/Textures/Shared/Avatar_Loli_Tex_FaceLightmap.png",
|
||||||
|
"_FaceShadow": "Assets/Characters/Nahida/Textures/Shared/Avatar_Tex_Face_Shadow.png",
|
||||||
|
"_ShadowRamp": "Assets/Characters/Nahida/Textures/Nahida/Avatar_Loli_Catalyst_Nahida_Tex_Body_Shadow_Ramp.png"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
fileFormatVersion: 1
|
||||||
|
guid: 4f1b23288e264487b6658db635991d0d
|
||||||
|
folderAsset: false
|
||||||
|
importer: MaterialImporter
|
||||||
|
importerVersion: 8
|
||||||
24
project/Assets/Characters/Nahida/Materials/Nahida_Dress1.mat
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"shader": "Assets/Shaders/XCCharacterToon.shader",
|
||||||
|
"renderQueue": "geometry",
|
||||||
|
"keywords": ["_EMISSION", "_NORMAL_MAP", "_RIM", "_SPECULAR"],
|
||||||
|
"renderState": {
|
||||||
|
"cull": "none"
|
||||||
|
},
|
||||||
|
"properties": {
|
||||||
|
"_DoubleSided": 1.0,
|
||||||
|
"_OutlineZOffset": 0.5,
|
||||||
|
"_UseEmission": 1.0,
|
||||||
|
"_UseNormalMap": 1.0,
|
||||||
|
"_UseRim": 1.0,
|
||||||
|
"_UseSmoothNormal": 1.0,
|
||||||
|
"_UseSpecular": 1.0
|
||||||
|
},
|
||||||
|
"textures": {
|
||||||
|
"_BaseMap": "Assets/Characters/Nahida/Textures/Nahida/Avatar_Loli_Catalyst_Nahida_Tex_Body_Diffuse.png",
|
||||||
|
"_LightMap": "Assets/Characters/Nahida/Textures/Nahida/Avatar_Loli_Catalyst_Nahida_Tex_Body_Lightmap.png",
|
||||||
|
"_NormalMap": "Assets/Characters/Nahida/Textures/Nahida/Avatar_Loli_Catalyst_Nahida_Tex_Body_Normalmap.png",
|
||||||
|
"_ShadowRamp": "Assets/Characters/Nahida/Textures/Nahida/Avatar_Loli_Catalyst_Nahida_Tex_Body_Shadow_Ramp.png",
|
||||||
|
"_MetalMap": "Assets/Characters/Nahida/Textures/Shared/Avatar_Tex_MetalMap.png"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
fileFormatVersion: 1
|
||||||
|
guid: cb6d6cf5229d47ecb22f5467b66d831f
|
||||||
|
folderAsset: false
|
||||||
|
importer: MaterialImporter
|
||||||
|
importerVersion: 8
|
||||||
23
project/Assets/Characters/Nahida/Materials/Nahida_Dress2.mat
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"shader": "Assets/Shaders/XCCharacterToon.shader",
|
||||||
|
"renderQueue": "geometry",
|
||||||
|
"keywords": ["_EMISSION", "_NORMAL_MAP", "_RIM", "_SPECULAR"],
|
||||||
|
"renderState": {
|
||||||
|
"cull": "none"
|
||||||
|
},
|
||||||
|
"properties": {
|
||||||
|
"_DoubleSided": 1.0,
|
||||||
|
"_OutlineZOffset": 0.5,
|
||||||
|
"_UseEmission": 1.0,
|
||||||
|
"_UseNormalMap": 1.0,
|
||||||
|
"_UseRim": 1.0,
|
||||||
|
"_UseSpecular": 1.0
|
||||||
|
},
|
||||||
|
"textures": {
|
||||||
|
"_BaseMap": "Assets/Characters/Nahida/Textures/Nahida/Avatar_Loli_Catalyst_Nahida_Tex_Hair_Diffuse.png",
|
||||||
|
"_LightMap": "Assets/Characters/Nahida/Textures/Nahida/Avatar_Loli_Catalyst_Nahida_Tex_Hair_Lightmap.png",
|
||||||
|
"_NormalMap": "Assets/Characters/Nahida/Textures/Nahida/Avatar_Loli_Catalyst_Nahida_Tex_Hair_Normalmap.png",
|
||||||
|
"_ShadowRamp": "Assets/Characters/Nahida/Textures/Nahida/Avatar_Loli_Catalyst_Nahida_Tex_Hair_Shadow_Ramp.png",
|
||||||
|
"_MetalMap": "Assets/Characters/Nahida/Textures/Shared/Avatar_Tex_MetalMap.png"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
fileFormatVersion: 1
|
||||||
|
guid: b05741f6efd64d40b18f33990be8fc8d
|
||||||
|
folderAsset: false
|
||||||
|
importer: MaterialImporter
|
||||||
|
importerVersion: 8
|
||||||
19
project/Assets/Characters/Nahida/Materials/Nahida_Face.mat
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"shader": "Assets/Shaders/XCCharacterToon.shader",
|
||||||
|
"renderQueue": "geometry",
|
||||||
|
"keywords": ["_IS_FACE", "_RIM"],
|
||||||
|
"properties": {
|
||||||
|
"_FaceDirection": [-0.0051237254, -0.11509954, -0.99334073, 0.0],
|
||||||
|
"_FaceBlushStrength": 0.3,
|
||||||
|
"_IsFace": 1.0,
|
||||||
|
"_OutlineZOffset": 0.5,
|
||||||
|
"_UseCustomMaterialType": 1.0,
|
||||||
|
"_UseRim": 1.0
|
||||||
|
},
|
||||||
|
"textures": {
|
||||||
|
"_BaseMap": "Assets/Characters/Nahida/Textures/Nahida/Avatar_Loli_Catalyst_Nahida_Tex_Face_Diffuse.png",
|
||||||
|
"_FaceLightMap": "Assets/Characters/Nahida/Textures/Shared/Avatar_Loli_Tex_FaceLightmap.png",
|
||||||
|
"_FaceShadow": "Assets/Characters/Nahida/Textures/Shared/Avatar_Tex_Face_Shadow.png",
|
||||||
|
"_ShadowRamp": "Assets/Characters/Nahida/Textures/Nahida/Avatar_Loli_Catalyst_Nahida_Tex_Body_Shadow_Ramp.png"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
fileFormatVersion: 1
|
||||||
|
guid: f8df278bf29d44ecb2f0ddcefe8f8c89
|
||||||
|
folderAsset: false
|
||||||
|
importer: MaterialImporter
|
||||||
|
importerVersion: 8
|
||||||
20
project/Assets/Characters/Nahida/Materials/Nahida_Hair.mat
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"shader": "Assets/Shaders/XCCharacterToon.shader",
|
||||||
|
"renderQueue": "geometry",
|
||||||
|
"keywords": ["_EMISSION", "_NORMAL_MAP", "_RIM", "_SPECULAR"],
|
||||||
|
"properties": {
|
||||||
|
"_UseEmission": 1.0,
|
||||||
|
"_UseNormalMap": 1.0,
|
||||||
|
"_UseRim": 1.0,
|
||||||
|
"_UseSmoothNormal": 1.0,
|
||||||
|
"_UseSpecular": 1.0,
|
||||||
|
"_OutlineColor": [0.2784314, 0.18039216, 0.14901961, 1.0]
|
||||||
|
},
|
||||||
|
"textures": {
|
||||||
|
"_BaseMap": "Assets/Characters/Nahida/Textures/Nahida/Avatar_Loli_Catalyst_Nahida_Tex_Hair_Diffuse.png",
|
||||||
|
"_LightMap": "Assets/Characters/Nahida/Textures/Nahida/Avatar_Loli_Catalyst_Nahida_Tex_Hair_Lightmap.png",
|
||||||
|
"_NormalMap": "Assets/Characters/Nahida/Textures/Nahida/Avatar_Loli_Catalyst_Nahida_Tex_Hair_Normalmap.png",
|
||||||
|
"_ShadowRamp": "Assets/Characters/Nahida/Textures/Nahida/Avatar_Loli_Catalyst_Nahida_Tex_Hair_Shadow_Ramp.png",
|
||||||
|
"_MetalMap": "Assets/Characters/Nahida/Textures/Shared/Avatar_Tex_MetalMap.png"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
fileFormatVersion: 1
|
||||||
|
guid: 9e4b6736db6d4fc1ab0ea6933043f422
|
||||||
|
folderAsset: false
|
||||||
|
importer: MaterialImporter
|
||||||
|
importerVersion: 8
|
||||||
5
project/Assets/Characters/Nahida/Meshes.meta
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
fileFormatVersion: 1
|
||||||
|
guid: 344e873aa42dd08f92d5f330479a4a17
|
||||||
|
folderAsset: true
|
||||||
|
importer: FolderImporter
|
||||||
|
importerVersion: 7
|
||||||
2195
project/Assets/Characters/Nahida/Meshes/Nahida_Body_Smooth.mesh
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
fileFormatVersion: 1
|
||||||
|
guid: 1b61124cf698a5ef085f249b9fa733cd
|
||||||
|
folderAsset: false
|
||||||
|
importer: DefaultImporter
|
||||||
|
importerVersion: 7
|
||||||
5
project/Assets/Characters/Nahida/Model.meta
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
fileFormatVersion: 1
|
||||||
|
guid: 545136f78545ba2ab47f94b9f88f11cd
|
||||||
|
folderAsset: true
|
||||||
|
importer: FolderImporter
|
||||||
|
importerVersion: 7
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
# meshLocalID=materialPath[|materialPath...]
|
||||||
|
5268898388415806497=Assets/Characters/Nahida/Materials/Nahida_Hair.mat
|
||||||
|
5268897288904178286=Assets/Characters/Nahida/Materials/Nahida_Body.mat
|
||||||
|
5268896189392550075=Assets/Characters/Nahida/Materials/Nahida_Dress1.mat
|
||||||
|
5268895089880921864=Assets/Characters/Nahida/Materials/Nahida_Dress2.mat
|
||||||
|
15841426242793151682=Assets/Characters/Nahida/Materials/Nahida_Brow.mat
|
||||||
|
692846506840157104=builtin://materials/default-primitive
|
||||||
|
8234240765526303311=Assets/Characters/Nahida/Materials/Nahida_Face.mat
|
||||||
|
1306782875462705981=Assets/Characters/Nahida/Materials/Nahida_Face.mat
|
||||||
|
977130118610429631=Assets/Characters/Nahida/Materials/Nahida_Face.mat
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
fileFormatVersion: 1
|
||||||
|
guid: c59d5039bc5145a5dfdbdf8a2b301d2d
|
||||||
|
folderAsset: false
|
||||||
|
importer: DefaultImporter
|
||||||
|
importerVersion: 7
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
fileFormatVersion: 1
|
||||||
|
guid: 9c225844db0e8be81d1887d23a25fb6d
|
||||||
|
folderAsset: false
|
||||||
|
importer: ModelImporter
|
||||||
|
importerVersion: 12
|
||||||
49
project/Assets/Characters/Nahida/README.md
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
# Nahida Staging Assets
|
||||||
|
|
||||||
|
This folder is the non-breaking staging area for migrating the Unity Nahida sample into the engine-native project structure.
|
||||||
|
|
||||||
|
Current safety rule:
|
||||||
|
- Keep `project/Assets/Models/nahida` in place for now.
|
||||||
|
- `project/Assets/Scenes/Main.xc` still references the old imported FBX asset GUID from `Assets/Models/nahida/Avatar_Loli_Catalyst_Nahida.fbx`.
|
||||||
|
- Do not rename or delete the old Nahida folder until a `Model`-driven scene path is ready.
|
||||||
|
|
||||||
|
Directory layout:
|
||||||
|
- `Model/`
|
||||||
|
- Canonical FBX source that should become the future `Model` entry asset.
|
||||||
|
- `Textures/Nahida/`
|
||||||
|
- Character-local body, hair, and face textures copied from the Unity sample.
|
||||||
|
- `Textures/Shared/`
|
||||||
|
- Shared textures used by the shader and face shading path.
|
||||||
|
- `Meshes/`
|
||||||
|
- Extra mesh data needed by the outline path.
|
||||||
|
- `docs/reference/NahidaUnity/`
|
||||||
|
- Unity-side semantic reference only.
|
||||||
|
- Stored outside `project/Assets` on purpose so the asset pipeline does not try to import Unity `.mat` and `.shader` files as engine-native assets.
|
||||||
|
|
||||||
|
What has been staged in this phase:
|
||||||
|
- Full Nahida FBX source.
|
||||||
|
- Full body, hair, face, and shared texture set.
|
||||||
|
- `Nahida_Body_Smooth.mesh` outline dependency.
|
||||||
|
- Unity reference materials, shader files, runtime scripts, sample scene, and URP settings under `docs/reference/NahidaUnity/`.
|
||||||
|
|
||||||
|
What has not been done yet:
|
||||||
|
- No skinned runtime path, animator path, or blend-shape port.
|
||||||
|
- No dedicated outline pass port yet.
|
||||||
|
- Face shading now follows the Unity-style `_FaceLightMap + _FaceShadow + _FaceDirection` path in the forward shader, but it still does not have Unity's runtime `_FaceDirection` driver.
|
||||||
|
- The preview scene still uses imported FBX sub-meshes, but the main character materials now point at engine-native `.mat` assets.
|
||||||
|
- The main character materials now use Unity-aligned property names, keywords, and normal-map bindings, but they still render through a single forward pass in the current engine pipeline.
|
||||||
|
|
||||||
|
Current FBX material remap bridge:
|
||||||
|
- `Model` instantiation now looks for a sidecar file named `<model>.materialmap`.
|
||||||
|
- Each line maps an imported mesh localID to one or more material paths:
|
||||||
|
- `meshLocalID=Assets/.../Material.mat`
|
||||||
|
- Multiple slots can be provided with `|`
|
||||||
|
- Nahida remap files are staged at:
|
||||||
|
- `Assets/Models/nahida/Avatar_Loli_Catalyst_Nahida.fbx.materialmap`
|
||||||
|
- `Assets/Characters/Nahida/Model/Avatar_Loli_Catalyst_Nahida.fbx.materialmap`
|
||||||
|
|
||||||
|
Recommended next implementation order:
|
||||||
|
1. Visually validate `Assets/Scenes/NahidaPreview.xc` and tune framing/light balance against the Unity reference scene.
|
||||||
|
2. Add a proper outline solution without introducing skinning work.
|
||||||
|
3. Add a lightweight runtime `_FaceDirection` material driver for static preview scenes.
|
||||||
|
4. Decide whether the static preview should keep the full instantiated FBX node tree or collapse helper/bone-only nodes for readability.
|
||||||
5
project/Assets/Characters/Nahida/README.md.meta
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
fileFormatVersion: 1
|
||||||
|
guid: 113b61db9e093c2f322baac3044d87c4
|
||||||
|
folderAsset: false
|
||||||
|
importer: DefaultImporter
|
||||||
|
importerVersion: 7
|
||||||
5
project/Assets/Characters/Nahida/Textures.meta
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
fileFormatVersion: 1
|
||||||
|
guid: 9c357f39264001a509e920d6fe32a964
|
||||||
|
folderAsset: true
|
||||||
|
importer: FolderImporter
|
||||||
|
importerVersion: 7
|
||||||
5
project/Assets/Characters/Nahida/Textures/Nahida.meta
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
fileFormatVersion: 1
|
||||||
|
guid: ad60afb17f902c3354694f6ed843f338
|
||||||
|
folderAsset: true
|
||||||
|
importer: FolderImporter
|
||||||
|
importerVersion: 7
|
||||||
|
After Width: | Height: | Size: 812 KiB |
@@ -0,0 +1,5 @@
|
|||||||
|
fileFormatVersion: 1
|
||||||
|
guid: c7b42a277c1cf135d5e9cb6ee524c503
|
||||||
|
folderAsset: false
|
||||||
|
importer: TextureImporter
|
||||||
|
importerVersion: 9
|
||||||
|
After Width: | Height: | Size: 452 KiB |
@@ -0,0 +1,5 @@
|
|||||||
|
fileFormatVersion: 1
|
||||||
|
guid: 58ccdf0abb398c5c9788bf8b3cf504d2
|
||||||
|
folderAsset: false
|
||||||
|
importer: TextureImporter
|
||||||
|
importerVersion: 9
|
||||||
|
After Width: | Height: | Size: 304 KiB |
@@ -0,0 +1,5 @@
|
|||||||
|
fileFormatVersion: 1
|
||||||
|
guid: 3cd3aafb26fc8002b87fd2530da1d15b
|
||||||
|
folderAsset: false
|
||||||
|
importer: TextureImporter
|
||||||
|
importerVersion: 9
|
||||||
|
After Width: | Height: | Size: 614 B |
@@ -0,0 +1,5 @@
|
|||||||
|
fileFormatVersion: 1
|
||||||
|
guid: 33e3526cdaec1749e8ceb79a4ca75a18
|
||||||
|
folderAsset: false
|
||||||
|
importer: TextureImporter
|
||||||
|
importerVersion: 9
|
||||||
|
After Width: | Height: | Size: 125 B |
@@ -0,0 +1,5 @@
|
|||||||
|
fileFormatVersion: 1
|
||||||
|
guid: e51d3a747171881244c074abee912603
|
||||||
|
folderAsset: false
|
||||||
|
importer: TextureImporter
|
||||||
|
importerVersion: 9
|
||||||
|
After Width: | Height: | Size: 348 KiB |
@@ -0,0 +1,5 @@
|
|||||||
|
fileFormatVersion: 1
|
||||||
|
guid: ab5235785541c991a236d0003f000989
|
||||||
|
folderAsset: false
|
||||||
|
importer: TextureImporter
|
||||||
|
importerVersion: 9
|
||||||
|
After Width: | Height: | Size: 800 KiB |
@@ -0,0 +1,5 @@
|
|||||||
|
fileFormatVersion: 1
|
||||||
|
guid: 9734cd1fe5cd61f0b3b2a04993d36dd3
|
||||||
|
folderAsset: false
|
||||||
|
importer: TextureImporter
|
||||||
|
importerVersion: 9
|
||||||
|
After Width: | Height: | Size: 501 KiB |
@@ -0,0 +1,5 @@
|
|||||||
|
fileFormatVersion: 1
|
||||||
|
guid: 29eb407776ceb38fa6463e2d6938d808
|
||||||
|
folderAsset: false
|
||||||
|
importer: TextureImporter
|
||||||
|
importerVersion: 9
|
||||||
|
After Width: | Height: | Size: 172 KiB |
@@ -0,0 +1,5 @@
|
|||||||
|
fileFormatVersion: 1
|
||||||
|
guid: 7122f1b290e26ea8fe7c68a63f5003c2
|
||||||
|
folderAsset: false
|
||||||
|
importer: TextureImporter
|
||||||
|
importerVersion: 9
|
||||||
|
After Width: | Height: | Size: 540 B |
@@ -0,0 +1,5 @@
|
|||||||
|
fileFormatVersion: 1
|
||||||
|
guid: c76b66d7f54717749fb4ac684872b94a
|
||||||
|
folderAsset: false
|
||||||
|
importer: TextureImporter
|
||||||
|
importerVersion: 9
|
||||||
5
project/Assets/Characters/Nahida/Textures/Shared.meta
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
fileFormatVersion: 1
|
||||||
|
guid: bd5012ae058d3240669b1e2278f780f9
|
||||||
|
folderAsset: true
|
||||||
|
importer: FolderImporter
|
||||||
|
importerVersion: 7
|
||||||
|
After Width: | Height: | Size: 93 KiB |
@@ -0,0 +1,5 @@
|
|||||||
|
fileFormatVersion: 1
|
||||||
|
guid: d97403a58e720b96269ee683519ee167
|
||||||
|
folderAsset: false
|
||||||
|
importer: TextureImporter
|
||||||
|
importerVersion: 9
|
||||||
|
After Width: | Height: | Size: 14 KiB |
@@ -0,0 +1,5 @@
|
|||||||
|
fileFormatVersion: 1
|
||||||
|
guid: e632e900c73bd6cdc4c51951e1ab02b6
|
||||||
|
folderAsset: false
|
||||||
|
importer: TextureImporter
|
||||||
|
importerVersion: 9
|
||||||
|
After Width: | Height: | Size: 14 KiB |
@@ -0,0 +1,5 @@
|
|||||||
|
fileFormatVersion: 1
|
||||||
|
guid: b1928595729c1c6863c89d17b054288b
|
||||||
|
folderAsset: false
|
||||||
|
importer: TextureImporter
|
||||||
|
importerVersion: 9
|
||||||
1762
project/Assets/Scenes/NahidaPreview.xc
Normal file
5
project/Assets/Scenes/NahidaPreview.xc.meta
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
fileFormatVersion: 1
|
||||||
|
guid: 0ef5176ec0a14e2d9f9d1201ca5a2d5a
|
||||||
|
folderAsset: false
|
||||||
|
importer: DefaultImporter
|
||||||
|
importerVersion: 7
|
||||||
5
project/Assets/Shaders.meta
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
fileFormatVersion: 1
|
||||||
|
guid: 1f85e3636b3f31e874d42632b48eee0a
|
||||||
|
folderAsset: true
|
||||||
|
importer: FolderImporter
|
||||||
|
importerVersion: 7
|
||||||
583
project/Assets/Shaders/XCCharacterToon.shader
Normal file
@@ -0,0 +1,583 @@
|
|||||||
|
Shader "XC Character Toon"
|
||||||
|
{
|
||||||
|
Properties
|
||||||
|
{
|
||||||
|
_BaseMap ("Base Map", 2D) = "white" [Semantic(BaseColorTexture)]
|
||||||
|
_BaseColor ("Base Color", Color) = (1,1,1,1) [Semantic(BaseColor)]
|
||||||
|
_Cutoff ("Alpha Cutoff", Range) = 0.5 [Semantic(AlphaCutoff)]
|
||||||
|
_IsDay ("Is Day", Float) = 1
|
||||||
|
_DoubleSided ("Double Sided", Float) = 0
|
||||||
|
_LightMap ("Light Map", 2D) = "white"
|
||||||
|
_LightDirectionMultiplier ("Light Direction Multiplier", Vector) = (1,0.5,1,0)
|
||||||
|
_ShadowOffset ("Shadow Offset", Float) = 0.1
|
||||||
|
_ShadowSmoothness ("Shadow Smoothness", Float) = 0.4
|
||||||
|
_ShadowColor ("Shadow Color", Color) = (1.1,1.1,1.1,1)
|
||||||
|
_ShadowRamp ("Shadow Ramp", 2D) = "white"
|
||||||
|
_UseCustomMaterialType ("Use Custom Material Type", Float) = 0
|
||||||
|
_CustomMaterialType ("Custom Material Type", Float) = 1
|
||||||
|
_UseEmission ("Use Emission", Float) = 0
|
||||||
|
_EmissionIntensity ("Emission Intensity", Float) = 0.2
|
||||||
|
_UseNormalMap ("Use Normal Map", Float) = 0
|
||||||
|
_NormalMap ("Normal Map", 2D) = "bump"
|
||||||
|
_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,0.72156864,0.69803923,1)
|
||||||
|
_FaceBlushStrength ("Face Blush Strength", Float) = 0
|
||||||
|
_FaceLightMap ("Face Light Map", 2D) = "white"
|
||||||
|
_FaceShadow ("Face Shadow", 2D) = "white"
|
||||||
|
_UseSpecular ("Use Specular", Float) = 0
|
||||||
|
_SpecularSmoothness ("Specular Smoothness", Float) = 5
|
||||||
|
_NonmetallicIntensity ("Nonmetallic Intensity", Float) = 0.3
|
||||||
|
_MetallicIntensity ("Metallic Intensity", Float) = 8
|
||||||
|
_MetalMap ("Metal Map", 2D) = "white"
|
||||||
|
_UseRim ("Use Rim", Float) = 0
|
||||||
|
_RimOffset ("Rim Offset", Float) = 5
|
||||||
|
_RimThreshold ("Rim Threshold", Float) = 0.5
|
||||||
|
_RimIntensity ("Rim Intensity", Float) = 0.5
|
||||||
|
_UseSmoothNormal ("Use Smooth Normal", Float) = 0
|
||||||
|
_OutlineWidth ("Outline Width", Float) = 1.6
|
||||||
|
_OutlineWidthParams ("Outline Width Params", Vector) = (0,6,0.1,0.6)
|
||||||
|
_OutlineZOffset ("Outline Z Offset", Float) = 0.1
|
||||||
|
_ScreenOffset ("Screen Offset", Vector) = (0,0,0,0)
|
||||||
|
_OutlineColor ("Outline Color", Color) = (0.5176471,0.35686275,0.34117648,1)
|
||||||
|
_OutlineColor2 ("Outline Color 2", Color) = (0.3529412,0.3529412,0.3529412,1)
|
||||||
|
_OutlineColor3 ("Outline Color 3", Color) = (0.47058824,0.47058824,0.5647059,1)
|
||||||
|
_OutlineColor4 ("Outline Color 4", Color) = (0.5176471,0.35686275,0.34117648,1)
|
||||||
|
_OutlineColor5 ("Outline Color 5", Color) = (0.35,0.35,0.35,1)
|
||||||
|
}
|
||||||
|
HLSLINCLUDE
|
||||||
|
cbuffer PerObjectConstants
|
||||||
|
{
|
||||||
|
float4x4 gProjectionMatrix;
|
||||||
|
float4x4 gViewMatrix;
|
||||||
|
float4x4 gModelMatrix;
|
||||||
|
float4x4 gNormalMatrix;
|
||||||
|
};
|
||||||
|
|
||||||
|
static const int XC_MAX_ADDITIONAL_LIGHTS = 8;
|
||||||
|
|
||||||
|
struct AdditionalLightData
|
||||||
|
{
|
||||||
|
float4 colorAndIntensity;
|
||||||
|
float4 positionAndRange;
|
||||||
|
float4 directionAndType;
|
||||||
|
float4 spotAnglesAndFlags;
|
||||||
|
};
|
||||||
|
|
||||||
|
cbuffer LightingConstants
|
||||||
|
{
|
||||||
|
float4 gMainLightDirectionAndIntensity;
|
||||||
|
float4 gMainLightColorAndFlags;
|
||||||
|
float4 gLightingParams;
|
||||||
|
AdditionalLightData gAdditionalLights[XC_MAX_ADDITIONAL_LIGHTS];
|
||||||
|
};
|
||||||
|
|
||||||
|
cbuffer MaterialConstants
|
||||||
|
{
|
||||||
|
float4 _BaseColor;
|
||||||
|
float4 _Cutoff;
|
||||||
|
float4 _IsDay;
|
||||||
|
float4 _DoubleSided;
|
||||||
|
float4 _LightDirectionMultiplier;
|
||||||
|
float4 _ShadowOffset;
|
||||||
|
float4 _ShadowSmoothness;
|
||||||
|
float4 _ShadowColor;
|
||||||
|
float4 _UseCustomMaterialType;
|
||||||
|
float4 _CustomMaterialType;
|
||||||
|
float4 _UseEmission;
|
||||||
|
float4 _EmissionIntensity;
|
||||||
|
float4 _UseNormalMap;
|
||||||
|
float4 _IsFace;
|
||||||
|
float4 _FaceDirection;
|
||||||
|
float4 _FaceShadowOffset;
|
||||||
|
float4 _FaceBlushColor;
|
||||||
|
float4 _FaceBlushStrength;
|
||||||
|
float4 _UseSpecular;
|
||||||
|
float4 _SpecularSmoothness;
|
||||||
|
float4 _NonmetallicIntensity;
|
||||||
|
float4 _MetallicIntensity;
|
||||||
|
float4 _UseRim;
|
||||||
|
float4 _RimOffset;
|
||||||
|
float4 _RimThreshold;
|
||||||
|
float4 _RimIntensity;
|
||||||
|
float4 _UseSmoothNormal;
|
||||||
|
float4 _OutlineWidth;
|
||||||
|
float4 _OutlineWidthParams;
|
||||||
|
float4 _OutlineZOffset;
|
||||||
|
float4 _ScreenOffset;
|
||||||
|
float4 _OutlineColor;
|
||||||
|
float4 _OutlineColor2;
|
||||||
|
float4 _OutlineColor3;
|
||||||
|
float4 _OutlineColor4;
|
||||||
|
float4 _OutlineColor5;
|
||||||
|
};
|
||||||
|
|
||||||
|
cbuffer ShadowReceiverConstants
|
||||||
|
{
|
||||||
|
float4x4 gWorldToShadowMatrix;
|
||||||
|
float4 gShadowBiasAndTexelSize;
|
||||||
|
float4 gShadowOptions;
|
||||||
|
};
|
||||||
|
|
||||||
|
Texture2D BaseColorTexture;
|
||||||
|
Texture2D _LightMap;
|
||||||
|
Texture2D _ShadowRamp;
|
||||||
|
Texture2D _NormalMap;
|
||||||
|
Texture2D _FaceLightMap;
|
||||||
|
Texture2D _FaceShadow;
|
||||||
|
Texture2D _MetalMap;
|
||||||
|
SamplerState LinearClampSampler;
|
||||||
|
Texture2D ShadowMapTexture;
|
||||||
|
SamplerState ShadowMapSampler;
|
||||||
|
|
||||||
|
struct VSInput
|
||||||
|
{
|
||||||
|
float3 position : POSITION;
|
||||||
|
float3 normal : NORMAL;
|
||||||
|
float4 color : COLOR;
|
||||||
|
float2 texcoord : TEXCOORD0;
|
||||||
|
float2 backTexcoord : TEXCOORD1;
|
||||||
|
float3 tangent : TEXCOORD2;
|
||||||
|
float3 bitangent : TEXCOORD3;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct PSInput
|
||||||
|
{
|
||||||
|
float4 position : SV_POSITION;
|
||||||
|
float2 texcoord : TEXCOORD0;
|
||||||
|
float2 backTexcoord : TEXCOORD1;
|
||||||
|
float3 positionWS : TEXCOORD2;
|
||||||
|
float3 positionVS : TEXCOORD3;
|
||||||
|
float3 normalWS : TEXCOORD4;
|
||||||
|
float3 tangentWS : TEXCOORD5;
|
||||||
|
float3 bitangentWS : TEXCOORD6;
|
||||||
|
float4 color : COLOR;
|
||||||
|
};
|
||||||
|
|
||||||
|
float3 NormalizeSafe(float3 value, float3 fallbackValue)
|
||||||
|
{
|
||||||
|
const float lengthSq = dot(value, value);
|
||||||
|
if (lengthSq <= 1e-6f) {
|
||||||
|
return fallbackValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
return value * rsqrt(lengthSq);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UseEmissionFeature()
|
||||||
|
{
|
||||||
|
#ifdef _EMISSION
|
||||||
|
return true;
|
||||||
|
#else
|
||||||
|
return _UseEmission.x > 0.5f;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UseNormalMapFeature()
|
||||||
|
{
|
||||||
|
#ifdef _NORMAL_MAP
|
||||||
|
return true;
|
||||||
|
#else
|
||||||
|
return _UseNormalMap.x > 0.5f;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UseFaceFeature()
|
||||||
|
{
|
||||||
|
#ifdef _IS_FACE
|
||||||
|
return true;
|
||||||
|
#else
|
||||||
|
return _IsFace.x > 0.5f;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UseSpecularFeature()
|
||||||
|
{
|
||||||
|
#ifdef _SPECULAR
|
||||||
|
return true;
|
||||||
|
#else
|
||||||
|
return _UseSpecular.x > 0.5f;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UseRimFeature()
|
||||||
|
{
|
||||||
|
#ifdef _RIM
|
||||||
|
return true;
|
||||||
|
#else
|
||||||
|
return _UseRim.x > 0.5f;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UseDoubleSidedFeature()
|
||||||
|
{
|
||||||
|
#ifdef _DOUBLE_SIDED
|
||||||
|
return true;
|
||||||
|
#else
|
||||||
|
return _DoubleSided.x > 0.5f;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
float2 ResolveMaterialTexcoord(PSInput input, bool isFrontFace)
|
||||||
|
{
|
||||||
|
if (!UseDoubleSidedFeature()) {
|
||||||
|
return input.texcoord;
|
||||||
|
}
|
||||||
|
|
||||||
|
return lerp(input.texcoord, input.backTexcoord, isFrontFace ? 0.0f : 1.0f);
|
||||||
|
}
|
||||||
|
|
||||||
|
PSInput MainVS(VSInput input)
|
||||||
|
{
|
||||||
|
PSInput output;
|
||||||
|
const float4 positionWS = mul(gModelMatrix, float4(input.position, 1.0f));
|
||||||
|
const float4 positionVS = mul(gViewMatrix, positionWS);
|
||||||
|
output.position = mul(gProjectionMatrix, positionVS);
|
||||||
|
output.texcoord = input.texcoord;
|
||||||
|
output.backTexcoord = input.backTexcoord;
|
||||||
|
output.positionWS = positionWS.xyz;
|
||||||
|
output.positionVS = positionVS.xyz;
|
||||||
|
output.normalWS = normalize(mul((float3x3)gNormalMatrix, input.normal));
|
||||||
|
output.tangentWS = normalize(mul((float3x3)gNormalMatrix, input.tangent));
|
||||||
|
output.bitangentWS = normalize(mul((float3x3)gNormalMatrix, input.bitangent));
|
||||||
|
output.color = input.color;
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
float ComputeShadowAttenuation(float3 positionWS, float3 normalWS, float3 lightDirectionWS)
|
||||||
|
{
|
||||||
|
#ifndef XC_MAIN_LIGHT_SHADOWS
|
||||||
|
return 1.0f;
|
||||||
|
#else
|
||||||
|
if (gShadowOptions.x < 0.5f) {
|
||||||
|
return 1.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
const float3 resolvedNormalWS = NormalizeSafe(normalWS, float3(0.0f, 1.0f, 0.0f));
|
||||||
|
const float3 resolvedLightDirectionWS =
|
||||||
|
NormalizeSafe(lightDirectionWS, float3(0.0f, -1.0f, 0.0f));
|
||||||
|
const float nDotL = saturate(dot(resolvedNormalWS, resolvedLightDirectionWS));
|
||||||
|
const float normalBiasWorld = gShadowOptions.y * gShadowOptions.z * (1.0f - nDotL);
|
||||||
|
const float3 shadowPositionWS = positionWS + resolvedNormalWS * normalBiasWorld;
|
||||||
|
const float4 shadowClip = mul(gWorldToShadowMatrix, float4(shadowPositionWS, 1.0f));
|
||||||
|
if (shadowClip.w <= 0.0f) {
|
||||||
|
return 1.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
const float3 shadowNdc = shadowClip.xyz / shadowClip.w;
|
||||||
|
#if UNITY_UV_STARTS_AT_TOP
|
||||||
|
const float shadowUvY = shadowNdc.y * -0.5f + 0.5f;
|
||||||
|
#else
|
||||||
|
const float shadowUvY = shadowNdc.y * 0.5f + 0.5f;
|
||||||
|
#endif
|
||||||
|
const float2 shadowUv = float2(shadowNdc.x * 0.5f + 0.5f, shadowUvY);
|
||||||
|
#if UNITY_NEAR_CLIP_VALUE < 0
|
||||||
|
if (shadowUv.x < 0.0f || shadowUv.x > 1.0f ||
|
||||||
|
shadowUv.y < 0.0f || shadowUv.y > 1.0f ||
|
||||||
|
shadowNdc.z < -1.0f || shadowNdc.z > 1.0f) {
|
||||||
|
return 1.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
const float receiverDepth = shadowNdc.z * 0.5f + 0.5f - gShadowBiasAndTexelSize.x;
|
||||||
|
#else
|
||||||
|
if (shadowUv.x < 0.0f || shadowUv.x > 1.0f ||
|
||||||
|
shadowUv.y < 0.0f || shadowUv.y > 1.0f ||
|
||||||
|
shadowNdc.z < 0.0f || shadowNdc.z > 1.0f) {
|
||||||
|
return 1.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
const float receiverDepth = shadowNdc.z - gShadowBiasAndTexelSize.x;
|
||||||
|
#endif
|
||||||
|
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 lerp(1.0f - shadowStrength, 1.0f, visibility);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
float ComputeRangeAttenuation(float distanceSq, float range)
|
||||||
|
{
|
||||||
|
if (range <= 0.0f) {
|
||||||
|
return 0.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
const float clampedRange = max(range, 0.0001f);
|
||||||
|
const float rangeSq = clampedRange * clampedRange;
|
||||||
|
if (distanceSq >= rangeSq) {
|
||||||
|
return 0.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
const float distance = sqrt(max(distanceSq, 0.0f));
|
||||||
|
const float normalized = saturate(1.0f - distance / clampedRange);
|
||||||
|
return normalized * normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
float ComputeSpotAttenuation(AdditionalLightData light, float3 directionToLightWS)
|
||||||
|
{
|
||||||
|
const float cosOuter = light.spotAnglesAndFlags.x;
|
||||||
|
const float cosInner = light.spotAnglesAndFlags.y;
|
||||||
|
const float3 spotAxisToLightWS = NormalizeSafe(light.directionAndType.xyz, float3(0.0f, -1.0f, 0.0f));
|
||||||
|
const float cosTheta = dot(spotAxisToLightWS, directionToLightWS);
|
||||||
|
return saturate((cosTheta - cosOuter) / max(cosInner - cosOuter, 1e-4f));
|
||||||
|
}
|
||||||
|
|
||||||
|
float3 EvaluateAdditionalLight(AdditionalLightData light, float3 normalWS, float3 positionWS)
|
||||||
|
{
|
||||||
|
const float lightType = light.directionAndType.w;
|
||||||
|
const float3 lightColor = light.colorAndIntensity.rgb;
|
||||||
|
const float lightIntensity = light.colorAndIntensity.w;
|
||||||
|
|
||||||
|
float3 directionToLightWS = float3(0.0f, 0.0f, 0.0f);
|
||||||
|
float attenuation = 1.0f;
|
||||||
|
|
||||||
|
if (lightType < 0.5f) {
|
||||||
|
directionToLightWS = NormalizeSafe(light.directionAndType.xyz, float3(0.0f, -1.0f, 0.0f));
|
||||||
|
} else {
|
||||||
|
const float3 lightVectorWS = light.positionAndRange.xyz - positionWS;
|
||||||
|
const float distanceSq = dot(lightVectorWS, lightVectorWS);
|
||||||
|
if (distanceSq <= 1e-6f) {
|
||||||
|
return float3(0.0f, 0.0f, 0.0f);
|
||||||
|
}
|
||||||
|
|
||||||
|
directionToLightWS = lightVectorWS * rsqrt(distanceSq);
|
||||||
|
attenuation = ComputeRangeAttenuation(distanceSq, light.positionAndRange.w);
|
||||||
|
if (attenuation <= 0.0f) {
|
||||||
|
return float3(0.0f, 0.0f, 0.0f);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lightType > 1.5f) {
|
||||||
|
attenuation *= ComputeSpotAttenuation(light, directionToLightWS);
|
||||||
|
if (attenuation <= 0.0f) {
|
||||||
|
return float3(0.0f, 0.0f, 0.0f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const float diffuse = saturate(dot(normalWS, directionToLightWS));
|
||||||
|
if (diffuse <= 0.0f) {
|
||||||
|
return float3(0.0f, 0.0f, 0.0f);
|
||||||
|
}
|
||||||
|
|
||||||
|
return lightColor * (diffuse * lightIntensity * attenuation);
|
||||||
|
}
|
||||||
|
|
||||||
|
float3 ComputeSurfaceNormalWS(PSInput input, float2 texcoord)
|
||||||
|
{
|
||||||
|
float3 normalWS = NormalizeSafe(input.normalWS, float3(0.0f, 1.0f, 0.0f));
|
||||||
|
if (!UseNormalMapFeature()) {
|
||||||
|
return normalWS;
|
||||||
|
}
|
||||||
|
|
||||||
|
const float3 tangentWS = NormalizeSafe(input.tangentWS, float3(1.0f, 0.0f, 0.0f));
|
||||||
|
const float3 bitangentWS = NormalizeSafe(input.bitangentWS, float3(0.0f, 0.0f, 1.0f));
|
||||||
|
const float3 normalTS = _NormalMap.Sample(LinearClampSampler, texcoord).xyz * 2.0f - 1.0f;
|
||||||
|
const float3 mappedNormalWS =
|
||||||
|
tangentWS * normalTS.x +
|
||||||
|
bitangentWS * normalTS.y +
|
||||||
|
normalWS * normalTS.z;
|
||||||
|
return NormalizeSafe(mappedNormalWS, normalWS);
|
||||||
|
}
|
||||||
|
|
||||||
|
float3 GetAdjustedMainLightDirection()
|
||||||
|
{
|
||||||
|
const float3 scaledDirection =
|
||||||
|
gMainLightDirectionAndIntensity.xyz * _LightDirectionMultiplier.xyz;
|
||||||
|
return NormalizeSafe(
|
||||||
|
scaledDirection,
|
||||||
|
NormalizeSafe(gMainLightDirectionAndIntensity.xyz, float3(0.0f, -1.0f, 0.0f)));
|
||||||
|
}
|
||||||
|
|
||||||
|
float ResolveMaterialSelector(float lightMapAlpha)
|
||||||
|
{
|
||||||
|
const float useCustomMaterialType = _UseCustomMaterialType.x > 0.5f ? 1.0f : 0.0f;
|
||||||
|
return lerp(lightMapAlpha, _CustomMaterialType.x, useCustomMaterialType);
|
||||||
|
}
|
||||||
|
|
||||||
|
float GetGenericShadow(float3 normalWS, float3 lightDirectionWS, float aoFactor)
|
||||||
|
{
|
||||||
|
const float ndotl = dot(normalWS, lightDirectionWS);
|
||||||
|
const float halfLambert = ndotl * 0.5f + 0.5f;
|
||||||
|
const float shadow = saturate(2.0f * halfLambert * aoFactor);
|
||||||
|
return lerp(shadow, 1.0f, step(0.9f, aoFactor));
|
||||||
|
}
|
||||||
|
|
||||||
|
float GetFaceShadow(PSInput input, float2 texcoord, float3 lightDirectionWS)
|
||||||
|
{
|
||||||
|
const float3 faceDirection = NormalizeSafe(
|
||||||
|
float3(_FaceDirection.x, 0.0f, _FaceDirection.z),
|
||||||
|
float3(0.0f, 0.0f, 1.0f));
|
||||||
|
const float3 flatLightDirection = NormalizeSafe(
|
||||||
|
float3(lightDirectionWS.x, 0.0f, lightDirectionWS.z),
|
||||||
|
float3(0.0f, 0.0f, 1.0f));
|
||||||
|
const float faceDotLight = dot(faceDirection, flatLightDirection);
|
||||||
|
const float faceCrossLight = cross(faceDirection, flatLightDirection).y;
|
||||||
|
|
||||||
|
float2 shadowUv = texcoord;
|
||||||
|
shadowUv.x = lerp(shadowUv.x, 1.0f - shadowUv.x, step(0.0f, faceCrossLight));
|
||||||
|
|
||||||
|
const float faceShadowMap = _FaceLightMap.Sample(LinearClampSampler, shadowUv).r;
|
||||||
|
const float faceShadow = step(-0.5f * faceDotLight + 0.5f + _FaceShadowOffset.x, faceShadowMap);
|
||||||
|
const float faceMask = _FaceShadow.Sample(LinearClampSampler, texcoord).a;
|
||||||
|
return lerp(faceShadow, 1.0f, faceMask);
|
||||||
|
}
|
||||||
|
|
||||||
|
float3 GetShadowColor(float shadow, float materialSelector)
|
||||||
|
{
|
||||||
|
float rampIndex = 4.0f;
|
||||||
|
rampIndex = lerp(rampIndex, 1.0f, step(0.2f, materialSelector));
|
||||||
|
rampIndex = lerp(rampIndex, 2.0f, step(0.4f, materialSelector));
|
||||||
|
rampIndex = lerp(rampIndex, 0.0f, step(0.6f, materialSelector));
|
||||||
|
rampIndex = lerp(rampIndex, 3.0f, step(0.8f, materialSelector));
|
||||||
|
|
||||||
|
const float rangeMin = 0.5f + _ShadowOffset.x - _ShadowSmoothness.x;
|
||||||
|
const float rangeMax = 0.5f + _ShadowOffset.x;
|
||||||
|
const float2 rampUv = float2(
|
||||||
|
smoothstep(rangeMin, rangeMax, shadow),
|
||||||
|
rampIndex / 10.0f + 0.5f * saturate(_IsDay.x) + 0.05f);
|
||||||
|
const float3 shadowRamp = _ShadowRamp.Sample(LinearClampSampler, rampUv).rgb;
|
||||||
|
|
||||||
|
float3 shadowColor =
|
||||||
|
shadowRamp * lerp(_ShadowColor.rgb, float3(1.0f, 1.0f, 1.0f), smoothstep(0.9f, 1.0f, rampUv.x));
|
||||||
|
shadowColor = lerp(shadowColor, float3(1.0f, 1.0f, 1.0f), step(rangeMax, shadow));
|
||||||
|
return shadowColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
float3 GetSpecular(
|
||||||
|
PSInput input,
|
||||||
|
float3 normalWS,
|
||||||
|
float3 lightDirectionWS,
|
||||||
|
float3 albedo,
|
||||||
|
float3 lightMap)
|
||||||
|
{
|
||||||
|
const float3 viewDirectionVS = NormalizeSafe(-input.positionVS, float3(0.0f, 0.0f, 1.0f));
|
||||||
|
const float3 lightDirectionVS = NormalizeSafe(
|
||||||
|
mul((float3x3)gViewMatrix, lightDirectionWS),
|
||||||
|
float3(0.0f, 0.0f, 1.0f));
|
||||||
|
const float3 normalVS = NormalizeSafe(mul((float3x3)gViewMatrix, normalWS), float3(0.0f, 0.0f, 1.0f));
|
||||||
|
const float3 halfDirectionVS = NormalizeSafe(lightDirectionVS + viewDirectionVS, lightDirectionVS);
|
||||||
|
const float nDotH = dot(normalVS, halfDirectionVS);
|
||||||
|
const float blinnPhong = pow(saturate(nDotH), max(_SpecularSmoothness.x, 1.0f));
|
||||||
|
|
||||||
|
const float2 matcapUv = normalVS.xy * 0.5f + 0.5f;
|
||||||
|
const float3 metalMap = _MetalMap.Sample(LinearClampSampler, matcapUv).rgb;
|
||||||
|
|
||||||
|
const float3 nonMetallic =
|
||||||
|
step(1.1f, lightMap.b + blinnPhong) * lightMap.r * _NonmetallicIntensity.x;
|
||||||
|
const float3 metallic =
|
||||||
|
blinnPhong * lightMap.b * albedo * metalMap * _MetallicIntensity.x;
|
||||||
|
return lerp(nonMetallic, metallic, step(0.9f, lightMap.r));
|
||||||
|
}
|
||||||
|
|
||||||
|
float GetRim(PSInput input, float3 normalWS)
|
||||||
|
{
|
||||||
|
const float3 viewDirectionVS = NormalizeSafe(-input.positionVS, float3(0.0f, 0.0f, 1.0f));
|
||||||
|
const float3 normalVS = NormalizeSafe(mul((float3x3)gViewMatrix, normalWS), float3(0.0f, 0.0f, 1.0f));
|
||||||
|
const float nDotV = dot(normalVS, viewDirectionVS);
|
||||||
|
const float fresnel = pow(saturate(1.0f - nDotV), max(_RimOffset.x, 0.001f));
|
||||||
|
return smoothstep(0.0f, max(_RimThreshold.x, 0.001f), fresnel) * _RimIntensity.x;
|
||||||
|
}
|
||||||
|
|
||||||
|
float4 MainPS(PSInput input, bool isFrontFace : SV_IsFrontFace) : SV_TARGET
|
||||||
|
{
|
||||||
|
const float2 texcoord = ResolveMaterialTexcoord(input, isFrontFace);
|
||||||
|
float4 baseMap = BaseColorTexture.Sample(LinearClampSampler, texcoord);
|
||||||
|
float3 albedo = baseMap.rgb * _BaseColor.rgb;
|
||||||
|
const float alphaMask = baseMap.a;
|
||||||
|
|
||||||
|
if (UseFaceFeature()) {
|
||||||
|
albedo = lerp(albedo, _FaceBlushColor.rgb, _FaceBlushStrength.x * alphaMask);
|
||||||
|
}
|
||||||
|
|
||||||
|
const float3 normalWS = ComputeSurfaceNormalWS(input, texcoord);
|
||||||
|
|
||||||
|
float3 color = albedo;
|
||||||
|
if (gMainLightColorAndFlags.a >= 0.5f) {
|
||||||
|
const float3 lightDirectionWS = GetAdjustedMainLightDirection();
|
||||||
|
const float4 lightMap = _LightMap.Sample(LinearClampSampler, texcoord);
|
||||||
|
const float materialSelector = ResolveMaterialSelector(lightMap.a);
|
||||||
|
float shadow = UseFaceFeature()
|
||||||
|
? GetFaceShadow(input, texcoord, lightDirectionWS)
|
||||||
|
: GetGenericShadow(normalWS, lightDirectionWS, lightMap.g * input.color.x);
|
||||||
|
shadow *= ComputeShadowAttenuation(input.positionWS, normalWS, lightDirectionWS);
|
||||||
|
|
||||||
|
const float3 shadowColor = GetShadowColor(shadow, materialSelector);
|
||||||
|
|
||||||
|
float3 specular = float3(0.0f, 0.0f, 0.0f);
|
||||||
|
if (UseSpecularFeature()) {
|
||||||
|
specular = GetSpecular(input, normalWS, lightDirectionWS, albedo, lightMap.rgb);
|
||||||
|
}
|
||||||
|
|
||||||
|
float3 emission = float3(0.0f, 0.0f, 0.0f);
|
||||||
|
if (UseEmissionFeature()) {
|
||||||
|
emission = albedo * _EmissionIntensity.x * alphaMask;
|
||||||
|
}
|
||||||
|
|
||||||
|
float3 rim = float3(0.0f, 0.0f, 0.0f);
|
||||||
|
if (UseRimFeature()) {
|
||||||
|
rim = albedo * GetRim(input, normalWS);
|
||||||
|
}
|
||||||
|
|
||||||
|
color = albedo * shadowColor + specular + emission + rim;
|
||||||
|
}
|
||||||
|
|
||||||
|
const int additionalLightCount = min((int)gLightingParams.x, XC_MAX_ADDITIONAL_LIGHTS);
|
||||||
|
[unroll]
|
||||||
|
for (int lightIndex = 0; lightIndex < XC_MAX_ADDITIONAL_LIGHTS; ++lightIndex) {
|
||||||
|
if (lightIndex >= additionalLightCount) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
color +=
|
||||||
|
albedo *
|
||||||
|
EvaluateAdditionalLight(gAdditionalLights[lightIndex], normalWS, input.positionWS) *
|
||||||
|
0.15f;
|
||||||
|
}
|
||||||
|
|
||||||
|
return float4(saturate(color), 1.0f);
|
||||||
|
}
|
||||||
|
ENDHLSL
|
||||||
|
SubShader
|
||||||
|
{
|
||||||
|
Cull Back
|
||||||
|
ZWrite On
|
||||||
|
ZTest LEqual
|
||||||
|
Pass
|
||||||
|
{
|
||||||
|
Name "ForwardLit"
|
||||||
|
Tags { "LightMode" = "ForwardLit" }
|
||||||
|
HLSLPROGRAM
|
||||||
|
#pragma target 4.5
|
||||||
|
#pragma vertex MainVS
|
||||||
|
#pragma fragment MainPS
|
||||||
|
#pragma multi_compile _ XC_MAIN_LIGHT_SHADOWS
|
||||||
|
#pragma shader_feature_local _ XC_ALPHA_TEST
|
||||||
|
#pragma shader_feature_local _ _EMISSION
|
||||||
|
#pragma shader_feature_local _ _NORMAL_MAP
|
||||||
|
#pragma shader_feature_local _ _IS_FACE
|
||||||
|
#pragma shader_feature_local _ _SPECULAR
|
||||||
|
#pragma shader_feature_local _ _RIM
|
||||||
|
ENDHLSL
|
||||||
|
}
|
||||||
|
UsePass "Builtin Depth Only/DepthOnly"
|
||||||
|
UsePass "Builtin Shadow Caster/ShadowCaster"
|
||||||
|
}
|
||||||
|
}
|
||||||
5
project/Assets/Shaders/XCCharacterToon.shader.meta
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
fileFormatVersion: 1
|
||||||
|
guid: 5a51f782e3c149bd8bf65c8b0e25d3a1
|
||||||
|
folderAsset: false
|
||||||
|
importer: ShaderImporter
|
||||||
|
importerVersion: 8
|
||||||
@@ -147,6 +147,48 @@ TEST(MeshFilterComponent_Test, SetMeshPathPreservesPathWithoutLoadedResource) {
|
|||||||
EXPECT_EQ(component.GetMesh(), nullptr);
|
EXPECT_EQ(component.GetMesh(), nullptr);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST(MeshFilterComponent_Test, SetMeshAssetRefPreservesProjectSubAssetReference) {
|
||||||
|
namespace fs = std::filesystem;
|
||||||
|
|
||||||
|
ResourceManager& manager = ResourceManager::Get();
|
||||||
|
manager.Initialize();
|
||||||
|
|
||||||
|
const fs::path projectRoot = fs::temp_directory_path() / "xc_mesh_filter_asset_ref_test";
|
||||||
|
const fs::path assetsDir = projectRoot / "Assets";
|
||||||
|
const fs::path meshPath = assetsDir / "runtime.mesh";
|
||||||
|
|
||||||
|
fs::remove_all(projectRoot);
|
||||||
|
fs::create_directories(assetsDir);
|
||||||
|
{
|
||||||
|
std::ofstream meshFile(meshPath);
|
||||||
|
ASSERT_TRUE(meshFile.is_open());
|
||||||
|
meshFile << "placeholder";
|
||||||
|
}
|
||||||
|
|
||||||
|
manager.SetResourceRoot(projectRoot.string().c_str());
|
||||||
|
|
||||||
|
AssetRef meshRef;
|
||||||
|
ASSERT_TRUE(manager.TryGetAssetRef("Assets/runtime.mesh", ResourceType::Mesh, meshRef));
|
||||||
|
|
||||||
|
MeshFilterComponent component;
|
||||||
|
component.SetMeshAssetRef(meshRef);
|
||||||
|
|
||||||
|
EXPECT_TRUE(component.GetMeshAssetRef().IsValid());
|
||||||
|
EXPECT_EQ(component.GetMeshAssetRef().assetGuid, meshRef.assetGuid);
|
||||||
|
EXPECT_EQ(component.GetMeshAssetRef().localID, meshRef.localID);
|
||||||
|
EXPECT_EQ(component.GetMeshAssetRef().resourceType, meshRef.resourceType);
|
||||||
|
EXPECT_EQ(component.GetMeshPath(), "Assets/runtime.mesh");
|
||||||
|
|
||||||
|
std::stringstream stream;
|
||||||
|
component.Serialize(stream);
|
||||||
|
EXPECT_NE(stream.str().find("meshRef="), std::string::npos);
|
||||||
|
EXPECT_EQ(stream.str().find("meshRef=;"), std::string::npos);
|
||||||
|
|
||||||
|
manager.SetResourceRoot("");
|
||||||
|
manager.Shutdown();
|
||||||
|
fs::remove_all(projectRoot);
|
||||||
|
}
|
||||||
|
|
||||||
TEST(MeshFilterComponent_Test, DeferredSceneDeserializeLoadsMeshAsyncByPath) {
|
TEST(MeshFilterComponent_Test, DeferredSceneDeserializeLoadsMeshAsyncByPath) {
|
||||||
ResourceManager& manager = ResourceManager::Get();
|
ResourceManager& manager = ResourceManager::Get();
|
||||||
manager.Initialize();
|
manager.Initialize();
|
||||||
@@ -276,6 +318,56 @@ TEST(MeshRendererComponent_Test, SetMaterialPathPreservesPathWithoutLoadedResour
|
|||||||
EXPECT_EQ(component.GetMaterial(1), nullptr);
|
EXPECT_EQ(component.GetMaterial(1), nullptr);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST(MeshRendererComponent_Test, SetMaterialAssetRefPreservesProjectSubAssetReference) {
|
||||||
|
namespace fs = std::filesystem;
|
||||||
|
|
||||||
|
ResourceManager& manager = ResourceManager::Get();
|
||||||
|
manager.Initialize();
|
||||||
|
|
||||||
|
const fs::path projectRoot = fs::temp_directory_path() / "xc_mesh_renderer_sub_asset_ref_test";
|
||||||
|
const fs::path assetsDir = projectRoot / "Assets";
|
||||||
|
const fs::path materialPath = assetsDir / "runtime.material";
|
||||||
|
|
||||||
|
fs::remove_all(projectRoot);
|
||||||
|
fs::create_directories(assetsDir);
|
||||||
|
|
||||||
|
{
|
||||||
|
std::ofstream materialFile(materialPath);
|
||||||
|
ASSERT_TRUE(materialFile.is_open());
|
||||||
|
materialFile << "{\n";
|
||||||
|
materialFile << " \"renderQueue\": \"geometry\",\n";
|
||||||
|
materialFile << " \"renderState\": {\n";
|
||||||
|
materialFile << " \"cull\": \"back\"\n";
|
||||||
|
materialFile << " }\n";
|
||||||
|
materialFile << "}";
|
||||||
|
}
|
||||||
|
|
||||||
|
manager.SetResourceRoot(projectRoot.string().c_str());
|
||||||
|
|
||||||
|
AssetRef materialRef;
|
||||||
|
ASSERT_TRUE(manager.TryGetAssetRef("Assets/runtime.material", ResourceType::Material, materialRef));
|
||||||
|
|
||||||
|
MeshRendererComponent component;
|
||||||
|
component.SetMaterialAssetRef(0, materialRef);
|
||||||
|
|
||||||
|
ASSERT_EQ(component.GetMaterialCount(), 1u);
|
||||||
|
EXPECT_TRUE(component.GetMaterialAssetRefs()[0].IsValid());
|
||||||
|
EXPECT_EQ(component.GetMaterialAssetRefs()[0].assetGuid, materialRef.assetGuid);
|
||||||
|
EXPECT_EQ(component.GetMaterialAssetRefs()[0].localID, materialRef.localID);
|
||||||
|
EXPECT_EQ(component.GetMaterialAssetRefs()[0].resourceType, materialRef.resourceType);
|
||||||
|
EXPECT_EQ(component.GetMaterialPath(0), "Assets/runtime.material");
|
||||||
|
ASSERT_NE(component.GetMaterial(0), nullptr);
|
||||||
|
|
||||||
|
std::stringstream stream;
|
||||||
|
component.Serialize(stream);
|
||||||
|
EXPECT_NE(stream.str().find("materialRefs="), std::string::npos);
|
||||||
|
EXPECT_EQ(stream.str().find("materialRefs=;"), std::string::npos);
|
||||||
|
|
||||||
|
manager.SetResourceRoot("");
|
||||||
|
manager.Shutdown();
|
||||||
|
fs::remove_all(projectRoot);
|
||||||
|
}
|
||||||
|
|
||||||
TEST(MeshRendererComponent_Test, DeserializeIgnoresPlainMaterialPathsWithoutAssetRefs) {
|
TEST(MeshRendererComponent_Test, DeserializeIgnoresPlainMaterialPathsWithoutAssetRefs) {
|
||||||
MeshRendererComponent target;
|
MeshRendererComponent target;
|
||||||
|
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ add_custom_target(rendering_integration_tests
|
|||||||
rendering_integration_offscreen_scene
|
rendering_integration_offscreen_scene
|
||||||
rendering_integration_skybox_scene
|
rendering_integration_skybox_scene
|
||||||
rendering_integration_post_process_scene
|
rendering_integration_post_process_scene
|
||||||
|
rendering_integration_nahida_preview_scene
|
||||||
)
|
)
|
||||||
|
|
||||||
add_custom_target(rendering_all_tests
|
add_custom_target(rendering_all_tests
|
||||||
|
|||||||
@@ -25,3 +25,4 @@ add_subdirectory(volume_scene)
|
|||||||
add_subdirectory(volume_occlusion_scene)
|
add_subdirectory(volume_occlusion_scene)
|
||||||
add_subdirectory(volume_transform_scene)
|
add_subdirectory(volume_transform_scene)
|
||||||
add_subdirectory(gaussian_splat_scene)
|
add_subdirectory(gaussian_splat_scene)
|
||||||
|
add_subdirectory(nahida_preview_scene)
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
cmake_minimum_required(VERSION 3.15)
|
||||||
|
|
||||||
|
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR})
|
||||||
|
|
||||||
|
project(rendering_integration_nahida_preview_scene)
|
||||||
|
|
||||||
|
set(ENGINE_ROOT_DIR ${CMAKE_SOURCE_DIR}/engine)
|
||||||
|
set(PACKAGE_DIR ${CMAKE_SOURCE_DIR}/mvs/OpenGL/package)
|
||||||
|
|
||||||
|
get_filename_component(PROJECT_ROOT_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../../../.. ABSOLUTE)
|
||||||
|
|
||||||
|
find_package(Vulkan QUIET)
|
||||||
|
|
||||||
|
add_executable(rendering_integration_nahida_preview_scene
|
||||||
|
main.cpp
|
||||||
|
${CMAKE_SOURCE_DIR}/tests/RHI/integration/fixtures/RHIIntegrationFixture.cpp
|
||||||
|
${PACKAGE_DIR}/src/glad.c
|
||||||
|
)
|
||||||
|
|
||||||
|
target_include_directories(rendering_integration_nahida_preview_scene PRIVATE
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}
|
||||||
|
${CMAKE_SOURCE_DIR}/tests/RHI/integration/fixtures
|
||||||
|
${ENGINE_ROOT_DIR}/include
|
||||||
|
${PACKAGE_DIR}/include
|
||||||
|
${PROJECT_ROOT_DIR}/engine/src
|
||||||
|
)
|
||||||
|
|
||||||
|
target_link_libraries(rendering_integration_nahida_preview_scene PRIVATE
|
||||||
|
d3d12
|
||||||
|
dxgi
|
||||||
|
d3dcompiler
|
||||||
|
winmm
|
||||||
|
opengl32
|
||||||
|
XCEngine
|
||||||
|
GTest::gtest
|
||||||
|
)
|
||||||
|
|
||||||
|
if(TARGET Vulkan::Vulkan)
|
||||||
|
target_link_libraries(rendering_integration_nahida_preview_scene PRIVATE Vulkan::Vulkan)
|
||||||
|
target_compile_definitions(rendering_integration_nahida_preview_scene PRIVATE XCENGINE_SUPPORT_VULKAN)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
target_compile_definitions(rendering_integration_nahida_preview_scene PRIVATE
|
||||||
|
UNICODE
|
||||||
|
_UNICODE
|
||||||
|
XCENGINE_SUPPORT_OPENGL
|
||||||
|
XCENGINE_SUPPORT_D3D12
|
||||||
|
)
|
||||||
|
|
||||||
|
add_custom_command(TARGET rendering_integration_nahida_preview_scene POST_BUILD
|
||||||
|
COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
||||||
|
${CMAKE_SOURCE_DIR}/tests/RHI/integration/compare_ppm.py
|
||||||
|
$<TARGET_FILE_DIR:rendering_integration_nahida_preview_scene>/
|
||||||
|
COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
||||||
|
${ENGINE_ROOT_DIR}/third_party/renderdoc/renderdoc.dll
|
||||||
|
$<TARGET_FILE_DIR:rendering_integration_nahida_preview_scene>/
|
||||||
|
COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
||||||
|
${ENGINE_ROOT_DIR}/third_party/assimp/bin/assimp-vc143-mt.dll
|
||||||
|
$<TARGET_FILE_DIR:rendering_integration_nahida_preview_scene>/
|
||||||
|
)
|
||||||
|
|
||||||
|
if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/GT.ppm)
|
||||||
|
add_custom_command(TARGET rendering_integration_nahida_preview_scene POST_BUILD
|
||||||
|
COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/GT.ppm
|
||||||
|
$<TARGET_FILE_DIR:rendering_integration_nahida_preview_scene>/GT.ppm
|
||||||
|
)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
include(GoogleTest)
|
||||||
|
gtest_discover_tests(rendering_integration_nahida_preview_scene)
|
||||||
@@ -185,7 +185,7 @@ void DumpMeshDiagnostics(const char* label, const Mesh* mesh) {
|
|||||||
|
|
||||||
const StaticMeshVertex& firstVertex = vertices[0];
|
const StaticMeshVertex& firstVertex = vertices[0];
|
||||||
std::printf(
|
std::printf(
|
||||||
"[NahidaDiag] %s firstVertex pos=(%.4f, %.4f, %.4f) normal=(%.4f, %.4f, %.4f) tangent=(%.4f, %.4f, %.4f) bitangent=(%.4f, %.4f, %.4f) uv=(%.4f, %.4f)\n",
|
"[NahidaDiag] %s firstVertex pos=(%.4f, %.4f, %.4f) normal=(%.4f, %.4f, %.4f) tangent=(%.4f, %.4f, %.4f) bitangent=(%.4f, %.4f, %.4f) uv0=(%.4f, %.4f) uv1=(%.4f, %.4f) color=(%.4f, %.4f, %.4f, %.4f)\n",
|
||||||
label,
|
label,
|
||||||
firstVertex.position.x,
|
firstVertex.position.x,
|
||||||
firstVertex.position.y,
|
firstVertex.position.y,
|
||||||
@@ -200,7 +200,13 @@ void DumpMeshDiagnostics(const char* label, const Mesh* mesh) {
|
|||||||
firstVertex.bitangent.y,
|
firstVertex.bitangent.y,
|
||||||
firstVertex.bitangent.z,
|
firstVertex.bitangent.z,
|
||||||
firstVertex.uv0.x,
|
firstVertex.uv0.x,
|
||||||
firstVertex.uv0.y);
|
firstVertex.uv0.y,
|
||||||
|
firstVertex.uv1.x,
|
||||||
|
firstVertex.uv1.y,
|
||||||
|
firstVertex.color.x,
|
||||||
|
firstVertex.color.y,
|
||||||
|
firstVertex.color.z,
|
||||||
|
firstVertex.color.w);
|
||||||
std::printf(
|
std::printf(
|
||||||
"[NahidaDiag] %s uvRange min=(%.4f, %.4f) max=(%.4f, %.4f)\n",
|
"[NahidaDiag] %s uvRange min=(%.4f, %.4f) max=(%.4f, %.4f)\n",
|
||||||
label,
|
label,
|
||||||
@@ -356,6 +362,16 @@ void NahidaPreviewSceneTest::SetUp() {
|
|||||||
ASSERT_NE(m_scene->Find("Face"), nullptr);
|
ASSERT_NE(m_scene->Find("Face"), nullptr);
|
||||||
|
|
||||||
PreloadSceneResources();
|
PreloadSceneResources();
|
||||||
|
|
||||||
|
auto* bodyMeshObject = m_scene->Find("Body_Mesh0");
|
||||||
|
ASSERT_NE(bodyMeshObject, nullptr);
|
||||||
|
auto* bodyMeshFilter = bodyMeshObject->GetComponent<MeshFilterComponent>();
|
||||||
|
ASSERT_NE(bodyMeshFilter, nullptr);
|
||||||
|
Mesh* bodyMesh = bodyMeshFilter->GetMesh();
|
||||||
|
ASSERT_NE(bodyMesh, nullptr);
|
||||||
|
EXPECT_TRUE(HasVertexAttribute(bodyMesh->GetVertexAttributes(), VertexAttribute::Color));
|
||||||
|
EXPECT_TRUE(HasVertexAttribute(bodyMesh->GetVertexAttributes(), VertexAttribute::UV1));
|
||||||
|
|
||||||
ApplyIsolationFilter();
|
ApplyIsolationFilter();
|
||||||
ApplyDiagnosticOverrides();
|
ApplyDiagnosticOverrides();
|
||||||
DumpTargetDiagnostics();
|
DumpTargetDiagnostics();
|
||||||
|
|||||||
@@ -135,7 +135,7 @@ private:
|
|||||||
TEST(BuiltinForwardPipeline_Test, UsesFloat3PositionInputLayoutForStaticMeshVertices) {
|
TEST(BuiltinForwardPipeline_Test, UsesFloat3PositionInputLayoutForStaticMeshVertices) {
|
||||||
const InputLayoutDesc inputLayout = BuiltinForwardPipeline::BuildInputLayout();
|
const InputLayoutDesc inputLayout = BuiltinForwardPipeline::BuildInputLayout();
|
||||||
|
|
||||||
ASSERT_EQ(inputLayout.elements.size(), 5u);
|
ASSERT_EQ(inputLayout.elements.size(), 7u);
|
||||||
|
|
||||||
const InputElementDesc& position = inputLayout.elements[0];
|
const InputElementDesc& position = inputLayout.elements[0];
|
||||||
EXPECT_EQ(position.semanticName, "POSITION");
|
EXPECT_EQ(position.semanticName, "POSITION");
|
||||||
@@ -158,19 +158,33 @@ TEST(BuiltinForwardPipeline_Test, UsesFloat3PositionInputLayoutForStaticMeshVert
|
|||||||
EXPECT_EQ(texcoord.inputSlot, 0u);
|
EXPECT_EQ(texcoord.inputSlot, 0u);
|
||||||
EXPECT_EQ(texcoord.alignedByteOffset, static_cast<uint32_t>(offsetof(StaticMeshVertex, uv0)));
|
EXPECT_EQ(texcoord.alignedByteOffset, static_cast<uint32_t>(offsetof(StaticMeshVertex, uv0)));
|
||||||
|
|
||||||
const InputElementDesc& tangent = inputLayout.elements[3];
|
const InputElementDesc& backTexcoord = inputLayout.elements[3];
|
||||||
|
EXPECT_EQ(backTexcoord.semanticName, "TEXCOORD");
|
||||||
|
EXPECT_EQ(backTexcoord.semanticIndex, 1u);
|
||||||
|
EXPECT_EQ(backTexcoord.format, static_cast<uint32_t>(Format::R32G32_Float));
|
||||||
|
EXPECT_EQ(backTexcoord.inputSlot, 0u);
|
||||||
|
EXPECT_EQ(backTexcoord.alignedByteOffset, static_cast<uint32_t>(offsetof(StaticMeshVertex, uv1)));
|
||||||
|
|
||||||
|
const InputElementDesc& tangent = inputLayout.elements[4];
|
||||||
EXPECT_EQ(tangent.semanticName, "TEXCOORD");
|
EXPECT_EQ(tangent.semanticName, "TEXCOORD");
|
||||||
EXPECT_EQ(tangent.semanticIndex, 1u);
|
EXPECT_EQ(tangent.semanticIndex, 2u);
|
||||||
EXPECT_EQ(tangent.format, static_cast<uint32_t>(Format::R32G32B32_Float));
|
EXPECT_EQ(tangent.format, static_cast<uint32_t>(Format::R32G32B32_Float));
|
||||||
EXPECT_EQ(tangent.inputSlot, 0u);
|
EXPECT_EQ(tangent.inputSlot, 0u);
|
||||||
EXPECT_EQ(tangent.alignedByteOffset, static_cast<uint32_t>(offsetof(StaticMeshVertex, tangent)));
|
EXPECT_EQ(tangent.alignedByteOffset, static_cast<uint32_t>(offsetof(StaticMeshVertex, tangent)));
|
||||||
|
|
||||||
const InputElementDesc& bitangent = inputLayout.elements[4];
|
const InputElementDesc& bitangent = inputLayout.elements[5];
|
||||||
EXPECT_EQ(bitangent.semanticName, "TEXCOORD");
|
EXPECT_EQ(bitangent.semanticName, "TEXCOORD");
|
||||||
EXPECT_EQ(bitangent.semanticIndex, 2u);
|
EXPECT_EQ(bitangent.semanticIndex, 3u);
|
||||||
EXPECT_EQ(bitangent.format, static_cast<uint32_t>(Format::R32G32B32_Float));
|
EXPECT_EQ(bitangent.format, static_cast<uint32_t>(Format::R32G32B32_Float));
|
||||||
EXPECT_EQ(bitangent.inputSlot, 0u);
|
EXPECT_EQ(bitangent.inputSlot, 0u);
|
||||||
EXPECT_EQ(bitangent.alignedByteOffset, static_cast<uint32_t>(offsetof(StaticMeshVertex, bitangent)));
|
EXPECT_EQ(bitangent.alignedByteOffset, static_cast<uint32_t>(offsetof(StaticMeshVertex, bitangent)));
|
||||||
|
|
||||||
|
const InputElementDesc& color = inputLayout.elements[6];
|
||||||
|
EXPECT_EQ(color.semanticName, "COLOR");
|
||||||
|
EXPECT_EQ(color.semanticIndex, 0u);
|
||||||
|
EXPECT_EQ(color.format, static_cast<uint32_t>(Format::R32G32B32A32_Float));
|
||||||
|
EXPECT_EQ(color.inputSlot, 0u);
|
||||||
|
EXPECT_EQ(color.alignedByteOffset, static_cast<uint32_t>(offsetof(StaticMeshVertex, color)));
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST(RenderSurfacePipelineUtils_Test, ResolvesContiguousSurfaceAttachmentFormatsIntoPipelineDesc) {
|
TEST(RenderSurfacePipelineUtils_Test, ResolvesContiguousSurfaceAttachmentFormatsIntoPipelineDesc) {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
#include <gtest/gtest.h>
|
#include <gtest/gtest.h>
|
||||||
#include <XCEngine/Core/Asset/AssetDatabase.h>
|
#include <XCEngine/Core/Asset/AssetDatabase.h>
|
||||||
|
#include <XCEngine/Core/Asset/ArtifactContainer.h>
|
||||||
#include <XCEngine/Core/Asset/ResourceManager.h>
|
#include <XCEngine/Core/Asset/ResourceManager.h>
|
||||||
#include <XCEngine/Resources/BuiltinResources.h>
|
#include <XCEngine/Resources/BuiltinResources.h>
|
||||||
#include <XCEngine/Resources/Material/MaterialLoader.h>
|
#include <XCEngine/Resources/Material/MaterialLoader.h>
|
||||||
@@ -304,13 +305,19 @@ TEST(MeshLoader, ProjectBackpackSampleArtifactRetainsSectionMaterialTextures) {
|
|||||||
AssetDatabase::ResolvedAsset resolvedAsset;
|
AssetDatabase::ResolvedAsset resolvedAsset;
|
||||||
ASSERT_TRUE(database.EnsureArtifact("Assets/Models/backpack/backpack.obj", ResourceType::Model, resolvedAsset));
|
ASSERT_TRUE(database.EnsureArtifact("Assets/Models/backpack/backpack.obj", ResourceType::Model, resolvedAsset));
|
||||||
ASSERT_TRUE(resolvedAsset.artifactReady);
|
ASSERT_TRUE(resolvedAsset.artifactReady);
|
||||||
EXPECT_EQ(fs::path(resolvedAsset.artifactMainPath.CStr()).filename().string(), "main.xcmodel");
|
EXPECT_EQ(fs::path(resolvedAsset.artifactMainPath.CStr()).extension().string(), ".xcmodel");
|
||||||
|
|
||||||
const fs::path meshArtifactPath = fs::path(resolvedAsset.artifactDirectory.CStr()) / "mesh_0.xcmesh";
|
const String meshArtifactPath =
|
||||||
ASSERT_TRUE(fs::exists(meshArtifactPath));
|
BuildArtifactContainerEntryPath(resolvedAsset.artifactMainPath, "mesh_0.xcmesh");
|
||||||
|
XCEngine::Containers::Array<XCEngine::Core::uint8> payload;
|
||||||
|
ASSERT_TRUE(ReadArtifactContainerEntryPayload(
|
||||||
|
resolvedAsset.artifactMainPath,
|
||||||
|
"mesh_0.xcmesh",
|
||||||
|
ResourceType::Mesh,
|
||||||
|
payload));
|
||||||
|
|
||||||
MeshLoader loader;
|
MeshLoader loader;
|
||||||
const LoadResult result = loader.Load(meshArtifactPath.string().c_str());
|
const LoadResult result = loader.Load(meshArtifactPath);
|
||||||
ASSERT_TRUE(result);
|
ASSERT_TRUE(result);
|
||||||
ASSERT_NE(result.resource, nullptr);
|
ASSERT_NE(result.resource, nullptr);
|
||||||
|
|
||||||
@@ -331,6 +338,60 @@ TEST(MeshLoader, ProjectBackpackSampleArtifactRetainsSectionMaterialTextures) {
|
|||||||
manager.Shutdown();
|
manager.Shutdown();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST(MeshLoader, ProjectNahidaSampleMarksFallbackUv1WhenSecondaryChannelIsAbsent) {
|
||||||
|
namespace fs = std::filesystem;
|
||||||
|
|
||||||
|
const fs::path repositoryRoot = GetRepositoryRoot();
|
||||||
|
const fs::path projectRoot = repositoryRoot / "project";
|
||||||
|
const fs::path nahidaModelPath =
|
||||||
|
projectRoot / "Assets" / "Characters" / "Nahida" / "Model" / "Avatar_Loli_Catalyst_Nahida.fbx";
|
||||||
|
const fs::path assimpDllPath = repositoryRoot / "engine" / "third_party" / "assimp" / "bin" / "assimp-vc143-mt.dll";
|
||||||
|
|
||||||
|
if (!fs::exists(nahidaModelPath)) {
|
||||||
|
GTEST_SKIP() << "Nahida sample model is not available in the local project fixture.";
|
||||||
|
}
|
||||||
|
|
||||||
|
ASSERT_TRUE(fs::exists(assimpDllPath));
|
||||||
|
|
||||||
|
#ifdef _WIN32
|
||||||
|
struct DllGuard {
|
||||||
|
HMODULE module = nullptr;
|
||||||
|
~DllGuard() {
|
||||||
|
if (module != nullptr) {
|
||||||
|
FreeLibrary(module);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} dllGuard;
|
||||||
|
dllGuard.module = LoadLibraryW(assimpDllPath.wstring().c_str());
|
||||||
|
ASSERT_NE(dllGuard.module, nullptr);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
ResourceManager& manager = ResourceManager::Get();
|
||||||
|
manager.Initialize();
|
||||||
|
manager.SetResourceRoot(projectRoot.string().c_str());
|
||||||
|
|
||||||
|
MeshLoader loader;
|
||||||
|
const LoadResult result = loader.Load(nahidaModelPath.string().c_str());
|
||||||
|
ASSERT_TRUE(result);
|
||||||
|
ASSERT_NE(result.resource, nullptr);
|
||||||
|
|
||||||
|
auto* mesh = static_cast<Mesh*>(result.resource);
|
||||||
|
ASSERT_NE(mesh, nullptr);
|
||||||
|
EXPECT_TRUE(HasVertexAttribute(mesh->GetVertexAttributes(), VertexAttribute::UV0));
|
||||||
|
EXPECT_TRUE(HasVertexAttribute(mesh->GetVertexAttributes(), VertexAttribute::UV1));
|
||||||
|
EXPECT_TRUE(HasVertexAttribute(mesh->GetVertexAttributes(), VertexAttribute::Color));
|
||||||
|
|
||||||
|
const auto* vertices = static_cast<const StaticMeshVertex*>(mesh->GetVertexData());
|
||||||
|
ASSERT_NE(vertices, nullptr);
|
||||||
|
ASSERT_GT(mesh->GetVertexCount(), 0u);
|
||||||
|
EXPECT_FLOAT_EQ(vertices[0].uv0.x, vertices[0].uv1.x);
|
||||||
|
EXPECT_FLOAT_EQ(vertices[0].uv0.y, vertices[0].uv1.y);
|
||||||
|
|
||||||
|
delete mesh;
|
||||||
|
manager.SetResourceRoot("");
|
||||||
|
manager.Shutdown();
|
||||||
|
}
|
||||||
|
|
||||||
TEST(MeshLoader, AssetDatabaseCreatesModelArtifactAndReusesItWithoutReimport) {
|
TEST(MeshLoader, AssetDatabaseCreatesModelArtifactAndReusesItWithoutReimport) {
|
||||||
namespace fs = std::filesystem;
|
namespace fs = std::filesystem;
|
||||||
using namespace std::chrono_literals;
|
using namespace std::chrono_literals;
|
||||||
@@ -378,22 +439,35 @@ TEST(MeshLoader, AssetDatabaseCreatesModelArtifactAndReusesItWithoutReimport) {
|
|||||||
ASSERT_TRUE(database.EnsureArtifact("Assets/textured_triangle.obj", ResourceType::Model, firstResolve));
|
ASSERT_TRUE(database.EnsureArtifact("Assets/textured_triangle.obj", ResourceType::Model, firstResolve));
|
||||||
ASSERT_TRUE(firstResolve.exists);
|
ASSERT_TRUE(firstResolve.exists);
|
||||||
ASSERT_TRUE(firstResolve.artifactReady);
|
ASSERT_TRUE(firstResolve.artifactReady);
|
||||||
EXPECT_EQ(fs::path(firstResolve.artifactMainPath.CStr()).filename().string(), "main.xcmodel");
|
EXPECT_EQ(fs::path(firstResolve.artifactMainPath.CStr()).extension().string(), ".xcmodel");
|
||||||
EXPECT_TRUE(fs::exists(projectRoot / "Assets" / "textured_triangle.obj.meta"));
|
EXPECT_TRUE(fs::exists(projectRoot / "Assets" / "textured_triangle.obj.meta"));
|
||||||
EXPECT_TRUE(fs::exists(projectRoot / "Library" / "SourceAssetDB" / "assets.db"));
|
EXPECT_TRUE(fs::exists(projectRoot / "Library" / "assets.db"));
|
||||||
EXPECT_TRUE(fs::exists(projectRoot / "Library" / "ArtifactDB" / "artifacts.db"));
|
EXPECT_TRUE(fs::exists(projectRoot / "Library" / "artifacts.db"));
|
||||||
EXPECT_TRUE(fs::exists(firstResolve.artifactMainPath.CStr()));
|
EXPECT_TRUE(fs::exists(firstResolve.artifactMainPath.CStr()));
|
||||||
EXPECT_TRUE(fs::exists(fs::path(firstResolve.artifactDirectory.CStr()) / "mesh_0.xcmesh"));
|
const String meshArtifactPath =
|
||||||
EXPECT_TRUE(fs::exists(
|
BuildArtifactContainerEntryPath(firstResolve.artifactMainPath, "mesh_0.xcmesh");
|
||||||
fs::path(firstResolve.artifactDirectory.CStr()) /
|
const String materialArtifactPath = BuildArtifactContainerEntryPath(
|
||||||
("material_" + std::to_string(sourceMaterialIndex) + ".xcmat")));
|
firstResolve.artifactMainPath,
|
||||||
EXPECT_TRUE(fs::exists((fs::path(firstResolve.artifactDirectory.CStr()) / "texture_0.xctex")));
|
String(("material_" + std::to_string(sourceMaterialIndex) + ".xcmat").c_str()));
|
||||||
|
XCEngine::Containers::Array<XCEngine::Core::uint8> artifactPayload;
|
||||||
|
EXPECT_TRUE(ReadArtifactContainerEntryPayload(
|
||||||
|
firstResolve.artifactMainPath,
|
||||||
|
"mesh_0.xcmesh",
|
||||||
|
ResourceType::Mesh,
|
||||||
|
artifactPayload));
|
||||||
|
EXPECT_TRUE(ReadArtifactContainerEntryPayload(
|
||||||
|
firstResolve.artifactMainPath,
|
||||||
|
String(("material_" + std::to_string(sourceMaterialIndex) + ".xcmat").c_str()),
|
||||||
|
ResourceType::Material,
|
||||||
|
artifactPayload));
|
||||||
|
EXPECT_TRUE(ReadArtifactContainerEntryPayload(
|
||||||
|
firstResolve.artifactMainPath,
|
||||||
|
"texture_0.xctex",
|
||||||
|
ResourceType::Texture,
|
||||||
|
artifactPayload));
|
||||||
|
|
||||||
MaterialLoader materialLoader;
|
MaterialLoader materialLoader;
|
||||||
LoadResult materialArtifactResult =
|
LoadResult materialArtifactResult = materialLoader.Load(materialArtifactPath);
|
||||||
materialLoader.Load(
|
|
||||||
(fs::path(firstResolve.artifactDirectory.CStr()) /
|
|
||||||
("material_" + std::to_string(sourceMaterialIndex) + ".xcmat")).string().c_str());
|
|
||||||
ASSERT_TRUE(materialArtifactResult);
|
ASSERT_TRUE(materialArtifactResult);
|
||||||
ASSERT_NE(materialArtifactResult.resource, nullptr);
|
ASSERT_NE(materialArtifactResult.resource, nullptr);
|
||||||
auto* artifactMaterial = static_cast<Material*>(materialArtifactResult.resource);
|
auto* artifactMaterial = static_cast<Material*>(materialArtifactResult.resource);
|
||||||
@@ -414,8 +488,7 @@ TEST(MeshLoader, AssetDatabaseCreatesModelArtifactAndReusesItWithoutReimport) {
|
|||||||
delete artifactMaterial;
|
delete artifactMaterial;
|
||||||
|
|
||||||
MeshLoader meshLoader;
|
MeshLoader meshLoader;
|
||||||
LoadResult meshArtifactResult =
|
LoadResult meshArtifactResult = meshLoader.Load(meshArtifactPath);
|
||||||
meshLoader.Load((fs::path(firstResolve.artifactDirectory.CStr()) / "mesh_0.xcmesh").string().c_str());
|
|
||||||
ASSERT_TRUE(meshArtifactResult);
|
ASSERT_TRUE(meshArtifactResult);
|
||||||
ASSERT_NE(meshArtifactResult.resource, nullptr);
|
ASSERT_NE(meshArtifactResult.resource, nullptr);
|
||||||
auto* artifactMesh = static_cast<Mesh*>(meshArtifactResult.resource);
|
auto* artifactMesh = static_cast<Mesh*>(meshArtifactResult.resource);
|
||||||
@@ -566,7 +639,13 @@ TEST(MeshLoader, ResourceManagerLoadsImportedMeshSubAssetByAssetRefFromProjectAs
|
|||||||
ASSERT_TRUE(meshAssetRef.IsValid());
|
ASSERT_TRUE(meshAssetRef.IsValid());
|
||||||
|
|
||||||
ASSERT_TRUE(manager.TryResolveAssetPath(meshAssetRef, resolvedMeshPath));
|
ASSERT_TRUE(manager.TryResolveAssetPath(meshAssetRef, resolvedMeshPath));
|
||||||
EXPECT_EQ(fs::path(resolvedMeshPath.CStr()).filename().string(), "mesh_0.xcmesh");
|
String resolvedContainerPath;
|
||||||
|
String resolvedEntryName;
|
||||||
|
ASSERT_TRUE(TryParseArtifactContainerEntryPath(
|
||||||
|
resolvedMeshPath,
|
||||||
|
resolvedContainerPath,
|
||||||
|
resolvedEntryName));
|
||||||
|
EXPECT_EQ(resolvedEntryName, "mesh_0.xcmesh");
|
||||||
}
|
}
|
||||||
|
|
||||||
manager.UnloadAll();
|
manager.UnloadAll();
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ set(MODEL_TEST_SOURCES
|
|||||||
test_model.cpp
|
test_model.cpp
|
||||||
test_model_loader.cpp
|
test_model_loader.cpp
|
||||||
test_model_import_pipeline.cpp
|
test_model_import_pipeline.cpp
|
||||||
|
test_model_scene_instantiation.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
add_executable(model_tests ${MODEL_TEST_SOURCES})
|
add_executable(model_tests ${MODEL_TEST_SOURCES})
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
#include <gtest/gtest.h>
|
#include <gtest/gtest.h>
|
||||||
|
|
||||||
#include <XCEngine/Core/Asset/AssetDatabase.h>
|
#include <XCEngine/Core/Asset/AssetDatabase.h>
|
||||||
|
#include <XCEngine/Core/Asset/ArtifactContainer.h>
|
||||||
#include <XCEngine/Core/Asset/ResourceManager.h>
|
#include <XCEngine/Core/Asset/ResourceManager.h>
|
||||||
#include <XCEngine/Resources/Mesh/Mesh.h>
|
#include <XCEngine/Resources/Mesh/Mesh.h>
|
||||||
#include <XCEngine/Resources/Mesh/MeshLoader.h>
|
#include <XCEngine/Resources/Mesh/MeshLoader.h>
|
||||||
@@ -85,11 +86,24 @@ TEST(ModelImportPipeline, AssetDatabaseImportsObjAsModelArtifact) {
|
|||||||
AssetDatabase::ResolvedAsset resolvedAsset;
|
AssetDatabase::ResolvedAsset resolvedAsset;
|
||||||
ASSERT_TRUE(database.EnsureArtifact("Assets/textured_triangle.obj", ResourceType::Model, resolvedAsset));
|
ASSERT_TRUE(database.EnsureArtifact("Assets/textured_triangle.obj", ResourceType::Model, resolvedAsset));
|
||||||
ASSERT_TRUE(resolvedAsset.artifactReady);
|
ASSERT_TRUE(resolvedAsset.artifactReady);
|
||||||
EXPECT_EQ(fs::path(resolvedAsset.artifactMainPath.CStr()).filename().string(), "main.xcmodel");
|
EXPECT_EQ(fs::path(resolvedAsset.artifactMainPath.CStr()).extension().string(), ".xcmodel");
|
||||||
EXPECT_TRUE(fs::exists(resolvedAsset.artifactMainPath.CStr()));
|
EXPECT_TRUE(fs::exists(resolvedAsset.artifactMainPath.CStr()));
|
||||||
EXPECT_TRUE(fs::exists(fs::path(resolvedAsset.artifactDirectory.CStr()) / "mesh_0.xcmesh"));
|
XCEngine::Containers::Array<XCEngine::Core::uint8> payload;
|
||||||
EXPECT_TRUE(fs::exists(fs::path(resolvedAsset.artifactDirectory.CStr()) / "material_0.xcmat"));
|
EXPECT_TRUE(ReadArtifactContainerEntryPayload(
|
||||||
EXPECT_TRUE(fs::exists(fs::path(resolvedAsset.artifactDirectory.CStr()) / "texture_0.xctex"));
|
resolvedAsset.artifactMainPath,
|
||||||
|
"mesh_0.xcmesh",
|
||||||
|
ResourceType::Mesh,
|
||||||
|
payload));
|
||||||
|
EXPECT_TRUE(ReadArtifactContainerEntryPayload(
|
||||||
|
resolvedAsset.artifactMainPath,
|
||||||
|
"material_0.xcmat",
|
||||||
|
ResourceType::Material,
|
||||||
|
payload));
|
||||||
|
EXPECT_TRUE(ReadArtifactContainerEntryPayload(
|
||||||
|
resolvedAsset.artifactMainPath,
|
||||||
|
"texture_0.xctex",
|
||||||
|
ResourceType::Texture,
|
||||||
|
payload));
|
||||||
|
|
||||||
ModelLoader modelLoader;
|
ModelLoader modelLoader;
|
||||||
const LoadResult modelResult = modelLoader.Load(resolvedAsset.artifactMainPath);
|
const LoadResult modelResult = modelLoader.Load(resolvedAsset.artifactMainPath);
|
||||||
@@ -109,7 +123,8 @@ TEST(ModelImportPipeline, AssetDatabaseImportsObjAsModelArtifact) {
|
|||||||
|
|
||||||
MeshLoader meshLoader;
|
MeshLoader meshLoader;
|
||||||
const LoadResult meshResult =
|
const LoadResult meshResult =
|
||||||
meshLoader.Load((fs::path(resolvedAsset.artifactDirectory.CStr()) / "mesh_0.xcmesh").string().c_str());
|
meshLoader.Load(
|
||||||
|
BuildArtifactContainerEntryPath(resolvedAsset.artifactMainPath, "mesh_0.xcmesh"));
|
||||||
ASSERT_TRUE(meshResult);
|
ASSERT_TRUE(meshResult);
|
||||||
ASSERT_NE(meshResult.resource, nullptr);
|
ASSERT_NE(meshResult.resource, nullptr);
|
||||||
|
|
||||||
@@ -182,4 +197,64 @@ TEST(ModelImportPipeline, ResourceManagerLoadsModelFromProjectAsset) {
|
|||||||
fs::remove_all(projectRoot);
|
fs::remove_all(projectRoot);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST(ModelImportPipeline, ProjectNahidaSampleArtifactPreservesFallbackUv1Semantic) {
|
||||||
|
namespace fs = std::filesystem;
|
||||||
|
|
||||||
|
const fs::path repositoryRoot = GetRepositoryRoot();
|
||||||
|
const fs::path projectRoot = repositoryRoot / "project";
|
||||||
|
const fs::path nahidaModelPath =
|
||||||
|
projectRoot / "Assets" / "Characters" / "Nahida" / "Model" / "Avatar_Loli_Catalyst_Nahida.fbx";
|
||||||
|
const fs::path assimpDllPath = repositoryRoot / "engine" / "third_party" / "assimp" / "bin" / "assimp-vc143-mt.dll";
|
||||||
|
|
||||||
|
if (!fs::exists(nahidaModelPath)) {
|
||||||
|
GTEST_SKIP() << "Nahida sample model is not available in the local project fixture.";
|
||||||
|
}
|
||||||
|
|
||||||
|
ASSERT_TRUE(fs::exists(assimpDllPath));
|
||||||
|
|
||||||
|
#ifdef _WIN32
|
||||||
|
AssimpDllGuard dllGuard;
|
||||||
|
dllGuard.module = LoadLibraryW(assimpDllPath.wstring().c_str());
|
||||||
|
ASSERT_NE(dllGuard.module, nullptr);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
ResourceManager& manager = ResourceManager::Get();
|
||||||
|
manager.Initialize();
|
||||||
|
manager.SetResourceRoot(projectRoot.string().c_str());
|
||||||
|
|
||||||
|
AssetDatabase database;
|
||||||
|
database.Initialize(projectRoot.string().c_str());
|
||||||
|
|
||||||
|
AssetDatabase::ResolvedAsset resolvedAsset;
|
||||||
|
ASSERT_TRUE(database.EnsureArtifact(
|
||||||
|
"Assets/Characters/Nahida/Model/Avatar_Loli_Catalyst_Nahida.fbx",
|
||||||
|
ResourceType::Model,
|
||||||
|
resolvedAsset));
|
||||||
|
ASSERT_TRUE(resolvedAsset.artifactReady);
|
||||||
|
EXPECT_EQ(fs::path(resolvedAsset.artifactMainPath.CStr()).extension().string(), ".xcmodel");
|
||||||
|
|
||||||
|
MeshLoader meshLoader;
|
||||||
|
const LoadResult meshResult =
|
||||||
|
meshLoader.Load(BuildArtifactContainerEntryPath(resolvedAsset.artifactMainPath, "mesh_0.xcmesh"));
|
||||||
|
ASSERT_TRUE(meshResult);
|
||||||
|
ASSERT_NE(meshResult.resource, nullptr);
|
||||||
|
|
||||||
|
auto* mesh = static_cast<Mesh*>(meshResult.resource);
|
||||||
|
ASSERT_NE(mesh, nullptr);
|
||||||
|
EXPECT_TRUE(HasVertexAttribute(mesh->GetVertexAttributes(), VertexAttribute::UV0));
|
||||||
|
EXPECT_TRUE(HasVertexAttribute(mesh->GetVertexAttributes(), VertexAttribute::UV1));
|
||||||
|
EXPECT_TRUE(HasVertexAttribute(mesh->GetVertexAttributes(), VertexAttribute::Color));
|
||||||
|
|
||||||
|
const auto* vertices = static_cast<const StaticMeshVertex*>(mesh->GetVertexData());
|
||||||
|
ASSERT_NE(vertices, nullptr);
|
||||||
|
ASSERT_GT(mesh->GetVertexCount(), 0u);
|
||||||
|
EXPECT_FLOAT_EQ(vertices[0].uv0.x, vertices[0].uv1.x);
|
||||||
|
EXPECT_FLOAT_EQ(vertices[0].uv0.y, vertices[0].uv1.y);
|
||||||
|
|
||||||
|
delete mesh;
|
||||||
|
database.Shutdown();
|
||||||
|
manager.SetResourceRoot("");
|
||||||
|
manager.Shutdown();
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|||||||
173
tests/Resources/Model/test_model_scene_instantiation.cpp
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
#include <gtest/gtest.h>
|
||||||
|
|
||||||
|
#include <XCEngine/Components/MeshFilterComponent.h>
|
||||||
|
#include <XCEngine/Components/MeshRendererComponent.h>
|
||||||
|
#include <XCEngine/Core/Asset/AssetRef.h>
|
||||||
|
#include <XCEngine/Core/Asset/ResourceManager.h>
|
||||||
|
#include <XCEngine/Resources/Model/Model.h>
|
||||||
|
#include <XCEngine/Scene/ModelSceneInstantiation.h>
|
||||||
|
#include <XCEngine/Scene/Scene.h>
|
||||||
|
|
||||||
|
#include <filesystem>
|
||||||
|
#include <fstream>
|
||||||
|
|
||||||
|
#ifdef _WIN32
|
||||||
|
#ifndef NOMINMAX
|
||||||
|
#define NOMINMAX
|
||||||
|
#endif
|
||||||
|
#include <windows.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
|
using namespace XCEngine;
|
||||||
|
using namespace XCEngine::Components;
|
||||||
|
using namespace XCEngine::Resources;
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
std::filesystem::path GetRepositoryRoot() {
|
||||||
|
return std::filesystem::path(__FILE__).parent_path().parent_path().parent_path().parent_path();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string GetMeshFixturePath(const char* fileName) {
|
||||||
|
return (std::filesystem::path(XCENGINE_TEST_FIXTURES_DIR) / "Resources" / "Mesh" / fileName).string();
|
||||||
|
}
|
||||||
|
|
||||||
|
void CopyTexturedTriangleFixture(const std::filesystem::path& assetsDir) {
|
||||||
|
namespace fs = std::filesystem;
|
||||||
|
|
||||||
|
fs::copy_file(
|
||||||
|
GetMeshFixturePath("textured_triangle.obj"),
|
||||||
|
assetsDir / "textured_triangle.obj",
|
||||||
|
fs::copy_options::overwrite_existing);
|
||||||
|
fs::copy_file(
|
||||||
|
GetMeshFixturePath("textured_triangle.mtl"),
|
||||||
|
assetsDir / "textured_triangle.mtl",
|
||||||
|
fs::copy_options::overwrite_existing);
|
||||||
|
fs::copy_file(
|
||||||
|
GetMeshFixturePath("checker.bmp"),
|
||||||
|
assetsDir / "checker.bmp",
|
||||||
|
fs::copy_options::overwrite_existing);
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef _WIN32
|
||||||
|
struct AssimpDllGuard {
|
||||||
|
HMODULE module = nullptr;
|
||||||
|
|
||||||
|
~AssimpDllGuard() {
|
||||||
|
if (module != nullptr) {
|
||||||
|
FreeLibrary(module);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
#endif
|
||||||
|
|
||||||
|
TEST(ModelSceneInstantiation, BuildsHierarchyFromManualModelGraph) {
|
||||||
|
Scene scene("Instantiation Scene");
|
||||||
|
Model model;
|
||||||
|
IResource::ConstructParams params = {};
|
||||||
|
params.name = "ManualModel";
|
||||||
|
params.path = "Assets/manual_model.fbx";
|
||||||
|
params.guid = ResourceGUID::Generate(params.path);
|
||||||
|
model.Initialize(params);
|
||||||
|
|
||||||
|
ModelNode rootNode;
|
||||||
|
rootNode.name = "Root";
|
||||||
|
rootNode.parentIndex = -1;
|
||||||
|
rootNode.localPosition = Math::Vector3(1.0f, 2.0f, 3.0f);
|
||||||
|
|
||||||
|
ModelNode childNode;
|
||||||
|
childNode.name = "Child";
|
||||||
|
childNode.parentIndex = 0;
|
||||||
|
childNode.localPosition = Math::Vector3(4.0f, 5.0f, 6.0f);
|
||||||
|
childNode.localScale = Math::Vector3(2.0f, 2.0f, 2.0f);
|
||||||
|
|
||||||
|
model.AddNode(rootNode);
|
||||||
|
model.AddNode(childNode);
|
||||||
|
model.SetRootNodeIndex(0u);
|
||||||
|
|
||||||
|
ModelSceneInstantiationResult result;
|
||||||
|
Containers::String errorMessage;
|
||||||
|
ASSERT_TRUE(InstantiateModelHierarchy(scene, model, AssetRef(), nullptr, &result, &errorMessage))
|
||||||
|
<< errorMessage.CStr();
|
||||||
|
|
||||||
|
ASSERT_NE(result.rootObject, nullptr);
|
||||||
|
EXPECT_EQ(result.rootObject->GetName(), "Root");
|
||||||
|
ASSERT_EQ(result.nodeObjects.size(), 2u);
|
||||||
|
ASSERT_NE(result.nodeObjects[1], nullptr);
|
||||||
|
EXPECT_EQ(result.nodeObjects[1]->GetParent(), result.rootObject);
|
||||||
|
EXPECT_EQ(result.rootObject->GetTransform()->GetLocalPosition(), Math::Vector3(1.0f, 2.0f, 3.0f));
|
||||||
|
EXPECT_EQ(result.nodeObjects[1]->GetTransform()->GetLocalPosition(), Math::Vector3(4.0f, 5.0f, 6.0f));
|
||||||
|
EXPECT_EQ(result.nodeObjects[1]->GetTransform()->GetLocalScale(), Math::Vector3(2.0f, 2.0f, 2.0f));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(ModelSceneInstantiation, RestoresMeshAndMaterialSubAssetRefsFromImportedModel) {
|
||||||
|
namespace fs = std::filesystem;
|
||||||
|
|
||||||
|
const fs::path projectRoot = fs::temp_directory_path() / "xc_model_scene_instantiation";
|
||||||
|
const fs::path assetsDir = projectRoot / "Assets";
|
||||||
|
fs::remove_all(projectRoot);
|
||||||
|
fs::create_directories(assetsDir);
|
||||||
|
CopyTexturedTriangleFixture(assetsDir);
|
||||||
|
|
||||||
|
#ifdef _WIN32
|
||||||
|
AssimpDllGuard dllGuard;
|
||||||
|
const fs::path assimpDllPath = GetRepositoryRoot() / "engine" / "third_party" / "assimp" / "bin" / "assimp-vc143-mt.dll";
|
||||||
|
ASSERT_TRUE(fs::exists(assimpDllPath));
|
||||||
|
dllGuard.module = LoadLibraryW(assimpDllPath.wstring().c_str());
|
||||||
|
ASSERT_NE(dllGuard.module, nullptr);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
ResourceManager& manager = ResourceManager::Get();
|
||||||
|
manager.Initialize();
|
||||||
|
manager.SetResourceRoot(projectRoot.string().c_str());
|
||||||
|
|
||||||
|
const auto modelHandle = manager.Load<Model>("Assets/textured_triangle.obj");
|
||||||
|
ASSERT_TRUE(modelHandle.IsValid());
|
||||||
|
|
||||||
|
AssetRef modelAssetRef;
|
||||||
|
ASSERT_TRUE(manager.TryGetAssetRef("Assets/textured_triangle.obj", ResourceType::Model, modelAssetRef));
|
||||||
|
|
||||||
|
Scene scene("Instantiation Scene");
|
||||||
|
ModelSceneInstantiationResult result;
|
||||||
|
Containers::String errorMessage;
|
||||||
|
ASSERT_TRUE(InstantiateModelHierarchy(scene, *modelHandle, modelAssetRef, nullptr, &result, &errorMessage))
|
||||||
|
<< errorMessage.CStr();
|
||||||
|
|
||||||
|
ASSERT_NE(result.rootObject, nullptr);
|
||||||
|
ASSERT_FALSE(result.meshObjects.empty());
|
||||||
|
auto* meshObject = result.meshObjects.front();
|
||||||
|
ASSERT_NE(meshObject, nullptr);
|
||||||
|
|
||||||
|
auto* meshFilter = meshObject->GetComponent<MeshFilterComponent>();
|
||||||
|
auto* meshRenderer = meshObject->GetComponent<MeshRendererComponent>();
|
||||||
|
ASSERT_NE(meshFilter, nullptr);
|
||||||
|
ASSERT_NE(meshRenderer, nullptr);
|
||||||
|
ASSERT_NE(meshFilter->GetMesh(), nullptr);
|
||||||
|
ASSERT_EQ(meshRenderer->GetMaterialCount(), 1u);
|
||||||
|
ASSERT_NE(meshRenderer->GetMaterial(0), nullptr);
|
||||||
|
|
||||||
|
const AssetRef& meshRef = meshFilter->GetMeshAssetRef();
|
||||||
|
EXPECT_TRUE(meshRef.IsValid());
|
||||||
|
EXPECT_EQ(meshRef.assetGuid, modelAssetRef.assetGuid);
|
||||||
|
EXPECT_EQ(meshRef.localID, modelHandle->GetMeshBindings()[0].meshLocalID);
|
||||||
|
EXPECT_EQ(meshRef.resourceType, ResourceType::Mesh);
|
||||||
|
|
||||||
|
ASSERT_EQ(meshRenderer->GetMaterialAssetRefs().size(), 1u);
|
||||||
|
const AssetRef& materialRef = meshRenderer->GetMaterialAssetRefs()[0];
|
||||||
|
EXPECT_TRUE(materialRef.IsValid());
|
||||||
|
EXPECT_EQ(materialRef.assetGuid, modelAssetRef.assetGuid);
|
||||||
|
EXPECT_EQ(materialRef.localID, modelHandle->GetMaterialBindings()[0].materialLocalID);
|
||||||
|
EXPECT_EQ(materialRef.resourceType, ResourceType::Material);
|
||||||
|
|
||||||
|
const std::string serializedScene = scene.SerializeToString();
|
||||||
|
EXPECT_NE(serializedScene.find("meshRef="), std::string::npos);
|
||||||
|
EXPECT_EQ(serializedScene.find("meshRef=;"), std::string::npos);
|
||||||
|
EXPECT_NE(serializedScene.find("materialRefs="), std::string::npos);
|
||||||
|
EXPECT_EQ(serializedScene.find("materialRefs=;"), std::string::npos);
|
||||||
|
|
||||||
|
manager.SetResourceRoot("");
|
||||||
|
manager.Shutdown();
|
||||||
|
fs::remove_all(projectRoot);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||