描画した図形をマウスで操作する(7)

PythonOpenGLライブラリを用いて、3次元図形の描画を行う練習をしている。とりあえずは、次のプロセスに沿っていろいろと機能を試して練習を進める。

  1. なんでもいいので3次元の図形を描画する(practice1.py)
  2. 描いた図形をマウスで回転できるようにする(practice2mod.py)
  3. 表面が連続した四角形からなる物体を描画する(practice3.py)
  4. それをマウスで回転できるようにする(practice3.py)
  5. wxPythonGUIを作成し、図形を描画できるようにする(practice4.py)
  6. 描いた物体をマウスで平行移動したり、スケールしたりする(practice5.py) <--今、ここ。
  7. その他

今回は、http://d.hatena.ne.jp/Megumi221/20110411 で作成したpractice4.pyを改良してマウスで操作できるようにする。wxPythonOpenGLを利用するには、wx.glcanvasモジュールを利用する。wxPyhton demoのGLCanvas.pyに使用方法の例があるので、それに基づいて作成している。
以下、今回改良したコードpractice5.py。マウスのイベントハンドラの部分を中心に書き加えた。描画する物体の頂点の座標は、頂点が多くなることを考慮して別ファイルに保存してあるものとする。

# practice5.py
import wx
import sys
import math

from wx import glcanvas
from OpenGL.GL import *
from OpenGL.GLUT import *
from OpenGL.GLU import *

ESCAPE = 27

class ButtonPanel(wx.Panel):       #ボタンがあるだけのパネルを作る
    def __init__(self, parent, log):
        wx.Panel.__init__(self, parent, -1)
        self.log = log

        box = wx.BoxSizer(wx.VERTICAL)
        box.Add((20, 30))
        btn = wx.Button(self, 0, 'My')  #ボタンを押すと描画ウィンドウが開く
        box.Add(btn, 0, wx.ALIGN_CENTER|wx.ALL, 15)
        self.Bind(wx.EVT_BUTTON, self.OnButton, btn)
        self.SetAutoLayout(True)
        self.SetSizer(box)

    def OnButton(self, evt):
        canvasClass = eval('MyCanvas')
        frame = wx.Frame(None, -1, 'OpenGL', size=(400,400)) #描画ウィンドウ
        canvas = canvasClass(frame)
        frame.Show(True)

class MyCanvasBase(glcanvas.GLCanvas):
    def __init__(self, parent):
        glcanvas.GLCanvas.__init__(self, parent, -1)
        self.init = False
        self.lastx = self.x = 0
        self.lasty = self.y = 0
        self.tranx = self.trany = 0
        self.zooming = 1.0
        self.size = None
        self.Bind(wx.EVT_ERASE_BACKGROUND, self.OnEraseBackground)
        self.Bind(wx.EVT_SIZE, self.OnSize)
        self.Bind(wx.EVT_PAINT, self.OnPaint)
        self.Bind(wx.EVT_LEFT_DOWN, self.OnMouseDown) #マウス左ボタンを押すイベント
        self.Bind(wx.EVT_LEFT_UP, self.OnMouseUp)   #マウス右ボタンを押すイベント
        self.Bind(wx.EVT_RIGHT_DOWN, self.OnMouseDown)
        self.Bind(wx.EVT_RIGHT_UP, self.OnMouseUp)
        self.Bind(wx.EVT_MIDDLE_DOWN, self.OnMouseDown)
        self.Bind(wx.EVT_MIDDLE_UP, self.OnMouseUp)
        self.Bind(wx.EVT_MOTION, self.OnMouseMotion) #マウスを動かすイベント
        self.Bind(wx.EVT_CHAR, self.OnKeyboard) #キーを入力するイベント
#        self.Bind(wx.EVT_IDLE, self.OnIdle)
        self.Bind(wx.EVT_MOUSEWHEEL, self.OnMouseWheel) #マウスホイールを動かすイベント

    def OnEraseBackground(self, event):
        pass

    def OnSize(self, event):     #描画ウィンドウのリサイズ
        size = self.size = self.GetClientSize()
        if self.GetContext():
            self.SetCurrent()
            glViewport(0, 0, size.width, size.height) #ビューポートの再設定
        event.Skip()

    def OnPaint(self, event):
        dc = wx.PaintDC(self)
        self.SetCurrent()
        if not self.init:
            self.InitGL()
            self.init = True
        self.OnDraw()

    def OnMouseDown(self, evt):
        self.CaptureMouse()
        self.x, self.y = self.lastx, self.lasty = evt.GetPosition()

    def OnMouseUp(self, evt):
        self.ReleaseMouse()

    def OnMouseMotion(self, evt):
        if evt.Dragging() and evt.LeftIsDown(): #マウスの左ボタンによるドラッグ
            self.lastx, self.lasty = self.x, self.y #物体の回転のための処理
            self.x, self.y = evt.GetPosition()
            self.Refresh(False)
        elif evt.Dragging() and evt.RightIsDown(): #マウスの右ボタンによるドラッグ
            lastx, lasty = self.x, self.y
            x, y = evt.GetPosition()
            self.tranx = 0.001*(x - lastx)     #物体の平行移動のための処理
            self.trany = 0.001*(lasty - y)#ワールド空間と描画ウィンドウでy方向が逆になる
            self.Refresh(False)

    def OnMouseWheel(self, evt):     #マウスホイールを回す
        if evt.GetWheelRotation() > 0:  #奥に回す
            self.zooming -= 0.1
            self.Refresh(False)
        else:               #手前に回す
            self.zooming += 0.1
            self.Refresh(False)
     
    def OnKeyboard(self, evt):      #キーを押したとき
        if evt.KeyCode == ESCAPE:
            sys.exit()

