Skip to content

Commit 3fa9fd3

Browse files
committed
Updating the default projector to behave better with changing framebuffer
1 parent f8a5490 commit 3fa9fd3

File tree

3 files changed

+106
-75
lines changed

3 files changed

+106
-75
lines changed

arcade/camera/default.py

Lines changed: 83 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -9,62 +9,115 @@
99

1010
from arcade.types import LBWH, Point, Rect
1111
from arcade.window_commands import get_window
12+
from arcade.camera.data_types import DEFAULT_NEAR_ORTHO, DEFAULT_FAR
1213

1314
if TYPE_CHECKING:
1415
from arcade.context import ArcadeContext
1516

16-
__all__ = ["ViewportProjector", "DefaultProjector"]
17+
__all__ = ()
1718

1819

19-
class ViewportProjector:
20+
class DefaultProjector:
2021
"""
21-
A simple Projector which does not rely on any camera PoDs.
22+
An extremely limited projector which lacks any kind of control. This is only
23+
here to act as the default camera used internally by Arcade. There should be
24+
no instance where a developer would want to use this class.
25+
26+
The default viewport tries it's best to allow
27+
simple usecases with no need to use a camera.
2228
23-
Does not have a way of moving, rotating, or zooming the camera.
24-
perfect for something like UI or for mapping to an offscreen framebuffer.
29+
It does this by defaulting to the size of the active
30+
framebuffer. If the user sets the framebuffer's viewport
31+
without a camera then the default camera will match it
32+
until the framebuffer is changed again.
2533
2634
Args:
27-
viewport: The viewport to project to.
28-
context: The window context to bind the camera to. Defaults to the currently active window.
35+
context: The window context to bind the camera to. Defaults to the currently active context.
2936
"""
3037

31-
def __init__(
32-
self,
33-
viewport: Rect | None = None,
34-
*,
35-
context: ArcadeContext | None = None,
36-
):
38+
def __init__(self, *, context: ArcadeContext | None = None):
3739
self._ctx: ArcadeContext = context or get_window().ctx
38-
self._viewport: Rect = viewport or LBWH(*self._ctx.viewport)
39-
self._projection_matrix: Mat4 = Mat4.orthogonal_projection(
40-
0.0, self._viewport.width, 0.0, self._viewport.height, -100, 100
41-
)
40+
self._viewport: Rect | None = None
41+
self._scissor: Rect | None = None
42+
self._matrix: Mat4 | None = None
4243

43-
@property
44-
def viewport(self) -> Rect:
44+
def update_viewport(self):
4545
"""
46-
The viewport use to derive projection and view matrix.
46+
Called when the ArcadeContext's viewport or active
47+
framebuffer has been set. It only actually updates
48+
the viewport if no other camera is active. Also
49+
setting the viewport to match the size of the active
50+
framebuffer sets the viewport to None.
4751
"""
52+
if self._ctx.current_camera != self:
53+
return
54+
if self._ctx.viewport[2] != self.width or self._ctx.viewport[3] != self.height:
55+
self._viewport = LBWH(*self._ctx.viewport)
56+
self._viewport = None
57+
58+
self.use()
59+
60+
@property
61+
def viewport(self) -> Rect | None:
4862
return self._viewport
4963

