有句老話說:「程式是照你寫的跑,不是照你想的跑」,然而有時候程式照開發者想的跑時,它不見得是正確的,程式不照寫的跑時,也不見得是錯誤的,如果沒有認清楚問題的本質,那執行結果的對或錯,都只是開發者腦中的幻想罷了。

為了實現路徑擠出

3D建模時,經常有個需求:給定一個2D圖案,依照指定的隨意路徑產生3D模型。例如,給定2D圖案與路徑,建立一條水管狀模型,在3D建模領域中,這功能稱為路徑擠出(path extrude),比較專業點的建模軟體中,應該都會提供這類功能,不過,有時擠出結果不是建模師想要的,甚至以為是個程式臭蟲,這是因為許多人對路徑擠出存在著幻想,我一開始也是如此!

OpenSCAD本身沒有隨意路徑擠出的模組,因而當初在為其建立程式庫時,隨意路徑擠出自然就成為實現目標之一,可惜的是,網路上幾乎沒有文件,討論如何用程式碼來實現路徑擠出,既然如此,那就從簡單的需求開始吧!

OpenSCAD本身有個線性擠出(linear_extrude)模組,名為線性,基本上就是將2D圖案沿著直線擠出,這只要有兩個2D圖案分別作為底面與頂面,就可以自行實現。

這麼說來,更複雜的擠出,可視為多個2D圖案形成的多個面,依需求調整面的法向量,並移動至指定座標點上,接下來,只要有個模組能掃掠(Sweep)這些面,就可以建立3D模型,這過程就像是為燈籠骨架貼上外皮。

也就是說,擠出的本質就是將2D變3D,既然如此,就必須提供額外資訊,有這些額外資訊才能構成3D模型。對線性擠出來說,必須多一個頂面,對於複雜的擠出,就依需求看要額外提供哪些面,3D建模領域中,2D圖案形成的面稱為斷面或切面,而隨意路徑擠出的問題就在於:這些斷面的來源會是什麼?

「為了不令問題一開始過於複雜,得先簡化問題。」假設斷面來源是使用者提供,畢竟有些以滑鼠操作為主的3D建模軟體,確實也是對使用者手動提供的斷面進行掃掠,這類功能有時也稱為放樣成形(loft),在OpenSCAD要實現這個功能並不難,只要將每個斷面上的座標點,視為最後3D模型的頂點,依序連接就可以了(參考https://bit.ly/31nY75d)。

自動產生斷面

手動提供斷面資訊,在以滑鼠操作為主的建模軟體中還算可行,然而在OpenSCAD中,使用者須明確提供斷面上的座標點,但這太過麻煩了,現在問題就清楚地座落到:怎麼從路徑自動產生斷面?「雖然是個子問題,然而還是可以再拆解出更小的子問題」,一是路徑是在斷面上哪個位置?二是路徑與斷面怎麼相交?

在3D建模軟體中,若提供路徑擠出,兩個問題或許都會提供可設定的選項。

在OpenSCAD中實現時,對於選項一,我假設2D圖案的原點就是路徑經過的點,依此實現了cross_sections函式(https://bit.ly/2I7gWlC),可由使用者提供斷面旋轉角度,自行決定路徑與斷面怎麼相交,這解決了選項二的問題,而該函式最後傳回自動產生的多個斷面。

提供斷面旋轉角度,總是比提供斷面座標來得簡單多了,而且在使用者的路徑是來自於某個數學公式時,公式本身若有角度相關的參數,就可以提供函式使用,自然地,在這個函式作為基礎下,像是圓路徑、阿基米德螺旋,或是球體路徑等可用數學公式描述的路徑,在先前假設的前提下,就可以精確地建立路徑擠出的結果。

當然,2D圖案原點是路徑經過斷面的點,不過就是選項之一,路徑可能經過斷面上的頂點或邊,基於不同的選項,可以撰寫不同的函式,我確實也實現了幾個常用的函式,像是paths2sections(https://bit.ly/2ZcXYj0),就可以用多條經過斷面頂點的路徑,來決定斷面如何產生。

只有點座標的時候

該是來處理使用者只提供路徑時的問題了,因為使用者只提供構成路徑的座標點資訊,於是,我假設2D圖案的原點是路徑經過斷面的點,斷面始終於與路徑正交,從而將問題簡化為:如何自動產生與路徑正交的斷面?

既然是正交,我想到路徑上的兩個點形成的向量,可以作為斷面法向量,而有向量就有了旋轉斷面的依據,於是也就有了路徑擠出的第一個版本。

第一個版本在大部份的情境之下,都可以運作,然而,在半年後被提出了ISSUE(https://bit.ly/2IPGe98),在擠出的過程當中,若路徑沿著Z軸,會發生模型斷面扭轉180度的結果,這一度被認為是個臭蟲,Blender也曾有過這個問題(https://bit.ly/2KHBcMq),發生的原因在於,只採用歐拉角繞XYZ軸來旋轉斷面,未提供斷面本身繞法向量旋轉的資訊,因此,路徑擠出的第二個版本,解決方式自然就是能繞法方量來旋轉斷面,這解決了模型斷面扭轉的問題。

然而,過沒多久時間之後,又有人提出了另一個ISSUE(https://bit.ly/2F0VkVO),因為在盤旋(helix)路徑時,第一個版本與第二個版本的路徑擠出結果不同,他認為,第一個版本才是他「想要的正確結果」,這突然讓我想到,其實,兩個版本的實現都是正確的,畢竟「問題的本質」在於,使用者只提供了路徑上的座標點!

提供足夠的資訊是必要的,如果有斷面就要提供足夠斷面,若有公式就提供必要的參數,這樣才能正確地進行路徑擠出,然而若真的只有提供路徑上的座標點呢?那麼,就只能從這些點中猜測,使用者想要的是什麼樣的路徑擠出,猜測的方式可以是使用歐拉角針對XYZ軸來旋轉,或者是令斷面的轉動是針對特定軸旋轉。

在採用歐拉角而最後的模型產生扭轉時,第一個版本是錯的嗎?其實,它的行為是對的,只不過不是使用者腦袋中想要的,使用者覺得它是錯的,是因為腦中存在著未提供給路徑擠出模組的資訊;相對地,第二個版本在針對特定軸旋轉而盤旋結果非預期時,也是使用者沒將腦中的資訊提供給模組,然後就以為模型結果錯了,然而那是模組的正確行為;最終,我在模組上提供了個參數,請使用者自行指定該怎麼「猜」,也就是要明確指定歐拉角或是特定軸旋轉。

簡化、分解、理解問題的本質

只要提供路徑,就能從2D圖案產生3D模型,因此,隨意路徑擠出的這個功能容易令人產生幻想,甚至令我忽略了一開始是怎麼簡化、分解問題,提供子問題必要資訊,來逐一解決各個子問題的過程,也忘了隨意路徑擠出的問題本質,正是沒有提供足夠資訊!

也許大部份開發者不會接觸3D程式,也從不會實現路徑擠出這類功能,然而,現實的軟體開發中,有不少特性或功能,其實就像路徑擠出這樣,容易令人存在幻想吧!

「程式是照你寫的跑,不是照你想的跑」的前提是,清楚地理解問題的本質,否則,想的是對是錯,從一開始就無法定義,那又何以判斷寫出來的執行結果是否正確呢?

專欄作者

熱門新聞

Advertisement