diff --git a/engine/include/XCEngine/Components/CameraComponent.h b/engine/include/XCEngine/Components/CameraComponent.h index c0081715..7aee6df3 100644 --- a/engine/include/XCEngine/Components/CameraComponent.h +++ b/engine/include/XCEngine/Components/CameraComponent.h @@ -9,6 +9,7 @@ #include #include +#include namespace XCEngine { namespace Components { @@ -90,11 +91,16 @@ public: const Math::Color& GetSkyboxBottomColor() const { return m_skyboxBottomColor; } void SetSkyboxBottomColor(const Math::Color& value) { m_skyboxBottomColor = value; } - bool IsColorScalePostProcessEnabled() const { return m_colorScalePostProcessEnabled; } - void SetColorScalePostProcessEnabled(bool value) { m_colorScalePostProcessEnabled = value; } + bool IsColorScalePostProcessEnabled() const; + void SetColorScalePostProcessEnabled(bool value); - const Math::Vector4& GetColorScalePostProcessScale() const { return m_colorScalePostProcessScale; } - void SetColorScalePostProcessScale(const Math::Vector4& value) { m_colorScalePostProcessScale = value; } + const Math::Vector4& GetColorScalePostProcessScale() const; + void SetColorScalePostProcessScale(const Math::Vector4& value); + + const std::vector& GetColorScalePostProcessPasses() const { return m_colorScalePostProcessPasses; } + void SetColorScalePostProcessPasses(const std::vector& values); + void AddColorScalePostProcessPass(const Math::Vector4& value); + void ClearColorScalePostProcessPasses() { m_colorScalePostProcessPasses.clear(); } void Serialize(std::ostream& os) const override; void Deserialize(std::istream& is) override; @@ -119,8 +125,8 @@ private: Math::Color m_skyboxTopColor = Math::Color(0.18f, 0.36f, 0.74f, 1.0f); Math::Color m_skyboxHorizonColor = Math::Color(0.78f, 0.84f, 0.92f, 1.0f); Math::Color m_skyboxBottomColor = Math::Color(0.92f, 0.93f, 0.95f, 1.0f); - bool m_colorScalePostProcessEnabled = false; - Math::Vector4 m_colorScalePostProcessScale = Math::Vector4::One(); + Math::Vector4 m_colorScalePostProcessDefaultScale = Math::Vector4::One(); + std::vector m_colorScalePostProcessPasses; }; } // namespace Components diff --git a/engine/src/Components/CameraComponent.cpp b/engine/src/Components/CameraComponent.cpp index 59798e22..bc9d0a4f 100644 --- a/engine/src/Components/CameraComponent.cpp +++ b/engine/src/Components/CameraComponent.cpp @@ -42,8 +42,65 @@ bool TryDecodeAssetRef(const std::string& value, Resources::AssetRef& outRef) { return outRef.IsValid(); } +std::string EncodeVector4(const Math::Vector4& value) { + std::ostringstream stream; + stream << value.x << "," << value.y << "," << value.z << "," << value.w; + return stream.str(); +} + +bool TryParseVector4(const std::string& value, Math::Vector4& outValue) { + std::string normalized = value; + std::replace(normalized.begin(), normalized.end(), ',', ' '); + std::istringstream stream(normalized); + stream >> outValue.x >> outValue.y >> outValue.z >> outValue.w; + return !stream.fail(); +} + } // namespace +bool CameraComponent::IsColorScalePostProcessEnabled() const { + return !m_colorScalePostProcessPasses.empty(); +} + +void CameraComponent::SetColorScalePostProcessEnabled(bool value) { + if (value) { + if (m_colorScalePostProcessPasses.empty()) { + m_colorScalePostProcessPasses.push_back(m_colorScalePostProcessDefaultScale); + } + return; + } + + m_colorScalePostProcessPasses.clear(); +} + +const Math::Vector4& CameraComponent::GetColorScalePostProcessScale() const { + return m_colorScalePostProcessPasses.empty() + ? m_colorScalePostProcessDefaultScale + : m_colorScalePostProcessPasses.front(); +} + +void CameraComponent::SetColorScalePostProcessScale(const Math::Vector4& value) { + m_colorScalePostProcessDefaultScale = value; + if (!m_colorScalePostProcessPasses.empty()) { + m_colorScalePostProcessPasses.front() = value; + } +} + +void CameraComponent::SetColorScalePostProcessPasses(const std::vector& values) { + m_colorScalePostProcessPasses = values; + if (!m_colorScalePostProcessPasses.empty()) { + m_colorScalePostProcessDefaultScale = m_colorScalePostProcessPasses.front(); + } +} + +void CameraComponent::AddColorScalePostProcessPass(const Math::Vector4& value) { + if (m_colorScalePostProcessPasses.empty()) { + m_colorScalePostProcessDefaultScale = value; + } + + m_colorScalePostProcessPasses.push_back(value); +} + void CameraComponent::SetFieldOfView(float value) { m_fieldOfView = std::clamp(value, 1.0f, 179.0f); } @@ -149,22 +206,28 @@ void CameraComponent::Serialize(std::ostream& os) const { os << "skyboxTopColor=" << m_skyboxTopColor.r << "," << m_skyboxTopColor.g << "," << m_skyboxTopColor.b << "," << m_skyboxTopColor.a << ";"; os << "skyboxHorizonColor=" << m_skyboxHorizonColor.r << "," << m_skyboxHorizonColor.g << "," << m_skyboxHorizonColor.b << "," << m_skyboxHorizonColor.a << ";"; os << "skyboxBottomColor=" << m_skyboxBottomColor.r << "," << m_skyboxBottomColor.g << "," << m_skyboxBottomColor.b << "," << m_skyboxBottomColor.a << ";"; - os << "colorScalePostProcessEnabled=" << (m_colorScalePostProcessEnabled ? 1 : 0) << ";"; - os << "colorScalePostProcessScale=" - << m_colorScalePostProcessScale.x << "," - << m_colorScalePostProcessScale.y << "," - << m_colorScalePostProcessScale.z << "," - << m_colorScalePostProcessScale.w << ";"; + os << "colorScalePostProcessEnabled=" << (IsColorScalePostProcessEnabled() ? 1 : 0) << ";"; + os << "colorScalePostProcessScale=" << EncodeVector4(GetColorScalePostProcessScale()) << ";"; + os << "colorScalePostProcessPassCount=" << m_colorScalePostProcessPasses.size() << ";"; + for (size_t index = 0; index < m_colorScalePostProcessPasses.size(); ++index) { + os << "colorScalePostProcessPass" << index << "=" + << EncodeVector4(m_colorScalePostProcessPasses[index]) << ";"; + } } void CameraComponent::Deserialize(std::istream& is) { m_skyboxMaterial.Reset(); m_skyboxMaterialPath.clear(); m_skyboxMaterialRef.Reset(); + m_colorScalePostProcessDefaultScale = Math::Vector4::One(); + m_colorScalePostProcessPasses.clear(); std::string token; std::string pendingSkyboxMaterialPath; Resources::AssetRef pendingSkyboxMaterialRef; + bool legacyColorScaleEnabled = false; + size_t colorScalePassCount = 0; + std::vector deserializedColorScalePasses; while (std::getline(is, token, ';')) { if (token.empty()) { continue; @@ -227,14 +290,22 @@ void CameraComponent::Deserialize(std::istream& is) { std::istringstream ss(value); ss >> m_skyboxBottomColor.r >> m_skyboxBottomColor.g >> m_skyboxBottomColor.b >> m_skyboxBottomColor.a; } else if (key == "colorScalePostProcessEnabled") { - m_colorScalePostProcessEnabled = (std::stoi(value) != 0); + legacyColorScaleEnabled = (std::stoi(value) != 0); } else if (key == "colorScalePostProcessScale") { - std::replace(value.begin(), value.end(), ',', ' '); - std::istringstream ss(value); - ss >> m_colorScalePostProcessScale.x - >> m_colorScalePostProcessScale.y - >> m_colorScalePostProcessScale.z - >> m_colorScalePostProcessScale.w; + TryParseVector4(value, m_colorScalePostProcessDefaultScale); + } else if (key == "colorScalePostProcessPassCount") { + colorScalePassCount = static_cast(std::stoul(value)); + deserializedColorScalePasses.clear(); + deserializedColorScalePasses.resize(colorScalePassCount, Math::Vector4::One()); + } else if (key.rfind("colorScalePostProcessPass", 0) == 0) { + const std::string indexString = key.substr(std::string("colorScalePostProcessPass").size()); + if (!indexString.empty()) { + const size_t index = static_cast(std::stoul(indexString)); + if (index >= deserializedColorScalePasses.size()) { + deserializedColorScalePasses.resize(index + 1, Math::Vector4::One()); + } + TryParseVector4(value, deserializedColorScalePasses[index]); + } } } @@ -259,6 +330,12 @@ void CameraComponent::Deserialize(std::istream& is) { if (m_skyboxMaterial.Get() == nullptr && pendingSkyboxMaterialRef.IsValid()) { m_skyboxMaterialRef = pendingSkyboxMaterialRef; } + + if (!deserializedColorScalePasses.empty()) { + SetColorScalePostProcessPasses(deserializedColorScalePasses); + } else if (legacyColorScaleEnabled) { + SetColorScalePostProcessEnabled(true); + } } } // namespace Components diff --git a/engine/src/Rendering/Execution/SceneRenderer.cpp b/engine/src/Rendering/Execution/SceneRenderer.cpp index ee27acc2..c0fe48c2 100644 --- a/engine/src/Rendering/Execution/SceneRenderer.cpp +++ b/engine/src/Rendering/Execution/SceneRenderer.cpp @@ -99,12 +99,16 @@ void SceneRenderer::AttachCameraPostProcessRequests( for (size_t index = 0; index < requests.size(); ++index) { CameraRenderRequest& request = requests[index]; if (request.camera == nullptr || - !request.camera->IsColorScalePostProcessEnabled() || request.context.device == nullptr || !HasValidColorTarget(request.surface)) { continue; } + const std::vector& colorScalePasses = request.camera->GetColorScalePostProcessPasses(); + if (colorScalePasses.empty()) { + continue; + } + const std::vector& colorAttachments = request.surface.GetColorAttachments(); const RHI::Format colorFormat = colorAttachments[0]->GetFormat(); if (colorFormat == RHI::Format::Unknown) { @@ -128,8 +132,10 @@ void SceneRenderer::AttachCameraPostProcessRequests( } std::unique_ptr postProcessSequence = std::make_unique(); - postProcessSequence->AddPass(std::make_unique( - request.camera->GetColorScalePostProcessScale())); + for (const Math::Vector4& colorScale : colorScalePasses) { + postProcessSequence->AddPass( + std::make_unique(colorScale)); + } RenderSurface sourceSurface = sourceEntry->surface; sourceSurface.SetDepthAttachment(request.surface.GetDepthAttachment()); diff --git a/tests/Components/test_camera_light_component.cpp b/tests/Components/test_camera_light_component.cpp index 4ae8b5b0..2356d390 100644 --- a/tests/Components/test_camera_light_component.cpp +++ b/tests/Components/test_camera_light_component.cpp @@ -37,6 +37,7 @@ TEST(CameraComponent_Test, DefaultValues) { EXPECT_FLOAT_EQ(camera.GetColorScalePostProcessScale().y, 1.0f); EXPECT_FLOAT_EQ(camera.GetColorScalePostProcessScale().z, 1.0f); EXPECT_FLOAT_EQ(camera.GetColorScalePostProcessScale().w, 1.0f); + EXPECT_TRUE(camera.GetColorScalePostProcessPasses().empty()); } TEST(CameraComponent_Test, SetterClamping) { @@ -74,8 +75,10 @@ TEST(CameraComponent_Test, SerializeRoundTripPreservesViewportAndClearState) { source.SetSkyboxTopColor(XCEngine::Math::Color(0.12f, 0.21f, 0.64f, 1.0f)); source.SetSkyboxHorizonColor(XCEngine::Math::Color(0.71f, 0.76f, 0.88f, 1.0f)); source.SetSkyboxBottomColor(XCEngine::Math::Color(0.92f, 0.82f, 0.58f, 1.0f)); - source.SetColorScalePostProcessEnabled(true); - source.SetColorScalePostProcessScale(XCEngine::Math::Vector4(0.55f, 0.8f, 1.2f, 1.0f)); + source.SetColorScalePostProcessPasses({ + XCEngine::Math::Vector4(0.55f, 0.8f, 1.2f, 1.0f), + XCEngine::Math::Vector4(1.0f, 0.95f, 0.75f, 1.0f) + }); std::stringstream stream; source.Serialize(stream); @@ -100,6 +103,34 @@ TEST(CameraComponent_Test, SerializeRoundTripPreservesViewportAndClearState) { EXPECT_FLOAT_EQ(target.GetColorScalePostProcessScale().y, 0.8f); EXPECT_FLOAT_EQ(target.GetColorScalePostProcessScale().z, 1.2f); EXPECT_FLOAT_EQ(target.GetColorScalePostProcessScale().w, 1.0f); + ASSERT_EQ(target.GetColorScalePostProcessPasses().size(), 2u); + EXPECT_FLOAT_EQ(target.GetColorScalePostProcessPasses()[1].x, 1.0f); + EXPECT_FLOAT_EQ(target.GetColorScalePostProcessPasses()[1].y, 0.95f); + EXPECT_FLOAT_EQ(target.GetColorScalePostProcessPasses()[1].z, 0.75f); + EXPECT_FLOAT_EQ(target.GetColorScalePostProcessPasses()[1].w, 1.0f); +} + +TEST(CameraComponent_Test, LegacyColorScaleAccessorsDriveFirstPass) { + CameraComponent camera; + + camera.SetColorScalePostProcessScale(XCEngine::Math::Vector4(0.55f, 0.8f, 1.2f, 1.0f)); + EXPECT_FALSE(camera.IsColorScalePostProcessEnabled()); + + camera.SetColorScalePostProcessEnabled(true); + ASSERT_EQ(camera.GetColorScalePostProcessPasses().size(), 1u); + EXPECT_FLOAT_EQ(camera.GetColorScalePostProcessPasses()[0].x, 0.55f); + EXPECT_FLOAT_EQ(camera.GetColorScalePostProcessPasses()[0].y, 0.8f); + EXPECT_FLOAT_EQ(camera.GetColorScalePostProcessPasses()[0].z, 1.2f); + EXPECT_FLOAT_EQ(camera.GetColorScalePostProcessPasses()[0].w, 1.0f); + + camera.AddColorScalePostProcessPass(XCEngine::Math::Vector4(1.0f, 0.95f, 0.75f, 1.0f)); + camera.SetColorScalePostProcessScale(XCEngine::Math::Vector4(0.6f, 0.7f, 1.1f, 1.0f)); + + ASSERT_EQ(camera.GetColorScalePostProcessPasses().size(), 2u); + EXPECT_FLOAT_EQ(camera.GetColorScalePostProcessPasses()[0].x, 0.6f); + EXPECT_FLOAT_EQ(camera.GetColorScalePostProcessPasses()[0].y, 0.7f); + EXPECT_FLOAT_EQ(camera.GetColorScalePostProcessPasses()[0].z, 1.1f); + EXPECT_FLOAT_EQ(camera.GetColorScalePostProcessPasses()[1].y, 0.95f); } TEST(LightComponent_Test, DefaultValues) { diff --git a/tests/Rendering/integration/camera_post_process_scene/main.cpp b/tests/Rendering/integration/camera_post_process_scene/main.cpp index 2801f65a..91bb0609 100644 --- a/tests/Rendering/integration/camera_post_process_scene/main.cpp +++ b/tests/Rendering/integration/camera_post_process_scene/main.cpp @@ -243,8 +243,10 @@ void CameraPostProcessSceneTest::BuildScene() { mCamera->SetNearClipPlane(0.1f); mCamera->SetFarClipPlane(20.0f); mCamera->SetClearColor(XCEngine::Math::Color(0.06f, 0.08f, 0.12f, 1.0f)); - mCamera->SetColorScalePostProcessEnabled(true); - mCamera->SetColorScalePostProcessScale(Vector4(0.55f, 0.7125f, 0.75f, 1.0f)); + mCamera->SetColorScalePostProcessPasses({ + Vector4(1.0f, 0.75f, 0.75f, 1.0f), + Vector4(0.55f, 0.95f, 1.0f, 1.0f) + }); GameObject* quadObject = mScene->CreateGameObject("Quad"); quadObject->GetTransform()->SetLocalPosition(Vector3(0.0f, 0.0f, 3.0f)); diff --git a/tests/Rendering/unit/test_camera_scene_renderer.cpp b/tests/Rendering/unit/test_camera_scene_renderer.cpp index a617a0a9..627dd8b1 100644 --- a/tests/Rendering/unit/test_camera_scene_renderer.cpp +++ b/tests/Rendering/unit/test_camera_scene_renderer.cpp @@ -1770,7 +1770,7 @@ TEST(SceneRenderer_Test, PreservesExistingSurfaceRenderAreaForFullViewportCamera EXPECT_EQ(renderArea.height, 240); } -TEST(SceneRenderer_Test, BuildsCameraColorScalePostProcessRequestFromCameraSettings) { +TEST(SceneRenderer_Test, BuildsCameraColorScalePostProcessRequestFromCameraPassStack) { Scene scene("SceneRendererCameraPostProcessScene"); GameObject* cameraObject = scene.CreateGameObject("Camera"); @@ -1778,8 +1778,10 @@ TEST(SceneRenderer_Test, BuildsCameraColorScalePostProcessRequestFromCameraSetti camera->SetPrimary(true); camera->SetDepth(2.0f); camera->SetViewportRect(XCEngine::Math::Rect(0.25f, 0.125f, 0.5f, 0.625f)); - camera->SetColorScalePostProcessEnabled(true); - camera->SetColorScalePostProcessScale(XCEngine::Math::Vector4(0.55f, 0.8f, 1.1f, 1.0f)); + camera->SetColorScalePostProcessPasses({ + XCEngine::Math::Vector4(1.0f, 0.75f, 0.75f, 1.0f), + XCEngine::Math::Vector4(0.55f, 0.95f, 1.1f, 1.0f) + }); auto allocationState = std::make_shared(); MockShadowDevice device(allocationState); @@ -1811,7 +1813,7 @@ TEST(SceneRenderer_Test, BuildsCameraColorScalePostProcessRequestFromCameraSetti EXPECT_TRUE(request.postProcess.IsRequested()); EXPECT_TRUE(request.postProcess.IsValid()); EXPECT_NE(request.postProcess.passes, nullptr); - ASSERT_EQ(request.postProcess.passes->GetPassCount(), 1u); + ASSERT_EQ(request.postProcess.passes->GetPassCount(), 2u); EXPECT_EQ(request.postProcess.destinationSurface.GetColorAttachments()[0], backBufferColorView); EXPECT_EQ(request.postProcess.destinationSurface.GetDepthAttachment(), depthView); EXPECT_EQ(request.postProcess.sourceSurface.GetDepthAttachment(), depthView);