ae.kivy_glsl

add glsl shaders to your kivy widget

this ae namespace portion provides the mixin class ShadersMixin that can be combined with any Kivy widget to display GLSL-/shader-based graphics, gradients and animations.

additionally some built-in shaders are integrated into this portion. more shader examples can be found in the glsl sub-folder of the GlslTester demo application.

usage of ShadersMixin class

to add the ShadersMixin mixin class to a Kivy widget in your python code file you have to specify it in the declaration of your widget class. the following example is extending Kivy’s BoxLayout layout with a shader:

from kivy.uix.boxlayout import BoxLayout
from ae.kivy_glsl import ShadersMixin

class MyBoxLayoutWithShader(BoxLayout, ShadersMixin):

alternatively you can declare a your shader-widget as a new kv rule within a kv file:

<MyBoxLayoutWithShader@BoxLayout+ShadersMixin>

to register a shader, call the ShadersMixin.add_shader() method:

shader_id = widget_instance.add_shader()

by default add_shader() is using the built-in plasma hearts shader, provided by this portion. the next example is instead using the built-in plunge waves shader:

from ae.kivy_glsl import BUILT_IN_SHADERS

widget_instance.add_shader(shader_code=BUILT_IN_SHADERS['plunge_waves'])

alternatively you can use your own shader code by specifying it on call of the method add_shader() either as code block string to the paramref:`~ShadersMixin.add_shader.shader_code argument or as file name to the paramref:`~ShadersMixin.add_shader.shader_file argument.

animation shaders like the built-in plunge waves and plasma hearts shaders need to be refreshed by a timer. the refreshing frequency can be specified via the update_freq parameter. to disable the automatic creation of a timer event pass a zero value to this argument.

Hint

the demo apps ComPartY and GlslTester are disabling the automatic timer event for each shader and using instead a Kivy clock timer to update the frames of all active shaders.

store the return value of add_shader() to stop, pause or to delete the shader later. the following examples demonstrates the deletion of a shader by calling the del_shader() method:

widget_instance.del_shader(shader_id)

Note

you can activate multiple shaders for the same widget. the visibility and intensity of each shader depends then on the implementation of the shader codes and the values of the input arguments (especially alpha and tex_col_mix) for each shader (see parameter glsl_dyn_args).

shader compilation errors and renderer crashes

on some devices (mostly on Android) the shader script does not compile. the success property of Kivy’s shader class is then set to False and an error message like the following gets printed on to the console output:

[ERROR  ] [Shader      ] <fragment> failed to compile (gl:0)
[INFO   ] [Shader      ] fragment shader: <b"0:27(6): error: ....

some common failure reasons are:

  • missing declaration of used uniform input variables.

  • non-input/output variables declared on module level (they should be moved into main or any other function).

in other cases the shader code compiles fine but then the renderer is crashing in the vbo.so library and w/o printing any Python traceback to the console - see also this Kivy issues).

sometimes this crashes can be prevented if the texture of the widget (or of the last shader) gets fetched (w/ the function texture2D(texture0, tex_coord0)) - even if it is not used for the final gl_FragColor output variable.

in some cases additional to fetch the texture, the return value of the texture2D call has to be accessed at least once at the first render cycle.

if an error occurred then the run_state value of the shader id dict will be set to ‘error’ - check the error_message value of the shader id dict for more details on the error.

built-in shaders

the circled alpha shader is a simple gradient pixel shader without any time-based animations.

the plunge waves shader is animated and inspired by the kivy pulse shader example (Danguafer/Silexars, 2010) https://github.com/kivy/kivy/blob/master/examples/shader/shadertree.py.

the animated plasma hearts shader is inspired by the kivy plasma shader example https://github.com/kivy/kivy/blob/master/examples/shader/plasma.py.

Hint

the GlslTester and ComPartY applications are demonstrating the usage of this portion.

the literals of the built-in shaders got converted into constants, following the recommendations given in the accepted answer of this SO question.

Module Attributes

DEFAULT_FPS

default frames-per-second

BUILT_IN_SHADERS

dict of built-in shader code blocks - key specifies shader in shader_code.

