From b187c8970bacc993771ecf4cb66151e913649695 Mon Sep 17 00:00:00 2001 From: ssdfasd <2156608475@qq.com> Date: Fri, 10 Apr 2026 21:49:53 +0800 Subject: [PATCH] Formalize GaussianSplat scene extraction --- engine/CMakeLists.txt | 3 + .../GaussianSplatRendererComponent.h | 77 ++++ .../Extraction/RenderSceneExtractor.h | 1 + .../Rendering/Extraction/RenderSceneUtility.h | 10 + .../Rendering/FrameData/RenderSceneData.h | 2 + .../FrameData/VisibleGaussianSplatItem.h | 30 ++ .../Components/ComponentFactoryRegistry.cpp | 2 + .../GaussianSplatRendererComponent.cpp | 420 ++++++++++++++++++ .../Extraction/RenderSceneExtractor.cpp | 20 +- .../Extraction/RenderSceneUtility.cpp | 59 +++ tests/Components/CMakeLists.txt | 1 + .../test_component_factory_registry.cpp | 3 + ...test_gaussian_splat_renderer_component.cpp | 182 ++++++++ .../unit/test_render_scene_extractor.cpp | 182 ++++++++ 14 files changed, 991 insertions(+), 1 deletion(-) create mode 100644 engine/include/XCEngine/Components/GaussianSplatRendererComponent.h create mode 100644 engine/include/XCEngine/Rendering/FrameData/VisibleGaussianSplatItem.h create mode 100644 engine/src/Components/GaussianSplatRendererComponent.cpp create mode 100644 tests/Components/test_gaussian_splat_renderer_component.cpp diff --git a/engine/CMakeLists.txt b/engine/CMakeLists.txt index 7f63d3ed..b7510dcd 100644 --- a/engine/CMakeLists.txt +++ b/engine/CMakeLists.txt @@ -449,6 +449,7 @@ add_library(XCEngine STATIC ${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Components/GameObject.h ${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Components/CameraComponent.h ${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Components/LightComponent.h + ${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Components/GaussianSplatRendererComponent.h ${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Components/MeshFilterComponent.h ${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Components/MeshRendererComponent.h ${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Components/VolumeRendererComponent.h @@ -460,6 +461,7 @@ add_library(XCEngine STATIC ${CMAKE_CURRENT_SOURCE_DIR}/src/Components/GameObject.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Components/CameraComponent.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Components/LightComponent.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/Components/GaussianSplatRendererComponent.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Components/MeshFilterComponent.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Components/MeshRendererComponent.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Components/VolumeRendererComponent.cpp @@ -477,6 +479,7 @@ add_library(XCEngine STATIC ${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Rendering/Execution/SceneRenderer.h ${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Rendering/FrameData/RenderCameraData.h ${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Rendering/FrameData/RenderSceneData.h + ${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Rendering/FrameData/VisibleGaussianSplatItem.h ${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Rendering/FrameData/VisibleRenderItem.h ${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Rendering/FrameData/VisibleVolumeItem.h ${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Rendering/Planning/CameraRenderRequest.h diff --git a/engine/include/XCEngine/Components/GaussianSplatRendererComponent.h b/engine/include/XCEngine/Components/GaussianSplatRendererComponent.h new file mode 100644 index 00000000..0f0b8978 --- /dev/null +++ b/engine/include/XCEngine/Components/GaussianSplatRendererComponent.h @@ -0,0 +1,77 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include +#include + +namespace XCEngine { +namespace Components { + +class GaussianSplatRendererComponent : public Component { +public: + std::string GetName() const override { return "GaussianSplatRenderer"; } + + Resources::GaussianSplat* GetGaussianSplat() const; + const Resources::ResourceHandle& GetGaussianSplatHandle() const; + const std::string& GetGaussianSplatPath() const { return m_gaussianSplatPath; } + const Resources::AssetRef& GetGaussianSplatAssetRef() const { return m_gaussianSplatRef; } + + void SetGaussianSplatPath(const std::string& gaussianSplatPath); + void SetGaussianSplat(const Resources::ResourceHandle& gaussianSplat); + void SetGaussianSplat(Resources::GaussianSplat* gaussianSplat); + void ClearGaussianSplat(); + + Resources::Material* GetMaterial() const; + const Resources::ResourceHandle& GetMaterialHandle() const; + const std::string& GetMaterialPath() const { return m_materialPath; } + const Resources::AssetRef& GetMaterialAssetRef() const { return m_materialRef; } + + void SetMaterialPath(const std::string& materialPath); + void SetMaterial(const Resources::ResourceHandle& material); + void SetMaterial(Resources::Material* material); + void ClearMaterial(); + + bool GetCastShadows() const { return m_castShadows; } + void SetCastShadows(bool value) { m_castShadows = value; } + + bool GetReceiveShadows() const { return m_receiveShadows; } + void SetReceiveShadows(bool value) { m_receiveShadows = value; } + + void Serialize(std::ostream& os) const override; + void Deserialize(std::istream& is) override; + +private: + struct PendingGaussianSplatLoadState; + struct PendingMaterialLoadState; + + void BeginAsyncGaussianSplatLoad(const std::string& gaussianSplatPath); + void EnsureDeferredAsyncGaussianSplatLoadStarted(); + void ResolvePendingGaussianSplat(); + + void BeginAsyncMaterialLoad(const std::string& materialPath); + void EnsureDeferredAsyncMaterialLoadStarted(); + void ResolvePendingMaterial(); + + Resources::ResourceHandle m_gaussianSplat; + std::string m_gaussianSplatPath; + Resources::AssetRef m_gaussianSplatRef; + std::shared_ptr m_pendingGaussianSplatLoad; + bool m_asyncGaussianSplatLoadRequested = false; + + Resources::ResourceHandle m_material; + std::string m_materialPath; + Resources::AssetRef m_materialRef; + std::shared_ptr m_pendingMaterialLoad; + bool m_asyncMaterialLoadRequested = false; + + bool m_castShadows = true; + bool m_receiveShadows = true; +}; + +} // namespace Components +} // namespace XCEngine diff --git a/engine/include/XCEngine/Rendering/Extraction/RenderSceneExtractor.h b/engine/include/XCEngine/Rendering/Extraction/RenderSceneExtractor.h index be4df180..8567a988 100644 --- a/engine/include/XCEngine/Rendering/Extraction/RenderSceneExtractor.h +++ b/engine/include/XCEngine/Rendering/Extraction/RenderSceneExtractor.h @@ -42,6 +42,7 @@ private: const Math::Vector3& cameraPosition, uint32_t cullingMask, std::vector& outVisibleItems, + std::vector& outVisibleGaussianSplats, std::vector& outVisibleVolumes) const; }; diff --git a/engine/include/XCEngine/Rendering/Extraction/RenderSceneUtility.h b/engine/include/XCEngine/Rendering/Extraction/RenderSceneUtility.h index 7ce19d72..9cf3da1b 100644 --- a/engine/include/XCEngine/Rendering/Extraction/RenderSceneUtility.h +++ b/engine/include/XCEngine/Rendering/Extraction/RenderSceneUtility.h @@ -2,6 +2,7 @@ #include #include +#include #include #include @@ -33,10 +34,19 @@ void AppendVisibleVolumesForGameObject( const Math::Vector3& cameraPosition, std::vector& outVisibleVolumes); +void AppendVisibleGaussianSplatsForGameObject( + Components::GameObject& gameObject, + const Math::Vector3& cameraPosition, + std::vector& outVisibleGaussianSplats); + bool CompareVisibleRenderItemsStable( const VisibleRenderItem& lhs, const VisibleRenderItem& rhs); +bool CompareVisibleGaussianSplatsStable( + const VisibleGaussianSplatItem& lhs, + const VisibleGaussianSplatItem& rhs); + bool CompareVisibleVolumesStable( const VisibleVolumeItem& lhs, const VisibleVolumeItem& rhs); diff --git a/engine/include/XCEngine/Rendering/FrameData/RenderSceneData.h b/engine/include/XCEngine/Rendering/FrameData/RenderSceneData.h index f2da04ea..d05e8a13 100644 --- a/engine/include/XCEngine/Rendering/FrameData/RenderSceneData.h +++ b/engine/include/XCEngine/Rendering/FrameData/RenderSceneData.h @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include @@ -88,6 +89,7 @@ struct RenderSceneData { RenderLightingData lighting; Resources::ShaderKeywordSet globalShaderKeywords; std::vector visibleItems; + std::vector visibleGaussianSplats; std::vector visibleVolumes; bool HasCamera() const { diff --git a/engine/include/XCEngine/Rendering/FrameData/VisibleGaussianSplatItem.h b/engine/include/XCEngine/Rendering/FrameData/VisibleGaussianSplatItem.h new file mode 100644 index 00000000..7f36e62d --- /dev/null +++ b/engine/include/XCEngine/Rendering/FrameData/VisibleGaussianSplatItem.h @@ -0,0 +1,30 @@ +#pragma once + +#include +#include + +namespace XCEngine { +namespace Components { +class GameObject; +class GaussianSplatRendererComponent; +} // namespace Components + +namespace Resources { +class GaussianSplat; +class Material; +} // namespace Resources + +namespace Rendering { + +struct VisibleGaussianSplatItem { + Components::GameObject* gameObject = nullptr; + Components::GaussianSplatRendererComponent* gaussianSplatRenderer = nullptr; + Resources::GaussianSplat* gaussianSplat = nullptr; + const Resources::Material* material = nullptr; + Core::int32 renderQueue = 0; + float cameraDistanceSq = 0.0f; + Math::Matrix4x4 localToWorld = Math::Matrix4x4::Identity(); +}; + +} // namespace Rendering +} // namespace XCEngine diff --git a/engine/src/Components/ComponentFactoryRegistry.cpp b/engine/src/Components/ComponentFactoryRegistry.cpp index a8347727..71196b24 100644 --- a/engine/src/Components/ComponentFactoryRegistry.cpp +++ b/engine/src/Components/ComponentFactoryRegistry.cpp @@ -3,6 +3,7 @@ #include "Components/AudioListenerComponent.h" #include "Components/AudioSourceComponent.h" #include "Components/CameraComponent.h" +#include "Components/GaussianSplatRendererComponent.h" #include "Components/GameObject.h" #include "Components/LightComponent.h" #include "Components/MeshFilterComponent.h" @@ -34,6 +35,7 @@ ComponentFactoryRegistry::ComponentFactoryRegistry() { RegisterFactory("AudioListener", &CreateBuiltInComponent); RegisterFactory("MeshFilter", &CreateBuiltInComponent); RegisterFactory("MeshRenderer", &CreateBuiltInComponent); + RegisterFactory("GaussianSplatRenderer", &CreateBuiltInComponent); RegisterFactory("VolumeRenderer", &CreateBuiltInComponent); RegisterFactory("ScriptComponent", &CreateBuiltInComponent); } diff --git a/engine/src/Components/GaussianSplatRendererComponent.cpp b/engine/src/Components/GaussianSplatRendererComponent.cpp new file mode 100644 index 00000000..1f3bc0d2 --- /dev/null +++ b/engine/src/Components/GaussianSplatRendererComponent.cpp @@ -0,0 +1,420 @@ +#include "Components/GaussianSplatRendererComponent.h" + +#include "Core/Asset/ResourceManager.h" + +namespace XCEngine { +namespace Components { + +namespace { + +std::string ToStdString(const Containers::String& value) { + return std::string(value.CStr()); +} + +bool HasVirtualPathScheme(const std::string& path) { + return path.find("://") != std::string::npos; +} + +std::string EncodeAssetRef(const Resources::AssetRef& assetRef) { + if (!assetRef.IsValid()) { + return std::string(); + } + + return ToStdString(assetRef.assetGuid.ToString()) + "," + + std::to_string(assetRef.localID) + "," + + std::to_string(static_cast(assetRef.resourceType)); +} + +bool TryDecodeAssetRef(const std::string& value, Resources::AssetRef& outRef) { + const size_t firstComma = value.find(','); + const size_t secondComma = firstComma == std::string::npos ? std::string::npos : value.find(',', firstComma + 1); + if (firstComma == std::string::npos || secondComma == std::string::npos) { + return false; + } + + const Containers::String guidText(value.substr(0, firstComma).c_str()); + outRef.assetGuid = Resources::AssetGUID::ParseOrDefault(guidText); + outRef.localID = static_cast(std::stoull(value.substr(firstComma + 1, secondComma - firstComma - 1))); + outRef.resourceType = static_cast(std::stoi(value.substr(secondComma + 1))); + return outRef.IsValid(); +} + +std::string GaussianSplatPathFromHandle(const Resources::ResourceHandle& gaussianSplat) { + return gaussianSplat.Get() != nullptr ? ToStdString(gaussianSplat->GetPath()) : std::string(); +} + +std::string MaterialPathFromHandle(const Resources::ResourceHandle& material) { + return material.Get() != nullptr ? ToStdString(material->GetPath()) : std::string(); +} + +} // namespace + +struct GaussianSplatRendererComponent::PendingGaussianSplatLoadState { + Resources::LoadResult result; + bool completed = false; +}; + +struct GaussianSplatRendererComponent::PendingMaterialLoadState { + Resources::LoadResult result; + bool completed = false; +}; + +Resources::GaussianSplat* GaussianSplatRendererComponent::GetGaussianSplat() const { + const_cast(this)->EnsureDeferredAsyncGaussianSplatLoadStarted(); + const_cast(this)->ResolvePendingGaussianSplat(); + return m_gaussianSplat.Get(); +} + +const Resources::ResourceHandle& GaussianSplatRendererComponent::GetGaussianSplatHandle() const { + const_cast(this)->EnsureDeferredAsyncGaussianSplatLoadStarted(); + const_cast(this)->ResolvePendingGaussianSplat(); + return m_gaussianSplat; +} + +void GaussianSplatRendererComponent::SetGaussianSplatPath(const std::string& gaussianSplatPath) { + m_pendingGaussianSplatLoad.reset(); + m_asyncGaussianSplatLoadRequested = false; + m_gaussianSplatPath = gaussianSplatPath; + if (m_gaussianSplatPath.empty()) { + m_gaussianSplat.Reset(); + m_gaussianSplatRef.Reset(); + return; + } + + m_gaussianSplat = Resources::ResourceManager::Get().Load(m_gaussianSplatPath.c_str()); + if (!Resources::ResourceManager::Get().TryGetAssetRef( + m_gaussianSplatPath.c_str(), + Resources::ResourceType::GaussianSplat, + m_gaussianSplatRef)) { + m_gaussianSplatRef.Reset(); + } +} + +void GaussianSplatRendererComponent::SetGaussianSplat( + const Resources::ResourceHandle& gaussianSplat) { + m_pendingGaussianSplatLoad.reset(); + m_asyncGaussianSplatLoadRequested = false; + m_gaussianSplat = gaussianSplat; + m_gaussianSplatPath = GaussianSplatPathFromHandle(gaussianSplat); + if (m_gaussianSplatPath.empty() || + !Resources::ResourceManager::Get().TryGetAssetRef( + m_gaussianSplatPath.c_str(), + Resources::ResourceType::GaussianSplat, + m_gaussianSplatRef)) { + m_gaussianSplatRef.Reset(); + } +} + +void GaussianSplatRendererComponent::SetGaussianSplat(Resources::GaussianSplat* gaussianSplat) { + SetGaussianSplat(Resources::ResourceHandle(gaussianSplat)); +} + +void GaussianSplatRendererComponent::ClearGaussianSplat() { + m_pendingGaussianSplatLoad.reset(); + m_asyncGaussianSplatLoadRequested = false; + m_gaussianSplat.Reset(); + m_gaussianSplatPath.clear(); + m_gaussianSplatRef.Reset(); +} + +Resources::Material* GaussianSplatRendererComponent::GetMaterial() const { + const_cast(this)->EnsureDeferredAsyncMaterialLoadStarted(); + const_cast(this)->ResolvePendingMaterial(); + return m_material.Get(); +} + +const Resources::ResourceHandle& GaussianSplatRendererComponent::GetMaterialHandle() const { + const_cast(this)->EnsureDeferredAsyncMaterialLoadStarted(); + const_cast(this)->ResolvePendingMaterial(); + return m_material; +} + +void GaussianSplatRendererComponent::SetMaterialPath(const std::string& materialPath) { + m_pendingMaterialLoad.reset(); + m_asyncMaterialLoadRequested = false; + m_materialPath = materialPath; + if (m_materialPath.empty()) { + m_material.Reset(); + m_materialRef.Reset(); + return; + } + + m_material = Resources::ResourceManager::Get().Load(m_materialPath.c_str()); + if (!Resources::ResourceManager::Get().TryGetAssetRef( + m_materialPath.c_str(), + Resources::ResourceType::Material, + m_materialRef)) { + m_materialRef.Reset(); + } +} + +void GaussianSplatRendererComponent::SetMaterial( + const Resources::ResourceHandle& material) { + m_pendingMaterialLoad.reset(); + m_asyncMaterialLoadRequested = false; + m_material = material; + m_materialPath = MaterialPathFromHandle(material); + if (m_materialPath.empty() || + !Resources::ResourceManager::Get().TryGetAssetRef( + m_materialPath.c_str(), + Resources::ResourceType::Material, + m_materialRef)) { + m_materialRef.Reset(); + } +} + +void GaussianSplatRendererComponent::SetMaterial(Resources::Material* material) { + SetMaterial(Resources::ResourceHandle(material)); +} + +void GaussianSplatRendererComponent::ClearMaterial() { + m_pendingMaterialLoad.reset(); + m_asyncMaterialLoadRequested = false; + m_material.Reset(); + m_materialPath.clear(); + m_materialRef.Reset(); +} + +void GaussianSplatRendererComponent::Serialize(std::ostream& os) const { + Resources::AssetRef gaussianSplatRef = m_gaussianSplatRef; + if (!gaussianSplatRef.IsValid() && + !m_gaussianSplatPath.empty() && + !HasVirtualPathScheme(m_gaussianSplatPath) && + Resources::ResourceManager::Get().TryGetAssetRef( + m_gaussianSplatPath.c_str(), + Resources::ResourceType::GaussianSplat, + gaussianSplatRef)) { + } + + Resources::AssetRef materialRef = m_materialRef; + if (!materialRef.IsValid() && + !m_materialPath.empty() && + !HasVirtualPathScheme(m_materialPath) && + Resources::ResourceManager::Get().TryGetAssetRef( + m_materialPath.c_str(), + Resources::ResourceType::Material, + materialRef)) { + } + + os << "gaussianSplatRef=" << EncodeAssetRef(gaussianSplatRef) << ";"; + if (!gaussianSplatRef.IsValid() && !m_gaussianSplatPath.empty() && HasVirtualPathScheme(m_gaussianSplatPath)) { + os << "gaussianSplatPath=" << m_gaussianSplatPath << ";"; + } + + os << "materialRef=" << EncodeAssetRef(materialRef) << ";"; + if (!materialRef.IsValid() && !m_materialPath.empty() && HasVirtualPathScheme(m_materialPath)) { + os << "materialPath=" << m_materialPath << ";"; + } + + os << "castShadows=" << (m_castShadows ? 1 : 0) << ";"; + os << "receiveShadows=" << (m_receiveShadows ? 1 : 0) << ";"; +} + +void GaussianSplatRendererComponent::Deserialize(std::istream& is) { + ClearGaussianSplat(); + ClearMaterial(); + m_castShadows = true; + m_receiveShadows = true; + + std::string token; + std::string pendingGaussianSplatPath; + Resources::AssetRef pendingGaussianSplatRef; + std::string pendingMaterialPath; + Resources::AssetRef pendingMaterialRef; + while (std::getline(is, token, ';')) { + if (token.empty()) { + continue; + } + + const size_t eqPos = token.find('='); + if (eqPos == std::string::npos) { + continue; + } + + const std::string key = token.substr(0, eqPos); + const std::string value = token.substr(eqPos + 1); + + if (key == "gaussianSplatPath") { + pendingGaussianSplatPath = value; + } else if (key == "gaussianSplatRef") { + TryDecodeAssetRef(value, pendingGaussianSplatRef); + } else if (key == "materialPath") { + pendingMaterialPath = value; + } else if (key == "materialRef") { + TryDecodeAssetRef(value, pendingMaterialRef); + } else if (key == "castShadows") { + m_castShadows = (std::stoi(value) != 0); + } else if (key == "receiveShadows") { + m_receiveShadows = (std::stoi(value) != 0); + } + } + + if (pendingGaussianSplatRef.IsValid()) { + bool restoredOrQueued = false; + m_gaussianSplatRef = pendingGaussianSplatRef; + if (Resources::ResourceManager::Get().IsDeferredSceneLoadEnabled()) { + Containers::String resolvedPath; + if (Resources::ResourceManager::Get().TryResolveAssetPath(pendingGaussianSplatRef, resolvedPath)) { + m_gaussianSplatPath = ToStdString(resolvedPath); + restoredOrQueued = true; + } + } + + if (!restoredOrQueued) { + m_gaussianSplat = Resources::ResourceManager::Get().Load(pendingGaussianSplatRef); + m_gaussianSplatPath = m_gaussianSplat.Get() != nullptr + ? GaussianSplatPathFromHandle(m_gaussianSplat) + : pendingGaussianSplatPath; + } + } else if (!pendingGaussianSplatPath.empty() && HasVirtualPathScheme(pendingGaussianSplatPath)) { + if (Resources::ResourceManager::Get().IsDeferredSceneLoadEnabled()) { + m_gaussianSplatPath = pendingGaussianSplatPath; + } else { + SetGaussianSplatPath(pendingGaussianSplatPath); + } + } + + if (pendingMaterialRef.IsValid()) { + bool restoredOrQueued = false; + m_materialRef = pendingMaterialRef; + if (Resources::ResourceManager::Get().IsDeferredSceneLoadEnabled()) { + Containers::String resolvedPath; + if (Resources::ResourceManager::Get().TryResolveAssetPath(pendingMaterialRef, resolvedPath)) { + m_materialPath = ToStdString(resolvedPath); + restoredOrQueued = true; + } + } + + if (!restoredOrQueued) { + m_material = Resources::ResourceManager::Get().Load(pendingMaterialRef); + m_materialPath = m_material.Get() != nullptr + ? MaterialPathFromHandle(m_material) + : pendingMaterialPath; + } + } else if (!pendingMaterialPath.empty() && HasVirtualPathScheme(pendingMaterialPath)) { + if (Resources::ResourceManager::Get().IsDeferredSceneLoadEnabled()) { + m_materialPath = pendingMaterialPath; + } else { + SetMaterialPath(pendingMaterialPath); + } + } +} + +void GaussianSplatRendererComponent::BeginAsyncGaussianSplatLoad(const std::string& gaussianSplatPath) { + if (gaussianSplatPath.empty()) { + m_pendingGaussianSplatLoad.reset(); + m_asyncGaussianSplatLoadRequested = false; + m_gaussianSplat.Reset(); + return; + } + + m_asyncGaussianSplatLoadRequested = true; + m_gaussianSplat.Reset(); + m_pendingGaussianSplatLoad = std::make_shared(); + std::weak_ptr weakState = m_pendingGaussianSplatLoad; + Resources::ResourceManager::Get().LoadAsync( + gaussianSplatPath.c_str(), + Resources::ResourceType::GaussianSplat, + [weakState](Resources::LoadResult result) { + if (std::shared_ptr state = weakState.lock()) { + state->result = std::move(result); + state->completed = true; + } + }); +} + +void GaussianSplatRendererComponent::EnsureDeferredAsyncGaussianSplatLoadStarted() { + if (m_asyncGaussianSplatLoadRequested || m_gaussianSplat.Get() != nullptr || m_gaussianSplatPath.empty()) { + return; + } + + BeginAsyncGaussianSplatLoad(m_gaussianSplatPath); +} + +void GaussianSplatRendererComponent::ResolvePendingGaussianSplat() { + if (!m_pendingGaussianSplatLoad || !m_pendingGaussianSplatLoad->completed) { + return; + } + + std::shared_ptr completedLoad = std::move(m_pendingGaussianSplatLoad); + m_pendingGaussianSplatLoad.reset(); + + if (!completedLoad->result || completedLoad->result.resource == nullptr) { + return; + } + + m_gaussianSplat = Resources::ResourceHandle( + static_cast(completedLoad->result.resource)); + if (m_gaussianSplat.Get() == nullptr) { + return; + } + + m_gaussianSplatPath = GaussianSplatPathFromHandle(m_gaussianSplat); + if (!Resources::ResourceManager::Get().TryGetAssetRef( + m_gaussianSplatPath.c_str(), + Resources::ResourceType::GaussianSplat, + m_gaussianSplatRef)) { + m_gaussianSplatRef.Reset(); + } +} + +void GaussianSplatRendererComponent::BeginAsyncMaterialLoad(const std::string& materialPath) { + if (materialPath.empty()) { + m_pendingMaterialLoad.reset(); + m_asyncMaterialLoadRequested = false; + m_material.Reset(); + return; + } + + m_asyncMaterialLoadRequested = true; + m_material.Reset(); + m_pendingMaterialLoad = std::make_shared(); + std::weak_ptr weakState = m_pendingMaterialLoad; + Resources::ResourceManager::Get().LoadAsync( + materialPath.c_str(), + Resources::ResourceType::Material, + [weakState](Resources::LoadResult result) { + if (std::shared_ptr state = weakState.lock()) { + state->result = std::move(result); + state->completed = true; + } + }); +} + +void GaussianSplatRendererComponent::EnsureDeferredAsyncMaterialLoadStarted() { + if (m_asyncMaterialLoadRequested || m_material.Get() != nullptr || m_materialPath.empty()) { + return; + } + + BeginAsyncMaterialLoad(m_materialPath); +} + +void GaussianSplatRendererComponent::ResolvePendingMaterial() { + if (!m_pendingMaterialLoad || !m_pendingMaterialLoad->completed) { + return; + } + + std::shared_ptr completedLoad = std::move(m_pendingMaterialLoad); + m_pendingMaterialLoad.reset(); + + if (!completedLoad->result || completedLoad->result.resource == nullptr) { + return; + } + + m_material = Resources::ResourceHandle( + static_cast(completedLoad->result.resource)); + if (m_material.Get() == nullptr) { + return; + } + + m_materialPath = MaterialPathFromHandle(m_material); + if (!Resources::ResourceManager::Get().TryGetAssetRef( + m_materialPath.c_str(), + Resources::ResourceType::Material, + m_materialRef)) { + m_materialRef.Reset(); + } +} + +} // namespace Components +} // namespace XCEngine diff --git a/engine/src/Rendering/Extraction/RenderSceneExtractor.cpp b/engine/src/Rendering/Extraction/RenderSceneExtractor.cpp index b90f7b97..04112972 100644 --- a/engine/src/Rendering/Extraction/RenderSceneExtractor.cpp +++ b/engine/src/Rendering/Extraction/RenderSceneExtractor.cpp @@ -119,6 +119,12 @@ bool CompareVisibleVolumes(const VisibleVolumeItem& lhs, const VisibleVolumeItem return CompareVisibleVolumesStable(lhs, rhs); } +bool CompareVisibleGaussianSplats( + const VisibleGaussianSplatItem& lhs, + const VisibleGaussianSplatItem& rhs) { + return CompareVisibleGaussianSplatsStable(lhs, rhs); +} + } // namespace RenderSceneData RenderSceneExtractor::Extract( @@ -143,6 +149,7 @@ RenderSceneData RenderSceneExtractor::Extract( cameraPosition, cullingMask, sceneData.visibleItems, + sceneData.visibleGaussianSplats, sceneData.visibleVolumes); } @@ -150,6 +157,10 @@ RenderSceneData RenderSceneExtractor::Extract( sceneData.visibleItems.begin(), sceneData.visibleItems.end(), CompareVisibleItems); + std::stable_sort( + sceneData.visibleGaussianSplats.begin(), + sceneData.visibleGaussianSplats.end(), + CompareVisibleGaussianSplats); std::stable_sort( sceneData.visibleVolumes.begin(), sceneData.visibleVolumes.end(), @@ -181,6 +192,7 @@ RenderSceneData RenderSceneExtractor::ExtractForCamera( cameraPosition, cullingMask, sceneData.visibleItems, + sceneData.visibleGaussianSplats, sceneData.visibleVolumes); } @@ -188,6 +200,10 @@ RenderSceneData RenderSceneExtractor::ExtractForCamera( sceneData.visibleItems.begin(), sceneData.visibleItems.end(), CompareVisibleItems); + std::stable_sort( + sceneData.visibleGaussianSplats.begin(), + sceneData.visibleGaussianSplats.end(), + CompareVisibleGaussianSplats); std::stable_sort( sceneData.visibleVolumes.begin(), sceneData.visibleVolumes.end(), @@ -308,6 +324,7 @@ void RenderSceneExtractor::ExtractVisibleContent( const Math::Vector3& cameraPosition, uint32_t cullingMask, std::vector& visibleItems, + std::vector& visibleGaussianSplats, std::vector& visibleVolumes) const { if (gameObject == nullptr || !gameObject->IsActiveInHierarchy()) { return; @@ -318,11 +335,12 @@ void RenderSceneExtractor::ExtractVisibleContent( if (isVisibleInCameraMask) { AppendRenderItemsForGameObject(*gameObject, cameraPosition, visibleItems); + AppendVisibleGaussianSplatsForGameObject(*gameObject, cameraPosition, visibleGaussianSplats); AppendVisibleVolumesForGameObject(*gameObject, cameraPosition, visibleVolumes); } for (Components::GameObject* child : gameObject->GetChildren()) { - ExtractVisibleContent(child, cameraPosition, cullingMask, visibleItems, visibleVolumes); + ExtractVisibleContent(child, cameraPosition, cullingMask, visibleItems, visibleGaussianSplats, visibleVolumes); } } diff --git a/engine/src/Rendering/Extraction/RenderSceneUtility.cpp b/engine/src/Rendering/Extraction/RenderSceneUtility.cpp index 525d3d23..130c4dd4 100644 --- a/engine/src/Rendering/Extraction/RenderSceneUtility.cpp +++ b/engine/src/Rendering/Extraction/RenderSceneUtility.cpp @@ -1,6 +1,7 @@ #include "Rendering/Extraction/RenderSceneUtility.h" #include "Components/CameraComponent.h" +#include "Components/GaussianSplatRendererComponent.h" #include "Components/GameObject.h" #include "Components/LightComponent.h" #include "Components/MeshFilterComponent.h" @@ -175,6 +176,41 @@ void AppendVisibleVolumesForGameObject( outVisibleVolumes.push_back(visibleVolume); } +void AppendVisibleGaussianSplatsForGameObject( + Components::GameObject& gameObject, + const Math::Vector3& cameraPosition, + std::vector& outVisibleGaussianSplats) { + if (!gameObject.IsActiveInHierarchy()) { + return; + } + + auto* gaussianSplatRenderer = gameObject.GetComponent(); + if (gaussianSplatRenderer == nullptr || !gaussianSplatRenderer->IsEnabled()) { + return; + } + + Resources::GaussianSplat* gaussianSplat = gaussianSplatRenderer->GetGaussianSplat(); + Resources::Material* material = gaussianSplatRenderer->GetMaterial(); + if (gaussianSplat == nullptr || + !gaussianSplat->IsValid() || + material == nullptr) { + return; + } + + const Math::Matrix4x4 localToWorld = gameObject.GetTransform()->GetLocalToWorldMatrix(); + const Math::Vector3 worldPosition = localToWorld.GetTranslation(); + + VisibleGaussianSplatItem visibleGaussianSplat = {}; + visibleGaussianSplat.gameObject = &gameObject; + visibleGaussianSplat.gaussianSplatRenderer = gaussianSplatRenderer; + visibleGaussianSplat.gaussianSplat = gaussianSplat; + visibleGaussianSplat.material = material; + visibleGaussianSplat.renderQueue = ResolveMaterialRenderQueue(material); + visibleGaussianSplat.cameraDistanceSq = (worldPosition - cameraPosition).SqrMagnitude(); + visibleGaussianSplat.localToWorld = localToWorld; + outVisibleGaussianSplats.push_back(visibleGaussianSplat); +} + bool CompareVisibleRenderItemsStable( const VisibleRenderItem& lhs, const VisibleRenderItem& rhs) { @@ -210,6 +246,29 @@ bool CompareVisibleRenderItemsStable( return false; } +bool CompareVisibleGaussianSplatsStable( + const VisibleGaussianSplatItem& lhs, + const VisibleGaussianSplatItem& rhs) { + if (lhs.renderQueue != rhs.renderQueue) { + return lhs.renderQueue < rhs.renderQueue; + } + + const bool isTransparentQueue = IsTransparentRenderQueue(lhs.renderQueue); + if (lhs.cameraDistanceSq != rhs.cameraDistanceSq) { + return isTransparentQueue + ? lhs.cameraDistanceSq > rhs.cameraDistanceSq + : lhs.cameraDistanceSq < rhs.cameraDistanceSq; + } + + const Core::uint64 lhsObjectId = lhs.gameObject != nullptr ? lhs.gameObject->GetID() : 0u; + const Core::uint64 rhsObjectId = rhs.gameObject != nullptr ? rhs.gameObject->GetID() : 0u; + if (lhsObjectId != rhsObjectId) { + return lhsObjectId < rhsObjectId; + } + + return false; +} + bool CompareVisibleVolumesStable( const VisibleVolumeItem& lhs, const VisibleVolumeItem& rhs) { diff --git a/tests/Components/CMakeLists.txt b/tests/Components/CMakeLists.txt index 26dda75d..87e4a1ce 100644 --- a/tests/Components/CMakeLists.txt +++ b/tests/Components/CMakeLists.txt @@ -8,6 +8,7 @@ set(COMPONENTS_TEST_SOURCES test_transform_component.cpp test_game_object.cpp test_camera_light_component.cpp + test_gaussian_splat_renderer_component.cpp test_mesh_render_components.cpp test_volume_renderer_component.cpp ) diff --git a/tests/Components/test_component_factory_registry.cpp b/tests/Components/test_component_factory_registry.cpp index 7ed15cb1..51b9835c 100644 --- a/tests/Components/test_component_factory_registry.cpp +++ b/tests/Components/test_component_factory_registry.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include @@ -23,6 +24,7 @@ TEST(ComponentFactoryRegistry_Test, BuiltInTypesAreRegistered) { EXPECT_TRUE(registry.IsRegistered("AudioListener")); EXPECT_TRUE(registry.IsRegistered("MeshFilter")); EXPECT_TRUE(registry.IsRegistered("MeshRenderer")); + EXPECT_TRUE(registry.IsRegistered("GaussianSplatRenderer")); EXPECT_TRUE(registry.IsRegistered("VolumeRenderer")); EXPECT_FALSE(registry.IsRegistered("Transform")); EXPECT_FALSE(registry.IsRegistered("MissingComponent")); @@ -38,6 +40,7 @@ TEST(ComponentFactoryRegistry_Test, CreateBuiltInComponentsByTypeName) { EXPECT_NE(dynamic_cast(registry.CreateComponent(&gameObject, "AudioListener")), nullptr); EXPECT_NE(dynamic_cast(registry.CreateComponent(&gameObject, "MeshFilter")), nullptr); EXPECT_NE(dynamic_cast(registry.CreateComponent(&gameObject, "MeshRenderer")), nullptr); + EXPECT_NE(dynamic_cast(registry.CreateComponent(&gameObject, "GaussianSplatRenderer")), nullptr); EXPECT_NE(dynamic_cast(registry.CreateComponent(&gameObject, "VolumeRenderer")), nullptr); EXPECT_EQ(registry.CreateComponent(&gameObject, "MissingComponent"), nullptr); } diff --git a/tests/Components/test_gaussian_splat_renderer_component.cpp b/tests/Components/test_gaussian_splat_renderer_component.cpp new file mode 100644 index 00000000..cc6780ef --- /dev/null +++ b/tests/Components/test_gaussian_splat_renderer_component.cpp @@ -0,0 +1,182 @@ +#include + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +using namespace XCEngine::Components; +using namespace XCEngine::Resources; + +namespace { + +GaussianSplat* CreateTestGaussianSplat(const char* name, const char* path) { + auto* gaussianSplat = new GaussianSplat(); + IResource::ConstructParams params = {}; + params.name = name; + params.path = path; + params.guid = ResourceGUID::Generate(path); + gaussianSplat->Initialize(params); + + GaussianSplatMetadata metadata = {}; + metadata.splatCount = 1u; + + XCEngine::Containers::Array sections; + sections.Resize(1); + sections[0].type = GaussianSplatSectionType::Positions; + sections[0].format = GaussianSplatSectionFormat::VectorFloat32; + sections[0].dataOffset = 0u; + sections[0].dataSize = sizeof(GaussianSplatPositionRecord); + sections[0].elementCount = 1u; + sections[0].elementStride = sizeof(GaussianSplatPositionRecord); + + XCEngine::Containers::Array payload; + payload.Resize(sizeof(GaussianSplatPositionRecord)); + const GaussianSplatPositionRecord positionRecord = { XCEngine::Math::Vector3(1.0f, 2.0f, 3.0f) }; + std::memcpy(payload.Data(), &positionRecord, sizeof(positionRecord)); + + EXPECT_TRUE(gaussianSplat->CreateOwned(metadata, std::move(sections), std::move(payload))); + return gaussianSplat; +} + +Material* CreateTestMaterial(const char* name, const char* path) { + auto* material = new Material(); + IResource::ConstructParams params = {}; + params.name = name; + params.path = path; + params.guid = ResourceGUID::Generate(path); + material->Initialize(params); + return material; +} + +class FakeAsyncGaussianSplatLoader final : public IResourceLoader { +public: + ResourceType GetResourceType() const override { return ResourceType::GaussianSplat; } + + XCEngine::Containers::Array GetSupportedExtensions() const override { + XCEngine::Containers::Array extensions; + extensions.PushBack("ply"); + extensions.PushBack("xcgsplat"); + return extensions; + } + + bool CanLoad(const XCEngine::Containers::String& path) const override { + (void)path; + return true; + } + + LoadResult Load( + const XCEngine::Containers::String& path, + const ImportSettings* settings = nullptr) override { + (void)settings; + return LoadResult(CreateTestGaussianSplat("AsyncGaussianSplat", path.CStr())); + } + + ImportSettings* GetDefaultSettings() const override { + return nullptr; + } +}; + +bool PumpAsyncLoadsUntilIdle( + ResourceManager& manager, + std::chrono::milliseconds timeout = std::chrono::milliseconds(2000)) { + const auto deadline = std::chrono::steady_clock::now() + timeout; + while (manager.IsAsyncLoading() && std::chrono::steady_clock::now() < deadline) { + manager.UpdateAsyncLoads(); + std::this_thread::sleep_for(std::chrono::milliseconds(5)); + } + + manager.UpdateAsyncLoads(); + return !manager.IsAsyncLoading(); +} + +TEST(GaussianSplatRendererComponent_Test, SetResourcesCachesHandlesPathsAndFlags) { + GameObject gameObject("GaussianSplatHolder"); + auto* component = gameObject.AddComponent(); + GaussianSplat* gaussianSplat = CreateTestGaussianSplat("Room", "GaussianSplats/room.xcgsplat"); + Material* material = CreateTestMaterial("GaussianSplatMaterial", "Materials/gaussian_splat.mat"); + + component->SetGaussianSplat(gaussianSplat); + component->SetMaterial(material); + component->SetCastShadows(false); + component->SetReceiveShadows(false); + + EXPECT_EQ(component->GetGaussianSplat(), gaussianSplat); + EXPECT_EQ(component->GetGaussianSplatPath(), "GaussianSplats/room.xcgsplat"); + EXPECT_EQ(component->GetMaterial(), material); + EXPECT_EQ(component->GetMaterialPath(), "Materials/gaussian_splat.mat"); + EXPECT_FALSE(component->GetCastShadows()); + EXPECT_FALSE(component->GetReceiveShadows()); + + component->ClearGaussianSplat(); + component->ClearMaterial(); + delete gaussianSplat; + delete material; +} + +TEST(GaussianSplatRendererComponent_Test, SerializeAndDeserializePreservesVirtualPathsAndFlags) { + GaussianSplatRendererComponent source; + source.SetGaussianSplatPath("test://gaussian_splats/room.ply"); + source.SetMaterialPath("test://materials/gaussian_splat.mat"); + source.SetCastShadows(false); + source.SetReceiveShadows(true); + + std::stringstream stream; + source.Serialize(stream); + const std::string serialized = stream.str(); + EXPECT_NE(serialized.find("gaussianSplatPath=test://gaussian_splats/room.ply;"), std::string::npos); + EXPECT_NE(serialized.find("materialPath=test://materials/gaussian_splat.mat;"), std::string::npos); + EXPECT_NE(serialized.find("castShadows=0;"), std::string::npos); + EXPECT_NE(serialized.find("receiveShadows=1;"), std::string::npos); + + GaussianSplatRendererComponent target; + std::stringstream deserializeStream(serialized); + target.Deserialize(deserializeStream); + + EXPECT_EQ(target.GetGaussianSplatPath(), "test://gaussian_splats/room.ply"); + EXPECT_EQ(target.GetMaterialPath(), "test://materials/gaussian_splat.mat"); + EXPECT_FALSE(target.GetCastShadows()); + EXPECT_TRUE(target.GetReceiveShadows()); + EXPECT_FALSE(target.GetGaussianSplatAssetRef().IsValid()); + EXPECT_FALSE(target.GetMaterialAssetRef().IsValid()); +} + +TEST(GaussianSplatRendererComponent_Test, DeferredSceneDeserializeLoadsGaussianSplatAsyncByPath) { + ResourceManager& manager = ResourceManager::Get(); + manager.Initialize(); + + IResourceLoader* originalLoader = manager.GetLoader(ResourceType::GaussianSplat); + FakeAsyncGaussianSplatLoader fakeLoader; + manager.RegisterLoader(&fakeLoader); + + GaussianSplatRendererComponent target; + const auto pendingBeforeDeserialize = manager.GetAsyncPendingCount(); + { + ResourceManager::ScopedDeferredSceneLoad deferredLoadScope; + EXPECT_TRUE(manager.IsDeferredSceneLoadEnabled()); + std::stringstream stream( + "gaussianSplatPath=test://gaussian_splats/async_room.ply;gaussianSplatRef=;materialRef=;castShadows=1;receiveShadows=1;"); + target.Deserialize(stream); + } + + EXPECT_EQ(target.GetGaussianSplatPath(), "test://gaussian_splats/async_room.ply"); + EXPECT_EQ(target.GetGaussianSplat(), nullptr); + EXPECT_GT(manager.GetAsyncPendingCount(), pendingBeforeDeserialize); + ASSERT_TRUE(PumpAsyncLoadsUntilIdle(manager)); + ASSERT_NE(target.GetGaussianSplat(), nullptr); + EXPECT_EQ(target.GetGaussianSplatPath(), "test://gaussian_splats/async_room.ply"); + EXPECT_EQ(target.GetGaussianSplat()->GetSplatCount(), 1u); + + manager.RegisterLoader(originalLoader); + manager.Shutdown(); +} + +} // namespace diff --git a/tests/Rendering/unit/test_render_scene_extractor.cpp b/tests/Rendering/unit/test_render_scene_extractor.cpp index 427c9630..0a2aa7c9 100644 --- a/tests/Rendering/unit/test_render_scene_extractor.cpp +++ b/tests/Rendering/unit/test_render_scene_extractor.cpp @@ -1,6 +1,7 @@ #include #include +#include #include #include #include @@ -16,6 +17,7 @@ #include #include #include +#include #include #include #include @@ -23,6 +25,7 @@ #include #include +#include #include using namespace XCEngine::Components; @@ -117,6 +120,35 @@ VolumeField* CreateTestVolumeField(const char* path) { return volumeField; } +GaussianSplat* CreateTestGaussianSplat(const char* path) { + auto* gaussianSplat = new GaussianSplat(); + IResource::ConstructParams params = {}; + params.name = "TestGaussianSplat"; + params.path = path; + params.guid = ResourceGUID::Generate(path); + gaussianSplat->Initialize(params); + + GaussianSplatMetadata metadata = {}; + metadata.splatCount = 1u; + + XCEngine::Containers::Array sections; + sections.Resize(1); + sections[0].type = GaussianSplatSectionType::Positions; + sections[0].format = GaussianSplatSectionFormat::VectorFloat32; + sections[0].dataOffset = 0u; + sections[0].dataSize = sizeof(GaussianSplatPositionRecord); + sections[0].elementCount = 1u; + sections[0].elementStride = sizeof(GaussianSplatPositionRecord); + + XCEngine::Containers::Array payload; + payload.Resize(sizeof(GaussianSplatPositionRecord)); + const GaussianSplatPositionRecord positionRecord = { Vector3(0.0f, 0.0f, 0.0f) }; + std::memcpy(payload.Data(), &positionRecord, sizeof(positionRecord)); + + EXPECT_TRUE(gaussianSplat->CreateOwned(metadata, std::move(sections), std::move(payload))); + return gaussianSplat; +} + Texture* CreateTestTexture(const char* path, TextureType type) { auto* texture = new Texture(); IResource::ConstructParams params = {}; @@ -468,6 +500,101 @@ TEST(RenderSceneExtractor_Test, ExtractsVisibleVolumesAndSortsByRenderQueue) { delete hiddenMaterial; } +TEST(RenderSceneExtractor_Test, ExtractsVisibleGaussianSplatsAndSortsByRenderQueueAndDistance) { + Scene scene("GaussianSplatScene"); + + GameObject* cameraObject = scene.CreateGameObject("Camera"); + auto* camera = cameraObject->AddComponent(); + camera->SetPrimary(true); + camera->SetCullingMask(1u << 0); + + GameObject* opaqueSplatObject = scene.CreateGameObject("OpaqueGaussianSplat"); + opaqueSplatObject->SetLayer(0); + opaqueSplatObject->GetTransform()->SetLocalPosition(Vector3(0.0f, 0.0f, 2.0f)); + auto* opaqueSplatRenderer = opaqueSplatObject->AddComponent(); + GaussianSplat* opaqueGaussianSplat = CreateTestGaussianSplat("GaussianSplats/opaque_room.xcgsplat"); + Material* opaqueMaterial = CreateTestMaterial( + "Materials/opaque_gaussian_splat.mat", + static_cast(MaterialRenderQueue::Geometry)); + opaqueSplatRenderer->SetGaussianSplat(opaqueGaussianSplat); + opaqueSplatRenderer->SetMaterial(opaqueMaterial); + + GameObject* farTransparentObject = scene.CreateGameObject("FarTransparentGaussianSplat"); + farTransparentObject->SetLayer(0); + farTransparentObject->GetTransform()->SetLocalPosition(Vector3(0.0f, 0.0f, 8.0f)); + auto* farTransparentRenderer = farTransparentObject->AddComponent(); + GaussianSplat* farTransparentGaussianSplat = CreateTestGaussianSplat("GaussianSplats/far_transparent_room.xcgsplat"); + Material* transparentMaterial = CreateTestMaterial( + "Materials/transparent_gaussian_splat.mat", + static_cast(MaterialRenderQueue::Transparent)); + farTransparentRenderer->SetGaussianSplat(farTransparentGaussianSplat); + farTransparentRenderer->SetMaterial(transparentMaterial); + + GameObject* nearTransparentObject = scene.CreateGameObject("NearTransparentGaussianSplat"); + nearTransparentObject->SetLayer(0); + nearTransparentObject->GetTransform()->SetLocalPosition(Vector3(0.0f, 0.0f, 4.0f)); + auto* nearTransparentRenderer = nearTransparentObject->AddComponent(); + GaussianSplat* nearTransparentGaussianSplat = CreateTestGaussianSplat("GaussianSplats/near_transparent_room.xcgsplat"); + nearTransparentRenderer->SetGaussianSplat(nearTransparentGaussianSplat); + nearTransparentRenderer->SetMaterial(transparentMaterial); + + GameObject* hiddenSplatObject = scene.CreateGameObject("HiddenGaussianSplat"); + hiddenSplatObject->SetLayer(2); + auto* hiddenSplatRenderer = hiddenSplatObject->AddComponent(); + GaussianSplat* hiddenGaussianSplat = CreateTestGaussianSplat("GaussianSplats/hidden_room.xcgsplat"); + Material* hiddenMaterial = CreateTestMaterial( + "Materials/hidden_gaussian_splat.mat", + static_cast(MaterialRenderQueue::Geometry)); + hiddenSplatRenderer->SetGaussianSplat(hiddenGaussianSplat); + hiddenSplatRenderer->SetMaterial(hiddenMaterial); + + RenderSceneExtractor extractor; + const RenderSceneData sceneData = extractor.Extract(scene, nullptr, 800, 600); + + ASSERT_TRUE(sceneData.HasCamera()); + EXPECT_TRUE(sceneData.visibleItems.empty()); + EXPECT_TRUE(sceneData.visibleVolumes.empty()); + ASSERT_EQ(sceneData.visibleGaussianSplats.size(), 3u); + + EXPECT_EQ(sceneData.visibleGaussianSplats[0].gameObject, opaqueSplatObject); + EXPECT_EQ(sceneData.visibleGaussianSplats[0].gaussianSplatRenderer, opaqueSplatRenderer); + EXPECT_EQ(sceneData.visibleGaussianSplats[0].gaussianSplat, opaqueGaussianSplat); + EXPECT_EQ(sceneData.visibleGaussianSplats[0].material, opaqueMaterial); + EXPECT_EQ(sceneData.visibleGaussianSplats[0].renderQueue, static_cast(MaterialRenderQueue::Geometry)); + EXPECT_EQ(sceneData.visibleGaussianSplats[0].localToWorld.GetTranslation(), Vector3(0.0f, 0.0f, 2.0f)); + + EXPECT_EQ(sceneData.visibleGaussianSplats[1].gameObject, farTransparentObject); + EXPECT_EQ(sceneData.visibleGaussianSplats[1].gaussianSplatRenderer, farTransparentRenderer); + EXPECT_EQ(sceneData.visibleGaussianSplats[1].gaussianSplat, farTransparentGaussianSplat); + EXPECT_EQ(sceneData.visibleGaussianSplats[1].material, transparentMaterial); + EXPECT_EQ(sceneData.visibleGaussianSplats[1].renderQueue, static_cast(MaterialRenderQueue::Transparent)); + EXPECT_EQ(sceneData.visibleGaussianSplats[1].localToWorld.GetTranslation(), Vector3(0.0f, 0.0f, 8.0f)); + + EXPECT_EQ(sceneData.visibleGaussianSplats[2].gameObject, nearTransparentObject); + EXPECT_EQ(sceneData.visibleGaussianSplats[2].gaussianSplatRenderer, nearTransparentRenderer); + EXPECT_EQ(sceneData.visibleGaussianSplats[2].gaussianSplat, nearTransparentGaussianSplat); + EXPECT_EQ(sceneData.visibleGaussianSplats[2].material, transparentMaterial); + EXPECT_EQ(sceneData.visibleGaussianSplats[2].renderQueue, static_cast(MaterialRenderQueue::Transparent)); + EXPECT_EQ(sceneData.visibleGaussianSplats[2].localToWorld.GetTranslation(), Vector3(0.0f, 0.0f, 4.0f)); + + opaqueSplatRenderer->ClearGaussianSplat(); + opaqueSplatRenderer->ClearMaterial(); + farTransparentRenderer->ClearGaussianSplat(); + farTransparentRenderer->ClearMaterial(); + nearTransparentRenderer->ClearGaussianSplat(); + nearTransparentRenderer->ClearMaterial(); + hiddenSplatRenderer->ClearGaussianSplat(); + hiddenSplatRenderer->ClearMaterial(); + + delete opaqueGaussianSplat; + delete farTransparentGaussianSplat; + delete nearTransparentGaussianSplat; + delete hiddenGaussianSplat; + delete opaqueMaterial; + delete transparentMaterial; + delete hiddenMaterial; +} + TEST(RenderSceneExtractor_Test, ExtractsSectionLevelVisibleItemsAndSortsByRenderQueue) { Scene scene("SectionScene"); @@ -971,6 +1098,61 @@ TEST(RenderMaterialUtility_Test, ExposesSchemaDrivenMaterialConstantPayload) { EXPECT_FLOAT_EQ(cutoffValues[0], 0.6f); } +TEST(RenderMaterialUtility_Test, BuildsBuiltinDepthStylePayloadFromMaterialSemantics) { + auto* shader = new Shader(); + + ShaderPropertyDesc colorProperty = {}; + colorProperty.name = "_BaseColor"; + colorProperty.type = ShaderPropertyType::Color; + colorProperty.defaultValue = "(1,1,1,1)"; + colorProperty.semantic = "BaseColor"; + shader->AddProperty(colorProperty); + + ShaderPropertyDesc cutoffProperty = {}; + cutoffProperty.name = "_Cutoff"; + cutoffProperty.type = ShaderPropertyType::Range; + cutoffProperty.defaultValue = "0.5"; + cutoffProperty.semantic = "AlphaCutoff"; + shader->AddProperty(cutoffProperty); + + Material material; + material.SetShader(ResourceHandle(shader)); + material.SetFloat4("_BaseColor", Vector4(0.2f, 0.4f, 0.6f, 0.8f)); + material.SetFloat("_Cutoff", 0.35f); + + BuiltinDepthStyleMaterialConstants constants = {}; + MaterialConstantFieldDesc layout[2] = {}; + const MaterialConstantPayloadView payload = + ResolveBuiltinDepthStyleMaterialConstantPayload(&material, constants, layout); + + ASSERT_TRUE(payload.IsValid()); + EXPECT_EQ(payload.size, sizeof(BuiltinDepthStyleMaterialConstants)); + ASSERT_TRUE(payload.layout.IsValid()); + EXPECT_EQ(payload.layout.count, 2u); + EXPECT_EQ(payload.layout.fields[0].name, "gBaseColorFactor"); + EXPECT_EQ(payload.layout.fields[1].name, "gAlphaCutoffParams"); + + const auto* typedConstants = static_cast(payload.data); + ASSERT_NE(typedConstants, nullptr); + EXPECT_EQ(typedConstants->baseColorFactor, Vector4(0.2f, 0.4f, 0.6f, 0.8f)); + EXPECT_EQ(typedConstants->alphaCutoffParams, Vector4(0.35f, 0.0f, 0.0f, 0.0f)); +} + +TEST(RenderMaterialUtility_Test, BuildsBuiltinDepthStylePayloadDefaultsWithoutMaterial) { + BuiltinDepthStyleMaterialConstants constants = {}; + MaterialConstantFieldDesc layout[2] = {}; + const MaterialConstantPayloadView payload = + ResolveBuiltinDepthStyleMaterialConstantPayload(nullptr, constants, layout); + + ASSERT_TRUE(payload.IsValid()); + EXPECT_EQ(payload.size, sizeof(BuiltinDepthStyleMaterialConstants)); + + const auto* typedConstants = static_cast(payload.data); + ASSERT_NE(typedConstants, nullptr); + EXPECT_EQ(typedConstants->baseColorFactor, Vector4::One()); + EXPECT_EQ(typedConstants->alphaCutoffParams, Vector4(0.5f, 0.0f, 0.0f, 0.0f)); +} + TEST(RenderMaterialUtility_Test, DoesNotUseOpacityFallbackWithoutFormalShaderSemanticMetadata) { Material material; material.SetFloat("opacity", 0.35f);