Processing:平移和旋转

1. z轴指任意一点的深度。这貌似很抽象,这么理解吧,x轴和y轴分别代表萤幕的宽和高,那么z轴就位于你萤幕的背后(负值)或之前(正值)。

2. 用二维的方式也可以做出三维的效果,比如下例展示一个由小变大的矩形,看起来就像它从远处向我们走来:
Code

float r = 8;

void setup() {
  size(200,200);
}

void draw() {
  background(255);
  
  // 在萤幕中央显示一个矩形
  stroke(0);
  fill(175);
  rectMode(CENTER);
  rect(width/2,height/2,r,r);
  
  // 增大矩形
  r++ ;
  
  // 反复开启矩形的动态
  if (r > width) {
    r = 0;
  }
}

3. 幸运的是,Processing懂得透视,并选取适宜的二维像素来创建三维效果。我们应当认识到,在我们进入3D像素坐标的同时,一部分的控制必须交由p5的渲染器来完成。你不在能像控制2D图形那样精确的控制它们的像素位置,因为XY位置将被调整用来计算3D透视。

4. 下图为笛卡尔3D系统的坐标图示:

3-D coordinate
3-D coordinate

5. 注意,这么写是错误的:

rect(x, y, z, w, h);

6. 在p5的世界为形状使用3D坐标,我们必须学习一个新的函数,叫做translate()。函数translate()移动原点—(0,0)—相对其之前状态。我们知道当一个sketch开始运行,原点位于萤幕左上角。如果我们写translate(50,50),结果将如下图所示:

translate(50,50);
translate(50,50);

另外,每当运行一次draw(),原点都会将其重置回萤幕左上角。

7. 看这个例子:
Code

void setup() {
  size(200,200);
  smooth();
}

void draw() {
  background(255);
  stroke(0);
  fill(175);
  
  // 抓取鼠标位置, constrain到萤幕尺寸
  int mx = constrain(mouseX,0,width);
  int my = constrain(mouseY,0,height);
  
  // 平移到鼠标位置
  translate(mx,my);
  ellipse(0,0,8,8);
  
  // 向右平移100像素
  translate(100,0);
  ellipse(0,0,8,8);
  
  // 向下平移100像素
  translate(0,100);
  ellipse(0,0,8,8);
  
  // 向左平移100像素
  translate(-100,0);
  ellipse(0,0,8,8);
}

translate()第一个引数控制左右平移,第二个则控制上下平移。translate()的“相对之前状态”在这个例子里体现的比较明显,相对的不永远是ellipse(0,0,8,8)这个球;,而是translate()之后的那个“新球”

8. translate()可以接受第三个引数作为z轴值。

// 沿z轴平移    
translate(0,0,50);  
rectMode(CENTER);  
rect(100,100,8,8);

以上代码沿z轴平移50像素,然后在位置(100,100)绘制一个矩形。这在技术上是正确的,但当我们使用translate()时,一个好习惯是将位置(x,y)指定为平移的一部分,如下:

// 当使用translate()时,矩形的位置时(0,0),因为translate()将带我们到矩形的位置
translate(100,100,50);  
rectMode(CENTER);  
rect(0,0,8,8);

最后,我们可以用它照做一开始那个矩形变大的例子:
Code

// 一个Z(深度)坐标的变量
float z = 0;

void setup() {
  // 当时用 (x,y,z) 坐标时, 我们必须告诉p5我们需要一个3D sketch.
  // 我们为此为size()函数增加第三个引数, P3D (或 OPENGL)
  size(200,200,P3D);
}

void draw() {
  background(255);
  stroke(0);
  fill(175);
  
  // 在一个形状显示前平移一个点
  translate(width/2,height/2,z);
  rectMode(CENTER);
  rect(0,0,8,8);
  
  // 增大z
  z++ ;
  
  // 重启矩形
  if (z > 200) {
    z = 0;
  }  
}

14-3
14-3

9. 这个题又把我打败了,,折腾好久才搞出来,不过终于算是对各种形状同时translate()的情况有点心得了(再次注意,它的平移是基于上一个点的位置,这是理解这个函数至关重要的一点):

size(200,200);
background(255);
stroke(0);
fill(0,100);
rectMode(CENTER);
translate(width/2,height/2);
rect(0,0,100,100);
translate(50,-50);
rect(0,0,100,100);
translate(-100,100);
line(0,0,-50,50);

