From f80fb9860e14bb8c56dc1d6918456b3eec400e20 Mon Sep 17 00:00:00 2001 From: ssdfasd <2156608475@qq.com> Date: Wed, 1 Apr 2026 13:01:11 +0800 Subject: [PATCH] feat: add camera viewport rect render areas --- .../XCEngine/Components/CameraComponent.h | 5 ++ .../XCEngine/Rendering/RenderSurface.h | 10 +++ engine/src/Components/CameraComponent.cpp | 17 +++++ engine/src/Rendering/CameraRenderer.cpp | 9 ++- .../Pipelines/BuiltinForwardPipeline.cpp | 21 +++--- engine/src/Rendering/RenderSurface.cpp | 60 +++++++++++++++++ engine/src/Rendering/SceneRenderer.cpp | 21 +++++- .../test_camera_light_component.cpp | 21 +++++- .../unit/test_camera_scene_renderer.cpp | 67 +++++++++++++++++++ 9 files changed, 219 insertions(+), 12 deletions(-) diff --git a/engine/include/XCEngine/Components/CameraComponent.h b/engine/include/XCEngine/Components/CameraComponent.h index b60ee33b..09436a21 100644 --- a/engine/include/XCEngine/Components/CameraComponent.h +++ b/engine/include/XCEngine/Components/CameraComponent.h @@ -2,6 +2,7 @@ #include #include +#include namespace XCEngine { namespace Components { @@ -49,6 +50,9 @@ public: uint32_t GetCullingMask() const { return m_cullingMask; } void SetCullingMask(uint32_t value) { m_cullingMask = value; } + const Math::Rect& GetViewportRect() const { return m_viewportRect; } + void SetViewportRect(const Math::Rect& value); + const Math::Color& GetClearColor() const { return m_clearColor; } void SetClearColor(const Math::Color& value) { m_clearColor = value; } @@ -65,6 +69,7 @@ private: bool m_primary = true; CameraClearMode m_clearMode = CameraClearMode::Auto; uint32_t m_cullingMask = 0xFFFFFFFFu; + Math::Rect m_viewportRect = Math::Rect(0.0f, 0.0f, 1.0f, 1.0f); Math::Color m_clearColor = Math::Color(0.192f, 0.302f, 0.475f, 1.0f); }; diff --git a/engine/include/XCEngine/Rendering/RenderSurface.h b/engine/include/XCEngine/Rendering/RenderSurface.h index 36b61a39..4ef26ad0 100644 --- a/engine/include/XCEngine/Rendering/RenderSurface.h +++ b/engine/include/XCEngine/Rendering/RenderSurface.h @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include @@ -29,6 +30,13 @@ public: void SetDepthAttachment(RHI::RHIResourceView* depthAttachment) { m_depthAttachment = depthAttachment; } RHI::RHIResourceView* GetDepthAttachment() const { return m_depthAttachment; } + void SetRenderArea(const Math::RectInt& renderArea); + void ResetRenderArea(); + bool HasCustomRenderArea() const { return m_hasCustomRenderArea; } + Math::RectInt GetRenderArea() const; + uint32_t GetRenderAreaWidth() const; + uint32_t GetRenderAreaHeight() const; + void SetClearColorOverride(const Math::Color& clearColor); void ClearClearColorOverride(); bool HasClearColorOverride() const { return m_hasClearColorOverride; } @@ -48,6 +56,8 @@ private: uint32_t m_height = 0; std::vector m_colorAttachments; RHI::RHIResourceView* m_depthAttachment = nullptr; + bool m_hasCustomRenderArea = false; + Math::RectInt m_renderArea; bool m_hasClearColorOverride = false; Math::Color m_clearColorOverride = Math::Color::Black(); bool m_autoTransition = true; diff --git a/engine/src/Components/CameraComponent.cpp b/engine/src/Components/CameraComponent.cpp index abedcdc1..5ce886eb 100644 --- a/engine/src/Components/CameraComponent.cpp +++ b/engine/src/Components/CameraComponent.cpp @@ -25,6 +25,16 @@ void CameraComponent::SetFarClipPlane(float value) { m_farClipPlane = std::max(m_nearClipPlane + 0.001f, value); } +void CameraComponent::SetViewportRect(const Math::Rect& value) { + const float x = std::clamp(value.x, 0.0f, 1.0f); + const float y = std::clamp(value.y, 0.0f, 1.0f); + const float width = std::clamp(value.width, 0.0f, 1.0f); + const float height = std::clamp(value.height, 0.0f, 1.0f); + const float right = std::min(1.0f, x + width); + const float bottom = std::min(1.0f, y + height); + m_viewportRect = Math::Rect(x, y, right - x, bottom - y); +} + void CameraComponent::Serialize(std::ostream& os) const { os << "projection=" << static_cast(m_projectionType) << ";"; os << "fov=" << m_fieldOfView << ";"; @@ -35,6 +45,7 @@ void CameraComponent::Serialize(std::ostream& os) const { os << "primary=" << (m_primary ? 1 : 0) << ";"; os << "clearMode=" << static_cast(m_clearMode) << ";"; os << "cullingMask=" << m_cullingMask << ";"; + os << "viewportRect=" << m_viewportRect.x << "," << m_viewportRect.y << "," << m_viewportRect.width << "," << m_viewportRect.height << ";"; os << "clearColor=" << m_clearColor.r << "," << m_clearColor.g << "," << m_clearColor.b << "," << m_clearColor.a << ";"; } @@ -71,6 +82,12 @@ void CameraComponent::Deserialize(std::istream& is) { m_clearMode = static_cast(std::stoi(value)); } else if (key == "cullingMask") { m_cullingMask = static_cast(std::stoul(value)); + } else if (key == "viewportRect") { + std::replace(value.begin(), value.end(), ',', ' '); + std::istringstream ss(value); + Math::Rect viewportRect; + ss >> viewportRect.x >> viewportRect.y >> viewportRect.width >> viewportRect.height; + SetViewportRect(viewportRect); } else if (key == "clearColor") { std::replace(value.begin(), value.end(), ',', ' '); std::istringstream ss(value); diff --git a/engine/src/Rendering/CameraRenderer.cpp b/engine/src/Rendering/CameraRenderer.cpp index 0822ee73..d41d72eb 100644 --- a/engine/src/Rendering/CameraRenderer.cpp +++ b/engine/src/Rendering/CameraRenderer.cpp @@ -101,11 +101,16 @@ bool CameraRenderer::Render( return false; } + if (request.surface.GetRenderAreaWidth() == 0 || + request.surface.GetRenderAreaHeight() == 0) { + return false; + } + RenderSceneData sceneData = m_sceneExtractor.ExtractForCamera( *request.scene, *request.camera, - request.surface.GetWidth(), - request.surface.GetHeight()); + request.surface.GetRenderAreaWidth(), + request.surface.GetRenderAreaHeight()); if (!sceneData.HasCamera()) { return false; } diff --git a/engine/src/Rendering/Pipelines/BuiltinForwardPipeline.cpp b/engine/src/Rendering/Pipelines/BuiltinForwardPipeline.cpp index b56318d0..79ab67c4 100644 --- a/engine/src/Rendering/Pipelines/BuiltinForwardPipeline.cpp +++ b/engine/src/Rendering/Pipelines/BuiltinForwardPipeline.cpp @@ -324,19 +324,24 @@ bool BuiltinForwardPipeline::ExecuteForwardOpaquePass(const RenderPassContext& p renderTargets.data(), surface.GetDepthAttachment()); + const Math::RectInt renderArea = surface.GetRenderArea(); + if (renderArea.width <= 0 || renderArea.height <= 0) { + return false; + } + const RHI::Viewport viewport = { - 0.0f, - 0.0f, - static_cast(surface.GetWidth()), - static_cast(surface.GetHeight()), + static_cast(renderArea.x), + static_cast(renderArea.y), + static_cast(renderArea.width), + static_cast(renderArea.height), 0.0f, 1.0f }; const RHI::Rect scissorRect = { - 0, - 0, - static_cast(surface.GetWidth()), - static_cast(surface.GetHeight()) + renderArea.x, + renderArea.y, + renderArea.x + renderArea.width, + renderArea.y + renderArea.height }; commandList->SetViewport(viewport); commandList->SetScissorRect(scissorRect); diff --git a/engine/src/Rendering/RenderSurface.cpp b/engine/src/Rendering/RenderSurface.cpp index 5fe31ef2..befc7f09 100644 --- a/engine/src/Rendering/RenderSurface.cpp +++ b/engine/src/Rendering/RenderSurface.cpp @@ -1,8 +1,39 @@ #include "Rendering/RenderSurface.h" +#include +#include + namespace XCEngine { namespace Rendering { +namespace { + +Math::RectInt ClampRenderArea( + const Math::RectInt& renderArea, + uint32_t surfaceWidth, + uint32_t surfaceHeight) { + const int32_t maxWidth = static_cast(surfaceWidth); + const int32_t maxHeight = static_cast(surfaceHeight); + + const int32_t left = std::clamp(renderArea.x, 0, maxWidth); + const int32_t top = std::clamp(renderArea.y, 0, maxHeight); + const int64_t unclampedRight = static_cast(renderArea.x) + + static_cast(std::max(renderArea.width, 0)); + const int64_t unclampedBottom = static_cast(renderArea.y) + + static_cast(std::max(renderArea.height, 0)); + const int32_t right = std::clamp( + static_cast(std::min(unclampedRight, maxWidth)), + left, + maxWidth); + const int32_t bottom = std::clamp( + static_cast(std::min(unclampedBottom, maxHeight)), + top, + maxHeight); + return Math::RectInt(left, top, right - left, bottom - top); +} + +} // namespace + RenderSurface::RenderSurface(uint32_t width, uint32_t height) : m_width(width) , m_height(height) { @@ -11,6 +42,9 @@ RenderSurface::RenderSurface(uint32_t width, uint32_t height) void RenderSurface::SetSize(uint32_t width, uint32_t height) { m_width = width; m_height = height; + if (m_hasCustomRenderArea) { + m_renderArea = ClampRenderArea(m_renderArea, m_width, m_height); + } } void RenderSurface::SetColorAttachment(RHI::RHIResourceView* colorAttachment) { @@ -24,6 +58,32 @@ void RenderSurface::SetColorAttachments(const std::vector m_colorAttachments = colorAttachments; } +void RenderSurface::SetRenderArea(const Math::RectInt& renderArea) { + m_renderArea = ClampRenderArea(renderArea, m_width, m_height); + m_hasCustomRenderArea = true; +} + +void RenderSurface::ResetRenderArea() { + m_hasCustomRenderArea = false; + m_renderArea = {}; +} + +Math::RectInt RenderSurface::GetRenderArea() const { + if (!m_hasCustomRenderArea) { + return Math::RectInt(0, 0, static_cast(m_width), static_cast(m_height)); + } + + return ClampRenderArea(m_renderArea, m_width, m_height); +} + +uint32_t RenderSurface::GetRenderAreaWidth() const { + return static_cast(std::max(GetRenderArea().width, 0)); +} + +uint32_t RenderSurface::GetRenderAreaHeight() const { + return static_cast(std::max(GetRenderArea().height, 0)); +} + void RenderSurface::SetClearColorOverride(const Math::Color& clearColor) { m_hasClearColorOverride = true; m_clearColorOverride = clearColor; diff --git a/engine/src/Rendering/SceneRenderer.cpp b/engine/src/Rendering/SceneRenderer.cpp index c0aa403a..46327138 100644 --- a/engine/src/Rendering/SceneRenderer.cpp +++ b/engine/src/Rendering/SceneRenderer.cpp @@ -5,6 +5,7 @@ #include "Scene/Scene.h" #include +#include namespace XCEngine { namespace Rendering { @@ -40,6 +41,20 @@ RenderClearFlags ResolveClearFlags(const Components::CameraComponent& camera, si } } +Math::RectInt ResolveCameraRenderArea( + const Components::CameraComponent& camera, + const RenderSurface& surface) { + const Math::Rect viewportRect = camera.GetViewportRect(); + const float surfaceWidth = static_cast(surface.GetWidth()); + const float surfaceHeight = static_cast(surface.GetHeight()); + + const int32_t left = static_cast(std::floor(viewportRect.x * surfaceWidth)); + const int32_t top = static_cast(std::floor(viewportRect.y * surfaceHeight)); + const int32_t right = static_cast(std::ceil((viewportRect.x + viewportRect.width) * surfaceWidth)); + const int32_t bottom = static_cast(std::ceil((viewportRect.y + viewportRect.height) * surfaceHeight)); + return Math::RectInt(left, top, right - left, bottom - top); +} + } // namespace SceneRenderer::SceneRenderer() = default; @@ -101,9 +116,13 @@ std::vector SceneRenderer::BuildRenderRequests( request.camera = camera; request.context = context; request.surface = surface; + request.surface.SetRenderArea(ResolveCameraRenderArea(*camera, surface)); request.cameraDepth = camera->GetDepth(); request.clearFlags = ResolveClearFlags(*camera, cameraIndex); - requests.push_back(request); + if (request.surface.GetRenderAreaWidth() > 0 && + request.surface.GetRenderAreaHeight() > 0) { + requests.push_back(request); + } } return requests; diff --git a/tests/Components/test_camera_light_component.cpp b/tests/Components/test_camera_light_component.cpp index 2c0d7221..eeee6103 100644 --- a/tests/Components/test_camera_light_component.cpp +++ b/tests/Components/test_camera_light_component.cpp @@ -20,6 +20,10 @@ TEST(CameraComponent_Test, DefaultValues) { EXPECT_TRUE(camera.IsPrimary()); EXPECT_EQ(camera.GetClearMode(), CameraClearMode::Auto); EXPECT_EQ(camera.GetCullingMask(), 0xFFFFFFFFu); + EXPECT_FLOAT_EQ(camera.GetViewportRect().x, 0.0f); + EXPECT_FLOAT_EQ(camera.GetViewportRect().y, 0.0f); + EXPECT_FLOAT_EQ(camera.GetViewportRect().width, 1.0f); + EXPECT_FLOAT_EQ(camera.GetViewportRect().height, 1.0f); } TEST(CameraComponent_Test, SetterClamping) { @@ -36,10 +40,21 @@ TEST(CameraComponent_Test, SetterClamping) { EXPECT_GT(camera.GetFarClipPlane(), camera.GetNearClipPlane()); } -TEST(CameraComponent_Test, SerializeRoundTripPreservesClearMode) { +TEST(CameraComponent_Test, ViewportRectIsClampedToNormalizedSurfaceRange) { + CameraComponent camera; + camera.SetViewportRect(XCEngine::Math::Rect(-0.25f, 0.2f, 1.5f, 1.1f)); + + EXPECT_FLOAT_EQ(camera.GetViewportRect().x, 0.0f); + EXPECT_FLOAT_EQ(camera.GetViewportRect().y, 0.2f); + EXPECT_FLOAT_EQ(camera.GetViewportRect().width, 1.0f); + EXPECT_FLOAT_EQ(camera.GetViewportRect().height, 0.8f); +} + +TEST(CameraComponent_Test, SerializeRoundTripPreservesViewportAndClearState) { CameraComponent source; source.SetClearMode(CameraClearMode::DepthOnly); source.SetCullingMask(0x0000000Fu); + source.SetViewportRect(XCEngine::Math::Rect(0.25f, 0.125f, 0.5f, 0.625f)); std::stringstream stream; source.Serialize(stream); @@ -49,6 +64,10 @@ TEST(CameraComponent_Test, SerializeRoundTripPreservesClearMode) { EXPECT_EQ(target.GetClearMode(), CameraClearMode::DepthOnly); EXPECT_EQ(target.GetCullingMask(), 0x0000000Fu); + EXPECT_FLOAT_EQ(target.GetViewportRect().x, 0.25f); + EXPECT_FLOAT_EQ(target.GetViewportRect().y, 0.125f); + EXPECT_FLOAT_EQ(target.GetViewportRect().width, 0.5f); + EXPECT_FLOAT_EQ(target.GetViewportRect().height, 0.625f); } TEST(LightComponent_Test, DefaultValues) { diff --git a/tests/Rendering/unit/test_camera_scene_renderer.cpp b/tests/Rendering/unit/test_camera_scene_renderer.cpp index 29d5198f..a2a1e6d8 100644 --- a/tests/Rendering/unit/test_camera_scene_renderer.cpp +++ b/tests/Rendering/unit/test_camera_scene_renderer.cpp @@ -23,6 +23,12 @@ struct MockPipelineState { bool renderResult = true; uint32_t lastSurfaceWidth = 0; uint32_t lastSurfaceHeight = 0; + int32_t lastRenderAreaX = 0; + int32_t lastRenderAreaY = 0; + int32_t lastRenderAreaWidth = 0; + int32_t lastRenderAreaHeight = 0; + uint32_t lastCameraViewportWidth = 0; + uint32_t lastCameraViewportHeight = 0; CameraComponent* lastCamera = nullptr; size_t lastVisibleItemCount = 0; RenderClearFlags lastClearFlags = RenderClearFlags::All; @@ -59,7 +65,14 @@ public: ++m_state->renderCalls; m_state->lastSurfaceWidth = surface.GetWidth(); m_state->lastSurfaceHeight = surface.GetHeight(); + const XCEngine::Math::RectInt renderArea = surface.GetRenderArea(); + m_state->lastRenderAreaX = renderArea.x; + m_state->lastRenderAreaY = renderArea.y; + m_state->lastRenderAreaWidth = renderArea.width; + m_state->lastRenderAreaHeight = renderArea.height; m_state->lastCamera = sceneData.camera; + m_state->lastCameraViewportWidth = sceneData.cameraData.viewportWidth; + m_state->lastCameraViewportHeight = sceneData.cameraData.viewportHeight; m_state->lastVisibleItemCount = sceneData.visibleItems.size(); m_state->lastClearFlags = sceneData.cameraData.clearFlags; m_state->renderedCameras.push_back(sceneData.camera); @@ -163,6 +176,12 @@ TEST(CameraRenderer_Test, UsesOverrideCameraAndSurfaceSizeWhenSubmittingScene) { EXPECT_EQ(state->renderCalls, 1); EXPECT_EQ(state->lastSurfaceWidth, 640u); EXPECT_EQ(state->lastSurfaceHeight, 480u); + EXPECT_EQ(state->lastRenderAreaX, 0); + EXPECT_EQ(state->lastRenderAreaY, 0); + EXPECT_EQ(state->lastRenderAreaWidth, 640); + EXPECT_EQ(state->lastRenderAreaHeight, 480); + EXPECT_EQ(state->lastCameraViewportWidth, 640u); + EXPECT_EQ(state->lastCameraViewportHeight, 480u); EXPECT_EQ(state->lastCamera, overrideCamera); EXPECT_NE(state->lastCamera, primaryCamera); EXPECT_EQ(state->lastVisibleItemCount, 0u); @@ -330,6 +349,54 @@ TEST(SceneRenderer_Test, HonorsExplicitOverrideCameraClearMode) { EXPECT_EQ(requests[0].clearFlags, RenderClearFlags::Depth); } +TEST(SceneRenderer_Test, ResolvesNormalizedCameraViewportRectToPerRequestRenderArea) { + Scene scene("SceneRendererViewportRectScene"); + + GameObject* cameraObject = scene.CreateGameObject("Camera"); + auto* camera = cameraObject->AddComponent(); + camera->SetPrimary(true); + camera->SetDepth(2.0f); + camera->SetViewportRect(XCEngine::Math::Rect(0.25f, 0.1f, 0.5f, 0.4f)); + + SceneRenderer renderer; + const std::vector requests = + renderer.BuildRenderRequests(scene, nullptr, CreateValidContext(), RenderSurface(800, 600)); + + ASSERT_EQ(requests.size(), 1u); + const XCEngine::Math::RectInt renderArea = requests[0].surface.GetRenderArea(); + EXPECT_EQ(renderArea.x, 200); + EXPECT_EQ(renderArea.y, 60); + EXPECT_EQ(renderArea.width, 400); + EXPECT_EQ(renderArea.height, 240); +} + +TEST(CameraRenderer_Test, UsesResolvedRenderAreaForCameraViewportDimensions) { + Scene scene("CameraRendererViewportRectScene"); + + GameObject* cameraObject = scene.CreateGameObject("Camera"); + auto* camera = cameraObject->AddComponent(); + camera->SetPrimary(true); + camera->SetViewportRect(XCEngine::Math::Rect(0.125f, 0.25f, 0.5f, 0.5f)); + + auto state = std::make_shared(); + CameraRenderer renderer(std::make_unique(state)); + + CameraRenderRequest request; + request.scene = &scene; + request.camera = camera; + request.context = CreateValidContext(); + request.surface = RenderSurface(640, 480); + request.surface.SetRenderArea(XCEngine::Math::RectInt(80, 120, 320, 240)); + + ASSERT_TRUE(renderer.Render(request)); + EXPECT_EQ(state->lastRenderAreaX, 80); + EXPECT_EQ(state->lastRenderAreaY, 120); + EXPECT_EQ(state->lastRenderAreaWidth, 320); + EXPECT_EQ(state->lastRenderAreaHeight, 240); + EXPECT_EQ(state->lastCameraViewportWidth, 320u); + EXPECT_EQ(state->lastCameraViewportHeight, 240u); +} + TEST(SceneRenderer_Test, ForwardsPipelineLifetimeAndRenderCallsToCameraRenderer) { Scene scene("SceneRendererScene");