引言
年初开始进入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/daid/Cura
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%的填充。中间的填充率自然介于两者之间,就像一张渔网,填充率越高网眼越细。
软件会先把内墙里面的部分统统标记成填充,之后再进一步判断其中有哪些部分要转换成为上下表面。至于是哪些部分,在设置里会有一个上下表面层数的设置,它代表了模型的上下与空气接触的表面有几层,这里就会用到这个参数,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平台上划分成为50um*50um的网格,每个网格的中心点再延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的注释里供用户参考。
gcode生成不用另文详细分析,但是gcode的说明还是可以专文分析一下,敬请期待。
待续
写了这么多,Cura的切片流程也只能讲个大概,也算是个提纲,希望对大家有所帮助。我计划对于上面的第一个步骤再专文分析。除此之外,还有Cura界面部分以及Cura与CuraEngine的通讯也可以讲讲。之后就是我半年创作,自我感觉良好到觉得可以超越Cura的Pango,也是不说不快的。
未完待续,敬请期待。