クォータニオンの利点
1.コンパクトな表現
回転行列だと9つの要素.それに対して,クォータニオンは,3次元空間における回転を4要素で実現.
2.ジンバルロック回避
特定の回転軸が他の軸と一致する際に,自由度が失われる現象(ジンバルロック)が発生し,特定の回転を表現できなくなることを防ぐことができる.
3.スムーズな補間
2つのクォータニオンの間をスムーズに補間するSlerp(球面線形補間)と呼ばれる手法が非常にシンプルで効率的に実装できる.(アニメーションやシミュレーション時に有効)
4.正規化が容易
連続的な回転を扱う際に,浮動小数点演算の誤差により正規直交性が失われることがある.これを補正するための再正規化処理が必要.
5.回転の連結が高速
2つのクォータニオンを合成することで,2つの回転を連結する操作が回転行列の乗算よりも高速.
quaternion_input_gif.py
# coding: utf-8
import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
from matplotlib.animation import FuncAnimation
import tkinter as tk
from tkinter import simpledialog
# --- 1. クォータニオンの基本操作を定義するクラス ---
class Quaternion:
def __init__(self, w, x, y, z):
"""
クォータニオンを初期化します。
q = w + xi + yj + zk
"""
self.w = w
self.x = x
self.y = y
self.z = z
def normalize(self):
"""クォータニオンを正規化します(単位クォータニオンにする)。"""
norm = np.sqrt(self.w**2 + self.x**2 + self.y**2 + self.z**2)
if norm == 0:
return Quaternion(0, 0, 0, 0) # ゼロクォータニオンの場合
return Quaternion(self.w / norm, self.x / norm, self.y / norm, self.z / norm)
def conjugate(self):
"""共役クォータニオンを返します。"""
return Quaternion(self.w, -self.x, -self.y, -self.z)
def multiply(self, other):
"""
このクォータニオンと別のクォータニオンを乗算します (q1 * q2)。
"""
w1, x1, y1, z1 = self.w, self.x, self.y, self.z
w2, x2, y2, z2 = other.w, other.x, other.y, other.z
w = w1 * w2 - x1 * x2 - y1 * y2 - z1 * z2
x = w1 * x2 + x1 * w2 + y1 * z2 - z1 * y2
y = w1 * y2 - x1 * z2 + y1 * w2 + z1 * x2
z = w1 * z2 + x1 * y2 - y1 * x2 + z1 * w2
return Quaternion(w, x, y, z)
@staticmethod
def from_axis_angle(axis, angle_rad):
"""
回転軸と角度(ラジアン)からクォータニオンを作成します。
axis: numpy配列 (x, y, z)
angle_rad: ラジアン
"""
axis = np.array(axis) / np.linalg.norm(axis) # 軸を正規化
half_angle = angle_rad / 2
sin_half_angle = np.sin(half_angle)
w = np.cos(half_angle)
x = axis[0] * sin_half_angle
y = axis[1] * sin_half_angle
z = axis[2] * sin_half_angle
return Quaternion(w, x, y, z).normalize()
def rotate_vector(self, vector):
"""
このクォータニオンを使用してベクトルを回転させます。
v_rotated = q * v * q_conjugate
vector: numpy配列 (x, y, z)
"""
p = Quaternion(0, vector[0], vector[1], vector[2]) # ベクトルを純粋クォータニオンとして表現
rotated_p = self.multiply(p).multiply(self.conjugate())
return np.array([rotated_p.x, rotated_p.y, rotated_p.z])
# --- 2. 立方体の頂点データの定義 ---
# 立方体の各頂点の座標
# 中心を原点(0,0,0)とする
half_side = 0.5
cube_vertices = np.array([
[-half_side, -half_side, -half_side],
[ half_side, -half_side, -half_side],
[ half_side, half_side, -half_side],
[-half_side, half_side, -half_side],
[-half_side, -half_side, half_side],
[ half_side, -half_side, half_side],
[ half_side, half_side, half_side],
[-half_side, half_side, half_side]
])
# 立方体の辺の接続定義
cube_edges = [
(0, 1), (1, 2), (2, 3), (3, 0), # 底面
(4, 5), (5, 6), (6, 7), (7, 4), # 天面
(0, 4), (1, 5), (2, 6), (3, 7) # 側面
]
# --- 3. ユーザー入力ダイアログを表示 ---
def get_rotation_angles():
root = tk.Tk()
root.withdraw() # メインウィンドウを非表示にする
try:
angle_x = simpledialog.askfloat("Rotation Angle", "Enter rotation angle around X-axis (degrees):", initialvalue=0.0)
if angle_x is None: return None, None, None # キャンセルされた場合
angle_y = simpledialog.askfloat("Rotation Angle", "Enter rotation angle around Y-axis (degrees):", initialvalue=0.0)
if angle_y is None: return None, None, None
angle_z = simpledialog.askfloat("Rotation Angle", "Enter rotation angle around Z-axis (degrees):", initialvalue=0.0)
if angle_z is None: return None, None, None
return angle_x, angle_y, angle_z
except ValueError:
print("Invalid input. Please enter a number.")
return None, None, None
finally:
root.destroy() # Tkinterのルートウィンドウを破棄
# --- グローバル変数 ---
current_rotation_q = Quaternion(1, 0, 0, 0) # 現在の回転クォータニオン
total_frames = 90 # アニメーションの総フレーム数 (ここでは30FPSで3秒間)
# --- 4. アニメーションの更新関数 ---
def update(frame):
global current_rotation_q
if frame == 0:
# アニメーション開始時に現在の回転をリセット
current_rotation_q.w = 1.0
current_rotation_q.x = 0.0
current_rotation_q.y = 0.0
current_rotation_q.z = 0.0
# 毎フレームの微小回転角度
delta_angle_x = np.deg2rad(INPUT_ANGLE_X) / total_frames
delta_angle_y = np.deg2rad(INPUT_ANGLE_Y) / total_frames
delta_angle_z = np.deg2rad(INPUT_ANGLE_Z) / total_frames
# 各軸の微小回転クォータニオンを生成
delta_q_x = Quaternion.from_axis_angle(np.array([1, 0, 0]), delta_angle_x)
delta_q_y = Quaternion.from_axis_angle(np.array([0, 1, 0]), delta_angle_y)
delta_q_z = Quaternion.from_axis_angle(np.array([0, 0, 1]), delta_angle_z)
# 現在の回転クォータニオンに、各軸の微小回転を適用
# グローバル座標系におけるZ-Y-X順での回転の累積
current_rotation_q = delta_q_x.multiply(current_rotation_q).normalize()
current_rotation_q = delta_q_y.multiply(current_rotation_q).normalize()
current_rotation_q = delta_q_z.multiply(current_rotation_q).normalize()
# 各頂点を回転させる
rotated_vertices = np.zeros_like(cube_vertices)
rotation_origin = np.array([0.0, 0.0, 0.0]) # 回転の中心(原点)
for i, vertex in enumerate(cube_vertices):
relative_vertex = vertex - rotation_origin
rotated_relative_vertex = current_rotation_q.rotate_vector(relative_vertex)
rotated_vertices[i] = rotated_relative_vertex + rotation_origin
# 更新された頂点データで線を描画
for i, (p1_idx, p2_idx) in enumerate(cube_edges):
x_data = [rotated_vertices[p1_idx, 0], rotated_vertices[p2_idx, 0]]
y_data = [rotated_vertices[p1_idx, 1], rotated_vertices[p2_idx, 1]]
z_data = [rotated_vertices[p1_idx, 2], rotated_vertices[p2_idx, 2]]
lines[i].set_data(x_data, y_data)
lines[i].set_3d_properties(z_data)
return lines
# --- メイン処理 ---
if __name__ == "__main__":
# 1. ユーザーから回転角度を取得
INPUT_ANGLE_X, INPUT_ANGLE_Y, INPUT_ANGLE_Z = get_rotation_angles()
if INPUT_ANGLE_X is None:
print("アニメーションがユーザーによってキャンセルされました。")
else:
# 2. アニメーションの初期化
fig = plt.figure(figsize=(8, 8))
ax = fig.add_subplot(111, projection='3d')
ax.set_title(f"Cube Rotation: X={INPUT_ANGLE_X}°, Y={INPUT_ANGLE_Y}°, Z={INPUT_ANGLE_Z}°")
ax.set_xlim([-1, 1])
ax.set_ylim([-1, 1])
ax.set_zlim([-1, 1])
ax.set_xlabel("X-axis")
ax.set_ylabel("Y-axis")
ax.set_zlabel("Z-axis")
ax.set_aspect('auto')
lines = [ax.plot([], [], [], 'b-')[0] for _ in cube_edges]
# 3. アニメーションの実行
# interval=33ms は約30FPSに相当 (1000ms / 30フレーム = 33.3ms)
# total_framesを90に設定することで、全体で3秒のアニメーション (90フレーム / 30FPS = 3秒)
ani = FuncAnimation(fig, update, frames=total_frames, interval=33, blit=True, repeat=False)
# 4. GIFファイルとして保存
gif_filename = f"cube_rotation_X{INPUT_ANGLE_X}_Y{INPUT_ANGLE_Y}_Z{INPUT_ANGLE_Z}.gif"
print(f"アニメーションを '{gif_filename}' に保存中...")
ani.save(gif_filename, writer='pillow', fps=30) # fpsはintervalと合わせる
print("保存が完了しました!")
plt.show()