TadaoYamaokaの開発日記

個人開発しているスマホアプリや将棋AIの開発ネタを中心に書いていきます。

PyOpenGLを使ってみる その6(テクスチャに頂点配列を使う)

テクスチャに頂点配列を使う。

import sys
from OpenGL.GL import *
from OpenGL.GLU import *
from OpenGL.GLUT import *
from PIL import Image

def load_texture():
    img = Image.open("sample2.png")
    w, h = img.size
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, w, h, 0, GL_RGBA, GL_UNSIGNED_BYTE, img.tobytes())

def display():
    glClear(GL_COLOR_BUFFER_BIT)

    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
    glEnable(GL_BLEND)

    glEnableClientState(GL_VERTEX_ARRAY)
    glEnableClientState(GL_TEXTURE_COORD_ARRAY)

    glEnable(GL_TEXTURE_2D)

    glVertexPointerf([
        (-1.0, -1.0, 0.0),
        ( 1.0, -1.0, 0.0),
        (-1.0,  1.0, 0.0),
        ( 1.0,  1.0, 0.0),
        ])
    glTexCoordPointerf([
        (0.0, 1.0),
        (1.0, 1.0),
        (0.0, 0.0),
        (1.0, 0.0),
        ])

    glDrawArrays(GL_TRIANGLE_STRIP, 0, 4)

    glDisable(GL_TEXTURE_2D)

    glDisable(GL_BLEND)

    glFlush()

def main():
    glutInit(sys.argv)
    glutInitDisplayMode(GLUT_RGBA)
    glutInitWindowSize(300, 300)
    glutCreateWindow(b"TextureSample3")
    glutDisplayFunc(display)
    glClearColor(0.0, 0.0, 1.0, 1.0)

    load_texture()
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR)
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR)

    glutMainLoop()

if __name__ == '__main__':
    main()
解説
    glEnableClientState(GL_TEXTURE_COORD_ARRAY)

で、テクスチャの頂点配列を有効にする。

    glTexCoordPointerf([
        (0.0, 1.0),
        (1.0, 1.0),
        (0.0, 0.0),
        (1.0, 0.0),
        ])

で、テクスチャの頂点配列を定義する。
glVertexPointerfで定義したテクスチャを貼るポリゴンの頂点と順番を一致させる。

    glDrawArrays(GL_TRIANGLE_STRIP, 0, 4)

で、テクスチャを描画する。
この例で、GL_TRIANGLE_STRIPで四角形を描画している。

実行結果

f:id:TadaoYamaoka:20170307065527p:plain

PyOpenGLを使ってみる その5(頂点配列にnumpyを使う)

前回は頂点配列にPythonの組み込みのタプルとリストを使用したが、numpyを使うこともできる。
座標変換はOpenGLの機能で行った方が簡単だが、場合によってはnumpyを使用できると便利である。

import sys
from OpenGL.GL import *
from OpenGL.GLU import *
from OpenGL.GLUT import *
import numpy as np

def display():
    glClear(GL_COLOR_BUFFER_BIT)

    glEnableClientState(GL_VERTEX_ARRAY)

    glColor3f(1.0, 0.0, 0.0)

    glVertexPointerf(np.array([
        [ 0.0,  1.0, 0.0],
        [-0.5,  0.0, 0.0],
        [ 0.5,  0.0, 0.0],
        ], dtype=np.float))
    glDrawArrays(GL_TRIANGLES, 0, 3)

    glColor3f(0.0, 1.0, 0.0)
    glVertexPointerf(np.array([
        [-0.5,  0.0, 0.0],
        [-0.5, -1.0, 0.0],
        [ 0.5,  0.0, 0.0],
        [ 0.5, -1.0, 0.0],
        ], dtype=np.float))
    glDrawArrays(GL_TRIANGLE_STRIP, 0, 4)

    glFlush()

def main():
    glutInit(sys.argv)
    glutInitDisplayMode(GLUT_RGBA)
    glutInitWindowSize(300, 300)
    glutCreateWindow(b"sample3")
    glutDisplayFunc(display)
    glClearColor(0.0, 0.0, 1.0, 1.0)

    glutMainLoop()

