今天我們來一起學習這個案例吧:

完成這個案例大體分為4個步驟:
- 在正交視角下繪制一系列的正方體
- 將box指令替換為單獨畫正方體的每個矩形面的方式,從而控制每個面的圖案
- 編排正方體的旋轉(zhuǎn)動作,并給他們的旋轉(zhuǎn)加入一點時間差
- 讓每個正方形的樣式略有不同
I. 在正交視角下繪制一系列的正方體
首先我們來學習一下關(guān)于正交視角的知識
正交視角下3d坐標系呈現(xiàn)一種2.5D的效果。相比于透視(perspective)呈現(xiàn)出的近大遠小,在正交視角下,相同大小的物體在不同距離上看起來是一樣的。拿游戲來舉例:魔獸世界是透視視角,而魔獸爭霸是正交視角。

在processing中使用正交視角可以使用以下指令:
ortho(left, right, bottom, top, near, far)
其中 left, right, bottom, top 分別代表相機的左右上下邊界,near, far代表遠近截距。6個參數(shù)定義了一個虛擬空間的立方體空間,我們繪制的物體落在這個空間內(nèi)的部分才會被看到。
參考Processing官方ortho示例
讓我們開始寫代碼吧:
首先,在setup函數(shù)中設(shè)置渲染模式為P3D,并創(chuàng)建一個正交相機
void setup() {
// 設(shè)置畫布大小為 430, 420 且渲染模式為P3D
size(430, 420, P3D);
// 針對RetinaDisplay高像素密度的優(yōu)化
pixelDensity(displayDensity());
// 設(shè)置3d相機為正交相機
ortho(-width/2, width/2, -height/2, height/2);
}
至此我們創(chuàng)建了一個正交相機,其左邊界為-width/2,右邊界為width/2,上邊界為-height/2,下邊界為 height/2。width和height是窗口的寬度和高度,在這個程序里分別為430,420。
下一步,我們在draw函數(shù)中開始畫立方體
void setup(){
...
}
void draw(){
// 每一幀開始時把畫布清空成黑色背景
// (括號里為灰度值,0是純黑255是純白,中間是灰色)
background(0);
// 在當前坐標下,畫一個大小為20的立方體
box(200);
}
運行效果如下

我們發(fā)現(xiàn)在左上角有一個白色方塊,那個正是我們使用box畫出來的方塊。顯然,他的位置不對。我們需要改變它的繪畫位置。
對于3D圖形元素,我們不再能像2D元素(rect, ellipse等)一樣在指令中直接指定其畫圖的位置,而是需要通過坐標系變換來改變它的畫圖位置。
坐標系變換的順序一般是先平移(translate),然后旋轉(zhuǎn)(rotate),最后縮放(scale)。
需要注意的是,我們當前的坐標原點(0,0,0)并不在屏幕的正中央,而是在屏幕左上角的位置,我們先做一個translate(width/2, height/2),即可把坐標系移動到屏幕中央。之后再使用box指令畫圖,即可看到正方體移動到屏幕中心了。
void setup(){
...
}
void draw(){
...
// 移動坐標系至屏幕中央
translate(width/2, height/2);
// 在當前坐標下,畫一個大小為20的立方體
box(200);
}
運行效果如下

目前立方體看起來就是一個正方形,因為它的一個面正對著攝像機。接下來我們把它做一些旋轉(zhuǎn),讓它有一個立體的感覺。
我們來復(fù)習一下坐標軸旋轉(zhuǎn)的相關(guān)知識
3D繪圖中旋轉(zhuǎn)相關(guān)的指令有3個,分別是rotateX(angle), rotateY(angle), rotateZ(angle)。他們的工作方式類似,其中XYZ代表旋轉(zhuǎn)圍繞的坐標軸,而括號內(nèi)的參數(shù)代表旋轉(zhuǎn)的角度。角度按當某個坐標軸正對你時的順時針方向來測定,單位為弧度radians。


為了把正方形變成下面的樣子,我們需要讓正方形做一些旋轉(zhuǎn)。可以在box(200)指令前加一些坐標軸旋轉(zhuǎn)的指令。
大家猜猜看需要旋轉(zhuǎn)哪幾個軸,又需要各旋轉(zhuǎn)多少度?

答案:旋轉(zhuǎn)x軸-30度,旋轉(zhuǎn)y軸-45度
void setup(){
...
}
void draw(){
...
// 移動坐標系至屏幕中央
translate(width/2, height/2);
// 旋轉(zhuǎn)-30度,等于 -pi/6
rotateX(-PI/6);
// 旋轉(zhuǎn)-45度,等于 -pi/4
rotateY(-PI/4);
// 在當前坐標下,畫一個大小為20的立方體
box(200);
}

至此,我們完成了單個立方體的繪制。
下一步,我們使用循環(huán)將立方體分別畫在空間的不同地方。
我們首先來規(guī)劃一下我們的網(wǎng)格。

參考原圖,我們發(fā)現(xiàn)空間中的立方體個數(shù)是15x15個。對比每個立方體的坐標,我們可以發(fā)現(xiàn)他們的z坐標是相同的,只是x、y坐標分布在一個網(wǎng)格上。而由于相鄰兩個立方體之間的間隔相同,他們的x、y坐標可以用兩個等差數(shù)列來分別表示。
以10像素為間隔為例,可以構(gòu)建如下坐標系,其中每個圓點上可以放置一個立方體。

我們一般以 i, j 來標記二維矩陣中的每個點,i 代表橫軸上的序號,也就是列號;而 j 代表豎軸上的序號,也就是行號;并且序號是從0開始;所以i = 5, j = 3代表第6行第4列的那個點(上圖黃點)。
我們來做一下計算
i=0, j = 0 -> x= -70, y = -70
i=1, j = 0 -> x= -70 + 10, y = -70
i=1, j = 1 -> x= -70 + 10, y = -70 + 10
i=2, j = 0 -> x= -70 + 20, y = -70
i=2, j = 1 -> x= -70 + 20, y = -70 + 10
i=2, j = 2 -> x= -70 + 20, y = -70 + 20
......
我們發(fā)現(xiàn)
x = -70 + i * 10
y = -70 + j * 10
更近一步的說,假設(shè)N是每行/列的點的數(shù)量,D是間隔的距離
左邊界/上邊界的坐標 = -(N-1)/2 * D = -70
x = -(N-1)/2 * D + i * D = -70 + i * 10
y = -(N-1)/2 * D + j * D = -70 + j * 10
我們只需要讓 i 和 j 分別依次等于0,1,2,3,...14即可得到所有的坐標值,寫成代碼即為
for(int j = 0 ; j < 15 ; j++){
for(int i = 0 ; i < 15 ; i++){
float x = -70 + i * 10;
float y = -70 + j * 10;
}
}
完成坐標的計算以后,我們需要:
- 將坐標軸從目前的位置(畫布中央)依次移動到每一個(x , y)坐標上
- 旋轉(zhuǎn)合適的角度
- 繪制正方體
- 將坐標軸移動回畫布的中央
需要注意的是,我們目前的位置已經(jīng)是經(jīng)過一次translate(width/2 , height/2)之后所得到的。后續(xù)的平移是疊加在先前的平移之上的。
關(guān)于使用pushMatrix()和popMatrix()來管理疊加的坐標系: pushMatrix()和popMatrix()必須成對使用;從pushMatrix()到popMatrix()之間的坐標系變換在使用popMatrix()指令后被撤銷,使程序的當前坐標系恢復(fù)到使用pushMatrix()之前的狀態(tài)。
在對每個單獨的立方體進行坐標系變換前,我們可以使用pushMatrix來保存當前的坐標系;等繪制完box以后,使用popMatrix來撤銷針對于這個立方體所做的所有坐標系變換,從而回到push之前的坐標系。
針對于每個立方體的代碼即為:
...
// 保存當前的坐標系
pushMatrix();
// 1. 將坐標軸從目前的位置(畫布中央)依次移動到每一個(x , y)坐標上
translate(x,y);
// 2. 旋轉(zhuǎn)合適的角度
rotateX(-PI/6);
rotateY(-PI/4);
// 3. 繪制正方體
box(5);
// 4. 將坐標軸移動回畫布的中央
popMatrix();
將這些代碼放到循環(huán)中:
for (int j = 0; j < 15; j++) {
for (int i = 0; i < 15; i++) {
float x = -70 + i * 10;
float y = -70 + j * 10;
// 保存當前的坐標系
pushMatrix();
// 1. 將坐標軸從目前的位置(畫布中央)依次移動(translate)到每一個(x, y)坐標上
translate(x, y);
// 2. 旋轉(zhuǎn)合適的角度
rotateX(-PI/6);
rotateY(-PI/4);
// 3. 繪制正方體。
box(5);
// 4. 將坐標軸移動回畫布的中央
popMatrix();
}
}
用它來替換之前單個立方體的繪圖代碼部分:
void setup() {
...
}
void draw() {
...
// 移動坐標系至屏幕中央
translate(width/2, height/2);
======= 以下部分被替換 =======
// 旋轉(zhuǎn)-30度,等于 -pi/6
//rotateX(-PI/6);
// 旋轉(zhuǎn)-45度,等于 -pi/4
//rotateY(-PI/4);
// 在當前坐標下,畫一個大小為20的立方體
//box(200);
=============================
for (int j = 0; j < 15; j++) {
for (int i = 0; i < 15; i++) {
float x = -70 + i * 10;
float y = -70 + j * 10;
// 保存當前的坐標系
pushMatrix();
// 1. 將坐標軸從目前的位置(畫布中央)依次移動(translate)到每一個(x, y)坐標上
translate(x, y);
// 2. 旋轉(zhuǎn)合適的角度
rotateX(-PI/6);
rotateY(-PI/4);
// 3. 繪制正方體。
box(5);
// 4. 將坐標軸移動回畫布的中央
popMatrix();
}
}
}

至此,我們完成了第一個步驟,繪制一個立方體矩陣。
為自己鼓鼓掌??吧,你已經(jīng)完成了最基礎(chǔ)最重要的一步了!