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

PythonOpenGLライブラリを用いて、3次元図形の描画を行う練習をしている。
マウスで物体を回転したり平行移動したりしたかったのだが、回転と平行移動を組み合わせるとうまくいかない。そこで、とりあえずは平行移動をあきらめて、現状のコードをまとめる。

  • このコードについて
    • 外部ファイルに頂点の座標を定義しており、それを読み込んで物体を描画している。
    • wxPythonのwxglcanvasモジュールを利用している。
    • 描いた物体をマウスで回転、スケールの操作ができる。回転は左ボタンを押しながらドラッグする。スケールはマウスホイールを回すことで実現できる。
    • 物体の配置を初期の位置に戻すには、キーボードのxキー(小文字)を押す。

現状でベストなコードは以下の通り。

# practice6mod.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:
            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):
        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

        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)

        glPushMatrix()
        glCallList(self.index)
        glPopMatrix()

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