processingの3Dモードでカメラを自由に操る

processingの3Dモードで立体物を作るのは簡単だが、そのままでは一方向からの視点でしか見ることができない。カメラのアングルを変更するプログラミングは多少やるとして、ゲームのような3D空間を簡単に見る操作を作るとなると大変だ。そこで、カメラを自由に操るためのクラスを用意する。

アルゴリズムは非常に難しいので今回は丸パクリさせてもらうことにした。詳細を理解したい人は頑張って行列(高校数学)を勉強しよう!

マウス操作によるカメラ移動 with Processing

参考までにこちらも気になった
Processingでキー入力によるカメラ操作をする

サンプルプログラムはJavaで書かれているのでPythonに書き直した。

import copy

class MouseCamera(object):
    def __init__(self, radius, eyeX, eyeY, eyeZ, centerX, centerY, centerZ, upX, upY, upZ):
        self.eyeX, self.eyeY, self.eyeZ = 0.0, 0.0, 0.0
        self.centerX, self.centerY, self.centerZ = 0.0, 0.0, 0.0
        self.upX, self.upY, self.upZ = 0.0, 0.0, 0.0
        # self.eyeX = 0.0
        # self.eyeY = 0.0
        # self.eyeZ = 0.0
        # self.centerX = 0.0
        # self.centerY = 0.0
        # self.centerZ = 0.0
        # self.upX = 0.0
        # self.upY = 0.0
        # self.upZ = 0.0
        self.radius = 0.0 # 仮想的な球の半径
        self.matrix = None # 行列
        self.preVector = PVector() # PVectorはProcessingの組み込みクラス

    # def MouseCamera(self, radius = 0.0):
    #     self(radius, width / 2.0, height / 2.0, (height / 2.0)/tan(PI*30.0/180.0), width / 2.0, height / 2.0, 0, 0, 1, 0)

        self.radius = radius
        self.eyeX = eyeX
        self.eyeY = eyeY
        self.eyeZ = eyeZ
        self.centerX = centerX
        self.centerY = centerY
        self.centerZ = centerZ
        self.upX = upX
        self.upY = upY
        self.upZ = upZ
        self.matrix = self.getIdentityMatrix()

    # 毎フレーム処理
    def update(self):
        beginCamera()
        camera(self.eyeX, self.eyeY, self.eyeZ, self.centerX, self.centerY, self.centerZ, self.upX, self.upY, self.upZ)
        applyMatrix(self.matrix[0][0], self.matrix[0][1], self.matrix[0][2], self.matrix[0][3],
                    self.matrix[1][0], self.matrix[1][1], self.matrix[1][2], self.matrix[1][3],
                    self.matrix[2][0], self.matrix[2][1], self.matrix[2][2], self.matrix[2][3],
                    self.matrix[3][0], self.matrix[3][1], self.matrix[3][2], self.matrix[3][3])
        endCamera()

    # クリックしたときに呼び出す関数
    def mousePressed(self):
        # 右ボタンをクリックをしたときの処理
        if mouseButton == RIGHT:
            matrix = self.getIdentityMatrix()
        # 左ボタンをクリックをしたときの処理
        elif mouseButton == LEFT:
            self.preVector = self.mouseOnSphere(mouseX-width / 2.0, mouseY-height / 2.0)
        # 中ボタンをクリックしたときの処理
        elif mouseButton == CENTER:
            self.preVector = PVector(mouseX-width / 2.0, mouseY-height / 2.0)

    # ドラッグしたときに呼び出す関数
    def mouseDragged(self):
        # 左ボタンをドラッグしたときの処理
        if mouseButton == LEFT:
            v = self.mouseOnSphere(mouseX-width / 2.0, mouseY-height / 2.0)
            self.matrix = self.mult(self.getRotationMatrix(self.preVector, v), self.matrix)
            self.preVector = v
            
        # 中ボタンをドラッグしたときの処理
        elif mouseButton == CENTER:
            v = PVector(mouseX-width / 2.0, mouseY-height / 2.0)
            self.matrix = self.mult(self.getTranslationMatrix(self.preVector, v), self.matrix)
            self.preVector = v

    # マウスホイールを動かしたときに呼び出す関数
    def mouseWheel(self, event):
        self.matrix = self.mult(self.getScaleMatrix(event.getCount()), self.matrix)

    # 単位行列の取得
    def getIdentityMatrix(self):
        return [[1.0, 0.0, 0.0, 0.0],
                [0.0, 1.0, 0.0, 0.0],
                [0.0, 0.0, 1.0, 0.0],
                [0.0, 0.0, 0.0, 1.0]]

    # 回転行列の取得
    def getRotationMatrix(self, v1, v2):
        v1tmp = copy.copy(v1) # 保護するためにコピーして使う(normalizeの挙動がjavaと違うのか、値渡しができない影響か、オブジェクトの生成の挙動の違いかわからん)
        v = v1tmp.cross(v2).normalize() # 回転軸
        c = v1.dot(v2) # cos
        s = v1.cross(v2).mag() # sin
        return [[c + v.x*v.x*(1.0-c), v.x*v.y*(1.0-c) - v.z*s, v.x*v.z*(1.0-c) + v.y*s, 0.0],
                [v.y*v.x*(1.0-c) + v.z*s, c + v.y*v.y*(1.0-c), v.y*v.z*(1.0-c) - v.x*s, 0.0],
                [v.z*v.x*(1.0-c) - v.y*s, v.z*v.y*(1.0-c) + v.x*s, c + v.z*v.z*(1.0-c), 0.0],
                [0.0, 0.0, 0.0, 1.0]]

    # 平行移動行列の取得
    def getTranslationMatrix(self, v1, v2):
        return [[1.0, 0.0, 0.0, v2.x-v1.x],
                [0.0, 1.0, 0.0, v2.y-v1.y],
                [0.0, 0.0, 1.0, v2.z-v1.z],
                [0.0, 0.0, 0.0, 1.0]]

    # 拡大縮小行列の取得
    def getScaleMatrix(self, wheelCount):
        temp = 10.0 # wheelCountの値が大きいので定数で割る
        return [[exp(-wheelCount/temp), 0.0, 0.0, 0.0],
                [0.0, exp(-wheelCount/temp), 0.0, 0.0],
                [0.0, 0.0, exp(-wheelCount/temp), 0.0],
                [0.0, 0.0, 0.0, 1.0]]

    # マウスの座標から球面上の位置ベクトルを取得する
    def mouseOnSphere(self, x, y):
        _x = x / self.radius
        _y = y / self.radius
        res = PVector(_x, _y, 0.0)
        if _x * _x + _y * _y > 1.0:
            res.normalize()
        else:
            res.z = sqrt(1.0 - _x * _x - _y * _y)
        return res

    # 行列の積 (m1 * m2)
    def mult(self, m1, m2):
        assert(len(m1[0]) == len(m2))
        res = [[0 for i in range(len(m1))] for j in range(len(m2[0]))]
        for i in range(len(m1)):
            for j in range(len(m2[0])):
                sum = 0.0
                for k in range(len(m1[0])):
                    sum += m1[i][k] * m2[k][j]
                res[i][j] = sum
        return res


