読者です 読者をやめる 読者になる 読者になる

人工知能に関する断創録

人工知能、認知科学、心理学、ロボティクス、生物学などに興味を持っています。このブログでは人工知能のさまざまな分野について調査したことをまとめています。最近は、機械学習・Deep Learningに関する記事が多いです。



平行移動・回転・拡大縮小

Pygame

ティーポットのような3次元オブジェクトを平行移動、回転、拡大縮小する方法です。平行移動、回転、拡大縮小は3次元オブジェクトの頂点座標を変換することで実現できるのでモデリング変換とかアフィン変換と呼ばれます。数学的には頂点の座標にある行列をかけることで実現しますが、OpenGLを使うだけなら詳細は知らなくても大丈夫です。

f:id:aidiary:20100814104922p:plain
f:id:aidiary:20100814104923p:plain
f:id:aidiary:20100814104924p:plain

サンプルスクリプト

#!/usr/bin/env python
#coding:utf-8
from OpenGL.GL import *
from OpenGL.GLU import *
from OpenGL.GLUT import *
import sys

angle = 0.0

def main():
    glutInit(sys.argv)
    glutInitDisplayMode(GLUT_RGB | GLUT_DOUBLE | GLUT_DEPTH)
    glutInitWindowSize(300, 300)
    glutInitWindowPosition(100, 100)
    glutCreateWindow("回転")
    glutDisplayFunc(display)
    glutReshapeFunc(reshape)
    glutIdleFunc(idle)
    init(300, 300)
    glutMainLoop()

def init(width, height):
    """初期化"""
    glClearColor(0.0, 0.0, 0.0, 1.0)
    glEnable(GL_DEPTH_TEST)
    
    glMatrixMode(GL_PROJECTION)
    glLoadIdentity()
    gluPerspective(45.0, float(width)/float(height), 0.1, 100.0)

def display():
    """描画処理"""
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
    
    glMatrixMode(GL_MODELVIEW)
    glLoadIdentity()
    gluLookAt(0.0, 0.0, 5.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0)
    
    # Y軸に沿ってangleだけ回転
    glRotatef(angle, 0.0, 1.0, 0.0)
    
    # Teapot
    glColor3f(1.0, 0.0, 0.0)
    glutSolidTeapot(1.0)
    
    glutSwapBuffers()  # バッファ切り替え

def idle():
    """アイドル時に呼ばれるコールバック関数"""
    global angle
    angle += 2.0  # 角度を更新
    glutPostRedisplay()  # 再描画

def reshape(width, height):
    """画面サイズの変更時に呼ばれるコールバック関数"""
    glViewport(0, 0, width, height)
    glMatrixMode(GL_PROJECTION)
    glLoadIdentity()
    gluPerspective(45.0, float(width)/float(height), 0.1, 100.0)

if __name__ == "__main__":
    main()

アニメーション

アニメーションは、オブジェクトを少しずつ動かしながら描画することで実現します。

def main():
    # GLUT_SINGLE → GLUT_DOUBLE (ダブルバッファリング使用)
    glutInitDisplayMode(GLUT_RGB | GLUT_DOUBLE | GLUT_DEPTH)
    glutIdleFunc(idle)  # idle関数を登録

def display():
    # オブジェクトの描画
    glutSwapBuffers()  # バッファ切り替え(ダブルバッファリング)

def idle():
    """アイドル時に呼ばれるコールバック関数"""
    global angle
    angle += 2.0  # 角度を更新
    glutPostRedisplay()  # 再描画

アニメーションさせるには、新しくidleというコールバック関数を登録します。idle()は update()と似た感じでしょうか。OpenGLがアイドル状態(暇な状態)に入るたびに自動的に呼び出されます。このidleではオブジェクトの回転角度を更新しています。定期的に回転角度が更新されるのでオブジェクトが回転しているように見えるわけです。角度を更新したら glutPostRedisplay()で画面を再描画する必要があります。

その他に画面のちらつきをおさえるためにダブルバッファリングを使っています。 glutInitDisplayMode()にGLUT_SINGLEではなく、GLUT_DOBULEを指定します。こうするとバッファがダブルになるのでちらつきが抑えられます。また、ダブルバッファリングを使うときは、glFlush()の代わりにglutSwapBuffers()を使います。実際、シングルバッファでもあんまりちらつきませんでしたが・・・

回転

まず回転です。

def display():
    """描画処理"""
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
    
    glMatrixMode(GL_MODELVIEW)
    glLoadIdentity()
    gluLookAt(0.0, 0.0, 5.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0)
    
    # Y軸に沿ってangleだけ回転
    glRotatef(angle, 0.0, 1.0, 0.0)
    
    # Teapot
    glColor3f(1.0, 0.0, 0.0)
    glutSolidTeapot(1.0)
    
    glutSwapBuffers()  # バッファ切り替え

オブジェクトの移動、回転、拡大縮小する前には、glMatrixMode(GL_MODELVIEW)と初期化のglLoadIdentity()を呼び出す必要があります。これからGL_MODELVIEWを変換していくことを意味します。回転は、

  # 原点から (x,y,z) を通る直線に関して左回りにangle度だけオブジェクトを回転
  glRotatef(angle, x, y, z)

です。(x,y,z) は、X軸を中心に回転するなら (1,0,0)、Y軸を中心に回転するなら (0,1,0)、Z軸を中心に回転するなら (0,0,1)を与えます。任意の軸も指定できます。先に説明したようにangleはグローバル変数でidle()関数が定期的に呼び出されるたびに更新されます。そのため、ティーポットは回転しているように見えるわけです。

平行移動

