iT邦幫忙

2024 iThome 鐵人賽

DAY 28
1
Modern Web

p5.js 的環形藝術系列 第 28

[Day 28] p5.js 實戰演練(十一) –– 行星環繞動畫(二)

  • 分享至 

  • xImage
  •  

接續上一個單元的實作,我們接下來繼續講解如何渲染出行星的行走軌跡。

目前程式架構

先貼上目前已完成的程式內容:

  • mySketch.js
class Planet {
    constructor(opts) {
        this.orbit_radius = opts.orbit_radius;
        this.rotate_speed = opts.rotate_speed;
        this.start_angle = opts.start_angle;
        this.track_color = opts.track_color;
        this.tracks = [];
    }

    get_pos(frame_cnt) {
        let radius = this.orbit_radius;
        let angle = this.start_angle + this.rotate_speed * frame_cnt;
        return [radius * cos(angle), radius * sin(angle)];
    }
}

let rectShader; 
let planet_list = [ // 被初始化的行星們
    new Planet(
        {
            orbit_radius: 100,
            rotate_speed: 1/60/1.6 * 2 * Math.PI,
            start_angle: Math.random() * 2 * Math.PI,
            track_color: "#FF6B6B",
        }
    ),
    new Planet(
        {
            orbit_radius: 50,
            rotate_speed: -1/60/1.6 * 2 * Math.PI,
            start_angle: Math.random() * 2 * Math.PI,
            track_color: "#FFCA3A",
        }
    ),
    new Planet(
        {
            orbit_radius: 150,
            rotate_speed: 1/60/3 * 2 * Math.PI,
            start_angle: Math.random() * 2 * Math.PI,
            track_color: "#A4C6FF",
        }
    ),
    new Planet(
        {
            orbit_radius: 120,
            rotate_speed: -1/60/2.5 * 2 * Math.PI,
            start_angle: Math.random() * 2 * Math.PI,
            track_color: "#8AC926",
        }
    ),
    new Planet(
        {
            orbit_radius: 180,
            rotate_speed: -1/60/3 * 2 * Math.PI,
            start_angle: Math.random() * 2 * Math.PI,
            track_color: "#C490E4",
        }
    ),
    new Planet(
        {
            orbit_radius: 220,
            rotate_speed: 1/60/2 * 2 * Math.PI,
            start_angle: Math.random() * 2 * Math.PI,
            track_color: "#D6E6FF",
        }
    )
];


function preload(){ 
  rectShader = loadShader('shader.vert', 'shader.frag'); 
} 
  
function setup() { 
  pixelDensity(1); 
  createCanvas(600, 600, WEBGL); 
  noStroke();   
}
  
function draw() {    
  shader(rectShader); 
  
  rectShader.setUniform('u_resolution', [width, height]);
	rectShader.setUniform('u_planet_pos_list', planet_list.map(p => p.get_pos(frameCount)).flat());
	rectShader.setUniform('u_planet_cnt', planet_list.length);
     
  rect(0,0,width, height); 
}
  • shader.vert
#version 300 es

in vec3 aPosition; 
  
void main() { 
 vec4 positionVec4 = vec4(aPosition, 1.0); 
 positionVec4.xy = positionVec4.xy * 2.0 - 1.0;  
 gl_Position = positionVec4; 
}
  • shader.frag
#version 300 es
precision highp float;

uniform vec2 u_resolution;
uniform vec2 u_planet_pos_list[10];
uniform int u_planet_cnt;
out vec4 fragColor;

void main() {
    vec2 st = gl_FragCoord.xy / u_resolution;

    vec3 c = vec3(0.0);

    float dist = distance(st, vec2(0.5, 0.5));

    float light_ratio = 80.0/dist * 0.00015;
    c += light_ratio * vec3(1.0, 1.0, 1.0);

    // 渲染恆星光源後再逐一渲染每個行星
    for (int i = 0; i < u_planet_cnt; i++) {
        vec2 uv = vec2(
            0.5 + u_planet_pos_list[i].x / u_resolution.x,
            0.5 + u_planet_pos_list[i].y / u_resolution.y
        ); // 計算目前行星所在位置
				
        float dist = distance(uv, st);
        float light_ratio = 10.0/dist * 0.00015; // 分配行星較弱光源
        c += light_ratio * vec3(1.0, 1.0, 1.0); // 每個像素疊加行星光源
    }

    fragColor = vec4(c, 1.0);
}

以下為渲染結果:

Imgur

紀錄歷史軌跡到 this.tracks

在 class Planet 裡面有一個 tracks 變數,就是為了紀錄過往的行星軌跡,我們要在每次 draw() 執行時,將上一幀的行星位置紀錄在 this.tracks 裡面。

  • class Planet
let trackCountMax = 50; // 紀錄每個行星最多存多少幀的歷史軌跡

class Planet {
    constructor(opts) {
        this.orbit_radius = opts.orbit_radius;
        this.rotate_speed = opts.rotate_speed;
        this.start_angle = opts.start_angle;
        this.track_color = opts.track_color;
        this.tracks = [];
    }

    get_pos(frame_cnt) {
        let radius = this.orbit_radius;
        let angle = this.start_angle + this.rotate_speed * frame_cnt;
        return [radius * cos(angle), radius * sin(angle)];
    }
	
    // 每次儲存上一幀的的行星位置到 this.tracks
    record_track(frame_cnt) {
        // 每兩幀才儲存一次行星位置到 this.tracks,可以把軌跡光源拖長一點
        if (frame_cnt % 2 == 0) {
            return;
        }
        // 加入行星位置到 this.tracks 的頭部 
        this.tracks.unshift(this.get_pos(frame_cnt)); 

        // 超過 50 個歷史軌跡就刪掉最舊的
        if (this.tracks.length > trackCountMax) {
            this.tracks.pop();
        }
    }
}
  • draw()
