From aa6eb4411aaa187d8515533d22ae684d3c0bebf6 Mon Sep 17 00:00:00 2001 From: DragonMoffon Date: Fri, 15 Mar 2024 21:02:09 +1300 Subject: [PATCH 1/4] created a depth of field example --- arcade/experimental/depth_of_field.py | 167 ++++++++++++++++++++++++++ 1 file changed, 167 insertions(+) create mode 100644 arcade/experimental/depth_of_field.py diff --git a/arcade/experimental/depth_of_field.py b/arcade/experimental/depth_of_field.py new file mode 100644 index 000000000..8cb2c6f0f --- /dev/null +++ b/arcade/experimental/depth_of_field.py @@ -0,0 +1,167 @@ +from typing import Tuple, Optional +from contextlib import contextmanager +from math import cos, pi + +from PIL import Image + +from arcade.gl import geometry, NEAREST, Texture2D +from arcade.experimental.postprocessing import GaussianBlur +from arcade import get_window, draw_text + + +class DepthOfField: + + def __init__(self, size: Optional[Tuple[int, int]] = None): + self._geo = geometry.quad_2d_fs() + self._win = get_window() + size = size or self._win.size + + self.stale = True + + self._render_target = self._win.ctx.framebuffer( + color_attachments=[ + self._win.ctx.texture( + size, + components=4, + filter=(NEAREST, NEAREST), + wrap_x=self._win.ctx.REPEAT, + wrap_y=self._win.ctx.REPEAT + ), + ], + depth_attachment=self._win.ctx.depth_texture( + size + ) + ) + + self._blur_process = GaussianBlur( + size, + 10, + 2.0, + 2.0, + step=4 + ) + + self._blurred = None + + self._blur_target = self._win.ctx.framebuffer( + color_attachments=[ + self._win.ctx.texture( + size, + components=4, + filter=(NEAREST, NEAREST), + wrap_x=self._win.ctx.REPEAT, + wrap_y=self._win.ctx.REPEAT + ) + ] + ) + + self._render_program = self._win.ctx.program( + vertex_shader=( + "#version 330\n" + "\n" + "in vec2 in_vert;\n" + "in vec2 in_uv;\n" + "\n" + "out vec2 out_uv;\n" + "\n" + "void main(){\n" + " gl_Position = vec4(in_vert, 0.0, 1.0);\n" + " out_uv = in_uv;\n" + "}\n" + ), + fragment_shader=( + "#version 330\n" + "\n" + "uniform sampler2D texture_0;\n" + "uniform sampler2D texture_1;\n" + "uniform sampler2D depth_0;\n" + "\n" + "uniform float focus_depth;\n" + "\n" + "in vec2 out_uv;\n" + "\n" + "out vec4 frag_colour;\n" + "\n" + "void main() {\n" + " float depth_val = texture(depth_0, out_uv).x;\n" + " float depth_adjusted = min(1.0, 2.0 * abs(depth_val - focus_depth));\n" + " vec4 crisp_tex = texture(texture_0, out_uv);\n" + " vec3 blur_tex = texture(texture_1, out_uv).rgb;\n" + " frag_colour = mix(crisp_tex, vec4(blur_tex, crisp_tex.a), depth_adjusted);\n" + " //if (depth_adjusted < 0.1){frag_colour = vec4(1.0, 0.0, 0.0, 1.0);}\n" + "}\n" + ) + ) + self._render_program['texture_0'] = 0 + self._render_program['texture_1'] = 1 + self._render_program['depth_0'] = 2 + + @contextmanager + def draw_into(self, color: Optional = None): + self.stale = True + previous_fbo = self._win.ctx.active_framebuffer + try: + self._win.ctx.enable(self._win.ctx.DEPTH_TEST) + self._render_target.clear(color or (155.0, 155.0, 155.0, 255.0)) + self._render_target.use() + yield self._render_target + finally: + self._win.ctx.disable(self._win.ctx.DEPTH_TEST) + previous_fbo.use() + + def process(self): + self._blurred = self._blur_process.render(self._render_target.color_attachments[0]) + self._win.use() + + self.stale = False + + def render(self): + if self.stale: + self.process() + + self._render_target.color_attachments[0].use(0) + self._blurred.use(1) + self._render_target.depth_attachment.use(2) + self._geo.render(self._render_program) + + +if __name__ == '__main__': + from random import uniform, randint + from arcade import Window, SpriteSolidColor, SpriteList + win = Window() + t = 0.0 + l = SpriteList() + for _ in range(100): + d = uniform(-100, 100) + c = int(255 * (d+100)/200) + s = SpriteSolidColor( + randint(100, 200), randint(100, 200), + uniform(20, win.width-20), uniform(20, win.height-20), + (c, c, c, 255), + uniform(0, 360) + ) + s.depth = d + l.append(s) + dof = DepthOfField() + + def update(delta_time: float): + global t + t += delta_time + dof._render_program["focus_depth"] = round(16 * (cos(pi * 0.1 * t)*0.5 + 0.5)) / 16 + + win.on_update = update + + def draw(): + win.clear() + with dof.draw_into(): + l.draw(pixelated=True) + win.use() + + dof.render() + draw_text(str(dof._render_program["focus_depth"]), win.width/2, win.height/2, (255, 0, 0, 255), align="center") + + + win.on_draw = draw + + win.run() + From 38f863c28c0a8fed2084a9c3b5b9fb2ec208a8fe Mon Sep 17 00:00:00 2001 From: DragonMoffon Date: Fri, 15 Mar 2024 22:40:01 +1300 Subject: [PATCH 2/4] linting fix --- arcade/experimental/depth_of_field.py | 75 +++++++++++++-------------- 1 file changed, 36 insertions(+), 39 deletions(-) diff --git a/arcade/experimental/depth_of_field.py b/arcade/experimental/depth_of_field.py index 8cb2c6f0f..7ce7e8b44 100644 --- a/arcade/experimental/depth_of_field.py +++ b/arcade/experimental/depth_of_field.py @@ -1,10 +1,10 @@ from typing import Tuple, Optional from contextlib import contextmanager from math import cos, pi +from random import uniform, randint +from arcade import Window, SpriteSolidColor, SpriteList -from PIL import Image - -from arcade.gl import geometry, NEAREST, Texture2D +from arcade.gl import geometry, NEAREST from arcade.experimental.postprocessing import GaussianBlur from arcade import get_window, draw_text @@ -97,12 +97,12 @@ def __init__(self, size: Optional[Tuple[int, int]] = None): self._render_program['depth_0'] = 2 @contextmanager - def draw_into(self, color: Optional = None): + def draw_into(self): self.stale = True previous_fbo = self._win.ctx.active_framebuffer try: self._win.ctx.enable(self._win.ctx.DEPTH_TEST) - self._render_target.clear(color or (155.0, 155.0, 155.0, 255.0)) + self._render_target.clear((155.0, 155.0, 155.0, 255.0)) self._render_target.use() yield self._render_target finally: @@ -125,43 +125,40 @@ def render(self): self._geo.render(self._render_program) -if __name__ == '__main__': - from random import uniform, randint - from arcade import Window, SpriteSolidColor, SpriteList - win = Window() - t = 0.0 - l = SpriteList() - for _ in range(100): - d = uniform(-100, 100) - c = int(255 * (d+100)/200) - s = SpriteSolidColor( - randint(100, 200), randint(100, 200), - uniform(20, win.width-20), uniform(20, win.height-20), - (c, c, c, 255), - uniform(0, 360) - ) - s.depth = d - l.append(s) - dof = DepthOfField() - - def update(delta_time: float): - global t - t += delta_time - dof._render_program["focus_depth"] = round(16 * (cos(pi * 0.1 * t)*0.5 + 0.5)) / 16 - - win.on_update = update +class App(Window): + + def __init__(self): + super().__init__() + self.t = 0.0 + self.l: SpriteList = SpriteList() + for _ in range(100): + d = uniform(-100, 100) + c = int(255 * (d + 100) / 200) + s = SpriteSolidColor( + randint(100, 200), randint(100, 200), + uniform(20, self.width - 20), uniform(20, selfheight - 20), + (c, c, c, 255), + uniform(0, 360) + ) + s.depth = d + self.l.append(s) + self.dof = DepthOfField() - def draw(): - win.clear() - with dof.draw_into(): - l.draw(pixelated=True) - win.use() + def on_update(self, delta_time: float): + self.t += delta_time + self.dof._render_program["focus_depth"] = round(16 * (cos(pi * 0.1 * self.t)*0.5 + 0.5)) / 16 - dof.render() - draw_text(str(dof._render_program["focus_depth"]), win.width/2, win.height/2, (255, 0, 0, 255), align="center") + def on_draw(self): + self.clear() + with self.dof.draw_into(): + self.l.draw(pixelated=True) + self.use() + self.dof.render() + draw_text(str(self.dof._render_program["focus_depth"]), self.width / 2, self.height / 2, (255, 0, 0, 255), + align="center") - win.on_draw = draw - win.run() +if __name__ == '__main__': + App().run() From 5afc7f4bf890399d1c7f3a3bc94381336d6451b2 Mon Sep 17 00:00:00 2001 From: DragonMoffon Date: Fri, 15 Mar 2024 22:41:01 +1300 Subject: [PATCH 3/4] oops --- arcade/experimental/depth_of_field.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/arcade/experimental/depth_of_field.py b/arcade/experimental/depth_of_field.py index 7ce7e8b44..36c5e455e 100644 --- a/arcade/experimental/depth_of_field.py +++ b/arcade/experimental/depth_of_field.py @@ -136,7 +136,7 @@ def __init__(self): c = int(255 * (d + 100) / 200) s = SpriteSolidColor( randint(100, 200), randint(100, 200), - uniform(20, self.width - 20), uniform(20, selfheight - 20), + uniform(20, self.width - 20), uniform(20, self.height - 20), (c, c, c, 255), uniform(0, 360) ) From f124bb200c473cf4557710eead535f370198702f Mon Sep 17 00:00:00 2001 From: Paul <36696816+pushfoo@users.noreply.github.com> Date: Sun, 17 Mar 2024 03:14:53 -0400 Subject: [PATCH 4/4] Depth-of-field style improvements (#3) * Add top-level docstring with run instructions * Clean up source style for shaders using dedent * Make sprite instantiation more legible * Make clear color cleaner and less hardcoded * Make render program a property * Switch to batch label instead of draw_text * Add annotations and comments for clarity * Clean up imports and spacing * Remove redundant Color creation since we convert internally anyway * Add docstring for DepthOfField * Expand top-level docstring * Spacing + initial label value fix * Make the focus change speed a keyword argument of window + explain it in docstrings * Fix mypy issue --- arcade/experimental/depth_of_field.py | 207 ++++++++++++++++++-------- 1 file changed, 141 insertions(+), 66 deletions(-) diff --git a/arcade/experimental/depth_of_field.py b/arcade/experimental/depth_of_field.py index 36c5e455e..a1c5d96f7 100644 --- a/arcade/experimental/depth_of_field.py +++ b/arcade/experimental/depth_of_field.py @@ -1,23 +1,61 @@ -from typing import Tuple, Optional -from contextlib import contextmanager +"""An experimental depth-of-field example. + +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. + +This example works by doing the following for each frame: + +1. Render a depth value for pixel into a buffer +2. Render a gaussian blurred version of the scene +3. For each pixel, use the current depth value to lerp between the + blurred and unblurred versions of the scene. + +This is more expensive than rendering the scene directly, but it's +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 +""" +from typing import Tuple, Optional, cast +from textwrap import dedent from math import cos, pi from random import uniform, randint -from arcade import Window, SpriteSolidColor, SpriteList +from contextlib import contextmanager + +from pyglet.graphics import Batch -from arcade.gl import geometry, NEAREST +from arcade import get_window, Window, SpriteSolidColor, SpriteList, Text +from arcade.color import RED +from arcade.types import Color, RGBA255 + +from arcade.gl import geometry, NEAREST, Program, Texture2D from arcade.experimental.postprocessing import GaussianBlur -from arcade import get_window, draw_text class DepthOfField: + """A depth-of-field effect we can use as a render context manager. + + :param size: The size of the buffers. + :param clear_color: The color which will be used as the background. + """ - def __init__(self, size: Optional[Tuple[int, int]] = None): + def __init__( + self, + size: Optional[Tuple[int, int]] = None, + clear_color: RGBA255 = (155, 155, 155, 255) + ): self._geo = geometry.quad_2d_fs() - self._win = get_window() - size = size or self._win.size + self._win: Window = get_window() + + size = cast(Tuple[int, int], size or self._win.size) + self._clear_color: Color = Color.from_iterable(clear_color) self.stale = True + # Set up our depth buffer to hold per-pixel depth self._render_target = self._win.ctx.framebuffer( color_attachments=[ self._win.ctx.texture( @@ -33,16 +71,16 @@ def __init__(self, size: Optional[Tuple[int, int]] = None): ) ) + # Set up everything we need to perform blur and store results. + # This includes the blur effect, a framebuffer, and an instance + # variable to store the returned texture holding blur results. self._blur_process = GaussianBlur( size, - 10, - 2.0, - 2.0, + kernel_size=10, + sigma=2.0, + multiplier=2.0, step=4 ) - - self._blurred = None - self._blur_target = self._win.ctx.framebuffer( color_attachments=[ self._win.ctx.texture( @@ -54,55 +92,64 @@ def __init__(self, size: Optional[Tuple[int, int]] = None): ) ] ) + self._blurred: Optional[Texture2D] = None + # To keep this example in one file, we use strings for our + # our shaders. You may want to use pathlib.Path.read_text in + # your own code instead. self._render_program = self._win.ctx.program( - vertex_shader=( - "#version 330\n" - "\n" - "in vec2 in_vert;\n" - "in vec2 in_uv;\n" - "\n" - "out vec2 out_uv;\n" - "\n" - "void main(){\n" - " gl_Position = vec4(in_vert, 0.0, 1.0);\n" - " out_uv = in_uv;\n" - "}\n" - ), - fragment_shader=( - "#version 330\n" - "\n" - "uniform sampler2D texture_0;\n" - "uniform sampler2D texture_1;\n" - "uniform sampler2D depth_0;\n" - "\n" - "uniform float focus_depth;\n" - "\n" - "in vec2 out_uv;\n" - "\n" - "out vec4 frag_colour;\n" - "\n" - "void main() {\n" - " float depth_val = texture(depth_0, out_uv).x;\n" - " float depth_adjusted = min(1.0, 2.0 * abs(depth_val - focus_depth));\n" - " vec4 crisp_tex = texture(texture_0, out_uv);\n" - " vec3 blur_tex = texture(texture_1, out_uv).rgb;\n" - " frag_colour = mix(crisp_tex, vec4(blur_tex, crisp_tex.a), depth_adjusted);\n" - " //if (depth_adjusted < 0.1){frag_colour = vec4(1.0, 0.0, 0.0, 1.0);}\n" - "}\n" - ) + vertex_shader=dedent( + """#version 330 + + in vec2 in_vert; + in vec2 in_uv; + + out vec2 out_uv; + + void main(){ + gl_Position = vec4(in_vert, 0.0, 1.0); + out_uv = in_uv; + }"""), + fragment_shader=dedent( + """#version 330 + + uniform sampler2D texture_0; + uniform sampler2D texture_1; + uniform sampler2D depth_0; + + uniform float focus_depth; + + in vec2 out_uv; + + out vec4 frag_colour; + + void main() { + float depth_val = texture(depth_0, out_uv).x; + float depth_adjusted = min(1.0, 2.0 * abs(depth_val - focus_depth)); + vec4 crisp_tex = texture(texture_0, out_uv); + vec3 blur_tex = texture(texture_1, out_uv).rgb; + frag_colour = mix(crisp_tex, vec4(blur_tex, crisp_tex.a), depth_adjusted); + //if (depth_adjusted < 0.1){frag_colour = vec4(1.0, 0.0, 0.0, 1.0);} + }""") ) + + # Set the buffers the shader program will use self._render_program['texture_0'] = 0 self._render_program['texture_1'] = 1 self._render_program['depth_0'] = 2 + @property + def render_program(self) -> Program: + """The compiled shader for this effect.""" + return self._render_program + @contextmanager def draw_into(self): self.stale = True previous_fbo = self._win.ctx.active_framebuffer try: self._win.ctx.enable(self._win.ctx.DEPTH_TEST) - self._render_target.clear((155.0, 155.0, 155.0, 255.0)) + self._render_target.clear(self._clear_color) self._render_target.use() yield self._render_target finally: @@ -126,39 +173,67 @@ def render(self): class App(Window): - - def __init__(self): + """Window subclass to hold sprites and rendering helpers. + + :param text_color: The color of the focus indicator. + :param focus_range: The range the focus value will oscillate between. + :param focus_change_speed: How fast the focus bounces back and forth + between the ``-focus_range`` and ``focus_range``. + """ + def __init__( + self, + text_color: RGBA255 = RED, + focus_range: float = 16.0, + focus_change_speed: float = 0.1 + ): super().__init__() - self.t = 0.0 - self.l: SpriteList = SpriteList() + self.time: float = 0.0 + self.sprites: SpriteList = SpriteList() + self._batch = Batch() + self.focus_range: float = focus_range + self.focus_change_speed: float = focus_change_speed + self.indicator_label = Text( + f"Focus depth: {0:.3f} / {focus_range}", + self.width / 2, self.height / 2, + text_color, + align="center", + anchor_x="center", + batch=self._batch + ) + + # Randomize sprite depth, size, and angle, but set color from depth. for _ in range(100): - d = uniform(-100, 100) - c = int(255 * (d + 100) / 200) + depth = uniform(-100, 100) + color = Color.from_gray(int(255 * (depth + 100) / 200)) s = SpriteSolidColor( randint(100, 200), randint(100, 200), uniform(20, self.width - 20), uniform(20, self.height - 20), - (c, c, c, 255), + color, uniform(0, 360) ) - s.depth = d - self.l.append(s) + s.depth = depth + self.sprites.append(s) + self.dof = DepthOfField() def on_update(self, delta_time: float): - self.t += delta_time - self.dof._render_program["focus_depth"] = round(16 * (cos(pi * 0.1 * self.t)*0.5 + 0.5)) / 16 + self.time += delta_time + raw_focus = self.focus_range * (cos(pi * self.focus_change_speed * self.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}" def on_draw(self): self.clear() + + # Render the depth-of-field layer's frame buffer with self.dof.draw_into(): - self.l.draw(pixelated=True) - self.use() + self.sprites.draw(pixelated=True) + # Draw the blurred frame buffer and then the focus display + self.use() self.dof.render() - draw_text(str(self.dof._render_program["focus_depth"]), self.width / 2, self.height / 2, (255, 0, 0, 255), - align="center") + self._batch.draw() if __name__ == '__main__': App().run() -