10. translate()函数对相对一个给出的中点绘制的一系列形状很有用。
Code

void display()  {  
    // 画Zoog的身体
    fill(150);  
    rect(x, y,w/6,h*2);
  
    // 画Zoog的头
   fill(255);  
       ellipse(  x,y-h/2   ,w,h);      
     }
使用translate()可以将代码简化为:
void display()  {  
       // 将原点(0,0)移至(x,y)  
        translate(x,y);    
       // 画Zoog的身体  
       fill(150);  
       rect(0,0,w/6,h*2);  
       // 画Zoog的头  
       fill(255);  
       ellipse(0,-h/2,w,h);      
     }

11. 观察之前的例子,你会发现我们赋予了size()函数第三个引数,这代表绘制模式。默认(不写)为JAVA2D,是的,顾名思义,这是一个绘制2D图像的模式。要绘制3D图形,有以下两个方式:
◎ P3D——Processing开发者创造的一个3D渲染器。有一点需要注意,P3D不支持平滑效果(通过smooth()函数实现)。
◎ OPENGL——借助硬件加速的3D渲染器。如果你的机器有支持OPENGL的显卡装载,你便可以使用这个模式。因为在渲染速度上的优势,它在在高分辨率窗口展示大量形状时被推荐使用。而且经过对比,我发觉OPENGL模式的确比P3D模式的显像更平滑。

12. 要使用OPENGL模式,你还需实现导入OPENGL库。Sketch -> Import Library -> OpenGL 或 手写 import processing.opengl.*; 均可完成此操作。

13. 除了之前教过的绘制矩形、圆形、点、线等,我们可以利用
beginShape(), endShape(), 和vertex()函数创建我们自己想要的形状。
矩形:

rect(50,50,100,100);

等同于:

beginShape();  
vertex(50,50);  
vertex(150,50);  
vertex(150,150);
vertex(50,150);
endShape(CLOSE);

是的,在这个例子里显得比原先更加复杂,但是这种方法在绘制不规则图形或多边形的时候是必备的。

14. beginShape()表示我们将开始用数个顶点(vertex)绘制我们的自定义形状,vertex()当然是用来标注我们多边形内各个顶点的位置,endShape()表明我们已完成了顶点添加,其引数CLOSE表明这个形状应当被关闭,即,最后一个顶点将与第一个顶点相连。参阅Processing站点参考获取更多关于这三个函数的调用方法。

15. 你可以使用引数限制beginShape()绘制的图像,比如LINES,TRIANGLES等等。

16. 另外,你可以用curveVertex()替代vertex(),这将用曲线替代直线。注意,使用curveVertex()时,第一个顶点和最后一个顶点将不会被显示。这是因为,这两点要被用来定义弯曲率。

17. 让我们用beginShape() , endShape() , 和vertex()画一个立体的金字塔,共五面,四面三角形,底座一个矩形。

虽然麻烦,但我建议你在草稿纸上画出上述诸点,以帮助你理解它们的空间位置关系
虽然麻烦,但我建议你在草稿纸上画出上述诸点,以帮助你理解它们的空间位置关系

Code

void setup() {
  size(200,200,P3D);
}

void draw() {
  background(255);
  
  // 金字塔的顶点相对于一个中点绘制
  // 因此,我们呼叫translate()来将金字塔放在窗口适宜位置.
  // 一个稍好一点的选择也许是将位移放入drawPyramid()函数并将x,y,z传递为引数
  translate(100,100,0);
  drawPyramid(150);
}

// 这个函数在中点周围的弹性距离内设置金字塔的顶点,
// 这个弹性距离取决于传递给它的引数的值.
void drawPyramid(int t) {  
  stroke(0);
  
  // 这个金字塔有四面,每面被绘制为一个单独的三角形
  // 每面有三个顶点,形成一个三角形的形状
  // 参数" t " 决定了三角形的大小。

  beginShape(TRIANGLES);  
  fill(255,150); // 注意每个多边形都可以具有自己的颜色.
  vertex(-t,-t,-t);
  vertex( t,-t,-t);
  vertex( 0, 0, t);
  
  fill(150,150);
  vertex( t,-t,-t);
  vertex( t, t,-t);
  vertex( 0, 0, t);
  
  fill(255,150);
  vertex( t, t,-t);
  vertex(-t, t,-t);
  vertex( 0, 0, t);
  
  fill(150,150);
  vertex(-t, t,-t);
  vertex(-t,-t,-t);
  vertex( 0, 0, t);
  
  endShape();
}