def torus(R, r, countS, countT):
    for s in range(countS):
        theta1 = map(s, 0, countS, 0, 2*PI)
        theta2 = map(s+1, 0, countS, 0, 2*PI)
        beginShape(TRIANGLE_STRIP)
        for t in range(countT+1):
            phi = map(t, 0, countT, 0, 2*PI)
            vertex((R+r*cos(phi))*cos(theta1), (R+r*cos(phi))*sin(theta1), r*sin(phi))
            vertex((R+r*cos(phi))*cos(theta2), (R+r*cos(phi))*sin(theta2), r*sin(phi))
        endShape()

mouseCamera = None

def setup():
    global mouseCamera
    size(800,800, P3D)
    mouseCamera = MouseCamera(800.0, 0.0, 0.0, (height / 2.0)/tan(PI*30.0/180.0), 0.0, 0.0, 0.0, 0.0, 1.0, 0.0) # MouseCameraの生成
    noFill()

def draw():
    background(200)
    mouseCamera.update()
    sphere(100);
    torus(250.0, 50.0, 60, 30);

def mousePressed():
    mouseCamera.mousePressed()
def mouseDragged():
    mouseCamera.mouseDragged()
def mouseWheel(event):
    mouseCamera.mouseWheel(event)

単純に書き直したが、ドラッグすると高速回転してしまって挙動がおかしい。値を調べていったら getRotationMatrix に行き着いた。

getRotationMatrix()関数に渡したv1をnormalize()するとv1自体の値が変わってしまい、後続のc, sの計算に影響を与えてしまうため、コピーしてからnormalize()するように修正した。

コメントする