diff --git a/arcade/camera/camera_2d.py b/arcade/camera/camera_2d.py index 10e3d50bde..19785d954c 100644 --- a/arcade/camera/camera_2d.py +++ b/arcade/camera/camera_2d.py @@ -8,6 +8,8 @@ from typing_extensions import Self from arcade.camera.data_types import ( + DEFAULT_FAR, + DEFAULT_NEAR_ORTHO, CameraData, OrthographicProjectionData, ZeroProjectionDimension, @@ -90,8 +92,8 @@ def __init__( up: tuple[float, float] = (0.0, 1.0), zoom: float = 1.0, projection: Rect | None = None, - near: float = -100.0, - far: float = 100.0, + near: float = DEFAULT_NEAR_ORTHO, + far: float = DEFAULT_FAR, *, scissor: Rect | None = None, render_target: Framebuffer | None = None, diff --git a/arcade/camera/data_types.py b/arcade/camera/data_types.py index 6fbf197e94..c0515762d3 100644 --- a/arcade/camera/data_types.py +++ b/arcade/camera/data_types.py @@ -7,7 +7,7 @@ from __future__ import annotations from contextlib import contextmanager -from typing import Generator, Protocol +from typing import Final, Generator, Protocol from pyglet.math import Vec2, Vec3 from typing_extensions import Self @@ -16,6 +16,8 @@ __all__ = [ "CameraData", + "DEFAULT_FAR", + "DEFAULT_NEAR_ORTHO", "OrthographicProjectionData", "PerspectiveProjectionData", "Projection", @@ -25,6 +27,23 @@ "duplicate_camera_data", ] +DEFAULT_NEAR_ORTHO: Final[float] = -100.0 +"""The default backward-facing depth cutoff for orthographic rendering. + +Unless an orthographic camera is provided a different value, this will be +used as the near cutoff for its point of view. + +The :py:class:`~arcade.camera.perspective.PerspectiveProjector` uses +``0.01`` as its default near value to avoid division by zero. +""" + +DEFAULT_FAR: Final[float] = 100.0 +"""The default forward-facing depth cutoff for all Arcade cameras. + +Unless a camera is provided a different value, anything further away than this +value will not be drawn. +""" + class ZeroProjectionDimension(ValueError): """A projection's dimensions were zero along at least one axis. diff --git a/arcade/camera/orthographic.py b/arcade/camera/orthographic.py index 7e90b06c46..ed408c697e 100644 --- a/arcade/camera/orthographic.py +++ b/arcade/camera/orthographic.py @@ -6,7 +6,13 @@ from pyglet.math import Mat4, Vec2, Vec3 from typing_extensions import Self -from arcade.camera.data_types import CameraData, OrthographicProjectionData, Projector +from arcade.camera.data_types import ( + DEFAULT_FAR, + DEFAULT_NEAR_ORTHO, + CameraData, + OrthographicProjectionData, + Projector, +) from arcade.camera.projection_functions import ( generate_orthographic_matrix, generate_view_matrix, @@ -78,8 +84,8 @@ def __init__( 0.5 * self._window.width, # Left, Right -0.5 * self._window.height, 0.5 * self._window.height, # Bottom, Top - -100, - 100, # Near, Far + DEFAULT_NEAR_ORTHO, + DEFAULT_FAR, # Near, Far ) @property diff --git a/arcade/camera/perspective.py b/arcade/camera/perspective.py index 73c4ae268a..b65b86cbf0 100644 --- a/arcade/camera/perspective.py +++ b/arcade/camera/perspective.py @@ -7,7 +7,7 @@ from pyglet.math import Mat4, Vec2, Vec3 from typing_extensions import Self -from arcade.camera.data_types import CameraData, PerspectiveProjectionData, Projector +from arcade.camera.data_types import DEFAULT_FAR, CameraData, PerspectiveProjectionData, Projector from arcade.camera.projection_functions import ( generate_perspective_matrix, generate_view_matrix, @@ -27,6 +27,12 @@ class PerspectiveProjector(Projector): """ The simplest from of a perspective camera. + + .. warning:: Near cutoffs for perspective projection must be greater than zero. + + This prevents division by zero errors since perspective involves + dividing by distance. + Using ViewData and PerspectiveProjectionData PoDs (Pack of Data) it generates the correct projection and view matrices. It also provides methods and a context manager for using the matrices in @@ -78,7 +84,7 @@ def __init__( self._window.width / self._window.height, # Aspect 60, # Field of View, 0.01, - 100.0, # near, # far + DEFAULT_FAR, # near, # far ) @property diff --git a/arcade/experimental/depth_of_field.py b/arcade/examples/depth_of_field.py similarity index 83% rename from arcade/experimental/depth_of_field.py rename to arcade/examples/depth_of_field.py index c332f81f5c..e1fcf40515 100644 --- a/arcade/experimental/depth_of_field.py +++ b/arcade/examples/depth_of_field.py @@ -3,8 +3,9 @@ It uses the depth attribute of along with blurring and shaders to roughly approximate depth-based blur effects. The focus bounces back forth automatically between a maximum and minimum depth value -based on time. Adjust the arguments to the App class at the bottom -of the file to change the speed. +based on time. Change the speed and focus via either the constants +at the top of the file or the arguments passed to it at the bottom of +the file. This example works by doing the following for each frame: @@ -17,7 +18,7 @@ both easier and more performant than more accurate blur approaches. If Python and Arcade are installed, this example can be run from the command line with: -python -m arcade.experimental.examples.array_backed_grid +python -m arcade.examples.depth_of_field """ from __future__ import annotations @@ -30,12 +31,20 @@ from pyglet.graphics import Batch -from arcade import SpriteList, SpriteSolidColor, Text, Window, get_window +import arcade +from arcade import get_window, SpriteList, SpriteSolidColor, Text, Window, View +from arcade.camera.data_types import DEFAULT_NEAR_ORTHO, DEFAULT_FAR from arcade.color import RED from arcade.experimental.postprocessing import GaussianBlur from arcade.gl import NEAREST, Program, Texture2D, geometry from arcade.types import RGBA255, Color +WINDOW_TITLE = "Depth of Field Example" + +WINDOW_WIDTH = 1280 +WINDOW_HEIGHT = 720 +BACKGROUND_GRAY = Color(155, 155, 155, 255) + class DepthOfField: """A depth-of-field effect we can use as a render context manager. @@ -50,7 +59,7 @@ class DepthOfField: def __init__( self, size: tuple[int, int] | None = None, - clear_color: RGBA255 = (155, 155, 155, 255), + clear_color: RGBA255 = BACKGROUND_GRAY ): self._geo = geometry.quad_2d_fs() self._win: Window = get_window() @@ -171,9 +180,13 @@ def render(self): self._geo.render(self._render_program) -class App(Window): +class GameView(View): """Window subclass to hold sprites and rendering helpers. + To keep the code simpler, this example uses a default camera. That means + that any sprite outside Arcade's default camera near and far render cutoffs + (``-100.0`` to ``100.0``) will not be drawn. + Args: text_color: The color of the focus indicator. @@ -182,6 +195,10 @@ class App(Window): focus_change_speed: How fast the focus bounces back and forth between the ``-focus_range`` and ``focus_range``. + min_sprite_depth: + The minimum sprite depth we'll generate sprites between + max_sprite_depth: + The maximum sprite depth we'll generate sprites between. """ def __init__( @@ -189,6 +206,8 @@ def __init__( text_color: RGBA255 = RED, focus_range: float = 16.0, focus_change_speed: float = 0.1, + min_sprite_depth: float = DEFAULT_NEAR_ORTHO, + max_sprite_depth: float = DEFAULT_FAR ): super().__init__() self.sprites: SpriteList = SpriteList() @@ -207,7 +226,7 @@ def __init__( # Randomize sprite depth, size, and angle, but set color from depth. for _ in range(100): - depth = uniform(-100, 100) + depth = uniform(min_sprite_depth, max_sprite_depth) color = Color.from_gray(int(255 * (depth + 100) / 200)) s = SpriteSolidColor( randint(100, 200), @@ -223,7 +242,8 @@ def __init__( self.dof = DepthOfField() def on_update(self, delta_time: float): - raw_focus = self.focus_range * (cos(pi * self.focus_change_speed * self.time) * 0.5 + 0.5) + time = self.window.time + raw_focus = self.focus_range * (cos(pi * self.focus_change_speed * time) * 0.5 + 0.5) self.dof.render_program["focus_depth"] = raw_focus / self.focus_range self.indicator_label.value = f"Focus depth: {raw_focus:.3f} / {self.focus_range}" @@ -235,10 +255,25 @@ def on_draw(self): self.sprites.draw(pixelated=True) # Draw the blurred frame buffer and then the focus display - self.use() + window = self.window + window.use() self.dof.render() self._batch.draw() +def main(): + # Create a Window to show the view defined above. + window = arcade.Window(WINDOW_WIDTH, WINDOW_HEIGHT, WINDOW_TITLE) + + # Create the view + app = GameView() + + # Show GameView on screen + window.show_view(app) + + # Start the arcade game loop + window.run() + + if __name__ == "__main__": - App().run() + main() diff --git a/arcade/experimental/sprite_depth_cosine.py b/arcade/examples/sprite_depth_cosine.py similarity index 67% rename from arcade/experimental/sprite_depth_cosine.py rename to arcade/examples/sprite_depth_cosine.py index 299c525f0c..ef519a66aa 100644 --- a/arcade/experimental/sprite_depth_cosine.py +++ b/arcade/examples/sprite_depth_cosine.py @@ -6,12 +6,12 @@ During each update, the depth of each sprite is updated to follow a cosine wave. Afterward, the following is drawn: - * All sprites in depth-sorted order - * A white square centered over each sprite along the x-axis, and moving - with the wave along the y-axis +* All sprites in depth-sorted order +* A white square centered over each sprite along the x-axis, and moving + with the wave along the y-axis If Python and Arcade are installed, this example can be run from the command line with: -python -m arcade.experimental.sprite_depth_cosine +python -m arcade.examples.sprite_depth_cosine """ from __future__ import annotations @@ -23,21 +23,22 @@ import arcade # All constants are in pixels -WIDTH, HEIGHT = 1280, 720 +WINDOW_WIDTH, WINDOW_HEIGHT = 1280, 720 +WINDOW_TITLE = "Sprite Depth Testing Example w/ a Cosine Wave" NUM_SPRITES = 10 SPRITE_X_START = 150 SPRITE_X_STEP = 50 -SPRITE_Y = HEIGHT // 2 +SPRITE_Y = WINDOW_HEIGHT // 2 DOT_SIZE = 10 -class MyGame(arcade.Window): +class GameView(arcade.View): def __init__(self): - super().__init__(WIDTH, HEIGHT, "Sprite Depth Testing Example w/ a Cosine Wave") + super().__init__() texture = arcade.load_texture(":resources:images/test_textures/xy_square.png") self.text_batch = Batch() @@ -53,7 +54,6 @@ def __init__(self): ) self.sprite_list = arcade.SpriteList() - self.time = 0.0 for i in range(NUM_SPRITES): sprite = arcade.Sprite( @@ -64,9 +64,10 @@ def __init__(self): def on_draw(self): self.clear() + ctx = self.window.ctx if self.use_depth: - # This context manager temporarily enables depth testing - with self.ctx.enabled(self.ctx.DEPTH_TEST): + # This with block temporarily enables depth testing + with ctx.enabled(ctx.DEPTH_TEST): self.sprite_list.draw() else: self.sprite_list.draw() @@ -88,11 +89,26 @@ def on_key_press(self, symbol: int, modifiers: int): self.text_use_depth.text = f"SPACE: Toggle depth testing ({self.use_depth})" def on_update(self, delta_time): - self.time += delta_time - + # Using time from the window's clock simplifies the math below + time = self.window.time for i, sprite in enumerate(self.sprite_list): - sprite.depth = math.cos(self.time + i) * SPRITE_X_STEP + sprite.depth = math.cos(time + i) * SPRITE_X_STEP + + +def main(): + """ Main function """ + # Create a window class. This is what actually shows up on screen + window = arcade.Window(WINDOW_WIDTH, WINDOW_HEIGHT, WINDOW_TITLE) + + # Create the GameView + game = GameView() + + # Show GameView on screen + window.show_view(game) + + # Start the arcade game loop + arcade.run() if __name__ == "__main__": - MyGame().run() + main() diff --git a/doc/example_code/depth_of_field.rst b/doc/example_code/depth_of_field.rst new file mode 100644 index 0000000000..eaa61a80b0 --- /dev/null +++ b/doc/example_code/depth_of_field.rst @@ -0,0 +1,15 @@ +:orphan: + +.. _depth_of_field: + +Depth of Field Blur +=================== + +.. image:: images/depth_of_field.png + :width: 600px + :align: center + :alt: Screenshot of a time-dependent depth of field blur effect. + +.. literalinclude:: ../../arcade/examples/depth_of_field.py + :caption: depth_of_field.py + :linenos: diff --git a/doc/example_code/images/depth_of_field.png b/doc/example_code/images/depth_of_field.png new file mode 100644 index 0000000000..6f70838b78 Binary files /dev/null and b/doc/example_code/images/depth_of_field.png differ diff --git a/doc/example_code/images/sprite_depth_cosine.png b/doc/example_code/images/sprite_depth_cosine.png new file mode 100644 index 0000000000..e53385903f Binary files /dev/null and b/doc/example_code/images/sprite_depth_cosine.png differ diff --git a/doc/example_code/index.rst b/doc/example_code/index.rst index 0ff999fa9a..d6201d69a6 100644 --- a/doc/example_code/index.rst +++ b/doc/example_code/index.rst @@ -182,6 +182,9 @@ Player Movement :ref:`sprite_rotate_around_tank` + + + Non-Player Movement ^^^^^^^^^^^^^^^^^^^ @@ -221,6 +224,7 @@ Non-Player Movement :ref:`sprite_rotate_around_point` + Easing ^^^^^^ @@ -290,6 +294,12 @@ Sprite Properties :ref:`sprite_change_coins` +.. figure:: images/thumbs/sprite_depth_cosine.png + :figwidth: 170px + :target: sprite_depth_cosine.html + + :ref:`sprite_depth_cosine` + Games with Levels ^^^^^^^^^^^^^^^^^ @@ -773,6 +783,13 @@ Frame Buffers :ref:`perspective` +.. figure:: images/thumbs/depth_of_field.png + :figwidth: 170px + :target: depth_of_field.html + + :ref:`depth_of_field` + + .. _opengl: OpenGL diff --git a/doc/example_code/sprite_depth_cosine.rst b/doc/example_code/sprite_depth_cosine.rst new file mode 100644 index 0000000000..68b713cfd6 --- /dev/null +++ b/doc/example_code/sprite_depth_cosine.rst @@ -0,0 +1,15 @@ +:orphan: + +.. _sprite_depth_cosine: + +Sprite Depth Controlled by a Cosine Wave +======================================== + +.. image:: images/sprite_depth_cosine.png + :width: 600px + :align: center + :alt: Screenshot of sprites whose depth value is controlled by time-dependent cosine wave. + +.. literalinclude:: ../../arcade/examples/sprite_depth_cosine.py + :caption: sprite_depth_cosine.py + :linenos: