From 11cc5c7173725839b2ff0d7c80ae24a202708a43 Mon Sep 17 00:00:00 2001 From: DragonMoffon Date: Sat, 10 Jun 2023 12:45:43 +1200 Subject: [PATCH 01/31] Setup primary files for experimental refactor of the Camera, Backgrounds, and Scenes. See the #Possible Scene Improvements #Cameras topics in the arcade-dev forum on the Python Arcade discord server. --- arcade/experimental/background_refactor.py | 18 +++++ arcade/experimental/camera_refactor.py | 77 ++++++++++++++++++++++ arcade/experimental/scene_refactor.py | 0 3 files changed, 95 insertions(+) create mode 100644 arcade/experimental/background_refactor.py create mode 100644 arcade/experimental/camera_refactor.py create mode 100644 arcade/experimental/scene_refactor.py diff --git a/arcade/experimental/background_refactor.py b/arcade/experimental/background_refactor.py new file mode 100644 index 0000000000..e80f41f87d --- /dev/null +++ b/arcade/experimental/background_refactor.py @@ -0,0 +1,18 @@ +from typing import TYPE_CHECKING, Optional + +from arcade.gl import Program, Geometry + +from arcade.experimental.camera_refactor import CameraData + + +class BackgroundTexture: + + def __init__(self): + pass + + +class Background: + + def __init__(self, data: CameraData, texture: BackgroundTexture, color, + shader: Optional[Program] = None, geometry: Optional[Geometry] = None): + pass diff --git a/arcade/experimental/camera_refactor.py b/arcade/experimental/camera_refactor.py new file mode 100644 index 0000000000..3c3bbead33 --- /dev/null +++ b/arcade/experimental/camera_refactor.py @@ -0,0 +1,77 @@ +from typing import TYPE_CHECKING, Tuple, Optional, Union + +from dataclasses import dataclass + +from arcade.window_commands import get_window +from arcade.gl import Program, Geometry + +from pyglet.math import Mat4 + +if TYPE_CHECKING: + from arcade.application import Window + + +@dataclass +class CameraData: + """ + A PoD (Packet of Data) which holds the necessary data for a functional + 2D orthographic camera + + :param viewport: The pixel bounds which will be drawn onto. (left, bottom, width, height) + :param projection: The co-ordinate bounds which will be mapped to the viewport bounds. (left, right, bottom, top) + :param up: A 2D vector which describes which direction is up (+y) + :param zoom: A scaler which scales the size of the projection. + Is equivalent to increasing the size of the projection. + """ + + viewport: Tuple[int, int, int, int] + projection: Tuple[float, float, float, float] + up: Tuple[float, float] = (0.0, 1.0) + zoom: float = 1.0 + + +class Camera2DOrthographic: + """ + The simplest form of a 2D orthographic camera. + Using a CameraData PoD (Packet of Data) it generates + the correct projection and view matrices. It also + provides methods and a context manager for using the + matrices in glsl shaders. + + This class provides no methods for manipulating the CameraData. + + There are also two static class variables which control + the near and far clipping planes of the projection matrix. For most uses + this should be satisfactory. + + The current implementation will recreate the view and projection matrix every time + the camera is used. If used every frame or multiple times per frame this may be + inefficient. If you suspect that this may be causing issues profile before optimising. + + :param data: The CameraData PoD, will create a basic screen sized camera if no provided + """ + near_plane: float = -100.0 # the near clipping plane of the camera + far_plane: float = 100.0 # the far clipping plane of the camera + + def __init__(self, *, data: Optional[CameraData] = None, window: Optional[Window] = None): + # A reference to the current active Arcade Window. Used to access the current gl context. + self._window = window or get_window() + + # The camera data used to generate the view and projection matrix. + self._data = data or CameraData( + (0, 0, self._window.width, self._window.height), + (0.0, self._window.width, 0.0, self._window.height) + ) + + @property + def viewport_size(self): + return self._data.viewport[3:] + + @property + def viewport_width(self): + return self._data.viewport[3] + + @property + def viewport_height(self): + return self._data.viewport[4] + diff --git a/arcade/experimental/scene_refactor.py b/arcade/experimental/scene_refactor.py new file mode 100644 index 0000000000..e69de29bb2 From 882c83bdc6ad35b540af2acbc6ce166607a64088 Mon Sep 17 00:00:00 2001 From: DragonMoffon Date: Sat, 10 Jun 2023 13:58:41 +1200 Subject: [PATCH 02/31] Completed the Camera2DOrthographic class with doc strings and comments, and started the Camera2DController, and SimpleCamera classes. See the #Possible Scene Improvements #Cameras topics in the arcade-dev forum on the Python Arcade discord server for more info. --- arcade/experimental/camera_refactor.py | 190 +++++++++++++++++++++++-- 1 file changed, 181 insertions(+), 9 deletions(-) diff --git a/arcade/experimental/camera_refactor.py b/arcade/experimental/camera_refactor.py index 3c3bbead33..962f05c4a3 100644 --- a/arcade/experimental/camera_refactor.py +++ b/arcade/experimental/camera_refactor.py @@ -1,11 +1,11 @@ from typing import TYPE_CHECKING, Tuple, Optional, Union +from contextlib import contextmanager from dataclasses import dataclass from arcade.window_commands import get_window -from arcade.gl import Program, Geometry -from pyglet.math import Mat4 +from pyglet.math import Mat4, Vec3 if TYPE_CHECKING: from arcade.application import Window @@ -20,14 +20,15 @@ class CameraData: :param viewport: The pixel bounds which will be drawn onto. (left, bottom, width, height) :param projection: The co-ordinate bounds which will be mapped to the viewport bounds. (left, right, bottom, top) :param up: A 2D vector which describes which direction is up (+y) - :param zoom: A scaler which scales the size of the projection. + :param scale: A scaler which scales the size of the projection matrix from the center. Is equivalent to increasing the size of the projection. """ viewport: Tuple[int, int, int, int] projection: Tuple[float, float, float, float] - up: Tuple[float, float] = (0.0, 1.0) - zoom: float = 1.0 + position: Tuple[float, float, float] + up: Tuple[float, float, float] = (0.0, 1.0, 0.0) + scale: float = 1.0 class Camera2DOrthographic: @@ -60,18 +61,189 @@ def __init__(self, *, data: Optional[CameraData] = None, window: Optional[Window # The camera data used to generate the view and projection matrix. self._data = data or CameraData( (0, 0, self._window.width, self._window.height), - (0.0, self._window.width, 0.0, self._window.height) + (0.0, self._window.width, 0.0, self._window.height), + (self._window.width / 2, self._window.height / 2, 0.0) ) @property - def viewport_size(self): + def data(self) -> CameraData: + """ + Returns the CameraData which is used to make the matrices + """ + return self._data + + @property + def viewport_size(self) -> Tuple[int, int]: + """ + Returns the width and height of the viewport + """ return self._data.viewport[3:] @property - def viewport_width(self): + def viewport_width(self) -> int: + """ + Returns the width of the viewport + """ return self._data.viewport[3] @property - def viewport_height(self): + def viewport_height(self) -> int: + """ + Returns the height of the viewport + """ return self._data.viewport[4] + @property + def viewport(self) -> Tuple[int, int, int, int]: + """ + Returns the Viewport. + """ + return self._data.viewport + + @property + def projection(self) -> Tuple[float, float, float, float]: + """ + Returns the projection values used to generate the projection matrix + """ + return self._data.projection + + @property + def position(self) -> Tuple[float, float]: + """ + returns the 2D position of the camera + """ + return self._data.position[:2] + + @property + def up(self) -> Tuple[float, float]: + """ + returns the 2d up direction of the camera + """ + return self._data.up[:2] + + @property + def scale(self) -> float: + """ + returns the zoom value of the camera + """ + return self._data.scale + + def _generate_view_matrix(self) -> Mat4: + """ + Generates a view matrix which always has the z axis at 0, and looks towards the positive z axis. + Ignores the z component of the position, and up vectors. + + To protect against unexpected behaviour ensure that the up vector is unit length without the z axis as + it is not normalised before use. + """ + return Mat4.look_at(Vec3(*self.position[:2], 0.0), + Vec3(*self.position[:2], 1.0), + Vec3(*self.up[:2], 0.0)) + + def _generate_projection_matrix(self) -> Mat4: + """ + Generates the projection matrix. This uses the values provided by the CameraData.projection tuple. + It is then scaled by the Camera.scale float. It is scaled from the center of the projection values not 0,0. + + Generally keep the scale value to integers or negative powers of 2 (0.5, 0.25, etc.) to keep + the pixels uniform in size. Avoid a scale of 0.0. + """ + + # Find the center of the projection values (often 0,0 or the center of the screen) + _projection_center = ( + (self._data.projection[0] + self._data.projection[1]) / 2, + (self._data.projection[2] + self._data.projection[3]) / 2 + ) + + # Find half the width of the projection + _projection_half_size = ( + (self._data.projection[1] - self._data.projection[0]) / 2, + (self._data.projection[3] - self._data.projection[2]) / 2 + ) + + # Scale the projection by the scale value. Both the width and the height + # share a scale value to avoid ugly stretching. + _true_projection = ( + _projection_center[0] - _projection_half_size[0] * self._data.scale, + _projection_center[0] + _projection_half_size[0] * self._data.scale, + _projection_center[1] - _projection_half_size[1] * self._data.scale, + _projection_center[1] + _projection_half_size[1] * self._data.scale + ) + return Mat4.orthogonal_projection(*_true_projection, self.near_plane, self.far_plane) + + def use(self): + """ + Sets the active camera to this object. + Then generates the view and projection matrices. + Finally, the gl context viewport is set, as well as the projection and view matrices. + """ + + self._window.current_camera = self + + _projection = self._generate_projection_matrix() + _view = self._generate_view_matrix() + + self._window.ctx.viewport = self._data.viewport + self._window.projection = _projection + self._window.view = _view + + @contextmanager + def activate(self): + """ + A context manager version of Camera2DOrthographic.use() which allows for the use of + `with` blocks. For example, `with camera.activate() as cam: ...`. + """ + previous_camera = self._window.current_camera + try: + self.use() + yield self + finally: + previous_camera.use() + + +class Camera2DController: + """ + + """ + def __init__(self, *, data: Optional[CameraData] = None, window: Optional[Window] = None): + # A reference to the current active Arcade Window. Used to access the current gl context. + self._window = window or get_window() + + # The camera data used to generate the view and projection matrix. + self._data = data or CameraData( + (0, 0, self._window.width, self._window.height), + (0.0, self._window.width, 0.0, self._window.height), + (self._window.width / 2, self._window.height / 2, 0.0) + ) + + +class SimpleCamera: + """ + + """ + + def __init__(self, *, data: Optional[CameraData] = None, window: Optional[Window] = None, + viewport: Optional[Tuple[int, int, int, int]] = None, + projection: Optional[Tuple[float, float, float, float]] = None, + position: Optional[Tuple[float, float, float]] = None, + up: Optional[Tuple[float, float, float]] = None, scale: Optional[float] = None): + # A reference to the current active Arcade Window. Used to access the current gl context. + self._window: Window = window or get_window() + + # For backwards compatibility both the new camera data, + # and the old raw tuples are available for initialisation. + # If both are supplied the camera cannot decide which values to use and will raise a ValueError + if any((viewport, projection, up, scale)) and data: + raise ValueError(f"Both the CameraData {data}," + f" and the values {viewport, projection, position, up, scale} have been supplied." + f"Ensure only one of the two is provided.") + + # The camera data used to generate the view and projection matrix. + if any((viewport, projection, up, scale)): + self._data = CameraData(viewport, projection, position, up, scale) + else: + self._data = data or CameraData( + (0, 0, self._window.width, self._window.height), + (0.0, self._window.width, 0.0, self._window.height), + (self._window.width / 2, self._window.height / 2, 0.0) + ) From 17d1a9b58770ccf63fb94532edec49dc78f411dd Mon Sep 17 00:00:00 2001 From: DragonMoffon Date: Sat, 10 Jun 2023 23:52:25 +1200 Subject: [PATCH 03/31] Slight change to Cameras to allow for a perspective projection matrix. Created basic Perspective and Orthographic Cameras. See the #Possible Scene Improvements #Cameras topics in the arcade-dev forum on the Python Arcade discord server for more info. --- arcade/experimental/camera_refactor.py | 399 +++++++++++++++---------- 1 file changed, 238 insertions(+), 161 deletions(-) diff --git a/arcade/experimental/camera_refactor.py b/arcade/experimental/camera_refactor.py index 962f05c4a3..96923d8af9 100644 --- a/arcade/experimental/camera_refactor.py +++ b/arcade/experimental/camera_refactor.py @@ -1,4 +1,4 @@ -from typing import TYPE_CHECKING, Tuple, Optional, Union +from typing import TYPE_CHECKING, Tuple, Optional, Union, Protocol from contextlib import contextmanager from dataclasses import dataclass @@ -10,166 +10,179 @@ if TYPE_CHECKING: from arcade.application import Window +from arcade.application import Window + @dataclass -class CameraData: +class ViewData: """ - A PoD (Packet of Data) which holds the necessary data for a functional - 2D orthographic camera + A PoD (Packet of Data) which holds the necessary data for a functional camera excluding the projection data :param viewport: The pixel bounds which will be drawn onto. (left, bottom, width, height) - :param projection: The co-ordinate bounds which will be mapped to the viewport bounds. (left, right, bottom, top) - :param up: A 2D vector which describes which direction is up (+y) - :param scale: A scaler which scales the size of the projection matrix from the center. - Is equivalent to increasing the size of the projection. - """ + :param position: A 3D vector which describes where the camera is located. + :param up: A 3D vector which describes which direction is up (+y). + :param forward: a 3D vector which describes which direction is forwards (+z). + """ + # Viewport data viewport: Tuple[int, int, int, int] - projection: Tuple[float, float, float, float] + + # View matrix data position: Tuple[float, float, float] - up: Tuple[float, float, float] = (0.0, 1.0, 0.0) - scale: float = 1.0 + up: Tuple[float, float, float] + forward: Tuple[float, float, float] -class Camera2DOrthographic: +@dataclass +class OrthographicProjectionData: """ - The simplest form of a 2D orthographic camera. - Using a CameraData PoD (Packet of Data) it generates - the correct projection and view matrices. It also - provides methods and a context manager for using the - matrices in glsl shaders. - - This class provides no methods for manipulating the CameraData. + A PoD (Packet of Data) which holds the necessary data for a functional Orthographic Projection matrix. + + This is by default a Left-handed system. with the X axis going from left to right, The Y axis going from + bottom to top, and the Z axis going from towards the screen to away from the screen. This can be made + right-handed by making the near value greater than the far value. + + :param left: The left most value, which gets mapped to x = -1.0 (anything below this value is not visible). + :param right: The right most value, which gets mapped to x = 1.0 (anything above this value is not visible). + :param bottom: The bottom most value, which gets mapped to y = -1.0 (anything below this value is not visible). + :param top: The top most value, which gets mapped to y = 1.0 (anything above this value is not visible). + :param near: The 'closest' value, which gets mapped to z = -1.0 (anything below this value is not visible). + :param far: The 'furthest' value, Which gets mapped to z = 1.0 (anything above this value is not visible). + :param zoom: A scaler which defines how much the Orthographic projection scales by. Is a simpler way of changing the + width and height of the projection. + """ + left: float + right: float + bottom: float + top: float + near: float + far: float + zoom: float - There are also two static class variables which control - the near and far clipping planes of the projection matrix. For most uses - this should be satisfactory. - The current implementation will recreate the view and projection matrix every time - the camera is used. If used every frame or multiple times per frame this may be - inefficient. If you suspect that this may be causing issues profile before optimising. +@dataclass +class PerspectiveProjectionData: + """ + A PoD (Packet of Data) which holds the necessary data for a functional Perspective matrix. - :param data: The CameraData PoD, will create a basic screen sized camera if no provided + :param aspect: The aspect ratio of the screen (width over height). + :param fov: The field of view in degrees. With the aspect ratio defines + the size of the projection at any given depth. + :param near: The 'closest' value, which gets mapped to z = -1.0 (anything below this value is not visible). + :param far: The 'furthest' value, Which gets mapped to z = 1.0 (anything above this value is not visible). """ - near_plane: float = -100.0 # the near clipping plane of the camera - far_plane: float = 100.0 # the far clipping plane of the camera - - def __init__(self, *, data: Optional[CameraData] = None, window: Optional[Window] = None): - # A reference to the current active Arcade Window. Used to access the current gl context. - self._window = window or get_window() - - # The camera data used to generate the view and projection matrix. - self._data = data or CameraData( - (0, 0, self._window.width, self._window.height), - (0.0, self._window.width, 0.0, self._window.height), - (self._window.width / 2, self._window.height / 2, 0.0) - ) + aspect: float + fov: float + near: float + far: float + zoom: float - @property - def data(self) -> CameraData: - """ - Returns the CameraData which is used to make the matrices - """ - return self._data - @property - def viewport_size(self) -> Tuple[int, int]: - """ - Returns the width and height of the viewport - """ - return self._data.viewport[3:] +class Projector(Protocol): - @property - def viewport_width(self) -> int: - """ - Returns the width of the viewport - """ - return self._data.viewport[3] + def use(self) -> None: + ... - @property - def viewport_height(self) -> int: - """ - Returns the height of the viewport - """ - return self._data.viewport[4] + @contextmanager + def activate(self) -> "Projector": + ... - @property - def viewport(self) -> Tuple[int, int, int, int]: - """ - Returns the Viewport. - """ - return self._data.viewport - @property - def projection(self) -> Tuple[float, float, float, float]: - """ - Returns the projection values used to generate the projection matrix - """ - return self._data.projection +class Projection(Protocol): + near: float + far: float - @property - def position(self) -> Tuple[float, float]: - """ - returns the 2D position of the camera - """ - return self._data.position[:2] - @property - def up(self) -> Tuple[float, float]: - """ - returns the 2d up direction of the camera - """ - return self._data.up[:2] +class Camera(Protocol): + _view: ViewData + _projection: Projection - @property - def scale(self) -> float: - """ - returns the zoom value of the camera - """ - return self._data.scale - def _generate_view_matrix(self) -> Mat4: - """ - Generates a view matrix which always has the z axis at 0, and looks towards the positive z axis. - Ignores the z component of the position, and up vectors. +class OrthographicCamera: + """ + The simplest from of an orthographic camera. + Using ViewData and OrthographicProjectionData PoDs (Pack of Data) + it generates the correct projection and view matrices. It also + provides meths and a context manager for using the matrices in + glsl shaders. + + This class provides no methods for manipulating the PoDs. + + The current implementation will recreate the view and + projection matrices every time the camera is used. + If used every frame or multiple times per frame this may + be inefficient. If you suspect this is causing slowdowns + profile before optimising with a dirty value check. + """ - To protect against unexpected behaviour ensure that the up vector is unit length without the z axis as - it is not normalised before use. - """ - return Mat4.look_at(Vec3(*self.position[:2], 0.0), - Vec3(*self.position[:2], 1.0), - Vec3(*self.up[:2], 0.0)) + def __init__(self, *, + window: Optional["Window"] = None, + view: Optional[ViewData] = None, + projection: Optional[OrthographicProjectionData] = None): + self._window: "Window" = window or get_window() + + self._view = view or ViewData( + (0, 0, self._window.width, self._window.height), # Viewport + (self._window.width / 2, self._window.height / 2, 0), # Position + (0.0, 1.0, 0.0), # Up + (0.0, 0.0, 1.0) # Forward + ) + + self._projection = projection or OrthographicProjectionData( + 0, self._window.width, # Left, Right + 0, self._window.height, # Bottom, Top + -100, 100, # Near, Far + 1.0 # Zoom + ) + + @property + def viewport(self): + return self._view.viewport + + @property + def position(self): + return self._view.position def _generate_projection_matrix(self) -> Mat4: """ - Generates the projection matrix. This uses the values provided by the CameraData.projection tuple. - It is then scaled by the Camera.scale float. It is scaled from the center of the projection values not 0,0. + Using the OrthographicProjectionData a projection matrix is generated where the size of the + objects is not affected by depth. - Generally keep the scale value to integers or negative powers of 2 (0.5, 0.25, etc.) to keep + Generally keep the scale value to integers or negative powers of integers (2^-1, 3^-1, 2^-2, etc.) to keep the pixels uniform in size. Avoid a scale of 0.0. """ # Find the center of the projection values (often 0,0 or the center of the screen) _projection_center = ( - (self._data.projection[0] + self._data.projection[1]) / 2, - (self._data.projection[2] + self._data.projection[3]) / 2 + (self._projection.left + self._projection.right) / 2, + (self._projection.bottom + self._projection.top) / 2 ) # Find half the width of the projection _projection_half_size = ( - (self._data.projection[1] - self._data.projection[0]) / 2, - (self._data.projection[3] - self._data.projection[2]) / 2 + (self._projection.right - self._projection.left) / 2, + (self._projection.top - self._projection.bottom) / 2 ) - # Scale the projection by the scale value. Both the width and the height - # share a scale value to avoid ugly stretching. + # Scale the projection by the zoom value. Both the width and the height + # share a zoom value to avoid ugly stretching. _true_projection = ( - _projection_center[0] - _projection_half_size[0] * self._data.scale, - _projection_center[0] + _projection_half_size[0] * self._data.scale, - _projection_center[1] - _projection_half_size[1] * self._data.scale, - _projection_center[1] + _projection_half_size[1] * self._data.scale + _projection_center[0] - _projection_half_size[0] / self._projection.zoom, + _projection_center[0] + _projection_half_size[0] / self._projection.zoom, + _projection_center[1] - _projection_half_size[1] / self._projection.zoom, + _projection_center[1] + _projection_half_size[1] / self._projection.zoom + ) + return Mat4.orthogonal_projection(*_true_projection, self._projection.near, self._projection.far) + + def _generate_view_matrix(self) -> Mat4: + """ + Using the ViewData it generates a view matrix from the pyglet Mat4 look at function + """ + return Mat4.look_at( + self._view.position, + (self._view.position[0] + self._view.forward[0], self._view.position[1] + self._view.forward[1]), + self._view.up ) - return Mat4.orthogonal_projection(*_true_projection, self.near_plane, self.far_plane) def use(self): """ @@ -183,67 +196,131 @@ def use(self): _projection = self._generate_projection_matrix() _view = self._generate_view_matrix() - self._window.ctx.viewport = self._data.viewport + self._window.ctx.viewport = self._view.viewport self._window.projection = _projection self._window.view = _view @contextmanager - def activate(self): + def activate(self) -> Projector: """ A context manager version of Camera2DOrthographic.use() which allows for the use of `with` blocks. For example, `with camera.activate() as cam: ...`. + + :WARNING: + Currently there is no 'default' camera within arcade. This means this method will raise a value error + as self._window.current_camera is None initially. To solve this issue you only need to make a default + camera and call the use() method. """ - previous_camera = self._window.current_camera + previous_projector = self._window.current_camera try: self.use() yield self finally: - previous_camera.use() + previous_projector.use() -class Camera2DController: +class PerspectiveCamera: """ - + The simplest from of a perspective camera. + Using ViewData and PerspectiveProjectionData PoDs (Pack of Data) + it generates the correct projection and view matrices. It also + provides methods and a context manager for using the matrices in + glsl shaders. + + This class provides no methods for manipulating the PoDs. + + The current implementation will recreate the view and + projection matrices every time the camera is used. + If used every frame or multiple times per frame this may + be inefficient. """ - def __init__(self, *, data: Optional[CameraData] = None, window: Optional[Window] = None): - # A reference to the current active Arcade Window. Used to access the current gl context. - self._window = window or get_window() - - # The camera data used to generate the view and projection matrix. - self._data = data or CameraData( - (0, 0, self._window.width, self._window.height), - (0.0, self._window.width, 0.0, self._window.height), - (self._window.width / 2, self._window.height / 2, 0.0) + + def __init__(self, *, + window: Optional["Window"] = None, + view: Optional[ViewData] = None, + projection: Optional[PerspectiveProjectionData] = None): + self._window: "Window" = window or get_window() + + self._view = view or ViewData( + (0, 0, self._window.width, self._window.height), # Viewport + (self._window.width / 2, self._window.height / 2, 0), # Position + (0.0, 1.0, 0.0), # Up + (0.0, 0.0, 1.0) # Forward ) + self._projection = projection or PerspectiveProjectionData( + self._window.width / self._window.height, # Aspect ratio + 90, # Field of view (degrees) + 0.1, 100, # Near, Far + 1.0 # Zoom. + ) -class SimpleCamera: - """ + @property + def viewport(self): + return self._view.viewport - """ + @property + def position(self): + return self._view.position + + def _generate_projection_matrix(self) -> Mat4: + """ + Using the PerspectiveProjectionData a projection matrix is generated where the size of the + objects is affected by depth. + + The zoom value shrinks the effective fov of the camera. For example a zoom of two will have the + fov resulting in 2x zoom effect. + """ + + _true_fov = self._projection.fov / self._projection.zoom + return Mat4.perspective_projection( + self._projection.aspect, + self._projection.near, + self._projection.far, + _true_fov + ) + + def _generate_view_matrix(self) -> Mat4: + """ + Using the ViewData it generates a view matrix from the pyglet Mat4 look at function + """ + return Mat4.look_at( + self._view.position, + (self._view.position[0] + self._view.forward[0], self._view.position[1] + self._view.forward[1]), + self._view.up + ) + + def use(self): + """ + Sets the active camera to this object. + Then generates the view and projection matrices. + Finally, the gl context viewport is set, as well as the projection and view matrices. + """ + + self._window.current_camera = self + + _projection = self._generate_projection_matrix() + _view = self._generate_view_matrix() + + self._window.ctx.viewport = self._view.viewport + self._window.projection = _projection + self._window.view = _view + + @contextmanager + def activate(self) -> Projector: + """ + A context manager version of Camera2DOrthographic.use() which allows for the use of + `with` blocks. For example, `with camera.activate() as cam: ...`. + + :WARNING: + Currently there is no 'default' camera within arcade. This means this method will raise a value error + as self._window.current_camera is None initially. To solve this issue you only need to make a default + camera and call the use() method. + """ + previous_projector = self._window.current_camera + try: + self.use() + yield self + finally: + previous_projector.use() - def __init__(self, *, data: Optional[CameraData] = None, window: Optional[Window] = None, - viewport: Optional[Tuple[int, int, int, int]] = None, - projection: Optional[Tuple[float, float, float, float]] = None, - position: Optional[Tuple[float, float, float]] = None, - up: Optional[Tuple[float, float, float]] = None, scale: Optional[float] = None): - # A reference to the current active Arcade Window. Used to access the current gl context. - self._window: Window = window or get_window() - - # For backwards compatibility both the new camera data, - # and the old raw tuples are available for initialisation. - # If both are supplied the camera cannot decide which values to use and will raise a ValueError - if any((viewport, projection, up, scale)) and data: - raise ValueError(f"Both the CameraData {data}," - f" and the values {viewport, projection, position, up, scale} have been supplied." - f"Ensure only one of the two is provided.") - - # The camera data used to generate the view and projection matrix. - if any((viewport, projection, up, scale)): - self._data = CameraData(viewport, projection, position, up, scale) - else: - self._data = data or CameraData( - (0, 0, self._window.width, self._window.height), - (0.0, self._window.width, 0.0, self._window.height), - (self._window.width / 2, self._window.height / 2, 0.0) - ) From 369545af455408c27be8b67d8d03b25086789cf8 Mon Sep 17 00:00:00 2001 From: DragonMoffon Date: Fri, 16 Jun 2023 17:16:12 +1200 Subject: [PATCH 04/31] Completed Orthographic Camera. Had some issues with the view matrix, This has been fixed and applied to both of the base cameras. Added a new get map coordinates function (open to change). Placed framework for backwards compatible simple camera. See the #Possible Scene Improvements #Cameras topics in the arcade-dev forum on the Python Arcade discord server for more info. --- arcade/experimental/camera_refactor.py | 234 ++++++++++++++++++++----- 1 file changed, 194 insertions(+), 40 deletions(-) diff --git a/arcade/experimental/camera_refactor.py b/arcade/experimental/camera_refactor.py index 96923d8af9..1c601caba1 100644 --- a/arcade/experimental/camera_refactor.py +++ b/arcade/experimental/camera_refactor.py @@ -1,16 +1,21 @@ -from typing import TYPE_CHECKING, Tuple, Optional, Union, Protocol +from typing import TYPE_CHECKING, Tuple, Optional, Protocol, Union from contextlib import contextmanager from dataclasses import dataclass from arcade.window_commands import get_window -from pyglet.math import Mat4, Vec3 +from pyglet.math import Mat4, Vec3, Vec4, Vec2 if TYPE_CHECKING: from arcade.application import Window -from arcade.application import Window +from arcade import Window + +FourIntTuple = Tuple[int, int, int, int] +FourFloatTuple = Union[Tuple[float, float, float, float], Vec4] +ThreeFloatTuple = Union[Tuple[float, float, float], Vec3] +TwoFloatTuple = Union[Tuple[float, float], Vec2] @dataclass @@ -23,14 +28,20 @@ class ViewData: :param position: A 3D vector which describes where the camera is located. :param up: A 3D vector which describes which direction is up (+y). :param forward: a 3D vector which describes which direction is forwards (+z). + :param zoom: A scaler that records the zoom of the camera. While this most often affects the projection matrix + it allows camera controllers access to the zoom functionality + without interacting with the projection data. """ # Viewport data - viewport: Tuple[int, int, int, int] + viewport: FourIntTuple # View matrix data - position: Tuple[float, float, float] - up: Tuple[float, float, float] - forward: Tuple[float, float, float] + position: ThreeFloatTuple + up: ThreeFloatTuple + forward: ThreeFloatTuple + + # Zoom + zoom: float @dataclass @@ -48,8 +59,6 @@ class OrthographicProjectionData: :param top: The top most value, which gets mapped to y = 1.0 (anything above this value is not visible). :param near: The 'closest' value, which gets mapped to z = -1.0 (anything below this value is not visible). :param far: The 'furthest' value, Which gets mapped to z = 1.0 (anything above this value is not visible). - :param zoom: A scaler which defines how much the Orthographic projection scales by. Is a simpler way of changing the - width and height of the projection. """ left: float right: float @@ -57,7 +66,6 @@ class OrthographicProjectionData: top: float near: float far: float - zoom: float @dataclass @@ -75,7 +83,11 @@ class PerspectiveProjectionData: fov: float near: float far: float - zoom: float + + +class Projection(Protocol): + near: float + far: float class Projector(Protocol): @@ -87,10 +99,8 @@ def use(self) -> None: def activate(self) -> "Projector": ... - -class Projection(Protocol): - near: float - far: float + def get_map_coordinates(self, screen_coordinate: TwoFloatTuple) -> TwoFloatTuple: + ... class Camera(Protocol): @@ -103,7 +113,7 @@ class OrthographicCamera: The simplest from of an orthographic camera. Using ViewData and OrthographicProjectionData PoDs (Pack of Data) it generates the correct projection and view matrices. It also - provides meths and a context manager for using the matrices in + provides methods and a context manager for using the matrices in glsl shaders. This class provides no methods for manipulating the PoDs. @@ -123,18 +133,26 @@ def __init__(self, *, self._view = view or ViewData( (0, 0, self._window.width, self._window.height), # Viewport - (self._window.width / 2, self._window.height / 2, 0), # Position - (0.0, 1.0, 0.0), # Up - (0.0, 0.0, 1.0) # Forward + Vec3(self._window.width / 2, self._window.height / 2, 0), # Position + Vec3(0.0, 1.0, 0.0), # Up + Vec3(0.0, 0.0, 1.0), # Forward + 1.0 # Zoom ) self._projection = projection or OrthographicProjectionData( - 0, self._window.width, # Left, Right - 0, self._window.height, # Bottom, Top + -0.5 * self._window.width, 0.5 * self._window.width, # Left, Right + -0.5 * self._window.height, 0.5 * self._window.height, # Bottom, Top -100, 100, # Near, Far - 1.0 # Zoom ) + @property + def view(self): + return self._view + + @property + def projection(self): + return self._projection + @property def viewport(self): return self._view.viewport @@ -167,10 +185,10 @@ def _generate_projection_matrix(self) -> Mat4: # Scale the projection by the zoom value. Both the width and the height # share a zoom value to avoid ugly stretching. _true_projection = ( - _projection_center[0] - _projection_half_size[0] / self._projection.zoom, - _projection_center[0] + _projection_half_size[0] / self._projection.zoom, - _projection_center[1] - _projection_half_size[1] / self._projection.zoom, - _projection_center[1] + _projection_half_size[1] / self._projection.zoom + _projection_center[0] - _projection_half_size[0] / self._view.zoom, + _projection_center[0] + _projection_half_size[0] / self._view.zoom, + _projection_center[1] - _projection_half_size[1] / self._view.zoom, + _projection_center[1] + _projection_half_size[1] / self._view.zoom ) return Mat4.orthogonal_projection(*_true_projection, self._projection.near, self._projection.far) @@ -178,11 +196,17 @@ def _generate_view_matrix(self) -> Mat4: """ Using the ViewData it generates a view matrix from the pyglet Mat4 look at function """ - return Mat4.look_at( - self._view.position, - (self._view.position[0] + self._view.forward[0], self._view.position[1] + self._view.forward[1]), - self._view.up - ) + fo = Vec3(*self._view.forward).normalize() # Forward Vector + up = Vec3(*self._view.up).normalize() # Initial Up Vector (Not perfectly aligned to forward vector) + ri = fo.cross(up) # Right Vector + up = ri.cross(fo) # Up Vector + po = Vec3(*self._view.position) + return Mat4(( + ri.x, up.x, fo.x, 0, + ri.y, up.y, fo.y, 0, + ri.z, up.z, fo.z, 0, + -ri.dot(po), -up.dot(po), -fo.dot(po), 1 + )) def use(self): """ @@ -218,6 +242,23 @@ def activate(self) -> Projector: finally: previous_projector.use() + def get_map_coordinates(self, screen_coordinate: TwoFloatTuple) -> TwoFloatTuple: + """ + Maps a screen position to a pixel position. + """ + + screen_x = 2.0 * (screen_coordinate[0] - self._view.viewport[0]) / self._view.viewport[2] - 1 + screen_y = 2.0 * (screen_coordinate[1] - self._view.viewport[1]) / self._view.viewport[3] - 1 + + _view = self._generate_view_matrix() + _projection = self._generate_projection_matrix() + + screen_position = Vec4(screen_x, screen_y, 0.0, 1.0) + + _full = ~(_projection @ _view) + + return _full @ screen_position + class PerspectiveCamera: """ @@ -245,14 +286,14 @@ def __init__(self, *, (0, 0, self._window.width, self._window.height), # Viewport (self._window.width / 2, self._window.height / 2, 0), # Position (0.0, 1.0, 0.0), # Up - (0.0, 0.0, 1.0) # Forward + (0.0, 0.0, 1.0), # Forward + 1.0 # Zoom ) self._projection = projection or PerspectiveProjectionData( self._window.width / self._window.height, # Aspect ratio 90, # Field of view (degrees) - 0.1, 100, # Near, Far - 1.0 # Zoom. + 0.1, 100 # Near, Far ) @property @@ -272,7 +313,7 @@ def _generate_projection_matrix(self) -> Mat4: fov resulting in 2x zoom effect. """ - _true_fov = self._projection.fov / self._projection.zoom + _true_fov = self._projection.fov / self._view.zoom return Mat4.perspective_projection( self._projection.aspect, self._projection.near, @@ -284,11 +325,17 @@ def _generate_view_matrix(self) -> Mat4: """ Using the ViewData it generates a view matrix from the pyglet Mat4 look at function """ - return Mat4.look_at( - self._view.position, - (self._view.position[0] + self._view.forward[0], self._view.position[1] + self._view.forward[1]), - self._view.up - ) + fo = Vec3(*self._view.forward).normalize() # Forward Vector + up = Vec3(*self._view.up).normalize() # Initial Up Vector (Not perfectly aligned to forward vector) + ri = fo.cross(up) # Right Vector + up = ri.cross(fo) # Up Vector + po = Vec3(*self._view.position) + return Mat4(( + ri.x, up.x, fo.x, 0, + ri.y, up.y, fo.y, 0, + ri.z, up.z, fo.z, 0, + -ri.dot(po), -up.dot(po), -fo.dot(po), 1 + )) def use(self): """ @@ -324,3 +371,110 @@ def activate(self) -> Projector: finally: previous_projector.use() + def get_map_coordinates(self, screen_coordinate: TwoFloatTuple) -> TwoFloatTuple: + """ + Maps a screen position to a pixel position at the near clipping plane of the camera. + """ + ... + + def get_map_coordinates_at_depth(self, + screen_coordinate: TwoFloatTuple, + depth: float) -> TwoFloatTuple: + """ + Maps a screen position to a pixel position at the specific depth supplied. + """ + ... + + +class SimpleCamera: + """ + A simple camera which uses an orthographic camera and a simple 2D Camera Controller. + It also implements an update method that allows for an interpolation between two points + + Written to be backwards compatible with the old SimpleCamera. + """ + + def __init__(self, *, + window: Optional["Window"] = None, + viewport: Optional[FourIntTuple] = None, + projection: Optional[FourFloatTuple] = None, + position: Optional[TwoFloatTuple] = None, + up: Optional[TwoFloatTuple] = None, + zoom: Optional[float] = None, + near: Optional[float] = None, + far: Optional[float] = None, + view_data: Optional[ViewData] = None, + projection_data: Optional[OrthographicProjectionData] = None + ): + self._window = window or get_window() + + if any((viewport, projection, position, up, zoom, near, far)) and any((view_data, projection_data)): + raise ValueError("Provided both view data or projection data, and raw values." + "You only need to supply one or the other") + + if any((viewport, projection, position, up, zoom, near, far)): + self._view = ViewData( + viewport or (0, 0, self._window.width, self._window.height), + position or (self._window.width / 2, self._window.height / 2, 0.0), + up or (0, 1.0, 0.0), + (0.0, 0.0, 1.0), + zoom or 1.0 + ) + _projection = OrthographicProjectionData( + projection[0] or 0.0, projection[1] or self._window.height, # Left, Right + projection[2] or 0.0, projection[3] or self._window.height, # Bottom, Top + near or -100, far or 100 # Near, Far + ) + else: + self._view = view_data or ViewData( + (0, 0, self._window.width, self._window.height), # Viewport + (self._window.width / 2, self._window.height / 2, 0.0), # Position + (0, 1.0, 0.0), # Up + (0.0, 0.0, 1.0), # Forward + 1.0 # Zoom + ) + _projection = projection_data or OrthographicProjectionData( + 0.0, self._window.width, # Left, Right + 0.0, self._window.height, # Bottom, Top + -100, 100 # Near, Far + ) + + self._camera = OrthographicCamera( + window=self._window, + view=self._view, + projection=_projection + ) + + def use(self): + """ + Sets the active camera to this object. + Then generates the view and projection matrices. + Finally, the gl context viewport is set, as well as the projection and view matrices. + """ + + ... + + @contextmanager + def activate(self) -> Projector: + """ + A context manager version of Camera2DOrthographic.use() which allows for the use of + `with` blocks. For example, `with camera.activate() as cam: ...`. + + :WARNING: + Currently there is no 'default' camera within arcade. This means this method will raise a value error + as self._window.current_camera is None initially. To solve this issue you only need to make a default + camera and call the use() method. + """ + previous_projector = self._window.current_camera + try: + self.use() + yield self + finally: + previous_projector.use() + + def get_map_coordinates(self, screen_coordinate: Tuple[float, float]) -> Tuple[float, float]: + """ + Maps a screen position to a pixel position. + """ + + ... From 1540f607aff693a5c6f562352f5dee84828c8d12 Mon Sep 17 00:00:00 2001 From: DragonMoffon Date: Sun, 18 Jun 2023 02:13:53 +1200 Subject: [PATCH 05/31] Update camera_refactor.py Finished Simple Camera. Is backwards compatible with current Simple Camera implementation. --- arcade/experimental/camera_refactor.py | 289 ++++++++++++++++++++++++- 1 file changed, 278 insertions(+), 11 deletions(-) diff --git a/arcade/experimental/camera_refactor.py b/arcade/experimental/camera_refactor.py index 1c601caba1..7837b8526c 100644 --- a/arcade/experimental/camera_refactor.py +++ b/arcade/experimental/camera_refactor.py @@ -1,5 +1,6 @@ from typing import TYPE_CHECKING, Tuple, Optional, Protocol, Union from contextlib import contextmanager +from math import radians, degrees, cos, sin, atan2, pi from dataclasses import dataclass @@ -375,7 +376,7 @@ def get_map_coordinates(self, screen_coordinate: TwoFloatTuple) -> TwoFloatTuple """ Maps a screen position to a pixel position at the near clipping plane of the camera. """ - ... + # TODO def get_map_coordinates_at_depth(self, screen_coordinate: TwoFloatTuple, @@ -383,7 +384,7 @@ def get_map_coordinates_at_depth(self, """ Maps a screen position to a pixel position at the specific depth supplied. """ - ... + # TODO class SimpleCamera: @@ -409,18 +410,18 @@ def __init__(self, *, self._window = window or get_window() if any((viewport, projection, position, up, zoom, near, far)) and any((view_data, projection_data)): - raise ValueError("Provided both view data or projection data, and raw values." - "You only need to supply one or the other") + raise ValueError("Provided both data structures and raw values." + "Only supply one or the other") if any((viewport, projection, position, up, zoom, near, far)): self._view = ViewData( viewport or (0, 0, self._window.width, self._window.height), - position or (self._window.width / 2, self._window.height / 2, 0.0), + position or (0.0, 0.0, 0.0), up or (0, 1.0, 0.0), (0.0, 0.0, 1.0), zoom or 1.0 ) - _projection = OrthographicProjectionData( + self._projection = OrthographicProjectionData( projection[0] or 0.0, projection[1] or self._window.height, # Left, Right projection[2] or 0.0, projection[3] or self._window.height, # Bottom, Top near or -100, far or 100 # Near, Far @@ -433,7 +434,7 @@ def __init__(self, *, (0.0, 0.0, 1.0), # Forward 1.0 # Zoom ) - _projection = projection_data or OrthographicProjectionData( + self._projection = projection_data or OrthographicProjectionData( 0.0, self._window.width, # Left, Right 0.0, self._window.height, # Bottom, Top -100, 100 # Near, Far @@ -442,17 +443,233 @@ def __init__(self, *, self._camera = OrthographicCamera( window=self._window, view=self._view, - projection=_projection + projection=self._projection + ) + + self._easing_speed = 0.0 + self._position_goal = None + + # Basic properties for modifying the viewport and orthographic projection + + @property + def viewport_width(self) -> int: + """ Returns the width of the viewport """ + return self._view.viewport[2] + + @property + def viewport_height(self) -> int: + """ Returns the height of the viewport """ + return self._view.viewport[3] + + @property + def viewport(self) -> FourIntTuple: + """ The pixel area that will be drawn to while this camera is active (left, bottom, width, height) """ + return self._view.viewport + + @viewport.setter + def viewport(self, viewport: FourIntTuple) -> None: + """ Set the viewport (left, bottom, width, height) """ + self.set_viewport(viewport) + + def set_viewport(self, viewport: FourIntTuple) -> None: + self._view.viewport = viewport + + @property + def projection(self) -> FourFloatTuple: + """ + The dimensions that will be projected to the viewport. (left, right, bottom, top). + """ + return self._projection.left, self._projection.right, self._projection.bottom, self._projection.top + + @projection.setter + def projection(self, projection: FourFloatTuple) -> None: + """ + Update the orthographic projection of the camera. (left, right, bottom, top). + """ + self._projection.left = projection[0] + self._projection.right = projection[1] + self._projection.bottom = projection[2] + self._projection.top = projection[3] + + # Methods for retrieving the viewport - projection ratios. Originally written by Alejandro Casanovas. + @property + def viewport_to_projection_width_ratio(self) -> float: + """ + The ratio of viewport width to projection width. + A value of 1.0 represents that an object that moves one unit will move one pixel. + A value less than one means that one pixel is equivalent to more than one unit (Zoom out). + """ + return (self.viewport_width * self.zoom) / (self._projection.left - self._projection.right) + + @property + def viewport_to_projection_height_ratio(self) -> float: + """ + The ratio of viewport height to projection height. + A value of 1.0 represents that an object that moves one unit will move one pixel. + A value less than one means that one pixel is equivalent to more than one unit (Zoom out). + """ + return (self.viewport_height * self.zoom) / (self._projection.bottom - self._projection.top) + + @property + def projection_to_viewport_width_ratio(self) -> float: + """ + The ratio of projection width to viewport width. + A value of 1.0 represents that an object that moves one unit will move one pixel. + A value less than one means that one pixel is equivalent to less than one unit (Zoom in). + """ + return (self._projection.left - self._projection.right) / (self.zoom * self.viewport_width) + + @property + def projection_to_viewport_height_ratio(self) -> float: + """ + The ratio of projection height to viewport height. + A value of 1.0 represents that an object that moves one unit will move one pixel. + A value less than one means that one pixel is equivalent to less than one unit (Zoom in). + """ + return (self._projection.bottom - self._projection.top) / (self.zoom * self.viewport_height) + + # Control methods (movement, zooming, rotation) + @property + def position(self) -> TwoFloatTuple: + """ + The position of the camera based on the bottom left coordinate. + """ + return self._view.position[0], self._view.position[1] + + @position.setter + def position(self, pos: TwoFloatTuple) -> None: + """ + Set the position of the camera based on the bottom left coordinate. + """ + self._view.position.x = pos[0] + self._view.position.y = pos[1] + + @property + def zoom(self) -> float: + """ + A scaler which adjusts the size of the orthographic projection. + A higher zoom value means larger pixels. + For best results keep the zoom value an integer to an integer or an integer to the power of -1. + """ + return self._view.zoom + + @zoom.setter + def zoom(self, zoom: float) -> None: + """ + A scaler which adjusts the size of the orthographic projection. + A higher zoom value means larger pixels. + For best results keep the zoom value an integer to an integer or an integer to the power of -1. + """ + self._view.zoom = zoom + + @property + def up(self) -> TwoFloatTuple: + """ + A 2D normalised vector which defines which direction corresponds to the +Y axis. + """ + return self._view.up[0], self._view.up[1] + + @up.setter + def up(self, up: TwoFloatTuple) -> None: + """ + A 2D normalised vector which defines which direction corresponds to the +Y axis. + generally easier to use the `rotate` and `rotate_to` methods as they use an angle value. + """ + self._view.up = Vec3(up[0], up[1], 0.0).normalize() + + @property + def angle(self) -> float: + """ + An alternative way of setting the up vector of the camera. + The angle value goes clock-wise starting from (0.0, 1.0). + """ + return degrees(atan2(self.up[0], self.up[1])) + + @angle.setter + def angle(self, angle: float) -> None: + """ + An alternative way of setting the up vector of the camera. + The angle value goes clock-wise starting from (0.0, 1.0). + """ + rad = radians(angle) + self.up = ( + cos(rad), + sin(rad) + ) + + def move_to(self, vector: TwoFloatTuple, speed: float = 1.0) -> None: + """ + Sets the goal position of the camera. + + The camera will lerp towards this position based on the provided speed, + updating its position every time the use() function is called. + + :param Vec2 vector: Vector to move the camera towards. + :param Vec2 speed: How fast to move the camera, 1.0 is instant, 0.1 moves slowly + """ + self._position_goal = Vec2(*vector) + self._easing_speed = speed + + def move(self, vector: TwoFloatTuple) -> None: + """ + Moves the camera with a speed of 1.0, aka instant move + + This is equivalent to calling move_to(my_pos, 1.0) + """ + self.move_to(vector, 1.0) + + def center(self, vector: TwoFloatTuple, speed: float = 1.0) -> None: + """ + Centers the camera. Allows for a linear lerp like the move_to() method. + """ + viewport_center = self.viewport_width / 2, self.viewport_height / 2 + + adjusted_vector = ( + vector[0] * self.viewport_to_projection_width_ratio, + vector[1] * self.viewport_to_projection_height_ratio + ) + + target = ( + adjusted_vector[0] - viewport_center[0], + adjusted_vector[1] - viewport_center[1] ) + self.move_to(target, speed) + + # General Methods + + def update(self): + """ + Update the camera's position. + """ + if self._easing_speed > 0.0: + x_a = self.position[0] + x_b = self._position_goal[0] + + y_a = self.position[1] + y_b = self._position_goal[1] + + self.position = ( + x_a + (x_b - x_a) * self._easing_speed, # Linear Lerp X position + y_a + (y_b - y_a) * self._easing_speed # Linear Lerp Y position + ) + if self.position == self._position_goal: + self._easing_speed = 0.0 + def use(self): """ Sets the active camera to this object. Then generates the view and projection matrices. Finally, the gl context viewport is set, as well as the projection and view matrices. + This method also calls the update method. This can cause the camera to move faster than expected + if the camera is used multiple times in a single frame. """ - ... + # Updated the position + self.update() + + # set matrices + self._camera.use() @contextmanager def activate(self) -> Projector: @@ -472,9 +689,59 @@ def activate(self) -> Projector: finally: previous_projector.use() - def get_map_coordinates(self, screen_coordinate: Tuple[float, float]) -> Tuple[float, float]: + def get_map_coordinates(self, screen_coordinate: Tuple[float, float]) -> TwoFloatTuple: """ Maps a screen position to a pixel position. """ - ... + return self._camera.get_map_coordinates(screen_coordinate) + + +class Camera2D: + """ + A simple orthographic camera. Similar to SimpleCamera, but takes better advantage of the new data structures. + As the Simple Camera is depreciated any new project should use this camera instead. + """ + + +class DefaultProjector: + """ + An extremely limited projector which lacks any kind of control. This is only here to act as the default camera + used internally by arcade. There should be no instance where a developer would want to use this class. + """ + + def __init__(self, *, window: Optional["Window"] = None): + self._window: "Window" = window or get_window() + + self._viewport: FourIntTuple = self._window.viewport + + self._projection_matrix: Mat4 = Mat4() + + def _generate_projection_matrix(self): + left = self._viewport[0] + right = self._viewport[0] + self._viewport[2] + + bottom = self._viewport[1] + top = self._viewport[1] + self._viewport[3] + + self._projection_matrix = Mat4.orthogonal_projection(left, right, bottom, top, -100, 100) + + def use(self): + if self._viewport != self._window.viewport: + self._viewport = self._window.viewport + self._generate_projection_matrix() + + self._window.view = Mat4() + self._window.projection = self._projection_matrix + + @contextmanager + def activate(self) -> Projector: + previous = self._window.current_camera + try: + self.use() + yield self + finally: + previous.use() + + def get_map_coordinates(self, screen_coordinate: TwoFloatTuple) -> TwoFloatTuple: + return screen_coordinate From 23041b2b125bdebaa93a07168e45674341f1d7f2 Mon Sep 17 00:00:00 2001 From: DragonMoffon Date: Sun, 18 Jun 2023 20:04:28 +1200 Subject: [PATCH 06/31] PR cleanup Cleaning up PR to only include camera refactor --- arcade/experimental/background_refactor.py | 18 ------------------ arcade/experimental/camera_refactor.py | 20 ++++++++++++-------- arcade/experimental/scene_refactor.py | 0 3 files changed, 12 insertions(+), 26 deletions(-) delete mode 100644 arcade/experimental/background_refactor.py delete mode 100644 arcade/experimental/scene_refactor.py diff --git a/arcade/experimental/background_refactor.py b/arcade/experimental/background_refactor.py deleted file mode 100644 index e80f41f87d..0000000000 --- a/arcade/experimental/background_refactor.py +++ /dev/null @@ -1,18 +0,0 @@ -from typing import TYPE_CHECKING, Optional - -from arcade.gl import Program, Geometry - -from arcade.experimental.camera_refactor import CameraData - - -class BackgroundTexture: - - def __init__(self): - pass - - -class Background: - - def __init__(self, data: CameraData, texture: BackgroundTexture, color, - shader: Optional[Program] = None, geometry: Optional[Geometry] = None): - pass diff --git a/arcade/experimental/camera_refactor.py b/arcade/experimental/camera_refactor.py index 7837b8526c..d14f4915ba 100644 --- a/arcade/experimental/camera_refactor.py +++ b/arcade/experimental/camera_refactor.py @@ -1,4 +1,4 @@ -from typing import TYPE_CHECKING, Tuple, Optional, Protocol, Union +from typing import TYPE_CHECKING, Tuple, Optional, Protocol, Union, Iterator from contextlib import contextmanager from math import radians, degrees, cos, sin, atan2, pi @@ -97,7 +97,7 @@ def use(self) -> None: ... @contextmanager - def activate(self) -> "Projector": + def activate(self) -> Iterator["Projector"]: ... def get_map_coordinates(self, screen_coordinate: TwoFloatTuple) -> TwoFloatTuple: @@ -226,7 +226,7 @@ def use(self): self._window.view = _view @contextmanager - def activate(self) -> Projector: + def activate(self) -> Iterator[Projector]: """ A context manager version of Camera2DOrthographic.use() which allows for the use of `with` blocks. For example, `with camera.activate() as cam: ...`. @@ -355,7 +355,7 @@ def use(self): self._window.view = _view @contextmanager - def activate(self) -> Projector: + def activate(self) -> Iterator[Projector]: """ A context manager version of Camera2DOrthographic.use() which allows for the use of `with` blocks. For example, `with camera.activate() as cam: ...`. @@ -421,9 +421,13 @@ def __init__(self, *, (0.0, 0.0, 1.0), zoom or 1.0 ) + _projection = projection or ( + 0.0, self._window.width, + 0.0, self._window.height + ) self._projection = OrthographicProjectionData( - projection[0] or 0.0, projection[1] or self._window.height, # Left, Right - projection[2] or 0.0, projection[3] or self._window.height, # Bottom, Top + _projection[0] or 0.0, _projection[1] or self._window.hwidth, # Left, Right + _projection[2] or 0.0, _projection[3] or self._window.height, # Bottom, Top near or -100, far or 100 # Near, Far ) else: @@ -672,7 +676,7 @@ def use(self): self._camera.use() @contextmanager - def activate(self) -> Projector: + def activate(self) -> Iterator[Projector]: """ A context manager version of Camera2DOrthographic.use() which allows for the use of `with` blocks. For example, `with camera.activate() as cam: ...`. @@ -735,7 +739,7 @@ def use(self): self._window.projection = self._projection_matrix @contextmanager - def activate(self) -> Projector: + def activate(self) -> Iterator[Projector]: previous = self._window.current_camera try: self.use() diff --git a/arcade/experimental/scene_refactor.py b/arcade/experimental/scene_refactor.py deleted file mode 100644 index e69de29bb2..0000000000 From dd7531c638f7b3236e479a84e6f87009ccc329f4 Mon Sep 17 00:00:00 2001 From: DragonMoffon Date: Mon, 19 Jun 2023 15:15:26 +1200 Subject: [PATCH 07/31] New Camera Code Integration Moved experimental code into new "cinematic" folder within arcade. Also made the default camera in arcade the "DefaultCamera" class. and made it's type be "Projector" --- arcade/application.py | 12 +- arcade/cinematic/__init__.py | 8 + arcade/cinematic/camera_2D.py | 6 + arcade/cinematic/data.py | 69 +++ arcade/cinematic/default.py | 52 ++ arcade/cinematic/orthographic.py | 163 ++++++ arcade/cinematic/perspective.py | 160 ++++++ arcade/cinematic/simple_camera.py | 326 +++++++++++ arcade/cinematic/simple_controllers.py | 1 + arcade/cinematic/types.py | 28 + arcade/experimental/camera_refactor.py | 751 ------------------------- 11 files changed, 820 insertions(+), 756 deletions(-) create mode 100644 arcade/cinematic/__init__.py create mode 100644 arcade/cinematic/camera_2D.py create mode 100644 arcade/cinematic/data.py create mode 100644 arcade/cinematic/default.py create mode 100644 arcade/cinematic/orthographic.py create mode 100644 arcade/cinematic/perspective.py create mode 100644 arcade/cinematic/simple_camera.py create mode 100644 arcade/cinematic/simple_controllers.py create mode 100644 arcade/cinematic/types.py delete mode 100644 arcade/experimental/camera_refactor.py diff --git a/arcade/application.py b/arcade/application.py index e2d96fc8a8..f1461188fb 100644 --- a/arcade/application.py +++ b/arcade/application.py @@ -22,6 +22,8 @@ from arcade.types import Color, RGBA255, RGBA255OrNormalized from arcade import SectionManager from arcade.utils import is_raspberry_pi +from arcade.cinematic import Projector +from arcade.cinematic.default import DefaultProjector LOG = logging.getLogger(__name__) @@ -201,17 +203,17 @@ def __init__( # self.invalid = False set_window(self) + self._ctx: ArcadeContext = ArcadeContext(self, gc_mode=gc_mode, gl_api=gl_api) + set_viewport(0, self.width, 0, self.height) + self._background_color: Color = TRANSPARENT_BLACK + self._current_view: Optional[View] = None - self.current_camera: Optional[arcade.SimpleCamera] = None + self.current_camera: Optional[Projector] = DefaultProjector(window=self) self.textbox_time = 0.0 self.key: Optional[int] = None self.flip_count: int = 0 self.static_display: bool = False - self._ctx: ArcadeContext = ArcadeContext(self, gc_mode=gc_mode, gl_api=gl_api) - set_viewport(0, self.width, 0, self.height) - self._background_color: Color = TRANSPARENT_BLACK - # See if we should center the window if center_window: self.center_window() diff --git a/arcade/cinematic/__init__.py b/arcade/cinematic/__init__.py new file mode 100644 index 0000000000..00d69731eb --- /dev/null +++ b/arcade/cinematic/__init__.py @@ -0,0 +1,8 @@ +from arcade.cinematic.data import ViewData, OrthographicProjectionData, PerspectiveProjectionData +from arcade.cinematic.types import Projection, Projector, Camera + +from arcade.cinematic.orthographic import OrthographicCamera +from arcade.cinematic.perspective import PerspectiveCamera + +from arcade.cinematic.simple_camera import SimpleCamera +from arcade.cinematic.camera_2D import Camera2D diff --git a/arcade/cinematic/camera_2D.py b/arcade/cinematic/camera_2D.py new file mode 100644 index 0000000000..082e4138a7 --- /dev/null +++ b/arcade/cinematic/camera_2D.py @@ -0,0 +1,6 @@ +# TODO +class Camera2D: + """ + A simple orthographic camera. Similar to SimpleCamera, but takes better advantage of the new data structures. + As the Simple Camera is depreciated any new project should use this camera instead. + """ diff --git a/arcade/cinematic/data.py b/arcade/cinematic/data.py new file mode 100644 index 0000000000..ded814843d --- /dev/null +++ b/arcade/cinematic/data.py @@ -0,0 +1,69 @@ +from typing import Tuple +from dataclasses import dataclass + + +@dataclass +class ViewData: + """ + A PoD (Packet of Data) which holds the necessary data for a functional camera excluding the projection data + + :param viewport: The pixel bounds which will be drawn onto. (left, bottom, width, height) + + :param position: A 3D vector which describes where the camera is located. + :param up: A 3D vector which describes which direction is up (+y). + :param forward: a 3D vector which describes which direction is forwards (+z). + :param zoom: A scaler that records the zoom of the camera. While this most often affects the projection matrix + it allows camera controllers access to the zoom functionality + without interacting with the projection data. + """ + # Viewport data + viewport: Tuple[int, int, int, int] + + # View matrix data + position: Tuple[float, float, float] + up: Tuple[float, float, float] + forward: Tuple[float, float, float] + + # Zoom + zoom: float + + +@dataclass +class OrthographicProjectionData: + """ + A PoD (Packet of Data) which holds the necessary data for a functional Orthographic Projection matrix. + + This is by default a Left-handed system. with the X axis going from left to right, The Y axis going from + bottom to top, and the Z axis going from towards the screen to away from the screen. This can be made + right-handed by making the near value greater than the far value. + + :param left: The left most value, which gets mapped to x = -1.0 (anything below this value is not visible). + :param right: The right most value, which gets mapped to x = 1.0 (anything above this value is not visible). + :param bottom: The bottom most value, which gets mapped to y = -1.0 (anything below this value is not visible). + :param top: The top most value, which gets mapped to y = 1.0 (anything above this value is not visible). + :param near: The 'closest' value, which gets mapped to z = -1.0 (anything below this value is not visible). + :param far: The 'furthest' value, Which gets mapped to z = 1.0 (anything above this value is not visible). + """ + left: float + right: float + bottom: float + top: float + near: float + far: float + + +@dataclass +class PerspectiveProjectionData: + """ + A PoD (Packet of Data) which holds the necessary data for a functional Perspective matrix. + + :param aspect: The aspect ratio of the screen (width over height). + :param fov: The field of view in degrees. With the aspect ratio defines + the size of the projection at any given depth. + :param near: The 'closest' value, which gets mapped to z = -1.0 (anything below this value is not visible). + :param far: The 'furthest' value, Which gets mapped to z = 1.0 (anything above this value is not visible). + """ + aspect: float + fov: float + near: float + far: float diff --git a/arcade/cinematic/default.py b/arcade/cinematic/default.py new file mode 100644 index 0000000000..76f6e8ff97 --- /dev/null +++ b/arcade/cinematic/default.py @@ -0,0 +1,52 @@ +from typing import Optional, Tuple, Iterator, TYPE_CHECKING +from contextlib import contextmanager + +from pyglet.math import Mat4 + +from arcade.cinematic.types import Projector +from arcade.window_commands import get_window +if TYPE_CHECKING: + from arcade.application import Window + + +class DefaultProjector: + """ + An extremely limited projector which lacks any kind of control. This is only here to act as the default camera + used internally by arcade. There should be no instance where a developer would want to use this class. + """ + + def __init__(self, *, window: Optional["Window"] = None): + self._window: "Window" = window or get_window() + + self._viewport: Tuple[int, int, int, int] = self._window.viewport + + self._projection_matrix: Mat4 = Mat4() + + def _generate_projection_matrix(self): + left = self._viewport[0] + right = self._viewport[0] + self._viewport[2] + + bottom = self._viewport[1] + top = self._viewport[1] + self._viewport[3] + + self._projection_matrix = Mat4.orthogonal_projection(left, right, bottom, top, -100, 100) + + def use(self): + if self._viewport != self._window.viewport: + self._viewport = self._window.viewport + self._generate_projection_matrix() + + self._window.view = Mat4() + self._window.projection = self._projection_matrix + + @contextmanager + def activate(self) -> Iterator[Projector]: + previous = self._window.current_camera + try: + self.use() + yield self + finally: + previous.use() + + def get_map_coordinates(self, screen_coordinate: Tuple[float, float]) -> Tuple[float, float]: + return screen_coordinate diff --git a/arcade/cinematic/orthographic.py b/arcade/cinematic/orthographic.py new file mode 100644 index 0000000000..ea83af53d4 --- /dev/null +++ b/arcade/cinematic/orthographic.py @@ -0,0 +1,163 @@ +from typing import Optional, Tuple, Iterator, TYPE_CHECKING +from contextlib import contextmanager + +from pyglet.math import Mat4, Vec3, Vec4 + +from arcade.cinematic.data import ViewData, OrthographicProjectionData +from arcade.cinematic.types import Projector + +from arcade.window_commands import get_window +if TYPE_CHECKING: + from arcade import Window + + +class OrthographicCamera: + """ + The simplest from of an orthographic camera. + Using ViewData and OrthographicProjectionData PoDs (Pack of Data) + it generates the correct projection and view matrices. It also + provides methods and a context manager for using the matrices in + glsl shaders. + + This class provides no methods for manipulating the PoDs. + + The current implementation will recreate the view and + projection matrices every time the camera is used. + If used every frame or multiple times per frame this may + be inefficient. If you suspect this is causing slowdowns + profile before optimising with a dirty value check. + """ + + def __init__(self, *, + window: Optional["Window"] = None, + view: Optional[ViewData] = None, + projection: Optional[OrthographicProjectionData] = None): + self._window: "Window" = window or get_window() + + self._view = view or ViewData( + (0, 0, self._window.width, self._window.height), # Viewport + (self._window.width / 2, self._window.height / 2, 0), # Position + (0.0, 1.0, 0.0), # Up + (0.0, 0.0, 1.0), # Forward + 1.0 # Zoom + ) + + self._projection = projection or OrthographicProjectionData( + -0.5 * self._window.width, 0.5 * self._window.width, # Left, Right + -0.5 * self._window.height, 0.5 * self._window.height, # Bottom, Top + -100, 100, # Near, Far + ) + + @property + def view(self): + return self._view + + @property + def projection(self): + return self._projection + + @property + def viewport(self): + return self._view.viewport + + @property + def position(self): + return self._view.position + + def _generate_projection_matrix(self) -> Mat4: + """ + Using the OrthographicProjectionData a projection matrix is generated where the size of the + objects is not affected by depth. + + Generally keep the scale value to integers or negative powers of integers (2^-1, 3^-1, 2^-2, etc.) to keep + the pixels uniform in size. Avoid a scale of 0.0. + """ + + # Find the center of the projection values (often 0,0 or the center of the screen) + _projection_center = ( + (self._projection.left + self._projection.right) / 2, + (self._projection.bottom + self._projection.top) / 2 + ) + + # Find half the width of the projection + _projection_half_size = ( + (self._projection.right - self._projection.left) / 2, + (self._projection.top - self._projection.bottom) / 2 + ) + + # Scale the projection by the zoom value. Both the width and the height + # share a zoom value to avoid ugly stretching. + _true_projection = ( + _projection_center[0] - _projection_half_size[0] / self._view.zoom, + _projection_center[0] + _projection_half_size[0] / self._view.zoom, + _projection_center[1] - _projection_half_size[1] / self._view.zoom, + _projection_center[1] + _projection_half_size[1] / self._view.zoom + ) + return Mat4.orthogonal_projection(*_true_projection, self._projection.near, self._projection.far) + + def _generate_view_matrix(self) -> Mat4: + """ + Using the ViewData it generates a view matrix from the pyglet Mat4 look at function + """ + fo = Vec3(*self._view.forward).normalize() # Forward Vector + up = Vec3(*self._view.up).normalize() # Initial Up Vector (Not perfectly aligned to forward vector) + ri = fo.cross(up) # Right Vector + up = ri.cross(fo) # Up Vector + po = Vec3(*self._view.position) + return Mat4(( + ri.x, up.x, fo.x, 0, + ri.y, up.y, fo.y, 0, + ri.z, up.z, fo.z, 0, + -ri.dot(po), -up.dot(po), -fo.dot(po), 1 + )) + + def use(self): + """ + Sets the active camera to this object. + Then generates the view and projection matrices. + Finally, the gl context viewport is set, as well as the projection and view matrices. + """ + + self._window.current_camera = self + + _projection = self._generate_projection_matrix() + _view = self._generate_view_matrix() + + self._window.ctx.viewport = self._view.viewport + self._window.projection = _projection + self._window.view = _view + + @contextmanager + def activate(self) -> Iterator[Projector]: + """ + A context manager version of Camera2DOrthographic.use() which allows for the use of + `with` blocks. For example, `with camera.activate() as cam: ...`. + + :WARNING: + Currently there is no 'default' camera within arcade. This means this method will raise a value error + as self._window.current_camera is None initially. To solve this issue you only need to make a default + camera and call the use() method. + """ + previous_projector = self._window.current_camera + try: + self.use() + yield self + finally: + previous_projector.use() + + def get_map_coordinates(self, screen_coordinate: Tuple[float, float]) -> Tuple[float, float]: + """ + Maps a screen position to a pixel position. + """ + + screen_x = 2.0 * (screen_coordinate[0] - self._view.viewport[0]) / self._view.viewport[2] - 1 + screen_y = 2.0 * (screen_coordinate[1] - self._view.viewport[1]) / self._view.viewport[3] - 1 + + _view = self._generate_view_matrix() + _projection = self._generate_projection_matrix() + + screen_position = Vec4(screen_x, screen_y, 0.0, 1.0) + + _full = ~(_projection @ _view) + + return _full @ screen_position diff --git a/arcade/cinematic/perspective.py b/arcade/cinematic/perspective.py new file mode 100644 index 0000000000..fd6c0dc75a --- /dev/null +++ b/arcade/cinematic/perspective.py @@ -0,0 +1,160 @@ +from typing import Optional, Tuple, Iterator, TYPE_CHECKING +from contextlib import contextmanager + +from pyglet.math import Mat4, Vec3, Vec4 + +from arcade.cinematic.data import ViewData, PerspectiveProjectionData +from arcade.cinematic.types import Projector + +from arcade.window_commands import get_window +if TYPE_CHECKING: + from arcade import Window + + +class PerspectiveCamera: + """ + The simplest from of a perspective camera. + Using ViewData and PerspectiveProjectionData PoDs (Pack of Data) + it generates the correct projection and view matrices. It also + provides methods and a context manager for using the matrices in + glsl shaders. + + This class provides no methods for manipulating the PoDs. + + The current implementation will recreate the view and + projection matrices every time the camera is used. + If used every frame or multiple times per frame this may + be inefficient. + """ + + def __init__(self, *, + window: Optional["Window"] = None, + view: Optional[ViewData] = None, + projection: Optional[PerspectiveProjectionData] = None): + self._window: "Window" = window or get_window() + + self._view = view or ViewData( + (0, 0, self._window.width, self._window.height), # Viewport + (self._window.width / 2, self._window.height / 2, 0), # Position + (0.0, 1.0, 0.0), # Up + (0.0, 0.0, 1.0), # Forward + 1.0 # Zoom + ) + + self._projection = projection or PerspectiveProjectionData( + self._window.width / self._window.height, # Aspect ratio + 90, # Field of view (degrees) + 0.1, 100 # Near, Far + ) + + @property + def viewport(self): + return self._view.viewport + + @property + def position(self): + return self._view.position + + def _generate_projection_matrix(self) -> Mat4: + """ + Using the PerspectiveProjectionData a projection matrix is generated where the size of the + objects is affected by depth. + + The zoom value shrinks the effective fov of the camera. For example a zoom of two will have the + fov resulting in 2x zoom effect. + """ + + _true_fov = self._projection.fov / self._view.zoom + return Mat4.perspective_projection( + self._projection.aspect, + self._projection.near, + self._projection.far, + _true_fov + ) + + def _generate_view_matrix(self) -> Mat4: + """ + Using the ViewData it generates a view matrix from the pyglet Mat4 look at function + """ + fo = Vec3(*self._view.forward).normalize() # Forward Vector + up = Vec3(*self._view.up).normalize() # Initial Up Vector (Not perfectly aligned to forward vector) + ri = fo.cross(up) # Right Vector + up = ri.cross(fo) # Up Vector + po = Vec3(*self._view.position) + return Mat4(( + ri.x, up.x, fo.x, 0, + ri.y, up.y, fo.y, 0, + ri.z, up.z, fo.z, 0, + -ri.dot(po), -up.dot(po), -fo.dot(po), 1 + )) + + def use(self): + """ + Sets the active camera to this object. + Then generates the view and projection matrices. + Finally, the gl context viewport is set, as well as the projection and view matrices. + """ + + self._window.current_camera = self + + _projection = self._generate_projection_matrix() + _view = self._generate_view_matrix() + + self._window.ctx.viewport = self._view.viewport + self._window.projection = _projection + self._window.view = _view + + @contextmanager + def activate(self) -> Iterator[Projector]: + """ + A context manager version of Camera2DOrthographic.use() which allows for the use of + `with` blocks. For example, `with camera.activate() as cam: ...`. + + :WARNING: + Currently there is no 'default' camera within arcade. This means this method will raise a value error + as self._window.current_camera is None initially. To solve this issue you only need to make a default + camera and call the use() method. + """ + previous_projector = self._window.current_camera + try: + self.use() + yield self + finally: + previous_projector.use() + + def get_map_coordinates(self, screen_coordinate: Tuple[float, float]) -> Tuple[float, float]: + """ + Maps a screen position to a pixel position at the near clipping plane of the camera. + """ + + screen_x = 2.0 * (screen_coordinate[0] - self._view.viewport[0]) / self._view.viewport[2] - 1 + screen_y = 2.0 * (screen_coordinate[1] - self._view.viewport[1]) / self._view.viewport[3] - 1 + + _view = self._generate_view_matrix() + _projection = self._generate_projection_matrix() + + screen_position = Vec4(screen_x, screen_y, -1.0, 1.0) + + _full = ~(_projection @ _view) + + return _full @ screen_position + + def get_map_coordinates_at_depth(self, + screen_coordinate: Tuple[float, float], + depth: float) -> Tuple[float, float]: + """ + Maps a screen position to a pixel position at the specific depth supplied. + """ + screen_x = 2.0 * (screen_coordinate[0] - self._view.viewport[0]) / self._view.viewport[2] - 1 + screen_y = 2.0 * (screen_coordinate[1] - self._view.viewport[1]) / self._view.viewport[3] - 1 + + _view = self._generate_view_matrix() + _projection = self._generate_projection_matrix() + + _depth = 2.0 * depth / (self._projection.far - self._projection.near) - 1 + + screen_position = Vec4(screen_x, screen_y, _depth, 1.0) + + _full = ~(_projection @ _view) + + return _full @ screen_position diff --git a/arcade/cinematic/simple_camera.py b/arcade/cinematic/simple_camera.py new file mode 100644 index 0000000000..d4852734b7 --- /dev/null +++ b/arcade/cinematic/simple_camera.py @@ -0,0 +1,326 @@ +from typing import Optional, Tuple, Iterator, TYPE_CHECKING +from contextlib import contextmanager +from math import atan2, cos, sin, degrees, radians + +from pyglet.math import Vec3 + +from arcade.cinematic.data import ViewData, OrthographicProjectionData +from arcade.cinematic.types import Projector +from arcade.cinematic.orthographic import OrthographicCamera + +from arcade.window_commands import get_window +if TYPE_CHECKING: + from arcade import Window + + +class SimpleCamera: + """ + A simple camera which uses an orthographic camera and a simple 2D Camera Controller. + It also implements an update method that allows for an interpolation between two points + + Written to be backwards compatible with the old SimpleCamera. + """ + + def __init__(self, *, + window: Optional["Window"] = None, + viewport: Optional[Tuple[int, int, int, int]] = None, + projection: Optional[Tuple[float, float, float, float]] = None, + position: Optional[Tuple[float, float]] = None, + up: Optional[Tuple[float, float]] = None, + zoom: Optional[float] = None, + near: Optional[float] = None, + far: Optional[float] = None, + view_data: Optional[ViewData] = None, + projection_data: Optional[OrthographicProjectionData] = None + ): + self._window = window or get_window() + + if any((viewport, projection, position, up, zoom, near, far)) and any((view_data, projection_data)): + raise ValueError("Provided both data structures and raw values." + "Only supply one or the other") + + if any((viewport, projection, position, up, zoom, near, far)): + self._view = ViewData( + viewport or (0, 0, self._window.width, self._window.height), + position or (0.0, 0.0, 0.0), + up or (0, 1.0, 0.0), + (0.0, 0.0, 1.0), + zoom or 1.0 + ) + _projection = projection or ( + 0.0, self._window.width, + 0.0, self._window.height + ) + self._projection = OrthographicProjectionData( + _projection[0] or 0.0, _projection[1] or self._window.hwidth, # Left, Right + _projection[2] or 0.0, _projection[3] or self._window.height, # Bottom, Top + near or -100, far or 100 # Near, Far + ) + else: + self._view = view_data or ViewData( + (0, 0, self._window.width, self._window.height), # Viewport + (self._window.width / 2, self._window.height / 2, 0.0), # Position + (0, 1.0, 0.0), # Up + (0.0, 0.0, 1.0), # Forward + 1.0 # Zoom + ) + self._projection = projection_data or OrthographicProjectionData( + 0.0, self._window.width, # Left, Right + 0.0, self._window.height, # Bottom, Top + -100, 100 # Near, Far + ) + + self._camera = OrthographicCamera( + window=self._window, + view=self._view, + projection=self._projection + ) + + self._easing_speed = 0.0 + self._position_goal = None + + # Basic properties for modifying the viewport and orthographic projection + + @property + def viewport_width(self) -> int: + """ Returns the width of the viewport """ + return self._view.viewport[2] + + @property + def viewport_height(self) -> int: + """ Returns the height of the viewport """ + return self._view.viewport[3] + + @property + def viewport(self) -> Tuple[int, int, int, int]: + """ The pixel area that will be drawn to while this camera is active (left, bottom, width, height) """ + return self._view.viewport + + @viewport.setter + def viewport(self, viewport: Tuple[int, int, int, int]) -> None: + """ Set the viewport (left, bottom, width, height) """ + self.set_viewport(viewport) + + def set_viewport(self, viewport:Tuple[int, int, int, int]) -> None: + self._view.viewport = viewport + + @property + def projection(self) -> Tuple[float, float, float, float]: + """ + The dimensions that will be projected to the viewport. (left, right, bottom, top). + """ + return self._projection.left, self._projection.right, self._projection.bottom, self._projection.top + + @projection.setter + def projection(self, projection: Tuple[float, float, float, float]) -> None: + """ + Update the orthographic projection of the camera. (left, right, bottom, top). + """ + self._projection.left = projection[0] + self._projection.right = projection[1] + self._projection.bottom = projection[2] + self._projection.top = projection[3] + + # Methods for retrieving the viewport - projection ratios. Originally written by Alejandro Casanovas. + @property + def viewport_to_projection_width_ratio(self) -> float: + """ + The ratio of viewport width to projection width. + A value of 1.0 represents that an object that moves one unit will move one pixel. + A value less than one means that one pixel is equivalent to more than one unit (Zoom out). + """ + return (self.viewport_width * self.zoom) / (self._projection.left - self._projection.right) + + @property + def viewport_to_projection_height_ratio(self) -> float: + """ + The ratio of viewport height to projection height. + A value of 1.0 represents that an object that moves one unit will move one pixel. + A value less than one means that one pixel is equivalent to more than one unit (Zoom out). + """ + return (self.viewport_height * self.zoom) / (self._projection.bottom - self._projection.top) + + @property + def projection_to_viewport_width_ratio(self) -> float: + """ + The ratio of projection width to viewport width. + A value of 1.0 represents that an object that moves one unit will move one pixel. + A value less than one means that one pixel is equivalent to less than one unit (Zoom in). + """ + return (self._projection.left - self._projection.right) / (self.zoom * self.viewport_width) + + @property + def projection_to_viewport_height_ratio(self) -> float: + """ + The ratio of projection height to viewport height. + A value of 1.0 represents that an object that moves one unit will move one pixel. + A value less than one means that one pixel is equivalent to less than one unit (Zoom in). + """ + return (self._projection.bottom - self._projection.top) / (self.zoom * self.viewport_height) + + # Control methods (movement, zooming, rotation) + @property + def position(self) -> Tuple[float, float]: + """ + The position of the camera based on the bottom left coordinate. + """ + return self._view.position[0], self._view.position[1] + + @position.setter + def position(self, pos: Tuple[float, float]) -> None: + """ + Set the position of the camera based on the bottom left coordinate. + """ + self._view.position = (pos[0], pos[1], self._view.position[2]) + + @property + def zoom(self) -> float: + """ + A scaler which adjusts the size of the orthographic projection. + A higher zoom value means larger pixels. + For best results keep the zoom value an integer to an integer or an integer to the power of -1. + """ + return self._view.zoom + + @zoom.setter + def zoom(self, zoom: float) -> None: + """ + A scaler which adjusts the size of the orthographic projection. + A higher zoom value means larger pixels. + For best results keep the zoom value an integer to an integer or an integer to the power of -1. + """ + self._view.zoom = zoom + + @property + def up(self) -> Tuple[float, float]: + """ + A 2D normalised vector which defines which direction corresponds to the +Y axis. + """ + return self._view.up[0], self._view.up[1] + + @up.setter + def up(self, up: Tuple[float, float]) -> None: + """ + A 2D normalised vector which defines which direction corresponds to the +Y axis. + generally easier to use the `rotate` and `rotate_to` methods as they use an angle value. + """ + self._view.up = tuple(Vec3(up[0], up[1], 0.0).normalize()) + + @property + def angle(self) -> float: + """ + An alternative way of setting the up vector of the camera. + The angle value goes clock-wise starting from (0.0, 1.0). + """ + return degrees(atan2(self.up[0], self.up[1])) + + @angle.setter + def angle(self, angle: float) -> None: + """ + An alternative way of setting the up vector of the camera. + The angle value goes clock-wise starting from (0.0, 1.0). + """ + rad = radians(angle) + self.up = ( + cos(rad), + sin(rad) + ) + + def move_to(self, vector: Tuple[float, float], speed: float = 1.0) -> None: + """ + Sets the goal position of the camera. + + The camera will lerp towards this position based on the provided speed, + updating its position every time the use() function is called. + + :param Vec2 vector: Vector to move the camera towards. + :param Vec2 speed: How fast to move the camera, 1.0 is instant, 0.1 moves slowly + """ + self._position_goal = vector + self._easing_speed = speed + + def move(self, vector: Tuple[float, float]) -> None: + """ + Moves the camera with a speed of 1.0, aka instant move + + This is equivalent to calling move_to(my_pos, 1.0) + """ + self.move_to(vector, 1.0) + + def center(self, vector: Tuple[float, float], speed: float = 1.0) -> None: + """ + Centers the camera. Allows for a linear lerp like the move_to() method. + """ + viewport_center = self.viewport_width / 2, self.viewport_height / 2 + + adjusted_vector = ( + vector[0] * self.viewport_to_projection_width_ratio, + vector[1] * self.viewport_to_projection_height_ratio + ) + + target = ( + adjusted_vector[0] - viewport_center[0], + adjusted_vector[1] - viewport_center[1] + ) + + self.move_to(target, speed) + + # General Methods + + def update(self): + """ + Update the camera's position. + """ + if self._easing_speed > 0.0: + x_a = self.position[0] + x_b = self._position_goal[0] + + y_a = self.position[1] + y_b = self._position_goal[1] + + self.position = ( + x_a + (x_b - x_a) * self._easing_speed, # Linear Lerp X position + y_a + (y_b - y_a) * self._easing_speed # Linear Lerp Y position + ) + if self.position == self._position_goal: + self._easing_speed = 0.0 + + def use(self): + """ + Sets the active camera to this object. + Then generates the view and projection matrices. + Finally, the gl context viewport is set, as well as the projection and view matrices. + This method also calls the update method. This can cause the camera to move faster than expected + if the camera is used multiple times in a single frame. + """ + + # Updated the position + self.update() + + # set matrices + self._camera.use() + + @contextmanager + def activate(self) -> Iterator[Projector]: + """ + A context manager version of Camera2DOrthographic.use() which allows for the use of + `with` blocks. For example, `with camera.activate() as cam: ...`. + + :WARNING: + Currently there is no 'default' camera within arcade. This means this method will raise a value error + as self._window.current_camera is None initially. To solve this issue you only need to make a default + camera and call the use() method. + """ + previous_projector = self._window.current_camera + try: + self.use() + yield self + finally: + previous_projector.use() + + def get_map_coordinates(self, screen_coordinate: Tuple[float, float]) -> Tuple[float, float]: + """ + Maps a screen position to a pixel position. + """ + + return self._camera.get_map_coordinates(screen_coordinate) \ No newline at end of file diff --git a/arcade/cinematic/simple_controllers.py b/arcade/cinematic/simple_controllers.py new file mode 100644 index 0000000000..464090415c --- /dev/null +++ b/arcade/cinematic/simple_controllers.py @@ -0,0 +1 @@ +# TODO diff --git a/arcade/cinematic/types.py b/arcade/cinematic/types.py new file mode 100644 index 0000000000..52a7115813 --- /dev/null +++ b/arcade/cinematic/types.py @@ -0,0 +1,28 @@ +from typing import Protocol, Tuple, Iterator +from contextlib import contextmanager + +from arcade.cinematic.data import ViewData, PerspectiveProjectionData, OrthographicProjectionData + + +class Projection(Protocol): + near: float + far: float + + +class Projector(Protocol): + + def use(self) -> None: + ... + + @contextmanager + def activate(self) -> Iterator["Projector"]: + ... + + def get_map_coordinates(self, screen_coordinate: Tuple[float, float]) -> Tuple[float, float]: + ... + + +class Camera(Protocol): + _view: ViewData + _projection: Projection + diff --git a/arcade/experimental/camera_refactor.py b/arcade/experimental/camera_refactor.py deleted file mode 100644 index d14f4915ba..0000000000 --- a/arcade/experimental/camera_refactor.py +++ /dev/null @@ -1,751 +0,0 @@ -from typing import TYPE_CHECKING, Tuple, Optional, Protocol, Union, Iterator -from contextlib import contextmanager -from math import radians, degrees, cos, sin, atan2, pi - -from dataclasses import dataclass - -from arcade.window_commands import get_window - -from pyglet.math import Mat4, Vec3, Vec4, Vec2 - -if TYPE_CHECKING: - from arcade.application import Window - -from arcade import Window - -FourIntTuple = Tuple[int, int, int, int] -FourFloatTuple = Union[Tuple[float, float, float, float], Vec4] -ThreeFloatTuple = Union[Tuple[float, float, float], Vec3] -TwoFloatTuple = Union[Tuple[float, float], Vec2] - - -@dataclass -class ViewData: - """ - A PoD (Packet of Data) which holds the necessary data for a functional camera excluding the projection data - - :param viewport: The pixel bounds which will be drawn onto. (left, bottom, width, height) - - :param position: A 3D vector which describes where the camera is located. - :param up: A 3D vector which describes which direction is up (+y). - :param forward: a 3D vector which describes which direction is forwards (+z). - :param zoom: A scaler that records the zoom of the camera. While this most often affects the projection matrix - it allows camera controllers access to the zoom functionality - without interacting with the projection data. - """ - # Viewport data - viewport: FourIntTuple - - # View matrix data - position: ThreeFloatTuple - up: ThreeFloatTuple - forward: ThreeFloatTuple - - # Zoom - zoom: float - - -@dataclass -class OrthographicProjectionData: - """ - A PoD (Packet of Data) which holds the necessary data for a functional Orthographic Projection matrix. - - This is by default a Left-handed system. with the X axis going from left to right, The Y axis going from - bottom to top, and the Z axis going from towards the screen to away from the screen. This can be made - right-handed by making the near value greater than the far value. - - :param left: The left most value, which gets mapped to x = -1.0 (anything below this value is not visible). - :param right: The right most value, which gets mapped to x = 1.0 (anything above this value is not visible). - :param bottom: The bottom most value, which gets mapped to y = -1.0 (anything below this value is not visible). - :param top: The top most value, which gets mapped to y = 1.0 (anything above this value is not visible). - :param near: The 'closest' value, which gets mapped to z = -1.0 (anything below this value is not visible). - :param far: The 'furthest' value, Which gets mapped to z = 1.0 (anything above this value is not visible). - """ - left: float - right: float - bottom: float - top: float - near: float - far: float - - -@dataclass -class PerspectiveProjectionData: - """ - A PoD (Packet of Data) which holds the necessary data for a functional Perspective matrix. - - :param aspect: The aspect ratio of the screen (width over height). - :param fov: The field of view in degrees. With the aspect ratio defines - the size of the projection at any given depth. - :param near: The 'closest' value, which gets mapped to z = -1.0 (anything below this value is not visible). - :param far: The 'furthest' value, Which gets mapped to z = 1.0 (anything above this value is not visible). - """ - aspect: float - fov: float - near: float - far: float - - -class Projection(Protocol): - near: float - far: float - - -class Projector(Protocol): - - def use(self) -> None: - ... - - @contextmanager - def activate(self) -> Iterator["Projector"]: - ... - - def get_map_coordinates(self, screen_coordinate: TwoFloatTuple) -> TwoFloatTuple: - ... - - -class Camera(Protocol): - _view: ViewData - _projection: Projection - - -class OrthographicCamera: - """ - The simplest from of an orthographic camera. - Using ViewData and OrthographicProjectionData PoDs (Pack of Data) - it generates the correct projection and view matrices. It also - provides methods and a context manager for using the matrices in - glsl shaders. - - This class provides no methods for manipulating the PoDs. - - The current implementation will recreate the view and - projection matrices every time the camera is used. - If used every frame or multiple times per frame this may - be inefficient. If you suspect this is causing slowdowns - profile before optimising with a dirty value check. - """ - - def __init__(self, *, - window: Optional["Window"] = None, - view: Optional[ViewData] = None, - projection: Optional[OrthographicProjectionData] = None): - self._window: "Window" = window or get_window() - - self._view = view or ViewData( - (0, 0, self._window.width, self._window.height), # Viewport - Vec3(self._window.width / 2, self._window.height / 2, 0), # Position - Vec3(0.0, 1.0, 0.0), # Up - Vec3(0.0, 0.0, 1.0), # Forward - 1.0 # Zoom - ) - - self._projection = projection or OrthographicProjectionData( - -0.5 * self._window.width, 0.5 * self._window.width, # Left, Right - -0.5 * self._window.height, 0.5 * self._window.height, # Bottom, Top - -100, 100, # Near, Far - ) - - @property - def view(self): - return self._view - - @property - def projection(self): - return self._projection - - @property - def viewport(self): - return self._view.viewport - - @property - def position(self): - return self._view.position - - def _generate_projection_matrix(self) -> Mat4: - """ - Using the OrthographicProjectionData a projection matrix is generated where the size of the - objects is not affected by depth. - - Generally keep the scale value to integers or negative powers of integers (2^-1, 3^-1, 2^-2, etc.) to keep - the pixels uniform in size. Avoid a scale of 0.0. - """ - - # Find the center of the projection values (often 0,0 or the center of the screen) - _projection_center = ( - (self._projection.left + self._projection.right) / 2, - (self._projection.bottom + self._projection.top) / 2 - ) - - # Find half the width of the projection - _projection_half_size = ( - (self._projection.right - self._projection.left) / 2, - (self._projection.top - self._projection.bottom) / 2 - ) - - # Scale the projection by the zoom value. Both the width and the height - # share a zoom value to avoid ugly stretching. - _true_projection = ( - _projection_center[0] - _projection_half_size[0] / self._view.zoom, - _projection_center[0] + _projection_half_size[0] / self._view.zoom, - _projection_center[1] - _projection_half_size[1] / self._view.zoom, - _projection_center[1] + _projection_half_size[1] / self._view.zoom - ) - return Mat4.orthogonal_projection(*_true_projection, self._projection.near, self._projection.far) - - def _generate_view_matrix(self) -> Mat4: - """ - Using the ViewData it generates a view matrix from the pyglet Mat4 look at function - """ - fo = Vec3(*self._view.forward).normalize() # Forward Vector - up = Vec3(*self._view.up).normalize() # Initial Up Vector (Not perfectly aligned to forward vector) - ri = fo.cross(up) # Right Vector - up = ri.cross(fo) # Up Vector - po = Vec3(*self._view.position) - return Mat4(( - ri.x, up.x, fo.x, 0, - ri.y, up.y, fo.y, 0, - ri.z, up.z, fo.z, 0, - -ri.dot(po), -up.dot(po), -fo.dot(po), 1 - )) - - def use(self): - """ - Sets the active camera to this object. - Then generates the view and projection matrices. - Finally, the gl context viewport is set, as well as the projection and view matrices. - """ - - self._window.current_camera = self - - _projection = self._generate_projection_matrix() - _view = self._generate_view_matrix() - - self._window.ctx.viewport = self._view.viewport - self._window.projection = _projection - self._window.view = _view - - @contextmanager - def activate(self) -> Iterator[Projector]: - """ - A context manager version of Camera2DOrthographic.use() which allows for the use of - `with` blocks. For example, `with camera.activate() as cam: ...`. - - :WARNING: - Currently there is no 'default' camera within arcade. This means this method will raise a value error - as self._window.current_camera is None initially. To solve this issue you only need to make a default - camera and call the use() method. - """ - previous_projector = self._window.current_camera - try: - self.use() - yield self - finally: - previous_projector.use() - - def get_map_coordinates(self, screen_coordinate: TwoFloatTuple) -> TwoFloatTuple: - """ - Maps a screen position to a pixel position. - """ - - screen_x = 2.0 * (screen_coordinate[0] - self._view.viewport[0]) / self._view.viewport[2] - 1 - screen_y = 2.0 * (screen_coordinate[1] - self._view.viewport[1]) / self._view.viewport[3] - 1 - - _view = self._generate_view_matrix() - _projection = self._generate_projection_matrix() - - screen_position = Vec4(screen_x, screen_y, 0.0, 1.0) - - _full = ~(_projection @ _view) - - return _full @ screen_position - - -class PerspectiveCamera: - """ - The simplest from of a perspective camera. - Using ViewData and PerspectiveProjectionData PoDs (Pack of Data) - it generates the correct projection and view matrices. It also - provides methods and a context manager for using the matrices in - glsl shaders. - - This class provides no methods for manipulating the PoDs. - - The current implementation will recreate the view and - projection matrices every time the camera is used. - If used every frame or multiple times per frame this may - be inefficient. - """ - - def __init__(self, *, - window: Optional["Window"] = None, - view: Optional[ViewData] = None, - projection: Optional[PerspectiveProjectionData] = None): - self._window: "Window" = window or get_window() - - self._view = view or ViewData( - (0, 0, self._window.width, self._window.height), # Viewport - (self._window.width / 2, self._window.height / 2, 0), # Position - (0.0, 1.0, 0.0), # Up - (0.0, 0.0, 1.0), # Forward - 1.0 # Zoom - ) - - self._projection = projection or PerspectiveProjectionData( - self._window.width / self._window.height, # Aspect ratio - 90, # Field of view (degrees) - 0.1, 100 # Near, Far - ) - - @property - def viewport(self): - return self._view.viewport - - @property - def position(self): - return self._view.position - - def _generate_projection_matrix(self) -> Mat4: - """ - Using the PerspectiveProjectionData a projection matrix is generated where the size of the - objects is affected by depth. - - The zoom value shrinks the effective fov of the camera. For example a zoom of two will have the - fov resulting in 2x zoom effect. - """ - - _true_fov = self._projection.fov / self._view.zoom - return Mat4.perspective_projection( - self._projection.aspect, - self._projection.near, - self._projection.far, - _true_fov - ) - - def _generate_view_matrix(self) -> Mat4: - """ - Using the ViewData it generates a view matrix from the pyglet Mat4 look at function - """ - fo = Vec3(*self._view.forward).normalize() # Forward Vector - up = Vec3(*self._view.up).normalize() # Initial Up Vector (Not perfectly aligned to forward vector) - ri = fo.cross(up) # Right Vector - up = ri.cross(fo) # Up Vector - po = Vec3(*self._view.position) - return Mat4(( - ri.x, up.x, fo.x, 0, - ri.y, up.y, fo.y, 0, - ri.z, up.z, fo.z, 0, - -ri.dot(po), -up.dot(po), -fo.dot(po), 1 - )) - - def use(self): - """ - Sets the active camera to this object. - Then generates the view and projection matrices. - Finally, the gl context viewport is set, as well as the projection and view matrices. - """ - - self._window.current_camera = self - - _projection = self._generate_projection_matrix() - _view = self._generate_view_matrix() - - self._window.ctx.viewport = self._view.viewport - self._window.projection = _projection - self._window.view = _view - - @contextmanager - def activate(self) -> Iterator[Projector]: - """ - A context manager version of Camera2DOrthographic.use() which allows for the use of - `with` blocks. For example, `with camera.activate() as cam: ...`. - - :WARNING: - Currently there is no 'default' camera within arcade. This means this method will raise a value error - as self._window.current_camera is None initially. To solve this issue you only need to make a default - camera and call the use() method. - """ - previous_projector = self._window.current_camera - try: - self.use() - yield self - finally: - previous_projector.use() - - def get_map_coordinates(self, screen_coordinate: TwoFloatTuple) -> TwoFloatTuple: - """ - Maps a screen position to a pixel position at the near clipping plane of the camera. - """ - # TODO - - def get_map_coordinates_at_depth(self, - screen_coordinate: TwoFloatTuple, - depth: float) -> TwoFloatTuple: - """ - Maps a screen position to a pixel position at the specific depth supplied. - """ - # TODO - - -class SimpleCamera: - """ - A simple camera which uses an orthographic camera and a simple 2D Camera Controller. - It also implements an update method that allows for an interpolation between two points - - Written to be backwards compatible with the old SimpleCamera. - """ - - def __init__(self, *, - window: Optional["Window"] = None, - viewport: Optional[FourIntTuple] = None, - projection: Optional[FourFloatTuple] = None, - position: Optional[TwoFloatTuple] = None, - up: Optional[TwoFloatTuple] = None, - zoom: Optional[float] = None, - near: Optional[float] = None, - far: Optional[float] = None, - view_data: Optional[ViewData] = None, - projection_data: Optional[OrthographicProjectionData] = None - ): - self._window = window or get_window() - - if any((viewport, projection, position, up, zoom, near, far)) and any((view_data, projection_data)): - raise ValueError("Provided both data structures and raw values." - "Only supply one or the other") - - if any((viewport, projection, position, up, zoom, near, far)): - self._view = ViewData( - viewport or (0, 0, self._window.width, self._window.height), - position or (0.0, 0.0, 0.0), - up or (0, 1.0, 0.0), - (0.0, 0.0, 1.0), - zoom or 1.0 - ) - _projection = projection or ( - 0.0, self._window.width, - 0.0, self._window.height - ) - self._projection = OrthographicProjectionData( - _projection[0] or 0.0, _projection[1] or self._window.hwidth, # Left, Right - _projection[2] or 0.0, _projection[3] or self._window.height, # Bottom, Top - near or -100, far or 100 # Near, Far - ) - else: - self._view = view_data or ViewData( - (0, 0, self._window.width, self._window.height), # Viewport - (self._window.width / 2, self._window.height / 2, 0.0), # Position - (0, 1.0, 0.0), # Up - (0.0, 0.0, 1.0), # Forward - 1.0 # Zoom - ) - self._projection = projection_data or OrthographicProjectionData( - 0.0, self._window.width, # Left, Right - 0.0, self._window.height, # Bottom, Top - -100, 100 # Near, Far - ) - - self._camera = OrthographicCamera( - window=self._window, - view=self._view, - projection=self._projection - ) - - self._easing_speed = 0.0 - self._position_goal = None - - # Basic properties for modifying the viewport and orthographic projection - - @property - def viewport_width(self) -> int: - """ Returns the width of the viewport """ - return self._view.viewport[2] - - @property - def viewport_height(self) -> int: - """ Returns the height of the viewport """ - return self._view.viewport[3] - - @property - def viewport(self) -> FourIntTuple: - """ The pixel area that will be drawn to while this camera is active (left, bottom, width, height) """ - return self._view.viewport - - @viewport.setter - def viewport(self, viewport: FourIntTuple) -> None: - """ Set the viewport (left, bottom, width, height) """ - self.set_viewport(viewport) - - def set_viewport(self, viewport: FourIntTuple) -> None: - self._view.viewport = viewport - - @property - def projection(self) -> FourFloatTuple: - """ - The dimensions that will be projected to the viewport. (left, right, bottom, top). - """ - return self._projection.left, self._projection.right, self._projection.bottom, self._projection.top - - @projection.setter - def projection(self, projection: FourFloatTuple) -> None: - """ - Update the orthographic projection of the camera. (left, right, bottom, top). - """ - self._projection.left = projection[0] - self._projection.right = projection[1] - self._projection.bottom = projection[2] - self._projection.top = projection[3] - - # Methods for retrieving the viewport - projection ratios. Originally written by Alejandro Casanovas. - @property - def viewport_to_projection_width_ratio(self) -> float: - """ - The ratio of viewport width to projection width. - A value of 1.0 represents that an object that moves one unit will move one pixel. - A value less than one means that one pixel is equivalent to more than one unit (Zoom out). - """ - return (self.viewport_width * self.zoom) / (self._projection.left - self._projection.right) - - @property - def viewport_to_projection_height_ratio(self) -> float: - """ - The ratio of viewport height to projection height. - A value of 1.0 represents that an object that moves one unit will move one pixel. - A value less than one means that one pixel is equivalent to more than one unit (Zoom out). - """ - return (self.viewport_height * self.zoom) / (self._projection.bottom - self._projection.top) - - @property - def projection_to_viewport_width_ratio(self) -> float: - """ - The ratio of projection width to viewport width. - A value of 1.0 represents that an object that moves one unit will move one pixel. - A value less than one means that one pixel is equivalent to less than one unit (Zoom in). - """ - return (self._projection.left - self._projection.right) / (self.zoom * self.viewport_width) - - @property - def projection_to_viewport_height_ratio(self) -> float: - """ - The ratio of projection height to viewport height. - A value of 1.0 represents that an object that moves one unit will move one pixel. - A value less than one means that one pixel is equivalent to less than one unit (Zoom in). - """ - return (self._projection.bottom - self._projection.top) / (self.zoom * self.viewport_height) - - # Control methods (movement, zooming, rotation) - @property - def position(self) -> TwoFloatTuple: - """ - The position of the camera based on the bottom left coordinate. - """ - return self._view.position[0], self._view.position[1] - - @position.setter - def position(self, pos: TwoFloatTuple) -> None: - """ - Set the position of the camera based on the bottom left coordinate. - """ - self._view.position.x = pos[0] - self._view.position.y = pos[1] - - @property - def zoom(self) -> float: - """ - A scaler which adjusts the size of the orthographic projection. - A higher zoom value means larger pixels. - For best results keep the zoom value an integer to an integer or an integer to the power of -1. - """ - return self._view.zoom - - @zoom.setter - def zoom(self, zoom: float) -> None: - """ - A scaler which adjusts the size of the orthographic projection. - A higher zoom value means larger pixels. - For best results keep the zoom value an integer to an integer or an integer to the power of -1. - """ - self._view.zoom = zoom - - @property - def up(self) -> TwoFloatTuple: - """ - A 2D normalised vector which defines which direction corresponds to the +Y axis. - """ - return self._view.up[0], self._view.up[1] - - @up.setter - def up(self, up: TwoFloatTuple) -> None: - """ - A 2D normalised vector which defines which direction corresponds to the +Y axis. - generally easier to use the `rotate` and `rotate_to` methods as they use an angle value. - """ - self._view.up = Vec3(up[0], up[1], 0.0).normalize() - - @property - def angle(self) -> float: - """ - An alternative way of setting the up vector of the camera. - The angle value goes clock-wise starting from (0.0, 1.0). - """ - return degrees(atan2(self.up[0], self.up[1])) - - @angle.setter - def angle(self, angle: float) -> None: - """ - An alternative way of setting the up vector of the camera. - The angle value goes clock-wise starting from (0.0, 1.0). - """ - rad = radians(angle) - self.up = ( - cos(rad), - sin(rad) - ) - - def move_to(self, vector: TwoFloatTuple, speed: float = 1.0) -> None: - """ - Sets the goal position of the camera. - - The camera will lerp towards this position based on the provided speed, - updating its position every time the use() function is called. - - :param Vec2 vector: Vector to move the camera towards. - :param Vec2 speed: How fast to move the camera, 1.0 is instant, 0.1 moves slowly - """ - self._position_goal = Vec2(*vector) - self._easing_speed = speed - - def move(self, vector: TwoFloatTuple) -> None: - """ - Moves the camera with a speed of 1.0, aka instant move - - This is equivalent to calling move_to(my_pos, 1.0) - """ - self.move_to(vector, 1.0) - - def center(self, vector: TwoFloatTuple, speed: float = 1.0) -> None: - """ - Centers the camera. Allows for a linear lerp like the move_to() method. - """ - viewport_center = self.viewport_width / 2, self.viewport_height / 2 - - adjusted_vector = ( - vector[0] * self.viewport_to_projection_width_ratio, - vector[1] * self.viewport_to_projection_height_ratio - ) - - target = ( - adjusted_vector[0] - viewport_center[0], - adjusted_vector[1] - viewport_center[1] - ) - - self.move_to(target, speed) - - # General Methods - - def update(self): - """ - Update the camera's position. - """ - if self._easing_speed > 0.0: - x_a = self.position[0] - x_b = self._position_goal[0] - - y_a = self.position[1] - y_b = self._position_goal[1] - - self.position = ( - x_a + (x_b - x_a) * self._easing_speed, # Linear Lerp X position - y_a + (y_b - y_a) * self._easing_speed # Linear Lerp Y position - ) - if self.position == self._position_goal: - self._easing_speed = 0.0 - - def use(self): - """ - Sets the active camera to this object. - Then generates the view and projection matrices. - Finally, the gl context viewport is set, as well as the projection and view matrices. - This method also calls the update method. This can cause the camera to move faster than expected - if the camera is used multiple times in a single frame. - """ - - # Updated the position - self.update() - - # set matrices - self._camera.use() - - @contextmanager - def activate(self) -> Iterator[Projector]: - """ - A context manager version of Camera2DOrthographic.use() which allows for the use of - `with` blocks. For example, `with camera.activate() as cam: ...`. - - :WARNING: - Currently there is no 'default' camera within arcade. This means this method will raise a value error - as self._window.current_camera is None initially. To solve this issue you only need to make a default - camera and call the use() method. - """ - previous_projector = self._window.current_camera - try: - self.use() - yield self - finally: - previous_projector.use() - - def get_map_coordinates(self, screen_coordinate: Tuple[float, float]) -> TwoFloatTuple: - """ - Maps a screen position to a pixel position. - """ - - return self._camera.get_map_coordinates(screen_coordinate) - - -class Camera2D: - """ - A simple orthographic camera. Similar to SimpleCamera, but takes better advantage of the new data structures. - As the Simple Camera is depreciated any new project should use this camera instead. - """ - - -class DefaultProjector: - """ - An extremely limited projector which lacks any kind of control. This is only here to act as the default camera - used internally by arcade. There should be no instance where a developer would want to use this class. - """ - - def __init__(self, *, window: Optional["Window"] = None): - self._window: "Window" = window or get_window() - - self._viewport: FourIntTuple = self._window.viewport - - self._projection_matrix: Mat4 = Mat4() - - def _generate_projection_matrix(self): - left = self._viewport[0] - right = self._viewport[0] + self._viewport[2] - - bottom = self._viewport[1] - top = self._viewport[1] + self._viewport[3] - - self._projection_matrix = Mat4.orthogonal_projection(left, right, bottom, top, -100, 100) - - def use(self): - if self._viewport != self._window.viewport: - self._viewport = self._window.viewport - self._generate_projection_matrix() - - self._window.view = Mat4() - self._window.projection = self._projection_matrix - - @contextmanager - def activate(self) -> Iterator[Projector]: - previous = self._window.current_camera - try: - self.use() - yield self - finally: - previous.use() - - def get_map_coordinates(self, screen_coordinate: TwoFloatTuple) -> TwoFloatTuple: - return screen_coordinate From 7bcc802fd3ac460f968cfb6ce06b34e0330db3f6 Mon Sep 17 00:00:00 2001 From: DragonMoffon Date: Mon, 19 Jun 2023 15:24:32 +1200 Subject: [PATCH 08/31] Code inspection Clean up --- arcade/application.py | 2 +- arcade/camera.py | 13 ++++++++++++- arcade/cinematic/simple_camera.py | 13 ++++++++----- 3 files changed, 21 insertions(+), 7 deletions(-) diff --git a/arcade/application.py b/arcade/application.py index f1461188fb..3f84cd6c85 100644 --- a/arcade/application.py +++ b/arcade/application.py @@ -208,7 +208,7 @@ def __init__( self._background_color: Color = TRANSPARENT_BLACK self._current_view: Optional[View] = None - self.current_camera: Optional[Projector] = DefaultProjector(window=self) + self.current_camera: Projector = DefaultProjector(window=self) self.textbox_time = 0.0 self.key: Optional[int] = None self.flip_count: int = 0 diff --git a/arcade/camera.py b/arcade/camera.py index 4fdaabf246..193bfab70c 100644 --- a/arcade/camera.py +++ b/arcade/camera.py @@ -2,12 +2,14 @@ Camera class """ import math -from typing import TYPE_CHECKING, List, Optional, Tuple, Union +from typing import TYPE_CHECKING, List, Optional, Tuple, Union, Iterator +from contextlib import contextmanager from pyglet.math import Mat4, Vec2, Vec3 import arcade from arcade.types import Point +from arcade.cinematic.types import Projector from arcade.math import get_distance if TYPE_CHECKING: @@ -263,6 +265,15 @@ def use(self) -> None: self._window.projection = self._combined_matrix # sets projection position and zoom self._window.view = Mat4() # Set to identity matrix for now + @contextmanager + def activate(self) -> Iterator[Projector]: + previous_camera = self._window.current_camera + try: + self.use() + yield self + finally: + previous_camera.use() + class Camera(SimpleCamera): """ diff --git a/arcade/cinematic/simple_camera.py b/arcade/cinematic/simple_camera.py index d4852734b7..f88446827b 100644 --- a/arcade/cinematic/simple_camera.py +++ b/arcade/cinematic/simple_camera.py @@ -40,10 +40,12 @@ def __init__(self, *, "Only supply one or the other") if any((viewport, projection, position, up, zoom, near, far)): + _pos = position or (0.0, 0.0) + _up = up or (0.0, 1.0) self._view = ViewData( viewport or (0, 0, self._window.width, self._window.height), - position or (0.0, 0.0, 0.0), - up or (0, 1.0, 0.0), + (_pos[0], _pos[1], 0.0), + (_up[0], _up[1], 0.0), (0.0, 0.0, 1.0), zoom or 1.0 ) @@ -76,8 +78,8 @@ def __init__(self, *, projection=self._projection ) - self._easing_speed = 0.0 - self._position_goal = None + self._easing_speed: float = 0.0 + self._position_goal: Tuple[float, float] = self.position # Basic properties for modifying the viewport and orthographic projection @@ -204,7 +206,8 @@ def up(self, up: Tuple[float, float]) -> None: A 2D normalised vector which defines which direction corresponds to the +Y axis. generally easier to use the `rotate` and `rotate_to` methods as they use an angle value. """ - self._view.up = tuple(Vec3(up[0], up[1], 0.0).normalize()) + _up = Vec3(up[0], up[1], 0.0).normalize() + self._view.up = (_up[0], _up[1], _up[2]) @property def angle(self) -> float: From f573ba85643687fd9d0aeeca746eb34de9d62a20 Mon Sep 17 00:00:00 2001 From: DragonMoffon Date: Mon, 19 Jun 2023 15:32:42 +1200 Subject: [PATCH 09/31] Inspection Fix 2 --- arcade/__init__.py | 1 + arcade/cinematic/__init__.py | 18 ++++++++++++++++++ arcade/cinematic/types.py | 2 +- 3 files changed, 20 insertions(+), 1 deletion(-) diff --git a/arcade/__init__.py b/arcade/__init__.py index 2a470690a8..9dbfefd186 100644 --- a/arcade/__init__.py +++ b/arcade/__init__.py @@ -219,6 +219,7 @@ def configure_logging(level: Optional[int] = None): # Module imports from arcade import color as color from arcade import csscolor as csscolor +from arcade import cinematic as cinematic from arcade import key as key from arcade import resources as resources from arcade import types as types diff --git a/arcade/cinematic/__init__.py b/arcade/cinematic/__init__.py index 00d69731eb..33090763d1 100644 --- a/arcade/cinematic/__init__.py +++ b/arcade/cinematic/__init__.py @@ -1,3 +1,8 @@ +""" +The Cinematic Types, Classes, and Methods of Arcade. +Providing a multitude of camera's for any need. +""" + from arcade.cinematic.data import ViewData, OrthographicProjectionData, PerspectiveProjectionData from arcade.cinematic.types import Projection, Projector, Camera @@ -6,3 +11,16 @@ from arcade.cinematic.simple_camera import SimpleCamera from arcade.cinematic.camera_2D import Camera2D + +__all__ = [ + 'Projection', + 'Projector', + 'Camera', + 'ViewData', + 'OrthographicProjectionData', + 'OrthographicCamera', + 'PerspectiveProjectionData', + 'PerspectiveCamera', + 'SimpleCamera', + 'Camera2D' +] diff --git a/arcade/cinematic/types.py b/arcade/cinematic/types.py index 52a7115813..8c27f5314c 100644 --- a/arcade/cinematic/types.py +++ b/arcade/cinematic/types.py @@ -1,7 +1,7 @@ from typing import Protocol, Tuple, Iterator from contextlib import contextmanager -from arcade.cinematic.data import ViewData, PerspectiveProjectionData, OrthographicProjectionData +from arcade.cinematic.data import ViewData class Projection(Protocol): From c741d08c298b5603ba53cf8b325016a2c636fe6e Mon Sep 17 00:00:00 2001 From: DragonMoffon Date: Mon, 19 Jun 2023 15:33:51 +1200 Subject: [PATCH 10/31] Code inspection fix 3 --- arcade/cinematic/simple_camera.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/arcade/cinematic/simple_camera.py b/arcade/cinematic/simple_camera.py index f88446827b..8ab7673831 100644 --- a/arcade/cinematic/simple_camera.py +++ b/arcade/cinematic/simple_camera.py @@ -326,4 +326,4 @@ def get_map_coordinates(self, screen_coordinate: Tuple[float, float]) -> Tuple[f Maps a screen position to a pixel position. """ - return self._camera.get_map_coordinates(screen_coordinate) \ No newline at end of file + return self._camera.get_map_coordinates(screen_coordinate) From d4e6be53753ed7b3e495b7a42aef2d38699673a5 Mon Sep 17 00:00:00 2001 From: DragonMoffon Date: Mon, 19 Jun 2023 15:49:38 +1200 Subject: [PATCH 11/31] Round 4 --- arcade/camera.py | 6 ++++-- arcade/cinematic/orthographic.py | 4 +++- arcade/cinematic/perspective.py | 8 ++++++-- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/arcade/camera.py b/arcade/camera.py index 193bfab70c..39f8aef70b 100644 --- a/arcade/camera.py +++ b/arcade/camera.py @@ -217,13 +217,15 @@ def center(self, vector: Union[Vec2, tuple], speed: float = 1.0) -> None: self.move_to(target, speed) - def get_map_coordinates(self, camera_vector: Union[Vec2, tuple]) -> Vec2: + def get_map_coordinates(self, camera_vector: Union[Vec2, tuple]) -> Tuple[float, float]: """ Returns map coordinates in pixels from screen coordinates based on the camera position :param Vec2 camera_vector: Vector captured from the camera viewport """ - return Vec2(*self.position) + Vec2(*camera_vector) + _mapped_position = Vec2(*self.position) + Vec2(*camera_vector) + + return _mapped_position[0], _mapped_position[1] def resize(self, viewport_width: int, viewport_height: int, *, resize_projection: bool = True) -> None: diff --git a/arcade/cinematic/orthographic.py b/arcade/cinematic/orthographic.py index ea83af53d4..c3a50d6875 100644 --- a/arcade/cinematic/orthographic.py +++ b/arcade/cinematic/orthographic.py @@ -160,4 +160,6 @@ def get_map_coordinates(self, screen_coordinate: Tuple[float, float]) -> Tuple[f _full = ~(_projection @ _view) - return _full @ screen_position + _mapped_position = _full @ screen_position + + return _mapped_position[0], _mapped_position[1] diff --git a/arcade/cinematic/perspective.py b/arcade/cinematic/perspective.py index fd6c0dc75a..634d4cde0d 100644 --- a/arcade/cinematic/perspective.py +++ b/arcade/cinematic/perspective.py @@ -137,7 +137,9 @@ def get_map_coordinates(self, screen_coordinate: Tuple[float, float]) -> Tuple[f _full = ~(_projection @ _view) - return _full @ screen_position + _mapped_position = _full @ screen_position + + return _mapped_position[0], _mapped_position[1] def get_map_coordinates_at_depth(self, screen_coordinate: Tuple[float, float], @@ -157,4 +159,6 @@ def get_map_coordinates_at_depth(self, _full = ~(_projection @ _view) - return _full @ screen_position + _mapped_position = _full @ screen_position + + return _mapped_position[0], _mapped_position[1] From 05b624e7d621095398df11a2c7011157ec80cc4d Mon Sep 17 00:00:00 2001 From: DragonMoffon Date: Fri, 21 Jul 2023 00:24:36 +1200 Subject: [PATCH 12/31] Writitng initial Unit Tests Created files for unit tests, and wrote a few. Started work on Camera2D (replacement for simple camera) --- arcade/application.py | 4 +- arcade/cinematic/__init__.py | 14 +-- arcade/cinematic/camera_2D.py | 6 -- arcade/cinematic/camera_2d.py | 54 +++++++++++ arcade/cinematic/data.py | 4 +- arcade/cinematic/default.py | 2 +- arcade/cinematic/orthographic.py | 26 ++---- arcade/cinematic/perspective.py | 16 ++-- arcade/cinematic/simple_camera.py | 25 +++--- arcade/cinematic/simple_controllers.py | 2 +- arcade/cinematic/types.py | 4 +- tests/unit/camera/test_camera_2d.py | 0 tests/unit/camera/test_camera_controllers.py | 0 tests/unit/camera/test_default_camera.py | 0 tests/unit/camera/test_orthographic_camera.py | 90 +++++++++++++++++++ tests/unit/camera/test_perspective_camera.py | 17 ++++ tests/unit/camera/test_simple_camera.py | 0 17 files changed, 201 insertions(+), 63 deletions(-) delete mode 100644 arcade/cinematic/camera_2D.py create mode 100644 arcade/cinematic/camera_2d.py create mode 100644 tests/unit/camera/test_camera_2d.py create mode 100644 tests/unit/camera/test_camera_controllers.py create mode 100644 tests/unit/camera/test_default_camera.py create mode 100644 tests/unit/camera/test_orthographic_camera.py create mode 100644 tests/unit/camera/test_perspective_camera.py create mode 100644 tests/unit/camera/test_simple_camera.py diff --git a/arcade/application.py b/arcade/application.py index 3f84cd6c85..c5dd573b81 100644 --- a/arcade/application.py +++ b/arcade/application.py @@ -23,7 +23,7 @@ from arcade import SectionManager from arcade.utils import is_raspberry_pi from arcade.cinematic import Projector -from arcade.cinematic.default import DefaultProjector +from arcade.cinematic.default import _DefaultProjector LOG = logging.getLogger(__name__) @@ -208,7 +208,7 @@ def __init__( self._background_color: Color = TRANSPARENT_BLACK self._current_view: Optional[View] = None - self.current_camera: Projector = DefaultProjector(window=self) + self.current_camera: Projector = _DefaultProjector(window=self) self.textbox_time = 0.0 self.key: Optional[int] = None self.flip_count: int = 0 diff --git a/arcade/cinematic/__init__.py b/arcade/cinematic/__init__.py index 33090763d1..5ee302b961 100644 --- a/arcade/cinematic/__init__.py +++ b/arcade/cinematic/__init__.py @@ -3,24 +3,24 @@ Providing a multitude of camera's for any need. """ -from arcade.cinematic.data import ViewData, OrthographicProjectionData, PerspectiveProjectionData +from arcade.cinematic.data import CameraData, OrthographicProjectionData, PerspectiveProjectionData from arcade.cinematic.types import Projection, Projector, Camera -from arcade.cinematic.orthographic import OrthographicCamera -from arcade.cinematic.perspective import PerspectiveCamera +from arcade.cinematic.orthographic import OrthographicProjector +from arcade.cinematic.perspective import PerspectiveProjector from arcade.cinematic.simple_camera import SimpleCamera -from arcade.cinematic.camera_2D import Camera2D +from arcade.cinematic.camera_2d import Camera2D __all__ = [ 'Projection', 'Projector', 'Camera', - 'ViewData', + 'CameraData', 'OrthographicProjectionData', - 'OrthographicCamera', + 'OrthographicProjector', 'PerspectiveProjectionData', - 'PerspectiveCamera', + 'PerspectiveProjector', 'SimpleCamera', 'Camera2D' ] diff --git a/arcade/cinematic/camera_2D.py b/arcade/cinematic/camera_2D.py deleted file mode 100644 index 082e4138a7..0000000000 --- a/arcade/cinematic/camera_2D.py +++ /dev/null @@ -1,6 +0,0 @@ -# TODO -class Camera2D: - """ - A simple orthographic camera. Similar to SimpleCamera, but takes better advantage of the new data structures. - As the Simple Camera is depreciated any new project should use this camera instead. - """ diff --git a/arcade/cinematic/camera_2d.py b/arcade/cinematic/camera_2d.py new file mode 100644 index 0000000000..c1bebc9b88 --- /dev/null +++ b/arcade/cinematic/camera_2d.py @@ -0,0 +1,54 @@ +from typing import Optional, Tuple +from warnings import warn + +from arcade.cinematic.data import CameraData, OrthographicProjectionData + +from arcade.application import Window +from arcade.window_commands import get_window + + +class Camera2D: + """ + A simple orthographic camera. Similar to SimpleCamera, but takes better advantage of the new data structures. + As the Simple Camera is depreciated any new project should use this camera instead. + """ + def __init__(self, *, + window: Optional[Window] = None, + viewport: Optional[Tuple[int, int, int, int]] = None, + position: Optional[Tuple[float, float]] = None, + up: Optional[Tuple[float, float]] = None, + zoom: Optional[float] = None, + projection: Optional[Tuple[float, float, float, float]] = None, + near: Optional[float] = None, + far: Optional[float] = None, + camera_data: Optional[CameraData] = None, + projection_data: Optional[OrthographicProjectionData] = None + ): + self._window = window or get_window() + + if any((viewport, position, up, zoom)) and camera_data: + warn("Camera2D Warning: Provided both a CameraData object and raw values. Defaulting to CameraData.") + + if any((projection, near, far)) and projection_data: + warn("Camera2D Warning: Provided both an OrthographicProjectionData object and raw values." + "Defaulting to OrthographicProjectionData.") + + _pos = position or (self._window.width / 2, self._window.height / 2) + _up = up or (0.0, 1.0) + self._data = camera_data or CameraData( + viewport or (0, 0, self._window.width, self._window.height), + (_pos[0], _pos[1], 0.0), + (_up[0], _up[1], 0.0), + (0.0, 0.0, 1.0), + zoom or 1.0 + ) + + _proj = projection or (-self._window.width/2, self._window.width/2, + -self._window.height/2, self._window.height/2) + self._projection = projection_data or OrthographicProjectionData( + _proj[0], _proj[1], # Left and Right. + _proj[2], _proj[3], # Bottom and Top. + near or 0.0, far or 100.0 # Near and Far. + ) + + diff --git a/arcade/cinematic/data.py b/arcade/cinematic/data.py index ded814843d..b8e0aa0bf3 100644 --- a/arcade/cinematic/data.py +++ b/arcade/cinematic/data.py @@ -3,9 +3,9 @@ @dataclass -class ViewData: +class CameraData: """ - A PoD (Packet of Data) which holds the necessary data for a functional camera excluding the projection data + A PoD (Packet of Data) which holds the necessary data for a functional camera excluding the projection data. :param viewport: The pixel bounds which will be drawn onto. (left, bottom, width, height) diff --git a/arcade/cinematic/default.py b/arcade/cinematic/default.py index 76f6e8ff97..407503d9f8 100644 --- a/arcade/cinematic/default.py +++ b/arcade/cinematic/default.py @@ -9,7 +9,7 @@ from arcade.application import Window -class DefaultProjector: +class _DefaultProjector: """ An extremely limited projector which lacks any kind of control. This is only here to act as the default camera used internally by arcade. There should be no instance where a developer would want to use this class. diff --git a/arcade/cinematic/orthographic.py b/arcade/cinematic/orthographic.py index c3a50d6875..3371dda54a 100644 --- a/arcade/cinematic/orthographic.py +++ b/arcade/cinematic/orthographic.py @@ -3,7 +3,7 @@ from pyglet.math import Mat4, Vec3, Vec4 -from arcade.cinematic.data import ViewData, OrthographicProjectionData +from arcade.cinematic.data import CameraData, OrthographicProjectionData from arcade.cinematic.types import Projector from arcade.window_commands import get_window @@ -11,7 +11,7 @@ from arcade import Window -class OrthographicCamera: +class OrthographicProjector: """ The simplest from of an orthographic camera. Using ViewData and OrthographicProjectionData PoDs (Pack of Data) @@ -30,11 +30,11 @@ class OrthographicCamera: def __init__(self, *, window: Optional["Window"] = None, - view: Optional[ViewData] = None, + view: Optional[CameraData] = None, projection: Optional[OrthographicProjectionData] = None): self._window: "Window" = window or get_window() - self._view = view or ViewData( + self._view = view or CameraData( (0, 0, self._window.width, self._window.height), # Viewport (self._window.width / 2, self._window.height / 2, 0), # Position (0.0, 1.0, 0.0), # Up @@ -49,21 +49,13 @@ def __init__(self, *, ) @property - def view(self): + def view_data(self) -> CameraData: return self._view @property - def projection(self): + def projection_data(self) -> OrthographicProjectionData: return self._projection - @property - def viewport(self): - return self._view.viewport - - @property - def position(self): - return self._view.position - def _generate_projection_matrix(self) -> Mat4: """ Using the OrthographicProjectionData a projection matrix is generated where the size of the @@ -132,11 +124,6 @@ def activate(self) -> Iterator[Projector]: """ A context manager version of Camera2DOrthographic.use() which allows for the use of `with` blocks. For example, `with camera.activate() as cam: ...`. - - :WARNING: - Currently there is no 'default' camera within arcade. This means this method will raise a value error - as self._window.current_camera is None initially. To solve this issue you only need to make a default - camera and call the use() method. """ previous_projector = self._window.current_camera try: @@ -149,6 +136,7 @@ def get_map_coordinates(self, screen_coordinate: Tuple[float, float]) -> Tuple[f """ Maps a screen position to a pixel position. """ + # TODO: better doc string screen_x = 2.0 * (screen_coordinate[0] - self._view.viewport[0]) / self._view.viewport[2] - 1 screen_y = 2.0 * (screen_coordinate[1] - self._view.viewport[1]) / self._view.viewport[3] - 1 diff --git a/arcade/cinematic/perspective.py b/arcade/cinematic/perspective.py index 634d4cde0d..61d8ccc496 100644 --- a/arcade/cinematic/perspective.py +++ b/arcade/cinematic/perspective.py @@ -3,7 +3,7 @@ from pyglet.math import Mat4, Vec3, Vec4 -from arcade.cinematic.data import ViewData, PerspectiveProjectionData +from arcade.cinematic.data import CameraData, PerspectiveProjectionData from arcade.cinematic.types import Projector from arcade.window_commands import get_window @@ -11,7 +11,7 @@ from arcade import Window -class PerspectiveCamera: +class PerspectiveProjector: """ The simplest from of a perspective camera. Using ViewData and PerspectiveProjectionData PoDs (Pack of Data) @@ -29,11 +29,11 @@ class PerspectiveCamera: def __init__(self, *, window: Optional["Window"] = None, - view: Optional[ViewData] = None, + view: Optional[CameraData] = None, projection: Optional[PerspectiveProjectionData] = None): self._window: "Window" = window or get_window() - self._view = view or ViewData( + self._view = view or CameraData( (0, 0, self._window.width, self._window.height), # Viewport (self._window.width / 2, self._window.height / 2, 0), # Position (0.0, 1.0, 0.0), # Up @@ -48,12 +48,12 @@ def __init__(self, *, ) @property - def viewport(self): - return self._view.viewport + def view(self) -> CameraData: + return self._view @property - def position(self): - return self._view.position + def projection(self) -> PerspectiveProjectionData: + return self._projection def _generate_projection_matrix(self) -> Mat4: """ diff --git a/arcade/cinematic/simple_camera.py b/arcade/cinematic/simple_camera.py index 8ab7673831..36262883ca 100644 --- a/arcade/cinematic/simple_camera.py +++ b/arcade/cinematic/simple_camera.py @@ -1,16 +1,15 @@ -from typing import Optional, Tuple, Iterator, TYPE_CHECKING +from typing import Optional, Tuple, Iterator from contextlib import contextmanager from math import atan2, cos, sin, degrees, radians from pyglet.math import Vec3 -from arcade.cinematic.data import ViewData, OrthographicProjectionData +from arcade.cinematic.data import CameraData, OrthographicProjectionData from arcade.cinematic.types import Projector -from arcade.cinematic.orthographic import OrthographicCamera +from arcade.cinematic.orthographic import OrthographicProjector from arcade.window_commands import get_window -if TYPE_CHECKING: - from arcade import Window +from arcade import Window class SimpleCamera: @@ -30,19 +29,19 @@ def __init__(self, *, zoom: Optional[float] = None, near: Optional[float] = None, far: Optional[float] = None, - view_data: Optional[ViewData] = None, + camera_data: Optional[CameraData] = None, projection_data: Optional[OrthographicProjectionData] = None ): self._window = window or get_window() - if any((viewport, projection, position, up, zoom, near, far)) and any((view_data, projection_data)): + if any((viewport, projection, position, up, zoom, near, far)) and any((camera_data, projection_data)): raise ValueError("Provided both data structures and raw values." "Only supply one or the other") if any((viewport, projection, position, up, zoom, near, far)): _pos = position or (0.0, 0.0) _up = up or (0.0, 1.0) - self._view = ViewData( + self._view = CameraData( viewport or (0, 0, self._window.width, self._window.height), (_pos[0], _pos[1], 0.0), (_up[0], _up[1], 0.0), @@ -59,7 +58,7 @@ def __init__(self, *, near or -100, far or 100 # Near, Far ) else: - self._view = view_data or ViewData( + self._view = camera_data or CameraData( (0, 0, self._window.width, self._window.height), # Viewport (self._window.width / 2, self._window.height / 2, 0.0), # Position (0, 1.0, 0.0), # Up @@ -72,7 +71,7 @@ def __init__(self, *, -100, 100 # Near, Far ) - self._camera = OrthographicCamera( + self._camera = OrthographicProjector( window=self._window, view=self._view, projection=self._projection @@ -308,11 +307,6 @@ def activate(self) -> Iterator[Projector]: """ A context manager version of Camera2DOrthographic.use() which allows for the use of `with` blocks. For example, `with camera.activate() as cam: ...`. - - :WARNING: - Currently there is no 'default' camera within arcade. This means this method will raise a value error - as self._window.current_camera is None initially. To solve this issue you only need to make a default - camera and call the use() method. """ previous_projector = self._window.current_camera try: @@ -325,5 +319,6 @@ def get_map_coordinates(self, screen_coordinate: Tuple[float, float]) -> Tuple[f """ Maps a screen position to a pixel position. """ + # TODO: better doc string return self._camera.get_map_coordinates(screen_coordinate) diff --git a/arcade/cinematic/simple_controllers.py b/arcade/cinematic/simple_controllers.py index 464090415c..e151d4aaf4 100644 --- a/arcade/cinematic/simple_controllers.py +++ b/arcade/cinematic/simple_controllers.py @@ -1 +1 @@ -# TODO +# TODO: diff --git a/arcade/cinematic/types.py b/arcade/cinematic/types.py index 8c27f5314c..6381fe877a 100644 --- a/arcade/cinematic/types.py +++ b/arcade/cinematic/types.py @@ -1,7 +1,7 @@ from typing import Protocol, Tuple, Iterator from contextlib import contextmanager -from arcade.cinematic.data import ViewData +from arcade.cinematic.data import CameraData class Projection(Protocol): @@ -23,6 +23,6 @@ def get_map_coordinates(self, screen_coordinate: Tuple[float, float]) -> Tuple[f class Camera(Protocol): - _view: ViewData + _view: CameraData _projection: Projection diff --git a/tests/unit/camera/test_camera_2d.py b/tests/unit/camera/test_camera_2d.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/unit/camera/test_camera_controllers.py b/tests/unit/camera/test_camera_controllers.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/unit/camera/test_default_camera.py b/tests/unit/camera/test_default_camera.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/unit/camera/test_orthographic_camera.py b/tests/unit/camera/test_orthographic_camera.py new file mode 100644 index 0000000000..85d252354a --- /dev/null +++ b/tests/unit/camera/test_orthographic_camera.py @@ -0,0 +1,90 @@ +import pytest as pytest + +from arcade import cinematic, Window + + +def test_orthographic_camera(window: Window): + default_camera = window.current_camera + + cam_default = cinematic.OrthographicProjector() + default_view = cam_default.view + default_projection = cam_default.projection + + # test that the camera correctly generated the default view and projection PoDs. + assert default_view == cinematic.CameraData( + (0, 0, window.width, window.height), # Viewport + (window.width/2, window.height/2, 0), # Position + (0.0, 1.0, 0.0), # Up + (0.0, 0.0, 1.0), # Forward + 1.0, # Zoom + ) + assert default_projection == cinematic.OrthographicProjectionData( + -0.5 * window.width, 0.5 * window.width, # Left, Right + -0.5 * window.height, 0.5 * window.height, # Bottom, Top + -100, 100 # Near, Far + ) + + # test that the camera properties work + assert cam_default.position == default_view.position + assert cam_default.viewport == default_view.viewport + + # Test that the camera is actually recognised by the camera as being activated + assert window.current_camera == default_camera + with cam_default.activate() as cam: + assert window.current_camera == cam and cam == cam_default + assert window.current_camera == default_camera + + # Test that the camera is being used. + cam_default.use() + assert window.current_camera == cam_default + default_camera.use() + assert window.current_camera == default_camera + + set_view = cinematic.CameraData( + (0, 0, 1, 1), # Viewport + (0.0, 0.0, 0.0), # Position + (0.0, 1.0, 0.0), # Up + (0.0, 0.0, 1.0), # Forward + 1.0 # Zoom + ) + set_projection = cinematic.OrthographicProjectionData( + 0.0, 1.0, # Left, Right + 0.0, 1.0, # Bottom, Top + -1.0, 1.0 # Near, Far + ) + cam_set = cinematic.OrthographicProjector( + view=set_view, + projection=set_projection + ) + + # test that the camera correctly used the provided Pods. + assert cam_set.view == set_view + assert cam_set.projection == set_projection + + # test that the camera properties work + assert cam_set.position == set_view.position + assert cam_set.viewport == set_view.viewport + + # Test that the camera is actually recognised by the camera as being activated + assert window.current_camera == default_camera + with cam_set.activate() as cam: + assert window.current_camera == cam and cam == cam_set + assert window.current_camera == default_camera + + # Test that the camera is being used. + cam_set.use() + assert window.current_camera == cam_set + default_camera.use() + assert window.current_camera == default_camera + + +def test_orthographic_projection_matrix(): + pass + + +def test_orthographic_view_matrix(): + pass + + +def test_orthographic_map_coordinates(): + pass diff --git a/tests/unit/camera/test_perspective_camera.py b/tests/unit/camera/test_perspective_camera.py new file mode 100644 index 0000000000..d359ba1831 --- /dev/null +++ b/tests/unit/camera/test_perspective_camera.py @@ -0,0 +1,17 @@ +import pytest as pytest + + +def test_perspective_camera(): + pass + + +def test_perspective_projection_matrix(): + pass + + +def test_perspective_view_matrix(): + pass + + +def test_perspective_map_coordinates(): + pass diff --git a/tests/unit/camera/test_simple_camera.py b/tests/unit/camera/test_simple_camera.py new file mode 100644 index 0000000000..e69de29bb2 From b3900c03a082905ea6c3dfd1a21b2192d66eb69e Mon Sep 17 00:00:00 2001 From: DragonMoffon Date: Mon, 31 Jul 2023 23:55:04 +1200 Subject: [PATCH 13/31] Finished base of Camera2D class. The basics for Camera2D have been provided with full doc strings. Other helper methods may be added in the future. --- arcade/cinematic/camera_2d.py | 790 +++++++++++++++++++++++++++++- arcade/cinematic/default.py | 1 + arcade/cinematic/orthographic.py | 1 + arcade/cinematic/perspective.py | 1 + arcade/cinematic/simple_camera.py | 5 +- 5 files changed, 788 insertions(+), 10 deletions(-) diff --git a/arcade/cinematic/camera_2d.py b/arcade/cinematic/camera_2d.py index c1bebc9b88..d3609f22b9 100644 --- a/arcade/cinematic/camera_2d.py +++ b/arcade/cinematic/camera_2d.py @@ -1,17 +1,58 @@ -from typing import Optional, Tuple +from typing import Optional, Tuple, Iterator from warnings import warn +from math import degrees, radians, atan2, cos, sin +from contextlib import contextmanager from arcade.cinematic.data import CameraData, OrthographicProjectionData +from arcade.cinematic.orthographic import OrthographicProjector +from arcade.cinematic.types import Projector + from arcade.application import Window from arcade.window_commands import get_window +__all__ = { + 'Camera2D' +} + class Camera2D: """ A simple orthographic camera. Similar to SimpleCamera, but takes better advantage of the new data structures. As the Simple Camera is depreciated any new project should use this camera instead. + + It provides properties to access every important variable for controlling the camera. + 3D properties such as pos, and up are constrained to a 2D plane. There is no access to the + forward vector (as a property). + + The method fully fulfils both the Camera and Projector protocols. + + There are also ease of use methods for matching the viewport and projector to the window size. + + Provides 4 sets of left, right, bottom, top: + - Positional in world space. + - Projection without zoom scaling. + - Projection with zoom scaling. + - Viewport. + + NOTE Once initialised, the CameraData and OrthographicProjectionData SHOULD NOT be changed. + Only getter methods are provided through data and projection_data respectively. + + + :param Window window: The Arcade Window instance that you want to bind the camera to. Uses current if undefined. + :param tuple viewport: The pixel area bounds the camera should draw to. (can be provided through camera_data) + :param tuple position: The X and Y position of the camera. (can be provided through camera_data) + :param tuple up: The up vector which defines the +Y axis in screen space. (can be provided through camera_data) + :param float zoom: A float which scales the viewport. (can be provided through camera_data) + :param tuple projection: The area which will be mapped to screen space. (can be provided through projection_data) + :param float near: The closest Z position before clipping. (can be provided through projection_data) + :param float far: The furthest Z position before clipping. (can be provided through projection_data) + :param CameraData camera_data: A data class which holds all the data needed to define the view of the camera. + :param ProjectionData projection_data: A data class which holds all the data needed to define the projection of + the camera. """ + # TODO: ADD PARAMS TO DOC FOR __init__ + def __init__(self, *, window: Optional[Window] = None, viewport: Optional[Tuple[int, int, int, int]] = None, @@ -26,12 +67,16 @@ def __init__(self, *, ): self._window = window or get_window() - if any((viewport, position, up, zoom)) and camera_data: - warn("Camera2D Warning: Provided both a CameraData object and raw values. Defaulting to CameraData.") + assert ( + any((viewport, position, up, zoom)) and camera_data, + "Camera2D Warning: Provided both a CameraData object and raw values. Defaulting to CameraData." + ) - if any((projection, near, far)) and projection_data: - warn("Camera2D Warning: Provided both an OrthographicProjectionData object and raw values." - "Defaulting to OrthographicProjectionData.") + assert ( + any((projection, near, far)) and projection_data, + "Camera2D Warning: Provided both an OrthographicProjectionData object and raw values." + "Defaulting to OrthographicProjectionData." + ) _pos = position or (self._window.width / 2, self._window.height / 2) _up = up or (0.0, 1.0) @@ -43,12 +88,741 @@ def __init__(self, *, zoom or 1.0 ) - _proj = projection or (-self._window.width/2, self._window.width/2, - -self._window.height/2, self._window.height/2) + _proj = projection or ( + -self._window.width/2, self._window.width/2, + -self._window.height/2, self._window.height/2 + ) self._projection = projection_data or OrthographicProjectionData( _proj[0], _proj[1], # Left and Right. _proj[2], _proj[3], # Bottom and Top. near or 0.0, far or 100.0 # Near and Far. ) + self._ortho_projector: OrthographicProjector = OrthographicProjector( + window=self._window, + view=self._data, + projection=self._projection + ) + + @property + def data(self) -> CameraData: + """ + Return the view data for the camera. This includes the + viewport, position, forward vector, up direction, and zoom. + + If you use any of the built-in arcade camera-controllers + or make your own this is the property to access. + """ + # TODO: Do not add setter + return self._data + + @property + def projection_data(self) -> OrthographicProjectionData: + """ + Return the projection data for the camera. + This is an Orthographic projection. with a + right, left, top, bottom, near, and far value. + An easy way to understand the use of the projection is + that the right value of the projection tells the + camera what value will be at the right most + pixel in the viewport. + + Due to the view data having a zoom component + most use cases will only change the projection + on screen resize. + """ + # TODO: Do not add setter + return self._projection + + @property + def pos(self) -> Tuple[float, float]: + """ + The 2D position of the camera along + the X and Y axis. Arcade has the positive + Y direction go towards the top of the screen. + """ + return self._data.position[:2] + + @pos.setter + def pos(self, _pos: Tuple[float, float]) -> None: + """ + Set the X and Y position of the camera. + """ + self._data.position = _pos + self._data.position[2:] + + @property + def left(self) -> float: + """ + The left side of the camera in world space. + Use this to check if a sprite is on screen. + """ + return self._data.position[0] + self._projection.left/self._data.zoom + + @left.setter + def left(self, _left: float) -> None: + """ + Set the left side of the camera. This moves the position of the camera. + To change the left of the projection use projection_left. + """ + self._data.position = (_left - self._projection.left/self._data.zoom,) + self._data.position[1:] + + @property + def right(self) -> float: + """ + The right side of the camera in world space. + Use this to check if a sprite is on screen. + """ + return self._data.position[0] + self._projection.right/self._data.zoom + + @right.setter + def right(self, _right: float) -> None: + """ + Set the right side of the camera. This moves the position of the camera. + To change the right of the projection use projection_right. + """ + self._data.position = (_right - self._projection.right/self._data.zoom,) + self._data.position[1:] + + @property + def bottom(self) -> float: + """ + The bottom side of the camera in world space. + Use this to check if a sprite is on screen. + """ + return self._data.position[1] + self._projection.bottom/self._data.zoom + + @bottom.setter + def bottom(self, _bottom: float) -> None: + """ + Set the bottom side of the camera. This moves the position of the camera. + To change the bottom of the projection use projection_bottom. + """ + self._data.position = ( + self._data.position[0], + _bottom - self._projection.bottom/self._data.zoom, + self._data.position[2] + ) + + @property + def top(self) -> float: + """ + The top side of the camera in world space. + Use this to check if a sprite is on screen. + """ + return self._data.position[1] + self._projection.top/self._data.zoom + + @top.setter + def top(self, _top: float) -> None: + """ + Set the top side of the camera. This moves the position of the camera. + To change the top of the projection use projection_top. + """ + self._data.position = ( + self._data.position[0], + _top - self._projection.top/self._data.zoom, + self._data.position[2] + ) + + @property + def projection(self) -> Tuple[float, float, float, float]: + """ + The left, right, bottom, top values + that maps world space coordinates to pixel positions. + """ + _p = self._projection + return _p.left, _p.right, _p.bottom, _p.top + + @projection.setter + def projection(self, value: Tuple[float, float, float, float]) -> None: + """ + Set the left, right, bottom, top values + that maps world space coordinates to pixel positions. + """ + _p = self._projection + _p.left, _p.right, _p.bottom, _p.top = value + + @property + def projection_width(self) -> float: + """ + The width of the projection from left to right. + This is in world space coordinates not pixel coordinates. + + NOTE this IS NOT scaled by zoom. + If this isn't what you want, + use projection_width_scaled instead. + """ + return self._projection.right - self._projection.left + + @projection_width.setter + def projection_width(self, _width: float): + """ + Set the width of the projection from left to right. + This is in world space coordinates not pixel coordinates. + + NOTE this IS NOT scaled by zoom. + If this isn't what you want, + use projection_width_scaled instead. + """ + w = self.projection_width + l = self.projection_left / w # Normalised Projection left + r = self.projection_right / w # Normalised Projection Right + + self.projection_left = l * _width + self.projection_right = r * _width + + @property + def projection_width_scaled(self) -> float: + """ + The width of the projection from left to right. + This is in world space coordinates not pixel coordinates. + + NOTE this IS scaled by zoom. + If this isn't what you want, + use projection_width instead. + """ + return (self._projection.right - self._projection.left) / self._data.zoom + + @projection_width_scaled.setter + def projection_width_scaled(self, _width: float) -> None: + """ + Set the width of the projection from left to right. + This is in world space coordinates not pixel coordinates. + + NOTE this IS scaled by zoom. + If this isn't what you want, + use projection_width instead. + """ + w = self.projection_width * self._data.zoom + l = self.projection_left / w # Normalised Projection left + r = self.projection_right / w # Normalised Projection Right + + self.projection_left = l * _width + self.projection_right = r * _width + + @property + def projection_height(self) -> float: + """ + The height of the projection from bottom to top. + This is in world space coordinates not pixel coordinates. + + NOTE this IS NOT scaled by zoom. + If this isn't what you want, + use projection_height_scaled instead. + """ + return self._projection.top - self._projection.bottom + + @projection_height.setter + def projection_height(self, _height: float) -> None: + """ + Set the height of the projection from bottom to top. + This is in world space coordinates not pixel coordinates. + + NOTE this IS NOT scaled by zoom. + If this isn't what you want, + use projection_height_scaled instead. + """ + h = self.projection_height + b = self.projection_bottom / h # Normalised Projection Bottom + t = self.projection_top / h # Normalised Projection Top + + self.projection_bottom = b * _height + self.projection_top = t * _height + + @property + def projection_height_scaled(self) -> float: + """ + The height of the projection from bottom to top. + This is in world space coordinates not pixel coordinates. + + NOTE this IS scaled by zoom. + If this isn't what you want, + use projection_height instead. + """ + return (self._projection.top - self._projection.bottom) / self._data.zoom + + @projection_height_scaled.setter + def projection_height_scaled(self, _height: float) -> None: + """ + Set the height of the projection from bottom to top. + This is in world space coordinates not pixel coordinates. + + NOTE this IS scaled by zoom. + If this isn't what you want, + use projection_height instead. + """ + h = self.projection_height * self._data.zoom + b = self.projection_bottom / h # Normalised Projection Bottom + t = self.projection_top / h # Normalised Projection Top + + self.projection_bottom = b * _height + self.projection_top = t * _height + + @property + def projection_left(self) -> float: + """ + The left edge of the projection in world space. + This is not adjusted with the camera position. + + NOTE this IS NOT scaled by zoom. + If this isn't what you want, + use projection_left_scaled instead. + """ + return self._projection.left + + @projection_left.setter + def projection_left(self, _left: float) -> None: + """ + Set the left edge of the projection in world space. + This is not adjusted with the camera position. + + NOTE this IS NOT scaled by zoom. + If this isn't what you want, + use projection_left_scaled instead. + """ + self._projection.left = _left + + @property + def projection_left_scaled(self) -> float: + """ + The left edge of the projection in world space. + This is not adjusted with the camera position. + + NOTE this IS scaled by zoom. + If this isn't what you want, + use projection_left instead. + """ + return self._projection.left / self._data.zoom + + @projection_left_scaled.setter + def projection_left_scaled(self, _left: float) -> None: + """ + The left edge of the projection in world space. + This is not adjusted with the camera position. + + NOTE this IS scaled by zoom. + If this isn't what you want, + use projection_left instead. + """ + self._projection.left = _left * self._data.zoom + + @property + def projection_right(self) -> float: + """ + The right edge of the projection in world space. + This is not adjusted with the camera position. + + NOTE this IS NOT scaled by zoom. + If this isn't what you want, + use projection_right_scaled instead. + """ + return self._projection.right + + @projection_right.setter + def projection_right(self, _right: float) -> None: + """ + Set the right edge of the projection in world space. + This is not adjusted with the camera position. + + NOTE this IS NOT scaled by zoom. + If this isn't what you want, + use projection_right_scaled instead. + """ + self._projection.right = _right + + @property + def projection_right_scaled(self) -> float: + """ + The right edge of the projection in world space. + This is not adjusted with the camera position. + + NOTE this IS scaled by zoom. + If this isn't what you want, + use projection_right instead. + """ + return self._projection.right / self._data.zoom + + @projection_right_scaled.setter + def projection_right_scaled(self, _right: float) -> None: + """ + Set the right edge of the projection in world space. + This is not adjusted with the camera position. + + NOTE this IS scaled by zoom. + If this isn't what you want, + use projection_right instead. + """ + self._projection.right = _right * self._data.zoom + + @property + def projection_bottom(self) -> float: + """ + The bottom edge of the projection in world space. + This is not adjusted with the camera position. + + NOTE this IS NOT scaled by zoom. + If this isn't what you want, + use projection_bottom_scaled instead. + """ + return self._projection.bottom + + @projection_bottom.setter + def projection_bottom(self, _bottom: float) -> None: + """ + Set the bottom edge of the projection in world space. + This is not adjusted with the camera position. + + NOTE this IS NOT scaled by zoom. + If this isn't what you want, + use projection_bottom_scaled instead. + """ + self._projection.bottom = _bottom + + @property + def projection_bottom_scaled(self) -> float: + """ + The bottom edge of the projection in world space. + This is not adjusted with the camera position. + + NOTE this IS scaled by zoom. + If this isn't what you want, + use projection_bottom instead. + """ + return self._projection.bottom / self._data.zoom + + @projection_bottom_scaled.setter + def projection_bottom_scaled(self, _bottom: float) -> None: + """ + Set the bottom edge of the projection in world space. + This is not adjusted with the camera position. + + NOTE this IS scaled by zoom. + If this isn't what you want, + use projection_bottom instead. + """ + self._projection.bottom = _bottom * self._data.zoom + + @property + def projection_top(self) -> float: + """ + The top edge of the projection in world space. + This is not adjusted with the camera position. + + NOTE this IS NOT scaled by zoom. + If this isn't what you want, + use projection_top_scaled instead. + """ + return self._projection.top + + @projection_top.setter + def projection_top(self, _top: float) -> None: + """ + Set the top edge of the projection in world space. + This is not adjusted with the camera position. + + NOTE this IS NOT scaled by zoom. + If this isn't what you want, + use projection_top_scaled instead. + """ + self._projection.top = _top + + @property + def projection_top_scaled(self) -> float: + """ + The top edge of the projection in world space. + This is not adjusted with the camera position. + + NOTE this IS scaled by zoom. + If this isn't what you want, + use projection_top instead. + """ + return self._projection.top / self._data.zoom + + @projection_top_scaled.setter + def projection_top_scaled(self, _top: float) -> None: + """ + Set the top edge of the projection in world space. + This is not adjusted with the camera position. + + NOTE this IS scaled by zoom. + If this isn't what you want, + use projection_top instead. + """ + self._projection.top = _top * self._data.zoom + + @property + def projection_near(self) -> float: + """ + The near plane of the projection in world space. + This is not adjusted with the camera position. + + NOTE this IS NOT scaled by zoom. + """ + return self._projection.near + + @projection_near.setter + def projection_near(self, _near: float) -> None: + """ + Set the near plane of the projection in world space. + This is not adjusted with the camera position. + + NOTE this IS NOT scaled by zoom. + """ + self._projection.near = _near + + @property + def projection_far(self) -> float: + """ + The far plane of the projection in world space. + This is not adjusted with the camera position. + + NOTE this IS NOT scaled by zoom. + """ + return self._projection.far + + @projection_far.setter + def projection_far(self, _far: float) -> None: + """ + Set the far plane of the projection in world space. + This is not adjusted with the camera position. + + NOTE this IS NOT scaled by zoom. + """ + self._projection.far = _far + + @property + def viewport(self) -> Tuple[int, int, int, int]: + """ + The pixel area that will be drawn to while the camera is active. + (left, right, bottom, top) + """ + return self._data.viewport + + @viewport.setter + def viewport(self, _viewport: Tuple[int, int, int, int]) -> None: + """ + Set the pixel area that will be drawn to while the camera is active. + (left, bottom, width, height) + """ + self._data.viewport = _viewport + + @property + def viewport_width(self) -> int: + """ + The width of the viewport. + Defines the number of pixels drawn too horizontally. + """ + return self._data.viewport[2] + + @viewport_width.setter + def viewport_width(self, _width: int) -> None: + """ + Set the width of the viewport. + Defines the number of pixels drawn too horizontally + """ + self._data.viewport = self._data.viewport[:2] + (_width, self._data.viewport[3]) + + @property + def viewport_height(self) -> int: + """ + The height of the viewport. + Defines the number of pixels drawn too vertically. + """ + return self._data.viewport[3] + + @viewport_height.setter + def viewport_height(self, _height: int) -> None: + """ + Set the height of the viewport. + Defines the number of pixels drawn too vertically. + """ + self._data.viewport = self._data.viewport[:3] + (_height,) + + @property + def viewport_left(self) -> int: + """ + The left most pixel drawn to on the X axis. + """ + return self._data.viewport[0] + + @viewport_left.setter + def viewport_left(self, _left: int) -> None: + """ + Set the left most pixel drawn to on the X axis. + """ + self._data.viewport = (_left,) + self._data.viewport[2:] + + @property + def viewport_right(self) -> int: + """ + The right most pixel drawn to on the X axis. + """ + return self._data.viewport[0] + self._data.viewport[2] + + @viewport_right.setter + def viewport_right(self, _right: int) -> None: + """ + Set the right most pixel drawn to on the X axis. + This moves the position of the viewport, not change the size. + """ + self._data.viewport = (_right - self._data.viewport[2],) + self._data.viewport[1:] + + @property + def viewport_bottom(self) -> int: + """ + The bottom most pixel drawn to on the Y axis. + """ + return self._data.viewport[1] + + @viewport_bottom.setter + def viewport_bottom(self, _bottom: int) -> None: + """ + Set the bottom most pixel drawn to on the Y axis. + """ + self._data.viewport = (self._data.viewport[0], _bottom) + self._data.viewport[2:] + + @property + def viewport_top(self) -> int: + """ + The top most pixel drawn to on the Y axis. + """ + return self._data.viewport[1] + self._data.viewport[3] + + @viewport_top.setter + def viewport_top(self, _top: int) -> None: + """ + Set the top most pixel drawn to on the Y axis. + This moves the position of the viewport, not change the size. + """ + self._data.viewport = (self._data.viewport[0], _top - self._data.viewport[3]) + self._data.viewport[2:] + + @property + def up(self) -> Tuple[float, float]: + """ + A 2D vector which describes what is mapped + to the +Y direction on screen. + This is equivalent to rotating the screen. + The base vector is 3D, but the simplified + camera only provides a 2D view. + """ + return self._data.up[:2] + + @up.setter + def up(self, _up: Tuple[float, float]) -> None: + """ + Set the 2D vector which describes what is + mapped to the +Y direction on screen. + This is equivalent to rotating the screen. + The base vector is 3D, but the simplified + camera only provides a 2D view. + + NOTE that this is assumed to be normalised. + """ + self._data.up = _up + (0,) + + @property + def angle(self) -> float: + """ + An angle representation of the 2D UP vector. + This starts with 0 degrees as [0, 1] rotating + clock-wise. + """ + # Note that this is flipped as we want 0 degrees to be vert. Normally you have y first and then x. + return atan2(self._data.position[0], self._data.position[1]) + + @angle.setter + def angle(self, value: float): + """ + Set the 2D UP vector using an angle. + This starts with 0 degrees as [0, 1] + rotating clock-wise. + """ + _r = radians(value) + # Note that this is flipped as we want 0 degrees to be vert. + self._data.position = (sin(_r), cos(_r), 0.0) + + @property + def zoom(self) -> float: + """ + A scalar value which describes + how much the projection should + be scaled towards from its center. + + A value of 2.0 causes the projection + to be half its original size. + This causes sprites to appear 2.0x larger. + """ + return self._data.zoom + + @zoom.setter + def zoom(self, _zoom: float) -> None: + """ + Set the scalar value which describes + how much the projection should + be scaled towards from its center. + + A value of 2.0 causes the projection + to be half its original size. + This causes sprites to appear 2.0x larger. + """ + self._data.zoom = _zoom + + def equalise(self) -> None: + """ + Forces the projection to match the size of the viewport. + When matching the projection to the viewport the method keeps + the projections center in the same relative place. + """ + + self.projection_width = self.viewport_width + self.projection_height = self.viewport_height + + def match_screen(self, and_projection: bool = True) -> None: + """ + Sets the viewport to the size of the screen. + Should be called when the screen is resized. + + :param and_projection: Also equalises the projection if True. + """ + self.viewport = (0, 0, self._window.width, self._window.height) + + if and_projection: + self.equalise() + + def use(self) -> None: + """ + Set internal projector as window projector, + and set the projection and view matrix. + call every time you want to 'look through' this camera. + + If you want to use a 'with' block use activate() instead. + """ + self._ortho_projector.use() + + @contextmanager + def activate(self) -> Iterator[Projector]: + """ + Set internal projector as window projector, + and set the projection and view matrix. + + This method works with 'with' blocks. + After using this method it automatically resets + the projector to the one previously in use. + """ + previous_projection = self._window.current_camera + try: + self.use() + yield self + finally: + previous_projection.use() + + def get_map_coordinate(self, screen_coordinates: Tuple[float, float]) -> Tuple[float, float]: + """ + Take in a pixel coordinate from within + the range of the viewport and returns + the world space coordinates. + + Essentially reverses the effects of the projector. + + :param screen_coordinates: The pixel coordinates to map back to world coordinates. + """ + return self._ortho_projector.get_map_coordinates(screen_coordinates) diff --git a/arcade/cinematic/default.py b/arcade/cinematic/default.py index 407503d9f8..3d0303080c 100644 --- a/arcade/cinematic/default.py +++ b/arcade/cinematic/default.py @@ -14,6 +14,7 @@ class _DefaultProjector: An extremely limited projector which lacks any kind of control. This is only here to act as the default camera used internally by arcade. There should be no instance where a developer would want to use this class. """ + # TODO: ADD PARAMS TO DOC FOR __init__ def __init__(self, *, window: Optional["Window"] = None): self._window: "Window" = window or get_window() diff --git a/arcade/cinematic/orthographic.py b/arcade/cinematic/orthographic.py index 3371dda54a..9e70474bb3 100644 --- a/arcade/cinematic/orthographic.py +++ b/arcade/cinematic/orthographic.py @@ -27,6 +27,7 @@ class OrthographicProjector: be inefficient. If you suspect this is causing slowdowns profile before optimising with a dirty value check. """ + # TODO: ADD PARAMS TO DOC FOR __init__ def __init__(self, *, window: Optional["Window"] = None, diff --git a/arcade/cinematic/perspective.py b/arcade/cinematic/perspective.py index 61d8ccc496..36af57b41e 100644 --- a/arcade/cinematic/perspective.py +++ b/arcade/cinematic/perspective.py @@ -26,6 +26,7 @@ class PerspectiveProjector: If used every frame or multiple times per frame this may be inefficient. """ + # TODO: ADD PARAMS TO DOC FOR __init__ def __init__(self, *, window: Optional["Window"] = None, diff --git a/arcade/cinematic/simple_camera.py b/arcade/cinematic/simple_camera.py index 36262883ca..c2e7b38570 100644 --- a/arcade/cinematic/simple_camera.py +++ b/arcade/cinematic/simple_camera.py @@ -19,6 +19,7 @@ class SimpleCamera: Written to be backwards compatible with the old SimpleCamera. """ + # TODO: ADD PARAMS TO DOC FOR __init__ def __init__(self, *, window: Optional["Window"] = None, @@ -224,8 +225,8 @@ def angle(self, angle: float) -> None: """ rad = radians(angle) self.up = ( - cos(rad), - sin(rad) + sin(rad), + cos(rad) ) def move_to(self, vector: Tuple[float, float], speed: float = 1.0) -> None: From b3aed09876d524a87a6b285240d226985e8ba786 Mon Sep 17 00:00:00 2001 From: DragonMoffon Date: Tue, 1 Aug 2023 00:07:31 +1200 Subject: [PATCH 14/31] code-inspection clean-up on Camera2D Fixed `mypy`, `pyright`, `ruff` errors. Also added __all__ property to every file for a better importing experience. NOTE arcade/camera.py is still there, and it does not match the current system so the code-inspection still complains. Will resolve later. --- arcade/cinematic/camera_2d.py | 17 +++++++++-------- arcade/cinematic/data.py | 7 +++++++ arcade/cinematic/default.py | 6 +++++- arcade/cinematic/orthographic.py | 7 ++++++- arcade/cinematic/perspective.py | 13 +++++++++---- arcade/cinematic/simple_camera.py | 7 ++++++- arcade/cinematic/types.py | 9 ++++++++- 7 files changed, 50 insertions(+), 16 deletions(-) diff --git a/arcade/cinematic/camera_2d.py b/arcade/cinematic/camera_2d.py index d3609f22b9..91a1f036b7 100644 --- a/arcade/cinematic/camera_2d.py +++ b/arcade/cinematic/camera_2d.py @@ -1,5 +1,4 @@ from typing import Optional, Tuple, Iterator -from warnings import warn from math import degrees, radians, atan2, cos, sin from contextlib import contextmanager @@ -11,9 +10,9 @@ from arcade.application import Window from arcade.window_commands import get_window -__all__ = { +__all__ = [ 'Camera2D' -} +] class Camera2D: @@ -68,12 +67,14 @@ def __init__(self, *, self._window = window or get_window() assert ( - any((viewport, position, up, zoom)) and camera_data, + any((viewport, position, up, zoom)) and camera_data + ), ( "Camera2D Warning: Provided both a CameraData object and raw values. Defaulting to CameraData." ) assert ( - any((projection, near, far)) and projection_data, + any((projection, near, far)) and projection_data + ), ( "Camera2D Warning: Provided both an OrthographicProjectionData object and raw values." "Defaulting to OrthographicProjectionData." ) @@ -648,7 +649,7 @@ def viewport_left(self, _left: int) -> None: """ Set the left most pixel drawn to on the X axis. """ - self._data.viewport = (_left,) + self._data.viewport[2:] + self._data.viewport = (_left,) + self._data.viewport[1:] @property def viewport_right(self) -> int: @@ -726,7 +727,7 @@ def angle(self) -> float: clock-wise. """ # Note that this is flipped as we want 0 degrees to be vert. Normally you have y first and then x. - return atan2(self._data.position[0], self._data.position[1]) + return degrees(atan2(self._data.position[0], self._data.position[1])) @angle.setter def angle(self, value: float): @@ -814,7 +815,7 @@ def activate(self) -> Iterator[Projector]: finally: previous_projection.use() - def get_map_coordinate(self, screen_coordinates: Tuple[float, float]) -> Tuple[float, float]: + def map_coordinate(self, screen_coordinates: Tuple[float, float]) -> Tuple[float, float]: """ Take in a pixel coordinate from within the range of the viewport and returns diff --git a/arcade/cinematic/data.py b/arcade/cinematic/data.py index b8e0aa0bf3..3edd2188fd 100644 --- a/arcade/cinematic/data.py +++ b/arcade/cinematic/data.py @@ -2,6 +2,13 @@ from dataclasses import dataclass +__all__ = [ + 'CameraData', + 'OrthographicProjectionData', + 'PerspectiveProjectionData' +] + + @dataclass class CameraData: """ diff --git a/arcade/cinematic/default.py b/arcade/cinematic/default.py index 3d0303080c..05f8b1b871 100644 --- a/arcade/cinematic/default.py +++ b/arcade/cinematic/default.py @@ -8,6 +8,10 @@ if TYPE_CHECKING: from arcade.application import Window +__all__ = [ + '_DefaultProjector' +] + class _DefaultProjector: """ @@ -49,5 +53,5 @@ def activate(self) -> Iterator[Projector]: finally: previous.use() - def get_map_coordinates(self, screen_coordinate: Tuple[float, float]) -> Tuple[float, float]: + def map_coordinate(self, screen_coordinate: Tuple[float, float]) -> Tuple[float, float]: return screen_coordinate diff --git a/arcade/cinematic/orthographic.py b/arcade/cinematic/orthographic.py index 9e70474bb3..731257a9ca 100644 --- a/arcade/cinematic/orthographic.py +++ b/arcade/cinematic/orthographic.py @@ -11,6 +11,11 @@ from arcade import Window +__all__ = [ + 'OrthographicProjector' +] + + class OrthographicProjector: """ The simplest from of an orthographic camera. @@ -133,7 +138,7 @@ def activate(self) -> Iterator[Projector]: finally: previous_projector.use() - def get_map_coordinates(self, screen_coordinate: Tuple[float, float]) -> Tuple[float, float]: + def map_coordinate(self, screen_coordinate: Tuple[float, float]) -> Tuple[float, float]: """ Maps a screen position to a pixel position. """ diff --git a/arcade/cinematic/perspective.py b/arcade/cinematic/perspective.py index 36af57b41e..58722c4b3d 100644 --- a/arcade/cinematic/perspective.py +++ b/arcade/cinematic/perspective.py @@ -11,6 +11,11 @@ from arcade import Window +__all__ = [ + 'PerspectiveProjector' +] + + class PerspectiveProjector: """ The simplest from of a perspective camera. @@ -123,7 +128,7 @@ def activate(self) -> Iterator[Projector]: finally: previous_projector.use() - def get_map_coordinates(self, screen_coordinate: Tuple[float, float]) -> Tuple[float, float]: + def map_coordinate(self, screen_coordinate: Tuple[float, float]) -> Tuple[float, float]: """ Maps a screen position to a pixel position at the near clipping plane of the camera. """ @@ -142,9 +147,9 @@ def get_map_coordinates(self, screen_coordinate: Tuple[float, float]) -> Tuple[f return _mapped_position[0], _mapped_position[1] - def get_map_coordinates_at_depth(self, - screen_coordinate: Tuple[float, float], - depth: float) -> Tuple[float, float]: + def map_coordinate_at_depth(self, + screen_coordinate: Tuple[float, float], + depth: float) -> Tuple[float, float]: """ Maps a screen position to a pixel position at the specific depth supplied. """ diff --git a/arcade/cinematic/simple_camera.py b/arcade/cinematic/simple_camera.py index c2e7b38570..8ae49bb180 100644 --- a/arcade/cinematic/simple_camera.py +++ b/arcade/cinematic/simple_camera.py @@ -12,6 +12,11 @@ from arcade import Window +__all__ = [ + 'SimpleCamera' +] + + class SimpleCamera: """ A simple camera which uses an orthographic camera and a simple 2D Camera Controller. @@ -316,7 +321,7 @@ def activate(self) -> Iterator[Projector]: finally: previous_projector.use() - def get_map_coordinates(self, screen_coordinate: Tuple[float, float]) -> Tuple[float, float]: + def map_coordinate(self, screen_coordinate: Tuple[float, float]) -> Tuple[float, float]: """ Maps a screen position to a pixel position. """ diff --git a/arcade/cinematic/types.py b/arcade/cinematic/types.py index 6381fe877a..50c1ab4c03 100644 --- a/arcade/cinematic/types.py +++ b/arcade/cinematic/types.py @@ -4,6 +4,13 @@ from arcade.cinematic.data import CameraData +__all__ = [ + 'Projection', + 'Projector', + 'Camera' +] + + class Projection(Protocol): near: float far: float @@ -18,7 +25,7 @@ def use(self) -> None: def activate(self) -> Iterator["Projector"]: ... - def get_map_coordinates(self, screen_coordinate: Tuple[float, float]) -> Tuple[float, float]: + def map_coordinate(self, screen_coordinate: Tuple[float, float]) -> Tuple[float, float]: ... From 16bc0b96207a5f86bcb03809a3b626104ec1ba57 Mon Sep 17 00:00:00 2001 From: DragonMoffon Date: Tue, 1 Aug 2023 23:44:51 +1200 Subject: [PATCH 15/31] Removed all reference to old camera system. This included deleting `arcade/camera.py`, and fixing the ui and sections to use either the Default Ortho Projector. NOTE I removed a quick index to the `camera.rst`. That will need to be fixed. Hey look I linted before pushing for once! --- arcade/__init__.py | 3 - arcade/application.py | 4 +- arcade/camera.py | 588 ------------------------------ arcade/cinematic/camera_2d.py | 6 +- arcade/cinematic/default.py | 7 +- arcade/cinematic/simple_camera.py | 2 +- arcade/gui/ui_manager.py | 12 +- arcade/sections.py | 12 +- util/update_quick_index.py | 1 - 9 files changed, 24 insertions(+), 611 deletions(-) delete mode 100644 arcade/camera.py diff --git a/arcade/__init__.py b/arcade/__init__.py index 9dbfefd186..0113c4326d 100644 --- a/arcade/__init__.py +++ b/arcade/__init__.py @@ -84,7 +84,6 @@ def configure_logging(level: Optional[int] = None): from .window_commands import unschedule from .window_commands import schedule_once -from .camera import SimpleCamera, Camera from .sections import Section, SectionManager from .application import MOUSE_BUTTON_LEFT @@ -241,8 +240,6 @@ def configure_logging(level: Optional[int] = None): 'AnimatedWalkingSprite', 'AnimationKeyframe', 'ArcadeContext', - 'Camera', - 'SimpleCamera', 'ControllerManager', 'FACE_DOWN', 'FACE_LEFT', diff --git a/arcade/application.py b/arcade/application.py index c5dd573b81..3f84cd6c85 100644 --- a/arcade/application.py +++ b/arcade/application.py @@ -23,7 +23,7 @@ from arcade import SectionManager from arcade.utils import is_raspberry_pi from arcade.cinematic import Projector -from arcade.cinematic.default import _DefaultProjector +from arcade.cinematic.default import DefaultProjector LOG = logging.getLogger(__name__) @@ -208,7 +208,7 @@ def __init__( self._background_color: Color = TRANSPARENT_BLACK self._current_view: Optional[View] = None - self.current_camera: Projector = _DefaultProjector(window=self) + self.current_camera: Projector = DefaultProjector(window=self) self.textbox_time = 0.0 self.key: Optional[int] = None self.flip_count: int = 0 diff --git a/arcade/camera.py b/arcade/camera.py deleted file mode 100644 index 39f8aef70b..0000000000 --- a/arcade/camera.py +++ /dev/null @@ -1,588 +0,0 @@ -""" -Camera class -""" -import math -from typing import TYPE_CHECKING, List, Optional, Tuple, Union, Iterator -from contextlib import contextmanager - -from pyglet.math import Mat4, Vec2, Vec3 - -import arcade -from arcade.types import Point -from arcade.cinematic.types import Projector -from arcade.math import get_distance - -if TYPE_CHECKING: - from arcade import Sprite, SpriteList - -# type aliases -FourIntTuple = Tuple[int, int, int, int] -FourFloatTuple = Tuple[float, float, float, float] - -__all__ = [ - "SimpleCamera", - "Camera" -] - - -class SimpleCamera: - """ - A simple camera that allows to change the viewport, the projection and can move around. - That's it. - See arcade.Camera for more advance stuff. - - :param viewport: Size of the viewport: (left, bottom, width, height) - :param projection: Space to allocate in the viewport of the camera (left, right, bottom, top) - """ - def __init__( - self, - *, - viewport: Optional[FourIntTuple] = None, - projection: Optional[FourFloatTuple] = None, - window: Optional["arcade.Window"] = None, - ) -> None: - # Reference to Context, used to update projection matrix - self._window: "arcade.Window" = window or arcade.get_window() - - # store the viewport and projection tuples - # viewport is the space the camera will hold on the screen (left, bottom, width, height) - self._viewport: FourIntTuple = viewport or (0, 0, self._window.width, self._window.height) - - # projection is what you want to project into the camera viewport (left, right, bottom, top) - self._projection: FourFloatTuple = projection or (0, self._window.width, - 0, self._window.height) - if viewport is not None and projection is None: - # if viewport is provided but projection is not, projection - # will match the provided viewport - self._projection = (viewport[0], viewport[2], viewport[1], viewport[3]) - - # Matrices - - # Projection Matrix is used to apply the camera viewport size - self._projection_matrix: Mat4 = Mat4() - # View Matrix is what the camera is looking at(position) - self._view_matrix: Mat4 = Mat4() - # We multiply projection and view matrices to get combined, - # this is what actually gets sent to GL context - self._combined_matrix: Mat4 = Mat4() - - # Position - self.position: Vec2 = Vec2(0, 0) - - # Camera movement - self.goal_position: Vec2 = Vec2(0, 0) - self.move_speed: float = 1.0 # 1.0 is instant - self.moving: bool = False - - # Init matrixes - # This will pre-compute the projection, view and combined matrixes - self._set_projection_matrix(update_combined_matrix=False) - self._set_view_matrix() - - @property - def viewport_width(self) -> int: - """ Returns the width of the viewport """ - return self._viewport[2] - - @property - def viewport_height(self) -> int: - """ Returns the height of the viewport """ - return self._viewport[3] - - @property - def viewport(self) -> FourIntTuple: - """ The space the camera will hold on the screen (left, bottom, width, height) """ - return self._viewport - - @viewport.setter - def viewport(self, viewport: FourIntTuple) -> None: - """ Sets the viewport """ - self.set_viewport(viewport) - - def set_viewport(self, viewport: FourIntTuple) -> None: - """ Sets the viewport """ - self._viewport = viewport or (0, 0, self._window.width, self._window.height) - - # the viewport affects the view matrix - self._set_view_matrix() - - @property - def projection(self) -> FourFloatTuple: - """ - The dimensions of the space to project in the camera viewport (left, right, bottom, top). - The projection is what you want to project into the camera viewport. - """ - return self._projection - - @projection.setter - def projection(self, new_projection: FourFloatTuple) -> None: - """ - Update the projection of the camera. This also updates the projection matrix with an orthogonal - projection based on the projection size of the camera and the zoom applied. - """ - self._projection = new_projection or (0, self._window.width, 0, self._window.height) - self._set_projection_matrix() - - @property - def viewport_to_projection_width_ratio(self): - """ The ratio of viewport width to projection width """ - return self.viewport_width / (self._projection[1] - self._projection[0]) - - @property - def viewport_to_projection_height_ratio(self): - """ The ratio of viewport height to projection height """ - return self.viewport_height / (self._projection[3] - self._projection[2]) - - @property - def projection_to_viewport_width_ratio(self): - """ The ratio of projection width to viewport width """ - return (self._projection[1] - self._projection[0]) / self.viewport_width - - @property - def projection_to_viewport_height_ratio(self): - """ The ratio of projection height to viewport height """ - return (self._projection[3] - self._projection[2]) / self.viewport_height - - def _set_projection_matrix(self, *, update_combined_matrix: bool = True) -> None: - """ - Helper method. This will just pre-compute the projection and combined matrix - - :param bool update_combined_matrix: if True will also update the combined matrix (projection @ view) - """ - self._projection_matrix = Mat4.orthogonal_projection(*self._projection, -100, 100) - if update_combined_matrix: - self._set_combined_matrix() - - def _set_view_matrix(self, *, update_combined_matrix: bool = True) -> None: - """ - Helper method. This will just pre-compute the view and combined matrix - - :param bool update_combined_matrix: if True will also update the combined matrix (projection @ view) - """ - - # Figure out our 'real' position - result_position = Vec3( - (self.position[0] / (self.viewport_width / 2)), - (self.position[1] / (self.viewport_height / 2)), - 0 - ) - self._view_matrix = ~(Mat4.from_translation(result_position)) - if update_combined_matrix: - self._set_combined_matrix() - - def _set_combined_matrix(self) -> None: - """ Helper method. This will just pre-compute the combined matrix""" - self._combined_matrix = self._view_matrix @ self._projection_matrix - - def move_to(self, vector: Union[Vec2, tuple], speed: float = 1.0) -> None: - """ - Sets the goal position of the camera. - - The camera will lerp towards this position based on the provided speed, - updating its position every time the use() function is called. - - :param Vec2 vector: Vector to move the camera towards. - :param Vec2 speed: How fast to move the camera, 1.0 is instant, 0.1 moves slowly - """ - self.goal_position = Vec2(*vector) - self.move_speed = speed - self.moving = True - - def move(self, vector: Union[Vec2, tuple]) -> None: - """ - Moves the camera with a speed of 1.0, aka instant move - - This is equivalent to calling move_to(my_pos, 1.0) - """ - self.move_to(vector, 1.0) - - def center(self, vector: Union[Vec2, tuple], speed: float = 1.0) -> None: - """ - Centers the camera on coordinates - """ - if not isinstance(vector, Vec2): - vector2: Vec2 = Vec2(*vector) - else: - vector2 = vector - - # get the center of the camera viewport - center = Vec2(self.viewport_width, self.viewport_height) / 2 - - # adjust vector to projection ratio - vector2 = Vec2(vector2.x * self.viewport_to_projection_width_ratio, - vector2.y * self.viewport_to_projection_height_ratio) - - # move to the vector subtracting the center - target = (vector2 - center) - - self.move_to(target, speed) - - def get_map_coordinates(self, camera_vector: Union[Vec2, tuple]) -> Tuple[float, float]: - """ - Returns map coordinates in pixels from screen coordinates based on the camera position - - :param Vec2 camera_vector: Vector captured from the camera viewport - """ - _mapped_position = Vec2(*self.position) + Vec2(*camera_vector) - - return _mapped_position[0], _mapped_position[1] - - def resize(self, viewport_width: int, viewport_height: int, *, - resize_projection: bool = True) -> None: - """ - Resize the camera's viewport. Call this when the window resizes. - - :param int viewport_width: Width of the viewport - :param int viewport_height: Height of the viewport - :param bool resize_projection: if True the projection will also be resized - """ - new_viewport = (self._viewport[0], self._viewport[1], viewport_width, viewport_height) - self.set_viewport(new_viewport) - if resize_projection: - self.projection = (self._projection[0], viewport_width, - self._projection[2], viewport_height) - - def update(self): - """ - Update the camera's viewport to the current settings. - """ - if self.moving: - # Apply Goal Position - self.position = self.position.lerp(self.goal_position, self.move_speed) - if self.position == self.goal_position: - self.moving = False - self._set_view_matrix() # this will also set the combined matrix - - def use(self) -> None: - """ - Select this camera for use. Do this right before you draw. - """ - self._window.current_camera = self - - # update camera position and calculate matrix values if needed - self.update() - - # set Viewport / projection - self._window.ctx.viewport = self._viewport # sets viewport of the camera - self._window.projection = self._combined_matrix # sets projection position and zoom - self._window.view = Mat4() # Set to identity matrix for now - - @contextmanager - def activate(self) -> Iterator[Projector]: - previous_camera = self._window.current_camera - try: - self.use() - yield self - finally: - previous_camera.use() - - -class Camera(SimpleCamera): - """ - The Camera class is used for controlling the visible viewport, the projection, zoom and rotation. - It is very useful for separating a scrolling screen of sprites, and a GUI overlay. - For an example of this in action, see :ref:`sprite_move_scrolling`. - - :param tuple viewport: (left, bottom, width, height) size of the viewport. If None the window size will be used. - :param tuple projection: (left, right, bottom, top) size of the projection. If None the window size will be used. - :param float zoom: the zoom to apply to the projection - :param float rotation: the angle in degrees to rotate the projection - :param tuple anchor: the x, y point where the camera rotation will anchor. Default is the center of the viewport. - :param Window window: Window to associate with this camera, if working with a multi-window program. - """ - def __init__( - self, *, - viewport: Optional[FourIntTuple] = None, - projection: Optional[FourFloatTuple] = None, - zoom: float = 1.0, - rotation: float = 0.0, - anchor: Optional[Tuple[float, float]] = None, - window: Optional["arcade.Window"] = None, - ): - # scale and zoom - # zoom it's just x scale value. Setting zoom will set scale x, y to the same value - self._scale: Tuple[float, float] = (zoom, zoom) - - # Near and Far - self._near: int = -1 - self._far: int = 1 - - # Shake - self.shake_velocity: Vec2 = Vec2() - self.shake_offset: Vec2 = Vec2() - self.shake_speed: float = 0.0 - self.shake_damping: float = 0.0 - self.shaking: bool = False - - # Call init from superclass here, previous attributes are needed before this call - super().__init__(viewport=viewport, projection=projection, window=window) - - # Rotation - self._rotation: float = rotation # in degrees - self._anchor: Optional[Tuple[float, float]] = anchor # (x, y) to anchor the camera rotation - - # Matrixes - # Rotation matrix holds the matrix used to compute the - # rotation set in window.ctx.view_matrix_2d - self._rotation_matrix: Mat4 = Mat4() - - # Init matrixes - # This will pre-compute the rotation matrix - self._set_rotation_matrix() - - def set_viewport(self, viewport: FourIntTuple) -> None: - """ Sets the viewport """ - super().set_viewport(viewport) - - # the viewport affects the rotation matrix if the rotation anchor is not set - if self._anchor is None: - self._set_rotation_matrix() - - def _set_projection_matrix(self, *, update_combined_matrix: bool = True) -> None: - """ - Helper method. This will just pre-compute the projection and combined matrix - - :param bool update_combined_matrix: if True will also update the combined matrix (projection @ view) - """ - # apply zoom - left, right, bottom, top = self._projection - if self._scale != (1.0, 1.0): - right *= self._scale[0] # x axis scale - top *= self._scale[1] # y axis scale - - self._projection_matrix = Mat4.orthogonal_projection(left, right, bottom, top, self._near, - self._far) - if update_combined_matrix: - self._set_combined_matrix() - - def _set_view_matrix(self, *, update_combined_matrix: bool = True) -> None: - """ - Helper method. This will just pre-compute the view and combined matrix - :param bool update_combined_matrix: if True will also update the combined matrix (projection @ view) - """ - - # Figure out our 'real' position plus the shake - result_position = self.position + self.shake_offset - result_position = Vec3( - (result_position[0] / ((self.viewport_width * self._scale[0]) / 2)), - (result_position[1] / ((self.viewport_height * self._scale[1]) / 2)), - 0 - ) - self._view_matrix = ~(Mat4.from_translation(result_position) @ Mat4().scale( - Vec3(self._scale[0], self._scale[1], 1.0))) - if update_combined_matrix: - self._set_combined_matrix() - - def _set_rotation_matrix(self) -> None: - """ Helper method that computes the rotation_matrix every time is needed """ - rotate = Mat4.from_rotation(math.radians(self._rotation), Vec3(0, 0, 1)) - - # If no anchor is set, use the center of the screen - if self._anchor is None: - offset = Vec3(self.position.x, self.position.y, 0) - offset += Vec3(self.viewport_width / 2, self.viewport_height / 2, 0) - else: - offset = Vec3(self._anchor[0], self._anchor[1], 0) - - translate_pre = Mat4.from_translation(offset) - translate_post = Mat4.from_translation(-offset) - - self._rotation_matrix = translate_post @ rotate @ translate_pre - - @property - def scale(self) -> Tuple[float, float]: - """ - Returns the x, y scale. - """ - return self._scale - - @scale.setter - def scale(self, new_scale: Tuple[float, float]) -> None: - """ - Sets the x, y scale (zoom property just sets scale to the same value). - This also updates the projection matrix with an orthogonal - projection based on the projection size of the camera and the zoom applied. - """ - if new_scale[0] <= 0 or new_scale[1] <= 0: - raise ValueError("Scale must be greater than zero") - - self._scale = (float(new_scale[0]), float(new_scale[1])) - - # Changing the scale (zoom) affects both projection_matrix and view_matrix - self._set_projection_matrix( - update_combined_matrix=False) # combined matrix will be set in the next call - self._set_view_matrix() - - @property - def zoom(self) -> float: - """ The zoom applied to the projection. Just returns the x scale value. """ - return self._scale[0] - - @zoom.setter - def zoom(self, zoom: float) -> None: - """ Apply a zoom to the projection """ - self.scale = zoom, zoom - - @property - def near(self) -> int: - """ The near applied to the projection""" - return self._near - - @near.setter - def near(self, near: int) -> None: - """ - Update the near of the camera. This also updates the projection matrix with an orthogonal - projection based on the projection size of the camera and the zoom applied. - """ - self._near = near - self._set_projection_matrix() - - @property - def far(self) -> int: - """ The far applied to the projection""" - return self._far - - @far.setter - def far(self, far: int) -> None: - """ - Update the far of the camera. This also updates the projection matrix with an orthogonal - projection based on the projection size of the camera and the zoom applied. - """ - self._far = far - self._set_projection_matrix() - - @property - def rotation(self) -> float: - """ - Get or set the rotation in degrees. - - This will rotate the camera clockwise meaning - the contents will rotate counter-clockwise. - """ - return self._rotation - - @rotation.setter - def rotation(self, value: float) -> None: - self._rotation = value - self._set_rotation_matrix() - - @property - def anchor(self) -> Optional[Tuple[float, float]]: - """ - Get or set the rotation anchor for the camera. - - By default, the anchor is the center of the screen - and the anchor value is `None`. Assigning a custom - anchor point will override this behavior. - The anchor point is in world / global coordinates. - - Example:: - - # Set the anchor to the center of the world - camera.anchor = 0, 0 - # Set the anchor to the center of the player - camera.anchor = player.position - """ - return self._anchor - - @anchor.setter - def anchor(self, anchor: Optional[Tuple[float, float]]) -> None: - if anchor is None: - self._anchor = None - else: - self._anchor = anchor[0], anchor[1] - self._set_rotation_matrix() - - def update(self) -> None: - """ - Update the camera's viewport to the current settings. - """ - update_view_matrix = False - - if self.moving: - # Apply Goal Position - self.position = self.position.lerp(self.goal_position, self.move_speed) - if self.position == self.goal_position: - self.moving = False - update_view_matrix = True - - if self.shaking: - # Apply Camera Shake - - # Move our offset based on shake velocity - self.shake_offset += self.shake_velocity - - # Get x and ys - vx = self.shake_velocity[0] - vy = self.shake_velocity[1] - - ox = self.shake_offset[0] - oy = self.shake_offset[1] - - # Calculate the angle our offset is at, and how far out - angle = math.atan2(ox, oy) - distance = get_distance(0, 0, ox, oy) - velocity_mag = get_distance(0, 0, vx, vy) - - # Ok, what's the reverse? Pull it back in. - reverse_speed = min(self.shake_speed, distance) - opposite_angle = angle + math.pi - opposite_vector = Vec2( - math.sin(opposite_angle) * reverse_speed, - math.cos(opposite_angle) * reverse_speed, - ) - - # Shaking almost done? Zero it out - if velocity_mag < self.shake_speed and distance < self.shake_speed: - self.shake_velocity = Vec2(0, 0) - self.shake_offset = Vec2(0, 0) - self.shaking = False - - # Come up with a new velocity, pulled by opposite vector and damped - self.shake_velocity += opposite_vector - self.shake_velocity *= self.shake_damping - - update_view_matrix = True - - if update_view_matrix: - self._set_view_matrix() # this will also set the combined matrix - - def shake(self, velocity: Union[Vec2, tuple], speed: float = 1.5, damping: float = 0.9) -> None: - """ - Add a camera shake. - - :param Vec2 velocity: Vector to start moving the camera - :param float speed: How fast to shake - :param float damping: How fast to stop shaking - """ - if not isinstance(velocity, Vec2): - velocity = Vec2(*velocity) - - self.shake_velocity += velocity - self.shake_speed = speed - self.shake_damping = damping - self.shaking = True - - def use(self) -> None: - """ - Select this camera for use. Do this right before you draw. - """ - super().use() # call SimpleCamera.use() - - # set rotation matrix - self._window.ctx.view_matrix_2d = self._rotation_matrix # sets rotation and rotation anchor - - def get_sprites_at_point(self, point: "Point", sprite_list: "SpriteList") -> List["Sprite"]: - """ - Get a list of sprites at a particular point when - This function sees if any sprite overlaps - the specified point. If a sprite has a different center_x/center_y but touches the point, - this will return that sprite. - - :param Point point: Point to check - :param SpriteList sprite_list: SpriteList to check against - - :returns: List of sprites colliding, or an empty list. - :rtype: list - """ - raise NotImplementedError() diff --git a/arcade/cinematic/camera_2d.py b/arcade/cinematic/camera_2d.py index 91a1f036b7..7d34ff8df2 100644 --- a/arcade/cinematic/camera_2d.py +++ b/arcade/cinematic/camera_2d.py @@ -815,7 +815,7 @@ def activate(self) -> Iterator[Projector]: finally: previous_projection.use() - def map_coordinate(self, screen_coordinates: Tuple[float, float]) -> Tuple[float, float]: + def map_coordinate(self, screen_coordinate: Tuple[float, float]) -> Tuple[float, float]: """ Take in a pixel coordinate from within the range of the viewport and returns @@ -823,7 +823,7 @@ def map_coordinate(self, screen_coordinates: Tuple[float, float]) -> Tuple[float Essentially reverses the effects of the projector. - :param screen_coordinates: The pixel coordinates to map back to world coordinates. + :param screen_coordinate: The pixel coordinates to map back to world coordinates. """ - return self._ortho_projector.get_map_coordinates(screen_coordinates) + return self._ortho_projector.map_coordinate(screen_coordinate) diff --git a/arcade/cinematic/default.py b/arcade/cinematic/default.py index 05f8b1b871..8613c78f73 100644 --- a/arcade/cinematic/default.py +++ b/arcade/cinematic/default.py @@ -9,11 +9,14 @@ from arcade.application import Window __all__ = [ - '_DefaultProjector' + 'DefaultProjector' ] -class _DefaultProjector: +# 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: """ An extremely limited projector which lacks any kind of control. This is only here to act as the default camera used internally by arcade. There should be no instance where a developer would want to use this class. diff --git a/arcade/cinematic/simple_camera.py b/arcade/cinematic/simple_camera.py index 8ae49bb180..2c497f17a4 100644 --- a/arcade/cinematic/simple_camera.py +++ b/arcade/cinematic/simple_camera.py @@ -327,4 +327,4 @@ def map_coordinate(self, screen_coordinate: Tuple[float, float]) -> Tuple[float, """ # TODO: better doc string - return self._camera.get_map_coordinates(screen_coordinate) + return self._camera.map_coordinate(screen_coordinate) diff --git a/arcade/gui/ui_manager.py b/arcade/gui/ui_manager.py index 87bb8c5b38..13efe46fae 100644 --- a/arcade/gui/ui_manager.py +++ b/arcade/gui/ui_manager.py @@ -30,7 +30,7 @@ ) from arcade.gui.surface import Surface from arcade.gui.widgets import UIWidget, Rect -from arcade.camera import SimpleCamera +from arcade.cinematic import OrthographicProjector, OrthographicProjectionData W = TypeVar("W", bound=UIWidget) @@ -90,7 +90,9 @@ def __init__(self, window: Optional[arcade.Window] = None): self.children: Dict[int, List[UIWidget]] = defaultdict(list) self._rendered = False #: Camera used when drawing the UI - self.camera = SimpleCamera() + self.projector = OrthographicProjector( + projection=OrthographicProjectionData(0, self.window.width, 0, self.window.height, -100, 100) + ) self.register_event_type("on_event") def add(self, widget: W, *, index=None, layer=0) -> W: @@ -298,7 +300,7 @@ def draw(self) -> None: self._do_render() # Draw layers - self.camera.use() + self.projector.use() with ctx.enabled(ctx.BLEND): layers = sorted(self.children.keys()) for layer in layers: @@ -317,7 +319,7 @@ def adjust_mouse_coordinates(self, x, y): """ # NOTE: Only support scrolling until cameras support transforming # mouse coordinates - px, py = self.camera.position + px, py = self.projector.view_data.position[:2] return x + px, y + py def on_event(self, event) -> Union[bool, None]: @@ -373,7 +375,7 @@ def on_text_motion_select(self, motion): def on_resize(self, width, height): scale = self.window.get_pixel_ratio() - self.camera.resize(width, height) + self.projector.view_data.viewport = (0, 0, width, height) for surface in self._surfaces.values(): surface.resize(size=(width, height), pixel_ratio=scale) diff --git a/arcade/sections.py b/arcade/sections.py index 3745736882..7286863b36 100644 --- a/arcade/sections.py +++ b/arcade/sections.py @@ -3,7 +3,9 @@ from pyglet.event import EVENT_HANDLED, EVENT_UNHANDLED -from arcade import SimpleCamera, get_window +from arcade import get_window +from arcade.cinematic import Projector +from arcade.cinematic.default import DefaultProjector if TYPE_CHECKING: from arcade import View @@ -99,7 +101,7 @@ def __init__(self, left: int, bottom: int, width: int, height: int, self._ec_top: int = self.window.height if self._modal else self._top # optional section camera - self.camera: Optional[SimpleCamera] = None + self.camera: Optional[Projector] = None def __repr__(self): name = f'Section {self.name}' if self.name else 'Section' @@ -325,9 +327,7 @@ def __init__(self, view: "View"): # generic camera to reset after a custom camera is use # this camera is set to the whole viewport - self.camera: SimpleCamera = SimpleCamera(viewport=(0, 0, - self.view.window.width, - self.view.window.height)) + self.camera: DefaultProjector = DefaultProjector() # Holds the section the mouse is currently on top self.mouse_over_sections: List[Section] = [] @@ -502,7 +502,7 @@ def on_resize(self, width: int, height: int) -> None: :param width: the new width of the screen :param height: the new height of the screen """ - self.camera.resize(width, height) # resize the default camera + # The Default camera auto-resizes. if self.view_resize_first is True: self.view.on_resize(width, height) # call resize on the view for section in self.sections: diff --git a/util/update_quick_index.py b/util/update_quick_index.py index 610afd61b7..24bd1d11ce 100644 --- a/util/update_quick_index.py +++ b/util/update_quick_index.py @@ -15,7 +15,6 @@ titles = { 'application.py': ['Window and View', 'window.rst'], 'shape_list.py': ['Shape Lists', 'drawing_batch.rst'], - 'camera.py': ['Camera', 'camera.rst'], 'context.py': ['OpenGL Context', 'open_gl.rst'], 'drawing_support.py': ['Drawing - Utility', 'drawing_utilities.rst'], 'draw_commands.py': ['Drawing - Primitives', 'drawing_primitives.rst'], From 69d81c2f6d5e46631b67098c34fd6c520dc8f59f Mon Sep 17 00:00:00 2001 From: DragonMoffon Date: Wed, 2 Aug 2023 00:18:21 +1200 Subject: [PATCH 16/31] whoops circular imports --- arcade/cinematic/simple_camera.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/arcade/cinematic/simple_camera.py b/arcade/cinematic/simple_camera.py index 2c497f17a4..daf9e4f285 100644 --- a/arcade/cinematic/simple_camera.py +++ b/arcade/cinematic/simple_camera.py @@ -1,4 +1,4 @@ -from typing import Optional, Tuple, Iterator +from typing import TYPE_CHECKING, Optional, Tuple, Iterator from contextlib import contextmanager from math import atan2, cos, sin, degrees, radians @@ -9,7 +9,8 @@ from arcade.cinematic.orthographic import OrthographicProjector from arcade.window_commands import get_window -from arcade import Window +if TYPE_CHECKING: + from arcade import Window __all__ = [ From 756297db320fbc22df774e2ef771983edd0ba6a8 Mon Sep 17 00:00:00 2001 From: DragonMoffon Date: Wed, 2 Aug 2023 22:34:02 +1200 Subject: [PATCH 17/31] circular imports 2 --- arcade/cinematic/camera_2d.py | 7 ++++--- arcade/sections.py | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/arcade/cinematic/camera_2d.py b/arcade/cinematic/camera_2d.py index 7d34ff8df2..c0875b2a68 100644 --- a/arcade/cinematic/camera_2d.py +++ b/arcade/cinematic/camera_2d.py @@ -1,4 +1,4 @@ -from typing import Optional, Tuple, Iterator +from typing import TYPE_CHECKING, Optional, Tuple, Iterator from math import degrees, radians, atan2, cos, sin from contextlib import contextmanager @@ -6,9 +6,10 @@ from arcade.cinematic.orthographic import OrthographicProjector from arcade.cinematic.types import Projector - -from arcade.application import Window from arcade.window_commands import get_window +if TYPE_CHECKING: + from arcade.application import Window + __all__ = [ 'Camera2D' diff --git a/arcade/sections.py b/arcade/sections.py index 7286863b36..9115e2414d 100644 --- a/arcade/sections.py +++ b/arcade/sections.py @@ -4,10 +4,10 @@ from pyglet.event import EVENT_HANDLED, EVENT_UNHANDLED from arcade import get_window -from arcade.cinematic import Projector from arcade.cinematic.default import DefaultProjector if TYPE_CHECKING: + from arcade.cinematic import Projector from arcade import View __all__ = [ From eb40fbad971b8399a86135786d4b620dc010fea4 Mon Sep 17 00:00:00 2001 From: DragonMoffon Date: Wed, 2 Aug 2023 22:38:17 +1200 Subject: [PATCH 18/31] type checking --- arcade/cinematic/camera_2d.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/arcade/cinematic/camera_2d.py b/arcade/cinematic/camera_2d.py index c0875b2a68..1811346f22 100644 --- a/arcade/cinematic/camera_2d.py +++ b/arcade/cinematic/camera_2d.py @@ -54,7 +54,7 @@ class Camera2D: # TODO: ADD PARAMS TO DOC FOR __init__ def __init__(self, *, - window: Optional[Window] = None, + window: Optional["Window"] = None, viewport: Optional[Tuple[int, int, int, int]] = None, position: Optional[Tuple[float, float]] = None, up: Optional[Tuple[float, float]] = None, @@ -65,7 +65,7 @@ def __init__(self, *, camera_data: Optional[CameraData] = None, projection_data: Optional[OrthographicProjectionData] = None ): - self._window = window or get_window() + self._window: "Window" = window or get_window() assert ( any((viewport, position, up, zoom)) and camera_data From 7068c5fa79396910b41d891e13e09c6d86040349 Mon Sep 17 00:00:00 2001 From: DragonMoffon Date: Sat, 5 Aug 2023 02:19:04 +1200 Subject: [PATCH 19/31] Started work on some controllers Made a few function controllers which are mega simple. Also changed name from arcade.cinematic to arcade.camera. Also moved the controllers to arcade.camera.controllers. --- arcade/camera/__init__.py | 26 ++++++ arcade/{cinematic => camera}/camera_2d.py | 0 arcade/camera/controllers/__init__.py | 14 +++ arcade/camera/controllers/curve_controller.py | 2 + .../camera/controllers/input_controllers.py | 2 + .../controllers/isometric_controller.py | 3 + .../simple_controller_functions.py | 87 +++++++++++++++++++ arcade/{cinematic => camera}/data.py | 0 arcade/{cinematic => camera}/default.py | 0 arcade/{cinematic => camera}/orthographic.py | 0 arcade/{cinematic => camera}/perspective.py | 0 arcade/{cinematic => camera}/simple_camera.py | 0 arcade/{cinematic => camera}/types.py | 0 arcade/cinematic/__init__.py | 26 ------ arcade/cinematic/simple_controllers.py | 1 - 15 files changed, 134 insertions(+), 27 deletions(-) create mode 100644 arcade/camera/__init__.py rename arcade/{cinematic => camera}/camera_2d.py (100%) create mode 100644 arcade/camera/controllers/__init__.py create mode 100644 arcade/camera/controllers/curve_controller.py create mode 100644 arcade/camera/controllers/input_controllers.py create mode 100644 arcade/camera/controllers/isometric_controller.py create mode 100644 arcade/camera/controllers/simple_controller_functions.py rename arcade/{cinematic => camera}/data.py (100%) rename arcade/{cinematic => camera}/default.py (100%) rename arcade/{cinematic => camera}/orthographic.py (100%) rename arcade/{cinematic => camera}/perspective.py (100%) rename arcade/{cinematic => camera}/simple_camera.py (100%) rename arcade/{cinematic => camera}/types.py (100%) delete mode 100644 arcade/cinematic/__init__.py delete mode 100644 arcade/cinematic/simple_controllers.py diff --git a/arcade/camera/__init__.py b/arcade/camera/__init__.py new file mode 100644 index 0000000000..8563d228a1 --- /dev/null +++ b/arcade/camera/__init__.py @@ -0,0 +1,26 @@ +""" +The Cinematic Types, Classes, and Methods of Arcade. +Providing a multitude of camera's for any need. +""" + +from arcade.camera.data import CameraData, OrthographicProjectionData, PerspectiveProjectionData +from arcade.camera.types import Projection, Projector, Camera + +from arcade.camera.orthographic import OrthographicProjector +from arcade.camera.perspective import PerspectiveProjector + +from arcade.camera.simple_camera import SimpleCamera +from arcade.camera.camera_2d import Camera2D + +__all__ = [ + 'Projection', + 'Projector', + 'Camera', + 'CameraData', + 'OrthographicProjectionData', + 'OrthographicProjector', + 'PerspectiveProjectionData', + 'PerspectiveProjector', + 'SimpleCamera', + 'Camera2D' +] diff --git a/arcade/cinematic/camera_2d.py b/arcade/camera/camera_2d.py similarity index 100% rename from arcade/cinematic/camera_2d.py rename to arcade/camera/camera_2d.py diff --git a/arcade/camera/controllers/__init__.py b/arcade/camera/controllers/__init__.py new file mode 100644 index 0000000000..d9efd3efd1 --- /dev/null +++ b/arcade/camera/controllers/__init__.py @@ -0,0 +1,14 @@ +from arcade.camera.controllers.simple_controller_functions import ( + simple_follow, + simple_follow_2D, + simple_easing, + simple_easing_2D +) + + +__all__ = [ + 'simple_follow', + 'simple_follow_2D', + 'simple_easing', + 'simple_easing_2D' +] \ No newline at end of file diff --git a/arcade/camera/controllers/curve_controller.py b/arcade/camera/controllers/curve_controller.py new file mode 100644 index 0000000000..c9fb81f917 --- /dev/null +++ b/arcade/camera/controllers/curve_controller.py @@ -0,0 +1,2 @@ +# Todo: Provide controllers which move a camera along a set of bezier curves +# (atleast cubic, and that fancy const acceleration one, and const speed). diff --git a/arcade/camera/controllers/input_controllers.py b/arcade/camera/controllers/input_controllers.py new file mode 100644 index 0000000000..21854b3d2b --- /dev/null +++ b/arcade/camera/controllers/input_controllers.py @@ -0,0 +1,2 @@ +# TODO: Are 2D and 3D versions of a very simple controller +# intended to be used for debugging. diff --git a/arcade/camera/controllers/isometric_controller.py b/arcade/camera/controllers/isometric_controller.py new file mode 100644 index 0000000000..5ac8bbb93c --- /dev/null +++ b/arcade/camera/controllers/isometric_controller.py @@ -0,0 +1,3 @@ +# TODO: Treats the camera as a 3D Isometric camera +# and allows for spinning around a focal point +# and moving along the isometric grid diff --git a/arcade/camera/controllers/simple_controller_functions.py b/arcade/camera/controllers/simple_controller_functions.py new file mode 100644 index 0000000000..ebfa35fe13 --- /dev/null +++ b/arcade/camera/controllers/simple_controller_functions.py @@ -0,0 +1,87 @@ +from typing import Tuple, Callable + +from arcade.camera.data import CameraData +from arcade.easing import linear + + +__all__ = [ + 'simple_follow', + 'simple_follow_2D', + 'simple_easing', + 'simple_easing_2D' +] + + +def _3_lerp(s: Tuple[float, float, float], e: Tuple[float, float, float], t: float): + s_x, s_y, s_z = s + e_x, e_y, e_z = e + + return s_x + t * (e_x - s_x), s_y + t * (e_y - s_y), s_z + t * (e_z - s_z) + + +# A set of four methods for moving a camera smoothly in a straight line in various different ways. + +def simple_follow(speed: float, target: Tuple[float, float, float], data: CameraData): + """ + A simple method which moves the camera linearly towards the target point. + + :param speed: The percentage the camera should move towards the target. + :param target: The 3D position the camera should move towards in world space. + :param data: The camera data object which stores its position, rotation, and direction. + """ + + data.position = _3_lerp(data.position, target, speed) + + +def simple_follow_2D(speed: float, target: Tuple[float, float], data: CameraData): + """ + A 2D version of simple_follow. Moves the camera only along the X and Y axis. + + :param speed: The percentage the camera should move towards the target. + :param target: The 2D position the camera should move towards in world space. + :param data: The camera data object which stores its position, rotation, and direction. + """ + simple_follow(speed, target + (0,), data) + + +def simple_easing(percent: float, + start: Tuple[float, float, float], + target: Tuple[float, float, float], + data: CameraData, func: Callable[[float], float] = linear): + """ + A simple method which moves a camera in a straight line between two provided points. + It uses an easing function to make the motion smoother. You can use the collection of + easing methods found in arcade.easing. + + :param percent: The percentage from 0 to 1 which describes + how far between the two points to place the camera. + :param start: The 3D point which acts as the starting point for the camera motion. + :param target: The 3D point which acts as the final destination for the camera. + :param data: The camera data object which stores its position, rotation, and direction. + :param func: The easing method to use. It takes in a number between 0-1 + and returns a new number in the same range but altered so the + speed does not stay constant. See arcade.easing for examples. + """ + + data.position = _3_lerp(start, target, func(percent)) + + +def simple_easing_2D(percent: float, + start: Tuple[float, float], + target: Tuple[float, float], + data: CameraData, func: Callable[[float], float] = linear): + """ + A 2D version of simple_easing. Moves the camera only along the X and Y axis. + + :param percent: The percentage from 0 to 1 which describes + how far between the two points to place the camera. + :param start: The 3D point which acts as the starting point for the camera motion. + :param target: The 3D point which acts as the final destination for the camera. + :param data: The camera data object which stores its position, rotation, and direction. + :param func: The easing method to use. It takes in a number between 0-1 + and returns a new number in the same range but altered so the + speed does not stay constant. See arcade.easing for examples. + """ + + simple_easing(percent, start + (0,), target + (0,), data, func) + diff --git a/arcade/cinematic/data.py b/arcade/camera/data.py similarity index 100% rename from arcade/cinematic/data.py rename to arcade/camera/data.py diff --git a/arcade/cinematic/default.py b/arcade/camera/default.py similarity index 100% rename from arcade/cinematic/default.py rename to arcade/camera/default.py diff --git a/arcade/cinematic/orthographic.py b/arcade/camera/orthographic.py similarity index 100% rename from arcade/cinematic/orthographic.py rename to arcade/camera/orthographic.py diff --git a/arcade/cinematic/perspective.py b/arcade/camera/perspective.py similarity index 100% rename from arcade/cinematic/perspective.py rename to arcade/camera/perspective.py diff --git a/arcade/cinematic/simple_camera.py b/arcade/camera/simple_camera.py similarity index 100% rename from arcade/cinematic/simple_camera.py rename to arcade/camera/simple_camera.py diff --git a/arcade/cinematic/types.py b/arcade/camera/types.py similarity index 100% rename from arcade/cinematic/types.py rename to arcade/camera/types.py diff --git a/arcade/cinematic/__init__.py b/arcade/cinematic/__init__.py deleted file mode 100644 index 5ee302b961..0000000000 --- a/arcade/cinematic/__init__.py +++ /dev/null @@ -1,26 +0,0 @@ -""" -The Cinematic Types, Classes, and Methods of Arcade. -Providing a multitude of camera's for any need. -""" - -from arcade.cinematic.data import CameraData, OrthographicProjectionData, PerspectiveProjectionData -from arcade.cinematic.types import Projection, Projector, Camera - -from arcade.cinematic.orthographic import OrthographicProjector -from arcade.cinematic.perspective import PerspectiveProjector - -from arcade.cinematic.simple_camera import SimpleCamera -from arcade.cinematic.camera_2d import Camera2D - -__all__ = [ - 'Projection', - 'Projector', - 'Camera', - 'CameraData', - 'OrthographicProjectionData', - 'OrthographicProjector', - 'PerspectiveProjectionData', - 'PerspectiveProjector', - 'SimpleCamera', - 'Camera2D' -] diff --git a/arcade/cinematic/simple_controllers.py b/arcade/cinematic/simple_controllers.py deleted file mode 100644 index e151d4aaf4..0000000000 --- a/arcade/cinematic/simple_controllers.py +++ /dev/null @@ -1 +0,0 @@ -# TODO: From 04b6a41018000b0837e71ea7539421d5936f53c0 Mon Sep 17 00:00:00 2001 From: DragonMoffon Date: Sat, 5 Aug 2023 02:24:32 +1200 Subject: [PATCH 20/31] fixing silly pycharm muck-up when I changed the file name it didn't update any imports tsk tsk. --- arcade/__init__.py | 2 +- arcade/application.py | 4 ++-- arcade/camera/camera_2d.py | 6 +++--- arcade/camera/controllers/__init__.py | 2 +- arcade/camera/default.py | 2 +- arcade/camera/orthographic.py | 4 ++-- arcade/camera/perspective.py | 4 ++-- arcade/camera/simple_camera.py | 6 +++--- arcade/camera/types.py | 2 +- arcade/gui/ui_manager.py | 2 +- arcade/sections.py | 4 ++-- 11 files changed, 19 insertions(+), 19 deletions(-) diff --git a/arcade/__init__.py b/arcade/__init__.py index 0113c4326d..37fb4f9673 100644 --- a/arcade/__init__.py +++ b/arcade/__init__.py @@ -218,7 +218,7 @@ def configure_logging(level: Optional[int] = None): # Module imports from arcade import color as color from arcade import csscolor as csscolor -from arcade import cinematic as cinematic +from arcade import camera as camera from arcade import key as key from arcade import resources as resources from arcade import types as types diff --git a/arcade/application.py b/arcade/application.py index 3f84cd6c85..3a3ff707e9 100644 --- a/arcade/application.py +++ b/arcade/application.py @@ -22,8 +22,8 @@ from arcade.types import Color, RGBA255, RGBA255OrNormalized from arcade import SectionManager from arcade.utils import is_raspberry_pi -from arcade.cinematic import Projector -from arcade.cinematic.default import DefaultProjector +from arcade.camera import Projector +from arcade.camera.default import DefaultProjector LOG = logging.getLogger(__name__) diff --git a/arcade/camera/camera_2d.py b/arcade/camera/camera_2d.py index 1811346f22..218c7d934d 100644 --- a/arcade/camera/camera_2d.py +++ b/arcade/camera/camera_2d.py @@ -2,9 +2,9 @@ from math import degrees, radians, atan2, cos, sin from contextlib import contextmanager -from arcade.cinematic.data import CameraData, OrthographicProjectionData -from arcade.cinematic.orthographic import OrthographicProjector -from arcade.cinematic.types import Projector +from arcade.camera.data import CameraData, OrthographicProjectionData +from arcade.camera.orthographic import OrthographicProjector +from arcade.camera.types import Projector from arcade.window_commands import get_window if TYPE_CHECKING: diff --git a/arcade/camera/controllers/__init__.py b/arcade/camera/controllers/__init__.py index d9efd3efd1..97b07b51ba 100644 --- a/arcade/camera/controllers/__init__.py +++ b/arcade/camera/controllers/__init__.py @@ -11,4 +11,4 @@ 'simple_follow_2D', 'simple_easing', 'simple_easing_2D' -] \ No newline at end of file +] diff --git a/arcade/camera/default.py b/arcade/camera/default.py index 8613c78f73..81bb38eb28 100644 --- a/arcade/camera/default.py +++ b/arcade/camera/default.py @@ -3,7 +3,7 @@ from pyglet.math import Mat4 -from arcade.cinematic.types import Projector +from arcade.camera.types import Projector from arcade.window_commands import get_window if TYPE_CHECKING: from arcade.application import Window diff --git a/arcade/camera/orthographic.py b/arcade/camera/orthographic.py index 731257a9ca..f88877e63e 100644 --- a/arcade/camera/orthographic.py +++ b/arcade/camera/orthographic.py @@ -3,8 +3,8 @@ from pyglet.math import Mat4, Vec3, Vec4 -from arcade.cinematic.data import CameraData, OrthographicProjectionData -from arcade.cinematic.types import Projector +from arcade.camera.data import CameraData, OrthographicProjectionData +from arcade.camera.types import Projector from arcade.window_commands import get_window if TYPE_CHECKING: diff --git a/arcade/camera/perspective.py b/arcade/camera/perspective.py index 58722c4b3d..b394114141 100644 --- a/arcade/camera/perspective.py +++ b/arcade/camera/perspective.py @@ -3,8 +3,8 @@ from pyglet.math import Mat4, Vec3, Vec4 -from arcade.cinematic.data import CameraData, PerspectiveProjectionData -from arcade.cinematic.types import Projector +from arcade.camera.data import CameraData, PerspectiveProjectionData +from arcade.camera.types import Projector from arcade.window_commands import get_window if TYPE_CHECKING: diff --git a/arcade/camera/simple_camera.py b/arcade/camera/simple_camera.py index daf9e4f285..11f82238e9 100644 --- a/arcade/camera/simple_camera.py +++ b/arcade/camera/simple_camera.py @@ -4,9 +4,9 @@ from pyglet.math import Vec3 -from arcade.cinematic.data import CameraData, OrthographicProjectionData -from arcade.cinematic.types import Projector -from arcade.cinematic.orthographic import OrthographicProjector +from arcade.camera.data import CameraData, OrthographicProjectionData +from arcade.camera.types import Projector +from arcade.camera.orthographic import OrthographicProjector from arcade.window_commands import get_window if TYPE_CHECKING: diff --git a/arcade/camera/types.py b/arcade/camera/types.py index 50c1ab4c03..b32600463b 100644 --- a/arcade/camera/types.py +++ b/arcade/camera/types.py @@ -1,7 +1,7 @@ from typing import Protocol, Tuple, Iterator from contextlib import contextmanager -from arcade.cinematic.data import CameraData +from arcade.camera.data import CameraData __all__ = [ diff --git a/arcade/gui/ui_manager.py b/arcade/gui/ui_manager.py index 13efe46fae..23234023a7 100644 --- a/arcade/gui/ui_manager.py +++ b/arcade/gui/ui_manager.py @@ -30,7 +30,7 @@ ) from arcade.gui.surface import Surface from arcade.gui.widgets import UIWidget, Rect -from arcade.cinematic import OrthographicProjector, OrthographicProjectionData +from arcade.camera import OrthographicProjector, OrthographicProjectionData W = TypeVar("W", bound=UIWidget) diff --git a/arcade/sections.py b/arcade/sections.py index 9115e2414d..4b5d9b76cc 100644 --- a/arcade/sections.py +++ b/arcade/sections.py @@ -4,10 +4,10 @@ from pyglet.event import EVENT_HANDLED, EVENT_UNHANDLED from arcade import get_window -from arcade.cinematic.default import DefaultProjector +from arcade.camera.default import DefaultProjector if TYPE_CHECKING: - from arcade.cinematic import Projector + from arcade.camera import Projector from arcade import View __all__ = [ From 8d7dd214c4032d9fce74d4917335d50ff515433e Mon Sep 17 00:00:00 2001 From: DragonMoffon Date: Sat, 5 Aug 2023 02:32:16 +1200 Subject: [PATCH 21/31] removing doc ref Have not setup camera documentation so removing old ref. DO NOT PULL PR UNTIL FIXED. --- doc/api_docs/arcade.rst | 1 - 1 file changed, 1 deletion(-) diff --git a/doc/api_docs/arcade.rst b/doc/api_docs/arcade.rst index 85a8c119d3..198a3d34a1 100644 --- a/doc/api_docs/arcade.rst +++ b/doc/api_docs/arcade.rst @@ -20,7 +20,6 @@ for the Python Arcade library. See also: api/sprites api/sprite_list api/sprite_scenes - api/camera api/text api/tilemap api/texture From 0bc95ea764e1a361cd20d78f07bb7a1f8b3e890c Mon Sep 17 00:00:00 2001 From: DragonMoffon Date: Sat, 5 Aug 2023 02:38:52 +1200 Subject: [PATCH 22/31] Updated Orthographic Unit Tests grrrrr pycharm --- tests/unit/camera/test_orthographic_camera.py | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/tests/unit/camera/test_orthographic_camera.py b/tests/unit/camera/test_orthographic_camera.py index 85d252354a..eced85d790 100644 --- a/tests/unit/camera/test_orthographic_camera.py +++ b/tests/unit/camera/test_orthographic_camera.py @@ -1,32 +1,32 @@ import pytest as pytest -from arcade import cinematic, Window +from arcade import camera, Window def test_orthographic_camera(window: Window): default_camera = window.current_camera - cam_default = cinematic.OrthographicProjector() - default_view = cam_default.view - default_projection = cam_default.projection + cam_default = camera.OrthographicProjector() + default_view = cam_default.view_data + default_projection = cam_default.projection_data # test that the camera correctly generated the default view and projection PoDs. - assert default_view == cinematic.CameraData( + assert default_view == camera.CameraData( (0, 0, window.width, window.height), # Viewport (window.width/2, window.height/2, 0), # Position (0.0, 1.0, 0.0), # Up (0.0, 0.0, 1.0), # Forward 1.0, # Zoom ) - assert default_projection == cinematic.OrthographicProjectionData( + assert default_projection == camera.OrthographicProjectionData( -0.5 * window.width, 0.5 * window.width, # Left, Right -0.5 * window.height, 0.5 * window.height, # Bottom, Top -100, 100 # Near, Far ) # test that the camera properties work - assert cam_default.position == default_view.position - assert cam_default.viewport == default_view.viewport + assert cam_default.view_data.position == default_view.position + assert cam_default.view_data.viewport == default_view.viewport # Test that the camera is actually recognised by the camera as being activated assert window.current_camera == default_camera @@ -40,30 +40,30 @@ def test_orthographic_camera(window: Window): default_camera.use() assert window.current_camera == default_camera - set_view = cinematic.CameraData( + set_view = camera.CameraData( (0, 0, 1, 1), # Viewport (0.0, 0.0, 0.0), # Position (0.0, 1.0, 0.0), # Up (0.0, 0.0, 1.0), # Forward 1.0 # Zoom ) - set_projection = cinematic.OrthographicProjectionData( + set_projection = camera.OrthographicProjectionData( 0.0, 1.0, # Left, Right 0.0, 1.0, # Bottom, Top -1.0, 1.0 # Near, Far ) - cam_set = cinematic.OrthographicProjector( + cam_set = camera.OrthographicProjector( view=set_view, projection=set_projection ) # test that the camera correctly used the provided Pods. - assert cam_set.view == set_view - assert cam_set.projection == set_projection + assert cam_set.view_data == set_view + assert cam_set.projection_data == set_projection # test that the camera properties work - assert cam_set.position == set_view.position - assert cam_set.viewport == set_view.viewport + assert cam_set.view_data.position == set_view.position + assert cam_set.view_data.viewport == set_view.viewport # Test that the camera is actually recognised by the camera as being activated assert window.current_camera == default_camera From 61dbbab8a193a26374dbd6b6e6c40ca4ffb7dc37 Mon Sep 17 00:00:00 2001 From: DragonMoffon Date: Mon, 7 Aug 2023 00:09:03 +1200 Subject: [PATCH 23/31] Fixed all the examples NOTE this is a quick fix. It removed shaking from two examples. CANNOT BE PULLED IN WHILE THIS IS UNRESOLVED. Weirdly the linters didn't pick up on these errors --- arcade/camera/controllers/curve_controller.py | 2 +- arcade/camera/simple_camera.py | 17 ++++++++++++++++- arcade/examples/background_blending.py | 2 +- arcade/examples/background_groups.py | 2 +- arcade/examples/background_parallax.py | 2 +- arcade/examples/background_scrolling.py | 2 +- arcade/examples/background_stationary.py | 2 +- arcade/examples/camera_platform.py | 6 +++--- arcade/examples/minimap_camera.py | 6 +++--- arcade/examples/sprite_move_scrolling_shake.py | 11 ++++++----- util/update_quick_index.py | 2 ++ 11 files changed, 36 insertions(+), 18 deletions(-) diff --git a/arcade/camera/controllers/curve_controller.py b/arcade/camera/controllers/curve_controller.py index c9fb81f917..9adf52aa91 100644 --- a/arcade/camera/controllers/curve_controller.py +++ b/arcade/camera/controllers/curve_controller.py @@ -1,2 +1,2 @@ # Todo: Provide controllers which move a camera along a set of bezier curves -# (atleast cubic, and that fancy const acceleration one, and const speed). +# (atleast cubic, and that fancy acceleration one, and const speed). diff --git a/arcade/camera/simple_camera.py b/arcade/camera/simple_camera.py index 11f82238e9..b36da3f5d0 100644 --- a/arcade/camera/simple_camera.py +++ b/arcade/camera/simple_camera.py @@ -109,7 +109,7 @@ def viewport(self, viewport: Tuple[int, int, int, int]) -> None: """ Set the viewport (left, bottom, width, height) """ self.set_viewport(viewport) - def set_viewport(self, viewport:Tuple[int, int, int, int]) -> None: + def set_viewport(self, viewport: Tuple[int, int, int, int]) -> None: self._view.viewport = viewport @property @@ -329,3 +329,18 @@ def map_coordinate(self, screen_coordinate: Tuple[float, float]) -> Tuple[float, # TODO: better doc string return self._camera.map_coordinate(screen_coordinate) + + def resize(self, viewport_width: int, viewport_height: int, *, + resize_projection: bool = True) -> None: + """ + Resize the camera's viewport. Call this when the window resizes. + + :param int viewport_width: Width of the viewport + :param int viewport_height: Height of the viewport + :param bool resize_projection: if True the projection will also be resized + """ + new_viewport = (self.viewport[0], self.viewport[1], viewport_width, viewport_height) + self.set_viewport(new_viewport) + if resize_projection: + self.projection = (self._projection.left, viewport_width, + self._projection.bottom, viewport_height) diff --git a/arcade/examples/background_blending.py b/arcade/examples/background_blending.py index b1a8048edf..8e2820c29f 100644 --- a/arcade/examples/background_blending.py +++ b/arcade/examples/background_blending.py @@ -23,7 +23,7 @@ class MyGame(arcade.Window): def __init__(self): super().__init__(SCREEN_WIDTH, SCREEN_HEIGHT, SCREEN_TITLE, resizable=True) - self.camera = arcade.SimpleCamera() + self.camera = arcade.camera.SimpleCamera() # Load the first background from file. Sized to match the screen self.background_1 = background.Background.from_file( diff --git a/arcade/examples/background_groups.py b/arcade/examples/background_groups.py index caddaf0884..9e1c7d714d 100644 --- a/arcade/examples/background_groups.py +++ b/arcade/examples/background_groups.py @@ -28,7 +28,7 @@ def __init__(self): # Set the background color to equal to that of the first background. self.background_color = (5, 44, 70) - self.camera = arcade.SimpleCamera() + self.camera = arcade.camera.SimpleCamera() # create a background group which will hold all the backgrounds. self.backgrounds = background.BackgroundGroup() diff --git a/arcade/examples/background_parallax.py b/arcade/examples/background_parallax.py index 0ab9243ea7..878cac944f 100644 --- a/arcade/examples/background_parallax.py +++ b/arcade/examples/background_parallax.py @@ -38,7 +38,7 @@ def __init__(self): # Set the background color to match the sky in the background images self.background_color = (162, 84, 162, 255) - self.camera = arcade.SimpleCamera() + self.camera = arcade.camera.SimpleCamera() # Create a background group to hold all the landscape's layers self.backgrounds = background.ParallaxGroup() diff --git a/arcade/examples/background_scrolling.py b/arcade/examples/background_scrolling.py index 4ba542559a..7f22ed06f2 100644 --- a/arcade/examples/background_scrolling.py +++ b/arcade/examples/background_scrolling.py @@ -23,7 +23,7 @@ class MyGame(arcade.Window): def __init__(self): super().__init__(SCREEN_WIDTH, SCREEN_HEIGHT, SCREEN_TITLE, resizable=True) - self.camera = arcade.SimpleCamera() + self.camera = arcade.camera.SimpleCamera() # Load the background from file. Sized to match the screen self.background = background.Background.from_file( diff --git a/arcade/examples/background_stationary.py b/arcade/examples/background_stationary.py index 2f6e0a9cba..cc52359a98 100644 --- a/arcade/examples/background_stationary.py +++ b/arcade/examples/background_stationary.py @@ -22,7 +22,7 @@ class MyGame(arcade.Window): def __init__(self): super().__init__(SCREEN_WIDTH, SCREEN_HEIGHT, SCREEN_TITLE, resizable=True) - self.camera = arcade.SimpleCamera() + self.camera = arcade.camera.SimpleCamera() # Load the background from file. It defaults to the size of the texture with the bottom left corner at (0, 0). # Image from: diff --git a/arcade/examples/camera_platform.py b/arcade/examples/camera_platform.py index 3fe65e609e..16016a8a5a 100644 --- a/arcade/examples/camera_platform.py +++ b/arcade/examples/camera_platform.py @@ -130,8 +130,8 @@ def setup(self): self.scene.add_sprite("Player", self.player_sprite) viewport = (0, 0, SCREEN_WIDTH, SCREEN_HEIGHT) - self.camera = arcade.Camera(viewport=viewport) - self.gui_camera = arcade.Camera(viewport=viewport) + self.camera = arcade.camera.SimpleCamera(viewport=viewport) + self.gui_camera = arcade.camera.SimpleCamera(viewport=viewport) # Center camera on user self.pan_camera_to_user() @@ -255,7 +255,7 @@ def on_update(self, delta_time): for bomb in bombs_hit: bomb.remove_from_sprite_lists() print("Pow") - self.camera.shake((4, 7)) + # TODO: self.camera.shake((4, 7)) -> Camera Missing This Functionality # Pan to the user self.pan_camera_to_user(panning_fraction=0.12) diff --git a/arcade/examples/minimap_camera.py b/arcade/examples/minimap_camera.py index 229bcd3739..ad7575886f 100644 --- a/arcade/examples/minimap_camera.py +++ b/arcade/examples/minimap_camera.py @@ -56,7 +56,7 @@ def __init__(self, width, height, title): DEFAULT_SCREEN_HEIGHT - MINIMAP_HEIGHT, MINIMAP_WIDTH, MINIMAP_HEIGHT) minimap_projection = (0, MAP_PROJECTION_WIDTH, 0, MAP_PROJECTION_HEIGHT) - self.camera_minimap = arcade.Camera(viewport=minimap_viewport, projection=minimap_projection) + self.camera_minimap = arcade.camera.SimpleCamera(viewport=minimap_viewport, projection=minimap_projection) # Set up the player self.player_sprite = None @@ -66,8 +66,8 @@ def __init__(self, width, height, title): # Camera for sprites, and one for our GUI viewport = (0, 0, DEFAULT_SCREEN_WIDTH, DEFAULT_SCREEN_HEIGHT) projection = (0, DEFAULT_SCREEN_WIDTH, 0, DEFAULT_SCREEN_HEIGHT) - self.camera_sprites = arcade.Camera(viewport=viewport, projection=projection) - self.camera_gui = arcade.SimpleCamera(viewport=viewport) + self.camera_sprites = arcade.camera.SimpleCamera(viewport=viewport, projection=projection) + self.camera_gui = arcade.camera.SimpleCamera(viewport=viewport) self.selected_camera = self.camera_minimap diff --git a/arcade/examples/sprite_move_scrolling_shake.py b/arcade/examples/sprite_move_scrolling_shake.py index d1778fd2d9..fc20916dd6 100644 --- a/arcade/examples/sprite_move_scrolling_shake.py +++ b/arcade/examples/sprite_move_scrolling_shake.py @@ -54,8 +54,8 @@ def __init__(self, width, height, title): # Create the cameras. One for the GUI, one for the sprites. # We scroll the 'sprite world' but not the GUI. - self.camera_sprites = arcade.Camera() - self.camera_gui = arcade.Camera() + self.camera_sprites = arcade.camera.SimpleCamera() + self.camera_gui = arcade.camera.Camera() self.explosion_sound = arcade.load_sound(":resources:sounds/explosion1.wav") @@ -166,9 +166,10 @@ def on_update(self, delta_time): # How fast to damp the shake shake_damping = 0.9 # Do the shake - self.camera_sprites.shake(shake_vector, - speed=shake_speed, - damping=shake_damping) + # TODO: Camera missing shake. + # self.camera_sprites.shake(shake_vector, + # speed=shake_speed, + # damping=shake_damping) def scroll_to_player(self): """ diff --git a/util/update_quick_index.py b/util/update_quick_index.py index 24bd1d11ce..66b5b73458 100644 --- a/util/update_quick_index.py +++ b/util/update_quick_index.py @@ -48,6 +48,7 @@ 'texture/solid_color.py': ['Texture Management', 'texture.rst'], 'texture/tools.py': ['Texture Management', 'texture.rst'], 'texture/transforms.py': ['Texture Transforms', 'texture_transforms.rst'], + 'camera/camera_2d.py': ['Camera 2D', 'camera_2d.rst'], 'math.py': ['Math', 'math.rst'], 'types.py': ['Types', 'types.rst'], 'easing.py': ['Easing', 'easing.rst'], @@ -202,6 +203,7 @@ def process_directory(directory: Path, quick_index_file): "math.py": "arcade.math", "earclip.py": "arcade.earclip", "shape_list.py": "arcade.shape_list", + "camera": "arcade.camera" } package = mapping.get(path.name, None) or mapping.get(directory.name, None) From 10c6a70a49f4d41e69af1ea1780098c8ed68f78b Mon Sep 17 00:00:00 2001 From: DragonMoffon Date: Mon, 7 Aug 2023 00:10:18 +1200 Subject: [PATCH 24/31] linting fix 1-million --- arcade/examples/sprite_move_scrolling_shake.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/arcade/examples/sprite_move_scrolling_shake.py b/arcade/examples/sprite_move_scrolling_shake.py index fc20916dd6..0226a03746 100644 --- a/arcade/examples/sprite_move_scrolling_shake.py +++ b/arcade/examples/sprite_move_scrolling_shake.py @@ -157,14 +157,14 @@ def on_update(self, delta_time): # How 'far' to shake shake_amplitude = 10 # Calculate a vector based on that - shake_vector = ( - math.cos(shake_direction) * shake_amplitude, - math.sin(shake_direction) * shake_amplitude - ) + # shake_vector = ( + # math.cos(shake_direction) * shake_amplitude, + # math.sin(shake_direction) * shake_amplitude + # ) # Frequency of the shake - shake_speed = 1.5 + # shake_speed = 1.5 # How fast to damp the shake - shake_damping = 0.9 + # shake_damping = 0.9 # Do the shake # TODO: Camera missing shake. # self.camera_sprites.shake(shake_vector, From bff0c03e9482b0a300c174f6f95d067ec1da5a58 Mon Sep 17 00:00:00 2001 From: DragonMoffon Date: Mon, 7 Aug 2023 00:11:41 +1200 Subject: [PATCH 25/31] 1-million and 1 --- arcade/examples/sprite_move_scrolling_shake.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/arcade/examples/sprite_move_scrolling_shake.py b/arcade/examples/sprite_move_scrolling_shake.py index 0226a03746..f94f7fe939 100644 --- a/arcade/examples/sprite_move_scrolling_shake.py +++ b/arcade/examples/sprite_move_scrolling_shake.py @@ -153,9 +153,9 @@ def on_update(self, delta_time): # --- Shake the camera --- # Pick a random direction - shake_direction = random.random() * 2 * math.pi + # shake_direction = random.random() * 2 * math.pi # How 'far' to shake - shake_amplitude = 10 + # shake_amplitude = 10 # Calculate a vector based on that # shake_vector = ( # math.cos(shake_direction) * shake_amplitude, From da729424740a004badc347cada73743077534a18 Mon Sep 17 00:00:00 2001 From: DragonMoffon Date: Mon, 7 Aug 2023 00:12:19 +1200 Subject: [PATCH 26/31] 1-million and 2 --- arcade/examples/sprite_move_scrolling_shake.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/arcade/examples/sprite_move_scrolling_shake.py b/arcade/examples/sprite_move_scrolling_shake.py index f94f7fe939..2dd46b150a 100644 --- a/arcade/examples/sprite_move_scrolling_shake.py +++ b/arcade/examples/sprite_move_scrolling_shake.py @@ -8,7 +8,7 @@ """ import random -import math +# import math import arcade SPRITE_SCALING = 0.5 From e2b1693d93584fef1104f6905c0afc0750325eff Mon Sep 17 00:00:00 2001 From: DragonMoffon Date: Mon, 7 Aug 2023 00:25:22 +1200 Subject: [PATCH 27/31] MOAR example fixing --- arcade/examples/minimap.py | 4 ++-- arcade/examples/platform_tutorial/06_camera.py | 2 +- arcade/examples/platform_tutorial/07_coins_and_sound.py | 2 +- arcade/examples/platform_tutorial/08_score.py | 4 ++-- arcade/examples/platform_tutorial/09_load_map.py | 4 ++-- arcade/examples/platform_tutorial/10_multiple_levels.py | 4 ++-- arcade/examples/platform_tutorial/11_ladders_and_more.py | 4 ++-- arcade/examples/platform_tutorial/12_animate_character.py | 4 ++-- arcade/examples/platform_tutorial/13_add_enemies.py | 4 ++-- arcade/examples/platform_tutorial/14_moving_enemies.py | 4 ++-- .../examples/platform_tutorial/15_collision_with_enemies.py | 4 ++-- arcade/examples/platform_tutorial/16_shooting_bullets.py | 4 ++-- arcade/examples/platform_tutorial/17_views.py | 4 ++-- arcade/examples/procedural_caves_cellular.py | 4 ++-- arcade/examples/sprite_move_scrolling.py | 4 ++-- arcade/examples/sprite_move_scrolling_box.py | 4 ++-- arcade/examples/sprite_moving_platforms.py | 4 ++-- arcade/examples/sprite_tiled_map.py | 4 ++-- arcade/examples/template_platformer.py | 4 ++-- 19 files changed, 36 insertions(+), 36 deletions(-) diff --git a/arcade/examples/minimap.py b/arcade/examples/minimap.py index 6478f6526a..6add0430ea 100644 --- a/arcade/examples/minimap.py +++ b/arcade/examples/minimap.py @@ -63,8 +63,8 @@ def __init__(self, width, height, title): # Camera for sprites, and one for our GUI viewport = (0, 0, DEFAULT_SCREEN_WIDTH, DEFAULT_SCREEN_HEIGHT) - self.camera_sprites = arcade.SimpleCamera(viewport=viewport) - self.camera_gui = arcade.SimpleCamera(viewport=viewport) + self.camera_sprites = arcade.camera.SimpleCamera(viewport=viewport) + self.camera_gui = arcade.camera.SimpleCamera(viewport=viewport) def setup(self): """ Set up the game and initialize the variables. """ diff --git a/arcade/examples/platform_tutorial/06_camera.py b/arcade/examples/platform_tutorial/06_camera.py index 047d38c88d..38b7526308 100644 --- a/arcade/examples/platform_tutorial/06_camera.py +++ b/arcade/examples/platform_tutorial/06_camera.py @@ -48,7 +48,7 @@ def setup(self): """Set up the game here. Call this function to restart the game.""" # Set up the Camera - self.camera = arcade.SimpleCamera(viewport=(0, 0, self.width, self.height)) + self.camera = arcade.camera.SimpleCamera(viewport=(0, 0, self.width, self.height)) # Initialize Scene self.scene = arcade.Scene() diff --git a/arcade/examples/platform_tutorial/07_coins_and_sound.py b/arcade/examples/platform_tutorial/07_coins_and_sound.py index 2fa961c626..f0ec91192d 100644 --- a/arcade/examples/platform_tutorial/07_coins_and_sound.py +++ b/arcade/examples/platform_tutorial/07_coins_and_sound.py @@ -53,7 +53,7 @@ def setup(self): """Set up the game here. Call this function to restart the game.""" # Set up the Camera - self.camera = arcade.SimpleCamera(viewport=(0, 0, self.width, self.height)) + self.camera = arcade.camera.SimpleCamera(viewport=(0, 0, self.width, self.height)) # Initialize Scene self.scene = arcade.Scene() diff --git a/arcade/examples/platform_tutorial/08_score.py b/arcade/examples/platform_tutorial/08_score.py index 368321d332..588147037e 100644 --- a/arcade/examples/platform_tutorial/08_score.py +++ b/arcade/examples/platform_tutorial/08_score.py @@ -59,10 +59,10 @@ def setup(self): """Set up the game here. Call this function to restart the game.""" # Set up the Game Camera - self.camera = arcade.SimpleCamera(viewport=(0, 0, self.width, self.height)) + self.camera = arcade.camera.SimpleCamera(viewport=(0, 0, self.width, self.height)) # Set up the GUI Camera - self.gui_camera = arcade.SimpleCamera(viewport=(0, 0, self.width, self.height)) + self.gui_camera = arcade.camera.SimpleCamera(viewport=(0, 0, self.width, self.height)) # Keep track of the score self.score = 0 diff --git a/arcade/examples/platform_tutorial/09_load_map.py b/arcade/examples/platform_tutorial/09_load_map.py index 54b45da449..253a16b4ba 100644 --- a/arcade/examples/platform_tutorial/09_load_map.py +++ b/arcade/examples/platform_tutorial/09_load_map.py @@ -63,8 +63,8 @@ def setup(self): # Set up the Cameras viewport = (0, 0, self.width, self.height) - self.camera = arcade.SimpleCamera(viewport=viewport) - self.gui_camera = arcade.SimpleCamera(viewport=viewport) + self.camera = arcade.camera.SimpleCamera(viewport=viewport) + self.gui_camera = arcade.camera.SimpleCamera(viewport=viewport) # Name of map file to load map_name = ":resources:tiled_maps/map.json" diff --git a/arcade/examples/platform_tutorial/10_multiple_levels.py b/arcade/examples/platform_tutorial/10_multiple_levels.py index 1b333d2b0b..eee99e09a1 100644 --- a/arcade/examples/platform_tutorial/10_multiple_levels.py +++ b/arcade/examples/platform_tutorial/10_multiple_levels.py @@ -84,8 +84,8 @@ def setup(self): # Set up the Cameras viewport = (0, 0, self.width, self.height) - self.camera = arcade.SimpleCamera(viewport=viewport) - self.gui_camera = arcade.SimpleCamera(viewport=viewport) + self.camera = arcade.camera.SimpleCamera(viewport=viewport) + self.gui_camera = arcade.camera.SimpleCamera(viewport=viewport) # Map name map_name = f":resources:tiled_maps/map2_level_{self.level}.json" diff --git a/arcade/examples/platform_tutorial/11_ladders_and_more.py b/arcade/examples/platform_tutorial/11_ladders_and_more.py index e82c9b5810..370989ed49 100644 --- a/arcade/examples/platform_tutorial/11_ladders_and_more.py +++ b/arcade/examples/platform_tutorial/11_ladders_and_more.py @@ -78,8 +78,8 @@ def setup(self): # Set up the Cameras viewport = (0, 0, self.width, self.height) - self.camera = arcade.SimpleCamera(viewport=viewport) - self.gui_camera = arcade.SimpleCamera(viewport=viewport) + self.camera = arcade.camera.SimpleCamera(viewport=viewport) + self.gui_camera = arcade.camera.SimpleCamera(viewport=viewport) # Map name map_name = ":resources:tiled_maps/map_with_ladders.json" diff --git a/arcade/examples/platform_tutorial/12_animate_character.py b/arcade/examples/platform_tutorial/12_animate_character.py index 08b523ae24..e9a7567927 100644 --- a/arcade/examples/platform_tutorial/12_animate_character.py +++ b/arcade/examples/platform_tutorial/12_animate_character.py @@ -199,8 +199,8 @@ def setup(self): # Set up the Cameras viewport = (0, 0, self.width, self.height) - self.camera = arcade.SimpleCamera(viewport=viewport) - self.gui_camera = arcade.SimpleCamera(viewport=viewport) + self.camera = arcade.camera.SimpleCamera(viewport=viewport) + self.gui_camera = arcade.camera.SimpleCamera(viewport=viewport) # Map name map_name = ":resources:tiled_maps/map_with_ladders.json" diff --git a/arcade/examples/platform_tutorial/13_add_enemies.py b/arcade/examples/platform_tutorial/13_add_enemies.py index f4952157f3..7b5783551b 100644 --- a/arcade/examples/platform_tutorial/13_add_enemies.py +++ b/arcade/examples/platform_tutorial/13_add_enemies.py @@ -220,8 +220,8 @@ def setup(self): # Set up the Cameras viewport = (0, 0, self.width, self.height) - self.camera = arcade.SimpleCamera(viewport=viewport) - self.gui_camera = arcade.SimpleCamera(viewport=viewport) + self.camera = arcade.camera.SimpleCamera(viewport=viewport) + self.gui_camera = arcade.camera.SimpleCamera(viewport=viewport) # Map name map_name = ":resources:tiled_maps/map_with_ladders.json" diff --git a/arcade/examples/platform_tutorial/14_moving_enemies.py b/arcade/examples/platform_tutorial/14_moving_enemies.py index 3312bc15f5..3aa3362894 100644 --- a/arcade/examples/platform_tutorial/14_moving_enemies.py +++ b/arcade/examples/platform_tutorial/14_moving_enemies.py @@ -251,8 +251,8 @@ def setup(self): # Set up the Cameras viewport = (0, 0, self.width, self.height) - self.camera = arcade.SimpleCamera(viewport=viewport) - self.gui_camera = arcade.SimpleCamera(viewport=viewport) + self.camera = arcade.camera.SimpleCamera(viewport=viewport) + self.gui_camera = arcade.camera.SimpleCamera(viewport=viewport) # Map name map_name = ":resources:tiled_maps/map_with_ladders.json" diff --git a/arcade/examples/platform_tutorial/15_collision_with_enemies.py b/arcade/examples/platform_tutorial/15_collision_with_enemies.py index 5eea8deaff..9185a29156 100644 --- a/arcade/examples/platform_tutorial/15_collision_with_enemies.py +++ b/arcade/examples/platform_tutorial/15_collision_with_enemies.py @@ -251,8 +251,8 @@ def setup(self): # Set up the Cameras viewport = (0, 0, self.width, self.height) - self.camera = arcade.SimpleCamera(viewport=viewport) - self.gui_camera = arcade.SimpleCamera(viewport=viewport) + self.camera = arcade.camera.SimpleCamera(viewport=viewport) + self.gui_camera = arcade.camera.SimpleCamera(viewport=viewport) # Map name map_name = ":resources:tiled_maps/map_with_ladders.json" diff --git a/arcade/examples/platform_tutorial/16_shooting_bullets.py b/arcade/examples/platform_tutorial/16_shooting_bullets.py index 1caab59d56..1c226b8221 100644 --- a/arcade/examples/platform_tutorial/16_shooting_bullets.py +++ b/arcade/examples/platform_tutorial/16_shooting_bullets.py @@ -270,8 +270,8 @@ def setup(self): # Setup the Cameras viewport = (0, 0, self.width, self.height) - self.camera = arcade.SimpleCamera(viewport=viewport) - self.gui_camera = arcade.SimpleCamera(viewport=viewport) + self.camera = arcade.camera.SimpleCamera(viewport=viewport) + self.gui_camera = arcade.camera.SimpleCamera(viewport=viewport) # Map name map_name = ":resources:tiled_maps/map_with_ladders.json" diff --git a/arcade/examples/platform_tutorial/17_views.py b/arcade/examples/platform_tutorial/17_views.py index bb83827e4f..ef3b82c5b0 100644 --- a/arcade/examples/platform_tutorial/17_views.py +++ b/arcade/examples/platform_tutorial/17_views.py @@ -292,8 +292,8 @@ def setup(self): # Set up the Cameras viewport = (0, 0, self.window.width, self.window.height) - self.camera = arcade.SimpleCamera(viewport=viewport) - self.gui_camera = arcade.SimpleCamera(viewport=viewport) + self.camera = arcade.camera.SimpleCamera(viewport=viewport) + self.gui_camera = arcade.camera.SimpleCamera(viewport=viewport) # Map name map_name = ":resources:tiled_maps/map_with_ladders.json" diff --git a/arcade/examples/procedural_caves_cellular.py b/arcade/examples/procedural_caves_cellular.py index dcdaa4793a..5150bc2b21 100644 --- a/arcade/examples/procedural_caves_cellular.py +++ b/arcade/examples/procedural_caves_cellular.py @@ -155,8 +155,8 @@ def __init__(self): # Create the cameras. One for the GUI, one for the sprites. # We scroll the 'sprite world' but not the GUI. - self.camera_sprites = arcade.SimpleCamera() - self.camera_gui = arcade.SimpleCamera() + self.camera_sprites = arcade.camera.SimpleCamera() + self.camera_gui = arcade.camera.SimpleCamera() self.window.background_color = arcade.color.BLACK diff --git a/arcade/examples/sprite_move_scrolling.py b/arcade/examples/sprite_move_scrolling.py index 4f661e6c18..3203d4ee89 100644 --- a/arcade/examples/sprite_move_scrolling.py +++ b/arcade/examples/sprite_move_scrolling.py @@ -55,8 +55,8 @@ def __init__(self, width, height, title): # Create the cameras. One for the GUI, one for the sprites. # We scroll the 'sprite world' but not the GUI. - self.camera_sprites = arcade.SimpleCamera() - self.camera_gui = arcade.SimpleCamera() + self.camera_sprites = arcade.camera.SimpleCamera() + self.camera_gui = arcade.camera.SimpleCamera() def setup(self): """ Set up the game and initialize the variables. """ diff --git a/arcade/examples/sprite_move_scrolling_box.py b/arcade/examples/sprite_move_scrolling_box.py index 1baf848ad6..4dfddbfe81 100644 --- a/arcade/examples/sprite_move_scrolling_box.py +++ b/arcade/examples/sprite_move_scrolling_box.py @@ -55,8 +55,8 @@ def __init__(self, width, height, title): self.up_pressed = False self.down_pressed = False - self.camera_sprites = arcade.SimpleCamera() - self.camera_gui = arcade.SimpleCamera() + self.camera_sprites = arcade.camera.SimpleCamera() + self.camera_gui = arcade.camera.SimpleCamera() def setup(self): """ Set up the game and initialize the variables. """ diff --git a/arcade/examples/sprite_moving_platforms.py b/arcade/examples/sprite_moving_platforms.py index 11606f8f64..623031c47a 100644 --- a/arcade/examples/sprite_moving_platforms.py +++ b/arcade/examples/sprite_moving_platforms.py @@ -55,8 +55,8 @@ def __init__(self, width, height, title): # Create the cameras. One for the GUI, one for the sprites. # We scroll the 'sprite world' but not the GUI. - self.camera_sprites = arcade.SimpleCamera() - self.camera_gui = arcade.SimpleCamera() + self.camera_sprites = arcade.camera.SimpleCamera() + self.camera_gui = arcade.camera.SimpleCamera() self.left_down = False self.right_down = False diff --git a/arcade/examples/sprite_tiled_map.py b/arcade/examples/sprite_tiled_map.py index 0761c38a53..e16050aecc 100644 --- a/arcade/examples/sprite_tiled_map.py +++ b/arcade/examples/sprite_tiled_map.py @@ -126,8 +126,8 @@ def setup(self): self.player_sprite, walls, gravity_constant=GRAVITY ) - self.camera = arcade.SimpleCamera() - self.gui_camera = arcade.SimpleCamera() + self.camera = arcade.camera.SimpleCamera() + self.gui_camera = arcade.camera.SimpleCamera() # Center camera on user self.pan_camera_to_user() diff --git a/arcade/examples/template_platformer.py b/arcade/examples/template_platformer.py index 35a4a165e4..9509406993 100644 --- a/arcade/examples/template_platformer.py +++ b/arcade/examples/template_platformer.py @@ -65,8 +65,8 @@ def setup(self): """Set up the game here. Call this function to restart the game.""" # Setup the Cameras - self.camera_sprites = arcade.SimpleCamera() - self.camera_gui = arcade.SimpleCamera() + self.camera_sprites = arcade.camera.SimpleCamera() + self.camera_gui = arcade.camera.SimpleCamera() # Name of map file to load map_name = ":resources:tiled_maps/map.json" From eafa68cda1393df21842bc18649df7a445618d2a Mon Sep 17 00:00:00 2001 From: DragonMoffon Date: Mon, 7 Aug 2023 03:04:03 +1200 Subject: [PATCH 28/31] Setup 4 Splines for SplineController setup lerp, quadratic, cubic, b-spline Contemplating how to do spline controller --- arcade/camera/controllers/curve_controller.py | 143 +++++++++++++++++- 1 file changed, 141 insertions(+), 2 deletions(-) diff --git a/arcade/camera/controllers/curve_controller.py b/arcade/camera/controllers/curve_controller.py index 9adf52aa91..8b8e54ba6d 100644 --- a/arcade/camera/controllers/curve_controller.py +++ b/arcade/camera/controllers/curve_controller.py @@ -1,2 +1,141 @@ -# Todo: Provide controllers which move a camera along a set of bezier curves -# (atleast cubic, and that fancy acceleration one, and const speed). +from typing import Tuple + + +def _lerp_2D(_p1: Tuple[float, float], _p2: Tuple[float, float], _t: float): + _x1, _y1 = _p1 + _x2, _y2 = _p2 + + return _x1 + _t*(_x2 - _x1), _y1 + _t*(_y2 - _y1) + + +def _lerp_3D(_p1: Tuple[float, float, float], _p2: Tuple[float, float, float], _t: float): + _x1, _y1, _z1 = _p1 + _x2, _y2, _z2 = _p2 + + return _x1 + _t*(_x2 - _x1), _y1 + _t*(_y2 - _y1), _z1 + _t*(_z2 - _z1) + + +def _quad_2D(_p1: Tuple[float, float], _p2: Tuple[float, float], + _p3: Tuple[float, float], + _t: float): + _x1, _y1 = _p1 + _x2, _y2 = _p2 + _x3, _y3 = _p3 + _t2 = _t**2.0 + + return ( + _x1*(1.0 - 2.0*_t + _t2) + 2.0*_x2*(_t - _t2) + _x3*_t2, + _y1*(1.0 - 2.0*_t + _t2) + 2.0*_y2*(_t - _t2) + _y3*_t2 + ) + + +def _quad_3D(_p1: Tuple[float, float, float], _p2: Tuple[float, float, float], + _p3: Tuple[float, float, float], + _t: float): + _x1, _y1, _z1 = _p1 + _x2, _y2, _z2 = _p2 + _x3, _y3, _z3 = _p3 + _t2 = _t**2.0 + + return ( + _x1*(1.0 - 2.0*_t + _t2) + 2.0*_x2*(_t - _t2) + _x3*_t2, + _y1*(1.0 - 2.0*_t + _t2) + 2.0*_y2*(_t - _t2) + _y3*_t2, + _z1*(1.0 - 2.0*_t + _t2) + 2.0*_z2*(_t - _t2) + _z3*_t2 + ) + + +def _cubic_2D(_p1: Tuple[float, float], _p2: Tuple[float, float], + _p3: Tuple[float, float], _p4: Tuple[float, float], + _t: float): + _x1, _y1 = _p1 + _x2, _y2 = _p2 + _x3, _y3 = _p3 + _x4, _y4 = _p4 + _t2, _t3 = _t**2.0, _t**3.0 + + return ( + _x1*(-_t3 + 3.0*_t2 - 3.0*_t + 1.0) + _x2*(3.0*_t3 - 6.0*_t2 + 3.0*_t) + 3.0*_x3*(-_t3 + _t2) + _x4*_t3, + _y1*(-_t3 + 3.0*_t2 - 3.0*_t + 1.0) + _y2*(3.0*_t3 - 6.0*_t2 + 3.0*_t) + 3.0*_y3*(-_t3 + _t2) + _y4*_t3 + ) + + +def _cubic_3D(_p1: Tuple[float, float, float], _p2: Tuple[float, float, float], + _p3: Tuple[float, float, float], _p4: Tuple[float, float, float], + _t: float): + _x1, _y1, _z1 = _p1 + _x2, _y2, _z2 = _p2 + _x3, _y3, _z3 = _p3 + _x4, _y4, _z4 = _p4 + _t2, _t3 = _t**2.0, _t**3.0 + + return ( + _x1*(-_t3 + 3.0*_t2 - 3.0*_t + 1.0) + _x2*(3.0*_t3 - 6.0*_t2 + 3.0*_t) + 3.0*_x3*(-_t3 + _t2) + _x4*_t3, + _y1*(-_t3 + 3.0*_t2 - 3.0*_t + 1.0) + _y2*(3.0*_t3 - 6.0*_t2 + 3.0*_t) + 3.0*_y3*(-_t3 + _t2) + _y4*_t3, + _z1*(-_t3 + 3.0*_t2 - 3.0*_t + 1.0) + _z2*(3.0*_t3 - 6.0*_t2 + 3.0*_t) + 3.0*_z3*(-_t3 + _t2) + _z4*_t3 + ) + + +def _b_spline_2D(_p1: Tuple[float, float], _p2: Tuple[float, float], + _p3: Tuple[float, float], _p4: Tuple[float, float], + _t: float): + _x1, _y1 = _p1 + _x2, _y2 = _p2 + _x3, _y3 = _p3 + _x4, _y4 = _p4 + _t2, _t3 = _t**2.0, _t**3.0 + + return ( + (1/6)*( + _x1*(-_t3 + 3.0*_t2 - 3.0*_t + 1.0) + + _x2*(3.0*_t3 - 6.0*_t2 + 4.0) + + _x3*(-3.0*_t3 + 3*_t2 + 3.0*_t + 1.0) + + _x4*_t3 + ), + (1/6)*( + _y1*(-_t3 + 3.0*_t2 - 3.0*_t + 1.0) + + _y2*(3.0*_t3 - 6.0*_t2 + 4.0) + + _y3*(-3.0*_t3 + 3*_t2 + 3.0*_t + 1.0) + + _y4*_t3 + ) + ) + + +def _b_spline_3D(_p1: Tuple[float, float, float], _p2: Tuple[float, float, float], + _p3: Tuple[float, float, float], _p4: Tuple[float, float, float], + _t: float): + _x1, _y1, _z1 = _p1 + _x2, _y2, _z2 = _p2 + _x3, _y3, _z3 = _p3 + _x4, _y4, _z4 = _p4 + _t2, _t3 = _t**2.0, _t**3.0 + + return ( + (1 / 6)*( + _x1*(-_t3 + 3.0*_t2 - 3.0*_t + 1.0) + + _x2*(3.0*_t3 - 6.0*_t2 + 4.0) + + _x3*(-3.0*_t3 + 3*_t2 + 3.0*_t + 1.0) + + _x4*_t3 + ), + (1 / 6)*( + _y1*(-_t3 + 3.0*_t2 - 3.0*_t + 1.0) + + _y2*(3.0*_t3 - 6.0*_t2 + 4.0) + + _y3*(-3.0*_t3 + 3*_t2 + 3.0*_t + 1.0) + + _y4*_t3 + ), + (1 / 6)*( + _y1*(-_t3 + 3.0*_t2 - 3.0*_t + 1.0) + + _y2*(3.0*_t3 - 6.0*_t2 + 4.0) + + _y3*(-3.0*_t3 + 3*_t2 + 3.0*_t + 1.0) + + _y4*_t3 + ) + ) + + +__all__ = { + 'SplineController' +} + + +class SplineController: + + pass From 8818c2276f9caab2dcdbbec873606f17f22740c9 Mon Sep 17 00:00:00 2001 From: DragonMoffon Date: Wed, 9 Aug 2023 04:02:42 +1200 Subject: [PATCH 29/31] Removed splines from this PR Removed Splines Made Isometric Controller Fixed small issue with facing direction doing temp rendering test so don't mind `log.png` --- arcade/camera/camera_2d.py | 7 +- arcade/camera/controllers/curve_controller.py | 141 --------------- .../controllers/isometric_controller.py | 160 ++++++++++++++++++ arcade/camera/controllers/log.png | Bin 0 -> 306 bytes .../simple_controller_functions.py | 6 +- arcade/camera/orthographic.py | 10 +- arcade/camera/perspective.py | 10 +- 7 files changed, 177 insertions(+), 157 deletions(-) delete mode 100644 arcade/camera/controllers/curve_controller.py create mode 100644 arcade/camera/controllers/log.png diff --git a/arcade/camera/camera_2d.py b/arcade/camera/camera_2d.py index 218c7d934d..963dab65be 100644 --- a/arcade/camera/camera_2d.py +++ b/arcade/camera/camera_2d.py @@ -66,15 +66,16 @@ def __init__(self, *, projection_data: Optional[OrthographicProjectionData] = None ): self._window: "Window" = window or get_window() + print(camera_data, any((viewport, position, up, zoom))) assert ( - any((viewport, position, up, zoom)) and camera_data + not any((viewport, position, up, zoom)) and not camera_data ), ( "Camera2D Warning: Provided both a CameraData object and raw values. Defaulting to CameraData." ) assert ( - any((projection, near, far)) and projection_data + not any((projection, near, far)) and not projection_data ), ( "Camera2D Warning: Provided both an OrthographicProjectionData object and raw values." "Defaulting to OrthographicProjectionData." @@ -86,7 +87,7 @@ def __init__(self, *, viewport or (0, 0, self._window.width, self._window.height), (_pos[0], _pos[1], 0.0), (_up[0], _up[1], 0.0), - (0.0, 0.0, 1.0), + (0.0, 0.0, -1.0), zoom or 1.0 ) diff --git a/arcade/camera/controllers/curve_controller.py b/arcade/camera/controllers/curve_controller.py deleted file mode 100644 index 8b8e54ba6d..0000000000 --- a/arcade/camera/controllers/curve_controller.py +++ /dev/null @@ -1,141 +0,0 @@ -from typing import Tuple - - -def _lerp_2D(_p1: Tuple[float, float], _p2: Tuple[float, float], _t: float): - _x1, _y1 = _p1 - _x2, _y2 = _p2 - - return _x1 + _t*(_x2 - _x1), _y1 + _t*(_y2 - _y1) - - -def _lerp_3D(_p1: Tuple[float, float, float], _p2: Tuple[float, float, float], _t: float): - _x1, _y1, _z1 = _p1 - _x2, _y2, _z2 = _p2 - - return _x1 + _t*(_x2 - _x1), _y1 + _t*(_y2 - _y1), _z1 + _t*(_z2 - _z1) - - -def _quad_2D(_p1: Tuple[float, float], _p2: Tuple[float, float], - _p3: Tuple[float, float], - _t: float): - _x1, _y1 = _p1 - _x2, _y2 = _p2 - _x3, _y3 = _p3 - _t2 = _t**2.0 - - return ( - _x1*(1.0 - 2.0*_t + _t2) + 2.0*_x2*(_t - _t2) + _x3*_t2, - _y1*(1.0 - 2.0*_t + _t2) + 2.0*_y2*(_t - _t2) + _y3*_t2 - ) - - -def _quad_3D(_p1: Tuple[float, float, float], _p2: Tuple[float, float, float], - _p3: Tuple[float, float, float], - _t: float): - _x1, _y1, _z1 = _p1 - _x2, _y2, _z2 = _p2 - _x3, _y3, _z3 = _p3 - _t2 = _t**2.0 - - return ( - _x1*(1.0 - 2.0*_t + _t2) + 2.0*_x2*(_t - _t2) + _x3*_t2, - _y1*(1.0 - 2.0*_t + _t2) + 2.0*_y2*(_t - _t2) + _y3*_t2, - _z1*(1.0 - 2.0*_t + _t2) + 2.0*_z2*(_t - _t2) + _z3*_t2 - ) - - -def _cubic_2D(_p1: Tuple[float, float], _p2: Tuple[float, float], - _p3: Tuple[float, float], _p4: Tuple[float, float], - _t: float): - _x1, _y1 = _p1 - _x2, _y2 = _p2 - _x3, _y3 = _p3 - _x4, _y4 = _p4 - _t2, _t3 = _t**2.0, _t**3.0 - - return ( - _x1*(-_t3 + 3.0*_t2 - 3.0*_t + 1.0) + _x2*(3.0*_t3 - 6.0*_t2 + 3.0*_t) + 3.0*_x3*(-_t3 + _t2) + _x4*_t3, - _y1*(-_t3 + 3.0*_t2 - 3.0*_t + 1.0) + _y2*(3.0*_t3 - 6.0*_t2 + 3.0*_t) + 3.0*_y3*(-_t3 + _t2) + _y4*_t3 - ) - - -def _cubic_3D(_p1: Tuple[float, float, float], _p2: Tuple[float, float, float], - _p3: Tuple[float, float, float], _p4: Tuple[float, float, float], - _t: float): - _x1, _y1, _z1 = _p1 - _x2, _y2, _z2 = _p2 - _x3, _y3, _z3 = _p3 - _x4, _y4, _z4 = _p4 - _t2, _t3 = _t**2.0, _t**3.0 - - return ( - _x1*(-_t3 + 3.0*_t2 - 3.0*_t + 1.0) + _x2*(3.0*_t3 - 6.0*_t2 + 3.0*_t) + 3.0*_x3*(-_t3 + _t2) + _x4*_t3, - _y1*(-_t3 + 3.0*_t2 - 3.0*_t + 1.0) + _y2*(3.0*_t3 - 6.0*_t2 + 3.0*_t) + 3.0*_y3*(-_t3 + _t2) + _y4*_t3, - _z1*(-_t3 + 3.0*_t2 - 3.0*_t + 1.0) + _z2*(3.0*_t3 - 6.0*_t2 + 3.0*_t) + 3.0*_z3*(-_t3 + _t2) + _z4*_t3 - ) - - -def _b_spline_2D(_p1: Tuple[float, float], _p2: Tuple[float, float], - _p3: Tuple[float, float], _p4: Tuple[float, float], - _t: float): - _x1, _y1 = _p1 - _x2, _y2 = _p2 - _x3, _y3 = _p3 - _x4, _y4 = _p4 - _t2, _t3 = _t**2.0, _t**3.0 - - return ( - (1/6)*( - _x1*(-_t3 + 3.0*_t2 - 3.0*_t + 1.0) + - _x2*(3.0*_t3 - 6.0*_t2 + 4.0) + - _x3*(-3.0*_t3 + 3*_t2 + 3.0*_t + 1.0) + - _x4*_t3 - ), - (1/6)*( - _y1*(-_t3 + 3.0*_t2 - 3.0*_t + 1.0) + - _y2*(3.0*_t3 - 6.0*_t2 + 4.0) + - _y3*(-3.0*_t3 + 3*_t2 + 3.0*_t + 1.0) + - _y4*_t3 - ) - ) - - -def _b_spline_3D(_p1: Tuple[float, float, float], _p2: Tuple[float, float, float], - _p3: Tuple[float, float, float], _p4: Tuple[float, float, float], - _t: float): - _x1, _y1, _z1 = _p1 - _x2, _y2, _z2 = _p2 - _x3, _y3, _z3 = _p3 - _x4, _y4, _z4 = _p4 - _t2, _t3 = _t**2.0, _t**3.0 - - return ( - (1 / 6)*( - _x1*(-_t3 + 3.0*_t2 - 3.0*_t + 1.0) + - _x2*(3.0*_t3 - 6.0*_t2 + 4.0) + - _x3*(-3.0*_t3 + 3*_t2 + 3.0*_t + 1.0) + - _x4*_t3 - ), - (1 / 6)*( - _y1*(-_t3 + 3.0*_t2 - 3.0*_t + 1.0) + - _y2*(3.0*_t3 - 6.0*_t2 + 4.0) + - _y3*(-3.0*_t3 + 3*_t2 + 3.0*_t + 1.0) + - _y4*_t3 - ), - (1 / 6)*( - _y1*(-_t3 + 3.0*_t2 - 3.0*_t + 1.0) + - _y2*(3.0*_t3 - 6.0*_t2 + 4.0) + - _y3*(-3.0*_t3 + 3*_t2 + 3.0*_t + 1.0) + - _y4*_t3 - ) - ) - - -__all__ = { - 'SplineController' -} - - -class SplineController: - - pass diff --git a/arcade/camera/controllers/isometric_controller.py b/arcade/camera/controllers/isometric_controller.py index 5ac8bbb93c..aaa377c52d 100644 --- a/arcade/camera/controllers/isometric_controller.py +++ b/arcade/camera/controllers/isometric_controller.py @@ -1,3 +1,163 @@ # TODO: Treats the camera as a 3D Isometric camera # and allows for spinning around a focal point # and moving along the isometric grid +from typing import Tuple +from math import sin, cos, radians + +from arcade.camera.data import CameraData + + +class IsometricCameraController: + + def __init__(self, camera_data: CameraData, + target: Tuple[float, float, float] = (0.0, 0.0, 0.0), + angle: float = 0.0, + dist: float = 1.0, + pixel_angle: bool = True, + up: Tuple[float, float, float] = (0.0, 0.0, 1.0), + right: Tuple[float, float, float] = (1.0, 0.0, 0.0)): + self._data: CameraData = camera_data + self._target: Tuple[float, float, float] = target + self._angle: float = angle + self._dist: float = dist + + self._pixel_angle: bool = pixel_angle + + self._up: Tuple[float, float, float] = up + self._right: Tuple[float, float, float] = right + + def update_position(self): + # Ref: https://danceswithcode.net/engineeringnotes/quaternions/quaternions.html + _pos_rads = radians(26.565 if self._pixel_angle else 30.0) + _c, _s = cos(_pos_rads), sin(_pos_rads) + p1, p2, p3 = ( + (_c * self._right[0] + _s * self._up[0]), + (_c * self._right[1] + _s * self._up[1]), + (_c * self._right[2] + _s * self._up[2]) + ) + _rotation_rads = -radians(self._angle + 45) + _c2, _s2 = cos(_rotation_rads/2.0), sin(_rotation_rads/2.0) + q0, q1, q2, q3 = ( + _c2, + _s2 * self._up[0], + _s2 * self._up[1], + _s2 * self._up[2] + ) + q0_2, q1_2, q2_2, q3_2 = q0**2, q1**2, q2**2, q3**2 + q01, q02, q03, q12, q13, q23 = q0*q1, q0*q2, q0*q3, q1*q2, q1*q3, q2*q3 + + _x = p1 * (q0_2 + q1_2 - q2_2 - q3_2) + 2.0 * (p2 * (q12 - q03) + p3 * (q02 + q13)) + _y = p2 * (q0_2 - q1_2 + q2_2 - q3_2) + 2.0 * (p1 * (q03 + q12) + p3 * (q23 - q01)) + _z = p3 * (q0_2 - q1_2 - q2_2 + q3_2) + 2.0 * (p1 * (q13 - q02) + p2 * (q01 + q23)) + + self._data.up = self._up + self._data.forward = -_x, -_y, -_z + self._data.position = ( + self._target[0] + self._dist*_x, + self._target[1] + self._dist*_y, + self._target[2] + self._dist*_z + ) + + def toggle_pixel_angle(self): + self._pixel_angle = bool(1 - self._pixel_angle) + + @property + def pixel_angle(self) -> bool: + return self._pixel_angle + + @pixel_angle.setter + def pixel_angle(self, _px: bool) -> None: + self._pixel_angle = _px + + @property + def zoom(self) -> float: + return self._data.zoom + + @zoom.setter + def zoom(self, _zoom: float) -> None: + self._data.zoom = _zoom + + @property + def angle(self): + return self._angle + + @angle.setter + def angle(self, _angle: float) -> None: + self._angle = _angle + + @property + def target(self) -> Tuple[float, float]: + return self._target[:2] + + @target.setter + def target(self, _target: Tuple[float, float]) -> None: + self._target = _target + (self._target[2],) + + @property + def target_height(self) -> float: + return self._target[2] + + @target_height.setter + def target_height(self, _height: float) -> None: + self._target = self._target[:2] + (_height,) + + @property + def target_full(self) -> Tuple[float, float, float]: + return self._target + + @target_full.setter + def target_full(self, _target: Tuple[float, float, float]) -> None: + self._target = _target + + +def iso_test(): + from arcade import Window, SpriteSolidColor, Sprite, SpriteList + from random import choice, uniform + from arcade.camera import OrthographicProjector, Camera2D + vals = (50, 100, 150, 200, 250) + + win = Window(1920, 1080) + cam = OrthographicProjector() + cam.view_data.position = (0.0, 0.0, 0.0) + cam.projection_data.near = 0 + cam.projection_data.far = 2500 + controller = IsometricCameraController( + cam.view_data, + dist=1000 + ) + sprites = SpriteList(capacity=1200) + sprites.extend( + tuple( + SpriteSolidColor(100, 100, 100 * x, 100 * y, color=(choice(vals), choice(vals), choice(vals), 255)) + for x in range(-5, 6) for y in range(-5, 6) + ) + ) + _log = tuple( + Sprite('log.png') + for _ in range(500) + ) + for index, sprite in enumerate(_log): + sprite.depth = index/2.0 + sprites.extend(_log) + + def on_press(r, m): + controller.target = uniform(-250, 250), 0.0 + + def on_draw(): + win.clear() + cam.use() + sprites.draw(pixelated=True) + + def on_update(dt: float): + controller.angle = (controller.angle + 45 * dt) % 360 + controller.update_position() + + win.on_key_press = on_press + win.on_update = on_update + win.on_draw = on_draw + + win.run() + + +if __name__ == '__main__': + iso_test() diff --git a/arcade/camera/controllers/log.png b/arcade/camera/controllers/log.png new file mode 100644 index 0000000000000000000000000000000000000000..b509adc7fd2e5ef15c879e3b050076e7366d7358 GIT binary patch literal 306 zcmV-20nPr2P)Px#>`6pHR9J=Wm%9@`ee=U#1vTlitz*$xRM4g`UJ$O|+i_O!x z_mzUk@KYLnMc@xmyHY(Y*SJ0c+8)l+&{7>Zc`( zfKWH39E1j>sV%QTP%y3!6Dfq$Oiwcpkmkp!1x8!iKP Mat4: up = ri.cross(fo) # Up Vector po = Vec3(*self._view.position) return Mat4(( - ri.x, up.x, fo.x, 0, - ri.y, up.y, fo.y, 0, - ri.z, up.z, fo.z, 0, - -ri.dot(po), -up.dot(po), -fo.dot(po), 1 + ri.x, up.x, -fo.x, 0, + ri.y, up.y, -fo.y, 0, + ri.z, up.z, -fo.z, 0, + -ri.dot(po), -up.dot(po), fo.dot(po), 1 )) def use(self): diff --git a/arcade/camera/perspective.py b/arcade/camera/perspective.py index b394114141..abd87fc809 100644 --- a/arcade/camera/perspective.py +++ b/arcade/camera/perspective.py @@ -43,7 +43,7 @@ def __init__(self, *, (0, 0, self._window.width, self._window.height), # Viewport (self._window.width / 2, self._window.height / 2, 0), # Position (0.0, 1.0, 0.0), # Up - (0.0, 0.0, 1.0), # Forward + (0.0, 0.0, -1.0), # Forward 1.0 # Zoom ) @@ -88,10 +88,10 @@ def _generate_view_matrix(self) -> Mat4: up = ri.cross(fo) # Up Vector po = Vec3(*self._view.position) return Mat4(( - ri.x, up.x, fo.x, 0, - ri.y, up.y, fo.y, 0, - ri.z, up.z, fo.z, 0, - -ri.dot(po), -up.dot(po), -fo.dot(po), 1 + ri.x, up.x, -fo.x, 0, + ri.y, up.y, -fo.y, 0, + ri.z, up.z, -fo.z, 0, + -ri.dot(po), -up.dot(po), fo.dot(po), 1 )) def use(self): From 4a702345a91de989a468836de6455e5f1e3a30c8 Mon Sep 17 00:00:00 2001 From: DragonMoffon Date: Tue, 15 Aug 2023 21:55:03 +1200 Subject: [PATCH 30/31] General work --- .../camera/controllers/input_controllers.py | 217 ++++++++++++++++++ .../controllers/isometric_controller.py | 74 +----- arcade/camera/controllers/log.png | Bin 306 -> 0 bytes .../simple_controller_functions.py | 47 +++- arcade/camera/data.py | 8 +- arcade/camera/offscreen.py | 83 +++++++ arcade/camera/orthographic.py | 3 +- arcade/camera/perspective.py | 7 +- 8 files changed, 358 insertions(+), 81 deletions(-) delete mode 100644 arcade/camera/controllers/log.png create mode 100644 arcade/camera/offscreen.py diff --git a/arcade/camera/controllers/input_controllers.py b/arcade/camera/controllers/input_controllers.py index 21854b3d2b..f1e8f6cc38 100644 --- a/arcade/camera/controllers/input_controllers.py +++ b/arcade/camera/controllers/input_controllers.py @@ -1,2 +1,219 @@ # TODO: Are 2D and 3D versions of a very simple controller # intended to be used for debugging. +from typing import TYPE_CHECKING, Tuple +from copy import deepcopy + +from pyglet.math import Vec3 + +from arcade.camera.data import CameraData +from arcade.camera.controllers.simple_controller_functions import rotate_around_forward +from arcade.window_commands import get_window +import arcade.key as KEYS +if TYPE_CHECKING: + from arcade.application import Window + + +class PolledCameraController2D: + MOVE_UP: int = KEYS.W + MOVE_DOWN: int = KEYS.S + + MOVE_RIGHT: int = KEYS.D + MOVE_LEFT: int = KEYS.A + + ROTATE_RIGHT: int = KEYS.E + ROTATE_LEFT: int = KEYS.Q + + ZOOM_IN: int = KEYS.PLUS + ZOOM_OUT: int = KEYS.MINUS + + RESET: int = KEYS.R + SAVE: int = KEYS.U + + TOGGLE_MOUSE_CENTER: int = KEYS.T + + MOVE_SPEED: float = 600.0 + ROTATE_SPEED: float = 60.0 + + def __init__(self, data: CameraData, *, window: "Window" = None): + self._win: "Window" = window or get_window() + + self._data: CameraData = data + + self._original_data: CameraData = deepcopy(data) + + self._mouse_old_pos: Tuple[float, float] = (0, 0) + + self._testy: float = 0. + + def reset(self): + self._data.viewport = self._original_data.viewport + + self._data.position = self._original_data.position + + self._data.up = self._original_data.up + self._data.forward = self._original_data.forward + self._data.zoom = self._original_data.zoom + + def save(self): + self._original_data = deepcopy(self._data) + + def change_control_data(self, _new: CameraData, _reset_prev: bool = False): + if _reset_prev: + self.reset() + self._data = _new + self._original_data = deepcopy(_new) + + def update(self, dt): + self._testy = ((self._testy - 0.1) + dt) % 2.0 + 0.1 + self._data.zoom = self._testy + + if self._win.keyboard[self.RESET]: + self.reset() + return + + if self._win.keyboard[self.SAVE]: + self.save() + return + + _rot = self._win.keyboard[self.ROTATE_LEFT] - self._win.keyboard[self.ROTATE_RIGHT] + if _rot: + rotate_around_forward(self._data, _rot * dt * self.ROTATE_SPEED) + + _vert = self._win.keyboard[self.MOVE_UP] - self._win.keyboard[self.MOVE_DOWN] + _hor = self._win.keyboard[self.MOVE_RIGHT] - self._win.keyboard[self.MOVE_LEFT] + + if _vert or _hor: + _dir = (_hor / (_vert**2.0 + _hor**2.0)**0.5, _vert / (_vert**2.0 + _hor**2.0)**0.5) + + _up = self._data.up + _right = Vec3(*self._data.forward).cross(Vec3(*self._data.up)) + + _cam_pos = self._data.position + + _cam_pos = ( + _cam_pos[0] + dt * self.MOVE_SPEED * (_right[0] * _dir[0] + _up[0] * _dir[1]), + _cam_pos[1] + dt * self.MOVE_SPEED * (_right[1] * _dir[0] + _up[1] * _dir[1]), + _cam_pos[2] + dt * self.MOVE_SPEED * (_right[2] * _dir[0] + _up[2] * _dir[1]) + ) + self._data.position = _cam_pos + + +class PolledCameraController3D: + MOVE_FORE: int = KEYS.W + MOVE_BACK: int = KEYS.S + + MOVE_RIGHT: int = KEYS.D + MOVE_LEFT: int = KEYS.A + + ROTATE_RIGHT: int = KEYS.E + ROTATE_LEFT: int = KEYS.Q + + ZOOM_IN: int = KEYS.PLUS + ZOOM_OUT: int = KEYS.MINUS + + RESET: int = KEYS.R + SAVE: int = KEYS.U + + MOVE_SPEED: float = 600.0 + ROTATE_SPEED: float = 60.0 + + def __init__(self, data: CameraData, *, window: "Window" = None, center_mouse: bool = True): + self._win: "Window" = window or get_window() + + self._data: CameraData = data + + self._original_data: CameraData = deepcopy(data) + + self._mouse_old_pos: Tuple[float, float] = (0, 0) + self._center_mouse: bool = center_mouse + + def toggle_center_mouse(self): + self._center_mouse = bool(1 - self._center_mouse) + + def reset(self): + self._data.viewport = self._original_data.viewport + + self._data.position = self._original_data.position + + self._data.up = self._original_data.up + self._data.forward = self._original_data.forward + self._data.zoom = self._original_data.zoom + + def save(self): + self._original_data = deepcopy(self._data) + + def change_control_data(self, _new: CameraData, _reset_prev: bool = False): + if _reset_prev: + self.reset() + self._data = _new + self._original_data = deepcopy(_new) + + def update(self, dt): + + if self._center_mouse: + self._win.set_exclusive_mouse() + + if self._win.keyboard[self.RESET]: + self.reset() + return + + if self._win.keyboard[self.SAVE]: + self.save() + return + + _rot = self._win.keyboard[self.ROTATE_LEFT] - self._win.keyboard[self.ROTATE_RIGHT] + if _rot: + print(self.ROTATE_SPEED) + rotate_around_forward(self._data, _rot * dt * self.ROTATE_SPEED) + + _move = self._win.keyboard[self.MOVE_FORE] - self._win.keyboard[self.MOVE_BACK] + _strafe = self._win.keyboard[self.MOVE_RIGHT] - self._win.keyboard[self.MOVE_LEFT] + + if _strafe or _move: + _for = self._data.forward + _right = Vec3(*self._data.forward).cross(Vec3(*self._data.up)) + + _cam_pos = self._data.position + + _cam_pos = ( + _cam_pos[0] + dt * self.MOVE_SPEED * (_right[0] * _strafe + _for[0] * _move), + _cam_pos[1] + dt * self.MOVE_SPEED * (_right[1] * _strafe + _for[1] * _move), + _cam_pos[2] + dt * self.MOVE_SPEED * (_right[2] * _strafe + _for[2] * _move) + ) + self._data.position = _cam_pos + + +def fps_test(): + from random import randrange as uniform + + from arcade import Window, SpriteSolidColor, SpriteList + from arcade.camera import OrthographicProjector, PerspectiveProjector + + win = Window() + proj = OrthographicProjector() + cont = PolledCameraController2D(proj.view_data) + sprites = SpriteList() + sprites.extend( + tuple(SpriteSolidColor(uniform(25, 125), uniform(25, 125), uniform(0, win.width), uniform(0, win.height)) + for _ in range(uniform(10, 15))) + ) + + def on_mouse_motion(x, y, dx, dy, *args): + pass + win.on_mouse_motion = on_mouse_motion + + def on_update(dt): + cont.update(dt) + win.on_update = on_update + + def on_draw(): + win.clear() + proj.use() + sprites.draw(pixelated=True) + win.on_draw = on_draw + + win.run() + + +if __name__ == '__main__': + fps_test() \ No newline at end of file diff --git a/arcade/camera/controllers/isometric_controller.py b/arcade/camera/controllers/isometric_controller.py index aaa377c52d..944fd74561 100644 --- a/arcade/camera/controllers/isometric_controller.py +++ b/arcade/camera/controllers/isometric_controller.py @@ -1,10 +1,8 @@ -# TODO: Treats the camera as a 3D Isometric camera -# and allows for spinning around a focal point -# and moving along the isometric grid from typing import Tuple from math import sin, cos, radians from arcade.camera.data import CameraData +from arcade.camera.controllers.simple_controller_functions import quaternion_rotation class IsometricCameraController: @@ -30,26 +28,13 @@ def update_position(self): # Ref: https://danceswithcode.net/engineeringnotes/quaternions/quaternions.html _pos_rads = radians(26.565 if self._pixel_angle else 30.0) _c, _s = cos(_pos_rads), sin(_pos_rads) - p1, p2, p3 = ( + p = ( (_c * self._right[0] + _s * self._up[0]), (_c * self._right[1] + _s * self._up[1]), (_c * self._right[2] + _s * self._up[2]) ) - _rotation_rads = -radians(self._angle + 45) - _c2, _s2 = cos(_rotation_rads/2.0), sin(_rotation_rads/2.0) - q0, q1, q2, q3 = ( - _c2, - _s2 * self._up[0], - _s2 * self._up[1], - _s2 * self._up[2] - ) - q0_2, q1_2, q2_2, q3_2 = q0**2, q1**2, q2**2, q3**2 - q01, q02, q03, q12, q13, q23 = q0*q1, q0*q2, q0*q3, q1*q2, q1*q3, q2*q3 - - _x = p1 * (q0_2 + q1_2 - q2_2 - q3_2) + 2.0 * (p2 * (q12 - q03) + p3 * (q02 + q13)) - _y = p2 * (q0_2 - q1_2 + q2_2 - q3_2) + 2.0 * (p1 * (q03 + q12) + p3 * (q23 - q01)) - _z = p3 * (q0_2 - q1_2 - q2_2 + q3_2) + 2.0 * (p1 * (q13 - q02) + p2 * (q01 + q23)) + _x, _y, _z = quaternion_rotation(self._up, p, self._angle + 45) self._data.up = self._up self._data.forward = -_x, -_y, -_z self._data.position = ( @@ -108,56 +93,3 @@ def target_full(self) -> Tuple[float, float, float]: @target_full.setter def target_full(self, _target: Tuple[float, float, float]) -> None: self._target = _target - - -def iso_test(): - from arcade import Window, SpriteSolidColor, Sprite, SpriteList - from random import choice, uniform - from arcade.camera import OrthographicProjector, Camera2D - vals = (50, 100, 150, 200, 250) - - win = Window(1920, 1080) - cam = OrthographicProjector() - cam.view_data.position = (0.0, 0.0, 0.0) - cam.projection_data.near = 0 - cam.projection_data.far = 2500 - controller = IsometricCameraController( - cam.view_data, - dist=1000 - ) - sprites = SpriteList(capacity=1200) - sprites.extend( - tuple( - SpriteSolidColor(100, 100, 100 * x, 100 * y, color=(choice(vals), choice(vals), choice(vals), 255)) - for x in range(-5, 6) for y in range(-5, 6) - ) - ) - _log = tuple( - Sprite('log.png') - for _ in range(500) - ) - for index, sprite in enumerate(_log): - sprite.depth = index/2.0 - sprites.extend(_log) - - def on_press(r, m): - controller.target = uniform(-250, 250), 0.0 - - def on_draw(): - win.clear() - cam.use() - sprites.draw(pixelated=True) - - def on_update(dt: float): - controller.angle = (controller.angle + 45 * dt) % 360 - controller.update_position() - - win.on_key_press = on_press - win.on_update = on_update - win.on_draw = on_draw - - win.run() - - -if __name__ == '__main__': - iso_test() diff --git a/arcade/camera/controllers/log.png b/arcade/camera/controllers/log.png deleted file mode 100644 index b509adc7fd2e5ef15c879e3b050076e7366d7358..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 306 zcmV-20nPr2P)Px#>`6pHR9J=Wm%9@`ee=U#1vTlitz*$xRM4g`UJ$O|+i_O!x z_mzUk@KYLnMc@xmyHY(Y*SJ0c+8)l+&{7>Zc`( zfKWH39E1j>sV%QTP%y3!6Dfq$Oiwcpkmkp!1x8!iKP Tuple[float, float, float]: + # Ref: https://danceswithcode.net/engineeringnotes/quaternions/quaternions.html + _rotation_rads = -radians(_angle) + p1, p2, p3 = _vector + _c2, _s2 = cos(_rotation_rads / 2.0), sin(_rotation_rads / 2.0) + + q0, q1, q2, q3 = ( + _c2, + _s2 * _axis[0], + _s2 * _axis[1], + _s2 * _axis[2] + ) + q0_2, q1_2, q2_2, q3_2 = q0 ** 2, q1 ** 2, q2 ** 2, q3 ** 2 + q01, q02, q03, q12, q13, q23 = q0 * q1, q0 * q2, q0 * q3, q1 * q2, q1 * q3, q2 * q3 + + _x = p1 * (q0_2 + q1_2 - q2_2 - q3_2) + 2.0 * (p2 * (q12 - q03) + p3 * (q02 + q13)) + _y = p2 * (q0_2 - q1_2 + q2_2 - q3_2) + 2.0 * (p1 * (q03 + q12) + p3 * (q23 - q01)) + _z = p3 * (q0_2 - q1_2 - q2_2 + q3_2) + 2.0 * (p1 * (q13 - q02) + p2 * (q01 + q23)) + + return _x, _y, _z + + +def rotate_around_forward(data: CameraData, angle: float): + data.up = quaternion_rotation(data.forward, data.up, angle) + + +def rotate_around_up(data: CameraData, angle: float): + data.forward = quaternion_rotation(data.up, data.forward, angle) + + +def rotate_around_right(data: CameraData, angle: float): + _right = tuple(Vec3(*data.forward).cross(*data.up)) + data.forward = quaternion_rotation(_right, data.forward, angle) + data.up = quaternion_rotation(_right, data.up, angle) + + def _interpolate_3D(s: Tuple[float, float, float], e: Tuple[float, float, float], t: float): s_x, s_y, s_z = s e_x, e_y, e_z = e diff --git a/arcade/camera/data.py b/arcade/camera/data.py index 3edd2188fd..dfaac3f4f6 100644 --- a/arcade/camera/data.py +++ b/arcade/camera/data.py @@ -27,12 +27,12 @@ class CameraData: viewport: Tuple[int, int, int, int] # View matrix data - position: Tuple[float, float, float] - up: Tuple[float, float, float] - forward: Tuple[float, float, float] + position: Tuple[float, float, float] = (0.0, 0.0, 0.0) + up: Tuple[float, float, float] = (0.0, 0.0, 1.0) + forward: Tuple[float, float, float] = (0.0, -1.0, 0.0) # Zoom - zoom: float + zoom: float = 1.0 @dataclass diff --git a/arcade/camera/offscreen.py b/arcade/camera/offscreen.py new file mode 100644 index 0000000000..70746a19d2 --- /dev/null +++ b/arcade/camera/offscreen.py @@ -0,0 +1,83 @@ +from typing import TYPE_CHECKING, Optional, Union, List, Tuple +from contextlib import contextmanager + +from arcade.window_commands import get_window +from arcade.camera.types import Projector +from arcade.gl import Framebuffer, Texture2D, Geometry, Program +from arcade.gl.geometry import quad_2d_fs +if TYPE_CHECKING: + from arcade.application import Window + + +class OffScreenSpace: + vertex_shader: str = """ + #version 330 + + in vec2 in_uv; + in vec2 in_vert; + + out vec2 out_uv; + + void main(){ + out_uv = in_uv; + gl_Position = vec4(in_vert, 0.0, 1.0); + } + """ + fragment_shader: str = """ + #version 330 + + uniform sampler2D texture0; + + in vec2 out_uv; + + out vec4 out_colour; + + void main(){ + out_colour = texture(texture0, out_uv); + } + """ + geometry: Geometry = None + program: Program = None + + def __init__(self, *, + window: "Window" = None, + size: Tuple[int, int] = None, + color_attachments: Optional[Union[Texture2D, List[Texture2D]]] = None, + depth_attachment: Optional[Texture2D] = None): + self._win: "Window" = window or get_window() + tex_size = size or self._win.size + near = self._win.ctx.NEAREST + self._fbo: Framebuffer = self._win.ctx.framebuffer( + color_attachments=color_attachments or self._win.ctx.texture(tex_size, filter=(near, near)), + depth_attachment=depth_attachment or None + ) + + if OffScreenSpace.geometry is None: + OffScreenSpace.geometry = quad_2d_fs() + if OffScreenSpace.program is None: + OffScreenSpace.program = self._win.ctx.program( + vertex_shader=OffScreenSpace.vertex_shader, + fragment_shader=OffScreenSpace.fragment_shader + ) + + def show(self): + self._fbo.color_attachments[0].use(0) + OffScreenSpace.geometry.render(OffScreenSpace.program) + + @contextmanager + def activate(self, *, projector: Projector = None, show: bool = False, clear: bool = False): + previous = self._win.ctx.active_framebuffer + prev_cam = self._win.current_camera if projector is not None else None + try: + self._fbo.use() + if clear: + self._fbo.clear() + if projector is not None: + projector.use() + yield self._fbo + finally: + previous.use() + if prev_cam is not None: + prev_cam.use() + if show: + self.show() diff --git a/arcade/camera/orthographic.py b/arcade/camera/orthographic.py index 15586928ce..d8b937281a 100644 --- a/arcade/camera/orthographic.py +++ b/arcade/camera/orthographic.py @@ -12,7 +12,8 @@ __all__ = [ - 'OrthographicProjector' + 'OrthographicProjector', + 'OrthographicProjectionData' ] diff --git a/arcade/camera/perspective.py b/arcade/camera/perspective.py index abd87fc809..5b4265ec04 100644 --- a/arcade/camera/perspective.py +++ b/arcade/camera/perspective.py @@ -12,7 +12,8 @@ __all__ = [ - 'PerspectiveProjector' + 'PerspectiveProjector', + 'PerspectiveProjectionData' ] @@ -50,11 +51,11 @@ def __init__(self, *, self._projection = projection or PerspectiveProjectionData( self._window.width / self._window.height, # Aspect ratio 90, # Field of view (degrees) - 0.1, 100 # Near, Far + 0.1, 1000 # Near, Far ) @property - def view(self) -> CameraData: + def view_data(self) -> CameraData: return self._view @property From c946cc941fff75839c8d6b7ff1a54a80c2823425 Mon Sep 17 00:00:00 2001 From: DragonMoffon Date: Tue, 15 Aug 2023 22:22:11 +1200 Subject: [PATCH 31/31] Cleaned up Offscreen renderer Added 'nother default glsl shader. Also cleaned up some linting. --- .../camera/controllers/input_controllers.py | 42 ++-------------- .../simple_controller_functions.py | 3 +- arcade/camera/offscreen.py | 48 ++++--------------- arcade/context.py | 8 ++++ .../system/shaders/util/textured_quad_fs.glsl | 11 +++++ .../system/shaders/util/textured_quad_vs.glsl | 11 +++++ 6 files changed, 43 insertions(+), 80 deletions(-) create mode 100644 arcade/resources/system/shaders/util/textured_quad_fs.glsl create mode 100644 arcade/resources/system/shaders/util/textured_quad_vs.glsl diff --git a/arcade/camera/controllers/input_controllers.py b/arcade/camera/controllers/input_controllers.py index f1e8f6cc38..e504ddcd71 100644 --- a/arcade/camera/controllers/input_controllers.py +++ b/arcade/camera/controllers/input_controllers.py @@ -1,6 +1,6 @@ # TODO: Are 2D and 3D versions of a very simple controller # intended to be used for debugging. -from typing import TYPE_CHECKING, Tuple +from typing import TYPE_CHECKING, Tuple, Optional from copy import deepcopy from pyglet.math import Vec3 @@ -34,7 +34,7 @@ class PolledCameraController2D: MOVE_SPEED: float = 600.0 ROTATE_SPEED: float = 60.0 - def __init__(self, data: CameraData, *, window: "Window" = None): + def __init__(self, data: CameraData, *, window: Optional["Window"] = None): self._win: "Window" = window or get_window() self._data: CameraData = data @@ -117,7 +117,7 @@ class PolledCameraController3D: MOVE_SPEED: float = 600.0 ROTATE_SPEED: float = 60.0 - def __init__(self, data: CameraData, *, window: "Window" = None, center_mouse: bool = True): + def __init__(self, data: CameraData, *, window: Optional["Window"] = None, center_mouse: bool = True): self._win: "Window" = window or get_window() self._data: CameraData = data @@ -181,39 +181,3 @@ def update(self, dt): _cam_pos[2] + dt * self.MOVE_SPEED * (_right[2] * _strafe + _for[2] * _move) ) self._data.position = _cam_pos - - -def fps_test(): - from random import randrange as uniform - - from arcade import Window, SpriteSolidColor, SpriteList - from arcade.camera import OrthographicProjector, PerspectiveProjector - - win = Window() - proj = OrthographicProjector() - cont = PolledCameraController2D(proj.view_data) - sprites = SpriteList() - sprites.extend( - tuple(SpriteSolidColor(uniform(25, 125), uniform(25, 125), uniform(0, win.width), uniform(0, win.height)) - for _ in range(uniform(10, 15))) - ) - - def on_mouse_motion(x, y, dx, dy, *args): - pass - win.on_mouse_motion = on_mouse_motion - - def on_update(dt): - cont.update(dt) - win.on_update = on_update - - def on_draw(): - win.clear() - proj.use() - sprites.draw(pixelated=True) - win.on_draw = on_draw - - win.run() - - -if __name__ == '__main__': - fps_test() \ No newline at end of file diff --git a/arcade/camera/controllers/simple_controller_functions.py b/arcade/camera/controllers/simple_controller_functions.py index 33453c4d8d..8ca0c38a87 100644 --- a/arcade/camera/controllers/simple_controller_functions.py +++ b/arcade/camera/controllers/simple_controller_functions.py @@ -50,7 +50,8 @@ def rotate_around_up(data: CameraData, angle: float): def rotate_around_right(data: CameraData, angle: float): - _right = tuple(Vec3(*data.forward).cross(*data.up)) + _crossed_vec = Vec3(*data.forward).cross(*data.up) + _right: Tuple[float, float, float] = (_crossed_vec.x, _crossed_vec.y, _crossed_vec.z) data.forward = quaternion_rotation(_right, data.forward, angle) data.up = quaternion_rotation(_right, data.up, angle) diff --git a/arcade/camera/offscreen.py b/arcade/camera/offscreen.py index 70746a19d2..181fb38f26 100644 --- a/arcade/camera/offscreen.py +++ b/arcade/camera/offscreen.py @@ -3,45 +3,18 @@ from arcade.window_commands import get_window from arcade.camera.types import Projector -from arcade.gl import Framebuffer, Texture2D, Geometry, Program +from arcade.gl import Framebuffer, Texture2D, Geometry from arcade.gl.geometry import quad_2d_fs if TYPE_CHECKING: from arcade.application import Window class OffScreenSpace: - vertex_shader: str = """ - #version 330 - - in vec2 in_uv; - in vec2 in_vert; - - out vec2 out_uv; - - void main(){ - out_uv = in_uv; - gl_Position = vec4(in_vert, 0.0, 1.0); - } - """ - fragment_shader: str = """ - #version 330 - - uniform sampler2D texture0; - - in vec2 out_uv; - - out vec4 out_colour; - - void main(){ - out_colour = texture(texture0, out_uv); - } - """ - geometry: Geometry = None - program: Program = None + _geometry: Optional[Geometry] = quad_2d_fs() def __init__(self, *, - window: "Window" = None, - size: Tuple[int, int] = None, + window: Optional["Window"] = None, + size: Optional[Tuple[int, int]] = None, color_attachments: Optional[Union[Texture2D, List[Texture2D]]] = None, depth_attachment: Optional[Texture2D] = None): self._win: "Window" = window or get_window() @@ -52,20 +25,15 @@ def __init__(self, *, depth_attachment=depth_attachment or None ) - if OffScreenSpace.geometry is None: - OffScreenSpace.geometry = quad_2d_fs() - if OffScreenSpace.program is None: - OffScreenSpace.program = self._win.ctx.program( - vertex_shader=OffScreenSpace.vertex_shader, - fragment_shader=OffScreenSpace.fragment_shader - ) + if OffScreenSpace._geometry is None: + OffScreenSpace._geometry = quad_2d_fs() def show(self): self._fbo.color_attachments[0].use(0) - OffScreenSpace.geometry.render(OffScreenSpace.program) + OffScreenSpace._geometry.render(self._win.ctx.utility_textured_quad_program) @contextmanager - def activate(self, *, projector: Projector = None, show: bool = False, clear: bool = False): + def activate(self, *, projector: Optional[Projector] = None, show: bool = False, clear: bool = False): previous = self._win.ctx.active_framebuffer prev_cam = self._win.current_camera if projector is not None else None try: diff --git a/arcade/context.py b/arcade/context.py index 9bb3d7fbcb..7770150b98 100644 --- a/arcade/context.py +++ b/arcade/context.py @@ -132,6 +132,14 @@ def __init__(self, window: pyglet.window.Window, gc_mode: str = "context_gc", gl self.collision_buffer = self.buffer(reserve=1024 * 4) self.collision_query = self.query(samples=False, time=False, primitives=True) + # General Utility + + # renders a quad (without projection) with a single 4-component texture. + self.utility_textured_quad_program: Program = self.load_program( + vertex_shader=":system:shaders/util/textured_quad_vs.glsl", + fragment_shader=":system:shaders/collision/textured_quad_fs.glsl", + ) + # --- Pre-created geometry and buffers for unbuffered draw calls ---- # FIXME: These pre-created resources needs to be packaged nicely # Just having them globally in the context is probably not a good idea diff --git a/arcade/resources/system/shaders/util/textured_quad_fs.glsl b/arcade/resources/system/shaders/util/textured_quad_fs.glsl new file mode 100644 index 0000000000..6693a83d98 --- /dev/null +++ b/arcade/resources/system/shaders/util/textured_quad_fs.glsl @@ -0,0 +1,11 @@ +#version 330 + +uniform sampler2D texture0; + +in vec2 out_uv; + +out vec4 out_colour; + +void main(){ + out_colour = texture(texture0, out_uv); +} diff --git a/arcade/resources/system/shaders/util/textured_quad_vs.glsl b/arcade/resources/system/shaders/util/textured_quad_vs.glsl new file mode 100644 index 0000000000..9b7e8eb720 --- /dev/null +++ b/arcade/resources/system/shaders/util/textured_quad_vs.glsl @@ -0,0 +1,11 @@ +#version 330 + +in vec2 in_uv; +in vec2 in_vert; + +out vec2 out_uv; + +void main(){ + out_uv = in_uv; + gl_Position = vec4(in_vert, 0.0, 1.0); +} \ No newline at end of file