Reputation: 166
I am using kivy & kivymd and I am trying to achieve an optimal way to apply a gaussian blur to a FitImage that stands as a background image.
Naturally, I tried the EffectWidget, however, this is not optimal considering the fact that I animate it and I want the user to be able to resize the window and according to the docs, the effectwidget recreates the fbo every time there is a change to the widget.
The code I used:
from kivy.lang import Builder
from kivymd.app import MDApp
class MainApp(MDApp):
def __init__(self, **kwargs):
super(MainApp, self).__init__(**kwargs)
self.kv = Builder.load_string('''
#:kivy 2.0.0
#:import ew kivy.uix.effectwidget
EffectWidget:
effects: ew.HorizontalBlurEffect(size=12.0), ew.VerticalBlurEffect(size=12.0)
FitImage:
source: "images/song_img.jpg"
# the rest of my code...
''')
def build(self):
return self.kv
if __name__ == '__main__':
MainApp().run()
So I assume the only appropriate way for me to achieve what I want is to alter the glsl code.
The glsl code for ew.HorizontalBlurEffect:
effect_blur_h = '''
vec4 effect(vec4 color, sampler2D texture, vec2 tex_coords, vec2 coords)
{{
float dt = ({} / 4.0) * 1.0 / resolution.x;
vec4 sum = vec4(0.0);
sum += texture2D(texture, vec2(tex_coords.x - 4.0*dt, tex_coords.y))
* 0.05;
sum += texture2D(texture, vec2(tex_coords.x - 3.0*dt, tex_coords.y))
* 0.09;
sum += texture2D(texture, vec2(tex_coords.x - 2.0*dt, tex_coords.y))
* 0.12;
sum += texture2D(texture, vec2(tex_coords.x - dt, tex_coords.y))
* 0.15;
sum += texture2D(texture, vec2(tex_coords.x, tex_coords.y))
* 0.16;
sum += texture2D(texture, vec2(tex_coords.x + dt, tex_coords.y))
* 0.15;
sum += texture2D(texture, vec2(tex_coords.x + 2.0*dt, tex_coords.y))
* 0.12;
sum += texture2D(texture, vec2(tex_coords.x + 3.0*dt, tex_coords.y))
* 0.09;
sum += texture2D(texture, vec2(tex_coords.x + 4.0*dt, tex_coords.y))
* 0.05;
return vec4(sum.xyz, color.w);
}}
'''
According to the docs, the AdvancedEffectBase could help with such things, however, the problem is that I have no idea how to change these glsl codes in a way to achieve what I want.
I tried using others' glsl codes for applying a gaussian blur effect like these:
Shadertoy's code for gaussian blur effect
and others...
How am I supposed to achieve what I want?
UPDATE: @Tshirtman's answer seems to be the best one so far, however, I personally ran into a problem with it.
I use the blurred image in a separate screen and the image does not seem to follow the transition animation of the screenmanager, instead it just shows up while the other widgets slowly come into place. Is this fixable? Also, is there a way to up the resolution of the blur? It kind of seems low res.
My Code:
from kivy.lang import Builder
from kivy.core.window import Window
from kivy.graphics import RenderContext
from kivymd.utils.fitimage import FitImage
from kivymd.app import MDApp
class BlurredBackgroundImage(FitImage):
def __init__(self, **kwargs):
fs = '''
$HEADER$
uniform vec2 resolution;
void main(void) {
int radius = 4;
vec2 d = float(radius) / resolution;
for (int dx = -radius; dx < radius; dx++)
for (int dy = -radius; dy < radius; dy++)
gl_FragColor += texture2D(texture0, tex_coord0 + vec2(float(dx), float(dy)) * d);
gl_FragColor /= float( 4 * radius * radius);
}
'''
self.canvas = RenderContext()
self.canvas.shader.fs = fs
super(BlurredBackgroundImage, self).__init__(**kwargs)
def on_size(self, *args):
self.canvas['projection_mat'] = Window.render_context['projection_mat']
self.canvas['modelview_mat'] = Window.render_context['modelview_mat']
self.canvas['resolution'] = list(map(float, self.size))
print("size changed")
# tried updating the shader whenever the position changes but still no improvements
'''def on_pos(self, *args):
self.canvas['projection_mat'] = Window.render_context['projection_mat']
self.canvas['modelview_mat'] = Window.render_context['modelview_mat']
self.canvas['resolution'] = list(map(float, self.size))
print("pos changed")'''
class MainApp(MDApp):
def __init__(self, **kwargs):
super(MainApp, self).__init__(**kwargs)
self.kv = Builder.load_string('''
ScreenManager:
Screen:
name: "main-menu"
Button:
text: "Go to next Page!"
on_release:
root.transition.direction = "down"
root.current = "song-view"
Screen:
name: "song-view"
RelativeLayout:
BlurredBackgroundImage:
source: "images/song_img.jpg"
Button:
text: "Return to main menu!"
size_hint: .25, .25
pos_hint: {"center_x": .5, "center_y": .5}
on_release:
root.transition.direction = "up"
root.current = "main-menu"
''')
def build(self):
return self.kv
if __name__ == '__main__':
MainApp().run()
Upvotes: 3
Views: 1418
Reputation: 5947
If you don’t want to use an FBO, I think you should not use EffectWidget, but rater directly create your shader on a RenderContext, and bind the texture you want to use as a background to that, so the shader can lookup pixels in it
There is a multitexture example in the repository we can use as a starting point
I would think something like this (untested).
from kivy.clock import Clock
from kivy.app import App
from kivy.uix.widget import Widget
from kivy.lang import Builder
from kivy.core.window import Window
from kivy.graphics import RenderContext
fs_multitexture = '''
$HEADER$
uniform vec2 resolution;
void main(void) {
int radius = 4;
vec2 d = float(radius) / resolution;
for (int dx = -radius; dx < radius; dx++)
for (int dy = -radius; dy < radius; dy++)
gl_FragColor += texture2D(texture0, tex_coord0 + vec2(float(dx), float(dy)) * d);
gl_FragColor /= float( 4 * radius * radius);
}
'''
kv = """
<BackgroundBluredImage>:
canvas:
Color:
Rectangle:
# the shader will apply where the widget draws, so we need a rectangle of the appropriate size/pos
pos: self.pos
size: self.size
# binding the texture to it so we can look it up in the shader and do our thing
source: "Coast.jpg"
FloatLayout:
BackgroundBluredImage:
"""
class BackgroundBluredImage(Widget):
def __init__(self, **kwargs):
self.canvas = RenderContext()
self.canvas.shader.fs = fs_multitexture
super().__init__(**kwargs)
# We'll update our glsl variables in a clock to run every frame, but you could bind it to relevant events only, like size and pos
Clock.schedule_interval(self.update_glsl, 0)
def update_glsl(self, *largs):
# This is needed for the default vertex shader.
self.canvas['projection_mat'] = Window.render_context['projection_mat']
self.canvas['modelview_mat'] = Window.render_context['modelview_mat']
self.canvas['resolution'] = list(map(float, self.size))
class BluredBackgroundApp(App):
def build(self):
return Builder.load_string(kv)
if __name__ == '__main__':
BluredBackgroundApp().run()
Upvotes: 2