C.H. Random

【5日目】オイラー角や四元数で立方体描画【Python】

【5日目】オイラー角や四元数で立方体描画【Python】
目次
  1. 概要
  2. 環境
  3. 1. オイラー角 (Euler angles) を用いる方法
  4. オイラー角とは
  5. 実装
  6. 2. 四元数 (Quaternion) を用いる方法
  7. 四元数とは
  8. 四元数の利点
  9. pyquaternion のインストール
  10. 実装
  11. おわり
  12. あなたへのおすすめ

概要

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) を用いる方法

オイラー角とは

xx、yy、zz 軸回りの回転角を指定することで、剛体の姿勢を表す方法です。

実装

乱数で指定されたオイラー角を用い、極座標変換で回転後の座標を計算しています。
そこら辺に .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 が生成されるはず。 out1.gif

2. 四元数 (Quaternion) を用いる方法

四元数とは

  • 複素数では二次元上での回転を表現できましたが、三次元での回転を扱うときに使われるのが四元数です (大学数学)。3DCG において三次元での回転の実装によく用いられています。
  • q=a+bi+cj+dkq=a+bi+cj+dk ( a,b,c,da,b,c,d は実数、 i2=j2=k2=ijk=−1i^2=j^2=k^2=ijk=-1) という形で表されます。

四元数の利点

  • 実は、オイラー回転では Gimbal Lock (常に 33 自由度であるべきですが、特異点では 22 自由度となってしまう) が起こる可能性がありました。四元数ではこれを回避できます。
  • 任意軸周りの回転が簡単に表現可能です。
  • オイラー角に比して、計算量オーダーが小さいです。

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 が生成されるはず。 out2.gif

おわり

ゲーム制作に使えるので、高校数学 (と四元数) は頑張って勉強しておきましょう。

あなたへのおすすめ

CUE! はいいぞ