processing互动编程习作——作业博文

第一部分:0~2章的针对习作

0~2章的主要内容分别为随机数生成、向量的使用和牛顿运动力学的简单模拟。

第0章针对习作:随机点线生成

其可以当作是灯光,或者是雨点,或者是你认为的任何东西。表现图如下:


程序0.gif

在屏幕上不断出现这样的指向四周的射线,再不断消失。此外,鼠标移动到最左端,色彩的变化就会比较缓慢,而移动到最右端,色彩将会比较快速地切换。但不论如何,色彩总是平滑变化的。
随机效果应用在:
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章针对习作:向量运算的简单可视化。

支持加、减、数乘和数量积。表现如下:


程序1.1,求和.gif
程序1.2,求差.gif
程序1.3,数乘.gif
程序1.4,内积.gif

分别是求和、求差、数乘和数量积。
求和——鼠标点击至少两个向量后,按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章针对习作:牛顿力学的简单使用

做了个类似于粒子的效果。所有小球都有随机的大小和质量,都受到鼠标的引力吸引,同时可以增加左右方向的水平风力,开启空气阻力和重力。


程序2.0,默认状态.gif

程序2.1,重力,很多球都掉落了,少量球被吸引.gif

程序2.2,空气阻力作用下,小球快速聚集。再不断加水平向右的风力把它吹散.gif

操作如下:
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章针对习作:三角函数构成的粒子流动

改编自以前的程序。
会有许多粒子围绕鼠标位置,按一定规则形成各种波形,并“流动”。视觉效果很好。


程序3.0.gif

一些操作方法及功能:
S——改变波形的规则,使其轨道变为椭圆,再次点击则换回振荡波形。
T——增加对形状影响重大的参数TT,每次加0.5。
G——减少参数TT,每次减0.5。
滚轮——上下滚动,调整图形大小。
单击——改变插值规则,立刻显示当前插值的目标点形成的形状。

初始时,重要形状参数TT为10,单击后可见其完整波形如图:


TT是10时的形状

其方程为:
x

y

代码中使用lerp函数,向这个轨迹插值,每次插值0.5%,由于角速度变化的不同,形成了上面那张图的效果。单击后即每次插值100%,直接移动到这个轨迹上,形成确定的图案。

按下S,改变方程中TT的运算规则,变为
相位变化

这个轨迹是一个椭圆:


椭圆

这是鼠标移动时的结果。进行插值,会形成类似星云的效果:
程序3.1.gif

改变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个粒子。


程序4.0.gif

粒子会缓慢消失。

使用键盘来施加各种效果:
T——弹簧,将每个粒子都与鼠标用弹簧相连。粒子会被吸引。
G——施加重力。
A——施加空气阻力。
Y——施加引力。
W——开启振荡。它会像上一个程序一样,将位置直接向轨迹插值。
Q、E——加减轨迹的参数TT以改变形状。
滚轮——改变轨迹的大小。只在W开启时有效。

由于有初速度,仅仅插值轨迹是无法将粒子全部拉到轨迹上的。


动图太大传不上来

所以需要使躁动的粒子安静下来,施加空气阻力。在空气阻力的作用下,粒子的初速度渐渐被抹平。而位置插值不改变速度,所以即使看到粒子在高速运动,其“速度”变量的大小也是接近0的。此时就会明显地被拉到轨道上。


image.png

添加弹簧力后会压缩波形,把距离较远的点强行拉回;而引力做不到,引力对于远点的影响太小了:
弹簧力拉扁的波形

单弹簧力的效果。粒子快速、大范围的绕鼠标做椭圆运动。



加上空气阻力,则快速收敛到点:

加上重力,粒子被拉成长条,并宛如真的弹簧一样抖动:

加上引力,则由于越近,引力越大,粒子被加速抛出,再由弹簧拉回,不断反复,形成类似波动的运动效果,视觉上如同烟花:

加上重力,则在头部有小“烟花”:

代码文本较多,很多都和前面的相同。

粒子类中,使用了一个与求分力再求合力等效的算法:求分加速度再求合加速度。这样的好处是,重力完全不需要考虑,只需要将加速度加上竖直向下的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--;
      }
    }
  }
}

非常简洁。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,293评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,604评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,958评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,729评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,719评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,630评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,000评论 3 397
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,665评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,909评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,646评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,726评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,400评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,986评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,959评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,996评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,481评论 2 342