18. 是的,上例结果你拿给其他任何人看,以那个视角人人都不会觉得那是一个立体的金字塔,为了解决这个问题,我们需要将它转动一下。然后在后边的学习中,我们要模拟一个日地月旋转系统。先记下三条规则:
◎ 形状在p5内被rotate()函数转动。
rotate()函数使用一个引数,以弧度来衡量的角度
rotate()函数以顺时针方向转动形状。

14-5
14-5

19. 如果我们想要将一个矩形旋转45°,这么写:

rotate(radians(45));  
rectMode(CENTER);  
rect(width/2,height/2,100,100);

结果将是错误的。为什么?这里我们要记住一个非常重要的特性,p5内的旋转是——形状永远围绕原点旋转。上例内的原点在哪里?左上角!原点没有经过位移,因此矩形并不围绕它的中心旋转,而是围绕左上角旋转。

20. 当然,也许有一天你会做那种需要将图形围绕左上角旋转的项目,但是在那一天到来之前,你将始终需要在旋转前将原点移到合适的位置,然后再显示矩形。是的,
translate()将在此刻拯救你。

translate(width/2,height/2);  
rotate(radians(45));  
rectMode(CENTER);  
rect(0,0,100,100);

哦!是的!我们想做的正是这样!
我们还可以将上述代码扩展一下,用mouseX(当然也可以是mouseY)的值来计算一个旋转的角度出来。
Code

void setup() {
  size(200,200);
}

void draw() {
  background(255);
  stroke(0);
  fill(175);
  
  // 将原点位移到窗口中心
  translate(width/2,height/2);

  // 基于鼠标X轴的位置和窗口宽度,角的范围从0到PI
  float theta = PI*mouseX / width;
  
  // 依角theta的值旋转
  rotate(theta);
  
  // 用CENTER模式显示矩形
  rectMode(CENTER);
  rect(0,0,100,100);
}

以直线中心为中心顺时针旋转
以直线中心为中心顺时针旋转

21. Daniel出题,画右边这样的图,让图形一起围绕直线中心旋转,恩,终于顺利做出来了。。最大体会还是:要深刻领会translate()精神啊。

float theta = 0.0;

void setup() {
  size(200,200);
}

void draw() {
  background(255);
  theta += 0.01;
  
  // 将原点位移到中心
  translate(width/2,height/2);

  rotate(theta);
  
  drawLine(80);
  drawCircle(80);
  
  // 基于第一个圆的位置继续位移第二个圆
  translate (160, 0);
  drawCircle(80);
}

void drawLine (int t) {
  stroke(0);
  fill(175);
  
  beginShape (LINES);
  vertex (-t, 0);
  vertex (t, 0);
  endShape();
}

void drawCircle (int t) {
  stroke(0);
  fill(175);

ellipse(-t, 0, 10, 10);
}

22. 以上二维旋转属于围绕Z轴旋转,另外为围绕X和Y轴旋转的情况,p5还提供给我们rotateX()rotateY()两个函数支持,然后函数rotateZ()同样存在并等同于rotate()
绕Z轴旋转
绕X轴旋转
绕Y轴旋转

23. 是的,我们当然可以将各种旋转结合运用:

void setup() {
  size(200,200,P3D);
}

void draw() {
  background(255);
  stroke(0);
  fill(175);
  translate(width/2,height/2);
  rotateX(PI*mouseY/height);
  rotateY(PI*mouseX/width);
  rectMode(CENTER);
  rect(0,0,100,100);
}

24. OK,现在回到金字塔的例子,一起见证3D的效果吧。本例还包含另一个基于大金字塔偏移的小金字塔。注意,它仍然和大金字塔围绕同一个原点旋转(因为rotateX()rotateY()先于第二个translate()被呼叫,哦哦哦,原来p5是这么个由上而下的代码执行顺序呀)
Code

float theta = 0.0;

void setup() {
  size(200,200,P3D);
}

