一花一世界,一叶一菩提。每个人眼中的花是不一样的,每个人眼中的世界也是不一样的 。昔时佛祖拈花,惟迦叶微笑,既而步往极乐。在菩提树下,从一朵花中便能悟出整个世界,最终得升。
今天就来给大家介绍日本先生Atsushi Tanaka的眼中的两个世界。
第一个是泡泡世界——[Bubbles 02](https://www.openprocessing.org/sketch/682481),效果如下:
第二个是方块世界——[Space Running](https://www.openprocessing.org/sketch/583507),效果如下:
大家肯定会觉得很奇怪,为什么这次一次讲两个作品呢?是因为这个作品背后的原理十分的相似:Bubbles 02里面是一些球体粒子向上移动,Space Running里面是一些立方体粒子向前移动。只不过是这些球体粒子和这些立方体粒子的初始位置不同罢了。
这里得说明一下:这两个作品的交互方式略有不同,为了更加关注两个作品粒子运动方式的异同,下面的代码都是按照Bubbles 02的交互方式来的实现的。所以下面代码的效果和原作品的效果会有很小的差异,不过大家不用太在意,我们应该更多地关注代码背后的原理。
同时这两个作品作者都是P5.js写的,我还是给大家介绍Processing版本的代码。
准备
其实这两个作品就是3D的Particle System,所以首先我们来大概了解一下如何在Processing中绘制3D图形、如何设置光以及如何对坐标系进行旋转。
· 3D渲染器
如果大家想在Processing中绘制3D图形,就必须设置3D渲染器,设置的方法如下:
void setup(){
size(500, 500, P3D); // 设置3D渲染器
· 绘制基本图形
在2D渲染器的情况下,我们有圆形、矩形等基本图形。相似的,在3D时我们也有两个基本图形:球体、立方体。需要说明一点的是,在2D时我们是直接在绘制基本图形的函数中设置这些基本图形的位置,而在3D时我们用translate函数来达到相同目的。
大家请看下面这个例子:在屏幕中心画一个球体。
translate(width / 2, height / 2);
sphere(100); // 绘制一个半径为100的球体
· 光
光是在让我们作品丰富多彩的一个强有力的工具,下面我们就来看看几种基本的光,同时大家可以自己观察一下他们的异同。这里说明一下下面设置光的函数都可以传入6个参数,前三个参数用来设置光的颜色,后面三个参数用来设置光源的位置或者方向。
ambientLight
环境光不是从特定的方向发出的,光线在周围不断地反射,物体从四面八方被均匀地照亮。环境光几乎总是与其他类型的光结合使用。
请大家看下面这个例子。
ambientLight(255, 0, 0, 0, width / 2, 0); // 设置一束从(0, width / 2, 0)发射而来红色的环境光
translate(width / 2, height / 2);
sphere(100);
directionalLight
定向光来自一个方向:当它垂直撞击一个表面时,它会变得更强,而当它以一个柔和的角度撞击时,它会变得更弱。在击中一个表面后,定向光向各个方向散射。
请大家看下面这个例子。
// 后面3个参数分别是指定光在x、y、z方向的发射方向
directionalLight(255, 0, 0, -1, 0, 0); // 设置一束从右边射过来的红色的定向光
translate(width / 2, height / 2);
sphere(100);
pointLight
点光就是从一个点发射过来的光。
请大家看下面这个例子。
pointLight(255, 0, 0, 35, 40, 0);// 设置一束从(35, 40, 0)发射而来红色的点光
translate(width / 2, height / 2);
sphere(100);
· 旋转
因为在作品中观察到了有视角的变换,所以接下来我们来看看如何对坐标轴进行旋转。
请大家看下面这个例子。
translate(width / 2, height / 2);
// 根据鼠标位置设置旋转的角度
float thetaX = map(mouseY, 0, width, -PI, PI);
float thetaY = map(mouseX, 0, width, -PI, PI);
rotateX(thetaX); // 围绕X轴旋转角度参数指定的量
rotateY(thetaY); // 围绕Y轴旋转角度参数指定的量
sphere(100);
代码
在大概了解了一下Processing有关绘制3D图形的知识后我们就可以开始看代码了。那么首先我们下来看Bubbles 02。
· Bubbles 02
主要流程
因为我们之前已经很详细地讲解过粒子系统了,所以大家对这段代码一定不陌生。如果不熟悉的同学可以去看看:代码检测的“冰与火之歌”。
ParticleSystem ps;
void setup() {
//窗口大小设置为全屏,并且选择3D渲染器
fullScreen(P3D);
colorMode (RGB, 256);
ps = new ParticleSystem();
void draw() {
background(0);
ps.run();
class ParticleSystem
下面我们来看看ParticleSystem这个类,这个类也没有太多的需要说明,主要用来管理所有的粒子。对每一个粒子进行添加、删除、更新状态(位置、生命长度等)、绘制等。
class ParticleSystem {
final int MAX_CNT = 200; //系统中最大的粒子数量
ArrayList particles;
float thetaX, thetaY; //分别绕x、y方向的旋转角度
ParticleSystem() {
particles = new ArrayList();
void run() {
//向粒子系统中添加新的粒子
addParticle();
update();
display();
void addParticle() {
// 防止系统的粒子数量过多
if (particles.size() >= MAX_CNT)
return;
particles.add(new Particle());
void update() {
for (int i = particles.size() - 1; i >= 0; i--) {
Particle p = particles.get(i);
//删除不用的粒子
if (p.dead()) {
particles.remove(p);
continue;
p.update();
//根据鼠标位置确定绕着x、y方向旋转的角度
thetaX += (mouseX - thetaX) * 0.04;
thetaY += (mouseY - thetaY) * 0.04;
void display() {
// 设置坐标轴的原点为屏幕的中心
translate(width / 2, height / 2);
//设置不同的光
ambientLight (30, 20, 80);
pointLight (255, 0, 0, 200.0, 0.0, 200.0);
pointLight (255, 0, 0, -200.0, 0.0, -200.0);
pointLight (0, 255, 0, 0.0, 200.0, 200.0);
pointLight (0, 255, 0, 0.0, -200.0, -200.0);
rotateX(thetaY * 0.01);
rotateY(thetaX * 0.01);
for (Particle p : particles) {
p.display();
class Particle
现在我们来看Particle这个类。观察作品作品的每一个粒子是一个球体——泡泡。而这些球体是从屏幕下方向上移动的,所以在设置球体的初始位置的时候,我们将其y方向的位置设置的比屏幕大小的高度高一点。这里得注意目前我们坐标轴的原点是屏幕的中心,也就是(width/2, height/2, 0)的位置。
同时给每个粒子一个竖直方向向上的速度,然后我们这个速度来设置粒子的生命衰减速度。
现在大家理解下面的代码应该就没有问题了。
class Particle {
PVector velocity, acceleration, location;
float lifespan, lifeRate;
float size;
Particle() {
// 设置位置、加速度、速度
float x = random(-width/2, width/2);
float y = height / 2 + 40;
float z = random (-1000.0, 1000.0);
location = new PVector(x, y, z);
acceleration = new PVector();
velocity = new PVector(0, random(-20, -1), 0);
// 设置球体的半径、生命衰减速率、生命长度
size = random(120);
lifeRate = -velocity.y;
lifespan = y;
boolean dead() {
// 如果生命长度为0,那么这个粒子就不需要了
if (lifespan < 0)return true;
return false;
void update() {
// 更新粒子的生命长度、速度、位置
lifespan -= lifeRate;
velocity.add(acceleration);
location.add(velocity);
void display() {
pushMatrix();
noStroke ();
translate(location.x, location.y, location.z);
sphere(size);
popMatrix();
· Space Running
在讲解完Bubbles 02,现在我们来讲解Space Running。因为两个作品背后的原理非常的相似,所以我只给大家指出两者不同的地方。
每次添加的粒子数量
不同于Bubbles 02每次只添加一个粒子,Space Running每次添加3个粒子。
void addParticle() {
if (particles.size() >= MAX_CNT)
return;
for (int i = 0; i < 3; i++)
particles.add(new Particle());
粒子的初始位置、速度
在这个作品中粒子初始位置不同,是在z轴负方向很远的位置。同时每一个粒子的速度不是竖直向上的,是向z轴正方向的。
// 初始位置不同
float x = random (-600, 600);
float y = random (-600, 600);
float z = -MAX_DEPTH;
//初始速度不同
location = new PVector(x, y, z);
acceleration = new PVector();
velocity = new PVector(0, 0, 100);
其中MAX_DEPTH是粒子新增的一个成员变量。
final float MAX_DEPTH = 2000;
粒子的渲染方式
在Bubbles 02中每一个粒子是一个球体,没有颜色,而在Space Running中每一个粒子是一个立方体,并且有颜色。所以首先必须给每一个粒子添加一些成员变量,并且对它们进行初始化。
新增成员变量如下。
float sizeX, sizeY, sizeZ; // 长、宽、高
color c; //颜色
初始化方法如下。
c = color (random (256), random (256), random (256));
sizeX = random(100);
sizeY = random(100);
sizeZ = random(100);
绘制方法如下。
void display() {
pushMatrix();
translate(location.x, location.y, location.z);
// 根据离屏幕的距离设置颜色的透明度
float aphla = map(abs(location.z), MAX_DEPTH, 0, 0, 255);
fill(c, aphla);
stroke(255);
// 根据离屏幕的距离设置线条的粗细
float weight = map(abs(location.z), MAX_DEPTH, 0, 0, 1);
strokeWeight(weight);
// 绘制一个立方体
box(sizeX, sizeY, sizeZ);
popMatrix();
粒子是否需要删除的依据
在第一个作品中我们是根据粒子生命长度是否大于零来判断粒子是否需要删除的,而在这个作品中我们是根据粒子到屏幕的距离来判断的。
具体方法如下。
boolean dead() {
if (abs(location.z) > MAX_DEPTH)return true;
return false;
所以对于该作品的每一个粒子来说,lifespan和lifeRate这两个成员变量已经没有什么存在的必要了,所以将它们删除。同时修改一下Particle的update函数。
void update() {
velocity.add(acceleration);
location.add(velocity);
作者小结
和普通的粒子系统一样,3D的粒子系统也有很多可以拓展的空间。可以改变粒子不同的运动模式,DIY属于你自己眼中的世界。
而在这里我创造一个名叫FishRunning的世界:一些红色的立方体小鱼在一些泡泡里面穿梭。背后的原理很简单,其实就是把上述两个作品简单地结合了一下。效果如下。
感兴趣的同学可以去我的[OpenProcessing](https://www.openprocessing.org/sketch/731513)看在线效果
代码检查 | 如何用Processing实现3D世界同时我也把这三个作品的代码都放在了我的[github](https://github.com/pearmini/processing/tree/master/3DShapes)上。