PShape
Daniel Shiffman
(本教程的源代码在处理示例中。在处理IDE中选择文件→示例,然后选择主题→创建形状。)
使用处理编程时,首先要学习的是如何在屏幕上绘制“原始”形状:矩形、椭圆、直线、三角形等。
rect(x,y,w,h);
ellipse(x,y,w,h);
line(x1,y1,x2,y2);
triangle(x1,y1,x2,y2,x3,y3);
更高级的绘图选项是使用beginShape()和endShape()指定自定义多边形的顶点
beginShape();
vertex(x1,y1);
vertex(x2,y2);
vertex(x3,y3);
vertex(x4,y4);
// 等;
endShape();
通过将一组绘图函数组合在一起,甚至可以将它们组织到一个类中,可以构建更复杂的形状。
class MyWackyShape {
//一些变量
//构造器
//一些功能
//显示形状!
void display() {
rect(x,y,w,h);
ellipse(x,y,w,h);
beginShape();
vertex(x1,y1);
vertex(x2,y2);
vertex(x3,y3);
vertex(x4,y4);
// 等等;
endShape();
}
}
这一切都很好,会让你走得很远。只有知道上面的情况,你才能画出很少的东西。然而,还有另一个步骤。在某些情况下,这一步骤可以提高渲染速度,并为代码PShape提供更高级的组织模型。
PShape是用于存储形状的数据类型。这些形状可以是由自定义几何图形生成的形状,也可以是从外部文件(如SVG)加载的形状。
原始形状
让我们从使用PShape的一个最简单的例子开始。下面是一个简单的处理draw()方法,它在鼠标后面绘制一个矩形。
void draw() {
background(51);
stroke(255);
fill(127);
rect(mouseX,mouseY,100,50);
}
基本的东西。如果这是我们所有的代码,不一定有一个很好的理由使用PShape,但我们将继续推进,并制作一个PShape矩形无论如何作为演示。我们的目标是要有一个变量来存储该变量的颜色和维度,允许我们的draw函数看起来像这样。
void draw() {
background(51);
shape(rectangle);
}
这个“矩形”是什么?这是个假象。
PShape rectangle;
要初始化PShape,请使用createShape()方法。createShape()的第一个参数是一个常量,该常量指定要生成的PShape的类型。正如我们将在本教程中看到的,有很多选项:椭圆、矩形、圆弧、三角形、球体、长方体、直线、组等等。在这里,我们创建一个基本形状,一个矩形,所以我们使用常量“RECT”,后面的参数是形状的(x,y)位置以及它的尺寸(宽度,高度)。所以现在我们的setup()应该是:
PShape rectangle;
void setup() {
size(640,360,P2D);
rectangle = createShape(RECT,mouseX,mouseY,100,50);
}
不过,有个大问题。是的,我们希望矩形显示在鼠标位置,但是当草图首先开始mouseX时,mouseY设置为零。在我们循环draw()之前,mouseX和mouseY是不会复活的。重要的是要记住,当使用PShape时,我们真正要做的是配置相对于原点(0,0)的几何体。然后使用translate()、rotate()等变换在屏幕上移动该形状通常是有利的
PShape rectangle;
void setup() {
size(640,360,P2D);
rectangle = createShape(RECT,0,0,100,50);
}
如果我们想从中心画出矩形:
PShape rectangle;
void setup() {
size(640,360,P2D);
rectangle = createShape(RECT,-50,-25,100,50);
}
然后我们可以根据鼠标移动它。
void draw() {
background(51);
translate(mouseX,mouseY);
shape(rectangle);
}
PShape对象的一个优点是,除了几何图形外,它还可以存储颜色信息。创建形状以更改其填充或笔划后,请使用setFill()、setStroke()和setStrokeWeight()等方法。
void setup() {
size(640,360,P2D);
rectangle = createShape(RECT,-50,-25,100,50);
rectangle.setStroke(color(255));
rectangle.setStrokeWeight(4);
rectangle.setFill(color(127));
}
如果要动态更改形状的颜色,也可以在draw()期间调用这些方法。
void draw() {
background(51);
translate(mouseX, mouseY);
rectangle.setFill(color(map(mouseX, 0, width, 0, 255)));
shape(rectangle);
}
需要注意的是,与fill()和stroke()不同,必须将全色作为参数传递。i、 e.对于红色填充,不要说“setFill(255,0,0)”,而是说“setFill(color(255,0,0))”。此外,set fill()和set stroke()可以使用布尔参数(例如setFill(false))来打开或关闭给定顶点的填充或笔划,也可以使用整数(例如setFill(i,color(255,0,0))来设置特定顶点的填充或笔划。
请参阅示例“PrimitivePShape”(在“文件”—“示例”—“主题”—“创建形状”下),以获取演示上述所有代码的示例。
自定义形状
pshape也可以配置为自定义顶点。您可能以前做过,没有PShape,只使用beginShape()和endShape()。例如,假设你想在处理过程中画一颗星星。您可能有以下代码:
void draw() {
background(51);
translate(mouseX, mouseY);
fill(102);
stroke(255);
strokeWeight(2);
beginShape();
vertex(0, -50);
vertex(14, -20);
vertex(47, -15);
vertex(23, 7);
vertex(29, 40);
vertex(0, 25);
vertex(-29, 40);
vertex(-23, 7);
vertex(-47, -15);
vertex(-14, -20);
endShape(CLOSE);
}
在这里,与前面的示例一样,我们的目标是在draw()中将形状本身绘制为对象。
void draw() {
background(51);
translate(mouseX, mouseY);
shape(star);
}
要使用自定义顶点生成PShape,必须首先调用createShape()来生成shape对象,然后对该对象调beginShape()和endShape():
PShape star;
void setup() {
size(640,360,P2D);
star = createShape(); // 首先创建形状
star.beginShape(); // 现在调用beginShape();
// 所有的顶点信息都在这里。
star.endShape(CLOSE); // 现在调用endShape(CLOSE);
}
然后,可以通过调用新PShape对象“star”上的函数来指定所有顶点(和颜色)。请注意,这里不需要setFill()和setStroke(),只有在最初创建形状后选择更改颜色。
void setup() {
size(640,360,P2D);
// 首先创建形状
star = createShape();
star.beginShape();
// 可以设置填充和笔划
star.fill(102);
star.stroke(255);
star.strokeWeight(2);
// 这里,我们对一系列顶点进行硬编码
star.vertex(0, -50);
star.vertex(14, -20);
star.vertex(47, -15);
star.vertex(23, 7);
star.vertex(29, 40);
star.vertex(0, 25);
star.vertex(-29, 40);
star.vertex(-23, 7);
star.vertex(-47, -15);
star.vertex(-14, -20);
star.endShape(CLOSE);
}
请参阅示例“PolygonPShape”(在“文件>示例>主题>创建形状”下),以获取演示上述所有代码的示例。
更多形状
如前所述,使用PShape的一个原因是帮助您组织几何图形。然而,还有另一个原因。假设您有一个星型类,其中有一个display()函数,如下所示:
void display() {
pushMatrix();
translate(x, y);
fill(102);
stroke(255);
strokeWeight(2);
beginShape();
vertex(0, -50);
vertex(14, -20);
vertex(47, -15);
vertex(23, 7);
vertex(29, 40);
vertex(0, 25);
vertex(-29, 40);
vertex(-23, 7);
vertex(-47, -15);
vertex(-14, -20);
endShape(CLOSE);
popMatrix();
}
在draw()中,您将遍历一个星形对象数组,显示每个对象。
void draw() {
background(51);
for (int i = 0; i < stars.length; i++) {
stars[i].display();
}
}
试着画500颗左右的星星,你的素描可能会跑得很慢,大约每秒10帧。这是因为这种画法,通常被称为“立即”模式,要求渲染器每次通过绘制每个星星来计算几何体。但这有必要吗?毕竟,一次又一次是同一颗星星。使用PShape可以让处理基本上“记住”恒星的几何结构。绘制存储的几何体(在较低级别的OpenGL语法中称为“顶点缓冲对象”)称为“保留”模式,速度要快得多。这500颗恒星可以用PShape代替以60 FPS的速度轻松渲染。这可以通过将PShape变量作为Star类的一部分来实现。
class Star {
PShape s;
float x, y;
//然后需要在构造函数中初始化该PShape。这可以直接做,就在课堂上。
Star() {
// 首先创建形状
s = createShape();
s.beginShape();
// 可以设置填充和笔划
s.fill(102);
s.stroke(255);
s.strokeWeight(2);
// Here, we are hardcoding a series of vertices
s.vertex(0, -50);
s.vertex(14, -20);
s.vertex(47, -15);
s.vertex(23, 7);
s.vertex(29, 40);
s.vertex(0, 25);
s.vertex(-29, 40);
s.vertex(-23, 7);
s.vertex(-47, -15);
s.vertex(-14, -20);
s.endShape(CLOSE);
}
如果每个对象本身都有自己的几何体(通过算法生成),则此方法是有意义的。但是,如果每个对象都显示相同的PShape,那么在构造函数本身中传递对PShape的引用可能更有意义。让我们来看看这是如何工作的。假设我们创建了一个名为“Polygon”的泛型类,它引用了一个PShape(在display方法中绘制)。
class Polygon {
PShape s;
void display() {
shape(s);
}
}
在前面的例子中,形状是在对象的构造函数中创建的。在这里,我们将演示一种不同的方法来编写通过参数设置形状的构造函数。
Polygon(PShape s_) {
s = s_;
}
但是,要使其工作,必须在创建对象时传入PShape。下面是在带有setup()的主选项卡中的情况:
Polygon poly; // 多边形物体
void setup() {
size(640,360,P2D);
PShape star = createShape(); // 首先我们要做一个
star.beginShape();
star.noStroke();
star.fill(0, 127);
star.vertex(0, -50);
star.vertex(14, -20);
star.vertex(47, -15);
star.vertex(23, 7);
star.vertex(29, 40);
star.vertex(0, 25);
star.vertex(-29, 40);
star.vertex(-23, 7);
star.vertex(-47, -15);
star.vertex(-14, -20);
star.endShape(CLOSE);
// 通过传入对PShape的引用来生成多边形对象
poly = new Polygon(star);
}
这是一种非常灵活的方法。例如,如果有一个PShape对象数组,则可以使用随机PShape创建新的多边形对象。下面是一个简单的实现。
ArrayList polygons;
PShape[] shapes = new PShape[2]; // An array of PShapes
void setup() {
size(640, 360, P2D);
smooth();
shapes[0] = createShape(ELLIPSE,0,0,100,100); // Two PShapes
shapes[1] = createShape(RECT,0,0,100,100);
polygons = new ArrayList();
for (int i = 0; i < 25; i++) {
int selection = int(random(shapes.length)); // Pick a random index
Polygon p = new Polygon(shapes[selection]); // Use corresponding PShape to create Polygon
polygons.add(p);
}
}
对于完整的示例,请查看“polygonpshapeop1”、“polygonpshapeop2”和“polygonpshapeop3”(在“文件→示例→主题→创建形状”下)。
更多自定义形状
pshape支持在即时模式下可以绘制的所有相同类型的形状。这些包括:点、线、三角形、三角形扇形、三角形条、四边形和四边形条。例如,如果要创建四边形条带,可以说:
/
PShape s = createShape();
s.beginShape(QUAD_STRIP);
如果未指定模式,则该形状可以是任何不规则多边形,如我们在上一个星形示例中看到的。通过不关闭形状,PShape也可以是路径。下面是一个将正弦波作为PShape对象的路径示例。
PShape path = createShape();
path.beginShape();
float x = 0;
//将路径计算为正弦波
for (float a = 0; a < TWO_PI; a += 0.1) {
path.vertex(x,sin(a)*100);
x+= 5;
}
// 如果你想让形状成为路径,就不要“关闭”它
path.endShape();
有关完整实现,请参见示例“PathPShape”。
pshape还包括begincoutor()和endContour()方法。这些方法允许您从另一个形状中剪切一个形状,并在其中创建带有孔的形状。想想画一个字母的轮廓,比如P(用于处理)。P的轮廓可以绘制为一系列顶点,但是要从P的中间敲出相反的形状,需要开始/结束轮廓并绘制内部的路径。下面是一个简单的例子,它在一个外部正方形的内部绘制一个内部正方形。
// 成形
PShape s = createShape();
s.beginShape();
// 外形外部
s.vertex(-100,-100);
s.vertex(100,-100);
s.vertex(100,100);
s.vertex(-100,100);
s.vertex(-100,-100);
// 形状内部
s.beginContour();
s.vertex(-10,-10);
s.vertex(-10,10);
s.vertex(10,10);
s.vertex(10,-10);
s.endContour();
// 成品形状
s.endShape();
请注意,定义负形状的顶点必须与外部形状的方向相反。首先按顺时针顺序为外部形状绘制顶点,然后为内部形状逆时针绘制顶点。
有关完整实现,请参见示例“BeginedConteur”(在“文件→示例→主题→创建形状”下)。
PShape群
PShape的另一个便利之处是能够对形状进行分组。例如,如果你想从一组圆、矩形和自定义多边形中创建一个外星生物呢。如果头部是圆形,身体是矩形,你可能会认为你需要:
PShape head = createShape(ELLIPSE,0,0,50,50);
PShape body = createShape(RECT,0,50,50,100);
shape(head);
shape(body);
当然这是可行的,但如果你能把头部和身体组合成一个形状,那就方便多了。有了一个“小组”,你就可以了。
// 生成父形状
PShape alien = createShape(GROUP);
// 做两个形状
PShape head = createShape(ELLIPSE, 0, 0, 50, 50);
PShape body = createShape(RECT, 0, 50, 50, 100);
// 将两个“子”形状添加到父组
alien.addChild(head);
alien.addChild(body);
// 绘制组
translate(width/2, height/2);
shape(alien);
有关演示将基本形状、自定义多边形和路径组合在一起的PShape的完整示例,请参见“GroupPShape”(在“文件>示例>主题>创建形状”下)。
PShape组允许您构建复杂的形状层次结构。这又允许您通过在父级调用相应的方法来设置子形状的颜色和属性。类似地,通过在层次结构的给定级别调用转换函数,只会影响下面的形状。
加载外部形状文件
pshape还支持加载形状文件,例如SVG(用于2D形状)或OBJ(用于3D形状)。这可以通过loadShape()方法实现。
PShape svg;
void setup() {
size(640, 360, P2D);
svg = loadShape("star.svg");
}
void draw() {
background(255);
shape(svg);
}
实时操作PShape的顶点
在使用PShape大约五分钟后,不可避免地会出现一个问题:“如果我想晃动所有顶点,该怎么办?”PShape允许您通过getVertex()和setVertex()方法动态访问和更改顶点。
要在PShape的顶点上迭代,可以从0循环到顶点总数(getVertexCount())。假设一个PShape“s”,这看起来像:
for (int i = 0; i < s.getVertexCount(); i++) {
}
//可以使用getVertex()将顶点检索为PVector对象。
for (int i = 0; i < s.getVertexCount(); i++) {
PVector v = s.getVertex(i);
}
//然后,可以通过操纵PVector并使用setVertex()设置新值来移动该顶点。
for (int i = 0; i < s.getVertexCount(); i++) {
PVector v = s.getVertex(i);
v.x += random(-1,1);
v.y += random(-1,1);
s.setVertex(i,v.x,v.y);
}
有关使用Perlin噪波摆动多边形顶点的示例,请参见“WigglePShape”(在“文件”—“示例”—“主题”—“创建形状”下)。
3D和纹理
本教程主要介绍在二维图形环境中使用PShape对象的基础知识。但是,PShape也提供了P3D教程中描述的所有3D功能。主要区别在于,在三维中,顶点是用x、y和z坐标指定的。此外,PShape对象可以使用setTexture()函数自动生成纹理。对于像球体这样的复杂形状,这使事情变得很容易。
PImage img;
PShape globe;
void setup() {
// 加载图像
img = loadImage("earth.jpg");
globe = createShape(SPHERE, 50);
// 使用图像自动对形状进行纹理处理
globe.setTexture(img);
}