void draw() {
  background(255);
  theta += 0.01;
  
  translate(100,100,0);
  rotateX(theta);
  rotateY(theta);
  drawPyramid(50);
  
  // translate the scene again
  translate(50,50,20);
  // call the pyramid drawing function
  drawPyramid(10);
}

void drawPyramid(int t) {
  stroke(0);
  
  // this pyramid has 4 sides, each drawn as a separate triangle
  // each side has 3 vertices, making up a triangle shape
  // the parameter " t " determines the size of the pyramid
  beginShape(TRIANGLES);
  
  fill(150,0,0,127);
  vertex(-t,-t,-t);
  vertex( t,-t,-t);
  vertex( 0, 0, t);
  
  fill(0,150,0,127);
  vertex( t,-t,-t);
  vertex( t, t,-t);
  vertex( 0, 0, t);
  
  fill(0,0,150,127);
  vertex( t, t,-t);
  vertex(-t, t,-t);
  vertex( 0, 0, t);
  
  fill(150,0,150,127);
  vertex(-t, t,-t);
  vertex(-t,-t,-t);
  vertex( 0, 0, t);
  
  endShape();
}

25. 这本书的网站下有个留言,基于这个例子做出了很特殊的效果,见这个页面的评论部分。

26. 在 translate()和rotate()之外,还有一个函数叫做scale() ,它用于放大或缩小萤幕上对象的大小。和rotate()一样,比例效果也是基于原位置。引数:1.0即100%。例如:scale(0.5) 以原尺寸一半的大小绘制对象;scale(3.0)将对像大小增加到300%。下例展示了一个用scale()实现的矩形增大实验:
Code

   float r = 0.0;  
   void setup()  {  
        size(200,200);  
    }  
   void draw()  {  
        background(0);  
        // 移位到窗口正中  
        translate(width/2,height/2);  
        // 基于r重比例形状大小  
        scale(r);  

        stroke(255);  
        fill(100);  
        rectMode(CENTER);  
        rect(0,0,10,10);  
  
        r  +=  0.02;  
    }

注意上例中,scale()在使矩形变大的同时使得边框伴随变粗。scale()同样支持二个或三个引数(分别代表x,y,z)。

27. 矩阵(Matrix)动态保存当前状态并随时可供以后使用。这最终将使得我们在不影响他物的情况下自由移动或旋转我们的形状。你可以呼叫printMatrix()查看任意时刻的矩阵。

28. 一个最好的证明这个概念的例子,创造两个以不同速度和方向围绕各自中心旋转的矩形。在进行中,我们将发现错误,然后引入pushMatrix()和popMatrix()函数的介绍。先在左上角画一个绕Y轴转的矩形,再在右下角画一个 绕Y轴转的矩形,是的,独处的时候,二者都没有问题,但是一旦放到一块:
Code

    float theta1 = 0;  
    float theta2 = 0;
  
    void setup()  {  
    size(200,200,P3D);      
     }    

    void draw()  {  
    background(255);  
    stroke(0);  
    fill(175);  
    rectMode(CENTER);  
  
    translate(50,50);  
    rotateZ(theta1);  
    rect(0,0,60,60);
  
    theta1  +  =  0.02;  

    translate(100,100);  
    rotateY(theta2);  
    rect(0,0,60,60);  
    
    theta2  +  =  0.02;      
     }

试运行一下,问题出现,右下角的矩形也开始围绕左上角的矩形旋转(而不是独自在右下角绕Y轴旋转)。要记住,所有位移和旋装都是基于前一个坐标系统的状态。我们需要一个能够还原之前状态的矩阵系统以使得各个形状可以独立运动。

29. 保存和还原旋转/位移状态可以被pushMatrix()popMatrix() 完成。push=保存,pop=还原。

30. 为使各个矩形独立旋转,我们可以写下如下算法:
1)保存当前变形矩阵。这是我们的起点,从(0,0)并且没有旋转开始。
2)移动并旋转第一个矩形。
3)显示第一个矩形。
4)从第一步还原矩阵,使其不受第二、三步的影响。
5)移动并旋转第二个矩形。
6)显示第二个矩形。
用以上思路重写上例:
Code

