運用Matplotlib玩3D建模,有可能嗎?乍看是個努力用錯了方向的題目,不過,在找出方式的瞬間,總是有種又惡搞成功的快感,也有機會更進一步地認識手邊的工具!

初步的3D建模

因為個人興趣,也為了能在切入NumPy、Matplotlib時有個具體目標,我試著將過去對繪圖的研究課題以NumPy、Matplotlib實現以後,確實畫出了不少有趣的圖案,像是先前專欄〈陣列程式設計〉繪製的謝爾賓斯基三角形,或者是〈NumPy與海龜繪圖〉實現的海龜繪圖。

在能夠使用Matplotlib實現幾個2D圖案的繪製後,心中開始尋求的,就是實現3D建模的可行性了,確實地,目前的Matplotlib版本內建了mplot3d模組,可以透過Axes3D物件有3D座標系統中繪圖,而在二維座標系統中常見的繪圖需求,像是折線圖、散佈圖、輪廓圖等,在Axes3D物件上,都有對應的繪製方法。

我第一個想到的是,既然可以繪製3D折線圖,這表示要用Matplotlib從事3D版本的海龜繪圖,也是可行的;另外,在使用程式實現3D繪圖時,像是OpenSCAD、WebGL等,最常見的入門範例就是繪製函式曲面,這可以透過Axes3D物件的plot_surface方法,它需要個別提供x、y與z全部的座標點。若只想畫線框圖,也可以透過plot_wireframe方法;若對這些基本的3D繪製有興趣,在〈Matplotlib立體圖〉有幾個實際範例供參考。

既然可以繪製曲面,我想到的是,可否用來實現3D建模?就像OpenSCAD那樣,以程式撰寫生成3D物件需要的網面(mesh)資料?因為是網「面」,如果透過plot_surface繪製的面,能包覆成一個封閉的面,看來就像個3D模型了。

最簡單的就是立方體,基本作法就是準備六個平面的資料,呼叫六次plot_surface就可以達成任務。如果是有曲面的3D模型呢?例如一顆球?可以透過球面參數式,計算出全部的x、y與z座標點,再透過plot_surface繪製。

類似地,只要有環面的參數式,也可以plot_surface透過繪製環面,因為有公式,透過NumPy來計算出座標點也會更為便利。若有興趣看看程式實作,我們可以參考〈NumPy與環面〉

三角網面繪製

如果對Matplotlib有認識,也有3D建模經驗,你可能會看出方才的描述有點問題,因為「網面不是由三角形組成嗎?」確實地,餵給plot_surface的,並不是三角形構成的資料,正確來說是許多四邊形的資料;另一方面,許多3D模型光靠plot_surface是繪製不出來的,例如正四面體(tetrahedron),因為它是由四個正三角面組成。

如果想繪製三角網面,可以透過Axes3D物件的plot_trisurf方法,最基本的使用方式是,透過一維陣列分別提供每個點x、y與z座標,它會自動以x與y進行Delaunay三角分割,分割出來的三角形x、y資料再與z資料結合,成為各個3D三角面頂點資訊;另一個方式,是透過matplotlib.tri模組的Triangulation物件來做三角分割,再將Triangulation物件連同z座標資料餵給plot_trisurf。

也就是說,就算x、y與z座標是沒有順序的資料,也可以直接提供給plot_trisurf,單就繪製曲面而言,在提供資料的方式上,會比plot_surface方便,當然,因為須進行三角分割運算,速度上會比較慢。

而且,基本上,使用者不容易掌握Delaunay三角分割的結果,如果想自行控制三角分割的細節,我們可以透過plot_trisurf的triangles參數,指定每個三角形使用的座標索引,既然如此,這不就也能用來構造3D物件的網面嗎?例如,在〈Matplotlib三角曲面〉,就有個透過plot_trisurf繪製正四面體的範例。

在某些場合,手邊可能已經有每個三角形的頂點資料,將這些三角形各自畫出就可以組成3D物件外觀,這時plot_trisurf就顯得不適用,我們可以透過Axes3D的add_collection3d方法,它接受mplot3d.art3d.Poly3DCollection,可指定頂點座標建立Poly3DCollection物件,用來代表一個3D多邊形。

既然Poly3DCollection代表3D多邊形,這表示不用侷促在三角形的繪製,例如方才談到可以呼叫plot_surface六次來畫立方體的方式,其實可以改用Poly3DCollection將這六面收集起來,這樣只要呼叫一次add_collection3d,就能完成立方體的繪製,效率上會比較好。

載入、顯示模型檔案

無論是plot_trisurf或add_collection3d,基本上,都可以處理三角網面,那麼就有機會載入3D模型檔案,用Matplotlib來實現3D模型檢視器了,例如OBJ檔案格式的話,最簡單的格式可以只包含頂點的座標、以及頂點索引清單,以純文字編輯器開啟.obj檔案就可以看到:

v 21.616501 24.997404 2.460000
v 22.228498 24.997404 0.296000
f 4226 3927 3928
f 3927 4226 4227

v是頂點座標,f是頂點索引,想使用Python或NumPy處理這樣的格式並不難,不過,我們要留意:OBJ檔案的頂點索引是從1開始,記得減去1,才能給plot_trisurf的triangles參數使用,只不過對於複雜的3D模型檔案來說,會有為數龐大的三角面,使用plot_trisurf或add_collection3d來繪製,繪圖效率上會比較吃力。

若只是想載入3D模型檔案來產生圖片,在多邊形的繪製上,Matplotlib本身有2D版本的add_collection,以及PolyCollection,由於Matplotlib會依照PolyCollection的順序來繪製多邊形,為了得到正確的顯示結果,要將3D的三角面先依深度排序,投影至2D平面後依序加入PolyCollection,才不會因為深度不正確的三角形互相覆蓋,而造成顯示錯誤。

〈Custom 3D engine in Matplotlib〉中,就談到如何透過2D版本的add_collection與PolyCollection,載入模型檔、建立投影矩陣、排序並繪製為圖片存檔。

如果要載入STL檔案,因為它有二進位與文字格式,比較方便的作法是安裝numpy-stl程式庫,透過stl模組的mesh.Mesh.from_file函式,就可以指定STL檔案,接著結合add_collection3d就可以繪圖了,一個簡單的片段是:

mesh = Mesh.from_file('caterpillar.stl')
ax = plt.axes(projection='3d')
ax.add_collection3d(art3d.Poly3DCollection(mesh.vectors))

Hack玩法的樂趣

這一陣子以來,從發想出以自身對繪圖的興趣作為基礎,來切入Matplotlib(或NumPy),著實讓我得到了不少樂趣,這些樂趣來自於突發奇想的題目、尋找可能性、轉換解題的思考方式、任務的實現的過程,我後來總是稱這是讓它們不務正業,強迫它們做一些非典型的應用。

而這個過程中,對一些文件或書籍上不常見的特性,也多了不少認識的機會,尋找解答的過程有時不見得順利,然而,在找出方式的瞬間,總是有種又成功惡搞的成就感,也確實發掘出更多Matplotlib(或NumPy)的可能性呢!

專欄作者

熱門新聞

Advertisement