随机行为以及运动学
本学期参考《代码本色》,为了实现模拟一个生态系统的目标进行了循序渐进的学习。主要依照书本前五章的内容,按照书中提示的思路,拓展了书中的程序。
随机
多数仅仅涉及到数据计算以及逻辑问题的编程情况下,使用的随机数大多数是伪随机数。这类随机数的一个明显特点便是生成的数据不够平滑。在计算机图形学中,以及游戏设计领域,产生平滑的噪声是一个非常重要的课题。诸如游戏中的地形生成、水面生成,都需要用到平滑的随机数——Perlin噪声。
针对本章中利用Perlin噪声生成云以及生成树木的例子。根据分形艺术理论,编程实现了二维视角下的蒲公英和三维视角下的树木。
两种植物均采用了Perlin噪声来生成颜色,形成渐变而且不冲突的色彩。另外,对于树木,还采用了Perlin噪声来生成三个方向上不同角度的分支。生成颜色参考的是书中生成云彩的例子,即:
代码如下:
stroke(map(noise(r+=0.1),0,1,100,255),map(noise(r+=0.1),0,1,0,255),map(noise(r+=0.1),0,1,0,255));
两种植物均采用了分形艺术的手法,整体思路如下:
实现旋转以及平移时,针对坐标进行操作,并没有采用Processing自带的函数。而是依照计算机图形学的相关知识,自定义了方法:
void MultiMatrix(float[][] T)
{
float[][] pNew=new float[2][3];
for(int i=0;i<2;i++)
{
for(int j=0;j<3;j++)
{
pNew[i][j]=pOld[i][j];
}
}
for(int i=0;i<2;i++)
{
pOld[i][0]=pNew[i][0]*T[0][0]+pNew[i][1]*T[1][0]+pNew[i][2]*T[2][0];
pOld[i][1]=pNew[i][0]*T[0][1]+pNew[i][1]*T[1][1]+pNew[i][2]*T[2][1];
pOld[i][2]=1;
}
}
void Translate(float dx,float dy)
{
float[][] T=new float[3][2];
T[0][0] = 1; T[0][1] = 0;
T[1][0] = 0; T[1][1] = 1;
T[2][0] = dx; T[2][1] = dy;
MultiMatrix(T);
}
void Rotate(float thta)
{
float[][] T=new float[3][2];
T[0][0] = cos(thta*PI / 180); ; T[0][1] = sin(thta*PI / 180);
T[1][0] = -sin(thta*PI / 180); T[1][1] = cos(thta*PI / 180);
T[2][0] = 0; T[2][1] = 0;
MultiMatrix(T);
}
上述三个函数的作用分别是矩阵相乘、平移、旋转。有关计算机图形学的知识与本次的实验主旨相关性不大,不再赘述,有兴趣的读者可以翻阅相关资料。
上面提到了,依照向量的方式获得每一个新的分支的两个端点,图解如下:
上图中,P1、P2为需要生长的分支的两个端点。P1、P3(未知)是生长出的分支的两个端点。可以看到,新分支和老分支共用一个端点。而且在进行图形的平移旋转之前,两个分支是在一条直线上的。因此,如果知道了新分支和老分支的长度比alpha,就可以利用向量计算出P3,P3=P1+alpha*(P1-P2)。
float dx=L[1][0]-L[0][0];
float dy=L[1][1]-L[0][1];
pOld[0][0]=L[1][0];
pOld[0][1]=L[1][1];
pOld[0][2]=1;
pOld[1][0]=L[1][0]+dx/2;
pOld[1][1]=L[1][1]+dy/2;
为了能够向存储树的分支结点的数组中追加新的数据元素,采用了Processing的FloatList数据结构,计算出新的数据之后,利用其方法append向表中追加新的元素。
另外,编程过程中容易犯错的一点是,进行生长操作的每一个分支都必须是新产生的,所以需要记录追加进来的分支的索引:
void draw()
{
camera(eyex++, eyey++, eyez++, 320.0, 120.0, 0.0,
0.0, 1.0, 0.0);
Iteration++;
background(255,255,255);
stroke(0,0,0);
for(int i=0;i<treex.size();i+=2)
{
stroke(map(noise(r+=0.1),0,1,100,255),map(noise(r+=0.1),0,1,0,255),map(noise(r+=0.1),0,1,0,255));
line(treex.get(i),treey.get(i),treex.get(i+1),treey.get(i+1));
}
if(Iteration>4)
{
return;
}
int count=treex.size();
for(int i=0;i<branch;i++)
{
ext[0][0]=treex.get(count-1-2*i);ext[0][1]=treey.get(count-1-2*i);ext[0][2]=1;
ext[1][0]=treex.get(count-2-2*i);ext[1][1]=treey.get(count-2-2*i);ext[1][2]=1;
for(int j=-bm;j<=bm;j+=bm_step)
{
Extend(ext,j);
}
}
branch*=2*bm/bm_step+1;//记录加入了多少新的分支
}
可以看到,代码中指定的迭代次数为4次。因为按照程序中的约定,要对每一个分支生长出12个小分支。因此,迭代四次之后,分支总数就已经达到了1728个分支。继续计算数量级将会增加,绘制的效率会大大减小。因此需要约束,即迭代次数达到4次之后,就不再生长,只进行绘制。
向量
在第一个拓展实验中,实际上已经使用到了向量的有关知识,但是实际上是依照这数学原理进行的计算,Processing已经提供了PVector类,用于简单便捷地实现对向量的有关操作。
实验进行前,已经将书上的示例全部尝试了一边,从头到尾体会到了向量和编程艺术以及自然系统的紧密关系。结合向量的有关知识。编写了一个朝向鼠标移动,并且大小随着速度变化的简化池塘鱼群生态系统。
参考书中的Mover类以及代码1-11中物体朝向鼠标移动的函数,增加了鱼群的在运动的同时,围绕自身群体做圆周运动以及大小随着速度而变化的创新点。
主要函数和书中代码类似,叙述一下几个更改的地方:
(1)单条鱼的加速度:
为了实现鱼群朝向鼠标移动的效果,需要使得鱼群的加速度朝向鼠标方向。但是实际生活中,会存在少量鱼的游动方向始终相反,因此,需要参考第一章的程序,以一定的概率来决定某条鱼的游动方向。
void update(){
PVector mouse=new PVector(mouseX,mouseY);
PVector dir=PVector.sub(mouse,location);
dir.normalize();
dir.mult(0.5);
acceleration=dir;
if(random(1)>0.05){
acceleration.mult(random(1));
}
else{
acceleration.mult(random(-1,0));
}
velocity.add(acceleration);
velocity.limit(10);
location.add(velocity);
head.add(velocity);
}
(2)种群的集体环状游动方向:
为了实现种群的集体环状游动方向,采用的方法是让每一条鱼都绕原点旋转一定的角度,而且这个旋转不会被推送。作用到整个种群上。这样会产生意想不到的效果。
void display(float angle){
stroke(0);
fill(0,100,50);
rotate(angle);
ellipse(location.x,location.y,velocity.x,velocity.x);
}
力
力与向量的关系比较相近,很多时候是在向量的角度上多考虑的质量的情况,而且在编程世界中,物理量不必严格遵守现实世界中的规范,可以有很多自己的发挥。
按照书中重力、流体阻力、摩擦力、引力的案例,给第二个拓展实验中的每一一条鱼增加了随即质量,并让其受到重力、摩擦力、流体阻力的作用,还在个体之间添加了相互之间的斥力。
当移动到液体区域中时,鱼的游动速度减慢。
添加的各种作用力:‘
for(int i=0;i<movers.length;i++){
PVector friction=movers[i].velocity;
friction.mult(-1);
friction.normalize();
friction.mult(c);
float m=movers[i].mass;
PVector gravity=new PVector(0,1.5*m);
if(movers[i].isInside(liquid)){
movers[i].drag(liquid);
}
movers[i].applyForce(gravity);
movers[i].applyForce(friction);
movers[i].update();
movers[i].display();
movers[i].checkEdges();
}
添加流体阻力。流体类如下
class Liquid{
float x,y,w,h;
float c;
Liquid(float x,float y,float w,float h,float c){
this.x=x;
this.y=y;
this.w=w;
this.h=h;
this.c=c;
}
void display(){
noStroke();
fill(175,175,175,100);
rect(x,y,w,h);
}
}
对于每一条鱼,都要添加方法,实现流体作用力。并在主函数中调用。
void drag(Liquid l){
float speed=velocity.mag();
float drayMagnitude=l.c*speed*speed;
PVector drag=velocity;
drag.mult(-1);
drag.normalize();
drag.mult(drayMagnitude);
applyForce(drag);//应用力
}
振荡
提到振荡,首先想到的便是三角函数,三角函数反应现实世界,泰勒展开、欧拉公式可以将复杂的函数转化为三角函数。实验开始前尝试了书中的每一个代码。受到启发想到DNA的双螺旋结构,结合OOP思想,编写了一个DNA类,实现绘制DNA的效果。
代码思路是结合两个正弦波运动,在对应的位置,每隔一段步长,对应连线,DNA类包含三个更新方法,分别是两条正弦波的更新绘制和中间连线的绘制:
public void update1(){
//整体的正弦波运动
float angle=startAngle1;
float angleStep=0.2;
float x=0;
float xStep=10;
for(int i=0;i<section;i++){
//计算y
float y=amplitude*sin(0.8*angle);
//赋值
location1[i]=new PVector(x,y);
location1[i].add(Loc);
//增加角度与x
angle+=angleStep;
x+=xStep;
startAngle1+=0.0001;
}
}
public void update2(){
//整体的正弦波运动
float angle=startAngle2;
float angleStep=0.2;
float x=0;
float xStep=10;
for(int i=0;i<section;i++){
//计算y
float y=amplitude*sin(0.8*angle);
//赋值
location2[i]=new PVector(x,y);
location2[i].add(Loc);
//增加角度与x
angle+=angleStep;
x+=xStep;
startAngle2+=0.0001;
}
}
void update3(){
push();
translate(Loc.x,Loc.y);
rotate(rotation);
scale(size);
strokeWeight(10);
float x1,x2,y1,y2;
for(int i=0;i<section;i+=4){
x1=location1[i].x;
x2=location2[i].x;
y1=location1[i].y;
y2=location2[i].y;
line(x1,y1,x2,y2);
}
pop();
}
粒子系统
在本学期的Unity使用中,多次接触到了粒子系统,粒子系统是一个类的嵌套调用,包含了内部的单个粒子以及整体的粒子系统类。粒子系统广泛应用于模拟各种特效,包括烟雾、火焰等等,本次实验主要是针对前几章的经典物理学,对粒子系统添加了粒子之间的排斥力、粒子与鼠标之间的吸引力。实现了可交互式的粒子系统:
主要改进的地方有两个:
(1)参照牛顿运动定律,使得任意两个粒子之间都存在斥力:
Particle类:
PVector attract(Particle p){
float G=0.4;
PVector force=PVector.sub(location,p.location);
float distance=force.mag();
distance=constrain(distance,1,10);
force.normalize();
float strength=(G)/(distance*distance);
force.mult(strength);
return force;
}
ParticleSystem类:
void repulsion(){
for(int i=0;i<particles.size();i++){
for(int j=0;j<particles.size();j++){
if(i!=j){
PVector f=particles.get(j).attract(particles.get(i));
f.mult(5);
particles.get(j).applyForce(f);
}
}
}
}
(2)鼠标吸引所有的粒子,而且粒子的质量极小,加速度很大,容易冲过鼠标的位置,然后返回,中途不受任何阻力,形成回旋效果:
如下图所示,左侧粒子为初始方向,粒子被鼠标吸引,超鼠标飞去,由于加速度过大,而且不受阻力,不会停留在中间的理想位置,而是冲到右侧的圆形位置,然后反向冲回……循环往复,形成回旋效果。
PVector repel(Particle p) {
float x=mouseX;
float y=mouseY;
PVector mouseLocation=new PVector(x,y);
PVector dir =PVector.sub(mouseLocation, p.location);
float d = dir.mag();
d = constrain(d, 5, 100);
dir.normalize();
dir.mult(5);
return dir;
}
来源:CSDN
作者:AndreCrawley
链接:https://blog.csdn.net/AndreCrawley/article/details/103842037