From 11cc5c7173725839b2ff0d7c80ae24a202708a43 Mon Sep 17 00:00:00 2001 From: DragonMoffon Date: Sat, 10 Jun 2023 12:45:43 +1200 Subject: [PATCH 01/94] 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 000000000..e80f41f87 --- /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 000000000..3c3bbead3 --- /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 000000000..e69de29bb From 882c83bdc6ad35b540af2acbc6ce166607a64088 Mon Sep 17 00:00:00 2001 From: DragonMoffon Date: Sat, 10 Jun 2023 13:58:41 +1200 Subject: [PATCH 02/94] 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 3c3bbead3..962f05c4a 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/94] 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 962f05c4a..96923d8af 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/94] 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 96923d8af..1c601caba 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/94] 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 1c601caba..7837b8526 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/94] 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 e80f41f87..000000000 --- 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 7837b8526..d14f4915b 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 e69de29bb..000000000 From dd7531c638f7b3236e479a84e6f87009ccc329f4 Mon Sep 17 00:00:00 2001 From: DragonMoffon Date: Mon, 19 Jun 2023 15:15:26 +1200 Subject: [PATCH 07/94] 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 e2d96fc8a..f1461188f 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 000000000..00d69731e --- /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 000000000..082e4138a --- /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 000000000..ded814843 --- /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 000000000..76f6e8ff9 --- /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 000000000..ea83af53d --- /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 000000000..fd6c0dc75 --- /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 000000000..d4852734b --- /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 000000000..464090415 --- /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 000000000..52a711581 --- /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 d14f4915b..000000000 --- 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/94] 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 f1461188f..3f84cd6c8 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 4fdaabf24..193bfab70 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 d4852734b..f88446827 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/94] 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 2a470690a..9dbfefd18 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 00d69731e..33090763d 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 52a711581..8c27f5314 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/94] 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 f88446827..8ab767383 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/94] 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 193bfab70..39f8aef70 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 ea83af53d..c3a50d687 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 fd6c0dc75..634d4cde0 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/94] 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 3f84cd6c8..c5dd573b8 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 33090763d..5ee302b96 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 082e4138a..000000000 --- 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 000000000..c1bebc9b8 --- /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 ded814843..b8e0aa0bf 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 76f6e8ff9..407503d9f 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 c3a50d687..3371dda54 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 634d4cde0..61d8ccc49 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 8ab767383..36262883c 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 464090415..e151d4aaf 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 8c27f5314..6381fe877 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 000000000..e69de29bb diff --git a/tests/unit/camera/test_camera_controllers.py b/tests/unit/camera/test_camera_controllers.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/unit/camera/test_default_camera.py b/tests/unit/camera/test_default_camera.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/unit/camera/test_orthographic_camera.py b/tests/unit/camera/test_orthographic_camera.py new file mode 100644 index 000000000..85d252354 --- /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 000000000..d359ba183 --- /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 000000000..e69de29bb From b3900c03a082905ea6c3dfd1a21b2192d66eb69e Mon Sep 17 00:00:00 2001 From: DragonMoffon Date: Mon, 31 Jul 2023 23:55:04 +1200 Subject: [PATCH 13/94] 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 c1bebc9b8..d3609f22b 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 407503d9f..3d0303080 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 3371dda54..9e70474bb 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 61d8ccc49..36af57b41 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 36262883c..c2e7b3857 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/94] 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 d3609f22b..91a1f036b 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 b8e0aa0bf..3edd2188f 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 3d0303080..05f8b1b87 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 9e70474bb..731257a9c 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 36af57b41..58722c4b3 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 c2e7b3857..8ae49bb18 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 6381fe877..50c1ab4c0 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/94] 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 9dbfefd18..0113c4326 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 c5dd573b8..3f84cd6c8 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 39f8aef70..000000000 --- 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 91a1f036b..7d34ff8df 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 05f8b1b87..8613c78f7 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 8ae49bb18..2c497f17a 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 87bb8c5b3..13efe46fa 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 374573688..7286863b3 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 610afd61b..24bd1d11c 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/94] 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 2c497f17a..daf9e4f28 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/94] 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 7d34ff8df..c0875b2a6 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 7286863b3..9115e2414 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/94] 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 c0875b2a6..1811346f2 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/94] 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 000000000..8563d228a --- /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 000000000..d9efd3efd --- /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 000000000..c9fb81f91 --- /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 000000000..21854b3d2 --- /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 000000000..5ac8bbb93 --- /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 000000000..ebfa35fe1 --- /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 5ee302b96..000000000 --- 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 e151d4aaf..000000000 --- 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/94] 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 0113c4326..37fb4f967 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 3f84cd6c8..3a3ff707e 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 1811346f2..218c7d934 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 d9efd3efd..97b07b51b 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 8613c78f7..81bb38eb2 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 731257a9c..f88877e63 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 58722c4b3..b39411414 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 daf9e4f28..11f82238e 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 50c1ab4c0..b32600463 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 13efe46fa..23234023a 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 9115e2414..4b5d9b76c 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/94] 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 85a8c119d..198a3d34a 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/94] 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 85d252354..eced85d79 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/94] 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 c9fb81f91..9adf52aa9 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 11f82238e..b36da3f5d 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 b1a8048ed..8e2820c29 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 caddaf088..9e1c7d714 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 0ab9243ea..878cac944 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 4ba542559..7f22ed06f 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 2f6e0a9cb..cc52359a9 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 3fe65e609..16016a8a5 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 229bcd373..ad7575886 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 d1778fd2d..fc20916dd 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 24bd1d11c..66b5b7345 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/94] 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 fc20916dd..0226a0374 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/94] 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 0226a0374..f94f7fe93 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/94] 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 f94f7fe93..2dd46b150 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/94] 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 6478f6526..6add0430e 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 047d38c88..38b752630 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 2fa961c62..f0ec91192 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 368321d33..588147037 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 54b45da44..253a16b4b 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 1b333d2b0..eee99e09a 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 e82c9b581..370989ed4 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 08b523ae2..e9a756792 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 f4952157f..7b5783551 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 3312bc15f..3aa336289 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 5eea8deaf..9185a2915 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 1caab59d5..1c226b822 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 bb83827e4..ef3b82c5b 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 dcdaa4793..5150bc2b2 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 4f661e6c1..3203d4ee8 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 1baf848ad..4dfddbfe8 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 11606f8f6..623031c47 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 0761c38a5..e16050aec 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 35a4a165e..950940699 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/94] 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 9adf52aa9..8b8e54ba6 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/94] 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 218c7d934..963dab65b 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 8b8e54ba6..000000000 --- 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 5ac8bbb93..aaa377c52 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 b39411414..abd87fc80 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/94] 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 21854b3d2..f1e8f6cc3 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 aaa377c52..944fd7456 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 3edd2188f..dfaac3f4f 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 000000000..70746a19d --- /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 15586928c..d8b937281 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 abd87fc80..5b4265ec0 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/94] 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 f1e8f6cc3..e504ddcd7 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 33453c4d8..8ca0c38a8 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 70746a19d..181fb38f2 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 9bb3d7fbc..7770150b9 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 000000000..6693a83d9 --- /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 000000000..9b7e8eb72 --- /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 From 27066e04470e84dc6c8c5ed4d761afb59fdbea25 Mon Sep 17 00:00:00 2001 From: DragonMoffon Date: Fri, 18 Aug 2023 05:23:59 +1200 Subject: [PATCH 32/94] Squashed commit of the following: commit 7d56d39f6dfdeea48be659b4871ced059fe2e91e Author: DragonMoffon Date: Fri Aug 18 05:16:55 2023 +1200 Created a camera shake class Created a camera shake class. It isn't exactly like the shake provided before so people might what to have a review. --- arcade/camera/camera_2d.py | 3 - ...put_controllers.py => debug_controller.py} | 2 +- .../controllers/isometric_controller.py | 2 +- .../controllers/simple_controller_classes.py | 264 ++++++++++++++++++ .../simple_controller_functions.py | 5 +- arcade/camera/offscreen.py | 2 + arcade/context.py | 2 +- 7 files changed, 272 insertions(+), 8 deletions(-) rename arcade/camera/controllers/{input_controllers.py => debug_controller.py} (98%) create mode 100644 arcade/camera/controllers/simple_controller_classes.py diff --git a/arcade/camera/camera_2d.py b/arcade/camera/camera_2d.py index 963dab65b..a090423f6 100644 --- a/arcade/camera/camera_2d.py +++ b/arcade/camera/camera_2d.py @@ -51,7 +51,6 @@ class Camera2D: :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, @@ -116,7 +115,6 @@ def data(self) -> CameraData: 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 @@ -134,7 +132,6 @@ def projection_data(self) -> OrthographicProjectionData: most use cases will only change the projection on screen resize. """ - # TODO: Do not add setter return self._projection @property diff --git a/arcade/camera/controllers/input_controllers.py b/arcade/camera/controllers/debug_controller.py similarity index 98% rename from arcade/camera/controllers/input_controllers.py rename to arcade/camera/controllers/debug_controller.py index e504ddcd7..016b25728 100644 --- a/arcade/camera/controllers/input_controllers.py +++ b/arcade/camera/controllers/debug_controller.py @@ -1,4 +1,4 @@ -# TODO: Are 2D and 3D versions of a very simple controller +# TODO: Add 2D and 3D versions of a polled input controller # intended to be used for debugging. from typing import TYPE_CHECKING, Tuple, Optional from copy import deepcopy diff --git a/arcade/camera/controllers/isometric_controller.py b/arcade/camera/controllers/isometric_controller.py index 944fd7456..1f694fc26 100644 --- a/arcade/camera/controllers/isometric_controller.py +++ b/arcade/camera/controllers/isometric_controller.py @@ -24,7 +24,7 @@ def __init__(self, camera_data: CameraData, self._up: Tuple[float, float, float] = up self._right: Tuple[float, float, float] = right - def update_position(self): + def update_camera(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) diff --git a/arcade/camera/controllers/simple_controller_classes.py b/arcade/camera/controllers/simple_controller_classes.py new file mode 100644 index 000000000..568ac7fa6 --- /dev/null +++ b/arcade/camera/controllers/simple_controller_classes.py @@ -0,0 +1,264 @@ +from typing import TYPE_CHECKING, Optional, Tuple +from math import e, sin, cos, radians, pi, log +from random import uniform + +from arcade.camera.data import CameraData +from arcade.window_commands import get_window + +if TYPE_CHECKING: + from arcade.application import Window + + +class ScreenShaker2D: + """ + Uses the equation Ae^{-fx}sin(v 2 pi x) to create shake effect which falls off over time. + + where: + A is the amplitude (size) + f is the falloff + v is the speed + x is the time since start + e is euler's constant + """ + + stop_range: float = 0.1 + # TODO Doc Strings + + def __init__(self, camera_data: CameraData, default_shake_size: float = 1.0, default_shake_falloff: float = 1.0, *, + window: Optional["Window"] = None, + default_shake_speed: float = 1.0, + default_shake_jitter: float = 0.0, + default_shake_direction: Tuple[float, float] = (-1.0, 0.0)): + self._win: "Window" = window or get_window() + self._data: CameraData = camera_data + + self._d_dir = default_shake_direction + self._d_speed = default_shake_speed + self._d_amplitude = default_shake_size + self._d_falloff = default_shake_falloff + self._d_jitter = default_shake_jitter + + self._t_dir = default_shake_direction + self._t_speed = default_shake_speed + self._t_amplitude = default_shake_size + self._t_falloff = default_shake_falloff + self._t_jitter = default_shake_jitter + + self._shake: bool = False + self._time: float = 0.0 + self._stop_time: float = -1.0 + self._t_pos: Tuple[float, float] = camera_data.position[:2] + + def start(self, *, + true_pos: Optional[Tuple[float, float]] = None, + temp_size: Optional[float] = None, + temp_falloff: Optional[float] = None, + temp_speed: Optional[float] = None, + temp_jitter: Optional[float] = None, + temp_direction: Optional[Tuple[float, float]] = None): + + self._t_dir = temp_direction if temp_direction is not None else self._d_dir + self._t_jitter = temp_jitter if temp_jitter is not None else self._d_jitter + self._t_speed = temp_speed if temp_speed is not None else self._d_speed + self._t_falloff = temp_falloff if temp_falloff is not None else self._d_falloff + self._t_amplitude = temp_size if temp_size is not None else self._d_amplitude + + self._time = 0.0 + self._shake = True + self._t_pos = true_pos if true_pos is not None else self._t_pos + self._stop_time = self.estimated_length() + + def _curve(self, _t: float) -> float: + return self._t_amplitude * e**(-self._t_falloff*_t) * sin(self._t_speed * 2.0 * pi * self._time) + + def estimated_length(self) -> float: + _t = (log(self._t_amplitude) - log(self.stop_range)) / self._t_falloff + _dt = _t % (0.5 / self._t_speed) # Find the distance from to the last x = 0.0 + return _t - _dt + + def shaking(self) -> bool: + return self._shake + + def stop(self): + self.stop_in(self._t_speed - (self._time % self._t_speed)) + + def stop_in(self, _time: float): + # This derivation was a pain + _dt = self._time % (1.0 / self._t_speed) + + _f = log(self._t_amplitude) - log(0.1) - self._t_falloff * self._time + _a = self._t_amplitude * e**(_f * _dt - self._t_falloff * self._time) + + self._t_amplitude = _a + self._t_falloff = _f + self._time = _dt + + _st = _time % (0.5 / self._t_speed) + self._stop_time = _time - _st + + def update(self, delta_time: float, true_pos: Optional[Tuple[float, float]] = None): + if true_pos is not None: + self._t_pos = true_pos + + if 0.0 <= self._stop_time <= self._time + delta_time: + self._time = 0.0 + self._shake = False + return + + if not self._shake: + self._time = 0.0 + return + + _m = 0.5 / self._t_speed + if self._t_jitter != 0.0 and self._time % _m > (self._time + delta_time) % _m: + radians_shift = radians(uniform(-self._t_jitter/2.0, self._t_jitter/2.0)) + _c, _s = cos(radians_shift), sin(radians_shift) + dx = self._t_dir[0] * _c - self._t_dir[1] * _s + dy = self._t_dir[0] * _s + self._t_dir[1] * _c + self._t_dir = (dx, dy) + + self._time += delta_time + + def update_camera(self, true_pos: Optional[Tuple[float, float]] = None): + if true_pos is not None: + self._t_pos = true_pos + + step = self._curve(self._time) + pos = ( + self._t_pos[0] + step * self._t_dir[0], + self._t_pos[1] + step * self._t_dir[1] + ) + self._data.position = (pos[0], pos[1], self._data.position[2]) + + @property + def direction(self) -> Tuple[float, float]: + return self._t_dir + + @direction.setter + def direction(self, _dir: Tuple[float, float]): + self._t_dir = _dir + self._stop_time = self.estimated_length() + + @property + def jitter(self) -> float: + return self._t_jitter + + @jitter.setter + def jitter(self, _jitter: float): + self._t_jitter = _jitter + self._stop_time = self.estimated_length() + + @property + def speed(self) -> float: + return self._t_speed + + @speed.setter + def speed(self, _speed: float): + self._t_speed = _speed + self._stop_time = self.estimated_length() + + @property + def falloff(self) -> float: + return self._t_falloff + + @falloff.setter + def falloff(self, _falloff: float): + self._t_falloff = _falloff + self._stop_time = self.estimated_length() + + @property + def amplitude(self) -> float: + return self._t_amplitude + + @amplitude.setter + def amplitude(self, _amplitude: float): + self._t_amplitude = _amplitude + self._stop_time = self.estimated_length() + + @property + def default_direction(self) -> Tuple[float, float]: + return self._d_dir + + @default_direction.setter + def default_direction(self, _dir: Tuple[float, float]): + self._d_dir = _dir + self._t_dir = _dir + self._stop_time = self.estimated_length() + + @property + def default_jitter(self) -> float: + return self._d_jitter + + @default_jitter.setter + def default_jitter(self, _jitter: float): + self._d_jitter = _jitter + self._t_jitter = _jitter + self._stop_time = self.estimated_length() + + @property + def default_speed(self) -> float: + return self._d_speed + + @default_speed.setter + def default_speed(self, _speed: float): + self._d_speed = _speed + self._t_speed = _speed + self._stop_time = self.estimated_length() + + @property + def default_falloff(self) -> float: + return self._d_falloff + + @default_falloff.setter + def default_falloff(self, _falloff: float): + self._d_falloff = _falloff + self._t_falloff = _falloff + self._stop_time = self.estimated_length() + + @property + def default_amplitude(self) -> float: + return self._d_amplitude + + @default_amplitude.setter + def default_amplitude(self, _amplitude: float): + self._d_amplitude = _amplitude + self._t_amplitude = _amplitude + self._stop_time = self.estimated_length() + + +def _shakey(): + from arcade import Window, draw_point + + from arcade.camera import Camera2D + + win = Window() + cam = Camera2D() + shake = ScreenShaker2D(cam.data, 200, default_shake_speed=1.0, default_shake_jitter=20, default_shake_falloff=0.5) + + def on_key_press(*args): + if shake.shaking(): + shake.stop() + else: + shake.start() + + win.on_key_press = on_key_press + + def on_update(delta_time: float): + shake.update(delta_time) + + win.on_update = on_update + + def on_draw(): + win.clear() + shake.update_camera() + cam.use() + draw_point(100, 100, (255, 255, 255, 255), 10) + + win.on_draw = on_draw + + win.run() + + +if __name__ == '__main__': + _shakey() + diff --git a/arcade/camera/controllers/simple_controller_functions.py b/arcade/camera/controllers/simple_controller_functions.py index 8ca0c38a8..f0e428980 100644 --- a/arcade/camera/controllers/simple_controller_functions.py +++ b/arcade/camera/controllers/simple_controller_functions.py @@ -50,7 +50,9 @@ def rotate_around_up(data: CameraData, angle: float): def rotate_around_right(data: CameraData, angle: float): - _crossed_vec = Vec3(*data.forward).cross(*data.up) + _forward = Vec3(data.forward[0], data.forward[1], data.forward[2]) + _up = Vec3(data.up[0], data.up[1], data.up[2]) + _crossed_vec = _forward.cross(_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) @@ -128,4 +130,3 @@ def simple_easing_2D(percent: float, """ simple_easing(percent, start + (0,), target + (0,), data, func) - diff --git a/arcade/camera/offscreen.py b/arcade/camera/offscreen.py index 181fb38f2..1999cadb2 100644 --- a/arcade/camera/offscreen.py +++ b/arcade/camera/offscreen.py @@ -12,6 +12,8 @@ class OffScreenSpace: _geometry: Optional[Geometry] = quad_2d_fs() + # TODO: Doc String + def __init__(self, *, window: Optional["Window"] = None, size: Optional[Tuple[int, int]] = None, diff --git a/arcade/context.py b/arcade/context.py index 7770150b9..3b68bb830 100644 --- a/arcade/context.py +++ b/arcade/context.py @@ -137,7 +137,7 @@ def __init__(self, window: pyglet.window.Window, gc_mode: str = "context_gc", gl # 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", + fragment_shader=":system:shaders/util/textured_quad_fs.glsl", ) # --- Pre-created geometry and buffers for unbuffered draw calls ---- From 4d197638152acd37e1eaabae366bd67e7aed526d Mon Sep 17 00:00:00 2001 From: DragonMoffon Date: Fri, 18 Aug 2023 05:37:57 +1200 Subject: [PATCH 33/94] added screen shake doc --- .../controllers/simple_controller_classes.py | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/arcade/camera/controllers/simple_controller_classes.py b/arcade/camera/controllers/simple_controller_classes.py index 568ac7fa6..0e327fc94 100644 --- a/arcade/camera/controllers/simple_controller_classes.py +++ b/arcade/camera/controllers/simple_controller_classes.py @@ -11,7 +11,16 @@ class ScreenShaker2D: """ - Uses the equation Ae^{-fx}sin(v 2 pi x) to create shake effect which falls off over time. + Uses the equation Ae^{-fx}sin(v 2 pi x) to create shake effect which lessens over time. + The shaking can be started with using the Start method. Every value has a default, + and a temporary value. The temporary values are reset every time the animation + is started. To stop the animation call stop or stop_in. These do not instantly + stop the animation, but rather change the animation curve to have a smoother stop. + The animation also has a jitter value in degrees which is how much to rotate the + shake direction each oscillation. + + If you want the shaking to stay the same intensity and then quickly stop it is easiest + to set the falloff to 0.0, and then call stop when you want the shaking to stop. where: A is the amplitude (size) @@ -19,6 +28,16 @@ class ScreenShaker2D: v is the speed x is the time since start e is euler's constant + + + Args: + camera_data: The Camera Data to manipulate. + window: The Arcade Window, uses currently active by default. + default_shake_size: The maximum distance away from the starting position (Equal to A). + default_shake_falloff: The rate that the screen shake weakens (Equal to f). + default_shake_speed: The speed or frequency of oscillations (Equal to v). + default_shake_jitter: The max angle that the shake direction can change by. + default_shake_direction: The direction the oscillations follow. """ stop_range: float = 0.1 From d1175726f9285fd8af6bfb2980bf7d8c905fd274 Mon Sep 17 00:00:00 2001 From: DragonMoffon Date: Sun, 20 Aug 2023 03:24:46 +1200 Subject: [PATCH 34/94] lovely useless doc strings NOTE: The doc strings aren't useless. The code just is. Going to completely change the class. --- .../controllers/simple_controller_classes.py | 228 ++++++++++++++++-- 1 file changed, 208 insertions(+), 20 deletions(-) diff --git a/arcade/camera/controllers/simple_controller_classes.py b/arcade/camera/controllers/simple_controller_classes.py index 0e327fc94..138b5c7bc 100644 --- a/arcade/camera/controllers/simple_controller_classes.py +++ b/arcade/camera/controllers/simple_controller_classes.py @@ -23,17 +23,17 @@ class ScreenShaker2D: to set the falloff to 0.0, and then call stop when you want the shaking to stop. where: - A is the amplitude (size) - f is the falloff - v is the speed + A is the amplitude (size in pixels) + f is the falloff (Higher the faster. A value of one decays after 2.25 seconds) + v is the speed (each integer value above one increase the cycles per second by one) x is the time since start - e is euler's constant + e is euler's constant (pronounced oil-ers) Args: camera_data: The Camera Data to manipulate. window: The Arcade Window, uses currently active by default. - default_shake_size: The maximum distance away from the starting position (Equal to A). + default_shake_size: The maximum amplitude away from the starting position (Equal to A). default_shake_falloff: The rate that the screen shake weakens (Equal to f). default_shake_speed: The speed or frequency of oscillations (Equal to v). default_shake_jitter: The max angle that the shake direction can change by. @@ -74,7 +74,28 @@ def start(self, *, temp_falloff: Optional[float] = None, temp_speed: Optional[float] = None, temp_jitter: Optional[float] = None, - temp_direction: Optional[Tuple[float, float]] = None): + temp_direction: Optional[Tuple[float, float]] = None) -> None: + """ + Start the shaking animation, you can set a temporary position for any of the components of the + animation. These values will not be saved once the animation finishes. The true pos is NOT a + temp value. + + Args: + true_pos: The position that the shaking will originate from. + + temp_size: A temporary, one animation only, value of the max amplitude of the shaking + + temp_falloff: A temporary, one animation only, value for the speed of falloff. Higher is faster. + + temp_speed: A temporary, one animation only, value for the oscillation speeds. + Each integer value adds one full cycle per second. + + temp_jitter: A temporary, one animation only, value which determines the max + angle change after each cycle. + + temp_direction: A temporary, one animation only, tuple which describes the x, y direction + the shaking will follow. + """ self._t_dir = temp_direction if temp_direction is not None else self._d_dir self._t_jitter = temp_jitter if temp_jitter is not None else self._d_jitter @@ -88,20 +109,57 @@ def start(self, *, self._stop_time = self.estimated_length() def _curve(self, _t: float) -> float: + """ + Calculate the equation A exp(-ft) sin(2v pi t) which describes + the distance from the true pos. + + Args: + _t: Time in seconds. + Returns: + The calculated Amplitude. + """ return self._t_amplitude * e**(-self._t_falloff*_t) * sin(self._t_speed * 2.0 * pi * self._time) def estimated_length(self) -> float: + """ + Estimates how much longer the animation still has to go with the current settings. + + Returns: + Roughly the number of seconds until the shaking will stop. + """ + if self._t_falloff == 0.0: + return float('inf') _t = (log(self._t_amplitude) - log(self.stop_range)) / self._t_falloff _dt = _t % (0.5 / self._t_speed) # Find the distance from to the last x = 0.0 return _t - _dt def shaking(self) -> bool: + """ + Returns whether the controller is causing screen shake or updating. + """ return self._shake - def stop(self): - self.stop_in(self._t_speed - (self._time % self._t_speed)) + def stop(self) -> None: + """ + Tell the controller to stop at the next point of 0 amplitude. + + This alters the temporary values used by the controller. + If any are changed after the fact the controller may not stop. + """ + self.stop_in(1 / self._t_speed - (self._time % (0.5 / self._t_speed))) + + def stop_in(self, _time: float) -> None: + """ + Tell the controller to stop after a certain number of seconds. + The controller will actually stop at the closest point of low amplitude + to prevent any sudden position snapping. - def stop_in(self, _time: float): + This alters the temporary values used by the controller. + If any are changed after the fact the controller may not stop. + + Args: + _time: The number of seconds to stop after. + """ # This derivation was a pain _dt = self._time % (1.0 / self._t_speed) @@ -112,10 +170,24 @@ def stop_in(self, _time: float): self._t_falloff = _f self._time = _dt - _st = _time % (0.5 / self._t_speed) - self._stop_time = _time - _st + _st = (_time + _dt) % (0.5 / self._t_speed) + + self._stop_time = _dt + _time - _st + + def update(self, delta_time: float, true_pos: Optional[Tuple[float, float]] = None) -> None: + """ + Run the update logic for the controller. This should be called every update, but only once. + This does not actually change the position of the camera. That is done in update_camera. + + Args: + delta_time: The length of time since the last update call. Use the delta_time provided + in arcade.Window.on_update, or arcade.View.on_update. + 3.x+: will be replaced whatever delta_time solution is created in the future. - def update(self, delta_time: float, true_pos: Optional[Tuple[float, float]] = None): + true_pos: The position from which the oscillation originates. If you have another animation + which calculates the position for the camera you can pass it in here, but there is + also a pos property. + """ if true_pos is not None: self._t_pos = true_pos @@ -128,6 +200,9 @@ def update(self, delta_time: float, true_pos: Optional[Tuple[float, float]] = No self._time = 0.0 return + # The oscillator passes through 0.0 at regular intervals. + # If the next time is past this interval then it is clear that 0.0 + # has been passed and jitter should be calculated. _m = 0.5 / self._t_speed if self._t_jitter != 0.0 and self._time % _m > (self._time + delta_time) % _m: radians_shift = radians(uniform(-self._t_jitter/2.0, self._t_jitter/2.0)) @@ -138,10 +213,13 @@ def update(self, delta_time: float, true_pos: Optional[Tuple[float, float]] = No self._time += delta_time - def update_camera(self, true_pos: Optional[Tuple[float, float]] = None): - if true_pos is not None: - self._t_pos = true_pos - + def update_camera(self) -> None: + """ + Take the current time and temp values and update the position of the camera. + This method has no logic, and should never have any logic. Calling this method multiple times + will not change the result. If you need to update the true position of the camera + use the pos property, or the update method. + """ step = self._curve(self._time) pos = ( self._t_pos[0] + step * self._t_dir[0], @@ -149,97 +227,207 @@ def update_camera(self, true_pos: Optional[Tuple[float, float]] = None): ) self._data.position = (pos[0], pos[1], self._data.position[2]) + @property + def pos(self) -> Tuple[float, float]: + """ + The point that the shaking offsets from. + camera position will equal true pos when amplitude is 0 + """ + return self._t_pos + + @pos.setter + def pos(self, _pos: Tuple[float, float]) -> None: + """ + The point that the shaking offsets from. + camera position will equal true pos when amplitude is 0 + """ + self._t_pos = _pos + @property def direction(self) -> Tuple[float, float]: + """ + The direction the shaking will move in. + MAKE SURE THE LENGTH IS 1.0. + """ return self._t_dir @direction.setter def direction(self, _dir: Tuple[float, float]): + """ + Temporarily sets this value. To permanently change it use default_direction + + The direction the shaking will move in. + MAKE SURE THE LENGTH IS 1.0. + """ self._t_dir = _dir self._stop_time = self.estimated_length() @property def jitter(self) -> float: + """ + The max angle the direction can randomly change by every cycle. + """ return self._t_jitter @jitter.setter def jitter(self, _jitter: float): + """ + Temporarily sets this value. To permanently change it use default_jitter + + The max angle the direction can randomly change by every cycle. + """ self._t_jitter = _jitter self._stop_time = self.estimated_length() @property def speed(self) -> float: + """ + The number of oscillations per second. + Every integer increase causes one extra cycle. + """ return self._t_speed @speed.setter def speed(self, _speed: float): + """ + Temporarily sets this value. To permanently change it use default_speed + + The number of oscillations per second. + Every integer increase causes one extra cycle. + """ self._t_speed = _speed self._stop_time = self.estimated_length() @property def falloff(self) -> float: + """ + The rate at which the oscillations die down. Higher the faster. + The decay uses euler's (pronounced oil-ers) number. + """ return self._t_falloff @falloff.setter def falloff(self, _falloff: float): + """ + Temporarily sets this value. To permanently change it use default_falloff + + The rate at which the oscillations die down. Higher the faster. + The decay uses euler's (pronounced oil-ers) number. + """ self._t_falloff = _falloff self._stop_time = self.estimated_length() @property def amplitude(self) -> float: + """ + The maximum length away from the true pos in pixels. + """ return self._t_amplitude @amplitude.setter def amplitude(self, _amplitude: float): + """ + Temporarily sets this value. To permanently change it use default_amplitude + + The maximum length away from the true pos in pixels. + """ self._t_amplitude = _amplitude self._stop_time = self.estimated_length() @property def default_direction(self) -> Tuple[float, float]: + """ + The direction the shaking will move in. + MAKE SURE THE LENGTH IS 1.0. + """ + return self._d_dir @default_direction.setter def default_direction(self, _dir: Tuple[float, float]): + """ + Permanently sets this value. To temporarily change it use direction + + The direction the shaking will move in. + MAKE SURE THE LENGTH IS 1.0. + """ + self._d_dir = _dir self._t_dir = _dir self._stop_time = self.estimated_length() @property def default_jitter(self) -> float: + """ + The max angle the direction can randomly change by every cycle. + """ return self._d_jitter @default_jitter.setter def default_jitter(self, _jitter: float): + """ + Permanently sets this value. To temporarily change it use jitter + + The max angle the direction can randomly change by every cycle. + """ self._d_jitter = _jitter self._t_jitter = _jitter self._stop_time = self.estimated_length() @property def default_speed(self) -> float: + """ + The number of oscillations per second. + Every integer increase causes one extra cycle. + """ return self._d_speed @default_speed.setter def default_speed(self, _speed: float): + """ + Permanently sets this value. To temporarily change it use speed + + The number of oscillations per second. + Every integer increase causes one extra cycle. + """ self._d_speed = _speed self._t_speed = _speed self._stop_time = self.estimated_length() @property def default_falloff(self) -> float: + """ + The rate at which the oscillations die down. Higher the faster. + The decay uses euler's (pronounced oil-ers) number. + """ return self._d_falloff @default_falloff.setter def default_falloff(self, _falloff: float): + """ + Permanently sets this value. To temporarily change it use falloff + + The rate at which the oscillations die down. Higher the faster. + The decay uses euler's (pronounced oil-ers) number. + """ self._d_falloff = _falloff self._t_falloff = _falloff self._stop_time = self.estimated_length() @property def default_amplitude(self) -> float: + """ + The maximum length away from the true pos in pixels. + """ return self._d_amplitude @default_amplitude.setter def default_amplitude(self, _amplitude: float): + """ + Permanently sets this value. To temporarily change it use amplitude + + The maximum length away from the true pos in pixels. + """ self._d_amplitude = _amplitude self._t_amplitude = _amplitude self._stop_time = self.estimated_length() @@ -252,7 +440,7 @@ def _shakey(): win = Window() cam = Camera2D() - shake = ScreenShaker2D(cam.data, 200, default_shake_speed=1.0, default_shake_jitter=20, default_shake_falloff=0.5) + shake = ScreenShaker2D(cam.data, 200, default_shake_speed=3.0, default_shake_jitter=20, default_shake_falloff=0.0) def on_key_press(*args): if shake.shaking(): @@ -260,12 +448,12 @@ def on_key_press(*args): else: shake.start() - win.on_key_press = on_key_press + win.on_key_press = on_key_press # type: ignore def on_update(delta_time: float): shake.update(delta_time) - win.on_update = on_update + win.on_update = on_update # type: ignore def on_draw(): win.clear() @@ -273,7 +461,7 @@ def on_draw(): cam.use() draw_point(100, 100, (255, 255, 255, 255), 10) - win.on_draw = on_draw + win.on_draw = on_draw # type: ignore win.run() From f94eed4726a4bf9c5448ad7a72650d9e5b3f1a1b Mon Sep 17 00:00:00 2001 From: DragonMoffon Date: Sun, 10 Sep 2023 09:45:15 +1200 Subject: [PATCH 35/94] improving screen shake --- .../controllers/simple_controller_classes.py | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/arcade/camera/controllers/simple_controller_classes.py b/arcade/camera/controllers/simple_controller_classes.py index 138b5c7bc..36ddba648 100644 --- a/arcade/camera/controllers/simple_controller_classes.py +++ b/arcade/camera/controllers/simple_controller_classes.py @@ -10,6 +10,41 @@ class ScreenShaker2D: + """ + sqrt{x} * e^{-x} + """ + + def __init__(self, camera_data: CameraData, *, + default_falloff: float = 1.0, + default_acceleration: float = 1.0, + default_amplitude: float = 1.0, + default_direction: Optional[Tuple[float, float]] = None): + self._data: CameraData = camera_data + + self._d_falloff: float = default_falloff + self._d_acceleration: float = default_acceleration + self._d_amplitude: float = default_amplitude + self._d_direction: Optional[Tuple[float, float]] = default_direction + + self._t_falloff: float = default_falloff + self._t_acceleration: float = default_acceleration + self._t_amplitude: float = default_amplitude + self._t_direction: Optional[Tuple[float, float]] = default_direction + + def _curve(self, _t: float): + pass + + def start(self): + pass + + def stop(self): + pass + + def stop_after(self, seconds: float): + pass + + +class ScreenShaker2D_old: """ Uses the equation Ae^{-fx}sin(v 2 pi x) to create shake effect which lessens over time. The shaking can be started with using the Start method. Every value has a default, From a2ce14f741ff2feba7eea95b96208de93023e2bc Mon Sep 17 00:00:00 2001 From: DragonMoffon Date: Tue, 12 Sep 2023 13:46:28 +1200 Subject: [PATCH 36/94] touch-ups --- .../camera/controllers/simple_controller_classes.py | 2 +- .../controllers/simple_controller_functions.py | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/arcade/camera/controllers/simple_controller_classes.py b/arcade/camera/controllers/simple_controller_classes.py index 36ddba648..a51b0d6b0 100644 --- a/arcade/camera/controllers/simple_controller_classes.py +++ b/arcade/camera/controllers/simple_controller_classes.py @@ -11,7 +11,7 @@ class ScreenShaker2D: """ - sqrt{x} * e^{-x} + Ae^{-fx} - Ae^{-(z+f)x} """ def __init__(self, camera_data: CameraData, *, diff --git a/arcade/camera/controllers/simple_controller_functions.py b/arcade/camera/controllers/simple_controller_functions.py index f0e428980..fe8fcedf7 100644 --- a/arcade/camera/controllers/simple_controller_functions.py +++ b/arcade/camera/controllers/simple_controller_functions.py @@ -6,9 +6,9 @@ from pyglet.math import Vec3 __all__ = [ - 'simple_follow', + 'simple_follow_3D', 'simple_follow_2D', - 'simple_easing', + 'simple_easing_3D', 'simple_easing_2D', 'quaternion_rotation', 'rotate_around_forward', @@ -67,7 +67,7 @@ def _interpolate_3D(s: Tuple[float, float, float], e: Tuple[float, float, float] # 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): +def simple_follow_3D(speed: float, target: Tuple[float, float, float], data: CameraData): """ A simple method which moves the camera linearly towards the target point. @@ -87,10 +87,10 @@ def simple_follow_2D(speed: float, target: Tuple[float, float], data: CameraData :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) + simple_follow_3D(speed, target + (0,), data) -def simple_easing(percent: float, +def simple_easing_3D(percent: float, start: Tuple[float, float, float], target: Tuple[float, float, float], data: CameraData, func: Callable[[float], float] = linear): @@ -129,4 +129,4 @@ def simple_easing_2D(percent: float, speed does not stay constant. See arcade.easing for examples. """ - simple_easing(percent, start + (0,), target + (0,), data, func) + simple_easing_3D(percent, start + (0,), target + (0,), data, func) From ff61534664b892e157c8c05d4f9f35aa56450784 Mon Sep 17 00:00:00 2001 From: DragonMoffon Date: Wed, 20 Sep 2023 18:48:08 +1200 Subject: [PATCH 37/94] Finalised Core camera functionality - Removed all reference to arcade.ser_viewport method - fixed incorrect forward direction for SimpleCamera - Includes projectors, SimpleCamera, and Camera2D - Includes minimal functions for moving cameras. --- arcade/__init__.py | 2 - arcade/application.py | 49 +- arcade/camera/camera_2d.py | 5 +- arcade/camera/controllers/__init__.py | 8 +- arcade/camera/controllers/debug_controller.py | 183 ------- .../controllers/isometric_controller.py | 95 ---- .../controllers/simple_controller_classes.py | 506 ------------------ arcade/camera/default.py | 64 ++- arcade/camera/offscreen.py | 53 -- arcade/camera/simple_camera.py | 4 +- arcade/context.py | 72 +-- arcade/examples/astar_pathfinding.py | 20 +- arcade/examples/bloom_defender.py | 36 +- arcade/examples/full_screen_example.py | 17 +- arcade/examples/gl/custom_sprite.py | 11 +- arcade/examples/light_demo.py | 43 +- arcade/examples/line_of_sight.py | 47 +- arcade/examples/maze_depth_first.py | 64 +-- arcade/examples/maze_recursive.py | 44 +- arcade/examples/perspective.py | 8 +- arcade/examples/procedural_caves_bsp.py | 38 +- arcade/examples/procedural_caves_cellular.py | 2 +- .../examples/sprite_tiled_map_with_levels.py | 51 +- .../experimental/bloom_multilayer_defender.py | 39 +- arcade/experimental/light_demo.py | 2 +- arcade/experimental/light_demo_perf.py | 2 +- arcade/experimental/shapes_perf.py | 3 +- arcade/experimental/subpixel_experiment.py | 3 +- arcade/gui/surface.py | 7 +- arcade/texture_atlas/base.py | 20 +- arcade/window_commands.py | 60 --- doc/tutorials/lights/01_light_demo.py | 36 +- doc/tutorials/lights/light_demo.py | 38 +- doc/tutorials/views/03_views.py | 2 +- doc/tutorials/views/04_views.py | 4 +- 35 files changed, 338 insertions(+), 1300 deletions(-) delete mode 100644 arcade/camera/controllers/debug_controller.py delete mode 100644 arcade/camera/controllers/isometric_controller.py delete mode 100644 arcade/camera/controllers/simple_controller_classes.py delete mode 100644 arcade/camera/offscreen.py diff --git a/arcade/__init__.py b/arcade/__init__.py index 5a4144859..f12656da9 100644 --- a/arcade/__init__.py +++ b/arcade/__init__.py @@ -79,7 +79,6 @@ def configure_logging(level: Optional[int] = None): from .window_commands import schedule from .window_commands import run from .window_commands import set_background_color -from .window_commands import set_viewport from .window_commands import set_window from .window_commands import start_render from .window_commands import unschedule @@ -355,7 +354,6 @@ def configure_logging(level: Optional[int] = None): 'run', 'schedule', 'set_background_color', - 'set_viewport', 'set_window', 'start_render', 'stop_sound', diff --git a/arcade/application.py b/arcade/application.py index b3967c62e..14750af10 100644 --- a/arcade/application.py +++ b/arcade/application.py @@ -17,7 +17,6 @@ import arcade from arcade import get_display_size -from arcade import set_viewport from arcade import set_window from arcade.color import TRANSPARENT_BLACK from arcade.context import ArcadeContext @@ -205,11 +204,11 @@ def __init__( 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: Projector = DefaultProjector(window=self) + self._default_camera = DefaultProjector(window=self) + self.current_camera: Projector = self._default_camera self.textbox_time = 0.0 self.key: Optional[int] = None self.flip_count: int = 0 @@ -595,13 +594,8 @@ def on_resize(self, width: int, height: int): # The arcade context is not created at that time if hasattr(self, "_ctx"): # Retain projection scrolling if applied - original_viewport = self._ctx.projection_2d - self.set_viewport( - original_viewport[0], - original_viewport[0] + width, - original_viewport[2], - original_viewport[2] + height - ) + self._ctx.viewport = (0, 0, width, height) + self.use_default_camera() def set_min_size(self, width: int, height: int): """ Wrap the Pyglet window call to set minimum size @@ -665,30 +659,25 @@ def set_visible(self, visible: bool = True): """ super().set_visible(visible) - # noinspection PyMethodMayBeStatic - def set_viewport(self, left: float, right: float, bottom: float, top: float): - """ - Set the viewport. (What coordinates we can see. - Used to scale and/or scroll the screen). - - See :py:func:`arcade.set_viewport` for more detailed information. - - :param left: - :param right: - :param bottom: - :param top: - """ - set_viewport(left, right, bottom, top) - - # noinspection PyMethodMayBeStatic - def get_viewport(self) -> Tuple[float, float, float, float]: - """ Get the viewport. (What coordinates we can see.) """ - return self.ctx.projection_2d - def use(self): """Bind the window's framebuffer for rendering commands""" self.ctx.screen.use() + @property + def default_camera(self): + """ + Provides a reference to the default arcade camera. + Automatically sets projection and view to the size + of the screen. Good for resetting the screen. + """ + return self._default_camera + + def use_default_camera(self): + """ + Uses the default arcade camera. Good for quickly resetting the screen + """ + self._default_camera.use() + def test(self, frames: int = 10): """ Used by unit test cases. Runs the event loop a few times and stops. diff --git a/arcade/camera/camera_2d.py b/arcade/camera/camera_2d.py index a090423f6..f29bc9c65 100644 --- a/arcade/camera/camera_2d.py +++ b/arcade/camera/camera_2d.py @@ -65,16 +65,15 @@ def __init__(self, *, projection_data: Optional[OrthographicProjectionData] = None ): self._window: "Window" = window or get_window() - print(camera_data, any((viewport, position, up, zoom))) assert ( - not any((viewport, position, up, zoom)) and not camera_data + not any((viewport, position, up, zoom)) or not bool(camera_data) ), ( "Camera2D Warning: Provided both a CameraData object and raw values. Defaulting to CameraData." ) assert ( - not any((projection, near, far)) and not projection_data + not any((projection, near, far)) or not bool(projection_data) ), ( "Camera2D Warning: Provided both an OrthographicProjectionData object and raw values." "Defaulting to OrthographicProjectionData." diff --git a/arcade/camera/controllers/__init__.py b/arcade/camera/controllers/__init__.py index 97b07b51b..f37a45184 100644 --- a/arcade/camera/controllers/__init__.py +++ b/arcade/camera/controllers/__init__.py @@ -1,14 +1,14 @@ from arcade.camera.controllers.simple_controller_functions import ( - simple_follow, + simple_follow_3D, simple_follow_2D, - simple_easing, + simple_easing_3D, simple_easing_2D ) __all__ = [ - 'simple_follow', + 'simple_follow_3D', 'simple_follow_2D', - 'simple_easing', + 'simple_easing_3D', 'simple_easing_2D' ] diff --git a/arcade/camera/controllers/debug_controller.py b/arcade/camera/controllers/debug_controller.py deleted file mode 100644 index 016b25728..000000000 --- a/arcade/camera/controllers/debug_controller.py +++ /dev/null @@ -1,183 +0,0 @@ -# TODO: Add 2D and 3D versions of a polled input controller -# intended to be used for debugging. -from typing import TYPE_CHECKING, Tuple, Optional -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: Optional["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: Optional["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 diff --git a/arcade/camera/controllers/isometric_controller.py b/arcade/camera/controllers/isometric_controller.py deleted file mode 100644 index 1f694fc26..000000000 --- a/arcade/camera/controllers/isometric_controller.py +++ /dev/null @@ -1,95 +0,0 @@ -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: - - 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_camera(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) - 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]) - ) - - _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 = ( - 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 diff --git a/arcade/camera/controllers/simple_controller_classes.py b/arcade/camera/controllers/simple_controller_classes.py deleted file mode 100644 index a51b0d6b0..000000000 --- a/arcade/camera/controllers/simple_controller_classes.py +++ /dev/null @@ -1,506 +0,0 @@ -from typing import TYPE_CHECKING, Optional, Tuple -from math import e, sin, cos, radians, pi, log -from random import uniform - -from arcade.camera.data import CameraData -from arcade.window_commands import get_window - -if TYPE_CHECKING: - from arcade.application import Window - - -class ScreenShaker2D: - """ - Ae^{-fx} - Ae^{-(z+f)x} - """ - - def __init__(self, camera_data: CameraData, *, - default_falloff: float = 1.0, - default_acceleration: float = 1.0, - default_amplitude: float = 1.0, - default_direction: Optional[Tuple[float, float]] = None): - self._data: CameraData = camera_data - - self._d_falloff: float = default_falloff - self._d_acceleration: float = default_acceleration - self._d_amplitude: float = default_amplitude - self._d_direction: Optional[Tuple[float, float]] = default_direction - - self._t_falloff: float = default_falloff - self._t_acceleration: float = default_acceleration - self._t_amplitude: float = default_amplitude - self._t_direction: Optional[Tuple[float, float]] = default_direction - - def _curve(self, _t: float): - pass - - def start(self): - pass - - def stop(self): - pass - - def stop_after(self, seconds: float): - pass - - -class ScreenShaker2D_old: - """ - Uses the equation Ae^{-fx}sin(v 2 pi x) to create shake effect which lessens over time. - The shaking can be started with using the Start method. Every value has a default, - and a temporary value. The temporary values are reset every time the animation - is started. To stop the animation call stop or stop_in. These do not instantly - stop the animation, but rather change the animation curve to have a smoother stop. - The animation also has a jitter value in degrees which is how much to rotate the - shake direction each oscillation. - - If you want the shaking to stay the same intensity and then quickly stop it is easiest - to set the falloff to 0.0, and then call stop when you want the shaking to stop. - - where: - A is the amplitude (size in pixels) - f is the falloff (Higher the faster. A value of one decays after 2.25 seconds) - v is the speed (each integer value above one increase the cycles per second by one) - x is the time since start - e is euler's constant (pronounced oil-ers) - - - Args: - camera_data: The Camera Data to manipulate. - window: The Arcade Window, uses currently active by default. - default_shake_size: The maximum amplitude away from the starting position (Equal to A). - default_shake_falloff: The rate that the screen shake weakens (Equal to f). - default_shake_speed: The speed or frequency of oscillations (Equal to v). - default_shake_jitter: The max angle that the shake direction can change by. - default_shake_direction: The direction the oscillations follow. - """ - - stop_range: float = 0.1 - # TODO Doc Strings - - def __init__(self, camera_data: CameraData, default_shake_size: float = 1.0, default_shake_falloff: float = 1.0, *, - window: Optional["Window"] = None, - default_shake_speed: float = 1.0, - default_shake_jitter: float = 0.0, - default_shake_direction: Tuple[float, float] = (-1.0, 0.0)): - self._win: "Window" = window or get_window() - self._data: CameraData = camera_data - - self._d_dir = default_shake_direction - self._d_speed = default_shake_speed - self._d_amplitude = default_shake_size - self._d_falloff = default_shake_falloff - self._d_jitter = default_shake_jitter - - self._t_dir = default_shake_direction - self._t_speed = default_shake_speed - self._t_amplitude = default_shake_size - self._t_falloff = default_shake_falloff - self._t_jitter = default_shake_jitter - - self._shake: bool = False - self._time: float = 0.0 - self._stop_time: float = -1.0 - self._t_pos: Tuple[float, float] = camera_data.position[:2] - - def start(self, *, - true_pos: Optional[Tuple[float, float]] = None, - temp_size: Optional[float] = None, - temp_falloff: Optional[float] = None, - temp_speed: Optional[float] = None, - temp_jitter: Optional[float] = None, - temp_direction: Optional[Tuple[float, float]] = None) -> None: - """ - Start the shaking animation, you can set a temporary position for any of the components of the - animation. These values will not be saved once the animation finishes. The true pos is NOT a - temp value. - - Args: - true_pos: The position that the shaking will originate from. - - temp_size: A temporary, one animation only, value of the max amplitude of the shaking - - temp_falloff: A temporary, one animation only, value for the speed of falloff. Higher is faster. - - temp_speed: A temporary, one animation only, value for the oscillation speeds. - Each integer value adds one full cycle per second. - - temp_jitter: A temporary, one animation only, value which determines the max - angle change after each cycle. - - temp_direction: A temporary, one animation only, tuple which describes the x, y direction - the shaking will follow. - """ - - self._t_dir = temp_direction if temp_direction is not None else self._d_dir - self._t_jitter = temp_jitter if temp_jitter is not None else self._d_jitter - self._t_speed = temp_speed if temp_speed is not None else self._d_speed - self._t_falloff = temp_falloff if temp_falloff is not None else self._d_falloff - self._t_amplitude = temp_size if temp_size is not None else self._d_amplitude - - self._time = 0.0 - self._shake = True - self._t_pos = true_pos if true_pos is not None else self._t_pos - self._stop_time = self.estimated_length() - - def _curve(self, _t: float) -> float: - """ - Calculate the equation A exp(-ft) sin(2v pi t) which describes - the distance from the true pos. - - Args: - _t: Time in seconds. - Returns: - The calculated Amplitude. - """ - return self._t_amplitude * e**(-self._t_falloff*_t) * sin(self._t_speed * 2.0 * pi * self._time) - - def estimated_length(self) -> float: - """ - Estimates how much longer the animation still has to go with the current settings. - - Returns: - Roughly the number of seconds until the shaking will stop. - """ - if self._t_falloff == 0.0: - return float('inf') - _t = (log(self._t_amplitude) - log(self.stop_range)) / self._t_falloff - _dt = _t % (0.5 / self._t_speed) # Find the distance from to the last x = 0.0 - return _t - _dt - - def shaking(self) -> bool: - """ - Returns whether the controller is causing screen shake or updating. - """ - return self._shake - - def stop(self) -> None: - """ - Tell the controller to stop at the next point of 0 amplitude. - - This alters the temporary values used by the controller. - If any are changed after the fact the controller may not stop. - """ - self.stop_in(1 / self._t_speed - (self._time % (0.5 / self._t_speed))) - - def stop_in(self, _time: float) -> None: - """ - Tell the controller to stop after a certain number of seconds. - The controller will actually stop at the closest point of low amplitude - to prevent any sudden position snapping. - - This alters the temporary values used by the controller. - If any are changed after the fact the controller may not stop. - - Args: - _time: The number of seconds to stop after. - """ - # This derivation was a pain - _dt = self._time % (1.0 / self._t_speed) - - _f = log(self._t_amplitude) - log(0.1) - self._t_falloff * self._time - _a = self._t_amplitude * e**(_f * _dt - self._t_falloff * self._time) - - self._t_amplitude = _a - self._t_falloff = _f - self._time = _dt - - _st = (_time + _dt) % (0.5 / self._t_speed) - - self._stop_time = _dt + _time - _st - - def update(self, delta_time: float, true_pos: Optional[Tuple[float, float]] = None) -> None: - """ - Run the update logic for the controller. This should be called every update, but only once. - This does not actually change the position of the camera. That is done in update_camera. - - Args: - delta_time: The length of time since the last update call. Use the delta_time provided - in arcade.Window.on_update, or arcade.View.on_update. - 3.x+: will be replaced whatever delta_time solution is created in the future. - - true_pos: The position from which the oscillation originates. If you have another animation - which calculates the position for the camera you can pass it in here, but there is - also a pos property. - """ - if true_pos is not None: - self._t_pos = true_pos - - if 0.0 <= self._stop_time <= self._time + delta_time: - self._time = 0.0 - self._shake = False - return - - if not self._shake: - self._time = 0.0 - return - - # The oscillator passes through 0.0 at regular intervals. - # If the next time is past this interval then it is clear that 0.0 - # has been passed and jitter should be calculated. - _m = 0.5 / self._t_speed - if self._t_jitter != 0.0 and self._time % _m > (self._time + delta_time) % _m: - radians_shift = radians(uniform(-self._t_jitter/2.0, self._t_jitter/2.0)) - _c, _s = cos(radians_shift), sin(radians_shift) - dx = self._t_dir[0] * _c - self._t_dir[1] * _s - dy = self._t_dir[0] * _s + self._t_dir[1] * _c - self._t_dir = (dx, dy) - - self._time += delta_time - - def update_camera(self) -> None: - """ - Take the current time and temp values and update the position of the camera. - This method has no logic, and should never have any logic. Calling this method multiple times - will not change the result. If you need to update the true position of the camera - use the pos property, or the update method. - """ - step = self._curve(self._time) - pos = ( - self._t_pos[0] + step * self._t_dir[0], - self._t_pos[1] + step * self._t_dir[1] - ) - self._data.position = (pos[0], pos[1], self._data.position[2]) - - @property - def pos(self) -> Tuple[float, float]: - """ - The point that the shaking offsets from. - camera position will equal true pos when amplitude is 0 - """ - return self._t_pos - - @pos.setter - def pos(self, _pos: Tuple[float, float]) -> None: - """ - The point that the shaking offsets from. - camera position will equal true pos when amplitude is 0 - """ - self._t_pos = _pos - - @property - def direction(self) -> Tuple[float, float]: - """ - The direction the shaking will move in. - MAKE SURE THE LENGTH IS 1.0. - """ - return self._t_dir - - @direction.setter - def direction(self, _dir: Tuple[float, float]): - """ - Temporarily sets this value. To permanently change it use default_direction - - The direction the shaking will move in. - MAKE SURE THE LENGTH IS 1.0. - """ - self._t_dir = _dir - self._stop_time = self.estimated_length() - - @property - def jitter(self) -> float: - """ - The max angle the direction can randomly change by every cycle. - """ - return self._t_jitter - - @jitter.setter - def jitter(self, _jitter: float): - """ - Temporarily sets this value. To permanently change it use default_jitter - - The max angle the direction can randomly change by every cycle. - """ - self._t_jitter = _jitter - self._stop_time = self.estimated_length() - - @property - def speed(self) -> float: - """ - The number of oscillations per second. - Every integer increase causes one extra cycle. - """ - return self._t_speed - - @speed.setter - def speed(self, _speed: float): - """ - Temporarily sets this value. To permanently change it use default_speed - - The number of oscillations per second. - Every integer increase causes one extra cycle. - """ - self._t_speed = _speed - self._stop_time = self.estimated_length() - - @property - def falloff(self) -> float: - """ - The rate at which the oscillations die down. Higher the faster. - The decay uses euler's (pronounced oil-ers) number. - """ - return self._t_falloff - - @falloff.setter - def falloff(self, _falloff: float): - """ - Temporarily sets this value. To permanently change it use default_falloff - - The rate at which the oscillations die down. Higher the faster. - The decay uses euler's (pronounced oil-ers) number. - """ - self._t_falloff = _falloff - self._stop_time = self.estimated_length() - - @property - def amplitude(self) -> float: - """ - The maximum length away from the true pos in pixels. - """ - return self._t_amplitude - - @amplitude.setter - def amplitude(self, _amplitude: float): - """ - Temporarily sets this value. To permanently change it use default_amplitude - - The maximum length away from the true pos in pixels. - """ - self._t_amplitude = _amplitude - self._stop_time = self.estimated_length() - - @property - def default_direction(self) -> Tuple[float, float]: - """ - The direction the shaking will move in. - MAKE SURE THE LENGTH IS 1.0. - """ - - return self._d_dir - - @default_direction.setter - def default_direction(self, _dir: Tuple[float, float]): - """ - Permanently sets this value. To temporarily change it use direction - - The direction the shaking will move in. - MAKE SURE THE LENGTH IS 1.0. - """ - - self._d_dir = _dir - self._t_dir = _dir - self._stop_time = self.estimated_length() - - @property - def default_jitter(self) -> float: - """ - The max angle the direction can randomly change by every cycle. - """ - return self._d_jitter - - @default_jitter.setter - def default_jitter(self, _jitter: float): - """ - Permanently sets this value. To temporarily change it use jitter - - The max angle the direction can randomly change by every cycle. - """ - self._d_jitter = _jitter - self._t_jitter = _jitter - self._stop_time = self.estimated_length() - - @property - def default_speed(self) -> float: - """ - The number of oscillations per second. - Every integer increase causes one extra cycle. - """ - return self._d_speed - - @default_speed.setter - def default_speed(self, _speed: float): - """ - Permanently sets this value. To temporarily change it use speed - - The number of oscillations per second. - Every integer increase causes one extra cycle. - """ - self._d_speed = _speed - self._t_speed = _speed - self._stop_time = self.estimated_length() - - @property - def default_falloff(self) -> float: - """ - The rate at which the oscillations die down. Higher the faster. - The decay uses euler's (pronounced oil-ers) number. - """ - return self._d_falloff - - @default_falloff.setter - def default_falloff(self, _falloff: float): - """ - Permanently sets this value. To temporarily change it use falloff - - The rate at which the oscillations die down. Higher the faster. - The decay uses euler's (pronounced oil-ers) number. - """ - self._d_falloff = _falloff - self._t_falloff = _falloff - self._stop_time = self.estimated_length() - - @property - def default_amplitude(self) -> float: - """ - The maximum length away from the true pos in pixels. - """ - return self._d_amplitude - - @default_amplitude.setter - def default_amplitude(self, _amplitude: float): - """ - Permanently sets this value. To temporarily change it use amplitude - - The maximum length away from the true pos in pixels. - """ - self._d_amplitude = _amplitude - self._t_amplitude = _amplitude - self._stop_time = self.estimated_length() - - -def _shakey(): - from arcade import Window, draw_point - - from arcade.camera import Camera2D - - win = Window() - cam = Camera2D() - shake = ScreenShaker2D(cam.data, 200, default_shake_speed=3.0, default_shake_jitter=20, default_shake_falloff=0.0) - - def on_key_press(*args): - if shake.shaking(): - shake.stop() - else: - shake.start() - - win.on_key_press = on_key_press # type: ignore - - def on_update(delta_time: float): - shake.update(delta_time) - - win.on_update = on_update # type: ignore - - def on_draw(): - win.clear() - shake.update_camera() - cam.use() - draw_point(100, 100, (255, 255, 255, 255), 10) - - win.on_draw = on_draw # type: ignore - - win.run() - - -if __name__ == '__main__': - _shakey() - diff --git a/arcade/camera/default.py b/arcade/camera/default.py index 81bb38eb2..4f81efc42 100644 --- a/arcade/camera/default.py +++ b/arcade/camera/default.py @@ -9,43 +9,38 @@ from arcade.application import Window __all__ = [ + 'ViewportProjector', '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. - """ - # TODO: ADD PARAMS TO DOC FOR __init__ - - def __init__(self, *, window: Optional["Window"] = None): - self._window: "Window" = window or get_window() +class ViewportProjector: - self._viewport: Tuple[int, int, int, int] = self._window.viewport + def __init__(self, viewport: Optional[Tuple[int, int, int, int]] = None, *, window: Optional["Window"] = None): + self._window = window or get_window() - self._projection_matrix: Mat4 = Mat4() + self._viewport = viewport or self._window.ctx.viewport + self._projection_matrix: Mat4 = Mat4.orthogonal_projection(0, self._viewport[2], + 0, self._viewport[3], + -100, 100) - def _generate_projection_matrix(self): - left = self._viewport[0] - right = self._viewport[0] + self._viewport[2] + @property + def viewport(self): + return self._viewport - bottom = self._viewport[1] - top = self._viewport[1] + self._viewport[3] + @viewport.setter + def viewport(self, viewport: Tuple[int, int, int, int]): + self._viewport = viewport - self._projection_matrix = Mat4.orthogonal_projection(left, right, bottom, top, -100, 100) + self._projection_matrix = Mat4.orthogonal_projection(0, viewport[2], + 0, viewport[3], + -100, 100) def use(self): - if self._viewport != self._window.viewport: - self._viewport = self._window.viewport - self._generate_projection_matrix() + self._window.ctx.viewport = self._viewport - self._window.view = Mat4() - self._window.projection = self._projection_matrix + self._window.ctx.view_matrix = Mat4() + self._window.ctx.projection_matrix = self._projection_matrix @contextmanager def activate(self) -> Iterator[Projector]: @@ -58,3 +53,22 @@ def activate(self) -> Iterator[Projector]: def map_coordinate(self, screen_coordinate: Tuple[float, float]) -> Tuple[float, float]: return screen_coordinate + + +# As this class is only supposed to be used internally +# I wanted to place an _ in front, but the linting complains +# about it being a protected class. +class DefaultProjector(ViewportProjector): + """ + An extremely limited projector which lacks any kind of control. This is only 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): + super().__init__(window=window) + + def use(self): + if self._window.ctx.viewport != self.viewport: + self.viewport = self._window.ctx.viewport + super().use() diff --git a/arcade/camera/offscreen.py b/arcade/camera/offscreen.py deleted file mode 100644 index 1999cadb2..000000000 --- a/arcade/camera/offscreen.py +++ /dev/null @@ -1,53 +0,0 @@ -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 -from arcade.gl.geometry import quad_2d_fs -if TYPE_CHECKING: - from arcade.application import Window - - -class OffScreenSpace: - _geometry: Optional[Geometry] = quad_2d_fs() - - # TODO: Doc String - - def __init__(self, *, - 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() - 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() - - def show(self): - self._fbo.color_attachments[0].use(0) - OffScreenSpace._geometry.render(self._win.ctx.utility_textured_quad_program) - - @contextmanager - 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: - 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/simple_camera.py b/arcade/camera/simple_camera.py index b36da3f5d..34760b01b 100644 --- a/arcade/camera/simple_camera.py +++ b/arcade/camera/simple_camera.py @@ -52,7 +52,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 ) _projection = projection or ( @@ -69,7 +69,7 @@ def __init__(self, *, (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 + (0.0, 0.0, -1.0), # Forward 1.0 # Zoom ) self._projection = projection_data or OrthographicProjectionData( diff --git a/arcade/context.py b/arcade/context.py index 433ccc427..2128d4150 100644 --- a/arcade/context.py +++ b/arcade/context.py @@ -5,7 +5,7 @@ from __future__ import annotations from pathlib import Path -from typing import Any, Iterable, Dict, Optional, Tuple, Union, Sequence +from typing import Any, Iterable, Dict, Optional, Union, Sequence from contextlib import contextmanager import pyglet @@ -25,6 +25,7 @@ __all__ = ["ArcadeContext"] + class ArcadeContext(Context): """ An OpenGL context implementation for Arcade with added custom features. @@ -56,12 +57,14 @@ def __init__(self, window: pyglet.window.Window, gc_mode: str = "context_gc", gl # Set up a default orthogonal projection for sprites and shapes self._window_block: UniformBufferObject = window.ubo self.bind_window_block() - self.projection_2d = ( + self.viewport = ( 0, - self.screen.width, 0, + self.screen.width, self.screen.height, ) + self.projection_matrix = Mat4.orthogonal_projection(0, self.screen.width, 0, self.screen.height, + -1, 1) # --- Pre-load system shaders here --- # FIXME: These pre-created resources needs to be packaged nicely @@ -211,7 +214,9 @@ def reset(self) -> None: self.screen.use(force=True) self.bind_window_block() # self.active_program = None - arcade.set_viewport(0, self.window.width, 0, self.window.height) + self.viewport = 0, 0, self.window.width, self.window.height + self.view_matrix = Mat4() + self.projection_matrix = Mat4.orthogonal_projection(0, self.window.width, 0, self.window.height, -100, 100) self.enable_only(self.BLEND) self.blend_func = self.BLEND_DEFAULT self.point_size = 1.0 @@ -251,51 +256,7 @@ def default_atlas(self) -> TextureAtlas: return self._atlas @property - def projection_2d(self) -> Tuple[float, float, float, float]: - """Get or set the global orthogonal projection for arcade. - - This projection is used by sprites and shapes and is represented - by four floats: ``(left, right, bottom, top)`` - - When reading this property we reconstruct the projection parameters - from pyglet's projection matrix. When setting this property - we construct an orthogonal projection matrix and set it in pyglet. - - :type: Tuple[float, float, float, float] - """ - mat = self.window.projection - - # Reconstruct the projection values from the matrix - # TODO: Take scale into account - width = 2.0 / mat[0] - height = 2.0 / mat[5] - a = width * mat[12] - b = height * mat[13] - left = -(width + a) / 2 - right = left + width - bottom = -(height + b) / 2 - top = bottom + height - - return left, right, bottom, top - - @projection_2d.setter - def projection_2d(self, value: Tuple[float, float, float, float]): - if not isinstance(value, tuple) or len(value) != 4: - raise ValueError( - f"projection must be a 4-component tuple, not {type(value)}: {value}" - ) - - # Don't try to set zero projection leading to division by zero - width, height = self.window.get_size() - if width == 0 or height == 0: - return - - self.window.projection = Mat4.orthogonal_projection( - value[0], value[1], value[2], value[3], -100, 100, - ) - - @property - def projection_2d_matrix(self) -> Mat4: + def projection_matrix(self) -> Mat4: """ Get the current projection matrix. This 4x4 float32 matrix is calculated when setting :py:attr:`~arcade.ArcadeContext.projection_2d`. @@ -306,15 +267,15 @@ def projection_2d_matrix(self) -> Mat4: """ return self.window.projection - @projection_2d_matrix.setter - def projection_2d_matrix(self, value: Mat4): + @projection_matrix.setter + def projection_matrix(self, value: Mat4): if not isinstance(value, Mat4): raise ValueError("projection_matrix must be a Mat4 object") self.window.projection = value @property - def view_matrix_2d(self) -> Mat4: + def view_matrix(self) -> Mat4: """ Get the current view matrix. This 4x4 float32 matrix is calculated when setting :py:attr:`~arcade.ArcadeContext.view_matrix_2d`. @@ -325,13 +286,12 @@ def view_matrix_2d(self) -> Mat4: """ return self.window.view - @view_matrix_2d.setter - def view_matrix_2d(self, value: Mat4): + @view_matrix.setter + def view_matrix(self, value: Mat4): if not isinstance(value, Mat4): raise ValueError("view_matrix must be a Mat4 object") - self._view_matrix_2d = value - self.window.view = self._view_matrix_2d + self.window.view = value @contextmanager def pyglet_rendering(self): diff --git a/arcade/examples/astar_pathfinding.py b/arcade/examples/astar_pathfinding.py index 1ddb04793..46a47ed47 100644 --- a/arcade/examples/astar_pathfinding.py +++ b/arcade/examples/astar_pathfinding.py @@ -10,6 +10,7 @@ from __future__ import annotations import arcade +from arcade import camera import random SPRITE_IMAGE_SIZE = 128 @@ -67,6 +68,9 @@ def __init__(self, width, height, title): # Set the window background color self.background_color = arcade.color.AMAZON + # Camera + self.cam = None + def setup(self): """ Set up the game and initialize the variables. """ @@ -142,6 +146,8 @@ def setup(self): playing_field_bottom_boundary, playing_field_top_boundary) + self.cam = camera.Camera2D() + def on_draw(self): """ Render the screen. @@ -188,8 +194,8 @@ def on_update(self, delta_time): # --- Manage Scrolling --- - # Keep track of if we changed the boundary. We don't want to call the - # set_viewport command if we didn't change the view port. + # Keep track of if we changed the boundary. We don't want to update the + # viewport or projection if we didn't change the view port. changed = False # Scroll left @@ -225,10 +231,12 @@ def on_update(self, delta_time): # If we changed the boundary values, update the view port to match if changed: - arcade.set_viewport(self.view_left, - SCREEN_WIDTH + self.view_left, - self.view_bottom, - SCREEN_HEIGHT + self.view_bottom) + self.cam.projection = ( + self.view_left, + SCREEN_WIDTH + self.view_left, + self.view_bottom, + SCREEN_HEIGHT + self.view_bottom) + self.cam.use() def on_key_press(self, key, modifiers): """Called whenever a key is pressed. """ diff --git a/arcade/examples/bloom_defender.py b/arcade/examples/bloom_defender.py index 323ad7474..88e3cd3dc 100644 --- a/arcade/examples/bloom_defender.py +++ b/arcade/examples/bloom_defender.py @@ -113,7 +113,7 @@ class Bullet(arcade.SpriteSolidColor): """ Bullet """ def __init__(self, width, height, color): - super().__init__(width, height, color) + super().__init__(width, height, color=color) self.distance = 0 def update(self): @@ -163,12 +163,12 @@ def __init__(self, width, height, title): self.up_pressed = False self.down_pressed = False - self.view_bottom = 0 - self.view_left = 0 - # Set the background color of the window self.background_color = arcade.color.BLACK + # Camera + self.cam = arcade.camera.Camera2D() + # --- Bloom related --- # Frame to receive the glow, and color attachment to store each pixel's @@ -244,10 +244,7 @@ def on_draw(self): self.bloom_screen.use() self.bloom_screen.clear(arcade.color.TRANSPARENT_BLACK) - arcade.set_viewport(self.view_left, - SCREEN_WIDTH + self.view_left, - self.view_bottom, - SCREEN_HEIGHT + self.view_bottom) + self.cam.use() # Draw all the sprites on the screen that should have a bloom self.star_sprite_list.draw() @@ -256,10 +253,7 @@ def on_draw(self): # Now draw to the actual screen self.use() - arcade.set_viewport(self.view_left, - SCREEN_WIDTH + self.view_left, - self.view_bottom, - SCREEN_HEIGHT + self.view_bottom) + self.cam.use() # --- Bloom related --- @@ -306,23 +300,23 @@ def on_update(self, delta_time): self.bullet_sprite_list.append(particle) # Scroll left - left_boundary = self.view_left + VIEWPORT_MARGIN + left_boundary = self.cam.left + VIEWPORT_MARGIN if self.player_sprite.left < left_boundary: - self.view_left -= left_boundary - self.player_sprite.left + self.cam.left -= left_boundary - self.player_sprite.left # Scroll right - right_boundary = self.view_left + SCREEN_WIDTH - VIEWPORT_MARGIN + right_boundary = self.cam.right - VIEWPORT_MARGIN if self.player_sprite.right > right_boundary: - self.view_left += self.player_sprite.right - right_boundary + self.cam.right += self.player_sprite.right - right_boundary # Scroll up - self.view_bottom = DEFAULT_BOTTOM_VIEWPORT - top_boundary = self.view_bottom + SCREEN_HEIGHT - TOP_VIEWPORT_MARGIN + self.cam.bottom = DEFAULT_BOTTOM_VIEWPORT + top_boundary = DEFAULT_BOTTOM_VIEWPORT + SCREEN_HEIGHT - TOP_VIEWPORT_MARGIN if self.player_sprite.top > top_boundary: - self.view_bottom += self.player_sprite.top - top_boundary + self.cam.bottom += self.player_sprite.top - top_boundary - self.view_left = int(self.view_left) - self.view_bottom = int(self.view_bottom) + self.cam.left = int(self.cam.left) + self.cam.bottom = int(self.cam.bottom) def on_key_press(self, key, modifiers): """Called whenever a key is pressed. """ diff --git a/arcade/examples/full_screen_example.py b/arcade/examples/full_screen_example.py index 07e2a9e2c..2a3e04192 100644 --- a/arcade/examples/full_screen_example.py +++ b/arcade/examples/full_screen_example.py @@ -40,11 +40,13 @@ def __init__(self): # This will get the size of the window, and set the viewport to match. # So if the window is 1000x1000, then so will our viewport. If # you want something different, then use those coordinates instead. - width, height = self.get_size() - self.set_viewport(0, width, 0, height) self.background_color = arcade.color.AMAZON self.example_image = arcade.load_texture(":resources:images/tiles/boxCrate_double.png") + # The camera used to update the viewport and projection on screen resize. + # The position needs to be set to the bottom left corner. + self.cam = arcade.camera.Camera2D(position=(0.0, 0.0)) + def on_draw(self): """ Render the screen. @@ -53,7 +55,7 @@ def on_draw(self): self.clear() # Get viewport dimensions - left, screen_width, bottom, screen_height = self.get_viewport() + screen_width, screen_height = int(self.cam.projection_width), int(self.cam.projection_height) text_size = 18 # Draw text on the screen so the user has an idea of what is happening @@ -79,8 +81,9 @@ def on_key_press(self, key, modifiers): # Get the window coordinates. Match viewport to window coordinates # so there is a one-to-one mapping. - width, height = self.get_size() - self.set_viewport(0, width, 0, height) + self.cam.projection = 0, self.width, 0, self.height + self.cam.viewport = 0, 0, self.width, self.height + self.cam.use() if key == arcade.key.S: # User hits s. Flip between full and not full screen. @@ -89,7 +92,9 @@ def on_key_press(self, key, modifiers): # Instead of a one-to-one mapping, stretch/squash window to match the # constants. This does NOT respect aspect ratio. You'd need to # do a bit of math for that. - self.set_viewport(0, SCREEN_WIDTH, 0, SCREEN_HEIGHT) + self.cam.projection = 0, SCREEN_WIDTH, 0, SCREEN_HEIGHT + self.cam.viewport = 0, 0, self.width, self.height + self.cam.use() def main(): diff --git a/arcade/examples/gl/custom_sprite.py b/arcade/examples/gl/custom_sprite.py index d73e77054..0e2228ef3 100644 --- a/arcade/examples/gl/custom_sprite.py +++ b/arcade/examples/gl/custom_sprite.py @@ -27,6 +27,7 @@ from random import randint from array import array import arcade +from arcade.camera import Camera2D from arcade.gl.types import BufferDescription @@ -34,6 +35,7 @@ class GeoSprites(arcade.Window): def __init__(self): super().__init__(800, 600, "Custom Sprites", resizable=True) + self.cam = Camera2D() self.program = self.ctx.program( vertex_shader=""" #version 330 @@ -149,6 +151,7 @@ def __init__(self): def on_draw(self): self.clear() + self.cam.use() # Bind our sprite texture to channel 0 self.texture.use(unit=0) # Render the sprite data with our shader @@ -156,13 +159,7 @@ def on_draw(self): def on_mouse_drag(self, x: float, y: float, dx: float, dy: float, buttons: int, modifiers: int): """Make it easier to explore the geometry by scrolling""" - proj = self.ctx.projection_2d - self.ctx.projection_2d = ( - proj[0] - dx, - proj[1] - dx, - proj[2] - dy, - proj[3] - dy, - ) + self.cam.pos = self.cam.pos[0] - dx, self.cam.pos[1] - dy def gen_sprites(self, count: int): """Quickly generate some random sprite data""" diff --git a/arcade/examples/light_demo.py b/arcade/examples/light_demo.py index 9fe7bba0c..952fa6c05 100644 --- a/arcade/examples/light_demo.py +++ b/arcade/examples/light_demo.py @@ -38,9 +38,8 @@ def __init__(self, width, height, title): # Physics engine self.physics_engine = None - # Used for scrolling - self.view_left = 0 - self.view_bottom = 0 + # Camera + self.cam: arcade.camera.Camera2D = None # --- Light related --- # List of all the lights @@ -51,6 +50,9 @@ def __init__(self, width, height, title): def setup(self): """ Create everything """ + # Create camera + self.cam = arcade.camera.Camera2D() + # Create sprite lists self.background_sprite_list = arcade.SpriteList() self.player_list = arcade.SpriteList() @@ -189,11 +191,6 @@ def setup(self): # Create the physics engine self.physics_engine = arcade.PhysicsEngineSimple(self.player_sprite, self.wall_list) - # Set the viewport boundaries - # These numbers set where we have 'scrolled' to. - self.view_left = 0 - self.view_bottom = 0 - def on_draw(self): """ Draw everything. """ self.clear() @@ -213,12 +210,14 @@ def on_draw(self): # Now draw anything that should NOT be affected by lighting. arcade.draw_text("Press SPACE to turn character light on/off.", - 10 + self.view_left, 10 + self.view_bottom, + 10 + int(self.cam.left), 10 + int(self.cam.bottom), arcade.color.WHITE, 20) def on_resize(self, width, height): """ User resizes the screen. """ + self.cam.viewport = 0, 0, width, height + # --- Light related --- # We need to resize the light layer to self.light_layer.resize(width, height) @@ -254,40 +253,38 @@ def on_key_release(self, key, _): elif key == arcade.key.LEFT or key == arcade.key.RIGHT: self.player_sprite.change_x = 0 + def scroll_screen(self): """ Manage Scrolling """ # Scroll left - left_boundary = self.view_left + VIEWPORT_MARGIN + left_boundary = self.cam.left + VIEWPORT_MARGIN if self.player_sprite.left < left_boundary: - self.view_left -= left_boundary - self.player_sprite.left + self.cam.left -= left_boundary - self.player_sprite.left # Scroll right - right_boundary = self.view_left + self.width - VIEWPORT_MARGIN + right_boundary = self.cam.right - VIEWPORT_MARGIN if self.player_sprite.right > right_boundary: - self.view_left += self.player_sprite.right - right_boundary + self.cam.right += self.player_sprite.right - right_boundary # Scroll up - top_boundary = self.view_bottom + self.height - VIEWPORT_MARGIN + top_boundary = self.cam.top - VIEWPORT_MARGIN if self.player_sprite.top > top_boundary: - self.view_bottom += self.player_sprite.top - top_boundary + self.cam.top += self.player_sprite.top - top_boundary # Scroll down - bottom_boundary = self.view_bottom + VIEWPORT_MARGIN + bottom_boundary = self.cam.bottom + VIEWPORT_MARGIN if self.player_sprite.bottom < bottom_boundary: - self.view_bottom -= bottom_boundary - self.player_sprite.bottom + self.cam.bottom -= bottom_boundary - self.player_sprite.bottom # Make sure our boundaries are integer values. While the viewport does # support floating point numbers, for this application we want every pixel # in the view port to map directly onto a pixel on the screen. We don't want # any rounding errors. - self.view_left = int(self.view_left) - self.view_bottom = int(self.view_bottom) + self.cam.left = int(self.cam.left) + self.cam.bottom = int(self.cam.bottom) - arcade.set_viewport(self.view_left, - self.width + self.view_left, - self.view_bottom, - self.height + self.view_bottom) + self.cam.use() def on_update(self, delta_time): """ Movement and game logic """ diff --git a/arcade/examples/line_of_sight.py b/arcade/examples/line_of_sight.py index fe600d731..daf1105cb 100644 --- a/arcade/examples/line_of_sight.py +++ b/arcade/examples/line_of_sight.py @@ -52,9 +52,8 @@ def __init__(self, width, height, title): self.physics_engine = None - # Used in scrolling - self.view_bottom = 0 - self.view_left = 0 + # Camera for scrolling + self.cam = None # Set the background color self.background_color = arcade.color.AMAZON @@ -62,6 +61,9 @@ def __init__(self, width, height, title): def setup(self): """ Set up the game and initialize the variables. """ + # Camera + self.cam = arcade.camera.Camera2D() + # Sprite lists self.player_list = arcade.SpriteList() self.wall_list = arcade.SpriteList(use_spatial_hash=True) @@ -146,47 +148,44 @@ def on_update(self, delta_time): # --- Manage Scrolling --- - # Keep track of if we changed the boundary. We don't want to call the - # set_viewport command if we didn't change the view port. + # Keep track of if we changed the boundary. We don't want to + # update the camera if we don't need to. changed = False # Scroll left - left_boundary = self.view_left + VIEWPORT_MARGIN + left_boundary = self.cam.left + VIEWPORT_MARGIN if self.player.left < left_boundary: - self.view_left -= left_boundary - self.player.left + self.cam.left -= left_boundary - self.player.left changed = True # Scroll right - right_boundary = self.view_left + SCREEN_WIDTH - VIEWPORT_MARGIN + right_boundary = self.cam.right - VIEWPORT_MARGIN if self.player.right > right_boundary: - self.view_left += self.player.right - right_boundary + self.cam.right += self.player.right - right_boundary changed = True # Scroll up - top_boundary = self.view_bottom + SCREEN_HEIGHT - VIEWPORT_MARGIN + top_boundary = self.cam.top - VIEWPORT_MARGIN if self.player.top > top_boundary: - self.view_bottom += self.player.top - top_boundary + self.cam.top += self.player.top - top_boundary changed = True # Scroll down - bottom_boundary = self.view_bottom + VIEWPORT_MARGIN + bottom_boundary = self.cam.bottom + VIEWPORT_MARGIN if self.player.bottom < bottom_boundary: - self.view_bottom -= bottom_boundary - self.player.bottom + self.cam.bottom -= bottom_boundary - self.player.bottom changed = True - # Make sure our boundaries are integer values. While the view port does - # support floating point numbers, for this application we want every pixel - # in the view port to map directly onto a pixel on the screen. We don't want - # any rounding errors. - self.view_left = int(self.view_left) - self.view_bottom = int(self.view_bottom) - # If we changed the boundary values, update the view port to match if changed: - arcade.set_viewport(self.view_left, - SCREEN_WIDTH + self.view_left, - self.view_bottom, - SCREEN_HEIGHT + self.view_bottom) + # Make sure our boundaries are integer values. While the view port does + # support floating point numbers, for this application we want every pixel + # in the view port to map directly onto a pixel on the screen. We don't want + # any rounding errors. + self.cam.left = int(self.cam.left) + self.cam.bottom = int(self.cam.bottom) + + self.cam.use() def on_key_press(self, key, modifiers): """Called whenever a key is pressed. """ diff --git a/arcade/examples/maze_depth_first.py b/arcade/examples/maze_depth_first.py index 90e3fc132..8f41917e1 100644 --- a/arcade/examples/maze_depth_first.py +++ b/arcade/examples/maze_depth_first.py @@ -103,9 +103,8 @@ def __init__(self, width, height, title): # Physics engine self.physics_engine = None - # Used to scroll - self.view_bottom = 0 - self.view_left = 0 + # Camera for scrolling + self.cam = None # Time to process self.processing_time = 0 @@ -183,10 +182,8 @@ def setup(self): # Set the background color self.background_color = arcade.color.AMAZON - # Set the viewport boundaries - # These numbers set where we have 'scrolled' to. - self.view_left = 0 - self.view_bottom = 0 + # Setup Camera + self.cam = arcade.camera.Camera2D() def on_draw(self): """ @@ -208,20 +205,20 @@ def on_draw(self): output = f"Sprite Count: {sprite_count}" arcade.draw_text(output, - self.view_left + 20, - SCREEN_HEIGHT - 20 + self.view_bottom, + self.cam.left + 20, + SCREEN_HEIGHT - 20 + self.cam.bottom, arcade.color.WHITE, 16) output = f"Drawing time: {self.draw_time:.3f}" arcade.draw_text(output, - self.view_left + 20, - SCREEN_HEIGHT - 40 + self.view_bottom, + self.cam.left + 20, + SCREEN_HEIGHT - 40 + self.cam.bottom, arcade.color.WHITE, 16) output = f"Processing time: {self.processing_time:.3f}" arcade.draw_text(output, - self.view_left + 20, - SCREEN_HEIGHT - 60 + self.view_bottom, + self.cam.left + 20, + SCREEN_HEIGHT - 60 + self.cam.bottom, arcade.color.WHITE, 16) self.draw_time = timeit.default_timer() - draw_start_time @@ -257,39 +254,44 @@ def on_update(self, delta_time): # --- Manage Scrolling --- - # Track if we need to change the viewport - + # Keep track of if we changed the boundary. We don't want to + # update the camera if we don't need to. changed = False # Scroll left - left_bndry = self.view_left + VIEWPORT_MARGIN - if self.player_sprite.left < left_bndry: - self.view_left -= left_bndry - self.player_sprite.left + left_boundary = self.cam.left + VIEWPORT_MARGIN + if self.player_sprite.left < left_boundary: + self.cam.left -= left_boundary - self.player_sprite.left changed = True # Scroll right - right_bndry = self.view_left + SCREEN_WIDTH - VIEWPORT_MARGIN - if self.player_sprite.right > right_bndry: - self.view_left += self.player_sprite.right - right_bndry + right_boundary = self.cam.right - VIEWPORT_MARGIN + if self.player_sprite.right > right_boundary: + self.cam.right += self.player_sprite.right - right_boundary changed = True # Scroll up - top_bndry = self.view_bottom + SCREEN_HEIGHT - VIEWPORT_MARGIN - if self.player_sprite.top > top_bndry: - self.view_bottom += self.player_sprite.top - top_bndry + top_boundary = self.cam.top - VIEWPORT_MARGIN + if self.player_sprite.top > top_boundary: + self.cam.top += self.player_sprite.top - top_boundary changed = True # Scroll down - bottom_bndry = self.view_bottom + VIEWPORT_MARGIN - if self.player_sprite.bottom < bottom_bndry: - self.view_bottom -= bottom_bndry - self.player_sprite.bottom + bottom_boundary = self.cam.bottom + VIEWPORT_MARGIN + if self.player_sprite.bottom < bottom_boundary: + self.cam.bottom -= bottom_boundary - self.player_sprite.bottom changed = True + # If we changed the boundary values, update the view port to match if changed: - arcade.set_viewport(self.view_left, - SCREEN_WIDTH + self.view_left, - self.view_bottom, - SCREEN_HEIGHT + self.view_bottom) + # Make sure our boundaries are integer values. While the view port does + # support floating point numbers, for this application we want every pixel + # in the view port to map directly onto a pixel on the screen. We don't want + # any rounding errors. + self.cam.left = int(self.cam.left) + self.cam.bottom = int(self.cam.bottom) + + self.cam.use() # Save the time it took to do this. self.processing_time = timeit.default_timer() - start_time diff --git a/arcade/examples/maze_recursive.py b/arcade/examples/maze_recursive.py index 4bdb65f33..3a9ee37b0 100644 --- a/arcade/examples/maze_recursive.py +++ b/arcade/examples/maze_recursive.py @@ -157,9 +157,8 @@ def __init__(self, width, height, title): # Physics engine self.physics_engine = None - # Used to scroll - self.view_bottom = 0 - self.view_left = 0 + # camera for scrolling + self.cam = None # Time to process self.processing_time = 0 @@ -238,10 +237,8 @@ def setup(self): # Set the background color self.background_color = arcade.color.AMAZON - # Set the viewport boundaries - # These numbers set where we have 'scrolled' to. - self.view_left = 0 - self.view_bottom = 0 + # setup camera + self.cam = arcade.camera.Camera2D() def on_draw(self): """ Render the screen. """ @@ -261,20 +258,20 @@ def on_draw(self): output = f"Sprite Count: {sprite_count}" arcade.draw_text(output, - self.view_left + 20, - SCREEN_HEIGHT - 20 + self.view_bottom, + self.cam.left + 20, + SCREEN_HEIGHT - 20 + self.cam.bottom, arcade.color.WHITE, 16) output = f"Drawing time: {self.draw_time:.3f}" arcade.draw_text(output, - self.view_left + 20, - SCREEN_HEIGHT - 40 + self.view_bottom, + self.cam.left + 20, + SCREEN_HEIGHT - 40 + self.cam.bottom, arcade.color.WHITE, 16) output = f"Processing time: {self.processing_time:.3f}" arcade.draw_text(output, - self.view_left + 20, - SCREEN_HEIGHT - 60 + self.view_bottom, + self.cam.left + 20, + SCREEN_HEIGHT - 60 + self.cam.bottom, arcade.color.WHITE, 16) self.draw_time = timeit.default_timer() - draw_start_time @@ -315,34 +312,31 @@ def on_update(self, delta_time): changed = False # Scroll left - left_bndry = self.view_left + VIEWPORT_MARGIN + left_bndry = self.cam.left + VIEWPORT_MARGIN if self.player_sprite.left < left_bndry: - self.view_left -= left_bndry - self.player_sprite.left + self.cam.left -= left_bndry - self.player_sprite.left changed = True # Scroll right - right_bndry = self.view_left + SCREEN_WIDTH - VIEWPORT_MARGIN + right_bndry = self.cam.right - VIEWPORT_MARGIN if self.player_sprite.right > right_bndry: - self.view_left += self.player_sprite.right - right_bndry + self.cam.left += self.player_sprite.right - right_bndry changed = True # Scroll up - top_bndry = self.view_bottom + SCREEN_HEIGHT - VIEWPORT_MARGIN + top_bndry = self.cam.top - VIEWPORT_MARGIN if self.player_sprite.top > top_bndry: - self.view_bottom += self.player_sprite.top - top_bndry + self.cam.bottom += self.player_sprite.top - top_bndry changed = True # Scroll down - bottom_bndry = self.view_bottom + VIEWPORT_MARGIN + bottom_bndry = self.cam.bottom + VIEWPORT_MARGIN if self.player_sprite.bottom < bottom_bndry: - self.view_bottom -= bottom_bndry - self.player_sprite.bottom + self.cam.bottom -= bottom_bndry - self.player_sprite.bottom changed = True if changed: - arcade.set_viewport(self.view_left, - SCREEN_WIDTH + self.view_left, - self.view_bottom, - SCREEN_HEIGHT + self.view_bottom) + self.cam.use() # Save the time it took to do this. self.processing_time = timeit.default_timer() - start_time diff --git a/arcade/examples/perspective.py b/arcade/examples/perspective.py index f994ad245..a6d1019ca 100644 --- a/arcade/examples/perspective.py +++ b/arcade/examples/perspective.py @@ -108,6 +108,12 @@ def __init__(self): ) self.time = 0 + self.offscreen_cam = arcade.camera.Camera2D( + position=(0.0, 0.0), + viewport=(0, 0, self.fbo.width, self.fbo.height), + projection=(0, self.fbo.width, 0, self.fbo.height) + ) + def on_draw(self): # Every frame we can update the offscreen texture if needed self.draw_offscreen() @@ -136,7 +142,7 @@ def draw_offscreen(self): # Activate the offscreen framebuffer and draw the sprites into it with self.fbo.activate() as fbo: fbo.clear() - arcade.set_viewport(0, self.fbo.width, 0, self.fbo.height) + self.offscreen_cam.use() self.spritelist.draw() def on_resize(self, width: int, height: int): diff --git a/arcade/examples/procedural_caves_bsp.py b/arcade/examples/procedural_caves_bsp.py index f9b40c793..8fcbc8d44 100644 --- a/arcade/examples/procedural_caves_bsp.py +++ b/arcade/examples/procedural_caves_bsp.py @@ -272,8 +272,7 @@ def __init__(self, width, height, title): self.wall_list = None self.player_list = None self.player_sprite = None - self.view_bottom = 0 - self.view_left = 0 + self.cam = None self.physics_engine = None self.processing_time = 0 @@ -350,6 +349,8 @@ def setup(self): self.physics_engine = arcade.PhysicsEngineSimple(self.player_sprite, self.wall_list) + self.cam = arcade.camera.Camera2D() + def on_draw(self): """ Render the screen. """ @@ -369,20 +370,20 @@ def on_draw(self): output = f"Sprite Count: {sprite_count}" arcade.draw_text(output, - self.view_left + 20, - WINDOW_HEIGHT - 20 + self.view_bottom, + self.cam.left + 20, + WINDOW_HEIGHT - 20 + self.cam.bottom, arcade.color.WHITE, 16) output = f"Drawing time: {self.draw_time:.3f}" arcade.draw_text(output, - self.view_left + 20, - WINDOW_HEIGHT - 40 + self.view_bottom, + self.cam.left + 20, + WINDOW_HEIGHT - 40 + self.cam.bottom, arcade.color.WHITE, 16) output = f"Processing time: {self.processing_time:.3f}" arcade.draw_text(output, - self.view_left + 20, - WINDOW_HEIGHT - 60 + self.view_bottom, + self.cam.left + 20, + WINDOW_HEIGHT - 60 + self.cam.bottom, arcade.color.WHITE, 16) self.draw_time = timeit.default_timer() - draw_start_time @@ -422,34 +423,31 @@ def on_update(self, delta_time): changed = False # Scroll left - left_bndry = self.view_left + VIEWPORT_MARGIN + left_bndry = self.cam.left + VIEWPORT_MARGIN if self.player_sprite.left < left_bndry: - self.view_left -= left_bndry - self.player_sprite.left + self.cam.left -= left_bndry - self.player_sprite.left changed = True # Scroll right - right_bndry = self.view_left + WINDOW_WIDTH - VIEWPORT_MARGIN + right_bndry = self.cam.right - VIEWPORT_MARGIN if self.player_sprite.right > right_bndry: - self.view_left += self.player_sprite.right - right_bndry + self.cam.left += self.player_sprite.right - right_bndry changed = True # Scroll up - top_bndry = self.view_bottom + WINDOW_HEIGHT - VIEWPORT_MARGIN + top_bndry = self.cam.top - VIEWPORT_MARGIN if self.player_sprite.top > top_bndry: - self.view_bottom += self.player_sprite.top - top_bndry + self.cam.bottom += self.player_sprite.top - top_bndry changed = True # Scroll down - bottom_bndry = self.view_bottom + VIEWPORT_MARGIN + bottom_bndry = self.cam.bottom + VIEWPORT_MARGIN if self.player_sprite.bottom < bottom_bndry: - self.view_bottom -= bottom_bndry - self.player_sprite.bottom + self.cam.bottom -= bottom_bndry - self.player_sprite.bottom changed = True if changed: - arcade.set_viewport(self.view_left, - WINDOW_WIDTH + self.view_left, - self.view_bottom, - WINDOW_HEIGHT + self.view_bottom) + self.cam.use() # Save the time it took to do this. self.processing_time = timeit.default_timer() - start_time diff --git a/arcade/examples/procedural_caves_cellular.py b/arcade/examples/procedural_caves_cellular.py index 274513e0c..534b475a6 100644 --- a/arcade/examples/procedural_caves_cellular.py +++ b/arcade/examples/procedural_caves_cellular.py @@ -114,7 +114,7 @@ def on_show_view(self): # Reset the viewport, necessary if we have a scrolling game and we need # to reset the viewport back to the start so we can see what we draw. - arcade.set_viewport(0, self.window.width, 0, self.window.height) + self.window.use_default_camera() def on_draw(self): """ Draw this view """ diff --git a/arcade/examples/sprite_tiled_map_with_levels.py b/arcade/examples/sprite_tiled_map_with_levels.py index d5ad29099..b477ce688 100644 --- a/arcade/examples/sprite_tiled_map_with_levels.py +++ b/arcade/examples/sprite_tiled_map_with_levels.py @@ -26,8 +26,8 @@ # and the edge of the screen. VIEWPORT_MARGIN_TOP = 60 VIEWPORT_MARGIN_BOTTOM = 60 -VIEWPORT_RIGHT_MARGIN = 270 -VIEWPORT_LEFT_MARGIN = 270 +VIEWPORT_MARGIN_RIGHT = 270 +VIEWPORT_MARGIN_LEFT = 270 # Physics MOVEMENT_SPEED = 5 @@ -55,8 +55,7 @@ def __init__(self): self.player_sprite = None self.physics_engine = None - self.view_left = 0 - self.view_bottom = 0 + self.cam = None self.end_of_map = 0 self.game_over = False self.last_time = None @@ -111,10 +110,8 @@ def load_level(self, level): if self.tile_map.background_color: self.background_color = self.tile_map.background_color - # Set the view port boundaries - # These numbers set where we have 'scrolled' to. - self.view_left = 0 - self.view_bottom = 0 + # Reset cam + self.cam = arcade.camera.Camera2D() def on_draw(self): """ @@ -137,8 +134,8 @@ def on_draw(self): if self.fps_message: arcade.draw_text( self.fps_message, - self.view_left + 10, - self.view_bottom + 40, + self.cam.left + 10, + self.cam.bottom + 40, arcade.color.BLACK, 14, ) @@ -152,14 +149,14 @@ def on_draw(self): distance = self.player_sprite.right output = f"Distance: {distance:.0f}" arcade.draw_text( - output, self.view_left + 10, self.view_bottom + 20, arcade.color.BLACK, 14 + output, self.cam.left + 10, self.cam.bottom + 20, arcade.color.BLACK, 14 ) if self.game_over: arcade.draw_text( "Game Over", - self.view_left + 200, - self.view_bottom + 200, + self.cam.left + 200, + self.cam.bottom + 200, arcade.color.BLACK, 30, ) @@ -204,44 +201,36 @@ def on_update(self, delta_time): # --- Manage Scrolling --- - # Track if we need to change the view port + # Track if we need to change the viewport changed = False # Scroll left - left_bndry = self.view_left + VIEWPORT_LEFT_MARGIN + left_bndry = self.cam.left + VIEWPORT_MARGIN_LEFT if self.player_sprite.left < left_bndry: - self.view_left -= left_bndry - self.player_sprite.left + self.cam.left -= left_bndry - self.player_sprite.left changed = True # Scroll right - right_bndry = self.view_left + SCREEN_WIDTH - VIEWPORT_RIGHT_MARGIN + right_bndry = self.cam.right - VIEWPORT_MARGIN_RIGHT if self.player_sprite.right > right_bndry: - self.view_left += self.player_sprite.right - right_bndry + self.cam.left += self.player_sprite.right - right_bndry changed = True # Scroll up - top_bndry = self.view_bottom + SCREEN_HEIGHT - VIEWPORT_MARGIN_TOP + top_bndry = self.cam.top - VIEWPORT_MARGIN_TOP if self.player_sprite.top > top_bndry: - self.view_bottom += self.player_sprite.top - top_bndry + self.cam.bottom += self.player_sprite.top - top_bndry changed = True # Scroll down - bottom_bndry = self.view_bottom + VIEWPORT_MARGIN_BOTTOM + bottom_bndry = self.cam.bottom + VIEWPORT_MARGIN_BOTTOM if self.player_sprite.bottom < bottom_bndry: - self.view_bottom -= bottom_bndry - self.player_sprite.bottom + self.cam.bottom -= bottom_bndry - self.player_sprite.bottom changed = True - # If we need to scroll, go ahead and do it. if changed: - self.view_left = int(self.view_left) - self.view_bottom = int(self.view_bottom) - arcade.set_viewport( - self.view_left, - SCREEN_WIDTH + self.view_left, - self.view_bottom, - SCREEN_HEIGHT + self.view_bottom, - ) + self.cam.use() def main(): diff --git a/arcade/experimental/bloom_multilayer_defender.py b/arcade/experimental/bloom_multilayer_defender.py index 8e4f2284e..5124eedc6 100644 --- a/arcade/experimental/bloom_multilayer_defender.py +++ b/arcade/experimental/bloom_multilayer_defender.py @@ -174,8 +174,8 @@ def __init__(self, width, height, title): self.up_pressed = False self.down_pressed = False - self.view_bottom = 0 - self.view_left = 0 + # Camera + self.cam = None # Set the background color self.background_color = arcade.color.BLACK @@ -243,6 +243,8 @@ def setup(self): sprite.center_y = random.randrange(600) self.enemy_sprite_list.append(sprite) + self.cam = arcade.camera.Camera2D() + def on_draw(self): """ Render the screen. """ # This command has to happen before we start drawing @@ -254,10 +256,7 @@ def on_draw(self): self.slight_bloom_screen.use() self.slight_bloom_screen.clear(arcade.color.TRANSPARENT_BLACK) - arcade.set_viewport(self.view_left, - SCREEN_WIDTH + self.view_left, - self.view_bottom, - SCREEN_HEIGHT + self.view_bottom) + self.cam.use() # Draw all the sprites on the screen that should have a 'slight' bloom self.star_sprite_list.draw() @@ -266,10 +265,7 @@ def on_draw(self): self.intense_bloom_screen.use() self.intense_bloom_screen.clear(arcade.color.TRANSPARENT_BLACK) - arcade.set_viewport(self.view_left, - SCREEN_WIDTH + self.view_left, - self.view_bottom, - SCREEN_HEIGHT + self.view_bottom) + self.cam.use() # Draw all the sprites on the screen that should have a 'intense' bloom self.bullet_sprite_list.draw() @@ -277,10 +273,7 @@ def on_draw(self): # Now draw to the actual screen self.use() - arcade.set_viewport(self.view_left, - SCREEN_WIDTH + self.view_left, - self.view_bottom, - SCREEN_HEIGHT + self.view_bottom) + self.cam.use() # --- Bloom related --- @@ -329,23 +322,23 @@ def on_update(self, delta_time): self.bullet_sprite_list.append(particle) # Scroll left - left_boundary = self.view_left + VIEWPORT_MARGIN + left_boundary = self.cam.left + VIEWPORT_MARGIN if self.player_sprite.left < left_boundary: - self.view_left -= left_boundary - self.player_sprite.left + self.cam.left -= left_boundary - self.player_sprite.left # Scroll right - right_boundary = self.view_left + SCREEN_WIDTH - VIEWPORT_MARGIN + right_boundary = self.cam.right - VIEWPORT_MARGIN if self.player_sprite.right > right_boundary: - self.view_left += self.player_sprite.right - right_boundary + self.cam.right += self.player_sprite.right - right_boundary # Scroll up - self.view_bottom = DEFAULT_BOTTOM_VIEWPORT - top_boundary = self.view_bottom + SCREEN_HEIGHT - TOP_VIEWPORT_MARGIN + self.cam.bottom = DEFAULT_BOTTOM_VIEWPORT + top_boundary = self.cam.top - TOP_VIEWPORT_MARGIN if self.player_sprite.top > top_boundary: - self.view_bottom += self.player_sprite.top - top_boundary + self.cam.top += self.player_sprite.top - top_boundary - self.view_left = int(self.view_left) - self.view_bottom = int(self.view_bottom) + self.cam.left = int(self.cam.left) + self.cam.bottom = int(self.cam.bottom) def on_key_press(self, key, modifiers): """Called whenever a key is pressed. """ diff --git a/arcade/experimental/light_demo.py b/arcade/experimental/light_demo.py index 38478d60d..b0cb690bb 100644 --- a/arcade/experimental/light_demo.py +++ b/arcade/experimental/light_demo.py @@ -76,7 +76,7 @@ def on_update(self, dt): self.moving_light.radius = 300 + math.sin(self.time * 2.34) * 150 def on_resize(self, width, height): - arcade.set_viewport(0, width, 0, height) + self.use_default_camera() self.light_layer.resize(width, height) diff --git a/arcade/experimental/light_demo_perf.py b/arcade/experimental/light_demo_perf.py index 82f691c22..36715b03d 100644 --- a/arcade/experimental/light_demo_perf.py +++ b/arcade/experimental/light_demo_perf.py @@ -61,7 +61,7 @@ def on_update(self, dt): print(e) def on_resize(self, width, height): - arcade.set_viewport(0, width, 0, height) + self.use_default_camera() self.light_layer.resize(width, height) diff --git a/arcade/experimental/shapes_perf.py b/arcade/experimental/shapes_perf.py index e4ba9be77..f29b35bc7 100644 --- a/arcade/experimental/shapes_perf.py +++ b/arcade/experimental/shapes_perf.py @@ -9,6 +9,7 @@ import arcade from arcade.types import NamedPoint +from pyglet.math import Mat4 SCREEN_WIDTH = 800 SCREEN_HEIGHT = 600 @@ -215,7 +216,7 @@ def on_update(self, dt): def on_resize(self, width: float, height: float): w, h = self.get_framebuffer_size() self.ctx.viewport = 0, 0, w, h - self.ctx.projection_2d = 0, 800, 0, 600 + self.ctx.projection_matrix = Mat4.orthogonal_projection(0, 800, 0, 600, -100, 100) if __name__ == '__main__': diff --git a/arcade/experimental/subpixel_experiment.py b/arcade/experimental/subpixel_experiment.py index 5e957d465..3c8a52a46 100644 --- a/arcade/experimental/subpixel_experiment.py +++ b/arcade/experimental/subpixel_experiment.py @@ -57,8 +57,7 @@ def on_update(self, dt): def on_resize(self, width, height): print("Resize", width, height) - arcade.set_viewport(0, SCREEN_WIDTH, 0, SCREEN_HEIGHT) - # arcade.set_viewport(100, 200, 100, 200) + self.use_default_camera() if __name__ == "__main__": diff --git a/arcade/gui/surface.py b/arcade/gui/surface.py index 1141a3fc0..e24f42a23 100644 --- a/arcade/gui/surface.py +++ b/arcade/gui/surface.py @@ -9,6 +9,7 @@ from arcade.gl import Framebuffer from arcade.gui.nine_patch import NinePatchTexture from arcade.types import RGBA255, FloatRect, Point +from pyglet.math import Mat4 class Surface: @@ -132,7 +133,7 @@ def activate(self): Also resets the limit of the surface (viewport). """ # Set viewport and projection - proj = self.ctx.projection_2d + proj = self.ctx.projection_matrix self.limit(0, 0, *self.size) # Set blend function blend_func = self.ctx.blend_func @@ -142,7 +143,7 @@ def activate(self): yield self # Restore projection and blend function - self.ctx.projection_2d = proj + self.ctx.projection_matrix = proj self.ctx.blend_func = blend_func def limit(self, x, y, width, height): @@ -156,7 +157,7 @@ def limit(self, x, y, width, height): width = max(width, 1) height = max(height, 1) - self.ctx.projection_2d = 0, width, 0, height + self.ctx.projection_matrix = Mat4.orthogonal_projection(0, width, 0, height, -100, 100) def draw( self, diff --git a/arcade/texture_atlas/base.py b/arcade/texture_atlas/base.py index f1a20c7b2..c111b17ad 100644 --- a/arcade/texture_atlas/base.py +++ b/arcade/texture_atlas/base.py @@ -308,6 +308,7 @@ def __init__( for tex in textures or []: self.add(tex) + @property def width(self) -> int: """ @@ -354,7 +355,8 @@ def max_size(self) -> Tuple[int, int]: The maximum size of the atlas in pixels (x, y) """ - return self._max_size + # TODO solve typing issue + return self._max_size # type: ignore @property def auto_resize(self) -> bool: @@ -880,7 +882,7 @@ def render_into( were `0, 0` is the lower left corner and `width, height` (of texture) is the upper right corner. - This method should should be used with the ``with`` statement:: + This method should be used with the ``with`` statement:: with atlas.render_into(texture): # Draw commands here @@ -894,22 +896,24 @@ def render_into( This parameter can be left blank if no projection changes are needed. The tuple values are: (left, right, button, top) """ + prev_projection = self._ctx.projection_matrix + region = self._texture_regions[texture.atlas_name] - proj_prev = self._ctx.projection_2d # Use provided projection or default projection = projection or (0, region.width, 0, region.height) - # Flip the top and bottom because we need to render things upside down - projection = projection[0], projection[1], projection[3], projection[2] - self._ctx.projection_2d = projection + + self._ctx.projection_matrix = Mat4.orthogonal_projection(projection[0], projection[1], + projection[3], projection[2], + -100, 100) with self._fbo.activate() as fbo: fbo.viewport = region.x, region.y, region.width, region.height try: yield fbo finally: - fbo.viewport = 0, 0, *self._fbo.size + fbo.viewport = 0, 0, *fbo.size - self._ctx.projection_2d = proj_prev + self._ctx.projection_matrix = prev_projection @classmethod def create_from_texture_sequence(cls, textures: Sequence["Texture"], border: int = 1) -> "TextureAtlas": diff --git a/arcade/window_commands.py b/arcade/window_commands.py index 381fdd500..90b8f04ef 100644 --- a/arcade/window_commands.py +++ b/arcade/window_commands.py @@ -31,7 +31,6 @@ "pause", "get_window", "set_window", - "set_viewport", "close_window", "run", "exit", @@ -99,65 +98,6 @@ def set_window(window: Optional["Window"]) -> None: _window = window -def set_viewport(left: float, right: float, bottom: float, top: float) -> None: - """ - This sets what coordinates the window will cover. - - .. tip:: Beginners will want to use :py:class:`~arcade.Camera`. - It provides easy to use support for common tasks - such as screen shake and movement to a destination. - - If you are making a game with complex control over the viewport, - this function can help. - - By default, the lower left coordinate will be ``(0, 0)``, the top y - coordinate will be the height of the window in pixels, and the right x - coordinate will be the width of the window in pixels. - - .. warning:: Be careful of fractional or non-multiple values! - - It is recommended to only set the viewport to integer values that - line up with the pixels on the screen. Otherwise, tiled pixel art - may not line up well during render, creating rectangle artifacts. - - .. note:: :py:meth:`Window.on_resize ` - calls ``set_viewport`` by default. If you want to set your - own custom viewport during the game, you may need to - override the - :py:meth:`Window.on_resize ` - method. - - .. note:: For more advanced users - - This functions sets the orthogonal projection - used by shapes and sprites. It also updates the viewport to match the current - screen resolution. - ``window.ctx.projection_2d`` (:py:meth:`~arcade.ArcadeContext.projection_2d`) - and ``window.ctx.viewport`` (:py:meth:`~arcade.gl.Context.viewport`) - can be used to set viewport and projection separately. - - :param left: Left-most (smallest) x value. - :param right: Right-most (largest) x value. - :param bottom: Bottom (smallest) y value. - :param top: Top (largest) y value. - """ - window = get_window() - # Get the active framebuffer - fbo = window.ctx.fbo - # If the framebuffer is the default one (aka. window framebuffer) - # we can't trust its size and need to get that from the window. - # This is because the default framebuffer is only introspected - # during context creation and it doesn't update size internally - # when the window is resizing. - if fbo.is_default: - fbo.viewport = 0, 0, window.width, window.height - # Otherwise it's an offscreen framebuffer and we can trust the size - else: - fbo.viewport = 0, 0, *fbo.size - - window.ctx.projection_2d = left, right, bottom, top - - def close_window() -> None: """ Closes the current window, and then runs garbage collection. The garbage collection diff --git a/doc/tutorials/lights/01_light_demo.py b/doc/tutorials/lights/01_light_demo.py index 0ea4ffd92..d4fc3009b 100644 --- a/doc/tutorials/lights/01_light_demo.py +++ b/doc/tutorials/lights/01_light_demo.py @@ -25,9 +25,8 @@ def __init__(self, width, height, title): # Physics engine self.physics_engine = None - # Used for scrolling - self.view_left = 0 - self.view_bottom = 0 + # camera for scrolling + self.cam = None def setup(self): """ Create everything """ @@ -52,10 +51,8 @@ def setup(self): # Create the physics engine self.physics_engine = arcade.PhysicsEngineSimple(self.player_sprite, self.wall_list) - # Set the viewport boundaries - # These numbers set where we have 'scrolled' to. - self.view_left = 0 - self.view_bottom = 0 + # setup camera + self.cam = arcade.camera.Camera2D() def on_draw(self): """ Draw everything. """ @@ -88,36 +85,33 @@ def scroll_screen(self): """ Manage Scrolling """ # Scroll left - left_boundary = self.view_left + VIEWPORT_MARGIN + left_boundary = self.cam.left + VIEWPORT_MARGIN if self.player_sprite.left < left_boundary: - self.view_left -= left_boundary - self.player_sprite.left + self.cam.left -= left_boundary - self.player_sprite.left # Scroll right - right_boundary = self.view_left + self.width - VIEWPORT_MARGIN + right_boundary = self.cam.right - VIEWPORT_MARGIN if self.player_sprite.right > right_boundary: - self.view_left += self.player_sprite.right - right_boundary + self.cam.right += self.player_sprite.right - right_boundary # Scroll up - top_boundary = self.view_bottom + self.height - VIEWPORT_MARGIN + top_boundary = self.cam.top - VIEWPORT_MARGIN if self.player_sprite.top > top_boundary: - self.view_bottom += self.player_sprite.top - top_boundary + self.cam.top += self.player_sprite.top - top_boundary # Scroll down - bottom_boundary = self.view_bottom + VIEWPORT_MARGIN + bottom_boundary = self.cam.bottom + VIEWPORT_MARGIN if self.player_sprite.bottom < bottom_boundary: - self.view_bottom -= bottom_boundary - self.player_sprite.bottom + self.cam.bottom -= bottom_boundary - self.player_sprite.bottom # Make sure our boundaries are integer values. While the viewport does # support floating point numbers, for this application we want every pixel # in the view port to map directly onto a pixel on the screen. We don't want # any rounding errors. - self.view_left = int(self.view_left) - self.view_bottom = int(self.view_bottom) + self.cam.left = int(self.cam.left) + self.cam.bottom = int(self.cam.bottom) - arcade.set_viewport(self.view_left, - self.width + self.view_left, - self.view_bottom, - self.height + self.view_bottom) + self.cam.use() def on_update(self, delta_time): """ Movement and game logic """ diff --git a/doc/tutorials/lights/light_demo.py b/doc/tutorials/lights/light_demo.py index 5efce83c9..a7116cda1 100644 --- a/doc/tutorials/lights/light_demo.py +++ b/doc/tutorials/lights/light_demo.py @@ -37,9 +37,8 @@ def __init__(self, width, height, title): # Physics engine self.physics_engine = None - # Used for scrolling - self.view_left = 0 - self.view_bottom = 0 + # camera for scrolling + self.cam = None # --- Light related --- # List of all the lights @@ -186,10 +185,8 @@ def setup(self): # Create the physics engine self.physics_engine = arcade.PhysicsEngineSimple(self.player_sprite, self.wall_list) - # Set the viewport boundaries - # These numbers set where we have 'scrolled' to. - self.view_left = 0 - self.view_bottom = 0 + # setup camera + self.cam = arcade.camera.Camera2D() def on_draw(self): """ Draw everything. """ @@ -210,7 +207,7 @@ def on_draw(self): # Now draw anything that should NOT be affected by lighting. arcade.draw_text("Press SPACE to turn character light on/off.", - 10 + self.view_left, 10 + self.view_bottom, + 10 + self.cam.left, 10 + self.cam.bottom, arcade.color.WHITE, 20) def on_resize(self, width, height): @@ -255,36 +252,33 @@ def scroll_screen(self): """ Manage Scrolling """ # Scroll left - left_boundary = self.view_left + VIEWPORT_MARGIN + left_boundary = self.cam.left + VIEWPORT_MARGIN if self.player_sprite.left < left_boundary: - self.view_left -= left_boundary - self.player_sprite.left + self.cam.left -= left_boundary - self.player_sprite.left # Scroll right - right_boundary = self.view_left + self.width - VIEWPORT_MARGIN + right_boundary = self.cam.right - VIEWPORT_MARGIN if self.player_sprite.right > right_boundary: - self.view_left += self.player_sprite.right - right_boundary + self.cam.right += self.player_sprite.right - right_boundary # Scroll up - top_boundary = self.view_bottom + self.height - VIEWPORT_MARGIN + top_boundary = self.cam.top - VIEWPORT_MARGIN if self.player_sprite.top > top_boundary: - self.view_bottom += self.player_sprite.top - top_boundary + self.cam.top += self.player_sprite.top - top_boundary # Scroll down - bottom_boundary = self.view_bottom + VIEWPORT_MARGIN + bottom_boundary = self.cam.bottom + VIEWPORT_MARGIN if self.player_sprite.bottom < bottom_boundary: - self.view_bottom -= bottom_boundary - self.player_sprite.bottom + self.cam.bottom -= bottom_boundary - self.player_sprite.bottom # Make sure our boundaries are integer values. While the viewport does # support floating point numbers, for this application we want every pixel # in the view port to map directly onto a pixel on the screen. We don't want # any rounding errors. - self.view_left = int(self.view_left) - self.view_bottom = int(self.view_bottom) + self.cam.left = int(self.cam.left) + self.cam.bottom = int(self.cam.bottom) - arcade.set_viewport(self.view_left, - self.width + self.view_left, - self.view_bottom, - self.height + self.view_bottom) + self.cam.use() def on_update(self, delta_time): """ Movement and game logic """ diff --git a/doc/tutorials/views/03_views.py b/doc/tutorials/views/03_views.py index 71a33b4e2..d6cdacd30 100644 --- a/doc/tutorials/views/03_views.py +++ b/doc/tutorials/views/03_views.py @@ -20,7 +20,7 @@ def on_show_view(self): # Reset the viewport, necessary if we have a scrolling game and we need # to reset the viewport back to the start so we can see what we draw. - arcade.set_viewport(0, self.window.width, 0, self.window.height) + self.window.use_default_camera() def on_draw(self): """ Draw this view """ diff --git a/doc/tutorials/views/04_views.py b/doc/tutorials/views/04_views.py index 48eb145fd..b67ba471e 100644 --- a/doc/tutorials/views/04_views.py +++ b/doc/tutorials/views/04_views.py @@ -20,7 +20,7 @@ def on_show_view(self): # Reset the viewport, necessary if we have a scrolling game and we need # to reset the viewport back to the start so we can see what we draw. - arcade.set_viewport(0, self.window.width, 0, self.window.height) + self.window.use_default_camera() def on_draw(self): """ Draw this view """ @@ -47,7 +47,7 @@ def __init__(self): # Reset the viewport, necessary if we have a scrolling game and we need # to reset the viewport back to the start so we can see what we draw. - arcade.set_viewport(0, SCREEN_WIDTH - 1, 0, SCREEN_HEIGHT - 1) + self.window.use_default_camera() def on_draw(self): """ Draw this view """ From 7383fff2317eb35904dd59aeafa8962126869c09 Mon Sep 17 00:00:00 2001 From: DragonMoffon Date: Wed, 20 Sep 2023 19:36:30 +1200 Subject: [PATCH 38/94] Removed references to projection_2d --- arcade/context.py | 2 +- arcade/experimental/actor_map.py | 7 ++--- arcade/experimental/geo_culling_check.py | 7 ++--- tests/unit/gl/test_opengl_context.py | 27 +++++-------------- tests/unit/gl/test_opengl_query.py | 1 - tests/unit/gui/test_ninepatch_draw.py | 3 ++- .../texture/test_texture_transform_render.py | 3 ++- 7 files changed, 19 insertions(+), 31 deletions(-) diff --git a/arcade/context.py b/arcade/context.py index 2128d4150..fb9c55a69 100644 --- a/arcade/context.py +++ b/arcade/context.py @@ -259,7 +259,7 @@ def default_atlas(self) -> TextureAtlas: def projection_matrix(self) -> Mat4: """ Get the current projection matrix. - This 4x4 float32 matrix is calculated when setting :py:attr:`~arcade.ArcadeContext.projection_2d`. + This 4x4 float32 matrix is calculated by cameras. This property simply gets and sets pyglet's projection matrix. diff --git a/arcade/experimental/actor_map.py b/arcade/experimental/actor_map.py index fec2dcd10..c91d8b05d 100644 --- a/arcade/experimental/actor_map.py +++ b/arcade/experimental/actor_map.py @@ -13,6 +13,7 @@ import arcade from arcade.gl import geometry from arcade import hitbox +from pyglet.math import Mat4 class ActorMap(arcade.Window): @@ -27,7 +28,7 @@ def __init__(self): def on_draw(self): self.clear() - self.ctx.projection_2d = 0, self.width, 0, self.height + self.ctx.projection_matrix = Mat4.orthogonal_projection(0, self.width, 0, self.height, -100, 100) self.actor.draw(self.time) def on_update(self, delta_time: float): @@ -104,7 +105,7 @@ def __init__(self, ctx, *, size: Tuple[int, int]): self.fbo = self.ctx.framebuffer( color_attachments=[ self.ctx.texture( - (self.size), + self.size, components=4, wrap_x=self.ctx.CLAMP_TO_EDGE, wrap_y=self.ctx.CLAMP_TO_EDGE, @@ -166,7 +167,7 @@ def draw(self): with self.fbo.activate() as fbo: fbo.clear() # Change projection to match the contents - self.ctx.projection_2d = 0, self.width, 0, self.height + self.ctx.projection = Mat4.orthogonal_projection(0, self.width, 0, self.height, -100, 100) self.sprites.draw() diff --git a/arcade/experimental/geo_culling_check.py b/arcade/experimental/geo_culling_check.py index b7d9fabc9..6d500c384 100644 --- a/arcade/experimental/geo_culling_check.py +++ b/arcade/experimental/geo_culling_check.py @@ -9,6 +9,7 @@ from __future__ import annotations from arcade.sprite import Sprite +from pyglet.math import Mat4 import PIL import arcade @@ -17,7 +18,7 @@ class GeoCullingTest(arcade.Window): def __init__(self): super().__init__(800, 400, "Cull test", resizable=True) - self.proj = self.ctx.projection_2d + self.proj = 0, self.width, 0, self.height self.texture = arcade.Texture( PIL.Image.new("RGBA", (2048, 2), (255, 255, 255, 255)), hash="weird_texture", @@ -37,12 +38,12 @@ def __init__(self): def on_draw(self): self.clear() - self.ctx.projection_2d = self.proj + self.ctx.projection_matrix = Mat4.orthogonal_projection(*self.proj, -100, 100) self.spritelist.draw() def on_resize(self, width, height): super().on_resize(width, height) - self.proj = self.ctx.projection_2d + self.proj = 0, width, 0, height def on_mouse_drag(self, x: float, y: float, dx: float, dy: float, buttons: int, modifiers: int): self.proj = ( diff --git a/tests/unit/gl/test_opengl_context.py b/tests/unit/gl/test_opengl_context.py index 92069846a..ae87af0ba 100644 --- a/tests/unit/gl/test_opengl_context.py +++ b/tests/unit/gl/test_opengl_context.py @@ -27,34 +27,19 @@ def test_viewport(ctx): assert ctx.viewport == vp -def test_projection(window): - ctx = window.ctx - assert ctx.projection_2d == (0, window.width, 0, window.height) - ctx.projection_2d = (1, 10, 2, 11) - assert ctx.projection_2d == (1, 10, 2, 11) +def test_view_matrix(window): + """Test setting the view matrix directly""" + window.ctx.view_matrix = Mat4() - # Attempt to assign illegal values with pytest.raises(ValueError): - ctx.projection_2d = "moo" - - with pytest.raises(ValueError): - ctx.projection_2d = 1, 2, 3, 4, 5 - - # Set matrices directly checking projection - # parameter reconstruction - ctx.projection_2d_matrix = Mat4.orthogonal_projection(0, 100, 0, 200, -100, 100) - assert ctx.projection_2d == (0, 100, 0, 200) - ctx.projection_2d_matrix = Mat4.orthogonal_projection(100, 200, 200, 400, -100, 100) - assert ctx.projection_2d == (100, 200, 200, 400) - ctx.projection_2d_matrix = Mat4.orthogonal_projection(200, 800, 300, 900, -100, 100) - assert ctx.projection_2d == (200, 800, 300, 900) + window.ctx.view_matrix = "moo" def test_projection_matrix(window): """Test setting projection matrix directly""" - window.ctx.projection_2d_matrix = Mat4() + window.ctx.projection_matrix = Mat4() with pytest.raises(ValueError): - window.ctx.projection_2d_matrix = "moo" + window.ctx.projection_matrix = "moo" def test_point_size(ctx): diff --git a/tests/unit/gl/test_opengl_query.py b/tests/unit/gl/test_opengl_query.py index bd225c8ef..e024b266c 100644 --- a/tests/unit/gl/test_opengl_query.py +++ b/tests/unit/gl/test_opengl_query.py @@ -6,7 +6,6 @@ def test_create(window: arcade.Window): ctx = window.ctx SCREEN_WIDTH, SCREEN_HEIGHT = window.get_size() - # print(ctx.viewport, ctx.projection_2d) program = ctx.program( vertex_shader=""" diff --git a/tests/unit/gui/test_ninepatch_draw.py b/tests/unit/gui/test_ninepatch_draw.py index 58656bca1..e2db8e0b0 100644 --- a/tests/unit/gui/test_ninepatch_draw.py +++ b/tests/unit/gui/test_ninepatch_draw.py @@ -2,6 +2,7 @@ from typing import Tuple import pytest import arcade +from pyglet.math import Mat4 from arcade.gui import NinePatchTexture from PIL import Image, ImageDraw @@ -67,7 +68,7 @@ def test_draw(ctx, fbo, left, right, bottom, top): ) with fbo.activate(): fbo.clear() - ctx.projection_2d = (0, PATCH_SIZE[0], 0, PATCH_SIZE[1]) + ctx.projection_matrix = Mat4.orthogonal_projection(0, PATCH_SIZE[0], 0, PATCH_SIZE[1], -100, 100) patch.draw_sized( size=PATCH_SIZE, position=(0, 0), diff --git a/tests/unit/texture/test_texture_transform_render.py b/tests/unit/texture/test_texture_transform_render.py index 1a72ccc3f..0de33d663 100644 --- a/tests/unit/texture/test_texture_transform_render.py +++ b/tests/unit/texture/test_texture_transform_render.py @@ -3,6 +3,7 @@ """ import arcade import pytest +from pyglet.math import Mat4 from PIL import Image, ImageDraw from arcade.texture.transforms import ( Transform, @@ -47,7 +48,7 @@ def test_rotate90_transform(ctx: arcade.ArcadeContext, image, transform, pil_tra sprite = arcade.Sprite(texture, center_x=image.width // 2, center_y=image.height // 2) with fbo.activate(): fbo.clear() - ctx.projection_2d = (0, image.width, 0, image.height) + ctx.projection_matrix = Mat4.orthogonal_projection(0, image.width, 0, image.height, -100, 100) sprite.draw(pixelated=True) expected_image = image.transpose(pil_transform) From 62db585f97cf6817bdfc9a9ce8f31d1e6dbf9ef2 Mon Sep 17 00:00:00 2001 From: DragonMoffon Date: Tue, 26 Sep 2023 00:37:35 +1300 Subject: [PATCH 39/94] Fixing unit tests --- arcade/application.py | 8 +- arcade/camera/controllers/__init__.py | 12 ++- .../simple_controller_functions.py | 99 ++++++++++++++++--- arcade/camera/default.py | 2 + arcade/camera/simple_camera.py | 4 + arcade/examples/procedural_caves_cellular.py | 2 +- arcade/experimental/light_demo.py | 2 +- arcade/experimental/light_demo_perf.py | 2 +- arcade/experimental/subpixel_experiment.py | 2 +- doc/tutorials/views/03_views.py | 2 +- doc/tutorials/views/04_views.py | 4 +- tests/unit/camera/test_orthographic_camera.py | 16 +-- tests/unit/window/test_window.py | 6 +- 13 files changed, 123 insertions(+), 38 deletions(-) diff --git a/arcade/application.py b/arcade/application.py index 14750af10..2da1f5dcd 100644 --- a/arcade/application.py +++ b/arcade/application.py @@ -595,7 +595,7 @@ def on_resize(self, width: int, height: int): if hasattr(self, "_ctx"): # Retain projection scrolling if applied self._ctx.viewport = (0, 0, width, height) - self.use_default_camera() + self.default_camera.use() def set_min_size(self, width: int, height: int): """ Wrap the Pyglet window call to set minimum size @@ -672,12 +672,6 @@ def default_camera(self): """ return self._default_camera - def use_default_camera(self): - """ - Uses the default arcade camera. Good for quickly resetting the screen - """ - self._default_camera.use() - def test(self, frames: int = 10): """ Used by unit test cases. Runs the event loop a few times and stops. diff --git a/arcade/camera/controllers/__init__.py b/arcade/camera/controllers/__init__.py index f37a45184..d81f717d2 100644 --- a/arcade/camera/controllers/__init__.py +++ b/arcade/camera/controllers/__init__.py @@ -2,7 +2,11 @@ simple_follow_3D, simple_follow_2D, simple_easing_3D, - simple_easing_2D + simple_easing_2D, + quaternion_rotation, + rotate_around_up, + rotate_around_right, + rotate_around_forward ) @@ -10,5 +14,9 @@ 'simple_follow_3D', 'simple_follow_2D', 'simple_easing_3D', - 'simple_easing_2D' + 'simple_easing_2D', + 'quaternion_rotation', + 'rotate_around_up', + 'rotate_around_right', + 'rotate_around_forward' ] diff --git a/arcade/camera/controllers/simple_controller_functions.py b/arcade/camera/controllers/simple_controller_functions.py index fe8fcedf7..67e8977d9 100644 --- a/arcade/camera/controllers/simple_controller_functions.py +++ b/arcade/camera/controllers/simple_controller_functions.py @@ -17,19 +17,61 @@ ] -def quaternion_rotation(_axis: Tuple[float, float, float], - _vector: Tuple[float, float, float], - _angle: float) -> Tuple[float, float, float]: - # Ref: https://danceswithcode.net/engineeringnotes/quaternions/quaternions.html - _rotation_rads = -radians(_angle) - p1, p2, p3 = _vector +def quaternion_rotation(axis: Tuple[float, float, float], + vector: Tuple[float, float, float], + angle: float) -> Tuple[float, float, float]: + """ + Rotate a 3-dimensional vector of any length clockwise around a 3-dimensional unit length vector. + + This method of vector rotation is immune to rotation-lock, however it takes a little more effort + to find the axis of rotation rather than 3 angles of rotation. + Ref: https://danceswithcode.net/engineeringnotes/quaternions/quaternions.html. + + Example: + import arcade + from arcade.camera.controllers import quaternion_rotation + + + # Rotating a sprite around a point + sprite = arcade.Sprite(center_x=0.0, center_y=10.0) + rotation_point = (0.0, 0.0) + + # Find the relative vector between the sprite and point to rotate. (Must be a 3D vector) + relative_position = sprite.center_x - rotation_point[0], sprite.center_y - rotation_point[1], 0.0 + + # Because arcade uses the X and Y axis for 2D co-ordinates the Z-axis becomes the rotation axis. + rotation_axis = (0.0, 0.0, 1.0) + + # Rotate the vector 45 degrees clockwise. + new_relative_position = quaternion_rotation(rotation_axis, relative_position, 45) + + + sprite.position = ( + rotation_point[0] + new_relative_position[0], + rotation_point[1] + new_relative_position[1] + ) + + Args: + axis: + The unit length vector that will be rotated around + vector: + The 3-dimensional vector to be rotated + angle: + The angle in degrees to rotate the vector clock-wise by + + Returns: + A rotated 3-dimension vector with the same length as the argument vector. + """ + + _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] + _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 @@ -42,14 +84,49 @@ def quaternion_rotation(_axis: Tuple[float, float, float], def rotate_around_forward(data: CameraData, angle: float): + """ + Rotate the CameraData up vector around the CameraData forward vector, perfect for rotating the screen. + This rotation will be around (0.0, 0.0) of the camera projection. + If that is not the center of the screen this method may appear erroneous. + Uses arcade.camera.controllers.quaternion_rotation internally. + + Args: + data: + The camera data to modify. The data's up vector is rotated around its forward vector + angle: + The angle in degrees to rotate clockwise by + """ data.up = quaternion_rotation(data.forward, data.up, angle) def rotate_around_up(data: CameraData, angle: float): + """ + Rotate the CameraData forward vector around the CameraData up vector. + Generally only useful in 3D games. + Uses arcade.camera.controllers.quaternion_rotation internally. + + Args: + data: + The camera data to modify. The data's forward vector is rotated around its up vector + angle: + The angle in degrees to rotate clockwise by + """ data.forward = quaternion_rotation(data.up, data.forward, angle) def rotate_around_right(data: CameraData, angle: float): + """ + Rotate both the CameraData's forward vector and up vector around a calculated right vector. + Generally only useful in 3D games. + Uses arcade.camera.controllers.quaternion_rotation internally. + + Args: + data: + The camera data to modify. The data's forward vector is rotated around its up vector + angle: + The angle in degrees to rotate clockwise by + """ + _forward = Vec3(data.forward[0], data.forward[1], data.forward[2]) _up = Vec3(data.up[0], data.up[1], data.up[2]) _crossed_vec = _forward.cross(_up) @@ -87,7 +164,7 @@ def simple_follow_2D(speed: float, target: Tuple[float, float], data: CameraData :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_3D(speed, target + (0,), data) + simple_follow_3D(speed, (target[0], target[1], 0.0), data) def simple_easing_3D(percent: float, @@ -129,4 +206,4 @@ def simple_easing_2D(percent: float, speed does not stay constant. See arcade.easing for examples. """ - simple_easing_3D(percent, start + (0,), target + (0,), data, func) + simple_easing_3D(percent, (start[0], start[1], 0.0), (target[0], target[1], 0.0), data, func) diff --git a/arcade/camera/default.py b/arcade/camera/default.py index 4f81efc42..d831a4a88 100644 --- a/arcade/camera/default.py +++ b/arcade/camera/default.py @@ -37,6 +37,8 @@ def viewport(self, viewport: Tuple[int, int, int, int]): -100, 100) def use(self): + self._window.current_camera = self + self._window.ctx.viewport = self._viewport self._window.ctx.view_matrix = Mat4() diff --git a/arcade/camera/simple_camera.py b/arcade/camera/simple_camera.py index 34760b01b..133d4520d 100644 --- a/arcade/camera/simple_camera.py +++ b/arcade/camera/simple_camera.py @@ -1,4 +1,5 @@ from typing import TYPE_CHECKING, Optional, Tuple, Iterator +from warnings import warn from contextlib import contextmanager from math import atan2, cos, sin, degrees, radians @@ -39,6 +40,9 @@ def __init__(self, *, camera_data: Optional[CameraData] = None, projection_data: Optional[OrthographicProjectionData] = None ): + warn("arcade.camera.SimpleCamera has been depreciated please use arcade.camera.Camera2D instead", + DeprecationWarning) + self._window = window or get_window() if any((viewport, projection, position, up, zoom, near, far)) and any((camera_data, projection_data)): diff --git a/arcade/examples/procedural_caves_cellular.py b/arcade/examples/procedural_caves_cellular.py index 88bd8b666..fecc47b03 100644 --- a/arcade/examples/procedural_caves_cellular.py +++ b/arcade/examples/procedural_caves_cellular.py @@ -112,7 +112,7 @@ def on_show_view(self): # Reset the viewport, necessary if we have a scrolling game and we need # to reset the viewport back to the start so we can see what we draw. - self.window.use_default_camera() + self.window.default_camera.use()() def on_draw(self): """ Draw this view """ diff --git a/arcade/experimental/light_demo.py b/arcade/experimental/light_demo.py index b0cb690bb..2a0e7c419 100644 --- a/arcade/experimental/light_demo.py +++ b/arcade/experimental/light_demo.py @@ -76,7 +76,7 @@ def on_update(self, dt): self.moving_light.radius = 300 + math.sin(self.time * 2.34) * 150 def on_resize(self, width, height): - self.use_default_camera() + self.default_camera.use()() self.light_layer.resize(width, height) diff --git a/arcade/experimental/light_demo_perf.py b/arcade/experimental/light_demo_perf.py index 36715b03d..a7d24e343 100644 --- a/arcade/experimental/light_demo_perf.py +++ b/arcade/experimental/light_demo_perf.py @@ -61,7 +61,7 @@ def on_update(self, dt): print(e) def on_resize(self, width, height): - self.use_default_camera() + self.default_camera.use()() self.light_layer.resize(width, height) diff --git a/arcade/experimental/subpixel_experiment.py b/arcade/experimental/subpixel_experiment.py index 3c8a52a46..025b1dcd1 100644 --- a/arcade/experimental/subpixel_experiment.py +++ b/arcade/experimental/subpixel_experiment.py @@ -57,7 +57,7 @@ def on_update(self, dt): def on_resize(self, width, height): print("Resize", width, height) - self.use_default_camera() + self.default_camera.use()() if __name__ == "__main__": diff --git a/doc/tutorials/views/03_views.py b/doc/tutorials/views/03_views.py index d6cdacd30..4885a617d 100644 --- a/doc/tutorials/views/03_views.py +++ b/doc/tutorials/views/03_views.py @@ -20,7 +20,7 @@ def on_show_view(self): # Reset the viewport, necessary if we have a scrolling game and we need # to reset the viewport back to the start so we can see what we draw. - self.window.use_default_camera() + self.window.default_camera.use()() def on_draw(self): """ Draw this view """ diff --git a/doc/tutorials/views/04_views.py b/doc/tutorials/views/04_views.py index b67ba471e..f404cbfc0 100644 --- a/doc/tutorials/views/04_views.py +++ b/doc/tutorials/views/04_views.py @@ -20,7 +20,7 @@ def on_show_view(self): # Reset the viewport, necessary if we have a scrolling game and we need # to reset the viewport back to the start so we can see what we draw. - self.window.use_default_camera() + self.window.default_camera.use()() def on_draw(self): """ Draw this view """ @@ -47,7 +47,7 @@ def __init__(self): # Reset the viewport, necessary if we have a scrolling game and we need # to reset the viewport back to the start so we can see what we draw. - self.window.use_default_camera() + self.window.default_camera.use()() def on_draw(self): """ Draw this view """ diff --git a/tests/unit/camera/test_orthographic_camera.py b/tests/unit/camera/test_orthographic_camera.py index eced85d79..df0d427bd 100644 --- a/tests/unit/camera/test_orthographic_camera.py +++ b/tests/unit/camera/test_orthographic_camera.py @@ -4,7 +4,7 @@ def test_orthographic_camera(window: Window): - default_camera = window.current_camera + default_camera = window.default_camera cam_default = camera.OrthographicProjector() default_view = cam_default.view_data @@ -15,7 +15,7 @@ def test_orthographic_camera(window: Window): (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 + (0.0, 0.0, -1.0), # Forward 1.0, # Zoom ) assert default_projection == camera.OrthographicProjectionData( @@ -28,18 +28,18 @@ def test_orthographic_camera(window: Window): 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 - 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 + # 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 + set_view = camera.CameraData( (0, 0, 1, 1), # Viewport (0.0, 0.0, 0.0), # Position diff --git a/tests/unit/window/test_window.py b/tests/unit/window/test_window.py index 2ccc15851..977852ffe 100644 --- a/tests/unit/window/test_window.py +++ b/tests/unit/window/test_window.py @@ -36,10 +36,10 @@ def test_window(window: arcade.Window): w.set_mouse_visible(True) w.set_size(width, height) - v = window.get_viewport() + v = window.ctx.viewport assert v[0] == 0 - assert v[1] == width - assert v[2] == 0 + assert v[1] == 0 + assert v[2] == width assert v[3] == height factor = window.get_pixel_ratio() From 0a5f5f355316ad93bfd8388c3abcfb73e6b6c0c3 Mon Sep 17 00:00:00 2001 From: DragonMoffon Date: Tue, 26 Sep 2023 02:33:57 +1300 Subject: [PATCH 40/94] Solved pytest issues in GUI due to Camera Code --- arcade/camera/camera_2d.py | 18 ++++++- arcade/camera/data.py | 4 +- arcade/camera/simple_camera.py | 2 +- arcade/gui/surface.py | 3 ++ arcade/gui/ui_manager.py | 26 ++++----- tests/unit/camera/test_orthographic_camera.py | 53 ++++++++++++++++--- tests/unit/gui/test_uimanager_camera.py | 16 +++--- 7 files changed, 90 insertions(+), 32 deletions(-) diff --git a/arcade/camera/camera_2d.py b/arcade/camera/camera_2d.py index f29bc9c65..a27936395 100644 --- a/arcade/camera/camera_2d.py +++ b/arcade/camera/camera_2d.py @@ -147,7 +147,23 @@ 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:] + self._data.position = (_pos[0], _pos[1], self._data.position[2]) + + @property + def position(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] + + @position.setter + def position(self, _pos: Tuple[float, float]) -> None: + """ + Set the X and Y position of the camera. + """ + self._data.position = (_pos[0], _pos[1], self._data.position[2]) @property def left(self) -> float: diff --git a/arcade/camera/data.py b/arcade/camera/data.py index dfaac3f4f..5e2c786ef 100644 --- a/arcade/camera/data.py +++ b/arcade/camera/data.py @@ -28,8 +28,8 @@ class CameraData: # View matrix data 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) + up: Tuple[float, float, float] = (0.0, 1.0, 0.0) + forward: Tuple[float, float, float] = (0.0, 0.0, -1.0) # Zoom zoom: float = 1.0 diff --git a/arcade/camera/simple_camera.py b/arcade/camera/simple_camera.py index 133d4520d..c5b8c2028 100644 --- a/arcade/camera/simple_camera.py +++ b/arcade/camera/simple_camera.py @@ -71,7 +71,7 @@ def __init__(self, *, else: 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.0, 0.0, 0.0), # Position (0, 1.0, 0.0), # Up (0.0, 0.0, -1.0), # Forward 1.0 # Zoom diff --git a/arcade/gui/surface.py b/arcade/gui/surface.py index e24f42a23..23d32b470 100644 --- a/arcade/gui/surface.py +++ b/arcade/gui/surface.py @@ -134,6 +134,7 @@ def activate(self): """ # Set viewport and projection proj = self.ctx.projection_matrix + view_port = self.ctx.viewport self.limit(0, 0, *self.size) # Set blend function blend_func = self.ctx.blend_func @@ -144,6 +145,7 @@ def activate(self): # Restore projection and blend function self.ctx.projection_matrix = proj + self.ctx.viewport = view_port self.ctx.blend_func = blend_func def limit(self, x, y, width, height): @@ -158,6 +160,7 @@ def limit(self, x, y, width, height): width = max(width, 1) height = max(height, 1) self.ctx.projection_matrix = Mat4.orthogonal_projection(0, width, 0, height, -100, 100) + self.ctx.viewport = (0, 0, int(width), int(height)) def draw( self, diff --git a/arcade/gui/ui_manager.py b/arcade/gui/ui_manager.py index f3cb550a3..84800c889 100644 --- a/arcade/gui/ui_manager.py +++ b/arcade/gui/ui_manager.py @@ -32,7 +32,7 @@ ) from arcade.gui.surface import Surface from arcade.gui.widgets import UIWidget, Rect -from arcade.camera import OrthographicProjector, OrthographicProjectionData +from arcade.camera import OrthographicProjector, OrthographicProjectionData, CameraData W = TypeVar("W", bound=UIWidget) @@ -93,6 +93,7 @@ def __init__(self, window: Optional[arcade.Window] = None): self._rendered = False #: Camera used when drawing the UI self.projector = OrthographicProjector( + view=CameraData((0, 0, self.window.width, self.window.height)), projection=OrthographicProjectionData(0, self.window.width, 0, self.window.height, -100, 100) ) self.register_event_type("on_event") @@ -302,27 +303,22 @@ def draw(self) -> None: self._do_render() # Draw layers - self.projector.use() - with ctx.enabled(ctx.BLEND): - layers = sorted(self.children.keys()) - for layer in layers: - self._get_surface(layer).draw() + with self.projector.activate() as cam: + with ctx.enabled(ctx.BLEND): + layers = sorted(self.children.keys()) + for layer in layers: + self._get_surface(layer).draw() def adjust_mouse_coordinates(self, x, y): """ This method is used, to translate mouse coordinates to coordinates respecting the viewport and projection of cameras. - The implementation should work in most common cases. - - If you use scrolling in the :py:class:`arcade.Camera` you have to reset scrolling - or overwrite this method using the camera conversion:: - ui_manager.adjust_mouse_coordinates = camera.mouse_coordinates_to_world + It uses the internal camera's map_coordinate methods, and should work with + all transformations possible with the basic orthographic camera. """ - # NOTE: Only support scrolling until cameras support transforming - # mouse coordinates - px, py = self.projector.view_data.position[:2] - return x + px, y + py + + return self.window.current_camera.map_coordinate((x, y)) def on_event(self, event) -> Union[bool, None]: layers = sorted(self.children.keys(), reverse=True) diff --git a/tests/unit/camera/test_orthographic_camera.py b/tests/unit/camera/test_orthographic_camera.py index df0d427bd..a69684cef 100644 --- a/tests/unit/camera/test_orthographic_camera.py +++ b/tests/unit/camera/test_orthographic_camera.py @@ -78,13 +78,54 @@ def test_orthographic_camera(window: Window): assert window.current_camera == default_camera -def test_orthographic_projection_matrix(): - pass +def test_orthographic_projection_matrix(window: Window): + cam_default = camera.OrthographicProjector() + default_view = cam_default.view_data + default_projection = cam_default.projection_data -def test_orthographic_view_matrix(): - pass +def test_orthographic_view_matrix(window: Window): + cam_default = camera.OrthographicProjector() + default_view = cam_default.view_data + default_projection = cam_default.projection_data + +def test_orthographic_map_coordinates(window: Window): + cam_default = camera.OrthographicProjector() + default_view = cam_default.view_data + default_projection = cam_default.projection_data -def test_orthographic_map_coordinates(): - pass + # Test that the camera maps coordinates properly when no values have been adjusted + assert cam_default.map_coordinate((100.0, 100.0)) == (pytest.approx(100.0), pytest.approx(100.0)) + + # Test that the camera maps coordinates properly when 0.0, 0.0 is in the center of the screen + default_view.position = (0.0, 0.0, 0.0) + assert (cam_default.map_coordinate((window.width//2, window.height//2)) == (0.0, 0.0)) + + # Test that the camera maps coordinates properly when the position has changed. + default_view.position = (100.0, 100.0, 0.0) + assert (cam_default.map_coordinate((100.0, 100.0)) == + (pytest.approx(200.0 - window.width//2), pytest.approx(200.0 - window.height//2))) + + # Test that the camera maps coordinates properly when the rotation has changed. + default_view.position = (0.0, 0.0, 0.0) + default_view.up = (1.0, 0.0, 0.0) + assert (cam_default.map_coordinate((window.width//2, window.height//2 + 100.0)) == + (pytest.approx(100.0), pytest.approx(00.0))) + + # Test that the camera maps coordinates properly when the rotation and position has changed. + default_view.position = (100.0, 100.0, 0.0) + default_view.up = (0.7071067812, 0.7071067812, 0.0) + assert (cam_default.map_coordinate((window.width//2, window.height//2)) == + (pytest.approx(100.0), pytest.approx(100.0))) + + # Test that the camera maps coordinates properly when zoomed in. + default_view.position = (0.0, 0.0, 0.0) + default_view.up = (0.0, 1.0, 0.0) + default_view.zoom = 2.0 + assert (cam_default.map_coordinate((window.width, window.height)) == + (pytest.approx(window.width//4), pytest.approx(window.height//4))) + + # Test that the camera maps coordinates properly when the viewport is not the default + default_view.zoom = 1.0 + default_view.viewport = window.width//2, window.height//2, window.width//2, window.height//2 diff --git a/tests/unit/gui/test_uimanager_camera.py b/tests/unit/gui/test_uimanager_camera.py index d2ecb8b65..c073100cb 100644 --- a/tests/unit/gui/test_uimanager_camera.py +++ b/tests/unit/gui/test_uimanager_camera.py @@ -9,31 +9,33 @@ def test_ui_manager_respects_camera_viewport(uimanager, window): # GIVEN uimanager.use_super_mouse_adjustment = True - camera = arcade.Camera(viewport=(0, 0, window.width, window.height), window=window) + camera = arcade.camera.Camera2D(position=(0.0, 0.0), projection=(0.0, window.width, 0.0, window.height), + window=window) # WHEN - camera.viewport = 0, 0, 300, 200 + camera.viewport = 0, 0, 400, 200 camera.use() uimanager.click(100, 100) # THEN assert isinstance(uimanager.last_event, UIMouseReleaseEvent) - assert uimanager.last_event.pos == (200, 200) + assert uimanager.last_event.pos == (pytest.approx(200), pytest.approx(300)) + @pytest.mark.xfail def test_ui_manager_respects_camera_pos(uimanager, window): # GIVEN uimanager.use_super_mouse_adjustment = True - camera = arcade.Camera(viewport=(0, 0, window.width, window.height), window=window) + camera = arcade.camera.Camera2D(position=(0.0, 0.0), projection=(0.0, window.width, 0.0, window.height), + window=window) # WHEN - camera.position = Vec2(-100, -100) - camera.update() + camera.position = (100, 100) camera.use() uimanager.click(100, 100) # THEN assert isinstance(uimanager.last_event, UIMouseReleaseEvent) - assert uimanager.last_event.pos == (200, 200) + assert uimanager.last_event.pos == (pytest.approx(200), pytest.approx(200)) From 049043effa9e80e19b910cb8e705ac05676aaea2 Mon Sep 17 00:00:00 2001 From: DragonMoffon Date: Tue, 26 Sep 2023 02:35:06 +1300 Subject: [PATCH 41/94] Fixed small linting issue --- arcade/gui/ui_manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/arcade/gui/ui_manager.py b/arcade/gui/ui_manager.py index 84800c889..f41d9e49e 100644 --- a/arcade/gui/ui_manager.py +++ b/arcade/gui/ui_manager.py @@ -303,7 +303,7 @@ def draw(self) -> None: self._do_render() # Draw layers - with self.projector.activate() as cam: + with self.projector.activate(): with ctx.enabled(ctx.BLEND): layers = sorted(self.children.keys()) for layer in layers: From 310b154ee8a995d6da2c764a0ed436378f3805c2 Mon Sep 17 00:00:00 2001 From: DragonMoffon Date: Tue, 26 Sep 2023 02:43:48 +1300 Subject: [PATCH 42/94] Fixed double brackets caused by find and replace --- arcade/examples/procedural_caves_cellular.py | 2 +- arcade/experimental/light_demo.py | 2 +- arcade/experimental/light_demo_perf.py | 2 +- arcade/experimental/subpixel_experiment.py | 2 +- doc/tutorials/views/03_views.py | 2 +- doc/tutorials/views/04_views.py | 4 ++-- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/arcade/examples/procedural_caves_cellular.py b/arcade/examples/procedural_caves_cellular.py index fecc47b03..090d689c2 100644 --- a/arcade/examples/procedural_caves_cellular.py +++ b/arcade/examples/procedural_caves_cellular.py @@ -112,7 +112,7 @@ def on_show_view(self): # Reset the viewport, necessary if we have a scrolling game and we need # to reset the viewport back to the start so we can see what we draw. - self.window.default_camera.use()() + self.window.default_camera.use() def on_draw(self): """ Draw this view """ diff --git a/arcade/experimental/light_demo.py b/arcade/experimental/light_demo.py index 2a0e7c419..d580d6e70 100644 --- a/arcade/experimental/light_demo.py +++ b/arcade/experimental/light_demo.py @@ -76,7 +76,7 @@ def on_update(self, dt): self.moving_light.radius = 300 + math.sin(self.time * 2.34) * 150 def on_resize(self, width, height): - self.default_camera.use()() + self.default_camera.use() self.light_layer.resize(width, height) diff --git a/arcade/experimental/light_demo_perf.py b/arcade/experimental/light_demo_perf.py index a7d24e343..8f9d48c11 100644 --- a/arcade/experimental/light_demo_perf.py +++ b/arcade/experimental/light_demo_perf.py @@ -61,7 +61,7 @@ def on_update(self, dt): print(e) def on_resize(self, width, height): - self.default_camera.use()() + self.default_camera.use() self.light_layer.resize(width, height) diff --git a/arcade/experimental/subpixel_experiment.py b/arcade/experimental/subpixel_experiment.py index 025b1dcd1..d07a85cc7 100644 --- a/arcade/experimental/subpixel_experiment.py +++ b/arcade/experimental/subpixel_experiment.py @@ -57,7 +57,7 @@ def on_update(self, dt): def on_resize(self, width, height): print("Resize", width, height) - self.default_camera.use()() + self.default_camera.use() if __name__ == "__main__": diff --git a/doc/tutorials/views/03_views.py b/doc/tutorials/views/03_views.py index 4885a617d..86e5948c8 100644 --- a/doc/tutorials/views/03_views.py +++ b/doc/tutorials/views/03_views.py @@ -20,7 +20,7 @@ def on_show_view(self): # Reset the viewport, necessary if we have a scrolling game and we need # to reset the viewport back to the start so we can see what we draw. - self.window.default_camera.use()() + self.window.default_camera.use() def on_draw(self): """ Draw this view """ diff --git a/doc/tutorials/views/04_views.py b/doc/tutorials/views/04_views.py index f404cbfc0..ddb0f15c4 100644 --- a/doc/tutorials/views/04_views.py +++ b/doc/tutorials/views/04_views.py @@ -20,7 +20,7 @@ def on_show_view(self): # Reset the viewport, necessary if we have a scrolling game and we need # to reset the viewport back to the start so we can see what we draw. - self.window.default_camera.use()() + self.window.default_camera.use() def on_draw(self): """ Draw this view """ @@ -47,7 +47,7 @@ def __init__(self): # Reset the viewport, necessary if we have a scrolling game and we need # to reset the viewport back to the start so we can see what we draw. - self.window.default_camera.use()() + self.window.default_camera.use() def on_draw(self): """ Draw this view """ From a84fcacae7362396190b0ee0082bf9457fae2345 Mon Sep 17 00:00:00 2001 From: DragonMoffon Date: Tue, 26 Sep 2023 02:54:07 +1300 Subject: [PATCH 43/94] Whoops didn't run integration tests --- arcade/examples/sprite_move_scrolling_shake.py | 2 +- doc/tutorials/raycasting/step_08.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/arcade/examples/sprite_move_scrolling_shake.py b/arcade/examples/sprite_move_scrolling_shake.py index 2dd46b150..2e6746d24 100644 --- a/arcade/examples/sprite_move_scrolling_shake.py +++ b/arcade/examples/sprite_move_scrolling_shake.py @@ -55,7 +55,7 @@ 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.SimpleCamera() - self.camera_gui = arcade.camera.Camera() + self.camera_gui = arcade.camera.SimpleCamera() self.explosion_sound = arcade.load_sound(":resources:sounds/explosion1.wav") diff --git a/doc/tutorials/raycasting/step_08.py b/doc/tutorials/raycasting/step_08.py index cb425ad85..3341e890e 100644 --- a/doc/tutorials/raycasting/step_08.py +++ b/doc/tutorials/raycasting/step_08.py @@ -39,8 +39,8 @@ def __init__(self, width, height, title): self.physics_engine = None # Create cameras used for scrolling - self.camera_sprites = arcade.SimpleCamera() - self.camera_gui = arcade.SimpleCamera() + self.camera_sprites = arcade.camera.SimpleCamera() + self.camera_gui = arcade.camera.SimpleCamera() self.generate_sprites() From 96ffe34dacd93971a60e8d8ec931768b0db94edf Mon Sep 17 00:00:00 2001 From: DragonMoffon Date: Mon, 9 Oct 2023 02:05:38 +1300 Subject: [PATCH 44/94] Created Camera Shake Controller. Created a camera shake controller to replace the one removed. --- arcade/camera/__init__.py | 5 +- arcade/camera/controllers/__init__.py | 7 +- .../controllers/simple_controller_classes.py | 285 ++++++++++++++++++ arcade/examples/camera_platform.py | 43 +-- 4 files changed, 319 insertions(+), 21 deletions(-) create mode 100644 arcade/camera/controllers/simple_controller_classes.py diff --git a/arcade/camera/__init__.py b/arcade/camera/__init__.py index 8563d228a..8aa91c84b 100644 --- a/arcade/camera/__init__.py +++ b/arcade/camera/__init__.py @@ -12,6 +12,8 @@ from arcade.camera.simple_camera import SimpleCamera from arcade.camera.camera_2d import Camera2D +import arcade.camera.controllers as controllers + __all__ = [ 'Projection', 'Projector', @@ -22,5 +24,6 @@ 'PerspectiveProjectionData', 'PerspectiveProjector', 'SimpleCamera', - 'Camera2D' + 'Camera2D', + 'controllers' ] diff --git a/arcade/camera/controllers/__init__.py b/arcade/camera/controllers/__init__.py index d81f717d2..7787bf48e 100644 --- a/arcade/camera/controllers/__init__.py +++ b/arcade/camera/controllers/__init__.py @@ -9,6 +9,10 @@ rotate_around_forward ) +from arcade.camera.controllers.simple_controller_classes import ( + ScreenShakeController +) + __all__ = [ 'simple_follow_3D', @@ -18,5 +22,6 @@ 'quaternion_rotation', 'rotate_around_up', 'rotate_around_right', - 'rotate_around_forward' + 'rotate_around_forward', + 'ScreenShakeController' ] diff --git a/arcade/camera/controllers/simple_controller_classes.py b/arcade/camera/controllers/simple_controller_classes.py new file mode 100644 index 000000000..3f5bd7422 --- /dev/null +++ b/arcade/camera/controllers/simple_controller_classes.py @@ -0,0 +1,285 @@ +""" +A small group of classes that offer common controller use-cases. + +ScreenShakeController: + Provides an easy way to cause a camera to shake. +""" + + +from typing import Tuple +from math import exp, log, pi, sin, floor +from random import uniform + +from arcade.camera.data import CameraData +from arcade.camera.controllers import quaternion_rotation + +__all__ = [ + 'ScreenShakeController' +] + + +class ScreenShakeController: + """ + Offsets the camera position in a random direction repeatedly over + a set length of time to create a screen shake effect. + + The amplitude of the screen-shaking grows based on two functions. + The first is a simple sin wave whose frequency is adjustable. + This is multiplied with a pair of equations which go from 0-1 smoothly. + the equation rises using a inverse exponential equation, before decreasing + using a modified smooth-step sigmoid. + + Attributes: + max_amplitude: The largest possible world space offset. + falloff_duration: The length of time in seconds it takes the shaking + to reach 0 after reaching the maximum. Can be set + to a negative number to disable falloff. + shake_frequency: The number of peaks per second. Avoid making it + a multiple of half the target frame-rate. + (e.g. at 60 fps avoid 30, 60, 90, 120, etc.) + """ + + def __init__(self, camera_data: CameraData, *, + max_amplitude: float = 1.0, + falloff_time: float = 1.0, + acceleration_duration: float = 1, + shake_frequency: float = 15.0): + """ + Initialise a screen-shake controller. + + Args: + camera_data: The CameraData PoD that the controller modifies. + Should not be changed once initialised. + max_amplitude: The largest possible world space offset. + falloff_time: The length of time in seconds it takes the shaking + to reach 0 after reaching the maximum. Can be set + to a negative number to disable falloff. + acceleration_duration: The length of time in seconds it takes the + shaking to reach max amplitude. Can be set + to 0.0 to start at max amplitude. + shake_frequency: The number of peaks per second. Avoid making it + a multiple of half the target frame-rate. + (e.g. at 60 fps avoid 30, 60, 90, 120, etc.) + """ + self._data: CameraData = camera_data + + self.max_amplitude: float = max_amplitude + self.falloff_duration: float = falloff_time + self.shake_frequency: float = shake_frequency + self._acceleration_duration: float = acceleration_duration + + self._shaking: bool = False + self._length_shaking: float = 0.0 + + self._current_vec: Tuple[float, float, float] = (0.0, 0.0, 0.0) + self._last_vector: Tuple[float, float, float] = (0.0, 0.0, 0.0) + self._last_update_time: float = 0.0 + + @property + def shaking(self) -> bool: + """Read only property to check if the controller is currently shaking the camera.""" + return self._shaking + + @property + def duration(self) -> float: + """ + The length of the screen shake in seconds. + + If falloff is disabled (by setting falloff_duration to a negative number) only returns the + acceleration duration. + + Setting the duration to a negative number disables falloff. + While the falloff is disabled setting duration will only set the acceleration. + Otherwise, scales both the acceleration and falloff time to match new duration. + """ + if self.falloff_duration < 0.0: + return self._acceleration_duration + return self._acceleration_duration + self.falloff_duration + + @duration.setter + def duration(self, _duration: float): + if _duration <= 0.0: + self.falloff_duration = -1.0 + + elif self.falloff_duration < 0.0: + self._acceleration_duration = _duration + return + + else: + ratio = _duration / self.duration + self._acceleration_duration = ratio * self._acceleration_duration + self.falloff_duration = ratio * self.falloff_duration + + @property + def current_amplitude(self) -> float: + """Read only property which provides the current shake amplitude.""" + return self._calc_amplitude() * self.max_amplitude + + @property + def acceleration_duration(self) -> float: + """ + The length of time in seconds it takes for the shaking to reach max amplitude. + + Setting to a value less than zero causes the amplitude to start at max. + """ + return self._acceleration_duration + + @acceleration_duration.setter + def acceleration_duration(self, _duration): + if _duration < 0.0: + self._acceleration_duration = 0.0 + else: + self._acceleration_duration = _duration + + @property + def acceleration(self) -> float: + """ + The inverse of acceleration time. + + setting to a value less than zero causes the amplitude to start at max. + """ + if self._acceleration_duration <= 0.0: + return 0.0 + return 1 / self._acceleration_duration + + @acceleration.setter + def acceleration(self, _acceleration: float): + if _acceleration <= 0.0: + self._acceleration_duration = 0.0 + else: + self._acceleration_duration = 1 / _acceleration + + @property + def falloff(self) -> float: + """ + The maximum gradient of the amplitude falloff, + and is the gradient at the inflection point of the sigmoid equation. + + Is inversely proportional to the falloff duration by a factor of 15/8. + """ + if self.falloff_duration < 0.0: + return -1.0 + return (15 / 8) * (1 / self.falloff_duration) + + @falloff.setter + def falloff(self, _falloff: float): + if _falloff <= 0.0: + self.falloff_duration = -1.0 + else: + self.falloff_duration = (15 / 8) * (1 / _falloff) + + def _acceleration_amp(self, _t: float) -> float: + """ + The equation for the growing half of the amplitude equation. + It uses 1.0001 so that at _t = 1.0 the amplitude equals 1.0. + + Args: + _t: The scaled time. Should be between 0.0 and 1.0 + """ + return 1.0001 - 1.0001*exp(log(0.0001/1.0001) * _t) + + def _falloff_amp(self, _t: float) -> float: + """ + The equation for the falloff half of the amplitude equation. + It is based on the 'smootherstep' function. + + Args: + _t: The scaled time. Should be between 0.0 and 1.0 + """ + return 1 - _t**3 * (_t * (_t * 6.0 - 15.0) + 10.0) + + def _calc_max_amp(self): + if self._length_shaking <= self._acceleration_duration: + _t = self._length_shaking / self._acceleration_duration + return self._acceleration_amp(_t) + + if self.falloff_duration < 0.0: + return self.max_amplitude + + if self._length_shaking <= self.duration: + _t = (self._length_shaking - self._acceleration_duration) / self.falloff_duration + return self._falloff_amp(_t) + + return 0.0 + + def _calc_amplitude(self): + _max_amp = self._calc_max_amp() + _sin_amp = sin(self.shake_frequency * 2.0 * pi * self._length_shaking) + + return _sin_amp * _max_amp + + def reset(self): + """ + Reset the temporary shaking variables. WILL NOT STOP OR START SCREEN SHAKE. + """ + self._current_vec = (0.0, 0.0, 0.0) + self._last_vector = (0.0, 0.0, 0.0) + self._last_update_time = 0.0 + self._length_shaking = 0.0 + + def start(self): + """ + Start the screen-shake. + """ + self.reset() + self._shaking = True + + def stop(self): + """ + Instantly stop the screen-shake. + """ + self.reset() + self._shaking = False + + def update(self, delta_time: float): + """ + Update the time, and decide if the shaking should stop. + Does not actually set the camera position. + Should not be called more than once an update cycle. + + Args: + delta_time: the length of time in seconds between update calls. + Generally pass in the delta_time provided by the + arcade.Window's on_update method. + """ + if not self._shaking: + return + + self._length_shaking += delta_time + + if self.falloff_duration > 0.0 and self._length_shaking >= self.duration: + self.stop() + + def update_camera(self): + """ + Update the position of the camera. Call this just before using the camera. + because the controller is modifying the PoD directly it stores the last + offset and resets the camera's position before adding the next offset. + """ + if not self._shaking: + return + + if (floor(self._last_update_time * 2 * self.shake_frequency) < + floor(self._length_shaking * 2.0 * self.shake_frequency))\ + or self._last_update_time == 0.0: + _dir = uniform(-180, 180) + self._current_vec = quaternion_rotation(self._data.forward, self._data.up, _dir) + + _amp = self._calc_amplitude() * self.max_amplitude + _vec = self._current_vec + + _last = self._last_vector + _pos = self._data.position + + self._data.position = ( + _pos[0] - _last[0] + _vec[0] * _amp, + _pos[1] - _last[1] + _vec[1] * _amp, + _pos[2] - _last[2] + _vec[2] * _amp + ) + + self._last_vector = ( + _vec[0] * _amp, + _vec[1] * _amp, + _vec[2] * _amp + ) + self._last_update_time = self._length_shaking diff --git a/arcade/examples/camera_platform.py b/arcade/examples/camera_platform.py index 16016a8a5..6bca5c63a 100644 --- a/arcade/examples/camera_platform.py +++ b/arcade/examples/camera_platform.py @@ -68,9 +68,11 @@ def __init__(self): self.fps_message = None # Cameras - self.camera = None + self.camera: arcade.camera.Camera2D = None self.gui_camera = None + self.camera_shake = None + self.shake_offset_1 = 0 self.shake_offset_2 = 0 self.shake_vel_1 = 0 @@ -130,8 +132,14 @@ def setup(self): self.scene.add_sprite("Player", self.player_sprite) viewport = (0, 0, SCREEN_WIDTH, SCREEN_HEIGHT) - self.camera = arcade.camera.SimpleCamera(viewport=viewport) - self.gui_camera = arcade.camera.SimpleCamera(viewport=viewport) + self.camera = arcade.camera.Camera2D(viewport=viewport) + self.gui_camera = arcade.camera.Camera2D(viewport=viewport) + + self.camera_shake = arcade.camera.controllers.ScreenShakeController(self.camera.data, + max_amplitude=12.5, + acceleration_duration=0.05, + falloff_time=0.20, + shake_frequency=15.0) # Center camera on user self.pan_camera_to_user() @@ -156,14 +164,15 @@ def setup(self): def on_resize(self, width, height): """Resize window""" - self.camera.resize(width, height) - self.gui_camera.resize(width, height) + self.camera.projection = self.gui_camera.projection = (-width/2, width/2, -height/2, height/2) + self.camera.viewport = self.gui_camera.viewport = (0, 0, width, height) def on_draw(self): """Render the screen.""" self.clear() self.camera.use() + self.camera_shake.update_camera() # Draw our Scene self.scene.draw() @@ -185,9 +194,8 @@ def on_draw(self): # Draw game over if self.game_over: - x = 200 + self.camera.position[0] - y = 200 + self.camera.position[1] - arcade.draw_text("Game Over", x, y, arcade.color.BLACK, 30) + arcade.draw_text("Game Over", self.width/2, self.height/2, arcade.color.BLACK, + 30) self.frame_count += 1 @@ -219,17 +227,14 @@ def pan_camera_to_user(self, panning_fraction: float = 1.0): """ # This spot would center on the user - screen_center_x = self.player_sprite.center_x - (self.camera.viewport_width / 2) - screen_center_y = self.player_sprite.center_y - ( - self.camera.viewport_height / 2 - ) - if screen_center_x < 0: - screen_center_x = 0 - if screen_center_y < 0: - screen_center_y = 0 + screen_center_x, screen_center_y = self.player_sprite.position + if screen_center_x < self.camera.viewport_width/2: + screen_center_x = self.camera.viewport_width/2 + if screen_center_y < self.camera.viewport_height/2: + screen_center_y = self.camera.viewport_height/2 user_centered = screen_center_x, screen_center_y - self.camera.move_to(user_centered, panning_fraction) + arcade.camera.controllers.simple_follow_2D(panning_fraction, user_centered, self.camera.data) def on_update(self, delta_time): """Movement and game logic""" @@ -240,6 +245,7 @@ def on_update(self, delta_time): # Call update on all sprites if not self.game_over: self.physics_engine.update() + self.camera_shake.update(delta_time) coins_hit = arcade.check_for_collision_with_list( self.player_sprite, self.scene.get_sprite_list("Coins") @@ -254,8 +260,7 @@ def on_update(self, delta_time): ) for bomb in bombs_hit: bomb.remove_from_sprite_lists() - print("Pow") - # TODO: self.camera.shake((4, 7)) -> Camera Missing This Functionality + self.camera_shake.start() # Pan to the user self.pan_camera_to_user(panning_fraction=0.12) From 867c9d1750e0e9ae3411bcbdba44c0509fae6b32 Mon Sep 17 00:00:00 2001 From: DragonMoffon Date: Tue, 10 Oct 2023 11:31:34 +1300 Subject: [PATCH 45/94] Completed Doc Strings --- arcade/camera/camera_2d.py | 238 ++++-------------- .../simple_controller_functions.py | 54 ++-- arcade/camera/data.py | 46 ++-- arcade/camera/default.py | 42 ++++ arcade/camera/orthographic.py | 54 ++-- arcade/camera/perspective.py | 78 ++++-- arcade/camera/simple_camera.py | 65 +++-- arcade/camera/types.py | 3 +- arcade/examples/camera_platform.py | 2 +- arcade/gui/ui_manager.py | 7 +- tests/unit/camera/test_orthographic_camera.py | 48 ++-- 11 files changed, 322 insertions(+), 315 deletions(-) diff --git a/arcade/camera/camera_2d.py b/arcade/camera/camera_2d.py index a27936395..e8f698991 100644 --- a/arcade/camera/camera_2d.py +++ b/arcade/camera/camera_2d.py @@ -36,20 +36,7 @@ class Camera2D: - 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. + Only getter properties are provided through data and projection_data respectively. """ def __init__(self, *, @@ -64,6 +51,25 @@ def __init__(self, *, camera_data: Optional[CameraData] = None, projection_data: Optional[OrthographicProjectionData] = None ): + """ + Initialise a Camera2D instance. Either with camera PoDs or individual arguments. + + Args: + window: The Arcade Window to bind the camera to. + Defaults to the currently active window. + viewport: A 4-int tuple which defines the pixel bounds which the camera with project to. + position: The 2D position of the camera in the XY plane. + up: The 2D unit vector which defines the +Y-axis of the camera space. + zoom: A scalar value which is inversely proportional to the size of the camera projection. + i.e. a zoom of 2.0 halves the size of the projection, doubling the perceived size of objects. + projection: A 4-float tuple which defines the world space + bounds which the camera projects to the viewport. + near: The near clipping plane of the camera. + far: The far clipping place of the camera. + camera_data: A CameraData PoD which describes the viewport, position, up, and zoom + projection_data: A OrthographicProjectionData PoD which describes the left, right, top, + bottom, far, near planes for an orthographic projection. + """ self._window: "Window" = window or get_window() assert ( @@ -82,7 +88,6 @@ def __init__(self, *, _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), @@ -96,7 +101,9 @@ def __init__(self, *, 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. + near or 0.0, far or 100.0, # Near and Far. + + viewport or (0, 0, self._window.width, self._window.height) # Viewport ) self._ortho_projector: OrthographicProjector = OrthographicProjector( @@ -144,9 +151,6 @@ def pos(self) -> Tuple[float, float]: @pos.setter def pos(self, _pos: Tuple[float, float]) -> None: - """ - Set the X and Y position of the camera. - """ self._data.position = (_pos[0], _pos[1], self._data.position[2]) @property @@ -160,9 +164,6 @@ def position(self) -> Tuple[float, float]: @position.setter def position(self, _pos: Tuple[float, float]) -> None: - """ - Set the X and Y position of the camera. - """ self._data.position = (_pos[0], _pos[1], self._data.position[2]) @property @@ -175,10 +176,6 @@ def left(self) -> float: @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 @@ -191,10 +188,6 @@ def right(self) -> float: @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 @@ -207,10 +200,6 @@ def bottom(self) -> float: @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, @@ -227,10 +216,6 @@ def top(self) -> float: @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, @@ -248,10 +233,6 @@ def projection(self) -> Tuple[float, float, float, float]: @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 @@ -269,14 +250,6 @@ def projection_width(self) -> float: @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 @@ -298,14 +271,6 @@ def projection_width_scaled(self) -> float: @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 @@ -327,14 +292,6 @@ def projection_height(self) -> float: @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 @@ -356,14 +313,6 @@ def projection_height_scaled(self) -> float: @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 @@ -385,14 +334,6 @@ def projection_left(self) -> float: @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 @@ -409,14 +350,6 @@ def projection_left_scaled(self) -> float: @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 @@ -433,14 +366,6 @@ def projection_right(self) -> float: @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 @@ -457,14 +382,6 @@ def projection_right_scaled(self) -> float: @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 @@ -481,14 +398,6 @@ def projection_bottom(self) -> float: @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 @@ -505,14 +414,6 @@ def projection_bottom_scaled(self) -> float: @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 @@ -529,14 +430,6 @@ def projection_top(self) -> float: @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 @@ -553,14 +446,6 @@ def projection_top_scaled(self) -> float: @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 @@ -575,12 +460,6 @@ def projection_near(self) -> float: @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 @@ -595,12 +474,6 @@ def projection_far(self) -> float: @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 @@ -609,15 +482,11 @@ 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 + return self._projection.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 + self._projection.viewport = _viewport @property def viewport_width(self) -> int: @@ -625,15 +494,12 @@ def viewport_width(self) -> int: The width of the viewport. Defines the number of pixels drawn too horizontally. """ - return self._data.viewport[2] + return self._projection.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]) + self._projection.viewport = (self._projection.viewport[0], self._projection.viewport[1], + _width, self._projection.viewport[3]) @property def viewport_height(self) -> int: @@ -641,36 +507,30 @@ def viewport_height(self) -> int: The height of the viewport. Defines the number of pixels drawn too vertically. """ - return self._data.viewport[3] + return self._projection.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,) + self._projection.viewport = (self._projection.viewport[0], self._projection.viewport[1], + self._projection.viewport[2], _height) @property def viewport_left(self) -> int: """ The left most pixel drawn to on the X axis. """ - return self._data.viewport[0] + return self._projection.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[1:] + self._projection.viewport = (_left,) + self._projection.viewport[1:] @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] + return self._projection.viewport[0] + self._projection.viewport[2] @viewport_right.setter def viewport_right(self, _right: int) -> None: @@ -678,28 +538,30 @@ 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:] + self._projection.viewport = (_right - self._projection.viewport[2], self._projection.viewport[1], + self._projection.viewport[2], self._projection.viewport[3]) @property def viewport_bottom(self) -> int: """ The bottom most pixel drawn to on the Y axis. """ - return self._data.viewport[1] + return self._projection.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:] + self._projection.viewport = (self._projection.viewport[0], _bottom, + self._projection.viewport[2], self._projection.viewport[3]) @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] + return self._projection.viewport[1] + self._projection.viewport[3] @viewport_top.setter def viewport_top(self, _top: int) -> None: @@ -707,7 +569,8 @@ 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:] + self._projection.viewport = (self._projection.viewport[0], _top - self._projection.viewport[3], + self._projection.viewport[2], self._projection.viewport[3]) @property def up(self) -> Tuple[float, float]: @@ -731,7 +594,7 @@ def up(self, _up: Tuple[float, float]) -> None: NOTE that this is assumed to be normalised. """ - self._data.up = _up + (0,) + self._data.up = (_up[0], _up[1], 0.0) @property def angle(self) -> float: @@ -795,7 +658,8 @@ 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. + Args: + and_projection: Flag whether to also equalise the projection to the viewport. """ self.viewport = (0, 0, self._window.width, self._window.height) @@ -832,12 +696,18 @@ def activate(self) -> Iterator[Projector]: 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 + the range of the window size and returns the world space coordinates. Essentially reverses the effects of the projector. - :param screen_coordinate: The pixel coordinates to map back to world coordinates. + Args: + screen_coordinate: A 2D position in pixels from the bottom left of the screen. + This should ALWAYS be in the range of 0.0 - screen size. + Returns: + A 2D vector (Along the XY plane) in world space (same as sprites). + perfect for finding if the mouse overlaps with a sprite or ui element irrespective + of the camera. """ - return self._ortho_projector.map_coordinate(screen_coordinate) + return self._ortho_projector.map_coordinate(screen_coordinate)[:2] diff --git a/arcade/camera/controllers/simple_controller_functions.py b/arcade/camera/controllers/simple_controller_functions.py index 67e8977d9..d7fbdb6e6 100644 --- a/arcade/camera/controllers/simple_controller_functions.py +++ b/arcade/camera/controllers/simple_controller_functions.py @@ -148,9 +148,10 @@ def simple_follow_3D(speed: float, target: Tuple[float, float, float], data: Cam """ 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. + Args: + speed: The percentage the camera should move towards the target (0.0 - 1.0 range) + target: The 3D position the camera should move towards in world space. + data: The camera data object which stores its position, rotation, and direction. """ data.position = _interpolate_3D(data.position, target, speed) @@ -160,9 +161,10 @@ 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. + Args: + speed: The percentage the camera should move towards the target (0.0 - 1.0 range) + target: The 2D position the camera should move towards in world space. (vector in XY-plane) + data: The camera data object which stores its position, rotation, and direction. """ simple_follow_3D(speed, (target[0], target[1], 0.0), data) @@ -172,18 +174,19 @@ def simple_easing_3D(percent: 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. + A simple method which moves a camera in a straight line between two 3D 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. + Args: + percent: The percentage from 0 to 1 which describes + how far between the two points to place the camera. + start: The 3D point which acts as the starting point for the camera motion. + target: The 3D point which acts as the final destination for the camera. + data: The camera data object which stores its position, rotation, and direction. + 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 = _interpolate_3D(start, target, func(percent)) @@ -194,16 +197,19 @@ def simple_easing_2D(percent: 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. + A simple method which moves a camera in a straight line between two 2D points (along XY plane). + 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. + Args: + percent: The percentage from 0 to 1 which describes + how far between the two points to place the camera. + start: The 2D point which acts as the starting point for the camera motion. + target: The 2D point which acts as the final destination for the camera. + data: The camera data object which stores its position, rotation, and direction. + 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_3D(percent, (start[0], start[1], 0.0), (target[0], target[1], 0.0), data, func) diff --git a/arcade/camera/data.py b/arcade/camera/data.py index 5e2c786ef..09e5a6575 100644 --- a/arcade/camera/data.py +++ b/arcade/camera/data.py @@ -14,18 +14,14 @@ class CameraData: """ 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. + Attributes: + position: A 3D vector which describes where the camera is located. + up: A 3D vector which describes which direction is up (+y). + forward: a 3D vector which describes which direction is forwards (+z). + 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] = (0.0, 0.0, 0.0) up: Tuple[float, float, float] = (0.0, 1.0, 0.0) @@ -44,12 +40,14 @@ class OrthographicProjectionData: 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). + Attributes: + left: The left most value, which gets mapped to x = -1.0 (anything below this value is not visible). + right: The right most value, which gets mapped to x = 1.0 (anything above this value is not visible). + bottom: The bottom most value, which gets mapped to y = -1.0 (anything below this value is not visible). + top: The top most value, which gets mapped to y = 1.0 (anything above this value is not visible). + near: The 'closest' value, which gets mapped to z = -1.0 (anything below this value is not visible). + far: The 'furthest' value, Which gets mapped to z = 1.0 (anything above this value is not visible). + viewport: The pixel bounds which will be drawn onto. (left, bottom, width, height) """ left: float right: float @@ -58,19 +56,25 @@ class OrthographicProjectionData: near: float far: float + viewport: Tuple[int, int, int, int] + @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 + Attributes: + aspect: The aspect ratio of the screen (width over height). + 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: The 'closest' value, which gets mapped to z = -1.0 (anything below this value is not visible). + far: The 'furthest' value, Which gets mapped to z = 1.0 (anything above this value is not visible). + viewport: The pixel bounds which will be drawn onto. (left, bottom, width, height) """ aspect: float fov: float near: float far: float + + viewport: Tuple[int, int, int, int] diff --git a/arcade/camera/default.py b/arcade/camera/default.py index d831a4a88..27127867f 100644 --- a/arcade/camera/default.py +++ b/arcade/camera/default.py @@ -15,8 +15,21 @@ class ViewportProjector: + """ + A simple Projector which does not rely on any camera PoDs. + + Does not have a way of moving, rotating, or zooming the camera. + perfect for something like UI or for mapping to an offscreen framebuffer. + """ def __init__(self, viewport: Optional[Tuple[int, int, int, int]] = None, *, window: Optional["Window"] = None): + """ + Initialise a ViewportProjector + + Args: + viewport: The viewport to project to. + window: The window to bind the camera to. Defaults to the currently active camera. + """ self._window = window or get_window() self._viewport = viewport or self._window.ctx.viewport @@ -26,6 +39,9 @@ def __init__(self, viewport: Optional[Tuple[int, int, int, int]] = None, *, wind @property def viewport(self): + """ + The viewport use to derive projection and view matrix. + """ return self._viewport @viewport.setter @@ -37,6 +53,10 @@ def viewport(self, viewport: Tuple[int, int, int, int]): -100, 100) def use(self): + """ + Set the window's projection and view matrix. + Also sets the projector as the windows current camera. + """ self._window.current_camera = self self._window.ctx.viewport = self._viewport @@ -46,6 +66,11 @@ def use(self): @contextmanager def activate(self) -> Iterator[Projector]: + """ + The context manager version of the use method. + + usable with the 'with' block. e.g. 'with ViewportProjector.activate() as cam: ...' + """ previous = self._window.current_camera try: self.use() @@ -54,6 +79,11 @@ def activate(self) -> Iterator[Projector]: previous.use() def map_coordinate(self, screen_coordinate: Tuple[float, float]) -> Tuple[float, float]: + """ + Map the screen pos to screen_coordinates. + + Due to the nature of viewport projector this does not do anything. + """ return screen_coordinate @@ -68,9 +98,21 @@ class DefaultProjector(ViewportProjector): # TODO: ADD PARAMS TO DOC FOR __init__ def __init__(self, *, window: Optional["Window"] = None): + """ + Initialise a ViewportProjector. + + Args: + window: The window to bind the camera to. Defaults to the currently active camera. + """ super().__init__(window=window) def use(self): + """ + Set the window's Projection and View matrices. + + cache's the window viewport to determine the projection matrix. + """ + if self._window.ctx.viewport != self.viewport: self.viewport = self._window.ctx.viewport super().use() diff --git a/arcade/camera/orthographic.py b/arcade/camera/orthographic.py index d8b937281..b4c975341 100644 --- a/arcade/camera/orthographic.py +++ b/arcade/camera/orthographic.py @@ -12,8 +12,7 @@ __all__ = [ - 'OrthographicProjector', - 'OrthographicProjectionData' + 'OrthographicProjector' ] @@ -33,16 +32,23 @@ 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, view: Optional[CameraData] = None, projection: Optional[OrthographicProjectionData] = None): + """ + Initialise a Projector which produces an orthographic projection matrix using + a CameraData and PerspectiveProjectionData PoDs. + + Args: + window: The window to bind the camera to. Defaults to the currently active camera. + view: The CameraData PoD. contains the viewport, position, up, forward, and zoom. + projection: The OrthographicProjectionData PoD. + contains the left, right, bottom top, near, and far planes. + """ self._window: "Window" = window or get_window() - self._view = view or CameraData( - (0, 0, self._window.width, self._window.height), # Viewport + self._view = view or CameraData( # 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 @@ -53,14 +59,22 @@ def __init__(self, *, -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 + + (0, 0, self._window.width, self._window.height) # Viewport ) @property - def view_data(self) -> CameraData: + def view(self) -> CameraData: + """ + The CameraData. Is a read only property. + """ return self._view @property - def projection_data(self) -> OrthographicProjectionData: + def projection(self) -> OrthographicProjectionData: + """ + The OrthographicProjectionData. Is a read only property. + """ return self._projection def _generate_projection_matrix(self) -> Mat4: @@ -122,7 +136,7 @@ def use(self): _projection = self._generate_projection_matrix() _view = self._generate_view_matrix() - self._window.ctx.viewport = self._view.viewport + self._window.ctx.viewport = self._projection.viewport self._window.projection = _projection self._window.view = _view @@ -139,14 +153,22 @@ def activate(self) -> Iterator[Projector]: finally: previous_projector.use() - def map_coordinate(self, screen_coordinate: Tuple[float, float]) -> Tuple[float, float]: - """ - Maps a screen position to a pixel position. + def map_coordinate(self, screen_coordinate: Tuple[float, float]) -> Tuple[float, float, float]: """ - # TODO: better doc string + Take in a pixel coordinate from within + the range of the window size and returns + the world space coordinates. - 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 + Essentially reverses the effects of the projector. + + Args: + screen_coordinate: A 2D position in pixels from the bottom left of the screen. + This should ALWAYS be in the range of 0.0 - screen size. + Returns: + A 3D vector in world space. + """ + screen_x = 2.0 * (screen_coordinate[0] - self._projection.viewport[0]) / self._projection.viewport[2] - 1 + screen_y = 2.0 * (screen_coordinate[1] - self._projection.viewport[1]) / self._projection.viewport[3] - 1 _view = self._generate_view_matrix() _projection = self._generate_projection_matrix() @@ -157,4 +179,4 @@ def map_coordinate(self, screen_coordinate: Tuple[float, float]) -> Tuple[float, _mapped_position = _full @ screen_position - return _mapped_position[0], _mapped_position[1] + return _mapped_position[0], _mapped_position[1], _mapped_position[2] diff --git a/arcade/camera/perspective.py b/arcade/camera/perspective.py index 5b4265ec0..a6c905fb9 100644 --- a/arcade/camera/perspective.py +++ b/arcade/camera/perspective.py @@ -12,8 +12,7 @@ __all__ = [ - 'PerspectiveProjector', - 'PerspectiveProjectionData' + 'PerspectiveProjector' ] @@ -32,16 +31,24 @@ 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, view: Optional[CameraData] = None, projection: Optional[PerspectiveProjectionData] = None): + """ + Initialise a Projector which produces a perspective projection matrix using + a CameraData and PerspectiveProjectionData PoDs. + + Args: + window: The window to bind the camera to. Defaults to the currently active camera. + view: The CameraData PoD. contains the viewport, position, up, forward, and zoom. + projection: The PerspectiveProjectionData PoD. + contains the aspect ratio, fov, near plane, and far plane. + """ self._window: "Window" = window or get_window() 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 (0.0, 0.0, -1.0), # Forward @@ -51,15 +58,22 @@ def __init__(self, *, self._projection = projection or PerspectiveProjectionData( self._window.width / self._window.height, # Aspect ratio 90, # Field of view (degrees) - 0.1, 1000 # Near, Far + 0.1, 1000, # Near, Far + (0, 0, self._window.width, self._window.height), # Viewport ) @property - def view_data(self) -> CameraData: + def view(self) -> CameraData: + """ + Is a CameraData. Is a read only property + """ return self._view @property def projection(self) -> PerspectiveProjectionData: + """ + Is the PerspectiveProjectionData. is a read only property. + """ return self._projection def _generate_projection_matrix(self) -> Mat4: @@ -107,7 +121,7 @@ def use(self): _projection = self._generate_projection_matrix() _view = self._generate_view_matrix() - self._window.ctx.viewport = self._view.viewport + self._window.ctx.viewport = self._projection.viewport self._window.projection = _projection self._window.view = _view @@ -115,12 +129,7 @@ def use(self): 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. + `with` blocks. For example, `with camera.activate() as cam: ...`.. """ previous_projector = self._window.current_camera try: @@ -129,13 +138,22 @@ def activate(self) -> Iterator[Projector]: finally: previous_projector.use() - 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. + def map_coordinate(self, screen_coordinate: Tuple[float, float]) -> Tuple[float, float, float]: """ + Take in a pixel coordinate from within + the range of the window size and returns + the world space coordinates. + + Essentially reverses the effects of the projector. - 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 + Args: + screen_coordinate: A 2D position in pixels from the bottom left of the screen. + This should ALWAYS be in the range of 0.0 - screen size. + Returns: + A 3D vector in world space. + """ + screen_x = 2.0 * (screen_coordinate[0] - self._projection.viewport[0]) / self._projection.viewport[2] - 1 + screen_y = 2.0 * (screen_coordinate[1] - self._projection.viewport[1]) / self._projection.viewport[3] - 1 _view = self._generate_view_matrix() _projection = self._generate_projection_matrix() @@ -146,16 +164,28 @@ def map_coordinate(self, screen_coordinate: Tuple[float, float]) -> Tuple[float, _mapped_position = _full @ screen_position - return _mapped_position[0], _mapped_position[1] + return _mapped_position[0], _mapped_position[1], _mapped_position[2] def map_coordinate_at_depth(self, screen_coordinate: Tuple[float, float], - depth: float) -> Tuple[float, float]: + depth: float) -> Tuple[float, float, float]: """ - Maps a screen position to a pixel position at the specific depth supplied. + Take in a pixel coordinate from within + the range of the window size and returns + the world space coordinates. + + Essentially reverses the effects of the projector. + + Args: + screen_coordinate: A 2D position in pixels from the bottom left of the screen. + This should ALWAYS be in the range of 0.0 - screen size. + depth: the depth that the mouse should be compared against. + Should range from near to far planes of projection. + Returns: + A 3D vector in world space. """ - 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 + screen_x = 2.0 * (screen_coordinate[0] - self._projection.viewport[0]) / self._projection.viewport[2] - 1 + screen_y = 2.0 * (screen_coordinate[1] - self._projection.viewport[1]) / self._projection.viewport[3] - 1 _view = self._generate_view_matrix() _projection = self._generate_projection_matrix() @@ -168,4 +198,4 @@ def map_coordinate_at_depth(self, _mapped_position = _full @ screen_position - return _mapped_position[0], _mapped_position[1] + return _mapped_position[0], _mapped_position[1], _mapped_position[2] diff --git a/arcade/camera/simple_camera.py b/arcade/camera/simple_camera.py index c5b8c2028..57e01082c 100644 --- a/arcade/camera/simple_camera.py +++ b/arcade/camera/simple_camera.py @@ -26,7 +26,6 @@ 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, @@ -40,6 +39,25 @@ def __init__(self, *, camera_data: Optional[CameraData] = None, projection_data: Optional[OrthographicProjectionData] = None ): + """ + Initialise a Simple Camera Instance with either Camera PoDs or individual arguments + + Args: + window: The Arcade Window to bind the camera to. + Defaults to the currently active window. + viewport: A 4-int tuple which defines the pixel bounds which the camera with project to. + position: The 2D position of the camera in the XY plane. + up: The 2D unit vector which defines the +Y-axis of the camera space. + zoom: A scalar value which is inversely proportional to the size of the camera projection. + i.e. a zoom of 2.0 halves the size of the projection, doubling the perceived size of objects. + projection: A 4-float tuple which defines the world space + bounds which the camera projects to the viewport. + near: The near clipping plane of the camera. + far: The far clipping place of the camera. + camera_data: A CameraData PoD which describes the viewport, position, up, and zoom + projection_data: A OrthographicProjectionData PoD which describes the left, right, top, + bottom, far, near planes for an orthographic projection. + """ warn("arcade.camera.SimpleCamera has been depreciated please use arcade.camera.Camera2D instead", DeprecationWarning) @@ -53,7 +71,6 @@ def __init__(self, *, _pos = position or (0.0, 0.0) _up = up or (0.0, 1.0) 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), (0.0, 0.0, -1.0), @@ -66,11 +83,11 @@ def __init__(self, *, 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 + near or -100, far or 100, # Near, Far + viewport or (0, 0, self._window.width, self._window.height), # Viewport ) else: self._view = camera_data or CameraData( - (0, 0, self._window.width, self._window.height), # Viewport (0.0, 0.0, 0.0), # Position (0, 1.0, 0.0), # Up (0.0, 0.0, -1.0), # Forward @@ -79,7 +96,8 @@ def __init__(self, *, self._projection = projection_data or OrthographicProjectionData( 0.0, self._window.width, # Left, Right 0.0, self._window.height, # Bottom, Top - -100, 100 # Near, Far + -100, 100, # Near, Far + (0, 0, self._window.width, self._window.height), # Viewport ) self._camera = OrthographicProjector( @@ -96,17 +114,17 @@ def __init__(self, *, @property def viewport_width(self) -> int: """ Returns the width of the viewport """ - return self._view.viewport[2] + return self._projection.viewport[2] @property def viewport_height(self) -> int: """ Returns the height of the viewport """ - return self._view.viewport[3] + return self._projection.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 + return self._projection.viewport @viewport.setter def viewport(self, viewport: Tuple[int, int, int, int]) -> None: @@ -114,7 +132,7 @@ def viewport(self, viewport: Tuple[int, int, int, int]) -> None: self.set_viewport(viewport) def set_viewport(self, viewport: Tuple[int, int, int, int]) -> None: - self._view.viewport = viewport + self._projection.viewport = viewport @property def projection(self) -> Tuple[float, float, float, float]: @@ -246,8 +264,9 @@ def move_to(self, vector: Tuple[float, float], speed: float = 1.0) -> None: 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 + Args: + vector: The 2D vector position to move the camera towards (in the XY plane) + speed: How fast to move the camera, 1.0 is instant, 0.1 moves slowly """ self._position_goal = vector self._easing_speed = speed @@ -328,20 +347,32 @@ def activate(self) -> Iterator[Projector]: def map_coordinate(self, screen_coordinate: Tuple[float, float]) -> Tuple[float, float]: """ - Maps a screen position to a pixel position. + 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. + + Args: + screen_coordinate: A 2D position in pixels from the bottom left of the screen. + This should ALWAYS be in the range of 0.0 - screen size. + Returns: + A 2D vector (Along the XY plane) in world space (same as sprites). + perfect for finding if the mouse overlaps with a sprite or ui element irrespective + of the camera. """ - # TODO: better doc string - return self._camera.map_coordinate(screen_coordinate) + return self._camera.map_coordinate(screen_coordinate)[:2] 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 + Args: + viewport_width: Width of the viewport. + viewport_height: Height of the viewport. + 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) diff --git a/arcade/camera/types.py b/arcade/camera/types.py index b32600463..e035881ca 100644 --- a/arcade/camera/types.py +++ b/arcade/camera/types.py @@ -12,6 +12,7 @@ class Projection(Protocol): + viewport: Tuple[int, int, int, int] near: float far: float @@ -25,7 +26,7 @@ def use(self) -> None: def activate(self) -> Iterator["Projector"]: ... - def map_coordinate(self, screen_coordinate: Tuple[float, float]) -> Tuple[float, float]: + def map_coordinate(self, screen_coordinate: Tuple[float, float]) -> Tuple[float, ...]: ... diff --git a/arcade/examples/camera_platform.py b/arcade/examples/camera_platform.py index 6bca5c63a..19ca3dbd9 100644 --- a/arcade/examples/camera_platform.py +++ b/arcade/examples/camera_platform.py @@ -234,7 +234,7 @@ def pan_camera_to_user(self, panning_fraction: float = 1.0): screen_center_y = self.camera.viewport_height/2 user_centered = screen_center_x, screen_center_y - arcade.camera.controllers.simple_follow_2D(panning_fraction, user_centered, self.camera.data) + arcade.camera.controllers.simple_follow_2D(panning_fraction, user_centered, self.camera.view_data) def on_update(self, delta_time): """Movement and game logic""" diff --git a/arcade/gui/ui_manager.py b/arcade/gui/ui_manager.py index f41d9e49e..ea4b551ab 100644 --- a/arcade/gui/ui_manager.py +++ b/arcade/gui/ui_manager.py @@ -93,8 +93,9 @@ def __init__(self, window: Optional[arcade.Window] = None): self._rendered = False #: Camera used when drawing the UI self.projector = OrthographicProjector( - view=CameraData((0, 0, self.window.width, self.window.height)), - projection=OrthographicProjectionData(0, self.window.width, 0, self.window.height, -100, 100) + view=CameraData(), + projection=OrthographicProjectionData(0, self.window.width, 0, self.window.height, -100, 100, + (0, 0, self.window.width, self.window.height)) ) self.register_event_type("on_event") @@ -373,7 +374,7 @@ def on_text_motion_select(self, motion): def on_resize(self, width, height): scale = self.window.get_pixel_ratio() - self.projector.view_data.viewport = (0, 0, width, height) + self.projector.projection.viewport = (0, 0, width, height) for surface in self._surfaces.values(): surface.resize(size=(width, height), pixel_ratio=scale) diff --git a/tests/unit/camera/test_orthographic_camera.py b/tests/unit/camera/test_orthographic_camera.py index a69684cef..c902fa909 100644 --- a/tests/unit/camera/test_orthographic_camera.py +++ b/tests/unit/camera/test_orthographic_camera.py @@ -7,12 +7,11 @@ def test_orthographic_camera(window: Window): default_camera = window.default_camera cam_default = camera.OrthographicProjector() - default_view = cam_default.view_data - default_projection = cam_default.projection_data + 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 == 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 @@ -21,12 +20,13 @@ def test_orthographic_camera(window: Window): 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 + -100, 100, # Near, Far + (0, 0, window.width, window.height), # Viewport ) # test that the camera properties work - assert cam_default.view_data.position == default_view.position - assert cam_default.view_data.viewport == default_view.viewport + assert cam_default.view.position == default_view.position + assert cam_default.projection.viewport == default_projection.viewport # Test that the camera is being used. cam_default.use() @@ -41,7 +41,6 @@ def test_orthographic_camera(window: Window): assert window.current_camera == default_camera 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 @@ -50,7 +49,8 @@ def test_orthographic_camera(window: Window): set_projection = camera.OrthographicProjectionData( 0.0, 1.0, # Left, Right 0.0, 1.0, # Bottom, Top - -1.0, 1.0 # Near, Far + -1.0, 1.0, # Near, Far + (0, 0, 1, 1), # Viewport ) cam_set = camera.OrthographicProjector( view=set_view, @@ -58,12 +58,12 @@ def test_orthographic_camera(window: Window): ) # test that the camera correctly used the provided Pods. - assert cam_set.view_data == set_view - assert cam_set.projection_data == set_projection + assert cam_set.view == set_view + assert cam_set.projection == set_projection # test that the camera properties work - assert cam_set.view_data.position == set_view.position - assert cam_set.view_data.viewport == set_view.viewport + assert cam_set.view.position == set_view.position + assert cam_set.projection.viewport == set_projection.viewport # Test that the camera is actually recognised by the camera as being activated assert window.current_camera == default_camera @@ -80,51 +80,51 @@ def test_orthographic_camera(window: Window): def test_orthographic_projection_matrix(window: Window): cam_default = camera.OrthographicProjector() - default_view = cam_default.view_data - default_projection = cam_default.projection_data + default_view = cam_default.view + default_projection = cam_default.projection def test_orthographic_view_matrix(window: Window): cam_default = camera.OrthographicProjector() - default_view = cam_default.view_data - default_projection = cam_default.projection_data + default_view = cam_default.view + default_projection = cam_default.projection def test_orthographic_map_coordinates(window: Window): cam_default = camera.OrthographicProjector() - default_view = cam_default.view_data - default_projection = cam_default.projection_data + default_view = cam_default.view + default_projection = cam_default.projection # Test that the camera maps coordinates properly when no values have been adjusted - assert cam_default.map_coordinate((100.0, 100.0)) == (pytest.approx(100.0), pytest.approx(100.0)) + assert cam_default.map_coordinate((100.0, 100.0)) == (pytest.approx(100.0), pytest.approx(100.0), 0.0) # Test that the camera maps coordinates properly when 0.0, 0.0 is in the center of the screen default_view.position = (0.0, 0.0, 0.0) - assert (cam_default.map_coordinate((window.width//2, window.height//2)) == (0.0, 0.0)) + assert (cam_default.map_coordinate((window.width//2, window.height//2)) == (0.0, 0.0, 0.0)) # Test that the camera maps coordinates properly when the position has changed. default_view.position = (100.0, 100.0, 0.0) assert (cam_default.map_coordinate((100.0, 100.0)) == - (pytest.approx(200.0 - window.width//2), pytest.approx(200.0 - window.height//2))) + (pytest.approx(200.0 - window.width//2), pytest.approx(200.0 - window.height//2), 0.0)) # Test that the camera maps coordinates properly when the rotation has changed. default_view.position = (0.0, 0.0, 0.0) default_view.up = (1.0, 0.0, 0.0) assert (cam_default.map_coordinate((window.width//2, window.height//2 + 100.0)) == - (pytest.approx(100.0), pytest.approx(00.0))) + (pytest.approx(100.0), pytest.approx(00.0), 0.0)) # Test that the camera maps coordinates properly when the rotation and position has changed. default_view.position = (100.0, 100.0, 0.0) default_view.up = (0.7071067812, 0.7071067812, 0.0) assert (cam_default.map_coordinate((window.width//2, window.height//2)) == - (pytest.approx(100.0), pytest.approx(100.0))) + (pytest.approx(100.0), pytest.approx(100.0), 0.0)) # Test that the camera maps coordinates properly when zoomed in. default_view.position = (0.0, 0.0, 0.0) default_view.up = (0.0, 1.0, 0.0) default_view.zoom = 2.0 assert (cam_default.map_coordinate((window.width, window.height)) == - (pytest.approx(window.width//4), pytest.approx(window.height//4))) + (pytest.approx(window.width//4), pytest.approx(window.height//4), 0.0)) # Test that the camera maps coordinates properly when the viewport is not the default default_view.zoom = 1.0 From 6dd22a1b2e894ec81be38fabd2aaa5d9e07b004e Mon Sep 17 00:00:00 2001 From: DragonMoffon Date: Tue, 10 Oct 2023 13:49:09 +1300 Subject: [PATCH 46/94] Replacing all references of SimpleCamera with Camera2D --- arcade/camera/camera_2d.py | 2 +- arcade/examples/background_blending.py | 26 +++++------ arcade/examples/background_groups.py | 26 +++++------ arcade/examples/background_parallax.py | 8 ++-- arcade/examples/background_scrolling.py | 11 +++-- arcade/examples/background_stationary.py | 24 +++++------ arcade/examples/camera_platform.py | 2 +- arcade/examples/minimap.py | 4 +- arcade/examples/minimap_camera.py | 6 +-- .../examples/platform_tutorial/06_camera.py | 18 ++++---- .../platform_tutorial/07_coins_and_sound.py | 20 ++++----- arcade/examples/platform_tutorial/08_score.py | 22 +++++----- .../examples/platform_tutorial/09_load_map.py | 22 +++++----- .../platform_tutorial/10_multiple_levels.py | 22 +++++----- .../platform_tutorial/11_ladders_and_more.py | 22 +++++----- .../platform_tutorial/12_animate_character.py | 22 +++++----- .../platform_tutorial/13_add_enemies.py | 22 +++++----- .../platform_tutorial/14_moving_enemies.py | 22 +++++----- .../15_collision_with_enemies.py | 24 +++++------ .../platform_tutorial/16_shooting_bullets.py | 24 +++++------ arcade/examples/platform_tutorial/17_views.py | 26 +++++------ arcade/examples/procedural_caves_cellular.py | 16 +++---- arcade/examples/sprite_move_scrolling.py | 16 +++---- arcade/examples/sprite_move_scrolling_box.py | 32 ++++++-------- .../examples/sprite_move_scrolling_shake.py | 43 ++++++++----------- arcade/examples/sprite_moving_platforms.py | 10 ++--- arcade/examples/sprite_tiled_map.py | 20 ++++----- arcade/examples/template_platformer.py | 23 +++++----- tests/unit/camera/test_orthographic_camera.py | 12 ------ tests/unit/camera/test_perspective_camera.py | 8 ---- 30 files changed, 256 insertions(+), 299 deletions(-) diff --git a/arcade/camera/camera_2d.py b/arcade/camera/camera_2d.py index e8f698991..5f2b5a1d6 100644 --- a/arcade/camera/camera_2d.py +++ b/arcade/camera/camera_2d.py @@ -113,7 +113,7 @@ def __init__(self, *, ) @property - def data(self) -> CameraData: + def view_data(self) -> CameraData: """ Return the view data for the camera. This includes the viewport, position, forward vector, up direction, and zoom. diff --git a/arcade/examples/background_blending.py b/arcade/examples/background_blending.py index bbcc21e76..6c8bee47b 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.camera.SimpleCamera() + self.camera = arcade.camera.Camera2D() # Load the first background from file. Sized to match the screen self.background_1 = background.Background.from_file( @@ -51,21 +51,21 @@ def __init__(self): def pan_camera_to_player(self): # This will center the camera on the player. - target_x = self.player_sprite.center_x - (self.camera.viewport_width / 2) - target_y = self.player_sprite.center_y - (self.camera.viewport_height / 2) + target_x = self.player_sprite.center_x + target_y = self.player_sprite.center_y # This limits where the player can see. Ensuring they never go too far from the transition. - if -self.camera.viewport_width / 2 > target_x: - target_x = -self.camera.viewport_width / 2 - elif target_x > self.background_1.size[0] * 2 - self.camera.viewport_width / 2: - target_x = self.background_1.size[0] * 2 - self.camera.viewport_width / 2 + if 0.0 > target_x: + target_x = 0.0 + elif target_x > self.background_1.size[0] * 2: + target_x = self.background_1.size[0] * 2 - if -self.camera.viewport_height / 2 > target_y: - target_y = -self.camera.viewport_height / 2 - elif target_y > self.background_1.size[1] - self.camera.viewport_height / 2: - target_y = self.background_1.size[1] - self.camera.viewport_height / 2 + if 0.0 > target_y: + target_y = 0.0 + elif target_y > self.background_1.size[1]: + target_y = self.background_1.size[1] - self.camera.move_to((target_x, target_y), 0.1) + arcade.camera.controllers.simple_follow_2D(0.1, (target_x, target_y), self.camera.view_data) def on_update(self, delta_time: float): new_position = ( @@ -120,7 +120,7 @@ def on_key_release(self, symbol: int, modifiers: int): def on_resize(self, width: int, height: int): super().on_resize(width, height) - self.camera.resize(width, height) + self.camera.match_screen(and_projection=True) # This is to ensure the background covers the entire screen. self.background_1.size = (width, height) diff --git a/arcade/examples/background_groups.py b/arcade/examples/background_groups.py index 2d45aecbb..a4607b170 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.camera.SimpleCamera() + self.camera = arcade.camera.Camera2D() # create a background group which will hold all the backgrounds. self.backgrounds = background.BackgroundGroup() @@ -66,21 +66,21 @@ def __init__(self): def pan_camera_to_player(self): # This will center the camera on the player. - target_x = self.player_sprite.center_x - (self.camera.viewport_width / 2) - target_y = self.player_sprite.center_y - (self.camera.viewport_height / 2) + target_x = self.player_sprite.center_x + target_y = self.player_sprite.center_y # This ensures the background is almost always at least partially visible. - if -self.camera.viewport_width / 2 > target_x: - target_x = -self.camera.viewport_width / 2 - elif target_x > 1.5 * self.camera.viewport_width: - target_x = 1.5 * self.camera.viewport_width + if 0.0 > target_x: + target_x = 0.0 + elif target_x > 2.0 * self.camera.viewport_width: + target_x = 2.0 * self.camera.viewport_width - if -self.camera.viewport_height / 2 > target_y: - target_y = -self.camera.viewport_height / 2 - elif target_y > 1.5 * self.camera.viewport_height: - target_y = 1.5 * self.camera.viewport_height + if 0.0 > target_y: + target_y = 0.0 + elif target_y > 2.0 * self.camera.viewport_height: + target_y = 2.0 * self.camera.viewport_height - self.camera.move_to((target_x, target_y), 0.1) + arcade.camera.controllers.simple_follow_2D(0.5, (target_x, target_y), self.camera.view_data) def on_update(self, delta_time: float): new_position = ( @@ -121,7 +121,7 @@ def on_key_release(self, symbol: int, modifiers: int): def on_resize(self, width: int, height: int): super().on_resize(width, height) - self.camera.resize(width, height) + self.camera.match_screen(and_projection=True) def main(): diff --git a/arcade/examples/background_parallax.py b/arcade/examples/background_parallax.py index 8ded10309..09aebaf93 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.camera.SimpleCamera() + self.camera = arcade.camera.Camera2D() # Create a background group to hold all the landscape's layers self.backgrounds = background.ParallaxGroup() @@ -89,8 +89,8 @@ def __init__(self): def pan_camera_to_player(self): # Move the camera toward the center of the player's sprite - target_x = self.player_sprite.center_x - (self.camera.viewport_width / 2) - self.camera.move_to((target_x, 0.0), 0.1) + target_x = self.player_sprite.center_x + arcade.camera.controllers.simple_follow_2D(0.1, (target_x, 0.0), self.camera.view_data) def on_update(self, delta_time: float): # Move the player in our infinite world @@ -147,7 +147,7 @@ def on_key_release(self, symbol: int, modifiers: int): def on_resize(self, width: int, height: int): super().on_resize(width, height) - self.camera.resize(width, height) + self.camera.match_screen(and_projection=True) full_width_size = (width, SCALED_BG_LAYER_HEIGHT_PX) # We can iterate through a background group, diff --git a/arcade/examples/background_scrolling.py b/arcade/examples/background_scrolling.py index aeeecaa90..76b1cefa3 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.camera.SimpleCamera() + self.camera = arcade.camera.Camera2D() # Load the background from file. Sized to match the screen self.background = background.Background.from_file( @@ -42,10 +42,9 @@ def __init__(self): def pan_camera_to_player(self): # This will center the camera on the player. - target_x = self.player_sprite.center_x - (self.camera.viewport_width / 2) - target_y = self.player_sprite.center_y - (self.camera.viewport_height / 2) - - self.camera.move_to((target_x, target_y), 0.05) + target_x = self.player_sprite.center_x + target_y = self.player_sprite.center_y + arcade.camera.controllers.simple_follow_2D(0.5, (target_x, target_y), self.camera.view_data) def on_update(self, delta_time: float): new_position = ( @@ -92,7 +91,7 @@ def on_key_release(self, symbol: int, modifiers: int): def on_resize(self, width: int, height: int): super().on_resize(width, height) - self.camera.resize(width, height) + self.camera.match_screen(and_projection=True) # This is to ensure the background covers the entire screen. self.background.size = (width, height) diff --git a/arcade/examples/background_stationary.py b/arcade/examples/background_stationary.py index 55f67d8a1..95cd25182 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.camera.SimpleCamera() + self.camera = arcade.camera.Camera2D() # Load the background from file. It defaults to the size of the texture with the bottom left corner at (0, 0). # Image from: @@ -42,21 +42,21 @@ def __init__(self): def pan_camera_to_player(self): # This will center the camera on the player. - target_x = self.player_sprite.center_x - (self.camera.viewport_width / 2) - target_y = self.player_sprite.center_y - (self.camera.viewport_height / 2) + target_x = self.player_sprite.center_x + target_y = self.player_sprite.center_y # This ensures the background is always at least partially visible. - if -self.camera.viewport_width / 2 > target_x: - target_x = -self.camera.viewport_width / 2 - elif target_x > self.background.size[0] - self.camera.viewport_width / 2: - target_x = self.background.size[0] - self.camera.viewport_width / 2 + if 0.0 > target_x: + target_x = 0.0 + elif target_x > self.background.size[0]: + target_x = self.background.size[0] - if -self.camera.viewport_height / 2 > target_y: - target_y = -self.camera.viewport_height / 2 - elif target_y > self.background.size[1] - self.camera.viewport_height / 2: - target_y = self.background.size[1] - self.camera.viewport_height / 2 + if 0.0 > target_y: + target_y = 0.0 + elif target_y > self.background.size[1]: + target_y = self.background.size[1] - self.camera.move_to((target_x, target_y), 0.1) + arcade.camera.controllers.simple_follow_2D(0.1, (target_x, target_y), self.camera.view_data) def on_update(self, delta_time: float): new_position = ( diff --git a/arcade/examples/camera_platform.py b/arcade/examples/camera_platform.py index 19ca3dbd9..36b6c179c 100644 --- a/arcade/examples/camera_platform.py +++ b/arcade/examples/camera_platform.py @@ -135,7 +135,7 @@ def setup(self): self.camera = arcade.camera.Camera2D(viewport=viewport) self.gui_camera = arcade.camera.Camera2D(viewport=viewport) - self.camera_shake = arcade.camera.controllers.ScreenShakeController(self.camera.data, + self.camera_shake = arcade.camera.controllers.ScreenShakeController(self.camera.view_data, max_amplitude=12.5, acceleration_duration=0.05, falloff_time=0.20, diff --git a/arcade/examples/minimap.py b/arcade/examples/minimap.py index 6add0430e..a158b1593 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.camera.SimpleCamera(viewport=viewport) - self.camera_gui = arcade.camera.SimpleCamera(viewport=viewport) + self.camera_sprites = arcade.camera.Camera2D(viewport=viewport) + self.camera_gui = arcade.camera.Camera2D(viewport=viewport) def setup(self): """ Set up the game and initialize the variables. """ diff --git a/arcade/examples/minimap_camera.py b/arcade/examples/minimap_camera.py index ad7575886..a23f1bb7d 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.SimpleCamera(viewport=minimap_viewport, projection=minimap_projection) + self.camera_minimap = arcade.camera.Camera2D(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.SimpleCamera(viewport=viewport, projection=projection) - self.camera_gui = arcade.camera.SimpleCamera(viewport=viewport) + self.camera_sprites = arcade.camera.Camera2D(viewport=viewport, projection=projection) + self.camera_gui = arcade.camera.Camera2D(viewport=viewport) self.selected_camera = self.camera_minimap diff --git a/arcade/examples/platform_tutorial/06_camera.py b/arcade/examples/platform_tutorial/06_camera.py index 38b752630..9fcbd40f2 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.camera.SimpleCamera(viewport=(0, 0, self.width, self.height)) + self.camera = arcade.camera.Camera2D(viewport=(0, 0, self.width, self.height)) # Initialize Scene self.scene = arcade.Scene() @@ -121,19 +121,17 @@ def on_key_release(self, key, modifiers): self.player_sprite.change_x = 0 def center_camera_to_player(self): - screen_center_x = self.player_sprite.center_x - (self.camera.viewport_width / 2) - screen_center_y = self.player_sprite.center_y - ( - self.camera.viewport_height / 2 - ) + screen_center_x = self.player_sprite.center_x + screen_center_y = self.player_sprite.center_y # Don't let camera travel past 0 - if screen_center_x < 0: - screen_center_x = 0 - if screen_center_y < 0: - screen_center_y = 0 + if screen_center_x - self.width/2 < 0: + screen_center_x = self.width/2 + if screen_center_y - self.height/2 < 0: + screen_center_y = self.height/2 player_centered = screen_center_x, screen_center_y - self.camera.move_to(player_centered) + arcade.camera.controllers.simple_follow_2D(0.2, player_centered, self.camera.view_data) def on_update(self, delta_time): """Movement and game logic""" diff --git a/arcade/examples/platform_tutorial/07_coins_and_sound.py b/arcade/examples/platform_tutorial/07_coins_and_sound.py index f0ec91192..b6c8408b9 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.camera.SimpleCamera(viewport=(0, 0, self.width, self.height)) + self.camera = arcade.camera.Camera2D(viewport=(0, 0, self.width, self.height)) # Initialize Scene self.scene = arcade.Scene() @@ -130,17 +130,17 @@ def on_key_release(self, key, modifiers): self.player_sprite.change_x = 0 def center_camera_to_player(self): - screen_center_x = self.player_sprite.center_x - (self.camera.viewport_width / 2) - screen_center_y = self.player_sprite.center_y - ( - self.camera.viewport_height / 2 - ) - if screen_center_x < 0: - screen_center_x = 0 - if screen_center_y < 0: - screen_center_y = 0 + screen_center_x = self.player_sprite.center_x + screen_center_y = self.player_sprite.center_y + + # Don't let camera travel past 0 + if screen_center_x - self.width/2 < 0: + screen_center_x = self.width/2 + if screen_center_y - self.height/2 < 0: + screen_center_y = self.height/2 player_centered = screen_center_x, screen_center_y - self.camera.move_to(player_centered) + arcade.camera.controllers.simple_follow_2D(0.2, player_centered, self.camera.view_data) def on_update(self, delta_time): """Movement and game logic""" diff --git a/arcade/examples/platform_tutorial/08_score.py b/arcade/examples/platform_tutorial/08_score.py index 588147037..732c6a1e7 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.camera.SimpleCamera(viewport=(0, 0, self.width, self.height)) + self.camera = arcade.camera.Camera2D(viewport=(0, 0, self.width, self.height)) # Set up the GUI Camera - self.gui_camera = arcade.camera.SimpleCamera(viewport=(0, 0, self.width, self.height)) + self.gui_camera = arcade.camera.Camera2D(viewport=(0, 0, self.width, self.height)) # Keep track of the score self.score = 0 @@ -155,17 +155,17 @@ def on_key_release(self, key, modifiers): self.player_sprite.change_x = 0 def center_camera_to_player(self): - screen_center_x = self.player_sprite.center_x - (self.camera.viewport_width / 2) - screen_center_y = self.player_sprite.center_y - ( - self.camera.viewport_height / 2 - ) - if screen_center_x < 0: - screen_center_x = 0 - if screen_center_y < 0: - screen_center_y = 0 + screen_center_x = self.player_sprite.center_x + screen_center_y = self.player_sprite.center_y + + # Don't let camera travel past 0 + if screen_center_x - self.width/2 < 0: + screen_center_x = self.width/2 + if screen_center_y - self.height/2 < 0: + screen_center_y = self.height/2 player_centered = screen_center_x, screen_center_y - self.camera.move_to(player_centered) + arcade.camera.controllers.simple_follow_2D(0.2, player_centered, self.camera.view_data) def on_update(self, delta_time): """Movement and game logic""" diff --git a/arcade/examples/platform_tutorial/09_load_map.py b/arcade/examples/platform_tutorial/09_load_map.py index 253a16b4b..ade33569f 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.camera.SimpleCamera(viewport=viewport) - self.gui_camera = arcade.camera.SimpleCamera(viewport=viewport) + self.camera = arcade.camera.Camera2D(viewport=viewport) + self.gui_camera = arcade.camera.Camera2D(viewport=viewport) # Name of map file to load map_name = ":resources:tiled_maps/map.json" @@ -151,17 +151,17 @@ def on_key_release(self, key, modifiers): self.player_sprite.change_x = 0 def center_camera_to_player(self): - screen_center_x = self.player_sprite.center_x - (self.camera.viewport_width / 2) - screen_center_y = self.player_sprite.center_y - ( - self.camera.viewport_height / 2 - ) - if screen_center_x < 0: - screen_center_x = 0 - if screen_center_y < 0: - screen_center_y = 0 + screen_center_x = self.player_sprite.center_x + screen_center_y = self.player_sprite.center_y + + # Don't let camera travel past 0 + if screen_center_x - self.width/2 < 0: + screen_center_x = self.width/2 + if screen_center_y - self.height/2 < 0: + screen_center_y = self.height/2 player_centered = screen_center_x, screen_center_y - self.camera.move_to(player_centered) + arcade.camera.controllers.simple_follow_2D(0.2, player_centered, self.camera.view_data) def on_update(self, delta_time): """Movement and game logic""" diff --git a/arcade/examples/platform_tutorial/10_multiple_levels.py b/arcade/examples/platform_tutorial/10_multiple_levels.py index eee99e09a..e83d259b4 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.camera.SimpleCamera(viewport=viewport) - self.gui_camera = arcade.camera.SimpleCamera(viewport=viewport) + self.camera = arcade.camera.Camera2D(viewport=viewport) + self.gui_camera = arcade.camera.Camera2D(viewport=viewport) # Map name map_name = f":resources:tiled_maps/map2_level_{self.level}.json" @@ -192,17 +192,17 @@ def on_key_release(self, key, modifiers): self.player_sprite.change_x = 0 def center_camera_to_player(self): - screen_center_x = self.player_sprite.center_x - (self.camera.viewport_width / 2) - screen_center_y = self.player_sprite.center_y - ( - self.camera.viewport_height / 2 - ) - if screen_center_x < 0: - screen_center_x = 0 - if screen_center_y < 0: - screen_center_y = 0 + screen_center_x = self.player_sprite.center_x + screen_center_y = self.player_sprite.center_y + + # Don't let camera travel past 0 + if screen_center_x - self.width/2 < 0: + screen_center_x = self.width/2 + if screen_center_y - self.height/2 < 0: + screen_center_y = self.height/2 player_centered = screen_center_x, screen_center_y - self.camera.move_to(player_centered) + arcade.camera.controllers.simple_follow_2D(0.2, player_centered, self.camera.view_data) def on_update(self, delta_time): """Movement and game logic""" diff --git a/arcade/examples/platform_tutorial/11_ladders_and_more.py b/arcade/examples/platform_tutorial/11_ladders_and_more.py index 370989ed4..cabfbe211 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.camera.SimpleCamera(viewport=viewport) - self.gui_camera = arcade.camera.SimpleCamera(viewport=viewport) + self.camera = arcade.camera.Camera2D(viewport=viewport) + self.gui_camera = arcade.camera.Camera2D(viewport=viewport) # Map name map_name = ":resources:tiled_maps/map_with_ladders.json" @@ -190,17 +190,17 @@ def on_key_release(self, key, modifiers): self.player_sprite.change_x = 0 def center_camera_to_player(self): - screen_center_x = self.player_sprite.center_x - (self.camera.viewport_width / 2) - screen_center_y = self.player_sprite.center_y - ( - self.camera.viewport_height / 2 - ) - if screen_center_x < 0: - screen_center_x = 0 - if screen_center_y < 0: - screen_center_y = 0 + screen_center_x = self.player_sprite.center_x + screen_center_y = self.player_sprite.center_y + + # Don't let camera travel past 0 + if screen_center_x - self.width/2 < 0: + screen_center_x = self.width/2 + if screen_center_y - self.height/2 < 0: + screen_center_y = self.height/2 player_centered = screen_center_x, screen_center_y - self.camera.move_to(player_centered, 0.2) + arcade.camera.controllers.simple_follow_2D(0.2, player_centered, self.camera.view_data) def on_update(self, delta_time): """Movement and game logic""" diff --git a/arcade/examples/platform_tutorial/12_animate_character.py b/arcade/examples/platform_tutorial/12_animate_character.py index e9a756792..33fd3fdb1 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.camera.SimpleCamera(viewport=viewport) - self.gui_camera = arcade.camera.SimpleCamera(viewport=viewport) + self.camera = arcade.camera.Camera2D(viewport=viewport) + self.gui_camera = arcade.camera.Camera2D(viewport=viewport) # Map name map_name = ":resources:tiled_maps/map_with_ladders.json" @@ -349,17 +349,17 @@ def on_key_release(self, key, modifiers): self.process_keychange() def center_camera_to_player(self): - screen_center_x = self.player_sprite.center_x - (self.camera.viewport_width / 2) - screen_center_y = self.player_sprite.center_y - ( - self.camera.viewport_height / 2 - ) - if screen_center_x < 0: - screen_center_x = 0 - if screen_center_y < 0: - screen_center_y = 0 + screen_center_x = self.player_sprite.center_x + screen_center_y = self.player_sprite.center_y + + # Don't let camera travel past 0 + if screen_center_x - self.width/2 < 0: + screen_center_x = self.width/2 + if screen_center_y - self.height/2 < 0: + screen_center_y = self.height/2 player_centered = screen_center_x, screen_center_y - self.camera.move_to(player_centered, 0.2) + arcade.camera.controllers.simple_follow_2D(0.2, player_centered, self.camera.view_data) def on_update(self, delta_time): """Movement and game logic""" diff --git a/arcade/examples/platform_tutorial/13_add_enemies.py b/arcade/examples/platform_tutorial/13_add_enemies.py index 7b5783551..cb4da5fd0 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.camera.SimpleCamera(viewport=viewport) - self.gui_camera = arcade.camera.SimpleCamera(viewport=viewport) + self.camera = arcade.camera.Camera2D(viewport=viewport) + self.gui_camera = arcade.camera.Camera2D(viewport=viewport) # Map name map_name = ":resources:tiled_maps/map_with_ladders.json" @@ -390,17 +390,17 @@ def on_key_release(self, key, modifiers): self.process_keychange() def center_camera_to_player(self): - screen_center_x = self.player_sprite.center_x - (self.camera.viewport_width / 2) - screen_center_y = self.player_sprite.center_y - ( - self.camera.viewport_height / 2 - ) - if screen_center_x < 0: - screen_center_x = 0 - if screen_center_y < 0: - screen_center_y = 0 + screen_center_x = self.player_sprite.center_x + screen_center_y = self.player_sprite.center_y + + # Don't let camera travel past 0 + if screen_center_x - self.width/2 < 0: + screen_center_x = self.width/2 + if screen_center_y - self.height/2 < 0: + screen_center_y = self.height/2 player_centered = screen_center_x, screen_center_y - self.camera.move_to(player_centered, 0.2) + arcade.camera.controllers.simple_follow_2D(0.2, player_centered, self.camera.view_data) def on_update(self, delta_time): """Movement and game logic""" diff --git a/arcade/examples/platform_tutorial/14_moving_enemies.py b/arcade/examples/platform_tutorial/14_moving_enemies.py index 3aa336289..a3e5a0be9 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.camera.SimpleCamera(viewport=viewport) - self.gui_camera = arcade.camera.SimpleCamera(viewport=viewport) + self.camera = arcade.camera.Camera2D(viewport=viewport) + self.gui_camera = arcade.camera.Camera2D(viewport=viewport) # Map name map_name = ":resources:tiled_maps/map_with_ladders.json" @@ -431,17 +431,17 @@ def on_key_release(self, key, modifiers): self.process_keychange() def center_camera_to_player(self): - screen_center_x = self.player_sprite.center_x - (self.camera.viewport_width / 2) - screen_center_y = self.player_sprite.center_y - ( - self.camera.viewport_height / 2 - ) - if screen_center_x < 0: - screen_center_x = 0 - if screen_center_y < 0: - screen_center_y = 0 + screen_center_x = self.player_sprite.center_x + screen_center_y = self.player_sprite.center_y + + # Don't let camera travel past 0 + if screen_center_x - self.width/2 < 0: + screen_center_x = self.width/2 + if screen_center_y - self.height/2 < 0: + screen_center_y = self.height/2 player_centered = screen_center_x, screen_center_y - self.camera.move_to(player_centered, 0.2) + arcade.camera.controllers.simple_follow_2D(0.2, player_centered, self.camera.view_data) def on_update(self, delta_time): """Movement and game logic""" diff --git a/arcade/examples/platform_tutorial/15_collision_with_enemies.py b/arcade/examples/platform_tutorial/15_collision_with_enemies.py index 9185a2915..d08a472c0 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.camera.SimpleCamera(viewport=viewport) - self.gui_camera = arcade.camera.SimpleCamera(viewport=viewport) + self.camera = arcade.camera.Camera2D(viewport=viewport) + self.gui_camera = arcade.camera.Camera2D(viewport=viewport) # Map name map_name = ":resources:tiled_maps/map_with_ladders.json" @@ -430,18 +430,18 @@ def on_key_release(self, key, modifiers): self.process_keychange() - def center_camera_to_player(self, speed=0.2): - screen_center_x = self.player_sprite.center_x - (self.camera.viewport_width / 2) - screen_center_y = self.player_sprite.center_y - ( - self.camera.viewport_height / 2 - ) - if screen_center_x < 0: - screen_center_x = 0 - if screen_center_y < 0: - screen_center_y = 0 + def center_camera_to_player(self): + screen_center_x = self.player_sprite.center_x + screen_center_y = self.player_sprite.center_y + + # Don't let camera travel past 0 + if screen_center_x - self.width/2 < 0: + screen_center_x = self.width/2 + if screen_center_y - self.height/2 < 0: + screen_center_y = self.height/2 player_centered = screen_center_x, screen_center_y - self.camera.move_to(player_centered, speed) + arcade.camera.controllers.simple_follow_2D(0.2, player_centered, self.camera.view_data) def on_update(self, delta_time): """Movement and game logic""" diff --git a/arcade/examples/platform_tutorial/16_shooting_bullets.py b/arcade/examples/platform_tutorial/16_shooting_bullets.py index 1c226b822..8a34f277a 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.camera.SimpleCamera(viewport=viewport) - self.gui_camera = arcade.camera.SimpleCamera(viewport=viewport) + self.camera = arcade.camera.Camera2D(viewport=viewport) + self.gui_camera = arcade.camera.Camera2D(viewport=viewport) # Map name map_name = ":resources:tiled_maps/map_with_ladders.json" @@ -462,18 +462,18 @@ def on_key_release(self, key, modifiers): self.process_keychange() - def center_camera_to_player(self, speed=0.2): - screen_center_x = self.player_sprite.center_x - (self.camera.viewport_width / 2) - screen_center_y = self.player_sprite.center_y - ( - self.camera.viewport_height / 2 - ) - if screen_center_x < 0: - screen_center_x = 0 - if screen_center_y < 0: - screen_center_y = 0 + def center_camera_to_player(self): + screen_center_x = self.player_sprite.center_x + screen_center_y = self.player_sprite.center_y + + # Don't let camera travel past 0 + if screen_center_x - self.width/2 < 0: + screen_center_x = self.width/2 + if screen_center_y - self.height/2 < 0: + screen_center_y = self.height/2 player_centered = screen_center_x, screen_center_y - self.camera.move_to(player_centered, speed) + arcade.camera.controllers.simple_follow_2D(0.2, player_centered, self.camera.view_data) def on_update(self, delta_time): """Movement and game logic""" diff --git a/arcade/examples/platform_tutorial/17_views.py b/arcade/examples/platform_tutorial/17_views.py index ef3b82c5b..bbba5f74a 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.camera.SimpleCamera(viewport=viewport) - self.gui_camera = arcade.camera.SimpleCamera(viewport=viewport) + self.camera = arcade.camera.Camera2D(viewport=viewport) + self.gui_camera = arcade.camera.Camera2D(viewport=viewport) # Map name map_name = ":resources:tiled_maps/map_with_ladders.json" @@ -498,18 +498,18 @@ def on_mouse_scroll(self, x, y, scroll_x, scroll_y): except Exception: pass - def center_camera_to_player(self, speed=0.2): - screen_center_x = self.player_sprite.center_x - (self.camera.viewport_width / 2) - screen_center_y = self.player_sprite.center_y - ( - self.camera.viewport_height / 2 - ) - if screen_center_x < 0: - screen_center_x = 0 - if screen_center_y < 0: - screen_center_y = 0 - player_centered = (screen_center_x, screen_center_y) + def center_camera_to_player(self): + screen_center_x = self.player_sprite.center_x + screen_center_y = self.player_sprite.center_y + + # Don't let camera travel past 0 + if screen_center_x - self.window.width/2 < 0: + screen_center_x = self.window.width/2 + if screen_center_y - self.window.height/2 < 0: + screen_center_y = self.window.height/2 + player_centered = screen_center_x, screen_center_y - self.camera.move_to(player_centered, speed) + arcade.camera.controllers.simple_follow_2D(0.2, player_centered, self.camera.view_data) def on_update(self, delta_time): """Movement and game logic""" diff --git a/arcade/examples/procedural_caves_cellular.py b/arcade/examples/procedural_caves_cellular.py index 090d689c2..3d1012c46 100644 --- a/arcade/examples/procedural_caves_cellular.py +++ b/arcade/examples/procedural_caves_cellular.py @@ -11,7 +11,6 @@ import random import arcade import timeit -from pyglet.math import Vec2 # Sprite scaling. Make this larger, like 0.5 to zoom in and add # 'mystery' to what you can see. Make it smaller, like 0.1 to see @@ -155,8 +154,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.camera.SimpleCamera() - self.camera_gui = arcade.camera.SimpleCamera() + self.camera_sprites = arcade.camera.Camera2D() + self.camera_gui = arcade.camera.Camera2D() self.window.background_color = arcade.color.BLACK @@ -311,18 +310,17 @@ def scroll_to_player(self, speed=CAMERA_SPEED): pan. """ - position = Vec2(self.player_sprite.center_x - self.window.width / 2, - self.player_sprite.center_y - self.window.height / 2) - self.camera_sprites.move_to(position, speed) - self.camera_sprites.update() + position = (self.player_sprite.center_x, self.player_sprite.center_y) + arcade.camera.controllers.simple_follow_2D(speed, position, self.camera_sprites.view_data) def on_resize(self, width: int, height: int): """ Resize window Handle the user grabbing the edge and resizing the window. """ - self.camera_sprites.resize(width, height) - self.camera_gui.resize(width, height) + super().on_resize(width, height) + self.camera_sprites.match_screen(and_projection=True) + self.camera_gui.match_screen(and_projection=True) def on_update(self, delta_time): """ Movement and game logic """ diff --git a/arcade/examples/sprite_move_scrolling.py b/arcade/examples/sprite_move_scrolling.py index 3203d4ee8..32de6539b 100644 --- a/arcade/examples/sprite_move_scrolling.py +++ b/arcade/examples/sprite_move_scrolling.py @@ -9,7 +9,6 @@ import random import arcade -from pyglet.math import Vec2 SPRITE_SCALING = 0.5 @@ -55,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.SimpleCamera() - self.camera_gui = arcade.camera.SimpleCamera() + self.camera_sprites = arcade.camera.Camera2D() + self.camera_gui = arcade.camera.Camera2D() def setup(self): """ Set up the game and initialize the variables. """ @@ -169,17 +168,18 @@ def scroll_to_player(self): pan. """ - position = Vec2(self.player_sprite.center_x - self.width / 2, - self.player_sprite.center_y - self.height / 2) - self.camera_sprites.move_to(position, CAMERA_SPEED) + position = (self.player_sprite.center_x, + self.player_sprite.center_y) + arcade.camera.controllers.simple_follow_2D(CAMERA_SPEED, position, self.camera_sprites.view_data) def on_resize(self, width: int, height: int): """ Resize window Handle the user grabbing the edge and resizing the window. """ - self.camera_sprites.resize(width, height) - self.camera_gui.resize(width, height) + super().on_resize(width, height) + self.camera_sprites.match_screen(and_projection=True) + self.camera_gui.match_screen(and_projection=True) def main(): diff --git a/arcade/examples/sprite_move_scrolling_box.py b/arcade/examples/sprite_move_scrolling_box.py index 4dfddbfe8..9b5c23af2 100644 --- a/arcade/examples/sprite_move_scrolling_box.py +++ b/arcade/examples/sprite_move_scrolling_box.py @@ -45,18 +45,14 @@ def __init__(self, width, height, title): self.physics_engine = None - # Used in scrolling - self.view_bottom = 0 - self.view_left = 0 - # Track the current state of what key is pressed self.left_pressed = False self.right_pressed = False self.up_pressed = False self.down_pressed = False - self.camera_sprites = arcade.camera.SimpleCamera() - self.camera_gui = arcade.camera.SimpleCamera() + self.camera_sprites = arcade.camera.Camera2D() + self.camera_gui = arcade.camera.Camera2D() def setup(self): """ Set up the game and initialize the variables. """ @@ -87,11 +83,6 @@ def setup(self): # Set the background color self.background_color = arcade.color.AMAZON - # Set the viewport boundaries - # These numbers set where we have 'scrolled' to. - self.view_left = 0 - self.view_bottom = 0 - def on_draw(self): """ Render the screen. @@ -185,37 +176,40 @@ def scroll_to_player(self): # --- Manage Scrolling --- + _target_x, _target_y = self.camera_sprites.position + # Scroll left - left_boundary = self.view_left + VIEWPORT_MARGIN + left_boundary = self.camera_sprites.left + VIEWPORT_MARGIN if self.player_sprite.left < left_boundary: - self.view_left -= left_boundary - self.player_sprite.left + _target_x -= left_boundary - self.player_sprite.left # Scroll right right_boundary = self.view_left + self.width - VIEWPORT_MARGIN if self.player_sprite.right > right_boundary: - self.view_left += self.player_sprite.right - right_boundary + _target_x += self.player_sprite.right - right_boundary # Scroll up top_boundary = self.view_bottom + self.height - VIEWPORT_MARGIN if self.player_sprite.top > top_boundary: - self.view_bottom += self.player_sprite.top - top_boundary + _target_y += self.player_sprite.top - top_boundary # Scroll down bottom_boundary = self.view_bottom + VIEWPORT_MARGIN if self.player_sprite.bottom < bottom_boundary: - self.view_bottom -= bottom_boundary - self.player_sprite.bottom + _target_y -= bottom_boundary - self.player_sprite.bottom # Scroll to the proper location position = self.view_left, self.view_bottom - self.camera_sprites.move_to(position, CAMERA_SPEED) + arcade.camera.controllers.simple_follow_2D(CAMERA_SPEED, position, self.camera_sprites.view_data) def on_resize(self, width: int, height: int): """ Resize window Handle the user grabbing the edge and resizing the window. """ - self.camera_sprites.resize(width, height) - self.camera_gui.resize(width, height) + super().on_resize(width, height) + self.camera_sprites.match_screen(and_projection=True) + self.camera_gui.match_screen(and_projection=True) def main(): diff --git a/arcade/examples/sprite_move_scrolling_shake.py b/arcade/examples/sprite_move_scrolling_shake.py index 2e6746d24..967aaee22 100644 --- a/arcade/examples/sprite_move_scrolling_shake.py +++ b/arcade/examples/sprite_move_scrolling_shake.py @@ -54,8 +54,14 @@ 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.SimpleCamera() - self.camera_gui = arcade.camera.SimpleCamera() + self.camera_sprites = arcade.camera.Camera2D() + self.camera_gui = arcade.camera.Camera2D() + + self.camera_shake = arcade.camera.controllers.ScreenShakeController(self.camera.view_data, + max_amplitude=12.5, + acceleration_duration=0.05, + falloff_time=0.20, + shake_frequency=15.0) self.explosion_sound = arcade.load_sound(":resources:sounds/explosion1.wav") @@ -108,6 +114,7 @@ def on_draw(self): self.clear() # Select the camera we'll use to draw all our sprites + self.camera_shake.update_camera() self.camera_sprites.use() # Draw all the sprites. @@ -141,6 +148,7 @@ def on_update(self, delta_time): # Call update on all sprites (The sprites don't do much in this # example though.) self.physics_engine.update() + self.camera_shake.update(delta_time) # Scroll the screen to the player self.scroll_to_player() @@ -151,25 +159,7 @@ def on_update(self, delta_time): bomb.remove_from_sprite_lists() self.explosion_sound.play() - # --- Shake the camera --- - # Pick a random direction - # shake_direction = random.random() * 2 * math.pi - # 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 - # ) - # Frequency of the shake - # shake_speed = 1.5 - # How fast to damp the shake - # shake_damping = 0.9 - # Do the shake - # TODO: Camera missing shake. - # self.camera_sprites.shake(shake_vector, - # speed=shake_speed, - # damping=shake_damping) + self.camera_shake.start() def scroll_to_player(self): """ @@ -181,18 +171,19 @@ def scroll_to_player(self): """ position = ( - self.player_sprite.center_x - self.width / 2, - self.player_sprite.center_y - self.height / 2 + self.player_sprite.center_x, + self.player_sprite.center_y ) - self.camera_sprites.move_to(position, CAMERA_SPEED) + arcade.camera.controllers.simple_follow_2D(CAMERA_SPEED, position, self.camera_sprites.view_data) def on_resize(self, width: int, height: int): """ Resize window Handle the user grabbing the edge and resizing the window. """ - self.camera_sprites.resize(width, height) - self.camera_gui.resize(width, height) + super().on_resize(width, height) + self.camera_sprites.match_screen(and_projection=True) + self.camera_gui.match_screen(and_projection=True) def main(): diff --git a/arcade/examples/sprite_moving_platforms.py b/arcade/examples/sprite_moving_platforms.py index 623031c47..2918b11fe 100644 --- a/arcade/examples/sprite_moving_platforms.py +++ b/arcade/examples/sprite_moving_platforms.py @@ -7,7 +7,6 @@ python -m arcade.examples.sprite_moving_platforms """ import arcade -from pyglet.math import Vec2 SPRITE_SCALING = 0.5 @@ -55,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.SimpleCamera() - self.camera_gui = arcade.camera.SimpleCamera() + self.camera_sprites = arcade.camera.Camera2D() + self.camera_gui = arcade.camera.Camera2D() self.left_down = False self.right_down = False @@ -205,9 +204,8 @@ def scroll_to_player(self): pan. """ - position = Vec2(self.player_sprite.center_x - self.width / 2, - self.player_sprite.center_y - self.height / 2) - self.camera_sprites.move_to(position, CAMERA_SPEED) + position = (self.player_sprite.center_x, self.player_sprite.center_y) + arcade.camera.controllers.simple_follow_2D(CAMERA_SPEED, position, self.camera_sprites.view_data) def main(): diff --git a/arcade/examples/sprite_tiled_map.py b/arcade/examples/sprite_tiled_map.py index e16050aec..2aba918a7 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.camera.SimpleCamera() - self.gui_camera = arcade.camera.SimpleCamera() + self.camera = arcade.camera.Camera2D() + self.gui_camera = arcade.camera.Camera2D() # Center camera on user self.pan_camera_to_user() @@ -229,17 +229,15 @@ def pan_camera_to_user(self, panning_fraction: float = 1.0): """ Manage Scrolling """ # This spot would center on the user - screen_center_x = self.player_sprite.center_x - (self.camera.viewport_width / 2) - screen_center_y = self.player_sprite.center_y - ( - self.camera.viewport_height / 2 - ) - if screen_center_x < 0: - screen_center_x = 0 - if screen_center_y < 0: - screen_center_y = 0 + screen_center_x = self.player_sprite.center_x + screen_center_y = self.player_sprite.center_y + if screen_center_x - self.width/2 < 0: + screen_center_x = self.width/2 + if screen_center_y - self.height/2 < 0: + screen_center_y = self.height/2 user_centered = screen_center_x, screen_center_y - self.camera.move_to(user_centered, panning_fraction) + arcade.camera.controllers.simple_follow_2D(panning_fraction, user_centered, self.camera.view_data) def main(): diff --git a/arcade/examples/template_platformer.py b/arcade/examples/template_platformer.py index 950940699..d637991a6 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.camera.SimpleCamera() - self.camera_gui = arcade.camera.SimpleCamera() + self.camera_sprites = arcade.camera.Camera2D() + self.camera_gui = arcade.camera.Camera2D() # Name of map file to load map_name = ":resources:tiled_maps/map.json" @@ -170,18 +170,18 @@ def on_key_release(self, key, modifiers): def center_camera_to_player(self): # Find where player is, then calculate lower left corner from that - screen_center_x = self.player_sprite.center_x - (self.camera_sprites.viewport_width / 2) - screen_center_y = self.player_sprite.center_y - (self.camera_sprites.viewport_height / 2) + screen_center_x = self.player_sprite.center_x + screen_center_y = self.player_sprite.center_y # Set some limits on how far we scroll - if screen_center_x < 0: - screen_center_x = 0 - if screen_center_y < 0: - screen_center_y = 0 + if screen_center_x - self.width/2 < 0: + screen_center_x = self.width/2 + if screen_center_y - self.height/2 < 0: + screen_center_y = self.height/2 # Here's our center, move to it player_centered = screen_center_x, screen_center_y - self.camera_sprites.move_to(player_centered) + arcade.camera.controllers.simple_follow_2D(0.1, player_centered, self.camera_sprites.view_data) def on_update(self, delta_time): """Movement and game logic""" @@ -206,8 +206,9 @@ def on_update(self, delta_time): def on_resize(self, width: int, height: int): """ Resize window """ - self.camera_sprites.resize(width, height) - self.camera_gui.resize(width, height) + super().on_resize(width, height) + self.camera_sprites.match_screen(and_projection=True) + self.camera_gui.match_screen(and_projection=True) def main(): diff --git a/tests/unit/camera/test_orthographic_camera.py b/tests/unit/camera/test_orthographic_camera.py index c902fa909..409cc99ab 100644 --- a/tests/unit/camera/test_orthographic_camera.py +++ b/tests/unit/camera/test_orthographic_camera.py @@ -78,18 +78,6 @@ def test_orthographic_camera(window: Window): assert window.current_camera == default_camera -def test_orthographic_projection_matrix(window: Window): - cam_default = camera.OrthographicProjector() - default_view = cam_default.view - default_projection = cam_default.projection - - -def test_orthographic_view_matrix(window: Window): - cam_default = camera.OrthographicProjector() - default_view = cam_default.view - default_projection = cam_default.projection - - def test_orthographic_map_coordinates(window: Window): cam_default = camera.OrthographicProjector() default_view = cam_default.view diff --git a/tests/unit/camera/test_perspective_camera.py b/tests/unit/camera/test_perspective_camera.py index d359ba183..a0b28276a 100644 --- a/tests/unit/camera/test_perspective_camera.py +++ b/tests/unit/camera/test_perspective_camera.py @@ -5,13 +5,5 @@ def test_perspective_camera(): pass -def test_perspective_projection_matrix(): - pass - - -def test_perspective_view_matrix(): - pass - - def test_perspective_map_coordinates(): pass From e2fb24734db512ed1e7b5e7748e14268d19a7d9a Mon Sep 17 00:00:00 2001 From: DragonMoffon Date: Wed, 11 Oct 2023 19:12:04 +1300 Subject: [PATCH 47/94] Fixing integration test errors, and examples. Did not get to all examples --- arcade/camera/orthographic.py | 2 +- arcade/camera/perspective.py | 2 +- arcade/examples/astar_pathfinding.py | 37 +++++----------- arcade/examples/background_blending.py | 4 +- arcade/examples/background_parallax.py | 8 ++-- arcade/examples/background_scrolling.py | 4 +- arcade/examples/background_stationary.py | 2 +- arcade/examples/minimap.py | 11 +++-- arcade/examples/minimap_camera.py | 17 ++++--- arcade/examples/sprite_move_scrolling_box.py | 8 ++-- .../examples/sprite_move_scrolling_shake.py | 10 ++--- arcade/gui/ui_manager.py | 12 ++--- arcade/gui/widgets/buttons.py | 1 - arcade/texture_atlas/base.py | 44 +++++++++++-------- doc/tutorials/raycasting/step_08.py | 9 ++-- 15 files changed, 82 insertions(+), 89 deletions(-) diff --git a/arcade/camera/orthographic.py b/arcade/camera/orthographic.py index b4c975341..b0774c35a 100644 --- a/arcade/camera/orthographic.py +++ b/arcade/camera/orthographic.py @@ -143,7 +143,7 @@ def use(self): @contextmanager def activate(self) -> Iterator[Projector]: """ - A context manager version of Camera2DOrthographic.use() which allows for the use of + A context manager version of OrthographicProjector.use() which allows for the use of `with` blocks. For example, `with camera.activate() as cam: ...`. """ previous_projector = self._window.current_camera diff --git a/arcade/camera/perspective.py b/arcade/camera/perspective.py index a6c905fb9..2b9714930 100644 --- a/arcade/camera/perspective.py +++ b/arcade/camera/perspective.py @@ -128,7 +128,7 @@ def use(self): @contextmanager def activate(self) -> Iterator[Projector]: """ - A context manager version of Camera2DOrthographic.use() which allows for the use of + A context manager version of PerspectiveProjector.use() which allows for the use of `with` blocks. For example, `with camera.activate() as cam: ...`.. """ previous_projector = self._window.current_camera diff --git a/arcade/examples/astar_pathfinding.py b/arcade/examples/astar_pathfinding.py index 17ca72606..7912e9b6b 100644 --- a/arcade/examples/astar_pathfinding.py +++ b/arcade/examples/astar_pathfinding.py @@ -59,10 +59,6 @@ def __init__(self, width, height, title): # List of points we checked to see if there is a barrier there self.barrier_list = None - # Used in scrolling - self.view_bottom = 0 - self.view_left = 0 - # Set the window background color self.background_color = arcade.color.AMAZON @@ -150,6 +146,8 @@ def on_draw(self): """ Render the screen. """ + self.cam.use() + # This command has to happen before we start drawing self.clear() @@ -197,44 +195,29 @@ def on_update(self, delta_time): changed = False # Scroll left - left_boundary = self.view_left + VIEWPORT_MARGIN + left_boundary = self.cam.left + VIEWPORT_MARGIN if self.player.left < left_boundary: - self.view_left -= left_boundary - self.player.left - changed = True + self.cam.left -= left_boundary - self.player.left # Scroll right - right_boundary = self.view_left + SCREEN_WIDTH - VIEWPORT_MARGIN + right_boundary = self.cam.right - VIEWPORT_MARGIN if self.player.right > right_boundary: - self.view_left += self.player.right - right_boundary - changed = True + self.cam.right += self.player.right - right_boundary # Scroll up - top_boundary = self.view_bottom + SCREEN_HEIGHT - VIEWPORT_MARGIN + top_boundary = self.cam.top - VIEWPORT_MARGIN if self.player.top > top_boundary: - self.view_bottom += self.player.top - top_boundary - changed = True + self.cam.top += self.player.top - top_boundary # Scroll down - bottom_boundary = self.view_bottom + VIEWPORT_MARGIN + bottom_boundary = self.cam.bottom + VIEWPORT_MARGIN if self.player.bottom < bottom_boundary: - self.view_bottom -= bottom_boundary - self.player.bottom - changed = True + self.cam.bottom -= bottom_boundary - self.player.bottom # Make sure our boundaries are integer values. While the view port does # support floating point numbers, for this application we want every pixel # in the view port to map directly onto a pixel on the screen. We don't want # any rounding errors. - self.view_left = int(self.view_left) - self.view_bottom = int(self.view_bottom) - - # If we changed the boundary values, update the view port to match - if changed: - self.cam.projection = ( - self.view_left, - SCREEN_WIDTH + self.view_left, - self.view_bottom, - SCREEN_HEIGHT + self.view_bottom) - self.cam.use() def on_key_press(self, key, modifiers): """Called whenever a key is pressed. """ diff --git a/arcade/examples/background_blending.py b/arcade/examples/background_blending.py index 6c8bee47b..1f16e0924 100644 --- a/arcade/examples/background_blending.py +++ b/arcade/examples/background_blending.py @@ -87,8 +87,8 @@ def on_draw(self): self.camera.use() # Ensure the background aligns with the camera - self.background_1.pos = self.camera.position - self.background_2.pos = self.camera.position + self.background_1.pos = self.camera.left, self.camera.bottom + self.background_2.pos = self.camera.left, self.camera.bottom # Offset the background texture. self.background_1.texture.offset = self.camera.position diff --git a/arcade/examples/background_parallax.py b/arcade/examples/background_parallax.py index 09aebaf93..bc9c8412a 100644 --- a/arcade/examples/background_parallax.py +++ b/arcade/examples/background_parallax.py @@ -80,7 +80,7 @@ def __init__(self): # Create & position the player sprite in the center of the camera's view self.player_sprite = arcade.Sprite( ":resources:/images/miami_synth_parallax/car/car-idle.png", - center_x=self.camera.viewport_width // 2, scale=PIXEL_SCALE + center_x=self.camera.viewport_width // 2, center_y=-200.0, scale=PIXEL_SCALE ) self.player_sprite.bottom = 0 @@ -90,7 +90,7 @@ def __init__(self): def pan_camera_to_player(self): # Move the camera toward the center of the player's sprite target_x = self.player_sprite.center_x - arcade.camera.controllers.simple_follow_2D(0.1, (target_x, 0.0), self.camera.view_data) + arcade.camera.controllers.simple_follow_2D(0.1, (target_x, self.height//2), self.camera.view_data) def on_update(self, delta_time: float): # Move the player in our infinite world @@ -109,8 +109,8 @@ def on_draw(self): # Fake an endless world with scrolling terrain # Try experimenting with commenting out 1 or both of the 2 lines # below to get an intuitive understanding of what each does! - bg.offset = self.camera.position # Fake depth by moving layers - bg.pos = self.camera.position # Follow the car to fake infinity + bg.offset = self.camera.left, self.camera.bottom # Fake depth by moving layers + bg.pos = self.camera.left, self.camera.bottom # Follow the car to fake infinity # Draw the background & the player's car bg.draw() diff --git a/arcade/examples/background_scrolling.py b/arcade/examples/background_scrolling.py index 76b1cefa3..32ba4aec6 100644 --- a/arcade/examples/background_scrolling.py +++ b/arcade/examples/background_scrolling.py @@ -61,10 +61,10 @@ def on_draw(self): self.camera.use() # Ensure the background aligns with the camera - self.background.pos = self.camera.position + self.background.pos = self.camera.left, self.camera.bottom # Offset the background texture. - self.background.texture.offset = self.camera.position + self.background.texture.offset = self.camera.left, self.camera.bottom self.background.draw() self.player_sprite.draw() diff --git a/arcade/examples/background_stationary.py b/arcade/examples/background_stationary.py index 95cd25182..4e5d517cf 100644 --- a/arcade/examples/background_stationary.py +++ b/arcade/examples/background_stationary.py @@ -97,7 +97,7 @@ def on_key_release(self, symbol: int, modifiers: int): def on_resize(self, width: int, height: int): super().on_resize(width, height) - self.camera.resize(width, height) + self.camera.match_screen(and_projection=True) def main(): diff --git a/arcade/examples/minimap.py b/arcade/examples/minimap.py index a158b1593..b6d360dc6 100644 --- a/arcade/examples/minimap.py +++ b/arcade/examples/minimap.py @@ -11,7 +11,6 @@ from uuid import uuid4 import arcade -from pyglet.math import Vec2 SPRITE_SCALING = 0.5 @@ -180,17 +179,17 @@ def scroll_to_player(self): """ # Scroll to the proper location - position = Vec2(self.player_sprite.center_x - self.width / 2, - self.player_sprite.center_y - self.height / 2) - self.camera_sprites.move_to(position, CAMERA_SPEED) + position = (self.player_sprite.center_x, self.player_sprite.center_y) + arcade.camera.controllers.simple_follow_2D(CAMERA_SPEED, position, self.camera_sprites.view_data) def on_resize(self, width: int, height: int): """ Resize window Handle the user grabbing the edge and resizing the window. """ - self.camera_sprites.resize(width, height) - self.camera_gui.resize(width, height) + super().on_resize(width, height) + self.camera_sprites.match_screen(and_projection=True) + self.camera_gui.match_screen(and_projection=True) def main(): diff --git a/arcade/examples/minimap_camera.py b/arcade/examples/minimap_camera.py index a23f1bb7d..3c0eaf4a6 100644 --- a/arcade/examples/minimap_camera.py +++ b/arcade/examples/minimap_camera.py @@ -55,7 +55,8 @@ def __init__(self, width, height, title): minimap_viewport = (DEFAULT_SCREEN_WIDTH - MINIMAP_WIDTH, DEFAULT_SCREEN_HEIGHT - MINIMAP_HEIGHT, MINIMAP_WIDTH, MINIMAP_HEIGHT) - minimap_projection = (0, MAP_PROJECTION_WIDTH, 0, MAP_PROJECTION_HEIGHT) + minimap_projection = (-MAP_PROJECTION_WIDTH/2, MAP_PROJECTION_WIDTH/2, + -MAP_PROJECTION_HEIGHT/2, MAP_PROJECTION_HEIGHT/2) self.camera_minimap = arcade.camera.Camera2D(viewport=minimap_viewport, projection=minimap_projection) # Set up the player @@ -65,8 +66,7 @@ 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.Camera2D(viewport=viewport, projection=projection) + self.camera_sprites = arcade.camera.Camera2D(viewport=viewport) self.camera_gui = arcade.camera.Camera2D(viewport=viewport) self.selected_camera = self.camera_minimap @@ -178,18 +178,21 @@ def on_update(self, delta_time): self.physics_engine.update() # Center the screen to the player - self.camera_sprites.center(self.player_sprite.position, CAMERA_SPEED) + arcade.camera.controllers.simple_follow_2D(CAMERA_SPEED, self.player_sprite.position, + self.camera_sprites.view_data) # Center the minimap viewport to the player in the minimap - self.camera_minimap.center(self.player_sprite.position, CAMERA_SPEED) + arcade.camera.controllers.simple_follow_2D(CAMERA_SPEED, self.player_sprite.position, + self.camera_minimap.view_data) def on_resize(self, width: int, height: int): """ Resize window Handle the user grabbing the edge and resizing the window. """ - self.camera_sprites.resize(width, height, resize_projection=False) - self.camera_gui.resize(width, height) + super().on_resize(width, height) + self.camera_sprites.match_screen() + self.camera_gui.match_screen() self.camera_minimap.viewport = (width - self.camera_minimap.viewport_width, height - self.camera_minimap.viewport_height, self.camera_minimap.viewport_width, diff --git a/arcade/examples/sprite_move_scrolling_box.py b/arcade/examples/sprite_move_scrolling_box.py index 9b5c23af2..3a5ed18ab 100644 --- a/arcade/examples/sprite_move_scrolling_box.py +++ b/arcade/examples/sprite_move_scrolling_box.py @@ -184,22 +184,22 @@ def scroll_to_player(self): _target_x -= left_boundary - self.player_sprite.left # Scroll right - right_boundary = self.view_left + self.width - VIEWPORT_MARGIN + right_boundary = self.camera_sprites.right - VIEWPORT_MARGIN if self.player_sprite.right > right_boundary: _target_x += self.player_sprite.right - right_boundary # Scroll up - top_boundary = self.view_bottom + self.height - VIEWPORT_MARGIN + top_boundary = self.camera_sprites.top - VIEWPORT_MARGIN if self.player_sprite.top > top_boundary: _target_y += self.player_sprite.top - top_boundary # Scroll down - bottom_boundary = self.view_bottom + VIEWPORT_MARGIN + bottom_boundary = self.camera_sprites.bottom + VIEWPORT_MARGIN if self.player_sprite.bottom < bottom_boundary: _target_y -= bottom_boundary - self.player_sprite.bottom # Scroll to the proper location - position = self.view_left, self.view_bottom + position = _target_x, _target_y arcade.camera.controllers.simple_follow_2D(CAMERA_SPEED, position, self.camera_sprites.view_data) def on_resize(self, width: int, height: int): diff --git a/arcade/examples/sprite_move_scrolling_shake.py b/arcade/examples/sprite_move_scrolling_shake.py index 967aaee22..106920167 100644 --- a/arcade/examples/sprite_move_scrolling_shake.py +++ b/arcade/examples/sprite_move_scrolling_shake.py @@ -57,11 +57,11 @@ def __init__(self, width, height, title): self.camera_sprites = arcade.camera.Camera2D() self.camera_gui = arcade.camera.Camera2D() - self.camera_shake = arcade.camera.controllers.ScreenShakeController(self.camera.view_data, - max_amplitude=12.5, - acceleration_duration=0.05, - falloff_time=0.20, - shake_frequency=15.0) + self.camera_shake = arcade.camera.controllers.ScreenShakeController(self.camera_sprites.view_data, + max_amplitude=15.0, + acceleration_duration=0.1, + falloff_time=0.5, + shake_frequency=10.0) self.explosion_sound = arcade.load_sound(":resources:sounds/explosion1.wav") diff --git a/arcade/gui/ui_manager.py b/arcade/gui/ui_manager.py index ea4b551ab..68f690ec7 100644 --- a/arcade/gui/ui_manager.py +++ b/arcade/gui/ui_manager.py @@ -318,8 +318,8 @@ def adjust_mouse_coordinates(self, x, y): It uses the internal camera's map_coordinate methods, and should work with all transformations possible with the basic orthographic camera. """ - - return self.window.current_camera.map_coordinate((x, y)) + print(self.window.current_camera) + return self.window.current_camera.map_coordinate((x, y))[:2] def on_event(self, event) -> Union[bool, None]: layers = sorted(self.children.keys(), reverse=True) @@ -338,7 +338,7 @@ def on_mouse_motion(self, x: float, y: float, dx: float, dy: float): return self.dispatch_ui_event(UIMouseMovementEvent(self, x, y, dx, dy)) # type: ignore def on_mouse_press(self, x: float, y: float, button: int, modifiers: int): - x, y = self.adjust_mouse_coordinates(x, y) + x, y = self.adjust_mouse_coordinates(x, y)[:2] return self.dispatch_ui_event(UIMousePressEvent(self, x, y, button, modifiers)) # type: ignore def on_mouse_drag( @@ -348,7 +348,7 @@ def on_mouse_drag( return self.dispatch_ui_event(UIMouseDragEvent(self, x, y, dx, dy, buttons, modifiers)) # type: ignore def on_mouse_release(self, x: float, y: float, button: int, modifiers: int): - x, y = self.adjust_mouse_coordinates(x, y) + x, y = self.adjust_mouse_coordinates(x, y)[:2] return self.dispatch_ui_event(UIMouseReleaseEvent(self, x, y, button, modifiers)) # type: ignore def on_mouse_scroll(self, x, y, scroll_x, scroll_y): @@ -374,7 +374,9 @@ def on_text_motion_select(self, motion): def on_resize(self, width, height): scale = self.window.get_pixel_ratio() - self.projector.projection.viewport = (0, 0, width, height) + _p = self.projector.projection + _p.viewport = (0.0, 0.0, width, height) + _p.left, _p.right, _p.bottom, _p.top = 0.0, width, 0.0, height for surface in self._surfaces.values(): surface.resize(size=(width, height), pixel_ratio=scale) diff --git a/arcade/gui/widgets/buttons.py b/arcade/gui/widgets/buttons.py index ffb063e9b..8cc884884 100644 --- a/arcade/gui/widgets/buttons.py +++ b/arcade/gui/widgets/buttons.py @@ -219,7 +219,6 @@ def apply_style(self, style: UITextureButtonStyle): self.ui_label.rect = self.ui_label.rect.max_size(self.content_width, self.content_height) - class UIFlatButton(UIInteractiveWidget, UIStyledWidget, UITextWidget): """ A text button, with support for background color and a border. diff --git a/arcade/texture_atlas/base.py b/arcade/texture_atlas/base.py index 9cfa9141d..0df5de31e 100644 --- a/arcade/texture_atlas/base.py +++ b/arcade/texture_atlas/base.py @@ -44,6 +44,7 @@ import arcade from arcade.gl.framebuffer import Framebuffer from arcade.texture.transforms import Transform +from arcade.camera import OrthographicProjector, CameraData, OrthographicProjectionData if TYPE_CHECKING: from arcade import ArcadeContext, Texture @@ -304,6 +305,18 @@ def __init__( self._image_uv_data_changed = True self._texture_uv_data_changed = True + #Render into camera + self._render_camera: OrthographicProjector = OrthographicProjector( + view=CameraData(), + projection=OrthographicProjectionData( + 0.0, self._fbo.width, + 0.0, self._fbo.height, + -100.0, 100.0, + (0, 0, int(self._fbo.width), int(self._fbo.height)) + ) + + ) + # Add all the textures for tex in textures or []: self.add(tex) @@ -870,8 +883,8 @@ def use_uv_texture(self, unit: int = 0) -> None: @contextmanager def render_into( - self, texture: "Texture", - projection: Optional[Tuple[float, float, float, float]] = None, + self, texture: "Texture", + projection: Optional[Tuple[float, float, float, float]] = None ): """ Render directly into a sub-section of the atlas. @@ -896,24 +909,19 @@ def render_into( This parameter can be left blank if no projection changes are needed. The tuple values are: (left, right, button, top) """ - prev_projection = self._ctx.projection_matrix region = self._texture_regions[texture.atlas_name] - # Use provided projection or default - projection = projection or (0, region.width, 0, region.height) - - self._ctx.projection_matrix = Mat4.orthogonal_projection(projection[0], projection[1], - projection[3], projection[2], - -100, 100) - - with self._fbo.activate() as fbo: - fbo.viewport = region.x, region.y, region.width, region.height - try: - yield fbo - finally: - fbo.viewport = 0, 0, *fbo.size - - self._ctx.projection_matrix = prev_projection + # Use provided projection or default, and flip for rendering into a texture. + _p = self._render_camera.projection + _p.left, _p.right, _p.top, _p.bottom = projection or (0, region.width, 0, region.height) + + with self._render_camera.activate(): + with self._fbo.activate() as fbo: + fbo.viewport = region.x, region.y, region.width, region.height + try: + yield fbo + finally: + fbo.viewport = 0, 0, *fbo.size @classmethod def create_from_texture_sequence(cls, textures: Sequence["Texture"], border: int = 1) -> "TextureAtlas": diff --git a/doc/tutorials/raycasting/step_08.py b/doc/tutorials/raycasting/step_08.py index 3341e890e..95bf03c2f 100644 --- a/doc/tutorials/raycasting/step_08.py +++ b/doc/tutorials/raycasting/step_08.py @@ -39,8 +39,8 @@ def __init__(self, width, height, title): self.physics_engine = None # Create cameras used for scrolling - self.camera_sprites = arcade.camera.SimpleCamera() - self.camera_gui = arcade.camera.SimpleCamera() + self.camera_sprites = arcade.camera.Camera2D() + self.camera_gui = arcade.camera.Camera2D() self.generate_sprites() @@ -187,9 +187,8 @@ def scroll_to_player(self, speed=CAMERA_SPEED): pan. """ - position = Vec2(self.player_sprite.center_x - self.width / 2, - self.player_sprite.center_y - self.height / 2) - self.camera_sprites.move_to(position, speed) + position = (self.player_sprite.center_x, self.player_sprite.center_y) + arcade.camera.controllers.simple_follow_2D(speed, position, self.camera.view_data) def on_resize(self, width: int, height: int): super().on_resize(width, height) From 190dc2fdf929e16e6f055ca1d6b028bfb38d0d39 Mon Sep 17 00:00:00 2001 From: DragonMoffon Date: Thu, 12 Oct 2023 15:55:50 +1300 Subject: [PATCH 48/94] attempting to fix gui part 1 --- arcade/gui/surface.py | 35 +++++++++++++++++++---------- doc/tutorials/raycasting/step_08.py | 12 +++++----- 2 files changed, 28 insertions(+), 19 deletions(-) diff --git a/arcade/gui/surface.py b/arcade/gui/surface.py index 23d32b470..a14538654 100644 --- a/arcade/gui/surface.py +++ b/arcade/gui/surface.py @@ -6,6 +6,7 @@ import arcade from arcade import Texture from arcade.color import TRANSPARENT_BLACK +from arcade.camera import OrthographicProjector, OrthographicProjectionData, CameraData from arcade.gl import Framebuffer from arcade.gui.nine_patch import NinePatchTexture from arcade.types import RGBA255, FloatRect, Point @@ -52,6 +53,16 @@ def __init__( fragment_shader=":system:shaders/gui/surface_fs.glsl", ) + self._cam = OrthographicProjector( + view=CameraData(), + projection=OrthographicProjectionData( + 0.0, self.width, + 0.0, self.height, + -100, 100, + (0, 0, self.width, self.height) + ) + ) + @property def position(self) -> Point: """Get or set the surface position""" @@ -133,23 +144,22 @@ def activate(self): Also resets the limit of the surface (viewport). """ # Set viewport and projection - proj = self.ctx.projection_matrix - view_port = self.ctx.viewport self.limit(0, 0, *self.size) # Set blend function blend_func = self.ctx.blend_func - self.ctx.blend_func = self.blend_func_render_into - with self.fbo.activate(): - yield self - - # Restore projection and blend function - self.ctx.projection_matrix = proj - self.ctx.viewport = view_port - self.ctx.blend_func = blend_func + try: + self.ctx.blend_func = self.blend_func_render_into + with self._cam.activate(): + with self.fbo.activate(): + yield self + finally: + # Restore blend function. + self.ctx.blend_func = blend_func def limit(self, x, y, width, height): """Reduces the draw area to the given rect""" + self.fbo.viewport = ( int(x * self._pixel_ratio), int(y * self._pixel_ratio), @@ -159,8 +169,9 @@ def limit(self, x, y, width, height): width = max(width, 1) height = max(height, 1) - self.ctx.projection_matrix = Mat4.orthogonal_projection(0, width, 0, height, -100, 100) - self.ctx.viewport = (0, 0, int(width), int(height)) + _p = self._cam.projection + _p.left, _p.right, _p.bottom, _p.top = 0, width, 0, height + self._cam.projection.viewport = (0, 0, int(width), int(height)) def draw( self, diff --git a/doc/tutorials/raycasting/step_08.py b/doc/tutorials/raycasting/step_08.py index 95bf03c2f..54eff0af5 100644 --- a/doc/tutorials/raycasting/step_08.py +++ b/doc/tutorials/raycasting/step_08.py @@ -103,8 +103,6 @@ def generate_sprites(self): # Start centered on the player self.scroll_to_player(1.0) - self.camera_sprites.update() - def on_draw(self): # Use our scrolled camera @@ -128,8 +126,8 @@ def on_draw(self): # Calculate the light position. We have to subtract the camera position # from the player position to get screen-relative coordinates. - p = (self.player_sprite.position[0] - self.camera_sprites.position[0], - self.player_sprite.position[1] - self.camera_sprites.position[1]) + p = (self.player_sprite.position[0] - self.camera_sprites.left, + self.player_sprite.position[1] - self.camera_sprites.bottom) # Set the uniform data self.shadertoy.program['lightPosition'] = p @@ -188,12 +186,12 @@ def scroll_to_player(self, speed=CAMERA_SPEED): """ position = (self.player_sprite.center_x, self.player_sprite.center_y) - arcade.camera.controllers.simple_follow_2D(speed, position, self.camera.view_data) + arcade.camera.controllers.simple_follow_2D(speed, position, self.camera_sprites.view_data) def on_resize(self, width: int, height: int): super().on_resize(width, height) - self.camera_sprites.resize(width, height) - self.camera_gui.resize(width, height) + self.camera_sprites.match_screen(and_projection=True) + self.camera_gui.match_screen(and_projection=True) self.shadertoy.resize((width, height)) From a70dcad6d886783b8da2b304f1d8a51e561b2a3e Mon Sep 17 00:00:00 2001 From: DragonMoffon Date: Tue, 17 Oct 2023 09:42:59 +1300 Subject: [PATCH 49/94] Fixed erroneous ui behaviour with new camera. --- arcade/examples/gui_flat_button.py | 1 - arcade/gui/surface.py | 14 ++++++++------ arcade/gui/ui_manager.py | 25 +++++++++---------------- 3 files changed, 17 insertions(+), 23 deletions(-) diff --git a/arcade/examples/gui_flat_button.py b/arcade/examples/gui_flat_button.py index 133fe5af3..c8c0ef8cc 100644 --- a/arcade/examples/gui_flat_button.py +++ b/arcade/examples/gui_flat_button.py @@ -22,7 +22,6 @@ def on_click(self, event: arcade.gui.UIOnClickEvent): class MyView(arcade.View): def __init__(self): super().__init__() - # --- Required for all code that uses UI element, # a UIManager to handle the UI. self.ui = arcade.gui.UIManager() diff --git a/arcade/gui/surface.py b/arcade/gui/surface.py index a14538654..e74d95d2d 100644 --- a/arcade/gui/surface.py +++ b/arcade/gui/surface.py @@ -10,7 +10,6 @@ from arcade.gl import Framebuffer from arcade.gui.nine_patch import NinePatchTexture from arcade.types import RGBA255, FloatRect, Point -from pyglet.math import Mat4 class Surface: @@ -150,9 +149,8 @@ def activate(self): try: self.ctx.blend_func = self.blend_func_render_into - with self._cam.activate(): - with self.fbo.activate(): - yield self + with self.fbo.activate(): + yield self finally: # Restore blend function. self.ctx.blend_func = blend_func @@ -160,18 +158,21 @@ def activate(self): def limit(self, x, y, width, height): """Reduces the draw area to the given rect""" - self.fbo.viewport = ( + viewport = ( int(x * self._pixel_ratio), int(y * self._pixel_ratio), int(width * self._pixel_ratio), int(height * self._pixel_ratio), ) + self.fbo.viewport = viewport width = max(width, 1) height = max(height, 1) _p = self._cam.projection _p.left, _p.right, _p.bottom, _p.top = 0, width, 0, height - self._cam.projection.viewport = (0, 0, int(width), int(height)) + self._cam.projection.viewport = viewport + + self._cam.use() def draw( self, @@ -214,3 +215,4 @@ def resize(self, *, size: Tuple[int, int], pixel_ratio: float) -> None: self.texture = self.ctx.texture(self.size_scaled, components=4) self.fbo = self.ctx.framebuffer(color_attachments=[self.texture]) self.fbo.clear() + diff --git a/arcade/gui/ui_manager.py b/arcade/gui/ui_manager.py index 68f690ec7..8f1eef4c8 100644 --- a/arcade/gui/ui_manager.py +++ b/arcade/gui/ui_manager.py @@ -32,7 +32,7 @@ ) from arcade.gui.surface import Surface from arcade.gui.widgets import UIWidget, Rect -from arcade.camera import OrthographicProjector, OrthographicProjectionData, CameraData +from arcade.camera import OrthographicProjector, OrthographicProjectionData, CameraData, Projector W = TypeVar("W", bound=UIWidget) @@ -92,11 +92,6 @@ 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.projector = OrthographicProjector( - view=CameraData(), - projection=OrthographicProjectionData(0, self.window.width, 0, self.window.height, -100, 100, - (0, 0, self.window.width, self.window.height)) - ) self.register_event_type("on_event") def add(self, widget: W, *, index=None, layer=0) -> W: @@ -296,6 +291,7 @@ def on_update(self, time_delta): return self.dispatch_ui_event(UIOnUpdateEvent(self, time_delta)) def draw(self) -> None: + current_cam = self.window.current_camera # Request Widgets to prepare for next frame self._do_layout() @@ -303,12 +299,14 @@ def draw(self) -> None: with ctx.enabled(ctx.BLEND): self._do_render() + # Correct that the ui changes the currently active camera. + current_cam.use() + # Draw layers - with self.projector.activate(): - with ctx.enabled(ctx.BLEND): - layers = sorted(self.children.keys()) - for layer in layers: - self._get_surface(layer).draw() + with ctx.enabled(ctx.BLEND): + layers = sorted(self.children.keys()) + for layer in layers: + self._get_surface(layer).draw() def adjust_mouse_coordinates(self, x, y): """ @@ -318,7 +316,6 @@ def adjust_mouse_coordinates(self, x, y): It uses the internal camera's map_coordinate methods, and should work with all transformations possible with the basic orthographic camera. """ - print(self.window.current_camera) return self.window.current_camera.map_coordinate((x, y))[:2] def on_event(self, event) -> Union[bool, None]: @@ -374,10 +371,6 @@ def on_text_motion_select(self, motion): def on_resize(self, width, height): scale = self.window.get_pixel_ratio() - _p = self.projector.projection - _p.viewport = (0.0, 0.0, width, height) - _p.left, _p.right, _p.bottom, _p.top = 0.0, width, 0.0, height - for surface in self._surfaces.values(): surface.resize(size=(width, height), pixel_ratio=scale) From cd13036638c97fbb5dccaa009a361e2eb2da765b Mon Sep 17 00:00:00 2001 From: DragonMoffon Date: Tue, 17 Oct 2023 09:45:17 +1300 Subject: [PATCH 50/94] fixed linting issues --- arcade/examples/astar_pathfinding.py | 4 ---- arcade/gui/ui_manager.py | 3 +-- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/arcade/examples/astar_pathfinding.py b/arcade/examples/astar_pathfinding.py index 7912e9b6b..cd0e05222 100644 --- a/arcade/examples/astar_pathfinding.py +++ b/arcade/examples/astar_pathfinding.py @@ -190,10 +190,6 @@ def on_update(self, delta_time): # --- Manage Scrolling --- - # Keep track of if we changed the boundary. We don't want to update the - # viewport or projection if we didn't change the view port. - changed = False - # Scroll left left_boundary = self.cam.left + VIEWPORT_MARGIN if self.player.left < left_boundary: diff --git a/arcade/gui/ui_manager.py b/arcade/gui/ui_manager.py index 8f1eef4c8..8756b1a45 100644 --- a/arcade/gui/ui_manager.py +++ b/arcade/gui/ui_manager.py @@ -32,7 +32,6 @@ ) from arcade.gui.surface import Surface from arcade.gui.widgets import UIWidget, Rect -from arcade.camera import OrthographicProjector, OrthographicProjectionData, CameraData, Projector W = TypeVar("W", bound=UIWidget) @@ -301,7 +300,7 @@ def draw(self) -> None: # Correct that the ui changes the currently active camera. current_cam.use() - + # Draw layers with ctx.enabled(ctx.BLEND): layers = sorted(self.children.keys()) From e40d2f763e3b5dde51a0c42f480b089a36c73d50 Mon Sep 17 00:00:00 2001 From: DragonMoffon Date: Sun, 10 Dec 2023 15:57:44 +1300 Subject: [PATCH 51/94] =?UTF-8?q?Fixing=20Issues=20from=20pvcraven=C2=B4s?= =?UTF-8?q?=20review?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit resolved pvcraven´s review. Also created a perspective camera demo. setup unit tests. --- arcade/camera/__init__.py | 3 +- arcade/camera/camera_2d.py | 2 +- arcade/camera/controllers/__init__.py | 2 + .../simple_controller_functions.py | 36 ++- arcade/camera/default.py | 8 +- arcade/camera/orthographic.py | 16 +- arcade/camera/perspective.py | 53 ++--- arcade/camera/simple_camera.py | 2 +- arcade/camera/types.py | 11 +- arcade/examples/camera_perspective_demo.py | 115 +++++++++ .../examples/platform_tutorial/06_camera.py | 5 +- .../platform_tutorial/07_coins_and_sound.py | 5 +- arcade/examples/platform_tutorial/08_score.py | 5 +- .../examples/platform_tutorial/09_load_map.py | 5 +- .../platform_tutorial/10_multiple_levels.py | 5 +- .../platform_tutorial/11_ladders_and_more.py | 5 +- .../platform_tutorial/12_animate_character.py | 5 +- .../platform_tutorial/13_add_enemies.py | 5 +- .../platform_tutorial/14_moving_enemies.py | 5 +- .../15_collision_with_enemies.py | 5 +- .../platform_tutorial/16_shooting_bullets.py | 5 +- arcade/examples/platform_tutorial/17_views.py | 5 +- doc/tutorials/lights/01_light_demo.py | 26 +- doc/tutorials/lights/light_demo.py | 28 +-- tests/unit/camera/test_camera_2d.py | 0 tests/unit/camera/test_camera_controllers.py | 3 + tests/unit/camera/test_default_camera.py | 0 tests/unit/camera/test_orthographic_camera.py | 225 ++++++++++-------- tests/unit/camera/test_perspective_camera.py | 122 +++++++++- tests/unit/camera/test_simple_camera.py | 0 30 files changed, 508 insertions(+), 204 deletions(-) create mode 100644 arcade/examples/camera_perspective_demo.py delete mode 100644 tests/unit/camera/test_camera_2d.py delete mode 100644 tests/unit/camera/test_default_camera.py delete mode 100644 tests/unit/camera/test_simple_camera.py diff --git a/arcade/camera/__init__.py b/arcade/camera/__init__.py index 8aa91c84b..431216a30 100644 --- a/arcade/camera/__init__.py +++ b/arcade/camera/__init__.py @@ -4,7 +4,7 @@ """ from arcade.camera.data import CameraData, OrthographicProjectionData, PerspectiveProjectionData -from arcade.camera.types import Projection, Projector, Camera +from arcade.camera.types import Projection, Projector from arcade.camera.orthographic import OrthographicProjector from arcade.camera.perspective import PerspectiveProjector @@ -17,7 +17,6 @@ __all__ = [ 'Projection', 'Projector', - 'Camera', 'CameraData', 'OrthographicProjectionData', 'OrthographicProjector', diff --git a/arcade/camera/camera_2d.py b/arcade/camera/camera_2d.py index 5f2b5a1d6..0a39762b1 100644 --- a/arcade/camera/camera_2d.py +++ b/arcade/camera/camera_2d.py @@ -52,7 +52,7 @@ def __init__(self, *, projection_data: Optional[OrthographicProjectionData] = None ): """ - Initialise a Camera2D instance. Either with camera PoDs or individual arguments. + Initialize a Camera2D instance. Either with camera PoDs or individual arguments. Args: window: The Arcade Window to bind the camera to. diff --git a/arcade/camera/controllers/__init__.py b/arcade/camera/controllers/__init__.py index 7787bf48e..eb4eaa6ef 100644 --- a/arcade/camera/controllers/__init__.py +++ b/arcade/camera/controllers/__init__.py @@ -3,6 +3,7 @@ simple_follow_2D, simple_easing_3D, simple_easing_2D, + strafe, quaternion_rotation, rotate_around_up, rotate_around_right, @@ -19,6 +20,7 @@ 'simple_follow_2D', 'simple_easing_3D', 'simple_easing_2D', + 'strafe', 'quaternion_rotation', 'rotate_around_up', 'rotate_around_right', diff --git a/arcade/camera/controllers/simple_controller_functions.py b/arcade/camera/controllers/simple_controller_functions.py index d7fbdb6e6..bfdb21c09 100644 --- a/arcade/camera/controllers/simple_controller_functions.py +++ b/arcade/camera/controllers/simple_controller_functions.py @@ -1,4 +1,4 @@ -from typing import Tuple, Callable +from typing import Tuple, Callable, Optional from math import sin, cos, radians from arcade.camera.data import CameraData @@ -10,6 +10,7 @@ 'simple_follow_2D', 'simple_easing_3D', 'simple_easing_2D', + 'strafe', 'quaternion_rotation', 'rotate_around_forward', 'rotate_around_up', @@ -17,6 +18,27 @@ ] +def strafe(data: CameraData, direction: Tuple[float, float]): + """ + Move the CameraData in a 2D direction aligned to the up-right plane of the view. + A value of [1, 0] will move the camera sideways while a value of [0, 1] + will move the camera upwards. Works irrespective of which direction the camera is facing. + Ensure the up and forward vectors are unit-length or the size of the motion will be incorrect. + """ + _forward = Vec3(*data.forward) + _up = Vec3(*data.up) + _right = _forward.cross(_up) + + _pos = data.position + + offset = _right * direction[0] + _up * direction[1] + data.position = ( + _pos[0] + offset[0], + _pos[1] + offset[1], + _pos[2] + offset[2] + ) + + def quaternion_rotation(axis: Tuple[float, float, float], vector: Tuple[float, float, float], angle: float) -> Tuple[float, float, float]: @@ -114,7 +136,7 @@ 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): +def rotate_around_right(data: CameraData, angle: float, forward: bool = True, up: bool = True): """ Rotate both the CameraData's forward vector and up vector around a calculated right vector. Generally only useful in 3D games. @@ -125,14 +147,20 @@ def rotate_around_right(data: CameraData, angle: float): The camera data to modify. The data's forward vector is rotated around its up vector angle: The angle in degrees to rotate clockwise by + forward: + Whether to rotate the forward vector around the right vector + up: + Whether to rotate the up vector around the right vector """ _forward = Vec3(data.forward[0], data.forward[1], data.forward[2]) _up = Vec3(data.up[0], data.up[1], data.up[2]) _crossed_vec = _forward.cross(_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) + if forward: + data.forward = quaternion_rotation(_right, data.forward, angle) + if up: + data.up = quaternion_rotation(_right, data.up, angle) def _interpolate_3D(s: Tuple[float, float, float], e: Tuple[float, float, float], t: float): diff --git a/arcade/camera/default.py b/arcade/camera/default.py index 27127867f..11b16d283 100644 --- a/arcade/camera/default.py +++ b/arcade/camera/default.py @@ -24,17 +24,17 @@ class ViewportProjector: def __init__(self, viewport: Optional[Tuple[int, int, int, int]] = None, *, window: Optional["Window"] = None): """ - Initialise a ViewportProjector + Initialize a ViewportProjector Args: viewport: The viewport to project to. - window: The window to bind the camera to. Defaults to the currently active camera. + window: The window to bind the camera to. Defaults to the currently active window. """ self._window = window or get_window() self._viewport = viewport or self._window.ctx.viewport - self._projection_matrix: Mat4 = Mat4.orthogonal_projection(0, self._viewport[2], - 0, self._viewport[3], + self._projection_matrix: Mat4 = Mat4.orthogonal_projection(0.0, self._viewport[2], + 0.0, self._viewport[3], -100, 100) @property diff --git a/arcade/camera/orthographic.py b/arcade/camera/orthographic.py index b0774c35a..a0cb38d5b 100644 --- a/arcade/camera/orthographic.py +++ b/arcade/camera/orthographic.py @@ -37,7 +37,7 @@ def __init__(self, *, view: Optional[CameraData] = None, projection: Optional[OrthographicProjectionData] = None): """ - Initialise a Projector which produces an orthographic projection matrix using + Initialize a Projector which produces an orthographic projection matrix using a CameraData and PerspectiveProjectionData PoDs. Args: @@ -112,10 +112,11 @@ def _generate_view_matrix(self) -> Mat4: """ Using the ViewData it generates a view matrix from the pyglet Mat4 look at function """ + # Even if forward and up are normalised floating point error means every vector must be normalised. 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 + up = Vec3(*self._view.up) # Initial Up Vector (Not necessarily perpendicular to forward vector) + ri = fo.cross(up).normalize() # Right Vector + up = ri.cross(fo).normalize() # Up Vector po = Vec3(*self._view.position) return Mat4(( ri.x, up.x, -fo.x, 0, @@ -153,7 +154,7 @@ def activate(self) -> Iterator[Projector]: finally: previous_projector.use() - def map_coordinate(self, screen_coordinate: Tuple[float, float]) -> Tuple[float, float, float]: + def map_coordinate(self, screen_coordinate: Tuple[float, float], depth: float = 0.0) -> Tuple[float, float, float]: """ Take in a pixel coordinate from within the range of the window size and returns @@ -164,16 +165,19 @@ def map_coordinate(self, screen_coordinate: Tuple[float, float]) -> Tuple[float, Args: screen_coordinate: A 2D position in pixels from the bottom left of the screen. This should ALWAYS be in the range of 0.0 - screen size. + depth: The depth of the query Returns: A 3D vector in world space. """ + # TODO: Integrate z-depth screen_x = 2.0 * (screen_coordinate[0] - self._projection.viewport[0]) / self._projection.viewport[2] - 1 screen_y = 2.0 * (screen_coordinate[1] - self._projection.viewport[1]) / self._projection.viewport[3] - 1 + screen_z = 2.0 * (depth - self._projection.near) / (self._projection.far - self._projection.near) - 1 _view = self._generate_view_matrix() _projection = self._generate_projection_matrix() - screen_position = Vec4(screen_x, screen_y, 0.0, 1.0) + screen_position = Vec4(screen_x, screen_y, screen_z, 1.0) _full = ~(_projection @ _view) diff --git a/arcade/camera/perspective.py b/arcade/camera/perspective.py index 2b9714930..9996ab73f 100644 --- a/arcade/camera/perspective.py +++ b/arcade/camera/perspective.py @@ -1,5 +1,6 @@ from typing import Optional, Tuple, Iterator, TYPE_CHECKING from contextlib import contextmanager +from math import pi, tan from pyglet.math import Mat4, Vec3, Vec4 @@ -37,7 +38,7 @@ def __init__(self, *, view: Optional[CameraData] = None, projection: Optional[PerspectiveProjectionData] = None): """ - Initialise a Projector which produces a perspective projection matrix using + Initialize a Projector which produces a perspective projection matrix using a CameraData and PerspectiveProjectionData PoDs. Args: @@ -98,9 +99,9 @@ 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 + up = Vec3(*self._view.up) # Initial Up Vector (Not necessarily perpendicular to forward vector) + ri = fo.cross(up).normalize() # Right Vector + up = ri.cross(fo).normalize() # Up Vector po = Vec3(*self._view.position) return Mat4(( ri.x, up.x, -fo.x, 0, @@ -138,7 +139,7 @@ def activate(self) -> Iterator[Projector]: finally: previous_projector.use() - def map_coordinate(self, screen_coordinate: Tuple[float, float]) -> Tuple[float, float, float]: + def map_coordinate(self, screen_coordinate: Tuple[float, float], depth: float = 0.0) -> Tuple[float, float, float]: """ Take in a pixel coordinate from within the range of the window size and returns @@ -146,53 +147,29 @@ def map_coordinate(self, screen_coordinate: Tuple[float, float]) -> Tuple[float, Essentially reverses the effects of the projector. - Args: - screen_coordinate: A 2D position in pixels from the bottom left of the screen. - This should ALWAYS be in the range of 0.0 - screen size. - Returns: - A 3D vector in world space. - """ - screen_x = 2.0 * (screen_coordinate[0] - self._projection.viewport[0]) / self._projection.viewport[2] - 1 - screen_y = 2.0 * (screen_coordinate[1] - self._projection.viewport[1]) / self._projection.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) - - _mapped_position = _full @ screen_position - - return _mapped_position[0], _mapped_position[1], _mapped_position[2] - - def map_coordinate_at_depth(self, - screen_coordinate: Tuple[float, float], - depth: float) -> Tuple[float, float, float]: - """ - Take in a pixel coordinate from within - the range of the window size and returns - the world space coordinates. - - Essentially reverses the effects of the projector. + Because the scale changes depending on the depth tested at, + the depth is calculated to be the point at which the projection area + matches the area of the camera viewport. This would be the depth at which + one pixel in a texture is one pixel on screen. Args: screen_coordinate: A 2D position in pixels from the bottom left of the screen. This should ALWAYS be in the range of 0.0 - screen size. - depth: the depth that the mouse should be compared against. - Should range from near to far planes of projection. + depth: The depth of the query. Returns: A 3D vector in world space. """ + # TODO Integrate Z-depth screen_x = 2.0 * (screen_coordinate[0] - self._projection.viewport[0]) / self._projection.viewport[2] - 1 screen_y = 2.0 * (screen_coordinate[1] - self._projection.viewport[1]) / self._projection.viewport[3] - 1 + screen_z = 2.0 * (depth - self._projection.near) / (self._projection.far - self._projection.near) - 1 _view = self._generate_view_matrix() _projection = self._generate_projection_matrix() - _depth = 2.0 * depth / (self._projection.far - self._projection.near) - 1 + print(screen_x, screen_y, screen_z) - screen_position = Vec4(screen_x, screen_y, _depth, 1.0) + screen_position = Vec4(screen_x, screen_y, screen_z, 1.0) _full = ~(_projection @ _view) diff --git a/arcade/camera/simple_camera.py b/arcade/camera/simple_camera.py index 57e01082c..a791682d0 100644 --- a/arcade/camera/simple_camera.py +++ b/arcade/camera/simple_camera.py @@ -40,7 +40,7 @@ def __init__(self, *, projection_data: Optional[OrthographicProjectionData] = None ): """ - Initialise a Simple Camera Instance with either Camera PoDs or individual arguments + Initialize a Simple Camera Instance with either Camera PoDs or individual arguments Args: window: The Arcade Window to bind the camera to. diff --git a/arcade/camera/types.py b/arcade/camera/types.py index e035881ca..821f918ad 100644 --- a/arcade/camera/types.py +++ b/arcade/camera/types.py @@ -6,8 +6,7 @@ __all__ = [ 'Projection', - 'Projector', - 'Camera' + 'Projector' ] @@ -26,11 +25,5 @@ def use(self) -> None: def activate(self) -> Iterator["Projector"]: ... - def map_coordinate(self, screen_coordinate: Tuple[float, float]) -> Tuple[float, ...]: + def map_coordinate(self, screen_coordinate: Tuple[float, float], depth: float = 0.0) -> Tuple[float, ...]: ... - - -class Camera(Protocol): - _view: CameraData - _projection: Projection - diff --git a/arcade/examples/camera_perspective_demo.py b/arcade/examples/camera_perspective_demo.py new file mode 100644 index 000000000..ddd6f8a91 --- /dev/null +++ b/arcade/examples/camera_perspective_demo.py @@ -0,0 +1,115 @@ +from array import array + +import arcade +import arcade.gl as gl + +# TODO: comment for days + +win = arcade.Window() +win.set_exclusive_mouse() +geo = gl.geometry.cube((0.5, 0.5, 0.5)) +prog = win.ctx.program( + vertex_shader="\n".join(( + "#version 330", + "uniform WindowBlock {", + " mat4 projection;", + " mat4 view;", + "} window;", + "in vec3 in_position;", + "in vec3 in_normal;", + "in vec2 in_uv;", + "", + "out vec3 vs_normal;", + "out vec2 vs_uv;", + "void main() {", + "gl_Position = window.projection * window.view * vec4(in_position, 1.0);", + "vs_normal = in_normal;", + "vs_uv = in_uv;", + "}" + )), + fragment_shader="\n".join(( + "#version 330", + "in vec3 vs_normal;", + "in vec2 vs_uv;", + "out vec4 fs_colour;", + "", + "void main() {", + "vec3 uv_colour = vec3(vs_uv, 0.2);", + "float light_intensity = 0.2 + max(0.0, dot(vs_normal, vec3(pow(3, -0.5), pow(3, -0.5), pow(3, -0.5))));", + "fs_colour = vec4(light_intensity * uv_colour, 1.0);", + "}" + )) +) + +cam = arcade.camera.PerspectiveProjector() +cam.view.position = (0.0, 0.0, 1.0) + +forward = 0 +strafe = 0 + + +def on_mouse_motion(x, y, dx, dy): + _l = (dx**2 + dy**2)**0.5 + + arcade.camera.controllers.rotate_around_up(cam.view, 2.0 * dx/_l) + _f = cam.view.forward + arcade.camera.controllers.rotate_around_right(cam.view, 2.0 * -dy/_l, up=False) + if abs(cam.view.forward[0]*cam.view.up[0]+cam.view.forward[1]*cam.view.up[1]+cam.view.forward[2]*cam.view.up[2]) > 0.90: + cam.view.forward = _f +win.on_mouse_motion = on_mouse_motion + + +def on_key_press(symbol, modifier): + global forward, strafe + if symbol == arcade.key.ESCAPE: + win.close() + + if symbol == arcade.key.W: + forward += 1 + elif symbol == arcade.key.S: + forward -= 1 + elif symbol == arcade.key.D: + strafe += 1 + elif symbol == arcade.key.A: + strafe -= 1 +win.on_key_press = on_key_press + + +def on_key_release(symbol, modifier): + global forward, strafe + if symbol == arcade.key.W: + forward -= 1 + elif symbol == arcade.key.S: + forward += 1 + elif symbol == arcade.key.D: + strafe -= 1 + elif symbol == arcade.key.A: + strafe += 1 +win.on_key_release = on_key_release + + +def on_update(delta_time): + win.set_mouse_position(0, 0) + arcade.camera.controllers.strafe(cam.view, (strafe * delta_time * 1.0, 0.0)) + + _pos = cam.view.position + _for = cam.view.forward + + cam.view.position = ( + _pos[0] + _for[0] * forward * delta_time * 1.0, + _pos[1] + _for[1] * forward * delta_time * 1.0, + _pos[2] + _for[2] * forward * delta_time * 1.0, + ) + +win.on_update = on_update + + +def on_draw(): + win.ctx.enable(win.ctx.DEPTH_TEST) + win.clear() + cam.use() + geo.render(prog) +win.on_draw = on_draw + +win.run() + diff --git a/arcade/examples/platform_tutorial/06_camera.py b/arcade/examples/platform_tutorial/06_camera.py index 9fcbd40f2..98016b6de 100644 --- a/arcade/examples/platform_tutorial/06_camera.py +++ b/arcade/examples/platform_tutorial/06_camera.py @@ -14,6 +14,9 @@ CHARACTER_SCALING = 1 TILE_SCALING = 0.5 +# Speed of the camera following the player +CAMERA_SPEED = 0.2 + # Movement speed of player, in pixels per frame PLAYER_MOVEMENT_SPEED = 5 GRAVITY = 1 @@ -131,7 +134,7 @@ def center_camera_to_player(self): screen_center_y = self.height/2 player_centered = screen_center_x, screen_center_y - arcade.camera.controllers.simple_follow_2D(0.2, player_centered, self.camera.view_data) + arcade.camera.controllers.simple_follow_2D(CAMERA_SPEED, player_centered, self.camera.view_data) def on_update(self, delta_time): """Movement and game logic""" diff --git a/arcade/examples/platform_tutorial/07_coins_and_sound.py b/arcade/examples/platform_tutorial/07_coins_and_sound.py index b6c8408b9..0f2a24748 100644 --- a/arcade/examples/platform_tutorial/07_coins_and_sound.py +++ b/arcade/examples/platform_tutorial/07_coins_and_sound.py @@ -15,6 +15,9 @@ TILE_SCALING = 0.5 COIN_SCALING = 0.5 +# Speed of the camera following the player +CAMERA_SPEED = 0.2 + # Movement speed of player, in pixels per frame PLAYER_MOVEMENT_SPEED = 5 GRAVITY = 1 @@ -140,7 +143,7 @@ def center_camera_to_player(self): screen_center_y = self.height/2 player_centered = screen_center_x, screen_center_y - arcade.camera.controllers.simple_follow_2D(0.2, player_centered, self.camera.view_data) + arcade.camera.controllers.simple_follow_2D(CAMERA_SPEED, player_centered, self.camera.view_data) def on_update(self, delta_time): """Movement and game logic""" diff --git a/arcade/examples/platform_tutorial/08_score.py b/arcade/examples/platform_tutorial/08_score.py index 732c6a1e7..b1f339d0a 100644 --- a/arcade/examples/platform_tutorial/08_score.py +++ b/arcade/examples/platform_tutorial/08_score.py @@ -15,6 +15,9 @@ TILE_SCALING = 0.5 COIN_SCALING = 0.5 +# Speed of the camera following the player +CAMERA_SPEED = 0.2 + # Movement speed of player, in pixels per frame PLAYER_MOVEMENT_SPEED = 5 GRAVITY = 1 @@ -165,7 +168,7 @@ def center_camera_to_player(self): screen_center_y = self.height/2 player_centered = screen_center_x, screen_center_y - arcade.camera.controllers.simple_follow_2D(0.2, player_centered, self.camera.view_data) + arcade.camera.controllers.simple_follow_2D(CAMERA_SPEED, player_centered, self.camera.view_data) def on_update(self, delta_time): """Movement and game logic""" diff --git a/arcade/examples/platform_tutorial/09_load_map.py b/arcade/examples/platform_tutorial/09_load_map.py index ade33569f..fc88c86d3 100644 --- a/arcade/examples/platform_tutorial/09_load_map.py +++ b/arcade/examples/platform_tutorial/09_load_map.py @@ -15,6 +15,9 @@ TILE_SCALING = 0.5 COIN_SCALING = 0.5 +# Speed of the camera following the player +CAMERA_SPEED = 0.2 + # Movement speed of player, in pixels per frame PLAYER_MOVEMENT_SPEED = 10 GRAVITY = 1 @@ -161,7 +164,7 @@ def center_camera_to_player(self): screen_center_y = self.height/2 player_centered = screen_center_x, screen_center_y - arcade.camera.controllers.simple_follow_2D(0.2, player_centered, self.camera.view_data) + arcade.camera.controllers.simple_follow_2D(CAMERA_SPEED, player_centered, self.camera.view_data) def on_update(self, delta_time): """Movement and game logic""" diff --git a/arcade/examples/platform_tutorial/10_multiple_levels.py b/arcade/examples/platform_tutorial/10_multiple_levels.py index e83d259b4..b92483a65 100644 --- a/arcade/examples/platform_tutorial/10_multiple_levels.py +++ b/arcade/examples/platform_tutorial/10_multiple_levels.py @@ -17,6 +17,9 @@ SPRITE_PIXEL_SIZE = 128 GRID_PIXEL_SIZE = SPRITE_PIXEL_SIZE * TILE_SCALING +# Speed of the camera following the player +CAMERA_SPEED = 0.2 + # Movement speed of player, in pixels per frame PLAYER_MOVEMENT_SPEED = 10 GRAVITY = 1 @@ -202,7 +205,7 @@ def center_camera_to_player(self): screen_center_y = self.height/2 player_centered = screen_center_x, screen_center_y - arcade.camera.controllers.simple_follow_2D(0.2, player_centered, self.camera.view_data) + arcade.camera.controllers.simple_follow_2D(CAMERA_SPEED, player_centered, self.camera.view_data) def on_update(self, delta_time): """Movement and game logic""" diff --git a/arcade/examples/platform_tutorial/11_ladders_and_more.py b/arcade/examples/platform_tutorial/11_ladders_and_more.py index cabfbe211..4c9c7aab2 100644 --- a/arcade/examples/platform_tutorial/11_ladders_and_more.py +++ b/arcade/examples/platform_tutorial/11_ladders_and_more.py @@ -17,6 +17,9 @@ SPRITE_PIXEL_SIZE = 128 GRID_PIXEL_SIZE = SPRITE_PIXEL_SIZE * TILE_SCALING +# Speed of the camera following the player +CAMERA_SPEED = 0.2 + # Movement speed of player, in pixels per frame PLAYER_MOVEMENT_SPEED = 7 GRAVITY = 1.5 @@ -200,7 +203,7 @@ def center_camera_to_player(self): screen_center_y = self.height/2 player_centered = screen_center_x, screen_center_y - arcade.camera.controllers.simple_follow_2D(0.2, player_centered, self.camera.view_data) + arcade.camera.controllers.simple_follow_2D(CAMERA_SPEED, player_centered, self.camera.view_data) def on_update(self, delta_time): """Movement and game logic""" diff --git a/arcade/examples/platform_tutorial/12_animate_character.py b/arcade/examples/platform_tutorial/12_animate_character.py index 33fd3fdb1..95c2624ec 100644 --- a/arcade/examples/platform_tutorial/12_animate_character.py +++ b/arcade/examples/platform_tutorial/12_animate_character.py @@ -17,6 +17,9 @@ SPRITE_PIXEL_SIZE = 128 GRID_PIXEL_SIZE = SPRITE_PIXEL_SIZE * TILE_SCALING +# Speed of the camera following the player +CAMERA_SPEED = 0.2 + # Movement speed of player, in pixels per frame PLAYER_MOVEMENT_SPEED = 7 GRAVITY = 1.5 @@ -359,7 +362,7 @@ def center_camera_to_player(self): screen_center_y = self.height/2 player_centered = screen_center_x, screen_center_y - arcade.camera.controllers.simple_follow_2D(0.2, player_centered, self.camera.view_data) + arcade.camera.controllers.simple_follow_2D(CAMERA_SPEED, player_centered, self.camera.view_data) def on_update(self, delta_time): """Movement and game logic""" diff --git a/arcade/examples/platform_tutorial/13_add_enemies.py b/arcade/examples/platform_tutorial/13_add_enemies.py index cb4da5fd0..c135246f0 100644 --- a/arcade/examples/platform_tutorial/13_add_enemies.py +++ b/arcade/examples/platform_tutorial/13_add_enemies.py @@ -19,6 +19,9 @@ SPRITE_PIXEL_SIZE = 128 GRID_PIXEL_SIZE = SPRITE_PIXEL_SIZE * TILE_SCALING +# Speed of the camera following the player +CAMERA_SPEED = 0.2 + # Movement speed of player, in pixels per frame PLAYER_MOVEMENT_SPEED = 7 GRAVITY = 1.5 @@ -400,7 +403,7 @@ def center_camera_to_player(self): screen_center_y = self.height/2 player_centered = screen_center_x, screen_center_y - arcade.camera.controllers.simple_follow_2D(0.2, player_centered, self.camera.view_data) + arcade.camera.controllers.simple_follow_2D(CAMERA_SPEED, player_centered, self.camera.view_data) def on_update(self, delta_time): """Movement and game logic""" diff --git a/arcade/examples/platform_tutorial/14_moving_enemies.py b/arcade/examples/platform_tutorial/14_moving_enemies.py index a3e5a0be9..a6915f33b 100644 --- a/arcade/examples/platform_tutorial/14_moving_enemies.py +++ b/arcade/examples/platform_tutorial/14_moving_enemies.py @@ -19,6 +19,9 @@ SPRITE_PIXEL_SIZE = 128 GRID_PIXEL_SIZE = SPRITE_PIXEL_SIZE * TILE_SCALING +# Speed of the camera following the player +CAMERA_SPEED = 0.2 + # Movement speed of player, in pixels per frame PLAYER_MOVEMENT_SPEED = 7 GRAVITY = 1.5 @@ -441,7 +444,7 @@ def center_camera_to_player(self): screen_center_y = self.height/2 player_centered = screen_center_x, screen_center_y - arcade.camera.controllers.simple_follow_2D(0.2, player_centered, self.camera.view_data) + arcade.camera.controllers.simple_follow_2D(CAMERA_SPEED, player_centered, self.camera.view_data) def on_update(self, delta_time): """Movement and game logic""" diff --git a/arcade/examples/platform_tutorial/15_collision_with_enemies.py b/arcade/examples/platform_tutorial/15_collision_with_enemies.py index d08a472c0..1b7efa545 100644 --- a/arcade/examples/platform_tutorial/15_collision_with_enemies.py +++ b/arcade/examples/platform_tutorial/15_collision_with_enemies.py @@ -19,6 +19,9 @@ SPRITE_PIXEL_SIZE = 128 GRID_PIXEL_SIZE = SPRITE_PIXEL_SIZE * TILE_SCALING +# Speed of the camera following the player +CAMERA_SPEED = 0.2 + # Movement speed of player, in pixels per frame PLAYER_MOVEMENT_SPEED = 7 GRAVITY = 1.5 @@ -441,7 +444,7 @@ def center_camera_to_player(self): screen_center_y = self.height/2 player_centered = screen_center_x, screen_center_y - arcade.camera.controllers.simple_follow_2D(0.2, player_centered, self.camera.view_data) + arcade.camera.controllers.simple_follow_2D(CAMERA_SPEED, player_centered, self.camera.view_data) def on_update(self, delta_time): """Movement and game logic""" diff --git a/arcade/examples/platform_tutorial/16_shooting_bullets.py b/arcade/examples/platform_tutorial/16_shooting_bullets.py index 8a34f277a..5a0d279d5 100644 --- a/arcade/examples/platform_tutorial/16_shooting_bullets.py +++ b/arcade/examples/platform_tutorial/16_shooting_bullets.py @@ -25,6 +25,9 @@ BULLET_SPEED = 12 BULLET_DAMAGE = 25 +# Speed of the camera following the player +CAMERA_SPEED = 0.2 + # Movement speed of player, in pixels per frame PLAYER_MOVEMENT_SPEED = 7 GRAVITY = 1.5 @@ -473,7 +476,7 @@ def center_camera_to_player(self): screen_center_y = self.height/2 player_centered = screen_center_x, screen_center_y - arcade.camera.controllers.simple_follow_2D(0.2, player_centered, self.camera.view_data) + arcade.camera.controllers.simple_follow_2D(CAMERA_SPEED, player_centered, self.camera.view_data) def on_update(self, delta_time): """Movement and game logic""" diff --git a/arcade/examples/platform_tutorial/17_views.py b/arcade/examples/platform_tutorial/17_views.py index bbba5f74a..5cbadd2e4 100644 --- a/arcade/examples/platform_tutorial/17_views.py +++ b/arcade/examples/platform_tutorial/17_views.py @@ -25,6 +25,9 @@ BULLET_SPEED = 12 BULLET_DAMAGE = 25 +# Speed of the camera following the player +CAMERA_SPEED = 0.2 + # Movement speed of player, in pixels per frame PLAYER_MOVEMENT_SPEED = 7 GRAVITY = 1.5 @@ -509,7 +512,7 @@ def center_camera_to_player(self): screen_center_y = self.window.height/2 player_centered = screen_center_x, screen_center_y - arcade.camera.controllers.simple_follow_2D(0.2, player_centered, self.camera.view_data) + arcade.camera.controllers.simple_follow_2D(CAMERA_SPEED, player_centered, self.camera.view_data) def on_update(self, delta_time): """Movement and game logic""" diff --git a/doc/tutorials/lights/01_light_demo.py b/doc/tutorials/lights/01_light_demo.py index d4fc3009b..fad8fec1e 100644 --- a/doc/tutorials/lights/01_light_demo.py +++ b/doc/tutorials/lights/01_light_demo.py @@ -26,7 +26,7 @@ def __init__(self, width, height, title): self.physics_engine = None # camera for scrolling - self.cam = None + self.camera = None def setup(self): """ Create everything """ @@ -52,7 +52,7 @@ def setup(self): self.physics_engine = arcade.PhysicsEngineSimple(self.player_sprite, self.wall_list) # setup camera - self.cam = arcade.camera.Camera2D() + self.camera = arcade.camera.Camera2D() def on_draw(self): """ Draw everything. """ @@ -85,33 +85,33 @@ def scroll_screen(self): """ Manage Scrolling """ # Scroll left - left_boundary = self.cam.left + VIEWPORT_MARGIN + left_boundary = self.camera.left + VIEWPORT_MARGIN if self.player_sprite.left < left_boundary: - self.cam.left -= left_boundary - self.player_sprite.left + self.camera.left -= left_boundary - self.player_sprite.left # Scroll right - right_boundary = self.cam.right - VIEWPORT_MARGIN + right_boundary = self.camera.right - VIEWPORT_MARGIN if self.player_sprite.right > right_boundary: - self.cam.right += self.player_sprite.right - right_boundary + self.camera.right += self.player_sprite.right - right_boundary # Scroll up - top_boundary = self.cam.top - VIEWPORT_MARGIN + top_boundary = self.camera.top - VIEWPORT_MARGIN if self.player_sprite.top > top_boundary: - self.cam.top += self.player_sprite.top - top_boundary + self.camera.top += self.player_sprite.top - top_boundary # Scroll down - bottom_boundary = self.cam.bottom + VIEWPORT_MARGIN + bottom_boundary = self.camera.bottom + VIEWPORT_MARGIN if self.player_sprite.bottom < bottom_boundary: - self.cam.bottom -= bottom_boundary - self.player_sprite.bottom + self.camera.bottom -= bottom_boundary - self.player_sprite.bottom # Make sure our boundaries are integer values. While the viewport does # support floating point numbers, for this application we want every pixel # in the view port to map directly onto a pixel on the screen. We don't want # any rounding errors. - self.cam.left = int(self.cam.left) - self.cam.bottom = int(self.cam.bottom) + self.camera.left = int(self.camera.left) + self.camera.bottom = int(self.camera.bottom) - self.cam.use() + self.camera.use() def on_update(self, delta_time): """ Movement and game logic """ diff --git a/doc/tutorials/lights/light_demo.py b/doc/tutorials/lights/light_demo.py index a7116cda1..18a8b1e1c 100644 --- a/doc/tutorials/lights/light_demo.py +++ b/doc/tutorials/lights/light_demo.py @@ -38,7 +38,7 @@ def __init__(self, width, height, title): self.physics_engine = None # camera for scrolling - self.cam = None + self.camera = None # --- Light related --- # List of all the lights @@ -186,7 +186,7 @@ def setup(self): self.physics_engine = arcade.PhysicsEngineSimple(self.player_sprite, self.wall_list) # setup camera - self.cam = arcade.camera.Camera2D() + self.camera = arcade.camera.Camera2D() def on_draw(self): """ Draw everything. """ @@ -207,7 +207,7 @@ def on_draw(self): # Now draw anything that should NOT be affected by lighting. arcade.draw_text("Press SPACE to turn character light on/off.", - 10 + self.cam.left, 10 + self.cam.bottom, + 10 + self.camera.left, 10 + self.camera.bottom, arcade.color.WHITE, 20) def on_resize(self, width, height): @@ -252,33 +252,33 @@ def scroll_screen(self): """ Manage Scrolling """ # Scroll left - left_boundary = self.cam.left + VIEWPORT_MARGIN + left_boundary = self.camera.left + VIEWPORT_MARGIN if self.player_sprite.left < left_boundary: - self.cam.left -= left_boundary - self.player_sprite.left + self.camera.left -= left_boundary - self.player_sprite.left # Scroll right - right_boundary = self.cam.right - VIEWPORT_MARGIN + right_boundary = self.camera.right - VIEWPORT_MARGIN if self.player_sprite.right > right_boundary: - self.cam.right += self.player_sprite.right - right_boundary + self.camera.right += self.player_sprite.right - right_boundary # Scroll up - top_boundary = self.cam.top - VIEWPORT_MARGIN + top_boundary = self.camera.top - VIEWPORT_MARGIN if self.player_sprite.top > top_boundary: - self.cam.top += self.player_sprite.top - top_boundary + self.camera.top += self.player_sprite.top - top_boundary # Scroll down - bottom_boundary = self.cam.bottom + VIEWPORT_MARGIN + bottom_boundary = self.camera.bottom + VIEWPORT_MARGIN if self.player_sprite.bottom < bottom_boundary: - self.cam.bottom -= bottom_boundary - self.player_sprite.bottom + self.camera.bottom -= bottom_boundary - self.player_sprite.bottom # Make sure our boundaries are integer values. While the viewport does # support floating point numbers, for this application we want every pixel # in the view port to map directly onto a pixel on the screen. We don't want # any rounding errors. - self.cam.left = int(self.cam.left) - self.cam.bottom = int(self.cam.bottom) + self.camera.left = int(self.camera.left) + self.camera.bottom = int(self.camera.bottom) - self.cam.use() + self.camera.use() def on_update(self, delta_time): """ Movement and game logic """ diff --git a/tests/unit/camera/test_camera_2d.py b/tests/unit/camera/test_camera_2d.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/tests/unit/camera/test_camera_controllers.py b/tests/unit/camera/test_camera_controllers.py index e69de29bb..308d61d09 100644 --- a/tests/unit/camera/test_camera_controllers.py +++ b/tests/unit/camera/test_camera_controllers.py @@ -0,0 +1,3 @@ +import pytest as pytest + +from arcade import camera, Window \ No newline at end of file diff --git a/tests/unit/camera/test_default_camera.py b/tests/unit/camera/test_default_camera.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/tests/unit/camera/test_orthographic_camera.py b/tests/unit/camera/test_orthographic_camera.py index 409cc99ab..59e9e25f4 100644 --- a/tests/unit/camera/test_orthographic_camera.py +++ b/tests/unit/camera/test_orthographic_camera.py @@ -3,117 +3,150 @@ from arcade import camera, Window -def test_orthographic_camera(window: Window): - default_camera = window.default_camera - +def test_orthographic_projector_use(window: Window): + # Given + from pyglet.math import Mat4 cam_default = camera.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 == camera.CameraData( - (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 == 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 - (0, 0, window.width, window.height), # Viewport - ) - - # test that the camera properties work - assert cam_default.view.position == default_view.position - assert cam_default.projection.viewport == default_projection.viewport - - # Test that the camera is being used. + + view_matrix = Mat4(( + 1.0, 0.0, 0.0, 0.0, + 0.0, 1.0, 0.0, 0.0, + 0.0, 0.0, 1.0, 0.0, + -400.0, -300.0, 0.0, 1.0 + )) + proj_matrix = Mat4(( + 1.0/400.0, 0.0, 0.0, 0.0, + 0.0, 1/300, 0.0, 0.0, + 0.0, 0.0, -0.01, 0.0, + 0.0, 0.0, 0.0, 1.0 + )) + + # When cam_default.use() - assert window.current_camera == cam_default - default_camera.use() - assert window.current_camera == default_camera - # Test that the camera is actually recognised by the camera as being activated - assert window.current_camera == default_camera + # Then + assert window.current_camera is cam_default + assert window.ctx.view_matrix == view_matrix + assert window.ctx.projection_matrix == proj_matrix + + +def test_orthographic_projector_activate(window: Window): + # Given + from pyglet.math import Mat4 + cam_default = camera.OrthographicProjector() + + view_matrix = Mat4(( + 1.0, 0.0, 0.0, 0.0, + 0.0, 1.0, 0.0, 0.0, + 0.0, 0.0, 1.0, 0.0, + -400.0, -300.0, 0.0, 1.0 + )) + proj_matrix = Mat4(( + 1.0 / 400.0, 0.0, 0.0, 0.0, + 0.0, 1 / 300, 0.0, 0.0, + 0.0, 0.0, -0.01, 0.0, + 0.0, 0.0, 0.0, 1.0 + )) + + # When with cam_default.activate() as cam: - assert window.current_camera == cam and cam == cam_default - assert window.current_camera == default_camera - - set_view = camera.CameraData( - (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 = camera.OrthographicProjectionData( - 0.0, 1.0, # Left, Right - 0.0, 1.0, # Bottom, Top - -1.0, 1.0, # Near, Far - (0, 0, 1, 1), # Viewport - ) - 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 - - # test that the camera properties work - assert cam_set.view.position == set_view.position - assert cam_set.projection.viewport == set_projection.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_map_coordinates(window: Window): + # Initially + assert window.current_camera is cam is cam_default + assert window.ctx.view_matrix == view_matrix + assert window.ctx.projection_matrix == proj_matrix + + # Finally + assert window.current_camera is window.default_camera + + +def test_orthographic_projector_map_coordinates(window: Window): + # Given + cam_default = camera.OrthographicProjector() + + # When + mouse_pos_a = (100.0, 100.0) + mouse_pos_b = (100.0, 0.0) + mouse_pos_c = (230.0, 800.0) + + # Then + assert cam_default.map_coordinate(mouse_pos_a) == pytest.approx((100.0, 100.0, 0.0)) + assert cam_default.map_coordinate(mouse_pos_b) == pytest.approx((100.0, 0.0, 0.0)) + assert cam_default.map_coordinate(mouse_pos_c) == pytest.approx((230.0, 800.0, 0.0)) + + +def test_orthographic_projector_map_coordinates_move(window: Window): + # Given cam_default = camera.OrthographicProjector() default_view = cam_default.view - default_projection = cam_default.projection - # Test that the camera maps coordinates properly when no values have been adjusted - assert cam_default.map_coordinate((100.0, 100.0)) == (pytest.approx(100.0), pytest.approx(100.0), 0.0) + mouse_pos_a = (window.width//2, window.height//2) + mouse_pos_b = (100.0, 100.0) - # Test that the camera maps coordinates properly when 0.0, 0.0 is in the center of the screen + # When default_view.position = (0.0, 0.0, 0.0) - assert (cam_default.map_coordinate((window.width//2, window.height//2)) == (0.0, 0.0, 0.0)) - # Test that the camera maps coordinates properly when the position has changed. + # Then + assert cam_default.map_coordinate(mouse_pos_a) == pytest.approx((0.0, 0.0, 0.0)) + assert cam_default.map_coordinate(mouse_pos_b) == pytest.approx((-300.0, -200.0, 0.0)) + + # And + + # When default_view.position = (100.0, 100.0, 0.0) - assert (cam_default.map_coordinate((100.0, 100.0)) == - (pytest.approx(200.0 - window.width//2), pytest.approx(200.0 - window.height//2), 0.0)) - # Test that the camera maps coordinates properly when the rotation has changed. - default_view.position = (0.0, 0.0, 0.0) + # Then + assert cam_default.map_coordinate(mouse_pos_a) == pytest.approx((100.0, 100.0, 0.0)) + assert cam_default.map_coordinate(mouse_pos_b) == pytest.approx((-200.0, -100.0, 0.0)) + + +def test_orthographic_projector_map_coordinates_rotate(window: Window): + # Given + cam_default = camera.OrthographicProjector() + default_view = cam_default.view + + mouse_pos_a = (window.width//2, window.height//2) + mouse_pos_b = (100.0, 100.0) + + # When default_view.up = (1.0, 0.0, 0.0) - assert (cam_default.map_coordinate((window.width//2, window.height//2 + 100.0)) == - (pytest.approx(100.0), pytest.approx(00.0), 0.0)) + default_view.position = (0.0, 0.0, 0.0) + + # Then + assert cam_default.map_coordinate(mouse_pos_a) == pytest.approx((0.0, 0.0, 0.0)) + assert cam_default.map_coordinate(mouse_pos_b) == pytest.approx((-200.0, 300.0, 0.0)) + + # And - # Test that the camera maps coordinates properly when the rotation and position has changed. + # When + default_view.up = (2.0**-0.5, 2.0**-0.5, 0.0) default_view.position = (100.0, 100.0, 0.0) - default_view.up = (0.7071067812, 0.7071067812, 0.0) - assert (cam_default.map_coordinate((window.width//2, window.height//2)) == - (pytest.approx(100.0), pytest.approx(100.0), 0.0)) - # Test that the camera maps coordinates properly when zoomed in. - default_view.position = (0.0, 0.0, 0.0) - default_view.up = (0.0, 1.0, 0.0) + # Then + assert cam_default.map_coordinate(mouse_pos_a) == pytest.approx((100.0, 100.0, 0.0)) + assert cam_default.map_coordinate(mouse_pos_b) == pytest.approx((-253.553390, 170.710678, 0.0)) + + +def test_orthographic_projector_map_coordinates_zoom(window: Window): + # Given + cam_default = camera.OrthographicProjector() + default_view = cam_default.view + + mouse_pos_a = (window.width, window.height) + mouse_pos_b = (100.0, 100.0) + + # When default_view.zoom = 2.0 - assert (cam_default.map_coordinate((window.width, window.height)) == - (pytest.approx(window.width//4), pytest.approx(window.height//4), 0.0)) - # Test that the camera maps coordinates properly when the viewport is not the default - default_view.zoom = 1.0 - default_view.viewport = window.width//2, window.height//2, window.width//2, window.height//2 + # Then + assert cam_default.map_coordinate(mouse_pos_a) == pytest.approx((window.width*0.75, window.height*0.75, 0.0)) + assert cam_default.map_coordinate(mouse_pos_b) == pytest.approx((250.0, 200.0, 0.0)) + + # And + + # When + default_view.position = (0.0, 0.0, 0.0) + default_view.zoom = 0.25 + + # Then + assert cam_default.map_coordinate(mouse_pos_a) == pytest.approx((window.width*2.0, window.height*2.0, 0.0)) + assert cam_default.map_coordinate(mouse_pos_b) == pytest.approx((-1200.0, -800.0, 0.0)) diff --git a/tests/unit/camera/test_perspective_camera.py b/tests/unit/camera/test_perspective_camera.py index a0b28276a..b7a6ba769 100644 --- a/tests/unit/camera/test_perspective_camera.py +++ b/tests/unit/camera/test_perspective_camera.py @@ -1,9 +1,123 @@ import pytest as pytest +from arcade import camera, Window -def test_perspective_camera(): - pass +def test_perspective_projector_use(window: Window): + # Given + from pyglet.math import Mat4 + cam_default = camera.PerspectiveProjector() -def test_perspective_map_coordinates(): - pass + view_matrix = Mat4(( + 1.0, 0.0, 0.0, 0.0, + 0.0, 1.0, 0.0, 0.0, + 0.0, 0.0, 1.0, 0.0, + -400.0, -300.0, 0.0, 1.0 + )) + proj_matrix = Mat4(( + 0.7500000000000002, 0.0, 0.0, 0.0, + 0.0, 1.0000000000000002, 0.0, 0.0, + 0.0, 0.0, -1.0002000200020003, -1.0, + 0.0, 0.0, -0.20002000200020004, 0.0 + )) + + # When + cam_default.use() + + # Then + assert window.current_camera is cam_default + assert window.ctx.view_matrix == view_matrix + assert window.ctx.projection_matrix == proj_matrix + + +def test_perspective_projector_map_coordinates(window: Window): + # Given + cam_default = camera.PerspectiveProjector() + + # When + mouse_pos_a = (100.0, 100.0) + mouse_pos_b = (100.0, 0.0) + mouse_pos_c = (230.0, 800.0) + + # Then + assert cam_default.map_coordinate(mouse_pos_a) == pytest.approx((100.0, 100.0, 0.0)) + assert cam_default.map_coordinate(mouse_pos_b) == pytest.approx((100.0, 0.0, 0.0)) + assert cam_default.map_coordinate(mouse_pos_c) == pytest.approx((230.0, 800.0, 0.0)) + + +def test_perspective_projector_map_coordinates_move(window: Window): + # Given + cam_default = camera.PerspectiveProjector() + default_view = cam_default.view + + mouse_pos_a = (window.width//2, window.height//2) + mouse_pos_b = (100.0, 100.0) + + # When + default_view.position = (0.0, 0.0, 0.0) + + # Then + assert cam_default.map_coordinate(mouse_pos_a) == pytest.approx((0.0, 0.0, 0.0)) + assert cam_default.map_coordinate(mouse_pos_b) == pytest.approx((-300.0, -200.0, 0.0)) + + # And + + # When + default_view.position = (100.0, 100.0, 0.0) + + # Then + assert cam_default.map_coordinate(mouse_pos_a) == pytest.approx((100.0, 100.0, 0.0)) + assert cam_default.map_coordinate(mouse_pos_b) == pytest.approx((-200.0, -100.0, 0.0)) + + +def test_perspective_projector_map_coordinates_rotate(window: Window): + # Given + cam_default = camera.PerspectiveProjector() + default_view = cam_default.view + + mouse_pos_a = (window.width//2, window.height//2) + mouse_pos_b = (100.0, 100.0) + + # When + default_view.up = (1.0, 0.0, 0.0) + default_view.position = (0.0, 0.0, 0.0) + + # Then + assert cam_default.map_coordinate(mouse_pos_a) == pytest.approx((0.0, 0.0, 0.0)) + assert cam_default.map_coordinate(mouse_pos_b) == pytest.approx((-200.0, 300.0, 0.0)) + + # And + + # When + default_view.up = (2.0**-0.5, 2.0**-0.5, 0.0) + default_view.position = (100.0, 100.0, 0.0) + + # Then + assert cam_default.map_coordinate(mouse_pos_a) == pytest.approx((100.0, 100.0, 0.0)) + assert cam_default.map_coordinate(mouse_pos_b) == pytest.approx((-253.553390, 170.710678, 0.0)) + + +def test_perspective_projector_map_coordinates_zoom(window: Window): + # Given + cam_default = camera.PerspectiveProjector() + default_view = cam_default.view + + mouse_pos_a = (window.width, window.height) + mouse_pos_b = (100.0, 100.0) + + # When + default_view.zoom = 2.0 + + # Then + assert cam_default.map_coordinate(mouse_pos_a) == pytest.approx((window.width*0.75, window.height*0.75, 0.0)) + assert cam_default.map_coordinate(mouse_pos_b) == pytest.approx((250.0, 200.0, 0.0)) + + # And + + # When + default_view.position = (0.0, 0.0, 0.0) + default_view.zoom = 0.25 + + # Then + assert cam_default.map_coordinate(mouse_pos_a) == pytest.approx((window.width*2.0, window.height*2.0, 0.0)) + assert cam_default.map_coordinate(mouse_pos_b) == pytest.approx((-1200.0, -800.0, 0.0)) diff --git a/tests/unit/camera/test_simple_camera.py b/tests/unit/camera/test_simple_camera.py deleted file mode 100644 index e69de29bb..000000000 From e1509065cd8393b07f5b2eb1265581991af355de Mon Sep 17 00:00:00 2001 From: DragonMoffon Date: Mon, 11 Dec 2023 00:21:20 +1300 Subject: [PATCH 52/94] Fixed issues pointed out by MiCurry thanks MiCurry for the editing on that! Also did linting. Currently fails on the camera_perspective_demo.py because of how I threw it together. Will fix in next commit --- arcade/camera/camera_2d.py | 10 +++++----- .../controllers/simple_controller_functions.py | 2 +- arcade/camera/default.py | 2 +- arcade/camera/perspective.py | 1 - arcade/camera/simple_camera.py | 4 ++-- arcade/camera/types.py | 2 -- arcade/examples/camera_perspective_demo.py | 15 ++++++++++++--- 7 files changed, 21 insertions(+), 15 deletions(-) diff --git a/arcade/camera/camera_2d.py b/arcade/camera/camera_2d.py index 0a39762b1..d92ed1dd3 100644 --- a/arcade/camera/camera_2d.py +++ b/arcade/camera/camera_2d.py @@ -19,7 +19,7 @@ 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. + 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 @@ -65,7 +65,7 @@ def __init__(self, *, projection: A 4-float tuple which defines the world space bounds which the camera projects to the viewport. near: The near clipping plane of the camera. - far: The far clipping place of the camera. + far: The far clipping plane of the camera. camera_data: A CameraData PoD which describes the viewport, position, up, and zoom projection_data: A OrthographicProjectionData PoD which describes the left, right, top, bottom, far, near planes for an orthographic projection. @@ -116,7 +116,7 @@ def __init__(self, *, def view_data(self) -> CameraData: """ Return the view data for the camera. This includes the - viewport, position, forward vector, up direction, and zoom. + 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. @@ -693,7 +693,7 @@ def activate(self) -> Iterator[Projector]: finally: previous_projection.use() - def map_coordinate(self, screen_coordinate: Tuple[float, float]) -> Tuple[float, float]: + def map_coordinate(self, screen_coordinate: Tuple[float, float], depth: float = 0.0) -> Tuple[float, float]: """ Take in a pixel coordinate from within the range of the window size and returns @@ -710,4 +710,4 @@ def map_coordinate(self, screen_coordinate: Tuple[float, float]) -> Tuple[float, of the camera. """ - return self._ortho_projector.map_coordinate(screen_coordinate)[:2] + return self._ortho_projector.map_coordinate(screen_coordinate, depth)[:2] diff --git a/arcade/camera/controllers/simple_controller_functions.py b/arcade/camera/controllers/simple_controller_functions.py index bfdb21c09..e30e3c361 100644 --- a/arcade/camera/controllers/simple_controller_functions.py +++ b/arcade/camera/controllers/simple_controller_functions.py @@ -1,4 +1,4 @@ -from typing import Tuple, Callable, Optional +from typing import Tuple, Callable from math import sin, cos, radians from arcade.camera.data import CameraData diff --git a/arcade/camera/default.py b/arcade/camera/default.py index 11b16d283..cce973b99 100644 --- a/arcade/camera/default.py +++ b/arcade/camera/default.py @@ -78,7 +78,7 @@ def activate(self) -> Iterator[Projector]: finally: previous.use() - def map_coordinate(self, screen_coordinate: Tuple[float, float]) -> Tuple[float, float]: + def map_coordinate(self, screen_coordinate: Tuple[float, float], depth = 0.0) -> Tuple[float, float]: """ Map the screen pos to screen_coordinates. diff --git a/arcade/camera/perspective.py b/arcade/camera/perspective.py index 9996ab73f..f10fc5073 100644 --- a/arcade/camera/perspective.py +++ b/arcade/camera/perspective.py @@ -1,6 +1,5 @@ from typing import Optional, Tuple, Iterator, TYPE_CHECKING from contextlib import contextmanager -from math import pi, tan from pyglet.math import Mat4, Vec3, Vec4 diff --git a/arcade/camera/simple_camera.py b/arcade/camera/simple_camera.py index a791682d0..5c388a91c 100644 --- a/arcade/camera/simple_camera.py +++ b/arcade/camera/simple_camera.py @@ -345,7 +345,7 @@ def activate(self) -> Iterator[Projector]: finally: previous_projector.use() - def map_coordinate(self, screen_coordinate: Tuple[float, float]) -> Tuple[float, float]: + def map_coordinate(self, screen_coordinate: Tuple[float, float], depth: float = 0.0) -> Tuple[float, float]: """ Take in a pixel coordinate from within the range of the viewport and returns @@ -362,7 +362,7 @@ def map_coordinate(self, screen_coordinate: Tuple[float, float]) -> Tuple[float, of the camera. """ - return self._camera.map_coordinate(screen_coordinate)[:2] + return self._camera.map_coordinate(screen_coordinate, depth)[:2] def resize(self, viewport_width: int, viewport_height: int, *, resize_projection: bool = True) -> None: diff --git a/arcade/camera/types.py b/arcade/camera/types.py index 821f918ad..d0196d44c 100644 --- a/arcade/camera/types.py +++ b/arcade/camera/types.py @@ -1,8 +1,6 @@ from typing import Protocol, Tuple, Iterator from contextlib import contextmanager -from arcade.camera.data import CameraData - __all__ = [ 'Projection', diff --git a/arcade/examples/camera_perspective_demo.py b/arcade/examples/camera_perspective_demo.py index ddd6f8a91..0ff6ed50e 100644 --- a/arcade/examples/camera_perspective_demo.py +++ b/arcade/examples/camera_perspective_demo.py @@ -1,9 +1,8 @@ -from array import array - import arcade import arcade.gl as gl # TODO: comment for days +# TODO: Make fit arcade standards for demo win = arcade.Window() win.set_exclusive_mouse() @@ -54,8 +53,11 @@ def on_mouse_motion(x, y, dx, dy): arcade.camera.controllers.rotate_around_up(cam.view, 2.0 * dx/_l) _f = cam.view.forward arcade.camera.controllers.rotate_around_right(cam.view, 2.0 * -dy/_l, up=False) - if abs(cam.view.forward[0]*cam.view.up[0]+cam.view.forward[1]*cam.view.up[1]+cam.view.forward[2]*cam.view.up[2]) > 0.90: + cam_dot = cam.view.forward[0]*cam.view.up[0]+cam.view.forward[1]*cam.view.up[1]+cam.view.forward[2]*cam.view.up[2] + if abs(cam_dot) > 0.90: cam.view.forward = _f + + win.on_mouse_motion = on_mouse_motion @@ -72,6 +74,8 @@ def on_key_press(symbol, modifier): strafe += 1 elif symbol == arcade.key.A: strafe -= 1 + + win.on_key_press = on_key_press @@ -85,6 +89,8 @@ def on_key_release(symbol, modifier): strafe -= 1 elif symbol == arcade.key.A: strafe += 1 + + win.on_key_release = on_key_release @@ -101,6 +107,7 @@ def on_update(delta_time): _pos[2] + _for[2] * forward * delta_time * 1.0, ) + win.on_update = on_update @@ -109,6 +116,8 @@ def on_draw(): win.clear() cam.use() geo.render(prog) + + win.on_draw = on_draw win.run() From 5db234fd46cc6c1ec919e3e79f94746fc258d732 Mon Sep 17 00:00:00 2001 From: DragonMoffon Date: Sun, 24 Dec 2023 15:39:34 +1300 Subject: [PATCH 53/94] Improved Camera Shake and Unit Tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Changed the screen shake to store the shake dir but not vector so it should work with rotating the camera now. Added a `readjust_camera` method to the SceenShakeController. Also finished it´s unit tests. --- .../controllers/simple_controller_classes.py | 35 +++++-- arcade/examples/camera_platform.py | 4 + .../examples/sprite_move_scrolling_shake.py | 3 + tests/unit/camera/test_camera_shake.py | 94 +++++++++++++++++++ 4 files changed, 130 insertions(+), 6 deletions(-) create mode 100644 tests/unit/camera/test_camera_shake.py diff --git a/arcade/camera/controllers/simple_controller_classes.py b/arcade/camera/controllers/simple_controller_classes.py index 3f5bd7422..34719b3f5 100644 --- a/arcade/camera/controllers/simple_controller_classes.py +++ b/arcade/camera/controllers/simple_controller_classes.py @@ -42,7 +42,7 @@ class ScreenShakeController: def __init__(self, camera_data: CameraData, *, max_amplitude: float = 1.0, falloff_time: float = 1.0, - acceleration_duration: float = 1, + acceleration_duration: float = 1.0, shake_frequency: float = 15.0): """ Initialise a screen-shake controller. @@ -71,7 +71,7 @@ def __init__(self, camera_data: CameraData, *, self._shaking: bool = False self._length_shaking: float = 0.0 - self._current_vec: Tuple[float, float, float] = (0.0, 0.0, 0.0) + self._current_dir: float = 0.0 self._last_vector: Tuple[float, float, float] = (0.0, 0.0, 0.0) self._last_update_time: float = 0.0 @@ -189,6 +189,10 @@ def _falloff_amp(self, _t: float) -> float: return 1 - _t**3 * (_t * (_t * 6.0 - 15.0) + 10.0) def _calc_max_amp(self): + """ + Determine the maximum amplitude by using either _acceleration_amp() or _falloff_amp(). + If falloff duration is less than 0.0 then the falloff never begins and + """ if self._length_shaking <= self._acceleration_duration: _t = self._length_shaking / self._acceleration_duration return self._acceleration_amp(_t) @@ -212,7 +216,7 @@ def reset(self): """ Reset the temporary shaking variables. WILL NOT STOP OR START SCREEN SHAKE. """ - self._current_vec = (0.0, 0.0, 0.0) + self._current_dir = 0.0 self._last_vector = (0.0, 0.0, 0.0) self._last_update_time = 0.0 self._length_shaking = 0.0 @@ -228,6 +232,12 @@ def stop(self): """ Instantly stop the screen-shake. """ + self._data.position = ( + self._data.position[0] - self._last_vector[0], + self._data.position[1] - self._last_vector[1], + self._data.position[2] - self._last_vector[2] + ) + self.reset() self._shaking = False @@ -262,11 +272,10 @@ def update_camera(self): if (floor(self._last_update_time * 2 * self.shake_frequency) < floor(self._length_shaking * 2.0 * self.shake_frequency))\ or self._last_update_time == 0.0: - _dir = uniform(-180, 180) - self._current_vec = quaternion_rotation(self._data.forward, self._data.up, _dir) + self._current_dir = uniform(-180, 180) _amp = self._calc_amplitude() * self.max_amplitude - _vec = self._current_vec + _vec = quaternion_rotation(self._data.forward, self._data.up, self._current_dir) _last = self._last_vector _pos = self._data.position @@ -283,3 +292,17 @@ def update_camera(self): _vec[2] * _amp ) self._last_update_time = self._length_shaking + + def readjust_camera(self): + """ + Can be called after the camera has been used revert the screen_shake. + While not strictly necessary it is highly advisable. If you are moving the + camera using an animation or something similar the behavior can start to go + awry if you do not readjust after the screen shake. + """ + self._data.position = ( + self._data.position[0] - self._last_vector[0], + self._data.position[1] - self._last_vector[1], + self._data.position[2] - self._last_vector[2] + ) + self._last_vector = (0.0, 0.0, 0.0) diff --git a/arcade/examples/camera_platform.py b/arcade/examples/camera_platform.py index 36b6c179c..3ec15eefe 100644 --- a/arcade/examples/camera_platform.py +++ b/arcade/examples/camera_platform.py @@ -177,6 +177,10 @@ def on_draw(self): # Draw our Scene self.scene.draw() + # Readjust the camera so the screen shake doesn't affect + # the camera following algorithm. + self.camera_shake.readjust_camera() + self.gui_camera.use() # Update fps text periodically diff --git a/arcade/examples/sprite_move_scrolling_shake.py b/arcade/examples/sprite_move_scrolling_shake.py index 106920167..cca6c6371 100644 --- a/arcade/examples/sprite_move_scrolling_shake.py +++ b/arcade/examples/sprite_move_scrolling_shake.py @@ -122,6 +122,9 @@ def on_draw(self): self.bomb_list.draw() self.player_list.draw() + # Readjust the camera's screen_shake + self.camera_shake.readjust_camera() + def on_key_press(self, key, modifiers): """Called whenever a key is pressed. """ diff --git a/tests/unit/camera/test_camera_shake.py b/tests/unit/camera/test_camera_shake.py new file mode 100644 index 000000000..c83d16fe3 --- /dev/null +++ b/tests/unit/camera/test_camera_shake.py @@ -0,0 +1,94 @@ +from arcade import camera, Window +from arcade.camera.controllers.simple_controller_classes import ScreenShakeController + + +def test_reset(window: Window): + # Given + camera_view = camera.CameraData() + screen_shaker = ScreenShakeController(camera_view) + + # When + screen_shaker.start() + screen_shaker.update(1.0) + screen_shaker.update_camera() + screen_shaker.readjust_camera() + screen_shaker.reset() + + # Then + assert screen_shaker._current_dir == 0.0, "ScreenShakeController failed to reset properly [current_dir]" + assert screen_shaker._last_vector == (0.0, 0.0, 0.0), "ScreenShakeController failed to reset properly [last_vector]" + assert screen_shaker._length_shaking == 0.0, "ScreenShakeController failed to reset properly [length_shaking]" + assert screen_shaker._last_update_time == 0.0, "ScreenShakeController failed to reset properly [last_update_time]" + + +def test_update(window: Window): + # Given + camera_view = camera.CameraData() + screen_shaker = ScreenShakeController(camera_view) + + # When + screen_shaker.update(1/60) + + # Then + assert screen_shaker._length_shaking == 0.0, "ScreenShakeController updated when it had not started" + + # When + screen_shaker.start() + screen_shaker.update(1/60) + + # Then + assert screen_shaker._length_shaking == 1/60, "ScreenShakeController failed to update by the correct dt" + + # When + screen_shaker.stop() + screen_shaker.update(1/60) + + # Then + assert screen_shaker._length_shaking == 0.0, "ScreenShakeController failed to stop updating" + + # When + screen_shaker.start() + screen_shaker.update(2.0) + + # Then + assert not screen_shaker.shaking, "ScreenShakeController failed to stop when shaking for too long" + + +def test_update_camera(window: Window): + # Given + camera_view = camera.CameraData() + screen_shaker = ScreenShakeController(camera_view) + + cam_pos = camera_view.position + + # When + screen_shaker.start() + screen_shaker.update(1/60) + screen_shaker.update_camera() + + # Then + assert camera_view.position != cam_pos, "ScreenShakeController failed to change the camera's position" + assert screen_shaker._last_vector != (0.0, 0.0, 0.0), "ScreenShakeController failed to store the last vector" + _adjust_test = ( + camera_view.position[0] - screen_shaker._last_vector[0], + camera_view.position[1] - screen_shaker._last_vector[1], + camera_view.position[2] - screen_shaker._last_vector[2], + ) + assert _adjust_test == cam_pos, "ScreenShakeController failed to store the correct last vector" + + +def test_readjust_camera(window: Window): + camera_view = camera.CameraData() + screen_shaker = ScreenShakeController(camera_view) + + cam_pos = camera_view.position + + # When + screen_shaker.start() + screen_shaker.update(1 / 60) + screen_shaker.update_camera() + screen_shaker.readjust_camera() + + # Then + assert camera_view.position == cam_pos, "ScreenShakeController failed to readjust the camera position" + assert screen_shaker._last_vector == (0.0, 0.0, 0.0), "ScreenShakeController failed to reset the last vector" From acd403b9a81095f2edb3ee5f8f027e86ed28bce9 Mon Sep 17 00:00:00 2001 From: DragonMoffon Date: Sun, 24 Dec 2023 16:12:46 +1300 Subject: [PATCH 54/94] Improved Orthographic and Perspective Unit Tests --- tests/unit/camera/test_orthographic_camera.py | 102 ++++++++---------- tests/unit/camera/test_perspective_camera.py | 98 ++++++++++------- 2 files changed, 105 insertions(+), 95 deletions(-) diff --git a/tests/unit/camera/test_orthographic_camera.py b/tests/unit/camera/test_orthographic_camera.py index 59e9e25f4..c377fc018 100644 --- a/tests/unit/camera/test_orthographic_camera.py +++ b/tests/unit/camera/test_orthographic_camera.py @@ -6,52 +6,31 @@ def test_orthographic_projector_use(window: Window): # Given from pyglet.math import Mat4 - cam_default = camera.OrthographicProjector() - - view_matrix = Mat4(( - 1.0, 0.0, 0.0, 0.0, - 0.0, 1.0, 0.0, 0.0, - 0.0, 0.0, 1.0, 0.0, - -400.0, -300.0, 0.0, 1.0 - )) - proj_matrix = Mat4(( - 1.0/400.0, 0.0, 0.0, 0.0, - 0.0, 1/300, 0.0, 0.0, - 0.0, 0.0, -0.01, 0.0, - 0.0, 0.0, 0.0, 1.0 - )) + camera_default = camera.OrthographicProjector() + + view_matrix = camera_default._generate_view_matrix() + proj_matrix = camera_default._generate_projection_matrix() # When - cam_default.use() + camera_default.use() # Then - assert window.current_camera is cam_default + assert window.current_camera is camera_default assert window.ctx.view_matrix == view_matrix assert window.ctx.projection_matrix == proj_matrix def test_orthographic_projector_activate(window: Window): # Given - from pyglet.math import Mat4 - cam_default = camera.OrthographicProjector() - - view_matrix = Mat4(( - 1.0, 0.0, 0.0, 0.0, - 0.0, 1.0, 0.0, 0.0, - 0.0, 0.0, 1.0, 0.0, - -400.0, -300.0, 0.0, 1.0 - )) - proj_matrix = Mat4(( - 1.0 / 400.0, 0.0, 0.0, 0.0, - 0.0, 1 / 300, 0.0, 0.0, - 0.0, 0.0, -0.01, 0.0, - 0.0, 0.0, 0.0, 1.0 - )) + camera_default: camera.OrthographicProjector = camera.OrthographicProjector() + + view_matrix = camera_default._generate_view_matrix() + proj_matrix = camera_default._generate_projection_matrix() # When - with cam_default.activate() as cam: + with camera_default.activate() as cam: # Initially - assert window.current_camera is cam is cam_default + assert window.current_camera is cam is camera_default assert window.ctx.view_matrix == view_matrix assert window.ctx.projection_matrix == proj_matrix @@ -61,7 +40,7 @@ def test_orthographic_projector_activate(window: Window): def test_orthographic_projector_map_coordinates(window: Window): # Given - cam_default = camera.OrthographicProjector() + camera_default = camera.OrthographicProjector() # When mouse_pos_a = (100.0, 100.0) @@ -69,15 +48,15 @@ def test_orthographic_projector_map_coordinates(window: Window): mouse_pos_c = (230.0, 800.0) # Then - assert cam_default.map_coordinate(mouse_pos_a) == pytest.approx((100.0, 100.0, 0.0)) - assert cam_default.map_coordinate(mouse_pos_b) == pytest.approx((100.0, 0.0, 0.0)) - assert cam_default.map_coordinate(mouse_pos_c) == pytest.approx((230.0, 800.0, 0.0)) + assert camera_default.map_coordinate(mouse_pos_a) == pytest.approx((100.0, 100.0, 0.0)) + assert camera_default.map_coordinate(mouse_pos_b) == pytest.approx((100.0, 0.0, 0.0)) + assert camera_default.map_coordinate(mouse_pos_c) == pytest.approx((230.0, 800.0, 0.0)) def test_orthographic_projector_map_coordinates_move(window: Window): # Given - cam_default = camera.OrthographicProjector() - default_view = cam_default.view + camera_default = camera.OrthographicProjector() + default_view = camera_default.view mouse_pos_a = (window.width//2, window.height//2) mouse_pos_b = (100.0, 100.0) @@ -86,8 +65,8 @@ def test_orthographic_projector_map_coordinates_move(window: Window): default_view.position = (0.0, 0.0, 0.0) # Then - assert cam_default.map_coordinate(mouse_pos_a) == pytest.approx((0.0, 0.0, 0.0)) - assert cam_default.map_coordinate(mouse_pos_b) == pytest.approx((-300.0, -200.0, 0.0)) + assert camera_default.map_coordinate(mouse_pos_a) == pytest.approx((0.0, 0.0, 0.0)) + assert camera_default.map_coordinate(mouse_pos_b) == pytest.approx((-300.0, -200.0, 0.0)) # And @@ -95,14 +74,14 @@ def test_orthographic_projector_map_coordinates_move(window: Window): default_view.position = (100.0, 100.0, 0.0) # Then - assert cam_default.map_coordinate(mouse_pos_a) == pytest.approx((100.0, 100.0, 0.0)) - assert cam_default.map_coordinate(mouse_pos_b) == pytest.approx((-200.0, -100.0, 0.0)) + assert camera_default.map_coordinate(mouse_pos_a) == pytest.approx((100.0, 100.0, 0.0)) + assert camera_default.map_coordinate(mouse_pos_b) == pytest.approx((-200.0, -100.0, 0.0)) def test_orthographic_projector_map_coordinates_rotate(window: Window): # Given - cam_default = camera.OrthographicProjector() - default_view = cam_default.view + camera_default = camera.OrthographicProjector() + default_view = camera_default.view mouse_pos_a = (window.width//2, window.height//2) mouse_pos_b = (100.0, 100.0) @@ -112,8 +91,8 @@ def test_orthographic_projector_map_coordinates_rotate(window: Window): default_view.position = (0.0, 0.0, 0.0) # Then - assert cam_default.map_coordinate(mouse_pos_a) == pytest.approx((0.0, 0.0, 0.0)) - assert cam_default.map_coordinate(mouse_pos_b) == pytest.approx((-200.0, 300.0, 0.0)) + assert camera_default.map_coordinate(mouse_pos_a) == pytest.approx((0.0, 0.0, 0.0)) + assert camera_default.map_coordinate(mouse_pos_b) == pytest.approx((-200.0, 300.0, 0.0)) # And @@ -122,14 +101,14 @@ def test_orthographic_projector_map_coordinates_rotate(window: Window): default_view.position = (100.0, 100.0, 0.0) # Then - assert cam_default.map_coordinate(mouse_pos_a) == pytest.approx((100.0, 100.0, 0.0)) - assert cam_default.map_coordinate(mouse_pos_b) == pytest.approx((-253.553390, 170.710678, 0.0)) + assert camera_default.map_coordinate(mouse_pos_a) == pytest.approx((100.0, 100.0, 0.0)) + assert camera_default.map_coordinate(mouse_pos_b) == pytest.approx((-253.553390, 170.710678, 0.0)) def test_orthographic_projector_map_coordinates_zoom(window: Window): # Given - cam_default = camera.OrthographicProjector() - default_view = cam_default.view + camera_default = camera.OrthographicProjector() + default_view = camera_default.view mouse_pos_a = (window.width, window.height) mouse_pos_b = (100.0, 100.0) @@ -138,8 +117,8 @@ def test_orthographic_projector_map_coordinates_zoom(window: Window): default_view.zoom = 2.0 # Then - assert cam_default.map_coordinate(mouse_pos_a) == pytest.approx((window.width*0.75, window.height*0.75, 0.0)) - assert cam_default.map_coordinate(mouse_pos_b) == pytest.approx((250.0, 200.0, 0.0)) + assert camera_default.map_coordinate(mouse_pos_a) == pytest.approx((window.width*0.75, window.height*0.75, 0.0)) + assert camera_default.map_coordinate(mouse_pos_b) == pytest.approx((250.0, 200.0, 0.0)) # And @@ -148,5 +127,18 @@ def test_orthographic_projector_map_coordinates_zoom(window: Window): default_view.zoom = 0.25 # Then - assert cam_default.map_coordinate(mouse_pos_a) == pytest.approx((window.width*2.0, window.height*2.0, 0.0)) - assert cam_default.map_coordinate(mouse_pos_b) == pytest.approx((-1200.0, -800.0, 0.0)) + assert camera_default.map_coordinate(mouse_pos_a) == pytest.approx((window.width*2.0, window.height*2.0, 0.0)) + assert camera_default.map_coordinate(mouse_pos_b) == pytest.approx((-1200.0, -800.0, 0.0)) + + +def test_perspective_projector_map_coordinates_depth(window): + # Given + camera_default = camera.PerspectiveProjector() + default_view = camera_default.view + + mouse_pos_a = (window.width, window.height) + mouse_pos_b = (100.0, 100.0) + + # When + + # Then diff --git a/tests/unit/camera/test_perspective_camera.py b/tests/unit/camera/test_perspective_camera.py index b7a6ba769..66d69dbef 100644 --- a/tests/unit/camera/test_perspective_camera.py +++ b/tests/unit/camera/test_perspective_camera.py @@ -5,34 +5,39 @@ def test_perspective_projector_use(window: Window): # Given - from pyglet.math import Mat4 - cam_default = camera.PerspectiveProjector() - - view_matrix = Mat4(( - 1.0, 0.0, 0.0, 0.0, - 0.0, 1.0, 0.0, 0.0, - 0.0, 0.0, 1.0, 0.0, - -400.0, -300.0, 0.0, 1.0 - )) - proj_matrix = Mat4(( - 0.7500000000000002, 0.0, 0.0, 0.0, - 0.0, 1.0000000000000002, 0.0, 0.0, - 0.0, 0.0, -1.0002000200020003, -1.0, - 0.0, 0.0, -0.20002000200020004, 0.0 - )) + camera_default = camera.PerspectiveProjector() + + view_matrix = camera_default._generate_projection_matrix() + proj_matrix = camera_default._generate_projection_matrix() # When - cam_default.use() + camera_default.use() # Then - assert window.current_camera is cam_default + assert window.current_camera is camera_default assert window.ctx.view_matrix == view_matrix assert window.ctx.projection_matrix == proj_matrix +def test_perspective_projector_activate(window: Window): + # Given + camera_default = camera.PerspectiveProjector() + + view_matrix = camera_default._generate_view_matrix() + proj_matrix = camera_default._generate_projection_matrix() + + # When + with camera_default.activate() as cam: + # Initially + assert window.current_camera is cam is camera_default + assert window.ctx.view_matrix == view_matrix + assert window.ctx.projection_matrix == proj_matrix + + # Finally + assert window.current_camera is window.default_camera def test_perspective_projector_map_coordinates(window: Window): # Given - cam_default = camera.PerspectiveProjector() + camera_default = camera.PerspectiveProjector() # When mouse_pos_a = (100.0, 100.0) @@ -40,15 +45,15 @@ def test_perspective_projector_map_coordinates(window: Window): mouse_pos_c = (230.0, 800.0) # Then - assert cam_default.map_coordinate(mouse_pos_a) == pytest.approx((100.0, 100.0, 0.0)) - assert cam_default.map_coordinate(mouse_pos_b) == pytest.approx((100.0, 0.0, 0.0)) - assert cam_default.map_coordinate(mouse_pos_c) == pytest.approx((230.0, 800.0, 0.0)) + assert camera_default.map_coordinate(mouse_pos_a) == pytest.approx((100.0, 100.0, 0.0)) + assert camera_default.map_coordinate(mouse_pos_b) == pytest.approx((100.0, 0.0, 0.0)) + assert camera_default.map_coordinate(mouse_pos_c) == pytest.approx((230.0, 800.0, 0.0)) def test_perspective_projector_map_coordinates_move(window: Window): - # Given - cam_default = camera.PerspectiveProjector() - default_view = cam_default.view + # GivenT + camera_default = camera.PerspectiveProjector() + default_view = camera_default.view mouse_pos_a = (window.width//2, window.height//2) mouse_pos_b = (100.0, 100.0) @@ -57,8 +62,8 @@ def test_perspective_projector_map_coordinates_move(window: Window): default_view.position = (0.0, 0.0, 0.0) # Then - assert cam_default.map_coordinate(mouse_pos_a) == pytest.approx((0.0, 0.0, 0.0)) - assert cam_default.map_coordinate(mouse_pos_b) == pytest.approx((-300.0, -200.0, 0.0)) + assert camera_default.map_coordinate(mouse_pos_a) == pytest.approx((0.0, 0.0, 0.0)) + assert camera_default.map_coordinate(mouse_pos_b) == pytest.approx((-300.0, -200.0, 0.0)) # And @@ -66,14 +71,14 @@ def test_perspective_projector_map_coordinates_move(window: Window): default_view.position = (100.0, 100.0, 0.0) # Then - assert cam_default.map_coordinate(mouse_pos_a) == pytest.approx((100.0, 100.0, 0.0)) - assert cam_default.map_coordinate(mouse_pos_b) == pytest.approx((-200.0, -100.0, 0.0)) + assert camera_default.map_coordinate(mouse_pos_a) == pytest.approx((100.0, 100.0, 0.0)) + assert camera_default.map_coordinate(mouse_pos_b) == pytest.approx((-200.0, -100.0, 0.0)) def test_perspective_projector_map_coordinates_rotate(window: Window): # Given - cam_default = camera.PerspectiveProjector() - default_view = cam_default.view + camera_default = camera.PerspectiveProjector() + default_view = camera_default.view mouse_pos_a = (window.width//2, window.height//2) mouse_pos_b = (100.0, 100.0) @@ -83,8 +88,8 @@ def test_perspective_projector_map_coordinates_rotate(window: Window): default_view.position = (0.0, 0.0, 0.0) # Then - assert cam_default.map_coordinate(mouse_pos_a) == pytest.approx((0.0, 0.0, 0.0)) - assert cam_default.map_coordinate(mouse_pos_b) == pytest.approx((-200.0, 300.0, 0.0)) + assert camera_default.map_coordinate(mouse_pos_a) == pytest.approx((0.0, 0.0, 0.0)) + assert camera_default.map_coordinate(mouse_pos_b) == pytest.approx((-200.0, 300.0, 0.0)) # And @@ -93,14 +98,14 @@ def test_perspective_projector_map_coordinates_rotate(window: Window): default_view.position = (100.0, 100.0, 0.0) # Then - assert cam_default.map_coordinate(mouse_pos_a) == pytest.approx((100.0, 100.0, 0.0)) - assert cam_default.map_coordinate(mouse_pos_b) == pytest.approx((-253.553390, 170.710678, 0.0)) + assert camera_default.map_coordinate(mouse_pos_a) == pytest.approx((100.0, 100.0, 0.0)) + assert camera_default.map_coordinate(mouse_pos_b) == pytest.approx((-253.553390, 170.710678, 0.0)) def test_perspective_projector_map_coordinates_zoom(window: Window): # Given - cam_default = camera.PerspectiveProjector() - default_view = cam_default.view + camera_default = camera.PerspectiveProjector() + default_view = camera_default.view mouse_pos_a = (window.width, window.height) mouse_pos_b = (100.0, 100.0) @@ -109,8 +114,8 @@ def test_perspective_projector_map_coordinates_zoom(window: Window): default_view.zoom = 2.0 # Then - assert cam_default.map_coordinate(mouse_pos_a) == pytest.approx((window.width*0.75, window.height*0.75, 0.0)) - assert cam_default.map_coordinate(mouse_pos_b) == pytest.approx((250.0, 200.0, 0.0)) + assert camera_default.map_coordinate(mouse_pos_a) == pytest.approx((window.width*0.75, window.height*0.75, 0.0)) + assert camera_default.map_coordinate(mouse_pos_b) == pytest.approx((250.0, 200.0, 0.0)) # And @@ -119,5 +124,18 @@ def test_perspective_projector_map_coordinates_zoom(window: Window): default_view.zoom = 0.25 # Then - assert cam_default.map_coordinate(mouse_pos_a) == pytest.approx((window.width*2.0, window.height*2.0, 0.0)) - assert cam_default.map_coordinate(mouse_pos_b) == pytest.approx((-1200.0, -800.0, 0.0)) + assert camera_default.map_coordinate(mouse_pos_a) == pytest.approx((window.width*2.0, window.height*2.0, 0.0)) + assert camera_default.map_coordinate(mouse_pos_b) == pytest.approx((-1200.0, -800.0, 0.0)) + + +def test_perspective_projector_map_coordinates_depth(window): + # Given + camera_default = camera.PerspectiveProjector() + default_view = camera_default.view + + mouse_pos_a = (window.width, window.height) + mouse_pos_b = (100.0, 100.0) + + # When + + # Then From ef2749f1acdc15c15a4b339e18f75ef456a46d8e Mon Sep 17 00:00:00 2001 From: DragonMoffon Date: Sun, 24 Dec 2023 16:15:12 +1300 Subject: [PATCH 55/94] Added cull_mode to spritelists Currently only picks between 'orthographic' and 'disabled' --- arcade/sprite_list/sprite_list.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/arcade/sprite_list/sprite_list.py b/arcade/sprite_list/sprite_list.py index 9f9004162..cc2d62aa7 100644 --- a/arcade/sprite_list/sprite_list.py +++ b/arcade/sprite_list/sprite_list.py @@ -93,6 +93,12 @@ class SpriteList(Generic[SpriteType]): :ref:`pg_spritelist_advanced_lazy_spritelists` to learn more. :param visible: Setting this to False will cause the SpriteList to not be drawn. When draw is called, the method will just return without drawing. + :param cull_mode: The mode which determines how sprites are culled. + Only 'orthographic' and 'disabled' are the only options currently. + - 'orthographic' is a simple algorithm that assumes an orthographic projection. + This is the default in arcade. If using a perspective projection + it is highly recommended to not use this mode. + - 'disabled' is no algorithm, and is used when no valid mode is selected. """ def __init__( @@ -103,6 +109,7 @@ def __init__( capacity: int = 100, lazy: bool = False, visible: bool = True, + cull_mode: str = "orthographic" ): self.program = None self._atlas: Optional[TextureAtlas] = atlas @@ -110,6 +117,8 @@ def __init__( self._lazy = lazy self._visible = visible self._color: Tuple[float, float, float, float] = (1.0, 1.0, 1.0, 1.0) + # The cull mode which determines how sprites should be culled within the geometry shader. + self._cull_mode: str = cull_mode # The initial capacity of the spritelist buffers (internal) self._buf_capacity = abs(capacity) or _DEFAULT_CAPACITY @@ -188,7 +197,8 @@ def _init_deferred(self): return self.ctx = get_window().ctx - self.program = self.ctx.sprite_list_program_cull + self.program = (self.ctx.sprite_list_program_cull if self._cull_mode == 'orthographic' else + self.ctx.sprite_list_program_no_cull) if not self._atlas: self._atlas = self.ctx.default_atlas From 3a3e61658fd83c1c199682587f9d081af9c84478 Mon Sep 17 00:00:00 2001 From: DragonMoffon Date: Sun, 24 Dec 2023 16:36:38 +1300 Subject: [PATCH 56/94] Started working on simple camera controller unit tests --- .../simple_controller_functions.py | 7 +- .../camera/test_camera_controller_methods.py | 89 +++++++++++++++++++ tests/unit/camera/test_camera_controllers.py | 3 - 3 files changed, 90 insertions(+), 9 deletions(-) create mode 100644 tests/unit/camera/test_camera_controller_methods.py delete mode 100644 tests/unit/camera/test_camera_controllers.py diff --git a/arcade/camera/controllers/simple_controller_functions.py b/arcade/camera/controllers/simple_controller_functions.py index e30e3c361..a626bdbd4 100644 --- a/arcade/camera/controllers/simple_controller_functions.py +++ b/arcade/camera/controllers/simple_controller_functions.py @@ -89,12 +89,7 @@ def quaternion_rotation(axis: Tuple[float, float, float], 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, 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 diff --git a/tests/unit/camera/test_camera_controller_methods.py b/tests/unit/camera/test_camera_controller_methods.py new file mode 100644 index 000000000..b63e6e48f --- /dev/null +++ b/tests/unit/camera/test_camera_controller_methods.py @@ -0,0 +1,89 @@ +import pytest as pytest + +from arcade import camera, Window +import arcade.camera.controllers as controllers + + +def test_strafe(window: Window): + # Given + camera_data = camera.CameraData() + directions = ((1.0, 0.0), (0.0, 1.0), (-1.0, 0.0), (0.0, -1.0), (0.5, 0.5)) + + # When + camera_data.forward = (0.0 ,0.0, -1.0) + camera_data.up = (0.0, 0.0, 1.0) + + # Then + for dirs in directions: + controllers.strafe(camera_data, dirs) + assert ( + camera_data.position == (dirs[0], dirs[1], 0.0), + f"Strafe failed to move the camera data correctly, {dirs}" + ) + camera_data.position = (0.0, 0.0, 0.0) + + # Given + camera_data.forward = (0.0, 0.0, -1.0) + camera_data.up = (0.0, 0.0, 1.0) + + for dirs in directions: + controllers.strafe(camera_data, dirs) + assert ( + camera_data.position == (0.0, dirs[1], dirs[0]), + f"Strafe failed to move the camera data correctly, {dirs}" + ) + camera_data.position = (0.0, 0.0, 0.0) + + +def test_rotate_around_forward(window: Window): + # Given + camera_data = camera.CameraData() + + # When + + # Then + + +def test_rotate_around_up(window: Window): + # Given + camera_data = camera.CameraData() + + # When + + # Then + + +def test_rotate_around_right(window: Window): + # Given + camera_data = camera.CameraData() + + # When + + # Then + + +def test_interpolate(window: Window): + # Given + camera_data = camera.CameraData() + + # When + + # Then + + +def test_simple_follow(window: Window): + # Given + camera_data = camera.CameraData() + + # When + + # Then + + +def test_simple_easing(window: Window): + # Given + camera_data = camera.CameraData() + + # When + + # Then diff --git a/tests/unit/camera/test_camera_controllers.py b/tests/unit/camera/test_camera_controllers.py deleted file mode 100644 index 308d61d09..000000000 --- a/tests/unit/camera/test_camera_controllers.py +++ /dev/null @@ -1,3 +0,0 @@ -import pytest as pytest - -from arcade import camera, Window \ No newline at end of file From 21ccfd350f72b3415d36fa3f3a538e7b502cbe39 Mon Sep 17 00:00:00 2001 From: DragonMoffon Date: Sun, 4 Feb 2024 23:49:40 +1300 Subject: [PATCH 57/94] continue perspective work --- arcade/camera/perspective.py | 17 ++++++------ arcade/examples/camera_perspective_demo.py | 6 +++-- tests/unit/camera/test_perspective_camera.py | 28 +++++++------------- 3 files changed, 23 insertions(+), 28 deletions(-) diff --git a/arcade/camera/perspective.py b/arcade/camera/perspective.py index f10fc5073..6c265ea35 100644 --- a/arcade/camera/perspective.py +++ b/arcade/camera/perspective.py @@ -2,7 +2,9 @@ from contextlib import contextmanager from pyglet.math import Mat4, Vec3, Vec4 +from math import tan, radians +import arcade from arcade.camera.data import CameraData, PerspectiveProjectionData from arcade.camera.types import Projector @@ -138,7 +140,7 @@ def activate(self) -> Iterator[Projector]: finally: previous_projector.use() - def map_coordinate(self, screen_coordinate: Tuple[float, float], depth: float = 0.0) -> Tuple[float, float, float]: + def map_coordinate(self, screen_coordinate: Tuple[float, float], depth: Optional[float] = None) -> Tuple[float, float, float]: """ Take in a pixel coordinate from within the range of the window size and returns @@ -146,22 +148,21 @@ def map_coordinate(self, screen_coordinate: Tuple[float, float], depth: float = Essentially reverses the effects of the projector. - Because the scale changes depending on the depth tested at, - the depth is calculated to be the point at which the projection area - matches the area of the camera viewport. This would be the depth at which - one pixel in a texture is one pixel on screen. + The scale changes as the depth increases. You can Args: screen_coordinate: A 2D position in pixels from the bottom left of the screen. This should ALWAYS be in the range of 0.0 - screen size. - depth: The depth of the query. + depth: The depth of the query. equivalent to the 'Z' coordinate. + If left as None it will attempt to match the scale of the screen. Returns: A 3D vector in world space. """ - # TODO Integrate Z-depth + depth = depth or self._projection.viewport[3] / (2 * tan(radians(self._projection.fov/2))) + screen_x = 2.0 * (screen_coordinate[0] - self._projection.viewport[0]) / self._projection.viewport[2] - 1 screen_y = 2.0 * (screen_coordinate[1] - self._projection.viewport[1]) / self._projection.viewport[3] - 1 - screen_z = 2.0 * (depth - self._projection.near) / (self._projection.far - self._projection.near) - 1 + screen_z = (depth - self._projection.near) / (self._projection.far - self._projection.near) _view = self._generate_view_matrix() _projection = self._generate_projection_matrix() diff --git a/arcade/examples/camera_perspective_demo.py b/arcade/examples/camera_perspective_demo.py index 0ff6ed50e..0ea504e34 100644 --- a/arcade/examples/camera_perspective_demo.py +++ b/arcade/examples/camera_perspective_demo.py @@ -114,9 +114,11 @@ def on_update(delta_time): def on_draw(): win.ctx.enable(win.ctx.DEPTH_TEST) win.clear() - cam.use() - geo.render(prog) + with cam.activate(): + geo.render(prog) + + arcade.draw_text("Press ESC to quit", win.width//2, 25, anchor_x="center") win.on_draw = on_draw diff --git a/tests/unit/camera/test_perspective_camera.py b/tests/unit/camera/test_perspective_camera.py index 66d69dbef..e81ee3c01 100644 --- a/tests/unit/camera/test_perspective_camera.py +++ b/tests/unit/camera/test_perspective_camera.py @@ -7,7 +7,7 @@ def test_perspective_projector_use(window: Window): # Given camera_default = camera.PerspectiveProjector() - view_matrix = camera_default._generate_projection_matrix() + view_matrix = camera_default._generate_view_matrix() proj_matrix = camera_default._generate_projection_matrix() # When @@ -18,6 +18,7 @@ def test_perspective_projector_use(window: Window): assert window.ctx.view_matrix == view_matrix assert window.ctx.projection_matrix == proj_matrix + def test_perspective_projector_activate(window: Window): # Given camera_default = camera.PerspectiveProjector() @@ -33,21 +34,25 @@ def test_perspective_projector_activate(window: Window): assert window.ctx.projection_matrix == proj_matrix # Finally - assert window.current_camera is window.default_camera + assert window.current_camera is not window.default_camera + def test_perspective_projector_map_coordinates(window: Window): # Given camera_default = camera.PerspectiveProjector() + # for d in range(int(camera_default.projection.near), int(camera_default.projection.far)): + # print(camera_default.map_coordinate((100.0, 100.0), d)) + # When mouse_pos_a = (100.0, 100.0) mouse_pos_b = (100.0, 0.0) mouse_pos_c = (230.0, 800.0) # Then - assert camera_default.map_coordinate(mouse_pos_a) == pytest.approx((100.0, 100.0, 0.0)) - assert camera_default.map_coordinate(mouse_pos_b) == pytest.approx((100.0, 0.0, 0.0)) - assert camera_default.map_coordinate(mouse_pos_c) == pytest.approx((230.0, 800.0, 0.0)) + # assert camera_default.map_coordinate(mouse_pos_a) == pytest.approx((100.0, 100.0, 0.0)) + # assert camera_default.map_coordinate(mouse_pos_b) == pytest.approx((100.0, 0.0, 0.0)) + # assert camera_default.map_coordinate(mouse_pos_c) == pytest.approx((230.0, 800.0, 0.0)) def test_perspective_projector_map_coordinates_move(window: Window): @@ -126,16 +131,3 @@ def test_perspective_projector_map_coordinates_zoom(window: Window): # Then assert camera_default.map_coordinate(mouse_pos_a) == pytest.approx((window.width*2.0, window.height*2.0, 0.0)) assert camera_default.map_coordinate(mouse_pos_b) == pytest.approx((-1200.0, -800.0, 0.0)) - - -def test_perspective_projector_map_coordinates_depth(window): - # Given - camera_default = camera.PerspectiveProjector() - default_view = camera_default.view - - mouse_pos_a = (window.width, window.height) - mouse_pos_b = (100.0, 100.0) - - # When - - # Then From 030c6300d174639a6d015b0d2c6a9c55070f1b87 Mon Sep 17 00:00:00 2001 From: DragonMoffon Date: Mon, 5 Feb 2024 01:23:56 +1300 Subject: [PATCH 58/94] Removed unfinished perspective camera To allow the new camera code to be integrated into 3.0 the perspective camera was removed. Unit tests for controller methods have not been finished. --- arcade/camera/__init__.py | 5 +- arcade/camera/camera_2d.py | 2 + .../simple_controller_functions.py | 5 +- arcade/camera/perspective.py | 178 ------------------ arcade/examples/camera_perspective_demo.py | 126 ------------- .../camera/test_camera_controller_methods.py | 14 ++ tests/unit/camera/test_orthographic_camera.py | 83 ++++---- tests/unit/camera/test_perspective_camera.py | 133 ------------- 8 files changed, 57 insertions(+), 489 deletions(-) delete mode 100644 arcade/camera/perspective.py delete mode 100644 arcade/examples/camera_perspective_demo.py delete mode 100644 tests/unit/camera/test_perspective_camera.py diff --git a/arcade/camera/__init__.py b/arcade/camera/__init__.py index 431216a30..8916f3d20 100644 --- a/arcade/camera/__init__.py +++ b/arcade/camera/__init__.py @@ -3,11 +3,10 @@ Providing a multitude of camera's for any need. """ -from arcade.camera.data import CameraData, OrthographicProjectionData, PerspectiveProjectionData +from arcade.camera.data import CameraData, OrthographicProjectionData from arcade.camera.types import Projection, Projector 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 @@ -20,8 +19,6 @@ 'CameraData', 'OrthographicProjectionData', 'OrthographicProjector', - 'PerspectiveProjectionData', - 'PerspectiveProjector', 'SimpleCamera', 'Camera2D', 'controllers' diff --git a/arcade/camera/camera_2d.py b/arcade/camera/camera_2d.py index d92ed1dd3..d58cc67bc 100644 --- a/arcade/camera/camera_2d.py +++ b/arcade/camera/camera_2d.py @@ -704,6 +704,8 @@ def map_coordinate(self, screen_coordinate: Tuple[float, float], depth: float = Args: screen_coordinate: A 2D position in pixels from the bottom left of the screen. This should ALWAYS be in the range of 0.0 - screen size. + depth: The depth value which is mapped along with the screen coordinates. Because of how + Orthographic perspectives work this does not impact how the screen_coordinates are mapped. Returns: A 2D vector (Along the XY plane) in world space (same as sprites). perfect for finding if the mouse overlaps with a sprite or ui element irrespective diff --git a/arcade/camera/controllers/simple_controller_functions.py b/arcade/camera/controllers/simple_controller_functions.py index a626bdbd4..ffcec7509 100644 --- a/arcade/camera/controllers/simple_controller_functions.py +++ b/arcade/camera/controllers/simple_controller_functions.py @@ -23,10 +23,9 @@ def strafe(data: CameraData, direction: Tuple[float, float]): Move the CameraData in a 2D direction aligned to the up-right plane of the view. A value of [1, 0] will move the camera sideways while a value of [0, 1] will move the camera upwards. Works irrespective of which direction the camera is facing. - Ensure the up and forward vectors are unit-length or the size of the motion will be incorrect. """ - _forward = Vec3(*data.forward) - _up = Vec3(*data.up) + _forward = Vec3(*data.forward).normalize() + _up = Vec3(*data.up).normalize() _right = _forward.cross(_up) _pos = data.position diff --git a/arcade/camera/perspective.py b/arcade/camera/perspective.py deleted file mode 100644 index 6c265ea35..000000000 --- a/arcade/camera/perspective.py +++ /dev/null @@ -1,178 +0,0 @@ -from typing import Optional, Tuple, Iterator, TYPE_CHECKING -from contextlib import contextmanager - -from pyglet.math import Mat4, Vec3, Vec4 -from math import tan, radians - -import arcade -from arcade.camera.data import CameraData, PerspectiveProjectionData -from arcade.camera.types import Projector - -from arcade.window_commands import get_window -if TYPE_CHECKING: - from arcade import Window - - -__all__ = [ - 'PerspectiveProjector' -] - - -class PerspectiveProjector: - """ - 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[CameraData] = None, - projection: Optional[PerspectiveProjectionData] = None): - """ - Initialize a Projector which produces a perspective projection matrix using - a CameraData and PerspectiveProjectionData PoDs. - - Args: - window: The window to bind the camera to. Defaults to the currently active camera. - view: The CameraData PoD. contains the viewport, position, up, forward, and zoom. - projection: The PerspectiveProjectionData PoD. - contains the aspect ratio, fov, near plane, and far plane. - """ - self._window: "Window" = window or get_window() - - self._view = view or CameraData( - (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, 1000, # Near, Far - (0, 0, self._window.width, self._window.height), # Viewport - ) - - @property - def view(self) -> CameraData: - """ - Is a CameraData. Is a read only property - """ - return self._view - - @property - def projection(self) -> PerspectiveProjectionData: - """ - Is the PerspectiveProjectionData. is a read only property. - """ - return self._projection - - 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) # Initial Up Vector (Not necessarily perpendicular to forward vector) - ri = fo.cross(up).normalize() # Right Vector - up = ri.cross(fo).normalize() # 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._projection.viewport - self._window.projection = _projection - self._window.view = _view - - @contextmanager - def activate(self) -> Iterator[Projector]: - """ - A context manager version of PerspectiveProjector.use() which allows for the use of - `with` blocks. For example, `with camera.activate() as cam: ...`.. - """ - previous_projector = self._window.current_camera - try: - self.use() - yield self - finally: - previous_projector.use() - - def map_coordinate(self, screen_coordinate: Tuple[float, float], depth: Optional[float] = None) -> Tuple[float, float, float]: - """ - Take in a pixel coordinate from within - the range of the window size and returns - the world space coordinates. - - Essentially reverses the effects of the projector. - - The scale changes as the depth increases. You can - - Args: - screen_coordinate: A 2D position in pixels from the bottom left of the screen. - This should ALWAYS be in the range of 0.0 - screen size. - depth: The depth of the query. equivalent to the 'Z' coordinate. - If left as None it will attempt to match the scale of the screen. - Returns: - A 3D vector in world space. - """ - depth = depth or self._projection.viewport[3] / (2 * tan(radians(self._projection.fov/2))) - - screen_x = 2.0 * (screen_coordinate[0] - self._projection.viewport[0]) / self._projection.viewport[2] - 1 - screen_y = 2.0 * (screen_coordinate[1] - self._projection.viewport[1]) / self._projection.viewport[3] - 1 - screen_z = (depth - self._projection.near) / (self._projection.far - self._projection.near) - - _view = self._generate_view_matrix() - _projection = self._generate_projection_matrix() - - print(screen_x, screen_y, screen_z) - - screen_position = Vec4(screen_x, screen_y, screen_z, 1.0) - - _full = ~(_projection @ _view) - - _mapped_position = _full @ screen_position - - return _mapped_position[0], _mapped_position[1], _mapped_position[2] diff --git a/arcade/examples/camera_perspective_demo.py b/arcade/examples/camera_perspective_demo.py deleted file mode 100644 index 0ea504e34..000000000 --- a/arcade/examples/camera_perspective_demo.py +++ /dev/null @@ -1,126 +0,0 @@ -import arcade -import arcade.gl as gl - -# TODO: comment for days -# TODO: Make fit arcade standards for demo - -win = arcade.Window() -win.set_exclusive_mouse() -geo = gl.geometry.cube((0.5, 0.5, 0.5)) -prog = win.ctx.program( - vertex_shader="\n".join(( - "#version 330", - "uniform WindowBlock {", - " mat4 projection;", - " mat4 view;", - "} window;", - "in vec3 in_position;", - "in vec3 in_normal;", - "in vec2 in_uv;", - "", - "out vec3 vs_normal;", - "out vec2 vs_uv;", - "void main() {", - "gl_Position = window.projection * window.view * vec4(in_position, 1.0);", - "vs_normal = in_normal;", - "vs_uv = in_uv;", - "}" - )), - fragment_shader="\n".join(( - "#version 330", - "in vec3 vs_normal;", - "in vec2 vs_uv;", - "out vec4 fs_colour;", - "", - "void main() {", - "vec3 uv_colour = vec3(vs_uv, 0.2);", - "float light_intensity = 0.2 + max(0.0, dot(vs_normal, vec3(pow(3, -0.5), pow(3, -0.5), pow(3, -0.5))));", - "fs_colour = vec4(light_intensity * uv_colour, 1.0);", - "}" - )) -) - -cam = arcade.camera.PerspectiveProjector() -cam.view.position = (0.0, 0.0, 1.0) - -forward = 0 -strafe = 0 - - -def on_mouse_motion(x, y, dx, dy): - _l = (dx**2 + dy**2)**0.5 - - arcade.camera.controllers.rotate_around_up(cam.view, 2.0 * dx/_l) - _f = cam.view.forward - arcade.camera.controllers.rotate_around_right(cam.view, 2.0 * -dy/_l, up=False) - cam_dot = cam.view.forward[0]*cam.view.up[0]+cam.view.forward[1]*cam.view.up[1]+cam.view.forward[2]*cam.view.up[2] - if abs(cam_dot) > 0.90: - cam.view.forward = _f - - -win.on_mouse_motion = on_mouse_motion - - -def on_key_press(symbol, modifier): - global forward, strafe - if symbol == arcade.key.ESCAPE: - win.close() - - if symbol == arcade.key.W: - forward += 1 - elif symbol == arcade.key.S: - forward -= 1 - elif symbol == arcade.key.D: - strafe += 1 - elif symbol == arcade.key.A: - strafe -= 1 - - -win.on_key_press = on_key_press - - -def on_key_release(symbol, modifier): - global forward, strafe - if symbol == arcade.key.W: - forward -= 1 - elif symbol == arcade.key.S: - forward += 1 - elif symbol == arcade.key.D: - strafe -= 1 - elif symbol == arcade.key.A: - strafe += 1 - - -win.on_key_release = on_key_release - - -def on_update(delta_time): - win.set_mouse_position(0, 0) - arcade.camera.controllers.strafe(cam.view, (strafe * delta_time * 1.0, 0.0)) - - _pos = cam.view.position - _for = cam.view.forward - - cam.view.position = ( - _pos[0] + _for[0] * forward * delta_time * 1.0, - _pos[1] + _for[1] * forward * delta_time * 1.0, - _pos[2] + _for[2] * forward * delta_time * 1.0, - ) - - -win.on_update = on_update - - -def on_draw(): - win.ctx.enable(win.ctx.DEPTH_TEST) - win.clear() - - with cam.activate(): - geo.render(prog) - - arcade.draw_text("Press ESC to quit", win.width//2, 25, anchor_x="center") - -win.on_draw = on_draw - -win.run() - diff --git a/tests/unit/camera/test_camera_controller_methods.py b/tests/unit/camera/test_camera_controller_methods.py index b63e6e48f..5c8565488 100644 --- a/tests/unit/camera/test_camera_controller_methods.py +++ b/tests/unit/camera/test_camera_controller_methods.py @@ -36,15 +36,21 @@ def test_strafe(window: Window): def test_rotate_around_forward(window: Window): + # TODO + # Given camera_data = camera.CameraData() # When + controllers.rotate_around_forward(camera_data, 90) # Then + assert camera_data.up == pytest.approx((-1.0, 0.0, 0.0)) def test_rotate_around_up(window: Window): + # TODO + # Given camera_data = camera.CameraData() @@ -54,6 +60,8 @@ def test_rotate_around_up(window: Window): def test_rotate_around_right(window: Window): + # TODO + # Given camera_data = camera.CameraData() @@ -63,6 +71,8 @@ def test_rotate_around_right(window: Window): def test_interpolate(window: Window): + # TODO + # Given camera_data = camera.CameraData() @@ -72,6 +82,8 @@ def test_interpolate(window: Window): def test_simple_follow(window: Window): + # TODO + # Given camera_data = camera.CameraData() @@ -81,6 +93,8 @@ def test_simple_follow(window: Window): def test_simple_easing(window: Window): + # TODO + # Given camera_data = camera.CameraData() diff --git a/tests/unit/camera/test_orthographic_camera.py b/tests/unit/camera/test_orthographic_camera.py index c377fc018..d302a1e2e 100644 --- a/tests/unit/camera/test_orthographic_camera.py +++ b/tests/unit/camera/test_orthographic_camera.py @@ -6,41 +6,47 @@ def test_orthographic_projector_use(window: Window): # Given from pyglet.math import Mat4 - camera_default = camera.OrthographicProjector() + ortho_camera = camera.OrthographicProjector() - view_matrix = camera_default._generate_view_matrix() - proj_matrix = camera_default._generate_projection_matrix() + view_matrix = ortho_camera._generate_view_matrix() + proj_matrix = ortho_camera._generate_projection_matrix() # When - camera_default.use() + ortho_camera.use() # Then - assert window.current_camera is camera_default + assert window.current_camera is ortho_camera assert window.ctx.view_matrix == view_matrix assert window.ctx.projection_matrix == proj_matrix + # Reset the window for later tests + window.default_camera.use() + def test_orthographic_projector_activate(window: Window): # Given - camera_default: camera.OrthographicProjector = camera.OrthographicProjector() + ortho_camera: camera.OrthographicProjector = camera.OrthographicProjector() - view_matrix = camera_default._generate_view_matrix() - proj_matrix = camera_default._generate_projection_matrix() + view_matrix = ortho_camera._generate_view_matrix() + proj_matrix = ortho_camera._generate_projection_matrix() # When - with camera_default.activate() as cam: + with ortho_camera.activate() as cam: # Initially - assert window.current_camera is cam is camera_default + assert window.current_camera is cam is ortho_camera assert window.ctx.view_matrix == view_matrix assert window.ctx.projection_matrix == proj_matrix # Finally assert window.current_camera is window.default_camera + # Reset the window for later tests + window.default_camera.use() + def test_orthographic_projector_map_coordinates(window: Window): # Given - camera_default = camera.OrthographicProjector() + ortho_camera = camera.OrthographicProjector() # When mouse_pos_a = (100.0, 100.0) @@ -48,15 +54,15 @@ def test_orthographic_projector_map_coordinates(window: Window): mouse_pos_c = (230.0, 800.0) # Then - assert camera_default.map_coordinate(mouse_pos_a) == pytest.approx((100.0, 100.0, 0.0)) - assert camera_default.map_coordinate(mouse_pos_b) == pytest.approx((100.0, 0.0, 0.0)) - assert camera_default.map_coordinate(mouse_pos_c) == pytest.approx((230.0, 800.0, 0.0)) + assert ortho_camera.map_coordinate(mouse_pos_a) == pytest.approx((100.0, 100.0, 0.0)) + assert ortho_camera.map_coordinate(mouse_pos_b) == pytest.approx((100.0, 0.0, 0.0)) + assert ortho_camera.map_coordinate(mouse_pos_c) == pytest.approx((230.0, 800.0, 0.0)) def test_orthographic_projector_map_coordinates_move(window: Window): # Given - camera_default = camera.OrthographicProjector() - default_view = camera_default.view + ortho_camera = camera.OrthographicProjector() + default_view = ortho_camera.view mouse_pos_a = (window.width//2, window.height//2) mouse_pos_b = (100.0, 100.0) @@ -65,8 +71,8 @@ def test_orthographic_projector_map_coordinates_move(window: Window): default_view.position = (0.0, 0.0, 0.0) # Then - assert camera_default.map_coordinate(mouse_pos_a) == pytest.approx((0.0, 0.0, 0.0)) - assert camera_default.map_coordinate(mouse_pos_b) == pytest.approx((-300.0, -200.0, 0.0)) + assert ortho_camera.map_coordinate(mouse_pos_a) == pytest.approx((0.0, 0.0, 0.0)) + assert ortho_camera.map_coordinate(mouse_pos_b) == pytest.approx((-300.0, -200.0, 0.0)) # And @@ -74,14 +80,14 @@ def test_orthographic_projector_map_coordinates_move(window: Window): default_view.position = (100.0, 100.0, 0.0) # Then - assert camera_default.map_coordinate(mouse_pos_a) == pytest.approx((100.0, 100.0, 0.0)) - assert camera_default.map_coordinate(mouse_pos_b) == pytest.approx((-200.0, -100.0, 0.0)) + assert ortho_camera.map_coordinate(mouse_pos_a) == pytest.approx((100.0, 100.0, 0.0)) + assert ortho_camera.map_coordinate(mouse_pos_b) == pytest.approx((-200.0, -100.0, 0.0)) def test_orthographic_projector_map_coordinates_rotate(window: Window): # Given - camera_default = camera.OrthographicProjector() - default_view = camera_default.view + ortho_camera = camera.OrthographicProjector() + default_view = ortho_camera.view mouse_pos_a = (window.width//2, window.height//2) mouse_pos_b = (100.0, 100.0) @@ -91,8 +97,8 @@ def test_orthographic_projector_map_coordinates_rotate(window: Window): default_view.position = (0.0, 0.0, 0.0) # Then - assert camera_default.map_coordinate(mouse_pos_a) == pytest.approx((0.0, 0.0, 0.0)) - assert camera_default.map_coordinate(mouse_pos_b) == pytest.approx((-200.0, 300.0, 0.0)) + assert ortho_camera.map_coordinate(mouse_pos_a) == pytest.approx((0.0, 0.0, 0.0)) + assert ortho_camera.map_coordinate(mouse_pos_b) == pytest.approx((-200.0, 300.0, 0.0)) # And @@ -101,14 +107,14 @@ def test_orthographic_projector_map_coordinates_rotate(window: Window): default_view.position = (100.0, 100.0, 0.0) # Then - assert camera_default.map_coordinate(mouse_pos_a) == pytest.approx((100.0, 100.0, 0.0)) - assert camera_default.map_coordinate(mouse_pos_b) == pytest.approx((-253.553390, 170.710678, 0.0)) + assert ortho_camera.map_coordinate(mouse_pos_a) == pytest.approx((100.0, 100.0, 0.0)) + assert ortho_camera.map_coordinate(mouse_pos_b) == pytest.approx((-253.553390, 170.710678, 0.0)) def test_orthographic_projector_map_coordinates_zoom(window: Window): # Given - camera_default = camera.OrthographicProjector() - default_view = camera_default.view + ortho_camera = camera.OrthographicProjector() + default_view = ortho_camera.view mouse_pos_a = (window.width, window.height) mouse_pos_b = (100.0, 100.0) @@ -117,8 +123,8 @@ def test_orthographic_projector_map_coordinates_zoom(window: Window): default_view.zoom = 2.0 # Then - assert camera_default.map_coordinate(mouse_pos_a) == pytest.approx((window.width*0.75, window.height*0.75, 0.0)) - assert camera_default.map_coordinate(mouse_pos_b) == pytest.approx((250.0, 200.0, 0.0)) + assert ortho_camera.map_coordinate(mouse_pos_a) == pytest.approx((window.width*0.75, window.height*0.75, 0.0)) + assert ortho_camera.map_coordinate(mouse_pos_b) == pytest.approx((250.0, 200.0, 0.0)) # And @@ -127,18 +133,5 @@ def test_orthographic_projector_map_coordinates_zoom(window: Window): default_view.zoom = 0.25 # Then - assert camera_default.map_coordinate(mouse_pos_a) == pytest.approx((window.width*2.0, window.height*2.0, 0.0)) - assert camera_default.map_coordinate(mouse_pos_b) == pytest.approx((-1200.0, -800.0, 0.0)) - - -def test_perspective_projector_map_coordinates_depth(window): - # Given - camera_default = camera.PerspectiveProjector() - default_view = camera_default.view - - mouse_pos_a = (window.width, window.height) - mouse_pos_b = (100.0, 100.0) - - # When - - # Then + assert ortho_camera.map_coordinate(mouse_pos_a) == pytest.approx((window.width*2.0, window.height*2.0, 0.0)) + assert ortho_camera.map_coordinate(mouse_pos_b) == pytest.approx((-1200.0, -800.0, 0.0)) \ No newline at end of file diff --git a/tests/unit/camera/test_perspective_camera.py b/tests/unit/camera/test_perspective_camera.py deleted file mode 100644 index e81ee3c01..000000000 --- a/tests/unit/camera/test_perspective_camera.py +++ /dev/null @@ -1,133 +0,0 @@ -import pytest as pytest - -from arcade import camera, Window - - -def test_perspective_projector_use(window: Window): - # Given - camera_default = camera.PerspectiveProjector() - - view_matrix = camera_default._generate_view_matrix() - proj_matrix = camera_default._generate_projection_matrix() - - # When - camera_default.use() - - # Then - assert window.current_camera is camera_default - assert window.ctx.view_matrix == view_matrix - assert window.ctx.projection_matrix == proj_matrix - - -def test_perspective_projector_activate(window: Window): - # Given - camera_default = camera.PerspectiveProjector() - - view_matrix = camera_default._generate_view_matrix() - proj_matrix = camera_default._generate_projection_matrix() - - # When - with camera_default.activate() as cam: - # Initially - assert window.current_camera is cam is camera_default - assert window.ctx.view_matrix == view_matrix - assert window.ctx.projection_matrix == proj_matrix - - # Finally - assert window.current_camera is not window.default_camera - - -def test_perspective_projector_map_coordinates(window: Window): - # Given - camera_default = camera.PerspectiveProjector() - - # for d in range(int(camera_default.projection.near), int(camera_default.projection.far)): - # print(camera_default.map_coordinate((100.0, 100.0), d)) - - # When - mouse_pos_a = (100.0, 100.0) - mouse_pos_b = (100.0, 0.0) - mouse_pos_c = (230.0, 800.0) - - # Then - # assert camera_default.map_coordinate(mouse_pos_a) == pytest.approx((100.0, 100.0, 0.0)) - # assert camera_default.map_coordinate(mouse_pos_b) == pytest.approx((100.0, 0.0, 0.0)) - # assert camera_default.map_coordinate(mouse_pos_c) == pytest.approx((230.0, 800.0, 0.0)) - - -def test_perspective_projector_map_coordinates_move(window: Window): - # GivenT - camera_default = camera.PerspectiveProjector() - default_view = camera_default.view - - mouse_pos_a = (window.width//2, window.height//2) - mouse_pos_b = (100.0, 100.0) - - # When - default_view.position = (0.0, 0.0, 0.0) - - # Then - assert camera_default.map_coordinate(mouse_pos_a) == pytest.approx((0.0, 0.0, 0.0)) - assert camera_default.map_coordinate(mouse_pos_b) == pytest.approx((-300.0, -200.0, 0.0)) - - # And - - # When - default_view.position = (100.0, 100.0, 0.0) - - # Then - assert camera_default.map_coordinate(mouse_pos_a) == pytest.approx((100.0, 100.0, 0.0)) - assert camera_default.map_coordinate(mouse_pos_b) == pytest.approx((-200.0, -100.0, 0.0)) - - -def test_perspective_projector_map_coordinates_rotate(window: Window): - # Given - camera_default = camera.PerspectiveProjector() - default_view = camera_default.view - - mouse_pos_a = (window.width//2, window.height//2) - mouse_pos_b = (100.0, 100.0) - - # When - default_view.up = (1.0, 0.0, 0.0) - default_view.position = (0.0, 0.0, 0.0) - - # Then - assert camera_default.map_coordinate(mouse_pos_a) == pytest.approx((0.0, 0.0, 0.0)) - assert camera_default.map_coordinate(mouse_pos_b) == pytest.approx((-200.0, 300.0, 0.0)) - - # And - - # When - default_view.up = (2.0**-0.5, 2.0**-0.5, 0.0) - default_view.position = (100.0, 100.0, 0.0) - - # Then - assert camera_default.map_coordinate(mouse_pos_a) == pytest.approx((100.0, 100.0, 0.0)) - assert camera_default.map_coordinate(mouse_pos_b) == pytest.approx((-253.553390, 170.710678, 0.0)) - - -def test_perspective_projector_map_coordinates_zoom(window: Window): - # Given - camera_default = camera.PerspectiveProjector() - default_view = camera_default.view - - mouse_pos_a = (window.width, window.height) - mouse_pos_b = (100.0, 100.0) - - # When - default_view.zoom = 2.0 - - # Then - assert camera_default.map_coordinate(mouse_pos_a) == pytest.approx((window.width*0.75, window.height*0.75, 0.0)) - assert camera_default.map_coordinate(mouse_pos_b) == pytest.approx((250.0, 200.0, 0.0)) - - # And - - # When - default_view.position = (0.0, 0.0, 0.0) - default_view.zoom = 0.25 - - # Then - assert camera_default.map_coordinate(mouse_pos_a) == pytest.approx((window.width*2.0, window.height*2.0, 0.0)) - assert camera_default.map_coordinate(mouse_pos_b) == pytest.approx((-1200.0, -800.0, 0.0)) From e3d5d7a112d552aed88e20aafd7f7ce194ce9ba0 Mon Sep 17 00:00:00 2001 From: Einar Forselv Date: Sat, 24 Feb 2024 19:19:31 +0100 Subject: [PATCH 59/94] Missing tuple import --- arcade/context.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/arcade/context.py b/arcade/context.py index 69efa8537..119147b3a 100644 --- a/arcade/context.py +++ b/arcade/context.py @@ -5,7 +5,7 @@ from __future__ import annotations from pathlib import Path -from typing import Any, Iterable, Dict, Optional, Union, Sequence +from typing import Any, Iterable, Dict, Optional, Union, Sequence, Tuple from contextlib import contextmanager import pyglet From 1644ec387a603a0e442ab0010a2a03378b6a0a33 Mon Sep 17 00:00:00 2001 From: Einar Forselv Date: Sat, 24 Feb 2024 19:23:47 +0100 Subject: [PATCH 60/94] hwidth -> width --- arcade/camera/simple_camera.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/arcade/camera/simple_camera.py b/arcade/camera/simple_camera.py index 5c388a91c..bb9d3c41e 100644 --- a/arcade/camera/simple_camera.py +++ b/arcade/camera/simple_camera.py @@ -81,7 +81,7 @@ def __init__(self, *, 0.0, self._window.height ) self._projection = OrthographicProjectionData( - _projection[0] or 0.0, _projection[1] or self._window.hwidth, # Left, Right + _projection[0] or 0.0, _projection[1] or self._window.width, # Left, Right _projection[2] or 0.0, _projection[3] or self._window.height, # Bottom, Top near or -100, far or 100, # Near, Far viewport or (0, 0, self._window.width, self._window.height), # Viewport From 73e154be2886142a0fd6cfcd1bc79dc883cacc5c Mon Sep 17 00:00:00 2001 From: Einar Forselv Date: Sat, 24 Feb 2024 19:46:03 +0100 Subject: [PATCH 61/94] Revert cull_mode in SpriteList --- arcade/sprite_list/sprite_list.py | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/arcade/sprite_list/sprite_list.py b/arcade/sprite_list/sprite_list.py index c73b09767..68618401f 100644 --- a/arcade/sprite_list/sprite_list.py +++ b/arcade/sprite_list/sprite_list.py @@ -93,12 +93,6 @@ class SpriteList(Generic[SpriteType]): :ref:`pg_spritelist_advanced_lazy_spritelists` to learn more. :param visible: Setting this to False will cause the SpriteList to not be drawn. When draw is called, the method will just return without drawing. - :param cull_mode: The mode which determines how sprites are culled. - Only 'orthographic' and 'disabled' are the only options currently. - - 'orthographic' is a simple algorithm that assumes an orthographic projection. - This is the default in arcade. If using a perspective projection - it is highly recommended to not use this mode. - - 'disabled' is no algorithm, and is used when no valid mode is selected. """ def __init__( @@ -109,7 +103,6 @@ def __init__( capacity: int = 100, lazy: bool = False, visible: bool = True, - cull_mode: str = "orthographic" ): self.program: Optional[Program] = None self._atlas: Optional[TextureAtlas] = atlas @@ -117,8 +110,6 @@ def __init__( self._lazy = lazy self._visible = visible self._color: Tuple[float, float, float, float] = (1.0, 1.0, 1.0, 1.0) - # The cull mode which determines how sprites should be culled within the geometry shader. - self._cull_mode: str = cull_mode # The initial capacity of the spritelist buffers (internal) self._buf_capacity = abs(capacity) or _DEFAULT_CAPACITY @@ -200,8 +191,7 @@ def _init_deferred(self) -> None: return self.ctx = get_window().ctx - self.program = (self.ctx.sprite_list_program_cull if self._cull_mode == 'orthographic' else - self.ctx.sprite_list_program_no_cull) + self.program = self.ctx.sprite_list_program_cull if not self._atlas: self._atlas = self.ctx.default_atlas From 34c48ec57b329f74bba24fa608e7d038e6428add Mon Sep 17 00:00:00 2001 From: pushfoo <36696816+pushfoo@users.noreply.github.com> Date: Thu, 29 Feb 2024 18:45:38 -0500 Subject: [PATCH 62/94] Add & improve type annotations * Add missing annotations in simple_controller_classes.py * Add missing return annotation to OrthographicProjector.use() * Add missing return annotation to SimpleCamerea.use() * Add Camera Protocol type to arcade.camera.types * Correct overly-wide lines in ViewportProjector.__init__ * Add missing return annotation to ViewportProjector.viewport property * Fix return annotation + overwide lines in ViewportProjector.viewport setter * Add return type annotation to ViewportProjector.use() * Add return annotation to DefaultProjector.use() * Add return annotation to Camera2D.projection_width setter * Add return annotation to Camera2D.angle setter * Add Camera protocol to __all__ in arcade.camera.types * Use recursive definition in Projector Protocol --- arcade/camera/camera_2d.py | 4 +-- .../controllers/simple_controller_classes.py | 24 +++++++++--------- arcade/camera/default.py | 25 ++++++++++--------- arcade/camera/orthographic.py | 2 +- arcade/camera/simple_camera.py | 2 +- arcade/camera/types.py | 15 +++++++++-- 6 files changed, 42 insertions(+), 30 deletions(-) diff --git a/arcade/camera/camera_2d.py b/arcade/camera/camera_2d.py index d58cc67bc..b4e80fcd6 100644 --- a/arcade/camera/camera_2d.py +++ b/arcade/camera/camera_2d.py @@ -249,7 +249,7 @@ def projection_width(self) -> float: return self._projection.right - self._projection.left @projection_width.setter - def projection_width(self, _width: float): + def projection_width(self, _width: float) -> None: w = self.projection_width l = self.projection_left / w # Normalised Projection left r = self.projection_right / w # Normalised Projection Right @@ -607,7 +607,7 @@ def angle(self) -> float: return degrees(atan2(self._data.position[0], self._data.position[1])) @angle.setter - def angle(self, value: float): + def angle(self, value: float) -> None: """ Set the 2D UP vector using an angle. This starts with 0 degrees as [0, 1] diff --git a/arcade/camera/controllers/simple_controller_classes.py b/arcade/camera/controllers/simple_controller_classes.py index 34719b3f5..5ef7d6465 100644 --- a/arcade/camera/controllers/simple_controller_classes.py +++ b/arcade/camera/controllers/simple_controller_classes.py @@ -97,7 +97,7 @@ def duration(self) -> float: return self._acceleration_duration + self.falloff_duration @duration.setter - def duration(self, _duration: float): + def duration(self, _duration: float) -> None: if _duration <= 0.0: self.falloff_duration = -1.0 @@ -125,7 +125,7 @@ def acceleration_duration(self) -> float: return self._acceleration_duration @acceleration_duration.setter - def acceleration_duration(self, _duration): + def acceleration_duration(self, _duration: float) -> None: if _duration < 0.0: self._acceleration_duration = 0.0 else: @@ -143,7 +143,7 @@ def acceleration(self) -> float: return 1 / self._acceleration_duration @acceleration.setter - def acceleration(self, _acceleration: float): + def acceleration(self, _acceleration: float) -> None: if _acceleration <= 0.0: self._acceleration_duration = 0.0 else: @@ -162,7 +162,7 @@ def falloff(self) -> float: return (15 / 8) * (1 / self.falloff_duration) @falloff.setter - def falloff(self, _falloff: float): + def falloff(self, _falloff: float) -> None: if _falloff <= 0.0: self.falloff_duration = -1.0 else: @@ -188,7 +188,7 @@ def _falloff_amp(self, _t: float) -> float: """ return 1 - _t**3 * (_t * (_t * 6.0 - 15.0) + 10.0) - def _calc_max_amp(self): + def _calc_max_amp(self) -> float: """ Determine the maximum amplitude by using either _acceleration_amp() or _falloff_amp(). If falloff duration is less than 0.0 then the falloff never begins and @@ -206,13 +206,13 @@ def _calc_max_amp(self): return 0.0 - def _calc_amplitude(self): + def _calc_amplitude(self) -> float: _max_amp = self._calc_max_amp() _sin_amp = sin(self.shake_frequency * 2.0 * pi * self._length_shaking) return _sin_amp * _max_amp - def reset(self): + def reset(self) -> None: """ Reset the temporary shaking variables. WILL NOT STOP OR START SCREEN SHAKE. """ @@ -221,14 +221,14 @@ def reset(self): self._last_update_time = 0.0 self._length_shaking = 0.0 - def start(self): + def start(self) -> None: """ Start the screen-shake. """ self.reset() self._shaking = True - def stop(self): + def stop(self) -> None: """ Instantly stop the screen-shake. """ @@ -241,7 +241,7 @@ def stop(self): self.reset() self._shaking = False - def update(self, delta_time: float): + def update(self, delta_time: float) -> None: """ Update the time, and decide if the shaking should stop. Does not actually set the camera position. @@ -260,7 +260,7 @@ def update(self, delta_time: float): if self.falloff_duration > 0.0 and self._length_shaking >= self.duration: self.stop() - def update_camera(self): + def update_camera(self) -> None: """ Update the position of the camera. Call this just before using the camera. because the controller is modifying the PoD directly it stores the last @@ -293,7 +293,7 @@ def update_camera(self): ) self._last_update_time = self._length_shaking - def readjust_camera(self): + def readjust_camera(self) -> None: """ Can be called after the camera has been used revert the screen_shake. While not strictly necessary it is highly advisable. If you are moving the diff --git a/arcade/camera/default.py b/arcade/camera/default.py index cce973b99..096da4eef 100644 --- a/arcade/camera/default.py +++ b/arcade/camera/default.py @@ -31,28 +31,29 @@ def __init__(self, viewport: Optional[Tuple[int, int, int, int]] = None, *, wind window: The window to bind the camera to. Defaults to the currently active window. """ self._window = window or get_window() - self._viewport = viewport or self._window.ctx.viewport - self._projection_matrix: Mat4 = Mat4.orthogonal_projection(0.0, self._viewport[2], - 0.0, self._viewport[3], - -100, 100) + self._projection_matrix: Mat4 = Mat4.orthogonal_projection( + 0.0, self._viewport[2], + 0.0, self._viewport[3], + -100, 100 + ) @property - def viewport(self): + def viewport(self) -> Tuple[int, int, int, int]: """ The viewport use to derive projection and view matrix. """ return self._viewport @viewport.setter - def viewport(self, viewport: Tuple[int, int, int, int]): + def viewport(self, viewport: Tuple[int, int, int, int]) -> None: self._viewport = viewport + self._projection_matrix = Mat4.orthogonal_projection( + 0, viewport[2], + 0, viewport[3], + -100, 100) - self._projection_matrix = Mat4.orthogonal_projection(0, viewport[2], - 0, viewport[3], - -100, 100) - - def use(self): + def use(self) -> None: """ Set the window's projection and view matrix. Also sets the projector as the windows current camera. @@ -106,7 +107,7 @@ def __init__(self, *, window: Optional["Window"] = None): """ super().__init__(window=window) - def use(self): + def use(self) -> None: """ Set the window's Projection and View matrices. diff --git a/arcade/camera/orthographic.py b/arcade/camera/orthographic.py index a0cb38d5b..e87db484a 100644 --- a/arcade/camera/orthographic.py +++ b/arcade/camera/orthographic.py @@ -125,7 +125,7 @@ def _generate_view_matrix(self) -> Mat4: -ri.dot(po), -up.dot(po), fo.dot(po), 1 )) - def use(self): + def use(self) -> None: """ Sets the active camera to this object. Then generates the view and projection matrices. diff --git a/arcade/camera/simple_camera.py b/arcade/camera/simple_camera.py index bb9d3c41e..83cc889d1 100644 --- a/arcade/camera/simple_camera.py +++ b/arcade/camera/simple_camera.py @@ -317,7 +317,7 @@ def update(self): if self.position == self._position_goal: self._easing_speed = 0.0 - def use(self): + def use(self) -> None: """ Sets the active camera to this object. Then generates the view and projection matrices. diff --git a/arcade/camera/types.py b/arcade/camera/types.py index d0196d44c..093d70775 100644 --- a/arcade/camera/types.py +++ b/arcade/camera/types.py @@ -1,10 +1,12 @@ +from __future__ import annotations from typing import Protocol, Tuple, Iterator from contextlib import contextmanager __all__ = [ 'Projection', - 'Projector' + 'Projector', + 'Camera' ] @@ -20,8 +22,17 @@ def use(self) -> None: ... @contextmanager - def activate(self) -> Iterator["Projector"]: + def activate(self) -> Iterator[Projector]: ... def map_coordinate(self, screen_coordinate: Tuple[float, float], depth: float = 0.0) -> Tuple[float, ...]: ... + + +class Camera(Protocol): + + def use(self) -> None: + ... + + def activate(self) -> Iterator[Projector]: + ... From 752355036700374434015ade5b6168adbbd23d26 Mon Sep 17 00:00:00 2001 From: DragonMoffon Date: Fri, 1 Mar 2024 21:38:58 +1300 Subject: [PATCH 63/94] Remove pos from Camera2D --- arcade/camera/camera_2d.py | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/arcade/camera/camera_2d.py b/arcade/camera/camera_2d.py index b4e80fcd6..9abe5d592 100644 --- a/arcade/camera/camera_2d.py +++ b/arcade/camera/camera_2d.py @@ -140,19 +140,6 @@ def projection_data(self) -> OrthographicProjectionData: """ 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: - self._data.position = (_pos[0], _pos[1], self._data.position[2]) - @property def position(self) -> Tuple[float, float]: """ From 5e91c235c53441b276355b0671c9a5e4acac69f7 Mon Sep 17 00:00:00 2001 From: DragonMoffon Date: Sun, 3 Mar 2024 20:37:57 +1300 Subject: [PATCH 64/94] Add method to duplicate CameraData --- arcade/camera/data.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/arcade/camera/data.py b/arcade/camera/data.py index 09e5a6575..bb1a4ffb0 100644 --- a/arcade/camera/data.py +++ b/arcade/camera/data.py @@ -31,6 +31,10 @@ class CameraData: zoom: float = 1.0 +def duplicate_camera_data(origin: CameraData): + return CameraData(tuple(origin.position), tuple(origin.up), tuple(origin.forward), float(origin.zoom)) + + @dataclass class OrthographicProjectionData: """ From a255ea0efce720ae2e77572b8f957e47732d3abf Mon Sep 17 00:00:00 2001 From: DragonMoffon Date: Sun, 3 Mar 2024 20:56:22 +1300 Subject: [PATCH 65/94] Fix typing for linting as always >:) --- arcade/camera/data.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/arcade/camera/data.py b/arcade/camera/data.py index bb1a4ffb0..b2b71311f 100644 --- a/arcade/camera/data.py +++ b/arcade/camera/data.py @@ -32,7 +32,7 @@ class CameraData: def duplicate_camera_data(origin: CameraData): - return CameraData(tuple(origin.position), tuple(origin.up), tuple(origin.forward), float(origin.zoom)) + return CameraData(origin.position, origin.up, origin.forward, float(origin.zoom)) @dataclass From 57670af424cb604193ada62714127cab409b3ab6 Mon Sep 17 00:00:00 2001 From: Einar Forselv Date: Sat, 9 Mar 2024 18:57:20 +0100 Subject: [PATCH 66/94] Working test_strafe --- .../camera/test_camera_controller_methods.py | 20 +++++++------------ 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/tests/unit/camera/test_camera_controller_methods.py b/tests/unit/camera/test_camera_controller_methods.py index 5c8565488..fba660375 100644 --- a/tests/unit/camera/test_camera_controller_methods.py +++ b/tests/unit/camera/test_camera_controller_methods.py @@ -4,38 +4,32 @@ import arcade.camera.controllers as controllers -def test_strafe(window: Window): +def test_strafe(): # Given camera_data = camera.CameraData() directions = ((1.0, 0.0), (0.0, 1.0), (-1.0, 0.0), (0.0, -1.0), (0.5, 0.5)) # When camera_data.forward = (0.0 ,0.0, -1.0) - camera_data.up = (0.0, 0.0, 1.0) + camera_data.up = (0.0, 1.0, 0.0) # Then for dirs in directions: controllers.strafe(camera_data, dirs) - assert ( - camera_data.position == (dirs[0], dirs[1], 0.0), - f"Strafe failed to move the camera data correctly, {dirs}" - ) + assert camera_data.position == (dirs[0], dirs[1], 0.0), f"Strafe failed to move the camera data correctly, {dirs}" camera_data.position = (0.0, 0.0, 0.0) # Given - camera_data.forward = (0.0, 0.0, -1.0) - camera_data.up = (0.0, 0.0, 1.0) + camera_data.forward = (1.0, 0.0, 0.0) + camera_data.up = (0.0, 1.0, 0.0) for dirs in directions: controllers.strafe(camera_data, dirs) - assert ( - camera_data.position == (0.0, dirs[1], dirs[0]), - f"Strafe failed to move the camera data correctly, {dirs}" - ) + assert camera_data.position == (0.0, dirs[1], dirs[0]), f"Strafe failed to move the camera data correctly, {dirs}" camera_data.position = (0.0, 0.0, 0.0) -def test_rotate_around_forward(window: Window): +def test_rotate_around_forward(): # TODO # Given From 0286a6f72164f86655f3e06e95b8da04ccee84ef Mon Sep 17 00:00:00 2001 From: Einar Forselv Date: Sat, 9 Mar 2024 22:22:18 +0100 Subject: [PATCH 67/94] Docstring fixes * Initializer docs should be in the class docstring * Convert all google style docstrings to standard format --- arcade/camera/camera_2d.py | 59 ++++++------ .../controllers/simple_controller_classes.py | 41 ++++---- .../simple_controller_functions.py | 96 +++++++------------ arcade/camera/default.py | 20 ++-- arcade/camera/orthographic.py | 20 ++-- arcade/camera/simple_camera.py | 37 ++++--- 6 files changed, 111 insertions(+), 162 deletions(-) diff --git a/arcade/camera/camera_2d.py b/arcade/camera/camera_2d.py index 9abe5d592..607cde000 100644 --- a/arcade/camera/camera_2d.py +++ b/arcade/camera/camera_2d.py @@ -35,41 +35,36 @@ class Camera2D: - Projection with zoom scaling. - Viewport. - NOTE Once initialised, the CameraData and OrthographicProjectionData SHOULD NOT be changed. + NOTE: Once initialized, the CameraData and OrthographicProjectionData SHOULD NOT be changed. Only getter properties are provided through data and projection_data respectively. - """ + :param window: The Arcade Window to bind the camera to. + Defaults to the currently active window. + :param viewport: A 4-int tuple which defines the pixel bounds which the camera with project to. + :param position: The 2D position of the camera in the XY plane. + :param up: The 2D unit vector which defines the +Y-axis of the camera space. + :param zoom: A scalar value which is inversely proportional to the size of the camera projection. + i.e. a zoom of 2.0 halves the size of the projection, doubling the perceived size of objects. + :param projection: A 4-float tuple which defines the world space + bounds which the camera projects to the viewport. + :param near: The near clipping plane of the camera. + :param far: The far clipping plane of the camera. + :param camera_data: A CameraData PoD which describes the viewport, position, up, and zoom + :param projection_data: A OrthographicProjectionData PoD which describes the left, right, top, + bottom, far, near planes for an orthographic projection. + """ 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 - ): - """ - Initialize a Camera2D instance. Either with camera PoDs or individual arguments. - - Args: - window: The Arcade Window to bind the camera to. - Defaults to the currently active window. - viewport: A 4-int tuple which defines the pixel bounds which the camera with project to. - position: The 2D position of the camera in the XY plane. - up: The 2D unit vector which defines the +Y-axis of the camera space. - zoom: A scalar value which is inversely proportional to the size of the camera projection. - i.e. a zoom of 2.0 halves the size of the projection, doubling the perceived size of objects. - projection: A 4-float tuple which defines the world space - bounds which the camera projects to the viewport. - near: The near clipping plane of the camera. - far: The far clipping plane of the camera. - camera_data: A CameraData PoD which describes the viewport, position, up, and zoom - projection_data: A OrthographicProjectionData PoD which describes the left, right, top, - bottom, far, near planes for an orthographic projection. - """ + 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" = window or get_window() assert ( diff --git a/arcade/camera/controllers/simple_controller_classes.py b/arcade/camera/controllers/simple_controller_classes.py index 5ef7d6465..c91d5ef32 100644 --- a/arcade/camera/controllers/simple_controller_classes.py +++ b/arcade/camera/controllers/simple_controller_classes.py @@ -4,8 +4,6 @@ ScreenShakeController: Provides an easy way to cause a camera to shake. """ - - from typing import Tuple from math import exp, log, pi, sin, floor from random import uniform @@ -37,6 +35,19 @@ class ScreenShakeController: shake_frequency: The number of peaks per second. Avoid making it a multiple of half the target frame-rate. (e.g. at 60 fps avoid 30, 60, 90, 120, etc.) + + :param camera_data: The CameraData PoD that the controller modifies. + Should not be changed once initialized. + :param max_amplitude: The largest possible world space offset. + :param falloff_time: The length of time in seconds it takes the shaking + to reach 0 after reaching the maximum. Can be set + to a negative number to disable falloff. + :param acceleration_duration: The length of time in seconds it takes the + shaking to reach max amplitude. Can be set + to 0.0 to start at max amplitude. + :param shake_frequency: The number of peaks per second. Avoid making it + a multiple of half the target frame-rate. + (e.g. at 60 fps avoid 30, 60, 90, 120, etc.) """ def __init__(self, camera_data: CameraData, *, @@ -44,23 +55,6 @@ def __init__(self, camera_data: CameraData, *, falloff_time: float = 1.0, acceleration_duration: float = 1.0, shake_frequency: float = 15.0): - """ - Initialise a screen-shake controller. - - Args: - camera_data: The CameraData PoD that the controller modifies. - Should not be changed once initialised. - max_amplitude: The largest possible world space offset. - falloff_time: The length of time in seconds it takes the shaking - to reach 0 after reaching the maximum. Can be set - to a negative number to disable falloff. - acceleration_duration: The length of time in seconds it takes the - shaking to reach max amplitude. Can be set - to 0.0 to start at max amplitude. - shake_frequency: The number of peaks per second. Avoid making it - a multiple of half the target frame-rate. - (e.g. at 60 fps avoid 30, 60, 90, 120, etc.) - """ self._data: CameraData = camera_data self.max_amplitude: float = max_amplitude @@ -173,8 +167,7 @@ def _acceleration_amp(self, _t: float) -> float: The equation for the growing half of the amplitude equation. It uses 1.0001 so that at _t = 1.0 the amplitude equals 1.0. - Args: - _t: The scaled time. Should be between 0.0 and 1.0 + :param _t: The scaled time. Should be between 0.0 and 1.0 """ return 1.0001 - 1.0001*exp(log(0.0001/1.0001) * _t) @@ -183,8 +176,7 @@ def _falloff_amp(self, _t: float) -> float: The equation for the falloff half of the amplitude equation. It is based on the 'smootherstep' function. - Args: - _t: The scaled time. Should be between 0.0 and 1.0 + :param _t: The scaled time. Should be between 0.0 and 1.0 """ return 1 - _t**3 * (_t * (_t * 6.0 - 15.0) + 10.0) @@ -247,8 +239,7 @@ def update(self, delta_time: float) -> None: Does not actually set the camera position. Should not be called more than once an update cycle. - Args: - delta_time: the length of time in seconds between update calls. + :param delta_time: the length of time in seconds between update calls. Generally pass in the delta_time provided by the arcade.Window's on_update method. """ diff --git a/arcade/camera/controllers/simple_controller_functions.py b/arcade/camera/controllers/simple_controller_functions.py index ffcec7509..601e25633 100644 --- a/arcade/camera/controllers/simple_controller_functions.py +++ b/arcade/camera/controllers/simple_controller_functions.py @@ -71,19 +71,12 @@ def quaternion_rotation(axis: Tuple[float, float, float], rotation_point[0] + new_relative_position[0], rotation_point[1] + new_relative_position[1] ) - - Args: - axis: - The unit length vector that will be rotated around - vector: - The 3-dimensional vector to be rotated - angle: - The angle in degrees to rotate the vector clock-wise by - - Returns: - A rotated 3-dimension vector with the same length as the argument vector. + + :param axis: The unit length vector that will be rotated around + :param vector: The 3-dimensional vector to be rotated + :param angle: The angle in degrees to rotate the vector clock-wise by + :return: A rotated 3-dimension vector with the same length as the argument vector. """ - _rotation_rads = -radians(angle) p1, p2, p3 = vector _c2, _s2 = cos(_rotation_rads / 2.0), sin(_rotation_rads / 2.0) @@ -106,11 +99,8 @@ def rotate_around_forward(data: CameraData, angle: float): If that is not the center of the screen this method may appear erroneous. Uses arcade.camera.controllers.quaternion_rotation internally. - Args: - data: - The camera data to modify. The data's up vector is rotated around its forward vector - angle: - The angle in degrees to rotate clockwise by + :param data: The camera data to modify. The data's up vector is rotated around its forward vector + :param angle: The angle in degrees to rotate clockwise by """ data.up = quaternion_rotation(data.forward, data.up, angle) @@ -121,11 +111,8 @@ def rotate_around_up(data: CameraData, angle: float): Generally only useful in 3D games. Uses arcade.camera.controllers.quaternion_rotation internally. - Args: - data: - The camera data to modify. The data's forward vector is rotated around its up vector - angle: - The angle in degrees to rotate clockwise by + :param data: The camera data to modify. The data's forward vector is rotated around its up vector + :param angle: The angle in degrees to rotate clockwise by """ data.forward = quaternion_rotation(data.up, data.forward, angle) @@ -136,17 +123,11 @@ def rotate_around_right(data: CameraData, angle: float, forward: bool = True, up Generally only useful in 3D games. Uses arcade.camera.controllers.quaternion_rotation internally. - Args: - data: - The camera data to modify. The data's forward vector is rotated around its up vector - angle: - The angle in degrees to rotate clockwise by - forward: - Whether to rotate the forward vector around the right vector - up: - Whether to rotate the up vector around the right vector + :param data: The camera data to modify. The data's forward vector is rotated around its up vector + :param angle: The angle in degrees to rotate clockwise by + :param forward: Whether to rotate the forward vector around the right vector + :param up: Whether to rotate the up vector around the right vector """ - _forward = Vec3(data.forward[0], data.forward[1], data.forward[2]) _up = Vec3(data.up[0], data.up[1], data.up[2]) _crossed_vec = _forward.cross(_up) @@ -170,12 +151,10 @@ def simple_follow_3D(speed: float, target: Tuple[float, float, float], data: Cam """ A simple method which moves the camera linearly towards the target point. - Args: - speed: The percentage the camera should move towards the target (0.0 - 1.0 range) - target: The 3D position the camera should move towards in world space. - data: The camera data object which stores its position, rotation, and direction. + :param speed: The percentage the camera should move towards the target (0.0 - 1.0 range) + :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 = _interpolate_3D(data.position, target, speed) @@ -183,10 +162,9 @@ 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. - Args: - speed: The percentage the camera should move towards the target (0.0 - 1.0 range) - target: The 2D position the camera should move towards in world space. (vector in XY-plane) - data: The camera data object which stores its position, rotation, and direction. + :param speed: The percentage the camera should move towards the target (0.0 - 1.0 range) + :param target: The 2D position the camera should move towards in world space. (vector in XY-plane) + :param data: The camera data object which stores its position, rotation, and direction. """ simple_follow_3D(speed, (target[0], target[1], 0.0), data) @@ -200,15 +178,14 @@ def simple_easing_3D(percent: float, It uses an easing function to make the motion smoother. You can use the collection of easing methods found in arcade.easing. - Args: - percent: The percentage from 0 to 1 which describes - how far between the two points to place the camera. - start: The 3D point which acts as the starting point for the camera motion. - target: The 3D point which acts as the final destination for the camera. - data: The camera data object which stores its position, rotation, and direction. - 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. + :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 = _interpolate_3D(start, target, func(percent)) @@ -223,15 +200,14 @@ def simple_easing_2D(percent: float, It uses an easing function to make the motion smoother. You can use the collection of easing methods found in arcade.easing. - Args: - percent: The percentage from 0 to 1 which describes - how far between the two points to place the camera. - start: The 2D point which acts as the starting point for the camera motion. - target: The 2D point which acts as the final destination for the camera. - data: The camera data object which stores its position, rotation, and direction. - 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. + + :param percent: The percentage from 0 to 1 which describes + how far between the two points to place the camera. + :param start: The 2D point which acts as the starting point for the camera motion. + :param target: The 2D 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_3D(percent, (start[0], start[1], 0.0), (target[0], target[1], 0.0), data, func) diff --git a/arcade/camera/default.py b/arcade/camera/default.py index 096da4eef..ee42dd943 100644 --- a/arcade/camera/default.py +++ b/arcade/camera/default.py @@ -20,16 +20,12 @@ class ViewportProjector: Does not have a way of moving, rotating, or zooming the camera. perfect for something like UI or for mapping to an offscreen framebuffer. - """ + Args: + viewport: The viewport to project to. + window: The window to bind the camera to. Defaults to the currently active window. + """ def __init__(self, viewport: Optional[Tuple[int, int, int, int]] = None, *, window: Optional["Window"] = None): - """ - Initialize a ViewportProjector - - Args: - viewport: The viewport to project to. - window: The window to bind the camera to. Defaults to the currently active window. - """ self._window = window or get_window() self._viewport = viewport or self._window.ctx.viewport self._projection_matrix: Mat4 = Mat4.orthogonal_projection( @@ -95,16 +91,12 @@ class DefaultProjector(ViewportProjector): """ 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. + + :param window: The window to bind the camera to. Defaults to the currently active camera. """ # TODO: ADD PARAMS TO DOC FOR __init__ def __init__(self, *, window: Optional["Window"] = None): - """ - Initialise a ViewportProjector. - - Args: - window: The window to bind the camera to. Defaults to the currently active camera. - """ super().__init__(window=window) def use(self) -> None: diff --git a/arcade/camera/orthographic.py b/arcade/camera/orthographic.py index e87db484a..d8d93b4d1 100644 --- a/arcade/camera/orthographic.py +++ b/arcade/camera/orthographic.py @@ -30,22 +30,20 @@ class OrthographicProjector: 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. + profile before optimizing with a dirty value check. + + Initialize a Projector which produces an orthographic projection matrix using + a CameraData and PerspectiveProjectionData PoDs. + + :param window: The window to bind the camera to. Defaults to the currently active camera. + :param view: The CameraData PoD. contains the viewport, position, up, forward, and zoom. + :param projection: The OrthographicProjectionData PoD. + contains the left, right, bottom top, near, and far planes. """ def __init__(self, *, window: Optional["Window"] = None, view: Optional[CameraData] = None, projection: Optional[OrthographicProjectionData] = None): - """ - Initialize a Projector which produces an orthographic projection matrix using - a CameraData and PerspectiveProjectionData PoDs. - - Args: - window: The window to bind the camera to. Defaults to the currently active camera. - view: The CameraData PoD. contains the viewport, position, up, forward, and zoom. - projection: The OrthographicProjectionData PoD. - contains the left, right, bottom top, near, and far planes. - """ self._window: "Window" = window or get_window() self._view = view or CameraData( # Viewport diff --git a/arcade/camera/simple_camera.py b/arcade/camera/simple_camera.py index 83cc889d1..9c3168477 100644 --- a/arcade/camera/simple_camera.py +++ b/arcade/camera/simple_camera.py @@ -25,8 +25,24 @@ class SimpleCamera: It also implements an update method that allows for an interpolation between two points Written to be backwards compatible with the old SimpleCamera. - """ + Initialize a Simple Camera Instance with either Camera PoDs or individual arguments + + :param window: The Arcade Window to bind the camera to. + Defaults to the currently active window. + :param viewport: A 4-int tuple which defines the pixel bounds which the camera with project to. + :param position: The 2D position of the camera in the XY plane. + :param up: The 2D unit vector which defines the +Y-axis of the camera space. + :param zoom: A scalar value which is inversely proportional to the size of the camera projection. + i.e. a zoom of 2.0 halves the size of the projection, doubling the perceived size of objects. + :param projection: A 4-float tuple which defines the world space + bounds which the camera projects to the viewport. + :param near: The near clipping plane of the camera. + :param far: The far clipping place of the camera. + :param camera_data: A CameraData PoD which describes the viewport, position, up, and zoom + :param projection_data: A OrthographicProjectionData PoD which describes the left, right, top, + bottom, far, near planes for an orthographic projection. + """ def __init__(self, *, window: Optional["Window"] = None, viewport: Optional[Tuple[int, int, int, int]] = None, @@ -39,25 +55,6 @@ def __init__(self, *, camera_data: Optional[CameraData] = None, projection_data: Optional[OrthographicProjectionData] = None ): - """ - Initialize a Simple Camera Instance with either Camera PoDs or individual arguments - - Args: - window: The Arcade Window to bind the camera to. - Defaults to the currently active window. - viewport: A 4-int tuple which defines the pixel bounds which the camera with project to. - position: The 2D position of the camera in the XY plane. - up: The 2D unit vector which defines the +Y-axis of the camera space. - zoom: A scalar value which is inversely proportional to the size of the camera projection. - i.e. a zoom of 2.0 halves the size of the projection, doubling the perceived size of objects. - projection: A 4-float tuple which defines the world space - bounds which the camera projects to the viewport. - near: The near clipping plane of the camera. - far: The far clipping place of the camera. - camera_data: A CameraData PoD which describes the viewport, position, up, and zoom - projection_data: A OrthographicProjectionData PoD which describes the left, right, top, - bottom, far, near planes for an orthographic projection. - """ warn("arcade.camera.SimpleCamera has been depreciated please use arcade.camera.Camera2D instead", DeprecationWarning) From 055f73447fb0b1ef5f15ecf5685beb2da548ea00 Mon Sep 17 00:00:00 2001 From: Paul <36696816+pushfoo@users.noreply.github.com> Date: Mon, 11 Mar 2024 04:00:34 -0400 Subject: [PATCH 68/94] Improve camera docs, typing, and style (#2) * Cleanup the docstrings for properties in Camera2D * Simplify projection initialization in Camera2D * Remove redundant bool() calls in Camera2D.__init__ * Further simplify Camera2D.__init__ * Docstring cleanup for arcade.camera.data * Camera2D top-level docstring improvements --- arcade/camera/camera_2d.py | 105 ++++++++++++++++++++----------------- arcade/camera/data.py | 16 +++--- 2 files changed, 66 insertions(+), 55 deletions(-) diff --git a/arcade/camera/camera_2d.py b/arcade/camera/camera_2d.py index 607cde000..f922f2689 100644 --- a/arcade/camera/camera_2d.py +++ b/arcade/camera/camera_2d.py @@ -30,13 +30,17 @@ class Camera2D: 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 initialized, the CameraData and OrthographicProjectionData SHOULD NOT be changed. - Only getter properties are provided through data and projection_data respectively. + * View Data, or where the camera is in + * Projection without zoom scaling. + * Projection with zoom scaling. + * Viewport in screen pixels + + .. warning:: Do not replace the ``camera_data`` and ``projection_data`` + instances after initialization! + + Replacing the camera data and projection data may break controllers. Their + contents are exposed via properties rather than directly to prevent this. :param window: The Arcade Window to bind the camera to. Defaults to the currently active window. @@ -49,9 +53,10 @@ class Camera2D: bounds which the camera projects to the viewport. :param near: The near clipping plane of the camera. :param far: The far clipping plane of the camera. - :param camera_data: A CameraData PoD which describes the viewport, position, up, and zoom - :param projection_data: A OrthographicProjectionData PoD which describes the left, right, top, - bottom, far, near planes for an orthographic projection. + :param camera_data: A :py:class:`~arcade.camera.data.CameraData` + describing the viewport, position, up, and zoom. + :param projection_data: A :py:class:`~arcade.camera.data.OrthographicProjectionData` + which describes the left, right, top, bottom, far, near planes for an orthographic projection. """ def __init__(self, *, window: Optional["Window"] = None, @@ -65,22 +70,26 @@ def __init__(self, *, camera_data: Optional[CameraData] = None, projection_data: Optional[OrthographicProjectionData] = None ): - self._window: "Window" = window or get_window() + window = window or get_window() + self._window: "Window" = window assert ( - not any((viewport, position, up, zoom)) or not bool(camera_data) + not any((viewport, position, up, zoom)) or not camera_data ), ( "Camera2D Warning: Provided both a CameraData object and raw values. Defaulting to CameraData." ) assert ( - not any((projection, near, far)) or not bool(projection_data) + not any((projection, near, far)) or not 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) + half_width = window.width/2 + half_height = window.height/2 + + _pos = position or (half_width, half_height) _up = up or (0.0, 1.0) self._data = camera_data or CameraData( (_pos[0], _pos[1], 0.0), @@ -89,16 +98,11 @@ 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 - ) - self._projection = projection_data or OrthographicProjectionData( - _proj[0], _proj[1], # Left and Right. - _proj[2], _proj[3], # Bottom and Top. + self._projection: OrthographicProjectionData = projection_data or OrthographicProjectionData( + -half_width, half_width, # Left and Right. + -half_height, half_height, # Bottom and Top. near or 0.0, far or 100.0, # Near and Far. - - viewport or (0, 0, self._window.width, self._window.height) # Viewport + viewport or (0, 0, window.width, window.height) # Viewport ) self._ortho_projector: OrthographicProjector = OrthographicProjector( @@ -109,21 +113,27 @@ def __init__(self, *, @property def view_data(self) -> CameraData: - """ - Return the view data for the camera. This includes the - position, forward vector, up direction, and zoom. + """The view data for the camera. - If you use any of the built-in arcade camera-controllers - or make your own this is the property to access. + This includes: + + * the position + * forward vector + * up direction + * zoom. + + Camera controllers use this property. You will need to access + it if you use implement a custom one. """ return self._data @property def projection_data(self) -> OrthographicProjectionData: - """ - Return the projection data for the camera. + """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 @@ -137,11 +147,7 @@ def projection_data(self) -> OrthographicProjectionData: @property def position(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. - """ + """The 2D world position of the camera along the X and Y axes.""" return self._data.position[:2] @position.setter @@ -150,9 +156,9 @@ def position(self, _pos: Tuple[float, float]) -> None: @property def left(self) -> float: - """ - The left side of the camera in world space. - Use this to check if a sprite is on screen. + """The left side of the camera in world space. + + Useful for checking if a :py:class:`~arcade.Sprite` is on screen. """ return self._data.position[0] + self._projection.left/self._data.zoom @@ -162,9 +168,9 @@ def left(self, _left: float) -> None: @property def right(self) -> float: - """ - The right side of the camera in world space. - Use this to check if a sprite is on screen. + """The right edge of the camera in world space. + + Useful for checking if a :py:class:`~arcade.Sprite` is on screen. """ return self._data.position[0] + self._projection.right/self._data.zoom @@ -174,9 +180,9 @@ def right(self, _right: float) -> None: @property def bottom(self) -> float: - """ - The bottom side of the camera in world space. - Use this to check if a sprite is on screen. + """The bottom edge of the camera in world space. + + Useful for checking if a :py:class:`~arcade.Sprite` is on screen. """ return self._data.position[1] + self._projection.bottom/self._data.zoom @@ -190,9 +196,9 @@ def bottom(self, _bottom: float) -> None: @property def top(self) -> float: - """ - The top side of the camera in world space. - Use this to check if a sprite is on screen. + """The top edge of the camera in world space. + + Useful for checking if a :py:class:`~arcade.Sprite` is on screen. """ return self._data.position[1] + self._projection.top/self._data.zoom @@ -206,9 +212,10 @@ def top(self, _top: float) -> None: @property def projection(self) -> Tuple[float, float, float, float]: - """ - The left, right, bottom, top values - that maps world space coordinates to pixel positions. + """The camera's left, right, bottom, top projection values. + + These control how the camera projects the world onto the pixels + of the screen. """ _p = self._projection return _p.left, _p.right, _p.bottom, _p.top diff --git a/arcade/camera/data.py b/arcade/camera/data.py index b2b71311f..ad5fd6447 100644 --- a/arcade/camera/data.py +++ b/arcade/camera/data.py @@ -1,3 +1,8 @@ +"""Dataclasses supporting cameras. + +These are placed in their own module to simplify imports due to their +wide usage throughout Arcade's camera code. +""" from typing import Tuple from dataclasses import dataclass @@ -11,8 +16,9 @@ @dataclass class CameraData: - """ - A PoD (Packet of Data) which holds the necessary data for a functional camera excluding the projection data. + """Stores position, orientation, and zoom for a camera. + + This is like where a camera is placed in 3D space. Attributes: position: A 3D vector which describes where the camera is located. @@ -37,8 +43,7 @@ def duplicate_camera_data(origin: CameraData): @dataclass class OrthographicProjectionData: - """ - A PoD (Packet of Data) which holds the necessary data for a functional Orthographic Projection matrix. + """Describes an Orthographic projection. 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 @@ -65,8 +70,7 @@ class OrthographicProjectionData: @dataclass class PerspectiveProjectionData: - """ - A PoD (Packet of Data) which holds the necessary data for a functional Perspective matrix. + """Describes a perspective projection. Attributes: aspect: The aspect ratio of the screen (width over height). From 17ebe42815c1cb594335cd5afd215f1fbe4c11b4 Mon Sep 17 00:00:00 2001 From: DragonMoffon Date: Mon, 11 Mar 2024 22:32:14 +1300 Subject: [PATCH 69/94] Moving protocols and making the PoDs into slots classes --- arcade/camera/{data.py => data_types.py} | 111 ++++++++++++++++++----- arcade/camera/types.py | 38 -------- 2 files changed, 88 insertions(+), 61 deletions(-) rename arcade/camera/{data.py => data_types.py} (52%) diff --git a/arcade/camera/data.py b/arcade/camera/data_types.py similarity index 52% rename from arcade/camera/data.py rename to arcade/camera/data_types.py index ad5fd6447..50a31e0c9 100644 --- a/arcade/camera/data.py +++ b/arcade/camera/data_types.py @@ -1,20 +1,23 @@ -"""Dataclasses supporting cameras. +"""Packets of data and base types supporting cameras. These are placed in their own module to simplify imports due to their wide usage throughout Arcade's camera code. """ -from typing import Tuple -from dataclasses import dataclass +from __future__ import annotations +from typing import Protocol, Tuple, Iterator +from contextlib import contextmanager __all__ = [ 'CameraData', 'OrthographicProjectionData', - 'PerspectiveProjectionData' + 'PerspectiveProjectionData', + 'Projection', + 'Projector', + 'Camera' ] -@dataclass class CameraData: """Stores position, orientation, and zoom for a camera. @@ -28,20 +31,28 @@ class CameraData: it allows camera controllers access to the zoom functionality without interacting with the projection data. """ - # View matrix data - position: Tuple[float, float, float] = (0.0, 0.0, 0.0) - up: Tuple[float, float, float] = (0.0, 1.0, 0.0) - forward: Tuple[float, float, float] = (0.0, 0.0, -1.0) - # Zoom - zoom: float = 1.0 + __slots__ = ("position", "up", "forward", "zoom") + + def __init__(self, + position: Tuple[float, float, float] = (0.0, 0.0, 0.0), + up: Tuple[float, float, float] = (0.0, 1.0, 0.0), + forward: Tuple[float, float, float] = (0.0, 0.0, -1.0), + zoom: float = 1.0): + + # View matrix data + self.position: Tuple[float, float, float] = position + self.up: Tuple[float, float, float] = up + self.forward: Tuple[float, float, float] = forward + + # Zoom + self.zoom: float = zoom def duplicate_camera_data(origin: CameraData): return CameraData(origin.position, origin.up, origin.forward, float(origin.zoom)) -@dataclass class OrthographicProjectionData: """Describes an Orthographic projection. @@ -58,17 +69,31 @@ class OrthographicProjectionData: far: The 'furthest' value, Which gets mapped to z = 1.0 (anything above this value is not visible). viewport: The pixel bounds which will be drawn onto. (left, bottom, width, height) """ - left: float - right: float - bottom: float - top: float - near: float - far: float - viewport: Tuple[int, int, int, int] + __slots__ = ("left", "right", "bottom", "top", "near", "far", "viewport") + + def __init__( + self, + left: float, + right: float, + bottom: float, + top: float, + near: float, + far: float, + viewport: Tuple[int, int, int, int]): + + # Data for generating Orthographic Projection matrix + self.left: float = left + self.right: float = right + self.bottom: float = bottom + self.top: float = top + self.near: float = near + self.far: float = far + + # Viewport for setting which pixels to draw to + self.viewport: Tuple[int, int, int, int] = viewport -@dataclass class PerspectiveProjectionData: """Describes a perspective projection. @@ -80,9 +105,49 @@ class PerspectiveProjectionData: far: The 'furthest' value, Which gets mapped to z = 1.0 (anything above this value is not visible). viewport: The pixel bounds which will be drawn onto. (left, bottom, width, height) """ - aspect: float - fov: float + __slots__ = ("aspect", "fov", "near", "far", "viewport") + + def __init__(self, + aspect: float, + fov: float, + near: float, + far: float, + + viewport: Tuple[int, int, int, int]): + # Data for generating Perspective Projection matrix + self.aspect: float = aspect + self.fov: float = fov + self.near: float = near + self.far: float = far + + # Viewport for setting which pixels to draw to + self.viewport: Tuple[int, int, int, int] = viewport + + +class Projection(Protocol): + viewport: Tuple[int, int, int, int] near: float far: float - viewport: Tuple[int, int, int, int] + +class Projector(Protocol): + + def use(self) -> None: + ... + + @contextmanager + def activate(self) -> Iterator[Projector]: + ... + + def map_coordinate(self, screen_coordinate: Tuple[float, float], depth: float = 0.0) -> Tuple[float, ...]: + ... + + +class Camera(Protocol): + + def use(self) -> None: + ... + + @contextmanager + def activate(self) -> Iterator[Projector]: + ... diff --git a/arcade/camera/types.py b/arcade/camera/types.py index 093d70775..e69de29bb 100644 --- a/arcade/camera/types.py +++ b/arcade/camera/types.py @@ -1,38 +0,0 @@ -from __future__ import annotations -from typing import Protocol, Tuple, Iterator -from contextlib import contextmanager - - -__all__ = [ - 'Projection', - 'Projector', - 'Camera' -] - - -class Projection(Protocol): - viewport: Tuple[int, int, int, int] - near: float - far: float - - -class Projector(Protocol): - - def use(self) -> None: - ... - - @contextmanager - def activate(self) -> Iterator[Projector]: - ... - - def map_coordinate(self, screen_coordinate: Tuple[float, float], depth: float = 0.0) -> Tuple[float, ...]: - ... - - -class Camera(Protocol): - - def use(self) -> None: - ... - - def activate(self) -> Iterator[Projector]: - ... From fa97758bbf54635be345139abd99470f28745635 Mon Sep 17 00:00:00 2001 From: DragonMoffon Date: Mon, 11 Mar 2024 22:44:03 +1300 Subject: [PATCH 70/94] Fixing linting (Thanks Push) --- arcade/camera/__init__.py | 3 +-- arcade/camera/camera_2d.py | 3 +-- .../controllers/simple_controller_classes.py | 6 +++--- .../controllers/simple_controller_functions.py | 16 ++++++++-------- arcade/camera/default.py | 2 +- arcade/camera/orthographic.py | 3 +-- arcade/camera/simple_camera.py | 3 +-- arcade/experimental/clock/clock_window.py | 1 + 8 files changed, 17 insertions(+), 20 deletions(-) diff --git a/arcade/camera/__init__.py b/arcade/camera/__init__.py index 8916f3d20..f75b84714 100644 --- a/arcade/camera/__init__.py +++ b/arcade/camera/__init__.py @@ -3,8 +3,7 @@ Providing a multitude of camera's for any need. """ -from arcade.camera.data import CameraData, OrthographicProjectionData -from arcade.camera.types import Projection, Projector +from arcade.camera.data_types import Projection, Projector, CameraData, OrthographicProjectionData from arcade.camera.orthographic import OrthographicProjector diff --git a/arcade/camera/camera_2d.py b/arcade/camera/camera_2d.py index f922f2689..a87578223 100644 --- a/arcade/camera/camera_2d.py +++ b/arcade/camera/camera_2d.py @@ -2,9 +2,8 @@ from math import degrees, radians, atan2, cos, sin from contextlib import contextmanager -from arcade.camera.data import CameraData, OrthographicProjectionData from arcade.camera.orthographic import OrthographicProjector -from arcade.camera.types import Projector +from arcade.camera.data_types import CameraData, OrthographicProjectionData, Projector from arcade.window_commands import get_window if TYPE_CHECKING: diff --git a/arcade/camera/controllers/simple_controller_classes.py b/arcade/camera/controllers/simple_controller_classes.py index c91d5ef32..02c3e4a81 100644 --- a/arcade/camera/controllers/simple_controller_classes.py +++ b/arcade/camera/controllers/simple_controller_classes.py @@ -8,7 +8,7 @@ from math import exp, log, pi, sin, floor from random import uniform -from arcade.camera.data import CameraData +from arcade.camera.data_types import CameraData from arcade.camera.controllers import quaternion_rotation __all__ = [ @@ -42,10 +42,10 @@ class ScreenShakeController: :param falloff_time: The length of time in seconds it takes the shaking to reach 0 after reaching the maximum. Can be set to a negative number to disable falloff. - :param acceleration_duration: The length of time in seconds it takes the + :param acceleration_duration: The length of time in seconds it takes the shaking to reach max amplitude. Can be set to 0.0 to start at max amplitude. - :param shake_frequency: The number of peaks per second. Avoid making it + :param shake_frequency: The number of peaks per second. Avoid making it a multiple of half the target frame-rate. (e.g. at 60 fps avoid 30, 60, 90, 120, etc.) """ diff --git a/arcade/camera/controllers/simple_controller_functions.py b/arcade/camera/controllers/simple_controller_functions.py index 601e25633..dc8953458 100644 --- a/arcade/camera/controllers/simple_controller_functions.py +++ b/arcade/camera/controllers/simple_controller_functions.py @@ -1,7 +1,7 @@ from typing import Tuple, Callable from math import sin, cos, radians -from arcade.camera.data import CameraData +from arcade.camera.data_types import CameraData from arcade.easing import linear from pyglet.math import Vec3 @@ -71,7 +71,7 @@ def quaternion_rotation(axis: Tuple[float, float, float], rotation_point[0] + new_relative_position[0], rotation_point[1] + new_relative_position[1] ) - + :param axis: The unit length vector that will be rotated around :param vector: The 3-dimensional vector to be rotated :param angle: The angle in degrees to rotate the vector clock-wise by @@ -152,7 +152,7 @@ def simple_follow_3D(speed: float, target: Tuple[float, float, float], data: Cam A simple method which moves the camera linearly towards the target point. :param speed: The percentage the camera should move towards the target (0.0 - 1.0 range) - :param target: The 3D position the camera should move towards in world space. + :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 = _interpolate_3D(data.position, target, speed) @@ -181,11 +181,11 @@ def simple_easing_3D(percent: float, :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 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. + speed does not stay constant. See arcade.easing for examples. """ data.position = _interpolate_3D(start, target, func(percent)) @@ -200,11 +200,11 @@ def simple_easing_2D(percent: float, 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 2D point which acts as the starting point for the camera motion. - :param target: The 2D point which acts as the final destination for the camera. + :param target: The 2D 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 diff --git a/arcade/camera/default.py b/arcade/camera/default.py index ee42dd943..1be632122 100644 --- a/arcade/camera/default.py +++ b/arcade/camera/default.py @@ -3,7 +3,7 @@ from pyglet.math import Mat4 -from arcade.camera.types import Projector +from arcade.camera.data_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 d8d93b4d1..c3ac50363 100644 --- a/arcade/camera/orthographic.py +++ b/arcade/camera/orthographic.py @@ -3,8 +3,7 @@ from pyglet.math import Mat4, Vec3, Vec4 -from arcade.camera.data import CameraData, OrthographicProjectionData -from arcade.camera.types import Projector +from arcade.camera.data_types import Projector, CameraData, OrthographicProjectionData 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 9c3168477..98b57f9c0 100644 --- a/arcade/camera/simple_camera.py +++ b/arcade/camera/simple_camera.py @@ -5,8 +5,7 @@ from pyglet.math import Vec3 -from arcade.camera.data import CameraData, OrthographicProjectionData -from arcade.camera.types import Projector +from arcade.camera.data_types import Projector, CameraData, OrthographicProjectionData from arcade.camera.orthographic import OrthographicProjector from arcade.window_commands import get_window diff --git a/arcade/experimental/clock/clock_window.py b/arcade/experimental/clock/clock_window.py index 97dbf7863..667ceff0e 100644 --- a/arcade/experimental/clock/clock_window.py +++ b/arcade/experimental/clock/clock_window.py @@ -1,3 +1,4 @@ +# mypy: ignore-errors """Clock variants of :py:class:`arcade.Window` and :py:class:`arcade.View`. Unlike the main versions, they add support for using a clock and fixed From 2948d4bf5cf52fa149ac75d5ff4d3040f613f0af Mon Sep 17 00:00:00 2001 From: DragonMoffon Date: Mon, 11 Mar 2024 23:58:09 +1300 Subject: [PATCH 71/94] Extract non PoD arguments from the Camera2D init and add a RenderTarget property to Camera2D The render target change was discussed with @einarf. May extend to other camera types later. --- arcade/camera/camera_2d.py | 116 ++++++++++++++++++++++--------------- 1 file changed, 69 insertions(+), 47 deletions(-) diff --git a/arcade/camera/camera_2d.py b/arcade/camera/camera_2d.py index a87578223..136c7feec 100644 --- a/arcade/camera/camera_2d.py +++ b/arcade/camera/camera_2d.py @@ -4,6 +4,7 @@ from arcade.camera.orthographic import OrthographicProjector from arcade.camera.data_types import CameraData, OrthographicProjectionData, Projector +from arcade.gl import Framebuffer from arcade.window_commands import get_window if TYPE_CHECKING: @@ -43,71 +44,88 @@ class Camera2D: :param window: The Arcade Window to bind the camera to. Defaults to the currently active window. - :param viewport: A 4-int tuple which defines the pixel bounds which the camera with project to. - :param position: The 2D position of the camera in the XY plane. - :param up: The 2D unit vector which defines the +Y-axis of the camera space. - :param zoom: A scalar value which is inversely proportional to the size of the camera projection. - i.e. a zoom of 2.0 halves the size of the projection, doubling the perceived size of objects. - :param projection: A 4-float tuple which defines the world space - bounds which the camera projects to the viewport. - :param near: The near clipping plane of the camera. - :param far: The far clipping plane of the camera. :param camera_data: A :py:class:`~arcade.camera.data.CameraData` describing the viewport, position, up, and zoom. :param projection_data: A :py:class:`~arcade.camera.data.OrthographicProjectionData` which describes the left, right, top, bottom, far, near planes for an orthographic projection. + :param render_target: The FrameBuffer that the camera uses. Defaults to the screen. + If the framebuffer is not the default screen nothing drawn after this camera is used will + show up. The FrameBuffer's internal viewport is ignored. """ 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 - ): - window = window or get_window() - self._window: "Window" = window + camera_data: CameraData, + projection_data: OrthographicProjectionData, + render_target: Optional[Framebuffer] = None, + window: Optional["Window"] = None): + self._window: "Window" = window or get_window() + self.render_target: Framebuffer = render_target or self._window.ctx.screen - assert ( - not any((viewport, position, up, zoom)) or not camera_data - ), ( - "Camera2D Warning: Provided both a CameraData object and raw values. Defaulting to CameraData." - ) + self._data = camera_data + self._projection: OrthographicProjectionData = projection_data - assert ( - not any((projection, near, far)) or not projection_data - ), ( - "Camera2D Warning: Provided both an OrthographicProjectionData object and raw values." - "Defaulting to OrthographicProjectionData." + self._ortho_projector: OrthographicProjector = OrthographicProjector( + window=self._window, + view=self._data, + projection=self._projection ) - half_width = window.width/2 - half_height = window.height/2 + @staticmethod + def from_raw_data( + viewport: Optional[Tuple[int, int, int, int]] = None, + position: Optional[Tuple[float, float]] = None, + up: Tuple[float, float] = (0.0, 1.0), + zoom: float = 1.0, + projection: Optional[Tuple[float, float, float, float]] = None, + near: float = -100, + far: float = 100, + *, + render_target: Optional[Framebuffer] = None, + window: Optional["Window"] = None + ): + """ + Create a Camera2D without first defining CameraData or an OrthographicProjectionData object. + + :param viewport: A 4-int tuple which defines the pixel bounds which the camera with project to. + :param position: The 2D position of the camera in the XY plane. + :param up: The 2D unit vector which defines the +Y-axis of the camera space. + :param zoom: A scalar value which is inversely proportional to the size of the camera projection. + i.e. a zoom of 2.0 halves the size of the projection, doubling the perceived size of objects. + :param projection: A 4-float tuple which defines the world space + bounds which the camera projects to the viewport. + :param near: The near clipping plane of the camera. + :param far: The far clipping plane of the camera. + :param render_target: The FrameBuffer that the camera uses. Defaults to the screen. + If the framebuffer is not the default screen nothing drawn after this camera is used will + show up. The FrameBuffer's internal viewport is ignored. + :param window: The Arcade Window to bind the camera to. + Defaults to the currently active window. + """ + window = window or get_window() + + half_width = window.width / 2 + half_height = window.height / 2 _pos = position or (half_width, half_height) - _up = up or (0.0, 1.0) - self._data = camera_data or CameraData( - (_pos[0], _pos[1], 0.0), - (_up[0], _up[1], 0.0), - (0.0, 0.0, -1.0), - zoom or 1.0 + _data: CameraData = CameraData( + (_pos[0], _pos[1], 0.0), # position + (up[0], up[1], 0.0), # up vector + (0.0, 0.0, -1.0), # forward vector + zoom # zoom ) - self._projection: OrthographicProjectionData = projection_data or OrthographicProjectionData( - -half_width, half_width, # Left and Right. - -half_height, half_height, # Bottom and Top. + left, right, bottom, top = projection or (-half_width, half_width, -half_height, half_height) + _projection: OrthographicProjectionData = OrthographicProjectionData( + left, right, # Left and Right. + top, bottom, # Bottom and Top. near or 0.0, far or 100.0, # Near and Far. viewport or (0, 0, window.width, window.height) # Viewport ) - self._ortho_projector: OrthographicProjector = OrthographicProjector( - window=self._window, - view=self._data, - projection=self._projection + return Camera2D( + camera_data=_data, + projection_data=_projection, + window=window, + render_target= (render_target or window.ctx.screen) ) @property @@ -662,6 +680,7 @@ def use(self) -> None: If you want to use a 'with' block use activate() instead. """ + self.render_target.use() self._ortho_projector.use() @contextmanager @@ -675,10 +694,13 @@ def activate(self) -> Iterator[Projector]: the projector to the one previously in use. """ previous_projection = self._window.current_camera + previous_framebuffer = self._window.ctx.active_framebuffer try: + self.render_target.use() self.use() yield self finally: + previous_framebuffer.use() previous_projection.use() def map_coordinate(self, screen_coordinate: Tuple[float, float], depth: float = 0.0) -> Tuple[float, float]: From a54a5705316d24a25e0ca123779845945cb62809 Mon Sep 17 00:00:00 2001 From: DragonMoffon Date: Tue, 12 Mar 2024 00:34:08 +1300 Subject: [PATCH 72/94] Added an easy screenshot method to window commands CURRENTLY DOES NOT WORK NEED TO DEBUG WITH @einarf --- arcade/window_commands.py | 29 ++++++++++++++++++++++++++--- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/arcade/window_commands.py b/arcade/window_commands.py index 009cd55eb..a8c5095ba 100644 --- a/arcade/window_commands.py +++ b/arcade/window_commands.py @@ -7,16 +7,20 @@ import gc import os - -import pyglet - from typing import ( Callable, Optional, Tuple, + Union, TYPE_CHECKING ) +from pathlib import Path + +from PIL import Image +import pyglet + from arcade.types import RGBA255, Color +from arcade.resources import resolve_resource_path if TYPE_CHECKING: from arcade import Window @@ -71,6 +75,25 @@ def get_window() -> "Window": return _window +def save_screenshot(location: Union[str, Path], *, window: Optional["Window"] = None): + """ + Quickly save the screen or a gl texture to a png image. Is not a fast operation so should be used sparingly. + Currently only supports 3 and 4 component 8-bit float gl textures. This should be fine for most use cases. + + :param location: The string path to save the image to. + :param window: Optionally supply a specific arcade window. Defaults to the currently active screen. + """ + win: "Window" = window or get_window() + + img = Image.frombytes( + "RGBA", + win.ctx.screen.size, + win.ctx.screen.read(components=4) + ) + + img.save(location, "PNG") + + def set_window(window: Optional["Window"]) -> None: """ Set a handle to the current window. From 1536ecde4810908b148d7bcfece7e2644e133e40 Mon Sep 17 00:00:00 2001 From: DragonMoffon Date: Fri, 15 Mar 2024 21:02:09 +1300 Subject: [PATCH 73/94] created a depth of field example --- arcade/experimental/depth_of_field.py | 167 ++++++++++++++++++++++++++ 1 file changed, 167 insertions(+) create mode 100644 arcade/experimental/depth_of_field.py diff --git a/arcade/experimental/depth_of_field.py b/arcade/experimental/depth_of_field.py new file mode 100644 index 000000000..8cb2c6f0f --- /dev/null +++ b/arcade/experimental/depth_of_field.py @@ -0,0 +1,167 @@ +from typing import Tuple, Optional +from contextlib import contextmanager +from math import cos, pi + +from PIL import Image + +from arcade.gl import geometry, NEAREST, Texture2D +from arcade.experimental.postprocessing import GaussianBlur +from arcade import get_window, draw_text + + +class DepthOfField: + + def __init__(self, size: Optional[Tuple[int, int]] = None): + self._geo = geometry.quad_2d_fs() + self._win = get_window() + size = size or self._win.size + + self.stale = True + + self._render_target = self._win.ctx.framebuffer( + color_attachments=[ + self._win.ctx.texture( + size, + components=4, + filter=(NEAREST, NEAREST), + wrap_x=self._win.ctx.REPEAT, + wrap_y=self._win.ctx.REPEAT + ), + ], + depth_attachment=self._win.ctx.depth_texture( + size + ) + ) + + self._blur_process = GaussianBlur( + size, + 10, + 2.0, + 2.0, + step=4 + ) + + self._blurred = None + + self._blur_target = self._win.ctx.framebuffer( + color_attachments=[ + self._win.ctx.texture( + size, + components=4, + filter=(NEAREST, NEAREST), + wrap_x=self._win.ctx.REPEAT, + wrap_y=self._win.ctx.REPEAT + ) + ] + ) + + self._render_program = self._win.ctx.program( + vertex_shader=( + "#version 330\n" + "\n" + "in vec2 in_vert;\n" + "in vec2 in_uv;\n" + "\n" + "out vec2 out_uv;\n" + "\n" + "void main(){\n" + " gl_Position = vec4(in_vert, 0.0, 1.0);\n" + " out_uv = in_uv;\n" + "}\n" + ), + fragment_shader=( + "#version 330\n" + "\n" + "uniform sampler2D texture_0;\n" + "uniform sampler2D texture_1;\n" + "uniform sampler2D depth_0;\n" + "\n" + "uniform float focus_depth;\n" + "\n" + "in vec2 out_uv;\n" + "\n" + "out vec4 frag_colour;\n" + "\n" + "void main() {\n" + " float depth_val = texture(depth_0, out_uv).x;\n" + " float depth_adjusted = min(1.0, 2.0 * abs(depth_val - focus_depth));\n" + " vec4 crisp_tex = texture(texture_0, out_uv);\n" + " vec3 blur_tex = texture(texture_1, out_uv).rgb;\n" + " frag_colour = mix(crisp_tex, vec4(blur_tex, crisp_tex.a), depth_adjusted);\n" + " //if (depth_adjusted < 0.1){frag_colour = vec4(1.0, 0.0, 0.0, 1.0);}\n" + "}\n" + ) + ) + self._render_program['texture_0'] = 0 + self._render_program['texture_1'] = 1 + self._render_program['depth_0'] = 2 + + @contextmanager + def draw_into(self, color: Optional = None): + self.stale = True + previous_fbo = self._win.ctx.active_framebuffer + try: + self._win.ctx.enable(self._win.ctx.DEPTH_TEST) + self._render_target.clear(color or (155.0, 155.0, 155.0, 255.0)) + self._render_target.use() + yield self._render_target + finally: + self._win.ctx.disable(self._win.ctx.DEPTH_TEST) + previous_fbo.use() + + def process(self): + self._blurred = self._blur_process.render(self._render_target.color_attachments[0]) + self._win.use() + + self.stale = False + + def render(self): + if self.stale: + self.process() + + self._render_target.color_attachments[0].use(0) + self._blurred.use(1) + self._render_target.depth_attachment.use(2) + self._geo.render(self._render_program) + + +if __name__ == '__main__': + from random import uniform, randint + from arcade import Window, SpriteSolidColor, SpriteList + win = Window() + t = 0.0 + l = SpriteList() + for _ in range(100): + d = uniform(-100, 100) + c = int(255 * (d+100)/200) + s = SpriteSolidColor( + randint(100, 200), randint(100, 200), + uniform(20, win.width-20), uniform(20, win.height-20), + (c, c, c, 255), + uniform(0, 360) + ) + s.depth = d + l.append(s) + dof = DepthOfField() + + def update(delta_time: float): + global t + t += delta_time + dof._render_program["focus_depth"] = round(16 * (cos(pi * 0.1 * t)*0.5 + 0.5)) / 16 + + win.on_update = update + + def draw(): + win.clear() + with dof.draw_into(): + l.draw(pixelated=True) + win.use() + + dof.render() + draw_text(str(dof._render_program["focus_depth"]), win.width/2, win.height/2, (255, 0, 0, 255), align="center") + + + win.on_draw = draw + + win.run() + From 191b8df7eea0d0269ef3c50820a54b7f32b79e73 Mon Sep 17 00:00:00 2001 From: DragonMoffon Date: Fri, 15 Mar 2024 22:31:12 +1300 Subject: [PATCH 74/94] Revert "created a depth of field example" This reverts commit 1536ecde4810908b148d7bcfece7e2644e133e40. --- arcade/experimental/depth_of_field.py | 167 -------------------------- 1 file changed, 167 deletions(-) delete mode 100644 arcade/experimental/depth_of_field.py diff --git a/arcade/experimental/depth_of_field.py b/arcade/experimental/depth_of_field.py deleted file mode 100644 index 8cb2c6f0f..000000000 --- a/arcade/experimental/depth_of_field.py +++ /dev/null @@ -1,167 +0,0 @@ -from typing import Tuple, Optional -from contextlib import contextmanager -from math import cos, pi - -from PIL import Image - -from arcade.gl import geometry, NEAREST, Texture2D -from arcade.experimental.postprocessing import GaussianBlur -from arcade import get_window, draw_text - - -class DepthOfField: - - def __init__(self, size: Optional[Tuple[int, int]] = None): - self._geo = geometry.quad_2d_fs() - self._win = get_window() - size = size or self._win.size - - self.stale = True - - self._render_target = self._win.ctx.framebuffer( - color_attachments=[ - self._win.ctx.texture( - size, - components=4, - filter=(NEAREST, NEAREST), - wrap_x=self._win.ctx.REPEAT, - wrap_y=self._win.ctx.REPEAT - ), - ], - depth_attachment=self._win.ctx.depth_texture( - size - ) - ) - - self._blur_process = GaussianBlur( - size, - 10, - 2.0, - 2.0, - step=4 - ) - - self._blurred = None - - self._blur_target = self._win.ctx.framebuffer( - color_attachments=[ - self._win.ctx.texture( - size, - components=4, - filter=(NEAREST, NEAREST), - wrap_x=self._win.ctx.REPEAT, - wrap_y=self._win.ctx.REPEAT - ) - ] - ) - - self._render_program = self._win.ctx.program( - vertex_shader=( - "#version 330\n" - "\n" - "in vec2 in_vert;\n" - "in vec2 in_uv;\n" - "\n" - "out vec2 out_uv;\n" - "\n" - "void main(){\n" - " gl_Position = vec4(in_vert, 0.0, 1.0);\n" - " out_uv = in_uv;\n" - "}\n" - ), - fragment_shader=( - "#version 330\n" - "\n" - "uniform sampler2D texture_0;\n" - "uniform sampler2D texture_1;\n" - "uniform sampler2D depth_0;\n" - "\n" - "uniform float focus_depth;\n" - "\n" - "in vec2 out_uv;\n" - "\n" - "out vec4 frag_colour;\n" - "\n" - "void main() {\n" - " float depth_val = texture(depth_0, out_uv).x;\n" - " float depth_adjusted = min(1.0, 2.0 * abs(depth_val - focus_depth));\n" - " vec4 crisp_tex = texture(texture_0, out_uv);\n" - " vec3 blur_tex = texture(texture_1, out_uv).rgb;\n" - " frag_colour = mix(crisp_tex, vec4(blur_tex, crisp_tex.a), depth_adjusted);\n" - " //if (depth_adjusted < 0.1){frag_colour = vec4(1.0, 0.0, 0.0, 1.0);}\n" - "}\n" - ) - ) - self._render_program['texture_0'] = 0 - self._render_program['texture_1'] = 1 - self._render_program['depth_0'] = 2 - - @contextmanager - def draw_into(self, color: Optional = None): - self.stale = True - previous_fbo = self._win.ctx.active_framebuffer - try: - self._win.ctx.enable(self._win.ctx.DEPTH_TEST) - self._render_target.clear(color or (155.0, 155.0, 155.0, 255.0)) - self._render_target.use() - yield self._render_target - finally: - self._win.ctx.disable(self._win.ctx.DEPTH_TEST) - previous_fbo.use() - - def process(self): - self._blurred = self._blur_process.render(self._render_target.color_attachments[0]) - self._win.use() - - self.stale = False - - def render(self): - if self.stale: - self.process() - - self._render_target.color_attachments[0].use(0) - self._blurred.use(1) - self._render_target.depth_attachment.use(2) - self._geo.render(self._render_program) - - -if __name__ == '__main__': - from random import uniform, randint - from arcade import Window, SpriteSolidColor, SpriteList - win = Window() - t = 0.0 - l = SpriteList() - for _ in range(100): - d = uniform(-100, 100) - c = int(255 * (d+100)/200) - s = SpriteSolidColor( - randint(100, 200), randint(100, 200), - uniform(20, win.width-20), uniform(20, win.height-20), - (c, c, c, 255), - uniform(0, 360) - ) - s.depth = d - l.append(s) - dof = DepthOfField() - - def update(delta_time: float): - global t - t += delta_time - dof._render_program["focus_depth"] = round(16 * (cos(pi * 0.1 * t)*0.5 + 0.5)) / 16 - - win.on_update = update - - def draw(): - win.clear() - with dof.draw_into(): - l.draw(pixelated=True) - win.use() - - dof.render() - draw_text(str(dof._render_program["focus_depth"]), win.width/2, win.height/2, (255, 0, 0, 255), align="center") - - - win.on_draw = draw - - win.run() - From 7892e1afc63699ee4edbbc79c86d5bdb11248cd8 Mon Sep 17 00:00:00 2001 From: DragonMoffon Date: Tue, 19 Mar 2024 20:12:55 +1300 Subject: [PATCH 75/94] Revert "Added an easy screenshot method to window commands" This reverts commit a54a5705316d24a25e0ca123779845945cb62809. --- arcade/window_commands.py | 29 +++-------------------------- 1 file changed, 3 insertions(+), 26 deletions(-) diff --git a/arcade/window_commands.py b/arcade/window_commands.py index a8c5095ba..009cd55eb 100644 --- a/arcade/window_commands.py +++ b/arcade/window_commands.py @@ -7,20 +7,16 @@ import gc import os + +import pyglet + from typing import ( Callable, Optional, Tuple, - Union, TYPE_CHECKING ) -from pathlib import Path - -from PIL import Image -import pyglet - from arcade.types import RGBA255, Color -from arcade.resources import resolve_resource_path if TYPE_CHECKING: from arcade import Window @@ -75,25 +71,6 @@ def get_window() -> "Window": return _window -def save_screenshot(location: Union[str, Path], *, window: Optional["Window"] = None): - """ - Quickly save the screen or a gl texture to a png image. Is not a fast operation so should be used sparingly. - Currently only supports 3 and 4 component 8-bit float gl textures. This should be fine for most use cases. - - :param location: The string path to save the image to. - :param window: Optionally supply a specific arcade window. Defaults to the currently active screen. - """ - win: "Window" = window or get_window() - - img = Image.frombytes( - "RGBA", - win.ctx.screen.size, - win.ctx.screen.read(components=4) - ) - - img.save(location, "PNG") - - def set_window(window: Optional["Window"]) -> None: """ Set a handle to the current window. From f49d0ec2244a0e14cdecb4fd17c8a98eb0a1af0f Mon Sep 17 00:00:00 2001 From: DragonMoffon Date: Fri, 29 Mar 2024 17:49:28 +1300 Subject: [PATCH 76/94] Updating arcade.math changed lerp_vec to lerp_2d created lerp_3d, and quaternion_rotation methods. Also updated any methods which used lerp_vec --- arcade/math.py | 76 +++++++++++++++++++++++++++++++++++++++-- arcade/paths.py | 4 +-- tests/unit/test_math.py | 6 ++-- 3 files changed, 78 insertions(+), 8 deletions(-) diff --git a/arcade/math.py b/arcade/math.py index fc964112d..68ceba97e 100644 --- a/arcade/math.py +++ b/arcade/math.py @@ -2,6 +2,8 @@ import math import random +from typing import Tuple, List, Union +from pyglet.math import Vec2, Vec3 from arcade.types import Point, Vector _PRECISION = 2 @@ -11,7 +13,8 @@ "round_fast", "clamp", "lerp", - "lerp_vec", + "lerp_2d", + "lerp_3d", "lerp_angle", "rand_in_rect", "rand_in_circle", @@ -25,6 +28,7 @@ "rotate_point", "get_angle_degrees", "get_angle_radians", + "quaternion_rotation" ] @@ -64,13 +68,25 @@ def lerp(v1: float, v2: float, u: float) -> float: return v1 + ((v2 - v1) * u) -def lerp_vec(v1: Vector, v2: Vector, u: float) -> Vector: +V_2D = Union[Vec2, Tuple[float, float], List[float]] +V_3D = Union[Vec3, Tuple[float, float, float], List[float]] + + +def lerp_2d(v1: V_2D, v2: V_2D, u: float) -> Tuple[float, float]: return ( lerp(v1[0], v2[0], u), lerp(v1[1], v2[1], u) ) +def lerp_3d(v1: V_3D, v2: V_3D, u: float) -> Tuple[float, float, float]: + return ( + lerp(v1[0], v2[0], u), + lerp(v1[1], v2[1], u), + lerp(v1[2], v2[2], u) + ) + + def lerp_angle(start_angle: float, end_angle: float, u: float) -> float: """ Linearly interpolate between two angles in degrees, @@ -160,7 +176,7 @@ def rand_on_line(pos1: Point, pos2: Point) -> Point: :return: A random point on the line """ u = random.uniform(0.0, 1.0) - return lerp_vec(pos1, pos2, u) + return lerp_2d(pos1, pos2, u) def rand_angle_360_deg() -> float: @@ -354,3 +370,57 @@ def get_angle_radians(x1: float, y1: float, x2: float, y2: float) -> float: x_diff = x2 - x1 y_diff = y2 - y1 return math.atan2(x_diff, y_diff) + + +def quaternion_rotation(axis: Tuple[float, float, float], + vector: Tuple[float, float, float], + angle: float) -> Tuple[float, float, float]: + """ + Rotate a 3-dimensional vector of any length clockwise around a 3-dimensional unit length vector. + + This method of vector rotation is immune to rotation-lock, however it takes a little more effort + to find the axis of rotation rather than 3 angles of rotation. + Ref: https://danceswithcode.net/engineeringnotes/quaternions/quaternions.html. + + Example: + import arcade + from arcade.camera.controllers import quaternion_rotation + + + # Rotating a sprite around a point + sprite = arcade.Sprite(center_x=0.0, center_y=10.0) + rotation_point = (0.0, 0.0) + + # Find the relative vector between the sprite and point to rotate. (Must be a 3D vector) + relative_position = sprite.center_x - rotation_point[0], sprite.center_y - rotation_point[1], 0.0 + + # Because arcade uses the X and Y axis for 2D co-ordinates the Z-axis becomes the rotation axis. + rotation_axis = (0.0, 0.0, 1.0) + + # Rotate the vector 45 degrees clockwise. + new_relative_position = quaternion_rotation(rotation_axis, relative_position, 45) + + + sprite.position = ( + rotation_point[0] + new_relative_position[0], + rotation_point[1] + new_relative_position[1] + ) + + :param axis: The unit length vector that will be rotated around + :param vector: The 3-dimensional vector to be rotated + :param angle: The angle in degrees to rotate the vector clock-wise by + :return: A rotated 3-dimension vector with the same length as the argument vector. + """ + _rotation_rads = -math.radians(angle) + p1, p2, p3 = vector + _c2, _s2 = math.cos(_rotation_rads / 2.0), math.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 diff --git a/arcade/paths.py b/arcade/paths.py index 13ec8267c..5ce1af0f1 100644 --- a/arcade/paths.py +++ b/arcade/paths.py @@ -19,7 +19,7 @@ check_for_collision_with_list, get_sprites_at_point ) -from arcade.math import get_distance, lerp_vec +from arcade.math import get_distance, lerp_2d from arcade.types import Point __all__ = [ @@ -360,7 +360,7 @@ def has_line_of_sight(observer: Point, for step in range(steps + 1): step_distance = step * check_resolution u = step_distance / distance - midpoint = lerp_vec(observer, target, u) + midpoint = lerp_2d(observer, target, u) if step_distance > max_distance: return False sprite_list = get_sprites_at_point(midpoint, walls) diff --git a/tests/unit/test_math.py b/tests/unit/test_math.py index 7d4dd0aa0..b43b9786c 100644 --- a/tests/unit/test_math.py +++ b/tests/unit/test_math.py @@ -15,11 +15,11 @@ def test_lerp(): assert lerp(2.0, 4.0, 0.75) == approx(3.5) -def test_lerp_vec(): - vec = lerp_vec((0.0, 2.0), (8.0, 4.0), 0.25) +def test_lerp_2d(): + vec = lerp_2d((0.0, 2.0), (8.0, 4.0), 0.25) assert vec[0] == approx(2.0) assert vec[1] == approx(2.5) - vec = lerp_vec((0.0, 2.0), (8.0, 4.0), -0.25) + vec = lerp_2d((0.0, 2.0), (8.0, 4.0), -0.25) assert vec[0] == approx(-2.0) assert vec[1] == approx(1.5) From de00c98373e33ad469b73d861ac0efb7dff95f55 Mon Sep 17 00:00:00 2001 From: DragonMoffon Date: Fri, 29 Mar 2024 17:52:00 +1300 Subject: [PATCH 77/94] gave arcade.camera.Camera2D default init arguments --- arcade/camera/camera_2d.py | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/arcade/camera/camera_2d.py b/arcade/camera/camera_2d.py index 136c7feec..a4598d73c 100644 --- a/arcade/camera/camera_2d.py +++ b/arcade/camera/camera_2d.py @@ -53,15 +53,28 @@ class Camera2D: show up. The FrameBuffer's internal viewport is ignored. """ def __init__(self, *, - camera_data: CameraData, - projection_data: OrthographicProjectionData, + camera_data: Optional[CameraData] = None, + projection_data: Optional[OrthographicProjectionData] = None, render_target: Optional[Framebuffer] = None, window: Optional["Window"] = None): self._window: "Window" = window or get_window() self.render_target: Framebuffer = render_target or self._window.ctx.screen - self._data = camera_data - self._projection: OrthographicProjectionData = projection_data + half_width = self._window.width / 2 + half_height = self._window.height / 2 + + self._data = camera_data or CameraData( + (half_width, half_height, 0.0), # position + (0.0, 1.0, 0.0), # up vector + (0.0, 0.0, -1.0), # forward vector + 1.0 # zoom + ) + self._projection: OrthographicProjectionData = projection_data or OrthographicProjectionData( + -half_width, half_width, # Left and Right. + -half_height, half_height, # Bottom and Top. + 0.0, 100.0, # Near and Far. + (0, 0, self._window.width, self._window.height) # Viewport + ) self._ortho_projector: OrthographicProjector = OrthographicProjector( window=self._window, @@ -125,7 +138,7 @@ def from_raw_data( camera_data=_data, projection_data=_projection, window=window, - render_target= (render_target or window.ctx.screen) + render_target=(render_target or window.ctx.screen) ) @property From dc4bf31e89ef138a930cee7174301026f3a0f78f Mon Sep 17 00:00:00 2001 From: DragonMoffon Date: Fri, 29 Mar 2024 17:52:34 +1300 Subject: [PATCH 78/94] Added function to constrain arcade.camera.CameraData --- arcade/camera/data_types.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/arcade/camera/data_types.py b/arcade/camera/data_types.py index 50a31e0c9..89f918419 100644 --- a/arcade/camera/data_types.py +++ b/arcade/camera/data_types.py @@ -7,6 +7,8 @@ from typing import Protocol, Tuple, Iterator from contextlib import contextmanager +from pyglet.math import Vec3 + __all__ = [ 'CameraData', @@ -53,6 +55,26 @@ def duplicate_camera_data(origin: CameraData): return CameraData(origin.position, origin.up, origin.forward, float(origin.zoom)) +def constrain_camera_data(data: CameraData, forward_priority: bool = False): + """ + Ensure that the camera data forward and up vectors are length one, + and are perpendicular + + :param data: the camera data to constrain + :param forward_priority: whether up or forward gets constrained + """ + forward_vec = Vec3(*data.forward).normalize() + up_vec = Vec3(*data.up).normalize() + right_vec = forward_vec.cross(up_vec).normalize() + if forward_priority: + up_vec = forward_vec.cross(right_vec) + else: + forward_vec = up_vec.cross(right_vec) + + data.forward = (forward_vec.x, forward_vec.y, forward_vec.z) + data.up = (up_vec.x, up_vec.y, up_vec.z) + + class OrthographicProjectionData: """Describes an Orthographic projection. From dc529b1e4b96de0ba869bdb9c58f314eef72d039 Mon Sep 17 00:00:00 2001 From: DragonMoffon Date: Fri, 29 Mar 2024 17:53:25 +1300 Subject: [PATCH 79/94] moved arcade.camera.controllers to arcade.camera.grips --- arcade/camera/__init__.py | 5 +- arcade/camera/controllers/__init__.py | 29 --- .../simple_controller_functions.py | 213 ------------------ arcade/camera/grips/__init__.py | 12 + arcade/camera/grips/rotate.py | 59 +++++ .../screen_shake_2d.py} | 15 +- arcade/camera/grips/strafe.py | 25 ++ .../camera/test_camera_controller_methods.py | 54 +---- tests/unit/camera/test_camera_shake.py | 10 +- 9 files changed, 121 insertions(+), 301 deletions(-) delete mode 100644 arcade/camera/controllers/__init__.py delete mode 100644 arcade/camera/controllers/simple_controller_functions.py create mode 100644 arcade/camera/grips/__init__.py create mode 100644 arcade/camera/grips/rotate.py rename arcade/camera/{controllers/simple_controller_classes.py => grips/screen_shake_2d.py} (98%) create mode 100644 arcade/camera/grips/strafe.py diff --git a/arcade/camera/__init__.py b/arcade/camera/__init__.py index f75b84714..cc4f9f2a3 100644 --- a/arcade/camera/__init__.py +++ b/arcade/camera/__init__.py @@ -10,7 +10,8 @@ from arcade.camera.simple_camera import SimpleCamera from arcade.camera.camera_2d import Camera2D -import arcade.camera.controllers as controllers +import arcade.camera.grips as grips + __all__ = [ 'Projection', @@ -20,5 +21,5 @@ 'OrthographicProjector', 'SimpleCamera', 'Camera2D', - 'controllers' + 'grips' ] diff --git a/arcade/camera/controllers/__init__.py b/arcade/camera/controllers/__init__.py deleted file mode 100644 index eb4eaa6ef..000000000 --- a/arcade/camera/controllers/__init__.py +++ /dev/null @@ -1,29 +0,0 @@ -from arcade.camera.controllers.simple_controller_functions import ( - simple_follow_3D, - simple_follow_2D, - simple_easing_3D, - simple_easing_2D, - strafe, - quaternion_rotation, - rotate_around_up, - rotate_around_right, - rotate_around_forward -) - -from arcade.camera.controllers.simple_controller_classes import ( - ScreenShakeController -) - - -__all__ = [ - 'simple_follow_3D', - 'simple_follow_2D', - 'simple_easing_3D', - 'simple_easing_2D', - 'strafe', - 'quaternion_rotation', - 'rotate_around_up', - 'rotate_around_right', - 'rotate_around_forward', - 'ScreenShakeController' -] diff --git a/arcade/camera/controllers/simple_controller_functions.py b/arcade/camera/controllers/simple_controller_functions.py deleted file mode 100644 index dc8953458..000000000 --- a/arcade/camera/controllers/simple_controller_functions.py +++ /dev/null @@ -1,213 +0,0 @@ -from typing import Tuple, Callable -from math import sin, cos, radians - -from arcade.camera.data_types import CameraData -from arcade.easing import linear -from pyglet.math import Vec3 - -__all__ = [ - 'simple_follow_3D', - 'simple_follow_2D', - 'simple_easing_3D', - 'simple_easing_2D', - 'strafe', - 'quaternion_rotation', - 'rotate_around_forward', - 'rotate_around_up', - 'rotate_around_right' -] - - -def strafe(data: CameraData, direction: Tuple[float, float]): - """ - Move the CameraData in a 2D direction aligned to the up-right plane of the view. - A value of [1, 0] will move the camera sideways while a value of [0, 1] - will move the camera upwards. Works irrespective of which direction the camera is facing. - """ - _forward = Vec3(*data.forward).normalize() - _up = Vec3(*data.up).normalize() - _right = _forward.cross(_up) - - _pos = data.position - - offset = _right * direction[0] + _up * direction[1] - data.position = ( - _pos[0] + offset[0], - _pos[1] + offset[1], - _pos[2] + offset[2] - ) - - -def quaternion_rotation(axis: Tuple[float, float, float], - vector: Tuple[float, float, float], - angle: float) -> Tuple[float, float, float]: - """ - Rotate a 3-dimensional vector of any length clockwise around a 3-dimensional unit length vector. - - This method of vector rotation is immune to rotation-lock, however it takes a little more effort - to find the axis of rotation rather than 3 angles of rotation. - Ref: https://danceswithcode.net/engineeringnotes/quaternions/quaternions.html. - - Example: - import arcade - from arcade.camera.controllers import quaternion_rotation - - - # Rotating a sprite around a point - sprite = arcade.Sprite(center_x=0.0, center_y=10.0) - rotation_point = (0.0, 0.0) - - # Find the relative vector between the sprite and point to rotate. (Must be a 3D vector) - relative_position = sprite.center_x - rotation_point[0], sprite.center_y - rotation_point[1], 0.0 - - # Because arcade uses the X and Y axis for 2D co-ordinates the Z-axis becomes the rotation axis. - rotation_axis = (0.0, 0.0, 1.0) - - # Rotate the vector 45 degrees clockwise. - new_relative_position = quaternion_rotation(rotation_axis, relative_position, 45) - - - sprite.position = ( - rotation_point[0] + new_relative_position[0], - rotation_point[1] + new_relative_position[1] - ) - - :param axis: The unit length vector that will be rotated around - :param vector: The 3-dimensional vector to be rotated - :param angle: The angle in degrees to rotate the vector clock-wise by - :return: A rotated 3-dimension vector with the same length as the argument vector. - """ - _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): - """ - Rotate the CameraData up vector around the CameraData forward vector, perfect for rotating the screen. - This rotation will be around (0.0, 0.0) of the camera projection. - If that is not the center of the screen this method may appear erroneous. - Uses arcade.camera.controllers.quaternion_rotation internally. - - :param data: The camera data to modify. The data's up vector is rotated around its forward vector - :param angle: The angle in degrees to rotate clockwise by - """ - data.up = quaternion_rotation(data.forward, data.up, angle) - - -def rotate_around_up(data: CameraData, angle: float): - """ - Rotate the CameraData forward vector around the CameraData up vector. - Generally only useful in 3D games. - Uses arcade.camera.controllers.quaternion_rotation internally. - - :param data: The camera data to modify. The data's forward vector is rotated around its up vector - :param angle: The angle in degrees to rotate clockwise by - """ - data.forward = quaternion_rotation(data.up, data.forward, angle) - - -def rotate_around_right(data: CameraData, angle: float, forward: bool = True, up: bool = True): - """ - Rotate both the CameraData's forward vector and up vector around a calculated right vector. - Generally only useful in 3D games. - Uses arcade.camera.controllers.quaternion_rotation internally. - - :param data: The camera data to modify. The data's forward vector is rotated around its up vector - :param angle: The angle in degrees to rotate clockwise by - :param forward: Whether to rotate the forward vector around the right vector - :param up: Whether to rotate the up vector around the right vector - """ - _forward = Vec3(data.forward[0], data.forward[1], data.forward[2]) - _up = Vec3(data.up[0], data.up[1], data.up[2]) - _crossed_vec = _forward.cross(_up) - _right: Tuple[float, float, float] = (_crossed_vec.x, _crossed_vec.y, _crossed_vec.z) - if forward: - data.forward = quaternion_rotation(_right, data.forward, angle) - if up: - 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 - - 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_3D(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 (0.0 - 1.0 range) - :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 = _interpolate_3D(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 (0.0 - 1.0 range) - :param target: The 2D position the camera should move towards in world space. (vector in XY-plane) - :param data: The camera data object which stores its position, rotation, and direction. - """ - simple_follow_3D(speed, (target[0], target[1], 0.0), data) - - -def simple_easing_3D(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 3D 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 = _interpolate_3D(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 simple method which moves a camera in a straight line between two 2D points (along XY plane). - 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 2D point which acts as the starting point for the camera motion. - :param target: The 2D 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_3D(percent, (start[0], start[1], 0.0), (target[0], target[1], 0.0), data, func) diff --git a/arcade/camera/grips/__init__.py b/arcade/camera/grips/__init__.py new file mode 100644 index 000000000..544c904a2 --- /dev/null +++ b/arcade/camera/grips/__init__.py @@ -0,0 +1,12 @@ +from arcade.camera.grips.strafe import strafe +from arcade.camera.grips.rotate import rotate_around_forward, rotate_around_up, rotate_around_right +from arcade.camera.grips.screen_shake_2d import ScreenShake2D + + +__all__ = ( + "strafe", + "rotate_around_right", + "rotate_around_up", + "rotate_around_forward", + "ScreenShake2D" +) diff --git a/arcade/camera/grips/rotate.py b/arcade/camera/grips/rotate.py new file mode 100644 index 000000000..dfa8edad6 --- /dev/null +++ b/arcade/camera/grips/rotate.py @@ -0,0 +1,59 @@ +from typing import Tuple + +from pyglet.math import Vec3 + +from arcade.math import quaternion_rotation +from arcade.camera.data_types import CameraData + + +__all__ = ( + "rotate_around_forward", + "rotate_around_up", + "rotate_around_right" +) + + +def rotate_around_forward(data: CameraData, angle: float) -> Tuple[float, float, float]: + """ + Rotate the CameraData up vector around the CameraData forward vector, perfect for rotating the screen. + This rotation will be around (0.0, 0.0) of the camera projection. + If that is not the center of the screen this method may appear erroneous. + Uses arcade.camera.controllers.quaternion_rotation internally. + + :param data: The camera data to modify. The data's up vector is rotated around its forward vector + :param angle: The angle in degrees to rotate clockwise by + """ + return quaternion_rotation(data.forward, data.up, angle) + + +def rotate_around_up(data: CameraData, angle: float) -> Tuple[float, float, float]: + """ + Rotate the CameraData forward vector around the CameraData up vector. + Generally only useful in 3D games. + Uses arcade.camera.controllers.quaternion_rotation internally. + + :param data: The camera data to modify. The data's forward vector is rotated around its up vector + :param angle: The angle in degrees to rotate clockwise by + """ + return quaternion_rotation(data.up, data.forward, angle) + + +def rotate_around_right( + data: CameraData, + angle: float) -> Tuple[Tuple[float, float, float], Tuple[float, float, float]]: + """ + Rotate both the CameraData's forward vector and up vector around a calculated right vector. + Generally only useful in 3D games. + Uses arcade.camera.controllers.quaternion_rotation internally. + + :param data: The camera data to modify. The data's forward vector is rotated around its up vector + :param angle: The angle in degrees to rotate clockwise by + """ + _forward = Vec3(data.forward[0], data.forward[1], data.forward[2]) + _up = Vec3(data.up[0], data.up[1], data.up[2]) + _crossed_vec = _forward.cross(_up) + _right: Tuple[float, float, float] = (_crossed_vec.x, _crossed_vec.y, _crossed_vec.z) + new_forward = quaternion_rotation(_right, data.forward, angle) + new_up = quaternion_rotation(_right, data.up, angle) + return new_up, new_forward + diff --git a/arcade/camera/controllers/simple_controller_classes.py b/arcade/camera/grips/screen_shake_2d.py similarity index 98% rename from arcade/camera/controllers/simple_controller_classes.py rename to arcade/camera/grips/screen_shake_2d.py index 02c3e4a81..557ab95be 100644 --- a/arcade/camera/controllers/simple_controller_classes.py +++ b/arcade/camera/grips/screen_shake_2d.py @@ -1,7 +1,5 @@ """ -A small group of classes that offer common controller use-cases. - -ScreenShakeController: +ScreenShakeController2D: Provides an easy way to cause a camera to shake. """ from typing import Tuple @@ -9,14 +7,15 @@ from random import uniform from arcade.camera.data_types import CameraData -from arcade.camera.controllers import quaternion_rotation +from arcade.math import quaternion_rotation + -__all__ = [ - 'ScreenShakeController' -] +__all__ = ( + "ScreenShake2D", +) -class ScreenShakeController: +class ScreenShake2D: """ Offsets the camera position in a random direction repeatedly over a set length of time to create a screen shake effect. diff --git a/arcade/camera/grips/strafe.py b/arcade/camera/grips/strafe.py new file mode 100644 index 000000000..de9b744f0 --- /dev/null +++ b/arcade/camera/grips/strafe.py @@ -0,0 +1,25 @@ +from typing import Tuple + +from pyglet.math import Vec3 + +from arcade.camera.data_types import CameraData + + +def strafe(data: CameraData, direction: Tuple[float, float]) -> Tuple[float, float, float]: + """ + Move the CameraData in a 2D direction aligned to the up-right plane of the view. + A value of [1, 0] will move the camera sideways while a value of [0, 1] + will move the camera upwards. Works irrespective of which direction the camera is facing. + """ + _forward = Vec3(*data.forward).normalize() + _up = Vec3(*data.up).normalize() + _right = _forward.cross(_up) + + _pos = data.position + + offset = _right * direction[0] + _up * direction[1] + return ( + _pos[0] + offset[0], + _pos[1] + offset[1], + _pos[2] + offset[2] + ) diff --git a/tests/unit/camera/test_camera_controller_methods.py b/tests/unit/camera/test_camera_controller_methods.py index fba660375..c937d08da 100644 --- a/tests/unit/camera/test_camera_controller_methods.py +++ b/tests/unit/camera/test_camera_controller_methods.py @@ -1,7 +1,7 @@ import pytest as pytest from arcade import camera, Window -import arcade.camera.controllers as controllers +import arcade.camera.grips as grips def test_strafe(): @@ -10,12 +10,12 @@ def test_strafe(): directions = ((1.0, 0.0), (0.0, 1.0), (-1.0, 0.0), (0.0, -1.0), (0.5, 0.5)) # When - camera_data.forward = (0.0 ,0.0, -1.0) + camera_data.forward = (0.0, 0.0, -1.0) camera_data.up = (0.0, 1.0, 0.0) # Then for dirs in directions: - controllers.strafe(camera_data, dirs) + camera_data.position = grips.strafe(camera_data, dirs) assert camera_data.position == (dirs[0], dirs[1], 0.0), f"Strafe failed to move the camera data correctly, {dirs}" camera_data.position = (0.0, 0.0, 0.0) @@ -24,74 +24,40 @@ def test_strafe(): camera_data.up = (0.0, 1.0, 0.0) for dirs in directions: - controllers.strafe(camera_data, dirs) + camera_data.position = grips.strafe(camera_data, dirs) assert camera_data.position == (0.0, dirs[1], dirs[0]), f"Strafe failed to move the camera data correctly, {dirs}" camera_data.position = (0.0, 0.0, 0.0) def test_rotate_around_forward(): - # TODO - # Given camera_data = camera.CameraData() # When - controllers.rotate_around_forward(camera_data, 90) + camera_data.up = grips.rotate_around_forward(camera_data, 90) # Then assert camera_data.up == pytest.approx((-1.0, 0.0, 0.0)) def test_rotate_around_up(window: Window): - # TODO - # Given camera_data = camera.CameraData() # When + camera_data.forward = grips.rotate_around_up(camera_data, 90) # Then + assert camera_data.forward == pytest.approx((1.0, 0.0, 0.0)) def test_rotate_around_right(window: Window): - # TODO - - # Given - camera_data = camera.CameraData() - - # When - - # Then - - -def test_interpolate(window: Window): - # TODO - - # Given - camera_data = camera.CameraData() - - # When - - # Then - - -def test_simple_follow(window: Window): - # TODO - - # Given - camera_data = camera.CameraData() - - # When - - # Then - - -def test_simple_easing(window: Window): - # TODO - # Given camera_data = camera.CameraData() # When + camera_data.up, camera_data.forward = grips.rotate_around_right(camera_data, 90) # Then + assert camera_data.up == pytest.approx((0.0, 0.0, -1.0)) + assert camera_data.forward == pytest.approx((0.0, -1.0, 0.0)) diff --git a/tests/unit/camera/test_camera_shake.py b/tests/unit/camera/test_camera_shake.py index c83d16fe3..6e431bbfb 100644 --- a/tests/unit/camera/test_camera_shake.py +++ b/tests/unit/camera/test_camera_shake.py @@ -1,11 +1,11 @@ from arcade import camera, Window -from arcade.camera.controllers.simple_controller_classes import ScreenShakeController +from arcade.camera.grips import ScreenShake2D def test_reset(window: Window): # Given camera_view = camera.CameraData() - screen_shaker = ScreenShakeController(camera_view) + screen_shaker = ScreenShake2D(camera_view) # When screen_shaker.start() @@ -24,7 +24,7 @@ def test_reset(window: Window): def test_update(window: Window): # Given camera_view = camera.CameraData() - screen_shaker = ScreenShakeController(camera_view) + screen_shaker = ScreenShake2D(camera_view) # When screen_shaker.update(1/60) @@ -57,7 +57,7 @@ def test_update(window: Window): def test_update_camera(window: Window): # Given camera_view = camera.CameraData() - screen_shaker = ScreenShakeController(camera_view) + screen_shaker = ScreenShake2D(camera_view) cam_pos = camera_view.position @@ -79,7 +79,7 @@ def test_update_camera(window: Window): def test_readjust_camera(window: Window): camera_view = camera.CameraData() - screen_shaker = ScreenShakeController(camera_view) + screen_shaker = ScreenShake2D(camera_view) cam_pos = camera_view.position From be469070539bbe34db15e2df8058033776625fce Mon Sep 17 00:00:00 2001 From: DragonMoffon Date: Fri, 29 Mar 2024 17:53:44 +1300 Subject: [PATCH 80/94] updated examples to use new functions for camera control --- 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 | 3 +-- arcade/examples/camera_platform.py | 16 ++++++++-------- arcade/examples/full_screen_example.py | 2 +- arcade/examples/minimap.py | 6 +++--- arcade/examples/minimap_camera.py | 18 +++++++++++------- arcade/examples/perspective.py | 2 +- arcade/examples/sprite_move_scrolling.py | 5 ++--- arcade/examples/sprite_move_scrolling_box.py | 2 +- arcade/examples/sprite_move_scrolling_shake.py | 12 ++++++------ arcade/examples/sprite_moving_platforms.py | 2 +- arcade/examples/sprite_tiled_map.py | 2 +- arcade/examples/template_platformer.py | 2 +- doc/tutorials/raycasting/step_08.py | 2 +- 17 files changed, 42 insertions(+), 40 deletions(-) diff --git a/arcade/examples/background_blending.py b/arcade/examples/background_blending.py index 1f16e0924..30e7117eb 100644 --- a/arcade/examples/background_blending.py +++ b/arcade/examples/background_blending.py @@ -65,7 +65,7 @@ def pan_camera_to_player(self): elif target_y > self.background_1.size[1]: target_y = self.background_1.size[1] - arcade.camera.controllers.simple_follow_2D(0.1, (target_x, target_y), self.camera.view_data) + self.camera.position = arcade.math.lerp_2d(self.camera.position, (target_x, target_y), 0.1) def on_update(self, delta_time: float): new_position = ( diff --git a/arcade/examples/background_groups.py b/arcade/examples/background_groups.py index a4607b170..e0353e2af 100644 --- a/arcade/examples/background_groups.py +++ b/arcade/examples/background_groups.py @@ -80,7 +80,7 @@ def pan_camera_to_player(self): elif target_y > 2.0 * self.camera.viewport_height: target_y = 2.0 * self.camera.viewport_height - arcade.camera.controllers.simple_follow_2D(0.5, (target_x, target_y), self.camera.view_data) + self.camera.position = arcade.math.lerp_2d(self.camera.position, (target_x, target_y), 0.5) def on_update(self, delta_time: float): new_position = ( diff --git a/arcade/examples/background_parallax.py b/arcade/examples/background_parallax.py index bc9c8412a..f542dff79 100644 --- a/arcade/examples/background_parallax.py +++ b/arcade/examples/background_parallax.py @@ -90,7 +90,7 @@ def __init__(self): def pan_camera_to_player(self): # Move the camera toward the center of the player's sprite target_x = self.player_sprite.center_x - arcade.camera.controllers.simple_follow_2D(0.1, (target_x, self.height//2), self.camera.view_data) + self.camera.position = arcade.math.lerp_2d(self.camera.position, (target_x, self.height//2), 0.1) def on_update(self, delta_time: float): # Move the player in our infinite world diff --git a/arcade/examples/background_scrolling.py b/arcade/examples/background_scrolling.py index 32ba4aec6..abac9adff 100644 --- a/arcade/examples/background_scrolling.py +++ b/arcade/examples/background_scrolling.py @@ -44,7 +44,7 @@ def pan_camera_to_player(self): # This will center the camera on the player. target_x = self.player_sprite.center_x target_y = self.player_sprite.center_y - arcade.camera.controllers.simple_follow_2D(0.5, (target_x, target_y), self.camera.view_data) + self.camera.position = arcade.math.lerp_2d(self.camera.position, (target_x, target_y), 0.5) def on_update(self, delta_time: float): new_position = ( diff --git a/arcade/examples/background_stationary.py b/arcade/examples/background_stationary.py index 4e5d517cf..cc5930000 100644 --- a/arcade/examples/background_stationary.py +++ b/arcade/examples/background_stationary.py @@ -55,8 +55,7 @@ def pan_camera_to_player(self): target_y = 0.0 elif target_y > self.background.size[1]: target_y = self.background.size[1] - - arcade.camera.controllers.simple_follow_2D(0.1, (target_x, target_y), self.camera.view_data) + self.camera.position = arcade.math.lerp_2d(self.camera.position, (target_x, target_y), 0.1) def on_update(self, delta_time: float): new_position = ( diff --git a/arcade/examples/camera_platform.py b/arcade/examples/camera_platform.py index c83ff114e..a83b42d1c 100644 --- a/arcade/examples/camera_platform.py +++ b/arcade/examples/camera_platform.py @@ -132,14 +132,14 @@ def setup(self): self.scene.add_sprite("Player", self.player_sprite) viewport = (0, 0, SCREEN_WIDTH, SCREEN_HEIGHT) - self.camera = arcade.camera.Camera2D(viewport=viewport) - self.gui_camera = arcade.camera.Camera2D(viewport=viewport) + self.camera = arcade.camera.Camera2D.from_raw_data(viewport=viewport) + self.gui_camera = arcade.camera.Camera2D.from_raw_data(viewport=viewport) - self.camera_shake = arcade.camera.controllers.ScreenShakeController(self.camera.view_data, - max_amplitude=12.5, - acceleration_duration=0.05, - falloff_time=0.20, - shake_frequency=15.0) + self.camera_shake = arcade.camera.grips.ScreenShake2D(self.camera.view_data, + max_amplitude=12.5, + acceleration_duration=0.05, + falloff_time=0.20, + shake_frequency=15.0) # Center camera on user self.pan_camera_to_user() @@ -238,7 +238,7 @@ def pan_camera_to_user(self, panning_fraction: float = 1.0): screen_center_y = self.camera.viewport_height/2 user_centered = screen_center_x, screen_center_y - arcade.camera.controllers.simple_follow_2D(panning_fraction, user_centered, self.camera.view_data) + self.camera.position = arcade.math.lerp_2d(self.camera.position, user_centered, panning_fraction) def on_update(self, delta_time): """Movement and game logic""" diff --git a/arcade/examples/full_screen_example.py b/arcade/examples/full_screen_example.py index 307e31a50..f1cf28478 100644 --- a/arcade/examples/full_screen_example.py +++ b/arcade/examples/full_screen_example.py @@ -43,7 +43,7 @@ def __init__(self): # The camera used to update the viewport and projection on screen resize. # The position needs to be set to the bottom left corner. - self.cam = arcade.camera.Camera2D(position=(0.0, 0.0)) + self.cam = arcade.camera.Camera2D.from_raw_data(position=(0.0, 0.0)) def on_draw(self): """ diff --git a/arcade/examples/minimap.py b/arcade/examples/minimap.py index b6d360dc6..aeeea4784 100644 --- a/arcade/examples/minimap.py +++ b/arcade/examples/minimap.py @@ -62,8 +62,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.camera.Camera2D(viewport=viewport) - self.camera_gui = arcade.camera.Camera2D(viewport=viewport) + self.camera_sprites = arcade.camera.Camera2D.from_raw_data(viewport=viewport) + self.camera_gui = arcade.camera.Camera2D.from_raw_data(viewport=viewport) def setup(self): """ Set up the game and initialize the variables. """ @@ -180,7 +180,7 @@ def scroll_to_player(self): # Scroll to the proper location position = (self.player_sprite.center_x, self.player_sprite.center_y) - arcade.camera.controllers.simple_follow_2D(CAMERA_SPEED, position, self.camera_sprites.view_data) + self.camera_sprites.position = arcade.math.lerp_2d(self.camera_sprites.position, position, CAMERA_SPEED) def on_resize(self, width: int, height: int): """ diff --git a/arcade/examples/minimap_camera.py b/arcade/examples/minimap_camera.py index 3c0eaf4a6..3d9df371a 100644 --- a/arcade/examples/minimap_camera.py +++ b/arcade/examples/minimap_camera.py @@ -57,7 +57,9 @@ def __init__(self, width, height, title): MINIMAP_WIDTH, MINIMAP_HEIGHT) minimap_projection = (-MAP_PROJECTION_WIDTH/2, MAP_PROJECTION_WIDTH/2, -MAP_PROJECTION_HEIGHT/2, MAP_PROJECTION_HEIGHT/2) - self.camera_minimap = arcade.camera.Camera2D(viewport=minimap_viewport, projection=minimap_projection) + self.camera_minimap = arcade.camera.Camera2D.from_raw_data( + viewport=minimap_viewport, projection=minimap_projection + ) # Set up the player self.player_sprite = None @@ -66,8 +68,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.camera.Camera2D(viewport=viewport) - self.camera_gui = arcade.camera.Camera2D(viewport=viewport) + self.camera_sprites = arcade.camera.Camera2D.from_raw_data(viewport=viewport) + self.camera_gui = arcade.camera.Camera2D.from_raw_data(viewport=viewport) self.selected_camera = self.camera_minimap @@ -178,12 +180,14 @@ def on_update(self, delta_time): self.physics_engine.update() # Center the screen to the player - arcade.camera.controllers.simple_follow_2D(CAMERA_SPEED, self.player_sprite.position, - self.camera_sprites.view_data) + self.camera_sprites.position = arcade.math.lerp_2d( + self.camera_sprites.position, self.player_sprite.position, CAMERA_SPEED + ) # Center the minimap viewport to the player in the minimap - arcade.camera.controllers.simple_follow_2D(CAMERA_SPEED, self.player_sprite.position, - self.camera_minimap.view_data) + self.camera_minimap.position = arcade.math.lerp_2d( + self.player_sprite.position, self.player_sprite.position, CAMERA_SPEED + ) def on_resize(self, width: int, height: int): """ diff --git a/arcade/examples/perspective.py b/arcade/examples/perspective.py index 4f89cb2b9..ee27ba49d 100644 --- a/arcade/examples/perspective.py +++ b/arcade/examples/perspective.py @@ -106,7 +106,7 @@ def __init__(self): ) self.time = 0 - self.offscreen_cam = arcade.camera.Camera2D( + self.offscreen_cam = arcade.camera.Camera2D.from_raw_data( position=(0.0, 0.0), viewport=(0, 0, self.fbo.width, self.fbo.height), projection=(0, self.fbo.width, 0, self.fbo.height) diff --git a/arcade/examples/sprite_move_scrolling.py b/arcade/examples/sprite_move_scrolling.py index 32de6539b..8f9894836 100644 --- a/arcade/examples/sprite_move_scrolling.py +++ b/arcade/examples/sprite_move_scrolling.py @@ -168,9 +168,8 @@ def scroll_to_player(self): pan. """ - position = (self.player_sprite.center_x, - self.player_sprite.center_y) - arcade.camera.controllers.simple_follow_2D(CAMERA_SPEED, position, self.camera_sprites.view_data) + position = (self.player_sprite.center_x, self.player_sprite.center_y) + self.camera_sprites.position = arcade.math.lerp_2d(self.camera_sprites.position, position, CAMERA_SPEED) def on_resize(self, width: int, height: int): """ diff --git a/arcade/examples/sprite_move_scrolling_box.py b/arcade/examples/sprite_move_scrolling_box.py index 3a5ed18ab..99d6712ba 100644 --- a/arcade/examples/sprite_move_scrolling_box.py +++ b/arcade/examples/sprite_move_scrolling_box.py @@ -200,7 +200,7 @@ def scroll_to_player(self): # Scroll to the proper location position = _target_x, _target_y - arcade.camera.controllers.simple_follow_2D(CAMERA_SPEED, position, self.camera_sprites.view_data) + self.camera_sprites.position = arcade.math.lerp_2d(self.camera_sprites.position, position, CAMERA_SPEED) def on_resize(self, width: int, height: int): """ diff --git a/arcade/examples/sprite_move_scrolling_shake.py b/arcade/examples/sprite_move_scrolling_shake.py index cca6c6371..a3ea08a78 100644 --- a/arcade/examples/sprite_move_scrolling_shake.py +++ b/arcade/examples/sprite_move_scrolling_shake.py @@ -57,11 +57,11 @@ def __init__(self, width, height, title): self.camera_sprites = arcade.camera.Camera2D() self.camera_gui = arcade.camera.Camera2D() - self.camera_shake = arcade.camera.controllers.ScreenShakeController(self.camera_sprites.view_data, - max_amplitude=15.0, - acceleration_duration=0.1, - falloff_time=0.5, - shake_frequency=10.0) + self.camera_shake = arcade.camera.grips.ScreenShake2D(self.camera_sprites.view_data, + max_amplitude=15.0, + acceleration_duration=0.1, + falloff_time=0.5, + shake_frequency=10.0) self.explosion_sound = arcade.load_sound(":resources:sounds/explosion1.wav") @@ -177,7 +177,7 @@ def scroll_to_player(self): self.player_sprite.center_x, self.player_sprite.center_y ) - arcade.camera.controllers.simple_follow_2D(CAMERA_SPEED, position, self.camera_sprites.view_data) + self.camera_sprites.position = arcade.math.lerp_2d(self.camera_sprites.position, position, CAMERA_SPEED) def on_resize(self, width: int, height: int): """ diff --git a/arcade/examples/sprite_moving_platforms.py b/arcade/examples/sprite_moving_platforms.py index 2918b11fe..5232a15ce 100644 --- a/arcade/examples/sprite_moving_platforms.py +++ b/arcade/examples/sprite_moving_platforms.py @@ -205,7 +205,7 @@ def scroll_to_player(self): """ position = (self.player_sprite.center_x, self.player_sprite.center_y) - arcade.camera.controllers.simple_follow_2D(CAMERA_SPEED, position, self.camera_sprites.view_data) + self.camera_sprites.position = arcade.math.lerp_2d(self.camera_sprites.position, position, CAMERA_SPEED) def main(): diff --git a/arcade/examples/sprite_tiled_map.py b/arcade/examples/sprite_tiled_map.py index 04fd8bbf2..d0dc85e39 100644 --- a/arcade/examples/sprite_tiled_map.py +++ b/arcade/examples/sprite_tiled_map.py @@ -237,7 +237,7 @@ def pan_camera_to_user(self, panning_fraction: float = 1.0): screen_center_y = self.height/2 user_centered = screen_center_x, screen_center_y - arcade.camera.controllers.simple_follow_2D(panning_fraction, user_centered, self.camera.view_data) + self.camera.position = arcade.math.lerp_2d(self.camera.position, user_centered, panning_fraction) def main(): diff --git a/arcade/examples/template_platformer.py b/arcade/examples/template_platformer.py index 3efadcd85..0b7e73019 100644 --- a/arcade/examples/template_platformer.py +++ b/arcade/examples/template_platformer.py @@ -181,7 +181,7 @@ def center_camera_to_player(self): # Here's our center, move to it player_centered = screen_center_x, screen_center_y - arcade.camera.controllers.simple_follow_2D(0.1, player_centered, self.camera_sprites.view_data) + self.camera_sprites.position = arcade.math.lerp_2d(self.camera_sprites.position, player_centered, 0.1) def on_update(self, delta_time): """Movement and game logic""" diff --git a/doc/tutorials/raycasting/step_08.py b/doc/tutorials/raycasting/step_08.py index 54eff0af5..cb1e998ea 100644 --- a/doc/tutorials/raycasting/step_08.py +++ b/doc/tutorials/raycasting/step_08.py @@ -186,7 +186,7 @@ def scroll_to_player(self, speed=CAMERA_SPEED): """ position = (self.player_sprite.center_x, self.player_sprite.center_y) - arcade.camera.controllers.simple_follow_2D(speed, position, self.camera_sprites.view_data) + self.camera_sprites.position = arcade.math.lerp_2d(self.camera_sprites.position, position, CAMERA_SPEED) def on_resize(self, width: int, height: int): super().on_resize(width, height) From bb2c1d8c9f73d3fac1889ccbdc539e437215f6f4 Mon Sep 17 00:00:00 2001 From: DragonMoffon Date: Fri, 29 Mar 2024 18:04:52 +1300 Subject: [PATCH 81/94] remove example from arcade.math.quaternion_rotation --- arcade/math.py | 24 ------------------------ 1 file changed, 24 deletions(-) diff --git a/arcade/math.py b/arcade/math.py index 68ceba97e..636cafb6e 100644 --- a/arcade/math.py +++ b/arcade/math.py @@ -382,30 +382,6 @@ def quaternion_rotation(axis: Tuple[float, float, float], to find the axis of rotation rather than 3 angles of rotation. Ref: https://danceswithcode.net/engineeringnotes/quaternions/quaternions.html. - Example: - import arcade - from arcade.camera.controllers import quaternion_rotation - - - # Rotating a sprite around a point - sprite = arcade.Sprite(center_x=0.0, center_y=10.0) - rotation_point = (0.0, 0.0) - - # Find the relative vector between the sprite and point to rotate. (Must be a 3D vector) - relative_position = sprite.center_x - rotation_point[0], sprite.center_y - rotation_point[1], 0.0 - - # Because arcade uses the X and Y axis for 2D co-ordinates the Z-axis becomes the rotation axis. - rotation_axis = (0.0, 0.0, 1.0) - - # Rotate the vector 45 degrees clockwise. - new_relative_position = quaternion_rotation(rotation_axis, relative_position, 45) - - - sprite.position = ( - rotation_point[0] + new_relative_position[0], - rotation_point[1] + new_relative_position[1] - ) - :param axis: The unit length vector that will be rotated around :param vector: The 3-dimensional vector to be rotated :param angle: The angle in degrees to rotate the vector clock-wise by From 3b118c8b84e5dec3bd886ce8d404d19096371cb7 Mon Sep 17 00:00:00 2001 From: DragonMoffon Date: Fri, 29 Mar 2024 19:45:14 +1300 Subject: [PATCH 82/94] improving typing for ui_manager --- arcade/gui/ui_manager.py | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/arcade/gui/ui_manager.py b/arcade/gui/ui_manager.py index 0cbde5247..6303f5446 100644 --- a/arcade/gui/ui_manager.py +++ b/arcade/gui/ui_manager.py @@ -11,7 +11,7 @@ from __future__ import annotations from collections import defaultdict -from typing import Dict, Iterable, List, Optional, Type, TypeVar, Union +from typing import Dict, Iterable, List, Optional, Type, TypeVar, Union, Tuple from pyglet.event import EVENT_HANDLED, EVENT_UNHANDLED, EventDispatcher from typing_extensions import TypeGuard @@ -329,7 +329,7 @@ def draw(self) -> None: for layer in layers: self._get_surface(layer).draw() - def adjust_mouse_coordinates(self, x, y): + def adjust_mouse_coordinates(self, x: float, y: float) -> Tuple[float, float]: """ This method is used, to translate mouse coordinates to coordinates respecting the viewport and projection of cameras. @@ -337,7 +337,8 @@ def adjust_mouse_coordinates(self, x, y): It uses the internal camera's map_coordinate methods, and should work with all transformations possible with the basic orthographic camera. """ - return self.window.current_camera.map_coordinate((x, y))[:2] + x_, y_, *c = self.window.current_camera.map_coordinate((x, y)) + return x_, y_ def on_event(self, event) -> Union[bool, None]: layers = sorted(self.children.keys(), reverse=True) @@ -352,27 +353,27 @@ def dispatch_ui_event(self, event): return self.dispatch_event("on_event", event) def on_mouse_motion(self, x: int, y: int, dx: int, dy: int): - x, y = self.adjust_mouse_coordinates(x, y) - return self.dispatch_ui_event(UIMouseMovementEvent(self, x, y, dx, dy)) # type: ignore + x_, y_ = self.adjust_mouse_coordinates(x, y) + return self.dispatch_ui_event(UIMouseMovementEvent(self, int(x_), int(y), dx, dy)) def on_mouse_press(self, x: int, y: int, button: int, modifiers: int): - x, y = self.adjust_mouse_coordinates(x, y) - return self.dispatch_ui_event(UIMousePressEvent(self, x, y, button, modifiers)) # type: ignore + x_, y_ = self.adjust_mouse_coordinates(x, y) + return self.dispatch_ui_event(UIMousePressEvent(self, int(x_), int(y_), button, modifiers)) def on_mouse_drag( self, x: int, y: int, dx: int, dy: int, buttons: int, modifiers: int ): - x, y = self.adjust_mouse_coordinates(x, y) - return self.dispatch_ui_event(UIMouseDragEvent(self, x, y, dx, dy, buttons, modifiers)) # type: ignore + x_, y_ = self.adjust_mouse_coordinates(x, y) + return self.dispatch_ui_event(UIMouseDragEvent(self, int(x_), int(y_), dx, dy, buttons, modifiers)) def on_mouse_release(self, x: int, y: int, button: int, modifiers: int): - x, y = self.adjust_mouse_coordinates(x, y) - return self.dispatch_ui_event(UIMouseReleaseEvent(self, x, y, button, modifiers)) # type: ignore + x_, y_ = self.adjust_mouse_coordinates(x, y) + return self.dispatch_ui_event(UIMouseReleaseEvent(self, int(x_), int(y_), button, modifiers)) def on_mouse_scroll(self, x, y, scroll_x, scroll_y): - x, y = self.adjust_mouse_coordinates(x, y) + x_, y_ = self.adjust_mouse_coordinates(x, y) return self.dispatch_ui_event( - UIMouseScrollEvent(self, x, y, scroll_x, scroll_y) + UIMouseScrollEvent(self, int(x_), int(y_), scroll_x, scroll_y) ) def on_key_press(self, symbol: int, modifiers: int): From 4e4d08e99bbf5974bd7b92ab307083dd37505b46 Mon Sep 17 00:00:00 2001 From: DragonMoffon Date: Fri, 29 Mar 2024 19:49:35 +1300 Subject: [PATCH 83/94] Revert "improving typing for ui_manager" This reverts commit 3b118c8b84e5dec3bd886ce8d404d19096371cb7. --- arcade/gui/ui_manager.py | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/arcade/gui/ui_manager.py b/arcade/gui/ui_manager.py index 6303f5446..0cbde5247 100644 --- a/arcade/gui/ui_manager.py +++ b/arcade/gui/ui_manager.py @@ -11,7 +11,7 @@ from __future__ import annotations from collections import defaultdict -from typing import Dict, Iterable, List, Optional, Type, TypeVar, Union, Tuple +from typing import Dict, Iterable, List, Optional, Type, TypeVar, Union from pyglet.event import EVENT_HANDLED, EVENT_UNHANDLED, EventDispatcher from typing_extensions import TypeGuard @@ -329,7 +329,7 @@ def draw(self) -> None: for layer in layers: self._get_surface(layer).draw() - def adjust_mouse_coordinates(self, x: float, y: float) -> Tuple[float, float]: + def adjust_mouse_coordinates(self, x, y): """ This method is used, to translate mouse coordinates to coordinates respecting the viewport and projection of cameras. @@ -337,8 +337,7 @@ def adjust_mouse_coordinates(self, x: float, y: float) -> Tuple[float, float]: It uses the internal camera's map_coordinate methods, and should work with all transformations possible with the basic orthographic camera. """ - x_, y_, *c = self.window.current_camera.map_coordinate((x, y)) - return x_, y_ + return self.window.current_camera.map_coordinate((x, y))[:2] def on_event(self, event) -> Union[bool, None]: layers = sorted(self.children.keys(), reverse=True) @@ -353,27 +352,27 @@ def dispatch_ui_event(self, event): return self.dispatch_event("on_event", event) def on_mouse_motion(self, x: int, y: int, dx: int, dy: int): - x_, y_ = self.adjust_mouse_coordinates(x, y) - return self.dispatch_ui_event(UIMouseMovementEvent(self, int(x_), int(y), dx, dy)) + x, y = self.adjust_mouse_coordinates(x, y) + return self.dispatch_ui_event(UIMouseMovementEvent(self, x, y, dx, dy)) # type: ignore def on_mouse_press(self, x: int, y: int, button: int, modifiers: int): - x_, y_ = self.adjust_mouse_coordinates(x, y) - return self.dispatch_ui_event(UIMousePressEvent(self, int(x_), int(y_), button, modifiers)) + x, y = self.adjust_mouse_coordinates(x, y) + return self.dispatch_ui_event(UIMousePressEvent(self, x, y, button, modifiers)) # type: ignore def on_mouse_drag( self, x: int, y: int, dx: int, dy: int, buttons: int, modifiers: int ): - x_, y_ = self.adjust_mouse_coordinates(x, y) - return self.dispatch_ui_event(UIMouseDragEvent(self, int(x_), int(y_), dx, dy, buttons, modifiers)) + x, y = self.adjust_mouse_coordinates(x, y) + return self.dispatch_ui_event(UIMouseDragEvent(self, x, y, dx, dy, buttons, modifiers)) # type: ignore def on_mouse_release(self, x: int, y: int, button: int, modifiers: int): - x_, y_ = self.adjust_mouse_coordinates(x, y) - return self.dispatch_ui_event(UIMouseReleaseEvent(self, int(x_), int(y_), button, modifiers)) + x, y = self.adjust_mouse_coordinates(x, y) + return self.dispatch_ui_event(UIMouseReleaseEvent(self, x, y, button, modifiers)) # type: ignore def on_mouse_scroll(self, x, y, scroll_x, scroll_y): - x_, y_ = self.adjust_mouse_coordinates(x, y) + x, y = self.adjust_mouse_coordinates(x, y) return self.dispatch_ui_event( - UIMouseScrollEvent(self, int(x_), int(y_), scroll_x, scroll_y) + UIMouseScrollEvent(self, x, y, scroll_x, scroll_y) ) def on_key_press(self, symbol: int, modifiers: int): From 788072e26f25819be7c5d84fe5550e3cc02a3122 Mon Sep 17 00:00:00 2001 From: DragonMoffon Date: Fri, 29 Mar 2024 19:49:40 +1300 Subject: [PATCH 84/94] Revert "Merge branch 'development' into Camera-Overhaul-Simple" This reverts commit 94b312852464da5194e4b3367c4377bb1829a0b2, reversing changes made to bb2c1d8c9f73d3fac1889ccbdc539e437215f6f4. --- .gitignore | 3 - arcade/application.py | 60 ++-- arcade/examples/array_backed_grid.py | 1 + arcade/examples/dual_stick_shooter.py | 9 +- arcade/examples/gl/3d_cube_with_cubes.py | 2 +- arcade/examples/light_demo.py | 6 +- arcade/examples/minimap.py | 2 +- arcade/examples/minimap_camera.py | 2 +- arcade/examples/particle_fireworks.py | 8 +- arcade/examples/particle_systems.py | 8 +- arcade/examples/performance_statistics.py | 1 + arcade/examples/perspective.py | 7 +- arcade/examples/pymunk_joint_builder.py | 8 +- arcade/examples/sections_demo_1.py | 2 +- arcade/examples/sections_demo_2.py | 31 +- arcade/examples/sprite_animated_keyframes.py | 7 +- arcade/examples/sprite_explosion_bitmapped.py | 2 +- arcade/examples/sprite_minimal.py | 6 +- arcade/examples/sprite_move_keyboard_accel.py | 2 +- arcade/examples/tetris.py | 2 +- arcade/examples/transform_feedback.py | 6 +- arcade/experimental/atlas_render_into.py | 4 +- arcade/experimental/clock/clock_window.py | 33 +- arcade/experimental/depth_of_field.py | 239 ------------- arcade/experimental/lights.py | 2 +- .../sprite_collect_coins_minimap.py | 2 +- arcade/experimental/texture_render_target.py | 2 +- arcade/gl/buffer.py | 2 +- arcade/gl/framebuffer.py | 88 ++--- arcade/gui/events.py | 12 +- arcade/gui/experimental/scroll_area.py | 4 +- arcade/gui/property.py | 48 +-- arcade/gui/ui_manager.py | 13 +- arcade/gui/widgets/__init__.py | 23 +- arcade/gui/widgets/text.py | 8 +- arcade/perf_graph.py | 2 +- arcade/shape_list.py | 3 +- arcade/sprite/animated.py | 28 +- arcade/sprite/base.py | 87 ++--- arcade/sprite_list/sprite_list.py | 2 +- arcade/text.py | 2 +- arcade/window_commands.py | 2 +- doc/_static/checkered.png | Bin 4885 -> 0 bytes doc/_static/css/custom.css | 124 ++----- doc/conf.py | 84 ++--- .../how_to_examples/gui_flat_button.rst | 26 -- .../gui_flat_button_positioned.png | Bin 6843 -> 0 bytes doc/get_started/install/index.rst | 12 - doc/index.rst | 322 +++++++++--------- doc/programming_guide/gui/concept.rst | 4 +- doc/programming_guide/sound.rst | 2 +- doc/tutorials/raycasting/example.py | 6 +- doc/tutorials/shader_inputs/texture_write.py | 2 +- doc/tutorials/shader_inputs/textures.py | 4 +- doc/tutorials/shader_toy_particles/index.rst | 3 +- pyproject.toml | 18 +- tests/conftest.py | 138 +------- .../examples}/check_examples_2.py | 0 .../examples}/check_samples.py | 0 .../integration/examples/run_all_examples.py | 68 ++++ .../integration/examples/test_all_examples.py | 78 +++++ tests/integration/examples/test_examples.py | 64 ---- .../tutorials/run_all_tutorials.py | 67 ++++ .../tutorials/test_all_tutoirals.py | 76 +++++ tests/integration/tutorials/test_tutorials.py | 47 --- tests/unit/gl/test_opengl_framebuffer.py | 11 +- tests/unit/gui/test_property.py | 69 +--- tests/unit/sprite/test_sprite.py | 8 +- .../sprite/test_sprite_texture_animation.py | 12 + 69 files changed, 768 insertions(+), 1258 deletions(-) delete mode 100644 arcade/experimental/depth_of_field.py delete mode 100644 doc/_static/checkered.png delete mode 100644 doc/example_code/how_to_examples/gui_flat_button_positioned.png rename tests/{doc => integration/examples}/check_examples_2.py (100%) rename tests/{doc => integration/examples}/check_samples.py (100%) create mode 100644 tests/integration/examples/run_all_examples.py create mode 100644 tests/integration/examples/test_all_examples.py delete mode 100644 tests/integration/examples/test_examples.py create mode 100644 tests/integration/tutorials/run_all_tutorials.py create mode 100644 tests/integration/tutorials/test_all_tutoirals.py delete mode 100644 tests/integration/tutorials/test_tutorials.py diff --git a/.gitignore b/.gitignore index aff91edf2..5b1be3dc3 100644 --- a/.gitignore +++ b/.gitignore @@ -61,6 +61,3 @@ temp/ *.tiled-session doc/api_docs/api/*.rst - -# Pyenv local -.python-version diff --git a/arcade/application.py b/arcade/application.py index 44b7d95d0..4c463e425 100644 --- a/arcade/application.py +++ b/arcade/application.py @@ -20,7 +20,7 @@ from arcade import set_window from arcade.color import TRANSPARENT_BLACK from arcade.context import ArcadeContext -from arcade.types import Color, RGBOrA255, RGBANormalized +from arcade.types import Color, RGBA255, RGBA255OrNormalized from arcade import SectionManager from arcade.utils import is_raspberry_pi from arcade.camera import Projector @@ -261,10 +261,10 @@ def ctx(self) -> ArcadeContext: return self._ctx def clear( - self, - color: Optional[RGBOrA255] = None, - color_normalized: Optional[RGBANormalized] = None, - viewport: Optional[Tuple[int, int, int, int]] = None, + self, + color: Optional[RGBA255OrNormalized] = None, + normalized: bool = False, + viewport: Optional[Tuple[int, int, int, int]] = None, ): """Clears the window with the configured background color set through :py:attr:`arcade.Window.background_color`. @@ -273,18 +273,14 @@ def clear( with one of the following: 1. A :py:class:`~arcade.types.Color` instance - 2. A 3 or 4-length RGB/RGBA :py:class:`tuple` of byte values (0 to 255) - - :param color_normalized: (Optional) override the current background color - using normalized values (0.0 to 1.0). For example, (1.0, 0.0, 0.0, 1.0) - making the window contents red. + 2. A 4-length RGBA :py:class:`tuple` of byte values (0 to 255) + 3. A 4-length RGBA :py:class:`tuple` of normalized floats (0.0 to 1.0) + :param normalized: If the color format is normalized (0.0 -> 1.0) or byte values :param Tuple[int, int, int, int] viewport: The viewport range to clear """ - # Use the configured background color if none is provided - if color is None and color_normalized is None: - color = self.background_color - self.ctx.screen.clear(color=color, color_normalized=color_normalized, viewport=viewport) + color = color if color is not None else self.background_color + self.ctx.screen.clear(color, normalized=normalized, viewport=viewport) @property def background_color(self) -> Color: @@ -302,7 +298,7 @@ def background_color(self) -> Color: MY_RED = arcade.types.Color(255, 0, 0) window.background_color = MY_RED - # Set the background color directly from an RGBA tuple + # Set the backgrund color directly from an RGBA tuple window.background_color = 255, 0, 0, 255 # (Discouraged) @@ -315,7 +311,7 @@ def background_color(self) -> Color: return self._background_color @background_color.setter - def background_color(self, value: RGBOrA255): + def background_color(self, value: RGBA255): self._background_color = Color.from_iterable(value) def run(self) -> None: @@ -479,7 +475,7 @@ def on_mouse_scroll(self, x: int, y: int, scroll_x: int, scroll_y: int): Override this function to respond to scroll events. The scroll arguments may be positive or negative to indicate direction, but - the units are unstandardized. How many scroll steps you receive + the units are unstandardized. How many scroll steps you recieve may vary wildly between computers depending a number of factors, including system settings and the input devices used (i.e. mouse scrollwheel, touchpad, etc). @@ -564,7 +560,7 @@ def on_key_release(self, symbol: int, modifiers: int): Situations that require handling key releases include: - * Rhythm games where a note must be held for a certain + * Rythm games where a note must be held for a certain amount of time * 'Charging up' actions that change strength depending on how long a key was pressed @@ -726,14 +722,10 @@ def show_view(self, new_view: 'View'): # Store the Window that is showing the "new_view" View. if new_view.window is None: new_view.window = self - # NOTE: This is not likely to happen and is creating issues for the test suite. - # elif new_view.window != self: - # raise RuntimeError(( - # "You are attempting to pass the same view " - # "object between multiple windows. A single " - # "view object can only be used in one window. " - # f"{self} != {new_view.window}" - # )) + elif new_view.window != self: + raise RuntimeError("You are attempting to pass the same view " + "object between multiple windows. A single " + "view object can only be used in one window.") # remove previously shown view's handlers if self._current_view is not None: @@ -983,26 +975,24 @@ def add_section(self, section, at_index: Optional[int] = None, at_draw_order: Op def clear( self, - color: Optional[RGBOrA255] = None, - color_normalized: Optional[RGBANormalized] = None, + color: Optional[RGBA255OrNormalized] = None, + normalized: bool = False, viewport: Optional[Tuple[int, int, int, int]] = None, ): - """Clears the window with the configured background color + """Clears the View's Window with the configured background color set through :py:attr:`arcade.Window.background_color`. :param color: (Optional) override the current background color with one of the following: 1. A :py:class:`~arcade.types.Color` instance - 2. A 3 or 4-length RGB/RGBA :py:class:`tuple` of byte values (0 to 255) - - :param color_normalized: (Optional) override the current background color - using normalized values (0.0 to 1.0). For example, (1.0, 0.0, 0.0, 1.0) - making the window contents red. + 2. A 4-length RGBA :py:class:`tuple` of byte values (0 to 255) + 3. A 4-length RGBA :py:class:`tuple` of normalized floats (0.0 to 1.0) + :param normalized: If the color format is normalized (0.0 -> 1.0) or byte values :param Tuple[int, int, int, int] viewport: The viewport range to clear """ - self.window.clear(color=color, color_normalized=color_normalized, viewport=viewport) + self.window.clear(color, normalized, viewport) def on_update(self, delta_time: float): """To be overridden""" diff --git a/arcade/examples/array_backed_grid.py b/arcade/examples/array_backed_grid.py index 324737646..f84fcda98 100644 --- a/arcade/examples/array_backed_grid.py +++ b/arcade/examples/array_backed_grid.py @@ -104,6 +104,7 @@ def on_mouse_press(self, x, y, button, modifiers): def main(): + MyGame(SCREEN_WIDTH, SCREEN_HEIGHT, SCREEN_TITLE) arcade.run() diff --git a/arcade/examples/dual_stick_shooter.py b/arcade/examples/dual_stick_shooter.py index e34dd1f63..c45f8fc16 100644 --- a/arcade/examples/dual_stick_shooter.py +++ b/arcade/examples/dual_stick_shooter.py @@ -338,11 +338,6 @@ def on_draw(self): anchor_y="center") - -def main(): - game = MyGame(SCREEN_WIDTH, SCREEN_HEIGHT, SCREEN_TITLE) - game.run() - - if __name__ == "__main__": - main() + game = MyGame(SCREEN_WIDTH, SCREEN_HEIGHT, SCREEN_TITLE) + arcade.run() diff --git a/arcade/examples/gl/3d_cube_with_cubes.py b/arcade/examples/gl/3d_cube_with_cubes.py index 803193bc8..09663cdaf 100644 --- a/arcade/examples/gl/3d_cube_with_cubes.py +++ b/arcade/examples/gl/3d_cube_with_cubes.py @@ -110,7 +110,7 @@ def on_draw(self): # Draw the current cube using the last one as a texture self.fbo1.use() - self.fbo1.clear(color_normalized=(1.0, 1.0, 1.0, 1.0)) + self.fbo1.clear(color=(1.0, 1.0, 1.0, 1.0), normalized=True) translate = Mat4.from_translation((0, 0, -1.75)) rx = Mat4.from_rotation(self.time, (1, 0, 0)) diff --git a/arcade/examples/light_demo.py b/arcade/examples/light_demo.py index cedd091a1..07d07ec57 100644 --- a/arcade/examples/light_demo.py +++ b/arcade/examples/light_demo.py @@ -300,11 +300,7 @@ def on_update(self, delta_time): self.scroll_screen() -def main(): +if __name__ == "__main__": window = MyGame(SCREEN_WIDTH, SCREEN_HEIGHT, SCREEN_TITLE) window.setup() arcade.run() - - -if __name__ == "__main__": - main() diff --git a/arcade/examples/minimap.py b/arcade/examples/minimap.py index baf2628b3..aeeea4784 100644 --- a/arcade/examples/minimap.py +++ b/arcade/examples/minimap.py @@ -110,7 +110,7 @@ def setup(self): def update_minimap(self): proj = 0, MAP_WIDTH, 0, MAP_HEIGHT with self.minimap_sprite_list.atlas.render_into(self.minimap_texture, projection=proj) as fbo: - fbo.clear(color=MINIMAP_BACKGROUND_COLOR) + fbo.clear(MINIMAP_BACKGROUND_COLOR) self.wall_list.draw() self.player_sprite.draw() diff --git a/arcade/examples/minimap_camera.py b/arcade/examples/minimap_camera.py index 806034f24..3d9df371a 100644 --- a/arcade/examples/minimap_camera.py +++ b/arcade/examples/minimap_camera.py @@ -126,7 +126,7 @@ def on_draw(self): # Draw new minimap using the camera self.camera_minimap.use() - self.clear(color=MINIMAP_BACKGROUND_COLOR) + self.clear(MINIMAP_BACKGROUND_COLOR) self.wall_list.draw() self.player_list.draw() diff --git a/arcade/examples/particle_fireworks.py b/arcade/examples/particle_fireworks.py index 361e83377..cb5f98029 100644 --- a/arcade/examples/particle_fireworks.py +++ b/arcade/examples/particle_fireworks.py @@ -362,10 +362,6 @@ def rocket_smoke_mutator(particle: LifetimeParticle): particle.scale = lerp(0.5, 3.0, particle.lifetime_elapsed / particle.lifetime_original) -def main(): - app = FireworksApp() - app.run() - - if __name__ == "__main__": - main() + app = FireworksApp() + arcade.run() diff --git a/arcade/examples/particle_systems.py b/arcade/examples/particle_systems.py index d1ac99efe..36590ecc7 100644 --- a/arcade/examples/particle_systems.py +++ b/arcade/examples/particle_systems.py @@ -766,10 +766,6 @@ def on_key_press(self, key, modifiers): arcade.close_window() -def main(): - game = MyGame() - game.run() - - if __name__ == "__main__": - main() + game = MyGame() + arcade.run() diff --git a/arcade/examples/performance_statistics.py b/arcade/examples/performance_statistics.py index 7b5137be6..3e02cbe00 100644 --- a/arcade/examples/performance_statistics.py +++ b/arcade/examples/performance_statistics.py @@ -40,6 +40,7 @@ COIN_COUNT = 1500 + # Turn on tracking for the number of event handler # calls and the average execution time of each type. arcade.enable_timings() diff --git a/arcade/examples/perspective.py b/arcade/examples/perspective.py index 32a04f581..ee27ba49d 100644 --- a/arcade/examples/perspective.py +++ b/arcade/examples/perspective.py @@ -148,9 +148,4 @@ def on_resize(self, width: int, height: int): self.program["projection"] = Mat4.perspective_projection(self.aspect_ratio, 0.1, 100, fov=75) -def main(): - Perspective().run() - - -if __name__ == "__main__": - main() +Perspective().run() diff --git a/arcade/examples/pymunk_joint_builder.py b/arcade/examples/pymunk_joint_builder.py index 1548a75a6..1c45230e1 100644 --- a/arcade/examples/pymunk_joint_builder.py +++ b/arcade/examples/pymunk_joint_builder.py @@ -314,10 +314,6 @@ def on_update(self, delta_time): self.processing_time = timeit.default_timer() - start_time -def main(): - window = MyApplication(SCREEN_WIDTH, SCREEN_HEIGHT, SCREEN_TITLE) - window.run() +window = MyApplication(SCREEN_WIDTH, SCREEN_HEIGHT, SCREEN_TITLE) - -if __name__ == "__main__": - main() +arcade.run() diff --git a/arcade/examples/sections_demo_1.py b/arcade/examples/sections_demo_1.py index 17f848e31..39b9e7adf 100644 --- a/arcade/examples/sections_demo_1.py +++ b/arcade/examples/sections_demo_1.py @@ -127,7 +127,7 @@ def __init__(self): def on_draw(self): # clear the screen - self.clear(color=arcade.color.BEAU_BLUE) + self.clear(arcade.color.BEAU_BLUE) # draw a line separating each Section arcade.draw_line(self.window.width / 2, 0, self.window.width / 2, diff --git a/arcade/examples/sections_demo_2.py b/arcade/examples/sections_demo_2.py index 05fef3090..c2962f353 100644 --- a/arcade/examples/sections_demo_2.py +++ b/arcade/examples/sections_demo_2.py @@ -20,7 +20,8 @@ """ import random -import arcade +from arcade import Window, Section, View, SpriteList, SpriteSolidColor, \ + SpriteCircle, draw_text, draw_line from arcade.color import BLACK, BLUE, RED, BEAU_BLUE, GRAY from arcade.key import W, S, UP, DOWN @@ -28,7 +29,7 @@ PLAYER_PADDLE_SPEED = 10 -class Player(arcade.Section): +class Player(Section): """ A Section representing the space in the screen where the player paddle can move @@ -43,7 +44,7 @@ def __init__(self, left: int, bottom: int, width: int, height: int, self.key_down: int = key_down # the player paddle - self.paddle: arcade.SpriteSolidColor = arcade.SpriteSolidColor(30, 100, color=BLACK) + self.paddle: SpriteSolidColor = SpriteSolidColor(30, 100, color=BLACK) # player score self.score: int = 0 @@ -64,14 +65,10 @@ def on_draw(self): else: keys = 'UP and DOWN' start_x = self.left - 290 - arcade.draw_text( - f'Player {self.name} (move paddle with: {keys})', - start_x, self.top - 20, BLUE, 9, - ) - arcade.draw_text( - f'Score: {self.score}', self.left + 20, - self.bottom + 20, BLUE, - ) + draw_text(f'Player {self.name} (move paddle with: {keys})', + start_x, self.top - 20, BLUE, 9) + draw_text(f'Score: {self.score}', self.left + 20, + self.bottom + 20, BLUE) # draw the paddle self.paddle.draw() @@ -88,14 +85,14 @@ def on_key_release(self, _symbol: int, _modifiers: int): self.paddle.stop() -class Pong(arcade.View): +class Pong(View): def __init__(self): super().__init__() # a sprite list that will hold each player paddle to # check for collisions - self.paddles: arcade.SpriteList = arcade.SpriteList() + self.paddles: SpriteList = SpriteList() # we store each Section self.left_player: Player = Player( @@ -114,7 +111,7 @@ def __init__(self): self.paddles.append(self.right_player.paddle) # create the ball - self.ball: arcade.SpriteCircle = arcade.SpriteCircle(20, RED) + self.ball: SpriteCircle = SpriteCircle(20, RED) def setup(self): # set up a new game @@ -164,19 +161,19 @@ def end_game(self, winner: Player): self.setup() # prepare a new game def on_draw(self): - self.clear(color=BEAU_BLUE) # clear the screen + self.clear(BEAU_BLUE) # clear the screen self.ball.draw() # draw the ball half_window_x = self.window.width / 2 # middle x # draw a line diving the screen in half - arcade.draw_line(half_window_x, 0, half_window_x, self.window.height, GRAY, 2) + draw_line(half_window_x, 0, half_window_x, self.window.height, GRAY, 2) def main(): # create the window - window = arcade.Window(title='Two player simple Pong with Sections!') + window = Window(title='Two player simple Pong with Sections!') # create the custom View game = Pong() diff --git a/arcade/examples/sprite_animated_keyframes.py b/arcade/examples/sprite_animated_keyframes.py index 13c6131a9..7b8bf901a 100644 --- a/arcade/examples/sprite_animated_keyframes.py +++ b/arcade/examples/sprite_animated_keyframes.py @@ -39,10 +39,5 @@ def on_update(self, delta_time: float): self.sprite.update_animation(delta_time) -def main(): - Animated().run() - - if __name__ == "__main__": - main() - + Animated().run() diff --git a/arcade/examples/sprite_explosion_bitmapped.py b/arcade/examples/sprite_explosion_bitmapped.py index 1543fec71..844953d14 100644 --- a/arcade/examples/sprite_explosion_bitmapped.py +++ b/arcade/examples/sprite_explosion_bitmapped.py @@ -86,7 +86,7 @@ def __init__(self): self.gun_sound = arcade.sound.load_sound(":resources:sounds/laser2.wav") self.hit_sound = arcade.sound.load_sound(":resources:sounds/explosion2.wav") - self.background_color = arcade.color.AMAZON + arcade.background_color = arcade.color.AMAZON def setup(self): diff --git a/arcade/examples/sprite_minimal.py b/arcade/examples/sprite_minimal.py index e611e8fec..048a45955 100644 --- a/arcade/examples/sprite_minimal.py +++ b/arcade/examples/sprite_minimal.py @@ -30,10 +30,6 @@ def on_draw(self): self.sprites.draw() -def main(): +if __name__ == "__main__": game = WhiteSpriteCircleExample() game.run() - - -if __name__ == "__main__": - main() diff --git a/arcade/examples/sprite_move_keyboard_accel.py b/arcade/examples/sprite_move_keyboard_accel.py index 5773d55ee..cd0175d01 100644 --- a/arcade/examples/sprite_move_keyboard_accel.py +++ b/arcade/examples/sprite_move_keyboard_accel.py @@ -84,7 +84,7 @@ def __init__(self, width, height, title): self.down_pressed = False # Set the background color - self.background_color = arcade.color.AMAZON + arcade.background_color = arcade.color.AMAZON def setup(self): """ Set up the game and initialize the variables. """ diff --git a/arcade/examples/tetris.py b/arcade/examples/tetris.py index fde178f70..36158de9e 100644 --- a/arcade/examples/tetris.py +++ b/arcade/examples/tetris.py @@ -201,7 +201,7 @@ def rotate_stone(self): self.stone = new_stone def on_update(self, dt): - """ Update, drop stone if warranted """ + """ Update, drop stone if warrented """ self.frame_count += 1 if self.frame_count % 10 == 0: self.drop() diff --git a/arcade/examples/transform_feedback.py b/arcade/examples/transform_feedback.py index b4ae25eeb..b4ce242c0 100644 --- a/arcade/examples/transform_feedback.py +++ b/arcade/examples/transform_feedback.py @@ -147,11 +147,7 @@ def on_draw(self): self.buffer_1, self.buffer_2 = self.buffer_2, self.buffer_1 -def main(): +if __name__ == "__main__": window = MyGame(SCREEN_WIDTH, SCREEN_HEIGHT, SCREEN_TITLE) window.center_window() arcade.run() - - -if __name__ == "__main__": - main() diff --git a/arcade/experimental/atlas_render_into.py b/arcade/experimental/atlas_render_into.py index d2b1e8482..c43919f9d 100644 --- a/arcade/experimental/atlas_render_into.py +++ b/arcade/experimental/atlas_render_into.py @@ -32,12 +32,12 @@ def on_draw(self): def render_into_sprite_texture(self): # Render shape into texture atlas in the first sprite texture's space with self.spritelist.atlas.render_into(self.texture_1) as fbo: - fbo.clear(color=(255, 0, 0, 255)) + fbo.clear((255, 0, 0, 255)) arcade.draw_rectangle_filled(128, 128, 160, 160, arcade.color.WHITE, self.elapsed_time * 100) # Render a shape into the second texture in the atlas with self.spritelist.atlas.render_into(self.texture_2) as fbo: - fbo.clear(color=(0, 255, 0, 255)) + fbo.clear((0, 255, 0, 255)) arcade.draw_circle_filled( 128, 128, diff --git a/arcade/experimental/clock/clock_window.py b/arcade/experimental/clock/clock_window.py index 3b2dd061c..667ceff0e 100644 --- a/arcade/experimental/clock/clock_window.py +++ b/arcade/experimental/clock/clock_window.py @@ -28,7 +28,7 @@ from arcade import NoOpenGLException from arcade.color import TRANSPARENT_BLACK from arcade.context import ArcadeContext -from arcade.types import Color, RGBOrA255, RGBOrANormalized +from arcade.types import Color, RGBA255, RGBA255OrNormalized from arcade import SectionManager from arcade.utils import is_raspberry_pi @@ -263,8 +263,8 @@ def ctx(self) -> ArcadeContext: def clear( self, - color: Optional[RGBOrA255] = None, - color_normalized: Optional[RGBOrANormalized] = None, + color: Optional[RGBA255OrNormalized] = None, + normalized: bool = False, viewport: Optional[Tuple[int, int, int, int]] = None, ): """Clears the window with the configured background color @@ -277,14 +277,11 @@ def clear( 2. A 4-length RGBA :py:class:`tuple` of byte values (0 to 255) 3. A 4-length RGBA :py:class:`tuple` of normalized floats (0.0 to 1.0) - :param color_normalized: (Optional) override the current background color - with a 4-length RGBA :py:class:`tuple` of normalized floats (0.0 to 1.0) - + :param normalized: If the color format is normalized (0.0 -> 1.0) or byte values :param Tuple[int, int, int, int] viewport: The viewport range to clear """ - if color is None and color_normalized is None: - color = self.background_color - self.ctx.screen.clear(color=color, color_normalized=color_normalized, viewport=viewport) + color = color if color is not None else self.background_color + self.ctx.screen.clear(color, normalized=normalized, viewport=viewport) @property def background_color(self) -> Color: @@ -315,7 +312,7 @@ def background_color(self) -> Color: return self._background_color @background_color.setter - def background_color(self, value: RGBOrA255): + def background_color(self, value: RGBA255): self._background_color = Color.from_iterable(value) @property @@ -1005,26 +1002,24 @@ def add_section(self, section, at_index: Optional[int] = None, at_draw_order: Op def clear( self, - color: Optional[RGBOrA255] = None, - color_normalized: Optional[RGBOrANormalized] = None, + color: Optional[RGBA255OrNormalized] = None, + normalized: bool = False, viewport: Optional[Tuple[int, int, int, int]] = None, ): - """Clears the window with the configured background color + """Clears the View's Window with the configured background color set through :py:attr:`arcade.Window.background_color`. :param color: (Optional) override the current background color with one of the following: 1. A :py:class:`~arcade.types.Color` instance - 2. A 3 or 4-length RGB/RGBA :py:class:`tuple` of byte values (0 to 255) - - :param color_normalized: (Optional) override the current background color - using normalized values (0.0 to 1.0). For example, (1.0, 0.0, 0.0, 1.0) - making the window contents red. + 2. A 4-length RGBA :py:class:`tuple` of byte values (0 to 255) + 3. A 4-length RGBA :py:class:`tuple` of normalized floats (0.0 to 1.0) + :param normalized: If the color format is normalized (0.0 -> 1.0) or byte values :param Tuple[int, int, int, int] viewport: The viewport range to clear """ - self.window.ctx.screen.clear(color=color, color_normalized=color_normalized, viewport=viewport) + self.window.clear(color, normalized, viewport) def on_update(self, delta_time: float): """To be overridden""" diff --git a/arcade/experimental/depth_of_field.py b/arcade/experimental/depth_of_field.py deleted file mode 100644 index a1c5d96f7..000000000 --- a/arcade/experimental/depth_of_field.py +++ /dev/null @@ -1,239 +0,0 @@ -"""An experimental depth-of-field example. - -It uses the depth attribute of along with blurring and shaders to -roughly approximate depth-based blur effects. The focus bounces -back forth automatically between a maximum and minimum depth value -based on time. Adjust the arguments to the App class at the bottom -of the file to change the speed. - -This example works by doing the following for each frame: - -1. Render a depth value for pixel into a buffer -2. Render a gaussian blurred version of the scene -3. For each pixel, use the current depth value to lerp between the - blurred and unblurred versions of the scene. - -This is more expensive than rendering the scene directly, but it's -both easier and more performant than more accurate blur approaches. - -If Python and Arcade are installed, this example can be run from the command line with: -python -m arcade.experimental.examples.array_backed_grid -""" -from typing import Tuple, Optional, cast -from textwrap import dedent -from math import cos, pi -from random import uniform, randint -from contextlib import contextmanager - -from pyglet.graphics import Batch - -from arcade import get_window, Window, SpriteSolidColor, SpriteList, Text -from arcade.color import RED -from arcade.types import Color, RGBA255 - -from arcade.gl import geometry, NEAREST, Program, Texture2D -from arcade.experimental.postprocessing import GaussianBlur - - -class DepthOfField: - """A depth-of-field effect we can use as a render context manager. - - :param size: The size of the buffers. - :param clear_color: The color which will be used as the background. - """ - - def __init__( - self, - size: Optional[Tuple[int, int]] = None, - clear_color: RGBA255 = (155, 155, 155, 255) - ): - self._geo = geometry.quad_2d_fs() - self._win: Window = get_window() - - size = cast(Tuple[int, int], size or self._win.size) - self._clear_color: Color = Color.from_iterable(clear_color) - - self.stale = True - - # Set up our depth buffer to hold per-pixel depth - self._render_target = self._win.ctx.framebuffer( - color_attachments=[ - self._win.ctx.texture( - size, - components=4, - filter=(NEAREST, NEAREST), - wrap_x=self._win.ctx.REPEAT, - wrap_y=self._win.ctx.REPEAT - ), - ], - depth_attachment=self._win.ctx.depth_texture( - size - ) - ) - - # Set up everything we need to perform blur and store results. - # This includes the blur effect, a framebuffer, and an instance - # variable to store the returned texture holding blur results. - self._blur_process = GaussianBlur( - size, - kernel_size=10, - sigma=2.0, - multiplier=2.0, - step=4 - ) - self._blur_target = self._win.ctx.framebuffer( - color_attachments=[ - self._win.ctx.texture( - size, - components=4, - filter=(NEAREST, NEAREST), - wrap_x=self._win.ctx.REPEAT, - wrap_y=self._win.ctx.REPEAT - ) - ] - ) - self._blurred: Optional[Texture2D] = None - - # To keep this example in one file, we use strings for our - # our shaders. You may want to use pathlib.Path.read_text in - # your own code instead. - self._render_program = self._win.ctx.program( - vertex_shader=dedent( - """#version 330 - - in vec2 in_vert; - in vec2 in_uv; - - out vec2 out_uv; - - void main(){ - gl_Position = vec4(in_vert, 0.0, 1.0); - out_uv = in_uv; - }"""), - fragment_shader=dedent( - """#version 330 - - uniform sampler2D texture_0; - uniform sampler2D texture_1; - uniform sampler2D depth_0; - - uniform float focus_depth; - - in vec2 out_uv; - - out vec4 frag_colour; - - void main() { - float depth_val = texture(depth_0, out_uv).x; - float depth_adjusted = min(1.0, 2.0 * abs(depth_val - focus_depth)); - vec4 crisp_tex = texture(texture_0, out_uv); - vec3 blur_tex = texture(texture_1, out_uv).rgb; - frag_colour = mix(crisp_tex, vec4(blur_tex, crisp_tex.a), depth_adjusted); - //if (depth_adjusted < 0.1){frag_colour = vec4(1.0, 0.0, 0.0, 1.0);} - }""") - ) - - # Set the buffers the shader program will use - self._render_program['texture_0'] = 0 - self._render_program['texture_1'] = 1 - self._render_program['depth_0'] = 2 - - @property - def render_program(self) -> Program: - """The compiled shader for this effect.""" - return self._render_program - - @contextmanager - def draw_into(self): - self.stale = True - previous_fbo = self._win.ctx.active_framebuffer - try: - self._win.ctx.enable(self._win.ctx.DEPTH_TEST) - self._render_target.clear(self._clear_color) - self._render_target.use() - yield self._render_target - finally: - self._win.ctx.disable(self._win.ctx.DEPTH_TEST) - previous_fbo.use() - - def process(self): - self._blurred = self._blur_process.render(self._render_target.color_attachments[0]) - self._win.use() - - self.stale = False - - def render(self): - if self.stale: - self.process() - - self._render_target.color_attachments[0].use(0) - self._blurred.use(1) - self._render_target.depth_attachment.use(2) - self._geo.render(self._render_program) - - -class App(Window): - """Window subclass to hold sprites and rendering helpers. - - :param text_color: The color of the focus indicator. - :param focus_range: The range the focus value will oscillate between. - :param focus_change_speed: How fast the focus bounces back and forth - between the ``-focus_range`` and ``focus_range``. - """ - def __init__( - self, - text_color: RGBA255 = RED, - focus_range: float = 16.0, - focus_change_speed: float = 0.1 - ): - super().__init__() - self.time: float = 0.0 - self.sprites: SpriteList = SpriteList() - self._batch = Batch() - self.focus_range: float = focus_range - self.focus_change_speed: float = focus_change_speed - self.indicator_label = Text( - f"Focus depth: {0:.3f} / {focus_range}", - self.width / 2, self.height / 2, - text_color, - align="center", - anchor_x="center", - batch=self._batch - ) - - # Randomize sprite depth, size, and angle, but set color from depth. - for _ in range(100): - depth = uniform(-100, 100) - color = Color.from_gray(int(255 * (depth + 100) / 200)) - s = SpriteSolidColor( - randint(100, 200), randint(100, 200), - uniform(20, self.width - 20), uniform(20, self.height - 20), - color, - uniform(0, 360) - ) - s.depth = depth - self.sprites.append(s) - - self.dof = DepthOfField() - - def on_update(self, delta_time: float): - self.time += delta_time - raw_focus = self.focus_range * (cos(pi * self.focus_change_speed * self.time) * 0.5 + 0.5) - self.dof.render_program["focus_depth"] = raw_focus / self.focus_range - self.indicator_label.value = f"Focus depth: {raw_focus:.3f} / {self.focus_range}" - - def on_draw(self): - self.clear() - - # Render the depth-of-field layer's frame buffer - with self.dof.draw_into(): - self.sprites.draw(pixelated=True) - - # Draw the blurred frame buffer and then the focus display - self.use() - self.dof.render() - self._batch.draw() - - -if __name__ == '__main__': - App().run() diff --git a/arcade/experimental/lights.py b/arcade/experimental/lights.py index 97305ee85..cbfdbfa47 100644 --- a/arcade/experimental/lights.py +++ b/arcade/experimental/lights.py @@ -154,7 +154,7 @@ def __getitem__(self, i) -> Light: def __enter__(self): self._prev_target = self.ctx.active_framebuffer self._fbo.use() - self._fbo.clear(color=self._background_color) + self._fbo.clear(self._background_color) return self def __exit__(self, exc_type, exc_val, exc_tb): diff --git a/arcade/experimental/sprite_collect_coins_minimap.py b/arcade/experimental/sprite_collect_coins_minimap.py index c4e00d50e..e670dd634 100644 --- a/arcade/experimental/sprite_collect_coins_minimap.py +++ b/arcade/experimental/sprite_collect_coins_minimap.py @@ -106,7 +106,7 @@ def on_draw(self): self.clear() self.offscreen.use() - self.offscreen.clear(color=arcade.color.AMAZON) + self.offscreen.clear(arcade.color.AMAZON) arcade.draw_rectangle_outline(SCREEN_WIDTH / 2, SCREEN_HEIGHT / 2, diff --git a/arcade/experimental/texture_render_target.py b/arcade/experimental/texture_render_target.py index ddee67f49..4a26d7228 100644 --- a/arcade/experimental/texture_render_target.py +++ b/arcade/experimental/texture_render_target.py @@ -40,7 +40,7 @@ def texture(self) -> Texture2D: def clear(self): """Clear the texture with the configured background color""" - self._fbo.clear(color=self._background_color) + self._fbo.clear(self._background_color) def set_background_color(self, color: RGBA255): """Set the background color for the light layer""" diff --git a/arcade/gl/buffer.py b/arcade/gl/buffer.py index 6e666197a..de7c37d70 100644 --- a/arcade/gl/buffer.py +++ b/arcade/gl/buffer.py @@ -62,7 +62,7 @@ def __init__( gl.glBindBuffer(gl.GL_ARRAY_BUFFER, self._glo) # print(f"glBufferData(gl.GL_ARRAY_BUFFER, {self._size}, data, {self._usage})") - if data is not None and len(data) > 0: # type: ignore + if data is not None and len(data) > 0: self._size, data = data_to_ctypes(data) gl.glBufferData(gl.GL_ARRAY_BUFFER, self._size, data, self._usage) elif reserve > 0: diff --git a/arcade/gl/framebuffer.py b/arcade/gl/framebuffer.py index dffdb30c2..649927dc4 100644 --- a/arcade/gl/framebuffer.py +++ b/arcade/gl/framebuffer.py @@ -2,7 +2,7 @@ from ctypes import c_int, string_at from contextlib import contextmanager -from typing import Generator, Optional, Tuple, List, TYPE_CHECKING +from typing import Optional, Tuple, List, TYPE_CHECKING, Union import weakref @@ -305,7 +305,7 @@ def __exit__(self, exc_type, exc_val, exc_tb): self._prev_fbo.use() @contextmanager - def activate(self) -> Generator[Framebuffer, None, None]: + def activate(self): """Context manager for binding the framebuffer. Unlike the default context manager in this class @@ -348,10 +348,10 @@ def _use(self, *, force: bool = False): def clear( self, + color: Union[RGBOrA255, RGBOrANormalized] = (0.0, 0.0, 0.0, 0.0), *, - color: Optional[RGBOrA255] = None, - color_normalized: Optional[RGBOrANormalized] = None, depth: float = 1.0, + normalized: bool = False, viewport: Optional[Tuple[int, int, int, int]] = None, ): """ @@ -361,13 +361,12 @@ def clear( fb.clear(color=arcade.color.WHITE) # Clear framebuffer using the color red in normalized form - fbo.clear(color_normalized=(1.0, 0.0, 0.0, 1.0)) + fbo.clear(color=(1.0, 0.0, 0.0, 1.0), normalized=True) If the background color is an ``RGB`` value instead of ``RGBA``` we assume alpha value 255. - :param color: A 3 or 4 component tuple containing the color (prioritized over color_normalized) - :param color_normalized: A 3 or 4 component tuple containing the color in normalized form + :param color: A 3 or 4 component tuple containing the color :param depth: Value to clear the depth buffer (unused) :param normalized: If the color values are normalized or not :param Tuple[int, int, int, int] viewport: The viewport range to clear @@ -380,26 +379,24 @@ def clear( else: self.scissor = None - clear_color = 0.0, 0.0, 0.0, 0.0 - if color is not None: + if normalized: + # If the colors are already normalized we can pass them right in if len(color) == 3: - clear_color = color[0] / 255, color[1] / 255, color[2] / 255, 1.0 - elif len(color) == 4: - clear_color = color[0] / 255, color[1] / 255, color[2] / 255, color[3] / 255 + gl.glClearColor(*color, 1.0) else: - raise ValueError("Color should be a 3 or 4 component tuple") - elif color_normalized is not None: - if len(color_normalized) == 3: - clear_color = color_normalized[0], color_normalized[1], color_normalized[2], 1.0 - elif len(color_normalized) == 4: - clear_color = color_normalized + gl.glClearColor(*color) + else: + # OpenGL wants normalized colors (0.0 -> 1.0) + if len(color) == 3: + gl.glClearColor(color[0] / 255, color[1] / 255, color[2] / 255, 1.0) else: - raise ValueError("Color should be a 3 or 4 component tuple") - - gl.glClearColor(*clear_color) + # mypy does not understand that color[3] is guaranteed to work in this codepath, pyright does. + # We can remove this type: ignore if we switch to pyright. + gl.glClearColor( + color[0] / 255, color[1] / 255, color[2] / 255, color[3] / 255 # type: ignore + ) if self.depth_attachment: - gl.glClearDepth(depth) gl.glClear(gl.GL_COLOR_BUFFER_BIT | gl.GL_DEPTH_BUFFER_BIT) else: gl.glClear(gl.GL_COLOR_BUFFER_BIT) @@ -428,23 +425,15 @@ def read( raise ValueError(f"Invalid dtype '{dtype}'") with self.activate(): - # Configure attachment to read from. Does not work on default framebuffer. - if not self.is_default: - gl.glReadBuffer(gl.GL_COLOR_ATTACHMENT0 + attachment) - - gl.glPixelStorei(gl.GL_PACK_ALIGNMENT, 1) - gl.glPixelStorei(gl.GL_UNPACK_ALIGNMENT, 1) - + # Configure attachment to read from + gl.glReadBuffer(gl.GL_COLOR_ATTACHMENT0 + attachment) if viewport: x, y, width, height = viewport else: - x, y, width, height = 0, 0, *self.size - + x, y, width, height = 0, 0, self._width, self._height data = (gl.GLubyte * (components * component_size * width * height))(0) gl.glReadPixels(x, y, width, height, base_format, pixel_type, data) - - if not self.is_default: - gl.glReadBuffer(gl.GL_COLOR_ATTACHMENT0) # Reset to default + gl.glReadBuffer(gl.GL_COLOR_ATTACHMENT0) # Reset to default return string_at(data, len(data)) @@ -579,37 +568,6 @@ def __init__(self, ctx: "Context"): # HACK: Signal the default framebuffer having depth buffer self._depth_attachment = True # type: ignore - @property - def size(self) -> Tuple[int, int]: - """ - Size as a ``(w, h)`` tuple - - :type: tuple (int, int) - """ - return self._ctx.window.get_framebuffer_size() - - @property - def width(self) -> int: - """ - The width of the framebuffer in pixels - - :type: int - """ - return self.size[0] - - @property - def height(self) -> int: - """ - The height of the framebuffer in pixels - - :type: int - """ - return self.size[1] - - def _get_framebuffer_size(self) -> Tuple[int, int]: - """Get the framebuffer size of the window""" - return self._ctx.window.get_framebuffer_size() - def _get_viewport(self) -> Tuple[int, int, int, int]: """ Get or set the framebuffer's viewport. diff --git a/arcade/gui/events.py b/arcade/gui/events.py index d22cf5e4d..00b885195 100644 --- a/arcade/gui/events.py +++ b/arcade/gui/events.py @@ -21,8 +21,8 @@ class UIMouseEvent(UIEvent): Covers all mouse event """ - x: int - y: int + x: float + y: float @property def pos(self): @@ -32,8 +32,8 @@ def pos(self): @dataclass class UIMouseMovementEvent(UIMouseEvent): """Triggered when the mouse is moved.""" - dx: int - dy: int + dx: float + dy: float @dataclass @@ -46,8 +46,8 @@ class UIMousePressEvent(UIMouseEvent): @dataclass class UIMouseDragEvent(UIMouseEvent): """Triggered when the mouse moves while one of its buttons being pressed.""" - dx: int - dy: int + dx: float + dy: float buttons: int modifiers: int diff --git a/arcade/gui/experimental/scroll_area.py b/arcade/gui/experimental/scroll_area.py index 2f1245651..77e539ad6 100644 --- a/arcade/gui/experimental/scroll_area.py +++ b/arcade/gui/experimental/scroll_area.py @@ -113,7 +113,7 @@ def on_event(self, event: UIEvent) -> Optional[bool]: child_event = event if isinstance(event, UIMouseEvent): child_event = type(event)(**event.__dict__) # type: ignore - child_event.x = int(event.x - self.x + self.scroll_x) - child_event.y = int(event.y - self.y + self.scroll_y) + child_event.x = event.x - self.x + self.scroll_x + child_event.y = event.y - self.y + self.scroll_y return super().on_event(child_event) diff --git a/arcade/gui/property.py b/arcade/gui/property.py index cec628b6b..cd1faff80 100644 --- a/arcade/gui/property.py +++ b/arcade/gui/property.py @@ -18,41 +18,21 @@ class _Obs(Generic[P]): def __init__(self, value: P): self.value = value # This will keep any added listener even if it is not referenced anymore and would be garbage collected - self.listeners: Set[Callable[[Any, P], Any]] = set() + self.listeners: Set[Callable[[], Any]] = set() class Property(Generic[P]): """ An observable property which triggers observers when changed. -.. code-block:: python - - def log_change(instance, value): - print("Something changed") - - class MyObject: - name = Property() - - my_obj = MyObject() - bind(my_obj, "name", log_change) - unbind(my_obj, "name", log_change) - - my_obj.name = "Hans" - # > Something changed - :param default: Default value which is returned, if no value set before :param default_factory: A callable which returns the default value. Will be called with the property and the instance """ - __slots__ = ("name", "default_factory", "obs") name: str - def __init__( - self, - default: Optional[P] = None, - default_factory: Optional[Callable[[Any, Any], P]] = None, - ): + def __init__(self, default: Optional[P] = None, default_factory: Optional[Callable[[Any, Any], P]] = None): if default_factory is None: default_factory = lambda prop, instance: cast(P, default) @@ -80,11 +60,7 @@ def dispatch(self, instance, value): obs = self._get_obs(instance) for listener in obs.listeners: try: - try: - listener(instance, value) - except TypeError: - # If the listener does not accept arguments, we call it without it - listener() # type: ignore + listener() except Exception: print( f"Change listener for {instance}.{self.name} = {value} raised an exception!", @@ -119,8 +95,8 @@ def bind(instance, property: str, callback): Binds a function to the change event of the property. A reference to the function will be kept, so that it will be still invoked, even if it would normally have been garbage collected. - def log_change(instance, value): - print(f"Value of {instance} changed to {value}") + def log_change(): + print("Something changed") class MyObject: name = Property() @@ -129,7 +105,7 @@ class MyObject: bind(my_obj, "name", log_change) my_obj.name = "Hans" - # > Value of <__main__.MyObject ...> changed to Hans + # > Something changed :param instance: Instance owning the property :param property: Name of the property @@ -146,7 +122,7 @@ def unbind(instance, property: str, callback): """ Unbinds a function from the change event of the property. - def log_change(instance, value): + def log_change(): print("Something changed") class MyObject: @@ -174,7 +150,10 @@ class MyObject: class _ObservableDict(dict): """Internal class to observe changes inside a native python dict.""" - __slots__ = ("prop", "obj") + __slots__ = ( + "prop", + "obj" + ) def __init__(self, prop: Property, instance, *largs): self.prop: Property = prop @@ -232,7 +211,10 @@ def set(self, instance, value: dict): class _ObservableList(list): """Internal class to observe changes inside a native python list.""" - __slots__ = ("prop", "obj") + __slots__ = ( + "prop", + "obj" + ) def __init__(self, prop: Property, instance, *largs): self.prop: Property = prop diff --git a/arcade/gui/ui_manager.py b/arcade/gui/ui_manager.py index 0cbde5247..fca3d0b98 100644 --- a/arcade/gui/ui_manager.py +++ b/arcade/gui/ui_manager.py @@ -84,6 +84,7 @@ def __init__(self, window: Optional[arcade.Window] = None): self._surfaces: Dict[int, Surface] = {} self.children: Dict[int, List[UIWidget]] = defaultdict(list) self._requires_render = True + #: Camera used when drawing the UI self.register_event_type("on_event") def add(self, widget: W, *, index=None, layer=0) -> W: @@ -351,22 +352,22 @@ def on_event(self, event) -> Union[bool, None]: def dispatch_ui_event(self, event): return self.dispatch_event("on_event", event) - def on_mouse_motion(self, x: int, y: int, dx: int, dy: int): + def on_mouse_motion(self, x: float, y: float, dx: float, dy: float): x, y = self.adjust_mouse_coordinates(x, y) return self.dispatch_ui_event(UIMouseMovementEvent(self, x, y, dx, dy)) # type: ignore - def on_mouse_press(self, x: int, y: int, button: int, modifiers: int): - x, y = self.adjust_mouse_coordinates(x, y) + def on_mouse_press(self, x: float, y: float, button: int, modifiers: int): + x, y = self.adjust_mouse_coordinates(x, y)[:2] return self.dispatch_ui_event(UIMousePressEvent(self, x, y, button, modifiers)) # type: ignore def on_mouse_drag( - self, x: int, y: int, dx: int, dy: int, buttons: int, modifiers: int + self, x: float, y: float, dx: float, dy: float, buttons: int, modifiers: int ): x, y = self.adjust_mouse_coordinates(x, y) return self.dispatch_ui_event(UIMouseDragEvent(self, x, y, dx, dy, buttons, modifiers)) # type: ignore - def on_mouse_release(self, x: int, y: int, button: int, modifiers: int): - x, y = self.adjust_mouse_coordinates(x, y) + def on_mouse_release(self, x: float, y: float, button: int, modifiers: int): + x, y = self.adjust_mouse_coordinates(x, y)[:2] return self.dispatch_ui_event(UIMouseReleaseEvent(self, x, y, button, modifiers)) # type: ignore def on_mouse_scroll(self, x, y, scroll_x, scroll_y): diff --git a/arcade/gui/widgets/__init__.py b/arcade/gui/widgets/__init__.py index a7108276c..55156cd81 100644 --- a/arcade/gui/widgets/__init__.py +++ b/arcade/gui/widgets/__init__.py @@ -1,5 +1,6 @@ from __future__ import annotations +import builtins from abc import ABC from random import randint from typing import ( @@ -550,32 +551,32 @@ def with_border(self, *, width=2, color=(0, 0, 0)) -> Self: def with_padding( self, *, - top: Optional[int] = None, - right: Optional[int] = None, - bottom: Optional[int] = None, - left: Optional[int] = None, - all: Optional[int] = None, + top: Union["builtins.ellipsis", int] = ..., + right: Union["builtins.ellipsis", int] = ..., + bottom: Union["builtins.ellipsis", int] = ..., + left: Union["builtins.ellipsis", int] = ..., + all: Union["builtins.ellipsis", int] = ..., ) -> "UIWidget": """ Changes the padding to the given values if set. Returns itself :return: self """ - if all is not None: + if all is not ...: self.padding = all - if top is not None: + if top is not ...: self._padding_top = top - if right is not None: + if right is not ...: self._padding_right = right - if bottom is not None: + if bottom is not ...: self._padding_bottom = bottom - if left is not None: + if left is not ...: self._padding_left = left return self def with_background( self, *, - color: Union[None, Color] = ..., # type: ignore + color: Union["builtins.ellipsis", Color] = ..., texture: Union[None, Texture, NinePatchTexture] = ..., # type: ignore ) -> "UIWidget": """ diff --git a/arcade/gui/widgets/text.py b/arcade/gui/widgets/text.py index 0e904c004..efd5650dd 100644 --- a/arcade/gui/widgets/text.py +++ b/arcade/gui/widgets/text.py @@ -354,7 +354,7 @@ def __init__( self.doc, width - self.LAYOUT_OFFSET, height, multiline=multiline ) self.layout.x += self.LAYOUT_OFFSET - self.caret = Caret(self.layout, color=Color.from_iterable(caret_color)) + self.caret = Caret(self.layout, color=caret_color) self.caret.visible = False self._blink_state = self._get_caret_blink_state() @@ -383,8 +383,7 @@ def on_event(self, event: UIEvent) -> Optional[bool]: # If active check to deactivate if self._active and isinstance(event, UIMousePressEvent): if self.rect.collide_with_point(event.x, event.y): - x = int(event.x - self.x - self.LAYOUT_OFFSET) - y = int(event.y - self.y) + x, y = event.x - self.x - self.LAYOUT_OFFSET, event.y - self.y self.caret.on_mouse_press(x, y, event.button, event.modifiers) else: self._active = False @@ -408,8 +407,7 @@ def on_event(self, event: UIEvent) -> Optional[bool]: if isinstance(event, UIMouseEvent) and self.rect.collide_with_point( event.x, event.y ): - x = int(event.x - self.x - self.LAYOUT_OFFSET) - y = int(event.y - self.y) + x, y = event.x - self.x - self.LAYOUT_OFFSET, event.y - self.y if isinstance(event, UIMouseDragEvent): self.caret.on_mouse_drag( x, y, event.dx, event.dy, event.buttons, event.modifiers diff --git a/arcade/perf_graph.py b/arcade/perf_graph.py index 44df94352..cf3952e75 100644 --- a/arcade/perf_graph.py +++ b/arcade/perf_graph.py @@ -292,7 +292,7 @@ def update_graph(self, delta_time: float): self.minimap_texture, projection=self.proj) as fbo: # Set the background color - fbo.clear(color=self.background_color) + fbo.clear(self.background_color) # Draw lines & their labels for text in self._all_text_objects: diff --git a/arcade/shape_list.py b/arcade/shape_list.py index 29a53f6a2..fcbbd977b 100644 --- a/arcade/shape_list.py +++ b/arcade/shape_list.py @@ -30,7 +30,8 @@ from arcade.gl import BufferDescription from arcade.gl import Program from arcade import ArcadeContext -from arcade.math import rotate_point + +from .math import rotate_point __all__ = [ diff --git a/arcade/sprite/animated.py b/arcade/sprite/animated.py index a7c7230a8..28e5299d6 100644 --- a/arcade/sprite/animated.py +++ b/arcade/sprite/animated.py @@ -22,7 +22,6 @@ class TextureKeyframe: :param duration: Duration in milliseconds to display this keyframe. :param tile_id: Tile ID for this keyframe (only used for tiled maps) """ - __slots__ = ("texture", "duration", "tile_id") def __init__( self, texture: Texture, @@ -47,12 +46,10 @@ class TextureAnimation: :param keyframes: List of keyframes for the animation. :param loop: If the animation should loop. """ - __slots__ = ("_keyframes", "_duration_ms", "_timeline") - - def __init__(self, keyframes: List[TextureKeyframe]): - self._keyframes = keyframes + def __init__(self, keyframes: Optional[List[TextureKeyframe]] = None): + self._keyframes = keyframes or [] self._duration_ms = 0 - self._timeline: List[int] = self._create_timeline(self._keyframes) + self._timeline: List[int] = self._create_timeline(self._keyframes) if self._keyframes else [] @property def keyframes(self) -> Tuple[TextureKeyframe, ...]: @@ -97,6 +94,25 @@ def _create_timeline(self, keyframes: List[TextureKeyframe]) -> List[int]: self._duration_ms = current_time_ms return timeline + def append_keyframe(self, keyframe: TextureKeyframe) -> None: + """ + Add a keyframe to the animation. + + :param keyframe: Keyframe to add. + """ + self._keyframes.append(keyframe) + self._timeline.append(self._duration_ms) + self._timeline = self._create_timeline(self._keyframes) + + def remove_keyframe(self, index: int) -> None: + """ + Remove a keyframe from the animation. + + :param index: Index of the keyframe to remove. + """ + del self._keyframes[index] + self._timeline = self._create_timeline(self._keyframes) + def get_keyframe(self, time: float, loop: bool = True) -> Tuple[int, TextureKeyframe]: """ Get the frame at a given time. diff --git a/arcade/sprite/base.py b/arcade/sprite/base.py index a1f878b4a..68e0d57b3 100644 --- a/arcade/sprite/base.py +++ b/arcade/sprite/base.py @@ -3,7 +3,7 @@ from typing import TYPE_CHECKING, Iterable, List, TypeVar, Any import arcade -from arcade.types import Point, Color, RGBA255, RGBOrA255, PointList +from arcade.types import Point, Color, RGBA255, PointList from arcade.color import BLACK from arcade.hitbox import HitBox from arcade.texture import Texture @@ -38,7 +38,6 @@ class BasicSprite: "_color", "_texture", "_hit_box", - "_visible", "sprite_lists", "_angle", "__weakref__", @@ -50,7 +49,6 @@ def __init__( scale: float = 1.0, center_x: float = 0, center_y: float = 0, - visible: bool = True, **kwargs: Any, ) -> None: self._position = (center_x, center_y) @@ -59,7 +57,6 @@ def __init__( self._width = texture.width * scale self._height = texture.height * scale self._scale = scale, scale - self._visible = bool(visible) self._color: Color = Color(255, 255, 255, 255) self.sprite_lists: List["SpriteList"] = [] @@ -296,44 +293,29 @@ def top(self, amount: float): @property def visible(self) -> bool: - """Get or set the visibility of this sprite. - - When set to ``False``, each :py:class:`~arcade.SpriteList` and - its attached shaders will treat the sprite as if has an - :py:attr:`.alpha` of 0. However, the sprite's actual values for - :py:attr:`.alpha` and :py:attr:`.color` will not change. - - .. code-block:: python - - # The initial color of the sprite - >>> sprite.color - Color(255, 255, 255, 255) + """ + Get or set the visibility of this sprite. + This is a shortcut for changing the alpha value of a sprite + to 0 or 255:: # Make the sprite invisible - >>> sprite.visible = False - # The sprite's color value has not changed - >>> sprite.color - Color(255, 255, 255, 255) - # The sprite's alpha value hasn't either - >>> sprite.alpha - 255 - - # Restore visibility - >>> sprite.visible = True - # Shorthand to toggle visible - >>> sprite.visible = not sprite.visible + sprite.visible = False + # Change back to visible + sprite.visible = True + # Toggle visible + sprite.visible = not sprite.visible """ - return self._visible + return self._color[3] > 0 @visible.setter def visible(self, value: bool): - value = bool(value) - if self._visible == value: - return - - self._visible = value - + self._color = Color( + self._color[0], + self._color[1], + self._color[2], + 255 if value else 0, + ) for sprite_list in self.sprite_lists: sprite_list._update_color(self) @@ -363,22 +345,27 @@ def color(self) -> Color: return self._color @color.setter - def color(self, color: RGBOrA255): - if color == self._color: - return - - r, g, b, *_a = color - - if _a: - if len(_a) > 1: - raise ValueError(f"iterable must unpack to 3 or 4 values not {len(color)}") - a = _a[0] + def color(self, color: RGBA255): + if len(color) == 4: + if ( + self._color[0] == color[0] + and self._color[1] == color[1] + and self._color[2] == color[2] + and self._color[3] == color[3] + ): + return + self._color = Color.from_iterable(color) + + elif len(color) == 3: + if ( + self._color[0] == color[0] + and self._color[1] == color[1] + and self._color[2] == color[2] + ): + return + self._color = Color(color[0], color[1], color[2], self._color[3]) else: - a = self._color.a - - # We don't handle alpha and .visible interactions here - # because it's implemented in SpriteList._update_color - self._color = Color(r, g, b, a) + raise ValueError("Color must be three or four ints from 0-255") for sprite_list in self.sprite_lists: sprite_list._update_color(self) diff --git a/arcade/sprite_list/sprite_list.py b/arcade/sprite_list/sprite_list.py index a608466b3..5cc832ada 100644 --- a/arcade/sprite_list/sprite_list.py +++ b/arcade/sprite_list/sprite_list.py @@ -1262,7 +1262,7 @@ def _update_color(self, sprite: SpriteType) -> None: self._sprite_color_data[slot * 4] = int(sprite._color[0]) self._sprite_color_data[slot * 4 + 1] = int(sprite._color[1]) self._sprite_color_data[slot * 4 + 2] = int(sprite._color[2]) - self._sprite_color_data[slot * 4 + 3] = int(sprite._color[3] * sprite._visible) + self._sprite_color_data[slot * 4 + 3] = int(sprite._color[3]) self._sprite_color_changed = True def _update_size(self, sprite: SpriteType) -> None: diff --git a/arcade/text.py b/arcade/text.py index 9b2d115c5..8a2879bde 100644 --- a/arcade/text.py +++ b/arcade/text.py @@ -651,7 +651,7 @@ def create_text_sprite( texture_atlas = arcade.get_window().ctx.default_atlas texture_atlas.add(texture) with texture_atlas.render_into(texture) as fbo: - fbo.clear(color=background_color or arcade.color.TRANSPARENT_BLACK) + fbo.clear(background_color or arcade.color.TRANSPARENT_BLACK) text_object.draw() return arcade.Sprite( diff --git a/arcade/window_commands.py b/arcade/window_commands.py index a233c2d25..009cd55eb 100644 --- a/arcade/window_commands.py +++ b/arcade/window_commands.py @@ -113,7 +113,7 @@ def run(): # Used in some unit test if os.environ.get('ARCADE_TEST'): - window.on_update(1.0 / 60.0) + window.on_update(window._update_rate) window.on_draw() elif window.headless: # We are entering headless more an will emulate an event loop diff --git a/doc/_static/checkered.png b/doc/_static/checkered.png deleted file mode 100644 index 6f8f0bf9f6b4b581a93ef1d3b32c382fa59390cc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4885 zcmeHKYgiL!7LG&_QN*Qmr66^TxP@YpNiw;XAP|B?4Wc3-UdkkyzzE5NWFSF{iim}( z@h-4xp%xYKLh*_yUW%fwg19JJg$1!yD+*d_OTCc&CZOW8{p0g&|4B$Pb1v_9&U?;x z@=b1J#6%ZoPiGp9=AsA>oesXKYiB1%@VSb6xr#=k=P!tgC8wii$Y3P2crpf&=>`nK zEV!0Nv)tSpGdt~}@30pMA08O!Gr;ZQ>G-+=?@PbzJazC`*ZqZ`J<31xee5RZnHM6O zdRk64zs$u zp|Z|@Nb`%!K}jKl4p>f3i*~pfxR3CMZruwSnpwa0ha*o z{-)T;X*QQ_TT~1e(cs@o{nYx0S=+wW(H?E=8s@%w>DNP#v~TvOf6y7ymh$&mzSs*# zzweA&yhl}O=B)9kV+e+J29JYi_`KYR!(T1lQ(F}_)f#j7r?thM)qi9jYFN}*s|%aj zm^UnBAB9&OP5< zx>kNkPFEE87YEf3c1(>vzb)8(eCm8(r^4989ksl<ix3YaYTKC+>M7H{Jv(ahIz+-Ar@ZrYP85=OwYc;ylEf*G zzp`#u0p?$Dp!KOY?-????x?#DGiT0YGE?T~MeNUJ>hXr!!w(NnA5-W#f6uP@hyIy_ zHx++#diMOAdwz;eyE$gN^kK@>V;zU@RgvL)C%a02yLv6P{b=6$L-O{#C-la|@|yL# zZkK$L6g#sjyXc7|H;V_?WCm3%<-?AsYu)i_P67E|TW&90wl8Pcnv^vSiLj$%b;PL8 zwl*>Ejw8O@}|xQr=h-vn`^=Di|eyy0l|xw#$U>?w$*k_lED^ zyx6biV25JZqq1}PjdQD0j&yihkELEWH0?dM{Lb*|xz_UdiX>4_nC+sdC&+f$@WoV{ zm*_;jjb)wqjD5Rew%O%e^SoPoxMJi;nS9=uf%q_+gJF|ySC(5G!BGkrs{BmECP zh&TQ`8sYf(v8+pzPM zU&5_@;7LcC1-xCw`4`J_zKI!#hy5)$RJ)1eufV&lWeW#(FUo73)swo|8h9w413_+= z;p&6A*K>CYbXi@2DJwT(8@snyt`-kMis6$J87Uhy^G106zV)gxvU`fr zwxagD^V`?#b_lbkCkJIce0JK_Y9{u!ovFxkk8o&aTe~>JmZfECn^R7#_THK9Ft9Mw zzv@v-BCVmY`1-B57fv~P`pD}#+4 zd!h{5{;Ruxbuk#6R&MZJSbMKox;5kZksa4gj_%lOG}gSNxr15iiG!ILtDGWH6FL^E zAygR4qBDS*N}~l#uozHv5=KHQECJU`8J)*YG9X+dWyJ86u+kvI67lc_Mr_7{h$!`f zB(+$>m=Ne3V37a-9Y&&%MVG8MNi0%^9ajRbDKVP?*-gkKDI-=H3CRc}1|ckj1vBLq zJdMi;bcOHLy7>n61;X z`+Jy3c^Uxe3+PWhOi|#chdmuL5vfKsCQrlk`L#i>^o{mP%#*#4|Fg1Zy zId3gFQK5`{?LkqHfa?r)FF^KNmL#ryL)KfdQ5t(X{R08+uW{e9ewDl37+5Kll2AgO zN`=_U4sb*~4$S2V_)J8@(J~Rf3TBFUn1;y{2)P2TNGnhw+%l)+`eZzPe)D5(VwQpOZqpK5udh{AQ)3=*Z-hunGr)t!Wcym$x;Rt5=430+Zzh#ElN0U z0v731$M^O83@oMhu6GM0<8~DU+1plvs(TkPp=nreMSx$gN}Y)66ELv9`x@$%9RHJI z!6J?d<*QI8tU`rML@NgM28Cv7v?z=T5dpXn*wc88ZX&d#88u?T2_RFD4QL>HHjuBK zO244<`Y3gMMQG`1Q)b zcmdOj{d!#WaYoVj7yk9t;xAkQK)+w)o%nrE*L%9&iGg=Ae&1d1>3Syy-pTlVcm3b! za(?r`gXzI3$P6B5=3dC|1dl?Fs>u^WX*TLrRJC(GkPI<|&oa?ytg+O4fMxrHG$0&I zDwOiU_vv(3H{Wp=(=j0Wh?K{YGD1h4n`!pr9HxQHIGG4h$GMoy;Uj_3SVd@Xlt+Vl zPg$Am@)Ac+PtW>nk9?1uf%K9!Pi!_@bq$ZU?DHWj3Y&5-0dtx{9uc}PB!20C0f0?b AjQ{`u diff --git a/doc/_static/css/custom.css b/doc/_static/css/custom.css index 73354904e..38ea33d70 100644 --- a/doc/_static/css/custom.css +++ b/doc/_static/css/custom.css @@ -4,87 +4,33 @@ body { /* TOC - Main landing page */ /* Show two columns if screen is wide enough */ @media screen and (min-width: 700px) { - /* For regular items */ .toctree-wrapper > ul { columns: 2; } - /* For Social and Learning Resources sections */ - .toc-outside-links { - columns: 2; - } -} - -/* Wrap individual main page items for easier group manipulation - of contents and images. See PR #2027 for main page css changes */ -.main-page-item-wrapper { - align-items: flex-start; - display: flex; - margin: 10px; -} -.main-page-item-wrapper > .toctree-wrapper { - width: 100%; -} - -/* single-col-box for items on main page with 2 bullet points, use if - wanting to avoid having 2 columns with 1 item each inside */ -.main-page-item-wrapper > .single-col-box { - width: 100%; -} -.main-page-item-wrapper > .single-col-box ul { - columns: 1; -} - -/* For Social and Learning Resources to make -title + list appear like other categories */ -.main-page-item-wrapper > .main-page-item-sub-wrapper { - display: flex; - flex-direction: column; - margin: 0px; - width: 100%; -} -.main-page-item-wrapper > .main-page-item-title { - width: 100%; } -.main-page-item-title > p { - font-size: var(--font-size--small); - margin-bottom: 0; - text-align: initial; - text-transform: uppercase; +.toctree-l1 { + margin-left: 5px; } -.main-page-item-sub-wrapper > .toc-outside-links { - margin-left: 0px; - width: 100%; +.toc-outside-links { + margin-left: 35px; } - -/* Wrappers and formatting for sprinter, START HERE, github star button -to align them neatly */ -.main-page-item-wrapper-header { - align-items: center; - display: flex; - margin: 10px; +.toc-outside-links ul { + columns: 2; } -.main-page-box { - width: 100%; - display: flex; - justify-content: space-between; - align-items: center; +.toc-outside-links li p, .toc-outside-links ul li { + margin-left: 5px; } -.main-page-box > .main-page-link { - display: flex; - width: 100%; +.toc-outside-links a { + text-decoration: none; } -.main-page-box > .main-page-link a { - display: flex; +.toctree-wrapper a { + text-decoration: none; } -.sprinter-box { - margin-left: 10px; - height: 55px; - display: flex; +.toc-outside-links a:hover { + text-decoration: underline; } -.start-here-box { - align-items: center; - display: flex; - margin-left: -10px; +.toctree-wrapper a:hover { + text-decoration: underline; } #the-python-arcade-library h2 { font-size: var(--font-size--small); @@ -93,34 +39,16 @@ to align them neatly */ margin-top: .5rem; text-align: initial; text-transform: uppercase; - width: 100%; - display: flex; -} -.main-page-box > .main-page-box-gh { - display: flex; - align-items: center; - margin-right: 0px; -} -#github-stars { - width: 141px; - height: 30px; - margin-bottom: -9px; -} - -/* Formatting for list items */ -.toctree-l1 { - margin-left: 5px; -} -.toctree-wrapper a { - text-decoration: none; -} -.toctree-wrapper a:hover { - text-decoration: underline; } #the-python-arcade-library ul { margin-top: 0; margin-bottom: 0; } +#github-stars { + width: 170px; + height: 30px; + margin-bottom: -18px; +} .heading-icon { columns: 2; position: relative; @@ -176,6 +104,9 @@ img.right-image { width: 78px; padding-right: 15px; } +.main-page-table { + width: 100%; +} .vimeo-video { border: 0; position:absolute; @@ -243,10 +174,3 @@ body:not([data-theme="light"]) .highlight .c1 { .highlight .c1 { color: #007507; } - -.checkered { - background-image: url(../checkered.png); - background-repeat: repeat; - margin:0px; - padding: 0px; -} \ No newline at end of file diff --git a/doc/conf.py b/doc/conf.py index 7e7f16ea2..f8bc966eb 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -3,14 +3,12 @@ Generate HTML docs """ -import docutils.nodes -import os -import re import runpy +import sys +import os import sphinx.ext.autodoc import sphinx.transforms -import sys - +import docutils.nodes # --- Pre-processing Tasks @@ -93,7 +91,7 @@ # General information about the project. project = 'Python Arcade Library' -copyright = '2024, Paul Vincent Craven' +copyright = '2023, Paul Vincent Craven' author = 'Paul Vincent Craven' # The version info for the project you're documenting, acts as replacement for @@ -207,57 +205,45 @@ def warn_undocumented_members(_app, what, name, _obj, _options, lines): )) -def generate_color_table(filename, source): - """This function Generates the Color tables in the docs for color and csscolor packages""" - - append_text = "\n\n.. raw:: html\n\n" - append_text += " \n" - - # Will match a line containing: - # name '(?P[a-z_A-Z]*)' followed by - # a Color '(?: *= *Color *\( *)' followed by - # red '(?P\d*)' followed by - # green '(?P\d*)' followed by - # blue '(?P\d*)' followed by - # alpha '(?P\d*)' - color_match = re.compile(r'(?P[a-z_A-Z]*)(?:[ =]*Color[ (]*)(?P\d*)[ ,]*(?P\d*)[ ,]*(?P\d*)[ ,]*(?P\d*)') - - with open(filename) as color_file: - for line in color_file: +def source_read(_app, docname, source): - # Check if the line has a Color. - matches = color_match.match(line) - if not matches: - continue - - color_rgba = f"({matches.group('red')}, {matches.group('green')}, {matches.group('blue')}, {matches.group('alpha')})" - - # Generate the alpha for CSS color function - alpha = int( matches.group('alpha') ) / 255 - css_rgba = f"({matches.group('red')}, {matches.group('green')}, {matches.group('blue')}, {alpha!s:.4})" + # print(f" XXX Reading {docname}") + import os + file_path = os.path.dirname(os.path.abspath(__file__)) + os.chdir(file_path) + filename = None + if docname == "api_docs/arcade.color": + filename = "../arcade/color/__init__.py" + elif docname == "api_docs/arcade.csscolor": + filename = "../arcade/csscolor/__init__.py" - append_text += " " - append_text += f"" - append_text += f"" - append_text += f"" - append_text += "\n" + if filename: + # print(f" XXX Handling color file: {filename}") + import re + p = re.compile(r"^([A-Z_]+) = (\(.*\))") - append_text += "
{matches.group('name')}{color_rgba}
 
" - source[0] += append_text + original_text = source[0] + append_text = "\n\n.. raw:: html\n\n" + append_text += " \n" + color_file = open(filename) + for line in color_file: + match = p.match(line) -def source_read(_app, docname, source): - """Event handler for source-read event""" + if match: + color_variable_name = match.group(1) + color_tuple = tuple(int(num) for num in match.group(2).strip('()').split(',')) + color_rgb_string = ', '.join(str(i) for i in color_tuple[:3]) - file_path = os.path.dirname(os.path.abspath(__file__)) - os.chdir(file_path) + append_text += " " + append_text += f"" + append_text += f"" + append_text += f"" + append_text += "\n" - # Transform source for arcade.color and arcade.csscolor - if docname == "api_docs/arcade.color": - generate_color_table("../arcade/color/__init__.py", source) - elif docname == "api_docs/arcade.csscolor": - generate_color_table("../arcade/csscolor/__init__.py", source) + append_text += "
{color_variable_name}{color_tuple}
" + source[0] = original_text + append_text def post_process(_app, _exception): diff --git a/doc/example_code/how_to_examples/gui_flat_button.rst b/doc/example_code/how_to_examples/gui_flat_button.rst index dd2da02ba..42fd64c31 100644 --- a/doc/example_code/how_to_examples/gui_flat_button.rst +++ b/doc/example_code/how_to_examples/gui_flat_button.rst @@ -32,29 +32,3 @@ that uses all three ways. .. literalinclude:: ../../../arcade/examples/gui_flat_button.py :caption: gui_flat_button.py :linenos: - -See :class:`arcade.gui.UIBoxLayout` and :class:`arcade.gui.UIAnchorLayout` -for more information about positioning the buttons. -For example, this change to line 31: - -.. code-block:: python - - self.v_box = arcade.gui.widgets.layout.UIBoxLayout(space_between=20, vertical=False); - -and to line 60: - -.. code-block:: python - - ui_anchor_layout.add(child=self.v_box, - anchor_x="left", - anchor_y="bottom", - align_x=10, - align_y=10); - -in the code above will align the buttons horizontally and anchor them to the -bottom left of the window with 10px margins. - -.. image:: ../how_to_examples/gui_flat_button_positioned.png - :width: 600px - :align: center - :alt: Screen shot of flat text buttons in bottom left of window \ No newline at end of file diff --git a/doc/example_code/how_to_examples/gui_flat_button_positioned.png b/doc/example_code/how_to_examples/gui_flat_button_positioned.png deleted file mode 100644 index c77e9f3e103e41913a016ef8cfd3aab1ca2713c2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6843 zcmeHM2~d;Smi`fyD6K&e5NMzg1#MK6Rb+cCD##L1b_7KBUABaa|rfHB(blsnlO}^WAgL{mwbxxi|mg z8z$Ed9TYhT0Kg#weZ89i!2J^d?16&za7Ip-`#W+T2kz-x2Lb>u|IU{Sc=h@?XYeN{ z{hP)B5PAv#?neQ@CY$s81^~eq0DyW204~1-03p9;4j-=qfQYt%p0-)Y&^*~A+-xeV zWeg@QdB6OOqt0HvW7@~6K%|35ifhc}Hi-2n%+UID9rI)PUroGHe~vp*`^$ZEf8Cta zxYW}Dg@Tb5V-L!$B+s9I`LPrPx|o6=0dqU;)k~`ddRA~e0n>OqR!vTU{W{R~sDX3} z7jU1-ue6u58D8Kocenu1Spe882;6Vk0}SZ8_)e66N(`)Cv3Q2`_B+i5+?;RS1LV67 z4@J?nP>BdOYaK(EKCC+VgsI&3%892ww;2%#zR;6L(>21N%(V=OqznZ&lbFL| zP+!C52=JGg8RQ$rP^)b>jUOv&T{=~>8F3^oZbx#7!wIY!VK!jlZe$(*lm>;ZA zn-weuuEpQrLs&MNWtsBN~eNm!9@-c(Y+J<2XZha=wNB31Y3|FD@iLs{?7*&d` zzQHg{w6bgF8OeG%9r(rjE&(rdG{OEXfUTE$72f7!o%( zFn=*oKxg1%{lVeD*wXE2hB;=sWC(pi9G!m}9oso`-huKS2k&3a7D&Jk2OUTM%GHEU zlJZrLeMymdrXr7Y9)0JsPn2y<7jY;qO`Uk~O*WhqbBgOexwH)0RPy0)W>ghqVq{H- z!q6tX>LC@QPbXV;;AF?AA{zQdRVQcE!e|OiiC4avX)ISGk71-0;uDRiWHcBwS&EzO zBMh8r>il8wn4tm_d|=`xuKsH1Bmeg}w>610iRR`H&yHjvd(bi#2$U{rq55QSieoIX z92j8gRbw~I;upwK{nRRp;N|nSH)_XLg6Cw%m*PU;SK#pBk@BlbLSIbq2j{k7d5cZN zShKm4*I)+yea}BD^(q+|L^PDeo_MN%vNzbM-z#yBBp>~G3O|~4tA=RvD3NVnTKjcQ zwsIbJi(ks;3IFZIVl)VlH!w8*(UzH@AmD??S{1bJ-v`Xv2N}sCinF^r<{G7V_M~q7OWltFdZwDXdl?^2{#B&{2a-ydN%oOVh6H`Q-D@!NfsRL&2{3pp7ia z=m$cm%sQ6s<&{Ap-VYsK5iEP#YERLQOY$JVyQ<`-A+)Ki`BdrQL1%J(a1+Gc{zx*+ ziwy@!o*d7*yUj z;U%Rp+<56CD(h}UK4IXeJ%CwiT^nhj<`#%h>gca;7X65|9q6yC|L*u7FSiS{BeCBD+QrDu)IYZLy`bGu;yAv` zr(KqIgLcvOr{cXUO8+=1z8AEMwwCGL70x?wx{Dt9DZs&|fLnf5#_r+H@(GgiHN zMak1yAoUmo?lE+J^h#6cp@Z?fG8$S_wxlBSLMTnsGOs2tr_vcdmd7ux>G!NGlRt#% z;N(dft;x>`S>~0qw381Wnv3T>nD8?t?}Y=rikdQNc*}wOQ526aGQDY2J_jNHR0PrsJ-f%stFDMzeVZ+aJsnRSqYtFWVb~-9x*6kxfwUUmUmJ*^#1W-vw5}3u3;% z$=QbdoDI&X?@XeZg~i$|!eYXz`}44$V$QsyJd9$jMQbGTKX%3xo;PM+>*~U?w5xMk z5r^2@LxojdBr2haRFsZ{$T`%NulA)tV@J<1eAt*p!i) zm{EVCQ}U==>;P_IGzO%_sthT_b!sViz{kqn6??PVOi$&Wdie3-dcyIPbnA(ZR$XF4 zxt9H!!RQi3zZhj+G0n;r-<+MumB|bm>hnMgr5?jvTyc<$UgDP)2rs=PB_$m_#*&R@H= z2kFmH=-F1P3isF6?lfViH=~ED))!VXA~WpPqmng6ejBDdd{X^spo$_)9luMJUg+4(VQA?F;wX>Q7nBWdC^S?>+?$g zq+1U{CQS_2g>7=ZLGgzeU0SSly4g?;_8tnK`n4aDj66xUl8m;g@+2#5RoBd%^ZXH~ z+DwG#f;76d3a46&f0mVX!@m^6CHc_Z4P9No9FY032D%^wJJbLdwQXQ;4?1;dLI)gU za+JTuDqoX1iY)_cJ@GB#)A9AqzUXwVd3}`0qwIt5?~y%xIUhR$UqPX^K42*4E|q$$ zu7Z;L`ueZ0aGP=N?o9G(X@bY9U~5(7bb?nP%ZCg+0L6XOCfOT zC&P8$CKC;MyU@0>lG3>h6)(;mlCA3H%a}DP0&P_ODv5fQo43C$_#|$<>X1VRNJgLwYG>bDu`H zCaW^h&E4F>VjDjG9FNC0Hu4vgaEf>8m&g|jpIx1u^@gi`>~5Cd9WE@sZ9SCDwC04y zizrr%TD`rk$?RJkC<_eDq|d3SZ%*d4svJHoJA5H*O_Z32F~?|=J&5nX%^rWT9(<%i zd8O&~Wkf33n?;;J2WMKGm$Xo$z@O`=9$ zZDSC`z1{eti@qZYQ&UrU!K$kCw@OYOm5=$^Ulw1s$x-2(-DEpBeDfBBMIX|R&;>0m z%b~)~V|>XGJcsL5qn}u3U$q}aHm;AxkQ~BXK#pw;I#~U;kj%@>E$V|&$!K@QY!_UV zARb9WRoEYrJSK<;@Q!g(4-cz@E+knsd(SZ)C@;6KgCqrU%GW_f z815mjkk9Qb`LckI)bjH3D;a4gWm9CYTe`UP zyA$5JfFu`TD* z7Sb}aY-PGTS?_IKof`a&1swiy3nrL@Y(0JUY~0oC!9q^w1zi`Up+1Y zlr{M#x5n#s{p-DfJ$0u=5?_andQd8D5=E@nHGa$MOUi}Br%^O3Loc6wY9uK1qS}MF z%;~2NNVX|B$Wg+m2uW1=d7^}tYDBj&mE)krdSYW2ev~Y8@5EDl4>F>H zr^aBB3-*K9?FG>rN0NJ9E?MBxy3pbqOg^118zm%s8XGu`hdk^PawrO$ZFM{c_En-r zc8V%{Uzjh+K!q~o#H*S{mR&v?zw(WMz^U0+1<%>sea>eU7Ax%6xNJPLH-}37xMkU` z*??;)^hYmQ?(OdO{f#veO#QJ)$?J=K7+HYR`2uIU(luFKtD6tbczr2q-dyJf zdj)SyVu?wtZx99>S{mc+S3OHUB^iBAi}v%;$I!*_yC6rxhmTheL{`nX_bTS)Uf%q)m2e%B8}! z%V!ump-P)K;J=h^)XZ2-TUZIODJ*m{^+V4;_Vg>+o}b@iL- ziqtGVon(j1rl~6|p4;`_)}{=z#PPR1Iv}1&@zSMJG9s!NruBZlNcu!7gKun=1`!q( zu9(3W78N!6d=h(q3hTU_3EPh7GmXhb-`w!P_8_3QJ*T=eA%oqz(U0}+j(s>4BY~Jz z{$kw#?RqG*+Kt5`*ll3B#(NWL19N@vVQ@+zC*2EDQ89c+Qy=&Cns)|!3+a3+u&!7+ zkheH4`JuniGD&p3kBg_>w|1<)?^!5aWVLCTLCWhOOeSX-+y zvdXNjtzlq^`m#uQuiF70>|{;*dXG?A#(u%78VxNUM`H-$WpD77pWwb0UhB7 zXCm#<3w7DSb6;LZObas?EVbzAiQMSTwzRqSiK$x-+g|b0JX+fusF>th7h;T};Oabl zQKPMM`(4i!$>`qR@mnUExgNw0PJ-gnkO3sJZm50GW=XN_8R z1yQ7hRem*?{0_I_tYF>h*DddBYGqO$lk78kD-9}&M6AD6?B`+owrKMxL$nwe^(HTl zpy!Of5ACX|d7kQXJcyoZm=>-Wz9GTcp#);`+XCgK^NGztH(5@TR*5LLY(Q0NeakVC z!z_D(#=gmPHec9G?Q=bAwE<-@f+LK$C8H6R_9D0>u|WKhZ|CKJlIA@XgRxUoPmhRY2oCD%poz+_4qdmXW zU(y48*S{Mr_UC#V*4?uK;6~!kYnMO1jQ)N{zpv541_K8dwasBXzt(YHJOKt*O!P{0 H?mYMxZ;|D) diff --git a/doc/get_started/install/index.rst b/doc/get_started/install/index.rst index 6f47c01f8..0f62152c4 100644 --- a/doc/get_started/install/index.rst +++ b/doc/get_started/install/index.rst @@ -19,15 +19,3 @@ Select the instructions for your platform: pycharm obsolete -Advanced --------- - -Use the following command to install the latest development build of Arcade: - -.. code-block:: bash - - pip install -I https://github.com/pythonarcade/arcade/archive/refs/heads/development.zip - -This pre-release build will give you access to the latest features, but it may be unstable. - -You can also get pre-release versions from the `Arcade PyPi Release History `_. \ No newline at end of file diff --git a/doc/index.rst b/doc/index.rst index 9cac29faf..49929c5d3 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -20,169 +20,161 @@ The Python Arcade Library game without learning a complex framework.

-.. container:: main-page-item-wrapper-header - - .. raw:: html - -
- -
- -
-
- -.. container:: main-page-item-wrapper - - .. image:: images/example_games.svg - :alt: Get Started icon - :class: heading-icon - - .. toctree:: - :maxdepth: 1 - :caption: Get Started - - get_started/introduction - get_started/get_started - get_started/install/index - get_started/how_to_get_help - -.. container:: main-page-item-wrapper - - .. image:: images/example_code.svg - :alt: Example Code - :class: heading-icon - - .. toctree:: - :maxdepth: 1 - :caption: Examples - - example_code/how_to_examples/index - example_code/game_jam_2020 - example_code/sample_games - -.. container:: main-page-item-wrapper - - .. image:: images/learn.svg - :alt: Tutorials - :class: heading-icon - - .. toctree:: - :maxdepth: 1 - :caption: Tutorials - - tutorials/platform_tutorial/index - tutorials/pymunk_platformer/index - tutorials/views/index - tutorials/card_game/index - tutorials/lights/index - tutorials/bundling_with_pyinstaller/index - tutorials/compiling_with_nuitka/index - tutorials/shader_tutorials - tutorials/menu/index - tutorials/framebuffer/index - -.. container:: main-page-item-wrapper - - .. image:: images/example_games.svg - :alt: Programming guide icon - :class: heading-icon - - .. toctree:: - :maxdepth: 1 - :caption: Guide - - programming_guide/sprites/index - programming_guide/keyboard - programming_guide/sound - programming_guide/textures - programming_guide/sections - programming_guide/gui/index - programming_guide/texture_atlas - programming_guide/edge_artifacts/index - programming_guide/logging - programming_guide/opengl_notes - programming_guide/performance_tips - programming_guide/headless - programming_guide/vsync - programming_guide/pygame_comparison - -.. container:: main-page-item-wrapper - - .. image:: images/API.svg - :alt: API icon - :class: heading-icon - - .. toctree:: - :maxdepth: 1 - :caption: API - - Index - Reference - api_docs/resources - -.. container:: main-page-item-wrapper - - .. image:: images/source.svg - :alt: Source icon - :class: heading-icon - - .. toctree:: - :maxdepth: 1 - :caption: Source Code - - GitHub - programming_guide/release_notes - License - contributing_guide/index - contributing_guide/release_checklist - -.. container:: main-page-item-wrapper - - .. image:: images/social.svg - :alt: Social icon - :class: heading-icon - - .. container:: main-page-item-sub-wrapper - - .. toctree:: - :maxdepth: 1 - :caption: Social - - Discord (most active spot) - Reddit /r/pythonarcade - Twitter @ArcadeLibrary - Instagram @PythonArcadeLibrary - Facebook @ArcadeLibrary - community/diversity - -.. container:: main-page-item-wrapper - - .. image:: images/performance.svg - :alt: Performance icon - :class: heading-icon - - .. container:: main-page-item-sub-wrapper - - .. toctree:: - :maxdepth: 1 - :caption: Learning Resources - - Book - Learn to program with Arcade - Peer To Peer Gaming With Arcade and Python Banyan - US PyCon 2022 Talk - US PyCon 2019 Tutorial - Aus PyCon 2018 Multiplayer Games - US PyCon 2018 Talk +.. |Go| image:: images/woman_sprinter.svg + :width: 48 + :alt: Start Here + :target: get_started.html + +.. raw:: html + + + + + + +
+

+ + Start Here + Start Here + + +

+
+ +
+ +.. image:: images/example_games.svg + :alt: Get Started icon + :class: heading-icon + +.. toctree:: + :maxdepth: 1 + :caption: Get Started + + get_started/introduction + get_started/get_started + get_started/install/index + get_started/how_to_get_help + +.. image:: images/example_code.svg + :alt: Example Code + :class: heading-icon + +.. toctree:: + :maxdepth: 1 + :caption: Examples + + example_code/how_to_examples/index + example_code/game_jam_2020 + example_code/sample_games + +.. image:: images/learn.svg + :alt: Tutorials + :class: heading-icon + +.. toctree:: + :maxdepth: 1 + :caption: Tutorials + + tutorials/platform_tutorial/index + tutorials/pymunk_platformer/index + tutorials/views/index + tutorials/card_game/index + tutorials/lights/index + tutorials/bundling_with_pyinstaller/index + tutorials/compiling_with_nuitka/index + tutorials/shader_tutorials + tutorials/menu/index + tutorials/framebuffer/index + +.. image:: images/example_games.svg + :alt: Programming guide icon + :class: heading-icon + +.. toctree:: + :maxdepth: 1 + :caption: Guide + + programming_guide/sprites/index + programming_guide/keyboard + programming_guide/sound + programming_guide/textures + programming_guide/sections + programming_guide/gui/index + programming_guide/texture_atlas + programming_guide/edge_artifacts/index + programming_guide/logging + programming_guide/opengl_notes + programming_guide/performance_tips + programming_guide/headless + programming_guide/vsync + programming_guide/pygame_comparison + +.. image:: images/API.svg + :alt: API icon + :class: heading-icon + +.. toctree:: + :maxdepth: 1 + :caption: API + + Index + Reference + api_docs/resources + +.. image:: images/source.svg + :alt: Source icon + :class: heading-icon + +.. toctree:: + :maxdepth: 1 + :caption: Source Code + + GitHub + programming_guide/release_notes + License + +.. image:: images/source.svg + :alt: Source icon + :class: heading-icon + +.. toctree:: + :maxdepth: 1 + :caption: Contributing + + contributing_guide/index + contributing_guide/release_checklist + +.. image:: images/social.svg + :alt: Social icon + :class: heading-icon + +Social +------ + +.. container:: toc-outside-links + + * `Discord (most active spot) `_ + * `Reddit /r/pythonarcade `_ + * `Twitter @ArcadeLibrary `_ + * `Instagram @PythonArcadeLibrary `_ + * `Facebook @ArcadeLibrary `_ + * :ref:`diversity_statement` + +.. image:: images/performance.svg + :alt: Performance icon + :class: heading-icon + +Learning Resources +------------------ + +.. container:: toc-outside-links + + * `Book - Learn to program with Arcade `_ + * `Peer To Peer Gaming With Arcade and Python Banyan `_ + * `US PyCon 2022 Talk `_ + * `US PyCon 2019 Tutorial `_ + * `Aus PyCon 2018 Multiplayer Games `_ + * `US PyCon 2018 Talk `_ diff --git a/doc/programming_guide/gui/concept.rst b/doc/programming_guide/gui/concept.rst index e283b3e3c..d61fcd8c3 100644 --- a/doc/programming_guide/gui/concept.rst +++ b/doc/programming_guide/gui/concept.rst @@ -547,5 +547,5 @@ Property ```````` :py:class:`~arcade.gui.Property` is an pure-Python implementation of Kivy -like Properties. They are used to detect attribute changes of widgets and trigger -rendering. They are mostly used within GUI widgets, but are globally available since 3.0.0. +Properties. They are used to detect attribute changes of widgets and trigger +rendering. They should only be used in arcade internal code. diff --git a/doc/programming_guide/sound.rst b/doc/programming_guide/sound.rst index 2a0bae66e..f4f1ae656 100644 --- a/doc/programming_guide/sound.rst +++ b/doc/programming_guide/sound.rst @@ -279,7 +279,7 @@ fully decompressed albums of music in RAM. Each decompressed minute of CD quality audio uses slightly over 10 MB of RAM. This adds up quickly, and can slow down or freeze a computer if it fills RAM completely. -For music and long background audio, you should strongly consider +For music and long background audio, you should should strongly consider :ref:`streaming ` from compressed files instead. diff --git a/doc/tutorials/raycasting/example.py b/doc/tutorials/raycasting/example.py index f6d7f0ebe..404b8fad6 100644 --- a/doc/tutorials/raycasting/example.py +++ b/doc/tutorials/raycasting/example.py @@ -90,12 +90,12 @@ def load_shader(self): def on_draw(self): self.channel0.use() # clear_color = 0, 0, 0, 0 - # self.channel0.clear(color=clear_color) + # self.channel0.clear(clear_color) self.wall_list.draw() self.channel1.use() - # self.channel1.clear(color=clear_color) - self.channel1.clear(color=arcade.color.ARMY_GREEN) + # self.channel1.clear(clear_color) + self.channel1.clear(arcade.color.ARMY_GREEN) self.bomb_list.draw() self.use() diff --git a/doc/tutorials/shader_inputs/texture_write.py b/doc/tutorials/shader_inputs/texture_write.py index c6ad51a75..e28513960 100644 --- a/doc/tutorials/shader_inputs/texture_write.py +++ b/doc/tutorials/shader_inputs/texture_write.py @@ -20,7 +20,7 @@ def __init__(self): self.fbo = self.ctx.framebuffer(color_attachments=[self.tex]) # Put something in the framebuffer to start - self.fbo.clear(color=arcade.color.ALMOND) + self.fbo.clear(arcade.color.ALMOND) with self.fbo: arcade.draw_circle_filled( SCREEN_WIDTH / 2, diff --git a/doc/tutorials/shader_inputs/textures.py b/doc/tutorials/shader_inputs/textures.py index f792b91cf..f6cb7af4d 100644 --- a/doc/tutorials/shader_inputs/textures.py +++ b/doc/tutorials/shader_inputs/textures.py @@ -23,8 +23,8 @@ def __init__(self): self.fbo_1 = self.ctx.framebuffer(color_attachments=[self.tex_1]) # Fill the textures with solid colours - self.fbo_0.clear(color_normalized=(0.0, 0.0, 1.0, 1.0)) - self.fbo_1.clear(color_normalized=(1.0, 0.0, 0.0, 1.0)) + self.fbo_0.clear(color=(0.0, 0.0, 1.0, 1.0), normalized=True) + self.fbo_1.clear(color=(1.0, 0.0, 0.0, 1.0), normalized=True) # Create a simple shader program self.prog = self.ctx.program( diff --git a/doc/tutorials/shader_toy_particles/index.rst b/doc/tutorials/shader_toy_particles/index.rst index 7a56dc20a..52e096e58 100644 --- a/doc/tutorials/shader_toy_particles/index.rst +++ b/doc/tutorials/shader_toy_particles/index.rst @@ -4,8 +4,7 @@ Shader Toy - Particles ====================== .. contents:: - :class: this-will-duplicate-information-and-it-is-still-useful-here - + .. raw:: html diff --git a/pyproject.toml b/pyproject.toml index 0b6abf181..4e92ca258 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,14 +17,13 @@ classifiers = [ "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", - "Programming Language :: Python :: 3.12", "Programming Language :: Python :: Implementation :: CPython", "Topic :: Software Development :: Libraries :: Python Modules" ] dependencies = [ - "pyglet>=2.0.14,<2.1", - "pillow~=10.2.0", - "pymunk~=6.6.0", + "pyglet>=2.0.12,<2.1", + "pillow~=10.0.0", + "pymunk~=6.5.1", "pytiled-parser~=2.2.3" ] dynamic = ["version"] @@ -48,16 +47,16 @@ dev = [ "coveralls", "pytest-mock", "pytest-cov", - "pygments==2.17.2", + "pygments==2.16.1", "docutils==0.20.1", "furo", - "pyright==1.1.355", + "pyright==1.1.352", "pyyaml==6.0.1", - "sphinx==7.2.6", - "sphinx-autobuild==2024.2.4", + "sphinx==7.2.2", + "sphinx-autobuild==2021.3.14", "sphinx-copybutton==0.5.2", "sphinx-sitemap==2.5.1", - "typer[all]==0.11.0", + "typer[all]==0.7.0", "wheel", ] # Testing only @@ -66,6 +65,7 @@ testing_libraries = [ "pytest-mock", "pytest-cov", "pyyaml==6.0.1", + "typer[all]==0.7.0", ] [project.scripts] diff --git a/tests/conftest.py b/tests/conftest.py index 46b581477..5f2ed9b80 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,7 +1,5 @@ import gc import os -import sys -from contextlib import contextmanager from pathlib import Path if os.environ.get("ARCADE_PYTEST_USE_RUST"): @@ -15,14 +13,13 @@ PROJECT_ROOT = (Path(__file__).parent.parent).resolve() FIXTURE_ROOT = PROJECT_ROOT / "tests" / "fixtures" arcade.resources.add_resource_handle("fixtures", FIXTURE_ROOT) -REAL_WINDOW_CLASS = arcade.Window WINDOW = None -def create_window(width=800, height=600, caption="Testing", **kwargs): +def create_window(): global WINDOW if not WINDOW: - WINDOW = REAL_WINDOW_CLASS(title="Testing", vsync=False, antialiasing=False) + WINDOW = arcade.Window(title="Testing", vsync=False, antialiasing=False) WINDOW.set_vsync(False) # This value is being monkey-patched into the Window class so that tests can identify if we are using # arcade-accelerate easily in case they need to disable something when it is enabled. @@ -41,10 +38,6 @@ def prepare_window(window: arcade.Window): arcade.cleanup_texture_cache() # Clear the global texture cache window.hide_view() # Disable views if any is active window.dispatch_pending_events() - try: - arcade.disable_timings() - except Exception: - pass # Reset context (various states) ctx.reset() @@ -100,130 +93,3 @@ def window(): arcade.set_window(window) prepare_window(window) return window - - -class WindowProxy: - """Fake window extended by integration tests""" - - def __init__(self, width=800, height=600, caption="Test Window", *args, **kwargs): - self.window = create_window() - arcade.set_window(self) - prepare_window(self.window) - if caption: - self.window.set_caption(caption) - if width and height: - self.window.set_size(width, height) - self.window.set_viewport(0, width, 0, height) - - self._update_rate = 60 - - @property - def ctx(self): - return self.window.ctx - - @property - def width(self): - return self.window.width - - @property - def height(self): - return self.window.height - - @property - def size(self): - return self.window.size - - @property - def aspect_ratio(self): - return self.window.aspect_ratio - - @property - def mouse(self): - return self.window.mouse - - @property - def keyboard(self): - return self.window.keyboard - - def current_view(self): - return self.window.current_view - - @property - def background_color(self): - return self.window.background_color - - @background_color.setter - def background_color(self, color): - self.window.background_color = color - - def clear(self, *args, **kwargs): - return self.window.clear(*args, **kwargs) - - def flip(self): - if self.window.has_exit: - return - return self.window.flip() - - def on_draw(self): - return self.window.on_draw() - - def on_update(self, dt): - return self.window.on_update(dt) - - def show_view(self, view): - return self.window.show_view(view) - - def hide_view(self): - return self.window.hide_view() - - def get_size(self): - return self.window.get_size() - - def set_size(self, width, height): - self.window.set_size(width, height) - - def get_pixel_ratio(self): - return self.window.get_pixel_ratio() - - def set_mouse_visible(self, visible): - self.window.set_mouse_visible(visible) - - def center_window(self): - self.window.center_window() - - def set_vsync(self, vsync): - self.window.set_vsync(vsync) - - def get_viewport(self): - return self.window.get_viewport() - - def set_viewport(self, left, right, bottom, top): - self.window.set_viewport(left, right, bottom, top) - - def use(self): - self.window.use() - - def push_handlers(self, *handlers): - self.window.push_handlers(*handlers) - - def remove_handlers(self, *handlers): - self.window.remove_handlers(*handlers) - - def run(self): - self.window.run() - - -@pytest.fixture(scope="function") -def window_proxy(): - """Monkey patch the open_window function and return a WindowTools instance.""" - _window = arcade.Window - arcade.Window = WindowProxy - - _open_window = arcade.open_window - def open_window(*args, **kwargs): - return create_window(*args, **kwargs) - arcade.open_window = open_window - - yield None - arcade.Window = _window - arcade.open_window = _open_window diff --git a/tests/doc/check_examples_2.py b/tests/integration/examples/check_examples_2.py similarity index 100% rename from tests/doc/check_examples_2.py rename to tests/integration/examples/check_examples_2.py diff --git a/tests/doc/check_samples.py b/tests/integration/examples/check_samples.py similarity index 100% rename from tests/doc/check_samples.py rename to tests/integration/examples/check_samples.py diff --git a/tests/integration/examples/run_all_examples.py b/tests/integration/examples/run_all_examples.py new file mode 100644 index 000000000..2e293294a --- /dev/null +++ b/tests/integration/examples/run_all_examples.py @@ -0,0 +1,68 @@ +""" +Run All Examples + +If Python and Arcade are installed, this example can be run from the command line with: +python -m tests.test_examples.run_all_examples +""" +import subprocess +import os +import glob + +EXAMPLE_SUBDIR = "../../../arcade/examples" + + +def _get_short_name(fullpath): + return os.path.splitext(os.path.basename(fullpath))[0] + + +def _get_examples(start_path): + query_path = os.path.join(start_path, "*.py") + examples = glob.glob(query_path) + examples = [_get_short_name(e) for e in examples] + examples = [e for e in examples if e != "run_all_examples"] + examples = [e for e in examples if not e.startswith('_')] + examples = ["arcade.examples." + e for e in examples if not e.startswith('_')] + return examples + + +def run_examples(indices_in_range, index_skip_list): + """Run all examples in the arcade/examples directory""" + examples = _get_examples(EXAMPLE_SUBDIR) + examples.sort() + print(f"Found {len(examples)} examples in {EXAMPLE_SUBDIR}") + + file_path = os.path.dirname(os.path.abspath(__file__)) + print(file_path) + os.chdir(file_path+"/../..") + # run examples + for (idx, example) in enumerate(examples): + if indices_in_range is not None and idx not in indices_in_range: + continue + if index_skip_list is not None and idx in index_skip_list: + continue + print(f"=================== Example {idx + 1:3} of {len(examples)}: {example}") + # print('%s %s (index #%d of %d)' % ('=' * 20, example, idx, len(examples) - 1)) + + # Directly call venv, necessary for github action runner + cmd = 'python -m ' + example + + # print(cmd) + result = subprocess.check_output(cmd, shell=True) + if result: + print(f"ERROR: Got a result of: {result}.") + + +def all_examples(): + file_path = os.path.dirname(os.path.abspath(__file__)) + os.chdir(file_path) + + # Set an environment variable that will just run on_update() and on_draw() + # once, then quit. + os.environ['ARCADE_TEST'] = "TRUE" + + indices_in_range = None + index_skip_list = None + run_examples(indices_in_range, index_skip_list) + + +all_examples() diff --git a/tests/integration/examples/test_all_examples.py b/tests/integration/examples/test_all_examples.py new file mode 100644 index 000000000..437fec4ec --- /dev/null +++ b/tests/integration/examples/test_all_examples.py @@ -0,0 +1,78 @@ +""" +Run All Examples + +If Python and Arcade are installed, this example can be run from the command line with: +python -m tests.test_examples.test_all_examples +""" +import glob +import os +import subprocess + +import pytest + +EXAMPLE_SUBDIR = "../../../arcade/examples" +# These examples are allowed to print to stdout +ALLOW_STDOUT = set([ + "arcade.examples.dual_stick_shooter", + "arcade.examples.net_process_animal_facts", +]) + +def _get_short_name(fullpath): + return os.path.splitext(os.path.basename(fullpath))[0] + + +def _get_examples(start_path): + query_path = os.path.join(start_path, "*.py") + examples = glob.glob(query_path) + examples = [_get_short_name(e) for e in examples] + examples = [e for e in examples if e != "run_all_examples"] + examples = [e for e in examples if not e.startswith('_')] + examples = ["arcade.examples." + e for e in examples if not e.startswith('_')] + return examples + + +def find_examples(indices_in_range, index_skip_list): + """List all examples in the arcade/examples directory""" + examples = _get_examples(EXAMPLE_SUBDIR) + examples.sort() + print(f"Found {len(examples)} examples in {EXAMPLE_SUBDIR}") + + file_path = os.path.dirname(os.path.abspath(__file__)) + print(file_path) + os.chdir(f"{file_path}/../..") + + for (idx, example) in enumerate(examples): + if indices_in_range is not None and idx not in indices_in_range: + continue + if index_skip_list is not None and idx in index_skip_list: + continue + + allow_stdout = example in ALLOW_STDOUT + yield f'python -m {example}', allow_stdout + + +def list_examples(indices_in_range, index_skip_list): + file_path = os.path.dirname(os.path.abspath(__file__)) + os.chdir(file_path) + + return list(find_examples(indices_in_range, index_skip_list)) + + +@pytest.mark.parametrize( + "cmd, allow_stdout", + argvalues=list_examples( + indices_in_range=None, + index_skip_list=None + ) +) +def test_all(cmd, allow_stdout): + # Set an environment variable that will just run on_update() and on_draw() + # once, then quit. + import pyglet + test_env = os.environ.copy() + test_env["ARCADE_TEST"] = "TRUE" + + result = subprocess.check_output(cmd, shell=True, env=test_env) + if result and not allow_stdout: + print(f"ERROR: Got a result of: {result}.") + assert not result diff --git a/tests/integration/examples/test_examples.py b/tests/integration/examples/test_examples.py deleted file mode 100644 index e81e2f992..000000000 --- a/tests/integration/examples/test_examples.py +++ /dev/null @@ -1,64 +0,0 @@ -""" -Import and run all examples one frame -""" -import contextlib -import io -import inspect -import os -from importlib.machinery import SourceFileLoader -from pathlib import Path - -import arcade -import pytest - -# TODO: Also add platform_tutorial and gl -EXAMPLE_DIR = Path(arcade.__file__).parent / "examples" -# These examples are allowed to print to stdout -ALLOW_STDOUT = set([ - "arcade.examples.dual_stick_shooter", - "arcade.examples.net_process_animal_facts", -]) -IGNORE_PATTERNS = [ - 'net_process_animal_facts' -] - -def list_examples(): - for example in EXAMPLE_DIR.glob("*.py"): - if example.stem.startswith("_"): - continue - if example.stem in IGNORE_PATTERNS: - continue - yield f"arcade.examples.{example.stem}", example, True - - -def find_class_inheriting_from_window(module): - for name, obj in module.__dict__.items(): - match = inspect.isclass(obj) and issubclass(obj, arcade.Window) - if match: - return obj - return None - - -def find_main_function(module): - if "main" in module.__dict__: - return module.__dict__["main"] - return None - - -@pytest.mark.parametrize( - "module_path, file_path, allow_stdout", - list_examples(), -) -def test_examples(window_proxy, module_path, file_path, allow_stdout): - """Run all examples""" - os.environ["ARCADE_TEST"] = "TRUE" - - stdout = io.StringIO() - with contextlib.redirect_stdout(stdout): - # Manually load the module as __main__ so it runs on import - loader = SourceFileLoader("__main__", str(file_path)) - loader.exec_module(loader.load_module()) - - if not allow_stdout: - output = stdout.getvalue() - assert not output, f"Example {module_path} printed to stdout: {output}" diff --git a/tests/integration/tutorials/run_all_tutorials.py b/tests/integration/tutorials/run_all_tutorials.py new file mode 100644 index 000000000..3abd1c36f --- /dev/null +++ b/tests/integration/tutorials/run_all_tutorials.py @@ -0,0 +1,67 @@ +""" +Run All tutorials + +If Python and Arcade are installed, this tutorial can be run from the command line with: +python -m tests.test_tutorials.run_all_tutorials +""" +import glob +import subprocess +import os +from pathlib import Path + +TUTORIAL_SUBDIR = "../../../doc/tutorials/" + + +def _get_short_name(fullpath): + return os.path.splitext(os.path.basename(fullpath))[0] + +def _get_tutorials(start_path): + query_path = os.path.join(start_path, "*.py") + tutorials = glob.glob(query_path) + tutorials = [_get_short_name(e) for e in tutorials] + tutorials = [e for e in tutorials if e != "run_all_tutorials"] + tutorials = [e for e in tutorials if not e.startswith('_')] + tutorials = [f"doc.tutorials.{start_path.name}." + e for e in tutorials if not e.startswith('_')] + return tutorials + +def run_tutorials(indices_in_range = None, index_skip_list = None): + """Run all tutorials in the doc/tutorials directory""" + for tutorial_subdir in [path for path in list(Path.cwd().joinpath(TUTORIAL_SUBDIR).iterdir()) if path.is_dir()]: + tutorials = _get_tutorials(tutorial_subdir) + tutorials.sort() + print(f"Found {len(tutorials)} tutorials in {tutorial_subdir}") + + file_path = os.path.dirname(os.path.abspath(__file__)) + print(file_path) + os.chdir(file_path+"/../..") + # run tutorials + for (idx, tutorial) in enumerate(tutorials): + if indices_in_range is not None and idx not in indices_in_range: + continue + if index_skip_list is not None and idx in index_skip_list: + continue + print(f"=================== tutorial {idx + 1:3} of {len(tutorials)}: {tutorial}") + # print('%s %s (index #%d of %d)' % ('=' * 20, tutorial, idx, len(tutorials) - 1)) + + # Directly call venv, necessary for github action runner + cmd = 'python -m ' + tutorial + + # print(cmd) + result = subprocess.check_output(cmd, shell=True) + if result: + print(f"ERROR: Got a result of: {result}.") + +def all_tutorials(): + file_path = os.path.dirname(os.path.abspath(__file__)) + os.chdir(file_path) + + # Set an environment variable that will just run on_update() and on_draw() + # once, then quit. + os.environ['ARCADE_TEST'] = "TRUE" + + indices_in_range = None + index_skip_list = None + run_tutorials(indices_in_range, index_skip_list) + + +all_tutorials() diff --git a/tests/integration/tutorials/test_all_tutoirals.py b/tests/integration/tutorials/test_all_tutoirals.py new file mode 100644 index 000000000..aeb94419b --- /dev/null +++ b/tests/integration/tutorials/test_all_tutoirals.py @@ -0,0 +1,76 @@ +""" +Run All tutorials + +If Python and Arcade are installed, this tutorial can be run from the command line with: +python -m tests.test_tutorials.test_all_tutorials +""" +import glob +import os +import subprocess +from pathlib import Path + +import pytest + +TUTORIAL_SUBDIR = "../../../doc/tutorials/" +# These tutorials are allowed to print to stdout +ALLOW_STDOUT = set() + +def _get_short_name(fullpath): + return os.path.splitext(os.path.basename(fullpath))[0] + +def _get_tutorials(start_path): + query_path = os.path.join(start_path, "*.py") + tutorials = glob.glob(query_path) + tutorials = [_get_short_name(e) for e in tutorials] + tutorials = [e for e in tutorials if e != "run_all_tutorials"] + tutorials = [e for e in tutorials if not e.startswith('_')] + tutorials = [f"{e}.py" for e in tutorials if not e.startswith('_')] + return tutorials + +def find_tutorials(indices_in_range, index_skip_list): + """List all tutorials in the doc/tutorials directory""" + file_dir = Path(__file__).parent + for tutorial_subdir in [path for path in list((file_dir / TUTORIAL_SUBDIR).iterdir()) if path.is_dir()]: + tutorials = _get_tutorials(tutorial_subdir) + tutorials.sort() + print(f"Found {len(tutorials)} tutorials in {tutorial_subdir}") + if len(tutorials) == 0: + continue + print(tutorial_subdir) + # os.chdir(tutorial_subdir) + for (idx, tutorial) in enumerate(tutorials): + if indices_in_range is not None and idx not in indices_in_range: + continue + if index_skip_list is not None and idx in index_skip_list: + continue + + allow_stdout = tutorial in ALLOW_STDOUT + yield f'python {tutorial}', allow_stdout, tutorial_subdir + # os.chdir("../") + + +def list_tutorials(indices_in_range, index_skip_list): + file_path = os.path.dirname(os.path.abspath(__file__)) + os.chdir(file_path) + + return list(find_tutorials(indices_in_range, index_skip_list)) + + +@pytest.mark.parametrize( + "cmd, allow_stdout, tutorial_subdir", + argvalues=list_tutorials( + indices_in_range=None, + index_skip_list=None + ) +) +def test_all(cmd, allow_stdout, tutorial_subdir): + # Set an environment variable that will just run on_update() and on_draw() + # once, then quit. + import pyglet + test_env = os.environ.copy() + test_env["ARCADE_TEST"] = "TRUE" + os.chdir(tutorial_subdir) + result = subprocess.check_output(cmd, shell=True, env=test_env) + if result and not allow_stdout: + print(f"ERROR: Got a result of: {result}.") + assert not result diff --git a/tests/integration/tutorials/test_tutorials.py b/tests/integration/tutorials/test_tutorials.py deleted file mode 100644 index dd179b276..000000000 --- a/tests/integration/tutorials/test_tutorials.py +++ /dev/null @@ -1,47 +0,0 @@ -""" -FInd and run all tutorials in the doc/tutorials directory -""" -import io -import os -import contextlib -from importlib.machinery import SourceFileLoader -from pathlib import Path -import pytest -import arcade - -TUTORIAL_DIR = Path(arcade.__file__).parent.parent / "doc" /"tutorials" -ALLOW_STDOUT = {} - - -def find_tutorials(): - # Loop the directory of tutorials dirs - for dir in TUTORIAL_DIR.iterdir(): - if not dir.is_dir(): - continue - - print(dir) - # Find python files in each tutorial dir - for file in dir.glob("*.py"): - if file.stem.startswith("_"): - continue - # print("->", file) - yield file, file.stem in ALLOW_STDOUT - - -@pytest.mark.parametrize( - "file_path, allow_stdout", - find_tutorials(), -) -def test_tutorials(window_proxy, file_path, allow_stdout): - """Run all tutorials""" - os.environ["ARCADE_TEST"] = "TRUE" - stdout = io.StringIO() - with contextlib.redirect_stdout(stdout): - # Manually load the module as __main__ so it runs on import - os.chdir(file_path.parent) - loader = SourceFileLoader("__main__", str(file_path)) - loader.exec_module(loader.load_module()) - - if not allow_stdout: - output = stdout.getvalue() - assert not output, f"Example {file_path} printed to stdout: {output}" diff --git a/tests/unit/gl/test_opengl_framebuffer.py b/tests/unit/gl/test_opengl_framebuffer.py index a809945d5..7877556e7 100644 --- a/tests/unit/gl/test_opengl_framebuffer.py +++ b/tests/unit/gl/test_opengl_framebuffer.py @@ -55,8 +55,8 @@ def test_clear(ctx): ctx.window.use() fb = create(ctx, 10, 20, components=4) fb.clear() - fb.clear(color_normalized=(0, 0, 0, 0)) - fb.clear(color_normalized=(0, 0, 0)) + fb.clear(color=(0, 0, 0, 0), normalized=True) + fb.clear(color=(0, 0, 0), normalized=True) fb.clear(color=arcade.csscolor.AZURE) fb.clear(color=(0, 0, 0)) fb.clear(color=(0, 0, 0, 0)) @@ -156,10 +156,3 @@ def test_resize(ctx): fbo.resize() assert fbo.size == tex.size assert fbo.viewport == (0, 0, *fbo.size) - -def test_read_screen_framebuffer(window): - components = 3 - data = window.ctx.screen.read(components=components) - assert isinstance(data, bytes) - w, h = window.get_framebuffer_size() - assert len(data) == w * h * components diff --git a/tests/unit/gui/test_property.py b/tests/unit/gui/test_property.py index 9ac5cefad..3e48bfb7b 100644 --- a/tests/unit/gui/test_property.py +++ b/tests/unit/gui/test_property.py @@ -8,82 +8,37 @@ class MyObject: class Observer: - call_args = None - called = False + called = None - def call(self): - self.call_args = tuple() - self.called = True + def call(self, *args, **kwargs): + self.called = (args, kwargs) - def call_with_args(self, instance, value): - """Match expected signature of 2 parameters""" - self.call_args = (instance, value) - self.called = True - - def __call__(self, *args): - self.call_args = args - self.called = True + def __call__(self, *args, **kwargs): + self.called = (args, kwargs) def test_bind_callback(): observer = Observer() - my_obj = MyObject() - bind(my_obj, "name", observer.call) - - assert not observer.call_args - - # WHEN - my_obj.name = "New Name" - - assert observer.call_args == tuple() - - -def test_bind_callback_with_args(): - """ - A bound callback can have 0 or 2 arguments. - 0 arguments are used for simple callbacks, like `log_change`. - 2 arguments are used for callbacks that need to know the instance and the new value. - """ - observer = Observer() - - my_obj = MyObject() - bind(my_obj, "name", observer.call_with_args) - - assert not observer.call_args - - # WHEN - my_obj.name = "New Name" - - assert observer.call_args == (my_obj, "New Name") - - # Remove reference of call_args to my_obj, otherwise it will keep the object alive - observer.call_args = None - - -def test_bind_callback_with_star_args(): - observer = Observer() - my_obj = MyObject() bind(my_obj, "name", observer) + assert not observer.called + # WHEN my_obj.name = "New Name" - assert observer.call_args == (my_obj, "New Name") - - # Remove reference of call_args to my_obj, otherwise it will keep the object alive - observer.call_args = None + assert observer.called == (tuple(), {}) def test_unbind_callback(): observer = Observer() my_obj = MyObject() - bind(my_obj, "name", observer.call) + bind(my_obj, "name", observer) # WHEN - unbind(my_obj, "name", observer.call) + unbind(my_obj, "name", observer) my_obj.name = "New Name" assert not observer.called @@ -119,7 +74,7 @@ def test_does_not_trigger_if_value_unchanged(): observer = Observer() my_obj = MyObject() my_obj.name = "CONSTANT" - bind(my_obj, "name", observer.call) + bind(my_obj, "name", observer) assert not observer.called @@ -141,7 +96,7 @@ def test_gc_entries_are_collected(): del obj gc.collect() - # No leftovers + # No left overs assert len(MyObject.name.obs) == 0 diff --git a/tests/unit/sprite/test_sprite.py b/tests/unit/sprite/test_sprite.py index 1e88f6221..0ebcb2001 100644 --- a/tests/unit/sprite/test_sprite.py +++ b/tests/unit/sprite/test_sprite.py @@ -321,19 +321,15 @@ def test_visible(): assert sprite.alpha == 255 assert sprite.visible is True - # initialise alpha value - sprite.alpha = 100 - assert sprite.alpha == 100 - # Make invisible sprite.visible = False assert sprite.visible is False - assert sprite.alpha == 100 + assert sprite.alpha == 0 # Make visible again sprite.visible = True assert sprite.visible is True - assert sprite.alpha == 100 + assert sprite.alpha == 255 def test_sprite_scale_xy(window): diff --git a/tests/unit/sprite/test_sprite_texture_animation.py b/tests/unit/sprite/test_sprite_texture_animation.py index 7ed6b5a5a..5aca5afff 100644 --- a/tests/unit/sprite/test_sprite_texture_animation.py +++ b/tests/unit/sprite/test_sprite_texture_animation.py @@ -38,6 +38,18 @@ def test_animation(keyframes): """Test animation class""" anim = arcade.TextureAnimation(keyframes=keyframes) + # Add keyframe + anim.append_keyframe(arcade.TextureKeyframe(keyframes[0].texture, 1000)) + assert anim.num_frames == 9 + assert anim.duration_ms == 9000 + assert anim.duration_seconds == 9.0 + + # Remove keyframe + anim.remove_keyframe(8) + assert anim.num_frames == 8 + assert anim.duration_ms == 8000 + assert anim.duration_seconds == 8.0 + # Get keyframes at specific times (0.5s increments) for i in range(16): time = i / 2 From 6346f763d430b9bf888796d1f9766599ff93a8c9 Mon Sep 17 00:00:00 2001 From: DragonMoffon Date: Sat, 30 Mar 2024 02:34:22 +1300 Subject: [PATCH 85/94] update map_coordiantes to map_screen_to_world_coordinates --- arcade/camera/README.md | 89 +++++++++++++++++++ arcade/camera/camera_2d.py | 8 +- arcade/camera/data_types.py | 6 +- arcade/camera/default.py | 2 +- arcade/camera/orthographic.py | 6 +- arcade/gui/ui_manager.py | 2 +- tests/unit/camera/test_orthographic_camera.py | 30 +++---- 7 files changed, 122 insertions(+), 21 deletions(-) create mode 100644 arcade/camera/README.md diff --git a/arcade/camera/README.md b/arcade/camera/README.md new file mode 100644 index 000000000..fa2827d40 --- /dev/null +++ b/arcade/camera/README.md @@ -0,0 +1,89 @@ +# Arcade Camera + +This is an overview of how the new Arcade Cameras work. + +## Key Concepts + +### World Space +Whenever an object has a position within arcade that position is in world space. How much 1 unit in world +space represents is arbitrary. For example when a sprite has a scale of 1.0 then 1 unit in world space is +equal to one pixel of the sprite's source texture. This does not necessarily equate to one pixel on the screen. + +### Screen Space +The final positions of anything drawn to screen is in screen space. The mouse positions returned by window +events like `on_mouse_press` are also in screen space. Moving 1 unit in screen space is equivalent to moving +one pixel. Often positions in screen space are integer values, but this is not a strict rule. + +### View Matrices +The view matrix represents what part of world space should be focused on. It is made of three components. +The first is the position. This represents what world space position should be at (0, 0, 0). The second is +the forward vector. This is the direction which is considered forward and backwards in world space. the +final component is the up vector. Which determines what world space positions are upwards or downwards in +world space. + +The goal of the view matrix is to prepare everything in world space for projection into screen space. It +achieves this by applying its three components to every world space position. In the end any object with +a world space position equal to the view matrix position will be at (0, 0, 0). Any object along the forward +vector after moving will be placed along the z-axis, and any object along the up vector will be place along +the y-axis. This transformation moves the objects from screen space into view space. Importantly one unit in +world space is equal to one unit in view space + +### Projection Matrices +The projection matrix takes the positions of objects in view space and projects them into screen space. +depending on the type of projection matrix how this exactly applies changes. Projection matrices along +do not fully project objects into screen space, instead they transform positions into unit space. This +special coordinate space ranges from -1 to 1 in the x, y, and z axis. Anything within this range will +be transformed into screen space, and everything outside this range is discarded and left undrawn. +you can conceptualise projection matrices as taking a 6 sided 3D prism volume in view space and +squashing it down into a uniformly sized cube. In every case the closest position projected along the +z-axis is given by the near value, while the furthest is given by the far value. + +#### orthographic +In an orthographic projection the distance from the origin does not impact how much a position gets projected. +This type of projection can be visualised as a rectangular prism with a set width, height, and depth +determined by left, right, bottom, top, near, far values. These values tell you the bounding box of positions +in view space which get projected. + +#### perspective +In an orthographic projection the distance from the origin directly impacts how much a position is projected. +This type of projection can be visualised as a rectangular prism with the sharp end removed. This shape means +that more positions further away from the origin will be squashed down into unit space. This makes objects +that are further away appear smaller. The shape of the prism is determined by an aspect ratio, the field of view, +and the near and far values. The aspect ratio defines the ratio between the height and width of the projection. +The field of view is half of the angle used to determine the height of the projection at a particular depth. + +### Viewports +The final concept to cover is the viewport. This is the pixel area which the unit space will scale to. The ratio +between the size of the viewport and the size of the projection determines the relationship between units in +world space and pixels in screen space. For example if width and height of an orthographic projection is equal +to the width and height of the viewport then one unit in world space will equal one pixel in screen space. This +is the default for arcade. + +The viewport also defines which pixels get drawn to in the final image. Generally this is equal to the entire +screen, but it is possible to draw to only a specific area by defining the right viewport. Note that doing this +will change the ratio of the viewport and projection, so ensure that they match if you would like to keep the same +unit to pixel ratio. Any position outside the viewport which would normally be a valid pixel position will +not be drawn. + +## Key Objects + +- Objects which modify the view and perspective matrices are called "Projectors" + - `arcade.camera.Projector` is a `Protocol` used internally by arcade + - `Projector.use()` sets the internal projection and view matrices used by Arcade and Pyglet + - `Projector.activate()` is the same as use, but works within a context manager using the `with` syntax + - `Projector.map_screen_to_world_coordinate(screen_coordinate, depth)` +provides a way to find the world position of any pixel position on screen. +- There are multiple data types which provide the information required to make view and projection matrices + - `camera.CameraData` holds the position, forward, and up vectors along with a zoom value used to create the +view matrix + - `camera.OrthographicProjectionData` holds the left, right, bottom, top, near, far values needed to create a +orthographic projection matrix + - `camera.PerspectiveProjectionData` holds the aspect ratio, field of view, near and far needed to create a +perspective projection matrix. + - both ProjectionData data types also provide a viewport for setting the draw area when using the camera. +- There are three primary `Projectors` in `arcade.camera` + - `arcade.camera.Camera2D` is locked to the x-y plane and is perfect for most use cases within arcade. + - `arcade.camera.OrthographicProjector` can be freely positioned in 3D space, but the scale of objects does not +depend on the distance to the projector + - [not yet implemented ] `arcade.camera.PerspectiveProjector` can be freely position in 3D space, +and objects look smaller the further from the camera they are diff --git a/arcade/camera/camera_2d.py b/arcade/camera/camera_2d.py index a4598d73c..3b59283ce 100644 --- a/arcade/camera/camera_2d.py +++ b/arcade/camera/camera_2d.py @@ -716,7 +716,11 @@ def activate(self) -> Iterator[Projector]: previous_framebuffer.use() previous_projection.use() - def map_coordinate(self, screen_coordinate: Tuple[float, float], depth: float = 0.0) -> Tuple[float, float]: + def map_screen_to_world_coordinate( + self, + screen_coordinate: Tuple[float, float], + depth: float = 0.0 + ) -> Tuple[float, float]: """ Take in a pixel coordinate from within the range of the window size and returns @@ -735,4 +739,4 @@ def map_coordinate(self, screen_coordinate: Tuple[float, float], depth: float = of the camera. """ - return self._ortho_projector.map_coordinate(screen_coordinate, depth)[:2] + return self._ortho_projector.map_screen_to_world_coordinate(screen_coordinate, depth)[:2] diff --git a/arcade/camera/data_types.py b/arcade/camera/data_types.py index 89f918419..0891b9934 100644 --- a/arcade/camera/data_types.py +++ b/arcade/camera/data_types.py @@ -161,7 +161,11 @@ def use(self) -> None: def activate(self) -> Iterator[Projector]: ... - def map_coordinate(self, screen_coordinate: Tuple[float, float], depth: float = 0.0) -> Tuple[float, ...]: + def map_screen_to_world_coordinate( + self, + screen_coordinate: Tuple[float, float], + depth: float = 0.0 + ) -> Tuple[float, ...]: ... diff --git a/arcade/camera/default.py b/arcade/camera/default.py index 1be632122..0b2ed43f0 100644 --- a/arcade/camera/default.py +++ b/arcade/camera/default.py @@ -75,7 +75,7 @@ def activate(self) -> Iterator[Projector]: finally: previous.use() - def map_coordinate(self, screen_coordinate: Tuple[float, float], depth = 0.0) -> Tuple[float, float]: + def map_screen_to_world_coordinate(self, screen_coordinate: Tuple[float, float], depth=0.0) -> Tuple[float, float]: """ Map the screen pos to screen_coordinates. diff --git a/arcade/camera/orthographic.py b/arcade/camera/orthographic.py index c3ac50363..8eea9cf60 100644 --- a/arcade/camera/orthographic.py +++ b/arcade/camera/orthographic.py @@ -151,7 +151,11 @@ def activate(self) -> Iterator[Projector]: finally: previous_projector.use() - def map_coordinate(self, screen_coordinate: Tuple[float, float], depth: float = 0.0) -> Tuple[float, float, float]: + def map_screen_to_world_coordinate( + self, + screen_coordinate: Tuple[float, float], + depth: float = 0.0 + ) -> Tuple[float, float, float]: """ Take in a pixel coordinate from within the range of the window size and returns diff --git a/arcade/gui/ui_manager.py b/arcade/gui/ui_manager.py index fca3d0b98..b3a31f73e 100644 --- a/arcade/gui/ui_manager.py +++ b/arcade/gui/ui_manager.py @@ -338,7 +338,7 @@ def adjust_mouse_coordinates(self, x, y): It uses the internal camera's map_coordinate methods, and should work with all transformations possible with the basic orthographic camera. """ - return self.window.current_camera.map_coordinate((x, y))[:2] + return self.window.current_camera.map_screen_to_world_coordinate((x, y))[:2] def on_event(self, event) -> Union[bool, None]: layers = sorted(self.children.keys(), reverse=True) diff --git a/tests/unit/camera/test_orthographic_camera.py b/tests/unit/camera/test_orthographic_camera.py index d302a1e2e..843e39ed4 100644 --- a/tests/unit/camera/test_orthographic_camera.py +++ b/tests/unit/camera/test_orthographic_camera.py @@ -54,9 +54,9 @@ def test_orthographic_projector_map_coordinates(window: Window): mouse_pos_c = (230.0, 800.0) # Then - assert ortho_camera.map_coordinate(mouse_pos_a) == pytest.approx((100.0, 100.0, 0.0)) - assert ortho_camera.map_coordinate(mouse_pos_b) == pytest.approx((100.0, 0.0, 0.0)) - assert ortho_camera.map_coordinate(mouse_pos_c) == pytest.approx((230.0, 800.0, 0.0)) + assert ortho_camera.map_screen_to_world_coordinate(mouse_pos_a) == pytest.approx((100.0, 100.0, 0.0)) + assert ortho_camera.map_screen_to_world_coordinate(mouse_pos_b) == pytest.approx((100.0, 0.0, 0.0)) + assert ortho_camera.map_screen_to_world_coordinate(mouse_pos_c) == pytest.approx((230.0, 800.0, 0.0)) def test_orthographic_projector_map_coordinates_move(window: Window): @@ -71,8 +71,8 @@ def test_orthographic_projector_map_coordinates_move(window: Window): default_view.position = (0.0, 0.0, 0.0) # Then - assert ortho_camera.map_coordinate(mouse_pos_a) == pytest.approx((0.0, 0.0, 0.0)) - assert ortho_camera.map_coordinate(mouse_pos_b) == pytest.approx((-300.0, -200.0, 0.0)) + assert ortho_camera.map_screen_to_world_coordinate(mouse_pos_a) == pytest.approx((0.0, 0.0, 0.0)) + assert ortho_camera.map_screen_to_world_coordinate(mouse_pos_b) == pytest.approx((-300.0, -200.0, 0.0)) # And @@ -80,8 +80,8 @@ def test_orthographic_projector_map_coordinates_move(window: Window): default_view.position = (100.0, 100.0, 0.0) # Then - assert ortho_camera.map_coordinate(mouse_pos_a) == pytest.approx((100.0, 100.0, 0.0)) - assert ortho_camera.map_coordinate(mouse_pos_b) == pytest.approx((-200.0, -100.0, 0.0)) + assert ortho_camera.map_screen_to_world_coordinate(mouse_pos_a) == pytest.approx((100.0, 100.0, 0.0)) + assert ortho_camera.map_screen_to_world_coordinate(mouse_pos_b) == pytest.approx((-200.0, -100.0, 0.0)) def test_orthographic_projector_map_coordinates_rotate(window: Window): @@ -97,8 +97,8 @@ def test_orthographic_projector_map_coordinates_rotate(window: Window): default_view.position = (0.0, 0.0, 0.0) # Then - assert ortho_camera.map_coordinate(mouse_pos_a) == pytest.approx((0.0, 0.0, 0.0)) - assert ortho_camera.map_coordinate(mouse_pos_b) == pytest.approx((-200.0, 300.0, 0.0)) + assert ortho_camera.map_screen_to_world_coordinate(mouse_pos_a) == pytest.approx((0.0, 0.0, 0.0)) + assert ortho_camera.map_screen_to_world_coordinate(mouse_pos_b) == pytest.approx((-200.0, 300.0, 0.0)) # And @@ -107,8 +107,8 @@ def test_orthographic_projector_map_coordinates_rotate(window: Window): default_view.position = (100.0, 100.0, 0.0) # Then - assert ortho_camera.map_coordinate(mouse_pos_a) == pytest.approx((100.0, 100.0, 0.0)) - assert ortho_camera.map_coordinate(mouse_pos_b) == pytest.approx((-253.553390, 170.710678, 0.0)) + assert ortho_camera.map_screen_to_world_coordinate(mouse_pos_a) == pytest.approx((100.0, 100.0, 0.0)) + assert ortho_camera.map_screen_to_world_coordinate(mouse_pos_b) == pytest.approx((-253.553390, 170.710678, 0.0)) def test_orthographic_projector_map_coordinates_zoom(window: Window): @@ -123,8 +123,8 @@ def test_orthographic_projector_map_coordinates_zoom(window: Window): default_view.zoom = 2.0 # Then - assert ortho_camera.map_coordinate(mouse_pos_a) == pytest.approx((window.width*0.75, window.height*0.75, 0.0)) - assert ortho_camera.map_coordinate(mouse_pos_b) == pytest.approx((250.0, 200.0, 0.0)) + assert ortho_camera.map_screen_to_world_coordinate(mouse_pos_a) == pytest.approx((window.width*0.75, window.height*0.75, 0.0)) + assert ortho_camera.map_screen_to_world_coordinate(mouse_pos_b) == pytest.approx((250.0, 200.0, 0.0)) # And @@ -133,5 +133,5 @@ def test_orthographic_projector_map_coordinates_zoom(window: Window): default_view.zoom = 0.25 # Then - assert ortho_camera.map_coordinate(mouse_pos_a) == pytest.approx((window.width*2.0, window.height*2.0, 0.0)) - assert ortho_camera.map_coordinate(mouse_pos_b) == pytest.approx((-1200.0, -800.0, 0.0)) \ No newline at end of file + assert ortho_camera.map_screen_to_world_coordinate(mouse_pos_a) == pytest.approx((window.width*2.0, window.height*2.0, 0.0)) + assert ortho_camera.map_screen_to_world_coordinate(mouse_pos_b) == pytest.approx((-1200.0, -800.0, 0.0)) \ No newline at end of file From 1beb25b00ee988cbd4d552fa98e7600c3ebbaed8 Mon Sep 17 00:00:00 2001 From: DragonMoffon Date: Sat, 30 Mar 2024 02:35:42 +1300 Subject: [PATCH 86/94] Adding a temporary README to cameras for documentation --- arcade/camera/simple_camera.py | 32 +++++++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/arcade/camera/simple_camera.py b/arcade/camera/simple_camera.py index 98b57f9c0..93119ca97 100644 --- a/arcade/camera/simple_camera.py +++ b/arcade/camera/simple_camera.py @@ -347,18 +347,48 @@ def map_coordinate(self, screen_coordinate: Tuple[float, float], depth: float = the range of the viewport and returns the world space coordinates. + .. deprecated:: 3.0 + Use :meth:`.map_screen_to_world_coordinate` instead. + + Essentially reverses the effects of the projector. + + Args: + screen_coordinate: A 2D position in pixels from the bottom left of the screen. + This should ALWAYS be in the range of 0.0 - screen size. + depth: The depth value which is mapped along with the screen coordinates. Because of how + Orthographic perspectives work this does not impact how the screen_coordinates are mapped. + Returns: + A 2D vector (Along the XY plane) in world space (same as sprites). + perfect for finding if the mouse overlaps with a sprite or ui element irrespective + of the camera. + """ + + return self._camera.map_screen_to_world_coordinate(screen_coordinate, depth)[:2] + + def map_screen_to_world_coordinate( + self, + screen_coordinate: Tuple[float, float], + depth: float = 0.0 + ) -> Tuple[float, float]: + """ + Take in a pixel coordinate from within + the range of the window size and returns + the world space coordinates. + Essentially reverses the effects of the projector. Args: screen_coordinate: A 2D position in pixels from the bottom left of the screen. This should ALWAYS be in the range of 0.0 - screen size. + depth: The depth value which is mapped along with the screen coordinates. Because of how + Orthographic perspectives work this does not impact how the screen_coordinates are mapped. Returns: A 2D vector (Along the XY plane) in world space (same as sprites). perfect for finding if the mouse overlaps with a sprite or ui element irrespective of the camera. """ - return self._camera.map_coordinate(screen_coordinate, depth)[:2] + return self._camera.map_screen_to_world_coordinate(screen_coordinate, depth)[:2] def resize(self, viewport_width: int, viewport_height: int, *, resize_projection: bool = True) -> None: From 4cc94cfdabb16ac3b79782ae9cd038bf2c76f922 Mon Sep 17 00:00:00 2001 From: DragonMoffon Date: Sat, 30 Mar 2024 02:46:49 +1300 Subject: [PATCH 87/94] Completed clean-up and sanity check --- arcade/camera/default.py | 3 +-- arcade/camera/orthographic.py | 2 +- arcade/camera/simple_camera.py | 3 +++ arcade/camera/types.py | 0 4 files changed, 5 insertions(+), 3 deletions(-) delete mode 100644 arcade/camera/types.py diff --git a/arcade/camera/default.py b/arcade/camera/default.py index 0b2ed43f0..71dfee10a 100644 --- a/arcade/camera/default.py +++ b/arcade/camera/default.py @@ -92,9 +92,8 @@ class DefaultProjector(ViewportProjector): 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. - :param window: The window to bind the camera to. Defaults to the currently active camera. + :param window: The window to bind the camera to. Defaults to the currently active window. """ - # TODO: ADD PARAMS TO DOC FOR __init__ def __init__(self, *, window: Optional["Window"] = None): super().__init__(window=window) diff --git a/arcade/camera/orthographic.py b/arcade/camera/orthographic.py index 8eea9cf60..addd16fae 100644 --- a/arcade/camera/orthographic.py +++ b/arcade/camera/orthographic.py @@ -80,7 +80,7 @@ def _generate_projection_matrix(self) -> Mat4: 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. + the pixels uniform in size. Avoid a zoom of 0.0. """ # Find the center of the projection values (often 0,0 or the center of the screen) diff --git a/arcade/camera/simple_camera.py b/arcade/camera/simple_camera.py index 93119ca97..3297bdcd1 100644 --- a/arcade/camera/simple_camera.py +++ b/arcade/camera/simple_camera.py @@ -27,6 +27,9 @@ class SimpleCamera: Initialize a Simple Camera Instance with either Camera PoDs or individual arguments + .. depreciated:: 3.0 + use :cls:`.Camera2D` instead + :param window: The Arcade Window to bind the camera to. Defaults to the currently active window. :param viewport: A 4-int tuple which defines the pixel bounds which the camera with project to. diff --git a/arcade/camera/types.py b/arcade/camera/types.py deleted file mode 100644 index e69de29bb..000000000 From f0e657ca0ef1b1067d6be4936f839c79ea2c414b Mon Sep 17 00:00:00 2001 From: DragonMoffon Date: Sat, 30 Mar 2024 02:51:01 +1300 Subject: [PATCH 88/94] Revert "Revert "Merge branch 'development' into Camera-Overhaul-Simple"" This reverts commit 788072e26f25819be7c5d84fe5550e3cc02a3122. --- .gitignore | 3 + arcade/application.py | 60 ++-- arcade/examples/array_backed_grid.py | 1 - arcade/examples/dual_stick_shooter.py | 9 +- arcade/examples/gl/3d_cube_with_cubes.py | 2 +- arcade/examples/light_demo.py | 6 +- arcade/examples/minimap.py | 2 +- arcade/examples/minimap_camera.py | 2 +- arcade/examples/particle_fireworks.py | 8 +- arcade/examples/particle_systems.py | 8 +- arcade/examples/performance_statistics.py | 1 - arcade/examples/perspective.py | 7 +- arcade/examples/pymunk_joint_builder.py | 8 +- arcade/examples/sections_demo_1.py | 2 +- arcade/examples/sections_demo_2.py | 31 +- arcade/examples/sprite_animated_keyframes.py | 7 +- arcade/examples/sprite_explosion_bitmapped.py | 2 +- arcade/examples/sprite_minimal.py | 6 +- arcade/examples/sprite_move_keyboard_accel.py | 2 +- arcade/examples/tetris.py | 2 +- arcade/examples/transform_feedback.py | 6 +- arcade/experimental/atlas_render_into.py | 4 +- arcade/experimental/clock/clock_window.py | 33 +- arcade/experimental/depth_of_field.py | 239 +++++++++++++ arcade/experimental/lights.py | 2 +- .../sprite_collect_coins_minimap.py | 2 +- arcade/experimental/texture_render_target.py | 2 +- arcade/gl/buffer.py | 2 +- arcade/gl/framebuffer.py | 88 +++-- arcade/gui/events.py | 12 +- arcade/gui/experimental/scroll_area.py | 4 +- arcade/gui/property.py | 48 ++- arcade/gui/ui_manager.py | 13 +- arcade/gui/widgets/__init__.py | 23 +- arcade/gui/widgets/text.py | 8 +- arcade/perf_graph.py | 2 +- arcade/shape_list.py | 3 +- arcade/sprite/animated.py | 28 +- arcade/sprite/base.py | 87 +++-- arcade/sprite_list/sprite_list.py | 2 +- arcade/text.py | 2 +- arcade/window_commands.py | 2 +- doc/_static/checkered.png | Bin 0 -> 4885 bytes doc/_static/css/custom.css | 124 +++++-- doc/conf.py | 84 +++-- .../how_to_examples/gui_flat_button.rst | 26 ++ .../gui_flat_button_positioned.png | Bin 0 -> 6843 bytes doc/get_started/install/index.rst | 12 + doc/index.rst | 322 +++++++++--------- doc/programming_guide/gui/concept.rst | 4 +- doc/programming_guide/sound.rst | 2 +- doc/tutorials/raycasting/example.py | 6 +- doc/tutorials/shader_inputs/texture_write.py | 2 +- doc/tutorials/shader_inputs/textures.py | 4 +- doc/tutorials/shader_toy_particles/index.rst | 3 +- pyproject.toml | 18 +- tests/conftest.py | 138 +++++++- .../examples => doc}/check_examples_2.py | 0 .../examples => doc}/check_samples.py | 0 .../integration/examples/run_all_examples.py | 68 ---- .../integration/examples/test_all_examples.py | 78 ----- tests/integration/examples/test_examples.py | 64 ++++ .../tutorials/run_all_tutorials.py | 67 ---- .../tutorials/test_all_tutoirals.py | 76 ----- tests/integration/tutorials/test_tutorials.py | 47 +++ tests/unit/gl/test_opengl_framebuffer.py | 11 +- tests/unit/gui/test_property.py | 69 +++- tests/unit/sprite/test_sprite.py | 8 +- .../sprite/test_sprite_texture_animation.py | 12 - 69 files changed, 1258 insertions(+), 768 deletions(-) create mode 100644 arcade/experimental/depth_of_field.py create mode 100644 doc/_static/checkered.png create mode 100644 doc/example_code/how_to_examples/gui_flat_button_positioned.png rename tests/{integration/examples => doc}/check_examples_2.py (100%) rename tests/{integration/examples => doc}/check_samples.py (100%) delete mode 100644 tests/integration/examples/run_all_examples.py delete mode 100644 tests/integration/examples/test_all_examples.py create mode 100644 tests/integration/examples/test_examples.py delete mode 100644 tests/integration/tutorials/run_all_tutorials.py delete mode 100644 tests/integration/tutorials/test_all_tutoirals.py create mode 100644 tests/integration/tutorials/test_tutorials.py diff --git a/.gitignore b/.gitignore index 5b1be3dc3..aff91edf2 100644 --- a/.gitignore +++ b/.gitignore @@ -61,3 +61,6 @@ temp/ *.tiled-session doc/api_docs/api/*.rst + +# Pyenv local +.python-version diff --git a/arcade/application.py b/arcade/application.py index 4c463e425..44b7d95d0 100644 --- a/arcade/application.py +++ b/arcade/application.py @@ -20,7 +20,7 @@ from arcade import set_window from arcade.color import TRANSPARENT_BLACK from arcade.context import ArcadeContext -from arcade.types import Color, RGBA255, RGBA255OrNormalized +from arcade.types import Color, RGBOrA255, RGBANormalized from arcade import SectionManager from arcade.utils import is_raspberry_pi from arcade.camera import Projector @@ -261,10 +261,10 @@ def ctx(self) -> ArcadeContext: return self._ctx def clear( - self, - color: Optional[RGBA255OrNormalized] = None, - normalized: bool = False, - viewport: Optional[Tuple[int, int, int, int]] = None, + self, + color: Optional[RGBOrA255] = None, + color_normalized: Optional[RGBANormalized] = None, + viewport: Optional[Tuple[int, int, int, int]] = None, ): """Clears the window with the configured background color set through :py:attr:`arcade.Window.background_color`. @@ -273,14 +273,18 @@ def clear( with one of the following: 1. A :py:class:`~arcade.types.Color` instance - 2. A 4-length RGBA :py:class:`tuple` of byte values (0 to 255) - 3. A 4-length RGBA :py:class:`tuple` of normalized floats (0.0 to 1.0) + 2. A 3 or 4-length RGB/RGBA :py:class:`tuple` of byte values (0 to 255) + + :param color_normalized: (Optional) override the current background color + using normalized values (0.0 to 1.0). For example, (1.0, 0.0, 0.0, 1.0) + making the window contents red. - :param normalized: If the color format is normalized (0.0 -> 1.0) or byte values :param Tuple[int, int, int, int] viewport: The viewport range to clear """ - color = color if color is not None else self.background_color - self.ctx.screen.clear(color, normalized=normalized, viewport=viewport) + # Use the configured background color if none is provided + if color is None and color_normalized is None: + color = self.background_color + self.ctx.screen.clear(color=color, color_normalized=color_normalized, viewport=viewport) @property def background_color(self) -> Color: @@ -298,7 +302,7 @@ def background_color(self) -> Color: MY_RED = arcade.types.Color(255, 0, 0) window.background_color = MY_RED - # Set the backgrund color directly from an RGBA tuple + # Set the background color directly from an RGBA tuple window.background_color = 255, 0, 0, 255 # (Discouraged) @@ -311,7 +315,7 @@ def background_color(self) -> Color: return self._background_color @background_color.setter - def background_color(self, value: RGBA255): + def background_color(self, value: RGBOrA255): self._background_color = Color.from_iterable(value) def run(self) -> None: @@ -475,7 +479,7 @@ def on_mouse_scroll(self, x: int, y: int, scroll_x: int, scroll_y: int): Override this function to respond to scroll events. The scroll arguments may be positive or negative to indicate direction, but - the units are unstandardized. How many scroll steps you recieve + the units are unstandardized. How many scroll steps you receive may vary wildly between computers depending a number of factors, including system settings and the input devices used (i.e. mouse scrollwheel, touchpad, etc). @@ -560,7 +564,7 @@ def on_key_release(self, symbol: int, modifiers: int): Situations that require handling key releases include: - * Rythm games where a note must be held for a certain + * Rhythm games where a note must be held for a certain amount of time * 'Charging up' actions that change strength depending on how long a key was pressed @@ -722,10 +726,14 @@ def show_view(self, new_view: 'View'): # Store the Window that is showing the "new_view" View. if new_view.window is None: new_view.window = self - elif new_view.window != self: - raise RuntimeError("You are attempting to pass the same view " - "object between multiple windows. A single " - "view object can only be used in one window.") + # NOTE: This is not likely to happen and is creating issues for the test suite. + # elif new_view.window != self: + # raise RuntimeError(( + # "You are attempting to pass the same view " + # "object between multiple windows. A single " + # "view object can only be used in one window. " + # f"{self} != {new_view.window}" + # )) # remove previously shown view's handlers if self._current_view is not None: @@ -975,24 +983,26 @@ def add_section(self, section, at_index: Optional[int] = None, at_draw_order: Op def clear( self, - color: Optional[RGBA255OrNormalized] = None, - normalized: bool = False, + color: Optional[RGBOrA255] = None, + color_normalized: Optional[RGBANormalized] = None, viewport: Optional[Tuple[int, int, int, int]] = None, ): - """Clears the View's Window with the configured background color + """Clears the window with the configured background color set through :py:attr:`arcade.Window.background_color`. :param color: (Optional) override the current background color with one of the following: 1. A :py:class:`~arcade.types.Color` instance - 2. A 4-length RGBA :py:class:`tuple` of byte values (0 to 255) - 3. A 4-length RGBA :py:class:`tuple` of normalized floats (0.0 to 1.0) + 2. A 3 or 4-length RGB/RGBA :py:class:`tuple` of byte values (0 to 255) + + :param color_normalized: (Optional) override the current background color + using normalized values (0.0 to 1.0). For example, (1.0, 0.0, 0.0, 1.0) + making the window contents red. - :param normalized: If the color format is normalized (0.0 -> 1.0) or byte values :param Tuple[int, int, int, int] viewport: The viewport range to clear """ - self.window.clear(color, normalized, viewport) + self.window.clear(color=color, color_normalized=color_normalized, viewport=viewport) def on_update(self, delta_time: float): """To be overridden""" diff --git a/arcade/examples/array_backed_grid.py b/arcade/examples/array_backed_grid.py index f84fcda98..324737646 100644 --- a/arcade/examples/array_backed_grid.py +++ b/arcade/examples/array_backed_grid.py @@ -104,7 +104,6 @@ def on_mouse_press(self, x, y, button, modifiers): def main(): - MyGame(SCREEN_WIDTH, SCREEN_HEIGHT, SCREEN_TITLE) arcade.run() diff --git a/arcade/examples/dual_stick_shooter.py b/arcade/examples/dual_stick_shooter.py index c45f8fc16..e34dd1f63 100644 --- a/arcade/examples/dual_stick_shooter.py +++ b/arcade/examples/dual_stick_shooter.py @@ -338,6 +338,11 @@ def on_draw(self): anchor_y="center") -if __name__ == "__main__": + +def main(): game = MyGame(SCREEN_WIDTH, SCREEN_HEIGHT, SCREEN_TITLE) - arcade.run() + game.run() + + +if __name__ == "__main__": + main() diff --git a/arcade/examples/gl/3d_cube_with_cubes.py b/arcade/examples/gl/3d_cube_with_cubes.py index 09663cdaf..803193bc8 100644 --- a/arcade/examples/gl/3d_cube_with_cubes.py +++ b/arcade/examples/gl/3d_cube_with_cubes.py @@ -110,7 +110,7 @@ def on_draw(self): # Draw the current cube using the last one as a texture self.fbo1.use() - self.fbo1.clear(color=(1.0, 1.0, 1.0, 1.0), normalized=True) + self.fbo1.clear(color_normalized=(1.0, 1.0, 1.0, 1.0)) translate = Mat4.from_translation((0, 0, -1.75)) rx = Mat4.from_rotation(self.time, (1, 0, 0)) diff --git a/arcade/examples/light_demo.py b/arcade/examples/light_demo.py index 07d07ec57..cedd091a1 100644 --- a/arcade/examples/light_demo.py +++ b/arcade/examples/light_demo.py @@ -300,7 +300,11 @@ def on_update(self, delta_time): self.scroll_screen() -if __name__ == "__main__": +def main(): window = MyGame(SCREEN_WIDTH, SCREEN_HEIGHT, SCREEN_TITLE) window.setup() arcade.run() + + +if __name__ == "__main__": + main() diff --git a/arcade/examples/minimap.py b/arcade/examples/minimap.py index aeeea4784..baf2628b3 100644 --- a/arcade/examples/minimap.py +++ b/arcade/examples/minimap.py @@ -110,7 +110,7 @@ def setup(self): def update_minimap(self): proj = 0, MAP_WIDTH, 0, MAP_HEIGHT with self.minimap_sprite_list.atlas.render_into(self.minimap_texture, projection=proj) as fbo: - fbo.clear(MINIMAP_BACKGROUND_COLOR) + fbo.clear(color=MINIMAP_BACKGROUND_COLOR) self.wall_list.draw() self.player_sprite.draw() diff --git a/arcade/examples/minimap_camera.py b/arcade/examples/minimap_camera.py index 3d9df371a..806034f24 100644 --- a/arcade/examples/minimap_camera.py +++ b/arcade/examples/minimap_camera.py @@ -126,7 +126,7 @@ def on_draw(self): # Draw new minimap using the camera self.camera_minimap.use() - self.clear(MINIMAP_BACKGROUND_COLOR) + self.clear(color=MINIMAP_BACKGROUND_COLOR) self.wall_list.draw() self.player_list.draw() diff --git a/arcade/examples/particle_fireworks.py b/arcade/examples/particle_fireworks.py index cb5f98029..361e83377 100644 --- a/arcade/examples/particle_fireworks.py +++ b/arcade/examples/particle_fireworks.py @@ -362,6 +362,10 @@ def rocket_smoke_mutator(particle: LifetimeParticle): particle.scale = lerp(0.5, 3.0, particle.lifetime_elapsed / particle.lifetime_original) -if __name__ == "__main__": +def main(): app = FireworksApp() - arcade.run() + app.run() + + +if __name__ == "__main__": + main() diff --git a/arcade/examples/particle_systems.py b/arcade/examples/particle_systems.py index 36590ecc7..d1ac99efe 100644 --- a/arcade/examples/particle_systems.py +++ b/arcade/examples/particle_systems.py @@ -766,6 +766,10 @@ def on_key_press(self, key, modifiers): arcade.close_window() -if __name__ == "__main__": +def main(): game = MyGame() - arcade.run() + game.run() + + +if __name__ == "__main__": + main() diff --git a/arcade/examples/performance_statistics.py b/arcade/examples/performance_statistics.py index 3e02cbe00..7b5137be6 100644 --- a/arcade/examples/performance_statistics.py +++ b/arcade/examples/performance_statistics.py @@ -40,7 +40,6 @@ COIN_COUNT = 1500 - # Turn on tracking for the number of event handler # calls and the average execution time of each type. arcade.enable_timings() diff --git a/arcade/examples/perspective.py b/arcade/examples/perspective.py index ee27ba49d..32a04f581 100644 --- a/arcade/examples/perspective.py +++ b/arcade/examples/perspective.py @@ -148,4 +148,9 @@ def on_resize(self, width: int, height: int): self.program["projection"] = Mat4.perspective_projection(self.aspect_ratio, 0.1, 100, fov=75) -Perspective().run() +def main(): + Perspective().run() + + +if __name__ == "__main__": + main() diff --git a/arcade/examples/pymunk_joint_builder.py b/arcade/examples/pymunk_joint_builder.py index 1c45230e1..1548a75a6 100644 --- a/arcade/examples/pymunk_joint_builder.py +++ b/arcade/examples/pymunk_joint_builder.py @@ -314,6 +314,10 @@ def on_update(self, delta_time): self.processing_time = timeit.default_timer() - start_time -window = MyApplication(SCREEN_WIDTH, SCREEN_HEIGHT, SCREEN_TITLE) +def main(): + window = MyApplication(SCREEN_WIDTH, SCREEN_HEIGHT, SCREEN_TITLE) + window.run() -arcade.run() + +if __name__ == "__main__": + main() diff --git a/arcade/examples/sections_demo_1.py b/arcade/examples/sections_demo_1.py index 39b9e7adf..17f848e31 100644 --- a/arcade/examples/sections_demo_1.py +++ b/arcade/examples/sections_demo_1.py @@ -127,7 +127,7 @@ def __init__(self): def on_draw(self): # clear the screen - self.clear(arcade.color.BEAU_BLUE) + self.clear(color=arcade.color.BEAU_BLUE) # draw a line separating each Section arcade.draw_line(self.window.width / 2, 0, self.window.width / 2, diff --git a/arcade/examples/sections_demo_2.py b/arcade/examples/sections_demo_2.py index c2962f353..05fef3090 100644 --- a/arcade/examples/sections_demo_2.py +++ b/arcade/examples/sections_demo_2.py @@ -20,8 +20,7 @@ """ import random -from arcade import Window, Section, View, SpriteList, SpriteSolidColor, \ - SpriteCircle, draw_text, draw_line +import arcade from arcade.color import BLACK, BLUE, RED, BEAU_BLUE, GRAY from arcade.key import W, S, UP, DOWN @@ -29,7 +28,7 @@ PLAYER_PADDLE_SPEED = 10 -class Player(Section): +class Player(arcade.Section): """ A Section representing the space in the screen where the player paddle can move @@ -44,7 +43,7 @@ def __init__(self, left: int, bottom: int, width: int, height: int, self.key_down: int = key_down # the player paddle - self.paddle: SpriteSolidColor = SpriteSolidColor(30, 100, color=BLACK) + self.paddle: arcade.SpriteSolidColor = arcade.SpriteSolidColor(30, 100, color=BLACK) # player score self.score: int = 0 @@ -65,10 +64,14 @@ def on_draw(self): else: keys = 'UP and DOWN' start_x = self.left - 290 - draw_text(f'Player {self.name} (move paddle with: {keys})', - start_x, self.top - 20, BLUE, 9) - draw_text(f'Score: {self.score}', self.left + 20, - self.bottom + 20, BLUE) + arcade.draw_text( + f'Player {self.name} (move paddle with: {keys})', + start_x, self.top - 20, BLUE, 9, + ) + arcade.draw_text( + f'Score: {self.score}', self.left + 20, + self.bottom + 20, BLUE, + ) # draw the paddle self.paddle.draw() @@ -85,14 +88,14 @@ def on_key_release(self, _symbol: int, _modifiers: int): self.paddle.stop() -class Pong(View): +class Pong(arcade.View): def __init__(self): super().__init__() # a sprite list that will hold each player paddle to # check for collisions - self.paddles: SpriteList = SpriteList() + self.paddles: arcade.SpriteList = arcade.SpriteList() # we store each Section self.left_player: Player = Player( @@ -111,7 +114,7 @@ def __init__(self): self.paddles.append(self.right_player.paddle) # create the ball - self.ball: SpriteCircle = SpriteCircle(20, RED) + self.ball: arcade.SpriteCircle = arcade.SpriteCircle(20, RED) def setup(self): # set up a new game @@ -161,19 +164,19 @@ def end_game(self, winner: Player): self.setup() # prepare a new game def on_draw(self): - self.clear(BEAU_BLUE) # clear the screen + self.clear(color=BEAU_BLUE) # clear the screen self.ball.draw() # draw the ball half_window_x = self.window.width / 2 # middle x # draw a line diving the screen in half - draw_line(half_window_x, 0, half_window_x, self.window.height, GRAY, 2) + arcade.draw_line(half_window_x, 0, half_window_x, self.window.height, GRAY, 2) def main(): # create the window - window = Window(title='Two player simple Pong with Sections!') + window = arcade.Window(title='Two player simple Pong with Sections!') # create the custom View game = Pong() diff --git a/arcade/examples/sprite_animated_keyframes.py b/arcade/examples/sprite_animated_keyframes.py index 7b8bf901a..13c6131a9 100644 --- a/arcade/examples/sprite_animated_keyframes.py +++ b/arcade/examples/sprite_animated_keyframes.py @@ -39,5 +39,10 @@ def on_update(self, delta_time: float): self.sprite.update_animation(delta_time) -if __name__ == "__main__": +def main(): Animated().run() + + +if __name__ == "__main__": + main() + diff --git a/arcade/examples/sprite_explosion_bitmapped.py b/arcade/examples/sprite_explosion_bitmapped.py index 844953d14..1543fec71 100644 --- a/arcade/examples/sprite_explosion_bitmapped.py +++ b/arcade/examples/sprite_explosion_bitmapped.py @@ -86,7 +86,7 @@ def __init__(self): self.gun_sound = arcade.sound.load_sound(":resources:sounds/laser2.wav") self.hit_sound = arcade.sound.load_sound(":resources:sounds/explosion2.wav") - arcade.background_color = arcade.color.AMAZON + self.background_color = arcade.color.AMAZON def setup(self): diff --git a/arcade/examples/sprite_minimal.py b/arcade/examples/sprite_minimal.py index 048a45955..e611e8fec 100644 --- a/arcade/examples/sprite_minimal.py +++ b/arcade/examples/sprite_minimal.py @@ -30,6 +30,10 @@ def on_draw(self): self.sprites.draw() -if __name__ == "__main__": +def main(): game = WhiteSpriteCircleExample() game.run() + + +if __name__ == "__main__": + main() diff --git a/arcade/examples/sprite_move_keyboard_accel.py b/arcade/examples/sprite_move_keyboard_accel.py index cd0175d01..5773d55ee 100644 --- a/arcade/examples/sprite_move_keyboard_accel.py +++ b/arcade/examples/sprite_move_keyboard_accel.py @@ -84,7 +84,7 @@ def __init__(self, width, height, title): self.down_pressed = False # Set the background color - arcade.background_color = arcade.color.AMAZON + self.background_color = arcade.color.AMAZON def setup(self): """ Set up the game and initialize the variables. """ diff --git a/arcade/examples/tetris.py b/arcade/examples/tetris.py index 36158de9e..fde178f70 100644 --- a/arcade/examples/tetris.py +++ b/arcade/examples/tetris.py @@ -201,7 +201,7 @@ def rotate_stone(self): self.stone = new_stone def on_update(self, dt): - """ Update, drop stone if warrented """ + """ Update, drop stone if warranted """ self.frame_count += 1 if self.frame_count % 10 == 0: self.drop() diff --git a/arcade/examples/transform_feedback.py b/arcade/examples/transform_feedback.py index b4ce242c0..b4ae25eeb 100644 --- a/arcade/examples/transform_feedback.py +++ b/arcade/examples/transform_feedback.py @@ -147,7 +147,11 @@ def on_draw(self): self.buffer_1, self.buffer_2 = self.buffer_2, self.buffer_1 -if __name__ == "__main__": +def main(): window = MyGame(SCREEN_WIDTH, SCREEN_HEIGHT, SCREEN_TITLE) window.center_window() arcade.run() + + +if __name__ == "__main__": + main() diff --git a/arcade/experimental/atlas_render_into.py b/arcade/experimental/atlas_render_into.py index c43919f9d..d2b1e8482 100644 --- a/arcade/experimental/atlas_render_into.py +++ b/arcade/experimental/atlas_render_into.py @@ -32,12 +32,12 @@ def on_draw(self): def render_into_sprite_texture(self): # Render shape into texture atlas in the first sprite texture's space with self.spritelist.atlas.render_into(self.texture_1) as fbo: - fbo.clear((255, 0, 0, 255)) + fbo.clear(color=(255, 0, 0, 255)) arcade.draw_rectangle_filled(128, 128, 160, 160, arcade.color.WHITE, self.elapsed_time * 100) # Render a shape into the second texture in the atlas with self.spritelist.atlas.render_into(self.texture_2) as fbo: - fbo.clear((0, 255, 0, 255)) + fbo.clear(color=(0, 255, 0, 255)) arcade.draw_circle_filled( 128, 128, diff --git a/arcade/experimental/clock/clock_window.py b/arcade/experimental/clock/clock_window.py index 667ceff0e..3b2dd061c 100644 --- a/arcade/experimental/clock/clock_window.py +++ b/arcade/experimental/clock/clock_window.py @@ -28,7 +28,7 @@ from arcade import NoOpenGLException from arcade.color import TRANSPARENT_BLACK from arcade.context import ArcadeContext -from arcade.types import Color, RGBA255, RGBA255OrNormalized +from arcade.types import Color, RGBOrA255, RGBOrANormalized from arcade import SectionManager from arcade.utils import is_raspberry_pi @@ -263,8 +263,8 @@ def ctx(self) -> ArcadeContext: def clear( self, - color: Optional[RGBA255OrNormalized] = None, - normalized: bool = False, + color: Optional[RGBOrA255] = None, + color_normalized: Optional[RGBOrANormalized] = None, viewport: Optional[Tuple[int, int, int, int]] = None, ): """Clears the window with the configured background color @@ -277,11 +277,14 @@ def clear( 2. A 4-length RGBA :py:class:`tuple` of byte values (0 to 255) 3. A 4-length RGBA :py:class:`tuple` of normalized floats (0.0 to 1.0) - :param normalized: If the color format is normalized (0.0 -> 1.0) or byte values + :param color_normalized: (Optional) override the current background color + with a 4-length RGBA :py:class:`tuple` of normalized floats (0.0 to 1.0) + :param Tuple[int, int, int, int] viewport: The viewport range to clear """ - color = color if color is not None else self.background_color - self.ctx.screen.clear(color, normalized=normalized, viewport=viewport) + if color is None and color_normalized is None: + color = self.background_color + self.ctx.screen.clear(color=color, color_normalized=color_normalized, viewport=viewport) @property def background_color(self) -> Color: @@ -312,7 +315,7 @@ def background_color(self) -> Color: return self._background_color @background_color.setter - def background_color(self, value: RGBA255): + def background_color(self, value: RGBOrA255): self._background_color = Color.from_iterable(value) @property @@ -1002,24 +1005,26 @@ def add_section(self, section, at_index: Optional[int] = None, at_draw_order: Op def clear( self, - color: Optional[RGBA255OrNormalized] = None, - normalized: bool = False, + color: Optional[RGBOrA255] = None, + color_normalized: Optional[RGBOrANormalized] = None, viewport: Optional[Tuple[int, int, int, int]] = None, ): - """Clears the View's Window with the configured background color + """Clears the window with the configured background color set through :py:attr:`arcade.Window.background_color`. :param color: (Optional) override the current background color with one of the following: 1. A :py:class:`~arcade.types.Color` instance - 2. A 4-length RGBA :py:class:`tuple` of byte values (0 to 255) - 3. A 4-length RGBA :py:class:`tuple` of normalized floats (0.0 to 1.0) + 2. A 3 or 4-length RGB/RGBA :py:class:`tuple` of byte values (0 to 255) + + :param color_normalized: (Optional) override the current background color + using normalized values (0.0 to 1.0). For example, (1.0, 0.0, 0.0, 1.0) + making the window contents red. - :param normalized: If the color format is normalized (0.0 -> 1.0) or byte values :param Tuple[int, int, int, int] viewport: The viewport range to clear """ - self.window.clear(color, normalized, viewport) + self.window.ctx.screen.clear(color=color, color_normalized=color_normalized, viewport=viewport) def on_update(self, delta_time: float): """To be overridden""" diff --git a/arcade/experimental/depth_of_field.py b/arcade/experimental/depth_of_field.py new file mode 100644 index 000000000..a1c5d96f7 --- /dev/null +++ b/arcade/experimental/depth_of_field.py @@ -0,0 +1,239 @@ +"""An experimental depth-of-field example. + +It uses the depth attribute of along with blurring and shaders to +roughly approximate depth-based blur effects. The focus bounces +back forth automatically between a maximum and minimum depth value +based on time. Adjust the arguments to the App class at the bottom +of the file to change the speed. + +This example works by doing the following for each frame: + +1. Render a depth value for pixel into a buffer +2. Render a gaussian blurred version of the scene +3. For each pixel, use the current depth value to lerp between the + blurred and unblurred versions of the scene. + +This is more expensive than rendering the scene directly, but it's +both easier and more performant than more accurate blur approaches. + +If Python and Arcade are installed, this example can be run from the command line with: +python -m arcade.experimental.examples.array_backed_grid +""" +from typing import Tuple, Optional, cast +from textwrap import dedent +from math import cos, pi +from random import uniform, randint +from contextlib import contextmanager + +from pyglet.graphics import Batch + +from arcade import get_window, Window, SpriteSolidColor, SpriteList, Text +from arcade.color import RED +from arcade.types import Color, RGBA255 + +from arcade.gl import geometry, NEAREST, Program, Texture2D +from arcade.experimental.postprocessing import GaussianBlur + + +class DepthOfField: + """A depth-of-field effect we can use as a render context manager. + + :param size: The size of the buffers. + :param clear_color: The color which will be used as the background. + """ + + def __init__( + self, + size: Optional[Tuple[int, int]] = None, + clear_color: RGBA255 = (155, 155, 155, 255) + ): + self._geo = geometry.quad_2d_fs() + self._win: Window = get_window() + + size = cast(Tuple[int, int], size or self._win.size) + self._clear_color: Color = Color.from_iterable(clear_color) + + self.stale = True + + # Set up our depth buffer to hold per-pixel depth + self._render_target = self._win.ctx.framebuffer( + color_attachments=[ + self._win.ctx.texture( + size, + components=4, + filter=(NEAREST, NEAREST), + wrap_x=self._win.ctx.REPEAT, + wrap_y=self._win.ctx.REPEAT + ), + ], + depth_attachment=self._win.ctx.depth_texture( + size + ) + ) + + # Set up everything we need to perform blur and store results. + # This includes the blur effect, a framebuffer, and an instance + # variable to store the returned texture holding blur results. + self._blur_process = GaussianBlur( + size, + kernel_size=10, + sigma=2.0, + multiplier=2.0, + step=4 + ) + self._blur_target = self._win.ctx.framebuffer( + color_attachments=[ + self._win.ctx.texture( + size, + components=4, + filter=(NEAREST, NEAREST), + wrap_x=self._win.ctx.REPEAT, + wrap_y=self._win.ctx.REPEAT + ) + ] + ) + self._blurred: Optional[Texture2D] = None + + # To keep this example in one file, we use strings for our + # our shaders. You may want to use pathlib.Path.read_text in + # your own code instead. + self._render_program = self._win.ctx.program( + vertex_shader=dedent( + """#version 330 + + in vec2 in_vert; + in vec2 in_uv; + + out vec2 out_uv; + + void main(){ + gl_Position = vec4(in_vert, 0.0, 1.0); + out_uv = in_uv; + }"""), + fragment_shader=dedent( + """#version 330 + + uniform sampler2D texture_0; + uniform sampler2D texture_1; + uniform sampler2D depth_0; + + uniform float focus_depth; + + in vec2 out_uv; + + out vec4 frag_colour; + + void main() { + float depth_val = texture(depth_0, out_uv).x; + float depth_adjusted = min(1.0, 2.0 * abs(depth_val - focus_depth)); + vec4 crisp_tex = texture(texture_0, out_uv); + vec3 blur_tex = texture(texture_1, out_uv).rgb; + frag_colour = mix(crisp_tex, vec4(blur_tex, crisp_tex.a), depth_adjusted); + //if (depth_adjusted < 0.1){frag_colour = vec4(1.0, 0.0, 0.0, 1.0);} + }""") + ) + + # Set the buffers the shader program will use + self._render_program['texture_0'] = 0 + self._render_program['texture_1'] = 1 + self._render_program['depth_0'] = 2 + + @property + def render_program(self) -> Program: + """The compiled shader for this effect.""" + return self._render_program + + @contextmanager + def draw_into(self): + self.stale = True + previous_fbo = self._win.ctx.active_framebuffer + try: + self._win.ctx.enable(self._win.ctx.DEPTH_TEST) + self._render_target.clear(self._clear_color) + self._render_target.use() + yield self._render_target + finally: + self._win.ctx.disable(self._win.ctx.DEPTH_TEST) + previous_fbo.use() + + def process(self): + self._blurred = self._blur_process.render(self._render_target.color_attachments[0]) + self._win.use() + + self.stale = False + + def render(self): + if self.stale: + self.process() + + self._render_target.color_attachments[0].use(0) + self._blurred.use(1) + self._render_target.depth_attachment.use(2) + self._geo.render(self._render_program) + + +class App(Window): + """Window subclass to hold sprites and rendering helpers. + + :param text_color: The color of the focus indicator. + :param focus_range: The range the focus value will oscillate between. + :param focus_change_speed: How fast the focus bounces back and forth + between the ``-focus_range`` and ``focus_range``. + """ + def __init__( + self, + text_color: RGBA255 = RED, + focus_range: float = 16.0, + focus_change_speed: float = 0.1 + ): + super().__init__() + self.time: float = 0.0 + self.sprites: SpriteList = SpriteList() + self._batch = Batch() + self.focus_range: float = focus_range + self.focus_change_speed: float = focus_change_speed + self.indicator_label = Text( + f"Focus depth: {0:.3f} / {focus_range}", + self.width / 2, self.height / 2, + text_color, + align="center", + anchor_x="center", + batch=self._batch + ) + + # Randomize sprite depth, size, and angle, but set color from depth. + for _ in range(100): + depth = uniform(-100, 100) + color = Color.from_gray(int(255 * (depth + 100) / 200)) + s = SpriteSolidColor( + randint(100, 200), randint(100, 200), + uniform(20, self.width - 20), uniform(20, self.height - 20), + color, + uniform(0, 360) + ) + s.depth = depth + self.sprites.append(s) + + self.dof = DepthOfField() + + def on_update(self, delta_time: float): + self.time += delta_time + raw_focus = self.focus_range * (cos(pi * self.focus_change_speed * self.time) * 0.5 + 0.5) + self.dof.render_program["focus_depth"] = raw_focus / self.focus_range + self.indicator_label.value = f"Focus depth: {raw_focus:.3f} / {self.focus_range}" + + def on_draw(self): + self.clear() + + # Render the depth-of-field layer's frame buffer + with self.dof.draw_into(): + self.sprites.draw(pixelated=True) + + # Draw the blurred frame buffer and then the focus display + self.use() + self.dof.render() + self._batch.draw() + + +if __name__ == '__main__': + App().run() diff --git a/arcade/experimental/lights.py b/arcade/experimental/lights.py index cbfdbfa47..97305ee85 100644 --- a/arcade/experimental/lights.py +++ b/arcade/experimental/lights.py @@ -154,7 +154,7 @@ def __getitem__(self, i) -> Light: def __enter__(self): self._prev_target = self.ctx.active_framebuffer self._fbo.use() - self._fbo.clear(self._background_color) + self._fbo.clear(color=self._background_color) return self def __exit__(self, exc_type, exc_val, exc_tb): diff --git a/arcade/experimental/sprite_collect_coins_minimap.py b/arcade/experimental/sprite_collect_coins_minimap.py index e670dd634..c4e00d50e 100644 --- a/arcade/experimental/sprite_collect_coins_minimap.py +++ b/arcade/experimental/sprite_collect_coins_minimap.py @@ -106,7 +106,7 @@ def on_draw(self): self.clear() self.offscreen.use() - self.offscreen.clear(arcade.color.AMAZON) + self.offscreen.clear(color=arcade.color.AMAZON) arcade.draw_rectangle_outline(SCREEN_WIDTH / 2, SCREEN_HEIGHT / 2, diff --git a/arcade/experimental/texture_render_target.py b/arcade/experimental/texture_render_target.py index 4a26d7228..ddee67f49 100644 --- a/arcade/experimental/texture_render_target.py +++ b/arcade/experimental/texture_render_target.py @@ -40,7 +40,7 @@ def texture(self) -> Texture2D: def clear(self): """Clear the texture with the configured background color""" - self._fbo.clear(self._background_color) + self._fbo.clear(color=self._background_color) def set_background_color(self, color: RGBA255): """Set the background color for the light layer""" diff --git a/arcade/gl/buffer.py b/arcade/gl/buffer.py index de7c37d70..6e666197a 100644 --- a/arcade/gl/buffer.py +++ b/arcade/gl/buffer.py @@ -62,7 +62,7 @@ def __init__( gl.glBindBuffer(gl.GL_ARRAY_BUFFER, self._glo) # print(f"glBufferData(gl.GL_ARRAY_BUFFER, {self._size}, data, {self._usage})") - if data is not None and len(data) > 0: + if data is not None and len(data) > 0: # type: ignore self._size, data = data_to_ctypes(data) gl.glBufferData(gl.GL_ARRAY_BUFFER, self._size, data, self._usage) elif reserve > 0: diff --git a/arcade/gl/framebuffer.py b/arcade/gl/framebuffer.py index 649927dc4..dffdb30c2 100644 --- a/arcade/gl/framebuffer.py +++ b/arcade/gl/framebuffer.py @@ -2,7 +2,7 @@ from ctypes import c_int, string_at from contextlib import contextmanager -from typing import Optional, Tuple, List, TYPE_CHECKING, Union +from typing import Generator, Optional, Tuple, List, TYPE_CHECKING import weakref @@ -305,7 +305,7 @@ def __exit__(self, exc_type, exc_val, exc_tb): self._prev_fbo.use() @contextmanager - def activate(self): + def activate(self) -> Generator[Framebuffer, None, None]: """Context manager for binding the framebuffer. Unlike the default context manager in this class @@ -348,10 +348,10 @@ def _use(self, *, force: bool = False): def clear( self, - color: Union[RGBOrA255, RGBOrANormalized] = (0.0, 0.0, 0.0, 0.0), *, + color: Optional[RGBOrA255] = None, + color_normalized: Optional[RGBOrANormalized] = None, depth: float = 1.0, - normalized: bool = False, viewport: Optional[Tuple[int, int, int, int]] = None, ): """ @@ -361,12 +361,13 @@ def clear( fb.clear(color=arcade.color.WHITE) # Clear framebuffer using the color red in normalized form - fbo.clear(color=(1.0, 0.0, 0.0, 1.0), normalized=True) + fbo.clear(color_normalized=(1.0, 0.0, 0.0, 1.0)) If the background color is an ``RGB`` value instead of ``RGBA``` we assume alpha value 255. - :param color: A 3 or 4 component tuple containing the color + :param color: A 3 or 4 component tuple containing the color (prioritized over color_normalized) + :param color_normalized: A 3 or 4 component tuple containing the color in normalized form :param depth: Value to clear the depth buffer (unused) :param normalized: If the color values are normalized or not :param Tuple[int, int, int, int] viewport: The viewport range to clear @@ -379,24 +380,26 @@ def clear( else: self.scissor = None - if normalized: - # If the colors are already normalized we can pass them right in + clear_color = 0.0, 0.0, 0.0, 0.0 + if color is not None: if len(color) == 3: - gl.glClearColor(*color, 1.0) + clear_color = color[0] / 255, color[1] / 255, color[2] / 255, 1.0 + elif len(color) == 4: + clear_color = color[0] / 255, color[1] / 255, color[2] / 255, color[3] / 255 else: - gl.glClearColor(*color) - else: - # OpenGL wants normalized colors (0.0 -> 1.0) - if len(color) == 3: - gl.glClearColor(color[0] / 255, color[1] / 255, color[2] / 255, 1.0) + raise ValueError("Color should be a 3 or 4 component tuple") + elif color_normalized is not None: + if len(color_normalized) == 3: + clear_color = color_normalized[0], color_normalized[1], color_normalized[2], 1.0 + elif len(color_normalized) == 4: + clear_color = color_normalized else: - # mypy does not understand that color[3] is guaranteed to work in this codepath, pyright does. - # We can remove this type: ignore if we switch to pyright. - gl.glClearColor( - color[0] / 255, color[1] / 255, color[2] / 255, color[3] / 255 # type: ignore - ) + raise ValueError("Color should be a 3 or 4 component tuple") + + gl.glClearColor(*clear_color) if self.depth_attachment: + gl.glClearDepth(depth) gl.glClear(gl.GL_COLOR_BUFFER_BIT | gl.GL_DEPTH_BUFFER_BIT) else: gl.glClear(gl.GL_COLOR_BUFFER_BIT) @@ -425,15 +428,23 @@ def read( raise ValueError(f"Invalid dtype '{dtype}'") with self.activate(): - # Configure attachment to read from - gl.glReadBuffer(gl.GL_COLOR_ATTACHMENT0 + attachment) + # Configure attachment to read from. Does not work on default framebuffer. + if not self.is_default: + gl.glReadBuffer(gl.GL_COLOR_ATTACHMENT0 + attachment) + + gl.glPixelStorei(gl.GL_PACK_ALIGNMENT, 1) + gl.glPixelStorei(gl.GL_UNPACK_ALIGNMENT, 1) + if viewport: x, y, width, height = viewport else: - x, y, width, height = 0, 0, self._width, self._height + x, y, width, height = 0, 0, *self.size + data = (gl.GLubyte * (components * component_size * width * height))(0) gl.glReadPixels(x, y, width, height, base_format, pixel_type, data) - gl.glReadBuffer(gl.GL_COLOR_ATTACHMENT0) # Reset to default + + if not self.is_default: + gl.glReadBuffer(gl.GL_COLOR_ATTACHMENT0) # Reset to default return string_at(data, len(data)) @@ -568,6 +579,37 @@ def __init__(self, ctx: "Context"): # HACK: Signal the default framebuffer having depth buffer self._depth_attachment = True # type: ignore + @property + def size(self) -> Tuple[int, int]: + """ + Size as a ``(w, h)`` tuple + + :type: tuple (int, int) + """ + return self._ctx.window.get_framebuffer_size() + + @property + def width(self) -> int: + """ + The width of the framebuffer in pixels + + :type: int + """ + return self.size[0] + + @property + def height(self) -> int: + """ + The height of the framebuffer in pixels + + :type: int + """ + return self.size[1] + + def _get_framebuffer_size(self) -> Tuple[int, int]: + """Get the framebuffer size of the window""" + return self._ctx.window.get_framebuffer_size() + def _get_viewport(self) -> Tuple[int, int, int, int]: """ Get or set the framebuffer's viewport. diff --git a/arcade/gui/events.py b/arcade/gui/events.py index 00b885195..d22cf5e4d 100644 --- a/arcade/gui/events.py +++ b/arcade/gui/events.py @@ -21,8 +21,8 @@ class UIMouseEvent(UIEvent): Covers all mouse event """ - x: float - y: float + x: int + y: int @property def pos(self): @@ -32,8 +32,8 @@ def pos(self): @dataclass class UIMouseMovementEvent(UIMouseEvent): """Triggered when the mouse is moved.""" - dx: float - dy: float + dx: int + dy: int @dataclass @@ -46,8 +46,8 @@ class UIMousePressEvent(UIMouseEvent): @dataclass class UIMouseDragEvent(UIMouseEvent): """Triggered when the mouse moves while one of its buttons being pressed.""" - dx: float - dy: float + dx: int + dy: int buttons: int modifiers: int diff --git a/arcade/gui/experimental/scroll_area.py b/arcade/gui/experimental/scroll_area.py index 77e539ad6..2f1245651 100644 --- a/arcade/gui/experimental/scroll_area.py +++ b/arcade/gui/experimental/scroll_area.py @@ -113,7 +113,7 @@ def on_event(self, event: UIEvent) -> Optional[bool]: child_event = event if isinstance(event, UIMouseEvent): child_event = type(event)(**event.__dict__) # type: ignore - child_event.x = event.x - self.x + self.scroll_x - child_event.y = event.y - self.y + self.scroll_y + child_event.x = int(event.x - self.x + self.scroll_x) + child_event.y = int(event.y - self.y + self.scroll_y) return super().on_event(child_event) diff --git a/arcade/gui/property.py b/arcade/gui/property.py index cd1faff80..cec628b6b 100644 --- a/arcade/gui/property.py +++ b/arcade/gui/property.py @@ -18,21 +18,41 @@ class _Obs(Generic[P]): def __init__(self, value: P): self.value = value # This will keep any added listener even if it is not referenced anymore and would be garbage collected - self.listeners: Set[Callable[[], Any]] = set() + self.listeners: Set[Callable[[Any, P], Any]] = set() class Property(Generic[P]): """ An observable property which triggers observers when changed. +.. code-block:: python + + def log_change(instance, value): + print("Something changed") + + class MyObject: + name = Property() + + my_obj = MyObject() + bind(my_obj, "name", log_change) + unbind(my_obj, "name", log_change) + + my_obj.name = "Hans" + # > Something changed + :param default: Default value which is returned, if no value set before :param default_factory: A callable which returns the default value. Will be called with the property and the instance """ + __slots__ = ("name", "default_factory", "obs") name: str - def __init__(self, default: Optional[P] = None, default_factory: Optional[Callable[[Any, Any], P]] = None): + def __init__( + self, + default: Optional[P] = None, + default_factory: Optional[Callable[[Any, Any], P]] = None, + ): if default_factory is None: default_factory = lambda prop, instance: cast(P, default) @@ -60,7 +80,11 @@ def dispatch(self, instance, value): obs = self._get_obs(instance) for listener in obs.listeners: try: - listener() + try: + listener(instance, value) + except TypeError: + # If the listener does not accept arguments, we call it without it + listener() # type: ignore except Exception: print( f"Change listener for {instance}.{self.name} = {value} raised an exception!", @@ -95,8 +119,8 @@ def bind(instance, property: str, callback): Binds a function to the change event of the property. A reference to the function will be kept, so that it will be still invoked, even if it would normally have been garbage collected. - def log_change(): - print("Something changed") + def log_change(instance, value): + print(f"Value of {instance} changed to {value}") class MyObject: name = Property() @@ -105,7 +129,7 @@ class MyObject: bind(my_obj, "name", log_change) my_obj.name = "Hans" - # > Something changed + # > Value of <__main__.MyObject ...> changed to Hans :param instance: Instance owning the property :param property: Name of the property @@ -122,7 +146,7 @@ def unbind(instance, property: str, callback): """ Unbinds a function from the change event of the property. - def log_change(): + def log_change(instance, value): print("Something changed") class MyObject: @@ -150,10 +174,7 @@ class MyObject: class _ObservableDict(dict): """Internal class to observe changes inside a native python dict.""" - __slots__ = ( - "prop", - "obj" - ) + __slots__ = ("prop", "obj") def __init__(self, prop: Property, instance, *largs): self.prop: Property = prop @@ -211,10 +232,7 @@ def set(self, instance, value: dict): class _ObservableList(list): """Internal class to observe changes inside a native python list.""" - __slots__ = ( - "prop", - "obj" - ) + __slots__ = ("prop", "obj") def __init__(self, prop: Property, instance, *largs): self.prop: Property = prop diff --git a/arcade/gui/ui_manager.py b/arcade/gui/ui_manager.py index b3a31f73e..33e54092c 100644 --- a/arcade/gui/ui_manager.py +++ b/arcade/gui/ui_manager.py @@ -84,7 +84,6 @@ def __init__(self, window: Optional[arcade.Window] = None): self._surfaces: Dict[int, Surface] = {} self.children: Dict[int, List[UIWidget]] = defaultdict(list) self._requires_render = True - #: Camera used when drawing the UI self.register_event_type("on_event") def add(self, widget: W, *, index=None, layer=0) -> W: @@ -352,22 +351,22 @@ def on_event(self, event) -> Union[bool, None]: def dispatch_ui_event(self, event): return self.dispatch_event("on_event", event) - def on_mouse_motion(self, x: float, y: float, dx: float, dy: float): + def on_mouse_motion(self, x: int, y: int, dx: int, dy: int): x, y = self.adjust_mouse_coordinates(x, y) return self.dispatch_ui_event(UIMouseMovementEvent(self, x, y, dx, dy)) # type: ignore - def on_mouse_press(self, x: float, y: float, button: int, modifiers: int): - x, y = self.adjust_mouse_coordinates(x, y)[:2] + def on_mouse_press(self, x: int, y: int, button: int, modifiers: int): + x, y = self.adjust_mouse_coordinates(x, y) return self.dispatch_ui_event(UIMousePressEvent(self, x, y, button, modifiers)) # type: ignore def on_mouse_drag( - self, x: float, y: float, dx: float, dy: float, buttons: int, modifiers: int + self, x: int, y: int, dx: int, dy: int, buttons: int, modifiers: int ): x, y = self.adjust_mouse_coordinates(x, y) return self.dispatch_ui_event(UIMouseDragEvent(self, x, y, dx, dy, buttons, modifiers)) # type: ignore - def on_mouse_release(self, x: float, y: float, button: int, modifiers: int): - x, y = self.adjust_mouse_coordinates(x, y)[:2] + def on_mouse_release(self, x: int, y: int, button: int, modifiers: int): + x, y = self.adjust_mouse_coordinates(x, y) return self.dispatch_ui_event(UIMouseReleaseEvent(self, x, y, button, modifiers)) # type: ignore def on_mouse_scroll(self, x, y, scroll_x, scroll_y): diff --git a/arcade/gui/widgets/__init__.py b/arcade/gui/widgets/__init__.py index 55156cd81..a7108276c 100644 --- a/arcade/gui/widgets/__init__.py +++ b/arcade/gui/widgets/__init__.py @@ -1,6 +1,5 @@ from __future__ import annotations -import builtins from abc import ABC from random import randint from typing import ( @@ -551,32 +550,32 @@ def with_border(self, *, width=2, color=(0, 0, 0)) -> Self: def with_padding( self, *, - top: Union["builtins.ellipsis", int] = ..., - right: Union["builtins.ellipsis", int] = ..., - bottom: Union["builtins.ellipsis", int] = ..., - left: Union["builtins.ellipsis", int] = ..., - all: Union["builtins.ellipsis", int] = ..., + top: Optional[int] = None, + right: Optional[int] = None, + bottom: Optional[int] = None, + left: Optional[int] = None, + all: Optional[int] = None, ) -> "UIWidget": """ Changes the padding to the given values if set. Returns itself :return: self """ - if all is not ...: + if all is not None: self.padding = all - if top is not ...: + if top is not None: self._padding_top = top - if right is not ...: + if right is not None: self._padding_right = right - if bottom is not ...: + if bottom is not None: self._padding_bottom = bottom - if left is not ...: + if left is not None: self._padding_left = left return self def with_background( self, *, - color: Union["builtins.ellipsis", Color] = ..., + color: Union[None, Color] = ..., # type: ignore texture: Union[None, Texture, NinePatchTexture] = ..., # type: ignore ) -> "UIWidget": """ diff --git a/arcade/gui/widgets/text.py b/arcade/gui/widgets/text.py index efd5650dd..0e904c004 100644 --- a/arcade/gui/widgets/text.py +++ b/arcade/gui/widgets/text.py @@ -354,7 +354,7 @@ def __init__( self.doc, width - self.LAYOUT_OFFSET, height, multiline=multiline ) self.layout.x += self.LAYOUT_OFFSET - self.caret = Caret(self.layout, color=caret_color) + self.caret = Caret(self.layout, color=Color.from_iterable(caret_color)) self.caret.visible = False self._blink_state = self._get_caret_blink_state() @@ -383,7 +383,8 @@ def on_event(self, event: UIEvent) -> Optional[bool]: # If active check to deactivate if self._active and isinstance(event, UIMousePressEvent): if self.rect.collide_with_point(event.x, event.y): - x, y = event.x - self.x - self.LAYOUT_OFFSET, event.y - self.y + x = int(event.x - self.x - self.LAYOUT_OFFSET) + y = int(event.y - self.y) self.caret.on_mouse_press(x, y, event.button, event.modifiers) else: self._active = False @@ -407,7 +408,8 @@ def on_event(self, event: UIEvent) -> Optional[bool]: if isinstance(event, UIMouseEvent) and self.rect.collide_with_point( event.x, event.y ): - x, y = event.x - self.x - self.LAYOUT_OFFSET, event.y - self.y + x = int(event.x - self.x - self.LAYOUT_OFFSET) + y = int(event.y - self.y) if isinstance(event, UIMouseDragEvent): self.caret.on_mouse_drag( x, y, event.dx, event.dy, event.buttons, event.modifiers diff --git a/arcade/perf_graph.py b/arcade/perf_graph.py index cf3952e75..44df94352 100644 --- a/arcade/perf_graph.py +++ b/arcade/perf_graph.py @@ -292,7 +292,7 @@ def update_graph(self, delta_time: float): self.minimap_texture, projection=self.proj) as fbo: # Set the background color - fbo.clear(self.background_color) + fbo.clear(color=self.background_color) # Draw lines & their labels for text in self._all_text_objects: diff --git a/arcade/shape_list.py b/arcade/shape_list.py index fcbbd977b..29a53f6a2 100644 --- a/arcade/shape_list.py +++ b/arcade/shape_list.py @@ -30,8 +30,7 @@ from arcade.gl import BufferDescription from arcade.gl import Program from arcade import ArcadeContext - -from .math import rotate_point +from arcade.math import rotate_point __all__ = [ diff --git a/arcade/sprite/animated.py b/arcade/sprite/animated.py index 28e5299d6..a7c7230a8 100644 --- a/arcade/sprite/animated.py +++ b/arcade/sprite/animated.py @@ -22,6 +22,7 @@ class TextureKeyframe: :param duration: Duration in milliseconds to display this keyframe. :param tile_id: Tile ID for this keyframe (only used for tiled maps) """ + __slots__ = ("texture", "duration", "tile_id") def __init__( self, texture: Texture, @@ -46,10 +47,12 @@ class TextureAnimation: :param keyframes: List of keyframes for the animation. :param loop: If the animation should loop. """ - def __init__(self, keyframes: Optional[List[TextureKeyframe]] = None): - self._keyframes = keyframes or [] + __slots__ = ("_keyframes", "_duration_ms", "_timeline") + + def __init__(self, keyframes: List[TextureKeyframe]): + self._keyframes = keyframes self._duration_ms = 0 - self._timeline: List[int] = self._create_timeline(self._keyframes) if self._keyframes else [] + self._timeline: List[int] = self._create_timeline(self._keyframes) @property def keyframes(self) -> Tuple[TextureKeyframe, ...]: @@ -94,25 +97,6 @@ def _create_timeline(self, keyframes: List[TextureKeyframe]) -> List[int]: self._duration_ms = current_time_ms return timeline - def append_keyframe(self, keyframe: TextureKeyframe) -> None: - """ - Add a keyframe to the animation. - - :param keyframe: Keyframe to add. - """ - self._keyframes.append(keyframe) - self._timeline.append(self._duration_ms) - self._timeline = self._create_timeline(self._keyframes) - - def remove_keyframe(self, index: int) -> None: - """ - Remove a keyframe from the animation. - - :param index: Index of the keyframe to remove. - """ - del self._keyframes[index] - self._timeline = self._create_timeline(self._keyframes) - def get_keyframe(self, time: float, loop: bool = True) -> Tuple[int, TextureKeyframe]: """ Get the frame at a given time. diff --git a/arcade/sprite/base.py b/arcade/sprite/base.py index 68e0d57b3..a1f878b4a 100644 --- a/arcade/sprite/base.py +++ b/arcade/sprite/base.py @@ -3,7 +3,7 @@ from typing import TYPE_CHECKING, Iterable, List, TypeVar, Any import arcade -from arcade.types import Point, Color, RGBA255, PointList +from arcade.types import Point, Color, RGBA255, RGBOrA255, PointList from arcade.color import BLACK from arcade.hitbox import HitBox from arcade.texture import Texture @@ -38,6 +38,7 @@ class BasicSprite: "_color", "_texture", "_hit_box", + "_visible", "sprite_lists", "_angle", "__weakref__", @@ -49,6 +50,7 @@ def __init__( scale: float = 1.0, center_x: float = 0, center_y: float = 0, + visible: bool = True, **kwargs: Any, ) -> None: self._position = (center_x, center_y) @@ -57,6 +59,7 @@ def __init__( self._width = texture.width * scale self._height = texture.height * scale self._scale = scale, scale + self._visible = bool(visible) self._color: Color = Color(255, 255, 255, 255) self.sprite_lists: List["SpriteList"] = [] @@ -293,29 +296,44 @@ def top(self, amount: float): @property def visible(self) -> bool: - """ - Get or set the visibility of this sprite. - This is a shortcut for changing the alpha value of a sprite - to 0 or 255:: + """Get or set the visibility of this sprite. + + When set to ``False``, each :py:class:`~arcade.SpriteList` and + its attached shaders will treat the sprite as if has an + :py:attr:`.alpha` of 0. However, the sprite's actual values for + :py:attr:`.alpha` and :py:attr:`.color` will not change. + + .. code-block:: python + + # The initial color of the sprite + >>> sprite.color + Color(255, 255, 255, 255) # Make the sprite invisible - sprite.visible = False - # Change back to visible - sprite.visible = True - # Toggle visible - sprite.visible = not sprite.visible + >>> sprite.visible = False + # The sprite's color value has not changed + >>> sprite.color + Color(255, 255, 255, 255) + # The sprite's alpha value hasn't either + >>> sprite.alpha + 255 + + # Restore visibility + >>> sprite.visible = True + # Shorthand to toggle visible + >>> sprite.visible = not sprite.visible """ - return self._color[3] > 0 + return self._visible @visible.setter def visible(self, value: bool): - self._color = Color( - self._color[0], - self._color[1], - self._color[2], - 255 if value else 0, - ) + value = bool(value) + if self._visible == value: + return + + self._visible = value + for sprite_list in self.sprite_lists: sprite_list._update_color(self) @@ -345,27 +363,22 @@ def color(self) -> Color: return self._color @color.setter - def color(self, color: RGBA255): - if len(color) == 4: - if ( - self._color[0] == color[0] - and self._color[1] == color[1] - and self._color[2] == color[2] - and self._color[3] == color[3] - ): - return - self._color = Color.from_iterable(color) - - elif len(color) == 3: - if ( - self._color[0] == color[0] - and self._color[1] == color[1] - and self._color[2] == color[2] - ): - return - self._color = Color(color[0], color[1], color[2], self._color[3]) + def color(self, color: RGBOrA255): + if color == self._color: + return + + r, g, b, *_a = color + + if _a: + if len(_a) > 1: + raise ValueError(f"iterable must unpack to 3 or 4 values not {len(color)}") + a = _a[0] else: - raise ValueError("Color must be three or four ints from 0-255") + a = self._color.a + + # We don't handle alpha and .visible interactions here + # because it's implemented in SpriteList._update_color + self._color = Color(r, g, b, a) for sprite_list in self.sprite_lists: sprite_list._update_color(self) diff --git a/arcade/sprite_list/sprite_list.py b/arcade/sprite_list/sprite_list.py index 5cc832ada..a608466b3 100644 --- a/arcade/sprite_list/sprite_list.py +++ b/arcade/sprite_list/sprite_list.py @@ -1262,7 +1262,7 @@ def _update_color(self, sprite: SpriteType) -> None: self._sprite_color_data[slot * 4] = int(sprite._color[0]) self._sprite_color_data[slot * 4 + 1] = int(sprite._color[1]) self._sprite_color_data[slot * 4 + 2] = int(sprite._color[2]) - self._sprite_color_data[slot * 4 + 3] = int(sprite._color[3]) + self._sprite_color_data[slot * 4 + 3] = int(sprite._color[3] * sprite._visible) self._sprite_color_changed = True def _update_size(self, sprite: SpriteType) -> None: diff --git a/arcade/text.py b/arcade/text.py index 8a2879bde..9b2d115c5 100644 --- a/arcade/text.py +++ b/arcade/text.py @@ -651,7 +651,7 @@ def create_text_sprite( texture_atlas = arcade.get_window().ctx.default_atlas texture_atlas.add(texture) with texture_atlas.render_into(texture) as fbo: - fbo.clear(background_color or arcade.color.TRANSPARENT_BLACK) + fbo.clear(color=background_color or arcade.color.TRANSPARENT_BLACK) text_object.draw() return arcade.Sprite( diff --git a/arcade/window_commands.py b/arcade/window_commands.py index 009cd55eb..a233c2d25 100644 --- a/arcade/window_commands.py +++ b/arcade/window_commands.py @@ -113,7 +113,7 @@ def run(): # Used in some unit test if os.environ.get('ARCADE_TEST'): - window.on_update(window._update_rate) + window.on_update(1.0 / 60.0) window.on_draw() elif window.headless: # We are entering headless more an will emulate an event loop diff --git a/doc/_static/checkered.png b/doc/_static/checkered.png new file mode 100644 index 0000000000000000000000000000000000000000..6f8f0bf9f6b4b581a93ef1d3b32c382fa59390cc GIT binary patch literal 4885 zcmeHKYgiL!7LG&_QN*Qmr66^TxP@YpNiw;XAP|B?4Wc3-UdkkyzzE5NWFSF{iim}( z@h-4xp%xYKLh*_yUW%fwg19JJg$1!yD+*d_OTCc&CZOW8{p0g&|4B$Pb1v_9&U?;x z@=b1J#6%ZoPiGp9=AsA>oesXKYiB1%@VSb6xr#=k=P!tgC8wii$Y3P2crpf&=>`nK zEV!0Nv)tSpGdt~}@30pMA08O!Gr;ZQ>G-+=?@PbzJazC`*ZqZ`J<31xee5RZnHM6O zdRk64zs$u zp|Z|@Nb`%!K}jKl4p>f3i*~pfxR3CMZruwSnpwa0ha*o z{-)T;X*QQ_TT~1e(cs@o{nYx0S=+wW(H?E=8s@%w>DNP#v~TvOf6y7ymh$&mzSs*# zzweA&yhl}O=B)9kV+e+J29JYi_`KYR!(T1lQ(F}_)f#j7r?thM)qi9jYFN}*s|%aj zm^UnBAB9&OP5< zx>kNkPFEE87YEf3c1(>vzb)8(eCm8(r^4989ksl<ix3YaYTKC+>M7H{Jv(ahIz+-Ar@ZrYP85=OwYc;ylEf*G zzp`#u0p?$Dp!KOY?-????x?#DGiT0YGE?T~MeNUJ>hXr!!w(NnA5-W#f6uP@hyIy_ zHx++#diMOAdwz;eyE$gN^kK@>V;zU@RgvL)C%a02yLv6P{b=6$L-O{#C-la|@|yL# zZkK$L6g#sjyXc7|H;V_?WCm3%<-?AsYu)i_P67E|TW&90wl8Pcnv^vSiLj$%b;PL8 zwl*>Ejw8O@}|xQr=h-vn`^=Di|eyy0l|xw#$U>?w$*k_lED^ zyx6biV25JZqq1}PjdQD0j&yihkELEWH0?dM{Lb*|xz_UdiX>4_nC+sdC&+f$@WoV{ zm*_;jjb)wqjD5Rew%O%e^SoPoxMJi;nS9=uf%q_+gJF|ySC(5G!BGkrs{BmECP zh&TQ`8sYf(v8+pzPM zU&5_@;7LcC1-xCw`4`J_zKI!#hy5)$RJ)1eufV&lWeW#(FUo73)swo|8h9w413_+= z;p&6A*K>CYbXi@2DJwT(8@snyt`-kMis6$J87Uhy^G106zV)gxvU`fr zwxagD^V`?#b_lbkCkJIce0JK_Y9{u!ovFxkk8o&aTe~>JmZfECn^R7#_THK9Ft9Mw zzv@v-BCVmY`1-B57fv~P`pD}#+4 zd!h{5{;Ruxbuk#6R&MZJSbMKox;5kZksa4gj_%lOG}gSNxr15iiG!ILtDGWH6FL^E zAygR4qBDS*N}~l#uozHv5=KHQECJU`8J)*YG9X+dWyJ86u+kvI67lc_Mr_7{h$!`f zB(+$>m=Ne3V37a-9Y&&%MVG8MNi0%^9ajRbDKVP?*-gkKDI-=H3CRc}1|ckj1vBLq zJdMi;bcOHLy7>n61;X z`+Jy3c^Uxe3+PWhOi|#chdmuL5vfKsCQrlk`L#i>^o{mP%#*#4|Fg1Zy zId3gFQK5`{?LkqHfa?r)FF^KNmL#ryL)KfdQ5t(X{R08+uW{e9ewDl37+5Kll2AgO zN`=_U4sb*~4$S2V_)J8@(J~Rf3TBFUn1;y{2)P2TNGnhw+%l)+`eZzPe)D5(VwQpOZqpK5udh{AQ)3=*Z-hunGr)t!Wcym$x;Rt5=430+Zzh#ElN0U z0v731$M^O83@oMhu6GM0<8~DU+1plvs(TkPp=nreMSx$gN}Y)66ELv9`x@$%9RHJI z!6J?d<*QI8tU`rML@NgM28Cv7v?z=T5dpXn*wc88ZX&d#88u?T2_RFD4QL>HHjuBK zO244<`Y3gMMQG`1Q)b zcmdOj{d!#WaYoVj7yk9t;xAkQK)+w)o%nrE*L%9&iGg=Ae&1d1>3Syy-pTlVcm3b! za(?r`gXzI3$P6B5=3dC|1dl?Fs>u^WX*TLrRJC(GkPI<|&oa?ytg+O4fMxrHG$0&I zDwOiU_vv(3H{Wp=(=j0Wh?K{YGD1h4n`!pr9HxQHIGG4h$GMoy;Uj_3SVd@Xlt+Vl zPg$Am@)Ac+PtW>nk9?1uf%K9!Pi!_@bq$ZU?DHWj3Y&5-0dtx{9uc}PB!20C0f0?b AjQ{`u literal 0 HcmV?d00001 diff --git a/doc/_static/css/custom.css b/doc/_static/css/custom.css index 38ea33d70..73354904e 100644 --- a/doc/_static/css/custom.css +++ b/doc/_static/css/custom.css @@ -4,33 +4,87 @@ body { /* TOC - Main landing page */ /* Show two columns if screen is wide enough */ @media screen and (min-width: 700px) { + /* For regular items */ .toctree-wrapper > ul { columns: 2; } + /* For Social and Learning Resources sections */ + .toc-outside-links { + columns: 2; + } } -.toctree-l1 { - margin-left: 5px; + +/* Wrap individual main page items for easier group manipulation + of contents and images. See PR #2027 for main page css changes */ +.main-page-item-wrapper { + align-items: flex-start; + display: flex; + margin: 10px; } -.toc-outside-links { - margin-left: 35px; +.main-page-item-wrapper > .toctree-wrapper { + width: 100%; } -.toc-outside-links ul { - columns: 2; + +/* single-col-box for items on main page with 2 bullet points, use if + wanting to avoid having 2 columns with 1 item each inside */ +.main-page-item-wrapper > .single-col-box { + width: 100%; } -.toc-outside-links li p, .toc-outside-links ul li { - margin-left: 5px; +.main-page-item-wrapper > .single-col-box ul { + columns: 1; } -.toc-outside-links a { - text-decoration: none; + +/* For Social and Learning Resources to make +title + list appear like other categories */ +.main-page-item-wrapper > .main-page-item-sub-wrapper { + display: flex; + flex-direction: column; + margin: 0px; + width: 100%; } -.toctree-wrapper a { - text-decoration: none; +.main-page-item-wrapper > .main-page-item-title { + width: 100%; } -.toc-outside-links a:hover { - text-decoration: underline; +.main-page-item-title > p { + font-size: var(--font-size--small); + margin-bottom: 0; + text-align: initial; + text-transform: uppercase; } -.toctree-wrapper a:hover { - text-decoration: underline; +.main-page-item-sub-wrapper > .toc-outside-links { + margin-left: 0px; + width: 100%; +} + +/* Wrappers and formatting for sprinter, START HERE, github star button +to align them neatly */ +.main-page-item-wrapper-header { + align-items: center; + display: flex; + margin: 10px; +} +.main-page-box { + width: 100%; + display: flex; + justify-content: space-between; + align-items: center; +} +.main-page-box > .main-page-link { + display: flex; + width: 100%; +} +.main-page-box > .main-page-link a { + display: flex; +} +.sprinter-box { + margin-left: 10px; + height: 55px; + display: flex; +} +.start-here-box { + align-items: center; + display: flex; + margin-left: -10px; } #the-python-arcade-library h2 { font-size: var(--font-size--small); @@ -39,15 +93,33 @@ body { margin-top: .5rem; text-align: initial; text-transform: uppercase; + width: 100%; + display: flex; } -#the-python-arcade-library ul { - margin-top: 0; - margin-bottom: 0; +.main-page-box > .main-page-box-gh { + display: flex; + align-items: center; + margin-right: 0px; } #github-stars { - width: 170px; + width: 141px; height: 30px; - margin-bottom: -18px; + margin-bottom: -9px; +} + +/* Formatting for list items */ +.toctree-l1 { + margin-left: 5px; +} +.toctree-wrapper a { + text-decoration: none; +} +.toctree-wrapper a:hover { + text-decoration: underline; +} +#the-python-arcade-library ul { + margin-top: 0; + margin-bottom: 0; } .heading-icon { columns: 2; @@ -104,9 +176,6 @@ img.right-image { width: 78px; padding-right: 15px; } -.main-page-table { - width: 100%; -} .vimeo-video { border: 0; position:absolute; @@ -174,3 +243,10 @@ body:not([data-theme="light"]) .highlight .c1 { .highlight .c1 { color: #007507; } + +.checkered { + background-image: url(../checkered.png); + background-repeat: repeat; + margin:0px; + padding: 0px; +} \ No newline at end of file diff --git a/doc/conf.py b/doc/conf.py index f8bc966eb..7e7f16ea2 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -3,12 +3,14 @@ Generate HTML docs """ -import runpy -import sys +import docutils.nodes import os +import re +import runpy import sphinx.ext.autodoc import sphinx.transforms -import docutils.nodes +import sys + # --- Pre-processing Tasks @@ -91,7 +93,7 @@ # General information about the project. project = 'Python Arcade Library' -copyright = '2023, Paul Vincent Craven' +copyright = '2024, Paul Vincent Craven' author = 'Paul Vincent Craven' # The version info for the project you're documenting, acts as replacement for @@ -205,45 +207,57 @@ def warn_undocumented_members(_app, what, name, _obj, _options, lines): )) -def source_read(_app, docname, source): +def generate_color_table(filename, source): + """This function Generates the Color tables in the docs for color and csscolor packages""" - # print(f" XXX Reading {docname}") - import os - file_path = os.path.dirname(os.path.abspath(__file__)) - os.chdir(file_path) + append_text = "\n\n.. raw:: html\n\n" + append_text += " \n" - filename = None - if docname == "api_docs/arcade.color": - filename = "../arcade/color/__init__.py" - elif docname == "api_docs/arcade.csscolor": - filename = "../arcade/csscolor/__init__.py" + # Will match a line containing: + # name '(?P[a-z_A-Z]*)' followed by + # a Color '(?: *= *Color *\( *)' followed by + # red '(?P\d*)' followed by + # green '(?P\d*)' followed by + # blue '(?P\d*)' followed by + # alpha '(?P\d*)' + color_match = re.compile(r'(?P[a-z_A-Z]*)(?:[ =]*Color[ (]*)(?P\d*)[ ,]*(?P\d*)[ ,]*(?P\d*)[ ,]*(?P\d*)') - if filename: - # print(f" XXX Handling color file: {filename}") - import re - p = re.compile(r"^([A-Z_]+) = (\(.*\))") + with open(filename) as color_file: + for line in color_file: - original_text = source[0] - append_text = "\n\n.. raw:: html\n\n" - append_text += "
\n" - color_file = open(filename) + # Check if the line has a Color. + matches = color_match.match(line) + if not matches: + continue + + color_rgba = f"({matches.group('red')}, {matches.group('green')}, {matches.group('blue')}, {matches.group('alpha')})" + + # Generate the alpha for CSS color function + alpha = int( matches.group('alpha') ) / 255 + css_rgba = f"({matches.group('red')}, {matches.group('green')}, {matches.group('blue')}, {alpha!s:.4})" - for line in color_file: - match = p.match(line) - if match: - color_variable_name = match.group(1) - color_tuple = tuple(int(num) for num in match.group(2).strip('()').split(',')) - color_rgb_string = ', '.join(str(i) for i in color_tuple[:3]) + append_text += " " + append_text += f"" + append_text += f"" + append_text += f"" + append_text += "\n" - append_text += " " - append_text += f"" - append_text += f"" - append_text += f"" - append_text += "\n" + append_text += "
{matches.group('name')}{color_rgba}
 
{color_variable_name}{color_tuple}
" + source[0] += append_text + + +def source_read(_app, docname, source): + """Event handler for source-read event""" - append_text += " " - source[0] = original_text + append_text + file_path = os.path.dirname(os.path.abspath(__file__)) + os.chdir(file_path) + + # Transform source for arcade.color and arcade.csscolor + if docname == "api_docs/arcade.color": + generate_color_table("../arcade/color/__init__.py", source) + elif docname == "api_docs/arcade.csscolor": + generate_color_table("../arcade/csscolor/__init__.py", source) def post_process(_app, _exception): diff --git a/doc/example_code/how_to_examples/gui_flat_button.rst b/doc/example_code/how_to_examples/gui_flat_button.rst index 42fd64c31..dd2da02ba 100644 --- a/doc/example_code/how_to_examples/gui_flat_button.rst +++ b/doc/example_code/how_to_examples/gui_flat_button.rst @@ -32,3 +32,29 @@ that uses all three ways. .. literalinclude:: ../../../arcade/examples/gui_flat_button.py :caption: gui_flat_button.py :linenos: + +See :class:`arcade.gui.UIBoxLayout` and :class:`arcade.gui.UIAnchorLayout` +for more information about positioning the buttons. +For example, this change to line 31: + +.. code-block:: python + + self.v_box = arcade.gui.widgets.layout.UIBoxLayout(space_between=20, vertical=False); + +and to line 60: + +.. code-block:: python + + ui_anchor_layout.add(child=self.v_box, + anchor_x="left", + anchor_y="bottom", + align_x=10, + align_y=10); + +in the code above will align the buttons horizontally and anchor them to the +bottom left of the window with 10px margins. + +.. image:: ../how_to_examples/gui_flat_button_positioned.png + :width: 600px + :align: center + :alt: Screen shot of flat text buttons in bottom left of window \ No newline at end of file diff --git a/doc/example_code/how_to_examples/gui_flat_button_positioned.png b/doc/example_code/how_to_examples/gui_flat_button_positioned.png new file mode 100644 index 0000000000000000000000000000000000000000..c77e9f3e103e41913a016ef8cfd3aab1ca2713c2 GIT binary patch literal 6843 zcmeHM2~d;Smi`fyD6K&e5NMzg1#MK6Rb+cCD##L1b_7KBUABaa|rfHB(blsnlO}^WAgL{mwbxxi|mg z8z$Ed9TYhT0Kg#weZ89i!2J^d?16&za7Ip-`#W+T2kz-x2Lb>u|IU{Sc=h@?XYeN{ z{hP)B5PAv#?neQ@CY$s81^~eq0DyW204~1-03p9;4j-=qfQYt%p0-)Y&^*~A+-xeV zWeg@QdB6OOqt0HvW7@~6K%|35ifhc}Hi-2n%+UID9rI)PUroGHe~vp*`^$ZEf8Cta zxYW}Dg@Tb5V-L!$B+s9I`LPrPx|o6=0dqU;)k~`ddRA~e0n>OqR!vTU{W{R~sDX3} z7jU1-ue6u58D8Kocenu1Spe882;6Vk0}SZ8_)e66N(`)Cv3Q2`_B+i5+?;RS1LV67 z4@J?nP>BdOYaK(EKCC+VgsI&3%892ww;2%#zR;6L(>21N%(V=OqznZ&lbFL| zP+!C52=JGg8RQ$rP^)b>jUOv&T{=~>8F3^oZbx#7!wIY!VK!jlZe$(*lm>;ZA zn-weuuEpQrLs&MNWtsBN~eNm!9@-c(Y+J<2XZha=wNB31Y3|FD@iLs{?7*&d` zzQHg{w6bgF8OeG%9r(rjE&(rdG{OEXfUTE$72f7!o%( zFn=*oKxg1%{lVeD*wXE2hB;=sWC(pi9G!m}9oso`-huKS2k&3a7D&Jk2OUTM%GHEU zlJZrLeMymdrXr7Y9)0JsPn2y<7jY;qO`Uk~O*WhqbBgOexwH)0RPy0)W>ghqVq{H- z!q6tX>LC@QPbXV;;AF?AA{zQdRVQcE!e|OiiC4avX)ISGk71-0;uDRiWHcBwS&EzO zBMh8r>il8wn4tm_d|=`xuKsH1Bmeg}w>610iRR`H&yHjvd(bi#2$U{rq55QSieoIX z92j8gRbw~I;upwK{nRRp;N|nSH)_XLg6Cw%m*PU;SK#pBk@BlbLSIbq2j{k7d5cZN zShKm4*I)+yea}BD^(q+|L^PDeo_MN%vNzbM-z#yBBp>~G3O|~4tA=RvD3NVnTKjcQ zwsIbJi(ks;3IFZIVl)VlH!w8*(UzH@AmD??S{1bJ-v`Xv2N}sCinF^r<{G7V_M~q7OWltFdZwDXdl?^2{#B&{2a-ydN%oOVh6H`Q-D@!NfsRL&2{3pp7ia z=m$cm%sQ6s<&{Ap-VYsK5iEP#YERLQOY$JVyQ<`-A+)Ki`BdrQL1%J(a1+Gc{zx*+ ziwy@!o*d7*yUj z;U%Rp+<56CD(h}UK4IXeJ%CwiT^nhj<`#%h>gca;7X65|9q6yC|L*u7FSiS{BeCBD+QrDu)IYZLy`bGu;yAv` zr(KqIgLcvOr{cXUO8+=1z8AEMwwCGL70x?wx{Dt9DZs&|fLnf5#_r+H@(GgiHN zMak1yAoUmo?lE+J^h#6cp@Z?fG8$S_wxlBSLMTnsGOs2tr_vcdmd7ux>G!NGlRt#% z;N(dft;x>`S>~0qw381Wnv3T>nD8?t?}Y=rikdQNc*}wOQ526aGQDY2J_jNHR0PrsJ-f%stFDMzeVZ+aJsnRSqYtFWVb~-9x*6kxfwUUmUmJ*^#1W-vw5}3u3;% z$=QbdoDI&X?@XeZg~i$|!eYXz`}44$V$QsyJd9$jMQbGTKX%3xo;PM+>*~U?w5xMk z5r^2@LxojdBr2haRFsZ{$T`%NulA)tV@J<1eAt*p!i) zm{EVCQ}U==>;P_IGzO%_sthT_b!sViz{kqn6??PVOi$&Wdie3-dcyIPbnA(ZR$XF4 zxt9H!!RQi3zZhj+G0n;r-<+MumB|bm>hnMgr5?jvTyc<$UgDP)2rs=PB_$m_#*&R@H= z2kFmH=-F1P3isF6?lfViH=~ED))!VXA~WpPqmng6ejBDdd{X^spo$_)9luMJUg+4(VQA?F;wX>Q7nBWdC^S?>+?$g zq+1U{CQS_2g>7=ZLGgzeU0SSly4g?;_8tnK`n4aDj66xUl8m;g@+2#5RoBd%^ZXH~ z+DwG#f;76d3a46&f0mVX!@m^6CHc_Z4P9No9FY032D%^wJJbLdwQXQ;4?1;dLI)gU za+JTuDqoX1iY)_cJ@GB#)A9AqzUXwVd3}`0qwIt5?~y%xIUhR$UqPX^K42*4E|q$$ zu7Z;L`ueZ0aGP=N?o9G(X@bY9U~5(7bb?nP%ZCg+0L6XOCfOT zC&P8$CKC;MyU@0>lG3>h6)(;mlCA3H%a}DP0&P_ODv5fQo43C$_#|$<>X1VRNJgLwYG>bDu`H zCaW^h&E4F>VjDjG9FNC0Hu4vgaEf>8m&g|jpIx1u^@gi`>~5Cd9WE@sZ9SCDwC04y zizrr%TD`rk$?RJkC<_eDq|d3SZ%*d4svJHoJA5H*O_Z32F~?|=J&5nX%^rWT9(<%i zd8O&~Wkf33n?;;J2WMKGm$Xo$z@O`=9$ zZDSC`z1{eti@qZYQ&UrU!K$kCw@OYOm5=$^Ulw1s$x-2(-DEpBeDfBBMIX|R&;>0m z%b~)~V|>XGJcsL5qn}u3U$q}aHm;AxkQ~BXK#pw;I#~U;kj%@>E$V|&$!K@QY!_UV zARb9WRoEYrJSK<;@Q!g(4-cz@E+knsd(SZ)C@;6KgCqrU%GW_f z815mjkk9Qb`LckI)bjH3D;a4gWm9CYTe`UP zyA$5JfFu`TD* z7Sb}aY-PGTS?_IKof`a&1swiy3nrL@Y(0JUY~0oC!9q^w1zi`Up+1Y zlr{M#x5n#s{p-DfJ$0u=5?_andQd8D5=E@nHGa$MOUi}Br%^O3Loc6wY9uK1qS}MF z%;~2NNVX|B$Wg+m2uW1=d7^}tYDBj&mE)krdSYW2ev~Y8@5EDl4>F>H zr^aBB3-*K9?FG>rN0NJ9E?MBxy3pbqOg^118zm%s8XGu`hdk^PawrO$ZFM{c_En-r zc8V%{Uzjh+K!q~o#H*S{mR&v?zw(WMz^U0+1<%>sea>eU7Ax%6xNJPLH-}37xMkU` z*??;)^hYmQ?(OdO{f#veO#QJ)$?J=K7+HYR`2uIU(luFKtD6tbczr2q-dyJf zdj)SyVu?wtZx99>S{mc+S3OHUB^iBAi}v%;$I!*_yC6rxhmTheL{`nX_bTS)Uf%q)m2e%B8}! z%V!ump-P)K;J=h^)XZ2-TUZIODJ*m{^+V4;_Vg>+o}b@iL- ziqtGVon(j1rl~6|p4;`_)}{=z#PPR1Iv}1&@zSMJG9s!NruBZlNcu!7gKun=1`!q( zu9(3W78N!6d=h(q3hTU_3EPh7GmXhb-`w!P_8_3QJ*T=eA%oqz(U0}+j(s>4BY~Jz z{$kw#?RqG*+Kt5`*ll3B#(NWL19N@vVQ@+zC*2EDQ89c+Qy=&Cns)|!3+a3+u&!7+ zkheH4`JuniGD&p3kBg_>w|1<)?^!5aWVLCTLCWhOOeSX-+y zvdXNjtzlq^`m#uQuiF70>|{;*dXG?A#(u%78VxNUM`H-$WpD77pWwb0UhB7 zXCm#<3w7DSb6;LZObas?EVbzAiQMSTwzRqSiK$x-+g|b0JX+fusF>th7h;T};Oabl zQKPMM`(4i!$>`qR@mnUExgNw0PJ-gnkO3sJZm50GW=XN_8R z1yQ7hRem*?{0_I_tYF>h*DddBYGqO$lk78kD-9}&M6AD6?B`+owrKMxL$nwe^(HTl zpy!Of5ACX|d7kQXJcyoZm=>-Wz9GTcp#);`+XCgK^NGztH(5@TR*5LLY(Q0NeakVC z!z_D(#=gmPHec9G?Q=bAwE<-@f+LK$C8H6R_9D0>u|WKhZ|CKJlIA@XgRxUoPmhRY2oCD%poz+_4qdmXW zU(y48*S{Mr_UC#V*4?uK;6~!kYnMO1jQ)N{zpv541_K8dwasBXzt(YHJOKt*O!P{0 H?mYMxZ;|D) literal 0 HcmV?d00001 diff --git a/doc/get_started/install/index.rst b/doc/get_started/install/index.rst index 0f62152c4..6f47c01f8 100644 --- a/doc/get_started/install/index.rst +++ b/doc/get_started/install/index.rst @@ -19,3 +19,15 @@ Select the instructions for your platform: pycharm obsolete +Advanced +-------- + +Use the following command to install the latest development build of Arcade: + +.. code-block:: bash + + pip install -I https://github.com/pythonarcade/arcade/archive/refs/heads/development.zip + +This pre-release build will give you access to the latest features, but it may be unstable. + +You can also get pre-release versions from the `Arcade PyPi Release History `_. \ No newline at end of file diff --git a/doc/index.rst b/doc/index.rst index 49929c5d3..9cac29faf 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -20,161 +20,169 @@ The Python Arcade Library game without learning a complex framework.

-.. |Go| image:: images/woman_sprinter.svg - :width: 48 - :alt: Start Here - :target: get_started.html - -.. raw:: html - - - - - - -
-

- - Start Here - Start Here - - -

-
- -
- -.. image:: images/example_games.svg - :alt: Get Started icon - :class: heading-icon - -.. toctree:: - :maxdepth: 1 - :caption: Get Started - - get_started/introduction - get_started/get_started - get_started/install/index - get_started/how_to_get_help - -.. image:: images/example_code.svg - :alt: Example Code - :class: heading-icon - -.. toctree:: - :maxdepth: 1 - :caption: Examples - - example_code/how_to_examples/index - example_code/game_jam_2020 - example_code/sample_games - -.. image:: images/learn.svg - :alt: Tutorials - :class: heading-icon - -.. toctree:: - :maxdepth: 1 - :caption: Tutorials - - tutorials/platform_tutorial/index - tutorials/pymunk_platformer/index - tutorials/views/index - tutorials/card_game/index - tutorials/lights/index - tutorials/bundling_with_pyinstaller/index - tutorials/compiling_with_nuitka/index - tutorials/shader_tutorials - tutorials/menu/index - tutorials/framebuffer/index - -.. image:: images/example_games.svg - :alt: Programming guide icon - :class: heading-icon - -.. toctree:: - :maxdepth: 1 - :caption: Guide - - programming_guide/sprites/index - programming_guide/keyboard - programming_guide/sound - programming_guide/textures - programming_guide/sections - programming_guide/gui/index - programming_guide/texture_atlas - programming_guide/edge_artifacts/index - programming_guide/logging - programming_guide/opengl_notes - programming_guide/performance_tips - programming_guide/headless - programming_guide/vsync - programming_guide/pygame_comparison - -.. image:: images/API.svg - :alt: API icon - :class: heading-icon - -.. toctree:: - :maxdepth: 1 - :caption: API - - Index - Reference - api_docs/resources - -.. image:: images/source.svg - :alt: Source icon - :class: heading-icon - -.. toctree:: - :maxdepth: 1 - :caption: Source Code - - GitHub - programming_guide/release_notes - License - -.. image:: images/source.svg - :alt: Source icon - :class: heading-icon - -.. toctree:: - :maxdepth: 1 - :caption: Contributing - - contributing_guide/index - contributing_guide/release_checklist - -.. image:: images/social.svg - :alt: Social icon - :class: heading-icon - -Social ------- - -.. container:: toc-outside-links - - * `Discord (most active spot) `_ - * `Reddit /r/pythonarcade `_ - * `Twitter @ArcadeLibrary `_ - * `Instagram @PythonArcadeLibrary `_ - * `Facebook @ArcadeLibrary `_ - * :ref:`diversity_statement` - -.. image:: images/performance.svg - :alt: Performance icon - :class: heading-icon - -Learning Resources ------------------- - -.. container:: toc-outside-links - - * `Book - Learn to program with Arcade `_ - * `Peer To Peer Gaming With Arcade and Python Banyan `_ - * `US PyCon 2022 Talk `_ - * `US PyCon 2019 Tutorial `_ - * `Aus PyCon 2018 Multiplayer Games `_ - * `US PyCon 2018 Talk `_ +.. container:: main-page-item-wrapper-header + + .. raw:: html + +
+ +
+ +
+
+ +.. container:: main-page-item-wrapper + + .. image:: images/example_games.svg + :alt: Get Started icon + :class: heading-icon + + .. toctree:: + :maxdepth: 1 + :caption: Get Started + + get_started/introduction + get_started/get_started + get_started/install/index + get_started/how_to_get_help + +.. container:: main-page-item-wrapper + + .. image:: images/example_code.svg + :alt: Example Code + :class: heading-icon + + .. toctree:: + :maxdepth: 1 + :caption: Examples + + example_code/how_to_examples/index + example_code/game_jam_2020 + example_code/sample_games + +.. container:: main-page-item-wrapper + + .. image:: images/learn.svg + :alt: Tutorials + :class: heading-icon + + .. toctree:: + :maxdepth: 1 + :caption: Tutorials + + tutorials/platform_tutorial/index + tutorials/pymunk_platformer/index + tutorials/views/index + tutorials/card_game/index + tutorials/lights/index + tutorials/bundling_with_pyinstaller/index + tutorials/compiling_with_nuitka/index + tutorials/shader_tutorials + tutorials/menu/index + tutorials/framebuffer/index + +.. container:: main-page-item-wrapper + + .. image:: images/example_games.svg + :alt: Programming guide icon + :class: heading-icon + + .. toctree:: + :maxdepth: 1 + :caption: Guide + + programming_guide/sprites/index + programming_guide/keyboard + programming_guide/sound + programming_guide/textures + programming_guide/sections + programming_guide/gui/index + programming_guide/texture_atlas + programming_guide/edge_artifacts/index + programming_guide/logging + programming_guide/opengl_notes + programming_guide/performance_tips + programming_guide/headless + programming_guide/vsync + programming_guide/pygame_comparison + +.. container:: main-page-item-wrapper + + .. image:: images/API.svg + :alt: API icon + :class: heading-icon + + .. toctree:: + :maxdepth: 1 + :caption: API + + Index + Reference + api_docs/resources + +.. container:: main-page-item-wrapper + + .. image:: images/source.svg + :alt: Source icon + :class: heading-icon + + .. toctree:: + :maxdepth: 1 + :caption: Source Code + + GitHub + programming_guide/release_notes + License + contributing_guide/index + contributing_guide/release_checklist + +.. container:: main-page-item-wrapper + + .. image:: images/social.svg + :alt: Social icon + :class: heading-icon + + .. container:: main-page-item-sub-wrapper + + .. toctree:: + :maxdepth: 1 + :caption: Social + + Discord (most active spot) + Reddit /r/pythonarcade + Twitter @ArcadeLibrary + Instagram @PythonArcadeLibrary + Facebook @ArcadeLibrary + community/diversity + +.. container:: main-page-item-wrapper + + .. image:: images/performance.svg + :alt: Performance icon + :class: heading-icon + + .. container:: main-page-item-sub-wrapper + + .. toctree:: + :maxdepth: 1 + :caption: Learning Resources + + Book - Learn to program with Arcade + Peer To Peer Gaming With Arcade and Python Banyan + US PyCon 2022 Talk + US PyCon 2019 Tutorial + Aus PyCon 2018 Multiplayer Games + US PyCon 2018 Talk diff --git a/doc/programming_guide/gui/concept.rst b/doc/programming_guide/gui/concept.rst index d61fcd8c3..e283b3e3c 100644 --- a/doc/programming_guide/gui/concept.rst +++ b/doc/programming_guide/gui/concept.rst @@ -547,5 +547,5 @@ Property ```````` :py:class:`~arcade.gui.Property` is an pure-Python implementation of Kivy -Properties. They are used to detect attribute changes of widgets and trigger -rendering. They should only be used in arcade internal code. +like Properties. They are used to detect attribute changes of widgets and trigger +rendering. They are mostly used within GUI widgets, but are globally available since 3.0.0. diff --git a/doc/programming_guide/sound.rst b/doc/programming_guide/sound.rst index f4f1ae656..2a0bae66e 100644 --- a/doc/programming_guide/sound.rst +++ b/doc/programming_guide/sound.rst @@ -279,7 +279,7 @@ fully decompressed albums of music in RAM. Each decompressed minute of CD quality audio uses slightly over 10 MB of RAM. This adds up quickly, and can slow down or freeze a computer if it fills RAM completely. -For music and long background audio, you should should strongly consider +For music and long background audio, you should strongly consider :ref:`streaming ` from compressed files instead. diff --git a/doc/tutorials/raycasting/example.py b/doc/tutorials/raycasting/example.py index 404b8fad6..f6d7f0ebe 100644 --- a/doc/tutorials/raycasting/example.py +++ b/doc/tutorials/raycasting/example.py @@ -90,12 +90,12 @@ def load_shader(self): def on_draw(self): self.channel0.use() # clear_color = 0, 0, 0, 0 - # self.channel0.clear(clear_color) + # self.channel0.clear(color=clear_color) self.wall_list.draw() self.channel1.use() - # self.channel1.clear(clear_color) - self.channel1.clear(arcade.color.ARMY_GREEN) + # self.channel1.clear(color=clear_color) + self.channel1.clear(color=arcade.color.ARMY_GREEN) self.bomb_list.draw() self.use() diff --git a/doc/tutorials/shader_inputs/texture_write.py b/doc/tutorials/shader_inputs/texture_write.py index e28513960..c6ad51a75 100644 --- a/doc/tutorials/shader_inputs/texture_write.py +++ b/doc/tutorials/shader_inputs/texture_write.py @@ -20,7 +20,7 @@ def __init__(self): self.fbo = self.ctx.framebuffer(color_attachments=[self.tex]) # Put something in the framebuffer to start - self.fbo.clear(arcade.color.ALMOND) + self.fbo.clear(color=arcade.color.ALMOND) with self.fbo: arcade.draw_circle_filled( SCREEN_WIDTH / 2, diff --git a/doc/tutorials/shader_inputs/textures.py b/doc/tutorials/shader_inputs/textures.py index f6cb7af4d..f792b91cf 100644 --- a/doc/tutorials/shader_inputs/textures.py +++ b/doc/tutorials/shader_inputs/textures.py @@ -23,8 +23,8 @@ def __init__(self): self.fbo_1 = self.ctx.framebuffer(color_attachments=[self.tex_1]) # Fill the textures with solid colours - self.fbo_0.clear(color=(0.0, 0.0, 1.0, 1.0), normalized=True) - self.fbo_1.clear(color=(1.0, 0.0, 0.0, 1.0), normalized=True) + self.fbo_0.clear(color_normalized=(0.0, 0.0, 1.0, 1.0)) + self.fbo_1.clear(color_normalized=(1.0, 0.0, 0.0, 1.0)) # Create a simple shader program self.prog = self.ctx.program( diff --git a/doc/tutorials/shader_toy_particles/index.rst b/doc/tutorials/shader_toy_particles/index.rst index 52e096e58..7a56dc20a 100644 --- a/doc/tutorials/shader_toy_particles/index.rst +++ b/doc/tutorials/shader_toy_particles/index.rst @@ -4,7 +4,8 @@ Shader Toy - Particles ====================== .. contents:: - + :class: this-will-duplicate-information-and-it-is-still-useful-here + .. raw:: html diff --git a/pyproject.toml b/pyproject.toml index 4e92ca258..0b6abf181 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,13 +17,14 @@ classifiers = [ "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", "Programming Language :: Python :: Implementation :: CPython", "Topic :: Software Development :: Libraries :: Python Modules" ] dependencies = [ - "pyglet>=2.0.12,<2.1", - "pillow~=10.0.0", - "pymunk~=6.5.1", + "pyglet>=2.0.14,<2.1", + "pillow~=10.2.0", + "pymunk~=6.6.0", "pytiled-parser~=2.2.3" ] dynamic = ["version"] @@ -47,16 +48,16 @@ dev = [ "coveralls", "pytest-mock", "pytest-cov", - "pygments==2.16.1", + "pygments==2.17.2", "docutils==0.20.1", "furo", - "pyright==1.1.352", + "pyright==1.1.355", "pyyaml==6.0.1", - "sphinx==7.2.2", - "sphinx-autobuild==2021.3.14", + "sphinx==7.2.6", + "sphinx-autobuild==2024.2.4", "sphinx-copybutton==0.5.2", "sphinx-sitemap==2.5.1", - "typer[all]==0.7.0", + "typer[all]==0.11.0", "wheel", ] # Testing only @@ -65,7 +66,6 @@ testing_libraries = [ "pytest-mock", "pytest-cov", "pyyaml==6.0.1", - "typer[all]==0.7.0", ] [project.scripts] diff --git a/tests/conftest.py b/tests/conftest.py index 5f2ed9b80..46b581477 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,5 +1,7 @@ import gc import os +import sys +from contextlib import contextmanager from pathlib import Path if os.environ.get("ARCADE_PYTEST_USE_RUST"): @@ -13,13 +15,14 @@ PROJECT_ROOT = (Path(__file__).parent.parent).resolve() FIXTURE_ROOT = PROJECT_ROOT / "tests" / "fixtures" arcade.resources.add_resource_handle("fixtures", FIXTURE_ROOT) +REAL_WINDOW_CLASS = arcade.Window WINDOW = None -def create_window(): +def create_window(width=800, height=600, caption="Testing", **kwargs): global WINDOW if not WINDOW: - WINDOW = arcade.Window(title="Testing", vsync=False, antialiasing=False) + WINDOW = REAL_WINDOW_CLASS(title="Testing", vsync=False, antialiasing=False) WINDOW.set_vsync(False) # This value is being monkey-patched into the Window class so that tests can identify if we are using # arcade-accelerate easily in case they need to disable something when it is enabled. @@ -38,6 +41,10 @@ def prepare_window(window: arcade.Window): arcade.cleanup_texture_cache() # Clear the global texture cache window.hide_view() # Disable views if any is active window.dispatch_pending_events() + try: + arcade.disable_timings() + except Exception: + pass # Reset context (various states) ctx.reset() @@ -93,3 +100,130 @@ def window(): arcade.set_window(window) prepare_window(window) return window + + +class WindowProxy: + """Fake window extended by integration tests""" + + def __init__(self, width=800, height=600, caption="Test Window", *args, **kwargs): + self.window = create_window() + arcade.set_window(self) + prepare_window(self.window) + if caption: + self.window.set_caption(caption) + if width and height: + self.window.set_size(width, height) + self.window.set_viewport(0, width, 0, height) + + self._update_rate = 60 + + @property + def ctx(self): + return self.window.ctx + + @property + def width(self): + return self.window.width + + @property + def height(self): + return self.window.height + + @property + def size(self): + return self.window.size + + @property + def aspect_ratio(self): + return self.window.aspect_ratio + + @property + def mouse(self): + return self.window.mouse + + @property + def keyboard(self): + return self.window.keyboard + + def current_view(self): + return self.window.current_view + + @property + def background_color(self): + return self.window.background_color + + @background_color.setter + def background_color(self, color): + self.window.background_color = color + + def clear(self, *args, **kwargs): + return self.window.clear(*args, **kwargs) + + def flip(self): + if self.window.has_exit: + return + return self.window.flip() + + def on_draw(self): + return self.window.on_draw() + + def on_update(self, dt): + return self.window.on_update(dt) + + def show_view(self, view): + return self.window.show_view(view) + + def hide_view(self): + return self.window.hide_view() + + def get_size(self): + return self.window.get_size() + + def set_size(self, width, height): + self.window.set_size(width, height) + + def get_pixel_ratio(self): + return self.window.get_pixel_ratio() + + def set_mouse_visible(self, visible): + self.window.set_mouse_visible(visible) + + def center_window(self): + self.window.center_window() + + def set_vsync(self, vsync): + self.window.set_vsync(vsync) + + def get_viewport(self): + return self.window.get_viewport() + + def set_viewport(self, left, right, bottom, top): + self.window.set_viewport(left, right, bottom, top) + + def use(self): + self.window.use() + + def push_handlers(self, *handlers): + self.window.push_handlers(*handlers) + + def remove_handlers(self, *handlers): + self.window.remove_handlers(*handlers) + + def run(self): + self.window.run() + + +@pytest.fixture(scope="function") +def window_proxy(): + """Monkey patch the open_window function and return a WindowTools instance.""" + _window = arcade.Window + arcade.Window = WindowProxy + + _open_window = arcade.open_window + def open_window(*args, **kwargs): + return create_window(*args, **kwargs) + arcade.open_window = open_window + + yield None + arcade.Window = _window + arcade.open_window = _open_window diff --git a/tests/integration/examples/check_examples_2.py b/tests/doc/check_examples_2.py similarity index 100% rename from tests/integration/examples/check_examples_2.py rename to tests/doc/check_examples_2.py diff --git a/tests/integration/examples/check_samples.py b/tests/doc/check_samples.py similarity index 100% rename from tests/integration/examples/check_samples.py rename to tests/doc/check_samples.py diff --git a/tests/integration/examples/run_all_examples.py b/tests/integration/examples/run_all_examples.py deleted file mode 100644 index 2e293294a..000000000 --- a/tests/integration/examples/run_all_examples.py +++ /dev/null @@ -1,68 +0,0 @@ -""" -Run All Examples - -If Python and Arcade are installed, this example can be run from the command line with: -python -m tests.test_examples.run_all_examples -""" -import subprocess -import os -import glob - -EXAMPLE_SUBDIR = "../../../arcade/examples" - - -def _get_short_name(fullpath): - return os.path.splitext(os.path.basename(fullpath))[0] - - -def _get_examples(start_path): - query_path = os.path.join(start_path, "*.py") - examples = glob.glob(query_path) - examples = [_get_short_name(e) for e in examples] - examples = [e for e in examples if e != "run_all_examples"] - examples = [e for e in examples if not e.startswith('_')] - examples = ["arcade.examples." + e for e in examples if not e.startswith('_')] - return examples - - -def run_examples(indices_in_range, index_skip_list): - """Run all examples in the arcade/examples directory""" - examples = _get_examples(EXAMPLE_SUBDIR) - examples.sort() - print(f"Found {len(examples)} examples in {EXAMPLE_SUBDIR}") - - file_path = os.path.dirname(os.path.abspath(__file__)) - print(file_path) - os.chdir(file_path+"/../..") - # run examples - for (idx, example) in enumerate(examples): - if indices_in_range is not None and idx not in indices_in_range: - continue - if index_skip_list is not None and idx in index_skip_list: - continue - print(f"=================== Example {idx + 1:3} of {len(examples)}: {example}") - # print('%s %s (index #%d of %d)' % ('=' * 20, example, idx, len(examples) - 1)) - - # Directly call venv, necessary for github action runner - cmd = 'python -m ' + example - - # print(cmd) - result = subprocess.check_output(cmd, shell=True) - if result: - print(f"ERROR: Got a result of: {result}.") - - -def all_examples(): - file_path = os.path.dirname(os.path.abspath(__file__)) - os.chdir(file_path) - - # Set an environment variable that will just run on_update() and on_draw() - # once, then quit. - os.environ['ARCADE_TEST'] = "TRUE" - - indices_in_range = None - index_skip_list = None - run_examples(indices_in_range, index_skip_list) - - -all_examples() diff --git a/tests/integration/examples/test_all_examples.py b/tests/integration/examples/test_all_examples.py deleted file mode 100644 index 437fec4ec..000000000 --- a/tests/integration/examples/test_all_examples.py +++ /dev/null @@ -1,78 +0,0 @@ -""" -Run All Examples - -If Python and Arcade are installed, this example can be run from the command line with: -python -m tests.test_examples.test_all_examples -""" -import glob -import os -import subprocess - -import pytest - -EXAMPLE_SUBDIR = "../../../arcade/examples" -# These examples are allowed to print to stdout -ALLOW_STDOUT = set([ - "arcade.examples.dual_stick_shooter", - "arcade.examples.net_process_animal_facts", -]) - -def _get_short_name(fullpath): - return os.path.splitext(os.path.basename(fullpath))[0] - - -def _get_examples(start_path): - query_path = os.path.join(start_path, "*.py") - examples = glob.glob(query_path) - examples = [_get_short_name(e) for e in examples] - examples = [e for e in examples if e != "run_all_examples"] - examples = [e for e in examples if not e.startswith('_')] - examples = ["arcade.examples." + e for e in examples if not e.startswith('_')] - return examples - - -def find_examples(indices_in_range, index_skip_list): - """List all examples in the arcade/examples directory""" - examples = _get_examples(EXAMPLE_SUBDIR) - examples.sort() - print(f"Found {len(examples)} examples in {EXAMPLE_SUBDIR}") - - file_path = os.path.dirname(os.path.abspath(__file__)) - print(file_path) - os.chdir(f"{file_path}/../..") - - for (idx, example) in enumerate(examples): - if indices_in_range is not None and idx not in indices_in_range: - continue - if index_skip_list is not None and idx in index_skip_list: - continue - - allow_stdout = example in ALLOW_STDOUT - yield f'python -m {example}', allow_stdout - - -def list_examples(indices_in_range, index_skip_list): - file_path = os.path.dirname(os.path.abspath(__file__)) - os.chdir(file_path) - - return list(find_examples(indices_in_range, index_skip_list)) - - -@pytest.mark.parametrize( - "cmd, allow_stdout", - argvalues=list_examples( - indices_in_range=None, - index_skip_list=None - ) -) -def test_all(cmd, allow_stdout): - # Set an environment variable that will just run on_update() and on_draw() - # once, then quit. - import pyglet - test_env = os.environ.copy() - test_env["ARCADE_TEST"] = "TRUE" - - result = subprocess.check_output(cmd, shell=True, env=test_env) - if result and not allow_stdout: - print(f"ERROR: Got a result of: {result}.") - assert not result diff --git a/tests/integration/examples/test_examples.py b/tests/integration/examples/test_examples.py new file mode 100644 index 000000000..e81e2f992 --- /dev/null +++ b/tests/integration/examples/test_examples.py @@ -0,0 +1,64 @@ +""" +Import and run all examples one frame +""" +import contextlib +import io +import inspect +import os +from importlib.machinery import SourceFileLoader +from pathlib import Path + +import arcade +import pytest + +# TODO: Also add platform_tutorial and gl +EXAMPLE_DIR = Path(arcade.__file__).parent / "examples" +# These examples are allowed to print to stdout +ALLOW_STDOUT = set([ + "arcade.examples.dual_stick_shooter", + "arcade.examples.net_process_animal_facts", +]) +IGNORE_PATTERNS = [ + 'net_process_animal_facts' +] + +def list_examples(): + for example in EXAMPLE_DIR.glob("*.py"): + if example.stem.startswith("_"): + continue + if example.stem in IGNORE_PATTERNS: + continue + yield f"arcade.examples.{example.stem}", example, True + + +def find_class_inheriting_from_window(module): + for name, obj in module.__dict__.items(): + match = inspect.isclass(obj) and issubclass(obj, arcade.Window) + if match: + return obj + return None + + +def find_main_function(module): + if "main" in module.__dict__: + return module.__dict__["main"] + return None + + +@pytest.mark.parametrize( + "module_path, file_path, allow_stdout", + list_examples(), +) +def test_examples(window_proxy, module_path, file_path, allow_stdout): + """Run all examples""" + os.environ["ARCADE_TEST"] = "TRUE" + + stdout = io.StringIO() + with contextlib.redirect_stdout(stdout): + # Manually load the module as __main__ so it runs on import + loader = SourceFileLoader("__main__", str(file_path)) + loader.exec_module(loader.load_module()) + + if not allow_stdout: + output = stdout.getvalue() + assert not output, f"Example {module_path} printed to stdout: {output}" diff --git a/tests/integration/tutorials/run_all_tutorials.py b/tests/integration/tutorials/run_all_tutorials.py deleted file mode 100644 index 3abd1c36f..000000000 --- a/tests/integration/tutorials/run_all_tutorials.py +++ /dev/null @@ -1,67 +0,0 @@ -""" -Run All tutorials - -If Python and Arcade are installed, this tutorial can be run from the command line with: -python -m tests.test_tutorials.run_all_tutorials -""" -import glob -import subprocess -import os -from pathlib import Path - -TUTORIAL_SUBDIR = "../../../doc/tutorials/" - - -def _get_short_name(fullpath): - return os.path.splitext(os.path.basename(fullpath))[0] - -def _get_tutorials(start_path): - query_path = os.path.join(start_path, "*.py") - tutorials = glob.glob(query_path) - tutorials = [_get_short_name(e) for e in tutorials] - tutorials = [e for e in tutorials if e != "run_all_tutorials"] - tutorials = [e for e in tutorials if not e.startswith('_')] - tutorials = [f"doc.tutorials.{start_path.name}." + e for e in tutorials if not e.startswith('_')] - return tutorials - -def run_tutorials(indices_in_range = None, index_skip_list = None): - """Run all tutorials in the doc/tutorials directory""" - for tutorial_subdir in [path for path in list(Path.cwd().joinpath(TUTORIAL_SUBDIR).iterdir()) if path.is_dir()]: - tutorials = _get_tutorials(tutorial_subdir) - tutorials.sort() - print(f"Found {len(tutorials)} tutorials in {tutorial_subdir}") - - file_path = os.path.dirname(os.path.abspath(__file__)) - print(file_path) - os.chdir(file_path+"/../..") - # run tutorials - for (idx, tutorial) in enumerate(tutorials): - if indices_in_range is not None and idx not in indices_in_range: - continue - if index_skip_list is not None and idx in index_skip_list: - continue - print(f"=================== tutorial {idx + 1:3} of {len(tutorials)}: {tutorial}") - # print('%s %s (index #%d of %d)' % ('=' * 20, tutorial, idx, len(tutorials) - 1)) - - # Directly call venv, necessary for github action runner - cmd = 'python -m ' + tutorial - - # print(cmd) - result = subprocess.check_output(cmd, shell=True) - if result: - print(f"ERROR: Got a result of: {result}.") - -def all_tutorials(): - file_path = os.path.dirname(os.path.abspath(__file__)) - os.chdir(file_path) - - # Set an environment variable that will just run on_update() and on_draw() - # once, then quit. - os.environ['ARCADE_TEST'] = "TRUE" - - indices_in_range = None - index_skip_list = None - run_tutorials(indices_in_range, index_skip_list) - - -all_tutorials() diff --git a/tests/integration/tutorials/test_all_tutoirals.py b/tests/integration/tutorials/test_all_tutoirals.py deleted file mode 100644 index aeb94419b..000000000 --- a/tests/integration/tutorials/test_all_tutoirals.py +++ /dev/null @@ -1,76 +0,0 @@ -""" -Run All tutorials - -If Python and Arcade are installed, this tutorial can be run from the command line with: -python -m tests.test_tutorials.test_all_tutorials -""" -import glob -import os -import subprocess -from pathlib import Path - -import pytest - -TUTORIAL_SUBDIR = "../../../doc/tutorials/" -# These tutorials are allowed to print to stdout -ALLOW_STDOUT = set() - -def _get_short_name(fullpath): - return os.path.splitext(os.path.basename(fullpath))[0] - -def _get_tutorials(start_path): - query_path = os.path.join(start_path, "*.py") - tutorials = glob.glob(query_path) - tutorials = [_get_short_name(e) for e in tutorials] - tutorials = [e for e in tutorials if e != "run_all_tutorials"] - tutorials = [e for e in tutorials if not e.startswith('_')] - tutorials = [f"{e}.py" for e in tutorials if not e.startswith('_')] - return tutorials - -def find_tutorials(indices_in_range, index_skip_list): - """List all tutorials in the doc/tutorials directory""" - file_dir = Path(__file__).parent - for tutorial_subdir in [path for path in list((file_dir / TUTORIAL_SUBDIR).iterdir()) if path.is_dir()]: - tutorials = _get_tutorials(tutorial_subdir) - tutorials.sort() - print(f"Found {len(tutorials)} tutorials in {tutorial_subdir}") - if len(tutorials) == 0: - continue - print(tutorial_subdir) - # os.chdir(tutorial_subdir) - for (idx, tutorial) in enumerate(tutorials): - if indices_in_range is not None and idx not in indices_in_range: - continue - if index_skip_list is not None and idx in index_skip_list: - continue - - allow_stdout = tutorial in ALLOW_STDOUT - yield f'python {tutorial}', allow_stdout, tutorial_subdir - # os.chdir("../") - - -def list_tutorials(indices_in_range, index_skip_list): - file_path = os.path.dirname(os.path.abspath(__file__)) - os.chdir(file_path) - - return list(find_tutorials(indices_in_range, index_skip_list)) - - -@pytest.mark.parametrize( - "cmd, allow_stdout, tutorial_subdir", - argvalues=list_tutorials( - indices_in_range=None, - index_skip_list=None - ) -) -def test_all(cmd, allow_stdout, tutorial_subdir): - # Set an environment variable that will just run on_update() and on_draw() - # once, then quit. - import pyglet - test_env = os.environ.copy() - test_env["ARCADE_TEST"] = "TRUE" - os.chdir(tutorial_subdir) - result = subprocess.check_output(cmd, shell=True, env=test_env) - if result and not allow_stdout: - print(f"ERROR: Got a result of: {result}.") - assert not result diff --git a/tests/integration/tutorials/test_tutorials.py b/tests/integration/tutorials/test_tutorials.py new file mode 100644 index 000000000..dd179b276 --- /dev/null +++ b/tests/integration/tutorials/test_tutorials.py @@ -0,0 +1,47 @@ +""" +FInd and run all tutorials in the doc/tutorials directory +""" +import io +import os +import contextlib +from importlib.machinery import SourceFileLoader +from pathlib import Path +import pytest +import arcade + +TUTORIAL_DIR = Path(arcade.__file__).parent.parent / "doc" /"tutorials" +ALLOW_STDOUT = {} + + +def find_tutorials(): + # Loop the directory of tutorials dirs + for dir in TUTORIAL_DIR.iterdir(): + if not dir.is_dir(): + continue + + print(dir) + # Find python files in each tutorial dir + for file in dir.glob("*.py"): + if file.stem.startswith("_"): + continue + # print("->", file) + yield file, file.stem in ALLOW_STDOUT + + +@pytest.mark.parametrize( + "file_path, allow_stdout", + find_tutorials(), +) +def test_tutorials(window_proxy, file_path, allow_stdout): + """Run all tutorials""" + os.environ["ARCADE_TEST"] = "TRUE" + stdout = io.StringIO() + with contextlib.redirect_stdout(stdout): + # Manually load the module as __main__ so it runs on import + os.chdir(file_path.parent) + loader = SourceFileLoader("__main__", str(file_path)) + loader.exec_module(loader.load_module()) + + if not allow_stdout: + output = stdout.getvalue() + assert not output, f"Example {file_path} printed to stdout: {output}" diff --git a/tests/unit/gl/test_opengl_framebuffer.py b/tests/unit/gl/test_opengl_framebuffer.py index 7877556e7..a809945d5 100644 --- a/tests/unit/gl/test_opengl_framebuffer.py +++ b/tests/unit/gl/test_opengl_framebuffer.py @@ -55,8 +55,8 @@ def test_clear(ctx): ctx.window.use() fb = create(ctx, 10, 20, components=4) fb.clear() - fb.clear(color=(0, 0, 0, 0), normalized=True) - fb.clear(color=(0, 0, 0), normalized=True) + fb.clear(color_normalized=(0, 0, 0, 0)) + fb.clear(color_normalized=(0, 0, 0)) fb.clear(color=arcade.csscolor.AZURE) fb.clear(color=(0, 0, 0)) fb.clear(color=(0, 0, 0, 0)) @@ -156,3 +156,10 @@ def test_resize(ctx): fbo.resize() assert fbo.size == tex.size assert fbo.viewport == (0, 0, *fbo.size) + +def test_read_screen_framebuffer(window): + components = 3 + data = window.ctx.screen.read(components=components) + assert isinstance(data, bytes) + w, h = window.get_framebuffer_size() + assert len(data) == w * h * components diff --git a/tests/unit/gui/test_property.py b/tests/unit/gui/test_property.py index 3e48bfb7b..9ac5cefad 100644 --- a/tests/unit/gui/test_property.py +++ b/tests/unit/gui/test_property.py @@ -8,37 +8,82 @@ class MyObject: class Observer: - called = None + call_args = None + called = False - def call(self, *args, **kwargs): - self.called = (args, kwargs) + def call(self): + self.call_args = tuple() + self.called = True - def __call__(self, *args, **kwargs): - self.called = (args, kwargs) + def call_with_args(self, instance, value): + """Match expected signature of 2 parameters""" + self.call_args = (instance, value) + self.called = True + + def __call__(self, *args): + self.call_args = args + self.called = True def test_bind_callback(): observer = Observer() my_obj = MyObject() - bind(my_obj, "name", observer) + bind(my_obj, "name", observer.call) - assert not observer.called + assert not observer.call_args # WHEN my_obj.name = "New Name" - assert observer.called == (tuple(), {}) + assert observer.call_args == tuple() -def test_unbind_callback(): +def test_bind_callback_with_args(): + """ + A bound callback can have 0 or 2 arguments. + 0 arguments are used for simple callbacks, like `log_change`. + 2 arguments are used for callbacks that need to know the instance and the new value. + """ + observer = Observer() + + my_obj = MyObject() + bind(my_obj, "name", observer.call_with_args) + + assert not observer.call_args + + # WHEN + my_obj.name = "New Name" + + assert observer.call_args == (my_obj, "New Name") + + # Remove reference of call_args to my_obj, otherwise it will keep the object alive + observer.call_args = None + + +def test_bind_callback_with_star_args(): observer = Observer() my_obj = MyObject() bind(my_obj, "name", observer) # WHEN - unbind(my_obj, "name", observer) + my_obj.name = "New Name" + + assert observer.call_args == (my_obj, "New Name") + + # Remove reference of call_args to my_obj, otherwise it will keep the object alive + observer.call_args = None + + +def test_unbind_callback(): + observer = Observer() + + my_obj = MyObject() + bind(my_obj, "name", observer.call) + + # WHEN + unbind(my_obj, "name", observer.call) my_obj.name = "New Name" assert not observer.called @@ -74,7 +119,7 @@ def test_does_not_trigger_if_value_unchanged(): observer = Observer() my_obj = MyObject() my_obj.name = "CONSTANT" - bind(my_obj, "name", observer) + bind(my_obj, "name", observer.call) assert not observer.called @@ -96,7 +141,7 @@ def test_gc_entries_are_collected(): del obj gc.collect() - # No left overs + # No leftovers assert len(MyObject.name.obs) == 0 diff --git a/tests/unit/sprite/test_sprite.py b/tests/unit/sprite/test_sprite.py index 0ebcb2001..1e88f6221 100644 --- a/tests/unit/sprite/test_sprite.py +++ b/tests/unit/sprite/test_sprite.py @@ -321,15 +321,19 @@ def test_visible(): assert sprite.alpha == 255 assert sprite.visible is True + # initialise alpha value + sprite.alpha = 100 + assert sprite.alpha == 100 + # Make invisible sprite.visible = False assert sprite.visible is False - assert sprite.alpha == 0 + assert sprite.alpha == 100 # Make visible again sprite.visible = True assert sprite.visible is True - assert sprite.alpha == 255 + assert sprite.alpha == 100 def test_sprite_scale_xy(window): diff --git a/tests/unit/sprite/test_sprite_texture_animation.py b/tests/unit/sprite/test_sprite_texture_animation.py index 5aca5afff..7ed6b5a5a 100644 --- a/tests/unit/sprite/test_sprite_texture_animation.py +++ b/tests/unit/sprite/test_sprite_texture_animation.py @@ -38,18 +38,6 @@ def test_animation(keyframes): """Test animation class""" anim = arcade.TextureAnimation(keyframes=keyframes) - # Add keyframe - anim.append_keyframe(arcade.TextureKeyframe(keyframes[0].texture, 1000)) - assert anim.num_frames == 9 - assert anim.duration_ms == 9000 - assert anim.duration_seconds == 9.0 - - # Remove keyframe - anim.remove_keyframe(8) - assert anim.num_frames == 8 - assert anim.duration_ms == 8000 - assert anim.duration_seconds == 8.0 - # Get keyframes at specific times (0.5s increments) for i in range(16): time = i / 2 From be260c17a3f5f0456b77bf4c45889bc187470605 Mon Sep 17 00:00:00 2001 From: DragonMoffon Date: Sat, 30 Mar 2024 02:52:19 +1300 Subject: [PATCH 89/94] Fix typing issues in ui manager --- arcade/gui/ui_manager.py | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/arcade/gui/ui_manager.py b/arcade/gui/ui_manager.py index 33e54092c..e8384f593 100644 --- a/arcade/gui/ui_manager.py +++ b/arcade/gui/ui_manager.py @@ -11,7 +11,7 @@ from __future__ import annotations from collections import defaultdict -from typing import Dict, Iterable, List, Optional, Type, TypeVar, Union +from typing import Dict, Iterable, List, Optional, Type, TypeVar, Union, Tuple from pyglet.event import EVENT_HANDLED, EVENT_UNHANDLED, EventDispatcher from typing_extensions import TypeGuard @@ -329,7 +329,7 @@ def draw(self) -> None: for layer in layers: self._get_surface(layer).draw() - def adjust_mouse_coordinates(self, x, y): + def adjust_mouse_coordinates(self, x: float, y: float) -> Tuple[float, float]: """ This method is used, to translate mouse coordinates to coordinates respecting the viewport and projection of cameras. @@ -337,7 +337,8 @@ def adjust_mouse_coordinates(self, x, y): It uses the internal camera's map_coordinate methods, and should work with all transformations possible with the basic orthographic camera. """ - return self.window.current_camera.map_screen_to_world_coordinate((x, y))[:2] + x_, y_, *c = self.window.current_camera.map_screen_to_world_coordinate((x, y)) + return x_, y_ def on_event(self, event) -> Union[bool, None]: layers = sorted(self.children.keys(), reverse=True) @@ -352,27 +353,27 @@ def dispatch_ui_event(self, event): return self.dispatch_event("on_event", event) def on_mouse_motion(self, x: int, y: int, dx: int, dy: int): - x, y = self.adjust_mouse_coordinates(x, y) - return self.dispatch_ui_event(UIMouseMovementEvent(self, x, y, dx, dy)) # type: ignore + x_, y_ = self.adjust_mouse_coordinates(x, y) + return self.dispatch_ui_event(UIMouseMovementEvent(self, int(x_), int(y), dx, dy)) def on_mouse_press(self, x: int, y: int, button: int, modifiers: int): - x, y = self.adjust_mouse_coordinates(x, y) - return self.dispatch_ui_event(UIMousePressEvent(self, x, y, button, modifiers)) # type: ignore + x_, y_ = self.adjust_mouse_coordinates(x, y) + return self.dispatch_ui_event(UIMousePressEvent(self, int(x_), int(y_), button, modifiers)) def on_mouse_drag( self, x: int, y: int, dx: int, dy: int, buttons: int, modifiers: int ): - x, y = self.adjust_mouse_coordinates(x, y) - return self.dispatch_ui_event(UIMouseDragEvent(self, x, y, dx, dy, buttons, modifiers)) # type: ignore + x_, y_ = self.adjust_mouse_coordinates(x, y) + return self.dispatch_ui_event(UIMouseDragEvent(self, int(x_), int(y_), dx, dy, buttons, modifiers)) def on_mouse_release(self, x: int, y: int, button: int, modifiers: int): - x, y = self.adjust_mouse_coordinates(x, y) - return self.dispatch_ui_event(UIMouseReleaseEvent(self, x, y, button, modifiers)) # type: ignore + x_, y_ = self.adjust_mouse_coordinates(x, y) + return self.dispatch_ui_event(UIMouseReleaseEvent(self, int(x_), int(y_), button, modifiers)) def on_mouse_scroll(self, x, y, scroll_x, scroll_y): - x, y = self.adjust_mouse_coordinates(x, y) + x_, y_ = self.adjust_mouse_coordinates(x, y) return self.dispatch_ui_event( - UIMouseScrollEvent(self, x, y, scroll_x, scroll_y) + UIMouseScrollEvent(self, int(x_), int(y_), scroll_x, scroll_y) ) def on_key_press(self, symbol: int, modifiers: int): From 3de0a2708baa9aea870bc32f99d9725b494f7b63 Mon Sep 17 00:00:00 2001 From: DragonMoffon Date: Sat, 30 Mar 2024 03:02:37 +1300 Subject: [PATCH 90/94] Fixed WindowProxy to use modern camera sensibilities --- tests/conftest.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 46b581477..6b8a7bfbe 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -113,7 +113,7 @@ def __init__(self, width=800, height=600, caption="Test Window", *args, **kwargs self.window.set_caption(caption) if width and height: self.window.set_size(width, height) - self.window.set_viewport(0, width, 0, height) + self.window.default_camera.use() self._update_rate = 60 @@ -194,11 +194,14 @@ def center_window(self): def set_vsync(self, vsync): self.window.set_vsync(vsync) - def get_viewport(self): - return self.window.get_viewport() - - def set_viewport(self, left, right, bottom, top): - self.window.set_viewport(left, right, bottom, top) + @property + def default_camera(self): + """ + Provides a reference to the default arcade camera. + Automatically sets projection and view to the size + of the screen. Good for resetting the screen. + """ + return self.window.default_camera def use(self): self.window.use() From 9f026ee156c8c73fdcdbcf1f8ec285e87170b652 Mon Sep 17 00:00:00 2001 From: DragonMoffon Date: Sun, 31 Mar 2024 02:35:58 +1300 Subject: [PATCH 91/94] Give the data PoDs __str__ and __repr__ methods --- arcade/camera/data_types.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/arcade/camera/data_types.py b/arcade/camera/data_types.py index 0891b9934..8e09cb791 100644 --- a/arcade/camera/data_types.py +++ b/arcade/camera/data_types.py @@ -50,6 +50,12 @@ def __init__(self, # Zoom self.zoom: float = zoom + def __str__(self): + return f"CameraData<{self.position=}, {self.up=}, {self.forward=}, {self.zoom=}>" + + def __repr__(self): + return self.__str__() + def duplicate_camera_data(origin: CameraData): return CameraData(origin.position, origin.up, origin.forward, float(origin.zoom)) @@ -115,6 +121,12 @@ def __init__( # Viewport for setting which pixels to draw to self.viewport: Tuple[int, int, int, int] = viewport + def __str__(self): + return f"OrthographicProjection" + + def __repr__(self): + return self.__str__() + class PerspectiveProjectionData: """Describes a perspective projection. @@ -145,6 +157,12 @@ def __init__(self, # Viewport for setting which pixels to draw to self.viewport: Tuple[int, int, int, int] = viewport + def __str__(self): + return f"PerspectiveProjection<{self.aspect=}, {self.fov=}, {self.near=}, {self.far=}, {self.viewport=}>" + + def __repr__(self): + return self.__str__() + class Projection(Protocol): viewport: Tuple[int, int, int, int] From 4e773ee41e86bca7d894bf5ed74f2fd9fd939587 Mon Sep 17 00:00:00 2001 From: DragonMoffon Date: Sun, 31 Mar 2024 04:23:33 +1300 Subject: [PATCH 92/94] Linting fix for __repr__ methods --- arcade/camera/data_types.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/arcade/camera/data_types.py b/arcade/camera/data_types.py index 8e09cb791..442e324cb 100644 --- a/arcade/camera/data_types.py +++ b/arcade/camera/data_types.py @@ -122,7 +122,11 @@ def __init__( self.viewport: Tuple[int, int, int, int] = viewport def __str__(self): - return f"OrthographicProjection" + return (f"OrthographicProjection<" + f"LRBT={(self.left, self.right, self.bottom, self.top)}, " + f"{self.near=}, " + f"{self.far=}, " + f"{self.viewport=}>") def __repr__(self): return self.__str__() From f05e46c92aa61cbe8900ab2788459c6215a4857d Mon Sep 17 00:00:00 2001 From: DragonMoffon Date: Sun, 31 Mar 2024 04:24:49 +1300 Subject: [PATCH 93/94] Updated unit tests to parameterize window size --- tests/unit/camera/test_orthographic_camera.py | 78 +++++++++++++++---- 1 file changed, 64 insertions(+), 14 deletions(-) diff --git a/tests/unit/camera/test_orthographic_camera.py b/tests/unit/camera/test_orthographic_camera.py index 843e39ed4..3ccf8e968 100644 --- a/tests/unit/camera/test_orthographic_camera.py +++ b/tests/unit/camera/test_orthographic_camera.py @@ -44,8 +44,10 @@ def test_orthographic_projector_activate(window: Window): window.default_camera.use() -def test_orthographic_projector_map_coordinates(window: Window): +@pytest.mark.parametrize("width, height", [(800, 600), (1280, 720), (500, 500)]) +def test_orthographic_projector_map_coordinates(window: Window, width, height): # Given + window.set_size(width, height) ortho_camera = camera.OrthographicProjector() # When @@ -59,12 +61,16 @@ def test_orthographic_projector_map_coordinates(window: Window): assert ortho_camera.map_screen_to_world_coordinate(mouse_pos_c) == pytest.approx((230.0, 800.0, 0.0)) -def test_orthographic_projector_map_coordinates_move(window: Window): +@pytest.mark.parametrize("width, height", [(800, 600), (1280, 720), (500, 500)]) +def test_orthographic_projector_map_coordinates_move(window: Window, width, height): # Given + window.set_size(width, height) ortho_camera = camera.OrthographicProjector() default_view = ortho_camera.view - mouse_pos_a = (window.width//2, window.height//2) + half_width, half_height = window.width//2, window.height//2 + + mouse_pos_a = (half_width, half_height) mouse_pos_b = (100.0, 100.0) # When @@ -72,7 +78,11 @@ def test_orthographic_projector_map_coordinates_move(window: Window): # Then assert ortho_camera.map_screen_to_world_coordinate(mouse_pos_a) == pytest.approx((0.0, 0.0, 0.0)) - assert ortho_camera.map_screen_to_world_coordinate(mouse_pos_b) == pytest.approx((-300.0, -200.0, 0.0)) + assert ( + ortho_camera.map_screen_to_world_coordinate(mouse_pos_b) + == + pytest.approx((-half_width+100.0, -half_height+100, 0.0)) + ) # And @@ -81,15 +91,23 @@ def test_orthographic_projector_map_coordinates_move(window: Window): # Then assert ortho_camera.map_screen_to_world_coordinate(mouse_pos_a) == pytest.approx((100.0, 100.0, 0.0)) - assert ortho_camera.map_screen_to_world_coordinate(mouse_pos_b) == pytest.approx((-200.0, -100.0, 0.0)) + assert ( + ortho_camera.map_screen_to_world_coordinate(mouse_pos_b) + == + pytest.approx((-half_width+200.0, -half_height+200.0, 0.0)) + ) -def test_orthographic_projector_map_coordinates_rotate(window: Window): +@pytest.mark.parametrize("width, height", [(800, 600), (1280, 720), (500, 500)]) +def test_orthographic_projector_map_coordinates_rotate(window: Window, width, height): # Given + window.set_size(width, height) ortho_camera = camera.OrthographicProjector() default_view = ortho_camera.view - mouse_pos_a = (window.width//2, window.height//2) + half_width, half_height = window.width//2, window.height//2 + + mouse_pos_a = (half_width, half_height) mouse_pos_b = (100.0, 100.0) # When @@ -98,7 +116,11 @@ def test_orthographic_projector_map_coordinates_rotate(window: Window): # Then assert ortho_camera.map_screen_to_world_coordinate(mouse_pos_a) == pytest.approx((0.0, 0.0, 0.0)) - assert ortho_camera.map_screen_to_world_coordinate(mouse_pos_b) == pytest.approx((-200.0, 300.0, 0.0)) + assert ( + ortho_camera.map_screen_to_world_coordinate(mouse_pos_b) + == + pytest.approx((-half_height+100.0, half_width-100.0, 0.0)) + ) # And @@ -106,16 +128,28 @@ def test_orthographic_projector_map_coordinates_rotate(window: Window): default_view.up = (2.0**-0.5, 2.0**-0.5, 0.0) default_view.position = (100.0, 100.0, 0.0) + b_shift_x = -half_width + 100.0 + b_shift_y = -half_height + 100.0 + b_rotated_x = b_shift_x / (2.0**0.5) + b_shift_y / (2.0**0.5) + 100 + b_rotated_y = -b_shift_x / (2.0**0.5) + b_shift_y / (2.0**0.5) + 100 # Then assert ortho_camera.map_screen_to_world_coordinate(mouse_pos_a) == pytest.approx((100.0, 100.0, 0.0)) - assert ortho_camera.map_screen_to_world_coordinate(mouse_pos_b) == pytest.approx((-253.553390, 170.710678, 0.0)) + assert ( + ortho_camera.map_screen_to_world_coordinate(mouse_pos_b) + == + pytest.approx((b_rotated_x, b_rotated_y, 0.0)) + ) -def test_orthographic_projector_map_coordinates_zoom(window: Window): +@pytest.mark.parametrize("width, height", [(800, 600), (1280, 720), (500, 500)]) +def test_orthographic_projector_map_coordinates_zoom(window: Window, width, height): # Given + window.set_size(width, height) ortho_camera = camera.OrthographicProjector() default_view = ortho_camera.view + half_width, half_height = window.width//2, window.height//2 + mouse_pos_a = (window.width, window.height) mouse_pos_b = (100.0, 100.0) @@ -123,8 +157,16 @@ def test_orthographic_projector_map_coordinates_zoom(window: Window): default_view.zoom = 2.0 # Then - assert ortho_camera.map_screen_to_world_coordinate(mouse_pos_a) == pytest.approx((window.width*0.75, window.height*0.75, 0.0)) - assert ortho_camera.map_screen_to_world_coordinate(mouse_pos_b) == pytest.approx((250.0, 200.0, 0.0)) + assert ( + ortho_camera.map_screen_to_world_coordinate(mouse_pos_a) + == + pytest.approx((window.width*0.75, window.height*0.75, 0.0)) + ) + assert ( + ortho_camera.map_screen_to_world_coordinate(mouse_pos_b) + == + pytest.approx((half_width + (100 - half_width)*0.5, half_height + (100 - half_height)*0.5, 0.0)) + ) # And @@ -133,5 +175,13 @@ def test_orthographic_projector_map_coordinates_zoom(window: Window): default_view.zoom = 0.25 # Then - assert ortho_camera.map_screen_to_world_coordinate(mouse_pos_a) == pytest.approx((window.width*2.0, window.height*2.0, 0.0)) - assert ortho_camera.map_screen_to_world_coordinate(mouse_pos_b) == pytest.approx((-1200.0, -800.0, 0.0)) \ No newline at end of file + assert ( + ortho_camera.map_screen_to_world_coordinate(mouse_pos_a) + == + pytest.approx((window.width*2.0, window.height*2.0, 0.0)) + ) + assert ( + ortho_camera.map_screen_to_world_coordinate(mouse_pos_b) + == + pytest.approx(((100 - half_width)*4.0, (100 - half_height)*4.0, 0.0)) + ) \ No newline at end of file From 7af79b3fcecb953e1c78a43eb9b032f8b71250f3 Mon Sep 17 00:00:00 2001 From: DragonMoffon Date: Sun, 31 Mar 2024 11:43:11 +1300 Subject: [PATCH 94/94] Improving the state reset between unit tests --- tests/conftest.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/conftest.py b/tests/conftest.py index 6b8a7bfbe..5e17e4f4e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -36,6 +36,9 @@ def prepare_window(window: arcade.Window): raise RuntimeError("Please do not close the global test window :D") window.switch_to() + if window.get_size() < (800, 600): + window.set_size(800, 600) + ctx = window.ctx ctx._atlas = None # Clear the global atlas arcade.cleanup_texture_cache() # Clear the global texture cache @@ -51,6 +54,7 @@ def prepare_window(window: arcade.Window): window.set_vsync(False) window.flip() window.clear() + window.default_camera.use() ctx.gc_mode = "context_gc" ctx.gc() gc.collect()