Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions arcade/camera/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@

from arcade.camera.orthographic import OrthographicProjector
from arcade.camera.perspective import PerspectiveProjector
from arcade.camera.viewport import ViewportProjector

from arcade.camera.camera_2d import Camera2D

Expand All @@ -33,6 +34,7 @@
"Projection",
"Projector",
"CameraData",
"ViewportProjector",
"generate_view_matrix",
"OrthographicProjectionData",
"generate_orthographic_matrix",
Expand Down
169 changes: 98 additions & 71 deletions arcade/camera/default.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,66 +5,134 @@
from typing import TYPE_CHECKING

from pyglet.math import Mat4, Vec2, Vec3
from pyglet.window.key import F
from typing_extensions import Self

from arcade.camera.data_types import DEFAULT_FAR, DEFAULT_NEAR_ORTHO
from arcade.types import LBWH, Point, Rect
from arcade.window_commands import get_window

if TYPE_CHECKING:
from arcade.context import ArcadeContext

__all__ = ["ViewportProjector", "DefaultProjector"]
__all__ = ()


class ViewportProjector:
class DefaultProjector:
"""
A simple Projector which does not rely on any camera PoDs.
An extremely limited projector which lacks any kind of control. This is only
here to act as the default camera used internally by Arcade. There should be
no instance where a developer would want to use this class.

The default viewport tries it's best to allow
simple usecases with no need to use a camera.

Does not have a way of moving, rotating, or zooming the camera.
perfect for something like UI or for mapping to an offscreen framebuffer.
It does this by defaulting to the size of the active
framebuffer. If the user sets the framebuffer's viewport
without a camera then the default camera will match it
until the framebuffer is changed again.

Args:
viewport: The viewport to project to.
context: The window context to bind the camera to. Defaults to the currently active window.
context: The window context to bind the camera to. Defaults to the currently active context.
"""

def __init__(
self,
viewport: Rect | None = None,
*,
context: ArcadeContext | None = None,
):
def __init__(self, *, context: ArcadeContext | None = None):
self._ctx: ArcadeContext = context or get_window().ctx
self._viewport: Rect = viewport or LBWH(*self._ctx.viewport)
self._projection_matrix: Mat4 = Mat4.orthogonal_projection(
0.0, self._viewport.width, 0.0, self._viewport.height, -100, 100
)
self._viewport: Rect | None = None
self._scissor: Rect | None = None
self._matrix: Mat4 | None = None
self._updating: bool = False

@property
def viewport(self) -> Rect:
def update_viewport(self):
"""
The viewport use to derive projection and view matrix.
Called when the ArcadeContext's viewport or active
framebuffer has been set. It only actually updates
the viewport if no other camera is active. Also
setting the viewport to match the size of the active
framebuffer sets the viewport to None.
"""

# If another camera is active then the viewport was probably set
# by camera.use()
if self._ctx.current_camera != self or self._updating:
return
self._updating = True

if (
self._ctx.viewport[2] != self._ctx.fbo.width
or self._ctx.viewport[3] != self._ctx.fbo.height
):
self.viewport = LBWH(*self._ctx.viewport)
else:
self.viewport = None

self.use()
self._updating = False

@property
def viewport(self) -> Rect | None:
return self._viewport

