iT邦幫忙

2024 iThome 鐵人賽

DAY 25
0
Modern Web

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

[Day 25] glsl 基礎教學(四) –– 繪製線條

  • 分享至 

  • xImage
  •  

本篇開始講解一些使用在片段著色器的繪圖技巧,第一步就是學會如何繪製基本的函數曲線,經過本單元的講解,我們可以學會如何繪製以下的圖案:

Imgur

程式基礎模板

因為是著重在片段著色器的繪圖技巧,所以本單元只會改動片段著色器 shader.frag 的程式,p5.js 主程式和頂點著色器 shader.vert 並不會做任何更動,以下是本單元的基礎模板:

  • 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]); 
  rect(0,0,width, height); 
} 
  • shader.vert
#version 300 es

in vec3 aPosition;  
  
void main() { 
 vec4 positionVec4 = vec4(aPosition, 1.0); 
 positionVec4.xy = positionVec4.xy * 2.0 - 1.0; 
 gl_Position = positionVec4; 
}
  • shader.frag
#version 300 es
precision mediump float;

uniform vec2 u_resolution;
out vec4 fragColor;

void main() {
    fragColor = vec4(vec3(0.8), 1.0);
}

根據這個程式的模板,你應該會看到一個灰色的畫布。

根據線條距離決定顏色

因為 glsl 是在像素層級上的運算,程式沒有辦法知道另一個像素的狀態,因此無法以一個畫布做為整體進行繪製。

假設要在畫布上繪製一個 f(x) = x^5 函數,比較簡單的方法是,計算目前片段(也就是目標像素點)的座標點距離 f(x) = x^5 這個函數有多近,來決定要繪製畫布背景色,還是線條的顏色:

#version 300 es
precision mediump float;

uniform vec2 u_resolution;
out vec4 fragColor;

void main() {
    vec2 st = gl_FragCoord.xy / u_resolution;

    float y = pow(st.x, 5.0);

    if (abs(st.y - y) < 0.004) {
        fragColor = vec4(vec3(0.0,1.0,0.0), 1.0);
    } else {
        fragColor = vec4(vec3(0.0), 1.0);
    }
}

我們判斷距離的方法很簡單,衡量目標像素點的 y 座標,和 f(x) = x^5 這個曲線的「垂直距離」,若超過 0.004,則渲染成背景的黑色,反之則是綠色,就能做出類似的效果了。

Imgur

但現在的問題是,這個線條看起來有些粗糙,上面會有一些畫素過低的顆粒感,解決方法是,不直接套用非黑即綠的粗暴繪製方法,而是根據距離摻入過渡的混合色。

這個方法必須引入之前在 p5.js 實戰演練(六) –– 煙霧動畫實作(一) 介紹到的 smoothstep 函數。

引入 smoothstep 函數

smoothstep 函數的數學表示法為

Imgur

在計算機圖學中,用來做平滑的過渡插值,我們要使用這個函數,來進行線條顏色(綠色)和背景色(黑色)的過渡插值。

若將該函數圖形化,會長這樣:

Imgur

在 glsl 中,我們不用自己列出算式來計算 smoothstep 的數值,glsl 已經有一個同名的函數給我們使用:

genType smoothstep(genType edge0, genType edge1, genType x);

所謂 genType 代表 smoothstep 支援多種型態,包括 floatvec2vec3vec4,但現在只會用到 float,所以:

float smoothstep(float edge0, float edge1, float x);

在數學上的 smoothstep 函數,是以 0 和 1 作為邊界,但 glsl 的 smoothstep 函數可以自己設定邊界,比如說 smoothstep(2, 4, x) 的函數圖像化:

Imgur

又或是反向的邊界 smoothstep(2, 1, x)

Imgur
通常我們會用這樣的組合函數 smooth(0.5, 1, x) - smooth(1, 1.5, x) 來柔化線條的顆粒感:

Imgur

越靠近中間點,線條主色就越強,越偏離中心點,背景色就會越強,所以用這樣的思維去更改程式:

#version 300 es
precision mediump float;

uniform vec2 u_resolution;
out vec4 fragColor;

float get_smooth_ratio(float width, float bias) {
    return smoothstep(-width, 0.0, bias) - smoothstep(0.0, width, bias);
}

void main() {
    vec2 st = gl_FragCoord.xy / u_resolution;

    float y = pow(st.x, 5.0);
    float smooth_ratio = get_smooth_ratio(0.01, y-st.y);
		
    vec3 c = smooth_ratio * vec3(0.0, 1.0, 0.0) +
             (1.0-smooth_ratio) * vec3(0.0);

    fragColor = vec4(c, 1.0);
}

Imgur

現在我們已經成功的柔化了線條邊界,看不到原先的顆粒感了,雖然還是有線條粗度的問題,主要是比對 y 座標的距離,而不是像素點針對該曲線的真正最短路徑,但因為這是一個很複雜的數學問題,無法在這系列的教學中完全解決,我們只能關注其他目前技術可以克服的部分。

