0
2

コード紹介

Gist

コード
tetrahedron_spin.py
import math
import tkinter as tk
import sys
BG_COLOR = "black"
FG_COLOR = "white"

def rotate_point(x, y, z, angle_x, angle_y, angle_z):
    y1 = y * math.cos(angle_x) - z * math.sin(angle_x)
    z1 = y * math.sin(angle_x) + z * math.cos(angle_x)

    x2 = x * math.cos(angle_y) + z1 * math.sin(angle_y)
    z2 = -x * math.sin(angle_y) + z1 * math.cos(angle_y)

    x3 = x2 * math.cos(angle_z) - y1 * math.sin(angle_z)
    y3 = x2 * math.sin(angle_z) + y1 * math.cos(angle_z)

    return x3, y3, z2

def get_shading(x, y, z, light_dir):
    dot_product = x * light_dir[0] + y * light_dir[1] + z * light_dir[2]
    length = math.sqrt(x ** 2 + y ** 2 + z ** 2) * math.sqrt(light_dir[0] ** 2 + light_dir[1] ** 2 + light_dir[2] ** 2)
    shading = dot_product / length
    return shading

def interpolate_points(p1, p2, num_points):
    points = []
    for i in range(num_points + 1):
        t = i / num_points
        x = p1[0] * (1 - t) + p2[0] * t
        y = p1[1] * (1 - t) + p2[1] * t
        z = p1[2] * (1 - t) + p2[2] * t
        points.append((x, y, z))
    return points

def get_edge_char(shading):
    chars = "MIZUHA"
    index = int((shading + 1) / 2 * (len(chars) - 1))
    return chars[index]

def get_face_char(shading):
    chars = r"."
    index = int((shading + 1) / 2 * (len(chars) - 1))
    return chars[index]