@viewport.setter
def viewport(self, viewport: Rect) -> None:
def viewport(self, viewport: Rect | None) -> None:
if viewport == self._viewport:
return
self._viewport = viewport
self._projection_matrix = Mat4.orthogonal_projection(
0, viewport.width, 0, viewport.height, -100, 100
self._matrix = Mat4.orthogonal_projection(
0, self.width, 0, self.height, DEFAULT_NEAR_ORTHO, DEFAULT_FAR
)

@viewport.deleter
def viewport(self):
self.viewport = None

@property
def scissor(self) -> Rect | None:
return self._scissor

@scissor.setter
def scissor(self, scissor: Rect | None) -> None:
self._scissor = scissor

@scissor.deleter
def scissor(self) -> None:
self._scissor = None

@property
def width(self) -> int:
if self._viewport is not None:
return int(self._viewport.width)
return self._ctx.fbo.width

@property
def height(self) -> int:
if self._viewport is not None:
return int(self._viewport.height)
return self._ctx.fbo.height

def get_current_viewport(self) -> tuple[int, int, int, int]:
if self._viewport is not None:
return self._viewport.lbwh_int
return (0, 0, self._ctx.fbo.width, self._ctx.fbo.height)

def use(self) -> None:
"""
Set the window's projection and view matrix.
Also sets the projector as the windows current camera.
Set the window's Projection and View matrices.
"""
self._ctx.current_camera = self

self._ctx.viewport = self.viewport.lbwh_int # get the integer 4-tuple LBWH
viewport = self.get_current_viewport()

self._ctx.current_camera = self
if self._ctx.viewport != viewport:
self._ctx.viewport = viewport
self._ctx.scissor = None if self._scissor is None else self._scissor.lbwh_int

self._ctx.view_matrix = Mat4()
self._ctx.projection_matrix = self._projection_matrix
if self._matrix is None:
self._matrix = Mat4.orthogonal_projection(
0, viewport[2], 0, viewport[3], DEFAULT_NEAR_ORTHO, DEFAULT_FAR
)
self._ctx.projection_matrix = self._matrix

@contextmanager
def activate(self) -> Generator[Self, None, None]:
Expand All @@ -74,10 +142,12 @@ def activate(self) -> Generator[Self, None, None]:
usable with the 'with' block. e.g. 'with ViewportProjector.activate() as cam: ...'
"""
previous = self._ctx.current_camera
previous_viewport = self._ctx.viewport
try:
self.use()
yield self
finally:
self._ctx.viewport = previous_viewport
previous.use()

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

return Vec3(x, y, z)


# As this class is only supposed to be used internally
# I wanted to place an _ in front, but the linting complains
# about it being a protected class.
class DefaultProjector(ViewportProjector):
"""
An extremely limited projector which lacks any kind of control. This is only
here to act as the default camera used internally by Arcade. There should be
no instance where a developer would want to use this class.

Args:
context: The window context to bind the camera to. Defaults to the currently active window.
"""

def __init__(self, *, context: ArcadeContext | None = None):
super().__init__(context=context)

def use(self) -> None:
"""
Set the window's Projection and View matrices.

cache's the window viewport to determine the projection matrix.
"""

viewport = self.viewport.lbwh_int
# If the viewport is correct and the default camera is in use,
# then don't waste time resetting the view and projection matrices
if self._ctx.viewport == viewport and self._ctx.current_camera == self:
return

# If the viewport has changed while the default camera is active then the
# default needs to update itself.
# If it was another camera's viewport being used the default camera should not update.
if self._ctx.viewport != viewport and self._ctx.current_camera == self:
self.viewport = LBWH(*self._ctx.viewport)
else:
self._ctx.viewport = viewport

self._ctx.current_camera = self

self._ctx.view_matrix = Mat4()
self._ctx.projection_matrix = self._projection_matrix
103 changes: 103 additions & 0 deletions arcade/camera/viewport.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
from __future__ import annotations

from collections.abc import Generator
from contextlib import contextmanager
from typing import TYPE_CHECKING

from pyglet.math import Mat4, Vec2, Vec3
from typing_extensions import Self

from arcade.camera.data_types import DEFAULT_FAR, DEFAULT_NEAR_ORTHO
from arcade.types import LBWH, Point, Rect
from arcade.window_commands import get_window

if TYPE_CHECKING:
from arcade.context import ArcadeContext

__all__ = ["ViewportProjector"]


class ViewportProjector:
"""
A simple Projector which does not rely on any camera PoDs.

Does not have a way of moving, rotating, or zooming the camera.
perfect for something like UI or for mapping to an offscreen framebuffer.

Args:
viewport: The viewport to project to.
context: The window context to bind the camera to. Defaults to the currently active window.
"""

def __init__(
self,
viewport: Rect | None = None,
*,
context: ArcadeContext | None = None,
):
self._ctx: ArcadeContext = context or get_window().ctx
self._viewport: Rect = viewport or LBWH(*self._ctx.viewport)
self._projection_matrix: Mat4 = Mat4.orthogonal_projection(
0.0, self.viewport.width, 0.0, self.viewport.height, DEFAULT_NEAR_ORTHO, DEFAULT_FAR
)

@property
def viewport(self) -> Rect | None:
"""
The viewport use to derive projection and view matrix.
"""
return self._viewport

@viewport.setter
def viewport(self, viewport: Rect) -> None:
self._viewport = viewport
self._projection_matrix = Mat4.orthogonal_projection(
0, viewport.width, 0, viewport.height, DEFAULT_NEAR_ORTHO, DEFAULT_FAR
)

def use(self) -> None:
"""
Set the window's projection and view matrix.
Also sets the projector as the windows current camera.
"""
self._ctx.current_camera = self

if self.viewport:
self._ctx.viewport = self.viewport.lbwh_int

self._ctx.view_matrix = Mat4()
self._ctx.projection_matrix = self._projection_matrix

@contextmanager
def activate(self) -> Generator[Self, None, None]:
"""
The context manager version of the use method.

usable with the 'with' block. e.g. 'with ViewportProjector.activate() as cam: ...'
"""
previous = self._ctx.current_camera
previous_viewport = self._ctx.viewport
try:
self.use()
yield self
finally:
self._ctx.viewport = previous_viewport
previous.use()

def project(self, world_coordinate: Point) -> Vec2:
"""
Take a Vec2 or Vec3 of coordinates and return the related screen coordinate
"""
x, y, *z = world_coordinate
return Vec2(x, y)

def unproject(self, screen_coordinate: Point) -> Vec3:
"""
Map the screen pos to screen_coordinates.

Due to the nature of viewport projector this does not do anything.
"""
x, y, *_z = screen_coordinate
z = 0.0 if not _z else _z[0]

return Vec3(x, y, z)
12 changes: 10 additions & 2 deletions arcade/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -372,6 +372,15 @@ def default_atlas(self) -> TextureAtlasBase:

return self._atlas

@property
def active_framebuffer(self):
return self._active_framebuffer

@active_framebuffer.setter
def active_framebuffer(self, framebuffer: Framebuffer):
self._active_framebuffer = framebuffer
self._default_camera.update_viewport()

@property
def viewport(self) -> tuple[int, int, int, int]:
"""
Expand All @@ -393,8 +402,7 @@ def viewport(self) -> tuple[int, int, int, int]:
@viewport.setter
def viewport(self, value: tuple[int, int, int, int]):
self.active_framebuffer.viewport = value
if self._default_camera == self.current_camera:
self._default_camera.use()
self._default_camera.update_viewport()

@property
def projection_matrix(self) -> Mat4:
Expand Down
15 changes: 13 additions & 2 deletions arcade/gl/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,7 @@ def __init__(
# Tracking active program
self.active_program: Program | ComputeShader | None = None
# Tracking active framebuffer. On context creation the window is the default render target
self.active_framebuffer: Framebuffer = self._screen
self._active_framebuffer: Framebuffer = self._screen
self._stats: ContextStats = ContextStats(warn_threshold=1000)

self._primitive_restart_index = -1
Expand Down Expand Up @@ -327,7 +327,18 @@ def fbo(self) -> Framebuffer:
"""
Get the currently active framebuffer (read only).
"""
return self.active_framebuffer
return self._active_framebuffer

@property
def active_framebuffer(self) -> Framebuffer:
"""
Get the currently active framebuffer.
"""
return self._active_framebuffer

@active_framebuffer.setter
def active_framebuffer(self, framebuffer: Framebuffer) -> None:
self._active_framebuffer = framebuffer

def gc(self) -> int:
"""
Expand Down
Loading
Loading