前言
雖然我們每個人處在三維的世界中,但是計算機屏幕卻是二維的。那么如何將三維的世界展示到二維的屏幕上呢?這時候就需要用到投影。本質(zhì)上,屏幕上展示的是二維圖形,然而二維投影圖形會給我們一種視覺上的錯覺,大腦經(jīng)過聯(lián)想和組合,就會“認為”我們看到了三維物體。

投影是一個數(shù)學(xué)上的術(shù)語,來源于生活。想想看,現(xiàn)在有一個立方體(假設(shè)它的表面是可以透光的,并且可以自由旋轉(zhuǎn)),有一束光從立方體上面照下來,為了簡化問題,這里只考慮平行光,不考慮點光源的情況(在實際的情況中,燈光在三維世界中很重要,影響著物體被如何渲染)。這樣,就在地面上留下了一個影子。類比來說,地面就是計算機的屏幕,影子就是屏幕上繪制的圖形,立方體就是實際要表現(xiàn)的東西。當立方體自由旋轉(zhuǎn)時,把每一次的影子記錄下來,當立方體全方位旋轉(zhuǎn)后,所有影子組合起來,就完完全全地描述了這個三維的立方體。這種情況被稱之為正交投影。
轉(zhuǎn)化
理論知識扯起來很容易,但是如何寫出實際的代碼就要看手上的功夫了。首先,要描述一個三維物體,需要一些度量值,也就是坐標了。三維物體嘛,當然得有三個坐標,在JavaScript中,可以使用數(shù)組來表示坐標結(jié)構(gòu)。每一個坐標數(shù)組包含了三個元素,代表了空間中的一個點(node)。兩個點,就組成了一條線(edge),多條線段就組成了線框圖(wireframe),也就是我想要的效果。三維圖形的基礎(chǔ)在于點,準確地表達每一個點的坐標非常重要。坐標又是基于坐標系的,所以首先的問題在于明確坐標系。在這個項目,我使用右手系,和canvas繪圖標準的坐標系統(tǒng)統(tǒng)一起來 :坐標原點在屏幕左上角,X軸正方向向右,Y軸正方向向下,Z軸正方向從內(nèi)向外。
基礎(chǔ)繪制
有了坐標系統(tǒng),一切都順風(fēng)順水了。在項目中,有一個index.html文件,主要包含了canvas標簽,大小是320X480。然后引用了cube.js文件,這個文件中處理了所有的邏輯。一開始,獲取繪圖環(huán)境,并移動坐標原點到畫布中心。
var c = document.getElementById('world');
var ctx = c.getContext('2d');
ctx.translate(160, 240)
然后就是立方體的一大堆描述數(shù)據(jù),中心在坐標原點,棱長為160。
//這是八個頂點
var node0 = [-80, -80, -80];
var node1 = [-80, -80, 80];
...
var node7 = [ 80, 80, 80];
var nodes = [node0, node1, node2, node3, node4, node5, node6, node7];
//連接頂點,形成十二條棱
var edge0 = [0, 1];
var edge1 = [1, 3];
...
var edge11 = [3, 7];
var edges = [edge0, edge1, edge2, edge3, edge4, edge5, edge6, edge7, edge8, edge9, edge10, edge11];
最后把每條棱繪制出來,為了表現(xiàn)效果,著重繪制了每個頂點,并使用不同的顏色區(qū)分頂點和棱長。
var draw = function(edges, nodes, texts) {
ctx.beginPath();
...
ctx.save();
ctx.translate(0.5,0.5);
for (var e = 0; e < edges.length; e++) {
var n0 = edges[e][0];
var n1 = edges[e][2];
var node0 = nodes[n0];
var node1 = nodes[n1];
ctx.moveTo(node0[0], node0[1]);
ctx.lineTo(node1[0], node1[1]);
}
ctx.stroke();
ctx.restore();
...
}
ctx.stroke是核心的描邊方法,在調(diào)用此方法前用moveTo與lineTo勾畫好路徑。在這一步有兩個點要注意:第一是繪制之前使用beginPath方法來重新開始新的繪圖路徑,防止循環(huán)調(diào)用draw方法時出現(xiàn)殘影現(xiàn)象。第二是繪制前先使用save方法保存繪圖環(huán)境,然后translate(0.5,0.5)偏移0.5像素,繪制完成后復(fù)原,來緩解canvas的像素模糊效應(yīng)(出現(xiàn)的原因是canvas繪圖時最小的繪制單位是一像素,當使用整數(shù)點坐標繪制一像素寬的線段時,線條會被擴展到兩個像素寬,使得線段看起來比較模糊)。
繪制完成后,用瀏覽器觀察,結(jié)果不是很理想:因為屏幕上只有一個正方形而已。其中的原因也很好理解,盡管三維線框圖已經(jīng)被完全繪制在canvas上,但坐標系中的Z軸沒有被表現(xiàn)出來,只看到Z軸方向的投影。小學(xué)生都知道正方體的俯視圖是正方形,項目已經(jīng)成功了一半。
前言中說過,立方體的假設(shè)條件是可以自由旋轉(zhuǎn)?,F(xiàn)在就要用到這個條件,開始旋轉(zhuǎn)立方體,時時刻刻繪制正方體的每一次投影,在旋轉(zhuǎn)的時候,角度改變,頂點的位置由原始頂點的投影得到,三角函數(shù)派上用場。
旋轉(zhuǎn)起來
簡化問題,先考慮繞著Z軸旋轉(zhuǎn)的情況。從本質(zhì)上說,坐標系中的每一個點的坐標就是這個點在坐標軸上的投影位置。

初始頂點(x,y)的位置可以用下方的公式一表示:

當繞Z軸旋轉(zhuǎn)β角度時,新的頂點(x',y')的位置用公式二表示:

根據(jù)腦子里還沒忘光的三角公式,可以把公式二展開成公式三:

將公式一代入公式三,最后得到核心的公式四:

有了核心的公式四,把數(shù)學(xué)語言翻譯成編程語言,寫到cube.js文件中。
var rotateZ3D = function(theta, nodes) {
var sin_t = Math.sin(theta);
var cos_t = Math.cos(theta);
for (var n = 0; n < nodes.length; n++) {
var node = nodes[n];
var x = node[0];
var y = node[1];
node[0] = x * cos_t - y * sin_t;
node[1] = y * cos_t + x * sin_t;
}
}
X軸和Y軸上的旋轉(zhuǎn)與此類似。接下來的處理很簡單,監(jiān)聽鼠標拖動的事件,清空畫布ctx.clearRect(-160, -240, 320, 480);,根據(jù)拖動的距離和方向設(shè)定旋轉(zhuǎn)的角度以及所繞的坐標軸,以此為參數(shù)調(diào)用旋轉(zhuǎn)方法。更新頂點坐標后,以新的坐標點繪制線框圖。為了便于理解,在畫布上,我還繪制了坐標軸,在每個點上方添加了數(shù)字標識,這些都不是重點,就不一一細說了。
鏈接
后記
以前覺得所學(xué)到的各種高深的數(shù)學(xué)知識并沒有什么用處,完全是為了應(yīng)付考試才聽了一點兒?,F(xiàn)在看來,是我太年輕。教育與實踐的脫鉤讓很多學(xué)生不能夠?qū)W以致用。知識其實并不是力量,能夠運用知識才是力量,我正在努力挖掘自身的力量。