float theta1 = 0;
   float theta2 = 0;
   void setup()  {  
        size(200,200,P3D);
    }  

   void draw()  {  
        background(255);
        stroke(0);
        fill(175);
        rectMode(CENTER);
         pushMatrix();
  
        translate(50,50);
        rotateZ(theta1);
        rect(0,0,60,60);
         popMatrix();
  
        pushMatrix();
        translate(150,150);
        rotateY(theta2);
        rect(0,0,60,60);
        popMatrix();
        theta1 +=  0.02;
        theta2  +=  0.02;
   }

尽管技术上并不需要,在第二个矩形前后加pushMatrix()popMatrix()是一个好习惯(比如如果我们需要添加更多的形状),同时也更容易将各个部分当作不同的个体来对待。实际上,这个例子真的应当以面向对象的方式来做,然后是的每个对象呼叫自己各自的pushMatrix(), translate(), rotate()和popMatrix()

31. 接下来我们如法炮制用数组做一堆旋转的矩形:
Code

// Rotater对象数组
Rotater[] rotaters;

void setup() {
  size(200,200);
  
  rotaters = new Rotater[20];
  
  // 随机决定Rotaters各属性
  for (int i = 0; i < rotaters.length; i++ ) {
    rotaters[i] = new Rotater(random(width),random(height),random(-0.1,0.1),random(48));
  }
}

void draw() {
  background(255);
  
  // All Rotaters spin and are displayed
  for (int i = 0; i < rotaters.length; i++ ) {
    rotaters[i].spin();
    rotaters[i].display();
  }
}

// 一个Rotater类
class Rotater {
  
  float x,y;   // x,y位置
  float theta; // 旋转角度
  float speed; // 旋转速度
  float w;     // 矩形大小
  
  Rotater(float tempX, float tempY, float tempSpeed, float tempW) {
    x = tempX;
    y = tempY;
    // 角度总是初始化为0
    theta = 0;
    speed = tempSpeed;
    w = tempW;
  }
  
  // 增大角度
  void spin() {
    theta += speed;
  }
  
  // 显示矩形
  void display() {
    rectMode(CENTER);
    stroke(0);
    fill(0,100);
    // pushMatrix()和popMatrix()在类的display()方法中被呼叫.
    // 这样,每个Rotater对象都将在各自的位移和旋转上被渲染出来!
    pushMatrix();
    translate(x,y);
    rotate(theta);
    rect(0,0,w,w);
    popMatrix();
  }
}

32. 嵌套使用pushMatrix()和popMatrix()会产生有趣的效果。在计算机科学领域push(推)和pop(送)常被理解为“堆叠(stack)”。stack是什么呢?设想一位老师在夜晚阅卷,将卷子整理为一叠,那么她放在桌面上的第一张试卷将被她最后阅读,而最后一张放上的试卷将被她最先阅读。在对列里这恰恰是反过来的。不像排队买电影票,排在第一位的总是第一个买到票,而排在最后的总是最后买到票。push意味着将一些东西放入堆叠中的过程,pop取出一些东西。因此,在程序中,pushMatrix()popMatrix()的数量永远是想等的。以旋转的矩形为基础,我们可以看到嵌套pushMatrix()popMatrix()是如何的有用,下例为一个行星系统的模拟:
Code

// 围绕太阳及行星旋转的角度
float theta = 0;

void setup() {
  size(200,200);
  smooth();
}

void draw() {
  background(255);
  stroke(0);
  
  // 位移到窗口正中绘制太阳
  translate(width/2,height/2);
  fill(255,200,50);
  ellipse(0,0,20,20);
  
  // 地球围绕太阳旋转
  pushMatrix();
  rotate(theta);
  translate(50,0);
  fill(50,200,255);
  ellipse(0,0,10,10);
  
  // 月亮#1围绕地球传
  // 在绘制月亮#1前呼叫以保持其位移状态
  //
这样,我们便能在绘制月亮#2前pop并返回地球。
  // 所有月亮都围绕地球旋转 (地球自身同时围绕太阳转).
  pushMatrix();
  rotate(-theta*4); // 角度为负则逆时针旋转
  translate(15,0);
  fill(50,255,200);
  ellipse(0,0,6,6);
  popMatrix();
  
  // 月亮#2同样围绕地球转
  pushMatrix();
  rotate(theta*2);
  translate(25,0);
  fill(50,255,200);
  ellipse(0,0,6,6);
  popMatrix();
  
  popMatrix();
  
  theta += 0.01;
}