if __name__ == '__main__':
    main()

glVertexPointerfに渡すndarrayのshapeは、(N, 3)である必要がある。

PyOpenGLを使ってみる その4(頂点配列)

頂点配列を使って描画を行う。

import sys
from OpenGL.GL import *
from OpenGL.GLU import *
from OpenGL.GLUT import *

def display():
    glClear(GL_COLOR_BUFFER_BIT)

    glEnableClientState(GL_VERTEX_ARRAY)

    glColor3f(1.0, 0.0, 0.0)
    glVertexPointerf([
        ( 0.0,  1.0, 0.0),
        (-0.5,  0.0, 0.0),
        ( 0.5,  0.0, 0.0),
        ])
    glDrawArrays(GL_TRIANGLES, 0, 3)

    glColor3f(0.0, 1.0, 0.0)
    glVertexPointerf([
        (-0.5,  0.0, 0.0),
        (-0.5, -1.0, 0.0),
        ( 0.5,  0.0, 0.0),
        ( 0.5, -1.0, 0.0),
        ])
    glDrawArrays(GL_TRIANGLE_STRIP, 0, 4)

    glFlush()

def main():
    glutInit(sys.argv)
    glutInitDisplayMode(GLUT_RGBA)
    glutInitWindowSize(300, 300)
    glutCreateWindow(b"sample2")
    glutDisplayFunc(display)
    glClearColor(0.0, 0.0, 1.0, 1.0)

    glutMainLoop()

if __name__ == '__main__':
    main()
解説

頂点配列を使用するには、

    glEnableClientState(GL_VERTEX_ARRAY)

で、頂点配列を有効にする。

次に、頂点配列を定義する。PyOpenGLでは、glVertexPointerfを使うことで、頂点のタプルのリストをPythonの組み込み型のまま直接使用できる。
上記の例では、三角形の頂点を

    glVertexPointerf([
        ( 0.0,  1.0, 0.0),
        (-0.5,  0.0, 0.0),
        ( 0.5,  0.0, 0.0),
        ])

で定義している。

次に、定義した頂点配列を使用して、glDrawArraysで三角形を描画する。引数にGL_TRIANGLESを指定することで三角形の描画になる。

GL_TRIANGLES以外にもいくつか描画方法がある。詳しくは、マニュアルを参照。

GL_TRIANGLE_STRIP

GL_TRIANGLE_STRIPを使用すると四角形を描画することができる。
4つの頂点を定義して、頂点番号(0, 1, 2)と(1, 2, 3)の2つの三角形を描画する。
上記の例では、

    glVertexPointerf([
        (-0.5,  0.0, 0.0),
        (-0.5, -1.0, 0.0),
        ( 0.5,  0.0, 0.0),
        ( 0.5, -1.0, 0.0),
        ])

で、4つの頂点を定義して、

    glDrawArrays(GL_TRIANGLE_STRIP, 0, 4)

で、GL_TRIANGLE_STRIPを使用して四角を描画している。

実行結果

f:id:TadaoYamaoka:20170305124717p:plain

PyOpenGLを使ってみる その3(移動と回転)

前回表示したテクスチャを移動と回転させる。

import sys
from OpenGL.GL import *
from OpenGL.GLU import *
from OpenGL.GLUT import *
from PIL import Image

def load_texture():
    img = Image.open("sample1.png")
    w, h = img.size
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, w, h, 0, GL_RGBA, GL_UNSIGNED_BYTE, img.tobytes())

