Depth of Field Blur

depth_of_field.py
1"""An experimental depth-of-field example.
2
3It uses the depth attribute of along with blurring and shaders to
4roughly approximate depth-based blur effects. The focus bounces
5back forth automatically between a maximum and minimum depth value
6based on time. Change the speed and focus via either the constants
7at the top of the file or the arguments passed to it at the bottom of
8the file.
9
10This example works by doing the following for each frame:
11
121. Render a depth value for pixel into a buffer
132. Render a gaussian blurred version of the scene
143. For each pixel, use the current depth value to lerp between the
15 blurred and un-blurred versions of the scene.
16
17This is more expensive than rendering the scene directly, but it's
18both easier and more performant than more accurate blur approaches.
19
20If Python and Arcade are installed, this example can be run from the command line with:
21python -m arcade.examples.depth_of_field
22"""
23
24from __future__ import annotations
25
26from contextlib import contextmanager
27from math import cos, pi
28from random import randint, uniform
29from textwrap import dedent
30from typing import cast
31
32from pyglet.graphics import Batch
33
34import arcade
35from arcade import get_window, SpriteList, SpriteSolidColor, Text, Window, View
36from arcade.camera.data_types import DEFAULT_NEAR_ORTHO, DEFAULT_FAR
37from arcade.color import RED
38from arcade.experimental.postprocessing import GaussianBlur
39from arcade.gl import NEAREST, Program, Texture2D, geometry
40from arcade.types import RGBA255, Color
41
42WINDOW_TITLE = "Depth of Field Example"
43
44WINDOW_WIDTH = 1280
45WINDOW_HEIGHT = 720
46BACKGROUND_GRAY = Color(155, 155, 155, 255)
47
48
49class DepthOfField:
50 """A depth-of-field effect we can use as a render context manager.
51
52 Args:
53 size:
54 The size of the buffers.
55 clear_color:
56 The color which will be used as the background.
57 """
58
59 def __init__(
60 self,
61 size: tuple[int, int] | None = None,
62 clear_color: RGBA255 = BACKGROUND_GRAY
63 ):
64 self._geo = geometry.quad_2d_fs()
65 self._win: Window = get_window()
66
67 size = cast(tuple[int, int], size or self._win.size)
68 self._clear_color: Color = Color.from_iterable(clear_color)
69
70 self.stale = True
71
72 # Set up our depth buffer to hold per-pixel depth
73 self._render_target = self._win.ctx.framebuffer(
74 color_attachments=[
75 self._win.ctx.texture(
76 size,
77 components=4,
78 filter=(NEAREST, NEAREST),
79 wrap_x=self._win.ctx.REPEAT,
80 wrap_y=self._win.ctx.REPEAT,
81 ),
82 ],
83 depth_attachment=self._win.ctx.depth_texture(size),
84 )
85
86 # Set up everything we need to perform blur and store results.
87 # This includes the blur effect, a framebuffer, and an instance
88 # variable to store the returned texture holding blur results.
89 self._blur_process = GaussianBlur(size, kernel_size=10, sigma=2.0, multiplier=2.0, step=4)
90 self._blur_target = self._win.ctx.framebuffer(
91 color_attachments=[
92 self._win.ctx.texture(
93 size,
94 components=4,
95 filter=(NEAREST, NEAREST),
96 wrap_x=self._win.ctx.REPEAT,
97 wrap_y=self._win.ctx.REPEAT,
98 )
99 ]
100 )
101 self._blurred: Texture2D | None = None
102
103 # To keep this example in one file, we use strings for our
104 # our shaders. You may want to use pathlib.Path.read_text in
105 # your own code instead.
106 self._render_program = self._win.ctx.program(
107 vertex_shader=dedent(
108 """#version 330
109
110 in vec2 in_vert;
111 in vec2 in_uv;
112
113 out vec2 out_uv;
114
115 void main(){
116 gl_Position = vec4(in_vert, 0.0, 1.0);
117 out_uv = in_uv;
118 }"""
119 ),
120 fragment_shader=dedent(
121 """#version 330
122
123 uniform sampler2D texture_0;
124 uniform sampler2D texture_1;
125 uniform sampler2D depth_0;
126
127 uniform float focus_depth;
128
129 in vec2 out_uv;
130
131 out vec4 frag_colour;
132
133 void main() {
134 float depth_val = texture(depth_0, out_uv).x;
135 float depth_adjusted = min(1.0, 2.0 * abs(depth_val - focus_depth));
136 vec4 crisp_tex = texture(texture_0, out_uv);
137 vec3 blur_tex = texture(texture_1, out_uv).rgb;
138 frag_colour = mix(crisp_tex, vec4(blur_tex, crisp_tex.a), depth_adjusted);
139 //if (depth_adjusted < 0.1){frag_colour = vec4(1.0, 0.0, 0.0, 1.0);}
140 }"""
141 ),
142 )
143
144 # Set the buffers the shader program will use
145 self._render_program["texture_0"] = 0
146 self._render_program["texture_1"] = 1
147 self._render_program["depth_0"] = 2
148
149 @property
150 def render_program(self) -> Program:
151 """The compiled shader for this effect."""
152 return self._render_program
153
154 @contextmanager
155 def draw_into(self):
156 self.stale = True
157 previous_fbo = self._win.ctx.active_framebuffer
158 try:
159 self._win.ctx.enable(self._win.ctx.DEPTH_TEST)
160 self._render_target.clear(color=self._clear_color)
161 self._render_target.use()
162 yield self._render_target
163 finally:
164 self._win.ctx.disable(self._win.ctx.DEPTH_TEST)
165 previous_fbo.use()
166
167 def process(self):
168 self._blurred = self._blur_process.render(self._render_target.color_attachments[0])
169 self._win.use()
170
171 self.stale = False
172
173 def render(self):
174 if self.stale:
175 self.process()
176
177 self._render_target.color_attachments[0].use(0)
178 self._blurred.use(1)
179 self._render_target.depth_attachment.use(2)
180 self._geo.render(self._render_program)
181
182
183class GameView(View):
184 """Window subclass to hold sprites and rendering helpers.
185
186 To keep the code simpler, this example uses a default camera. That means
187 that any sprite outside Arcade's default camera near and far render cutoffs
188 (``-100.0`` to ``100.0``) will not be drawn.
189
190 Args:
191 text_color:
192 The color of the focus indicator.
193 focus_range:
194 The range the focus value will oscillate between.
195 focus_change_speed:
196 How fast the focus bounces back and forth
197 between the ``-focus_range`` and ``focus_range``.
198 min_sprite_depth:
199 The minimum sprite depth we'll generate sprites between
200 max_sprite_depth:
201 The maximum sprite depth we'll generate sprites between.
202 """
203
204 def __init__(
205 self,
206 text_color: RGBA255 = RED,
207 focus_range: float = 16.0,
208 focus_change_speed: float = 0.1,
209 min_sprite_depth: float = DEFAULT_NEAR_ORTHO,
210 max_sprite_depth: float = DEFAULT_FAR
211 ):
212 super().__init__()
213 self.sprites: SpriteList = SpriteList()
214 self._batch = Batch()
215 self.focus_range: float = focus_range
216 self.focus_change_speed: float = focus_change_speed
217 self.indicator_label = Text(
218 f"Focus depth: {0:.3f} / {focus_range}",
219 self.width / 2,
220 self.height / 2,
221 text_color,
222 align="center",
223 anchor_x="center",
224 batch=self._batch,
225 )
226
227 # Randomize sprite depth, size, and angle, but set color from depth.
228 for _ in range(100):
229 depth = uniform(min_sprite_depth, max_sprite_depth)
230 color = Color.from_gray(int(255 * (depth + 100) / 200))
231 s = SpriteSolidColor(
232 randint(100, 200),
233 randint(100, 200),
234 uniform(20, self.width - 20),
235 uniform(20, self.height - 20),
236 color,
237 uniform(0, 360),
238 )
239 s.depth = depth
240 self.sprites.append(s)
241
242 self.dof = DepthOfField()
243
244 def on_update(self, delta_time: float):
245 time = self.window.time
246 raw_focus = self.focus_range * (cos(pi * self.focus_change_speed * time) * 0.5 + 0.5)
247 self.dof.render_program["focus_depth"] = raw_focus / self.focus_range
248 self.indicator_label.value = f"Focus depth: {raw_focus:.3f} / {self.focus_range}"
249
250 def on_draw(self):
251 self.clear()
252
253 # Render the depth-of-field layer's frame buffer
254 with self.dof.draw_into():
255 self.sprites.draw(pixelated=True)
256
257 # Draw the blurred frame buffer and then the focus display
258 window = self.window
259 window.use()
260 self.dof.render()
261 self._batch.draw()
262
263
264def main():
265 # Create a Window to show the view defined above.
266 window = arcade.Window(WINDOW_WIDTH, WINDOW_HEIGHT, WINDOW_TITLE)
267
268 # Create the view
269 app = GameView()
270
271 # Show GameView on screen
272 window.show_view(app)
273
274 # Start the arcade game loop
275 window.run()
276
277
278if __name__ == "__main__":
279 main()