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();
}