繪製座標網格

掌握繪製線條的方法之後,我們接下來嘗試在畫布上繪製座標網格:

#version 300 es
precision mediump float;

uniform vec2 u_resolution;
uniform float u_grid_unit;
out vec4 fragColor;

float get_smooth_ratio(float width, float bias) {
    return smoothstep(-width, 0.0, bias) - smoothstep(0.0, width, bias);
}

void main() {
    vec2 st = gl_FragCoord.xy / u_resolution;
    vec3 c = vec3(0.0);		

    for (float i = 0.0; i < gl_FragCoord.x/u_resolution.x; i += u_grid_unit/u_resolution.x) { // 畫直線
        float x = i; // 畫出 x = i 的函數線 
				float smooth_ratio = get_smooth_ratio(0.005, x-st.x);
        c = smooth_ratio * vec3(0.2, 0.2, 0.2) +
            (1.0-smooth_ratio) * c;
    }

    for (float i = 0.0; i < gl_FragCoord.y/u_resolution.y; i += u_grid_unit/u_resolution.y) { // 畫直線
        float y = i; // 畫出 y = i 的函數線
				float smooth_ratio = get_smooth_ratio(0.005, y-st.y);
        c = smooth_ratio * vec3(0.2, 0.2, 0.2) +
            (1.0-smooth_ratio) * c;
    }

    float y = pow(st.x, 5.0); // 畫出 f(x) = x^5 的函數線
    float smooth_ratio = get_smooth_ratio(0.01, y-st.y);

    c = smooth_ratio * vec3(0.0, 1.0, 0.0) +
        (1.0-smooth_ratio) * c;

    fragColor = vec4(c, 1.0);
}

Imgur

在程式中我另外加了一個 uniform 變數 u_grid_unit,所以在 MySketch.js 我加入 rectShader.setUniform('u_grid_unit', 30); 代表一個座標格的長度單位為 30 pixel。

可以注意到直線和橫線分別用不一樣的式子(x-st.xy-st.y)來作為 get_smooth_ratiobias 參數,主因是參考點的不同,因為畫直線的時候是要參考目標像素點的 x 座標和 x = i 的距離,畫橫線的時候則是要參考目標像素點的 y 座標和 y = i 的距離,用文字可能還是難以描述,讀者需要自行領略其中差別 XD。

繪製圓形

接下來我們要再畫一個圓形,這個圓形的 bias 參考點不打算用 x 座標和 y 座標來計算,而是用極座標的 r,也就是說我們要以目標像素點和圓心的距離作為 bias 參考點。

#version 300 es
precision mediump float;

uniform vec2 u_resolution;
uniform float u_grid_unit;
out vec4 fragColor;

float get_smooth_ratio(float width, float bias) {
    return smoothstep(-width, 0.0, bias) - smoothstep(0.0, width, bias);
}

void main() {
    vec2 st = gl_FragCoord.xy / u_resolution;
    vec3 c = vec3(0.0);		

    for (float i = 0.0; i < gl_FragCoord.x/u_resolution.x; i += u_grid_unit/u_resolution.x) { // 畫直線
        float x = i; // 畫出 x = i 的函數線 
        float smooth_ratio = get_smooth_ratio(0.005, x-st.x);
        c = smooth_ratio * vec3(0.2, 0.2, 0.2) +
            (1.0-smooth_ratio) * c;
    }

    for (float i = 0.0; i < gl_FragCoord.y/u_resolution.y; i += u_grid_unit/u_resolution.y) { // 畫直線
        float y = i; // 畫出 y = i 的函數線
        float smooth_ratio = get_smooth_ratio(0.005, y-st.y);
        c = smooth_ratio * vec3(0.2, 0.2, 0.2) +
            (1.0-smooth_ratio) * c;
    }

    float smooth_ratio;
    vec2 center = vec2(0.5);
    float r = distance(center, st); // 畫出圓心為 (0.5, 0.5),半徑為 0.2 的圓
    smooth_ratio = get_smooth_ratio(0.005, 0.2-r);
    c = smooth_ratio * vec3(0.0, 1.0, 1.0) +
        (1.0-smooth_ratio) * c;

    float y = pow(st.x, 5.0); // 畫出 y = x^5 的函數線
    smooth_ratio = get_smooth_ratio(0.01, y-st.y);

    c = smooth_ratio * vec3(0.0, 1.0, 0.0) +
        (1.0-smooth_ratio) * c;

    fragColor = vec4(c, 1.0);
}

Imgur

參考資料

https://thebookofshaders.com/05/


上一篇
[Day 24] glsl 基礎教學(三) –– 著色器原理(二)
下一篇
[Day 26] glsl 基礎教學(五) –– 繪製發光線條和物體
系列文
p5.js 的環形藝術30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言