RENDER_SHAPES

supported render shapes, specified in render_shape.

SHADER_PARAMETER_MATCHER

match uniform var declarations

HIDDEN_SHADER_PARAMETERS

uniform vars that are not editable by the user

Functions

shader_parameter_alias(shader_code, arg_name)

check for alias for the passed shader argument name in the passed shader code.

shader_parameters(shader_code)

shader arg names of the shader code in the passed shader code.

Classes

ShadersMixin()

shader mixin base class

DEFAULT_FPS = 30.0

default frames-per-second

ShaderIdType

shader internal data and id

alias of Dict[str, Any]

BUILT_IN_SHADERS = {'circled_alpha': 'uniform float alpha;\nuniform float tex_col_mix;\nuniform vec2 center_pos;\nuniform vec2 win_pos;\nuniform vec2 resolution;\nuniform vec4 tint_ink;\n\nvoid main(void)\n{\n  vec2 pix_pos = (frag_modelview_mat * gl_FragCoord).xy;\n  float len = length(pix_pos - center_pos);\n  pix_pos -= win_pos;\n  float dis = len / max(pix_pos.x, max(pix_pos.y, max(resolution.x - pix_pos.x, resolution.y - pix_pos.y)));\n  vec3 col = tint_ink.rgb;\n  if (tex_col_mix != 0.0) {\n    vec4 tex = texture2D(texture0, tex_coord0);\n    col = mix(tex.rgb, col, tex_col_mix);\n  }\n  gl_FragColor = vec4(col, dis * alpha);\n}\n', 'colored_smoke': 'uniform float alpha;\nuniform float tex_col_mix;\nuniform float time;\nuniform vec2 center_pos;\nuniform vec2 mouse;         // density, speed\nuniform vec2 resolution;\nuniform vec4 tint_ink;\n\nconst float ONE = 0.99999999999999;\n\nfloat rand(vec2 n) {\n //This is just a compounded expression to simulate a random number based on a seed given as n\n return fract(cos(dot(n, vec2(12.98982, 4.14141))) * 43758.54531);\n}\n\nfloat noise(vec2 n) {\n //Uses the rand function to generate noise\n const vec2 d = vec2(0.0, ONE);\n vec2 b = floor(n), f = smoothstep(vec2(0.0), vec2(ONE), fract(n));\n return mix(mix(rand(b), rand(b + d.yx), f.x), mix(rand(b + d.xy), rand(b + d.yy), f.x), f.y);\n}\n\nfloat fbm(vec2 n) {\n //fbm stands for "Fractal Brownian Motion" https://en.wikipedia.org/wiki/Fractional_Brownian_motion\n float total = 0.0;\n float amplitude = 1.62;\n for (int i = 0; i < 3; i++) {\n  total += noise(n) * amplitude;\n  n += n;\n  amplitude *= 0.51;\n }\n return total;\n}\n\nvoid main() {\n //This is where our shader comes together\n const vec3 c1 = vec3(126.0/255.0, 0.0/255.0, 96.9/255.0);\n //const vec3 c2 = vec3(173.0/255.0, 0.0/255.0, 161.4/255.0);\n vec3 c2 = tint_ink.rgb;\n const vec3 c3 = vec3(0.21, 0.0, 0.0);\n const vec3 c4 = vec3(165.0/255.0, 129.0/255.0, 214.4/255.0);\n const vec3 c5 = vec3(0.12);\n const vec3 c6 = vec3(0.9);\n vec2 pix_pos = (gl_FragCoord.xy - center_pos) / resolution.xy - vec2(0.0, 0.51);\n //this is how "packed" the smoke is in our area. try changing 15.0 to 2.1, or something else\n vec2 p = pix_pos * (ONE + mouse.x / resolution.x * 15.0);\n //the fbm function takes p as its seed (so each pixel looks different) and time (so it shifts over time)\n float q = fbm(p - time * 0.12);\n float speed = 3.9 * time * mouse.y / resolution.y;\n vec2 r = vec2(fbm(p + q + speed - p.x - p.y), fbm(p + q - speed));\n vec3 col = (mix(c1, c2, fbm(p + r)) + mix(c3, c4, r.y) - mix(c5, c6, r.x)) * cos(pix_pos.y);\n col *= ONE - pix_pos.y;\n if (tex_col_mix != 0.0) {\n  vec4 tex = texture2D(texture0, tex_coord0);\n  col = mix(tex.rgb, col, tex_col_mix);\n }\n gl_FragColor = vec4(col, (alpha + tint_ink.a) / 2.01);\n}\n', 'fire_storm': 'uniform float alpha;\nuniform float contrast;  // speed\nuniform float tex_col_mix;\nuniform float time;\nuniform vec2 center_pos;\nuniform vec2 mouse;  // intensity, granularity\nuniform vec2 resolution;\nuniform vec4 tint_ink;\n\n#define TAU 6.283185307182\n#define MAX_ITER 15\n\nvoid main( void ) {\n float t = time*contrast + 23.01;\n // uv should be the 0-1 uv of texture...\n vec2 xy = (gl_FragCoord.xy - center_pos) / resolution.yy; // - vec2(0.9);\n vec2 uv = vec2(atan(xy.y, xy.x) * 6.99999 / TAU, log(length(xy)) * (0.21 + mouse.y / resolution.y) - time * 0.21);\n vec2 p = mod(uv*TAU, TAU)-250.02;\n vec2 i = vec2(p);\n float c = 8.52;\n float intensity = 0.003 + mouse.x / resolution.x / 333.3;  // = .005;\n\n for (int n = 0; n < MAX_ITER; n++) {\n   float t = t * (1.02 - (3.498 / float(n+1)));\n   i = p + vec2(cos(t - i.x) + sin(t + i.y), sin(t - i.y) + cos(t + i.x));\n   c += 1.0/length(vec2(p.x / (sin(i.x+t)/intensity),p.y / (cos(i.y+t)/intensity)));\n }\n c /= float(MAX_ITER);\n c = 1.272 - pow(c, 6.42);\n vec3 colour = vec3(pow(abs(c), 8.01));\n colour = clamp(colour + tint_ink.rgb, 0.0, 0.999999);\n if (tex_col_mix != 0.0) {\n  vec4 tex = texture2D(texture0, tex_coord0);\n  colour = mix(tex.rgb, colour, tex_col_mix);\n }\n gl_FragColor = vec4(colour, (alpha + tint_ink.a) / 2.00001);\n }\n', 'plasma_hearts': 'uniform float alpha;\nuniform float contrast;\nuniform float tex_col_mix;\nuniform float time;\nuniform vec2 center_pos;\nuniform vec2 win_pos;\nuniform vec2 resolution;\nuniform vec4 tint_ink;\n\nconst float THOUSAND = 963.9;\nconst float HUNDRED = 69.3;\nconst float TEN = 9.9;\nconst float TWO = 1.83;\nconst float ONE = 0.99;\n\nvoid main(void)\n{\n  vec2 pix_pos = (frag_modelview_mat * gl_FragCoord).xy - win_pos;\n  vec2 rel_center = center_pos - win_pos;\n  float x = abs(pix_pos.x - rel_center.x);\n  float y = abs(pix_pos.y - rel_center.y - resolution.y);\n\n  float m1 = x + y + cos(sin(time) * TWO) * HUNDRED + sin(x / HUNDRED) * THOUSAND;\n  float m2 = y / resolution.y;\n  float m3 = x / resolution.x + time * TWO;\n\n  float c1 = abs(sin(m2 + time) / TWO + cos(m3 / TWO - m2 - m3 + time));\n  float c2 = abs(sin(c1 + sin(m1 / THOUSAND + time) + sin(y / HUNDRED + time) + sin((x + y) / HUNDRED) * TWO));\n  float c3 = abs(sin(c2 + cos(m2 + m3 + c2) + cos(m3) + sin(x / THOUSAND)));\n\n  vec4 tex = texture2D(texture0, tex_coord0);\n  float dis = TWO * distance(pix_pos, rel_center) / max(resolution.x, resolution.y);\n  vec4 col = vec4(c1, c2, c3, contrast * (ONE - dis)) * tint_ink * TWO;\n  col = mix(tex, col, tex_col_mix);\n  gl_FragColor = vec4(col.rgb, col.a * sqrt(alpha));\n}\n', 'plunge_waves': 'uniform float alpha;\nuniform float contrast;\nuniform float tex_col_mix;\nuniform float time;\nuniform vec2 center_pos;\nuniform vec2 win_pos;\nuniform vec2 resolution;\nuniform vec4 tint_ink;\n\nconst float TEN = 9.99999;\nconst float TWO = 2.00001;\nconst float ONE = 0.99999;\n\nvoid main(void)\n{\n  vec2 pix_pos = (frag_modelview_mat * gl_FragCoord).xy;\n  float len = length(pix_pos - center_pos);\n  float col_comp = (sin(len / TEN - mod(time, TEN) * TEN) + ONE) / TEN;\n  float dis = len / (TWO * min(resolution.x, resolution.y));\n  vec4 col = tint_ink / vec4(col_comp, col_comp, col_comp, dis / (ONE / TEN + contrast)) / TEN;\n  if (tex_col_mix != 0.0) {\n    vec4 tex = texture2D(texture0, tex_coord0);\n    col = mix(tex, col, tex_col_mix);\n  }\n  gl_FragColor = vec4(col.rgb, col.a * alpha * alpha);\n}\n', 'worm_whole': 'uniform float alpha;\nuniform float contrast;\nuniform float tex_col_mix;\nuniform float time;\nuniform vec2 center_pos;\nuniform vec2 mouse;         // off1, off2\nuniform vec2 resolution;\nuniform vec4 tint_ink;\n\nconst float XIS = 0.6;\nconst float ONE = 0.99999999999;\nconst float TWO = 1.99999999998;\nconst float SIX = 6.0;\n\nvoid main(void){\n vec2 centered_coord = (TWO * (gl_FragCoord.xy - center_pos) - resolution) / resolution.y;\n centered_coord += vec2(resolution.x / resolution.y, ONE);\n centered_coord.y *= dot(centered_coord, centered_coord);\n float dist_from_center = length(centered_coord);\n float dist_from_center_y = length(centered_coord.y);\n float u = SIX / dist_from_center_y + time * SIX;\n float v = (SIX / dist_from_center_y) * centered_coord.x;\n float grid = (ONE - pow(sin(u) + ONE, XIS) + (ONE - pow(sin(v) + ONE, XIS))) * dist_from_center_y * contrast * 6000.0;\n float off1 = sin((fract(time / SIX) + dist_from_center) * SIX) * (XIS + mouse.x / resolution.x);\n float off2 = sin((fract(time / SIX) + dist_from_center_y * TWO) * SIX) * (XIS + mouse.y / resolution.y);\n vec3 col = vec3(grid) * vec3(tint_ink.r * off1, tint_ink.g * off1 * off2 * TWO, tint_ink.b * off2);\n if (tex_col_mix != 0.0) {\n  vec4 tex = texture2D(texture0, tex_coord0);\n  col = mix(tex.rgb, col, tex_col_mix);\n }\n gl_FragColor=vec4(col, alpha);\n}\n'}

