#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace XCEngine::Components; using namespace XCEngine::Core; using namespace XCEngine::Math; using namespace XCEngine::Rendering; using namespace XCEngine::Resources; namespace { class MockRenderBuffer final : public XCEngine::RHI::RHIBuffer { public: explicit MockRenderBuffer(uint64_t size = 512u, uint32_t stride = 16u) : m_size(size) , m_stride(stride) { } void* Map() override { return nullptr; } void Unmap() override {} void SetData(const void*, size_t, size_t) override {} uint64_t GetSize() const override { return m_size; } XCEngine::RHI::BufferType GetBufferType() const override { return XCEngine::RHI::BufferType::Storage; } void SetBufferType(XCEngine::RHI::BufferType) override {} uint32_t GetStride() const override { return m_stride; } void SetStride(uint32_t stride) override { m_stride = stride; } void* GetNativeHandle() override { return nullptr; } XCEngine::RHI::ResourceStates GetState() const override { return XCEngine::RHI::ResourceStates::Common; } void SetState(XCEngine::RHI::ResourceStates) override {} const std::string& GetName() const override { return m_name; } void SetName(const std::string& name) override { m_name = name; } void Shutdown() override {} private: uint64_t m_size = 0; uint32_t m_stride = 0; std::string m_name; }; Mesh* CreateTestMesh(const char* path) { auto* mesh = new Mesh(); IResource::ConstructParams params = {}; params.name = "TestMesh"; params.path = path; params.guid = ResourceGUID::Generate(path); mesh->Initialize(params); return mesh; } Mesh* CreateSectionedTestMesh(const char* path, std::initializer_list materialIds) { Mesh* mesh = CreateTestMesh(path); uint32_t startIndex = 0; for (uint32_t materialId : materialIds) { MeshSection section = {}; section.baseVertex = 0; section.vertexCount = 3; section.startIndex = startIndex; section.indexCount = 3; section.materialID = materialId; mesh->AddSection(section); startIndex += 3; } return mesh; } Material* CreateTestMaterial(const char* path, int32_t renderQueue) { auto* material = new Material(); IResource::ConstructParams params = {}; params.name = "TestMaterial"; params.path = path; params.guid = ResourceGUID::Generate(path); material->Initialize(params); material->SetRenderQueue(renderQueue); return material; } VolumeField* CreateTestVolumeField(const char* path) { auto* volumeField = new VolumeField(); IResource::ConstructParams params = {}; params.name = "TestVolume"; params.path = path; params.guid = ResourceGUID::Generate(path); volumeField->Initialize(params); const unsigned char payload[8] = { 1, 2, 3, 4, 5, 6, 7, 8 }; EXPECT_TRUE(volumeField->Create( VolumeStorageKind::NanoVDB, payload, sizeof(payload), Bounds(Vector3::Zero(), Vector3(2.0f, 2.0f, 2.0f)), Vector3(0.25f, 0.25f, 0.25f))); 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 = {}; params.name = path; params.path = path; params.guid = ResourceGUID::Generate(path); texture->Initialize(params); const uint32 arraySize = (type == TextureType::TextureCube || type == TextureType::TextureCubeArray) ? 6u : 1u; const unsigned char pixels[6 * 4] = { 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255 }; EXPECT_TRUE(texture->Create( 1u, 1u, 1u, 1u, type, TextureFormat::RGBA8_UNORM, pixels, static_cast(arraySize) * 4u, arraySize)); return texture; } TEST(RenderSceneExtractor_Test, SelectsHighestDepthPrimaryCameraAndVisibleObjects) { Scene scene("RenderScene"); GameObject* cameraObjectA = scene.CreateGameObject("CameraA"); auto* cameraA = cameraObjectA->AddComponent(); cameraA->SetPrimary(true); cameraA->SetDepth(0.0f); GameObject* cameraObjectB = scene.CreateGameObject("CameraB"); auto* cameraB = cameraObjectB->AddComponent(); cameraB->SetPrimary(true); cameraB->SetDepth(5.0f); cameraObjectB->GetTransform()->SetLocalPosition(Vector3(2.0f, 3.0f, 4.0f)); GameObject* visibleObject = scene.CreateGameObject("VisibleQuad"); visibleObject->GetTransform()->SetLocalPosition(Vector3(1.0f, 2.0f, 3.0f)); auto* meshFilter = visibleObject->AddComponent(); visibleObject->AddComponent(); Mesh* visibleMesh = CreateTestMesh("Meshes/visible.mesh"); meshFilter->SetMesh(visibleMesh); GameObject* hiddenObject = scene.CreateGameObject("HiddenQuad"); hiddenObject->SetActive(false); auto* hiddenMeshFilter = hiddenObject->AddComponent(); hiddenObject->AddComponent(); Mesh* hiddenMesh = CreateTestMesh("Meshes/hidden.mesh"); hiddenMeshFilter->SetMesh(hiddenMesh); RenderSceneExtractor extractor; const RenderSceneData sceneData = extractor.Extract(scene, nullptr, 1280, 720); ASSERT_TRUE(sceneData.HasCamera()); EXPECT_EQ(sceneData.camera, cameraB); EXPECT_EQ(sceneData.cameraData.viewportWidth, 1280u); EXPECT_EQ(sceneData.cameraData.viewportHeight, 720u); EXPECT_EQ(sceneData.cameraData.worldPosition, Vector3(2.0f, 3.0f, 4.0f)); ASSERT_EQ(sceneData.visibleItems.size(), 1u); EXPECT_EQ(sceneData.visibleItems[0].gameObject, visibleObject); EXPECT_EQ(sceneData.visibleItems[0].mesh, visibleMesh); EXPECT_EQ(sceneData.visibleItems[0].localToWorld.GetTranslation(), Vector3(1.0f, 2.0f, 3.0f)); EXPECT_FALSE(sceneData.visibleItems[0].hasSection); EXPECT_EQ(sceneData.visibleItems[0].renderQueue, static_cast(MaterialRenderQueue::Geometry)); meshFilter->ClearMesh(); hiddenMeshFilter->ClearMesh(); delete visibleMesh; delete hiddenMesh; } TEST(RenderSceneExtractor_Test, OverrideCameraTakesPriority) { Scene scene("OverrideScene"); GameObject* primaryObject = scene.CreateGameObject("PrimaryCamera"); auto* primaryCamera = primaryObject->AddComponent(); primaryCamera->SetPrimary(true); primaryCamera->SetDepth(10.0f); GameObject* overrideObject = scene.CreateGameObject("OverrideCamera"); auto* overrideCamera = overrideObject->AddComponent(); overrideCamera->SetPrimary(false); overrideCamera->SetDepth(-1.0f); RenderSceneExtractor extractor; const RenderSceneData sceneData = extractor.Extract(scene, overrideCamera, 640, 480); ASSERT_TRUE(sceneData.HasCamera()); EXPECT_EQ(sceneData.camera, overrideCamera); EXPECT_NE(sceneData.camera, primaryCamera); EXPECT_EQ(sceneData.cameraData.viewportWidth, 640u); EXPECT_EQ(sceneData.cameraData.viewportHeight, 480u); } TEST(RenderSceneExtractor_Test, ExtractsBrightestDirectionalLightAsMainLight) { Scene scene("LightingScene"); GameObject* cameraObject = scene.CreateGameObject("Camera"); auto* camera = cameraObject->AddComponent(); camera->SetPrimary(true); GameObject* fillLightObject = scene.CreateGameObject("FillLight"); auto* fillLight = fillLightObject->AddComponent(); fillLight->SetLightType(LightType::Directional); fillLight->SetColor(Color(0.2f, 0.4f, 0.8f, 1.0f)); fillLight->SetIntensity(0.5f); GameObject* pointLightObject = scene.CreateGameObject("PointLight"); auto* pointLight = pointLightObject->AddComponent(); pointLight->SetLightType(LightType::Point); pointLight->SetIntensity(10.0f); GameObject* mainLightObject = scene.CreateGameObject("MainLight"); auto* mainLight = mainLightObject->AddComponent(); mainLight->SetLightType(LightType::Directional); mainLight->SetColor(Color(1.0f, 0.8f, 0.6f, 1.0f)); mainLight->SetIntensity(2.5f); mainLight->SetCastsShadows(true); mainLightObject->GetTransform()->SetLocalRotation( Quaternion::LookRotation(Vector3(-0.3f, -1.0f, -0.2f).Normalized())); RenderSceneExtractor extractor; const RenderSceneData sceneData = extractor.Extract(scene, nullptr, 800, 600); ASSERT_TRUE(sceneData.HasCamera()); ASSERT_TRUE(sceneData.lighting.HasMainDirectionalLight()); EXPECT_FLOAT_EQ(sceneData.lighting.mainDirectionalLight.intensity, 2.5f); EXPECT_EQ(sceneData.lighting.mainDirectionalLight.color.r, 1.0f); EXPECT_EQ(sceneData.lighting.mainDirectionalLight.color.g, 0.8f); EXPECT_EQ(sceneData.lighting.mainDirectionalLight.color.b, 0.6f); EXPECT_TRUE(sceneData.lighting.mainDirectionalLight.castsShadows); EXPECT_EQ( sceneData.lighting.mainDirectionalLight.direction, mainLightObject->GetTransform()->GetForward().Normalized() * -1.0f); ASSERT_EQ(sceneData.lighting.additionalLightCount, 2u); EXPECT_EQ(sceneData.lighting.additionalLights[0].type, RenderLightType::Directional); EXPECT_FLOAT_EQ(sceneData.lighting.additionalLights[0].intensity, 0.5f); EXPECT_EQ(sceneData.lighting.additionalLights[1].type, RenderLightType::Point); EXPECT_FLOAT_EQ(sceneData.lighting.additionalLights[1].intensity, 10.0f); } TEST(RenderSceneExtractor_Test, ExtractsAdditionalLightsWithoutMainDirectionalAndFiltersByLightCullingMask) { Scene scene("AdditionalLightsScene"); GameObject* cameraObject = scene.CreateGameObject("Camera"); auto* camera = cameraObject->AddComponent(); camera->SetPrimary(true); camera->SetCullingMask(1u << 3); GameObject* hiddenSpotObject = scene.CreateGameObject("HiddenSpot"); hiddenSpotObject->SetLayer(0); auto* hiddenSpot = hiddenSpotObject->AddComponent(); hiddenSpot->SetLightType(LightType::Spot); hiddenSpot->SetIntensity(100.0f); hiddenSpot->SetRange(50.0f); hiddenSpot->SetSpotAngle(60.0f); hiddenSpotObject->GetTransform()->SetLocalPosition(Vector3(0.0f, 0.0f, 1.0f)); GameObject* visiblePointObject = scene.CreateGameObject("VisiblePoint"); visiblePointObject->SetLayer(3); auto* visiblePoint = visiblePointObject->AddComponent(); visiblePoint->SetLightType(LightType::Point); visiblePoint->SetColor(Color(0.9f, 0.2f, 0.1f, 1.0f)); visiblePoint->SetIntensity(4.0f); visiblePoint->SetRange(12.0f); visiblePointObject->GetTransform()->SetLocalPosition(Vector3(0.0f, 0.0f, 4.0f)); GameObject* visibleSpotObject = scene.CreateGameObject("VisibleSpot"); visibleSpotObject->SetLayer(3); auto* visibleSpot = visibleSpotObject->AddComponent(); visibleSpot->SetLightType(LightType::Spot); visibleSpot->SetColor(Color(0.1f, 0.8f, 0.4f, 1.0f)); visibleSpot->SetIntensity(3.0f); visibleSpot->SetRange(8.0f); visibleSpot->SetSpotAngle(48.0f); visibleSpotObject->GetTransform()->SetLocalPosition(Vector3(0.0f, 0.0f, 2.0f)); visibleSpotObject->GetTransform()->SetLocalRotation( Quaternion::LookRotation(Vector3(0.0f, 0.0f, -1.0f))); RenderSceneExtractor extractor; const RenderSceneData sceneData = extractor.Extract(scene, nullptr, 800, 600); EXPECT_FALSE(sceneData.lighting.HasMainDirectionalLight()); ASSERT_TRUE(sceneData.lighting.HasAdditionalLights()); ASSERT_EQ(sceneData.lighting.additionalLightCount, 2u); const RenderAdditionalLightData& spotLight = sceneData.lighting.additionalLights[0]; EXPECT_EQ(spotLight.type, RenderLightType::Spot); EXPECT_EQ(spotLight.color.g, 0.8f); EXPECT_EQ(spotLight.position, Vector3(0.0f, 0.0f, 2.0f)); EXPECT_EQ(spotLight.direction, Vector3(0.0f, 0.0f, 1.0f)); EXPECT_FLOAT_EQ(spotLight.range, 8.0f); EXPECT_FLOAT_EQ(spotLight.spotAngle, 48.0f); const RenderAdditionalLightData& pointLightData = sceneData.lighting.additionalLights[1]; EXPECT_EQ(pointLightData.type, RenderLightType::Point); EXPECT_EQ(pointLightData.color.r, 0.9f); EXPECT_EQ(pointLightData.position, Vector3(0.0f, 0.0f, 4.0f)); EXPECT_FLOAT_EQ(pointLightData.range, 12.0f); EXPECT_FLOAT_EQ(pointLightData.spotAngle, 0.0f); } TEST(RenderSceneExtractor_Test, LimitsAdditionalLightsToBoundedCountUsingStableTieBreakOrder) { Scene scene("AdditionalLightCapScene"); GameObject* cameraObject = scene.CreateGameObject("Camera"); auto* camera = cameraObject->AddComponent(); camera->SetPrimary(true); for (uint32_t index = 0; index < RenderLightingData::kMaxAdditionalLightCount + 2u; ++index) { GameObject* lightObject = scene.CreateGameObject("PointLight" + std::to_string(index)); auto* light = lightObject->AddComponent(); light->SetLightType(LightType::Point); light->SetIntensity(1.0f); light->SetRange(16.0f); light->SetColor(Color(static_cast(index), 0.0f, 0.0f, 1.0f)); lightObject->GetTransform()->SetLocalPosition(Vector3(0.0f, 0.0f, 4.0f)); } RenderSceneExtractor extractor; const RenderSceneData sceneData = extractor.Extract(scene, nullptr, 800, 600); ASSERT_FALSE(sceneData.lighting.HasMainDirectionalLight()); ASSERT_EQ(sceneData.lighting.additionalLightCount, RenderLightingData::kMaxAdditionalLightCount); for (uint32_t index = 0; index < RenderLightingData::kMaxAdditionalLightCount; ++index) { EXPECT_EQ(sceneData.lighting.additionalLights[index].type, RenderLightType::Point); EXPECT_FLOAT_EQ(sceneData.lighting.additionalLights[index].color.r, static_cast(index)); } } TEST(RenderSceneExtractor_Test, FiltersVisibleItemsByCameraCullingMask) { Scene scene("CullingMaskScene"); GameObject* cameraObject = scene.CreateGameObject("Camera"); auto* camera = cameraObject->AddComponent(); camera->SetPrimary(true); camera->SetCullingMask(1u << 5); GameObject* visibleObject = scene.CreateGameObject("VisibleLayer5"); visibleObject->SetLayer(5); auto* visibleMeshFilter = visibleObject->AddComponent(); visibleObject->AddComponent(); Mesh* visibleMesh = CreateTestMesh("Meshes/layer5.mesh"); visibleMeshFilter->SetMesh(visibleMesh); GameObject* hiddenObject = scene.CreateGameObject("HiddenLayer0"); hiddenObject->SetLayer(0); auto* hiddenMeshFilter = hiddenObject->AddComponent(); hiddenObject->AddComponent(); Mesh* hiddenMesh = CreateTestMesh("Meshes/layer0.mesh"); hiddenMeshFilter->SetMesh(hiddenMesh); RenderSceneExtractor extractor; const RenderSceneData sceneData = extractor.Extract(scene, nullptr, 800, 600); ASSERT_EQ(sceneData.visibleItems.size(), 1u); EXPECT_EQ(sceneData.visibleItems[0].gameObject, visibleObject); EXPECT_EQ(sceneData.visibleItems[0].mesh, visibleMesh); visibleMeshFilter->ClearMesh(); hiddenMeshFilter->ClearMesh(); delete visibleMesh; delete hiddenMesh; } TEST(RenderSceneExtractor_Test, ExtractsVisibleVolumesAndSortsByRenderQueue) { Scene scene("VolumeScene"); GameObject* cameraObject = scene.CreateGameObject("Camera"); auto* camera = cameraObject->AddComponent(); camera->SetPrimary(true); camera->SetCullingMask(1u << 0); GameObject* opaqueVolumeObject = scene.CreateGameObject("OpaqueVolume"); opaqueVolumeObject->SetLayer(0); opaqueVolumeObject->GetTransform()->SetLocalPosition(Vector3(0.0f, 0.0f, 2.0f)); auto* opaqueVolumeRenderer = opaqueVolumeObject->AddComponent(); VolumeField* opaqueVolume = CreateTestVolumeField("Volumes/opaque.nvdb"); Material* opaqueMaterial = CreateTestMaterial( "Materials/opaque_volume.mat", static_cast(MaterialRenderQueue::Geometry)); opaqueVolumeRenderer->SetVolumeField(opaqueVolume); opaqueVolumeRenderer->SetMaterial(opaqueMaterial); GameObject* transparentVolumeObject = scene.CreateGameObject("TransparentVolume"); transparentVolumeObject->SetLayer(0); transparentVolumeObject->GetTransform()->SetLocalPosition(Vector3(0.0f, 0.0f, 8.0f)); auto* transparentVolumeRenderer = transparentVolumeObject->AddComponent(); VolumeField* transparentVolume = CreateTestVolumeField("Volumes/transparent.nvdb"); Material* transparentMaterial = CreateTestMaterial( "Materials/transparent_volume.mat", static_cast(MaterialRenderQueue::Transparent)); transparentVolumeRenderer->SetVolumeField(transparentVolume); transparentVolumeRenderer->SetMaterial(transparentMaterial); GameObject* hiddenVolumeObject = scene.CreateGameObject("HiddenVolume"); hiddenVolumeObject->SetLayer(2); auto* hiddenVolumeRenderer = hiddenVolumeObject->AddComponent(); VolumeField* hiddenVolume = CreateTestVolumeField("Volumes/hidden.nvdb"); Material* hiddenMaterial = CreateTestMaterial( "Materials/hidden_volume.mat", static_cast(MaterialRenderQueue::Geometry)); hiddenVolumeRenderer->SetVolumeField(hiddenVolume); hiddenVolumeRenderer->SetMaterial(hiddenMaterial); RenderSceneExtractor extractor; const RenderSceneData sceneData = extractor.Extract(scene, nullptr, 800, 600); ASSERT_TRUE(sceneData.HasCamera()); EXPECT_TRUE(sceneData.visibleItems.empty()); ASSERT_EQ(sceneData.visibleVolumes.size(), 2u); EXPECT_EQ(sceneData.visibleVolumes[0].gameObject, opaqueVolumeObject); EXPECT_EQ(sceneData.visibleVolumes[0].volumeRenderer, opaqueVolumeRenderer); EXPECT_EQ(sceneData.visibleVolumes[0].volumeField, opaqueVolume); EXPECT_EQ(sceneData.visibleVolumes[0].material, opaqueMaterial); EXPECT_EQ(sceneData.visibleVolumes[0].renderQueue, static_cast(MaterialRenderQueue::Geometry)); EXPECT_EQ(sceneData.visibleVolumes[0].localToWorld.GetTranslation(), Vector3(0.0f, 0.0f, 2.0f)); EXPECT_EQ(sceneData.visibleVolumes[1].gameObject, transparentVolumeObject); EXPECT_EQ(sceneData.visibleVolumes[1].volumeRenderer, transparentVolumeRenderer); EXPECT_EQ(sceneData.visibleVolumes[1].volumeField, transparentVolume); EXPECT_EQ(sceneData.visibleVolumes[1].material, transparentMaterial); EXPECT_EQ(sceneData.visibleVolumes[1].renderQueue, static_cast(MaterialRenderQueue::Transparent)); EXPECT_EQ(sceneData.visibleVolumes[1].localToWorld.GetTranslation(), Vector3(0.0f, 0.0f, 8.0f)); opaqueVolumeRenderer->ClearVolumeField(); opaqueVolumeRenderer->ClearMaterial(); transparentVolumeRenderer->ClearVolumeField(); transparentVolumeRenderer->ClearMaterial(); hiddenVolumeRenderer->ClearVolumeField(); hiddenVolumeRenderer->ClearMaterial(); delete opaqueVolume; delete opaqueMaterial; delete transparentVolume; delete transparentMaterial; delete hiddenVolume; 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"); GameObject* cameraObject = scene.CreateGameObject("Camera"); auto* camera = cameraObject->AddComponent(); camera->SetPrimary(true); GameObject* renderObject = scene.CreateGameObject("MultiSectionMesh"); renderObject->GetTransform()->SetLocalPosition(Vector3(0.0f, 0.0f, 5.0f)); auto* meshFilter = renderObject->AddComponent(); auto* meshRenderer = renderObject->AddComponent(); Mesh* mesh = CreateSectionedTestMesh("Meshes/sectioned.mesh", { 1u, 0u }); Material* opaqueMaterial = CreateTestMaterial( "Materials/opaque.mat", static_cast(MaterialRenderQueue::Geometry)); Material* transparentMaterial = CreateTestMaterial( "Materials/transparent.mat", static_cast(MaterialRenderQueue::Transparent)); meshFilter->SetMesh(mesh); meshRenderer->SetMaterial(0, opaqueMaterial); meshRenderer->SetMaterial(1, transparentMaterial); RenderSceneExtractor extractor; const RenderSceneData sceneData = extractor.Extract(scene, nullptr, 800, 600); ASSERT_TRUE(sceneData.HasCamera()); ASSERT_EQ(sceneData.visibleItems.size(), 2u); EXPECT_TRUE(sceneData.visibleItems[0].hasSection); EXPECT_EQ(sceneData.visibleItems[0].sectionIndex, 1u); EXPECT_EQ(sceneData.visibleItems[0].materialIndex, 0u); EXPECT_EQ(sceneData.visibleItems[0].material, opaqueMaterial); EXPECT_EQ(sceneData.visibleItems[0].renderQueue, static_cast(MaterialRenderQueue::Geometry)); EXPECT_TRUE(sceneData.visibleItems[1].hasSection); EXPECT_EQ(sceneData.visibleItems[1].sectionIndex, 0u); EXPECT_EQ(sceneData.visibleItems[1].materialIndex, 1u); EXPECT_EQ(sceneData.visibleItems[1].material, transparentMaterial); EXPECT_EQ(sceneData.visibleItems[1].renderQueue, static_cast(MaterialRenderQueue::Transparent)); meshRenderer->ClearMaterials(); meshFilter->ClearMesh(); delete opaqueMaterial; delete transparentMaterial; delete mesh; } TEST(RenderSceneExtractor_Test, SortsOpaqueFrontToBackAndTransparentBackToFront) { Scene scene("QueueSortScene"); GameObject* cameraObject = scene.CreateGameObject("Camera"); auto* camera = cameraObject->AddComponent(); camera->SetPrimary(true); auto createRenderable = [&](const char* name, float z, int32_t renderQueue) -> Mesh* { GameObject* object = scene.CreateGameObject(name); object->GetTransform()->SetLocalPosition(Vector3(0.0f, 0.0f, z)); auto* meshFilter = object->AddComponent(); auto* meshRenderer = object->AddComponent(); Mesh* mesh = CreateTestMesh(name); Material* material = CreateTestMaterial(name, renderQueue); meshFilter->SetMesh(mesh); meshRenderer->SetMaterial(0, material); return mesh; }; Mesh* opaqueFarMesh = createRenderable("OpaqueFar", 10.0f, static_cast(MaterialRenderQueue::Geometry)); Mesh* transparentNearMesh = createRenderable("TransparentNear", 2.0f, static_cast(MaterialRenderQueue::Transparent)); Mesh* opaqueNearMesh = createRenderable("OpaqueNear", 2.0f, static_cast(MaterialRenderQueue::Geometry)); Mesh* transparentFarMesh = createRenderable("TransparentFar", 10.0f, static_cast(MaterialRenderQueue::Transparent)); RenderSceneExtractor extractor; const RenderSceneData sceneData = extractor.Extract(scene, nullptr, 800, 600); ASSERT_EQ(sceneData.visibleItems.size(), 4u); EXPECT_STREQ(sceneData.visibleItems[0].gameObject->GetName().c_str(), "OpaqueNear"); EXPECT_STREQ(sceneData.visibleItems[1].gameObject->GetName().c_str(), "OpaqueFar"); EXPECT_STREQ(sceneData.visibleItems[2].gameObject->GetName().c_str(), "TransparentFar"); EXPECT_STREQ(sceneData.visibleItems[3].gameObject->GetName().c_str(), "TransparentNear"); auto cleanupObject = [](GameObject* object) { auto* meshFilter = object->GetComponent(); auto* meshRenderer = object->GetComponent(); Material* material = meshRenderer != nullptr ? meshRenderer->GetMaterial(0) : nullptr; if (meshRenderer != nullptr) { meshRenderer->ClearMaterials(); } if (meshFilter != nullptr) { meshFilter->ClearMesh(); } delete material; }; cleanupObject(scene.Find("OpaqueFar")); cleanupObject(scene.Find("TransparentNear")); cleanupObject(scene.Find("OpaqueNear")); cleanupObject(scene.Find("TransparentFar")); delete opaqueFarMesh; delete transparentNearMesh; delete opaqueNearMesh; delete transparentFarMesh; } TEST(RenderSceneExtractor_Test, FallsBackToEmbeddedMeshMaterialsWhenRendererHasNoExplicitSlots) { Scene scene("EmbeddedMaterialScene"); GameObject* cameraObject = scene.CreateGameObject("Camera"); auto* camera = cameraObject->AddComponent(); camera->SetPrimary(true); GameObject* renderObject = scene.CreateGameObject("EmbeddedBackpack"); auto* meshFilter = renderObject->AddComponent(); auto* meshRenderer = renderObject->AddComponent(); Mesh* mesh = CreateSectionedTestMesh("Meshes/embedded.mesh", { 0u }); Material* embeddedMaterial = CreateTestMaterial( "Materials/embedded.mat", static_cast(MaterialRenderQueue::Transparent)); mesh->AddMaterial(embeddedMaterial); meshFilter->SetMesh(mesh); meshRenderer->ClearMaterials(); RenderSceneExtractor extractor; const RenderSceneData sceneData = extractor.Extract(scene, nullptr, 800, 600); ASSERT_EQ(sceneData.visibleItems.size(), 1u); EXPECT_EQ(sceneData.visibleItems[0].material, embeddedMaterial); EXPECT_EQ(sceneData.visibleItems[0].renderQueue, static_cast(MaterialRenderQueue::Transparent)); EXPECT_EQ(ResolveMaterial(sceneData.visibleItems[0]), embeddedMaterial); meshFilter->ClearMesh(); delete mesh; } TEST(RenderMaterialUtility_Test, NullMaterialsDoNotMatchBuiltinSurfacePasses) { EXPECT_FALSE(MatchesBuiltinPass(nullptr, BuiltinMaterialPass::ForwardLit)); EXPECT_FALSE(MatchesBuiltinPass(nullptr, BuiltinMaterialPass::Unlit)); } TEST(RenderMaterialUtility_Test, MaterialsWithoutShaderMetadataDoNotMatchBuiltinSurfacePasses) { Material noMetadataMaterial; EXPECT_FALSE(MatchesBuiltinPass(&noMetadataMaterial, BuiltinMaterialPass::ForwardLit)); EXPECT_FALSE(MatchesBuiltinPass(&noMetadataMaterial, BuiltinMaterialPass::Unlit)); } TEST(RenderMaterialUtility_Test, ShaderPassMetadataSupportsUnlitDepthAndObjectId) { Material unlitMaterial; auto* unlitShader = new Shader(); ShaderPass unlitPass = {}; unlitPass.name = "Unlit"; unlitShader->AddPass(unlitPass); unlitMaterial.SetShader(ResourceHandle(unlitShader)); EXPECT_TRUE(MatchesBuiltinPass(&unlitMaterial, BuiltinMaterialPass::Unlit)); EXPECT_FALSE(MatchesBuiltinPass(&unlitMaterial, BuiltinMaterialPass::ForwardLit)); Material depthMaterial; auto* depthShader = new Shader(); ShaderPass depthPass = {}; depthPass.name = "DepthOnly"; depthShader->AddPass(depthPass); depthMaterial.SetShader(ResourceHandle(depthShader)); EXPECT_TRUE(MatchesBuiltinPass(&depthMaterial, BuiltinMaterialPass::DepthOnly)); EXPECT_FALSE(MatchesBuiltinPass(&depthMaterial, BuiltinMaterialPass::Unlit)); Material objectIdMaterial; auto* objectIdShader = new Shader(); ShaderPass objectIdPass = {}; objectIdPass.name = "ObjectId"; objectIdShader->AddPass(objectIdPass); objectIdMaterial.SetShader(ResourceHandle(objectIdShader)); EXPECT_TRUE(MatchesBuiltinPass(&objectIdMaterial, BuiltinMaterialPass::ObjectId)); EXPECT_FALSE(MatchesBuiltinPass(&objectIdMaterial, BuiltinMaterialPass::ForwardLit)); } TEST(RenderMaterialUtility_Test, ShaderPassMetadataCanDriveBuiltinPassMatching) { Material material; auto* shader = new Shader(); ShaderPass forwardPass = {}; forwardPass.name = "ForwardLit"; shader->AddPass(forwardPass); ResourceHandle shaderHandle(shader); material.SetShader(shaderHandle); EXPECT_TRUE(MatchesBuiltinPass(&material, BuiltinMaterialPass::ForwardLit)); EXPECT_FALSE(MatchesBuiltinPass(&material, BuiltinMaterialPass::Unlit)); } TEST(RenderMaterialUtility_Test, CanonicalBuiltinPassMetadataMatchesFinalColorAndPostProcessPasses) { ShaderPass finalColorPass = {}; finalColorPass.name = "FinalColor"; EXPECT_TRUE(ShaderPassMatchesBuiltinPass(finalColorPass, BuiltinMaterialPass::FinalColor)); ShaderPass postProcessPass = {}; postProcessPass.name = "ColorScale"; EXPECT_TRUE(ShaderPassMatchesBuiltinPass(postProcessPass, BuiltinMaterialPass::PostProcess)); } TEST(RenderMaterialUtility_Test, LegacyBuiltinPassAliasesDoNotMatchCanonicalBuiltinPasses) { ShaderPass forwardAliasByName = {}; forwardAliasByName.name = "ForwardBase"; EXPECT_FALSE(ShaderPassMatchesBuiltinPass(forwardAliasByName, BuiltinMaterialPass::ForwardLit)); ShaderPassTagEntry forwardAliasTag = {}; forwardAliasTag.name = "LightMode"; forwardAliasTag.value = "ForwardBase"; ShaderPass forwardAliasByTag = {}; forwardAliasByTag.name = "Default"; forwardAliasByTag.tags.PushBack(forwardAliasTag); EXPECT_FALSE(ShaderPassMatchesBuiltinPass(forwardAliasByTag, BuiltinMaterialPass::ForwardLit)); ShaderPassTagEntry finalColorAliasTag = {}; finalColorAliasTag.name = "LightMode"; finalColorAliasTag.value = "FinalOutput"; ShaderPass finalColorAliasPass = {}; finalColorAliasPass.name = "Default"; finalColorAliasPass.tags.PushBack(finalColorAliasTag); EXPECT_FALSE(ShaderPassMatchesBuiltinPass(finalColorAliasPass, BuiltinMaterialPass::FinalColor)); ShaderPassTagEntry postProcessAliasTag = {}; postProcessAliasTag.name = "LightMode"; postProcessAliasTag.value = "PostProcess"; ShaderPass postProcessAliasPass = {}; postProcessAliasPass.name = "Default"; postProcessAliasPass.tags.PushBack(postProcessAliasTag); EXPECT_FALSE(ShaderPassMatchesBuiltinPass(postProcessAliasPass, BuiltinMaterialPass::PostProcess)); } TEST(RenderMaterialUtility_Test, ExplicitShaderPassMetadataDisablesImplicitForwardFallback) { Material material; auto* shader = new Shader(); ShaderPass unlitPass = {}; unlitPass.name = "Unlit"; shader->AddPass(unlitPass); ResourceHandle shaderHandle(shader); material.SetShader(shaderHandle); EXPECT_FALSE(MatchesBuiltinPass(&material, BuiltinMaterialPass::ForwardLit)); EXPECT_TRUE(MatchesBuiltinPass(&material, BuiltinMaterialPass::Unlit)); } TEST(RenderMaterialUtility_Test, ShaderMetadataOverridesConflictingMaterialTags) { Material material; auto* shader = new Shader(); ShaderPass forwardPass = {}; forwardPass.name = "ForwardLit"; shader->AddPass(forwardPass); material.SetShader(ResourceHandle(shader)); material.SetTag("LightMode", "DepthOnly"); EXPECT_TRUE(MatchesBuiltinPass(&material, BuiltinMaterialPass::ForwardLit)); EXPECT_FALSE(MatchesBuiltinPass(&material, BuiltinMaterialPass::ShadowCaster)); EXPECT_FALSE(MatchesBuiltinPass(&material, BuiltinMaterialPass::DepthOnly)); } TEST(RenderMaterialUtility_Test, MaterialTagsDoNotOverrideMissingShaderMetadata) { Material material; auto* shader = new Shader(); ShaderPass defaultPass = {}; defaultPass.name = "Default"; shader->AddPass(defaultPass); material.SetShader(ResourceHandle(shader)); material.SetTag("LightMode", "ShadowCaster"); EXPECT_FALSE(MatchesBuiltinPass(&material, BuiltinMaterialPass::ForwardLit)); EXPECT_FALSE(MatchesBuiltinPass(&material, BuiltinMaterialPass::ShadowCaster)); } TEST(RenderMaterialUtility_Test, ShadersWithoutBuiltinMetadataDoNotMatchBuiltinSurfacePasses) { Material material; auto* shader = new Shader(); ShaderPass defaultPass = {}; defaultPass.name = "Default"; shader->AddPass(defaultPass); material.SetShader(ResourceHandle(shader)); EXPECT_FALSE(MatchesBuiltinPass(&material, BuiltinMaterialPass::ForwardLit)); EXPECT_FALSE(MatchesBuiltinPass(&material, BuiltinMaterialPass::Unlit)); } TEST(RenderMaterialUtility_Test, MaterialsWithoutFormalShaderMetadataResolveBuiltinDefaultsOnly) { Material material; material.SetFloat4("baseColor", Vector4(0.2f, 0.4f, 0.6f, 0.8f)); material.SetFloat("_Cutoff", 0.3f); material.SetFloat("opacity", 0.35f); Texture* baseColorTexture = new Texture(); IResource::ConstructParams textureParams = {}; textureParams.name = "AliasBaseColor"; textureParams.path = "Textures/alias_base_color.texture"; textureParams.guid = ResourceGUID::Generate(textureParams.path); baseColorTexture->Initialize(textureParams); material.SetTexture("_BaseColorTexture", ResourceHandle(baseColorTexture)); EXPECT_EQ(ResolveBuiltinBaseColorFactor(&material), Vector4(1.0f, 1.0f, 1.0f, 1.0f)); EXPECT_FLOAT_EQ(ResolveBuiltinAlphaCutoff(&material), 0.5f); EXPECT_EQ(ResolveBuiltinBaseColorTexture(&material), nullptr); EXPECT_FALSE(ResolveSchemaMaterialConstantPayload(&material).IsValid()); } TEST(RenderMaterialUtility_Test, ResolvesBuiltinForwardMaterialContractFromShaderSemanticMetadata) { auto* shader = new Shader(); ShaderPropertyDesc colorProperty = {}; colorProperty.name = "TintColor"; colorProperty.displayName = "Tint"; colorProperty.type = ShaderPropertyType::Color; colorProperty.semantic = "BaseColor"; shader->AddProperty(colorProperty); ShaderPropertyDesc textureProperty = {}; textureProperty.name = "AlbedoMap"; textureProperty.displayName = "Albedo"; textureProperty.type = ShaderPropertyType::Texture2D; textureProperty.semantic = "BaseColorTexture"; shader->AddProperty(textureProperty); ShaderPropertyDesc cutoffProperty = {}; cutoffProperty.name = "AlphaClipThreshold"; cutoffProperty.displayName = "Alpha Clip"; cutoffProperty.type = ShaderPropertyType::Range; cutoffProperty.defaultValue = "0.5"; cutoffProperty.semantic = "AlphaCutoff"; shader->AddProperty(cutoffProperty); Material material; material.SetShader(ResourceHandle(shader)); material.SetFloat4("TintColor", Vector4(0.3f, 0.5f, 0.7f, 0.9f)); material.SetFloat("AlphaClipThreshold", 0.61f); Texture* texture = new Texture(); IResource::ConstructParams textureParams = {}; textureParams.name = "SemanticTexture"; textureParams.path = "Textures/semantic_base_color.texture"; textureParams.guid = ResourceGUID::Generate(textureParams.path); texture->Initialize(textureParams); material.SetTexture("AlbedoMap", ResourceHandle(texture)); EXPECT_EQ(ResolveBuiltinBaseColorFactor(&material), Vector4(0.3f, 0.5f, 0.7f, 0.9f)); EXPECT_EQ(ResolveBuiltinBaseColorTexture(&material), texture); EXPECT_FLOAT_EQ(ResolveBuiltinAlphaCutoff(&material), 0.61f); } TEST(RenderMaterialUtility_Test, ResolvesBuiltinForwardMaterialContractFromShaderSemanticDefaults) { auto* shader = new Shader(); ShaderPropertyDesc colorProperty = {}; colorProperty.name = "TintColor"; colorProperty.displayName = "Tint"; colorProperty.type = ShaderPropertyType::Color; colorProperty.defaultValue = "(0.11,0.22,0.33,0.44)"; colorProperty.semantic = "BaseColor"; shader->AddProperty(colorProperty); ShaderPropertyDesc cutoffProperty = {}; cutoffProperty.name = "_Cutoff"; cutoffProperty.type = ShaderPropertyType::Range; cutoffProperty.defaultValue = "0.37"; cutoffProperty.semantic = "AlphaCutoff"; shader->AddProperty(cutoffProperty); Material material; material.SetShader(ResourceHandle(shader)); EXPECT_EQ(ResolveBuiltinBaseColorFactor(&material), Vector4(0.11f, 0.22f, 0.33f, 0.44f)); EXPECT_FLOAT_EQ(ResolveBuiltinAlphaCutoff(&material), 0.37f); EXPECT_EQ(ResolveBuiltinBaseColorTexture(&material), nullptr); } TEST(RenderMaterialUtility_Test, ResolvesBuiltinSkyboxMaterialContractFromShaderSemanticMetadata) { auto* shader = new Shader(); ShaderPropertyDesc tintProperty = {}; tintProperty.name = "SkyTintColor"; tintProperty.type = ShaderPropertyType::Color; tintProperty.semantic = "Tint"; shader->AddProperty(tintProperty); ShaderPropertyDesc exposureProperty = {}; exposureProperty.name = "ExposureControl"; exposureProperty.type = ShaderPropertyType::Float; exposureProperty.semantic = "Exposure"; shader->AddProperty(exposureProperty); ShaderPropertyDesc rotationProperty = {}; rotationProperty.name = "SkyYawDegrees"; rotationProperty.type = ShaderPropertyType::Float; rotationProperty.semantic = "Rotation"; shader->AddProperty(rotationProperty); ShaderPropertyDesc panoramicProperty = {}; panoramicProperty.name = "SkyPanorama"; panoramicProperty.type = ShaderPropertyType::Texture2D; panoramicProperty.semantic = "SkyboxPanoramicTexture"; shader->AddProperty(panoramicProperty); Material panoramicMaterial; panoramicMaterial.SetShader(ResourceHandle(shader)); panoramicMaterial.SetFloat4("SkyTintColor", Vector4(0.2f, 0.3f, 0.4f, 1.0f)); panoramicMaterial.SetFloat("ExposureControl", 1.35f); panoramicMaterial.SetFloat("SkyYawDegrees", 27.0f); Texture* panoramicTexture = CreateTestTexture("Textures/sky_panorama.texture", TextureType::Texture2D); panoramicMaterial.SetTexture("SkyPanorama", ResourceHandle(panoramicTexture)); EXPECT_EQ(ResolveSkyboxTint(&panoramicMaterial), Vector4(0.2f, 0.3f, 0.4f, 1.0f)); EXPECT_FLOAT_EQ(ResolveSkyboxExposure(&panoramicMaterial), 1.35f); EXPECT_FLOAT_EQ(ResolveSkyboxRotationDegrees(&panoramicMaterial), 27.0f); EXPECT_EQ(ResolveSkyboxPanoramicTexture(&panoramicMaterial), panoramicTexture); EXPECT_EQ(ResolveSkyboxCubemapTexture(&panoramicMaterial), nullptr); EXPECT_EQ(ResolveSkyboxTextureMode(&panoramicMaterial), BuiltinSkyboxTextureMode::Panoramic); auto* cubemapShader = new Shader(); ShaderPropertyDesc cubemapProperty = {}; cubemapProperty.name = "EnvironmentCube"; cubemapProperty.type = ShaderPropertyType::TextureCube; cubemapProperty.semantic = "SkyboxTexture"; cubemapShader->AddProperty(cubemapProperty); Material cubemapMaterial; cubemapMaterial.SetShader(ResourceHandle(cubemapShader)); Texture* cubemapTexture = CreateTestTexture("Textures/sky_cube.texture", TextureType::TextureCube); cubemapMaterial.SetTexture("EnvironmentCube", ResourceHandle(cubemapTexture)); EXPECT_EQ(ResolveSkyboxPanoramicTexture(&cubemapMaterial), nullptr); EXPECT_EQ(ResolveSkyboxCubemapTexture(&cubemapMaterial), cubemapTexture); EXPECT_EQ(ResolveSkyboxTextureMode(&cubemapMaterial), BuiltinSkyboxTextureMode::Cubemap); } TEST(RenderMaterialUtility_Test, SkyboxMaterialDoesNotUseLegacyPropertyNameFallbacksWithoutSemanticMetadata) { Material material; material.SetFloat4("_Tint", Vector4(0.8f, 0.7f, 0.6f, 1.0f)); material.SetFloat("_Exposure", 2.0f); material.SetFloat("_Rotation", 45.0f); Texture* panoramicTexture = CreateTestTexture("Textures/legacy_panorama.texture", TextureType::Texture2D); Texture* cubemapTexture = CreateTestTexture("Textures/legacy_cube.texture", TextureType::TextureCube); material.SetTexture("_MainTex", ResourceHandle(panoramicTexture)); material.SetTexture("_Tex", ResourceHandle(cubemapTexture)); EXPECT_EQ(ResolveSkyboxTint(&material), Vector4::One()); EXPECT_FLOAT_EQ(ResolveSkyboxExposure(&material), 1.0f); EXPECT_FLOAT_EQ(ResolveSkyboxRotationDegrees(&material), 0.0f); EXPECT_EQ(ResolveSkyboxPanoramicTexture(&material), nullptr); EXPECT_EQ(ResolveSkyboxCubemapTexture(&material), nullptr); EXPECT_EQ(ResolveSkyboxTextureMode(&material), BuiltinSkyboxTextureMode::None); } TEST(RenderMaterialUtility_Test, ExposesSchemaDrivenMaterialConstantPayload) { auto* shader = new Shader(); ShaderPropertyDesc colorProperty = {}; colorProperty.name = "_BaseColor"; colorProperty.type = ShaderPropertyType::Color; colorProperty.defaultValue = "(0.25,0.5,0.75,1.0)"; colorProperty.semantic = "BaseColor"; shader->AddProperty(colorProperty); ShaderPropertyDesc cutoffProperty = {}; cutoffProperty.name = "_Cutoff"; cutoffProperty.type = ShaderPropertyType::Range; cutoffProperty.defaultValue = "0.6"; cutoffProperty.semantic = "AlphaCutoff"; shader->AddProperty(cutoffProperty); Material material; material.SetShader(ResourceHandle(shader)); const MaterialConstantPayloadView payload = ResolveSchemaMaterialConstantPayload(&material); ASSERT_TRUE(payload.IsValid()); ASSERT_EQ(payload.size, 32u); ASSERT_TRUE(payload.layout.IsValid()); ASSERT_EQ(payload.layout.count, 2u); EXPECT_EQ(payload.layout.size, 32u); EXPECT_EQ(payload.layout.fields[0].name, "_BaseColor"); EXPECT_EQ(payload.layout.fields[0].offset, 0u); EXPECT_EQ(payload.layout.fields[0].size, 16u); EXPECT_EQ(payload.layout.fields[0].alignedSize, 16u); EXPECT_EQ(payload.layout.fields[1].name, "_Cutoff"); EXPECT_EQ(payload.layout.fields[1].offset, 16u); EXPECT_EQ(payload.layout.fields[1].size, 4u); EXPECT_EQ(payload.layout.fields[1].alignedSize, 16u); const float* values = static_cast(payload.data); EXPECT_FLOAT_EQ(values[0], 0.25f); EXPECT_FLOAT_EQ(values[1], 0.5f); EXPECT_FLOAT_EQ(values[2], 0.75f); EXPECT_FLOAT_EQ(values[3], 1.0f); const float* cutoffValues = static_cast(payload.data) + 4; 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); EXPECT_EQ(ResolveBuiltinBaseColorFactor(&material), Vector4(1.0f, 1.0f, 1.0f, 1.0f)); material.SetFloat4("baseColor", Vector4(0.9f, 0.8f, 0.7f, 0.6f)); EXPECT_EQ(ResolveBuiltinBaseColorFactor(&material), Vector4(1.0f, 1.0f, 1.0f, 1.0f)); } TEST(RenderMaterialUtility_Test, ResolvesRuntimeStructuredBufferIntoBufferViewMetadata) { Material material; MockRenderBuffer buffer(1024u, 32u); MaterialBufferBindingViewDesc viewDesc = {}; viewDesc.firstElement = 2u; viewDesc.elementCount = 6u; material.SetBuffer("VolumeNodes", &buffer, viewDesc); BuiltinPassResourceBindingDesc binding = {}; binding.name = "VolumeNodes"; binding.semantic = BuiltinPassResourceSemantic::MaterialBuffer; binding.resourceType = ShaderResourceType::StructuredBuffer; binding.location = { 2u, 1u }; MaterialBufferResourceView resolvedView = {}; ASSERT_TRUE(TryResolveMaterialBufferResourceView(&material, binding, resolvedView)); EXPECT_EQ(resolvedView.buffer, &buffer); EXPECT_EQ(resolvedView.viewType, XCEngine::RHI::ResourceViewType::ShaderResource); EXPECT_EQ(resolvedView.viewDesc.dimension, XCEngine::RHI::ResourceViewDimension::StructuredBuffer); EXPECT_EQ(resolvedView.viewDesc.firstElement, 2u); EXPECT_EQ(resolvedView.viewDesc.elementCount, 6u); EXPECT_EQ(resolvedView.viewDesc.structureByteStride, 32u); } TEST(RenderMaterialUtility_Test, MapsMaterialRenderStateToRhiDescriptors) { Material material; MaterialRenderState renderState; renderState.cullMode = MaterialCullMode::Back; renderState.blendEnable = true; renderState.srcBlend = MaterialBlendFactor::SrcAlpha; renderState.dstBlend = MaterialBlendFactor::InvSrcAlpha; renderState.srcBlendAlpha = MaterialBlendFactor::One; renderState.dstBlendAlpha = MaterialBlendFactor::InvSrcAlpha; renderState.blendOp = MaterialBlendOp::Add; renderState.blendOpAlpha = MaterialBlendOp::Subtract; renderState.colorWriteMask = 0x7; renderState.depthTestEnable = true; renderState.depthWriteEnable = false; renderState.depthFunc = MaterialComparisonFunc::LessEqual; renderState.depthBiasFactor = 1.25f; renderState.depthBiasUnits = 4; renderState.stencil.enabled = true; renderState.stencil.reference = 9; renderState.stencil.readMask = 0x3F; renderState.stencil.writeMask = 0x1F; renderState.stencil.front.func = MaterialComparisonFunc::Equal; renderState.stencil.front.failOp = MaterialStencilOp::Replace; renderState.stencil.front.passOp = MaterialStencilOp::IncrWrap; renderState.stencil.front.depthFailOp = MaterialStencilOp::DecrSat; renderState.stencil.back.func = MaterialComparisonFunc::NotEqual; renderState.stencil.back.failOp = MaterialStencilOp::Invert; renderState.stencil.back.passOp = MaterialStencilOp::DecrWrap; renderState.stencil.back.depthFailOp = MaterialStencilOp::Zero; material.SetRenderState(renderState); const MaterialRenderState effectiveRenderState = ResolveEffectiveRenderState(nullptr, &material); const XCEngine::RHI::RasterizerDesc rasterizerState = BuildRasterizerState(effectiveRenderState); const XCEngine::RHI::BlendDesc blendState = BuildBlendState(effectiveRenderState); const XCEngine::RHI::DepthStencilStateDesc depthStencilState = BuildDepthStencilState(effectiveRenderState); EXPECT_EQ(rasterizerState.cullMode, static_cast(XCEngine::RHI::CullMode::Back)); EXPECT_EQ(rasterizerState.frontFace, static_cast(XCEngine::RHI::FrontFace::CounterClockwise)); EXPECT_FLOAT_EQ(rasterizerState.slopeScaledDepthBias, 1.25f); EXPECT_EQ(rasterizerState.depthBias, 4); EXPECT_TRUE(blendState.blendEnable); EXPECT_EQ(blendState.srcBlend, static_cast(XCEngine::RHI::BlendFactor::SrcAlpha)); EXPECT_EQ(blendState.dstBlend, static_cast(XCEngine::RHI::BlendFactor::InvSrcAlpha)); EXPECT_EQ(blendState.srcBlendAlpha, static_cast(XCEngine::RHI::BlendFactor::One)); EXPECT_EQ(blendState.dstBlendAlpha, static_cast(XCEngine::RHI::BlendFactor::InvSrcAlpha)); EXPECT_EQ(blendState.blendOp, static_cast(XCEngine::RHI::BlendOp::Add)); EXPECT_EQ(blendState.blendOpAlpha, static_cast(XCEngine::RHI::BlendOp::Subtract)); EXPECT_EQ(blendState.colorWriteMask, 0x7); EXPECT_TRUE(depthStencilState.depthTestEnable); EXPECT_FALSE(depthStencilState.depthWriteEnable); EXPECT_EQ(depthStencilState.depthFunc, static_cast(XCEngine::RHI::ComparisonFunc::LessEqual)); EXPECT_TRUE(depthStencilState.stencilEnable); EXPECT_EQ(depthStencilState.stencilReadMask, 0x3F); EXPECT_EQ(depthStencilState.stencilWriteMask, 0x1F); EXPECT_EQ(depthStencilState.front.func, static_cast(XCEngine::RHI::ComparisonFunc::Equal)); EXPECT_EQ(depthStencilState.front.failOp, static_cast(XCEngine::RHI::StencilOp::Replace)); EXPECT_EQ(depthStencilState.front.passOp, static_cast(XCEngine::RHI::StencilOp::Incr)); EXPECT_EQ(depthStencilState.front.depthFailOp, static_cast(XCEngine::RHI::StencilOp::DecrSat)); EXPECT_EQ(depthStencilState.back.func, static_cast(XCEngine::RHI::ComparisonFunc::NotEqual)); EXPECT_EQ(depthStencilState.back.failOp, static_cast(XCEngine::RHI::StencilOp::Invert)); EXPECT_EQ(depthStencilState.back.passOp, static_cast(XCEngine::RHI::StencilOp::Decr)); EXPECT_EQ(depthStencilState.back.depthFailOp, static_cast(XCEngine::RHI::StencilOp::Zero)); } TEST(RenderMaterialUtility_Test, PipelineStateKeyStripsDynamicStencilReference) { MaterialRenderState renderState = {}; renderState.stencil.enabled = true; renderState.stencil.reference = 12; renderState.stencil.front.func = MaterialComparisonFunc::Always; renderState.stencil.back.func = MaterialComparisonFunc::Always; const MaterialRenderState keyState = BuildStaticPipelineRenderStateKey(renderState); EXPECT_TRUE(keyState.stencil.enabled); EXPECT_EQ(keyState.stencil.reference, 0u); EXPECT_EQ(keyState.stencil.front.func, MaterialComparisonFunc::Always); EXPECT_EQ(keyState.stencil.back.func, MaterialComparisonFunc::Always); } TEST(RenderMaterialUtility_Test, ShaderPassFixedFunctionStateIsUsedBeforeLegacyMaterialOverride) { ShaderPass shaderPass = {}; shaderPass.name = "ForwardLit"; shaderPass.hasFixedFunctionState = true; shaderPass.fixedFunctionState.cullMode = MaterialCullMode::Back; shaderPass.fixedFunctionState.blendEnable = true; shaderPass.fixedFunctionState.srcBlend = MaterialBlendFactor::SrcAlpha; shaderPass.fixedFunctionState.dstBlend = MaterialBlendFactor::InvSrcAlpha; shaderPass.fixedFunctionState.srcBlendAlpha = MaterialBlendFactor::SrcAlpha; shaderPass.fixedFunctionState.dstBlendAlpha = MaterialBlendFactor::InvSrcAlpha; shaderPass.fixedFunctionState.depthWriteEnable = false; shaderPass.fixedFunctionState.depthFunc = MaterialComparisonFunc::LessEqual; Material material; MaterialRenderState effectiveState = ResolveEffectiveRenderState(&shaderPass, &material); EXPECT_EQ(effectiveState.cullMode, MaterialCullMode::Back); EXPECT_TRUE(effectiveState.blendEnable); EXPECT_FALSE(effectiveState.depthWriteEnable); EXPECT_EQ(effectiveState.depthFunc, MaterialComparisonFunc::LessEqual); MaterialRenderState legacyOverride = {}; legacyOverride.cullMode = MaterialCullMode::Front; legacyOverride.depthWriteEnable = true; material.SetRenderState(legacyOverride); effectiveState = ResolveEffectiveRenderState(&shaderPass, &material); EXPECT_EQ(effectiveState.cullMode, MaterialCullMode::Front); EXPECT_FALSE(effectiveState.blendEnable); EXPECT_TRUE(effectiveState.depthWriteEnable); EXPECT_EQ(effectiveState.depthFunc, MaterialComparisonFunc::Less); } TEST(RenderMaterialUtility_Test, ShaderPassQueueTagResolvesAuthoringRenderQueue) { ShaderPass shaderPass = {}; shaderPass.tags.PushBack({ "Queue", "Transparent" }); int32 renderQueue = 0; EXPECT_TRUE(TryResolveShaderPassRenderQueue(shaderPass, renderQueue)); EXPECT_EQ(renderQueue, static_cast(MaterialRenderQueue::Transparent)); } } // namespace