iT邦幫忙

2024 iThome 鐵人賽

DAY 29
0
Modern Web

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

[Day 29] glsl 基礎教學(六) –– random 函數

  • 分享至 

  • xImage
  •  

今天要講解的是運用在 glsl 上的隨機化技巧,也就是 random 的相關功能。

但是 glsl 並不像 p5.js 有提供內建的 random 函數,我們只能利用數學技巧來建構一個偽隨機函數。

偽隨機函數

在前面的文章 p5.js 實戰演練(四) –– 結晶動畫實作(一) 其實就有介紹過如何用數學技巧建構隨機函數:

function my_random(x) {
    return fract(sin(x*425.121)*437.53123);
}

接下來我們來逐一拆解為何這種方法能構成一個類隨機函數。

在現實世界,不太可能構造出一個完全隨機的函數,只能做出一個類似隨機的函數,也就是說給定「一個時間點」或是「一個或多個函數輸入」,這個函數的輸出「很難看出其短區間變動趨勢(上升或下降)」,以及「輸出和輸入之間的關係」。

先給出一個簡單的 sin 函數:

function my_random(x) {
    return sin(x*1.0);
}

Imgur

若我們對這個 sin 函數再套上 fract 取其正小數部分,就能稍微凸顯出其「難以預測」特質,且輸出範圍也剛好對應在一般 random 函數的 0 ~ 1 輸出區間:

function my_random(x) {
    return fract(sin(x*1.0));
}

Imgur

但我們仍然可以知道該函數在哪一段區間呈現上升趨勢,哪一段區間呈現下降趨勢,且很多區間仍然是完整的 sin 波形狀。

但接下來的工作非常簡單,只要加強原本 sin 波的頻率和振幅就行了。

首先把頻率拉到很高,可以讓明確的上升下降區間變得極小,但平時對 random 函數並不會取區間極小的多個數值進行輸入,所以沒辦法從 random 函數的輸出看出明確的變化趨勢。

另外就是振幅拉高,用處就是讓 fract 輸出值的變動頻率拉高,也能達到模糊趨勢的類似效果。

function my_random(x) { // 加大振幅版本
    return fract(sin(x*1.0) * 10);
}

Imgur

function my_random(x) { // 加大振幅和頻率版本
    return fract(sin(x*10) * 10);
}

Imgur

因為變動實在太快了,上圖我還縮小了 x 軸的可視區間。

依據最後構造出的函數 fract(sin(x*10) * 10),若我們用 0.2, 0.4, 0.6, 0.8, 1.0 五個值作為輸入進行取樣:

x fract(sin(x*10) * 10)
0.2 0.092974
0.4 0.431975
0.6 0.205845
0.8 0.893582
1.0 0.559789

這是對應的輸出結果,其實很難看出這個函數的變化趨勢和輸入輸出的關係。

所以現在我們已經找到一個好方法來建構出一個偽隨機函數了。

二維隨機函數

假設隨機函數的輸入不只是一個 x 數值,而是一個位置座標 (x, y) 呢?比如說我現在想做出一個白噪音的圖案,每個像素位置都是獨立的灰度值:

Imgur

這個簡單,只要原本含有 x 的隨機算式再加入 y 因子就行了,比如說:

function my_random(x, y) { // 加大振幅和頻率版本
    return fract(sin(x*553.3178 + y*694.1234) * 2574.1243);
}

放在 glsl 我們可以再讓他看起來更學術化一點,用向量內積的方式表示:

float random (vec2 st) { // 目標像素點的位置座標
    return fract(
        sin(
            dot(
                st.xy,
                vec2(553.3178,694.1234)
            )
        )* 2574.1243
    );
}

然後再將這個算是的結果作為該像素的灰度值,就能實現二維白噪音的效果了:

  • 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);
}
  • shader.vert
#version 300 es

in vec3 aPosition; 
in vec2 aTexCoord; 
  
void main() { 
 vec4 positionVec4 = vec4(aPosition, 1.0); 
 positionVec4.xy = positionVec4.xy * 2.0 - 1.0; 
 gl_Position = positionVec4; 
}
  • mySketch.js
let rectShader; 
  
function preload(){     
  rectShader = loadShader('shader.vert', 'shader.frag'); 
} 
  
function setup() { 
  pixelDensity(1);
  createCanvas(200, 200, WEBGL); 
  noStroke(); 
} 

