diff --git a/brayns/engine/camera/Camera.cpp b/brayns/engine/camera/Camera.cpp index e0c1e9624..b8ebf1b43 100644 --- a/brayns/engine/camera/Camera.cpp +++ b/brayns/engine/camera/Camera.cpp @@ -72,6 +72,26 @@ class NearClipIntegrity } }; +class ImageRegionIntegrity +{ +public: + static void check(const brayns::Box2 ®ion) + { + auto checkBetween = [](auto value, auto min, auto max) + { + if (value < min || value > max) + { + throw std::invalid_argument("Image region should be normalized and not empty"); + } + }; + + checkBetween(region.upper.x, 0.0F, 1.0F); + checkBetween(region.upper.y, 0.0F, 1.0F); + checkBetween(region.lower.x, 0.0F, 1.0F); + checkBetween(region.lower.y, 0.0F, 1.0F); + } +}; + class FrameSizeIntegrity { public: @@ -99,6 +119,7 @@ Camera &Camera::operator=(Camera &&other) noexcept _data = std::move(other._data); _view = other._view; _nearClippingDistance = other._nearClippingDistance; + _imageRegion = other._imageRegion; _aspectRatio = other._aspectRatio; _flag = std::move(other._flag); return *this; @@ -117,6 +138,7 @@ Camera &Camera::operator=(const Camera &other) _data->pushTo(_handle); _view = other._view; _nearClippingDistance = other._nearClippingDistance; + _imageRegion = other._imageRegion; _aspectRatio = other._aspectRatio; _flag.setModified(true); return *this; @@ -149,6 +171,17 @@ void Camera::setNearClippingDistance(float distance) _flag.update(_nearClippingDistance, distance); } +const Box2 &Camera::getImageRegion() const +{ + return _imageRegion; +} + +void Camera::setImageRegion(const Box2 &imageRegion) +{ + ImageRegionIntegrity::check(imageRegion); + _flag.update(_imageRegion, imageRegion); +} + void Camera::setAspectRatioFromFrameSize(const Vector2ui &frameSize) { FrameSizeIntegrity::check(frameSize); @@ -202,7 +235,7 @@ void Camera::_updateAspectRatio() void Camera::_updateImageOrientation() { - _handle.setParam(CameraParameters::imageStart, Vector2f(0, 1)); - _handle.setParam(CameraParameters::imageEnd, Vector2f(1, 0)); + _handle.setParam(CameraParameters::imageStart, _imageRegion.lower); + _handle.setParam(CameraParameters::imageEnd, _imageRegion.upper); } } diff --git a/brayns/engine/camera/Camera.h b/brayns/engine/camera/Camera.h index c3d003b9d..21a496775 100644 --- a/brayns/engine/camera/Camera.h +++ b/brayns/engine/camera/Camera.h @@ -117,6 +117,20 @@ class Camera */ void setNearClippingDistance(float distance); + /** + * @brief Get the Image Region object + * + * @return const Box2& + */ + const Box2 &getImageRegion() const; + + /** + * @brief Set the Image Region object + * + * @param imageRegion + */ + void setImageRegion(const Box2 &imageRegion); + /** * @brief Sets the render plane aspect ratio. * @param aspectRatio (width/height). @@ -149,6 +163,7 @@ class Camera std::unique_ptr> _data; View _view; float _nearClippingDistance = 1e-6f; + Box2 _imageRegion = {{0, 1}, {1, 0}}; float _aspectRatio = 1.f; ModifiedFlag _flag; }; diff --git a/brayns/network/NetworkManager.cpp b/brayns/network/NetworkManager.cpp index 24c952211..ed8372be2 100644 --- a/brayns/network/NetworkManager.cpp +++ b/brayns/network/NetworkManager.cpp @@ -36,6 +36,7 @@ #include #include #include +#include #include #include #include @@ -118,6 +119,7 @@ class CoreEntrypointRegistry builder.add(engine); builder.add(engine); builder.add(engine); + builder.add(engine); builder.add(engine); builder.add(engine); builder.add(models); @@ -151,6 +153,7 @@ class CoreEntrypointRegistry builder.add(engine); builder.add(engine); builder.add(engine); + builder.add(engine); builder.add(engine); builder.add(models); builder.add(models); diff --git a/brayns/network/entrypoints/CameraRegionEntrypoint.cpp b/brayns/network/entrypoints/CameraRegionEntrypoint.cpp new file mode 100644 index 000000000..ebbdfed0d --- /dev/null +++ b/brayns/network/entrypoints/CameraRegionEntrypoint.cpp @@ -0,0 +1,69 @@ +/* Copyright (c) 2015-2024 EPFL/Blue Brain Project + * All rights reserved. Do not distribute without permission. + * Responsible Author: adrien.fleury@epfl.ch + * + * This file is part of Brayns + * + * This library is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License version 3.0 as published + * by the Free Software Foundation. + * + * This library is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more + * details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "CameraRegionEntrypoint.h" + +namespace brayns +{ +SetCameraRegionEntrypoint::SetCameraRegionEntrypoint(Engine &engine): + _engine(engine) +{ +} + +std::string SetCameraRegionEntrypoint::getMethod() const +{ + return "set-camera-region"; +} + +std::string SetCameraRegionEntrypoint::getDescription() const +{ + return "Update the camera image region"; +} + +void SetCameraRegionEntrypoint::onRequest(const Request &request) +{ + auto params = request.getParams(); + auto &camera = _engine.getCamera(); + camera.setImageRegion({params.image_start, params.image_end}); + request.reply(EmptyJson()); +} + +GetCameraRegionEntrypoint::GetCameraRegionEntrypoint(Engine &engine): + _engine(engine) +{ +} + +std::string GetCameraRegionEntrypoint::getMethod() const +{ + return "get-camera-region"; +} + +std::string GetCameraRegionEntrypoint::getDescription() const +{ + return "Retreive the current camera image region"; +} + +void GetCameraRegionEntrypoint::onRequest(const Request &request) +{ + auto &camera = _engine.getCamera(); + auto region = camera.getImageRegion(); + request.reply({region.lower, region.upper}); +} +} diff --git a/brayns/network/entrypoints/CameraRegionEntrypoint.h b/brayns/network/entrypoints/CameraRegionEntrypoint.h new file mode 100644 index 000000000..a23fe703c --- /dev/null +++ b/brayns/network/entrypoints/CameraRegionEntrypoint.h @@ -0,0 +1,54 @@ +/* Copyright (c) 2015-2024 EPFL/Blue Brain Project + * All rights reserved. Do not distribute without permission. + * Responsible Author: adrien.fleury@epfl.ch + * + * This file is part of Brayns + * + * This library is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License version 3.0 as published + * by the Free Software Foundation. + * + * This library is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more + * details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#pragma once + +#include +#include +#include + +namespace brayns +{ +class SetCameraRegionEntrypoint final : public Entrypoint +{ +public: + explicit SetCameraRegionEntrypoint(Engine &engine); + + std::string getMethod() const override; + std::string getDescription() const override; + void onRequest(const Request &request) override; + +private: + Engine &_engine; +}; + +class GetCameraRegionEntrypoint final : public Entrypoint +{ +public: + explicit GetCameraRegionEntrypoint(Engine &engine); + + std::string getMethod() const override; + std::string getDescription() const override; + void onRequest(const Request &request) override; + +private: + Engine &_engine; +}; +} diff --git a/brayns/network/entrypoints/ExportGBuffersEntrypoint.cpp b/brayns/network/entrypoints/ExportGBuffersEntrypoint.cpp index aceaf8613..a81e224bc 100644 --- a/brayns/network/entrypoints/ExportGBuffersEntrypoint.cpp +++ b/brayns/network/entrypoints/ExportGBuffersEntrypoint.cpp @@ -46,6 +46,10 @@ class ParamsBuilder params.camera_view = view; params.camera_near_clip = camera.getNearClippingDistance(); + const auto ®ion = camera.getImageRegion(); + params.image_start = region.lower; + params.image_end = region.upper; + auto ¶msManager = engine.getParametersManager(); auto &appParams = paramsManager.getApplicationParameters(); @@ -198,6 +202,7 @@ class ExportHandler camera.setAspectRatioFromFrameSize(imageSize); camera.setView(params.camera_view); camera.setNearClippingDistance(params.camera_near_clip); + camera.setImageRegion({params.image_start, params.image_end}); camera.commit(); // Scene diff --git a/brayns/network/entrypoints/SnapshotEntrypoint.cpp b/brayns/network/entrypoints/SnapshotEntrypoint.cpp index 0670b9f1d..c16c5658a 100644 --- a/brayns/network/entrypoints/SnapshotEntrypoint.cpp +++ b/brayns/network/entrypoints/SnapshotEntrypoint.cpp @@ -48,6 +48,10 @@ class ParamsBuilder params.camera_view = view; params.camera_near_clip = camera.getNearClippingDistance(); + const auto ®ion = camera.getImageRegion(); + params.image_start = region.lower; + params.image_end = region.upper; + auto ¶msManager = engine.getParametersManager(); auto &appParams = paramsManager.getApplicationParameters(); @@ -178,6 +182,7 @@ class SnapshotHandler camera.setAspectRatioFromFrameSize(imageSize); camera.setView(params.camera_view); camera.setNearClippingDistance(params.camera_near_clip); + camera.setImageRegion({params.image_start, params.image_end}); camera.commit(); // Scene diff --git a/brayns/network/messages/CameraRegionMessage.h b/brayns/network/messages/CameraRegionMessage.h new file mode 100644 index 000000000..8dda8a333 --- /dev/null +++ b/brayns/network/messages/CameraRegionMessage.h @@ -0,0 +1,55 @@ +/* Copyright (c) 2015-2024 EPFL/Blue Brain Project + * All rights reserved. Do not distribute without permission. + * + * Responsible Author: adrien.fleury@epfl.ch + * + * This file is part of Brayns + * + * This library is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License version 3.0 as published + * by the Free Software Foundation. + * + * This library is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more + * details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#pragma once + +#include + +namespace brayns +{ +struct CameraRegionMessage +{ + Vector2f image_start = {1, 0}; + Vector2f image_end = {0, 1}; +}; + +template<> +struct JsonAdapter : ObjectAdapter +{ + static JsonObjectInfo reflect() + { + auto builder = Builder("CameraRegionMessage"); + builder + .getset( + "image_start", + [](auto &object) { return object.image_start; }, + [](auto &object, auto value) { object.image_start = value; }) + .description("Camera image region lower bound XY normalized"); + builder + .getset( + "image_end", + [](auto &object) { return object.image_end; }, + [](auto &object, auto value) { object.image_end = value; }) + .description("Camera image region upper bound XY normalized"); + return builder.build(); + } +}; +} // namespace brayns diff --git a/brayns/network/messages/ExportGBuffersMessage.h b/brayns/network/messages/ExportGBuffersMessage.h index 7d76a2d02..d4768ad3d 100644 --- a/brayns/network/messages/ExportGBuffersMessage.h +++ b/brayns/network/messages/ExportGBuffersMessage.h @@ -34,6 +34,8 @@ struct GBuffersParams EngineObjectData camera; View camera_view; float camera_near_clip = 0.0f; + Vector2f image_start = {0, 1}; + Vector2f image_end = {1, 0}; EngineObjectData renderer; uint32_t simulation_frame = 0; std::string file_path; @@ -74,6 +76,20 @@ struct JsonAdapter : ObjectAdapter [](auto &object, const auto &value) { object.camera_near_clip = value; }) .description("Camera near clipping distance") .required(false); + builder + .getset( + "image_start", + [](auto &object) -> auto & { return object.image_start; }, + [](auto &object, const auto &value) { object.image_start = value; }) + .description("Image region start XY normalized") + .required(false); + builder + .getset( + "image_end", + [](auto &object) -> auto & { return object.image_end; }, + [](auto &object, const auto &value) { object.image_end = value; }) + .description("Image region end XY normalized") + .required(false); builder .getset( "renderer", diff --git a/brayns/network/messages/SnapshotMessage.h b/brayns/network/messages/SnapshotMessage.h index 5f93eef03..d08229a49 100644 --- a/brayns/network/messages/SnapshotMessage.h +++ b/brayns/network/messages/SnapshotMessage.h @@ -36,6 +36,8 @@ struct SnapshotParams EngineObjectData camera; View camera_view; float camera_near_clip = 0.0f; + Vector2f image_start = {0, 1}; + Vector2f image_end = {1, 0}; EngineObjectData renderer; uint32_t simulation_frame; std::string file_path; @@ -76,6 +78,20 @@ struct JsonAdapter : ObjectAdapter [](auto &object, const auto &value) { object.camera_near_clip = value; }) .description("Camera near clipping distance") .required(false); + builder + .getset( + "image_start", + [](auto &object) -> auto & { return object.image_start; }, + [](auto &object, const auto &value) { object.image_start = value; }) + .description("Image region start XY normalized") + .required(false); + builder + .getset( + "image_end", + [](auto &object) -> auto & { return object.image_end; }, + [](auto &object, const auto &value) { object.image_end = value; }) + .description("Image region end XY normalized") + .required(false); builder .getset( "renderer", diff --git a/brayns/utils/MathTypes.h b/brayns/utils/MathTypes.h index 904d8107a..1759d3cdd 100644 --- a/brayns/utils/MathTypes.h +++ b/brayns/utils/MathTypes.h @@ -60,6 +60,11 @@ using Quaternion = math::quaternionf; */ using AxisAlignedBounds = math::box3f; +/** + * 2D box + */ +using Box2 = math::box2f; + /** * Matrix definitions */ diff --git a/python/brayns/__init__.py b/python/brayns/__init__.py index edaca28f7..a2408d090 100644 --- a/python/brayns/__init__.py +++ b/python/brayns/__init__.py @@ -104,6 +104,7 @@ get_camera_name, get_camera_near_clip, get_camera_projection, + get_camera_region, get_camera_view, get_color_methods, get_color_ramp, @@ -127,6 +128,7 @@ set_camera, set_camera_near_clip, set_camera_projection, + set_camera_region, set_camera_view, set_color_ramp, set_framebuffer, @@ -303,6 +305,7 @@ "get_camera_name", "get_camera_near_clip", "get_camera_projection", + "get_camera_region", "get_camera_view", "get_camera", "get_circuit_ids", @@ -393,6 +396,7 @@ "ServiceUnavailableError", "set_camera_near_clip", "set_camera_projection", + "set_camera_region", "set_camera_view", "set_camera", "set_circuit_thickness", diff --git a/python/brayns/core/__init__.py b/python/brayns/core/__init__.py index bcfe22f50..e3d0266eb 100644 --- a/python/brayns/core/__init__.py +++ b/python/brayns/core/__init__.py @@ -117,6 +117,7 @@ get_camera_projection, set_camera_projection, ) +from .region import get_camera_region, set_camera_region from .renderer import ( InteractiveRenderer, ProductionRenderer, @@ -179,6 +180,7 @@ "get_camera_name", "get_camera_near_clip", "get_camera_projection", + "get_camera_region", "get_camera_view", "get_camera", "get_color_methods", @@ -233,6 +235,7 @@ "serialize_view", "set_camera_near_clip", "set_camera_projection", + "set_camera_region", "set_camera_view", "set_camera", "set_color_ramp", diff --git a/python/brayns/core/camera.py b/python/brayns/core/camera.py index 6e696acd4..d9871f712 100644 --- a/python/brayns/core/camera.py +++ b/python/brayns/core/camera.py @@ -24,7 +24,7 @@ from dataclasses import dataclass, field from brayns.network import Instance -from brayns.utils import Rotation, Vector3 +from brayns.utils import Rotation, Vector2, Vector3 from .near_clip import get_camera_near_clip, set_camera_near_clip from .projection import ( @@ -33,6 +33,7 @@ get_camera_projection, set_camera_projection, ) +from .region import get_camera_region, set_camera_region from .view import View, get_camera_view, set_camera_view @@ -46,6 +47,8 @@ class Camera: view: View = field(default_factory=lambda: View.front) projection: Projection = field(default_factory=PerspectiveProjection) near_clipping_distance: float = 1e-6 + image_start: Vector2 = Vector2(0, 1) + image_end: Vector2 = Vector2(1, 0) @property def name(self) -> str: @@ -221,7 +224,8 @@ def get_camera(instance: Instance, projection_type: type[Projection]) -> Camera: view = get_camera_view(instance) projection = get_camera_projection(instance, projection_type) distance = get_camera_near_clip(instance) - return Camera(view, projection, distance) + start, end = get_camera_region(instance) + return Camera(view, projection, distance, start, end) def set_camera(instance: Instance, camera: Camera) -> None: @@ -235,3 +239,4 @@ def set_camera(instance: Instance, camera: Camera) -> None: set_camera_view(instance, camera.view) set_camera_projection(instance, camera.projection) set_camera_near_clip(instance, camera.near_clipping_distance) + set_camera_region(instance, camera.image_start, camera.image_end) diff --git a/python/brayns/core/gbuffer_exporter.py b/python/brayns/core/gbuffer_exporter.py index 933374828..72ad388bc 100644 --- a/python/brayns/core/gbuffer_exporter.py +++ b/python/brayns/core/gbuffer_exporter.py @@ -176,6 +176,8 @@ def _serialize_export( message["camera_view"] = serialize_view(camera.view) message["camera"] = camera.projection.get_properties_with_name() message["camera_near_clip"] = camera.near_clipping_distance + message["image_start"] = list(camera.image_start) + message["image_end"] = list(camera.image_end) if export.renderer is not None: message["renderer"] = export.renderer.get_properties_with_name() if export.frame is not None: diff --git a/python/brayns/core/region.py b/python/brayns/core/region.py new file mode 100644 index 000000000..8568a5b04 --- /dev/null +++ b/python/brayns/core/region.py @@ -0,0 +1,32 @@ +# Copyright (c) 2015-2024 EPFL/Blue Brain Project +# All rights reserved. Do not distribute without permission. +# +# Responsible Author: adrien.fleury@epfl.ch +# +# This file is part of Brayns +# +# This library is free software; you can redistribute it and/or modify it under +# the terms of the GNU Lesser General Public License version 3.0 as published +# by the Free Software Foundation. +# +# This library is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +# details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this library; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +from brayns.network import Instance +from brayns.utils import Vector2 + + +def get_camera_region(instance: Instance) -> tuple[Vector2, Vector2]: + result = instance.request("get-camera-region") + return (Vector2(*result["image_start"]), Vector2(*result["image_end"])) + + +def set_camera_region(instance: Instance, start: Vector2, end: Vector2) -> None: + params = {"image_start": list(start), "image_end": list(end)} + instance.request("set-camera-region", params) diff --git a/python/brayns/core/snapshot.py b/python/brayns/core/snapshot.py index a4864b8a5..6eff5e823 100644 --- a/python/brayns/core/snapshot.py +++ b/python/brayns/core/snapshot.py @@ -184,6 +184,8 @@ def _serialize_snapshot( message["camera_view"] = serialize_view(camera.view) message["camera"] = camera.projection.get_properties_with_name() message["camera_near_clip"] = camera.near_clipping_distance + message["image_start"] = list(camera.image_start) + message["image_end"] = list(camera.image_end) if snapshot.renderer is not None: message["renderer"] = snapshot.renderer.get_properties_with_name() if snapshot.frame is not None: diff --git a/python/brayns/core/version.py b/python/brayns/core/version.py index f23509171..58b485501 100644 --- a/python/brayns/core/version.py +++ b/python/brayns/core/version.py @@ -21,10 +21,9 @@ from dataclasses import dataclass from typing import Any -from brayns.version import VERSION - from brayns.network import Instance from brayns.utils import Error +from brayns.version import VERSION @dataclass diff --git a/python/testapi/core/test_camera.py b/python/testapi/core/test_camera.py index 1779b0a91..1588faab8 100644 --- a/python/testapi/core/test_camera.py +++ b/python/testapi/core/test_camera.py @@ -105,3 +105,14 @@ def test_render_near_clip(self) -> None: render_and_validate(self, "clip_two", settings) camera.near_clipping_distance = 3 render_and_validate(self, "clip_one", settings) + + def test_getset_region(self) -> None: + lower, upper = brayns.get_camera_region(self.instance) + self.assertEqual(lower, brayns.Vector2(0, 1)) + self.assertEqual(upper, brayns.Vector2(1, 0)) + brayns.set_camera_region( + self.instance, brayns.Vector2(0.5, 0.5), brayns.Vector2(1, 1) + ) + lower, upper = brayns.get_camera_region(self.instance) + self.assertEqual(lower, brayns.Vector2(0.5, 0.5)) + self.assertEqual(upper, brayns.Vector2(1, 1)) diff --git a/python/tests/core/test_gbuffer_exporter.py b/python/tests/core/test_gbuffer_exporter.py index c88eb9658..6e6795228 100644 --- a/python/tests/core/test_gbuffer_exporter.py +++ b/python/tests/core/test_gbuffer_exporter.py @@ -80,6 +80,8 @@ def mock_exporter_message(self) -> dict[str, Any]: "camera_view": mock_view_message(), "camera": brayns.PerspectiveProjection().get_properties_with_name(), "camera_near_clip": 1.5, + "image_start": [0, 1], + "image_end": [1, 0], "renderer": brayns.ProductionRenderer().get_properties_with_name(), "simulation_frame": 12, } diff --git a/python/tests/core/test_snapshot.py b/python/tests/core/test_snapshot.py index 541c7cb2c..d7d5bc13d 100644 --- a/python/tests/core/test_snapshot.py +++ b/python/tests/core/test_snapshot.py @@ -92,6 +92,8 @@ def mock_snapshot_message(self) -> dict[str, Any]: "camera_view": mock_view_message(), "camera": brayns.PerspectiveProjection().get_properties_with_name(), "camera_near_clip": 1.5, + "image_start": [0, 1], + "image_end": [1, 0], "renderer": brayns.ProductionRenderer().get_properties_with_name(), "simulation_frame": 12, "metadata": {