EffectWidgetのソースやShaderのサンプルコードを読んでいると必ずRenderContextが出てきます。どうやらこれがGLSLを使う上で必要な物のようです。
kivy.graphics.RenderContext
説明を読むにこのClassはCanvasに描画に必要な情報や状態を付随させた物でしょうか。名前にContextと入っているのできっとそうだと思います。という事はCanvas自体はそういった情報を持っていないという事になる...?よく分かりませんが深追いはしないでおきます。ともかく私が参考にしたソースコード全てでこのClassは以下のように使われています。
class SomeWidget(SomeSuperWidget):
def __init__(self, **kwargs):
self.canvas = RenderContext(....)
super(SomeWidget, self).__init__(**kwargs)
親の__init__()を呼ぶ前にcanvas属性にRenderContextのInstanceを入れています。こうする事で通常のCanvasに代わってRenderContextが組み込まれた特殊なWidgetになるのでしょうか。こういった風にWidgetのcanvas属性にCanvasの派生ClassであるRenderContextやFboを入れるコードはKivyのソースコード内でよく見かけますが、それによって何にどんな影響を与えるのか今の所よく分かっていません。
このRenderContextですが、初期化時に渡している引数に違いがあるのが気になります。EffectWidgetではこう、examplesのrotated.pyではこう、examplesのshadertree.pyではこうなっています。examplesのplasma.pyではこう(引数が空)なってはいますが、これはおそらく説明にある古いやり方を使っているだけなので、実質他のexamplesの物と同じでしょう。これらの引数は英語の意味的に親の変換行列を使うか否かを指定する物だと思いますが、何故EffectWidgetがexamplesとは異なる引数を与えているのか分かりません。
理解はできていませんが、親の__init__()を呼ぶ前にcanvasにRenderContextを入れることがGLSLを使うのに必要な手順の一つとして呪文として覚えておきます。次にこのRenderContextが持つ属性shaderを見ていきます。
kivy.graphics.Shader
RenderContextのshader属性に入っているのがこのClassのInstanceで、各参考コード上での使われ方は以下のようになっています。
全て
def on_fs(self, instance, value):
shader = self.canvas.shader
old_value = shader.fs # A
shader.fs = value # B
if not shader.success: # C
shader.fs = old_value # D
raise Exception('failed') # D
のようなコードになっています。
- 以前のGLSLを保存(A行)
- 新しいGLSLを設定(B行)
- 新しいGLSLがうまく設定できたか?(C行)
- 失敗した場合は、以前の物に戻し例外を投げる(D行)
これをGLSLの設定とError対処の仕方として覚えておきます。
uniform変数
uniform変数はGLSLに外部から値を与える仕組みで、Web上のGLSLを見ていると経過時間(time)や描画領域の大きさ(resolution)をuniform変数を使って渡しているのが分かります。ここではどのようにしてこのuniform変数を設定するのか調べます。
各参考コード上でのuniform変数を更新している箇所
全て
canvas['time'] = ...
canvas['resolustion'] = ...
のようになっています。なのでRenderContextのInstanceに辞書のように書き込む事でuniform変数が更新されるとみて間違いなさそうです。
$HEADER$
plasma.pyを見るとGLSLのコードに$HEADER$
と書かれているのが分かります。これに関する説明はShaderのDocumentationに書いてあって、要するにGLSLを書く時に毎回書く事になるお決まりのコードを一々書かなくてもいいようにそこに定型文を展開してくれる物のようです。
実践
これでGLSLを使うのに必要な手順は理解できたと思うので実際に自分でコーディングしてみます。
canvasの置き換え
from kivy.app import runTouchApp
from kivy.factory import Factory
from kivy.graphics import RenderContext
class ShaderWidget(Factory.Widget):
def __init__(self, **kwargs):
self.canvas = RenderContext(
use_parent_projection=True,
use_parent_modelview=True,
use_parent_frag_modelview=True)
super().__init__(**kwargs)
root = ShaderWidget()
runTouchApp(root)
[INFO] [Logger ] Record log in /tmp/kivy_logs/kivy_18-04-07_0.txt
[INFO] [Kivy ] v1.10.1.dev0, git-Unknown, 20180406
[INFO] [Python ] v3.6.0 (default, Apr 30 2017, 20:11:24)
[GCC 4.8.4]
[INFO] [Factory ] 194 symbols loaded
[INFO] [Image ] Providers: img_tex, img_dds, img_sdl2, img_pil, img_gif (img_ffpyplayer ignored)
[Finished in 0.7s with exit code -11]
とりあえずcanvasだけ置き換えて実行した所、Windowは出ず例外も起こらずexit code -11
だけで終わりました。
GLSLを設定する為のStringPropertyを実装
from kivy.app import runTouchApp
from kivy.factory import Factory
from kivy.graphics import RenderContext
from kivy.properties import StringProperty
class ShaderWidget(Factory.Widget):
fs = StringProperty(None)
def __init__(self, **kwargs):
self.canvas = RenderContext(
use_parent_projection=True,
use_parent_modelview=True,
use_parent_frag_modelview=True)
super().__init__(**kwargs)
def on_fs(self, __, value):
shader = self.canvas.shader
old_value = shader.fs
shader.fs = value
if not shader.success:
shader.fs = old_value
raise Exception('Failed to set shader.')
root = ShaderWidget()
runTouchApp(root)
これだけだと当然結果は変わりません。
簡単なGLSLを書いてみる
GLSL_CODE = '''
void main(void)
{
gl_FragColor = vec4( 1.0, 0.0, 0.0, 1.0);
}
'''
root = ShaderWidget(fs=GLSL_CODE)
runTouchApp(root)
これでも結果は変わりませんでした。何かがおかしいです。
うまくいかない原因
print()で調べた結果super().__init__()
に入る前にプログラムが落ちている事が分かりました。調べた感じRenderContextの作成に先立って何らかの初期化処理が完了していないと上のexit code -11
が起こるようです。そしてそれをさせる方法の一つが
import kivy.core.window
で、もうひとつが
from kivy.base import EventLoop
EventLoop.ensure_window()
です。上のコードを書くことでexit code -11
が出なくなりちゃんとWindowが出てくれるようになりました。後で気付いたのですがソースコードにちゃんと以下のようにそれらしい説明がありました。
# Make sure opengl context exists
EventLoop.ensure_window()
# imported early for side effects needed for Shader
import kivy.core.window
from kivy.core.window import Window # side effects needed by Shader
何とかWindowは表示されるようになりましたが、画面は期待している真っ赤ではなく真っ黒です。原因はShaderWidgetに何の描画命令も与えていないからでした。FragmentShaderを理解している人にとっては当然なのかもしれませんが、何らかの描画命令によってPixelへの書き込みの必要性が生じた時に初めてFragmentShaderが働くようです。つまりはFragmentShaderとは別に何かの描画命令を与えないといけないです。実際
Builder.load_string(r'''
<ShaderWidget>:
canvas:
Rectangle:
pos: self.pos
size: self.size
''')
としたことで期待通りの画面を出せました。
またRectangleに代えて以下のようにEllipseを使うと
Builder.load_string(r'''
<ShaderWidget>:
canvas:
Ellipse:
pos: self.pos
size: self.size
''')
結果が
となる事から分かるように、FragmentShaderは書き込む必要の生じたPixelのみに働きます。(この事自体は10年前くらいに3D Graphicsを扱った時に知っていたはずですが、忘れてました。)
uniform変数で描画領域の大きさを伝える
class ShaderWidget(Factory.Widget):
def on_size(self, __, size):
self.canvas['resolution'] = [float(size[0]), float(size[1]), ]
GLSL_CODE = '''
uniform vec2 resolution;
略
'''
明示的に型変換してあげないといけない。
グラデーション
GLSL_CODE = '''
uniform vec2 resolution;
void main(void)
{
float sx = gl_FragCoord.x / resolution.x;
float sy = gl_FragCoord.y / resolution.y;
gl_FragColor = vec4( 1.0, sx, sy, 1.0);
}
'''
gl_FragCoordにはPixelの座標が入っているので、それを描画領域の大きさで割れば割合で座標が得られます。
参考コード
effectwidget.py
shader.pxd
shader.pyx
RenderContextのドキュメント
Shaderのドキュメント
shaderのexamples