今天我們要用前一個單元 glsl 基礎教學(五) –– 繪製發光線條和物體 介紹的發光效果繪圖技巧,製作一個類似太陽系的行星環繞動畫,最終成品看起來像這樣:
我們先用前一個單元 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);
}
接下來要考慮到行星動畫的各項參數:
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_list
和 u_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);
}
這是程式最後渲染出來的效果:
恆星軌跡光源的渲染留到下一個單元繼續實作!