Skip to content

Commit

Permalink
Fix Camera2D render target size bug & improve durability (#2050)
Browse files Browse the repository at this point in the history
* Fix Camera2D.__init__ ignoring render target size

* Use final render_target.size instead of window size

* Clean up . accesses for shorter code + tiny speed boost

* Fix Camera2D.from_raw_data ignoring target size

* Get render_target earlier

* Use render_target.size to define width and height

* Make code cleaner +_ a tiny bit faster with less . access

* Add tests for using render_target size

* Use durable keyword args in Camera2D.__init__

* Use more durable keyword arguments in Camera2D.from_raw_data

* Make Camera2D.from_raw_data a class method

* Make it a class method with a typing_extensions.Self return type

* Add a test for subclassing safety

* Fix type issue

* Correct Camera2D.viewport docstring per Discord discussion with Dragon

* Correct the Camera2D tests to check viewport property

* Move Camera2D tests to their own file

* Move orthographic projector tests to better named file

* Add missing newline at end of file
  • Loading branch information
pushfoo committed Apr 6, 2024
1 parent 637c401 commit e2ef4a9
Show file tree
Hide file tree
Showing 3 changed files with 92 additions and 32 deletions.
66 changes: 36 additions & 30 deletions arcade/camera/camera_2d.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
from math import degrees, radians, atan2, cos, sin
from contextlib import contextmanager

from typing_extensions import Self

from arcade.camera.orthographic import OrthographicProjector
from arcade.camera.data_types import CameraData, OrthographicProjectionData, Projector
from arcade.gl import Framebuffer
Expand Down Expand Up @@ -59,21 +61,21 @@ def __init__(self, *,
window: Optional["Window"] = None):
self._window: "Window" = window or get_window()
self.render_target: Framebuffer = render_target or self._window.ctx.screen

half_width = self._window.width / 2
half_height = self._window.height / 2
width, height = self.render_target.size
half_width = width / 2
half_height = 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
position=(half_width, half_height, 0.0),
up=(0.0, 1.0, 0.0),
forward=(0.0, 0.0, -1.0),
zoom=1.0
)
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
left=-half_width, right=half_width,
bottom=-half_height, top=half_height,
near=0.0, far=100.0,
viewport=(0, 0, width, height)
)

self._ortho_projector: OrthographicProjector = OrthographicProjector(
Expand All @@ -82,8 +84,9 @@ def __init__(self, *,
projection=self._projection
)

@staticmethod
@classmethod
def from_raw_data(
cls,
viewport: Optional[Tuple[int, int, int, int]] = None,
position: Optional[Tuple[float, float]] = None,
up: Tuple[float, float] = (0.0, 1.0),
Expand All @@ -94,7 +97,7 @@ def from_raw_data(
*,
render_target: Optional[Framebuffer] = None,
window: Optional["Window"] = None
):
) -> Self:
"""
Create a Camera2D without first defining CameraData or an OrthographicProjectionData object.
Expand All @@ -114,31 +117,32 @@ def from_raw_data(
Defaults to the currently active window.
"""
window = window or get_window()

half_width = window.width / 2
half_height = window.height / 2
render_target = render_target or window.ctx.screen
width, height = render_target.size
half_width = width / 2
half_height = height / 2

_pos = position or (half_width, half_height)
_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
_data = CameraData(
position=(_pos[0], _pos[1], 0.0),
up=(up[0], up[1], 0.0),
forward=(0.0, 0.0, -1.0),
zoom=zoom
)

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
left=left, right=right,
top=top, bottom=bottom,
near=near or 0.0, far=far or 100.0,
viewport=viewport or (0, 0, width, height)
)

return Camera2D(
return cls(
camera_data=_data,
projection_data=_projection,
window=window,
render_target=(render_target or window.ctx.screen)
render_target=render_target
)

@property
Expand Down Expand Up @@ -497,9 +501,11 @@ def projection_far(self, _far: float) -> None:

@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)
"""Get/set pixels of the ``render_target`` drawn to when active.
The pixel area is defined as integer pixel coordinates starting
from the bottom left of ``self.render_target``. They are ordered
as ``(left, bottom, width, height)``.
"""
return self._projection.viewport

Expand Down
55 changes: 55 additions & 0 deletions tests/unit/camera/test_camera2d.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import pytest as pytest

from arcade import Window
from arcade.camera import Camera2D


def test_camera2d_from_raw_data_inheritance_safety(window: Window):
class MyCamera2D(Camera2D):
...

subclassed = MyCamera2D.from_raw_data(zoom=10.0)
assert isinstance(subclassed, MyCamera2D)


RENDER_TARGET_SIZES = [
(800, 600), # Normal window size
(1280, 720), # Bigger
(16, 16) # Tiny
]


@pytest.mark.parametrize("width, height", RENDER_TARGET_SIZES)
def test_camera2d_init_uses_render_target_size(window: Window, width, height):

size = (width, height)
texture = window.ctx.texture(size, components=4)
framebuffer = window.ctx.framebuffer(color_attachments=[texture])

ortho_camera = Camera2D(render_target=framebuffer)
assert ortho_camera.viewport_width == width
assert ortho_camera.viewport_height == height

assert ortho_camera.viewport == (0, 0, width, height)
assert ortho_camera.viewport_left == 0
assert ortho_camera.viewport_right == width
assert ortho_camera.viewport_bottom == 0
assert ortho_camera.viewport_top == height


@pytest.mark.parametrize("width, height", RENDER_TARGET_SIZES)
def test_camera2d_from_raw_data_uses_render_target_size(window: Window, width, height):

size = (width, height)
texture = window.ctx.texture(size, components=4)
framebuffer = window.ctx.framebuffer(color_attachments=[texture])

ortho_camera = Camera2D.from_raw_data(render_target=framebuffer)
assert ortho_camera.viewport_width == width
assert ortho_camera.viewport_height == height

assert ortho_camera.viewport == (0, 0, width, height)
assert ortho_camera.viewport_left == 0
assert ortho_camera.viewport_right == width
assert ortho_camera.viewport_bottom == 0
assert ortho_camera.viewport_top == height
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@

def test_orthographic_projector_use(window: Window):
# Given
from pyglet.math import Mat4
ortho_camera = camera.OrthographicProjector()

view_matrix = ortho_camera._generate_view_matrix()
Expand Down Expand Up @@ -184,4 +183,4 @@ def test_orthographic_projector_map_coordinates_zoom(window: Window, width, heig
ortho_camera.map_screen_to_world_coordinate(mouse_pos_b)
==
pytest.approx(((100 - half_width)*4.0, (100 - half_height)*4.0, 0.0))
)
)

0 comments on commit e2ef4a9

Please sign in to comment.