次に平行移動のサンプルです。サンプルスクリプトのdisplay()を下のdisplay()に置き換えてください。

def display():
    """描画処理"""
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
    
    glMatrixMode(GL_MODELVIEW)
    # カメラは原点でZの負の方向を撮影(デフォルト)
    
    # 赤いTeapot
    glLoadIdentity()
    glTranslatef(-2.0, 0.0, -10.0)   # 平行移動
    glRotatef(angle, 0.0, 1.0, 0.0)  # 回転
    glColor3f(1.0, 0.0, 0.0)
    glutSolidTeapot(1.0)
    
    # 青いTeapot
    glLoadIdentity()
    glTranslatef(2.0, 0.0, -10.0)    # 平行移動
    glRotatef(angle, 1.0, 0.0, 0.0)  # 回転
    glColor3f(0.0, 0.0, 1.0)
    glutSolidTeapot(1.0)

    glutSwapBuffers()

ここでは2つのティーポットが出てきますが、個々のオブジェクトの移動、回転はそれぞれ独立しているので移動、回転の前にglLoadIdentity()で(行列を)初期化する必要があります。平行移動は、

  # (x,y,z)だけオブジェクトを移動
  glTranslatef(x, y, z)

です。回転に比べれば単純です。平行移動と回転をどのような順番で適用するかは非常に重要です。オブジェクトを平行移動してから回転するのと回転してから平行移動するのはまったく別です。ややこしいのですが、OpenGLでは下に書いてある変換が先に実行されます。つまり、

    glTranslatef(-2.0, 0.0, -10.0)   # 平行移動
    glRotatef(angle, 0.0, 1.0, 0.0)  # 回転
    glutSolidTeapot(1.0)

のように書いてあるときはティーポットをY軸を中心に回転してから平行移動することを意味します。ためしに赤いティーポットの平行移動と回転を入れ替えてみます。この場合、ティーポットを平行移動してからY軸を中心に回転します。

    glRotatef(angle, 0.0, 1.0, 0.0)  # 回転
    glTranslatef(-2.0, 0.0, -10.0)   # 平行移動

動きが違うことがわかると思います。一般的には、オブジェクトを回転してから平行移動する方が多いと思います。またこの例では、カメラを動かさないでオブジェクトを動かしている点に注意してください。今まではカメラをZ軸の正の方向(手前)にぐーっと動かして原点のオブジェクトが映るようにしていました。今回は、カメラを動かさずオブジェクトをZ軸の負の方向(奥)にぐーっと動かしてカメラは原点に置いたままです(デフォルトではカメラは原点でZ軸の負の方向を向いています)。実は、OpenGLの世界では、オブジェクトを動かすこととカメラを逆方向に動かすのはまったく同じです。なので上のサンプルと同じ効果はカメラを動かす下のサンプルとまったく同じ効果です。

def display():
    """描画処理"""
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
    
    glMatrixMode(GL_MODELVIEW)
    # カメラは原点でZの負の方向を撮影(デフォルト)
    
    # 赤いTeapot
    glLoadIdentity()
    gluLookAt(0.0, 0.0, 10.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0)  # カメラを手前に引く
    glTranslatef(-2.0, 0.0, 0.0)   # 平行移動(オブジェクトのZはそのまま)
    glRotatef(angle, 0.0, 1.0, 0.0)  # 回転
    glColor3f(1.0, 0.0, 0.0)
    glutSolidTeapot(1.0)
    
    # 青いTeapot
    glLoadIdentity()
    gluLookAt(0.0, 0.0, 10.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0)  # カメラを手前に引く
    glTranslatef(2.0, 0.0, 0.0)    # 平行移動(オブジェクトのZはそのまま)
    glRotatef(angle, 1.0, 0.0, 0.0)  # 回転
    glColor3f(0.0, 0.0, 1.0)
    glutSolidTeapot(1.0)

    glutSwapBuffers()

このようにオブジェクトの移動とカメラ(視野)の移動は同じなのでGL_MODELVIEWというようにモデル(オブジェクト)とビュー(視野)がまとめてあるわけですね。

拡大縮小

最後に拡大縮小のサンプルです。サンプルスクリプトのdisplay()を下のdisplay()に置き換えてください。

def display():
    """描画処理"""
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
    
    glMatrixMode(GL_MODELVIEW)
    # カメラは原点でZの負の方向を撮影(デフォルト)
    
    # 赤いTeapot
    glLoadIdentity()
    glTranslatef(-2.0, 0.0, -10.0)   # 平行移動
    glRotatef(angle, 0.0, 1.0, 0.0)  # 回転
    glScale(1.0, 0.5, 1.0)           # 縮小
    glColor3f(1.0, 0.0, 0.0)
    glutSolidTeapot(1.0)
    
    # 青いTeapot
    glLoadIdentity()
    glTranslatef(2.0, 0.0, -10.0)    # 平行移動
    glRotatef(angle, 1.0, 0.0, 0.0)  # 回転
    glScale(1.0, 3.0, 1.0)           # 拡大
    glColor3f(0.0, 0.0, 1.0)
    glutSolidTeapot(1.0)

    glutSwapBuffers()

この例では、赤いティーポットは、Y軸方向を半分に縮小しています(ひらべったくなります)。一方、青いティーポットは、Y軸方向を3倍に拡大しています(のっぽになります)。拡大縮小は、

  # 各軸について (x,y,z) 倍だけ縮小拡大する
  glScale(x,y,z)

です。1より大きいと拡大、1未満だと縮小になります。1だとそのままです。ここでも変換を適用する順番は非常に重要です。一般的には、拡大縮小→回転→平行移動の順番で適用することが多いようです。