Even in the dark, you must know that the stars, the sun, and the moon have never left. We can’t see the light, but the light is always there.
背景&创意
“I want to swim in the ocean of stars!”
我喜欢星空,不记得从什么时候开始我已经看不到夜晚的星空,我只能从记忆和图片里享受这种静谧和浩瀚。遥远的星团在慢慢转动,几颗耀眼的恒星在拼命发光,无数的小星星点缀了原本沉寂的漆黑。我有个想法,如果我能触碰这些光亮,再如果我能操控这些星团描绘出我内心的星海,现在这些已经成为现实。我要用我的Particle World程序,呈现我内心的星海!
Particle World是基于Processing开发的一款简单的模拟星空程序,利用粒子模型结合引力、斥力和向量相关技术最大限度的呈现了星团、恒星、小星星等星空中的可见部分,并且添加了一些交互如吸引、扰动等影响星空动态的功能1。
功能
1.创建自定义的窗口大小;
2.随机生成不同颜色大小的粒子;
3.移动鼠标,在鼠标周围产生可变范围的斥力,影响粒子运动;
4.按住鼠标并移动,在鼠标位置处产生可变范围的吸引力,影响所有粒子的运动;
5.窗口边界检测,粒子生存周期检测;
6.随机生成星团;
程序框架
核心技术2
完整的粒子系统的建立
在详细阅读了《代码本色》的粒子系统部分章节以后,结合目标程序实现功能的复杂性,决定先建立好基础,即将粒子系统做的尽可能完善,粒子系统的代码框架如下。
其中边界检测函数,对粒子触碰到窗口的上、下、左、右四个边界进行检测,当粒子触碰到边界时,将会以同样大小的力向相反的方向进行反弹。
// 窗口边缘检测函数
void checkEdges(int flag) {
switch(flag) {
// 触及边缘反弹
case 1:
if (location.x > width) {
location.x = width;
velocity.x = -1;
}
if (location.x < 0) {
location.x = 0;
velocity.x = 1;
}
if (location.y > height) {
velocity.y *= -1;
location.y = height;
}
if (location.y < 0) {
velocity.y *= -1;
location.y = 0;
}
星团
实现思路:显示窗口内随机位置处一定范围内生成大量粒子,此范围内的所有粒子受到相同的向心力的作用,围绕星团中心做向心运动3。
1.星团圆形范围内随机生成粒子,如果用传统的random()函数,在给定范围内随机取二维坐标,随着粒子数量的增加,最终星团呈现的是是一个正方形区域,要使粒子能够在圆形的区域内随机生成,首先要编写一个circleLocation()函数,确定星团内粒子的位置。
// 圆形范围内随机取点函数
PVector circleLocation() {
PVector cl = new PVector();
float currentR;
float currentAngle;
currentR = random(0,scope);
currentAngle = random(1,360);
cl.x = origin.x + sin(currentAngle * PI / 180) * currentR;
cl.y = origin.y + cos(currentAngle * PI / 180) * currentR;
return cl;
}
2.星团内所有粒子要针对星团中心做向心运动,首先要遍历在星团范围内的所有粒子,根据公式加速度与引力的中心与粒子的距离的二次方成反比,对所有粒子的运动方式进行调整。
// 星团内粒子应用引力
void applyInward() {
float magnetism = 100.0; // 引力强度
float deceleration = 0.01; // 移动减速
for (MyParticle p: starCluster) {
// 计算星团中心和星团内粒子的距离
float distance = dist(origin.x,origin.y,p.location.x,p.location.y);
if (distance > 1) {
p.acceleration.x = magnetism * (origin.x - p.location.x) / (distance * distance);
p.acceleration.y = magnetism * (origin.y - p.location.y) / (distance * distance);
}
p.velocity.x += p.acceleration.x ;
p.velocity.y += p.acceleration.y ;
p.velocity.x = p.velocity.x * deceleration;
p.velocity.y = p.velocity.y * deceleration;
p.location.x += p.velocity.x;
p.velocity.y += p.velocity.y;
}
}
鼠标引力/斥力点
根据代码本色的指导,粒子类(Particle)中的加速度是一个常量,它从来不会发生变化,为了为了实现一个更好的模拟框架应该遵循牛顿力学定律,并将力的累加算法作用在粒子上,创建一个applyForce()函数;进一步优化粒子系统,在其中加入引力对象(Attractor)和斥力对象(Repeller),与applyForce()函数给粒子作用力不同,引力和斥力对于每个粒子的作用力都不同,所以需要对每个粒子进行逐个计算。在加入引力和斥力对象的过程中,不需要在粒子类(Particle)上作出任何修改,粒子不需要知道外部环境细节,它只需要管理自身的位置、速度、加速度,并接受外力的作用。
class Attractor {
float strength = 300; // 引力强弱
PVector location; // 吸引对象并不移动,只要知晓位置即可
float r = 10;
Attractor(float x,float y) {
location = new PVector(x,y);
}
void display() {
stroke(255);
fill(175);
ellipse(location.x,location.y,r*2,r*2);
}
// 计算斥力(与引力方向相反)
PVector attract(MyParticle p) {
// 1.获取力的方向
PVector dir = PVector.sub(location,p.location); // 力的方向
// 2.获取距离(限制距离)
float d = dir.mag();
d = constrain(d,5,100);
dir.normalize();
// 3.计算力的大小
float force = 1*strength/(d*d);
// 4.组合方向和大小
dir.mult(force);
return dir;
}
}
呈现效果
总结
三体里有一句话“有时下夜班,仰望星空,觉得群星就像发光的沙漠,我自己就是一个被丢弃在沙漠.上的可怜的孩子…我有那种感觉:地球生命真的是宇宙中的偶然里的偶然,宇宙是个空荡荡的大宫殿,人类是这宫殿中唯一的一只蚂蚁。这想法让我的后半辈子有一种很矛盾的心态:有时觉得生命真珍贵,一切都重于泰山;有时觉得人是那么渺小,什么都不值得一提。反正日子就在这种奇怪的感觉中一天天过去,不知不觉人就老了”
当看向星空的时候,你明确的知道我们,我们所有的一切是一个整体,孩童会张开手想要摸摸星云,我们这些自名为成年人的无趣者不再仰望星空,事实生活里的琐事烦扰着我们,星空?不了,哦黑洞被拍到了吗?可以看看,但也没有什么改变吧。可是我们对生命的敬畏和对未知的探寻呢?也这么被搁置了吧。
“我们都是阴沟里的虫子,但总还是得有人仰望星空”。
- 部分功能需要进一步优化,以实际程序呈现效果为主。 ↩︎
- 部分功能实现参考《代码本色》。 ↩︎
- 可继续优化为椭圆运动轨迹。 ↩︎