function draw() {   
  shader(rectShader);  
  rectShader.setUniform('u_resolution', [width, height]);     
  rect(0,0,width, height); 
} 

程式渲染結果:

Imgur

白噪音動畫

如果要讓這個白噪音流動起來,我們可以先用 sin 函數讓每個像素點的灰度值隨時間進行變動:

  • mySketch.js
let rectShader; 
  
function preload(){ 
    
  rectShader = loadShader('shader.vert', 'shader.frag'); 
} 
  
function setup() { 
	pixelDensity(1);
  // shaders require WEBGL mode to work 
  createCanvas(600, 600, WEBGL); 
  noStroke(); 
    
} 
  
function draw() {   
  // shader() sets the active shader with our shader 
  shader(rectShader); 
  
  // lets send the time and resolution to our shader 
  rectShader.setUniform('u_resolution', [width, height]); 
  rectShader.setUniform('u_time', frameCount);
    
  // rect gives us some geometry on the screen 
  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;
uniform float u_time;
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 = 0.5 + sin(u_time/10.0) * 0.5;

    fragColor = vec4(vec3(rnd),1.0);
}

Imgur

然後利用剛剛的隨機值作為每個像素點的相位差,因為每一個點的 sin 波起點都是不一樣的,所以灰度值還是會有顯著的不同,呈現出來的就是會流動的 白噪音動畫:

  • mySketch.js
let rectShader; 
  
function preload(){ 
    
  rectShader = loadShader('shader.vert', 'shader.frag'); 
} 
  
function setup() { 
	pixelDensity(1);
  // shaders require WEBGL mode to work 
  createCanvas(600, 600, WEBGL); 
  noStroke(); 
    
} 

function draw() {   
  // shader() sets the active shader with our shader 
  shader(rectShader); 
  
  // lets send the time and resolution to our shader 
  rectShader.setUniform('u_resolution', [width, height]); 
  rectShader.setUniform('u_time', frameCount);
    
  // rect gives us some geometry on the screen 
  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;
uniform float u_time;
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 = 0.5 + sin(u_time/10.0 + random( st ) * 2.0 * PI) * 0.5;

    fragColor = vec4(vec3(rnd),1.0);
}

Imgur

朦朧的太陽系動畫

有趣的是在 p5.js 實戰演練(十一) –– 行星環繞動畫(二) 所完成的動畫中,我們也可以用這一個單元所學到的技巧進行白噪音式的亮度調節,然後可以得到一個朦朧的太陽系動畫:

  • mySketch.js
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)];
    }

    record_track(frame_cnt) {
        if (frame_cnt % 2 == 0) {
            return;
        }
        this.tracks.unshift(this.get_pos(frame_cnt));

        if (this.tracks.length > trackCountMax) {
            this.tracks.pop();
        }
    }
}

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);
    // shaders require WEBGL mode to work 
    createCanvas(600, 600, WEBGL); 
    noStroke();
} 

function draw() {

    // shader() sets the active shader with our shader 
    shader(rectShader); 

    planet_list.forEach((element, index) => {
        element.record_track(frameCount);
    });

    // lets send the time and resolution to our shader 
    rectShader.setUniform('u_resolution', [width, height]);
    rectShader.setUniform('u_time', frameCount); 
    rectShader.setUniform('u_planet_pos_list', planet_list.map(p => p.get_pos(frameCount)).flat());
    rectShader.setUniform('u_track_list', planet_list.map(p => p.tracks).flat().flat());
    rectShader.setUniform('u_track_cnt', planet_list[0].tracks.length);
    rectShader.setUniform('u_planet_cnt', planet_list.length);
    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 gives us some geometry on the screen 
    rect(0,0,width, height); 
} 


  • shader.frag
#version 300 es
precision highp float;

uniform vec2 u_resolution;
uniform float u_time;
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;

#define PI 3.14159265358979323846

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;

    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];
        }
    }
    
    // 利用 sin 波讓亮度比例在 0.6 ~ 1.0 之間調整 
    float rnd = 0.8 + sin(u_time/10.0 + random( st ) * 2.0 * PI) * 0.2;
    c *= rnd;

    fragColor = vec4(c, 1.0);
}

這是程式渲染的結果:

Imgur


上一篇
[Day 28] p5.js 實戰演練(十一) –– 行星環繞動畫(二)
下一篇
[Day 30] p5.js 實戰演練(十二) –– noise 函數與馬賽克動畫
系列文
p5.js 的環形藝術30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言