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

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

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

前回までで(まだ挙動が不完全だが)描いた図形をマウスで回転、平行移動、スケールができるようになった。今回は、何かキーを押すと物体の表示が初期の配置に戻るようにした。
wxPythonOpenGLを利用するには、wxglcanvasモジュールを利用する。wxPython demoのGLCanvas.pyに使用方法の例があるので、それに基づいて作成している。
前回のコードpractice5.pyでは、投影変換、ビューイング変換、モデリング変換の処理が曖昧だったので、どこでどのような変換を行うべきかをきちんと整理して初期化部分(InitGL)を書き変えた。それによって、ある程度思った通りの配置で物体の描画ができるようになった。また、「x」キーを押したとき、物体の描画が初期のものに戻るようにした。「x」のキーコードは120。大文字の「X」は88。今回は小文字の「x」をこの操作のためのキーに選んだ。
以下、今回改良したコードpractice6.py。描画する物体の頂点は、頂点が多くなることを考慮して別ファイルに保存してある。

# practice6.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
XKEY = 120

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)
            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()
        elif evt.KeyCode == XKEY: # xキーが押されたときの処理
            self.x = self.y = 0  # 変数を初期化
            self.lastx = self.lasty = 0
            self.tranx = self.trany = 0
            self.InitGL()       # 初期の物体描画に戻る
            self.Refresh(True) #

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) #モデルビューマトリックスを設定対象にする
# 続いてビューイング変換を行う部分
        glLoadIdentity() #モデルビューマトリックスを単位行列で初期化する
        gluLookAt( 0.0, 0.0,  5.0,
                   0.0, 0.0,  0.0,
                   0.0, 1.0,  0.0 ) #観測者の位置指定。つまりビューイングボリューム
                   #を移動する

        glEnable(GL_LIGHTING)
        glEnable(GL_LIGHT0)
#        glColorMaterial(GL_FRONT_AND_BACK, GL_DIFFUSE)
#        glPolygonMode( GL_FRONT_AND_BACK, GL_LINE )
#        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[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

        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:])

これで「x」キーを押すと、常に起動時の描画状態に戻る。

  • 今後の課題
    • マウスによる物体操作において、回転と平行移動を何回か行っているうちに挙動がへんになるバグを修正したい。
    • 現状では、個々の操作は問題なく動く。操作を組み合わせる(回転して平行移動とか)と段々操作不能に陥る。