feat: add camera viewport rect render areas

This commit is contained in:
2026-04-01 13:01:11 +08:00
parent 0fe02fd1b4
commit f80fb9860e
9 changed files with 219 additions and 12 deletions

View File

@@ -2,6 +2,7 @@
#include <XCEngine/Components/Component.h>
#include <XCEngine/Core/Math/Color.h>
#include <XCEngine/Core/Math/Rect.h>
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);
};

View File

@@ -1,6 +1,7 @@
#pragma once
#include <XCEngine/Core/Math/Color.h>
#include <XCEngine/Core/Math/Rect.h>
#include <XCEngine/RHI/RHIEnums.h>
#include <cstdint>
@@ -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<RHI::RHIResourceView*> 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;

View File

@@ -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<int>(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<int>(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<CameraClearMode>(std::stoi(value));
} else if (key == "cullingMask") {
m_cullingMask = static_cast<uint32_t>(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);

View File

@@ -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;
}

View File

@@ -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<float>(surface.GetWidth()),
static_cast<float>(surface.GetHeight()),
static_cast<float>(renderArea.x),
static_cast<float>(renderArea.y),
static_cast<float>(renderArea.width),
static_cast<float>(renderArea.height),
0.0f,
1.0f
};
const RHI::Rect scissorRect = {
0,
0,
static_cast<int32_t>(surface.GetWidth()),
static_cast<int32_t>(surface.GetHeight())
renderArea.x,
renderArea.y,
renderArea.x + renderArea.width,
renderArea.y + renderArea.height
};
commandList->SetViewport(viewport);
commandList->SetScissorRect(scissorRect);

View File

@@ -1,8 +1,39 @@
#include "Rendering/RenderSurface.h"
#include <algorithm>
#include <cstdint>
namespace XCEngine {
namespace Rendering {
namespace {
Math::RectInt ClampRenderArea(
const Math::RectInt& renderArea,
uint32_t surfaceWidth,
uint32_t surfaceHeight) {
const int32_t maxWidth = static_cast<int32_t>(surfaceWidth);
const int32_t maxHeight = static_cast<int32_t>(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<int64_t>(renderArea.x) +
static_cast<int64_t>(std::max(renderArea.width, 0));
const int64_t unclampedBottom = static_cast<int64_t>(renderArea.y) +
static_cast<int64_t>(std::max(renderArea.height, 0));
const int32_t right = std::clamp(
static_cast<int32_t>(std::min<int64_t>(unclampedRight, maxWidth)),
left,
maxWidth);
const int32_t bottom = std::clamp(
static_cast<int32_t>(std::min<int64_t>(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<RHI::RHIResourceView*>
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<int32_t>(m_width), static_cast<int32_t>(m_height));
}
return ClampRenderArea(m_renderArea, m_width, m_height);
}
uint32_t RenderSurface::GetRenderAreaWidth() const {
return static_cast<uint32_t>(std::max(GetRenderArea().width, 0));
}
uint32_t RenderSurface::GetRenderAreaHeight() const {
return static_cast<uint32_t>(std::max(GetRenderArea().height, 0));
}
void RenderSurface::SetClearColorOverride(const Math::Color& clearColor) {
m_hasClearColorOverride = true;
m_clearColorOverride = clearColor;

View File

@@ -5,6 +5,7 @@
#include "Scene/Scene.h"
#include <algorithm>
#include <cmath>
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<float>(surface.GetWidth());
const float surfaceHeight = static_cast<float>(surface.GetHeight());
const int32_t left = static_cast<int32_t>(std::floor(viewportRect.x * surfaceWidth));
const int32_t top = static_cast<int32_t>(std::floor(viewportRect.y * surfaceHeight));
const int32_t right = static_cast<int32_t>(std::ceil((viewportRect.x + viewportRect.width) * surfaceWidth));
const int32_t bottom = static_cast<int32_t>(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<CameraRenderRequest> 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;

View File

@@ -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) {

View File

@@ -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<CameraComponent>();
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<CameraRenderRequest> 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<CameraComponent>();
camera->SetPrimary(true);
camera->SetViewportRect(XCEngine::Math::Rect(0.125f, 0.25f, 0.5f, 0.5f));
auto state = std::make_shared<MockPipelineState>();
CameraRenderer renderer(std::make_unique<MockPipeline>(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");