function draw() {    
  shader(rectShader); 
	
  // 每次都嘗試所有行星的上一幀歷史軌跡存到 this.tracks
  planet_list.forEach((element, index) => {
    element.record_track(frameCount);
  });
  
  rectShader.setUniform('u_resolution', [width, height]);
  rectShader.setUniform('u_planet_pos_list', planet_list.map(p => p.get_pos(frameCount)).flat());
  rectShader.setUniform('u_planet_cnt', planet_list.length);
	
  // 將所有行星的軌跡記錄全數攤平,傳入到 u_track_list
  rectShader.setUniform('u_track_list', planet_list.map(p => p.tracks).flat().flat());
	
  // 計算每個行星目前已紀錄多少幀軌跡,最多 trackCountMax 個,傳入到 u_track_cnt
  rectShader.setUniform('u_track_cnt', planet_list[0].tracks.length);
	
  // 將 #xxxxxx 等色碼字串,轉換成代表 rgb 的 vec3 變數,傳入到 u_track_color_list
  rectShader.setUniform("u_track_color_list", planet_list.map(p => [red(p.track_color)/255, green(p.track_color)/255, blue(p.track_color)/255]).flat());
     
  rect(0,0,width, height); 
}

這裡要注意到的是,目前程式有 6 顆行星,每個行星最多 50 個軌跡,每個軌跡位置為長度為 2[x, y] 陣列,因此針對傳入 u_track_list 變數的資料,他的原始結構為 6 * 50 * 2 的三維陣列,因此要使用 .flat().flat() 將其攤平為一維陣列,才可以傳進片段著色器的 uniform 變數。

另外針對色碼字串(ex. #FF6B6B),p5.js 有提供 greenredblue 函數,將其轉換成對應的 1 ~ 255 rgb 數值,然後用 flat() 將其攤平並傳入到 u_track_color_list 變數中。

片段著色器計算軌跡光源

以下為新增變數:

uniform vec2 u_track_list[10 * 50]; // 最大行星數量 * 最大軌跡數量
uniform int u_track_cnt;
uniform vec3 u_track_color_list[10];

然後針對每顆行星,計算其歷史軌跡再去疊加光源:

    // 渲染恆星光源後再逐一渲染每個行星
    for (int i = 0; i < u_planet_cnt; i++) {
        vec2 uv = vec2(
            0.5 + u_planet_pos_list[i].x / u_resolution.x,
            0.5 + u_planet_pos_list[i].y / u_resolution.y
        ); // 計算目前行星所在位置
				
        float dist = distance(uv, st);
        float light_ratio = 20.0/dist * 0.00015; // 分配行星較弱光源
        c += light_ratio * u_track_color_list[i]; // 每個像素疊加行星光源,白色改成指定顏色
				
        // 針對每顆行星,再去計算其軌跡光源
        for (int j = 0; j < u_track_cnt; j++) {
            vec2 track_uv = vec2(
                0.5 + u_track_list[i * u_track_cnt + j].x / u_resolution.x,
                0.5 + u_track_list[i * u_track_cnt + j].y / u_resolution.y
            ); // 計算軌跡所在位置
						
            float dist = distance(track_uv, st);
            float light_ratio = 1.5/dist * 0.00015; // 分配給軌跡更弱光源
            c += light_ratio * u_track_color_list[i]; // 每個像素疊加軌跡光源
         }
    }

這裡要注意到的是 u_track_list 的 index 指派方法 i * u_track_cnt + j,腦筋需要清楚一點,才能正確取得對應軌跡資料。

這是整個 shader.frag 的樣子:

#version 300 es
precision highp float;

uniform vec2 u_resolution;
uniform vec2 u_planet_pos_list[10];
uniform vec2 u_track_list[500];
uniform int u_track_cnt;
uniform int u_planet_cnt;
uniform vec3 u_track_color_list[10];

out vec4 fragColor;

void main() {
    vec2 st = gl_FragCoord.xy / u_resolution;

    vec3 c = vec3(0.0);

    float dist = distance(st, vec2(0.5, 0.5));

    float light_ratio = 80.0/dist * 0.00015;
    c += light_ratio * vec3(1.0, 1.0, 1.0);

    for (int i = 0; i < 10; i++) {
        if (i >= u_planet_cnt) {
            break;
        }
        vec2 uv = vec2(
            0.5 + u_planet_pos_list[i].x / u_resolution.x,
            0.5 + u_planet_pos_list[i].y / u_resolution.y
        );
        float d = distance(st, uv);
        float l_ratio = 20.0/d * 0.00015;
        c += l_ratio * u_track_color_list[i];

        for (int j = 0; j < 100; j++) {
            if (j >= u_track_cnt) {
                break;
            }

            vec2 uv = vec2(
                0.5 + u_track_list[i * u_track_cnt + j].x / u_resolution.x,
                0.5 + u_track_list[i * u_track_cnt + j].y / u_resolution.y
            );

            float d = distance(st, uv);
            float l_ratio = 1.5/d * 0.00015;
            c += l_ratio * u_track_color_list[i];
        }
    }

    fragColor = vec4(c, 1.0);
}

我們已經完成了作品目標,這是最後的渲染結果:

Imgur


上一篇
[Day 27] p5.js 實戰演練(十) –– 行星環繞動畫(一)
下一篇
[Day 29] glsl 基礎教學(六) –– random 函數
系列文
p5.js 的環形藝術30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言