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

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

偽隨機函數

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

function my_random(x) {
    return fract(sin(x425.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(x1.0)  10);
}

Imgur

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

Imgur

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

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

x
fract(sin(x10) 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(x553.3178 + y694.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 = 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 = 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