KivyでGLSL #2 RenderContextに関する調べ事

[EffectWidgetのソース][effectwidget.py]や[Shaderのサンプルコード](https://github.com/kivy/kivy/tree/master/examples/shader)を読んでいると必ずRenderContextが出てきます。どうやらこれがGLSLを使う上で必要な物のようです。

# [kivy.graphics.RenderContext][RenderContext.doc]
![Untitled Diagram.png](https://qiita-image-store.s3.amazonaws.com/0/144673/3ec6d41f-52ac-a048-8275-9a3ccc23cfbd.png)

説明を読むにこのClassはCanvasに描画に必要な情報や状態を付随させた物でしょうか。名前にContextと入っているのできっとそうだと思います。という事はCanvas自体はそういった情報を持っていないという事になる...？よく分かりませんが深追いはしないでおきます。ともかく私が参考にしたソースコード全てでこのClassは以下のように使われています。

```python3

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では[こう](https://github.com/kivy/kivy/blob/d3d517dd236da48876742fec6881d8822ac356df/kivy/uix/effectwidget.py#L654-L655)、examplesのrotated.pyでは[こう](https://github.com/kivy/kivy/blob/d3d517dd236da48876742fec6881d8822ac356df/examples/shader/rotated.py#L69-L71)、examplesのshadertree.pyでは[こう](https://github.com/kivy/kivy/blob/d3d517dd236da48876742fec6881d8822ac356df/examples/shader/shadertree.py#L104)なっています。examplesのplasma.pyでは[こう](https://github.com/kivy/kivy/blob/d3d517dd236da48876742fec6881d8822ac356df/examples/shader/plasma.py#L52)(引数が空)なってはいますが、これはおそらく説明にある[古いやり方][RenderContext.doc]を使っているだけなので、実質他のexamplesの物と同じでしょう。これらの引数は英語の意味的に親の変換行列を使うか否かを指定する物だと思いますが、何故EffectWidgetがexamplesとは異なる引数を与えているのか分かりません。
　理解はできていませんが、**親の\_\_init\_\_()を呼ぶ前にcanvasにRenderContextを入れることがGLSLを使うのに必要な手順の一つ**として呪文として覚えておきます。次にこのRenderContextが持つ属性shaderを見ていきます。

# [kivy.graphics.Shader][Shader.doc]

RenderContextのshader属性に入っているのがこのClassのInstanceで、各参考コード上での使われ方は以下のようになっています。

- [plasma.py](https://github.com/kivy/kivy/blob/d3d517dd236da48876742fec6881d8822ac356df/examples/shader/plasma.py#L61-L68)
- [shadertree.py](https://github.com/kivy/kivy/blob/d3d517dd236da48876742fec6881d8822ac356df/examples/shader/shadertree.py#L128-L135)
- [rotated.py](https://github.com/kivy/kivy/blob/d3d517dd236da48876742fec6881d8822ac356df/examples/shader/rotated.py#L83-L90)
- [effectwidget.py](https://github.com/kivy/kivy/blob/d3d517dd236da48876742fec6881d8822ac356df/kivy/uix/effectwidget.py#L592-L602)

全て

```python3
    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
```

のようなコードになっています。

1. 以前のGLSLを保存(A行)
2. 新しいGLSLを設定(B行)
3. 新しいGLSLがうまく設定できたか?(C行)
4. 失敗した場合は、以前の物に戻し例外を投げる(D行)

これを**GLSLの設定とError対処の仕方**として覚えておきます。

# uniform変数

uniform変数はGLSLに外部から値を与える仕組みで、Web上のGLSLを見ていると経過時間(time)や描画領域の大きさ(resolution)をuniform変数を使って渡しているのが分かります。ここではどのようにしてこのuniform変数を設定するのか調べます。

各参考コード上でのuniform変数を更新している箇所

- [plasma.py](https://github.com/kivy/kivy/blob/d3d517dd236da48876742fec6881d8822ac356df/examples/shader/plasma.py#L70-L77)
- [effectwidget.py](https://github.com/kivy/kivy/blob/d3d517dd236da48876742fec6881d8822ac356df/kivy/uix/effectwidget.py#L686-L696)
- [rotated.py](https://github.com/kivy/kivy/blob/d3d517dd236da48876742fec6881d8822ac356df/examples/shader/rotated.py#L80-L81)
- [shadertree.py](https://github.com/kivy/kivy/blob/d3d517dd236da48876742fec6881d8822ac356df/examples/shader/shadertree.py#L124-L126)

全て

```python3
canvas['time'] = ...
canvas['resolustion'] = ...
```

のようになっています。なので**RenderContextのInstanceに辞書のように書き込む事でuniform変数が更新される**とみて間違いなさそうです。

# $HEADER$

[plasma.py][plasma.py]を見るとGLSLのコードに`$HEADER$`と書かれているのが分かります。これに関する説明は[ShaderのDocumentation][Shader.doc]に書いてあって、要するにGLSLを書く時に毎回書く事になるお決まりのコードを一々書かなくてもいいようにそこに定型文を展開してくれる物のようです。

# 実践

これでGLSLを使うのに必要な手順は理解できたと思うので実際に自分でコーディングしてみます。

### canvasの置き換え

```python3
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)
```

```text:出力
[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を実装

```python3
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を書いてみる

```python3
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`が起こるようです。そしてそれをさせる方法の一つが

```python3:examples/shader/各.pyで使われている方法
import kivy.core.window
```

で、もうひとつが

```python3:effectwidget.pyで使われている方法
from kivy.base import EventLoop
EventLoop.ensure_window()
```

です。上のコードを書くことで`exit code -11`が出なくなりちゃんとWindowが出てくれるようになりました。後で気付いたのですがソースコードにちゃんと以下のようにそれらしい説明がありました。

```python3:effectwidget.pyの一部
        # Make sure opengl context exists
        EventLoop.ensure_window()
```

```python3:rotated.pyの一部
# imported early for side effects needed for Shader
import kivy.core.window
```
```python3:shadertree.pyの一部
from kivy.core.window import Window  # side effects needed by Shader
```

　何とかWindowは表示されるようになりましたが、画面は期待している真っ赤ではなく真っ黒です。原因はShaderWidgetに何の描画命令も与えていないからでした。FragmentShaderを理解している人にとっては当然なのかもしれませんが、何らかの描画命令によってPixelへの書き込みの必要性が生じた時に初めてFragmentShaderが働くようです。つまりはFragmentShaderとは別に何かの描画命令を与えないといけないです。実際

```python3
Builder.load_string(r'''
<ShaderWidget>:
    canvas:
        Rectangle:
            pos: self.pos
            size: self.size
''')
```
としたことで期待通りの画面を出せました。

![](https://qiita-image-store.s3.amazonaws.com/0/144673/e42c2053-ab5e-a0d9-0f23-2eeab3df38ad.png)

またRectangleに代えて以下のようにEllipseを使うと

```python3
Builder.load_string(r'''
<ShaderWidget>:
    canvas:
        Ellipse:
            pos: self.pos
            size: self.size
''')
```

結果が

![](https://qiita-image-store.s3.amazonaws.com/0/144673/5d85a914-d119-a429-439a-b8dcc627296a.png)

となる事から分かるように、FragmentShaderは書き込む必要の生じたPixelのみに働きます。(この事自体は10年前くらいに3D Graphicsを扱った時に知っていたはずですが、忘れてました。)

### uniform変数で描画領域の大きさを伝える

```python3
class ShaderWidget(Factory.Widget):

    def on_size(self, __, size):
        self.canvas['resolution'] = [float(size[0]), float(size[1]), ]

GLSL_CODE = '''
uniform vec2 resolution;
略
'''
```

明示的に型変換してあげないといけない。

### グラデーション

```python3
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の座標が入っているので、それを描画領域の大きさで割れば割合で座標が得られます。

![](https://qiita-image-store.s3.amazonaws.com/0/144673/c5538f39-caaa-f2e5-3eb9-861cf2b6aa03.png)

[コード全体](https://gist.github.com/gottadiveintopython/d9a2935560469bd8a8d0a890180003c7)

# 参考コード

[effectwidget.py][effectwidget.py]
[shader.pxd][shader.pxd]
[shader.pyx][shader.pyx]
[RenderContextのドキュメント][RenderContext.doc]
[Shaderのドキュメント][Shader.doc]
[shaderのexamples][shader_examples]

[shader_examples]:https://github.com/kivy/kivy/tree/master/examples/shader
[effectwidget.py]:https://github.com/kivy/kivy/blob/master/kivy/uix/effectwidget.py
[RenderContext.doc]:https://kivy.org/docs/api-kivy.graphics.instructions.html#kivy.graphics.instructions.RenderContext
[shader.pxd]:https://github.com/kivy/kivy/blob/master/kivy/graphics/shader.pxd
[shader.pyx]:https://github.com/kivy/kivy/blob/master/kivy/graphics/shader.pyx
[Shader.doc]:https://kivy.org/docs/api-kivy.graphics.shader.html
[plasma.py]:https://github.com/kivy/kivy/blob/master/examples/shader/plasma.py