かねてより興味があったGLSLに触る事ができたのでメモ。

Kivy上でShaderを扱う方法

https://github.com/kivy/kivy/tree/master/examples/shader に幾つかShaderを使ったSampleCodeがありますが、そのやり方よりもEffectWidgetを使ったやり方の方が簡単そうなのでこっちを選びました。

簡単な物を作ってみた

from kivy.factory import Factory
from kivy.app import runTouchApp
from kivy.uix.effectwidget import EffectWidget, EffectBase


GLSL_CODE = r'''
vec4 effect(vec4 color, sampler2D texture, vec2 tex_coords, vec2 coords)
{
    return vec4(color.rg, 0.0, color.a);  // A
}
'''

effect = EffectBase(glsl=GLSL_CODE)  # B
root = EffectWidget(effects=[effect, ])  # C
root.add_widget(Factory.Label(
    font_size=30,
    text='GLSL TEST'))

runTouchApp(root)

やり方は簡単で書いたGLSLのコードを引数にEffectBase(又はAdvancedEffectBase)のInstanceを作り(B行)、それをEffectWidgetのeffectsプロパティに与えるだけです(C行)。(effectsがlist型なのは複数のeffectを重ねがけできるようにする為)。GLSLのコードでやっている事はPixelの色成分の内、青以外はそのまま出力して青だけは0にして出力しています(A行)。なので結果は以下のようになります。

もし緑成分のみを0にしたいならA行をreturn vec4(color.r, 0.0, color.ba);とすればいいです。

もっと格好いい物を

その1

今度はWeb上に公開されている格好いいShaderを使ってみます。まずはこれを。(このShaderは本来はマウスカーソルの位置によってShaderの結果が変化するものですが、位置をShaderに伝える処理を私が書いていない為、そうはならないようになっています。)

from kivy.app import runTouchApp
from kivy.uix.effectwidget import EffectWidget, EffectBase


GLSL_CODE = r'''
uniform vec2 mouse;

vec4 effect(vec4 color, sampler2D texture, vec2 tex_coords, vec2 coords)
{
    vec2 r = resolution,
    o = coords.xy - r/2.;
    o = vec2(length(o) / r.y - .3, atan(o.y,o.x));
    vec4 s = .1*cos(1.6*vec4(0,1,2,3) + time + o.y + sin(o.x) * sin(mouse.x * time / 1. + 5.4)*2.),
    e = s.yzwx,
    f = min(o.x-s,e-o.x);
    return dot(clamp(f*r.y,0.,1.), 40.*(s-e)) * (s-.1) - f;
}
'''

effect = EffectBase(glsl=GLSL_CODE)
root = EffectWidget(effects=[effect, ])

runTouchApp(root)

いつも思うのですが、たったこれだけのコード量でこんな綺麗なアニメーションが書けるGLSLは本当にすごいです。

その1 + PixelateEffect

その2

次はこれです。今度の物はマウスカーソルの位置をShaderに伝える処理を私が書いている為、それによってShaderの結果が変化します。

from kivy.app import runTouchApp
from kivy.uix.effectwidget import EffectWidget, AdvancedEffectBase


GLSL_CODE = r'''
#define PI 3.14159265359

uniform vec2 mouse;

vec3 hsv2rgb(vec3 c) {
    vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0);
    vec3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www);
    return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y);
}

vec4 effect(vec4 color, sampler2D texture, vec2 tex_coords, vec2 coords) {

    vec2 p = coords.xy / resolution.xy;
    vec2 d = normalize(mouse - p);
    float h = 0.5 * atan(d.y, d.x) / PI;
    vec3 c = hsv2rgb(vec3(h + time / PI, 1.0, 1.0));
    float m = smoothstep(0.4, 0.5, fract(10.0 * h)) + smoothstep(0.6, 0.5, fract(10.0 * h));
    return vec4(c, 1.0) * m;
}
'''

effect = AdvancedEffectBase(
    uniforms={'mouse': (0, 0, )},
    glsl=GLSL_CODE)
root = EffectWidget(effects=[effect, ])

# on_touch_moveイベントはマウスのボタンを押し下げている時にしか起きないので、Shaderに位置が伝わるのも当然その時だけ
root.bind(on_touch_move=lambda __, touch: effect.uniforms.__setitem__('mouse', touch.spos))

runTouchApp(root)

その3

次はこれ

from kivy.app import runTouchApp
from kivy.uix.effectwidget import EffectWidget, EffectBase


