您當前的位置:首頁 > 攝影

程式碼檢查|如何用processing實現星際漫遊效果

作者:由 OF COURSE想當然 發表于 攝影時間:2019-04-10

程式碼檢查|如何用processing實現星際漫遊效果

黑洞 《星際穿越》 劇照

北京時間4月10日21點整,全球六地透過協調召開全球新聞釋出會,事件視界望遠鏡宣佈一項重大成果,與超大質量黑洞的照片有關。

俯察品類之盛,仰觀宇宙之大。總能給人們帶來愉悅,現在就讓我們一起用processing來模擬我們頭頂的那片星空吧!

程式碼檢查|如何用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(initialCapacity)

/*引數*/

Type //Class Name: 放入ArrayList的資料型別或者物件

initialCapacity //int: 定義ArrayList的初始容量,初始為0

/*案例*/

ArrayList particles = new 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程式的視窗,建立螢幕座標系。然後在螢幕上任選一個點,作為消失點,該點在螢幕座標系下的座標為:

程式碼檢查|如何用processing實現星際漫遊效果

接著在這個長方體裡隨機生成一系列的星星,每個星星由一個三維向量 表示,這是它在世界座標系下的座標。

程式碼檢查|如何用processing實現星際漫遊效果

接下來我們要做的就是把每一個星星的在世界座標系的座標,轉換成在processing窗口裡面的座標:

程式碼檢查|如何用processing實現星際漫遊效果

這樣我們就可以在螢幕上把星星繪製出來。

所以,首先需要計算星星在世界座標系下相對於消失點(endpoint) 方向的偏移量,接下來將這個偏移量根據星星的 進行放縮,最後再將放縮完成的偏移量加回消失點的座標,得到星星在螢幕上的座標。

公式如下:

程式碼檢查|如何用processing實現星際漫遊效果

其中 是放縮比例,用來控制控制星域的範圍。

分析公式可以發現 越大對應的 , 越大,也就是說離消失點越遠。也就說我們只要將 初始設為一個比較大的值,然後在不斷減小它,這樣就會出現星星離消失點越來越遠,離螢幕越來越近的效果。同時我們根據 的大小設定星星的直徑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。 }

第二步,我們的主要任務是將星星繪製在螢幕上,實現以下效果:

程式碼檢查|如何用processing實現星際漫遊效果

首先在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 stars;

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;

接下來我們進入第二階段,讓星星動起來。完成時的效果如下:

程式碼檢查|如何用processing實現星際漫遊效果

首先我們在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。 }

第四步我們實現點選滑鼠,星星的速度發生改變的效果。效果如下:

程式碼檢查|如何用processing實現星際漫遊效果

直接在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。 }

最後一步我們實現視角改變的效果。效果如下:

程式碼檢查|如何用processing實現星際漫遊效果

首先在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 stars;

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。 }