コード紹介
コード
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()
今度は多分正になってると思うhttps://t.co/GzgeUG1M6G https://t.co/By3yTjYINZ pic.twitter.com/DJFBN2cLFH
— みずは (@mizuha_python) July 7, 2024
解説
手順
手順は以下の通り
点ごとに以下の動きが行われる
- 回転行列を適用して新しい座標を計算
- シェーディングを計算
- 2D画面に投影して、キャンバス上の位置を決定
- キャンバスに対応するキャラクターを描画
1. 正四面体の頂点
正四面体の頂点は次の座標に設定されている (典型的な正四面体)
- (1, 1, 1)
- (-1, -1, 1)
- (-1, 1, -1)
- (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
関数は、四面体のエッジと面を描画するための関数です
テトラヘドロンの頂点、エッジ、および面は、それぞれvertices
、edges
、faces
で定義されています
interpolate_points
関数を使用してエッジの間を補間し、回転後の座標を計算してから、get_edge_char
とget_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!