Add model and GaussianSplat asset pipelines
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
#include <gtest/gtest.h>
|
||||
#include <XCEngine/Core/Asset/ResourceTypes.h>
|
||||
#include <XCEngine/Core/Types.h>
|
||||
#include <XCEngine/Resources/GaussianSplat/GaussianSplat.h>
|
||||
#include <XCEngine/Resources/Model/Model.h>
|
||||
#include <XCEngine/Resources/UI/UIDocuments.h>
|
||||
|
||||
using namespace XCEngine::Resources;
|
||||
@@ -25,6 +27,8 @@ TEST(Resources_Types, ResourceType_EnumValues) {
|
||||
EXPECT_EQ(static_cast<uint8_t>(ResourceType::UITheme), 14);
|
||||
EXPECT_EQ(static_cast<uint8_t>(ResourceType::UISchema), 15);
|
||||
EXPECT_EQ(static_cast<uint8_t>(ResourceType::VolumeField), 16);
|
||||
EXPECT_EQ(static_cast<uint8_t>(ResourceType::Model), 17);
|
||||
EXPECT_EQ(static_cast<uint8_t>(ResourceType::GaussianSplat), 18);
|
||||
}
|
||||
|
||||
TEST(Resources_Types, GetResourceTypeName) {
|
||||
@@ -35,6 +39,8 @@ TEST(Resources_Types, GetResourceTypeName) {
|
||||
EXPECT_STREQ(GetResourceTypeName(ResourceType::UITheme), "UITheme");
|
||||
EXPECT_STREQ(GetResourceTypeName(ResourceType::UISchema), "UISchema");
|
||||
EXPECT_STREQ(GetResourceTypeName(ResourceType::VolumeField), "VolumeField");
|
||||
EXPECT_STREQ(GetResourceTypeName(ResourceType::Model), "Model");
|
||||
EXPECT_STREQ(GetResourceTypeName(ResourceType::GaussianSplat), "GaussianSplat");
|
||||
EXPECT_STREQ(GetResourceTypeName(ResourceType::Unknown), "Unknown");
|
||||
}
|
||||
|
||||
@@ -49,6 +55,8 @@ TEST(Resources_Types, GetResourceType_TemplateSpecializations) {
|
||||
EXPECT_EQ(GetResourceType<UITheme>(), ResourceType::UITheme);
|
||||
EXPECT_EQ(GetResourceType<UISchema>(), ResourceType::UISchema);
|
||||
EXPECT_EQ(GetResourceType<VolumeField>(), ResourceType::VolumeField);
|
||||
EXPECT_EQ(GetResourceType<Model>(), ResourceType::Model);
|
||||
EXPECT_EQ(GetResourceType<GaussianSplat>(), ResourceType::GaussianSplat);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
# ============================================================
|
||||
|
||||
add_subdirectory(Texture)
|
||||
add_subdirectory(GaussianSplat)
|
||||
add_subdirectory(Model)
|
||||
add_subdirectory(Mesh)
|
||||
add_subdirectory(Material)
|
||||
add_subdirectory(Shader)
|
||||
|
||||
34
tests/Resources/GaussianSplat/CMakeLists.txt
Normal file
34
tests/Resources/GaussianSplat/CMakeLists.txt
Normal file
@@ -0,0 +1,34 @@
|
||||
# ============================================================
|
||||
# GaussianSplat Tests
|
||||
# ============================================================
|
||||
|
||||
set(GAUSSIAN_SPLAT_TEST_SOURCES
|
||||
test_gaussian_splat.cpp
|
||||
test_gaussian_splat_loader.cpp
|
||||
)
|
||||
|
||||
add_executable(gaussian_splat_tests ${GAUSSIAN_SPLAT_TEST_SOURCES})
|
||||
|
||||
if(MSVC)
|
||||
set_target_properties(gaussian_splat_tests PROPERTIES
|
||||
LINK_FLAGS "/NODEFAULTLIB:libcpmt.lib /NODEFAULTLIB:libcmt.lib"
|
||||
)
|
||||
endif()
|
||||
|
||||
target_link_libraries(gaussian_splat_tests
|
||||
PRIVATE
|
||||
XCEngine
|
||||
GTest::gtest
|
||||
GTest::gtest_main
|
||||
)
|
||||
|
||||
target_include_directories(gaussian_splat_tests PRIVATE
|
||||
${CMAKE_SOURCE_DIR}/engine/include
|
||||
)
|
||||
|
||||
target_compile_definitions(gaussian_splat_tests PRIVATE
|
||||
XCENGINE_TEST_ROOM_PLY_PATH="${CMAKE_SOURCE_DIR}/mvs/3DGS-Unity/room.ply"
|
||||
)
|
||||
|
||||
include(GoogleTest)
|
||||
gtest_discover_tests(gaussian_splat_tests)
|
||||
73
tests/Resources/GaussianSplat/test_gaussian_splat.cpp
Normal file
73
tests/Resources/GaussianSplat/test_gaussian_splat.cpp
Normal file
@@ -0,0 +1,73 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <XCEngine/Core/Math/Bounds.h>
|
||||
#include <XCEngine/Resources/GaussianSplat/GaussianSplat.h>
|
||||
|
||||
using namespace XCEngine::Resources;
|
||||
using namespace XCEngine::Math;
|
||||
|
||||
namespace {
|
||||
|
||||
TEST(GaussianSplat, CreateOwnedStoresMetadataSectionsAndPayload) {
|
||||
GaussianSplat gaussianSplat;
|
||||
|
||||
GaussianSplatMetadata metadata;
|
||||
metadata.contentVersion = 7u;
|
||||
metadata.splatCount = 2u;
|
||||
metadata.bounds.SetMinMax(Vector3(-1.0f, -2.0f, -3.0f), Vector3(4.0f, 5.0f, 6.0f));
|
||||
metadata.positionFormat = GaussianSplatSectionFormat::VectorFloat32;
|
||||
metadata.otherFormat = GaussianSplatSectionFormat::OtherFloat32;
|
||||
metadata.colorFormat = GaussianSplatSectionFormat::ColorRGBA32F;
|
||||
metadata.shFormat = GaussianSplatSectionFormat::SHFloat32;
|
||||
|
||||
XCEngine::Containers::Array<GaussianSplatSection> sections;
|
||||
sections.PushBack(GaussianSplatSection{
|
||||
GaussianSplatSectionType::Positions,
|
||||
GaussianSplatSectionFormat::VectorFloat32,
|
||||
0u,
|
||||
24u,
|
||||
2u,
|
||||
12u
|
||||
});
|
||||
|
||||
XCEngine::Containers::Array<XCEngine::Core::uint8> payload;
|
||||
payload.Resize(24u);
|
||||
for (size_t index = 0; index < payload.Size(); ++index) {
|
||||
payload[index] = static_cast<XCEngine::Core::uint8>(index);
|
||||
}
|
||||
|
||||
ASSERT_TRUE(gaussianSplat.CreateOwned(metadata, std::move(sections), std::move(payload)));
|
||||
EXPECT_TRUE(gaussianSplat.IsValid());
|
||||
EXPECT_EQ(gaussianSplat.GetType(), ResourceType::GaussianSplat);
|
||||
EXPECT_EQ(gaussianSplat.GetContentVersion(), 7u);
|
||||
EXPECT_EQ(gaussianSplat.GetSplatCount(), 2u);
|
||||
EXPECT_EQ(gaussianSplat.GetBounds().GetMin(), Vector3(-1.0f, -2.0f, -3.0f));
|
||||
EXPECT_EQ(gaussianSplat.GetBounds().GetMax(), Vector3(4.0f, 5.0f, 6.0f));
|
||||
ASSERT_NE(gaussianSplat.FindSection(GaussianSplatSectionType::Positions), nullptr);
|
||||
EXPECT_EQ(gaussianSplat.GetPayloadSize(), 24u);
|
||||
EXPECT_NE(gaussianSplat.GetSectionData(GaussianSplatSectionType::Positions), nullptr);
|
||||
}
|
||||
|
||||
TEST(GaussianSplat, RejectsInvalidSectionLayout) {
|
||||
GaussianSplat gaussianSplat;
|
||||
|
||||
GaussianSplatMetadata metadata;
|
||||
metadata.splatCount = 1u;
|
||||
|
||||
XCEngine::Containers::Array<GaussianSplatSection> sections;
|
||||
sections.PushBack(GaussianSplatSection{
|
||||
GaussianSplatSectionType::Positions,
|
||||
GaussianSplatSectionFormat::VectorFloat32,
|
||||
16u,
|
||||
16u,
|
||||
1u,
|
||||
12u
|
||||
});
|
||||
|
||||
XCEngine::Containers::Array<XCEngine::Core::uint8> payload;
|
||||
payload.Resize(24u);
|
||||
|
||||
EXPECT_FALSE(gaussianSplat.CreateOwned(metadata, std::move(sections), std::move(payload)));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
526
tests/Resources/GaussianSplat/test_gaussian_splat_loader.cpp
Normal file
526
tests/Resources/GaussianSplat/test_gaussian_splat_loader.cpp
Normal file
@@ -0,0 +1,526 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <XCEngine/Core/Asset/AssetDatabase.h>
|
||||
#include <XCEngine/Core/Asset/AssetRef.h>
|
||||
#include <XCEngine/Core/Asset/ResourceManager.h>
|
||||
#include <XCEngine/Core/Math/Bounds.h>
|
||||
#include <XCEngine/Resources/GaussianSplat/GaussianSplat.h>
|
||||
#include <XCEngine/Resources/GaussianSplat/GaussianSplatArtifactIO.h>
|
||||
#include <XCEngine/Resources/GaussianSplat/GaussianSplatLoader.h>
|
||||
|
||||
#include <chrono>
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
using namespace XCEngine::Resources;
|
||||
using namespace XCEngine::Math;
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr float kSHC0 = 0.2820948f;
|
||||
|
||||
struct SyntheticGaussianSplatVertex {
|
||||
Vector3 position = Vector3::Zero();
|
||||
Vector3 dc0 = Vector3::Zero();
|
||||
float opacity = 0.0f;
|
||||
Vector3 scaleLog = Vector3::Zero();
|
||||
float rotationWXYZ[4] = { 1.0f, 0.0f, 0.0f, 0.0f };
|
||||
float sh[kGaussianSplatSHCoefficientCount] = {};
|
||||
};
|
||||
|
||||
struct SampleArtifactData {
|
||||
GaussianSplatMetadata metadata;
|
||||
XCEngine::Containers::Array<GaussianSplatSection> sections;
|
||||
XCEngine::Containers::Array<XCEngine::Core::uint8> payload;
|
||||
};
|
||||
|
||||
std::filesystem::path GetRoomPlyPath() {
|
||||
return std::filesystem::path(XCENGINE_TEST_ROOM_PLY_PATH);
|
||||
}
|
||||
|
||||
void ExpectVector3Near(const Vector3& actual, const Vector3& expected, float epsilon = 1e-5f) {
|
||||
EXPECT_NEAR(actual.x, expected.x, epsilon);
|
||||
EXPECT_NEAR(actual.y, expected.y, epsilon);
|
||||
EXPECT_NEAR(actual.z, expected.z, epsilon);
|
||||
}
|
||||
|
||||
void ExpectVector4Near(const Vector4& actual, const Vector4& expected, float epsilon = 1e-5f) {
|
||||
EXPECT_NEAR(actual.x, expected.x, epsilon);
|
||||
EXPECT_NEAR(actual.y, expected.y, epsilon);
|
||||
EXPECT_NEAR(actual.z, expected.z, epsilon);
|
||||
EXPECT_NEAR(actual.w, expected.w, epsilon);
|
||||
}
|
||||
|
||||
void ExpectQuaternionNear(const Quaternion& actual, const Quaternion& expected, float epsilon = 1e-5f) {
|
||||
EXPECT_NEAR(actual.x, expected.x, epsilon);
|
||||
EXPECT_NEAR(actual.y, expected.y, epsilon);
|
||||
EXPECT_NEAR(actual.z, expected.z, epsilon);
|
||||
EXPECT_NEAR(actual.w, expected.w, epsilon);
|
||||
}
|
||||
|
||||
float Sigmoid(float value) {
|
||||
return 1.0f / (1.0f + std::exp(-value));
|
||||
}
|
||||
|
||||
Vector3 LinearScale(const Vector3& value) {
|
||||
return Vector3(
|
||||
std::abs(std::exp(value.x)),
|
||||
std::abs(std::exp(value.y)),
|
||||
std::abs(std::exp(value.z)));
|
||||
}
|
||||
|
||||
Quaternion NormalizeRotationWXYZ(float w, float x, float y, float z) {
|
||||
const float magnitude = std::sqrt(w * w + x * x + y * y + z * z);
|
||||
if (magnitude <= 1e-8f) {
|
||||
return Quaternion::Identity();
|
||||
}
|
||||
|
||||
return Quaternion(x / magnitude, y / magnitude, z / magnitude, w / magnitude);
|
||||
}
|
||||
|
||||
Vector4 SH0ToColorOpacity(const Vector3& dc0, float opacityRaw) {
|
||||
return Vector4(
|
||||
dc0.x * kSHC0 + 0.5f,
|
||||
dc0.y * kSHC0 + 0.5f,
|
||||
dc0.z * kSHC0 + 0.5f,
|
||||
Sigmoid(opacityRaw));
|
||||
}
|
||||
|
||||
void WriteBinaryFloat(std::ofstream& output, float value) {
|
||||
output.write(reinterpret_cast<const char*>(&value), sizeof(value));
|
||||
}
|
||||
|
||||
void WriteSyntheticGaussianSplatPly(
|
||||
const std::filesystem::path& path,
|
||||
const std::vector<SyntheticGaussianSplatVertex>& vertices) {
|
||||
std::ofstream output(path, std::ios::binary | std::ios::trunc);
|
||||
ASSERT_TRUE(output.is_open());
|
||||
|
||||
output << "ply\n";
|
||||
output << "format binary_little_endian 1.0\n";
|
||||
output << "element vertex " << vertices.size() << "\n";
|
||||
output << "property float opacity\n";
|
||||
output << "property float y\n";
|
||||
output << "property float scale_2\n";
|
||||
output << "property float rot_3\n";
|
||||
output << "property float f_dc_1\n";
|
||||
output << "property float x\n";
|
||||
output << "property float scale_0\n";
|
||||
output << "property float rot_1\n";
|
||||
output << "property float f_dc_2\n";
|
||||
output << "property float z\n";
|
||||
output << "property float scale_1\n";
|
||||
output << "property float rot_0\n";
|
||||
output << "property float f_dc_0\n";
|
||||
output << "property float rot_2\n";
|
||||
for (XCEngine::Core::uint32 index = 0; index < kGaussianSplatSHCoefficientCount; ++index) {
|
||||
output << "property float f_rest_" << index << "\n";
|
||||
}
|
||||
output << "end_header\n";
|
||||
|
||||
for (const SyntheticGaussianSplatVertex& vertex : vertices) {
|
||||
WriteBinaryFloat(output, vertex.opacity);
|
||||
WriteBinaryFloat(output, vertex.position.y);
|
||||
WriteBinaryFloat(output, vertex.scaleLog.z);
|
||||
WriteBinaryFloat(output, vertex.rotationWXYZ[3]);
|
||||
WriteBinaryFloat(output, vertex.dc0.y);
|
||||
WriteBinaryFloat(output, vertex.position.x);
|
||||
WriteBinaryFloat(output, vertex.scaleLog.x);
|
||||
WriteBinaryFloat(output, vertex.rotationWXYZ[1]);
|
||||
WriteBinaryFloat(output, vertex.dc0.z);
|
||||
WriteBinaryFloat(output, vertex.position.z);
|
||||
WriteBinaryFloat(output, vertex.scaleLog.y);
|
||||
WriteBinaryFloat(output, vertex.rotationWXYZ[0]);
|
||||
WriteBinaryFloat(output, vertex.dc0.x);
|
||||
WriteBinaryFloat(output, vertex.rotationWXYZ[2]);
|
||||
for (float coefficient : vertex.sh) {
|
||||
WriteBinaryFloat(output, coefficient);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SampleArtifactData BuildSampleArtifactData() {
|
||||
const GaussianSplatPositionRecord positions[2] = {
|
||||
{ Vector3(0.0f, 1.0f, 2.0f) },
|
||||
{ Vector3(3.0f, 4.0f, 5.0f) }
|
||||
};
|
||||
const GaussianSplatOtherRecord other[2] = {
|
||||
{ Quaternion::Identity(), Vector3(1.0f, 1.0f, 1.0f), 0.0f },
|
||||
{ Quaternion(0.0f, 0.5f, 0.0f, 0.8660254f), Vector3(2.0f, 2.0f, 2.0f), 0.0f }
|
||||
};
|
||||
const GaussianSplatColorRecord colors[2] = {
|
||||
{ Vector4(1.0f, 0.0f, 0.0f, 0.25f) },
|
||||
{ Vector4(0.0f, 1.0f, 0.0f, 0.75f) }
|
||||
};
|
||||
|
||||
GaussianSplatSHRecord sh[2];
|
||||
for (XCEngine::Core::uint32 index = 0; index < kGaussianSplatSHCoefficientCount; ++index) {
|
||||
sh[0].coefficients[index] = 0.01f * static_cast<float>(index + 1u);
|
||||
sh[1].coefficients[index] = -0.02f * static_cast<float>(index + 1u);
|
||||
}
|
||||
|
||||
SampleArtifactData sample;
|
||||
sample.metadata.contentVersion = 3u;
|
||||
sample.metadata.splatCount = 2u;
|
||||
sample.metadata.bounds.SetMinMax(Vector3(-2.0f, -1.0f, -3.0f), Vector3(5.0f, 4.0f, 6.0f));
|
||||
sample.metadata.positionFormat = GaussianSplatSectionFormat::VectorFloat32;
|
||||
sample.metadata.otherFormat = GaussianSplatSectionFormat::OtherFloat32;
|
||||
sample.metadata.colorFormat = GaussianSplatSectionFormat::ColorRGBA32F;
|
||||
sample.metadata.shFormat = GaussianSplatSectionFormat::SHFloat32;
|
||||
|
||||
sample.sections.Reserve(4u);
|
||||
size_t payloadOffset = 0u;
|
||||
auto appendSection = [&](GaussianSplatSectionType type,
|
||||
GaussianSplatSectionFormat format,
|
||||
const void* data,
|
||||
size_t dataSize,
|
||||
XCEngine::Core::uint32 elementCount,
|
||||
XCEngine::Core::uint32 elementStride) {
|
||||
GaussianSplatSection section;
|
||||
section.type = type;
|
||||
section.format = format;
|
||||
section.dataOffset = payloadOffset;
|
||||
section.dataSize = dataSize;
|
||||
section.elementCount = elementCount;
|
||||
section.elementStride = elementStride;
|
||||
sample.sections.PushBack(section);
|
||||
|
||||
const size_t newPayloadSize = sample.payload.Size() + dataSize;
|
||||
sample.payload.Resize(newPayloadSize);
|
||||
std::memcpy(sample.payload.Data() + payloadOffset, data, dataSize);
|
||||
payloadOffset = newPayloadSize;
|
||||
};
|
||||
|
||||
appendSection(
|
||||
GaussianSplatSectionType::Positions,
|
||||
GaussianSplatSectionFormat::VectorFloat32,
|
||||
positions,
|
||||
sizeof(positions),
|
||||
2u,
|
||||
sizeof(GaussianSplatPositionRecord));
|
||||
appendSection(
|
||||
GaussianSplatSectionType::Other,
|
||||
GaussianSplatSectionFormat::OtherFloat32,
|
||||
other,
|
||||
sizeof(other),
|
||||
2u,
|
||||
sizeof(GaussianSplatOtherRecord));
|
||||
appendSection(
|
||||
GaussianSplatSectionType::Color,
|
||||
GaussianSplatSectionFormat::ColorRGBA32F,
|
||||
colors,
|
||||
sizeof(colors),
|
||||
2u,
|
||||
sizeof(GaussianSplatColorRecord));
|
||||
appendSection(
|
||||
GaussianSplatSectionType::SH,
|
||||
GaussianSplatSectionFormat::SHFloat32,
|
||||
sh,
|
||||
sizeof(sh),
|
||||
2u,
|
||||
sizeof(GaussianSplatSHRecord));
|
||||
|
||||
return sample;
|
||||
}
|
||||
|
||||
GaussianSplat BuildSampleGaussianSplat(const char* artifactPath) {
|
||||
SampleArtifactData sample = BuildSampleArtifactData();
|
||||
|
||||
GaussianSplat gaussianSplat;
|
||||
XCEngine::Resources::IResource::ConstructParams params;
|
||||
params.name = "sample.xcgsplat";
|
||||
params.path = artifactPath;
|
||||
params.guid = ResourceGUID::Generate(params.path);
|
||||
gaussianSplat.Initialize(params);
|
||||
const bool created = gaussianSplat.CreateOwned(
|
||||
sample.metadata,
|
||||
std::move(sample.sections),
|
||||
std::move(sample.payload));
|
||||
EXPECT_TRUE(created);
|
||||
return gaussianSplat;
|
||||
}
|
||||
|
||||
std::filesystem::path CreateTestProjectRoot(const char* folderName) {
|
||||
return std::filesystem::current_path() / "__xc_gaussian_splat_test_runtime" / folderName;
|
||||
}
|
||||
|
||||
void LinkOrCopyFixture(const std::filesystem::path& sourcePath, const std::filesystem::path& destinationPath) {
|
||||
std::error_code ec;
|
||||
std::filesystem::create_hard_link(sourcePath, destinationPath, ec);
|
||||
if (ec) {
|
||||
ec.clear();
|
||||
std::filesystem::copy_file(sourcePath, destinationPath, std::filesystem::copy_options::overwrite_existing, ec);
|
||||
}
|
||||
ASSERT_FALSE(ec);
|
||||
}
|
||||
|
||||
XCEngine::Core::uint32 ReadPlyVertexCount(const std::filesystem::path& path) {
|
||||
std::ifstream input(path, std::ios::binary);
|
||||
EXPECT_TRUE(input.is_open());
|
||||
|
||||
std::string line;
|
||||
while (std::getline(input, line)) {
|
||||
if (line == "end_header") {
|
||||
break;
|
||||
}
|
||||
|
||||
if (line.rfind("element vertex ", 0) == 0) {
|
||||
return static_cast<XCEngine::Core::uint32>(std::stoul(line.substr(15)));
|
||||
}
|
||||
}
|
||||
|
||||
return 0u;
|
||||
}
|
||||
|
||||
TEST(GaussianSplatLoader, GetResourceType) {
|
||||
GaussianSplatLoader loader;
|
||||
EXPECT_EQ(loader.GetResourceType(), ResourceType::GaussianSplat);
|
||||
}
|
||||
|
||||
TEST(GaussianSplatLoader, CanLoad) {
|
||||
GaussianSplatLoader loader;
|
||||
EXPECT_TRUE(loader.CanLoad("sample.xcgsplat"));
|
||||
EXPECT_TRUE(loader.CanLoad("sample.XCGSPLAT"));
|
||||
EXPECT_FALSE(loader.CanLoad("sample.ply"));
|
||||
}
|
||||
|
||||
TEST(GaussianSplatLoader, LoadInvalidPath) {
|
||||
GaussianSplatLoader loader;
|
||||
const LoadResult result = loader.Load("invalid/path/sample.xcgsplat");
|
||||
EXPECT_FALSE(result);
|
||||
}
|
||||
|
||||
TEST(GaussianSplatLoader, WritesAndLoadsArtifact) {
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
const fs::path tempDir = fs::temp_directory_path() / "xc_gaussian_splat_artifact_test";
|
||||
const fs::path artifactPath = tempDir / "sample.xcgsplat";
|
||||
|
||||
fs::remove_all(tempDir);
|
||||
fs::create_directories(tempDir);
|
||||
|
||||
const GaussianSplat source = BuildSampleGaussianSplat(artifactPath.string().c_str());
|
||||
XCEngine::Containers::String errorMessage;
|
||||
ASSERT_TRUE(WriteGaussianSplatArtifactFile(artifactPath.string().c_str(), source, &errorMessage))
|
||||
<< errorMessage.CStr();
|
||||
|
||||
GaussianSplatLoader loader;
|
||||
const LoadResult result = loader.Load(artifactPath.string().c_str());
|
||||
ASSERT_TRUE(result);
|
||||
ASSERT_NE(result.resource, nullptr);
|
||||
|
||||
auto* gaussianSplat = static_cast<GaussianSplat*>(result.resource);
|
||||
ASSERT_NE(gaussianSplat, nullptr);
|
||||
EXPECT_EQ(gaussianSplat->GetContentVersion(), 3u);
|
||||
EXPECT_EQ(gaussianSplat->GetSplatCount(), 2u);
|
||||
EXPECT_EQ(gaussianSplat->GetBounds().GetMin(), Vector3(-2.0f, -1.0f, -3.0f));
|
||||
EXPECT_EQ(gaussianSplat->GetBounds().GetMax(), Vector3(5.0f, 4.0f, 6.0f));
|
||||
ASSERT_EQ(gaussianSplat->GetSections().Size(), 4u);
|
||||
const GaussianSplatSection* shSection = gaussianSplat->FindSection(GaussianSplatSectionType::SH);
|
||||
ASSERT_NE(shSection, nullptr);
|
||||
EXPECT_EQ(shSection->elementCount, 2u);
|
||||
EXPECT_EQ(shSection->elementStride, sizeof(GaussianSplatSHRecord));
|
||||
ASSERT_NE(gaussianSplat->GetColorRecords(), nullptr);
|
||||
EXPECT_EQ(gaussianSplat->GetColorRecords()[1].colorOpacity, Vector4(0.0f, 1.0f, 0.0f, 0.75f));
|
||||
|
||||
delete gaussianSplat;
|
||||
fs::remove_all(tempDir);
|
||||
}
|
||||
|
||||
TEST(GaussianSplatLoader, ResourceManagerRegistersGaussianSplatLoader) {
|
||||
ResourceManager& manager = ResourceManager::Get();
|
||||
manager.Initialize();
|
||||
|
||||
EXPECT_NE(manager.GetLoader(ResourceType::GaussianSplat), nullptr);
|
||||
|
||||
manager.Shutdown();
|
||||
}
|
||||
|
||||
TEST(GaussianSplatLoader, ResourceManagerLoadsArtifactByPath) {
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
const fs::path tempDir = fs::temp_directory_path() / "xc_gaussian_splat_manager_load_test";
|
||||
const fs::path artifactPath = tempDir / "sample.xcgsplat";
|
||||
|
||||
fs::remove_all(tempDir);
|
||||
fs::create_directories(tempDir);
|
||||
|
||||
const GaussianSplat source = BuildSampleGaussianSplat(artifactPath.string().c_str());
|
||||
XCEngine::Containers::String errorMessage;
|
||||
ASSERT_TRUE(WriteGaussianSplatArtifactFile(artifactPath.string().c_str(), source, &errorMessage))
|
||||
<< errorMessage.CStr();
|
||||
|
||||
ResourceManager& manager = ResourceManager::Get();
|
||||
manager.Initialize();
|
||||
|
||||
{
|
||||
const auto handle = manager.Load<GaussianSplat>(artifactPath.string().c_str());
|
||||
ASSERT_TRUE(handle.IsValid());
|
||||
EXPECT_EQ(handle->GetSplatCount(), 2u);
|
||||
EXPECT_EQ(handle->GetContentVersion(), 3u);
|
||||
ASSERT_NE(handle->FindSection(GaussianSplatSectionType::Color), nullptr);
|
||||
}
|
||||
|
||||
manager.UnloadAll();
|
||||
manager.Shutdown();
|
||||
fs::remove_all(tempDir);
|
||||
}
|
||||
|
||||
TEST(GaussianSplatLoader, AssetDatabaseImportsSyntheticPlyAndLinearizesData) {
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
const fs::path projectRoot = CreateTestProjectRoot("gaussian_splat_synthetic_import");
|
||||
const fs::path assetsDir = projectRoot / "Assets";
|
||||
const fs::path sourcePath = assetsDir / "sample.ply";
|
||||
|
||||
fs::remove_all(projectRoot);
|
||||
fs::create_directories(assetsDir);
|
||||
|
||||
std::vector<SyntheticGaussianSplatVertex> vertices(2);
|
||||
vertices[0].position = Vector3(1.0f, 2.0f, 3.0f);
|
||||
vertices[0].dc0 = Vector3(0.2f, -0.1f, 0.0f);
|
||||
vertices[0].opacity = 0.25f;
|
||||
vertices[0].scaleLog = Vector3(0.0f, std::log(2.0f), std::log(4.0f));
|
||||
vertices[0].rotationWXYZ[0] = 2.0f;
|
||||
vertices[0].rotationWXYZ[1] = 0.0f;
|
||||
vertices[0].rotationWXYZ[2] = 0.0f;
|
||||
vertices[0].rotationWXYZ[3] = 0.0f;
|
||||
for (XCEngine::Core::uint32 index = 0; index < kGaussianSplatSHCoefficientCount; ++index) {
|
||||
vertices[0].sh[index] = 0.01f * static_cast<float>(index + 1u);
|
||||
}
|
||||
|
||||
vertices[1].position = Vector3(-4.0f, -5.0f, -6.0f);
|
||||
vertices[1].dc0 = Vector3(1.0f, 0.5f, -0.5f);
|
||||
vertices[1].opacity = -1.0f;
|
||||
vertices[1].scaleLog = Vector3(std::log(0.5f), 0.0f, std::log(3.0f));
|
||||
vertices[1].rotationWXYZ[0] = 0.0f;
|
||||
vertices[1].rotationWXYZ[1] = 0.0f;
|
||||
vertices[1].rotationWXYZ[2] = 3.0f;
|
||||
vertices[1].rotationWXYZ[3] = 4.0f;
|
||||
for (XCEngine::Core::uint32 index = 0; index < kGaussianSplatSHCoefficientCount; ++index) {
|
||||
vertices[1].sh[index] = -0.02f * static_cast<float>(index + 1u);
|
||||
}
|
||||
|
||||
WriteSyntheticGaussianSplatPly(sourcePath, vertices);
|
||||
|
||||
AssetDatabase database;
|
||||
database.Initialize(projectRoot.string().c_str());
|
||||
|
||||
AssetDatabase::ResolvedAsset resolved;
|
||||
ASSERT_TRUE(database.EnsureArtifact("Assets/sample.ply", ResourceType::GaussianSplat, resolved));
|
||||
ASSERT_TRUE(resolved.artifactReady);
|
||||
EXPECT_TRUE(fs::exists(resolved.artifactMainPath.CStr()));
|
||||
EXPECT_EQ(fs::path(resolved.artifactMainPath.CStr()).extension().generic_string(), ".xcgsplat");
|
||||
|
||||
GaussianSplatLoader loader;
|
||||
LoadResult result = loader.Load(resolved.artifactMainPath);
|
||||
ASSERT_TRUE(result);
|
||||
ASSERT_NE(result.resource, nullptr);
|
||||
|
||||
auto* gaussianSplat = static_cast<GaussianSplat*>(result.resource);
|
||||
ASSERT_NE(gaussianSplat, nullptr);
|
||||
EXPECT_EQ(gaussianSplat->GetSplatCount(), 2u);
|
||||
EXPECT_EQ(gaussianSplat->GetChunkCount(), 0u);
|
||||
EXPECT_EQ(gaussianSplat->GetCameraCount(), 0u);
|
||||
ExpectVector3Near(gaussianSplat->GetBounds().GetMin(), Vector3(-4.0f, -5.0f, -6.0f));
|
||||
ExpectVector3Near(gaussianSplat->GetBounds().GetMax(), Vector3(1.0f, 2.0f, 3.0f));
|
||||
|
||||
ASSERT_NE(gaussianSplat->GetPositionRecords(), nullptr);
|
||||
ASSERT_NE(gaussianSplat->GetOtherRecords(), nullptr);
|
||||
ASSERT_NE(gaussianSplat->GetColorRecords(), nullptr);
|
||||
ASSERT_NE(gaussianSplat->GetSHRecords(), nullptr);
|
||||
|
||||
ExpectVector3Near(gaussianSplat->GetPositionRecords()[0].position, vertices[0].position);
|
||||
ExpectVector3Near(gaussianSplat->GetPositionRecords()[1].position, vertices[1].position);
|
||||
ExpectQuaternionNear(
|
||||
gaussianSplat->GetOtherRecords()[0].rotation,
|
||||
NormalizeRotationWXYZ(2.0f, 0.0f, 0.0f, 0.0f));
|
||||
ExpectQuaternionNear(
|
||||
gaussianSplat->GetOtherRecords()[1].rotation,
|
||||
NormalizeRotationWXYZ(0.0f, 0.0f, 3.0f, 4.0f));
|
||||
ExpectVector3Near(gaussianSplat->GetOtherRecords()[0].scale, LinearScale(vertices[0].scaleLog));
|
||||
ExpectVector3Near(gaussianSplat->GetOtherRecords()[1].scale, LinearScale(vertices[1].scaleLog));
|
||||
ExpectVector4Near(gaussianSplat->GetColorRecords()[0].colorOpacity, SH0ToColorOpacity(vertices[0].dc0, vertices[0].opacity));
|
||||
ExpectVector4Near(gaussianSplat->GetColorRecords()[1].colorOpacity, SH0ToColorOpacity(vertices[1].dc0, vertices[1].opacity));
|
||||
EXPECT_NEAR(gaussianSplat->GetSHRecords()[0].coefficients[0], vertices[0].sh[0], 1e-6f);
|
||||
EXPECT_NEAR(gaussianSplat->GetSHRecords()[0].coefficients[44], vertices[0].sh[44], 1e-6f);
|
||||
EXPECT_NEAR(gaussianSplat->GetSHRecords()[1].coefficients[0], vertices[1].sh[0], 1e-6f);
|
||||
EXPECT_NEAR(gaussianSplat->GetSHRecords()[1].coefficients[44], vertices[1].sh[44], 1e-6f);
|
||||
|
||||
delete gaussianSplat;
|
||||
database.Shutdown();
|
||||
fs::remove_all(projectRoot);
|
||||
}
|
||||
|
||||
TEST(GaussianSplatLoader, RoomPlyBuildsArtifactAndLoadsThroughResourceManager) {
|
||||
namespace fs = std::filesystem;
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
const fs::path fixturePath = GetRoomPlyPath();
|
||||
ASSERT_TRUE(fs::exists(fixturePath));
|
||||
|
||||
const XCEngine::Core::uint32 expectedVertexCount = ReadPlyVertexCount(fixturePath);
|
||||
ASSERT_GT(expectedVertexCount, 0u);
|
||||
|
||||
const fs::path projectRoot = CreateTestProjectRoot("gaussian_splat_room_import");
|
||||
const fs::path assetsDir = projectRoot / "Assets";
|
||||
const fs::path roomPath = assetsDir / "room.ply";
|
||||
|
||||
fs::remove_all(projectRoot);
|
||||
fs::create_directories(assetsDir);
|
||||
LinkOrCopyFixture(fixturePath, roomPath);
|
||||
|
||||
ResourceManager& manager = ResourceManager::Get();
|
||||
manager.Initialize();
|
||||
manager.SetResourceRoot(projectRoot.string().c_str());
|
||||
|
||||
{
|
||||
const auto firstHandle = manager.Load<GaussianSplat>("Assets/room.ply");
|
||||
ASSERT_TRUE(firstHandle.IsValid());
|
||||
EXPECT_EQ(firstHandle->GetSplatCount(), expectedVertexCount);
|
||||
EXPECT_EQ(firstHandle->GetPositionFormat(), GaussianSplatSectionFormat::VectorFloat32);
|
||||
EXPECT_EQ(firstHandle->GetOtherFormat(), GaussianSplatSectionFormat::OtherFloat32);
|
||||
EXPECT_EQ(firstHandle->GetColorFormat(), GaussianSplatSectionFormat::ColorRGBA32F);
|
||||
EXPECT_EQ(firstHandle->GetSHFormat(), GaussianSplatSectionFormat::SHFloat32);
|
||||
|
||||
AssetRef assetRef;
|
||||
ASSERT_TRUE(manager.TryGetAssetRef("Assets/room.ply", ResourceType::GaussianSplat, assetRef));
|
||||
EXPECT_TRUE(assetRef.IsValid());
|
||||
|
||||
AssetDatabase database;
|
||||
database.Initialize(projectRoot.string().c_str());
|
||||
|
||||
AssetDatabase::ResolvedAsset firstResolve;
|
||||
ASSERT_TRUE(database.EnsureArtifact("Assets/room.ply", ResourceType::GaussianSplat, firstResolve));
|
||||
ASSERT_TRUE(firstResolve.artifactReady);
|
||||
EXPECT_TRUE(fs::exists(firstResolve.artifactMainPath.CStr()));
|
||||
EXPECT_EQ(fs::path(firstResolve.artifactMainPath.CStr()).extension().generic_string(), ".xcgsplat");
|
||||
const auto originalArtifactWriteTime = fs::last_write_time(firstResolve.artifactMainPath.CStr());
|
||||
|
||||
std::this_thread::sleep_for(50ms);
|
||||
|
||||
AssetDatabase::ResolvedAsset secondResolve;
|
||||
ASSERT_TRUE(database.EnsureArtifact("Assets/room.ply", ResourceType::GaussianSplat, secondResolve));
|
||||
EXPECT_EQ(firstResolve.artifactMainPath, secondResolve.artifactMainPath);
|
||||
EXPECT_EQ(originalArtifactWriteTime, fs::last_write_time(secondResolve.artifactMainPath.CStr()));
|
||||
|
||||
database.Shutdown();
|
||||
|
||||
manager.UnloadAll();
|
||||
|
||||
const auto secondHandle = manager.Load<GaussianSplat>(assetRef);
|
||||
ASSERT_TRUE(secondHandle.IsValid());
|
||||
EXPECT_EQ(secondHandle->GetSplatCount(), expectedVertexCount);
|
||||
ASSERT_NE(secondHandle->FindSection(GaussianSplatSectionType::SH), nullptr);
|
||||
}
|
||||
|
||||
manager.SetResourceRoot("");
|
||||
manager.Shutdown();
|
||||
fs::remove_all(projectRoot);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
@@ -6,6 +6,7 @@
|
||||
#include <XCEngine/Resources/Mesh/MeshLoader.h>
|
||||
#include <XCEngine/Resources/Mesh/MeshImportSettings.h>
|
||||
#include <XCEngine/Resources/Material/Material.h>
|
||||
#include <XCEngine/Resources/Model/Model.h>
|
||||
#include <XCEngine/Resources/Texture/Texture.h>
|
||||
#include <XCEngine/Core/Asset/ResourceTypes.h>
|
||||
#include <XCEngine/Core/Containers/Array.h>
|
||||
@@ -301,11 +302,15 @@ TEST(MeshLoader, ProjectBackpackSampleArtifactRetainsSectionMaterialTextures) {
|
||||
database.Initialize(projectRoot.string().c_str());
|
||||
|
||||
AssetDatabase::ResolvedAsset resolvedAsset;
|
||||
ASSERT_TRUE(database.EnsureArtifact("Assets/Models/backpack/backpack.obj", ResourceType::Mesh, resolvedAsset));
|
||||
ASSERT_TRUE(database.EnsureArtifact("Assets/Models/backpack/backpack.obj", ResourceType::Model, resolvedAsset));
|
||||
ASSERT_TRUE(resolvedAsset.artifactReady);
|
||||
EXPECT_EQ(fs::path(resolvedAsset.artifactMainPath.CStr()).filename().string(), "main.xcmodel");
|
||||
|
||||
const fs::path meshArtifactPath = fs::path(resolvedAsset.artifactDirectory.CStr()) / "mesh_0.xcmesh";
|
||||
ASSERT_TRUE(fs::exists(meshArtifactPath));
|
||||
|
||||
MeshLoader loader;
|
||||
const LoadResult result = loader.Load(resolvedAsset.artifactMainPath.CStr());
|
||||
const LoadResult result = loader.Load(meshArtifactPath.string().c_str());
|
||||
ASSERT_TRUE(result);
|
||||
ASSERT_NE(result.resource, nullptr);
|
||||
|
||||
@@ -370,21 +375,25 @@ TEST(MeshLoader, AssetDatabaseCreatesModelArtifactAndReusesItWithoutReimport) {
|
||||
database.Initialize(projectRoot.string().c_str());
|
||||
|
||||
AssetDatabase::ResolvedAsset firstResolve;
|
||||
ASSERT_TRUE(database.EnsureArtifact("Assets/textured_triangle.obj", ResourceType::Mesh, firstResolve));
|
||||
ASSERT_TRUE(database.EnsureArtifact("Assets/textured_triangle.obj", ResourceType::Model, firstResolve));
|
||||
ASSERT_TRUE(firstResolve.exists);
|
||||
ASSERT_TRUE(firstResolve.artifactReady);
|
||||
EXPECT_EQ(fs::path(firstResolve.artifactMainPath.CStr()).filename().string(), "main.xcmodel");
|
||||
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" / "ArtifactDB" / "artifacts.db"));
|
||||
EXPECT_TRUE(fs::exists(firstResolve.artifactMainPath.CStr()));
|
||||
EXPECT_TRUE(fs::exists((fs::path(firstResolve.artifactDirectory.CStr()) /
|
||||
("material_" + std::to_string(sourceMaterialIndex) + ".xcmat"))));
|
||||
EXPECT_TRUE(fs::exists(fs::path(firstResolve.artifactDirectory.CStr()) / "mesh_0.xcmesh"));
|
||||
EXPECT_TRUE(fs::exists(
|
||||
fs::path(firstResolve.artifactDirectory.CStr()) /
|
||||
("material_" + std::to_string(sourceMaterialIndex) + ".xcmat")));
|
||||
EXPECT_TRUE(fs::exists((fs::path(firstResolve.artifactDirectory.CStr()) / "texture_0.xctex")));
|
||||
|
||||
MaterialLoader materialLoader;
|
||||
LoadResult materialArtifactResult =
|
||||
materialLoader.Load((fs::path(firstResolve.artifactDirectory.CStr()) /
|
||||
("material_" + std::to_string(sourceMaterialIndex) + ".xcmat")).string().c_str());
|
||||
materialLoader.Load(
|
||||
(fs::path(firstResolve.artifactDirectory.CStr()) /
|
||||
("material_" + std::to_string(sourceMaterialIndex) + ".xcmat")).string().c_str());
|
||||
ASSERT_TRUE(materialArtifactResult);
|
||||
ASSERT_NE(materialArtifactResult.resource, nullptr);
|
||||
auto* artifactMaterial = static_cast<Material*>(materialArtifactResult.resource);
|
||||
@@ -405,7 +414,8 @@ TEST(MeshLoader, AssetDatabaseCreatesModelArtifactAndReusesItWithoutReimport) {
|
||||
delete artifactMaterial;
|
||||
|
||||
MeshLoader meshLoader;
|
||||
LoadResult meshArtifactResult = meshLoader.Load(firstResolve.artifactMainPath.CStr());
|
||||
LoadResult meshArtifactResult =
|
||||
meshLoader.Load((fs::path(firstResolve.artifactDirectory.CStr()) / "mesh_0.xcmesh").string().c_str());
|
||||
ASSERT_TRUE(meshArtifactResult);
|
||||
ASSERT_NE(meshArtifactResult.resource, nullptr);
|
||||
auto* artifactMesh = static_cast<Mesh*>(meshArtifactResult.resource);
|
||||
@@ -430,14 +440,14 @@ TEST(MeshLoader, AssetDatabaseCreatesModelArtifactAndReusesItWithoutReimport) {
|
||||
delete artifactMesh;
|
||||
|
||||
AssetRef assetRef;
|
||||
ASSERT_TRUE(database.TryGetAssetRef("Assets/textured_triangle.obj", ResourceType::Mesh, assetRef));
|
||||
ASSERT_TRUE(database.TryGetAssetRef("Assets/textured_triangle.obj", ResourceType::Model, assetRef));
|
||||
EXPECT_TRUE(assetRef.IsValid());
|
||||
|
||||
const auto originalArtifactWriteTime = fs::last_write_time(firstResolve.artifactMainPath.CStr());
|
||||
std::this_thread::sleep_for(50ms);
|
||||
|
||||
AssetDatabase::ResolvedAsset secondResolve;
|
||||
ASSERT_TRUE(database.EnsureArtifact("Assets/textured_triangle.obj", ResourceType::Mesh, secondResolve));
|
||||
ASSERT_TRUE(database.EnsureArtifact("Assets/textured_triangle.obj", ResourceType::Model, secondResolve));
|
||||
EXPECT_EQ(firstResolve.artifactMainPath, secondResolve.artifactMainPath);
|
||||
EXPECT_EQ(originalArtifactWriteTime, fs::last_write_time(secondResolve.artifactMainPath.CStr()));
|
||||
|
||||
@@ -469,7 +479,7 @@ TEST(MeshLoader, AssetDatabaseReimportsModelWhenDependencyChanges) {
|
||||
database.Initialize(projectRoot.string().c_str());
|
||||
|
||||
AssetDatabase::ResolvedAsset firstResolve;
|
||||
ASSERT_TRUE(database.EnsureArtifact("Assets/textured_triangle.obj", ResourceType::Mesh, firstResolve));
|
||||
ASSERT_TRUE(database.EnsureArtifact("Assets/textured_triangle.obj", ResourceType::Model, firstResolve));
|
||||
ASSERT_TRUE(firstResolve.artifactReady);
|
||||
const String firstArtifactPath = firstResolve.artifactMainPath;
|
||||
database.Shutdown();
|
||||
@@ -484,7 +494,7 @@ TEST(MeshLoader, AssetDatabaseReimportsModelWhenDependencyChanges) {
|
||||
|
||||
database.Initialize(projectRoot.string().c_str());
|
||||
AssetDatabase::ResolvedAsset secondResolve;
|
||||
ASSERT_TRUE(database.EnsureArtifact("Assets/textured_triangle.obj", ResourceType::Mesh, secondResolve));
|
||||
ASSERT_TRUE(database.EnsureArtifact("Assets/textured_triangle.obj", ResourceType::Model, secondResolve));
|
||||
ASSERT_TRUE(secondResolve.artifactReady);
|
||||
EXPECT_NE(firstArtifactPath, secondResolve.artifactMainPath);
|
||||
const String secondArtifactPath = secondResolve.artifactMainPath;
|
||||
@@ -495,7 +505,7 @@ TEST(MeshLoader, AssetDatabaseReimportsModelWhenDependencyChanges) {
|
||||
|
||||
database.Initialize(projectRoot.string().c_str());
|
||||
AssetDatabase::ResolvedAsset thirdResolve;
|
||||
ASSERT_TRUE(database.EnsureArtifact("Assets/textured_triangle.obj", ResourceType::Mesh, thirdResolve));
|
||||
ASSERT_TRUE(database.EnsureArtifact("Assets/textured_triangle.obj", ResourceType::Model, thirdResolve));
|
||||
ASSERT_TRUE(thirdResolve.artifactReady);
|
||||
EXPECT_NE(secondArtifactPath, thirdResolve.artifactMainPath);
|
||||
EXPECT_TRUE(fs::exists(thirdResolve.artifactMainPath.CStr()));
|
||||
@@ -504,7 +514,7 @@ TEST(MeshLoader, AssetDatabaseReimportsModelWhenDependencyChanges) {
|
||||
fs::remove_all(projectRoot);
|
||||
}
|
||||
|
||||
TEST(MeshLoader, ResourceManagerLoadsModelByAssetRefFromProjectAssets) {
|
||||
TEST(MeshLoader, ResourceManagerLoadsImportedMeshSubAssetByAssetRefFromProjectAssets) {
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
ResourceManager& manager = ResourceManager::Get();
|
||||
@@ -527,60 +537,97 @@ TEST(MeshLoader, ResourceManagerLoadsModelByAssetRefFromProjectAssets) {
|
||||
|
||||
manager.SetResourceRoot(projectRoot.string().c_str());
|
||||
|
||||
const auto firstHandle = manager.Load<Mesh>("Assets/textured_triangle.obj");
|
||||
ASSERT_TRUE(firstHandle.IsValid());
|
||||
EXPECT_EQ(firstHandle->GetVertexCount(), 3u);
|
||||
EXPECT_EQ(firstHandle->GetIndexCount(), 3u);
|
||||
EXPECT_GE(firstHandle->GetMaterials().Size(), 1u);
|
||||
EXPECT_EQ(firstHandle->GetTextures().Size(), 0u);
|
||||
const auto initialMaterialCount = firstHandle->GetMaterials().Size();
|
||||
const XCEngine::Core::uint32 firstSectionMaterialIndex = GetFirstSectionMaterialIndex(*firstHandle.Get());
|
||||
EXPECT_LT(firstSectionMaterialIndex, initialMaterialCount);
|
||||
Material* firstMaterial = GetFirstSectionMaterial(*firstHandle.Get());
|
||||
ASSERT_NE(firstMaterial, nullptr);
|
||||
ASSERT_NE(firstMaterial->GetShader(), nullptr);
|
||||
EXPECT_EQ(firstMaterial->GetShader()->GetPath(), GetBuiltinForwardLitShaderPath());
|
||||
EXPECT_EQ(firstMaterial->GetTextureBindingCount(), 1u);
|
||||
EXPECT_EQ(firstMaterial->GetTextureBindingName(0), "_MainTex");
|
||||
EXPECT_FALSE(firstMaterial->GetTextureBindingPath(0).Empty());
|
||||
const ResourceHandle<Texture> firstLazyTexture = firstMaterial->GetTexture("_MainTex");
|
||||
EXPECT_FALSE(firstLazyTexture.IsValid());
|
||||
EXPECT_GT(manager.GetAsyncPendingCount(), 0u);
|
||||
ASSERT_TRUE(PumpAsyncLoadsUntilIdle(manager));
|
||||
const ResourceHandle<Texture> firstResolvedTexture = firstMaterial->GetTexture("_MainTex");
|
||||
ASSERT_TRUE(firstResolvedTexture.IsValid());
|
||||
EXPECT_EQ(firstResolvedTexture->GetWidth(), 2u);
|
||||
EXPECT_EQ(firstResolvedTexture->GetHeight(), 2u);
|
||||
{
|
||||
const auto directSourceMesh = manager.Load<Mesh>("Assets/textured_triangle.obj");
|
||||
ASSERT_TRUE(directSourceMesh.IsValid());
|
||||
EXPECT_EQ(directSourceMesh->GetVertexCount(), 3u);
|
||||
EXPECT_EQ(directSourceMesh->GetIndexCount(), 3u);
|
||||
EXPECT_EQ(directSourceMesh->GetTextures().Size(), 1u);
|
||||
Material* directMaterial = GetFirstSectionMaterial(*directSourceMesh.Get());
|
||||
ASSERT_NE(directMaterial, nullptr);
|
||||
const ResourceHandle<Texture> directTexture = directMaterial->GetTexture("_MainTex");
|
||||
ASSERT_TRUE(directTexture.IsValid());
|
||||
EXPECT_EQ(directTexture->GetWidth(), 2u);
|
||||
EXPECT_EQ(directTexture->GetHeight(), 2u);
|
||||
}
|
||||
|
||||
AssetRef assetRef;
|
||||
ASSERT_TRUE(manager.TryGetAssetRef("Assets/textured_triangle.obj", ResourceType::Mesh, assetRef));
|
||||
EXPECT_TRUE(assetRef.IsValid());
|
||||
AssetRef modelAssetRef;
|
||||
AssetRef meshAssetRef;
|
||||
String resolvedMeshPath;
|
||||
{
|
||||
const auto modelHandle = manager.Load<Model>("Assets/textured_triangle.obj");
|
||||
ASSERT_TRUE(modelHandle.IsValid());
|
||||
ASSERT_TRUE(manager.TryGetAssetRef("Assets/textured_triangle.obj", ResourceType::Model, modelAssetRef));
|
||||
ASSERT_TRUE(modelHandle->GetMeshBindings().Size() >= 1u);
|
||||
|
||||
meshAssetRef.assetGuid = modelAssetRef.assetGuid;
|
||||
meshAssetRef.localID = modelHandle->GetMeshBindings()[0].meshLocalID;
|
||||
meshAssetRef.resourceType = ResourceType::Mesh;
|
||||
ASSERT_TRUE(meshAssetRef.IsValid());
|
||||
|
||||
ASSERT_TRUE(manager.TryResolveAssetPath(meshAssetRef, resolvedMeshPath));
|
||||
EXPECT_EQ(fs::path(resolvedMeshPath.CStr()).filename().string(), "mesh_0.xcmesh");
|
||||
}
|
||||
|
||||
manager.UnloadAll();
|
||||
|
||||
const auto secondHandle = manager.Load<Mesh>(assetRef);
|
||||
ASSERT_TRUE(secondHandle.IsValid());
|
||||
EXPECT_EQ(secondHandle->GetPath(), "Assets/textured_triangle.obj");
|
||||
EXPECT_EQ(secondHandle->GetVertexCount(), 3u);
|
||||
EXPECT_EQ(secondHandle->GetIndexCount(), 3u);
|
||||
EXPECT_EQ(secondHandle->GetMaterials().Size(), initialMaterialCount);
|
||||
EXPECT_EQ(secondHandle->GetTextures().Size(), 0u);
|
||||
EXPECT_EQ(GetFirstSectionMaterialIndex(*secondHandle.Get()), firstSectionMaterialIndex);
|
||||
Material* secondMaterial = GetFirstSectionMaterial(*secondHandle.Get());
|
||||
ASSERT_NE(secondMaterial, nullptr);
|
||||
ASSERT_NE(secondMaterial->GetShader(), nullptr);
|
||||
EXPECT_EQ(secondMaterial->GetShader()->GetPath(), GetBuiltinForwardLitShaderPath());
|
||||
EXPECT_EQ(secondMaterial->GetTextureBindingCount(), 1u);
|
||||
EXPECT_EQ(secondMaterial->GetTextureBindingName(0), "_MainTex");
|
||||
EXPECT_FALSE(secondMaterial->GetTextureBindingPath(0).Empty());
|
||||
const ResourceHandle<Texture> secondLazyTexture = secondMaterial->GetTexture("_MainTex");
|
||||
EXPECT_FALSE(secondLazyTexture.IsValid());
|
||||
EXPECT_GT(manager.GetAsyncPendingCount(), 0u);
|
||||
ASSERT_TRUE(PumpAsyncLoadsUntilIdle(manager));
|
||||
const ResourceHandle<Texture> secondResolvedTexture = secondMaterial->GetTexture("_MainTex");
|
||||
ASSERT_TRUE(secondResolvedTexture.IsValid());
|
||||
EXPECT_EQ(secondResolvedTexture->GetWidth(), 2u);
|
||||
EXPECT_EQ(secondResolvedTexture->GetHeight(), 2u);
|
||||
XCEngine::Core::uint32 initialMaterialCount = 0;
|
||||
XCEngine::Core::uint32 firstSectionMaterialIndex = 0;
|
||||
{
|
||||
const auto firstHandle = manager.Load<Mesh>(meshAssetRef);
|
||||
ASSERT_TRUE(firstHandle.IsValid());
|
||||
EXPECT_EQ(firstHandle->GetPath(), resolvedMeshPath);
|
||||
EXPECT_EQ(firstHandle->GetVertexCount(), 3u);
|
||||
EXPECT_EQ(firstHandle->GetIndexCount(), 3u);
|
||||
EXPECT_GE(firstHandle->GetMaterials().Size(), 1u);
|
||||
EXPECT_EQ(firstHandle->GetTextures().Size(), 0u);
|
||||
initialMaterialCount = firstHandle->GetMaterials().Size();
|
||||
firstSectionMaterialIndex = GetFirstSectionMaterialIndex(*firstHandle.Get());
|
||||
EXPECT_LT(firstSectionMaterialIndex, initialMaterialCount);
|
||||
Material* firstMaterial = GetFirstSectionMaterial(*firstHandle.Get());
|
||||
ASSERT_NE(firstMaterial, nullptr);
|
||||
ASSERT_NE(firstMaterial->GetShader(), nullptr);
|
||||
EXPECT_EQ(firstMaterial->GetShader()->GetPath(), GetBuiltinForwardLitShaderPath());
|
||||
EXPECT_EQ(firstMaterial->GetTextureBindingCount(), 1u);
|
||||
EXPECT_EQ(firstMaterial->GetTextureBindingName(0), "_MainTex");
|
||||
EXPECT_FALSE(firstMaterial->GetTextureBindingPath(0).Empty());
|
||||
const ResourceHandle<Texture> firstLazyTexture = firstMaterial->GetTexture("_MainTex");
|
||||
EXPECT_FALSE(firstLazyTexture.IsValid());
|
||||
EXPECT_GT(manager.GetAsyncPendingCount(), 0u);
|
||||
ASSERT_TRUE(PumpAsyncLoadsUntilIdle(manager));
|
||||
const ResourceHandle<Texture> firstResolvedTexture = firstMaterial->GetTexture("_MainTex");
|
||||
ASSERT_TRUE(firstResolvedTexture.IsValid());
|
||||
EXPECT_EQ(firstResolvedTexture->GetWidth(), 2u);
|
||||
EXPECT_EQ(firstResolvedTexture->GetHeight(), 2u);
|
||||
}
|
||||
|
||||
manager.UnloadAll();
|
||||
|
||||
{
|
||||
const auto secondHandle = manager.Load<Mesh>(meshAssetRef);
|
||||
ASSERT_TRUE(secondHandle.IsValid());
|
||||
EXPECT_EQ(secondHandle->GetPath(), resolvedMeshPath);
|
||||
EXPECT_EQ(secondHandle->GetVertexCount(), 3u);
|
||||
EXPECT_EQ(secondHandle->GetIndexCount(), 3u);
|
||||
EXPECT_EQ(secondHandle->GetMaterials().Size(), initialMaterialCount);
|
||||
EXPECT_EQ(secondHandle->GetTextures().Size(), 0u);
|
||||
EXPECT_EQ(GetFirstSectionMaterialIndex(*secondHandle.Get()), firstSectionMaterialIndex);
|
||||
Material* secondMaterial = GetFirstSectionMaterial(*secondHandle.Get());
|
||||
ASSERT_NE(secondMaterial, nullptr);
|
||||
ASSERT_NE(secondMaterial->GetShader(), nullptr);
|
||||
EXPECT_EQ(secondMaterial->GetShader()->GetPath(), GetBuiltinForwardLitShaderPath());
|
||||
EXPECT_EQ(secondMaterial->GetTextureBindingCount(), 1u);
|
||||
EXPECT_EQ(secondMaterial->GetTextureBindingName(0), "_MainTex");
|
||||
EXPECT_FALSE(secondMaterial->GetTextureBindingPath(0).Empty());
|
||||
const ResourceHandle<Texture> secondLazyTexture = secondMaterial->GetTexture("_MainTex");
|
||||
EXPECT_FALSE(secondLazyTexture.IsValid());
|
||||
EXPECT_GT(manager.GetAsyncPendingCount(), 0u);
|
||||
ASSERT_TRUE(PumpAsyncLoadsUntilIdle(manager));
|
||||
const ResourceHandle<Texture> secondResolvedTexture = secondMaterial->GetTexture("_MainTex");
|
||||
ASSERT_TRUE(secondResolvedTexture.IsValid());
|
||||
EXPECT_EQ(secondResolvedTexture->GetWidth(), 2u);
|
||||
EXPECT_EQ(secondResolvedTexture->GetHeight(), 2u);
|
||||
}
|
||||
|
||||
manager.SetResourceRoot("");
|
||||
manager.Shutdown();
|
||||
|
||||
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