Add model and GaussianSplat asset pipelines
This commit is contained in:
42
tests/Resources/Model/CMakeLists.txt
Normal file
42
tests/Resources/Model/CMakeLists.txt
Normal file
@@ -0,0 +1,42 @@
|
||||
# ============================================================
|
||||
# Model Tests
|
||||
# ============================================================
|
||||
|
||||
set(MODEL_TEST_SOURCES
|
||||
test_model.cpp
|
||||
test_model_loader.cpp
|
||||
test_model_import_pipeline.cpp
|
||||
)
|
||||
|
||||
add_executable(model_tests ${MODEL_TEST_SOURCES})
|
||||
|
||||
if(MSVC)
|
||||
set_target_properties(model_tests PROPERTIES
|
||||
LINK_FLAGS "/NODEFAULTLIB:libcpmt.lib /NODEFAULTLIB:libcmt.lib"
|
||||
)
|
||||
endif()
|
||||
|
||||
target_link_libraries(model_tests
|
||||
PRIVATE
|
||||
XCEngine
|
||||
GTest::gtest
|
||||
GTest::gtest_main
|
||||
)
|
||||
|
||||
target_include_directories(model_tests PRIVATE
|
||||
${CMAKE_SOURCE_DIR}/engine/include
|
||||
${CMAKE_SOURCE_DIR}/tests/Fixtures
|
||||
)
|
||||
|
||||
target_compile_definitions(model_tests PRIVATE
|
||||
XCENGINE_TEST_FIXTURES_DIR="${CMAKE_SOURCE_DIR}/tests/Fixtures"
|
||||
)
|
||||
|
||||
add_custom_command(TARGET model_tests POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
||||
${CMAKE_SOURCE_DIR}/engine/third_party/assimp/bin/assimp-vc143-mt.dll
|
||||
$<TARGET_FILE_DIR:model_tests>/assimp-vc143-mt.dll
|
||||
)
|
||||
|
||||
include(GoogleTest)
|
||||
gtest_discover_tests(model_tests)
|
||||
97
tests/Resources/Model/test_model.cpp
Normal file
97
tests/Resources/Model/test_model.cpp
Normal file
@@ -0,0 +1,97 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <XCEngine/Resources/Model/Model.h>
|
||||
|
||||
using namespace XCEngine::Resources;
|
||||
using namespace XCEngine::Math;
|
||||
|
||||
namespace {
|
||||
|
||||
TEST(Model, DefaultConstructor) {
|
||||
Model model;
|
||||
EXPECT_EQ(model.GetType(), ResourceType::Model);
|
||||
EXPECT_FALSE(model.HasRootNode());
|
||||
EXPECT_EQ(model.GetRootNodeIndex(), kInvalidModelNodeIndex);
|
||||
EXPECT_TRUE(model.GetNodes().Empty());
|
||||
EXPECT_TRUE(model.GetMeshBindings().Empty());
|
||||
EXPECT_TRUE(model.GetMaterialBindings().Empty());
|
||||
EXPECT_EQ(model.GetMemorySize(), 0u);
|
||||
}
|
||||
|
||||
TEST(Model, AddGraphDataUpdatesState) {
|
||||
Model model;
|
||||
|
||||
IResource::ConstructParams params;
|
||||
params.name = "robot.fbx";
|
||||
params.path = "Library/Artifacts/aa/main.xcmodel";
|
||||
params.guid = ResourceGUID::Generate(params.path);
|
||||
model.Initialize(params);
|
||||
|
||||
model.SetRootNodeIndex(0u);
|
||||
|
||||
ModelNode rootNode;
|
||||
rootNode.name = "Root";
|
||||
rootNode.parentIndex = -1;
|
||||
rootNode.meshBindingStart = 0u;
|
||||
rootNode.meshBindingCount = 1u;
|
||||
rootNode.localPosition = Vector3(1.0f, 2.0f, 3.0f);
|
||||
rootNode.localScale = Vector3(1.0f, 1.5f, 2.0f);
|
||||
model.AddNode(rootNode);
|
||||
|
||||
ModelMeshBinding meshBinding;
|
||||
meshBinding.meshLocalID = 11u;
|
||||
meshBinding.materialBindingStart = 0u;
|
||||
meshBinding.materialBindingCount = 2u;
|
||||
model.AddMeshBinding(meshBinding);
|
||||
|
||||
ModelMaterialBinding materialBinding0;
|
||||
materialBinding0.slotIndex = 0u;
|
||||
materialBinding0.materialLocalID = 21u;
|
||||
model.AddMaterialBinding(materialBinding0);
|
||||
|
||||
ModelMaterialBinding materialBinding1;
|
||||
materialBinding1.slotIndex = 1u;
|
||||
materialBinding1.materialLocalID = 22u;
|
||||
model.AddMaterialBinding(materialBinding1);
|
||||
|
||||
ASSERT_TRUE(model.HasRootNode());
|
||||
EXPECT_EQ(model.GetRootNodeIndex(), 0u);
|
||||
ASSERT_EQ(model.GetNodes().Size(), 1u);
|
||||
EXPECT_EQ(model.GetNodes()[0].name, "Root");
|
||||
EXPECT_EQ(model.GetNodes()[0].localPosition, Vector3(1.0f, 2.0f, 3.0f));
|
||||
EXPECT_EQ(model.GetNodes()[0].localScale, Vector3(1.0f, 1.5f, 2.0f));
|
||||
ASSERT_EQ(model.GetMeshBindings().Size(), 1u);
|
||||
EXPECT_EQ(model.GetMeshBindings()[0].meshLocalID, 11u);
|
||||
ASSERT_EQ(model.GetMaterialBindings().Size(), 2u);
|
||||
EXPECT_EQ(model.GetMaterialBindings()[1].materialLocalID, 22u);
|
||||
EXPECT_GT(model.GetMemorySize(), 0u);
|
||||
}
|
||||
|
||||
TEST(Model, ReleaseClearsGraphData) {
|
||||
Model model;
|
||||
|
||||
IResource::ConstructParams params;
|
||||
params.name = "robot.fbx";
|
||||
params.path = "Library/Artifacts/aa/main.xcmodel";
|
||||
params.guid = ResourceGUID::Generate(params.path);
|
||||
model.Initialize(params);
|
||||
|
||||
model.SetRootNodeIndex(0u);
|
||||
|
||||
ModelNode node;
|
||||
node.name = "Root";
|
||||
model.AddNode(node);
|
||||
model.AddMeshBinding(ModelMeshBinding{ 3u, 0u, 0u });
|
||||
model.AddMaterialBinding(ModelMaterialBinding{ 0u, 7u });
|
||||
|
||||
model.Release();
|
||||
|
||||
EXPECT_FALSE(model.IsValid());
|
||||
EXPECT_FALSE(model.HasRootNode());
|
||||
EXPECT_TRUE(model.GetNodes().Empty());
|
||||
EXPECT_TRUE(model.GetMeshBindings().Empty());
|
||||
EXPECT_TRUE(model.GetMaterialBindings().Empty());
|
||||
EXPECT_EQ(model.GetMemorySize(), 0u);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
185
tests/Resources/Model/test_model_import_pipeline.cpp
Normal file
185
tests/Resources/Model/test_model_import_pipeline.cpp
Normal file
@@ -0,0 +1,185 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <XCEngine/Core/Asset/AssetDatabase.h>
|
||||
#include <XCEngine/Core/Asset/ResourceManager.h>
|
||||
#include <XCEngine/Resources/Mesh/Mesh.h>
|
||||
#include <XCEngine/Resources/Mesh/MeshLoader.h>
|
||||
#include <XCEngine/Resources/Model/Model.h>
|
||||
#include <XCEngine/Resources/Model/ModelLoader.h>
|
||||
|
||||
#include <filesystem>
|
||||
|
||||
#ifdef _WIN32
|
||||
#ifndef NOMINMAX
|
||||
#define NOMINMAX
|
||||
#endif
|
||||
#include <windows.h>
|
||||
#endif
|
||||
|
||||
using namespace XCEngine::Resources;
|
||||
|
||||
namespace {
|
||||
|
||||
std::string GetMeshFixturePath(const char* fileName) {
|
||||
return (std::filesystem::path(XCENGINE_TEST_FIXTURES_DIR) / "Resources" / "Mesh" / fileName).string();
|
||||
}
|
||||
|
||||
std::filesystem::path GetRepositoryRoot() {
|
||||
return std::filesystem::path(__FILE__).parent_path().parent_path().parent_path().parent_path();
|
||||
}
|
||||
|
||||
void CopyTexturedTriangleFixture(const std::filesystem::path& assetsDir) {
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
fs::copy_file(
|
||||
GetMeshFixturePath("textured_triangle.obj"),
|
||||
assetsDir / "textured_triangle.obj",
|
||||
fs::copy_options::overwrite_existing);
|
||||
fs::copy_file(
|
||||
GetMeshFixturePath("textured_triangle.mtl"),
|
||||
assetsDir / "textured_triangle.mtl",
|
||||
fs::copy_options::overwrite_existing);
|
||||
fs::copy_file(
|
||||
GetMeshFixturePath("checker.bmp"),
|
||||
assetsDir / "checker.bmp",
|
||||
fs::copy_options::overwrite_existing);
|
||||
}
|
||||
|
||||
#ifdef _WIN32
|
||||
struct AssimpDllGuard {
|
||||
HMODULE module = nullptr;
|
||||
|
||||
~AssimpDllGuard() {
|
||||
if (module != nullptr) {
|
||||
FreeLibrary(module);
|
||||
}
|
||||
}
|
||||
};
|
||||
#endif
|
||||
|
||||
TEST(ModelImportPipeline, AssetDatabaseImportsObjAsModelArtifact) {
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
const fs::path projectRoot = fs::temp_directory_path() / "xc_model_import_pipeline_asset_database";
|
||||
const fs::path assetsDir = projectRoot / "Assets";
|
||||
|
||||
fs::remove_all(projectRoot);
|
||||
fs::create_directories(assetsDir);
|
||||
CopyTexturedTriangleFixture(assetsDir);
|
||||
|
||||
#ifdef _WIN32
|
||||
AssimpDllGuard dllGuard;
|
||||
const fs::path assimpDllPath = GetRepositoryRoot() / "engine" / "third_party" / "assimp" / "bin" / "assimp-vc143-mt.dll";
|
||||
ASSERT_TRUE(fs::exists(assimpDllPath));
|
||||
dllGuard.module = LoadLibraryW(assimpDllPath.wstring().c_str());
|
||||
ASSERT_NE(dllGuard.module, nullptr);
|
||||
#endif
|
||||
|
||||
AssetDatabase database;
|
||||
database.Initialize(projectRoot.string().c_str());
|
||||
|
||||
ResourceType importType = ResourceType::Unknown;
|
||||
ASSERT_TRUE(database.TryGetImportableResourceType("Assets/textured_triangle.obj", importType));
|
||||
EXPECT_EQ(importType, ResourceType::Model);
|
||||
|
||||
AssetDatabase::ResolvedAsset resolvedAsset;
|
||||
ASSERT_TRUE(database.EnsureArtifact("Assets/textured_triangle.obj", ResourceType::Model, resolvedAsset));
|
||||
ASSERT_TRUE(resolvedAsset.artifactReady);
|
||||
EXPECT_EQ(fs::path(resolvedAsset.artifactMainPath.CStr()).filename().string(), "main.xcmodel");
|
||||
EXPECT_TRUE(fs::exists(resolvedAsset.artifactMainPath.CStr()));
|
||||
EXPECT_TRUE(fs::exists(fs::path(resolvedAsset.artifactDirectory.CStr()) / "mesh_0.xcmesh"));
|
||||
EXPECT_TRUE(fs::exists(fs::path(resolvedAsset.artifactDirectory.CStr()) / "material_0.xcmat"));
|
||||
EXPECT_TRUE(fs::exists(fs::path(resolvedAsset.artifactDirectory.CStr()) / "texture_0.xctex"));
|
||||
|
||||
ModelLoader modelLoader;
|
||||
const LoadResult modelResult = modelLoader.Load(resolvedAsset.artifactMainPath);
|
||||
ASSERT_TRUE(modelResult);
|
||||
ASSERT_NE(modelResult.resource, nullptr);
|
||||
|
||||
auto* model = static_cast<Model*>(modelResult.resource);
|
||||
ASSERT_NE(model, nullptr);
|
||||
EXPECT_TRUE(model->HasRootNode());
|
||||
EXPECT_GE(model->GetNodes().Size(), 1u);
|
||||
EXPECT_EQ(model->GetRootNodeIndex(), 0u);
|
||||
EXPECT_EQ(model->GetMeshBindings().Size(), 1u);
|
||||
EXPECT_EQ(model->GetMaterialBindings().Size(), 1u);
|
||||
EXPECT_NE(model->GetMeshBindings()[0].meshLocalID, kInvalidLocalID);
|
||||
EXPECT_NE(model->GetMaterialBindings()[0].materialLocalID, kInvalidLocalID);
|
||||
delete model;
|
||||
|
||||
MeshLoader meshLoader;
|
||||
const LoadResult meshResult =
|
||||
meshLoader.Load((fs::path(resolvedAsset.artifactDirectory.CStr()) / "mesh_0.xcmesh").string().c_str());
|
||||
ASSERT_TRUE(meshResult);
|
||||
ASSERT_NE(meshResult.resource, nullptr);
|
||||
|
||||
auto* mesh = static_cast<Mesh*>(meshResult.resource);
|
||||
ASSERT_NE(mesh, nullptr);
|
||||
EXPECT_EQ(mesh->GetVertexCount(), 3u);
|
||||
EXPECT_EQ(mesh->GetIndexCount(), 3u);
|
||||
EXPECT_EQ(mesh->GetSections().Size(), 1u);
|
||||
EXPECT_EQ(mesh->GetMaterials().Size(), 1u);
|
||||
EXPECT_NE(mesh->GetMaterial(0), nullptr);
|
||||
delete mesh;
|
||||
|
||||
database.Shutdown();
|
||||
fs::remove_all(projectRoot);
|
||||
}
|
||||
|
||||
TEST(ModelImportPipeline, ResourceManagerLoadsModelFromProjectAsset) {
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
const fs::path projectRoot = fs::temp_directory_path() / "xc_model_import_pipeline_resource_manager";
|
||||
const fs::path assetsDir = projectRoot / "Assets";
|
||||
|
||||
fs::remove_all(projectRoot);
|
||||
fs::create_directories(assetsDir);
|
||||
CopyTexturedTriangleFixture(assetsDir);
|
||||
|
||||
#ifdef _WIN32
|
||||
AssimpDllGuard dllGuard;
|
||||
const fs::path assimpDllPath = GetRepositoryRoot() / "engine" / "third_party" / "assimp" / "bin" / "assimp-vc143-mt.dll";
|
||||
ASSERT_TRUE(fs::exists(assimpDllPath));
|
||||
dllGuard.module = LoadLibraryW(assimpDllPath.wstring().c_str());
|
||||
ASSERT_NE(dllGuard.module, nullptr);
|
||||
#endif
|
||||
|
||||
ResourceManager& manager = ResourceManager::Get();
|
||||
manager.Initialize();
|
||||
manager.SetResourceRoot(projectRoot.string().c_str());
|
||||
|
||||
{
|
||||
const auto modelHandle = manager.Load<Model>("Assets/textured_triangle.obj");
|
||||
ASSERT_TRUE(modelHandle.IsValid());
|
||||
EXPECT_EQ(modelHandle->GetPath(), "Assets/textured_triangle.obj");
|
||||
EXPECT_TRUE(modelHandle->HasRootNode());
|
||||
EXPECT_EQ(modelHandle->GetMeshBindings().Size(), 1u);
|
||||
EXPECT_EQ(modelHandle->GetMaterialBindings().Size(), 1u);
|
||||
}
|
||||
|
||||
{
|
||||
const auto meshHandle = manager.Load<Mesh>("Assets/textured_triangle.obj");
|
||||
ASSERT_TRUE(meshHandle.IsValid());
|
||||
EXPECT_EQ(meshHandle->GetVertexCount(), 3u);
|
||||
EXPECT_EQ(meshHandle->GetIndexCount(), 3u);
|
||||
}
|
||||
|
||||
AssetRef modelAssetRef;
|
||||
ASSERT_TRUE(manager.TryGetAssetRef("Assets/textured_triangle.obj", ResourceType::Model, modelAssetRef));
|
||||
EXPECT_TRUE(modelAssetRef.IsValid());
|
||||
|
||||
manager.UnloadAll();
|
||||
|
||||
{
|
||||
const auto modelHandle = manager.Load<Model>(modelAssetRef);
|
||||
ASSERT_TRUE(modelHandle.IsValid());
|
||||
EXPECT_EQ(modelHandle->GetMeshBindings().Size(), 1u);
|
||||
EXPECT_EQ(modelHandle->GetMaterialBindings().Size(), 1u);
|
||||
}
|
||||
|
||||
manager.SetResourceRoot("");
|
||||
manager.Shutdown();
|
||||
fs::remove_all(projectRoot);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
122
tests/Resources/Model/test_model_loader.cpp
Normal file
122
tests/Resources/Model/test_model_loader.cpp
Normal file
@@ -0,0 +1,122 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <XCEngine/Core/Asset/ResourceManager.h>
|
||||
#include <XCEngine/Resources/Model/Model.h>
|
||||
#include <XCEngine/Resources/Model/ModelArtifactIO.h>
|
||||
#include <XCEngine/Resources/Model/ModelLoader.h>
|
||||
|
||||
#include <filesystem>
|
||||
|
||||
using namespace XCEngine::Resources;
|
||||
using namespace XCEngine::Math;
|
||||
|
||||
namespace {
|
||||
|
||||
Model BuildSampleModel(const char* artifactPath) {
|
||||
Model model;
|
||||
|
||||
IResource::ConstructParams params;
|
||||
params.name = "robot.fbx";
|
||||
params.path = artifactPath;
|
||||
params.guid = ResourceGUID::Generate(params.path);
|
||||
model.Initialize(params);
|
||||
|
||||
model.SetRootNodeIndex(0u);
|
||||
|
||||
ModelNode rootNode;
|
||||
rootNode.name = "Root";
|
||||
rootNode.parentIndex = -1;
|
||||
rootNode.meshBindingStart = 0u;
|
||||
rootNode.meshBindingCount = 1u;
|
||||
rootNode.localPosition = Vector3(0.0f, 1.0f, 2.0f);
|
||||
rootNode.localRotation = Quaternion::FromEulerAngles(0.1f, 0.2f, 0.3f);
|
||||
rootNode.localScale = Vector3(1.0f, 1.0f, 1.0f);
|
||||
model.AddNode(rootNode);
|
||||
|
||||
ModelNode childNode;
|
||||
childNode.name = "Body";
|
||||
childNode.parentIndex = 0;
|
||||
childNode.meshBindingStart = 1u;
|
||||
childNode.meshBindingCount = 1u;
|
||||
childNode.localPosition = Vector3(3.0f, 4.0f, 5.0f);
|
||||
childNode.localRotation = Quaternion::Identity();
|
||||
childNode.localScale = Vector3(0.5f, 0.5f, 0.5f);
|
||||
model.AddNode(childNode);
|
||||
|
||||
model.AddMeshBinding(ModelMeshBinding{ 101u, 0u, 2u });
|
||||
model.AddMeshBinding(ModelMeshBinding{ 102u, 2u, 1u });
|
||||
|
||||
model.AddMaterialBinding(ModelMaterialBinding{ 0u, 201u });
|
||||
model.AddMaterialBinding(ModelMaterialBinding{ 1u, 202u });
|
||||
model.AddMaterialBinding(ModelMaterialBinding{ 0u, 203u });
|
||||
|
||||
return model;
|
||||
}
|
||||
|
||||
TEST(ModelLoader, GetResourceType) {
|
||||
ModelLoader loader;
|
||||
EXPECT_EQ(loader.GetResourceType(), ResourceType::Model);
|
||||
}
|
||||
|
||||
TEST(ModelLoader, CanLoad) {
|
||||
ModelLoader loader;
|
||||
EXPECT_TRUE(loader.CanLoad("test.xcmodel"));
|
||||
EXPECT_TRUE(loader.CanLoad("test.XCMODEL"));
|
||||
EXPECT_FALSE(loader.CanLoad("test.fbx"));
|
||||
EXPECT_FALSE(loader.CanLoad("test.txt"));
|
||||
}
|
||||
|
||||
TEST(ModelLoader, LoadInvalidPath) {
|
||||
ModelLoader loader;
|
||||
const LoadResult result = loader.Load("invalid/path/model.xcmodel");
|
||||
EXPECT_FALSE(result);
|
||||
}
|
||||
|
||||
TEST(ModelLoader, WritesAndLoadsModelArtifact) {
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
const fs::path tempDir = fs::temp_directory_path() / "xc_model_artifact_test";
|
||||
const fs::path artifactPath = tempDir / "sample.xcmodel";
|
||||
|
||||
fs::remove_all(tempDir);
|
||||
fs::create_directories(tempDir);
|
||||
|
||||
const Model sourceModel = BuildSampleModel(artifactPath.string().c_str());
|
||||
XCEngine::Containers::String errorMessage;
|
||||
ASSERT_TRUE(WriteModelArtifactFile(artifactPath.string().c_str(), sourceModel, &errorMessage))
|
||||
<< errorMessage.CStr();
|
||||
|
||||
ModelLoader loader;
|
||||
const LoadResult result = loader.Load(artifactPath.string().c_str());
|
||||
ASSERT_TRUE(result);
|
||||
ASSERT_NE(result.resource, nullptr);
|
||||
|
||||
auto* model = static_cast<Model*>(result.resource);
|
||||
ASSERT_NE(model, nullptr);
|
||||
ASSERT_TRUE(model->HasRootNode());
|
||||
EXPECT_EQ(model->GetRootNodeIndex(), 0u);
|
||||
ASSERT_EQ(model->GetNodes().Size(), 2u);
|
||||
EXPECT_EQ(model->GetNodes()[0].name, "Root");
|
||||
EXPECT_EQ(model->GetNodes()[1].name, "Body");
|
||||
EXPECT_EQ(model->GetNodes()[1].parentIndex, 0);
|
||||
EXPECT_EQ(model->GetNodes()[1].localPosition, Vector3(3.0f, 4.0f, 5.0f));
|
||||
ASSERT_EQ(model->GetMeshBindings().Size(), 2u);
|
||||
EXPECT_EQ(model->GetMeshBindings()[0].meshLocalID, 101u);
|
||||
EXPECT_EQ(model->GetMeshBindings()[1].materialBindingCount, 1u);
|
||||
ASSERT_EQ(model->GetMaterialBindings().Size(), 3u);
|
||||
EXPECT_EQ(model->GetMaterialBindings()[1].materialLocalID, 202u);
|
||||
|
||||
delete model;
|
||||
fs::remove_all(tempDir);
|
||||
}
|
||||
|
||||
TEST(ModelLoader, ResourceManagerRegistersModelLoader) {
|
||||
ResourceManager& manager = ResourceManager::Get();
|
||||
manager.Initialize();
|
||||
|
||||
EXPECT_NE(manager.GetLoader(ResourceType::Model), nullptr);
|
||||
|
||||
manager.Shutdown();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
Reference in New Issue
Block a user