我试了一下,将最后一个popMatrix();移到地球之后运行,则所有球都围绕着太阳旋转,这给我们一个启示,在嵌套pushMatrix()popMatrix()中,无论有多少组pushMatrix()和popMatrix(),它们的translate()都是基于首先出现在pushMatrix()popMatrix()里边的那个对象的位置。你可以基于这一点,改写代码,使得在其他旋转都不改变的情况下月亮#1(将其改为其他颜色并将旋转角度加大将能更好的看到效果)围绕月亮#2旋转。是的,又是一个容易让人头晕混淆的概念,但是你必须弄清楚这些基础性的东西。

令人晕眩的动态特效
令人晕眩的动态特效

33. pushMatrix()popMatrix()同样可以在for和while循环中被嵌套,制造牛逼的特效,见下例:

float theta = 0;

void setup() {
  size(200, 200);
  smooth();
}

void draw() {
  background(255);
  stroke(0);
  
  translate(width/2,height/2);
  
  // 由0度循环到360度 (2*PI弧度)
  for(float i = 0; i < TWO_PI; i += 0.2) {
    
    // Push, 旋转并画一条线!
    // 变形状态通过for循环在每个循环开始之初被保存背在结尾被还原。
    // 试着将所有pushMatrix()和popMatrix()注释掉看不同!
    pushMatrix();
    rotate(theta + i);
    line(0,0,100,0);
    
    // 由0度循环到360度 (2*PI弧度)
    for(float j = 0; j < TWO_PI; j += 0.5) {
      // Push, 旋转并画一条线!
      pushMatrix();
      translate(100,0);
      rotate(-theta-j);
      line(0,0,50,0);
      // 如果我们完成了内部循环, 则pop掉!
      popMatrix();
    }
    
    // 如果我们完成了外部循环, 则pop掉!
    popMatrix();
  }
  
  theta += 0.01;
}

34. Daniel出题:把你的无论是金字塔还是立方体做入一个类,让每个对象都呼叫它们各自的pushMatrix()popMatrix()。你能做一个在3D里各自独立旋转的对象的数组吗?

35. 下边我们把本课学到的所有知识汇总,做一个微缩太阳系,是上边地月系的升级版,并有两大改变:
◎ 每个行星都是一个对象,是Planet类的一员
◎ 一个行星数组将围绕太阳旋转
Code

// 一个由8个行星对象组成的数组
Planet[] planets = new Planet[8];

void setup() {
  size(200,200);
  smooth();
  
  // 行星对象用计数器变量初始化
  for (int i = 0; i < planets.length; i++ ) {
    planets[i] = new Planet(20 + i*10,i + 8);
  }
}

void draw() {
  background(255);
  
  // 绘制太阳
  pushMatrix();
  translate(width/2,height/2);
  stroke(0);
  fill(255);
  ellipse(0,0,20,20);
  
  // 绘制所有行星
  for (int i = 0; i < planets.length; i++ ) {
    planets[i].update();
    planets[i].display();
  }
  popMatrix();
}

class Planet {
  // 每个行星对象都持续记录其旋转角度.
  float theta;      // 绕日旋转
  float diameter;   // 行星大小
  float distance;   // 距太阳距离
  float orbitspeed; // 旋转速度
  
  Planet(float distance_, float diameter_) {
    distance = distance_;
    diameter = diameter_;
    theta = 0;
    orbitspeed = random(0.01,0.03);
  }
  
  void update() {
    // 加快旋转角度
    theta += orbitspeed;
  }
  
  void display() {
    // 在旋转和位移前, 矩阵的状态被pushMatrix()保存下来.
    pushMatrix();
    // 旋转
    rotate(theta);
    // 位移distance
    translate(distance,0);
    stroke(0);
    fill(175);
    ellipse(0,0,diameter,diameter);
    // 行星一旦被绘制, 矩阵被popMatrix()还原,因此其他行星不会受到影响.
    popMatrix();
  }
}

36. Daniel出题:为以上行星分别制作一颗卫星,答案见

37. Daniel出题:用sphere()box()替代ellipse(),从而使得太阳系进入三维空间。哦,他开始让我们学着去看p5的官方reference了。

38. 越来越绕了。怎么办 🙁

Be Sociable, Share!

Leave a Reply

Your email address will not be published. Required fields are marked *