【12】processing-变换(中文)

2D平面转变

J David Eisenberg(人名)

 

处理具有内置功能,使您可以轻松地让对象在草图中translate(移动)rotate(旋转)scale(缩放)。本教程将向您介绍“平移”、“旋转”和“缩放”功能,以便您可以在草图中使用它们。

 

平移:移动栅格

如你所知,你的处理窗口就像一张纸。当你想画东西时,你可以在图上指定它的坐标。这是一个用代码rect(20,20,40,40)绘制的简单矩形。坐标系(一个花哨的词,意为“相纸”)显示为灰色。

 

 

如果要将矩形向右移动60个单位,向下移动80个单位,只需将坐标添加到x和y的起点:rect(20+60,20+80,40,40),矩形将显示在不同的位置。(我们把箭放在那里是为了达到戏剧性的效果。)

 

但是有一个更有趣的方法:移动图表。如果你把相纸向右移动60个单位,向下移动80个单位,你会得到完全相同的视觉效果。移动坐标系称为平移。

 

 

在上图中要注意的重要一点是,就矩形而言,它根本没有移动。它的左上角仍然在(20,20)。当你使用变换时,你画的东西永远不会改变位置;坐标系会改变。

 

下面的代码通过更改矩形的坐标以红色绘制矩形,然后通过移动网格以蓝色绘制矩形。矩形是半透明的,因此您可以看到它们(视觉上)在同一个位置。只有用来移动它们的方法改变了。将此代码复制并粘贴到处理程序中,并进行尝试。

 

void setup()
{
  size(200, 200);
  background(255);
  noStroke();
  // 用灰色绘制原始位置
  fill(192);
  rect(20, 20, 40, 40);
  
  // 通过更改坐标绘制半透明的红色矩形
  fill(255, 0, 0, 128);
  rect(20 + 60, 20 + 80, 40, 40);
  
  // 通过平移网格绘制半透明的蓝色矩形
  fill(0, 0, 255, 128);
  pushMatrix();
  translate(60, 80);
  rect(20, 20, 40, 40);
  popMatrix();
}

 

让我们更详细地看一下翻译代码。pushMatrix()是一个内置函数,用于保存坐标系的当前位置。translate(60, 80)将坐标系向右移动60个单位,向下移动80个单位。rect(20, 20, 40, 40)在原来的位置绘制矩形。记住,你画的东西不会移动网格。最后,popMatrix()将坐标系恢复到执行转换之前的状态。

是的,您可以执行translate(-60, -80)将网格移回其原始位置。但是,当您开始使用坐标系执行更复杂的操作时,使用pushMatrix()popMatrix()来保存和恢复状态会更容易,而不必撤消所有操作。在本教程的后面,您将了解为什么这些函数看起来有这么奇怪的名称。

 

有什么好处?

你可能在想,拿起坐标系并移动它比添加坐标要麻烦得多。对于矩形这样的简单示例,您是正确的。但是让我们举一个例子,translate()可以让生活更轻松。这是一些绘制一排房屋的代码。它使用一个调用名为house()的函数的循环,该函数将房屋左上角的x和y位置作为其参数。

void setup()
{
  size(400, 100);
  background(255);
  for (int i = 10; i < 350; i = i + 50)
  {
    house(i, 20);
  }
}

这是通过改变房子的位置来绘制房子的代码。看看你需要跟踪的所有附加内容。

void house(int x, int y)
{
  triangle(x + 15, y, x, y + 15, x + 30, y + 15);
  rect(x, y + 15, 30, 30);
  rect(x + 12, y + 30, 10, 15);
}

将其与使用translate()的函数版本进行比较。在这种情况下,代码每次都在同一个地方绘制房屋,其左上角位于(0,0),并让翻译代替它来完成所有工作。

void house(int x, int y)
{
  pushMatrix();
  translate(x, y);
  triangle(15, 0, 0, 15, 30, 15);
  rect(0, 15, 30, 30);
  rect(12, 30, 10, 15);
  popMatrix();
}

 

 

 

 

旋转

除了移动网格之外,还可以使用rotate()函数旋转网格。此函数接受一个参数,即要旋转的弧度数。在处理过程中,所有与旋转有关的函数都以弧度而不是度数来测量角度。当你谈论角度时,你说一个完整的圆有360度。当你谈到弧度的角度时,你说一个完整的圆有2π弧度。这是一个图表,显示了处理如何以度(黑色)和弧度(红色)度量角度。

(补充声明:TAU等于2*PI,也就是圆周弧长,通过TAU除以360,可以计算出1度的弧长)

由于大多数人都是按度数思考的,因此处理过程有一个内置的radians()函数,该函数以度数作为参数,并为您转换它。它还有一个degrees()函数,可以将弧度转换为度。考虑到这个背景,我们试着把正方形顺时针旋转45度。