dict of built-in shader code blocks - key specifies shader in shader_code.

RENDER_SHAPES = (<class 'kivy.graphics.vertex_instructions.Ellipse'>, <class 'kivy.graphics.vertex_instructions.Rectangle'>, <class 'kivy.graphics.vertex_instructions.RoundedRectangle'>)

supported render shapes, specified in render_shape.

SHADER_PARAMETER_MATCHER = re.compile('^uniform (float|vec2|vec4) ([a-z_]+);', re.MULTILINE)

match uniform var declarations

HIDDEN_SHADER_PARAMETERS = ('resolution', 'time', 'win_pos')

uniform vars that are not editable by the user

shader_parameter_alias(shader_code, arg_name)[source]

check for alias for the passed shader argument name in the passed shader code.

Parameters
  • shader_code (str) – shader code to determine the parameter alias names (from comment of uniform declaration).

  • arg_name (str) – shader arg name.

Return type

str

Returns

alias (if found) or shader arg name (if not).

shader_parameters(shader_code)[source]

shader arg names of the shader code in the passed shader code.

Parameters

shader_code (str) – shader code from which to determine the declared uniform parameter names.

Return type

Tuple[str, …]

Returns

tuple of shader arg names of the shader code passed into the argument shader_code, plus the start_time argument which controls, independent from to be included in the shader code, how the time argument get prepared in each render frame (see also start_time).

