三维是二维的进阶。在学习新的知识点前,希望你已经在二维绘图上足够娴熟。
用代码在三维空间绘图,与 3dMax ,Maya 这类三维软件相比,有完全不一样的体验。由于里面没有直观的界面来控制和调整物体的位置,所以写起来会比较抽象。其中有一难点,要熟悉空间坐标系以及对应的各种变换操作。
在平面上绘图只需考虑两个维度,x 轴和 y 轴。但在三维空间中,会多出一个 z 轴。
三维空间中的坐标系
下面先来看三维空间中的坐标分布情况
对比原来的平面坐标系可以发现,x,y 轴的方向与原来是一致。新增加的 z 轴,正方向朝向的是观察者。它表示深度,物体的远近也取决于 z 轴上的数值。
而三维空间中的坐标系原点,与平面二维坐标系的坐标原点位置是一致的。都处于窗口的左上方,上图为了便于演示各个坐标轴的朝向,才对原点进行了平移。下图才是坐标系的默认状态
-
注:添加了旋转动画效果展示深度
在绘图时,这个坐标系在头脑中必须非常明晰,包括各轴的正方向、负方向。为了加深印象,便于理解。下面再针对具体的坐标点做一些举例。
假设网格之间的距离为 50,某个空间坐标数值为(300,0,0),它便会处于以下位置。
-
注:坐标原点进行过平移处理
当空间坐标值为 (-300,0,0) 时,便会往左偏移
坐标值为 (0,300,0)
坐标值为 (0,0,300)
坐标值为 (300,300,300)
绘制正方体
前面用了较多篇幅去讲坐标系。这是 3D 绘图的基础。只有熟悉它,才能在空间随心所欲地按自己的构想画出点线面。
下面正式进入绘图部分。从最简单的图形开始,在窗口中绘制一个正方体。
代码示例(13-1):
boolean showWire; void setup() { size(700, 700, P3D); } void draw() { background(0); if (showWire) { // 关闭填充,只显示线框 fill(255); } else { // 开启填充 noFill(); stroke(255); } translate(width/2, height/2, mouseX); strokeWeight(3); box(100); } void mousePressed() { showWire = !showWire; }
运行效果:
代码说明:
-
Processing 默认绘图采用的二维模式。因此在进行三维绘图前,需要先进行一些设置。size 函数是一个可重载的参数,最后一个参数可以设置成 P3D 或 OPENGL 模式。不同模式会对应不同的渲染器。P3D 最简单,兼容性最强。OPENGL 效率更高,能够使用显卡加速。这里先选择 P3D 进行演示。
-
例子中设置了一个布尔变量 showWire 用于切换显示模式。当点击鼠标时,showWire 的值会进行取反。从而影响到 draw 函数中的判定语句,实现切换效果
-
translate 的作用大家并不陌生,是对坐标系进行平移。它同时是一个可重载的函数,当输入的参数个数为 3 时,第 3 个参数便会影响 z 轴。前两个参数分别设置成了 width/2 和 height/2。所以坐标原点就恰好居中到屏幕中央,最后的参数由 mouseX 控制。因此左右移动鼠标,可以看到图形的远近发生变化
-
这里看似物体的位置在移动。但实质上仅仅是坐标系的位置发生了变化,物体的坐标其实仍处于原点
旋转正方体
前面由于摄像机的视角是固定的,物体也没有旋转,所以很不立体,更像是平面的。下面介绍的 rotate() 函数,可以让正方体旋转起来。
rotate 函数有几种变式。分别是 rotateX,rotateY,rotateZ。它们会绕不同的坐标轴进行旋转。
其中,rotate 类的函数始终是以坐标原点为中心进行旋转的。所以若想某物体以自身为中心旋转,需要先使用 translate 将坐标原点变换到原物体的坐标上。
代码示例(13-2):
int rotateMode; void setup() { size(700, 700, P3D); } void draw() { background(0); translate(width/2, height/2); if (rotateMode == 0) { rotateX(millis() * 0.002); } else if (rotateMode == 1) { rotateY(millis() * 0.002); } else { rotateZ(millis() * 0.002); } noFill(); strokeWeight(3); stroke(255); box(300); } void mousePressed() { rotateMode++; if (rotateMode == 3) { rotateMode = 0; } }
运行效果:
代码说明:
-
鼠标单击便可切换不同的旋转模式,分别绕 x 轴,y 轴,z 轴进行旋转。整型变量 rotateMode 作为中介
-
在绘制三维图形时,translate() 允许只传入两个参数。此时 z 坐标默认值为 0
关于 rotate 的旋转方向
rotate 函数的旋转方向是有固定规律的。当传入的参数为递增时,坐标系便会面朝旋转轴的正方向,进行逆时针旋转。与之相反,递减时则呈顺时针旋转。
下面是数值递增时的旋转情况。
rotateX(millis() * 0.001);
rotateY(millis() * 0.001);
rotateZ(millis() * 0.001);
除了用常规的方式记忆以外,还可以借鉴中学物理课上提到的右手螺旋定则(也叫安培定则)
所谓的右手螺旋定则,是一种判定电流和电流磁场的磁感线方向的方法。右手握住通电导线,大拇指朝向电流方向,其余四指的指向便会对应磁感线方向。
程序中没有电流,没有磁场。但可以用类似的方式来描述方向。例如,当 rotateX 传入的参数递增时,就可以采用“左手螺旋定则”。左手握住旋转的坐标轴 X 轴,大拇指朝向坐标轴的正方向,其余四指的指向便会对应旋转方向。当数值递减时,则采用“右手螺旋定则”,判定方法是一致的。右手握住旋转的坐标轴,大拇指朝向坐标轴的正方向,其余四指的指向便会对应旋转方向。(可对照前面的动图理解此法则)
3D 绘图的相关函数
前面只介绍了一个绘图函数 – box,下面将罗列 Processing 中内置的 3D 绘图函数以及对应的各种重载形式。
函数名 | 函数功能 | 参数说明 |
---|---|---|
sphere(float r) | 绘制球体 | r:球体半径 |
sphereDetail(float a) | 设置球体精度 | a:球体精度 |
sphereDetail(float a,float b) | 设置球体精度 | a:球体经度精度,b:球体纬度精度 |
box(float size) | 绘制正方体 | size:正方体边长 |
box(float w,float h,float d) | 绘制长方体 | w、h、d 分别代表长方体的宽度,高度,深度 |
box 函数,它有两种重载形式。当参数个数为 1 时,数值就代表正方体的边长。参数个数为 3 时,可以分别控制长方体的长宽高。
sphere 函数可以绘制球体,结合 sphereDetail 可以设置球体精度。以上便是 Processing 中仅有的两种内置 3D 函数。
对 3D 绘图而言,仅用这两个函数显然是远远不够的。如果希望绘制一些更复杂的图形,则可以通过自定义函数的方式来实现。由于 3D 图形在程序中本质是由一个个面拼接而成的,所以只要定义好每个面的顶点位置,就能组成一个新的立体图形。下面将提供一个有关圆柱体绘制的函数。只要复制到程序中就可直接调用。
绘制圆柱体
代码示例(13-3):
void setup() { size(700, 700, P3D); } void draw() { background(0); translate(width/2, height/2); rotateX(millis() * 0.001); rotateY(millis() * 0.001); stroke(255); strokeWeight(3); noFill(); drawCylinder(int(mouseX * 0.05), 120, 350); } void drawCylinder(int sides, float r, float h) { float angle = 2 * PI / sides; float halfHeight = h / 2; // 绘制顶面 beginShape(); for (int i = 0; i < sides; i++) { float x = cos( i * angle ) * r; float y = sin( i * angle ) * r; vertex( x, y, -halfHeight ); } endShape(CLOSE); // 绘制底面 beginShape(); for (int i = 0; i < sides; i++) { float x = cos( i * angle ) * r; float y = sin( i * angle ) * r; vertex( x, y, halfHeight ); } endShape(CLOSE); // 绘制侧面 beginShape(TRIANGLE_STRIP); for (int i = 0; i < sides + 1; i++) { float x = cos( i * angle ) * r; float y = sin( i * angle ) * r; vertex( x, y, halfHeight); vertex( x, y, -halfHeight); } endShape(CLOSE); }
运行效果:
代码说明:
-
函数 drawCylinder 可传入三个参数,其中 sides 代表圆柱的底面精度,r 表示圆柱半径,h 表示圆柱的高
-
要绘制一个圆柱形,第一步要思考它能划分成哪些体块。理想状态下,圆柱的顶面和底面分别是两个圆形,而侧面,则是一个在空间上弯曲,带有弧度的曲面。但在计算机中,其实不存在“圆形”和“曲面”,它们是由更基本的单元构成的。例如程序中只显示边线的圆,就是由一根根的线段拼接而成,当数量足够多,就会变得平滑,也趋近于圆。而只显示填充色的圆,则是由三角面构成。曲面是同样的道理,构成元素也是三角面,当面数越多,图形就越光滑。
-
因此,要绘制一个带有体积的图形,重中之重就要考虑如何在空间中绘制三角面。前面有提到过 triangle 函数,但它只能在二维平面上画三角形。如果要在空间中表示,就需必须要使用 beginShape ,endShape 和 vertex 函数。Vertex 是一个可重载函数,当传入的参数个数为 3 时,就表示空间坐标。“绘制顶面”与“绘制底面”的两段代码大家应该不会陌生,用三角函数计算得出圆的各个顶点,前后再用 beginShape 和 endShape 进行包裹,就能表示一个圆。
-
而“绘制侧面”的代码,里面有一句命令“ beginShape(TRIANGLE_STRIP) ” ,TRIANGLE_STRIP 用于设置顶点的连线模式。它会决定点和点之间,是哪些共同组成一个三角面。当不填任何参数时,默认是根据点的先后顺序,连成一个多边形
-
当使用 TRIANGLE_STRIP 后,会先将前三个坐标连成一个三角形,之后每增加一个坐标,便会和前两个坐标组成新的三角形。通过这种互相交替的方式,先后连接底面和顶面的顶点,就能生成曲面
-
这段有关圆柱体的代码无需完全理解,随着学习的深入。会发现有大量的函数和类库是别人已经封装好的,对设计师而已没有必要过度深究这些底层知识。毕竟创作才是最终目的,只要了解调用的方法
除了这些有体积的绘图函数,前面在平面视图下提到过的 line,triangle,rect,ellipse 等函数也能在三维空间中正常显示。只是这类函数大多只有 x ,y 坐标。要表示深度关系,只能结合 translate 函数来对坐标系进行平移。其中 line 函数比较特殊,它允许重载,坐标部分可以多写一个 z 坐标。
函数名 | 函数功能 | 参数说明 |
---|---|---|
line(float x1,float y1,float z1,float x2,float y2,float z2) | 绘制直线 | 前三个参数代表直线端点 A 的空间坐标,后三个参数代表端点 B 的空间坐标 |
3D绘图函数-综合示例01
由于图形的绘制都大同小异,这里就不独立列举每个函数的使用方法。下面整合了一个实例。
代码示例(13-4):
int index; void setup() { size(700,700,P3D); } void draw() { background(0); translate(width/2, height/2); rotateX(millis() * 0.001); rotateY(millis() * 0.0015); noFill(); stroke(255); strokeWeight(3); switch (index) { case 0: box(300); break; case 1: sphereDetail(12); sphere(180); break; case 2: line(-200, 0, 200, 0); break; case 3: triangle(150, -200, 0, 150, -150, -200); break; case 4: rectMode(CENTER); rect(0, 0, 300, 300); break; case 5: ellipse(0, 0, 400, 400); break; default: break; } } void keyPressed() { if (keyCode == LEFT) { index--; if (index < 0) { index = 5; } } if (keyCode == RIGHT) { index++; if (index > 5) { index = 0; } } }
运行效果:
代码说明:
-
rotateX 和 rotateY 的旋转速度略有不同,目的是为了让视角变化更丰富。
-
为了观察空间体积,使用了 noFill 函数只显示线框
-
LEFT,RIGHT 作为特殊键值,分别代表方向键的左键和右键
-
变量 index 代表了不同的模式。方向键的左键和右键,可以切换不同的图形
摄像机的控制
下面将介绍摄像机的用法,使用它可以更灵活地控制窗口视角。使用摄像机,需要用到一个名叫 “PeasyCam” 的库。这不是 Processing 中内置的,所以需要到 Libraries 处下载并安装。
安装完毕后,便能使用以下代码
代码示例(13-5):
import peasy.*; PeasyCam cam; void setup() { size(700,700,P3D); cam = new PeasyCam(this, 600); } void draw() { background(0); stroke(255); noFill(); strokeWeight(3); box(300); }
运行效果:
代码说明:
-
通过 import 导入库
-
在对 PeasyCam 对象初始化时,默认摄像机是朝向坐标原点的,其中第二个参数表示摄像机到目标的距离。
-
PeasyCam 集成了很多方便的功能,下面是操作方法
操作方式一览:
鼠标左键拖动(笔记本触摸板三指移动):会以原点为目标,旋转摄像机视角
鼠标右键上下拖动(笔记本触摸板双指移动):推拉摄像机,远近变化
鼠标左键拖动 + Command 键:平移摄像机
鼠标左键双击:恢复摄像机初始视角
-
使用 PeasyCam 有一个好处,坐标原点会默认居中到屏幕中央。这样就无需每次使用 translate(width/2,height/2) 。
灯光系统
谈到三维绘图,不得不提到灯光。要使物体具有立体感,灯光是不可获缺的。Processing 中,就提供多种类型的灯光。如果你之前使用过 3dMax,Maya 这类三维动画软件,一定不会陌生。
在 Processing 中可以使用的灯光类型有这几类。
环境光:光在环境中进行了充分散射,没有光源位置和方向,强度均匀。
平行光:当某个光源距离物体接近无限远,就会产生平行光。在程序中无需设定位置,只有方向,常用于模拟太阳光照
点光源:点光源的光都会从某个点放出,它对各个方向的光照强度是一致的,并会随距离衰减。常用于模拟灯光
聚光灯:聚光灯是一种舞台灯光,投出的灯光近似于锥形。它有多个参数。如角度,集中度等。
下面先以点光源为例。
代码示例(13-6):
import peasy.*; PeasyCam cam; void setup() { size(700, 700, P3D); cam = new PeasyCam(this, 500); } void draw() { background(0); // 启用灯光 pushMatrix(); pointLight(255, 255, 255, -150, -150, 150); rotateX(millis() * 0.0005); rotateY(millis() * 0.0005); noStroke(); box(250); popMatrix(); // 绘制灯光位置 noLights(); translate(-150, -150, 150); fill(255); sphere(10); }
运行效果:
代码说明:
-
pointLight 函数的作用是在场景中启用点光源。使用时必须放在 draw 函数中,在它之后的绘图函数都会受到点光源的影响,产生光照效果。其中前三个参数代表光源颜色,后三个参数代表点光源的空间坐标
-
由于灯光本身是不会被程序绘制出来,但为了更直观我们可以手动绘制。例如将灯光的坐标用球体表示。noLights() 函数的作用是关闭灯光,这样球体就不会受灯光影响,并会采用平涂的方式进行着色。若不使用 noLights(),由于程序里点光源的位置恰好处在球心当中,所以外表面就不会被照亮,下面是不使用 noLights() 的效果
-
有一点值得注意,光源的位置会受 rotate ,translate 等函数的影响。例子中为了让点光源的位置固定在(-150, -150, 150),所以写在 rotateX 和 rotateY 之前
深入灯光系统
下面通过一个综合实例了解各种类型的灯光用法。
代码示例(13-7):
import peasy.*; PeasyCam cam; int index, lightMode; void setup() { cam = new PeasyCam(this, 600); size(700, 700, P3D); } void draw() { background(50); if (lightMode == 0) { ambientLight(255, 0, 0); } else if (lightMode == 1) { directionalLight(0, 255, 0, mouseX, mouseY, 0); } else if (lightMode == 2) { // 绘制点光源位置 pushMatrix(); translate(-200, -200, 200); fill(255); sphere(10); popMatrix(); // 开启点光源 pointLight(0, 0, 255, -200, -200, 200); } else if (lightMode == 3) { // 绘制聚光灯位置 pushMatrix(); translate(0, 0, 300); fill(255); sphere(10); popMatrix(); // 开启聚光灯 spotLight(255, 255, 255, 0, 0, 300, 0, 0, -1, mouseX/float(width), mouseY/float(height) * 1000); } pushMatrix(); rotateX(millis() * 0.001 * 0.3); rotateY(millis() * 0.0015 * 0.3); noStroke(); switch (index) { case 0: box(300); break; case 1: sphereDetail(200); sphere(180); break; case 2: stroke(255); strokeWeight(3); line(-200, 0, 200, 0); break; case 3: triangle(150, -200, 0, 150, -150, -200); break; case 4: rectMode(CENTER); rect(0, 0, 300, 300); break; case 5: ellipse(0, 0, 400, 400); break; default: break; } popMatrix(); } void keyPressed() { if (keyCode == LEFT) { index--; if (index < 0) { index = 5; } } if (keyCode == RIGHT) { index++; if (index > 5) { index = 0; } } if (keyCode == UP) { lightMode--; if (lightMode < 0) { lightMode = 3; } } if (keyCode == DOWN) { lightMode++; if (lightMode > 3) { lightMode = 0; } } }
运行效果:
环境光:
平行光:
点光源:
聚光灯:
代码说明:
-
键盘方向键的左右键可切换图形,上下键可切换光照模式
-
环境光,平行光,聚光灯与点光源一样,需要写在 draw 函数中
-
函数 ambientLight() 可创建环境光。里面的 3 个参数代表光源颜色,分别对应 R,G,B。环境光会从各个方向放出均匀的光线,因此例子中环境光一旦设置成红色,物体表面呈现的颜色就是均匀的。
-
函数 directionalLight() 可创建平行光。前 3 个参数代表光源颜色,分别对应 R,G,B。后 3 个参数代表光线的指向方向,分别对应 x,y,z 轴。这里使用了 mouseX 和 mouseY 作为参数,因此可以通过移动鼠标来改变光源方向。
-
点光源和聚光灯是有特定位置的,为了直观地显示。所以在前面使用 sphere 来绘制灯光。
-
函数 spotLight() 可创建聚光灯。它的参数最为复杂,多达 11 个。前 3 个参数代表光源颜色,分别对应 R,G,B。后面的 3 个参数代表聚光灯的位置,紧跟后面的 3 个参数代表聚光灯的指向,分别对应 x,y,z 轴。倒数第二个参数代表光锥的角度,最后一个参数代表集中度(光线边缘的模糊程度)。
-
除了 line 函数,其他绘图函数都会受灯光影响。
灯光的综合实例01
代码示例(13-8):
void setup() { size(700, 700, P3D); } void draw() { background(0,50,71); translate(width/2, height/2); pointLight(255, 0, 255, 200, -200, 200); pointLight(0, 255, 255, -200, 200, 200); int num = 5; float interval = 120; float w = (num - 1)* interval; for (int i = 0; i < num; i++) { for (int j = 0; j < num; j++) { pushMatrix(); translate(-w/2 + i * interval,-w/2 + j * interval); rotateX(millis()/1000.0 + i * 0.1); rotateY(millis()/1000.0 + j * 0.1); noStroke(); box(65); popMatrix(); } } }
运行效果:
代码说明:
-
像现实中的灯光一样,程序中的光照效果也是可以叠加的。这里就设置了两个不同颜色,不同位置的点光源,这样可以使物体更有层次感
-
局部变量 w 代表的是整个矩阵的宽度,通过间距和方体的数量可以计算得出。获得了 w 的数值之后,就可以计算偏移量,让整个矩阵恰好居中到屏幕中央
灯光的综合实例02
代码示例(13-9):
int num; float []x,y,z; void setup() { size(700, 700, P3D); num = 50; x = new float[num]; y = new float[num]; z = new float[num]; for (int i = 0; i < num; i++) { x[i] = random(-200, 200); y[i] = random(-200, 200); z[i] = random(-200, 200); } } void draw() { colorMode(RGB); background(79, 50, 127); translate(width/2, height/2); pointLight(255, 0, 0, 200, -200, 200); pointLight(255, 0, 255, -200, 200, 200); rotateX(millis() / 1000.0); rotateY(millis() / 1000.0); for (int i = 0; i < num; i++) { pushMatrix(); translate(x[i], y[i], z[i]); rotateX(i * 0.2 + millis() * 0.004); rotateY(i * 0.3 + millis() * 0.004); noStroke(); box(30,10,90); popMatrix(); } for (int i = 0; i < num - 1; i++) { if (random(1) < 0.08) { colorMode(HSB); stroke(random(255),255,255); strokeWeight(4); line(x[i], y[i], z[i], x[i + 1], y[i + 1], z[i+1]); } } }
运行效果:
代码说明:
-
colorMode(HSB) 的作用是将颜色模式设置为 HSB 模式。这样就能控制线条的色相产生随机变化。由于背景色的参数采用的是 RGB 模式,所以需要在开头使用 colorMode(RGB) 将模式切换回 RGB。
-
第二个 for 循环,作用是对方体之间随机进行连线
灯光系统总结
Processing 的灯光系统有两个特性。一是没有投影,二是物体之间不会相互影响,光没有反射也没有折射。所以在真实感上比不上 Unity 这类游戏引擎,画面的精致程度也很难达到 C4D,Maya 等软件渲染出的效果。
尽管如此,作为一个不那么仿真,阉割版的灯光,只要参数调节得当,还是可以有不错的三维表现力。若是觉得无法满足创作需求了,就可以尝试学习一些更高阶的内容 – Shader Programming , 使用它就不会有以上的限制,可以用 GPU 充分挖掘图形的性能。
若想更多地了解 Shader,推荐 Patricio 的入门教程(http://thebookofshaders.com/),中文版(http://thebookofshaders.com/?lan=ch)
shadertoy 网站可以查看更多绚丽作品,你能从中领略 shader 的魅力。(https://www.shadertoy.com/)
END
至此,基础部分已经完结。回顾前半部分的写作。整个过程并不轻松,作为基础教程,必须在有条件限制的情况下将核心概念讲清楚。保证通俗的同时,实例要简炼有趣。作为初稿,自我评价还算过关。
编程技能的修炼之路上,遵循某种“二八定律”。只要掌握 20 % 的核心概念,就能解决 80 % 的问题。虽然上部分涉及的知识点还远远不到 20 %,但只要把基础巩固好。就能更游刃有余地往下深入。
后面会开始进入新的章节。不谈技术的细枝末节,更多地围绕图形创作展开。敬请期待~
未经允许不得转载:前端撸码笔记 » 写给设计师的 Processing 编程指南(13) – 3D绘图