833 lines
32 KiB
C++
833 lines
32 KiB
C++
#define NOMINMAX
|
|
#include <windows.h>
|
|
|
|
#include <gtest/gtest.h>
|
|
|
|
#include "../RenderingIntegrationMain.h"
|
|
#include "../RenderingIntegrationImageAssert.h"
|
|
|
|
#include <XCEngine/Components/CameraComponent.h>
|
|
#include <XCEngine/Components/GameObject.h>
|
|
#include <XCEngine/Components/GaussianSplatRendererComponent.h>
|
|
#include <XCEngine/Core/Asset/AssetDatabase.h>
|
|
#include <XCEngine/Core/Asset/IResource.h>
|
|
#include <XCEngine/Core/Asset/ResourceManager.h>
|
|
#include <XCEngine/Core/Math/Color.h>
|
|
#include <XCEngine/Core/Math/Vector3.h>
|
|
#include <XCEngine/Rendering/Execution/SceneRenderer.h>
|
|
#include <XCEngine/Rendering/RenderContext.h>
|
|
#include <XCEngine/Rendering/RenderSurface.h>
|
|
#include <XCEngine/Resources/GaussianSplat/GaussianSplatArtifactIO.h>
|
|
#include <XCEngine/Resources/GaussianSplat/GaussianSplat.h>
|
|
#include <XCEngine/Resources/Material/Material.h>
|
|
#include <XCEngine/Resources/BuiltinResources.h>
|
|
#include <XCEngine/Resources/Shader/Shader.h>
|
|
#include <XCEngine/RHI/RHITexture.h>
|
|
#include <XCEngine/Scene/Scene.h>
|
|
|
|
#include "../../../RHI/integration/fixtures/RHIIntegrationFixture.h"
|
|
|
|
#include <algorithm>
|
|
#include <cstdio>
|
|
#include <cstdlib>
|
|
#include <cstring>
|
|
#include <filesystem>
|
|
#include <limits>
|
|
#include <memory>
|
|
#include <string>
|
|
#include <system_error>
|
|
#include <vector>
|
|
|
|
using namespace XCEngine::Components;
|
|
using namespace XCEngine::Math;
|
|
using namespace XCEngine::Rendering;
|
|
using namespace XCEngine::Resources;
|
|
using namespace XCEngine::RHI;
|
|
using namespace XCEngine::RHI::Integration;
|
|
using namespace RenderingIntegrationTestUtils;
|
|
|
|
namespace {
|
|
|
|
constexpr const char* kD3D12Screenshot = "gaussian_splat_scene_d3d12.ppm";
|
|
constexpr const char* kOpenGLScreenshot = "gaussian_splat_scene_opengl.ppm";
|
|
constexpr const char* kVulkanScreenshot = "gaussian_splat_scene_vulkan.ppm";
|
|
constexpr const char* kD3D12AlphaDebugScreenshot = "gaussian_splat_scene_d3d12_alpha.ppm";
|
|
constexpr const char* kOpenGLAlphaDebugScreenshot = "gaussian_splat_scene_opengl_alpha.ppm";
|
|
constexpr const char* kVulkanAlphaDebugScreenshot = "gaussian_splat_scene_vulkan_alpha.ppm";
|
|
constexpr uint32_t kFrameWidth = 1280;
|
|
constexpr uint32_t kFrameHeight = 720;
|
|
constexpr uint32_t kBaselineSubsetSplatCount = 262144u;
|
|
constexpr const char* kSubsetGaussianSplatAssetPath = "Assets/room_subset.xcgsplat";
|
|
constexpr float kTargetSceneExtent = 4.0f;
|
|
constexpr float kGaussianPointScale = 1.00f;
|
|
const Vector3 kDefaultCameraPosition(0.0f, 1.0f, 1.0f);
|
|
const Vector3 kDefaultCameraLookAt(0.0f, 1.0f, 0.0f);
|
|
const Vector3 kDefaultRootPosition = Vector3::Zero();
|
|
|
|
enum class GaussianSplatDebugView : uint8_t {
|
|
Scene = 0,
|
|
Alpha = 1
|
|
};
|
|
|
|
XCEngine::Core::uint16 FloatToHalfBits(float value) {
|
|
uint32_t bits = 0u;
|
|
std::memcpy(&bits, &value, sizeof(bits));
|
|
|
|
const uint32_t sign = (bits >> 16u) & 0x8000u;
|
|
uint32_t mantissa = bits & 0x007fffffu;
|
|
int32_t exponent = static_cast<int32_t>((bits >> 23u) & 0xffu) - 127 + 15;
|
|
|
|
if (exponent <= 0) {
|
|
if (exponent < -10) {
|
|
return static_cast<XCEngine::Core::uint16>(sign);
|
|
}
|
|
|
|
mantissa = (mantissa | 0x00800000u) >> static_cast<uint32_t>(1 - exponent);
|
|
if ((mantissa & 0x00001000u) != 0u) {
|
|
mantissa += 0x00002000u;
|
|
}
|
|
|
|
return static_cast<XCEngine::Core::uint16>(sign | (mantissa >> 13u));
|
|
}
|
|
|
|
if (exponent >= 31) {
|
|
return static_cast<XCEngine::Core::uint16>(sign | 0x7c00u);
|
|
}
|
|
|
|
if ((mantissa & 0x00001000u) != 0u) {
|
|
mantissa += 0x00002000u;
|
|
if ((mantissa & 0x00800000u) != 0u) {
|
|
mantissa = 0u;
|
|
++exponent;
|
|
if (exponent >= 31) {
|
|
return static_cast<XCEngine::Core::uint16>(sign | 0x7c00u);
|
|
}
|
|
}
|
|
}
|
|
|
|
return static_cast<XCEngine::Core::uint16>(
|
|
sign |
|
|
(static_cast<uint32_t>(exponent) << 10u) |
|
|
(mantissa >> 13u));
|
|
}
|
|
|
|
uint32_t PackHalfRange(float minValue, float maxValue) {
|
|
return static_cast<uint32_t>(FloatToHalfBits(minValue)) |
|
|
(static_cast<uint32_t>(FloatToHalfBits(maxValue)) << 16u);
|
|
}
|
|
|
|
std::filesystem::path GetRoomPlyPath() {
|
|
return std::filesystem::path(XCENGINE_TEST_ROOM_PLY_PATH);
|
|
}
|
|
|
|
std::filesystem::path CreateRuntimeProjectRoot() {
|
|
return GetExecutableDirectory() /
|
|
("__xc_gaussian_splat_scene_runtime_" + std::to_string(static_cast<unsigned long>(::GetCurrentProcessId())));
|
|
}
|
|
|
|
void LinkOrCopyFixture(const std::filesystem::path& sourcePath, const std::filesystem::path& destinationPath) {
|
|
std::error_code ec;
|
|
std::filesystem::create_directories(destinationPath.parent_path(), ec);
|
|
ec.clear();
|
|
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) << ec.message();
|
|
}
|
|
|
|
GaussianSplatDebugView GetDebugViewFromEnvironment() {
|
|
const char* debugViewValue = std::getenv("XCENGINE_GAUSSIAN_SPLAT_DEBUG_VIEW");
|
|
if (debugViewValue == nullptr) {
|
|
return GaussianSplatDebugView::Scene;
|
|
}
|
|
|
|
if (_stricmp(debugViewValue, "alpha") == 0 ||
|
|
std::strcmp(debugViewValue, "1") == 0) {
|
|
return GaussianSplatDebugView::Alpha;
|
|
}
|
|
|
|
return GaussianSplatDebugView::Scene;
|
|
}
|
|
|
|
float GetFloatFromEnvironment(const char* name, float defaultValue) {
|
|
const char* value = std::getenv(name);
|
|
if (value == nullptr || value[0] == '\0') {
|
|
return defaultValue;
|
|
}
|
|
|
|
char* end = nullptr;
|
|
const float parsedValue = std::strtof(value, &end);
|
|
return end != value ? parsedValue : defaultValue;
|
|
}
|
|
|
|
uint32_t GetUIntFromEnvironment(const char* name, uint32_t defaultValue) {
|
|
const char* value = std::getenv(name);
|
|
if (value == nullptr || value[0] == '\0') {
|
|
return defaultValue;
|
|
}
|
|
|
|
char* end = nullptr;
|
|
const unsigned long parsedValue = std::strtoul(value, &end, 10);
|
|
return end != value ? static_cast<uint32_t>(parsedValue) : defaultValue;
|
|
}
|
|
|
|
Vector3 GetVector3FromEnvironment(const char* name, const Vector3& defaultValue) {
|
|
const char* value = std::getenv(name);
|
|
if (value == nullptr || value[0] == '\0') {
|
|
return defaultValue;
|
|
}
|
|
|
|
float x = 0.0f;
|
|
float y = 0.0f;
|
|
float z = 0.0f;
|
|
if (std::sscanf(value, "%f,%f,%f", &x, &y, &z) == 3) {
|
|
return Vector3(x, y, z);
|
|
}
|
|
|
|
return defaultValue;
|
|
}
|
|
|
|
void ExpectVector3Near(const Vector3& actual, const Vector3& expected, float epsilon = 1.0e-6f) {
|
|
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 = 1.0e-6f) {
|
|
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 = 1.0e-6f) {
|
|
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 ExpectGaussianSplatRoundTripMatches(
|
|
const GaussianSplat& source,
|
|
const GaussianSplat& loaded) {
|
|
ASSERT_EQ(source.GetSplatCount(), loaded.GetSplatCount());
|
|
ASSERT_EQ(source.GetChunkCount(), loaded.GetChunkCount());
|
|
ASSERT_EQ(source.GetSHOrder(), loaded.GetSHOrder());
|
|
ExpectVector3Near(loaded.GetBounds().GetMin(), source.GetBounds().GetMin());
|
|
ExpectVector3Near(loaded.GetBounds().GetMax(), source.GetBounds().GetMax());
|
|
ASSERT_NE(source.GetPositionRecords(), nullptr);
|
|
ASSERT_NE(loaded.GetPositionRecords(), nullptr);
|
|
ASSERT_NE(source.GetOtherRecords(), nullptr);
|
|
ASSERT_NE(loaded.GetOtherRecords(), nullptr);
|
|
ASSERT_NE(source.GetColorRecords(), nullptr);
|
|
ASSERT_NE(loaded.GetColorRecords(), nullptr);
|
|
ASSERT_NE(source.GetSHRecords(), nullptr);
|
|
ASSERT_NE(loaded.GetSHRecords(), nullptr);
|
|
|
|
const uint32_t sampleIndices[] = {
|
|
0u,
|
|
source.GetSplatCount() / 2u,
|
|
source.GetSplatCount() - 1u
|
|
};
|
|
for (uint32_t sampleIndex : sampleIndices) {
|
|
ExpectVector3Near(
|
|
loaded.GetPositionRecords()[sampleIndex].position,
|
|
source.GetPositionRecords()[sampleIndex].position);
|
|
ExpectQuaternionNear(
|
|
loaded.GetOtherRecords()[sampleIndex].rotation,
|
|
source.GetOtherRecords()[sampleIndex].rotation);
|
|
ExpectVector3Near(
|
|
loaded.GetOtherRecords()[sampleIndex].scale,
|
|
source.GetOtherRecords()[sampleIndex].scale);
|
|
ExpectVector4Near(
|
|
loaded.GetColorRecords()[sampleIndex].colorOpacity,
|
|
source.GetColorRecords()[sampleIndex].colorOpacity);
|
|
|
|
for (uint32_t coefficientIndex = 0u; coefficientIndex < kGaussianSplatSHCoefficientCount; ++coefficientIndex) {
|
|
EXPECT_NEAR(
|
|
loaded.GetSHRecords()[sampleIndex].coefficients[coefficientIndex],
|
|
source.GetSHRecords()[sampleIndex].coefficients[coefficientIndex],
|
|
1.0e-6f);
|
|
}
|
|
}
|
|
|
|
const auto* sourceChunkSection = static_cast<const GaussianSplatChunkRecord*>(
|
|
source.GetSectionData(GaussianSplatSectionType::Chunks));
|
|
const auto* loadedChunkSection = static_cast<const GaussianSplatChunkRecord*>(
|
|
loaded.GetSectionData(GaussianSplatSectionType::Chunks));
|
|
ASSERT_NE(sourceChunkSection, nullptr);
|
|
ASSERT_NE(loadedChunkSection, nullptr);
|
|
if (source.GetChunkCount() > 0u) {
|
|
const uint32_t chunkIndices[] = { 0u, source.GetChunkCount() - 1u };
|
|
for (uint32_t chunkIndex : chunkIndices) {
|
|
EXPECT_EQ(loadedChunkSection[chunkIndex].colR, sourceChunkSection[chunkIndex].colR);
|
|
EXPECT_EQ(loadedChunkSection[chunkIndex].colG, sourceChunkSection[chunkIndex].colG);
|
|
EXPECT_EQ(loadedChunkSection[chunkIndex].colB, sourceChunkSection[chunkIndex].colB);
|
|
EXPECT_EQ(loadedChunkSection[chunkIndex].colA, sourceChunkSection[chunkIndex].colA);
|
|
ExpectVector3Near(
|
|
Vector3(
|
|
loadedChunkSection[chunkIndex].posX.x,
|
|
loadedChunkSection[chunkIndex].posY.x,
|
|
loadedChunkSection[chunkIndex].posZ.x),
|
|
Vector3(
|
|
sourceChunkSection[chunkIndex].posX.x,
|
|
sourceChunkSection[chunkIndex].posY.x,
|
|
sourceChunkSection[chunkIndex].posZ.x));
|
|
ExpectVector3Near(
|
|
Vector3(
|
|
loadedChunkSection[chunkIndex].posX.y,
|
|
loadedChunkSection[chunkIndex].posY.y,
|
|
loadedChunkSection[chunkIndex].posZ.y),
|
|
Vector3(
|
|
sourceChunkSection[chunkIndex].posX.y,
|
|
sourceChunkSection[chunkIndex].posY.y,
|
|
sourceChunkSection[chunkIndex].posZ.y));
|
|
EXPECT_EQ(loadedChunkSection[chunkIndex].sclX, sourceChunkSection[chunkIndex].sclX);
|
|
EXPECT_EQ(loadedChunkSection[chunkIndex].sclY, sourceChunkSection[chunkIndex].sclY);
|
|
EXPECT_EQ(loadedChunkSection[chunkIndex].sclZ, sourceChunkSection[chunkIndex].sclZ);
|
|
EXPECT_EQ(loadedChunkSection[chunkIndex].shR, sourceChunkSection[chunkIndex].shR);
|
|
EXPECT_EQ(loadedChunkSection[chunkIndex].shG, sourceChunkSection[chunkIndex].shG);
|
|
EXPECT_EQ(loadedChunkSection[chunkIndex].shB, sourceChunkSection[chunkIndex].shB);
|
|
}
|
|
}
|
|
}
|
|
|
|
const char* GetScreenshotFilename(RHIType backendType, GaussianSplatDebugView debugView) {
|
|
switch (backendType) {
|
|
case RHIType::D3D12:
|
|
return debugView == GaussianSplatDebugView::Alpha
|
|
? kD3D12AlphaDebugScreenshot
|
|
: kD3D12Screenshot;
|
|
case RHIType::Vulkan:
|
|
return debugView == GaussianSplatDebugView::Alpha
|
|
? kVulkanAlphaDebugScreenshot
|
|
: kVulkanScreenshot;
|
|
case RHIType::OpenGL:
|
|
default:
|
|
return debugView == GaussianSplatDebugView::Alpha
|
|
? kOpenGLAlphaDebugScreenshot
|
|
: kOpenGLScreenshot;
|
|
}
|
|
}
|
|
|
|
GaussianSplat* CreateGaussianSplatSubset(
|
|
const GaussianSplat& source,
|
|
uint32_t maxSplatCount,
|
|
const char* path) {
|
|
const uint32_t sourceSplatCount = source.GetSplatCount();
|
|
const uint32_t subsetSplatCount = std::min(sourceSplatCount, maxSplatCount);
|
|
if (subsetSplatCount == 0u) {
|
|
return nullptr;
|
|
}
|
|
|
|
const GaussianSplatPositionRecord* positions = source.GetPositionRecords();
|
|
const GaussianSplatOtherRecord* other = source.GetOtherRecords();
|
|
const GaussianSplatColorRecord* colors = source.GetColorRecords();
|
|
const GaussianSplatSHRecord* sh = source.GetSHRecords();
|
|
const GaussianSplatSection* positionsSection = source.FindSection(GaussianSplatSectionType::Positions);
|
|
const GaussianSplatSection* otherSection = source.FindSection(GaussianSplatSectionType::Other);
|
|
const GaussianSplatSection* colorSection = source.FindSection(GaussianSplatSectionType::Color);
|
|
const GaussianSplatSection* shSection = source.FindSection(GaussianSplatSectionType::SH);
|
|
if (positions == nullptr ||
|
|
other == nullptr ||
|
|
colors == nullptr ||
|
|
positionsSection == nullptr ||
|
|
otherSection == nullptr ||
|
|
colorSection == nullptr) {
|
|
return nullptr;
|
|
}
|
|
|
|
std::vector<uint32_t> selectedIndices(subsetSplatCount, 0u);
|
|
for (uint32_t subsetIndex = 0u; subsetIndex < subsetSplatCount; ++subsetIndex) {
|
|
const uint64_t scaledIndex =
|
|
(static_cast<uint64_t>(subsetIndex) * static_cast<uint64_t>(sourceSplatCount)) /
|
|
static_cast<uint64_t>(subsetSplatCount);
|
|
selectedIndices[subsetIndex] =
|
|
static_cast<uint32_t>(std::min<uint64_t>(scaledIndex, static_cast<uint64_t>(sourceSplatCount - 1u)));
|
|
}
|
|
|
|
std::vector<GaussianSplatPositionRecord> subsetPositions(subsetSplatCount);
|
|
std::vector<GaussianSplatOtherRecord> subsetOther(subsetSplatCount);
|
|
std::vector<GaussianSplatColorRecord> subsetColors(subsetSplatCount);
|
|
std::vector<GaussianSplatSHRecord> subsetSh(shSection != nullptr && sh != nullptr ? subsetSplatCount : 0u);
|
|
Bounds subsetBounds;
|
|
bool hasSubsetBounds = false;
|
|
for (uint32_t subsetIndex = 0u; subsetIndex < subsetSplatCount; ++subsetIndex) {
|
|
const uint32_t sourceIndex = selectedIndices[subsetIndex];
|
|
subsetPositions[subsetIndex] = positions[sourceIndex];
|
|
subsetOther[subsetIndex] = other[sourceIndex];
|
|
subsetColors[subsetIndex] = colors[sourceIndex];
|
|
if (!subsetSh.empty()) {
|
|
subsetSh[subsetIndex] = sh[sourceIndex];
|
|
}
|
|
|
|
const Vector3& position = subsetPositions[subsetIndex].position;
|
|
if (!hasSubsetBounds) {
|
|
subsetBounds.SetMinMax(position, position);
|
|
hasSubsetBounds = true;
|
|
} else {
|
|
subsetBounds.Encapsulate(position);
|
|
}
|
|
}
|
|
|
|
const uint32_t subsetChunkCount =
|
|
(subsetSplatCount + (kGaussianSplatChunkSize - 1u)) / kGaussianSplatChunkSize;
|
|
std::vector<GaussianSplatChunkRecord> subsetChunks(subsetChunkCount);
|
|
for (uint32_t chunkIndex = 0u; chunkIndex < subsetChunkCount; ++chunkIndex) {
|
|
const uint32_t startIndex = chunkIndex * kGaussianSplatChunkSize;
|
|
const uint32_t endIndex = std::min(startIndex + kGaussianSplatChunkSize, subsetSplatCount);
|
|
|
|
Vector3 minPosition(
|
|
std::numeric_limits<float>::max(),
|
|
std::numeric_limits<float>::max(),
|
|
std::numeric_limits<float>::max());
|
|
Vector3 maxPosition(
|
|
-std::numeric_limits<float>::max(),
|
|
-std::numeric_limits<float>::max(),
|
|
-std::numeric_limits<float>::max());
|
|
Vector3 minScale(
|
|
std::numeric_limits<float>::max(),
|
|
std::numeric_limits<float>::max(),
|
|
std::numeric_limits<float>::max());
|
|
Vector3 maxScale(
|
|
-std::numeric_limits<float>::max(),
|
|
-std::numeric_limits<float>::max(),
|
|
-std::numeric_limits<float>::max());
|
|
|
|
for (uint32_t subsetIndex = startIndex; subsetIndex < endIndex; ++subsetIndex) {
|
|
const Vector3& position = subsetPositions[subsetIndex].position;
|
|
const Vector3& scale = subsetOther[subsetIndex].scale;
|
|
minPosition.x = std::min(minPosition.x, position.x);
|
|
minPosition.y = std::min(minPosition.y, position.y);
|
|
minPosition.z = std::min(minPosition.z, position.z);
|
|
maxPosition.x = std::max(maxPosition.x, position.x);
|
|
maxPosition.y = std::max(maxPosition.y, position.y);
|
|
maxPosition.z = std::max(maxPosition.z, position.z);
|
|
minScale.x = std::min(minScale.x, scale.x);
|
|
minScale.y = std::min(minScale.y, scale.y);
|
|
minScale.z = std::min(minScale.z, scale.z);
|
|
maxScale.x = std::max(maxScale.x, scale.x);
|
|
maxScale.y = std::max(maxScale.y, scale.y);
|
|
maxScale.z = std::max(maxScale.z, scale.z);
|
|
}
|
|
|
|
GaussianSplatChunkRecord chunk = {};
|
|
chunk.posX = Vector2(minPosition.x, maxPosition.x);
|
|
chunk.posY = Vector2(minPosition.y, maxPosition.y);
|
|
chunk.posZ = Vector2(minPosition.z, maxPosition.z);
|
|
chunk.sclX = PackHalfRange(minScale.x, maxScale.x);
|
|
chunk.sclY = PackHalfRange(minScale.y, maxScale.y);
|
|
chunk.sclZ = PackHalfRange(minScale.z, maxScale.z);
|
|
subsetChunks[chunkIndex] = chunk;
|
|
}
|
|
|
|
XCEngine::Containers::Array<GaussianSplatSection> sections;
|
|
XCEngine::Containers::Array<XCEngine::Core::uint8> payload;
|
|
sections.Reserve(shSection != nullptr && sh != nullptr ? 5u : 4u);
|
|
|
|
size_t payloadOffset = 0u;
|
|
auto appendSection = [&](const GaussianSplatSection& sourceSection,
|
|
const void* sourceData,
|
|
uint32_t elementCount,
|
|
uint32_t elementStride) {
|
|
GaussianSplatSection section = sourceSection;
|
|
section.dataOffset = payloadOffset;
|
|
section.elementCount = elementCount;
|
|
section.elementStride = elementStride;
|
|
section.dataSize = static_cast<XCEngine::Core::uint64>(elementCount) * elementStride;
|
|
sections.PushBack(section);
|
|
|
|
const size_t newPayloadSize = payload.Size() + static_cast<size_t>(section.dataSize);
|
|
payload.Resize(newPayloadSize);
|
|
std::memcpy(payload.Data() + payloadOffset, sourceData, static_cast<size_t>(section.dataSize));
|
|
payloadOffset = newPayloadSize;
|
|
};
|
|
|
|
appendSection(
|
|
*positionsSection,
|
|
subsetPositions.data(),
|
|
subsetSplatCount,
|
|
sizeof(GaussianSplatPositionRecord));
|
|
appendSection(
|
|
*otherSection,
|
|
subsetOther.data(),
|
|
subsetSplatCount,
|
|
sizeof(GaussianSplatOtherRecord));
|
|
appendSection(
|
|
*colorSection,
|
|
subsetColors.data(),
|
|
subsetSplatCount,
|
|
sizeof(GaussianSplatColorRecord));
|
|
if (shSection != nullptr && sh != nullptr) {
|
|
appendSection(
|
|
*shSection,
|
|
subsetSh.data(),
|
|
subsetSplatCount,
|
|
sizeof(GaussianSplatSHRecord));
|
|
}
|
|
GaussianSplatSection chunksSection = {};
|
|
chunksSection.type = GaussianSplatSectionType::Chunks;
|
|
chunksSection.format = GaussianSplatSectionFormat::ChunkFloat32;
|
|
appendSection(
|
|
chunksSection,
|
|
subsetChunks.data(),
|
|
subsetChunkCount,
|
|
sizeof(GaussianSplatChunkRecord));
|
|
|
|
GaussianSplatMetadata metadata = source.GetMetadata();
|
|
metadata.splatCount = subsetSplatCount;
|
|
metadata.bounds = subsetBounds;
|
|
metadata.chunkCount = subsetChunkCount;
|
|
metadata.cameraCount = 0u;
|
|
metadata.chunkFormat = GaussianSplatSectionFormat::ChunkFloat32;
|
|
metadata.cameraFormat = GaussianSplatSectionFormat::Unknown;
|
|
|
|
auto* gaussianSplat = new GaussianSplat();
|
|
IResource::ConstructParams params = {};
|
|
params.name = "GaussianSplatSceneSubset";
|
|
params.path = path;
|
|
params.guid = ResourceGUID::Generate(params.path);
|
|
gaussianSplat->Initialize(params);
|
|
if (!gaussianSplat->CreateOwned(metadata, std::move(sections), std::move(payload))) {
|
|
delete gaussianSplat;
|
|
return nullptr;
|
|
}
|
|
|
|
return gaussianSplat;
|
|
}
|
|
|
|
class GaussianSplatSceneTest : public RHIIntegrationFixture {
|
|
protected:
|
|
void SetUp() override;
|
|
void TearDown() override;
|
|
void RenderFrame() override;
|
|
|
|
private:
|
|
void PrepareRuntimeProject();
|
|
void BuildScene();
|
|
RHIResourceView* GetCurrentBackBufferView();
|
|
|
|
std::filesystem::path m_projectRoot;
|
|
std::unique_ptr<Scene> m_scene;
|
|
std::unique_ptr<SceneRenderer> m_sceneRenderer;
|
|
std::vector<RHIResourceView*> m_backBufferViews;
|
|
RHITexture* m_depthTexture = nullptr;
|
|
RHIResourceView* m_depthView = nullptr;
|
|
ResourceHandle<GaussianSplat> m_gaussianSplat;
|
|
ResourceHandle<GaussianSplat> m_subsetGaussianSplat;
|
|
Material* m_material = nullptr;
|
|
GaussianSplatDebugView m_debugView = GaussianSplatDebugView::Scene;
|
|
};
|
|
|
|
void GaussianSplatSceneTest::SetUp() {
|
|
RHIIntegrationFixture::SetUp();
|
|
m_debugView = GetDebugViewFromEnvironment();
|
|
PrepareRuntimeProject();
|
|
|
|
m_sceneRenderer = std::make_unique<SceneRenderer>();
|
|
m_scene = std::make_unique<Scene>("GaussianSplatScene");
|
|
|
|
BuildScene();
|
|
|
|
TextureDesc depthDesc = {};
|
|
depthDesc.width = kFrameWidth;
|
|
depthDesc.height = kFrameHeight;
|
|
depthDesc.depth = 1;
|
|
depthDesc.mipLevels = 1;
|
|
depthDesc.arraySize = 1;
|
|
depthDesc.format = static_cast<uint32_t>(Format::D24_UNorm_S8_UInt);
|
|
depthDesc.textureType = static_cast<uint32_t>(XCEngine::RHI::TextureType::Texture2D);
|
|
depthDesc.sampleCount = 1;
|
|
depthDesc.sampleQuality = 0;
|
|
depthDesc.flags = 0;
|
|
m_depthTexture = GetDevice()->CreateTexture(depthDesc);
|
|
ASSERT_NE(m_depthTexture, nullptr);
|
|
|
|
ResourceViewDesc depthViewDesc = {};
|
|
depthViewDesc.format = static_cast<uint32_t>(Format::D24_UNorm_S8_UInt);
|
|
depthViewDesc.dimension = ResourceViewDimension::Texture2D;
|
|
depthViewDesc.mipLevel = 0;
|
|
m_depthView = GetDevice()->CreateDepthStencilView(m_depthTexture, depthViewDesc);
|
|
ASSERT_NE(m_depthView, nullptr);
|
|
|
|
m_backBufferViews.resize(2, nullptr);
|
|
}
|
|
|
|
void GaussianSplatSceneTest::TearDown() {
|
|
m_sceneRenderer.reset();
|
|
|
|
if (m_depthView != nullptr) {
|
|
m_depthView->Shutdown();
|
|
delete m_depthView;
|
|
m_depthView = nullptr;
|
|
}
|
|
|
|
if (m_depthTexture != nullptr) {
|
|
m_depthTexture->Shutdown();
|
|
delete m_depthTexture;
|
|
m_depthTexture = nullptr;
|
|
}
|
|
|
|
for (RHIResourceView*& backBufferView : m_backBufferViews) {
|
|
if (backBufferView != nullptr) {
|
|
backBufferView->Shutdown();
|
|
delete backBufferView;
|
|
backBufferView = nullptr;
|
|
}
|
|
}
|
|
m_backBufferViews.clear();
|
|
|
|
m_scene.reset();
|
|
|
|
delete m_material;
|
|
m_material = nullptr;
|
|
m_subsetGaussianSplat.Reset();
|
|
m_gaussianSplat.Reset();
|
|
|
|
ResourceManager& manager = ResourceManager::Get();
|
|
manager.UnloadAll();
|
|
manager.SetResourceRoot("");
|
|
manager.Shutdown();
|
|
|
|
if (!m_projectRoot.empty()) {
|
|
std::error_code ec;
|
|
std::filesystem::remove_all(m_projectRoot, ec);
|
|
}
|
|
|
|
RHIIntegrationFixture::TearDown();
|
|
}
|
|
|
|
void GaussianSplatSceneTest::PrepareRuntimeProject() {
|
|
const std::filesystem::path roomPlyPath = GetRoomPlyPath();
|
|
ASSERT_TRUE(std::filesystem::exists(roomPlyPath)) << roomPlyPath.string();
|
|
|
|
m_projectRoot = CreateRuntimeProjectRoot();
|
|
const std::filesystem::path assetsDir = m_projectRoot / "Assets";
|
|
const std::filesystem::path runtimeRoomPath = assetsDir / "room.ply";
|
|
|
|
std::error_code ec;
|
|
std::filesystem::remove_all(m_projectRoot, ec);
|
|
std::filesystem::create_directories(assetsDir, ec);
|
|
ASSERT_FALSE(ec) << ec.message();
|
|
|
|
LinkOrCopyFixture(roomPlyPath, runtimeRoomPath);
|
|
|
|
ResourceManager& manager = ResourceManager::Get();
|
|
manager.Initialize();
|
|
manager.SetResourceRoot(m_projectRoot.string().c_str());
|
|
|
|
AssetDatabase database;
|
|
database.Initialize(m_projectRoot.string().c_str());
|
|
|
|
AssetDatabase::ResolvedAsset roomResolve;
|
|
ASSERT_TRUE(database.EnsureArtifact("Assets/room.ply", ResourceType::GaussianSplat, roomResolve));
|
|
ASSERT_TRUE(roomResolve.artifactReady);
|
|
ASSERT_FALSE(roomResolve.artifactMainPath.Empty());
|
|
|
|
m_gaussianSplat = manager.Load<GaussianSplat>(roomResolve.artifactMainPath);
|
|
ASSERT_TRUE(m_gaussianSplat.IsValid());
|
|
ASSERT_NE(m_gaussianSplat.Get(), nullptr);
|
|
ASSERT_TRUE(m_gaussianSplat->IsValid());
|
|
ASSERT_GT(m_gaussianSplat->GetSplatCount(), 0u);
|
|
ASSERT_EQ(m_gaussianSplat->GetSHOrder(), 3u);
|
|
|
|
std::unique_ptr<GaussianSplat> subsetGaussianSplat(
|
|
CreateGaussianSplatSubset(
|
|
*m_gaussianSplat.Get(),
|
|
GetUIntFromEnvironment("XCENGINE_GAUSSIAN_SPLAT_SUBSET_COUNT", kBaselineSubsetSplatCount),
|
|
kSubsetGaussianSplatAssetPath));
|
|
ASSERT_NE(subsetGaussianSplat, nullptr);
|
|
ASSERT_TRUE(subsetGaussianSplat->IsValid());
|
|
ASSERT_GT(subsetGaussianSplat->GetSplatCount(), 0u);
|
|
ASSERT_EQ(subsetGaussianSplat->GetSHOrder(), 3u);
|
|
|
|
const std::filesystem::path subsetArtifactPath = assetsDir / "room_subset.xcgsplat";
|
|
XCEngine::Containers::String subsetWriteError;
|
|
ASSERT_TRUE(WriteGaussianSplatArtifactFile(
|
|
subsetArtifactPath.string().c_str(),
|
|
*subsetGaussianSplat,
|
|
&subsetWriteError)) << subsetWriteError.CStr();
|
|
|
|
m_subsetGaussianSplat = manager.Load<GaussianSplat>(
|
|
XCEngine::Containers::String(subsetArtifactPath.string().c_str()));
|
|
ASSERT_TRUE(m_subsetGaussianSplat.IsValid());
|
|
ASSERT_NE(m_subsetGaussianSplat.Get(), nullptr);
|
|
ASSERT_TRUE(m_subsetGaussianSplat->IsValid());
|
|
ASSERT_GT(m_subsetGaussianSplat->GetSplatCount(), 0u);
|
|
ASSERT_EQ(m_subsetGaussianSplat->GetSHOrder(), 3u);
|
|
ASSERT_NE(m_subsetGaussianSplat->FindSection(GaussianSplatSectionType::Chunks), nullptr);
|
|
ExpectGaussianSplatRoundTripMatches(*subsetGaussianSplat, *m_subsetGaussianSplat.Get());
|
|
|
|
database.Shutdown();
|
|
}
|
|
|
|
void GaussianSplatSceneTest::BuildScene() {
|
|
ASSERT_NE(m_scene, nullptr);
|
|
ASSERT_TRUE(m_subsetGaussianSplat.IsValid());
|
|
|
|
m_material = new Material();
|
|
IResource::ConstructParams params = {};
|
|
params.name = "GaussianSplatSceneMaterial";
|
|
params.path = "Tests/Rendering/GaussianSplatScene/Room.material";
|
|
params.guid = ResourceGUID::Generate(params.path);
|
|
m_material->Initialize(params);
|
|
m_material->SetShader(ResourceManager::Get().Load<Shader>(GetBuiltinGaussianSplatShaderPath()));
|
|
m_material->SetRenderQueue(MaterialRenderQueue::Transparent);
|
|
m_material->SetFloat(
|
|
"_PointScale",
|
|
GetFloatFromEnvironment("XCENGINE_GAUSSIAN_SPLAT_POINT_SCALE", kGaussianPointScale));
|
|
m_material->SetFloat("_OpacityScale", 1.0f);
|
|
m_material->SetFloat("_DebugViewMode", m_debugView == GaussianSplatDebugView::Alpha ? 1.0f : 0.0f);
|
|
|
|
GameObject* cameraObject = m_scene->CreateGameObject("MainCamera");
|
|
auto* camera = cameraObject->AddComponent<CameraComponent>();
|
|
camera->SetPrimary(true);
|
|
camera->SetFieldOfView(45.0f);
|
|
camera->SetNearClipPlane(0.1f);
|
|
camera->SetFarClipPlane(500.0f);
|
|
camera->SetClearColor(XCEngine::Math::Color(0.02f, 0.025f, 0.035f, 1.0f));
|
|
|
|
const Bounds& bounds = m_subsetGaussianSplat->GetBounds();
|
|
const Vector3 boundsMin = bounds.GetMin();
|
|
const Vector3 boundsMax = bounds.GetMax();
|
|
const Vector3 center(
|
|
(boundsMin.x + boundsMax.x) * 0.5f,
|
|
(boundsMin.y + boundsMax.y) * 0.5f,
|
|
(boundsMin.z + boundsMax.z) * 0.5f);
|
|
const float sizeX = std::max(boundsMax.x - boundsMin.x, 0.001f);
|
|
const float sizeY = std::max(boundsMax.y - boundsMin.y, 0.001f);
|
|
const float sizeZ = std::max(boundsMax.z - boundsMin.z, 0.001f);
|
|
const float maxExtent = std::max(sizeX, std::max(sizeY, sizeZ));
|
|
const float uniformScale =
|
|
GetFloatFromEnvironment("XCENGINE_GAUSSIAN_SPLAT_TARGET_EXTENT", kTargetSceneExtent) / maxExtent;
|
|
|
|
GameObject* root = m_scene->CreateGameObject("GaussianSplatRoot");
|
|
root->GetTransform()->SetLocalPosition(
|
|
GetVector3FromEnvironment("XCENGINE_GAUSSIAN_SPLAT_ROOT_POS", kDefaultRootPosition));
|
|
root->GetTransform()->SetLocalScale(Vector3(uniformScale, uniformScale, uniformScale));
|
|
|
|
GameObject* splatObject = m_scene->CreateGameObject("RoomGaussianSplat");
|
|
splatObject->SetParent(root, false);
|
|
splatObject->GetTransform()->SetLocalPosition(Vector3(-center.x, -center.y, -center.z));
|
|
|
|
auto* splatRenderer = splatObject->AddComponent<GaussianSplatRendererComponent>();
|
|
splatRenderer->SetGaussianSplat(m_subsetGaussianSplat.Get());
|
|
splatRenderer->SetMaterial(m_material);
|
|
splatRenderer->SetCastShadows(false);
|
|
splatRenderer->SetReceiveShadows(false);
|
|
|
|
cameraObject->GetTransform()->SetLocalPosition(
|
|
GetVector3FromEnvironment("XCENGINE_GAUSSIAN_SPLAT_CAMERA_POS", kDefaultCameraPosition));
|
|
cameraObject->GetTransform()->LookAt(
|
|
GetVector3FromEnvironment("XCENGINE_GAUSSIAN_SPLAT_CAMERA_LOOK_AT", kDefaultCameraLookAt));
|
|
}
|
|
|
|
RHIResourceView* GaussianSplatSceneTest::GetCurrentBackBufferView() {
|
|
const int backBufferIndex = GetCurrentBackBufferIndex();
|
|
if (backBufferIndex < 0) {
|
|
return nullptr;
|
|
}
|
|
|
|
if (static_cast<size_t>(backBufferIndex) >= m_backBufferViews.size()) {
|
|
m_backBufferViews.resize(static_cast<size_t>(backBufferIndex) + 1, nullptr);
|
|
}
|
|
|
|
if (m_backBufferViews[backBufferIndex] == nullptr) {
|
|
ResourceViewDesc viewDesc = {};
|
|
viewDesc.format = static_cast<uint32_t>(Format::R8G8B8A8_UNorm);
|
|
viewDesc.dimension = ResourceViewDimension::Texture2D;
|
|
viewDesc.mipLevel = 0;
|
|
m_backBufferViews[backBufferIndex] =
|
|
GetDevice()->CreateRenderTargetView(GetCurrentBackBuffer(), viewDesc);
|
|
}
|
|
|
|
return m_backBufferViews[backBufferIndex];
|
|
}
|
|
|
|
void GaussianSplatSceneTest::RenderFrame() {
|
|
ASSERT_NE(m_scene, nullptr);
|
|
ASSERT_NE(m_sceneRenderer, nullptr);
|
|
|
|
RHICommandList* commandList = GetCommandList();
|
|
ASSERT_NE(commandList, nullptr);
|
|
|
|
commandList->Reset();
|
|
|
|
RenderSurface surface(kFrameWidth, kFrameHeight);
|
|
surface.SetColorAttachment(GetCurrentBackBufferView());
|
|
surface.SetDepthAttachment(m_depthView);
|
|
|
|
RenderContext renderContext = {};
|
|
renderContext.device = GetDevice();
|
|
renderContext.commandList = commandList;
|
|
renderContext.commandQueue = GetCommandQueue();
|
|
renderContext.backendType = GetBackendType();
|
|
|
|
ASSERT_TRUE(m_sceneRenderer->Render(*m_scene, nullptr, renderContext, surface));
|
|
|
|
commandList->Close();
|
|
void* commandLists[] = { commandList };
|
|
GetCommandQueue()->ExecuteCommandLists(1, commandLists);
|
|
}
|
|
|
|
TEST_P(GaussianSplatSceneTest, RenderRoomGaussianSplatScene) {
|
|
RHICommandQueue* commandQueue = GetCommandQueue();
|
|
RHISwapChain* swapChain = GetSwapChain();
|
|
ASSERT_NE(commandQueue, nullptr);
|
|
ASSERT_NE(swapChain, nullptr);
|
|
|
|
constexpr int kTargetFrameCount = 2;
|
|
const GaussianSplatDebugView debugView = GetDebugViewFromEnvironment();
|
|
const char* screenshotFilename = GetScreenshotFilename(GetBackendType(), debugView);
|
|
|
|
for (int frameCount = 0; frameCount <= kTargetFrameCount; ++frameCount) {
|
|
if (frameCount > 0) {
|
|
commandQueue->WaitForPreviousFrame();
|
|
}
|
|
|
|
BeginRender();
|
|
RenderFrame();
|
|
|
|
if (frameCount >= kTargetFrameCount) {
|
|
commandQueue->WaitForIdle();
|
|
ASSERT_TRUE(TakeScreenshot(screenshotFilename));
|
|
|
|
if (debugView != GaussianSplatDebugView::Scene) {
|
|
SUCCEED() << "Debug view screenshot captured for inspection: " << screenshotFilename;
|
|
break;
|
|
}
|
|
|
|
const std::filesystem::path gtPath = ResolveRuntimePath("GT.ppm");
|
|
if (!std::filesystem::exists(gtPath)) {
|
|
GTEST_SKIP() << "GT.ppm missing, screenshot captured for baseline generation: " << screenshotFilename;
|
|
}
|
|
|
|
ASSERT_TRUE(CompareWithGoldenTemplate(screenshotFilename, "GT.ppm", 8.0f));
|
|
break;
|
|
}
|
|
|
|
swapChain->Present(0, 0);
|
|
}
|
|
}
|
|
|
|
} // namespace
|
|
|
|
INSTANTIATE_TEST_SUITE_P(D3D12, GaussianSplatSceneTest, ::testing::Values(RHIType::D3D12));
|
|
#if defined(XCENGINE_SUPPORT_OPENGL)
|
|
INSTANTIATE_TEST_SUITE_P(OpenGL, GaussianSplatSceneTest, ::testing::Values(RHIType::OpenGL));
|
|
#endif
|
|
#if defined(XCENGINE_SUPPORT_VULKAN)
|
|
INSTANTIATE_TEST_SUITE_P(Vulkan, GaussianSplatSceneTest, ::testing::Values(RHIType::Vulkan));
|
|
#endif
|
|
|
|
GTEST_API_ int main(int argc, char** argv) {
|
|
return RunRenderingIntegrationTestMain(argc, argv);
|
|
}
|