2026-04-10 20:55:48 +08:00
|
|
|
#include <gtest/gtest.h>
|
|
|
|
|
|
|
|
|
|
#include <XCEngine/Core/Asset/AssetDatabase.h>
|
2026-04-11 20:16:49 +08:00
|
|
|
#include <XCEngine/Core/Asset/ArtifactContainer.h>
|
2026-04-10 20:55:48 +08:00
|
|
|
#include <XCEngine/Core/Asset/ResourceManager.h>
|
|
|
|
|
#include <XCEngine/Resources/Mesh/Mesh.h>
|
|
|
|
|
#include <XCEngine/Resources/Mesh/MeshLoader.h>
|
|
|
|
|
#include <XCEngine/Resources/Model/Model.h>
|
|
|
|
|
#include <XCEngine/Resources/Model/ModelLoader.h>
|
|
|
|
|
|
|
|
|
|
#include <filesystem>
|
|
|
|
|
|
|
|
|
|
#ifdef _WIN32
|
|
|
|
|
#ifndef NOMINMAX
|
|
|
|
|
#define NOMINMAX
|
|
|
|
|
#endif
|
|
|
|
|
#include <windows.h>
|
|
|
|
|
#endif
|
|
|
|
|
|
|
|
|
|
using namespace XCEngine::Resources;
|
|
|
|
|
|
|
|
|
|
namespace {
|
|
|
|
|
|
|
|
|
|
std::string GetMeshFixturePath(const char* fileName) {
|
|
|
|
|
return (std::filesystem::path(XCENGINE_TEST_FIXTURES_DIR) / "Resources" / "Mesh" / fileName).string();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
std::filesystem::path GetRepositoryRoot() {
|
|
|
|
|
return std::filesystem::path(__FILE__).parent_path().parent_path().parent_path().parent_path();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void CopyTexturedTriangleFixture(const std::filesystem::path& assetsDir) {
|
|
|
|
|
namespace fs = std::filesystem;
|
|
|
|
|
|
|
|
|
|
fs::copy_file(
|
|
|
|
|
GetMeshFixturePath("textured_triangle.obj"),
|
|
|
|
|
assetsDir / "textured_triangle.obj",
|
|
|
|
|
fs::copy_options::overwrite_existing);
|
|
|
|
|
fs::copy_file(
|
|
|
|
|
GetMeshFixturePath("textured_triangle.mtl"),
|
|
|
|
|
assetsDir / "textured_triangle.mtl",
|
|
|
|
|
fs::copy_options::overwrite_existing);
|
|
|
|
|
fs::copy_file(
|
|
|
|
|
GetMeshFixturePath("checker.bmp"),
|
|
|
|
|
assetsDir / "checker.bmp",
|
|
|
|
|
fs::copy_options::overwrite_existing);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#ifdef _WIN32
|
|
|
|
|
struct AssimpDllGuard {
|
|
|
|
|
HMODULE module = nullptr;
|
|
|
|
|
|
|
|
|
|
~AssimpDllGuard() {
|
|
|
|
|
if (module != nullptr) {
|
|
|
|
|
FreeLibrary(module);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
#endif
|
|
|
|
|
|
|
|
|
|
TEST(ModelImportPipeline, AssetDatabaseImportsObjAsModelArtifact) {
|
|
|
|
|
namespace fs = std::filesystem;
|
|
|
|
|
|
|
|
|
|
const fs::path projectRoot = fs::temp_directory_path() / "xc_model_import_pipeline_asset_database";
|
|
|
|
|
const fs::path assetsDir = projectRoot / "Assets";
|
|
|
|
|
|
|
|
|
|
fs::remove_all(projectRoot);
|
|
|
|
|
fs::create_directories(assetsDir);
|
|
|
|
|
CopyTexturedTriangleFixture(assetsDir);
|
|
|
|
|
|
|
|
|
|
#ifdef _WIN32
|
|
|
|
|
AssimpDllGuard dllGuard;
|
|
|
|
|
const fs::path assimpDllPath = GetRepositoryRoot() / "engine" / "third_party" / "assimp" / "bin" / "assimp-vc143-mt.dll";
|
|
|
|
|
ASSERT_TRUE(fs::exists(assimpDllPath));
|
|
|
|
|
dllGuard.module = LoadLibraryW(assimpDllPath.wstring().c_str());
|
|
|
|
|
ASSERT_NE(dllGuard.module, nullptr);
|
|
|
|
|
#endif
|
|
|
|
|
|
|
|
|
|
AssetDatabase database;
|
|
|
|
|
database.Initialize(projectRoot.string().c_str());
|
|
|
|
|
|
|
|
|
|
ResourceType importType = ResourceType::Unknown;
|
|
|
|
|
ASSERT_TRUE(database.TryGetImportableResourceType("Assets/textured_triangle.obj", importType));
|
|
|
|
|
EXPECT_EQ(importType, ResourceType::Model);
|
|
|
|
|
|
|
|
|
|
AssetDatabase::ResolvedAsset resolvedAsset;
|
|
|
|
|
ASSERT_TRUE(database.EnsureArtifact("Assets/textured_triangle.obj", ResourceType::Model, resolvedAsset));
|
|
|
|
|
ASSERT_TRUE(resolvedAsset.artifactReady);
|
2026-04-11 20:16:49 +08:00
|
|
|
EXPECT_EQ(fs::path(resolvedAsset.artifactMainPath.CStr()).extension().string(), ".xcmodel");
|
2026-04-10 20:55:48 +08:00
|
|
|
EXPECT_TRUE(fs::exists(resolvedAsset.artifactMainPath.CStr()));
|
2026-04-11 20:16:49 +08:00
|
|
|
XCEngine::Containers::Array<XCEngine::Core::uint8> payload;
|
|
|
|
|
EXPECT_TRUE(ReadArtifactContainerEntryPayload(
|
|
|
|
|
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));
|
2026-04-10 20:55:48 +08:00
|
|
|
|
|
|
|
|
ModelLoader modelLoader;
|
|
|
|
|
const LoadResult modelResult = modelLoader.Load(resolvedAsset.artifactMainPath);
|
|
|
|
|
ASSERT_TRUE(modelResult);
|
|
|
|
|
ASSERT_NE(modelResult.resource, nullptr);
|
|
|
|
|
|
|
|
|
|
auto* model = static_cast<Model*>(modelResult.resource);
|
|
|
|
|
ASSERT_NE(model, nullptr);
|
|
|
|
|
EXPECT_TRUE(model->HasRootNode());
|
|
|
|
|
EXPECT_GE(model->GetNodes().Size(), 1u);
|
|
|
|
|
EXPECT_EQ(model->GetRootNodeIndex(), 0u);
|
|
|
|
|
EXPECT_EQ(model->GetMeshBindings().Size(), 1u);
|
|
|
|
|
EXPECT_EQ(model->GetMaterialBindings().Size(), 1u);
|
|
|
|
|
EXPECT_NE(model->GetMeshBindings()[0].meshLocalID, kInvalidLocalID);
|
|
|
|
|
EXPECT_NE(model->GetMaterialBindings()[0].materialLocalID, kInvalidLocalID);
|
|
|
|
|
delete model;
|
|
|
|
|
|
|
|
|
|
MeshLoader meshLoader;
|
|
|
|
|
const LoadResult meshResult =
|
2026-04-11 20:16:49 +08:00
|
|
|
meshLoader.Load(
|
|
|
|
|
BuildArtifactContainerEntryPath(resolvedAsset.artifactMainPath, "mesh_0.xcmesh"));
|
2026-04-10 20:55:48 +08:00
|
|
|
ASSERT_TRUE(meshResult);
|
|
|
|
|
ASSERT_NE(meshResult.resource, nullptr);
|
|
|
|
|
|
|
|
|
|
auto* mesh = static_cast<Mesh*>(meshResult.resource);
|
|
|
|
|
ASSERT_NE(mesh, nullptr);
|
|
|
|
|
EXPECT_EQ(mesh->GetVertexCount(), 3u);
|
|
|
|
|
EXPECT_EQ(mesh->GetIndexCount(), 3u);
|
|
|
|
|
EXPECT_EQ(mesh->GetSections().Size(), 1u);
|
|
|
|
|
EXPECT_EQ(mesh->GetMaterials().Size(), 1u);
|
|
|
|
|
EXPECT_NE(mesh->GetMaterial(0), nullptr);
|
|
|
|
|
delete mesh;
|
|
|
|
|
|
|
|
|
|
database.Shutdown();
|
|
|
|
|
fs::remove_all(projectRoot);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
TEST(ModelImportPipeline, ResourceManagerLoadsModelFromProjectAsset) {
|
|
|
|
|
namespace fs = std::filesystem;
|
|
|
|
|
|
|
|
|
|
const fs::path projectRoot = fs::temp_directory_path() / "xc_model_import_pipeline_resource_manager";
|
|
|
|
|
const fs::path assetsDir = projectRoot / "Assets";
|
|
|
|
|
|
|
|
|
|
fs::remove_all(projectRoot);
|
|
|
|
|
fs::create_directories(assetsDir);
|
|
|
|
|
CopyTexturedTriangleFixture(assetsDir);
|
|
|
|
|
|
|
|
|
|
#ifdef _WIN32
|
|
|
|
|
AssimpDllGuard dllGuard;
|
|
|
|
|
const fs::path assimpDllPath = GetRepositoryRoot() / "engine" / "third_party" / "assimp" / "bin" / "assimp-vc143-mt.dll";
|
|
|
|
|
ASSERT_TRUE(fs::exists(assimpDllPath));
|
|
|
|
|
dllGuard.module = LoadLibraryW(assimpDllPath.wstring().c_str());
|
|
|
|
|
ASSERT_NE(dllGuard.module, nullptr);
|
|
|
|
|
#endif
|
|
|
|
|
|
|
|
|
|
ResourceManager& manager = ResourceManager::Get();
|
|
|
|
|
manager.Initialize();
|
|
|
|
|
manager.SetResourceRoot(projectRoot.string().c_str());
|
|
|
|
|
|
|
|
|
|
{
|
|
|
|
|
const auto modelHandle = manager.Load<Model>("Assets/textured_triangle.obj");
|
|
|
|
|
ASSERT_TRUE(modelHandle.IsValid());
|
|
|
|
|
EXPECT_EQ(modelHandle->GetPath(), "Assets/textured_triangle.obj");
|
|
|
|
|
EXPECT_TRUE(modelHandle->HasRootNode());
|
|
|
|
|
EXPECT_EQ(modelHandle->GetMeshBindings().Size(), 1u);
|
|
|
|
|
EXPECT_EQ(modelHandle->GetMaterialBindings().Size(), 1u);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
{
|
|
|
|
|
const auto meshHandle = manager.Load<Mesh>("Assets/textured_triangle.obj");
|
|
|
|
|
ASSERT_TRUE(meshHandle.IsValid());
|
|
|
|
|
EXPECT_EQ(meshHandle->GetVertexCount(), 3u);
|
|
|
|
|
EXPECT_EQ(meshHandle->GetIndexCount(), 3u);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
AssetRef modelAssetRef;
|
|
|
|
|
ASSERT_TRUE(manager.TryGetAssetRef("Assets/textured_triangle.obj", ResourceType::Model, modelAssetRef));
|
|
|
|
|
EXPECT_TRUE(modelAssetRef.IsValid());
|
|
|
|
|
|
|
|
|
|
manager.UnloadAll();
|
|
|
|
|
|
|
|
|
|
{
|
|
|
|
|
const auto modelHandle = manager.Load<Model>(modelAssetRef);
|
|
|
|
|
ASSERT_TRUE(modelHandle.IsValid());
|
|
|
|
|
EXPECT_EQ(modelHandle->GetMeshBindings().Size(), 1u);
|
|
|
|
|
EXPECT_EQ(modelHandle->GetMaterialBindings().Size(), 1u);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
manager.SetResourceRoot("");
|
|
|
|
|
manager.Shutdown();
|
|
|
|
|
fs::remove_all(projectRoot);
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-11 20:16:49 +08:00
|
|
|
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();
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-10 20:55:48 +08:00
|
|
|
} // namespace
|