class ShadersMixin[source]

Bases: object

shader mixin base class

canvas: Any
center_x: float
center_y: float
fbind: Callable
parent: Any
pos: list
size: list
to_window: Callable
unbind_uid: Callable
added_shaders

list of shader-ids/kwarg-dicts for each shader

running_shaders: List[Dict[str, Any]] = []

list/pool of active/running shaders/render-contexts

_pos_fbind_uid: int = 0
_size_fbind_uid: int = 0
add_shader(add_to='', shader_code='uniform float alpha;\\nuniform float contrast;\\nuniform float tex_col_mix;\\nuniform float time;\\nuniform vec2 center_pos;\\nuniform vec2 win_pos;\\nuniform vec2 resolution;\\nuniform vec4 tint_ink;\\n\\nconst float THOUSAND = 963.9;\\nconst float HUNDRED = 69.3;\\nconst float TEN = 9.9;\\nconst float TWO = 1.83;\\nconst float ONE = 0.99;\\n\\nvoid main(void)\\n{\\n  vec2 pix_pos = (frag_modelview_mat * gl_FragCoord).xy - win_pos;\\n  vec2 rel_center = center_pos - win_pos;\\n  float x = abs(pix_pos.x - rel_center.x);\\n  float y = abs(pix_pos.y - rel_center.y - resolution.y);\\n\\n  float m1 = x + y + cos(sin(time) * TWO) * HUNDRED + sin(x / HUNDRED) * THOUSAND;\\n  float m2 = y / resolution.y;\\n  float m3 = x / resolution.x + time * TWO;\\n\\n  float c1 = abs(sin(m2 + time) / TWO + cos(m3 / TWO - m2 - m3 + time));\\n  float c2 = abs(sin(c1 + sin(m1 / THOUSAND + time) + sin(y / HUNDRED + time) + sin((x + y) / HUNDRED) * TWO));\\n  float c3 = abs(sin(c2 + cos(m2 + m3 + c2) + cos(m3) + sin(x / THOUSAND)));\\n\\n  vec4 tex = texture2D(texture0, tex_coord0);\\n  float dis = TWO * distance(pix_pos, rel_center) / max(resolution.x, resolution.y);\\n  vec4 col = vec4(c1, c2, c3, contrast * (ONE - dis)) * tint_ink * TWO;\\n  col = mix(tex, col, tex_col_mix);\\n  gl_FragColor = vec4(col.rgb, col.a * sqrt(alpha));\\n}\\n', shader_file='', start_time=0.0, update_freq=30.0, run_state='running', render_shape=<class 'kivy.graphics.vertex_instructions.Rectangle'>, **glsl_dyn_args)[source]

