今天我們要用前一個單元 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 p.get_pos(frameCount)).flat()); rectShader.setUniform('u_planet_cnt', planet_list.length); rect(0,0,width, height); }

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

Imgur

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