class MyCanvas(MyCanvasBase):
    def InitGL(self):
        glClearDepth(1.0)
        glEnable(GL_DEPTH_TEST)
        glClearColor(0.0, 0.5, 0.0, 0.0)
        glShadeModel(GL_SMOOTH)

        glMatrixMode(GL_PROJECTION)
        glLoadIdentity()
        gluPerspective( 30.0, 1.0, 1.0, 10.0)
        glMatrixMode(GL_MODELVIEW)

        glTranslatef(0.0, 0.0, -5.0)
#        glRotatef(self.y, 1.0, 0.0, 0.0)
#        glRotatef(self.x, 0.0, 1.0, 0.0)
        glEnable(GL_LIGHTING)
        glEnable(GL_LIGHT0)
        glColorMaterial(GL_FRONT_AND_BACK, GL_DIFFUSE)
        glEnable(GL_COLOR_MATERIAL)

        self.vpos = list()
        self.readpos()
        self.displaylist()

    def readpos(self):  #外部ファイルから頂点の座標を読み込む
        fp = open('coordpos.txt', 'r')
        num = int(fp.readline())
        poss = fp.read().splitlines()
        for i in xrange(num):
            self.vpos.append([float(x) for x in poss[i].split(',')])
        fp.close()

    def crossproduct(self, a, b, c): #法線ベクトルの計算
        cross = [0.0, 0.0, 0.0]
        cross[0] = (a[1]-b[1])*(c[2]-b[2]) - (a[2]-b[2])*(c[1]-b[1])
        cross[1] = (a[2]-b[2])*(c[0]-b[0]) - (a[0]-b[0])*(c[2]-b[2])
        cross[2] = (a[0]-b[0])*(c[1]-b[1]) - (a[1]-b[1])*(c[0]-b[0])
        cabs = math.sqrt(cross[0]**2+cross[1]**2+cross[2]**2)
        cc = [x/cabs for x in cross]
        return cc

    def displaylist(self):
        ps = self.vpos
        self.index = glGenLists(1)
        glNewList(self.index, GL_COMPILE_AND_EXECUTE)

        glBegin(GL_QUADS)  #物体の描画

        glNormal3fv(self.crossproduct(ps[4],ps[1],ps[0])) #法線ベクトル
        glVertex3fv(ps[0]) #面を構成する頂点の座標
        glVertex3fv(ps[1])
        glVertex3fv(ps[4])
        glVertex3fv(ps[3])

        glNormal3fv(self.crossproduct(ps[5],ps[2],ps[1]))
        glVertex3fv(ps[1])
        glVertex3fv(ps[2])
        glVertex3fv(ps[5])
        glVertex3fv(ps[4])
# ...
# (頂点が多いので、中略)
# ...
        glNormal3fv(self.crossproduct(ps[25],ps[16],ps[15]))
        glVertex3fv(ps[15])
        glVertex3fv(ps[16])
        glVertex3fv(ps[25])
        glVertex3fv(ps[24])

        glEnd()

        glEndList()

    def OnDraw(self):
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
        glPushMatrix()

        glCallList(self.index)
        glPopMatrix()

        if self.size is None:
            self.size = self.GetClientSize()
        w, h = self.size    #描画ウィンドウの長さを取得
        w = max(w, 1.0)
        h = max(h, 1.0)
        xScale = 180.0 / w
        yScale = 180.0 / h
        glRotatef((self.y - self.lasty) * yScale, 1.0, 0.0, 0.0); #モデル座標系の回転
        glRotatef((self.x - self.lastx) * xScale, 0.0, 1.0, 0.0);
        glTranslatef(self.tranx, self.trany, 0)     #モデル座標系の平行移動
        glScalef(self.zooming, self.zooming, self.zooming) #モデル座標系のスケール
        self.zooming = 1.0  #スケール率を毎回1に戻す

        self.SwapBuffers()

def runTest(frame, nb, log):
    win = ButtonPanel(nb, log)
    return win

if __name__ == '__main__':
    import sys,os
    import run
    run.main(['', os.path.basename(sys.argv[0])] + sys.argv[1:])

実行方法は、wxPython demoのrun.pyをそのまま使って、

$ python run.py practice5.py

とする。

▲起動画面。「My」ボタンを押すと描画ウィンドウが開く。

▲マウスの左ボタンを押しながらドラッグすると、物体が回転する。

▲マウスのホイールを回すと物体が拡大、縮小表示される。奥に回すと縮小。手前に回すと拡大。

▲マウスの右ボタンを押しながらドラッグすると物体が平行移動するはずが、思うように動いてくれない。大きさが変わったりしてしまう。ここでやっていることは、モデリング座標系の平行移動なのだが、透視投影変換している(gluPerspective)ので、可視領域(ビューイングボリューム)の形状が踏み台が捩じれたような形をしている。そのため、単に平行移動させただけでは平行移動に見えないのだろうな。きっと。平行投影変換であれば、ちゃんと平行移動するように見えるはず。