void setup()
{
  size(200, 200);
  background(255);
  smooth();
  fill(192);
  noStroke();
  rect(40, 40, 40, 40);
  
  pushMatrix();
  rotate(radians(45));
  fill(0);
  rect(40, 40, 40, 40);
  popMatrix();
}

嘿,怎么了?为什么广场被移动和切断?答案是:广场没有移动。网格被旋转。这就是真正发生的事情。如您所见,在旋转坐标系中,正方形的左上角仍位于(40,40)。

 

 

旋转方向正确

旋转正方形的正确方法是:

1。将坐标系的原点(0,0)平移到您希望的正方形左上角的位置。

2。旋转网格π/4弧度(45°)

3。在原点画正方形。

 

这是代码和结果,没有网格标记。

void setup()
{
  size(200, 200);
  background(255);
  smooth();
  fill(192);
  noStroke();
  rect(40, 40, 40, 40);
  
  pushMatrix();
  // 将原点移到轴点
  translate(40, 40); 
  
  // 然后旋转网格
  rotate(radians(45));
  
  //在原点画正方形
  fill(0);
  rect(0, 0, 40, 40);
  popMatrix();
}

这是一个程序,它通过旋转产生一个颜色轮。减少屏幕截图以节省空间。

 

 

void setup() {
  size(200, 200);
  background(255);
  smooth();
  noStroke();
}
void draw(){
  if (frameCount % 10 == 0) {
    fill(frameCount * 3 % 255, frameCount * 5 % 255,
      frameCount * 7 % 255);
    pushMatrix();
    translate(100, 100);
    rotate(radians(frameCount * 2  % 360));
    rect(0, 0, 80, 20);
    popMatrix();
  }
}

缩放比例

最后的坐标系转换是缩放,这会改变网格的大小。看一看这个例子,它绘制了一个正方形,然后将网格缩放到其正常大小的两倍,然后再次绘制。

void setup()
{
  size(200,200);
  background(255);
  
  stroke(128);
  rect(20, 20, 40, 40);
  
  stroke(0);
  pushMatrix();
  scale(2.0);
  rect(20, 20, 40, 40);
  popMatrix();
}

首先,你可以看到广场似乎已经移动了。当然没有。它的左上角仍然位于缩放网格上的(20,20),但该点现在距离原点的距离是原始坐标系中的两倍。你还可以看到线条更粗。这并不是幻觉,因为坐标系被缩放到原来的两倍,所以这些线的厚度实际上是原来的两倍。

编程挑战:放大黑色方块,但将其左上角与灰色方块保持在同一位置。提示:使用translate()移动原点,然后使用scale()。

 

没有法律规定你必须同样地缩放x和y维度。尝试使用scale(3.0, 0.5)将x维度设置为其正常大小的三倍,而y维度仅为其正常大小的一半。

订单事项

当您执行多个转换时,顺序会有所不同。先旋转后平移再缩放的结果与先平移后旋转再缩放的结果不同。下面是一些示例代码和结果。

void setup()
{
  size(200, 200);
  background(255);
  smooth();
  line(0, 0, 200, 0); // 绘制坐标轴
  line(0, 0, 0, 200);
  
  pushMatrix();
  fill(255, 0, 0); // 红场
  rotate(radians(30));
  translate(70, 70);
  scale(2.0);
  rect(0, 0, 20, 20);
  popMatrix();
  pushMatrix();
  fill(255); // 白色方块
  translate(70, 70);
  rotate(radians(30));
  scale(2.0);
  rect(0, 0, 20, 20);
  popMatrix();
}

 

变换矩阵

每次进行旋转、平移或缩放时,转换所需的信息都会累积到一个数字表中。这个表或矩阵只有几行和几列,然而,通过数学的奇迹,它包含了进行任何一系列转换所需的所有信息。这就是为什么pushMatrix()和popMatrix()的名称中都有这个词。

 

Push 和 Pop

名字中的“推”和“弹出”部分呢?它们来自一个叫做堆栈的计算机概念,它的工作原理类似于自助餐厅中的弹簧托盘分配器。当有人把托盘放回堆栈时,托盘的重量会把平台压下去。当有人需要托盘时,他会从托盘的顶部取出,剩下的托盘会弹出一点。

以类似的方式,pushMatrix()将坐标系的当前状态放在内存区域的顶部,而popMatrix()将该状态拉回来。前面的示例使用pushMatrix()和popMatrix()来确保在绘图的每个部分之前坐标系是“干净的”。在所有其他的例子中,对这两个函数的调用并不是真正必要的,但是保存和恢复网格状态并没有任何影响。

