From 121e601c7b64dbddd3a70f9240a8164e58dd152f Mon Sep 17 00:00:00 2001 From: DragonMoffon Date: Tue, 15 Jul 2025 14:10:12 +1200 Subject: [PATCH 1/5] Move viewport camera to it's own file --- arcade/camera/__init__.py | 13 +++--- arcade/camera/default.py | 97 ++------------------------------------ arcade/camera/viewport.py | 98 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 109 insertions(+), 99 deletions(-) create mode 100644 arcade/camera/viewport.py diff --git a/arcade/camera/__init__.py b/arcade/camera/__init__.py index 902e8e402..1ea7e167b 100644 --- a/arcade/camera/__init__.py +++ b/arcade/camera/__init__.py @@ -3,7 +3,7 @@ Providing a multitude of camera's for any need. """ -from arcade.camera.data_types import ( +from .data_types import ( Projection, Projector, CameraData, @@ -11,7 +11,7 @@ PerspectiveProjectionData, ) -from arcade.camera.projection_functions import ( +from .projection_functions import ( generate_view_matrix, generate_orthographic_matrix, generate_perspective_matrix, @@ -21,10 +21,10 @@ unproject_perspective, ) -from arcade.camera.orthographic import OrthographicProjector -from arcade.camera.perspective import PerspectiveProjector - -from arcade.camera.camera_2d import Camera2D +from .viewport import ViewportProjector +from .orthographic import OrthographicProjector +from .perspective import PerspectiveProjector +from .camera_2d import Camera2D import arcade.camera.grips as grips @@ -32,6 +32,7 @@ __all__ = [ "Projection", "Projector", + "ViewportProjector", "CameraData", "generate_view_matrix", "OrthographicProjectionData", diff --git a/arcade/camera/default.py b/arcade/camera/default.py index dcb94d4b9..8f278560a 100644 --- a/arcade/camera/default.py +++ b/arcade/camera/default.py @@ -1,107 +1,18 @@ from __future__ import annotations -from collections.abc import Generator -from contextlib import contextmanager from typing import TYPE_CHECKING -from pyglet.math import Mat4, Vec2, Vec3 -from typing_extensions import Self +from pyglet.math import Mat4 +from arcade.types import LBWH -from arcade.types import LBWH, Point, Rect -from arcade.window_commands import get_window +from .viewport import ViewportProjector if TYPE_CHECKING: from arcade.context import ArcadeContext -__all__ = ["ViewportProjector", "DefaultProjector"] +__all__ = ("DefaultProjector",) -class ViewportProjector: - """ - A simple Projector which does not rely on any camera PoDs. - - Does not have a way of moving, rotating, or zooming the camera. - perfect for something like UI or for mapping to an offscreen framebuffer. - - Args: - viewport: The viewport to project to. - context: The window context to bind the camera to. Defaults to the currently active window. - """ - - def __init__( - self, - viewport: Rect | None = None, - *, - context: ArcadeContext | None = None, - ): - self._ctx: ArcadeContext = context or get_window().ctx - self._viewport: Rect = viewport or LBWH(*self._ctx.viewport) - self._projection_matrix: Mat4 = Mat4.orthogonal_projection( - 0.0, self._viewport.width, 0.0, self._viewport.height, -100, 100 - ) - - @property - def viewport(self) -> Rect: - """ - The viewport use to derive projection and view matrix. - """ - return self._viewport - - @viewport.setter - def viewport(self, viewport: Rect) -> None: - self._viewport = viewport - self._projection_matrix = Mat4.orthogonal_projection( - 0, viewport.width, 0, viewport.height, -100, 100 - ) - - def use(self) -> None: - """ - Set the window's projection and view matrix. - Also sets the projector as the windows current camera. - """ - self._ctx.current_camera = self - - self._ctx.viewport = self.viewport.lbwh_int # get the integer 4-tuple LBWH - - self._ctx.view_matrix = Mat4() - self._ctx.projection_matrix = self._projection_matrix - - @contextmanager - def activate(self) -> Generator[Self, None, None]: - """ - The context manager version of the use method. - - usable with the 'with' block. e.g. 'with ViewportProjector.activate() as cam: ...' - """ - previous = self._ctx.current_camera - try: - self.use() - yield self - finally: - previous.use() - - def project(self, world_coordinate: Point) -> Vec2: - """ - Take a Vec2 or Vec3 of coordinates and return the related screen coordinate - """ - x, y, *z = world_coordinate - return Vec2(x, y) - - def unproject(self, screen_coordinate: Point) -> Vec3: - """ - Map the screen pos to screen_coordinates. - - Due to the nature of viewport projector this does not do anything. - """ - x, y, *_z = screen_coordinate - z = 0.0 if not _z else _z[0] - - return Vec3(x, y, z) - - -# As this class is only supposed to be used internally -# I wanted to place an _ in front, but the linting complains -# about it being a protected class. class DefaultProjector(ViewportProjector): """ An extremely limited projector which lacks any kind of control. This is only diff --git a/arcade/camera/viewport.py b/arcade/camera/viewport.py new file mode 100644 index 000000000..7e9c01b4b --- /dev/null +++ b/arcade/camera/viewport.py @@ -0,0 +1,98 @@ +from __future__ import annotations + +from collections.abc import Generator +from contextlib import contextmanager +from typing import TYPE_CHECKING + +from pyglet.math import Mat4, Vec2, Vec3 +from typing_extensions import Self + +from arcade.types import LBWH, Point, Rect +from arcade.window_commands import get_window + +if TYPE_CHECKING: + from arcade.context import ArcadeContext + +__all__ = ("ViewportProjector",) + +class ViewportProjector: + """ + A simple Projector which does not rely on any camera PoDs. + + Does not have a way of moving, rotating, or zooming the camera. + perfect for something like UI or for mapping to an offscreen framebuffer. + + Args: + viewport: The viewport to project to. + context: The window context to bind the camera to. Defaults to the currently active window. + """ + + def __init__( + self, + viewport: Rect | None = None, + *, + context: ArcadeContext | None = None, + ): + self._ctx: ArcadeContext = context or get_window().ctx + self._viewport: Rect = viewport or LBWH(*self._ctx.viewport) + self._projection_matrix: Mat4 = Mat4.orthogonal_projection( + 0.0, self._viewport.width, 0.0, self._viewport.height, -100, 100 + ) + + @property + def viewport(self) -> Rect: + """ + The viewport use to derive projection and view matrix. + """ + return self._viewport + + @viewport.setter + def viewport(self, viewport: Rect) -> None: + self._viewport = viewport + self._projection_matrix = Mat4.orthogonal_projection( + 0, viewport.width, 0, viewport.height, -100, 100 + ) + + def use(self) -> None: + """ + Set the window's projection and view matrix. + Also sets the projector as the windows current camera. + """ + self._ctx.current_camera = self + + self._ctx.viewport = self.viewport.lbwh_int # get the integer 4-tuple LBWH + + self._ctx.view_matrix = Mat4() + self._ctx.projection_matrix = self._projection_matrix + + @contextmanager + def activate(self) -> Generator[Self, None, None]: + """ + The context manager version of the use method. + + usable with the 'with' block. e.g. 'with ViewportProjector.activate() as cam: ...' + """ + previous = self._ctx.current_camera + try: + self.use() + yield self + finally: + previous.use() + + def project(self, world_coordinate: Point) -> Vec2: + """ + Take a Vec2 or Vec3 of coordinates and return the related screen coordinate + """ + x, y, *z = world_coordinate + return Vec2(x, y) + + def unproject(self, screen_coordinate: Point) -> Vec3: + """ + Map the screen pos to screen_coordinates. + + Due to the nature of viewport projector this does not do anything. + """ + x, y, *_z = screen_coordinate + z = 0.0 if not _z else _z[0] + + return Vec3(x, y, z) \ No newline at end of file From c1f4d291ea5300ea98c1b14bea8b2490b6a797a0 Mon Sep 17 00:00:00 2001 From: DragonMoffon Date: Tue, 15 Jul 2025 14:10:52 +1200 Subject: [PATCH 2/5] Update context's reset method to use the default camera protecting the unit tests from corrupting camera state --- arcade/context.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/arcade/context.py b/arcade/context.py index b727fa179..d0c933a80 100644 --- a/arcade/context.py +++ b/arcade/context.py @@ -56,6 +56,7 @@ def __init__( gc_mode: str = "context_gc", gl_api: str = "gl", ) -> None: + super().__init__(window, gc_mode=gc_mode, gl_api=gl_api) # Set up a default orthogonal projection for sprites and shapes @@ -68,6 +69,7 @@ def __init__( self.current_camera: Projector = self._default_camera self.viewport = (0, 0, window.width, window.height) + # --- Pre-load system shaders here --- # FIXME: These pre-created resources needs to be packaged nicely @@ -311,10 +313,7 @@ def reset(self) -> None: self.bind_window_block() # self.active_program = None self.viewport = 0, 0, self.window.width, self.window.height - self.view_matrix = Mat4() - self.projection_matrix = Mat4.orthogonal_projection( - 0, self.window.width, 0, self.window.height, -100, 100 - ) + self._default_camera.use() self.enable_only(self.BLEND) self.blend_func = self.BLEND_DEFAULT self.point_size = 1.0 @@ -356,7 +355,7 @@ def default_atlas(self) -> TextureAtlasBase: ) return self._atlas - + @property def viewport(self) -> tuple[int, int, int, int]: """ @@ -378,7 +377,7 @@ def viewport(self) -> tuple[int, int, int, int]: @viewport.setter def viewport(self, value: tuple[int, int, int, int]): self.active_framebuffer.viewport = value - if self._default_camera == self.current_camera: + if self._default_camera is self.current_camera: self._default_camera.use() @property From 79dc705c4cd81612e21caca34633a4717d23350e Mon Sep 17 00:00:00 2001 From: DragonMoffon Date: Tue, 15 Jul 2025 14:11:20 +1200 Subject: [PATCH 3/5] Reset context state before all draw calls --- arcade/application.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/arcade/application.py b/arcade/application.py index 759e7a7c3..8980638b9 100644 --- a/arcade/application.py +++ b/arcade/application.py @@ -553,6 +553,8 @@ def _dispatch_frame(self, delta_time: float) -> None: # In case the window close in on_update, on_fixed_update or input callbacks if not self.closed: + # Reset the context, camera, and active framebuffer. + self._ctx.reset() self.draw(self._accumulated_draw_time) self._accumulated_draw_time %= self._draw_rate From 5c5611a03ec7ef0f98051482c511a5bd69b8d08a Mon Sep 17 00:00:00 2001 From: DragonMoffon Date: Tue, 15 Jul 2025 14:21:38 +1200 Subject: [PATCH 4/5] viewport hack to force the default camera to update it's viewport. --- arcade/gl/framebuffer.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/arcade/gl/framebuffer.py b/arcade/gl/framebuffer.py index 65d109161..5fdd050d0 100644 --- a/arcade/gl/framebuffer.py +++ b/arcade/gl/framebuffer.py @@ -225,6 +225,9 @@ def use(self, *, force: bool = False): self._use(force=force) self._ctx.active_framebuffer = self + # This is a hack to ensure the default camera has the correct viewport. + self._ctx.viewport = self.viewport + @abstractmethod def _use(self, *, force: bool = False): """Internal use that do not change the global active framebuffer""" From 2a22c411e40606da239d2ac133f01b151c7cdcae Mon Sep 17 00:00:00 2001 From: DragonMoffon Date: Tue, 15 Jul 2025 14:34:36 +1200 Subject: [PATCH 5/5] linting --- arcade/camera/default.py | 1 + arcade/camera/viewport.py | 3 ++- arcade/context.py | 4 +--- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/arcade/camera/default.py b/arcade/camera/default.py index 8f278560a..3b4da0912 100644 --- a/arcade/camera/default.py +++ b/arcade/camera/default.py @@ -3,6 +3,7 @@ from typing import TYPE_CHECKING from pyglet.math import Mat4 + from arcade.types import LBWH from .viewport import ViewportProjector diff --git a/arcade/camera/viewport.py b/arcade/camera/viewport.py index 7e9c01b4b..c08526310 100644 --- a/arcade/camera/viewport.py +++ b/arcade/camera/viewport.py @@ -15,6 +15,7 @@ __all__ = ("ViewportProjector",) + class ViewportProjector: """ A simple Projector which does not rely on any camera PoDs. @@ -95,4 +96,4 @@ def unproject(self, screen_coordinate: Point) -> Vec3: x, y, *_z = screen_coordinate z = 0.0 if not _z else _z[0] - return Vec3(x, y, z) \ No newline at end of file + return Vec3(x, y, z) diff --git a/arcade/context.py b/arcade/context.py index d0c933a80..1f114232e 100644 --- a/arcade/context.py +++ b/arcade/context.py @@ -56,7 +56,6 @@ def __init__( gc_mode: str = "context_gc", gl_api: str = "gl", ) -> None: - super().__init__(window, gc_mode=gc_mode, gl_api=gl_api) # Set up a default orthogonal projection for sprites and shapes @@ -69,7 +68,6 @@ def __init__( self.current_camera: Projector = self._default_camera self.viewport = (0, 0, window.width, window.height) - # --- Pre-load system shaders here --- # FIXME: These pre-created resources needs to be packaged nicely @@ -355,7 +353,7 @@ def default_atlas(self) -> TextureAtlasBase: ) return self._atlas - + @property def viewport(self) -> tuple[int, int, int, int]: """