Skip to content
Merged
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
239 changes: 239 additions & 0 deletions arcade/experimental/depth_of_field.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,239 @@
"""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 contextlib import contextmanager

from pyglet.graphics import Batch

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


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,
clear_color: RGBA255 = (155, 155, 155, 255)
):
self._geo = geometry.quad_2d_fs()
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(
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
)
)

# 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,
kernel_size=10,
sigma=2.0,
multiplier=2.0,
step=4
)
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._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=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(self._clear_color)
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)


class App(Window):
"""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.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):
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),
color,
uniform(0, 360)
)
s.depth = depth
self.sprites.append(s)

self.dof = DepthOfField()

def on_update(self, delta_time: float):
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.sprites.draw(pixelated=True)

# Draw the blurred frame buffer and then the focus display
self.use()
self.dof.render()
self._batch.draw()


if __name__ == '__main__':
App().run()