從一棵樹到一片林的創作過程

透過程式繪製一棵樹在程式繪圖領域是很基本的練習,所有訓練有素的程式設計人員都可以反射性地想到利用「遞迴函式」來畫出樹的結構。我所繪製的這片樹林也不例外,是從「遞迴函式」開始出發。一開始的版本只是用一堆直線段構成的樹,僵硬又無趣,然後我分成好幾個階段慢慢改進,加入粗細漸變、隨機抖動的樹枝、陰影明暗、樹皮紋理等,直到出現像鉛筆素描一般的寫實質感。你可能會猜想我大概用了 3D繪圖、打光效果、材質貼圖等技術來繪製這幅作品,但其實完全沒有;這幅作品是純粹的 2D繪圖,也沒有使用任何點陣圖材質,從頭到尾只有使用點(point)、圓(ellipse)、線(line)、以及基本的數學計算來繪製,這在Processing或是其他的程式繪圖工具裡都是很基本的功能。
在這篇文章裡我會展示一些圖片和程式碼片段,並且說明這個作品是如何從最初的版本漸漸變成像極了鉛筆素描的作品。
從最基本的線段開始
最開始的版本,我只是用簡單的遞迴函式在黑色的背景上繪出白色的樹枝,樹枝是用Processing裡的 line() 一筆繪製出來,然後在每次遞迴呼叫的時候隨機的長出2或3個不同角度的分支,粗細也隨著每次的呼叫遞減。通常在 Processing裡面你可以在遞迴函式裡用 pushMatirx() 來改變座標原點和座標角度來簡單的畫出分支並減少數學計算,不過我認為那樣會創造大量的記憶體堆疊,所以我實際上是用簡單的極座標計算出每一個分支的起點以及終點位置,然後直接在原始的座標系統上繪圖。
加入粗細和抖動的效果
目前已經有了樹的雛形,不過這樣畫出來的樹缺乏生命力,我開始思考如何讓樹枝由粗慢慢變細。這並不困難,只要將原本的直線段分成好幾個不同的小線段,然後讓每個小線段的粗細慢慢遞減。當然,為了增進程式的效率,比較長的樹枝我會分成比較多小線段,比較短的樹枝就可以不用分成太多小線段,這樣可以在畫面的平順和效能之間取得平衡。將樹枝分成許多小線段的另一個好處是,可以將每個小線段的角度隨機改變一下,連接起來之後就更像真實的樹枝了。
加入陰影
一開始我繪製的每棵樹都只有單一個顏色,雖然可以改變每棵樹的灰階來營造遠近景深的效果,但整體畫面顯得很平面,所以現在開始思考如何在每個樹枝上做出明暗變化。同樣的這並不難做到,我把原本的每一個小線段從左到右分成6到8等份,然後用寬度比較細的線條依序繪製,每次繪製的時候就將顏色的亮度變暗一些,做出明暗的效果。為了增進程式效率,比較粗的樹枝我會分成比較多層次,比較細的樹枝就可以減少層次。
加入樹皮紋理
我最初嘗試過用網路上找到的樹皮材質點陣圖來貼圖,但實際上的效果並不好,所以我還是決定繼續用原始的方法繪製。在繪製完每一個小線段之後,我就會在該線段的範圍內隨機加入一些大小不一的小圓點,以及一些大圓點,這樣就產生一棵看起來很不錯的樹了。
從一棵樹到一片林
為了讓畫面看起來細膩平順,我會盡可能將樹枝切分成非常小的片段,因此每一棵樹其實都是由成千上萬的小線段和圓點繪製出來的,在我的電腦上 (我的配備是 CPU i7 / GPU 1050Ti) 即使只是畫一棵樹都要花費好幾秒的時間,想像你要畫出上百棵的樹,你的程式會馬上陷入卡住無法回應的狀態。因此在Processing裡面我是透過 draw() 在每個影格 (frame) 只畫出一個小線段,畫完一棵樹之後再繼續畫下一棵樹,這樣就可以避免程式卡住的困境,也可以用動畫的方式觀察樹林慢慢生長的過程。實際上我的程式大概花了20分鐘來完成最終的作品。
結論
即使只用最簡單的工具,也可以做出令人訝異的效果,希望你也能從這篇文章中得到一些啟發。如果你有興趣的話,可以到我的網站上玩玩我寫的參數設計工具,看看這些樹林是如何被繪製出來的。




在這篇文章裡我會展示一些圖片和程式碼片段,並且說明這個作品是如何從最初的版本漸漸變成像極了鉛筆素描的作品。
從最基本的線段開始
最開始的版本,我只是用簡單的遞迴函式在黑色的背景上繪出白色的樹枝,樹枝是用Processing裡的 line() 一筆繪製出來,然後在每次遞迴呼叫的時候隨機的長出2或3個不同角度的分支,粗細也隨著每次的呼叫遞減。通常在 Processing裡面你可以在遞迴函式裡用 pushMatirx() 來改變座標原點和座標角度來簡單的畫出分支並減少數學計算,不過我認為那樣會創造大量的記憶體堆疊,所以我實際上是用簡單的極座標計算出每一個分支的起點以及終點位置,然後直接在原始的座標系統上繪圖。
加入粗細和抖動的效果
目前已經有了樹的雛形,不過這樣畫出來的樹缺乏生命力,我開始思考如何讓樹枝由粗慢慢變細。這並不困難,只要將原本的直線段分成好幾個不同的小線段,然後讓每個小線段的粗細慢慢遞減。當然,為了增進程式的效率,比較長的樹枝我會分成比較多小線段,比較短的樹枝就可以不用分成太多小線段,這樣可以在畫面的平順和效能之間取得平衡。將樹枝分成許多小線段的另一個好處是,可以將每個小線段的角度隨機改變一下,連接起來之後就更像真實的樹枝了。