def display():
    glClear(GL_COLOR_BUFFER_BIT)

    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
    glEnable(GL_BLEND)

    glEnable(GL_TEXTURE_2D)

    glPushMatrix()
    glTranslatef(0.0, 0.5, 0.0)
    glRotatef(90.0, 0.0, 0.0, 1.0)

    glBegin(GL_QUADS)
    glTexCoord2d(0.0, 1.0)
    glVertex3d(-0.5, -0.5,  0.0)
    glTexCoord2d(1.0, 1.0)
    glVertex3d( 0.5, -0.5,  0.0)
    glTexCoord2d(1.0, 0.0)
    glVertex3d( 0.5,  0.5,  0.0)
    glTexCoord2d(0.0, 0.0)
    glVertex3d(-0.5,  0.5,  0.0)
    glEnd()

    glPopMatrix()

    glDisable(GL_TEXTURE_2D)

    glDisable(GL_BLEND)

    glFlush()

def main():
    glutInit(sys.argv)
    glutInitDisplayMode(GLUT_RGBA)
    glutInitWindowSize(300, 300)
    glutCreateWindow(b"TextureSample1")
    glutDisplayFunc(display)
    glClearColor(0.0, 0.0, 1.0, 1.0)

    load_texture()
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR)
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR)

    glutMainLoop()

if __name__ == '__main__':
    main()
解説

表示するテクスチャを貼った四角の座標はローカル座標系で指定するため、行列スタックを使用して、現在の座標系(この例では、ワールド座標系)を保存する。

    glPushMatrix()

ローカル座標系での操作が終わった後、

    glPopMatrix()

で保存時の座標系(この例では、ワールド座標系)に復元する。
この例ではローカル座標系は1つしか扱っていないので、保存しなくても問題ない。
2つ以上のローカル座標系を扱う場合や親子関係のオブジェクトを扱う場合は行列スタックが必要になる。


移動は、

    glTranslatef(0.0, 0.5, 0.0)

で行う。

回転は、

    glRotatef(90.0, 0.0, 0.0, 1.0)

で行う。
この例では、Z軸周りに90度(右回り)回転させて、Y軸方向に0.5移動している。

移動の行列をA_2、回転をA_1、頂点のベクトルを\mathbf{x}とすると、ローカル座標系からワールド座標系への変換(モデリング変換)は、A_2 A_1 \mathbf{x}の式で計算される。
行列の積に交換法則は成立しないので、A_2A_1を逆にできないことに注意する。
回転してから移動と、移動してから回転では、回転の原点が異なることは直観的に理解できる。

補足

初期状態では、モデリング変換は、単位行列になっている。
何か操作を行った後に、単位行列に初期化したい場合は、

    glLoadIdentity()

を実行する。

透視変換の行列の操作をした後は、

    glMatrixMode(GL_MODELVIEW)

モデリング変換の行列を選択することを忘れないようにする。

実行結果

f:id:TadaoYamaoka:20170304162124p:plain

PyOpenGLを使ってみる その2(テクスチャ)

PyOpenGLでテクスチャを表示する。

import sys
from OpenGL.GL import *
from OpenGL.GLU import *
from OpenGL.GLUT import *
from PIL import Image

def load_texture():
    img = Image.open("sample1.png")
    w, h = img.size
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, w, h, 0, GL_RGBA, GL_UNSIGNED_BYTE, img.tobytes())

def display():
    glClear(GL_COLOR_BUFFER_BIT)

    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
    glEnable(GL_BLEND)

    glEnable(GL_TEXTURE_2D)

    glBegin(GL_QUADS)
    glTexCoord2d(0.0, 1.0)
    glVertex3d(-1.0, -1.0,  0.0)
    glTexCoord2d(1.0, 1.0)
    glVertex3d( 1.0, -1.0,  0.0)
    glTexCoord2d(1.0, 0.0)
    glVertex3d( 1.0,  1.0,  0.0)
    glTexCoord2d(0.0, 0.0)
    glVertex3d(-1.0,  1.0,  0.0)
    glEnd()

    glDisable(GL_TEXTURE_2D)

    glDisable(GL_BLEND)

    glFlush()

def main():
    glutInit(sys.argv)
    glutInitDisplayMode(GLUT_RGBA)
    glutInitWindowSize(300, 300)
    glutCreateWindow(b"TextureSample1")
    glutDisplayFunc(display)
    glClearColor(0.0, 0.0, 1.0, 1.0)

    load_texture()
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR)
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR)

    glutMainLoop()

