iT邦幫忙

2024 iThome 鐵人賽

DAY 27
0
Modern Web

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

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

  • 分享至 

  • xImage
  •  

今天我們要用前一個單元 glsl 基礎教學(五) –– 繪製發光線條和物體 介紹的發光效果繪圖技巧,製作一個類似太陽系的行星環繞動畫,最終成品看起來像這樣:

Imgur

程式基礎模板

我們先用前一個單元 glsl 基礎教學(五) –– 繪製發光線條和物體 的第一個範例作為基礎模板(然後把發光體的亮度調小一點),中間的發光體就是作品的恆星位置:

  • mySketch.js
let rectShader; 
  
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]);
     
  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;
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);

    fragColor = vec4(c, 1.0);
}
  • 程式結果

Imgur

初始化行星實體

接下來要考慮到行星動畫的各項參數:

  • 軌道半徑 orbit_radius
  • 繞行速度 rotate_speed
  • 起始角度 start_angle
  • 軌跡位置陣列 tracks
  • 軌跡顏色 track_color

一開始已經展示了完成品的動畫,軌跡就是行星行走過後的歷史軌跡,但是軌跡的實作比較麻煩,在這個單元我們先來解決行星繞行的主架構。

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)];
    }
}

我們用類別 Planet 來封裝上面五個行星所具備的特性,並且用 get_pos 來計算行星在當下 frameCount 所在的位置。

然後用一個 array 變數 planet_list 儲存多個被初始化的行星實體:

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]); 
     
  rect(0,0,width, height); 
}

將行星特性傳入片段著色器

接下來要處理的是如何將行星的特性傳入片段著色器並進行渲染,首先要考慮我們要設定什麼 uniform 變數在 shader.frag 裡面。

這裡要注意到的是,在 mySketch.js 初始化的行星個數是不一定的,但是在 shader.frag 裡面設定的陣列變數必須要明確指定長度,比如說:

uniform vec2 u_planet_pos_list[5];

一個可行的方法是,人為制定 u_planet_pos_list 最大的可能長度,比如說設為 10,然後再傳入一個行星個數的參數 int u_planet_cnt;,然後記得在 p5.js 的程式中,初始化的行星個數不可以超過 10 個。

mySketch.js 新增兩行 rectShader.setUniform

    rectShader.setUniform('u_planet_pos_list', planet_list.map(p => p.get_pos(frameCount)).flat());
    rectShader.setUniform('u_planet_cnt', planet_list.length);

shader.frag 新增兩行 uniform 變數:

uniform vec2 u_planet_pos_list[10];
uniform int u_planet_cnt;

這裡要注意到 u_planet_pos_list 變數是如何被傳入的,vec2 由兩個 float 所組成,所以 vec2 [10] 會是由 20 個 float 所構成。

在傳入的規則上 rectShader.setUniform 不能傳入 2 * 10 的二維陣列,而是傳入長度為 20 的 float array 才能將 u_planet_pos_list 的變數空間所填滿(但可以填入長度 20 以下的 float array,只是 u_planet_pos_list 未填充的變數為空值)。

所以 rectShader.setUniform('u_planet_pos_list', planet_list.map(p => p.get_pos(frameCount)).flat()); 才會加入 flat() 將二維陣列攤平為一維陣列。

片段著色器計算行星光源

接下來我要使用被傳入的新參數 u_planet_pos_listu_planet_cnt 來渲染繞著恆星公轉的行星光源。

#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 = 20.0/dist * 0.00015; // 分配行星較弱光源
        c += light_ratio * vec3(1.0, 1.0, 1.0); // 每個像素疊加行星光源
    }

    fragColor = vec4(c, 1.0);
}

搭配剛剛 mySketch.js 的更動:

...

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); 
}

這是程式最後渲染出來的效果:

Imgur

恆星軌跡光源的渲染留到下一個單元繼續實作!


上一篇
[Day 26] glsl 基礎教學(五) –– 繪製發光線條和物體
下一篇
[Day 28] p5.js 實戰演練(十一) –– 行星環繞動畫(二)
系列文
p5.js 的環形藝術30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言