注意:在处理过程中,每次执行draw()函数时,坐标系都会恢复到其原始状态(原点在窗口左上角,无旋转和缩放)。

 

三维变换

如果在三维空间中工作,则可以调用translate()函数,其中包含x、y和z距离的三个参数。类似地,可以使用三个参数调用scale(),这些参数告诉您希望在每个维度中缩放网格的大小。

对于旋转,调用rotateX()、rotateY()或rotateZ()函数绕每个轴旋转。所有这三个函数都需要一个参数:要旋转的弧度数。

 

案例研究:一个挥舞手臂的机器人

让我们使用这些变换来设置蓝色机器人挥舞手臂的动画。我们将分阶段完成这项工作,而不是一下子全部写下来。第一步是画出没有任何动画的机器人。

这个机器人是按照这张图做模型的,尽管看起来没有那么迷人。首先,我们绘制机器人,使其左侧和顶部接触到x和y轴。这将允许我们使用translate()轻松地将机器人放置在我们想要的任何位置,或者制作机器人的多个副本,就像我们在房屋示例中所做的那样。

当我们在这张图中提到左和右时,我们指的是你的左和右(显示器的左和右),而不是机器人的左和右。

 

void setup()
{
  size(200, 200);
  background(255);
  smooth();
  drawRobot();
}
void drawRobot()
{
  noStroke();
  fill(38, 38, 200);
  rect(20, 0, 38, 30); // 头
  rect(14, 32, 50, 50); // 身体
  rect(0, 32, 12, 37); // 左臂
  rect(66, 32, 12, 37); // 右臂
  
  rect(22, 84, 16, 50); // 左腿
  rect(40, 84, 16, 50); // 右腿
  
  fill(222, 222, 249);
  ellipse(30, 12, 12, 12); // 左眼
  ellipse(47, 12, 12, 12); // 右眼
}

 

 

下一步是确定手臂的旋转点。如图所示。轴心点是(12,32)和(66,32)。注:术语“旋转中心”是对轴点的更正式的术语。

 

现在,分离绘制左右手臂的代码,并将每只手臂的旋转中心移动到原点,因为您总是围绕(0,0)点旋转。为了节省空间,我们不重复setup()的代码。

void drawRobot()
{
  noStroke();
  fill(38, 38, 200);
  rect(20, 0, 38, 30); // 头
  rect(14, 32, 50, 50); // 身体
  drawLeftArm();
  drawRightArm();
  rect(22, 84, 16, 50); // 左腿
  rect(40, 84, 16, 50); // 右腿
  
  fill(222, 222, 249);
  ellipse(30, 12, 12, 12); // 左眼
  ellipse(47, 12, 12, 12);  // 右眼
}

void drawLeftArm()
{
  pushMatrix();
  translate(12, 32);
  rect(-12, 0, 12, 37);
  popMatrix();
}

void drawRightArm()
{
  pushMatrix();
  translate(66, 32);
  rect(0, 0, 12, 37);
  popMatrix();
}

现在测试手臂是否正确旋转。与其尝试完整的动画,我们将只旋转左侧手臂135度和右侧手臂 -45度 作为测试。这是需要添加的代码和结果。由于窗口边界,左侧手臂被切断,但我们将在最终动画中修复该问题。

 

 

现在我们通过播放动画来完成这个程序。左臂必须从0°旋转到135°并向后旋转。由于手臂摆动是对称的,所以右臂角度始终是左臂角度的负值。为了简单起见,我们将以5度为增量。

int armAngle = 0;
int angleChange = 5;
final int ANGLE_LIMIT = 135;
void setup()
{
  size(200, 200);
  smooth();
  frameRate(30);
}
void draw()
{
  background(255);
  pushMatrix();
  translate(50, 50); // 放置机器人使手臂始终在屏幕上
  drawRobot();
  armAngle += angleChange;
  
  //如果手臂已经超过极限,
    //反转方向并设置在限制范围内。    
  if (armAngle > ANGLE_LIMIT || armAngle < 0)
  {
    angleChange = -angleChange;
    armAngle += angleChange;
  }
  popMatrix();
}
void drawRobot()
{
  noStroke();
  fill(38, 38, 200);
  rect(20, 0, 38, 30); // 头
  rect(14, 32, 50, 50); // 身体
  drawLeftArm();
  drawRightArm();
  rect(22, 84, 16, 50); // 左腿
  rect(40, 84, 16, 50); // 右腿
  
  fill(222, 222, 249);
  ellipse(30, 12, 12, 12); // 左眼
  ellipse(47, 12, 12, 12);  // 右眼
}
void drawLeftArm()
{
  pushMatrix();
  translate(12, 32);
  rotate(radians(armAngle));
  rect(-12, 0, 12, 37); // left arm
  popMatrix();
}
void drawRightArm()
{
  pushMatrix();
  translate(66, 32);
  rotate(radians(-armAngle));
  rect(0, 0, 12, 37); // right arm
  popMatrix();
}

