在本系列教學的最後一篇,我們來探索如何用前一篇 glsl 基礎教學(六) –– random 函數 所講解的隨機化技巧,來實作 noise 函數的效果。
並應用 noise 函數的效果,以及加入時間因子的灰度值隨機變動,來完成本系列最後一個作品馬賽克動畫:
簡單來說,就是讓少數的像素點執行二維 random
函數得到隨機的灰度值,然後剩下的像素點呢?就是根據這些周圍已決定灰度值的像素點來執行內插法。
先從程式的基本模板開始:
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]);
rectShader.setUniform('u_time', frameCount);
rect(0,0,width, height);
}
shader.frag
#version 300 es
#ifdef GL_ES
precision highp float;
#endif
#define PI 3.14159265358979323846
uniform vec2 u_resolution;
out vec4 fragColor;
float random (vec2 st) { // 目標像素點的位置座標
return fract(
sin(
dot(
st.xy,
vec2(553.3178,694.1234)
)
)* 2574.1243
);
}
void main() {
vec2 st = gl_FragCoord.xy/u_resolution.xy;
float rnd = random( st );
fragColor = vec4(vec3(rnd),1.0);
}
以白噪音渲染作為一開始的基礎模板,原本是以每個像素點的灰度值進行隨機值計算,現在要化簡為只有固定幾個點需要計算隨機灰度值。
我們打算以網格點作為要計算隨機灰度值的位置,剩下位置的灰度值可以比照其左下角網格點的灰度值,也就是說,要將畫布「馬賽克化」:
#version 300 es
#ifdef GL_ES
precision highp float;
#endif
#define PI 3.14159265358979323846
uniform vec2 u_resolution;
out vec4 fragColor;
float random (vec2 st) { // 目標像素點的位置座標
return fract(
sin(
dot(
st.xy,
vec2(553.3178,694.1234)
)
)* 2574.1243
);
}
float noise(vec2 st) {
st -= mod(st, vec2(1./20.));
return random(st);
}
void main() {
vec2 st = gl_FragCoord.xy/u_resolution.xy;
float rnd = noise( st );
fragColor = vec4(vec3(rnd),1.0);
}
程式結果:
簡單來說就是以 1/20
也就是 0.05
為一個網格單位,假設目標像素點為 (0.23, 0.56)
,程式就會利用 st -= mod(st, vec2(1./20.));
將其統一為其左下角的網格點 (0.2, 0.55)
再送入 random
函數做灰度值計算。
比較漂亮的作法為:
#version 300 es
#ifdef GL_ES
precision highp float;
#endif
#define PI 3.14159265358979323846
uniform vec2 u_resolution;
out vec4 fragColor;
float random (vec2 st) { // 目標像素點的位置座標
return fract(
sin(
dot(
st.xy,
vec2(553.3178,694.1234)
)
)* 2574.1243
);
}
float noise(vec2 st) {
st *= 20.;
vec2 floor_st = floor(st);
return random(floor_st);
}
void main() {
vec2 st = gl_FragCoord.xy/u_resolution.xy;
float rnd = noise( st );
fragColor = vec4(vec3(rnd),1.0);
}
程式結果:
將座標乘上 20
並進行無條件捨去,也能得到左下角網格點一樣的隨機灰度值。
接下來要將每個網格之間的色差「平滑化」,也就是每個網格的顏色不在比照左下角的網格點灰度值,而是依據網格的四個角進行內插法決定灰度值。
我們的做法如圖所示:
其中網格的四個角,其位置座標分別為:
floor_st + vec2(0., 0.)
=> 左下角floor_st + vec2(0., 1.)
=> 左上角floor_st + vec2(1., 0.)
=> 右下角floor_st + vec2(1., 1.)
=> 右上角把位置座標傳入 random
函數就能得到其灰度值,分別為:
random(floor_st + vec2(0., 0.))
=> 用 v00
表示random(floor_st + vec2(0., 1.))
=> 用 v01
表示random(floor_st + vec2(1., 0.))
=> 用 v10
表示random(floor_st + vec2(1., 1.))
=> 用 v11
表示方格中間的黑點就是目標像素點 st
所在之處,也就是我們要進行內插法計算最終灰度值的位置。
方法就是,畫一個平行於 x 軸的橫線,直到碰觸到該網格的左右邊界,我們必須計算這左右交點的內差灰度值,然後由這兩個交點再進行內差得出目標像素點 st
的最終灰度值。
左交點的內插灰度值為:
mix(v00, v01, fract_st.y)
=> 用 V0
表示
右交點的內插灰度值為:
mix(v10, v11, fract_st.y)
=> 用 V1
表示
其中 mix
為 glsl 所提供的內建函數,用來進行內插計算,比如說,mix(v00, v01, fract_st.y)
就是由 1-fract_st.y
比例的 v00
和 fract_st.y
的 v01
所混合而成,就是內插法的意思。
最後再由左交點和右交點的灰度值,來內插計算目標像素點 st
的灰度值:
mix(V0, V1, fract_st.x)
依照上述原理,我們能將 noise
函數改成以下形式:
float noise(vec2 st) {
st *= 20.;
vec2 floor_st = floor(st);
vec2 fract_st = fract(st);
return mix(
mix(
random(floor_st + vec2(0., 0.)), // => v00
random(floor_st + vec2(0., 1.)), // => v01
fract_st.y
), // => V0
mix(
random(floor_st + vec2(1., 0.)), // => v10
random(floor_st + vec2(1., 1.)), // => v11
fract_st.y
), // => V1
fract_st.x
); // -> 最終結果
}
程式結果:
目前可以看到現在的結果還是有一些「尖銳感」,簡單來說就是還不夠「平滑」,解決方法很簡單,就是用 p5.js 創作應用(六) –– 煙霧動畫實作(一) 介紹過的 smoothstep
函數來增加平滑效果。
float noise(vec2 st) {
st *= 20.;
vec2 floor_st = floor(st);
vec2 fract_st = fract(st);
// 使用 smoothstep 函數對 fract_st 進行二次處理
fract_st = fract_st * fract_st * (3. - 2.*fract_st);
return mix(
mix(
random(floor_st + vec2(0., 0.)), // => v00
random(floor_st + vec2(0., 1.)), // => v01
fract_st.y
), // => V0
mix(
random(floor_st + vec2(1., 0.)), // => v10
random(floor_st + vec2(1., 1.)), // => v11
fract_st.y
), // => V1
fract_st.x
); // -> 最終結果
}
接下來我們要實作最前面給大家展示的馬賽克動畫,這個動畫就是以前面的 noise
研究成果為基底來生成的,只要再加入時間的因子進行變化。
其中一個重點是要讓每個網格的變化都不同步,這裡用的技巧是我們會在傳入 random
函數的位置座標做一些手腳,讓其可以跟隨時間進行變化:
...
uniform float u_time; // 記得要引入 u_time
...
float noise(vec2 st) {
st *= 20.;
vec2 floor_st = floor(st);
vec2 fract_st = fract(st);
// 使用 smoothstep 函數對 fract_st 進行二次處理
fract_st = fract_st * fract_st * (3. - 2.*fract_st);
return mix(
mix(
random(floor_st + floor(u_time/60.) + vec2(0., 0.)),
random(floor_st + floor(u_time/60.) + vec2(0., 1.)),
fract_st.y
),
mix(
random(floor_st + floor(u_time/60.) + vec2(1., 0.)),
random(floor_st + floor(u_time/60.) + vec2(1., 1.)),
fract_st.y
),
fract_st.x
);
}
動畫呈現結果:
利用 floor(u_time/60.)
每過 60 幀就在四個角的 xy 座標都加上 1,實現了隨時間移動的效果。
但現在的動畫每定格一秒才會移動,我希望的是這個 noise 圖案隨時都會進行變化。
在這裡採用的方法是,把當下的定格動畫和下一次的定格動畫進行時間的內插:
float noise(vec2 st) {
st *= 20.;
vec2 floor_st = floor(st);
vec2 fract_st = fract(st);
fract_st = fract_st * fract_st * (3. - 2.*fract_st);
float floor_noise = mix( // 當下定格動畫
mix(
random(floor_st + floor(u_time/60.) + vec2(0., 0.)),
random(floor_st + floor(u_time/60.) + vec2(0., 1.)),
fract_st.y
),
mix(
random(floor_st + floor(u_time/60.) + vec2(1., 0.)),
random(floor_st + floor(u_time/60.) + vec2(1., 1.)),
fract_st.y
),
fract_st.x
);
float ceil_noise = mix( // 下一個定格動畫
mix(
random(floor_st + ceil(u_time/60.) + vec2(0., 0.)),
random(floor_st + ceil(u_time/60.) + vec2(0., 1.)),
fract_st.y
),
mix(
random(floor_st + ceil(u_time/60.) + vec2(1., 0.)),
random(floor_st + ceil(u_time/60.) + vec2(1., 1.)),
fract_st.y
),
fract_st.x
);
// 對時間進行內插
return mix(floor_noise, ceil_noise, u_time/60. - floor(u_time/60.));
}
程式執行結果:
現在已經確定動畫不會再定格了,下一步要做的是讓每個網格行進的速度都不一樣,也就是說讓每個網格不會都以 60 幀作為變換定格圖案的週期。
其中一個方法就是讓每個網格的左下角網格點放進 random
並計算出變換定格圖案的週期隨機值:
float noise(vec2 st) {
st *= 20.;
vec2 floor_st = floor(st);
vec2 fract_st = fract(st);
fract_st = fract_st * fract_st * (3. - 2.*fract_st);
// 每個網格都會得到不同的變動週期 (60 ~ 180 幀)
float fig_change_interval = random(floor_st) * 120. + 60.;
float time_floor = floor(u_time/fig_change_interval);
float time_ceil = ceil(u_time/fig_change_interval);
float time_fract = fract(u_time/fig_change_interval);
float floor_noise = mix( // 當下定格動畫
mix(
random(floor_st + time_floor + vec2(0., 0.)),
random(floor_st + time_floor + vec2(0., 1.)),
fract_st.y
),
mix(
random(floor_st + time_floor + vec2(1., 0.)),
random(floor_st + time_floor + vec2(1., 1.)),
fract_st.y
),
fract_st.x
);
float ceil_noise = mix( // 下一個定格動畫
mix(
random(floor_st + time_ceil + vec2(0., 0.)),
random(floor_st + time_ceil + vec2(0., 1.)),
fract_st.y
),
mix(
random(floor_st + time_ceil + vec2(1., 0.)),
random(floor_st + time_ceil + vec2(1., 1.)),
fract_st.y
),
fract_st.x
);
// 對時間進行內插
return mix(floor_noise, ceil_noise, time_fract);
}
最後我們根據左下角的網格點座標得出 60 ~ 180
幀之間的隨機值,也就是每個網格在 1 ~ 3
秒間會進行定格圖案的變化,但重點是每個網格的變動時間必須要有明確差異,下面是最後的結果:
作品網址:
https://openprocessing.org/sketch/2304997
可喜可賀,我們終於完成 p5.js 環形藝術的所有教學內容了。