def draw_tetrahedron(angle_x, angle_y, angle_z, light_dir, canvas, canvas_size):
    vertices = [
        (1, 1, 1),
        (-1, -1, 1),
        (-1, 1, -1),
        (1, -1, -1)
    ]

    edges = [
        (vertices[0], vertices[1]),
        (vertices[0], vertices[2]),
        (vertices[0], vertices[3]),
        (vertices[1], vertices[2]),
        (vertices[1], vertices[3]),
        (vertices[2], vertices[3])
    ]

    faces = [
        (vertices[0], vertices[1], vertices[2]),
        (vertices[0], vertices[1], vertices[3]),
        (vertices[0], vertices[2], vertices[3]),
        (vertices[1], vertices[2], vertices[3])
    ]

    num_points = 50

    for v1, v2 in edges:
        edge_points = interpolate_points(v1, v2, num_points)
        for x, y, z in edge_points:
            x_rot, y_rot, z_rot = rotate_point(x, y, z, angle_x, angle_y, angle_z)
            canvas_x = int(x_rot * 35 + canvas_size // 2)
            canvas_y = int(y_rot * 35 + canvas_size // 2)
            if 0 <= canvas_x < canvas_size and 0 <= canvas_y < canvas_size:
                shading = get_shading(x_rot, y_rot, z_rot, light_dir)
                char = get_edge_char(shading)
                canvas[canvas_y][canvas_x] = char

    for v1, v2, v3 in faces:
        for i in range(num_points + 1):
            for j in range(num_points + 1 - i):
                t1 = i / num_points
                t2 = j / num_points
                t3 = 1 - t1 - t2
                x = v1[0] * t1 + v2[0] * t2 + v3[0] * t3
                y = v1[1] * t1 + v2[1] * t2 + v3[1] * t3
                z = v1[2] * t1 + v2[2] * t2 + v3[2] * t3
                x_rot, y_rot, z_rot = rotate_point(x, y, z, angle_x, angle_y, angle_z)
                canvas_x = int(x_rot * 35 + canvas_size // 2)
                canvas_y = int(y_rot * 35 + canvas_size // 2)
                if 0 <= canvas_x < canvas_size and 0 <= canvas_y < canvas_size and canvas[canvas_y][canvas_x] == ' ':
                    shading = get_shading(x_rot, y_rot, z_rot, light_dir)
                    char = get_face_char(shading)
                    canvas[canvas_y][canvas_x] = char

    return canvas

def clear_screen():
    sys.stdout.write("\033[2J\033[H")
    sys.stdout.flush()

def update_frame(canvas, canvas_size, label, angle_x, angle_y, angle_z, light_dir):
    canvas = [[' ' for _ in range(canvas_size)] for _ in range(canvas_size)]
    canvas = draw_tetrahedron(angle_x, angle_y, angle_z, light_dir, canvas, canvas_size)
    frame = '\n'.join(''.join(row) for row in canvas)
    label.config(text=frame)
    label.after(20, update_frame, canvas, canvas_size, label, angle_x + 0.03, angle_y + 0.02, angle_z + 0.01, light_dir)

def main():
    root = tk.Tk()
    root.title("3D Tetrahedron Animation")
    root.configure(bg=BG_COLOR)

    canvas_size = 220
    canvas = [[' ' for _ in range(canvas_size)] for _ in range(canvas_size)]
    label = tk.Label(root, bg=BG_COLOR, fg=FG_COLOR, font=("Courier", 8), justify=tk.LEFT)
    label.pack(expand=True)

    angle_x, angle_y, angle_z = 0, 0, 0
    light_dir = (0, 0, 1)

    update_frame(canvas, canvas_size, label, angle_x, angle_y, angle_z, light_dir)

    root.mainloop()

if __name__ == "__main__":
    main()

解説

手順

手順は以下の通り
点ごとに以下の動きが行われる

  1. 回転行列を適用して新しい座標を計算
  2. シェーディングを計算
  3. 2D画面に投影して、キャンバス上の位置を決定
  4. キャンバスに対応するキャラクターを描画

1. 正四面体の頂点

正四面体の頂点は次の座標に設定されている (典型的な正四面体)

  1. (1, 1, 1)
  2. (-1, -1, 1)
  3. (-1, 1, -1)
  4. (1, -1, -1)

2. 回転行列

図形を回転させるために、3つの回転行列を使用する
ただし、$θ$ は初期値で 0 となっており、そこから1フレームごとに
$angle_x$ は 0.03、$angle_y$ は 0.02、$angle_z$ は 0.01 ずつ増加する

x軸回りの回転
angle_x = 
\begin{pmatrix}
1 & 0 & 0 \\
0 & \cos(\theta_x) & -\sin(\theta_x) \\
0 & \sin(\theta_x) & \cos(\theta_x)
\end{pmatrix}
y軸回りの回転
angle_y = 
\begin{pmatrix}
\cos(\theta_y) & 0 & \sin(\theta_y) \\
0 & 1 & 0 \\
-\sin(\theta_y) & 0 & \cos(\theta_y)
\end{pmatrix}
z軸回りの回転
angle_z = 
\begin{pmatrix}
\cos(\theta_z) & -\sin(\theta_z) & 0 \\
\sin(\theta_z) & \cos(\theta_z) & 0 \\
0 & 0 & 1
\end{pmatrix}

3. シェーディング

シェーディングは光源の方向と面の法線ベクトルの間の角度に基づいて計算されます。ドット積を使って法線ベクトルと光源方向ベクトルの角度を計算し、面の明るさを決定します。

4. 補間

各辺と面の点は、線形補間によって計算されます。補間は、頂点間の点を一定の間隔で計算するために使用されます。

5. 2Dへの投影

3D座標が計算されると、これらの座標は2D画面に投影されます。画面の中心を基準として、スケーリングと平行移動を行い、各点の画面座標を決定します。

6. キャンバスへの描画

各点が計算されると、それぞれの点がキャンバスに描画されます。シェーディングの値に応じて、各点のキャラクターが選択され、エッジや面を描画します。

コード1行解説

1-3行目

import math
import tkinter as tk
import sys

パッケージの読み込み

math, tkinter, sys はどれも標準パッケージです
mathは名前通り数学計算を行うパッケージで、tkinterはGUIに出力するためのパッケージ、sysはシステム固有のパラメーターと関数を操作するためのパッケージです
as tkは、tkとして読み込む、の意味

4-5行目

BG_COLOR = "black"
FG_COLOR = "white"

tkinterで使用する画面のバッググラウンドと文字の色を指定します

7-17行目

def rotate_point(x, y, z, angle_x, angle_y, angle_z):
    y1 = y * math.cos(angle_x) - z * math.sin(angle_x)
    z1 = y * math.sin(angle_x) + z * math.cos(angle_x)

    x2 = x * math.cos(angle_y) + z1 * math.sin(angle_y)
    z2 = -x * math.sin(angle_y) + z1 * math.cos(angle_y)

    x3 = x2 * math.cos(angle_z) - y1 * math.sin(angle_z)
    y3 = x2 * math.sin(angle_z) + y1 * math.cos(angle_z)

    return x3, y3, z2

先ほど解説した回転行列の導入です
このrotate_point関数では、点(x, y, z)をx軸、y軸、z軸周りにそれぞれangle_x、angle_y、angle_zだけ回転させます
三角関数math.cos()math.sin()を使用して、回転後の座標(x3, y3, z2)を計算します

19-23行目

def get_shading(x, y, z, light_dir):
    dot_product = x * light_dir[0] + y * light_dir[1] + z * light_dir[2]
    length = math.sqrt(x ** 2 + y ** 2 + z ** 2) * math.sqrt(light_dir[0] ** 2 + light_dir[1] ** 2 + light_dir[2] ** 2)
    shading = dot_product / length
    return shading

このget_shading関数は、光の方向light_dirに対する点(x, y, z)の影の強さ(シェーディング)を計算します。具体的には、点積を計算して光の方向との関係を求め、その結果を返します。

25-33行目

def interpolate_points(p1, p2, num_points):
    points = []
    for i in range(num_points + 1):
        t = i / num_points
        x = p1[0] * (1 - t) + p2[0] * t
        y = p1[1] * (1 - t) + p2[1] * t
        z = p1[2] * (1 - t) + p2[2] * t
        points.append((x, y, z))
    return points

interpolate_points関数は、2つの点p1とp2の間を線形補間して、指定された数の中間点を生成します。num_pointsは中間点の数を指定します。

35-38行目

def get_edge_char(shading):
    chars = "MIZUHA"
    index = int((shading + 1) / 2 * (len(chars) - 1))
    return chars[index]

get_edge_char関数は、辺のシェーディングの値に基づいて適切な文字を選択します。shadingが-1から1の間で変化し、文字列"MIZUHA"のインデックスを計算して返します。

40-43行目

def get_face_char(shading):
    chars = r"."
    index = int((shading + 1) / 2 * (len(chars) - 1))
    return chars[index]

get_face_char関数は、面のシェーディングの値に基づいて適切な文字を選択します。shadingが-1から1の間で変化し、文字列"."のインデックスを計算して返します。

45-99行目

def draw_tetrahedron(angle_x, angle_y, angle_z, light_dir, canvas, canvas_size):
    vertices = [
        (1, 1, 1),
        (-1, -1, 1),
        (-1, 1, -1),
        (1, -1, -1)
    ]

    edges = [
        (vertices[0], vertices[1]),
        (vertices[0], vertices[2]),
        (vertices[0], vertices[3]),
        (vertices[1], vertices[2]),
        (vertices[1], vertices[3]),
        (vertices[2], vertices[3])
    ]

    faces = [
        (vertices[0], vertices[1], vertices[2]),
        (vertices[0], vertices[1], vertices[3]),
        (vertices[0], vertices[2], vertices[3]),
        (vertices[1], vertices[2], vertices[3])
    ]

    num_points = 50

    for v1, v2 in edges:
        edge_points = interpolate_points(v1, v2, num_points)
        for x, y, z in edge_points:
            x_rot, y_rot, z_rot = rotate_point(x, y, z, angle_x, angle_y, angle_z)
            canvas_x = int(x_rot * 35 + canvas_size // 2)
            canvas_y = int(y_rot * 35 + canvas_size // 2)
            if 0 <= canvas_x < canvas_size and 0 <= canvas_y < canvas_size:
                shading = get_shading(x_rot, y_rot, z_rot, light_dir)
                char = get_edge_char(shading)
                canvas[canvas_y][canvas_x] = char

    for v1, v2, v3 in faces:
        for i in range(num_points + 1):
            for j in range(num_points + 1 - i):
                t1 = i / num_points
                t2 = j / num_points
                t3 = 1 - t1 - t2
                x = v1[0] * t1 + v2[0] * t2 + v3[0] * t3
                y = v1[1] * t1 + v2[1] * t2 + v3[1] * t3
                z = v1[2] * t1 + v2[2] * t2 + v3[2] * t3
                x_rot, y_rot, z_rot = rotate_point(x, y, z, angle_x, angle_y, angle_z)
                canvas_x = int(x_rot * 35 + canvas_size // 2)
                canvas_y = int(y_rot * 35 + canvas_size // 2)
                if 0 <= canvas_x < canvas_size and 0 <= canvas_y < canvas_size and canvas[canvas_y][canvas_x] == ' ':
                    shading = get_shading(x_rot, y_rot, z_rot, light_dir)
                    char = get_face_char(shading)
                    canvas[canvas_y][canvas_x] = char

    return canvas

draw_tetrahedron関数は、四面体のエッジと面を描画するための関数です
テトラヘドロンの頂点、エッジ、および面は、それぞれverticesedgesfacesで定義されています
interpolate_points関数を使用してエッジの間を補間し、回転後の座標を計算してから、get_edge_charget_face_char関数を使ってシェーディングに応じた文字を選択し、tkinterのcanvasに描画します。

101-103行目

def clear_screen():
    sys.stdout.write("\033[2J\033[H")
    sys.stdout.flush()

clear_screen関数は、コンソール画面をクリアするための関数です。
ANSIエスケープシーケンス "\033[2J\033[H"を使用して、画面をクリアし、sys.stdout.flush()で出力をフラッシュします。

105-110行目

def update_frame(canvas, canvas_size, label, angle_x, angle_y, angle_z, light_dir):
    canvas = [[' ' for _ in range(canvas_size)] for _ in range(canvas_size)]
    canvas = draw_tetrahedron(angle_x, angle_y, angle_z, light_dir, canvas, canvas_size)
    frame = '\n'.join(''.join(row) for row in canvas)
    label.config(text=frame)
    label.after(20, update_frame, canvas, canvas_size, label, angle_x + 0.03, angle_y + 0.02, angle_z + 0.01, light_dir)

update_frame関数は、アニメーションのフレームを更新するための関数です
まず、canvasを空の文字列で初期化し、draw_tetrahedron関数を使って四面体を描画します
その後、canvasを文字列に変換し、labelのテキストとして設定します
最後に、label.afterを使って20ミリ秒ごとに自身を再帰的に呼び出し、アニメーションを更新します

112-127行目

def main():
    root = tk.Tk()
    root.title("3D Tetrahedron Animation")
    root.configure(bg=BG_COLOR)

    canvas_size = 220
    canvas = [[' ' for _ in range(canvas_size)] for _ in range(canvas_size)]
    label = tk.Label(root, bg=BG_COLOR, fg=FG_COLOR, font=("Courier", 8), justify=tk.LEFT)
    label.pack(expand=True)

    angle_x, angle_y, angle_z = 0, 0, 0
    light_dir = (0, 0, 1)

    update_frame(canvas, canvas_size, label, angle_x, angle_y, angle_z, light_dir)

    root.mainloop()

main()関数は、プログラムのエントリーポイントです
Tkinterのウィンドウを作成し、タイトルを設定し、背景色を設定します
canvasのサイズと初期化、labelの設定を行い、初期の回転角度angle_x、angle_y、angle_zと光の方向light_dirを設定して、update_frame()関数を呼び出します
最後に、root.mainloop()でイベントループを開始し、ウィンドウを表示します

129~130行目

if __name__ == "__main__":
    main()

このファイルが直接的に実行されているとき、main関数を実行します
この実装方法をとった理由は、今後それぞれの関数をimportすることになるかもしれないな、と思ったからです


Thank you for reading!

0
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
2