#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 "../../../RHI/integration/fixtures/RHIIntegrationFixture.h" #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 uint32_t kFrameWidth = 1280; constexpr uint32_t kFrameHeight = 720; constexpr uint32_t kBaselineSubsetSplatCount = 65536u; 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(); } const char* GetScreenshotFilename(RHIType backendType) { switch (backendType) { case RHIType::D3D12: return kD3D12Screenshot; case RHIType::Vulkan: return kVulkanScreenshot; case RHIType::OpenGL: default: return 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); 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 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()); for (uint32_t subsetIndex = startIndex; subsetIndex < endIndex; ++subsetIndex) { const Vector3& position = subsetPositions[subsetIndex].position; 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); } GaussianSplatChunkRecord chunk = {}; chunk.posX = Vector2(minPosition.x, maxPosition.x); chunk.posY = Vector2(minPosition.y, maxPosition.y); chunk.posZ = Vector2(minPosition.z, maxPosition.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 = source.GetBounds(); 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; GaussianSplat* m_subsetGaussianSplat = nullptr; Material* m_material = nullptr; }; void GaussianSplatSceneTest::SetUp() { RHIIntegrationFixture::SetUp(); 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; delete m_subsetGaussianSplat; m_subsetGaussianSplat = nullptr; 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()); m_gaussianSplat = manager.Load("Assets/room.ply"); 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); m_subsetGaussianSplat = CreateGaussianSplatSubset( *m_gaussianSplat.Get(), kBaselineSubsetSplatCount, "Tests/Rendering/GaussianSplatScene/RoomSubset.xcgsplat"); ASSERT_NE(m_subsetGaussianSplat, nullptr); ASSERT_TRUE(m_subsetGaussianSplat->IsValid()); ASSERT_GT(m_subsetGaussianSplat->GetSplatCount(), 0u); ASSERT_EQ(m_subsetGaussianSplat->GetSHOrder(), 3u); } void GaussianSplatSceneTest::BuildScene() { ASSERT_NE(m_scene, nullptr); ASSERT_NE(m_subsetGaussianSplat, nullptr); 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", 1.0f); m_material->SetFloat("_OpacityScale", 1.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 = 3.0f / maxExtent; GameObject* root = m_scene->CreateGameObject("GaussianSplatRoot"); root->GetTransform()->SetLocalPosition(Vector3(0.0f, -0.35f, 3.2f)); 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); splatRenderer->SetMaterial(m_material); splatRenderer->SetCastShadows(false); splatRenderer->SetReceiveShadows(false); } 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 = 1; const char* screenshotFilename = GetScreenshotFilename(GetBackendType()); for (int frameCount = 0; frameCount <= kTargetFrameCount; ++frameCount) { if (frameCount > 0) { commandQueue->WaitForPreviousFrame(); } BeginRender(); RenderFrame(); if (frameCount >= kTargetFrameCount) { commandQueue->WaitForIdle(); ASSERT_TRUE(TakeScreenshot(screenshotFilename)); 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); }