From db56261a468d7bf554e44fb9a7f2216cdbfb0540 Mon Sep 17 00:00:00 2001 From: Alexander Taylor Date: Wed, 19 Mar 2014 00:07:00 +0000 Subject: [PATCH] Changed EffectWidget to use effect classes --- examples/widgets/effectwidget.py | 217 ++++++++++++++----------------- kivy/uix/effectwidget.py | 160 +++++++++++++++++++---- 2 files changed, 232 insertions(+), 145 deletions(-) diff --git a/examples/widgets/effectwidget.py b/examples/widgets/effectwidget.py index ded4577fb..a5203d67b 100644 --- a/examples/widgets/effectwidget.py +++ b/examples/widgets/effectwidget.py @@ -15,16 +15,34 @@ from kivy.uix.boxlayout import BoxLayout from kivy.lang import Builder from kivy.properties import ObjectProperty -from kivy.uix.effectwidget import (effect_monochrome, - effect_red, - effect_blue, - effect_green, - effect_invert, - effect_mix, - effect_flash, - effect_blur_h, - effect_blur_v, - effect_plasma) +# from kivy.uix.effectwidget import (effect_monochrome, +# effect_red, +# effect_blue, +# effect_green, +# effect_invert, +# effect_mix, +# effect_blur_h, +# effect_blur_v, +# effect_postprocessing, +# effect_pixelate, +# effect_waterpaint, +# effect_fxaa, +# effect_plasma) + +from kivy.uix.effectwidget import (MonochromeEffect, + InvertEffect, + ScanlinesEffect, + ChannelMixEffect, + ScanlinesEffect, + FXAAEffect, + PixelateEffect, + HorizontalBlurEffect, + VerticalBlurEffect) + + + +class ComparisonWidget(EffectWidget): + pass class EffectSpinner(Spinner): pass @@ -33,30 +51,28 @@ class SpinnerRow(BoxLayout): effectwidget = ObjectProperty() def update_effectwidget(self, *args): effects = [] - for child in self.children: + for child in self.children[::-1]: text = child.text if text == 'none': pass + if text == 'fxaa': + effects.append(FXAAEffect()) if text == 'monochrome': - effects.append(effect_monochrome) - if text == 'red': - effects.append(effect_red) - if text == 'blue': - effects.append(effect_blue) - if text == 'green': - effects.append(effect_green) + effects.append(MonochromeEffect()) if text == 'invert': - effects.append(effect_invert) + effects.append(InvertEffect()) if text == 'mix': - effects.append(effect_mix) + effects.append(ChannelMixEffect()) if text == 'flash': - effects.append(effect_flash) + effects.append(FlashEffect()) if text == 'blur_h': - effects.append(effect_blur_h) + effects.append(HorizontalBlurEffect()) if text == 'blur_v': - effects.append(effect_blur_v) - if text == 'plasma': - effects.append(effect_plasma) + effects.append(VerticalBlurEffect()) + if text == 'postprocessing': + effects.append(ScanlinesEffect()) + if text == 'pixelate': + effects.append(PixelateEffect()) if self.effectwidget: self.effectwidget.effects = effects @@ -65,103 +81,62 @@ example = Builder.load_string(''' BoxLayout: orientation: 'vertical' BoxLayout: - EffectWidget: + ComparisonWidget: id: effect1 - Widget: - canvas: - Color: - rgba: 1, 0, 0, 1 - Ellipse: - pos: Vector(self.pos) + 0.5*Vector(self.size) - size: 0.4*Vector(self.size) - Color: - rgba: 0, 1, 0.3, 1 - Ellipse: - pos: Vector(self.pos) + 0.1*Vector(self.size) - size: 0.6*Vector(self.size) - Color: - rgba: 0.5, 0.5, 0.2, 1 - Ellipse: - pos: Vector(self.pos) + Vector([0, 0.6])*Vector(self.size) - size: 0.4*Vector(self.size) - Color: - rgba: 1, 0.8, 0.1, 1 - Ellipse: - pos: Vector(self.pos) + Vector([0.5, 0])*Vector(self.size) - size: 0.4*Vector(self.size) - Color: - rgba: 0, 0, 0.8, 1 - Line: - points: - [self.x, self.y, - self.x + self.width, self.y + 0.3*self.height, - self.x + 0.2*self.width, self.y + 0.1*self.height, - self.x + 0.85*self.width, self.y + 0.72*self.height, - self.x + 0.31*self.width, self.y + 0.6*self.height, - self.x, self.top] - width: 1 - Color: - rgba: 0, 0.9, 0.1, 1 - Line: - points: - [self.x + self.width, self.y + self.height, - self.x + 0.35*self.width, self.y + 0.6*self.height, - self.x + 0.7*self.width, self.y + 0.15*self.height, - self.x + 0.2*self.width, self.y + 0.22*self.height, - self.x + 0.3*self.width, self.y + 0.92*self.height] - width: 2 - EffectWidget: + ComparisonWidget: id: effect2 - Widget: - canvas: - Color: - rgba: 1, 0, 0, 1 - Ellipse: - pos: Vector(self.pos) + 0.5*Vector(self.size) - size: 0.4*Vector(self.size) - Color: - rgba: 0, 1, 0.3, 1 - Ellipse: - pos: Vector(self.pos) + 0.1*Vector(self.size) - size: 0.6*Vector(self.size) - Color: - rgba: 0.5, 0.5, 0.2, 1 - Ellipse: - pos: Vector(self.pos) + Vector([0, 0.6])*Vector(self.size) - size: 0.4*Vector(self.size) - Color: - rgba: 1, 0.8, 0.1, 1 - Ellipse: - pos: Vector(self.pos) + Vector([0.5, 0])*Vector(self.size) - size: 0.4*Vector(self.size) - Color: - rgba: 0, 0, 0.8, 1 - Line: - points: - [self.x, self.y, - self.x + self.width, self.y + 0.3*self.height, - self.x + 0.2*self.width, self.y + 0.1*self.height, - self.x + 0.85*self.width, self.y + 0.72*self.height, - self.x + 0.31*self.width, self.y + 0.6*self.height, - self.x, self.top] - width: 1 - Color: - rgba: 0, 0.9, 0.1, 1 - Line: - points: - [self.x + self.width, self.y + self.height, - self.x + 0.35*self.width, self.y + 0.6*self.height, - self.x + 0.7*self.width, self.y + 0.15*self.height, - self.x + 0.2*self.width, self.y + 0.22*self.height, - self.x + 0.3*self.width, self.y + 0.92*self.height] - width: 2 - SpinnerRow: effectwidget: effect1 - text: 'bg effects' + text: 'left effects' SpinnerRow: effectwidget: effect2 - text: 'scatter effects' + text: 'right effects' + + +: + Widget: + canvas: + Color: + rgba: 1, 0, 0, 1 + Ellipse: + pos: Vector(self.pos) + 0.5*Vector(self.size) + size: 0.4*Vector(self.size) + Color: + rgba: 0, 1, 0.3, 1 + Ellipse: + pos: Vector(self.pos) + 0.1*Vector(self.size) + size: 0.6*Vector(self.size) + Color: + rgba: 0.5, 0.3, 0.8, 1 + Ellipse: + pos: Vector(self.pos) + Vector([0, 0.6])*Vector(self.size) + size: 0.4*Vector(self.size) + Color: + rgba: 1, 0.8, 0.1, 1 + Ellipse: + pos: Vector(self.pos) + Vector([0.5, 0])*Vector(self.size) + size: 0.4*Vector(self.size) + Color: + rgba: 0, 0, 0.8, 1 + Line: + points: + [self.x, self.y, + self.x + self.width, self.y + 0.3*self.height, + self.x + 0.2*self.width, self.y + 0.1*self.height, + self.x + 0.85*self.width, self.y + 0.72*self.height, + self.x + 0.31*self.width, self.y + 0.6*self.height, + self.x, self.top] + width: 1 + Color: + rgba: 0, 0.9, 0.1, 1 + Line: + points: + [self.x + self.width, self.y + self.height, + self.x + 0.35*self.width, self.y + 0.6*self.height, + self.x + 0.7*self.width, self.y + 0.15*self.height, + self.x + 0.2*self.width, self.y + 0.22*self.height, + self.x + 0.3*self.width, self.y + 0.92*self.height] + width: 2 : orientation: 'horizontal' @@ -180,17 +155,17 @@ BoxLayout: : text: 'none' values: - ['none', 'monochrome', - 'invert', 'plasma', 'mix', - 'flash', 'blur_h', 'blur_v'] + ['none', 'fxaa', 'monochrome', + 'invert', 'mix', + 'blur_h', 'blur_v', + 'postprocessing', 'pixelate',] ''') - - class EffectApp(App): def build(self): return example + EffectApp().run() diff --git a/kivy/uix/effectwidget.py b/kivy/uix/effectwidget.py index fa0564689..ecb0106d0 100644 --- a/kivy/uix/effectwidget.py +++ b/kivy/uix/effectwidget.py @@ -38,10 +38,12 @@ the widget. from kivy.clock import Clock from kivy.lang import Builder from kivy.uix.boxlayout import BoxLayout -from kivy.properties import StringProperty, ObjectProperty, ListProperty +from kivy.properties import (StringProperty, ObjectProperty, ListProperty, + NumericProperty) from kivy.graphics import (RenderContext, Fbo, Color, Rectangle, Translate, PushMatrix, PopMatrix, ClearColor, ClearBuffers) +from kivy.event import EventDispatcher from kivy.base import EventLoop Builder.load_string(''' @@ -133,22 +135,15 @@ vec4 effect(vec4 color, sampler2D texture, vec2 tex_coords, vec2 coords) effect_mix = ''' vec4 effect(vec4 color, sampler2D texture, vec2 tex_coords, vec2 coords) -{ - return vec4(color.z, color.x, color.y, 1.0); -} -''' - -effect_flash = ''' -vec4 effect(vec4 color, sampler2D texture, vec2 tex_coords, vec2 coords) -{ - return color * abs(sin(time)); -} +{{ + return vec4(color.{}, color.{}, color.{}, 1.0); +}} ''' effect_blur_h = ''' vec4 effect(vec4 color, sampler2D texture, vec2 tex_coords, vec2 coords) -{ - float dt = 0.5 * 1.0 / resolution.x; +{{ + 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; @@ -160,13 +155,13 @@ vec4 effect(vec4 color, sampler2D texture, vec2 tex_coords, vec2 coords) 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 sum; -} +}} ''' effect_blur_v = ''' vec4 effect(vec4 color, sampler2D texture, vec2 tex_coords, vec2 coords) -{ - float dt = 0.5 * 1.0 / resolution.y; +{{ + float dt = ({} / 4.0) * 1.0 / resolution.x; vec4 sum = vec4(0.0); sum += texture2D(texture, vec2(tex_coords.x, tex_coords.y - 4.0*dt)) * 0.05; sum += texture2D(texture, vec2(tex_coords.x, tex_coords.y - 3.0*dt)) * 0.09; @@ -178,7 +173,7 @@ vec4 effect(vec4 color, sampler2D texture, vec2 tex_coords, vec2 coords) sum += texture2D(texture, vec2(tex_coords.x, tex_coords.y + 3.0*dt)) * 0.09; sum += texture2D(texture, vec2(tex_coords.x, tex_coords.y + 4.0*dt)) * 0.05; return sum; -} +}} ''' effect_postprocessing = ''' @@ -230,13 +225,13 @@ vec4 effect(vec4 color, sampler2D texture, vec2 tex_coords, vec2 coords) effect_pixelate = ''' vec4 effect(vec4 vcolor, sampler2D texture, vec2 texcoord, vec2 pixel_coords) -{ - vec2 pixelSize = 10.0 / resolution; +{{ + vec2 pixelSize = {} / resolution; vec2 xy = floor(texcoord/pixelSize)*pixelSize + pixelSize/2.0; return texture2D(texture, xy); -} +}} ''' effect_waterpaint = ''' @@ -358,6 +353,125 @@ vec4 effect( vec4 color, sampler2D buf0, vec2 texCoords, vec2 coords) ''' +class EffectBase(EventDispatcher): + '''The base class for GLSL effects. It simply returns its input. + + See module documentation for more details. + + ''' + glsl = StringProperty(effect_trivial) + + +class EffectFromFile(EffectBase): + source = StringProperty('') + + def on_source(self, instance, value): + if not value: + return + filename = resource_find(self.source) + if filename is None: + return Logger.error('Error reading file {filename}'. + format(filename=self.source)) + with open(filename) as fileh: + self.glsl = fileh.read() + + +class MonochromeEffect(EffectBase): + '''Returns its input colours in monochrome.''' + def __init__(self, *args, **kwargs): + super(MonochromeEffect, self).__init__(*args, **kwargs) + self.glsl = effect_monochrome + + +class InvertEffect(EffectBase): + '''Inverts the colours in the input.''' + def __init__(self, *args, **kwargs): + super(InvertEffect, self).__init__(*args, **kwargs) + self.glsl = effect_invert + + +class ScanlinesEffect(EffectBase): + '''Adds scanlines to the input.''' + def __init__(self, *args, **kwargs): + super(ScanlinesEffect, self).__init__(*args, **kwargs) + self.glsl = effect_postprocessing + + +class ChannelMixEffect(EffectBase): + '''Mixes the color channels of the input according to the order + property. Channels may be arbitrarily rearranged or repeated.''' + + order = ListProperty([1, 2, 0]) + '''The new sorted order of the rgb channels. Defaults to [1, 2, 0], + corresponding to (g, b, r).''' + + def __init__(self, *args, **kwargs): + super(ChannelMixEffect, self).__init__(*args, **kwargs) + self.do_glsl() + + def on_size(self, *args): + self.do_glsl() + + def do_glsl(self): + letters = [{0: 'x', 1: 'y', 2: 'z'}[i] for i in self.order] + self.glsl = effect_mix.format(*letters) + + +class PixelateEffect(EffectBase): + '''Pixelates the input according to its + :attr:`~PixelateEffect.pixel_size`''' + pixel_size = NumericProperty(10) + + def __init__(self, *args, **kwargs): + super(PixelateEffect, self).__init__(*args, **kwargs) + self.do_glsl() + + def on_pixel_size(self, *args): + self.do_glsl() + + def do_glsl(self): + self.glsl = effect_pixelate.format(float(self.pixel_size)) + + +class HorizontalBlurEffect(EffectBase): + '''Blurs the input horizontally, with the width given by + :attr:`~HorizontalBlurEffect.size`.''' + size = NumericProperty(4.0) + + def __init__(self, *args, **kwargs): + super(HorizontalBlurEffect, self).__init__(*args, **kwargs) + self.do_glsl() + + def on_size(self, *args): + self.do_glsl() + + def do_glsl(self): + self.glsl = effect_blur_h.format(float(self.size)) + + +class VerticalBlurEffect(EffectBase): + '''Blurs the input vertically, with the width given by + :attr:`~VerticalBlurEffect.width`.''' + size = NumericProperty(4.0) + + def __init__(self, *args, **kwargs): + super(VerticalBlurEffect, self).__init__(*args, **kwargs) + self.do_glsl() + + def on_size(self, *args): + self.do_glsl() + + def do_glsl(self): + self.glsl = effect_blur_h.format(float(self.size)) + + +class FXAAEffect(EffectBase): + '''Applies very simple antialiasing via fxaa.''' + def __init__(self, *args, **kwargs): + super(FXAAEffect, self).__init__(*args, **kwargs) + self.glsl = effect_fxaa + + class EffectFbo(Fbo): def __init__(self, *args, **kwargs): super(EffectFbo, self).__init__(*args, **kwargs) @@ -472,8 +586,9 @@ class EffectWidget(BoxLayout): fbo = self.fbo_list[i] fbo.texture_rectangle.texture = self.fbo_list[i - 1].texture + # Build effect shaders for effect, fbo in zip(self.effects, self.fbo_list): - fbo.set_fs(shader_header + shader_uniforms + effect + + fbo.set_fs(shader_header + shader_uniforms + effect.glsl + shader_footer_effect) self.fbo_list[0].texture_rectangle.texture = self.fbo.texture @@ -509,6 +624,3 @@ class EffectWidget(BoxLayout): super(EffectWidget, self).clear_widgets(children) self.canvas = c - -class BlurEffectWidget(EffectWidget): - effects = ListProperty([effect_blur_h, effect_blur_v])