下面這段程式碼反映了上述的概念。這不是我實際的程式碼,我將它簡化以易於理解。
void createBranch(float startX, float startY, float branchAngle, float branchLength, float branchWeight, int twigSteps, int branchColor){
//Begin to draw branch
float twigStartX = startX;
float twigStartY = startY;
float twigLength = branchLength / twigSteps;
float branchEndWeight = branchWeight * 0.8;
//Divide each branch into many twigs
for(int i = 0; i < twigSteps; i++){
float twigRandAngle = random(-PI, PI) * 0.05;
float twigRandLength = random(0.5, 1.5);
float twigEndX = twigStartX + cos(branchAngle + twigRandAngle) * (twigLength * twigRandLength);
float twigEndY = twigStartY + sin(branchAngle + twigRandAngle) * (twigLength * twigRandLength);
float twigWeight = map(i, 0, twigSteps, branchWeight, branchEndWeight);
drawTwig(twigStartX, twigStartY, twigEndX, twigEndY, twigWeight, branchColor);
twigStartX = twigEndX;
twigStartY = twigEndY;
}
//Create sub branches
if( branchWeight > 1 ){
int branchNum = (int)random(2, 4);
for(int i = 0; i < branchNum; i++){
float newBranchX = twigStartX;
float newBranchY = twigStartY;
float newBranchAngle = branchAngle + random(-PI/4, PI/4);
float newBranchLength = branchLength * 0.6;
float newBranchWeight = branchEndWeight;
int newTwigSteps = (int)twigSteps*0.9;
int newBranchColor = (int)branchColor*0.9;
createBranch(newBranchX , newBranchY , newBranchAngle, newBranchLength , newBranchWeight , newTwigSteps , newBranchColor);
}
}
}
加入陰影
一開始我繪製的每棵樹都只有單一個顏色,雖然可以改變每棵樹的灰階來營造遠近景深的效果,但整體畫面顯得很平面,所以現在開始思考如何在每個樹枝上做出明暗變化。同樣的這並不難做到,我把原本的每一個小線段從左到右分成6到8等份,然後用寬度比較細的線條依序繪製,每次繪製的時候就將顏色的亮度變暗一些,做出明暗的效果。為了增進程式效率,比較粗的樹枝我會分成比較多層次,比較細的樹枝就可以減少層次。
加入樹皮紋理
我最初嘗試過用網路上找到的樹皮材質點陣圖來貼圖,但實際上的效果並不好,所以我還是決定繼續用原始的方法繪製。在繪製完每一個小線段之後,我就會在該線段的範圍內隨機加入一些大小不一的小圓點,以及一些大圓點,這樣就產生一棵看起來很不錯的樹了。



下面這段程式碼說明我如何繪製陰影和紋理。
void drawTwig(float twigStartX, float twigStartY, float twigEndX, float twigEndY, float twigWeight, int twigColor){
//Use coordinate transformation to simplify calculation
pushMatrix();
translate(twigStartX, twigStartY);
rotate(atan2(twigEndY- twigStartY, twigEndX- twigStartX));
//Draw twig from light to dark
int gradientStep = 6;
float gradientWeight = twigWeight/gradientStep;
for(int i = 0; i < gradientStep; i++){
float x1 = -twigWeight/2 + i * gradientWeight ;
float y1 = 0;
float x2 = -twigWeight/2 + i * gradientWeight ;
float y2 = twigEndY - twigStartY;
int c = twigColor - (gradientStep/2) * 5 + i * 5;
stroke(c);
strokeWeight(gradientWeight);
line(x1, y1, x2, y2);
}
//Draw dots
for(int i = 0; i < twigWeight*2; i++){
float dotX = random(-twigWeight/2, twigWeight/2);
float dotY = random(0, twigEndY - twigStartY);
stroke(twigColor);
if(random(0, 1) < 0.2){
strokeWeight(twigWeight * random( .2, .4)); //Draw big dot
}else{
strokeWeight(twigWeight * random( .01, .1)); //Draw small dot
}
point(dotX, dotY);
}
popMatrix();
}
從一棵樹到一片林
為了讓畫面看起來細膩平順,我會盡可能將樹枝切分成非常小的片段,因此每一棵樹其實都是由成千上萬的小線段和圓點繪製出來的,在我的電腦上 (我的配備是 CPU i7 / GPU 1050Ti) 即使只是畫一棵樹都要花費好幾秒的時間,想像你要畫出上百棵的樹,你的程式會馬上陷入卡住無法回應的狀態。因此在Processing裡面我是透過 draw() 在每個影格 (frame) 只畫出一個小線段,畫完一棵樹之後再繼續畫下一棵樹,這樣就可以避免程式卡住的困境,也可以用動畫的方式觀察樹林慢慢生長的過程。實際上我的程式大概花了20分鐘來完成最終的作品。
結論
即使只用最簡單的工具,也可以做出令人訝異的效果,希望你也能從這篇文章中得到一些啟發。如果你有興趣的話,可以到我的網站上玩玩我寫的參數設計工具,看看這些樹林是如何被繪製出來的。



