iT邦幫忙

2024 iThome 鐵人賽

DAY 30
0
Modern Web

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

[Day 30] p5.js 實戰演練(十二) –– noise 函數與馬賽克動畫

  • 分享至 

  • xImage
  •  

前言

在本系列教學的最後一篇,我們來探索如何用前一篇 glsl 基礎教學(六) –– random 函數 所講解的隨機化技巧,來實作 noise 函數的效果。

並應用 noise 函數的效果,以及加入時間因子的灰度值隨機變動,來完成本系列最後一個作品馬賽克動畫:

Imgur

程式基礎模板

簡單來說,就是讓少數的像素點執行二維 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);
}

程式結果:

Imgur

簡單來說就是以 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);
}

程式結果:

Imgur

將座標乘上 20 並進行無條件捨去,也能得到左下角網格點一樣的隨機灰度值。

用內插法計算灰度值

接下來要將每個網格之間的色差「平滑化」,也就是每個網格的顏色不在比照左下角的網格點灰度值,而是依據網格的四個角進行內插法決定灰度值。

我們的做法如圖所示:

Imgur

其中網格的四個角,其位置座標分別為:

  • 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 比例的 v00fract_st.yv01 所混合而成,就是內插法的意思。

最後再由左交點和右交點的灰度值,來內插計算目標像素點 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
    );                                       // -> 最終結果
}

程式結果:

Imgur

引入 smoothstep

目前可以看到現在的結果還是有一些「尖銳感」,簡單來說就是還不夠「平滑」,解決方法很簡單,就是用 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
    );                                       // -> 最終結果
}

Imgur

引入時間因子進行變化

接下來我們要實作最前面給大家展示的馬賽克動畫,這個動畫就是以前面的 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
    );
}

動畫呈現結果:

Imgur

利用 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.));
}

程式執行結果:

Imgur

變動速度差異化

現在已經確定動畫不會再定格了,下一步要做的是讓每個網格行進的速度都不一樣,也就是說讓每個網格不會都以 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 秒間會進行定格圖案的變化,但重點是每個網格的變動時間必須要有明確差異,下面是最後的結果:

Imgur

作品網址:
https://openprocessing.org/sketch/2304997

可喜可賀,我們終於完成 p5.js 環形藝術的所有教學內容了。


上一篇
[Day 29] glsl 基礎教學(六) –– random 函數
系列文
p5.js 的環形藝術30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言