if __name__ == '__main__':
    main()

テクスチャに使う画像は、PILを使用してロードし、tobytes()でバイト列にして、glTexImage2Dに渡す。
画像のフォーマットは、PILで対応していればなんでもよいが、透明を含む画像の場合は、PNGを使う。

上記のコード例では、透明を含む画像を読み込んで、四角のポリゴンに張り付けている。

透明画像を表示する場合は、glutInitDisplayModeにGL_RGBAを指定し、glTexImage2Dでテクスチャを作成する際にもGL_RGBAを指定する。
また、レンダリング時に、

    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
    glEnable(GL_BLEND)

でアルファブレンドを有効にする。

テクスチャの座標系( (s, t)座標 )は、画像の左下が原点で座標の範囲が(0, 0) - (1, 1)となる。
画像の上下が反転することに注意する。

実行結果

f:id:TadaoYamaoka:20170304134534p:plain

PyOpenGLを使ってみる

前回の日記でPyOpenGLの導入を行った。

2D画像の変形をOpenGLを使って行うことが目標だが、現時点でOpenGLの知識がほとんどない。
画像の変形はFree-Form Deformationという方法でポリゴンの頂点を変形させることで実現できそうだが、あせらず基礎的なことから試していこうと思う。

ひとまず画面に豆腐(白い四角)を表示させてみる。

GLUTを使ったウィンドウを表示するだけのスケルトンプログラムは以下の通り記述する。

import sys
from OpenGL.GL import *
from OpenGL.GLU import *
from OpenGL.GLUT import *

def display():
    glClear(GL_COLOR_BUFFER_BIT)
    glFlush()

def main():
    glutInit(sys.argv)
    glutInitDisplayMode(GLUT_RGB)
    glutInitWindowSize(300, 300)
    glutCreateWindow(b"skeleton")
    glutDisplayFunc(display)

    glutMainLoop()

if __name__ == '__main__':
    main()

これを元にコードを付け足していく。

白い四角を表示させる。

import sys
from OpenGL.GL import *
from OpenGL.GLU import *
from OpenGL.GLUT import *

def display():
    glClear(GL_COLOR_BUFFER_BIT)

    glColor3f(1.0, 1.0, 1.0) # 白色
    glBegin(GL_POLYGON)
    glVertex3f(0.5, 0.5, 0.0)
    glVertex3f(-0.5, 0.5, 0.0)
    glVertex3f(-0.5, -0.5, 0.0)
    glVertex3f(0.5, -0.5, 0.0)
    glEnd()

    glFlush()

def main():
    glutInit(sys.argv)
    glutInitDisplayMode(GLUT_RGB)
    glutInitWindowSize(300, 300)
    glutCreateWindow(b"sample1")
    glutDisplayFunc(display)

    glutMainLoop()

if __name__ == '__main__':
    main()

四角が表示される。
f:id:TadaoYamaoka:20170302221859p:plain

投影法

上記のコードでは投影法について指定していないが、デフォルトで平行投影となる。
mainに以下のコードを書いても同じである。

    glMatrixMode(GL_PROJECTION)
    glLoadIdentity()
    glOrtho(-1.0, 1.0, -1.0, 1.0, -1.0, 1.0)

glOrthoのOrthoは、Orthogonal projection(平行投影、直交射影)の略である。
指定した直方体(ビューボリューム)の中にある物体をそのままの大きさでスクリーンに投影する。

なお、glMatrixMode(GL_PROJECTION)は、投影変換の行列スタックを選択し、glLoadIdentity()でスタックをクリアして、glOrthoで新たな投影変換の行列を設定している。

座標系

頂点の座標を指定する際は、座標系を理解しておく必要がある。
他の解説サイトがあるので、リンクを張っておく。

http://www.arakin.dyndns.org/gl_coord.php

DirectXとは座標系が異なるので注意が必要である。
面は頂点が反時計回りになるように定義する。

