在〈實作海龜繪圖〉中,海龜在沙灘上爬行,爬過的路線會留下痕跡,如果 現在這海龜爬入海中了,游過的路線會留下痕跡,那會是什麼情況?因為在海中,海龜可不只會在一個平面上,而會是在立體空間中游動的啊!
簡單的構想?
因為我們已經實作過 2D 的海龜繪圖了,若想實現 3D 海龜繪圖,一個簡單的構想是,除了平面上的轉動之外,多記錄海龜抬頭的角度。
這樣做的好處是,可以在〈實作海龜繪圖〉中的程式碼基礎上修改,增加 φ 方面的計算,這並不難,然而,海龜就只會有原地轉動與抬頭兩個自由度,另一個問題是,如果你想畫個三角形,而這個三角形構成的平面,必須與 xy 平面夾 30 度,那麼海龜應該怎麼動作?
你也許會想,讓海龜前進一個邊長,轉動 120 度,抬頭 30 度,再前進一個邊長之類的,很可惜地,這是錯的,實際上,第一次轉動必須是 125.264 度(試著計算看看吧!)也就是說,這種方式雖行得通,但計算上非常麻煩。
改用向量來思考
實際上,實作海龜繪圖時,可以用向量來思考,假設一開始海龜頭朝 x 軸方向,而海龜本身有個座標系統 x'、y',可以想像一下你坐在海龜上,這個 x'、y' 是以你的觀點看到的座標。
目前在上面的圖中,海龜本身 x'、y' 座標系統與 x、y 座標系統是一致的,如果用 (1, 0) 表示 x' 方向的單位向量,(0, 1) 表示 y' 方向的單位向量,現在海龜轉動 θ 角度的話:
那麼從 x、y 座標系統來看,轉動前 x' 方向的單位向量 (1, 0) 以及 y' 方向的單位向量 (0, 1),轉動後 x' 方向的單位向量,就是將 (1, 0) 這個座標繞 z 軸轉動,而轉動後 y' 方向的單位向量,就是將 (0, 1) 這個座標繞 z 軸轉動。
因此,你必須某個座標 (x, y) 若繞 z 軸轉動後,新座標該如何計算,這可以參考〈三 維直角座標之繞軸旋轉〉。
一個簡單的情況是 θ 為 45 度時,那麼從 x、y 座標系統來看,x' 方向的單位向量會變成 (1 / sqrt(2), 1 / sqrt(2)),y' 方向的單位向量會變成 (- 1 / sqrt(2), 1 / sqrt(2))。如果海龜現在朝著 x' 方向前進 L 的話,實際上產生的向量會是 (L * 1 / sqrt(2), L * 1 / sqrt(2)),也就是 L 乘上 x' 的單位向量,得到的是下圖中的橘色向量。
因此,如果不是從原點出發,而是從某個點 (x1, y2) 出發,那麼海龜新的 xy 座標就會是 (x1 + L * 1 / sqrt(2), y1 + L * 1 / sqrt(2))。
在〈實作海龜繪圖〉中,其實就只是假設海龜只會在 x' 方向移動,因而可以不用考慮 y' 方向上移動,只用純綷的三角函式來進行計算,而不用使用向量來思考。
然而,一旦改用向量的方式來思考,那麼你也可以讓海龜在 y' 方向上移動,這就像是…可以橫著走的海龜,是有點怪啦!把海龜想像成一個有全向輪的機器人,可能比較符合這樣的移動方式吧!只不過,海龜繪圖這名字比較有名,就還是用海龜 吧!
來到 3D 海龜繪圖
使用向量來思考海龜繪圖,接著要擴充到 3D 時就簡單多了,我們有個座標系統 (x, y, z),然而,你坐在海龜上,看到的座標系統為 (x', y', z'),同樣地,一開始我們使用 (1, 0, 0)、(0, 1, 0)、(0, 0, 1),從 (x, y, z) 的觀點來表示 x'、y'、z' 軸上的單位向量。
同樣地,無論海龜是繞 x 軸、y 軸或 z 軸轉動,都透過〈三 維直角座標之繞軸旋轉〉計算,得到從單位向量的新值,然後,如果海龜現在朝著 x' 方向前進 L 的話,就是用 x' 方向的單位向量乘上 L,然後加上目前海龜的座標點,就可以得到海龜前進後新的座標點。
當然,就這個方法而言,除了在 x' 方向移動之外,你也可以讓海龜在 y' 或者 z' 方向移動(就別管海龜能不能側著游了啦!)
根據這樣的觀念,我們可以實作出以下的 3D 海龜繪圖程式碼,為了能在 3D 空間畫線,使用到〈3D
線段〉實作的 polyline3D
模組:
function x(pt) = pt[0];
function y(pt) = pt[1];
function z(pt) = pt[2];
function pt3D(x, y, z) = [x, y, z];
function turtle3D(pt, coordVt) = [pt, coordVt];
function getPt(turtle) = turtle[0];
function getCoordVt(turtle) = turtle[1];
function plus(pt, n) = pt3D(x(pt) + n, y(pt) + n, z(pt) + n);
function minus(pt, n) = pt3D(x(pt) - n, y(pt) - n, z(pt) + n);
function mlt(pt, n) = pt3D(x(pt) * n, y(pt) * n, z(pt) * n);
function div(pt, n) = pt3D(x(pt) / n, y(pt) / n, z(pt) / n);
function neg(pt, n) = mlt(pt, -1);
function ptPlus(pt1, pt2) = pt3D(x(pt1) + x(pt2), y(pt1) + y(pt2), z(pt1) + z(pt2));
// 在海龜的 x' 軸方向移動
function moveX(turtle, leng) = turtle3D(
ptPlus(getPt(turtle), mlt(getCoordVt(turtle)[0], leng)),
getCoordVt(turtle)
);
// 在海龜的 y' 軸方向移動
function moveY(turtle, leng) = turtle3D(
ptPlus(getPt(turtle), mlt(getCoordVt(turtle)[1], leng)),
getCoordVt(turtle)
);
// 在海龜的 z' 軸方向移動
function moveZ(turtle, leng) = turtle3D(
ptPlus(getPt(turtle), mlt(getCoordVt(turtle)[2], leng)),
getCoordVt(turtle)
);
// 繞海龜的 x' 軸轉動,計算 x' 方向單位向量
function turnX(turtle, a) = turtle3D(
getPt(turtle),
[
getCoordVt(turtle)[0],
ptPlus(mlt(getCoordVt(turtle)[1], cos(a)), mlt(getCoordVt(turtle)[2], sin(a))),
ptPlus(mlt(neg(getCoordVt(turtle)[1]), sin(a)), mlt(getCoordVt(turtle)[2], cos(a)))
]
);
// 繞海龜的 y' 軸轉動,計算 y' 方向單位向量
function turnY(turtle, a) = turtle3D(
getPt(turtle),
[
ptPlus(mlt(getCoordVt(turtle)[0], cos(a)), mlt(neg(getCoordVt(turtle)[2]), sin(a))),
getCoordVt(turtle)[1],
ptPlus(mlt(getCoordVt(turtle)[0], sin(a)), mlt(getCoordVt(turtle)[2],cos(a)))
]
);
// 繞海龜的 z' 軸轉動,計算 z' 方向單位向量
function turnZ(turtle, a) = turtle3D(
getPt(turtle),
[
ptPlus(mlt(getCoordVt(turtle)[0], cos(a)), mlt(getCoordVt(turtle)[1],sin(a))),
ptPlus(mlt(neg(getCoordVt(turtle)[0]), sin(a)), mlt(getCoordVt(turtle)[1], cos(a))),
getCoordVt(turtle)[2],
]
);
// 3D 線段
module line3D(p1, p2, thickness, fn = 24) {
$fn = fn;
hull() {
translate(p1) sphere(thickness / 2);
translate(p2) sphere(thickness / 2);
}
}
// 根據多個點繪製 3D 線段
module polyline3D(points, thickness, fn) {
module polyline3D_inner(points, index) {
if(index < len(points)) {
line3D(points[index - 1], points[index], thickness, fn);
polyline3D_inner(points, index + 1);
}
}
polyline3D_inner(points, 1);
}
t = turtle3D(
pt3D(0, 0, 0),
[pt3D(1, 0, 0), pt3D(0, 1, 0), pt3D(0, 0, 1)] // 海龜座標單位向量
);
leng = 10;
t2 = moveX(t, leng);
polyline3D([getPt(t), getPt(t2)], 1 , 4);
t3 = moveX(turnZ(t2, 120), leng);
polyline3D([getPt(t2), getPt(t3)], 1 , 4);
t4 = moveX(turnZ(t3, 120), leng);
polyline3D([getPt(t3), getPt(t4)], 1 , 4);
既然是 3D 海龜繪圖,當然也可以實行 2D 海龜繪圖囉!上面的程式碼最後在 xy 平面畫了個三角形:
如果想實現方才的需求,畫個三角形,而這個三角形構成的平面,必須與 xy 平面夾 30 度,該怎麼做呢?你只要先讓海龜繞 x' 軸轉 30 度,那麼海龜現在就是與 xy 平面夾 30 度啦!接著就是以海龜的觀點畫三角形,也就是繞 z' 轉 120 畫邊長,重複三次就可以了:
程式碼同前...略…
leng = 10;
t2 = moveX(turnX(t, 30), leng); // 只要改這邊,先繞 x' 軸轉 30 度就可以了
polyline3D([getPt(t), getPt(t2)], 1 , 4);
t3 = moveX(turnZ(t2, 120), leng);
polyline3D([getPt(t2), getPt(t3)], 1 , 4);
t4 = moveX(turnZ(t3, 120), leng);
polyline3D([getPt(t3), getPt(t4)], 1 , 4);
當然,實現 3D 海龜繪圖,需要多一點的數學,不過這值得,之後就可以畫些三維的圖樣了,想想看,你有辦法畫個 3D 版本的〈樹 木曲線〉嗎?