add/register a new shader for the mixing-in widget.

Parameters
  • add_to (str) – ‘’ to add to current canvas, ‘before’ and ‘after’ to the before/after canvas of the widget instance mixed-into. if the canvas does not exist then the shaders render context will be set as a current canvas.

  • shader_code (str) – fragment shader code block or key of BUILT_IN_SHADERS dict if first character is ‘=’. this argument will be ignored if shader_file is not empty.

  • shader_file (str) – filename with the glsl shader code (with “–VERTEX” or “–FRAGMENT” sections) to load.

  • start_time (Optional[float]) – base/start time. passing the default value zero is syncing the time glsl parameter of of this shader with kivy.clock.Clock.get_boottime(). pass None to initialize this argument to the current Clock boot time, to start the time glsl argument at zero.

  • update_freq (float) – shader render update frequency. pass 0.0 to disable creation of an update timer.

  • run_state (str) – optional shader run state (default=’running’), pass ‘paused’/’error’ to not run it.

  • render_shape (Union[Any, str]) – pass one of the supported shapes (RENDER_SHAPES) either as shape class or str.

  • glsl_dyn_args

    extra/user dynamic shader parameters, depending on the used shader code. the keys of this dict are the names of the corresponding glsl input variables in your shader code. the built-in shaders (provided by this module) providing the following glsl input variables:

    • ’alpha’: opacity (float, 0.0 - 1.0).

    • ’center_pos’: center position in Window coordinates (tuple(float, float)).

    • ’contrast’: color contrast (float, 0.0 - 1.0).

    • ’mouse’: mouse pointer position in Window coordinates (tuple(float, float)).

    • ’resolution’: width and height in Window coordinates (tuple(float, float)).

    • ’tex_col_mix’: factor (float, 0.0 - 1.0) to mix the kivy input texture

      and the calculated color. a value of 1.0 will only show the shader color, whereas 0.0 will result in the color of the input texture (uniform texture0).

    • ’tint_ink’: tint color with color parts in the range 0.0 till 1.0.

    • ’time’: animation time (offset to start_time) in seconds. if

      specified as constant (non-dynamic) value then you have to call the next_tick() method to increment the timer for this shader.

    pass a callable to provide a dynamic/current value, which will be called on each rendering frame without arguments and the return value will be passed into the glsl shader.

    Note

    don’t pass int values because some renderer will interpret them as 0.0.