WindowsのPythonでOpenGLを使う

写真の線画化を、iBUG 300-Wの300枚の画像に試してみたけど、あまりきれいな線画が生成できなかった。
学習データの生成には他のアプローチを試したい。
Live2Dのような方法で、線画のキャラクタの向きや表情のバリエーションを増やせないかと考えている。
Live2Dは2Dの画像を変形させるために、ポリゴンを使用しているようなので、OpenGLを使って同じようなことを試そうと思う。

OpenGLC++の情報が多いが、最近Pythonの使いやすさに慣れてしまったので、OpenGLPythonから使えたらそちらを使いたい。
PythonOpenGLを使うには、PyOpenGLというライブラリがあるようだ。
ということで、PyOpenGLを試してみた。

下記の例では、ウィンドウの表示部分にGLUTを使用した。
PyQt4を使うこともできるようなので、後で試したい。

実行環境

PyOpenGLのインストール

公式の説明通り、pipでPyOpenGLをインストールする。

pip install PyOpenGL PyOpenGL_accelerate

FreeGLUTのインストール

PyOpenGLとは別に、GLUTをインストールする必要がある。
GLUTは、OpenGLをプラットフォーム非依存で扱うためのライブラリのようだ。

本家のGLUTは、Windowsの64bitのバイナリがダウンロードできない。
代わりに、64bitのバイナリが提供されているFreeGLUTを導入する。

FreeGLUTのページから
Martin Payne's Windows binaries (MSVC and MinGW)
のリンクをたどり、
Download freeglut 3.0.0 for MSVC
からfreeglut-MSVC-3.0.0-2.mp.zipをダウンロードする。

zipファイル内のfreeglut\bin\x64\にあるfreeglut.dllをC:\Windows\System32にコピーする。

これで、GLUTが使用できるようになる。

サンプル実行

こちらのページを参考に、ティーポットを表示してみる。
pyOpenGL を Windows8 (64bit) にインストールする | n3956.net

from OpenGL.GL import *
from OpenGL.GLU import *
from OpenGL.GLUT import *
import sys

def main():
    glutInit(sys.argv)
    glutInitDisplayMode(GLUT_RGB | GLUT_SINGLE | GLUT_DEPTH)
    glutInitWindowSize(300, 300)     # window size
    glutInitWindowPosition(100, 100) # window position
    glutCreateWindow(b"teapot")      # show window
    glutDisplayFunc(display)         # draw callback function
    glutReshapeFunc(reshape)         # resize callback function
    init(300, 300)
    glutMainLoop()

def init(width, height):
    """ initialize """
    glClearColor(0.0, 0.0, 0.0, 1.0)
    glEnable(GL_DEPTH_TEST) # enable shading

    glMatrixMode(GL_PROJECTION)
    glLoadIdentity()
    ##set perspective
    gluPerspective(45.0, float(width)/float(height), 0.1, 100.0)

def display():
    """ display """
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
    glMatrixMode(GL_MODELVIEW)
    glLoadIdentity()
    ##set camera
    gluLookAt(0.0, 1.0, 5.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0)
    ##draw a teapot
    glColor3f(1.0, 0.0, 0.0)
    glutWireTeapot(1.0)   # wireframe
#    glutSolidTeapot(1.0)  # solid
    glFlush()  # enforce OpenGL command

def reshape(width, height):
    """callback function resize window"""
    glViewport(0, 0, width, height)
    glMatrixMode(GL_PROJECTION)
    glLoadIdentity()
    gluPerspective(45.0, float(width)/float(height), 0.1, 100.0)

if __name__ == "__main__":
    main()
注意点

glutCreateWindowの引数にPythonの文字列をそのまま渡すと、TypeErrorになる。
Python2ではそのまま渡すことができるが、Python3で文字列がデフォルトでunicode文字列になったため、文字列リテラルの前にbを付けて、バイト列として渡す必要がある。

実行結果

f:id:TadaoYamaoka:20170228064226p:plain