Add gaussian splat asset caching groundwork

This commit is contained in:
2026-04-12 11:15:59 +08:00
parent b7ce8618d2
commit 7ee28a7969
16 changed files with 1652 additions and 88 deletions

View File

@@ -28,10 +28,13 @@
#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>
@@ -48,12 +51,23 @@ 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 = 65536u;
constexpr uint32_t kBaselineSubsetSplatCount = 262144u;
constexpr const char* kSubsetGaussianSplatAssetPath = "Assets/room_subset.xcgsplat";
constexpr float kTargetSceneExtent = 4.0f;
constexpr float kGaussianPointScale = 3.00f;
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;
@@ -128,15 +142,178 @@ void LinkOrCopyFixture(const std::filesystem::path& sourcePath, const std::files
ASSERT_FALSE(ec) << ec.message();
}
const char* GetScreenshotFilename(RHIType backendType) {
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 kD3D12Screenshot;
return debugView == GaussianSplatDebugView::Alpha
? kD3D12AlphaDebugScreenshot
: kD3D12Screenshot;
case RHIType::Vulkan:
return kVulkanScreenshot;
return debugView == GaussianSplatDebugView::Alpha
? kVulkanAlphaDebugScreenshot
: kVulkanScreenshot;
case RHIType::OpenGL:
default:
return kOpenGLScreenshot;
return debugView == GaussianSplatDebugView::Alpha
? kOpenGLAlphaDebugScreenshot
: kOpenGLScreenshot;
}
}
@@ -180,6 +357,8 @@ GaussianSplat* CreateGaussianSplatSubset(
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];
@@ -188,6 +367,14 @@ GaussianSplat* CreateGaussianSplatSubset(
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 =
@@ -296,7 +483,7 @@ GaussianSplat* CreateGaussianSplatSubset(
GaussianSplatMetadata metadata = source.GetMetadata();
metadata.splatCount = subsetSplatCount;
metadata.bounds = source.GetBounds();
metadata.bounds = subsetBounds;
metadata.chunkCount = subsetChunkCount;
metadata.cameraCount = 0u;
metadata.chunkFormat = GaussianSplatSectionFormat::ChunkFloat32;
@@ -336,10 +523,12 @@ private:
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>();
@@ -452,7 +641,7 @@ void GaussianSplatSceneTest::PrepareRuntimeProject() {
std::unique_ptr<GaussianSplat> subsetGaussianSplat(
CreateGaussianSplatSubset(
*m_gaussianSplat.Get(),
kBaselineSubsetSplatCount,
GetUIntFromEnvironment("XCENGINE_GAUSSIAN_SPLAT_SUBSET_COUNT", kBaselineSubsetSplatCount),
kSubsetGaussianSplatAssetPath));
ASSERT_NE(subsetGaussianSplat, nullptr);
ASSERT_TRUE(subsetGaussianSplat->IsValid());
@@ -474,6 +663,7 @@ void GaussianSplatSceneTest::PrepareRuntimeProject() {
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();
}
@@ -490,8 +680,11 @@ void GaussianSplatSceneTest::BuildScene() {
m_material->Initialize(params);
m_material->SetShader(ResourceManager::Get().Load<Shader>(GetBuiltinGaussianSplatShaderPath()));
m_material->SetRenderQueue(MaterialRenderQueue::Transparent);
m_material->SetFloat("_PointScale", kGaussianPointScale);
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>();
@@ -512,10 +705,12 @@ void GaussianSplatSceneTest::BuildScene() {
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 = kTargetSceneExtent / maxExtent;
const float uniformScale =
GetFloatFromEnvironment("XCENGINE_GAUSSIAN_SPLAT_TARGET_EXTENT", kTargetSceneExtent) / maxExtent;
GameObject* root = m_scene->CreateGameObject("GaussianSplatRoot");
root->GetTransform()->SetLocalPosition(Vector3::Zero());
root->GetTransform()->SetLocalPosition(
GetVector3FromEnvironment("XCENGINE_GAUSSIAN_SPLAT_ROOT_POS", kDefaultRootPosition));
root->GetTransform()->SetLocalScale(Vector3(uniformScale, uniformScale, uniformScale));
GameObject* splatObject = m_scene->CreateGameObject("RoomGaussianSplat");
@@ -527,7 +722,11 @@ void GaussianSplatSceneTest::BuildScene() {
splatRenderer->SetMaterial(m_material);
splatRenderer->SetCastShadows(false);
splatRenderer->SetReceiveShadows(false);
cameraObject->GetTransform()->SetLocalPosition(Vector3(0.0f, 1.0f, 1.0f));
cameraObject->GetTransform()->SetLocalPosition(
GetVector3FromEnvironment("XCENGINE_GAUSSIAN_SPLAT_CAMERA_POS", kDefaultCameraPosition));
cameraObject->GetTransform()->LookAt(
GetVector3FromEnvironment("XCENGINE_GAUSSIAN_SPLAT_CAMERA_LOOK_AT", kDefaultCameraLookAt));
}
RHIResourceView* GaussianSplatSceneTest::GetCurrentBackBufferView() {
@@ -585,7 +784,8 @@ TEST_P(GaussianSplatSceneTest, RenderRoomGaussianSplatScene) {
ASSERT_NE(swapChain, nullptr);
constexpr int kTargetFrameCount = 2;
const char* screenshotFilename = GetScreenshotFilename(GetBackendType());
const GaussianSplatDebugView debugView = GetDebugViewFromEnvironment();
const char* screenshotFilename = GetScreenshotFilename(GetBackendType(), debugView);
for (int frameCount = 0; frameCount <= kTargetFrameCount; ++frameCount) {
if (frameCount > 0) {
@@ -599,6 +799,11 @@ TEST_P(GaussianSplatSceneTest, RenderRoomGaussianSplatScene) {
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;