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(ShadersMixin, BoxLayout):
alternatively you can declare a shader for your widget as a new kv rule within a kv file:
<MyBoxLayoutWithShader@ShadersMixin+BoxLayout>
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).
if a compile error occurred then the run_state value of the shader id dict will be set to ‘error’ - check the value of the error_message key of the shader id dict for more details on the error.
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).
if the crash only happens on some Adreno GPUs then see issues p4a #2723 and kivy #8080 and a possible fix in kivy #8098. sometimes these type of 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.
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 frames-per-second |
|
shader internal data and id |
|
dict of built-in shader code blocks - key specifies shader in |
|
supported render shapes, specified in |
|
match uniform var declarations |
|
uniform vars that are not editable by the user |
Functions
|
check for alias for the passed shader argument name in the specified shader code. |
|
shader arg names of the shader code in the specified shader code. |
Classes
shader mixin base class |
- DEFAULT_FPS = 30.0
default frames-per-second
- 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 specified shader code.
- shader_parameters(shader_code)[source]
shader arg names of the shader code in the specified shader code.
- Parameters:
shader_code¶ (
str
) – shader code from which to determine the declared uniform parameter names.- Return type:
- Returns:
tuple of shader arg names of the shader code passed into the argument
shader_code
, plus the start_time argument which controls, independent of to be included in the shader code, how the time argument get prepared in each render frame (see alsostart_time
).
- class ShadersMixin[source]
Bases:
object
shader mixin base class
- added_shaders
list of shader-ids/kwarg-dicts for each shader
- 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 ifshader_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 this shader withkivy.clock.Clock.get_boottime()
. passNone
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.
- ’time’: animation time (offset to
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:
- 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.
- 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 byadd_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.
- 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:
- Returns:
“” if shader is running/playing or error message string.
- _refresh_glsl(shader_id, _dt)[source]
timer/clock event handler to animate and sync one canvas shader.