はじめに
公式の3Dのexampleやgarden.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)
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)
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の型vec2
がfloat
2つの意味なので、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命令を用意して
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を書いて
---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を適用して
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命令を書いてあげると
<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を用意して、それが時間経過と共に増えていくようにします。
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命令に回転処理を加えます。
<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の子にしてみると
root = Builder.load_string(r'''
FloatLayout:
RelativeLayout:
pos: 100, 100
''')
renderer = Renderer()
root.children[0].add_widget(renderer)
runTouchApp(root)
案の定、RelativeLayoutの持つ移動行列を無視して同じ位置に立方体が表示されてしまいました。というわけで3Dを用いながらも親の行列をちゃんと利用する方法を見つける事が次の課題になりそうです。