KivyでGLSL #3 Mesh

# はじめに

[公式の3Dのexample][3dexample]や[garden.ddd(3Dモデルを表示する為の物)][garden.ddd]を見ているとどれも[Mesh][mesh_doc]を使っているのが分かります。今回は3Dの描画に必要と思われるこの`Mesh`について調べてみました。

# 簡單な物を書いてみる

(ここでやっている事は`Mesh`を使わずとも[Triangle][triangle_doc](三角形を一つだけ描く事に特化した命令)で済む事なのですが、勉強の為にあえて`Mesh`を使ってます。)

```python3
from kivy.app import runTouchApp
from kivy.lang import Builder

root = Builder.load_string(r'''
Widget:
    canvas:
        Mesh:
            vertices:
                [  # 1行で1頂点分のデータで、各行には x座標,y座標,Textureのx座標,Textureのy座標 の順でデータが入っている。
                *self.pos, 0, 1,
                self.right, self.y, 1, 1,
                self.center_x, self.top, 0.5, 0,
                ]
            indices: range(3)
            mode: 'triangles'
            source: 'kivy-logo-black-128.png'  # https://kivy.org/logos/kivy-logo-black-128.png
''')

runTouchApp(root)
```

![Screenshot at 2018-04-16 14-33-23.png](https://qiita-image-store.s3.amazonaws.com/0/144673/0f9485f6-f149-b659-67b2-94aad066ad10.png)

Textureの座標の原点が左上なのが間違えやすいです。

# 頂点の形式を指定

上の例で分かるように、既定ではMeshに渡す頂点の形式は**2Dの頂点座標**と**2DのTexture座標**なので、3D描画の為にはこれを変える必要があります。なのでまず頂点の形式の指定方法を調べました。

```python3
from kivy.app import runTouchApp
from kivy.lang import Builder
from kivy.factory import Factory


class CustomizedMesh(Factory.Mesh):
    def __init__(self, *args, **kwargs):
        kwargs['fmt'] = [(b'vPosition', 2, 'float'), ]  # A
        super().__init__(*args, **kwargs)


Factory.register('CustomizedMesh', cls=CustomizedMesh)  # B

root = Builder.load_string(r'''
Widget:
    canvas:
        CustomizedMesh:
            vertices: [*self.pos, self.right, self.y, self.center_x, self.top, ]  # C
            indices: range(3)
            mode: 'triangles'
''')

runTouchApp(root)
```

![Screenshot at 2018-04-16 15-31-08.png](https://qiita-image-store.s3.amazonaws.com/0/144673/866e7718-7e00-0397-f6ba-fd2b1e01d2a2.png)

Meshの`__init__()`に渡すKeyword引数`fmt`がそれで、A行で頂点の形式を2Dの頂点座標のみに指定したので、C行で渡す頂点のデータもそれだけになっています。またB行ですが、任意のClassをKv言語上(の「:」の左側)で使うのに必要な登録作業で、Meshの派生Classを使わずに以下のように

```text
Mesh:
    fmt: [(b'vPosition', 2, 'float'), ]
```

書く事が許されなかった為そうしました。Kv言語上ではなくPython上でMeshを作るのなら普通に

```python3
Mesh(fmt=[(b'vPosition', 2, 'float'), ])
```

とはできるのですが...。ここら辺はおそらく`fmt`が`__init__()`のKeyword引数ではあってもInstance化後に設定できるPropertyでは無い事が関係していると思います。

ところで`[(b'vPosition', 2, 'float'), ]`ですが、この`vPosition`がどこからきた識別子かというと、それは既定のShaderを見ると分かります。



# 既定のShader

前回の記事で調べた`RenderContext`ですが、その後`既定のRenderContext`という物があることが分かりました。この`既定のRenderContext`の持っている`Shader`が`既定のShader`です。

```python3
from kivy.core.window import Window

render_context = Window.render_context  # 既定のRenderContext
shader = render_context.shader  # 既定のShader
print(shader.vs)
print('------------------------------')
print(shader.fs)
```

出力結果は[ここ][default_shader]を見れば分かるので全ては載せませんが、とりあえず頂点Shaderに

```glsl
/* vertex attributes */
attribute vec2     vPosition;
attribute vec2     vTexCoords0;
```

があるのが分かります。これが`Mesh`の引数`fmt`に対応しています。GLSLの型`vec2`が`float`２つの意味なので、GLSLの`attribute vec2 vPosition;`が fmt引数の`(b'vPosition', 2, 'float')`になるわけです。

この既定の頂点Shaderですが、完全に2D用であることも分かります。上の`attribute vec2 vPosition;`をはじめ次の行

```glsl
 gl_Position = projection_mat * modelview_mat * vec4(vPosition.xy, 0.0, 1.0);
```

も完全に2D用だからです。なので3Dの描画には自前で頂点Shaderを書く必要がありそうです。まとめると3D描画の為には

- Meshのfmtにて頂点の座標が3Dである事を伝える。 `fmt=[(b'vPosition', 3, 'float'), ]`
- 頂点Shader側にも3D座標を受け取る為のattributes変数を用意する。 `attribute vec3 vPosiiton;`
- 頂点Shaderにて3D用の変換処理を書く

とすれば良いはずです。というわけで実践してみます。

# 実践

3D描画用のMesh命令を用意して

```python3:main.pyの一部
class CustomizedMesh(Factory.Mesh):
    def __init__(self, *args, **kwargs):
        kwargs['fmt'] = [(b'v_pos', 3, 'float'), ]  # 頂点の座標が３Dである事を伝えている
        super().__init__(*args, **kwargs)


Factory.register('CustomizedMesh', cls=CustomizedMesh)
```

GLSLを書いて

```glsl:simple.glsl
---VERTEX SHADER---
#ifdef GL_ES
    precision highp float;
#endif

attribute vec3  v_pos;  // Meshのfmt引数に対応している

uniform mat4 modelview_mat;
uniform mat4 projection_mat;

void main (void) {
    gl_Position = projection_mat * modelview_mat * vec4(v_pos, 1.0);
}

---FRAGMENT SHADER---
#ifdef GL_ES
    precision highp float;
#endif

void main(void)
{
    gl_FragColor = vec4(1.0);
}
```

書いたGLSLを適用して

```python3:main.pyの一部
    def __init__(self, **kwargs):
        self.canvas = RenderContext()  # use_parent_xxx系の引数を渡すとうまく動かなかったので削除
        self.canvas.shader.source = './simple.glsl'
        super().__init__(**kwargs)
```

射影行列を設定して

```python3
    def on_size(self, __, size):
        aspect_ratio = self.width / float(self.height)
        projection_mat = Matrix().view_clip(
            -aspect_ratio, aspect_ratio, -1, 1, 1, 100, 1)  # exampleのものを貼り付けただけなので意味は理解していません
        self.canvas['projection_mat'] = projection_mat
```

canvas命令を書いてあげると

```kivy:main.pyの一部
<Renderer>:
    canvas:
        PushMatrix:
        Translate:
            z: -2
        CustomizedMesh:
            vertices:
                [  # 立方体
                .5, .5, .5,
                .5, .5, -0.5,
                -0.5, .5, -0.5,
                -0.5, .5, .5,
                .5, -0.5, .5,
                .5, -0.5, -0.5,
                -0.5, -0.5, -0.5,
                -0.5, -0.5, .5,
                ]
            indices:
                [
                0, 1, 1, 2, 2, 3, 3, 0,
                4, 5, 5, 6, 6, 7, 7, 4,
                0, 4, 1, 5, 2, 6, 3, 7,
                ]
            mode: 'lines'  # 陰影処理も無いのに面を描画すると分かりづらくなるので、線のみを描画する
        PopMatrix:
```

それらしい物ができました。

![](https://qiita-image-store.s3.amazonaws.com/0/144673/34c0455f-4749-7139-77ce-447333ee9cdb.png)

ただこの静止画だけだと若干3Dであることが分かりづらいので、立方体を回転させ続けるようにします。
まずangleというPropertyを用意して、それが時間経過と共に増えていくようにします。

```python3:main.pyの一部
class Renderer(Factory.Widget):

    angle = NumericProperty()

    def __init__(self, **kwargs):
        self.canvas = RenderContext()
        self.canvas.shader.source = './simple.glsl'
        super().__init__(**kwargs)
        Clock.schedule_interval(self._update_angle, 0)

    def _update_angle(self, dt):
        self.angle += dt * 20
```

そしてcanvas命令に回転処理を加えます。

```kivy:main.pyの一部
<Renderer>:
    canvas:
        PushMatrix:
        Translate:
            z: -2
        Rotate:
            angle: root.angle
            axis: 0.5, 0.7, 0
```

すると立方体が回り続け、明らかに3Dの描画ができていることが確認できました。

![](https://qiita-image-store.s3.amazonaws.com/0/144673/14c8057f-7b38-854d-8c4e-00fbccfd5bba.png)

### 一つの懸念

ただ一つ気がかりな事があり、それはRenderContextへの引数`use_parent_xxx`を与えなかった事です。この引数が親の行列を使うか否かを指定する物だと私が思っているのは前の記事で書いた通りで、この引数を与えなかった時にRelativeLayoutのような行列を操作するWidgetとうまく協調してくれるかが気になるのです。試しに以下のようにRendererをRelativeLayoutの子にしてみると

```python3:main.pyの一部
root = Builder.load_string(r'''
FloatLayout:
    RelativeLayout:
        pos: 100, 100
''')
renderer = Renderer()
root.children[0].add_widget(renderer)
runTouchApp(root)
```

案の定、RelativeLayoutの持つ移動行列を無視して同じ位置に立方体が表示されてしまいました。というわけで3Dを用いながらも親の行列をちゃんと利用する方法を見つける事が次の課題になりそうです。

[source code](https://gist.github.com/gottadiveintopython/e597acfb24463418f7145f7a12e5f5fe)

# Link

[3Dのexample][3dexample]
[garde.ddd][garden.ddd]
[既定のShader][default_shader]
[MeshのDocumentation][mesh_doc]
[既定の頂点形式][default_vertex_format]

[mesh_doc]:https://kivy.org/docs/api-kivy.graphics.html#kivy.graphics.Mesh
[triangle_doc]:https://kivy.org/docs/api-kivy.graphics.html#kivy.graphics.Triangle
[3dexample]:https://github.com/kivy/kivy/tree/master/examples/3Drendering
[garden.ddd]:https://github.com/kivy-garden/garden.ddd
[default_shader]:https://github.com/kivy/kivy/tree/master/kivy/data/glsl
[default_vertex_format]:https://github.com/kivy/kivy/blob/master/kivy/graphics/vbo.pyx#L34-L35