既然 Matplotlib 可以畫立體圖,我就有了一個想法,可以用來 3D 建模嗎?例如,簡單畫個環面 3D 模型?
上圖其實是用 plot_surface
畫出來的,就像一塊布包在圓環,看來就像個 3D 模型了,你可以試著用純 Python 與 Matplot 來畫畫看,不過,這邊直接用 NumPy 來實現。
首先,試著用 NumPy 建立 plot_surface
需要的 x、y、z 軸資料,先來個平面的資料就可以了:
import numpy as np
import matplotlib.pyplot as plt
width = 96
x = np.arange(width)
y = np.arange(width)
X, Y = np.meshgrid(x, y)
Z = np.zeros((width, width))
ax = plt.axes(projection='3d')
ax.plot_surface(X, Y, Z)
ax.set_xlabel('x')
ax.set_ylabel('y')
ax.set_zlabel('z')
plt.show()
這邊看到了 NumPy 的 zeros
函式,你可以指定陣列的形狀,該函式產生的陣列元素,全部都會是 0,也就是畫出來的平面是在 xy 平面上:
下一步是,試著讓這個平面變成一個圓筒面,這需要指定圓筒半徑,然後讓面繞著 z 軸轉一圈,為此,可以將 x 值當成是圓筒的每個步進,原本的 y 就當成是圓筒的高:
import numpy as np
import matplotlib.pyplot as plt
fn = 96
radius = 150
# 因為要接合起來,因此必須是 fn + 1
x = np.arange(fn + 1)
y = np.arange(fn + 1)
X, Y = np.meshgrid(x, y)
theta = X * (np.pi * 2 / fn)
TX = radius * np.cos(theta)
TY = radius * np.sin(theta)
TZ = Y
ax = plt.axes(projection='3d')
ax.plot_surface(TX, TY, TZ)
ax.set_xlabel('x')
ax.set_ylabel('y')
ax.set_zlabel('z')
plt.show()
因為圓周現在是由 radius
決定了,在這邊將 width
改成了 fn
,表示一個圓可以切為幾段直線來表示,會比較符合這個參數的意義,結果可以畫出底下的圓筒:
下一步要將圓筒面上每個與 z 軸平行的線,也繞成一個圓,這需要指定另一個半徑,而 y 值當成是圓的每個步進。
在計算的順序上,這邊採取先繞成一個圓,再推至圓筒半徑處繞一個環,在先繞圓時,圓上每個點的 x 座標會是 radius2 * np.cos(theta2)
,theta2
旋轉的角度,而推至圓筒半徑處時,就會是 radius1 + radius2 * np.cos(theta2)
,至於圓的 z 座標會是 radius2 * np.sin(theta2)
,推至圓筒半徑處再旋轉後不會改變。
結果就是,以下的程式可以畫出環面:
import numpy as np
import matplotlib.pyplot as plt
fn = 6
radius1 = 150
radius2 = 50
x = np.arange(fn + 1)
y = np.arange(fn + 1)
theta_step = np.pi * 2 / fn
X, Y = np.meshgrid(x, y)
theta1 = X * theta_step
theta2 = Y * theta_step
r = radius1 + radius2 * np.cos(theta2)
TX = r * np.cos(theta1)
TY = r * np.sin(theta1)
TZ = radius2 * np.sin(theta2)
ax = plt.axes(projection='3d')
ax.plot_surface(TX, TY, TZ)
ax.set_xlabel('x')
ax.set_ylabel('y')
ax.set_zlabel('z')
ax.set_box_aspect((1, 1, radius2 / radius1)) # 調整比例,不然 1:1:1 看來會很怪
plt.show()
畫出來的圖案會是:
嗯?不是環面?因為 fn
故意設成 6,就繪圖來說,圓從來就不完美,只能是正多邊形罷了,這篇文章一開始的圓,是 fn
設為 96 的結果,在人眼看來就覺得很圓了!