PYTHON
【5日目】オイラー角や四元数で立方体描画【Python】
公開日:
2022/03/30
【5日目】オイラー角や四元数で立方体描画【Python】

概要

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

オイラー角とは

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

実装

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

loading...