processingの3Dモードで立体物を作るのは簡単だが、そのままでは一方向からの視点でしか見ることができない。カメラのアングルを変更するプログラミングは多少やるとして、ゲームのような3D空間を簡単に見る操作を作るとなると大変だ。そこで、カメラを自由に操るためのクラスを用意する。
アルゴリズムは非常に難しいので今回は丸パクリさせてもらうことにした。詳細を理解したい人は頑張って行列(高校数学)を勉強しよう!
参考までにこちらも気になった
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()するように修正した。