概要
Vtuber や、原神などの 3D ゲームが隆盛を誇っている今、ますます多くのプログラマが 3D 技術に触れることになるでしょう。
勿論、Unity や MMD などを用いると、内部実装を知らずとも簡単にオブジェクトの回転を実現できます。しかし本記事では、オイラー角あるいは四元数を用いて、立方体が回転する gif を生成することで、三次元座標の回転をより深く理解しようと思います。
環境
Windows 10 + WSL (Ubuntu 20.04.4 LTS) + VSCode 1.65.2 + Python 3.10.4
$ cat /etc/os-release | head -n 2
NAME="Ubuntu"
VERSION="20.04.4 LTS (Focal Fossa)"
$ python3 -V
Python 3.10.4
1. オイラー角 (Euler angles) を用いる方法
オイラー角とは
実装
乱数で指定されたオイラー角を用い、極座標変換で回転後の座標を計算しています。
そこら辺に .py ファイルを作って下記コードをコピペして実行してください。
ちなみに # %%
はブロックごとに分割実行する VSCode の機能のために書いています。
#!/usr/bin/python3
# %%
from PIL import Image, ImageDraw
import math
import random
# C++ の __builtin_popcount に当たる関数, 計算量 O(log n)
def count_ones(x):
x = (x & 0x55555555) + (x >> 1 & 0x55555555)
x = (x & 0x33333333) + (x >> 2 & 0x33333333)
x = (x & 0x0F0F0F0F) + (x >> 4 & 0x0F0F0F0F)
x = (x & 0x00FF00FF) + (x >> 8 & 0x00FF00FF)
x = (x & 0x0000FFFF) + (x >> 16 & 0x0000FFFF)
return x
# 上の関数と等価, 計算量 O(n)
# def count_ones(x):
# return format(x, "b").count("1")
def rotate(x, y, rad):
return x * math.cos(rad) - y * math.sin(rad), x * math.sin(rad) + y * math.cos(rad)
# %%
size = 360
r = 84
# z, x, y 軸回りの回転角
rads = [random.uniform(0, math.pi * 2) for _ in range(3)]
add_rad = math.pi * random.uniform(0.04, 0.08)
imgs = []
# 24 フレームの gif を生成
for _ in range(24):
img = Image.new("RGB", (size, size), "White")
draw = ImageDraw.Draw(img)
xyzs = [
[r if i & 1 else -r, r if i & 0b10 else -r, r if i & 0b100 else -r]
for i in range(8)
]
# 回転先の座標を求める
for xyz in xyzs:
for i, j in [(0, 1), (1, 2), (2, 0)]:
xyz[i], xyz[j] = rotate(xyz[i], xyz[j], rads[i])
for i in range(8):
for j in range(i + 1, 8):
if count_ones(i ^ j) == 1:
draw.line([(xyzs[i][0] + size / 2, xyzs[i][1] + size / 2),
(xyzs[j][0] + size / 2, xyzs[j][1] + size / 2)],
fill="Black")
imgs.append(img)
# 回転
rads[0] += add_rad
imgs[0].save('out1.gif', save_all=True, append_images=imgs[1:])
こんなかんじの gif が生成されるはず。
2. 四元数 (Quaternion) を用いる方法
四元数とは
- 複素数では二次元上での回転を表現できましたが、三次元での回転を扱うときに使われるのが四元数です (大学数学)。3DCG において三次元での回転の実装によく用いられています。
( は実数、 ) という形で表されます。
四元数の利点
- 実は、オイラー回転では Gimbal Lock (常に
自由度であるべきですが、特異点では 自由度となってしまう) が起こる可能性がありました。四元数ではこれを回避できます。 - 任意軸周りの回転が簡単に表現可能です。
- オイラー角に比して、計算量オーダーが小さいです。
pyquaternion のインストール
四元数ライブラリ pyquaternion を使うのでインストールしてください。
$ pip3 install pyquaternion
実装
#!/usr/bin/python3
# %%
from pyquaternion import Quaternion
from PIL import Image, ImageDraw
import math
import random
# C++ の __builtin_popcount に当たる関数, 計算量 O(log n)
def count_ones(x):
x = (x & 0x55555555) + (x >> 1 & 0x55555555)
x = (x & 0x33333333) + (x >> 2 & 0x33333333)
x = (x & 0x0F0F0F0F) + (x >> 4 & 0x0F0F0F0F)
x = (x & 0x00FF00FF) + (x >> 8 & 0x00FF00FF)
x = (x & 0x0000FFFF) + (x >> 16 & 0x0000FFFF)
return x
# 上の関数と等価, 計算量 O(n)
# def count_ones(x):
# return format(x, "b").count("1")
# %%
size = 360
r = 84
xyzs = [
[r if i & 1 else -r, r if i & 0b10 else -r, r if i & 0b100 else -r]
for i in range(8)
]
initial_quat = Quaternion.random()
for i in range(8):
xyzs[i] = initial_quat.rotate(xyzs[i])
# どんな軸周りに何ラジアン回転するか
quat = Quaternion(
axis=[random.uniform(-1, 1) for _ in range(3)],
angle=math.pi * random.uniform(0.04, 0.08)
)
imgs = []
# 24 フレームの gif を生成
for _ in range(24):
img = Image.new("RGB", (size, size), "White")
draw = ImageDraw.Draw(img)
for i in range(8):
for j in range(i + 1, 8):
if count_ones(i ^ j) == 1:
draw.line([(xyzs[i][0] + size / 2, xyzs[i][1] + size / 2),
(xyzs[j][0] + size / 2, xyzs[j][1] + size / 2)],
fill="Black")
imgs.append(img)
# 回転
for i in range(8):
xyzs[i] = quat.rotate(xyzs[i])
imgs[0].save('out2.gif', save_all=True, append_images=imgs[1:])
こんなかんじの gif が生成されるはず。
おわり
ゲーム制作に使えるので、高校数学 (と四元数) は頑張って勉強しておきましょう。
あなたへのおすすめ
CUE! はいいぞ