今天要講解的是運用在 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);
}
若我們對這個 sin
函數再套上 fract
取其正小數部分,就能稍微凸顯出其「難以預測」特質,且輸出範圍也剛好對應在一般 random
函數的 0 ~ 1
輸出區間:
function my_random(x) {
return fract(sin(x*1.0));
}
但我們仍然可以知道該函數在哪一段區間呈現上升趨勢,哪一段區間呈現下降趨勢,且很多區間仍然是完整的 sin 波形狀。
但接下來的工作非常簡單,只要加強原本 sin 波的頻率和振幅就行了。
首先把頻率拉到很高,可以讓明確的上升下降區間變得極小,但平時對 random 函數並不會取區間極小的多個數值進行輸入,所以沒辦法從 random 函數的輸出看出明確的變化趨勢。
另外就是振幅拉高,用處就是讓 fract
輸出值的變動頻率拉高,也能達到模糊趨勢的類似效果。
function my_random(x) { // 加大振幅版本
return fract(sin(x*1.0) * 10);
}
function my_random(x) { // 加大振幅和頻率版本
return fract(sin(x*10) * 10);
}
因為變動實在太快了,上圖我還縮小了 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)
呢?比如說我現在想做出一個白噪音的圖案,每個像素位置都是獨立的灰度值:
這個簡單,只要原本含有 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);
}
程式渲染結果:
如果要讓這個白噪音流動起來,我們可以先用 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);
}
然後利用剛剛的隨機值作為每個像素點的相位差,因為每一個點的 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);
}
有趣的是在 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);
}
這是程式渲染的結果: