接續上一個單元的實作,我們接下來繼續講解如何渲染出行星的行走軌跡。
先貼上目前已完成的程式內容:
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);
}
以下為渲染結果:
this.tracks
在 class Planet
裡面有一個 tracks
變數,就是為了紀錄過往的行星軌跡,我們要在每次 draw()
執行時,將上一幀的行星位置紀錄在 this.tracks
裡面。
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 有提供 green
、red
、blue
函數,將其轉換成對應的 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);
}
我們已經完成了作品目標,這是最後的渲染結果: