今天我们来一起学习这个案例吧:
完成这个案例大体分为4个步骤:
- 在正交视角下绘制一系列的正方体
- 将box指令替换为单独画正方体的每个矩形面的方式,从而控制每个面的图案
- 编排正方体的旋转动作,并给他们的旋转加入一点时间差
- 让每个正方形的样式略有不同
I. 在正交视角下绘制一系列的正方体
首先我们来学习一下关于正交视角的知识
正交视角下3d坐标系呈现一种2.5D的效果。相比于透视(perspective)呈现出的近大远小,在正交视角下,相同大小的物体在不同距离上看起来是一样的。拿游戏来举例:魔兽世界是透视视角,而魔兽争霸是正交视角。
在processing中使用正交视角可以使用以下指令:
ortho(left, right, bottom, top, near, far)
其中 left, right, bottom, top 分别代表相机的左右上下边界,near, far代表远近截距。6个参数定义了一个虚拟空间的立方体空间,我们绘制的物体落在这个空间内的部分才会被看到。
参考Processing官方ortho示例
让我们开始写代码吧:
首先,在setup函数中设置渲染模式为P3D,并创建一个正交相机
void setup() {
// 设置画布大小为 430, 420 且渲染模式为P3D
size(430, 420, P3D);
// 针对RetinaDisplay高像素密度的优化
pixelDensity(displayDensity());
// 设置3d相机为正交相机
ortho(-width/2, width/2, -height/2, height/2);
}
至此我们创建了一个正交相机,其左边界为-width/2,右边界为width/2,上边界为-height/2,下边界为 height/2。width和height是窗口的宽度和高度,在这个程序里分别为430,420。
下一步,我们在draw函数中开始画立方体
void setup(){
...
}
void draw(){
// 每一帧开始时把画布清空成黑色背景
// (括号里为灰度值,0是纯黑255是纯白,中间是灰色)
background(0);
// 在当前坐标下,画一个大小为20的立方体
box(200);
}
运行效果如下
我们发现在左上角有一个白色方块,那个正是我们使用box画出来的方块。显然,他的位置不对。我们需要改变它的绘画位置。
对于3D图形元素,我们不再能像2D元素(rect, ellipse等)一样在指令中直接指定其画图的位置,而是需要通过坐标系变换来改变它的画图位置。
坐标系变换的顺序一般是先平移(translate),然后旋转(rotate),最后缩放(scale)。
需要注意的是,我们当前的坐标原点(0,0,0)并不在屏幕的正中央,而是在屏幕左上角的位置,我们先做一个translate(width/2, height/2),即可把坐标系移动到屏幕中央。之后再使用box指令画图,即可看到正方体移动到屏幕中心了。
void setup(){
...
}
void draw(){
...
// 移动坐标系至屏幕中央
translate(width/2, height/2);
// 在当前坐标下,画一个大小为20的立方体
box(200);
}
运行效果如下
目前立方体看起来就是一个正方形,因为它的一个面正对着摄像机。接下来我们把它做一些旋转,让它有一个立体的感觉。
我们来复习一下坐标轴旋转的相关知识
3D绘图中旋转相关的指令有3个,分别是rotateX(angle), rotateY(angle), rotateZ(angle)。他们的工作方式类似,其中XYZ代表旋转围绕的坐标轴,而括号内的参数代表旋转的角度。角度按当某个坐标轴正对你时的顺时针方向来测定,单位为弧度radians。
为了把正方形变成下面的样子,我们需要让正方形做一些旋转。可以在box(200)指令前加一些坐标轴旋转的指令。
大家猜猜看需要旋转哪几个轴,又需要各旋转多少度?
答案:旋转x轴-30度,旋转y轴-45度
void setup(){
...
}
void draw(){
...
// 移动坐标系至屏幕中央
translate(width/2, height/2);
// 旋转-30度,等于 -pi/6
rotateX(-PI/6);
// 旋转-45度,等于 -pi/4
rotateY(-PI/4);
// 在当前坐标下,画一个大小为20的立方体
box(200);
}
至此,我们完成了单个立方体的绘制。
下一步,我们使用循环将立方体分别画在空间的不同地方。
我们首先来规划一下我们的网格。
参考原图,我们发现空间中的立方体个数是15x15个。对比每个立方体的坐标,我们可以发现他们的z坐标是相同的,只是x、y坐标分布在一个网格上。而由于相邻两个立方体之间的间隔相同,他们的x、y坐标可以用两个等差数列来分别表示。
以10像素为间隔为例,可以构建如下坐标系,其中每个圆点上可以放置一个立方体。
我们一般以 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
......
我们发现
x = -70 + i * 10
y = -70 + j * 10
更近一步的说,假设N是每行/列的点的数量,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)坐标上
- 旋转合适的角度
- 绘制正方体
- 将坐标轴移动回画布的中央
需要注意的是,我们目前的位置已经是经过一次translate(width/2 , height/2)之后所得到的。后续的平移是叠加在先前的平移之上的。
关于使用pushMatrix()和popMatrix()来管理叠加的坐标系: pushMatrix()和popMatrix()必须成对使用;从pushMatrix()到popMatrix()之间的坐标系变换在使用popMatrix()指令后被撤销,使程序的当前坐标系恢复到使用pushMatrix()之前的状态。
在对每个单独的立方体进行坐标系变换前,我们可以使用pushMatrix来保存当前的坐标系;等绘制完box以后,使用popMatrix来撤销针对于这个立方体所做的所有坐标系变换,从而回到push之前的坐标系。
针对于每个立方体的代码即为:
...
// 保存当前的坐标系
pushMatrix();
// 1. 将坐标轴从目前的位置(画布中央)依次移动到每一个(x , y)坐标上
translate(x,y);
// 2. 旋转合适的角度
rotateX(-PI/6);
rotateY(-PI/4);
// 3. 绘制正方体
box(5);
// 4. 将坐标轴移动回画布的中央
popMatrix();
将这些代码放到循环中:
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. 旋转合适的角度
rotateX(-PI/6);
rotateY(-PI/4);
// 3. 绘制正方体。
box(5);
// 4. 将坐标轴移动回画布的中央
popMatrix();
}
}
用它来替换之前单个立方体的绘图代码部分:
void setup() {
...
}
void draw() {
...
// 移动坐标系至屏幕中央
translate(width/2, height/2);
======= 以下部分被替换 =======
// 旋转-30度,等于 -pi/6
//rotateX(-PI/6);
// 旋转-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. 旋转合适的角度
rotateX(-PI/6);
rotateY(-PI/4);
// 3. 绘制正方体。
box(5);
// 4. 将坐标轴移动回画布的中央
popMatrix();
}
}
}
至此,我们完成了第一个步骤,绘制一个立方体矩阵。
为自己鼓鼓掌👏吧,你已经完成了最基础最重要的一步了!