|
| 1 | +from typing import Optional, Tuple, Iterator, TYPE_CHECKING |
| 2 | +from contextlib import contextmanager |
| 3 | + |
| 4 | +from math import tan, radians |
| 5 | +from pyglet.math import Mat4, Vec3, Vec4 |
| 6 | + |
| 7 | +from arcade.camera.data_types import Projector, CameraData, PerspectiveProjectionData |
| 8 | + |
| 9 | +from arcade.window_commands import get_window |
| 10 | +if TYPE_CHECKING: |
| 11 | + from arcade import Window |
| 12 | + |
| 13 | + |
| 14 | +__all__ = ("PerspectiveProjector",) |
| 15 | + |
| 16 | + |
| 17 | +class PerspectiveProjector: |
| 18 | + """ |
| 19 | + The simplest from of a perspective camera. |
| 20 | + Using ViewData and PerspectiveProjectionData PoDs (Pack of Data) |
| 21 | + it generates the correct projection and view matrices. It also |
| 22 | + provides methods and a context manager for using the matrices in |
| 23 | + glsl shaders. |
| 24 | +
|
| 25 | + This class provides no methods for manipulating the PoDs. |
| 26 | +
|
| 27 | + The current implementation will recreate the view and |
| 28 | + projection matrices every time the camera is used. |
| 29 | + If used every frame or multiple times per frame this may |
| 30 | + be inefficient. If you suspect this is causing slowdowns |
| 31 | + profile before optimizing with a dirty value check. |
| 32 | +
|
| 33 | + Initialize a Projector which produces a perspective projection matrix using |
| 34 | + a CameraData and PerspectiveProjectionData PoDs. |
| 35 | +
|
| 36 | + :param window: The window to bind the camera to. Defaults to the currently active camera. |
| 37 | + :param view: The CameraData PoD. contains the viewport, position, up, forward, and zoom. |
| 38 | + :param projection: The PerspectiveProjectionData PoD. |
| 39 | + contains the field of view, aspect ratio, and then near and far planes. |
| 40 | + """ |
| 41 | + |
| 42 | + def __init__(self, *, |
| 43 | + window: Optional["Window"] = None, |
| 44 | + view: Optional[CameraData] = None, |
| 45 | + projection: Optional[PerspectiveProjectionData] = None): |
| 46 | + self._window: "Window" = window or get_window() |
| 47 | + |
| 48 | + self._view = view or CameraData( # Viewport |
| 49 | + (self._window.width / 2, self._window.height / 2, 0), # Position |
| 50 | + (0.0, 1.0, 0.0), # Up |
| 51 | + (0.0, 0.0, -1.0), # Forward |
| 52 | + 1.0 # Zoom |
| 53 | + ) |
| 54 | + |
| 55 | + self._projection = projection or PerspectiveProjectionData( |
| 56 | + self._window.width / self._window.height, # Aspect |
| 57 | + 60, # Field of View, |
| 58 | + 0.01, 100.0, # near, # far |
| 59 | + (0, 0, self._window.width, self._window.height) # Viewport |
| 60 | + ) |
| 61 | + |
| 62 | + @property |
| 63 | + def view(self) -> CameraData: |
| 64 | + """ |
| 65 | + The CameraData. Is a read only property. |
| 66 | + """ |
| 67 | + return self._view |
| 68 | + |
| 69 | + @property |
| 70 | + def projection(self) -> PerspectiveProjectionData: |
| 71 | + """ |
| 72 | + The OrthographicProjectionData. Is a read only property. |
| 73 | + """ |
| 74 | + return self._projection |
| 75 | + |
| 76 | + def _generate_projection_matrix(self) -> Mat4: |
| 77 | + """ |
| 78 | + Using the OrthographicProjectionData a projection matrix is generated where the size of the |
| 79 | + objects is not affected by depth. |
| 80 | +
|
| 81 | + Generally keep the scale value to integers or negative powers of integers (2^-1, 3^-1, 2^-2, etc.) to keep |
| 82 | + the pixels uniform in size. Avoid a zoom of 0.0. |
| 83 | + """ |
| 84 | + _proj = self._projection |
| 85 | + |
| 86 | + return Mat4.perspective_projection(_proj.aspect, _proj.near, _proj.far, _proj.fov / self._view.zoom) |
| 87 | + |
| 88 | + def _generate_view_matrix(self) -> Mat4: |
| 89 | + """ |
| 90 | + Using the ViewData it generates a view matrix from the pyglet Mat4 look at function |
| 91 | + """ |
| 92 | + # Even if forward and up are normalised floating point error means every vector must be normalised. |
| 93 | + fo = Vec3(*self._view.forward).normalize() # Forward Vector |
| 94 | + up = Vec3(*self._view.up) # Initial Up Vector (Not necessarily perpendicular to forward vector) |
| 95 | + ri = fo.cross(up).normalize() # Right Vector |
| 96 | + up = ri.cross(fo).normalize() # Up Vector |
| 97 | + po = Vec3(*self._view.position) |
| 98 | + return Mat4(( |
| 99 | + ri.x, up.x, -fo.x, 0, |
| 100 | + ri.y, up.y, -fo.y, 0, |
| 101 | + ri.z, up.z, -fo.z, 0, |
| 102 | + -ri.dot(po), -up.dot(po), fo.dot(po), 1 |
| 103 | + )) |
| 104 | + |
| 105 | + def use(self) -> None: |
| 106 | + """ |
| 107 | + Sets the active camera to this object. |
| 108 | + Then generates the view and projection matrices. |
| 109 | + Finally, the gl context viewport is set, as well as the projection and view matrices. |
| 110 | + """ |
| 111 | + |
| 112 | + self._window.current_camera = self |
| 113 | + |
| 114 | + _projection = self._generate_projection_matrix() |
| 115 | + _view = self._generate_view_matrix() |
| 116 | + |
| 117 | + self._window.ctx.viewport = self._projection.viewport |
| 118 | + self._window.projection = _projection |
| 119 | + self._window.view = _view |
| 120 | + |
| 121 | + @contextmanager |
| 122 | + def activate(self) -> Iterator[Projector]: |
| 123 | + """ |
| 124 | + A context manager version of OrthographicProjector.use() which allows for the use of |
| 125 | + `with` blocks. For example, `with camera.activate() as cam: ...`. |
| 126 | + """ |
| 127 | + previous_projector = self._window.current_camera |
| 128 | + try: |
| 129 | + self.use() |
| 130 | + yield self |
| 131 | + finally: |
| 132 | + previous_projector.use() |
| 133 | + |
| 134 | + def map_screen_to_world_coordinate( |
| 135 | + self, |
| 136 | + screen_coordinate: Tuple[float, float], |
| 137 | + depth: Optional[float] = None |
| 138 | + ) -> Tuple[float, float, float]: |
| 139 | + """ |
| 140 | + Take in a pixel coordinate from within |
| 141 | + the range of the window size and returns |
| 142 | + the world space coordinates. |
| 143 | +
|
| 144 | + Essentially reverses the effects of the projector. |
| 145 | +
|
| 146 | + Args: |
| 147 | + screen_coordinate: A 2D position in pixels from the bottom left of the screen. |
| 148 | + This should ALWAYS be in the range of 0.0 - screen size. |
| 149 | + depth: The depth of the query |
| 150 | + Returns: |
| 151 | + A 3D vector in world space. |
| 152 | + """ |
| 153 | + depth = depth or (0.5 * self._projection.viewport[3] / tan( |
| 154 | + radians(0.5 * self._projection.fov / self._view.zoom))) |
| 155 | + |
| 156 | + screen_x = 2.0 * (screen_coordinate[0] - self._projection.viewport[0]) / self._projection.viewport[2] - 1 |
| 157 | + screen_y = 2.0 * (screen_coordinate[1] - self._projection.viewport[1]) / self._projection.viewport[3] - 1 |
| 158 | + |
| 159 | + screen_x *= depth |
| 160 | + screen_y *= depth |
| 161 | + |
| 162 | + projected_position = Vec4(screen_x, screen_y, 1.0, 1.0) |
| 163 | + |
| 164 | + _projection = ~self._generate_projection_matrix() |
| 165 | + view_position = _projection @ projected_position |
| 166 | + _view = ~self._generate_view_matrix() |
| 167 | + world_position = _view @ Vec4(view_position.x, view_position.y, depth, 1.0) |
| 168 | + |
| 169 | + return world_position.x, world_position.y, world_position.z |
0 commit comments