5064
@viewport.setter
51-
def viewport(self, viewport: Rect) -> None:
65+
def viewport(self, viewport: Rect | None) -> None:
66+
if viewport == self._viewport:
67+
return
5268
self._viewport = viewport
53-
self._projection_matrix = Mat4.orthogonal_projection(
54-
0, viewport.width, 0, viewport.height, -100, 100
69+
self._matrix = Mat4.orthogonal_projection(
70+
0, self.width, 0, self.height, DEFAULT_NEAR_ORTHO, DEFAULT_FAR
5571
)
5672

73+
@viewport.deleter
74+
def viewport(self):
75+
self.viewport = None
76+
77+
@property
78+
def scissor(self) -> Rect | None:
79+
return self._scissor
80+
81+
@scissor.setter
82+
def scissor(self, scissor: Rect | None) -> None:
83+
self._scissor = scissor
84+
85+
@scissor.deleter
86+
def scissor(self) -> None:
87+
self._scissor = None
88+
89+
@property
90+
def width(self) -> int:
91+
if self._viewport is not None:
92+
return int(self._viewport.width)
93+
return self._ctx.active_framebuffer.width
94+
95+
@property
96+
def height(self) -> int:
97+
if self._viewport is not None:
98+
return int(self._viewport.height)
99+
return self._ctx.active_framebuffer.height
100+
57101
def use(self) -> None:
58102
"""
59-
Set the window's projection and view matrix.
60-
Also sets the projector as the windows current camera.
103+
Set the window's Projection and View matrices.
61104
"""
62-
self._ctx.current_camera = self
63105

64-
self._ctx.viewport = self.viewport.lbwh_int # get the integer 4-tuple LBWH
106+
viewport = (0, 0, self.width, self.height)
107+
# If the viewport is correct and the default camera is in use,
108+
# then don't waste time resetting the view and projection matrices
109+
if self._ctx.viewport == viewport and self._ctx.current_camera == self:
110+
return
111+
112+
self._ctx.current_camera = self
113+
self._ctx.viewport = viewport
65114

66115
self._ctx.view_matrix = Mat4()
67-
self._ctx.projection_matrix = self._projection_matrix
116+
if self._matrix is None:
117+
self._matrix = Mat4.orthogonal_projection(
118+
0, 0, viewport[2], viewport[3], DEFAULT_NEAR_ORTHO, DEFAULT_FAR
119+
)
120+
self._ctx.projection_matrix = self._matrix
68121

69122
@contextmanager
70123
def activate(self) -> Generator[Self, None, None]:
@@ -74,10 +127,12 @@ def activate(self) -> Generator[Self, None, None]:
74127
usable with the 'with' block. e.g. 'with ViewportProjector.activate() as cam: ...'
75128
"""
76129
previous = self._ctx.current_camera
130+
previous_viewport = self._ctx.viewport
77131
try:
78132
self.use()
79133
yield self
80134
finally:
135+
self._ctx.viewport = previous_viewport
81136
previous.use()
82137

83138
def project(self, world_coordinate: Point) -> Vec2:
@@ -97,46 +152,3 @@ def unproject(self, screen_coordinate: Point) -> Vec3:
97152
z = 0.0 if not _z else _z[0]
98153

99154
return Vec3(x, y, z)
100-
101-
102-
# As this class is only supposed to be used internally
103-
# I wanted to place an _ in front, but the linting complains
104-
# about it being a protected class.
105-
class DefaultProjector(ViewportProjector):
106-
"""
107-
An extremely limited projector which lacks any kind of control. This is only
108-
here to act as the default camera used internally by Arcade. There should be
109-
no instance where a developer would want to use this class.
110-
111-
Args:
112-
context: The window context to bind the camera to. Defaults to the currently active window.
113-
"""
114-
115-
def __init__(self, *, context: ArcadeContext | None = None):
116-
super().__init__(context=context)
117-
118-
def use(self) -> None:
119-
"""
120-
Set the window's Projection and View matrices.
121-
122-
cache's the window viewport to determine the projection matrix.
123-
"""
124-
125-
viewport = self.viewport.lbwh_int
126-
# If the viewport is correct and the default camera is in use,
127-
# then don't waste time resetting the view and projection matrices
128-
if self._ctx.viewport == viewport and self._ctx.current_camera == self:
129-
return
130-
131-
# If the viewport has changed while the default camera is active then the
132-
# default needs to update itself.
133-
# If it was another camera's viewport being used the default camera should not update.
134-
if self._ctx.viewport != viewport and self._ctx.current_camera == self:
135-
self.viewport = LBWH(*self._ctx.viewport)
136-
else:
137-
self._ctx.viewport = viewport
138-
139-
self._ctx.current_camera = self
140-
141-
self._ctx.view_matrix = Mat4()
142-
self._ctx.projection_matrix = self._projection_matrix

arcade/context.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -372,6 +372,15 @@ def default_atlas(self) -> TextureAtlasBase:
372372

373373
return self._atlas
374374

375+
@property
376+
def active_framebuffer(self):
377+
return self._active_framebuffer
378+
379+
@active_framebuffer.setter
380+
def active_framebuffer(self, framebuffer: Framebuffer):
381+
self._active_framebuffer = framebuffer
382+
self._default_camera.update_viewport()
383+
375384
@property
376385
def viewport(self) -> tuple[int, int, int, int]:
377386
"""
@@ -393,8 +402,7 @@ def viewport(self) -> tuple[int, int, int, int]:
393402
@viewport.setter
394403
def viewport(self, value: tuple[int, int, int, int]):
395404
self.active_framebuffer.viewport = value
396-
if self._default_camera == self.current_camera:
397-
self._default_camera.use()
405+
self._default_camera.update_viewport()
398406

399407
@property
400408
def projection_matrix(self) -> Mat4:

arcade/gl/context.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -221,7 +221,7 @@ def __init__(
221221
# Tracking active program
222222
self.active_program: Program | ComputeShader | None = None
223223
# Tracking active framebuffer. On context creation the window is the default render target
224-
self.active_framebuffer: Framebuffer = self._screen
224+
self._active_framebuffer: Framebuffer = self._screen
225225
self._stats: ContextStats = ContextStats(warn_threshold=1000)
226226

227227
self._primitive_restart_index = -1
@@ -327,7 +327,18 @@ def fbo(self) -> Framebuffer:
327327
"""
328328
Get the currently active framebuffer (read only).
329329
"""
330-
return self.active_framebuffer
330+
return self._active_framebuffer
331+
332+
@property
333+
def active_framebuffer(self) -> Framebuffer:
334+
"""
335+
Get the currently active framebuffer.
336+
"""
337+
return self._active_framebuffer
338+
339+
@active_framebuffer.setter
340+
def active_framebuffer(self, framebuffer: Framebuffer) -> None:
341+
self._active_framebuffer = framebuffer
331342

332343
def gc(self) -> int:
333344
"""

0 commit comments

Comments
 (0)