GLSL_CODE = r'''
#ifdef GL_ES
precision mediump float;
#endif

vec4 effect(vec4 color, sampler2D texture, vec2 tex_coords, vec2 coords) {
    vec4 return_value;
    vec2 uv = gl_FragCoord.xy / resolution.xy;
    float dist = 0.;
    uv.x = -3.+4.*uv.x;
    uv.y = -1.+2.*uv.y;
    // comment the next line to see the fully zoomed out view
    uv *=pow(.1,4.+cos(.1*time));
    uv.x += .275015;//;
    uv.y += .0060445;//
    //uv /= 5.;
    vec4 col =vec4(1.);
    vec2 z = vec2(0.0);

    int trap=0;
    for(int i = 0; i < 400; i++){
        if(dot(z,z)>4.){trap = i;break;}
        dist = min( 1e20, dot(z,z))+cos(float(i)*12.+3.*time);
        z = mat2(z,-z.y,z.x)*z + uv;
    }
    dist = sqrt(dist);
    float orb = sqrt(float(trap))/64.;
    return_value=vec4(0.,log(dist)*sqrt(dist)-orb-orb,log(dist)*sqrt(dist-abs(sin(time))),1.);
    if(orb == 0.){return_value = vec4(0.);}
    return return_value;
}
'''

effect = EffectBase(glsl=GLSL_CODE)
root = EffectWidget(effects=[effect, ])
runTouchApp(root)

その4

最後はこれです。

from kivy.app import runTouchApp
from kivy.uix.effectwidget import EffectWidget, EffectBase


GLSL_CODE = r'''
#ifdef GL_ES
precision mediump float;
#endif
// shadertoy globals
float iTime;
vec3  iResolution;

// --------[ Original ShaderToy begins here ]---------- //

// Inspired by:
//  http://cmdrkitten.tumblr.com/post/172173936860


#define Pi 3.14159265359

struct Gear
{
    float t;            // Time
    float gearR;        // Gear radius
    float teethH;       // Teeth height
    float teethR;       // Teeth "roundness"
    float teethCount;   // Teeth count
    float diskR;        // Inner or outer border radius
    vec3 color;         // Color
};



float GearFunction(vec2 uv, Gear g)
{
    float r = length(uv);
    float a = atan(uv.y, uv.x);

    // Gear polar function:
    //  A sine squashed by a logistic function gives a convincing
    //  gear shape!
    float p = g.gearR-0.5*g.teethH +
              g.teethH/(1.0+exp(g.teethR*sin(g.t + g.teethCount*a)));

    float gear = r - p;
    float disk = r - g.diskR;

    return g.gearR > g.diskR ? max(-disk, gear) : max(disk, -gear);
}


float GearDe(vec2 uv, Gear g)
{
    // IQ's f/|Grad(f)| distance estimator:
    float f = GearFunction(uv, g);
    vec2 eps = vec2(0.00011, 0);
    vec2 grad = vec2(
        GearFunction(uv + eps.xy, g) - GearFunction(uv - eps.xy, g),
        GearFunction(uv + eps.yx, g) - GearFunction(uv - eps.yx, g)) / (2.0*eps.x);

    return (f)/length(grad);
}



float GearShadow(vec2 uv, Gear g)
{
    float r = length(uv+vec2(0.1));
    float de = r - g.diskR + 1.0*(g.diskR - g.gearR);
    float eps = 1.4*g.diskR;
    return smoothstep(eps, 0., abs(de));
}


void DrawGear(inout vec3 color, vec2 uv, Gear g, float eps)
{
    float d = smoothstep(eps, -eps, GearDe(uv, g));
    float s = 1.0 - 0.7*GearShadow(uv, g);
    color = mix(s*color, g.color, d);
}


vec4 mainImage( out vec4 fragColor, in vec2 fragCoord )
{
    float t = 1.5*iTime;
    vec2 uv = 3.0*(fragCoord - 0.5*iResolution.xy)/iResolution.y;
    float eps = 2.0/iResolution.y;

    // Scene parameters;
    vec3 base = vec3(0.95, 0.7, 0.2);
    const float count = 8.0;

    Gear outer = Gear(3.0, 0.8, 0.08, 4.0, 32.0, 0.9, base);
    Gear inner = Gear(0.0, 0.4, 0.08, 4.0, 16.0, 0.3, base);


    // Draw inner gears back to front:
    vec3 color = vec3(0.0);
    for(float i=0.0; i<count; i++)
    {
        t += 2.0*Pi/count;
        inner.t = 16.0*t;
        inner.color = base*(0.35 + 0.6*i/(count-1.0));
        DrawGear(color, uv+0.4*vec2(cos(t),sin(t)), inner, eps);
    }

    // Draw outer gear:
    DrawGear(color, uv, outer, eps);


    return vec4(color,1.0);
}
// --------[ Original ShaderToy ends here ]---------- //

vec4 effect(vec4 color, sampler2D texture, vec2 tex_coords, vec2 coords) {
    iTime = time;
    iResolution = vec3(resolution, 0.0);
    return mainImage(gl_FragColor, gl_FragCoord.xy);
}

'''

effect = EffectBase(glsl=GLSL_CODE)
root = EffectWidget(effects=[effect, ])
runTouchApp(root)

最後に

Web上にあるShaderをEffectWidgetで使うためにはそれ用に書き換えないといけないのが面倒くさいです。冒頭で挙げた https://github.com/kivy/kivy/tree/master/examples/shader のやり方だとその必要がなさそうなので今度試そうと思います。

Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account log in.