程式碼檢查|如何用processing實現星際漫遊效果
黑洞 《星際穿越》 劇照
北京時間4月10日21點整,全球六地透過協調召開全球新聞釋出會,事件視界望遠鏡宣佈一項重大成果,與超大質量黑洞的照片有關。
俯察品類之盛,仰觀宇宙之大。總能給人們帶來愉悅,現在就讓我們一起用processing來模擬我們頭頂的那片星空吧!
效果展示
https://www。zhihu。com/video/1099381263122743296
一、準備工作
當大家學習完這篇文章時,會了解到 processing中Pvector(向量),ArrayList物件,constrain函式以及final關鍵字的基本用法。
如果對這幾個內容都很瞭解的同學可以直接跳過這部分。其他同學可以根據需要對這部分進行學習。
Pvector是一個描述二維或者三維向量的類,在processing作品中被廣泛用於描述物體position(位置),velocity(速度),acceleration(加速度),是模擬物體運動的效果的一把利器。下面是一些基本的用法。
*瞭解更多: https://processing。org/reference/PVector。html
/*成員變數*/
x //向量x方向的值
y //向量y方向的值
z //向量z方向的值
/*建構函式*/
PVector() // 預設情況下x = y = z =0
PVector(x, y, z)
PVector(x, y) //預設情況下z = 0
/*引數*/
x //float: x座標
y //float: y座標
z //float: z座標
/*成員函式*/
add() //一個向量到另一個向量或者兩個獨立向量的x,y,z相加
sub() //一個向量到另一個向量或者兩個獨立向量的x,y,z相減法
div() //向量的x,y,z都同時除以一個數
mult() //向量的x,y,z都同時乘一個數
ArrayList用一個來儲存可變數量物件的容器。和一個普通陣列很像,但是其具有方便增加和刪除元素,以及動態的改變陣列的大小的特點,這些使得PVector在processing作品中被廣泛用於儲存粒子系統。下面是一些基本的用法
*瞭解更多:
https://
processing。org/referenc
e/ArrayList。html
/*建構函式*/
ArrayList
ArrayList
/*引數*/
Type //Class Name: 放入ArrayList的資料型別或者物件
initialCapacity //int: 定義ArrayList的初始容量,初始為0
/*案例*/
ArrayList
particles。add(new Particle());// 新增一個元素
Particle part = particles。get(0);// 獲得一個元素
particles。remove(0) //刪除一個元素
int total = particles。size();// size()返回數組裡面的元素個數
// 兩種遍歷所有元素的辦法
for (int i = 0; i < particles。size(); i++) {
Particle part = particles。get(i);
part。display(); //呼叫Particle物件的display方法
}
for (Particle part : particles) {
part。display();
}
Constrain函式的作用是限制一個值不超過最大值和最小值,可以用於防止物體越界。下面是基本的用法。
*瞭解更多:
https://
processing。org/referenc
e/constrain_。html
/*語法*/
result = constrain(amt, low, high)
/*引數*/
amt //int, or float: 需要限制的值
low //int, or float: 下限
high //int, or float: 上限
/*返回值*/
result // float or int:如果amt超過最大值,result就等於最大值(最小值情況相同),否則返回原來的值
Final是一個用來宣告一個值,類或者變數是不能改變的,可以用來宣告一些常數。下面是基本用法。
*瞭解更多:
https://
processing。org/referenc
e/final。html
final float constant = 12。84753;
constant += 12。84; // 這樣會報錯,不能對其進行修改
二、原理分析
首先透過觀看影片,我們可以發現在滑鼠所在的點附近的星星是最小的,離該點越遠,星星的面積越大,移動的速度越快。就好像星星都從這個點裡出來一樣,我們暫且把這個點叫做消失點。並且觀察可得,消失點把螢幕分成了四個區域A,B,C,D(如圖一所示),同時把星星也分成了四類。其中A中的星星向左上方移動,B中點向右上方移動,C中的點向左下方移動,D中的點向右下方移動。
接下來我們來看一下程式碼實現的大概思路。
我們在腦海裡假想一個長方體,以這個長方體的一個頂點為原點,建立世界座標系。然後選取一個面作為processing程式的視窗,建立螢幕座標系。然後在螢幕上任選一個點,作為消失點,該點在螢幕座標系下的座標為:
接著在這個長方體裡隨機生成一系列的星星,每個星星由一個三維向量 表示,這是它在世界座標系下的座標。
接下來我們要做的就是把每一個星星的在世界座標系的座標,轉換成在processing窗口裡面的座標:
這樣我們就可以在螢幕上把星星繪製出來。
所以,首先需要計算星星在世界座標系下相對於消失點(endpoint) 方向的偏移量,接下來將這個偏移量根據星星的 進行放縮,最後再將放縮完成的偏移量加回消失點的座標,得到星星在螢幕上的座標。
公式如下:
其中 是放縮比例,用來控制控制星域的範圍。
分析公式可以發現 越大對應的 , 越大,也就是說離消失點越遠。也就說我們只要將 初始設為一個比較大的值,然後在不斷減小它,這樣就會出現星星離消失點越來越遠,離螢幕越來越近的效果。同時我們根據 的大小設定星星的直徑diam設定, 越大,diam越小,這樣就符合近大遠小的透視規律。
三、程式碼實現
首先我們來確定程式碼的結構,建立三個processing 檔案,分別為main。pde,StarField。pde,Star。pde。其中main。pde裡面是程式執行的主流程。StarField。pde裡面主要定義了StarField類,Star。pde主要定義了Star類。
第一步,我們主要確定程式執行的主要流程,在main。pde中輸入如下程式碼:
1。 /*這個是一個由星星構成的粒子系統
2。 擁有星星的所有狀態和行為*/
3。 StarField sf;
4。 void setup(){
5。 size(400, 400);
6。 sf = new StarField();
7。 }
8。
9。 void draw(){
10。 background(0);
11。 /*執行星域系統
12。 更新星星的狀態和繪製星星*/
13。 sf。run();
14。 }
15。
16。 /*滑鼠按下時呼叫的函式
17。 用來改變星域的速度*/
18。 void mousePressed(){
19。 }
20。
21。 /*滑鼠移動的時候呼叫的函式
22。 用來改變視角*/
23。 void mouseMoved(){
24。 }
第二步,我們的主要任務是將星星繪製在螢幕上,實現以下效果:
首先在StarField。pde中輸入如下程式碼:
1。 class StarField{
2。 //常量宣告
3。 //變數宣告
4。 //建構函式
5。 //成員函式
6。 }
以上程式碼我們確定了該類的四大組成部分,接下來我們在將建構函式替換為如下程式碼:
1。 StarField(){
2。
3。 /*將初始消失點設定在滑鼠最開始的位置*/
4。 endpoint = new PVector(mouseX, mouseY);
5。
6。 /*初始化所有的星星*/
7。 stars = new ArrayList();
8。 for(int i = 0; i < STAR_COUNT; i++){
9。 stars。add(new Star());
10。 }
11。
12。 }
接著在StarField類的成員函式中定義run函式:
1。 void run(){
2。 for(Star s : stars){
3。
4。 /*對星星進行座標變換
5。 獲得星星在螢幕座標系的座標
6。 用於之後的星星的渲染*/
7。 s。transform(endpoint);
8。
9。 /*對螢幕外的星星進行裁剪
10。 同時生成一個新的星星
11。 使得星星可以源源不斷的出現*/
12。 s。checkEdge();
13。
14。 /*依據螢幕座標系渲染星星,
15。 使得星星在螢幕上出現*/
16。 s。display();
17。 }
18。 }
接下來我們來定義出現在StarField的建構函式中和run函式中的變數和常量
1。 /*該粒子系統包含的星星的個數
2。 星星越多畫面越密*/
3。 final int STAR_COUNT = width / 2;
4。
5。 /*資料型別為Star object的動態陣列
6。 儲存該星域中所有的星星*/
7。 ArrayList
8。
9。 /*消失點
10。 用來控制視角*/
11。 PVector endpoint;
之後我們來定義Star物件,輸入如下程式碼:
1。 class Star{
2。 //常量宣告
3。 //變數宣告
4。 //建構函式
5。 //成員函式
6。 }
將Star類的建構函式替換為:
1。 Star(){
2。 /*在一個長方體區域內隨機生成一個點
3。 返回它在世界座標系下的座標*/
4。 worldPosition = new PVector(random(0, width), random(0, height), random(0, MAX_DEPTH));
5。 }
接下來在Star類成員函式部分加入transfrom函式,這個函式是本作品最關鍵的地方,希望大家能好好體會一下。
1。 void transform(PVector endpoint){
2。 /*將星星的座標從世界世界座標系變換到消失點座標系
3。 以下程式碼等同於:
4。 viewPosition。x = (worldPosition。x - endpoint。x) / worldPosition。z * SCALE;
5。 viewPosition。y = (worldPosition。y - endpoint。y) / worldPosition。z * SCALE;
6。 */
7。 viewPosition = PVector。sub(worldPosition, endpoint)。div(worldPosition。z)。mult(SCALE);
8。
9。 /*將星星的座標從消失點座標系變換到螢幕座標系
10。 以下程式碼等同與:
11。 screenPosition。x = endpoint。x + viewPosition。x;
12。 screenPosition。y = endpoint。y + viewPosition。y;
13。 */
14。 screenPosition = PVector。add(endpoint, viewPosition);
15。
16。 /*根據世界座標z的大小來確定星星的直徑
17。 z越小,說明越靠近螢幕,所以星星越大*/
18。 diam = map(worldPosition。z, 0, MAX_DEPTH, MAX_DIAM, 0);
19。 }
接著在Star類的成員函式部分加入checkEdge函式的定義:
1。 void checkEdge(){
2。 if(screenPosition。x <= 0 || screenPosition。x >= width || screenPosition。y <=0 || screenPosition。y >= height){
3。 /*如果這個點已經在螢幕外了,那麼將其裁剪,
4。 同時在相同的長方體區域隨機生成一個新的點*/
5。 worldPosition。set(random(0, width), random(0, height), MAX_DEPTH);
6。 }
7。 }
然後繼續在Star類的成員函式部分Star類的display函式
1。 void display(){
2。 /*在螢幕上繪製該星星
3。 每一個星星是一個白色的、沒有邊的圓*/
4。 fill(255);
5。 noStroke();
6。 ellipse(screenPosition。x, screenPosition。y, diam, diam);
7。 }
在第二步的最後我們把Star類需要的一些成員變數和常量加上,把它們新增到Star類的常量和變數部分。
1。 /*星星在螢幕座標系直徑的最大值
2。 用於控制星星在螢幕上的整體大小*/
3。 final float MAX_DIAM = 16;
4。
5。 /*星星裡螢幕最遠的距離
6。 用來控制星域的立體感*/
7。 final float MAX_DEPTH = width / 2;
8。
9。 /*進行座標變換時候的
10。 用來控制星星在螢幕上的分佈範圍*/
11。 final float SCALE = MAX_DEPTH;
12。 /*星星在三個座標系下的座標
13。 記錄它們在不同參考系下的位置*/
14。 PVector worldPosition, screenPosition, viewPosition;
15。
16。 /*星星在螢幕座標系下的直徑
17。 確定星星在螢幕上的大小*/
18。 float diam;
接下來我們進入第二階段,讓星星動起來。完成時的效果如下:
首先我們在StarField的run方法中,給每一個星星新增如下的行為:
1。 /*更新在世界座標系的座標
2。 讓星星飛向觀察者*/
3。 s。move(speed);
接著在StarField的構造方法中初始化speed這個值。
1。 /*初始化星星的移動速度
2。 讓移動速度不用太快和太慢*/
3。 speed = (MAX_SPEED + MIN_SPEED) / 2;
然後在StarField的變數和常量部分,定義新增加和星星速度有關的常量和變數。
1。 /*分別代表星星移動的最大速度,最小速度,
2。 用來控制星星移動速度的範圍*/
3。 final int MAX_SPEED = 11, MIN_SPEED = 1;
4。
5。 /*速度改變的步長
6。 每一次增加或者減小速度的時速度改變的最小值*/
7。 final int SPEED_STEP = 1;
8。
9。 /*星星移動的速度
10。 控制星星移動快慢*/
11。 int speed;
這之後我們在Star的成員函式部分加入如下程式碼,然後第二階段就到此結束。
1。 void move(float speed){
2。 worldPosition。z -= speed;
3。
4。 /*限制星星世界座標系的位置
5。 防止出現星星移動到螢幕外的情況*/
6。 worldPosition。z = constrain(worldPosition。z, 0, MAX_DEPTH);
7。 }
第四步我們實現點選滑鼠,星星的速度發生改變的效果。效果如下:
直接在main。pde的mousePressed中加入如下程式碼:
1。 if(mouseButton == LEFT){
2。 /*如果是滑鼠左鍵按下了
3。 提高星域的速度*/
4。 sf。speedUP();
5。 }else if(mouseButton == RIGHT){
6。 /*如果是滑鼠右鍵按下了
7。 降低星域的速度*/
8。 sf。speedDown();
9。 }
然後在StarField的成員函式中定義speedUP和speedDown函式:
1。 void speedUP(){
2。 speed += SPEED_STEP;
3。
4。 /*限制speed在最大和最小值之間
5。 防止速度太快*/
6。 speed = constrain(speed, MIN_SPEED, MAX_SPEED);
7。 }
8。
9。 void speedDown(){
10。 speed -= SPEED_STEP;
11。
12。 /*限制speed在最大和最小值之間
13。 防止速度小於零*/
14。 speed = constrain(speed, MIN_SPEED, MAX_SPEED);
15。 }
最後一步我們實現視角改變的效果。效果如下:
首先在mouseMoved函數里面加入如下程式碼:
1。 /*根據滑鼠的位置來更新消失點的位置
2。 達到改變視角的目的*/
3。 sf。updateEndpoint(mouseX, mouseY);
然後再StarField的成員函式部分對上面呼叫的updateEndpoint函式進行定義:
1。 void updateEndpoint(float x, float y){
2。 endpoint。x = x;
3。 endpoint。y = y;
4。 }
完成!
結語
我們有很多可以擴充套件的地方,比如改變Star類裡的MAX_DEPTH來改變星域的立體感,改變StarField類的STAR_COUNT,Star類SCALE的來改變星域的密度和範圍。
目前為止對於每一星星我們只是簡單的畫了一個白色的小圓,其實這裡有很大的發揮空間。比如用Noise函式根據星星在螢幕座標系的位置計算星星的顏色,或者用你思念的人的名字或者其中的字母來替代圓圈,創造出那一片獨一無二,只屬於你的燦爛的星域!
//全部程式碼
Main。pde
1。 /*這個是一個由星星構成的粒子系統
2。 擁有星星的所有狀態和行為*/
3。 StarField sf;
4。 void setup(){
5。 size(400, 400);
6。 sf = new StarField();
7。 }
8。
9。 void draw(){
10。 background(0);
11。 /*執行星域系統
12。 更新星星的狀態和繪製星星*/
13。 sf。run();
14。 }
15。
16。 /*滑鼠按下時呼叫的函式
17。 用來改變星域的速度*/
18。 void mousePressed(){
19。 if(mouseButton == LEFT){
20。 /*如果是滑鼠左鍵按下了
21。 提高星域的速度*/
22。 sf。speedUP();
23。 }else if(mouseButton == RIGHT){
24。 /*如果是滑鼠右鍵按下了
25。 降低星域的速度*/
26。 sf。speedDown();
27。 }
28。 }
29。
30。 /*滑鼠移動的時候呼叫的函式
31。 用來改變視角*/
32。 void mouseMoved(){
33。 /*根據滑鼠的位置來更新消失點的位置
34。 達到改變視角的目的*/
35。 sf。updateEndpoint(mouseX, mouseY);
36。 }
StarField。pde
1。 class StarField{
2。 /*該粒子系統包含的星星的個數
3。 星星越多畫面越密*/
4。 final int STAR_COUNT = width / 2;
5。
6。 /*分別代表星星移動的最大速度,最小速度,
7。 用來控制星星移動速度的範圍*/
8。 final int MAX_SPEED = 11, MIN_SPEED = 1;
9。
10。 /*速度改變的步長
11。 每一次增加或者減小速度的時速度改變的最小值*/
12。 final int SPEED_STEP = 1;
13。
14。 /*資料型別為Star object的動態陣列
15。 儲存該星域中所有的星星*/
16。 ArrayList
17。
18。 /*消失點
19。 用來控制視角*/
20。 PVector endpoint;
21。
22。 /*星星移動的速度
23。 控制星星移動快慢*/
24。 int speed;
25。
26。 StarField(){
27。
28。 /*將初始消失點設定在滑鼠最開始的位置*/
29。 endpoint = new PVector(mouseX, mouseY);
30。
31。 /*初始化所有的星星*/
32。 stars = new ArrayList();
33。 for(int i = 0; i < STAR_COUNT; i++){
34。 stars。add(new Star());
35。 }
36。
37。 /*初始化星星的移動速度
38。 讓移動速度不用太快和太慢*/
39。 speed = (MAX_SPEED + MIN_SPEED) / 2;
40。 }
41。
42。
43。 void run(){
44。 for(Star s : stars){
45。 /*更新在世界座標系的座標
46。 讓星星飛向觀察者*/
47。 s。move(speed);
48。
49。 /*對星星進行座標變換
50。 獲得星星在螢幕座標系的座標
51。 用於之後的星星的渲染*/
52。 s。transform(endpoint);
53。
54。 /*對螢幕外的星星進行裁剪
55。 同時生成一個新的星星
56。 使得星星可以源源不斷的出現*/
57。 s。checkEdge();
58。
59。 /*依據螢幕座標系渲染星星,
60。 使得星星在螢幕上出現*/
61。 s。display();
62。 }
63。 }
64。
65。 void updateEndpoint(float x, float y){
66。 endpoint。x = x;
67。 endpoint。y = y;
68。 }
69。
70。 void speedUP(){
71。 speed += SPEED_STEP;
72。
73。 /*限制speed在最大和最小值之間
74。 防止速度太快*/
75。 speed = constrain(speed, MIN_SPEED, MAX_SPEED);
76。 }
77。
78。 void speedDown(){
79。 speed -= SPEED_STEP;
80。
81。 /*限制speed在最大和最小值之間
82。 防止速度小於零*/
83。 speed = constrain(speed, MIN_SPEED, MAX_SPEED);
84。 }
85。
86。 }
Star。pde
1。 class Star{
2。 /*星星在螢幕座標系直徑的最大值
3。 用於控制星星在螢幕上的整體大小*/
4。 final float MAX_DIAM = 16;
5。
6。 /*星星裡螢幕最遠的距離
7。 用來控制星域的立體感*/
8。 final float MAX_DEPTH = width / 2;
9。
10。 /*進行座標變換時候的
11。 用來控制星星在螢幕上的分佈範圍*/
12。 final float SCALE = MAX_DEPTH;
13。
14。 /*星星在三個座標系下的座標
15。 記錄它們在不同參考系下的位置*/
16。 PVector worldPosition, screenPosition, viewPosition;
17。
18。 /*星星在螢幕座標系下的直徑
19。 確定星星在螢幕上的大小*/
20。 float diam;
21。
22。 Star(){
23。 /*在一個長方體區域內隨機生成一個點
24。 返回它在世界座標系下的座標*/
25。 worldPosition = new PVector(random(0, width), random(0, height), random(0, MAX_DEPTH));
26。 }
27。
28。 void move(float speed){
29。 worldPosition。z -= speed;
30。
31。 /*限制星星世界座標系的位置
32。 防止出現星星移動到螢幕外的情況*/
33。 worldPosition。z = constrain(worldPosition。z, 0, MAX_DEPTH);
34。 }
35。
36。 void transfrom(PVector endpoint){
37。 /*將星星的座標從世界世界座標系變換到消失點座標系
38。 以下程式碼等同於:
39。 viewPosition。x = (worldPosition。x - endpoint。x) / worldPosition。z * SCALE;
40。 viewPosition。y = (worldPosition。y - endpoint。y) / worldPosition。z * SCALE;
41。 */
42。 viewPosition = PVector。sub(worldPosition, endpoint)。div(worldPosition。z)。mult(SCALE);
43。
44。 /*將星星的座標從消失點座標系變換到螢幕座標系
45。 以下程式碼等同與:
46。 screenPosition。x = endpoint。x + viewPosition。x;
47。 screenPosition。y = endpoint。y + viewPosition。y;
48。 */
49。 screenPosition = PVector。add(endpoint, viewPosition);
50。
51。 /*根據世界座標z的大小來確定星星的直徑
52。 z越小,說明越靠近螢幕,所以星星越大*/
53。 diam = map(worldPosition。z, 0, MAX_DEPTH, MAX_DIAM, 0);
54。 }
55。
56。 void checkEdge(){
57。 if(screenPosition。x <= 0 || screenPosition。x >= width || screenPosition。y <=0 || screenPosition。y >= height){
58。 /*如果這個點已經在螢幕外了,那麼將其裁剪,
59。 同時在相同的長方體區域隨機生成一個新的點*/
60。 worldPosition。set(random(0, width), random(0, height), MAX_DEPTH);
61。 }
62。 }
63。
64。 void display(){
65。 /*在螢幕上繪製該星星
66。 每一個星星是一個白色的、沒有邊的圓*/
67。 fill(255);
68。 noStroke();
69。 ellipse(screenPosition。x, screenPosition。y, diam, diam);
70。 }
71。
72。 }