Processing 案例 | 扑面而来的满天繁星
文章目录
- 引言
- 效果展现
- 原理分析
- 代码实现
- 确定代码执行流程
- 星星绘制在屏幕上
- 让星星动起来
- 点击鼠标,星星的速度发生改变
- 视角改变
- 完整代码
- 结语
引言
清明时节雨纷纷,路上行人欲断魂”,每到清明,你总能从那如丝如雨缕的春雨绵绵中感到心上缭绕的那点忧愁。
很少有这样一个节日,像清明这样的矛盾。当你放歌踏青时,它是一个轻盈的日子,是节气;当你慎终追远的时候,它是一个悲怆的日子,是节日。
在每一个清明冰凉的夜,你是否也曾仰望满天的繁星,追忆那已故的人?他们就像那满天的繁星,在你生命的漫漫长夜中匆匆划过,照亮了一段时光。
现在就让我们一起用 Processing 来模拟那片星域,让记忆中的人和事翻上心头,细细评味。
效果展现
原理分析
首先通过观看视频,我们可以发现在鼠标所在的点附近的星星是最小的,离该点越远,星星的面积越大,移动的速度越快。就好像星星都从这个点里出来一样,我们暂且把这个点叫做消失点。并且观察可得,消失点把屏幕分成了四个区域A,B,C,D(如图一所示),同时把星星也分成了四类。其中A中的星星向左上方移动,B中点向右上方移动,C中的点向左下方移动,D中的点向右下方移动。
接下来我们来看一下代码实现的大概思路。
我们在脑海里假想一个长方体,以这个长方体的一个顶点为原点,建立世界坐标系。然后选取一个面作为Processing 程序的窗口,建立屏幕坐标系。然后在屏幕上任选一个点,作为消失点,该点在屏幕坐标系下的坐标为(x_endpoint,y_endpoint )。
接着在这个长方体里随机生成一系列的星星,每个星星由一个三维向量(x_world,y_world,z_world)表示,这是它在世界坐标系下的坐标。
接下来我们要做的就是把每一个星星的在世界坐标系的坐标,转换成在processing窗口里面的坐标(x_screen,y_screen),这样我们就可以在屏幕上把星星绘制出来。
首先需要计算星星在世界坐标系下相对于消失点(endpoint)x,y方向的偏移量,接下来将这个偏移量根据星星的z_world进行放缩,最后再将放缩完成的偏移量加回消失点的坐标,得到星星在屏幕上的坐标。
公式如下:
x s c r e e n = ( x w o r l d − x e n d p o i n t ) / z w o r l d ∗ s c a l e + x e n d p o i n t x_{screen}=(x_{world}-x_{endpoint})/z_{world} *scale+x_{endpoint} xscreen=(xworld−xendpoint)/zworld∗scale+xendpoint
y s c r e e n = ( y w o r l d − y e n d p o i n t ) / z w o r l d ∗ s c a l e + y e n d p o i n t y_{screen}=(y_{world}-y_{endpoint})/z_{world} *scale+y_{endpoint} yscreen=(yworld−yendpoint)/zworld∗scale+yendpoint
其中scale是放缩比例,用来控制控制星域的范围。
分析公式可以发现z_world越大对应的 x_screen、y_screen 越大,也就是说离消失点越远。也就说我们只要将z_world 初始设为一个比较大的值,然后在不断减小它,这样就会出现星星离消失点越来越远,离屏幕越来越近的效果。同时我们根据 z_world 的大小设置星星的直径 diam 设置,z_world 越大,diam 越小,这样就符合近大远小的透视规律。
代码实现
首先我们来确定代码的结构,创建三个 Processing 文件,分别为 main.pde、StarField.pde、Star.pde。其中main.pde 里面是程序执行的主流程。StarField.pde 里面主要定义了 StarField类、Star.pde 主要定义了 Star 类。
确定代码执行流程
在 main.pde 中输入如下代码:
/*这个是一个由星星构成的粒子系统 拥有星星的所有状态和行为*/ StarField sf; void setup(){ size(400, 400); sf = new StarField(); } void draw(){ background(0); /*运行星域系统 更新星星的状态和绘制星星*/ sf.run(); } /*鼠标按下时调用的函数 用来改变星域的速度*/ void mousePressed(){ } /*鼠标移动的时候调用的函数 用来改变视角*/ void mouseMoved(){ }
星星绘制在屏幕上
首先在StarField.pde中输入如下代码:
以上代码我们确定了该类的四大组成部分,接下来我们在将构造函数替换为如下代码:
StarField(){ /*将初始消失点设置在鼠标最开始的位置*/ endpoint = new PVector(mouseX, mouseY); /*初始化所有的星星*/ stars = new ArrayList(); for(int i = 0; i < STAR_COUNT; i++){ stars.add(new Star()); } }接着在StarField类的成员函数中定义run函数:
void run(){ for(Star s : stars){ /*对星星进行坐标变换 获得星星在屏幕坐标系的坐标 用于之后的星星的渲染*/ s.transform(endpoint); /*对屏幕外的星星进行裁剪 同时生成一个新的星星 使得星星可以源源不断的出现*/ s.checkEdge(); /*依据屏幕坐标系渲染星星, 使得星星在屏幕上出现*/ s.display(); } }接下来我们来定义出现在 StarField 的构造函数中和 run 函数中的变量和常量。
/*该粒子系统包含的星星的个数 星星越多画面越密*/ final int STAR_COUNT = width / 2; /*数据类型为Star object的动态数组 存储该星域中所有的星星*/ ArrayList<Star> stars; /*消失点 用来控制视角*/ PVector endpoint;之后我们来定义Star对象,输入如下代码:
class Star{ //常量声明 //变量声明 //构造函数 //成员函数 }将 Star 类的构造函数替换为:
Star(){ /*在一个长方体区域内随机生成一个点 返回它在世界坐标系下的坐标*/ worldPosition = new PVector(random(0, width), random(0, height), random(0, MAX_DEPTH)); }接下来在Star类成员函数部分加入 transfrom 函数,这个函数是本作品最关键的地方,希望大家能好好体会一下。
void transform(PVector endpoint){ /*将星星的坐标从世界世界坐标系变换到消失点坐标系 以下代码等同于: viewPosition.x = (worldPosition.x - endpoint.x) / worldPosition.z * SCALE; viewPosition.y = (worldPosition.y - endpoint.y) / worldPosition.z * SCALE; */ viewPosition = PVector.sub(worldPosition, endpoint).div(worldPosition.z).mult(SCALE); /*将星星的坐标从消失点坐标系变换到屏幕坐标系 以下代码等同与: screenPosition.x = endpoint.x + viewPosition.x; screenPosition.y = endpoint.y + viewPosition.y; */ screenPosition = PVector.add(endpoint, viewPosition); /*根据世界坐标z的大小来确定星星的直径 z越小,说明越靠近屏幕,所以星星越大*/ diam = map(worldPosition.z, 0, MAX_DEPTH, MAX_DIAM, 0); }接着在Star类的成员函数部分加入checkEdge函数的定义
void checkEdge(){ if(screenPosition.x <= 0 || screenPosition.x >= width || screenPosition.y <=0 || screenPosition.y >= height){ /*如果这个点已经在屏幕外了,那么将其裁剪, 同时在相同的长方体区域随机生成一个新的点*/ worldPosition.set(random(0, width), random(0, height), MAX_DEPTH); } }然后继续在Star类的成员函数部分Star类的display函数。
void display(){ /*在屏幕上绘制该星星 每一个星星是一个白色的、没有边的圆*/ fill(255); noStroke(); ellipse(screenPosition.x, screenPosition.y, diam, diam); }在第二步的最后我们把Star类需要的一些成员变量和常量加上,把它们添加到Star类的常量和变量部分。
/*星星在屏幕坐标系直径的最大值 用于控制星星在屏幕上的整体大小*/ final float MAX_DIAM = 16; /*星星里屏幕最远的距离 用来控制星域的立体感*/ final float MAX_DEPTH = width / 2; /*进行坐标变换时候的 用来控制星星在屏幕上的分布范围*/ final float SCALE = MAX_DEPTH; /*星星在三个坐标系下的坐标 记录它们在不同参考系下的位置*/ PVector worldPosition, screenPosition, viewPosition; /*星星在屏幕坐标系下的直径 确定星星在屏幕上的大小*/ float diam;
让星星动起来
首先我们在 StarField 的 run 方法中,给每一个星星添加如下的行为:
接着在StarField的构造方法中初始化 speed 这个值。
/*初始化星星的移动速度 让移动速度不用太快和太慢*/ speed = (MAX_SPEED + MIN_SPEED) / 2;然后在 StarField 的变量和常量部分,定义新增加和星星速度有关的常量和变量。
/*分别代表星星移动的最大速度,最小速度, 用来控制星星移动速度的范围*/ final int MAX_SPEED = 11, MIN_SPEED = 1; /*速度改变的步长 每一次增加或者减小速度的时速度改变的最小值*/ final int SPEED_STEP = 1; /*星星移动的速度 控制星星移动快慢*/ int speed;这之后我们在 Star 的成员函数部分加入如下代码,然后第三阶段就到此结束。
void move(float speed){ worldPosition.z -= speed; /*限制星星世界坐标系的位置 防止出现星星移动到屏幕外的情况*/ worldPosition.z = constrain(worldPosition.z, 0, MAX_DEPTH); }
点击鼠标,星星的速度发生改变
直接在 main.pde 的 mousePressed 中加入如下代码:
然后在 StarField 的成员函数中定义 speedUP 和 speedDown 函数:
void speedUP(){ speed += SPEED_STEP; /*限制speed在最大和最小值之间 防止速度太快*/ speed = constrain(speed, MIN_SPEED, MAX_SPEED); } void speedDown(){ speed -= SPEED_STEP; /*限制speed在最大和最小值之间 防止速度小于零*/ speed = constrain(speed, MIN_SPEED, MAX_SPEED); }&enmsp;
视角改变
首先在 mouseMoved 函数里面加入如下代码:
到了现在我们就完成了代码的所有编写。
完整代码
我的github
我的openprocessing
结语
我们有很多可以扩展的地方,比如改变 Star 类里的 MAX_DEPTH 来改变星域的立体感,改变 StarField 类的 STAR_COUNT,Star 类 SCALE 的来改变星域的密度和范围。
目前为止对于每一星星我们只是简单的画了一个白色的小圆,其实这里有很大的发挥空间。比如用 Noise 函数根据星星在屏幕坐标系的位置计算星星的颜色,或者用你思念的人的名字或者其中的字母来替代圆圈,创造出那一片独一无二,只属于你的灿烂的星域!
最后,祝大家清明节快乐!
总结
以上是生活随笔为你收集整理的Processing 案例 | 扑面而来的满天繁星的全部内容,希望文章能够帮你解决所遇到的问题。
- 上一篇: 微软Win10彻底封杀exFAT/FAT
- 下一篇: 最全的 JVM 面试知识点(二):垃圾收