Kivy

KivyでGLSL #3 Mesh

はじめに

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

簡單な物を書いてみる

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

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

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

頂点の形式を指定

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

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

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

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

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

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

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

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

既定のShader

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

from kivy.core.window import Window

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

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

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

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

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

 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命令を用意して

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


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

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を適用して

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

射影行列を設定して

    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命令を書いてあげると

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:

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

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

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命令に回転処理を加えます。

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

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

一つの懸念

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

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

main.py
simple.glsl

Link

3Dのexample
garde.ddd
既定のShader
MeshのDocumentation
既定の頂点形式