Skip to content

Commit bf0cfb8

Browse files
Update the DefaultProjector to behave better when changing framebuffers (#2802)
* move viewport projector to it's own file to hopefully improve it's visibility * Updating the default projector to behave better with changing framebuffer * fix recursion, and explicit viewport bugs * use fbo instead of active_framebuffer, and explicitly get fbo width/height * linting, formatting, and fixing reference location for default projector * reset window viewport from drawing command * Handle resetting the default camera on the context during unit tests * remove None from ViewportProjector viewport property * remove Rect from default projector as it isn't needed, and avoid recursion loop --------- Co-authored-by: Darren Eberly <darren.eberly@proton.me>
1 parent 9984b18 commit bf0cfb8

File tree

6 files changed

+233
-81
lines changed

6 files changed

+233
-81
lines changed

arcade/camera/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323

2424
from arcade.camera.orthographic import OrthographicProjector
2525
from arcade.camera.perspective import PerspectiveProjector
26+
from arcade.camera.viewport import ViewportProjector
2627

2728
from arcade.camera.camera_2d import Camera2D
2829

@@ -33,6 +34,7 @@
3334
"Projection",
3435
"Projector",
3536
"CameraData",
37+
"ViewportProjector",
3638
"generate_view_matrix",
3739
"OrthographicProjectionData",
3840
"generate_orthographic_matrix",

arcade/camera/default.py

Lines changed: 100 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -7,64 +7,125 @@
77
from pyglet.math import Mat4, Vec2, Vec3
88
from typing_extensions import Self
99

10-
from arcade.types import LBWH, Point, Rect
10+
from arcade.camera.data_types import DEFAULT_FAR, DEFAULT_NEAR_ORTHO
11+
from arcade.types import Point
1112
from arcade.window_commands import get_window
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.
2225
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.
26+
The job of the default projector is to ensure that when no other Projector
27+
(Camera2D, OthrographicProjector, PerspectiveProjector, etc) is in use the
28+
projection and view matrices are correct such at (0.0, 0.0) is in the bottom
29+
left corner of the viewport and that one pixel equals one 'unit'.
2530
2631
Args:
27-
viewport: The viewport to project to.
28-
context: The window context to bind the camera to. Defaults to the currently active window.
32+
context: The window context to bind the camera to. Defaults to the currently active context.
2933
"""
3034

31-
def __init__(
32-
self,
33-
viewport: Rect | None = None,
34-
*,
35-
context: ArcadeContext | None = None,
36-
):
35+
def __init__(self, *, context: ArcadeContext | None = None):
3736
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-
)
37+
self._viewport: tuple[int, int, int, int] | None = None
38+
self._scissor: tuple[int, int, int, int] | None = None
39+
self._matrix: Mat4 | None = None
4240

43-
@property
44-
def viewport(self) -> Rect:
41+
def update_viewport(self):
4542
"""
46-
The viewport use to derive projection and view matrix.
43+
Called when the ArcadeContext's viewport or active
44+
framebuffer has been set. It only actually updates
45+
the viewport if no other camera is active. Also
46+
setting the viewport to match the size of the active
47+
framebuffer sets the viewport to None.
4748
"""
49+
50+
# If another camera is active then the viewport was probably set
51+
# by camera.use()
52+
if self._ctx.current_camera != self:
53+
return
54+
55+
if (
56+
self._ctx.viewport[2] != self._ctx.fbo.width
57+
or self._ctx.viewport[3] != self._ctx.fbo.height
58+
):
59+
self.viewport = self._ctx.viewport
60+
else:
61+
self.viewport = None
62+
63+
self.use()
64+
65+
@property
66+
def viewport(self) -> tuple[int, int, int, int] | None:
4867
return self._viewport
4968

5069
@viewport.setter
51-
def viewport(self, viewport: Rect) -> None:
70+
def viewport(self, viewport: tuple[int, int, int, int] | None) -> None:
71+
if viewport == self._viewport:
72+
return
5273
self._viewport = viewport
53-
self._projection_matrix = Mat4.orthogonal_projection(
54-
0, viewport.width, 0, viewport.height, -100, 100
74+
self._matrix = Mat4.orthogonal_projection(
75+
0, self.width, 0, self.height, DEFAULT_NEAR_ORTHO, DEFAULT_FAR
5576
)
5677

78+
@viewport.deleter
79+
def viewport(self):
80+
self.viewport = None
81+
82+
@property
83+
def scissor(self) -> tuple[int, int, int, int] | None:
84+
return self._scissor
85+
86+
@scissor.setter
87+
def scissor(self, scissor: tuple[int, int, int, int] | None) -> None:
88+
self._scissor = scissor
89+
90+
@scissor.deleter
91+
def scissor(self) -> None:
92+
self._scissor = None
93+
94+
@property
95+
def width(self) -> int:
96+
if self._viewport is not None:
97+
return int(self._viewport[2])
98+
return self._ctx.fbo.width
99+
100+
@property
101+
def height(self) -> int:
102+
if self._viewport is not None:
103+
return int(self._viewport[3])
104+
return self._ctx.fbo.height
105+
106+
def get_current_viewport(self) -> tuple[int, int, int, int]:
107+
if self._viewport is not None:
108+
return self._viewport
109+
return (0, 0, self._ctx.fbo.width, self._ctx.fbo.height)
110+
57111
def use(self) -> None:
58112
"""
59-
Set the window's projection and view matrix.
60-
Also sets the projector as the windows current camera.
113+
Set the window's Projection and View matrices.
61114
"""
62-
self._ctx.current_camera = self
63115

64-
self._ctx.viewport = self.viewport.lbwh_int # get the integer 4-tuple LBWH
116+
viewport = self.get_current_viewport()
117+
118+
self._ctx.current_camera = self
119+
if self._ctx.viewport != viewport:
120+
self._ctx.active_framebuffer.viewport = viewport
121+
self._ctx.scissor = None if self._scissor is None else self._scissor
65122

66123
self._ctx.view_matrix = Mat4()
67-
self._ctx.projection_matrix = self._projection_matrix
124+
if self._matrix is None:
125+
self._matrix = Mat4.orthogonal_projection(
126+
0, viewport[2], 0, viewport[3], DEFAULT_NEAR_ORTHO, DEFAULT_FAR
127+
)
128+
self._ctx.projection_matrix = self._matrix
68129

69130
@contextmanager
70131
def activate(self) -> Generator[Self, None, None]:
@@ -73,12 +134,20 @@ def activate(self) -> Generator[Self, None, None]:
73134
74135
usable with the 'with' block. e.g. 'with ViewportProjector.activate() as cam: ...'
75136
"""
76-
previous = self._ctx.current_camera
137+
previous_projector = self._ctx.current_camera
138+
previous_view = self._ctx.view_matrix
139+
previous_projection = self._ctx.projection_matrix
140+
previous_scissor = self._ctx.scissor
141+
previous_viewport = self._ctx.viewport
77142
try:
78143
self.use()
79144
yield self
80145
finally:
81-
previous.use()
146+
self._ctx.viewport = previous_viewport
147+
self._ctx.scissor = previous_scissor
148+
self._ctx.projection_matrix = previous_projection
149+
self._ctx.view_matrix = previous_view
150+
self._ctx.current_camera = previous_projector
82151

83152
def project(self, world_coordinate: Point) -> Vec2:
84153
"""
@@ -97,46 +166,3 @@ def unproject(self, screen_coordinate: Point) -> Vec3:
97166
z = 0.0 if not _z else _z[0]
98167

99168
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/camera/viewport.py

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
from __future__ import annotations
2+
3+
from collections.abc import Generator
4+
from contextlib import contextmanager
5+
from typing import TYPE_CHECKING
6+
7+
from pyglet.math import Mat4, Vec2, Vec3
8+
from typing_extensions import Self
9+
10+
from arcade.camera.data_types import DEFAULT_FAR, DEFAULT_NEAR_ORTHO
11+
from arcade.types import LBWH, Point, Rect
12+
from arcade.window_commands import get_window
13+
14+
if TYPE_CHECKING:
15+
from arcade.context import ArcadeContext
16+
17+
__all__ = ["ViewportProjector"]
18+
19+
20+
class ViewportProjector:
21+
"""
22+
A simple Projector which does not rely on any camera PoDs.
23+
24+
Does not have a way of moving, rotating, or zooming the camera.
25+
perfect for something like UI or for mapping to an offscreen framebuffer.
26+
27+
Args:
28+
viewport: The viewport to project to.
29+
context: The window context to bind the camera to. Defaults to the currently active window.
30+
"""
31+
32+
def __init__(
33+
self,
34+
viewport: Rect | None = None,
35+
*,
36+
context: ArcadeContext | None = None,
37+
):
38+
self._ctx: ArcadeContext = context or get_window().ctx
39+
self._viewport: Rect = viewport or LBWH(*self._ctx.viewport)
40+
self._projection_matrix: Mat4 = Mat4.orthogonal_projection(
41+
0.0, self._viewport.width, 0.0, self._viewport.height, DEFAULT_NEAR_ORTHO, DEFAULT_FAR
42+
)
43+
44+
@property
45+
def viewport(self) -> Rect:
46+
"""
47+
The viewport use to derive projection and view matrix.
48+
"""
49+
return self._viewport
50+
51+
@viewport.setter
52+
def viewport(self, viewport: Rect) -> None:
53+
self._viewport = viewport
54+
self._projection_matrix = Mat4.orthogonal_projection(
55+
0, viewport.width, 0, viewport.height, DEFAULT_NEAR_ORTHO, DEFAULT_FAR
56+
)
57+
58+
def use(self) -> None:
59+
"""
60+
Set the window's projection and view matrix.
61+
Also sets the projector as the windows current camera.
62+
"""
63+
self._ctx.current_camera = self
64+
65+
if self.viewport:
66+
self._ctx.viewport = self.viewport.lbwh_int
67+
68+
self._ctx.view_matrix = Mat4()
69+
self._ctx.projection_matrix = self._projection_matrix
70+
71+
@contextmanager
72+
def activate(self) -> Generator[Self, None, None]:
73+
"""
74+
The context manager version of the use method.
75+
76+
usable with the 'with' block. e.g. 'with ViewportProjector.activate() as cam: ...'
77+
"""
78+
previous = self._ctx.current_camera
79+
previous_viewport = self._ctx.viewport
80+
try:
81+
self.use()
82+
yield self
83+
finally:
84+
self._ctx.viewport = previous_viewport
85+
previous.use()
86+
87+
def project(self, world_coordinate: Point) -> Vec2:
88+
"""
89+
Take a Vec2 or Vec3 of coordinates and return the related screen coordinate
90+
"""
91+
x, y, *z = world_coordinate
92+
return Vec2(x, y)
93+
94+
def unproject(self, screen_coordinate: Point) -> Vec3:
95+
"""
96+
Map the screen pos to screen_coordinates.
97+
98+
Due to the nature of viewport projector this does not do anything.
99+
"""
100+
x, y, *_z = screen_coordinate
101+
z = 0.0 if not _z else _z[0]
102+
103+
return Vec3(x, y, z)

arcade/context.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -334,6 +334,8 @@ def reset(self) -> None:
334334
self.projection_matrix = Mat4.orthogonal_projection(
335335
0, self.window.width, 0, self.window.height, -100, 100
336336
)
337+
self._default_camera: DefaultProjector = DefaultProjector(context=self)
338+
self.current_camera = self._default_camera
337339
self.enable_only(self.BLEND)
338340
self.blend_func = self.BLEND_DEFAULT
339341
self.point_size = 1.0
@@ -372,6 +374,15 @@ def default_atlas(self) -> TextureAtlasBase:
372374

373375
return self._atlas
374376

377+
@property
378+
def active_framebuffer(self):
379+
return self._active_framebuffer
380+
381+
@active_framebuffer.setter
382+
def active_framebuffer(self, framebuffer: Framebuffer):
383+
self._active_framebuffer = framebuffer
384+
self._default_camera.update_viewport()
385+
375386
@property
376387
def viewport(self) -> tuple[int, int, int, int]:
377388
"""
@@ -393,8 +404,7 @@ def viewport(self) -> tuple[int, int, int, int]:
393404
@viewport.setter
394405
def viewport(self, value: tuple[int, int, int, int]):
395406
self.active_framebuffer.viewport = value
396-
if self._default_camera == self.current_camera:
397-
self._default_camera.use()
407+
self._default_camera.update_viewport()
398408

399409
@property
400410
def projection_matrix(self) -> Mat4:

0 commit comments

Comments
 (0)