第一部分:0~2章的针对习作
0~2章的主要内容分别为随机数生成、向量的使用和牛顿运动力学的简单模拟。
第0章针对习作:随机点线生成
其可以当作是灯光,或者是雨点,或者是你认为的任何东西。表现图如下:
在屏幕上不断出现这样的指向四周的射线,再不断消失。此外,鼠标移动到最左端,色彩的变化就会比较缓慢,而移动到最右端,色彩将会比较快速地切换。但不论如何,色彩总是平滑变化的。
随机效果应用在:
1、射线起点(小圆圈)的位置——高斯随机数。
2、射线的方向——蒙特卡洛算法配合抛物线生成的随机方向。
3、射线的长度和线宽——自带的伪随机数函数。
4、射线的色彩——泊林噪声带来的平滑渐变效果,保证了屏幕上总是相近色居多。由于我太喜欢这个泊林噪声的平滑色彩了,所以在后面的程序里都用了这种色彩产生方式。相近色真的非常好看。
以下是代码文本,注释非常详细。
void setup(){
size(520,520);
fill(0,0,0,5);
rect(0,0,width,height);
frameRate(25);
}
float t=0;
float delt=0.001;
void draw(){
fill(0,0,0,2);
rect(0,0,width,height); //清屏
translate(width/2,height/2); //原点移动到画布中心
PVector startPoint=new PVector(0,0); //起点向量
PVector endPoint=new PVector(); //终点向量
PVector direct=new PVector(); //方向向量
float lengthOfline=0; //长度
float widthOfline=2; //宽度
color col; //颜色
float gus; //高斯随机数
//确定起点
gus=(float)randomGaussian();
startPoint.x=map(gus,-3,3,-width/2,+width/2);
gus=(float)randomGaussian();
startPoint.y=map(gus,-3,3,-height/2,+height/2);
int xx=1; //象限数
//确定起点象限
if(startPoint.x>=0&&startPoint.y>=0)xx=1;
else if(startPoint.x>=0&&startPoint.y<=0)xx=4;
else if(startPoint.x<=0&&startPoint.y<=0)xx=3;
else if(startPoint.x<=0&&startPoint.y>=0)xx=2;
//确定方向
direct=getDirect(xx);
//确定长度和线宽
lengthOfline=random(1,300);
widthOfline=random(1,8);
//确定终点
direct.mult(lengthOfline);
endPoint.set(startPoint);
endPoint.add(direct);
//确定颜色
float r,g,b;
r=noise(t);
g=noise(t+200);
b=noise(t+400); //rgb值错开,防止全是白色
r=map(r,0,1,100,255); //映射到一个比较亮的值
g=map(g,0,1,100,255);
b=map(b,0,1,100,255);
col=color(r,g,b);
stroke(col,175);
strokeWeight(widthOfline);
//画线
line(startPoint.x,startPoint.y,endPoint.x,endPoint.y);
fill(col,110);
noStroke();
//画圈
ellipse(startPoint.x,startPoint.y,widthOfline+10,widthOfline+10);
t+=delt;
delt=0.0001*mouseX;
}
PVector getDirect(int xx){
PVector direct=new PVector();
float angle=0;
while(true){
//蒙特卡洛算法,产生0~2pi的数字
float r1,P,r2;
r1=random(0,1);
P=-2*(r1-0.5)*(r1-0.5)+0.55; //概率设置为抛物线
if(xx==1){ //第一象限,需要的角度值为0~PI/2,其它象限以此类推
r1=map(r1,0,1,0,PI/2);
}
else if(xx==2){
r1=map(r1,0,1,PI/2,PI);
}
else if(xx==3){
r1=map(r1,0,1,PI,3*(PI/2));
}
else if(xx==4){
r1=map(r1,0,1,3*(PI/2),2*PI);
}
r2=random(0,0.55); //概率的最大值就是0.55
if(r2<P){
angle=r1; //角度记为angle
break;
}
}
//将角度换算成向量
direct.x=cos(angle); //方向向量,模为1
direct.y=sin(angle);
return direct;
}
其中,行8~9是泊林噪声的参数,每次递增值与鼠标位置相关(行72)。
高斯随机数的范围其实是负无穷到正无穷,但是一般都集中在0(均值)附近。所以映射时只要考虑0左右的一个不大的区间即可(具体区间大小与方差有关,方差越大,区间越大。但将近98%的数值都集中在均值左右3个标准差的区间内,证明需要概率论的知识),将它映射到画布尺寸,即可作为位置。(行23~28)。
确定方向使用了蒙特卡洛算法。根据起点的象限不同,其最终得到的方向范围也不同。因此在行83~94,对结果进行了映射。
蒙特卡洛算法是一种随机数生成算法,其算法为:先生成一个伪随机数r1,根据自定的概率函数来计算r1的概率P。再生成一个伪随机数r2,若r2小于P,则认为取到r1,否则重新执行算法。其优点在于,P的函数可以自行任意地定义。代码中选择了0~1的一个抛物线来作为角度的概率函数。
蒙特卡洛算法得到的是一个0~2PI的数字,即角度。它相当于极坐标下的theta量。用向量表示这个角度,则需要将极坐标转换成二维坐标,模长为一,即r为1。(行102~103)
得到起点和方向向量之后,终点向量则根据公式:终点=起点+方向*步长 得到。最后以起点和终点坐标画线,并在起点位置画个圆,即可。
第1章针对习作:向量运算的简单可视化。
支持加、减、数乘和数量积。表现如下:
分别是求和、求差、数乘和数量积。
求和——鼠标点击至少两个向量后,按S计算,画出所有向量的和向量。
求差——鼠标点击至少两个向量后,按M计算,以第一个向量为起点,依次减去后来画出的向量。如果只有两个向量,则追加画出三角形法则,红点为结果的终点。
数乘——鼠标点击至少两个向量后,按U计算,取第二个向量的模除十之后,去对第一个向量进行数乘,画出结果向量。因此,不会处理符号,即第一个向量永远是被延长的。
数量积——鼠标点击至少两个向量后,按D计算,将计算前两个向量的数量积,并以此为半径,画一个原点在中心的圆。
由于叉乘的结果向量是垂直于两个运算向量的,所以需要三维画布,就没做。
代码文本如下:
void setup(){
size(600,600);
}
int existpoints=0; //点的数量
ArrayList<PVector> points=new ArrayList<PVector>(); //点列表
void draw(){
fill(0);
rect(0,0,width,height);
translate(width/2,height/2);
float mousx,mousy;
mousx=map(mouseX,0,width-1,-width/2,width/2); //mouseX永远是以左上为原点的,需要重新映射
mousy=map(mouseY,0,height-1,-height/2,height/2);
if(existpoints==0){
stroke(255,255,255);
strokeWeight(3);
line(0,0,mousx,mousy); //跟随鼠标画线
}
if(existpoints>=1){ //画出列表里所有向量
//画出之前的所有向量
PVector ed;
ed=new PVector(0,0);
for(int i=0;i<points.size();i++){
ed.set(points.get(i));
stroke(255,255,255);
strokeWeight(3);
line(0,0,ed.x,ed.y);
}
line(0,0,mousx,mousy); //跟随鼠标画线
}
}
void mouseClicked() {
existpoints++; //鼠标点击就将这个点记入队列
PVector firstPoint=new PVector();
firstPoint.x=map(mouseX,0,width-1,-width/2,width/2);
firstPoint.y=map(mouseY,0,height-1,-height/2,height/2);
points.add(firstPoint);
}
void keyPressed() {
if (key == 's'||key == 'S') { //求和
if(existpoints>=2&&looping){
//至少有两个向量,且当前要在循环中
PVector sumPts=new PVector(0,0);
for(int i=0;i<points.size();i++){
sumPts.add(points.get(i)); //累加
}
//清屏,画出和
background(0);
//画出之前的所有向量
PVector ed;
ed=new PVector(0,0);
for(int i=0;i<points.size();i++){
ed.set(points.get(i));
stroke(255,255,255);
strokeWeight(3);
line(0,0,ed.x,ed.y);
}
stroke(55,25,215); //画结果
strokeWeight(4);
line(0,0,sumPts.x,sumPts.y);
noLoop(); //停掉循环
}
}
if(key == 'm'||key == 'M'){
//求差,顺次两两相减
if(existpoints>=2&&looping){
//至少有两个向量,且当前要在循环中
PVector minusPts=new PVector(0,0);
minusPts.set(points.get(0));
for(int i=1;i<points.size();i++){
minusPts.sub(points.get(i));
}
//清屏,画出差
background(0);
//画出之前的所有向量
PVector ed;
ed=new PVector(0,0);
for(int i=0;i<points.size();i++){
ed.set(points.get(i));
stroke(255,255,255);
strokeWeight(3);
line(0,0,ed.x,ed.y);
}
stroke(55,25,215);
strokeWeight(4);
line(0,0,minusPts.x,minusPts.y);
//如果只有两个向量相减,还会画出三角形法则图,向量终点画一个小红点
if(existpoints==2){
stroke(55,215,215);
strokeWeight(3);
line(points.get(1).x,points.get(1).y,points.get(0).x,points.get(0).y);
//终点是被减数的位置
fill(235,25,35);
noStroke();
ellipse(points.get(0).x,points.get(0).y,8,8);
}
noLoop(); //停掉循环
}
}
if(key == 'u'||key == 'U'){
//数乘,只处理第一和第二向量。第一个向量为目标向量,第二个向量取模/10作为倍数。
//不处理符号
if(existpoints>=2&&looping){
//至少有两个向量,且当前要在循环中
PVector multsPts=new PVector(0,0);
multsPts.set(points.get(0)); //取第一个向量
float lengths=points.get(1).mag()/10; //取第二个向量的模/10
multsPts.mult(lengths); //数乘
//清屏,画出数乘
background(0);
//先画结果
stroke(55,25,215);
strokeWeight(5);
line(0,0,multsPts.x,multsPts.y);
//再画原向量。第一向量必定被结果覆盖,所以后画
stroke(255,255,255);
strokeWeight(3);
line(0,0,points.get(0).x,points.get(0).y);
stroke(25,205,85);
line(0,0,points.get(1).x,points.get(1).y);
noLoop(); //停掉循环
}
}
if(key == 'd'||key == 'D'){
//点乘,只计算前两个向量的内积。
//结果是标量,用红色圆圈表示,其半径为内积结果
if(existpoints>=2&&looping){
//至少有两个向量,且当前要在循环中
float dotResult=0;
dotResult=PVector.dot(points.get(0),points.get(1)); //数量积
//清屏,画出内积
background(0);
//画出前两个向量
stroke(255,25,255);
strokeWeight(3);
line(0,0,points.get(0).x,points.get(0).y);
stroke(25,205,85);
line(0,0,points.get(1).x,points.get(1).y);
//画出内积
stroke(200,35,35);
noFill();
ellipse(0,0,dotResult,dotResult);
noLoop(); //停掉循环
}
}
if(key == 'c'||key == 'C'){
//清空队列,开启循环,即初始化
if(!looping){
points.clear();
existpoints=0;
loop();
}
}
}
由于原点设置在了画布中心,而mouseX和mouseY都是以左上角为原点来计数的,所有需要一次映射转换。
其余的,代码中有详尽注释。
第2章针对习作:牛顿力学的简单使用
做了个类似于粒子的效果。所有小球都有随机的大小和质量,都受到鼠标的引力吸引,同时可以增加左右方向的水平风力,开启空气阻力和重力。
操作如下:
S——风停。
A、D——向左吹风/向右吹风的风力增加。
G——开启/关闭重力。
F——开启/关闭空气阻力。
同样的,这样的相近色也是由泊林噪声随机生成的,生成方法与之前的一样。另外,小圆不加边框是来自同学的意见,表示这种朦胧感更适合浅色系。
代码文本如下:
主程序:
ArrayList<mover> movs=new ArrayList<mover>(); //列表
int num=60; //mover数量
PVector windForce; //风力向量
boolean isgravity=false; //是否开启重力和空气阻力
boolean isfAir=false;
void setup(){
size(1200,800);
float tt=1; //泊林噪声参数,在构造函数中用到
for(int i=0;i<num;i++){
movs.add(new mover(tt)); //生成小球们
tt+=0.045;
}
windForce=new PVector(0,0); //初始化风力
}
void draw(){
fill(255,5);
rect(0,0,width,height);
for(int i=0;i<num;i++){
movs.get(i).update(windForce,isgravity,isfAir); //更新位置
movs.get(i).Bounds(); //防止飞得太远,吸不回来
movs.get(i).display(); //画出自己
}
}
void keyPressed(){
if(key == 's'||key == 'S'){
//风停
windForce.set(0,0);
}
if(key == 'a'||key == 'A'){
//向左吹风风力增加
PVector addLeftForce=new PVector(-0.02,0);
windForce.add(addLeftForce);
}
if(key == 'd'||key == 'D'){
//向右吹风风力增加
PVector addRightForce=new PVector(0.02,0);
windForce.add(addRightForce);
}
if(key == 'g'||key == 'G'){
isgravity=!isgravity; //启动重力模仿。重力比较小,使得能够吸回大部分小球
}
if(key == 'f'||key == 'F'){
isfAir=!isfAir; //启动空气阻力模仿。
}
}
Mover类:
class mover{
PVector location; //位置
PVector speed; //速度
PVector acceleration; //加速度
float mass; //质量越大,半径越大,最大速度越小
float topspeed; //最大速度
float radius; //半径
color col; //色彩
public mover(float tt){ //构造函数
location=new PVector(random(0,width),random(0,height)); //随机位置
speed=new PVector(0,0); //无速度和加速度
acceleration=new PVector(0,0);
mass=random(1,5); //随机质量
topspeed=42.5/(mass); //根据质量算最大速度和半径
radius=mass*8;
float r,g,b; //泊林色彩
r=noise(tt);
g=noise(tt+300);
b=noise(tt+900);
r=map(r,0,1,100,255);
g=map(g,0,1,100,255);
b=map(b,0,1,100,255);
col=color(r,g,b);
}
void update(PVector windForce, boolean isgravity, boolean isfAir){
//传入风力、重力、空气阻力,自身永远受到向鼠标的牵引力
PVector Yin=new PVector(mouseX,mouseY); //求牵引力
Yin.sub(location); //从小球指向鼠标
float dist=Yin.mag(); //到鼠标的距离
Yin.normalize(); //引力的方向,规格化即可
float yinForce=mass*20/(dist); //引力大小,质量乘积并除以距离平方。
//但是距离平方效果太差,故改为只除一个距离
Yin.mult(yinForce); //数乘大小,得到引力向量
//算重力和空气阻力
PVector gravity,fAir;
gravity=new PVector(0,0);
fAir=new PVector(0,0);
if(isgravity){ //启动重力
//重力竖直向下,方向为Y轴正方向,大小为mg,g取9.8
gravity.set(0,1);
float gf=mass*9.8*0.003; //为了更好的效果,乘以一个系数0.003
gravity.mult(gf);
}
if(isfAir){
//阻力方向等于速度方向的相反方向。大小为系数*速度平方
fAir.set(speed);
float af=speed.mag(); //速度的大小
fAir.normalize();
af=af*af; //平方
af=af*0.015; //乘系数,得到大小
fAir.mult(-af); //数乘,必须是负的
}
PVector Forces=new PVector(0,0); //合力
Forces.set(Yin);
Forces.add(windForce);
Forces.add(gravity);
Forces.add(fAir); //合力
float a=Forces.mag()/mass; //加速度大小,除以质量,方向即合力方向
acceleration.set(Forces.normalize().mult(a));
//移动
speed.add(acceleration); //速度改变
speed.limit(topspeed); //速度限制
location.add(speed); //位置改变
}
void Bounds(){ //防止飞出边界太远
if(location.x<-100)location.x=-100;
if(location.y<-100)location.y=-100;
if(location.x>width+100)location.x=width+100;
if(location.y>height+100)location.y=height+100;
}
void display(){ //画球
noStroke();
//strokeWeight(1);
fill(col,85);
ellipse(location.x,location.y,radius,radius);
}
}
基本思路倒过来推比较容易理解:
画出小球
——需要得到小球的位置
——位置由速度改变,需要得到小球的速度
——速度由加速度改变,需要得到小球的加速度
——加速度由小球那个时刻受到的合力改变,需要得到小球的合力
——合力矢量由分力矢量相加得到,需要得到小球的分力
——分力矢量有大小和方向,这就是我们需要模拟的东西,剩下的交给小球自己就好。
看起来很麻烦,但实际上,我们只需要制定一套规则,除了分力之外,所有变量都会自行变化。规则可以自己随意定,比如让小球永远向速度反方向移动,等等。而牛顿制定的规则如下(以下无特殊说明,均为矢量运算):
位置=上一帧位置+速度;
速度=上一帧速度+加速度;
加速度=合力/质量(标量);
合力=分力累加。
在mover类中,行67、行69就做了位置上的运算。行57~61在计算合力,行63~64在计算加速度,其余要么在控制小球的性状(防止越出边界或者画出小球),要么就是在按照一定规则计算分力。
所有需要用到“上一帧”的变量都必须存储,这里是位置和速度。当然有算法可以只存储一个,那是比较高级的能发论文的算法。这些量在计算时总是调用add方法,即累加。而其它量只需要set,即直接赋值即可。
剩下的就只要指定分力即可。这里安置了重力、风力、空气阻力和引力这四个力。风力是与小球本身的运动情况和性状无关的,所以没有放在mover类中,其它力均与小球自身有关——重力只与质量有关,空气阻力与速度有关,引力与距离和质量有关。
根据力学知识,确定好小球受到的这些力的方向和大小,记作向量,就可以了。行29~55就在做这个工作。
为了最后表现效果比较好看,我微调了几个力的运算规则,所以可能不够真实,但是会有受到那个力的趋势。
第一部分:3~4章的针对习作
3~4章的主要内容分别为:旋转、三角函数、振荡、弹力和极坐标系相关知识,以及粒子系统。
第3章针对习作:三角函数构成的粒子流动
改编自以前的程序。
会有许多粒子围绕鼠标位置,按一定规则形成各种波形,并“流动”。视觉效果很好。
一些操作方法及功能:
S——改变波形的规则,使其轨道变为椭圆,再次点击则换回振荡波形。
T——增加对形状影响重大的参数TT,每次加0.5。
G——减少参数TT,每次减0.5。
滚轮——上下滚动,调整图形大小。
单击——改变插值规则,立刻显示当前插值的目标点形成的形状。
初始时,重要形状参数TT为10,单击后可见其完整波形如图:
代码中使用lerp函数,向这个轨迹插值,每次插值0.5%,由于角速度变化的不同,形成了上面那张图的效果。单击后即每次插值100%,直接移动到这个轨迹上,形成确定的图案。
按下S,改变方程中TT的运算规则,变为这个轨迹是一个椭圆:
这是鼠标移动时的结果。进行插值,会形成类似星云的效果:
改变TT,得到不同的图案:
代码文本如下:
int num = 400; //点的数量
float mts = PI/24; //最大角速度,影响每个点运动的快慢
int r = 400; //外围圆半径
int rdu = 4; //每个小圆的半径
float TT=10; //形状参数
PVector v[]=new PVector[num]; //位置,而非速度
boolean mv = true; //显示函数形状,把插值百分比变成1即可
boolean mo = true; //更改模式,椭圆为+-,曲线为*/
color c[] = new color[num]; //每个点的颜色
float mtheta[] = new float[num]; //弧度
float dtheta[] = new float[num]; //每个点走的快慢
float easing[] = new float[num]; //插值百分比。都一样的话,结果会很整齐,否则会有点杂乱。
int rdt[] = new int[num]; //偏离半径的大小
void setup() {
colorMode(RGB, 255, 255, 255);
float r, g, b;
float t=159;
size(800, 1000);
for (int i =0; i<num; i++) {
r=noise(t);
g=noise(t+300);
b=noise(t+900);
r=map(r, 0, 1, 100, 255);
g=map(g, 0, 1, 100, 255);
b=map(b, 0, 1, 100, 255);
c[i] = color(r, g, b);
v[i] = new PVector(random(width), random(height));
dtheta[i] = random(mts);
mtheta[i] = round(0)/180*PI; //初始角度转弧度
rdt[i] = 0; //不偏离轨道,得到一条整齐的细线
easing[i] =0.005;
t+=0.05;
}
}
void draw() {
fill(25, 25, 25, 5);
rect(0, 0, width, height);
pushMatrix();
noStroke();
if (mo) { //周期变化模式
for (int i = 0; i<num; i++) {
mtheta[i] += dtheta[i];
v[i].lerp(mouseX+cos(mtheta[i])*(rdt[i]+r), mouseY+cos(mtheta[i]/TT)*(rdt[i]+r), 0, easing[i]); // /为竖着的,*为横着的,且*的时候变化过于剧烈
//v[i].set(mouseX+cos(mtheta[i])*(rdt[i]+r), mouseY+sin(mtheta[i]/TT)*(rdt[i]+r));
fill(c[i], 70);
ellipse(v[i].x, v[i].y, rdu, rdu);
}
}
if (!mo) { //椭圆轨道模式
for (int i = 0; i<num; i++) {
mtheta[i] += dtheta[i];
v[i].lerp(mouseX+cos(mtheta[i])*(rdt[i]+r), mouseY+sin(mtheta[i]+TT)*(rdt[i]+r), 0, easing[i]); //椭圆轨道为基,TT=0的时候是圆
fill(c[i], 70);
ellipse(v[i].x, v[i].y, rdu, rdu);
}
}
popMatrix();
}
void mousePressed() {
if (mv) {
for (int i=0; i<num; i++) {
easing[i]=1;
}
mv=!mv;
} else {
for (int i=0; i<num; i++) {
easing[i]= 0.005;
}
mv=!mv;
}
}
void keyPressed() {
if (key == 's'||key == 'S') {
mo =!mo;
}
if (key=='t'||key=='T') {
TT+=0.5;
}
if (key=='g'||key=='G') {
TT-=0.5;
}
}
void mouseWheel(MouseEvent event) {
float e = event.getCount();
if (e == -1) r+=10;
if (e == 1) r-=10;
}
核心思想即:修改参数方程,对其插值。每个粒子给定一个角度和角度增量,带入方程,即可产生轨迹。又由于插值的关系,使得粒子运动不到轨道上,即会向下一个点插值,从而改变方向,就像被轨迹牵着走一样。
其中的关键函数“Lerp”,为插值函数。前三个参数为目标点,这里我们的目标点就是方程确定的轨迹点(三维。将第三个值z轴设为0)。最后一个参数为百分比,表示当前这个向量要均匀地向目标点插值百分之多少。它似乎会自动每帧返回到当前向量中。
对于参数方程:
经过实验总结,它的形状:
1、当x和y的三角函数周期相同,相位相同(括号里东西一模一样)时——圆
2、当x和y的周期不同时(theta进行乘除)——不同的波形
3、当x和y的周期相同,相位不同(theta进行加减)时——不同的椭圆
4、当x和y的三角函数名一样(都为sin或cos)时——遵从第2、3条,但形状不同(椭圆方向、扁圆之类的);若周期相同,相位相同,则为一条直线段
5、当x和y的三角函数名颠倒时——x和y轴颠倒
6、当三角函数中有负值时——与正值时的环绕顺序相反(顺时针变逆时针)
暂时就总结了这么多。
第4章针对习作:简单的粒子系统
其实现过程基本糅合了前四章的内容。没有什么新东西,只是用了粒子管理器这个类去管理粒子,而主程序只需要管理粒子管理器。
鼠标单击之后就会生成一个粒子管理器,从鼠标位置向四周发射200个粒子。
粒子会缓慢消失。
使用键盘来施加各种效果:
T——弹簧,将每个粒子都与鼠标用弹簧相连。粒子会被吸引。
G——施加重力。
A——施加空气阻力。
Y——施加引力。
W——开启振荡。它会像上一个程序一样,将位置直接向轨迹插值。
Q、E——加减轨迹的参数TT以改变形状。
滚轮——改变轨迹的大小。只在W开启时有效。
由于有初速度,仅仅插值轨迹是无法将粒子全部拉到轨迹上的。
所以需要使躁动的粒子安静下来,施加空气阻力。在空气阻力的作用下,粒子的初速度渐渐被抹平。而位置插值不改变速度,所以即使看到粒子在高速运动,其“速度”变量的大小也是接近0的。此时就会明显地被拉到轨道上。
添加弹簧力后会压缩波形,把距离较远的点强行拉回;而引力做不到,引力对于远点的影响太小了:
单弹簧力的效果。粒子快速、大范围的绕鼠标做椭圆运动。
加上空气阻力,则快速收敛到点:
加上重力,粒子被拉成长条,并宛如真的弹簧一样抖动:
加上引力,则由于越近,引力越大,粒子被加速抛出,再由弹簧拉回,不断反复,形成类似波动的运动效果,视觉上如同烟花:
加上重力,则在头部有小“烟花”:
代码文本较多,很多都和前面的相同。
粒子类中,使用了一个与求分力再求合力等效的算法:求分加速度再求合加速度。这样的好处是,重力完全不需要考虑,只需要将加速度加上竖直向下的9.8即可。
粒子渐渐消失,是由透明度得到的效果。当粒子本身的透明度为0时,其死亡。同时,被粒子管理器从队列移除。当例子管理器中的粒子队列里没有任何粒子时,它也会被主程序从管理器队列中移除,以此保证长时间运行时的帧率。
粒子类与前面的mover类类似,不再给出文本。
下面给出粒子管理器类:
class LizManager {
ArrayList<Liz> lizi; //粒子队列
public LizManager() {
lizi=new ArrayList<Liz>();
float t=random(0, 100); //泊林噪声初值
for (int i=0; i<num; i++) {
lizi.add(new Liz(t));
t+=0.007;
lizi.get(i).display();
}
}
void run() { //运行
for (int i=0; i<lizi.size(); i++) {
Liz p=lizi.get(i);
p.clearAc(); //每个粒子,清除加速度
if (tanhuang) {
p.tanhuangForce(); //计算弹簧力造成的分加速度并累加
}
if (isgravity) {
p.useGravity(); //计算重力加速度并累加
}
if (isYinli) {
p.useYinli(); //计算引力加速度并累加
}
if (wave) {
p.useWave(); //进行振荡位置插值
}
p.update(); //更新位置。关于空气阻力的判断放在了粒子类中
p.Bounds(); //判断是否越界
p.display(); //显示
if (p.isDead()) { //粒子是否死亡
lizi.remove(i); //死亡,粒子被移除
i--; //!!!!!移除后下标需要减一
}
}
}
}
由于队列移除时,会自动填补空位,所以需要把索引后移一位,这样循环时自加,就不会漏掉队列中任何一个元素。
主程序的draw函数:
void draw(){
fill(0,5);
rect(0,0,width,height);
if(lizM.size()>0){
for(int i=0;i<lizM.size();i++){
lizM.get(i).run();
if(lizM.get(i).lizi.size()<=0){ //这个粒子系统已经没有活粒子了,移除
lizM.remove(i);
i--;
}
}
}
}
非常简洁。