#define NOMINMAX #include #include #include "../RenderingIntegrationMain.h" #include "../RenderingIntegrationImageAssert.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "../../../RHI/integration/fixtures/RHIIntegrationFixture.h" #include #include #include #include #include #include #include #include #include #include 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((bits >> 23u) & 0xffu) - 127 + 15; if (exponent <= 0) { if (exponent < -10) { return static_cast(sign); } mantissa = (mantissa | 0x00800000u) >> static_cast(1 - exponent); if ((mantissa & 0x00001000u) != 0u) { mantissa += 0x00002000u; } return static_cast(sign | (mantissa >> 13u)); } if (exponent >= 31) { return static_cast(sign | 0x7c00u); } if ((mantissa & 0x00001000u) != 0u) { mantissa += 0x00002000u; if ((mantissa & 0x00800000u) != 0u) { mantissa = 0u; ++exponent; if (exponent >= 31) { return static_cast(sign | 0x7c00u); } } } return static_cast( sign | (static_cast(exponent) << 10u) | (mantissa >> 13u)); } uint32_t PackHalfRange(float minValue, float maxValue) { return static_cast(FloatToHalfBits(minValue)) | (static_cast(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(::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(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( source.GetSectionData(GaussianSplatSectionType::Chunks)); const auto* loadedChunkSection = static_cast( 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 selectedIndices(subsetSplatCount, 0u); for (uint32_t subsetIndex = 0u; subsetIndex < subsetSplatCount; ++subsetIndex) { const uint64_t scaledIndex = (static_cast(subsetIndex) * static_cast(sourceSplatCount)) / static_cast(subsetSplatCount); selectedIndices[subsetIndex] = static_cast(std::min(scaledIndex, static_cast(sourceSplatCount - 1u))); } std::vector subsetPositions(subsetSplatCount); std::vector subsetOther(subsetSplatCount); std::vector subsetColors(subsetSplatCount); std::vector 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 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::max(), std::numeric_limits::max(), std::numeric_limits::max()); Vector3 maxPosition( -std::numeric_limits::max(), -std::numeric_limits::max(), -std::numeric_limits::max()); Vector3 minScale( std::numeric_limits::max(), std::numeric_limits::max(), std::numeric_limits::max()); Vector3 maxScale( -std::numeric_limits::max(), -std::numeric_limits::max(), -std::numeric_limits::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 sections; XCEngine::Containers::Array 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(elementCount) * elementStride; sections.PushBack(section); const size_t newPayloadSize = payload.Size() + static_cast(section.dataSize); payload.Resize(newPayloadSize); std::memcpy(payload.Data() + payloadOffset, sourceData, static_cast(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 m_scene; std::unique_ptr m_sceneRenderer; std::vector m_backBufferViews; RHITexture* m_depthTexture = nullptr; RHIResourceView* m_depthView = nullptr; ResourceHandle m_gaussianSplat; ResourceHandle 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(); m_scene = std::make_unique("GaussianSplatScene"); BuildScene(); TextureDesc depthDesc = {}; depthDesc.width = kFrameWidth; depthDesc.height = kFrameHeight; depthDesc.depth = 1; depthDesc.mipLevels = 1; depthDesc.arraySize = 1; depthDesc.format = static_cast(Format::D24_UNorm_S8_UInt); depthDesc.textureType = static_cast(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(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(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 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( 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(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(); 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(); 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(backBufferIndex) >= m_backBufferViews.size()) { m_backBufferViews.resize(static_cast(backBufferIndex) + 1, nullptr); } if (m_backBufferViews[backBufferIndex] == nullptr) { ResourceViewDesc viewDesc = {}; viewDesc.format = static_cast(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); }