Add model and GaussianSplat asset pipelines

This commit is contained in:
2026-04-10 20:55:48 +08:00
parent 8f5c342799
commit 503e6408ed
39 changed files with 5900 additions and 141 deletions

View File

@@ -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

View File

@@ -3,6 +3,8 @@
# ============================================================
add_subdirectory(Texture)
add_subdirectory(GaussianSplat)
add_subdirectory(Model)
add_subdirectory(Mesh)
add_subdirectory(Material)
add_subdirectory(Shader)

View 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)

View 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

View 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

View File

@@ -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();

View 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)

View 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

View 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

View 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