Skip to content

Commit 8ecd746

Browse files
authored
Start end render (#2204)
* rect_draw: allow specifying atlas * Revamp start/finish_render * Fix examples still using start_render * Unit test + fix old tests
1 parent 5c21400 commit 8ecd746

18 files changed

Lines changed: 201 additions & 54 deletions

arcade/application.py

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
import logging
99
import os
1010
import time
11-
from typing import Optional
11+
from typing import TYPE_CHECKING, Optional
1212

1313
import pyglet
1414
import pyglet.gl as gl
@@ -17,12 +17,17 @@
1717
from pyglet.window import MouseCursor
1818

1919
import arcade
20-
from arcade import SectionManager, get_display_size, set_window
2120
from arcade.clock import Clock, FixedClock
2221
from arcade.color import TRANSPARENT_BLACK
2322
from arcade.context import ArcadeContext
23+
from arcade.sections import SectionManager
2424
from arcade.types import LBWH, Color, Rect, RGBANormalized, RGBOrA255
2525
from arcade.utils import is_raspberry_pi
26+
from arcade.window_commands import get_display_size, set_window
27+
28+
if TYPE_CHECKING:
29+
from arcade.start_finish_data import StartFinishRenderData
30+
2631

2732
LOG = logging.getLogger(__name__)
2833

@@ -235,8 +240,6 @@ def __init__(
235240
self._current_view: Optional[View] = None
236241
self.textbox_time = 0.0
237242
self.key: Optional[int] = None
238-
self.flip_count: int = 0
239-
self.static_display: bool = False
240243

241244
# See if we should center the window
242245
if center_window:
@@ -255,6 +258,12 @@ def __init__(
255258
self.keyboard = None
256259
self.mouse = None
257260

261+
# Framebuffer for drawing content into when start_render is called.
262+
# These are typically functions just at module level wrapped in
263+
# start_render and finish_render calls. The framebuffer is repeatedly
264+
# rendered to the window when the event loop starts.
265+
self._start_finish_render_data: Optional[StartFinishRenderData] = None
266+
258267
@property
259268
def current_view(self) -> Optional["View"]:
260269
"""
@@ -629,6 +638,11 @@ def on_draw(self) -> Optional[bool]:
629638
"""
630639
Override this function to add your custom drawing code.
631640
"""
641+
if self._start_finish_render_data:
642+
self.clear()
643+
self._start_finish_render_data.draw()
644+
return True
645+
632646
return False
633647

634648
def on_resize(self, width: int, height: int) -> Optional[bool]:
@@ -890,13 +904,6 @@ def flip(self) -> None:
890904
num_collected = self.ctx.gc()
891905
LOG.debug("Garbage collected %s OpenGL resource(s)", num_collected)
892906

893-
# Attempt to handle static draw setups
894-
if self.static_display:
895-
if self.flip_count > 0:
896-
return
897-
else:
898-
self.flip_count += 1
899-
900907
super().flip()
901908

902909
def switch_to(self) -> None:

arcade/draw/rect.py

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
import array
2+
from typing import Optional
23

34
from arcade import gl
45
from arcade.color import WHITE
56
from arcade.math import rotate_point
67
from arcade.sprite import BasicSprite
78
from arcade.texture import Texture
9+
from arcade.texture_atlas.base import TextureAtlasBase
810
from arcade.types import LBWH, LRBT, RGBA255, XYWH, Color, PointList, Rect
911
from arcade.window_commands import get_window
1012

@@ -20,6 +22,7 @@ def draw_texture_rect(
2022
blend=True,
2123
alpha=1.0,
2224
pixelated=False,
25+
atlas: Optional[TextureAtlasBase] = None,
2326
) -> None:
2427
"""
2528
Draw a texture on a rectangle.
@@ -30,6 +33,7 @@ def draw_texture_rect(
3033
:param angle: Rotation of the texture in degrees. Defaults to zero.
3134
:param blend: If True, enable alpha blending. Defaults to True.
3235
:param alpha: Transparency of image. 0.0 is fully transparent, 1.0 (default) is visible.
36+
:param atlas: The texture atlas the texture resides in. if not supplied the default texture atlas is used
3337
"""
3438
ctx = get_window().ctx
3539

@@ -38,9 +42,9 @@ def draw_texture_rect(
3842
else:
3943
ctx.disable(ctx.BLEND)
4044

41-
atlas = ctx.default_atlas
45+
atlas = atlas or ctx.default_atlas
4246

43-
texture_id, _ = ctx.default_atlas.add(texture)
47+
texture_id, _ = atlas.add(texture)
4448
if pixelated:
4549
atlas.texture.filter = gl.NEAREST, gl.NEAREST
4650
else:
@@ -64,11 +68,22 @@ def draw_texture_rect(
6468
ctx.disable(ctx.BLEND)
6569

6670

67-
def draw_sprite(sprite: BasicSprite, *, blend: bool = True, alpha=1.0, pixelated=False) -> None:
71+
def draw_sprite(
72+
sprite: BasicSprite,
73+
*,
74+
blend: bool = True,
75+
alpha=1.0,
76+
pixelated=False,
77+
atlas: Optional[TextureAtlasBase] = None,
78+
) -> None:
6879
"""
6980
Draw a sprite.
7081
7182
:param sprite: The sprite to draw.
83+
:param blend: Draw the sprite with or without alpha blending
84+
:param alpha: Fade the sprite from completely transparent to opaque (range: 0.0 to 1.0)
85+
:param pixelated: If true the sprite will be render in pixelated style. Otherwise smooth/linear
86+
:param atlas: The texture atlas the texture resides in. if not supplied the default texture atlas is used
7287
"""
7388
draw_texture_rect(
7489
sprite.texture,
@@ -78,16 +93,28 @@ def draw_sprite(sprite: BasicSprite, *, blend: bool = True, alpha=1.0, pixelated
7893
blend=blend,
7994
alpha=alpha,
8095
pixelated=pixelated,
96+
atlas=atlas,
8197
)
8298

8399

84100
def draw_sprite_rect(
85-
sprite: BasicSprite, rect: Rect, *, blend: bool = True, alpha=1.0, pixelated=False
101+
sprite: BasicSprite,
102+
rect: Rect,
103+
*,
104+
blend: bool = True,
105+
alpha=1.0,
106+
pixelated=False,
107+
atlas: Optional[TextureAtlasBase] = None,
86108
) -> None:
87109
"""
88110
Draw a sprite.
89111
90112
:param sprite: The sprite to draw.
113+
:param rect: The location and size of the sprite
114+
:param blend: Draw the sprite with or without alpha blending
115+
:param alpha: Fade the sprite from completely transparent to opaque (range: 0.0 to 1.0)
116+
:param pixelated: If true the sprite will be render in pixelated style. Otherwise smooth/linear
117+
:param atlas: The texture atlas the texture resides in. if not supplied the default texture atlas is used
91118
"""
92119
draw_texture_rect(
93120
sprite.texture,
@@ -97,6 +124,7 @@ def draw_sprite_rect(
97124
blend=blend,
98125
alpha=alpha,
99126
pixelated=pixelated,
127+
atlas=atlas,
100128
)
101129

102130

arcade/examples/drawing_primitives.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
import arcade
2222

2323
# Open the window. Set the window title and dimensions (width and height)
24-
arcade.open_window(600, 600, "Drawing Primitives Example")
24+
arcade.open_window(600, 600, "Drawing Primitives Example", resizable=True)
2525

2626
# Set the background color to white
2727
# For a list of named colors see

arcade/examples/happy_face.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
SCREEN_TITLE = "Happy Face Example"
1414

1515
# Open the window. Set the window title and dimensions
16-
arcade.open_window(SCREEN_WIDTH, SCREEN_HEIGHT, SCREEN_TITLE)
16+
arcade.open_window(SCREEN_WIDTH, SCREEN_HEIGHT, SCREEN_TITLE, resizable=True)
1717

1818
# Set the background color
1919
arcade.set_background_color(arcade.color.WHITE)

arcade/examples/sections_demo_3.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -283,7 +283,7 @@ def __init__(self):
283283
self.section_manager.add_section(self.map)
284284

285285
def on_draw(self):
286-
arcade.start_render()
286+
self.clear()
287287

288288

289289
def main():

arcade/gui/examples/exp_scroll_area.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ def __init__(self):
4141
anchor.add(UIInputText().with_border())
4242

4343
def on_draw(self):
44-
arcade.start_render()
44+
self.clear()
4545
self.ui.draw()
4646

4747

arcade/gui/examples/side_bars_with_box_layout.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ def on_key_press(self, symbol: int, modifiers: int):
5555
pass
5656

5757
def on_draw(self):
58-
arcade.start_render()
58+
self.clear()
5959
self.ui.draw()
6060

6161

arcade/start_finish_data.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
from arcade.draw.rect import draw_texture_rect
2+
from arcade.texture.texture import Texture
3+
from arcade.types import LBWH, AnchorPoint
4+
from arcade.window_commands import get_window
5+
6+
7+
class StartFinishRenderData:
8+
"""
9+
State data for offscreen rendering with :py:meth:`arcade.start_render` and
10+
:py:meth:`arcade.finish_render`. This is only meant for simply module level
11+
drawing like creating a static image we display on the screen.
12+
13+
:param pixelated: Should the image be pixelated or smooth when scaled?
14+
:param blend: Should we draw with alpha blending enabled?
15+
"""
16+
17+
def __init__(self, pixelated: bool = False, blend: bool = True):
18+
from arcade.texture_atlas.atlas_default import DefaultTextureAtlas
19+
20+
self.window = get_window()
21+
self.pixelated = pixelated
22+
self.blend = blend
23+
self.atlas = DefaultTextureAtlas(self.window.get_framebuffer_size(), border=0)
24+
self.texture = Texture.create_empty(
25+
"start_finish_render_texture", size=self.window.get_framebuffer_size()
26+
)
27+
self.atlas.add(self.texture)
28+
self._generator_func = None
29+
self.completed = False
30+
31+
def begin(self):
32+
"""Enable rendering into the buffer"""
33+
self.generator_func = self.atlas.render_into(self.texture)
34+
fbo = self.generator_func.__enter__()
35+
fbo.clear(color=self.window.background_color)
36+
37+
if self.blend:
38+
self.window.ctx.enable(self.window.ctx.BLEND)
39+
40+
def end(self):
41+
"""Switch back to rendering into the window"""
42+
self.generator_func.__exit__(None, None, None)
43+
self.completed = True
44+
45+
def draw(self):
46+
"""Draw the buffer to the screen"""
47+
# Stretch the texture to the window size with black bars if needed
48+
w, h = self.window.get_size()
49+
min_factor = min(w / self.texture.width, h / self.texture.height)
50+
region = LBWH(0, 0, self.texture.width, self.texture.height).scale(
51+
min_factor, anchor=AnchorPoint.BOTTOM_LEFT
52+
)
53+
region = region.move(dx=(w - region.width) / 2, dy=(h - region.height) / 2)
54+
55+
draw_texture_rect(
56+
self.texture, region, pixelated=self.pixelated, blend=False, atlas=self.atlas
57+
)

arcade/window_commands.py

Lines changed: 36 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -179,31 +179,51 @@ def exit() -> None:
179179
pyglet.app.exit()
180180

181181

182-
def start_render() -> None:
182+
def start_render(pixelated=False, blend=True) -> None:
183183
"""
184-
Clears the window.
184+
Start recording drawing functions into an offscreen buffer.
185+
Call :py:func:`arcade.finish_render` to stop recording. The
186+
start_render/finish_render calls can only be called once.
185187
186-
More practical alternatives to this function is
187-
:py:meth:`arcade.Window.clear`
188-
or :py:meth:`arcade.View.clear`.
189-
"""
190-
get_window().clear()
188+
When running arcade this buffer will be presented to the screen.
191189
190+
A few configuration options are available in this function.
192191
193-
def finish_render():
192+
:param pixelated: If True, the buffer will be be pixelated when resized. Otherwise, it will be smooth.
193+
:param blend: If alpha blending
194194
"""
195-
Swap buffers and displays what has been drawn.
195+
from arcade.start_finish_data import StartFinishRenderData
196+
197+
window = get_window()
198+
if window._start_finish_render_data is not None:
199+
raise RuntimeError(
200+
(
201+
"start_render() can only be called once during the application's lifetime "
202+
"and should only be used when calling draw functions at module level in "
203+
"a simple script to produce a static image. If you are seeing this error "
204+
"you likely intended to call clear() instead."
205+
)
206+
)
196207

197-
.. Warning::
208+
window._start_finish_render_data = StartFinishRenderData(pixelated=pixelated, blend=blend)
209+
window._start_finish_render_data.begin()
198210

199-
If you are extending the :py:class:`~arcade.Window` class, this function
200-
should not be called. The event loop will automatically swap the window
201-
framebuffer for you after ``on_draw``.
202211

212+
def finish_render() -> None:
203213
"""
204-
get_window().static_display = True
205-
get_window().flip_count = 0
206-
get_window().flip()
214+
Stop recording drawing functions into an offscreen buffer.
215+
:py:func:`arcade.start_render` should be called before this function.
216+
217+
:py:func:`arcade.run` can be called after this function to present the buffer.
218+
"""
219+
window = get_window()
220+
if window._start_finish_render_data is None:
221+
raise RuntimeError("finish_render() was called without a matching start_render() call.")
222+
223+
if window._start_finish_render_data.completed:
224+
raise RuntimeError("finish_render() was called more than once.")
225+
226+
window._start_finish_render_data.end()
207227

208228

209229
def set_background_color(color: RGBA255) -> None:

tests/conftest.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ def prepare_window(window: arcade.Window):
4848
# ctx._atlas = None # Clear the global atlas
4949
# default_texture_cache.flush() # Clear the global/default texture cache
5050
arcade.SpriteList.DEFAULT_TEXTURE_FILTER = gl.LINEAR, gl.LINEAR
51+
window._start_finish_render_data = None
5152
window.hide_view() # Disable views if any is active
5253
window.dispatch_pending_events()
5354
try:
@@ -170,6 +171,14 @@ def background_color(self, color):
170171
def current_camera(self):
171172
return self.window.current_camera
172173

174+
@property
175+
def _start_finish_render_data(self):
176+
return self.window._start_finish_render_data
177+
178+
@_start_finish_render_data.setter
179+
def _start_finish_render_data(self, data):
180+
self.window._start_finish_render_data = data
181+
173182
@current_camera.setter
174183
def current_camera(self, new_camera):
175184
self.window.current_camera = new_camera
@@ -200,6 +209,9 @@ def get_size(self):
200209
def set_size(self, width, height):
201210
self.window.set_size(width, height)
202211

212+
def get_framebuffer_size(self):
213+
return self.window.get_framebuffer_size()
214+
203215
def get_pixel_ratio(self):
204216
return self.window.get_pixel_ratio()
205217

@@ -283,7 +295,9 @@ def window_proxy():
283295

284296
_open_window = arcade.open_window
285297
def open_window(*args, **kwargs):
286-
return create_window(*args, **kwargs)
298+
window = create_window(*args, **kwargs)
299+
prepare_window(window)
300+
return window
287301
arcade.open_window = open_window
288302

289303
yield None

0 commit comments

Comments
 (0)