案例研究:互动旋转

我们将修改程序,使手臂在按下鼠标按钮时跟随鼠标,而不是让手臂自行移动。我们不只是在键盘上编写程序,而是首先考虑问题并找出程序需要做什么。

 

由于两个手臂相互独立地运动,我们需要为每个手臂的角度设置一个变量。很容易找出要追踪哪只手臂。如果鼠标位于机器人中心的左侧,则跟踪左臂;否则,跟踪右臂

 

剩下的问题是计算出旋转的角度。给定轴点位置和鼠标位置,如何确定连接这两点的直线的角度?答案来自atan2()函数,它给出(以弧度为单位)直线从原点到给定y和x坐标的角度。与大多数其他函数相比,y坐标是第一位的。atan2()返回一个从-π到π弧度的值,相当于-180°到180°。

 

但是找到一条不从原点开始的线的角度,比如从(10,37)到(48,59)的线,怎么样?没问题,它和线从(0,0)到(48-10,59-37)的角度是一样的。一般来说,要找到线从(x0,y0)到(x1,y1)的角度,请计算

 

void setup()
{
  size(200, 200);
}
void draw()
{
  float angle = atan2(mouseY - 100, mouseX - 100);
  
  background(255);
  pushMatrix();
  translate(100, 100);
  rotate(angle);
  rect(0, 0, 50, 10);
  popMatrix();
}

 

很好用。如果我们画一个比宽高的长方形会怎么样?更改前面的代码以读取rect(0,0,10,50)。为什么它看起来不再跟着老鼠了?答案是矩形实际上仍然跟随鼠标,但它是矩形的短边执行以下操作。我们的眼睛被训练成希望长边被跟踪。因为长边与短边成90度角,所以必须减去90度(或π/2弧度)才能获得所需的效果。将前面的代码更改为rotate(angle-HALF_PI),然后重试。由于处理几乎只处理弧度,为了方便起见,语言定义了常数PI(180°)、半π(90°)、四分之一π(45°)和两个π(360°)。

 

此时,我们可以编写arm跟踪程序的最终版本。我们从常量和变量的定义开始。中点_X定义中的数字39来自于这样一个事实:机器人的身体从X坐标14开始,宽度为50像素,因此39(14+25)是机器人身体的水平中点。

 

 

 

/* 机器人左上角出现在屏幕上的位置 */
final int ROBOT_X = 50;
final int ROBOT_Y = 50;

/* 机器人的中点和手臂枢轴点 */
final int MIDPOINT_X = 39;
final int LEFT_PIVOT_X = 12;
final int RIGHT_PIVOT_X = 66;
final int PIVOT_Y = 32;
float leftArmAngle = 0.0;
float rightArmAngle = 0.0;
void setup()
{
  size(200, 200);
  smooth();
  frameRate(30);
}

接下来是draw()函数。它确定是否按下鼠标以及鼠标位置和轴心点之间的角度,并相应地设置leftArmAngle和rightArmAngle。

 

 

 

void draw()
{
  /*
   *这些变量用于mouseX和mouseY,
     *调整为相对于机器人的坐标系
     *而不是窗口的坐标系。    
   */
  float mX;
  float mY;
  
  background(255);
  pushMatrix();
  translate(ROBOT_X, ROBOT_Y); // 放置机器人使手臂始终在屏幕上
  if (mousePressed)
  {
    mX = mouseX - ROBOT_X;
    mY = mouseY - ROBOT_Y;
    if (mX < MIDPOINT_X) // 机器人左侧
    {
      leftArmAngle = atan2(mY - PIVOT_Y, mX - LEFT_PIVOT_X)
      - HALF_PI;
    }
    else
    {
      rightArmAngle = atan2(mY - PIVOT_Y, mX - RIGHT_PIVOT_X)
      - HALF_PI;
   }
  }
  drawRobot();
  
  popMatrix();
}

drawRobot()函数保持不变,但现在需要对drawLeftArm()和drawRightArm()进行一些小的更改。因为leftArmAngle和rightArmAngle现在是以弧度计算的,所以这些函数不需要进行任何转换。对这两个功能的更改以粗体显示。

 

 

 

void drawLeftArm()
{
  pushMatrix();
  translate(12, 32);
  rotate(leftArmAngle);
  rect(-12, 0, 12, 37); // 左臂
  popMatrix();
}
void drawRightArm()
{
  pushMatrix();
  translate(66, 32);
  rotate(rightArmAngle);
  rect(0, 0, 12, 37); // 右臂
  popMatrix();
}
  • 5
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值