引言
年初開始進入3D打印行業,受命以Cura為基礎,研發一款自主的3D打印切片軟件。
自主研發要取其長處,補其不足,首先自然是要搞清楚Cura到底做了什么,讀Cura的代碼是必需的。我一向都覺得比起自己寫代碼來,讀別人的代碼是一個漫又而痛苦的過程,讀者的思想與寫者總有偏差,往往又無法驗證自己的猜想是否正確,只嘆人腦不是電腦,無法把眼前的代碼從頭到尾執行一遍。不知道各位資深程序會有什么辦法,我的辦法是“翻譯”,看着別人寫的代碼,加上自己的理解之后,按自己的喜好重新寫出來,看一段翻譯一段,等全部翻譯完成,理論上作者的思路也明白了,同時還有了一份功能一模一樣的代碼,自己的理解是否正確,也可以通過執行“翻譯”出來的代碼驗證。
計划總是美好的,中間的工程卻是充滿變數,之間的曲折折疊不說。經過若干次推倒重寫,勉強算是有了一份自己的切片軟件,又經過了半年的推敲摸索以及打印經驗積累,一個還算另自己滿意的切片軟件最終誕生。起名Pango,先觀大略。
Pango的介紹和說明先按下不表,以會有機會另外發文詳述。
在Pango的開發過程中,我對於Cura的理解也日益深入。時至今日,我也有了信心可以把我的這些經驗、理解和心得分享出來,供大家參詳一二,若能對后來的Cura研究者有所助益,那是再好不過。
Cura的架構
Cura是一個python語言實現,使用wxpython圖形界面框架的3D打印切片界面軟件,說它是界面軟件是因為Cura本身並不會進行實際的切片操作。實際的切片工作是由另外一個C++語言實現的CuraEngine命令行軟件來具體負責的,用戶在Cura界面上的絕大多數操作,如加載模型、平穩旋轉縮放、參數設置等最終會轉換成並執行一條CuraEngine命令;CuraEngine把輸入的STL、DAE或OBJ模型文件切片輸出成gcode字符串返回給Cura;Cura再把gcode在3D界面上可視化成路徑展現給用戶。
我主要參考的代碼是CuraEngine,本文主要篇幅也會放在CuraEngine上。而Pango的界面代碼就主要靠我自己發揮了。
Cura和CuraEngine都可以Github上找到,地址:
https://github.com/Ultimaker/CuraEngine
我所參考的版本是15.04,15.06之后Cura和CuraEngine都有較大的改動,但核心流程沒變。所以本文分析的版本也到15.04為止。
言歸正傳,下面我們將開始一步一步揭開CuraEngine把一個模型文件轉換成為gcode的過程。
切片流程
從總體上講,CuraEngine的切片分為五個步驟:
步驟一:模型載入
有一點3D編程經驗的人都知道,計算機中的3D模型大多是以三角形面組合成的表面所包裹的空間來表示的。三角形作為3D模型的基本單元,有結構簡單,通用性強,可組合成任意面的特點;空間坐標中只要三個點就可以表示一個唯一的三角形,兩點只能表示一條直線,而再多的直線也無法組成一個平面;空間中的任意三個不共線的點都可以組成一個三角形,而四個點所組成的四邊形就必需要求四點共面;任意的表面都可以拆解成三角形,一個四邊形可以拆解成兩個三角形,但一個三角形卻沒有辦法用四邊形組合而成。計算機所擅長的事情就是把簡單的事情不斷重復,而三角形正是因為這些特性,成為了計算機3D世界的基石。
CuraEngine內部也是用三角形組合來表示模型的,不過同樣一個三角形組合,卻有無窮多種數據結構來進行存儲。CuraEngine切片的第一步,就是從外部讀入模型數據,轉換成以CuraEngine內部的數據結構所表示的三角形組合。
有了三角形組合還不夠,CuraEngine在載入模型階段還要對三角形進行關聯。兩個三角形共有一條邊的,就可以判斷它們為相鄰三角形。一個三角形有三條邊,所以最多可以有三個相鄰三角形。一般而言,如果模型是封閉的,那它的每一個三角形都會有三個相鄰三角形。
有了三角形的相鄰關系,可以大幅提高下一個步驟分層過程的處理速度。Cura之所以成為當前市場上切片速度最快的軟件,這是其中最顯著的優化之一。
步驟二:分層
如果把模型放在XY平面上,Z軸對應的就是模型高度。我們把XY平面抬高一定高度再與模型的表面相交,就可以得到模型在這個高度上的切片。所謂的分層就是每隔一定高度就用一個XY平面去和模型相交作切片,層與層之間的距離稱為層高。全部層高切完后就可以得到模型在每一個層上的輪廓線。就像是切土豆片一樣,把一個圓的或不圓的異或不管什么奇形怪狀的土豆用菜刀一刀一刀切開,最后就能得到一盤薄如紙片的土豆片,當然那還得你的刀功要足夠好才行。
分層本質上就是一個把3D模型轉化為一系列2D平面的過程,自此之后的所有操作就都是在2D圖形的基礎上進行了。
在前面模型載入階段我說到了CuraEngine埋了一個三角形關聯的伏筆,作用是什么,現在可以揭曉答案了。我們知道,兩個平面相交,得到的是一條直線,一個平面和一個三角形相交,就得到一條線段。當然也有可能什么也得不到,平台平行啦,三角形的三個點都在平面的同一面之類。這些我們可以不管,我們現在只關心和平面有交集的那些三角形即可。我們把一個平面和所有的三角形都相交了一遍,得到了許許多多的線段。但是我們需要的是2D圖形,三角形是2D圖形,四邊形,任意多邊形都是2D圖形,而線段不是。所以我們就要把這些線段試着連成一個多邊形,那么問題來了,要把這些線段連起來,只能兩個兩個地去試,看看它們是不是共端點。粗算一下,每一層都是平方級的復雜度,再算上層數,那就是三次方級。但現在,我們知道了三角形的關聯關系。兩個關聯的三角形,如果都與一個平面相交,那它們的交線一定也是關聯的。如此一來,每一條線段只需要判斷三個與它相鄰三角形,看看與這個平面有沒有交線即可,一下子就把問題的復雜度降了一個次元。速度自然可以有質的提升。
步驟三:划分組件
經過分層之后,我們得到了一疊2D平面圖形。接下來需要做的事情就是對每一層的平面圖形進行跑馬圈地,標記出哪里是外牆、內牆、填充、上下表面、支撐等等。
3D打印在每一層是以組件單位,所謂組件指的就是每一層2D平面圖形里可以連通的區域,比如左圖就可以拆分為黃、綠、藍三個組件。而打印的順序就每打印完一個組件,接着會挑選一個離上一個組件最近的組件作為下一個進行打印,如此循環直至一層的組件全部打印完成;接着會Z軸上升,重復上述步驟打印下一層的所有組件。
至於每一個組件怎么打印,就和我們手工畫畫一樣,先打邊線再對邊線內部填充。邊線可以打印多圈,最外層圈邊線稱為外牆,其它的統稱為內牆,CuraEngine之所以要對內外牆進行區分,是為了可以為它們定制不同的打印參數:外牆會被人觀察到,所以可以采用低速以提高表面質量,內牆只是起增加強度的作用,可以稍稍加快打印速度以節省時間。這些都可以在Cura界面的高級選項里進行配置。
有一點值得注意,這也是我半年打印的經驗:由於FDM擠出裝置的特性所至,擠出機是通過影響加熱腔里的熔絲壓力,間接決定噴嘴擠出速度的。而加熱腔本身對於壓力就有一個緩沖作用,所以擠出機進絲速度的突變並不會使得噴嘴的擠出速度立即跟着變化,而是有一個延遲。這一點在遠端送絲的機器上更為明顯,而恰恰我們公司的主打產品F3CL就是遠端送絲,在Pango中考慮到這個問題,並加上了特殊處理,事實證明的確對打印質量有一定的提升。具體辦法是什么,我先賣個關子,會Pango的專文里進行講解。
內外牆標記完之后就是填充和上下表面的標記了。填充有一個填充率,0%填充率就是無填充,100%就是打成一個密實的平面,上下表面就是填充率為100%的填充。中間的填充率自然介於兩者之間,就像一張漁網,填充率越高網眼越細。
軟件會先把內牆以內部分統統標記成填充,之后再進一步判斷其中有哪些部分要轉換成為上下表面。是哪些部分呢?在Cura的基本設置里有一個上下表面層數的設置,它代表了模型的上下與空氣接觸的表面有幾層,它就在這里會被用到。CuraEngine會把當前層上下n層(上下表面層數)取出來與當前層進行比較,凡是當前層有而上下n層沒有的部分就會被划歸到表皮。而原來的填充區域在割除被划到表皮的部分后剩下的部分就是最終的填充區域。
CuraEngine在處理過程中大量用到了2D圖形運算操作。有關2D圖形的運算,有很多人研究,也被做成許多成熟的庫以供調用。CuraEngine的作者拿來主義,選取了一個他認為比較好用的庫,叫ClipperLib的庫直接內嵌到軟件之中。ClipperLib所使用的2D圖形算法也很著名,叫Vatti's Clipping Algorithm,很復雜,我也沒有完全搞懂,有興趣的讀者要是搞懂了可以多多交流。
ClipperLib的網址是:http://www.angusj.com/delphi/clipper.php
這里我先簡單介紹一下CuraEngine所用到的幾種2D圖形的運算,都是由ClipperLib實現的:交、並、減、偏移。它們與集合操作類似,先看圖:
圖形相交
二元圖形操作,最終結果為兩個圖形共同包含的區域。記作:A * B
圖形相並
二元圖形操作,最終結果為兩個圖形其中之一或兩者所包含的區域。記作:A + B
圖形相減
二元圖形操作,最終結果為屬於前者但不屬於后者的區域。記作:A - B
圖形偏移(外擴)
一元圖形操作,最終結果為圖形區域的邊界向外擴展指定的距離。
圖形偏移(內縮)
一元圖形操作,最終結果為圖形區域的邊界向內收縮指定的距離。內縮與外擴互為逆運算。
這些就是CuraEngine所用到的2D圖形操作。運算不多,卻可以做許許多多的事情。比如上面所說的上下表面計算,就可以用數學公式來表示:
表面(i) = [填充(i) - 層(i + n)] + [填充(i) - 層(i - n)]
填充(i) = 填充(i) - 表面(i)
其中,i為當前層號,n為上下表面層數(可以不一樣)。多簡單,數學就是這么任性!
同樣的,組件里面內外牆,填充怎么划分,只用一個內縮運算就可以搞定:
外牆 = 組件.offset(-線寬)
內牆1 = 組件.offset(-線寬 * 2)
...
內牆n = 組件.offset(-線寬 * (n + 1))
填充 = 組件.offset(-線寬 * (n + 2))
如果模型無需支撐,那組件划分到這里就可以收工了。否則,接下就是計算支撐的時間。
我用CuraEngine半年下來覺得它最大的不足就是在支撐上,這也是我在Pango投入最大精力要改進的地方,這里就先簡單介紹一下CuraEngine所用的支撐算法。
CuraEngine首先把整個打印空間在XY平台上划分成為200um*200um的網格。每個網格的中心點再延Z軸向上作一條直線,這條直線可能會與組成3D模型的三角形相交。三角形與直線的交點以及這個三角形的傾斜度會被記錄到網格里面。
現在每個網格里記錄下了一串被稱為支撐點的列表,每個支撐點包含一個高度和一個傾斜度信息。接下來會對每個網格的支撐點列表按照高度從低到高排序。根據這些信息就可以判斷模型上任意一個點是否需要支撐了,怎么判斷,我們看圖說話:
讓我們從底面開始延着一根網格中心線往上走。起始我們是在模型外部的,當遇到第一個支撐點的時候,就從模型外部進入到了模型內部。我們稱這個支撐點為進點。
繼續向上,遇到了第二個支撐點,又從模型內部又退到了模型外部。我們稱這個支撐點為出點。
接着向上,我們可以發現,進點與出點總是交替出現的。
利用這個規律,對於模型上任何一個點,我們只要找到這個點所對應的網格,再找到這個網格里在這個點以上最近的一個支撐點,我們就可以得到兩個信息:這個點之上是否有模型懸空;這個點上面的懸空點的面的傾斜度是多少。
Cura界面的專家設置里面有支撐角度的設置,如果一個點處於模型懸空部分以下,並且懸空點傾斜度大於支撐角度,那這個點就是需要支撐的。所一個平台上所有的需要支撐的點連接起來圍成的2D圖形就是支撐區域。
CuraEngine所使用的支撐算法比較粗糙,但勝在速度很快。先不說網格化后失去了精度,通過傾斜角度來判斷,模型下方一旦傾斜角發生了突變,像左圖這種從負45
度一下突變成正45度,傾斜角判斷無能為力,除非把它改大到60度,這樣的話,整個模型都會被過度支撐。這樣矯枉過正,既不科學,也浪費材料和打印時間,還會對模型表面質量帶來不好的影響。
科學的支撐算法應該是找到模型局部最低點進行支撐,最低點以上不一定需要支撐。因為FDM材料本身的粘性,使得材料的走線可以有一部分懸空而不坍塌,這個效果被稱為
Overhang,只要上層材料的懸空距離小於一定的值,它就不需要支撐,這個距離以我的經驗應該在1/4到1/2線寬之間。我在Pango中就基於這個思路重新實現了支撐的算法,結果雖然速度不如Cura的支撐算法那么快,但效果非常好,該撐的地方撐,不該撐的地方也不會多此一舉。
Pango的支撐算法我會在以后專文介紹。順帶一說,CuraEngine在下半年做了很大的改動,其中之一就是拋棄了之前的支撐算法,而新的算法也和我上面所講的思想異曲同工。我要聲明的是Pango的支撐算法和CuraEngine誰也沒有抄誰,我的算法是自己拍腦袋想出來的。算是英雄所見略同吧。
支撐范圍確定之后,也和組件一樣,可以有外牆、內牆、填充、上下表面。依樣畫葫蘆即可。CuraEngine對於支撐,只會生成外牆和填充,Pango則會生成更多。
組件和支撐就是CuraEngine在這一步所生成的結果,這一步可以說是整個切片過程的核心。
步驟四:路徑生成
地圈好了,就該在里面種菜了。這一步路徑生成就要開始規划噴頭在不同的組件中怎么運動。路徑按大類來分,有輪廓和填充兩種。
輪廓很簡單,沿着2D圖形的邊線走一圈即可。前一步所生成的外牆、內牆都屬於輪廓,可以直接把它們的圖形以設置里的線寬轉換為輪廓路徑。
填充稍微要復雜一些,2D圖形指定的只是填充的邊界,而生成的路徑則是在邊界的范圍內的條紋或網格結構,就像窗簾或者漁網,如左圖。這兩種就最基本的結構,當然也許你還可以想出其它花式的填充,比如蜂窩狀或者S型,這些在新的Cura或者別的什么切片軟件里可能會實現,但我打印下來還是這兩種基本結構更讓人放心。CuraEngine在專家設置里可以對填充類型進行選擇,里面除了條紋和網格外還有一個自動選項,默認就是自動。自動模式會根據當前的填充率進行切換,當填充率小於20%就用條紋填充,否則使用網格填充。因為網格結構雖然更為合理,但它有一個問題,就是交點的地方會打兩次。填充率越高,交點越密,對打印質量的影響會越大。我們知道,表面就是100%的填充,如果表面用網格打,不但無法打密實,表面還會坑坑窪窪,所以100%填充只能用條紋打,這就是CuraEngine推薦自動模式的原因。
至於填充率,就反映在線與線的間距上。100%填充率間距為0;0%填充率間距無限大,一根線條也不會有。
每個組件獨立的路徑生成好了,還要確定打印的先后順序。順序先好了可以少走彎路,打印速度和質量都會有提升。路徑的順序以先近后遠為基本原則:每打印完一條路徑,當前位置是上一條路徑的終點;在當前層里剩下還沒打印的路徑中挑選一條起點離當前位置最近的一條路徑開打。路徑的起點可以是路徑中的任意一個點,程序會自行判斷。而路徑的終點有兩種可能:對於直線,圖形只有兩個點,終點就是除起點之外的那個點;對於輪廓,終點就是起點,因為輪廓是一個封閉圖形,從它的起點開始沿任意方向走一圈,最后還會回到起點。CuraEngine對路徑選擇做了一個估值,除了考慮到先近后遠外,還順便參考了下一個點相對於當前點的方向,它的物理意義就是減少噴頭轉彎。賽車在直道上開得一定比彎道快,不是么。
路徑的順序也確定了,還有一個問題需要考慮:如果前后兩條路徑首尾相連,那直接走就是了,但大多數情況並非如此,前一條路徑的終點往往和后一條路徑起點之間有一段距離。這時候去往下一點的路上要小心了,肯定不能繼續擠出材料,否則輕則拉絲,重則模型面目全非。這段路噴頭就需要空走,即噴頭只移動,不吐絲,那只要把擠出機停下來不轉就行了嗎?也不行,因為前面分析過,擠出機的速度要傳導到噴嘴,有一個延遲,不是你說停它就立即停下來的。這是FDM打印的通病,解決辦法就是回抽。所謂回抽,就是在空走之前先讓擠出機高速反轉一段材料,這樣就可以瞬間把加熱腔里的材料抽光,再移動過去,中間就不會擠出材料,到了下一個點,在打印之前,先把剛才抽回去的絲再按一樣的長度放回來,繼續打印。回抽可以很好地解決空走拉絲的問題,但是它很慢,以抽一次0.5秒來算的話,如果打印一個表面,0.4線寬,10厘米的距離至少回抽25下,10幾秒鍾的時間一層,幾百上千層打下來,光回抽所用的時間就是幾個小時,是可忍孰不可忍!
CuraEngine給我們提供了解決方案就是Comb,也就是繞路。我們先來看,是不是所有的回抽都是必需的呢?不回抽會拉絲是肯定的,但如果需要空走的路徑本來就要打印的,那拉絲又有何妨。按這個思路,就可以給每個組件設定一個邊界,只要路徑的起點和終點都在這個邊界之內的,空走都不回抽。這樣可以解決80%的問題,但如果是左圖這樣的情況就行不通。
紅色是起點,綠色是終點,直接走過去會走出邊界的范圍。這時我們就要繞一點路,走一條曲線到達我們的目的地。這就是Comb所做的事情,在Cura專家設置里面可以對Comb進行設置,選擇開啟、關閉還有表面不Comb。Comb可以大幅節省打印時間,但是同一個地方打印多次對模型質量還是會有細微的影響,個中利弊,交給用戶自己判斷。
Comb的調整是個細致活,Pango花了相當多的時間來微調Comb功能以求達到更好的效果,過程繁瑣,不再贅述。
步驟五:gcode生成
路徑都生成好了,還需要翻譯對打印機可以實別的gcode代碼才行。這一步花樣不多,按部就班即可。
先讓打印機做一些准備工作:歸零、加熱噴頭和平台、抬高噴頭、擠一小段絲、風扇設置。
從下到上一層一層打印,每層打印之前先用G0抬高Z坐標到相應位置。
按照路徑,每個點生成一條gcode。其中空走G0;邊擠邊走用G1,Cura的設置里有絲材的直徑、線寬,可以算出走這些距離需要擠出多少材料;G0和G1的速度也都在設置里可以調整。
若需回抽,用G1生成一條E軸倒退的代碼。在下一條G1執行之前,再用G1生成一條相應的E軸前進的代碼。
所有層都打完后讓打印機做一些收尾工作:關閉加熱、XY歸零、電機釋放。
生成gcode的過程中,CuraEngine也會模擬一遍打印過程,用來計算出打印所需要的時間和材料長度,這些也會寫在gcode的注釋里供用戶參考。
待續
寫了這么多,Cura的切片流程也只能講個大概,也算是個提綱,希望對大家有所幫助。我計划對於上面的第一個步驟再專文分析。除此之外,還有Cura界面部分以及Cura與CuraEngine的通訊也可以講講。之后就是我半年創作,自我感覺良好到覺得可以超越Cura的Pango,也是不說不快的。
未完待續,敬請期待。