實作 3D 海龜繪圖


在〈實作海龜繪圖〉中,海龜在沙灘上爬行,爬過的路線會留下痕跡,如果 現在這海龜爬入海中了,游過的路線會留下痕跡,那會是什麼情況?因為在海中,海龜可不只會在一個平面上,而會是在立體空間中游動的啊!

實作 3D 海龜繪圖

簡單的構想?

因為我們已經實作過 2D 的海龜繪圖了,若想實現 3D 海龜繪圖,一個簡單的構想是,除了平面上的轉動之外,多記錄海龜抬頭的角度。

實作 3D 海龜繪圖

這樣做的好處是,可以在〈實作海龜繪圖〉中的程式碼基礎上修改,增加 φ 方面的計算,這並不難,然而,海龜就只會有原地轉動與抬頭兩個自由度,另一個問題是,如果你想畫個三角形,而這個三角形構成的平面,必須與 xy 平面夾 30 度,那麼海龜應該怎麼動作?

實作 3D 海龜繪圖

你也許會想,讓海龜前進一個邊長,轉動 120 度,抬頭 30 度,再前進一個邊長之類的,很可惜地,這是錯的,實際上,第一次轉動必須是 125.264 度(試著計算看看吧!)也就是說,這種方式雖行得通,但計算上非常麻煩。

改用向量來思考

實際上,實作海龜繪圖時,可以用向量來思考,假設一開始海龜頭朝 x 軸方向,而海龜本身有個座標系統 x'、y',可以想像一下你坐在海龜上,這個 x'、y' 是以你的觀點看到的座標。

實作 3D 海龜繪圖

目前在上面的圖中,海龜本身 x'、y' 座標系統與 x、y 座標系統是一致的,如果用 (1, 0) 表示 x' 方向的單位向量,(0, 1) 表示 y' 方向的單位向量,現在海龜轉動 θ 角度的話:

實作 3D 海龜繪圖

那麼從 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' 的單位向量,得到的是下圖中的橘色向量。

實作 3D 海龜繪圖

因此,如果不是從原點出發,而是從某個點 (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' 軸上的單位向量。

實作 3D 海龜繪圖

同樣地,無論海龜是繞 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 平面畫了個三角形:

實作 3D 海龜繪圖

如果想實現方才的需求,畫個三角形,而這個三角形構成的平面,必須與 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 海龜繪圖,需要多一點的數學,不過這值得,之後就可以畫些三維的圖樣了,想想看,你有辦法畫個 3D 版本的〈樹 木曲線〉嗎?