Return type

Dict[str, Any]

Returns

index (id) of the created/added render context.

_compile_shader(shader_id)[source]

try to compile glsl shader file/code - raise ValueError if compilation failed.

Parameters

shader_id (Dict[str, Any]) – shader id (internal data dict with either shader_file (preference) or shader_code key representing the shader code to use).

Return type

RenderContext

Returns

kivy/glsl render context with the compiled shader attached to it.

del_shader(shader_id)[source]

remove shader_id added via add_shader.

Parameters

shader_id (Dict[str, Any]) – id of the shader to remove (returned by add_shader()). ignoring if the passed shader got already removed.

next_tick(increment=0.03333333333333333)[source]

increment glsl time input argument if running_shaders get updated manually/explicitly by the app.

Parameters

increment (float) – delta in seconds for the next refresh of all running_shaders with a time constant.

on_added_shaders(*_args)[source]

added_shaders list property changed event handler.

on_parent(*_args)[source]

parent changed event handler.

play_shader(shader_id)[source]

create new render context canvas and add it to the widget canvas to display shader output.

Parameters

shader_id (Dict[str, Any]) – shader id and internal data dict with either shader_file (preference) or shader_code key representing the shader code to use. a shader dict with the shader_code key is a fragment shader (w/o a vertex shader), that will be automatically prefixed with the Kivy fragment shader header file template, if the $HEADER$ placeholder is not included in the shader code (even if it is commented out, like in the following glsl code line: #ifdef GL_ES //$HEADER$.

Return type

str

Returns

“” if shader is running/playing or error message string.

_pos_changed(*_args)[source]

pos changed event handler.

_refresh_glsl(shader_id, _dt)[source]

timer/clock event handler to animate and sync one canvas shader.

refresh_running_shaders()[source]

manually update all running_shaders.

refresh_shader(shader_id)[source]

update the shader arguments for the current animation frame.

Parameters

shader_id (Dict[str, Any]) – dict with render context, rectangle and glsl input arguments.

Return type

str

Returns

empty string if arguments got passed to the shader without errors, else error message.

_size_changed(*_args)[source]

size changed event handler.

stop_shader(shader_id, set_run_state=True)[source]

stop shader by removing it from started shaders.

Parameters
  • shader_id (Dict[str, Any]) – id of the shader to stop. ignoring if the passed shader got already stopped.

  • set_run_state (bool) – pass False to prevent that the run_state of the shader_id gets changed to paused (for internal use to refresh all running shaders).

update_shaders()[source]

stop/unbind all shaders, then restart/bind the shaders having their run_state as ‘running’.