Skip to content

Commit e2ef4a9

Browse files
authored
Fix Camera2D render target size bug & improve durability (#2050)
* 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
1 parent 637c401 commit e2ef4a9

File tree

3 files changed

+92
-32
lines changed

3 files changed

+92
-32
lines changed

arcade/camera/camera_2d.py

Lines changed: 36 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
from math import degrees, radians, atan2, cos, sin
33
from contextlib import contextmanager
44

5+
from typing_extensions import Self
6+
57
from arcade.camera.orthographic import OrthographicProjector
68
from arcade.camera.data_types import CameraData, OrthographicProjectionData, Projector
79
from arcade.gl import Framebuffer
@@ -59,21 +61,21 @@ def __init__(self, *,
5961
window: Optional["Window"] = None):
6062
self._window: "Window" = window or get_window()
6163
self.render_target: Framebuffer = render_target or self._window.ctx.screen
62-
63-
half_width = self._window.width / 2
64-
half_height = self._window.height / 2
64+
width, height = self.render_target.size
65+
half_width = width / 2
66+
half_height = height / 2
6567

6668
self._data = camera_data or CameraData(
67-
(half_width, half_height, 0.0), # position
68-
(0.0, 1.0, 0.0), # up vector
69-
(0.0, 0.0, -1.0), # forward vector
70-
1.0 # zoom
69+
position=(half_width, half_height, 0.0),
70+
up=(0.0, 1.0, 0.0),
71+
forward=(0.0, 0.0, -1.0),
72+
zoom=1.0
7173
)
7274
self._projection: OrthographicProjectionData = projection_data or OrthographicProjectionData(
73-
-half_width, half_width, # Left and Right.
74-
-half_height, half_height, # Bottom and Top.
75-
0.0, 100.0, # Near and Far.
76-
(0, 0, self._window.width, self._window.height) # Viewport
75+
left=-half_width, right=half_width,
76+
bottom=-half_height, top=half_height,
77+
near=0.0, far=100.0,
78+
viewport=(0, 0, width, height)
7779
)
7880

7981
self._ortho_projector: OrthographicProjector = OrthographicProjector(
@@ -82,8 +84,9 @@ def __init__(self, *,
8284
projection=self._projection
8385
)
8486

85-
@staticmethod
87+
@classmethod
8688
def from_raw_data(
89+
cls,
8790
viewport: Optional[Tuple[int, int, int, int]] = None,
8891
position: Optional[Tuple[float, float]] = None,
8992
up: Tuple[float, float] = (0.0, 1.0),
@@ -94,7 +97,7 @@ def from_raw_data(
9497
*,
9598
render_target: Optional[Framebuffer] = None,
9699
window: Optional["Window"] = None
97-
):
100+
) -> Self:
98101
"""
99102
Create a Camera2D without first defining CameraData or an OrthographicProjectionData object.
100103
@@ -114,31 +117,32 @@ def from_raw_data(
114117
Defaults to the currently active window.
115118
"""
116119
window = window or get_window()
117-
118-
half_width = window.width / 2
119-
half_height = window.height / 2
120+
render_target = render_target or window.ctx.screen
121+
width, height = render_target.size
122+
half_width = width / 2
123+
half_height = height / 2
120124

121125
_pos = position or (half_width, half_height)
122-
_data: CameraData = CameraData(
123-
(_pos[0], _pos[1], 0.0), # position
124-
(up[0], up[1], 0.0), # up vector
125-
(0.0, 0.0, -1.0), # forward vector
126-
zoom # zoom
126+
_data = CameraData(
127+
position=(_pos[0], _pos[1], 0.0),
128+
up=(up[0], up[1], 0.0),
129+
forward=(0.0, 0.0, -1.0),
130+
zoom=zoom
127131
)
128132

129133
left, right, bottom, top = projection or (-half_width, half_width, -half_height, half_height)
130134
_projection: OrthographicProjectionData = OrthographicProjectionData(
131-
left, right, # Left and Right.
132-
top, bottom, # Bottom and Top.
133-
near or 0.0, far or 100.0, # Near and Far.
134-
viewport or (0, 0, window.width, window.height) # Viewport
135+
left=left, right=right,
136+
top=top, bottom=bottom,
137+
near=near or 0.0, far=far or 100.0,
138+
viewport=viewport or (0, 0, width, height)
135139
)
136140

137-
return Camera2D(
141+
return cls(
138142
camera_data=_data,
139143
projection_data=_projection,
140144
window=window,
141-
render_target=(render_target or window.ctx.screen)
145+
render_target=render_target
142146
)
143147

144148
@property
@@ -497,9 +501,11 @@ def projection_far(self, _far: float) -> None:
497501

498502
@property
499503
def viewport(self) -> Tuple[int, int, int, int]:
500-
"""
501-
The pixel area that will be drawn to while the camera is active.
502-
(left, right, bottom, top)
504+
"""Get/set pixels of the ``render_target`` drawn to when active.
505+
506+
The pixel area is defined as integer pixel coordinates starting
507+
from the bottom left of ``self.render_target``. They are ordered
508+
as ``(left, bottom, width, height)``.
503509
"""
504510
return self._projection.viewport
505511

tests/unit/camera/test_camera2d.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import pytest as pytest
2+
3+
from arcade import Window
4+
from arcade.camera import Camera2D
5+
6+
7+
def test_camera2d_from_raw_data_inheritance_safety(window: Window):
8+
class MyCamera2D(Camera2D):
9+
...
10+
11+
subclassed = MyCamera2D.from_raw_data(zoom=10.0)
12+
assert isinstance(subclassed, MyCamera2D)
13+
14+
15+
RENDER_TARGET_SIZES = [
16+
(800, 600), # Normal window size
17+
(1280, 720), # Bigger
18+
(16, 16) # Tiny
19+
]
20+
21+
22+
@pytest.mark.parametrize("width, height", RENDER_TARGET_SIZES)
23+
def test_camera2d_init_uses_render_target_size(window: Window, width, height):
24+
25+
size = (width, height)
26+
texture = window.ctx.texture(size, components=4)
27+
framebuffer = window.ctx.framebuffer(color_attachments=[texture])
28+
29+
ortho_camera = Camera2D(render_target=framebuffer)
30+
assert ortho_camera.viewport_width == width
31+
assert ortho_camera.viewport_height == height
32+
33+
assert ortho_camera.viewport == (0, 0, width, height)
34+
assert ortho_camera.viewport_left == 0
35+
assert ortho_camera.viewport_right == width
36+
assert ortho_camera.viewport_bottom == 0
37+
assert ortho_camera.viewport_top == height
38+
39+
40+
@pytest.mark.parametrize("width, height", RENDER_TARGET_SIZES)
41+
def test_camera2d_from_raw_data_uses_render_target_size(window: Window, width, height):
42+
43+
size = (width, height)
44+
texture = window.ctx.texture(size, components=4)
45+
framebuffer = window.ctx.framebuffer(color_attachments=[texture])
46+
47+
ortho_camera = Camera2D.from_raw_data(render_target=framebuffer)
48+
assert ortho_camera.viewport_width == width
49+
assert ortho_camera.viewport_height == height
50+
51+
assert ortho_camera.viewport == (0, 0, width, height)
52+
assert ortho_camera.viewport_left == 0
53+
assert ortho_camera.viewport_right == width
54+
assert ortho_camera.viewport_bottom == 0
55+
assert ortho_camera.viewport_top == height

tests/unit/camera/test_orthographic_camera.py renamed to tests/unit/camera/test_orthographic_projector.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55

66
def test_orthographic_projector_use(window: Window):
77
# Given
8-
from pyglet.math import Mat4
98
ortho_camera = camera.OrthographicProjector()
109

1110
view_matrix = ortho_camera._generate_view_matrix()
@@ -184,4 +183,4 @@ def test_orthographic_projector_map_coordinates_zoom(window: Window, width, heig
184183
ortho_camera.map_screen_to_world_coordinate(mouse_pos_b)
185184
==
186185
pytest.approx(((100 - half_width)*4.0, (100 - half_height)*4.0, 0.0))
187-
)
186+
)

0 commit comments

Comments
 (0)